From 5a23545ed8bc284978e1d4ee4ed3dff67f95c124 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Sat, 23 May 2026 21:11:59 +0800 Subject: [PATCH 01/18] hwtier system enabled --- base/CMakeLists.txt | 1 + .../system/hardware_tier/hardware_tier.h | 156 +++++++++++++ .../system/hardware_tier/hardware_tier_data.h | 211 +++++++++++++++++ base/system/CMakeLists.txt | 3 +- base/system/hardware_tier/CMakeLists.txt | 34 +++ .../default/default_assessor.cpp | 42 ++++ .../default/default_collector.cpp | 77 +++++++ .../default/default_cpu_scorer.cpp | 62 +++++ .../default/default_display_scorer.cpp | 71 ++++++ .../default/default_gpu_scorer.cpp | 38 +++ .../default/default_memory_scorer.cpp | 40 ++++ .../hardware_tier/default/default_policy.cpp | 62 +++++ base/system/hardware_tier/default_factories.h | 34 +++ base/system/hardware_tier/hardware_tier.cpp | 216 ++++++++++++++++++ .../hardware_tier/hardware_tier_assessor.h | 41 ++++ .../hardware_tier/hardware_tier_collector.h | 40 ++++ .../hardware_tier/hardware_tier_policy.h | 41 ++++ .../hardware_tier/hardware_tier_scorer.h | 40 ++++ example/base/system/CMakeLists.txt | 6 + example/base/system/example_hardware_tier.cpp | 57 +++++ test/system/CMakeLists.txt | 8 + test/system/test_hardware_tier.cpp | 105 +++++++++ 22 files changed, 1384 insertions(+), 1 deletion(-) create mode 100644 base/include/system/hardware_tier/hardware_tier.h create mode 100644 base/include/system/hardware_tier/hardware_tier_data.h create mode 100644 base/system/hardware_tier/CMakeLists.txt create mode 100644 base/system/hardware_tier/default/default_assessor.cpp create mode 100644 base/system/hardware_tier/default/default_collector.cpp create mode 100644 base/system/hardware_tier/default/default_cpu_scorer.cpp create mode 100644 base/system/hardware_tier/default/default_display_scorer.cpp create mode 100644 base/system/hardware_tier/default/default_gpu_scorer.cpp create mode 100644 base/system/hardware_tier/default/default_memory_scorer.cpp create mode 100644 base/system/hardware_tier/default/default_policy.cpp create mode 100644 base/system/hardware_tier/default_factories.h create mode 100644 base/system/hardware_tier/hardware_tier.cpp create mode 100644 base/system/hardware_tier/hardware_tier_assessor.h create mode 100644 base/system/hardware_tier/hardware_tier_collector.h create mode 100644 base/system/hardware_tier/hardware_tier_policy.h create mode 100644 base/system/hardware_tier/hardware_tier_scorer.h create mode 100644 example/base/system/example_hardware_tier.cpp create mode 100644 test/system/test_hardware_tier.cpp diff --git a/base/CMakeLists.txt b/base/CMakeLists.txt index 0a0f55588..58a8485b4 100644 --- a/base/CMakeLists.txt +++ b/base/CMakeLists.txt @@ -27,6 +27,7 @@ set(CFBASE_MODULE_TARGETS cfbase_network cfbase_gpu cfbase_console + cfbase_hardware_tier ) # Link static libraries from sub-modules with --whole-archive to ensure diff --git a/base/include/system/hardware_tier/hardware_tier.h b/base/include/system/hardware_tier/hardware_tier.h new file mode 100644 index 000000000..1d052bb6e --- /dev/null +++ b/base/include/system/hardware_tier/hardware_tier.h @@ -0,0 +1,156 @@ +/** + * @file hardware_tier.h + * @brief Declares the hardware tier assessment public API. + * + * Provides functions for registering pluggable pipeline stages, + * configuring DeviceConfig overrides, and performing hardware + * tier assessments. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ +#pragma once + +#include "base/expected/expected.hpp" +#include "base/export.h" +#include "system/hardware_tier/hardware_tier_data.h" + +#include +#include + +namespace cf { + +class IHardwareCollector; +class IHardwareScorer; +class IHardwareAssessor; +class IHardwarePolicy; + +// ───────────────────────────────────────────────────────── +// Pipeline Registry +// ───────────────────────────────────────────────────────── + +/** + * @brief Registers a custom hardware data collector. + * + * Replaces the default collector. Must be called before the first + * assessHardware() invocation. + * + * @param[in] collector Unique pointer to the collector implementation. + * + * @return void (replaces the registered collector). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +CF_BASE_EXPORT void registerCollector(std::unique_ptr collector); + +/** + * @brief Registers a custom scorer for a specific dimension. + * + * @param[in] dimension One of "cpu", "gpu", "memory", "display". + * @param[in] scorer Unique pointer to the scorer implementation. + * + * @return void (replaces the registered scorer for the dimension). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +CF_BASE_EXPORT void registerScorer(std::string_view dimension, + std::unique_ptr scorer); + +/** + * @brief Registers a custom assessor implementation. + * + * @param[in] assessor Unique pointer to the assessor implementation. + * + * @return void (replaces the registered assessor). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +CF_BASE_EXPORT void registerAssessor(std::unique_ptr assessor); + +/** + * @brief Registers a custom policy implementation. + * + * @param[in] policy Unique pointer to the policy implementation. + * + * @return void (replaces the registered policy). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +CF_BASE_EXPORT void registerPolicy(std::unique_ptr policy); + +// ───────────────────────────────────────────────────────── +// DeviceConfig Override +// ───────────────────────────────────────────────────────── + +/** + * @brief Sets a manual tier override from DeviceConfig. + * + * When set, the pipeline short-circuits: collection and scoring are + * skipped, and the specified tier level is used directly. + * + * @param[in] level The tier level to enforce. + * @param[in] reason Optional human-readable reason string. + * + * @return void (activates the tier override). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +CF_BASE_EXPORT void setDeviceConfigOverride(HardwareTierLevel level, std::string reason = {}); + +/** + * @brief Clears any active DeviceConfig override. + * + * @return void (restores normal pipeline behavior). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +CF_BASE_EXPORT void clearDeviceConfigOverride(); + +// ───────────────────────────────────────────────────────── +// Assessment API +// ───────────────────────────────────────────────────────── + +/** + * @brief Performs a complete hardware tier assessment. + * + * Executes the full pipeline: Collect -> Score -> Assess -> Policy. + * If a DeviceConfig override is active, the pipeline short-circuits + * and returns the overridden tier. + * + * The result is cached after the first successful call. Pass + * force_refresh = true to re-run the pipeline. + * + * @param[in] force_refresh If true, forces re-running the pipeline. + * + * @return expected containing HardwareTierAssessment on success, + * or HardwareTierError on failure. + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +CF_BASE_EXPORT expected +assessHardware(bool force_refresh = false); + +/** + * @brief Queries capability flags from the last assessment. + * + * Must be called after a successful assessHardware(). + * + * @return expected containing HardwareTierCapabilities on success, + * or HardwareTierError if no assessment exists. + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +CF_BASE_EXPORT expected getHardwareTierCapabilities(); + +} // namespace cf diff --git a/base/include/system/hardware_tier/hardware_tier_data.h b/base/include/system/hardware_tier/hardware_tier_data.h new file mode 100644 index 000000000..706343016 --- /dev/null +++ b/base/include/system/hardware_tier/hardware_tier_data.h @@ -0,0 +1,211 @@ +/** + * @file hardware_tier_data.h + * @brief Defines data structures for hardware tier assessment. + * + * Provides the core types used by the hardware tier pipeline: tier levels, + * per-dimension scores, collected hardware data, assessment results, + * capability flags, and error codes. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ +#pragma once + +#include "base/export.h" +#include +#include + +namespace cf { + +// ───────────────────────────────────────────────────────── +// Hardware Tier Level +// ───────────────────────────────────────────────────────── + +/** + * @brief Hardware capability tier classification. + * + * Represents the overall capability tier of the device, derived from + * multi-dimensional scoring. + * + * Tier examples: + * - Low: i.MX6ULL (528MHz A7) -- no animation, linuxfb, soft decode + * - Mid: RK3568 (4xA55, Mali-G52) -- partial animation, optional eglfs + * - High: RK3588 (8xA76/A55, Mali-G610) -- full animation, eglfs+OpenGL + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +enum class HardwareTierLevel : uint8_t { + Unknown = 0, ///< Assessment could not be completed. + Low = 1, ///< Minimal capability embedded device. + Mid = 2, ///< Mid-range capability SoC. + High = 3 ///< High capability desktop/workstation grade. +}; + +/** + * @brief Converts HardwareTierLevel to a human-readable string. + * + * @param[in] level The tier level. + * @return "Unknown", "Low", "Mid", or "High". + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +CF_BASE_EXPORT const char* hardwareTierLevelToString(HardwareTierLevel level) noexcept; + +// ───────────────────────────────────────────────────────── +// Per-Dimension Score Types +// ───────────────────────────────────────────────────────── + +/** + * @brief CPU capability score (0-100). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +struct CpuScore { + int value = 0; ///< Raw score (0-100). +}; + +/** + * @brief GPU capability score (0-100). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +struct GpuScore { + int value = 0; ///< Raw score (0-100). +}; + +/** + * @brief Memory capability score (0-100). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +struct MemoryScore { + int value = 0; ///< Raw score (0-100). +}; + +/** + * @brief Display capability score (0-100). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +struct DisplayScore { + int value = 0; ///< Raw score (0-100). +}; + +// ───────────────────────────────────────────────────────── +// Collected Hardware Data (raw snapshot) +// ───────────────────────────────────────────────────────── + +/** + * @brief Raw collected hardware data used as scorer input. + * + * Aggregate of all probe results gathered by the collector stage. + * Owns all data independently so it outlives any probe cache. + * Fields that could not be collected are left at defaults. + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +struct HardwareData { + // CPU + std::string cpu_model; ///< CPU model name. + std::string cpu_arch; ///< CPU architecture string. + uint16_t logical_cores = 0; ///< Logical CPU thread count. + uint16_t physical_cores = 0; ///< Physical CPU core count. + uint32_t max_frequency = 0; ///< Max frequency in MHz. + bool has_big_little = false; ///< big.LITTLE architecture flag. + uint32_t big_core_count = 0; ///< Number of performance cores. + + // GPU + std::string gpu_name; ///< GPU device name. + uint32_t gpu_vendor_id = 0; ///< PCI vendor ID. + bool gpu_is_discrete = false; ///< Discrete GPU flag. + bool gpu_is_software = false; ///< Software rendering flag. + int existing_gpu_score = 0; ///< Existing EnvironmentScore.gpu value (0-50). + + // Display + int display_width = 0; ///< Display width in pixels. + int display_height = 0; ///< Display height in pixels. + double display_refresh = 0.0; ///< Refresh rate in Hz. + double display_dpi = 96.0; ///< DPI. + int existing_display_score = 0; ///< Existing EnvironmentScore.display value (0-50). + + // Memory + uint64_t total_physical_bytes = 0; ///< Total physical RAM. + uint64_t total_swap_bytes = 0; ///< Total swap space. +}; + +// ───────────────────────────────────────────────────────── +// Assessment Result +// ───────────────────────────────────────────────────────── + +/** + * @brief Complete hardware tier assessment result. + * + * Contains per-dimension scores, the overall tier level, and + * override metadata. + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +struct HardwareTierAssessment { + CpuScore cpu; ///< CPU capability score. + GpuScore gpu; ///< GPU capability score. + MemoryScore memory; ///< Memory capability score. + DisplayScore display; ///< Display capability score. + HardwareTierLevel tier = HardwareTierLevel::Unknown; ///< Overall classification. + + bool is_overridden = false; ///< True if DeviceConfig override is active. + std::string override_reason; ///< Override reason (when is_overridden). +}; + +// ───────────────────────────────────────────────────────── +// Capability Flags +// ───────────────────────────────────────────────────────── + +/** + * @brief Capability flags derived from the policy stage. + * + * Drives downstream decisions: rendering backend, animation policy, + * decoder selection, etc. + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +struct HardwareTierCapabilities { + bool use_opengl = false; ///< Use OpenGL/EGLFS rendering. + bool use_software_render = false; ///< Use software rendering / linuxfb. + bool enable_animation = false; ///< Enable full UI animations. + bool enable_partial_animation = false; ///< Enable limited animations. + bool use_hardware_decode = false; ///< Use hardware video decoding. + bool use_eglfs = false; ///< Use EGLFS platform plugin. + bool use_linuxfb = false; ///< Use linuxfb platform plugin. +}; + +// ───────────────────────────────────────────────────────── +// Error Codes +// ───────────────────────────────────────────────────────── + +/** + * @brief Error types for hardware tier assessment. + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +enum class HardwareTierError { + NoError, ///< Assessment completed successfully. + CollectionFailed, ///< Hardware data collection failed. + ScoringFailed, ///< Scoring stage produced no results. + AssessmentFailed, ///< Assessment stage failed. + PolicyFailed, ///< Policy derivation failed. +}; + +} // namespace cf diff --git a/base/system/CMakeLists.txt b/base/system/CMakeLists.txt index 590492259..effddf00e 100644 --- a/base/system/CMakeLists.txt +++ b/base/system/CMakeLists.txt @@ -1,4 +1,5 @@ add_subdirectory(cpu) add_subdirectory(gpu) add_subdirectory(memory) -add_subdirectory(network) \ No newline at end of file +add_subdirectory(network) +add_subdirectory(hardware_tier) \ No newline at end of file diff --git a/base/system/hardware_tier/CMakeLists.txt b/base/system/hardware_tier/CMakeLists.txt new file mode 100644 index 000000000..602ee0f9e --- /dev/null +++ b/base/system/hardware_tier/CMakeLists.txt @@ -0,0 +1,34 @@ +# Hardware Tier Assessment Module +cmake_minimum_required(VERSION 3.16) + +add_library(cfbase_hardware_tier STATIC) +set_target_properties(cfbase_hardware_tier PROPERTIES + POSITION_INDEPENDENT_CODE ON +) + +target_compile_definitions(cfbase_hardware_tier PRIVATE CFBASE_EXPORTS) + +target_include_directories(cfbase_hardware_tier + PUBLIC + $ + $ + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../.. +) + +target_sources(cfbase_hardware_tier PRIVATE + hardware_tier.cpp + default/default_collector.cpp + default/default_cpu_scorer.cpp + default/default_gpu_scorer.cpp + default/default_memory_scorer.cpp + default/default_display_scorer.cpp + default/default_assessor.cpp + default/default_policy.cpp +) + +target_link_libraries(cfbase_hardware_tier PRIVATE + cfbase_cpu + cfbase_gpu + cfbase_memory +) diff --git a/base/system/hardware_tier/default/default_assessor.cpp b/base/system/hardware_tier/default/default_assessor.cpp new file mode 100644 index 000000000..9ff25a204 --- /dev/null +++ b/base/system/hardware_tier/default/default_assessor.cpp @@ -0,0 +1,42 @@ +/** + * @file default_assessor.cpp + * @brief Default assessor implementation. + * + * Uses a bottleneck-weighted approach: the lowest dimension caps the + * tier while the average provides a baseline. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "../default_factories.h" + +#include + +namespace cf { + +class DefaultAssessor : public IHardwareAssessor { + public: + HardwareTierLevel assess(int cpu, int gpu, int memory, int display) override { + int minScore = std::min({cpu, gpu, memory, display}); + int avg = (cpu + gpu + memory + display) / 4; + + // 60% bottleneck weight, 40% average + int effective = static_cast(minScore * 0.6 + avg * 0.4); + + if (effective >= 65) + return HardwareTierLevel::High; + if (effective >= 30) + return HardwareTierLevel::Mid; + return HardwareTierLevel::Low; + } +}; + +std::unique_ptr makeDefaultAssessor() { + return std::make_unique(); +} + +} // namespace cf diff --git a/base/system/hardware_tier/default/default_collector.cpp b/base/system/hardware_tier/default/default_collector.cpp new file mode 100644 index 000000000..4088dfb46 --- /dev/null +++ b/base/system/hardware_tier/default/default_collector.cpp @@ -0,0 +1,77 @@ +/** + * @file default_collector.cpp + * @brief Default hardware data collector implementation. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "../default_factories.h" + +#include "system/cpu/cfcpu.h" +#include "system/cpu/cfcpu_bonus.h" +#include "system/cpu/cfcpu_profile.h" +#include "system/gpu/gpu.h" +#include "system/memory/memory_info.h" + +namespace cf { + +class DefaultCollector : public IHardwareCollector { + public: + HardwareData collect() override { + HardwareData data; + collectCpu(data); + collectGpu(data); + collectMemory(data); + return data; + } + + private: + void collectCpu(HardwareData& data) { + if (auto info = getCPUInfo(); info.has_value()) { + data.cpu_model = std::string(info->model); + data.cpu_arch = std::string(info->arch); + } + if (auto profile = getCPUProfileInfo(); profile.has_value()) { + data.logical_cores = profile->logical_cnt; + data.physical_cores = profile->physical_cnt; + data.max_frequency = profile->max_frequency; + } + if (auto bonus = getCPUBonusInfo(); bonus.has_value()) { + data.has_big_little = bonus->has_big_little; + data.big_core_count = bonus->big_core_count; + } + } + + void collectGpu(HardwareData& data) { + if (auto gd = getGpuDisplayInfo(); gd.has_value()) { + data.gpu_name = gd->gpu.name; + data.gpu_vendor_id = gd->gpu.vendorId; + data.gpu_is_discrete = gd->gpu.isDiscrete; + data.gpu_is_software = gd->gpu.hasSoftware; + data.existing_gpu_score = gd->score.gpu; + + data.display_width = gd->display.width; + data.display_height = gd->display.height; + data.display_refresh = gd->display.refreshRate; + data.display_dpi = gd->display.dpi; + data.existing_display_score = gd->score.display; + } + } + + void collectMemory(HardwareData& data) { + MemoryInfo mem; + getSystemMemoryInfo(mem); + data.total_physical_bytes = mem.physical.total_bytes; + data.total_swap_bytes = mem.swap.total_bytes; + } +}; + +std::unique_ptr makeDefaultCollector() { + return std::make_unique(); +} + +} // namespace cf diff --git a/base/system/hardware_tier/default/default_cpu_scorer.cpp b/base/system/hardware_tier/default/default_cpu_scorer.cpp new file mode 100644 index 000000000..8e5883e70 --- /dev/null +++ b/base/system/hardware_tier/default/default_cpu_scorer.cpp @@ -0,0 +1,62 @@ +/** + * @file default_cpu_scorer.cpp + * @brief Default CPU dimension scorer implementation. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "../default_factories.h" + +#include + +namespace cf { + +class DefaultCpuScorer : public IHardwareScorer { + public: + int score(const HardwareData& data) override { + int s = 0; + + // Core count (max 40 points) + if (data.logical_cores >= 8) + s += 40; + else if (data.logical_cores >= 4) + s += 25; + else if (data.logical_cores >= 2) + s += 15; + else + s += 5; + + // Frequency (max 35 points) + if (data.max_frequency >= 2400) + s += 35; + else if (data.max_frequency >= 1800) + s += 25; + else if (data.max_frequency >= 1000) + s += 15; + else + s += 5; + + // Architecture bonus (max 25 points) + if (data.has_big_little && data.big_core_count >= 4) + s += 25; + else if (data.has_big_little) + s += 15; + else if (data.cpu_arch.find("aarch64") != std::string::npos || + data.cpu_arch.find("x86_64") != std::string::npos) + s += 20; + else + s += 5; + + return std::clamp(s, 0, 100); + } +}; + +std::unique_ptr makeDefaultCpuScorer() { + return std::make_unique(); +} + +} // namespace cf diff --git a/base/system/hardware_tier/default/default_display_scorer.cpp b/base/system/hardware_tier/default/default_display_scorer.cpp new file mode 100644 index 000000000..a41648117 --- /dev/null +++ b/base/system/hardware_tier/default/default_display_scorer.cpp @@ -0,0 +1,71 @@ +/** + * @file default_display_scorer.cpp + * @brief Default display dimension scorer implementation. + * + * Adapts the existing EnvironmentScore.display (0-50) to 0-100 range. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "../default_factories.h" + +#include + +namespace cf { + +class DefaultDisplayScorer : public IHardwareScorer { + public: + int score(const HardwareData& data) override { + if (data.existing_display_score > 0) { + return std::min(data.existing_display_score * 2, 100); + } + + // Fallback: compute from raw display data + auto pixels = static_cast(data.display_width) * data.display_height; + int s = 0; + + // Resolution (max 50 points) + if (pixels >= 3840LL * 2160) + s += 50; + else if (pixels >= 2560LL * 1440) + s += 40; + else if (pixels >= 1920LL * 1080) + s += 30; + else if (pixels >= 1280LL * 720) + s += 15; + else + s += 5; + + // Refresh rate (max 30 points) + if (data.display_refresh >= 120) + s += 30; + else if (data.display_refresh >= 90) + s += 25; + else if (data.display_refresh >= 60) + s += 20; + else + s += 5; + + // DPI (max 20 points) + if (data.display_dpi >= 200) + s += 20; + else if (data.display_dpi >= 150) + s += 15; + else if (data.display_dpi >= 96) + s += 10; + else + s += 3; + + return std::min(s, 100); + } +}; + +std::unique_ptr makeDefaultDisplayScorer() { + return std::make_unique(); +} + +} // namespace cf diff --git a/base/system/hardware_tier/default/default_gpu_scorer.cpp b/base/system/hardware_tier/default/default_gpu_scorer.cpp new file mode 100644 index 000000000..410f6892e --- /dev/null +++ b/base/system/hardware_tier/default/default_gpu_scorer.cpp @@ -0,0 +1,38 @@ +/** + * @file default_gpu_scorer.cpp + * @brief Default GPU dimension scorer implementation. + * + * Adapts the existing EnvironmentScore.gpu (0-50) to 0-100 range. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "../default_factories.h" + +#include + +namespace cf { + +class DefaultGpuScorer : public IHardwareScorer { + public: + int score(const HardwareData& data) override { + if (data.gpu_is_software) + return 5; + if (data.gpu_vendor_id == 0) + return 5; + + // Normalize existing 0-50 score to 0-100 + int base = data.existing_gpu_score * 2; + return std::min(base, 100); + } +}; + +std::unique_ptr makeDefaultGpuScorer() { + return std::make_unique(); +} + +} // namespace cf diff --git a/base/system/hardware_tier/default/default_memory_scorer.cpp b/base/system/hardware_tier/default/default_memory_scorer.cpp new file mode 100644 index 000000000..85e2bf1a1 --- /dev/null +++ b/base/system/hardware_tier/default/default_memory_scorer.cpp @@ -0,0 +1,40 @@ +/** + * @file default_memory_scorer.cpp + * @brief Default memory dimension scorer implementation. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "../default_factories.h" + +#include + +namespace cf { + +class DefaultMemoryScorer : public IHardwareScorer { + public: + int score(const HardwareData& data) override { + auto gb = static_cast(data.total_physical_bytes / (1024ULL * 1024 * 1024)); + if (gb >= 16) + return 100; + if (gb >= 8) + return 75; + if (gb >= 4) + return 55; + if (gb >= 2) + return 35; + if (gb >= 1) + return 20; + return 5; + } +}; + +std::unique_ptr makeDefaultMemoryScorer() { + return std::make_unique(); +} + +} // namespace cf diff --git a/base/system/hardware_tier/default/default_policy.cpp b/base/system/hardware_tier/default/default_policy.cpp new file mode 100644 index 000000000..2439be2a5 --- /dev/null +++ b/base/system/hardware_tier/default/default_policy.cpp @@ -0,0 +1,62 @@ +/** + * @file default_policy.cpp + * @brief Default policy implementation. + * + * Maps HardwareTierLevel to HardwareTierCapabilities based on the + * design document tier definitions. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "../default_factories.h" + +namespace cf { + +class DefaultPolicy : public IHardwarePolicy { + public: + HardwareTierCapabilities deriveCapabilities(HardwareTierLevel level, + const HardwareTierAssessment& assessment) override { + HardwareTierCapabilities caps; + + switch (level) { + case HardwareTierLevel::High: + caps.use_opengl = true; + caps.enable_animation = true; + caps.use_hardware_decode = true; + caps.use_eglfs = true; + break; + + case HardwareTierLevel::Mid: + caps.use_opengl = true; + caps.enable_partial_animation = true; + caps.use_hardware_decode = true; + caps.use_eglfs = (assessment.gpu.value >= 40); + caps.use_linuxfb = !caps.use_eglfs; + break; + + case HardwareTierLevel::Low: + caps.use_software_render = true; + caps.use_hardware_decode = false; + caps.use_linuxfb = true; + break; + + case HardwareTierLevel::Unknown: + default: + caps.use_software_render = true; + caps.use_linuxfb = true; + break; + } + + return caps; + } +}; + +std::unique_ptr makeDefaultPolicy() { + return std::make_unique(); +} + +} // namespace cf diff --git a/base/system/hardware_tier/default_factories.h b/base/system/hardware_tier/default_factories.h new file mode 100644 index 000000000..83faab929 --- /dev/null +++ b/base/system/hardware_tier/default_factories.h @@ -0,0 +1,34 @@ +/** + * @file default_factories.h + * @brief Declares factory functions for default pipeline implementations. + * + * Each default implementation .cpp file provides a factory function + * that creates the appropriate unique_ptr, avoiding the need for + * full class definitions in the pipeline executor. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ +#pragma once + +#include "hardware_tier_assessor.h" +#include "hardware_tier_collector.h" +#include "hardware_tier_policy.h" +#include "hardware_tier_scorer.h" + +#include + +namespace cf { + +std::unique_ptr makeDefaultCollector(); +std::unique_ptr makeDefaultCpuScorer(); +std::unique_ptr makeDefaultGpuScorer(); +std::unique_ptr makeDefaultMemoryScorer(); +std::unique_ptr makeDefaultDisplayScorer(); +std::unique_ptr makeDefaultAssessor(); +std::unique_ptr makeDefaultPolicy(); + +} // namespace cf diff --git a/base/system/hardware_tier/hardware_tier.cpp b/base/system/hardware_tier/hardware_tier.cpp new file mode 100644 index 000000000..beee9dc0a --- /dev/null +++ b/base/system/hardware_tier/hardware_tier.cpp @@ -0,0 +1,216 @@ +/** + * @file hardware_tier.cpp + * @brief Implements the hardware tier assessment pipeline. + * + * Contains the registry, caching logic, and pipeline orchestration + * for the Collect -> Score -> Assess -> Policy workflow. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "system/hardware_tier/hardware_tier.h" + +#include "hardware_tier_assessor.h" +#include "hardware_tier_collector.h" +#include "hardware_tier_policy.h" +#include "hardware_tier_scorer.h" + +#include "default_factories.h" + +#include +#include +#include +#include +#include + +namespace cf { + +// ───────────────────────────────────────────────────────── +// Pipeline Registry +// ───────────────────────────────────────────────────────── +struct HardwareTierRegistry { + std::unique_ptr collector; + std::unique_ptr cpu_scorer; + std::unique_ptr gpu_scorer; + std::unique_ptr memory_scorer; + std::unique_ptr display_scorer; + std::unique_ptr assessor; + std::unique_ptr policy; + + std::optional override_level; + std::string override_reason; + + HardwareTierRegistry(); +}; + +static HardwareTierRegistry& getRegistry() { + static HardwareTierRegistry reg; + return reg; +} + +// ───────────────────────────────────────────────────────── +// Cached Assessment State +// ───────────────────────────────────────────────────────── +struct CachedAssessment { + HardwareTierAssessment assessment; + HardwareTierCapabilities capabilities; + bool valid = false; +}; + +static CachedAssessment g_cached; +static std::mutex g_cache_mutex; + +// ───────────────────────────────────────────────────────── +// Public API +// ───────────────────────────────────────────────────────── + +const char* hardwareTierLevelToString(HardwareTierLevel level) noexcept { + switch (level) { + case HardwareTierLevel::Low: + return "Low"; + case HardwareTierLevel::Mid: + return "Mid"; + case HardwareTierLevel::High: + return "High"; + default: + return "Unknown"; + } +} + +void invalidateCache() { + std::lock_guard lock(g_cache_mutex); + g_cached.valid = false; +} + +void registerCollector(std::unique_ptr collector) { + getRegistry().collector = std::move(collector); + invalidateCache(); +} + +void registerScorer(std::string_view dimension, std::unique_ptr scorer) { + auto& reg = getRegistry(); + if (dimension == "cpu") + reg.cpu_scorer = std::move(scorer); + else if (dimension == "gpu") + reg.gpu_scorer = std::move(scorer); + else if (dimension == "memory") + reg.memory_scorer = std::move(scorer); + else if (dimension == "display") + reg.display_scorer = std::move(scorer); + invalidateCache(); +} + +void registerAssessor(std::unique_ptr assessor) { + getRegistry().assessor = std::move(assessor); + invalidateCache(); +} + +void registerPolicy(std::unique_ptr policy) { + getRegistry().policy = std::move(policy); + invalidateCache(); +} + +void setDeviceConfigOverride(HardwareTierLevel level, std::string reason) { + auto& reg = getRegistry(); + reg.override_level = level; + reg.override_reason = std::move(reason); + invalidateCache(); +} + +void clearDeviceConfigOverride() { + auto& reg = getRegistry(); + reg.override_level.reset(); + reg.override_reason.clear(); + invalidateCache(); +} + +expected assessHardware(bool force_refresh) { + if (force_refresh) { + invalidateCache(); + } + + { + std::lock_guard lock(g_cache_mutex); + if (g_cached.valid) { + return g_cached.assessment; + } + } + + auto& reg = getRegistry(); + HardwareTierAssessment result; + + // ── DeviceConfig override short-circuit ── + if (reg.override_level.has_value()) { + result.tier = *reg.override_level; + result.is_overridden = true; + result.override_reason = reg.override_reason; + + HardwareTierCapabilities caps; + if (reg.policy) { + caps = reg.policy->deriveCapabilities(result.tier, result); + } + std::lock_guard lock(g_cache_mutex); + g_cached.assessment = result; + g_cached.capabilities = caps; + g_cached.valid = true; + return result; + } + + // ── Stage 1: Collect ── + if (!reg.collector) { + return cf::unexpected(HardwareTierError::CollectionFailed); + } + HardwareData data = reg.collector->collect(); + + // ── Stage 2: Score ── + if (!reg.cpu_scorer || !reg.gpu_scorer || !reg.memory_scorer || !reg.display_scorer) { + return cf::unexpected(HardwareTierError::ScoringFailed); + } + + result.cpu.value = reg.cpu_scorer->score(data); + result.gpu.value = reg.gpu_scorer->score(data); + result.memory.value = reg.memory_scorer->score(data); + result.display.value = reg.display_scorer->score(data); + + // ── Stage 3: Assess ── + if (!reg.assessor) { + return cf::unexpected(HardwareTierError::AssessmentFailed); + } + result.tier = reg.assessor->assess(result.cpu.value, result.gpu.value, result.memory.value, + result.display.value); + + // ── Stage 4: Policy ── + HardwareTierCapabilities caps; + if (reg.policy) { + caps = reg.policy->deriveCapabilities(result.tier, result); + } + + std::lock_guard lock(g_cache_mutex); + g_cached.assessment = result; + g_cached.capabilities = caps; + g_cached.valid = true; + return result; +} + +expected getHardwareTierCapabilities() { + std::lock_guard lock(g_cache_mutex); + if (!g_cached.valid) { + return cf::unexpected(HardwareTierError::PolicyFailed); + } + return g_cached.capabilities; +} + +// ───────────────────────────────────────────────────────── +// Registry Constructor — installs all defaults +// ───────────────────────────────────────────────────────── +HardwareTierRegistry::HardwareTierRegistry() + : collector(makeDefaultCollector()), cpu_scorer(makeDefaultCpuScorer()), + gpu_scorer(makeDefaultGpuScorer()), memory_scorer(makeDefaultMemoryScorer()), + display_scorer(makeDefaultDisplayScorer()), assessor(makeDefaultAssessor()), + policy(makeDefaultPolicy()) {} + +} // namespace cf diff --git a/base/system/hardware_tier/hardware_tier_assessor.h b/base/system/hardware_tier/hardware_tier_assessor.h new file mode 100644 index 000000000..7b2b63ef3 --- /dev/null +++ b/base/system/hardware_tier/hardware_tier_assessor.h @@ -0,0 +1,41 @@ +/** + * @file hardware_tier_assessor.h + * @brief Declares the IHardwareAssessor interface. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ +#pragma once + +#include "system/hardware_tier/hardware_tier_data.h" + +namespace cf { + +/** + * @brief Interface for the assessment stage. + * + * Maps multi-dimensional scores to an overall HardwareTierLevel. + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +class IHardwareAssessor { + public: + virtual ~IHardwareAssessor() = default; + + /** + * @brief Determines the overall tier level from per-dimension scores. + * + * @param[in] cpu CPU dimension score. + * @param[in] gpu GPU dimension score. + * @param[in] memory Memory dimension score. + * @param[in] display Display dimension score. + * @return HardwareTierLevel classification. + */ + virtual HardwareTierLevel assess(int cpu, int gpu, int memory, int display) = 0; +}; + +} // namespace cf diff --git a/base/system/hardware_tier/hardware_tier_collector.h b/base/system/hardware_tier/hardware_tier_collector.h new file mode 100644 index 000000000..546813858 --- /dev/null +++ b/base/system/hardware_tier/hardware_tier_collector.h @@ -0,0 +1,40 @@ +/** + * @file hardware_tier_collector.h + * @brief Declares the IHardwareCollector interface. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ +#pragma once + +#include "system/hardware_tier/hardware_tier_data.h" + +namespace cf { + +/** + * @brief Interface for the hardware data collection stage. + * + * Implementations gather raw hardware data from platform probes. + * The default implementation calls the existing cf::getCPUInfo(), + * cf::getGpuDisplayInfo(), cf::getSystemMemoryInfo() probes. + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +class IHardwareCollector { + public: + virtual ~IHardwareCollector() = default; + + /** + * @brief Collects raw hardware data from all subsystems. + * + * @return HardwareData populated with probe results. + * Fields that could not be collected are left at defaults. + */ + virtual HardwareData collect() = 0; +}; + +} // namespace cf diff --git a/base/system/hardware_tier/hardware_tier_policy.h b/base/system/hardware_tier/hardware_tier_policy.h new file mode 100644 index 000000000..9abcb146a --- /dev/null +++ b/base/system/hardware_tier/hardware_tier_policy.h @@ -0,0 +1,41 @@ +/** + * @file hardware_tier_policy.h + * @brief Declares the IHardwarePolicy interface. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ +#pragma once + +#include "system/hardware_tier/hardware_tier_data.h" + +namespace cf { + +/** + * @brief Interface for the policy stage. + * + * Maps an HardwareTierLevel to concrete capability flags that drive + * downstream behavior (rendering backend, animations, etc.). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +class IHardwarePolicy { + public: + virtual ~IHardwarePolicy() = default; + + /** + * @brief Derives capability flags from the tier level. + * + * @param[in] level The assessed tier level. + * @param[in] assessment The full assessment (for dimension-aware policy). + * @return HardwareTierCapabilities flags. + */ + virtual HardwareTierCapabilities + deriveCapabilities(HardwareTierLevel level, const HardwareTierAssessment& assessment) = 0; +}; + +} // namespace cf diff --git a/base/system/hardware_tier/hardware_tier_scorer.h b/base/system/hardware_tier/hardware_tier_scorer.h new file mode 100644 index 000000000..1a74eec4c --- /dev/null +++ b/base/system/hardware_tier/hardware_tier_scorer.h @@ -0,0 +1,40 @@ +/** + * @file hardware_tier_scorer.h + * @brief Declares the IHardwareScorer interface. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ +#pragma once + +#include "system/hardware_tier/hardware_tier_data.h" + +namespace cf { + +/** + * @brief Interface for the per-dimension scoring stage. + * + * Each dimension (cpu, gpu, memory, display) gets its own scorer + * registered via registerScorer(). Scorers map raw collected data + * to a 0-100 score. + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +class IHardwareScorer { + public: + virtual ~IHardwareScorer() = default; + + /** + * @brief Scores a specific dimension from the collected data. + * + * @param[in] data The collected hardware data. + * @return Integer score in the range [0, 100]. + */ + virtual int score(const HardwareData& data) = 0; +}; + +} // namespace cf diff --git a/example/base/system/CMakeLists.txt b/example/base/system/CMakeLists.txt index 281b29f24..8a55f867f 100644 --- a/example/base/system/CMakeLists.txt +++ b/example/base/system/CMakeLists.txt @@ -25,3 +25,9 @@ add_executable(example_gpu_info example_gpu_info.cpp) target_link_libraries(example_gpu_info PRIVATE cfbase) cf_set_example_output_dir(example_gpu_info "base") cf_register_example_launcher(example_gpu_info "base") + +# Hardware Tier Assessment +add_executable(example_hardware_tier example_hardware_tier.cpp) +target_link_libraries(example_hardware_tier PRIVATE cfbase) +cf_set_example_output_dir(example_hardware_tier "base") +cf_register_example_launcher(example_hardware_tier "base") diff --git a/example/base/system/example_hardware_tier.cpp b/example/base/system/example_hardware_tier.cpp new file mode 100644 index 000000000..2533c46a0 --- /dev/null +++ b/example/base/system/example_hardware_tier.cpp @@ -0,0 +1,57 @@ +/** + * @file example_hardware_tier.cpp + * @brief Demonstrates hardware tier assessment on the current machine. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "system/hardware_tier/hardware_tier.h" + +#include + +using namespace cf; + +int main() { + auto assessment = assessHardware(); + if (!assessment.has_value()) { + std::cerr << "Failed to assess hardware tier, error: " + << static_cast(assessment.error()) << "\n"; + return 1; + } + + const auto& a = *assessment; + + std::cout << "========== Hardware Tier Assessment ==========\n"; + std::cout << "CPU Score: " << a.cpu.value << " / 100\n"; + std::cout << "GPU Score: " << a.gpu.value << " / 100\n"; + std::cout << "Memory Score: " << a.memory.value << " / 100\n"; + std::cout << "Display Score: " << a.display.value << " / 100\n"; + std::cout << "\n"; + std::cout << "Overall Tier: " << hardwareTierLevelToString(a.tier) << "\n"; + + if (a.is_overridden) { + std::cout << " (Overridden: " << a.override_reason << ")\n"; + } + + auto caps = getHardwareTierCapabilities(); + if (caps.has_value()) { + const auto& c = *caps; + std::cout << "\n========== Capability Flags ==========\n"; + std::cout << "OpenGL: " << (c.use_opengl ? "Yes" : "No") << "\n"; + std::cout << "Software Render: " << (c.use_software_render ? "Yes" : "No") << "\n"; + std::cout << "Animation: " + << (c.enable_animation ? "Full" + : c.enable_partial_animation ? "Partial" + : "None") + << "\n"; + std::cout << "Hardware Decode: " << (c.use_hardware_decode ? "Yes" : "No") << "\n"; + std::cout << "EGLFS: " << (c.use_eglfs ? "Yes" : "No") << "\n"; + std::cout << "LinuxFB: " << (c.use_linuxfb ? "Yes" : "No") << "\n"; + } + + return 0; +} diff --git a/test/system/CMakeLists.txt b/test/system/CMakeLists.txt index d8b4b9e07..06c2f2542 100644 --- a/test/system/CMakeLists.txt +++ b/test/system/CMakeLists.txt @@ -20,3 +20,11 @@ add_gtest_executable( LABELS "base;system" LOG_MODULE base_system ) + +add_gtest_executable( + TEST_NAME test_hardware_tier + SOURCE_FILE test_hardware_tier.cpp + LINK_LIBRARIES cfbase;GTest::gtest;GTest::gtest_main + LABELS "base;system" + LOG_MODULE base_system +) diff --git a/test/system/test_hardware_tier.cpp b/test/system/test_hardware_tier.cpp new file mode 100644 index 000000000..2b610a5c3 --- /dev/null +++ b/test/system/test_hardware_tier.cpp @@ -0,0 +1,105 @@ +/** + * @file test_hardware_tier.cpp + * @brief Unit tests for the hardware tier assessment pipeline. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "system/hardware_tier/hardware_tier.h" + +#include + +using namespace cf; + +// ── Basic Assessment ──────────────────────────────────── + +TEST(HardwareTierTest, AssessReturnsValidResult) { + auto result = assessHardware(true); + ASSERT_TRUE(result.has_value()); + + const auto& a = *result; + EXPECT_GE(a.cpu.value, 0); + EXPECT_LE(a.cpu.value, 100); + EXPECT_GE(a.gpu.value, 0); + EXPECT_LE(a.gpu.value, 100); + EXPECT_GE(a.memory.value, 0); + EXPECT_LE(a.memory.value, 100); + EXPECT_GE(a.display.value, 0); + EXPECT_LE(a.display.value, 100); + + EXPECT_NE(a.tier, HardwareTierLevel::Unknown); +} + +// ── Tier Level Strings ────────────────────────────────── + +TEST(HardwareTierTest, TierLevelStrings) { + EXPECT_STREQ(hardwareTierLevelToString(HardwareTierLevel::Low), "Low"); + EXPECT_STREQ(hardwareTierLevelToString(HardwareTierLevel::Mid), "Mid"); + EXPECT_STREQ(hardwareTierLevelToString(HardwareTierLevel::High), "High"); + EXPECT_STREQ(hardwareTierLevelToString(HardwareTierLevel::Unknown), "Unknown"); +} + +// ── Capabilities Accessible ───────────────────────────── + +TEST(HardwareTierTest, CapabilitiesAccessible) { + assessHardware(true); + + auto caps = getHardwareTierCapabilities(); + ASSERT_TRUE(caps.has_value()); + EXPECT_TRUE(caps->use_opengl || caps->use_software_render || caps->use_eglfs || + caps->use_linuxfb); +} + +// ── DeviceConfig Override ─────────────────────────────── + +TEST(HardwareTierTest, DeviceConfigOverride) { + setDeviceConfigOverride(HardwareTierLevel::Mid, "test override"); + + auto result = assessHardware(true); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->tier, HardwareTierLevel::Mid); + EXPECT_TRUE(result->is_overridden); + EXPECT_EQ(result->override_reason, "test override"); + + clearDeviceConfigOverride(); + assessHardware(true); +} + +// ── Caching Behavior ──────────────────────────────────── + +TEST(HardwareTierTest, CachingReturnsSameResult) { + auto first = assessHardware(); + ASSERT_TRUE(first.has_value()); + + auto second = assessHardware(); + ASSERT_TRUE(second.has_value()); + + EXPECT_EQ(first->cpu.value, second->cpu.value); + EXPECT_EQ(first->tier, second->tier); +} + +// ── Force Refresh ─────────────────────────────────────── + +TEST(HardwareTierTest, ForceRefreshProducesResult) { + auto result = assessHardware(true); + ASSERT_TRUE(result.has_value()); + EXPECT_NE(result->tier, HardwareTierLevel::Unknown); +} + +// ── Override Clear Restores Normal Assessment ─────────── + +TEST(HardwareTierTest, OverrideClearRestoresNormal) { + setDeviceConfigOverride(HardwareTierLevel::Low, "forced low"); + auto overridden = assessHardware(true); + ASSERT_TRUE(overridden.has_value()); + EXPECT_EQ(overridden->tier, HardwareTierLevel::Low); + + clearDeviceConfigOverride(); + auto normal = assessHardware(true); + ASSERT_TRUE(normal.has_value()); + EXPECT_FALSE(normal->is_overridden); +} From 7ccb63cdaf3004751d2d95706353af0c1e0635d9 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Sat, 23 May 2026 22:00:59 +0800 Subject: [PATCH 02/18] refactor: refactor the ui subsystem --- CMakeLists.txt | 2 +- README.md | 2 +- .../include/cfconfig/cfconfig_path_provider.h | 10 +- .../src/cfconfig_path_provider.cpp | 8 +- .../base/config_manager/01-quick-start.md | 2 +- .../desktop/base/config_manager/03-faq.md | 24 +- .../base/config_manager/04-architecture.md | 4 +- .../desktop/base/config_manager/README.md | 2 +- .../design_stage/01_phase1_hardware_probe.md | 13 +- .../design_stage/02_phase2_base_library.md | 4 +- document/todo/desktop/07_render_backend.md | 4 +- .../todo/done/01_hardware_probe_status.md | 299 ++--- document/todo/done/02_base_library_status.md | 2 +- .../todo/done/06_infrastructure_status.md | 2 +- document/todo/done/PROJECT_STATUS_REPORT.md | 21 +- test/ui/core/CMakeLists.txt | 13 +- test/ui/core/token_test.cpp | 579 --------- ui/core/CMakeLists.txt | 2 +- ui/core/material/cfmaterial_fonttype.cpp | 200 +-- ui/core/material/cfmaterial_fonttype.h | 235 +--- ui/core/material/cfmaterial_motion.cpp | 66 +- ui/core/material/cfmaterial_motion.h | 123 +- ui/core/material/cfmaterial_radius_scale.cpp | 41 +- ui/core/material/cfmaterial_radius_scale.h | 89 +- ui/core/material/cfmaterial_scheme.cpp | 32 +- ui/core/material/cfmaterial_scheme.h | 211 +--- ui/core/material/material_factory.cpp | 241 ++-- ui/core/material/material_factory.hpp | 1 + ui/core/token.hpp | 1018 ---------------- ui/widget/CMakeLists.txt | 1 + .../material/base/material_widget_base.cpp | 95 ++ .../material/base/material_widget_base.h | 214 ++++ ui/widget/material/base/state_machine.cpp | 145 +-- ui/widget/material/base/state_machine.h | 17 + ui/widget/material/widget/button/button.cpp | 140 +-- ui/widget/material/widget/button/button.h | 17 +- .../material/widget/checkbox/checkbox.cpp | 179 +-- ui/widget/material/widget/checkbox/checkbox.h | 691 ++++++----- .../material/widget/comboBox/combobox.cpp | 112 +- ui/widget/material/widget/comboBox/combobox.h | 15 +- .../widget/doublespinbox/doublespinbox.cpp | 89 +- .../widget/doublespinbox/doublespinbox.h | 15 +- .../material/widget/listview/listview.cpp | 100 +- ui/widget/material/widget/listview/listview.h | 15 +- .../widget/progressbar/progressbar.cpp | 69 +- .../material/widget/progressbar/progressbar.h | 13 +- .../widget/radiobutton/radiobutton.cpp | 186 +-- .../material/widget/radiobutton/radiobutton.h | 691 ++++++----- ui/widget/material/widget/slider/slider.cpp | 212 +--- ui/widget/material/widget/slider/slider.h | 17 +- ui/widget/material/widget/spinbox/spinbox.cpp | 90 +- ui/widget/material/widget/spinbox/spinbox.h | 15 +- ui/widget/material/widget/switch/switch.cpp | 188 +-- ui/widget/material/widget/switch/switch.h | 17 +- .../material/widget/tableview/tableview.cpp | 109 +- .../material/widget/tableview/tableview.h | 15 +- .../widget/tabview/private/materialtabbar.cpp | 61 +- .../widget/tabview/private/materialtabbar.h | 12 +- .../material/widget/textarea/textarea.cpp | 86 +- ui/widget/material/widget/textarea/textarea.h | 15 +- .../material/widget/textfield/textfield.cpp | 93 +- .../material/widget/textfield/textfield.h | 1068 ++++++++--------- .../material/widget/treeview/treeview.cpp | 107 +- ui/widget/material/widget/treeview/treeview.h | 15 +- 64 files changed, 2474 insertions(+), 5700 deletions(-) delete mode 100644 test/ui/core/token_test.cpp delete mode 100644 ui/core/token.hpp create mode 100644 ui/widget/material/base/material_widget_base.cpp create mode 100644 ui/widget/material/base/material_widget_base.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0af766211..bae2d248e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,7 @@ message("Including Requested CMake Dependencies OK") include(cmake/check_toolchain.cmake) project(CFDesktop - VERSION 0.18.0 + VERSION 0.19.0 DESCRIPTION "CFDesktop: Given Your Device Portable Desktop Anywhere" HOMEPAGE_URL "https://github.com/Awesome-Embedded-Learning-Studio/CFDesktop" LANGUAGES CXX C diff --git a/README.md b/README.md index d701455f5..2ba6ae1c9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ### 为嵌入式设备打造的现代化 Material Design 3 桌面框架 - [![License: MIT][license-badge]] [![Version: 0.18.0][version-badge]] + [![License: MIT][license-badge]] [![Version: 0.19.0][version-badge]] [![C++23][cpp-badge]] [![Qt 6.8][qt-badge]] [![CMake][cmake-badge]] [![Documentation][docs-badge]] diff --git a/desktop/base/config_manager/include/cfconfig/cfconfig_path_provider.h b/desktop/base/config_manager/include/cfconfig/cfconfig_path_provider.h index 6fda4b58c..2e815d54d 100644 --- a/desktop/base/config_manager/include/cfconfig/cfconfig_path_provider.h +++ b/desktop/base/config_manager/include/cfconfig/cfconfig_path_provider.h @@ -16,7 +16,7 @@ namespace cf::config { * @brief Abstract interface for providing config store file paths. * * This interface allows different projects to provide their own path logic: - * - Desktop project: uses standard paths like /etc/cfdesktop, ~/.config/cfdesktop + * - Desktop project: uses paths under the application-managed directory * - Other projects: can use custom paths * - Tests: can use mock/temporary paths * @@ -29,7 +29,7 @@ class IConfigStorePathProvider { /** * @brief Get the system-level config file path. * - * @return Full path to system config (e.g., "/etc/cfdesktop/system.ini") + * @return Full path to system config (e.g., "{app_dir}/system.ini") * @return Empty QString if system layer should be disabled * @throws None * @note None @@ -133,9 +133,9 @@ class IConfigStorePathProvider { * @brief Default path provider for CFDesktop project. * * Provides standard paths: - * - System: /etc/cfdesktop/system.ini + * - System: {app_dir}/system.ini (managed within CFDesktop's own directory) * - User: ~/.config/cfdesktop/user.ini - * - App: config/app.ini (relative to working directory) + * - App: {app_dir}/app.ini */ class DesktopConfigStorePathProvider : public IConfigStorePathProvider { public: @@ -143,7 +143,7 @@ class DesktopConfigStorePathProvider : public IConfigStorePathProvider { * @brief Construct with default paths. * * Uses: - * - system_path: "/etc/cfdesktop/system.ini" + * - system_path: disabled (empty) by default * - user_dir: "~/.config/cfdesktop" * - app_dir: "config" * @throws None diff --git a/desktop/base/config_manager/src/cfconfig_path_provider.cpp b/desktop/base/config_manager/src/cfconfig_path_provider.cpp index 9dc484302..b13576437 100644 --- a/desktop/base/config_manager/src/cfconfig_path_provider.cpp +++ b/desktop/base/config_manager/src/cfconfig_path_provider.cpp @@ -16,17 +16,17 @@ namespace cf::config { // ============================================================================ DesktopConfigStorePathProvider::DesktopConfigStorePathProvider() - : system_path_("/etc/cfdesktop/system.ini"), user_dir_(QDir::homePath() + "/.config/cfdesktop"), + : system_path_(QString()), user_dir_(QDir::homePath() + "/.config/cfdesktop"), app_dir_("config") {} DesktopConfigStorePathProvider::DesktopConfigStorePathProvider(const QString& app_base) - : system_path_("/etc/cfdesktop/system.ini"), user_dir_(QDir::homePath() + "/.config/cfdesktop"), + : system_path_(app_base + "/system.ini"), user_dir_(QDir::homePath() + "/.config/cfdesktop"), app_dir_(app_base) {} DesktopConfigStorePathProvider::DesktopConfigStorePathProvider(const QString& organization, const QString& application) - : system_path_("/etc/" + organization + "/system.ini"), - user_dir_(QDir::homePath() + "/.config/" + organization), app_dir_("config") { + : system_path_(QString()), user_dir_(QDir::homePath() + "/.config/" + organization), + app_dir_("config") { user_filename_ = application + ".ini"; app_filename_ = application + ".ini"; } diff --git a/document/HandBook/desktop/base/config_manager/01-quick-start.md b/document/HandBook/desktop/base/config_manager/01-quick-start.md index 01ce124c5..e14e45e90 100644 --- a/document/HandBook/desktop/base/config_manager/01-quick-start.md +++ b/document/HandBook/desktop/base/config_manager/01-quick-start.md @@ -36,7 +36,7 @@ auto& config = ConfigStore::instance(); class CustomPathProvider : public IConfigStorePathProvider { public: QString system_path() const override { - return "/etc/myapp/system.ini"; + return "{app_dir}/system.ini"; // CFDesktop 自管理目录内 } QString user_dir() const override { diff --git a/document/HandBook/desktop/base/config_manager/03-faq.md b/document/HandBook/desktop/base/config_manager/03-faq.md index 523043189..b584b84f8 100644 --- a/document/HandBook/desktop/base/config_manager/03-faq.md +++ b/document/HandBook/desktop/base/config_manager/03-faq.md @@ -108,6 +108,8 @@ ConfigStore::instance().set( #### 方法 2:自定义路径提供者 +> **注**: 以下示例展示如何为自定义应用实现路径提供者。CFDesktop 默认实现使用应用自管理目录,不读取 `/etc/` 系统路径。 + ```cpp #include #include @@ -149,6 +151,8 @@ ConfigStore::instance().initialize(std::make_shared()); #### 方法 3:使用环境变量 +> **注**: 以下为自定义应用的示例。CFDesktop 默认实现使用应用自管理目录。 + ```cpp #include #include @@ -600,7 +604,7 @@ watcher_handle = ConfigStore::instance().watch("batch.*", callback); | 平台 | 层级 | 路径 | |------|------|------| -| **Linux** | System | `/etc/cfdesktop/system.ini` | +| **Linux** | System | `<应用目录>/config/system.ini` | | | User | `~/.config/cfdesktop/user.ini` | | | App | `<应用目录>/config/app.ini` | | **Windows** | System | `HKEY_LOCAL_MACHINE\Software\CFDesktop` | @@ -621,7 +625,7 @@ watcher_handle = ConfigStore::instance().watch("batch.*", callback); class DebugPathProvider : public IConfigStorePathProvider { public: QString system_path() const override { - QString path = "/etc/cfdesktop/system.ini"; + QString path = QCoreApplication::applicationDirPath() + "/config/system.ini"; std::cout << "System path: " << path.toStdString() << std::endl; return path; } @@ -657,7 +661,7 @@ ConfigStore::instance().initialize(std::make_shared()); // 方法 2:直接检查文件是否存在 void check_config_files() { std::array paths = { - "/etc/cfdesktop/system.ini", + QCoreApplication::applicationDirPath().toStdString() + "/config/system.ini", std::getenv("HOME") + "/.config/cfdesktop/user.ini"s, QCoreApplication::applicationDirPath().toStdString() + "/config/app.ini" }; @@ -763,7 +767,7 @@ void diagnose_config_files() { std::cout << "\n=== 配置文件诊断 ===" << std::endl; std::array, 3> files = { - {Layer::System, "/etc/cfdesktop/system.ini"}, + {Layer::System, QCoreApplication::applicationDirPath().toStdString() + "/config/system.ini"}, {Layer::User, std::getenv("HOME") + "/.config/cfdesktop/user.ini"s}, {Layer::App, QCoreApplication::applicationDirPath().toStdString() + "/config/app.ini"} }; @@ -1196,13 +1200,15 @@ void ensure_config_directory(const std::string& path) { ensure_config_directory("~/.config/cfdesktop/user.ini"); ```text -#### 问题 2:System 层路径需要 root 权限 +#### 问题 2:System 层路径不可写 + +CFDesktop 使用应用自管理目录,System 层配置位于应用目录下(如 `{app_dir}/system.ini`),无需 root 权限。 ```bash -# System 层配置通常位于 /etc,需要 root 权限写入 -sudo touch /etc/cfdesktop/system.ini -sudo chown $USER:$USER /etc/cfdesktop/system.ini -# 或者将应用配置放在用户目录 +# 配置文件位于应用目录下,应用启动时自动创建 +# 例如: <应用目录>/config/system.ini +# 如果目录不存在,手动创建: +mkdir -p <应用目录>/config ```text #### 问题 3:INI 文件编码 diff --git a/document/HandBook/desktop/base/config_manager/04-architecture.md b/document/HandBook/desktop/base/config_manager/04-architecture.md index 96b983c60..f2856608a 100644 --- a/document/HandBook/desktop/base/config_manager/04-architecture.md +++ b/document/HandBook/desktop/base/config_manager/04-architecture.md @@ -28,7 +28,7 @@ ConfigStore 采用四层优先级架构,实现了配置的层次化管理和 | - 用户个性化设置,持久化存储 | +-----------------------------------------------+ | System Layer (最低优先级) | -| - 系统级配置,/etc/cfdesktop/system.ini | +| - 系统级配置,{app_dir}/system.ini (CFDesktop 自管理目录) | | - 全局默认配置,只读或需要特权写入 | +-----------------------------------------------+ ```cpp @@ -196,7 +196,7 @@ public: | 层级 | 路径格式 | 说明 | |------|----------------------------|--------------------------| -| System | `/etc/cfdesktop/system.ini` | 系统配置,需要权限写入 | +| System | `{app_dir}/system.ini` | 系统配置 (CFDesktop 自管理目录) | | User | `~/.config/cfdesktop/user.ini` | 用户配置,自动创建目录 | | App | `config/app.ini` | 相对路径,运行时可写 | | Temp | (内存) | 不持久化 | diff --git a/document/HandBook/desktop/base/config_manager/README.md b/document/HandBook/desktop/base/config_manager/README.md index 88cfefbe0..17f18c323 100644 --- a/document/HandBook/desktop/base/config_manager/README.md +++ b/document/HandBook/desktop/base/config_manager/README.md @@ -20,7 +20,7 @@ ConfigStore 采用四层优先级架构,支持配置的自然覆盖: | **Temp** | 最高 (3) | 读写 | 内存 | 临时配置,测试用,不持久化 | | **App** | 高 (2) | 读写 | `/config/app.ini` | 应用运行时配置 | | **User** | 中 (1) | 读写 | `~/.config/cfdesktop/user.ini` | 用户个人配置 | -| **System** | 低 (0) | 读写 | `/etc/cfdesktop/system.ini` | 系统默认配置 | +| **System** | 低 (0) | 读写 | `{app_dir}/system.ini` | 系统默认配置 (CFDesktop 自管理目录) | 查询时按优先级从高到低查找,写入时默认写入 App 层。 diff --git a/document/design_stage/01_phase1_hardware_probe.md b/document/design_stage/01_phase1_hardware_probe.md index f22d15ea3..75f2b6842 100644 --- a/document/design_stage/01_phase1_hardware_probe.md +++ b/document/design_stage/01_phase1_hardware_probe.md @@ -25,7 +25,7 @@ description: "Phase 1: 硬件探针与能力分级详细设计文档 的详细 - [ ] `HardwareProbe` 模块及其单元测试 - [ ] `CapabilityPolicy` 策略引擎 - [ ] `HWTier` 枚举定义及查询接口 -- [ ] 设备配置文件 `/etc/CFDesktop/device.conf` +- [ ] ~~设备配置文件 `/etc/CFDesktop/device.conf`~~ ⚠️ **过时**: 实际实现使用 `setDeviceConfigOverride()` API + CFDesktop 自管理目录,不读取系统级路径 - [ ] Mock 数据集用于单元测试 --- @@ -503,6 +503,9 @@ namespace CFDesktop::Base { /** * @brief 设备配置文件读取器 * + * ⚠️ **过时**: 实际实现使用 `setDeviceConfigOverride()` API,配置在 CFDesktop 自管理目录内。 + * 以下 `/etc/CFDesktop/` 路径仅为早期设计参考。 + * * 读取 /etc/CFDesktop/device.conf,允许用户手动覆盖硬件检测。 * * 配置文件格式: @@ -526,13 +529,13 @@ public: /** * @brief 加载配置文件 */ - static Config load(const QString& path = "/etc/CFDesktop/device.conf"); + static Config load(const QString& path = "/etc/CFDesktop/device.conf"); // ⚠️ 过时:实际使用应用内目录 /** * @brief 保存配置文件 */ static bool save(const Config& config, - const QString& path = "/etc/CFDesktop/device.conf"); + const QString& path = "/etc/CFDesktop/device.conf"); // ⚠️ 过时 /** * @brief 执行自定义检测脚本 @@ -805,13 +808,15 @@ void HardwareProbe::calculateTier(HardwareInfo& info) { ## 六、设备配置文件格式 +> ⚠️ **过时说明**: 以下 `/etc/CFDesktop/` 路径为早期设计。实际实现中,CFDesktop 不读取系统级路径,设备配置通过 `setDeviceConfigOverride()` API 或 ConfigStore 应用内目录管理。 + ### 6.1 配置文件模板 **文件**: `configs/device.conf.template` ```ini # CFDesktop 设备配置文件 -# 部署位置: /etc/CFDesktop/device.conf +# 部署位置: CFDesktop 自管理目录内 (非 /etc/) [Device] # 档位设置: auto(自动检测) | low | mid | high diff --git a/document/design_stage/02_phase2_base_library.md b/document/design_stage/02_phase2_base_library.md index f1a4b38d4..117f80057 100644 --- a/document/design_stage/02_phase2_base_library.md +++ b/document/design_stage/02_phase2_base_library.md @@ -753,7 +753,7 @@ namespace CFDesktop::Base { * @brief 配置类型 */ enum class ConfigType { - System, // 系统配置 (/etc/CFDesktop/config.conf) + System, // ⚠️ 过时:实际使用 CFDesktop 自管理目录,非 /etc/ User, // 用户配置 (~/.config/CFDesktop/config.conf) App, // 应用配置 Temp // 临时配置 (内存) @@ -869,7 +869,7 @@ void setConfig(const QString& key, const T& value) { ### 6.2 配置文件格式 -**系统配置**: `/etc/CFDesktop/config.conf` +**系统配置**: ~~`/etc/CFDesktop/config.conf`~~ ⚠️ 过时:实际使用 CFDesktop 自管理目录 ```ini [General] diff --git a/document/todo/desktop/07_render_backend.md b/document/todo/desktop/07_render_backend.md index eb57e52d4..49067ebd9 100644 --- a/document/todo/desktop/07_render_backend.md +++ b/document/todo/desktop/07_render_backend.md @@ -66,7 +66,7 @@ description: "状态: 🚧 部分完成 (接口已实现,具体后端待开发 - [ ] Mid Tier: 软件降级可选 - [ ] Low Tier: LinuxFB 直连 - [ ] 实现屏幕尺寸配置 - - [ ] 读取 `/etc/CFDesktop/display.conf` + - [ ] 读取 `{desktop_root}/config/display.conf` - [ ] 命令行参数 `--screen-size=WxH` - [ ] 环境变量 `CFDESKTOP_SCREEN_SIZE` - [ ] 实现 DRM 设备选择 @@ -174,7 +174,7 @@ description: "状态: 🚧 部分完成 (接口已实现,具体后端待开发 - [ ] `src/desktop/render/screen_simulator.cpp` ### 配置文件 -- [ ] `/etc/CFDesktop/display.conf` (Linux) +- [ ] `{desktop_root}/config/display.conf` (Linux) - [ ] `configs/display.conf` (Windows 开发) ### 测试文件 diff --git a/document/todo/done/01_hardware_probe_status.md b/document/todo/done/01_hardware_probe_status.md index bebe0e676..7189c1436 100644 --- a/document/todo/done/01_hardware_probe_status.md +++ b/document/todo/done/01_hardware_probe_status.md @@ -6,9 +6,9 @@ description: "模块ID: Phase 1,状态: 🚧 部分完成" # Phase 1: 硬件探针与能力分级 - 状态文档 > **模块ID**: Phase 1 -> **状态**: 🚧 部分完成 -> **总体进度**: ~90% -> **最后更新**: 2026-03-27 +> **状态**: ✅ 完成 +> **总体进度**: 100% +> **最后更新**: 2026-05-23 --- @@ -41,9 +41,9 @@ description: "模块ID: Phase 1,状态: 🚧 部分完成" | MemoryDetector | 95% | ✅ 完成 | | GPUDetector | 90% | ✅ 核心功能已完成 | | NetworkDetector | 85% | ✅ 核心功能已完成 | -| HWTier 系统 | 0% | ❌ 待实现 | -| CapabilityPolicy | 0% | ❌ 待实现 | -| HardwareProbe 主类 | 0% | ❌ 待实现 | +| HWTier 系统 | 100% | ✅ 完成 | +| CapabilityPolicy | 100% | ✅ 完成 | +| HardwareProbe 主类 | 100% | ✅ 完成 | --- @@ -187,181 +187,80 @@ if (mem_info) { --- -## 五、待完成模块 +## 五、HWTier 系统 (Phase 1 核心功能) — ✅ 已完成 -> **注意**: GPU 和 Network 检测器已完成,请参考上方"三、已完成工作"中的 3.3 和 3.4 节。 +> **注意**: GPU 和 Network 检测器已完成,HWTier 系统已实现,Phase 1 核心功能全部完成。 -### 5.1 HWTier 枚举定义 (P0) +### 5.1 HWTier 枚举定义 ✅ -**需求描述**: -- 定义 Low/Mid/High 三档枚举 -- 每档对应硬件配置说明 -- 档位字符串转换函数 +**实现位置**: `base/include/system/hardware_tier/hardware_tier_data.h` -**建议文件路径**: `base/hardware/HWTier.h` +- [x] `HardwareTierLevel` 枚举 (Unknown/Low/Mid/High) +- [x] 档位字符串转换函数 `hardwareTierLevelToString()` +- [x] 各档位对应硬件配置说明 (i.MX6ULL / RK3568 / RK3588) -**接口设计**: -```cpp -namespace CFDesktop::Base { +### 5.2 硬件数据收集器 ✅ -enum class HWTier { - Low, // IMX6ULL 级别 - Mid, // RK3568 级别 - High // RK3588 级别 -}; +**实现位置**: `base/system/hardware_tier/hardware_tier_collector.h`, `base/system/hardware_tier/default/default_collector.cpp` -QString tierToString(HWTier tier); -HWTier tierFromString(const QString& str); +- [x] `IHardwareCollector` 接口 — 可插拔收集器 +- [x] `HardwareData` 结构体 — CPU/GPU/Memory/Display 原始数据 +- [x] 默认收集器 — 整合 CPU/GPU/Memory/Display 检测器 +- [x] 跨平台支持 (Linux/Windows) -} // namespace CFDesktop::Base -```yaml +### 5.3 评分引擎 ✅ ---- +**实现位置**: `base/system/hardware_tier/hardware_tier_scorer.h`, `base/system/hardware_tier/default/default_*_scorer.cpp` -### 5.2 HardwareProbe 主类 (P0) +- [x] `IHardwareScorer` 接口 — 可插拔评分器 +- [x] 维度评分类型: `CpuScore`, `GpuScore`, `MemoryScore`, `DisplayScore` (各 0-100) +- [x] 默认评分器: CPU/GPU/Memory/Display 四维度独立评分 +- [x] 可通过 `registerScorer()` 替换各维度评分逻辑 -**需求描述**: -- 整合所有检测器 -- 实现档位计算逻辑 -- 单例模式 -- Mock 数据支持 +### 5.4 档位评估器 ✅ -**建议文件路径**: -- `base/hardware/HardwareProbe.h` -- `base/hardware/HardwareProbe.cpp` +**实现位置**: `base/system/hardware_tier/hardware_tier_assessor.h`, `base/system/hardware_tier/default/default_assessor.cpp` -**接口设计**: -```cpp -namespace CFDesktop::Base { - -struct HardwareInfo { - HWTier tier = HWTier::Low; - CPUInfo cpu; - GPUInfo gpu; - MemoryInfo memory; - QList networkInterfaces; - QString deviceTreeCompatible; - bool isUserOverridden = false; -}; - -class HardwareProbe : public QObject { - Q_OBJECT +- [x] `IHardwareAssessor` 接口 — 可插拔评估器 +- [x] `HardwareTierAssessment` 结构体 — 四维度评分 + 总档位 + 覆盖信息 +- [x] 默认评估器 — 综合四维度分数计算档位 -public: - static HardwareProbe* instance(); - HardwareInfo probe(); - HardwareInfo forceRedetect(); - HWTier currentTier() const; - const HardwareInfo& hardwareInfo() const; - void setMockData(const HardwareInfo& mockInfo); - -signals: - void probeCompleted(const HardwareInfo& info); - void tierChanged(HWTier newTier); - -private: - HardwareProbe(QObject* parent = nullptr); - void calculateTier(HardwareInfo& info); -}; +### 5.5 策略引擎 ✅ -} // namespace CFDesktop::Base -```yaml +**实现位置**: `base/system/hardware_tier/hardware_tier_policy.h`, `base/system/hardware_tier/default/default_policy.cpp` ---- +- [x] `IHardwarePolicy` 接口 — 可插拔策略 +- [x] `HardwareTierCapabilities` 结构体 — 能力标志 (OpenGL/软件渲染/动画/硬件解码/EGLFS/LinuxFB) +- [x] 默认策略 — 根据 Low/Mid/High 档位映射能力标志 +- [x] 部分动画支持 (`enable_partial_animation`) -### 5.3 CapabilityPolicy 策略引擎 (P0) +### 5.6 DeviceConfig 覆盖 ✅ -**需求描述**: -- 定义各策略结构体 -- 实现档位特定策略配置 -- 策略查询接口 +**实现位置**: `base/include/system/hardware_tier/hardware_tier.h` -**建议文件路径**: -- `base/hardware/CapabilityPolicy.h` -- `base/hardware/CapabilityPolicy.cpp` +- [x] `setDeviceConfigOverride()` — 手动强制档位 +- [x] `clearDeviceConfigOverride()` — 清除覆盖 +- [x] 覆盖时跳过收集/评分,直接使用指定档位 +- [x] 覆盖原因记录 (`override_reason`) -**接口设计**: -```cpp -namespace CFDesktop::Base { +### 5.7 管线注册 API ✅ -struct AnimationPolicy { - bool enabled = false; - int defaultDurationMs = 0; - int maxConcurrentAnimations = 0; - bool allowComplexEffects = false; -}; +**实现位置**: `base/include/system/hardware_tier/hardware_tier.h` -struct RenderingPolicy { - QString qtPlatform; - bool useOpenGL = false; - QString openGLVersion; - bool useVSync = false; - int maxFPS = 60; -}; +- [x] `registerCollector()` — 注册自定义收集器 +- [x] `registerScorer()` — 注册自定义评分器 (按维度) +- [x] `registerAssessor()` — 注册自定义评估器 +- [x] `registerPolicy()` — 注册自定义策略 +- [x] `assessHardware()` — 执行完整管线 (缓存支持) +- [x] `getHardwareTierCapabilities()` — 查询能力标志 -struct VideoDecoderPolicy { - bool useHardwareDecoder = false; - QStringList supportedCodecs; - int maxResolution = 0; - int maxBitrate = 0; -}; +### 5.8 示例代码 ✅ -struct MemoryPolicy { - int maxImageCacheBytes = 0; - int maxFontCacheBytes = 0; - bool enableTextureCompression = false; - int maxWindowSurfaces = 0; -}; +**实现位置**: `example/base/system/example_hardware_tier.cpp` -class CapabilityPolicy : public QObject { - Q_OBJECT - -public: - static CapabilityPolicy* instance(); - AnimationPolicy getAnimationPolicy() const; - RenderingPolicy getRenderingPolicy() const; - VideoDecoderPolicy getVideoDecoderPolicy() const; - MemoryPolicy getMemoryPolicy() const; - HWTier currentTier() const; - void overrideTier(HWTier tier); - -signals: - void policyChanged(HWTier newTier); -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -### 5.4 DeviceConfig 配置文件 (P1) - -**需求描述**: -- INI 格式配置文件解析 -- 支持档位手动覆盖 -- 自定义检测脚本执行 - -**建议文件路径**: -- `base/hardware/DeviceConfig.h` -- `base/hardware/DeviceConfig.cpp` - -**配置文件格式** (`/etc/CFDesktop/device.conf`): -```ini -[Device] -Tier=auto|low|mid|high -CustomScript=/opt/cfdesktop/detect-hardware.sh -BoardName=Generic-Board - -[Overrides] -EnableAnimations=true -AnimationDuration=200 -RenderingBackend=auto -ForceOpenGL=false -VideoDecoder=auto - -[Logging] -LogLevel=Info -```yaml +- [x] 完整的硬件分级评估示例 +- [x] 四维度评分展示 +- [x] 能力标志展示 --- @@ -478,72 +377,47 @@ base/ ### 待创建文件 +> HWTier 系统已全部实现,无待创建文件。 + ```text base/ -├── hardware/ -│ ├── HWTier.h # 档位枚举 -│ ├── HardwareInfo.h # 硬件信息结构体 -│ ├── HardwareProbe.h # 探针主类 -│ ├── CapabilityPolicy.h # 策略引擎 -│ ├── DeviceConfig.h # 配置文件 -│ ├── detectors/ -│ │ ├── GPUDetector.h # GPU 检测器 -│ │ └── NetworkDetector.h # 网络检测器 -│ └── platform/ -│ ├── LinuxDetector.cpp # Linux 平台实现 -│ └── WindowsDetector.cpp # Windows 平台实现 +├── include/system/hardware_tier/ +│ ├── hardware_tier.h # 公共 API (管线注册 + 评估入口) +│ └── hardware_tier_data.h # 数据结构 (枚举/评分/结果/能力标志) +├── system/hardware_tier/ +│ ├── hardware_tier_collector.h # IHardwareCollector 接口 +│ ├── hardware_tier_scorer.h # IHardwareScorer 接口 +│ ├── hardware_tier_assessor.h # IHardwareAssessor 接口 +│ ├── hardware_tier_policy.h # IHardwarePolicy 接口 +│ ├── default_factories.h # 默认工厂函数 +│ ├── hardware_tier.cpp # 管线实现 +│ └── default/ +│ ├── default_collector.cpp # 默认收集器 +│ ├── default_cpu_scorer.cpp # 默认 CPU 评分 +│ ├── default_gpu_scorer.cpp # 默认 GPU 评分 +│ ├── default_memory_scorer.cpp # 默认 Memory 评分 +│ ├── default_display_scorer.cpp # 默认 Display 评分 +│ ├── default_assessor.cpp # 默认评估器 +│ └── default_policy.cpp # 默认策略 ```text -### 测试文件 - -```text -tests/ -├── hardware/ -│ ├── test_hardware_probe.cpp # 主测试 -│ ├── test_capability_policy.cpp # 策略测试 -│ └── mock/ -│ └── proc/ # Mock 数据 -```yaml - --- ## 八、下一步行动建议 -### 优先级1 (高) - -1. **定义 HWTier 枚举** - - 优先级: 最高 - - 理由: 其他模块依赖此定义 - - 预计工作量: 0.5天 - -### 优先级2 (中) - -2. **实现 HardwareProbe 主类** - - 优先级: 中 - - 理由: 整合所有检测器 - - 预计工作量: 2-3天 - -3. **实现 CapabilityPolicy** - - 优先级: 中 - - 理由: 为上层提供策略配置 - - 预计工作量: 2-3天 - -4. **实现 DeviceConfig** - - 优先级: 中 - - 理由: 支持手动配置覆盖 - - 预计工作量: 1-2天 +### 已完成 ✅ -### 优先级3 (低) +1. **HWTier 枚举定义** — ✅ 已实现 (`HardwareTierLevel`) +2. **HardwareProbe 管线** — ✅ 已实现 (可插拔 Collect→Score→Assess→Policy 四阶段管线) +3. **CapabilityPolicy 策略引擎** — ✅ 已实现 (`IHardwarePolicy` + 默认策略) +4. **DeviceConfig 覆盖** — ✅ 已实现 (`setDeviceConfigOverride()`) -5. **创建 Mock 数据集** - - 优先级: 低 - - 理由: 单元测试需要 - - 预计工作量: 1天 +### 后续可选增强 -6. **编写单元测试** - - 优先级: 低 - - 理由: 确保代码质量 - - 预计工作量: 3-4天 +1. **集成 ConfigStore 查询覆盖** — 从 ConfigStore 读取设备档位配置,自动调用 `setDeviceConfigOverride()` +2. **自定义检测脚本执行** — 支持外部脚本注入硬件数据 +3. **Mock 数据集** — 单元测试用模拟数据 +4. **性能测试** — 各评分器在不同硬件下的基准测试 --- @@ -556,5 +430,6 @@ tests/ --- -*文档版本: v1.0* +*文档版本: v2.0* *生成时间: 2026-03-11* +*最后更新: 2026-05-23 (HWTier 系统完成)* diff --git a/document/todo/done/02_base_library_status.md b/document/todo/done/02_base_library_status.md index 887ebe886..6ec37edf5 100644 --- a/document/todo/done/02_base_library_status.md +++ b/document/todo/done/02_base_library_status.md @@ -462,7 +462,7 @@ flush(); |------|--------|------|------| | QSSProcessor | P2 | - | 待规划 | | VariableResolver | P2 | - | 待规划 | -| HWTier 降级逻辑 | P1 | HWTier | 待规划 | +| HWTier 降级逻辑 | P1 | HWTier | ✅ 已实现 (hardware_tier_policy) | | 屏幕参数检测 | P2 | - | 待规划 | | 模拟器注入接口 | P2 | - | 待规划 | diff --git a/document/todo/done/06_infrastructure_status.md b/document/todo/done/06_infrastructure_status.md index bd1022bc9..6388e93b4 100644 --- a/document/todo/done/06_infrastructure_status.md +++ b/document/todo/done/06_infrastructure_status.md @@ -96,7 +96,7 @@ description: "状态: ✅ 部分完成,总体进度: ~50%" #### 配置文件路径 -- System: `/etc/cfdesktop/system.ini` +- System: `{app_dir}/system.ini`(CFDesktop 自管理目录内) - User: `~/.config/cfdesktop/user.ini` - App: `config/app.ini` diff --git a/document/todo/done/PROJECT_STATUS_REPORT.md b/document/todo/done/PROJECT_STATUS_REPORT.md index 0c7ed5771..7549d2908 100644 --- a/document/todo/done/PROJECT_STATUS_REPORT.md +++ b/document/todo/done/PROJECT_STATUS_REPORT.md @@ -21,7 +21,7 @@ CFDesktop 是一个基于 Qt6 的嵌入式桌面框架项目,采用 Material D | 模块 | 完成度 | 状态 | 优先级 | |------|--------|------|--------| | Phase 0: 工程骨架 | 100% | ✅ 已完成并归档 | P0 | -| Phase 1: 硬件探针 | 90% | 🚧 进行中 | P0 | +| Phase 1: 硬件探针 | 100% | ✅ 完成 | P0 | | Phase 2: Base库核心 | 85% | 🚧 进行中 | P0 | | Phase 3: 输入抽象层 | 0% | ⬜ 待开始 | P1 | | Phase 4: 多平台模拟器 | 0% | ⬜ 待开始 | P1 | @@ -59,7 +59,7 @@ CFDesktop 是一个基于 Qt6 的嵌入式桌面框架项目,采用 Material D --- -### 1.1 硬件探针 (Phase 1) - 90% 🚧 进行中 +### 1.1 硬件探针 (Phase 1) - 100% ✅ 完成 #### ✅ 已完成 - **CPU 检测器** (100%) - `base/system/cpu/` 模块 @@ -99,11 +99,19 @@ CFDesktop 是一个基于 Qt6 的嵌入式桌面框架项目,采用 Material D - **网络检测器** (85%) - `base/system/network/` 模块 - 网卡枚举、IP地址、可达性检测 +#### ✅ 已完成 (HWTier 系统) +- **HWTier 系统** (100%) - `base/system/hardware_tier/` 模块 + - `HardwareTierLevel` 枚举 (Unknown/Low/Mid/High) + - 可插拔四阶段管线 (Collect→Score→Assess→Policy) + - 默认收集器、四维度评分器 (CPU/GPU/Memory/Display 各 0-100) + - 默认评估器和策略引擎 + - DeviceConfig 覆盖支持 (`setDeviceConfigOverride()`) + - `HardwareTierCapabilities` 能力标志 (OpenGL/软件渲染/动画/硬件解码/EGLFS/LinuxFB) + - 完整管线注册 API (`registerCollector/Scorer/Assessor/Policy`) + - 示例程序 `example/base/system/example_hardware_tier.cpp` + #### ❌ 待完成 -- HWTier 枚举定义 (Low/Mid/High 三档) -- HardwareProbe 主类整合 -- CapabilityPolicy 策略引擎 -- DeviceConfig 配置文件系统 +- 自定义检测脚本执行支持 --- @@ -472,6 +480,7 @@ CFDesktop/ │ ├── system/memory/ # ✅ 内存检测 (95%) │ ├── system/gpu/ # ✅ GPU检测 (90%) │ ├── system/network/ # ✅ 网络检测 (85%) +│ ├── system/hardware_tier/ # ✅ HWTier 硬件分级 (100%) │ ├── device/console/ # ✅ 控制台设备 (85%) │ └── include/base/policy_chain/ # ✅ 策略链 (80%) ├── ui/ # UI框架 (95%) diff --git a/test/ui/core/CMakeLists.txt b/test/ui/core/CMakeLists.txt index 00392a7cb..f4cec64e4 100644 --- a/test/ui/core/CMakeLists.txt +++ b/test/ui/core/CMakeLists.txt @@ -1,12 +1 @@ -log_info("UI_Core_Tests" "Configured ui/core tests") - -# ============================================================================= -# ui/core tests - token_test -# ============================================================================= -add_gtest_executable( - TEST_NAME token_test - SOURCE_FILE token_test.cpp - LINK_LIBRARIES Qt6::Core;Qt6::Gui;GTest::gtest;GTest::gtest_main;cfui;CFDesktop::base_headers - LABELS "ui;core;token" - LOG_MODULE ui_core_tests -) +log_info("UI_Core_Tests" "Configured ui/core tests") diff --git a/test/ui/core/token_test.cpp b/test/ui/core/token_test.cpp deleted file mode 100644 index e15f66a0d..000000000 --- a/test/ui/core/token_test.cpp +++ /dev/null @@ -1,579 +0,0 @@ -/** - * @file token_test.cpp - * @brief Comprehensive unit tests for cf::ui::core Token system using GoogleTest - * - * Test Coverage: - * 1. TokenRegistry singleton - * 2. StaticToken - compile-time type-safe tokens - * 3. DynamicToken - runtime type-erased tokens - * 4. Thread safety - concurrent access - * 5. Error handling - TokenError types - * 6. Complex types - */ - -#include "ui/core/token.hpp" -#include -#include -#include -#include -#include - -using namespace cf::ui::core; -// Bring _hash literal operator into scope -using namespace cf::hash; - -// ============================================================================= -// Test Suite 1: TokenRegistry Singleton -// ============================================================================= - -TEST(TokenRegistryTest, Singleton) { - auto& r1 = TokenRegistry::get(); - auto& r2 = TokenRegistry::get(); - - EXPECT_EQ(&r1, &r2); -} - -// ============================================================================= -// Test Suite 2: StaticToken -// ============================================================================= - -TEST(StaticTokenTest, RegisterAndGet) { - using TestToken = StaticToken; - auto& registry = TokenRegistry::get(); - - // Clean up if exists - registry.remove("testToken"_hash); - - auto result = registry.register_token(42); - EXPECT_TRUE(result) << "Registration should succeed"; - - auto get_result = TestToken::get(); - EXPECT_TRUE(get_result) << "Get should succeed"; - EXPECT_EQ(**get_result, 42); - - // Cleanup - registry.remove("testToken"_hash); -} - -TEST(StaticTokenTest, MultipleTypes) { - using IntToken = StaticToken; - using StringToken = StaticToken; - using DoubleToken = StaticToken; - - auto& registry = TokenRegistry::get(); - - // Clean up - registry.remove("intToken"_hash); - registry.remove("stringToken"_hash); - registry.remove("doubleToken"_hash); - - EXPECT_TRUE(registry.register_token(123)); - EXPECT_TRUE(registry.register_token("hello")); - EXPECT_TRUE(registry.register_token(3.14)); - - auto int_result = IntToken::get(); - auto string_result = StringToken::get(); - auto double_result = DoubleToken::get(); - - EXPECT_TRUE(int_result); - EXPECT_TRUE(string_result); - EXPECT_TRUE(double_result); - - EXPECT_EQ(**int_result, 123); - EXPECT_EQ(**string_result, "hello"); - EXPECT_DOUBLE_EQ(**double_result, 3.14); - - // Cleanup - registry.remove("intToken"_hash); - registry.remove("stringToken"_hash); - registry.remove("doubleToken"_hash); -} - -TEST(StaticTokenTest, NotFound) { - using NonExistentToken = StaticToken; - auto& registry = TokenRegistry::get(); - - // Make sure it doesn't exist - registry.remove("nonExistentToken"_hash); - - auto result = NonExistentToken::get(); - EXPECT_FALSE(result); - if (!result) { - EXPECT_EQ(result.error().kind, TokenError::Kind::NotFound); - } -} - -TEST(StaticTokenTest, ConstAccess) { - using ConstTestToken = StaticToken; - auto& registry = TokenRegistry::get(); - - // Clean up - registry.remove("constTestToken"_hash); - - EXPECT_TRUE(registry.register_token(999)); - - auto result = ConstTestToken::get_const(); - EXPECT_TRUE(result); - EXPECT_EQ(**result, 999); - - // Cleanup - registry.remove("constTestToken"_hash); -} - -TEST(StaticTokenTest, AlreadyRegistered) { - using DuplicateToken = StaticToken; - auto& registry = TokenRegistry::get(); - - // Clean up - registry.remove("duplicateToken"_hash); - - EXPECT_TRUE(registry.register_token(1)); - auto result2 = registry.register_token(2); - EXPECT_FALSE(result2); - if (!result2) { - EXPECT_EQ(result2.error().kind, TokenError::Kind::AlreadyRegistered); - } - - // First value should still be there - auto get_result = DuplicateToken::get(); - EXPECT_TRUE(get_result); - EXPECT_EQ(**get_result, 1); - - // Cleanup - registry.remove("duplicateToken"_hash); -} - -// ============================================================================= -// Test Suite 3: DynamicToken -// ============================================================================= - -TEST(DynamicTokenTest, RegisterAndGet) { - auto& registry = TokenRegistry::get(); - - std::string name = "dynamicTest"; - registry.remove(name); - - auto reg_result = registry.register_dynamic(name, 42); - EXPECT_TRUE(reg_result); - - auto get_result = registry.get_dynamic(name); - EXPECT_TRUE(get_result); - EXPECT_EQ(**get_result, 42); - - // Cleanup - registry.remove(name); -} - -TEST(DynamicTokenTest, MultipleTypes) { - auto& registry = TokenRegistry::get(); - - std::string name1 = "dynamicInt"; - std::string name2 = "dynamicString"; - std::string name3 = "dynamicVector"; - - registry.remove(name1); - registry.remove(name2); - registry.remove(name3); - - EXPECT_TRUE(registry.register_dynamic(name1, 456)); - EXPECT_TRUE(registry.register_dynamic(name2, "world")); - EXPECT_TRUE(registry.register_dynamic>(name3, {1, 2, 3})); - - auto r1 = registry.get_dynamic(name1); - auto r2 = registry.get_dynamic(name2); - auto r3 = registry.get_dynamic>(name3); - - EXPECT_TRUE(r1); - EXPECT_TRUE(r2); - EXPECT_TRUE(r3); - - EXPECT_EQ(**r1, 456); - EXPECT_EQ(**r2, "world"); - EXPECT_EQ((*r3)->size(), 3); - EXPECT_EQ((**r3)[0], 1); - EXPECT_EQ((**r3)[1], 2); - EXPECT_EQ((**r3)[2], 3); - - // Cleanup - registry.remove(name1); - registry.remove(name2); - registry.remove(name3); -} - -TEST(DynamicTokenTest, TypeMismatch) { - auto& registry = TokenRegistry::get(); - - std::string name = "typeMismatch"; - registry.remove(name); - - EXPECT_TRUE(registry.register_dynamic(name, 42)); - - // Try to get as wrong type - auto result = registry.get_dynamic(name); - EXPECT_FALSE(result); - if (!result) { - EXPECT_EQ(result.error().kind, TokenError::Kind::TypeMismatch); - } - - // Cleanup - registry.remove(name); -} - -TEST(DynamicTokenTest, NotFoundDynamic) { - auto& registry = TokenRegistry::get(); - - std::string name = "nonExistentDynamic"; - registry.remove(name); - - auto result = registry.get_dynamic(name); - EXPECT_FALSE(result); - if (!result) { - EXPECT_EQ(result.error().kind, TokenError::Kind::NotFound); - } -} - -TEST(DynamicTokenTest, RegisterByCopy) { - auto& registry = TokenRegistry::get(); - - std::string name = "copyTest"; - registry.remove(name); - - std::string value = "copied value"; - EXPECT_TRUE(registry.register_dynamic(name, value)); - - auto result = registry.get_dynamic(name); - EXPECT_TRUE(result); - EXPECT_EQ(**result, "copied value"); - EXPECT_EQ(value, "copied value"); // Original unchanged - - // Cleanup - registry.remove(name); -} - -TEST(DynamicTokenTest, RegisterByMove) { - auto& registry = TokenRegistry::get(); - - std::string name = "moveTest"; - registry.remove(name); - - std::string value = "moved value"; - EXPECT_TRUE(registry.register_dynamic(name, std::move(value))); - - auto result = registry.get_dynamic(name); - EXPECT_TRUE(result); - EXPECT_EQ(**result, "moved value"); - // Original may be in moved-from state - - // Cleanup - registry.remove(name); -} - -TEST(DynamicTokenTest, ConstAccess) { - auto& registry = TokenRegistry::get(); - - std::string name = "constDynamic"; - registry.remove(name); - - EXPECT_TRUE(registry.register_dynamic(name, 789)); - - auto result = registry.get_dynamic_const(name); - EXPECT_TRUE(result); - EXPECT_EQ(**result, 789); - - // Cleanup - registry.remove(name); -} - -TEST(DynamicTokenTest, Contains) { - auto& registry = TokenRegistry::get(); - - std::string name = "containsTest"; - registry.remove(name); - - EXPECT_FALSE(registry.contains(name)); - - EXPECT_TRUE(registry.register_dynamic(name, 1)); - EXPECT_TRUE(registry.contains(name)); - - // Cleanup - registry.remove(name); -} - -TEST(DynamicTokenTest, Remove) { - auto& registry = TokenRegistry::get(); - - std::string name = "removeTest"; - registry.remove(name); - - EXPECT_TRUE(registry.register_dynamic(name, 1)); - EXPECT_TRUE(registry.contains(name)); - - EXPECT_TRUE(registry.remove(name)); - EXPECT_FALSE(registry.contains(name)); - - EXPECT_FALSE(registry.remove(name)); // Already removed -} - -TEST(DynamicTokenTest, GetByHash) { - auto& registry = TokenRegistry::get(); - - std::string name = "hashLookup"; - constexpr uint64_t hash = "hashLookup"_hash; - registry.remove(hash); - - EXPECT_TRUE(registry.register_dynamic(name, 111)); - - auto result = registry.get_dynamic_by_hash(hash); - EXPECT_TRUE(result); - EXPECT_EQ(**result, 111); - - // Cleanup - registry.remove(hash); -} - -// ============================================================================= -// Test Suite 4: Complex Types -// ============================================================================= - -// Note: std::any requires copy-constructible types, so unique_ptr and atomic -// cannot be directly stored. Use shared_ptr or regular types instead. - -TEST(ComplexTypesTest, SharedPtr) { - using PtrToken = StaticToken, "ptrToken"_hash>; - auto& registry = TokenRegistry::get(); - - registry.remove("ptrToken"_hash); - - EXPECT_TRUE(registry.register_token(std::make_shared(333))); - - auto result = PtrToken::get(); - EXPECT_TRUE(result); - - auto ptr = **result; - EXPECT_EQ(*ptr.get(), 333); - - // Cleanup - registry.remove("ptrToken"_hash); -} - -TEST(ComplexTypesTest, Vector) { - auto& registry = TokenRegistry::get(); - - std::string name = "vectorToken"; - registry.remove(name); - - std::vector vec = {10, 20, 30, 40, 50}; - EXPECT_TRUE(registry.register_dynamic>(name, std::move(vec))); - - auto result = registry.get_dynamic>(name); - EXPECT_TRUE(result); - EXPECT_EQ((*result)->size(), 5); - EXPECT_EQ((**result)[0], 10); - EXPECT_EQ((**result)[4], 50); - - // Cleanup - registry.remove(name); -} - -// Custom struct for testing -struct TestStruct { - int a; - double b; - std::string c; - - bool operator==(const TestStruct& other) const { - return a == other.a && b == other.b && c == other.c; - } -}; - -TEST(ComplexTypesTest, CustomStruct) { - auto& registry = TokenRegistry::get(); - - std::string name = "structToken"; - registry.remove(name); - - TestStruct s{123, 4.56, "test"}; - EXPECT_TRUE(registry.register_dynamic(name, s)); - - auto result = registry.get_dynamic(name); - EXPECT_TRUE(result); - EXPECT_EQ((*result)->a, 123); - EXPECT_DOUBLE_EQ((*result)->b, 4.56); - EXPECT_EQ((*result)->c, "test"); - - // Cleanup - registry.remove(name); -} - -// ============================================================================= -// Test Suite 5: Thread Safety -// ============================================================================= - -// Note: std::atomic is not copy-constructible and cannot be stored in std::any. -// We use regular int and rely on TokenRegistry's internal mutex for safety. -TEST(ThreadSafetyTest, ConcurrentReads) { - using SharedToken = StaticToken; - auto& registry = TokenRegistry::get(); - - registry.remove("sharedToken"_hash); - - EXPECT_TRUE(registry.register_token(0)); - - constexpr int num_threads = 10; - constexpr int reads_per_thread = 100; - std::vector threads; - - for (int i = 0; i < num_threads; ++i) { - threads.emplace_back([&]() { - for (int j = 0; j < reads_per_thread; ++j) { - auto result = SharedToken::get(); - if (result) { - // Just verify we can read the value - EXPECT_GE(**result, 0); - } - } - }); - } - - for (auto& t : threads) { - t.join(); - } - - auto final_result = SharedToken::get(); - EXPECT_TRUE(final_result); - EXPECT_EQ(**final_result, 0); - - // Cleanup - registry.remove("sharedToken"_hash); -} - -TEST(ThreadSafetyTest, ConcurrentWrites) { - auto& registry = TokenRegistry::get(); - std::atomic success_count{0}; - std::atomic failure_count{0}; - - constexpr int num_threads = 10; - std::vector threads; - - for (int i = 0; i < num_threads; ++i) { - threads.emplace_back([&, i]() { - std::string name = "concurrent_" + std::to_string(i); - auto result = registry.register_dynamic(name, i); - if (result) { - success_count++; - } else { - failure_count++; - } - }); - } - - for (auto& t : threads) { - t.join(); - } - - // All registrations should succeed since names are different - EXPECT_EQ(success_count, num_threads); - EXPECT_EQ(failure_count, 0); - - // Cleanup - for (int i = 0; i < num_threads; ++i) { - std::string name = "concurrent_" + std::to_string(i); - registry.remove(name); - } -} - -TEST(ThreadSafetyTest, ReadDuringWrite) { - using ReadWriteToken = StaticToken; - auto& registry = TokenRegistry::get(); - - registry.remove("readWriteToken"_hash); - EXPECT_TRUE(registry.register_token(100)); - - std::atomic stop{false}; - std::atomic read_count{0}; - std::atomic write_count{0}; - - // Reader threads - std::vector readers; - for (int i = 0; i < 5; ++i) { - readers.emplace_back([&]() { - while (!stop) { - auto result = ReadWriteToken::get(); - if (result) { - read_count++; - } - } - }); - } - - // Writer thread - std::thread writer([&]() { - for (int i = 0; i < 100; ++i) { - // Re-register with new value - registry.remove("readWriteToken"_hash); - registry.register_token(100 + i); - write_count++; - std::this_thread::sleep_for(std::chrono::microseconds(100)); - } - }); - - writer.join(); - stop = true; - - for (auto& t : readers) { - t.join(); - } - - EXPECT_GT(read_count, 0); - EXPECT_EQ(write_count, 100); - - // Cleanup - registry.remove("readWriteToken"_hash); -} - -// ============================================================================= -// Test Suite 6: Size and Cleanup -// ============================================================================= - -TEST(TokenRegistryTest, SizeTracking) { - auto& registry = TokenRegistry::get(); - - size_t initial_size = registry.size(); - - std::string name1 = "sizeTest1"; - std::string name2 = "sizeTest2"; - - registry.remove(name1); - registry.remove(name2); - - EXPECT_TRUE(registry.register_dynamic(name1, 1)); - EXPECT_EQ(registry.size(), initial_size + 1); - - EXPECT_TRUE(registry.register_dynamic(name2, 2)); - EXPECT_EQ(registry.size(), initial_size + 2); - - registry.remove(name1); - EXPECT_EQ(registry.size(), initial_size + 1); - - registry.remove(name2); - EXPECT_EQ(registry.size(), initial_size); -} - -TEST(TokenRegistryTest, RemoveNonExistent) { - auto& registry = TokenRegistry::get(); - - std::string name = "doesNotExist"; - registry.remove(name); - - EXPECT_FALSE(registry.remove(name)); - EXPECT_FALSE(registry.remove("anotherNonExistent"_hash)); -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/ui/core/CMakeLists.txt b/ui/core/CMakeLists.txt index c5688531c..57c7f3917 100644 --- a/ui/core/CMakeLists.txt +++ b/ui/core/CMakeLists.txt @@ -10,7 +10,7 @@ add_subdirectory(material) # ============================================================ # STATIC library for linking into cfui.dll -# Note: token.hpp and color_scheme.h are header-only files +# Note: color_scheme.h is a header-only interface file add_library(cf_ui_core STATIC theme_manager.cpp ) diff --git a/ui/core/material/cfmaterial_fonttype.cpp b/ui/core/material/cfmaterial_fonttype.cpp index 5e9c3bbfd..03da6a8d6 100644 --- a/ui/core/material/cfmaterial_fonttype.cpp +++ b/ui/core/material/cfmaterial_fonttype.cpp @@ -15,20 +15,6 @@ namespace cf::ui::core { namespace detail { -// ============================================================================= -// System Font Detection -// ============================================================================= - -/** - * @brief 获取系统默认无衬线字体 - * - * 根据平台返回合适的默认字体: - * - Windows: Segoe UI - * - macOS: .SF NS Text (San Francisco) - * - Linux: Ubuntu - * - * @return QString 系统默认字体名称 - */ inline QString systemDefaultFont() { #ifdef Q_OS_WIN return "Segoe UI"; @@ -39,149 +25,73 @@ inline QString systemDefaultFont() { #endif } -/** - * @brief 创建指定配置的字体 - * - * @param sizeSp 字体大小(sp 单位) - * @param weight 字重 (QFont::Weight) - * @param italic 是否斜体 - * @return QFont 配置好的字体对象 - */ inline QFont createFont(int sizeSp, QFont::Weight weight, bool italic = false) { QFont font(systemDefaultFont()); font.setStyleHint(QFont::SansSerif); font.setWeight(weight); font.setItalic(italic); - font.setPointSizeF(sizeSp); // 使用 pointSize (Qt 会处理 DPI) + font.setPointSizeF(sizeSp); return font; } -// ============================================================================= -// Material Design 3 Type Scale Registration -// ============================================================================= - -/** - * @brief 注册所有默认字体到注册表 - * - * 使用 Material Design 3 规范的字体大小、字重和行高。 - * - * @param registry 目标注册表 - */ -inline void registerDefaultFonts(EmbeddedTokenRegistry& registry) { - namespace literals = ::cf::ui::core::token::literals; - - // ========================================================================= - // Display Styles - 用于英雄内容 - // ========================================================================= - registry.register_dynamic(literals::TYPOGRAPHY_DISPLAY_LARGE, - createFont(57, QFont::Normal)); - registry.register_dynamic(literals::TYPOGRAPHY_DISPLAY_MEDIUM, - createFont(45, QFont::Normal)); - registry.register_dynamic(literals::TYPOGRAPHY_DISPLAY_SMALL, - createFont(36, QFont::Normal)); - - // 行高 - registry.register_dynamic(literals::LINEHEIGHT_DISPLAY_LARGE, 64.0f); - registry.register_dynamic(literals::LINEHEIGHT_DISPLAY_MEDIUM, 52.0f); - registry.register_dynamic(literals::LINEHEIGHT_DISPLAY_SMALL, 44.0f); - - // ========================================================================= - // Headline Styles - 用于应用栏重要文本 - // ========================================================================= - registry.register_dynamic(literals::TYPOGRAPHY_HEADLINE_LARGE, - createFont(32, QFont::Normal)); - registry.register_dynamic(literals::TYPOGRAPHY_HEADLINE_MEDIUM, - createFont(28, QFont::Normal)); - registry.register_dynamic(literals::TYPOGRAPHY_HEADLINE_SMALL, - createFont(24, QFont::Normal)); - - // 行高 - registry.register_dynamic(literals::LINEHEIGHT_HEADLINE_LARGE, 40.0f); - registry.register_dynamic(literals::LINEHEIGHT_HEADLINE_MEDIUM, 36.0f); - registry.register_dynamic(literals::LINEHEIGHT_HEADLINE_SMALL, 32.0f); - - // ========================================================================= - // Title Styles - 用于分区标题 - // ========================================================================= - registry.register_dynamic(literals::TYPOGRAPHY_TITLE_LARGE, - createFont(22, QFont::Medium)); - registry.register_dynamic(literals::TYPOGRAPHY_TITLE_MEDIUM, - createFont(16, QFont::Medium)); - registry.register_dynamic(literals::TYPOGRAPHY_TITLE_SMALL, - createFont(14, QFont::Medium)); - - // 行高 - registry.register_dynamic(literals::LINEHEIGHT_TITLE_LARGE, 28.0f); - registry.register_dynamic(literals::LINEHEIGHT_TITLE_MEDIUM, 24.0f); - registry.register_dynamic(literals::LINEHEIGHT_TITLE_SMALL, 20.0f); - - // ========================================================================= - // Body Styles - 用于主要内容 - // ========================================================================= - registry.register_dynamic(literals::TYPOGRAPHY_BODY_LARGE, - createFont(16, QFont::Normal)); - registry.register_dynamic(literals::TYPOGRAPHY_BODY_MEDIUM, - createFont(14, QFont::Normal)); - registry.register_dynamic(literals::TYPOGRAPHY_BODY_SMALL, - createFont(12, QFont::Normal)); - - // 行高 - registry.register_dynamic(literals::LINEHEIGHT_BODY_LARGE, 24.0f); - registry.register_dynamic(literals::LINEHEIGHT_BODY_MEDIUM, 20.0f); - registry.register_dynamic(literals::LINEHEIGHT_BODY_SMALL, 16.0f); - - // ========================================================================= - // Label Styles - 用于次要信息 - // ========================================================================= - registry.register_dynamic(literals::TYPOGRAPHY_LABEL_LARGE, - createFont(14, QFont::Medium)); - registry.register_dynamic(literals::TYPOGRAPHY_LABEL_MEDIUM, - createFont(12, QFont::Medium)); - registry.register_dynamic(literals::TYPOGRAPHY_LABEL_SMALL, - createFont(11, QFont::Medium)); - - // 行高 - registry.register_dynamic(literals::LINEHEIGHT_LABEL_LARGE, 20.0f); - registry.register_dynamic(literals::LINEHEIGHT_LABEL_MEDIUM, 16.0f); - registry.register_dynamic(literals::LINEHEIGHT_LABEL_SMALL, 16.0f); -} - } // namespace detail -// ============================================================================= -// MaterialTypography Implementation -// ============================================================================= - MaterialTypography::MaterialTypography() { - font_cache_.reserve(15); - line_height_cache_.reserve(15); - - // 注册所有默认字体 - detail::registerDefaultFonts(registry_); + fonts_.reserve(15); + line_heights_.reserve(15); + + // Display Styles + fonts_["md.typography.displayLarge"] = detail::createFont(57, QFont::Normal); + fonts_["md.typography.displayMedium"] = detail::createFont(45, QFont::Normal); + fonts_["md.typography.displaySmall"] = detail::createFont(36, QFont::Normal); + line_heights_["md.lineHeight.displayLarge"] = 64.0f; + line_heights_["md.lineHeight.displayMedium"] = 52.0f; + line_heights_["md.lineHeight.displaySmall"] = 44.0f; + + // Headline Styles + fonts_["md.typography.headlineLarge"] = detail::createFont(32, QFont::Normal); + fonts_["md.typography.headlineMedium"] = detail::createFont(28, QFont::Normal); + fonts_["md.typography.headlineSmall"] = detail::createFont(24, QFont::Normal); + line_heights_["md.lineHeight.headlineLarge"] = 40.0f; + line_heights_["md.lineHeight.headlineMedium"] = 36.0f; + line_heights_["md.lineHeight.headlineSmall"] = 32.0f; + + // Title Styles + fonts_["md.typography.titleLarge"] = detail::createFont(22, QFont::Medium); + fonts_["md.typography.titleMedium"] = detail::createFont(16, QFont::Medium); + fonts_["md.typography.titleSmall"] = detail::createFont(14, QFont::Medium); + line_heights_["md.lineHeight.titleLarge"] = 28.0f; + line_heights_["md.lineHeight.titleMedium"] = 24.0f; + line_heights_["md.lineHeight.titleSmall"] = 20.0f; + + // Body Styles + fonts_["md.typography.bodyLarge"] = detail::createFont(16, QFont::Normal); + fonts_["md.typography.bodyMedium"] = detail::createFont(14, QFont::Normal); + fonts_["md.typography.bodySmall"] = detail::createFont(12, QFont::Normal); + line_heights_["md.lineHeight.bodyLarge"] = 24.0f; + line_heights_["md.lineHeight.bodyMedium"] = 20.0f; + line_heights_["md.lineHeight.bodySmall"] = 16.0f; + + // Label Styles + fonts_["md.typography.labelLarge"] = detail::createFont(14, QFont::Medium); + fonts_["md.typography.labelMedium"] = detail::createFont(12, QFont::Medium); + fonts_["md.typography.labelSmall"] = detail::createFont(11, QFont::Medium); + line_heights_["md.lineHeight.labelLarge"] = 20.0f; + line_heights_["md.lineHeight.labelMedium"] = 16.0f; + line_heights_["md.lineHeight.labelSmall"] = 16.0f; } QFont MaterialTypography::queryTargetFont(const char* name) { - // 检查缓存 - auto it = font_cache_.find(name); - if (it != font_cache_.end()) { + auto it = fonts_.find(name); + if (it != fonts_.end()) { return it->second; } - - // 从注册表获取 - auto result = registry_.get_dynamic(name); - if (result && *result) { - font_cache_[name] = **result; - return **result; - } - - // 回退到默认字体 QFont fallback(detail::systemDefaultFont()); fallback.setPointSizeF(14); return fallback; } float MaterialTypography::getLineHeight(const char* styleName) const { - // 将 md.typography.xxx 转换为 md.lineHeight.xxx std::string key = styleName; const std::string prefix = "md.typography."; const std::string lineHeightPrefix = "md.lineHeight."; @@ -191,20 +101,16 @@ float MaterialTypography::getLineHeight(const char* styleName) const { key.replace(0, prefix.length(), lineHeightPrefix); } - // 检查缓存 - auto it = line_height_cache_.find(key); - if (it != line_height_cache_.end()) { - return it->second; - } + auto it = line_heights_.find(key); + return it != line_heights_.end() ? it->second : 0.0f; +} - // 从注册表获取 - auto result = registry_.get_dynamic_const(key.c_str()); - if (result && *result) { - line_height_cache_[key] = **result; - return **result; - } +void MaterialTypography::setFont(const std::string& name, const QFont& font) { + fonts_[name] = font; +} - return 0.0f; +void MaterialTypography::setLineHeight(const std::string& name, float lineHeight) { + line_heights_[name] = lineHeight; } } // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_fonttype.h b/ui/core/material/cfmaterial_fonttype.h index 550fa1a28..e9d58e6d8 100644 --- a/ui/core/material/cfmaterial_fonttype.h +++ b/ui/core/material/cfmaterial_fonttype.h @@ -1,7 +1,7 @@ /** * @file cfmaterial_fonttype.h * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Typography with EmbeddedTokenRegistry + * @brief Material Design 3 Typography * @version 0.1 * @date 2026-02-26 * @@ -9,7 +9,6 @@ * * @details * Implements the complete Material Design 3 Type Scale system with 15 styles. - * Fonts are stored in an embedded registry for independent typography instances. * * Font selection: * - Windows: Segoe UI (Chinese fallback to Microsoft YaHei UI) @@ -19,163 +18,19 @@ #pragma once -#include #include #include #include #include "../export.h" -#include "base/hash/constexpr_fnv1a.hpp" #include "font_type.h" -#include "token.hpp" -#include "token/typography/cfmaterial_typography_token_literals.h" namespace cf::ui::core { -// ============================================================================= -// Typography Token Type Aliases - Material Typography System -// ============================================================================= -namespace tokens { -using namespace cf::ui::core::token::literals; - -// Display tokens -using DisplayLargeToken = StaticToken; -using DisplayMediumToken = StaticToken; -using DisplaySmallToken = StaticToken; - -// Headline tokens -using HeadlineLargeToken = StaticToken; -using HeadlineMediumToken = StaticToken; -using HeadlineSmallToken = StaticToken; - -// Title tokens -using TitleLargeToken = StaticToken; -using TitleMediumToken = StaticToken; -using TitleSmallToken = StaticToken; - -// Body tokens -using BodyLargeToken = StaticToken; -using BodyMediumToken = StaticToken; -using BodySmallToken = StaticToken; - -// Label tokens -using LabelLargeToken = StaticToken; -using LabelMediumToken = StaticToken; -using LabelSmallToken = StaticToken; - -} // namespace tokens - -// ============================================================================= -// Line Height Token Type Aliases -// ============================================================================= -namespace lineHeightTokens { -using namespace cf::ui::core::token::literals; - -using DisplayLargeLineHeight = StaticToken; -using DisplayMediumLineHeight = StaticToken; -using DisplaySmallLineHeight = StaticToken; - -using HeadlineLargeLineHeight = StaticToken; -using HeadlineMediumLineHeight = StaticToken; -using HeadlineSmallLineHeight = StaticToken; - -using TitleLargeLineHeight = StaticToken; -using TitleMediumLineHeight = StaticToken; -using TitleSmallLineHeight = StaticToken; - -using BodyLargeLineHeight = StaticToken; -using BodyMediumLineHeight = StaticToken; -using BodySmallLineHeight = StaticToken; - -using LabelLargeLineHeight = StaticToken; -using LabelMediumLineHeight = StaticToken; -using LabelSmallLineHeight = StaticToken; - -} // namespace lineHeightTokens - -// ============================================================================= -// Typography Group Structs - Material Typography System -// ============================================================================= - -/** - * @brief Display font styles group. - * - * Display styles are used for hero content with large emphasis. - * - * @since 0.1 - * @ingroup ui_core - */ -struct DisplayFonts { - tokens::DisplayLargeToken large; ///< 57sp, Regular, 64sp line-height - tokens::DisplayMediumToken medium; ///< 45sp, Regular, 52sp line-height - tokens::DisplaySmallToken small; ///< 36sp, Regular, 44sp line-height -}; - -/** - * @brief Headline font styles group. - * - * Headline styles are used for app bar important text. - * - * @since 0.1 - * @ingroup ui_core - */ -struct HeadlineFonts { - tokens::HeadlineLargeToken large; ///< 32sp, Regular, 40sp line-height - tokens::HeadlineMediumToken medium; ///< 28sp, Regular, 36sp line-height - tokens::HeadlineSmallToken small; ///< 24sp, Regular, 32sp line-height -}; - -/** - * @brief Title font styles group. - * - * Title styles are used for section headings. - * - * @since 0.1 - * @ingroup ui_core - */ -struct TitleFonts { - tokens::TitleLargeToken large; ///< 22sp, Medium, 28sp line-height - tokens::TitleMediumToken medium; ///< 16sp, Medium, 24sp line-height - tokens::TitleSmallToken small; ///< 14sp, Medium, 20sp line-height -}; - -/** - * @brief Body font styles group. - * - * Body styles are used for main content. - * - * @since 0.1 - * @ingroup ui_core - */ -struct BodyFonts { - tokens::BodyLargeToken large; ///< 16sp, Regular, 24sp line-height - tokens::BodyMediumToken medium; ///< 14sp, Regular, 20sp line-height - tokens::BodySmallToken small; ///< 12sp, Regular, 16sp line-height -}; - -/** - * @brief Label font styles group. - * - * Label styles are used for secondary information. - * - * @since 0.1 - * @ingroup ui_core - */ -struct LabelFonts { - tokens::LabelLargeToken large; ///< 14sp, Medium, 20sp line-height - tokens::LabelMediumToken medium; ///< 12sp, Medium, 16sp line-height - tokens::LabelSmallToken small; ///< 11sp, Medium, 16sp line-height -}; - -// ============================================================================= -// Material Typography - 实现 IFontType 接口 -// ============================================================================= - /** - * @brief Material Design 3 Typography with EmbeddedTokenRegistry. + * @brief Material Design 3 Typography. * * @details Implements the complete Material Design 3 Type Scale system with 15 styles. - * Fonts are stored in an embedded registry for independent typography instances. * * ### Type Scale Specifications * @@ -206,7 +61,7 @@ struct LabelFonts { * * @code * MaterialTypography typography; - * QFont font = typography.queryTargetFont("bodyLarge"); + * QFont font = typography.queryTargetFont("md.typography.bodyLarge"); * @endcode */ class CF_UI_EXPORT MaterialTypography : public IFontType { @@ -249,86 +104,30 @@ class CF_UI_EXPORT MaterialTypography : public IFontType { float getLineHeight(const char* styleName) const; /** - * @brief Access the embedded token registry. - * - * Provides direct access to the internal token registry for - * custom token manipulation. - * - * @return Reference to the EmbeddedTokenRegistry. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_core - */ - EmbeddedTokenRegistry& registry() { return registry_; } - - /** - * @brief Access the embedded token registry (const overload). - * - * Provides direct read-only access to the internal token registry. - * - * @return Const reference to the EmbeddedTokenRegistry. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_core - */ - const EmbeddedTokenRegistry& registry() const { return registry_; } - - // Font group accessors - 返回包含 Token 类型的结构体 - /** - * @brief 获取 Display 字体组 - * - * @return DisplayFonts 结构体 - * - * @since 0.1 - */ - [[nodiscard]] DisplayFonts display() const { return {}; } - - /** - * @brief 获取 Headline 字体组 - * - * @return HeadlineFonts 结构体 + * @brief Register a font by name. * - * @since 0.1 - */ - [[nodiscard]] HeadlineFonts headline() const { return {}; } - - /** - * @brief 获取 Title 字体组 - * - * @return TitleFonts 结构体 - * - * @since 0.1 - */ - [[nodiscard]] TitleFonts title() const { return {}; } - - /** - * @brief 获取 Body 字体组 - * - * @return BodyFonts 结构体 + * @param[in] name Font style name (e.g., "md.typography.displayLarge"). + * @param[in] font QFont value to register. * - * @since 0.1 + * @since 0.1 + * @ingroup ui_core */ - [[nodiscard]] BodyFonts body() const { return {}; } + void setFont(const std::string& name, const QFont& font); /** - * @brief 获取 Label 字体组 + * @brief Register a line height by name. * - * @return LabelFonts 结构体 + * @param[in] name Line height token name (e.g., "md.lineHeight.displayLarge"). + * @param[in] lineHeight Line height value in sp. * - * @since 0.1 + * @since 0.1 + * @ingroup ui_core */ - [[nodiscard]] LabelFonts label() const { return {}; } + void setLineHeight(const std::string& name, float lineHeight); private: - EmbeddedTokenRegistry registry_; - mutable std::unordered_map font_cache_; - mutable std::unordered_map line_height_cache_; + std::unordered_map fonts_; + std::unordered_map line_heights_; }; } // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_motion.cpp b/ui/core/material/cfmaterial_motion.cpp index a674bfb47..5be27f0d4 100644 --- a/ui/core/material/cfmaterial_motion.cpp +++ b/ui/core/material/cfmaterial_motion.cpp @@ -8,7 +8,7 @@ * @copyright Copyright (c) 2026 * * @details - * Implements MaterialMotionScheme with token registration and query methods. + * Implements MaterialMotionScheme with motion preset registration and query methods. * All motion presets follow Material Design 3 motion specifications. */ #include "cfmaterial_motion.h" @@ -16,75 +16,59 @@ namespace cf::ui::core { MaterialMotionScheme::MaterialMotionScheme() { - // Register all motion tokens - namespace literals = cf::ui::core::token::literals; - auto& r = registry_; - // Durations (Material Design 3 Motion specifications) - r.register_dynamic(literals::MOTION_SHORT_ENTER_DURATION, 200); - r.register_dynamic(literals::MOTION_SHORT_EXIT_DURATION, 150); - r.register_dynamic(literals::MOTION_MEDIUM_ENTER_DURATION, 300); - r.register_dynamic(literals::MOTION_MEDIUM_EXIT_DURATION, 250); - r.register_dynamic(literals::MOTION_LONG_ENTER_DURATION, 450); - r.register_dynamic(literals::MOTION_LONG_EXIT_DURATION, 400); - r.register_dynamic(literals::MOTION_STATE_CHANGE_DURATION, 200); - r.register_dynamic(literals::MOTION_RIPPLE_EXPAND_DURATION, 400); - r.register_dynamic(literals::MOTION_RIPPLE_FADE_DURATION, 150); + durations_["md.motion.shortEnter.duration"] = 200; + durations_["md.motion.shortExit.duration"] = 150; + durations_["md.motion.mediumEnter.duration"] = 300; + durations_["md.motion.mediumExit.duration"] = 250; + durations_["md.motion.longEnter.duration"] = 450; + durations_["md.motion.longExit.duration"] = 400; + durations_["md.motion.stateChange.duration"] = 200; + durations_["md.motion.rippleExpand.duration"] = 400; + durations_["md.motion.rippleFade.duration"] = 150; // Easing types using EType = cf::ui::base::Easing::Type; - r.register_dynamic(literals::MOTION_SHORT_ENTER_EASING, - static_cast(EType::EmphasizedDecelerate)); - r.register_dynamic(literals::MOTION_SHORT_EXIT_EASING, - static_cast(EType::EmphasizedAccelerate)); - r.register_dynamic(literals::MOTION_MEDIUM_ENTER_EASING, - static_cast(EType::EmphasizedDecelerate)); - r.register_dynamic(literals::MOTION_MEDIUM_EXIT_EASING, - static_cast(EType::EmphasizedAccelerate)); - r.register_dynamic(literals::MOTION_LONG_ENTER_EASING, - static_cast(EType::Emphasized)); - r.register_dynamic(literals::MOTION_LONG_EXIT_EASING, static_cast(EType::Emphasized)); - r.register_dynamic(literals::MOTION_STATE_CHANGE_EASING, - static_cast(EType::Standard)); - r.register_dynamic(literals::MOTION_RIPPLE_EXPAND_EASING, - static_cast(EType::Standard)); - r.register_dynamic(literals::MOTION_RIPPLE_FADE_EASING, static_cast(EType::Linear)); + easings_["md.motion.shortEnter.easing"] = static_cast(EType::EmphasizedDecelerate); + easings_["md.motion.shortExit.easing"] = static_cast(EType::EmphasizedAccelerate); + easings_["md.motion.mediumEnter.easing"] = static_cast(EType::EmphasizedDecelerate); + easings_["md.motion.mediumExit.easing"] = static_cast(EType::EmphasizedAccelerate); + easings_["md.motion.longEnter.easing"] = static_cast(EType::Emphasized); + easings_["md.motion.longExit.easing"] = static_cast(EType::Emphasized); + easings_["md.motion.stateChange.easing"] = static_cast(EType::Standard); + easings_["md.motion.rippleExpand.easing"] = static_cast(EType::Standard); + easings_["md.motion.rippleFade.easing"] = static_cast(EType::Linear); } int MaterialMotionScheme::queryDuration(const char* name) { - // Build the full duration token name std::string fullName = std::string("md.motion.") + name + ".duration"; - auto result = registry_.get_dynamic(fullName.c_str()); - return result ? **result : 200; // Default 200ms + auto it = durations_.find(fullName); + return it != durations_.end() ? it->second : 200; } int MaterialMotionScheme::queryEasing(const char* name) { - // Build the full easing token name std::string fullName = std::string("md.motion.") + name + ".easing"; - auto result = registry_.get_dynamic(fullName.c_str()); - return result ? **result : static_cast(cf::ui::base::Easing::Type::Standard); + auto it = easings_.find(fullName); + return it != easings_.end() ? it->second + : static_cast(cf::ui::base::Easing::Type::Standard); } int MaterialMotionScheme::queryDelay(const char* name) { - // Motion specs default to 0 delay - (void)name; // Suppress unused parameter warning + (void)name; return 0; } MotionSpec MaterialMotionScheme::getMotionSpec(const char* name) { - // Check cache first auto it = spec_cache_.find(name); if (it != spec_cache_.end()) { return it->second; } - // Build new spec MotionSpec spec; spec.durationMs = queryDuration(name); spec.easing = static_cast(queryEasing(name)); spec.delayMs = queryDelay(name); - // Cache for future queries spec_cache_[name] = spec; return spec; } diff --git a/ui/core/material/cfmaterial_motion.h b/ui/core/material/cfmaterial_motion.h index a8ecd514f..42420c983 100644 --- a/ui/core/material/cfmaterial_motion.h +++ b/ui/core/material/cfmaterial_motion.h @@ -1,7 +1,7 @@ /** * @file cfmaterial_motion.h * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Motion Scheme with EmbeddedTokenRegistry + * @brief Material Design 3 Motion Scheme * @version 0.1 * @date 2026-02-26 * @@ -9,8 +9,7 @@ * * @details * Implements the complete Material Design 3 motion system with duration, - * easing, and delay specifications. Motion specs are stored in an embedded - * registry for independent scheme instances. + * easing, and delay specifications. */ #pragma once #include @@ -18,45 +17,11 @@ #include #include "../motion_spec.h" -#include "../token.hpp" -#include "../token/motion/cfmaterial_motion_token_literals.h" #include "base/easing.h" -#include "base/hash/constexpr_fnv1a.hpp" #include "export.h" namespace cf::ui::core { -// ============================================================================= -// Motion Token Type Aliases - Material Motion System -// ============================================================================= -namespace tokens { -using namespace cf::ui::core::token::literals; - -// Duration tokens -using ShortEnterDurationToken = StaticToken; -using ShortExitDurationToken = StaticToken; -using MediumEnterDurationToken = StaticToken; -using MediumExitDurationToken = StaticToken; -using LongEnterDurationToken = StaticToken; -using LongExitDurationToken = StaticToken; -using StateChangeDurationToken = StaticToken; -using RippleExpandDurationToken = - StaticToken; -using RippleFadeDurationToken = StaticToken; - -// Easing tokens (stored as int enum values) -using ShortEnterEasingToken = StaticToken; -using ShortExitEasingToken = StaticToken; -using MediumEnterEasingToken = StaticToken; -using MediumExitEasingToken = StaticToken; -using LongEnterEasingToken = StaticToken; -using LongExitEasingToken = StaticToken; -using StateChangeEasingToken = StaticToken; -using RippleExpandEasingToken = StaticToken; -using RippleFadeEasingToken = StaticToken; - -} // namespace tokens - // ============================================================================= // Motion Data Structure // ============================================================================= @@ -65,8 +30,7 @@ using RippleFadeEasingToken = StaticToken durations_; + std::unordered_map easings_; mutable std::unordered_map spec_cache_; }; diff --git a/ui/core/material/cfmaterial_radius_scale.cpp b/ui/core/material/cfmaterial_radius_scale.cpp index 8b0098f9f..a76446d38 100644 --- a/ui/core/material/cfmaterial_radius_scale.cpp +++ b/ui/core/material/cfmaterial_radius_scale.cpp @@ -14,39 +14,22 @@ namespace cf::ui::core { MaterialRadiusScale::MaterialRadiusScale() { - registerDefaultCorners(); -} - -void MaterialRadiusScale::registerDefaultCorners() { - namespace literals = cf::ui::core::token::literals; - auto& r = registry_; - - // 注册默认值(Material Design 3 规范) - r.register_dynamic(literals::CORNER_NONE, 0.0f); - r.register_dynamic(literals::CORNER_EXTRA_SMALL, 4.0f); - r.register_dynamic(literals::CORNER_SMALL, 8.0f); - r.register_dynamic(literals::CORNER_MEDIUM, 12.0f); - r.register_dynamic(literals::CORNER_LARGE, 16.0f); - r.register_dynamic(literals::CORNER_EXTRA_LARGE, 28.0f); - r.register_dynamic(literals::CORNER_EXTRA_EXTRA_LARGE, 32.0f); + radii_["md.shape.cornerNone"] = 0.0f; + radii_["md.shape.cornerExtraSmall"] = 4.0f; + radii_["md.shape.cornerSmall"] = 8.0f; + radii_["md.shape.cornerMedium"] = 12.0f; + radii_["md.shape.cornerLarge"] = 16.0f; + radii_["md.shape.cornerExtraLarge"] = 28.0f; + radii_["md.shape.cornerExtraExtraLarge"] = 32.0f; } float MaterialRadiusScale::queryRadiusScale(const char* name) { - // 先查缓存 - auto it = radius_cache_.find(name); - if (it != radius_cache_.end()) { - return it->second; - } - - // 从注册表获取 - auto result = registry_.get_dynamic(name); - if (result && *result) { - auto [iter, inserted] = radius_cache_.emplace(name, **result); - return iter->second; - } + auto it = radii_.find(name); + return it != radii_.end() ? it->second : 0.0f; +} - // 默认回退值 - return 0.0f; +void MaterialRadiusScale::setRadius(const std::string& name, float radius) { + radii_[name] = radius; } } // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_radius_scale.h b/ui/core/material/cfmaterial_radius_scale.h index 887401132..1e9e2efdd 100644 --- a/ui/core/material/cfmaterial_radius_scale.h +++ b/ui/core/material/cfmaterial_radius_scale.h @@ -1,7 +1,7 @@ /** * @file cfmaterial_radius_scale.h * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Radius Scale with EmbeddedTokenRegistry + * @brief Material Design 3 Radius Scale * @version 0.1 * @date 2026-02-26 * @@ -9,7 +9,6 @@ * * @details * Implements the complete Material Design 3 corner radius system with 7 scales. - * Radii are stored in an embedded registry for independent radius scale instances. */ #pragma once @@ -18,50 +17,26 @@ #include #include "../export.h" -#include "base/hash/constexpr_fnv1a.hpp" #include "radius_scale.h" -#include "token.hpp" -#include "token/radius_scale/cfmaterial_radius_scale_literals.h" namespace cf::ui::core { -// ============================================================================= -// Radius Scale Token Type Aliases - Material Radius Scale System -// ============================================================================= -namespace tokens { -using namespace cf::ui::core::token::literals; - -using CornerNoneToken = StaticToken; -using CornerExtraSmallToken = StaticToken; -using CornerSmallToken = StaticToken; -using CornerMediumToken = StaticToken; -using CornerLargeToken = StaticToken; -using CornerExtraLargeToken = StaticToken; -using CornerExtraExtraLargeToken = StaticToken; - -} // namespace tokens - -// ============================================================================= -// Material Radius Scale - 实现 IRadiusScale 接口 -// ============================================================================= - /** - * @brief Material Design 3 Radius Scale with EmbeddedTokenRegistry. + * @brief Material Design 3 Radius Scale. * * @details Implements the complete Material Design 3 Corner Radius system with 7 scales. - * Radii are stored in an embedded registry for independent radius scale instances. * * ### Radius Scale Specifications * * | Token | Name | Value (dp) | Usage | * |-------|------|------------|-------| - * | CORNER_NONE | cornerNone | 0dp | No corner radius | - * | CORNER_EXTRA_SMALL | cornerExtraSmall | 4dp | Chips, small cards | - * | CORNER_SMALL | cornerSmall | 8dp | Text fields, checkboxes | - * | CORNER_MEDIUM | cornerMedium | 12dp | Cards | - * | CORNER_LARGE | cornerLarge | 16dp | Alert dialogs | - * | CORNER_EXTRA_LARGE | cornerExtraLarge | 28dp | FAB, modals | - * | CORNER_EXTRA_EXTRA_LARGE | cornerExtraExtraLarge | 32dp | Drawers | + * | cornerNone | cornerNone | 0dp | No corner radius | + * | cornerExtraSmall | cornerExtraSmall | 4dp | Chips, small cards | + * | cornerSmall | cornerSmall | 8dp | Text fields, checkboxes | + * | cornerMedium | cornerMedium | 12dp | Cards | + * | cornerLarge | cornerLarge | 16dp | Alert dialogs | + * | cornerExtraLarge | cornerExtraLarge | 28dp | FAB, modals | + * | cornerExtraExtraLarge | cornerExtraExtraLarge | 32dp | Drawers | * * @note None * @warning None @@ -71,7 +46,7 @@ using CornerExtraExtraLargeToken = StaticToken radius_cache_; + std::unordered_map radii_; }; } // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_scheme.cpp b/ui/core/material/cfmaterial_scheme.cpp index 44454d6e5..fd2a78b1e 100644 --- a/ui/core/material/cfmaterial_scheme.cpp +++ b/ui/core/material/cfmaterial_scheme.cpp @@ -14,38 +14,32 @@ namespace cf::ui::core { MaterialColorScheme::MaterialColorScheme() { - color_cache_.reserve(32); + colors_.reserve(32); } QColor& MaterialColorScheme::queryExpectedColor(const char* name) { - auto it = color_cache_.find(name); - if (it != color_cache_.end()) { + auto it = colors_.find(name); + if (it != colors_.end()) { return it->second; } - - auto result = registry_.get_dynamic(name); - if (result && *result) { - auto [iter, inserted] = color_cache_.emplace(name, (*result)->native_color()); - return iter->second; - } - - // Fallback color static QColor fallback(Qt::black); return fallback; } QColor MaterialColorScheme::queryColor(const char* name) const { - auto it = color_cache_.find(name); - if (it != color_cache_.end()) { + auto it = colors_.find(name); + if (it != colors_.end()) { return it->second; } + return QColor(Qt::black); +} - auto result = registry_.get_dynamic_const(name); - if (result && *result) { - return (*result)->native_color(); - } +void MaterialColorScheme::setColor(const std::string& name, const QColor& color) { + colors_[name] = color; +} - return QColor(Qt::black); +bool MaterialColorScheme::hasColor(const std::string& name) const { + return colors_.find(name) != colors_.end(); } -} // namespace cf::ui::core \ No newline at end of file +} // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_scheme.h b/ui/core/material/cfmaterial_scheme.h index 06aebeb0e..3f6f5d958 100644 --- a/ui/core/material/cfmaterial_scheme.h +++ b/ui/core/material/cfmaterial_scheme.h @@ -1,7 +1,7 @@ /** * @file cfmaterial_scheme.h * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Color Scheme with EmbeddedTokenRegistry + * @brief Material Design 3 Color Scheme * @version 0.1 * @date 2026-02-25 * @@ -9,185 +9,20 @@ * */ #pragma once -#include #include #include #include #include "../../export.h" -#include "base/color.h" -#include "base/hash/constexpr_fnv1a.hpp" #include "color_scheme.h" -#include "token.hpp" -#include "token/material_scheme/cfmaterial_token_literals.h" namespace cf::ui::core { -using CFColor = cf::ui::base::CFColor; - -// ============================================================================= -// Color Token Type Aliases - Material Color System -// ============================================================================= -namespace tokens { -using namespace cf::ui::core::token::literals; - -// Primary color tokens -using PrimaryToken = StaticToken; -using OnPrimaryToken = StaticToken; -using PrimaryContainerToken = StaticToken; -using OnPrimaryContainerToken = StaticToken; - -// Secondary color tokens -using SecondaryToken = StaticToken; -using OnSecondaryToken = StaticToken; -using SecondaryContainerToken = StaticToken; -using OnSecondaryContainerToken = StaticToken; - -// Tertiary color tokens -using TertiaryToken = StaticToken; -using OnTertiaryToken = StaticToken; -using TertiaryContainerToken = StaticToken; -using OnTertiaryContainerToken = StaticToken; - -// Error color tokens -using ErrorToken = StaticToken; -using OnErrorToken = StaticToken; -using ErrorContainerToken = StaticToken; -using OnErrorContainerToken = StaticToken; - -// Surface color tokens -using BackgroundToken = StaticToken; -using OnBackgroundToken = StaticToken; -using SurfaceToken = StaticToken; -using OnSurfaceToken = StaticToken; -using SurfaceVariantToken = StaticToken; -using OnSurfaceVariantToken = StaticToken; -using OutlineToken = StaticToken; -using OutlineVariantToken = StaticToken; - -// Utility color tokens -using ShadowToken = StaticToken; -using ScrimToken = StaticToken; -using InverseSurfaceToken = StaticToken; -using InverseOnSurfaceToken = StaticToken; -using InversePrimaryToken = StaticToken; - -} // namespace tokens - -// ============================================================================= -// Color Group Structs - Material Color System -// ============================================================================= - /** - * @brief Primary color group. - * - * Primary colors are used for key components throughout the UI, - * such as prominent buttons, active states, and tonal surfaces. - * - * @since 0.1 - * @ingroup ui_core - */ -struct PrimaryColors { - tokens::PrimaryToken primary; - tokens::OnPrimaryToken onPrimary; - tokens::PrimaryContainerToken primaryContainer; - tokens::OnPrimaryContainerToken onPrimaryContainer; -}; - -/** - * @brief Secondary color group. - * - * Secondary colors provide alternative ways to distinguish components. - * They are used for less prominent components and for accents. - * - * @since 0.1 - * @ingroup ui_core - */ -struct SecondaryColors { - tokens::SecondaryToken secondary; - tokens::OnSecondaryToken onSecondary; - tokens::SecondaryContainerToken secondaryContainer; - tokens::OnSecondaryContainerToken onSecondaryContainer; -}; - -/** - * @brief Tertiary color group. - * - * Tertiary colors are used to derive contrasting accents that can be used - * for balance and to express unique branding. - * - * @since 0.1 - * @ingroup ui_core - */ -struct TertiaryColors { - tokens::TertiaryToken tertiary; - tokens::OnTertiaryToken onTertiary; - tokens::TertiaryContainerToken tertiaryContainer; - tokens::OnTertiaryContainerToken onTertiaryContainer; -}; - -/** - * @brief Error color group. - * - * Error colors are used for error states, destructive actions, - * and validation feedback. - * - * @since 0.1 - * @ingroup ui_core - */ -struct ErrorColors { - tokens::ErrorToken error; - tokens::OnErrorToken onError; - tokens::ErrorContainerToken errorContainer; - tokens::OnErrorContainerToken onErrorContainer; -}; - -/** - * @brief Background and Surface color group. - * - * These colors define the base surfaces of the UI, including - * backgrounds, surfaces, and their variants. - * - * @since 0.1 - * @ingroup ui_core - */ -struct SurfaceColors { - tokens::BackgroundToken background; - tokens::OnBackgroundToken onBackground; - tokens::SurfaceToken surface; - tokens::OnSurfaceToken onSurface; - tokens::SurfaceVariantToken surfaceVariant; - tokens::OnSurfaceVariantToken onSurfaceVariant; - tokens::OutlineToken outline; - tokens::OutlineVariantToken outlineVariant; -}; - -/** - * @brief Utility color group. - * - * Utility colors support elevated surfaces and inverse states - * for special UI scenarios. - * - * @since 0.1 - * @ingroup ui_core - */ -struct UtilityColors { - tokens::ShadowToken shadow; - tokens::ScrimToken scrim; - tokens::InverseSurfaceToken inverseSurface; - tokens::InverseOnSurfaceToken inverseOnSurface; - tokens::InversePrimaryToken inversePrimary; -}; - -// ============================================================================= -// Material Color Scheme -// ============================================================================= - -/** - * @brief Material Design 3 Color Scheme with EmbeddedTokenRegistry. + * @brief Material Design 3 Color Scheme. * * @details Implements the complete Material Design 3 color system with 26 color tokens. - * Colors are stored in an embedded registry for independent scheme instances. + * Colors are stored in a flat map keyed by string token names. * * Factory functions are available in the cf::ui::core::material namespace. * @@ -227,7 +62,7 @@ class CF_UI_EXPORT MaterialColorScheme : public ICFColorScheme { * * @throws None. * - * @note Returns a cached reference that is valid for the lifetime + * @note Returns a reference that is valid for the lifetime * of the color scheme. * * @warning None. @@ -246,7 +81,7 @@ class CF_UI_EXPORT MaterialColorScheme : public ICFColorScheme { * * @throws None. * - * @note Returns a cached copy or default color if not found. + * @note Returns a default black color if not found. * * @warning None. * @@ -256,48 +91,30 @@ class CF_UI_EXPORT MaterialColorScheme : public ICFColorScheme { QColor queryColor(const char* name) const; /** - * @brief Access the embedded token registry. - * - * @return Reference to the EmbeddedTokenRegistry. - * - * @throws None. + * @brief Register a color by name. * - * @note Provides direct access to internal token storage. - * - * @warning Modifying tokens directly may affect color scheme behavior. + * @param[in] name Color token name (e.g., "md.primary"). + * @param[in] color Color value to register. * * @since 0.1 * @ingroup ui_core */ - EmbeddedTokenRegistry& registry() { return registry_; } + void setColor(const std::string& name, const QColor& color); /** - * @brief Access the embedded token registry (const overload). - * - * @return Const reference to the EmbeddedTokenRegistry. - * - * @throws None. + * @brief Check if a color token exists. * - * @note Read-only access to internal token storage. + * @param[in] name Color token name. * - * @warning None. + * @return true if the token exists. * * @since 0.1 * @ingroup ui_core */ - const EmbeddedTokenRegistry& registry() const { return registry_; } - - // Color group accessors - return structs with token types - [[nodiscard]] PrimaryColors primary() const { return {}; } - [[nodiscard]] SecondaryColors secondary() const { return {}; } - [[nodiscard]] TertiaryColors tertiary() const { return {}; } - [[nodiscard]] ErrorColors error() const { return {}; } - [[nodiscard]] SurfaceColors surface() const { return {}; } - [[nodiscard]] UtilityColors utility() const { return {}; } + bool hasColor(const std::string& name) const; private: - EmbeddedTokenRegistry registry_; - mutable std::unordered_map color_cache_; + std::unordered_map colors_; }; } // namespace cf::ui::core diff --git a/ui/core/material/material_factory.cpp b/ui/core/material/material_factory.cpp index b6e50cace..5b68a156c 100644 --- a/ui/core/material/material_factory.cpp +++ b/ui/core/material/material_factory.cpp @@ -20,11 +20,6 @@ #include "cfmaterial_fonttype.h" #include "cfmaterial_motion.h" #include "cfmaterial_radius_scale.h" -#include "token/material_scheme/cfmaterial_token_literals.h" - -// Type aliases to avoid namespace lookup issues -using CFColor = ::cf::ui::base::CFColor; - namespace cf::ui::core::material { namespace detail { @@ -123,55 +118,46 @@ struct DarkColors { // ============================================================================= template inline void registerAllColors(MaterialColorScheme& scheme) { - auto& r = scheme.registry(); - namespace literals = ::cf::ui::core::token::literals; - using CFColor = ::cf::ui::base::CFColor; - // Primary colors - r.register_dynamic(literals::PRIMARY, CFColor(ColorDefs::primary)); - r.register_dynamic(literals::ON_PRIMARY, CFColor(ColorDefs::onPrimary)); - r.register_dynamic(literals::PRIMARY_CONTAINER, CFColor(ColorDefs::primaryContainer)); - r.register_dynamic(literals::ON_PRIMARY_CONTAINER, - CFColor(ColorDefs::onPrimaryContainer)); + scheme.setColor("md.primary", QColor(ColorDefs::primary)); + scheme.setColor("md.onPrimary", QColor(ColorDefs::onPrimary)); + scheme.setColor("md.primaryContainer", QColor(ColorDefs::primaryContainer)); + scheme.setColor("md.onPrimaryContainer", QColor(ColorDefs::onPrimaryContainer)); // Secondary colors - r.register_dynamic(literals::SECONDARY, CFColor(ColorDefs::secondary)); - r.register_dynamic(literals::ON_SECONDARY, CFColor(ColorDefs::onSecondary)); - r.register_dynamic(literals::SECONDARY_CONTAINER, - CFColor(ColorDefs::secondaryContainer)); - r.register_dynamic(literals::ON_SECONDARY_CONTAINER, - CFColor(ColorDefs::onSecondaryContainer)); + scheme.setColor("md.secondary", QColor(ColorDefs::secondary)); + scheme.setColor("md.onSecondary", QColor(ColorDefs::onSecondary)); + scheme.setColor("md.secondaryContainer", QColor(ColorDefs::secondaryContainer)); + scheme.setColor("md.onSecondaryContainer", QColor(ColorDefs::onSecondaryContainer)); // Tertiary colors - r.register_dynamic(literals::TERTIARY, CFColor(ColorDefs::tertiary)); - r.register_dynamic(literals::ON_TERTIARY, CFColor(ColorDefs::onTertiary)); - r.register_dynamic(literals::TERTIARY_CONTAINER, - CFColor(ColorDefs::tertiaryContainer)); - r.register_dynamic(literals::ON_TERTIARY_CONTAINER, - CFColor(ColorDefs::onTertiaryContainer)); + scheme.setColor("md.tertiary", QColor(ColorDefs::tertiary)); + scheme.setColor("md.onTertiary", QColor(ColorDefs::onTertiary)); + scheme.setColor("md.tertiaryContainer", QColor(ColorDefs::tertiaryContainer)); + scheme.setColor("md.onTertiaryContainer", QColor(ColorDefs::onTertiaryContainer)); // Error colors - r.register_dynamic(literals::ERROR, CFColor(ColorDefs::error)); - r.register_dynamic(literals::ON_ERROR, CFColor(ColorDefs::onError)); - r.register_dynamic(literals::ERROR_CONTAINER, CFColor(ColorDefs::errorContainer)); - r.register_dynamic(literals::ON_ERROR_CONTAINER, CFColor(ColorDefs::onErrorContainer)); + scheme.setColor("md.error", QColor(ColorDefs::error)); + scheme.setColor("md.onError", QColor(ColorDefs::onError)); + scheme.setColor("md.errorContainer", QColor(ColorDefs::errorContainer)); + scheme.setColor("md.onErrorContainer", QColor(ColorDefs::onErrorContainer)); // Surface colors - r.register_dynamic(literals::BACKGROUND, CFColor(ColorDefs::background)); - r.register_dynamic(literals::ON_BACKGROUND, CFColor(ColorDefs::onBackground)); - r.register_dynamic(literals::SURFACE, CFColor(ColorDefs::surface)); - r.register_dynamic(literals::ON_SURFACE, CFColor(ColorDefs::onSurface)); - r.register_dynamic(literals::SURFACE_VARIANT, CFColor(ColorDefs::surfaceVariant)); - r.register_dynamic(literals::ON_SURFACE_VARIANT, CFColor(ColorDefs::onSurfaceVariant)); - r.register_dynamic(literals::OUTLINE, CFColor(ColorDefs::outline)); - r.register_dynamic(literals::OUTLINE_VARIANT, CFColor(ColorDefs::outlineVariant)); + scheme.setColor("md.background", QColor(ColorDefs::background)); + scheme.setColor("md.onBackground", QColor(ColorDefs::onBackground)); + scheme.setColor("md.surface", QColor(ColorDefs::surface)); + scheme.setColor("md.onSurface", QColor(ColorDefs::onSurface)); + scheme.setColor("md.surfaceVariant", QColor(ColorDefs::surfaceVariant)); + scheme.setColor("md.onSurfaceVariant", QColor(ColorDefs::onSurfaceVariant)); + scheme.setColor("md.outline", QColor(ColorDefs::outline)); + scheme.setColor("md.outlineVariant", QColor(ColorDefs::outlineVariant)); // Utility colors - r.register_dynamic(literals::SHADOW, CFColor(ColorDefs::shadow)); - r.register_dynamic(literals::SCRIM, CFColor(ColorDefs::scrim)); - r.register_dynamic(literals::INVERSE_SURFACE, CFColor(ColorDefs::inverseSurface)); - r.register_dynamic(literals::INVERSE_ON_SURFACE, CFColor(ColorDefs::inverseOnSurface)); - r.register_dynamic(literals::INVERSE_PRIMARY, CFColor(ColorDefs::inversePrimary)); + scheme.setColor("md.shadow", QColor(ColorDefs::shadow)); + scheme.setColor("md.scrim", QColor(ColorDefs::scrim)); + scheme.setColor("md.inverseSurface", QColor(ColorDefs::inverseSurface)); + scheme.setColor("md.inverseOnSurface", QColor(ColorDefs::inverseOnSurface)); + scheme.setColor("md.inversePrimary", QColor(ColorDefs::inversePrimary)); } } // namespace detail @@ -220,7 +206,6 @@ Result fromJson(const QByteArray& json, bool isDark) { } MaterialColorScheme scheme; - auto& r = scheme.registry(); // Map of MD3 color names to registry keys const std::unordered_map colorMapping = { @@ -264,144 +249,135 @@ Result fromJson(const QByteArray& json, bool isDark) { MaterialSchemeError::Kind::InvalidColorFormat, "Invalid color format for " + key + ": " + colorStr.toStdString()}); } - r.register_dynamic(registryKey, CFColor(colorStr)); + scheme.setColor(registryKey, QColor(colorStr)); } } return scheme; } -MaterialColorScheme fromKeyColor(CFColor keyColor, bool isDark) { +MaterialColorScheme fromKeyColor(cf::ui::base::CFColor keyColor, bool isDark) { using ::cf::ui::base::tonalPalette; MaterialColorScheme scheme; - auto& r = scheme.registry(); // Generate tonal palette from key color - QList tonal = tonalPalette(keyColor); + QList tonal = tonalPalette(keyColor); // Helper to get color from tonal palette by index - auto getTone = [&tonal](int index) -> CFColor { - // tonalPalette returns 13 colors: 0, 10, 20, ..., 90, 95, 99, 100 - // Map index 0-12 to these tones + auto getTone = [&tonal](int index) -> cf::ui::base::CFColor { if (index >= 0 && index < tonal.size()) { return tonal[index]; } - return CFColor("#808080"); // Fallback gray + return cf::ui::base::CFColor("#808080"); // Fallback gray }; if (!isDark) { // Light scheme generation - r.register_dynamic("md.primary", getTone(4)); // Tone 40 - r.register_dynamic("md.onPrimary", CFColor("#FFFFFF")); - r.register_dynamic("md.primaryContainer", getTone(9)); // Tone 90 - r.register_dynamic("md.onPrimaryContainer", getTone(0)); // Tone 0 + scheme.setColor("md.primary", getTone(4).native_color()); + scheme.setColor("md.onPrimary", QColor("#FFFFFF")); + scheme.setColor("md.primaryContainer", getTone(9).native_color()); + scheme.setColor("md.onPrimaryContainer", getTone(0).native_color()); // Secondary: Use complementary hue - CFColor secondaryKey(std::fmod(keyColor.hue() + 60.0f, 360.0f), keyColor.chroma() * 0.8f, - keyColor.tone()); - QList secondaryPalette = tonalPalette(secondaryKey); - r.register_dynamic("md.secondary", secondaryPalette[5]); - r.register_dynamic("md.onSecondary", CFColor("#FFFFFF")); - r.register_dynamic("md.secondaryContainer", secondaryPalette[9]); - r.register_dynamic("md.onSecondaryContainer", secondaryPalette[0]); + cf::ui::base::CFColor secondaryKey(std::fmod(keyColor.hue() + 60.0f, 360.0f), + keyColor.chroma() * 0.8f, keyColor.tone()); + QList secondaryPalette = tonalPalette(secondaryKey); + scheme.setColor("md.secondary", secondaryPalette[5].native_color()); + scheme.setColor("md.onSecondary", QColor("#FFFFFF")); + scheme.setColor("md.secondaryContainer", secondaryPalette[9].native_color()); + scheme.setColor("md.onSecondaryContainer", secondaryPalette[0].native_color()); // Tertiary: Use complementary hue - CFColor tertiaryKey(std::fmod(keyColor.hue() + 120.0f, 360.0f), keyColor.chroma() * 0.6f, - keyColor.tone()); - QList tertiaryPalette = tonalPalette(tertiaryKey); - r.register_dynamic("md.tertiary", tertiaryPalette[5]); - r.register_dynamic("md.onTertiary", CFColor("#FFFFFF")); - r.register_dynamic("md.tertiaryContainer", tertiaryPalette[9]); - r.register_dynamic("md.onTertiaryContainer", tertiaryPalette[0]); + cf::ui::base::CFColor tertiaryKey(std::fmod(keyColor.hue() + 120.0f, 360.0f), + keyColor.chroma() * 0.6f, keyColor.tone()); + QList tertiaryPalette = tonalPalette(tertiaryKey); + scheme.setColor("md.tertiary", tertiaryPalette[5].native_color()); + scheme.setColor("md.onTertiary", QColor("#FFFFFF")); + scheme.setColor("md.tertiaryContainer", tertiaryPalette[9].native_color()); + scheme.setColor("md.onTertiaryContainer", tertiaryPalette[0].native_color()); // Error colors (fixed red-based) - r.register_dynamic("md.error", CFColor("#B3261E")); - r.register_dynamic("md.onError", CFColor("#FFFFFF")); - r.register_dynamic("md.errorContainer", CFColor("#F9DEDC")); - r.register_dynamic("md.onErrorContainer", CFColor("#410E0B")); + scheme.setColor("md.error", QColor("#B3261E")); + scheme.setColor("md.onError", QColor("#FFFFFF")); + scheme.setColor("md.errorContainer", QColor("#F9DEDC")); + scheme.setColor("md.onErrorContainer", QColor("#410E0B")); // Surface colors - r.register_dynamic("md.background", CFColor("#FFFBFE")); - r.register_dynamic("md.onBackground", CFColor("#1C1B1F")); - r.register_dynamic("md.surface", CFColor("#FFFBFE")); - r.register_dynamic("md.onSurface", CFColor("#1C1B1F")); - r.register_dynamic("md.surfaceVariant", getTone(11)); - r.register_dynamic("md.onSurfaceVariant", getTone(2)); - r.register_dynamic("md.outline", getTone(7)); - r.register_dynamic("md.outlineVariant", getTone(10)); + scheme.setColor("md.background", QColor("#FFFBFE")); + scheme.setColor("md.onBackground", QColor("#1C1B1F")); + scheme.setColor("md.surface", QColor("#FFFBFE")); + scheme.setColor("md.onSurface", QColor("#1C1B1F")); + scheme.setColor("md.surfaceVariant", getTone(11).native_color()); + scheme.setColor("md.onSurfaceVariant", getTone(2).native_color()); + scheme.setColor("md.outline", getTone(7).native_color()); + scheme.setColor("md.outlineVariant", getTone(10).native_color()); // Utility colors - r.register_dynamic("md.shadow", CFColor("#000000")); - r.register_dynamic("md.scrim", CFColor("#000000")); - r.register_dynamic("md.inverseSurface", CFColor("#313033")); - r.register_dynamic("md.inverseOnSurface", CFColor("#F4EFF4")); - r.register_dynamic("md.inversePrimary", getTone(8)); + scheme.setColor("md.shadow", QColor("#000000")); + scheme.setColor("md.scrim", QColor("#000000")); + scheme.setColor("md.inverseSurface", QColor("#313033")); + scheme.setColor("md.inverseOnSurface", QColor("#F4EFF4")); + scheme.setColor("md.inversePrimary", getTone(8).native_color()); } else { // Dark scheme generation - r.register_dynamic("md.primary", getTone(8)); // Tone 80 - r.register_dynamic("md.onPrimary", getTone(0)); // Tone 0 - r.register_dynamic("md.primaryContainer", getTone(3)); // Tone 30 - r.register_dynamic("md.onPrimaryContainer", getTone(11)); // Tone 95 + scheme.setColor("md.primary", getTone(8).native_color()); + scheme.setColor("md.onPrimary", getTone(0).native_color()); + scheme.setColor("md.primaryContainer", getTone(3).native_color()); + scheme.setColor("md.onPrimaryContainer", getTone(11).native_color()); // Secondary - CFColor secondaryKey(std::fmod(keyColor.hue() + 60.0f, 360.0f), keyColor.chroma() * 0.8f, - keyColor.tone()); - QList secondaryPalette = tonalPalette(secondaryKey); - r.register_dynamic("md.secondary", secondaryPalette[8]); - r.register_dynamic("md.onSecondary", secondaryPalette[0]); - r.register_dynamic("md.secondaryContainer", secondaryPalette[3]); - r.register_dynamic("md.onSecondaryContainer", secondaryPalette[11]); + cf::ui::base::CFColor secondaryKey(std::fmod(keyColor.hue() + 60.0f, 360.0f), + keyColor.chroma() * 0.8f, keyColor.tone()); + QList secondaryPalette = tonalPalette(secondaryKey); + scheme.setColor("md.secondary", secondaryPalette[8].native_color()); + scheme.setColor("md.onSecondary", secondaryPalette[0].native_color()); + scheme.setColor("md.secondaryContainer", secondaryPalette[3].native_color()); + scheme.setColor("md.onSecondaryContainer", secondaryPalette[11].native_color()); // Tertiary - CFColor tertiaryKey(std::fmod(keyColor.hue() + 120.0f, 360.0f), keyColor.chroma() * 0.6f, - keyColor.tone()); - QList tertiaryPalette = tonalPalette(tertiaryKey); - r.register_dynamic("md.tertiary", tertiaryPalette[8]); - r.register_dynamic("md.onTertiary", tertiaryPalette[0]); - r.register_dynamic("md.tertiaryContainer", tertiaryPalette[3]); - r.register_dynamic("md.onTertiaryContainer", tertiaryPalette[11]); + cf::ui::base::CFColor tertiaryKey(std::fmod(keyColor.hue() + 120.0f, 360.0f), + keyColor.chroma() * 0.6f, keyColor.tone()); + QList tertiaryPalette = tonalPalette(tertiaryKey); + scheme.setColor("md.tertiary", tertiaryPalette[8].native_color()); + scheme.setColor("md.onTertiary", tertiaryPalette[0].native_color()); + scheme.setColor("md.tertiaryContainer", tertiaryPalette[3].native_color()); + scheme.setColor("md.onTertiaryContainer", tertiaryPalette[11].native_color()); // Error colors (fixed red-based for dark) - r.register_dynamic("md.error", CFColor("#F2B8B5")); - r.register_dynamic("md.onError", CFColor("#601410")); - r.register_dynamic("md.errorContainer", CFColor("#8C1D18")); - r.register_dynamic("md.onErrorContainer", CFColor("#F9DEDC")); + scheme.setColor("md.error", QColor("#F2B8B5")); + scheme.setColor("md.onError", QColor("#601410")); + scheme.setColor("md.errorContainer", QColor("#8C1D18")); + scheme.setColor("md.onErrorContainer", QColor("#F9DEDC")); // Surface colors - r.register_dynamic("md.background", CFColor("#1C1B1F")); - r.register_dynamic("md.onBackground", CFColor("#E6E1E5")); - r.register_dynamic("md.surface", CFColor("#1C1B1F")); - r.register_dynamic("md.onSurface", CFColor("#E6E1E5")); - r.register_dynamic("md.surfaceVariant", getTone(2)); - r.register_dynamic("md.onSurfaceVariant", getTone(10)); - r.register_dynamic("md.outline", getTone(5)); - r.register_dynamic("md.outlineVariant", getTone(2)); + scheme.setColor("md.background", QColor("#1C1B1F")); + scheme.setColor("md.onBackground", QColor("#E6E1E5")); + scheme.setColor("md.surface", QColor("#1C1B1F")); + scheme.setColor("md.onSurface", QColor("#E6E1E5")); + scheme.setColor("md.surfaceVariant", getTone(2).native_color()); + scheme.setColor("md.onSurfaceVariant", getTone(10).native_color()); + scheme.setColor("md.outline", getTone(5).native_color()); + scheme.setColor("md.outlineVariant", getTone(2).native_color()); // Utility colors - r.register_dynamic("md.shadow", CFColor("#000000")); - r.register_dynamic("md.scrim", CFColor("#000000")); - r.register_dynamic("md.inverseSurface", CFColor("#E6E1E5")); - r.register_dynamic("md.inverseOnSurface", CFColor("#313033")); - r.register_dynamic("md.inversePrimary", getTone(4)); + scheme.setColor("md.shadow", QColor("#000000")); + scheme.setColor("md.scrim", QColor("#000000")); + scheme.setColor("md.inverseSurface", QColor("#E6E1E5")); + scheme.setColor("md.inverseOnSurface", QColor("#313033")); + scheme.setColor("md.inversePrimary", getTone(4).native_color()); } return scheme; } QByteArray toJson(const MaterialColorScheme& scheme) { - QJsonObject root; QJsonObject lightScheme; - auto& r = scheme.registry(); - - // Helper to get color string from registry - auto getColorString = [&r](const char* key) -> QString { - auto result = r.get_dynamic_const<::cf::ui::base::CFColor>(key); - if (result && *result) { - return (*result)->native_color().name(); - } - return "#000000"; + // Helper to get color string from scheme + auto getColorString = [&scheme](const char* key) -> QString { + QColor color = scheme.queryColor(key); + return color.isValid() ? color.name() : "#000000"; }; // Build scheme object @@ -438,6 +414,7 @@ QByteArray toJson(const MaterialColorScheme& scheme) { QJsonObject schemes; schemes["light"] = lightScheme; + QJsonObject root; root["schemes"] = schemes; QJsonDocument doc(root); diff --git a/ui/core/material/material_factory.hpp b/ui/core/material/material_factory.hpp index d9805d341..e48e3c653 100644 --- a/ui/core/material/material_factory.hpp +++ b/ui/core/material/material_factory.hpp @@ -14,6 +14,7 @@ #include #include "../../export.h" +#include "base/color.h" #include "base/expected/expected.hpp" #include "cfmaterial_fonttype.h" #include "cfmaterial_motion.h" diff --git a/ui/core/token.hpp b/ui/core/token.hpp deleted file mode 100644 index fc8f23399..000000000 --- a/ui/core/token.hpp +++ /dev/null @@ -1,1018 +0,0 @@ -/** - * @file token.hpp - * @brief High-performance type-safe token system with runtime registration. - * - * Provides a dual-mode token system: - * 1. StaticToken: Compile-time type-safe tokens with zero overhead - * 2. DynamicToken: Runtime type-erased tokens using std::any - * 3. TokenRegistry: Read-safe registry using shared_mutex for read-heavy workloads - * - * @author Charliechen114514 - * @date 2026-02-25 - * @version 0.1 - * @since 0.1 - * @ingroup ui_core - */ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "base/expected/expected.hpp" -#include "base/hash/constexpr_fnv1a.hpp" - -namespace cf::ui::core { - -// ============================================================================= -// Forward Declarations -// ============================================================================= -class TokenRegistry; -/// @brief Compile-time type-safe token with zero runtime overhead. -template class StaticToken; - -// ============================================================================= -// Error Types -// ============================================================================= -/** - * @brief Error type for Token operations. - * - * @since 0.1 - * @ingroup ui_core - */ -struct TokenError { - /** - * @brief Error kind enumeration. - * - * @since 0.1 - */ - enum class Kind { - NotFound, ///< Token not found in registry - TypeMismatch, ///< Type mismatch during get() - AlreadyRegistered, ///< Token already registered - Empty ///< Token has no value - } kind = Kind::NotFound; - - /** - * @brief Human-readable error message. - * - * @since 0.1 - */ - std::string message; - - /** - * @brief Default constructor. - * - * @since 0.1 - */ - TokenError() = default; - - /** - * @brief Construct from kind and message. - * - * @param k Error kind. - * @param msg Error message. - * - * @since 0.1 - */ - TokenError(Kind k, std::string msg) : kind(k), message(std::move(msg)) {} - - /** - * @brief Implicit bool conversion for error checking. - * - * @return true if this represents an error. - * - * @since 0.1 - */ - explicit operator bool() const noexcept { return kind != Kind::NotFound; } -}; - -// ============================================================================= -// StaticToken - Compile-Time Type-Safe Token -// ============================================================================= - -/** - * @brief Compile-time type-safe token with zero runtime overhead. - * - * StaticToken provides compile-time type safety and hash-based lookup. - * The token type and hash are template parameters, enabling complete - * compiler optimization. - * - * @tparam T Value type stored in this token. - * @tparam Hash Compile-time hash of the token name. - * - * @ingroup ui_core - * - * @note Thread-safe for all operations via TokenRegistry's shared_mutex. - * - * @warning Do not use the same Hash value with different T types. - * - * @code - * // Define a token at compile time - * using CounterToken = StaticToken; - * - * // Register and use - * TokenRegistry::get().register_token(42); - * auto result = CounterToken::get(); - * if (result) { int value = *result; } - * @endcode - */ -template class StaticToken { - public: - /// @brief Value type stored in this token. - using value_type = T; - - /// @brief Compile-time hash of the token name. - static constexpr uint64_t hash_value = Hash; - - /// @brief Default constructor. - StaticToken() = default; - - /// @brief Copy constructor is deleted. - StaticToken(const StaticToken&) = delete; - - /// @brief Copy assignment operator is deleted. - StaticToken& operator=(const StaticToken&) = delete; - - /** - * @brief Type-safe value accessor for registry. - * - * @tparam T Value type stored in this token. - * @return cf::expected containing pointer to the token's value or TokenError. - * @throws None - * @note Thread-safe for concurrent reads. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ - static cf::expected get(); - - /** - * @brief Const version of value accessor. - * - * @tparam T Value type stored in this token. - * @return cf::expected containing const pointer to the token's value or TokenError. - * @throws None - * @note Thread-safe for concurrent reads. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ - static cf::expected get_const(); -}; - -// ============================================================================= -// TokenRegistry - Shared-Mutex Protected Storage -// ============================================================================= - -namespace detail { - -/** - * @brief Storage slot for a single token entry. - * - * @ingroup ui_core - * @internal - */ -struct TokenSlot { - std::unique_ptr data; ///< Type-erased token data. - const std::type_info* type_info = nullptr; ///< Type identifier for validation. - std::string name; ///< Human-readable name (debug). - - TokenSlot() = default; - TokenSlot(const TokenSlot&) = delete; - TokenSlot& operator=(const TokenSlot&) = delete; - - TokenSlot(TokenSlot&& other) noexcept - : data(std::move(other.data)), type_info(other.type_info), name(std::move(other.name)) {} - - TokenSlot& operator=(TokenSlot&& other) noexcept { - if (this != &other) { - data = std::move(other.data); - type_info = other.type_info; - name = std::move(other.name); - } - return *this; - } -}; - -} // namespace detail - -/** - * @brief Thread-safe token registry. - * - * Uses std::shared_mutex to allow concurrent reads while serialising writes. - * All get() calls hold the shared lock for the duration of the access, - * which prevents use-after-free when a concurrent writer calls remove(). - * - * @ingroup ui_core - * - * @note Thread-safe for all operations. - * - * @warning Token registration is relatively expensive; design for - * infrequent writes. - * - * @code - * auto& registry = TokenRegistry::get(); - * registry.register_token>(42); - * registry.register_dynamic("userId", 12345); - * auto result = registry.get_dynamic("userId"); - * if (result) { int id = *result; } - * @endcode - */ -class TokenRegistry { - public: - /** - * @brief Result type for token operations. - */ - template using Result = cf::expected; - - /** - * @brief Gets the singleton registry instance. - * - * @return Reference to the singleton registry instance. - * @throws None - * @note Thread-safe singleton initialization. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ - static TokenRegistry& get(); - - // Non-copyable, non-movable - TokenRegistry(const TokenRegistry&) = delete; - TokenRegistry& operator=(const TokenRegistry&) = delete; - TokenRegistry(TokenRegistry&&) = delete; - TokenRegistry& operator=(TokenRegistry&&) = delete; - - /** - * @brief Registers a static token with a value. - * - * @tparam TokenToken StaticToken type (contains T and Hash). - * @tparam Args Constructor argument types. - * @param args Arguments to construct the value. - * @return Result containing void or TokenError::AlreadyRegistered. - */ - template Result register_token(Args&&... args); - - /** - * @brief Gets a static token's value. - * - * @tparam TokenToken StaticToken type. - * @return Result containing pointer to the token's value or TokenError. - */ - template Result get(); - - /** - * @brief Gets a static token's value (const). - * - * @tparam TokenToken StaticToken type. - * @return Result containing const pointer to the token's value or TokenError. - */ - template Result get_const() const; - - /** - * @brief Registers a dynamic token (forwarding constructor). - * @return Result containing void or TokenError::AlreadyRegistered. - */ - template - Result register_dynamic(std::string_view name, Args&&... args); - - /** - * @brief Registers a dynamic token (copy). - * @return Result containing void or TokenError::AlreadyRegistered. - */ - template Result register_dynamic(std::string_view name, const T& value); - - /** - * @brief Registers a dynamic token (move). - * @return Result containing void or TokenError::AlreadyRegistered. - */ - template Result register_dynamic(std::string_view name, T&& value); - - /** - * @brief Gets a dynamic token's value. - * @return Result containing pointer to the token's value or TokenError. - */ - template Result get_dynamic(std::string_view name); - - /** - * @brief Gets a dynamic token's value by hash. - * @return Result containing pointer to the token's value or TokenError. - */ - template Result get_dynamic_by_hash(uint64_t hash); - - /** - * @brief Gets a dynamic token's value (const). - * @return Result containing const pointer to the token's value or TokenError. - */ - template Result get_dynamic_const(std::string_view name) const; - - /** - * @brief Checks if a token exists by hash. - * @return true if the token exists, false otherwise. - */ - bool contains(uint64_t hash) const noexcept; - - /** - * @brief Checks if a dynamic token exists by name. - * @return true if the token exists, false otherwise. - */ - bool contains(std::string_view name) const noexcept; - - /** - * @brief Removes a token from the registry by hash. - * @return true if removed, false if not found. - */ - bool remove(uint64_t hash); - - /** - * @brief Removes a dynamic token from the registry by name. - * @return true if removed, false if not found. - */ - bool remove(std::string_view name); - - /** - * @brief Gets the number of registered tokens. - * @return Number of tokens in the registry. - */ - size_t size() const noexcept; - - private: - TokenRegistry() = default; - ~TokenRegistry() = default; - - // Internal helpers — caller must already hold the appropriate lock. - // Returns nullptr if not found. Lock must be held by caller. - detail::TokenSlot* find_slot_locked(uint64_t hash); - const detail::TokenSlot* find_slot_locked(uint64_t hash) const; - - // Shared implementation for get_dynamic / get_dynamic_by_hash (non-const). - template Result get_by_hash_impl(uint64_t hash, const std::string& name_hint); - - // Shared implementation for get_dynamic_const. - template - Result get_by_hash_impl_const(uint64_t hash, const std::string& name_hint) const; - - mutable std::shared_mutex registry_mutex_; - std::unordered_map slot_map_; -}; - -// ============================================================================= -// Inline Implementations - StaticToken -// ============================================================================= - -/** - * @brief Type-safe value accessor for registry (implementation). - * - * @tparam T Value type stored in this token. - * @tparam Hash Compile-time hash of the token name. - * @return cf::expected containing pointer to the token's value or TokenError. - * @throws None - * @note Delegates to TokenRegistry::get(). - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -template -auto StaticToken::get() -> cf::expected { - return TokenRegistry::get().get>(); -} - -/** - * @brief Const version of value accessor (implementation). - * - * @tparam T Value type stored in this token. - * @tparam Hash Compile-time hash of the token name. - * @return cf::expected containing const pointer to the token's value or TokenError. - * @throws None - * @note Delegates to TokenRegistry::get_const(). - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -template -auto StaticToken::get_const() -> cf::expected { - return TokenRegistry::get().get_const>(); -} - -// ============================================================================= -// Inline Implementations - TokenRegistry -// ============================================================================= - -/** - * @brief Gets the singleton TokenRegistry instance. - * - * @return Reference to the singleton registry instance. - * @throws None - * @note Thread-safe singleton initialization. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline TokenRegistry& TokenRegistry::get() { - static TokenRegistry instance; - return instance; -} - -/** - * @brief Gets the number of registered tokens. - * - * @return Number of tokens in the registry. - * @throws None - * @note Thread-safe for concurrent reads. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline size_t TokenRegistry::size() const noexcept { - std::shared_lock lock(registry_mutex_); - return slot_map_.size(); -} - -/** - * @brief Checks if a token exists by hash. - * - * @param[in] hash Hash value of the token to check. - * @return true if the token exists, false otherwise. - * @throws None - * @note Thread-safe for concurrent reads. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline bool TokenRegistry::contains(uint64_t hash) const noexcept { - std::shared_lock lock(registry_mutex_); - return slot_map_.find(hash) != slot_map_.end(); -} - -/** - * @brief Checks if a token exists by name. - * - * @param[in] name Name of the token to check. - * @return true if the token exists, false otherwise. - * @throws None - * @note Thread-safe for concurrent reads. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline bool TokenRegistry::contains(std::string_view name) const noexcept { - return contains(cf::hash::fnv1a64(name)); -} - -/** - * @brief Removes a token from the registry by hash. - * - * @param[in] hash Hash value of the token to remove. - * @return true if the token was removed, false if not found. - * @throws None - * @note Thread-safe for exclusive access. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline bool TokenRegistry::remove(uint64_t hash) { - std::unique_lock lock(registry_mutex_); - return slot_map_.erase(hash) > 0; -} - -/** - * @brief Removes a token from the registry by name. - * - * @param[in] name Name of the token to remove. - * @return true if the token was removed, false if not found. - * @throws None - * @note Thread-safe for exclusive access. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline bool TokenRegistry::remove(std::string_view name) { - return remove(cf::hash::fnv1a64(name)); -} - -// These helpers are only called while the caller already holds the mutex. -inline detail::TokenSlot* TokenRegistry::find_slot_locked(uint64_t hash) { - auto it = slot_map_.find(hash); - return (it != slot_map_.end()) ? &it->second : nullptr; -} - -inline const detail::TokenSlot* TokenRegistry::find_slot_locked(uint64_t hash) const { - auto it = slot_map_.find(hash); - return (it != slot_map_.end()) ? &it->second : nullptr; -} - -// ----------------------------------------------------------------------------- -// Static Token Registration -// ----------------------------------------------------------------------------- - -template -auto TokenRegistry::register_token(Args&&... args) -> Result { - using T = typename TokenToken::value_type; - constexpr uint64_t hash = TokenToken::hash_value; - - std::unique_lock lock(registry_mutex_); - - if (slot_map_.find(hash) != slot_map_.end()) { - return cf::unexpected( - TokenError{TokenError::Kind::AlreadyRegistered, - "Token already registered, hash: " + std::to_string(hash)}); - } - - detail::TokenSlot slot; - slot.data = std::make_unique(T(std::forward(args)...)); - slot.type_info = &typeid(T); - slot.name = "static_token_" + std::to_string(hash); - - slot_map_.emplace(hash, std::move(slot)); - return {}; -} - -// ----------------------------------------------------------------------------- -// Static Token Get -// ----------------------------------------------------------------------------- - -template -auto TokenRegistry::get() -> Result { - using T = typename TokenToken::value_type; - constexpr uint64_t hash = TokenToken::hash_value; - - // Hold shared lock for the entire operation to prevent remove() from - // destroying the slot while reading from it. - std::shared_lock lock(registry_mutex_); - - const detail::TokenSlot* slot = find_slot_locked(hash); - if (!slot) { - return cf::unexpected(TokenError{TokenError::Kind::NotFound, - "Token not found, hash: " + std::to_string(hash)}); - } - - std::any* a = slot->data.get(); - if (!a || !a->has_value()) { - return cf::unexpected(TokenError{TokenError::Kind::Empty, "Token has no value"}); - } - - if (slot->type_info != &typeid(T)) { - return cf::unexpected( - TokenError{TokenError::Kind::TypeMismatch, "Type mismatch for token"}); - } - - return std::any_cast(a); -} - -template -auto TokenRegistry::get_const() const -> Result { - using T = typename TokenToken::value_type; - constexpr uint64_t hash = TokenToken::hash_value; - - std::shared_lock lock(registry_mutex_); - - const detail::TokenSlot* slot = find_slot_locked(hash); - if (!slot) { - return cf::unexpected(TokenError{TokenError::Kind::NotFound, - "Token not found, hash: " + std::to_string(hash)}); - } - - const std::any* a = slot->data.get(); - if (!a || !a->has_value()) { - return cf::unexpected(TokenError{TokenError::Kind::Empty, "Token has no value"}); - } - - if (slot->type_info != &typeid(T)) { - return cf::unexpected( - TokenError{TokenError::Kind::TypeMismatch, "Type mismatch for token"}); - } - - return std::any_cast(a); -} - -// ----------------------------------------------------------------------------- -// Dynamic Token Registration -// ----------------------------------------------------------------------------- - -template -auto TokenRegistry::register_dynamic(std::string_view name, Args&&... args) -> Result { - uint64_t hash = cf::hash::fnv1a64(name); - - std::unique_lock lock(registry_mutex_); - - if (slot_map_.find(hash) != slot_map_.end()) { - return cf::unexpected(TokenError{TokenError::Kind::AlreadyRegistered, - "Already registered: " + std::string(name)}); - } - - detail::TokenSlot slot; - slot.data = std::make_unique(T(std::forward(args)...)); - slot.type_info = &typeid(T); - slot.name = name; - - slot_map_.emplace(hash, std::move(slot)); - return {}; -} - -template -auto TokenRegistry::register_dynamic(std::string_view name, const T& value) -> Result { - uint64_t hash = cf::hash::fnv1a64(name); - - std::unique_lock lock(registry_mutex_); - - if (slot_map_.find(hash) != slot_map_.end()) { - return cf::unexpected(TokenError{TokenError::Kind::AlreadyRegistered, - "Already registered: " + std::string(name)}); - } - - detail::TokenSlot slot; - slot.data = std::make_unique(value); - slot.type_info = &typeid(T); - slot.name = name; - - slot_map_.emplace(hash, std::move(slot)); - return {}; -} - -template -auto TokenRegistry::register_dynamic(std::string_view name, T&& value) -> Result { - uint64_t hash = cf::hash::fnv1a64(name); - - std::unique_lock lock(registry_mutex_); - - if (slot_map_.find(hash) != slot_map_.end()) { - return cf::unexpected(TokenError{TokenError::Kind::AlreadyRegistered, - "Already registered: " + std::string(name)}); - } - - detail::TokenSlot slot; - slot.data = std::make_unique(std::forward(value)); - slot.type_info = &typeid(T); - slot.name = name; - - slot_map_.emplace(hash, std::move(slot)); - return {}; -} - -// ----------------------------------------------------------------------------- -// Dynamic Token Get — shared impl -// ----------------------------------------------------------------------------- - -template -auto TokenRegistry::get_by_hash_impl(uint64_t hash, const std::string& name_hint) -> Result { - // Lock held by callers (get_dynamic / get_dynamic_by_hash) for entire scope. - const detail::TokenSlot* slot = find_slot_locked(hash); - if (!slot) { - return cf::unexpected( - TokenError{TokenError::Kind::NotFound, "Token not found: " + name_hint}); - } - - std::any* a = slot->data.get(); - if (!a || !a->has_value()) { - return cf::unexpected( - TokenError{TokenError::Kind::Empty, "Token has no value: " + name_hint}); - } - - if (slot->type_info != &typeid(T)) { - return cf::unexpected( - TokenError{TokenError::Kind::TypeMismatch, "Type mismatch for token: " + name_hint}); - } - - return std::any_cast(a); -} - -template -auto TokenRegistry::get_by_hash_impl_const(uint64_t hash, - const std::string& name_hint) const -> Result { - const detail::TokenSlot* slot = find_slot_locked(hash); - if (!slot) { - return cf::unexpected( - TokenError{TokenError::Kind::NotFound, "Token not found: " + name_hint}); - } - - const std::any* a = slot->data.get(); - if (!a || !a->has_value()) { - return cf::unexpected( - TokenError{TokenError::Kind::Empty, "Token has no value: " + name_hint}); - } - - if (slot->type_info != &typeid(T)) { - return cf::unexpected( - TokenError{TokenError::Kind::TypeMismatch, "Type mismatch for token: " + name_hint}); - } - - return std::any_cast(a); -} - -template auto TokenRegistry::get_dynamic(std::string_view name) -> Result { - std::shared_lock lock(registry_mutex_); - return get_by_hash_impl(cf::hash::fnv1a64(name), std::string(name)); -} - -template auto TokenRegistry::get_dynamic_by_hash(uint64_t hash) -> Result { - std::shared_lock lock(registry_mutex_); - return get_by_hash_impl(hash, std::to_string(hash)); -} - -template -auto TokenRegistry::get_dynamic_const(std::string_view name) const -> Result { - std::shared_lock lock(registry_mutex_); - return get_by_hash_impl_const(cf::hash::fnv1a64(name), std::string(name)); -} - -// ============================================================================= -// EmbeddedTokenRegistry - Non-Singleton Embeddable Registry -// ============================================================================= - -/** - * @brief Embeddable token registry for component-scoped token storage. - * - * Provides the same API as TokenRegistry but without singleton semantics. - * Can be embedded in other classes for independent token management. - * - * @ingroup ui_core - * - * @note Thread-safe for all operations. - * - * @code - * class MyComponent { - * public: - * EmbeddedTokenRegistry registry; - * void init() { registry.register_dynamic("counter", 0); } - * }; - * @endcode - */ -class EmbeddedTokenRegistry { - public: - template using Result = cf::expected; - - EmbeddedTokenRegistry() = default; - ~EmbeddedTokenRegistry() = default; - - EmbeddedTokenRegistry(const EmbeddedTokenRegistry&) = delete; - EmbeddedTokenRegistry& operator=(const EmbeddedTokenRegistry&) = delete; - - EmbeddedTokenRegistry(EmbeddedTokenRegistry&& other) noexcept - : slot_map_(std::move(other.slot_map_)) {} - - EmbeddedTokenRegistry& operator=(EmbeddedTokenRegistry&& other) noexcept { - if (this != &other) - slot_map_ = std::move(other.slot_map_); - return *this; - } - - template - Result register_dynamic(std::string_view name, Args&&... args); - - template Result register_dynamic(std::string_view name, const T& value); - template Result register_dynamic(std::string_view name, T&& value); - - /** - * @brief Gets a dynamic token's value by name. - * @return Result containing pointer to the token's value or TokenError. - */ - template Result get_dynamic(std::string_view name); - /** - * @brief Gets a dynamic token's value by hash. - * @return Result containing pointer to the token's value or TokenError. - */ - template Result get_dynamic_by_hash(uint64_t hash); - /** - * @brief Gets a dynamic token's value by name (const). - * @return Result containing const pointer to the token's value or TokenError. - */ - template Result get_dynamic_const(std::string_view name) const; - - /** - * @brief Checks if a token exists by hash. - * @return true if the token exists, false otherwise. - */ - bool contains(uint64_t hash) const noexcept; - /** - * @brief Checks if a token exists by name. - * @return true if the token exists, false otherwise. - */ - bool contains(std::string_view name) const noexcept; - /** - * @brief Removes a token from the registry by hash. - * @return true if the token was removed, false if not found. - */ - bool remove(uint64_t hash); - /** - * @brief Removes a token from the registry by name. - * @return true if the token was removed, false if not found. - */ - bool remove(std::string_view name); - /** - * @brief Gets the number of registered tokens. - * @return Number of tokens in the registry. - */ - size_t size() const noexcept; - - private: - detail::TokenSlot* find_slot_locked(uint64_t hash); - const detail::TokenSlot* find_slot_locked(uint64_t hash) const; - - template Result get_by_hash_impl(uint64_t hash, const std::string& name_hint); - - template - Result get_by_hash_impl_const(uint64_t hash, const std::string& name_hint) const; - - mutable std::shared_mutex registry_mutex_; - std::unordered_map slot_map_; -}; - -// ============================================================================= -// Inline Implementations - EmbeddedTokenRegistry -// ============================================================================= - -/** - * @brief Gets the number of registered tokens. - * - * @return Number of tokens in the registry. - * @throws None - * @note Thread-safe for concurrent reads. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline size_t EmbeddedTokenRegistry::size() const noexcept { - std::shared_lock lock(registry_mutex_); - return slot_map_.size(); -} - -/** - * @brief Checks if a token exists by hash. - * - * @param[in] hash Hash value of the token to check. - * @return true if the token exists, false otherwise. - * @throws None - * @note Thread-safe for concurrent reads. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline bool EmbeddedTokenRegistry::contains(uint64_t hash) const noexcept { - std::shared_lock lock(registry_mutex_); - return slot_map_.find(hash) != slot_map_.end(); -} - -/** - * @brief Checks if a token exists by name. - * - * @param[in] name Name of the token to check. - * @return true if the token exists, false otherwise. - * @throws None - * @note Thread-safe for concurrent reads. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline bool EmbeddedTokenRegistry::contains(std::string_view name) const noexcept { - return contains(cf::hash::fnv1a64(name)); -} - -/** - * @brief Removes a token from the registry by hash. - * - * @param[in] hash Hash value of the token to remove. - * @return true if the token was removed, false if not found. - * @throws None - * @note Thread-safe for exclusive access. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline bool EmbeddedTokenRegistry::remove(uint64_t hash) { - std::unique_lock lock(registry_mutex_); - return slot_map_.erase(hash) > 0; -} - -/** - * @brief Removes a token from the registry by name. - * - * @param[in] name Name of the token to remove. - * @return true if the token was removed, false if not found. - * @throws None - * @note Thread-safe for exclusive access. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline bool EmbeddedTokenRegistry::remove(std::string_view name) { - return remove(cf::hash::fnv1a64(name)); -} - -inline detail::TokenSlot* EmbeddedTokenRegistry::find_slot_locked(uint64_t hash) { - auto it = slot_map_.find(hash); - return (it != slot_map_.end()) ? &it->second : nullptr; -} - -inline const detail::TokenSlot* EmbeddedTokenRegistry::find_slot_locked(uint64_t hash) const { - auto it = slot_map_.find(hash); - return (it != slot_map_.end()) ? &it->second : nullptr; -} - -template auto -EmbeddedTokenRegistry::register_dynamic(std::string_view name, Args&&... args) -> Result { - uint64_t hash = cf::hash::fnv1a64(name); - std::unique_lock lock(registry_mutex_); - if (slot_map_.find(hash) != slot_map_.end()) { - return cf::unexpected(TokenError{TokenError::Kind::AlreadyRegistered, - "Already registered: " + std::string(name)}); - } - detail::TokenSlot slot; - slot.data = std::make_unique(T(std::forward(args)...)); - slot.type_info = &typeid(T); - slot.name = name; - slot_map_.emplace(hash, std::move(slot)); - return {}; -} - -template auto EmbeddedTokenRegistry::register_dynamic(std::string_view name, - const T& value) -> Result { - uint64_t hash = cf::hash::fnv1a64(name); - std::unique_lock lock(registry_mutex_); - if (slot_map_.find(hash) != slot_map_.end()) { - return cf::unexpected(TokenError{TokenError::Kind::AlreadyRegistered, - "Already registered: " + std::string(name)}); - } - detail::TokenSlot slot; - slot.data = std::make_unique(value); - slot.type_info = &typeid(T); - slot.name = name; - slot_map_.emplace(hash, std::move(slot)); - return {}; -} - -template -auto EmbeddedTokenRegistry::register_dynamic(std::string_view name, T&& value) -> Result { - uint64_t hash = cf::hash::fnv1a64(name); - std::unique_lock lock(registry_mutex_); - if (slot_map_.find(hash) != slot_map_.end()) { - return cf::unexpected(TokenError{TokenError::Kind::AlreadyRegistered, - "Already registered: " + std::string(name)}); - } - detail::TokenSlot slot; - slot.data = std::make_unique(std::forward(value)); - slot.type_info = &typeid(T); - slot.name = name; - slot_map_.emplace(hash, std::move(slot)); - return {}; -} - -template auto -EmbeddedTokenRegistry::get_by_hash_impl(uint64_t hash, const std::string& name_hint) -> Result { - const detail::TokenSlot* slot = find_slot_locked(hash); - if (!slot) { - return cf::unexpected( - TokenError{TokenError::Kind::NotFound, "Token not found: " + name_hint}); - } - std::any* a = slot->data.get(); - if (!a || !a->has_value()) { - return cf::unexpected( - TokenError{TokenError::Kind::Empty, "Token has no value: " + name_hint}); - } - if (slot->type_info != &typeid(T)) { - return cf::unexpected( - TokenError{TokenError::Kind::TypeMismatch, "Type mismatch for token: " + name_hint}); - } - return std::any_cast(a); -} - -template auto EmbeddedTokenRegistry::get_by_hash_impl_const( - uint64_t hash, const std::string& name_hint) const -> Result { - const detail::TokenSlot* slot = find_slot_locked(hash); - if (!slot) { - return cf::unexpected( - TokenError{TokenError::Kind::NotFound, "Token not found: " + name_hint}); - } - const std::any* a = slot->data.get(); - if (!a || !a->has_value()) { - return cf::unexpected( - TokenError{TokenError::Kind::Empty, "Token has no value: " + name_hint}); - } - if (slot->type_info != &typeid(T)) { - return cf::unexpected( - TokenError{TokenError::Kind::TypeMismatch, "Type mismatch for token: " + name_hint}); - } - return std::any_cast(a); -} - -template auto EmbeddedTokenRegistry::get_dynamic(std::string_view name) -> Result { - std::shared_lock lock(registry_mutex_); - return get_by_hash_impl(cf::hash::fnv1a64(name), std::string(name)); -} - -template auto EmbeddedTokenRegistry::get_dynamic_by_hash(uint64_t hash) -> Result { - std::shared_lock lock(registry_mutex_); - return get_by_hash_impl(hash, std::to_string(hash)); -} - -template -auto EmbeddedTokenRegistry::get_dynamic_const(std::string_view name) const -> Result { - std::shared_lock lock(registry_mutex_); - return get_by_hash_impl_const(cf::hash::fnv1a64(name), std::string(name)); -} - -} // namespace cf::ui::core \ No newline at end of file diff --git a/ui/widget/CMakeLists.txt b/ui/widget/CMakeLists.txt index 98b042bb1..223927064 100644 --- a/ui/widget/CMakeLists.txt +++ b/ui/widget/CMakeLists.txt @@ -30,6 +30,7 @@ add_library(cf_ui_widget_material STATIC material/base/elevation_controller.cpp material/base/focus_ring.cpp material/base/painter_layer.cpp + material/base/material_widget_base.cpp material/widget/button/button.cpp material/widget/checkbox/checkbox.cpp material/widget/comboBox/combobox.cpp diff --git a/ui/widget/material/base/material_widget_base.cpp b/ui/widget/material/base/material_widget_base.cpp new file mode 100644 index 000000000..742cff4fb --- /dev/null +++ b/ui/widget/material/base/material_widget_base.cpp @@ -0,0 +1,95 @@ +/** + * @file material_widget_base.cpp + * @brief Common Material Design widget behavior composition helper. + * + * @ingroup ui_widget_material_base + */ + +#include "material_widget_base.h" + +#include "application_support/application.h" + +namespace cf::ui::widget::material::base { + +MaterialWidgetBase::MaterialWidgetBase(QWidget* owner, const Config& config) : m_owner(owner) { + auto factory = cf::WeakPtr::DynamicCast( + application_support::Application::animationFactory()); + + m_stateMachine = new StateMachine(factory, owner); + + if (config.useRipple) { + m_ripple = new RippleHelper(factory, owner); + m_ripple->setMode(config.rippleMode); + connect(m_ripple, &RippleHelper::repaintNeeded, owner, + static_cast(&QWidget::update)); + } + + if (config.useElevation) { + m_elevation = new MdElevationController(factory, owner); + m_elevation->setElevation(config.initialElevation); + connect(m_elevation, &MdElevationController::pressOffsetChanged, owner, + static_cast(&QWidget::update)); + } + + if (config.useFocusIndicator) { + m_focusIndicator = new MdFocusIndicator(factory, owner); + } + + connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, owner, + static_cast(&QWidget::update)); +} + +void MaterialWidgetBase::onEnterEvent() { + m_stateMachine->onHoverEnter(); + m_owner->update(); +} + +void MaterialWidgetBase::onLeaveEvent() { + m_stateMachine->onHoverLeave(); + if (m_ripple) + m_ripple->onCancel(); + m_owner->update(); +} + +void MaterialWidgetBase::onMousePress(const QPoint& pos, const QRectF& bounds) { + m_stateMachine->onPress(pos); + if (m_ripple) + m_ripple->onPress(pos, bounds); + if (m_elevation) + m_elevation->setPressed(true); + m_owner->update(); +} + +void MaterialWidgetBase::onMouseRelease() { + m_stateMachine->onRelease(); + if (m_ripple) + m_ripple->onRelease(); + if (m_elevation) + m_elevation->setPressed(false); + m_owner->update(); +} + +void MaterialWidgetBase::onFocusIn() { + m_stateMachine->onFocusIn(); + if (m_focusIndicator) + m_focusIndicator->onFocusIn(); + m_owner->update(); +} + +void MaterialWidgetBase::onFocusOut() { + m_stateMachine->onFocusOut(); + if (m_focusIndicator) + m_focusIndicator->onFocusOut(); + m_owner->update(); +} + +void MaterialWidgetBase::onEnabledChange(bool enabled) { + if (enabled) { + m_stateMachine->onEnable(); + } else { + m_stateMachine->onDisable(); + } + m_owner->update(); +} + +} // namespace cf::ui::widget::material::base diff --git a/ui/widget/material/base/material_widget_base.h b/ui/widget/material/base/material_widget_base.h new file mode 100644 index 000000000..4a332971a --- /dev/null +++ b/ui/widget/material/base/material_widget_base.h @@ -0,0 +1,214 @@ +/** + * @file material_widget_base.h + * @brief Common Material Design widget behavior composition helper. + * + * Encapsulates the shared initialization, signal connections, and event + * forwarding used by all interactive Material Design 3 widgets. + * + * @ingroup ui_widget_material_base + */ + +#pragma once + +#include "base/include/base/weak_ptr/weak_ptr.h" +#include "components/material/cfmaterial_animation_factory.h" +#include "export.h" +#include "focus_ring.h" +#include "ripple_helper.h" +#include "state_machine.h" +#include "widget/material/base/elevation_controller.h" + +#include +#include +#include +#include +#include +#include + +namespace cf::ui::widget::material::base { + +/** + * @brief Composition helper for Material Design widget behavior. + * + * @details Encapsulates the common initialization, signal connections, + * and event forwarding shared by all interactive Material Design 3 + * widgets. Uses a Config struct to control which helper components + * are created. + * + * This is a "has-a" composition, not inheritance, because widgets + * inherit from different Qt base classes (QPushButton, QCheckBox, + * QSlider, etc.). + * + * @note The owner QWidget must remain valid for the lifetime of this object. + * @warning Do not use with non-interactive widgets (Label, Separator). + * @since 0.1 + * @ingroup ui_widget_material_base + * + * @code + * // In widget constructor: + * m_material(this, MaterialWidgetBase::Config{ + * .useElevation = true, + * .initialElevation = 2 + * }); + * + * // In event handlers: + * void MyWidget::enterEvent(QEnterEvent* event) { + * QPushButton::enterEvent(event); + * m_material.onEnterEvent(); + * } + * @endcode + */ + +/** + * @brief Configuration for MaterialWidgetBase helper creation. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ +struct MaterialWidgetBaseConfig { + bool useRipple = true; ///< Create RippleHelper. + bool useElevation = false; ///< Create MdElevationController. + bool useFocusIndicator = true; ///< Create MdFocusIndicator. + RippleHelper::Mode rippleMode = RippleHelper::Mode::Bounded; ///< Ripple mode. + int initialElevation = 0; ///< Initial elevation level. +}; + +class CF_UI_EXPORT MaterialWidgetBase : public QObject { + Q_OBJECT + public: + using Config = MaterialWidgetBaseConfig; + + /** + * @brief Construct and initialize helper components. + * + * @param[in] owner The parent widget (must not be null). + * @param[in] config Configuration controlling which helpers to create. + * + * @throws None + * @note Connects repaint signals to owner's update() slot. + * @warning owner must remain valid for the lifetime of this object. + * @since 0.1 + * @ingroup ui_widget_material_base + */ + MaterialWidgetBase(QWidget* owner, const Config& config = Config{}); + + ~MaterialWidgetBase() override = default; + + // Non-copyable, movable + MaterialWidgetBase(const MaterialWidgetBase&) = delete; + MaterialWidgetBase& operator=(const MaterialWidgetBase&) = delete; + + // --- Event forwarding --- + + /** + * @brief Forward hover enter to helpers and trigger repaint. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + void onEnterEvent(); + + /** + * @brief Forward hover leave to helpers and trigger repaint. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + void onLeaveEvent(); + + /** + * @brief Forward mouse press to helpers and trigger repaint. + * + * @param[in] pos Mouse position relative to the widget. + * @param[in] bounds Widget bounds for ripple calculation. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + void onMousePress(const QPoint& pos, const QRectF& bounds); + + /** + * @brief Forward mouse release to helpers and trigger repaint. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + void onMouseRelease(); + + /** + * @brief Forward focus-in to helpers and trigger repaint. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + void onFocusIn(); + + /** + * @brief Forward focus-out to helpers and trigger repaint. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + void onFocusOut(); + + /** + * @brief Forward enabled-state change to helpers and trigger repaint. + * + * @param[in] enabled Whether the widget is now enabled. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + void onEnabledChange(bool enabled); + + // --- Accessors --- + + /** + * @brief Get the state machine helper. + * + * @return Pointer to StateMachine, or nullptr if not created. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + StateMachine* stateMachine() const { return m_stateMachine; } + + /** + * @brief Get the ripple helper. + * + * @return Pointer to RippleHelper, or nullptr if not created. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + RippleHelper* ripple() const { return m_ripple; } + + /** + * @brief Get the elevation controller. + * + * @return Pointer to MdElevationController, or nullptr if not created. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + MdElevationController* elevation() const { return m_elevation; } + + /** + * @brief Get the focus indicator. + * + * @return Pointer to MdFocusIndicator, or nullptr if not created. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + MdFocusIndicator* focusIndicator() const { return m_focusIndicator; } + + private: + QWidget* m_owner; + StateMachine* m_stateMachine = nullptr; + RippleHelper* m_ripple = nullptr; + MdElevationController* m_elevation = nullptr; + MdFocusIndicator* m_focusIndicator = nullptr; +}; + +} // namespace cf::ui::widget::material::base diff --git a/ui/widget/material/base/state_machine.cpp b/ui/widget/material/base/state_machine.cpp index 03ab27579..73416a6ea 100644 --- a/ui/widget/material/base/state_machine.cpp +++ b/ui/widget/material/base/state_machine.cpp @@ -39,8 +39,6 @@ using namespace cf::ui::components; StateMachine::StateMachine(cf::WeakPtr factory, QObject* parent) : QObject(parent), m_state(State::StateNormal), m_opacity(0.0f) { - // WeakPtr is stored but not locked here to avoid circular dependency - // Store the WeakPtr in a member variable for later use m_animator = factory; } @@ -61,8 +59,6 @@ StateMachine::~StateMachine() { */ float StateMachine::targetOpacityForState(States s) const { // Priority order: Disabled > Pressed > Dragged > Focused > Hovered > Normal - // When multiple states are active, use the highest priority - if (s & State::StateDisabled) { return 0.0f; } @@ -79,10 +75,10 @@ float StateMachine::targetOpacityForState(States s) const { return 0.08f; } if (s & State::StateChecked) { - return 0.08f; // Checked state uses same as hovered + return 0.08f; } - return 0.0f; // StateNormal + return 0.0f; } /** @@ -99,17 +95,12 @@ void StateMachine::cancelCurrentAnimation() { if (m_currentAnimation) { auto* anim = m_currentAnimation.Get(); if (anim) { - // Disconnect all signals from this animation to this object FIRST - // This prevents any late-arriving progressChanged signals after we've stopped disconnect(anim, &components::ICFAbstractAnimation::progressChanged, this, nullptr); disconnect(anim, &components::ICFAbstractAnimation::finished, this, nullptr); - // Stop the animation anim->stop(); } m_currentAnimation = nullptr; } - // Reset m_opacity to the correct target value for the current state - // This ensures we don't carry over stale progress values from cancelled animations m_opacity = targetOpacityForState(m_state); emit stateLayerOpacityChanged(m_opacity); } @@ -118,13 +109,10 @@ void StateMachine::cancelCurrentAnimation() { * @brief Slot called when the current animation finishes. * * Clears the animation reference and ensures m_opacity is set to the - * correct target value for the current state. This prevents stale - * intermediate values from being used when a new animation starts. + * correct target value for the current state. */ void StateMachine::onAnimationFinished() { m_currentAnimation = nullptr; - // Ensure m_opacity is exactly at the target value for the current state - // This guards against any rounding errors or incomplete animations float targetOpacity = targetOpacityForState(m_state); if (m_opacity != targetOpacity) { m_opacity = targetOpacity; @@ -141,7 +129,6 @@ void StateMachine::onAnimationFinished() { * @param to Target opacity value. */ void StateMachine::animateOpacityTo(float to) { - // Performance mode check: if animations are disabled, set directly auto* factory = m_animator.Get(); if (!factory || !factory->isAllEnabled()) { m_opacity = to; @@ -149,65 +136,48 @@ void StateMachine::animateOpacityTo(float to) { return; } - // CRITICAL FIX: Cancel any currently running animation before starting a new one. - // This prevents multiple animations from competing to update m_opacity, - // which causes visual glitches like flickering or incorrect opacity values - // during rapid hover enter/leave events. cancelCurrentAnimation(); - // Start animation from current opacity value float from = m_opacity; - // IMPORTANT: Use createAnimation instead of getAnimation to avoid sharing - // the same animation instance across multiple StateMachines. - // getAnimation returns a cached animation per token, which causes multiple - // StateMachines to connect their lambdas to the same animation object, - // resulting in cross-talk when any animation progresses. - // createAnimation creates a separate animation instance per call. - // See ElevationController::animatePressOffsetTo for the same pattern. - AnimationDescriptor desc( - "fade", // Animation type - "md.motion.shortEnter", // Motion spec (short duration for hover states) - "opacity", // Property (we'll override with setRange) - from, // Start value - to // End value - ); + AnimationDescriptor desc("fade", "md.motion.shortEnter", "opacity", from, to); auto anim = factory->createAnimation(desc, nullptr, this); if (!anim) { - // Fallback: direct set if animation creation fails m_opacity = to; emit stateLayerOpacityChanged(m_opacity); return; } - // Save animation reference for cancellation m_currentAnimation = anim; - - // Get raw pointer auto* rawAnim = anim.Get(); - // Connect progress signal - // Note: Qt::UniqueConnection cannot be used with lambdas, but cancelCurrentAnimation() - // disconnects all signals before starting a new animation, preventing duplicates - // - // CRITICAL: progressChanged emits 0-1 normalized progress, NOT actual opacity values. - // We must interpolate between from and to to get the actual opacity. connect(rawAnim, &components::ICFAbstractAnimation::progressChanged, this, [this, from, to](float progress) { - // progress is 0-1, interpolate to get actual opacity value m_opacity = from + (to - from) * progress; emit stateLayerOpacityChanged(m_opacity); }); - // Connect finished signal to clear the animation reference connect(rawAnim, &components::ICFAbstractAnimation::finished, this, &StateMachine::onAnimationFinished, Qt::UniqueConnection); - // Start animation rawAnim->start(components::ICFAbstractAnimation::Direction::Forward); } +// ============================================================================ +// State Transition +// ============================================================================ + +void StateMachine::transitionTo(States newState) { + if (m_state == newState) { + return; + } + States oldState = m_state; + m_state = newState; + emit stateChanged(m_state, oldState); + animateOpacityTo(targetOpacityForState(m_state)); +} + // ============================================================================ // Event Handlers // ============================================================================ @@ -215,108 +185,49 @@ void StateMachine::animateOpacityTo(float to) { void StateMachine::onHoverEnter() { if (m_state & State::StateDisabled) return; - - States oldState = m_state; - m_state |= State::StateHovered; - - if (oldState != m_state) { - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); - } + transitionTo(m_state | State::StateHovered); } void StateMachine::onHoverLeave() { - States oldState = m_state; - m_state &= ~static_cast(State::StateHovered); - - if (oldState != m_state) { - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); - } + transitionTo(m_state & ~static_cast(State::StateHovered)); } void StateMachine::onPress(const QPoint& pos) { Q_UNUSED(pos) if (m_state & State::StateDisabled) return; - - States oldState = m_state; - m_state |= State::StatePressed; - - if (oldState != m_state) { - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); - } + transitionTo(m_state | State::StatePressed); } void StateMachine::onRelease() { - States oldState = m_state; - m_state &= ~static_cast(State::StatePressed); - - if (oldState != m_state) { - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); - } + transitionTo(m_state & ~static_cast(State::StatePressed)); } void StateMachine::onFocusIn() { if (m_state & State::StateDisabled) return; - - States oldState = m_state; - m_state |= State::StateFocused; - - if (oldState != m_state) { - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); - } + transitionTo(m_state | State::StateFocused); } void StateMachine::onFocusOut() { - States oldState = m_state; - m_state &= ~static_cast(State::StateFocused); - - if (oldState != m_state) { - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); - } + transitionTo(m_state & ~static_cast(State::StateFocused)); } void StateMachine::onEnable() { - States oldState = m_state; - m_state &= ~static_cast(State::StateDisabled); - - if (oldState != m_state) { - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); - } + transitionTo(m_state & ~static_cast(State::StateDisabled)); } void StateMachine::onDisable() { - States oldState = m_state; - m_state |= State::StateDisabled; - - if (oldState != m_state) { - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); - } + transitionTo(m_state | State::StateDisabled); } void StateMachine::onCheckedChanged(bool checked) { if (m_state & State::StateDisabled) return; - - States oldState = m_state; - if (checked) { - m_state |= State::StateChecked; + transitionTo(m_state | State::StateChecked); } else { - m_state &= ~static_cast(State::StateChecked); - } - - if (oldState != m_state) { - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); + transitionTo(m_state & ~static_cast(State::StateChecked)); } } diff --git a/ui/widget/material/base/state_machine.h b/ui/widget/material/base/state_machine.h index 6cc8225f2..6b22c89e8 100644 --- a/ui/widget/material/base/state_machine.h +++ b/ui/widget/material/base/state_machine.h @@ -272,6 +272,23 @@ class CF_UI_EXPORT StateMachine : public QObject { */ void cancelCurrentAnimation(); + /** + * @brief Transitions to a new state if different from current. + * + * @details Compares new state with current state. If different, + * updates the state, emits stateChanged, and animates + * opacity to the target value for the new state. + * + * @param[in] newState The target state to transition to. + * + * @throws None + * @note No-op if newState equals current state. + * @warning None + * @since N/A + * @ingroup ui_widget_material_base + */ + void transitionTo(States newState); + /** * @brief Slot called when the current animation finishes. * diff --git a/ui/widget/material/widget/button/button.cpp b/ui/widget/material/widget/button/button.cpp index bba9f39aa..a188a5994 100644 --- a/ui/widget/material/widget/button/button.cpp +++ b/ui/widget/material/widget/button/button.cpp @@ -17,12 +17,7 @@ #include "application_support/application.h" #include "base/device_pixel.h" #include "base/geometry_helper.h" -#include "cfmaterial_animation_factory.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/elevation_controller.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -35,8 +30,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; using namespace cf::ui::base::geometry; -using namespace cf::ui::components; -using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; using namespace cf::ui::widget::material::base; @@ -46,33 +39,13 @@ using namespace cf::ui::widget::application_support; // Constructor / Destructor // ============================================================================ -Button::Button(ButtonVariant variant, QWidget* parent) : QPushButton(parent), variant_(variant) { - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_elevation = new MdElevationController(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set initial elevation based on variant - // All buttons now have elevation 2 by default for press effect - m_elevation->setElevation(2); - - // Set ripple mode - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - connect(m_elevation, &MdElevationController::pressOffsetChanged, this, - static_cast(&QWidget::update)); - - // Set default font +Button::Button(ButtonVariant variant, QWidget* parent) + : QPushButton(parent), m_material(this, + MaterialWidgetBase::Config{ + .useElevation = true, + .initialElevation = 2, + }), + variant_(variant) { setFont(labelFont()); } @@ -91,71 +64,52 @@ Button::~Button() { void Button::enterEvent(QEnterEvent* event) { QPushButton::enterEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverEnter(); - update(); + m_material.onEnterEvent(); } void Button::leaveEvent(QEvent* event) { QPushButton::leaveEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverLeave(); - if (m_ripple) - m_ripple->onCancel(); - update(); + m_material.onLeaveEvent(); } void Button::mousePressEvent(QMouseEvent* event) { QPushButton::mousePressEvent(event); - if (m_stateMachine) - m_stateMachine->onPress(event->pos()); - if (m_ripple) - m_ripple->onPress(event->pos(), rect()); - if (m_elevation && m_pressEffectEnabled) - m_elevation->setPressed(true); - update(); + if (m_pressEffectEnabled) { + m_material.onMousePress(event->pos(), rect()); + } else { + m_material.stateMachine()->onPress(event->pos()); + if (m_material.ripple()) + m_material.ripple()->onPress(event->pos(), rect()); + update(); + } } void Button::mouseReleaseEvent(QMouseEvent* event) { QPushButton::mouseReleaseEvent(event); - if (m_stateMachine) - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); - if (m_elevation && m_pressEffectEnabled) - m_elevation->setPressed(false); - update(); + if (m_pressEffectEnabled) { + m_material.onMouseRelease(); + } else { + m_material.stateMachine()->onRelease(); + if (m_material.ripple()) + m_material.ripple()->onRelease(); + update(); + } } void Button::focusInEvent(QFocusEvent* event) { QPushButton::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); - update(); + m_material.onFocusIn(); } void Button::focusOutEvent(QFocusEvent* event) { QPushButton::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); - update(); + m_material.onFocusOut(); } void Button::changeEvent(QEvent* event) { QPushButton::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } @@ -164,25 +118,25 @@ void Button::changeEvent(QEvent* event) { // ============================================================================ int Button::elevation() const { - return m_elevation ? m_elevation->elevation() : 0; + return m_material.elevation() ? m_material.elevation()->elevation() : 0; } void Button::setElevation(int level) { - if (m_elevation) { - m_elevation->setElevation(level); + if (m_material.elevation()) { + m_material.elevation()->setElevation(level); update(); } } void Button::setLightSourceAngle(float degrees) { - if (m_elevation) { - m_elevation->setLightSourceAngle(degrees); + if (m_material.elevation()) { + m_material.elevation()->setLightSourceAngle(degrees); update(); } } float Button::lightSourceAngle() const { - return m_elevation ? m_elevation->lightSourceAngle() : 15.0f; + return m_material.elevation() ? m_material.elevation()->lightSourceAngle() : 15.0f; } void Button::setLeadingIcon(const QIcon& icon) { @@ -454,8 +408,8 @@ void Button::paintEvent(QPaintEvent* event) { // 计算按压偏移 float pressOffset = 0.0f; - if (m_pressEffectEnabled && m_elevation) { - pressOffset = m_elevation->pressOffset(); + if (m_pressEffectEnabled && m_material.elevation()) { + pressOffset = m_material.elevation()->pressOffset(); } // Calculate content area (inset to make room for shadow) @@ -499,7 +453,7 @@ void Button::paintEvent(QPaintEvent* event) { // Calculate shadow margin (extra space needed for shadow) QMarginsF Button::shadowMargin() const { - if (!m_elevation || m_elevation->elevation() <= 0) { + if (!m_material.elevation() || m_material.elevation()->elevation() <= 0) { return QMarginsF(0, 0, 0, 0); } @@ -507,14 +461,14 @@ QMarginsF Button::shadowMargin() const { // 根据 elevation 级别动态计算边距 // Level 2: blur=4dp, offset=2dp, 最大偏移约 3dp // 预留边距 = offset + blur/2,更精确的阴影空间 - int level = m_elevation->elevation(); + int level = m_material.elevation()->elevation(); float margin = helper.dpToPx(2.0f + level * 1.5f); // level 2: 约 5dp return QMarginsF(margin, margin, margin, margin); } void Button::drawShadow(QPainter& p, const QRectF& contentRect, const QPainterPath& shape) { - if (m_elevation && m_elevation->elevation() > 0) { - m_elevation->paintShadow(&p, shape); + if (m_material.elevation() && m_material.elevation()->elevation() > 0) { + m_material.elevation()->paintShadow(&p, shape); } } @@ -538,11 +492,11 @@ void Button::drawBackground(QPainter& p, const QPainterPath& shape) { } void Button::drawStateLayer(QPainter& p, const QPainterPath& shape) { - if (!isEnabled() || !m_stateMachine) { + if (!isEnabled() || !m_material.stateMachine()) { return; } - float opacity = m_stateMachine->stateLayerOpacity(); + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity <= 0.0f) { return; } @@ -555,10 +509,10 @@ void Button::drawStateLayer(QPainter& p, const QPainterPath& shape) { } void Button::drawRipple(QPainter& p, const QPainterPath& shape) { - if (m_ripple) { + if (m_material.ripple()) { // Set ripple color based on label color (content color) - m_ripple->setColor(labelColor()); - m_ripple->paint(&p, shape); + m_material.ripple()->setColor(labelColor()); + m_material.ripple()->paint(&p, shape); } } @@ -637,8 +591,8 @@ void Button::drawContent(QPainter& p, const QRectF& contentRect) { } void Button::drawFocusIndicator(QPainter& p, const QPainterPath& shape) { - if (m_focusIndicator) { - m_focusIndicator->paint(&p, shape, labelColor()); + if (m_material.focusIndicator()) { + m_material.focusIndicator()->paint(&p, shape, labelColor()); } } diff --git a/ui/widget/material/widget/button/button.h b/ui/widget/material/widget/button/button.h index 3b884af8e..79b1b8207 100644 --- a/ui/widget/material/widget/button/button.h +++ b/ui/widget/material/widget/button/button.h @@ -17,23 +17,14 @@ #include #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" #include #include #include namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdElevationController; -class MdFocusIndicator; -} // namespace base - using CFColor = cf::ui::base::CFColor; /** @@ -397,11 +388,7 @@ class CF_UI_EXPORT Button : public QPushButton { QFont labelFont() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdElevationController* m_elevation; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; ButtonVariant variant_; QIcon leadingIcon_; diff --git a/ui/widget/material/widget/checkbox/checkbox.cpp b/ui/widget/material/widget/checkbox/checkbox.cpp index 21e47f8ca..29f68b68e 100644 --- a/ui/widget/material/widget/checkbox/checkbox.cpp +++ b/ui/widget/material/widget/checkbox/checkbox.cpp @@ -17,12 +17,8 @@ #include "base/device_pixel.h" #include "base/easing.h" #include "base/geometry_helper.h" -#include "components/material/cfmaterial_animation_factory.h" #include "components/material/cfmaterial_property_animation.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -35,7 +31,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; using namespace cf::ui::base::geometry; -using namespace cf::ui::components; using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; @@ -45,38 +40,17 @@ using namespace cf::ui::widget::material::base; // Constructor / Destructor // ============================================================================ -CheckBox::CheckBox(QWidget* parent) : QCheckBox(parent) { - // Set size policy - Preferred allows proper layout behavior +CheckBox::CheckBox(QWidget* parent) + : QCheckBox(parent), m_material(this, MaterialWidgetBase::Config{.useElevation = false}) { setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - // Get animation factory from Application - m_animationFactory = cf::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode to bounded (checkbox has defined bounds) - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - - // Initialize check animation based on current state - // Both PartiallyChecked and Checked use 1.0 for full mark visibility if (checkState() == Qt::Checked) { m_checkAnimationProgress = 1.0f; - m_stateMachine->onCheckedChanged(true); + m_material.stateMachine()->onCheckedChanged(true); } else if (checkState() == Qt::PartiallyChecked) { m_checkAnimationProgress = 1.0f; } - // Set default cursor setCursor(Qt::PointingHandCursor); } @@ -119,8 +93,8 @@ void CheckBox::setCheckState(Qt::CheckState state) { void CheckBox::updateAnimationProgress(float progress, bool checked) { m_checkAnimationProgress = progress; - if (m_stateMachine) { - m_stateMachine->onCheckedChanged(checked); + if (m_material.stateMachine()) { + m_material.stateMachine()->onCheckedChanged(checked); } update(); } @@ -128,23 +102,21 @@ void CheckBox::updateAnimationProgress(float progress, bool checked) { void CheckBox::startCheckMarkAnimation(float target) { float fromValue = m_checkAnimationProgress; - if (!m_animationFactory) { - // No factory, set directly + auto factory = cf::WeakPtr::DynamicCast( + application_support::Application::animationFactory()); + + if (!factory) { m_checkAnimationProgress = target; update(); return; } - // Create property animation - auto anim = m_animationFactory->createPropertyAnimation( - &m_checkAnimationProgress, fromValue, target, 300, - cf::ui::base::Easing::Type::EmphasizedDecelerate, this); + auto anim = + factory->createPropertyAnimation(&m_checkAnimationProgress, fromValue, target, 300, + cf::ui::base::Easing::Type::EmphasizedDecelerate, this); if (anim) { - // IMPORTANT: Update range to fix cached animation's stale from/to values - if (auto* propAnim = - dynamic_cast( - anim.Get())) { + if (auto* propAnim = dynamic_cast(anim.Get())) { propAnim->setRange(fromValue, target); } anim->start(); @@ -164,78 +136,46 @@ CheckBox::~CheckBox() { void CheckBox::enterEvent(QEnterEvent* event) { QCheckBox::enterEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverEnter(); - update(); + m_material.onEnterEvent(); } void CheckBox::leaveEvent(QEvent* event) { QCheckBox::leaveEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverLeave(); - if (m_ripple) - m_ripple->onCancel(); - update(); + m_material.onLeaveEvent(); } void CheckBox::mousePressEvent(QMouseEvent* event) { QCheckBox::mousePressEvent(event); - if (m_stateMachine) - m_stateMachine->onPress(event->pos()); - if (m_ripple) - m_ripple->onPress(event->pos(), checkboxRect()); - update(); + m_material.onMousePress(event->pos(), checkboxRect()); } void CheckBox::mouseReleaseEvent(QMouseEvent* event) { QCheckBox::mouseReleaseEvent(event); - if (m_stateMachine) - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); - update(); + m_material.onMouseRelease(); } void CheckBox::focusInEvent(QFocusEvent* event) { QCheckBox::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); - update(); + m_material.onFocusIn(); } void CheckBox::focusOutEvent(QFocusEvent* event) { QCheckBox::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); - update(); + m_material.onFocusOut(); } void CheckBox::changeEvent(QEvent* event) { QCheckBox::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } void CheckBox::nextCheckState() { - // Animate from current state to new state Qt::CheckState oldState = checkState(); QCheckBox::nextCheckState(); Qt::CheckState newState = checkState(); - // Determine target progress based on new state - // Both PartiallyChecked and Checked use 1.0 for full mark visibility float newTarget = 0.0f; bool checked = false; @@ -254,18 +194,14 @@ void CheckBox::nextCheckState() { break; } - // Update state machine checked state - if (m_stateMachine) { - m_stateMachine->onCheckedChanged(checked); + if (m_material.stateMachine()) { + m_material.stateMachine()->onCheckedChanged(checked); } - // Start 1-second check mark animation startCheckMarkAnimation(newTarget); } bool CheckBox::hitButton(const QPoint& pos) const { - // For custom-drawn checkbox, entire widget area is clickable - // This ensures proper click handling even when text is empty return rect().contains(pos); } @@ -292,13 +228,6 @@ void CheckBox::setError(bool error) { QSize CheckBox::sizeHint() const { CanvasUnitHelper helper(qApp->devicePixelRatio()); - // Material Design checkbox specifications: - // - Checkbox size: 18dp - // - Spacing between checkbox and text: 12dp - // - Left padding (for focus indicator): 12dp - // - Right padding: 12dp - // - Touch target: 48dp minimum - float leftPadding = helper.dpToPx(12.0f); float rightPadding = helper.dpToPx(12.0f); float boxSize = helper.dpToPx(18.0f); @@ -306,8 +235,6 @@ QSize CheckBox::sizeHint() const { float textWidth = text().isEmpty() ? 0.0f : fontMetrics().horizontalAdvance(text()); float width = leftPadding + boxSize + spacing + textWidth + rightPadding; - - // Ensure minimum 48dp width for easy clicking (even without text) float minWidth = helper.dpToPx(48.0f); width = std::max(width, minWidth); @@ -317,8 +244,6 @@ QSize CheckBox::sizeHint() const { } QSize CheckBox::minimumSizeHint() const { - // Return the same as sizeHint to prevent text truncation - // This ensures the checkbox always has enough space for its text return sizeHint(); } @@ -327,7 +252,6 @@ QSize CheckBox::minimumSizeHint() const { // ============================================================================ namespace { -// Fallback colors when theme is not available inline CFColor fallbackPrimary() { return CFColor(103, 80, 164); } // Purple 700 @@ -356,7 +280,6 @@ CFColor CheckBox::checkmarkColor() const { return CFColor(colorScheme.queryColor(ERROR)); } - // Text uses PRIMARY for checked/indeterminate, ON_SURFACE for unchecked if (isChecked() || checkState() == Qt::PartiallyChecked) { return CFColor(colorScheme.queryColor(PRIMARY)); } @@ -366,11 +289,9 @@ CFColor CheckBox::checkmarkColor() const { } } -// Color for the check mark/indeterminate line (drawn on colored background) CFColor CheckBox::markDrawColor() const { auto* app = application_support::Application::instance(); if (!app) { - // White mark on primary background return CFColor(255, 255, 255); } @@ -397,7 +318,6 @@ CFColor CheckBox::borderColor() const { return CFColor(colorScheme.queryColor(ERROR)); } - // Checked/indeterminate uses primary, unchecked uses outline if (isChecked() || checkState() == Qt::PartiallyChecked) { return CFColor(colorScheme.queryColor(PRIMARY)); } @@ -411,7 +331,6 @@ CFColor CheckBox::borderColor() const { CFColor CheckBox::backgroundColor() const { auto* app = application_support::Application::instance(); if (!app) { - // No background for unchecked return CFColor(Qt::transparent); } @@ -419,7 +338,6 @@ CFColor CheckBox::backgroundColor() const { const auto& theme = app->currentTheme(); auto& colorScheme = theme.color_scheme(); - // Only checked/indeterminate has background if (isChecked() || checkState() == Qt::PartiallyChecked) { return CFColor(colorScheme.queryColor(PRIMARY)); } @@ -430,24 +348,20 @@ CFColor CheckBox::backgroundColor() const { } CFColor CheckBox::stateLayerColor() const { - // State layer uses the same color as the checkbox return checkmarkColor(); } float CheckBox::cornerRadius() const { - // Checkbox uses small corner radius (2dp) CanvasUnitHelper helper(qApp->devicePixelRatio()); return helper.dpToPx(2.0f); } float CheckBox::checkboxSize() const { - // Material Design checkbox is 18dp CanvasUnitHelper helper(qApp->devicePixelRatio()); return helper.dpToPx(18.0f); } float CheckBox::strokeWidth() const { - // Border stroke width is 2dp CanvasUnitHelper helper(qApp->devicePixelRatio()); return helper.dpToPx(2.0f); } @@ -459,11 +373,8 @@ float CheckBox::strokeWidth() const { QRectF CheckBox::checkboxRect() const { CanvasUnitHelper helper(qApp->devicePixelRatio()); - // Calculate vertical centering float boxSize = checkboxSize(); float y = (height() - boxSize) / 2.0f; - - // Add padding from left (for focus indicator spacing) float x = helper.dpToPx(12.0f); return QRectF(x, y, boxSize, boxSize); @@ -501,15 +412,12 @@ void CheckBox::paintEvent(QPaintEvent* event) { drawBorder(p, box); // Step 3: Draw check mark or indeterminate mark - // Draw based on actual checkState, not animation progress - // This ensures visual consistency even during animation Qt::CheckState state = checkState(); if (state == Qt::PartiallyChecked) { drawIndeterminateMark(p, box); } else if (state == Qt::Checked) { drawCheckMark(p, box); } - // Unchecked: no mark to draw // Step 4: Draw ripple drawRipple(p, box); @@ -529,13 +437,12 @@ void CheckBox::paintEvent(QPaintEvent* event) { void CheckBox::drawBackground(QPainter& p, const QRectF& rect) { if (checkState() == Qt::Unchecked) { - return; // No background for unchecked + return; } CFColor bgColor = backgroundColor(); QColor color = bgColor.native_color(); - // Handle disabled state if (!isEnabled()) { color.setAlphaF(0.38f); } @@ -548,20 +455,17 @@ void CheckBox::drawBorder(QPainter& p, const QRectF& rect) { CFColor bColor = borderColor(); QColor color = bColor.native_color(); - // Handle disabled state if (!isEnabled()) { color.setAlphaF(0.38f); } - // Handle state layer overlay - if (m_stateMachine && isEnabled()) { - float opacity = m_stateMachine->stateLayerOpacity(); + if (m_material.stateMachine() && isEnabled()) { + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity > 0.0f) { CFColor stateColor = stateLayerColor(); QColor stateQColor = stateColor.native_color(); stateQColor.setAlphaF(opacity); - // Blend state color with border color int r = int(color.red() * (1.0f - opacity) + stateQColor.red() * opacity); int g = int(color.green() * (1.0f - opacity) + stateQColor.green() * opacity); int b = int(color.blue() * (1.0f - opacity) + stateQColor.blue() * opacity); @@ -571,7 +475,6 @@ void CheckBox::drawBorder(QPainter& p, const QRectF& rect) { p.save(); - // Create inset path for border float inset = strokeWidth() / 2.0f; QRectF insetRect = rect.adjusted(inset, inset, -inset, -inset); float adjustedRadius = std::max(0.0f, cornerRadius() - inset); @@ -600,14 +503,9 @@ void CheckBox::drawCheckMark(QPainter& p, const QRectF& rect) { pen.setJoinStyle(Qt::RoundJoin); p.setPen(pen); - // Draw check mark with animation progress - // The check mark has two segments: - // 1. From bottom-left to center (0.0 to 0.5 progress) - // 2. From center to top-right (0.5 to 1.0 progress) float w = rect.width(); float h = rect.height(); - // Define check mark points (relative to rect, with margins) float left = rect.left() + w * 0.25f; float bottom = rect.top() + h * 0.55f; @@ -617,20 +515,17 @@ void CheckBox::drawCheckMark(QPainter& p, const QRectF& rect) { float right = rect.left() + w * 0.75f; float top = rect.top() + h * 0.35f; - // Get animation progress float progress = m_checkAnimationProgress; - // Draw the first segment (bottom-left to center) if (progress > 0.0f) { - float segment1Progress = std::min(progress * 2.0f, 1.0f); // 0.0-0.5 maps to 0.0-1.0 + float segment1Progress = std::min(progress * 2.0f, 1.0f); float currentX = left + (centerX - left) * segment1Progress; float currentY = bottom + (centerY - bottom) * segment1Progress; p.drawLine(QPointF(left, bottom), QPointF(currentX, currentY)); } - // Draw the second segment (center to top-right) if (progress > 0.5f) { - float segment2Progress = (progress - 0.5f) * 2.0f; // 0.5-1.0 maps to 0.0-1.0 + float segment2Progress = (progress - 0.5f) * 2.0f; float currentX = centerX + (right - centerX) * segment2Progress; float currentY = centerY + (top - centerY) * segment2Progress; p.drawLine(QPointF(centerX, centerY), QPointF(currentX, currentY)); @@ -652,8 +547,6 @@ void CheckBox::drawIndeterminateMark(QPainter& p, const QRectF& rect) { pen.setCapStyle(Qt::RoundCap); p.setPen(pen); - // Indeterminate mark is a horizontal line at center - // Animated from left to right using the same progress as check mark float w = rect.width(); float h = rect.height(); @@ -662,10 +555,8 @@ void CheckBox::drawIndeterminateMark(QPainter& p, const QRectF& rect) { float x1 = rect.left() + margin; float x2 = rect.right() - margin; - // Get animation progress float progress = m_checkAnimationProgress; - // Draw animated indeterminate line (left to right) if (progress > 0.0f) { float currentX = x1 + (x2 - x1) * progress; p.drawLine(QPointF(x1, y), QPointF(currentX, y)); @@ -675,12 +566,11 @@ void CheckBox::drawIndeterminateMark(QPainter& p, const QRectF& rect) { } void CheckBox::drawRipple(QPainter& p, const QRectF& rect) { - if (m_ripple) { - // Update ripple color based on current state - m_ripple->setColor(stateLayerColor()); + if (m_material.ripple()) { + m_material.ripple()->setColor(stateLayerColor()); QPainterPath clipPath = roundedRect(rect, cornerRadius()); - m_ripple->paint(&p, clipPath); + m_material.ripple()->paint(&p, clipPath); } } @@ -695,23 +585,20 @@ void CheckBox::drawText(QPainter& p, const QRectF& rect) { p.setPen(textColor.native_color()); } - // Use widget's font p.setFont(font()); - // Draw text vertically centered, left aligned - QRectF textBounds = rect.adjusted(0, 2, 0, -2); // Small adjustment for visual centering + QRectF textBounds = rect.adjusted(0, 2, 0, -2); p.drawText(textBounds, Qt::AlignLeft | Qt::AlignVCenter, text()); } void CheckBox::drawFocusIndicator(QPainter& p, const QRectF& rect) { - if (m_focusIndicator) { - // Expand rect slightly for focus ring + if (m_material.focusIndicator()) { CanvasUnitHelper helper(qApp->devicePixelRatio()); float margin = helper.dpToPx(4.0f); QRectF focusRect = rect.adjusted(-margin, -margin, margin, margin); QPainterPath shape = roundedRect(focusRect, cornerRadius() + margin); - m_focusIndicator->paint(&p, shape, checkmarkColor()); + m_material.focusIndicator()->paint(&p, shape, checkmarkColor()); } } diff --git a/ui/widget/material/widget/checkbox/checkbox.h b/ui/widget/material/widget/checkbox/checkbox.h index 36ac08d5b..0046ae386 100644 --- a/ui/widget/material/widget/checkbox/checkbox.h +++ b/ui/widget/material/widget/checkbox/checkbox.h @@ -1,351 +1,340 @@ -/** - * @file ui/widget/material/widget/checkbox/checkbox.h - * @brief Material Design 3 CheckBox widget. - * - * Implements Material Design 3 checkbox with support for unchecked, checked, - * and indeterminate states. Includes ripple effects, state layers, and focus - * indicators following Material Design 3 specifications. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" -#include "export.h" -#include -#include - -namespace cf::ui::widget::material { - -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 CheckBox widget. - * - * @details Implements Material Design 3 checkbox with support for unchecked, - * checked, and indeterminate states. Includes ripple effects, state - * layers, and focus indicators following Material Design 3 - * specifications. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT CheckBox : public QCheckBox { - Q_OBJECT - Q_PROPERTY(bool error READ hasError WRITE setError NOTIFY errorChanged) - - public: - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit CheckBox(QWidget* parent = nullptr); - - /** - * @brief Constructor with text. - * - * @param[in] text CheckBox text label. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit CheckBox(const QString& text, QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~CheckBox() override; - - /** - * @brief Sets the checked state. - * - * @param[in] checked true to check, false to uncheck. - * - * @throws None - * @note Updates animation progress for correct rendering. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setChecked(bool checked); - - /** - * @brief Sets the check state. - * - * @param[in] state The check state to set. - * - * @throws None - * @note Updates animation progress for correct rendering. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setCheckState(Qt::CheckState state); - - /** - * @brief Gets whether the checkbox is in error state. - * - * @return true if error state is active, false otherwise. - * - * @throws None - * @note Error state affects the border color. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool hasError() const; - - /** - * @brief Sets the error state. - * - * @param[in] error true to set error state, false to clear. - * - * @throws None - * @note Error state uses error color for the border. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setError(bool error); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the checkbox. - * - * @throws None - * @note Based on icon size, text, and spacing. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures touch target size requirements. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - signals: - /** - * @brief Signal emitted when error state changes. - * - * @param[in] error The new error state. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void errorChanged(bool error); - - protected: - /** - * @brief Paints the checkbox. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements Material Design paint pipeline. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles mouse enter event. - * - * @param[in] event Enter event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void enterEvent(QEnterEvent* event) override; - - /** - * @brief Handles mouse leave event. - * - * @param[in] event Leave event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void leaveEvent(QEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple and press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Handles check state change event. - * - * @param[in] event State change event. - * - * @throws None - * @note Triggers check mark animation. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void nextCheckState() override; - - /** - * @brief Determines if a point is within the clickable button area. - * - * @param[in] pos The point to check, in widget coordinates. - * @return true if the point is within the clickable area, false otherwise. - * - * @throws None - * @note Overrides QAbstractButton behavior to make entire widget clickable. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool hitButton(const QPoint& pos) const override; - - private: - // Drawing helpers - Material Design paint pipeline - QRectF checkboxRect() const; - QRectF textRect() const; - void drawBackground(QPainter& p, const QRectF& rect); - void drawBorder(QPainter& p, const QRectF& rect); - void drawCheckMark(QPainter& p, const QRectF& rect); - void drawIndeterminateMark(QPainter& p, const QRectF& rect); - void drawRipple(QPainter& p, const QRectF& rect); - void drawText(QPainter& p, const QRectF& rect); - void drawFocusIndicator(QPainter& p, const QRectF& rect); - - // Animation helper - void updateAnimationProgress(float progress, bool checked); - void startCheckMarkAnimation(float target); - - // Color access methods - CFColor checkmarkColor() const; - CFColor markDrawColor() const; // Color for check/indeterminate mark on background - CFColor borderColor() const; - CFColor backgroundColor() const; - CFColor stateLayerColor() const; - float cornerRadius() const; - - // Helper to get checkbox size in pixels - float checkboxSize() const; - float strokeWidth() const; - - // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; - - // Check mark animation progress (0.0 to 1.0) - float m_checkAnimationProgress = 0.0f; - - // Error state - bool m_error = false; -}; - -} // namespace cf::ui::widget::material +/** + * @file ui/widget/material/widget/checkbox/checkbox.h + * @brief Material Design 3 CheckBox widget. + * + * Implements Material Design 3 checkbox with support for unchecked, checked, + * and indeterminate states. Includes ripple effects, state layers, and focus + * indicators following Material Design 3 specifications. + * + * @author CFDesktop Team + * @date 2026-03-01 + * @version 0.1 + * @since 0.1 + * @ingroup ui_widget_material_widget + */ +#pragma once + +#include "base/color.h" +#include "export.h" +#include "widget/material/base/material_widget_base.h" +#include +#include + +namespace cf::ui::widget::material { + +using CFColor = cf::ui::base::CFColor; + +/** + * @brief Material Design 3 CheckBox widget. + * + * @details Implements Material Design 3 checkbox with support for unchecked, + * checked, and indeterminate states. Includes ripple effects, state + * layers, and focus indicators following Material Design 3 + * specifications. + * + * @since 0.1 + * @ingroup ui_widget_material_widget + */ +class CF_UI_EXPORT CheckBox : public QCheckBox { + Q_OBJECT + Q_PROPERTY(bool error READ hasError WRITE setError NOTIFY errorChanged) + + public: + /** + * @brief Constructor. + * + * @param[in] parent QObject parent. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + explicit CheckBox(QWidget* parent = nullptr); + + /** + * @brief Constructor with text. + * + * @param[in] text CheckBox text label. + * @param[in] parent QObject parent. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + explicit CheckBox(const QString& text, QWidget* parent = nullptr); + + /** + * @brief Destructor. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + ~CheckBox() override; + + /** + * @brief Sets the checked state. + * + * @param[in] checked true to check, false to uncheck. + * + * @throws None + * @note Updates animation progress for correct rendering. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setChecked(bool checked); + + /** + * @brief Sets the check state. + * + * @param[in] state The check state to set. + * + * @throws None + * @note Updates animation progress for correct rendering. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setCheckState(Qt::CheckState state); + + /** + * @brief Gets whether the checkbox is in error state. + * + * @return true if error state is active, false otherwise. + * + * @throws None + * @note Error state affects the border color. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + bool hasError() const; + + /** + * @brief Sets the error state. + * + * @param[in] error true to set error state, false to clear. + * + * @throws None + * @note Error state uses error color for the border. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setError(bool error); + + /** + * @brief Gets the recommended size. + * + * @return Recommended size for the checkbox. + * + * @throws None + * @note Based on icon size, text, and spacing. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QSize sizeHint() const override; + + /** + * @brief Gets the minimum recommended size. + * + * @return Minimum recommended size. + * + * @throws None + * @note Ensures touch target size requirements. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QSize minimumSizeHint() const override; + + signals: + /** + * @brief Signal emitted when error state changes. + * + * @param[in] error The new error state. + * + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void errorChanged(bool error); + + protected: + /** + * @brief Paints the checkbox. + * + * @param[in] event Paint event. + * + * @throws None + * @note Implements Material Design paint pipeline. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void paintEvent(QPaintEvent* event) override; + + /** + * @brief Handles mouse enter event. + * + * @param[in] event Enter event. + * + * @throws None + * @note Updates hover state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void enterEvent(QEnterEvent* event) override; + + /** + * @brief Handles mouse leave event. + * + * @param[in] event Leave event. + * + * @throws None + * @note Updates hover state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void leaveEvent(QEvent* event) override; + + /** + * @brief Handles mouse press event. + * + * @param[in] event Mouse event. + * + * @throws None + * @note Triggers ripple and press state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void mousePressEvent(QMouseEvent* event) override; + + /** + * @brief Handles mouse release event. + * + * @param[in] event Mouse event. + * + * @throws None + * @note Updates press state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void mouseReleaseEvent(QMouseEvent* event) override; + + /** + * @brief Handles focus in event. + * + * @param[in] event Focus event. + * + * @throws None + * @note Shows focus indicator. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void focusInEvent(QFocusEvent* event) override; + + /** + * @brief Handles focus out event. + * + * @param[in] event Focus event. + * + * @throws None + * @note Hides focus indicator. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void focusOutEvent(QFocusEvent* event) override; + + /** + * @brief Handles widget state change event. + * + * @param[in] event Change event. + * + * @throws None + * @note Updates appearance based on state changes. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void changeEvent(QEvent* event) override; + + /** + * @brief Handles check state change event. + * + * @param[in] event State change event. + * + * @throws None + * @note Triggers check mark animation. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void nextCheckState() override; + + /** + * @brief Determines if a point is within the clickable button area. + * + * @param[in] pos The point to check, in widget coordinates. + * @return true if the point is within the clickable area, false otherwise. + * + * @throws None + * @note Overrides QAbstractButton behavior to make entire widget clickable. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + bool hitButton(const QPoint& pos) const override; + + private: + // Drawing helpers - Material Design paint pipeline + QRectF checkboxRect() const; + QRectF textRect() const; + void drawBackground(QPainter& p, const QRectF& rect); + void drawBorder(QPainter& p, const QRectF& rect); + void drawCheckMark(QPainter& p, const QRectF& rect); + void drawIndeterminateMark(QPainter& p, const QRectF& rect); + void drawRipple(QPainter& p, const QRectF& rect); + void drawText(QPainter& p, const QRectF& rect); + void drawFocusIndicator(QPainter& p, const QRectF& rect); + + // Animation helper + void updateAnimationProgress(float progress, bool checked); + void startCheckMarkAnimation(float target); + + // Color access methods + CFColor checkmarkColor() const; + CFColor markDrawColor() const; // Color for check/indeterminate mark on background + CFColor borderColor() const; + CFColor backgroundColor() const; + CFColor stateLayerColor() const; + float cornerRadius() const; + + // Helper to get checkbox size in pixels + float checkboxSize() const; + float strokeWidth() const; + + // Behavior components + base::MaterialWidgetBase m_material; + + // Check mark animation progress (0.0 to 1.0) + float m_checkAnimationProgress = 0.0f; + + // Error state + bool m_error = false; +}; + +} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/comboBox/combobox.cpp b/ui/widget/material/widget/comboBox/combobox.cpp index af70e4ca3..4186ae564 100644 --- a/ui/widget/material/widget/comboBox/combobox.cpp +++ b/ui/widget/material/widget/comboBox/combobox.cpp @@ -17,12 +17,10 @@ #include "base/device_pixel.h" #include "base/easing.h" #include "base/geometry_helper.h" +#include "base/include/base/weak_ptr/weak_ptr.h" #include "components/material/cfmaterial_animation_factory.h" #include "components/material/cfmaterial_property_animation.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -68,31 +66,19 @@ constexpr float DEFAULT_WIDTH_DP = 200.0f; // Constructor / Destructor // ============================================================================ -ComboBox::ComboBox(QWidget* parent) : QComboBox(parent), variant_(ComboBoxVariant::Filled) { +ComboBox::ComboBox(QWidget* parent) + : QComboBox(parent), variant_(ComboBoxVariant::Filled), + m_material(this, base::MaterialWidgetBase::Config{ + .useRipple = true, + .useElevation = false, + .useFocusIndicator = true, + }) { // Set size policy setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); // Set minimum contents length for proper sizing setMinimumContentsLength(1); - // Get animation factory from Application - m_animationFactory = cf::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode to bounded - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - // Initialize popup animation m_popupAnimation = new QPropertyAnimation(this); m_popupAnimation->setEasingCurve(QEasingCurve::OutCubic); @@ -123,75 +109,50 @@ void ComboBox::setVariant(ComboBoxVariant variant) { void ComboBox::enterEvent(QEnterEvent* event) { QComboBox::enterEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverEnter(); - update(); + m_material.onEnterEvent(); } void ComboBox::leaveEvent(QEvent* event) { QComboBox::leaveEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverLeave(); - if (m_ripple) - m_ripple->onCancel(); - update(); + m_material.onLeaveEvent(); } void ComboBox::mousePressEvent(QMouseEvent* event) { QComboBox::mousePressEvent(event); - if (m_stateMachine) - m_stateMachine->onPress(event->pos()); - if (m_ripple) - m_ripple->onPress(event->pos(), fieldRect()); - update(); + m_material.onMousePress(event->pos(), fieldRect()); } void ComboBox::mouseReleaseEvent(QMouseEvent* event) { QComboBox::mouseReleaseEvent(event); - if (m_stateMachine) - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); - update(); + m_material.onMouseRelease(); } void ComboBox::focusInEvent(QFocusEvent* event) { QComboBox::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); - update(); + m_material.onFocusIn(); } void ComboBox::focusOutEvent(QFocusEvent* event) { QComboBox::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); - update(); + m_material.onFocusOut(); } void ComboBox::changeEvent(QEvent* event) { QComboBox::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } void ComboBox::showPopup() { + // Get animation factory locally for custom arrow animation + auto factory = cf::WeakPtr::DynamicCast( + application_support::Application::animationFactory()); + // Start arrow rotation animation - if (m_animationFactory) { - auto anim = m_animationFactory->createPropertyAnimation( - &m_arrowRotation, 0.0f, 180.0f, 200, cf::ui::base::Easing::Type::Standard, this); + if (factory) { + auto anim = factory->createPropertyAnimation(&m_arrowRotation, 0.0f, 180.0f, 200, + cf::ui::base::Easing::Type::Standard, this); if (anim) { // IMPORTANT: Update range to fix cached animation's stale from/to values if (auto* propAnim = @@ -266,11 +227,14 @@ void ComboBox::hidePopup() { m_popupAnimation->stop(); } + // Get animation factory locally for custom arrow animation + auto factory = cf::WeakPtr::DynamicCast( + application_support::Application::animationFactory()); + // Reset arrow rotation animation - if (m_animationFactory) { - auto anim = m_animationFactory->createPropertyAnimation( - &m_arrowRotation, m_arrowRotation, 0.0f, 150, cf::ui::base::Easing::Type::Standard, - this); + if (factory) { + auto anim = factory->createPropertyAnimation(&m_arrowRotation, m_arrowRotation, 0.0f, 150, + cf::ui::base::Easing::Type::Standard, this); if (anim) { // IMPORTANT: Update range to fix cached animation's stale from/to values if (auto* propAnim = @@ -524,8 +488,8 @@ void ComboBox::drawOutline(QPainter& p, const QPainterPath& shape) { // For filled variant, outline is only visible when hovered/focused if (variant_ == ComboBoxVariant::Filled) { - if (m_stateMachine && isEnabled()) { - float opacity = m_stateMachine->stateLayerOpacity(); + if (m_material.stateMachine() && isEnabled()) { + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity > 0.0f) { color.setAlphaF(opacity); } else { @@ -549,11 +513,11 @@ void ComboBox::drawOutline(QPainter& p, const QPainterPath& shape) { } void ComboBox::drawStateLayer(QPainter& p, const QPainterPath& shape) { - if (!m_stateMachine || !isEnabled()) { + if (!m_material.stateMachine() || !isEnabled()) { return; } - float opacity = m_stateMachine->stateLayerOpacity(); + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity <= 0.0f) { return; } @@ -566,11 +530,11 @@ void ComboBox::drawStateLayer(QPainter& p, const QPainterPath& shape) { } void ComboBox::drawRipple(QPainter& p, const QPainterPath& shape) { - if (m_ripple) { + if (m_material.ripple()) { // Update ripple color based on current state - m_ripple->setColor(stateLayerColor()); + m_material.ripple()->setColor(stateLayerColor()); - m_ripple->paint(&p, shape); + m_material.ripple()->paint(&p, shape); } } @@ -624,7 +588,7 @@ void ComboBox::drawArrow(QPainter& p, const QRectF& rect) { } void ComboBox::drawFocusIndicator(QPainter& p, const QPainterPath& shape) { - if (m_focusIndicator) { + if (m_material.focusIndicator()) { // Expand rect slightly for focus ring CanvasUnitHelper helper(qApp->devicePixelRatio()); float margin = helper.dpToPx(FOCUS_RING_MARGIN_DP); @@ -632,7 +596,7 @@ void ComboBox::drawFocusIndicator(QPainter& p, const QPainterPath& shape) { QRectF focusRect = field.adjusted(-margin, -margin, margin, margin); QPainterPath focusShape = roundedRect(focusRect, cornerRadius() + margin); - m_focusIndicator->paint(&p, focusShape, containerColor()); + m_material.focusIndicator()->paint(&p, focusShape, containerColor()); } } diff --git a/ui/widget/material/widget/comboBox/combobox.h b/ui/widget/material/widget/comboBox/combobox.h index 95c57b010..f23b13d99 100644 --- a/ui/widget/material/widget/comboBox/combobox.h +++ b/ui/widget/material/widget/comboBox/combobox.h @@ -14,22 +14,14 @@ #pragma once #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" #include #include #include namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - using CFColor = cf::ui::base::CFColor; /** @@ -285,10 +277,7 @@ class CF_UI_EXPORT ComboBox : public QComboBox { float arrowRotation() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Variant ComboBoxVariant variant_; diff --git a/ui/widget/material/widget/doublespinbox/doublespinbox.cpp b/ui/widget/material/widget/doublespinbox/doublespinbox.cpp index f054c0eb9..ab8cea448 100644 --- a/ui/widget/material/widget/doublespinbox/doublespinbox.cpp +++ b/ui/widget/material/widget/doublespinbox/doublespinbox.cpp @@ -17,11 +17,7 @@ #include "application_support/application.h" #include "base/device_pixel.h" #include "base/geometry_helper.h" -#include "cfmaterial_animation_factory.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -35,8 +31,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; using namespace cf::ui::base::geometry; -using namespace cf::ui::components; -using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; using namespace cf::ui::widget::material::base; @@ -70,7 +64,14 @@ inline CFColor fallbackPrimary() { // ============================================================================ DoubleSpinBox::DoubleSpinBox(QWidget* parent) - : QDoubleSpinBox(parent), m_hoveringIncrementButton(false), m_hoveringDecrementButton(false), + : QDoubleSpinBox(parent), m_material(this, + MaterialWidgetBase::Config{ + .useRipple = true, + .useElevation = true, + .useFocusIndicator = true, + .initialElevation = 0, + }), + m_hoveringIncrementButton(false), m_hoveringDecrementButton(false), m_pressingIncrementButton(false), m_pressingDecrementButton(false) { // Disable native frame and background @@ -89,24 +90,6 @@ DoubleSpinBox::DoubleSpinBox(QWidget* parent) lineEdit()->setAlignment(Qt::AlignRight); } - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - // Set default font setFont(textFont()); @@ -127,22 +110,16 @@ DoubleSpinBox::~DoubleSpinBox() { void DoubleSpinBox::enterEvent(QEnterEvent* event) { QDoubleSpinBox::enterEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverEnter(); - update(); + m_material.onEnterEvent(); } void DoubleSpinBox::leaveEvent(QEvent* event) { QDoubleSpinBox::leaveEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverLeave(); - if (m_ripple) - m_ripple->onCancel(); + m_material.onLeaveEvent(); // Reset button hover states m_hoveringIncrementButton = false; m_hoveringDecrementButton = false; - update(); } void DoubleSpinBox::mousePressEvent(QMouseEvent* event) { @@ -162,11 +139,7 @@ void DoubleSpinBox::mousePressEvent(QMouseEvent* event) { } QDoubleSpinBox::mousePressEvent(event); - if (m_stateMachine) - m_stateMachine->onPress(event->pos()); - if (m_ripple) - m_ripple->onPress(event->pos(), rect()); - update(); + m_material.onMousePress(event->pos(), rect()); } void DoubleSpinBox::mouseReleaseEvent(QMouseEvent* event) { @@ -179,11 +152,7 @@ void DoubleSpinBox::mouseReleaseEvent(QMouseEvent* event) { } QDoubleSpinBox::mouseReleaseEvent(event); - if (m_stateMachine) - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); - update(); + m_material.onMouseRelease(); } void DoubleSpinBox::mouseMoveEvent(QMouseEvent* event) { @@ -193,32 +162,18 @@ void DoubleSpinBox::mouseMoveEvent(QMouseEvent* event) { void DoubleSpinBox::focusInEvent(QFocusEvent* event) { QDoubleSpinBox::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); - update(); + m_material.onFocusIn(); } void DoubleSpinBox::focusOutEvent(QFocusEvent* event) { QDoubleSpinBox::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); - update(); + m_material.onFocusOut(); } void DoubleSpinBox::changeEvent(QEvent* event) { QDoubleSpinBox::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } + m_material.onEnabledChange(isEnabled()); updateTextColor(); update(); } @@ -522,11 +477,11 @@ void DoubleSpinBox::drawBackground(QPainter& p, const QPainterPath& shape) { } void DoubleSpinBox::drawStateLayer(QPainter& p, const QPainterPath& shape) { - if (!isEnabled() || !m_stateMachine) { + if (!isEnabled() || !m_material.stateMachine()) { return; } - float opacity = m_stateMachine->stateLayerOpacity(); + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity <= 0.0f) { return; } @@ -539,10 +494,10 @@ void DoubleSpinBox::drawStateLayer(QPainter& p, const QPainterPath& shape) { } void DoubleSpinBox::drawRipple(QPainter& p, const QPainterPath& shape) { - if (m_ripple) { + if (m_material.ripple()) { // Set ripple color based on text color - m_ripple->setColor(textColor()); - m_ripple->paint(&p, shape); + m_material.ripple()->setColor(textColor()); + m_material.ripple()->paint(&p, shape); } } @@ -692,8 +647,8 @@ void DoubleSpinBox::drawButtons(QPainter& p, const QRectF& buttonRect) { } void DoubleSpinBox::drawFocusIndicator(QPainter& p, const QPainterPath& shape) { - if (m_focusIndicator && hasFocus()) { - m_focusIndicator->paint(&p, shape, focusOutlineColor()); + if (m_material.focusIndicator() && hasFocus()) { + m_material.focusIndicator()->paint(&p, shape, focusOutlineColor()); } } diff --git a/ui/widget/material/widget/doublespinbox/doublespinbox.h b/ui/widget/material/widget/doublespinbox/doublespinbox.h index c874676e8..41c5a1bca 100644 --- a/ui/widget/material/widget/doublespinbox/doublespinbox.h +++ b/ui/widget/material/widget/doublespinbox/doublespinbox.h @@ -18,21 +18,13 @@ #include #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" #include #include namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - using CFColor = cf::ui::base::CFColor; /** @@ -268,10 +260,7 @@ class CF_UI_EXPORT DoubleSpinBox : public QDoubleSpinBox { QFont textFont() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Internal state bool m_hoveringIncrementButton; diff --git a/ui/widget/material/widget/listview/listview.cpp b/ui/widget/material/widget/listview/listview.cpp index 1340d2189..9679338a4 100644 --- a/ui/widget/material/widget/listview/listview.cpp +++ b/ui/widget/material/widget/listview/listview.cpp @@ -18,11 +18,7 @@ #include "application_support/application.h" #include "base/device_pixel.h" #include "base/geometry_helper.h" -#include "cfmaterial_animation_factory.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -72,8 +68,6 @@ class ListViewDelegate : public QStyledItemDelegate { using namespace cf::ui::base; using namespace cf::ui::base::device; using namespace cf::ui::base::geometry; -using namespace cf::ui::components; -using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; using namespace cf::ui::widget::material::base; @@ -84,33 +78,21 @@ using namespace cf::ui::widget::application_support; // ============================================================================ ListView::ListView(QWidget* parent) - : QListView(parent), m_itemHeight(ItemHeight::SingleLine), m_showSeparator(true), - m_rippleEnabled(true), m_hoveredIndex(-1), m_pressedIndex(-1), m_delegate(nullptr) { - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - + : QListView(parent), m_material(this, + MaterialWidgetBase::Config{ + .useRipple = true, + .useElevation = false, + .useFocusIndicator = true, + }), + m_itemHeight(ItemHeight::SingleLine), m_showSeparator(true), m_rippleEnabled(true), + m_hoveredIndex(-1), m_pressedIndex(-1), m_delegate(nullptr) { // Initialize and set delegate for item size control m_delegate = new ListViewDelegate(this); setItemDelegate(m_delegate); - // Set ripple mode to bounded (clipped to item bounds) - m_ripple->setMode(RippleHelper::Mode::Bounded); - // Configure viewport for mouse tracking viewport()->setMouseTracking(true); - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, viewport(), - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - // Set default selection mode setSelectionMode(QAbstractItemView::SingleSelection); setSelectionBehavior(QAbstractItemView::SelectRows); @@ -132,20 +114,12 @@ ListView::~ListView() { void ListView::enterEvent(QEnterEvent* event) { QListView::enterEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverEnter(); - } - update(); + m_material.onEnterEvent(); } void ListView::leaveEvent(QEvent* event) { QListView::leaveEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverLeave(); - } - if (m_ripple) { - m_ripple->onCancel(); - } + m_material.onLeaveEvent(); m_hoveredIndex = -1; update(); } @@ -159,13 +133,7 @@ void ListView::mousePressEvent(QMouseEvent* event) { m_pressedIndex = index.row(); m_pressPosition = event->pos(); - if (m_stateMachine) { - m_stateMachine->onPress(event->pos()); - } - if (m_ripple) { - QRectF itemRect = visualItemRect(index); - m_ripple->onPress(event->pos(), itemRect); - } + m_material.onMousePress(event->pos(), visualItemRect(index)); } update(); } @@ -174,12 +142,7 @@ void ListView::mouseReleaseEvent(QMouseEvent* event) { QListView::mouseReleaseEvent(event); if (m_pressedIndex >= 0) { - if (m_stateMachine) { - m_stateMachine->onRelease(); - } - if (m_ripple) { - m_ripple->onRelease(); - } + m_material.onMouseRelease(); m_pressedIndex = -1; } update(); @@ -187,37 +150,18 @@ void ListView::mouseReleaseEvent(QMouseEvent* event) { void ListView::focusInEvent(QFocusEvent* event) { QListView::focusInEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusIn(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusIn(); - } - update(); + m_material.onFocusIn(); } void ListView::focusOutEvent(QFocusEvent* event) { QListView::focusOutEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusOut(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusOut(); - } - update(); + m_material.onFocusOut(); } void ListView::changeEvent(QEvent* event) { QListView::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } @@ -579,7 +523,7 @@ void ListView::drawItemBackground(QPainter& p, const QRectF& itemRect, int index } void ListView::drawItemStateLayer(QPainter& p, const QRectF& itemRect, int index) { - if (!isEnabled() || !m_stateMachine) { + if (!isEnabled() || !m_material.stateMachine()) { return; } @@ -588,7 +532,7 @@ void ListView::drawItemStateLayer(QPainter& p, const QRectF& itemRect, int index return; } - float opacity = m_stateMachine->stateLayerOpacity(); + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity <= 0.0f) { return; } @@ -602,11 +546,11 @@ void ListView::drawItemStateLayer(QPainter& p, const QRectF& itemRect, int index } void ListView::drawItemRipple(QPainter& p, const QRectF& itemRect, int index) { - if (!m_rippleEnabled || !m_ripple) { + if (!m_rippleEnabled || !m_material.ripple()) { return; } - m_ripple->setColor(textColor()); + m_material.ripple()->setColor(textColor()); // Create a clip path for the item QPainterPath clipPath; @@ -614,7 +558,7 @@ void ListView::drawItemRipple(QPainter& p, const QRectF& itemRect, int index) { p.save(); p.setClipPath(clipPath); - m_ripple->paint(&p, clipPath); + m_material.ripple()->paint(&p, clipPath); p.restore(); } @@ -727,7 +671,7 @@ void ListView::drawSeparator(QPainter& p, const QRectF& itemRect) { } void ListView::drawFocusIndicator(QPainter& p, const QRectF& itemRect, int index) { - if (!m_focusIndicator) { + if (!m_material.focusIndicator()) { return; } @@ -736,7 +680,7 @@ void ListView::drawFocusIndicator(QPainter& p, const QRectF& itemRect, int index shape.addRect(itemRect); // Use the text color as the focus indicator color - m_focusIndicator->paint(&p, shape, textColor()); + m_material.focusIndicator()->paint(&p, shape, textColor()); } } // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/listview/listview.h b/ui/widget/material/widget/listview/listview.h index 4b8838153..454e58dd5 100644 --- a/ui/widget/material/widget/listview/listview.h +++ b/ui/widget/material/widget/listview/listview.h @@ -19,19 +19,11 @@ #include #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - class ListViewDelegate; using CFColor = cf::ui::base::CFColor; @@ -341,10 +333,7 @@ class CF_UI_EXPORT ListView : public QListView { QFont secondaryFont() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Item properties ItemHeight m_itemHeight; diff --git a/ui/widget/material/widget/progressbar/progressbar.cpp b/ui/widget/material/widget/progressbar/progressbar.cpp index 50d3c515c..59e6fab15 100644 --- a/ui/widget/material/widget/progressbar/progressbar.cpp +++ b/ui/widget/material/widget/progressbar/progressbar.cpp @@ -18,11 +18,10 @@ #include "base/device_pixel.h" #include "base/easing.h" #include "base/geometry_helper.h" +#include "base/include/base/weak_ptr/weak_ptr.h" #include "components/material/cfmaterial_animation_factory.h" #include "components/material/cfmaterial_property_animation.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/state_machine.h" #include #include @@ -56,22 +55,15 @@ constexpr float FOCUS_RING_MARGIN_DP = 4.0f; // Constructor / Destructor // ============================================================================ -ProgressBar::ProgressBar(QWidget* parent) : QProgressBar(parent) { +ProgressBar::ProgressBar(QWidget* parent) + : QProgressBar(parent), m_material(this, base::MaterialWidgetBase::Config{ + .useRipple = false, + .useElevation = false, + .useFocusIndicator = true, + }) { // Set size policy setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - // Get animation factory from Application - m_animationFactory = cf::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Connect repaint signals - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - // Start indeterminate animation if in indeterminate mode if (minimum() == 0 && maximum() == 0) { startIndeterminateAnimation(); @@ -89,47 +81,28 @@ ProgressBar::~ProgressBar() { void ProgressBar::enterEvent(QEnterEvent* event) { QProgressBar::enterEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverEnter(); - update(); + m_material.onEnterEvent(); } void ProgressBar::leaveEvent(QEvent* event) { QProgressBar::leaveEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverLeave(); - update(); + m_material.onLeaveEvent(); } void ProgressBar::focusInEvent(QFocusEvent* event) { QProgressBar::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); - update(); + m_material.onFocusIn(); } void ProgressBar::focusOutEvent(QFocusEvent* event) { QProgressBar::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); - update(); + m_material.onFocusOut(); } void ProgressBar::changeEvent(QEvent* event) { QProgressBar::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } @@ -253,7 +226,11 @@ QRectF ProgressBar::trackRect() const { // ============================================================================ void ProgressBar::startIndeterminateAnimation() { - if (!m_animationFactory || m_indeterminateAnimating) { + // Get animation factory locally for custom indeterminate animation + auto factory = cf::WeakPtr::DynamicCast( + application_support::Application::animationFactory()); + + if (!factory || m_indeterminateAnimating) { return; } @@ -261,8 +238,8 @@ void ProgressBar::startIndeterminateAnimation() { // Create a looping animation for indeterminate mode // Using a property animation on m_indeterminatePosition - auto anim = m_animationFactory->createPropertyAnimation( - &m_indeterminatePosition, 0.0f, 1.0f, 1500, cf::ui::base::Easing::Type::Linear, this); + auto anim = factory->createPropertyAnimation(&m_indeterminatePosition, 0.0f, 1.0f, 1500, + cf::ui::base::Easing::Type::Linear, this); if (anim) { // IMPORTANT: Update range to fix cached animation's stale from/to values @@ -348,8 +325,8 @@ void ProgressBar::drawFill(QPainter& p, const QRectF& trackRect) { } // Handle state layer overlay - if (m_stateMachine && isEnabled()) { - float opacity = m_stateMachine->stateLayerOpacity(); + if (m_material.stateMachine() && isEnabled()) { + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity > 0.0f) { CFColor stateColor = stateLayerColor(); QColor stateQColor = stateColor.native_color(); @@ -442,13 +419,13 @@ void ProgressBar::drawText(QPainter& p, const QRectF& rect) { } void ProgressBar::drawFocusIndicator(QPainter& p, const QRectF& rect) { - if (m_focusIndicator) { + if (m_material.focusIndicator()) { CanvasUnitHelper helper(qApp->devicePixelRatio()); float margin = helper.dpToPx(FOCUS_RING_MARGIN_DP); QRectF focusRect = rect.adjusted(-margin, -margin, margin, margin); QPainterPath shape = roundedRect(focusRect, cornerRadius() + margin); - m_focusIndicator->paint(&p, shape, fillColor()); + m_material.focusIndicator()->paint(&p, shape, fillColor()); } } diff --git a/ui/widget/material/widget/progressbar/progressbar.h b/ui/widget/material/widget/progressbar/progressbar.h index cf32149a9..05a6324ac 100644 --- a/ui/widget/material/widget/progressbar/progressbar.h +++ b/ui/widget/material/widget/progressbar/progressbar.h @@ -15,20 +15,13 @@ #pragma once #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" #include #include namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class MdFocusIndicator; -} // namespace base - using CFColor = cf::ui::base::CFColor; /** @@ -197,9 +190,7 @@ class CF_UI_EXPORT ProgressBar : public QProgressBar { void updateIndeterminatePosition(); // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Indeterminate animation state (0.0 to 1.0) float m_indeterminatePosition = 0.0f; diff --git a/ui/widget/material/widget/radiobutton/radiobutton.cpp b/ui/widget/material/widget/radiobutton/radiobutton.cpp index 125e798dd..573e0d209 100644 --- a/ui/widget/material/widget/radiobutton/radiobutton.cpp +++ b/ui/widget/material/widget/radiobutton/radiobutton.cpp @@ -16,12 +16,8 @@ #include "application_support/application.h" #include "base/device_pixel.h" #include "base/easing.h" -#include "cfmaterial_animation_factory.h" #include "components/material/cfmaterial_property_animation.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -33,7 +29,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; -using namespace cf::ui::components; using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; @@ -42,14 +37,12 @@ using namespace cf::ui::widget::application_support; // Material Design 3 specifications for RadioButton namespace { -// Radio button size specifications (in dp) constexpr float RADIO_SIZE_DP = 20.0f; // Outer ring diameter constexpr float INNER_CIRCLE_SIZE_DP = 10.0f; // Inner circle diameter (50% of outer) constexpr float TOUCH_TARGET_DP = 48.0f; // Minimum touch target size constexpr float TEXT_SPACING_DP = 8.0f; // Spacing between radio and text constexpr float STROKE_WIDTH_DP = 2.0f; // Outer ring stroke width -// Inner circle animation scale values constexpr float INNER_CIRCLE_SCALE_UNCHECKED = 0.0f; constexpr float INNER_CIRCLE_SCALE_CHECKED = 1.0f; } // namespace @@ -77,29 +70,9 @@ inline CFColor fallbackError() { // Constructor / Destructor // ============================================================================ -RadioButton::RadioButton(QWidget* parent) : QRadioButton(parent) { - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode to bounded (clipped by widget bounds) - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - - // Set default font +RadioButton::RadioButton(QWidget* parent) + : QRadioButton(parent), m_material(this, MaterialWidgetBase::Config{.useElevation = false}) { setFont(labelFont()); - - // Initialize inner circle scale based on checked state m_innerCircleScale = isChecked() ? INNER_CIRCLE_SCALE_CHECKED : INNER_CIRCLE_SCALE_UNCHECKED; } @@ -117,80 +90,51 @@ RadioButton::~RadioButton() { void RadioButton::enterEvent(QEnterEvent* event) { QRadioButton::enterEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverEnter(); - } - update(); + m_material.onEnterEvent(); } void RadioButton::leaveEvent(QEvent* event) { QRadioButton::leaveEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverLeave(); - } - if (m_ripple) { - m_ripple->onCancel(); - } - update(); + m_material.onLeaveEvent(); } void RadioButton::mousePressEvent(QMouseEvent* event) { QRadioButton::mousePressEvent(event); - if (m_stateMachine) { - m_stateMachine->onPress(event->pos()); - } - if (m_ripple && m_pressEffectEnabled) { - // Calculate radio rect for ripple clipping + if (m_pressEffectEnabled) { QRectF radioRect = calculateRadioRect(); - m_ripple->onPress(event->pos(), radioRect.united(calculateTextRect(radioRect))); + m_material.onMousePress(event->pos(), radioRect.united(calculateTextRect(radioRect))); + } else { + m_material.stateMachine()->onPress(event->pos()); + update(); } - update(); } void RadioButton::mouseReleaseEvent(QMouseEvent* event) { QRadioButton::mouseReleaseEvent(event); - if (m_stateMachine) { - m_stateMachine->onRelease(); - } - if (m_ripple && m_pressEffectEnabled) { - m_ripple->onRelease(); + if (m_pressEffectEnabled) { + m_material.onMouseRelease(); + } else { + m_material.stateMachine()->onRelease(); + if (m_material.ripple()) + m_material.ripple()->onRelease(); + update(); } - update(); } void RadioButton::focusInEvent(QFocusEvent* event) { QRadioButton::focusInEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusIn(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusIn(); - } - update(); + m_material.onFocusIn(); } void RadioButton::focusOutEvent(QFocusEvent* event) { QRadioButton::focusOutEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusOut(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusOut(); - } - update(); + m_material.onFocusOut(); } void RadioButton::changeEvent(QEvent* event) { QRadioButton::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } @@ -199,10 +143,9 @@ void RadioButton::nextCheckState() { QRadioButton::nextCheckState(); bool isCheckedNow = isChecked(); - // Update inner circle animation when checked state changes if (wasChecked != isCheckedNow) { - if (m_stateMachine) { - m_stateMachine->onCheckedChanged(isCheckedNow); + if (m_material.stateMachine()) { + m_material.stateMachine()->onCheckedChanged(isCheckedNow); } startInnerCircleAnimation(isCheckedNow); } @@ -211,19 +154,16 @@ void RadioButton::nextCheckState() { void RadioButton::setChecked(bool checked) { bool wasChecked = isChecked(); if (wasChecked == checked) { - return; // No change + return; } QRadioButton::setChecked(checked); bool isCheckedNow = isChecked(); - // Sync inner circle scale with checked state - // This handles programmatic setChecked() calls which don't trigger nextCheckState if (wasChecked != isCheckedNow) { - if (m_stateMachine) { - m_stateMachine->onCheckedChanged(isCheckedNow); + if (m_material.stateMachine()) { + m_material.stateMachine()->onCheckedChanged(isCheckedNow); } - // For programmatic checked change, set scale immediately without animation m_innerCircleScale = isCheckedNow ? INNER_CIRCLE_SCALE_CHECKED : INNER_CIRCLE_SCALE_UNCHECKED; update(); @@ -257,8 +197,6 @@ void RadioButton::setPressEffectEnabled(bool enabled) { } bool RadioButton::hitButton(const QPoint& pos) const { - // For custom-drawn radio button, entire widget area is clickable - // This ensures proper click handling even when text is empty return rect().contains(pos); } @@ -269,23 +207,13 @@ bool RadioButton::hitButton(const QPoint& pos) const { QSize RadioButton::sizeHint() const { CanvasUnitHelper helper(qApp->devicePixelRatio()); - // Material Design 3 specifications: - // - Radio size: 20dp - // - Spacing between radio and text: 8dp - // - Left padding (for stroke clipping): 12dp - // - Touch target: 48dp minimum - float leftPadding = helper.dpToPx(12.0f); float radioSize = helper.dpToPx(RADIO_SIZE_DP); float textSpacing = helper.dpToPx(TEXT_SPACING_DP); - // Text width float textWidth = text().isEmpty() ? 0.0f : fontMetrics().horizontalAdvance(text()); - // Total width float totalWidth = leftPadding + radioSize + textSpacing + textWidth; - - // Height is at least the touch target size float height = helper.dpToPx(TOUCH_TARGET_DP); return QSize(int(std::ceil(totalWidth)), int(std::ceil(height))); @@ -310,12 +238,10 @@ QSize RadioButton::minimumSizeHint() const { // ============================================================================ CFColor RadioButton::radioColor() const { - // Error state takes priority if (m_hasError) { return errorColor(); } - // Checked state uses primary, unchecked uses outline if (isChecked()) { auto* app = Application::instance(); if (!app) { @@ -361,7 +287,6 @@ CFColor RadioButton::onRadioColor() const { } CFColor RadioButton::stateLayerColor() const { - // State layer uses the same color as the radio (primary when checked, onSurface when unchecked) if (isChecked()) { return radioColor(); } else { @@ -398,7 +323,6 @@ CFColor RadioButton::errorColor() const { QFont RadioButton::labelFont() const { auto* app = Application::instance(); if (!app) { - // Fallback to system font with reasonable size QFont font = QRadioButton::font(); font.setPixelSize(14); font.setWeight(QFont::Normal); @@ -425,9 +349,6 @@ QRectF RadioButton::calculateRadioRect() const { CanvasUnitHelper helper(qApp->devicePixelRatio()); float radioSize = helper.dpToPx(RADIO_SIZE_DP); float y = (height() - radioSize) / 2.0f; - - // Add left padding (for focus indicator and to prevent stroke clipping) - // This ensures the outer ring stroke is not clipped at the left edge float x = helper.dpToPx(12.0f); return QRectF(x, y, radioSize, radioSize); @@ -449,23 +370,21 @@ void RadioButton::startInnerCircleAnimation(bool checked) { float targetScale = checked ? INNER_CIRCLE_SCALE_CHECKED : INNER_CIRCLE_SCALE_UNCHECKED; float fromScale = m_innerCircleScale; - if (!m_animationFactory) { - // No factory, set directly + auto factory = cf::WeakPtr::DynamicCast( + Application::animationFactory()); + + if (!factory) { m_innerCircleScale = targetScale; update(); return; } - // Create property animation - auto anim = m_animationFactory->createPropertyAnimation( - &m_innerCircleScale, fromScale, targetScale, 200, - cf::ui::base::Easing::Type::EmphasizedDecelerate, this); + auto anim = + factory->createPropertyAnimation(&m_innerCircleScale, fromScale, targetScale, 200, + cf::ui::base::Easing::Type::EmphasizedDecelerate, this); if (anim) { - // IMPORTANT: Update range to fix cached animation's stale from/to values - if (auto* propAnim = - dynamic_cast( - anim.Get())) { + if (auto* propAnim = dynamic_cast(anim.Get())) { propAnim->setRange(fromScale, targetScale); } anim->start(); @@ -485,7 +404,6 @@ void RadioButton::paintEvent(QPaintEvent* event) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing); - // Calculate layout QRectF radioRect = calculateRadioRect(); QRectF textRect = calculateTextRect(radioRect); @@ -513,24 +431,21 @@ void RadioButton::paintEvent(QPaintEvent* event) { // ============================================================================ void RadioButton::drawStateLayer(QPainter& p, const QRectF& radioRect) { - if (!isEnabled() || !m_stateMachine) { + if (!isEnabled() || !m_material.stateMachine()) { return; } - float opacity = m_stateMachine->stateLayerOpacity(); + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity <= 0.0f) { return; } - // MD3 spec: state layer is 40dp circle (2x radio size), centered on radio - // This corresponds to the 48dp touch target CanvasUnitHelper helper(qApp->devicePixelRatio()); float stateLayerSize = helper.dpToPx(RADIO_SIZE_DP * 2.0f); // 40dp QPointF center = radioRect.center(); QRectF stateLayerRect(center.x() - stateLayerSize / 2.0f, center.y() - stateLayerSize / 2.0f, stateLayerSize, stateLayerSize); - // Create circular state layer path QPainterPath circlePath; circlePath.addEllipse(stateLayerRect); @@ -542,25 +457,22 @@ void RadioButton::drawStateLayer(QPainter& p, const QRectF& radioRect) { } void RadioButton::drawRipple(QPainter& p, const QRectF& radioRect) { - if (!m_ripple || !m_pressEffectEnabled) { + if (!m_material.ripple() || !m_pressEffectEnabled) { return; } - // Set ripple color based on state - m_ripple->setColor(stateLayerColor()); + m_material.ripple()->setColor(stateLayerColor()); - // MD3 spec: ripple is clipped to 40dp circle (2x radio size), centered on radio CanvasUnitHelper helper(qApp->devicePixelRatio()); float stateLayerSize = helper.dpToPx(RADIO_SIZE_DP * 2.0f); // 40dp QPointF center = radioRect.center(); QRectF stateLayerRect(center.x() - stateLayerSize / 2.0f, center.y() - stateLayerSize / 2.0f, stateLayerSize, stateLayerSize); - // Create clipping path for the state layer circle QPainterPath clipPath; clipPath.addEllipse(stateLayerRect); - m_ripple->paint(&p, clipPath); + m_material.ripple()->paint(&p, clipPath); } void RadioButton::drawOuterRing(QPainter& p, const QRectF& radioRect) { @@ -570,25 +482,20 @@ void RadioButton::drawOuterRing(QPainter& p, const QRectF& radioRect) { CFColor ringColor = radioColor(); QColor color = ringColor.native_color(); - // Handle disabled state if (!isEnabled()) { - color.setAlphaF(0.38f); // 38% opacity for disabled + color.setAlphaF(0.38f); } p.save(); - // Inset the rect by half the stroke width to ensure the stroke stays within bounds - // This prevents the left edge from being clipped when x=0 float inset = strokeWidth / 2.0f; QRectF insetRect = radioRect.adjusted(inset, inset, -inset, -inset); - // Create the outer ring path from the inset rect QPainterPath ringPath; ringPath.addEllipse(insetRect); - // Set pen for stroking the ring QPen pen(color, strokeWidth); - pen.setCosmetic(false); // Use device pixels for consistent stroke width + pen.setCosmetic(false); p.setPen(pen); p.setBrush(Qt::NoBrush); @@ -602,23 +509,20 @@ void RadioButton::drawInnerCircle(QPainter& p, const QRectF& radioRect) { return; } - // Calculate inner circle dimensions based on scale float outerRadius = radioRect.width() / 2.0f; - float innerRadius = outerRadius * 0.5f; // Inner circle is 50% of outer + float innerRadius = outerRadius * 0.5f; float scaledRadius = innerRadius * m_innerCircleScale; QPointF center = radioRect.center(); p.save(); - // Create inner circle path QPainterPath innerCirclePath; innerCirclePath.addEllipse(center, scaledRadius, scaledRadius); CFColor fillColor = radioColor(); QColor color = fillColor.native_color(); - // Handle disabled state if (!isEnabled()) { color.setAlphaF(0.38f); } @@ -659,29 +563,25 @@ void RadioButton::drawText(QPainter& p, const QRectF& textRect) { p.setPen(textColor.native_color()); } - // Use label font QFont font = labelFont(); p.setFont(font); - // Draw text with vertical center alignment and left alignment p.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, text()); } void RadioButton::drawFocusIndicator(QPainter& p, const QRectF& radioRect) { - if (!m_focusIndicator) { + if (!m_material.focusIndicator()) { return; } - // Create focus indicator path (slightly larger than the radio button) QPainterPath focusPath; CanvasUnitHelper helper(qApp->devicePixelRatio()); - float focusPadding = helper.dpToPx(4.0f); // 4dp padding for focus ring + float focusPadding = helper.dpToPx(4.0f); focusPath.addEllipse( radioRect.adjusted(-focusPadding, -focusPadding, focusPadding, focusPadding)); - // Use the radio color for the focus indicator CFColor indicatorColor = radioColor(); - m_focusIndicator->paint(&p, focusPath, indicatorColor); + m_material.focusIndicator()->paint(&p, focusPath, indicatorColor); } } // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/radiobutton/radiobutton.h b/ui/widget/material/widget/radiobutton/radiobutton.h index 5e6dfc591..714ac9f87 100644 --- a/ui/widget/material/widget/radiobutton/radiobutton.h +++ b/ui/widget/material/widget/radiobutton/radiobutton.h @@ -1,351 +1,340 @@ -/** - * @file ui/widget/material/widget/radiobutton/radiobutton.h - * @brief Material Design 3 RadioButton widget. - * - * Implements Material Design 3 radio button with circular selection area, - * inner circle scale animation, ripple effects, and focus indicators. - * Supports mutual exclusion through QButtonGroup integration. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" -#include "color.h" -#include "export.h" -#include -#include - -using CFColor = cf::ui::base::CFColor; - -namespace cf::ui::widget::material { - -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - -/** - * @brief Material Design 3 RadioButton widget. - * - * @details Implements Material Design 3 radio button with circular selection - * area, inner circle scale animation, ripple effects, and focus - * indicators. Supports mutual exclusion through QButtonGroup - * integration. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT RadioButton : public QRadioButton { - Q_OBJECT - Q_PROPERTY(bool error READ hasError WRITE setError DESIGNABLE true SCRIPTABLE true) - Q_PROPERTY(bool pressEffectEnabled READ pressEffectEnabled WRITE setPressEffectEnabled) - - public: - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit RadioButton(QWidget* parent = nullptr); - - /** - * @brief Constructor with text. - * - * @param[in] text Button text label. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit RadioButton(const QString& text, QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~RadioButton() override; - - /** - * @brief Gets whether the radio button is in error state. - * - * @return true if in error state, false otherwise. - * - * @throws None - * @note Error state displays with error color theme. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool hasError() const; - - /** - * @brief Sets the error state. - * - * @param[in] error true to set error state, false otherwise. - * - * @throws None - * @note Error state displays with error color theme. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setError(bool error); - - /** - * @brief Gets whether press effect is enabled. - * - * @return true if press effect is enabled, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool pressEffectEnabled() const; - - /** - * @brief Sets whether press effect is enabled. - * - * @param[in] enabled true to enable press effect, false to disable. - * - * @throws None - * @note Press effect includes ripple animation. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setPressEffectEnabled(bool enabled); - - /** - * @brief Sets the checked state of the radio button. - * - * @param[in] checked true to check, false to uncheck. - * - * @throws None - * @note Override to sync inner circle scale with checked state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setChecked(bool checked); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the radio button. - * - * @throws None - * @note Based on text and touch target requirements. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures touch target size requirements (48dp). - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - /** - * @brief Handles hit test for mouse events. - * - * @param[in] pos Mouse position. - * - * @return true if the position is within the clickable area. - * - * @throws None - * @note Entire widget area is clickable. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool hitButton(const QPoint& pos) const override; - - protected: - /** - * @brief Paints the radio button. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements Material Design paint pipeline. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles mouse enter event. - * - * @param[in] event Enter event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void enterEvent(QEnterEvent* event) override; - - /** - * @brief Handles mouse leave event. - * - * @param[in] event Leave event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void leaveEvent(QEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple and press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Handles check state change event. - * - * @param[in] checked Check state. - * - * @throws None - * @note Triggers inner circle scale animation. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void nextCheckState() override; - - private: - // Drawing helpers - Material Design paint pipeline - void drawStateLayer(QPainter& p, const QRectF& radioRect); - void drawRipple(QPainter& p, const QRectF& radioRect); - void drawOuterRing(QPainter& p, const QRectF& radioRect); - void drawInnerCircle(QPainter& p, const QRectF& radioRect); - void drawText(QPainter& p, const QRectF& textRect); - void drawFocusIndicator(QPainter& p, const QRectF& radioRect); - - // Color access methods - CFColor radioColor() const; - CFColor onRadioColor() const; - CFColor stateLayerColor() const; - CFColor errorColor() const; - QFont labelFont() const; - - // Layout calculations - QRectF calculateRadioRect() const; - QRectF calculateTextRect(const QRectF& radioRect) const; - - // Animation helpers - void startInnerCircleAnimation(bool checked); - - // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; - - // Animation state - float m_innerCircleScale = 0.0f; - - // Properties - bool m_hasError = false; - bool m_pressEffectEnabled = true; -}; - -} // namespace cf::ui::widget::material +/** + * @file ui/widget/material/widget/radiobutton/radiobutton.h + * @brief Material Design 3 RadioButton widget. + * + * Implements Material Design 3 radio button with circular selection area, + * inner circle scale animation, ripple effects, and focus indicators. + * Supports mutual exclusion through QButtonGroup integration. + * + * @author CFDesktop Team + * @date 2026-03-01 + * @version 0.1 + * @since 0.1 + * @ingroup ui_widget_material_widget + */ +#pragma once + +#include "base/color.h" +#include "export.h" +#include "widget/material/base/material_widget_base.h" +#include +#include + +namespace cf::ui::widget::material { + +using CFColor = cf::ui::base::CFColor; + +/** + * @brief Material Design 3 RadioButton widget. + * + * @details Implements Material Design 3 radio button with circular selection + * area, inner circle scale animation, ripple effects, and focus + * indicators. Supports mutual exclusion through QButtonGroup + * integration. + * + * @since 0.1 + * @ingroup ui_widget_material_widget + */ +class CF_UI_EXPORT RadioButton : public QRadioButton { + Q_OBJECT + Q_PROPERTY(bool error READ hasError WRITE setError DESIGNABLE true SCRIPTABLE true) + Q_PROPERTY(bool pressEffectEnabled READ pressEffectEnabled WRITE setPressEffectEnabled) + + public: + /** + * @brief Constructor. + * + * @param[in] parent QObject parent. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + explicit RadioButton(QWidget* parent = nullptr); + + /** + * @brief Constructor with text. + * + * @param[in] text Button text label. + * @param[in] parent QObject parent. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + explicit RadioButton(const QString& text, QWidget* parent = nullptr); + + /** + * @brief Destructor. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + ~RadioButton() override; + + /** + * @brief Gets whether the radio button is in error state. + * + * @return true if in error state, false otherwise. + * + * @throws None + * @note Error state displays with error color theme. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + bool hasError() const; + + /** + * @brief Sets the error state. + * + * @param[in] error true to set error state, false otherwise. + * + * @throws None + * @note Error state displays with error color theme. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setError(bool error); + + /** + * @brief Gets whether press effect is enabled. + * + * @return true if press effect is enabled, false otherwise. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + bool pressEffectEnabled() const; + + /** + * @brief Sets whether press effect is enabled. + * + * @param[in] enabled true to enable press effect, false to disable. + * + * @throws None + * @note Press effect includes ripple animation. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setPressEffectEnabled(bool enabled); + + /** + * @brief Sets the checked state of the radio button. + * + * @param[in] checked true to check, false to uncheck. + * + * @throws None + * @note Override to sync inner circle scale with checked state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setChecked(bool checked); + + /** + * @brief Gets the recommended size. + * + * @return Recommended size for the radio button. + * + * @throws None + * @note Based on text and touch target requirements. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QSize sizeHint() const override; + + /** + * @brief Gets the minimum recommended size. + * + * @return Minimum recommended size. + * + * @throws None + * @note Ensures touch target size requirements (48dp). + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QSize minimumSizeHint() const override; + + /** + * @brief Handles hit test for mouse events. + * + * @param[in] pos Mouse position. + * + * @return true if the position is within the clickable area. + * + * @throws None + * @note Entire widget area is clickable. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + bool hitButton(const QPoint& pos) const override; + + protected: + /** + * @brief Paints the radio button. + * + * @param[in] event Paint event. + * + * @throws None + * @note Implements Material Design paint pipeline. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void paintEvent(QPaintEvent* event) override; + + /** + * @brief Handles mouse enter event. + * + * @param[in] event Enter event. + * + * @throws None + * @note Updates hover state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void enterEvent(QEnterEvent* event) override; + + /** + * @brief Handles mouse leave event. + * + * @param[in] event Leave event. + * + * @throws None + * @note Updates hover state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void leaveEvent(QEvent* event) override; + + /** + * @brief Handles mouse press event. + * + * @param[in] event Mouse event. + * + * @throws None + * @note Triggers ripple and press state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void mousePressEvent(QMouseEvent* event) override; + + /** + * @brief Handles mouse release event. + * + * @param[in] event Mouse event. + * + * @throws None + * @note Updates press state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void mouseReleaseEvent(QMouseEvent* event) override; + + /** + * @brief Handles focus in event. + * + * @param[in] event Focus event. + * + * @throws None + * @note Shows focus indicator. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void focusInEvent(QFocusEvent* event) override; + + /** + * @brief Handles focus out event. + * + * @param[in] event Focus event. + * + * @throws None + * @note Hides focus indicator. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void focusOutEvent(QFocusEvent* event) override; + + /** + * @brief Handles widget state change event. + * + * @param[in] event Change event. + * + * @throws None + * @note Updates appearance based on state changes. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void changeEvent(QEvent* event) override; + + /** + * @brief Handles check state change event. + * + * @param[in] checked Check state. + * + * @throws None + * @note Triggers inner circle scale animation. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void nextCheckState() override; + + private: + // Drawing helpers - Material Design paint pipeline + void drawStateLayer(QPainter& p, const QRectF& radioRect); + void drawRipple(QPainter& p, const QRectF& radioRect); + void drawOuterRing(QPainter& p, const QRectF& radioRect); + void drawInnerCircle(QPainter& p, const QRectF& radioRect); + void drawText(QPainter& p, const QRectF& textRect); + void drawFocusIndicator(QPainter& p, const QRectF& radioRect); + + // Color access methods + CFColor radioColor() const; + CFColor onRadioColor() const; + CFColor stateLayerColor() const; + CFColor errorColor() const; + QFont labelFont() const; + + // Layout calculations + QRectF calculateRadioRect() const; + QRectF calculateTextRect(const QRectF& radioRect) const; + + // Animation helpers + void startInnerCircleAnimation(bool checked); + + // Behavior components + base::MaterialWidgetBase m_material; + + // Animation state + float m_innerCircleScale = 0.0f; + + // Properties + bool m_hasError = false; + bool m_pressEffectEnabled = true; +}; + +} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/slider/slider.cpp b/ui/widget/material/widget/slider/slider.cpp index 75de40059..6da1aef2b 100644 --- a/ui/widget/material/widget/slider/slider.cpp +++ b/ui/widget/material/widget/slider/slider.cpp @@ -16,12 +16,7 @@ #include "application_support/application.h" #include "base/device_pixel.h" #include "base/geometry_helper.h" -#include "components/material/cfmaterial_animation_factory.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/elevation_controller.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -34,8 +29,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; using namespace cf::ui::base::geometry; -using namespace cf::ui::components; -using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; using namespace cf::ui::widget::material::base; @@ -45,7 +38,6 @@ using namespace cf::ui::widget::material::base; // ============================================================================ namespace { -// Material Design 3 Slider specifications (in dp) constexpr float TRACK_HEIGHT_DP = 8.0f; constexpr float THUMB_DIAMETER_DP = 20.0f; constexpr float TOUCH_TARGET_DP = 48.0f; @@ -59,75 +51,25 @@ constexpr float FOCUS_RING_MARGIN_DP = 4.0f; // Constructor / Destructor // ============================================================================ -Slider::Slider(QWidget* parent) : QSlider(Qt::Horizontal, parent) { - // Set size policy +Slider::Slider(QWidget* parent) + : QSlider(Qt::Horizontal, parent), + m_material(this, MaterialWidgetBase::Config{.useElevation = true, .initialElevation = 1}) { if (orientation() == Qt::Horizontal) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); } else { setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); } - - // Get animation factory from Application - m_animationFactory = cf::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_elevation = new MdElevationController(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode to bounded - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Set initial elevation for thumb (level 1) - m_elevation->setElevation(1); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - connect(m_elevation, &MdElevationController::pressOffsetChanged, this, - static_cast(&QWidget::update)); - - // Set default cursor setCursor(Qt::PointingHandCursor); } -Slider::Slider(Qt::Orientation orientation, QWidget* parent) : QSlider(orientation, parent) { - // Set size policy +Slider::Slider(Qt::Orientation orientation, QWidget* parent) + : QSlider(orientation, parent), + m_material(this, MaterialWidgetBase::Config{.useElevation = true, .initialElevation = 1}) { if (orientation == Qt::Horizontal) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); } else { setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); } - - // Get animation factory from Application - m_animationFactory = cf::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_elevation = new MdElevationController(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode to bounded - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Set initial elevation for thumb (level 1) - m_elevation->setElevation(1); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - connect(m_elevation, &MdElevationController::pressOffsetChanged, this, - static_cast(&QWidget::update)); - - // Set default cursor setCursor(Qt::PointingHandCursor); } @@ -141,80 +83,43 @@ Slider::~Slider() { void Slider::enterEvent(QEnterEvent* event) { QSlider::enterEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverEnter(); - update(); + m_material.onEnterEvent(); } void Slider::leaveEvent(QEvent* event) { QSlider::leaveEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverLeave(); - if (m_ripple) - m_ripple->onCancel(); - update(); + m_material.onLeaveEvent(); } void Slider::mousePressEvent(QMouseEvent* event) { QSlider::mousePressEvent(event); m_lastPressPos = event->pos(); - if (m_stateMachine) - m_stateMachine->onPress(event->pos()); - if (m_ripple) - m_ripple->onPress(event->pos(), thumbRect()); - if (m_elevation) - m_elevation->setPressed(true); - update(); + m_material.onMousePress(event->pos(), thumbRect()); } void Slider::mouseReleaseEvent(QMouseEvent* event) { QSlider::mouseReleaseEvent(event); - if (m_stateMachine) - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); - if (m_elevation) - m_elevation->setPressed(false); - update(); + m_material.onMouseRelease(); } void Slider::mouseMoveEvent(QMouseEvent* event) { QSlider::mouseMoveEvent(event); - // Update ripple center if dragging - if (m_ripple && isSliderDown()) { - // Could update ripple position here for smooth follow effect - } } void Slider::focusInEvent(QFocusEvent* event) { QSlider::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); - update(); + m_material.onFocusIn(); } void Slider::focusOutEvent(QFocusEvent* event) { QSlider::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); - update(); + m_material.onFocusOut(); } void Slider::changeEvent(QEvent* event) { QSlider::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } @@ -224,22 +129,12 @@ void Slider::changeEvent(QEvent* event) { QSize Slider::sizeHint() const { CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Material Design 3 slider specifications: - // - Track height: 4dp - // - Thumb diameter: 20dp - // - Touch target: 48dp minimum - float touchTarget = helper.dpToPx(TOUCH_TARGET_DP); if (orientation() == Qt::Horizontal) { - int width = 200; // Minimum horizontal width - int height = int(std::ceil(touchTarget)); - return QSize(width, height); + return QSize(200, int(std::ceil(touchTarget))); } else { - int width = int(std::ceil(touchTarget)); - int height = 200; // Minimum vertical height - return QSize(width, height); + return QSize(int(std::ceil(touchTarget)), 200); } } @@ -248,13 +143,9 @@ QSize Slider::minimumSizeHint() const { float touchTarget = helper.dpToPx(TOUCH_TARGET_DP); if (orientation() == Qt::Horizontal) { - int width = 120; // Minimum horizontal width - int height = int(std::ceil(touchTarget)); - return QSize(width, height); + return QSize(120, int(std::ceil(touchTarget))); } else { - int width = int(std::ceil(touchTarget)); - int height = 120; // Minimum vertical height - return QSize(width, height); + return QSize(int(std::ceil(touchTarget)), 120); } } @@ -263,18 +154,17 @@ QSize Slider::minimumSizeHint() const { // ============================================================================ namespace { -// Fallback colors when theme is not available inline CFColor fallbackPrimary() { - return CFColor(103, 80, 164); // Purple 700 + return CFColor(103, 80, 164); } inline CFColor fallbackSurfaceVariant() { - return CFColor(230, 225, 229); // Surface variant + return CFColor(230, 225, 229); } inline CFColor fallbackOnPrimary() { - return CFColor(255, 255, 255); // White + return CFColor(255, 255, 255); } inline CFColor fallbackOutline() { - return CFColor(120, 124, 132); // Outline + return CFColor(120, 124, 132); } } // namespace @@ -336,7 +226,6 @@ float Slider::trackHeight() const { } float Slider::thumbRadius() const { - // Thumb is fully circular return thumbDiameter() / 2.0f; } @@ -358,29 +247,21 @@ float Slider::thumbPosition() const { return 0.0f; } - // Calculate position ratio (0.0 to 1.0) - float ratio = static_cast(curVal - minVal) / static_cast(maxVal - minVal); - return ratio; + return static_cast(curVal - minVal) / static_cast(maxVal - minVal); } QRectF Slider::trackRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); float tHeight = trackHeight(); float tDiameter = thumbDiameter(); - QRectF contentRect = rect(); if (orientation() == Qt::Horizontal) { - // Horizontal: center track vertically float y = (height() - tHeight) / 2.0f; - // Leave space for thumb at ends float thumbRadius = tDiameter / 2.0f; float x = thumbRadius; float w = width() - 2.0f * thumbRadius; return QRectF(x, y, w, tHeight); } else { - // Vertical: center track horizontally float x = (width() - tHeight) / 2.0f; - // Leave space for thumb at ends float thumbRadius = tDiameter / 2.0f; float y = thumbRadius; float h = height() - 2.0f * thumbRadius; @@ -393,16 +274,13 @@ QRectF Slider::activeTrackRect() const { float ratio = thumbPosition(); if (orientation() == Qt::Horizontal) { - // Active track should extend to thumb center, not thumb edge float thumbCenter = track.left() + track.width() * ratio; float activeWidth = thumbCenter - track.left(); return QRectF(track.left(), track.top(), activeWidth, track.height()); } else { - // Vertical: active part extends to thumb center from bottom float thumbCenter = track.bottom() - track.height() * ratio; float activeHeight = track.bottom() - thumbCenter; - float y = thumbCenter; - return QRectF(track.left(), y, track.width(), activeHeight); + return QRectF(track.left(), thumbCenter, track.width(), activeHeight); } } @@ -412,13 +290,11 @@ QRectF Slider::thumbRect() const { float tDiameter = thumbDiameter(); if (orientation() == Qt::Horizontal) { - // Calculate thumb center position float thumbCenter = track.left() + track.width() * ratio; float x = thumbCenter - tDiameter / 2.0f; float y = (height() - tDiameter) / 2.0f; return QRectF(x, y, tDiameter, tDiameter); } else { - // Vertical: calculate from bottom float thumbCenter = track.bottom() - track.height() * ratio; float x = (width() - tDiameter) / 2.0f; float y = thumbCenter - tDiameter / 2.0f; @@ -439,16 +315,14 @@ void Slider::paintEvent(QPaintEvent* event) { QRectF track = trackRect(); QRectF thumb = thumbRect(); - // Draw inactive track (only the portion after the thumb) + // Draw inactive track if (orientation() == Qt::Horizontal) { - // Right inactive track: 从滑块右边缘到轨道终点 qreal rightWidth = track.right() - thumb.right(); if (rightWidth > 0) { QRectF rightInactive(thumb.right(), track.top(), rightWidth, track.height()); drawInactiveTrack(p, rightInactive); } } else { - // Vertical: 类似逻辑,从下往上 qreal bottomHeight = track.bottom() - thumb.bottom(); if (bottomHeight > 0) { QRectF bottomInactive(track.left(), thumb.bottom(), track.width(), bottomHeight); @@ -462,13 +336,13 @@ void Slider::paintEvent(QPaintEvent* event) { } } - // Draw active track (延伸到滑块中心) + // Draw active track QRectF activeRect = activeTrackRect(); if (!activeRect.isEmpty()) { drawActiveTrack(p, activeRect); } - // Draw tick marks (if enabled) + // Draw tick marks if (tickPosition() != QSlider::NoTicks) { drawTickMarks(p, track); } @@ -491,7 +365,6 @@ void Slider::drawInactiveTrack(QPainter& p, const QRectF& rect) { CFColor tColor = inactiveTrackColor(); QColor color = tColor.native_color(); - // Handle disabled state if (!isEnabled()) { color.setAlphaF(0.38f); } @@ -508,14 +381,12 @@ void Slider::drawActiveTrack(QPainter& p, const QRectF& rect) { CFColor tColor = activeTrackColor(); QColor color = tColor.native_color(); - // Handle disabled state if (!isEnabled()) { color.setAlphaF(0.38f); } - // Handle state layer overlay - if (m_stateMachine && isEnabled()) { - float opacity = m_stateMachine->stateLayerOpacity(); + if (m_material.stateMachine() && isEnabled()) { + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity > 0.0f) { CFColor stateColor = stateLayerColor(); QColor stateQColor = stateColor.native_color(); @@ -553,7 +424,6 @@ void Slider::drawTickMarks(QPainter& p, const QRectF& trackRect) { int numTicks = (maxVal - minVal) / tickInterval; if (orientation() == Qt::Horizontal) { - // Draw ticks below the track float tickY = trackRect.bottom() + helper.dpToPx(TICK_MARK_TO_TRACK_GAP_DP); for (int i = 0; i <= numTicks; ++i) { @@ -562,7 +432,6 @@ void Slider::drawTickMarks(QPainter& p, const QRectF& trackRect) { p.drawLine(QPointF(x, tickY), QPointF(x, tickY + tickLength)); } } else { - // Draw ticks to the right of the track float tickX = trackRect.right() + helper.dpToPx(TICK_MARK_TO_TRACK_GAP_DP); for (int i = 0; i <= numTicks; ++i) { @@ -574,49 +443,40 @@ void Slider::drawTickMarks(QPainter& p, const QRectF& trackRect) { } void Slider::drawThumb(QPainter& p, const QRectF& rect) { - // Draw shadow first (for elevation effect) - if (m_elevation && isEnabled()) { + if (m_material.elevation() && isEnabled()) { QPainterPath thumbShape = roundedRect(rect, thumbRadius()); - m_elevation->paintShadow(&p, thumbShape); + m_material.elevation()->paintShadow(&p, thumbShape); } CFColor tColor = thumbColor(); QColor color = tColor.native_color(); - // Handle disabled state if (!isEnabled()) { color.setAlphaF(0.38f); } - // Draw thumb circle QPainterPath shape = roundedRect(rect, thumbRadius()); p.fillPath(shape, color); } void Slider::drawRipple(QPainter& p, const QRectF& rect) { - if (m_ripple) { - // Update ripple color based on current state - m_ripple->setColor(stateLayerColor()); + if (m_material.ripple()) { + m_material.ripple()->setColor(stateLayerColor()); QPainterPath clipPath = roundedRect(rect, thumbRadius()); - m_ripple->paint(&p, clipPath); + m_material.ripple()->paint(&p, clipPath); } } void Slider::drawFocusIndicator(QPainter& p, const QRectF& trackRect) { - if (m_focusIndicator) { + if (m_material.focusIndicator()) { CanvasUnitHelper helper(qApp->devicePixelRatio()); float margin = helper.dpToPx(FOCUS_RING_MARGIN_DP); - QRectF focusRect; - if (orientation() == Qt::Horizontal) { - focusRect = trackRect.adjusted(-margin, -margin, margin, margin); - } else { - focusRect = trackRect.adjusted(-margin, -margin, margin, margin); - } + QRectF focusRect = trackRect.adjusted(-margin, -margin, margin, margin); QPainterPath shape = roundedRect(focusRect, trackHeight() / 2.0f + margin); - m_focusIndicator->paint(&p, shape, activeTrackColor()); + m_material.focusIndicator()->paint(&p, shape, activeTrackColor()); } } diff --git a/ui/widget/material/widget/slider/slider.h b/ui/widget/material/widget/slider/slider.h index 52eafef6e..e45e4dfe8 100644 --- a/ui/widget/material/widget/slider/slider.h +++ b/ui/widget/material/widget/slider/slider.h @@ -15,22 +15,13 @@ #pragma once #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" #include #include namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdElevationController; -class MdFocusIndicator; -} // namespace base - using CFColor = cf::ui::base::CFColor; /** @@ -254,11 +245,7 @@ class CF_UI_EXPORT Slider : public QSlider { float thumbPosition() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdElevationController* m_elevation; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Track last press position for ripple QPoint m_lastPressPos; diff --git a/ui/widget/material/widget/spinbox/spinbox.cpp b/ui/widget/material/widget/spinbox/spinbox.cpp index cf5774795..aff2865fe 100644 --- a/ui/widget/material/widget/spinbox/spinbox.cpp +++ b/ui/widget/material/widget/spinbox/spinbox.cpp @@ -16,11 +16,7 @@ #include "application_support/application.h" #include "base/device_pixel.h" #include "base/geometry_helper.h" -#include "cfmaterial_animation_factory.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -35,8 +31,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; using namespace cf::ui::base::geometry; -using namespace cf::ui::components; -using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; using namespace cf::ui::widget::material::base; @@ -64,7 +58,14 @@ constexpr float kIconStrokeWidth = 2.5f; // Icon stroke width // ============================================================================ SpinBox::SpinBox(QWidget* parent) - : QSpinBox(parent), m_hoveringIncrementButton(false), m_hoveringDecrementButton(false), + : QSpinBox(parent), m_material(this, + MaterialWidgetBase::Config{ + .useRipple = true, + .useElevation = true, + .useFocusIndicator = true, + .initialElevation = 0, + }), + m_hoveringIncrementButton(false), m_hoveringDecrementButton(false), m_pressingIncrementButton(false), m_pressingDecrementButton(false) { // Disable native frame and background @@ -84,24 +85,6 @@ SpinBox::SpinBox(QWidget* parent) lineEdit()->setAlignment(Qt::AlignRight); } - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode to bounded for button areas - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - // Set default font setFont(textFont()); @@ -122,23 +105,16 @@ SpinBox::~SpinBox() { void SpinBox::enterEvent(QEnterEvent* event) { QSpinBox::enterEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverEnter(); - update(); + m_material.onEnterEvent(); } void SpinBox::leaveEvent(QEvent* event) { QSpinBox::leaveEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverLeave(); - if (m_ripple) - m_ripple->onCancel(); + m_material.onLeaveEvent(); // Reset button hover state m_hoveringIncrementButton = false; m_hoveringDecrementButton = false; - - update(); } void SpinBox::mousePressEvent(QMouseEvent* event) { @@ -161,11 +137,7 @@ void SpinBox::mousePressEvent(QMouseEvent* event) { // Handle text area click QSpinBox::mousePressEvent(event); - if (m_stateMachine) - m_stateMachine->onPress(pos); - if (m_ripple) - m_ripple->onPress(pos, rect()); - update(); + m_material.onMousePress(pos, rect()); } void SpinBox::mouseReleaseEvent(QMouseEvent* event) { @@ -178,11 +150,7 @@ void SpinBox::mouseReleaseEvent(QMouseEvent* event) { } QSpinBox::mouseReleaseEvent(event); - if (m_stateMachine) - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); - update(); + m_material.onMouseRelease(); } void SpinBox::mouseMoveEvent(QMouseEvent* event) { @@ -192,32 +160,18 @@ void SpinBox::mouseMoveEvent(QMouseEvent* event) { void SpinBox::focusInEvent(QFocusEvent* event) { QSpinBox::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); - update(); + m_material.onFocusIn(); } void SpinBox::focusOutEvent(QFocusEvent* event) { QSpinBox::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); - update(); + m_material.onFocusOut(); } void SpinBox::changeEvent(QEvent* event) { QSpinBox::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } + m_material.onEnabledChange(isEnabled()); updateTextColor(); update(); } @@ -546,11 +500,11 @@ void SpinBox::drawBackground(QPainter& p, const QPainterPath& shape) { } void SpinBox::drawStateLayer(QPainter& p, const QPainterPath& shape) { - if (!isEnabled() || !m_stateMachine) { + if (!isEnabled() || !m_material.stateMachine()) { return; } - float opacity = m_stateMachine->stateLayerOpacity(); + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity <= 0.0f) { return; } @@ -563,10 +517,10 @@ void SpinBox::drawStateLayer(QPainter& p, const QPainterPath& shape) { } void SpinBox::drawRipple(QPainter& p, const QPainterPath& shape) { - if (m_ripple) { + if (m_material.ripple()) { // Set ripple color based on text color - m_ripple->setColor(textColor()); - m_ripple->paint(&p, shape); + m_material.ripple()->setColor(textColor()); + m_material.ripple()->paint(&p, shape); } } @@ -727,8 +681,8 @@ void SpinBox::drawButtons(QPainter& p, const QRectF& buttonRect) { } void SpinBox::drawFocusIndicator(QPainter& p, const QPainterPath& shape) { - if (m_focusIndicator && hasFocus()) { - m_focusIndicator->paint(&p, shape, focusOutlineColor()); + if (m_material.focusIndicator() && hasFocus()) { + m_material.focusIndicator()->paint(&p, shape, focusOutlineColor()); } } diff --git a/ui/widget/material/widget/spinbox/spinbox.h b/ui/widget/material/widget/spinbox/spinbox.h index 4069c7093..0ffabc0e4 100644 --- a/ui/widget/material/widget/spinbox/spinbox.h +++ b/ui/widget/material/widget/spinbox/spinbox.h @@ -18,21 +18,13 @@ #include #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" #include #include namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - using CFColor = cf::ui::base::CFColor; /** @@ -267,10 +259,7 @@ class CF_UI_EXPORT SpinBox : public QSpinBox { QFont textFont() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Internal state bool m_hoveringIncrementButton; diff --git a/ui/widget/material/widget/switch/switch.cpp b/ui/widget/material/widget/switch/switch.cpp index 7bda094af..fff18c516 100644 --- a/ui/widget/material/widget/switch/switch.cpp +++ b/ui/widget/material/widget/switch/switch.cpp @@ -15,14 +15,10 @@ #include "switch.h" #include "application_support/application.h" #include "base/device_pixel.h" +#include "base/easing.h" #include "base/geometry_helper.h" -#include "components/material/cfmaterial_animation_factory.h" #include "components/material/cfmaterial_property_animation.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/elevation_controller.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -35,7 +31,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; using namespace cf::ui::base::geometry; -using namespace cf::ui::components; using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; @@ -46,11 +41,10 @@ using namespace cf::ui::widget::material::base; // ============================================================================ namespace { -// Material Design 3 Switch specifications (in dp) constexpr float TRACK_WIDTH_DP = 52.0f; constexpr float TRACK_HEIGHT_DP = 32.0f; constexpr float THUMB_DIAMETER_DP = 16.0f; -constexpr float THUMB_MARGIN_DP = 8.0f; // Distance from track edge +constexpr float THUMB_MARGIN_DP = 8.0f; constexpr float TEXT_SPACING_DP = 12.0f; constexpr float TOUCH_TARGET_DP = 48.0f; } // namespace @@ -59,41 +53,16 @@ constexpr float TOUCH_TARGET_DP = 48.0f; // Constructor / Destructor // ============================================================================ -Switch::Switch(QWidget* parent) : QCheckBox(parent) { - // Set size policy +Switch::Switch(QWidget* parent) + : QCheckBox(parent), + m_material(this, MaterialWidgetBase::Config{.useElevation = true, .initialElevation = 1}) { setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - // Get animation factory from Application - m_animationFactory = cf::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_elevation = new MdElevationController(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode to bounded - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Set initial elevation for thumb (level 1) - m_elevation->setElevation(1); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - connect(m_elevation, &MdElevationController::pressOffsetChanged, this, - static_cast(&QWidget::update)); - - // Initialize thumb position based on current state m_thumbPosition = isChecked() ? 1.0f : 0.0f; if (isChecked()) { - m_stateMachine->onCheckedChanged(true); + m_material.stateMachine()->onCheckedChanged(true); } - // Set default cursor setCursor(Qt::PointingHandCursor); } @@ -111,11 +80,9 @@ void Switch::setChecked(bool checked) { } QCheckBox::setChecked(checked); - // When called from nextCheckState(), skip position update - animation - // will handle it. Otherwise (direct call), snap immediately. if (!m_inNextCheckState) { - if (m_stateMachine) { - m_stateMachine->onCheckedChanged(checked); + if (m_material.stateMachine()) { + m_material.stateMachine()->onCheckedChanged(checked); } m_thumbPosition = checked ? 1.0f : 0.0f; update(); @@ -128,92 +95,54 @@ void Switch::setChecked(bool checked) { void Switch::enterEvent(QEnterEvent* event) { QCheckBox::enterEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverEnter(); - update(); + m_material.onEnterEvent(); } void Switch::leaveEvent(QEvent* event) { QCheckBox::leaveEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverLeave(); - if (m_ripple) - m_ripple->onCancel(); - update(); + m_material.onLeaveEvent(); } void Switch::mousePressEvent(QMouseEvent* event) { QCheckBox::mousePressEvent(event); - if (m_stateMachine) - m_stateMachine->onPress(event->pos()); - if (m_ripple) - m_ripple->onPress(event->pos(), trackRect()); - if (m_elevation) - m_elevation->setPressed(true); - update(); + m_material.onMousePress(event->pos(), trackRect()); } void Switch::mouseReleaseEvent(QMouseEvent* event) { QCheckBox::mouseReleaseEvent(event); - if (m_stateMachine) - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); - if (m_elevation) - m_elevation->setPressed(false); - update(); + m_material.onMouseRelease(); } void Switch::focusInEvent(QFocusEvent* event) { QCheckBox::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); - update(); + m_material.onFocusIn(); } void Switch::focusOutEvent(QFocusEvent* event) { QCheckBox::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); - update(); + m_material.onFocusOut(); } void Switch::changeEvent(QEvent* event) { QCheckBox::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } void Switch::nextCheckState() { - // Set guard flag BEFORE calling base class - this prevents setChecked() - // from snapping the position, since we'll animate it ourselves m_inNextCheckState = true; QCheckBox::nextCheckState(); m_inNextCheckState = false; - // Update state machine checked state - if (m_stateMachine) { - m_stateMachine->onCheckedChanged(isChecked()); + if (m_material.stateMachine()) { + m_material.stateMachine()->onCheckedChanged(isChecked()); } - // Animate thumb from current visual position to new target startThumbPositionAnimation(isChecked() ? 1.0f : 0.0f); } bool Switch::hitButton(const QPoint& pos) const { - // For custom-drawn switch, entire widget area is clickable return rect().contains(pos); } @@ -224,23 +153,15 @@ bool Switch::hitButton(const QPoint& pos) const { QSize Switch::sizeHint() const { CanvasUnitHelper helper(qApp->devicePixelRatio()); - // Material Design 3 switch specifications: - // - Track size: 52dp x 32dp - // - Spacing between track and text: 12dp - // - Touch target: 48dp minimum - float trackW = trackWidth(); - float trackH = trackHeight(); float spacing = helper.dpToPx(TEXT_SPACING_DP); float textWidth = text().isEmpty() ? 0.0f : fontMetrics().horizontalAdvance(text()); float width = trackW + spacing + textWidth; - - // Ensure minimum 48dp width for easy clicking (even without text) float minWidth = helper.dpToPx(TOUCH_TARGET_DP); width = std::max(width, minWidth); - float height = helper.dpToPx(TOUCH_TARGET_DP); // Fixed height for touch target + float height = helper.dpToPx(TOUCH_TARGET_DP); return QSize(int(std::ceil(width)), int(std::ceil(height))); } @@ -254,21 +175,20 @@ QSize Switch::minimumSizeHint() const { // ============================================================================ namespace { -// Fallback colors when theme is not available inline CFColor fallbackPrimary() { - return CFColor(103, 80, 164); // Purple 700 + return CFColor(103, 80, 164); } inline CFColor fallbackOutline() { - return CFColor(120, 124, 132); // Outline + return CFColor(120, 124, 132); } inline CFColor fallbackSurface() { - return CFColor(232, 226, 232); // Surface + return CFColor(232, 226, 232); } inline CFColor fallbackOnPrimary() { - return CFColor(255, 255, 255); // White + return CFColor(255, 255, 255); } inline CFColor fallbackOnSurface() { - return CFColor(29, 27, 32); // On Surface (near black) + return CFColor(29, 27, 32); } } // namespace @@ -282,7 +202,6 @@ CFColor Switch::trackColor() const { const auto& theme = app->currentTheme(); auto& colorScheme = theme.color_scheme(); - // Checked: PRIMARY, Unchecked: OUTLINE if (isChecked()) { return CFColor(colorScheme.queryColor(PRIMARY)); } @@ -302,7 +221,6 @@ CFColor Switch::thumbColor() const { const auto& theme = app->currentTheme(); auto& colorScheme = theme.color_scheme(); - // Checked: ON_PRIMARY (white), Unchecked: SURFACE if (isChecked()) { return CFColor(colorScheme.queryColor(ON_PRIMARY)); } @@ -347,12 +265,10 @@ CFColor Switch::labelColor() const { } float Switch::trackCornerRadius() const { - // Track uses full rounding (pill shape) return trackHeight() / 2.0f; } float Switch::thumbRadius() const { - // Thumb is fully circular return thumbDiameter() / 2.0f; } @@ -381,14 +297,9 @@ float Switch::thumbDiameter() const { QRectF Switch::trackRect() const { CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Calculate vertical centering float trackH = trackHeight(); float y = (height() - trackH) / 2.0f; - - // Track starts from left float x = 0; - return QRectF(x, y, trackWidth(), trackH); } @@ -397,7 +308,6 @@ QRectF Switch::thumbRect() const { CanvasUnitHelper helper(qApp->devicePixelRatio()); float margin = helper.dpToPx(THUMB_MARGIN_DP); - // Calculate thumb position based on animation progress float maxTravel = track.width() - 2.0f * margin - thumbDiameter(); float currentX = track.left() + margin + maxTravel * m_thumbPosition; float y = track.top() + (track.height() - thumbDiameter()) / 2.0f; @@ -422,23 +332,20 @@ QRectF Switch::textRect() const { // ============================================================================ void Switch::startThumbPositionAnimation(float target) { - if (!m_animationFactory) { + auto factory = cf::WeakPtr::DynamicCast( + application_support::Application::animationFactory()); + + if (!factory) { m_thumbPosition = target; update(); return; } - // Get cached animation from factory - auto anim = m_animationFactory->createPropertyAnimation( - &m_thumbPosition, m_thumbPosition, target, 200, cf::ui::base::Easing::Type::Standard, this); + auto anim = factory->createPropertyAnimation(&m_thumbPosition, m_thumbPosition, target, 200, + cf::ui::base::Easing::Type::Standard, this); if (anim) { - // IMPORTANT: Update range to fix cached animation's stale from/to values - // The cached animation has old values from previous transition, which - // would cause the thumb to jump to wrong position (bounce bug). - if (auto* propAnim = - dynamic_cast( - anim.Get())) { + if (auto* propAnim = dynamic_cast(anim.Get())) { propAnim->setRange(m_thumbPosition, target); } anim->start(); @@ -486,20 +393,17 @@ void Switch::drawTrack(QPainter& p, const QRectF& rect) { CFColor tColor = trackColor(); QColor color = tColor.native_color(); - // Handle disabled state if (!isEnabled()) { color.setAlphaF(0.38f); } - // Handle state layer overlay - if (m_stateMachine && isEnabled()) { - float opacity = m_stateMachine->stateLayerOpacity(); + if (m_material.stateMachine() && isEnabled()) { + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity > 0.0f) { CFColor stateColor = stateLayerColor(); QColor stateQColor = stateColor.native_color(); stateQColor.setAlphaF(opacity); - // For unchecked state, blend with surface if (!isChecked()) { auto* app = application_support::Application::instance(); if (app) { @@ -514,7 +418,6 @@ void Switch::drawTrack(QPainter& p, const QRectF& rect) { int(surface.blue() * (1.0f - opacity) + stateQColor.blue() * opacity); color = QColor(r, g, b, color.alpha()); } catch (...) { - // Fallback to simple blend int r = int(color.red() * (1.0f - opacity) + stateQColor.red() * opacity); int g = int(color.green() * (1.0f - opacity) + stateQColor.green() * opacity); @@ -528,7 +431,6 @@ void Switch::drawTrack(QPainter& p, const QRectF& rect) { color = QColor(r, g, b, color.alpha()); } } else { - // Checked state: blend with primary int r = int(color.red() * (1.0f - opacity) + stateQColor.red() * opacity); int g = int(color.green() * (1.0f - opacity) + stateQColor.green() * opacity); int b = int(color.blue() * (1.0f - opacity) + stateQColor.blue() * opacity); @@ -542,35 +444,28 @@ void Switch::drawTrack(QPainter& p, const QRectF& rect) { } void Switch::drawThumb(QPainter& p, const QRectF& rect) { - // Draw shadow first (for elevation effect) - if (m_elevation && isEnabled()) { + if (m_material.elevation() && isEnabled()) { QPainterPath thumbShape = roundedRect(rect, thumbRadius()); - m_elevation->paintShadow(&p, thumbShape); + m_material.elevation()->paintShadow(&p, thumbShape); } CFColor tColor = thumbColor(); QColor color = tColor.native_color(); - // Handle disabled state if (!isEnabled()) { color.setAlphaF(0.38f); } - // Draw thumb circle QPainterPath shape = roundedRect(rect, thumbRadius()); p.fillPath(shape, color); - - // Optional: Draw check icon on thumb when checked - // (Material Design 3 switches often show an icon when checked) } void Switch::drawRipple(QPainter& p, const QRectF& rect) { - if (m_ripple) { - // Update ripple color based on current state - m_ripple->setColor(stateLayerColor()); + if (m_material.ripple()) { + m_material.ripple()->setColor(stateLayerColor()); QPainterPath clipPath = roundedRect(rect, trackCornerRadius()); - m_ripple->paint(&p, clipPath); + m_material.ripple()->paint(&p, clipPath); } } @@ -585,23 +480,20 @@ void Switch::drawText(QPainter& p, const QRectF& rect) { p.setPen(textColor.native_color()); } - // Use widget's font p.setFont(font()); - // Draw text vertically centered, left aligned - QRectF textBounds = rect.adjusted(0, 2, 0, -2); // Small adjustment for visual centering + QRectF textBounds = rect.adjusted(0, 2, 0, -2); p.drawText(textBounds, Qt::AlignLeft | Qt::AlignVCenter, text()); } void Switch::drawFocusIndicator(QPainter& p, const QRectF& rect) { - if (m_focusIndicator) { - // Expand rect slightly for focus ring + if (m_material.focusIndicator()) { CanvasUnitHelper helper(qApp->devicePixelRatio()); float margin = helper.dpToPx(4.0f); QRectF focusRect = rect.adjusted(-margin, -margin, margin, margin); QPainterPath shape = roundedRect(focusRect, trackCornerRadius() + margin); - m_focusIndicator->paint(&p, shape, trackColor()); + m_material.focusIndicator()->paint(&p, shape, trackColor()); } } diff --git a/ui/widget/material/widget/switch/switch.h b/ui/widget/material/widget/switch/switch.h index e924e9217..0f0f98382 100644 --- a/ui/widget/material/widget/switch/switch.h +++ b/ui/widget/material/widget/switch/switch.h @@ -15,22 +15,13 @@ #pragma once #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" #include #include namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdElevationController; -class MdFocusIndicator; -} // namespace base - using CFColor = cf::ui::base::CFColor; /** @@ -285,11 +276,7 @@ class CF_UI_EXPORT Switch : public QCheckBox { float thumbDiameter() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdElevationController* m_elevation; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Thumb position animation progress (0.0 = unchecked/left, 1.0 = checked/right) float m_thumbPosition = 0.0f; diff --git a/ui/widget/material/widget/tableview/tableview.cpp b/ui/widget/material/widget/tableview/tableview.cpp index de4fd18f6..a60f8acf2 100644 --- a/ui/widget/material/widget/tableview/tableview.cpp +++ b/ui/widget/material/widget/tableview/tableview.cpp @@ -15,11 +15,7 @@ #include "tableview.h" #include "application_support/application.h" #include "base/device_pixel.h" -#include "cfmaterial_animation_factory.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -32,8 +28,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; -using namespace cf::ui::components; -using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; using namespace cf::ui::widget::material::base; @@ -67,27 +61,15 @@ inline CFColor fallbackPrimaryContainer() { // ============================================================================ TableView::TableView(QWidget* parent) - : QTableView(parent), rowHeight_(TableRowHeight::Standard), - gridStyle_(TableGridStyle::Horizontal), showHeader_(true), alternatingRowColors_(false), - rippleEnabled_(true), m_hasValidPressPos(false), m_hoveredRow(-1), m_pressedRow(-1) { - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode to bounded for table cells - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - + : QTableView(parent), m_material(this, + MaterialWidgetBase::Config{ + .useRipple = true, + .useElevation = false, + .useFocusIndicator = true, + }), + rowHeight_(TableRowHeight::Standard), gridStyle_(TableGridStyle::Horizontal), + showHeader_(true), alternatingRowColors_(false), rippleEnabled_(true), + m_hasValidPressPos(false), m_hoveredRow(-1), m_pressedRow(-1) { // Configure default QTableView properties for Material rendering setAttribute(Qt::WA_Hover, true); setMouseTracking(true); @@ -186,8 +168,8 @@ bool TableView::rippleEnabled() const { void TableView::setRippleEnabled(bool enabled) { if (rippleEnabled_ != enabled) { rippleEnabled_ = enabled; - if (!enabled && m_ripple) { - m_ripple->onCancel(); + if (!enabled && m_material.ripple()) { + m_material.ripple()->onCancel(); } } } @@ -269,7 +251,7 @@ void TableView::paintEvent(QPaintEvent* event) { } // Draw ripple effects - if (rippleEnabled_ && m_ripple && (m_pressedRow >= 0 || m_hoveredRow >= 0)) { + if (rippleEnabled_ && m_material.ripple() && (m_pressedRow >= 0 || m_hoveredRow >= 0)) { int row = (m_pressedRow >= 0) ? m_pressedRow : m_hoveredRow; if (row >= 0) { QRect rowRect = visualRect(model()->index(row, 0)); @@ -277,8 +259,8 @@ void TableView::paintEvent(QPaintEvent* event) { rowRect.setWidth(viewportRect.width()); QPainterPath clipPath; clipPath.addRect(rowRect); - m_ripple->setColor(onSurfaceColor()); - m_ripple->paint(&p, clipPath); + m_material.ripple()->setColor(onSurfaceColor()); + m_material.ripple()->paint(&p, clipPath); } } @@ -288,7 +270,7 @@ void TableView::paintEvent(QPaintEvent* event) { } // Draw focus indicator around the actual content area - if (hasFocus() && m_focusIndicator && model()) { + if (hasFocus() && m_material.focusIndicator() && model()) { // Calculate actual content bounds QRectF contentRect = viewportRect; @@ -326,15 +308,15 @@ void TableView::mousePressEvent(QMouseEvent* event) { int row = index.row(); if (row >= 0) { m_pressedRow = row; - if (m_stateMachine) { - m_stateMachine->onPress(event->pos()); + if (m_material.stateMachine()) { + m_material.stateMachine()->onPress(event->pos()); } - if (m_ripple && rippleEnabled_) { + if (m_material.ripple() && rippleEnabled_) { QRect rowRect = visualRect(model()->index(row, 0)); rowRect.setLeft(0); rowRect.setWidth(viewport()->width()); // Ripple expects viewport coordinates since rowRect is in viewport coords - m_ripple->onPress(viewportPos, rowRect); + m_material.ripple()->onPress(viewportPos, rowRect); } update(); } @@ -345,11 +327,11 @@ void TableView::mouseReleaseEvent(QMouseEvent* event) { QTableView::mouseReleaseEvent(event); if (m_pressedRow >= 0) { - if (m_stateMachine) { - m_stateMachine->onRelease(); + if (m_material.stateMachine()) { + m_material.stateMachine()->onRelease(); } - if (m_ripple && rippleEnabled_) { - m_ripple->onRelease(); + if (m_material.ripple() && rippleEnabled_) { + m_material.ripple()->onRelease(); } m_pressedRow = -1; m_hasValidPressPos = false; @@ -368,8 +350,8 @@ void TableView::mouseMoveEvent(QMouseEvent* event) { if (m_hoveredRow != row) { m_hoveredRow = row; - if (row >= 0 && m_stateMachine) { - m_stateMachine->onHoverEnter(); + if (row >= 0 && m_material.stateMachine()) { + m_material.stateMachine()->onHoverEnter(); } update(); } @@ -377,20 +359,12 @@ void TableView::mouseMoveEvent(QMouseEvent* event) { void TableView::enterEvent(QEnterEvent* event) { QTableView::enterEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverEnter(); - } - update(); + m_material.onEnterEvent(); } void TableView::leaveEvent(QEvent* event) { QTableView::leaveEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverLeave(); - } - if (m_ripple) { - m_ripple->onCancel(); - } + m_material.onLeaveEvent(); m_hoveredRow = -1; m_pressedRow = -1; update(); @@ -398,37 +372,18 @@ void TableView::leaveEvent(QEvent* event) { void TableView::focusInEvent(QFocusEvent* event) { QTableView::focusInEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusIn(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusIn(); - } - update(); + m_material.onFocusIn(); } void TableView::focusOutEvent(QFocusEvent* event) { QTableView::focusOutEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusOut(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusOut(); - } - update(); + m_material.onFocusOut(); } void TableView::changeEvent(QEvent* event) { QTableView::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } @@ -487,7 +442,7 @@ void TableView::drawGridLines(QPainter& p, const QRectF& viewportRect) { } void TableView::drawFocusIndicator(QPainter& p, const QRectF& rect) { - if (m_focusIndicator) { + if (m_material.focusIndicator()) { // Create focus indicator path (inset to account for both ring width and spacing) // Ring width: 3dp, Additional inset: 3dp = Total 6dp QPainterPath path; @@ -496,7 +451,7 @@ void TableView::drawFocusIndicator(QPainter& p, const QRectF& rect) { QRectF focusRect = rect.adjusted(-inset, -inset, inset, inset); path.addRoundedRect(focusRect, 4, 4); - m_focusIndicator->paint(&p, path, primaryContainerColor()); + m_material.focusIndicator()->paint(&p, path, primaryContainerColor()); } } diff --git a/ui/widget/material/widget/tableview/tableview.h b/ui/widget/material/widget/tableview/tableview.h index 7a99b5b86..a8e2e902e 100644 --- a/ui/widget/material/widget/tableview/tableview.h +++ b/ui/widget/material/widget/tableview/tableview.h @@ -15,21 +15,13 @@ #pragma once #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" #include #include namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - using CFColor = cf::ui::base::CFColor; /** @@ -401,10 +393,7 @@ class CF_UI_EXPORT TableView : public QTableView { CFColor primaryContainerColor() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Properties TableRowHeight rowHeight_; diff --git a/ui/widget/material/widget/tabview/private/materialtabbar.cpp b/ui/widget/material/widget/tabview/private/materialtabbar.cpp index 95e233bae..adaa9e08d 100644 --- a/ui/widget/material/widget/tabview/private/materialtabbar.cpp +++ b/ui/widget/material/widget/tabview/private/materialtabbar.cpp @@ -14,8 +14,6 @@ #include "application_support/application.h" #include "base/device_pixel.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/state_machine.h" #include #include @@ -27,7 +25,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; -using namespace cf::ui::components; using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; @@ -58,7 +55,11 @@ MaterialTabBar::MaterialTabBar(TabView* parent) : QTabBar(parent), m_tabView(parent), m_tabHeightDp(DEFAULT_TAB_HEIGHT_DP), m_tabMinWidthDp(DEFAULT_TAB_MIN_WIDTH_DP), m_showIndicator(true), m_indicatorPosition(0.0f), m_indicatorTargetPosition(0.0f), m_lastIndex(-1), m_hoveredIndex(-1), m_pressedIndex(-1), - m_closeButtonHoveredIndex(-1), m_stateMachine(nullptr), m_focusIndicator(nullptr) { + m_closeButtonHoveredIndex(-1), m_material(this, base::MaterialWidgetBase::Config{ + .useRipple = true, + .useElevation = false, + .useFocusIndicator = true, + }) { setDrawBase(false); setDocumentMode(true); @@ -67,14 +68,6 @@ MaterialTabBar::MaterialTabBar(TabView* parent) setElideMode(Qt::ElideRight); setMouseTracking(true); - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - m_stateMachine = new StateMachine(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); connect(this, &QTabBar::currentChanged, this, &MaterialTabBar::animateIndicatorTo); } @@ -153,15 +146,15 @@ void MaterialTabBar::drawTab(QPainter& p, int index) { drawTabStateLayer(p, tabRect, index); drawTabContent(p, tabRect, index); - if (index == currentIndex() && hasFocus() && m_focusIndicator) { + if (index == currentIndex() && hasFocus() && m_material.focusIndicator()) { QPainterPath shape; shape.addRoundedRect(tabRect.adjusted(2, 2, -2, -2), 4, 4); - m_focusIndicator->paint(&p, shape, focusIndicatorColor()); + m_material.focusIndicator()->paint(&p, shape, focusIndicatorColor()); } } void MaterialTabBar::drawTabStateLayer(QPainter& p, const QRect& tabRect, int index) { - if (!isEnabled() || !m_stateMachine) { + if (!isEnabled() || !m_material.stateMachine()) { return; } @@ -263,7 +256,9 @@ void MaterialTabBar::animateIndicatorTo(int index) { int oldIndex = m_lastIndex; m_lastIndex = index; - if (!m_animationFactory || !m_animationFactory->isAllEnabled()) { + auto factory = + cf::WeakPtr::DynamicCast(Application::animationFactory()); + if (!factory || !factory->isAllEnabled()) { m_indicatorPosition = static_cast(tabRect(index).left()); update(); return; @@ -355,17 +350,13 @@ void MaterialTabBar::drawCloseIcon(QPainter& p, const QRect& closeRect, int inde void MaterialTabBar::enterEvent(QEnterEvent* event) { QTabBar::enterEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverEnter(); - } + m_material.onEnterEvent(); update(); } void MaterialTabBar::leaveEvent(QEvent* event) { QTabBar::leaveEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverLeave(); - } + m_material.onLeaveEvent(); m_hoveredIndex = -1; m_closeButtonHoveredIndex = -1; update(); @@ -388,9 +379,7 @@ void MaterialTabBar::mousePressEvent(QMouseEvent* event) { int index = tabAt(event->pos()); if (index >= 0) { m_pressedIndex = index; - if (m_stateMachine) { - m_stateMachine->onPress(event->pos()); - } + m_material.onMousePress(event->pos(), QRectF(tabRect(index))); } QTabBar::mousePressEvent(event); update(); @@ -398,9 +387,7 @@ void MaterialTabBar::mousePressEvent(QMouseEvent* event) { void MaterialTabBar::mouseReleaseEvent(QMouseEvent* event) { m_pressedIndex = -1; - if (m_stateMachine) { - m_stateMachine->onRelease(); - } + m_material.onMouseRelease(); // Check if close button was clicked int index = tabAt(event->pos()); @@ -416,32 +403,20 @@ void MaterialTabBar::mouseReleaseEvent(QMouseEvent* event) { void MaterialTabBar::focusInEvent(QFocusEvent* event) { QTabBar::focusInEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusIn(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusIn(); - } + m_material.onFocusIn(); update(); } void MaterialTabBar::focusOutEvent(QFocusEvent* event) { QTabBar::focusOutEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusOut(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusOut(); - } + m_material.onFocusOut(); update(); } void MaterialTabBar::changeEvent(QEvent* event) { QTabBar::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - isEnabled() ? m_stateMachine->onEnable() : m_stateMachine->onDisable(); - } + m_material.onEnabledChange(isEnabled()); update(); } } diff --git a/ui/widget/material/widget/tabview/private/materialtabbar.h b/ui/widget/material/widget/tabview/private/materialtabbar.h index 81d949987..254d1012e 100644 --- a/ui/widget/material/widget/tabview/private/materialtabbar.h +++ b/ui/widget/material/widget/tabview/private/materialtabbar.h @@ -21,20 +21,14 @@ #include "base/device_pixel.h" #include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" -#include "material/base/elevation_controller.h" #include "material/widget/button/button.h" +#include "widget/material/base/material_widget_base.h" namespace cf::ui::widget::material { // Forward declarations class TabView; -namespace base { -class StateMachine; -class MdFocusIndicator; -} // namespace base -using namespace cf::ui::components::material; using CFColor = base::CFColor; /** * @brief Internal Material TabBar implementation. @@ -389,10 +383,8 @@ class MaterialTabBar : public QTabBar { QSet m_closeableTabs; // Set of closeable tab indices - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::MdFocusIndicator* m_focusIndicator; cf::WeakPtr m_indicatorAnimation; + base::MaterialWidgetBase m_material; }; } // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/textarea/textarea.cpp b/ui/widget/material/widget/textarea/textarea.cpp index cb4026e24..05feff4e6 100644 --- a/ui/widget/material/widget/textarea/textarea.cpp +++ b/ui/widget/material/widget/textarea/textarea.cpp @@ -17,11 +17,7 @@ #include "application_support/application.h" #include "base/device_pixel.h" #include "base/geometry_helper.h" -#include "cfmaterial_animation_factory.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -75,9 +71,15 @@ inline CFColor fallbackPrimary() { // ============================================================================ TextArea::TextArea(TextAreaVariant variant, QWidget* parent) - : QTextEdit(parent), m_variant(variant), m_showCharacterCounter(false), m_maxLength(0), - m_minLines(1), m_maxLines(0), m_isFloating(false), m_hasError(false), - m_floatingProgress(0.0f), m_updatingGeometry(false) { + : QTextEdit(parent), m_material(this, + MaterialWidgetBase::Config{ + .useRipple = true, + .useElevation = false, + .useFocusIndicator = true, + }), + m_variant(variant), m_showCharacterCounter(false), m_maxLength(0), m_minLines(1), + m_maxLines(0), m_isFloating(false), m_hasError(false), m_floatingProgress(0.0f), + m_updatingGeometry(false) { // Disable native frame setFrameStyle(QFrame::NoFrame); @@ -86,24 +88,6 @@ TextArea::TextArea(TextAreaVariant variant, QWidget* parent) setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - // Connect text change signal connect(this, &QTextEdit::textChanged, this, &TextArea::textChanged); @@ -141,53 +125,30 @@ TextArea::~TextArea() { void TextArea::mousePressEvent(QMouseEvent* event) { QTextEdit::mousePressEvent(event); - if (m_stateMachine) - m_stateMachine->onPress(event->pos()); - if (m_ripple) - m_ripple->onPress(event->pos(), rect()); - update(); + m_material.onMousePress(event->pos(), rect()); } void TextArea::mouseReleaseEvent(QMouseEvent* event) { QTextEdit::mouseReleaseEvent(event); - if (m_stateMachine) - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); - update(); + m_material.onMouseRelease(); } void TextArea::focusInEvent(QFocusEvent* event) { QTextEdit::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); + m_material.onFocusIn(); updateFloatingState(true); - update(); } void TextArea::focusOutEvent(QFocusEvent* event) { QTextEdit::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); + m_material.onFocusOut(); updateFloatingState(!toPlainText().isEmpty()); - update(); } void TextArea::changeEvent(QEvent* event) { QTextEdit::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } @@ -815,16 +776,16 @@ void TextArea::drawCharacterCounter(QPainter& p, const QRectF& helperRect) { void TextArea::drawFocusIndicator(QPainter& p, const QRectF& fieldRect) { if (!p.isActive()) return; // guard - if (m_focusIndicator && hasFocus()) { + if (m_material.focusIndicator() && hasFocus()) { QPainterPath shape = roundedRect(fieldRect, cornerRadius()); - m_focusIndicator->paint(&p, shape, focusOutlineColor()); + m_material.focusIndicator()->paint(&p, shape, focusOutlineColor()); } } void TextArea::drawRipple(QPainter& p, const QRectF& fieldRect) { if (!p.isActive()) return; // guard - if (m_ripple) { + if (m_material.ripple()) { QPainterPath shape; if (m_variant == TextAreaVariant::Outlined) { shape = roundedRect(fieldRect, cornerRadius()); @@ -832,7 +793,7 @@ void TextArea::drawRipple(QPainter& p, const QRectF& fieldRect) { // For filled variant, clip to background shape.addRect(fieldRect); } - m_ripple->paint(&p, shape); + m_material.ripple()->paint(&p, shape); } } @@ -856,16 +817,19 @@ void TextArea::animateFloatingTo(bool floating) { return; } - if (!m_animationFactory) { + auto factory = cf::WeakPtr::DynamicCast( + application_support::Application::animationFactory()); + + if (!factory) { m_floatingProgress = target; update(); return; } // Create property animation for floating progress (same approach as TextField) - auto anim = m_animationFactory->createPropertyAnimation( - &m_floatingProgress, m_floatingProgress, target, 200, - cf::ui::base::Easing::Type::EmphasizedDecelerate, this); + auto anim = + factory->createPropertyAnimation(&m_floatingProgress, m_floatingProgress, target, 200, + cf::ui::base::Easing::Type::EmphasizedDecelerate, this); if (anim) { anim->start(); diff --git a/ui/widget/material/widget/textarea/textarea.h b/ui/widget/material/widget/textarea/textarea.h index 8a4ce266a..2f7f38981 100644 --- a/ui/widget/material/widget/textarea/textarea.h +++ b/ui/widget/material/widget/textarea/textarea.h @@ -21,19 +21,11 @@ #include #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - using CFColor = cf::ui::base::CFColor; /** @@ -519,10 +511,7 @@ class CF_UI_EXPORT TextArea : public QTextEdit { QFont helperFont() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Properties TextAreaVariant m_variant; diff --git a/ui/widget/material/widget/textfield/textfield.cpp b/ui/widget/material/widget/textfield/textfield.cpp index 740852f8b..4c4d6ad41 100644 --- a/ui/widget/material/widget/textfield/textfield.cpp +++ b/ui/widget/material/widget/textfield/textfield.cpp @@ -17,12 +17,10 @@ #include "application_support/application.h" #include "base/device_pixel.h" #include "base/geometry_helper.h" +#include "base/include/base/weak_ptr/weak_ptr.h" #include "cfmaterial_animation_factory.h" #include "components/material/cfmaterial_property_animation.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -36,7 +34,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; using namespace cf::ui::base::geometry; -using namespace cf::ui::components; using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; @@ -74,8 +71,14 @@ inline CFColor fallbackPrimary() { // ============================================================================ TextField::TextField(TextFieldVariant variant, QWidget* parent) - : QLineEdit(parent), m_variant(variant), m_showCharacterCounter(false), m_maxLength(0), - m_isFloating(false), m_hasError(false), m_floatingProgress(0.0f), m_outlineWidth(1.0f), + : QLineEdit(parent), m_material(this, + MaterialWidgetBase::Config{ + .useRipple = true, + .useElevation = false, + .useFocusIndicator = true, + }), + m_variant(variant), m_showCharacterCounter(false), m_maxLength(0), m_isFloating(false), + m_hasError(false), m_floatingProgress(0.0f), m_outlineWidth(1.0f), m_hoveringClearButton(false), m_pressingClearButton(false) { // Disable native frame and background @@ -83,24 +86,6 @@ TextField::TextField(TextFieldVariant variant, QWidget* parent) setAttribute(Qt::WA_TranslucentBackground); setTextMargins(0, 0, 0, 0); - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - // Connect text change signal connect(this, &QLineEdit::textChanged, this, &TextField::textChanged); @@ -134,10 +119,10 @@ void TextField::mousePressEvent(QMouseEvent* event) { } QLineEdit::mousePressEvent(event); - if (m_stateMachine) - m_stateMachine->onPress(event->pos()); - if (m_ripple) - m_ripple->onPress(event->pos(), rect()); + if (m_material.stateMachine()) + m_material.stateMachine()->onPress(event->pos()); + if (m_material.ripple()) + m_material.ripple()->onPress(event->pos(), rect()); update(); } @@ -154,29 +139,29 @@ void TextField::mouseReleaseEvent(QMouseEvent* event) { } QLineEdit::mouseReleaseEvent(event); - if (m_stateMachine) - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); + if (m_material.stateMachine()) + m_material.stateMachine()->onRelease(); + if (m_material.ripple()) + m_material.ripple()->onRelease(); update(); } void TextField::focusInEvent(QFocusEvent* event) { QLineEdit::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); + if (m_material.stateMachine()) + m_material.stateMachine()->onFocusIn(); + if (m_material.focusIndicator()) + m_material.focusIndicator()->onFocusIn(); updateFloatingState(true); update(); } void TextField::focusOutEvent(QFocusEvent* event) { QLineEdit::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); + if (m_material.stateMachine()) + m_material.stateMachine()->onFocusOut(); + if (m_material.focusIndicator()) + m_material.focusIndicator()->onFocusOut(); updateFloatingState(!text().isEmpty()); update(); } @@ -184,13 +169,7 @@ void TextField::focusOutEvent(QFocusEvent* event) { void TextField::changeEvent(QEvent* event) { QLineEdit::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } + m_material.onEnabledChange(isEnabled()); update(); } } @@ -1035,14 +1014,14 @@ void TextField::drawCharacterCounter(QPainter& p, const QRectF& helperRect) { } void TextField::drawFocusIndicator(QPainter& p, const QRectF& fieldRect) { - if (m_focusIndicator && hasFocus()) { + if (m_material.focusIndicator() && hasFocus()) { QPainterPath shape = roundedRect(fieldRect, cornerRadius()); - m_focusIndicator->paint(&p, shape, focusOutlineColor()); + m_material.focusIndicator()->paint(&p, shape, focusOutlineColor()); } } void TextField::drawRipple(QPainter& p, const QRectF& fieldRect) { - if (m_ripple) { + if (m_material.ripple()) { QPainterPath shape; if (m_variant == TextFieldVariant::Outlined) { shape = roundedRect(fieldRect, cornerRadius()); @@ -1050,7 +1029,7 @@ void TextField::drawRipple(QPainter& p, const QRectF& fieldRect) { // For filled variant, clip to background shape.addRect(fieldRect); } - m_ripple->paint(&p, shape); + m_material.ripple()->paint(&p, shape); } } @@ -1074,16 +1053,20 @@ void TextField::animateFloatingTo(bool floating) { return; } - if (!m_animationFactory) { + // Get animation factory locally for custom property animations + auto factory = cf::WeakPtr::DynamicCast( + application_support::Application::animationFactory()); + + if (!factory) { m_floatingProgress = target; update(); return; } // Create property animation for floating progress - auto anim = m_animationFactory->createPropertyAnimation( - &m_floatingProgress, m_floatingProgress, target, 200, - cf::ui::base::Easing::Type::EmphasizedDecelerate, this); + auto anim = + factory->createPropertyAnimation(&m_floatingProgress, m_floatingProgress, target, 200, + cf::ui::base::Easing::Type::EmphasizedDecelerate, this); if (anim) { // IMPORTANT: Update range to fix cached animation's stale from/to values diff --git a/ui/widget/material/widget/textfield/textfield.h b/ui/widget/material/widget/textfield/textfield.h index 2c0ff7858..92e08f538 100644 --- a/ui/widget/material/widget/textfield/textfield.h +++ b/ui/widget/material/widget/textfield/textfield.h @@ -1,540 +1,528 @@ -/** - * @file ui/widget/material/widget/textfield/textfield.h - * @brief Material Design 3 TextField widget. - * - * Implements Material Design 3 text field with support for filled and outlined - * variants. Includes floating labels, prefix/suffix icons, character counter, - * helper/error text, and password mode. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include -#include -#include - -#include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" -#include "export.h" - -namespace cf::ui::widget::material { - -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 TextField widget. - * - * @details Implements Material Design 3 text field with support for filled - * and outlined variants. Includes floating labels, prefix/suffix icons, - * character counter, helper/error text, and password mode. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT TextField : public QLineEdit { - Q_OBJECT - Q_PROPERTY(TextFieldVariant variant READ variant WRITE setVariant) - Q_PROPERTY(QString label READ label WRITE setLabel) - Q_PROPERTY(QString helperText READ helperText WRITE setHelperText) - Q_PROPERTY(QString errorText READ errorText WRITE setErrorText) - Q_PROPERTY(int maxLength READ maxLength WRITE setMaxLength) - Q_PROPERTY(bool showCharacterCounter READ showCharacterCounter WRITE - setShowCharacterCounter) - Q_PROPERTY(bool isFloating READ isFloating NOTIFY floatingChanged) - Q_PROPERTY(QIcon prefixIcon READ prefixIcon WRITE setPrefixIcon) - Q_PROPERTY(QIcon suffixIcon READ suffixIcon WRITE setSuffixIcon) - - public: - /** - * @brief TextField visual variant. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - enum class TextFieldVariant { Filled, Outlined }; - Q_ENUM(TextFieldVariant); - - /** - * @brief Constructor with variant. - * - * @param[in] variant TextField visual variant. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit TextField(TextFieldVariant variant = TextFieldVariant::Filled, - QWidget* parent = nullptr); - - /** - * @brief Constructor with text and variant. - * - * @param[in] text Initial text content. - * @param[in] variant TextField visual variant. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit TextField(const QString& text, TextFieldVariant variant = TextFieldVariant::Filled, - QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~TextField() override; - - /** - * @brief Gets the text field variant. - * - * @return Current text field variant. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - TextFieldVariant variant() const; - - /** - * @brief Sets the text field variant. - * - * @param[in] variant TextField variant to use. - * - * @throws None - * @note Changing variant updates the visual appearance. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setVariant(TextFieldVariant variant); - - /** - * @brief Gets the label text. - * - * @return Current label text. - * - * @throws None - * @note The label floats when the field has focus or content. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QString label() const; - - /** - * @brief Sets the label text. - * - * @param[in] label Label text to display. - * - * @throws None - * @note The label floats when the field has focus or content. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setLabel(const QString& label); - - /** - * @brief Gets the helper text. - * - * @return Current helper text. - * - * @throws None - * @note Helper text is displayed below the text field. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QString helperText() const; - - /** - * @brief Sets the helper text. - * - * @param[in] text Helper text to display. - * - * @throws None - * @note Helper text is displayed below the text field. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setHelperText(const QString& text); - - /** - * @brief Gets the error text. - * - * @return Current error text. - * - * @throws None - * @note Error text takes precedence over helper text. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QString errorText() const; - - /** - * @brief Sets the error text. - * - * @param[in] text Error text to display. Empty string clears error state. - * - * @throws None - * @note Error text takes precedence over helper text. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setErrorText(const QString& text); - - /** - * @brief Checks if the label is in floating state. - * - * @return true if label is floating, false otherwise. - * - * @throws None - * @note Label floats when field has focus or content. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool isFloating() const; - - /** - * @brief Gets the prefix icon. - * - * @return Current prefix icon. - * - * @throws None - * @note Prefix icon is displayed at the start of the field. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QIcon prefixIcon() const; - - /** - * @brief Sets the prefix icon. - * - * @param[in] icon Icon to display at the start of the field. - * - * @throws None - * @note Prefix icon is displayed at the start of the field. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setPrefixIcon(const QIcon& icon); - - /** - * @brief Gets the suffix icon. - * - * @return Current suffix icon. - * - * @throws None - * @note Suffix icon is displayed at the end of the field. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QIcon suffixIcon() const; - - /** - * @brief Sets the suffix icon. - * - * @param[in] icon Icon to display at the end of the field. - * - * @throws None - * @note Suffix icon is displayed at the end of the field. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setSuffixIcon(const QIcon& icon); - - /** - * @brief Gets whether character counter is shown. - * - * @return true if character counter is visible, false otherwise. - * - * @throws None - * @note Character counter is shown in the helper text area. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool showCharacterCounter() const; - - /** - * @brief Sets whether character counter is shown. - * - * @param[in] show true to show character counter, false to hide. - * - * @throws None - * @note Character counter is shown in the helper text area. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setShowCharacterCounter(bool show); - - /** - * @brief Gets the maximum length. - * - * @return Maximum character length. - * - * @throws None - * @note 0 means no maximum. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - int maxLength() const; - - /** - * @brief Sets the maximum length. - * - * @param[in] length Maximum character length. 0 means no maximum. - * - * @throws None - * @note 0 means no maximum. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setMaxLength(int length); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the text field. - * - * @throws None - * @note Based on label, icons, padding, and helper text. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures touch target size requirements. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - signals: - /** - * @brief Signal emitted when floating state changes. - * - * @param[in] floating true if label is floating, false otherwise. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void floatingChanged(bool floating); - - protected: - /** - * @brief Paints the text field. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements Material Design paint pipeline. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles resize event. - * - * @param[in] event Resize event. - * - * @throws None - * @note Updates internal layout calculations. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void resizeEvent(QResizeEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple effect. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates ripple state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Updates floating label state and shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Updates floating label state and hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Handles text change event. - * - * @param[in] text New text content. - * - * @throws None - * @note Updates floating label state and character counter. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void textChanged(const QString& text); - - private: - // Drawing helpers - Material Design paint pipeline - void drawBackground(QPainter& p, const QRectF& fieldRect); - void drawOutline(QPainter& p, const QRectF& fieldRect); - void drawLabel(QPainter& p, const QRectF& fieldRect); - void drawText(QPainter& p, const QRectF& textRect); - void drawPrefixIcon(QPainter& p, const QRectF& textRect); - void drawSuffixIcon(QPainter& p, const QRectF& textRect); - void drawClearButton(QPainter& p, const QRectF& textRect); - void drawHelperText(QPainter& p, const QRectF& helperRect); - void drawCharacterCounter(QPainter& p, const QRectF& helperRect); - void drawFocusIndicator(QPainter& p, const QRectF& fieldRect); - void drawRipple(QPainter& p, const QRectF& fieldRect); - - // Layout helpers - QRectF fieldRect() const; - QRectF textRect() const; - QRectF helperTextRect() const; - QRectF prefixIconRect() const; - QRectF suffixIconRect() const; - QRectF clearButtonRect() const; - - // Animation helpers - void updateFloatingState(bool shouldFloat); - void animateFloatingTo(bool floating); - - // Color access methods - CFColor containerColor() const; - CFColor onContainerColor() const; - CFColor labelColor() const; - CFColor inputTextColor() const; - CFColor outlineColor() const; - CFColor focusOutlineColor() const; - CFColor errorColor() const; - CFColor helperTextColor() const; - float cornerRadius() const; - QFont inputFont() const; - QFont labelFont() const; - QFont helperFont() const; - - // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; - - // Properties - TextFieldVariant m_variant; - QString m_label; - QString m_helperText; - QString m_errorText; - QIcon m_prefixIcon; - QIcon m_suffixIcon; - bool m_showCharacterCounter; - int m_maxLength; - - // Internal state - bool m_isFloating; - bool m_hasError; - float m_floatingProgress; // 0.0 = resting, 1.0 = floating - float m_outlineWidth; // For animating outline width - bool m_hoveringClearButton; - bool m_pressingClearButton; -}; - -} // namespace cf::ui::widget::material +/** + * @file ui/widget/material/widget/textfield/textfield.h + * @brief Material Design 3 TextField widget. + * + * Implements Material Design 3 text field with support for filled and outlined + * variants. Includes floating labels, prefix/suffix icons, character counter, + * helper/error text, and password mode. + * + * @author CFDesktop Team + * @date 2026-03-01 + * @version 0.1 + * @since 0.1 + * @ingroup ui_widget_material_widget + */ +#pragma once + +#include +#include +#include + +#include "base/color.h" +#include "export.h" +#include "widget/material/base/material_widget_base.h" + +namespace cf::ui::widget::material { + +using CFColor = cf::ui::base::CFColor; + +/** + * @brief Material Design 3 TextField widget. + * + * @details Implements Material Design 3 text field with support for filled + * and outlined variants. Includes floating labels, prefix/suffix icons, + * character counter, helper/error text, and password mode. + * + * @since 0.1 + * @ingroup ui_widget_material_widget + */ +class CF_UI_EXPORT TextField : public QLineEdit { + Q_OBJECT + Q_PROPERTY(TextFieldVariant variant READ variant WRITE setVariant) + Q_PROPERTY(QString label READ label WRITE setLabel) + Q_PROPERTY(QString helperText READ helperText WRITE setHelperText) + Q_PROPERTY(QString errorText READ errorText WRITE setErrorText) + Q_PROPERTY(int maxLength READ maxLength WRITE setMaxLength) + Q_PROPERTY(bool showCharacterCounter READ showCharacterCounter WRITE setShowCharacterCounter) + Q_PROPERTY(bool isFloating READ isFloating NOTIFY floatingChanged) + Q_PROPERTY(QIcon prefixIcon READ prefixIcon WRITE setPrefixIcon) + Q_PROPERTY(QIcon suffixIcon READ suffixIcon WRITE setSuffixIcon) + + public: + /** + * @brief TextField visual variant. + * + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + enum class TextFieldVariant { Filled, Outlined }; + Q_ENUM(TextFieldVariant); + + /** + * @brief Constructor with variant. + * + * @param[in] variant TextField visual variant. + * @param[in] parent QObject parent. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + explicit TextField(TextFieldVariant variant = TextFieldVariant::Filled, + QWidget* parent = nullptr); + + /** + * @brief Constructor with text and variant. + * + * @param[in] text Initial text content. + * @param[in] variant TextField visual variant. + * @param[in] parent QObject parent. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + explicit TextField(const QString& text, TextFieldVariant variant = TextFieldVariant::Filled, + QWidget* parent = nullptr); + + /** + * @brief Destructor. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + ~TextField() override; + + /** + * @brief Gets the text field variant. + * + * @return Current text field variant. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + TextFieldVariant variant() const; + + /** + * @brief Sets the text field variant. + * + * @param[in] variant TextField variant to use. + * + * @throws None + * @note Changing variant updates the visual appearance. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setVariant(TextFieldVariant variant); + + /** + * @brief Gets the label text. + * + * @return Current label text. + * + * @throws None + * @note The label floats when the field has focus or content. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QString label() const; + + /** + * @brief Sets the label text. + * + * @param[in] label Label text to display. + * + * @throws None + * @note The label floats when the field has focus or content. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setLabel(const QString& label); + + /** + * @brief Gets the helper text. + * + * @return Current helper text. + * + * @throws None + * @note Helper text is displayed below the text field. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QString helperText() const; + + /** + * @brief Sets the helper text. + * + * @param[in] text Helper text to display. + * + * @throws None + * @note Helper text is displayed below the text field. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setHelperText(const QString& text); + + /** + * @brief Gets the error text. + * + * @return Current error text. + * + * @throws None + * @note Error text takes precedence over helper text. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QString errorText() const; + + /** + * @brief Sets the error text. + * + * @param[in] text Error text to display. Empty string clears error state. + * + * @throws None + * @note Error text takes precedence over helper text. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setErrorText(const QString& text); + + /** + * @brief Checks if the label is in floating state. + * + * @return true if label is floating, false otherwise. + * + * @throws None + * @note Label floats when field has focus or content. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + bool isFloating() const; + + /** + * @brief Gets the prefix icon. + * + * @return Current prefix icon. + * + * @throws None + * @note Prefix icon is displayed at the start of the field. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QIcon prefixIcon() const; + + /** + * @brief Sets the prefix icon. + * + * @param[in] icon Icon to display at the start of the field. + * + * @throws None + * @note Prefix icon is displayed at the start of the field. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setPrefixIcon(const QIcon& icon); + + /** + * @brief Gets the suffix icon. + * + * @return Current suffix icon. + * + * @throws None + * @note Suffix icon is displayed at the end of the field. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QIcon suffixIcon() const; + + /** + * @brief Sets the suffix icon. + * + * @param[in] icon Icon to display at the end of the field. + * + * @throws None + * @note Suffix icon is displayed at the end of the field. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setSuffixIcon(const QIcon& icon); + + /** + * @brief Gets whether character counter is shown. + * + * @return true if character counter is visible, false otherwise. + * + * @throws None + * @note Character counter is shown in the helper text area. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + bool showCharacterCounter() const; + + /** + * @brief Sets whether character counter is shown. + * + * @param[in] show true to show character counter, false to hide. + * + * @throws None + * @note Character counter is shown in the helper text area. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setShowCharacterCounter(bool show); + + /** + * @brief Gets the maximum length. + * + * @return Maximum character length. + * + * @throws None + * @note 0 means no maximum. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + int maxLength() const; + + /** + * @brief Sets the maximum length. + * + * @param[in] length Maximum character length. 0 means no maximum. + * + * @throws None + * @note 0 means no maximum. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setMaxLength(int length); + + /** + * @brief Gets the recommended size. + * + * @return Recommended size for the text field. + * + * @throws None + * @note Based on label, icons, padding, and helper text. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QSize sizeHint() const override; + + /** + * @brief Gets the minimum recommended size. + * + * @return Minimum recommended size. + * + * @throws None + * @note Ensures touch target size requirements. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QSize minimumSizeHint() const override; + + signals: + /** + * @brief Signal emitted when floating state changes. + * + * @param[in] floating true if label is floating, false otherwise. + * + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void floatingChanged(bool floating); + + protected: + /** + * @brief Paints the text field. + * + * @param[in] event Paint event. + * + * @throws None + * @note Implements Material Design paint pipeline. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void paintEvent(QPaintEvent* event) override; + + /** + * @brief Handles resize event. + * + * @param[in] event Resize event. + * + * @throws None + * @note Updates internal layout calculations. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void resizeEvent(QResizeEvent* event) override; + + /** + * @brief Handles mouse press event. + * + * @param[in] event Mouse event. + * + * @throws None + * @note Triggers ripple effect. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void mousePressEvent(QMouseEvent* event) override; + + /** + * @brief Handles mouse release event. + * + * @param[in] event Mouse event. + * + * @throws None + * @note Updates ripple state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void mouseReleaseEvent(QMouseEvent* event) override; + + /** + * @brief Handles focus in event. + * + * @param[in] event Focus event. + * + * @throws None + * @note Updates floating label state and shows focus indicator. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void focusInEvent(QFocusEvent* event) override; + + /** + * @brief Handles focus out event. + * + * @param[in] event Focus event. + * + * @throws None + * @note Updates floating label state and hides focus indicator. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void focusOutEvent(QFocusEvent* event) override; + + /** + * @brief Handles widget state change event. + * + * @param[in] event Change event. + * + * @throws None + * @note Updates appearance based on state changes. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void changeEvent(QEvent* event) override; + + /** + * @brief Handles text change event. + * + * @param[in] text New text content. + * + * @throws None + * @note Updates floating label state and character counter. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void textChanged(const QString& text); + + private: + // Drawing helpers - Material Design paint pipeline + void drawBackground(QPainter& p, const QRectF& fieldRect); + void drawOutline(QPainter& p, const QRectF& fieldRect); + void drawLabel(QPainter& p, const QRectF& fieldRect); + void drawText(QPainter& p, const QRectF& textRect); + void drawPrefixIcon(QPainter& p, const QRectF& textRect); + void drawSuffixIcon(QPainter& p, const QRectF& textRect); + void drawClearButton(QPainter& p, const QRectF& textRect); + void drawHelperText(QPainter& p, const QRectF& helperRect); + void drawCharacterCounter(QPainter& p, const QRectF& helperRect); + void drawFocusIndicator(QPainter& p, const QRectF& fieldRect); + void drawRipple(QPainter& p, const QRectF& fieldRect); + + // Layout helpers + QRectF fieldRect() const; + QRectF textRect() const; + QRectF helperTextRect() const; + QRectF prefixIconRect() const; + QRectF suffixIconRect() const; + QRectF clearButtonRect() const; + + // Animation helpers + void updateFloatingState(bool shouldFloat); + void animateFloatingTo(bool floating); + + // Color access methods + CFColor containerColor() const; + CFColor onContainerColor() const; + CFColor labelColor() const; + CFColor inputTextColor() const; + CFColor outlineColor() const; + CFColor focusOutlineColor() const; + CFColor errorColor() const; + CFColor helperTextColor() const; + float cornerRadius() const; + QFont inputFont() const; + QFont labelFont() const; + QFont helperFont() const; + + // Behavior components + base::MaterialWidgetBase m_material; + + // Properties + TextFieldVariant m_variant; + QString m_label; + QString m_helperText; + QString m_errorText; + QIcon m_prefixIcon; + QIcon m_suffixIcon; + bool m_showCharacterCounter; + int m_maxLength; + + // Internal state + bool m_isFloating; + bool m_hasError; + float m_floatingProgress; // 0.0 = resting, 1.0 = floating + float m_outlineWidth; // For animating outline width + bool m_hoveringClearButton; + bool m_pressingClearButton; +}; + +} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/treeview/treeview.cpp b/ui/widget/material/widget/treeview/treeview.cpp index eb025a0e4..1ae19d81c 100644 --- a/ui/widget/material/widget/treeview/treeview.cpp +++ b/ui/widget/material/widget/treeview/treeview.cpp @@ -20,12 +20,8 @@ #include "treeview.h" #include "application_support/application.h" #include "base/device_pixel.h" -#include "cfmaterial_animation_factory.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" #include "private/treeviewdelegate.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -36,8 +32,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; -using namespace cf::ui::components; -using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; using namespace cf::ui::widget::material::base; @@ -61,31 +55,18 @@ inline CFColor fallbackOnSurface() { // ============================================================================ TreeView::TreeView(QWidget* parent) - : QTreeView(parent), m_itemHeight(TreeItemHeight::Standard), - m_indentStyle(TreeIndentStyle::Material), m_showTreeLines(true), m_rootIsDecorated(true) { - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - + : QTreeView(parent), m_material(this, + MaterialWidgetBase::Config{ + .useRipple = true, + .useElevation = false, + .useFocusIndicator = true, + }), + m_itemHeight(TreeItemHeight::Standard), m_indentStyle(TreeIndentStyle::Material), + m_showTreeLines(true), m_rootIsDecorated(true) { // Initialize and set the delegate for item rendering m_delegate = new TreeViewItemDelegate(this); setItemDelegate(m_delegate); - // Set ripple mode to bounded (clipped to item bounds) - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - // QTreeView paints on viewport, so connect to viewport() for ripple updates - connect(m_ripple, &RippleHelper::repaintNeeded, viewport(), - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - // Configure tree view for Material appearance setHeaderHidden(true); // Disable Qt's default branch decoration to avoid duplicate arrow rendering @@ -256,9 +237,7 @@ CFColor TreeView::onSurfaceColor() const { void TreeView::enterEvent(QEnterEvent* event) { QTreeView::enterEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverEnter(); - } + m_material.onEnterEvent(); // Initialize hovered index on enter QPoint localPos = viewport()->mapFromGlobal(QCursor::pos()); m_hoveredIndex = indexAt(localPos); @@ -268,12 +247,7 @@ void TreeView::enterEvent(QEnterEvent* event) { void TreeView::leaveEvent(QEvent* event) { QTreeView::leaveEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverLeave(); - } - if (m_ripple) { - m_ripple->onCancel(); - } + m_material.onLeaveEvent(); // Clear hovered index if (m_hoveredIndex.isValid()) { QPersistentModelIndex oldIndex = m_hoveredIndex; @@ -343,12 +317,12 @@ void TreeView::mousePressEvent(QMouseEvent* event) { } // Still handle ripple/state effects - if (m_stateMachine) { - m_stateMachine->onPress(event->pos()); + if (m_material.stateMachine()) { + m_material.stateMachine()->onPress(event->pos()); } - if (m_ripple) { + if (m_material.ripple()) { // Use viewport coordinates for ripple (painter draws on viewport) - m_ripple->onPress(viewportPos, QRectF(itemRect)); + m_material.ripple()->onPress(viewportPos, QRectF(itemRect)); // Store clip rect for ripple animation m_rippleClipRect = QRectF(itemRect); } @@ -361,8 +335,8 @@ void TreeView::mousePressEvent(QMouseEvent* event) { // Non-icon area: use normal flow (selection, ripple, etc.) QTreeView::mousePressEvent(event); - if (m_stateMachine) { - m_stateMachine->onPress(event->pos()); + if (m_material.stateMachine()) { + m_material.stateMachine()->onPress(event->pos()); } // Track pressed index for ripple @@ -371,10 +345,10 @@ void TreeView::mousePressEvent(QMouseEvent* event) { m_pressedIndex = pressedIndex; m_pressPosition = event->pos(); - if (m_ripple) { + if (m_material.ripple()) { QRect itemRect = visualRect(pressedIndex); // Use viewport coordinates for ripple (painter draws on viewport) - m_ripple->onPress(viewportPos, QRectF(itemRect)); + m_material.ripple()->onPress(viewportPos, QRectF(itemRect)); // Store clip rect for ripple animation (so it continues after release) m_rippleClipRect = QRectF(itemRect); } @@ -385,11 +359,11 @@ void TreeView::mousePressEvent(QMouseEvent* event) { void TreeView::mouseReleaseEvent(QMouseEvent* event) { QTreeView::mouseReleaseEvent(event); - if (m_stateMachine) { - m_stateMachine->onRelease(); + if (m_material.stateMachine()) { + m_material.stateMachine()->onRelease(); } - if (m_ripple) { - m_ripple->onRelease(); + if (m_material.ripple()) { + m_material.ripple()->onRelease(); } m_pressedIndex = QPersistentModelIndex(); update(); @@ -397,37 +371,18 @@ void TreeView::mouseReleaseEvent(QMouseEvent* event) { void TreeView::focusInEvent(QFocusEvent* event) { QTreeView::focusInEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusIn(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusIn(); - } - update(); + m_material.onFocusIn(); } void TreeView::focusOutEvent(QFocusEvent* event) { QTreeView::focusOutEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusOut(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusOut(); - } - update(); + m_material.onFocusOut(); } void TreeView::changeEvent(QEvent* event) { QTreeView::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } @@ -465,30 +420,30 @@ void TreeView::paintEvent(QPaintEvent* event) { // Draw ripple if active (overlay on top of items) // RippleHelper manages its own state - always try to paint - if (m_ripple && !m_rippleClipRect.isEmpty()) { - m_ripple->setColor(onSurfaceColor()); + if (m_material.ripple() && !m_rippleClipRect.isEmpty()) { + m_material.ripple()->setColor(onSurfaceColor()); QPainterPath clipPath; clipPath.addRect(m_rippleClipRect); p.save(); p.setClipPath(clipPath); - m_ripple->paint(&p, clipPath); + m_material.ripple()->paint(&p, clipPath); p.restore(); // Clear clip rect when ripple is no longer active - if (!m_ripple->hasActiveRipple()) { + if (!m_material.ripple()->hasActiveRipple()) { m_rippleClipRect = QRectF(); } } // Draw focus indicator on current index if focused - if (m_focusIndicator && hasFocus()) { + if (m_material.focusIndicator() && hasFocus()) { QModelIndex current = currentIndex(); if (current.isValid()) { QRect itemRect = visualRect(current); if (itemRect.intersects(viewport()->rect())) { QPainterPath shape; shape.addRect(itemRect); - m_focusIndicator->paint(&p, shape, onSurfaceColor()); + m_material.focusIndicator()->paint(&p, shape, onSurfaceColor()); } } } diff --git a/ui/widget/material/widget/treeview/treeview.h b/ui/widget/material/widget/treeview/treeview.h index 764b356f7..f04527dc9 100644 --- a/ui/widget/material/widget/treeview/treeview.h +++ b/ui/widget/material/widget/treeview/treeview.h @@ -15,22 +15,14 @@ #pragma once #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" #include #include #include namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - class TreeViewItemDelegate; using CFColor = cf::ui::base::CFColor; @@ -397,10 +389,7 @@ class CF_UI_EXPORT TreeView : public QTreeView { CFColor onSurfaceColor() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Delegate for item rendering TreeViewItemDelegate* m_delegate; From 09f7025dc5a8c5b9801b529aa8c027f238edb588 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Sun, 24 May 2026 07:32:42 +0800 Subject: [PATCH 03/18] simplified: remove the old documentations --- base/README.md | 2 + desktop/base/logger/README.md | 2 + document/HandBook/base/weak_ptr.md | 36 +- document/HandBook/base/weak_ptr_factory.md | 57 +- .../base/config_manager/02-best-practices.md | 1618 +------------ .../desktop/base/config_manager/03-faq.md | 1421 +---------- .../desktop/base/config_manager/README.md | 134 - .../desktop/base/config_manager/index.md | 56 +- document/ci/README.md | 179 -- document/ci/index.md | 45 +- .../00_phase0_project_skeleton.md | 538 +---- .../design_stage/01_phase1_hardware_probe.md | 1078 +-------- .../design_stage/02_phase2_base_library.md | 1312 +--------- .../design_stage/03_phase3_input_layer.md | 1457 +---------- document/design_stage/04_phase6_simulator.md | 1024 +------- document/design_stage/05_phase8_testing.md | 915 +------ document/design_stage/README.md | 109 - document/design_stage/index.md | 49 +- .../multi_display_backend_architecture.md | 301 +-- .../system_architecture_overview.md | 629 +---- document/development/README.md | 173 -- document/development/index.md | 46 +- ...p-Behavior-Modeling-From-Bool-To-QFlags.md | 1112 +-------- .../notes/02-Qt-Window-Behavior-Analysis.md | 994 +------- .../03-Desktop-Strategy-Pattern-Design.md | 2147 +---------------- ...04-Desktop-Behavior-System-Architecture.md | 1739 +------------ .../05-Logger-Singleton-Link-Architecture.md | 359 +-- ...olicyChain-Clang18-Miscompilation-Debug.md | 164 +- document/notes/README.md | 189 -- document/notes/index.md | 100 +- document/scripts/README.md | 65 - document/scripts/build_helpers/README.md | 122 - document/scripts/build_helpers/index.md | 101 +- document/scripts/index.md | 53 +- document/scripts/lib/bash/README.md | 85 - document/scripts/lib/bash/index.md | 65 +- document/scripts/lib/powershell/README.md | 102 - document/scripts/lib/powershell/index.md | 81 +- document/scripts/release/hooks/README.md | 40 - document/scripts/release/hooks/index.md | 32 +- document/todo/README.md | 80 - .../todo/done/00_project_skeleton_status.md | 447 ---- .../todo/done/01_hardware_probe_status.md | 435 ---- document/todo/done/02_base_library_status.md | 625 ----- document/todo/done/03_input_layer_status.md | 541 ----- document/todo/done/04_simulator_status.md | 232 -- document/todo/done/05_testing_status.md | 303 --- .../todo/done/06_infrastructure_status.md | 170 -- document/todo/done/13_widget_apps_status.md | 156 -- .../todo/done/14_display_backend_status.md | 353 --- .../done/99_ui_material_framework_status.md | 629 ----- document/todo/done/PROJECT_STATUS_REPORT.md | 515 ---- document/todo/done/SUMMARY.md | 29 + document/todo/done/index.md | 36 - .../done/milestone_01_desktop_skeleton.md | 189 -- document/todo/index.md | 42 +- scripts/README.md | 2 + scripts/lib/README.md | 2 + scripts/release/hooks/README.md | 2 + 59 files changed, 1101 insertions(+), 22418 deletions(-) delete mode 100644 document/HandBook/desktop/base/config_manager/README.md delete mode 100644 document/ci/README.md delete mode 100644 document/design_stage/README.md delete mode 100644 document/development/README.md delete mode 100644 document/notes/README.md delete mode 100644 document/scripts/README.md delete mode 100644 document/scripts/build_helpers/README.md delete mode 100644 document/scripts/lib/bash/README.md delete mode 100644 document/scripts/lib/powershell/README.md delete mode 100644 document/scripts/release/hooks/README.md delete mode 100644 document/todo/README.md delete mode 100644 document/todo/done/00_project_skeleton_status.md delete mode 100644 document/todo/done/01_hardware_probe_status.md delete mode 100644 document/todo/done/02_base_library_status.md delete mode 100644 document/todo/done/03_input_layer_status.md delete mode 100644 document/todo/done/04_simulator_status.md delete mode 100644 document/todo/done/05_testing_status.md delete mode 100644 document/todo/done/06_infrastructure_status.md delete mode 100644 document/todo/done/13_widget_apps_status.md delete mode 100644 document/todo/done/14_display_backend_status.md delete mode 100644 document/todo/done/99_ui_material_framework_status.md delete mode 100644 document/todo/done/PROJECT_STATUS_REPORT.md create mode 100644 document/todo/done/SUMMARY.md delete mode 100644 document/todo/done/index.md delete mode 100644 document/todo/done/milestone_01_desktop_skeleton.md diff --git a/base/README.md b/base/README.md index 9436154a6..a3af0071a 100644 --- a/base/README.md +++ b/base/README.md @@ -79,3 +79,5 @@ For detailed API reference, implementation notes, and usage examples, see the Ha - Memory API: [document/HandBook/api/system/memory/memory_info.md](../document/HandBook/api/system/memory/memory_info.md) - Windows CPU implementation: [document/HandBook/implementation/windows/cpu_implementation.md](../document/HandBook/implementation/windows/cpu_implementation.md) - Linux CPU implementation: [document/HandBook/implementation/linux/cpu_implementation.md](../document/HandBook/implementation/linux/cpu_implementation.md) + +> 完整 API 手册: [document/HandBook/base/overview.md](../document/HandBook/base/overview.md) diff --git a/desktop/base/logger/README.md b/desktop/base/logger/README.md index d9b700b0a..6a6189fa1 100644 --- a/desktop/base/logger/README.md +++ b/desktop/base/logger/README.md @@ -126,3 +126,5 @@ desktop/base/logger/ - [Performance](../../../document/HandBook/desktop/base/logger/performance.md) - [Best Practices](../../../document/HandBook/desktop/base/logger/best_practices.md) - [Troubleshooting](../../../document/HandBook/desktop/base/logger/troubleshooting.md) + +> 完整日志手册: [document/HandBook/desktop/base/logger/](../../../document/HandBook/desktop/base/logger/) diff --git a/document/HandBook/base/weak_ptr.md b/document/HandBook/base/weak_ptr.md index a01799d90..74d85ba25 100644 --- a/document/HandBook/base/weak_ptr.md +++ b/document/HandBook/base/weak_ptr.md @@ -87,7 +87,7 @@ assert(!weak.IsValid()); assert(weak.Get() == nullptr); ```text -这个设计避免了 `shared_ptr` 的隐式生命周期延长问题。持有 `WeakPtr` 不会阻止对象被销毁,这也是它和 `std::weak_ptr` 的核心区别之一。 +这个设计避免了 `shared_ptr` 的隐式生命周期延长问题。持有 `WeakPtr` 不会阻止对象被销毁,这也是它和 `std::weak_ptr` 的核心区别之一。同时需要注意的是,`WeakPtr` 内部持有的是指向工厂内嵌标志的裸指针(而非 `shared_ptr`),因此 `WeakPtr` 的生命周期**绝不能**超过工厂——工厂析构后再访问 `WeakPtr` 是未定义行为。 ## 类型转换 @@ -126,7 +126,7 @@ if (derived_again) { ## 线程安全考虑 -`WeakPtr` **不是线程安全的**,应该在同一个线程(或序列)中使用。"检查有效性"和"访问对象"这两步操作不是原子的: +`WeakPtr` **不是完全线程安全的**。内部的存活标志使用 `std::atomic` 实现,`IsAlive()` 和 `Invalidate()` 本身是线程安全的。但"检查有效性"和"访问对象"这两步操作不是原子的: ```cpp // 危险:多线程环境下 @@ -140,7 +140,7 @@ if (weak.IsValid()) { // 检查通过 // 或者用其他同步机制保护整个检查+访问过程 ```text -这个限制和 `std::weak_ptr::lock()` 不一样。标准库的 `lock()` 是原子的,可以返回一个 `shared_ptr` 保证对象在使用期间存活。我们选择不提供这个功能,是因为项目里的使用场景大多是单线程的,不需要这个开销。 +这个限制和 `std::weak_ptr::lock()` 不一样。标准库的 `lock()` 是原子的,可以返回一个 `shared_ptr` 保证对象在使用期间存活。我们选择不提供这个功能,是因为我们的设计中对象有唯一拥有者,不存在共享所有权。此外,标志直接嵌入在工厂中(无堆分配),`WeakPtr` 只持有裸指针,无法像 `shared_ptr` 那样延长对象生命周期。 ## 手动失效 @@ -173,38 +173,20 @@ obj.InvalidateAllRefs(); // 手动失效 assert(!weak1.IsValid()); // 失效 assert(!weak2.IsValid()); // 失效 -// 失效后仍然可以创建新的弱引用 -auto weak3 = obj.GetWeakPtr(); -assert(weak3.IsValid()); // 新的弱引用有效 +// 注意:失效后不能再调用 GetWeakPtr(),会触发断言失败 +// auto weak3 = obj.GetWeakPtr(); // 断言失败! ```text -这个功能在某些场景下很有用,比如你想显式通知所有观察者对象不再可用,但又不想真的销毁对象。注意失效后创建的新弱引用是有效的,因为工厂内部会分配新的标志位。 - -## 检查外部引用 - -`WeakPtrFactory::HasWeakPtrs()` 可以检查是否有外部持有的弱引用: - -```cpp -class MyClass { -private: - cf::WeakPtrFactory weak_factory_{this}; - -public: - bool HasExternalReferences() const { - return weak_factory_.HasWeakPtrs(); - } -}; -```bash - -这个接口通过 `shared_ptr::use_count()` 实现,所以是 O(1) 的。如果 `use_count() > 1`,说明除了工厂自己外,还有其他地方持有弱引用标志位。这个功能在调试或内存泄漏排查时有用。 +这个功能在某些场景下很有用,比如你想显式通知所有观察者对象不再可用,但又不想真的销毁对象。与旧版实现不同,失效后**不能再创建新的弱引用**——`GetWeakPtr()` 会触发断言失败。这是因为存活标志直接嵌入在工厂中(使用 `std::atomic`),失效只是将标志设为 `false`,不会分配新的标志。如果你需要"重启"后继续创建弱引用,应该使用一个全新的工厂实例。 ## 与 std::weak_ptr 的对比 | 特性 | cf::WeakPtr | std::weak_ptr | |------|-------------|---------------| | 所有权模型 | 独占拥有者 | 引用计数 | -| 性能开销 | 极小 | 较小(引用计数) | -| 线程安全 | 不保证 | lock() 原子操作 | +| 性能开销 | 极小(裸指针 + 原子标志) | 较小(引用计数) | +| 标志存储 | 工厂内嵌,无堆分配 | 控制块堆分配 | +| 线程安全 | 标志检查原子,访问不保证 | lock() 原子操作 | | 使用场景 | 明确生命周期的对象 | 共享所有权 | | 依赖工厂 | 需要 WeakPtrFactory | 需要 shared_ptr | diff --git a/document/HandBook/base/weak_ptr_factory.md b/document/HandBook/base/weak_ptr_factory.md index 0d29e4fe5..9b1bcc988 100644 --- a/document/HandBook/base/weak_ptr_factory.md +++ b/document/HandBook/base/weak_ptr_factory.md @@ -70,7 +70,7 @@ assert(weak1.Get() == weak2.Get()); assert(weak2.Get() == weak3.Get()); ```text -每次调用都创建一个新的 `WeakPtr` 对象,但它们共享同一个内部的"存活标志"。对象销毁或调用 `InvalidateWeakPtrs()` 后,所有弱引用同时失效。 +每次调用都创建一个新的 `WeakPtr` 对象,但它们共享同一个内部的"存活标志"。对象销毁或调用 `InvalidateWeakPtrs()` 后,所有弱引用同时失效。存活标志(`WeakReferenceFlag`)直接嵌入在 `WeakPtrFactory` 内部,不涉及任何堆分配。所有 `WeakPtr` 实例通过裸指针引用这个标志,因此 `WeakPtr` 的创建开销极小。 ## 手动失效 @@ -95,31 +95,9 @@ private: }; ```text -`InvalidateWeakPtrs()` 会把当前的存活标志设为失效,然后分配一个新的。失效前创建的所有弱引用都会变成无效,失效后调用 `GetWeakPtr()` 得到的新弱引用使用新的标志,所以是有效的。 +`InvalidateWeakPtrs()` 会把内部的存活标志设为失效。失效前创建的所有弱引用都会变成无效。注意,与旧版实现不同,失效后**不能再创建新的弱引用**——后续调用 `GetWeakPtr()` 会触发断言失败。这是因为标志直接嵌入在工厂中,失效操作只是将 `std::atomic` 设为 `false`,不会分配新的标志。 -这个功能在对象"重置"或"重启"时很有用——你想通知所有观察者旧的状态已经不可用,但对象本身还在。 - -## 检查外部引用 - -`HasWeakPtrs()` 可以检查是否有外部持有的弱引用: - -```cpp -class Service { -public: - void Shutdown() { - if (weak_factory_.HasWeakPtrs()) { - // 通知所有持有弱引用的地方 - NotifyShutdown(); - } - // 实际关闭服务... - } - -private: - cf::WeakPtrFactory weak_factory_{this}; -}; -```text - -这个接口通过内部的 `shared_ptr` 引用计数实现,所以是 O(1) 的。如果 `use_count() > 1`,说明有外部弱引用存在。注意这个检查不是实时的——调用完 `HasWeakPtrs()` 后,其他线程可能立即创建或销毁弱引用。 +这个功能在对象"重置"或"关闭"时很有用——你想通知所有观察者对象不再可用,但对象本身还在。如果你需要"重启"后继续创建弱引用,应该考虑使用一个全新的工厂实例。 ## 不可复制不可移动 @@ -193,23 +171,24 @@ private: }; ```text -### 延迟销毁检查 +### 单次失效模式 + +由于 `InvalidateWeakPtrs()` 后不能再创建新的弱引用,推荐的使用模式是"失效即终局"——调用一次 `InvalidateWeakPtrs()` 表示对象进入不可用状态,之后不再创建新的弱引用: ```cpp class ResourceManager { public: void UnloadResource(const std::string& id) { - // 先让弱引用失效 + // 让所有弱引用失效,之后不能再创建新的 weak_factory_.InvalidateWeakPtrs(); - // 检查是否有地方还在使用 - if (weak_factory_.HasWeakPtrs()) { - // 有外部引用,延迟销毁 - ScheduleDeferredUnload(id); - } else { - // 立即销毁 - resources_.erase(id); - } + // 安全地清理资源,所有外部弱引用已失效 + resources_.erase(id); + } + + // 注意:UnloadResource 后 GetWeakPtr() 会断言失败 + cf::WeakPtr GetWeakPtr() { + return weak_factory_.GetWeakPtr(); } private: @@ -221,13 +200,15 @@ private: 第一,不要在构造函数里就把 `this` 传给其他地方。对象构造完成前,其他成员还没初始化,通过弱引用访问会导致未定义行为。`WeakPtrFactory` 的构造函数里有断言检查 `this` 非空,但不会检查构造是否完成。 -第二,不要在析构函数里调用 `GetWeakPtr()`。对象已经在销毁过程中,创建新的弱引用没有意义。如果你确实需要,重新考虑设计——为什么在析构函数里还要传递自己的引用? +第二,不要在析构函数里调用 `GetWeakPtr()`。对象已经在销毁过程中,创建新的弱引用没有意义。`WeakPtrFactory` 的析构函数会自动调用 `InvalidateWeakPtrs()`,之后 `GetWeakPtr()` 会断言失败。 + +第三,存活标志使用 `std::atomic` 实现,`IsAlive()` 和 `Invalidate()` 本身是线程安全的。但 `WeakPtr` 的"检查有效性 + 访问对象"两步操作不是原子的,仍然需要在同一线程(或序列)中使用。 -第三,`InvalidateWeakPtrs()` 不是原子的。多线程环境下如果一边失效一边创建新的弱引用,可能会出现新创建的弱引用意外失效。如果有这种需求,得自己加锁保护。 +第四,所有 `WeakPtr` 实例的生命周期不能超过工厂。因为 `WeakPtr` 持有的是指向工厂内嵌标志的裸指针(不是 `shared_ptr`),工厂析构后,任何对 `WeakPtr` 的访问都是未定义行为。这也是为什么工厂必须声明为最后一个成员——确保标志在其他成员的析构函数运行之前就失效,而不是等到工厂自身析构。 ## 与标准库的对比 -`WeakPtrFactory` 没有标准库的直接对应物。`std::enable_shared_from_this` 提供了类似的功能,但它是配合 `shared_ptr` 使用的,而我们假设对象有明确的唯一拥有者。 +`WeakPtrFactory` 没有标准库的直接对应物。`std::enable_shared_from_this` 提供了类似的功能,但它是配合 `shared_ptr` 使用的,而我们假设对象有明确的唯一拥有者。内部标志直接嵌入在工厂中(无堆分配),`WeakPtr` 通过裸指针引用该标志,整体开销比 `shared_ptr` 方案小得多。 如果你需要在已有代码中引入 `WeakPtrFactory`,最简单的迁移方式是把原来持有裸指针或引用的地方改成持有 `WeakPtr`。这通常不需要大幅改动调用代码,只需要在使用前检查有效性。 diff --git a/document/HandBook/desktop/base/config_manager/02-best-practices.md b/document/HandBook/desktop/base/config_manager/02-best-practices.md index 71ac45326..09720f4f8 100644 --- a/document/HandBook/desktop/base/config_manager/02-best-practices.md +++ b/document/HandBook/desktop/base/config_manager/02-best-practices.md @@ -5,12 +5,9 @@ description: ConfigStore 提供四层存储架构,每一层都有其特定的 # ConfigStore 最佳实践 -## 文档信息 - | 项目 | 内容 | |------|------| | 文档版本 | v1.0 | -| 创建日期 | 2026-03-17 | | 所属模块 | cf::config (ConfigStore) | | 前置知识 | 快速入门指南 (01-quick-start.md) | @@ -18,1570 +15,208 @@ description: ConfigStore 提供四层存储架构,每一层都有其特定的 ## 一、键命名规范 -### 1.1 分层结构建议 - -ConfigStore 使用点分隔的分层键名结构,建议采用 2-4 层的命名深度: +### 键名格式 -```bash +```text [level1].[level2].[level3].[level4] | | | | | | | +-- 具体属性名 (必需) - | | +----------- 子模块 (可选) - | +-------------------- 功能模块 (必需) - +----------------------------- 命名空间/域 (必需) -```text - -**推荐示例:** + | +----------- 子模块 (可选) + +-------------------- 功能模块 (必需) ++----------------------------- 命名空间/域 (必需) +``` -```text -app.theme.name # 应用主题名称 -app.theme.dark_mode # 主题深色模式 -ui.window.width # 窗口宽度 -ui.window.height # 窗口高度 -user.preferences.language # 用户语言偏好 -network.proxy.host # 代理主机地址 -network.proxy.port # 代理端口 -editor.font.size # 编辑器字体大小 -editor.font.family # 编辑器字体族 -```bash - -### 1.2 命名约定 - -遵循以下命名约定以确保代码一致性: +### 命名规则 -| 规则 | 说明 | 示例 | -|------|------|------| -| **小写字母** | 所有层级使用小写字母 | `app.theme.name` | -| **点分隔** | 使用 `.` 作为层级分隔符 | `ui.window.width` | -| **语义清晰** | 使用描述性名称,避免缩写 | `max_file_size` 而非 `mfs` | -| **下划线连接** | 多词属性用 `_` 连接 | `dark_mode` 而非 `darkMode` | -| **避免数字** | 除非是版本或编号 | `schema_v2` 而非 `style1` | -| **全英文** | 键名使用英文,非中文 | `background_color` 而非 `背景颜色` | +| 规则 | 说明 | 正确示例 | 错误示例 | +|------|------|----------|----------| +| 小写字母 | 所有层级使用小写 | `app.theme.name` | `App.Theme.Name` | +| 点分隔 | `.` 作为层级分隔符 | `ui.window.width` | `ui/window/width` | +| 语义清晰 | 使用描述性名称,避免缩写 | `max_file_size` | `mfs` | +| 下划线连接 | 多词属性用 `_` 连接 | `dark_mode` | `darkMode` | +| 避免数字 | 除非是版本或编号 | `schema_v2` | `style1` | +| 全英文 | 键名使用英文 | `background_color` | `背景颜色` | -### 1.3 避免冲突的建议 - -为避免键名冲突,推荐为不同模块或组件使用命名空间前缀: +### 命名空间预留 ```text -# 为每个子系统预留独立的命名空间 app.* # 应用程序级配置 ui.* # 用户界面配置 editor.* # 编辑器配置 network.* # 网络配置 database.* # 数据库配置 logger.* # 日志配置 -```text +``` -**常见命名空间:** +在代码中定义常量以避免拼写错误: ```cpp -// 在代码中定义命名空间常量 namespace ConfigKeys { constexpr std::string_view APP_PREFIX = "app"; - constexpr std::string_view UI_PREFIX = "ui"; - constexpr std::string_view EDITOR_PREFIX = "editor"; - - // 预定义的 KeyView inline constexpr KeyView APP_THEME_NAME{.group = "app.theme", .key = "name"}; inline constexpr KeyView UI_WINDOW_WIDTH{.group = "ui.window", .key = "width"}; - inline constexpr KeyView EDITOR_FONT_SIZE{.group = "editor.font", .key = "size"}; } -```yaml +``` --- ## 二、层级使用策略 -ConfigStore 提供四层存储架构,每一层都有其特定的使用场景: - -### 2.1 层级优先级图 +### 层级优先级 -```bash +```text +-----------------------------+ -| Temp 层 (优先级 3) | 内存临时数据,进程重启后丢失 +| Temp 层 (优先级 3) | 内存临时数据,进程重启后丢失 +-----------------------------+ - | - v 覆盖 + | 覆盖 +-----------------------------+ -| App 层 (优先级 2) | 应用运行时配置,会话有效 +| App 层 (优先级 2) | 应用运行时配置,会话有效 +-----------------------------+ - | - v 覆盖 + | 覆盖 +-----------------------------+ -| User 层 (优先级 1) | 用户个人偏好,跨会话持久化 +| User 层 (优先级 1) | 用户个人偏好,跨会话持久化 +-----------------------------+ - | - v 覆盖 + | 覆盖 +-----------------------------+ -| System 层 (优先级 0) | 系统默认配置,管理员维护 +| System 层 (优先级 0) | 系统默认配置,管理员维护 +-----------------------------+ -```text - -### 2.2 System 层:系统默认 - -**用途:** 存储应用程序的系统默认配置,由管理员或安装程序维护。 - -**使用场景:** -- 默认配置值 -- 系统级限制(如最大文件大小) -- 硬件相关的默认设置 -- 部署环境配置 - -**示例:** - -```cpp -// 初始化系统默认配置 (通常在安装时或首次运行时设置) -void init_system_defaults() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - // 系统默认配置应该设置为只读参考 - store.register_key( - Key{.full_key = "app.version", .full_description = "Application version"}, - std::string("1.0.0"), - Layer::System - ); - - store.register_key( - Key{.full_key = "file.max_size_mb", .full_description = "Max file size in MB"}, - 100, - Layer::System - ); - - store.register_key( - Key{.full_key = "performance.default_dpi", .full_description = "Default DPI setting"}, - 96, - Layer::System - ); - - store.sync(SyncMethod::Sync); // 同步写入 -} -```text - -### 2.3 User 层:用户偏好 - -**用途:** 存储用户的个人偏好设置,在用户配置文件中持久化。 - -**使用场景:** -- 主题和外观设置 -- 语言和区域设置 -- 用户习惯(窗口大小、位置) -- 功能开关 - -**示例:** - -```cpp -// 保存用户偏好设置 -void save_user_preferences(const UserPreferences& prefs) { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - store.set(KeyView{.group = "user.theme", .key = "name"}, - prefs.theme_name, Layer::User); - store.set(KeyView{.group = "user.theme", .key = "dark_mode"}, - prefs.dark_mode, Layer::User); - store.set(KeyView{.group = "user.interface", .key = "language"}, - prefs.language, Layer::User); - store.set(KeyView{.group = "user.interface", .key = "font_size"}, - prefs.font_size, Layer::User); - - // 异步保存,不阻塞 UI - store.sync(SyncMethod::Async); -} - -// 加载用户偏好设置 -UserPreferences load_user_preferences() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - UserPreferences prefs; - - prefs.theme_name = store.query( - KeyView{.group = "user.theme", .key = "name"}, "default"); - prefs.dark_mode = store.query( - KeyView{.group = "user.theme", .key = "dark_mode"}, false); - prefs.language = store.query( - KeyView{.group = "user.interface", .key = "language"}, "en_US"); - prefs.font_size = store.query( - KeyView{.group = "user.interface", .key = "font_size"}, 12); - - return prefs; -} -```text - -### 2.4 App 层:运行时状态 - -**用途:** 存储应用程序运行时状态,与应用程序生命周期绑定。 - -**使用场景:** -- 窗口几何状态 -- 最近打开的文件列表 -- 会话恢复数据 -- 应用程序内部状态 +``` -**示例:** +### 各层使用规则 -```cpp -// 保存窗口状态 -void save_window_state(QWidget* window) { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - auto geometry = window->geometry(); - store.set(KeyView{.group = "app.window", .key = "x"}, - geometry.x(), Layer::App); - store.set(KeyView{.group = "app.window", .key = "y"}, - geometry.y(), Layer::App); - store.set(KeyView{.group = "app.window", .key = "width"}, - geometry.width(), Layer::App); - store.set(KeyView{.group = "app.window", .key = "height"}, - geometry.height(), Layer::App); - store.set(KeyView{.group = "app.window", .key = "maximized"}, - window->isMaximized(), Layer::App); +| 层级 | 持久化 | 谁可修改 | 存储内容 | +|------|--------|----------|----------| +| **System** | 是 | 安装程序/管理员 | 默认配置值、系统级限制(最大文件大小)、硬件相关默认设置、部署环境配置 | +| **User** | 是 | 最终用户 | 主题/外观设置、语言/区域设置、用户习惯(窗口大小/位置)、功能开关 | +| **App** | 是 | 应用程序 | 窗口几何状态、最近打开文件列表、会话恢复数据、应用内部状态 | +| **Temp** | 否 | 应用程序/测试代码 | 单元测试临时配置、功能预览模式、会话令牌、调试标志 | - store.sync(SyncMethod::Async); -} +### 层级选择决策表 -// 恢复窗口状态 -void restore_window_state(QWidget* window) { - using namespace cf::config; - auto& store = ConfigStore::instance(); +| 场景 | 推荐层级 | +|------|---------| +| 应用默认值 / 系统限制 | System | +| 用户偏好 / 功能开关 | User | +| 窗口状态 / 会话数据 | App | +| 测试数据 / 会话令牌 | Temp | - int x = store.query(KeyView{.group = "app.window", .key = "x"}, 100); - int y = store.query(KeyView{.group = "app.window", .key = "y"}, 100); - int w = store.query(KeyView{.group = "app.window", .key = "width"}, 800); - int h = store.query(KeyView{.group = "app.window", .key = "height"}, 600); - bool maximized = store.query( - KeyView{.group = "app.window", .key = "maximized"}, false); - - window->setGeometry(x, y, w, h); - if (maximized) { - window->showMaximized(); - } -} -```text - -### 2.5 Temp 层:临时数据 - -**用途:** 存储进程生命周期内的临时数据,不持久化到磁盘。 - -**使用场景:** -- 单元测试中的临时配置 -- 功能演示/预览模式 -- 会话令牌 -- 调试标志 - -**示例:** +### 各层代码示例 ```cpp -// 使用 Temp 层进行单元测试 -TEST(ConfigStoreTest, OverrideWithTempLayer) { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - // 设置系统默认值 - store.set(KeyView{.group = "test", .key = "value"}, - "system_default", Layer::System); - - // 使用 Temp 层覆盖 (不持久化) - store.set(KeyView{.group = "test", .key = "value"}, - "temp_override", Layer::Temp); - - // 查询返回 Temp 层的值 - auto value = store.query( - KeyView{.group = "test", .key = "value"}); - EXPECT_EQ(value, "temp_override"); - - // 清除 Temp 层后,回退到 System 层 - store.clear_layer(Layer::Temp); - value = store.query( - KeyView{.group = "test", .key = "value"}); - EXPECT_EQ(value, "system_default"); -} - -// 临时启用调试模式 -void enable_debug_mode_temporarily() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - store.set(KeyView{.group = "debug", .key = "enabled"}, - true, Layer::Temp); - store.set(KeyView{.group = "debug", .key = "log_level"}, - 5, Layer::Temp); +// System 层:系统默认(安装时或首次运行时设置) +store.register_key(Key{.full_key = "file.max_size_mb", + .full_description = "Max file size in MB"}, 100, Layer::System); - // 不会持久化到磁盘 -} -```bash +// User 层:用户偏好 +store.set(KeyView{.group = "user.theme", .key = "dark_mode"}, true, Layer::User); +store.set(KeyView{.group = "user.interface", .key = "language"}, "zh_CN", Layer::User); +store.sync(SyncMethod::Async); -### 2.6 层级选择决策表 +// App 层:运行时状态 +store.set(KeyView{.group = "app.window", .key = "width"}, geometry.width(), Layer::App); +store.set(KeyView{.group = "app.window", .key = "maximized"}, window->isMaximized(), Layer::App); -| 场景 | 推荐层级 | 持久化 | 谁可修改 | -|------|---------|--------|----------| -| 应用默认值 | System | 是 | 安装程序/管理员 | -| 用户偏好 | User | 是 | 最终用户 | -| 窗口状态 | App | 是 | 应用程序 | -| 会话令牌 | Temp | 否 | 应用程序 | -| 测试数据 | Temp | 否 | 测试代码 | -| 限制配置 | System | 是 | 管理员 | -| 功能开关 | User | 是 | 最终用户 | +// Temp 层:临时数据(不持久化) +store.set(KeyView{.group = "debug", .key = "enabled"}, true, Layer::Temp); +``` --- ## 三、性能优化 -### 3.1 缓存利用技巧 - -ConfigStore 在内存中维护缓存,遵循以下最佳实践以充分利用缓存: - -**1. 优先使用优先级查询** - -```cpp -// 推荐:让 ConfigStore 自动查找最高优先级的值 -auto value = store.query(KeyView{.group = "app", .key = "timeout"}, 5000); - -// 不推荐:手动查询各层 -int value = 5000; -if (auto v = store.query(KeyView{.group = "app", .key = "timeout"}, Layer::Temp)) { - value = *v; -} else if (auto v = store.query(KeyView{.group = "app", .key = "timeout"}, Layer::App)) { - value = *v; -} // ... -```text - -**2. 批量读取配置** - -```cpp -// 推荐:一次性读取需要的配置 -struct AppSettings { - std::string theme; - int dpi; - bool dark_mode; - double scale; - - static AppSettings load() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - return AppSettings{ - .theme = store.query( - KeyView{.group = "app.theme", .key = "name"}, "default"), - .dpi = store.query( - KeyView{.group = "app.display", .key = "dpi"}, 96), - .dark_mode = store.query( - KeyView{.group = "app.theme", .key = "dark_mode"}, false), - .scale = store.query( - KeyView{.group = "app.display", .key = "scale"}, 1.0) - }; - } -}; - -// 使用缓存的配置 -auto settings = AppSettings::load(); -apply_settings(settings); -```text - -**3. 避免频繁查询同一配置** - -```cpp -// 不推荐:每次函数调用都查询 -void render_frame() { - auto scale = ConfigStore::instance().query( - KeyView{.group = "display", .key = "scale"}, 1.0); - // 使用 scale... -} - -// 推荐:缓存配置值,监听变化 -class Renderer { -public: - Renderer() { - // 初始化时读取 - scale_ = ConfigStore::instance().query( - KeyView{.group = "display", .key = "scale"}, 1.0); - - // 监听变化 - watcher_handle_ = ConfigStore::instance().watch( - "display.scale", - [this](const Key&, const std::any*, const std::any* new_val, Layer) { - if (new_val) { - scale_ = std::any_cast(*new_val); - on_scale_changed(); - } - } - ); - } - - ~Renderer() { - ConfigStore::instance().unwatch(watcher_handle_); - } +### 缓存利用 - void render_frame() { - // 直接使用缓存的值 - double scale = scale_; - // ... - } +| 做法 | 推荐/不推荐 | 说明 | +|------|-------------|------| +| 优先级查询(省略 Layer 参数) | 推荐 | 自动返回最高优先级值 | +| 手动遍历各层查询 | 不推荐 | 冗余且低效 | +| 批量读取配置到结构体 | 推荐 | 减少 API 调用次数 | +| 在高频函数中重复 `query()` | 不推荐 | 缓存值 + 监听变化更优 | -private: - double scale_; - WatcherHandle watcher_handle_; - - void on_scale_changed() { - // 处理缩放变化 - } -}; -```text - -### 3.2 批量操作建议 - -当需要修改多个配置项时,使用手动通知策略可以批量处理变更: +### 批量操作 ```cpp -// 批量修改配置 -void update_multiple_settings() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - // 使用 Manual 策略,避免每次 set 都触发 Watcher - NotifyPolicy manual = NotifyPolicy::Manual; - - store.set(KeyView{.group = "ui", .key = "theme"}, "dark", Layer::User, manual); - store.set(KeyView{.group = "ui", .key = "font_size"}, 14, Layer::User, manual); - store.set(KeyView{.group = "ui", .key = "scale"}, 1.25, Layer::User, manual); - - // 一次性触发所有 Watcher - store.notify(); - - // 一次性同步到磁盘 - store.sync(SyncMethod::Async); -} -```text +// 使用 Manual 策略批量修改,一次性触发 Watcher +NotifyPolicy manual = NotifyPolicy::Manual; +store.set(KeyView{.group = "ui", .key = "theme"}, "dark", Layer::User, manual); +store.set(KeyView{.group = "ui", .key = "font_size"}, 14, Layer::User, manual); +store.set(KeyView{.group = "ui", .key = "scale"}, 1.25, Layer::User, manual); +store.notify(); +store.sync(SyncMethod::Async); +``` -### 3.3 异步持久化的使用 +### 异步持久化 -对于频繁写入的配置,使用异步持久化避免阻塞主线程: +频繁写入时使用 `SyncMethod::Async` 避免阻塞主线程;应用退出时使用 `SyncMethod::Sync` 确保数据落盘。 ```cpp -// 异步保存配置示例 -class ConfigSaver { -public: - static ConfigSaver& instance() { - static ConfigSaver instance; - return instance; - } - - void schedule_save() { - if (!save_pending_.load()) { - save_pending_.store(true); - // 延迟 500ms 后保存,避免频繁 I/O - QTimer::singleShot(500, [this]() { - if (save_pending_.load()) { - ConfigStore::instance().sync(SyncMethod::Async); - save_pending_.store(false); - } - }); - } - } - - void save_immediately() { - ConfigStore::instance().sync(SyncMethod::Sync); - save_pending_.store(false); - } +// 运行时:延迟异步保存 +ConfigStore::instance().sync(SyncMethod::Async); -private: - ConfigSaver() = default; - std::atomic save_pending_{false}; -}; - -// 使用示例 -void on_config_changed() { - ConfigStore::instance().set(...); - ConfigSaver::instance().schedule_save(); // 延迟异步保存 -} - -void on_application_exit() { - ConfigSaver::instance().save_immediately(); // 退出时立即保存 -} -```yaml +// 退出时:立即同步保存 +ConfigStore::instance().sync(SyncMethod::Sync); +``` --- ## 四、线程安全 -### 4.1 读多写少场景的最佳实践 - -ConfigStore 使用 `std::shared_mutex` 实现读写锁,非常适合读多写少的场景: +### 规则速查 -```cpp -// 多线程读取配置 - 完全并发 -class ConfigReader { -public: - int get_timeout() const { - // query() 内部使用 shared_lock,允许多个线程并发读取 - return ConfigStore::instance().query( - KeyView{.group = "network", .key = "timeout"}, 5000); - } - - std::string get_server_url() const { - return ConfigStore::instance().query( - KeyView{.group = "network", .key = "server_url"}, "localhost"); - } -}; - -// 多线程安全使用 -void worker_thread(int id) { - ConfigReader reader; - for (int i = 0; i < 1000; ++i) { - int timeout = reader.get_timeout(); - std::string url = reader.get_server_url(); - // 使用配置... - } -} -```text - -### 4.2 避免死锁的注意事项 +| 规则 | 说明 | +|------|------| +| 读并发安全 | `query()` 内部使用 `shared_lock`,多线程可并发读取 | +| 禁止在 Watcher 回调中调用 `set()` | 可能导致死锁;改用原子标志位延迟处理 | +| 持有外部锁时避免调用 ConfigStore | 先释放外部锁,再调用 ConfigStore | +| Watcher 回调应轻量 | 提取信息后入队,由主线程或专用线程处理 | -**1. 不要在 Watcher 回调中调用 ConfigStore::set()** +### 典型错误与修正 ```cpp -// 危险:可能导致死锁 -auto handle = ConfigStore::instance().watch( - "app.theme", - [](const Key&, auto, auto, Layer) { - // 危险!在回调中再次写入可能导致死锁 - ConfigStore::instance().set(KeyView{.group = "app", .key = "changed"}, true); - } -); +// 危险:在 Watcher 回调中再次写入 +auto handle = store.watch("app.theme", [](const Key&, auto, auto, Layer) { + ConfigStore::instance().set(KeyView{.group = "app", .key = "changed"}, true); // 死锁! +}); -// 安全:使用标志位延迟处理 +// 安全:使用原子标志延迟处理 std::atomic theme_changed{false}; -auto handle = ConfigStore::instance().watch( - "app.theme", - [&theme_changed](const Key&, auto, auto, Layer) { - theme_changed.store(true); - } -); - -// 在主循环中处理 -void main_loop() { - if (theme_changed.load()) { - theme_changed.store(false); - // 安全地处理主题变更 - ConfigStore::instance().set(KeyView{.group = "app", .key = "changed"}, true); - } -} -```text - -**2. 持有其他锁时避免调用 ConfigStore** - -```cpp -class MyClass { - std::mutex data_mutex_; - SomeData data_; - - void update_config() { - // 不推荐:在持有 data_mutex_ 时调用 ConfigStore - std::lock_guard lock(data_mutex_); - ConfigStore::instance().set(...); // 可能导致死锁 - } - - // 推荐:分离锁的范围 - void update_config_safe() { - // 先准备数据 - SomeData new_data; - { - std::lock_guard lock(data_mutex_); - new_data = data_; - } - // 释放锁后再调用 ConfigStore - ConfigStore::instance().set(...); - } -}; -```text - -### 4.3 Watcher 回调中的注意事项 - -```cpp -// Watcher 回调的最佳实践 -class SafeWatcher { -public: - void start_watching() { - handle_ = ConfigStore::instance().watch( - "app.*", - [this](const Key& k, const std::any* old_val, - const std::any* new_val, Layer layer) { - // 1. 避免在回调中执行耗时操作 - // 2. 不要在回调中直接修改 ConfigStore - // 3. 使用线程安全的方式通知主线程 - - // 提取必要的信息 - std::string key = k.full_key; - std::any new_value = new_val ? *new_val : std::any(); - - // 将事件放入队列,由主线程处理 - std::lock_guard lock(queue_mutex_); - event_queue_.push({key, new_value, layer}); - cv_.notify_one(); - } - ); - - // 启动处理线程 - worker_thread_ = std::thread(&SafeWatcher::process_events, this); - } - - ~SafeWatcher() { - { - std::lock_guard lock(queue_mutex_); - stop_ = true; - } - cv_.notify_one(); - if (worker_thread_.joinable()) { - worker_thread_.join(); - } - ConfigStore::instance().unwatch(handle_); - } - -private: - struct Event { - std::string key; - std::any value; - Layer layer; - }; - - void process_events() { - while (true) { - std::unique_lock lock(queue_mutex_); - cv_.wait(lock, [this] { - return stop_ || !event_queue_.empty(); - }); - - if (stop_) break; - - while (!event_queue_.empty()) { - auto event = event_queue_.front(); - event_queue_.pop(); - lock.unlock(); - - // 处理事件 (不在锁内) - handle_event(event); - - lock.lock(); - } - } - } - - void handle_event(const Event& event) { - // 安全地处理配置变更 - // 可以在这里安全地读取 ConfigStore - } - - WatcherHandle handle_; - std::thread worker_thread_; - std::mutex queue_mutex_; - std::condition_variable cv_; - std::queue event_queue_; - bool stop_ = false; -}; -```yaml +auto handle = store.watch("app.theme", [&theme_changed](const Key&, auto, auto, Layer) { + theme_changed.store(true); +}); +``` --- ## 五、错误处理 -### 5.1 类型转换的处理 - -ConfigStore 使用 `std::any` 和 `QVariant` 存储值,类型转换可能失败: - -```cpp -// 安全的类型转换处理 -class SafeConfigReader { -public: - // 方法 1:使用默认值 - int get_int(KeyView kv, int default_value = 0) { - try { - return ConfigStore::instance().query(kv, default_value); - } catch (const std::exception& e) { - log_error("Failed to read int config: ", e.what()); - return default_value; - } - } - - // 方法 2:使用 optional 检查 - std::optional get_double(KeyView kv) { - auto value = ConfigStore::instance().query(kv); - if (value.has_value()) { - return value; - } - // 尝试从字符串转换 - auto str_value = ConfigStore::instance().query(kv); - if (str_value) { - try { - return std::stod(*str_value); - } catch (...) { - return std::nullopt; - } - } - return std::nullopt; - } - - // 方法 3:验证范围 - int get_percentage(KeyView kv) { - int value = ConfigStore::instance().query(kv, 100); - // 限制在 0-100 范围 - if (value < 0) return 0; - if (value > 100) return 100; - return value; - } -}; -```text - -### 5.2 键验证的处理 - -```cpp -// 键名验证 -class ConfigValidator { -public: - static bool is_valid_key(const std::string& key) { - // 检查键名格式 - if (key.empty() || key.length() > 256) { - return false; - } - - // 检查只包含允许的字符 - for (char c : key) { - if (!(std::isalnum(c) || c == '_' || c == '.')) { - return false; - } - } - - return true; - } - - static bool set_safe(const KeyView& kv, const std::string& value) { - // 先验证键名 - KeyHelper helper; - Key k; - if (!helper.fromKeyViewToKey(kv, k)) { - return false; - } - - if (!is_valid_key(k.full_key)) { - return false; - } - - // 再执行设置 - return ConfigStore::instance().set(kv, value); - } -}; -```text - -### 5.3 优雅降级策略 +| 策略 | 说明 | +|------|------| +| 提供默认值 | `query(kv, default_value)` 在键不存在时返回默认值 | +| 范围校验 | 对读取的值做 min/max 裁剪,防止配置文件被手动篡改导致异常 | +| 链式降级 | 先尝试主键,再尝试备用键,最后用硬编码默认值 | ```cpp -// 配置加载的优雅降级 -class RobustConfigLoader { -public: - struct Config { - std::string server_url; - int timeout; - int retry_count; - bool enable_cache; - - static Config load_with_fallbacks() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - Config config; - - // 尝试多个可能的键名 - config.server_url = store.query( - KeyView{.group = "network", .key = "server_url"}, - // 尝试备用键名 - store.query( - KeyView{.group = "network", .key = "host"}, - // 最后使用硬编码默认值 - "localhost" - ) - ); - - // 带验证的加载 - config.timeout = load_int_with_validation( - KeyView{.group = "network", .key = "timeout"}, - 1000, // 默认值 - 100, // 最小值 - 60000 // 最大值 - ); - - config.retry_count = load_int_with_validation( - KeyView{.group = "network", .key = "retry_count"}, - 3, // 默认值 - 0, // 最小值 - 10 // 最大值 - ); - - config.enable_cache = store.query( - KeyView{.group = "network", .key = "enable_cache"}, - true // 默认启用 - ); - - return config; - } - - private: - static int load_int_with_validation(KeyView kv, int default_value, - int min_value, int max_value) { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - int value = store.query(kv, default_value); - if (value < min_value) { - log_warning("Config value too low, using minimum: ", min_value); - return min_value; - } - if (value > max_value) { - log_warning("Config value too high, using maximum: ", max_value); - return max_value; - } - return value; - } - - static void log_warning(const std::string& msg, int value) { - // 实现日志记录 - } - }; -}; -```yaml +// 带范围校验的读取 +int timeout = store.query(KeyView{.group = "network", .key = "timeout"}, 1000); +timeout = std::clamp(timeout, 100, 60000); +``` --- -## 六、实践模式 - -### 6.1 模式一:配置管理器封装类 - -封装 ConfigStore 以提供类型安全的配置访问接口: - -```cpp -/** - * @brief 应用程序配置管理器 - * - * 封装 ConfigStore,提供类型安全的配置访问接口, - * 并集中管理配置键名和默认值。 - */ -class AppConfigManager { -public: - // 单例模式 - static AppConfigManager& instance() { - static AppConfigManager instance; - return instance; - } - - // ========== 主题配置 ========== - - struct ThemeConfig { - std::string name = "default"; - bool dark_mode = false; - int accent_color = 0x2196F3; - }; - - ThemeConfig get_theme() const { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - return ThemeConfig{ - .name = store.query( - KeyView{.group = "app.theme", .key = "name"}, "default"), - .dark_mode = store.query( - KeyView{.group = "app.theme", .key = "dark_mode"}, false), - .accent_color = store.query( - KeyView{.group = "app.theme", .key = "accent_color"}, 0x2196F3) - }; - } - - void set_theme(const ThemeConfig& config) { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - store.set(KeyView{.group = "app.theme", .key = "name"}, - config.name, Layer::User); - store.set(KeyView{.group = "app.theme", .key = "dark_mode"}, - config.dark_mode, Layer::User); - store.set(KeyView{.group = "app.theme", .key = "accent_color"}, - config.accent_color, Layer::User); - - store.sync(SyncMethod::Async); - } - - // ========== 网络配置 ========== - - struct NetworkConfig { - std::string server_url = "https://api.example.com"; - int timeout_ms = 5000; - int max_retries = 3; - bool enable_proxy = false; - std::string proxy_host; - int proxy_port = 8080; - }; - - NetworkConfig get_network() const { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - return NetworkConfig{ - .server_url = store.query( - KeyView{.group = "network", .key = "server_url"}, - "https://api.example.com"), - .timeout_ms = clamp_range( - store.query(KeyView{.group = "network", .key = "timeout_ms"}, 5000), - 1000, 30000), - .max_retries = clamp_range( - store.query(KeyView{.group = "network", .key = "max_retries"}, 3), - 0, 10), - .enable_proxy = store.query( - KeyView{.group = "network", .key = "enable_proxy"}, false), - .proxy_host = store.query( - KeyView{.group = "network", .key = "proxy_host"}, ""), - .proxy_port = store.query( - KeyView{.group = "network", .key = "proxy_port"}, 8080) - }; - } - - // ========== 编辑器配置 ========== - - struct EditorConfig { - std::string font_family = "Monospace"; - int font_size = 12; - bool line_numbers = true; - bool word_wrap = false; - int tab_size = 4; - }; - - EditorConfig get_editor() const { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - return EditorConfig{ - .font_family = store.query( - KeyView{.group = "editor", .key = "font_family"}, "Monospace"), - .font_size = clamp_range( - store.query(KeyView{.group = "editor", .key = "font_size"}, 12), - 8, 72), - .line_numbers = store.query( - KeyView{.group = "editor", .key = "line_numbers"}, true), - .word_wrap = store.query( - KeyView{.group = "editor", .key = "word_wrap"}, false), - .tab_size = clamp_range( - store.query(KeyView{.group = "editor", .key = "tab_size"}, 4), - 1, 8) - }; - } - - // ========== 初始化 ========== - - void initialize() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - // 注册所有配置键 - register_theme_keys(); - register_network_keys(); - register_editor_keys(); - - store.sync(SyncMethod::Sync); - } - -private: - AppConfigManager() = default; - ~AppConfigManager() = default; - - // 禁止拷贝和移动 - AppConfigManager(const AppConfigManager&) = delete; - AppConfigManager& operator=(const AppConfigManager&) = delete; - - static int clamp_range(int value, int min_val, int max_val) { - if (value < min_val) return min_val; - if (value > max_val) return max_val; - return value; - } - - void register_theme_keys() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - store.register_key( - Key{.full_key = "app.theme.name", - .full_description = "Application theme name"}, - std::string("default"), Layer::User); - - store.register_key( - Key{.full_key = "app.theme.dark_mode", - .full_description = "Enable dark mode"}, - false, Layer::User); - - store.register_key( - Key{.full_key = "app.theme.accent_color", - .full_description = "Theme accent color (ARGB)"}, - 0x2196F3, Layer::User); - } - - void register_network_keys() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - store.register_key( - Key{.full_key = "network.server_url", - .full_description = "API server URL"}, - std::string("https://api.example.com"), Layer::User); - - store.register_key( - Key{.full_key = "network.timeout_ms", - .full_description = "Network request timeout in milliseconds"}, - 5000, Layer::User); - - store.register_key( - Key{.full_key = "network.max_retries", - .full_description = "Maximum number of retry attempts"}, - 3, Layer::User); - } - - void register_editor_keys() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - store.register_key( - Key{.full_key = "editor.font_family", - .full_description = "Editor font family"}, - std::string("Monospace"), Layer::User); - - store.register_key( - Key{.full_key = "editor.font_size", - .full_description = "Editor font size in points"}, - 12, Layer::User); - - store.register_key( - Key{.full_key = "editor.tab_size", - .full_description = "Tab size in spaces"}, - 4, Layer::User); - } -}; - -// 使用示例 -void use_app_config() { - auto& config = AppConfigManager::instance(); - - // 获取配置 - auto theme = config.get_theme(); - auto network = config.get_network(); - auto editor = config.get_editor(); - - // 修改配置 - ThemeConfig new_theme; - new_theme.name = "dark"; - new_theme.dark_mode = true; - config.set_theme(new_theme); -} -```text - -### 6.2 模式二:热重载支持 - -实现配置文件的监听和热重载功能: - -```cpp -/** - * @brief 配置热重载管理器 - * - * 监听配置文件变化,自动重新加载配置并通知观察者。 - */ -class ConfigHotReload { -public: - using ConfigChangedCallback = std::function; - - static ConfigHotReload& instance() { - static ConfigHotReload instance; - return instance; - } - - void start_watching() { - if (watcher_) return; // 已启动 - - // 使用 QFileSystemWatcher 监听配置文件 - watcher_ = std::make_unique(); - - // 添加要监听的配置文件路径 - auto paths = get_config_paths(); - for (const auto& path : paths) { - if (QFile::exists(path)) { - watcher_->addPath(path); - } - } - - // 连接文件变化信号 - QObject::connect(watcher_.get(), &QFileSystemWatcher::fileChanged, - this, &ConfigHotReload::on_file_changed); - - // 设置防抖定时器 - debounce_timer_ = std::make_unique(); - debounce_timer_->setSingleShot(true); - debounce_timer_->setInterval(500); // 500ms 防抖 - - QObject::connect(debounce_timer_.get(), &QTimer::timeout, - this, &ConfigHotReload::reload_config); - } - - void stop_watching() { - watcher_.reset(); - debounce_timer_.reset(); - } - - void add_observer(ConfigChangedCallback callback) { - observers_.push_back(std::move(callback)); - } - -private: - ConfigHotReload() = default; - ~ConfigHotReload() = default; - - QStringList get_config_paths() { - using namespace cf::config; - // 获取配置文件路径 - // 这里简化处理,实际应该从 PathProvider 获取 - return { - QString::fromStdString(get_user_config_path()), - QString::fromStdString(get_app_config_path()) - }; - } - - std::string get_user_config_path() { - // 实现获取用户配置路径 - return "~/.config/cfdesktop/user.ini"; - } - - std::string get_app_config_path() { - // 实现获取应用配置路径 - return "./config/app.ini"; - } - - void on_file_changed(const QString& path) { - // 文件变化时,启动防抖定时器 - qDebug() << "Config file changed:" << path; - debounce_timer_->start(); - } - - void reload_config() { - using namespace cf::config; - - qDebug() << "Reloading configuration..."; - - // 重新加载配置 - ConfigStore::instance().reload(); - - // 通知所有观察者 - for (auto& observer : observers_) { - observer(); - } - - qDebug() << "Configuration reloaded"; - } - - std::unique_ptr watcher_; - std::unique_ptr debounce_timer_; - std::vector observers_; -}; - -// 使用示例 -class ThemeManager { -public: - ThemeManager() { - // 监听配置变化 - ConfigHotReload::instance().add_observer([this]() { - on_config_reloaded(); - }); - } +## 六、实践模式速查 - void apply_theme() { - auto& config = AppConfigManager::instance(); - auto theme = config.get_theme(); - // 应用主题... - qDebug() << "Theme applied:" << QString::fromStdString(theme.name); - } - -private: - void on_config_reloaded() { - qDebug() << "Config reloaded, reapplying theme..."; - apply_theme(); - } -}; - -// 在应用启动时启用热重载 -void initialize_application() { - // 初始化配置 - AppConfigManager::instance().initialize(); - - // 启用热重载 (开发环境) -#ifdef QT_DEBUG - ConfigHotReload::instance().start_watching(); -#endif -} -```text - -### 6.3 模式三:配置迁移 - -处理配置版本升级时的数据迁移: - -```cpp -/** - * @brief 配置迁移管理器 - * - * 处理不同版本配置之间的数据迁移。 - */ -class ConfigMigration { -public: - static ConfigMigration& instance() { - static ConfigMigration instance; - return instance; - } - - /** - * 执行配置迁移 - * @param from_version 迁移前的版本号 - * @param to_version 迁移后的版本号 - */ - bool migrate(int from_version, int to_version) { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - for (int v = from_version; v < to_version; ++v) { - if (!migrate_to(v + 1)) { - return false; - } - } - - // 更新版本号 - store.set(KeyView{.group = "app", .key = "config_version"}, - to_version, Layer::User); - store.sync(SyncMethod::Sync); - - return true; - } - - /** - * 检查是否需要迁移 - */ - bool needs_migration() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - int current_version = store.query( - KeyView{.group = "app", .key = "config_version"}, 1); - int target_version = get_target_version(); - - return current_version < target_version; - } - - int get_current_version() const { - using namespace cf::config; - return ConfigStore::instance().query( - KeyView{.group = "app", .key = "config_version"}, 1); - } - -private: - ConfigMigration() = default; - - static constexpr int get_target_version() { return 2; } - - bool migrate_to(int version) { - switch (version) { - case 2: - return migrate_v1_to_v2(); - // 添加未来的版本迁移 - // case 3: - // return migrate_v2_to_v3(); - default: - return false; - } - } - - /** - * v1 -> v2 迁移 - * - * 变更: - * - app.theme.* 重命名为 ui.theme.* - * - editor.font.family 重命名为 editor.font_family - */ - bool migrate_v1_to_v2() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - qDebug() << "Migrating config from v1 to v2..."; - - // 手动通知策略,避免在迁移过程中触发 Watcher - NotifyPolicy manual = NotifyPolicy::Manual; - - // 迁移 app.theme.* -> ui.theme.* - migrate_key(store, "app.theme.name", "ui.theme.name", manual); - migrate_key(store, "app.theme.dark_mode", "ui.theme.dark_mode", manual); - migrate_key(store, "app.theme.accent_color", "ui.theme.accent_color", manual); - - // 迁移 editor.font.family -> editor.font_family - migrate_key(store, "editor.font.family", "editor.font_family", manual); - - // 删除旧键 - store.unregister_key(Key{.full_key = "app.theme.name", .full_description = ""}, - Layer::User, manual); - store.unregister_key(Key{.full_key = "app.theme.dark_mode", .full_description = ""}, - Layer::User, manual); - store.unregister_key(Key{.full_key = "app.theme.accent_color", .full_description = ""}, - Layer::User, manual); - store.unregister_key(Key{.full_key = "editor.font.family", .full_description = ""}, - Layer::User, manual); - - // 触发通知 - store.notify(); - - qDebug() << "Migration v1 -> v2 completed"; - return true; - } - - void migrate_key(cf::config::ConfigStore& store, - const std::string& old_key, - const std::string& new_key, - cf::config::NotifyPolicy policy) { - // 读取旧值 - auto old_value = get_any_value(store, old_key); - if (!old_value.has_value()) { - return; // 旧键不存在,跳过 - } - - // 写入新键 - set_any_value(store, new_key, *old_value, Layer::User, policy); - } - - std::optional get_any_value(cf::config::ConfigStore& store, - const std::string& key) { - // 简化实现,实际需要根据类型获取 - // 这里假设所有值都是字符串 - KeyView kv{.group = key.substr(0, key.rfind('.')), - .key = key.substr(key.rfind('.') + 1)}; - auto value = store.query(kv); - if (value) { - return *value; - } - return std::nullopt; - } - - void set_any_value(cf::config::ConfigStore& store, - const std::string& key, - const std::any& value, - Layer layer, - NotifyPolicy policy) { - // 简化实现 - if (value.type() == typeid(std::string)) { - KeyView kv{.group = key.substr(0, key.rfind('.')), - .key = key.substr(key.rfind('.') + 1)}; - store.set(kv, std::any_cast(value), layer, policy); - } - } -}; - -// 应用启动时执行迁移 -void check_and_migrate_config() { - auto& migration = ConfigMigration::instance(); - - if (migration.needs_migration()) { - int current = migration.get_current_version(); - int target = 2; // ConfigMigration::get_target_version() - - qDebug() << "Migrating config from v" << current << " to v" << target; - - if (migration.migrate(current, target)) { - qDebug() << "Config migration successful"; - } else { - qWarning() << "Config migration failed"; - } - } -} -```text - -### 6.4 模式四:多实例隔离(使用自定义路径提供者) - -通过自定义路径提供者实现多个 ConfigStore 实例的隔离: - -```cpp -/** - * @brief 自定义路径提供者 - * - * 为测试或特殊场景提供自定义配置文件路径。 - */ -class CustomPathProvider : public IConfigStorePathProvider { -public: - /** - * @brief 创建测试用的路径提供者 - * @param base_path 基础路径 - * @param instance_name 实例名称,用于区分不同实例 - */ - CustomPathProvider(const QString& base_path, - const QString& instance_name) - : base_path_(base_path), instance_name_(instance_name) { - - // 确保目录存在 - QDir().mkpath(base_path); - } - - QString system_path() const override { - return base_path_ + "/" + instance_name_ + "_system.ini"; - } - - QString user_dir() const override { - return base_path_; - } - - QString user_filename() const override { - return instance_name_ + "_user.ini"; - } - - QString app_dir() const override { - return base_path_; - } - - QString app_filename() const override { - return instance_name_ + "_app.ini"; - } - - bool is_layer_enabled(int layer_index) const override { - // 启用所有层 - (void)layer_index; - return true; - } - -private: - QString base_path_; - QString instance_name_; -}; - -/** - * @brief 隔离的配置存储包装器 - * - * 使用不同的配置文件路径创建隔离的配置存储实例。 - */ -class IsolatedConfigStore { -public: - explicit IsolatedConfigStore(const QString& instance_name, - const QString& base_path = "/tmp/test_config") - : instance_name_(instance_name), base_path_(base_path) { - - // 创建自定义路径提供者 - auto path_provider = std::make_shared(base_path, instance_name); - - // 初始化 ConfigStore - cf::config::ConfigStore::instance().initialize(path_provider); - } - - ~IsolatedConfigStore() { - // 可选:清理测试文件 - // cleanup(); - } - - void cleanup() { - QFile::remove(base_path_ + "/" + instance_name_ + "_system.ini"); - QFile::remove(base_path_ + "/" + instance_name_ + "_user.ini"); - QFile::remove(base_path_ + "/" + instance_name_ + "_app.ini"); - } - - cf::config::ConfigStore& store() { - return cf::config::ConfigStore::instance(); - } - -private: - QString instance_name_; - QString base_path_; -}; - -// ========== 使用示例 ========== - -// 示例 1:单元测试中的隔离配置 -class ConfigTest : public ::testing::Test { -protected: - void SetUp() override { - // 每个测试使用独立的配置文件 - test_store_ = std::make_unique( - QString("test_") + QString::number(std::rand()), // 随机实例名 - "/tmp/config_tests" - ); - } - - void TearDown() override { - test_store_->cleanup(); - } - - cf::config::ConfigStore& config() { - return test_store_->store(); - } - -private: - std::unique_ptr test_store_; -}; - -TEST_F(ConfigTest, SetValueAndQuery) { - auto& store = config(); - - store.set(cf::config::KeyView{.group = "test", .key = "value"}, - 42, cf::config::Layer::App); - - auto result = store.query(cf::config::KeyView{.group = "test", .key = "value"}); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 42); -} - -// 示例 2:多用户配置隔离 -class MultiUserConfigManager { -public: - struct UserConfig { - std::string username; - std::string theme; - int font_size; - }; - - void set_user_config(const std::string& username, const UserConfig& config) { - auto& store = get_user_store(username); - - store.set(cf::config::KeyView{.group = "user", .key = "theme"}, - config.theme, cf::config::Layer::User); - store.set(cf::config::KeyView{.group = "user", .key = "font_size"}, - config.font_size, cf::config::Layer::User); - - store.sync(cf::config::ConfigStore::SyncMethod::Sync); - } - - UserConfig get_user_config(const std::string& username) { - auto& store = get_user_store(username); - - return UserConfig{ - .username = username, - .theme = store.query( - cf::config::KeyView{.group = "user", .key = "theme"}, "default"), - .font_size = store.query( - cf::config::KeyView{.group = "user", .key = "font_size"}, 12) - }; - } - -private: - cf::config::ConfigStore& get_user_store(const std::string& username) { - // 每个用户使用独立的配置文件 - auto it = user_stores_.find(username); - if (it == user_stores_.end()) { - auto store = std::make_unique( - QString::fromStdString(username), - "/tmp/multi_user_config" - ); - it = user_stores_.emplace(username, std::move(store)).first; - } - return it->second->store(); - } - - std::unordered_map> user_stores_; -}; -```bash +| 模式 | 用途 | 关键思路 | +|------|------|----------| +| 配置管理器封装 | 类型安全的配置访问 | 单例 + 结构体封装 get/set,集中管理键名和默认值 | +| 热重载 | 开发环境监听配置文件变化 | `QFileSystemWatcher` + 防抖定时器 + `ConfigStore::reload()` | +| 配置迁移 | 版本升级时数据迁移 | 递增版本号 + `migrate_key()` 逐键迁移 + `NotifyPolicy::Manual` | +| 多实例隔离 | 测试或多用户场景 | 自定义 `IConfigStorePathProvider`,为每个实例提供独立文件路径 | --- ## 附录:快速参考 -### A.1 层级优先级速查 +### 层级优先级速查 | 层级 | 优先级 | 持久化 | 用途 | |------|--------|--------|------| @@ -1590,7 +225,7 @@ private: | User | 1 | 是 | 用户偏好 | | System | 0 (最低) | 是 | 系统默认 | -### A.2 常用代码片段 +### 常用代码片段 ```cpp // 设置用户偏好 @@ -1603,28 +238,23 @@ auto theme = ConfigStore::instance().query( // 批量修改 store.set(..., Layer::User, NotifyPolicy::Manual); -store.set(..., Layer::User, NotifyPolicy::Manual); store.notify(); store.sync(SyncMethod::Async); // 监听变化 -auto handle = store.watch("user.theme.*", [](auto... args) { - // 处理变化 -}); +auto handle = store.watch("user.theme.*", [](auto... args) { /* 处理变化 */ }); store.unwatch(handle); -```yaml +``` -### A.3 错误处理检查清单 +### 检查清单 -- [ ] 查询时是否提供合理的默认值? -- [ ] 类型转换是否考虑了失败情况? -- [ ] 键名是否进行了验证? -- [ ] Watcher 回调是否避免再次调用 ConfigStore? -- [ ] 是否使用了异步持久化避免阻塞? -- [ ] 多线程场景是否考虑了锁的顺序? +- 查询时是否提供合理的默认值? +- 类型转换是否考虑了失败情况? +- 键名是否进行了验证? +- Watcher 回调是否避免再次调用 ConfigStore? +- 是否使用了异步持久化避免阻塞? +- 多线程场景是否考虑了锁的顺序? --- -**文档版本:** v1.0 -**最后更新:** 2026-03-17 **维护者:** CFDesktop 团队 diff --git a/document/HandBook/desktop/base/config_manager/03-faq.md b/document/HandBook/desktop/base/config_manager/03-faq.md index b584b84f8..9589b3f3a 100644 --- a/document/HandBook/desktop/base/config_manager/03-faq.md +++ b/document/HandBook/desktop/base/config_manager/03-faq.md @@ -10,9 +10,7 @@ description: 本文档列出了 ConfigStore 使用中的常见问题、故障排 ## 目录 - [常见问题](#常见问题) -- [故障排查](#故障排查) - [平台特定问题](#平台特定问题) -- [性能问题](#性能问题) --- @@ -20,544 +18,219 @@ description: 本文档列出了 ConfigStore 使用中的常见问题、故障排 ### Q1: 配置修改后没有生效? -#### 可能原因 1:未调用 sync() - -配置修改后需要调用 `sync()` 才能持久化到磁盘。 +修改后必须调用 `sync()` 才能持久化到磁盘: ```cpp -// 问题代码 -ConfigStore::instance().set(KeyView{.group = "app", .key = "theme"}, "dark"); -// 程序崩溃或重启后配置丢失 - -// 解决方案:手动同步 ConfigStore::instance().set(KeyView{.group = "app", .key = "theme"}, "dark"); ConfigStore::instance().sync(SyncMethod::Sync); // 同步写入 // 或使用异步同步(推荐) ConfigStore::instance().sync(SyncMethod::Async); // 不阻塞调用方 -```text - -#### 可能原因 2:层级优先级问题 +``` -查询时返回的是高优先级的值,低优先级的修改被覆盖。 +如果仍不生效,检查层级优先级——查询时返回的是高优先级(Temp > App > User > System)的值: ```cpp -// 问题场景 -ConfigStore::instance().set(KeyView{.group = "app", .key = "theme"}, "light", Layer::System); +// Temp 层优先级最高,会覆盖其他层的值 ConfigStore::instance().set(KeyView{.group = "app", .key = "theme"}, "dark", Layer::Temp); +ConfigStore::instance().set(KeyView{.group = "app", .key = "theme"}, "light", Layer::System); +// query 返回 "dark" -// 查询时始终返回 "dark"(Temp 优先级高于 System) -auto theme = ConfigStore::instance().query( - KeyView{.group = "app", .key = "theme"}, "" -); // 返回 "dark" - -// 解决方案 1:清除高优先级配置 +// 解决方案:清除高优先级配置,或查询指定层级 ConfigStore::instance().clear_layer(Layer::Temp); - -// 解决方案 2:查询指定层级 auto system_theme = ConfigStore::instance().query( - KeyView{.group = "app", .key = "theme"}, - Layer::System, - "" + KeyView{.group = "app", .key = "theme"}, Layer::System, "" ); -```text - -#### 可能原因 3:KeyView 转换失败 - -KeyView 中包含非法字符导致键转换失败。 - -```cpp -// 问题代码 -KeyView invalid_kv{.group = "app theme", .key = "name"}; // 包含空格 -ConfigStore::instance().set(invalid_kv, "value"); // 返回 false - -// 解决方案:使用合法字符 -KeyView valid_kv{.group = "app_theme", .key = "name"}; // 仅字母、数字、下划线 -ConfigStore::instance().set(valid_kv, "value"); // 成功 -```yaml +``` --- ### Q2: 如何在不同环境使用不同配置? -#### 方法 1:使用层级分离 +使用层级分离或自定义路径提供者: ```cpp -// 开发环境 - 使用 System 层 +// 方式 1:编译期通过宏选择层级 #ifdef DEBUG ConfigStore::instance().set( KeyView{.group = "api", .key = "endpoint"}, - "https://dev.api.example.com", - Layer::System + "https://dev.api.example.com", Layer::System ); #else ConfigStore::instance().set( KeyView{.group = "api", .key = "endpoint"}, - "https://api.example.com", - Layer::System + "https://api.example.com", Layer::System ); #endif -// 用户可以覆盖(User 层优先级更高) -ConfigStore::instance().set( - KeyView{.group = "api", .key = "endpoint"}, - "https://custom.api.com", - Layer::User -); -```text - -#### 方法 2:自定义路径提供者 - -> **注**: 以下示例展示如何为自定义应用实现路径提供者。CFDesktop 默认实现使用应用自管理目录,不读取 `/etc/` 系统路径。 - -```cpp -#include -#include - +// 方式 2:自定义 IConfigStorePathProvider class EnvironmentPathProvider : public IConfigStorePathProvider { public: QString system_path() const override { -#ifdef DEBUG - return QString::fromStdString("/etc/myapp/dev/system.ini"); -#else - return QString::fromStdString("/etc/myapp/prod/system.ini"); -#endif + return QString::fromStdString("/etc/myapp/system.ini"); } - QString user_dir() const override { return QString::fromStdString(std::string(getenv("HOME")) + "/.config/myapp"); } - - QString user_filename() const override { - return "user.ini"; - } - + QString user_filename() const override { return "user.ini"; } QString app_dir() const override { return QCoreApplication::applicationDirPath() + "/config"; } - - QString app_filename() const override { - return "app.ini"; - } - - bool is_layer_enabled(int layer_index) const override { - return true; // 启用所有层级 - } + QString app_filename() const override { return "app.ini"; } + bool is_layer_enabled(int) const override { return true; } }; - -// 初始化时使用 ConfigStore::instance().initialize(std::make_shared()); -```text - -#### 方法 3:使用环境变量 - -> **注**: 以下为自定义应用的示例。CFDesktop 默认实现使用应用自管理目录。 - -```cpp -#include -#include - -// 从环境变量读取配置路径 -const char* env_path = getenv("MYAPP_CONFIG_PATH"); -std::string config_dir = env_path ? env_path : "/etc/myapp"; - -class DynamicPathProvider : public IConfigStorePathProvider { - QString base_dir_; -public: - DynamicPathProvider(const QString& dir) : base_dir_(dir) {} - - QString system_path() const override { - return base_dir_ + "/system.ini"; - } - - QString user_dir() const override { - return QString::fromStdString(std::string(getenv("HOME")) + "/.config/myapp"); - } - - QString user_filename() const override { - return "user.ini"; - } - - QString app_dir() const override { - return QCoreApplication::applicationDirPath() + "/config"; - } - - QString app_filename() const override { - return "app.ini"; - } - - bool is_layer_enabled(int layer_index) const override { - return true; - } -}; - -// 使用 -ConfigStore::instance().initialize( - std::make_shared(QString::fromStdString(config_dir)) -); -```yaml +``` --- ### Q3: Watcher 回调没有被触发? -#### 原因 1:使用了 Manual 通知策略但未调用 notify() +最常见原因是键模式不匹配。使用通配符确保匹配目标键: ```cpp -// 问题代码 -ConfigStore::instance().watch( - "app.theme", - [](const Key& k, auto old, auto new_v, Layer layer) { - std::cout << "Theme changed" << std::endl; - }, - NotifyPolicy::Manual // 手动模式 -); +// 精确匹配 — 只匹配 "app.theme.name" +ConfigStore::instance().watch("app.theme.name", callback); -ConfigStore::instance().set(KeyView{.group = "app", .key = "theme"}, "dark"); -// 回调不会被触发 +// 通配符 — 匹配所有 theme 相关键 +ConfigStore::instance().watch("*.theme.*", callback); -// 解决方案:手动触发通知 -ConfigStore::instance().set(KeyView{.group = "app", .key = "theme"}, "dark", Layer::App, NotifyPolicy::Manual); -ConfigStore::instance().notify(); // 触发所有 Manual Watcher -```text +// 通配符 — 匹配 app 下所有键 +ConfigStore::instance().watch("app.*", callback); +``` -#### 原因 2:键模式不匹配 +如果使用了 `NotifyPolicy::Manual`,需手动调用 `notify()` 才会触发回调: ```cpp -// 问题代码 -ConfigStore::instance().watch( - "app.theme.name", // 精确匹配 - callback -); - -ConfigStore::instance().set(KeyView{.group = "other", .key = "theme"}, "dark"); -// 修改的是 "other.theme",不会触发 "app.theme.name" - -// 解决方案:使用通配符 -ConfigStore::instance().watch( - "*.theme.*", // 匹配所有 theme 相关键 - callback -); - -ConfigStore::instance().watch( - "app.*", // 匹配 app 下的所有键 - callback +ConfigStore::instance().set( + KeyView{.group = "app", .key = "theme"}, "dark", + Layer::App, NotifyPolicy::Manual ); -```text +ConfigStore::instance().notify(); // 手动触发所有 Manual Watcher +``` -#### 原因 3:Watcher 被提前取消 +确保 `WatcherHandle` 在需要监听的整个生命周期内保持有效: ```cpp -// 问题代码 -{ - WatcherHandle handle = ConfigStore::instance().watch("app.*", callback); - ConfigStore::instance().unwatch(handle); // 立即取消 - // 后续修改不会触发 -} - -// 解决方案:保持 handle 有效 class AppManager { WatcherHandle theme_watcher_; public: void init() { theme_watcher_ = ConfigStore::instance().watch( - "app.theme.*", - [this](const Key& k, auto old, auto new_v, Layer layer) { + "app.theme.*", [this](const Key& k, auto old, auto new_v, Layer layer) { onThemeChanged(k); } ); } - - ~AppManager() { - ConfigStore::instance().unwatch(theme_watcher_); - } + ~AppManager() { ConfigStore::instance().unwatch(theme_watcher_); } }; -```yaml +``` --- ### Q4: 类型转换失败怎么办? -#### 问题:读取的类型与存储的类型不匹配 +存储和读取的类型必须与 QVariant 兼容。如果以 `std::string` 存储,读取为 `int` 会失败: ```cpp -// 问题场景 -ConfigStore::instance().set(KeyView{.group = "test", .key = "value"}, std::string("123")); - -// 尝试读取为 int,但字符串无法直接转换 -auto result = ConfigStore::instance().query(KeyView{.group = "test", .key = "value"}, 0); -// 可能返回默认值 0 -```text +// 正确做法:存储时使用目标类型 +ConfigStore::instance().set(KeyView{.group = "test", .key = "value"}, 123); // 存为 int +int value = ConfigStore::instance().query( + KeyView{.group = "test", .key = "value"}, 0 +); // 返回 123 +``` -#### 解决方案 1:先读取字符串再转换 +如果类型不确定,先读取为字符串再手动转换: ```cpp auto str_value = ConfigStore::instance().query( KeyView{.group = "test", .key = "value"}, "" ); - if (!str_value.empty()) { try { int int_value = std::stoi(str_value); - // 使用 int_value } catch (const std::exception& e) { // 处理转换错误 } } -```text - -#### 解决方案 2:使用 QVariant 兼容的类型 - -```cpp -// 存储时使用 QVariant 能正确转换的类型 -ConfigStore::instance().set(KeyView{.group = "test", .key = "value"}, 123); // 存为 int - -// 读取时可以直接获取 -int value = ConfigStore::instance().query( - KeyView{.group = "test", .key = "value"}, 0 -); // 正确返回 123 -```text - -#### 解决方案 3:使用 std::any 处理多种类型 - -```cpp -std::any value = get_raw_value(KeyView{.group = "test", .key = "value"}); - -if (value.type() == typeid(int)) { - int int_value = std::any_cast(value); -} else if (value.type() == typeid(std::string)) { - std::string str_value = std::any_cast(value); - // 手动转换... -} -```yaml +``` --- ### Q5: 如何迁移旧的配置文件? -#### 方法 1:直接复制 INI 文件 - -```bash -# Linux -cp /old/path/config.ini ~/.config/myapp/user.ini - -# Windows -copy C:\OldPath\config.ini %APPDATA%\MyApp\user.ini - -# macOS -cp /old/path/config.plist ~/Library/Preferences/com.myapp.plist -```text - -#### 方法 2:使用 ConfigStore API 迁移 +使用 `QSettings` 读取旧配置,通过 ConfigStore API 写入新格式: ```cpp void migrate_old_config(const std::string& old_file_path) { QSettings old_settings( - QString::fromStdString(old_file_path), - QSettings::IniFormat + QString::fromStdString(old_file_path), QSettings::IniFormat ); - - // 遍历所有旧配置 for (const auto& group : old_settings.childGroups()) { old_settings.beginGroup(group); - for (const auto& key : old_settings.childKeys()) { QVariant value = old_settings.value(key); - - // 构造 KeyView - KeyView kv{ - .group = group.toStdString(), - .key = key.toStdString() - }; - - // 根据类型写入新配置 - if (value.type() == QVariant::Int) { + KeyView kv{.group = group.toStdString(), .key = key.toStdString()}; + if (value.type() == QVariant::Int) ConfigStore::instance().set(kv, value.toInt(), Layer::User); - } else if (value.type() == QVariant::String) { + else if (value.type() == QVariant::String) ConfigStore::instance().set(kv, value.toString().toStdString(), Layer::User); - } else if (value.type() == QVariant::Bool) { + else if (value.type() == QVariant::Bool) ConfigStore::instance().set(kv, value.toBool(), Layer::User); - } - // 其他类型... } - - old_settings.endGroup(); - } - - // 同步到磁盘 - ConfigStore::instance().sync(SyncMethod::Sync); -} -```text - -#### 方法 3:映射旧键名到新键名 - -```cpp -struct KeyMapping { - std::string old_group; - std::string old_key; - std::string new_group; - std::string new_key; -}; - -std::vector mappings = { - {"ui", "theme", "app.theme", "name"}, - {"ui", "dpi", "display", "dpi"}, - {"network", "server", "api", "endpoint"} -}; - -void migrate_with_mapping(const std::string& old_file) { - QSettings old_settings(QString::fromStdString(old_file), QSettings::IniFormat); - - for (const auto& mapping : mappings) { - old_settings.beginGroup(QString::fromStdString(mapping.old_group)); - QVariant value = old_settings.value(QString::fromStdString(mapping.old_key)); old_settings.endGroup(); - - if (!value.isNull()) { - KeyView new_kv{ - .group = mapping.new_group, - .key = mapping.new_key - }; - - // 根据类型写入 - if (value.type() == QVariant::String) { - ConfigStore::instance().set( - new_kv, - value.toString().toStdString(), - Layer::User - ); - } - // 其他类型... - } } - ConfigStore::instance().sync(SyncMethod::Sync); } -```yaml +``` --- ### Q6: Temp 层的数据什么时候会丢失? -#### 丢失场景 - -```cpp -// 1. 程序退出后(Temp 层不持久化) -ConfigStore::instance().set(KeyView{.group = "session", .key = "id"}, "abc123", Layer::Temp); -// 程序重启后,此数据丢失 - -// 2. 调用 reload() 后 -ConfigStore::instance().set(KeyView{.group = "temp", .key = "data"}, "value", Layer::Temp); -ConfigStore::instance().reload(); // Temp 层被清空 -// 之前的数据丢失 - -// 3. 调用 clear_layer(Layer::Temp) -ConfigStore::instance().clear_layer(Layer::Temp); -// Temp 层数据被清空 -```text - -#### 保留场景 +Temp 层为纯内存存储,在以下场景会丢失:程序退出、调用 `reload()`、调用 `clear_layer(Layer::Temp)`。`sync()` 不会清空 Temp 层。 -```cpp -// Temp 层数据在程序运行期间一直有效 -ConfigStore::instance().set(KeyView{.group = "cache", .key = "key"}, "value", Layer::Temp); - -// 程序运行期间可以正常读取 -auto value = ConfigStore::instance().query( - KeyView{.group = "cache", .key = "key"}, "" -); // 返回 "value" - -// sync() 不会清空 Temp 层 -ConfigStore::instance().sync(SyncMethod::Sync); -// Temp 层数据仍然存在 -```text - -#### 最佳实践 +适用场景:会话临时数据、调试配置、运行时缓存。需要持久化的数据请使用 User 或 App 层。 ```cpp -// Temp 层适合存储: -// 1. 会话临时数据 -ConfigStore::instance().set( - KeyView{.group = "session", .key = "id"}, - generate_session_id(), - Layer::Temp -); - -// 2. 测试/调试配置 -#ifdef DEBUG - ConfigStore::instance().set( - KeyView{.group = "debug", .key = "verbose"}, - true, - Layer::Temp - ); -#endif - -// 3. 运行时计算的缓存值 +// 会话数据 — 使用 Temp ConfigStore::instance().set( - KeyView{.group = "cache", .key = "computed_value"}, - expensive_computation(), - Layer::Temp + KeyView{.group = "session", .key = "id"}, generate_session_id(), Layer::Temp ); -// 需要持久化的数据,不要使用 Temp 层 +// 用户偏好 — 使用 User ConfigStore::instance().set( - KeyView{.group = "user", .key = "preference"}, - "dark", - Layer::User // 使用 User 或 App 层 + KeyView{.group = "user", .key = "preference"}, "dark", Layer::User ); -```yaml +``` --- ### Q7: 如何批量修改配置而不触发多次 Watcher? -#### 方法 1:使用 Manual 通知策略 +使用 `NotifyPolicy::Manual`,批量修改完成后一次性触发: ```cpp -// 问题代码:每次修改都触发 Watcher -ConfigStore::instance().set(KeyView{.group = "batch", .key = "a"}, 1); -// Watcher 被触发 -ConfigStore::instance().set(KeyView{.group = "batch", .key = "b"}, 2); -// Watcher 被触发 -ConfigStore::instance().set(KeyView{.group = "batch", .key = "c"}, 3); -// Watcher 被触发 - -// 解决方案:使用 Manual 策略 ConfigStore::instance().set( - KeyView{.group = "batch", .key = "a"}, 1, - Layer::App, - NotifyPolicy::Manual // 不立即触发 + KeyView{.group = "batch", .key = "a"}, 1, Layer::App, NotifyPolicy::Manual ); ConfigStore::instance().set( - KeyView{.group = "batch", .key = "b"}, 2, - Layer::App, - NotifyPolicy::Manual + KeyView{.group = "batch", .key = "b"}, 2, Layer::App, NotifyPolicy::Manual ); ConfigStore::instance().set( - KeyView{.group = "batch", .key = "c"}, 3, - Layer::App, - NotifyPolicy::Manual + KeyView{.group = "batch", .key = "c"}, 3, Layer::App, NotifyPolicy::Manual ); +ConfigStore::instance().notify(); // 一次性触发所有 Watcher +``` -// 批量修改完成后,一次性触发 -ConfigStore::instance().notify(); // 所有 Watcher 被触发一次 -```text - -#### 方法 2:使用事务模式 +也可封装为 RAII 事务: ```cpp -class ConfigTransaction { -public: - ConfigTransaction() { - // 开始事务:暂存 Watcher 状态 - } - +struct ConfigTransaction { ~ConfigTransaction() { - // 提交事务:触发 notify() ConfigStore::instance().notify(); ConfigStore::instance().sync(SyncMethod::Async); } - template void set(const KeyView& key, const T& value, Layer layer = Layer::App) { ConfigStore::instance().set(key, value, layer, NotifyPolicy::Manual); @@ -566,42 +239,17 @@ public: // 使用 { - ConfigTransaction transaction; - - transaction.set(KeyView{.group = "ui", .key = "width"}, 800); - transaction.set(KeyView{.group = "ui", .key = "height"}, 600); - transaction.set(KeyView{.group = "ui", .key = "theme"}, "dark"); - - // 事务结束,自动触发通知 -} -```text - -#### 方法 3:先取消 Watcher,批量修改后再添加 - -```cpp -// 保存 Watcher -auto callback = [](const Key& k, auto old, auto new_v, Layer layer) { - // 处理变更 -}; - -// 批量修改前取消监听 -ConfigStore::instance().unwatch(watcher_handle); - -// 批量修改 -ConfigStore::instance().set(KeyView{.group = "batch", .key = "a"}, 1); -ConfigStore::instance().set(KeyView{.group = "batch", .key = "b"}, 2); -ConfigStore::instance().set(KeyView{.group = "batch", .key = "c"}, 3); - -// 重新添加 Watcher -watcher_handle = ConfigStore::instance().watch("batch.*", callback); -```bash + ConfigTransaction tx; + tx.set(KeyView{.group = "ui", .key = "width"}, 800); + tx.set(KeyView{.group = "ui", .key = "height"}, 600); + tx.set(KeyView{.group = "ui", .key = "theme"}, "dark"); +} // 析构时自动 notify + sync +``` --- ### Q8: 配置文件位置在哪里? -#### 默认路径 - | 平台 | 层级 | 路径 | |------|------|------| | **Linux** | System | `<应用目录>/config/system.ini` | @@ -614,925 +262,58 @@ watcher_handle = ConfigStore::instance().watch("batch.*", callback); | | User | `~/Library/Preferences/com.cfdesktop.user.plist` | | | App | `<应用目录>/config/app.ini` | -#### 获取当前路径 - -```cpp -// 方法 1:通过自定义路径提供者获取 -#include -#include -#include - -class DebugPathProvider : public IConfigStorePathProvider { -public: - QString system_path() const override { - QString path = QCoreApplication::applicationDirPath() + "/config/system.ini"; - std::cout << "System path: " << path.toStdString() << std::endl; - return path; - } - - QString user_dir() const override { - QString path = QString::fromStdString(std::string(getenv("HOME")) + "/.config/cfdesktop"); - std::cout << "User dir: " << path.toStdString() << std::endl; - return path; - } - - QString user_filename() const override { - return "user.ini"; - } - - QString app_dir() const override { - QString path = QCoreApplication::applicationDirPath() + "/config"; - std::cout << "App dir: " << path.toStdString() << std::endl; - return path; - } - - QString app_filename() const override { - return "app.ini"; - } - - bool is_layer_enabled(int layer_index) const override { - return true; - } -}; - -// 初始化 -ConfigStore::instance().initialize(std::make_shared()); - -// 方法 2:直接检查文件是否存在 -void check_config_files() { - std::array paths = { - QCoreApplication::applicationDirPath().toStdString() + "/config/system.ini", - std::getenv("HOME") + "/.config/cfdesktop/user.ini"s, - QCoreApplication::applicationDirPath().toStdString() + "/config/app.ini" - }; - - for (const auto& path : paths) { - QFileInfo file(QString::fromStdString(path)); - if (file.exists()) { - std::cout << "Found: " << path << std::endl; - std::cout << " Size: " << file.size() << " bytes" << std::endl; - std::cout << " Modified: " << file.lastModified().toString().toStdString() << std::endl; - } else { - std::cout << "Not found: " << path << std::endl; - } - } -} -```yaml - ---- - -## 故障排查 - -### 问题诊断流程 - -```bash - 配置读取异常 - | - +---------------+---------------+ - | | - 返回默认值? 返回错误值? - | | - ↓ | - 键不存在? | - | | - +---+---+ | - | | - 是 否 | - | | | - ↓ ↓ | -检查键名 检查层级 | - | | | - | +-------+---------------+ - | | - | 检查文件 - | | - | +---+---+ - | | | - | 文件 权限 - | 存在 问题 - | | | - +-----------+-------+ - | - 解决问题 -```text - -### 详细诊断步骤 - -#### 步骤 1:确认键是否存在 - -```cpp -void diagnose_key(const KeyView& kv) { - std::cout << "诊断键: " << kv.group << "." << kv.key << std::endl; - - // 检查键是否有效 - KeyHelper helper; - Key k; - if (!helper.fromKeyViewToKey(kv, k)) { - std::cout << " [错误] 键名无效,包含非法字符" << std::endl; - std::cout << " 提示: 键名只能包含字母、数字、下划线和点号" << std::endl; - return; - } - std::cout << " [OK] 完整键名: " << k.full_key << std::endl; - - // 检查各层是否存在 - std::array layers = {Layer::System, Layer::User, Layer::App, Layer::Temp}; - std::array layer_names = {"System", "User", "App", "Temp"}; - - for (size_t i = 0; i < layers.size(); ++i) { - bool exists = ConfigStore::instance().has_key(kv, layers[i]); - std::cout << " [" << (exists ? "X" : " ") << "] " << layer_names[i] << " 层"; - - if (exists) { - auto value = ConfigStore::instance().query( - kv, layers[i], "" - ); - std::cout << " = \"" << value << "\""; - } - std::cout << std::endl; - } - - // 检查优先级查询结果 - auto final_value = ConfigStore::instance().query(kv, ""); - std::cout << " 最终值: \"" << final_value << "\" (优先级查询)" << std::endl; -} - -// 使用 -diagnose_key(KeyView{.group = "app.theme", .key = "name"}); -```text - -#### 步骤 2:检查配置文件 - -```cpp -void diagnose_config_files() { - std::cout << "\n=== 配置文件诊断 ===" << std::endl; - - std::array, 3> files = { - {Layer::System, QCoreApplication::applicationDirPath().toStdString() + "/config/system.ini"}, - {Layer::User, std::getenv("HOME") + "/.config/cfdesktop/user.ini"s}, - {Layer::App, QCoreApplication::applicationDirPath().toStdString() + "/config/app.ini"} - }; - - for (const auto& [layer, path] : files) { - std::cout << "\n[" << (layer == Layer::System ? "System" : - layer == Layer::User ? "User" : "App") << "] " << path << std::endl; - - QFileInfo file(QString::fromStdString(path)); - - // 检查文件是否存在 - if (!file.exists()) { - std::cout << " [警告] 文件不存在" << std::endl; - continue; - } - std::cout << " [OK] 文件存在" << std::endl; - - // 检查文件权限 - if (!file.isReadable()) { - std::cout << " [错误] 文件不可读" << std::endl; - } else { - std::cout << " [OK] 文件可读" << std::endl; - } - - if (!file.isWritable()) { - std::cout << " [警告] 文件不可写(可能无法保存配置)" << std::endl; - } else { - std::cout << " [OK] 文件可写" << std::endl; - } - - // 尝试打开文件 - QSettings settings(QString::fromStdString(path), QSettings::IniFormat); - if (settings.status() != QSettings::NoError) { - std::cout << " [错误] QSettings 错误: " - << (settings.status() == QSettings::AccessError ? "访问错误" : "格式错误") - << std::endl; - } else { - std::cout << " [OK] 文件格式正确" << std::endl; - } - - // 显示文件大小和修改时间 - std::cout << " 大小: " << file.size() << " bytes" << std::endl; - std::cout << " 修改时间: " << file.lastModified().toString().toStdString() << std::endl; - } -} -```text - -#### 步骤 3:检查 Watcher 状态 - -```cpp -void diagnose_watchers() { - std::cout << "\n=== Watcher 诊断 ===" << std::endl; - - // 添加测试 Watcher - bool triggered = false; - auto handle = ConfigStore::instance().watch( - "diagnostic.test", - [&triggered](const Key& k, auto old, auto new_v, Layer layer) { - triggered = true; - std::cout << " [OK] Watcher 被触发" << std::endl; - std::cout << " 键: " << k.full_key << std::endl; - std::cout << " 层: " << static_cast(layer) << std::endl; - }, - NotifyPolicy::Immediate - ); - - std::cout << "测试 Watcher: " << handle << std::endl; - - // 触发测试 - std::cout << "触发配置变更..." << std::endl; - ConfigStore::instance().set( - KeyView{.group = "diagnostic", .key = "test"}, - std::string("test_value"), - Layer::Temp - ); - - if (!triggered) { - std::cout << " [错误] Watcher 未被触发" << std::endl; - std::cout << " 可能原因:" << std::endl; - std::cout << " 1. Watcher 回调中抛出异常" << std::endl; - std::cout << " 2. 键名不匹配" << std::endl; - std::cout << " 3. 通知策略设置错误" << std::endl; - } - - // 清理 - ConfigStore::instance().unwatch(handle); - ConfigStore::instance().clear_layer(Layer::Temp); -} -```text - -### 日志输出分析方法 - -#### 启用详细日志 - -```cpp -// 在调试模式下,包装 ConfigStore 调用以输出日志 -class LoggingConfigStore { -public: - template - static T query(const KeyView& key, const T& default_value, Layer layer = Layer::System) { - std::cout << "[ConfigStore::query] " << key.group << "." << key.key - << " (layer: " << static_cast(layer) << ")" << std::endl; - - T value = ConfigStore::instance().query(key, layer, default_value); - - std::cout << " -> returned: " << value << std::endl; - return value; - } - - template - static bool set(const KeyView& key, const T& value, Layer layer = Layer::App) { - std::cout << "[ConfigStore::set] " << key.group << "." << key.key - << " = " << value - << " (layer: " << static_cast(layer) << ")" << std::endl; - - bool result = ConfigStore::instance().set(key, value, layer); - - std::cout << " -> result: " << (result ? "success" : "failed") << std::endl; - return result; - } -}; - -// 使用 -auto theme = LoggingConfigStore::query( - KeyView{.group = "app", .key = "theme"}, - "default", - Layer::User -); -```text - -#### 检查 pending_changes - -```cpp -void monitor_pending_changes() { - size_t before = ConfigStore::instance().pending_changes(); - std::cout << "待写入变更数: " << before << std::endl; - - // 执行一些操作 - ConfigStore::instance().set(KeyView{.group = "test", .key = "a"}, 1, Layer::App, NotifyPolicy::Manual); - ConfigStore::instance().set(KeyView{.group = "test", .key = "b"}, 2, Layer::App, NotifyPolicy::Manual); - - size_t after = ConfigStore::instance().pending_changes(); - std::cout << "待写入变更数: " << after << " (新增 " << (after - before) << ")" << std::endl; - - // 调用 notify - ConfigStore::instance().notify(); - size_t after_notify = ConfigStore::instance().pending_changes(); - std::cout << "notify 后待写入变更数: " << after_notify << std::endl; -} -```text - -### 调试技巧 - -#### 技巧 1:导出所有配置 +KeyView 中 group 和 key 只允许字母、数字、下划线,包含空格等非法字符会导致 `set()` 返回 false: ```cpp -void dump_all_config() { - std::cout << "\n=== 配置转储 ===" << std::endl; - - // 导出 Temp 层 - std::cout << "\n[Temp 层]" << std::endl; - dump_layer(Layer::Temp); - - // 导出 App 层 - std::cout << "\n[App 层]" << std::endl; - dump_layer(Layer::App); - - // 导出 User 层 - std::cout << "\n[User 层]" << std::endl; - dump_layer(Layer::User); - - // 导出 System 层 - std::cout << "\n[System 层]" << std::endl; - dump_layer(Layer::System); -} - -void dump_layer(Layer layer) { - // 由于 ConfigStore 没有遍历 API,这里需要通过 QSettings 直接读取 - std::string path = get_layer_path(layer); - QSettings settings(QString::fromStdString(path), QSettings::IniFormat); - - dump_group(settings, "", 0); -} - -void dump_group(QSettings& settings, const QString& group, int indent) { - if (!group.isEmpty()) { - settings.beginGroup(group); - } - - QString prefix = QString(indent, ' '); - - // 输出所有键值对 - for (const auto& key : settings.childKeys()) { - QVariant value = settings.value(key); - std::cout << prefix.toStdString() - << key.toStdString() - << " = " - << value.toString().toStdString() - << " (" - << value.typeName().toStdString() - << ")" - << std::endl; - } - - // 递归输出子组 - for (const auto& child : settings.childGroups()) { - std::cout << prefix.toStdString() << "[" << child.toStdString() << "]" << std::endl; - dump_group(settings, child, indent + 2); - } - - if (!group.isEmpty()) { - settings.endGroup(); - } -} -```text - -#### 技巧 2:验证类型转换 - -```cpp -void test_type_conversion(const KeyView& kv) { - std::cout << "\n类型转换测试: " << kv.group << "." << kv.key << std::endl; - - // 尝试不同类型的读取 - auto as_string = ConfigStore::instance().query(kv, ""); - auto as_int = ConfigStore::instance().query(kv, 0); - auto as_double = ConfigStore::instance().query(kv, 0.0); - auto as_bool = ConfigStore::instance().query(kv, false); - - std::cout << " as string: \"" << as_string << "\"" << std::endl; - std::cout << " as int: " << as_int << std::endl; - std::cout << " as double: " << as_double << std::endl; - std::cout << " as bool: " << (as_bool ? "true" : "false") << std::endl; -} -```text - -#### 技巧 3:Watcher 性能分析 - -```cpp -class PerformanceWatcher { -public: - PerformanceWatcher(const std::string& pattern) - : pattern_(pattern) - , call_count_(0) - , total_time_(0) - , max_time_(0) - {} - - WatcherHandle install() { - return ConfigStore::instance().watch( - pattern_, - [this](const Key& k, auto old, auto new_v, Layer layer) { - auto start = std::chrono::high_resolution_clock::now(); - - // 实际回调逻辑 - this->actual_callback(k, old, new_v, layer); - - auto end = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast(end - start); - - call_count_++; - total_time_ += duration.count(); - max_time_ = std::max(max_time_, duration.count()); - } - ); - } - - void print_stats() { - std::cout << "\nWatcher 性能统计: " << pattern_ << std::endl; - std::cout << " 调用次数: " << call_count_ << std::endl; - if (call_count_ > 0) { - std::cout << " 平均耗时: " << (total_time_ / call_count_) << " μs" << std::endl; - std::cout << " 最大耗时: " << max_time_ << " μs" << std::endl; - std::cout << " 总耗时: " << total_time_ << " μs" << std::endl; - } - } - -private: - void actual_callback(const Key& k, const std::any* old, const std::any* new_v, Layer layer) { - // 实际的处理逻辑 - std::cout << "Config changed: " << k.full_key << std::endl; - } - - std::string pattern_; - size_t call_count_; - uint64_t total_time_; - uint64_t max_time_; -}; - -// 使用 -auto watcher = std::make_unique("app.*"); -auto handle = watcher->install(); - -// ... 运行一段时间 ... - -watcher->print_stats(); -```yaml +KeyView invalid_kv{.group = "app theme", .key = "name"}; // 失败 +KeyView valid_kv{.group = "app_theme", .key = "name"}; // 成功 +``` --- -## 平台特定问题 - -### Windows - -#### 问题 1:注册表路径限制 - -Windows 版本使用注册表存储配置,路径长度有限制。 - -```cpp -// 问题:深层嵌套的键名可能超过注册表路径限制 -ConfigStore::instance().set( - KeyView{.group = "a.very.long.group.name.that.exceeds.registry.limit", .key = "key"}, - "value" -); // 可能失败 - -// 解决方案:使用扁平化的键名 -ConfigStore::instance().set( - KeyView{.group = "app_short", .key = "long_group_key"}, - "value" -); -```text - -#### 问题 2:注册表权限 - -某些注册表路径需要管理员权限。 - -```cpp -#include - -// 检查是否有写入权限 -bool check_registry_access() { - HKEY hKey; - LPCSTR subKey = "Software\\CFDesktop"; - - // 尝试打开注册表键 - LONG result = RegOpenKeyExA( - HKEY_LOCAL_MACHINE, // System 层使用 HKLM - subKey, - 0, - KEY_WRITE, - &hKey - ); - - if (result == ERROR_ACCESS_DENIED) { - std::cerr << "错误: 需要管理员权限写入 System 层配置" << std::endl; - std::cerr << "建议: 使用 User 层或以管理员身份运行" << std::endl; - return false; - } - - if (result == ERROR_SUCCESS) { - RegCloseKey(hKey); - return true; - } - - return false; -} -```text - -#### 问题 3:INI 格式与注册表格式差异 - -```cpp -// 如果想在 Windows 上使用 INI 文件而不是注册表 -#include -#include - -class WindowsIniPathProvider : public IConfigStorePathProvider { -public: - QString system_path() const override { - // 返回 INI 文件路径而不是注册表路径 - return "C:/ProgramData/CFDesktop/system.ini"; - } - - QString user_dir() const override { - char* appdata = nullptr; - size_t len = 0; - _dupenv_s(&appdata, &len, "APPDATA"); - QString path = QString::fromStdString(std::string(appdata)) + "/CFDesktop"; - free(appdata); - return path; - } - - QString user_filename() const override { - return "user.ini"; - } - - QString app_dir() const override { - return QCoreApplication::applicationDirPath() + "/config"; - } - - QString app_filename() const override { - return "app.ini"; - } - - bool is_layer_enabled(int layer_index) const override { - return true; - } -}; - -// QSettings 会根据路径扩展名自动选择格式 -// .ini -> INI 格式 -// 无扩展名 -> 注册表(Windows) -```text +### Q9: KeyView 合法字符规则是什么? -### Linux - -#### 问题 1:INI 文件权限 - -```cpp -// 确保 INI 文件目录存在且可写 -void ensure_config_directory(const std::string& path) { - QFileInfo file(QString::fromStdString(path)); - QDir dir = file.absoluteDir(); - - if (!dir.exists()) { - std::cout << "创建配置目录: " << dir.absolutePath().toStdString() << std::endl; - if (!dir.mkpath(".")) { - std::cerr << "错误: 无法创建配置目录" << std::endl; - std::cerr << "请检查父目录权限" << std::endl; - } - } - - // 检查写入权限 - QFileInfo dirInfo(dir.absolutePath()); - if (!dirInfo.isWritable()) { - std::cerr << "错误: 配置目录不可写" << std::endl; - std::cerr << "路径: " << dir.absolutePath().toStdString() << std::endl; - } -} - -// 使用 -ensure_config_directory("~/.config/cfdesktop/user.ini"); -```text - -#### 问题 2:System 层路径不可写 - -CFDesktop 使用应用自管理目录,System 层配置位于应用目录下(如 `{app_dir}/system.ini`),无需 root 权限。 - -```bash -# 配置文件位于应用目录下,应用启动时自动创建 -# 例如: <应用目录>/config/system.ini -# 如果目录不存在,手动创建: -mkdir -p <应用目录>/config -```text - -#### 问题 3:INI 文件编码 - -```cpp -// 确保使用 UTF-8 编码 -QSettings settings("/path/to/config.ini", QSettings::IniFormat); -settings.setIniCodec("UTF-8"); // 设置编码 - -// 写入包含非 ASCII 字符的配置 -ConfigStore::instance().set( - KeyView{.group = "app", .key = "title"}, - u8"应用程序标题", // UTF-8 字符串字面量 - Layer::User -); -```text - -### macOS - -#### 问题 1:plist vs INI - -macOS 原生使用 plist 格式,但 ConfigStore 统一使用 INI 格式。 - -```cpp -// 如果需要与系统 plist 配置交互,可以手动转换 -void import_from_plist(const std::string& plist_path) { - QSettings plist(QString::fromStdString(plist_path), QSettings::NativeFormat); - - // 读取所有键值 - for (const auto& key : plist.allKeys()) { - QVariant value = plist.value(key); - - // 转换键名格式 - std::string full_key = key.toStdString(); - std::replace(full_key.begin(), full_key.end(), '/', '.'); - - // 分离 group 和 key - size_t pos = full_key.rfind('.'); - std::string group = (pos != std::string::npos) ? full_key.substr(0, pos) : ""; - std::string key_name = (pos != std::string::npos) ? full_key.substr(pos + 1) : full_key; - - // 写入 ConfigStore - KeyView kv{.group = group, .key = key_name}; - ConfigStore::instance().set(kv, value.toString().toStdString(), Layer::User); - } -} -```text - -#### 问题 2:macOS 特殊目录 - -```cpp -#include -#include - -class MacOSPathProvider : public IConfigStorePathProvider { -public: - QString system_path() const override { - // macOS 系统配置 - return "/Library/Preferences/com.cfdesktop.system.ini"; - } - - QString user_dir() const override { - // 用户配置目录 - char* home = getenv("HOME"); - return QString::fromStdString(std::string(home)) + "/Library/Preferences"; - } - - QString user_filename() const override { - return "com.cfdesktop.user.ini"; - } - - QString app_dir() const override { - // 应用内配置(在 Bundle 内) - return QCoreApplication::applicationDirPath() + "/../Resources/config"; - } - - QString app_filename() const override { - return "app.ini"; - } - - bool is_layer_enabled(int layer_index) const override { - return true; - } -}; -```yaml +`group` 和 `key` 字段仅允许字母、数字、下划线和点号。包含空格或其他特殊字符会导致键转换失败,`set()` 返回 `false`。 --- -## 性能问题 - -### 内存占用过高 - -#### 原因分析 - -1. **缓存无限增长**:大量配置项被缓存 -2. **Watcher 泄漏**:未正确取消的 Watcher 占用内存 -3. **大量临时配置**:Temp 层积累过多数据 +### Q10: 如何减少 sync() 的性能开销? -#### 解决方案 +避免每次写入后立即同步。批量写入后调用一次异步同步: ```cpp -#include -#include - -// 方案 1:定期清理 Temp 层 -void cleanup_temp_layer() { - std::cout << "清理 Temp 层..." << std::endl; - ConfigStore::instance().clear_layer(Layer::Temp); -} - -// 方案 2:监控内存使用 -void monitor_memory_usage() { - // 使用系统 API 获取内存使用情况 -#ifdef __linux__ - std::ifstream file("/proc/self/status"); - std::string line; - while (std::getline(file, line)) { - if (line.find("VmRSS") != std::string::npos) { - std::cout << line << std::endl; - } - } -#endif -} - -// 方案 3:限制缓存大小(需要修改 ConfigStore 实现) -// 在 ConfigStoreImpl 中添加 LRU 缓存 -```text - -### 读写缓慢 - -#### 诊断 - -```cpp -#include - -void benchmark_config_operations() { - const int iterations = 1000; - - // 写入测试 - auto write_start = std::chrono::high_resolution_clock::now(); - for (int i = 0; i < iterations; ++i) { - ConfigStore::instance().set( - KeyView{.group = "bench", .key = "key_" + std::to_string(i)}, - i, - Layer::Temp - ); - } - auto write_end = std::chrono::high_resolution_clock::now(); - auto write_duration = std::chrono::duration_cast(write_end - write_start); - - std::cout << "写入 " << iterations << " 次: " - << write_duration.count() << " μs" - << " (平均 " << (write_duration.count() / iterations) << " μs/次)" - << std::endl; - - // 读取测试 - auto read_start = std::chrono::high_resolution_clock::now(); - for (int i = 0; i < iterations; ++i) { - ConfigStore::instance().query( - KeyView{.group = "bench", .key = "key_" + std::to_string(i)}, - 0 - ); - } - auto read_end = std::chrono::high_resolution_clock::now(); - auto read_duration = std::chrono::duration_cast(read_end - read_start); - - std::cout << "读取 " << iterations << " 次: " - << read_duration.count() << " μs" - << " (平均 " << (read_duration.count() / iterations) << " μs/次)" - << std::endl; - - // 清理 - ConfigStore::instance().clear_layer(Layer::Temp); -} -```text - -#### 优化建议 - -1. **减少 sync() 调用频率** -```cpp -// 不推荐:每次写入都同步 +// 不推荐:频繁同步 I/O for (int i = 0; i < 1000; ++i) { ConfigStore::instance().set(KeyView{.group = "data", .key = std::to_string(i)}, i); - ConfigStore::instance().sync(SyncMethod::Sync); // 频繁 I/O + ConfigStore::instance().sync(SyncMethod::Sync); } -// 推荐:批量写入后同步 +// 推荐:批量写入 + 一次异步同步 for (int i = 0; i < 1000; ++i) { - ConfigStore::instance().set(KeyView{.group = "data", .key = std::to_string(i)}, i, Layer::App, NotifyPolicy::Manual); -} -ConfigStore::instance().notify(); -ConfigStore::instance().sync(SyncMethod::Async); // 异步同步 -```text - -2. **使用 Temp 层存储频繁变化的配置** -```cpp -// 频繁更新的计数器,使用 Temp 层避免频繁 I/O -for (int i = 0; i < 10000; ++i) { ConfigStore::instance().set( - KeyView{.group = "counter", .key = "value"}, - i, - Layer::Temp // 不写入磁盘 + KeyView{.group = "data", .key = std::to_string(i)}, i, + Layer::App, NotifyPolicy::Manual ); } +ConfigStore::instance().notify(); +ConfigStore::instance().sync(SyncMethod::Async); +``` -// 需要持久化时再写入 -ConfigStore::instance().set( - KeyView{.group = "counter", .key = "final_value"}, - 10000, - Layer::App -); -ConfigStore::instance().sync(SyncMethod::Sync); -```text - -### Watcher 性能问题 - -#### 问题:Watcher 回调执行时间过长 - -```cpp -// 问题代码 -ConfigStore::instance().watch( - "app.*", - [](const Key& k, auto old, auto new_v, Layer layer) { - // 耗时操作 - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - // 这会阻塞所有后续的配置操作 - } -); -```text - -#### 解决方案 - -```cpp -// 方案 1:使用异步处理 -ConfigStore::instance().watch( - "app.*", - [](const Key& k, auto old, auto new_v, Layer layer) { - // 将处理任务放入队列,异步执行 - std::thread([k]() { - // 在另一个线程中处理 - handle_config_change(k); - }).detach(); - } -); - -// 方案 2:使用 Debounce(防抖) -class DebouncedWatcher { - std::string pattern_; - std::chrono::milliseconds delay_; - std::thread thread_; - std::queue queue_; - std::mutex mutex_; - std::condition_variable cv_; - bool running_ = true; - -public: - DebouncedWatcher(const std::string& pattern, std::chrono::milliseconds delay) - : pattern_(pattern), delay_(delay) - { - thread_ = std::thread([this]() { - while (running_) { - std::unique_lock lock(mutex_); - cv_.wait(lock, [this]() { return !queue_.empty() || !running_; }); - - if (!running_) break; - - if (!queue_.empty()) { - auto key = queue_.front(); - queue_.pop(); - - // 等待延迟 - lock.unlock(); - std::this_thread::sleep_for(delay_); - lock.lock(); - - // 检查是否有新元素 - if (queue_.empty()) { - // 没有新元素,执行处理 - handle_change(key); - } - } - } - }); - } - - ~DebouncedWatcher() { - running_ = false; - cv_.notify_all(); - if (thread_.joinable()) { - thread_.join(); - } - } - - WatcherHandle install() { - return ConfigStore::instance().watch( - pattern_, - [this](const Key& k, auto old, auto new_v, Layer layer) { - std::lock_guard lock(mutex_); - queue_.push(k); - cv_.notify_one(); - } - ); - } - -private: - void handle_change(const Key& k) { - std::cout << "处理配置变更: " << k.full_key << std::endl; - // 实际处理逻辑 - } -}; - -// 使用 -auto debounced_watcher = std::make_unique("app.*", std::chrono::milliseconds(100)); -debounced_watcher->install(); -```yaml +频繁变化的临时数据使用 Temp 层(不写磁盘),仅在需要持久化时写入 App/User 层。 --- -## 获取帮助 - -如果以上方法无法解决问题: +## 平台特定问题 -1. 参考 [快速入门](./01-quick-start.md) -2. 搜索或提交 Issue 到项目仓库 +| 平台 | 问题 | 原因 | 解决方案 | +|------|------|------|----------| +| Windows | 写入 System 层失败 | HKLM 需要管理员权限 | 使用 User 层或以管理员身份运行 | +| Windows | 深层嵌套键名写入失败 | 注册表路径长度限制 | 使用扁平化的 group/key 命名 | +| Windows | 想使用 INI 而非注册表 | QSettings 默认使用注册表 | 实现 `IConfigStorePathProvider`,返回 `.ini` 路径 | +| Linux | 配置目录不存在 | 首次运行未创建目录 | 应用启动时 `QDir::mkpath()` 确保目录存在 | +| Linux | INI 文件中文乱码 | 文件编码非 UTF-8 | `QSettings::setIniCodec("UTF-8")` | +| Linux | System 层写入失败 | CFDesktop 使用应用自管理目录 | 确保应用目录有写权限,无需 root | +| macOS | 想读取系统 plist | ConfigStore 统一使用 INI 格式 | 用 `QSettings(path, NativeFormat)` 读取后手动导入 | --- diff --git a/document/HandBook/desktop/base/config_manager/README.md b/document/HandBook/desktop/base/config_manager/README.md deleted file mode 100644 index 17f18c323..000000000 --- a/document/HandBook/desktop/base/config_manager/README.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: "ConfigStore - 配置管理中心" -description: ConfigStore 是 CFDesktop 桌面框架的配置管理中心,提供分层存储、结构化键名管理 ---- - -# ConfigStore - 配置管理中心 - -## 简介 - -ConfigStore 是 CFDesktop 桌面框架的配置管理中心,提供分层存储、结构化键名管理和变更监听机制。它基于 Qt 的 QSettings 实现,采用 INI 格式存储配置,支持跨平台使用。 - -## 核心特性 - -### 四层存储架构 - -ConfigStore 采用四层优先级架构,支持配置的自然覆盖: - -| 层级 | 优先级 | 读写权限 | 路径 (Linux) | 用途 | -|------|--------|----------|---------------|------| -| **Temp** | 最高 (3) | 读写 | 内存 | 临时配置,测试用,不持久化 | -| **App** | 高 (2) | 读写 | `/config/app.ini` | 应用运行时配置 | -| **User** | 中 (1) | 读写 | `~/.config/cfdesktop/user.ini` | 用户个人配置 | -| **System** | 低 (0) | 读写 | `{app_dir}/system.ini` | 系统默认配置 (CFDesktop 自管理目录) | - -查询时按优先级从高到低查找,写入时默认写入 App 层。 - -### 类型安全 - -通过模板 API 提供类型安全的配置访问: - -```cpp -// 查询配置 -int width = ConfigStore::instance().query( - KeyView{.group = "ui", .key = "width"}, 800); - -// 设置配置 -ConfigStore::instance().set( - KeyView{.group = "ui", .key = "width"}, 1024); -```text - -支持的基础类型: -- `int` / `unsigned` / `long` 等整数类型 -- `double` / `float` 浮点类型 -- `bool` 布尔类型 -- `std::string` 字符串类型 - -### 变更监听 - -Watcher 系统支持通配符模式匹配的变更监听: - -```cpp -// 监听所有 ui.* 的变更 -auto handle = ConfigStore::instance().watch( - "ui.*", - [](const Key& k, const std::any* old, const std::any* new_v, Layer layer) { - std::cout << "UI 配置变更: " << k.full_key << std::endl; - } -); -```text - -支持两种通知策略: -- **Immediate**: 每次变更立即触发 Watcher -- **Manual**: 需要手动调用 `notify()` 触发 - -### 线程安全 - -ConfigStore 采用读写锁设计,确保多线程环境下的安全访问: - -- **读操作**: 使用 `shared_lock`,支持多线程并发读取 -- **写操作**: 使用 `unique_lock`,独占访问 -- **延迟回调**: Watcher 回调在无锁状态下执行,避免死锁 - -### 高性能 - -- **内存缓存**: 热点数据缓存在内存中,减少 I/O -- **异步持久化**: 配置写入操作异步执行,不阻塞调用方 -- **脏标记**: 只持久化实际修改的层,减少 I/O 量 - -## 快速示例 - -```cpp -#include "cfconfig.hpp" - -using namespace cf::config; - -int main() { - // 获取单例实例 - auto& config = ConfigStore::instance(); - - // 读取配置(使用默认值) - std::string theme = config.query( - KeyView{.group = "app", .key = "theme"}, "default"); - - // 写入配置(默认写入 App 层) - config.set(KeyView{.group = "app", .key = "theme"}, std::string("dark")); - - // 监听变更 - config.watch("app.*", [](const Key& k, auto old, auto new_v, Layer layer) { - std::cout << "配置变更: " << k.full_key << std::endl; - }); - - // 同步到磁盘 - config.sync(SyncMethod::Async); - - return 0; -} -```bash - -## 文档导航 - -| 文档 | 描述 | -|------|------| -| [快速入门](./01-quick-start.md) | 从零开始使用 ConfigStore | -| [最佳实践](./02-best-practices.md) | 推荐的使用模式和设计建议 | -| [常见问题](./03-faq.md) | 问题诊断和故障排查 | -| [架构详解](./04-architecture.md) | 深入理解内部实现和设计决策 | - -## 代码示例 - -运行示例: - -```bash -cd build/example/desktop/base/config_manager -./example_usage -```text - -## 依赖模块 - -- **Qt 6.8+**: 提供 QSettings 后端存储 -- **cfbase**: 提供 SimpleSingleton 基类 - -## 相关链接 - -- **API 参考手册**: [document/desktop/base/config_manager/](../../../../../document/desktop/base/config_manager/) diff --git a/document/HandBook/desktop/base/config_manager/index.md b/document/HandBook/desktop/base/config_manager/index.md index e4bcf7851..901b30c3a 100644 --- a/document/HandBook/desktop/base/config_manager/index.md +++ b/document/HandBook/desktop/base/config_manager/index.md @@ -1,11 +1,63 @@ --- title: ConfigStore 手册 -description: ConfigStore 是 CFDesktop 的四层优先级配置管理系统,本手册详细介绍其存储层级( +description: ConfigStore 是 CFDesktop 的四层优先级配置管理系统,支持分层存储、结构化键名管理和变更监听机制。 --- # ConfigStore 手册 -ConfigStore 是 CFDesktop 的四层优先级配置管理系统,本手册详细介绍其存储层级(Temp / App / User / System)的使用方式、JSON 配置文件格式、热加载机制以及自定义配置项的扩展方法。 +ConfigStore 是 CFDesktop 桌面框架的配置管理中心,提供分层存储、结构化键名管理和变更监听机制。基于 Qt 的 QSettings 实现,采用 INI 格式存储配置,支持跨平台使用。 + +## 四层存储架构 + +ConfigStore 采用四层优先级架构,支持配置的自然覆盖: + +| 层级 | 优先级 | 读写权限 | 路径 (Linux) | 用途 | +|------|--------|----------|---------------|------| +| **Temp** | 最高 (3) | 读写 | 内存 | 临时配置,测试用,不持久化 | +| **App** | 高 (2) | 读写 | `/config/app.ini` | 应用运行时配置 | +| **User** | 中 (1) | 读写 | `~/.config/cfdesktop/user.ini` | 用户个人配置 | +| **System** | 低 (0) | 读写 | `{app_dir}/system.ini` | 系统默认配置 (CFDesktop 自管理目录) | + +查询时按优先级从高到低查找,写入时默认写入 App 层。 + +## 快速示例 + +```cpp +#include "cfconfig.hpp" + +using namespace cf::config; + +int main() { + // 获取单例实例 + auto& config = ConfigStore::instance(); + + // 读取配置(使用默认值) + std::string theme = config.query( + KeyView{.group = "app", .key = "theme"}, "default"); + + // 写入配置(默认写入 App 层) + config.set(KeyView{.group = "app", .key = "theme"}, std::string("dark")); + + // 监听变更 + config.watch("app.*", [](const Key& k, auto old, auto new_v, Layer layer) { + std::cout << "配置变更: " << k.full_key << std::endl; + }); + + // 同步到磁盘 + config.sync(SyncMethod::Async); + + return 0; +} +``` + +## 文档导航 + +| 文档 | 描述 | +|------|------| +| [快速入门](./01-quick-start.md) | 从零开始使用 ConfigStore | +| [最佳实践](./02-best-practices.md) | 推荐的使用模式和设计建议 | +| [常见问题](./03-faq.md) | 问题诊断和故障排查 | +| [架构详解](./04-architecture.md) | 深入理解内部实现和设计决策 | --- diff --git a/document/ci/README.md b/document/ci/README.md deleted file mode 100644 index 72cc40a0c..000000000 --- a/document/ci/README.md +++ /dev/null @@ -1,179 +0,0 @@ ---- -title: CFDesktop CI/CD 架构文档 -description: CFDesktop 采用多架构 Docker 容器构建方案,确保代码在不同平台上都能正确编译和运行。 ---- - -# CFDesktop CI/CD 架构文档 - -## 概述 - -CFDesktop 采用多架构 Docker 容器构建方案,确保代码在不同平台上都能正确编译和运行。 - -### 核心特性 - -- **利用现有机制**: 通过 `check_toolchain.cmake` 实现工具链选择 -- **多架构支持**: 使用 Docker `--platform` 参数支持 AMD64 和 ARM64 原生编译 -- **本地验证**: 通过 Docker 在本地进行多架构构建测试 -- **独立配置**: CI 构建与本地开发构建使用独立的配置和目录 - -## 系统架构 - -```text -开发者本地 Docker 容器构建 - │ │ - │ 修改代码 │ - │ │ │ - │ 本地测试 │ - │ │ │ - ├──────┴───────────────────────────────>│ - │ │ │ - │ │ ┌────┴────┐ - │ │ │ 选择架构 │ - │ │ └────┬────┘ - │ │ │ - │ │ ┌─────┴─────┐ - │ │ │ 启动容器 │ - │ │ └─────┬─────┘ - │ │ │ - │ │ ┌───────────┼───────────┐ - │ │ │ │ │ - │ │ ┌────▼───┐ ┌───▼────┐ ┌──▼─────┐ - │ │ │ AMD64 │ │ ARM64 │ │ ARM32 │ - │ │ │ Docker │ │ Docker │ │ Docker │ - │ │ │ + Test │ │ + Test │ │ + Test │ - │ │ └────────┘ └────────┘ └────────┘ - │ │ │ │ │ - │ │ └───────────┼───────────┘ - │ │ │ - │ │ 构建成功? - │ │ │ - │ │ ┌─────▼─────┐ - │ │ │ 验证产物 │ - │ │ └───────────┘ - │ │ - │ 继续开发 ✓ - │ - └──> 代码就绪 -```text - -## 设计理念 - -### 1. 工具链选择机制 - -项目使用 [`check_toolchain.cmake`](../../cmake/check_toolchain.cmake) 实现工具链自动选择: - -```text -配置: toolchain=linux/ci-x86_64 - ↓ -CMake: -DUSE_TOOLCHAIN=linux/ci-x86_64 - ↓ -自动解析: cmake/cmake_toolchain/linux/ci-x86_64-toolchain.cmake - ↓ -设置: CMAKE_TOOLCHAIN_FILE -```bash - -### 2. 分离的架构特定工具链 - -| 架构 | 工具链文件 | Qt 路径 | 配置文件 | -|------|-----------|---------|---------| -| AMD64/x86_64 | ci-x86_64-toolchain.cmake | /opt/Qt/6.8.1/gcc_64 | build_ci_config.ini | -| ARM64/aarch64 | ci-aarch64-toolchain.cmake | /opt/Qt/6.8.1/gcc_arm64 | build_ci_aarch64_config.ini | -| ARM32/armhf | ci-armhf-toolchain.cmake | /opt/Qt/6.8.1/gcc_armhf | build_ci_armhf_config.ini | - -### 3. 多架构 Docker 原生编译 - -| 对比项 | 交叉编译方案 | 多架构容器方案(本方案) | -|--------|---------------|------------------------| -| ARM 测试 | ❌ 无法在 x86_64 主机运行 | ✅ 在 ARM64 容器中运行 | -| 工具链复杂度 | ❌ 需要交叉编译工具链 | ✅ 使用原生工具链 | -| 测试真实性 | ⚠️ 无法验证 ARM 产物 | ✅ 真实 ARM 环境测试 | -| 配置复杂度 | ⚠️ 需要配置 sysroot 等 | ✅ 使用相同的工具链文件 | - -## 实施阶段 - -CI/CD 流水线分为 5 个实施阶段,当前已完成前 3 个阶段: - -| 阶段 | 名称 | 说明 | 状态 | -|------|------|------|------| -| Phase 1 | [CI 工具链设置](toolchain-setup.md) | 创建 CI 专用工具链文件 | ✅ 已完成 | -| Phase 2 | [Docker 构建环境](docker-environment.md) | 创建多架构 Dockerfile | ✅ 已完成 | -| Phase 3 | [CI 构建入口](ci-build-entry.md) | 创建统一构建脚本 | ✅ 已完成 | -| Phase 4 | GitHub Actions | 配置自动化工作流 | ⏭️ 跳过(不在远端构建) | -| Phase 5 | 异步合并机制 | 实现 pre-push hook | ⏳ 待实施 | - -## 文档导航 - -### Phase 1: CI 工具链设置 -- **[CI 工具链设置指南](toolchain-setup.md)** - 工具链文件设计和使用说明 - -### Phase 2: Docker 构建环境 -- **[Docker 构建环境设置指南](docker-environment.md)** - Docker 镜像和使用说明 - -### Phase 3: CI 构建入口 -- **[CI 构建入口设置指南](ci-build-entry.md)** - 构建脚本和配置说明 - -## 相关文件 - -### 工具链文件 - -| 文件路径 | 说明 | -|----------|------| -| [cmake/check_toolchain.cmake](../../cmake/check_toolchain.cmake) | 工具链选择机制 | -| [cmake/cmake_toolchain/linux/ci-x86_64-toolchain.cmake](../../cmake/cmake_toolchain/linux/ci-x86_64-toolchain.cmake) | AMD64 CI 工具链 | -| [cmake/cmake_toolchain/linux/ci-aarch64-toolchain.cmake](../../cmake/cmake_toolchain/linux/ci-aarch64-toolchain.cmake) | ARM64 CI 工具链 | -| [cmake/cmake_toolchain/linux/ci-armhf-toolchain.cmake](../../cmake/cmake_toolchain/linux/ci-armhf-toolchain.cmake) | ARM32 CI 工具链 (IMX6ULL) | - -### 配置文件 - -| 文件路径 | 说明 | -|----------|------| -| [scripts/build_helpers/build_ci_config.ini](../../scripts/build_helpers/build_ci_config.ini) | AMD64 CI 配置 | -| [scripts/build_helpers/build_ci_aarch64_config.ini](../../scripts/build_helpers/build_ci_aarch64_config.ini) | ARM64 CI 配置 | -| [scripts/build_helpers/build_ci_armhf_config.ini](../../scripts/build_helpers/build_ci_armhf_config.ini) | ARM32 CI 配置 | - -### 脚本文件 - -| 文件路径 | 说明 | -|----------|------| -| [scripts/build_helpers/ci_build_entry.sh](../../scripts/build_helpers/ci_build_entry.sh) | CI 构建入口脚本 | -| [scripts/build_helpers/docker_start.sh](../../scripts/build_helpers/docker_start.sh) | Linux Docker 启动脚本 | -| [scripts/build_helpers/docker_start.ps1](../../scripts/build_helpers/docker_start.ps1) | Windows Docker 启动脚本 | -| [scripts/dependency/install_build_dependencies.sh](../../scripts/dependency/install_build_dependencies.sh) | 依赖安装脚本 | - -### Docker 文件 - -| 文件路径 | 说明 | -|----------|------| -| [scripts/docker/Dockerfile.build](../../scripts/docker/Dockerfile.build) | 多架构 Docker 镜像定义 | -| [scripts/docker/docker-compose.yml](../../scripts/docker/docker-compose.yml) | Docker Compose 配置 | -| [scripts/docker/.dockerignore](../../scripts/docker/.dockerignore) | Docker 忽略规则 | - -## 快速开始 - -### Docker 快速验证 - -```bash -# AMD64 构建 -bash scripts/build_helpers/docker_start.sh --verify - -# ARM64 构建 -bash scripts/build_helpers/docker_start.sh --arch arm64 --verify -```text - -### 直接运行构建 - -```bash -# 在 Linux 环境中直接运行 -bash scripts/build_helpers/ci_build_entry.sh ci -```text - -### 查看完整文档 - -```bash -# 查看完整的流水线设计文档 -cat PIPELINE.md -```yaml - ---- - -*文档版本: v2.0 | 最后更新: 2026-03-07* diff --git a/document/ci/index.md b/document/ci/index.md index 8834f9d2e..a78540d75 100644 --- a/document/ci/index.md +++ b/document/ci/index.md @@ -1,11 +1,48 @@ --- title: CI/CD -description: CFDesktop 当前使用 GitHub Actions 分层验证。 +description: CFDesktop 当前使用 GitHub Actions 分层验证,并支持多架构 Docker 容器构建。 --- # CI/CD -CFDesktop 当前使用 GitHub Actions 分层验证。 +CFDesktop 采用多架构 Docker 容器构建方案,通过 `check_toolchain.cmake` 实现工具链选择,确保代码在 AMD64 / ARM64 / ARM32 平台上正确编译和运行。 + +## CI 架构 + +### 工具链与平台 + +项目为每种目标架构提供独立的工具链文件和构建配置: + +| 架构 | 工具链文件 | Qt 路径 | +|------|-----------|---------| +| AMD64/x86_64 | `ci-x86_64-toolchain.cmake` | `/opt/Qt/6.8.1/gcc_64` | +| ARM64/aarch64 | `ci-aarch64-toolchain.cmake` | `/opt/Qt/6.8.1/gcc_arm64` | +| ARM32/armhf | `ci-armhf-toolchain.cmake` | `/opt/Qt/6.8.1/gcc_armhf` | + +多架构容器方案使用 Docker `--platform` 参数实现原生编译,无需交叉编译工具链,可在对应架构容器中直接运行测试。 + +### Docker 快速验证 + +```bash +# AMD64 构建 +bash scripts/build_helpers/docker_start.sh --verify + +# ARM64 构建 +bash scripts/build_helpers/docker_start.sh --arch arm64 --verify + +# 直接运行 CI 构建 +bash scripts/build_helpers/ci_build_entry.sh ci +``` + +## 实施阶段 + +| 阶段 | 名称 | 说明 | 状态 | +|------|------|------|------| +| Phase 1 | [CI 工具链设置](toolchain-setup.md) | 创建 CI 专用工具链文件 | ✅ 已完成 | +| Phase 2 | [Docker 构建环境](docker-environment.md) | 创建多架构 Dockerfile | ✅ 已完成 | +| Phase 3 | [CI 构建入口](ci-build-entry.md) | 创建统一构建脚本 | ✅ 已完成 | +| Phase 4 | GitHub Actions | 配置自动化工作流 | ⏭️ 跳过 | +| Phase 5 | 异步合并机制 | 实现 pre-push hook | ⏳ 待实施 | ## 工作流 @@ -21,3 +58,7 @@ CFDesktop 当前使用 GitHub Actions 分层验证。 - 文档相关 PR: 打 `build-doc` 标签触发文档构建。 - `develop -> main`: 必须通过 C++ build/test 和 docs build。 - `main`: 合入后发布文档站。 + +--- + +*最后更新: 2026-05-23* diff --git a/document/design_stage/00_phase0_project_skeleton.md b/document/design_stage/00_phase0_project_skeleton.md index 2b730dfe5..8cc8ec330 100644 --- a/document/design_stage/00_phase0_project_skeleton.md +++ b/document/design_stage/00_phase0_project_skeleton.md @@ -1,534 +1,28 @@ --- title: "Phase 0: 工程骨架搭建详细设计文档" -description: "- \"xaver.clang-tidy\"" +description: "项目骨架:三层目录结构、CMake 构建系统、CI/CD 流水线的设计意图" --- -# Phase 0: 工程骨架搭建详细设计文档 +# Phase 0: 工程骨架搭建 -- 设计意图 -## 文档信息 -| 项目 | 内容 | -|------|------| -| 文档版本 | v1.0 | -| 创建日期 | 2026-02-20 | -| 阶段代号 | Phase 0 - 工程骨架 | -| 预计周期 | 1~2 周 | +## 为什么选择这种方案 ---- - -## 一、阶段目标 - -### 1.1 核心目标 -建立项目的工程化基础,确保后续所有开发工作在稳固的架构上进行。 - -### 1.2 具体交付物 -- [ ] 完整的 CMake 构建体系 -- [ ] 三层模块目录结构(base/sdk/shell) -- [ ] 跨平台 CI/CD 流水线 -- [ ] 开发环境配置指南 -- [ ] 代码规范与格式化配置 - ---- - -## 三、CMake 构建系统设计 - -### 3.1 主 CMakeLists.txt 结构 - -```cmake -cmake_minimum_required(VERSION 3.24) -project(CFDesktop - VERSION 0.1.0 - DESCRIPTION "CFDesktop - Qt-based Embedded Desktop System" - LANGUAGES CXX -) - -# ==================== 基础配置 ==================== -set(CMAKE_CXX_STANDARD 23) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) - -# 导出编译命令(用于 IDE 支持) -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - -# ==================== 依赖项 ==================== -find_package(Qt6 REQUIRED COMPONENTS - Core - Gui - Widgets - Multimedia - Network -) - -# ==================== 子目录 ==================== -add_subdirectory(cmake) -add_subdirectory(src/base) -add_subdirectory(src/sdk) -add_subdirectory(src/shell) -add_subdirectory(src/simulator) -add_subdirectory(tests) - -# ==================== 打包配置 ==================== -include(CPack) -```text - -### 3.2 Base 库 CMakeLists.txt - -```cmake -project(CFDesktopBase) - -# ==================== 源文件 ==================== -set(BASE_PUBLIC_HEADERS - include/CFDesktop/Base/HardwareProbe/HardwareProbe> - include/CFDesktop/Base/HardwareProbe/HWTier> - include/CFDesktop/Base/ThemeEngine/ThemeEngine> - include/CFDesktop/Base/AnimationManager/AnimationManager> - include/CFDesktop/Base/DPIManager/DPIManager> - include/CFDesktop/Base/ConfigStore/ConfigStore> - include/CFDesktop/Base/Logger/Logger> -) - -set(BASE_SOURCES - src/hardware/HardwareProbe.cpp - src/theme/ThemeEngine.cpp - src/animation/AnimationManager.cpp - src/dpi/DPIManager.cpp - src/config/ConfigStore.cpp - src/logging/Logger.cpp -) - -# ==================== 库目标 ==================== -add_library(CFDesktopBase STATIC - ${BASE_PUBLIC_HEADERS} - ${BASE_SOURCES} -) - -# ==================== 编译选项 ==================== -target_compile_features(CFDesktopBase PUBLIC cxx_std_23) -target_include_directories(CFDesktopBase PUBLIC - $ - $ -) - -target_link_libraries(CFDesktopBase PUBLIC - Qt6::Core - Qt6::Gui -) - -# ==================== 平台特定配置 ==================== -if(UNIX AND NOT APPLE) - target_link_libraries(CFDesktopBase PUBLIC pthread dl) -endif() - -# ==================== 安装规则 ==================== -install(TARGETS CFFesktopBase - ARCHIVE DESTINATION lib - LIBRARY DESTINATION lib -) - -install(FILES ${BASE_PUBLIC_HEADERS} - DESTINATION include/CFDesktop/Base -) -```text - -### 3.3 SDK 库 CMakeLists.txt - -```cmake -project(CFDesktopSDK) - -# 依赖 Base 库 -target_link_libraries(CFDesktopSDK PUBLIC CFDesktopBase) - -# 导出 CMake 配置 -include(CMakePackageConfigHelpers) -write_basic_package_version_file( - CFDesktopSDKConfigVersion.cmake - VERSION ${PROJECT_VERSION} - COMPATIBILITY SameMajorVersion -) - -install(FILES - cmake/CFDesktopSDKConfig.cmake - cmake/CFDesktopSDKConfigVersion.cmake - DESTINATION lib/cmake/CFDesktopSDK -) -```text - -### 3.4 Shell 主程序 CMakeLists.txt - -```cmake -project(CFDesktopShell) - -add_executable(cfdesktop-shell - src/main.cpp - # ... 其他源文件 -) - -target_link_libraries(cfdesktop-shell PRIVATE - CFDesktopSDK - Qt6::Widgets - Qt6::Multimedia -) - -install(TARGETS cfdesktop-shell - RUNTIME DESTINATION bin -) -```text - -### 3.5 交叉编译工具链配置 - -#### 3.5.1 ARMv7 (IMX6ULL) 工具链 - -```cmake -# cmake/toolchains/arm-linux-gnueabihf.cmake - -set(CMAKE_SYSTEM_NAME Linux) -set(CMAKE_SYSTEM_PROCESSOR arm) - -set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) -set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++) - -set(CMAKE_FIND_ROOT_PATH /opt/arm-sfm-toolchain) -set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) -set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) -set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) - -set(Qt6_DIR /opt/qt6-arm/lib/cmake/Qt6) -```text - -#### 3.5.2 ARM64 (RK3568/RK3588) 工具链 - -```cmake -# cmake/toolchains/aarch64-linux-gnu.cmake - -set(CMAKE_SYSTEM_NAME Linux) -set(CMAKE_SYSTEM_PROCESSOR aarch64) - -set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) -set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++) - -set(CMAKE_FIND_ROOT_PATH /opt/arm-hf-toolchain) -set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) -set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) -set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) - -set(Qt6_DIR /opt/qt6-arm64/lib/cmake/Qt6) -```text - -### 3.6 CMakePresets.json - -```json -{ - "version": 3, - "cmakeMinimumRequired": { - "major": 3, - "minor": 24, - "patch": 0 - }, - "configurePresets": [ - { - "name": "linux-default", - "displayName": "Linux x64", - "generator": "Ninja", - "binaryDir": "${sourceDir}/build/linux-default", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug", - "BUILD_TESTING": "ON" - } - }, - { - "name": "linux-arm-sf", - "displayName": "ARMv7 Software Rendering", - "generator": "Ninja", - "binaryDir": "${sourceDir}/build/linux-arm-sf", - "toolchainFile": "${sourceDir}/cmake/toolchains/arm-linux-gnueabihf.cmake", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", - "QT_PLATFORM": "linuxfb" - } - }, - { - "name": "linux-arm-hf", - "displayName": "ARM64 Hardware Accelerated", - "generator": "Ninja", - "binaryDir": "${sourceDir}/build/linux-arm-hf", - "toolchainFile": "${sourceDir}/cmake/toolchains/aarch64-linux-gnu.cmake", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", - "QT_PLATFORM": "eglfs" - } - } - ], - "buildPresets": [ - { - "name": "linux-default-debug", - "configurePreset": "linux-default", - "configuration": "Debug" - } - ] -} -```yaml - ---- - -## 四、CI/CD 流水线设计 - -### 4.1 主构建流程 (.github/workflows/build.yml) - -```yaml -name: Build Matrix - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main ] - -jobs: - build-linux-x64: - runs-on: ubuntu-latest - container: ghcr.io/cfdesktop/builder-linux-x64:latest +CFDesktop 需要同时支持 x86_64 开发机和 ARM 嵌入式板卡(IMX6ULL/RK3568/RK3588),因此构建系统必须在第一天就具备交叉编译能力。我们选择 CMake 作为构建基础,搭配 Ninja 后端和 CMakePresets.json,让开发者通过一条 preset 命令即可切换目标平台,避免手动维护多套 Makefile。 - steps: - - uses: actions/checkout@v3 +目录结构采用三层单向依赖(`desktop/ -> ui/ -> base/ -> Qt/OS API`),这是整个项目最重要的架构约束。每一层只允许依赖下层,禁止反向引用。这种严格分层确保了 `base/` 可以独立测试和发布,`ui/` 可以在不同桌面环境中复用,而 `desktop/` 作为最终集成层可以自由组合下层能力。 - - name: Configure CMake - run: | - cmake -B build -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_TESTING=ON +CI/CD 采用 GitHub Actions 矩阵构建,在 Linux x64、ARMv7、ARM64 三个目标上并行编译,并集成 clang-tidy 静态分析和 clang-format 格式检查。所有代码质量门禁通过 Git pre-commit hook 在本地前置拦截,减少 CI 反馈周期。 - - name: Build - run: cmake --build build --parallel +## 关键决策 - - name: Run Tests - run: ctest --test-dir build --output-on-failure - - - name: Upload Artifacts - uses: actions/upload-artifact@v3 - with: - name: linux-x64-build - path: build/src/ - - build-arm-sf: - runs-on: ubuntu-latest - container: ghcr.io/cfdesktop/builder-arm-sf:latest - - steps: - - uses: actions/checkout@v3 - - - name: Cross-Compile for ARMv7 - run: | - cmake -B build \ - -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/arm-linux-gnueabihf.cmake \ - -DCMAKE_BUILD_TYPE=Release - - - name: Build - run: cmake --build build --parallel - - - name: Package - run: cpack --config build/CPackConfig.cmake - - - name: Upload Artifacts - uses: actions/upload-artifact@v3 - with: - name: arm-sf-build - path: build/*.tar.gz - - code-quality: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Run clang-tidy - run: | - cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - run-clang-tidy -p build src/ - - - name: Format Check - run: | - ./tools/format.sh --check -```text - -### 4.2 部署流程 (.github/workflows/deploy.yml) - -```yaml -name: Deploy to Device - -on: - workflow_dispatch: - inputs: - target: - description: 'Target device' - required: true - type: choice - options: - - imx6ull-dev - - rk3568-dev - - rk3588-dev - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Download Build Artifacts - uses: actions/download-artifact@v3 - - - name: Deploy via SSH - uses: appleboy/ssh-action@v0.1.5 - with: - host: ${{ secrets.DEVICE_HOST }} - username: ${{ secrets.DEVICE_USER }} - key: ${{ secrets.DEVICE_SSH_KEY }} - script: | - systemctl stop cfdesktop - tar -xzf cfdesktop.tar.gz -C /opt/ - systemctl start cfdesktop -```yaml - ---- - -## 五、开发环境配置 - -### 5.1 VSCode 配置 (.vscode/settings.json) - -```json -{ - "cmake.configureArgs": [ - "-DBUILD_TESTING=ON" - ], - "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", - "files.associations": { - "*.cmake": "cmake", - "CMakeLists.txt": "cmake" - }, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "C_Cpp.clang_format_style": "file", - "clang-format.style.location": ".clang-format" -} -```text - -### 5.2 推荐扩展 - -- `ms-vscode.cmake-tools` -- `twxs.cmake` -- `xaver.clang-format` -- "xaver.clang-tidy" -- `ms-vscode.cpptools` - -### 5.3 代码格式化配置 (.clang-format) - -```yaml ---- -BasedOnStyle: Google -Language: Cpp -Standard: c++23 - -IndentWidth: 4 -TabWidth: 4 -UseTab: Never -ColumnLimit: 100 - -PointerAlignment: Left -ReferenceAlignment: Left - -SortIncludes: true -IncludeBlocks: Regroup -IncludeCategories: - - Regex: '^= 3.24 | 支持 C++23 和 CMakePresets | -| GCC | >= 12.0 | C++23 特性支持 | -| Qt6 | >= 6.5 | 核心依赖 | -| Ninja | >= 1.10 | 构建后端 | -| Docker | >= 24.0 | CI 容器化 | -| clang-format | >= 16.0 | 代码格式化 | -| clang-tidy | >= 16.0 | 静态分析 | - ---- - -## 九、风险与缓解措施 - -| 风险 | 影响 | 缓解措施 | -|------|------|----------| -| Qt6 交叉编译配置复杂 | 延迟 2-3 天 | 使用 Docker 预配置环境 | -| CI 资源不足 | 构建时间长 | 优化依赖缓存,使用矩阵策略 | -| 工具链版本兼容性 | 编译失败 | 固定 Docker 镜像版本 | - ---- +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| CMake + CMakePresets.json | 跨平台成熟、交叉编译原生支持、IDE 集成广泛 | Meson(生态不够成熟)、QMake(Qt6 已弃用) | +| 三层单向依赖 (base/ui/desktop) | 强制解耦、每层可独立测试、支持嵌入式裁剪 | 扁平结构(耦合严重)、四层以上(过度工程化) | +| 共享库输出 (DLL/SO) | 支持运行时替换模块、减少嵌入式 Flash 占用 | 静态库全链接(编译慢、无法按需裁剪) | +| GitHub Actions 矩阵构建 | 免费额度充足、ARM 交叉编译容器支持好 | Jenkins(维护成本高)、GitLab CI(需自建实例) | +| Git pre-commit hook | 质量门禁前置到本地,减少 CI 失败率 | 仅依赖 CI 检查(反馈周期长) | -## 十、下一步行动 +## 当前状态 -完成 Phase 0 后,立即进入 **Phase 1: 硬件探针与能力分级**。 +已实现 -- 三层目录、CMake 构建体系、GitHub Actions CI、代码格式化配置均已完成。详见 `document/design_stage/status/`。 diff --git a/document/design_stage/01_phase1_hardware_probe.md b/document/design_stage/01_phase1_hardware_probe.md index 75f2b6842..6266ef4d2 100644 --- a/document/design_stage/01_phase1_hardware_probe.md +++ b/document/design_stage/01_phase1_hardware_probe.md @@ -1,1074 +1,30 @@ --- title: "Phase 1: 硬件探针与能力分级详细设计文档" -description: "Phase 1: 硬件探针与能力分级详细设计文档 的详细文档" +description: "硬件检测与三级能力分级:评分算法、策略引擎、用户覆盖机制的设计意图" --- -# Phase 1: 硬件探针与能力分级详细设计文档 +# Phase 1: 硬件探针与能力分级 -- 设计意图 -## 文档信息 -| 项目 | 内容 | -|------|------| -| 文档版本 | v1.0 | -| 创建日期 | 2026-02-20 | -| 阶段代号 | Phase 1 - 硬件探针与能力分级 | -| 预计周期 | 2~3 周 | -| 依赖阶段 | Phase 0 | +## 为什么选择这种方案 ---- - -## 一、阶段目标 - -### 1.1 核心目标 -实现开机自动硬件检测,输出能力档位枚举,作为后续所有模块的行为裁剪依据。 - -### 1.2 具体交付物 -- [ ] `HardwareProbe` 模块及其单元测试 -- [ ] `CapabilityPolicy` 策略引擎 -- [ ] `HWTier` 枚举定义及查询接口 -- [ ] ~~设备配置文件 `/etc/CFDesktop/device.conf`~~ ⚠️ **过时**: 实际实现使用 `setDeviceConfigOverride()` API + CFDesktop 自管理目录,不读取系统级路径 -- [ ] Mock 数据集用于单元测试 - ---- - -## 二、模块架构设计 - -### 2.1 整体架构图 - -```text -┌─────────────────────────────────────────────────────────┐ -│ Application Layer │ -│ (使用 HWTier 查询) │ -├─────────────────────────────────────────────────────────┤ -│ CapabilityPolicy (策略引擎) │ -│ ┌────────────────────────────────────────────┐ │ -│ │ getAnimationPolicy() │ │ -│ │ getRenderingBackend() │ │ -│ │ getVideoDecoderPolicy() │ │ -│ │ getMemoryPolicy() │ │ -│ └────────────────────────────────────────────┘ │ -├─────────────────────────────────────────────────────────┤ -│ HardwareProbe (硬件探针) │ -│ ┌────────────────────────────────────────────┐ │ -│ │ detectCPU() │ │ -│ │ detectGPU() │ │ -│ │ detectMemory() │ │ -│ │ detectNetwork() │ │ -│ │ calculateTier() │ │ -│ └────────────────────────────────────────────┘ │ -├─────────────────────────────────────────────────────────┤ -│ Platform Abstraction (平台抽象) │ -│ /proc/cpuinfo /sys/class/ /dev/dri/ evdev │ -└─────────────────────────────────────────────────────────┘ -```text - -### 2.2 文件结构 - -```text -src/base/ -├── include/CFDesktop/Base/HardwareProbe/ -│ ├── HWTier.h # 档位枚举定义 -│ ├── HardwareInfo.h # 硬件信息结构体 -│ ├── HardwareProbe.h # 探针主类 -│ ├── CapabilityPolicy.h # 策略引擎 -│ └── DeviceConfig.h # 配置文件读取 -│ -└── src/hardware/ - ├── HardwareProbe.cpp - ├── CapabilityPolicy.cpp - ├── DeviceConfig.cpp - ├── detectors/ # 各检测器实现 - │ ├── CPUDetector.cpp - │ ├── GPUDetector.cpp - │ ├── MemoryDetector.cpp - │ └── NetworkDetector.cpp - └── platform/ # 平台特定实现 - ├── LinuxDetector.cpp - └── WindowsDetector.cpp -```yaml - ---- - -## 三、数据结构定义 - -### 3.1 HWTier 枚举 - -**文件**: `include/CFDesktop/Base/HardwareProbe/HWTier.h` - -```cpp -#pragma once -#include - -namespace CFDesktop::Base { - -/** - * @brief 硬件能力档位枚举 - * - * 定义三个标准档位,每个档位对应不同的硬件配置和性能等级。 - * 后续所有模块根据此枚举决定行为策略。 - */ -enum class HWTier { - /** - * @brief 低端档位 - * - * 典型硬件:IMX6ULL (528MHz Cortex-A7, 无GPU/软GPU) - * - 禁用所有动画 - * - 使用 linuxfb 渲染后端 - * - 软件视频解码 - * - 限制内存使用 < 64MB - */ - Low, - - /** - * @brief 中端档位 - * - * 典型硬件:RK3568 (4xCortex-A55, Mali-G52) - * - 部分动画(淡入淡出、位移) - * - eglfs 可选 - * - 硬件视频解码(H.264/H.265 部分) - * - 内存使用 < 256MB - */ - Mid, - - /** - * @brief 高端档位 - * - * 典型硬件:RK3588 (8xCortex-A76/A55, Mali-G610) - * - 全动画支持(包括缩放、旋转、复杂效果) - * - 强制 eglfs + OpenGL ES 3.2+ - * - 全格式硬件视频解码 - * - 内存使用 < 1GB - */ - High -}; - -/** - * @brief 获取档位字符串描述 - */ -QString tierToString(HWTier tier); - -/** - * @brief 从字符串解析档位 - */ -HWTier tierFromString(const QString& str); - -} // namespace CFDesktop::Base -```text - -### 3.2 HardwareInfo 结构体 - -**文件**: `include/CFDesktop/Base/HardwareProbe/HardwareInfo.h` - -```cpp -#pragma once -#include -#include -#include -#include "HWTier.h" - -namespace CFDesktop::Base { - -/** - * @brief CPU 信息 - */ -struct CPUInfo { - QString model; // CPU 型号名称 - int cores = 0; // 逻辑核心数 - int frequencyMHz = 0; // 主频 (MHz) - QString architecture; // 架构 (armv7l, aarch64, x86_64) - QStringList features; // CPU 特性 (neon, vfpv4, etc.) -}; - -/** - * @brief GPU 信息 - */ -struct GPUInfo { - QString renderer; // 渲染器名称 - QString vendor; // 供应商 - QString version; // 驱动版本 - bool hasHardwareAcceleration = false; // 是否有硬件加速 - QString driverPath; // 驱动设备路径 (/dev/dri/card0) - int maxTextureSize = 0; // 最大纹理尺寸 - QStringList extensions; // OpenGL 扩展列表 -}; - -/** - * @brief 内存信息 - */ -struct MemoryInfo { - quint64 totalBytes = 0; // 总内存 (字节) - quint64 availableBytes = 0; // 可用内存 (字节) - quint64 totalSwap = 0; // 总交换空间 - quint64 freeSwap = 0; // 可用交换空间 - QString lowMemoryBehavior; // 低内存行为 (oom, compress, etc.) -}; - -/** - * @brief 网络接口信息 - */ -struct NetworkInterface { - QString name; // 接口名称 (eth0, wlan0) - bool isUp = false; // 是否启用 - bool isWireless = false; // 是否无线 - QString macAddress; // MAC 地址 - QStringList ipAddresses; // IP 地址列表 -}; - -/** - * @brief 完整硬件信息 - */ -struct HardwareInfo { - HWTier tier = HWTier::Low; // 计算得出的档位 - - CPUInfo cpu; // CPU 信息 - GPUInfo gpu; // GPU 信息 - MemoryInfo memory; // 内存信息 - QList networkInterfaces; // 网络接口 - - QString deviceTreeCompatible; // 设备树 compatible 字符串 - QString boardName; // 板载名称 - - bool isUserOverridden = false; // 是否用户手动覆盖 -}; - -} // namespace CFDesktop::Base -```text - -### 3.3 CapabilityPolicy 配置结构 - -**文件**: `include/CFDesktop/Base/HardwareProbe/CapabilityPolicy.h` - -```cpp -#pragma once -#include "HWTier.h" -#include - -namespace CFDesktop::Base { - -/** - * @brief 动画策略 - */ -struct AnimationPolicy { - bool enabled = false; // 是否启用动画 - int defaultDurationMs = 0; // 默认动画时长 (毫秒) - int maxConcurrentAnimations = 0; // 最大并发动画数 - bool allowComplexEffects = false; // 是否允许复杂效果 (模糊、阴影等) -}; - -/** - * @brief 渲染后端策略 - */ -struct RenderingPolicy { - QString qtPlatform; // Qt 平台插件 (linuxfb, eglfs, xcb) - bool useOpenGL = false; // 是否使用 OpenGL - QString openGLVersion; // OpenGL 版本要求 - bool useVSync = false; // 是否垂直同步 - int maxFPS = 60; // 最大帧率 -}; - -/** - * @brief 视频解码策略 - */ -struct VideoDecoderPolicy { - bool useHardwareDecoder = false; // 是否使用硬件解码 - QStringList supportedCodecs; // 支持的编解码器 - int maxResolution = 0; // 最大分辨率 (宽) - int maxBitrate = 0; // 最大码率 (bps) - bool allowSimultaneousPlayback = false; // 是否允许同时播放多个 -}; - -/** - * @brief 内存策略 - */ -struct MemoryPolicy { - int maxImageCacheBytes = 0; // 图片缓存大小 - int maxFontCacheBytes = 0; // 字体缓存大小 - bool enableTextureCompression = false; // 是否启用纹理压缩 - int maxWindowSurfaces = 0; // 最大窗口表面数 -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -## 四、类接口设计 - -### 4.1 HardwareProbe 主类 - -**文件**: `include/CFDesktop/Base/HardwareProbe/HardwareProbe.h` - -```cpp -#pragma once -#include "HardwareInfo.h" -#include -#include -#include - -namespace CFDesktop::Base { - -/** - * @brief 硬件探针主类 - * - * 负责检测系统硬件能力,输出 HardwareInfo 结构。 - * 单例模式,进程内唯一实例。 - */ -class HardwareProbe : public QObject { - Q_OBJECT - -public: - /** - * @brief 获取单例实例 - */ - static HardwareProbe* instance(); - - /** - * @brief 执行硬件检测 - * @return 检测结果 - * - * 首次调用会执行完整检测,后续调用返回缓存结果。 - * 可通过 forceRedetect() 强制重新检测。 - */ - HardwareInfo probe(); - - /** - * @brief 强制重新检测 - */ - HardwareInfo forceRedetect(); - - /** - * @brief 获取当前硬件档位 - */ - HWTier currentTier() const; - - /** - * @brief 获取完整硬件信息 - */ - const HardwareInfo& hardwareInfo() const; - - /** - * @brief 设置 Mock 数据(仅用于测试) - */ - void setMockData(const HardwareInfo& mockInfo); - - /** - * @brief 检查特定功能是否可用 - */ - bool hasFeature(const QString& feature) const; - -signals: - /** - * @brief 硬件检测完成信号 - */ - void probeCompleted(const HardwareInfo& info); - - /** - * @brief 档位变化信号(热插拔设备等情况) - */ - void tierChanged(HWTier newTier); - -private: - HardwareProbe(QObject* parent = nullptr); - ~HardwareProbe() = default; - - // 禁用拷贝和移动 - HardwareProbe(const HardwareProbe&) = delete; - HardwareProbe& operator=(const HardwareProbe&) = delete; - - // 内部检测方法 - void detectCPU(HardwareInfo& info); - void detectGPU(HardwareInfo& info); - void detectMemory(HardwareInfo& info); - void detectNetwork(HardwareInfo& info); - void calculateTier(HardwareInfo& info); - - // 平台特定实现 - void probeLinux(HardwareInfo& info); - void probeWindows(HardwareInfo& info); - -private: - static HardwareProbe* s_instance; - HardwareInfo m_cachedInfo; - bool m_isProbed = false; - bool m_useMockData = false; - HardwareInfo m_mockInfo; -}; - -} // namespace CFDesktop::Base -```text - -### 4.2 CapabilityPolicy 策略引擎 - -**文件**: `include/CFDesktop/Base/HardwareProbe/CapabilityPolicy.h` - -```cpp -#pragma once -#include "HWTier.h" -#include - -namespace CFDesktop::Base { - -// 前向声明 -struct AnimationPolicy; -struct RenderingPolicy; -struct VideoDecoderPolicy; -struct MemoryPolicy; - -/** - * @brief 能力策略引擎 - * - * 根据 HWTier 提供各模块的能力配置。 - * 单例模式,根据当前硬件档位返回对应策略。 - */ -class CapabilityPolicy : public QObject { - Q_OBJECT - -public: - static CapabilityPolicy* instance(); - - /** - * @brief 获取动画策略 - */ - AnimationPolicy getAnimationPolicy() const; - - /** - * @brief 获取渲染策略 - */ - RenderingPolicy getRenderingPolicy() const; - - /** - * @brief 获取视频解码策略 - */ - VideoDecoderPolicy getVideoDecoderPolicy() const; - - /** - * @brief 获取内存策略 - */ - MemoryPolicy getMemoryPolicy() const; - - /** - * @brief 获取当前档位 - */ - HWTier currentTier() const; - - /** - * @brief 强制设置档位(用于测试或用户覆盖) - */ - void overrideTier(HWTier tier); - -signals: - void policyChanged(HWTier newTier); - -private: - CapabilityPolicy(QObject* parent = nullptr); - ~CapabilityPolicy() = default; - - // 档位特定策略配置 - AnimationPolicy getAnimationPolicyForLow() const; - AnimationPolicy getAnimationPolicyForMid() const; - AnimationPolicy getAnimationPolicyForHigh() const; - - RenderingPolicy getRenderingPolicyForLow() const; - RenderingPolicy getRenderingPolicyForMid() const; - RenderingPolicy getRenderingPolicyForHigh() const; - - VideoDecoderPolicy getVideoPolicyForLow() const; - VideoDecoderPolicy getVideoPolicyForMid() const; - VideoDecoderPolicy getVideoPolicyForHigh() const; - - MemoryPolicy getMemoryPolicyForLow() const; - MemoryPolicy getMemoryPolicyForMid() const; - MemoryPolicy getMemoryPolicyForHigh() const; - -private: - static CapabilityPolicy* s_instance; - HWTier m_currentTier = HWTier::Low; - bool m_isOverridden = false; -}; - -} // namespace CFDesktop::Base -```text - -### 4.3 DeviceConfig 配置文件 - -**文件**: `include/CFDesktop/Base/HardwareProbe/DeviceConfig.h` - -```cpp -#pragma once -#include "HWTier.h" -#include -#include - -namespace CFDesktop::Base { - -/** - * @brief 设备配置文件读取器 - * - * ⚠️ **过时**: 实际实现使用 `setDeviceConfigOverride()` API,配置在 CFDesktop 自管理目录内。 - * 以下 `/etc/CFDesktop/` 路径仅为早期设计参考。 - * - * 读取 /etc/CFDesktop/device.conf,允许用户手动覆盖硬件检测。 - * - * 配置文件格式: - * [Device] - * Tier=auto|low|mid|high - * CustomScript=/path/to/detection/script - * - * [Overrides] - * EnableAnimations=true|false - * ForceOpenGL=true|false - */ -class DeviceConfig { -public: - struct Config { - bool tierAutoDetect = true; - HWTier forcedTier = HWTier::Low; - QString customScriptPath; - QVariantMap overrides; - }; - - /** - * @brief 加载配置文件 - */ - static Config load(const QString& path = "/etc/CFDesktop/device.conf"); // ⚠️ 过时:实际使用应用内目录 - - /** - * @brief 保存配置文件 - */ - static bool save(const Config& config, - const QString& path = "/etc/CFDesktop/device.conf"); // ⚠️ 过时 - - /** - * @brief 执行自定义检测脚本 - * - * 脚本输出格式: JSON - * { - * "tier": "low|mid|high", - * "properties": {...} - * } - */ - static QVariantMap executeCustomScript(const QString& scriptPath); -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -## 五、检测逻辑详细设计 - -### 5.1 CPU 检测 - -#### 5.1.1 Linux 平台 - -**检测文件**: `src/hardware/detectors/CPUDetector.cpp` - -```cpp -CPUInfo CPUDetector::detectCPU() { - CPUInfo info; +CFDesktop 的目标设备跨度极大——从 528MHz 单核 Cortex-A7 (IMX6ULL) 到 8 核 Cortex-A76 (RK3588),性能差距超过 20 倍。如果用一套渲染和动画策略覆盖所有设备,要么在低端设备上卡顿,要么在高端设备上浪费算力。因此我们在系统启动时执行一次硬件探针,输出三级档位(Low/Mid/High),后续所有模块(主题引擎、动画管理器、渲染后端)根据档位自动裁剪行为。 - // 1. 读取 /proc/cpuinfo - QFile cpuInfo("/proc/cpuinfo"); - if (cpuInfo.open(QIODevice::ReadOnly)) { - QTextStream in(&cpuInfo); - QSet uniqueProcessors; +选择三级而非更细的分级(如五级或连续分数),是因为三级足以覆盖我们的目标硬件矩阵,且每级对应一组明确的能力策略(禁用动画/基础动画/全动画),避免了策略配置的指数膨胀。评分采用加权累加制:CPU 核数和频率、GPU 硬件加速、内存容量各占一定权重,阈值设为 60/120 分两档切分。 - while (!in.atEnd()) { - QString line = in.readLine(); - if (line.startsWith("Hardware")) { - info.model = line.section(':', 1).trimmed(); - } else if (line.startsWith("Processor")) { - uniqueProcessors.insert(line); - } else if (line.startsWith("CPU implementer")) { - info.architecture = detectArchitecture(line); - } else if (line.startsWith("Features")) { - info.features = line.section(':', 1).trimmed().split(' '); - } else if (line.startsWith("BogoMIPS")) { - info.frequencyMHz = static_cast( - line.section(':', 1).trimmed().toDouble() * 2); - } - } - info.cores = uniqueProcessors.size(); - } +检测方法按平台区分:Linux 通过 `/proc/cpuinfo`、`/proc/meminfo`、`/dev/dri/`、`/sys/class/net/` 等 sysfs 路径直接读取;Windows 通过 WMI 查询。GPU 检测额外尝试创建 OpenGL 上下文获取驱动信息和扩展列表。所有检测结果缓存在 `HardwareInfo` 结构体中,避免重复检测。 - // 2. 通过 uname 确认架构 - QProcess uname; - uname.start("uname", QStringList() << "-m"); - if (uname.waitForStarted() && uname.waitForFinished()) { - info.architecture = uname.readAllStandardOutput().trimmed(); - } +用户或系统集成商可通过 `setDeviceConfigOverride()` API 强制覆盖自动检测结果,也可提供自定义检测脚本(输出 JSON)用于扩展硬件识别。这保证了在特殊硬件(如定制板卡)上的灵活性。 - // 3. 读取设备树 (如果存在) - QFile compatible("/sys/firmware/devicetree/base/compatible"); - if (compatible.open(QIODevice::ReadOnly)) { - QByteArray data = compatible.readAll(); - // 设备树字符串以 null 结尾,可能有多项 - QList compatList = data.split('\0'); - if (!compatList.isEmpty() && !compatList.first().isEmpty()) { - info.model = compatList.first(); - } - } +## 关键决策 - return info; -} - -QString CPUDetector::detectArchitecture(const QString& cpuInfoLine) { - // CPU implementer 值映射 - QHash implementerMap = { - {"0x41", "ARM"}, - {"0x42", "Broadcom"}, - {"0x43", "Cavium"}, - {"0x44", "DEC"}, - {"0x49", "Infineon"}, - {"0x4d", "Motorola/Freescale"}, - {"0x4e", "NVIDIA"}, - {"0x50", "APM"}, - {"0x51", "Qualcomm"}, - {"0x56", "Marvell"}, - {"0x69", "Intel"}, - }; - - QString implementer = /* 从 cpuinfo 提取 */; - return implementerMap.value(implementer, "Unknown"); -} -```text - -#### 5.1.2 Windows 平台 - -```cpp -CPUInfo CPUDetector::detectCPUWindows() { - CPUInfo info; - - // 使用 WMI 查询 - QProcess wmic; - wmic.start("wmic", QStringList() - << "cpu" << "get" << "Name,NumberOfCores,MaxClockSpeed"); - // 解析输出... - - return info; -} -```text - -### 5.2 GPU 检测 - -```cpp -GPUInfo GPUDetector::detectGPU() { - GPUInfo info; - - // 1. 检查 DRM 设备 - QDir dri("/dev/dri"); - if (dri.exists()) { - QStringList devices = dri.entryList(QStringList() << "card*", QDir::Files); - if (!devices.isEmpty()) { - info.hasHardwareAcceleration = true; - info.driverPath = "/dev/dri/" + devices.first(); - } - } - - // 2. 尝试创建 OpenGL 上下文 - QOffscreenSurface surface; - QOpenGLContext context; - if (context.create()) { - context.makeCurrent(&surface); - - auto* functions = context.functions(); - info.vendor = reinterpret_cast( - functions->glGetString(GL_VENDOR)); - info.renderer = reinterpret_cast( - functions->glGetString(GL_RENDERER)); - info.version = reinterpret_cast( - functions->glGetString(GL_VERSION)); - - // 获取扩展列表 - auto* extraFunctions = context.extraFunctions(); - GLint numExtensions = 0; - functions->glGetIntegerv(GL_NUM_EXTENSIONS, &numExtensions); - for (int i = 0; i < numExtensions; ++i) { - const char* ext = reinterpret_cast( - functions->glGetStringi(GL_EXTENSIONS, i)); - info.extensions.append(ext); - } - - context.doneCurrent(); - } - - // 3. 检查特定 SoC 的 GPU 型号 - detectSoCGPU(info); - - return info; -} -```text - -### 5.3 内存检测 - -```cpp -MemoryInfo MemoryDetector::detectMemory() { - MemoryInfo info; - - // Linux: 读取 /proc/meminfo - QFile memInfo("/proc/meminfo"); - if (memInfo.open(QIODevice::ReadOnly)) { - QTextStream in(&memInfo); - QHash memData; - - while (!in.atEnd()) { - QString line = in.readLine(); - QString key = line.section(':', 0, 0); - QString value = line.section(':', 1).section(' ', 0, 0); - memData[key] = value.toULongLong() * 1024; // kB to bytes - } - - info.totalBytes = memData["MemTotal"]; - info.availableBytes = memData["MemAvailable"]; - info.totalSwap = memData["SwapTotal"]; - info.freeSwap = memData["SwapFree"]; - } - - return info; -} -```text - -### 5.4 网络检测 - -```cpp -QList NetworkDetector::detectNetwork() { - QList interfaces; - - // 读取 /sys/class/net - QDir netDir("/sys/class/net"); - QStringList devices = netDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - - for (const QString& device : devices) { - NetworkInterface iface; - iface.name = device; - - // 检查是否启用 - QFile operstate(QString("/sys/class/net/%1/operstate").arg(device)); - if (operstate.open(QIODevice::ReadOnly)) { - iface.isUp = operstate.readAll().trimmed() == "up"; - } - - // 检查是否无线 - QDir wireless("/sys/class/net/" + device + "/wireless"); - iface.isWireless = wireless.exists(); - - // 读取 MAC 地址 - QFile address(QString("/sys/class/net/%1/address").arg(device)); - if (address.open(QIODevice::ReadOnly)) { - iface.macAddress = address.readAll().trimmed(); - } - - // 通过 Socket ioctl 获取 IP 地址 - iface.ipAddresses = getIPAddresses(device); - - interfaces.append(iface); - } - - return interfaces; -} -```text - -### 5.5 档位计算逻辑 - -```cpp -void HardwareProbe::calculateTier(HardwareInfo& info) { - // 默认为 Low 档 - HWTier calculatedTier = HWTier::Low; - int score = 0; - - // CPU 评分 - score += std::min(info.cpu.cores, 8) * 10; // 最多 80 分 - if (info.cpu.frequencyMHz > 1000) score += 20; - if (info.cpu.features.contains("neon")) score += 10; - - // GPU 评分 - if (info.gpu.hasHardwareAcceleration) score += 50; - if (!info.gpu.driverPath.isEmpty()) score += 20; - - // 内存评分 - int memoryMB = info.memory.totalBytes / (1024 * 1024); - if (memoryMB >= 512) score += 20; - if (memoryMB >= 1024) score += 20; - if (memoryMB >= 2048) score += 10; - - // 根据评分决定档位 - // Low: 0-60, Mid: 61-120, High: 121+ - if (score >= 121) { - calculatedTier = HWTier::High; - } else if (score >= 61) { - calculatedTier = HWTier::Mid; - } else { - calculatedTier = HWTier::Low; - } - - info.tier = calculatedTier; -} -```yaml - ---- - -## 六、设备配置文件格式 - -> ⚠️ **过时说明**: 以下 `/etc/CFDesktop/` 路径为早期设计。实际实现中,CFDesktop 不读取系统级路径,设备配置通过 `setDeviceConfigOverride()` API 或 ConfigStore 应用内目录管理。 - -### 6.1 配置文件模板 - -**文件**: `configs/device.conf.template` - -```ini -# CFDesktop 设备配置文件 -# 部署位置: CFDesktop 自管理目录内 (非 /etc/) - -[Device] -# 档位设置: auto(自动检测) | low | mid | high -Tier=auto - -# 自定义检测脚本路径(可选) -# 脚本应输出 JSON 格式到 stdout -CustomScript=/opt/cfdesktop/detect-hardware.sh - -# 板载名称(可选,用于日志) -BoardName=Generic-Board - -[Overrides] -# 强制覆盖特定能力(可选) - -# 动画设置 -EnableAnimations=true -AnimationDuration=200 - -# 渲染后端: auto | linuxfb | eglfs | xcb -RenderingBackend=auto - -# OpenGL 设置 -ForceOpenGL=false - -# 视频解码: auto | software | hardware -VideoDecoder=auto - -[Logging] -# 硬件检测日志级别 -LogLevel=Info -```text - -### 6.2 自定义检测脚本示例 - -```bash -#!/bin/bash -# /opt/cfdesktop/detect-hardware.sh - -# 检测是否连接了特定扩展硬件 -if [ -e /sys/bus/i2c/devices/0-0050 ]; then - # 检测到扩展显示屏,提升档位 - echo '{"tier": "mid", "properties": {"externalDisplay": true}}' -else - echo '{"tier": "low"}' -fi -```yaml - ---- - -## 七、单元测试设计 - -### 7.1 Mock 数据目录结构 - -```text -tests/mock/ -└── proc/ - ├── cpuinfo_imx6ull # IMX6ULL CPU 信息 - ├── cpuinfo_rk3568 # RK3568 CPU 信息 - ├── cpuinfo_rk3588 # RK3588 CPU 信息 - ├── meminfo_512mb # 512MB 内存 - ├── meminfo_1gb # 1GB 内存 - ├── meminfo_4gb # 4GB 内存 - └── devices/ # 模拟设备文件 - └── dri/ - └── card0 # 模拟 GPU 设备 -```text - -### 7.2 测试用例清单 - -**文件**: `tests/unit/base/hardware/test_hardware_probe.cpp` - -```cpp -class TestHardwareProbe : public QObject { - Q_OBJECT - -private slots: - // ========== CPU 检测测试 ========== - void testDetectCPU_IMX6ULL(); - void testDetectCPU_RK3568(); - void testDetectCPU_RK3588(); - void testDetectCPU_X86_64(); - - // ========== GPU 检测测试 ========== - void testDetectGPU_WithDRM(); - void testDetectGPU_NoDRM(); - void testDetectGPU_OpenGLContext(); - - // ========== 内存检测测试 ========== - void testDetectMemory_512MB(); - void testDetectMemory_1GB(); - void testDetectMemory_4GB(); - - // ========== 档位计算测试 ========== - void testCalculateTier_IMX6ULL_returnsLow(); - void testCalculateTier_RK3568_returnsMid(); - void testCalculateTier_RK3588_returnsHigh(); - - // ========== 配置文件测试 ========== - void testDeviceConfig_LoadDefault(); - void testDeviceConfig_OverrideTier(); - void testDeviceConfig_CustomScript(); - - // ========== 策略引擎测试 ========== - void testCapabilityPolicy_LowTier(); - void testCapabilityPolicy_MidTier(); - void testCapabilityPolicy_HighTier(); - void testCapabilityPolicy_AnimationDisabledOnLow(); - - // ========== 边界情况 ========== - void testEmptyProcFiles(); - void testMalformedConfig(); - void testTierOverride(); -}; -```text - -### 7.3 示例测试用例 - -```cpp -void TestHardwareProbe::testDetectCPU_IMX6ULL() { - // 1. 设置 Mock 环境变量 - qputenv("CFDESKTOP_MOCK_PROC", "/path/to/mock/proc/cpuinfo_imx6ull"); - - // 2. 创建探针实例 - HardwareProbe probe; - HardwareInfo info = probe.probe(); - - // 3. 验证结果 - QCOMPARE(info.cpu.model, "Freescale i.MX6 UltraLite"); - QCOMPARE(info.cpu.cores, 1); - QVERIFY(info.cpu.features.contains("neon")); - QCOMPARE(info.cpu.architecture, "armv7l"); - - // 4. 验证档位 - QCOMPARE(info.tier, HWTier::Low); -} -```bash - ---- - -## 八、详细任务清单 - -### 8.1 Week 1: 基础检测器实现 - -#### Day 1-2: CPU 检测器 -- [ ] 创建 `CPUDetector` 类框架 -- [ ] 实现 `/proc/cpuinfo` 解析 -- [ ] 实现设备树 compatible 读取 -- [ ] 实现 `uname` 架构检测 -- [ ] 编写 CPU 检测单元测试 - -#### Day 3: GPU 检测器 -- [ ] 创建 `GPUDetector` 类框架 -- [ ] 实现 DRM 设备检测 -- [ ] 实现 OpenGL 上下文创建 -- [ ] 实现 GPU 能力查询 -- [ ] 编写 GPU 检测单元测试 - -#### Day 4: 内存与网络检测器 -- [ ] 实现 `/proc/meminfo` 解析 -- [ ] 实现网络接口枚举 -- [ ] 实现 IP 地址获取 -- [ ] 编写相应单元测试 - -#### Day 5: 档位计算逻辑 -- [ ] 实现评分算法 -- [ ] 定义档位阈值 -- [ ] 实现 `calculateTier()` 方法 -- [ ] 验证各板卡档位判定 - -### 8.2 Week 2: 策略引擎与配置 - -#### Day 1-2: CapabilityPolicy 实现 -- [ ] 定义各策略结构体 -- [ ] 实现档位特定策略配置 -- [ ] 实现 `getAnimationPolicy()` -- [ ] 实现 `getRenderingPolicy()` -- [ ] 实现 `getVideoDecoderPolicy()` -- [ ] 实现 `getMemoryPolicy()` - -#### Day 3: DeviceConfig 实现 -- [ ] 实现配置文件解析 -- [ ] 实现配置文件写入 -- [ ] 实现自定义脚本执行 -- [ ] 编写配置测试 - -#### Day 4: 集成测试 -- [ ] 创建端到端测试用例 -- [ ] 测试 IMX6ULL 真机 -- [ ] 测试 RK3568 真机 -- [ ] 验证自动档位判定 - -#### Day 5: 文档与优化 -- [ ] 编写 API 文档 -- [ ] 优化检测性能 -- [ ] 添加性能日志 -- [ ] Code Review - -### 8.3 Week 3: 完善与验证 - -#### Day 1-2: 跨平台支持 -- [ ] 实现 Windows 平台检测 -- [ ] 实现模拟器平台适配 -- [ ] 编写平台特定测试 - -#### Day 3-4: 真机验证 -- [ ] 在 IMX6ULL 上完整测试 -- [ ] 在 RK3568 上完整测试 -- [ ] 在 RK3588 上完整测试 -- [ ] 收集性能数据 - -#### Day 5: 发布准备 -- [ ] 最终测试 -- [ ] 更新文档 -- [ ] 准备演示 -- [ ] 合并主分支 - ---- - -## 九、验收标准 - -### 9.1 功能验收 -- [ ] 在 IMX6ULL 上正确识别为 Low 档 -- [ ] 在 RK3568 上正确识别为 Mid 档 -- [ ] 在 RK3588 上正确识别为 High 档 -- [ ] 所有单元测试通过(覆盖率 > 90%) -- [ ] 配置文件覆盖功能正常 -- [ ] 自定义脚本执行正常 - -### 9.2 性能验收 -- [ ] 硬件检测耗时 < 500ms -- [ ] 内存占用 < 5MB -- [ ] 不影响系统启动时间 - -### 9.3 代码质量 -- [ ] 符合代码规范 -- [ ] 通过 clang-tidy 检查 -- [ ] API 文档完整 -- [ ] 代码审查通过 - ---- - -## 十、已知问题与风险 - -| 问题 | 风险 | 缓解措施 | -|------|------|----------| -| 某些板卡没有设备树 | 档位误判 | 提供 CPU 特性回退检测 | -| GPU 驱动不稳定 | 检测失败 | 添加异常处理,设置超时 | -| 交叉编译测试困难 | 覆盖不足 | 使用 QEMU 用户模式模拟 | - ---- +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| 三级档位 (Low/Mid/High) | 覆盖目标硬件矩阵、策略配置简洁、避免过度细分 | 五级或连续分数(策略组合爆炸)、两级(粒度不够) | +| 加权评分算法 (CPU+GPU+Memory) | 各维度独立评分、阈值明确、易于调试 | 机器学习模型(过重、训练数据不足)、纯特征匹配(不通用) | +| 运行时检测 + 缓存 | 一次检测、全局复用、性能开销可控 | 编译时硬编码(无法适配新硬件)、每次查询都检测(性能浪费) | +| 策略引擎按档位返回策略结构体 | 上层模块只需读策略、不关心检测细节 | 上层模块自行解读硬件信息(逻辑分散、重复代码) | +| `setDeviceConfigOverride()` API 覆盖 | 灵活、不依赖系统路径、适配嵌入式部署 | 读取 `/etc/CFDesktop/device.conf`(需要 root 权限、路径不可移植) | -## 十一、下一步行动 +## 当前状态 -完成 Phase 1 后,进入 **Phase 2: Base 库核心**。 +已实现 -- CPU/GPU/Memory/Network 检测器、三级评分算法、策略引擎、用户覆盖 API 均已完成。详见 `document/design_stage/status/`。 diff --git a/document/design_stage/02_phase2_base_library.md b/document/design_stage/02_phase2_base_library.md index 117f80057..546390c3d 100644 --- a/document/design_stage/02_phase2_base_library.md +++ b/document/design_stage/02_phase2_base_library.md @@ -1,1308 +1,32 @@ --- title: "Phase 2: Base 库核心详细设计文档" -description: "Phase 2: Base 库核心详细设计文档 的详细文档" +description: "主题引擎五层流水线、DPI 适配、四层 ConfigStore、异步日志系统的设计意图" --- -# Phase 2: Base 库核心详细设计文档 +# Phase 2: Base 库核心 -- 设计意图 -## 文档信息 -| 项目 | 内容 | -|------|------| -| 文档版本 | v1.0 | -| 创建日期 | 2026-02-20 | -| 阶段代号 | Phase 2 - Base 库核心 | -| 预计周期 | 3~4 周 | -| 依赖阶段 | Phase 0, Phase 1 | +## 为什么选择这种方案 ---- - -## 一、阶段目标 - -### 1.1 核心目标 -建立所有上层模块依赖的基础设施,包括主题引擎、动画管理、分辨率适配、配置中心和日志系统。 - -### 1.2 具体交付物 -- [x] `ThemeEngine` 主题引擎模块 -- [x] `AnimationManager` 动画管理器 -- [ ] `DPIManager` 分辨率适配管理器 -- [x] `ConfigStore` 配置中心 -- [x] `Logger` 日志系统 -- [x] 各模块单元测试 - ---- - -## 二、模块架构设计 - -### 2.1 整体依赖关系 - -```text -┌──────────────────────────────────────────────────────────┐ -│ Application Layer │ -│ (Shell / Third-party Apps) │ -├──────────────────────────────────────────────────────────┤ -│ SDK Layer │ -├──────────────────────────────────────────────────────────┤ -│ Base Library │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ ThemeEngine │ │AnimationMgr │ │ DPIManager │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ ConfigStore │ │ Logger │ │HardwareProbe │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -├──────────────────────────────────────────────────────────┤ -│ Qt6 Framework │ -│ Core / Gui / Widgets / Multimedia / Network │ -└──────────────────────────────────────────────────────────┘ -```text - -### 2.2 Base 库目录结构 - -```text -src/base/ -├── include/CFDesktop/Base/ -│ ├── ThemeEngine/ -│ │ ├── ThemeEngine.h # 主题引擎主类 -│ │ ├── Theme.h # 主题数据结构 -│ │ ├── ThemeLoader.h # 主题加载器 -│ │ └── ThemeVariables.h # 主题变量系统 -│ │ -│ ├── AnimationManager/ -│ │ ├── AnimationManager.h # 动画管理器 -│ │ ├── Animation.h # 动画基类 -│ │ ├── PropertyAnimation.h # 属性动画 -│ │ ├── GroupAnimation.h # 组动画 -│ │ └── AnimationPolicy.h # 动画策略(来自 Phase 1) -│ │ -│ ├── DPIManager/ -│ │ ├── DPIManager.h # DPI 管理器 -│ │ ├── DisplayInfo.h # 显示信息 -│ │ └── DPIConverter.h # 单位转换工具 -│ │ -│ ├── ConfigStore/ -│ │ ├── ConfigStore.h # 配置存储主类 -│ │ ├── ConfigNode.h # 配置节点 -│ │ └── ConfigWatcher.h # 配置监视器 -│ │ -│ └── Logger/ -│ ├── Logger.h # 日志主类 -│ ├── LogMessage.h # 日志消息 -│ ├── LogSink.h # 日志输出抽象 -│ ├── FileSink.h # 文件输出 -│ ├── ConsoleSink.h # 控制台输出 -│ └── NetworkSink.h # 网络输出 -│ -└── src/ - ├── theme/ - │ ├── ThemeEngine.cpp - │ ├── ThemeLoader.cpp - │ ├── QSSProcessor.cpp # QSS 处理器 - │ └── VariableResolver.cpp # 变量解析器 - │ - ├── animation/ - │ ├── AnimationManager.cpp - │ ├── PropertyAnimation.cpp - │ └── GroupAnimation.cpp - │ - ├── dpi/ - │ ├── DPIManager.cpp - │ └── DPIConverter.cpp - │ - ├── config/ - │ ├── ConfigStore.cpp - │ └── ConfigWatcher.cpp - │ - └── logging/ - ├── Logger.cpp - ├── FileSink.cpp - ├── ConsoleSink.cpp - └── NetworkSink.cpp -```yaml - ---- - -## 三、主题引擎 (ThemeEngine) - -### 3.1 主题包结构 - -```text -assets/themes/ -└── default/ - ├── theme.json # 主题元数据 - ├── variables/ - │ ├── colors.json # 颜色变量 - │ ├── sizes.json # 尺寸变量 - │ └── fonts.json # 字体变量 - ├── styles/ - │ ├── base.qss # 基础样式 - │ ├── widgets.qss # 控件样式 - │ └── animations.qss # 动画样式 - └── icons/ - ├── actions/ # 动作图标 - ├── status/ # 状态图标 - └── devices/ # 设备图标 -```text - -### 3.2 主题元数据格式 - -**文件**: `assets/themes/default/theme.json` - -```json -{ - "meta": { - "name": "Default Light", - "version": "1.0.0", - "author": "CFDesktop Team", - "description": "Default light theme", - "hwTier": "any" - }, - "inherit": null, - "settings": { - "windowOpacity": 0.95, - "enableBlur": true, - "enableShadows": true, - "animationPolicy": "full" - } -} -```text - -### 3.3 颜色变量定义 - -**文件**: `assets/themes/default/variables/colors.json` - -```json -{ - "primary": "#2196F3", - "primaryDark": "#1976D2", - "primaryLight": "#BBDEFB", - "secondary": "#FF9800", - "accent": "#FF4081", - "background": "#FFFFFF", - "surface": "#F5F5F5", - "error": "#F44336", - "success": "#4CAF50", - "warning": "#FFC107", - "info": "#2196F3", - "text": { - "primary": "#212121", - "secondary": "#757575", - "disabled": "#BDBDBD", - "inverse": "#FFFFFF" - }, - "divider": "#E0E0E0", - "shadow": "rgba(0, 0, 0, 0.12)" -} -```text - -### 3.4 QSS 模板语法 - -```qss -/* assets/themes/default/styles/base.qss */ -QWidget { - background-color: @background; - color: @text.primary; -} - -QPushButton { - background-color: @primary; - color: @text.inverse; - border: none; - border-radius: @radius.medium; - padding: @padding.normal; - font-size: @font.size.medium; -} - -QPushButton:hover { - background-color: @primaryLight; -} - -QPushButton:pressed { - background-color: @primaryDark; -} - -QPushButton:disabled { - background-color: @divider; - color: @text.disabled; -} -```text - -### 3.5 ThemeEngine 类接口 - -**文件**: `include/CFDesktop/Base/ThemeEngine/ThemeEngine.h` - -```cpp -#pragma once -#include -#include -#include -#include -#include -#include "Theme.h" - -namespace CFDesktop::Base { - -/** - * @brief 主题引擎主类 - * - * 负责加载、切换和管理主题,提供变量查询接口。 - * 单例模式,进程内唯一实例。 - */ -class ThemeEngine : public QObject { - Q_OBJECT - -public: - static ThemeEngine* instance(); - - /** - * @brief 加载主题 - * @param themePath 主题路径 - * @return 是否成功 - */ - bool loadTheme(const QString& themePath); - - /** - * @brief 切换主题 - * @param themeName 主题名称 - */ - void switchTheme(const QString& themeName); - - /** - * @brief 获取当前主题 - */ - Theme currentTheme() const; - - /** - * @brief 获取颜色变量 - * @param key 变量名 (支持点分隔路径: "text.primary") - */ - QColor color(const QString& key) const; - - /** - * @brief 获取尺寸变量 - * @param key 变量名 - */ - int size(const QString& key) const; - - /** - * @brief 获取字体变量 - * @param key 变量名 - */ - QFont font(const QString& key) const; - - /** - * @brief 获取任意变量 - */ - QVariant variable(const QString& key) const; - - /** - * @brief 应用主题到应用 - * @param app 目标应用 - */ - void applyToApplication(QApplication* app); - - /** - * @brief 处理 QSS 中的变量引用 - * @param qss 原始 QSS - * @return 处理后的 QSS - */ - QString processQSS(const QString& qss) const; - - /** - * @brief 获取当前主题样式表 - */ - QString styleSheet() const; - - /** - * @brief 重新加载主题(热更新) - */ - void reload(); - -signals: - /** - * @brief 主题变化信号 - */ - void themeChanged(const Theme& theme); - - /** - * @brief 颜色变量变化信号 - */ - void colorChanged(const QString& key, const QColor& color); - -private: - ThemeEngine(QObject* parent = nullptr); - ~ThemeEngine() = default; - - void loadVariables(const QString& path); - void loadStyles(const QString& path); - void resolveThemeInheritance(Theme& theme); - QString resolveVariables(const QString& input) const; - -private: - static ThemeEngine* s_instance; - Theme m_currentTheme; - QMap m_colors; - QMap m_sizes; - QMap m_fonts; - QMap m_styleSheets; - QString m_themePath; -}; - -} // namespace CFDesktop::Base -```bash - -### 3.6 主题降级策略 - -根据 HWTier 自动应用不同效果: - -| 特性 | Low 档 | Mid 档 | High 档 | -|------|--------|--------|---------| -| 阴影效果 | 禁用 | 简单阴影 | 完整阴影 | -| 圆角 | 小圆角 (2px) | 中圆角 (4px) | 大圆角 (8px) | -| 模糊背景 | 禁用 | 半透明模糊 | 全模糊 | -| 渐变 | 禁用 | 简单渐变 | 复杂渐变 | -| 动画 | 禁用 | 基础动画 | 完整动画 | - ---- - -## 四、动画管理器 (AnimationManager) - -### 4.1 AnimationManager 类接口 - -**文件**: `include/CFDesktop/Base/AnimationManager/AnimationManager.h` - -```cpp -#pragma once -#include -#include -#include -#include -#include "Animation.h" - -namespace CFDesktop::Base { +Phase 2 是整个框架的基础设施层,为上层 UI 和桌面环境提供主题、动画、分辨率适配、配置和日志五大核心服务。这些服务必须在设计阶段就考虑嵌入式设备的资源约束——任何模块都不能在低端设备上成为性能瓶颈。 -/** - * @brief 动画管理器 - * - * 统一管理所有动画,根据 HWTier 自动调整动画参数。 - * Low 档位时所有动画时长归零,实现透明降级。 - */ -class AnimationManager : public QObject { - Q_OBJECT +**主题引擎**最终演化为五层流水线架构(Math & Utility -> Theme Engine -> Animation Engine -> Material Behavior -> Widget Adapter)。这一分层并非预先设计,而是在实现过程中发现:颜色运算(CFColor、GeometryHelper)与主题切换逻辑(ThemeManager、token system)职责不同,动画策略(CFMaterialAnimationFactory)与 Material Design 状态机行为(StateMachine、RippleHelper)也应该分离。五层流水线让每一层可以独立测试和替换,Low 档设备可以跳过高层(Layer 4/5)直接使用简化渲染。 -public: - static AnimationManager* instance(); - - /** - * @brief 创建属性动画 - * @param target 目标对象 - * @param propertyName 属性名 - * @param startValue 起始值 - * @param endValue 结束值 - * @param duration 动画时长 (毫秒),Low 档会自动归零 - * @return 动画指针 - */ - QPropertyAnimation* createPropertyAnimation( - QObject* target, - const QByteArray& propertyName, - const QVariant& startValue, - const QVariant& endValue, - int duration = 300 - ); - - /** - * @brief 创建淡入动画 - */ - QPropertyAnimation* createFadeIn( - QWidget* widget, - int duration = 300 - ); - - /** - * @brief 创建淡出动画 - */ - QPropertyAnimation* createFadeOut( - QWidget* widget, - int duration = 300 - ); - - /** - * @brief 创建滑入动画 - */ - QPropertyAnimation* createSlideIn( - QWidget* widget, - Qt::Direction direction, - int distance = 100, - int duration = 300 - ); - - /** - * @brief 创建缩放动画 - */ - QPropertyAnimation* createScale( - QWidget* widget, - qreal fromScale, - qreal toScale, - int duration = 300 - ); - - /** - * @brief 创建旋转动画 - */ - QPropertyAnimation* createRotation( - QWidget* widget, - qreal fromAngle, - qreal toAngle, - int duration = 300 - ); - - /** - * @brief 创建并行动画组 - */ - QParallelAnimationGroup* createParallelGroup( - const QList& animations - ); - - /** - * @brief 创建串行动画组 - */ - QSequentialAnimationGroup* createSequentialGroup( - const QList& animations - ); - - /** - * @brief 启动动画 - */ - void start(QAbstractAnimation* animation); - - /** - * @brief 停止动画 - */ - void stop(QAbstractAnimation* animation); - - /** - * @brief 停止所有动画 - */ - void stopAll(); - - /** - * @brief 调整动画时长(根据 HWTier) - * @param originalDuration 原始时长 - * @return 调整后的时长 - */ - int adjustDuration(int originalDuration) const; - - /** - * @brief 是否启用动画 - */ - bool isAnimationEnabled() const; - -signals: - /** - * @brief 动画策略变化信号 - */ - void animationPolicyChanged(const AnimationPolicy& policy); - -private: - AnimationManager(QObject* parent = nullptr); - ~AnimationManager() = default; - - void updatePolicy(); - QPropertyAnimation* createBaseAnimation( - QObject* target, - const QByteArray& propertyName - ); - -private: - static AnimationManager* s_instance; - AnimationPolicy m_policy; - QList> m_runningAnimations; -}; - -} // namespace CFDesktop::Base -```text - -### 4.2 预定义动画类型 - -```cpp -namespace CFDesktop::Base { - -/** - * @brief 预定义动画类型 - */ -enum class AnimationType { - None, // 无动画 - Fade, // 淡入淡出 - Slide, // 滑动 - Scale, // 缩放 - Rotate, // 旋转 - Bounce, // 弹跳 - Elastic, // 弹性 - Complex // 复杂组合 -}; - -/** - * @brief 缓动曲线类型 - */ -enum class EasingType { - Linear, - InQuad, OutQuad, InOutQuad, - InCubic, OutCubic, InOutCubic, - InQuart, OutQuart, InOutQuart, - InQuint, OutQuint, InOutQuint, - InBack, OutBack, InOutBack, - InBounce, OutBounce, InOutBounce -}; - -/** - * @brief 获取 Qt 缓动曲线 - */ -QEasingCurve easingCurve(EasingType type); - -} // namespace CFDesktop::Base -```yaml - ---- - -## 五、DPI 管理器 (DPIManager) - -### 5.1 DPIManager 类接口 - -**文件**: `include/CFDesktop/Base/DPIManager/DPIManager.h` - -```cpp -#pragma once -#include -#include -#include "DisplayInfo.h" - -namespace CFDesktop::Base { - -/** - * @brief DPI 管理器 - * - * 提供分辨率无关的尺寸单位,支持多屏幕和热插拔。 - */ -class DPIManager : public QObject { - Q_OBJECT - -public: - static DPIManager* instance(); - - /** - * @brief 密度无关像素 (dp) 转换为物理像素 - * - * 类 Android 的 dp 单位,在 160dpi 屏幕上 1dp = 1px - */ - int dp(int value) const; - - /** - * @brief 缩放无关像素 (sp) 转换为物理像素 - * - * 用于字体大小,会跟随用户字体缩放设置 - */ - int sp(int value) const; - - /** - * @brief 物理像素转换为 dp - */ - int pxToDp(int pixels) const; - - /** - * @brief 获取屏幕 DPI - */ - int screenDPI() const; - - /** - * @brief 获取设备像素比 - */ - qreal devicePixelRatio() const; - - /** - * @brief 获取屏幕尺寸 (英寸) - */ - QSizeF screenSizeInches() const; - - /** - * @brief 获取屏幕逻辑分辨率 - */ - QSize logicalSize() const; - - /** - * @brief 获取屏幕物理分辨率 - */ - QSize physicalSize() const; - - /** - * @brief 是否为高 DPI 屏幕 - */ - bool isHighDPI() const; - - /** - * @brief 注入屏幕参数(用于模拟器) - */ - void injectScreenParameters( - int dpi, - qreal devicePixelRatio, - const QSize& size - ); - - /** - * @brief 清除注入的参数 - */ - void clearInjectedParameters(); - -signals: - /** - * @brief DPI 变化信号 - */ - void dpiChanged(int oldDpi, int newDpi); - - /** - * @brief 屏幕变化信号 - */ - void screenChanged(const QRect& geometry); - -private: - DPIManager(QObject* parent = nullptr); - ~DPIManager() = default; - - void detectScreenParameters(); - void updateDpi(); - -private: - static DPIManager* s_instance; - int m_dpi = 96; - qreal m_devicePixelRatio = 1.0; - QSize m_logicalSize; - QSize m_physicalSize; - bool m_hasInjectedParams = false; -}; - -/** - * @brief 便捷函数 - */ -inline int dp(int value) { return DPIManager::instance()->dp(value); } -inline int sp(int value) { return DPIManager::instance()->sp(value); } - -} // namespace CFDesktop::Base -```text - -### 5.2 DPI 计算公式 - -```cpp -int DPIManager::dp(int value) const { - // 基准 DPI 为 160 - const qreal BASE_DPI = 160.0; - return qRound(value * m_dpi / BASE_DPI * m_devicePixelRatio); -} - -int DPIManager::sp(int value) const { - // sp 考虑字体缩放因子 - const qreal BASE_DPI = 160.0; - qreal fontScale = QApplication::font().pointSizeF() / 10.0; - return qRound(value * m_dpi / BASE_DPI * m_devicePixelRatio * fontScale); -} -```text - -### 5.3 常用尺寸定义 - -```cpp -namespace CFDesktop::Base::Sizes { - // 间距 (使用 dp) - inline int tiny() { return dp(2); } - inline int small() { return dp(4); } - inline int normal() { return dp(8); } - inline int medium() { return dp(16); } - inline int large() { return dp(24); } - inline int xlarge() { return dp(32); } - inline int xxlarge() { return dp(48); } - - // 圆角 - inline int radiusSmall() { return dp(2); } - inline int radiusMedium() { return dp(4); } - inline int radiusLarge() { return dp(8); } - inline int radiusXLarge() { return dp(16); } - - // 控件尺寸 - inline int buttonHeight() { return dp(40); } - inline int inputHeight() { return dp(36); } - inline int iconSizeSmall() { return dp(16); } - inline int iconSizeMedium() { return dp(24); } - inline int iconSizeLarge() { return dp(48); } -} -```yaml - ---- +**DPI 策略**采用 Android 风格的 dp/sp 单位系统,以 160dpi 为基准密度。选择这套方案而非 Qt 原生的 `QScreen::logicalDotsPerInch()` 直接缩放,是因为嵌入式设备的屏幕参数差异极大(从 7 寸 800x480 到 15 寸 1920x1080),dp/sp 提供了更可控的物理尺寸一致性。DPIManager 支持模拟器注入接口,便于在没有真实屏幕的开发机上调试布局。 -## 六、配置中心 (ConfigStore) +**ConfigStore** 采用四层优先级存储(Temp > App > User > System)。Temp 层纯内存,用于运行时临时状态;App 层随应用安装,提供默认配置;User 层存储用户偏好;System 层为系统管理员预留全局策略。四层覆盖而非单一配置文件,是因为嵌入式场景下需要区分「出厂默认」和「用户修改」,且系统管理员可能需要锁死某些策略(如禁用动画以节省电量)。 -### 6.1 ConfigStore 类接口 +**Logger** 采用异步 MPSC(多生产者单消费者)队列设计。日志写入是高频操作,如果在主线程同步写文件会严重影响 UI 流畅度。多线程通过 lock-free 队列提交日志消息,独立的后台消费者线程负责格式化和写入多个 Sink(文件、控制台、网络 UDP)。这确保了日志的性能影响始终低于 1%。 -**文件**: `include/CFDesktop/Base/ConfigStore/ConfigStore.h` +## 关键决策 -```cpp -#pragma once -#include -#include -#include -#include -#include "ConfigNode.h" - -namespace CFDesktop::Base { - -/** - * @brief 配置类型 - */ -enum class ConfigType { - System, // ⚠️ 过时:实际使用 CFDesktop 自管理目录,非 /etc/ - User, // 用户配置 (~/.config/CFDesktop/config.conf) - App, // 应用配置 - Temp // 临时配置 (内存) -}; - -/** - * @brief 配置中心 - * - * 提供层级化的配置存储,支持命名空间和变更监听。 - */ -class ConfigStore : public QObject { - Q_OBJECT - -public: - static ConfigStore* instance(); - - /** - * @brief 获取配置值 - * @param key 配置键,支持点分隔命名空间 (如 "ui.theme.name") - * @param defaultValue 默认值 - */ - QVariant get( - const QString& key, - const QVariant& defaultValue = QVariant() - ) const; - - /** - * @brief 设置配置值 - * @param key 配置键 - * @param value 配置值 - * @param type 配置类型 - */ - void set( - const QString& key, - const QVariant& value, - ConfigType type = ConfigType::User - ); - - /** - * @brief 删除配置 - */ - void remove(const QString& key, ConfigType type = ConfigType::User); - - /** - * @brief 检查配置是否存在 - */ - bool contains(const QString& key) const; - - /** - * @brief 获取配置节点 - */ - ConfigNode node(const QString& path) const; - - /** - * @brief 保存配置 - */ - void save(ConfigType type = ConfigType::User); - - /** - * @brief 重新加载配置 - */ - void reload(ConfigType type = ConfigType::User); - - /** - * @brief 设置配置变更监听 - * @param key 监听的键 - * @param receiver 接收对象 - * @param slot 槽函数 - */ - void watch( - const QString& key, - QObject* receiver, - const char* slot - ); - -signals: - /** - * @brief 配置变化信号 - */ - void configChanged(const QString& key, const QVariant& value); - -private: - ConfigStore(QObject* parent = nullptr); - ~ConfigStore(); - - void initialize(); - QString normalizeKey(const QString& key) const; - QSettings* settingsForType(ConfigType type) const; - -private: - static ConfigStore* s_instance; - QScopedPointer m_systemSettings; - QScopedPointer m_userSettings; - QHash m_tempSettings; - QMultiHash> m_watchers; -}; - -/** - * @brief 便捷访问函数 - */ -template -T getConfig(const QString& key, const T& defaultValue = T()) { - return ConfigStore::instance()->get(key, QVariant::fromValue(defaultValue)).value(); -} - -template -void setConfig(const QString& key, const T& value) { - ConfigStore::instance()->set(key, QVariant::fromValue(value)); -} - -} // namespace CFDesktop::Base -```text - -### 6.2 配置文件格式 - -**系统配置**: ~~`/etc/CFDesktop/config.conf`~~ ⚠️ 过时:实际使用 CFDesktop 自管理目录 - -```ini -[General] -Version=1.0 - -[Hardware] -Tier=auto -MaxMemoryMB=512 - -[UI] -Theme=default -EnableAnimations=auto -RenderingBackend=auto - -[Network] -AutoConnect=true -UpdateCheck=true - -[Logging] -Level=Info -MaxFileSizeMB=10 -MaxFiles=5 -```text - -**用户配置**: `~/.config/CFDesktop/config.conf` - -```ini -[General] -Language=zh_CN - -[UI] -Theme=dark -EnableAnimations=true -StatusBarPosition=top - -[User] -Name=User -AutoLogin=false -```yaml - ---- - -## 七、日志系统 (Logger) - -### 7.1 Logger 类接口 - -**文件**: `include/CFDesktop/Base/Logger/Logger.h` - -```cpp -#pragma once -#include -#include -#include "LogMessage.h" - -namespace CFDesktop::Base { - -/** - * @brief 日志等级 - */ -enum class LogLevel { - Trace = 0, - Debug, - Info, - Warning, - Error, - Fatal -}; - -/** - * @brief 日志输出抽象基类 - */ -class LogSink { -public: - virtual ~LogSink() = default; - virtual void write(const LogMessage& message) = 0; - virtual void flush() = 0; -}; - -/** - * @brief 日志主类 - * - * 提供统一的日志接口,支持多个输出目标。 - */ -class Logger : public QObject { - Q_OBJECT - -public: - static Logger* instance(); - - /** - * @brief 记录日志 - */ - void log(LogLevel level, const QString& tag, const QString& message); - - /** - * @brief 添加日志输出 - */ - void addSink(LogSink* sink); - - /** - * @brief 移除日志输出 - */ - void removeSink(LogSink* sink); - - /** - * @brief 设置最低日志等级 - */ - void setMinLevel(LogLevel level); - - /** - * @brief 获取最低日志等级 - */ - LogLevel minLevel() const; - - /** - * @brief 设置日志标签过滤器 - * - * 只有标签在列表中的日志才会被记录。 - * 空列表表示不过滤。 - */ - void setTagFilter(const QStringList& tags); - - /** - * @brief 刷新所有日志输出 - */ - void flush(); - - // 便捷方法 - void trace(const QString& tag, const QString& message); - void debug(const QString& tag, const QString& message); - void info(const QString& tag, const QString& message); - void warning(const QString& tag, const QString& message); - void error(const QString& tag, const QString& message); - void fatal(const QString& tag, const QString& message); - -signals: - /** - * @brief 日志写入信号 - */ - void messageLogged(const LogMessage& message); - -private: - Logger(QObject* parent = nullptr); - ~Logger() = default; - - QString formatMessage(const LogMessage& message) const; - QString levelToString(LogLevel level) const; - -private: - static Logger* s_instance; - QList m_sinks; - LogLevel m_minLevel = LogLevel::Info; - QStringList m_tagFilters; - QMutex m_mutex; -}; - -// 便捷宏 -#define LOG_TRACE(tag, msg) Logger::instance()->trace(tag, msg) -#define LOG_DEBUG(tag, msg) Logger::instance()->debug(tag, msg) -#define LOG_INFO(tag, msg) Logger::instance()->info(tag, msg) -#define LOG_WARNING(tag, msg) Logger::instance()->warning(tag, msg) -#define LOG_ERROR(tag, msg) Logger::instance()->error(tag, msg) -#define LOG_FATAL(tag, msg) Logger::instance()->fatal(tag, msg) - -} // namespace CFDesktop::Base -```text - -### 7.2 LogMessage 结构 - -```cpp -namespace CFDesktop::Base { - -/** - * @brief 日志消息结构 - */ -struct LogMessage { - LogLevel level; // 日志等级 - QString tag; // 标签 - QString message; // 消息内容 - QDateTime timestamp; // 时间戳 - QString threadId; // 线程 ID - QString sourceFile; // 源文件 - int sourceLine; // 源文件行号 - QString function; // 函数名 - - QString toString() const; -}; - -} // namespace CFDesktop::Base -```text - -### 7.3 LogSink 实现 - -#### FileSink - -```cpp -namespace CFDesktop::Base { - -/** - * @brief 文件日志输出 - */ -class FileSink : public LogSink { -public: - explicit FileSink(const QString& filePath); - ~FileSink() override; - - void write(const LogMessage& message) override; - void flush() override; - - void setMaxFileSize(int bytes); - void setMaxFiles(int count); - -private: - void checkRotation(); - void rotate(); - -private: - QFile m_file; - QTextStream m_stream; - int m_maxFileSize = 10 * 1024 * 1024; // 10MB - int m_maxFiles = 5; - QMutex m_mutex; -}; - -} // namespace CFDesktop::Base -```text - -#### ConsoleSink - -```cpp -namespace CFDesktop::Base { - -/** - * @brief 控制台日志输出 - * - * 支持彩色输出。 - */ -class ConsoleSink : public LogSink { -public: - explicit ConsoleSink(bool enableColor = true); - ~ConsoleSink() override = default; - - void write(const LogMessage& message) override; - void flush() override; - -private: - QString colorize(const QString& text, LogLevel level) const; - -private: - bool m_enableColor; -}; - -} // namespace CFDesktop::Base -```text - -#### NetworkSink - -```cpp -namespace CFDesktop::Base { - -/** - * @brief 网络日志输出 - * - * 通过 UDP 发送日志到远程服务器。 - */ -class NetworkSink : public LogSink { -public: - explicit NetworkSink(const QString& host, quint16 port); - ~NetworkSink() override; - - void write(const LogMessage& message) override; - void flush() override; - -private: - QUdpSocket m_socket; - QString m_host; - quint16 m_port; -}; - -} // namespace CFDesktop::Base -```text - -### 7.4 日志配置 - -**文件**: `configs/logging.conf` - -```ini -[General] -Level=Debug -# 过滤标签,空表示不过滤 -TagFilters=HardwareProbe,ThemeEngine,AnimationManager - -[Console] -Enabled=true -Color=true - -[File] -Enabled=true -Path=/var/log/CFDesktop/desktop.log -MaxFileSizeMB=10 -MaxFiles=5 - -[Network] -Enabled=false -Host=192.168.1.100 -Port=514 -```yaml - ---- - -## 八、详细任务清单 - -### 8.1 Week 1: 主题引擎 - -#### Day 1-2: 基础结构 -- [ ] 创建 ThemeEngine 类框架 -- [ ] 定义主题数据结构 -- [ ] 实现主题加载器 -- [ ] 创建默认主题包 - -#### Day 3: 变量系统 -- [ ] 实现变量解析器 -- [ ] 支持点分隔路径 -- [ ] 实现变量继承 - -#### Day 4: QSS 处理 -- [ ] 实现 QSS 变量替换 -- [ ] 实现 QSS 合并 -- [ ] 支持主题继承 - -#### Day 5: 集成与测试 -- [ ] 集成到应用 -- [ ] 编写单元测试 -- [ ] 性能优化 - -### 8.2 Week 2: 动画管理器 - -#### Day 1-2: 核心功能 -- [ ] 创建 AnimationManager 类 -- [ ] 实现基础动画创建 -- [ ] 实现 HWTier 降级逻辑 - -#### Day 3: 预定义动画 -- [ ] 实现淡入淡出 -- [ ] 实现滑动动画 -- [ ] 实现缩放动画 -- [ ] 实现旋转动画 - -#### Day 4: 组动画 -- [ ] 实现并行动画组 -- [ ] 实现串行动画组 -- [ ] 实现动画生命周期管理 - -#### Day 5: 测试与优化 -- [ ] 编写单元测试 -- [ ] 性能测试 -- [ ] 内存泄漏检查 - -### 8.3 Week 3: DPI 管理器与配置中心 - -#### Day 1-2: DPI 管理器 -- [ ] 创建 DPIManager 类 -- [ ] 实现屏幕检测 -- [ ] 实现 dp/sp 转换 -- [ ] 实现模拟器注入接口 - -#### Day 3-4: 配置中心 -- [ ] 创建 ConfigStore 类 -- [ ] 实现层级存储 -- [ ] 实现变更监听 -- [ ] 实现配置持久化 - -#### Day 5: 测试 -- [ ] 跨平台测试 -- [ ] 单元测试 -- [ ] 集成测试 - -### 8.4 Week 4: 日志系统与集成 - -#### Day 1-2: 日志系统 -- [ ] 创建 Logger 类 -- [ ] 实现 FileSink -- [ ] 实现 ConsoleSink -- [ ] 实现 NetworkSink - -#### Day 3: 日志增强 -- [ ] 实现日志轮转 -- [ ] 实现标签过滤 -- [ ] 实现彩色输出 - -#### Day 4: 集成测试 -- [ ] 全模块集成 -- [ ] 端到端测试 -- [ ] 性能测试 - -#### Day 5: 文档与发布 -- [ ] API 文档 -- [ ] 使用示例 -- [ ] Code Review - ---- - -## 九、验收标准 - -### 9.1 主题引擎 -- [ ] 支持动态切换主题 -- [ ] 支持变量继承 -- [ ] Low 档位自动禁用阴影/圆角/渐变 -- [ ] 热重载时间 < 100ms - -### 9.2 动画管理器 -- [ ] Low 档位动画时长归零 -- [ ] 所有预定义动画正常工作 -- [ ] 无内存泄漏 -- [ ] 动画不阻塞主线程 - -### 9.3 DPI 管理器 -- [ ] dp/sp 转换准确 -- [ ] 支持模拟器注入 -- [ ] 热插拔屏幕自动适配 - -### 9.4 配置中心 -- [ ] 层级存储正常 -- [ ] 变更监听触发 -- [ ] 持久化正常 - -### 9.5 日志系统 -- [ ] 多 Sink 并发安全 -- [ ] 日志轮转正常 -- [ ] 性能影响 < 1% - ---- +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| 主题引擎五层流水线 | 各层独立测试、Low 档可跳过高层的 Material 效果 | 单体 ThemeEngine 类(职责过重、难以裁剪) | +| dp/sp 密度无关单位 (160dpi 基准) | 物理尺寸一致性好、嵌入式屏幕参数差异大时仍可控 | 直接使用 Qt DPI 缩放(不同设备表现不一致) | +| ConfigStore 四层覆盖 (Temp/App/User/System) | 区分出厂默认与用户修改、支持管理员锁定策略 | 单一 JSON 配置文件(无法表达优先级和锁定) | +| Logger 异步 MPSC 队列 | 日志写入不阻塞主线程、多线程安全、性能影响 <1% | 同步写文件(主线程卡顿)、spdlog(引入外部依赖) | +| 主题降级由 HWTier 驱动 | Low 档自动禁用阴影/模糊/动画,Mid 档简化效果 | 手动配置每个视觉效果(用户负担重、易遗漏) | -## 十、下一步行动 +## 当前状态 -完成 Phase 2 后,进入 **Phase 3: 输入抽象层**。 +部分实现 -- ThemeEngine、AnimationManager、ConfigStore、Logger 已完成;DPIManager 尚未实现。UI 层已从 Phase 2 中分离为独立的 `ui/` 模块并完成 95%(P0/P1 widgets)。详见 `document/design_stage/status/`。 diff --git a/document/design_stage/03_phase3_input_layer.md b/document/design_stage/03_phase3_input_layer.md index 2c52ea5b0..302715eb5 100644 --- a/document/design_stage/03_phase3_input_layer.md +++ b/document/design_stage/03_phase3_input_layer.md @@ -1,1452 +1,29 @@ --- title: "Phase 3: 输入抽象层详细设计文档" -description: "完成 Phase 3 后,进入 Phase 4: Shell UI 主体。" +description: "统一触摸、按键、旋钮等输入事件,支持焦点导航和手势识别" --- -# Phase 3: 输入抽象层详细设计文档 +# Phase 3: 输入抽象层 -- 设计意图 -## 文档信息 -| 项目 | 内容 | -|------|------| -| 文档版本 | v1.0 | -| 创建日期 | 2026-02-20 | -| 阶段代号 | Phase 3 - 输入抽象层 | -| 预计周期 | 1~2 周 | -| 依赖阶段 | Phase 0, Phase 1, Phase 2 | +## 为什么选择这种方案 ---- - -## 一、阶段目标 - -### 1.1 核心目标 -屏蔽底层输入差异,统一触摸、物理按键、旋钮等输入事件,支持焦点导航模式。 - -### 1.2 具体交付物 -- [ ] `InputManager` 统一分发层 -- [ ] `TouchInputHandler` 触摸处理器 -- [ ] `KeyInputHandler` 按键处理器 -- [ ] `RotaryInputHandler` 旋钮处理器 -- [ ] `FocusNavigator` 焦点导航器 -- [ ] 单元测试 - ---- - -## 二、模块架构设计 - -### 2.1 整体架构图 - -```text -┌─────────────────────────────────────────────────────────────┐ -│ Application Layer │ -│ (Widgets receive events) │ -├─────────────────────────────────────────────────────────────┤ -│ Input Manager │ -│ ┌────────────────────────────────────────────────────┐ │ -│ │ 事件转换与分发 │ │ -│ │ - 统一事件接口 │ │ -│ │ - 事件过滤与拦截 │ │ -│ │ - 手势识别 │ │ -│ └────────────────────────────────────────────────────┘ │ -├──────────────────────┬──────────────────────────────────────┤ -│ Native Input Sources│ Qt Input Sources │ -│ ┌────────────────┐ │ ┌─────────────────────────────────┐ │ -│ │ evdev Devices │ │ │ QTouchEvent (linuxfb/eglfs) │ │ -│ │ /dev/input/event│ │ │ QKeyEvent (键盘) │ │ -│ │ GPIO Buttons │ │ │ QMouseEvent (鼠标) │ │ -│ │ Rotary Encoder │ │ │ QWheelEvent (滚轮) │ │ -│ └────────────────┘ │ └─────────────────────────────────┘ │ -├──────────────────────┴──────────────────────────────────────┤ -│ Kernel / Driver Layer │ -│ evdev, GPIO, I2C, SPI input drivers │ -└─────────────────────────────────────────────────────────────┘ -```text - -### 2.2 文件结构 - -```text -src/base/ -├── include/CFDesktop/Base/Input/ -│ ├── InputManager.h # 输入管理器主类 -│ ├── InputEvent.h # 统一事件结构 -│ ├── TouchInputHandler.h # 触摸处理器 -│ ├── KeyInputHandler.h # 按键处理器 -│ ├── RotaryInputHandler.h # 旋钮处理器 -│ ├── GestureRecognizer.h # 手势识别器 -│ ├── FocusNavigator.h # 焦点导航器 -│ └── InputDevice.h # 输入设备抽象 -│ -└── src/input/ - ├── InputManager.cpp - ├── TouchInputHandler.cpp - ├── KeyInputHandler.cpp - ├── RotaryInputHandler.cpp - ├── GestureRecognizer.cpp - ├── FocusNavigator.cpp - ├── native/ # 平台特定实现 - │ ├── EvdevDevice.cpp # Linux evdev 设备 - │ ├── GPIOButton.cpp # GPIO 按键 - │ └── RotaryEncoder.cpp # 旋转编码器 - └── simulator/ # 模拟器支持 - └── SimulatedInput.cpp -```yaml - ---- - -## 三、统一事件结构 - -### 3.1 输入事件类型 - -**文件**: `include/CFDesktop/Base/Input/InputEvent.h` - -```cpp -#pragma once -#include -#include -#include -#include - -namespace CFDesktop::Base { - -/** - * @brief 输入设备类型 - */ -enum class InputDeviceType { - Unknown, - Touchscreen, # 触摸屏 - Mouse, # 鼠标 - Keyboard, # 键盘 - PhysicalButton, # 物理按键 - RotaryEncoder, # 旋转编码器 - Gamepad, # 游戏手柄 - Custom # 自定义设备 -}; - -/** - * @brief 统一输入事件基类 - */ -class InputEvent { -public: - InputDeviceType deviceType = InputDeviceType::Unknown; - QString deviceId; # 设备 ID - quint64 timestamp; # 时间戳 (微秒) - - virtual ~InputEvent() = default; - virtual QEvent* toQtEvent() const = 0; -}; - -/** - * @brief 触摸/指针位置事件 - */ -class PointerEvent : public InputEvent { -public: - enum class Type { - Press, - Release, - Move, - Enter, - Leave - }; - - Type type; - QPointF position; # 屏幕坐标 - int pointerId = -1; # 多点触摸 ID - qreal pressure = 0.0; # 压力 (0.0 - 1.0) - int buttons = 0; # 按钮状态 - - QEvent* toQtEvent() const override; -}; - -/** - * @brief 按键事件 - */ -class KeyEvent : public InputEvent { -public: - enum class Type { - Press, - Release, - Repeat, - LongPress, # 长按 - MultiPress # 多连击 - }; - - Type type; - int keyCode; # Qt Key_Code - quint32 nativeCode; # 原生扫描码 - QString text; # 文本内容 - bool isAutoRepeat = false; - int clickCount = 0; # 连击计数 - - QEvent* toQtEvent() const override; -}; - -/** - * @brief 旋钮事件 - */ -class RotaryEvent : public InputEvent { -public: - enum class Direction { - Clockwise, - CounterClockwise - }; - - Direction direction; - int steps = 1; # 步数 - qreal angleDelta = 0.0; # 角度变化 - bool isPressed = false; # 是否被按下 - int clickCount = 0; # 旋转档位 - - QEvent* toQtEvent() const override; -}; - -/** - * @brief 手势事件 - */ -class GestureEvent : public InputEvent { -public: - enum class Type { - Swipe, - Pinch, - Rotate, - LongPress, - DoubleTap, - Custom - }; - - Type type; - QVariantMap data; # 手势特定数据 - QPointF centerPoint; - qreal angle = 0.0; - qreal scale = 1.0; - QPointF delta; -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -## 四、输入管理器 (InputManager) - -### 4.1 InputManager 类接口 - -**文件**: `include/CFDesktop/Base/Input/InputManager.h` - -```cpp -#pragma once -#include -#include -#include -#include "InputEvent.h" - -namespace CFDesktop::Base { - -class InputDevice; -class TouchInputHandler; -class KeyInputHandler; -class RotaryInputHandler; -class GestureRecognizer; - -/** - * @brief 输入管理器 - * - * 统一管理所有输入设备,提供事件分发和过滤。 - * 单例模式,在应用启动时初始化。 - */ -class InputManager : public QObject { - Q_OBJECT - -public: - static InputManager* instance(); - - /** - * @brief 初始化输入管理器 - */ - bool initialize(); - - /** - * @brief 注册输入设备 - */ - void registerDevice(InputDevice* device); - - /** - * @brief 注销输入设备 - */ - void unregisterDevice(InputDevice* device); - - /** - * @brief 获取所有设备 - */ - QList devices() const; - - /** - * @brief 获取指定类型的设备 - */ - QList devices(InputDeviceType type) const; - - /** - * @brief 添加事件过滤器 - * - * 过滤器返回 true 表示事件被拦截。 - */ - void addEventFilter(QObject* filter); - - /** - * @brief 移除事件过滤器 - */ - void removeEventFilter(QObject* filter); - - /** - * @brief 分发事件到指定对象 - */ - bool dispatchEvent(QObject* receiver, InputEvent* event); - - /** - * @brief 启用/禁用输入 - */ - void setEnabled(bool enabled); - bool isEnabled() const; - -signals: - /** - * @brief 设备连接信号 - */ - void deviceConnected(InputDevice* device); - - /** - * @brief 设备断开信号 - */ - void deviceDisconnected(InputDevice* device); - - /** - * @brief 输入事件信号(用于调试) - */ - void inputEventReceived(InputEvent* event); - -protected: - bool eventFilter(QObject* watched, QEvent* event) override; - -private: - InputManager(QObject* parent = nullptr); - ~InputManager() = default; - - void setupNativeInputDevices(); - void processQtEvent(QEvent* event); - -private: - static InputManager* s_instance; - QList> m_devices; - QList> m_eventFilters; - QPointer m_touchHandler; - QPointer m_keyHandler; - QPointer m_rotaryHandler; - QPointer m_gestureRecognizer; - bool m_enabled = true; -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -## 五、触摸处理器 (TouchInputHandler) - -### 5.1 TouchInputHandler 类接口 - -**文件**: `include/CFDesktop/Base/Input/TouchInputHandler.h` - -```cpp -#pragma once -#include -#include -#include "InputEvent.h" - -namespace CFDesktop::Base { - -/** - * @brief 触摸点状态 - */ -struct TouchPoint { - int id; - QPointF position; - QPointF startPosition; - qreal pressure = 0.0; - qint64 startTime = 0; - bool isPressed = false; -}; - -/** - * @brief 触摸处理器 - * - * 处理 Qt 原生触摸事件,转换为统一事件格式。 - * 支持多点触摸和手势识别。 - */ -class TouchInputHandler : public QObject { - Q_OBJECT - -public: - explicit TouchInputHandler(QObject* parent = nullptr); - ~TouchInputHandler() = default; - - /** - * @brief 处理 Qt 触摸事件 - */ - bool handleEvent(QTouchEvent* event); - - /** - * @brief 启用/禁用触摸输入 - */ - void setEnabled(bool enabled); - bool isEnabled() const; - - /** - * @brief 设置点击判定阈值 - * - * 移动距离小于此值视为点击。 - */ - void setClickThreshold(qreal pixels); - - /** - * @brief 设置长按超时 - */ - void setLongPressTimeout(int milliseconds); - - /** - * @brief 获取当前触摸点 - */ - QHash activeTouches() const; - -signals: - /** - * @brief 触摸事件信号 - */ - void touchEvent(PointerEvent* event); - - /** - * @brief 单击信号 - */ - void singleTap(const QPointF& position); - - /** - * @brief 双击信号 - */ - void doubleTap(const QPointF& position); - - /** - * @brief 长按信号 - */ - void longPress(const QPointF& position); - -private: - void updateTouchPoints(const QTouchEvent::TouchPoint& point); - void detectGestures(); - void resetGestureState(); - -private: - bool m_enabled = true; - qreal m_clickThreshold = 10.0; # dp(10) - int m_longPressTimeout = 500; - QHash m_activeTouches; - QTimer* m_longPressTimer; - QPointF m_lastPosition; -}; - -} // namespace CFDesktop::Base -```text - -### 5.2 触摸事件流程图 - -```text -QTouchEvent - │ - ├─> TouchPress - │ │ - │ ├─> 记录触摸点 - │ ├─> 启动长按定时器 - │ └─> emit touchEvent() - │ - ├─> TouchMove - │ │ - │ ├─> 更新触摸点位置 - │ ├─> 检测是否超过点击阈值 - │ ├─> 如果超过阈值,取消长按定时器 - │ └─> emit touchEvent() - │ - └─> TouchRelease - │ - ├─> 计算移动距离 - ├─> 如果距离小且时间短:单击 - ├─> 如果是第二次单击:双击 - ├─> 如果定时器触发:长按 - └─> 清理触摸点 -```yaml - ---- - -## 六、按键处理器 (KeyInputHandler) - -### 6.1 KeyInputHandler 类接口 - -**文件**: `include/CFDesktop/Base/Input/KeyInputHandler.h` - -```cpp -#pragma once -#include -#include -#include "InputEvent.h" - -namespace CFDesktop::Base { - -/** - * @brief 按键配置 - */ -struct KeyConfig { - int keyCode; # Qt 键码 - QString action; # 动作名称 - int longPressThreshold = 500; # 长按阈值 (毫秒) - int repeatDelay = 300; # 重复延迟 - int repeatInterval = 100; # 重复间隔 - bool supportLongPress = true; - bool supportMultiPress = true; - int maxMultiPressCount = 3; # 最大连击数 -}; - -/** - * @brief 按键处理器 - * - * 处理键盘和物理按键输入,支持长按、连击检测。 - */ -class KeyInputHandler : public QObject { - Q_OBJECT - -public: - explicit KeyInputHandler(QObject* parent = nullptr); - ~KeyInputHandler() = default; - - /** - * @brief 处理 Qt 按键事件 - */ - bool handleEvent(QKeyEvent* event); - - /** - * @brief 注册按键配置 - */ - void registerKey(const KeyConfig& config); - - /** - * @brief 取消按键注册 - */ - void unregisterKey(int keyCode); - - /** - * @brief 映射物理按键到 Qt 键码 - * - * 用于 evdev 扫描码到 Qt Key 的映射。 - */ - void mapKey(int nativeScanCode, int qtKeyCode); - - /** - * @brief 获取当前按下的按键 - */ - QList pressedKeys() const; - -signals: - /** - * @brief 按键事件信号 - */ - void keyEvent(KeyEvent* event); - - /** - * @brief 动作触发信号 - */ - void actionTriggered(const QString& action, int count); - - /** - * @brief 长按信号 - */ - void longPress(int keyCode); - - /** - * @brief 组合键信号 - */ - void combinationPressed(const QList& keyCodes); - -private: - void handleKeyPress(QKeyEvent* event); - void handleKeyRelease(QKeyEvent* event); - bool detectCombination(); - -private: - QHash m_keyConfigs; - QHash m_scanCodeToQtKey; - QList m_pressedKeys; - QHash m_keyPressTime; - QHash m_keyPressCount; - QHash m_longPressTimers; - QHash m_repeatTimers; -}; - -} // namespace CFDesktop::Base -```text - -### 6.2 预定义按键映射 - -```cpp -namespace CFDesktop::Base { - -/** - * @brief 预定义按键动作 - */ -enum class StandardAction { - // 导航 - Up, - Down, - Left, - Right, - Enter, - Back, - Home, - Menu, - - // 媒体 - VolumeUp, - VolumeDown, - Mute, - PlayPause, - Next, - Previous, - - // 系统 - Power, - Sleep, - WakeUp, - - // 自定义 - Custom1, - Custom2, - Custom3, - Custom4 -}; - -/** - * @brief 标准按键映射表 - */ -struct StandardKeyMapping { - static void initializeDefaults(KeyInputHandler* handler); - - // GPIO 按键默认映射 - static const QHash gpioButtonMap; -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -## 七、旋钮处理器 (RotaryInputHandler) - -### 7.1 RotaryInputHandler 类接口 - -**文件**: `include/CFDesktop/Base/Input/RotaryInputHandler.h` - -```cpp -#pragma once -#include -#include "InputEvent.h" - -namespace CFDesktop::Base { - -/** - * @brief 旋钮配置 - */ -struct RotaryConfig { - int deviceId = 0; - int stepsPerRevolution = 24; # 每圈步数 - bool hasButton = true; # 是否带按钮 - qreal acceleration = 1.0; # 加速因子 - int velocitySamples = 3; # 速度采样数 -}; - -/** - * @brief 旋钮处理器 - * - * 处理旋转编码器输入,支持加速和按钮模式。 - */ -class RotaryInputHandler : public QObject { - Q_OBJECT - -public: - explicit RotaryInputHandler(QObject* parent = nullptr); - ~RotaryInputHandler() = default; - - /** - * @brief 处理旋钮事件 - */ - bool handleEvent(int delta, bool buttonPressed); - - /** - * @brief 设置旋钮配置 - */ - void setConfig(const RotaryConfig& config); - - /** - * @brief 获取当前旋转速度 - * - * 返回步数/秒 - */ - qreal currentVelocity() const; - - /** - * @brief 获取累计旋转角度 - */ - qreal accumulatedAngle() const; - - /** - * @brief 重置累计角度 - */ - void resetAccumulatedAngle(); - -signals: - /** - * @brief 旋转事件信号 - */ - void rotated(RotaryEvent* event); - - /** - * @brief 简单旋转信号 - */ - void stepped(int steps, RotaryEvent::Direction direction); - - /** - * @brief 按钮按下信号 - */ - void buttonPressed(); - - /** - * @brief 按钮释放信号 - */ - void buttonReleased(); - - /** - * @brief 速度变化信号 - */ - void velocityChanged(qreal velocity); - -private: - void updateVelocity(); - int calculateAcceleratedSteps(int rawSteps); - -private: - RotaryConfig m_config; - qreal m_accumulatedAngle = 0.0; - QList m_timestampSamples; - QList m_deltaSamples; - qreal m_currentVelocity = 0.0; - bool m_buttonPressed = false; -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -## 八、手势识别器 (GestureRecognizer) - -### 8.1 GestureRecognizer 类接口 - -**文件**: `include/CFDesktop/Base/Input/GestureRecognizer.h` +CFDesktop 面向嵌入式工控屏、平板和桌面多种形态,输入源差异极大:低端工控屏仅有电阻触摸+GPIO 按键,中端设备带旋转编码器,桌面端则是鼠标键盘。如果不做输入抽象,每个 Widget 必须分别处理 QTouchEvent、QKeyEvent、QWheelEvent 以及原生 evdev/GPIO 事件,导致大量重复代码且难以保证跨设备行为一致。 -```cpp -#pragma once -#include -#include -#include "InputEvent.h" +因此设计了 `InputManager` 单一分发层:所有输入源(Qt 事件、evdev 设备、GPIO、旋转编码器)先被各自的 Handler 转换为统一的 `InputEvent` 体系(PointerEvent / KeyEvent / RotaryEvent / GestureEvent),再由 InputManager 统一分发到目标控件。这样 Widget 层只对接一套事件接口,新增输入类型只需添加新的 Handler,不影响上层。 -namespace CFDesktop::Base { +手势识别(GestureRecognizer)从触摸点流中检测 Swipe/Pinch/LongPress/DoubleTap 等手势,将结果作为 GestureEvent 分发。焦点导航(FocusNavigator)为无触摸设备提供基于空间距离的方向导航算法,支持自定义焦点链和循环策略。模拟器通过 `SimulatedInputConfig` 将鼠标/键盘映射为触摸/旋钮事件,确保开发机和目标设备行为一致。 -/** - * @brief 手势类型 - */ -enum class GestureType { - None, - Swipe, - Pinch, - Rotate, - LongPress, - DoubleTap, - TwoFingerTap, - ThreeFingerSwipe, - Custom -}; +## 关键决策 -/** - * @brief 手势方向 - */ -enum class GestureDirection { - Up, - Down, - Left, - Right, - In, - Out, - Clockwise, - CounterClockwise -}; - -/** - * @brief 手势识别结果 - */ -struct GestureResult { - GestureType type = GestureType::None; - GestureDirection direction = GestureDirection::Up; - QPointF centerPoint; - QPointF displacement; # 位移 - qreal scale = 1.0; # 缩放比例 - qreal angle = 0.0; # 旋转角度 - qint64 duration = 0; # 持续时间 - int fingerCount = 1; # 触摸点数 -}; - -/** - * @brief 手势配置 - */ -struct GestureConfig { - int swipeMinDistance = 50; # 最小滑动距离 (dp) - int swipeMaxDeviation = 30; # 最大偏离 (dp) - int swipeTimeout = 500; # 滑动超时 (毫秒) - - int pinchMinDistance = 20; # 最小捏合距离 - qreal pinchMinScale = 0.5; # 最小缩放比例 - - int longPressTimeout = 500; # 长按超时 - int longPressMaxMovement = 10; # 长按最大移动 - - int doubleTapInterval = 300; # 双击间隔 - int doubleTapMaxMovement = 20; # 双击最大移动 - - bool enableAll = true; # 启用所有手势 -}; - -/** - * @brief 手势识别器 - * - * 从触摸点流中识别常见手势。 - */ -class GestureRecognizer : public QObject { - Q_OBJECT - -public: - explicit GestureRecognizer(QObject* parent = nullptr); - ~GestureRecognizer() = default; - - /** - * @brief 处理触摸点更新 - */ - void processTouchPoints(const QHash& points); - - /** - * @brief 设置手势配置 - */ - void setConfig(const GestureConfig& config); - - /** - * @brief 启用/禁用特定手势 - */ - void setGestureEnabled(GestureType type, bool enabled); - - /** - * @brief 取消当前手势 - */ - void cancelCurrentGesture(); - -signals: - /** - * @brief 手势识别信号 - */ - void gestureRecognized(const GestureResult& gesture); - - /** - * @brief 手势开始信号 - */ - void gestureStarted(GestureType type); - - /** - * @brief 手势更新信号 - */ - void gestureUpdated(const GestureResult& gesture); - - /** - * @brief 手势结束信号 - */ - void gestureEnded(const GestureResult& gesture); - - /** - * @brief 手势取消信号 - */ - void gestureCancelled(GestureType type); - -private: - bool detectSwipe(); - bool detectPinch(); - bool detectRotate(); - bool detectLongPress(); - bool detectDoubleTap(); - GestureDirection detectDirection(const QPointF& delta); - - void reset(); - -private: - GestureConfig m_config; - QSet m_enabledGestures; - QHash m_activeTouches; - QList m_startPoints; - GestureResult m_currentGesture; - qint64 m_gestureStartTime = 0; - bool m_isGestureActive = false; -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -## 九、焦点导航器 (FocusNavigator) - -### 9.1 FocusNavigator 类接口 - -**文件**: `include/CFDesktop/Base/Input/FocusNavigator.h` - -```cpp -#pragma once -#include -#include -#include "InputEvent.h" - -namespace CFDesktop::Base { - -/** - * @brief 焦点移动方向 - */ -enum class FocusDirection { - Up, - Down, - Left, - Right, - Next, # Tab 顺序下一个 - Previous, # Tab 顺序上一个 - First, # 第一个控件 - Last # 最后一个控件 -}; - -/** - * @brief 焦点策略 - */ -enum class FocusPolicy { - // 自动策略 - Auto, - - // 严格策略:只跳到可视控件 - Strict, - - // 包含策略:包含隐藏/禁用控件 - Inclusive, - - // 循环策略:边界循环 - Wrap, - - // 自定义策略 - Custom -}; - -/** - * @brief 焦点导航器 - * - * 为无触摸设备提供键盘/按键焦点导航。 - */ -class FocusNavigator : public QObject { - Q_OBJECT - -public: - static FocusNavigator* instance(); - - /** - * @brief 设置导航根窗口 - */ - void setRootWidget(QWidget* widget); - - /** - * @brief 移动焦点 - */ - bool moveFocus(FocusDirection direction); - - /** - * @brief 设置焦点策略 - */ - void setFocusPolicy(FocusPolicy policy); - - /** - * @brief 添加焦点链 - * - * 自定义焦点跳转顺序。 - */ - void addFocusChain(QWidget* from, QWidget* to, FocusDirection direction); - - /** - * @brief 移除焦点链 - */ - void removeFocusChain(QWidget* from, FocusDirection direction); - - /** - * @brief 设置当前焦点控件 - */ - void setFocusWidget(QWidget* widget); - - /** - * @brief 获取当前焦点控件 - */ - QWidget* currentFocusWidget() const; - - /** - * @brief 获取下一个焦点控件 - */ - QWidget* nextFocusWidget(FocusDirection direction) const; - -signals: - /** - * @brief 焦点变化信号 - */ - void focusChanged(QWidget* oldWidget, QWidget* newWidget); - - /** - * @brief 无焦点可移动信号 - */ - void focusBlocked(QWidget* currentWidget, FocusDirection direction); - -private: - FocusNavigator(QObject* parent = nullptr); - ~FocusNavigator() = default; - - QWidget* findNearestWidget(QWidget* start, FocusDirection direction) const; - bool isFocusable(QWidget* widget) const; - qreal calculateDistance(QWidget* from, QWidget* to, FocusDirection direction) const; - bool isInDirection(QWidget* from, QWidget* to, FocusDirection direction) const; - -private: - static FocusNavigator* s_instance; - QPointer m_rootWidget; - QPointer m_currentWidget; - FocusPolicy m_policy = FocusPolicy::Auto; - QMultiHash> m_focusChains; -}; - -} // namespace CFDesktop::Base -```text - -### 9.2 焦点导航算法 - -```cpp -QWidget* FocusNavigator::findNearestWidget( - QWidget* start, - FocusDirection direction -) const { - if (!start || !m_rootWidget) { - return nullptr; - } - - QWidget* nearest = nullptr; - qreal minDistance = std::numeric_limits::max(); - - // 获取所有可聚焦的子控件 - QList candidates = m_rootWidget->findChildren(); - QRect startRect = start->geometry(); - - for (QWidget* candidate : candidates) { - if (!isFocusable(candidate) || candidate == start) { - continue; - } - - if (!isInDirection(start, candidate, direction)) { - continue; - } - - qreal distance = calculateDistance(start, candidate, direction); - if (distance < minDistance) { - minDistance = distance; - nearest = candidate; - } - } - - return nearest; -} - -bool FocusNavigator::isInDirection( - QWidget* from, - QWidget* to, - FocusDirection direction -) const { - QRect fromRect = from->geometry(); - QRect toRect = to->geometry(); - QPointF fromCenter = fromRect.center(); - QPointF toCenter = toRect.center(); - - switch (direction) { - case FocusDirection::Up: - return toCenter.y() < fromCenter.y(); - case FocusDirection::Down: - return toCenter.y() > fromCenter.y(); - case FocusDirection::Left: - return toCenter.x() < fromCenter.x(); - case FocusDirection::Right: - return toCenter.x() > fromCenter.x(); - default: - return true; - } -} -```yaml - ---- - -## 十、原生输入设备 - -### 10.1 Evdev 设备 - -**文件**: `src/input/native/EvdevDevice.h` - -```cpp -#pragma once -#include -#include -#include "InputDevice.h" - -namespace CFDesktop::Base { - -/** - * @brief Linux evdev 输入设备 - * - * 直接读取 /dev/input/eventX 设备。 - * 支持按键、相对轴(鼠标)、绝对轴(触摸屏)。 - */ -class EvdevDevice : public InputDevice { - Q_OBJECT - -public: - /** - * @brief 设备类型 - */ - enum class EvdevType { - Unknown, - Keyboard, - Mouse, - Touchscreen, - Joystick, - Accelerometer - }; - - /** - * @brief 打开设备 - */ - static EvdevDevice* open(const QString& devicePath); - - ~EvdevDevice() override; - - /** - * @brief 开始读取事件 - */ - bool start() override; - - /** - * @brief 停止读取事件 - */ - void stop() override; - - /** - * @brief 设备类型 - */ - EvdevType evdevType() const; - - /** - * @brief 设备名称 - */ - QString deviceName() const; - -signals: - /** - * @brief evdev 原始事件信号 - */ - void rawEvent(quint16 type, quint16 code, qint32 value); - -private: - EvdevDevice(const QString& path, int fd, QObject* parent = nullptr); - - void readEvents(); - void identifyDevice(); - -private: - QString m_devicePath; - int m_fd = -1; - EvdevType m_type = EvdevType::Unknown; - QString m_name; - QSocketNotifier* m_notifier = nullptr; -}; - -} // namespace CFDesktop::Base -```text - -### 10.2 GPIO 按键 - -**文件**: `src/input/native/GPIOButton.h` - -```cpp -#pragma once -#include -#include "InputDevice.h" - -namespace CFDesktop::Base { - -/** - * @brief GPIO 按键设备 - * - * 通过 sysfs 或 libgpiod 读取 GPIO 按键。 - */ -class GPIOButton : public InputDevice { - Q_OBJECT - -public: - /** - * @brief 打开 GPIO 按键 - * @param gpioPin GPIO 引脚号 - * @param activeLow 低电平有效 - */ - static GPIOButton* open(int gpioPin, bool activeLow = true); - - ~GPIOButton() override; - - bool start() override; - void stop() override; - - /** - * @brief 关联的 Qt 键码 - */ - int mappedKeyCode() const; - void setMappedKeyCode(int keyCode); - - /** - * @brief 防抖延迟 (毫秒) - */ - int debounceDelay() const; - void setDebounceDelay(int milliseconds); - -signals: - /** - * @brief 按钮按下信号 - */ - void pressed(); - - /** - * @brief 按钮释放信号 - */ - void released(); - -private: - GPIOButton(int gpioPin, QObject* parent = nullptr); - - void handleEdgeDetect(); - void setupGPIO(); - -private: - int m_gpioPin; - bool m_activeLow = true; - int m_mappedKeyCode = Qt::Key_unknown; - int m_debounceDelay = 50; - QFile m_valueFile; - QTimer* m_debounceTimer = nullptr; - bool m_lastState = false; -}; - -} // namespace CFDesktop::Base -```text - -### 10.3 旋转编码器 - -**文件**: `src/input/native/RotaryEncoder.h` - -```cpp -#pragma once -#include -#include "InputDevice.h" - -namespace CFDesktop::Base { - -/** - * @brief 旋转编码器设备 - * - * 支持 AB 相位编码器。 - */ -class RotaryEncoder : public InputDevice { - Q_OBJECT - -public: - /** - * @brief 打开旋转编码器 - * @param pinA A 相引脚 - * @param pinB B 相引脚 - */ - static RotaryEncoder* open(int pinA, int pinB); - - ~RotaryEncoder() override; - - bool start() override; - void stop() override; - - /** - * @brief 每圈步数 - */ - void setStepsPerRevolution(int steps); - - /** - * @brief 是否反转方向 - */ - void setInverted(bool inverted); - -signals: - /** - * @brief 旋转信号 - */ - void rotated(int steps); - - /** - * @brief 按钮信号(如果带按钮) - */ - void buttonPressed(); - void buttonReleased(); - -private: - RotaryEncoder(int pinA, int pinB, QObject* parent = nullptr); - - void decodeState(int stateA, int stateB); - -private: - int m_pinA; - int m_pinB; - int m_stepsPerRevolution = 24; - bool m_inverted = false; - int m_lastState = 0; - int m_position = 0; -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -## 十一、模拟器支持 - -### 11.1 模拟输入配置 - -```cpp -namespace CFDesktop::Base { - -/** - * @brief 模拟输入配置 - */ -struct SimulatedInputConfig { - // 鼠标模拟触摸 - bool mouseEmulatesTouch = true; - - // 键盘模拟方向键 - QHash keyToDirection = { - { Qt::Key_Up, FocusDirection::Up }, - { Qt::Key_Down, FocusDirection::Down }, - { Qt::Key_Left, FocusDirection::Left }, - { Qt::Key_Right, FocusDirection::Right }, - { Qt::Key_Tab, FocusDirection::Next }, - { Qt::Key_Backtab, FocusDirection::Previous } - }; - - // 触摸视觉反馈 - bool showTouchRipple = true; - int rippleDuration = 300; - - // 旋钮模拟 - bool mouseWheelEmulatesRotary = true; - qreal wheelScale = 1.0; -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -## 十二、详细任务清单 - -### 12.1 Week 1: 核心处理器 - -#### Day 1-2: InputManager 基础 -- [ ] 创建 InputManager 类 -- [ ] 实现设备注册/注销 -- [ ] 实现事件分发机制 -- [ ] 添加事件过滤器支持 - -#### Day 3: TouchInputHandler -- [ ] 创建 TouchInputHandler 类 -- [ ] 实现触摸点跟踪 -- [ ] 实现单击/双击检测 -- [ ] 实现长按检测 - -#### Day 4: KeyInputHandler -- [ ] 创建 KeyInputHandler 类 -- [ ] 实现按键状态跟踪 -- [ ] 实现长按检测 -- [ ] 实现连击检测 - -#### Day 5: RotaryInputHandler -- [ ] 创建 RotaryInputHandler 类 -- [ ] 实现旋转解码 -- [ ] 实现速度计算 -- [ ] 实现加速功能 - -### 12.2 Week 2: 手势与导航 - -#### Day 1-2: GestureRecognizer -- [ ] 创建 GestureRecognizer 类 -- [ ] 实现滑动手势 -- [ ] 实现捏合手势 -- [ ] 实现旋转手势 - -#### Day 3: FocusNavigator -- [ ] 创建 FocusNavigator 类 -- [ ] 实现方向导航算法 -- [ ] 实现焦点链自定义 -- [ ] 实现边界策略 - -#### Day 4: 原生设备 -- [ ] 实现 EvdevDevice -- [ ] 实现 GPIOButton -- [ ] 实现 RotaryEncoder - -#### Day 5: 测试与集成 -- [ ] 编写单元测试 -- [ ] 集成测试 -- [ ] 性能测试 - ---- - -## 十三、验收标准 - -### 13.1 功能验收 -- [ ] 触摸输入正常响应 -- [ ] 手势识别准确率 > 95% -- [ ] 焦点导航无死循环 -- [ ] 原生设备正常读取 - -### 13.2 性能验收 -- [ ] 事件延迟 < 16ms -- [ ] CPU 占用 < 5% - -### 13.3 兼容性验收 -- [ ] 模拟器和真机行为一致 - ---- +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| 统一 InputEvent 体系 (PointerEvent/KeyEvent/RotaryEvent/GestureEvent) | Widget 只需对接一套接口;新增输入类型只需加 Handler | 让 Widget 直接处理 Qt 原生事件 + evdev 并行 | +| InputManager 单例 + 事件过滤器链 | 集中管理设备生命周期,支持全局事件拦截(如全局手势) | 每个 Widget 独立注册输入源 | +| 手势识别器独立于 TouchHandler | 解耦触摸处理与手势判定,可单独禁用/替换手势引擎 | 将手势检测逻辑内嵌在 TouchHandler 中 | +| 空间距离焦点导航算法 | 工控屏布局不规则,Tab 顺序不够直观;空间距离更符合用户预期 | 仅使用 Qt Tab 顺序 | +| 原生设备层 (evdev/GPIO/RotaryEncoder) 直接读取 | linuxfb/eglfs 平台下 Qt 不一定转发所有输入;直接读取保证可靠性 | 完全依赖 Qt 平台插件转发 | +| 模拟器输入映射 (鼠标->触摸, 滚轮->旋钮) | 开发机没有触摸/旋钮硬件,映射确保桌面端可调试嵌入式行为 | 要求必须有触摸硬件才能开发 | -## 十四、下一步行动 +## 当前状态 -完成 Phase 3 后,进入 **Phase 4: Shell UI 主体**。 +未实现。参见 [status/current.md](../status/current.md) Phase 3 部分。 diff --git a/document/design_stage/04_phase6_simulator.md b/document/design_stage/04_phase6_simulator.md index d14c87bfe..1c8614eea 100644 --- a/document/design_stage/04_phase6_simulator.md +++ b/document/design_stage/04_phase6_simulator.md @@ -1,1020 +1,28 @@ --- title: "Phase 6: 多平台模拟器详细设计文档" -description: "完成 Phase 6 后,进入 Phase 4: Shell UI 主体 或 Phase 5: SD" +description: "在桌面端还原嵌入式设备 UI 效果,实现所见即所得开发体验" --- -# Phase 6: 多平台模拟器详细设计文档 +# Phase 6: 多平台模拟器 -- 设计意图 -## 文档信息 -| 项目 | 内容 | -|------|------| -| 文档版本 | v1.0 | -| 创建日期 | 2026-02-20 | -| 阶段代号 | Phase 6 - 多平台模拟器 | -| 预计周期 | 2~3 周 | -| 依赖阶段 | Phase 0, Phase 2, Phase 3 | +## 为什么选择这种方案 ---- - -## 一、阶段目标 - -### 1.1 核心目标 -在 Windows/Ubuntu 上通过 Qt Creator 还原真实嵌入式设备的 UI 效果,实现"所见即所得"的开发体验。 - -### 1.2 具体交付物 -- [ ] `SimulatorWindow` 模拟器主窗口 -- [ ] `DeviceFrame` 设备外壳渲染 -- [ ] `TouchVisualizer` 触摸可视化 -- [ ] `HWTierSelector` 硬件档位选择器 -- [ ] `ResolutionPreset` 分辨率预设管理 -- [ ] 独立可执行文件 `cfdesktop-sim` - ---- - -## 二、模块架构设计 - -### 2.1 整体架构图 - -```text -┌───────────────────────────────────────────────────────────────┐ -│ Simulator Window │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Device Frame │ │ -│ │ ┌───────────────────────────────────────────────────┐ │ │ -│ │ │ Shell UI Content │ │ │ -│ │ │ (共享真实设备的 Shell 代码) │ │ │ -│ │ │ │ │ │ -│ │ │ [Launcher] [WindowManager] [StatusBar] │ │ │ -│ │ └───────────────────────────────────────────────────┘ │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Control Panel │ │ -│ │ [Device Selector] [Resolution] [HWTier] [Touch Viz] │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└───────────────────────────────────────────────────────────────┘ - │ - ▼ -┌───────────────────────────────────────────────────────────────┐ -│ Injection Layer │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ -│ │ DPIManager │ │HardwareProbe │ │ InputManager │ │ -│ │ Injection │ │ Mock │ │ Simulation │ │ -│ └──────────────┘ └──────────────┘ └──────────────────┘ │ -└───────────────────────────────────────────────────────────────┘ - │ - ▼ -┌───────────────────────────────────────────────────────────────┐ -│ Shared Base & Shell │ -│ (与真实设备完全相同的代码) │ -└───────────────────────────────────────────────────────────────┘ -```text - -### 2.2 文件结构 - -```text -src/simulator/ -├── include/CFDesktop/Simulator/ -│ ├── SimulatorWindow.h # 模拟器主窗口 -│ ├── DeviceFrame.h # 设备外壳 -│ ├── DeviceProfile.h # 设备配置文件 -│ ├── TouchVisualizer.h # 触摸可视化 -│ ├── ControlPanel.h # 控制面板 -│ ├── HWTierSelector.h # 硬件档位选择器 -│ ├── ResolutionPreset.h # 分辨率预设 -│ └── SimulatorInjection.h # 注入接口 -│ -└── src/ - ├── SimulatorWindow.cpp - ├── DeviceFrame.cpp - ├── TouchVisualizer.cpp - ├── ControlPanel.cpp - ├── HWTierSelector.cpp - ├── ResolutionPreset.cpp - └── injection/ - ├── DPIInjector.cpp # DPI 注入 - ├── HardwareMock.cpp # 硬件 Mock - └── InputSimulator.cpp # 输入模拟 - -assets/simulator/ -├── devices/ # 设备外壳图片 -│ ├── phone-generic.png -│ ├── tablet-10inch.png -│ ├── panel-7inch.png -│ ├── panel-10inch.png -│ └── custom/ -│ └── device.svg -├── profiles/ # 设备配置 -│ ├── imx6ull-4.3.json -│ ├── imx6ull-7.0.json -│ ├── rk3568-7.0.json -│ ├── rk3568-10.1.json -│ ├── rk3588-10.1.json -│ └── generic-1080p.json -└── effects/ - └── touch-ripple.png # 触摸涟漪效果 -```yaml - ---- - -## 三、模拟器主窗口 (SimulatorWindow) - -### 3.1 SimulatorWindow 类接口 - -**文件**: `include/CFDesktop/Simulator/SimulatorWindow.h` - -```cpp -#pragma once -#include -#include -#include "DeviceProfile.h" - -namespace CFDesktop::Simulator { - -class DeviceFrame; -class ControlPanel; -class TouchVisualizer; -class DPIInjector; -class HardwareMock; - -/** - * @brief 模拟器主窗口 - * - * 提供完整的嵌入式设备模拟环境。 - */ -class SimulatorWindow : public QMainWindow { - Q_OBJECT - -public: - explicit SimulatorWindow(QWidget* parent = nullptr); - ~SimulatorWindow(); - - /** - * @brief 加载设备配置 - */ - bool loadDeviceProfile(const QString& profilePath); - - /** - * @brief 获取当前设备配置 - */ - DeviceProfile currentProfile() const; - - /** - * @brief 设置显示内容 - * - * 通常设置为 Shell UI 的主窗口。 - */ - void setContentWidget(QWidget* content); - - /** - * @brief 获取内容区域 - */ - QWidget* contentWidget() const; - - /** - * @brief 显示/隐藏控制面板 - */ - void setControlPanelVisible(bool visible); - bool isControlPanelVisible() const; - - /** - * @brief 显示/隐藏设备外壳 - */ - void setDeviceFrameVisible(bool visible); - bool isDeviceFrameVisible() const; - - /** - * @brief 启用/禁用触摸可视化 - */ - void setTouchVisualizationEnabled(bool enabled); - bool isTouchVisualizationEnabled() const; - -public slots: - /** - * @brief 切换全屏模式 - */ - void toggleFullscreen(); - - /** - * @brief 重置模拟状态 - */ - void resetSimulation(); - - /** - * @brief 截图 - */ - void takeScreenshot(); - -signals: - /** - * @brief 设备配置变化信号 - */ - void profileChanged(const DeviceProfile& profile); - - /** - * @brief 分辨率变化信号 - */ - void resolutionChanged(const QSize& resolution); - - /** - * @brief 硬件档位变化信号 - */ - void hwTierChanged(HWTier tier); - -private: - void setupUI(); - void setupConnections(); - void loadDefaultProfile(); - -private: - QScopedPointer m_deviceFrame; - QScopedPointer m_controlPanel; - QScopedPointer m_touchVisualizer; - QScopedPointer m_dpiInjector; - QScopedPointer m_hardwareMock; - - DeviceProfile m_currentProfile; - QPointer m_contentWidget; -}; - -} // namespace CFDesktop::Simulator -```text - -### 3.2 窗口布局 - -```text -┌─────────────────────────────────────────────────────────────┐ -│ Simulator Window │ -├─────────────────────────────────────────────────────────────┤ -│ ┌───┐ │ -│ │ │ ┌───────────────────────────────────────────────┐ │ -│ │ D │ │ │ │ -│ │ e │ │ Device Frame (外壳) │ │ -│ │ v │ │ ┌─────────────────────────────────────────┐ │ │ -│ │ i │ │ │ │ │ │ -│ │ c │ │ │ Screen Content Area │ │ │ -│ │ e │ │ │ (Shell UI 渲染) │ │ │ -│ │ │ │ │ │ │ │ -│ │ F │ │ │ │ │ │ -│ │ r │ │ │ │ │ │ -│ │ a │ │ │ │ │ │ -│ │ m │ │ │ │ │ │ -│ │ e │ │ └─────────────────────────────────────────┘ │ │ -│ └───┘ │ │ │ -│ └───────────────────────────────────────────────┘ │ -├─────────────────────────────────────────────────────────────┤ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Control Panel │ │ -│ │ [Device▼] [800×480▼] [Low▼] [Touch:ON] [Screenshot] │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -```yaml - ---- - -## 四、设备配置文件 (DeviceProfile) - -### 4.1 DeviceProfile 数据结构 - -**文件**: `include/CFDesktop/Simulator/DeviceProfile.h` - -```cpp -#pragma once -#include -#include -#include -#include "../Base/HardwareProbe/HWTier.h" - -namespace CFDesktop::Simulator { - -/** - * @brief 设备类型 - */ -enum class DeviceType { - Phone, # 手机 - Tablet, # 平板 - Panel, # 工控屏 - Custom # 自定义 -}; - -/** - * @brief 设备配置 - */ -struct DeviceProfile { - // 基本信息 - QString name; # 设备名称 - QString manufacturer; # 制造商 - QString model; # 型号 - DeviceType type = DeviceType::Panel; - - // 屏幕参数 - QSize screenSize; # 屏幕分辨率 - QSizeF physicalSize; # 物理尺寸 (毫米) - qreal dpi = 96.0; # DPI - qreal devicePixelRatio = 1.0; # 设备像素比 - - // 屏幕区域 (在外壳中的位置和大小) - QRect screenRect; # 屏幕区域 - - // 设备外壳 - QString frameImagePath; # 外壳图片路径 - QRect frameRect; # 外壳区域 - QColor frameColor; # 外壳颜色 (无图片时) - int frameCornerRadius = 0; # 外壳圆角 - - // 硬件能力 - HWTier hwTier = HWTier::Low; # 硬件档位 - - // 输入配置 - bool hasTouchscreen = true; - bool hasHardwareButtons = false; - bool hasRotaryEncoder = false; - - // 其他配置 - QString description; - QString version = "1.0"; - - /** - * @brief 从 JSON 加载 - */ - static DeviceProfile fromJson(const QString& path); - - /** - * @brief 保存到 JSON - */ - bool toJson(const QString& path) const; - - /** - * @brief 获取默认配置 - */ - static DeviceProfile defaultProfile(); -}; - -} // namespace CFDesktop::Simulator -```text - -### 4.2 设备配置 JSON 示例 - -**文件**: `assets/simulator/profiles/imx6ull-4.3.json` - -```json -{ - "name": "IMX6ULL 4.3 inch Panel", - "manufacturer": "NXP", - "model": "IMX6ULL-EVK", - "type": "panel", - - "screen": { - "size": { "width": 480, "height": 272 }, - "physicalSize": { "width": 95.0, "height": 54.0 }, - "dpi": 125.0, - "devicePixelRatio": 1.0, - "rect": { "x": 20, "y": 20, "width": 480, "height": 272 } - }, - - "frame": { - "image": "devices/panel-4.3.png", - "rect": { "x": 0, "y": 0, "width": 520, "height": 312 }, - "color": "#333333", - "cornerRadius": 8 - }, - - "hardware": { - "tier": "low" - }, - - "input": { - "touchscreen": true, - "hardwareButtons": true, - "rotaryEncoder": false - }, - - "description": "4.3 inch WVGA panel with IMX6ULL", - "version": "1.0" -} -```bash - -### 4.3 预设设备配置列表 - -| 配置文件 | 设备 | 分辨率 | DPI | 档位 | -|----------|------|--------|-----|------| -| `imx6ull-4.3.json` | 4.3寸工控屏 | 480×272 | 125 | Low | -| `imx6ull-7.0.json` | 7.0寸工控屏 | 800×480 | 133 | Low | -| `rk3568-7.0.json` | 7.0寸工控屏 | 1024×600 | 170 | Mid | -| `rk3568-10.1.json` | 10.1寸平板 | 1280×800 | 149 | Mid | -| `rk3588-10.1.json` | 10.1寸高端屏 | 1920×1200 | 224 | High | - ---- - -## 五、设备外壳 (DeviceFrame) - -### 5.1 DeviceFrame 类接口 - -**文件**: `include/CFDesktop/Simulator/DeviceFrame.h` - -```cpp -#pragma once -#include -#include "DeviceProfile.h" - -namespace CFDesktop::Simulator { - -/** - * @brief 设备外壳渲染 - * - * 渲染设备外壳和屏幕区域。 - */ -class DeviceFrame : public QWidget { - Q_OBJECT - -public: - explicit DeviceFrame(QWidget* parent = nullptr); - ~DeviceFrame() = default; - - /** - * @brief 设置设备配置 - */ - void setDeviceProfile(const DeviceProfile& profile); - DeviceProfile deviceProfile() const; - - /** - * @brief 获取屏幕区域 Widget - * - * Shell UI 内容应该作为这个 Widget 的子控件。 - */ - QWidget* screenContainer() const; - - /** - * @brief 绘制模式 - */ - enum class RenderMode { - Image, # 使用图片 - Vector, # 矢量绘制 - Simple # 简单边框 - }; - void setRenderMode(RenderMode mode); - -protected: - void paintEvent(QPaintEvent* event) override; - -private: - void drawFrameImage(QPainter& painter); - void drawFrameVector(QPainter& painter); - void drawFrameSimple(QPainter& painter); - void updateLayout(); - -private: - DeviceProfile m_profile; - RenderMode m_renderMode = RenderMode::Image; - QPixmap m_frameImage; - QPointer m_screenContainer; -}; - -} // namespace CFDesktop::Simulator -```text - -### 5.2 矢量绘制示例 - -```cpp -void DeviceFrame::drawFrameVector(QPainter& painter) { - painter.setRenderHint(QPainter::Antialiasing); - - // 外壳背景 - QPainterPath framePath; - framePath.addRoundedRect( - m_profile.frameRect, - m_profile.frameCornerRadius, - m_profile.frameCornerRadius - ); - - painter.fillPath(framePath, m_profile.frameColor); - - // 屏幕边框 - QPen borderPen(QColor(0, 0, 0, 100), 2); - painter.setPen(borderPen); - painter.drawRoundedRect( - m_profile.screenRect.adjusted(-2, -2, 2, 2), - 4, 4 - ); - - // 屏幕阴影 - QRect screenShadowRect = m_profile.screenRect; - screenShadowRect.translate(2, 2); - painter.setPen(Qt::NoPen); - painter.setBrush(QColor(0, 0, 0, 50)); - painter.drawRoundedRect(screenShadowRect, 4, 4); - - // 硬件按键 - if (m_profile.hasHardwareButtons) { - drawHardwareButtons(painter); - } -} -```yaml - ---- - -## 六、触摸可视化 (TouchVisualizer) - -### 6.1 TouchVisualizer 类接口 - -**文件**: `include/CFDesktop/Simulator/TouchVisualizer.h` - -```cpp -#pragma once -#include -#include -#include -#include - -namespace CFDesktop::Simulator { - -/** - * @brief 触摸点可视化数据 - */ -struct TouchPointVisual { - int id; - QPointF position; - qreal pressure = 0.0; - qint64 startTime = 0; - qreal radius = 20.0; - QColor color = QColor(0, 150, 255, 150); -}; - -/** - * @brief 触摸可视化 - * - * 在点击位置显示涟漪效果。 - */ -class TouchVisualizer : public QWidget { - Q_OBJECT - -public: - explicit TouchVisualizer(QWidget* parent = nullptr); - ~TouchVisualizer() = default; - - /** - * @brief 启用/禁用 - */ - void setEnabled(bool enabled); - bool isEnabled() const; - - /** - * @brief 添加触摸点 - */ - void addTouchPoint(int id, const QPointF& position); - - /** - * @brief 更新触摸点 - */ - void updateTouchPoint(int id, const QPointF& position); - - /** - * @brief 移除触摸点 - */ - void removeTouchPoint(int id); - - /** - * @brief 设置涟漪效果时长 - */ - void setRippleDuration(int milliseconds); +CFDesktop 的目标设备包括 IMX6ULL (480x272)、RK3568 (1024x600)、RK3588 (1920x1200) 等差异极大的嵌入式屏幕。开发者日常在 Windows/Ubuntu 桌面机上工作,如果没有模拟器,每次 UI 调整都必须烧录固件到真机验证,迭代周期从秒级拉长到分钟级。 - /** - * @brief 设置涟漪颜色 - */ - void setRippleColor(const QColor& color); +核心思路是 **DeviceFrame + 注入层**:模拟器窗口由 `DeviceFrame`(设备外壳渲染,提供真实屏幕边框和尺寸感)包裹 Shell UI 内容,下方 `ControlPanel` 提供设备/分辨率/档位切换。关键在于 Shell UI 代码与真机完全共享——模拟器不做任何 UI 级别的模拟,而是通过注入层(DPIInjector / HardwareMock / InputSimulator)将模拟的屏幕参数、硬件信息、输入事件注入到 base 层。这样模拟器中看到的 UI 效果与真机一致,且切换设备配置时 DPI、主题降级、动画开关等行为即时生效。 -protected: - void paintEvent(QPaintEvent* event) override; +触摸可视化(TouchVisualizer)通过涟漪动画显示触摸点位置和轨迹,帮助开发者在鼠标模拟触摸时直观确认输入行为。设备配置以 JSON 文件描述(DeviceProfile),预设覆盖主流嵌入式屏幕,也可自定义扩展。 -private: - void startRippleAnimation(const QPointF& position); - void cleanupExpiredTouches(); +## 关键决策 -private: - bool m_enabled = true; - int m_rippleDuration = 300; - QColor m_ippleColor; - QHash m_activeTouches; - QList> m_activeAnimations; -}; - -} // namespace CFDesktop::Simulator -```yaml - ---- - -## 七、控制面板 (ControlPanel) - -### 7.1 ControlPanel 类接口 - -**文件**: `include/CFDesktop/Simulator/ControlPanel.h` - -```cpp -#pragma once -#include -#include -#include -#include - -namespace CFDesktop::Simulator { - -/** - * @brief 模拟器控制面板 - * - * 提供设备选择、分辨率切换、档位切换等功能。 - */ -class ControlPanel : public QWidget { - Q_OBJECT - -public: - explicit ControlPanel(QWidget* parent = nullptr); - ~ControlPanel() = default; - - /** - * @brief 设置可用设备配置列表 - */ - void setAvailableProfiles(const QList& profiles); - - /** - * @brief 设置当前配置 - */ - void setCurrentProfile(const DeviceProfile& profile); - - /** - * @brief 设置当前分辨率 - */ - void setResolution(const QSize& resolution); - - /** - * @brief 设置当前硬件档位 - */ - void setHWTier(HWTier tier); - -signals: - /** - * @brief 设备配置变化 - */ - void profileChanged(const DeviceProfile& profile); - - /** - * @brief 分辨率变化 - */ - void resolutionChanged(const QSize& resolution); - - /** - * @brief 硬件档位变化 - */ - void hwTierChanged(HWTier tier); - - /** - * @brief 触摸可视化开关变化 - */ - void touchVisualizationChanged(bool enabled); - - /** - * @brief 请求截图 - */ - void screenshotRequested(); - - /** - * @brief 请求重置 - */ - void resetRequested(); - - /** - * @brief 请求全屏 - */ - void fullscreenRequested(); - -private: - void setupUI(); - void loadProfilesFromDirectory(); - -private: - QComboBox* m_profileCombo; - QComboBox* m_resolutionCombo; - QComboBox* m_hwTierCombo; - QCheckBox* m_touchVizCheck; - QPushButton* m_screenshotButton; - QPushButton* m_resetButton; - QPushButton* m_fullscreenButton; - - QList m_availableProfiles; -}; - -} // namespace CFDesktop::Simulator -```text - -### 7.2 硬件档位选择器 (HWTierSelector) - -**文件**: `include/CFDesktop/Simulator/HWTierSelector.h` - -```cpp -#pragma once -#include -#include -#include - -namespace CFDesktop::Simulator { - -/** - * @brief 硬件档位选择器 - * - * 允许在模拟器中切换硬件档位,验证降级行为。 - */ -class HWTierSelector : public QWidget { - Q_OBJECT - -public: - explicit HWTierSelector(QWidget* parent = nullptr); - ~HWTierSelector() = default; - - void setCurrentTier(HWTier tier); - HWTier currentTier() const; - -signals: - void tierChanged(HWTier tier); - -private: - QButtonGroup* m_tierGroup; - QRadioButton* m_lowRadio; - QRadioButton* m_midRadio; - QRadioButton* m_highRadio; -}; - -} // namespace CFDesktop::Simulator -```yaml - ---- - -## 八、注入接口 - -### 8.1 DPI 注入器 - -**文件**: `src/simulator/injection/DPIInjector.cpp` - -```cpp -namespace CFDesktop::Simulator { - -/** - * @brief DPI 注入器 - * - * 向 DPIManager 注入模拟的屏幕参数。 - */ -class DPIInjector : public QObject { - Q_OBJECT - -public: - explicit DPIInjector(QObject* parent = nullptr); - - /** - * @brief 注入屏幕参数 - */ - void inject( - const QSize& resolution, - qreal dpi, - qreal devicePixelRatio - ); - - /** - * @brief 清除注入 - */ - void clear(); - - /** - * @brief 是否已注入 - */ - bool isInjected() const; - -private: - bool m_injected = false; -}; - -} // namespace CFDesktop::Simulator -```text - -### 8.2 硬件 Mock - -**文件**: `src/simulator/injection/HardwareMock.cpp` - -```cpp -namespace CFDesktop::Simulator { - -/** - * @brief 硬件 Mock - * - * 向 HardwareProbe 注入模拟的硬件信息。 - */ -class HardwareMock : public QObject { - Q_OBJECT - -public: - explicit HardwareMock(QObject* parent = nullptr); - - /** - * @brief 注入硬件信息 - */ - void injectHardwareInfo(const HardwareInfo& info); - - /** - * @brief 快速注入档位 - */ - void injectTier(HWTier tier); - - /** - * @brief 清除 Mock - */ - void clear(); - -signals: - /** - * @brief 档位变化信号 - */ - void tierChanged(HWTier tier); - -private: - bool m_active = false; -}; - -} // namespace CFDesktop::Simulator -```text - -### 8.3 输入模拟器 - -**文件**: `src/simulator/injection/InputSimulator.cpp` - -```cpp -namespace CFDesktop::Simulator { - -/** - * @brief 输入模拟器配置 - */ -struct InputSimulationConfig { - bool mouseEmulatesTouch = true; - bool keyboardEmulatesButtons = true; - bool mouseWheelEmulatesRotary = true; - bool showTouchFeedback = true; -}; - -/** - * @brief 输入模拟器 - * - * 将鼠标/键盘输入转换为触摸/按键事件。 - */ -class InputSimulator : public QObject { - Q_OBJECT - -public: - explicit InputSimulator(QObject* parent = nullptr); - - void setConfig(const InputSimulationConfig& config); - InputSimulationConfig config() const; - - void setEnabled(bool enabled); - -protected: - bool eventFilter(QObject* watched, QEvent* event) override; - -private: - bool handleMouseEvent(QMouseEvent* event); - bool handleWheelEvent(QWheelEvent* event); - bool handleKeyEvent(QKeyEvent* event); - PointerEvent* createTouchEvent(QMouseEvent* event); - -private: - InputSimulationConfig m_config; - bool m_enabled = true; - QPointF m_lastTouchPosition; -}; - -} // namespace CFDesktop::Simulator -```yaml - ---- - -## 九、详细任务清单 - -### 9.1 Week 1: 基础框架 - -#### Day 1-2: 主窗口与设备外壳 -- [ ] 创建 SimulatorWindow 类 -- [ ] 实现 DeviceFrame 基础绘制 -- [ ] 实现矢量外壳绘制 -- [ ] 支持图片外壳 - -#### Day 3: 设备配置 -- [ ] 定义 DeviceProfile 结构 -- [ ] 实现 JSON 序列化 -- [ ] 创建预设配置文件 -- [ ] 实现配置加载 - -#### Day 4: 控制面板 -- [ ] 创建 ControlPanel 类 -- [ ] 实现设备选择器 -- [ ] 实现分辨率选择器 -- [ ] 实现档位选择器 - -#### Day 5: 集成测试 -- [ ] 集成各模块 -- [ ] 基本功能测试 - -### 9.2 Week 2: 注入与模拟 - -#### Day 1-2: DPI 注入 -- [ ] 实现 DPI 注入器 -- [ ] 修改 DPIManager 支持注入 -- [ ] 测试不同分辨率 - -#### Day 3: 硬件 Mock -- [ ] 实现硬件信息 Mock -- [ ] 修改 HardwareProbe 支持 Mock -- [ ] 测试档位切换 - -#### Day 4: 输入模拟 -- [ ] 实现输入模拟器 -- [ ] 鼠标转触摸 -- [ ] 键盘转按键 - -#### Day 5: 触摸可视化 -- [ ] 实现 TouchVisualizer -- [ ] 涟漪动画 -- [ ] 多点触摸支持 - -### 9.3 Week 3: 完善与优化 - -#### Day 1-2: UI 完善 -- [ ] 完善设备外壳 -- [ ] 添加更多预设 -- [ ] 美化控制面板 - -#### Day 3: 高级功能 -- [ ] 截图功能 -- [ ] 录屏功能 -- [ ] 性能监控 - -#### Day 4: 文档 -- [ ] 使用说明 -- [ ] API 文档 -- [ ] 示例代码 - -#### Day 5: 测试 -- [ ] 跨平台测试 -- [ ] 性能测试 -- [ ] 修复 Bug - ---- - -## 十、验收标准 - -### 10.1 功能验收 -- [ ] 能正确显示各种分辨率 -- [ ] 档位切换生效 -- [ ] 触摸模拟正常 -- [ ] 截图功能正常 - -### 10.2 性能验收 -- [ ] 启动时间 < 2 秒 -- [ ] 帧率稳定 60fps - -### 10.3 兼容性验收 -- [ ] Windows 10/11 正常运行 -- [ ] Ubuntu 20.04+ 正常运行 -- [ ] macOS 12+ 正常运行 - ---- - -## 十一、使用示例 - -### 11.1 启动模拟器 - -```cpp -int main(int argc, char* argv[]) { - QApplication app(argc, argv); - - // 创建模拟器窗口 - SimulatorWindow simulator; - simulator.show(); - - // 创建 Shell UI - ShellWindow* shell = new ShellWindow(); - simulator.setContentWidget(shell); - - return app.exec(); -} -```text - -### 11.2 切换设备配置 - -```cpp -// 加载新配置 -simulator.loadDeviceProfile("/path/to/imx6ull-7.0.json"); - -// 或者通过控制面板选择 -simulator.controlPanel()->setCurrentProfile("IMX6ULL 7.0 inch"); -```yaml - ---- +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| DeviceFrame 包裹式架构(共享 Shell UI 代码) | 模拟器与真机跑同一份 UI 代码,保证一致性,避免两套代码漂移 | 模拟器独立渲染一套简化 UI | +| 注入层 (DPIInjector/HardwareMock/InputSimulator) | 通过注入模拟参数让 base 层"以为"自己在真机上运行,不侵入业务代码 | 在 Shell UI 层到处加 `#ifdef SIMULATOR` 条件编译 | +| JSON 设备配置文件 (DeviceProfile) | 设备参数可热加载、用户可自定义新增设备,无需重编译 | 硬编码设备参数 | +| ControlPanel 运行时切换设备/档位 | 无需重启即可验证不同分辨率和硬件档位下的 UI 表现 | 每次切换需重启模拟器 | +| 触摸涟漪可视化 | 鼠标模拟触摸时没有触觉反馈,涟漪帮助确认点击位置和手势轨迹 | 不做可视化,依赖日志判断 | -## 十二、下一步行动 +## 当前状态 -完成 Phase 6 后,进入 **Phase 4: Shell UI 主体** 或 **Phase 5: SDK 导出层**。 +未实现。参见 [status/current.md](../status/current.md) Phase 6 部分。 diff --git a/document/design_stage/05_phase8_testing.md b/document/design_stage/05_phase8_testing.md index ec488100f..15486dd17 100644 --- a/document/design_stage/05_phase8_testing.md +++ b/document/design_stage/05_phase8_testing.md @@ -1,912 +1,25 @@ --- title: "Phase 8: 测试体系详细设计文档" -description: "Phase 8: 测试体系详细设计文档 的详细文档" +description: "建立单元测试、集成测试、UI 自动化测试和跨平台 CI 的完整测试体系" --- -# Phase 8: 测试体系详细设计文档 +# Phase 8: 测试体系 -- 设计意图 -## 文档信息 -| 项目 | 内容 | -|------|------| -| 文档版本 | v1.0 | -| 创建日期 | 2026-02-20 | -| 阶段代号 | Phase 8 - 测试体系 | -| 预计周期 | 贯穿全程 | -| 依赖阶段 | 所有阶段 | +## 为什么选择这种方案 ---- - -## 一、阶段目标 - -### 1.1 核心目标 -建立完整的测试体系,确保代码质量和系统稳定性。测试体系覆盖单元测试、集成测试、UI 自动化测试和跨平台 CI。 - -### 1.2 具体交付物 -- [ ] 单元测试框架和测试用例 -- [ ] 集成测试套件 -- [ ] UI 自动化测试 -- [ ] CI/CD 流水线配置 -- [ ] 测试覆盖率报告 -- [ ] 性能测试基准 - ---- - -## 二、测试体系架构 - -### 2.1 测试金字塔 - -```text - /\ - / \ - / E2E \ - / 测试 \ - /________\ - / \ - / \ - / UI 自动化 \ - / 测试 \ - /__________________\ - / \ - / 集成测试 \ - /________________________\ - / \ - / 单元测试 \ - /______________________________\ - / 70% 单元 \ - / 20% 集成 \ - / 10% UI \ - /____________________________________\ -```text - -### 2.2 测试目录结构 - -```text -tests/ -├── unit/ # 单元测试 -│ ├── base/ -│ │ ├── test_hardware_probe.cpp -│ │ ├── test_theme_engine.cpp -│ │ ├── test_animation_manager.cpp -│ │ ├── test_dpi_manager.cpp -│ │ ├── test_config_store.cpp -│ │ └── test_logger.cpp -│ │ -│ ├── sdk/ -│ │ ├── test_widgets.cpp -│ │ ├── test_system_api.cpp -│ │ └── test_app_lifecycle.cpp -│ │ -│ └── shell/ -│ ├── test_launcher.cpp -│ ├── test_window_manager.cpp -│ └── test_status_bar.cpp -│ -├── integration/ # 集成测试 -│ ├── test_base_integration.cpp -│ ├── test_sdk_integration.cpp -│ └── test_shell_integration.cpp -│ -├── ui/ # UI 自动化测试 -│ ├── test_launcher_ui.cpp -│ ├── test_window_manager_ui.cpp -│ └── test_app_switching.cpp -│ -├── performance/ # 性能测试 -│ ├── test_startup_time.cpp -│ ├── test_animation_fps.cpp -│ ├── test_memory_usage.cpp -│ └── benchmarks/ -│ ├── benchmark_theme_load.cpp -│ └── benchmark_input_processing.cpp -│ -├── mock/ # Mock 数据 -│ ├── proc/ -│ │ ├── cpuinfo_imx6ull -│ │ ├── cpuinfo_rk3568 -│ │ ├── meminfo_512mb -│ │ └── meminfo_1gb -│ └── devices/ -│ └── dri/ -│ └── card0 -│ -├── fixtures/ # 测试夹具 -│ ├── test_app.h -│ ├── test_widget.h -│ └── test_theme.h -│ -└── CMakeLists.txt -```yaml - ---- - -## 三、单元测试 - -### 3.1 测试框架配置 - -**文件**: `tests/CMakeLists.txt` - -```cmake -# 启用测试 -enable_testing() - -# Qt6 Test 包 -find_package(Qt6 REQUIRED COMPONENTS Test) - -# 添加测试子目录 -add_subdirectory(unit) -add_subdirectory(integration) -add_subdirectory(ui) -add_subdirectory(performance) -```text - -### 3.2 基础测试模板 - -```cpp -// tests/unit/base/test_hardware_probe.cpp - -#include -#include - -class TestHardwareProbe : public QObject { - Q_OBJECT - -private slots: - void initTestCase(); // 测试套件开始前执行一次 - void cleanupTestCase(); // 测试套件结束后执行一次 - void init(); // 每个测试用例前执行 - void cleanup(); // 每个测试用例后执行 - - // CPU 检测测试 - void testDetectCPU_IMX6ULL(); - void testDetectCPU_RK3568(); - void testDetectCPU_RK3588(); - void testDetectCPU_X86_64(); - - // 档位计算测试 - void testCalculateTier_LowEnd(); - void testCalculateTier_MidRange(); - void testCalculateTier_HighEnd(); - - // Mock 数据测试 - void testWithMockData(); - void testWithInvalidData(); -}; - -void TestHardwareProbe::initTestCase() { - // 设置测试环境 - qputenv("CFDESKTOP_TEST_MODE", "1"); -} - -void TestHardwareProbe::testDetectCPU_IMX6ULL() { - // 设置 Mock 数据路径 - qputenv("CFDESKTOP_MOCK_CPUINFO", - "/path/to/mock/proc/cpuinfo_imx6ull"); - - HardwareProbe probe; - HardwareInfo info = probe.probe(); - - // 验证 CPU 信息 - QCOMPARE(info.cpu.cores, 1); - QCOMPARE(info.cpu.architecture, QString("armv7l")); - QVERIFY(info.cpu.features.contains("neon")); - - // 验证档位 - QCOMPARE(info.tier, HWTier::Low); -} - -QTEST_MAIN(TestHardwareProbe) -#include "test_hardware_probe.moc" -```text - -### 3.3 Mock 系统设计 - -```cpp -// tests/fixtures/test_helpers.h - -namespace CFDesktop::Testing { - -/** - * @brief Mock 系统调用 - */ -class MockSystem { -public: - /** - * @brief Mock /proc/cpuinfo - */ - static void mockCpuinfo(const QString& mockFile); - - /** - * @brief Mock /proc/meminfo - */ - static void mockMeminfo(const QString& mockFile); - - /** - * @brief Mock /sys/class/net/* - */ - static void mockNetwork(const QList& interfaces); - - /** - * @brief 清除所有 Mock - */ - static void clearAll(); - - /** - * @brief 设置模拟屏幕参数 - */ - static void mockScreen(int dpi, qreal dpr, const QSize& size); -}; - -} // namespace CFDesktop::Testing -```bash - -### 3.4 关键单元测试用例清单 - -#### HardwareProbe 测试用例 - -| 测试用例 | 描述 | 预期结果 | -|----------|------|----------| -| `testDetectCPU_IMX6ULL` | IMX6ULL CPU 检测 | 识别为 Low 档 | -| `testDetectCPU_RK3568` | RK3568 CPU 检测 | 识别为 Mid 档 | -| `testDetectCPU_RK3588` | RK3588 CPU 检测 | 识别为 High 档 | -| `testDetectGPU_WithDRM` | DRM 设备检测 | 正确检测 GPU | -| `testDetectGPU_NoDRM` | 无 DRM 设备 | 返回无 GPU | -| `testDetectMemory_512MB` | 512MB 内存检测 | 正确报告内存 | -| `testCalculateTier_ScoreBased` | 评分计算 | 档位正确 | -| `testUserOverride` | 用户配置覆盖 | 使用用户档位 | -| `testCustomScript` | 自定义脚本执行 | 脚本结果生效 | - -#### ThemeEngine 测试用例 - -| 测试用例 | 描述 | 预期结果 | -|----------|------|----------| -| `testLoadDefaultTheme` | 加载默认主题 | 成功加载 | -| `testThemeVariableAccess` | 变量访问 | 返回正确值 | -| `testThemeInheritance` | 主题继承 | 继承生效 | -| `testQSSProcessing` | QSS 变量替换 | 变量被替换 | -| `testThemeHotReload` | 热重载 | 实时生效 | -| `testTierBasedFallback` | 档位降级 | Low 档禁用特效 | - -#### AnimationManager 测试用例 - -| 测试用例 | 描述 | 预期结果 | -|----------|------|----------| -| `testFadeInAnimation` | 淡入动画 | 正常播放 | -| `testLowTierNoAnimation` | Low 档 | 动画时长为 0 | -| `testParallelGroup` | 并行动画组 | 同时播放 | -| `testSequentialGroup` | 串行动画组 | 顺序播放 | -| `testAnimationLifecycle` | 动画生命周期 | 状态正确 | - -#### DPIManager 测试用例 - -| 测试用例 | 描述 | 预期结果 | -|----------|------|----------| -| `testDPConversion` | dp 转 px | 转换正确 | -| `testSPConversion` | sp 转 px | 考虑字体缩放 | -| `testInjection` | 参数注入 | 注入值生效 | -| `testHighDPI` | 高 DPI | 缩放正确 | - ---- - -## 四、集成测试 - -### 4.1 集成测试框架 - -```cpp -// tests/integration/test_base_integration.cpp - -#include -#include -#include -#include - -class TestBaseIntegration : public QObject { - Q_OBJECT - -private slots: - void testHardwareProbeToTheme(); - void testHardwareProbeToAnimation(); - void testThemeToWidget(); - void testFullBaseStack(); -}; - -void TestBaseIntegration::testHardwareProbeToTheme() { - // 1. 硬件检测 - HardwareProbe probe; - probe.setMockData(createLowEndMockInfo()); - HardwareInfo info = probe.probe(); - - // 2. 主题引擎应响应档位 - ThemeEngine* theme = ThemeEngine::instance(); - theme->loadTheme("/path/to/default/theme"); - QColor shadowColor = theme->color("shadow"); - - // Low 档应该禁用阴影 (透明或无色) - if (info.tier == HWTier::Low) { - QVERIFY(!shadowColor.alpha() > 0); - } -} - -void TestBaseIntegration::testFullBaseStack() { - // 完整的 Base 库集成测试 - // 模拟真实启动流程 -} -```text - -### 4.2 测试应用框架 - -```cpp -// tests/fixtures/test_app.h - -namespace CFDesktop::Testing { - -/** - * @brief 测试用应用程序 - * - * 提供独立的测试环境,不影响主应用。 - */ -class TestApplication : public QApplication { -public: - TestApplication(int& argc, char** argv); - - /** - * @brief 使用测试配置初始化 - */ - void initializeWithTestConfig(); - - /** - * @brief 创建测试窗口 - */ - QWidget* createTestWindow(); - - /** - * @brief 模拟用户输入 - */ - void simulateKeyPress(int key); - void simulateMouseClick(const QPoint& pos); - void simulateTouch(const QPointF& pos); - - /** - * @brief 等待条件满足 - */ - bool waitFor(std::function condition, int timeout = 5000); - -private: - void setupTestEnvironment(); - void cleanupTestEnvironment(); -}; - -} // namespace CFDesktop::Testing -```yaml - ---- - -## 五、UI 自动化测试 - -### 5.1 QTest UI 测试 - -```cpp -// tests/ui/test_launcher_ui.cpp - -#include -#include - -class TestLauncherUI : public QObject { - Q_OBJECT - -private slots: - void initTestCase(); - void cleanupTestCase(); - - void testLauncherDisplay(); - void testAppIconClick(); - void testSwipeGesture(); - void testAppLaunch(); - -private: - LauncherWindow* m_launcher; -}; - -void TestLauncherUI::testAppIconClick() { - // 查找应用图标 - QList icons = m_launcher->findChildren(); - QVERIFY(icons.size() > 0); - - AppIconWidget* firstIcon = icons.first(); - - // 模拟点击 - QTest::mouseClick(firstIcon, Qt::LeftButton); - - // 验证应用启动 - QSignalSpy spy(firstIcon, &AppIconWidget::launched); - QVERIFY(spy.wait(1000)); -} - -void TestLauncherUI::testSwipeGesture() { - // 模拟滑动手势 - QWidget* viewport = m_launcher->viewport(); - - QPoint startPos(400, 240); - QPoint endPos(100, 240); - - QTest::mousePress(viewport, Qt::LeftButton, startPos); - QTest::mouseMove(viewport, endPos); - QTest::mouseRelease(viewport, Qt::LeftButton, endPos); - - // 验证页面切换 - QTest::qWait(500); - QCOMPARE(m_launcher->currentPage(), 1); -} -```text - -### 5.2 可访问性测试 - -```cpp -void TestAccessibility::testWidgetNavigation() { - // 验证焦点导航 - QWidget* widget1 = m_window->findChild("widget1"); - QWidget* widget2 = m_window->findChild("widget2"); - - widget1->setFocus(); - QVERIFY(widget1->hasFocus()); - - // 模拟 Tab 键 - QTest::keyClick(m_window, Qt::Key_Tab); - QVERIFY(widget2->hasFocus()); -} - -void TestAccessibility::testScreenReader() { - // 验证可访问性接口 - QAccessibleInterface* iface = QAccessible::queryAccessibleInterface(m_button); - - QCOMPARE(iface->role(), QAccessible::Role::PushButton); - QVERIFY(!iface->text(QAccessible::Name).isEmpty()); -} -```yaml - ---- - -## 六、性能测试 - -### 6.1 基准测试框架 - -```cpp -// tests/performance/benchmarks/benchmark_theme_load.cpp - -#include -#include -#include - -class BenchmarkThemeLoad : public QObject { - Q_OBJECT - -private slots: - void testThemeLoadPerformance(); - void testVariableLookupPerformance(); - void testQSSProcessingPerformance(); -}; +CFDesktop 的测试策略遵循经典测试金字塔:70% 单元测试 + 20% 集成测试 + 10% UI 测试。选择 GoogleTest 作为主框架(项目已在 CLAUDE.md 中明确),配合 Qt6::Test 处理信号/控件相关的测试。这个组合既能测纯逻辑(CPU 检测、主题引擎、动画管理),也能测 Qt 信号槽和 Widget 行为。 -void BenchmarkThemeLoad::testThemeLoadPerformance() { - ThemeEngine* engine = ThemeEngine::instance(); - QString themePath = "/path/to/default/theme"; +测试目录按 `test///_test.cpp` 组织,与源码结构镜像,便于定位。硬件相关测试通过 Mock 系统注入伪造的 /proc/cpuinfo、/proc/meminfo 等文件,避免测试依赖真实硬件。CI 使用 GitHub Actions,单元测试和集成测试在容器中运行,UI 测试通过 Xvfb 虚拟显示执行,性能测试单独 job 跑 Release 构建。 - QElapsedTimer timer; - timer.start(); +## 关键决策 - engine->loadTheme(themePath); - - qint64 elapsed = timer.elapsed(); - - // 加载时间应 < 100ms - QVERIFY(elapsed < 100); - - qDebug() << "Theme load time:" << elapsed << "ms"; -} - -void BenchmarkThemeLoad::testVariableLookupPerformance() { - ThemeEngine* engine = ThemeEngine::instance(); - engine->loadTheme("/path/to/default/theme"); - - const int iterations = 10000; - - QElapsedTimer timer; - timer.start(); - - for (int i = 0; i < iterations; ++i) { - engine->color("primary"); - engine->size("normal"); - engine->font("body"); - } - - qint64 elapsed = timer.elapsed(); - qreal avgTime = static_cast(elapsed) / iterations; - - // 平均查找时间应 < 0.01ms - QVERIFY(avgTime < 0.01); - - qDebug() << "Average variable lookup time:" << avgTime * 1000 << "μs"; -} -```text - -### 6.2 内存使用测试 - -```cpp -// tests/performance/test_memory_usage.cpp - -class TestMemoryUsage : public QObject { - Q_OBJECT - -private slots: - void testThemeEngineMemory(); - void testAnimationManagerMemory(); - void testMultipleWindowsMemory(); - -private: - qint64 getCurrentMemoryUsage(); -}; - -void TestMemoryUsage::testThemeEngineMemory() { - qint64 before = getCurrentMemoryUsage(); - - ThemeEngine* engine = ThemeEngine::instance(); - engine->loadTheme("/path/to/default/theme"); - - qint64 after = getCurrentMemoryUsage(); - qint64 used = after - before; - - // 内存使用应 < 50MB - QVERIFY(used < 50 * 1024 * 1024); - - qDebug() << "Theme engine memory usage:" << used / 1024 << "KB"; -} - -qint64 TestMemoryUsage::getCurrentMemoryUsage() { - // Linux: 读取 /proc/self/status - QFile file("/proc/self/status"); - if (file.open(QIODevice::ReadOnly)) { - QTextStream in(&file); - while (!in.atEnd()) { - QString line = in.readLine(); - if (line.startsWith("VmRSS:")) { - QString value = line.section(':', 1).section('k', 0, 0).trimmed(); - return value.toLongLong() * 1024; - } - } - } - return -1; -} -```bash - -### 6.3 性能基准 - -| 模块 | 指标 | 目标值 | 测试方法 | -|------|------|--------|----------| -| 主题加载 | 加载时间 | < 100ms | 计时测试 | -| 变量查询 | 平均时间 | < 0.01ms | 循环测试 | -| 动画启动 | 启动延迟 | < 16ms | 计时测试 | -| 动画帧率 | 稳定性 | 60fps | 帧率测试 | -| 输入响应 | 延迟 | < 16ms | 事件测试 | -| 内存使用 | 基础占用 | < 100MB | 内存测试 | -| 启动时间 | 冷启动 | < 2s | 计时测试 | - ---- - -## 七、CI/CD 配置 - -### 7.1 GitHub Actions 配置 - -**文件**: `.github/workflows/test.yml` - -```yaml -name: Test Suite - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main ] - -jobs: - unit-tests: - runs-on: ubuntu-latest - container: ghcr.io/cfdesktop/builder-linux:latest - - steps: - - uses: actions/checkout@v3 - - - name: Configure - run: | - cmake -B build \ - -DCMAKE_BUILD_TYPE=Debug \ - -DBUILD_TESTING=ON \ - -DENABLE_COVERAGE=ON - - - name: Build - run: cmake --build build --parallel - - - name: Run Unit Tests - run: | - cd build - ctest --output-on-failure \ - --rerun-failed \ - -j$(nproc) - - - name: Generate Coverage - run: | - cd build - gcovr --xml-pretty --exclude-unreachable-branches \ - --print-summary -o coverage.xml - - - name: Upload Coverage - uses: codecov/codecov-action@v3 - with: - files: ./build/coverage.xml - - integration-tests: - runs-on: ubuntu-latest - needs: unit-tests - - steps: - - uses: actions/checkout@v3 - - - name: Build - run: | - cmake -B build -DBUILD_TESTING=ON - cmake --build build - - - name: Run Integration Tests - run: | - cd build - ctest -R integration --output-on-failure - - ui-tests: - runs-on: ubuntu-latest - needs: unit-tests - - steps: - - uses: actions/checkout@v3 - - - name: Build - run: | - cmake -B build -DBUILD_TESTING=ON - cmake --build build - - - name: Setup Virtual Display - run: | - Xvfb :99 -screen 0 1024x768x24 & - export DISPLAY=:99 - - - name: Run UI Tests - run: | - cd build - ctest -R ui --output-on-failure - - performance-tests: - runs-on: ubuntu-latest - needs: unit-tests - - steps: - - uses: actions/checkout@v3 - - - name: Build - run: | - cmake -B build -DBUILD_TESTING=ON - cmake --build build --release - - - name: Run Performance Tests - run: | - cd build - ctest -R performance --output-on-failure - - - name: Upload Results - uses: actions/upload-artifact@v3 - with: - name: performance-results - path: build/testing/performance/ -```bash - -### 7.2 代码覆盖率要求 - -| 模块 | 最低覆盖率 | 目标覆盖率 | -|------|-----------|-----------| -| HardwareProbe | 90% | 95% | -| ThemeEngine | 85% | 90% | -| AnimationManager | 85% | 90% | -| DPIManager | 85% | 90% | -| ConfigStore | 90% | 95% | -| Logger | 85% | 90% | -| InputManager | 80% | 85% | -| **整体** | **85%** | **90%** | - ---- - -## 八、测试数据管理 - -### 8.1 Mock 数据文件 - -**文件**: `tests/mock/proc/cpuinfo_imx6ull` - -```text -processor : 0 -model name : Freescale i.MX6 UltraLite 528 MHz -BogoMIPS : 264.00 -Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpd32 -CPU implementer : 0x41 -CPU architecture: 7 -CPU variant : 0x0 -CPU part : 0xc07 -CPU revision : 5 - -Hardware : Freescale i.MX6 UltraLite 528 MHz -Revision : 0000 -Serial : 0000000000000000 -```text - -**文件**: `tests/mock/proc/meminfo_512mb` - -```text -MemTotal: 524288 kB -MemFree: 262144 kB -MemAvailable: 393216 kB -Buffers: 16384 kB -Cached: 65536 kB -SwapCached: 0 kB -Active: 131072 kB -Inactive: 104857 kB -SwapTotal: 0 kB -SwapFree: 0 kB -```text - -### 8.2 测试夹具 - -```cpp -// tests/fixtures/test_theme.h - -namespace CFDesktop::Testing { - -/** - * @brief 测试主题生成器 - * - * 快速创建测试用主题。 - */ -class TestThemeBuilder { -public: - TestThemeBuilder& addColor(const QString& name, const QColor& color); - TestThemeBuilder& addSize(const QString& name, int size); - TestThemeBuilder& addFont(const QString& name, const QFont& font); - TestThemeBuilder& addStyle(const QString& name, const QString& qss); - - Theme build() const; - void save(const QString& path) const; - -private: - Theme m_theme; -}; - -/** - * @brief 便捷函数 - */ -inline Theme createDefaultTestTheme() { - return TestThemeBuilder() - .addColor("primary", QColor("#2196F3")) - .addColor("background", QColor("#FFFFFF")) - .addSize("normal", 8) - .addFont("body", QFont("Arial", 10)) - .build(); -} - -} // namespace CFDesktop::Testing -```bash - ---- - -## 九、详细任务清单 - -### 9.1 基础设施 (Week 1) - -- [ ] 配置 CMake 测试框架 -- [ ] 创建测试目录结构 -- [ ] 实现 Mock 系统 -- [ ] 创建测试应用框架 -- [ ] 配置 CI 基础流水线 - -### 9.2 单元测试 (Week 2-4) - -- [ ] HardwareProbe 测试套件 -- [ ] ThemeEngine 测试套件 -- [ ] AnimationManager 测试套件 -- [ ] DPIManager 测试套件 -- [ ] ConfigStore 测试套件 -- [ ] Logger 测试套件 -- [ ] InputManager 测试套件 - -### 9.3 集成测试 (Week 5) - -- [ ] Base 库集成测试 -- [ ] SDK 集成测试 -- [ ] Shell 集成测试 - -### 9.4 UI 测试 (Week 6) - -- [ ] Launcher UI 测试 -- [ ] WindowManager UI 测试 -- [ ] 应用切换测试 - -### 9.5 性能测试 (Week 7) - -- [ ] 基准测试框架 -- [ ] 启动时间测试 -- [ ] 内存使用测试 -- [ ] 动画性能测试 -- [ ] 建立性能基准 - ---- - -## 十、验收标准 - -### 10.1 测试覆盖率 -- [ ] 整体代码覆盖率 > 85% -- [ ] 核心模块覆盖率 > 90% -- [ ] 关键路径覆盖率 100% - -### 10.2 CI/CD -- [ ] 所有测试自动运行 -- [ ] 测试失败阻止合并 -- [ ] 覆盖率报告自动生成 - -### 10.3 性能 -- [ ] 所有性能测试达标 -- [ ] 无内存泄漏 -- [ ] 无明显性能退化 - ---- - -## 十一、测试最佳实践 - -### 11.1 命名约定 - -- 测试文件: `test_.cpp` -- 测试类: `Test` -- 测试用例: `test_` - -### 11.2 断言选择 - -| 场景 | 使用 | -|------|------| -| 简单比较 | `QVERIFY2`, `QCOMPARE` | -| 性能测试 | `QBenchmark` | -| 异常测试 | `QEXPECT_FAIL` | -| 跳过测试 | `QSKIP` | - -### 11.3 测试隔离 - -```cpp -void TestExample::init() { - // 每个测试用例前:重置状态 - m_testObject = new TestClass(); -} - -void TestExample::cleanup() { - // 每个测试用例后:清理资源 - delete m_testObject; -} -```bash - ---- - -## 十二、常见问题解决 - -| 问题 | 解决方案 | -|------|----------| -| Qt 测试找不到组件 | 设置 `QT_QPA_PLATFORM=offscreen` | -| CI 中 UI 测试失败 | 使用 `Xvfb` 虚拟显示 | -| Mock 数据路径错误 | 使用 `QFINDTESTDATA` | -| 测试相互影响 | 确保正确使用 `init()`/`cleanup()` | - ---- +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| GoogleTest + Qt6::Test 双框架 | GoogleTest 适合纯逻辑单元测试,Qt::Test 提供 QSignalSpy 等 Qt 专项支持 | 纯 GoogleTest(Qt 信号测试麻烦)或纯 QtTest(缺少参数化等高级特性) | +| Mock 文件注入(伪造 /proc、/sys) | 硬件探测逻辑依赖内核文件系统,Mock 让测试在任何机器上可复现 | 每次测试都要求特定硬件环境 | +| 测试金字塔 70/20/10 | 单元测试快速稳定覆盖大部分逻辑,集成/UI 测试聚焦关键路径 | 均衡分配或只做单元测试 | +| CI 分阶段 (unit -> integration -> UI -> performance) | 单元测试快速失败尽早反馈,UI/性能测试依赖前序通过再执行 | 所有测试一次性全跑 | -## 十三、下一步行动 +## 当前状态 -测试体系应与其他阶段并行开发: -- Phase 0 完成 CI 基础设施 -- Phase 1-2 同步编写单元测试 -- Phase 4-5 同步编写集成和 UI 测试 +部分实现(约 55%)。base 层核心模块已有单元测试覆盖,CI 已配置 GitHub Actions。参见 [status/current.md](../status/current.md) Phase 8 部分。 diff --git a/document/design_stage/README.md b/document/design_stage/README.md deleted file mode 100644 index 5850f0e41..000000000 --- a/document/design_stage/README.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: CFDesktop 前期阶段设计文档总览 -description: 本目录包含 CFDesktop 项目前期阶段的详细设计文档。 ---- - -# CFDesktop 前期阶段设计文档总览 - -## 文档目录 - -本目录包含 CFDesktop 项目前期阶段的详细设计文档。 - -### 文档列表 - -| 文档 | 阶段 | 内容概要 | -|------|------|----------| -| [system_architecture_overview.md](system_architecture_overview.md) | 总览 | 系统架构总览 — 模块层次、核心接口、平台抽象层、显示后端、UI 分层、初始化流程 | -| [multi_display_backend_architecture.md](multi_display_backend_architecture.md) | 架构 | 多显示后端架构 — 运行时后端选择、组件复用矩阵、各后端实现指导 | -| [00_phase0_project_skeleton.md](00_phase0_project_skeleton.md) | Phase 0 | 工程骨架搭建 - CMake 构建系统、目录结构、CI/CD 配置 | -| [01_phase1_hardware_probe.md](01_phase1_hardware_probe.md) | Phase 1 | 硬件探针与能力分级 - CPU/GPU/内存检测、HWTier 档位判定 | -| [02_phase2_base_library.md](02_phase2_base_library.md) | Phase 2 | Base 库核心 - 主题引擎、动画管理、DPI 适配、配置中心、日志系统 | -| [03_phase3_input_layer.md](03_phase3_input_layer.md) | Phase 3 | 输入抽象层 - 触摸/按键/旋钮处理、手势识别、焦点导航 | -| [04_phase6_simulator.md](04_phase6_simulator.md) | Phase 6 | 多平台模拟器 - PC 端模拟器、设备外壳、触摸可视化、参数注入 | -| [05_phase8_testing.md](05_phase8_testing.md) | Phase 8 | 测试体系 - 单元测试、集成测试、UI 测试、CI/CD | - ---- - -## 快速导航 - -### 按角色查找 - -#### 开发者 -- **新手上路**: 从 [Phase 0](00_phase0_project_skeleton.md) 开始,了解项目结构 -- **基础设施**: [Phase 1](01_phase1_hardware_probe.md) + [Phase 2](02_phase2_base_library.md) -- **UI 开发**: [Phase 3](03_phase3_input_layer.md) -- **调试工具**: [Phase 6](04_phase6_simulator.md) - -#### 测试工程师 -- **测试框架**: [Phase 8](05_phase8_testing.md) -- **单元测试**: 各 Phase 文档中的"单元测试"章节 - -#### 项目经理 -- **时间线**: 查看 BLUEPRINT.md 中的里程碑时间线 -- **任务分解**: 各 Phase 文档中的"详细任务清单"章节 - -### 按模块查找 - -| 模块 | 文档 | -|------|------| -| 系统架构 | [系统架构总览](system_architecture_overview.md) | -| 多显示后端 | [多显示后端架构](multi_display_backend_architecture.md) | -| 目录结构 | [Phase 0 - 二、目录结构设计](00_phase0_project_skeleton.md#二目录结构设计) | -| 构建系统 | [Phase 0 - 三、CMake 构建系统设计](00_phase0_project_skeleton.md#三cmake-构建系统设计) | -| 硬件检测 | [Phase 1 - 五、检测逻辑详细设计](01_phase1_hardware_probe.md#五检测逻辑详细设计) | -| 主题引擎 | [Phase 2 - 三、主题引擎](02_phase2_base_library.md#三主题引擎-themeengine) | -| 动画管理 | [Phase 2 - 四、动画管理器](02_phase2_base_library.md#四动画管理器-animationmanager) | -| DPI 适配 | [Phase 2 - 五、DPI 管理器](02_phase2_base_library.md#五dpi-管理器-dpimanager) | -| 配置系统 | [Phase 2 - 六、配置中心](02_phase2_base_library.md#六配置中心-configstore) | -| 日志系统 | [Phase 2 - 七、日志系统](02_phase2_base_library.md#七日志系统-logger) | -| 输入处理 | [Phase 3](03_phase3_input_layer.md) | -| 模拟器 | [Phase 6](04_phase6_simulator.md) | -| 测试 | [Phase 8](05_phase8_testing.md) | - ---- - -## 使用建议 - -### 阅读顺序 -1. 先阅读 [BLUEPRINT.md](../../BLUEPRINT.md) 了解整体规划 -2. 阅读 [Phase 0](00_phase0_project_skeleton.md) 了解工程结构 -3. 根据开发任务阅读对应 Phase 文档 - -### 开发流程 -1. **环境搭建**: 参考 Phase 0 创建项目骨架 -2. **基础模块**: 按 Phase 1 → Phase 2 → Phase 3 顺序开发 -3. **Shell UI**: 参考 BLUEPRINT.md Phase 4 -4. **SDK 层**: 参考 BLUEPRINT.md Phase 5 -5. **模拟器**: 参考 Phase 6 -6. **测试**: 参考 Phase 8,贯穿全程 - -### 文档更新 -- 设计文档会随开发进展更新 -- 变更记录请查看各文档的版本信息 -- 有问题请及时反馈 - ---- - -## 项目里程碑 - -| 里程碑 | 时间 | 交付物 | -|--------|------|--------| -| M0 | Week 2 | 工程骨架 + CI 跑通 | -| M1 | Week 5 | 硬件探针 + 三档能力分级 | -| M2 | Week 9 | Base 库 + 主题引擎 + 输入抽象 | -| M3 | Week 15 | Shell UI 主体可用 | -| M4 | Week 18 | SDK 导出 + 示例应用 | -| M5 | Week 21 | 模拟器可用 | -| M6 | Week 23 | 应用商店基础 + 完整 CI/CD | - ---- - -## 联系方式 - -如有问题或建议,请通过以下方式联系: -- GitHub Issues: [项目 Issues 页面] -- 技术讨论: [项目 Discussions 页面] - ---- - -*最后更新: 2026-03-30* diff --git a/document/design_stage/index.md b/document/design_stage/index.md index 81bf25b77..594c51238 100644 --- a/document/design_stage/index.md +++ b/document/design_stage/index.md @@ -1,11 +1,52 @@ --- -title: 设计阶段文档 -description: 本目录包含 CFDesktop 项目各阶段的设计文档,覆盖 Phase 0 至 Phase 8 的完 +title: CFDesktop 前期阶段设计文档总览 +description: 本目录包含 CFDesktop 项目前期阶段的详细设计文档,覆盖 Phase 0 至 Phase 8 的完整规划。 --- -# 设计阶段文档 +# CFDesktop 前期阶段设计文档总览 -本目录包含 CFDesktop 项目各阶段的设计文档,覆盖 Phase 0 至 Phase 8 的完整规划。内容包括系统整体架构设计、多显示后端方案、硬件探测层(Hardware Probe)设计、基础库(Base Library)架构以及输入抽象层(Input Layer)设计等核心模块的技术方案。 +本目录包含 CFDesktop 项目前期阶段的详细设计文档,覆盖 Phase 0 至 Phase 8 的完整规划。内容包括系统整体架构设计、多显示后端方案、硬件探测层设计、基础库架构以及输入抽象层设计等核心模块的技术方案。 + +## 文档列表 + +| 文档 | 阶段 | 内容概要 | +|------|------|----------| +| [system_architecture_overview.md](system_architecture_overview.md) | 总览 | 系统架构总览 — 模块层次、核心接口、平台抽象层、显示后端、UI 分层、初始化流程 | +| [multi_display_backend_architecture.md](multi_display_backend_architecture.md) | 架构 | 多显示后端架构 — 运行时后端选择、组件复用矩阵、各后端实现指导 | +| [00_phase0_project_skeleton.md](00_phase0_project_skeleton.md) | Phase 0 | 工程骨架搭建 - CMake 构建系统、目录结构、CI/CD 配置 | +| [01_phase1_hardware_probe.md](01_phase1_hardware_probe.md) | Phase 1 | 硬件探针与能力分级 - CPU/GPU/内存检测、HWTier 档位判定 | +| [02_phase2_base_library.md](02_phase2_base_library.md) | Phase 2 | Base 库核心 - 主题引擎、动画管理、DPI 适配、配置中心、日志系统 | +| [03_phase3_input_layer.md](03_phase3_input_layer.md) | Phase 3 | 输入抽象层 - 触摸/按键/旋钮处理、手势识别、焦点导航 | +| [04_phase6_simulator.md](04_phase6_simulator.md) | Phase 6 | 多平台模拟器 - PC 端模拟器、设备外壳、触摸可视化、参数注入 | +| [05_phase8_testing.md](05_phase8_testing.md) | Phase 8 | 测试体系 - 单元测试、集成测试、UI 测试、CI/CD | + +--- + +## 按模块查找 + +| 模块 | 文档 | +|------|------| +| 系统架构 | [系统架构总览](system_architecture_overview.md) | +| 多显示后端 | [多显示后端架构](multi_display_backend_architecture.md) | +| 目录结构 | [Phase 0](00_phase0_project_skeleton.md) | +| 构建系统 | [Phase 0](00_phase0_project_skeleton.md) | +| 硬件检测 | [Phase 1](01_phase1_hardware_probe.md) | +| 主题引擎 | [Phase 2](02_phase2_base_library.md) | +| 动画管理 | [Phase 2](02_phase2_base_library.md) | +| DPI 适配 | [Phase 2](02_phase2_base_library.md) | +| 配置系统 | [Phase 2](02_phase2_base_library.md) | +| 日志系统 | [Phase 2](02_phase2_base_library.md) | +| 输入处理 | [Phase 3](03_phase3_input_layer.md) | +| 模拟器 | [Phase 6](04_phase6_simulator.md) | +| 测试 | [Phase 8](05_phase8_testing.md) | + +--- + +## 阅读顺序 + +1. 先阅读系统架构总览了解整体设计 +2. 从 Phase 0 开始了解工程结构 +3. 根据开发任务阅读对应 Phase 文档 --- diff --git a/document/design_stage/multi_display_backend_architecture.md b/document/design_stage/multi_display_backend_architecture.md index 54974034f..f1b334b80 100644 --- a/document/design_stage/multi_display_backend_architecture.md +++ b/document/design_stage/multi_display_backend_architecture.md @@ -1,241 +1,37 @@ --- title: CFDesktop 多显示后端架构设计 -description: "最后更新: 2026-03-29,在 中增加:" +description: "多后端运行时选择、三种显示角色模型、组件复用矩阵的设计意图" --- -# CFDesktop 多显示后端架构设计 +# CFDesktop 多显示后端架构设计 -- 设计意图 -> **状态**: 设计中 -> **版本**: 0.1.0 -> **最后更新**: 2026-03-29 +## 为什么选择这种方案 ---- - -## 一、设计目标 - -CFDesktop 需要在以下场景中运行: - -| 场景 | 角色定位 | 运行环境 | -|------|---------|---------| -| Windows 伪桌面 | 客户端应用 | Windows 10/11,覆盖在 Windows 桌面上 | -| Linux 有桌面环境 | 客户端应用 | 有 Gnome/KDE 等完整桌面环境 | -| Linux 无桌面 (X11) | X11 窗口管理器 | 仅有 libx11/libxcb,无窗口管理器 | -| Linux 无桌面 (Wayland) | Wayland 合成器 | 仅有 libwayland,无合成器 | -| Linux 嵌入式 | 直接渲染 | 仅 libdrm/libgbm 或 linuxfb | - -核心设计原则:**同一套 Shell/Panel/WindowManager 代码在所有场景中复用,通过抽象层屏蔽平台差异。** - ---- - -## 二、系统角色模型 - -```text -┌─────────────────────────────────────────────────────┐ -│ CFDesktop Shell │ -│ (ShellLayer, PanelManager, WindowManager, IWindow) │ -├───────────────────────────────────────────────────────┤ -│ IDisplayServerBackend │ -│ ┌─────────┬──────────┬──────────┬────────────────┐ │ -│ │ Client │Compositor│Compositor│ DirectRender │ │ -│ │ Mode │ X11 WM │ Wayland │ EGLFS/FB │ │ -│ └────┬────┴────┬─────┴────┬─────┴───────┬────────┘ │ -│ │ │ │ │ │ -│ ┌────▼────┐┌───▼────┐┌───▼────┐┌───────▼──────┐ │ -│ │QWidget ││XCB WM ││QtWayland││Qt linuxfb/ │ │ -│ │Windows ││EWMH ││Compositor││EGLFS plugin │ │ -│ └─────────┘└────────┘└────────┘└──────────────┘ │ -├───────────────────────────────────────────────────────┤ -│ RenderBackend │ -│ (RHI abstraction, swapBuffers, capabilities) │ -├───────────────────────────────────────────────────────┤ -│ 硬件层 (GPU / DRM / FB / Win32) │ -└───────────────────────────────────────────────────────┘ -```bash - -### DisplayServerRole 枚举 - -| 值 | 含义 | 典型场景 | -|----|------|---------| -| `Client` | 在已有桌面环境中作为应用运行 | Windows, Linux with DE | -| `Compositor` | 自己就是显示服务器/合成器 | 无桌面 Linux (X11/Wayland) | -| `DirectRender` | 直接渲染到 framebuffer | 嵌入式设备 (EGLFS/linuxfb) | - ---- - -## 三、新增接口定义 - -### 3.1 IDisplayServerBackend (新增) - -**文件**: `desktop/ui/components/IDisplayServerBackend.h` - -```cpp -class IDisplayServerBackend : public QObject { - Q_OBJECT -public: - virtual DisplayServerRole role() const = 0; - virtual DisplayServerCapabilities capabilities() const = 0; - virtual bool initialize(int argc, char** argv) = 0; - virtual void shutdown() = 0; - virtual int runEventLoop() = 0; - virtual WeakPtr windowBackend() = 0; - virtual QList outputs() const = 0; - -signals: - void outputChanged(); - void externalWindowAppeared(WeakPtr window); - void externalWindowDisappeared(WeakPtr window); -}; -```text - -**职责**: -- 决定 CFDesktop 的运行角色(客户端 / 合成器 / 直接渲染) -- 管理显示输出的生命周期 -- 在合成器模式下,桥接外部窗口事件到 WindowManager - -### 3.2 RenderBackend (新增) - -**文件**: `desktop/ui/render/render_backend.h` - -```cpp -class RenderBackend { -public: - virtual ~RenderBackend() = default; - virtual bool initialize() = 0; - virtual void shutdown() = 0; - virtual BackendCapabilities capabilities() const = 0; - virtual QSize screenSize() const = 0; - virtual void* nativeHandle() const = 0; // EGLDisplay, HWND 等 -}; -```text - -**职责**: -- 抽象底层渲染硬件初始化 -- 提供渲染能力查询 -- 不负责具体绘制(由 Qt RHI 或 QPainter 负责) - -### 3.3 BackendCapabilities (新增) - -**文件**: `desktop/ui/render/backend_capabilities.h` - -```cpp -struct BackendCapabilities { - bool supportsMultiWindow = true; - bool supportsTransparency = true; - bool hasHardwareAcceleration = true; - bool supportsVSync = true; - bool supportsScreenshot = true; - int maxTextureSize = 4096; -}; -```yaml - ---- - -## 四、现有接口修改 - -### 4.1 IWindowBackend — 增加 capabilities() +CFDesktop 需要在五种运行场景中工作:Windows 客户端、Linux 有桌面环境客户端、Linux 无桌面 X11 窗口管理器、Linux 无桌面 Wayland 合成器、嵌入式直接渲染。这些场景对显示后端的需求截然不同 -- 有的只需跟踪外部窗口,有的需要自己充当合成器,有的甚至连窗口系统都没有。 -在 `IWindowBackend` 中增加: -```cpp -virtual BackendCapabilities capabilities() const = 0; -```text +核心设计原则是:**同一套 Shell / Panel / WindowManager 代码在所有场景中复用,通过 `IDisplayServerBackend` 抽象层屏蔽平台差异。** 系统定义了三种运行角色(`Client` / `Compositor` / `DirectRender`),每个后端实现一种角色。运行时通过 `DetectDisplayServerMode()` 自动检测环境(环境变量 -> Wayland/X11 socket -> DRM 设备 -> framebuffer),选择合适的后端实例化。上层代码通过 `capabilities()` 查询能力,对不具备的功能优雅降级。 -允许 WindowManager 和 Shell 在运行时查询后端能力,做出适配决策。 +## 关键决策 -### 4.2 qt_backend.h — 扩展枚举和检测 +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| 三种运行角色(Client/Compositor/DirectRender) | 覆盖所有已知场景,每种角色的能力边界清晰 | 只区分"有/无桌面环境"(无法处理嵌入式场景) | +| 运行时自动检测后端 | 用户无需配置即可适配当前环境,降低使用门槛 | 编译时硬编码后端(每个平台单独编译,维护成本翻倍) | +| `IDisplayServerBackend` 作为顶层抽象 | 角色检测、能力查询、生命周期管理集中在一个接口,上层只需对接这一层 | 每个平台各自暴露不同接口(上层需要大量条件分支) | +| `BackendCapabilities` 能力查询 | 允许上层在运行时根据实际能力做降级决策(如嵌入式不支持多窗口) | 假设所有能力都可用(在功能受限的嵌入式设备上崩溃) | +| `IShellLayer` 接口解耦 QWidget | Wayland 合成器不继承 QWidget,而是实现纯接口 `IShellLayer` | 强制所有后端都基于 QWidget(Wayland 原生合成器无法实现) | -```cpp -enum class LinuxQtBackend { - X11, // X11 窗口系统 - Wayland, // Wayland 合成器 - EGLFS, // EGL + OpenGL ES (无窗口系统) - LinuxFB, // Linux 帧缓冲 (纯软件渲染) - Unknown -}; +## 平台角色模型 -enum class DisplayServerMode { - Client, // 有现成的窗口系统可用 - NeedCompositor, // 需要自己做合成器 - DirectRender // 直接渲染到硬件 -}; +| 场景 | 角色 | 典型技术 | +|------|------|---------| +| Windows 伪桌面 | Client | QWidget + Win32 API + SetWinEventHook | +| Linux 有桌面环境 | Client | QWidget + X11/Wayland 客户端 | +| Linux 无桌面 (X11) | Compositor | XCB SubstructureRedirect + EWMH + XComposite | +| Linux 无桌面 (Wayland) | Compositor | QtWaylandCompositor + xdg-shell + DRM/KMS | +| Linux 嵌入式 | DirectRender | Qt EGLFS/linuxfb + libdrm + evdev | -DisplayServerMode DetectDisplayServerMode(); -```text - -`DetectDisplayServerMode()` 检测逻辑: -1. 检查环境变量 `CFDESKTOP_DISPLAY_SERVER` (强制覆盖) -2. 检查 `WAYLAND_DISPLAY` 或 `DISPLAY` (有显示服务器 → Client) -3. 检查 `/dev/dri/card*` (可做 EGLFS → DirectRender) -4. 检查 `/dev/fb0` (可做 linuxfb → DirectRender) -5. 默认: Client - -### 4.3 ShellLayer — 解耦 QWidget - -新增纯接口 `IShellLayer`: -```cpp -class IShellLayer { -public: - virtual ~IShellLayer() = default; - virtual void setStrategy(std::unique_ptr strategy) = 0; - virtual QRect geometry() const = 0; -}; -```text - -`ShellLayer` 同时继承 `IShellLayer` 和 `QWidget`: -```cpp -class ShellLayer : public QWidget, public IShellLayer { - // QWidget 实现 — Client 模式 -}; - -// Wayland 合成器可以实现 IShellLayer 而不继承 QWidget -```bash - ---- - -## 五、各后端实现指导 - -### 5.1 Windows 伪桌面 - -- **角色**: `DisplayServerRole::Client` -- **RenderBackend**: WindowsBackend (封装 Win32 + Qt RHI) -- **IWindowBackend**: 每个窗口 = QWidget 子控件 -- **特殊处理**: 无边框全屏 + WS_EX_TOOLWINDOW 避免任务栏 -- **现有代码复用**: `windows_display_size_policy.cpp` - -### 5.2 X11 客户端 - -- **角色**: `DisplayServerRole::Client` -- **RenderBackend**: 默认 Qt RHI -- **IWindowBackend**: QWidget 直接映射为 X11 Window -- **现有代码复用**: `linux_wsl_display_size_policy.cpp` 的 X11 路径 - -### 5.3 X11 窗口管理器 (无桌面) - -- **角色**: `DisplayServerRole::Compositor` -- **关键技术**: XCB SubstructureRedirect + EWMH + XComposite -- **IWindowBackend**: XCB Window → `IWindow` 适配器 -- **合成**: XComposite 重定向到 offscreen pixmap, OpenGL 合成 -- **输入**: XCB 事件循环转发 - -### 5.4 Wayland 合成器 (无桌面) - -- **角色**: `DisplayServerRole::Compositor` -- **关键技术**: QtWaylandCompositor C++ API + xdg-shell -- **IWindowBackend**: QWaylandSurface → `IWindow` 适配器 -- **合成**: Qt RHI (OpenGL/Vulkan) 直接合成 -- **输出**: DRM/KMS via QWaylandOutput -- **依赖**: Qt6::WaylandCompositor, libwayland-server, libdrm - -### 5.5 EGLFS / linuxfb - -- **角色**: `DisplayServerRole::DirectRender` -- **关键技术**: Qt EGLFS/linuxfb 平台插件 -- **IWindowBackend**: 全部"窗口" = QWidget 子控件 -- **输入**: evdev (udev) 直接读取 -- **限制**: 无多窗口、无外部应用管理 - ---- - -## 六、组件复用矩阵 +## 组件复用矩阵 | 组件 | Client | X11 WM | Wayland | DirectRender | |------|--------|--------|---------|-------------| @@ -246,55 +42,6 @@ class ShellLayer : public QWidget, public IShellLayer { | `ShellLayer` | QWidget 实现 | QWidget 实现 | IShellLayer 实现 | QWidget 实现 | | `IShellLayerStrategy` | 直接复用 | 直接复用 | 需新策略 | 直接复用 | ---- - -## 七、构建系统设计 - -```cmake -# 条件编译选项 -option(CFDESKTOP_ENABLE_WAYLAND_COMPOSITOR "Enable Wayland compositor backend" ON) -option(CFDESKTOP_ENABLE_X11_WM "Enable X11 window manager backend" ON) -option(CFDESKTOP_ENABLE_LINUXFB "Enable linuxfb backend" ON) - -# 自动检测依赖 -if(CFDESKTOP_ENABLE_WAYLAND_COMPOSITOR) - find_package(Qt6 OPTIONAL_COMPONENTS WaylandCompositor) -endif() - -if(CFDESKTOP_ENABLE_X11_WM) - find_package(X11) - find_package(PkgConfig) - pkg_check_modules(XCB OPTIONAL xcb xcb-composite xcb-ewmh) -endif() -```yaml - ---- - -## 八、运行时后端选择流程 +## 当前状态 -```text -CFDesktop 启动 - │ - ▼ -DetectDisplayServerMode() - │ - ├── 环境变量 CFDESKTOP_DISPLAY_SERVER? - │ └── 强制使用指定后端 - │ - ├── WAYLAND_DISPLAY 存在? - │ └── Client 模式 (Wayland 客户端) - │ - ├── DISPLAY 存在? - │ ├── 有其他 WM 在运行? - │ │ └── Client 模式 (X11 客户端) - │ └── 无 WM? - │ └── Compositor 模式 (X11 WM) - │ - ├── /dev/dri/card* 存在? - │ └── DirectRender 模式 (EGLFS) - │ - ├── /dev/fb0 存在? - │ └── DirectRender 模式 (linuxfb) - │ - └── 默认: Client 模式 -```text +部分实现。Windows Client 和 WSL X11 Client 后端已完成;X11 WM、Wayland 合成器、EGLFS/linuxfb 后端规划中。详细接口规格参见 HandBook。 diff --git a/document/design_stage/system_architecture_overview.md b/document/design_stage/system_architecture_overview.md index fe8e880f8..1f7d64f7f 100644 --- a/document/design_stage/system_architecture_overview.md +++ b/document/design_stage/system_architecture_overview.md @@ -1,618 +1,41 @@ --- title: CFDesktop 系统架构总览 -description: "版本: 0.13.1,最后更新: 2026-03-30" +description: "三层单向依赖架构、平台抽象层、DAG 初始化链的设计意图" --- -# CFDesktop 系统架构总览 +# CFDesktop 系统架构总览 -- 设计意图 -> **状态**: 已实现 -> **版本**: 0.13.1 -> **最后更新**: 2026-03-30 +## 为什么选择这种方案 ---- - -## 一、项目概述 - -**CFDesktop** 是一个基于 Qt 6 / C++23 的 Material Design 3 桌面环境框架,面向嵌入式设备设计,同时支持在 Windows 和 Linux/WSL 上以 Client 模式运行。 - -| 属性 | 值 | -|------|-----| -| 定位 | 便携式桌面环境框架 | -| 版本 | 0.13.1 | -| 语言 | C++23 | -| UI 框架 | Qt 6.8.3 | -| 设计系统 | Material Design 3 | -| 构建系统 | CMake 3.16+ | -| 目标平台 | Windows 10/11, Linux/WSL, 嵌入式 ARM | - -### 设计目标 - -1. **跨平台统一**:同一套 Shell / Panel / WindowManager 代码在所有平台复用 -2. **Material Design 3**:完整的 MD3 设计系统实现(颜色、排版、形状、动效) -3. **嵌入式适配**:动态检测硬件能力,优雅降级到低性能模式 -4. **可扩展性**:通过 Factory / Strategy 模式轻松添加新平台后端 - ---- - -## 二、模块层次与依赖 - -### 2.1 三层模块架构 - -```text -┌─────────────────────────────────────────────────────────┐ -│ desktop 模块 │ -│ 桌面环境实现 — Shell, Panel, 后端 │ -├─────────────────────────────────────────────────────────┤ -│ ui 模块 │ -│ UI 框架 — Material Design 3 组件与动画 │ -├─────────────────────────────────────────────────────────┤ -│ base 模块 │ -│ 基础库 — 工具、系统检测、工厂模式基础设施 │ -├─────────────────────────────────────────────────────────┤ -│ Qt 6 / OS API │ -└─────────────────────────────────────────────────────────┘ -```bash - -**依赖规则**:`desktop` → `ui` → `base`,严格单向依赖。 - -### 2.2 模块职责一览 - -| 模块 | 子模块 | 构建目标 | 职责 | -|------|--------|----------|------| -| **base** | `include/` | `cfbase_headers` | 头文件工具库(Factory、Singleton、WeakPtr、宏) | -| | `system/cpu/` | `cfbase_cpu` | CPU 检测与性能分级 | -| | `system/memory/` | `cfbase_memory` | 内存检测 | -| | `system/gpu/` | `cfbase_gpu` | GPU 检测 | -| | `system/network/` | `cfbase_network` | 网络状态检测 | -| | `device/console/` | `cfbase_console` | 控制台工具 | -| | — | **`cfbase`** | 统一聚合静态库 | -| **ui** | `base/` | `cf_ui_base` | 数学、颜色、几何、easing | -| | `core/` | `cf_ui_core` | 主题引擎、设计 Token | -| | `components/` | `cf_ui_components` | 动画框架 | -| | `widget/material/` | `cf_ui_components_material` | Material Design 3 控件 | -| | `widget/` | `cf_ui_widget` | Widget 适配层 | -| | `models/` | — | 数据模型 | -| | — | **`cfui`** | 统一聚合静态库 | -| **desktop** | `main/` | `CFDesktopMain` | 初始化链、启动入口 | -| | `ui/components/` | `cf_desktop_components` | 核心接口(IWindow, IWindowBackend, IDisplayServerBackend) | -| | `ui/platform/` | `cf_desktop_ui_platform` | 平台抽象层(Windows / WSL) | -| | `ui/render/` | `cf_desktop_render` | 渲染后端抽象 | -| | `ui/widget/` | — | 桌面特有 Widget | -| | `base/` | — | 配置、日志、文件操作 | -| | — | **`CFDesktop_shared`** | 统一聚合共享库(.dll/.so) | -| | — | **`CFDesktop`** | 薄壳可执行文件 | - -### 2.3 最终构建产物 - -```text -CFDesktop.exe / CFDesktop ← 薄壳 main.cpp - └── CFDesktop_shared.dll/.so ← 统一共享库(聚合所有静态库) - ├── CFDesktopMain ← 初始化与启动 - ├── CFDesktopUi ← 桌面 UI 聚合 - │ ├── cf_desktop_ui_platform ← 平台抽象层 - │ ├── cf_desktop_components ← 核心接口 - │ └── cf_desktop_render ← 渲染抽象 - ├── cfui ← UI 框架聚合 - │ ├── cf_ui_base - │ ├── cf_ui_core - │ ├── cf_ui_components - │ └── cf_ui_widget - ├── cfbase ← 基础库聚合 - │ ├── cfbase_cpu - │ ├── cfbase_memory - │ ├── cfbase_gpu - │ └── cfbase_console - ├── cflogger - ├── cffilesystem - ├── cfconfig - └── cfasciiart -```yaml - ---- - -## 三、核心接口体系 - -### 3.1 接口层次关系 - -```text -┌─────────────────────────────────────────────────────┐ -│ IDisplayServerBackend │ -│ (顶层抽象:角色、能力、生命周期、事件循环) │ -│ │ -│ ┌────────────────────────────────────────────────┐ │ -│ │ IWindowBackend │ │ -│ │ (窗口创建/销毁/跟踪,发出 window_came/gone) │ │ -│ │ │ │ -│ │ ┌──────────────────────────────────────────┐ │ │ -│ │ │ IWindow │ │ │ -│ │ │ (单个窗口:ID、标题、几何、关闭、置顶) │ │ │ -│ │ └──────────────────────────────────────────┘ │ │ -│ └────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────┘ -```text - -### 3.2 IDisplayServerBackend - -**文件**: `desktop/ui/components/IDisplayServerBackend.h` - -顶层显示后端抽象,定义 CFDesktop 在显示栈中的角色: - -```cpp -enum class DisplayServerRole { - Client, // 在已有桌面环境中运行 - Compositor, // CFDesktop 就是显示服务器 - DirectRender // 直接渲染到 framebuffer -}; - -struct DisplayServerCapabilities { - DisplayServerRole role; - bool canManageExternalWindows; // 能否管理外部应用窗口 - bool needsOwnCompositor; // 是否需要自己的合成场景 - bool supportsWaylandProtocol; - bool supportsX11Protocol; -}; -```bash - -**核心方法**: - -| 方法 | 说明 | -|------|------| -| `role()` | 返回运行角色 | -| `capabilities()` | 返回能力描述 | -| `initialize(argc, argv)` | 初始化后端 | -| `shutdown()` | 关闭后端 | -| `runEventLoop()` | 运行事件循环(通常调用 `QApplication::exec()`) | -| `windowBackend()` | 返回窗口后端弱引用 | -| `outputs()` | 返回屏幕几何列表 | - -**信号**: - -| 信号 | 触发时机 | -|------|---------| -| `outputChanged()` | 屏幕增减或分辨率变化 | -| `externalWindowAppeared()` | 外部应用窗口出现 | -| `externalWindowDisappeared()` | 外部应用窗口消失 | - -### 3.3 IWindowBackend - -**文件**: `desktop/ui/components/IWindowBackend.h` - -窗口创建与跟踪接口: - -| 方法 | 说明 | -|------|------| -| `createWindow(appId)` | 创建内部窗口 | -| `destroyWindow(window)` | 销毁窗口 | -| `windows()` | 返回所有跟踪中的窗口 | -| `capabilities()` | 返回渲染能力 | -| `make_weak()` | 返回后端弱引用 | - -**信号**: `window_came(WeakPtr)`, `window_gone(WeakPtr)` - -### 3.4 IWindow - -**文件**: `desktop/ui/components/IWindow.h` - -平台无关的窗口抽象: - -| 方法 | 说明 | -|------|------| -| `windowID()` | 唯一标识符(`win_id_t` = `QString`) | -| `title()` | 窗口标题 | -| `geometry()` | 窗口几何(设备像素) | -| `set_geometry(r)` | 移动/缩放窗口 | -| `requestClose()` | 请求关闭 | -| `raise()` | 置顶 | - -**信号**: `closeRequested()`, `titleChanged()`, `geometryChanged()` - -### 3.5 信号流转 - -```text -外部窗口出现(OS 层事件) - │ - ▼ -IWindowBackend::registerWindow() ← 平台后端检测到新窗口 - │ - ├─ 创建 IWindow 实例 - ├─ emit window_came(weak) - │ - ▼ -IDisplayServerBackend ← 转发信号 - │ (connect IWindowBackend::window_came → externalWindowAppeared) - │ - ▼ -CFDesktopEntity::run_init() ← 日志记录 + Shell 分发 - │ (connect IWindowBackend::window_came → slot) - │ - ▼ -WindowManager / ShellLayer ← UI 响应 -```yaml - ---- - -## 四、平台抽象层 - -### 4.1 设计模式 - -平台抽象层使用两种互补的设计模式: - -**Factory 模式** — 创建平台特定对象: - -```text -┌─────────────────────────────────────────────────┐ -│ RegisteredFactory │ -│ creator_: std::function │ -│ destroyer_: std::function │ -│ │ -│ make_unique() → unique_ptr│ -│ make_shared() → shared_ptr │ -├─────────────────────────────────────────────────┤ -│ StaticRegisteredFactory │ -│ (单例 + RegisteredFactory) │ -├─────────────────────────────────────────────────┤ -│ DisplayServerBackendFactory │ -│ : StaticRegisteredFactory│ -└─────────────────────────────────────────────────┘ -```text - -**Strategy 模式** — 封装平台特定行为: - -```text -IDesktopPropertyStrategy -├── IDesktopDisplaySizeStrategy ← 窗口尺寸与行为策略 -│ ├── WindowsDisplaySizePolicy ← 无边框、置底、避免系统 UI -│ └── LinuxWSLDisplaySizePolicy ← WSL 环境下的窗口策略 -└── (可扩展其他策略类型) -```text - -### 4.2 平台分发机制 - -每个平台目录提供两个分发函数,由公共 Helper 统一调度: - -```text -platform_helper.h ← 通用接口 - native_impl() → PlatformFactoryAPI(策略工厂) - native_display() → DisplayBackendFactoryAPI(后端工厂) - -display_backend_helper.h ← 显示后端专用接口 - native_display() → 转发到 native_display_impl() - native_display_impl() ← 每个平台实现此函数 -```text - -**调用链**: - -```text -CFDesktopEntity 构造 - │ - ├─ platform::native_display() - │ └─ native_display_impl() ← 平台分发 - │ ├─ Windows: new WindowsDisplayServerBackend - │ └─ WSL: new WSLDisplayServerBackend - │ - └─ DisplayServerBackendFactory::register_creator(creator, release) -```bash - -### 4.3 WSL 检测 - -**文件**: `cmake/env_check/detail_os_checker.cmake` - -三重检测机制(按优先级): - -1. **环境变量**: `WSL_DISTRO_NAME` 存在 -2. **内核版本**: `uname -r` 包含 `microsoft` 或 `wsl` -3. **procfs**: `/proc/version` 包含 `microsoft` 或 `wsl` - -检测到 WSL 后,CMake 设置 `IS_WSL=TRUE` 并定义 `CFDESKTOP_OS_WSL` 宏。 - ---- - -## 五、显示后端实现 - -### 5.1 后端对比 - -| 维度 | Windows 后端 | WSL/X11 后端 | -|------|-------------|-------------| -| **角色** | Compositor(伪桌面) | Compositor(伪桌面) | -| **运行模式** | 全屏覆盖层 | XWayland 客户端 | -| **窗口跟踪** | `SetWinEventHook` + `EnumWindows` | XCB `SubstructureNotify` + `xcb_query_tree` | -| **窗口封装** | `WindowsWindow(HWND)` | `WSLX11Window(xcb_window_t)` | -| **标题查询** | `GetWindowTextW` | `xcb_get_property(_NET_WM_NAME)` | -| **几何查询** | `GetWindowRect` | `xcb_get_geometry` + `xcb_translate_coordinates` | -| **关闭窗口** | `PostMessage(WM_CLOSE)` | `xcb_send_event(WM_DELETE_WINDOW)` | -| **事件集成** | `WINEVENT_OUTOFCONTEXT` → GUI 线程 | `QSocketNotifier` → Qt 事件循环 | -| **显示服务器** | Windows DWM | XWayland / Weston | +CFDesktop 的核心挑战是在 Windows 客户端、Linux X11/Wayland 合成器、嵌入式直接渲染等截然不同的运行场景中复用同一套 Shell / Panel / WindowManager 代码。为此,系统采用 **base -> ui -> desktop 三层单向依赖架构**:底层 `base` 提供硬件探测与基础工具,中层 `ui` 构建完整的 Material Design 3 设计系统(颜色、排版、形状、动效),顶层 `desktop` 组合两者实现桌面环境。严格单向依赖(`desktop` 可依赖 `ui` 和 `base`,反之不可)确保底层模块不感知上层业务,使每层可独立测试、独立替换。 -### 5.2 Windows 后端数据流 +平台差异通过 **Factory + Strategy 双模式抽象层** 屏蔽。`DisplayServerBackendFactory` 负责创建平台特定的显示后端对象,`IDesktopDisplaySizeStrategy` 封装平台特定的窗口行为策略。每个平台只需实现两个分发函数(`native_impl()` 和 `native_display()`),公共 Helper 统一调度,新增平台无需修改已有代码。 -```text -┌─────────────────────────────────────────────────────┐ -│ WindowsDisplayServerBackend │ -│ initialize(): │ -│ ├─ QApplication::instance() 检查 │ -│ ├─ WindowsWindowBackend::startTracking() │ -│ │ ├─ SetWinEventHook(CREATE/SHOW/DESTROY) │ -│ │ └─ EnumWindows() 扫描已有窗口 │ -│ └─ connect signals → externalWindowAppeared/... │ -├─────────────────────────────────────────────────────┤ -│ WindowsWindowBackend │ -│ WinEventProc (static callback) │ -│ ├─ EVENT_OBJECT_SHOW → onExternalWindowShown │ -│ │ ├─ shouldTrackWindow(hwnd) │ -│ │ │ ├─ IsWindow + IsWindowVisible │ -│ │ │ ├─ GetAncestor(GA_ROOT) == hwnd │ -│ │ │ ├─ GetWindowTextLength > 0 │ -│ │ │ ├─ PID != 自身 │ -│ │ │ └─ 排除 Progman/Shell_TrayWnd/WorkerW │ -│ │ └─ registerWindow(hwnd) → emit window_came │ -│ └─ EVENT_OBJECT_DESTROY → unregisterWindow │ -├─────────────────────────────────────────────────────┤ -│ WindowsWindow(HWND) │ -│ title() → GetWindowTextW │ -│ geometry() → GetWindowRect │ -│ set_geometry() → SetWindowPos │ -│ requestClose() → PostMessage(WM_CLOSE) │ -│ raise() → SetForegroundWindow + BringWindowToTop│ -└─────────────────────────────────────────────────────┘ -```text - -### 5.3 WSL/X11 后端数据流 - -```text -┌─────────────────────────────────────────────────────┐ -│ WSLDisplayServerBackend │ -│ initialize(): │ -│ ├─ connectToX11() │ -│ │ ├─ xcb_connect(nullptr, &screen_num) │ -│ │ ├─ xcb_get_setup → xcb_setup_roots_iterator │ -│ │ └─ 获取 root_window_ │ -│ ├─ WSLX11WindowBackend::startTracking(conn,root) │ -│ │ ├─ internAtoms() → 缓存 9 个 X11 Atom │ -│ │ ├─ xcb_change_window_attributes( │ -│ │ │ SUBSTRUCTURE_NOTIFY) │ -│ │ ├─ xcb_query_tree → 扫描已有窗口 │ -│ │ └─ QSocketNotifier(xcb_fd) → processEvents │ -│ └─ connect signals → externalWindowAppeared/... │ -├─────────────────────────────────────────────────────┤ -│ WSLX11WindowBackend │ -│ processXcbEvents() ← QSocketNotifier::activated │ -│ ├─ XCB_CREATE_NOTIFY │ -│ │ └─ shouldTrackWindow → registerWindow │ -│ ├─ XCB_MAP_NOTIFY │ -│ │ └─ shouldTrackWindow → registerWindow │ -│ ├─ XCB_DESTROY_NOTIFY → unregisterWindow │ -│ ├─ XCB_CONFIGURE_NOTIFY → geometryChanged signal │ -│ └─ XCB_PROPERTY_NOTIFY → titleChanged signal │ -│ │ -│ shouldTrackWindow(win): │ -│ ├─ xcb_get_window_attributes → viewable? │ -│ ├─ !override_redirect │ -│ ├─ _NET_WM_NAME / WM_NAME 非空 │ -│ ├─ _NET_WM_PID != 自身 PID │ -│ └─ _NET_WM_WINDOW_TYPE != DOCK/DESKTOP │ -├─────────────────────────────────────────────────────┤ -│ WSLX11Window(xcb_window_t) │ -│ title() → xcb_get_property(_NET_WM_NAME) │ -│ → fallback: WM_NAME │ -│ geometry() → xcb_get_geometry │ -│ + xcb_translate_coordinates → 根坐标 │ -│ set_geometry() → xcb_configure_window │ -│ requestClose() → WM_DELETE_WINDOW ClientMessage │ -│ → fallback: xcb_kill_client │ -│ raise() → xcb_configure_window(ABOVE) │ -└─────────────────────────────────────────────────────┘ -```bash - -### 5.4 未来后端规划 +初始化采用 **DAG 阶段链**(`RunEarlyInit` -> `RunStageInit` -> `boot_desktop` -> `exec`),而非扁平的 init 列表。阶段之间有明确的数据依赖:早期初始化建立日志和配置,阶段初始化完成系统检测和资源预加载,最后才创建 Qt Widget 和平台后端。`CFDesktopEntity` 作为中央单例管理整个生命周期,其 `release()` 必须在 `QApplication` 存活时调用(内部持有 QWidget 实例)。 -| 后端 | 角色 | 关键技术 | 状态 | -|------|------|---------|------| -| X11 窗口管理器 | Compositor | XCB SubstructureRedirect + EWMH + XComposite | 规划中 | -| Wayland 合成器 | Compositor | QtWaylandCompositor + xdg-shell + DRM/KMS | 规划中 | -| EGLFS | DirectRender | Qt EGLFS + libdrm/libgbm | 规划中 | -| LinuxFB | DirectRender | Qt linuxfb + evdev | 规划中 | - ---- +## 关键决策 -## 六、UI 框架分层 +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| 三层单向依赖(base/ui/desktop) | 防止循环依赖,每层可独立编译测试,底层不感知上层业务 | 扁平单层结构(耦合严重,无法独立测试) | +| Factory + Strategy 平台抽象 | 新增平台只实现接口,不修改已有代码;运行时可查询能力做降级 | 大量 `#ifdef` 平台分支(维护困难,违反开闭原则) | +| UI 五层流水线(数学->主题->动画->行为->控件) | MD3 设计系统本身分层清晰,逐层构建避免跨层抽象泄漏 | 单体 Widget 直接硬编码样式(无法换肤,无法复用行为) | +| DAG 阶段初始化链 | 阶段间有硬数据依赖(日志先于检测,检测先于 Widget 创建),错误可精确定位到阶段 | 随意顺序初始化(隐式依赖导致启动失败难以调试) | +| `--whole-archive` 共享库构建 | 工厂自动注册函数可能不被直接引用,whole-archive 确保符号不丢失 | 手动注册每个工厂(易遗漏,违背自动化设计) | +| `CFDesktopEntity` 单例生命周期管理 | 集中管理 Widget/后端/策略的析构顺序,保证 Qt 对象树正确销毁 | 分散在各模块各自清理(销毁顺序不可控,UAF 风险) | -UI 模块采用五层架构,从底层数学工具到顶层控件,逐层构建 Material Design 3 设计系统: +## 模块层次 ```text -┌─────────────────────────────────────────────────┐ -│ Layer 5: Widget 适配层 (cf_ui_widget) │ -│ Button, TextField, Switch, TabView... │ -│ (20+ 生产级 Widget,面向开发者) │ -├─────────────────────────────────────────────────┤ -│ Layer 4: Material 行为层 (cf_ui_components) │ -│ StateMachine, Ripple, Elevation, FocusRing │ -│ (MD3 交互行为:水波纹、状态机、焦点) │ -│ + Material 动画实现 │ -│ (Fade, Slide, Scale, Spring) │ -├─────────────────────────────────────────────────┤ -│ Layer 3: 动画引擎层 (cf_ui_components) │ -│ Animation, AnimationFactory, Timing/Spring │ -│ (动画基础设施:时间线、弹性物理、工厂) │ -├─────────────────────────────────────────────────┤ -│ Layer 2: 主题引擎层 (cf_ui_core) │ -│ ThemeManager, Token 系统 │ -│ (MD3 Token: 颜色、排版、形状、动效) │ -├─────────────────────────────────────────────────┤ -│ Layer 1: 数学与工具层 (cf_ui_base) │ -│ Color, Math, Geometry, Easing, DevicePixel │ -│ (基础数学:HCT 色彩、easing 曲线、DPI) │ -└─────────────────────────────────────────────────┘ -```yaml - -### 各层详情 - -**Layer 1 — 数学与工具层** (`ui/base/`) -- `color.h`: HCT 色彩系统(Hue-Chroma-Tone) -- `math_helper.h`: 数学辅助函数 -- `geometry_helper.h`: 几何计算 -- `easing.h`: 缓动曲线(Material Motion 标准) -- `device_pixel.h`: DPI 适配与设备像素比 - -**Layer 2 — 主题引擎层** (`ui/core/`) -- `theme_manager.h`: 主题管理器,动态切换主题 -- `token.hpp`: 设计 Token 系统(~35KB),包含完整的 MD3 Token 定义 -- `color_scheme.h`: 颜色方案(Light/Dark/动态) -- `font_type.h`: 排版系统 -- `motion_spec.h`: 动效规格 - -**Layer 3 — 动画引擎层** (`ui/components/`) -- `animation.h`: 动画基类 -- `animation_factory_manager.h`: 动画工厂管理器 -- `timing_animation.h`: 基于时间线的动画 -- Material 动画实现:Fade, Slide, Scale, Spring - -**Layer 4 — Material 行为层** (`ui/widget/material/base/`) -- `state_machine.h`: Widget 状态机(Pressed/Hovered/Focused/Disabled) -- `ripple_helper.h`: Material 水波纹效果 -- `elevation_controller.h`: Z 轴高度管理(阴影层次) -- `focus_ring.h`: 焦点指示环 - -**Layer 5 — Widget 适配层** (`ui/widget/material/widget/`) -- 20+ 生产级 Widget:Button, Checkbox, TextField, Slider, Switch, ComboBox, ListView, TableView, TreeView, TabView, ScrollView 等 - ---- +desktop/ (Layer 3) -- Shell, Panel, 后端, 初始化 + | +ui/ (Layer 2) -- MD3 组件, 主题, 动画, 20+ Widget + | +base/ (Layer 1) -- CPU/GPU/Memory 探测, 工具库, 工厂基础设施 + | +Qt 6 / OS API +``` -## 七、初始化与生命周期 - -### 7.1 完整启动流程 - -```cpp -main(argc, argv) -│ -├─ QApplication cf_desktop_app(argc, argv) ← Qt 应用创建 -│ -└─ run_desktop_session() ← DLL 入口 - │ - ├─ init_session::RunEarlyInit() ← 早期初始化 - │ └─ 日志、路径、配置 - │ - ├─ init_session::RunStageInit() ← 阶段初始化 - │ └─ 系统检测、资源预加载 - │ - ├─ boot_desktop() ← 桌面启动 - │ │ - │ ├─ CFDesktopEntity::instance() ← 创建单例 - │ │ ├─ new CFDesktop(this) ← 创建桌面 Widget - │ │ ├─ new PlatformFactory ← 创建策略工厂 - │ │ │ - │ │ └─ platform::native_display() ← 注册显示后端 - │ │ └─ DisplayServerBackendFactory::register_creator() - │ │ - │ ├─ CFDesktopEntity::run_init() - │ │ ├─ 创建 IDesktopDisplaySizeStrategy - │ │ ├─ CFDesktopWindowProxy::set_window_display_strategy() - │ │ ├─ DisplayServerBackendFactory::make_unique() ← 创建后端 - │ │ │ └─ native_display_impl() - │ │ │ ├─ Windows: new WindowsDisplayServerBackend - │ │ │ └─ WSL: new WSLDisplayServerBackend - │ │ │ - │ │ ├─ display_backend_->initialize() - │ │ │ ├─ connectToX11() / Win32 初始化 - │ │ │ ├─ window_backend_->startTracking() - │ │ │ └─ connect signals - │ │ │ - │ │ └─ connect window_came/gone → 日志 - │ │ - │ ├─ CFDesktopWindowProxy::show_desktop() ← 显示桌面窗口 - │ └─ init_session::ReleaseStageInitOldResources() - │ - ├─ QApplication::exec() ← 事件循环 - │ - ├─ CFDesktopEntity::release() ← 清理(QApplication 存活时) - └─ Logger::flush_sync() ← 刷日志 -```text - -### 7.2 CFDesktopEntity 生命周期 - -`CFDesktopEntity` 是桌面环境的中央单例,管理整个生命周期: - -```text -构造 ──────────────────────────────────────── 析构 - │ │ - ├─ CFDesktop* (Widget) ├─ stopTracking() - ├─ PlatformFactory (策略) ├─ xcb_disconnect() - ├─ DisplayServerBackend (后端) └─ 清理 Widget - │ └─ IWindowBackend (窗口跟踪) - │ └─ IWindow[] (窗口列表) - │ - └─ run_init() 初始化全部组件 -```yaml - -**关键约束**:`CFDesktopEntity::release()` 必须在 `QApplication` 存活时调用,因为其内部持有 `QWidget` 实例。 - ---- - -## 八、构建系统概览 - -### 8.1 顶层 CMake 结构 - -```cmake -project(CFDesktop VERSION 0.13.1) - -# 预配置:WSL 检测、构建类型、Qt 设置 -include(cmake/check_pre_configure.cmake) -include(cmake/generate_meta_info.cmake) - -# 三大模块,按依赖顺序添加 -add_subdirectory(base) # 1. 基础库 -add_subdirectory(ui) # 2. UI 框架(依赖 base) -add_subdirectory(desktop) # 3. 桌面实现(依赖 base + ui) - -# 辅助模块 -add_subdirectory(example) # 示例应用 -add_subdirectory(test) # 测试套件 -```bash - -### 8.2 条件编译 - -| 条件 | 效果 | -|------|------| -| `WIN32` | 编译 Windows 平台代码,链接 Win32 系统库 | -| `UNIX AND NOT APPLE` + `IS_WSL` | 编译 Linux/WSL 平台代码,链接 XCB | -| `CFDESKTOP_OS_WSL` | C++ 宏,标记 WSL 环境 | -| `CFDESKTOP_HAS_XCB` | C++ 宏,标记 XCB 可用 | - -### 8.3 共享库构建技巧 - -所有静态库通过 `--whole-archive` 链接到 `CFDesktop_shared`,确保即使没有被直接引用的符号(如工厂注册函数)也被包含: - -```cmake -target_link_libraries(CFDesktop_shared PRIVATE - "-Wl,--whole-archive" - ${CFDESKTOP_STATIC_LIBS} - "-Wl,--no-whole-archive" -) -```cmake - -最终可执行文件 `CFDesktop` 仅包含 `main.cpp`,链接 `CFDesktop_shared`。 - ---- - -## 九、WSL/X11 后端文件清单 - -本次实现新增的 WSL X11 后端文件: - -| 文件路径 | 职责 | -|----------|------| -| `desktop/ui/platform/linux_wsl/wsl_x11_window.h` | `WSLX11Window` — IWindow 的 XCB 实现 | -| `desktop/ui/platform/linux_wsl/wsl_x11_window.cpp` | 标题查询、几何操作、关闭/置顶 | -| `desktop/ui/platform/linux_wsl/wsl_x11_window_backend.h` | `WSLX11WindowBackend` — IWindowBackend 的 XCB 实现 | -| `desktop/ui/platform/linux_wsl/wsl_x11_window_backend.cpp` | Atom 缓存、事件监听、窗口过滤、信号发射 | -| `desktop/ui/platform/linux_wsl/wsl_x11_display_server_backend.h` | `WSLDisplayServerBackend` — IDisplayServerBackend 实现 | -| `desktop/ui/platform/linux_wsl/wsl_x11_display_server_backend.cpp` | XCB 连接管理、初始化/关闭、信号转发 | - -修改的现有文件: - -| 文件路径 | 变更 | -|----------|------| -| `desktop/ui/platform/linux_wsl/linux_wsl_platform.cpp` | `native_display_impl()` 从返回 nullptr 改为返回后端创建函数 | -| `desktop/ui/platform/CMakeLists.txt` | 添加 XCB 依赖(`pkg_check_modules` + `target_link_libraries`) | - ---- +## 当前状态 -*本文档随项目开发持续更新。* +已实现。详细接口规格参见 HandBook,构建产物与 CMake 目标参见 CLAUDE.md 模块地图。 diff --git a/document/development/README.md b/document/development/README.md deleted file mode 100644 index f894365ae..000000000 --- a/document/development/README.md +++ /dev/null @@ -1,173 +0,0 @@ ---- -title: CFDesktop 开发环境文档 -description: 欢迎使用 CFDesktop 开发环境设置指南。本文档系列将帮助您搭建完整的开发环境,从基础工具安装 ---- - -# CFDesktop 开发环境文档 - -欢迎使用 CFDesktop 开发环境设置指南。本文档系列将帮助您搭建完整的开发环境,从基础工具安装到高级配置,逐步引导您成为 CFDesktop 开发者。 - ---- - -## 项目简介 - -**CFDesktop** 是一个基于 Qt6 的现代化嵌入式桌面框架,旨在为各种嵌入式设备提供统一、现代化的桌面环境。 - -### 核心特性 - -- **跨平台支持**: Windows 10/11、Ubuntu 22.04+、Debian 12+ -- **多架构支持**: x86_64、ARM64、ARMhf -- **性能自适应**: 根据设备硬件能力自动调整 UI 特效和功能 -- **Material Design 3**: 完整实现的现代化 UI 组件库 -- **模块化设计**: 松耦合架构,便于裁剪和定制 - -### 技术栈 - -| 技术 | 版本 | 用途 | -|:---|:---:|:---| -| **C++** | C++23 | 核心开发语言 | -| **Qt** | 6.8.3+ | UI 框架 | -| **CMake** | 3.16+ | 构建系统 | -| **Docker** | 最新 | 多架构构建验证 | -| **Git** | 最新 | 版本控制 | - ---- - -## 文档导航 - -| 文档 | 内容 | 预计时间 | -|:---|:---|:---:| -| [01. 前置要求](01_prerequisites.md) | 硬件要求、操作系统支持、必需软件安装 | 15-30 分钟 | -| [02. 快速开始](02_quick_start.md) | 最快速的方式启动项目 | 5-10 分钟 | -| [04. 开发工具](04_development_tools.md) | 代码格式化、静态分析、调试工具 | 10-15 分钟 | -| [05. Docker 构建](05_docker_build.md) | Docker 多架构构建指南 | 15-20 分钟 | -| [06. Git Hooks](06_git_hooks.md) | Pre-commit 和 Pre-push Hook 使用说明 | 10-15 分钟 | -| [07. 常见问题](07_troubleshooting.md) | 问题排查和解决方案 | - | - ---- - -## 环境要求速览 - -### 硬件要求 - -| 组件 | 最低配置 | 推荐配置 | -|:---|:---:|:---:| -| **CPU** | 4 核心 | 8 核心以上 | -| **RAM** | 8GB | 16GB 或更多 | -| **硬盘** | 20GB 可用空间 | 50GB+ SSD | - -### 操作系统支持 - -| 平台 | 支持版本 | 工具链 | -|:---|:---|:---| -| **Windows** | Windows 10/11 | MinGW 或 LLVM | -| **Linux** | Ubuntu 22.04+, Debian 12+ | GCC 或 Clang | - -### 必需软件 - -| 软件 | 最低版本 | 推荐版本 | -|:---|:---:|:---:| -| **Docker Desktop** | 最新稳定版 | 最新版 | -| **Git** | 2.30+ | 最新版 | -| **VSCode** | (推荐) 最新版 | 最新版 | -| **Qt6** | 6.8.3 | 6.8.3+ | -| **CMake** | 3.16 | 3.20+ | -| **Python** | 3.8+ | 3.10+ (用于 aqtinstall) | - ---- - -## 推荐开发流程 - -```mermaid -graph LR - A[1. 环境准备] --> B[2. 克隆项目] - B --> C[3. 配置 Qt6] - C --> D[4. 首次构建] - D --> E[5. 运行测试] - E --> F[6. 开始开发] - - style A fill:#4CAF50 - style B fill:#2196F3 - style C fill:#9C27B0 - style D fill:#FF9800 - style E fill:#FF5722 - style F fill:#9E9E9E -```text - -### 快速开始 - -```bash -# 1. 克隆项目 -git clone https://github.com/your-org/CFDesktop.git -cd CFDesktop - -# 2. Windows 快速构建 -.\scripts\build_helpers\windows_fast_develop_build.ps1 - -# 3. Linux 快速构建 -./scripts/build_helpers/linux_fast_develop_build.sh -```yaml - ---- - -## 下一步 - -请按照文档顺序阅读: - -1. **[01. 前置要求](01_prerequisites.md)** - 确保您的开发环境满足所有要求 -2. **[02. 快速开始](02_quick_start.md)** - 快速上手开发 -3. **[03. 构建系统](03_build_system.md)** - 了解 CMake 构建系统 -4. **[04. 开发工具](04_development_tools.md)** - 配置您喜欢的开发工具 - ---- - -## 获取帮助 - -### 问题反馈 - -如果您在环境设置过程中遇到问题: - -- **GitHub Issues**: [提交问题](https://github.com/your-org/CFDesktop/issues) -- **讨论区**: [GitHub Discussions](https://github.com/your-org/CFDesktop/discussions) -- **文档**: 查看项目根目录下的 [README](../../) - -### 常见问题 - -**Q: 必须使用 Docker 吗?** - -A: 不是必须的,但推荐使用 Docker 进行多架构构建验证。本地开发可以直接使用 Qt6 和 CMake。 - -**Q: 可以使用其他 IDE 吗?** - -A: 可以。项目主要配置 VSCode + Clangd,但也支持 QtCreator 和其他支持 CMake 的 IDE。 - -**Q: Windows 下推荐使用 MinGW 还是 LLVM?** - -A: 两者都支持。LLVM/Clang 通常有更好的兼容性和错误信息,MinGW 则更轻量。 - ---- - -## 附录 - -### 相关链接 - -- [Qt6 官方文档](https://doc.qt.io/qt-6/) -- [CMake 官方文档](https://cmake.org/documentation/) -- [Docker 官方文档](https://docs.docker.com/) -- [aqtinstall 文档](https://aqtinstall.readthedocs.io/) - -### 文档更新 - -- **版本**: 0.13.1 -- **最后更新**: 2026-03-30 -- **维护者**: CFDesktop 开发团队 - ---- - -
- - [返回项目首页](../index.md) | [前置要求 →](01_prerequisites.md) - - **CFDesktop** - 为嵌入式设备打造的现代化桌面框架 - -
diff --git a/document/development/index.md b/document/development/index.md index dce85f3f7..df7fb0118 100644 --- a/document/development/index.md +++ b/document/development/index.md @@ -1,6 +1,6 @@ --- title: 开发指南 -description: 本节面向重新启动开发时的日常入口。,1. 当前项目状态 +description: 本节面向重新启动开发时的日常入口。 --- # 开发指南 @@ -33,4 +33,46 @@ QT_QPA_PLATFORM=offscreen ctest --test-dir out/build_develop/test --output-on-fa pnpm install pnpm dev pnpm build -```text +``` + +## 环境要求速览 + +### 硬件要求 + +| 组件 | 最低配置 | 推荐配置 | +|:---|:---:|:---:| +| **CPU** | 4 核心 | 8 核心以上 | +| **RAM** | 8GB | 16GB 或更多 | +| **硬盘** | 20GB 可用空间 | 50GB+ SSD | + +### 操作系统支持 + +| 平台 | 支持版本 | 工具链 | +|:---|:---|:---| +| **Windows** | Windows 10/11 | MinGW 或 LLVM | +| **Linux** | Ubuntu 22.04+, Debian 12+ | GCC 或 Clang | + +### 必需软件 + +| 软件 | 最低版本 | 推荐版本 | +|:---|:---:|:---:| +| **Docker Desktop** | 最新稳定版 | 最新版 | +| **Git** | 2.30+ | 最新版 | +| **VSCode** | (推荐) 最新版 | 最新版 | +| **Qt6** | 6.8.3 | 6.8.3+ | +| **CMake** | 3.16 | 3.20+ | +| **Python** | 3.8+ | 3.10+ (用于 aqtinstall) | + +## 快速克隆与构建 + +```bash +# 1. 克隆项目 +git clone https://github.com/Charliechen114514/CFDesktop.git +cd CFDesktop + +# 2. Windows 快速构建 +.\scripts\build_helpers\windows_fast_develop_build.ps1 + +# 3. Linux 快速构建 +./scripts/build_helpers/linux_fast_develop_build.sh +``` diff --git a/document/notes/01-Desktop-Behavior-Modeling-From-Bool-To-QFlags.md b/document/notes/01-Desktop-Behavior-Modeling-From-Bool-To-QFlags.md index a1443c493..2c65840c5 100644 --- a/document/notes/01-Desktop-Behavior-Modeling-From-Bool-To-QFlags.md +++ b/document/notes/01-Desktop-Behavior-Modeling-From-Bool-To-QFlags.md @@ -1,1109 +1,27 @@ --- title: 桌面行为建模:从 bool 到 QFlags -description: 1. 问题背景:为什么 struct bool 不够用 +description: 用 QFlags 替代 struct bool 字段,为窗口行为建模 --- -# 桌面行为建模:从 bool 到 QFlags +# 桌面行为建模:从 bool 到 QFlags -- 设计意图 -## 目录 +## 为什么选择这种方案 -1. [问题背景:为什么 struct bool 不够用](#问题背景为什么-struct-bool-不够用) -2. [Flag 模型的引入与核心思想](#flag-模型的引入与核心思想) -3. [Qt QFlags 深度解析](#qt-qflags-深度解析) -4. [工程化实践范式](#工程化实践范式) -5. [设计原则与最佳实践](#设计原则与最佳实践) -6. [常见陷阱与解决方案](#常见陷阱与解决方案) -7. [与其他方案的对比](#与其他方案的对比) +CFDesktop 需要描述窗口的多种行为特性(全屏、无边框、置底、可调整大小等)。最初考虑使用 `struct { bool fullscreen; bool frameless; ... }` 的朴素方案,但该方案存在根本缺陷:字段不可通过位运算组合、每次扩展字段都破坏 ABI、内存占用高(N 字节 vs 1-4 字节)、且无法与 Qt 元对象系统集成。 ---- - -## 问题背景:为什么 struct bool 不够用 - -### 传统 bool 结构体方案 - -在桌面应用程序开发中,我们经常需要描述窗口的各种行为特性。最直观的实现方式是使用包含多个布尔字段的结构体: - -```cpp -// ❌ 传统的 bool 结构体方案 -struct DesktopBehavior { - bool fullscreen; // 是否全屏 - bool frameless; // 是否无边框 - bool stayOnTop; // 是否置顶 - bool stayOnBottom; // 是否置底 - bool allowResize; // 是否允许调整大小 - bool avoidSystemUI; // 是否避开系统UI - bool transparent; // 是否透明 - bool clickThrough; // 是否点击穿透 -}; -```text - -### 方案缺陷分析 - -这种朴素的实现方式存在以下严重问题: - -#### 1. 内存效率低下 - -```cpp -sizeof(DesktopBehavior) // 通常为 8 × bool = 8 字节(甚至更多由于对齐) -```text - -虽然 8 字节看起来不大,但当我们需要存储大量行为配置时,这种内存开销会变得显著。更重要的是,bool 类型的操作通常不是原子的,在多线程环境下需要额外的同步机制。 - -#### 2. 不可组合性 - -```cpp -// ❌ 无法方便地组合行为 -DesktopBehavior b1 = {true, false, false, false, false, false, false, false}; -DesktopBehavior b2 = {false, true, false, false, false, false, false, false}; - -// 如何得到"全屏且无边框"的行为? -// 需要逐字段手动合并,繁琐且容易出错 -```text - -#### 3. 扩展性差 - -每次添加新的行为特性都需要: - -1. 修改结构体定义(破坏 ABI 兼容性) -2. 更新所有初始化代码 -3. 修改序列化/反序列化逻辑 -4. 调整所有相关的比较和复制操作 - -```cpp -// 添加新字段需要修改所有现有代码 -struct DesktopBehavior { - // ... 原有字段 - bool newFeature; // 新增字段 -}; -```text - -#### 4. 不适合策略系统 - -在策略模式中,我们经常需要: - -- 将多个行为组合成一个能力集合 -- 快速判断是否具备某个能力 -- 高效地传递行为配置 - -bool 结构体在这些场景下表现不佳: - -```cpp -// ❌ 判断能力繁琐 -bool hasFullscreen(const DesktopBehavior& b) { - return b.fullscreen; -} - -// ❌ 组合能力需要逐字段操作 -DesktopBehavior combine(const DesktopBehavior& a, const DesktopBehavior& b) { - return { - a.fullscreen || b.fullscreen, - a.frameless || b.frameless, - // ... 需要处理每个字段 - }; -} -```text - -### 更好的方案:Bitmask 模型 - -计算机科学中,处理多个独立布尔值的经典方法是使用位掩码(Bitmask): - -```text -位 0: Fullscreen -位 1: Frameless -位 2: StayOnTop -位 3: StayOnBottom -位 4: AllowResize -位 5: AvoidSystemUI -... -```yaml - -这种方式的优势: - -- **紧凑存储**:8 个行为只需 1 个字节(或更少的位数) -- **原生支持组合**:使用位运算符 `|`、`&`、`^` -- **高效查询**:使用位掩码和 `&` 操作符 -- **可扩展**:新增行为只需新增位值 - ---- - -## Flag 模型的引入与核心思想 - -### 基本概念 - -Flag 模型的核心思想是: - -> **行为 = 多个独立标志位的组合** - -每个标志位(Flag)代表一个独立的行为特性,多个标志位可以通过位运算组合成一个完整的行为描述。 - -### C++ 枚举作为标志位 - -使用枚举类型定义标志位是最常见的做法: - -```cpp -// ✅ 使用枚举定义行为标志 -enum DesktopBehaviorFlag { - FullscreenFlag = 1 << 0, // 二进制: 00000001 - FramelessFlag = 1 << 1, // 二进制: 00000010 - StayOnTopFlag = 1 << 2, // 二进制: 00000100 - StayOnBottomFlag = 1 << 3, // 二进制: 00001000 - AllowResizeFlag = 1 << 4, // 二进制: 00010000 - AvoidSystemUIFlag = 1 << 5, // 二进制: 00100000 - TransparentFlag = 1 << 6, // 二进制: 01000000 - ClickThroughFlag = 1 << 7, // 二进制: 10000000 -}; -```text - -### 标志位组合 - -```cpp -// ✅ 使用位运算符组合标志位 -unsigned int behaviors = FullscreenFlag | FramelessFlag; -// 结果: 00000011 (同时具备全屏和无边框特性) - -// 添加更多特性 -behaviors |= StayOnTopFlag; -// 结果: 00000111 (全屏 + 无边框 + 置顶) -```text - -### 标志位测试 - -```cpp -// ✅ 使用位运算测试特性 -bool isFullscreen = (behaviors & FullscreenFlag) != 0; -bool isFrameless = (behaviors & FramelessFlag) != 0; -```text - -### 标志位移除 - -```cpp -// ✅ 使用位运算移除特性 -behaviors &= ~FullscreenFlag; // 移除全屏特性 -// 结果: 00000110 -```yaml - ---- - -## Qt QFlags 深度解析 - -Qt 框架提供了 `QFlags` 模板类,这是一个类型安全的位标志实现,被广泛应用于 Qt 的各个模块中。 - -### 官方文档定义 - -根据 [Qt 6.10.2 QFlags 官方文档](https://doc.qt.io/qt-6/qflags.html): - -> "The QFlags class provides a type-safe way of storing OR-combinations of enum values." - -### QFlags 的核心特性 - -#### 1. 类型安全 - -与裸 `unsigned int` 相比,`QFlags` 提供了编译时类型检查: - -```cpp -// ❌ 裸整数实现 - 无类型检查 -unsigned int behaviors = FullscreenFlag | 123; // 编译通过,但语义错误 - -// ✅ QFlags 实现 - 有类型检查 -QFlags behaviors = FullscreenFlag | FramelessFlag; -// behaviors = FullscreenFlag | 123; // 编译错误 -```text - -#### 2. 运算符友好 - -`QFlags` 重载了所有必要的位运算符: - -```cpp -QFlags b1 = FullscreenFlag | FramelessFlag; -QFlags b2 = StayOnTopFlag; - -// 位或(组合) -auto combined = b1 | b2; - -// 位与(交集) -auto intersection = b1 & b2; - -// 异或(对称差) -auto difference = b1 ^ b2; - -// 取反 -auto inverted = ~b1; - -// 复合赋值 -b1 |= b2; -b1 &= b2; -b1 ^= b2; -```text - -#### 3. QVariant / MetaObject 兼容 - -通过 Qt 的元对象系统,`QFlags` 可以与 `QVariant` 无缝集成: - -```cpp -QVariant var = QVariant::fromValue(fullscreen | frameless); -auto flags = var.value>(); -```text - -### QFlags 声明宏 - -Qt 提供了两个重要的宏来简化 `QFlags` 的使用: - -#### Q_DECLARE_FLAGS - -```cpp -// 在类中声明标志类型 -class DesktopWindow { -public: - enum DesktopBehaviorFlag { - FullscreenFlag = 1 << 0, - FramelessFlag = 1 << 1, - // ... - }; - - Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) - // 等价于: typedef QFlags DesktopBehaviors; -}; -```text - -#### Q_DECLARE_OPERATORS_FOR_FLAGS - -```cpp -// 在类外声明运算符 -Q_DECLARE_OPERATORS_FOR_FLAGS(DesktopWindow::DesktopBehaviors) -```text - -这个宏为标志类型声明全局的 `operator|()` 函数,使得: - -```cpp -DesktopWindow::DesktopBehaviors behaviors = - DesktopWindow::FullscreenFlag | DesktopWindow::FramelessFlag; -```text - -### testFlag() 方法 - -`QFlags` 提供了便捷的 `testFlag()` 方法用于测试标志位: - -```cpp -DesktopBehaviors behaviors = FullscreenFlag | FramelessFlag; - -// ✅ 使用 testFlag() -if (behaviors.testFlag(FullscreenFlag)) { - // ... -} - -// 等价于: -if ((behaviors & FullscreenFlag) != 0) { - // ... -} -```text - -### Q_ENUM 与 Q_FLAGS 集成 - -从 Qt 5.5 开始,可以使用 `Q_ENUM` 和 `Q_FLAGS` 宏将枚举和标志注册到元对象系统: - -```cpp -class DesktopWindow : public QObject { - Q_OBJECT -public: - enum DesktopBehaviorFlag { - FullscreenFlag = 1 << 0, - FramelessFlag = 1 << 1, - // ... - }; - Q_ENUM(DesktopBehaviorFlag) - Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) - Q_FLAGS(DesktopBehaviors) -}; -```text - -这样做的优势: - -1. **QML 互操作**:可以在 QML 中使用这些枚举和标志 -2. **字符串转换**:`QMetaEnum` 提供枚举值与字符串的相互转换 -3. **调试支持**:调试器可以显示符号名称而不是数字值 - -### 完整示例 - -```cpp -#include - -// 1. 定义枚举 -enum class DesktopBehaviorFlag { - None = 0, - Fullscreen = 1 << 0, - Frameless = 1 << 1, - StayOnTop = 1 << 2, - StayOnBottom = 1 << 3, - AllowResize = 1 << 4, - AvoidSystemUI = 1 << 5, -}; - -// 2. 声明标志类型 -Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) - -// 3. 声明运算符 -Q_DECLARE_OPERATORS_FOR_FLAGS(DesktopBehaviors) - -// 4. 使用 -int main() { - // 创建行为组合 - DesktopBehaviors behaviors = DesktopBehaviorFlag::Fullscreen - | DesktopBehaviorFlag::Frameless - | DesktopBehaviorFlag::StayOnTop; - - // 测试标志位 - if (behaviors.testFlag(DesktopBehaviorFlag::Fullscreen)) { - qDebug() << "Fullscreen enabled"; - } - - // 添加标志位 - behaviors |= DesktopBehaviorFlag::AllowResize; - - // 移除标志位 - behaviors &= ~DesktopBehaviorFlag::StayOnTop; - - // 检查是否有任何标志位 - if (behaviors != DesktopBehaviorFlag::None) { - qDebug() << "Has behaviors"; - } - - return 0; -} -```yaml - ---- - -## 工程化实践范式 - -### 基础定义 - -```cpp -// DesktopBehavior.h -#pragma once -#include - -namespace desktop { - -// 行为标志枚举 -enum class DesktopBehaviorFlag { - None = 0, - Fullscreen = 1 << 0, - Frameless = 1 << 1, - StayOnTop = 1 << 2, - StayOnBottom = 1 << 3, - AllowResize = 1 << 4, - AvoidSystemUI = 1 << 5, - Transparent = 1 << 6, - ClickThrough = 1 << 7, - Modal = 1 << 8, - Popup = 1 << 9, - Tool = 1 << 10, - Splash = 1 << 11, -}; - -// 声明标志类型 -Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) - -// 声明运算符 -Q_DECLARE_OPERATORS_FOR_FLAGS(DesktopBehaviors) - -} // namespace desktop -```text - -### 常用预定义组合 - -```cpp -// DesktopBehavior.h -namespace desktop { - -// 常用行为组合 -constexpr auto NormalBehavior = DesktopBehaviorFlag::None; -constexpr auto FullscreenBehavior = DesktopBehaviorFlag::Fullscreen; -constexpr auto FramelessBehavior = DesktopBehaviorFlag::Frameless; -constexpr auto AlwaysOnTopBehavior = DesktopBehaviorFlag::StayOnTop | DesktopBehaviorFlag::Frameless; -constexpr auto WidgetBehavior = DesktopBehaviorFlag::Tool | DesktopBehaviorFlag::Frameless | DesktopBehaviorFlag::StayOnTop; -constexpr auto SplashBehavior = DesktopBehaviorFlag::Splash | DesktopBehaviorFlag::Frameless | DesktopBehaviorFlag::StayOnTop; - -} // namespace desktop -```text - -### 行为查询接口 - -```cpp -// DesktopBehaviorQuery.h -#pragma once -#include "DesktopBehavior.h" -#include - -namespace desktop { - -// 行为查询接口 -class IDesktopBehaviorQuery { -public: - virtual ~IDesktopBehaviorQuery() = default; - - // 查询所有行为 - virtual DesktopBehaviors behaviors() const = 0; - - // 查询单个行为 - virtual bool hasBehavior(DesktopBehaviorFlag flag) const = 0; - - // 查询行为组合 - virtual bool hasAnyBehavior(DesktopBehaviors flags) const = 0; - virtual bool hasAllBehaviors(DesktopBehaviors flags) const = 0; - - // 行为描述(用于日志/调试) - virtual QString behaviorDescription() const = 0; -}; - -// 默认实现 -class DesktopBehaviorQuery : public IDesktopBehaviorQuery { -public: - explicit DesktopBehaviorQuery(DesktopBehaviors behaviors) - : m_behaviors(behaviors) {} - - DesktopBehaviors behaviors() const override { - return m_behaviors; - } - - bool hasBehavior(DesktopBehaviorFlag flag) const override { - return m_behaviors.testFlag(flag); - } - - bool hasAnyBehavior(DesktopBehaviors flags) const override { - return (m_behaviors & flags) != DesktopBehaviorFlag::None; - } - - bool hasAllBehaviors(DesktopBehaviors flags) const override { - return (m_behaviors & flags) == flags; - } - - QString behaviorDescription() const override { - QStringList flags; - if (m_behaviors.testFlag(DesktopBehaviorFlag::Fullscreen)) - flags << "Fullscreen"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::Frameless)) - flags << "Frameless"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::StayOnTop)) - flags << "StayOnTop"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::StayOnBottom)) - flags << "StayOnBottom"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::AllowResize)) - flags << "AllowResize"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::AvoidSystemUI)) - flags << "AvoidSystemUI"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::Transparent)) - flags << "Transparent"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::ClickThrough)) - flags << "ClickThrough"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::Modal)) - flags << "Modal"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::Popup)) - flags << "Popup"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::Tool)) - flags << "Tool"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::Splash)) - flags << "Splash"; - - return flags.isEmpty() ? "None" : flags.join(", "); - } - -private: - DesktopBehaviors m_behaviors; -}; - -} // namespace desktop -```text - -### 行为修改接口 - -```cpp -// DesktopBehaviorModifier.h -#pragma once -#include "DesktopBehavior.h" -#include - -namespace desktop { - -// 行为修改接口 -class IDesktopBehaviorModifier { -public: - virtual ~IDesktopBehaviorModifier() = default; - - // 设置行为 - virtual void setBehavior(DesktopBehaviorFlag flag, bool enabled = true) = 0; - - // 批量设置行为 - virtual void setBehaviors(DesktopBehaviors flags) = 0; - - // 添加行为(不覆盖现有行为) - virtual void addBehavior(DesktopBehaviorFlag flag) = 0; - virtual void addBehaviors(DesktopBehaviors flags) = 0; - - // 移除行为 - virtual void removeBehavior(DesktopBehaviorFlag flag) = 0; - virtual void removeBehaviors(DesktopBehaviors flags) = 0; - - // 切换行为 - virtual void toggleBehavior(DesktopBehaviorFlag flag) = 0; - - // 清除所有行为 - virtual void clearBehaviors() = 0; -}; - -// 默认实现 -class DesktopBehaviorModifier : public IDesktopBehaviorModifier { -public: - explicit DesktopBehaviorModifier(DesktopBehaviors initialBehaviors = DesktopBehaviorFlag::None) - : m_behaviors(initialBehaviors) {} - - void setBehavior(DesktopBehaviorFlag flag, bool enabled) override { - if (enabled) { - m_behaviors |= flag; - } else { - m_behaviors &= ~flag; - } - } - - void setBehaviors(DesktopBehaviors flags) override { - m_behaviors = flags; - } - - void addBehavior(DesktopBehaviorFlag flag) override { - m_behaviors |= flag; - } - - void addBehaviors(DesktopBehaviors flags) override { - m_behaviors |= flags; - } - - void removeBehavior(DesktopBehaviorFlag flag) override { - m_behaviors &= ~flag; - } - - void removeBehaviors(DesktopBehaviors flags) override { - m_behaviors &= ~flags; - } - - void toggleBehavior(DesktopBehaviorFlag flag) override { - m_behaviors ^= flag; - } - - void clearBehaviors() override { - m_behaviors = DesktopBehaviorFlag::None; - } - - DesktopBehaviors behaviors() const { - return m_behaviors; - } - -private: - DesktopBehaviors m_behaviors; -}; - -} // namespace desktop -```text - -### 与 QWidget 集成 - -```cpp -// DesktopWindowBehavior.h -#pragma once -#include "DesktopBehavior.h" -#include "DesktopBehaviorQuery.h" -#include "DesktopBehaviorModifier.h" -#include - -namespace desktop { - -// QWidget 行为扩展 -class DesktopWindowBehavior : public QWidget, public IDesktopBehaviorQuery { -public: - explicit DesktopWindowBehavior(QWidget* parent = nullptr) - : QWidget(parent) {} - - // 从当前窗口状态查询行为 - DesktopBehaviors behaviors() const override { - DesktopBehaviors result = DesktopBehaviorFlag::None; - - if (isFullScreen()) - result |= DesktopBehaviorFlag::Fullscreen; - if (windowFlags() & Qt::FramelessWindowHint) - result |= DesktopBehaviorFlag::Frameless; - if (windowFlags() & Qt::WindowStaysOnTopHint) - result |= DesktopBehaviorFlag::StayOnTop; - if (windowFlags() & Qt::WindowStaysOnBottomHint) - result |= DesktopBehaviorFlag::StayOnBottom; - - // 推断行为 - QSize minSize = minimumSize(); - QSize maxSize = maximumSize(); - if (minSize.isEmpty() && maxSize.isEmpty()) - result |= DesktopBehaviorFlag::AllowResize; - - return result; - } - - bool hasBehavior(DesktopBehaviorFlag flag) const override { - return behaviors().testFlag(flag); - } - - bool hasAnyBehavior(DesktopBehaviors flags) const override { - return (behaviors() & flags) != DesktopBehaviorFlag::None; - } - - bool hasAllBehaviors(DesktopBehaviors flags) const override { - return (behaviors() & flags) == flags; - } - - QString behaviorDescription() const override { - return DesktopBehaviorQuery(behaviors()).behaviorDescription(); - } - - // 应用行为到窗口 - void applyBehaviors(DesktopBehaviors behaviors) { - Qt::WindowFlags flags = windowFlags(); - - // 清除相关标志 - flags &= ~(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowStaysOnBottomHint); - - // 应用新标志 - if (behaviors.testFlag(DesktopBehaviorFlag::Frameless)) - flags |= Qt::FramelessWindowHint; - if (behaviors.testFlag(DesktopBehaviorFlag::StayOnTop)) - flags |= Qt::WindowStaysOnTopHint; - if (behaviors.testFlag(DesktopBehaviorFlag::StayOnBottom)) - flags |= Qt::WindowStaysOnBottomHint; - - setWindowFlags(flags); - - // 全屏处理 - if (behaviors.testFlag(DesktopBehaviorFlag::Fullscreen)) { - showFullScreen(); - } else if (isFullScreen()) { - showNormal(); - } - - // 大小调整 - if (!behaviors.testFlag(DesktopBehaviorFlag::AllowResize)) { - setFixedSize(size()); - } - } -}; - -} // namespace desktop -```yaml - ---- - -## 设计原则与最佳实践 - -### 1. Flag = 能力集合(Capability Set) - -设计标志位时,应该将其视为"能力集合"而非"状态集合": - -```cpp -// ✅ 正确:描述能力 -enum class CapabilityFlag { - CanFullscreen = 1 << 0, // 支持全屏的能力 - CanResize = 1 << 1, // 支持调整大小的能力 -}; - -// ❌ 错误:描述状态 -enum class StateFlag { - IsFullscreen = 1 << 0, // 当前是否全屏 - IsResizing = 1 << 1, // 当前是否正在调整大小 -}; -```text - -### 2. 不返回单个枚举值 - -查询接口应该返回标志组合而非单个枚举值: - -```cpp -// ❌ 错误:返回单个枚举 -DesktopBehaviorFlag getCurrentBehavior() { - return DesktopBehaviorFlag::Fullscreen; -} - -// ✅ 正确:返回标志组合 -DesktopBehaviors getCurrentBehaviors() { - return DesktopBehaviorFlag::Fullscreen | DesktopBehaviorFlag::Frameless; -} -```text - -### 3. 保持 ABI 可扩展 - -使用位偏移而非连续值,方便后续扩展: - -```cpp -// ✅ 正确:使用位移 -enum class DesktopBehaviorFlag { - Fullscreen = 1 << 0, - Frameless = 1 << 1, - StayOnTop = 1 << 2, - // 未来可以添加更多标志 - NewFeature = 1 << 12, -}; - -// ❌ 错误:使用连续值 -enum class DesktopBehaviorFlag { - Fullscreen = 1, - Frameless = 2, - StayOnTop = 3, - // 添加新标志容易冲突 -}; -```text - -### 4. 使用 enum class 提高类型安全 - -C++11 引入的 `enum class` 提供了更强的类型安全: - -```cpp -// ✅ 使用 enum class -enum class DesktopBehaviorFlag { - Fullscreen = 1 << 0, - // ... -}; - -// ❌ 使用普通 enum -enum DesktopBehaviorFlag { - Fullscreen = 1 << 0, - // ... -}; -```text - -`enum class` 的优势: - -- 作用域枚举值,避免命名冲突 -- 隐式转换被禁用,提高类型安全 -- 更明确的前向声明 - -### 5. 提供便捷的测试方法 - -封装常用的测试逻辑: - -```cpp -// ✅ 封装测试方法 -class DesktopBehaviorsUtil { -public: - static bool isFullscreen(const DesktopBehaviors& behaviors) { - return behaviors.testFlag(DesktopBehaviorFlag::Fullscreen); - } - - static bool isFrameless(const DesktopBehaviors& behaviors) { - return behaviors.testFlag(DesktopBehaviorFlag::Frameless); - } - - static bool isAlwaysOnTop(const DesktopBehaviors& behaviors) { - return behaviors.testFlag(DesktopBehaviorFlag::StayOnTop); - } - - static bool isResizable(const DesktopBehaviors& behaviors) { - return behaviors.testFlag(DesktopBehaviorFlag::AllowResize); - } - - // 组合测试 - static bool isNormalWindow(const DesktopBehaviors& behaviors) { - return behaviors == DesktopBehaviorFlag::None; - } - - static bool isDialog(const DesktopBehaviors& behaviors) { - return behaviors.testFlag(DesktopBehaviorFlag::Modal) || - behaviors.testFlag(DesktopBehaviorFlag::Popup); - } -}; -```text - -### 6. 使用 constexpr 编译时计算 - -对于预定义的行为组合,使用 `constexpr` 确保编译时计算: - -```cpp -// ✅ 使用 constexpr -constexpr auto NormalBehavior = DesktopBehaviorFlag::None; -constexpr auto FullscreenBehavior = DesktopBehaviorFlag::Fullscreen; -constexpr auto FramelessBehavior = DesktopBehaviorFlag::Frameless; -constexpr auto AlwaysOnTopBehavior = - DesktopBehaviorFlag::StayOnTop | DesktopBehaviorFlag::Frameless; -```text - -### 7. 文档化标志位含义 - -为每个标志位提供清晰的文档注释: - -```cpp -/** - * @brief 桌面窗口行为标志 - * - * 这些标志位用于描述窗口的各种行为特性。 - * 多个标志位可以使用位运算符组合使用。 - */ -enum class DesktopBehaviorFlag { - None = 0, ///< 无特殊行为 - - /** - * @brief 全屏模式 - * - * 窗口占据整个屏幕,隐藏系统 UI(任务栏等)。 - * 与 Frameless 标志位配合使用时,将创建真正的无边框全屏窗口。 - */ - Fullscreen = 1 << 0, - - /** - * @brief 无边框窗口 - * - * 移除窗口的标题栏和边框。 - * 注意:无边框窗口需要自行实现窗口拖动功能。 - */ - Frameless = 1 << 1, - - /** - * @brief 置顶窗口 - * - * 窗口保持在其他窗口之上。 - * 注意:某些平台(如 Wayland)可能不支持此标志位。 - * - * @see Qt::WindowStaysOnTopHint - */ - StayOnTop = 1 << 2, - - // ... 更多标志位 -}; -```yaml - ---- - -## 常见陷阱与解决方案 - -### 陷阱 1:忘记 Q_DECLARE_OPERATORS_FOR_FLAGS - -```cpp -// ❌ 错误:忘记声明运算符 -Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) -// 缺少 Q_DECLARE_OPERATORS_FOR_FLAGS(DesktopBehaviors) - -DesktopBehaviors b = DesktopBehaviorFlag::Fullscreen | DesktopBehaviorFlag::Frameless; -// 编译错误:没有匹配的 operator| -```text - -**解决方案**:总是成对使用这两个宏 - -```cpp -// ✅ 正确 -Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) -Q_DECLARE_OPERATORS_FOR_FLAGS(DesktopBehaviors) -```text - -参考:[QFlags tutorial - Qt Wiki](https://wiki.qt.io/QFlags_tutorial) - -### 陷阱 2:enum class 隐式转换问题 - -```cpp -// ❌ 错误:enum class 不能隐式转换为整数 -enum class DesktopBehaviorFlag { - Fullscreen = 1 << 0, -}; - -DesktopBehaviorFlag flag = DesktopBehaviorFlag::Fullscreen; -int value = flag; // 编译错误 -```text - -**解决方案**:显式转换 - -```cpp -// ✅ 正确 -int value = static_cast(flag); -```text - -### 陷阱 3:位运算符优先级 - -```cpp -// ❌ 错误:位运算符优先级低于比较运算符 -DesktopBehaviors b = DesktopBehaviorFlag::Fullscreen | DesktopBehaviorFlag::Frameless; -if (b & DesktopBehaviorFlag::Fullscreen == DesktopBehaviorFlag::None) { - // 这永远不会执行!因为 == 优先级高于 & -} - -// 实际解析为:if (b & (DesktopBehaviorFlag::Fullscreen == DesktopBehaviorFlag::None)) -// 即:if (b & false) -// 即:if (b & 0) -```text - -**解决方案**:使用括号 - -```cpp -// ✅ 正确 -if ((b & DesktopBehaviorFlag::Fullscreen) == DesktopBehaviorFlag::None) { - // 或者使用 testFlag() - if (!b.testFlag(DesktopBehaviorFlag::Fullscreen)) { -```text - -### 陷阱 4:移除标志位时忘记取反 - -```cpp -// ❌ 错误:忘记取反 -DesktopBehaviors b = DesktopBehaviorFlag::Fullscreen | DesktopBehaviorFlag::Frameless; -b &= DesktopBehaviorFlag::Fullscreen; -// 结果:只剩下 Fullscreen,而不是移除 Fullscreen - -// 正确的做法是: -b &= ~DesktopBehaviorFlag::Fullscreen; -// 结果:只剩下 Frameless -```text - -### 陷阱 5:标志位冲突 - -```cpp -// ❌ 错误:StayOnTop 和 StayOnBottom 不应该同时存在 -DesktopBehaviors b = DesktopBehaviorFlag::StayOnTop | DesktopBehaviorFlag::StayOnBottom; -```text - -**解决方案**:在应用时进行冲突检测 - -```cpp -// ✅ 正确:添加冲突检测 -class DesktopBehaviorsUtil { -public: - static DesktopBehaviors resolveConflicts(DesktopBehaviors behaviors) { - // StayOnTop 和 StayOnBottom 冲突,优先保留 StayOnTop - if (behaviors.testFlag(DesktopBehaviorFlag::StayOnTop) && - behaviors.testFlag(DesktopBehaviorFlag::StayOnBottom)) { - behaviors &= ~DesktopBehaviorFlag::StayOnBottom; - } - - // Fullscreen 和 AllowResize 冲突 - if (behaviors.testFlag(DesktopBehaviorFlag::Fullscreen) && - behaviors.testFlag(DesktopBehaviorFlag::AllowResize)) { - behaviors &= ~DesktopBehaviorFlag::AllowResize; - } - - return behaviors; - } -}; -```text - -### 陷阱 6:跨平台兼容性 - -某些标志位在不同平台上的行为不一致: - -```cpp -// ⚠️ 警告:StayOnTop 在 Wayland 上可能不工作 -behaviors |= DesktopBehaviorFlag::StayOnTop; -```text - -**解决方案**:添加平台检测 - -```cpp -// ✅ 正确:平台检测 -#include - -class DesktopBehaviorsUtil { -public: - static DesktopBehaviors filterPlatformSupported(DesktopBehaviors behaviors) { -#if defined(Q_OS_WIN) - // Windows 完全支持 - return behaviors; -#elif defined(Q_OS_LINUX) - // Linux 需要区分 X11 和 Wayland - if (QGuiApplication::platformName() == "wayland") { - // Wayland 不支持某些标志位 - behaviors &= ~DesktopBehaviorFlag::StayOnTop; - behaviors &= ~DesktopBehaviorFlag::StayOnBottom; - } - return behaviors; -#else - // 其他平台 - return behaviors; -#endif - } -}; -```bash - -参考:[Wayland and Qt - Qt 6.11 文档](https://doc.qt.io/qt-6/wayland-and-qt.html) - ---- - -## 与其他方案的对比 - -### 方案对比表 - -| 方案 | 内存占用 | 组合性 | 类型安全 | 扩展性 | Qt 集成 | 推荐场景 | -|------|---------|-------|---------|--------|---------|---------| -| struct bool | 高(N 字节) | 差 | 中 | 差 | 需手动转换 | 简单场景,少量标志 | -| QFlags | 低(1-4 字节) | 优 | 优 | 优 | 原生支持 | Qt 项目,推荐使用 | -| std::bitset | 低(1+ 字节) | 中 | 中 | 中 | 需手动转换 | 非 Qt 项目,固定位数 | -| uint32_t | 低(4 字节) | 优 | 差 | 优 | 需手动转换 | 底层代码,性能关键 | - -### QFlags vs std::bitset - -```cpp -// QFlags -enum class Flag { A = 1 << 0, B = 1 << 1 }; -Q_DECLARE_FLAGS(Flags, Flag) -Q_DECLARE_OPERATORS_FOR_FLAGS(Flags) -Flags f = Flag::A | Flag::B; - -// std::bitset -std::bitset<2> f; -f[0] = true; // Flag A -f[1] = true; // Flag B -```text - -**QFlags 优势**: -- 类型安全(编译时检查) -- 符号名称(调试友好) -- Qt 元对象系统集成 -- 运算符更自然 - -**std::bitset 优势**: -- 动态大小(编译时确定) -- 更多操作(count, any, none, all) -- 标准库支持 - -### QFlags vs uint32_t - -```cpp -// QFlags -Flags f = Flag::A | Flag::B; -if (f.testFlag(Flag::A)) { } - -// uint32_t -uint32_t f = 0x03; -if (f & 0x01) { } -```yaml - -**QFlags 优势**: -- 类型安全 -- 自文档化(符号名称) -- 运算符重载 -- testFlag() 方法 - -**uint32_t 优势**: -- 跨语言兼容 -- 序列化简单 -- 底层控制 - ---- - -## 总结 - -### 核心要点 +最终选择 `QFlags` 作为行为描述的统一类型。QFlags 提供编译时类型安全(防止误将不相关枚举混入)、原生位运算符重载(`|`, `&`, `^`, `~`)、与 QVariant/QMetaObject 的无缝集成,以及 `testFlag()` 等便捷 API。使用 `1 << N` 位偏移赋值确保新标志位可以随时追加而不影响现有值。 -1. **类型安全**:`QFlags` 提供了比裸整数更安全的类型系统 -2. **组合能力**:位运算符使得行为组合变得简洁高效 -3. **Qt 集成**:与 `QVariant`、元对象系统、QML 无缝集成 -4. **可扩展性**:使用位偏移确保 ABI 可扩展 -5. **平台兼容**:需要注意跨平台差异,特别是 Wayland +行为标志位在概念上被定义为**能力集合(Capability Set)**而非状态集合,与 Strategy 系统配合描述"窗口应该具备什么行为",而非"窗口当前是什么状态"。 -### 参考资源 +## 关键决策 -- [Qt 6.10.2 QFlags 官方文档](https://doc.qt.io/qt-6/qflags.html) -- [QFlags tutorial - Qt Wiki](https://wiki.qt.io/QFlags_tutorial) -- [New in Qt 5.5: Q_ENUM and the C++ tricks behind it - Woboq](https://woboq.com/blog/q_enum.html) -- [C++11 standard conformant bitmasks using enum class - Stack Overflow](https://stackoverflow.com/questions/12059774/c11-standard-conformant-bitmasks-using-enum-class) -- [Typesafe Enum Class Bitmasks in C++ - StrikerX3.dev](https://www.strikerx3.dev/cpp/2019/02/27/typesafe-enum-class-bitmasks-in-cpp.html) +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| 使用 `QFlags` + `enum class` (bit-shift 赋值) | 类型安全、Qt 集成、可扩展、位运算组合 | `struct { bool ... }`(不可组合、扩展性差) | +| `DesktopBehaviorFlag` 值: `Fullscreen=0x1, Frameless=0x2, StayOnBottom=0x4, AllowResize=0x8, AvoidSystemUI=0x10` | 精简为实际使用的 5 个标志,移除未实现的 StayOnTop/Transparent/ClickThrough/Modal/Popup/Tool/Splash | 原始草案包含 12+ 标志位(多数无对应实现) | +| 通过 `Q_DECLARE_FLAGS` / `Q_DECLARE_OPERATORS_FOR_FLAGS` 宏声明 | 使 `DesktopBehaviorFlag::X \| DesktopBehaviorFlag::Y` 在全局作用域合法 | 手写 `operator|` 重载(冗余且易遗漏) | +| Flag = Capability Set 语义 | 与 Strategy 系统的 `query()` / `action()` 分离对齐 | Flag = State 语义(状态应由 `query()` 运行时检测) | -### 下一步 +## 当前状态 -在下一篇文档中,我们将深入探讨 **Qt 窗口行为解析**,了解如何从 `QWidget` 的状态反推行为,以及跨平台行为的差异处理。 +已实现。标志枚举和 QFlags 声明位于 `desktop/ui/platform/IDesktopDisplaySizeStrategy.h`,作为 Strategy 接口的一部分对外暴露。 diff --git a/document/notes/02-Qt-Window-Behavior-Analysis.md b/document/notes/02-Qt-Window-Behavior-Analysis.md index 6099dfed8..f68bf1c16 100644 --- a/document/notes/02-Qt-Window-Behavior-Analysis.md +++ b/document/notes/02-Qt-Window-Behavior-Analysis.md @@ -1,985 +1,35 @@ --- title: Qt 窗口行为解析:QWidget 到 DesktopBehaviors -description: 1. 引言:为什么需要从 Qt 状态反推行为 +description: Qt WindowFlags 到 DesktopBehaviors 的映射关系及跨平台差异 --- -# Qt 窗口行为解析:QWidget 到 DesktopBehaviors +# Qt 窗口行为解析:QWidget 到 DesktopBehaviors -- 设计意图 -## 目录 +## 为什么选择这种方案 -1. [引言:为什么需要从 Qt 状态反推行为](#引言为什么需要从-qt-状态反推行为) -2. [行为与 Qt API 映射表](#行为与-qt-api-映射表) -3. [从 QWidget 查询行为的标准实现](#从-qwidget-查询行为的标准实现) -4. [跨平台行为差异分析](#跨平台行为差异分析) -5. [不可直接获取的行为推断](#不可直接获取的行为推断) -6. [平台特定的行为查询策略](#平台特定的行为查询策略) -7. [实践示例与最佳实践](#实践示例与最佳实践) +CFDesktop 定义了平台无关的 `DesktopBehaviors` 标志来描述窗口行为意图,但实际执行必须映射到 Qt 的 `QWidget` API。由于 Qt 的窗口状态分散在 `windowFlags()`、`isFullScreen()`、`minimumSize()/maximumSize()` 等多个 API 中,且部分行为(如 AvoidSystemUI)根本没有直接对应的 Qt API,需要通过多条件推断,因此需要一份明确的映射表作为所有平台策略实现的参考契约。 ---- - -## 引言:为什么需要从 Qt 状态反推行为 - -### 问题背景 - -在桌面应用程序开发中,我们经常面临以下场景: - -1. **日志记录需求**:需要记录窗口当前的行为状态用于调试 -2. **状态同步**:多个组件需要了解窗口的当前行为配置 -3. **动态决策**:根据当前窗口状态决定后续操作 -4. **策略融合**:多个策略模块需要基于当前状态进行组合判断 - -在 CFDesktop 项目中,我们定义了 `DesktopBehaviors` 作为抽象的行为描述层,而 Qt 的 `QWidget` 则是底层实现。如何从 `QWidget` 的当前状态准确反推出对应的 `DesktopBehaviors`,是一个需要深入理解 Qt 窗口系统的问题。 - -### Qt 窗口系统概述 - -根据 [Qt 6.11 QWidget 官方文档](https://doc.qt.io/qt-6/qwidget.html): - -> "Window flags are a combination of a type (e.g. Qt::Dialog) and zero or more hints to the window system (e.g. Qt::FramelessWindowHint)." - -Qt 的窗口标志(Window Flags)由两部分组成: - -1. **窗口类型(Window Type)**:定义窗口的基本性质,只能有一个 -2. **窗口提示(Window Hints)**:定制窗口外观和行为的可选标志,可以有多个 - -```cpp -Qt::WindowFlags flags = Qt::Window | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint; -```cpp - -### 反推行为的挑战 - -从 Qt 状态反推行为存在以下挑战: - -| 挑战 | 描述 | 示例 | -|------|------|------| -| **状态分散** | 同一行为可能由多个 API 共同决定 | `AllowResize` 需要检查 `minimumSize()` 和 `maximumSize()` | -| **平台差异** | 同一标志在不同平台表现不同 | `WindowStaysOnBottomHint` 在 X11 上不稳定 | -| **隐式行为** | 某些行为无法直接查询 | `AvoidSystemUI` 是隐式的,需要推断 | -| **状态动态变化** | 窗口状态可能在运行时改变 | `isFullScreen()` 状态可能与创建时的标志不同 | -| **标志组合冲突** | 某些标志组合会产生意外结果 | `FramelessWindowHint` + `WindowTitleHint` | - ---- - -## 行为与 Qt API 映射表 - -### 核心映射表 - -这是 `DesktopBehaviorFlag` 到 Qt API 的完整映射关系: - -| DesktopBehaviorFlag | Qt API / 状态 | 获取方式 | 平台兼容性 | -|---------------------|---------------|---------|-----------| -| **Fullscreen** | `isFullScreen()` | `widget->isFullScreen()` | 全平台 | -| **Frameless** | `Qt::FramelessWindowHint` | `widget->windowFlags() & Qt::FramelessWindowHint` | 全平台 | -| **StayOnTop** | `Qt::WindowStaysOnTopHint` | `widget->windowFlags() & Qt::WindowStaysOnTopHint` | 全平台(Wayland 限制) | -| **StayOnBottom** | `Qt::WindowStaysOnBottomHint` | `widget->windowFlags() & Qt::WindowStaysOnBottomHint` | 全平台(X11 限制) | -| **AllowResize** | `minimumSize()` / `maximumSize()` | 尺寸比较 | 全平台 | -| **AvoidSystemUI** | 多种标志组合 | 推断 | 平台相关 | - -### 详细 API 映射 - -#### 1. Fullscreen(全屏模式) - -```cpp -// 直接查询 API -bool isFullscreen = widget->isFullScreen(); -```text - -**注意事项**: -- `isFullScreen()` 返回窗口当前是否处于全屏状态 -- 与 `showFullScreen()` / `showNormal()` 相关 -- 可能与窗口标志的初始设置不同 - -**平台差异**: -- **Windows**:完全支持,可以隐藏任务栏 -- **X11**:支持,依赖窗口管理器 -- **Wayland**:受限,需要 compositor 支持 -- **macOS**:支持,有自己的全屏动画 - -#### 2. Frameless(无边框窗口) - -```cpp -// 通过窗口标志查询 -Qt::WindowFlags flags = widget->windowFlags(); -bool isFrameless = (flags & Qt::FramelessWindowHint) != 0; -```text - -**相关标志**: -- `Qt::FramelessWindowHint`:无边框窗口 -- `Qt::CustomizeWindowHint`:自定义窗口标题栏 -- `Qt::WindowTitleHint`:窗口有标题栏 -- `Qt::WindowSystemMenuHint`:窗口有系统菜单 - -根据 [Window Flags Example 文档](https://doc.qt.io/qt-6/qtwidgets-widgets-windowflags-example.html): - -> "A window flag is either a type or a hint. A type is used to specify various window-system properties for the widget." - -**平台差异**: -- **Windows**:完全支持,需要自己实现窗口拖动 -- **X11**:支持,但窗口管理器可能添加装饰 -- **Wayland**:支持,但需要客户端装饰 -- **macOS**:受限,系统可能强制添加某些元素 - -#### 3. StayOnTop(置顶窗口) - -```cpp -bool isStayOnTop = (widget->windowFlags() & Qt::WindowStaysOnTopHint) != 0; -```text - -**相关标志**: -- `Qt::WindowStaysOnTopHint`:保持在其他窗口之上 -- `Qt::WindowStaysOnBottomHint`:保持在其他窗口之下(与置顶互斥) - -**平台差异**: -- **Windows**:完全支持 -- **X11**:支持,但依赖窗口管理器 -- **Wayland**:**不支持**,这是 Wayland 的安全限制 -- **macOS**:支持,但等级系统不同 - -#### 4. StayOnBottom(置底窗口) - -```cpp -bool isStayOnBottom = (widget->windowFlags() & Qt::WindowStaysOnBottomHint) != 0; -```text - -**平台差异**: -- **Windows**:完全支持 -- **X11**:**不稳定**,很多窗口管理器不完全支持 -- **Wayland**:**不支持** -- **macOS**:支持有限 - -#### 5. AllowResize(允许调整大小) - -```cpp -// 通过尺寸约束查询 -QSize minSize = widget->minimumSize(); -QSize maxSize = widget->maximumSize(); -bool isResizable = (minSize.isEmpty() || minSize.width() == 0) && - (maxSize.isEmpty() || maxSize.width() == QWIDGETSIZE_MAX); -```text - -**更精确的判断**: - -```cpp -bool isResizable(QWidget* widget) { - QSize minSize = widget->minimumSize(); - QSize maxSize = widget->maximumSize(); - - // 如果最小和最大尺寸相等,则不可调整 - if (minSize.isValid() && maxSize.isValid() && minSize == maxSize) { - return false; - } - - // 检查宽度和高度是否都可以调整 - bool widthResizable = (minSize.width() == 0 || maxSize.width() == QWIDGETSIZE_MAX || - (maxSize.width() > minSize.width())); - bool heightResizable = (minSize.height() == 0 || maxSize.height() == QWIDGETSIZE_MAX || - (maxSize.height() > minSize.height())); - - return widthResizable && heightResizable; -} -```text - -**相关 API**: -- `setFixedSize()`:设置固定大小 -- `setMinimumSize()`:设置最小尺寸 -- `setMaximumSize()`:设置最大尺寸 -- `sizePolicy()`:尺寸策略 - -#### 6. AvoidSystemUI(避开系统 UI) - -这是一个**推断性**的行为标志,没有直接的 API 可以查询。 - -**推断方法**: - -```cpp -bool hasAvoidSystemUI(QWidget* widget) { - Qt::WindowFlags flags = widget->windowFlags(); - - // 方法 1:检查是否全屏 - if (widget->isFullScreen()) { - return true; - } - - // 方法 2:检查是否为 Splash/ToolTip/Popup 类型 - Qt::WindowType type = static_cast(flags & Qt::WindowType_Mask); - if (type == Qt::SplashScreen || type == Qt::ToolTip || type == Qt::Popup) { - return true; - } - - // 方法 3:检查是否为 Tool 类型 + Frameless - if (type == Qt::Tool && (flags & Qt::FramelessWindowHint)) { - return true; - } - - // 方法 4:检查是否有 X11BypassWindowManagerHint - if (flags & Qt::X11BypassWindowManagerHint) { - return true; - } - - return false; -} -```yaml - ---- - -## 从 QWidget 查询行为的标准实现 - -### queryFromWidget 函数实现 - -以下是完整的 `queryFromWidget` 函数实现,用于从 `QWidget` 查询所有行为标志: - -```cpp -#include -#include -#include "IDesktopDisplaySizeStrategy.h" - -namespace cf::desktop::platform_strategy { - -/** - * @brief 从 QWidget 查询当前的桌面行为 - * - * 该函数通过检查 QWidget 的各种状态和属性, - * 推断出当前窗口的 DesktopBehaviors 标志。 - * - * @param widget 要查询的窗口部件 - * @return DesktopBehaviors 查询到的行为标志组合 - */ -DesktopBehaviors queryFromWidget(QWidget* widget) { - if (!widget) { - return DesktopBehaviorFlag::None; - } - - DesktopBehaviors behaviors = DesktopBehaviorFlag::None; - Qt::WindowFlags flags = widget->windowFlags(); - - // 1. 检查全屏状态 - if (widget->isFullScreen()) { - behaviors |= DesktopBehaviorFlag::Fullscreen; - } - - // 2. 检查无边框状态 - if (flags & Qt::FramelessWindowHint) { - behaviors |= DesktopBehaviorFlag::Frameless; - } - - // 3. 检查置底状态 - // 注意:StayOnBottom 与 StayOnTop 互斥,优先检查 StayOnBottom - if (flags & Qt::WindowStaysOnBottomHint) { - behaviors |= DesktopBehaviorFlag::StayOnBottom; - } - - // 4. 检查是否允许调整大小 - QSize minSize = widget->minimumSize(); - QSize maxSize = widget->maximumSize(); - - bool canResizeWidth = (minSize.width() == 0 || maxSize.width() == QWIDGETSIZE_MAX || - (maxSize.width() > minSize.width())); - bool canResizeHeight = (minSize.height() == 0 || maxSize.height() == QWIDGETSIZE_MAX || - (maxSize.height() > minSize.height())); - - if (canResizeWidth && canResizeHeight) { - behaviors |= DesktopBehaviorFlag::AllowResize; - } - - // 5. 推断 AvoidSystemUI - behaviors |= inferAvoidSystemUI(widget); - - // 6. 平台特定的行为过滤 - behaviors = filterPlatformBehaviors(behaviors); - - return behaviors; -} - -/** - * @brief 推断是否避开系统 UI - * - * 避开系统 UI 是一个隐式行为,需要综合多个条件判断。 - */ -DesktopBehaviors inferAvoidSystemUI(QWidget* widget) { - DesktopBehaviors result = DesktopBehaviorFlag::None; - Qt::WindowFlags flags = widget->windowFlags(); - - // 获取窗口类型 - Qt::WindowType type = static_cast(flags & Qt::WindowType_Mask); - - // 检查全屏状态 - if (widget->isFullScreen()) { - result |= DesktopBehaviorFlag::AvoidSystemUI; - } - - // 检查特殊窗口类型 - switch (type) { - case Qt::SplashScreen: - case Qt::ToolTip: - case Qt::Popup: - result |= DesktopBehaviorFlag::AvoidSystemUI; - break; - case Qt::Tool: - // Tool 类型 + Frameless 通常表示小工具 - if (flags & Qt::FramelessWindowHint) { - result |= DesktopBehaviorFlag::AvoidSystemUI; - } - break; - default: - break; - } - - // 检查 X11 Bypass Window Manager 标志 - if (flags & Qt::X11BypassWindowManagerHint) { - result |= DesktopBehaviorFlag::AvoidSystemUI; - } - - return result; -} - -/** - * @brief 过滤平台不支持的行为标志 - * - * 不同平台对窗口行为标志的支持程度不同, - * 这个函数根据当前平台过滤掉不支持的标志。 - */ -DesktopBehaviors filterPlatformBehaviors(DesktopBehaviors behaviors) { -#ifdef Q_OS_WIN - // Windows 平台:完全支持所有标志 - return behaviors; -#elif defined(Q_OS_LINUX) - // Linux 平台:需要区分 X11 和 Wayland - QString platformName = QGuiApplication::platformName(); - - if (platformName == QLatin1String("wayland")) { - // Wayland 平台限制 - behaviors &= ~DesktopBehaviorFlag::StayOnBottom; // Wayland 不支持 - - // StayOnTop 在 Wayland 上的支持有限 - // 某些 compositor 可能通过 layer-shell 协议支持 - // 但标准 Wayland 不支持 - } - // X11 平台:大部分支持,但 StayOnBottom 可能不稳定 - return behaviors; -#elif defined(Q_OS_MACOS) - // macOS 平台:大部分支持,但有特殊限制 - return behaviors; -#else - // 其他平台:保守处理 - return behaviors; -#endif -} - -/** - * @brief 获取行为的描述字符串(用于调试) - */ -QString behaviorDescription(const DesktopBehaviors& behaviors) { - QStringList descriptions; - - if (behaviors.testFlag(DesktopBehaviorFlag::Fullscreen)) { - descriptions << QStringLiteral("Fullscreen"); - } - if (behaviors.testFlag(DesktopBehaviorFlag::Frameless)) { - descriptions << QStringLiteral("Frameless"); - } - if (behaviors.testFlag(DesktopBehaviorFlag::StayOnBottom)) { - descriptions << QStringLiteral("StayOnBottom"); - } - if (behaviors.testFlag(DesktopBehaviorFlag::AllowResize)) { - descriptions << QStringLiteral("AllowResize"); - } - if (behaviors.testFlag(DesktopBehaviorFlag::AvoidSystemUI)) { - descriptions << QStringLiteral("AvoidSystemUI"); - } - - return descriptions.isEmpty() ? - QStringLiteral("None") : - descriptions.join(QStringLiteral(", ")); -} - -} // namespace cf::desktop::platform_strategy -```bash - ---- - -## 跨平台行为差异分析 - -### 平台支持矩阵 - -| 行为标志 | Windows | X11 | Wayland | macOS | EGLFS | Framebuffer | -|---------|---------|-----|---------|-------|-------|-------------| -| Fullscreen | 完全支持 | 支持 | 受限 | 支持 | 支持 | 支持 | -| Frameless | 完全支持 | 支持 | 需客户端装饰 | 受限 | 支持 | 支持 | -| StayOnTop | 完全支持 | 支持 | **不支持** | 支持 | 不支持 | 不支持 | -| StayOnBottom | 完全支持 | **不稳定** | **不支持** | 有限 | 不支持 | 不支持 | -| AllowResize | 完全支持 | 支持 | 支持 | 支持 | 支持 | 受限 | -| AvoidSystemUI | 完全支持 | 受限 | **严格限制** | 受限 | 支持 | 支持 | - -### 平台详细分析 - -#### Windows 平台 - -**特点**:Qt 在 Windows 上的原生平台,支持最全面 - -**完全支持的行为**: -- `Fullscreen`:真正的全屏,可以隐藏任务栏 -- `Frameless`:完全无边框,可以创建自定义标题栏 -- `StayOnTop`:使用 `HWND_TOPMOST` 实现 -- `StayOnBottom`:使用 `HWND_BOTTOM` 实现 - -**特殊标志**: -- `Qt::MSWindowsFixedSizeDialogHint`:创建固定大小的对话框 -- `Qt::WindowSystemMenuHint`:添加系统菜单 - -**代码示例**: - -```cpp -// Windows 平台最佳实践 -#ifdef Q_OS_WIN - // 创建无边框置顶窗口 - Qt::WindowFlags flags = Qt::Window | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint; - widget->setWindowFlags(flags); - - // Windows 特有的无边框处理 - // 需要处理 WM_NCHITTEST 消息实现窗口拖动 -#endif -```bash - -**参考**:[MSDN Window Styles](https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles) - -#### X11 平台 - -**特点**:依赖窗口管理器(Window Manager),行为多样 - -**支持情况**: -- `Fullscreen`:支持,通过 `_NET_WM_STATE_FULLSCREEN` EWMH 提示 -- `Frameless`:支持,通过 `MOTIF_WM_HINTS` 属性 -- `StayOnTop`:支持,通过 `_NET_WM_STATE_ABOVE` -- `StayOnBottom`:**不稳定**,通过 `_NET_WM_STATE_BELOW`,但不是所有 WM 都支持 - -**窗口管理器差异**: - -| 窗口管理器 | StayOnBottom | Frameless | 备注 | -|-----------|-------------|-----------|------| -| KWin | 支持 | 支持 | KDE 默认 WM | -| Mutter | 有限支持 | 支持 | GNOME 默认 WM | -| Openbox | 支持 | 支持 | 轻量级 WM | -| i3/sway | 支持 | 支持 | 平铺式 WM | - -**X11 特有标志**: -- `Qt::X11BypassWindowManagerHint`:绕过窗口管理器 - -```cpp -// X11 平台注意事项 -#ifdef Q_OS_LINUX - if (QGuiApplication::platformName() == QLatin1String("xcb")) { - // X11 特定处理 - - // 检查窗口管理器支持 - // StayOnBottom 在某些 WM 上可能不工作 - if (behaviors.testFlag(DesktopBehaviorFlag::StayOnBottom)) { - qWarning() << "StayOnBottom may not work properly on this window manager"; - } - } -#endif -```text - -#### Wayland 平台 - -**特点**:安全优先的协议,限制客户端的窗口控制能力 - -**核心限制**(根据 [Wayland and Qt 文档](https://doc.qt.io/qt-6/wayland-and-qt.html)): - -> "The Wayland protocol is designed with security and isolation in mind, and is strict/conservative about what information and functionality is available to clients." - -**不支持的行为**: -- `StayOnTop`:**完全不支持**(标准协议) -- `StayOnBottom`:**完全不支持** -- 窗口位置查询:客户端无法查询自己的窗口位置 -- 全局输入捕获:不支持 - -**受限的行为**: -- `Fullscreen`:需要 compositor 支持,通过 `xdg-shell` 协议 -- `Frameless`:需要客户端装饰(CSD),不支持服务端装饰(SSD) - -**Wayland 特殊考虑**: - -```cpp -// Wayland 平台特殊处理 -#ifdef Q_OS_LINUX - if (QGuiApplication::platformName() == QLatin1String("wayland")) { - // 移除不支持的标志 - behaviors &= ~DesktopBehaviorFlag::StayOnTop; - behaviors &= ~DesktopBehaviorFlag::StayOnBottom; - - // Wayland 上的 Frameless 需要 CSD - if (behaviors.testFlag(DesktopBehaviorFlag::Frameless)) { - // 需要实现客户端装饰 - } - } -#endif -```yaml - -**Wayland 协议扩展**: -某些 compositor 提供扩展协议: -- `xdg-shell`:标准的 shell 协议 -- `layer-shell`:用于面板/覆盖层的协议 -- `xdg-decoration`:装饰协商协议 - -#### EGLFS 平台 - -**特点**:嵌入式 Linux 平台,无窗口管理器 - -**支持情况**: -- `Fullscreen`:完全支持(默认行为) -- `Frameless`:支持(默认无装饰) -- `StayOnTop/Bottom`:**不支持**(无窗口管理器) -- `AllowResize`:支持有限 - -**适用场景**: -- 嵌入式设备 -- 信息亭模式 -- 数字标牌 - -#### Framebuffer 平台 - -**特点**:直接写入帧缓冲,最低级别的平台 - -**支持情况**: -- `Fullscreen`:完全支持(唯一模式) -- `Frameless`:完全支持(无窗口系统) -- `StayOnTop/Bottom`:**不支持**(无窗口堆栈) -- `AllowResize`:不支持(单一表面) - -**适用场景**: -- 极简嵌入式系统 -- 启动画面 -- 硬件直接访问 - ---- - -## 不可直接获取的行为推断 - -### 问题概述 - -某些 `DesktopBehaviorFlag` 不能直接从 Qt API 获取,需要通过以下方法推断: - -1. **组合条件推断**:多个条件综合判断 -2. **状态变化监听**:通过事件推断意图 -3. **平台特定 API**:使用原生平台 API -4. **启发式推断**:基于常见模式猜测 - -### AvoidSystemUI 推断详解 - -`AvoidSystemUI` 是最复杂的推断性标志,表示窗口希望避开系统 UI 元素(任务栏、Dock 等)。 - -#### 推断条件 - -```cpp -bool inferAvoidSystemUI(QWidget* widget) { - // 条件 1:全屏模式 - if (widget->isFullScreen()) { - return true; - } - - Qt::WindowFlags flags = widget->windowFlags(); - Qt::WindowType type = static_cast(flags & Qt::WindowType_Mask); - - // 条件 2:特殊窗口类型 - if (type == Qt::SplashScreen || // 启动画面 - type == Qt::ToolTip || // 工具提示 - type == Qt::Popup) { // 弹出菜单 - return true; - } - - // 条件 3:Tool + Frameless(小工具模式) - if (type == Qt::Tool && (flags & Qt::FramelessWindowHint)) { - return true; - } - - // 条件 4:X11 Bypass Window Manager - if (flags & Qt::X11BypassWindowManagerHint) { - return true; - } - - // 条件 5:检查位置(如果窗口位于屏幕边缘) - QRect screenGeometry = widget->screen()->geometry(); - QRect windowGeometry = widget->geometry(); - - // 窗口紧贴屏幕边缘,可能是面板/工具栏 - const int edgeThreshold = 10; - if (windowGeometry.left() <= screenGeometry.left() + edgeThreshold || - windowGeometry.top() <= screenGeometry.top() + edgeThreshold || - windowGeometry.right() >= screenGeometry.right() - edgeThreshold || - windowGeometry.bottom() >= screenGeometry.bottom() - edgeThreshold) { - return true; - } - - return false; -} -```text - -### AllowResize 推断详解 - -`AllowResize` 需要检查多个尺寸相关的属性: - -```cpp -bool inferAllowResize(QWidget* widget) { - // 方法 1:检查最小/最大尺寸 - QSize minSize = widget->minimumSize(); - QSize maxSize = widget->maximumSize(); - - // 如果设置了固定大小,则不允许调整 - if (minSize.isValid() && maxSize.isValid() && minSize == maxSize) { - return false; - } - - // 方法 2:检查 sizePolicy - QSizePolicy policy = widget->sizePolicy(); - if (policy.horizontalPolicy() == QSizePolicy::Fixed && - policy.verticalPolicy() == QSizePolicy::Fixed) { - return false; - } - - // 方法 3:检查是否有固定大小的设置 - // 通过 Qt::MSWindowsFixedSizeDialogHint 等标志 - Qt::WindowFlags flags = widget->windowFlags(); - if (flags & Qt::MSWindowsFixedSizeDialogHint) { - return false; - } - - // 方法 4:检查全屏状态(全屏时不可调整) - if (widget->isFullScreen()) { - return false; - } - - // 默认允许调整 - return true; -} -```text - -### 状态变化监听 - -对于某些行为,需要监听状态变化来推断: - -```cpp -class BehaviorTracker : public QObject { - Q_OBJECT -public: - explicit BehaviorTracker(QWidget* parent) - : QObject(parent), m_widget(parent) { - // 监听窗口标志变化 - connect(parent, &QWidget::windowTitleChanged, - this, &BehaviorTracker::onWindowFlagsChanged); - } - - DesktopBehaviors currentBehaviors() const { - return m_behaviors; - } - -signals: - void behaviorsChanged(DesktopBehaviors behaviors); - -private slots: - void onWindowFlagsChanged(const QString&) { - DesktopBehaviors newBehaviors = queryFromWidget(m_widget); - if (newBehaviors != m_behaviors) { - m_behaviors = newBehaviors; - emit behaviorsChanged(m_behaviors); - } - } - -private: - QWidget* m_widget; - DesktopBehaviors m_behaviors = DesktopBehaviorFlag::None; -}; -```yaml - ---- - -## 平台特定的行为查询策略 - -### 策略模式应用 - -在 CFDesktop 项目中,使用策略模式处理平台差异: - -```cpp -// 平台检测 -QString platformName() { - return QGuiApplication::platformName(); -} - -// 创建平台特定的策略 -std::unique_ptr createPlatformStrategy() { - QString platform = platformName(); - -#ifdef Q_OS_WIN - return std::make_unique(); -#elif defined(Q_OS_LINUX) - if (platform == QLatin1String("wayland")) { - return std::make_unique(); - } else if (platform == QLatin1String("xcb")) { - return std::make_unique(); - } else if (platform.startsWith(QLatin1String("eglfs"))) { - return std::make_unique(); - } -#elif defined(Q_OS_MACOS) - return std::make_unique(); -#endif - return std::make_unique(); -} -```text - -### 平台特定实现 - -#### Windows 策略 - -```cpp -class WindowsDisplaySizePolicy : public IDesktopDisplaySizeStrategy { -public: - DesktopBehaviors query() const override { - DesktopBehaviors behaviors = DesktopBehaviorFlag::None; - - // Windows 平台完全支持所有标志 - // 可以直接查询 - - return behaviors; - } - - bool action(QWidget* widget) override { - // Windows 特定的行为应用逻辑 - return true; - } -}; -```text - -#### Wayland 策略 - -```cpp -class WaylandDisplaySizePolicy : public IDesktopDisplaySizeStrategy { -public: - DesktopBehaviors query() const override { - DesktopBehaviors behaviors = DesktopBehaviorFlag::None; - - // Wayland 平台限制 - // 自动过滤不支持的标志 - - behaviors &= ~DesktopBehaviorFlag::StayOnTop; - behaviors &= ~DesktopBehaviorFlag::StayOnBottom; - - return behaviors; - } - - bool action(QWidget* widget) override { - // Wayland 特定的行为应用逻辑 - // 需要与 compositor 协商 - - return true; - } -}; -```yaml - ---- - -## 实践示例与最佳实践 - -### 完整示例:行为查询工具类 - -```cpp -/** - * @file DesktopBehaviorQuery.h - * @brief 桌面窗口行为查询工具 - */ - -#pragma once -#include -#include -#include "IDesktopDisplaySizeStrategy.h" - -namespace cf::desktop::platform_strategy { - -/** - * @brief 行为查询结果 - */ -struct BehaviorQueryResult { - DesktopBehaviors behaviors; // 查询到的行为 - QString platformName; // 平台名称 - QStringList warnings; // 警告信息 - QStringList unsupported; // 不支持的标志 - - bool isValid() const { - return behaviors != DesktopBehaviorFlag::None || !unsupported.isEmpty(); - } - - QString toString() const { - return QStringLiteral("Platform: %1, Behaviors: %2") - .arg(platformName) - .arg(behaviorDescription(behaviors)); - } -}; - -/** - * @brief 桌面行为查询工具类 - * - * 提供从 QWidget 查询行为的完整功能, - * 包括平台检测、行为推断、警告提示等。 - */ -class DesktopBehaviorQuery { -public: - /** - * @brief 从 widget 查询行为 - * - * @param widget 要查询的窗口部件 - * @return BehaviorQueryResult 查询结果,包含行为、平台、警告等信息 - */ - static BehaviorQueryResult query(QWidget* widget) { - BehaviorQueryResult result; - - if (!widget) { - result.warnings << QStringLiteral("Widget is null"); - return result; - } - - // 1. 获取平台信息 - result.platformName = QGuiApplication::platformName(); - - // 2. 查询基础行为 - result.behaviors = queryFromWidget(widget); - - // 3. 检查平台支持 - checkPlatformSupport(result); - - // 4. 检查行为冲突 - checkBehaviorConflicts(result); - - return result; - } - -private: - /** - * @brief 检查平台对行为的支持情况 - */ - static void checkPlatformSupport(BehaviorQueryResult& result) { - QString platform = result.platformName; - -#ifdef Q_OS_LINUX - if (platform == QLatin1String("wayland")) { - // Wayland 不支持的标志 - if (result.behaviors.testFlag(DesktopBehaviorFlag::StayOnTop)) { - result.unsupported << QStringLiteral("StayOnTop"); - result.warnings << QStringLiteral("StayOnTop is not supported on Wayland"); - } - if (result.behaviors.testFlag(DesktopBehaviorFlag::StayOnBottom)) { - result.unsupported << QStringLiteral("StayOnBottom"); - result.warnings << QStringLiteral("StayOnBottom is not supported on Wayland"); - } - } else if (platform == QLatin1String("xcb")) { - // X11 可能不稳定的标志 - if (result.behaviors.testFlag(DesktopBehaviorFlag::StayOnBottom)) { - result.warnings << QStringLiteral("StayOnBottom may be unstable on X11"); - } - } -#endif - } - - /** - * @brief 检查行为之间的冲突 - */ - static void checkBehaviorConflicts(BehaviorQueryResult& result) { - // Fullscreen 和 AllowResize 冲突 - if (result.behaviors.testFlag(DesktopBehaviorFlag::Fullscreen) && - result.behaviors.testFlag(DesktopBehaviorFlag::AllowResize)) { - result.warnings << QStringLiteral("Fullscreen conflicts with AllowResize"); - } - - // StayOnTop 和 StayOnBottom 冲突 - if (result.behaviors.testFlag(DesktopBehaviorFlag::StayOnTop) && - result.behaviors.testFlag(DesktopBehaviorFlag::StayOnBottom)) { - result.warnings << QStringLiteral("StayOnTop conflicts with StayOnBottom"); - } - } -}; - -} // namespace cf::desktop::platform_strategy -```text - -### 使用示例 - -```cpp -#include "DesktopBehaviorQuery.h" - -void logWidgetBehaviors(QWidget* widget) { - // 查询行为 - auto result = DesktopBehaviorQuery::query(widget); - - // 输出结果 - qDebug() << "Platform:" << result.platformName; - qDebug() << "Behaviors:" << behaviorDescription(result.behaviors); - - // 输出警告 - if (!result.warnings.isEmpty()) { - qWarning() << "Warnings:" << result.warnings; - } - - // 输出不支持的标志 - if (!result.unsupported.isEmpty()) { - qDebug() << "Unsupported on this platform:" << result.unsupported; - } -} -```text - -### 最佳实践总结 - -1. **始终进行平台检测**:在应用任何行为前,检测当前平台 -2. **优雅降级**:对于不支持的标志,提供替代方案或警告 -3. **监听状态变化**:窗口行为可能在运行时改变 -4. **文档化平台限制**:清晰记录每个平台的限制 -5. **提供测试工具**:帮助开发者理解不同平台的行为差异 - -### 调试技巧 - -```cpp -/** - * @brief 调试辅助函数:打印窗口的详细信息 - */ -void debugWindowInfo(QWidget* widget) { - qDebug() << "=== Window Debug Info ==="; - qDebug() << "Platform:" << QGuiApplication::platformName(); - qDebug() << "Window flags:" << widget->windowFlags(); - qDebug() << "Is fullscreen:" << widget->isFullScreen(); - qDebug() << "Is visible:" << widget->isVisible(); - qDebug() << "Is top level:" << widget->isWindow(); - qDebug() << "Geometry:" << widget->geometry(); - qDebug() << "Minimum size:" << widget->minimumSize(); - qDebug() << "Maximum size:" << widget->maximumSize(); - qDebug() << "Size policy:" << widget->sizePolicy(); - qDebug() << "========================"; -} -```yaml - ---- - -## 总结 - -### 核心要点 - -1. **行为反推是必要的**:在日志、同步、决策等场景中需要从 Qt 状态反推行为 -2. **API 映射是复杂的**:同一行为可能涉及多个 Qt API -3. **平台差异显著**:不同平台对窗口行为的支持程度不同 -4. **某些行为需要推断**:如 `AvoidSystemUI`、`AllowResize` 不能直接查询 -5. **策略模式是最佳实践**:使用策略模式处理平台差异 - -### 参考资源 +## 关键决策 -#### Qt 官方文档 +**核心映射表** -- `DesktopBehaviorFlag` 到 Qt API 的查询方式: -- [QWidget Class | Qt Widgets | Qt 6.11.0](https://doc.qt.io/qt-6/qwidget.html) -- [Window Flags Example | Qt Widgets | Qt 6.11.0](https://doc.qt.io/qt-6/qtwidgets-widgets-windowflags-example.html) -- [Wayland and Qt | Qt 6.11](https://doc.qt.io/qt-6/wayland-and-qt.html) -- [Qt Namespace | Qt Core | Qt 6.11.0](https://doc.qt.io/qt-6/qt.html) -- [QWindow Class | Qt GUI | Qt 6.11.0](https://doc.qt.io/qt-6/qwindow.html) -- [Window and Dialog Widgets](https://doc.qt.io/qt-6/application-windows.html) +| DesktopBehaviorFlag | Qt API / 状态 | 获取方式 | +|---------------------|---------------|---------| +| **Fullscreen** | `isFullScreen()` | `widget->isFullScreen()` | +| **Frameless** | `Qt::FramelessWindowHint` | `widget->windowFlags() & Qt::FramelessWindowHint` | +| **StayOnBottom** | `Qt::WindowStaysOnBottomHint` | `widget->windowFlags() & Qt::WindowStaysOnBottomHint` | +| **AllowResize** | `minimumSize()` / `maximumSize()` | min/max 尺寸比较推断 | +| **AvoidSystemUI** | 多条件推断 | 全屏 / SplashScreen+ToolTip+Popup 类型 / Tool+Frameless / X11BypassWM | -#### 相关文档 +**跨平台差异** -- 关键限制: -- [EGLFS - Qt for Linux](https://doc.qt.io/qt-6/embedded-linux.html) -- [LinuxFB Platform Plugin](https://doc.qt.io/qt-6/linuxfb.html) -- [X11 Platform Notes](https://doc.qt.io/qt-6/x11-platform.html) +| 行为 | Windows | X11 | Wayland | EGLFS | +|------|---------|-----|---------|-------| +| Fullscreen | 完全支持 | 支持 | 受限 | 支持 | +| Frameless | 完全支持 | 支持 | 需 CSD | 支持 | +| StayOnBottom | 完全支持 | 不稳定 | 不支持 | 不支持 | +| AvoidSystemUI | 完全支持 | 受限 | 严格限制 | 支持 | -### 下一步 +## 当前状态 -在下一篇文档中,我们将深入探讨 **桌面策略系统设计**,了解如何使用 Strategy 模式来管理不同平台的行为差异。 +映射逻辑通过各平台的 `IDesktopDisplaySizeStrategy` 实现落地:`desktop/ui/platform/windows/windows_display_size_policy.{h,cpp}`(Windows)和 `desktop/ui/platform/linux_wsl/linux_wsl_display_size_policy.{h,cpp}`(WSL/X11)。 diff --git a/document/notes/03-Desktop-Strategy-Pattern-Design.md b/document/notes/03-Desktop-Strategy-Pattern-Design.md index 8fae82169..71787826b 100644 --- a/document/notes/03-Desktop-Strategy-Pattern-Design.md +++ b/document/notes/03-Desktop-Strategy-Pattern-Design.md @@ -1,2141 +1,28 @@ --- title: 桌面策略系统设计(Strategy Pattern 实战) -description: 1. 为什么使用 Strategy 模式 +description: 使用 Strategy 模式 + CQRS 原则管理跨平台窗口行为 --- -# 桌面策略系统设计(Strategy Pattern 实战) +# 桌面策略系统设计(Strategy Pattern 实战) -- 设计意图 -## 目录 +## 为什么选择这种方案 -1. [为什么使用 Strategy 模式](#为什么使用-strategy-模式) - - [1.1 跨平台差异处理](#11-跨平台差异处理) - - [1.2 行为解耦与单一职责](#12-行为解耦与单一职责) - - [1.3 插件化架构支持](#13-插件化架构支持) - - [1.4 测试友好性](#14-测试友好性) -2. [Strategy Pattern 理论基础](#strategy-pattern-理论基础) - - [2.1 设计模式定义](#21-设计模式定义) - - [2.2 UML 结构图](#22-uml-结构图) - - [2.3 C++ 实现要点](#23-c-实现要点) -3. [接口设计:IDesktopDisplaySizeStrategy](#接口设计idesktopdisplaysizestrategy) - - [3.1 基础接口定义](#31-基础接口定义) - - [3.2 策略类型枚举](#32-策略类型枚举) - - [3.3 ABI 友好设计](#33-abi-友好设计) -4. [Action vs Query:核心思想对比](#action-vs-query核心思想对比) - - [4.1 CQRS 原则](#41-cqrs-原则) - - [4.2 Action 方法设计](#42-action-方法设计) - - [4.3 Query 方法设计](#43-query-方法设计) - - [4.4 分离的好处](#44-分离的好处) -5. [示例策略实现](#示例策略实现) - - [5.1 FullscreenStrategy](#51-fullscreenstrategy) - - [5.2 FramelessStrategy](#52-framelessstrategy) - - [5.3 WSL 平台策略](#53-wsl-平台策略) -6. [多策略组合实现](#多策略组合实现) - - [6.1 策略组合模式](#61-策略组合模式) - - [6.2 CompositeStrategy](#62-compositestrategy) - - [6.3 策略链模式](#63-策略链模式) -7. [冲突检测机制](#冲突检测机制) - - [7.1 行为冲突定义](#71-行为冲突定义) - - [7.2 冲突检测接口](#72-冲突检测接口) - - [7.3 冲突解决策略](#73-冲突解决策略) -8. [工厂模式集成](#工厂模式集成) - - [8.1 PlatformFactory 设计](#81-platformfactory-设计) - - [8.2 平台特定工厂](#82-平台特定工厂) - - [8.3 插件化工厂](#83-插件化工厂) -9. [最佳实践与设计原则](#最佳实践与设计原则) +窗口行为在不同操作系统上的实现截然不同(Windows 用 `WS_EX_TOPMOST`、X11 用 EWMH `_NET_WM_STATE_*`、Wayland 通过 xdg-shell 协议且禁止客户端控制 z-order)。如果直接在业务代码中堆砌 `#ifdef` 条件分支,会导致代码无法维护、无法测试、且每新增一个平台就需要修改所有分支。 ---- - -## 为什么使用 Strategy 模式 - -### 1.1 跨平台差异处理 - -在桌面应用开发中,不同操作系统对窗口行为有截然不同的实现方式: - -| 平台特性 | Windows | macOS | Linux (X11) | Linux (Wayland) | WSL | -|---------|---------|-------|------------|----------------|-----| -| 全屏实现 | `WM_SYSCOMMAND` | `NSFullScreenMode` | `_NET_WM_STATE_FULLSCREEN` | xdg-shell 全屏 | 代理实现 | -| 无边框窗口 | `WS_POPUP` | `NSWindowStyleMaskBorderless` | `_NET_WM_WINDOW_TYPE` | xdg-shell popup | 限制支持 | -| 窗口置顶 | `WS_EX_TOPMOST` | `NSWindowLevel` | `_NET_WM_STATE_ABOVE` | 不支持 | 不支持 | -| 窗口层级 | Z-Order | Window Level | EWMH 层级 | Wayland 协议 | 限制支持 | - -这种差异使得单一实现难以应对所有场景,而 Strategy 模式提供了一种优雅的解决方案: - -```cpp -// ❌ 不使用 Strategy:条件分支地狱 -void applyWindowFlags(QWidget* widget) { - #ifdef Q_OS_WIN - // Windows 特定代码 - #elif defined(Q_OS_MACOS) - // macOS 特定代码 - #elif defined(Q_OS_LINUX) - if (QGuiApplication::platformName() == "xcb") { - // X11 特定代码 - } else if (QGuiApplication::platformName() == "wayland") { - // Wayland 特定代码 - } - #endif - // 代码维护困难,难以扩展 -} - -// ✅ 使用 Strategy:平台无关接口 -void applyWindowFlags(QWidget* widget, IDesktopDisplaySizeStrategy* strategy) { - strategy->action(widget); // 平台特定实现被封装 -} -```text - -### 1.2 行为解耦与单一职责 - -Strategy 模式遵循**单一职责原则**(Single Responsibility Principle),将窗口行为的具体实现从主业务逻辑中分离: - -```cpp -// 职责分离示例 -class CFDesktop : public QWidget { - // CFDesktop 专注于: - // 1. 管理桌面组件(PanelManager、ShellLayer) - // 2. 提供代理访问(CFDesktopProxy) - // 3. 协调组件生命周期 - - // 不负责: - // ✗ 平台特定的窗口行为实现 - // ✗ 不同显示模式的具体逻辑 -}; - -// 策略专注于: -// 1. 实现特定平台的窗口行为 -// 2. 提供行为查询接口 -// 3. 管理平台相关的状态 -```text - -这种分离带来的好处: - -1. **代码可读性**:每个策略类职责明确,易于理解 -2. **可维护性**:修改平台特定逻辑不影响其他代码 -3. **可测试性**:可以独立测试每个策略 - -### 1.3 插件化架构支持 - -Strategy 模式为插件化架构提供了天然支持。通过工厂模式 + 策略模式,可以实现运行时动态加载平台实现: - -```cpp -// 插件化架构示例 -namespace cf::desktop::platform_strategy { - -// 平台工厂 API -struct PlatformFactoryAPI { - using CreateFunc = IDesktopPropertyStrategy*(StrategyType); - using ReleaseFunc = void(IDesktopPropertyStrategy*); - - CreateFunc creator_func; - ReleaseFunc release_func; -}; - -// 本地实现 -PlatformFactoryAPI native() noexcept; - -// TODO: 从 DLL 加载 -// PlatformFactoryAPI* remote() noexcept; - -} // namespace -```text - -这支持以下场景: - -1. **运行时平台检测**:根据运行环境自动选择策略 -2. **动态插件加载**:从共享库加载平台实现 -3. **A/B 测试**:可以同时测试不同的策略实现 - -### 1.4 测试友好性 - -Strategy 模式使得单元测试变得简单: - -```cpp -// 测试用例示例 -class MockDisplaySizeStrategy : public IDesktopDisplaySizeStrategy { -public: - const char* name() const noexcept override { - return "Mock Strategy"; - } - - bool action(QWidget* widget) override { - action_called = true; - last_widget = widget; - return true; - } - - DesktopBehaviors query() const override { - return DesktopBehaviorFlag::Fullscreen | DesktopBehaviorFlag::Frameless; - } - - // 测试辅助 - bool action_called = false; - QWidget* last_widget = nullptr; -}; - -// 使用 mock 进行测试 -TEST(DesktopTest, ApplyStrategy) { - MockDisplaySizeStrategy mock_strategy; - CFDesktop desktop; - - // 应用策略 - mock_strategy.action(&desktop); - - // 验证 - EXPECT_TRUE(mock_strategy.action_called); - EXPECT_EQ(mock_strategy.last_widget, &desktop); -} -```yaml - -参考资料: -- [Strategy in C++ / Design Patterns - Refactoring.Guru](https://refactoring.guru/design-patterns/strategy/cpp/example) -- [The Strategy Pattern – MC++ BLOG - Modernes C++](https://www.modernescpp.com/index.php/the-strategy-pattern/) -- [Design Patterns: Elements of Reusable Object-Oriented Software - GoF](https://en.wikipedia.org/wiki/Design_Patterns) - ---- - -## Strategy Pattern 理论基础 - -### 2.1 设计模式定义 - -根据 GoF(Gang of Four)的经典定义: - -> **Strategy Pattern**:定义一系列算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。 - -在桌面行为管理场景中: - -- **算法**:窗口行为的具体实现(全屏、无边框、置顶等) -- **客户**:CFDesktop 或其他需要应用窗口行为的组件 -- **变化**:不同平台、不同配置下的行为差异 - -### 2.2 UML 结构图 - -```text -┌─────────────────────────────────────────────────────────────────────┐ -│ Context (CFDesktop) │ -│ │ -│ - strategy: IDesktopDisplaySizeStrategy │ -│ + setStrategy(strategy: IDesktopDisplaySizeStrategy) │ -│ + applyBehavior() │ -└─────────────────────────────┬───────────────────────────────────────┘ - │ - │ uses - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ <> IDesktopDisplaySizeStrategy │ -│ │ -│ + action(widget: QWidget*): bool │ -│ + query(): DesktopBehaviors │ -│ + name(): const char* │ -└─────────────────────────────┬───────────────────────────────────────┘ - │ - │ implements - ┌───────────────────┼───────────────────┐ - ▼ ▼ ▼ -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│FullscreenStrategy│ │FramelessStrategy│ │ WSLDisplaySize │ -│ │ │ │ │ Strategy │ -│+ action() │ │+ action() │ │+ action() │ -│+ query() │ │+ query() │ │+ query() │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ -```text - -### 2.3 C++ 实现要点 - -#### 虚析构函数 - -策略接口必须声明虚析构函数以确保正确的资源清理: - -```cpp -class IDesktopDisplaySizeStrategy : public IDesktopPropertyStrategy { -public: - virtual ~IDesktopDisplaySizeStrategy() = default; - // ... -}; -```text - -#### 智能指针支持 - -使用智能指针管理策略生命周期: - -```cpp -class PlatformFactory { -public: - using StrategyDeleter = void (*)(IDesktopPropertyStrategy*); - - // 使用 unique_ptr,自定义删除器 - std::unique_ptr - factorize_unique(const IDesktopPropertyStrategy::StrategyType t); - - // 使用 shared_ptr,支持共享所有权 - std::shared_ptr - factorize_shared(const IDesktopPropertyStrategy::StrategyType t); -}; -```text - -#### WeakPtr 集成 - -策略支持 WeakPtr,避免循环引用: - -```cpp -class IDesktopDisplaySizeStrategy : public IDesktopPropertyStrategy { -public: - WeakPtr GetOne() { - return weak_factory_ptr_.GetWeakPtr(); - } - -private: - WeakPtrFactory weak_factory_ptr_; -}; -```yaml - -参考资料: -- [Strategy Design Pattern - GeeksforGeeks](https://www.geeksforgeeks.org/system-design/strategy-pattern-set-1/) -- [C++ Core Guidelines: C.129 - When designing a class hierarchy, distinguish between implementation inheritance and interface inheritance](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-interface) - ---- - -## 接口设计:IDesktopDisplaySizeStrategy - -### 3.1 基础接口定义 - -```cpp -/** - * @file IDesktopPropertyStrategy.h - * @brief 桌面属性策略基础接口 - * - * 所有桌面策略的抽象基类,定义了策略的基本契约。 - */ - -namespace cf::desktop::platform_strategy { - -/** - * @brief 桌面属性策略基类 - * - * @details 提供所有策略共享的类型标识和接口契约 - */ -class IDesktopPropertyStrategy { -public: - /** - * @brief 策略类型枚举 - * - * 用于策略工厂的类型标识,确保类型安全的策略创建 - */ - enum class StrategyType { - Unavailable, ///< 无效/不可用策略 - DisplaySizePolicy, ///< 显示尺寸策略(全屏、无边框等) - Extensions, ///< 扩展策略(输入法、系统托盘等) - }; - - /** - * @brief 构造函数 - * @param t 策略类型 - */ - IDesktopPropertyStrategy(const StrategyType t); - - /** - * @brief 虚析构函数 - * - * 确保通过基类指针删除派生类对象时正确调用派生类析构函数 - */ - virtual ~IDesktopPropertyStrategy() = default; - - /** - * @brief 获取策略名称 - * - * @return 策略的名称字符串,用于调试和日志 - * - * @note 必须由派生类实现,返回有效的字符串字面量 - * @note 使用 noexcept 确保不会抛出异常 - */ - virtual const char* name() const noexcept = 0; - -protected: - const StrategyType type_; ///< 策略类型,运行时类型标识 -}; - -} // namespace cf::desktop::platform_strategy -```text - -### 3.2 策略类型枚举 - -策略类型枚举提供了类型安全的策略创建机制: - -```cpp -/** - * @brief 策略类型到字符串的转换(用于日志) - */ -inline const char* strategyTypeToString(IDesktopPropertyStrategy::StrategyType type) { - switch (type) { - case IDesktopPropertyStrategy::StrategyType::Unavailable: - return "Unavailable"; - case IDesktopPropertyStrategy::StrategyType::DisplaySizePolicy: - return "DisplaySizePolicy"; - case IDesktopPropertyStrategy::StrategyType::Extensions: - return "Extensions"; - default: - return "Unknown"; - } -} -```text - -### 3.3 ABI 友好设计 - -为了确保 ABI(Application Binary Interface)兼容性,接口设计遵循以下原则: - -#### 1. 避免虚模板方法 - -```cpp -// ❌ 不推荐:模板虚方法破坏 ABI -class IDesktopPropertyStrategy { -public: - template - virtual T get() = 0; // 编译错误或 ABI 问题 -}; - -// ✅ 推荐:使用显式类型方法 -class IDesktopPropertyStrategy { -public: - virtual const char* name() const noexcept = 0; - virtual StrategyType type() const noexcept { return type_; } -}; -```text - -#### 2. 使用 Pimpl 模式隐藏实现 - -```cpp -// 头文件:稳定 ABI -class IDesktopPropertyStrategy { -public: - // 公共接口... -private: - class Impl; - std::unique_ptr impl_; // 实现细节 -}; - -// 实现文件:可变实现 -class IDesktopPropertyStrategy::Impl { - // 实现细节可以随意修改而不影响 ABI -}; -```text - -#### 3. 自定义删除器 - -```cpp -// 使用自定义删除器确保跨 DLL 边界的正确释放 -class PlatformFactory { -public: - using StrategyDeleter = void (*)(IDesktopPropertyStrategy*); - - std::unique_ptr - factorize_unique(const IDesktopPropertyStrategy::StrategyType t) { - // 创建策略并使用工厂的删除器 - return { createImpl(t), [](IDesktopPropertyStrategy* p) { - releaseImpl(p); // 使用工厂的释放函数 - }}; - } -}; -```text - -### 3.4 显示策略接口 - -```cpp -/** - * @file IDesktopDisplaySizeStrategy.h - * @brief 桌面显示尺寸策略接口 - * - * 定义了控制窗口显示行为(全屏、无边框等)的策略接口。 - */ - -namespace cf::desktop::platform_strategy { - -/** - * @brief 桌面行为标志枚举 - * - * 使用位标志表示窗口的各种行为特性。 - * 多个标志可以使用位运算符组合。 - */ -enum class DesktopBehaviorFlag { - Fullscreen = 0x1, ///< 全屏模式 - Frameless = 0x2, ///< 无边框窗口 - StayOnBottom = 0x4, ///< 置底窗口 - AllowResize = 0x8, ///< 允许调整大小 - AvoidSystemUI = 0x10, ///< 避开系统 UI -}; - -Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) -Q_DECLARE_OPERATORS_FOR_FLAGS(DesktopBehaviors) - -/** - * @brief 桌面显示尺寸策略接口 - * - * @details 定义了窗口显示行为的操作接口: - * - action(): 应用行为到窗口 - * - query(): 查询当前行为状态 - */ -class IDesktopDisplaySizeStrategy : public IDesktopPropertyStrategy { -public: - virtual ~IDesktopDisplaySizeStrategy() = default; - - /** - * @brief 获取策略的 WeakPtr - * - * @return WeakPtr 指向当前策略实例 - * - * @note 用于避免循环引用 - */ - WeakPtr GetOne() { - return weak_factory_ptr_.GetWeakPtr(); - } - - /** - * @brief 应用窗口行为 - * - * @param widget_data 目标 QWidget 指针 - * @return 成功返回 true,失败返回 false - * - * @note 默认实现不做任何操作,返回 true - * @note 派生类应重写此方法以实现特定行为 - */ - virtual bool action(QWidget* widget_data) { - return true; // 默认:不做任何操作 - } - - /** - * @brief 查询当前窗口行为 - * - * @return 当前行为的标志组合 - * - * @note 默认实现返回 None - * @note 派生类应重写此方法以查询实际状态 - */ - virtual DesktopBehaviors query() const; - -private: - WeakPtrFactory weak_factory_ptr_; -}; - -} // namespace cf::desktop::platform_strategy -```bash - -参考资料: -- [Qt 6.10.2 QFlags 官方文档](https://doc.qt.io/qt-6/qflags.html) -- [C++ ABI Compatibility - Itanium C++ ABI](https://itanium-cxx-abi.github.io/cxx-abi/abi.html) -- [Pimpl Idiom in C++ - Wikipedia](https://en.cppreference.com/w/cpp/language/pimpl) - ---- - -## Action vs Query:核心思想对比 - -### 4.1 CQRS 原则 - -**CQRS**(Command Query Responsibility Segregation)是 Bertrand Meyer 提出的设计原则,在 Martin Fowler 的文章中得到详细阐述: - -> "CQRS stands for Command Query Responsibility Segregation. It's a pattern that separates operations that read data (queries) from operations that modify data (commands)." - -在我们的策略系统中: - -| 类型 | 方法 | 职责 | 副作用 | 返回值 | -|------|------|------|--------|--------| -| **Action** | `action()` | 修改系统状态 | 有 | bool(成功/失败) | -| **Query** | `query()` | 读取系统状态 | 无 | DesktopBehaviors(状态) | - -### 4.2 Action 方法设计 - -Action 方法遵循以下设计原则: - -#### 1. 明确的副作用 - -```cpp -/** - * @brief 应用窗口行为 - * - * @details 此方法会: - * 1. 修改 widget 的窗口标志(Qt::WindowFlags) - * 2. 可能改变 widget 的显示状态 - * 3. 可能影响 widget 的位置和大小 - * - * @param widget_data 目标 QWidget - * @return true 表示操作成功,false 表示失败 - * - * @note 调用此方法会改变 widget 的状态 - * @warning 不应在持有锁的状态下调用此方法(可能触发 Qt 事件) - */ -virtual bool action(QWidget* widget_data); -```text - -#### 2. 返回值表示操作结果 - -```cpp -// ✅ 正确:使用返回值表示成功/失败 -bool result = strategy->action(widget); -if (!result) { - // 处理失败情况 - qWarning() << "Failed to apply strategy:" << strategy->name(); -} - -// ❌ 错误:在 action 中抛出异常(Qt 约定不使用异常) -virtual bool action(QWidget* widget_data) { - if (!widget_data) { - throw std::invalid_argument("widget is null"); // 不推荐 - } -} -```text - -#### 3. 幂等性 - -Action 方法应该是幂等的,多次调用应该产生相同的结果: - -```cpp -class FullscreenStrategy : public IDesktopDisplaySizeStrategy { -public: - bool action(QWidget* widget) override { - if (!widget) return false; - - // 幂等性检查:如果已经是全屏,不需要重复操作 - if (widget->isFullScreen()) { - return true; - } - - widget->showFullScreen(); - return true; - } -}; -```text - -### 4.3 Query 方法设计 - -Query 方法遵循以下设计原则: - -#### 1. 无副作用 - -```cpp -/** - * @brief 查询当前窗口行为 - * - * @details 此方法: - * 1. 不修改任何状态 - * 2. 不触发任何事件 - * 3. 可以安全地在任何上下文中调用 - * - * @return 当前行为的标志组合 - * - * @note 此方法是线程安全的(假设 DesktopBehaviors 是值类型) - * @note 可以多次调用而不影响系统状态 - */ -virtual DesktopBehaviors query() const; -```text - -#### 2. 返回完整信息 - -```cpp -// ✅ 正确:返回完整的状态信息 -DesktopBehaviors behaviors = strategy->query(); -if (behaviors.testFlag(DesktopBehaviorFlag::Fullscreen)) { - // 处理全屏状态 -} - -// ❌ 错误:提供多个查询方法 -bool isFullscreen() const; // 应该使用 query().testFlag() -bool isFrameless() const; // 应该使用 query().testFlag() -```text - -#### 3. const 正确性 - -```cpp -// Query 方法必须是 const 的 -virtual DesktopBehaviors query() const; - -// 允许在 const 上下文中调用 -void monitorDesktop(const IDesktopDisplaySizeStrategy& strategy) { - DesktopBehaviors current = strategy.query(); // OK -} -```text - -### 4.4 分离的好处 - -#### 1. 缓存优化 - -Query 方法可以被安全地缓存: - -```cpp -class CachedDesktopDisplaySizeStrategy : public IDesktopDisplaySizeStrategy { -public: - DesktopBehaviors query() const override { - // 缓存结果,避免重复查询 - if (!cache_valid_) { - cached_behaviors_ = doQuery(); - cache_valid_ = true; - } - return cached_behaviors_; - } - - bool action(QWidget* widget) override { - // Action 操作后使缓存失效 - bool result = doAction(widget); - cache_valid_ = false; - return result; - } - -private: - mutable DesktopBehaviors cached_behaviors_; - mutable bool cache_valid_ = false; -}; -```text - -#### 2. 并发访问 - -Query 方法可以被多个线程同时调用: - -```cpp -// 线程 1:读取状态 -DesktopBehaviors state1 = strategy.query(); - -// 线程 2:同时读取状态(无竞态条件) -DesktopBehaviors state2 = strategy.query(); - -// 线程 3:修改状态(需要同步) -std::lock_guard lock(mtx); -strategy.action(widget); -```text - -#### 3. 前置条件检查 - -在执行 Action 之前检查状态: - -```cpp -// 检查当前状态后再决定是否执行 action -DesktopBehaviors current = strategy.query(); -if (!current.testFlag(DesktopBehaviorFlag::Fullscreen)) { - // 只有在非全屏状态下才执行全屏操作 - strategy.action(widget); -} -```yaml - -参考资料: -- [CQRS - Martin Fowler](https://martinfowler.com/bliki/CQRS.html) -- [Command-Query Separation - Wikipedia](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation) -- [CQRS Pattern - Microsoft Azure Architecture Center](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs) - ---- - -## 示例策略实现 - -### 5.1 FullscreenStrategy - -全屏策略负责将窗口设置为全屏模式: - -```cpp -/** - * @file FullscreenStrategy.h - * @brief 全屏策略实现 - */ - -namespace cf::desktop::platform_strategy { - -/** - * @brief 全屏策略 - * - * @details 将窗口设置为全屏模式,隐藏标题栏和边框 - */ -class FullscreenStrategy : public IDesktopDisplaySizeStrategy { -public: - FullscreenStrategy() = default; - ~FullscreenStrategy() override = default; - - /** - * @brief 获取策略名称 - */ - const char* name() const noexcept override { - return "Fullscreen Strategy"; - } - - /** - * @brief 应用全屏模式 - * - * @param widget 目标窗口 - * @return 成功返回 true - */ - bool action(QWidget* widget) override { - if (!widget) { - qWarning() << "FullscreenStrategy: null widget"; - return false; - } - - // 幂等性检查 - if (widget->isFullScreen()) { - qDebug() << "Widget already in fullscreen mode"; - return true; - } - - // 应用全屏 - widget->showFullScreen(); - - qDebug() << "Applied fullscreen to" << widget; - return true; - } - - /** - * @brief 查询当前状态 - */ - DesktopBehaviors query() const override { - return DesktopBehaviorFlag::Fullscreen | - DesktopBehaviorFlag::Frameless; - } -}; - -} // namespace -```text - -### 5.2 FramelessStrategy - -无边框策略负责移除窗口的标题栏和边框: - -```cpp -/** - * @file FramelessStrategy.h - * @brief 无边框策略实现 - */ - -namespace cf::desktop::platform_strategy { - -/** - * @brief 无边框策略 - * - * @details 移除窗口的标题栏和边框,通常配合自定义标题栏使用 - */ -class FramelessStrategy : public IDesktopDisplaySizeStrategy { -public: - FramelessStrategy() = default; - ~FramelessStrategy() override = default; - - const char* name() const noexcept override { - return "Frameless Strategy"; - } - - bool action(QWidget* widget) override { - if (!widget) { - qWarning() << "FramelessStrategy: null widget"; - return false; - } - - // 获取当前窗口标志 - Qt::WindowFlags flags = widget->windowFlags(); - - // 幂等性检查 - if (flags & Qt::FramelessWindowHint) { - qDebug() << "Widget already frameless"; - return true; - } - - // 添加无边框标志 - flags |= Qt::FramelessWindowHint; - - // 设置窗口标志(会触发窗口重建) - widget->setWindowFlags(flags); - - // 显示窗口(setWindowFlags 会隐藏窗口) - widget->show(); - - qDebug() << "Applied frameless to" << widget; - return true; - } - - DesktopBehaviors query() const override { - return DesktopBehaviorFlag::Frameless; - } -}; - -} // namespace -```text - -### 5.3 WSL 平台策略 - -WSL(Windows Subsystem for Linux)平台的特殊实现: - -```cpp -/** - * @file linux_wsl_display_size_policy.h - * @brief WSL 平台显示策略 - */ - -namespace cf::desktop::platform_strategy::wsl { - -/** - * @brief WSL 显示尺寸策略 - * - * @details WSL 环境下的特殊实现,需要处理 Windows/WSL 边界 - */ -class DisplaySizePolicyMaker : public IDesktopDisplaySizeStrategy { -public: - DisplaySizePolicyMaker(); - ~DisplaySizePolicyMaker(); - - const char* name() const noexcept override { - return "WSL Desktop Size Policy"; - } - - /** - * @brief 应用 WSL 特定的窗口行为 - * - * @details WSL 环境下: - * 1. 需要检测是否在 WSL 内运行 - * 2. 某些窗口标志可能不可用 - * 3. 可能需要通过 WSLg (WSL GUI) 处理 - */ - bool action(QWidget* widget_data) override { - if (!widget_data) { - return false; - } - - // WSL 特定逻辑 - #ifdef Q_OS_WIN - // 运行在 Windows 上,可能需要特殊处理 - #elif defined(Q_OS_LINUX) - // 检测 WSL 环境 - if (isWSLEnvironment()) { - return applyWSLBehavior(widget_data); - } - #endif - - // 默认行为 - return true; - } - - /** - * @brief 查询 WSL 支持的行为 - */ - DesktopBehaviors query() const override { - DesktopBehaviors behaviors = DesktopBehaviorFlag::None; - - #ifdef Q_OS_WIN - // Windows 上支持全屏 - behaviors |= DesktopBehaviorFlag::Fullscreen; - #elif defined(Q_OS_LINUX) - if (isWSLEnvironment()) { - // WSL 限制支持 - behaviors |= DesktopBehaviorFlag::Frameless; - behaviors |= DesktopBehaviorFlag::AllowResize; - } - #endif - - return behaviors; - } - -private: - /** - * @brief 检测是否在 WSL 环境中运行 - */ - bool isWSLEnvironment() const { - // 检查 /proc/version 是否包含 "Microsoft" - QFile versionFile("/proc/version"); - if (versionFile.open(QIODevice::ReadOnly)) { - QByteArray content = versionFile.readAll(); - return content.contains("Microsoft"); - } - return false; - } - - /** - * @brief 应用 WSL 特定的窗口行为 - */ - bool applyWSLBehavior(QWidget* widget) { - // WSLg 特定实现 - // ... - - return true; - } -}; - -} // namespace cf::desktop::platform_strategy::wsl -```text - -### 5.4 组合行为策略 - -一个策略可以组合多个行为标志: - -```cpp -/** - * @brief 桌面覆盖层策略 - * - * @details 实现类似 Windows 小部件的桌面覆盖层: - * - 无边框 - * - 置底 - * - 避开系统 UI - * - 不接受键盘输入 - */ -class DesktopOverlayStrategy : public IDesktopDisplaySizeStrategy { -public: - const char* name() const noexcept override { - return "Desktop Overlay Strategy"; - } - - bool action(QWidget* widget) override { - if (!widget) { - return false; - } - - Qt::WindowFlags flags = widget->windowFlags(); - - // 移除冲突标志 - flags &= ~Qt::WindowStaysOnTopHint; - - // 添加所需标志 - flags |= Qt::FramelessWindowHint; - flags |= Qt::WindowStaysOnBottomHint; - flags |= Qt::WindowDoesNotAcceptFocus; - - widget->setWindowFlags(flags); - widget->show(); - - return true; - } - - DesktopBehaviors query() const override { - return DesktopBehaviorFlag::Frameless | - DesktopBehaviorFlag::StayOnBottom | - DesktopBehaviorFlag::AvoidSystemUI; - } -}; -```yaml - -参考资料: -- [QWidget::setWindowFlags - Qt Documentation](https://doc.qt.io/qt-6/qwidget.html#windowFlags-prop) -- [QWidget::showFullScreen - Qt Documentation](https://doc.qt.io/qt-6/qwidget.html#showFullScreen) -- [WSL GUI (WSLg) Documentation - Microsoft Learn](https://learn.microsoft.com/en-us/windows/wsl/tutorials/gui-apps) - ---- +Strategy 模式将这些平台特定的行为实现封装为独立的策略类,通过统一接口 `IDesktopDisplaySizeStrategy` 暴露给上层。Context(如 CFDesktop 主窗口)只依赖抽象接口,不关心具体平台实现。这带来了四个直接好处:(1) 新平台只需新增策略类,符合开闭原则;(2) 每个策略类只负责单一平台的单一行为维度,符合单一职责原则;(3) 策略可通过工厂 + DLL 插件化加载;(4) 单元测试中可用 Mock 策略替换真实实现。 -## 多策略组合实现 +## 关键决策 -### 6.1 策略组合模式 +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| Strategy 模式封装平台行为 | 消除条件分支地狱、支持开闭原则 | `#ifdef` 全局分支(不可测试、不可扩展) | +| 接口继承链: `IDesktopPropertyStrategy` -> `IDesktopDisplaySizeStrategy` | 基类提供 `StrategyType` 枚举和 `name()` ABI 标识;子类添加 `action()`/`query()` | 扁平单接口(无法区分 DisplaySizePolicy vs Extensions) | +| CQRS: `action(QWidget*) -> bool` + `query() const -> DesktopBehaviors` | Action 有副作用(修改窗口状态)、Query 无副作用(纯读取);分离后 Query 可缓存、可并发调用 | 单一 `apply()` 方法混合读写(无法安全缓存或并发) | +| `WeakPtrFactory` 集成 | 策略可能被多个组件引用,WeakPtr 避免循环引用和悬挂指针 | 原始指针(悬挂风险)、shared_ptr(循环引用风险) | +| 工厂模式 `PlatformFactory` + 自定义删除器 `StrategyDeleter` | 确保跨 DLL 边界的正确分配/释放;`factorize_unique()` / `factorize_shared()` 满足不同所有权需求 | 直接 `new/delete`(跨 DLL 不安全) | +| 冲突检测: `StayOnTop` vs `StayOnBottom` 互斥、`Fullscreen` vs `AllowResize` 冲突 | 某些行为标志语义上不能共存,需在应用前检测并按优先级自动解决 | 静默允许冲突(运行时未定义行为) | +| Action 方法幂等性要求 | `widget->isFullScreen()` 前置检查避免重复操作触发不必要的窗口重建事件 | 无幂等保证(多次调用可能闪烁或丢失状态) | -单个策略往往不足以应对复杂的场景,需要组合多个策略: - -```cpp -/** - * @brief 策略组合接口 - * - * @details 允许将多个策略组合成一个复合策略 - */ -class ICompositeStrategy : public IDesktopDisplaySizeStrategy { -public: - /** - * @brief 添加子策略 - * - * @param strategy 要添加的策略 - * @return 成功返回 true - */ - virtual bool addStrategy(std::shared_ptr strategy) = 0; - - /** - * @brief 移除子策略 - * - * @param strategy 要移除的策略 - * @return 成功返回 true - */ - virtual bool removeStrategy(IDesktopDisplaySizeStrategy* strategy) = 0; - - /** - * @brief 获取子策略数量 - */ - virtual size_t strategyCount() const = 0; - - /** - * @brief 清空所有子策略 - */ - virtual void clearStrategies() = 0; -}; -```text - -### 6.2 CompositeStrategy - -复合策略的实现: - -```cpp -/** - * @file CompositeStrategy.h - * @brief 复合策略实现 - */ - -namespace cf::desktop::platform_strategy { - -/** - * @brief 复合策略 - * - * @details 将多个策略组合成一个,按顺序执行所有策略 - */ -class CompositeStrategy : public ICompositeStrategy { -public: - CompositeStrategy() = default; - ~CompositeStrategy() override = default; - - const char* name() const noexcept override { - return "Composite Strategy"; - } - - bool addStrategy(std::shared_ptr strategy) override { - if (!strategy) { - return false; - } - - strategies_.push_back(strategy); - return true; - } - - bool removeStrategy(IDesktopDisplaySizeStrategy* strategy) override { - auto it = std::remove_if(strategies_.begin(), strategies_.end(), - [strategy](const auto& ptr) { - return ptr.get() == strategy; - }); - - if (it != strategies_.end()) { - strategies_.erase(it, strategies_.end()); - return true; - } - return false; - } - - size_t strategyCount() const override { - return strategies_.size(); - } - - void clearStrategies() override { - strategies_.clear(); - } - - /** - * @brief 应用所有子策略 - * - * @details 按添加顺序执行所有策略 - * 如果任一策略失败,继续执行后续策略 - */ - bool action(QWidget* widget) override { - if (!widget) { - return false; - } - - bool all_success = true; - - for (auto& strategy : strategies_) { - if (!strategy->action(widget)) { - qWarning() << "Strategy" << strategy->name() << "failed"; - all_success = false; - } - } - - return all_success; - } - - /** - * @brief 查询所有子策略的行为组合 - * - * @details 将所有子策略的行为标志组合在一起 - */ - DesktopBehaviors query() const override { - DesktopBehaviors combined = DesktopBehaviorFlag::None; - - for (const auto& strategy : strategies_) { - combined |= strategy->query(); - } - - return combined; - } - -private: - std::vector> strategies_; -}; - -} // namespace -```text - -### 6.3 策略链模式 - -策略链模式允许策略按链式执行,每个策略可以决定是否继续传递: - -```cpp -/** - * @brief 策略链上下文 - * - * @details 包含策略执行过程中的状态信息 - */ -struct StrategyChainContext { - QWidget* widget; ///< 目标窗口 - bool stop_processing; ///< 是否停止后续策略 - DesktopBehaviors applied; ///< 已应用的行为 - QString error_message; ///< 错误消息 - - StrategyChainContext(QWidget* w) - : widget(w), stop_processing(false), applied(DesktopBehaviorFlag::None) {} -}; - -/** - * @brief 链式策略接口 - * - * @details 每个策略可以决定是否继续执行后续策略 - */ -class IChainStrategy : public IDesktopDisplaySizeStrategy { -public: - /** - * @brief 执行策略(链式) - * - * @param widget 目标窗口 - * @param context 链上下文 - * @return 如果设置 context.stop_processing,停止后续策略 - */ - virtual bool process(QWidget* widget, StrategyChainContext& context) = 0; -}; - -/** - * @brief 策略链 - * - * @details 按顺序执行链中的策略,支持提前终止 - */ -class StrategyChain : public IDesktopDisplaySizeStrategy { -public: - const char* name() const noexcept override { - return "Strategy Chain"; - } - - void addStrategy(std::shared_ptr strategy) { - chain_.push_back(strategy); - } - - bool action(QWidget* widget) override { - if (!widget) { - return false; - } - - StrategyChainContext context(widget); - - for (auto& strategy : chain_) { - if (!strategy->process(widget, context)) { - // 策略失败 - qWarning() << "Chain strategy" << strategy->name() << "failed"; - } - - // 检查是否需要停止 - if (context.stop_processing) { - qDebug() << "Chain stopped at" << strategy->name() - << "reason:" << context.error_message; - break; - } - } - - return true; - } - - DesktopBehaviors query() const override { - DesktopBehaviors combined = DesktopBehaviorFlag::None; - for (const auto& strategy : chain_) { - combined |= strategy->query(); - } - return combined; - } - -private: - std::vector> chain_; -}; -```text - -### 6.4 条件策略 - -根据条件选择不同的策略: - -```cpp -/** - * @brief 条件策略 - * - * @details 根据运行时条件选择不同的策略 - */ -class ConditionalStrategy : public IDesktopDisplaySizeStrategy { -public: - using ConditionFunc = std::function; - - ConditionalStrategy( - ConditionFunc condition, - std::shared_ptr true_strategy, - std::shared_ptr false_strategy - ) : condition_(condition), - true_strategy_(true_strategy), - false_strategy_(false_strategy) {} - - const char* name() const noexcept override { - return "Conditional Strategy"; - } - - bool action(QWidget* widget) override { - auto strategy = condition_() ? true_strategy_ : false_strategy_; - if (!strategy) { - return false; - } - return strategy->action(widget); - } - - DesktopBehaviors query() const override { - auto strategy = condition_() ? true_strategy_ : false_strategy_; - if (!strategy) { - return DesktopBehaviorFlag::None; - } - return strategy->query(); - } - -private: - ConditionFunc condition_; - std::shared_ptr true_strategy_; - std::shared_ptr false_strategy_; -}; - -// 使用示例 -auto createConditionalStrategy() { - return std::make_shared( - []() { - // 条件:检测是否在 Wayland 上运行 - return QGuiApplication::platformName() == "wayland"; - }, - std::make_shared(), // true 分支 - std::make_shared() // false 分支 - ); -} -```yaml - -参考资料: -- [Composite Pattern - Refactoring.Guru](https://refactoring.guru/design-patterns/composite) -- [Chain of Responsibility Pattern - Refactoring.Guru](https://refactoring.guru/design-patterns/chain-of-responsibility) - ---- - -## 冲突检测机制 - -### 7.1 行为冲突定义 - -某些行为标志之间存在互斥关系,不能同时应用: - -```cpp -/** - * @brief 行为冲突规则 - * - * @details 定义了行为标志之间的冲突关系 - */ -namespace BehaviorConflictRules { - -/** - * @brief 冲突规则定义 - */ -struct ConflictRule { - DesktopBehaviors flags; ///< 要检查的标志组合 - DesktopBehaviors conflicts_with; ///< 与之冲突的标志 - const char* reason; ///< 冲突原因说明 -}; - -/** - * @brief 定义的所有冲突规则 - */ -inline const std::vector kConflictRules = { - // 置顶和置底冲突 - { - DesktopBehaviorFlag::StayOnTop, - DesktopBehaviorFlag::StayOnBottom, - "Window cannot stay on both top and bottom" - }, - // 全屏模式下通常不允许调整大小 - { - DesktopBehaviorFlag::Fullscreen, - DesktopBehaviorFlag::AllowResize, - "Fullscreen windows typically don't allow resize" - }, - // 避开系统 UI 通常需要无边框 - { - DesktopBehaviorFlag::AvoidSystemUI, - DesktopBehaviorFlag::None, // 不与其他标志冲突 - "AvoidSystemUI is typically used with Frameless" - }, -}; - -} // namespace BehaviorConflictRules -```text - -### 7.2 冲突检测接口 - -```cpp -/** - * @brief 冲突检测结果 - */ -struct ConflictDetectionResult { - bool has_conflicts; ///< 是否存在冲突 - DesktopBehaviors conflicts; ///< 冲突的标志 - std::vector reasons; ///< 冲突原因列表 - - ConflictDetectionResult() - : has_conflicts(false), conflicts(DesktopBehaviorFlag::None) {} - - void addConflict(DesktopBehaviorFlag flag, const char* reason) { - has_conflicts = true; - conflicts |= flag; - reasons.push_back(reason); - } - - QString toString() const { - if (!has_conflicts) { - return "No conflicts"; - } - - QString result = "Conflicts detected:\n"; - for (const auto& reason : reasons) { - result += " - "; - result += reason; - result += "\n"; - } - return result; - } -}; - -/** - * @brief 冲突检测器 - */ -class StrategyConflictDetector { -public: - /** - * @brief 检测行为冲突 - * - * @param behaviors 要检测的行为组合 - * @return 冲突检测结果 - */ - static ConflictDetectionResult detect(DesktopBehaviors behaviors) { - ConflictDetectionResult result; - - for (const auto& rule : BehaviorConflictRules::kConflictRules) { - // 检查是否包含规则中的标志 - if ((behaviors & rule.flags).operator bool()) { - // 检查是否同时包含冲突的标志 - if ((behaviors & rule.conflicts_with).operator bool()) { - result.addConflict(rule.conflicts_with, rule.reason); - } - } - } - - return result; - } - - /** - * @brief 检测策略列表冲突 - * - * @param strategies 策略列表 - * @return 冲突检测结果 - */ - static ConflictDetectionResult detect( - const std::vector>& strategies - ) { - DesktopBehaviors combined = DesktopBehaviorFlag::None; - - for (const auto& strategy : strategies) { - combined |= strategy->query(); - } - - return detect(combined); - } -}; -```text - -### 7.3 冲突解决策略 - -```cpp -/** - * @brief 冲突解决策略枚举 - */ -enum class ConflictResolution { - Fail, ///< 失败,不应用任何行为 - Warning, ///< 警告,应用行为但记录警告 - AutoResolve, ///< 自动解决,移除冲突的行为 - Prioritize ///< 优先级,使用预定义的优先级 -}; - -/** - * @brief 冲突解决器 - */ -class StrategyConflictResolver { -public: - /** - * @brief 解决冲突 - * - * @param behaviors 原始行为 - * @param resolution 解决策略 - * @return 解决后的行为 - */ - static DesktopBehaviors resolve( - DesktopBehaviors behaviors, - ConflictResolution resolution = ConflictResolution::Warning - ) { - auto detection = StrategyConflictDetector::detect(behaviors); - - if (!detection.has_conflicts) { - return behaviors; - } - - switch (resolution) { - case ConflictResolution::Fail: - qWarning() << "Conflict detection failed:" << detection.toString(); - return DesktopBehaviorFlag::None; - - case ConflictResolution::Warning: - qWarning() << "Conflicts detected (applying anyway):" - << detection.toString(); - return behaviors; - - case ConflictResolution::AutoResolve: - return autoResolve(behaviors, detection); - - case ConflictResolution::Prioritize: - return prioritizeResolve(behaviors); - } - - return behaviors; - } - -private: - /** - * @brief 自动解决冲突 - * - * @details 移除冲突的行为标志 - */ - static DesktopBehaviors autoResolve( - DesktopBehaviors behaviors, - const ConflictDetectionResult& detection - ) { - DesktopBehaviors resolved = behaviors; - - // 移除所有冲突的标志 - resolved &= ~detection.conflicts; - - qInfo() << "Auto-resolved conflicts, removed:" << detection.toString(); - - return resolved; - } - - /** - * @brief 优先级解决 - * - * @details 根据预定义的优先级解决冲突 - */ - static DesktopBehaviors prioritizeResolve(DesktopBehaviors behaviors) { - // 定义优先级(高到低) - const std::vector priority = { - DesktopBehaviorFlag::Fullscreen, // 最高优先级 - DesktopBehaviorFlag::Frameless, - DesktopBehaviorFlag::StayOnTop, - DesktopBehaviorFlag::StayOnBottom, - DesktopBehaviorFlag::AvoidSystemUI, - DesktopBehaviorFlag::AllowResize, // 最低优先级 - }; - - // 按优先级检查冲突,保留高优先级的行为 - for (size_t i = 0; i < priority.size(); ++i) { - for (size_t j = i + 1; j < priority.size(); ++j) { - // 检查 i 和 j 是否冲突 - if (behaviors.testFlag(priority[i]) && - behaviors.testFlag(priority[j])) { - - // 检查是否定义了冲突规则 - auto rules = BehaviorConflictRules::kConflictRules; - for (const auto& rule : rules) { - if ((rule.flags & priority[i]).operator bool() || - (rule.conflicts_with & priority[j]).operator bool()) { - // 移除低优先级的行为 - behaviors &= ~priority[j]; - qInfo() << "Prioritized" << static_cast(priority[i]) - << "over" << static_cast(priority[j]); - } - } - } - } - } - - return behaviors; - } -}; -```text - -### 7.4 集成到策略应用 - -```cpp -/** - * @brief 带冲突检测的策略应用器 - */ -class SafeStrategyApplier { -public: - /** - * @brief 应用策略(带冲突检测) - * - * @param widget 目标窗口 - * @param strategy 要应用的策略 - * @param resolution 冲突解决策略 - * @return 成功返回 true - */ - static bool apply( - QWidget* widget, - IDesktopDisplaySizeStrategy* strategy, - ConflictResolution resolution = ConflictResolution::Warning - ) { - if (!widget || !strategy) { - return false; - } - - // 查询行为 - DesktopBehaviors behaviors = strategy->query(); - - // 检测冲突 - auto detection = StrategyConflictDetector::detect(behaviors); - - if (detection.has_conflicts) { - qWarning() << "Strategy" << strategy->name() - << "has conflicts:" << detection.toString(); - - // 解决冲突 - behaviors = StrategyConflictResolver::resolve(behaviors, resolution); - - if (behaviors == DesktopBehaviorFlag::None && - resolution == ConflictResolution::Fail) { - return false; - } - } - - // 应用策略 - return strategy->action(widget); - } -}; -```yaml - -参考资料: -- [Conflict-Free Replicated Data Types (CRDTs) - Wikipedia](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) -- [Semantic Conflict Resolution - Microsoft Research](https://www.microsoft.com/en-us/research/publication/semantic-conflict-resolution/) - ---- - -## 工厂模式集成 - -### 8.1 PlatformFactory 设计 - -工厂模式与策略模式结合,提供策略的创建和管理: - -```cpp -/** - * @file DesktopPropertyStrategyFactory.h - * @brief 桌面属性策略工厂 - */ - -namespace cf::desktop::platform_strategy { - -/** - * @brief 平台工厂 - * - * @details 负责创建和管理平台特定的策略实例 - */ -class PlatformFactory { -public: - PlatformFactory(); - ~PlatformFactory(); - - /** - * @brief 策略删除器类型 - * - * @details 使用自定义删除器确保正确的内存管理 - */ - using StrategyDeleter = void (*)(IDesktopPropertyStrategy*); - - /** - * @brief 创建策略(unique_ptr 版本) - * - * @param t 策略类型 - * @return 策略的 unique_ptr,使用自定义删除器 - * - * @note 返回的策略调用者拥有所有权 - * @note 使用工厂的删除器确保跨 DLL 边界的安全释放 - */ - std::unique_ptr - factorize_unique(const IDesktopPropertyStrategy::StrategyType t); - - /** - * @brief 创建策略(shared_ptr 版本) - * - * @param t 策略类型 - * @return 策略的 shared_ptr - * - * @note 允许多个调用者共享策略所有权 - * @note 使用工厂的删除器确保正确的释放 - */ - std::shared_ptr - factorize_shared(const IDesktopPropertyStrategy::StrategyType t); - - /** - * @brief 检查策略类型是否可用 - * - * @param t 策略类型 - * @return 可用返回 true - */ - bool isAvailable(const IDesktopPropertyStrategy::StrategyType t) const; - -private: - class PlatformImpl; - std::unique_ptr impl_; -}; - -} // namespace -```text - -### 8.2 平台特定工厂 - -每个平台可以提供自己的工厂实现: - -```cpp -/** - * @file linux_wsl_factory.h - * @brief WSL 平台特定工厂 - */ - -namespace cf::desktop::platform_strategy::wsl { - -/** - * @brief WSL 桌面属性策略工厂 - * - * @details WSL 平台的策略工厂实现 - */ -class WSLDeskProStrategyFactory : public SimpleSingleton { -public: - WSLDeskProStrategyFactory(); - ~WSLDeskProStrategyFactory(); - - /** - * @brief 创建策略 - * - * @param t 策略类型 - * @return 策略指针,由工厂管理生命周期 - * - * @note 调用者不应手动删除返回的策略 - * @note 使用 release() 释放策略 - */ - IDesktopPropertyStrategy* create(IDesktopPropertyStrategy::StrategyType t); - - /** - * @brief 释放策略 - * - * @param ptr 要释放的策略指针 - * - * @note 只有通过 create() 创建的策略才能通过此方法释放 - */ - void release(IDesktopPropertyStrategy* ptr); - -private: - std::unique_ptr sz_policy; -}; - -} // namespace -```text - -### 8.3 插件化工厂 - -支持从动态库加载策略: - -```cpp -/** - * @brief 插件策略工厂 - * - * @details 从动态库加载策略实现 - */ -class PluginStrategyFactory { -public: - /** - * @brief 加载插件 - * - * @param plugin_path 插件路径 - * @return 成功返回 true - * - * @note 插件必须实现 PluginStrategyInterface - */ - bool loadPlugin(const QString& plugin_path); - - /** - * @brief 卸载插件 - * - * @param plugin_path 插件路径 - */ - void unloadPlugin(const QString& plugin_path); - - /** - * @brief 从插件创建策略 - * - * @param plugin_path 插件路径 - * @param strategy_name 策略名称 - * @return 策略指针 - */ - std::shared_ptr - createFromPlugin(const QString& plugin_path, const QString& strategy_name); - - /** - * @brief 获取所有可用的插件 - */ - QStringList availablePlugins() const; - -private: - struct PluginInfo { - QPluginLoader* loader; - QString name; - QString version; - QStringList strategies; - }; - - QHash plugins_; -}; - -/** - * @brief 插件策略接口 - * - * @details 所有插件策略必须实现此接口 - */ -class PluginStrategyInterface { -public: - virtual ~PluginStrategyInterface() = default; - - /** - * @brief 插件名称 - */ - virtual QString pluginName() const = 0; - - /** - * @brief 插件版本 - */ - virtual QString pluginVersion() const = 0; - - /** - * @brief 创建策略 - * - * @param strategy_name 策略名称 - * @return 策略指针 - */ - virtual IDesktopPropertyStrategy* createStrategy(const QString& strategy_name) = 0; - - /** - * @brief 获取支持的策略列表 - */ - virtual QStringList supportedStrategies() const = 0; -}; -```yaml - -参考资料: -- [How to Create Qt Plugins - Qt Documentation](https://doc.qt.io/qt-6/plugins-howto.html) -- [Factory Method Pattern - Refactoring.Guru](https://refactoring.guru/design-patterns/factory-method) -- [Abstract Factory Pattern - Refactoring.Guru](https://refactoring.guru/design-patterns/abstract-factory) - ---- - -## 最佳实践与设计原则 - -### 9.1 策略设计原则 - -#### 1. 单一职责 - -每个策略应该只负责一个明确的行为: - -```cpp -// ✅ 正确:单一职责 -class FullscreenStrategy : public IDesktopDisplaySizeStrategy { - // 只负责全屏行为 -}; - -// ❌ 错误:多重职责 -class WindowStrategy : public IDesktopDisplaySizeStrategy { - // 同时负责全屏、无边框、置顶等多个行为 - bool action(QWidget* widget) override { - widget->showFullScreen(); - widget->setWindowFlags(widget->windowFlags() | Qt::FramelessWindowHint); - // ... 更多行为 - } -}; -```text - -#### 2. 开闭原则 - -对扩展开放,对修改封闭: - -```cpp -// ✅ 正确:通过添加新策略扩展功能 -class NewBehaviorStrategy : public IDesktopDisplaySizeStrategy { - // 新功能通过新策略实现,不修改现有代码 -}; - -// ❌ 错误:修改现有策略添加新功能 -class FullscreenStrategy : public IDesktopDisplaySizeStrategy { - bool action(QWidget* widget) override { - widget->showFullScreen(); - // 添加了与全屏无关的新功能 - addNewFeature(widget); // 违反开闭原则 - } -}; -```text - -#### 3. 依赖倒置 - -依赖抽象而非具体实现: - -```cpp -// ✅ 正确:依赖抽象接口 -class CFDesktop { -private: - IDesktopDisplaySizeStrategy* display_strategy_; // 抽象接口 -}; - -// ❌ 错误:依赖具体实现 -class CFDesktop { -private: - FullscreenStrategy* display_strategy_; // 具体实现 -}; -```text - -### 9.2 命名约定 - -```cpp -// 策略接口命名:I + 功能 + Strategy -class IDesktopDisplaySizeStrategy; - -// 具体策略命名:功能 + Strategy -class FullscreenStrategy; -class FramelessStrategy; - -// 平台特定策略:平台 + 功能 + Strategy -class WSLDisplaySizePolicy; -class WindowsDisplaySizePolicy; - -// 工厂命名:功能 + Factory -class PlatformFactory; -class StrategyFactory; - -// 组合策略:Composite + 功能 -class CompositeStrategy; -class StrategyChain; -```text - -### 9.3 错误处理 - -```cpp -/** - * @brief 策略错误类型 - */ -enum class StrategyError { - None, ///< 无错误 - NullWidget, ///< 空指针 - NotSupported, ///< 不支持的操作 - Conflict, ///< 行为冲突 - Failed ///< 操作失败 -}; - -/** - * @brief 策略结果 - */ -struct StrategyResult { - bool success; ///< 成功标志 - StrategyError error; ///< 错误类型 - QString message; ///< 错误消息 - - static StrategyResult ok() { - return {true, StrategyError::None, QString()}; - } - - static StrategyResult fail(StrategyError error, const QString& msg) { - return {false, error, msg}; - } -}; - -// 在策略中使用 -class FullscreenStrategy : public IDesktopDisplaySizeStrategy { -public: - bool action(QWidget* widget) override { - if (!widget) { - last_result_ = StrategyResult::fail( - StrategyError::NullWidget, - "Widget pointer is null" - ); - return false; - } - - // ... 执行操作 - - last_result_ = StrategyResult::ok(); - return true; - } - - StrategyResult getLastResult() const { - return last_result_; - } - -private: - StrategyResult last_result_; -}; -```text - -### 9.4 性能考虑 - -```cpp -/** - * @brief 缓存策略结果 - */ -class CachedDisplaySizeStrategy : public IDesktopDisplaySizeStrategy { -public: - CachedDisplaySizeStrategy(std::shared_ptr impl) - : impl_(impl), cache_valid_(false) {} - - bool action(QWidget* widget) override { - bool result = impl_->action(widget); - - // action 后使缓存失效 - cache_valid_ = false; - - return result; - } - - DesktopBehaviors query() const override { - if (cache_valid_) { - return cached_behaviors_; - } - - cached_behaviors_ = impl_->query(); - cache_valid_ = true; - - return cached_behaviors_; - } - -private: - std::shared_ptr impl_; - mutable DesktopBehaviors cached_behaviors_; - mutable bool cache_valid_; -}; -```text - -### 9.5 线程安全 - -```cpp -/** - * @brief 线程安全的策略 - */ -class ThreadSafeDisplaySizeStrategy : public IDesktopDisplaySizeStrategy { -public: - ThreadSafeDisplaySizeStrategy(std::shared_ptr impl) - : impl_(impl) {} - - bool action(QWidget* widget) override { - std::lock_guard lock(mutex_); - return impl_->action(widget); - } - - DesktopBehaviors query() const override { - std::lock_guard lock(mutex_); - return impl_->query(); - } - -private: - std::shared_ptr impl_; - mutable std::mutex mutex_; -}; -```text - -### 9.6 测试策略 - -```cpp -// 测试辅助类 -class StrategyTestHelper { -public: - /** - * @brief 创建测试窗口 - */ - static std::unique_ptr createTestWidget() { - auto widget = std::make_unique(); - widget->resize(800, 600); - return widget; - } - - /** - * @brief 验证策略结果 - */ - static bool verifyStrategyResult( - IDesktopDisplaySizeStrategy* strategy, - const DesktopBehaviors& expected - ) { - DesktopBehaviors actual = strategy->query(); - return actual == expected; - } -}; - -// 测试用例 -TEST(FullscreenStrategyTest, ApplyFullscreen) { - FullscreenStrategy strategy; - auto widget = StrategyTestHelper::createTestWidget(); - - // 应用策略 - bool result = strategy.action(widget.get()); - EXPECT_TRUE(result); - - // 验证结果 - EXPECT_TRUE(StrategyTestHelper::verifyStrategyResult( - &strategy, - DesktopBehaviorFlag::Fullscreen | DesktopBehaviorFlag::Frameless - )); -} -```yaml - ---- - -## 总结 - -### 核心要点 - -1. **Strategy Pattern 适用场景**: - - 跨平台差异处理 - - 行为解耦与单一职责 - - 插件化架构支持 - - 测试友好设计 - -2. **Action vs Query 分离**: - - Action:修改状态,有副作用 - - Query:读取状态,无副作用 - - 遵循 CQRS 原则 - -3. **接口设计**: - - 虚析构函数 - - ABI 友好设计 - - WeakPtr 支持 - - 自定义删除器 - -4. **多策略组合**: - - Composite Pattern - - Chain of Responsibility - - 条件策略 - -5. **冲突检测**: - - 定义冲突规则 - - 自动检测冲突 - - 多种解决策略 - -### 参考资源 - -#### 官方文档 -- [Qt 6.10.2 QFlags 官方文档](https://doc.qt.io/qt-6/qflags.html) -- [How to Create Qt Plugins - Qt Documentation](https://doc.qt.io/qt-6/plugins-howto.html) -- [QWidget::setWindowFlags - Qt Documentation](https://doc.qt.io/qt-6/qwidget.html#windowFlags-prop) - -#### 设计模式参考 -- [Strategy in C++ / Design Patterns - Refactoring.Guru](https://refactoring.guru/design-patterns/strategy/cpp/example) -- [The Strategy Pattern – MC++ BLOG - Modernes C++](https://www.modernescpp.com/index.php/the-strategy-pattern/) -- [Design Patterns: Elements of Reusable Object-Oriented Software - GoF](https://en.wikipedia.org/wiki/Design_Patterns) - -#### CQRS 原则 -- [CQRS - Martin Fowler](https://martinfowler.com/bliki/CQRS.html) -- [Command-Query Separation - Wikipedia](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation) -- [CQRS Pattern - Microsoft Azure Architecture Center](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs) - -#### 跨平台开发 -- [Best Practices for "Cross-Platform" Development with Qt - Stack Overflow](https://stackoverflow.com/questions/4839350/best-practices-for-cross-platform-development-with-qt) -- [Wayland and Qt - Qt 6.11 文档](https://doc.qt.io/qt-6/wayland-and-qt.html) -- [WSL GUI (WSLg) Documentation - Microsoft Learn](https://learn.microsoft.com/en-us/windows/wsl/tutorials/gui-apps) - ---- +## 当前状态 -**文档版本**: 1.0 -**最后更新**: 2026-03-27 -**作者**: CFDesktop Team -**许可**: MIT License +已实现。接口定义位于 `desktop/ui/platform/IDesktopDisplaySizeStrategy.h` 和 `IDesktopPropertyStrategy.h`。平台实现位于 `desktop/ui/platform/windows/`(Windows)和 `desktop/ui/platform/linux_wsl/`(WSL/X11)。工厂位于 `desktop/ui/platform/DesktopPropertyStrategyFactory.{h,cpp}`。 diff --git a/document/notes/04-Desktop-Behavior-System-Architecture.md b/document/notes/04-Desktop-Behavior-System-Architecture.md index c802dbeea..7747d2e15 100644 --- a/document/notes/04-Desktop-Behavior-System-Architecture.md +++ b/document/notes/04-Desktop-Behavior-System-Architecture.md @@ -1,1733 +1,30 @@ --- title: 桌面行为系统设计:从策略到Window Manager抽象 -description: 桌面行为系统设计:从策略到Window Manager抽象 的详细文档 +description: 桌面行为系统的分层架构、行为流转管线、冲突解决与插件化策略的设计意图 --- -# 桌面行为系统设计:从策略到Window Manager抽象 +# 桌面行为系统设计:从策略到Window Manager抽象 -- 设计意图 -## 目录 +## 为什么选择这种方案 -1. [系统架构概览](#系统架构概览) -2. [分层架构设计](#分层架构设计) -3. [行为流转流程](#行为流转流程) -4. [冲突解决机制](#冲突解决机制) -5. [插件系统集成](#插件系统集成) -6. [未来扩展方向](#未来扩展方向) -7. [最佳实践总结](#最佳实践总结) +桌面行为系统需要管理窗口的全屏、无边框、置顶、透明等多种行为,这些行为在不同平台(Windows/X11/Wayland/Embedded)上的支持情况各异,且某些行为之间天然互斥(如 StayOnTop 与 StayOnBottom)。采用分层架构 + 策略模式的核心原因是:将行为的"意图"与"执行"彻底分离,使得应用层代码只描述"想要什么行为",而由底层各层负责"能不能做、怎么冲突解决、最终怎么映射到平台 API"。 ---- - -## 系统架构概览 - -### 设计目标 - -桌面行为系统的核心设计目标是构建一个**可扩展、跨平台、插件化**的窗口行为管理框架。该框架需要: - -1. **抽象统一**:将不同平台的具体行为抽象为统一的行为模型 -2. **策略可插拔**:支持通过插件方式添加新的行为策略 -3. **冲突可控**:提供明确的冲突检测和解决机制 -4. **平台无关**:应用层代码不依赖于特定平台 API - -### 架构原则 - -```cpp -// 核心架构原则 -namespace desktop::architecture { - -// 1. 依赖倒置:高层模块不依赖低层模块,都依赖抽象 -// 2. 开闭原则:对扩展开放,对修改关闭 -// 3. 单一职责:每个组件只负责一个明确的功能 -// 4. 接口隔离:客户端不应该依赖它不需要的接口 - -} // namespace desktop::architecture -```yaml - ---- - -## 分层架构设计 - -### 整体分层视图 - -```text -┌─────────────────────────────────────────────────────────────────┐ -│ Application Layer │ -│ (用户代码 / 业务逻辑层) │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ Behavior Abstraction │ -│ (行为抽象层 - Domain) │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ DesktopBehaviors (QFlags) │ │ -│ │ - 行为查询接口 (IDesktopBehaviorQuery) │ │ -│ │ - 行为修改接口 (IDesktopBehaviorModifier) │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ Strategy Layer │ -│ (策略层 - Business) │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ Fullscreen │ │ Frameless │ │ StayOnTop │ ... │ -│ │ Strategy │ │ Strategy │ │ Strategy │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Strategy Factory / Registry │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ Qt Integration Layer │ -│ (Qt 集成层 - Implementation) │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Qt WindowFlags / WindowState │ │ -│ │ QWidget / QWindow 行为适配 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ Platform Abstraction │ -│ (平台抽象层 - Infrastructure) │ -│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ -│ │ Windows │ │ X11 │ │ Wayland │ │ Embedded│ ... │ -│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Qt Platform Abstraction (QPA) │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ Window Manager / OS │ -│ (窗口管理器 / 操作系统层) │ -└─────────────────────────────────────────────────────────────────┘ -```text - -### 各层职责详解 - -#### 1. Application Layer(应用层) - -**职责**:业务逻辑和用户交互 - -```cpp -// 应用层代码示例 -namespace desktop::app { - -class MainWindow : public QWidget { -public: - void enableFullscreenMode() { - // 直接使用行为抽象,不关心具体实现 - auto behaviors = m_behaviorQuery->behaviors(); - behaviors |= DesktopBehaviorFlag::Fullscreen; - m_behaviorModifier->setBehaviors(behaviors); - } - -private: - IDesktopBehaviorQuery* m_behaviorQuery; - IDesktopBehaviorModifier* m_behaviorModifier; -}; - -} // namespace desktop::app -```text - -**特点**: -- 完全不依赖 Qt 具体类 -- 不包含平台相关代码 -- 易于测试和模拟 - -#### 2. Behavior Abstraction Layer(行为抽象层) - -**职责**:定义行为模型和操作接口 - -```cpp -// DesktopBehaviorAbstraction.h -#pragma once -#include - -namespace desktop::abstraction { - -// 行为标志定义 -enum class DesktopBehaviorFlag { - None = 0, - Fullscreen = 1 << 0, - Frameless = 1 << 1, - StayOnTop = 1 << 2, - StayOnBottom = 1 << 3, - AllowResize = 1 << 4, - AvoidSystemUI = 1 << 5, - Transparent = 1 << 6, - ClickThrough = 1 << 7, - Modal = 1 << 8, - Tool = 1 << 9, -}; - -Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) -Q_DECLARE_OPERATORS_FOR_FLAGS(DesktopBehaviors) - -// 行为查询接口 -class IDesktopBehaviorQuery { -public: - virtual ~IDesktopBehaviorQuery() = default; - - virtual DesktopBehaviors behaviors() const = 0; - virtual bool hasBehavior(DesktopBehaviorFlag flag) const = 0; - virtual bool hasAnyBehavior(DesktopBehaviors flags) const = 0; - virtual bool hasAllBehaviors(DesktopBehaviors flags) const = 0; -}; - -// 行为修改接口 -class IDesktopBehaviorModifier { -public: - virtual ~IDesktopBehaviorModifier() = default; - - virtual void setBehaviors(DesktopBehaviors flags) = 0; - virtual void addBehaviors(DesktopBehaviors flags) = 0; - virtual void removeBehaviors(DesktopBehaviors flags) = 0; - virtual void clearBehaviors() = 0; -}; - -} // namespace desktop::abstraction -```text - -#### 3. Strategy Layer(策略层) - -**职责**:实现具体的行为策略 - -```cpp -// StrategyLayer.h -#pragma once -#include "DesktopBehaviorAbstraction.h" - -namespace desktop::strategy { - -// 策略接口 -class IDesktopBehaviorStrategy { -public: - virtual ~IDesktopBehaviorStrategy() = default; - - // Query:查询策略能提供的行为 - virtual DesktopBehaviors query() const = 0; - - // Action:应用策略到目标 - virtual bool apply(IDesktopBehaviorModifier* modifier) = 0; - - // 优先级(用于冲突解决) - virtual int priority() const = 0; - - // 策略名称 - virtual QString name() const = 0; -}; - -// 策略工厂 -class IStrategyFactory { -public: - virtual ~IStrategyFactory() = default; - - virtual IDesktopBehaviorStrategy* create(const QString& strategyName) = 0; - virtual QStringList availableStrategies() const = 0; -}; - -// 策略注册表 -class StrategyRegistry { -public: - static StrategyRegistry& instance(); - - void registerFactory(const QString& name, IStrategyFactory* factory); - IDesktopBehaviorStrategy* createStrategy(const QString& name); - -private: - QMap m_factories; -}; - -} // namespace desktop::strategy -```text - -#### 4. Qt Integration Layer(Qt 集成层) - -**职责**:将行为抽象转换为 Qt API 调用 - -```cpp -// QtIntegrationLayer.h -#pragma once -#include "DesktopBehaviorAbstraction.h" -#include - -namespace desktop::qtintegration { - -// Qt 行为修改器实现 -class QtBehaviorModifier : public abstraction::IDesktopBehaviorModifier { -public: - explicit QtBehaviorModifier(QWidget* target) - : m_target(target) {} - - void setBehaviors(DesktopBehaviors flags) override { - if (!m_target) return; - - Qt::WindowFlags windowFlags = m_target->windowFlags(); - - // 清除相关标志 - windowFlags &= ~(Qt::FramelessWindowHint | - Qt::WindowStaysOnTopHint | - Qt::WindowStaysOnBottomHint | - Qt::ToolTip | - Qt::SplashScreen); - - // 应用新标志 - if (flags.testFlag(DesktopBehaviorFlag::Frameless)) - windowFlags |= Qt::FramelessWindowHint; - if (flags.testFlag(DesktopBehaviorFlag::StayOnTop)) - windowFlags |= Qt::WindowStaysOnTopHint; - if (flags.testFlag(DesktopBehaviorFlag::StayOnBottom)) - windowFlags |= Qt::WindowStaysOnBottomHint; - if (flags.testFlag(DesktopBehaviorFlag::Tool)) - windowFlags |= Qt::Tool; - if (flags.testFlag(DesktopBehaviorFlag::Splash)) - windowFlags |= Qt::SplashScreen; - - m_target->setWindowFlags(windowFlags); - - // 处理全屏 - if (flags.testFlag(DesktopBehaviorFlag::Fullscreen)) { - m_target->showFullScreen(); - } else if (m_target->isFullScreen()) { - m_target->showNormal(); - } - - // 处理大小调整 - if (!flags.testFlag(DesktopBehaviorFlag::AllowResize)) { - m_target->setFixedSize(m_target->size()); - } - } - - void addBehaviors(DesktopBehaviors flags) override { - auto current = queryCurrent(); - setBehaviors(current | flags); - } - - void removeBehaviors(DesktopBehaviors flags) override { - auto current = queryCurrent(); - setBehaviors(current & ~flags); - } - - void clearBehaviors() override { - setBehaviors(DesktopBehaviorFlag::None); - } - -private: - DesktopBehaviors queryCurrent() const { - DesktopBehaviors result = DesktopBehaviorFlag::None; - if (m_target->isFullScreen()) - result |= DesktopBehaviorFlag::Fullscreen; - if (m_target->windowFlags() & Qt::FramelessWindowHint) - result |= DesktopBehaviorFlag::Frameless; - if (m_target->windowFlags() & Qt::WindowStaysOnTopHint) - result |= DesktopBehaviorFlag::StayOnTop; - if (m_target->windowFlags() & Qt::WindowStaysOnBottomHint) - result |= DesktopBehaviorFlag::StayOnBottom; - if (m_target->windowFlags() & Qt::Tool) - result |= DesktopBehaviorFlag::Tool; - return result; - } - - QWidget* m_target; -}; - -// Qt 行为查询器实现 -class QtBehaviorQuery : public abstraction::IDesktopBehaviorQuery { -public: - explicit QtBehaviorQuery(QWidget* target) - : m_target(target) {} - - DesktopBehaviors behaviors() const override { - DesktopBehaviors result = DesktopBehaviorFlag::None; - - if (m_target->isFullScreen()) - result |= DesktopBehaviorFlag::Fullscreen; - - Qt::WindowFlags flags = m_target->windowFlags(); - if (flags & Qt::FramelessWindowHint) - result |= DesktopBehaviorFlag::Frameless; - if (flags & Qt::WindowStaysOnTopHint) - result |= DesktopBehaviorFlag::StayOnTop; - if (flags & Qt::WindowStaysOnBottomHint) - result |= DesktopBehaviorFlag::StayOnBottom; - if (flags & Qt::Tool) - result |= DesktopBehaviorFlag::Tool; - if (flags & Qt::SplashScreen) - result |= DesktopBehaviorFlag::Splash; - - // 推断行为 - if (canResize()) - result |= DesktopBehaviorFlag::AllowResize; - - return result; - } - - bool hasBehavior(DesktopBehaviorFlag flag) const override { - return behaviors().testFlag(flag); - } - - bool hasAnyBehavior(DesktopBehaviors flags) const override { - return (behaviors() & flags) != DesktopBehaviorFlag::None; - } - - bool hasAllBehaviors(DesktopBehaviors flags) const override { - return (behaviors() & flags) == flags; - } - -private: - bool canResize() const { - return m_target->minimumSize().isEmpty() && - m_target->maximumSize().isEmpty(); - } - - QWidget* m_target; -}; - -} // namespace desktop::qtintegration -```text - -#### 5. Platform Abstraction Layer(平台抽象层) - -**职责**:处理平台特定的行为差异 - -Qt 本身提供了 **Qt Platform Abstraction (QPA)** 层来处理平台差异。根据 [Qt Platform Abstraction 官方文档](https://doc.qt.io/qt-6/qpa.html): - -> "The Qt Platform Abstraction (QPA) is the main platform abstraction layer in Qt. -> The API can be identified by the QPlatform* class prefix." - -在我们的系统中,可以利用 QPA 来处理平台特定的行为: - -```cpp -// PlatformAbstraction.h -#pragma once -#include - -namespace desktop::platform { - -enum class PlatformType { - Windows, - macOS, - X11, - Wayland, - Embedded, - Unknown -}; - -class PlatformInfo { -public: - static PlatformType currentPlatform() { - QString platformName = QGuiApplication::platformName(); - - if (platformName == "windows") - return PlatformType::Windows; - if (platformName == "cocoa" || platformName == "macos") - return PlatformType::macOS; - if (platformName == "xcb") - return PlatformType::X11; - if (platformName == "wayland") - return PlatformType::Wayland; - if (platformName.startsWith("eglfs") || - platformName == "linuxfb" || - platformName == "offscreen") - return PlatformType::Embedded; - - return PlatformType::Unknown; - } - - static bool supportsAlwaysOnTop(PlatformType platform) { - switch (platform) { - case PlatformType::Windows: - case PlatformType::macOS: - case PlatformType::X11: - return true; - case PlatformType::Wayland: - return false; // Wayland 不支持 - case PlatformType::Embedded: - return false; - default: - return false; - } - } - - static bool supportsFrameless(PlatformType platform) { - // 所有平台都支持无边框 - return true; - } - - static bool supportsClickThrough(PlatformType platform) { - switch (platform) { - case PlatformType::Windows: - return true; - case PlatformType::X11: - return true; - default: - return false; - } - } -}; - -// 平台行为过滤器 -class PlatformBehaviorFilter { -public: - static DesktopBehaviors filter(DesktopBehaviors behaviors) { - PlatformType platform = PlatformInfo::currentPlatform(); - - // 移除不支持的标志 - if (!PlatformInfo::supportsAlwaysOnTop(platform)) { - behaviors &= ~DesktopBehaviorFlag::StayOnTop; - behaviors &= ~DesktopBehaviorFlag::StayOnBottom; - } - - if (!PlatformInfo::supportsClickThrough(platform)) { - behaviors &= ~DesktopBehaviorFlag::ClickThrough; - } - - return behaviors; - } -}; - -} // namespace desktop::platform -```text - -### 层间通信协议 - -```cpp -// LayerCommunication.h -#pragma once -#include "DesktopBehaviorAbstraction.h" -#include "QtIntegrationLayer.h" -#include "PlatformAbstraction.h" - -namespace desktop::communication { - -// 行为请求 -struct BehaviorRequest { - DesktopBehaviors desired; - DesktopBehaviors current; - QString context; // 请求上下文(如 "user_action", "plugin_request") -}; - -// 行为响应 -struct BehaviorResponse { - bool success; - DesktopBehaviors applied; - QStringList warnings; - QStringList errors; -}; - -// 行为协调器(连接各层) -class BehaviorCoordinator { -public: - BehaviorResponse apply(const BehaviorRequest& request) { - BehaviorResponse response; - - // 1. 平台过滤 - DesktopBehaviors filtered = - platform::PlatformBehaviorFilter::filter(request.desired); - - // 2. 冲突解决 - DesktopBehaviors resolved = - m_conflictResolver.resolve(filtered, request.current); - - // 3. 应用到 Qt - response.success = m_qtModifier->setBehaviors(resolved); - response.applied = resolved; - - // 4. 检查警告 - if (filtered != request.desired) { - response.warnings << "Some behaviors were filtered due to platform limitations"; - } - - return response; - } - -private: - qtintegration::QtBehaviorModifier* m_qtModifier; - ConflictResolver m_conflictResolver; -}; - -} // namespace desktop::communication -```yaml - ---- - -## 行为流转流程 - -### 完整流程图 - -```text -┌─────────────────────────────────────────────────────────────────┐ -│ 行为变更请求 │ -│ (User / Plugin / System) │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 1. Strategy 阶段 │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ - 调用相关策略的 query() 获取行为 │ │ -│ │ - 策略可能来自多个来源 (User, Plugin, System) │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 2. Merge 阶段 │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ - 合并来自不同策略的行为请求 │ │ -│ │ - 使用位或运算: final = s1 | s2 | s3 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 3. Resolve 阶段 │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ - 检测行为冲突 │ │ -│ │ - 应用优先级规则 │ │ -│ │ - 执行冲突解决策略 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 4. Filter 阶段 │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ - 平台能力过滤 │ │ -│ │ - 移除当前平台不支持的行为 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 5. Action 阶段 │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ - 调用策略的 apply() 方法 │ │ -│ │ - 将行为转换为 Qt API 调用 │ │ -│ │ - 更新窗口状态 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 6. Validate 阶段 │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ - 验证行为是否正确应用 │ │ -│ │ - 检测实际状态与期望状态的差异 │ │ -│ │ - 记录任何不一致问题 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 行为变更完成 │ -│ (通知观察者 / 触发事件) │ -└─────────────────────────────────────────────────────────────────┘ -```text - -### 流程代码实现 - -```cpp -// BehaviorFlow.h -#pragma once -#include "DesktopBehaviorAbstraction.h" -#include "StrategyLayer.h" -#include "PlatformAbstraction.h" -#include - -namespace desktop::flow { - -// 行为变更请求 -struct BehaviorChangeRequest { - DesktopBehaviors desired; - QString source; // "user", "plugin_xxx", "system" - int priority; // 0-100, 数值越大优先级越高 -}; - -// 行为变更结果 -struct BehaviorChangeResult { - bool success; - DesktopBehaviors final; - DesktopBehaviors applied; - QStringList warnings; - QStringList errors; -}; - -// 行为流程引擎 -class BehaviorFlowEngine { -public: - BehaviorChangeResult apply(const QVector& requests) { - BehaviorChangeResult result; - - // 阶段 1: Strategy - 收集所有策略的请求 - QVector strategyRequests = collectStrategyRequests(requests); - - // 阶段 2: Merge - 合并请求 - DesktopBehaviors merged = mergeRequests(strategyRequests); - - // 获取当前行为 - DesktopBehaviors current = queryCurrentBehaviors(); - - // 阶段 3: Resolve - 解决冲突 - DesktopBehaviors resolved = m_conflictResolver.resolve(merged, current); - - // 阶段 4: Filter - 平台过滤 - DesktopBehaviors filtered = platform::PlatformBehaviorFilter::filter(resolved); - - if (filtered != resolved) { - result.warnings << "Some behaviors were filtered due to platform limitations"; - } - - // 阶段 5: Action - 应用行为 - if (m_modifier) { - m_modifier->setBehaviors(filtered); - } - - // 阶段 6: Validate - 验证结果 - DesktopBehaviors actual = queryCurrentBehaviors(); - result.success = (actual == filtered); - result.final = actual; - result.applied = filtered; - - if (!result.success) { - result.errors << "Applied behaviors do not match expected"; - } - - return result; - } - - void setModifier(IDesktopBehaviorModifier* modifier) { - m_modifier = modifier; - } - - void setQuery(IDesktopBehaviorQuery* query) { - m_query = m_query; - } - -private: - struct StrategyRequest { - DesktopBehaviors behaviors; - QString source; - int priority; - }; - - QVector collectStrategyRequests( - const QVector& requests) { - - QVector strategyRequests; - for (const auto& req : requests) { - strategyRequests.append({ - req.desired, - req.source, - req.priority - }); - } - - // 添加系统策略 - strategyRequests.append({ - m_systemStrategy->query(), - "system", - 50 - }); - - return strategyRequests; - } - - DesktopBehaviors mergeRequests(const QVector& requests) { - DesktopBehaviors result = DesktopBehaviorFlag::None; - - // 按优先级排序 - auto sorted = requests; - std::sort(sorted.begin(), sorted.end(), - [](const StrategyRequest& a, const StrategyRequest& b) { - return a.priority > b.priority; - }); - - // 合并行为(高优先级的覆盖低优先级的) - for (const auto& req : sorted) { - result |= req.behaviors; - } - - return result; - } - - DesktopBehaviors queryCurrentBehaviors() const { - return m_query ? m_query->behaviors() : DesktopBehaviorFlag::None; - } - - IDesktopBehaviorModifier* m_modifier = nullptr; - IDesktopBehaviorQuery* m_query = nullptr; - IDesktopBehaviorStrategy* m_systemStrategy = nullptr; - ConflictResolver m_conflictResolver; -}; - -} // namespace desktop::flow -```text - -### 流程监控与调试 - -```cpp -// FlowMonitoring.h -#pragma once -#include "BehaviorFlow.h" - -namespace desktop::flow { - -// 流程跟踪器 -class FlowTracer { -public: - void traceRequest(const BehaviorChangeRequest& request) { - qDebug() << "[FlowTracer] Request from" << request.source - << ":" << formatBehaviors(request.desired); - } - - void traceMerge(const DesktopBehaviors& merged) { - qDebug() << "[FlowTracer] Merged:" << formatBehaviors(merged); - } - - void traceConflict(const DesktopBehaviors& conflict) { - qDebug() << "[FlowTracer] Conflict detected:" << formatBehaviors(conflict); - } - - void traceResolution(const DesktopBehaviors& resolved, - const DesktopBehaviors& removed) { - qDebug() << "[FlowTracer] Resolved to:" << formatBehaviors(resolved) - << "Removed:" << formatBehaviors(removed); - } - - void traceFilter(const DesktopBehaviors& before, - const DesktopBehaviors& after) { - qDebug() << "[FlowTracer] Filtered:" << formatBehaviors(before) - << "->" << formatBehaviors(after); - } - - void traceResult(const BehaviorChangeResult& result) { - qDebug() << "[FlowTracer] Result: success=" << result.success; - if (!result.warnings.isEmpty()) { - qDebug() << "[FlowTracer] Warnings:" << result.warnings; - } - if (!result.errors.isEmpty()) { - qDebug() << "[FlowTracer] Errors:" << result.errors; - } - } - -private: - QString formatBehaviors(DesktopBehaviors behaviors) { - QStringList flags; - if (behaviors.testFlag(DesktopBehaviorFlag::Fullscreen)) - flags << "Fullscreen"; - if (behaviors.testFlag(DesktopBehaviorFlag::Frameless)) - flags << "Frameless"; - if (behaviors.testFlag(DesktopBehaviorFlag::StayOnTop)) - flags << "StayOnTop"; - if (behaviors.testFlag(DesktopBehaviorFlag::StayOnBottom)) - flags << "StayOnBottom"; - if (behaviors.testFlag(DesktopBehaviorFlag::AllowResize)) - flags << "AllowResize"; - return flags.join("|"); - } -}; - -} // namespace desktop::flow -```yaml - ---- - -## 冲突解决机制 - -### 冲突类型定义 - -```cpp -// ConflictResolution.h -#pragma once -#include "DesktopBehaviorAbstraction.h" -#include - -namespace desktop::conflict { - -// 冲突类型 -enum class ConflictType { - MutuallyExclusive, // 互斥冲突(如 StayOnTop vs StayOnBottom) - Incompatible, // 不兼容冲突(如 Fullscreen vs AllowResize) - PlatformUnsupported, // 平台不支持 - PriorityOverride, // 优先级覆盖 -}; - -// 冲突定义 -struct ConflictDefinition { - DesktopBehaviorFlag flag1; - DesktopBehaviorFlag flag2; - ConflictType type; - QString description; - DesktopBehaviorFlag preferred; // 优先保留的标志 -}; - -// 冲突规则 -class ConflictRules { -public: - static QVector defaultRules() { - return { - // 互斥冲突 - { - DesktopBehaviorFlag::StayOnTop, - DesktopBehaviorFlag::StayOnBottom, - ConflictType::MutuallyExclusive, - "Window cannot be both on top and on bottom", - DesktopBehaviorFlag::StayOnTop // 优先保留 StayOnTop - }, - // 不兼容冲突 - { - DesktopBehaviorFlag::Fullscreen, - DesktopBehaviorFlag::AllowResize, - ConflictType::Incompatible, - "Fullscreen window should not be resizable", - DesktopBehaviorFlag::Fullscreen - }, - { - DesktopBehaviorFlag::Modal, - DesktopBehaviorFlag::Tool, - ConflictType::Incompatible, - "Modal window cannot be a tool window", - DesktopBehaviorFlag::Modal - }, - }; - } -}; - -// 解决策略 -enum class ResolutionStrategy { - Fail, // 失败,不应用任何行为 - Warning, // 警告,自动解决冲突 - AutoResolve, // 自动解决(使用优先级) - Prioritize, // 使用预定义优先级 - RemoveBoth, // 移除冲突的两个标志 -}; - -// 冲突解决器 -class ConflictResolver { -public: - ConflictResolver() - : m_resolutionStrategy(ResolutionStrategy::Prioritize) { - m_rules = ConflictRules::defaultRules(); - } - - void setResolutionStrategy(ResolutionStrategy strategy) { - m_resolutionStrategy = strategy; - } - - DesktopBehaviors resolve(const DesktopBehaviors& desired, - const DesktopBehaviors& current) { - DesktopBehaviors result = desired; - QVector conflicts = detectConflicts(desired); - - if (!conflicts.isEmpty()) { - switch (m_resolutionStrategy) { - case ResolutionStrategy::Fail: - return current; // 不做任何修改 - - case ResolutionStrategy::Warning: - emitWarning(conflicts); - result = autoResolve(result, conflicts); - break; - - case ResolutionStrategy::AutoResolve: - result = autoResolve(result, conflicts); - break; - - case ResolutionStrategy::Prioritize: - result = prioritizeResolve(result, conflicts); - break; - - case ResolutionStrategy::RemoveBoth: - result = removeBothResolve(result, conflicts); - break; - } - } +行为流转采用六阶段管线(Strategy -> Query -> Merge -> Resolve -> Filter -> Action -> Validate),每一阶段职责单一、可独立测试。这种管线式设计让冲突检测和平台过滤成为流程的显式步骤而非事后补救,确保行为变更的结果可预测。 - return result; - } - - QVector detectConflicts(const DesktopBehaviors& behaviors) { - QVector conflicts; - - for (const auto& rule : m_rules) { - if (behaviors.testFlag(rule.flag1) && - behaviors.testFlag(rule.flag2)) { - conflicts.append({ - rule.flag1, - rule.flag2, - rule.type, - rule.description - }); - } - } - - return conflicts; - } - -private: - struct Conflict { - DesktopBehaviorFlag flag1; - DesktopBehaviorFlag flag2; - ConflictType type; - QString description; - }; - - void emitWarning(const QVector& conflicts) { - for (const auto& conflict : conflicts) { - qWarning() << "[ConflictResolver]" << conflict.description; - } - } - - DesktopBehaviors autoResolve(const DesktopBehaviors& behaviors, - const QVector& conflicts) { - DesktopBehaviors result = behaviors; - - for (const auto& conflict : conflicts) { - // 查找对应的规则 - for (const auto& rule : m_rules) { - if (conflict.flag1 == rule.flag1 && - conflict.flag2 == rule.flag2) { - // 移除非优先标志 - result &= ~rule.preferred; - result |= rule.preferred; - break; - } - } - } - - return result; - } - - DesktopBehaviors prioritizeResolve(const DesktopBehaviors& behaviors, - const QVector& conflicts) { - DesktopBehaviors result = behaviors; - - // 使用预定义优先级 - QMap priorities = { - {DesktopBehaviorFlag::Fullscreen, 100}, - {DesktopBehaviorFlag::Modal, 90}, - {DesktopBehaviorFlag::StayOnTop, 80}, - {DesktopBehaviorFlag::Frameless, 70}, - {DesktopBehaviorFlag::StayOnBottom, 60}, - {DesktopBehaviorFlag::Tool, 50}, - {DesktopBehaviorFlag::AllowResize, 40}, - {DesktopBehaviorFlag::AvoidSystemUI, 30}, - {DesktopBehaviorFlag::Transparent, 20}, - {DesktopBehaviorFlag::ClickThrough, 10}, - }; - - for (const auto& conflict : conflicts) { - int priority1 = priorities.value(conflict.flag1, 0); - int priority2 = priorities.value(conflict.flag2, 0); - - if (priority1 > priority2) { - result &= ~conflict.flag2; // 移除低优先级的 - } else { - result &= ~conflict.flag1; - } - } - - return result; - } - - DesktopBehaviors removeBothResolve(const DesktopBehaviors& behaviors, - const QVector& conflicts) { - DesktopBehaviors result = behaviors; - - for (const auto& conflict : conflicts) { - result &= ~conflict.flag1; - result &= ~conflict.flag2; - } - - return result; - } - - ResolutionStrategy m_resolutionStrategy; - QVector m_rules; -}; - -// 冲突解决结果 -struct ResolutionResult { - bool hasConflicts; - DesktopBehaviors resolved; - QStringList warnings; - QVector conflicts; -}; - -} // namespace desktop::conflict -```text - -### 优先级系统 - -```cpp -// PrioritySystem.h -#pragma once -#include "DesktopBehaviorAbstraction.h" - -namespace desktop::priority { - -// 行为优先级 -enum class BehaviorPriority { - Critical = 100, // 关键行为(如 Modal) - High = 80, // 高优先级(如 StayOnTop) - Medium = 50, // 中优先级 - Low = 20, // 低优先级 - Optional = 0 // 可选行为 -}; - -// 优先级管理器 -class PriorityManager { -public: - static BehaviorPriority getPriority(DesktopBehaviorFlag flag) { - static QMap priorities = { - {DesktopBehaviorFlag::Modal, BehaviorPriority::Critical}, - {DesktopBehaviorFlag::Fullscreen, BehaviorPriority::High}, - {DesktopBehaviorFlag::StayOnTop, BehaviorPriority::High}, - {DesktopBehaviorFlag::Frameless, BehaviorPriority::Medium}, - {DesktopBehaviorFlag::StayOnBottom, BehaviorPriority::Medium}, - {DesktopBehaviorFlag::Tool, BehaviorPriority::Medium}, - {DesktopBehaviorFlag::Splash, BehaviorPriority::Medium}, - {DesktopBehaviorFlag::AllowResize, BehaviorPriority::Low}, - {DesktopBehaviorFlag::AvoidSystemUI, BehaviorPriority::Low}, - {DesktopBehaviorFlag::Transparent, BehaviorPriority::Optional}, - {DesktopBehaviorFlag::ClickThrough, BehaviorPriority::Optional}, - }; - return priorities.value(flag, BehaviorPriority::Optional); - } - - // 按优先级排序行为 - static QVector sortByPriority( - const QVector& flags) { - - QVector sorted = flags; - std::sort(sorted.begin(), sorted.end(), - [](DesktopBehaviorFlag a, DesktopBehaviorFlag b) { - return static_cast(getPriority(a)) > - static_cast(getPriority(b)); - }); - return sorted; - } -}; - -} // namespace desktop::priority -```yaml - ---- - -## 插件系统集成 - -### Qt 插件架构概述 - -Qt 提供了强大的插件系统,允许应用程序在运行时动态加载扩展。根据 [How to Create Qt Plugins - Qt Documentation](https://doc.qt.io/qt-6/plugins-howto.html): - -> "Writing a plugin that extends Qt itself is achieved by subclassing the appropriate plugin base class, implementing a few functions, and adding a macro." - -### 插件接口定义 - -```cpp -// DesktopBehaviorPlugin.h -#pragma once -#include -#include "StrategyLayer.h" - -namespace desktop::plugin { - -// 插件接口 -class IDesktopBehaviorPlugin { -public: - virtual ~IDesktopBehaviorPlugin() = default; - - // 插件信息 - virtual QString pluginName() const = 0; - virtual QString pluginVersion() const = 0; - virtual QString pluginDescription() const = 0; - - // 策略提供 - virtual QVector providedStrategies() const = 0; - virtual IDesktopBehaviorStrategy* createStrategy( - const QString& strategyName) = 0; - - // 初始化/清理 - virtual bool initialize() = 0; - virtual void cleanup() = 0; -}; - -// Qt 插件接口(用于 QPluginLoader) -#define DesktopBehaviorPlugin_iid "com.desktop.behavior.plugin/1.0" - -} // namespace desktop::plugin - -Q_DECLARE_INTERFACE(desktop::plugin::IDesktopBehaviorPlugin, - desktop::plugin::DesktopBehaviorPlugin_iid) -```text - -### 插件管理器 - -```cpp -// PluginManager.h -#pragma once -#include "DesktopBehaviorPlugin.h" -#include -#include - -namespace desktop::plugin { - -// 插件信息 -struct PluginInfo { - QString name; - QString version; - QString description; - QString filePath; - IDesktopBehaviorPlugin* plugin; - bool isLoaded; -}; - -// 插件管理器 -class PluginManager : public QObject { - Q_OBJECT - -public: - static PluginManager& instance(); - - // 加载插件 - bool loadPlugin(const QString& pluginPath); - - // 加载插件目录 - int loadPluginsFromDirectory(const QString& directory); - - // 卸载插件 - bool unloadPlugin(const QString& pluginName); - - // 获取所有插件 - QVector loadedPlugins() const; - - // 创建策略 - IDesktopBehaviorStrategy* createStrategy( - const QString& pluginName, - const QString& strategyName); - - // 获取所有可用策略 - QVector availableStrategies() const; - -signals: - void pluginLoaded(const QString& pluginName); - void pluginUnloaded(const QString& pluginName); - void pluginLoadFailed(const QString& pluginName, const QString& error); - -private: - PluginManager() = default; - - QMap m_plugins; -}; - -// 单例实现 -PluginManager& PluginManager::instance() { - static PluginManager manager; - return manager; -} - -} // namespace desktop::plugin -```text - -### 插件实现示例 - -```cpp -// ExamplePlugin.h -#pragma once -#include "DesktopBehaviorPlugin.h" - -namespace desktop::plugin::example { - -// 示例策略:全屏策略 -class ExampleFullscreenStrategy : public strategy::IDesktopBehaviorStrategy { -public: - DesktopBehaviors query() const override { - return DesktopBehaviorFlag::Fullscreen; - } - - bool apply(IDesktopBehaviorModifier* modifier) override { - if (!modifier) return false; - - DesktopBehaviors behaviors = modifier->behaviors(); - behaviors |= DesktopBehaviorFlag::Fullscreen; - behaviors &= ~DesktopBehaviorFlag::AllowResize; // 全屏时不允许调整大小 - - modifier->setBehaviors(behaviors); - return true; - } - - int priority() const override { - return 90; - } - - QString name() const override { - return "ExampleFullscreen"; - } -}; - -// 示例插件 -class ExamplePlugin : public QObject, - public IDesktopBehaviorPlugin { - Q_OBJECT - Q_PLUGIN_METADATA(IID DesktopBehaviorPlugin_iid FILE "example.json") - Q_INTERFACES(desktop::plugin::IDesktopBehaviorPlugin) - -public: - QString pluginName() const override { - return "ExamplePlugin"; - } - - QString pluginVersion() const override { - return "1.0.0"; - } - - QString pluginDescription() const override { - return "Example behavior plugin with fullscreen strategy"; - } - - QVector providedStrategies() const override { - return {"ExampleFullscreen"}; - } - - IDesktopBehaviorStrategy* createStrategy( - const QString& strategyName) override { - - if (strategyName == "ExampleFullscreen") { - return new ExampleFullscreenStrategy(); - } - return nullptr; - } - - bool initialize() override { - qDebug() << "ExamplePlugin initialized"; - return true; - } - - void cleanup() override { - qDebug() << "ExamplePlugin cleaned up"; - } -}; - -} // namespace desktop::plugin::example -```text - -### 插件元数据文件 - -```json -// example.json -{ - "Keys": ["ExampleFullscreen"], - "ClassName": "desktop::plugin::example::ExamplePlugin", - "IID": "com.desktop.behavior.plugin/1.0", - "MetaData": { - "Author": "Desktop Team", - "Date": "2025-01-01", - "Description": "Example behavior plugin", - "Version": "1.0.0", - "MinQtVersion": "6.5.0", - "RequiredFeatures": [] - } -} -```text - -### 动态加载策略 - -根据 [QPluginLoader Class | Qt Core | Qt 6.10.2](https://doc.qt.io/qt-6/qpluginloader.html): - -```cpp -// DynamicStrategyLoader.h -#pragma once -#include "PluginManager.h" - -namespace desktop::plugin { - -// 动态策略加载器 -class DynamicStrategyLoader { -public: - // 从插件加载策略 - static IDesktopBehaviorStrategy* loadStrategy( - const QString& pluginName, - const QString& strategyName) { - - PluginManager& manager = PluginManager::instance(); - return manager.createStrategy(pluginName, strategyName); - } - - // 按能力查找策略 - static QVector findStrategiesByCapability( - DesktopBehaviorFlag capability) { - - QVector result; - PluginManager& manager = PluginManager::instance(); - - for (const auto& pluginInfo : manager.loadedPlugins()) { - if (!pluginInfo.isLoaded) continue; - - for (const auto& strategyName : pluginInfo.plugin->providedStrategies()) { - auto* strategy = pluginInfo.plugin->createStrategy(strategyName); - if (strategy && strategy->query().testFlag(capability)) { - result.append(strategy); - } - } - } - - return result; - } -}; - -} // namespace desktop::plugin -```yaml - ---- - -## 未来扩展方向 - -### 新增行为特性 - -```cpp -// FutureBehaviors.h -#pragma once -#include "DesktopBehaviorAbstraction.h" - -namespace desktop::future { - -// 未来可能的行为标志 -enum class FutureBehaviorFlag { - // 现有行为... - None = 0, - Fullscreen = 1 << 0, - Frameless = 1 << 1, - StayOnTop = 1 << 2, - StayOnBottom = 1 << 3, - AllowResize = 1 << 4, - AvoidSystemUI = 1 << 5, - Transparent = 1 << 6, - ClickThrough = 1 << 7, - - // 新增行为 - AlwaysOnTop = 1 << 10, ///< 始终置顶(与 StayOnTop 类似但语义不同) - AlwaysOnBottom = 1 << 11, ///< 始终置底 - VirtualDesktop = 1 << 12, ///< 虚拟桌面支持 - SkipTaskbar = 1 << 13, ///< 不显示在任务栏 - SkipPager = 1 << 14, ///< 不显示在页面切换器 - SkipSwitcher = 1 << 15, ///< 不显示在窗口切换器 - Above = 1 << 16, ///< 位于特定窗口之上 - Below = 1 << 17, ///< 位于特定窗口之下 - DemandsAttention = 1 << 18, ///< 需要注意(闪烁等) - Focused = 1 << 19, ///< 获取焦点 - AcceptFocus = 1 << 20, ///< 接受焦点 - FocusOnButtonClick = 1 << 21, ///< 点击时获取焦点 - NoFocus = 1 << 22, ///< 不获取焦点 - GroupLeader = 1 << 23, ///< 组领导者 - Independent = 1 << 24, ///< 独立窗口 - BypassWindowManager = 1 << 25, ///< 绕过窗口管理器 - Maximized = 1 << 26, ///< 最大化 - Minimized = 1 << 27, ///< 最小化 - Active = 1 << 28, ///< 活动窗口 - Dock = 1 << 29, ///< 停靠窗口 - Desktop = 1 << 30, ///< 桌面窗口类型 -}; - -Q_DECLARE_FLAGS(FutureBehaviors, FutureBehaviorFlag) -Q_DECLARE_OPERATORS_FOR_FLAGS(FutureBehaviors) - -// 扩展的行为接口 -class IFutureDesktopBehaviorQuery { -public: - virtual ~IFutureDesktopBehaviorQuery() = default; - - virtual FutureBehaviors futureBehaviors() const = 0; - virtual bool hasFutureBehavior(FutureBehaviorFlag flag) const = 0; - - // 虚拟桌面相关 - virtual int currentVirtualDesktop() const = 0; - virtual void setVirtualDesktop(int desktop) = 0; - virtual QVector availableVirtualDesktops() const = 0; - - // 窗口层级相关 - virtual void setWindowLevel(WindowLevel level) = 0; - virtual WindowLevel windowLevel() const = 0; - - // 注意力相关 - virtual void setDemandsAttention(bool demand) = 0; - virtual bool demandsAttention() const = 0; -}; - -// 窗口层级定义 -enum class WindowLevel { - Normal, - Above, - Below, - TopMost, - BottomMost -}; - -} // namespace desktop::future -```text - -### 平台特定扩展 - -```cpp -// PlatformExtensions.h -#pragma once -#include "DesktopBehaviorAbstraction.h" - -namespace desktop::platform { - -// Windows 特定扩展 -#ifdef Q_OS_WIN -struct WindowsSpecificBehaviors { - bool appWindow = false; // 应用程序窗口 - bool toolWindow = false; // 工具窗口 - bool noActivate = false; // 不激活 - bool topMost = false; // 最顶层 -}; -#endif - -// macOS 特定扩展 -#ifdef Q_OS_MACOS -struct MacOSSpecificBehaviors { - bool floatingPanel = false; // 浮动面板 - bool hudWindow = false; // HUD 窗口 - bool utilityWindow = false; // 实用工具窗口 - bool docModal = false; // 文档模态 -}; -#endif - -// Linux 特定扩展 -#ifdef Q_OS_LINUX -struct LinuxSpecificBehaviors { - bool dockWindow = false; // 停靠窗口 - bool desktopWidget = false; // 桌面部件 - bool notification = false; // 通知窗口 - bool comboBoxPopup = false; // 组合框弹出 - bool dndPopup = false; // 拖放弹出 - bool tooltip = false; // 工具提示 -}; - -// X11 特定 -struct X11SpecificBehaviors { - bool bypassWindowManager = false; // 绕过窗口管理器 - bool x11BypassWM = false; // X11 绕过 WM - uint32_t x11WindowType = 0; // X11 窗口类型 -}; - -// Wayland 特定 -struct WaylandSpecificBehaviors { - bool layerShell = false; // 层级外壳 - bool xdgShell = false; // XDG 外壳 - bool subsurface = false; // 子表面 -}; -#endif - -} // namespace desktop::platform -```yaml - ---- - -## 最佳实践总结 - -### 架构设计原则 - -1. **单一职责原则(SRP)** - - 每个类只负责一个明确的功能 - - Strategy 只负责单一行为的实现 - - Coordinator 只负责流程协调 - -2. **开闭原则(OCP)** - - 对扩展开放:通过插件添加新策略 - - 对修改关闭:核心代码不需要修改 - -3. **依赖倒置原则(DIP)** - - 高层模块不依赖低层模块 - - 都依赖于抽象接口 - -4. **接口隔离原则(ISP)** - - 接口细粒度,客户端不依赖不需要的方法 - - Query 和 Action 分离 - -### 性能优化建议 - -```cpp -// PerformanceOptimizations.h -#pragma once -#include "BehaviorFlow.h" - -namespace desktop::performance { - -// 行为缓存 -class BehaviorCache { -public: - void cache(const QString& key, const DesktopBehaviors& behaviors) { - m_cache[key] = behaviors; - } - - DesktopBehaviors get(const QString& key) const { - return m_cache.value(key, DesktopBehaviorFlag::None); - } - - bool contains(const QString& key) const { - return m_cache.contains(key); - } - - void invalidate(const QString& key) { - m_cache.remove(key); - } - - void clear() { - m_cache.clear(); - } - -private: - QMap m_cache; -}; - -// 批量操作优化 -class BatchBehaviorProcessor { -public: - void addRequest(const BehaviorChangeRequest& request) { - m_requests.append(request); - } - - QVector processAll() { - QVector results; - - // 批量合并请求 - DesktopBehaviors merged = mergeBatchRequests(m_requests); - - // 一次性应用 - BehaviorChangeRequest combined; - combined.desired = merged; - combined.source = "batch"; - combined.priority = 0; - - BehaviorChangeResult result = m_engine.apply({combined}); - results.append(result); - - m_requests.clear(); - return results; - } - -private: - QVector m_requests; - BehaviorFlowEngine m_engine; - - DesktopBehaviors mergeBatchRequests( - const QVector& requests) { - - DesktopBehaviors result = DesktopBehaviorFlag::None; - for (const auto& req : requests) { - result |= req.desired; - } - return result; - } -}; - -} // namespace desktop::performance -```text - -### 测试策略 - -```cpp -// TestingSupport.h -#pragma once -#include "DesktopBehaviorAbstraction.h" - -namespace desktop::testing { - -// Mock 查询器 -class MockBehaviorQuery : public IDesktopBehaviorQuery { -public: - void setBehaviors(DesktopBehaviors behaviors) { - m_behaviors = behaviors; - } - - DesktopBehaviors behaviors() const override { - return m_behaviors; - } - - bool hasBehavior(DesktopBehaviorFlag flag) const override { - return m_behaviors.testFlag(flag); - } - - bool hasAnyBehavior(DesktopBehaviors flags) const override { - return (m_behaviors & flags) != DesktopBehaviorFlag::None; - } - - bool hasAllBehaviors(DesktopBehaviors flags) const override { - return (m_behaviors & flags) == flags; - } - -private: - DesktopBehaviors m_behaviors = DesktopBehaviorFlag::None; -}; - -// Mock 修改器 -class MockBehaviorModifier : public IDesktopBehaviorModifier { -public: - DesktopBehaviors lastSetBehaviors() const { - return m_lastBehaviors; - } - - int setBehaviorsCallCount() const { - return m_callCount; - } - - void setBehaviors(DesktopBehaviors flags) override { - m_lastBehaviors = flags; - m_callCount++; - } - - void addBehaviors(DesktopBehaviors flags) override { - m_lastBehaviors |= flags; - m_callCount++; - } - - void removeBehaviors(DesktopBehaviors flags) override { - m_lastBehaviors &= ~flags; - m_callCount++; - } - - void clearBehaviors() override { - m_lastBehaviors = DesktopBehaviorFlag::None; - m_callCount++; - } - -private: - DesktopBehaviors m_lastBehaviors = DesktopBehaviorFlag::None; - int m_callCount = 0; -}; - -} // namespace desktop::testing -```yaml - ---- - -## 参考资源 - -### Qt 官方文档 - -- [Qt Platform Abstraction | Platform Integration | Qt 6.11.0](https://doc.qt.io/qt-6/qpa.html) -- [QPluginLoader Class | Qt Core | Qt 6.10.2](https://doc.qt.io/qt-6/qpluginloader.html) -- [How to Create Qt Plugins - Qt Documentation](https://doc.qt.io/qt-6/plugins-howto.html) -- [QWidget Class | Qt Widgets | Qt 6.11.0](https://doc.qt.io/qt-6/qwidget.html) -- [Wayland and Qt | Qt 6.11](https://doc.qt.io/qt-6/wayland-and-qt.html) - -### 架构设计参考 - -- [Layered Architecture and Abstraction Layers | by Patryk Rogala](https://medium.com/@patrykrogedu/layered-architecture-and-abstraction-layers-167438dd1a8b) -- [Must-Know Software Architecture Patterns - ByteByteGo Newsletter](https://blog.bytebytego.com/p/must-know-software-architecture-patterns) -- [14 software architecture design patterns to know - Red Hat](https://www.redhat.com/en/blog/14-software-architecture-patterns) -- [Abstraction layer - Wikipedia](https://en.wikipedia.org/wiki/wiki/Abstraction_layer) - -### 插件开发参考 - -- [Adapting Embedded Devices with Qt 6 Plugins | ICS](https://www.ics.com/blog/adapting-embedded-devices-qt-6-plugins-coffee-machine-case-study) -- [Loading and initialising a Qt plug-in dynamically - Qt Wiki](https://qt.shoutwiki.com/wiki/Loading_and_initialising_a_Qt_plug-in_dynamically) - ---- +插件系统基于 Qt QPluginLoader,策略以动态库形式加载。`IDesktopBehaviorStrategy` 接口让每个插件只暴露 `query()` / `apply()` / `priority()` 三个方法,通过 `StrategyRegistry` 统一注册,避免了插件与核心框架的紧耦合。 -## 总结 +## 关键决策 -本文档详细介绍了桌面行为系统的完整架构设计,从分层架构到行为流转流程,从冲突解决机制到插件系统集成。这套架构的核心价值在于: +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| 五层架构(Application / Behavior Abstraction / Strategy / Qt Integration / Platform Abstraction) | 每层职责明确,依赖方向单一,便于跨平台扩展 | 三层架构(将 Qt 集成与平台抽象合并):平台差异处理会侵入策略层 | +| 行为以 `QFlags` 表示 | 位运算天然支持行为的合并与冲突检测,性能优于 set/map | `std::unordered_set`:每次查询/合并需要遍历,且无法用位运算做互斥检测 | +| 六阶段管线式流转 | 冲突解决和平台过滤成为显式步骤,结果可预测 | 回调链/观察者模式:行为变更的副作用隐式传播,难以追踪和调试 | +| 冲突解决默认使用 `Prioritize` 策略(预定义优先级表) | 自动解决冲突且行为可预测,优先级表可配置 | `Fail` 策略(遇到冲突直接失败):用户体验差;`RemoveBoth`:丢失合法行为 | +| Query/Modifier 接口分离(ISP) | 只读操作和写操作解耦,便于权限控制和 mock 测试 | 统一 `IDesktopBehavior` 接口:客户端暴露了不需要的修改能力 | +| 插件通过 `QPluginLoader` + `Q_PLUGIN_METADATA` 动态加载 | 支持运行时加载/卸载策略,符合开闭原则 | 静态注册策略:每次新增策略需要重新编译核心框架 | +| 平台能力通过 `PlatformBehaviorFilter` 在管线中显式过滤 | 行为请求先被平台能力裁剪再应用,避免调用不支持的平台 API | 各策略内部判断平台:策略与平台耦合,违反单一职责 | -1. **可扩展性**:通过插件系统轻松添加新的行为策略 -2. **跨平台性**:通过平台抽象层处理不同平台的差异 -3. **可维护性**:清晰的分层架构使代码易于理解和维护 -4. **可测试性**:依赖抽象接口便于编写单元测试 +## 当前状态 -希望这套架构能够为您的桌面应用开发提供有力支持! +架构设计已完成,部分接口已实现。核心接口定义位于 `desktop/` 层,策略注册表与冲突解决器为后续实现重点。 diff --git a/document/notes/05-Logger-Singleton-Link-Architecture.md b/document/notes/05-Logger-Singleton-Link-Architecture.md index 4bb6c2ee0..3ad53f874 100644 --- a/document/notes/05-Logger-Singleton-Link-Architecture.md +++ b/document/notes/05-Logger-Singleton-Link-Architecture.md @@ -1,355 +1,28 @@ --- title: Logger 单实例链接架构 -description: 本文档记录 CFDesktop 项目中 (日志系统)的单实例保证方案,包括 CMake 链接策略、 +description: 通过 INTERFACE 头文件库和 --whole-archive 保证 cflogger 单例在 DLL 边界内唯一 --- -# Logger 单实例链接架构 +# Logger 单实例链接架构 -- 设计意图 -本文档记录 CFDesktop 项目中 `cflogger`(日志系统)的单实例保证方案,包括 CMake 链接策略、`INTERFACE` 头文件库的引入、以及 DLL 导出/导入机制的设计决策。 +## 为什么选择这种方案 ---- - -## 1. 问题背景 - -### 1.1 项目架构 - -CFDesktop 采用"多静态库 → 单一共享库"架构: - -```text -多个 STATIC library (cflogger, cfbase, CFDesktopMain, CFDesktopUi, ...) - ↓ --whole-archive -CFDesktop_shared (SHARED / DLL) - ↓ -CFDesktop.exe -```bash - -所有静态库通过 `--whole-archive` 合并进一个 DLL,EXE 只链接这个 DLL。 - -### 1.2 Logger 单例问题 - -`cflogger` 内部使用单例模式(`Logger::instance()`)。如果同一份 `.cpp` 被编译进多个模块,会产生**多个独立的单例实例**,导致: - -- 日志输出丢失(模块 A 初始化的 Sink,模块 B 看不到) -- 全局状态不一致(日志级别、格式器各自独立) -- 静态初始化顺序问题(Static Initialization Order Fiasco) - -### 1.3 原始问题:cflogger 被到处 PUBLIC 链接 - -修改前,`cflogger` 被多个模块通过 `PUBLIC` 关键字链接: - -| 目标 | 链接方式 | 问题 | -|------|---------|------| -| `CFDesktopMain` | PRIVATE | 不传播,OK | -| `CFDesktopLog` | **PUBLIC** | 向所有消费者传播 | -| `CFDesktopEarlySession` | **PUBLIC** | 向所有消费者传播 | -| `CFDesktopPathSettings` | **PUBLIC** | 向所有消费者传播 | -| `cf_desktop_ui_platform` | **PUBLIC** | 向所有消费者传播 | -| `cf_desktop_ui_widget_init_session` | **PUBLIC** | 向所有消费者传播 | - -`PUBLIC` 链接会在编译期将 `cflogger` 的 include 目录和链接需求传播给所有下游消费者,造成不必要的耦合。虽然在当前 `--whole-archive` 方案下不会产生多实例(因为所有静态库最终打包进同一个 DLL),但这种传播增加了维护复杂度,且容易在架构调整时引入问题。 - ---- - -## 2. 解决方案:INTERFACE 头文件库 - -### 2.1 核心思想 - -引入一个 `cflogger_headers`(INTERFACE library),让 DLL 内部的模块**只拿头文件**,不链接静态库实体。唯一实际链接 `cflogger` 静态库的地方是 `CFDesktop_shared`(通过 `--whole-archive`)和 `CFDesktopMain`(PRIVATE,因为直接使用了内部类)。 - -### 2.2 实现代码 - -在 `desktop/base/logger/CMakeLists.txt` 中新增: - -```cmake -# Header-only interface for internal modules that only need cflog.h -# Modules inside CFDesktop_shared should use this instead of linking cflogger -# to avoid duplicating the static lib symbols — only CFDesktop_shared links the real library. -add_library(cflogger_headers INTERFACE) -target_include_directories(cflogger_headers INTERFACE - $ -) -target_link_libraries(cflogger_headers INTERFACE cfbase) -```cmake - -### 2.3 为什么需要 `cflogger_headers` 而不是直接用 `cflogger` - -| 方式 | 效果 | -|------|------| -| `target_link_libraries(X PUBLIC cflogger)` | X 的消费者也会链接 `libcflogger.a`(传播) | -| `target_link_libraries(X PRIVATE cflogger)` | 只有 X 链接 `libcflogger.a`(不传播),但 X 的 `.a` 里会包含对 cflogger 符号的引用 | -| `target_link_libraries(X PUBLIC cflogger_headers)` | X 及其消费者只获得 `cflog.h` 头文件路径,不链接任何 `.a` | - -`INTERFACE` 库本身没有编译产物(没有 `.a` / `.o`),它只是一个**编译期元数据载体**,传递 include 目录和编译定义。 - ---- - -## 3. 最终链接拓扑 - -### 3.1 完整依赖图 - -```text -cflogger (STATIC) ──────────────────────────────┐ - ├─ cflog.cpp, cflog_impl.cpp │ - ├─ console_sink.cpp, file_sink.cpp │ - ├─ console_formatter.cpp, file_formatter.cpp │ - └─ async_queue.cpp │ - │ --whole-archive -cflogger_headers (INTERFACE) ├────────────────→ CFDesktop_shared (DLL) - ├─ include dirs only │ - └─ links: cfbase │ - ↑ │ - │ use (headers only) │ - ├─ CFDesktopLog │ - ├─ CFDesktopEarlySession │ - ├─ CFDesktopPathSettings │ - ├─ cf_desktop_ui_platform │ - └─ cf_desktop_ui_widget_init_session │ - │ -CFDesktopMain (STATIC) ──PRIVATE──→ cflogger ───┘ - └─ 直接使用了 FileSink, ConsoleSink 等内部类 - 这些类没有 CFLOG_API 标记,不能通过 DLL 导入 -```bash - -### 3.2 各模块的角色 - -| 模块 | 如何使用 cflogger | 原因 | -|------|------------------|------| -| `CFDesktop_shared` | `--whole-archive` 链接 `cflogger` | 唯一的实际链接点,保证 DLL 内单实例 | -| `CFDesktopMain` | PRIVATE 链接 `cflogger` | `logger_stage.cpp` 直接实例化 `FileSink`/`ConsoleSink`,这些内部类没有 `CFLOG_API`,必须从静态库解析 | -| `CFDesktopLog` | PUBLIC 链接 `cflogger_headers` | 只调用 `CFLOG_INFO()` 等公开 API | -| `CFDesktopEarlySession` | PUBLIC 链接 `cflogger_headers` | 只调用公开 API | -| `CFDesktopPathSettings` | PUBLIC 链接 `cflogger_headers` | 只调用公开 API | -| `cf_desktop_ui_platform` | PUBLIC 链接 `cflogger_headers` | 只调用公开 API | -| `cf_desktop_ui_widget_init_session` | PUBLIC 链接 `cflogger_headers` | 只调用公开 API | -| Test / Example | `CFLOG_STATIC_BUILD` + 链接 `CFDesktop::logger` | 独立编译,不走 DLL 路径 | - ---- - -## 4. DLL 导出/导入机制 - -### 4.1 `CFLOG_API` 宏定义 - -定义在 `desktop/base/logger/include/cflog/cflog_export.h`: - -```cpp -#if defined(_WIN32) || defined(_MSC_VER) - #ifdef CFLOG_STATIC_BUILD - #define CFLOG_API // 测试/示例:无修饰 - #elif defined(CFLOG_BUILDING) - #define CFLOG_API __declspec(dllexport) // 构建 cflogger 时:导出 - #else - #define CFLOG_API __declspec(dllimport) // 消费者:导入 - #endif -#else - #define CFLOG_API __attribute__((visibility("default"))) // Linux:可见 -#endif -```bash - -### 4.2 标记规则 - -| 标记了 `CFLOG_API` 的 | 用途 | -|----------------------|------| -| `Logger` 类 (`cflog.hpp`) | 核心单例,EXE 通过 DLL 导入使用 | -| `trace()`, `debug()`, `info()`, `warning()`, `error()` 全局函数 | 公开日志 API | -| `set_level()`, `flush()` | 运行时配置 API | - -| 没有标记 `CFLOG_API` 的 | 原因 | -|------------------------|------| -| `ConsoleSink`, `FileSink` | 内部实现类,仅供 `CFDesktopMain` 的 `logger_stage.cpp` 使用 | -| `ConsoleFormatter`, `FileFormatter`, `AsciiColorFormatter` | 内部实现类 | -| `AsyncQueue` | 内部实现类 | - -### 4.3 为什么 `CFDesktopMain` 不能用 `cflogger_headers` - -如果 `CFDesktopMain` 改用 `cflogger_headers`,链接 `CFDesktop.exe` 时会报 `undefined symbol`: - -```text -ld.lld: error: undefined symbol: cf::log::FileSink::FileSink(...) -ld.lld: error: undefined symbol: vtable for cf::log::ConsoleSink -```yaml - -因为 `FileSink`、`ConsoleSink` 没有被 `CFLOG_API` 标记导出,EXE 无法通过 DLL 导入表找到它们。`CFDesktopMain` 必须 PRIVATE 链接 `cflogger` 静态库,让链接器直接从 `.a` 中解析这些符号。 - ---- - -## 5. LNK4217 警告修复:`CFLOG_STATIC_BUILD` 与 DLL 内部解析 - -### 5.1 问题现象 - -编译 `CFDesktop_shared.dll` 时,lld 链接器产生大量 LNK4217 警告: - -```text -ld.lld: warning: libCFDesktopMain.a(init_chain.cpp.obj): - locally defined symbol imported: - cf::log::Logger::instance() - (defined in libcflogger.a(cflog.cpp.obj)) [LNK4217] -```text - -涉及 `.a` 文件:`libCFDesktopMain.a`、`libCFDesktopUi.a`、`libcf_desktop_ui_platform.a`。 - -### 5.2 根因分析 +CFDesktop 采用"多静态库合并为单一共享库"架构:所有静态库通过 `--whole-archive` 打包进 `CFDesktop_shared.dll`,EXE 只链接这个 DLL。`cflogger` 使用单例模式,如果同一份 `.cpp` 被编译进多个模块,会产生多个独立实例,导致日志输出丢失和全局状态不一致。 -LLD 警告的含义是:**某 `.obj` 文件通过 `__declspec(dllimport)` 引用了一个符号,但该符号在同一 DLL 内有本地定义。** +核心问题在于:DLL 内部的模块需要调用日志 API(如 `CFLOG_INFO()`),但不应各自链接 `cflogger` 静态库实体。解决方案是引入 `cflogger_headers` 这个 CMake `INTERFACE` 库——它没有编译产物,仅传递 include 目录和编译定义。DLL 内部模块链接 `cflogger_headers` 只拿头文件,唯一实际链接 `cflogger` 静态库的地方是 `CFDesktop_shared`(通过 `--whole-archive`)。这保证了 `Logger` 实现只编译一次,单例在整个进程中唯一。 -具体链路: +`--whole-archive` 在此架构中不可省略:它强制链接器保留 `cflogger` 的所有符号(包括未被直接引用的全局对象构造函数),否则链接器会丢弃"未使用"的 Logger 实现。`CFLOG_STATIC_BUILD` 宏作为补充手段,消除 DLL 内部 `.obj` 文件通过 `__declspec(dllimport)` 引用本地符号时产生的 LNK4217 警告。 -```text -cflog_export.h 中的宏展开逻辑: +## 关键决策 - CFLOG_BUILDING → __declspec(dllexport) ← 仅 cflogger 自身编译时 - CFLOG_STATIC_BUILD → (空) ← 测试/示例用 - 默认 → __declspec(dllimport) ← 其他所有消费者 -```text +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| `cflogger_headers` INTERFACE 库隔离头文件与链接实体 | `.cpp` 只编译一次,`.h` 可以到处传,零运行时开销 | OBJECT library:需要所有使用方从同一 target 获取 `.o`,管理复杂度高 | +| `--whole-archive` 打包所有静态库进单一 DLL | 保证单例唯一且所有符号可被 EXE 通过 DLL 导入表访问 | 去掉 DLL 全静态链接:改动量大,不符合项目共享库架构 | +| `CFLOG_STATIC_BUILD` PRIVATE 编译定义 | DLL 内模块用本地引用,EXE 用 `dllimport`,各取所需 | PUBLIC 编译定义:会传播给 EXE 导致链接失败 | +| `CFLOG_API` 标记公开类,内部类不标记 | `Logger`/全局日志函数通过 DLL 导出,`FileSink`/`ConsoleSink` 等内部类仅供 `CFDesktopMain` 直接从静态库解析 | 所有类都标记导出:暴露实现细节,增加 ABI 维护负担 | +| `CFDesktopMain` PRIVATE 链接 `cflogger`(非 headers) | 直接使用 `FileSink`/`ConsoleSink` 等未导出的内部类,必须从静态库解析符号 | 改用 headers:链接时 undefined symbol | -DLL 内的静态库(如 `CFDesktopMain`)编译时: -- 没有定义 `CFLOG_BUILDING`(那是 cflogger 自己的) -- 没有定义 `CFLOG_STATIC_BUILD`(之前没加) -- 所以 `CFLOG_API` = `__declspec(dllimport)` - -但链接时,这些 `.obj` 文件和 `libcflogger.a` 被 `--whole-archive` 打进**同一个 DLL**。链接器发现:这些 `dllimport` 引用的符号其实就在本地。LLD 对此发出警告。 - -> **这不是多实例问题**——链接器确实使用了本地定义,运行时行为正确。 -> 但警告噪音大,且暗示链接意图不清晰。 - -### 5.3 修复方案 - -给所有会打入 `CFDesktop_shared` 的静态库目标添加 `CFLOG_STATIC_BUILD` **PRIVATE** 编译定义: - -```cmake -target_compile_definitions(CFDesktopMain PRIVATE CFLOG_STATIC_BUILD) -target_compile_definitions(CFDesktopUi PRIVATE CFLOG_STATIC_BUILD) -target_compile_definitions(cf_desktop_ui_platform PRIVATE CFLOG_STATIC_BUILD) -target_compile_definitions(CFDesktopLog PRIVATE CFLOG_STATIC_BUILD) -target_compile_definitions(cf_desktop_ui_widget_init_session PRIVATE CFLOG_STATIC_BUILD) -```bash - -关键:**必须是 PRIVATE**,不能是 PUBLIC 或 INTERFACE。 - -- `PRIVATE` → 只作用于该目标的 `.cpp` 文件,不传播给消费者 -- 如果用 PUBLIC → EXE 也会得到 `CFLOG_STATIC_BUILD`,导致 `CFLOG_API` = 空,EXE 不再 `dllimport`,链接失败 - -### 5.4 修复后的宏展开 - -| 编译目标 | 宏定义 | `CFLOG_API` 展开 | 效果 | -|---------|--------|-----------------|------| -| `cflogger`(静态库) | `CFLOG_BUILDING` | `__declspec(dllexport)` | 符号从 DLL 导出 | -| `CFDesktopMain`(静态库) | `CFLOG_STATIC_BUILD` | *(空)* | 本地引用,无 dllimport | -| `CFDesktopUi`(静态库) | `CFLOG_STATIC_BUILD` | *(空)* | 本地引用,无 dllimport | -| `cf_desktop_ui_platform`(静态库) | `CFLOG_STATIC_BUILD` | *(空)* | 本地引用,无 dllimport | -| `CFDesktop.exe` | *(无)* | `__declspec(dllimport)` | 从 DLL 导入,正确 | - -### 5.5 判断哪些目标需要添加 - -原则:**所有 `.obj` 文件会通过 `--whole-archive` 进入 DLL、且 `#include` 了 `cflog.h` 的静态库目标。** - -可通过编译输出中的 LNK4217 警告定位(警告里提到的 `.a` 文件对应的目标)。 - -不链接 cflogger 的目标(如 `cf_desktop_render`、`cfbase`)无需添加。 - ---- - -## 6. 单实例保证原理 - -### 6.1 在 DLL 架构下 - -```text -cflogger.cpp 编译一次 → libcflogger.a (静态库) - ↓ - --whole-archive 合并 - ↓ - CFDesktop_shared.dll (唯一包含 Logger 实现的模块) - -CFDesktop.exe 通过 dllimport 访问 Logger::instance() -CFDesktopMain.a 通过 PRIVATE 链接直接解析内部类符号 -其他 .a 通过 cflogger_headers 只拿到头文件,不链接 .a -```text - -`--whole-archive` 确保即使没有直接引用的符号也被保留在 DLL 中,避免链接器丢弃"未使用"的 Logger 实现。 - -### 6.2 验证方法 - -在代码中打印 Logger 实例地址,确认所有模块使用同一个: - -```cpp -printf("Logger: %p\n", &Logger::instance()); -```yaml - -如果在 DLL 中调用和 EXE 中调用得到的地址相同,说明单实例保证生效。 - ---- - -## 7. CMake 关键决策记录 - -### 7.1 为什么不用 OBJECT library - -```cmake -# 可选方案(未采用) -add_library(cflog_obj OBJECT cflog.cpp ...) -target_sources(CFDesktopMain PRIVATE $) -```text - -OBJECT library 要求所有使用 cflogger 的模块都从同一个 OBJECT target 获取 `.o` 文件,这在当前多模块架构中引入更多管理复杂度。当前的 `STATIC + --whole-archive` 方案已经足够保证单实例。 - -### 7.2 为什么不用 header-only 单例 - -```cpp -// 危险!不要这样做 -inline Logger& instance() { - static Logger x; // 每个 TU 可能独立实例化 - return x; -} -```bash - -`inline` 函数中的 `static` 局部变量在 C++17 前的行为是 UB(多个 TU 可能各自实例化),即使在 C++17 后也有跨动态库的复杂性。始终让 `.cpp` 文件只编译一次是最安全的做法。 - -### 7.3 未来如果去掉 DLL - -如果将来改为全静态单进程架构(无 DLL),修改量极小: - -1. `CFDesktop_shared` 从 `SHARED` 改为 `STATIC` -2. `CFDesktop.exe` 直接链接所有静态库(不需要 `--whole-archive`) -3. 移除 `CFLOG_API` 宏(或全部定义为空) -4. `cflogger_headers` 方案完全不需要改 - ---- - -## 8. 修改的文件清单 - -| 文件 | 改动内容 | -|------|---------| -| `desktop/base/logger/CMakeLists.txt` | 新增 `cflogger_headers` INTERFACE target | -| `desktop/main/log/CMakeLists.txt` | `cflogger` → `cflogger_headers`;添加 `CFLOG_STATIC_BUILD` PRIVATE | -| `desktop/main/early_session/CMakeLists.txt` | `cflogger` → `cflogger_headers` | -| `desktop/main/path/CMakeLists.txt` | `cflogger` → `cflogger_headers` | -| `desktop/main/CMakeLists.txt` | 添加 `CFLOG_STATIC_BUILD` PRIVATE;保持 `cflogger` PRIVATE | -| `desktop/ui/CMakeLists.txt` | 添加 `CFLOG_STATIC_BUILD` PRIVATE | -| `desktop/ui/platform/CMakeLists.txt` | `cflogger` → `cflogger_headers`;添加 `CFLOG_STATIC_BUILD` PRIVATE | -| `desktop/ui/widget/init_session/CMakeLists.txt` | `cflogger` → `cflogger_headers`;添加 `CFLOG_STATIC_BUILD` PRIVATE | - ---- - -## 9. 扩展建议 - -### 9.1 未来新模块使用 cflogger 的规则 - -- 如果只调用 `CFLOG_INFO()` 等公开 API → 链接 `cflogger_headers` -- 如果直接实例化 `FileSink`/`ConsoleSink` 等内部类 → PRIVATE 链接 `cflogger`(仅限 DLL 内部模块) -- 测试/示例 → `CFLOG_STATIC_BUILD` + `CFDesktop::logger` -- **新模块如果会进入 `CFDesktop_shared` DLL** → 必须添加 `CFLOG_STATIC_BUILD` PRIVATE(参见第 5 节) - -### 9.2 适用于其他核心系统 - -同样的模式可以用于事件系统、配置系统等需要单实例保证的核心模块: - -```cmake -# 事件系统示例 -add_library(cfevent STATIC ...) -add_library(cfevent_headers INTERFACE ...) -target_include_directories(cfevent_headers INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) -```yaml - -**原则:`.cpp` 只编译一次,`.h` 可以到处传。** - ---- +## 当前状态 -*本文档编写于 2026-03-30,基于 CFDesktop 项目 feat/windows_backend 分支的实际架构改动。* +已实现并验证。`cflogger_headers` 定义于 `desktop/base/logger/CMakeLists.txt`,所有 DLL 内部模块已迁移至链接 headers。该模式可作为事件系统、配置系统等其他核心单例模块的参考模板。 diff --git a/document/notes/06-PolicyChain-Clang18-Miscompilation-Debug.md b/document/notes/06-PolicyChain-Clang18-Miscompilation-Debug.md index 8c132fabf..36b7de792 100644 --- a/document/notes/06-PolicyChain-Clang18-Miscompilation-Debug.md +++ b/document/notes/06-PolicyChain-Clang18-Miscompilation-Debug.md @@ -1,157 +1,23 @@ -# PolicyChain Clang 18 Miscompilation — Debug Progress +--- +title: PolicyChain Clang 18 Miscompilation +description: Clang 18 -O3 对 std::optional 返回值的寄存器打包缺陷导致 policy_chain 行为异常的根因与修复 +--- -## 问题描述 +# PolicyChain Clang 18 Miscompilation -- 设计意图 -CI 中 Clang 18 (Ubuntu 24.04 Docker, Release/-O3) 有 4 个 `policy_chain_test` 测试失败, -GCC 和 Clang 22 均通过。 +## 为什么需要 workaround -**失败的测试:** -- `PolicyChainTest.Fallback_SecondPolicy` — `execute(-3)` 返回 `-6`(期望 `3`) -- `PolicyChainTest.Fallback_MultiplePolicies` — `execute(-5)` 返回 `-10`(期望 `0`) -- `PolicyChainTest.MakePolicyChain_Basic` — `execute(-3)` 返回 `-6`(期望 `3`) -- `PolicyChainTest.Builder_Basic` — `execute(-3)` 返回 `-6`(期望 `3`) +Clang 18 在 `-O3` 下将 `std::optional` 的小对象返回值打包进寄存器时存在缺陷:当 lambda 返回 `std::nullopt` 时,`x * 2` 的 64-bit 计算结果通过 `lea` 指令产生进位,污染了 packed register 中的 engaged byte(bit 32)。调用方通过 `has_value()` 检测时看到 `has_value == true`,实际值却是 `-6` 而非预期的 fallback 到下一条 policy。GCC 和 Clang 19+ 不受影响。 -**共同模式:** 第一个 policy 的 `if (x > 0)` 条件被 Clang 18 优化掉, -`x * 2` 被无条件执行,`std::nullopt` 返回路径被消除。 +修复在 `PolicyModel::invoke()` 中:接收 `std::invoke()` 结果后,对 Clang < 19 显式重建 nullopt 路径(`if (!result.has_value()) return std::nullopt;`),强制编译器保留 disengaged 分支。 -## 复现命令(Docker) +## 关键决策 -```bash -# 构建 -docker run --rm -v "$PWD":/project -w /project \ - ghcr.io/awesome-embedded-learning-studio/cfdesktop-build-env:latest \ - bash -lc 'ccache -C; bash scripts/build_helpers/linux_fast_develop_build.sh ci -c build_ci_clang_config.ini' +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| 在 `invoke()` 内显式重建 nullopt 路径 | 最小侵入,只影响 Clang 18 的代码生成,不影响其他编译器 | `noinline/optnone` 屏障:无法深入保护 lambda 内部的返回值打包 | +| `#if` 条件编译限制为 Clang < 19 | 已确认 Clang 19+ 修复了此寄存器打包问题 | 全平台加屏障:不必要的性能退化 | -# 跑单个失败测试 -docker run --rm -v "$PWD":/project -w /project \ - ghcr.io/awesome-embedded-learning-studio/cfdesktop-build-env:latest \ - bash -lc './out/build_ci_clang/test/bin/policy_chain_test --gtest_filter="PolicyChainTest.Fallback_SecondPolicy"' +## 当前状态 -# 跑全部 policy_chain 测试 -docker run --rm -v "$PWD":/project -w /project \ - ghcr.io/awesome-embedded-learning-studio/cfdesktop-build-env:latest \ - bash -lc 'ctest --test-dir out/build_ci_clang/test -R policy_chain --output-on-failure' -``` - -## 关键文件 - -- **实现:** `base/include/base/policy_chain/policy_chain.hpp` -- **测试:** `test/base/policy_chain/policy_chain_test.cpp` - -## 已有的 Workaround(不够) - -文件中已有 `invoke_policy` barrier(第 30-41 行): - -```cpp -#if defined(__clang__) && (__clang_major__ < 19) -# define CF_POLICY_CHAIN_INVOKE_BARRIER __attribute__((noinline, optnone)) -#else -# define CF_POLICY_CHAIN_INVOKE_BARRIER -#endif - -template -[[nodiscard]] CF_POLICY_CHAIN_INVOKE_BARRIER auto -invoke_policy(Policy const& policy, CallArgs&&... args) - -> decltype(policy(std::forward(args)...)) { - return policy(std::forward(args)...); -} -``` - -这个 barrier 无效——问题不在 `execute()` 的 `has_value()` 检查,而在更深层。 - -## 调试发现(通过 fprintf 日志定位) - -### 实验 1:在 `execute()` 和 `PolicyModel::invoke()` 都加 fprintf → PASS -### 实验 2:只在 `execute()` 加 fprintf,`PolicyModel::invoke()` 不加 → FAIL - -**关键日志(失败时):** -``` -execute(-3): - [DEBUG] invoke_policy returned, has_value=1 ← 只调了一次! - [DEBUG] returning value: -6 -``` - -**正确行为(加 fprintf 后):** -``` -execute(-3): - [DEBUG] PolicyModel::invoke(), has_value=0, val=0 ← 第一个 policy 返回 nullopt - [DEBUG] PolicyModel::invoke(), has_value=1, val=3 ← 第二个 policy 返回 3 - [DEBUG] returning value: 3 -``` - -**结论:** 第一个 policy 的 lambda 被错误编译——`if (x > 0)` 条件被消除。 -fprintf 在 `PolicyModel::invoke()` 中作为副作用阻止了该错误优化。 - -## 已尝试但无效的修复 - -1. `__attribute__((noinline, optnone))` on `PolicyModel::invoke()` — 编译错误(与 `[[nodiscard]]` 冲突) -2. `[[clang::noinline]]` on `PolicyModel::invoke()` — 编译通过但测试仍失败 -3. `[[clang::optnone]]` on `PolicyModel::invoke()` — 编译通过但测试仍失败 -4. `[[gnu::noinline, gnu::optnone]]` — Clang 不认识 `[[gnu::optnone]]`,被忽略 - -## 调用链分析 - -``` -PolicyChain::execute(args) - → policy_chain_detail::invoke_policy(policy, args) [noinline, optnone] ← 无效 - → PolicyEntry::operator()(args) - → PolicyConcept::invoke(args) [virtual dispatch] - → PolicyModel::invoke(args) ← lambda 在这里被调用 - → std::invoke(policy_, args...) ← lambda body 被错误优化 -``` - -**根因:** Clang 18 `-O3` 在编译 `PolicyModel::invoke()` 时, -将 lambda body 内联后错误优化掉了条件分支。`invoke_policy` 的 barrier 在调用链外层, -无法保护 `invoke()` 内部的 lambda 编译。 - -## 根因分析(2026-05-23 反汇编确认) - -反汇编显示,lambda 的条件分支并不是简单被删除,而是 Clang 18 在 `-O3` -下把 `std::optional` 的小对象返回值打包进寄存器时**污染了 engaged byte**。 - -失败用例的第一条 policy 生成了类似逻辑: - -```asm -xor %eax,%eax -test %edi,%edi -setg %al -shl $0x20,%rax ; has_value 放到 bit 32 -mov %edi,%ecx -lea (%rax,%rcx,2),%rax -``` - -当 `x == -3` 时,`x * 2` 的 64-bit `lea` 结果为 `0x00000001fffffffa`, -bit 32 被算术进位置 1,导致 disengaged optional 被调用方看成 -`has_value == true`,值为 `-6`。 - -### 试过但无效 - -- 把 lambda 调用包进新的 `noinline, optnone invoke_stored_policy()`:无效,因为 - lambda `operator()` 仍会作为单独 O3 函数生成坏的返回打包。 -- `std::function(int)>` 最小复现:同样失败。 -- `asm volatile("" : "+m"(result) : : "memory")`:无效,只是把已经污染的 - packed register 存回内存。 - -## 有效修复 - -在 `PolicyModel::invoke()` 中先接住 `std::invoke()` 的结果,再针对 Clang < 19 -显式重建 nullopt 路径: - -```cpp -auto result = std::invoke(policy_, args...); -#if defined(__clang__) && (__clang_major__ < 19) -if (!result.has_value()) { - return std::nullopt; -} -#endif -return result; -``` - -这会让 Clang 18 保留 disengaged 分支,避免直接复用被污染的 packed register。 - -## 验证 - -- Docker/CI Clang 18:`ctest --test-dir out/build_ci_clang/test -R policy_chain --output-on-failure` - 全部通过,1/1 test passed。 -- 本地 develop 构建:`./out/build_develop/test/bin/policy_chain_test` - 全部通过,29/29 tests passed。 +已修复并验证。CI Docker Clang 18 和本地 develop 构建均通过。相关代码位于 `base/include/base/policy_chain/policy_chain.hpp`。 diff --git a/document/notes/README.md b/document/notes/README.md deleted file mode 100644 index 0f1ba2f75..000000000 --- a/document/notes/README.md +++ /dev/null @@ -1,189 +0,0 @@ ---- -title: CFDesktop 桌面行为系统设计文档 -description: 一套完整的跨平台桌面应用窗口行为管理架构设计方案 ---- - -# CFDesktop 桌面行为系统设计文档 - -> 一套完整的跨平台桌面应用窗口行为管理架构设计方案 - -## 概述 - -本文档系列详细阐述了 CFDesktop 项目中窗口行为建模、Qt 集成、策略模式应用和系统架构设计的完整方案。该架构通过分层设计和策略模式,实现了可扩展、跨平台、插件化的窗口行为管理系统。 - -## 核心特性 - -- **类型安全**:基于 Qt `QFlags` 的类型安全行为建模 -- **跨平台**:统一的抽象层,支持 Windows、macOS、Linux (X11/Wayland)、Embedded -- **可扩展**:插件化架构,动态加载行为策略 -- **可测试**:依赖抽象接口,便于编写单元测试 -- **冲突可控**:完善的冲突检测与解决机制 - -## 文档导航 - -| 文档 | 描述 | 链接 | -|------|------|------| -| **01** | 桌面行为建模:从 bool 到 QFlags | [README](01-Desktop-Behavior-Modeling-From-Bool-To-QFlags.md) | -| **02** | Qt 窗口行为解析:QWidget → DesktopBehaviors | [README](02-Qt-Window-Behavior-Analysis.md) | -| **03** | 桌面策略系统设计:Strategy Pattern 实战 | [README](03-Desktop-Strategy-Pattern-Design.md) | -| **04** | 桌面行为系统设计:从策略到 Window Manager 抽象 | [README](04-Desktop-Behavior-System-Architecture.md) | - -## 快速开始 - -### 安装 - -将文档克隆到本地或直接在项目中查看: - -```bash -cd /home/charliechen/CFDesktop/document/notes -```text - -### 阅读顺序 - -```text -新手开发者: - 01 → 02 → 03 → 04 - -有经验开发者: - 直接阅读 04,需要时查阅其他文档 -```text - -### 代码示例 - -```cpp -// 定义行为标志 -enum class DesktopBehaviorFlag { - None = 0, - Fullscreen = 1 << 0, - Frameless = 1 << 1, - StayOnTop = 1 << 2, - // ... -}; - -Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) - -// 使用行为组合 -DesktopBehaviors behaviors = DesktopBehaviorFlag::Fullscreen - | DesktopBehaviorFlag::Frameless; - -// 测试行为 -if (behaviors.testFlag(DesktopBehaviorFlag::Fullscreen)) { - // 处理全屏逻辑 -} -```text - -## 架构概览 - -```text -┌─────────────────────────────────────────────────────────────┐ -│ Application Layer │ -│ (用户代码 / 业务逻辑) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Behavior Abstraction Layer │ -│ (DesktopBehaviors - QFlags) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Strategy Layer │ -│ (IDesktopBehaviorStrategy - 插件化) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Qt Integration Layer │ -│ (Qt WindowFlags / QWidget) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Platform Abstraction Layer │ -│ (Windows / macOS / X11 / Wayland / Embedded) │ -└─────────────────────────────────────────────────────────────┘ -```bash - -## 核心概念 - -### DesktopBehaviors - -使用 `QFlags` 定义的行为标志集合,用于描述窗口的各种行为特性。 - -### Strategy Pattern - -将每种行为封装为独立的策略类,实现可插拔的行为管理。 - -### Conflict Resolution - -自动检测和解决行为冲突,确保系统状态一致性。 - -### Plugin System - -支持动态加载行为策略插件,扩展系统能力。 - -## 技术栈 - -| 技术 | 版本 | 用途 | -|------|------|------| -| Qt | 6.x | GUI 框架 | -| C++ | 17+ | 编程语言 | -| QFlags | - | 类型安全标志 | -| QPluginLoader | - | 插件加载 | - -## 平台支持 - -| 平台 | 状态 | 备注 | -|------|------|------| -| Windows | ✅ 完全支持 | 所有特性可用 | -| macOS | ✅ 完全支持 | 所有特性可用 | -| Linux (X11) | ✅ 完全支持 | 所有特性可用 | -| Linux (Wayland) | ⚠️ 部分支持 | 部分特性受限 | -| Embedded | ⚠️ 部分支持 | 取决于具体平台 | - -## 项目结构 - -```text -desktop/ -├── ui/ -│ ├── CFDesktop.cpp # 主窗口实现 -│ ├── CFDesktop.h -│ └── platform/ # 平台特定代码 -│ ├── linux_wsl/ # WSL/Linux 策略 -│ ├── windows/ # Windows 策略 -│ └── ... -├── main/ -│ └── init/ # 初始化代码 -└── ... - -document/notes/ # 本文档目录 -├── README.md -├── index.md -├── 01-Desktop-Behavior-Modeling-From-Bool-To-QFlags.md -├── 02-Qt-Window-Behavior-Analysis.md -├── 03-Desktop-Strategy-Pattern-Design.md -└── 04-Desktop-Behavior-System-Architecture.md -```yaml - -## 贡献指南 - -本文档是 CFDesktop 项目的技术文档,欢迎团队成员: - -1. 报告文档错误或不清晰的地方 -2. 提出新的文档需求 -3. 补充代码示例 -4. 分享使用经验 - -## 许可证 - -本文档为 CFDesktop 项目的内部技术文档。 - -## 参考资源 - -- [Qt 官方文档](https://doc.qt.io/qt-6/) -- [QFlags Class Reference](https://doc.qt.io/qt-6/qflags.html) -- [Qt Platform Abstraction](https://doc.qt.io/qt-6/qpa.html) -- [Strategy Pattern - Refactoring.Guru](https://refactoring.guru/design-patterns/strategy) - ---- - -**更新时间**:2025-03-27 - -**维护者**:CFDesktop Team diff --git a/document/notes/index.md b/document/notes/index.md index 0254c0f99..4a1e84f4f 100644 --- a/document/notes/index.md +++ b/document/notes/index.md @@ -89,42 +89,44 @@ description: 本文档系列详细介绍了桌面应用程序中窗口行为建 --- -## 核心概念 +## 架构概览 -### DesktopBehaviors - -使用 `QFlags` 定义的行为标志集合: - -```cpp -enum class DesktopBehaviorFlag { - None = 0, - Fullscreen = 1 << 0, - Frameless = 1 << 1, - StayOnTop = 1 << 2, - StayOnBottom = 1 << 3, - AllowResize = 1 << 4, - AvoidSystemUI = 1 << 5, - // ... -}; - -Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) ```text - -### 架构分层 +┌─────────────────────────────────────────────────────────────┐ +│ Application Layer │ +│ (用户代码 / 业务逻辑) │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Behavior Abstraction Layer │ +│ (DesktopBehaviors - QFlags) │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Strategy Layer │ +│ (IDesktopBehaviorStrategy - 插件化) │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Qt Integration Layer │ +│ (Qt WindowFlags / QWidget) │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Platform Abstraction Layer │ +│ (Windows / macOS / X11 / Wayland / Embedded) │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 阅读顺序 ```text -Application Layer (用户代码) - ↓ -Behavior Abstraction (DesktopBehaviors) - ↓ -Strategy Layer (IDesktopBehaviorStrategy) - ↓ -Qt Integration (WindowFlags) - ↓ -Platform Abstraction (Windows/X11/Wayland) -```bash +新手开发者: + 01 → 02 → 03 → 04 ---- +有经验开发者: + 直接阅读 04,需要时查阅其他文档 +``` ## 参考资源 @@ -148,39 +150,3 @@ Platform Abstraction (Windows/X11/Wayland) - [Layered Architecture - Medium](https://medium.com/@patrykrogedu/layered-architecture-and-abstraction-layers-167438dd1a8b) - [Software Architecture Patterns - Red Hat](https://www.redhat.com/en/blog/14-software-architecture-patterns) - ---- - -## 快速开始 - -1. **阅读顺序建议**: - - 初学者:01 → 02 → 03 → 04 - - 有经验开发者:可直接阅读 04,需要时再查阅其他文档 - -2. **实践建议**: - - 在阅读文档时,参考您的项目代码:`desktop/ui/platform/` - - 尝试将现有代码重构为文档中描述的架构 - - 使用 Mock 对象进行单元测试 - -3. **扩展建议**: - - 根据项目需求定义额外的 `DesktopBehaviorFlag` - - 实现平台特定的 Strategy - - 开发自定义插件 - ---- - -## 版本历史 - -| 版本 | 日期 | 说明 | -|------|------|------| -| 1.0.0 | 2025-03-27 | 初始版本,包含四篇核心文档 | - ---- - -## 许可 - -本文档系列为 CFDesktop 项目的内部技术文档,仅供项目开发和维护使用。 - ---- - -*本文档由 Claude (Anthropic) 协助编写,基于项目实际代码和 Qt 官方文档整理。* diff --git a/document/scripts/README.md b/document/scripts/README.md deleted file mode 100644 index 14e5e25ea..000000000 --- a/document/scripts/README.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: Scripts文档 -description: "文档编写日期: 2026-03-20,本目录包含CFDesktop项目所有脚本的完整文档。" ---- - -# Scripts文档 - -> 文档编写日期: 2026-03-20 - -本目录包含CFDesktop项目所有脚本的完整文档。 - -## 目录结构 - -```text -scripts/ -├── build_helpers/ # 构建辅助脚本 (Linux/Windows) -├── dependency/ # 依赖安装 -├── develop/ # 开发工具 (代码格式化、清理) -├── docker/ # Docker配置 -├── doxygen/ # Doxygen工具 -├── lib/ # 库文件 -│ ├── bash/ # Bash库 -│ └── powershell/ # PowerShell库 -├── release/ # 发布相关 -│ └── hooks/ # Git钩子 -└── run_helpers/ # 运行辅助 -```bash - -## 快速导航 - -### 构建相关 -- [构建辅助脚本](build_helpers/) - Linux/Windows构建脚本 - -### 开发工具 -- [依赖安装](dependency/install_build_dependencies.sh.md) - 环境配置 -- [代码格式化](develop/format_cpp.sh.md) - C++代码格式化 -- [空格清理](develop/remove_trailing_space.sh.md) - 删除行尾空格 - -### 容器化 -- [Docker配置](docker/Dockerfile.build.md) - 构建环境镜像 - -### 库文件 -- [Bash库](lib/bash/) - Bash函数库 -- [PowerShell库](lib/powershell/) - PowerShell模块 - -### 版本控制 -- [Git钩子](release/hooks/) - Git hooks配置 - -## 文档规范 - -每个脚本文档包含: -- **使用办法**: 基本语法、参数、示例 -- **Scripts详解**: 用途、依赖、核心功能 - -## 脚本语言 - -| 平台 | 脚本类型 | -|------|----------| -| Linux/macOS | Bash (.sh) | -| Windows | PowerShell (.ps1) | - -## 相关文档 - -- [开发指南](../../development/) - 项目开发文档 -- [CI/CD文档](../../ci/) - 持续集成文档 diff --git a/document/scripts/build_helpers/README.md b/document/scripts/build_helpers/README.md deleted file mode 100644 index f9c24a925..000000000 --- a/document/scripts/build_helpers/README.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: 构建辅助脚本 (Build Helpers) -description: "文档编写日期: 2026-03-20,本目录包含CFDesktop项目的构建辅助脚本。" ---- - -# 构建辅助脚本 (Build Helpers) - -> 文档编写日期: 2026-03-20 - -本目录包含CFDesktop项目的构建辅助脚本。 - -## Linux脚本 - -| 脚本 | 说明 | -|------|------| -| ci_build_entry.sh | CI构建入口 | -| linux_configure.sh | CMake配置 | -| linux_develop_build.sh | 完整开发构建 | -| linux_fast_develop_build.sh | 快速开发构建 | -| linux_deploy_build.sh | 完整部署构建 | -| linux_fast_deploy_build.sh | 快速部署构建 | -| linux_run_tests.sh | 运行测试 | -| docker_start.sh | Docker启动脚本 | - -## 构建类型 - -| 类型 | 说明 | 配置文件 | -|------|------|----------| -| develop | 开发构建,包含调试符号 | build_develop_config.ini | -| deploy | 部署构建,优化体积 | build_deploy_config.ini | -| fast_develop | 快速开发构建,增量编译 | build_develop_config.ini | -| fast_deploy | 快速部署构建,增量编译 | build_deploy_config.ini | - -## 配置文件 - -| 文件 | 说明 | -|------|------| -| build_develop_config.ini | 开发构建配置 | -| build_deploy_config.ini | 部署构建配置 | -| build_ci_config.ini | CI构建配置 | -| build_ci_aarch64_config.ini | ARM64 CI配置 | -| build_ci_armhf_config.ini | ARM HF CI配置 | - -## 快速开始 - -### 开发构建 - -```bash -# 完整开发构建(清理后构建) -./scripts/build_helpers/linux_develop_build.sh - -# 快速开发构建(增量编译) -./scripts/build_helpers/linux_fast_develop_build.sh -```text - -### 部署构建 - -```bash -# 完整部署构建(清理后构建) -./scripts/build_helpers/linux_deploy_build.sh - -# 快速部署构建(增量编译) -./scripts/build_helpers/linux_fast_deploy_build.sh -```text - -### 仅配置 - -```bash -./scripts/build_helpers/linux_configure.sh [develop|deploy|ci] -```text - -### 运行测试 - -```bash -./scripts/build_helpers/linux_run_tests.sh [develop|deploy|ci] -```text - -### Docker构建 - -```bash -# 交互式shell -./scripts/build_helpers/docker_start.sh - -# CI构建验证 -./scripts/build_helpers/docker_start.sh --verify - -# 构建项目(完整清理) -./scripts/build_helpers/docker_start.sh --build-project - -# 构建项目(快速) -./scripts/build_helpers/docker_start.sh --build-project-fast - -# 运行测试 -./scripts/build_helpers/docker_start.sh --run-project-test -```bash - -## 架构支持 - -脚本支持多架构构建: - -| 架构 | 平台 | 说明 | -|------|------|------| -| x86_64 / amd64 | linux/amd64 | 标准PC架构 | -| aarch64 | linux/arm64 | ARM64架构 | -| armv7l / armhf | linux/armhf | ARM32架构 (IMX6ULL) | - -CI构建脚本会自动检测容器架构并选择相应的配置文件。 - -## 依赖库 - -所有脚本依赖以下公共库(位于 `scripts/lib/bash/`): - -- `lib_common.sh` - 通用日志和工具函数 -- `lib_config.sh` - 配置文件处理 -- `lib_paths.sh` - 路径解析 -- `lib_build.sh` - 构建相关函数 -- `lib_args.sh` - 参数解析 - -## 退出码 - -- `0` - 成功 -- `非0` - 失败(具体错误码取决于失败阶段) diff --git a/document/scripts/build_helpers/index.md b/document/scripts/build_helpers/index.md index 8195b0368..7e2136bb9 100644 --- a/document/scripts/build_helpers/index.md +++ b/document/scripts/build_helpers/index.md @@ -1,12 +1,105 @@ --- title: 构建辅助脚本 -description: 本目录包含平台相关的构建辅助脚本,提供 Linux(Bash)和 Windows(PowerShel +description: 本目录包含平台相关的构建辅助脚本,提供 Linux(Bash)和 Windows(PowerShell)双平台的 CMake 配置、快速开发构建、完整构建以及测试运行等标准化构建流程。同时提供 Docker 环境下的构建脚本以确保跨平台一致性。 --- # 构建辅助脚本 -本目录包含平台相关的构建辅助脚本,提供 Linux(Bash)和 Windows(PowerShell)双平台的 CMake 配置、快速开发构建、完整构建以及测试运行等标准化构建流程。同时提供 Docker 环境下的构建脚本以确保跨平台一致性。 +本目录包含 CFDesktop 项目的构建辅助脚本,提供 Linux(Bash)和 Windows(PowerShell)双平台的 CMake 配置、快速开发构建、完整构建以及测试运行等标准化构建流程。同时提供 Docker 环境下的构建脚本以确保跨平台一致性。 ---- +## Linux脚本 + +| 脚本 | 说明 | +|------|------| +| ci_build_entry.sh | CI构建入口 | +| linux_configure.sh | CMake配置 | +| linux_develop_build.sh | 完整开发构建 | +| linux_fast_develop_build.sh | 快速开发构建 | +| linux_deploy_build.sh | 完整部署构建 | +| linux_fast_deploy_build.sh | 快速部署构建 | +| linux_run_tests.sh | 运行测试 | +| docker_start.sh | Docker启动脚本 | + +## 构建类型 + +| 类型 | 说明 | 配置文件 | +|------|------|----------| +| develop | 开发构建,包含调试符号 | build_develop_config.ini | +| deploy | 部署构建,优化体积 | build_deploy_config.ini | +| fast_develop | 快速开发构建,增量编译 | build_develop_config.ini | +| fast_deploy | 快速部署构建,增量编译 | build_deploy_config.ini | + +## 配置文件 + +| 文件 | 说明 | +|------|------| +| build_develop_config.ini | 开发构建配置 | +| build_deploy_config.ini | 部署构建配置 | +| build_ci_config.ini | CI构建配置 | +| build_ci_aarch64_config.ini | ARM64 CI配置 | +| build_ci_armhf_config.ini | ARM HF CI配置 | + +## 快速开始 + +### 开发构建 + +```bash +# 完整开发构建(清理后构建) +./scripts/build_helpers/linux_develop_build.sh + +# 快速开发构建(增量编译) +./scripts/build_helpers/linux_fast_develop_build.sh +``` + +### 部署构建 + +```bash +# 完整部署构建(清理后构建) +./scripts/build_helpers/linux_deploy_build.sh + +# 快速部署构建(增量编译) +./scripts/build_helpers/linux_fast_deploy_build.sh +``` + +### 仅配置 + +```bash +./scripts/build_helpers/linux_configure.sh [develop|deploy|ci] +``` + +### 运行测试 + +```bash +./scripts/build_helpers/linux_run_tests.sh [develop|deploy|ci] +``` + +### Docker构建 + +```bash +# 交互式shell +./scripts/build_helpers/docker_start.sh + +# CI构建验证 +./scripts/build_helpers/docker_start.sh --verify + +# 构建项目(完整清理) +./scripts/build_helpers/docker_start.sh --build-project + +# 构建项目(快速) +./scripts/build_helpers/docker_start.sh --build-project-fast + +# 运行测试 +./scripts/build_helpers/docker_start.sh --run-project-test +``` + +## 架构支持 + +脚本支持多架构构建: + +| 架构 | 平台 | 说明 | +|------|------|------| +| x86_64 / amd64 | linux/amd64 | 标准PC架构 | +| aarch64 | linux/arm64 | ARM64架构 | +| armv7l / armhf | linux/armhf | ARM32架构 (IMX6ULL) | -*Last updated: 2026-03-20* +CI构建脚本会自动检测容器架构并选择相应的配置文件。 diff --git a/document/scripts/index.md b/document/scripts/index.md index 68facdca5..566e2446b 100644 --- a/document/scripts/index.md +++ b/document/scripts/index.md @@ -1,12 +1,57 @@ --- title: 脚本工具 -description: 本目录包含 CFDesktop 项目全部脚本工具的文档,涵盖构建辅助脚本(Linux Bash / +description: 本目录包含 CFDesktop 项目全部脚本工具的文档,涵盖构建辅助脚本(Linux Bash / Windows PowerShell)、Docker 构建与部署脚本、第三方依赖管理脚本、开发工作流辅助工具、Doxygen 文档生成脚本以及正式发布流程脚本。 --- # 脚本工具 -本目录包含 CFDesktop 项目全部脚本工具的文档,涵盖构建辅助脚本(Linux Bash / Windows PowerShell)、Docker 构建与部署脚本、第三方依赖管理脚本、开发工作流辅助工具、Doxygen 文档生成脚本以及正式发布流程脚本。 +本目录包含 CFDesktop 项目所有脚本的完整文档,涵盖构建辅助脚本(Linux Bash / Windows PowerShell)、Docker 构建与部署脚本、第三方依赖管理脚本、开发工作流辅助工具、Doxygen 文档生成脚本以及正式发布流程脚本。 ---- +## 目录结构 + +```text +scripts/ +├── build_helpers/ # 构建辅助脚本 (Linux/Windows) +├── dependency/ # 依赖安装 +├── develop/ # 开发工具 (代码格式化、清理) +├── docker/ # Docker配置 +├── doxygen/ # Doxygen工具 +├── lib/ # 库文件 +│ ├── bash/ # Bash库 +│ └── powershell/ # PowerShell库 +├── release/ # 发布相关 +│ └── hooks/ # Git钩子 +└── run_helpers/ # 运行辅助 +``` + +## 快速导航 + +### 构建相关 +- [构建辅助脚本](build_helpers/) - Linux/Windows构建脚本 + +### 开发工具 +- [依赖安装](dependency/install_build_dependencies.sh.md) - 环境配置 +- [代码格式化](develop/format_cpp.sh.md) - C++代码格式化 +- [空格清理](develop/remove_trailing_space.sh.md) - 删除行尾空格 + +### 容器化 +- [Docker配置](docker/Dockerfile.build.md) - 构建环境镜像 + +### 库文件 +- [Bash库](lib/bash/) - Bash函数库 +- [PowerShell库](lib/powershell/) - PowerShell模块 + +### 版本控制 +- [Git钩子](release/hooks/) - Git hooks配置 + +## 脚本语言 + +| 平台 | 脚本类型 | +|------|----------| +| Linux/macOS | Bash (.sh) | +| Windows | PowerShell (.ps1) | + +## 相关文档 -*Last updated: 2026-03-20* +- [开发指南](../../development/) - 项目开发文档 +- [CI/CD文档](../../ci/) - 持续集成文档 diff --git a/document/scripts/lib/bash/README.md b/document/scripts/lib/bash/README.md deleted file mode 100644 index e7051884a..000000000 --- a/document/scripts/lib/bash/README.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: Bash库文档 -description: "文档编写日期: 2026-03-20,本目录包含CFDesktop构建系统使用的Bash库文件。" ---- - -# Bash库文档 - -> 文档编写日期: 2026-03-20 - -本目录包含CFDesktop构建系统使用的Bash库文件。 - -## 库文件列表 - -| 文件 | 说明 | -|------|------| -| `lib_common.sh` | 日志输出、颜色定义、通用工具函数 | -| `lib_config.sh` | INI 配置文件解析 | -| `lib_build.sh` | 构建相关工具函数 | - -## 使用方式 - -所有库文件可以独立source使用,或被其他脚本引用。 - -### 基本加载方式 - -```bash -#!/bin/bash - -# 获取库目录 -SCRIPT_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# 加载需要的模块 -source "$SCRIPT_LIB/lib_common.sh" -source "$SCRIPT_LIB/lib_config.sh" -source "$SCRIPT_LIB/lib_build.sh" -```text - -## 依赖关系 - -```text -lib_common.sh - ├── (被依赖) lib_config.sh - └── (被依赖) lib_build.sh -```text - -- **lib_common.sh** 是基础库,不依赖其他库 -- **lib_config.sh** 依赖 lib_common.sh(用于日志输出) -- **lib_build.sh** 依赖 lib_common.sh(会自动加载) - -## 模块详细文档 - -- [lib_common.sh](lib_common.sh.md) - 日志、颜色、通用工具 -- [lib_build.sh](lib_build.sh.md) - 构建相关函数 -- [lib_config.sh](lib_config.sh.md) - 配置文件处理 - -## 快速参考 - -### 日志输出 (lib_common.sh) - -```bash -log_info "信息消息" -log_success "成功消息" -log_warn "警告消息" -log_error "错误消息" -log_separator -log_progress 5 10 "处理中" -```text - -### 配置解析 (lib_config.sh) - -```bash -eval "$(get_ini_config config.ini)" -echo "$config_cmake_generator" - -value=$(get_ini_value config.ini "cmake" "generator") -has_ini_value config.ini "cmake" "generator" && echo "存在" -```text - -### 构建操作 (lib_build.sh) - -```bash -clean_build_dir "$BUILD_DIR" -run_cmake_configure "Ninja" "Release" "$SOURCE_DIR" "$BUILD_DIR" -run_cmake_build "$BUILD_DIR" "--all" $(get_parallel_job_count) -```text diff --git a/document/scripts/lib/bash/index.md b/document/scripts/lib/bash/index.md index 22d953dd0..87a10e38d 100644 --- a/document/scripts/lib/bash/index.md +++ b/document/scripts/lib/bash/index.md @@ -1,12 +1,71 @@ --- title: Bash 脚本库 -description: 本目录包含 Bash 共享工具函数,为 Linux 平台的构建、测试与发布脚本提供通用功能支持,包括 +description: 本目录包含 Bash 共享工具函数,为 Linux 平台的构建、测试与发布脚本提供通用功能支持,包括彩色日志输出、路径工具、环境变量检测与 CMake 封装调用等。 --- # Bash 脚本库 本目录包含 Bash 共享工具函数,为 Linux 平台的构建、测试与发布脚本提供通用功能支持,包括彩色日志输出、路径工具、环境变量检测与 CMake 封装调用等。 ---- +## 库文件列表 + +| 文件 | 说明 | +|------|------| +| `lib_common.sh` | 日志输出、颜色定义、通用工具函数 | +| `lib_config.sh` | INI 配置文件解析 | +| `lib_build.sh` | 构建相关工具函数 | + +## 使用方式 + +所有库文件可以独立 source 使用,或被其他脚本引用。 + +### 基本加载方式 + +```bash +#!/bin/bash + +# 获取库目录 +SCRIPT_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# 加载需要的模块 +source "$SCRIPT_LIB/lib_common.sh" +source "$SCRIPT_LIB/lib_config.sh" +source "$SCRIPT_LIB/lib_build.sh" +``` + +## 模块详细文档 + +- [lib_common.sh](lib_common.sh.md) - 日志、颜色、通用工具 +- [lib_build.sh](lib_build.sh.md) - 构建相关函数 +- [lib_config.sh](lib_config.sh.md) - 配置文件处理 + +## 快速参考 + +### 日志输出 (lib_common.sh) + +```bash +log_info "信息消息" +log_success "成功消息" +log_warn "警告消息" +log_error "错误消息" +log_separator +log_progress 5 10 "处理中" +``` + +### 配置解析 (lib_config.sh) + +```bash +eval "$(get_ini_config config.ini)" +echo "$config_cmake_generator" + +value=$(get_ini_value config.ini "cmake" "generator") +has_ini_value config.ini "cmake" "generator" && echo "存在" +``` + +### 构建操作 (lib_build.sh) -*Last updated: 2026-03-20* +```bash +clean_build_dir "$BUILD_DIR" +run_cmake_configure "Ninja" "Release" "$SOURCE_DIR" "$BUILD_DIR" +run_cmake_build "$BUILD_DIR" "--all" $(get_parallel_job_count) +``` diff --git a/document/scripts/lib/powershell/README.md b/document/scripts/lib/powershell/README.md deleted file mode 100644 index ea546aba5..000000000 --- a/document/scripts/lib/powershell/README.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: PowerShell库文档 -description: "文档编写日期: 2026-03-20,本目录包含 CFDesktop 构建系统使用的 PowerSh" ---- - -# PowerShell库文档 - -> 文档编写日期: 2026-03-20 - -本目录包含 CFDesktop 构建系统使用的 PowerShell 库模块文档。 - -## 模块列表 - -| 文件 | 说明 | -|------|------| -| LibCommon.psm1 | 日志、通用工具函数 | -| LibBuild.psm1 | 构建相关函数(CMake、目录管理) | -| LibConfig.psm1 | INI 配置文件解析 | -| LibGit.psm1 | Git 相关操作函数 | -| LibPaths.psm1 | 路径处理函数 | -| LibArgs.psm1 | 参数解析函数 | - -## 使用方式 - -### 单个模块加载 - -```powershell -Import-Module scripts/lib/powershell/LibCommon.psm1 -Import-Module scripts/lib/powershell/LibConfig.psm1 -```text - -### 脚本内加载(点号加载) - -```powershell -. "$PSScriptRoot\LibCommon.psm1" -. "$PSScriptRoot\LibConfig.psm1" -. "$PSScriptRoot\LibBuild.psm1" -```text - -## 依赖关系 - -```text -LibCommon.psm1 (基础模块,无依赖) - ├── LibBuild.psm1 (依赖 LibCommon) - ├── LibConfig.psm1 (无依赖) - ├── LibPaths.psm1 (无依赖) - ├── LibArgs.psm1 (无依赖) - └── LibGit.psm1 (无依赖) -```text - -**注意**: LibBuild.psm1 依赖 LibCommon.psm1,使用前必须先加载 LibCommon.psm1。 - -## 模块详细文档 - -- [LibCommon.psm1](LibCommon.psm1.md) - 日志输出功能 -- [LibBuild.psm1](LibBuild.psm1.md) - 构建工具函数 -- [LibConfig.psm1](LibConfig.psm1.md) - 配置文件解析 - -## 快速参考 - -### 日志输出 (LibCommon) - -```powershell -Write-LogInfo "Information message" -Write-LogSuccess "Operation completed" -Write-LogWarning "Warning message" -Write-LogError "Error message" -Write-LogSeparator -Write-LogProgress -Current 5 -Total 10 -Message "Processing" -```text - -### 构建操作 (LibBuild) - -```powershell -# 清理构建目录 -Clean-BuildDir "C:\Build\Output" - -# 确保目录存在 -Ensure-BuildDir "C:\Build\Output" - -# CMake 配置 -Invoke-CMakeConfigure -Generator "Ninja" -BuildType "Release" -SourceDir "." -BuildDir "build" - -# CMake 构建 -Invoke-CMakeBuild -BuildDir "build" -Parallel (Get-ParallelJobCount) -```text - -### 配置读取 (LibConfig) - -```powershell -# 读取完整配置 -$config = Get-IniConfig -FilePath "config.ini" -$value = $config["section"]["key"] - -# 读取单个值 -$value = Get-IniValue -FilePath "config.ini" -Section "section" -Key "key" - -# 检查配置存在 -if (Test-IniValue -FilePath "config.ini" -Section "section" -Key "key") { - # 配置存在 -} -```text diff --git a/document/scripts/lib/powershell/index.md b/document/scripts/lib/powershell/index.md index ab5a37c13..670f647ba 100644 --- a/document/scripts/lib/powershell/index.md +++ b/document/scripts/lib/powershell/index.md @@ -1,12 +1,87 @@ --- title: PowerShell 脚本库 -description: 本目录包含 PowerShell 共享工具函数,为 Windows 平台的构建、测试与发布脚本提供通 +description: 本目录包含 PowerShell 共享工具函数,为 Windows 平台的构建、测试与发布脚本提供通用功能支持,包括日志输出、路径处理、环境检测与 CMake 封装调用等,与 Bash 脚本库保持功能对齐。 --- # PowerShell 脚本库 本目录包含 PowerShell 共享工具函数,为 Windows 平台的构建、测试与发布脚本提供通用功能支持,包括日志输出、路径处理、环境检测与 CMake 封装调用等,与 Bash 脚本库保持功能对齐。 ---- +## 模块列表 + +| 文件 | 说明 | +|------|------| +| LibCommon.psm1 | 日志、通用工具函数 | +| LibBuild.psm1 | 构建相关函数(CMake、目录管理) | +| LibConfig.psm1 | INI 配置文件解析 | +| LibGit.psm1 | Git 相关操作函数 | +| LibPaths.psm1 | 路径处理函数 | +| LibArgs.psm1 | 参数解析函数 | + +## 使用方式 + +### 单个模块加载 + +```powershell +Import-Module scripts/lib/powershell/LibCommon.psm1 +Import-Module scripts/lib/powershell/LibConfig.psm1 +``` + +### 脚本内加载(点号加载) + +```powershell +. "$PSScriptRoot\LibCommon.psm1" +. "$PSScriptRoot\LibConfig.psm1" +. "$PSScriptRoot\LibBuild.psm1" +``` + +## 模块详细文档 + +- [LibCommon.psm1](LibCommon.psm1.md) - 日志输出功能 +- [LibBuild.psm1](LibBuild.psm1.md) - 构建工具函数 +- [LibConfig.psm1](LibConfig.psm1.md) - 配置文件解析 + +## 快速参考 + +### 日志输出 (LibCommon) + +```powershell +Write-LogInfo "Information message" +Write-LogSuccess "Operation completed" +Write-LogWarning "Warning message" +Write-LogError "Error message" +Write-LogSeparator +Write-LogProgress -Current 5 -Total 10 -Message "Processing" +``` + +### 构建操作 (LibBuild) + +```powershell +# 清理构建目录 +Clean-BuildDir "C:\Build\Output" + +# 确保目录存在 +Ensure-BuildDir "C:\Build\Output" + +# CMake 配置 +Invoke-CMakeConfigure -Generator "Ninja" -BuildType "Release" -SourceDir "." -BuildDir "build" + +# CMake 构建 +Invoke-CMakeBuild -BuildDir "build" -Parallel (Get-ParallelJobCount) +``` + +### 配置读取 (LibConfig) + +```powershell +# 读取完整配置 +$config = Get-IniConfig -FilePath "config.ini" +$value = $config["section"]["key"] + +# 读取单个值 +$value = Get-IniValue -FilePath "config.ini" -Section "section" -Key "key" -*Last updated: 2026-03-20* +# 检查配置存在 +if (Test-IniValue -FilePath "config.ini" -Section "section" -Key "key") { + # 配置存在 +} +``` diff --git a/document/scripts/release/hooks/README.md b/document/scripts/release/hooks/README.md deleted file mode 100644 index 7c84ebbfe..000000000 --- a/document/scripts/release/hooks/README.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: Git Hooks -description: "文档编写日期: 2026-03-20,CFDesktop项目的Git hooks配置目录。" ---- - -# Git Hooks - -> 文档编写日期: 2026-03-20 - -CFDesktop项目的Git hooks配置目录。 - -## 快速安装 - -```bash -# Linux/macOS -bash scripts/release/hooks/install_hooks.sh - -# Windows PowerShell -.\scripts\release\hooks\install_hooks.ps1 -```bash - -## 文件说明 - -| 文件 | 说明 | -|------|------| -| pre-commit.sample | 代码格式检查钩子 | -| pre-push.sample | Docker构建验证钩子 | -| version_utils.sh | 版本号解析辅助函数 | -| install_hooks.sh | Linux/macOS安装脚本 | -| install_hooks.ps1 | Windows安装脚本 | - -## 验证级别 - -- **main分支**: X64 FastBuild + Tests -- **release分支**: 根据Major/Minor/Patch自动检测 -- **feat分支**: 跳过pre-push验证 - -## 详细文档 - -完整使用指南请参考: document/release_rule/git_hooks_guide.md diff --git a/document/scripts/release/hooks/index.md b/document/scripts/release/hooks/index.md index d59853b91..96cec5663 100644 --- a/document/scripts/release/hooks/index.md +++ b/document/scripts/release/hooks/index.md @@ -1,12 +1,38 @@ --- title: Git Hooks -description: 本目录包含发布流程中使用的 Git Hooks 脚本,用于在 等关键操作前后自动执行代码检查、文档 +description: 本目录包含发布流程中使用的 Git Hooks 脚本,用于在 `git push` 等关键操作前后自动执行代码检查、文档生成和版本一致性验证,防止不符合规范的代码进入远程仓库。 --- # Git Hooks 本目录包含发布流程中使用的 Git Hooks 脚本,用于在 `git push` 等关键操作前后自动执行代码检查、文档生成和版本一致性验证,防止不符合规范的代码进入远程仓库。 ---- +## 快速安装 + +```bash +# Linux/macOS +bash scripts/release/hooks/install_hooks.sh + +# Windows PowerShell +.\scripts\release\hooks\install_hooks.ps1 +``` + +## 文件说明 + +| 文件 | 说明 | +|------|------| +| pre-commit.sample | 代码格式检查钩子 | +| pre-push.sample | Docker构建验证钩子 | +| version_utils.sh | 版本号解析辅助函数 | +| install_hooks.sh | Linux/macOS安装脚本 | +| install_hooks.ps1 | Windows安装脚本 | + +## 验证级别 + +- **main分支**: X64 FastBuild + Tests +- **release分支**: 根据Major/Minor/Patch自动检测 +- **feat分支**: 跳过pre-push验证 + +## 详细文档 -*Last updated: 2026-03-20* +完整使用指南请参考: document/release_rule/git_hooks_guide.md diff --git a/document/todo/README.md b/document/todo/README.md deleted file mode 100644 index a2b50237f..000000000 --- a/document/todo/README.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -title: CFDesktop 项目 TODO 看板 -description: 本目录包含 CFDesktop 项目各模块的详细 TODO 清单,基于 设计文档和 架构规范整理 ---- - -# CFDesktop 项目 TODO 看板 - -## 概述 - -本目录包含 CFDesktop 项目各模块的详细 TODO 清单,基于 `design_stage` 设计文档和 `MaterialRules.md` 架构规范整理而成。 - -## 模块索引 - -| TODO 文件 | 模块 | 预计周期 | 依赖 | 状态 | -|----------|------|---------|------|------| -| [done/00_project_skeleton_status.md](done/00_project_skeleton_status.md) | 工程骨架搭建 | 1~2 周 | - | ✅ 100% | -| [done/01_hardware_probe_status.md](done/01_hardware_probe_status.md) | 硬件探针与能力分级 | 2~3 周 | Phase 0 | 🚧 90% | -| ~~[done/02_base_library_status.md](done/02_base_library_status.md)~~ | ~~Base 库核心~~ | ~~3~4 周~~ | Phase 0, 1 | ✅ 100% | -| [02_input_layer.md](base/02_input_layer.md) | 输入抽象层 | 1~2 周 | Phase 0, 1 | ⬜ 0% | -| [03_simulator.md](base/03_simulator.md) | 多平台模拟器 | 2~3 周 | Phase 0, 2 | ⬜ 0% | -| [04_testing.md](base/04_testing.md) | 测试体系 | 贯穿全程 | 所有阶段 | 🚧 55% | -| [99_ui_material_framework.md](base/99_ui_material_framework.md) | UI Material Framework | 持续迭代 | Phase 0-3 | 🚧 95% | -| desktop/ | Desktop 模块 (显示后端+窗口管理) | 持续迭代 | Phase 0-6 | 🚧 90% | - -## 状态图例 - -- ⬜ **待开始** (Todo) - 尚未开始的任务 -- 🚧 **进行中** (In Progress) - 正在开发的任务 -- ✅ **已完成** (Done) - 已完成的任务 -- ⚠️ **已废弃** (Deprecated) - 不再需要的任务 -- 🔄 **阻塞中** (Blocked) - 被依赖阻塞的任务 - -## 里程碑时间线 - -| 里程碑 | 时间 | 交付物 | -|--------|------|--------| -| M0 | Week 2 | ✅ 工程骨架 + Git Hooks CI/CD | -| M1 | Week 5 | 硬件探针 + 三档能力分级 | -| M2 | Week 9 | Base 库 + 主题引擎 + 输入抽象 | -| M3 | Week 15 | Shell UI 主体可用 | -| M4 | Week 18 | SDK 导出 + 示例应用 | -| M5 | Week 21 | 模拟器可用 | -| M6 | Week 23 | 应用商店基础 + 完整 CI/CD | - -## 快速链接 - -### 按角色查找 - -- **新手入门**: 从 [done/00_project_skeleton_status.md](done/00_project_skeleton_status.md) 开始 -- **基础开发**: [done/01_hardware_probe_status.md](done/01_hardware_probe_status.md) -- **UI 开发**: [99_ui_material_framework.md](base/99_ui_material_framework.md) + [02_input_layer.md](base/02_input_layer.md) -- **调试工具**: [03_simulator.md](base/03_simulator.md) -- **测试工程师**: [04_testing.md](base/04_testing.md) - -### 按任务类型查找 - -- **架构设计**: 各模块文档中的"架构设计"章节 -- **API 接口**: 各模块文档中的"类接口设计"章节 -- **单元测试**: [04_testing.md](base/04_testing.md) + 各模块文档中的"单元测试"章节 -- **性能优化**: 各模块文档中的"性能要求"章节 - -## 文档同步 - -本 TODO 目录与以下文档保持同步: -- `../design_stage/` - 详细设计文档 -- `../../ui/MaterialRules.md` - UI Material 架构规范 -- `../../BLUEPRINT.md` - 项目整体规划 - -## 更新记录 - -| 日期 | 变更 | 影响模块 | -|------|------|----------| -| 2026-03-30 | 更新 v0.13.1 进度: WSL X11 Backend Ready, Windows Desktop Backend, 显示后端架构完成 | desktop/, done/ | -| 2026-03-18 | Base库完成,删除02_base_library.md,重新编号 | base/ | -| 2026-03-07 | CI/CD 完成 (Git Hooks 策略) | 工程骨架 | -| 2026-03-05 | 创建 TODO 看板 | 全部 | - ---- - -*最后更新: 2026-03-30* diff --git a/document/todo/done/00_project_skeleton_status.md b/document/todo/done/00_project_skeleton_status.md deleted file mode 100644 index ff55a7eb8..000000000 --- a/document/todo/done/00_project_skeleton_status.md +++ /dev/null @@ -1,447 +0,0 @@ ---- -title: "Phase 0: 工程骨架搭建 - 状态文档" -description: "模块ID: Phase 0,总体进度: 100%" ---- - -# Phase 0: 工程骨架搭建 - 状态文档 - -> **模块ID**: Phase 0 -> **状态**: ✅ 已完成 -> **总体进度**: 100% -> **最后更新**: 2026-03-11 - ---- - -## 一、模块概述 - -工程骨架模块是 CFDesktop 项目的基础设施层,负责提供完整的构建系统、开发环境配置和 CI/CD 流水线。该模块为所有后续开发工作提供稳固的工程化基础。 - -### 核心职责 - -1. **CMake 构建系统** - 跨平台编译、依赖管理、输出配置 -2. **代码规范配置** - 格式化、静态分析、编码标准 -3. **开发工具集成** - IDE 配置、调试支持、辅助脚本 -4. **CI/CD 流水线** - 自动构建、文档部署、质量检查 - ---- - -## 二、完成状态总览 - -| 项目 | 完成度 | 状态 | -|------|--------|------| -| CMake 构建系统 | 90% | ✅ | -| 代码规范配置 | 80% | ✅ | -| 开发工具配置 | 70% | ✅ | -| CI/CD 流水线 | 100% | ✅ | -| 交叉编译支持 | 0% | ⚠️ 已取消 (Docker替代) | - ---- - -## 三、已完成工作 - -### 3.1 CMake 构建系统 (90%) - -**主配置文件**: `CMakeLists.txt` - -**已完成功能**: -- CMake 3.16+ 版本要求 -- C++17 标准设置 -- Qt6 集成 (Core, Gui, Widgets) -- 自动化 MOC/RCC/UIC 处理 -- 构建日志辅助系统 -- 编译命令导出 (compile_commands.json) -- 分类输出目录配置 (bin/, lib/, examples/) -- VSCode clangd 配置自动生成 -- VSCode 调试配置自动生成 - -**关键配置摘要**: -```cmake -cmake_minimum_required(VERSION 3.16) -project(CFDesktop VERSION 0.0.1 LANGUAGES CXX) - -# C++17 标准 -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# Qt6 依赖 -find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) - -# 子模块 -add_subdirectory(base) # 基础库 -add_subdirectory(ui) # UI 框架 -add_subdirectory(example) # 示例程序 -add_subdirectory(test) # 测试代码 -```text - -**依赖文件**: -- `cmake/build_log_helper.cmake` - 构建日志辅助 -- `cmake/check_toolchain.cmake` - 工具链检查 -- `cmake/custom_target_helper.cmake` - 自定义目标辅助 -- `cmake/OutputDirectoryConfig.cmake` - 输出目录配置 -- `cmake/generate_develop_helpers.cmake` - 开发辅助生成 -- `cmake/ExampleLauncher.cmake` - Windows 启动脚本生成 -- `cmake/QtDeployUtils.cmake` - Qt 部署工具 - -### 3.2 代码规范配置 (80%) - -**配置文件**: `.clang-format` - -**已完成功能**: -- 基于 LLVM 风格 -- 4 空格缩进 -- 100 字符列宽 -- 左对齐指针/引用 (int* a) -- 大括号附加风格 (Attach) -- 包含块排序 -- C++17 标准 - -**配置摘要**: -```yaml -BasedOnStyle: LLVM -IndentWidth: 4 -UseTab: Never -ColumnLimit: 100 -PointerAlignment: Left -BreakBeforeBraces: Attach -Standard: c++17 -SortIncludes: true -```text - -### 3.3 开发工具集成 (70%) - -**VSCode 配置**: `.vscode/settings.json` - -**已完成功能**: -- clangd 路径配置 -- 编译命令目录指定 -- 查询驱动器配置 -- 后台索引启用 -- clang-tidy 集成 -- 头文件插入优化 - -**其他 VSCode 配置**: -- `.vscode/extensions.json` - 推荐扩展 -- `.vscode/launch.json` - 调试配置 (自动生成) - -### 3.4 CI/CD 流水线 (100%) - -**策略**: Git Hooks 本地验证,无需远程 CI 构建流水线 - -#### 3.4.1 Git Pre-Push Hook - -**文件**: `scripts/release/hooks/pre-push.sample` - -**已完成功能**: -- 版本号检查 (阻止未更新版本的推送) -- Docker 构建验证 (本地执行) -- main 分支: X64 FastBuild + Tests -- release 分支: 根据 Major/Minor/Patch 自动检测验证级别 - - Major: X64 + ARM64 完整构建 - - Minor: X64 完整构建 - - Patch: X64 FastBuild + Tests - -#### 3.4.2 Git Pre-Commit Hook - -**文件**: `scripts/release/hooks/pre-commit.sample` - -**已完成功能**: -- 空白字符检查 (trailing whitespace) -- C++ 代码自动格式化 (clang-format) -- 支持跨平台 (Windows PowerShell / Linux bash) - -#### 3.4.3 Docker 构建系统 - -**文件**: `scripts/docker/Dockerfile.build` - -**已完成功能**: -- 多架构支持 (amd64/arm64/armhf) -- Qt 6.8.1 通过 aqtinstall 自动安装 -- 依赖自动化安装 (`scripts/dependency/install_build_dependencies.sh`) - -#### 3.4.4 Docker 构建脚本 - -**文件**: `scripts/build_helpers/docker_start.sh` - -**已完成功能**: -- 多架构构建 (--arch amd64/arm64) -- CI 验证模式 (--verify) -- 快速构建 (--fast-build) -- 项目构建 (--build-project) -- 测试运行 (--run-project-test) -- 美化日志输出 - -#### 3.4.5 CI 构建入口 - -**文件**: `scripts/build_helpers/ci_build_entry.sh` - -**已完成功能**: -- 自动架构检测 (x86_64/aarch64/armv7l) -- 自动选择对应配置文件 - -#### 3.4.6 版本工具 - -**文件**: `scripts/release/hooks/version_utils.sh` - -**已完成功能**: -- 版本号解析 (Major/Minor/Patch) -- 验证级别自动检测 -- 本地/远程版本比较 - -#### 3.4.7 Hooks 安装脚本 - -**文件**: `scripts/release/hooks/install_hooks.sh` - -**已完成功能**: -- 自动安装 pre-commit 和 pre-push hooks -- 支持交互式确认 -- 备份现有 hooks - -#### 3.4.8 文档部署流水线 - -**文件**: `.github/workflows/deploy.yml` - -**已完成功能**: -- MkDocs 文档自动构建 -- Doxygen API 文档生成 -- doxybook2 Markdown 转换 -- GitHub Pages 自动部署 -- Python 3.11 环境配置 - -**触发条件**: -- Push 到 main 分支 -- 手动触发 (workflow_dispatch) - -### 3.5 目录结构 - -**已建立的目录结构**: -```text -CFDesktop/ -├── base/ # 基础库模块 -│ ├── system/ # 系统检测 (CPU, 内存) -│ └── include/ # 公共头文件 -├── ui/ # UI 框架 -│ ├── core/ # 核心组件 -│ ├── material/ # Material Design -│ └── base/ # 基础工具 -├── example/ # 示例程序 -├── test/ # 测试代码 -├── cmake/ # CMake 模块 -├── .github/ # GitHub 配置 -└── .vscode/ # VSCode 配置 -```bash - ---- - -## 四、实施时间线 - -### Week 1 任务 - -#### Day 1-2: 目录结构创建 -- [x] 创建完整目录树 - - [x] `base/` - 基础库源码 (system/, utilities/) - - [x] `ui/` - UI 框架源码 (core/, components/, widget/) - - [x] `example/` - 示例程序 - - [x] `test/` - 测试代码 - - [x] `cmake/` - CMake 模块 - - [x] `scripts/` - 构建和辅助脚本 -- [x] 编写主 `CMakeLists.txt` - - [x] 设置 C++17 标准 - - [x] 配置 Qt6 依赖 - - [x] 添加子目录 - - [x] 自动化 MOC/RCC/UIC -- [x] 创建各子模块的 CMakeLists.txt 框架 -- [x] 配置 VSCode 开发环境 - - [x] `.vscode/settings.json` - clangd 配置 - - [x] `.vscode/extensions.json` - 推荐扩展 - - [x] `.vscode/launch.json` - 调试配置 - -#### Day 3-4: ~~交叉编译配置~~ (已取消,使用 Docker 多架构替代) -- ~~编写 ARMv7 工具链文件~~ -- ~~编写 ARM64 工具链文件~~ -- [x] Docker 多架构构建替代方案 - - [x] `scripts/docker/Dockerfile.build` - 支持 amd64/arm64/armhf - -#### Day 5: 代码规范配置 -- [x] 配置 `.clang-format` - - [x] 基于 LLVM 风格 - - [x] 设置缩进为 4 空格 - - [x] 配置列宽 100 - - [x] 设置指针对齐方式 -- [x] 配置 Git pre-commit hook - - [x] 创建 `scripts/release/hooks/pre-commit.sample` 脚本 - - [x] 自动格式化 (clang-format) - - [x] 添加 trailing whitespace 检查 - - [x] 配置可跳过选项 (git commit --no-verify) - - [x] 安装脚本配置 (`scripts/release/hooks/install_hooks.sh`) - -### Week 2 任务 - -#### Day 1-3: CI/CD 搭建 -- [x] 创建 Docker 构建镜像 - - [x] `scripts/docker/Dockerfile.build` - 多架构支持 (amd64/arm64/armhf) - - [x] 配置 Qt6 环境 (aqtinstall 6.8.1) -- [x] 编写 Git Hooks 工作流 - - [x] `scripts/release/hooks/pre-push.sample` - Push 前验证 - - [x] `scripts/release/hooks/pre-commit.sample` - Commit 前检查 - - [x] `scripts/release/hooks/version_utils.sh` - 版本工具 - - [x] `scripts/release/hooks/install_hooks.sh` - Hooks 安装脚本 -- [x] Docker 构建脚本 - - [x] `scripts/build_helpers/docker_start.sh` - 本地 Docker 构建 - - [x] `scripts/build_helpers/ci_build_entry.sh` - CI 入口 (自动架构检测) -- [x] 配置部署流程 `.github/workflows/deploy.yml` - MkDocs 文档自动部署 -- [x] 测试完整构建流程 - -#### Day 4-5: 开发工具完善 -- [x] 创建 VSCode 工作区配置 `.vscode/settings.json` - - [x] CMake 配置参数 - - [x] Clangd 配置 -- [x] 配置推荐扩展 `.vscode/extensions.json` - - [x] CMake Tools - - [x] C/C++ - - [x] clang-format -- [x] 编写 Hello World 测试程序 - - [x] `example/base/system/` - 系统信息示例 - - [x] `example/ui/widget/material/` - 控件示例 -- [x] 编写开发环境设置文档 - - [x] `document/development/01_prerequisites.md` - 前置要求 - - [x] `document/development/02_quick_start.md` - 快速开始 - - [x] `document/development/03_build_system.md` - 构建系统 - - [x] `document/development/04_development_tools.md` - 开发工具 - - [x] `document/development/05_docker_build.md` - Docker 构建 - - [x] `document/development/06_git_hooks.md` - Git Hooks - - [x] `document/development/07_troubleshooting.md` - 常见问题 - ---- - -## 五、重要架构决策 - -### 5.1 CMakePresets.json - 已取消 - -**原因**: -- 不方便维护 -- 配置与命令行参数重复 -- 团队更习惯使用 -DCMAKE_* 参数 - -**替代方案**: -- 使用 build_*.config.ini 配置文件 -- 通过命令行参数传递构建配置 - -### 5.2 src/base/sdk/shell 三层结构 - 已调整 - -**原计划**: -```text -src/ -├── base/ # 基础库 -├── sdk/ # SDK 层 -└── shell/ # Shell UI -```text - -**实际采用**: -```text -base/ # 基础库 -ui/ # UI 框架 -example/ # 示例程序 -```yaml - -**原因**: 更简洁的模块划分 - -### 5.3 ARM 交叉编译工具链 - 已推迟 - -**原因**: 优先实现桌面平台功能,嵌入式平台后续支持 - ---- - -## 六、验收标准 - -### CI/CD -- [x] Push 代码前自动触发本地构建验证 (pre-push hook) -- [x] Commit 前自动代码格式检查 (pre-commit hook) -- [x] 版本号检查 (阻止未更新版本的推送) -- [x] 多架构构建支持 (amd64/arm64/armhf) -- [x] 代码格式化一键执行 (pre-commit hook 自动格式化) -- [x] MkDocs 文档自动部署到 gh-pages - -### 开发环境 -- [x] VSCode 能够正确索引所有符号 (clangd 配置) -- [x] 代码格式化一键执行 (pre-commit hook) -- [x] 调试配置可用 (`.vscode/launch.json` 自动生成) -- [x] 新团队成员能在 30 分钟内完成环境搭建 (`document/development/`) - ---- - -## 七、关键文件路径 - -### 已实现文件 - -```text -CFDesktop/ -├── CMakeLists.txt # 主 CMake 配置 -├── .clang-format # 代码格式化配置 -├── .vscode/ -│ ├── settings.json # VSCode 设置 -│ ├── extensions.json # 推荐扩展 -│ └── launch.json # 调试配置 (自动生成) -├── .github/ -│ └── workflows/ -│ └── deploy.yml # 文档部署流水线 -├── scripts/ -│ ├── docker/ -│ │ └── Dockerfile.build # 多架构 Docker 镜像 -│ ├── build_helpers/ -│ │ ├── docker_start.sh # Docker 构建脚本 -│ │ ├── ci_build_entry.sh # CI 构建入口 -│ │ └── build_ci_*.ini # 多架构配置 -│ ├── release/ -│ │ └── hooks/ -│ │ ├── pre-commit.sample # Pre-commit hook -│ │ ├── pre-push.sample # Pre-push hook -│ │ ├── version_utils.sh # 版本工具 -│ │ └── install_hooks.sh # Hooks 安装脚本 -│ └── dependency/ -│ └── install_build_dependencies.sh # 依赖安装 -└── cmake/ - ├── build_log_helper.cmake # 构建日志 - ├── check_toolchain.cmake # 工具链检查 - ├── custom_target_helper.cmake # 自定义目标 - ├── OutputDirectoryConfig.cmake # 输出配置 - ├── generate_develop_helpers.cmake # 开发辅助生成 - ├── ExampleLauncher.cmake # 启动脚本生成 - └── QtDeployUtils.cmake # Qt 部署工具 -```text - -### 待创建文件 - -```text -CFDesktop/ -├── .clang-tidy # 静态分析配置 (不考虑) -├── .github/ -│ └── workflows/ -│ └── build.yml # 构建流水线 (不需要,Git Hooks 替代) -└── cmake/ - └── toolchains/ - ├── arm-linux-gnueabihf.cmake # ARMv7 工具链 (推迟) - └── aarch64-linux-gnu.cmake # ARM64 工具链 (推迟) -```bash - ---- - -## 八、风险与缓解 - -| 风险 | 影响 | 缓解措施 | -|------|------|----------| -| Docker 多架构构建时间较长 | 开发效率 | 使用 --fast-build 缓存镜像 | -| ARM64 QEMU 仿真速度慢 | CI 耗时 | 仅在 release/Major 版本验证 | -| 工具链版本兼容性 | 编译失败 | 固定 Docker 镜像版本 | - ---- - -## 九、相关文档 - -- 原始TODO: ~~已归档~~(原 `00_project_skeleton.md` 已删除) -- 设计文档: `../../design_stage/00_phase0_project_skeleton.md` -- 开发环境设置: `../../development/` - ---- - -*文档版本: v1.0* -*生成时间: 2026-03-11* diff --git a/document/todo/done/01_hardware_probe_status.md b/document/todo/done/01_hardware_probe_status.md deleted file mode 100644 index 7189c1436..000000000 --- a/document/todo/done/01_hardware_probe_status.md +++ /dev/null @@ -1,435 +0,0 @@ ---- -title: "Phase 1: 硬件探针与能力分级 - 状态文档" -description: "模块ID: Phase 1,状态: 🚧 部分完成" ---- - -# Phase 1: 硬件探针与能力分级 - 状态文档 - -> **模块ID**: Phase 1 -> **状态**: ✅ 完成 -> **总体进度**: 100% -> **最后更新**: 2026-05-23 - ---- - -## 一、模块概述 - -硬件探针模块是 CFDesktop 项目的能力分级核心,负责在启动时自动检测系统硬件能力,根据检测结果计算硬件档位 (HWTier),并为上层模块提供对应的能力策略配置。 - -### 核心职责 - -1. **硬件检测** - CPU、GPU、内存、网络自动检测 -2. **能力分级** - 根据检测结果计算 Low/Mid/High 三档 -3. **策略引擎** - 为各模块提供对应档位的能力配置 -4. **配置覆盖** - 支持手动配置和自定义检测脚本 - -### 档位定义 - -| 档位 | 典型硬件 | 动画 | 渲染 | 视频解码 | 内存限制 | -|------|----------|------|------|----------|----------| -| Low | IMX6ULL (528MHz Cortex-A7) | 禁用 | linuxfb | 软件 | < 64MB | -| Mid | RK3568 (4xCortex-A55, Mali-G52) | 部分启用 | eglfs可选 | H.264/H.265 部分 | < 256MB | -| High | RK3588 (8xCortex-A76/A55, Mali-G610) | 全部启用 | eglfs+OpenGL ES 3.2+ | 全格式硬件解码 | < 1GB | - ---- - -## 二、完成状态总览 - -| 模块 | 完成度 | 状态 | -|------|--------|------| -| CPUDetector | 100% | ✅ 完成 | -| MemoryDetector | 95% | ✅ 完成 | -| GPUDetector | 90% | ✅ 核心功能已完成 | -| NetworkDetector | 85% | ✅ 核心功能已完成 | -| HWTier 系统 | 100% | ✅ 完成 | -| CapabilityPolicy | 100% | ✅ 完成 | -| HardwareProbe 主类 | 100% | ✅ 完成 | - ---- - -## 三、已完成工作 - -### 3.1 CPU 检测器 (80%) - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `base/include/system/cpu/cfcpu.h` | CPU 基础信息接口 | -| `base/include/system/cpu/cfcpu_profile.h` | CPU 详细信息 (频率、核心数) | -| `base/include/system/cpu/cfcpu_bonus.h` | CPU 扩展信息 | -| `base/system/cpu/cfcpu.cpp` | 基础信息实现 | -| `base/system/cpu/cfcpu_profile.cpp` | 详细信息实现 | -| `base/system/cpu/cfcpu_bonus.cpp` | 扩展信息实现 | -| `base/system/cpu/private/linux_impl/*.cpp` | Linux 平台实现 | -| `base/system/cpu/private/win_impl/*.cpp` | Windows 平台实现 | - -#### 功能实现 - -- [x] `CPUInfoView` 结构体 - 型号、架构、厂商 -- [x] `/proc/cpuinfo` 解析 (Linux) -- [x] WMI 查询 (Windows) -- [x] CPU 核心数检测 -- [x] CPU 频率检测 -- [x] CPU 特性检测 (neon, vfpv4, AVX, etc.) -- [x] 设备树 compatible 读取 -- [x] `uname` 架构检测 - -#### 测试 - -- 测试文件: `test/system/test_cpu_info_query.cpp` - ---- - -### 3.2 内存检测器 (80%) - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `base/include/system/memory/memory_info.h` | 内存信息接口 | -| `base/system/memory/memory_info.cpp` | 内存信息实现 | -| `base/system/memory/private/linux_impl/*.cpp` | Linux 平台实现 | -| `base/system/memory/private/win_impl/*.cpp` | Windows 平台实现 | - -#### 功能实现 - -- [x] 总内存检测 -- [x] 可用内存检测 -- [x] Swap 内存检测 -- [x] 物理内存详情 (DIMM) -- [x] 进程内存统计 -- [x] 缓存内存统计 - -#### 测试 - -- 测试文件: `test/system/test_memory_info_query.cpp` - ---- - -### 3.3 GPU 检测器 (100%) - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `base/include/system/gpu/gpu.h` | GPU/显示信息接口 | -| `base/system/gpu/gpu.cpp` | 跨平台实现 | -| `base/system/gpu/private/linux_impl/gpu_info.cpp` | Linux 平台实现 | -| `base/system/gpu/private/win_impl/gpu_info.cpp` | Windows 平台实现 | - -#### 功能实现 - -- [x] `GPUInfo` 结构体 - GPU 设备信息 -- [x] `DisplayInfo` 结构体 - 显示器信息 -- [x] `EnvironmentScore` 结构体 - 环境评分 -- [x] Linux DRM 设备检测 (`/dev/dri/card*`) -- [x] Linux DeviceTree SoC 检测 -- [x] WSL2 GPU 探测 (`/dev/dxg`) -- [x] Windows DXGI 检测 -- [x] GPU 评分算法 (满分 50) -- [x] 显示评分算法 (满分 50) -- [x] 档位判定 (Low/Mid/High) - -#### 示例 - -- 示例文件: `example/base/system/example_gpu_info.cpp` - ---- - -### 3.4 网络检测器 (100%) - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `base/include/system/network/network.h` | 网络信息接口 | -| `base/system/network/network.cpp` | 基于 Qt 的跨平台实现 | - -#### 功能实现 - -- [x] `IpAddress` 结构体 - IPv4/IPv6 地址 -- [x] `InterfaceInfo` 结构体 - 网卡信息 -- [x] `AddressEntry` 结构体 - IP 地址条目 -- [x] `InterfaceFlags` 结构体 - 接口标志 -- [x] `NetworkStatus` 结构体 - 网络状态 -- [x] `Reachability` 枚举 - 网络可达性 -- [x] `TransportMedium` 枚举 - 传输介质 -- [x] `DnsEligibility` 枚举 - DNS 可达性 -- [x] `getNetworkInfo()` 函数 - -#### 示例 - -- 示例文件: `example/base/system/example_network_info.cpp` - ---- - -## 四、使用示例 - -```cpp -#include -#include - -// 查询 CPU 信息 -auto cpu_info = cf::getCPUInfo(); -if (cpu_info) { - qDebug() << "CPU:" << cpu_info.value().model.data(); - qDebug() << "Arch:" << cpu_info.value().arch.data(); -} - -// 查询内存信息 -auto mem_info = cf::getMemoryInfo(); -if (mem_info) { - qDebug() << "Total:" << mem_info.value().totalBytes; - qDebug() << "Available:" << mem_info.value().availableBytes; -} -```yaml - ---- - -## 五、HWTier 系统 (Phase 1 核心功能) — ✅ 已完成 - -> **注意**: GPU 和 Network 检测器已完成,HWTier 系统已实现,Phase 1 核心功能全部完成。 - -### 5.1 HWTier 枚举定义 ✅ - -**实现位置**: `base/include/system/hardware_tier/hardware_tier_data.h` - -- [x] `HardwareTierLevel` 枚举 (Unknown/Low/Mid/High) -- [x] 档位字符串转换函数 `hardwareTierLevelToString()` -- [x] 各档位对应硬件配置说明 (i.MX6ULL / RK3568 / RK3588) - -### 5.2 硬件数据收集器 ✅ - -**实现位置**: `base/system/hardware_tier/hardware_tier_collector.h`, `base/system/hardware_tier/default/default_collector.cpp` - -- [x] `IHardwareCollector` 接口 — 可插拔收集器 -- [x] `HardwareData` 结构体 — CPU/GPU/Memory/Display 原始数据 -- [x] 默认收集器 — 整合 CPU/GPU/Memory/Display 检测器 -- [x] 跨平台支持 (Linux/Windows) - -### 5.3 评分引擎 ✅ - -**实现位置**: `base/system/hardware_tier/hardware_tier_scorer.h`, `base/system/hardware_tier/default/default_*_scorer.cpp` - -- [x] `IHardwareScorer` 接口 — 可插拔评分器 -- [x] 维度评分类型: `CpuScore`, `GpuScore`, `MemoryScore`, `DisplayScore` (各 0-100) -- [x] 默认评分器: CPU/GPU/Memory/Display 四维度独立评分 -- [x] 可通过 `registerScorer()` 替换各维度评分逻辑 - -### 5.4 档位评估器 ✅ - -**实现位置**: `base/system/hardware_tier/hardware_tier_assessor.h`, `base/system/hardware_tier/default/default_assessor.cpp` - -- [x] `IHardwareAssessor` 接口 — 可插拔评估器 -- [x] `HardwareTierAssessment` 结构体 — 四维度评分 + 总档位 + 覆盖信息 -- [x] 默认评估器 — 综合四维度分数计算档位 - -### 5.5 策略引擎 ✅ - -**实现位置**: `base/system/hardware_tier/hardware_tier_policy.h`, `base/system/hardware_tier/default/default_policy.cpp` - -- [x] `IHardwarePolicy` 接口 — 可插拔策略 -- [x] `HardwareTierCapabilities` 结构体 — 能力标志 (OpenGL/软件渲染/动画/硬件解码/EGLFS/LinuxFB) -- [x] 默认策略 — 根据 Low/Mid/High 档位映射能力标志 -- [x] 部分动画支持 (`enable_partial_animation`) - -### 5.6 DeviceConfig 覆盖 ✅ - -**实现位置**: `base/include/system/hardware_tier/hardware_tier.h` - -- [x] `setDeviceConfigOverride()` — 手动强制档位 -- [x] `clearDeviceConfigOverride()` — 清除覆盖 -- [x] 覆盖时跳过收集/评分,直接使用指定档位 -- [x] 覆盖原因记录 (`override_reason`) - -### 5.7 管线注册 API ✅ - -**实现位置**: `base/include/system/hardware_tier/hardware_tier.h` - -- [x] `registerCollector()` — 注册自定义收集器 -- [x] `registerScorer()` — 注册自定义评分器 (按维度) -- [x] `registerAssessor()` — 注册自定义评估器 -- [x] `registerPolicy()` — 注册自定义策略 -- [x] `assessHardware()` — 执行完整管线 (缓存支持) -- [x] `getHardwareTierCapabilities()` — 查询能力标志 - -### 5.8 示例代码 ✅ - -**实现位置**: `example/base/system/example_hardware_tier.cpp` - -- [x] 完整的硬件分级评估示例 -- [x] 四维度评分展示 -- [x] 能力标志展示 - ---- - -## 六、测试设计 - -### 6.1 单元测试用例 - -**文件**: `test/hardware/test_hardware_probe.cpp` - -```cpp -class TestHardwareProbe : public QObject { - Q_OBJECT - -private slots: - // CPU 检测测试 - void testDetectCPU_IMX6ULL(); - void testDetectCPU_RK3568(); - void testDetectCPU_RK3588(); - - // GPU 检测测试 - void testDetectGPU_WithDRM(); - void testDetectGPU_NoDRM(); - void testDetectGPU_OpenGLContext(); - - // 内存检测测试 - void testDetectMemory_512MB(); - void testDetectMemory_1GB(); - void testDetectMemory_4GB(); - - // 档位计算测试 - void testCalculateTier_IMX6ULL_returnsLow(); - void testCalculateTier_RK3568_returnsMid(); - void testCalculateTier_RK3588_returnsHigh(); - - // 配置文件测试 - void testDeviceConfig_LoadDefault(); - void testDeviceConfig_OverrideTier(); - void testDeviceConfig_CustomScript(); - - // 策略引擎测试 - void testCapabilityPolicy_LowTier(); - void testCapabilityPolicy_MidTier(); - void testCapabilityPolicy_HighTier(); - void testCapabilityPolicy_AnimationDisabledOnLow(); - - // 边界情况 - void testEmptyProcFiles(); - void testMalformedConfig(); - void testTierOverride(); -}; -```text - -### 6.2 评分算法 - -```cpp -void HardwareProbe::calculateTier(HardwareInfo& info) { - HWTier calculatedTier = HWTier::Low; - int score = 0; - - // CPU 评分 - score += std::min(info.cpu.cores, 8) * 10; // 最多 80 分 - if (info.cpu.frequencyMHz > 1000) score += 20; - if (info.cpu.features.contains("neon")) score += 10; - - // GPU 评分 - if (info.gpu.hasHardwareAcceleration) score += 50; - if (!info.gpu.driverPath.isEmpty()) score += 20; - - // 内存评分 - int memoryMB = info.memory.totalBytes / (1024 * 1024); - if (memoryMB >= 512) score += 20; - if (memoryMB >= 1024) score += 20; - if (memoryMB >= 2048) score += 10; - - // 档位判定 - // Low: 0-60, Mid: 61-120, High: 121+ - if (score >= 121) { - calculatedTier = HWTier::High; - } else if (score >= 61) { - calculatedTier = HWTier::Mid; - } else { - calculatedTier = HWTier::Low; - } - - info.tier = calculatedTier; -} -```yaml - ---- - -## 七、关键文件路径 - -### 已实现文件 - -```text -base/ -├── system/ -│ ├── cpu/ -│ │ ├── cfcpu.h # CPU 检测接口 -│ │ ├── cfcpu_bonus.h # CPU 扩展信息 -│ │ ├── cfcpu_profile.h # CPU 性能分析 -│ │ └── private/ -│ │ ├── linux_impl/ # Linux 实现 -│ │ └── win_impl/ # Windows 实现 -│ └── memory/ -│ ├── memory_info.h # 内存检测接口 -│ └── private/ -│ ├── linux_impl/ # Linux 实现 -│ └── win_impl/ # Windows 实现 -└── include/ - ├── system/cpu/ # CPU 公共头文件 - └── system/memory/ # 内存公共头文件 -```text - -### 待创建文件 - -> HWTier 系统已全部实现,无待创建文件。 - -```text -base/ -├── include/system/hardware_tier/ -│ ├── hardware_tier.h # 公共 API (管线注册 + 评估入口) -│ └── hardware_tier_data.h # 数据结构 (枚举/评分/结果/能力标志) -├── system/hardware_tier/ -│ ├── hardware_tier_collector.h # IHardwareCollector 接口 -│ ├── hardware_tier_scorer.h # IHardwareScorer 接口 -│ ├── hardware_tier_assessor.h # IHardwareAssessor 接口 -│ ├── hardware_tier_policy.h # IHardwarePolicy 接口 -│ ├── default_factories.h # 默认工厂函数 -│ ├── hardware_tier.cpp # 管线实现 -│ └── default/ -│ ├── default_collector.cpp # 默认收集器 -│ ├── default_cpu_scorer.cpp # 默认 CPU 评分 -│ ├── default_gpu_scorer.cpp # 默认 GPU 评分 -│ ├── default_memory_scorer.cpp # 默认 Memory 评分 -│ ├── default_display_scorer.cpp # 默认 Display 评分 -│ ├── default_assessor.cpp # 默认评估器 -│ └── default_policy.cpp # 默认策略 -```text - ---- - -## 八、下一步行动建议 - -### 已完成 ✅ - -1. **HWTier 枚举定义** — ✅ 已实现 (`HardwareTierLevel`) -2. **HardwareProbe 管线** — ✅ 已实现 (可插拔 Collect→Score→Assess→Policy 四阶段管线) -3. **CapabilityPolicy 策略引擎** — ✅ 已实现 (`IHardwarePolicy` + 默认策略) -4. **DeviceConfig 覆盖** — ✅ 已实现 (`setDeviceConfigOverride()`) - -### 后续可选增强 - -1. **集成 ConfigStore 查询覆盖** — 从 ConfigStore 读取设备档位配置,自动调用 `setDeviceConfigOverride()` -2. **自定义检测脚本执行** — 支持外部脚本注入硬件数据 -3. **Mock 数据集** — 单元测试用模拟数据 -4. **性能测试** — 各评分器在不同硬件下的基准测试 - ---- - -## 九、相关文档 - -- 原始TODO: ~~已归档~~(原 `01_hardware_probe.md` 已删除) -- 设计文档: `../../../design_stage/01_phase1_hardware_probe/` -- CPU 实现: `../../../HandBook/api/system/cpu/overview/` -- 内存实现: `../../../HandBook/api/system/memory/memory_info/` - ---- - -*文档版本: v2.0* -*生成时间: 2026-03-11* -*最后更新: 2026-05-23 (HWTier 系统完成)* diff --git a/document/todo/done/02_base_library_status.md b/document/todo/done/02_base_library_status.md deleted file mode 100644 index 6ec37edf5..000000000 --- a/document/todo/done/02_base_library_status.md +++ /dev/null @@ -1,625 +0,0 @@ ---- -title: "Phase 2: 基础库核心 - 状态文档" -description: "模块ID: Phase 2,总体进度: 100%" ---- - -# Phase 2: 基础库核心 - 状态文档 - -> **模块ID**: Phase 2 -> **状态**: ✅ 完成 -> **总体进度**: 100% -> **最后更新**: 2026-03-18 -> **架构说明**: 原规划在 `base/` 目录,实际实现在 `ui/` 和 `desktop/base/` 目录 - ---- - -## 一、模块概述 - -Base库核心是CFDesktop项目的基础设施层,为所有上层模块提供统一的主题管理、动画控制、分辨率适配、配置存储和日志记录能力。 - -### 核心职责 - -1. **主题管理** - 动态主题切换、变量解析、QSS处理 -2. **动画管理** - 动画生命周期、性能降级、预定义动画 -3. **分辨率适配** - dp/sp单位转换、屏幕参数检测 -4. **配置中心** - 层级存储、变更监听、持久化 -5. **日志系统** - 多Sink支持、日志轮转、标签过滤 - ---- - -## 二、完成状态总览 - -| 模块 | 完成度 | 实现位置 | 备注 | -|------|--------|----------|------| -| ThemeEngine | 60% | `ui/core/` | 核心框架已实现 | -| AnimationManager | 65% | `ui/components/` | 接口已定义 | -| DPI management | 80% | `ui/base/` | 基础功能完成 | -| ConfigStore | 100% | `desktop/base/config_manager/` | ✅ 完成 | -| Logger | 100% | `desktop/base/logger/` | ✅ 完成 | - ---- - -## 三、已完成工作 - -### 3.1 主题引擎 ThemeEngine (60%) - -**实际实现位置**: `ui/core/` (非原计划的 `base/`) - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `ui/core/theme.h` | ICFTheme 接口 | -| `ui/core/theme_manager.h` | 主题管理器 (单例) | -| `ui/core/theme_factory.h` | 主题工厂接口 | -| `ui/core/material/cfmaterial_theme.h` | Material 主题实现 | -| `ui/core/material/cfmaterial_scheme.h` | Material 配色方案 | -| `ui/core/material/cfmaterial_radius_scale.h` | 圆角规范 | -| `ui/core/material/cfmaterial_fonttype.h` | 字体规范 | -| `ui/core/material/cfmaterial_motion.h` | 动画规范 | -| `ui/core/material/material_factory_class.h` | Material 工厂 | - -#### 功能实现 - -- [x] 主题管理器 - `ThemeManager` 单例 -- [x] 主题注册/卸载 - `insert_one()`, `remove_one()` -- [x] 主题切换 - `setThemeTo()` -- [x] 主题变更信号 - `themeChanged` -- [x] Widget 主题安装 - `install_widget()` -- [x] Material 配色方案 - MaterialColorScheme -- [x] Token 系统 - `cfmaterial_token_literals.h` -- [x] 工厂模式创建 - MaterialFactory - -#### 接口摘要 - -```cpp -namespace cf::ui::core { -class ThemeManager : public QObject { -public: - static ThemeManager& instance(); - const ICFTheme& theme(const std::string& name) const; - bool insert_one(const std::string& name, InstallerMaker make_one); - void remove_one(const std::string& name); - void install_widget(QWidget* w); - void remove_widget(QWidget* w); - void setThemeTo(const std::string& name, bool doBroadcast = true); - const std::string& currentThemeName() const; -signals: - void themeChanged(const ICFTheme& new_theme); -}; -} -```bash - -#### 测试 - -- [x] `test/ui/core/token_test.cpp` - -#### 示例 - ---- - -### 3.2 动画管理器 AnimationManager (65%) - -**实际实现位置**: `ui/components/` - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `ui/components/animation.h` | 动画基类 | -| `ui/components/animation_factory_manager.h` | 动画工厂管理器 | -| `ui/components/animation_group.h` | 动画组 | -| `ui/components/timing_animation.h` | 时序动画 | -| `ui/components/spring_animation.h` | 弹簧动画 | -| `ui/components/material/cfmaterial_animation_strategy.h` | 动画策略 | -| `ui/components/material/cfmaterial_animation_factory.h` | Material 动画工厂 | -| `ui/components/material/cfmaterial_fade_animation.h` | 淡入淡出 | -| `ui/components/material/cfmaterial_slide_animation.h` | 滑动动画 | -| `ui/components/material/cfmaterial_scale_animation.h` | 缩放动画 | -| `ui/components/material/cfmaterial_property_animation.h` | 属性动画 | - -#### 功能实现 - -- [x] 动画工厂管理器 -- [x] 动画注册/创建 -- [x] 预定义动画: - - [x] FadeAnimation (淡入淡出) - - [x] SlideAnimation (滑动,支持方向) - - [x] ScaleAnimation (缩放) -- [x] 动画组: - - [x] 并行组 - - [x] 串行组 -- [x] 动画生命周期管理 -- [x] 动画策略模式 -- [x] WeakPtr 所有权管理 - -#### 核心类型 - -```cpp -namespace cf::ui::components { -// 动画创建器函数类型 -using AnimationCreator = std::function; - -// 动画工厂管理器接口 -class ICFAnimationManagerFactory : public QObject { - virtual RegisteredResult registerOneAnimation(const QString& name, const QString& type) = 0; - virtual RegisteredResult registerAnimationCreator(const QString& name, AnimationCreator creator) = 0; - virtual cf::WeakPtr getAnimation(const char* name) = 0; - virtual void setEnabledAll(bool enabled) = 0; - virtual bool isAllEnabled() = 0; -}; - -// 动画状态枚举 -enum class State { Idle, Running, Paused, Finished }; -enum class Direction { Forward, Backward }; -} -```bash - -#### 示例 - ---- - -### 3.3 DPI 适配 (80%) - -**实际实现位置**: `ui/base/` - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `ui/base/device_pixel.h` | dp/sp 转换工具 | -| `ui/base/device_pixel.cpp` | 实现 | - -#### 功能实现 - -- [x] `CanvasUnitHelper` 结构体 -- [x] `dpToPx()` - 设备无关像素转物理像素 -- [x] `spToPx()` - 可缩放像素转物理像素 -- [x] `pxToDp()` - 物理像素转设备无关像素 -- [x] `dpi()` - 获取设备像素比 -- [x] `BreakPoint` - 响应式断点 (Compact/Medium/Expanded) - -#### 接口摘要 - -```cpp -namespace cf::ui::base::device { -struct CanvasUnitHelper { - CanvasUnitHelper(const qreal devicePixelRatio); - qreal dpToPx(qreal dp) const; - qreal spToPx(qreal sp) const; - qreal pxToDp(qreal px) const; - qreal dpi() const; - - enum class BreakPoint { Compact, Medium, Expanded }; - BreakPoint breakPoint(qreal widthDp); -}; -} -```bash - -#### 测试 - -- [x] `test/ui/base/device_pixel_test.cpp` - ---- - -### 3.4 颜色系统支持 - -**位置**: `ui/base/color.h`, `ui/base/color_helper.h` - -**已完成**: -- HCT颜色空间支持 `CFColor` 类 -- 颜色混合 `blend()` -- 对比度计算 `contrastRatio()` -- 色调板生成 `tonalPalette()` -- 高程叠加 `elevationOverlay()` -- 相对亮度计算 `relativeLuminance()` - ---- - -### 3.5 ConfigStore - 配置中心 (100%) - -**实现位置**: `desktop/base/config_manager/` - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/base/config_manager/include/cfconfig_key.h` | 配置键定义 | -| `desktop/base/config_manager/include/cfconfig_layer.h` | 配置层级 (Temp/App/User/System) | -| `desktop/base/config_manager/include/cfconfig_notify_policy.h` | 通知策略 | -| `desktop/base/config_manager/include/cfconfig/cfconfig_result.h` | 操作结果 | -| `desktop/base/config_manager/include/cfconfig/cfconfig_watcher.h` | 配置监听器 | -| `desktop/base/config_manager/include/cfconfig/cfconfig_path_provider.h` | 路径提供者 | -| `desktop/base/config_manager/src/cfconfig.cpp` | 实现 | -| `desktop/base/config_manager/src/impl/config_impl.h` | 内部实现 | -| `desktop/base/config_manager/src/impl/config_impl.cpp` | 内部实现 | - -#### 功能实现 - -- [x] 四层存储 (Temp/App/User/System) -- [x] 点分隔键名支持 (`group.subgroup.key`) -- [x] 优先级查询 (Temp → App → User → System) -- [x] 变更监听机制 (watch/unwatch) -- [x] 持久化存储 (JSON格式) -- [x] 同步/异步 sync -- [x] 类型安全查询 (模板 API) -- [x] 线程安全 - -#### 接口摘要 - -```cpp -namespace cf::config { - -enum class Layer { Temp, App, User, System }; - -class ConfigStore : public SimpleSingleton { -public: - // 查询操作 - template - std::optional query(const KeyView key); - template - Value query(const KeyView key, const Value& default_value); - template - std::optional query(const KeyView key, Layer layer); - bool has_key(const KeyView key); - - // 写入操作 - template - bool set(const KeyView key, const Value& v, Layer layer = Layer::App); - template - RegisterResult register_key(const Key& key, const Value& init_value, Layer layer = Layer::App); - UnRegisterResult unregister_key(const Key& key, Layer layer = Layer::App); - - // 监听操作 - WatcherHandle watch(const std::string& key_pattern, Watcher callback); - void unwatch(WatcherHandle handle); - - // 持久化 - void sync(SyncMethod m = SyncMethod::Async); - void reload(); -}; - -} -```bash - -#### 测试 - -- [x] `test/config_manager/config_store_test.cpp` - -#### 文档 - -- [x] `document/desktop/base/config_manager/` - API文档 -- [x] `document/HandBook/desktop/base/config_manager/` - 使用手册 - ---- - -### 3.6 Logger - 日志系统 (100%) - -**实现位置**: `desktop/base/logger/` - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/base/logger/include/cflog.h` | 便捷日志函数 | -| `desktop/base/logger/include/cflog/cflog_record.h` | 日志记录 | -| `desktop/base/logger/include/cflog/cflog_sink.h` | Sink 接口 | -| `desktop/base/logger/include/cflog/cflog_format.h` | 格式化接口 | -| `desktop/base/logger/include/cflog/cflog_format_factory.h` | 格式化工厂 | -| `desktop/base/logger/include/cflog/cflog_format_config.h` | 格式化配置 | -| `desktop/base/logger/include/cflog/cflog_format_flags.h` | 格式化标志 | -| `desktop/base/logger/src/logger/cflog.cpp` | 实现 | -| `desktop/base/logger/src/impl/cflog_impl.h` | 内部实现 | -| `desktop/base/logger/src/impl/cflog_impl.cpp` | 内部实现 | - -#### 功能实现 - -- [x] 多等级日志 (Trace/Debug/Info/Warning/Error) -- [x] ISink 接口 -- [x] 多种格式化器 -- [x] 异步日志 (后台线程处理) -- [x] 线程安全队列 -- [x] 等级过滤 -- [x] Tag 支持 -- [x] 源码位置自动捕获 (`std::source_location`) - -#### 接口摘要 - -```cpp -namespace cf::log { - -enum class level { Trace, Debug, Info, Warning, Error }; - -class Logger : public SimpleSingleton { -public: - bool log(level log_level, std::string_view msg, std::string_view tag, std::source_location loc); - void flush(); - void flush_sync(); - void setMininumLevel(const level lvl); - void add_sink(std::shared_ptr sink); - void remove_sink(ISink* sink); - void clear_sinks(); -}; - -// 便捷函数 -void trace(std::string_view msg, std::string_view tag = "CFLog", ...); -void debug(std::string_view msg, std::string_view tag = "CFLog", ...); -void info(std::string_view msg, std::string_view tag = "CFLog", ...); -void warning(std::string_view msg, std::string_view tag = "CFLog", ...); -void error(std::string_view msg, std::string_view tag = "CFLog", ...); - -class ISink { -public: - virtual bool write(const LogRecord& record) = 0; - virtual bool flush() = 0; - virtual bool setFormat(std::shared_ptr formatter); -}; - -} -```yaml - -#### 测试 - -- [x] `test/logger/logger_formatter_test.cpp` -- [x] `test/logger/logger_concurrency_test.cpp` - -#### 文档 - -- [x] `document/HandBook/desktop/base/logger/` - 使用手册 - ---- - -## 四、使用示例 - -### 主题切换 - -```cpp -#include - -auto& manager = cf::ui::core::ThemeManager::instance(); -manager.setThemeTo("theme.material.light"); -```text - -### 动画使用 - -```cpp -#include - -auto factory = std::make_unique(theme); -auto anim = factory->getAnimation("md.animation.fadeIn"); -if (anim) { - anim->start(); -} -```text - -### DPI 转换 - -```cpp -#include - -cf::ui::base::device::CanvasUnitHelper helper(2.0); -qreal pixels = helper.dpToPx(16.0); // 16dp -> 32px -```text - -### 配置读写 - -```cpp -#include - -using namespace cf::config; - -// 注册键 -Key theme_key{.full_key = "app.theme.name", .full_description = "Application theme"}; -ConfigStore::instance().register_key(theme_key, std::string("default"), Layer::App); - -// 查询配置 -KeyView kv{.group = "app.theme", .key = "name"}; -std::string theme = ConfigStore::instance().query(kv, "default"); - -// 修改配置 -ConfigStore::instance().set(kv, std::string("dark"), Layer::App); - -// 监听变化 -ConfigStore::instance().watch("app.theme.*", - [](const Key& k, const std::any* old, const std::any* new_val, Layer layer) { - // Handle change - }); - -// 同步到磁盘 -ConfigStore::instance().sync(SyncMethod::Async); -```text - -### 日志记录 - -```cpp -#include - -using namespace cf::log; - -// 便捷日志函数 -info("Application started", "MyApp"); -warning("Configuration file not found, using defaults", "Config"); -error("Failed to load resource", "Network"); - -// 设置日志等级 -set_level(level::Debug); - -// 使用 Logger 实例 -auto& logger = Logger::instance(); -logger.log(level::Info, "Message", "Tag", std::source_location::current()); - -// 刷新 -flush(); -```bash - ---- - -## 五、其他待完成模块 - -**注**: ConfigStore 和 Logger 已完成,以下是其他可选增强功能。 - -| 模块 | 优先级 | 依赖 | 状态 | -|------|--------|------|------| -| QSSProcessor | P2 | - | 待规划 | -| VariableResolver | P2 | - | 待规划 | -| HWTier 降级逻辑 | P1 | HWTier | ✅ 已实现 (hardware_tier_policy) | -| 屏幕参数检测 | P2 | - | 待规划 | -| 模拟器注入接口 | P2 | - | 待规划 | - -#### 主题加载器 (部分完成) - -**待完成**: -- JSON主题文件解析 -- 文件系统扫描 -- 主题继承链解析 -- 循环继承检测 -- QSS变量替换 - -**建议文件路径**: -- `ui/core/theme_loader.h` (新建) -- `ui/core/qss_processor.h` (新建) - -#### 变量解析器 (部分完成) - -**待完成**: -- 点分隔路径解析 (如 `text.primary`) -- 嵌套对象解析 -- 变量继承和回退 - -**建议文件路径**: -- `ui/core/variable_resolver.h` (新建) - ---- - -## 六、文件结构总览 - -### 已实现文件 - -#### ui/ 目录 (主题、动画、DPI) - -```text -ui/ -├── core/ -│ ├── theme_manager.h # 主题管理器 (核心) -│ ├── theme.h # ICFTheme接口 -│ ├── theme_factory.h # ThemeFactory抽象接口 -│ ├── color_scheme.h # ICFColorScheme颜色方案 -│ ├── motion_spec.h # IMotionSpec动画规范 -│ ├── font_type.h # IFontType字体接口 -│ └── radius_scale.h # IRadiusScale圆角接口 -├── components/ -│ ├── animation_factory_manager.h # 动画工厂管理器 -│ ├── animation.h # ICFAbstractAnimation基础接口 -│ ├── spring_animation.h # 弹簧动画 -│ ├── timing_animation.h # 时间曲线动画 -│ └── animation_group.h # 动画组 -└── base/ - ├── device_pixel.h # DPI转换工具 - ├── color.h # CFColor (HCT颜色) - ├── color_helper.h # 颜色工具函数 - ├── easing.h # 缓动曲线 - ├── geometry_helper.h # 几何工具 - └── math_helper.h # 数学工具 -```text - -#### desktop/base/config_manager/ (配置中心) ✅ - -```text -desktop/base/config_manager/ -├── include/ -│ ├── cfconfig.hpp # 主配置存储接口 -│ ├── cfconfig_key.h # 配置键定义 -│ ├── cfconfig_layer.h # 配置层级 -│ ├── cfconfig_notify_policy.h # 通知策略 -│ └── cfconfig/ -│ ├── cfconfig_result.h # 操作结果 -│ ├── cfconfig_watcher.h # 配置监听器 -│ └── cfconfig_path_provider.h # 路径提供者 -└── src/ - ├── cfconfig.cpp # 实现 - └── impl/ - ├── config_impl.h # 内部实现 - └── config_impl.cpp -```text - -#### desktop/base/logger/ (日志系统) ✅ - -```text -desktop/base/logger/ -├── include/ -│ ├── cflog.h # 便捷日志函数 -│ └── cflog/ -│ ├── cflog.hpp # 主 Logger 类 -│ ├── cflog_level.hpp # 日志等级 -│ ├── cflog_record.h # 日志记录 -│ ├── cflog_sink.h # Sink 接口 -│ ├── cflog_format.h # 格式化接口 -│ ├── cflog_format_factory.h # 格式化工厂 -│ ├── cflog_format_config.h # 格式化配置 -│ └── cflog_format_flags.h # 格式化标志 -└── src/ - ├── logger/ - │ └── cflog.cpp # 实现 - └── impl/ - ├── cflog_impl.h # 内部实现 - └── cflog_impl.cpp -```text - -### 待创建文件 (可选增强) - -```text -ui/core/ -├── theme_loader.h # 主题加载器 (待创建) -├── qss_processor.h # QSS处理器 (待创建) -└── variable_resolver.h # 变量解析器 (待创建) -```yaml - ---- - -## 七、下一步行动建议 - -**Phase 2 核心模块已全部完成!** 以下是可选增强功能。 - -### 优先级1 (高) - 可选增强 - -1. **完善主题加载器** - - 实现JSON主题文件解析 - - 实现主题继承 - - 预计工作量: 2-3天 - -2. **完善变量解析器** - - 实现点分隔路径解析 - - 实现变量回退机制 - - 预计工作量: 1-2天 - -### 优先级2 (中) - -3. **增强DPI管理** - - 添加模拟器注入接口 - - 支持热插拔屏幕 - - 预计工作量: 1-2天 - -4. **动画性能优化** - - 实现HWTier降级逻辑 - - 添加性能测试 - - 预计工作量: 2-3天 - -### 已完成模块 - -- [x] ConfigStore 配置中心 (100%) -- [x] Logger 日志系统 (100%) -- [x] ThemeEngine 主题引擎 (60%) -- [x] AnimationManager 动画管理器 (65%) -- [x] DPI management 分辨率适配 (80%) - ---- - -## 八、相关文档 - -- 原始TODO: ~~已归档~~(原 `02_base_library.md` 已删除) -- 设计文档: `../../design_stage/02_phase2_base_library.md` - ---- - -*文档版本: v2.0* -*生成时间: 2026-03-18* diff --git a/document/todo/done/03_input_layer_status.md b/document/todo/done/03_input_layer_status.md deleted file mode 100644 index a15ac02c7..000000000 --- a/document/todo/done/03_input_layer_status.md +++ /dev/null @@ -1,541 +0,0 @@ ---- -title: "Phase 3: 输入抽象层 - 状态文档" -description: "模块ID: Phase 3,状态: **模块ID**: Phase 3 -> **状态**: 未开始 -> **总体进度**: 0% -> **最后更新**: 2026-03-11 - ---- - -# 输入抽象层参考文档 - -> **文档版本**: 1.0 -> **最后更新**: 2026-03-05 -> **完成度**: 0% -> **说明**: 完全未实现,需要从头开发 - ---- - -## 一、模块概述 - -输入抽象层是CFDesktop项目的人机交互基础设施层,负责屏蔽底层输入设备差异,统一触摸、物理按键、旋钮等输入事件,并支持焦点导航模式。 - -### 核心职责 - -1. **统一输入分发** - 集中管理所有输入设备的注册、注销和事件分发 -2. **触摸处理** - 单击/双击检测、长按检测、多点触摸支持 -3. **按键处理** - 按键状态跟踪、长按检测、连击检测 -4. **旋钮处理** - AB相位解码、方向判定、速度计算、加速功能 -5. **手势识别** - 滑动、捏合、旋转手势识别 -6. **焦点导航** - 四方向导航、自定义焦点链、边界策略 - -### 设计目标 - -- **设备无关性**: 抽象底层硬件差异,提供统一的输入接口 -- **可扩展性**: 支持动态注册新类型输入设备 -- **模拟器友好**: 支持模拟器环境下的输入注入 -- **高性能**: 事件延迟 < 16ms,CPU占用 < 5% - ---- - -## 二、当前实现状态 - -### 总体完成度: 0% - -| 模块 | 完成度 | 实现位置 | 备注 | -|------|--------|----------|------| -| InputManager | 0% | - | 未实现 | -| TouchInputHandler | 0% | - | 未实现 | -| KeyInputHandler | 0% | - | 未实现 | -| RotaryInputHandler | 0% | - | 未实现 | -| GestureRecognizer | 0% | - | 未实现 | -| FocusNavigator | 0% | - | 未实现 | -| 原生设备驱动 | 0% | - | 未实现 | - ---- - -## 三、待完成项清单 - -### 3.1 InputManager - 统一分发层 (0%) - -**职责描述**: -- 设备注册/注销管理 -- 事件分发机制 -- 事件过滤器支持 - -**建议文件路径**: -- `include/CFDesktop/Base/Input/InputManager.h` -- `include/CFDesktop/Base/Input/InputEvent.h` -- `include/CFDesktop/Base/Input/InputDevice.h` -- `src/base/input/InputManager.cpp` - -**关键接口设计**: -```cpp -namespace cf::base::input { - -// 输入设备基类 -class InputDevice { -public: - virtual ~InputDevice() = default; - virtual QString deviceId() const = 0; - virtual QString deviceName() const = 0; - virtual DeviceType deviceType() const = 0; -}; - -// 输入事件基类 -class InputEvent { -public: - virtual ~InputEvent() = default; - virtual qint64 timestamp() const = 0; - virtual InputDevice* sourceDevice() const = 0; -}; - -// 输入管理器 -class InputManager : public QObject { - Q_OBJECT -public: - static InputManager& instance(); - - // 设备管理 - void registerDevice(std::unique_ptr device); - void unregisterDevice(const QString& deviceId); - QList devices() const; - InputDevice* device(const QString& deviceId) const; - - // 事件分发 - void dispatchEvent(std::unique_ptr event); - - // 事件过滤器 - void addEventFilter(QObject* filter); - void removeEventFilter(QObject* filter); - -signals: - void deviceRegistered(InputDevice* device); - void deviceUnregistered(const QString& deviceId); - void eventReceived(InputEvent* event); -}; - -} // namespace cf::base::input -```text - -### 3.2 TouchInputHandler - 触摸处理器 (0%) - -**职责描述**: -- 触摸点跟踪 (TouchPoint结构) -- 单击/双击检测 -- 长按检测 -- 多点触摸支持 -- 压力检测 - -**建议文件路径**: -- `include/CFDesktop/Base/Input/TouchInputHandler.h` -- `src/base/input/TouchInputHandler.cpp` - -**关键接口设计**: -```cpp -namespace cf::base::input { - -struct TouchPoint { - int id; // 触摸点ID - QPointF pos; // 当前位置 - QPointF startPos; // 起始位置 - float pressure; // 压力值 - qint64 timestamp; // 时间戳 -}; - -class TouchInputHandler : public QObject { - Q_OBJECT -public: - explicit TouchInputHandler(QObject* parent = nullptr); - - // 配置参数 - void setClickThreshold(float pixels); // 点击移动阈值 - void setDoubleClickInterval(int ms); // 双击时间间隔 - void setLongPressTimeout(int ms); // 长按超时时间 - - // 触摸点管理 - void addTouchPoint(const TouchPoint& point); - void updateTouchPoint(int id, const QPointF& newPos); - void removeTouchPoint(int id); - -signals: - void clicked(const QPointF& pos); - void doubleClicked(const QPointF& pos); - void longPressed(const QPointF& pos); - void touchMoved(const QList& points); - -private: - QMap activePoints_; - float clickThreshold_ = 10.0f; - int doubleClickInterval_ = 400; - int longPressTimeout_ = 500; -}; - -} // namespace cf::base::input -```text - -### 3.3 KeyInputHandler - 按键处理器 (0%) - -**职责描述**: -- 按键状态跟踪 -- 长按检测 -- 连击检测 -- 按键配置支持 - -**建议文件路径**: -- `include/CFDesktop/Base/Input/KeyInputHandler.h` -- `src/base/input/KeyInputHandler.cpp` - -**关键接口设计**: -```cpp -namespace cf::base::input { - -struct KeyEvent { - int keyCode; // 按键代码 - bool pressed; // true=按下, false=释放 - qint64 timestamp; // 时间戳 -}; - -class KeyInputHandler : public QObject { - Q_OBJECT -public: - explicit KeyInputHandler(QObject* parent = nullptr); - - // 按键处理 - void handleKeyEvent(const KeyEvent& event); - - // 配置参数 - void setLongPressThreshold(int ms); // 长按阈值 - void setMultiClickWindow(int ms); // 连击时间窗口 - -signals: - void keyPressed(int keyCode); - void keyReleased(int keyCode); - void keyLongPressed(int keyCode); // 长按触发 - void keyMultiClicked(int keyCode, int count); // 连击 - -private: - QMap keyPressTime_; - QMap keyClickCount_; - int longPressThreshold_ = 800; - int multiClickWindow_ = 300; -}; - -} // namespace cf::base::input -```text - -### 3.4 RotaryInputHandler - 旋钮处理器 (0%) - -**职责描述**: -- AB相位解码 -- 方向判定 -- 速度计算 -- 加速功能 - -**建议文件路径**: -- `include/CFDesktop/Base/Input/RotaryInputHandler.h` -- `src/base/input/RotaryInputHandler.cpp` - -**关键接口设计**: -```cpp -namespace cf::base::input { - -enum class RotaryDirection { Clockwise, CounterClockwise }; - -class RotaryInputHandler : public QObject { - Q_OBJECT -public: - explicit RotaryInputHandler(QObject* parent = nullptr); - - // AB相位输入 - void handlePhaseA(bool state); - void handlePhaseB(bool state); - - // 配置参数 - void setStepsPerRevolution(int steps); // 每圈步数 - void setAccelerationFactor(float factor); // 加速因子 - -signals: - void rotated(RotaryDirection direction, int steps); - void positionChanged(int absolutePosition); - -private: - bool phaseA_ = false; - bool phaseB_ = false; - int position_ = 0; - QList stepTimestamps_; // 用于速度计算 - float accelerationFactor_ = 1.0f; -}; - -} // namespace cf::base::input -```text - -### 3.5 GestureRecognizer - 手势识别器 (0%) - -**职责描述**: -- 滑动手势 (Swipe) -- 捏合手势 (Pinch) -- 旋转手势 (Rotation) - -**建议文件路径**: -- `include/CFDesktop/Base/Input/GestureRecognizer.h` -- `src/base/input/GestureRecognizer.cpp` - -**关键接口设计**: -```cpp -namespace cf::base::input { - -enum class SwipeDirection { Up, Down, Left, Right }; - -class GestureRecognizer : public QObject { - Q_OBJECT -public: - explicit GestureRecognizer(QObject* parent = nullptr); - - // 触摸输入 - void handleTouchPoints(const QList& points); - - // 配置参数 - void setSwipeThreshold(float pixels); // 滑动距离阈值 - void setSwipeTimeout(int ms); // 滑动超时 - void setPinchThreshold(float ratio); // 捏合阈值 - -signals: - void swipeDetected(SwipeDirection direction); - void pinchStarted(const QPointF& center); - void pinchZoomChanged(float scale); - void pinchFinished(); - void rotationStarted(const QPointF& center); - void rotationAngleChanged(float angle); - void rotationFinished(); - -private: - QList lastTouchPositions_; - qint64 lastTouchTime_ = 0; - float swipeThreshold_ = 50.0f; - int swipeTimeout_ = 300; -}; - -} // namespace cf::base::input -```text - -### 3.6 FocusNavigator - 焦点导航器 (0%) - -**职责描述**: -- 四方向导航算法 -- 自定义焦点链 -- 边界策略 (循环/停止) - -**建议文件路径**: -- `include/CFDesktop/Base/Input/FocusNavigator.h` -- `src/base/input/FocusNavigator.cpp` - -**关键接口设计**: -```cpp -namespace cf::base::input { - -enum class NavigationDirection { Up, Down, Left, Right }; -enum class BoundaryPolicy { Stop, Wrap, Jump }; - -class FocusNavigator : public QObject { - Q_OBJECT -public: - static FocusNavigator& instance(); - - // 焦点导航 - void navigate(NavigationDirection direction); - void setFocus(QWidget* widget); - QWidget* currentFocus() const; - - // 焦点策略 - void setBoundaryPolicy(BoundaryPolicy policy); - - // 自定义焦点链 - void addFocusChain(QWidget* from, NavigationDirection dir, QWidget* to); - void removeFocusChain(QWidget* from); - void clearFocusChains(); - -signals: - void focusChanged(QWidget* oldFocus, QWidget* newFocus); - void focusLost(QWidget* widget); - -private: - QWidget* currentFocus_ = nullptr; - BoundaryPolicy boundaryPolicy_ = BoundaryPolicy::Stop; - QMultiMap> focusChains_; -}; - -} // namespace cf::base::input -```bash - ---- - -## 四、原生设备驱动 - -### 4.1 EvdevDevice - Linux事件设备 (0%) - -**职责**: 读取 `/dev/input/eventX` 设备事件 - -**建议文件路径**: -- `src/base/input/native/EvdevDevice.cpp` -- `src/base/input/native/EvdevDevice.h` - -### 4.2 GPIOButton - GPIO按键 (0%) - -**职责**: 通过sysfs接口读取GPIO按键状态 - -**建议文件路径**: -- `src/base/input/native/GPIOButton.cpp` -- `src/base/input/native/GPIOButton.h` - -### 4.3 RotaryEncoder - 旋转编码器 (0%) - -**职责**: 读取GPIO编码器状态并解码 - -**建议文件路径**: -- `src/base/input/native/RotaryEncoder.cpp` -- `src/base/input/native/RotaryEncoder.h` - -### 4.4 SimulatedInput - 模拟器输入 (0%) - -**职责**: 为模拟器环境提供输入注入接口 - -**建议文件路径**: -- `src/base/input/simulator/SimulatedInput.cpp` -- `src/base/input/simulator/SimulatedInput.h` - ---- - -## 五、完成度百分比 - -| 模块 | 原计划需求 | 已实现 | 完成度 | -|------|-----------|--------|--------| -| InputManager | 100% | 0% | 0% | -| TouchInputHandler | 100% | 0% | 0% | -| KeyInputHandler | 100% | 0% | 0% | -| RotaryInputHandler | 100% | 0% | 0% | -| GestureRecognizer | 100% | 0% | 0% | -| FocusNavigator | 100% | 0% | 0% | -| 原生设备驱动 | 100% | 0% | 0% | -| **总体** | **100%** | **0%** | **0%** | - ---- - -## 六、关键文件路径 - -### 待创建文件 - -```text -include/CFDesktop/Base/Input/ -├── InputManager.h # 统一分发层 -├── InputEvent.h # 输入事件定义 -├── InputDevice.h # 输入设备基类 -├── TouchInputHandler.h # 触摸处理器 -├── KeyInputHandler.h # 按键处理器 -├── RotaryInputHandler.h # 旋钮处理器 -├── GestureRecognizer.h # 手势识别器 -└── FocusNavigator.h # 焦点导航器 - -src/base/input/ -├── InputManager.cpp -├── TouchInputHandler.cpp -├── KeyInputHandler.cpp -├── RotaryInputHandler.cpp -├── GestureRecognizer.cpp -├── FocusNavigator.cpp -├── native/ -│ ├── EvdevDevice.h/cpp # Linux evdev设备 -│ ├── GPIOButton.h/cpp # GPIO按键 -│ └── RotaryEncoder.h/cpp # 旋转编码器 -└── simulator/ - └── SimulatedInput.h/cpp # 模拟器输入注入 - -tests/unit/base/input/ -├── test_input_manager.cpp -├── test_touch_handler.cpp -├── test_key_handler.cpp -├── test_rotary_handler.cpp -├── test_gesture_recognizer.cpp -└── test_focus_navigator.cpp -```yaml - ---- - -## 七、下一步行动建议 - -### 优先级1 (高) - 核心框架 - -1. **实现InputManager核心框架** - - 优先级: 最高 (其他模块依赖) - - 预计工作量: 2-3天 - - 交付物: 设备注册、事件分发、过滤器支持 - -2. **实现TouchInputHandler** - - 优先级: 高 (触摸屏设备) - - 预计工作量: 3-4天 - - 交付物: 触摸点跟踪、点击/长按检测 - -### 优先级2 (中) - 特定设备 - -3. **实现KeyInputHandler** - - 优先级: 中 - - 预计工作量: 2-3天 - - 交付物: 按键状态跟踪、长按/连击检测 - -4. **实现FocusNavigator** - - 优先级: 中 (遥控器导航) - - 预计工作量: 2-3天 - - 交付物: 四方向导航、边界策略 - -### 优先级3 (低) - 增强功能 - -5. **实现RotaryInputHandler** - - 优先级: 低 (特定硬件) - - 预计工作量: 2天 - -6. **实现GestureRecognizer** - - 优先级: 低 (高级交互) - - 预计工作量: 3-4天 - -7. **原生设备驱动实现** - - 优先级: 低 (真机部署时) - - 预计工作量: 4-5天 - ---- - -## 八、验收标准 - -### 功能验收 - -- [ ] 触摸输入正常响应 (单击/双击/长按) -- [ ] 手势识别准确率 > 95% -- [ ] 焦点导航无死循环 -- [ ] 原生设备正常读取 (evdev/GPIO) - -### 性能验收 - -- [ ] 事件延迟 < 16ms -- [ ] CPU占用 < 5% -- [ ] 无内存泄漏 - -### 兼容性验收 - -- [ ] 模拟器和真机行为一致 -- [ ] Linux和Windows平台兼容 - ---- - -## 九、相关文档 - -- 原始TODO: [../base/02_input_layer.md](../base/02_input_layer.md) -- 设计文档: `../../design_stage/03_phase3_input_layer.md` -- 依赖模块: [Base库状态](./02_base_library_status.md) - ---- - -*最后更新: 2026-03-05* diff --git a/document/todo/done/04_simulator_status.md b/document/todo/done/04_simulator_status.md deleted file mode 100644 index 285664dbc..000000000 --- a/document/todo/done/04_simulator_status.md +++ /dev/null @@ -1,232 +0,0 @@ ---- -title: "Phase 4: 多平台模拟器 - 状态文档" -description: "模块ID: Phase 4,状态: **模块ID**: Phase 4 -> **状态**: 未开始 -> **总体进度**: 0% -> **最后更新**: 2026-03-11 - ---- - -# 04 - Multi-Platform Simulator Reference - -## Module Overview - -The Multi-Platform Simulator module provides cross-platform device simulation capabilities for CFDesktop, enabling developers to test the application on various device configurations without physical hardware. This module is essential for embedded device development and cross-platform UI testing. - -**Module Purpose:** -- Simulate different hardware configurations (memory, CPU, display) -- Provide device frame visualization (phone, tablet, embedded terminal) -- Enable touch input visualization for touch-enabled interfaces -- Support hardware tier selection for performance testing -- DPI injection for testing different screen densities - ---- - -## Current Implementation Status - -**Completion: 0%** - -This module is currently **NOT IMPLEMENTED**. No files exist for the simulator functionality. - ---- - -## Required Components - -### 1. SimulatorWindow (Main Window) -**Status:** Not Implemented - -**Purpose:** Main simulator window hosting device frames and controls. - -**Required Features:** -- Device frame container -- Configuration panel (device type, resolution, DPI) -- Runtime controls (start, stop, reset) -- Screenshot capture -- Performance monitoring overlay - -**Key Files to Create:** -- `ui/simulator/simulator_window.h` -- `ui/simulator/simulator_window.cpp` - ---- - -### 2. DeviceFrame (Device Shell) -**Status:** Not Implemented - -**Purpose:** Visual frame representing physical device外观. - -**Required Features:** -- Predefined device frames (phone, tablet, embedded terminal) -- Custom frame support -- Screen area clipping -- Device-specific bezels and buttons -- Status bar simulation - -**Key Files to Create:** -- `ui/simulator/device/device_frame.h` -- `ui/simulator/device/device_frame.cpp` -- `ui/simulator/device/frame_presets.h` (preset definitions) - ---- - -### 3. TouchVisualizer -**Status:** Not Implemented - -**Purpose:** Visualize touch events for debugging touch interfaces. - -**Required Features:** -- Touch point rendering (circles at touch locations) -- Multi-touch support (up to 10 points) -- Touch path/trail visualization -- Tap duration indication -- Gesture recognition display - -**Key Files to Create:** -- `ui/simulator/visualizer/touch_visualizer.h` -- `ui/simulator/visualizer/touch_visualizer.cpp` - ---- - -### 4. HWTierSelector (Hardware Tier Selector) -**Status:** Not Implemented - -**Purpose:** Select hardware performance tiers for testing. - -**Required Features:** -- Tier presets (Low, Medium, High, Ultra) -- Custom hardware configuration -- Memory limit simulation -- CPU throttling simulation -- Storage speed simulation - -**Key Files to Create:** -- `ui/simulator/config/hw_tier_selector.h` -- `ui/simulator/config/hw_tier_selector.cpp` -- `ui/simulator/config/hw_profile.h` (profile definitions) - ---- - -### 5. DPI Injector -**Status:** Not Implemented - -**Purpose:** Inject custom DPI values for testing high/low density displays. - -**Required Features:** -- DPI value override -- Scaling factor simulation -- Font DPI injection -- Coordinate system transformation - -**Key Files to Create:** -- `ui/simulator/injector/dpi_injector.h` -- `ui/simulator/injector/dpi_injector.cpp` - ---- - -### 6. Hardware Mock -**Status:** Not Implemented - -**Purpose:** Mock hardware APIs for embedded simulation. - -**Required Features:** -- Memory query mock -- CPU info mock -- Storage mock -- Network condition simulation -- Sensor mock (accelerometer, gyroscope) - -**Key Files to Create:** -- `ui/simulator/mock/hardware_mock.h` -- `ui/simulator/mock/hardware_mock.cpp` -- `ui/simulator/mock/system_info_mock.h` - ---- - -## Implementation Priority - -### Phase 1 - Core Structure (P0) -1. **SimulatorWindow** - Basic window with container -2. **DeviceFrame** - Simple frame with screen area -3. **HWTierSelector** - Basic tier selection UI - -### Phase 2 - Visualization (P1) -4. **TouchVisualizer** - Touch event rendering -5. **DPI Injector** - DPI override capability - -### Phase 3 - Advanced Features (P2) -6. **HardwareMock** - Full hardware API mocking -7. **Performance Profiling** - FPS, memory monitoring -8. **Screenshot/Recording** - Capture capabilities - ---- - -## Key File Paths - -| Component | Header Path | Source Path | -|-----------|-------------|-------------| -| SimulatorWindow | `ui/simulator/simulator_window.h` | `ui/simulator/simulator_window.cpp` | -| DeviceFrame | `ui/simulator/device/device_frame.h` | `ui/simulator/device/device_frame.cpp` | -| TouchVisualizer | `ui/simulator/visualizer/touch_visualizer.h` | `ui/simulator/visualizer/touch_visualizer.cpp` | -| HWTierSelector | `ui/simulator/config/hw_tier_selector.h` | `ui/simulator/config/hw_tier_selector.cpp` | -| DPI Injector | `ui/simulator/injector/dpi_injector.h` | `ui/simulator/injector/dpi_injector.cpp` | -| HardwareMock | `ui/simulator/mock/hardware_mock.h` | `ui/simulator/mock/hardware_mock.cpp` | - ---- - -## Design Considerations - -### Qt Integration -- Use `QWidget` as base for all simulator components -- Leverage `QGraphicsView` for device frame rendering -- Use `QTouchEvent` for touch simulation - -### Platform Support -- Windows: Full support -- Linux: Full support -- macOS: Full support (with limitations on touch) - -### Performance -- Simulator should not impact application performance significantly -- Mock operations should be lightweight -- Visualization should use hardware acceleration when available - ---- - -## Next Steps - -1. **Create directory structure:** - ``` - ui/simulator/ - ├── simulator_window.h/cpp - ├── device/ - ├── visualizer/ - ├── config/ - ├── injector/ - └── mock/ - ``` - -2. **Implement SimulatorWindow** - Start with basic container widget - -3. **Add CMakeLists.txt entries** for new simulator module - -4. **Integrate with existing Application class** - Add simulator launcher - -5. **Write unit tests** for each component - ---- - -## Related Documents - -- `01_architecture_ref.md` - Overall architecture -- `05_testing_ref.md` - Testing infrastructure -- `10_ui_embedding_ref.md` - UI embedding for embedded systems - ---- - -*Last Updated: 2026-03-05* -*Status: Not Implemented* diff --git a/document/todo/done/05_testing_status.md b/document/todo/done/05_testing_status.md deleted file mode 100644 index 43138b1b1..000000000 --- a/document/todo/done/05_testing_status.md +++ /dev/null @@ -1,303 +0,0 @@ ---- -title: "Phase 5: 测试系统 - 状态文档" -description: "模块ID: Phase 5,状态: 🚧 基础框架完成,UI控件测试缺失" ---- - -# Phase 5: 测试系统 - 状态文档 - -> **模块ID**: Phase 5 -> **状态**: 🚧 基础框架完成,UI控件测试缺失 -> **总体进度**: ~55% -> **最后更新**: 2026-03-27 - ---- - -## 一、模块概述 - -测试系统负责为 CFDesktop 项目提供完整的单元测试、集成测试和性能测试框架。 - -**模块职责:** -- 提供核心工具库的单元测试框架 -- 支持 UI 组件测试 -- 支持系统信息查询测试 -- 支持启动序列测试 - ---- - -## 二、完成状态总览 - -| 模块 | 测试文件数 | 覆盖率 | 状态 | -|------|-----------:|-------:|------| -| Base Utilities | 6 | 90% | ✅ Good | -| UI Base | 6 | 60% | ⚠️ Fair | -| UI Components | 5 | 60% | ⚠️ Fair | -| UI Core | 1 | 30% | ❌ Poor | -| UI Widgets | 0 | **0%** | ❌ **Critical** | -| System Queries | 2 | 70% | ⚠️ Fair | -| Logger | 4 | 85% | ✅ Good | -| ConfigManager | 1 | 80% | ✅ Good | -| Desktop | 1 | 5% | ❌ Poor | -| **Overall** | **26** | **55%** | ⚠️ **Fair** | - ---- - -## 三、已完成工作 - -### 3.1 测试基础框架 (100%) - -#### Google Test 配置 -- Google Test v1.14.0 集成 -- CMake `setup_third_party` 自动下载 -- Windows 特殊配置 (gtest_force_shared_crt ON) -- CTest 集成 -- 自定义 `add_gtest_executable` 辅助函数 - -#### 测试运行脚本 -- `run_all_tests.ps1` (Windows PowerShell) -- `run_all_tests.sh` (Linux/macOS Bash) - ---- - -### 3.2 测试目录结构 - -```text -test/ -├── CMakeLists.txt # 测试构建配置 -├── base/ # 基础工具库测试 -│ ├── weak_ptr/ -│ │ └── weak_ptr_test.cpp # WeakPtr 实现测试 -│ ├── expected/ -│ │ └── expected_test.cpp # Expected/Result 类型测试 -│ ├── hash/ -│ │ └── constexpr_fnv1a_test.cpp # 编译时哈希测试 -│ └── scope_guard/ -│ └── scope_guard_test.cpp # RAII 作用域守卫测试 -├── ui/ # UI 层测试 -│ ├── base/ -│ │ ├── device_pixel_test.cpp # 设备像素转换测试 -│ │ ├── easing_test.cpp # 缓动曲线测试 -│ │ ├── math_helper_test.cpp # 数学助手测试 (全面) -│ │ ├── color_test.cpp # 颜色工具测试 -│ │ ├── color_helper_test.cpp # 颜色助手测试 -│ │ └── geometry_helper_test.cpp # 几何助手测试 -│ ├── components/ -│ │ ├── elevation_controller_test.cpp # 高度阴影测试 -│ │ ├── focus_ring_test.cpp # 焦点指示器测试 -│ │ ├── painter_layer_test.cpp # 绘制层测试 -│ │ ├── ripple_helper_test.cpp # 涟漪效果测试 -│ │ └── state_machine_test.cpp # 状态机测试 -│ └── core/ -│ └── token_test.cpp # 主题 Token 测试 -├── system/ # 系统查询测试 -│ ├── test_memory_info_query.cpp # 内存信息查询测试 -│ └── test_cpu_info_query.cpp # CPU 信息查询测试 -└── boot_test/ # 启动序列测试 - ├── boot_detect.cpp # 启动检测 - ├── boot_test_core.cpp # 核心启动测试 - ├── boot_test_gui.cpp # GUI 启动测试 - ├── CMakeLists.txt - └── README.md # 启动测试文档 -```bash - ---- - -### 3.3 已实现测试清单 - -#### Base Utilities 测试 - -| 测试文件 | 路径 | 覆盖内容 | -|---------|------|---------| -| **expected_test.cpp** | `test/base/expected/expected_test.cpp` | `cf::expected` Result 类型:构造/销毁、值和错误访问、赋值、单子操作 (and_then, or_else, transform)、比较、交换、异常处理、移动语义、const 正确性 | -| **scope_guard_test.cpp** | `test/base/scope_guard/scope_guard_test.cpp` | RAII 作用域守卫基本功能 | -| **constexpr_fnv1a_test.cpp** | `test/base/hash/constexpr_fnv1a_test.cpp` | FNV1a 编译时哈希、抗冲突性 | -| **weak_ptr_test.cpp** | `test/base/weak_ptr/weak_ptr_test.cpp` | `cf::WeakPtr` 所有权语义、生命周期管理 | - -#### UI Base 测试 - -| 测试文件 | 路径 | 覆盖内容 | -|---------|------|---------| -| **math_helper_test.cpp** | `test/ui/base/math_helper_test.cpp` | `lerp` (7), `clamp` (6), `remap` (8), `cubicBezier` (7), `springStep` (7), `lerpAngle` (10) 测试用例 | -| **device_pixel_test.cpp** | `test/ui/base/device_pixel_test.cpp` | 设备像素比转换、DP 到像素转换 | -| **easing_test.cpp** | `test/ui/base/easing_test.cpp` | QEasingCurve 包装器、Material 缓动曲线验证 | -| **color_test.cpp** | `test/ui/base/color_test.cpp` | 颜色空间转换、Material 颜色系统、对比度计算 | -| **color_helper_test.cpp** | `test/ui/base/color_helper_test.cpp` | 颜色辅助工具 | -| **geometry_helper_test.cpp** | `test/ui/base/geometry_helper_test.cpp` | 圆角矩形路径生成、矩形裁剪 | - -#### UI Components 测试 - -| 测试文件 | 路径 | 覆盖内容 | -|---------|------|---------| -| **state_machine_test.cpp** | `test/ui/components/state_machine_test.cpp` | Material 行为层:状态转换 (8)、组合状态 (2)、透明度计算 (6)、状态优先级 (2) | -| **ripple_helper_test.cpp** | `test/ui/components/ripple_helper_test.cpp` | 涟漪创建和生命周期、多涟漪支持、淡入动画 | -| **elevation_controller_test.cpp** | `test/ui/components/elevation_controller_test.cpp` | 阴影渲染、高度级别验证 (0-5)、暗主题色调覆盖 | -| **focus_ring_test.cpp** | `test/ui/components/focus_ring_test.cpp` | 焦点环渲染、Material 规范合规 (3dp 宽度, 3dp 内边距) | -| **painter_layer_test.cpp** | `test/ui/components/painter_layer_test.cpp` | 颜色覆盖层、Alpha 混合验证 | - -#### UI Core 测试 - -| 测试文件 | 路径 | 覆盖内容 | -|---------|------|---------| -| **token_test.cpp** | `test/ui/core/token_test.cpp` | Token 系统验证、颜色方案 Token、排版 Token | - -#### System 测试 - -| 测试文件 | 路径 | 覆盖内容 | -|---------|------|---------| -| **test_memory_info_query.cpp** | `test/system/test_memory_info_query.cpp` | 内存信息查询、跨平台内存 API 验证 | -| **test_cpu_info_query.cpp** | `test/system/test_cpu_info_query.cpp` | CPU 信息查询、处理器计数检测 | - ---- - -## 四、待完成模块 - -### 4.1 Widget 测试 (P0 - Critical) - -**状态**: 完全未实现 (0%) - -**关键缺口**: 19个P0/P1控件全部无单元测试 - -**P0 核心控件** (7个): -- `button_test.cpp` - Button 控件测试 -- `textfield_test.cpp` - TextField 控件测试 -- `textarea_test.cpp` - TextArea 控件测试 -- `checkbox_test.cpp` - CheckBox 控件测试 -- `radiobutton_test.cpp` - RadioButton 控件测试 -- `label_test.cpp` - Label 控件测试 -- `groupbox_test.cpp` - GroupBox 控件测试 - -**P1 常用控件** (12个): -- `slider_test.cpp` - Slider 控件测试 -- `progressbar_test.cpp` - ProgressBar 控件测试 -- `switch_test.cpp` - Switch 控件测试 -- `combobox_test.cpp` - ComboBox 控件测试 -- `spinbox_test.cpp` - SpinBox 控件测试 -- `doublespinbox_test.cpp` - DoubleSpinBox 控件测试 -- `listview_test.cpp` - ListView 控件测试 -- `treeview_test.cpp` - TreeView 控件测试 -- `tableview_test.cpp` - TableView 控件测试 -- `tabview_test.cpp` - TabView 控件测试 -- `scrollview_test.cpp` - ScrollView 控件测试 -- `separator_test.cpp` - Separator 控件测试 - -**目标路径**: `test/ui/widgets/` - -### 4.2 Animation 测试 (P1) - -**状态**: 未实现 - -必需的测试文件: -- `timing_animation_test.cpp` - 时间动画测试 -- `spring_animation_test.cpp` - 弹簧动画测试 -- `animation_group_test.cpp` - 动画组测试 -- `animation_factory_test.cpp` - 动画工厂测试 - -**目标路径**: `test/ui/animation/` - -### 4.3 Theme 测试 (P1) - -**状态**: 部分 (token_test.cpp 已存在) - -缺失的测试: -- `theme_manager_test.cpp` - 主题管理器测试 -- `color_scheme_test.cpp` - 颜色方案测试 -- `motion_spec_test.cpp` - 动画规格测试 - -**目标路径**: `test/ui/theme/` - -### 4.4 集成/UI自动化/性能测试 (P2) - -**状态**: 未实现 - -**集成测试**: -- `base_integration_test.cpp` - Base 库集成测试 -- `sdk_integration_test.cpp` - SDK 集成测试 -- `shell_integration_test.cpp` - Shell 集成测试 - -**UI 自动化测试**: -- 鼠标事件模拟 -- 键盘事件模拟 -- 触摸事件模拟 -- 截图对比测试 - -**性能基准测试**: -- `widget_benchmark.cpp` - 控件渲染性能 -- `animation_benchmark.cpp` - 动画帧率测试 -- `memory_benchmark.cpp` - 内存使用分析 - ---- - -## 五、测试金字塔 - -```text - /\ - / \ - / E2E\ (端到端测试 - 少量) - /------\ - / 集成 \ (集成测试 - 适量) - /--------\ - / UI 自动化 \ (UI 自动化 - 中等) - /------------\ - / 单元测试 \ (单元测试 - 大量,已完成 ~60%) - /________________\ -```yaml - ---- - -## 六、运行测试 - -### 构建测试 - -```bash -# 从项目根目录 -cmake -B out/build_test -DBUILD_TESTING=ON -cmake --build out/build_test -```text - -### 运行所有测试 - -```bash -./out/build_test/bin/cf_desktop_tests -```text - -### 运行特定测试 - -```bash -# 运行特定测试套件 -./out/build_test/bin/cf_desktop_tests --gtest_filter=MathHelperTest.* - -# 运行特定测试用例 -./out/build_test/bin/cf_desktop_tests --gtest_filter=MathHelperTest.Lerp* -```text - -### 使用脚本运行 - -**Windows**: -```powershell -.\scripts\run_all_tests.ps1 -```text - -**Linux/macOS**: -```bash -./scripts/run_all_tests.sh -```yaml - ---- - -## 七、下一步行动建议 - -1. **[P0] 添加 Widget 测试** - 为所有 P0 Material 控件创建单元测试 -2. **[P1] 添加 Animation 测试** - 测试时间/弹簧动画和动画组 -3. **[P1] 完善 Theme 测试** - 补充主题管理器和颜色方案测试 -4. **[P2] 创建 Mock 框架** - 用于硬件抽象层测试 -5. **[P2] 添加 UI 自动化** - 鼠标/键盘/触摸模拟测试 -6. **[P2] 性能基准测试** - 建立基线性能指标 -7. **[P2] CI 集成** - 将自动化测试添加到 CI/CD 流程 - ---- - -## 八、相关文档 - ---- - -*文档版本: v1.0* -*生成时间: 2026-03-11* diff --git a/document/todo/done/06_infrastructure_status.md b/document/todo/done/06_infrastructure_status.md deleted file mode 100644 index 6388e93b4..000000000 --- a/document/todo/done/06_infrastructure_status.md +++ /dev/null @@ -1,170 +0,0 @@ ---- -title: "Phase A: 基础设施部分完成状态" -description: "状态: ✅ 部分完成,总体进度: ~50%" ---- - -# Phase A: 基础设施部分完成状态 - -> **状态**: ✅ 部分完成 -> **总体进度**: ~50% -> **最后更新**: 2026-03-21 - ---- - -## 一、已完成模块 - -### 1.1 GPU 检测器 (100%) - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `base/include/system/gpu/gpu.h` | GPU/显示信息接口 | -| `base/system/gpu/gpu.cpp` | 跨平台实现 | -| `base/system/gpu/private/linux_impl/gpu_info.h` | Linux 接口 | -| `base/system/gpu/private/linux_impl/gpu_info.cpp` | Linux 实现 | -| `base/system/gpu/private/win_impl/gpu_info.h` | Windows 接口 | -| `base/system/gpu/private/win_impl/gpu_info.cpp` | Windows 实现 | - -#### 功能实现 - -- [x] GPU 信息检测 (名称、厂商、驱动版本) -- [x] 显示器信息检测 (分辨率、刷新率、DPI) -- [x] 环境评分算法 (GPU 50分 + 显示 50分) -- [x] 档位判定 (Low < 45, Mid 45-74, High ≥ 75) -- [x] Linux DRM 设备检测 -- [x] Linux DeviceTree SoC 检测 -- [x] WSL2 GPU 探测 (/dev/dxg) -- [x] Windows DXGI 检测 - -#### 示例 - -- 示例文件: `example/base/system/example_gpu_info.cpp` - ---- - -### 1.2 Network 检测器 (100%) - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `base/include/system/network/network.h` | 网络信息接口 | -| `base/system/network/network.cpp` | 基于 Qt 的跨平台实现 | - -#### 功能实现 - -- [x] IpAddress 结构体 (IPv4/IPv6) -- [x] InterfaceInfo 结构体 (网卡信息) -- [x] AddressEntry 结构体 (IP地址条目) -- [x] InterfaceFlags 结构体 (接口标志) -- [x] NetworkStatus 结构体 (网络状态) -- [x] Reachability 枚举 (网络可达性) -- [x] TransportMedium 枚举 (传输介质) -- [x] DnsEligibility 枚举 (DNS可达性) -- [x] getNetworkInfo() 函数 -- [x] interfaceTypeName() 函数 - -#### 示例 - -- 示例文件: `example/base/system/example_network_info.cpp` - ---- - -### 1.3 ConfigStore 配置中心 (100%) - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/base/config_manager/src/cfconfig.cpp` | 主要实现 | -| `desktop/base/config_manager/include/cfconfig_layer.h` | 层级定义 | -| `desktop/base/config_manager/include/cfconfig/cfconfig_watcher.h` | 监听器定义 | -| `desktop/base/config_manager/include/cfconfig/cfconfig_path_provider.h` | 路径提供者 | - -#### 功能实现 - -- [x] 四层存储模型 (Temp > App > User > System) -- [x] 配置变更监听 (ConfigWatcher) -- [x] 通配符模式匹配 -- [x] 两种通知策略 (Immediate/Manual) -- [x] INI 格式持久化 (使用 QSettings) -- [x] 线程安全 (使用 shared_mutex) -- [x] 类型安全查询 (int, double, bool, string, QVariant) - -**注意**: 使用 INI 格式而非设计文档中的 JSON 格式 - -#### 配置文件路径 - -- System: `{app_dir}/system.ini`(CFDesktop 自管理目录内) -- User: `~/.config/cfdesktop/user.ini` -- App: `config/app.ini` - ---- - -### 1.4 Logger 日志系统 (100%) - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/base/logger/include/cflog/cflog_sink.h` | Sink 接口 | -| `desktop/base/logger/include/cflog/sinks/console_sink.h` | 控制台输出 | -| `desktop/base/logger/include/cflog/sinks/file_sink.h` | 文件输出 | -| `desktop/base/logger/include/cflog.h` | CF_LOG 宏系列 | - -#### 功能实现 - -- [x] 单例模式 Logger -- [x] 异步日志 (无锁 MPSC 队列) -- [x] ConsoleSink (stdout 输出) -- [x] FileSink (文件输出,支持追加/截断) -- [x] 多种日志级别 (TRACE, DEBUG, INFO, WARNING, ERROR) -- [x] CF_LOG 宏系列 (使用 C++ 模板) -- [x] 格式化日志 (tracef, debugf, infof 等) -- [x] 标签支持 -- [x] 自动捕获源码位置 - -#### 示例 - -- 示例文件: `example/desktop/base/logger/logger_init.cpp` - ---- - -## 二、待完成模块 - -### 2.1 HWTier 核心框架 (0%) - -- [ ] `HWTier` 枚举定义 (Low/Mid/High) -- [ ] `HardwareProbe` 主类 -- [ ] `HardwareInfo` 结构体 -- [ ] `CapabilityPolicy` 策略引擎 -- [ ] 档位覆写机制 - -### 2.2 CrashHandler 崩溃处理 (0%) - -- [ ] CrashHandler 类 -- [ ] CrashReport 结构 -- [ ] CrashReporter 弹窗程序 -- [ ] Watchdog 守护进程 -- [ ] 信号捕获 (SIGSEGV, SIGABRT 等) -- [ ] 堆栈回溯 - -### 2.3 IPC 进程间通信 (0%) - -- [ ] IPCMessage 消息格式 -- [ ] IPCClient 客户端 -- [ ] IPCServer 服务器 -- [ ] ServiceLocator 服务定位器 - ---- - -## 三、相关文档 - -- 原始 TODO: [`../desktop/06_infrastructure.md`](../desktop/06_infrastructure.md) -- GPU/Network 状态: [`./01_hardware_probe_status.md`](./01_hardware_probe_status.md) -- Logger 文档: (见 desktop/base/logger/) - ---- - -*最后更新: 2026-03-21* diff --git a/document/todo/done/13_widget_apps_status.md b/document/todo/done/13_widget_apps_status.md deleted file mode 100644 index ab974e48e..000000000 --- a/document/todo/done/13_widget_apps_status.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -title: "Phase G: Widget 应用与示例程序状态" -description: "完成日期: 2026-03-21,总体进度: 100%" ---- - -# Phase G: Widget 应用与示例程序状态 - -> **状态**: ✅ 已完成 -> **完成日期**: 2026-03-21 -> **总体进度**: 100% - ---- - -## 一、应用框架 - -### 1.1 Application 基础框架 - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `ui/widget/application_support/application.h` | 基础应用框架 | -| `ui/widget/application_support/application.cpp` | 实现文件 | - -#### 功能实现 - -- [x] 继承自 QApplication -- [x] 主题管理集成 -- [x] 动画管理器集成 -- [x] Token-based API 支持 -- [x] 全局访问接口 - ---- - -### 1.2 MaterialApplication 框架 - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `ui/widget/material/application/material_application.h` | Material 应用框架 | -| `ui/widget/material/application/material_application.cpp` | 实现文件 | - -#### 功能实现 - -- [x] 预配置 Material Design 3 主题 -- [x] 自动注册亮色和暗色主题 -- [x] 完整的 Material Design 组件支持 - ---- - -## 二、Material Widget Gallery - -### 2.1 主程序 - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `example/ui/widget/material_example/main.cpp` | 主入口 | -| `example/ui/widget/material_example/MainWindow.h` | 主窗口 | -| `example/ui/widget/material_example/MainWindow.cpp` | 主窗口实现 | - -#### 功能特性 - -- [x] 画廊形式展示 (左侧导航,右侧展示) -- [x] 实时预览和交互 -- [x] 每个控件独立演示页面 -- [x] 统一的 Material Design 风格 - ---- - -### 2.2 已实现的 Demo 程序 (19个) - -| # | Demo 名称 | 头文件 | 实现文件 | -|---|----------|--------|----------| -| 1 | ButtonDemo | `demos/ButtonDemo.h` | `demos/ButtonDemo.cpp` | -| 2 | CheckBoxDemo | `demos/CheckBoxDemo.h` | `demos/CheckBoxDemo.cpp` | -| 3 | ComboBoxDemo | `demos/ComboBoxDemo.h` | `demos/ComboBoxDemo.cpp` | -| 4 | DoubleSpinBoxDemo | `demos/DoubleSpinBoxDemo.h` | `demos/DoubleSpinBoxDemo.cpp` | -| 5 | GroupBoxDemo | `demos/GroupBoxDemo.h` | `demos/GroupBoxDemo.cpp` | -| 6 | LabelDemo | `demos/LabelDemo.h` | `demos/LabelDemo.cpp` | -| 7 | ListViewDemo | `demos/ListViewDemo.h` | `demos/ListViewDemo.cpp` | -| 8 | ProgressBarDemo | `demos/ProgressBarDemo.h` | `demos/ProgressBarDemo.cpp` | -| 9 | RadioButtonDemo | `demos/RadioButtonDemo.h` | `demos/RadioButtonDemo.cpp` | -| 10 | ScrollViewDemo | `demos/ScrollViewDemo.h` | `demos/ScrollViewDemo.cpp` | -| 11 | SeparatorDemo | `demos/SeparatorDemo.h` | `demos/SeparatorDemo.cpp` | -| 12 | SliderDemo | `demos/SliderDemo.h` | `demos/SliderDemo.cpp` | -| 13 | SpinBoxDemo | `demos/SpinBoxDemo.h` | `demos/SpinBoxDemo.cpp` | -| 14 | SwitchDemo | `demos/SwitchDemo.h` | `demos/SwitchDemo.cpp` | -| 15 | TabViewDemo | `demos/TabViewDemo.h` | `demos/TabViewDemo.cpp` | -| 16 | TableViewDemo | `demos/TableViewDemo.h` | `demos/TableViewDemo.cpp` | -| 17 | TextAreaDemo | `demos/TextAreaDemo.h` | `demos/TextAreaDemo.cpp` | -| 18 | TextFieldDemo | `demos/TextFieldDemo.h` | `demos/TextFieldDemo.cpp` | -| 19 | TreeViewDemo | `demos/TreeViewDemo.h` | `demos/TreeViewDemo.cpp` | - ---- - -## 三、已实现的 Material Design 组件 - -### P0 核心控件 (7/7) ✅ - -1. **Button** - 按钮 (5种变体: Filled, Tonal, Outlined, Text, Elevated) -2. **TextField** - 单行文本输入 -3. **TextArea** - 多行文本编辑器 -4. **Label** - 显示文本或图像 -5. **CheckBox** - 二态选择控件 -6. **RadioButton** - 互斥选择控件 -7. **GroupBox** - 容器/分组框 - -### P1 控件 (12/12) ✅ - -1. **ComboBox** - 下拉列表 -2. **Slider** - 滑块 -3. **ProgressBar** - 进度条 -4. **Switch** - 开关 -5. **ListView** - 列表视图 -6. **TreeView** - 树视图 -7. **TableView** - 表格视图 -8. **TabView** - 标签页 -9. **ScrollView** - 滚动区域 -10. **Separator** - 分隔线 -11. **SpinBox** - 整数微调框 -12. **DoubleSpinBox** - 浮点微调框 - ---- - -## 四、其他示例程序 - -### GUI 相关示例 - -- Material Color Scheme Gallery -- Material Typography Gallery -- Material Motion Spec Gallery -- Material Radius Scale Gallery -- Theme Gallery -- Toast 演示 - -### 系统示例 - -- CPU 信息示例 -- 内存信息示例 -- GPU 信息示例 -- 网络信息示例 - ---- - -## 五、相关文档 - -- 原始 TODO: [`../desktop/13_widget_apps.md`](../desktop/13_widget_apps.md) -- UI 框架状态: [99_ui_material_framework_status](./99_ui_material_framework_status.md) -- P1 控件状态: [13_widget_apps](../desktop/13_widget_apps.md) - ---- - -*最后更新: 2026-03-21* diff --git a/document/todo/done/14_display_backend_status.md b/document/todo/done/14_display_backend_status.md deleted file mode 100644 index c3251f27e..000000000 --- a/document/todo/done/14_display_backend_status.md +++ /dev/null @@ -1,353 +0,0 @@ ---- -title: "Phase H: 显示后端与窗口管理系统状态" -description: "状态: ✅ 核心完成,完成日期: 2026-03-30" ---- - -# Phase H: 显示后端与窗口管理系统状态 - -> **状态**: ✅ 核心完成 -> **完成日期**: 2026-03-30 -> **总体进度**: ~70% (两个平台后端已完成,Wayland/嵌入式待实现) - ---- - -## 一、显示服务器抽象架构 - -### 1.1 IDisplayServerBackend 接口 - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/components/IDisplayServerBackend.h` | 显示服务器后端抽象接口 | -| `desktop/ui/components/IDisplayServerBackend.cpp` | 接口实现 | - -#### 功能实现 - -- [x] 三种运行模式定义 (`DisplayServerRole`) - - Client: 作为应用运行在已有桌面环境中 - - Compositor: CFDesktop 自己就是显示服务器 - - DirectRender: 直接渲染到帧缓冲 (EGLFS/linuxfb) -- [x] 能力描述 (`DisplayServerCapabilities`) - - `canManageExternalWindows` — 外部窗口管理能力 - - `needsOwnCompositor` — 是否需要独立合成器 - - `supportsWaylandProtocol` / `supportsX11Protocol` — 协议支持 -- [x] 生命周期方法: `initialize()` / `shutdown()` / `runEventLoop()` -- [x] 窗口后端访问: `windowBackend()` -- [x] 多输出支持: `outputs()` (多显示器 QRect 列表) -- [x] 信号: `outputChanged()`, `externalWindowAppeared()`, `externalWindowDisappeared()` - ---- - -### 1.2 IWindowBackend 接口 - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/components/IWindowBackend.h` | 窗口后端抽象接口 | -| `desktop/ui/components/IWindowBackend.cpp` | 接口实现 | - -#### 功能实现 - -- [x] 窗口创建/销毁: `createWindow()` / `destroyWindow()` -- [x] 窗口列表查询: `windows()` -- [x] 能力查询: `capabilities()` -- [x] 信号: `window_came` / `window_gone` - ---- - -### 1.3 IWindow 接口 - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/components/IWindow.h` | 平台无关窗口接口 | -| `desktop/ui/components/IWindow.cpp` | 接口实现 | - -#### 功能实现 - -- [x] 窗口标识: `windowID()` (win_id_t = QString) -- [x] 窗口属性: `title()`, `geometry()` -- [x] 窗口操作: `set_geometry()`, `requestClose()`, `raise()` -- [x] 信号: `closeRequested()`, `titleChanged()`, `geometryChanged()` - ---- - -## 二、Windows 平台后端 - -### 2.1 WindowsDisplayServerBackend - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/platform/windows/windows_display_server_backend.h` | Windows 显示服务器后端 | -| `desktop/ui/platform/windows/windows_display_server_backend.cpp` | 实现 | - -#### 功能实现 - -- [x] 角色: Compositor (伪桌面模式) -- [x] Win32 事件钩子 (`SetWinEventHook`) 追踪第三方窗口 -- [x] `EnumWindows` 扫描已有窗口 -- [x] 能力: - - `canManageExternalWindows = true` - - `needsOwnCompositor = false` (Windows DWM 处理) - - 硬件加速、透明度、多窗口、VSync、截图 - -### 2.2 WindowsWindowBackend - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/platform/windows/windows_window_backend.h` | Windows 窗口后端 | -| `desktop/ui/platform/windows/windows_window_backend.cpp` | 实现 | - -#### 功能实现 - -- [x] `SetWinEventHook` + `WINEVENT_OUTOFCONTEXT` 事件追踪 -- [x] `EnumWindows` 初始扫描 -- [x] 事件处理: `EVENT_OBJECT_CREATE` / `SHOW` / `DESTROY` -- [x] 智能窗口过滤 (`shouldTrackWindow()`): - - 必须可见、顶层、有标题 - - 跳过自身进程窗口 - - 过滤工具窗口、桌面、任务栏、IME 窗口 - -### 2.3 WindowsWindow - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/platform/windows/windows_window.h` | Win32 HWND 窗口包装 | -| `desktop/ui/platform/windows/windows_window.cpp` | 实现 | - -#### 功能实现 - -- [x] 适配 Win32 HWND → IWindow 接口 -- [x] 标题: `GetWindowTextLengthW` / `GetWindowTextW` -- [x] 几何: `GetWindowRect` / `SetWindowPos` -- [x] 关闭: `PostMessageW(WM_CLOSE)` -- [x] 置顶: `SetForegroundWindow` / `BringWindowToTop` - ---- - -## 三、Linux/WSL X11 平台后端 - -### 3.1 WSLX11DisplayServerBackend - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/platform/linux_wsl/wsl_x11_display_server_backend.h` | X11 显示服务器后端 | -| `desktop/ui/platform/linux_wsl/wsl_x11_display_server_backend.cpp` | 实现 | - -#### 功能实现 - -- [x] 角色: Compositor (通过 X11 追踪实现伪合成器) -- [x] XCB 连接管理 (`xcb_connect`) -- [x] 根窗口监控 -- [x] 条件编译: `CFDESKTOP_HAS_XCB` - -### 3.2 WSLX11WindowBackend - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/platform/linux_wsl/wsl_x11_window_backend.h` | X11 窗口后端 | -| `desktop/ui/platform/linux_wsl/wsl_x11_window_backend.cpp` | 实现 | - -#### 功能实现 - -- [x] XCB 协议通信 -- [x] `SubstructureNotifyMask` 根窗口事件监控 -- [x] `QSocketNotifier` 集成 XCB 事件到 Qt 事件循环 -- [x] 事件处理: - - `XCB_CREATE_NOTIFY` → 窗口创建 - - `XCB_DESTROY_NOTIFY` → 窗口销毁 - - `XCB_MAP_NOTIFY` → 窗口映射 - - `XCB_CONFIGURE_NOTIFY` → 几何变化 - - `XCB_PROPERTY_NOTIFY` → 属性变化 (标题等) -- [x] Atom 缓存 (`_NET_WM_NAME`, `WM_NAME`, `_NET_WM_PID`, `WM_PROTOCOLS`, `WM_DELETE_WINDOW`, `_NET_WM_WINDOW_TYPE`) -- [x] 窗口过滤: 可见性、标题、PID、窗口类型 (排除 dock/desktop) - -### 3.3 WSLX11Window - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/platform/linux_wsl/wsl_x11_window.h` | X11 窗口包装 | -| `desktop/ui/platform/linux_wsl/wsl_x11_window.cpp` | 实现 | - -#### 功能实现 - -- [x] 适配 xcb_window_t → IWindow 接口 -- [x] 标题: `_NET_WM_NAME` (UTF-8) → `WM_NAME` 降级 -- [x] 几何: `xcb_get_geometry` + `xcb_translate_coordinates` -- [x] 设置几何: `xcb_configure_window` -- [x] 优雅关闭: `xcb_client_message_event` + `WM_DELETE_WINDOW` -- [x] 置顶: `xcb_configure_window` + `XCB_STACK_MODE_ABOVE` - ---- - -## 四、组件管理层 - -### 4.1 WindowManager - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/components/WindowManager.h` | 窗口管理器 | -| `desktop/ui/components/WindowManager.cpp` | 实现 | - -#### 功能实现 - -- [x] 窗口注册/查询/关闭/置顶 -- [x] 仅持有弱引用 (`std::unordered_map>`) -- [x] 不拥有窗口对象生命周期 - -### 4.2 PanelManager - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/components/PanelManager.h` | 面板管理器 | -| `desktop/ui/components/PanelManager.cpp` | 实现 | - -#### 功能实现 - -- [x] 边缘占用面板注册/注销 -- [x] 复杂布局算法: 按边缘分组 → 优先级排序 → 从边缘向内堆叠 → 计算可用几何 -- [x] 信号: `availableGeometryChanged()` - -### 4.3 ShellLayer / IShellLayer / IShellLayerStrategy - -- [x] Shell 层抽象接口 (与 QWidget 解耦) -- [x] 策略模式: 可插拔的 Shell 行为 (`IShellLayerStrategy`) -- [x] QWidget 实现: `ShellLayer` - -### 4.4 IPanel / IStatusBar - -- [x] 边缘面板抽象接口 (`IPanel`) -- [x] 状态栏接口 (`IStatusBar`) — 桩实现 - -### 4.5 DisplayServerBackendFactory - -- [x] 工厂模式创建平台特定后端 -- [x] 使用 `StaticRegisteredFactory` 静态注册 - ---- - -## 五、平台策略层 - -### 5.1 显示尺寸策略 - -- [x] `IDesktopDisplaySizeStrategy` — 策略模式控制窗口行为 -- [x] `WindowsDisplaySizePolicy` — Windows 实现 -- [x] `WSLDisplaySizePolicy` — WSL/X11 实现 - -### 5.2 属性策略 - -- [x] `IDesktopPropertyStrategy` — 属性策略基类 -- [x] `DesktopPropertyStrategyFactory` — 策略工厂 -- [x] 平台检测和策略选择 - -### 5.3 平台辅助 - -- [x] `platform_helper` — 平台工具 -- [x] `display_backend_helper` — 后端抽象 -- [x] `qt_backend` — Qt 后端检测 (X11/Wayland/EGLFS/LinuxFB) - ---- - -## 六、启动界面 Widget - -### 6.1 BootProgressWidget - -- [x] 抽象启动进度显示基类 -- [x] 工厂模式: `GetBootWidget(BootstrapStyle)` — Simple/Perfect/SelfDefined - -### 6.2 SimpleBootWidget - -- [x] 现代简约启动画面 -- [x] Logo + 品牌展示 -- [x] 分步进度指示器 (StepDotWidget) -- [x] Material Design 风格进度条 -- [x] 背景动画 (渐变色偏移) -- [x] 版本/版权信息 - -### 6.3 StepDotWidget - -- [x] 自定义步骤点绘制 -- [x] 三态: Inactive (灰色) / Active (紫色) / Completed (紫色 + 勾) - ---- - -## 七、渲染后端抽象 - -### 7.1 RenderBackend 接口 (设计完成,实现待开发) - -- [x] `render_backend.h` — 抽象接口 -- [x] `backend_capabilities.h` — 能力标志 -- [x] `render_backend_factory.h` — 工厂 (环境变量 → 平台检测 → 默认值) - -#### 待实现 - -- [ ] WindowsBackend (Windows 桌面渲染) -- [ ] EGLFSBackend (嵌入式 OpenGL ES) -- [ ] WaylandBackend (可选) -- [ ] X11Backend (可选) - ---- - -## 八、平台后端进度总结 - -| 平台 | 显示后端 | 窗口后端 | 渲染后端 | 状态 | -|------|---------|---------|---------|------| -| Windows | ✅ Win32 DWM | ✅ HWND + Hook | ❌ 待实现 | ✅ 可用 | -| Linux/WSL X11 | ✅ XCB + XWayland | ✅ XCB + QSocketNotifier | ❌ 待实现 | ✅ 可用 | -| Wayland | ❌ 未开始 | ❌ 未开始 | ❌ 待实现 | ⬜ 待开发 | -| EGLFS (嵌入式) | ❌ 未开始 | ❌ 未开始 | ❌ 待实现 | ⬜ 待开发 | -| LinuxFB | ❌ 未开始 | ❌ 未开始 | ❌ 待实现 | ⬜ 待开发 | - ---- - -## 九、关键架构模式 - -| 模式 | 应用 | -|------|------| -| 工厂模式 | DisplayServerBackendFactory, RenderBackendFactory, PlatformFactory | -| 策略模式 | IShellLayerStrategy, IDesktopDisplaySizeStrategy, IDesktopPropertyStrategy | -| 代理模式 | CFDesktopProxy, CFDesktopWindowProxy | -| 单例模式 | CFDesktopEntity | -| 弱引用模式 | WeakPtr 贯穿所有窗口和后端引用 | -| 观察者模式 | Qt Signal/Slot 事件链 | - ---- - -## 十、运行时平台检测链 - -优先级从高到低: - -1. `CFDESKTOP_DISPLAY_SERVER` 环境变量强制模式 -2. `WAYLAND_DISPLAY` 存在 → Client 模式 -3. `DISPLAY` 存在 → Client 模式 -4. `/dev/dri/card*` 存在 → DirectRender (EGLFS) -5. `/dev/fb0` 存在 → DirectRender (LinuxFB) -6. 默认 → Client 模式 - -编译时选择: -- Windows: CMake 自动包含 `windows/` 目录 -- Linux/WSL: CMake 自动包含 `linux_wsl/` 目录,检测 XCB 依赖 - ---- - -*最后更新: 2026-03-30* -*基于: 7 Agent 全面扫描结果* \ No newline at end of file diff --git a/document/todo/done/99_ui_material_framework_status.md b/document/todo/done/99_ui_material_framework_status.md deleted file mode 100644 index 853489c9c..000000000 --- a/document/todo/done/99_ui_material_framework_status.md +++ /dev/null @@ -1,629 +0,0 @@ ---- -title: "Phase 6: UI Material 框架 - 状态文档" -description: "模块ID: Phase 6,状态: 🚧 P1 控件完成,核心架构缺失" ---- - -# Phase 6: UI Material 框架 - 状态文档 - -> **模块ID**: Phase 6 -> **状态**: 🚧 P1 控件完成,核心架构缺失 -> **总体进度**: 75% -> **最后更新**: 2026-03-27 - ---- - -## 一、架构概览 - -### 1.1 分层架构 - -```text -Layer 6: Performance Profile (0%) -Layer 5: Material Widget Adapter (P0: 100%, P1: 100%, P2: 0%, P3: 0%) -Layer 4: Material Behavior Layer (100%) -Layer 3: Animation Engine Layer (90%) - 缺Rotate/Path动画 -Layer 2: Theme Engine Layer (100%) -Layer 1: Core Math & Utility Layer (100%) -Layer 0: Layout System (0%) - **新增:布局系统完全缺失** -```cpp - -### 1.2 设计规则 (RULE-01 to RULE-09) - -| 规则 | 描述 | -|------|------| -| **RULE-01** | Inheritance Only - 所有 Material 控件必须继承 Qt 原生控件 | -| **RULE-02** | Signal Preservation - 不移除/修改 Qt 公共信号 | -| **RULE-03** | Complete Paint Override - paintEvent 必须完全控制渲染,不使用 `style()->drawControl()` | -| **RULE-04** | No QSS - Qt StyleSheet 禁止用于 Material 视觉效果 | -| **RULE-05** | No Qt Quick - 不使用 QML/Qt Quick | -| **RULE-06** | Animation Disable - 动画系统必须支持全局禁用 | -| **RULE-07** | Theme Read-Only - 控件不能修改全局主题状态 | -| **RULE-08** | Component Injection - 渲染组件通过组合注入,而非直接 `new` | -| **RULE-09** | Token Access - 通过上下文感知接口和 token 字符串访问主题 | - -### 1.3 层级依赖规则 - -```cpp -ui/widget/material -> ui/components, ui/core, ui/base -ui/components -> ui/core, ui/base -ui/core -> ui/base -ui/base -> QtCore only (no QtWidgets/QtGui) -```bash - ---- - -## 二、完成状态总览 - -| Layer | 状态 | 完成度 | -|-------|------|--------| -| Layer 1: Core Math & Utility | ✅ | 100% | -| Layer 2: Theme Engine | ✅ | 100% | -| Layer 3: Animation Engine | ⚠️ | 90% | -| Layer 4: Material Behavior | ✅ | 100% | -| Layer 5: P0 核心控件 | ✅ | 100% (7/7) | -| Layer 5: P1 常用控件 | ✅ | 100% (12/12) | -| Layer 5: P2 高级控件 | ⬜ | 0% (0/27) | -| Layer 5: P3 专业控件 | ⬜ | 0% (0/25) | -| Layer 6: Performance Profile | ⬜ | 0% | -| Layer 0: Layout System | ❌ | 0% (新增) | - ---- - -## 三、已完成工作 - -### 3.1 Layer 1: Core Math & Utility (100%) - -| 文件 | 路径 | 功能 | -|------|------|------| -| MathHelper | `ui/base/math_helper.h` | 插值、钳位、曲线求值 | -| Color | `ui/base/color.h` | 颜色空间转换、HCT支持 | -| ColorHelper | `ui/base/color_helper.h` | 颜色混合、对比度 | -| Easing | `ui/base/easing.h` | Material缓动曲线 | -| GeometryHelper | `ui/base/geometry_helper.h` | 圆角形状生成 | -| DevicePixel | `ui/base/device_pixel.h` | DPI单位转换 | - -**核心特性:** -- 零 Qt UI 依赖 (仅 QtCore) -- 全面的动画和渲染数学函数 -- Material Color System 支持 -- 编译时哈希函数 (FNV-1a) - -### 3.2 Layer 2: Theme Engine (100%) - -#### Token 系统架构 - -```bash -Reference Token (MD3 spec) - | - v -System Token (cfmaterial scheme) - | - v -Component Token (widget-specific) -```text - -#### 已完成文件 - -**核心接口:** -- `ui/core/theme_manager.h` - 主题管理器单例 -- `ui/core/color_scheme.h` - 颜色方案接口 -- `ui/core/font_type.h` - 字体类型接口 -- `ui/core/motion_spec.h` - 动画规范接口 -- `ui/core/radius_scale.h` - 形状缩放接口 - -**Material 实现:** -- `ui/core/material/cfmaterial_scheme.h` - Material 颜色实现 -- `ui/core/material/cfmaterial_theme.h` - Material 主题 -- `ui/core/material/cfmaterial_fonttype.h` - Material 字体实现 -- `ui/core/material/cfmaterial_motion.h` - Material 动画实现 -- `ui/core/material/cfmaterial_radius_scale.h` - Material 形状实现 -- `ui/core/material/material_factory.h` - Material 主题工厂 - -**Token 目录:** (参见 ui/core/token/ 源码) - -### 3.3 Layer 3: Animation Engine (100%) - -#### 已完成文件 - -**核心接口:** -- `ui/components/animation.h` - ICFAbstractAnimation 基类 -- `ui/components/timing_animation.h` - ICFTimingAnimation (基于时间) -- `ui/components/animation_factory_manager.h` - 动画工厂管理器 - -**Material 实现:** -- `ui/components/material/cfmaterial_animation_factory.h` - Material 工厂实现 -- `ui/components/material/cfmaterial_animation_strategy.h` - 动画策略模式 -- `ui/components/material/cfmaterial_fade_animation.h` - 淡入淡出 -- `ui/components/material/cfmaterial_scale_animation.h` - 缩放变换 -- `ui/components/material/cfmaterial_slide_animation.h` - 滑动动画 -- `ui/components/material/cfmaterial_property_animation.h` - Qt 属性动画包装器 - -**Tokens:** -- `ui/components/material/token/animation_token_literals.h` - 动画 token 定义 -- `ui/components/material/token/animation_token_mapping.h` - Token 到动画映射 - -#### 动画状态 - -```cpp -Idle -> Running -> (Paused | Finished) - | - v - Stopped -```bash - -#### 支持的 Token 类型 -- `md.animation.fadeIn` -- `md.animation.fadeOut` -- `md.animation.slideUp` -- `md.animation.slideDown` -- `md.animation.scaleUp` -- `md.animation.scaleDown` -- `md.animation.rotateIn` -- `md.animation.rotateOut` - -### 3.4 Layer 4: Material Behavior (100%) - -| 组件 | 文件 | 功能 | -|------|------|------| -| StateMachine | `ui/widget/material/base/state_machine.h` | 视觉状态管理 | -| PainterLayer | `ui/widget/material/base/painter_layer.h` | 颜色叠加层绘制 | -| RippleHelper | `ui/widget/material/base/ripple_helper.h` | 涟漪效果控制器 | -| ElevationController | `ui/widget/material/base/elevation_controller.h` | 阴影/海拔系统 | -| FocusRing | `ui/widget/material/base/focus_ring.h` | 焦点环渲染器 | - -#### 状态机状态与透明度 - -| 状态 | 透明度 | 描述 | -|------|:------:|------| -| Normal | 0.00 | 默认状态 | -| Hovered | 0.08 | 鼠标悬停 | -| Pressed | 0.12 | 鼠标按下 | -| Focused | 0.12 | 键盘焦点 | -| Dragged | 0.16 | 正在拖拽 | -| Checked | 0.08 | 选中状态 | -| Disabled | 0.00 | 禁用状态 | - -**优先级:** Disabled > Pressed > Dragged > Focused > Hovered > Normal - -#### 海拔级别 - -| 级别 | DP | 阴影 (浅色) | 色调叠加 (深色) | -|------:|---:|-------------|------------------| -| 0 | 0dp | 无 | 无 | -| 1 | 1dp | blur=2px, offset=1px | overlay 1 | -| 2 | 3dp | blur=4px, offset=2px | overlay 2 | -| 3 | 6dp | blur=8px, offset=4px | overlay 3 | -| 4 | 8dp | blur=12px, offset=6px | overlay 4 | -| 5 | 12dp | blur=16px, offset=8px | overlay 5 | - -### 3.5 Layer 5: P0 核心控件 (100%) - -| 控件 | 文件 | Qt 基类 | 功能 | -|------|------|---------|------| -| **Button** | `ui/widget/material/widget/button/` | QPushButton | 5种变体、阴影、涟漪 | -| **TextField** | `ui/widget/material/widget/textfield/` | QLineEdit | 浮空标签、前后图标、计数器 | -| **TextArea** | `ui/widget/material/widget/textarea/` | QTextEdit | 多行文本输入 | -| **Label** | `ui/widget/material/widget/label/` | QLabel | 文本显示 | -| **CheckBox** | `ui/widget/material/widget/checkbox/` | QCheckBox | 复选框 | -| **RadioButton** | `ui/widget/material/widget/radiobutton/` | QRadioButton | 单选按钮 | -| **GroupBox** | `ui/widget/material/widget/groupbox/` | QGroupBox | 分组容器 | - -#### Button 变体 -- Filled (primary) - 填充主按钮 -- Outlined (secondary) - 轮廓次按钮 -- Text (tertiary) - 文本三级按钮 -- Tonal - 色调按钮 -- Elevated - 抬升按钮 - -#### TextField 功能 -- 2种变体:Filled, Outlined -- 浮空标签 -- 前后图标支持 -- 清除按钮 -- 字符计数器 -- 帮助文本/错误文本 -- 密码模式 - ---- - -## 四、待完成工作 - -### 4.1 Layer 5: P1 常用控件 (12/12) ✅ - -| 控件 | Qt 基类 | 路径 | Demo | -|------|---------|------|------| -| **Button** | QPushButton | `button/` | ButtonDemo | -| **CheckBox** | QCheckBox | `checkbox/` | CheckBoxDemo | -| **ComboBox** | QComboBox | `comboBox/` | ComboBoxDemo | -| **DoubleSpinBox** | QDoubleSpinBox | `doublespinbox/` | DoubleSpinBoxDemo | -| **GroupBox** | QGroupBox | `groupbox/` | GroupBoxDemo | -| **Label** | QLabel | `label/` | LabelDemo | -| **ListView** | QListView | `listview/` | ListViewDemo | -| **ProgressBar** | QProgressBar | `progressbar/` | ProgressBarDemo | -| **RadioButton** | QRadioButton | `radiobutton/` | RadioButtonDemo | -| **ScrollView** | QScrollArea | `scrollview/` | ScrollViewDemo | -| **Separator** | QFrame | `separator/` | SeparatorDemo | -| **Slider** | QSlider | `slider/` | SliderDemo | -| **SpinBox** | QSpinBox | `spinbox/` | SpinBoxDemo | -| **Switch** | QCheckBox | `switch/` | SwitchDemo | -| **TabView** | QTabWidget | `tabview/` | TabViewDemo | -| **TableView** | QTableView | `tableview/` | TableViewDemo | -| **TextArea** | QTextEdit | `textarea/` | TextAreaDemo | -| **TextField** | QLineEdit | `textfield/` | TextFieldDemo | -| **TreeView** | QTreeView | `treeview/` | TreeViewDemo | - -### 4.2 Layer 5: P2 高级控件 (0/27) - -| 控件 | Qt 基类 | 优先级 | -|------|---------|--------| -| DatePicker | QCalendarWidget | P2 | -| TimePicker | QTimeEdit | P2 | -| ColorPicker | QColorDialog | P2 | -| MenuBar | QMenuBar | P2 | -| ContextMenu | QMenu | P2 | -| ToolBar | QToolBar | P2 | -| StatusBar | QStatusBar | P2 | -| Dialog | QDialog | P2 | -| Snackbar | QWidget (custom) | P2 | -| Card | QFrame (custom) | P2 | -| FloatingActionButton | QPushButton (custom) | P2 | -| BottomNavigation | QWidget (custom) | P2 | -| Drawer | QWidget (custom) | P2 | -| SearchBar | QLineEdit (custom) | P2 | -| Dial | QDial | P2 | -| Stepper | QWidget (custom) | P2 | -| Chip | QWidget (custom) | P2 | -| Badge | QWidget (custom) | P2 | -| Tooltip | QToolTip | P2 | -| Popover | QWidget (custom) | P2 | -| CircularProgressBar | QWidget (custom) | P2 | -| NavigationRail | QWidget (custom) | P2 | -| NavigationDrawer | QWidget (custom) | P2 | -| TopAppBar | QWidget (custom) | P2 | -| BottomSheet | QWidget (custom) | P2 | -| AlertDialog | QWidget (custom) | P2 | -| Menu | QWidget (custom) | P2 | - -### 4.3 Layer 5: P3 专业控件 (0/25) - -| 控件 | 类型 | -|------|------| -| SplitView | 专业分割视图 | -| ChartView | 图表视图 | -| DrawingArea | 绘图区域 | -| Canvas | 画布控件 | -| FileList | 文件列表 | -| FileTree | 文件树 | -| CodeEditor | 代码编辑器 | -| Calendar | 日历控件 | -| Wizard | 向导对话框 | -| PropertyEditor | 属性编辑器 | -| TreeTable | 树表控件 | -| Gauge | 仪表盘 | -| Timeline | 时间轴 | -| GridView | 网格视图 | -| SwipeableItem | 可滑动项 | -| NotificationList | 通知列表 | -| Breadcrumbs | 面包屑导航 | -| Pagination | 分页控件 | -| RatingBar | 评分条 | -| RangeSlider | 范围滑块 | -| SliderCaptcha | 滑块验证码 | -| InputValidator | 输入验证器 | -| AutoComplete | 自动完成 | -| DropZone | 拖放区域 | -| RichTextToolBar | 富文本工具栏 | - -### 4.4 Layer 6: 性能配置 (0%) - -#### PerformanceProfile 枚举 - -```cpp -enum class PerformanceProfile { - Desktop, // 完整: 60fps, 阴影, 涟漪, 全部动画 - Embedded, // 降级: 30fps, 无阴影, 无涟漪, 仅状态动画 - UltraLow // 最小: 无动画, 无阴影, 仅颜色 -}; -```bash - -#### 降级策略 - -| 特性 | Desktop | Embedded | UltraLow | -|------|:-------:|:--------:|:--------:| -| 涟漪效果 | 是 | 否 | 否 | -| 阴影 | 是 | 否 | 否 | -| 状态动画 | 是 | 是 | 否 | -| 焦点动画 | 是 | 是 | 否 | -| 目标 FPS | 60 | 30 | N/A | - ---- - -## 五、实现规范 - -### 5.1 Widget 实现模板 - -所有控件遵循 Button 实现模式: - -```cpp -class Button : public QPushButton { - Q_OBJECT - -public: - explicit Button(QWidget* parent = nullptr); - void paintEvent(QPaintEvent* event) override; - QSize sizeHint() const override; - -protected: - void enterEvent(QEnterEvent* event) override; - void leaveEvent(QEvent* event) override; - void mousePressEvent(QMouseEvent* event) override; - void mouseReleaseEvent(QMouseEvent* event) override; - -private: - StateMachine* m_stateMachine; - RippleHelper* m_ripple; - MdElevationController* m_elevation; - MdFocusIndicator* m_focusIndicator; -}; -```text - -### 5.2 事件处理模式 - -```cpp -void Widget::enterEvent(QEnterEvent* event) { - QPushButton::enterEvent(event); // 始终先调用父类 - m_stateMachine->onHoverEnter(); - update(); -} -```text - -### 5.3 标准绘制模式 - -```cpp -void Widget::paintEvent(QPaintEvent* event) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - // 1. 背景 - painter.fillRect(rect(), backgroundColor()); - - // 2. 状态层 - m_stateMachine->paintStateLayer(&painter, rect()); - - // 3. 内容 - paintContent(&painter); - - // 4. 海拔阴影 - m_elevation->paintShadow(&painter, rect()); - - // 5. 焦点环 - if (hasFocus()) { - m_focusIndicator->paint(&painter, rect()); - } -} -```bash - ---- - -## 六、测试覆盖情况 - -### 已实现的测试 - -**ui/base 测试:** -- `math_helper_test.cpp` -- `color_test.cpp` -- `color_helper_test.cpp` -- `device_pixel_test.cpp` -- `geometry_helper_test.cpp` -- `easing_test.cpp` - -**ui/components 测试:** -- `state_machine_test.cpp` -- `ripple_helper_test.cpp` -- `elevation_controller_test.cpp` -- `focus_ring_test.cpp` -- `painter_layer_test.cpp` - -**ui/core 测试:** -- `token_test.cpp` - -### 关键缺口: UI控件测试 - -| 测试类型 | 状态 | 覆盖率 | -|---------|------|-------| -| UI Base 测试 | ✅ | 60% | -| UI Components 测试 | ✅ | 60% | -| UI Widgets 测试 | ❌ | **0%** | -| Widget 单元测试 | ❌ | **0%** | - -**急需补充**: 为19个P0/P1控件添加单元测试 -- button_test.cpp, textfield_test.cpp, textarea_test.cpp -- checkbox_test.cpp, radiobutton_test.cpp, groupbox_test.cpp -- label_test.cpp, slider_test.cpp, progressbar_test.cpp -- switch_test.cpp, combobox_test.cpp, spinbox_test.cpp -- doublespinbox_test.cpp, listview_test.cpp, treeview_test.cpp -- tableview_test.cpp, tabview_test.cpp, scrollview_test.cpp -- separator_test.cpp - ---- - -## 七、关键文件路径 - -### 已实现文件 - -**基础层 (Layer 1):** -```text -d:\ProjectHome\CFDesktop\ui\base\ -├── math_helper.h/cpp -├── color.h/cpp -├── color_helper.h/cpp -├── easing.h/cpp -├── geometry_helper.h/cpp -└── device_pixel.h/cpp -```text - -**核心层 (Layer 2):** -```text -d:\ProjectHome\CFDesktop\ui\core\ -├── theme_manager.h/cpp -├── color_scheme.h -├── material/ -│ ├── cfmaterial_scheme.h/cpp -│ ├── cfmaterial_theme.h/cpp -│ ├── cfmaterial_fonttype.h/cpp -│ ├── cfmaterial_motion.h/cpp -│ ├── cfmaterial_radius_scale.h/cpp -│ └── material_factory.h/cpp -└── token/ - ├── material_scheme/ - ├── motion/ - ├── radius_scale/ - ├── typography/ - └── theme_name/ -```text - -**动画层 (Layer 3):** -```text -d:\ProjectHome\CFDesktop\ui\components\ -├── animation.h/cpp -├── timing_animation.h/cpp -├── animation_factory_manager.h/cpp -└── material/ - ├── cfmaterial_animation_factory.h/cpp - ├── cfmaterial_animation_strategy.h/cpp - ├── cfmaterial_fade_animation.h/cpp - ├── cfmaterial_scale_animation.h/cpp - ├── cfmaterial_slide_animation.h/cpp - ├── cfmaterial_property_animation.h/cpp - └── token/ -```text - -**行为层 (Layer 4):** -```text -d:\ProjectHome\CFDesktop\ui\widget\material\base\ -├── state_machine.h/cpp -├── painter_layer.h/cpp -├── ripple_helper.h/cpp -├── elevation_controller.h/cpp -└── focus_ring.h/cpp -```text - -**控件层 (Layer 5 - P0 + P1):** -```text -/home/charliechen/CFDesktop/ui/widget/material/widget/ -├── button/button.h/cpp ✅ -├── textfield/textfield.h/cpp ✅ -├── textarea/textarea.h/cpp ✅ -├── label/label.h/cpp ✅ -├── checkbox/checkbox.h/cpp ✅ -├── radiobutton/radiobutton.h/cpp ✅ -├── groupbox/groupbox.h/cpp ✅ -├── combobox/combobox.h/cpp ✅ (P1) -├── slider/slider.h/cpp ✅ (P1) -├── progressbar/progressbar.h/cpp ✅ (P1) -├── switch/switch.h/cpp ✅ (P1) -├── listview/listview.h/cpp ✅ (P1) -├── treeview/treeview.h/cpp ✅ (P1) -├── tableview/tableview.h/cpp ✅ (P1) -├── tabview/tabview.h/cpp ✅ (P1) -├── scrollview/scrollview.h/cpp ✅ (P1) -├── separator/separator.h/cpp ✅ (P1) -├── spinbox/spinbox.h/cpp ✅ (P1) -└── doublespinbox/doublespinbox.h/cpp ✅ (P1) -```text - -### 待创建文件 - -**Layer 5 - P2 高级控件 (27个):** -```text -ui/widget/material/widget/ -├── datepicker/ # 日期选择器 -├── timepicker/ # 时间选择器 -├── colorpicker/ # 颜色选择器 -├── menubar/ # 菜单栏 -├── contextmenu/ # 右键菜单 -├── toolbar/ # 工具栏 -├── statusbar/ # 状态栏 -├── dialog/ # 对话框 -├── snackbar/ # 消息条 -├── card/ # 卡片 -├── floatingactionbutton/ # 浮动操作按钮 -├── bottomnavigation/ # 底部导航 -├── drawer/ # 抽屉 -├── searchbar/ # 搜索栏 -├── dial/ # 旋钮 -├── stepper/ # 步进器 -├── chip/ # 芯片 -├── badge/ # 徽章 -├── tooltip/ # 工具提示 -├── popover/ # 弹出框 -├── circularprogressbar/ # 圆形进度条 -├── navigationrail/ # 侧边导航 -├── navigationdrawer/ # 导航抽屉 -├── topappbar/ # 顶栏 -├── bottomsheet/ # 底部面板 -├── alertdialog/ # 警告对话框 -└── menu/ # 菜单 -```text - -**Layer 6 - 性能配置:** -```text -d:\ProjectHome\CFDesktop\ui\core\ -└── performance_profile.h/cpp -```bash - ---- - -## 八、P1 控件完成里程碑 (2026-03-19) - -### 完成概览 -- **完成日期**: 2026-03-18 -- **组件总数**: 19 个 (P0: 7 + P1: 12) -- **Demo 示例**: 19 个完整示例 -- **代码行数**: ~15000 行 (估算) - -### P1 新增组件详情 - -| 组件 | 主要功能 | 特性 | -|------|---------|------| -| **Slider** | 滑块输入 | 水平/垂直、刻度标记、轨道激活态 | -| **Switch** | 开关切换 | 动画滑块、52x32dp 尺寸、轨道颜色过渡 | -| **ProgressBar** | 进度显示 | 确定/不确定模式、平滑动画 | -| **ComboBox** | 下拉选择 | Filled/Outlined 变体、自定义弹出窗口 | -| **ListView** | 列表显示 | 单/双/三行项、自定义委托、分割线 | -| **TableView** | 表格显示 | Material 表头、网格线、行选择涟漪 | -| **TreeView** | 树形显示 | 展开/折叠动画、树连接线、56dp/级缩进 | -| **TabView** | 标签页 | 滑动指示器动画、关闭按钮、选项卡滚动 | -| **ScrollView** | 滚动视图 | 12dp 滚动条、淡入淡出、悬停扩展 | -| **Separator** | 分隔线 | 水平/垂直、FullBleed/Inset/MiddleInset | -| **SpinBox** | 整数输入 | 增量/减量按钮、轮廓样式、焦点指示器 | -| **DoubleSpinBox** | 浮点输入 | 同 SpinBox,支持浮点数 | - -### 共同特性实现 -- ✅ Material Design 3 色彩系统 (CFColor + MD3 tokens) -- ✅ 状态机管理 (hover/press/focus/disabled) -- ✅ 涟漪效果 (RippleHelper) -- ✅ 焦点指示器 (FocusRing) -- ✅ 动画支持 (CFMaterialAnimationFactory) -- ✅ 7步绘制流水线 - -### 构建集成 -所有组件已集成到 CMake 构建系统: -- `ui/widget/CMakeLists.txt` 已更新 -- `example/ui/widget/material_example/CMakeLists.txt` 已更新 -- MaterialGalleryWindow 已注册所有 Demo - -### 下一步工作 -1. **P2 高级控件** (27个) - 菜单、对话框、工具栏等 -2. **单元测试补充** - 提高 P0/P1 控件测试覆盖率 -3. **Layer 6: 性能配置** - HWTier 联动降级策略 - ---- - -## 九、相关文档 - -- 原始TODO: [../base/99_ui_material_framework.md](../base/99_ui_material_framework.md) - ---- - -*文档版本: v1.1* -*生成时间: 2026-03-19* diff --git a/document/todo/done/PROJECT_STATUS_REPORT.md b/document/todo/done/PROJECT_STATUS_REPORT.md deleted file mode 100644 index 7549d2908..000000000 --- a/document/todo/done/PROJECT_STATUS_REPORT.md +++ /dev/null @@ -1,515 +0,0 @@ ---- -title: CFDesktop 项目状态报告 -description: "报告日期: 2026-03-30,报告版本: v4.0" ---- - -# CFDesktop 项目状态报告 - -> **报告日期**: 2026-03-30 -> **报告版本**: v4.0 -> **项目版本**: 0.13.1 -> **扫描方式**: 7个并发Agent全面扫描 - ---- - -## 执行摘要 - -CFDesktop 是一个基于 Qt6 的嵌入式桌面框架项目,采用 Material Design 3 设计规范。项目使用 C++23 开发,旨在为嵌入式 Linux 设备提供完整的 UI 框架和开发工具链。 - -### 整体完成度: 约 75% - -| 模块 | 完成度 | 状态 | 优先级 | -|------|--------|------|--------| -| Phase 0: 工程骨架 | 100% | ✅ 已完成并归档 | P0 | -| Phase 1: 硬件探针 | 100% | ✅ 完成 | P0 | -| Phase 2: Base库核心 | 85% | 🚧 进行中 | P0 | -| Phase 3: 输入抽象层 | 0% | ⬜ 待开始 | P1 | -| Phase 4: 多平台模拟器 | 0% | ⬜ 待开始 | P1 | -| Phase 5: 测试体系 | 55% | 🚧 进行中 | P0 | -| Phase 6: UI框架 | 95% | 🚧 进行中 | P0 | -| Desktop 模块 | 90% | 🚧 进行中 | P0 | -| 显示后端 (Windows+WSL X11) | 70% | ✅ 核心完成 | P0 | -| 显示后端 (Wayland/嵌入式) | 0% | ⬜ 待开始 | P2 | - ---- - -## 一、各模块详细状态 - -### 1.0 工程骨架 (Phase 0) - 100% ✅ 已完成 - -#### ✅ 已完成 -- **CMake 构建系统** (100%) - - 多模块分层架构 (base/ui/desktop/example/test) - - C++23 标准 - - Qt 6.8.3 集成 - - 跨平台工具链配置 (Windows/LLVM、Windows/MSVC、Linux/GCC) - - 自动化 MOC/RCC/UIC - - VSCode 配置自动生成 (clangd、launch、tasks) - - 分类输出目录管理 - -- **代码规范配置** (100%) - - .clang-format 配置 - - .clangd 配置 - - Git pre-commit hook - -- **CI/CD** (100%) - - Git Hooks 本地验证策略 - - Docker 多架构构建镜像 - - MkDocs 文档自动部署 - ---- - -### 1.1 硬件探针 (Phase 1) - 100% ✅ 完成 - -#### ✅ 已完成 -- **CPU 检测器** (100%) - `base/system/cpu/` 模块 - - CPU型号、架构、制造商获取 - - CPU特性标志检测 (neon、aes、avx2等) - - 缓存大小查询 (L1、L2、L3) - - Big.LITTLE架构检测 - - 核心数量统计 - - CPU温度监测 - - 当前/最大频率查询 - - CPU使用率百分比 - - 跨平台支持 (Windows/Linux) - -- **内存检测器** (95%) - `base/system/memory/` 模块 - - 物理内存信息 (总内存、可用内存、空闲内存) - - 虚拟内存/交换空间信息 - - 进程内存信息 (RSS、虚拟内存、峰值) - - 内存模块(DIMM)信息 (容量、类型、频率、制造商) - - Linux特定缓存内存信息 - - 跨平台支持 (Windows/Linux) - -- **设备控制台** (85%) - `base/device/console/` 模块 - - 控制台尺寸查询 - - 颜色支持检测 - - 策略链模式实现容错机制 - -#### ⚠️ 部分完成 -- **策略链功能** (80%) - `base/include/base/policy_chain/` - - 核心PolicyChain类实现 - - 工厂函数和构建器API - - 完整的测试覆盖 - -#### ✅ 已完成 (新增) -- **GPU 检测器** (90%) - `base/system/gpu/` 模块 - - GPU设备信息、显示信息、环境评分 - - Linux DRM/WSL2检测、Windows DXGI检测 -- **网络检测器** (85%) - `base/system/network/` 模块 - - 网卡枚举、IP地址、可达性检测 - -#### ✅ 已完成 (HWTier 系统) -- **HWTier 系统** (100%) - `base/system/hardware_tier/` 模块 - - `HardwareTierLevel` 枚举 (Unknown/Low/Mid/High) - - 可插拔四阶段管线 (Collect→Score→Assess→Policy) - - 默认收集器、四维度评分器 (CPU/GPU/Memory/Display 各 0-100) - - 默认评估器和策略引擎 - - DeviceConfig 覆盖支持 (`setDeviceConfigOverride()`) - - `HardwareTierCapabilities` 能力标志 (OpenGL/软件渲染/动画/硬件解码/EGLFS/LinuxFB) - - 完整管线注册 API (`registerCollector/Scorer/Assessor/Policy`) - - 示例程序 `example/base/system/example_hardware_tier.cpp` - -#### ❌ 待完成 -- 自定义检测脚本执行支持 - ---- - -### 1.2 Base库核心 (Phase 2) - 80% 🚧 进行中 - -#### ✅ 已完成 -- **基础工具类** (100%) - - FNV-1a 哈希算法 - - Optional/Expected 类型 - - 单例模式 - - Scope Guard - - 弱指针实现 - - 跨平台宏定义 - - 无锁队列 (MPSC) - -- **配置管理系统** (85%) - `desktop/base/config_manager/` - - 四层配置存储系统 (Temp/App/User/System) - - 结构化键值管理 (KeyView, Key) - - 配置变更监控 (Watcher 机制) - - 异步/同步持久化 - - 类型安全的查询接口 - - 单例模式实现 - -- **日志系统** (90%) - `desktop/base/logger/` - - 多等级日志支持 (TRACE, DEBUG, INFO, WARNING, ERROR) - - 格式化日志函数 - - 异步日志队列 - - 多种输出格式化器 (console, file, default) - - 多种输出目标 (console sink, file sink) - - 线程安全的日志实现 - -- **DPI基础转换** (60%) - `ui/base/device_pixel.h` - - CanvasUnitHelper (dp/sp/px转换) - - BreakPoint响应式断点 (Compact/Medium/Expanded) - -#### ❌ 待完成 -- **DPI自动检测** (0%) - 自动获取显示器DPI -- **DPI自适应缩放** (0%) - 根据设备DPI自动调整UI -- 配置版本控制 -- 配置迁移机制 -- 配置验证功能 -- 网络日志输出器 -- 日志轮转功能 - ---- - -### 1.3 输入抽象层 (Phase 3) - 0% ⬜ 待开始 - -#### ❌ 完全未实现 -所有组件待实现: -- InputManager - 统一分发层 -- TouchInputHandler - 触摸处理器 -- KeyInputHandler - 按键处理器 -- RotaryInputHandler - 旋钮处理器 -- GestureRecognizer - 手势识别器 -- FocusNavigator - 焦点导航器 -- 原生设备驱动 (EvdevDevice, GPIOButton, RotaryEncoder) - ---- - -### 1.4 多平台模拟器 (Phase 4) - 0% ⬜ 待开始 - -#### ❌ 完全未实现 -需要实现: -- SimulatorWindow - 主窗口 -- DeviceFrame - 设备外壳 -- TouchVisualizer - 触摸可视化 -- HWTierSelector - 档位选择器 -- DPI 注入器 -- 硬件 Mock -- 输入模拟器 - ---- - -### 1.5 测试体系 (Phase 5) - 55% 🚧 进行中 - -#### ✅ 已完成 (测试覆盖率约55%) -- **测试框架** (100%) - - Google Test 集成 - - CMake 辅助函数 - - 跨平台测试脚本 - -- **base 模块测试** (90%) - - expected, scope_guard, hash, weak_ptr - - policy_chain - - mpsc_queue - -- **ui 模块测试** (60%) - - 基础工具 (color, math_helper, easing, geometry, device_pixel) - - Material组件 (state_machine, ripple, elevation, focus_ring, painter_layer) - - token_test - -- **system 模块测试** (70%) - - CPU信息查询测试 - - 内存信息查询测试 - -- **logger 模块测试** (85%) - - benchmark_test, error_handling_test, concurrency_test, formatter_test - -- **config_manager 模块测试** (80%) - - config_store_test - -#### ❌ 待完成 (覆盖率缺口约45%) -- **UI控件测试** (0%) - **关键缺口** - - 19个P0/P1控件全部无单元测试 - - button_test.cpp, textfield_test.cpp, checkbox_test.cpp, etc. - -- **desktop 模块** (5%) - - ASCII Art测试 (0%) - - 文件操作测试 (0%) - - desktop/base/ascii_art - - desktop/base/file_operations - -- **ui 模块缺口** - - UI核心功能 (theme, theme_manager) - -- **example 模块** - - GUI示例程序测试 - - 主题演示测试 - ---- - -### 1.6 UI Material Framework (Phase 6) - 75% 🚧 进行中 - -#### ✅ Layer 1-4 完成 100% -- **Layer 1**: Core Math & Utility (100%) - - math_helper, color, easing, geometry_helper, device_pixel - -- **Layer 2**: Theme Engine Layer (100%) - - ThemeManager, ThemeFactory, Token 系统 - - Material Color Scheme, Material Theme - - ColorScheme, FontType, MaterialScheme - - MaterialFactory, MaterialMotion, RadiusScale - -- **Layer 3**: Animation Engine Layer (90%) - - AnimationFactory, AnimationFactoryManager - - TimingAnimation, AnimationGroup - - Material动画策略 - - Material动画工厂 - - Fade/Slide/Scale动画 - - ⚠️ Rotate/Path动画缺失 - -- **Layer 4**: Material Behavior Layer (100%) - - StateMachine, RippleHelper, ElevationController - - FocusRing, PainterLayer - -#### ⚠️ 缺失部分 -- **布局管理系统** (0%) - Grid、Stack、ConstraintLayout -- **手势识别系统** (0%) - 触摸/手势统一接口 - -#### ✅ Layer 5 控件层 (P0-P1 完成 100%) -- **P0 核心控件** (7/7 完成 ✅) - - Button, TextField, TextArea, Label, CheckBox, RadioButton, GroupBox - -- **P1 常用控件** (12/12 完成 ✅) - - ComboBox, Slider, ProgressBar, Switch, ListView, TreeView, TableView, TabView, ScrollView, Separator, SpinBox, DoubleSpinBox - -- **P2 高级控件** (0/27 待实现) - - DatePicker, TimePicker, ColorPicker, MenuBar, ContextMenu, ToolBar, StatusBar, Dialog, Snackbar, Card, FloatingActionButton, BottomNavigation, Drawer, SearchBar, Dial, Stepper, Chip, Badge, Tooltip, Popover, CircularProgressBar 等 - -- **P3 专业控件** (0/25 待实现) - - SplitView, ChartView, DrawingArea, Canvas, FileList, FileTree, CodeEditor, Calendar, Wizard, PropertyEditor, TreeTable, Gauge, Timeline, GridView, SwipeableItem, BottomSheet, NotificationList, Breadcrumbs, Pagination, RatingBar, RangeSlider 等 - -#### ✅ 应用支持 (100%) -- Application, MaterialApplication - ---- - -### 1.7 Desktop 模块 - 90% 🚧 进行中 - -#### ✅ 已完成 -- **主入口程序** (85%) - `desktop/main/` - - 主入口程序 (main.cpp) - - DAG 初始化链 (InitSessionChain) — Kahn 算法拓扑排序 - - 早期配置阶段 (early_config_stage.cpp) - - 日志系统启动阶段 (logger_stage.cpp) - - 欢迎界面实现 (early_welcome_impl.cpp) - - GUI 初始化阶段 (gui_init_stage.cpp) - - 路径解析器 (path/) - -- **ASCII Art 模块** (100%) - `desktop/base/ascii_art/` - - 核心 Canvas 类 - - 四种边框样式 (Single, Double, Rounded, Solid) - - UTF-8 支持 - - 高级文本处理 - -- **文件操作模块** (70%) - `desktop/base/file_operations/` - - 文件/目录创建 - - 路径拼接 - - 路径存在检查 - - 应用运行时目录获取 - -- **显示后端架构** (90%) - `desktop/ui/components/` + `desktop/ui/platform/` - - IDisplayServerBackend 三模式抽象 (Client/Compositor/DirectRender) - - IWindowBackend 窗口后端接口 - - IWindow 平台无关窗口接口 - - WindowManager 窗口管理器 (弱引用模式) - - PanelManager 面板管理器 (边缘布局算法) - - ShellLayer / IShellLayer / IShellLayerStrategy - - DisplayServerBackendFactory 工厂 - -- **Windows 平台后端** (100%) - `desktop/ui/platform/windows/` - - WindowsDisplayServerBackend (Win32 DWM) - - WindowsWindowBackend (SetWinEventHook + EnumWindows) - - WindowsWindow (HWND → IWindow 适配) - - WindowsDisplaySizePolicy / WindowsPropertyStrategy - -- **WSL X11 平台后端** (100%) - `desktop/ui/platform/linux_wsl/` - - WSLX11DisplayServerBackend (XCB + XWayland) - - WSLX11WindowBackend (XCB + QSocketNotifier) - - WSLX11Window (xcb_window_t → IWindow 适配) - - WSLDisplaySizePolicy / WSLPlatformFactory - -- **启动界面 Widget** (100%) - `desktop/ui/widget/init_session/` - - SimpleBootWidget (Logo + 分步进度 + Material 风格) - - StepDotWidget (三态步骤点) - - BootWidgetFactory - -- **桌面核心类** (90%) - `desktop/ui/` - - CFDesktopEntity (单例,全局桌面生命周期) - - CFDesktop (主桌面 Widget) - - CFDesktopProxy / CFDesktopWindowProxy (代理模式) - -- **渲染后端抽象** (30%) - `desktop/ui/render/` - - RenderBackend 接口设计 - - BackendCapabilities 能力标志 - - RenderBackendFactory 工厂 - - ❌ 具体实现未开发 - -#### ❌ 待完成 -- Wayland 后端实现 -- EGLFS / LinuxFB 嵌入式后端 -- RenderBackend 具体实现 -- ReleaseEarlyInit() 函数 -- file_operations 高级功能 (权限/监控/加密) - ---- - -### 1.8 文档状态 - 60% 🚧 进行中 - -#### ✅ 已完成文档 (约153个Markdown文件) -- **架构设计文档** (高质量) - - 工程骨架 (Phase 0) - - 开发环境配置 - - CI/CD 流程 - - UI Material Design 层文档 - - 日志系统文档 (7篇) - -- **API 文档** (完整) - - CPU 检测 API - - 内存检测 API - - 基础工具类 API - - UI 框架 API - - Material 组件 API - -- **开发工具文档** - - Git Hooks 规范 - - Docker 构建指南 - - 代码格式化工具 - - 测试框架文档 - -#### ❌ 缺失文档 -- **核心基础模块** - - HardwareProbe 完整文档 - - CapabilityPolicy 策略引擎文档 - - ConfigStore 配置中心文档 - - InputLayer 输入抽象层文档 - -- **UI 控件文档** (大量缺失) - - P1 控件文档 (12个) - - P2 控件文档 (27个) - - P3 控件文档 (25个) - -- **模拟器和工具** - - Simulator 多平台模拟器文档 - - PerformanceProfile 性能优化文档 - -- **集成和部署** - - 应用打包指南 - - SDK 导出文档 - - 多架构部署文档 - -- **高级主题** - - 性能优化指南 - - 自定义主题开发 - - 国际化支持 - - 无障碍访问 - ---- - -### 1.9 示例代码状态 - 50% 🚧 进行中 - -#### ✅ 已有示例 -- **base 模块** (4个) - - example_policy_chain.cpp - - example_console_helper.cpp - - example_cpu_info.cpp - - example_memory_info.cpp - -- **desktop 模块** (4个) - - config_manager/example_usage.cpp - - logger/example_usage.cpp - - material_gallery (GUI示例) - - 主题相关示例 (4个) - -- **ui 模块** (19个控件演示) - - 所有 P0 控件演示 - - 所有 P1 控件演示 - -#### ❌ 缺失示例 -- **base 模块** - - hash/FNV-1a 哈希算法示例 - - lockfree/无锁队列示例 - - scope_guard 示例 - - singleton 示例 - - span 示例 - - weak_ptr 示例 - - expected 示例 - -- **desktop 模块** - - ascii_art 示例 - - file_operations 示例 - - main 主程序启动流程示例 - -- **ui 模块** - - core/ 核心UI机制示例 - - components/ 通用组件示例 - - models/ 数据模型示例 - ---- - -## 二、技术栈总结 - -| 组件 | 版本/技术 | -|------|----------| -| 语言 | C++23 | -| 框架 | Qt 6.8.3 | -| 构建 | CMake 3.16+ + Ninja | -| 测试 | Google Test | -| 文档 | MkDocs + Doxygen | -| 平台支持 | Windows, Linux, macOS | -| 架构支持 | x86_64, ARM64, ARMhf | - ---- - -## 三、风险识别 - -| 风险 | 级别 | 影响 | 缓解措施 | -|------|------|------|----------| -| UI控件测试覆盖率为0% | 高 | 质量风险 | 为P0/P1控件添加测试 | -| DPI自动检测缺失 | 中 | 用户体验 | 实现DPI自动检测和自适应 | -| ReleaseEarlyInit()未实现 | 中 | 启动流程不完整 | 实现完整初始化链 | -| 布局系统缺失 | 中 | UI功能受限 | 实现Grid/Stack布局 | -| P2/P3 UI控件未实现 | 中 | 功能完整性 | 按需实现 | -| 文档更新滞后 | 中 | 维护困难 | 同步更新文档 | - ---- - -## 四、关键文件路径 - -```text -CFDesktop/ -├── CMakeLists.txt # 主构建配置 (v0.13.1, C++23) -├── base/ # 基础库模块 -│ ├── system/cpu/ # ✅ CPU检测 (100%) -│ ├── system/memory/ # ✅ 内存检测 (95%) -│ ├── system/gpu/ # ✅ GPU检测 (90%) -│ ├── system/network/ # ✅ 网络检测 (85%) -│ ├── system/hardware_tier/ # ✅ HWTier 硬件分级 (100%) -│ ├── device/console/ # ✅ 控制台设备 (85%) -│ └── include/base/policy_chain/ # ✅ 策略链 (80%) -├── ui/ # UI框架 (95%) -│ ├── base/ # ✅ 基础工具 (100%) -│ │ └── device_pixel.h/cpp # ✅ DPI基础转换 (60%) -│ ├── core/ # ✅ 主题核心 (100%) -│ ├── components/ # ⚠️ 动画组件 (90%) -│ └── widget/material/ # ✅ P0/P1控件 (100%) -├── desktop/ # 桌面环境 (90%) -│ ├── main/ # 🚧 主程序入口 (85%) -│ ├── base/logger/ # ✅ 日志系统 (90%) -│ ├── base/config_manager/ # ✅ 配置管理 (85%) -│ ├── base/ascii_art/ # ✅ ASCII艺术 (100%) -│ ├── base/file_operations/ # ⚠️ 文件操作 (70%) -│ └── ui/ # ✅ Desktop UI (90%) -│ ├── components/ # ✅ 显示后端抽象+窗口管理 -│ ├── platform/windows/ # ✅ Windows后端 (100%) -│ ├── platform/linux_wsl/ # ✅ WSL X11后端 (100%) -│ ├── render/ # ⚠️ 渲染后端 (30%, 接口only) -│ └── widget/init_session/ # ✅ 启动Widget (100%) -├── test/ # 测试代码 (55%覆盖率, 24个测试) -├── example/ # 示例程序 (70%覆盖, 80个示例) -└── document/ # 文档 (80%覆盖, 271篇) -```yaml - ---- - -*报告生成时间: 2026-03-30* -*报告版本: v4.0* -*扫描方式: 7个并发Agent全面扫描* -*项目路径: /home/charliechen/CFDesktop* -*项目版本: 0.13.1* diff --git a/document/todo/done/SUMMARY.md b/document/todo/done/SUMMARY.md new file mode 100644 index 000000000..c734c5fb2 --- /dev/null +++ b/document/todo/done/SUMMARY.md @@ -0,0 +1,29 @@ +--- +title: 已完成阶段归档 +description: CFDesktop 各已完成阶段的历史状态汇总。详细报告已归档,当前事实来源请参阅 status/current.md。 +--- + +# 已完成阶段归档 + +各阶段的历史状态报告已归档。当前项目状态的唯一事实来源是 [status/current.md](../../status/current.md)。 + +## 阶段完成汇总 + +| 阶段 | 模块 | 状态 | 归档报告 | +|------|------|------|----------| +| Phase 0 | 工程骨架搭建 | ✅ 完成 | ~~已归档~~ | +| Phase 1 | 硬件探针与能力分级 | ✅ 完成 | ~~已归档~~ | +| Phase 2 | 基础库核心 | ✅ 完成 | ~~已归档~~ | +| Phase A | 基础设施 (CI/CD) | ✅ 完成 | ~~已归档~~ | +| Phase 6 | UI Material 框架 | ✅ 完成 | ~~已归档~~ | +| Phase G | Widget 应用与示例 | ✅ 完成 | ~~已归档~~ | +| Phase H | 显示后端与窗口管理 | ✅ 完成 | ~~已归档~~ | +| Milestone 1 | 桌面骨架可见 | ✅ 完成 | ~~已归档~~ | + +## 进行中 / 待开始阶段 + +参见 [TODO 索引](../index.md) 和 [当前状态](../../status/current.md)。 + +--- + +*归档时间: 2026-05-23* diff --git a/document/todo/done/index.md b/document/todo/done/index.md deleted file mode 100644 index bbb0e7e1a..000000000 --- a/document/todo/done/index.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: done -description: 已完成模块的状态文档索引,本目录记录 CFDesktop 各模块的完成状态和实现细节。 ---- - -# done - -> 已完成模块的状态文档索引 - -## Overview - -本目录记录 CFDesktop 各模块的完成状态和实现细节。 - -## 状态文档列表 - -| 文件 | 模块 | 完成度 | 最后更新 | -|------|------|--------|----------| -| [00_project_skeleton_status.md](00_project_skeleton_status.md) | Phase 0: 工程骨架 | ✅ 100% | 2026-03-11 | -| [01_hardware_probe_status.md](01_hardware_probe_status.md) | Phase 1: 硬件探针 | 🚧 90% | 2026-03-21 | -| [02_base_library_status.md](02_base_library_status.md) | Phase 2: Base库核心 | ✅ 100% | - | -| [03_input_layer_status.md](03_input_layer_status.md) | Phase 3: 输入抽象层 | ⬜ 0% | - | -| [04_simulator_status.md](04_simulator_status.md) | Phase 4: 多平台模拟器 | ⬜ 0% | - | -| [05_testing_status.md](05_testing_status.md) | Phase 5: 测试体系 | 🚧 55% | - | -| [06_infrastructure_status.md](06_infrastructure_status.md) | Phase A: 基础设施 (GPU/Network/Config/Logger) | 🚧 50% | 2026-03-21 | -| [13_widget_apps_status.md](13_widget_apps_status.md) | Phase G: Widget 应用与示例 | ✅ 100% | 2026-03-21 | -| [99_ui_material_framework_status.md](99_ui_material_framework_status.md) | Phase 6: UI Material Framework | 🚧 95% | - | -| [14_display_backend_status.md](14_display_backend_status.md) | Phase H: 显示后端与窗口管理 | 🚧 70% | 2026-03-30 | - -## 综合报告 - -- **[PROJECT_STATUS_REPORT.md](PROJECT_STATUS_REPORT.md)** — v4.0, 项目版本 0.13.1 -- **整体完成度**: ~75% - ---- - -*Last updated: 2026-03-30* diff --git a/document/todo/done/milestone_01_desktop_skeleton.md b/document/todo/done/milestone_01_desktop_skeleton.md deleted file mode 100644 index d3c7c41ea..000000000 --- a/document/todo/done/milestone_01_desktop_skeleton.md +++ /dev/null @@ -1,189 +0,0 @@ ---- -title: "Milestone 1: 桌面骨架可见" -description: "状态: ✅ 已完成 (2026-04-09)" ---- - -# Milestone 1: 桌面骨架可见 - -> **状态**: ✅ 已完成 (2026-04-09) -> **预计周期**: 1-2 天 -> **前置依赖**: 无 (当前基础设施已就绪) -> **目标**: 运行后不再是空白,能看到一个带背景的全屏桌面窗口 - ---- - -## 一、阶段目标 - -启动 CFDesktop 后,ShellLayer 渲染出桌面背景(纯色或壁纸),PanelManager 的可用区域计算正确,桌面主 Widget 正确显示。 - -**完成标志**: `./scripts/run.sh` 启动后看到全屏非空白的桌面画面。 - ---- - -## 二、当前状态分析 - -### 已有基础设施 - -| 组件 | 文件 | 状态 | -|------|------|------| -| `CFDesktop` (主 QWidget) | `desktop/ui/CFDesktop.h` | ✅ 已创建,`register_desktop_resources()` 可注入 PanelManager 和 ShellLayer | -| `CFDesktopEntity` (生命周期) | `desktop/ui/CFDesktopEntity.h/.cpp` | ✅ 单例,`run_init()` 已接入 DisplayBackend | -| `ShellLayer` | `desktop/ui/components/ShellLayer.h` | ✅ 接口+声明完整,**缺 .cpp 实现文件** | -| `IShellLayer` | `desktop/ui/components/IShellLayer.h` | ✅ `setStrategy()` + `geometry()` 接口 | -| `IShellLayerStrategy` | `desktop/ui/components/IShellLayerStrategy.h` | ✅ `activate(layer, wm)` + `deactivate()` + `onGeometryChanged()` | -| `PanelManager` | `desktop/ui/components/PanelManager.h/.cpp` | ✅ 完整实现,`registerPanel()` / `availableGeometry()` | -| `IPanel` | `desktop/ui/components/IPanel.h` | ✅ `position()` / `priority()` / `preferredSize()` / `widget()` | -| `SimpleBootWidget` | `desktop/ui/widget/init_session/simple_boot_widget.h/.cpp` | ✅ 启动画面已完整 | - -### 关键缺口 - -1. **ShellLayer.cpp 不存在** — 只有 `.h` 声明,没有实现 -2. **CFDesktop 没有 paintEvent** — 桌面主窗口不会画任何东西 -3. **没有壁纸/背景渲染逻辑** — IShellLayerStrategy 没有具体实现 -4. **CFDesktopEntity::run_init() 没有创建 ShellLayer** — 只做了 DisplayBackend 初始化 -5. **CFDesktop 没有被 show()** — 桌面主 Widget 可能从未显示 - ---- - -## 三、待实现任务 - -### Day 1: ShellLayer 实现与桌面背景渲染 - -#### Step 1: 创建 ShellLayer.cpp -- [x] 创建文件 `desktop/ui/components/ShellLayer.cpp` -- [x] 实现构造函数 `ShellLayer(QWidget* parent)` - - 设置 `setAttribute(Qt::WA_OpaquePaintEvent)` 减少闪烁 - - 设置 `setAutoFillBackground(false)` - - 接受 parent 的全尺寸 geometry -- [x] 实现 `setStrategy(std::unique_ptr strategy)` - - 若已有旧 strategy,先调用 `deactivate()` - - 存储新 strategy - - 调用 `activate(this->GetWeak(), wm_weak)` 激活 -- [x] 实现 `geometry()` — 返回 `QWidget::geometry()` -- [x] 实现 `onAvailableGeometryChanged(const QRect& rect)` - - 调用 `setGeometry(rect)` 调整 ShellLayer 尺寸 - - 转发给 `strategy_->onGeometryChanged(rect)` - -**参考已有接口**: -- `ShellLayer.h:35` — 类声明 -- `IShellLayer.h:37` — 接口定义 -- `IShellLayerStrategy.h:33` — 策略接口 - -#### Step 2: 创建 DefaultShellStrategy (桌面背景策略) -- [x] 创建文件 `desktop/ui/strategy/default_shell_strategy.h/.cpp` -- [x] 实现 `IShellLayerStrategy` 接口 -- [x] `activate()` 中: - - 持有 ShellLayer 的 weak 引用 - - 在 ShellLayer 上创建背景子 Widget 或直接绘制 -- [x] `onGeometryChanged()` 中: - - 更新背景区域尺寸 - - 触发重绘 -- [x] `deactivate()` 中:清理资源 - -#### Step 3: 为 ShellLayer 添加背景绘制 -- [x] 方案 A (推荐): 在 ShellLayer 的 `paintEvent()` 中绘制纯色背景 - ```cpp - void ShellLayer::paintEvent(QPaintEvent*) { - QPainter p(this); - auto* theme = ThemeManager::instance().currentTheme(); - p.fillRect(rect(), theme->colorScheme().surface()); - } - ``` -- [x] 方案 B: 壁纸图片渲染 - - 在 `DefaultShellStrategy::activate()` 中加载壁纸 QPixmap - - 提供 `WallpaperLayer` 子 Widget 或在 paintEvent 中 `drawPixmap()` -- [x] 选择配色:使用 ThemeManager 的 `colorScheme().surface()` 作为默认背景色 - -**可复用**: `ui/core/theme_manager.h` (ThemeManager 单例)、`ui/core/color_scheme.h` (颜色方案) - -#### Step 4: 修改 CFDesktopEntity::run_init() 连接所有组件 -- [x] 在 `CFDesktopEntity::run_init()` 中(文件 `desktop/ui/CFDesktopEntity.cpp:45`): - ```cpp - // 1. 创建 PanelManager - auto* panel_mgr = new PanelManager(desktop_entity_, desktop_entity_); - - // 2. 创建 ShellLayer - auto* shell = new ShellLayer(desktop_entity_); - - // 3. 注入到 CFDesktop - CFDesktop::InitResources res; - res.panel_manager_ = panel_mgr; - res.shell_layer_ = shell; - desktop_entity_->register_desktop_resources(res); - - // 4. 设置 ShellLayer 策略 - shell->setStrategy(std::make_unique()); - - // 5. 连接 PanelManager geometry 变化到 ShellLayer - connect(panel_mgr, &PanelManager::availableGeometryChanged, - shell, &ShellLayer::onAvailableGeometryChanged); - - // 6. 显示 - desktop_entity_->showFullScreen(); - ``` - -#### Step 5: 确保 CFDesktop 正确接收 geometry -- [x] 在 `CFDesktop::register_desktop_resources()` 中(文件 `desktop/ui/CFDesktop.cpp:21`): - - 验证 `panel_manager_` 和 `shell_layer_` 非空 - - 触发 PanelManager 初始布局计算 - -#### Step 6: 更新 CMakeLists.txt -- [x] 在 `desktop/ui/components/CMakeLists.txt` 中添加 `ShellLayer.cpp` -- [x] 如新建 strategy 目录,确保 CMake 能发现新文件 - ---- - -### Day 2: 验证与调试 - -#### Step 7: 构建并运行 -- [x] `cmake --build build` 确认编译通过 -- [x] 运行后验证: - - [x] 桌面全屏显示 - - [x] 背景非空白(纯色或壁纸) - - [x] 关闭后无 crash / 内存泄漏 - -#### Step 8: 调试布局 -- [x] 在 PanelManager::relayout() 中添加 debug log 输出 `availableGeometry()` -- [x] 确认 ShellLayer 的 geometry 正确跟随屏幕大小 -- [x] 确认后续注册 Panel 后 availableGeometry 会正确缩小 - ---- - -## 四、关键文件清单 - -### 需要修改的文件 -| 文件 | 修改内容 | -|------|----------| -| `desktop/ui/CFDesktopEntity.cpp` | `run_init()` 中创建 ShellLayer、PanelManager、设置策略、show | -| `desktop/ui/components/CMakeLists.txt` | 添加 ShellLayer.cpp | - -### 需要新建的文件 -| 文件 | 内容 | -|------|------| -| `desktop/ui/components/ShellLayer.cpp` | ShellLayer 实现 | -| `desktop/ui/strategy/default_shell_strategy.h` | 默认 Shell 策略头文件 | -| `desktop/ui/strategy/default_shell_strategy.cpp` | 默认 Shell 策略实现 | - -### 参考文件 (只读) -| 文件 | 用途 | -|------|------| -| `desktop/ui/CFDesktop.h:76-81` | `InitResources` 结构体 | -| `desktop/ui/CFDesktop.cpp:21-25` | `register_desktop_resources()` | -| `desktop/ui/components/PanelManager.h` | PanelManager 接口 | -| `desktop/ui/components/IShellLayerStrategy.h` | 策略接口 | -| `desktop/ui/widget/init_session/simple_boot_widget.cpp` | QPainter 绘制参考 | -| `ui/core/theme_manager.h` | ThemeManager 单例 | - ---- - -## 五、验收标准 - -- [x] `./scripts/run.sh` 后看到全屏非空白桌面 -- [x] 桌面背景色跟随 ThemeManager 的 surface 色值 -- [x] PanelManager 的 `availableGeometry()` 返回正确值 (初始为全屏) -- [x] 后续注册 Panel 后 `availableGeometry()` 会自动缩小 -- [x] 关闭后无内存泄漏 - ---- - -*最后更新: 2026-03-31* diff --git a/document/todo/index.md b/document/todo/index.md index 4617709b3..8903b9e18 100644 --- a/document/todo/index.md +++ b/document/todo/index.md @@ -1,22 +1,32 @@ --- -title: CFDesktop 项目 TODO 索引 -description: "- 综合报告: PROJECTSTATUSREPORT.md" +title: CFDesktop 项目 TODO 看板 +description: 本目录包含 CFDesktop 项目各模块的详细 TODO 清单,基于设计文档和架构规范整理而成。 --- -# CFDesktop 项目 TODO 索引 +# CFDesktop 项目 TODO 看板 -## 快速导航 +本目录包含 CFDesktop 项目各模块的详细 TODO 清单,基于 `design_stage` 设计文档和 `MaterialRules.md` 架构规范整理而成。 -| 模块 | TODO 文件 | 参考文档 | 状态 | -|------|----------|----------|------| -| 工程骨架 | ~~已归档~~ | [done/00_project_skeleton_status.md](done/00_project_skeleton_status.md) | ✅ 100% | -| 硬件探针 | ~~已归档~~ | [done/01_hardware_probe_status.md](done/01_hardware_probe_status.md) | 🚧 90% | -| ~~Base库~~ | ~~已归档~~ | [done/02_base_library_status.md](done/02_base_library_status.md) | ✅ 100% | -| 输入层 | [02_input_layer.md](base/02_input_layer.md) | [done/03_input_layer_status.md](done/03_input_layer_status.md) | ⬜ 0% | -| 模拟器 | [03_simulator.md](base/03_simulator.md) | [done/04_simulator_status.md](done/04_simulator_status.md) | ⬜ 0% | -| 测试 | [04_testing.md](base/04_testing.md) | [done/05_testing_status.md](done/05_testing_status.md) | 🚧 55% | -| UI框架 | [99_ui_material_framework.md](base/99_ui_material_framework.md) | [done/99_ui_material_framework_status.md](done/99_ui_material_framework_status.md) | 🚧 95% | -| 显示后端 | desktop/ | [done/14_display_backend_status.md](done/14_display_backend_status.md) | 🚧 70% | +## 模块索引 + +| TODO 文件 | 模块 | 预计周期 | 依赖 | 状态 | +|----------|------|---------|------|------| +| [done/00_project_skeleton_status.md](done/00_project_skeleton_status.md) | 工程骨架搭建 | 1~2 周 | - | ✅ 100% | +| [done/01_hardware_probe_status.md](done/01_hardware_probe_status.md) | 硬件探针与能力分级 | 2~3 周 | Phase 0 | 🚧 90% | +| ~~[done/02_base_library_status.md](done/02_base_library_status.md)~~ | ~~Base 库核心~~ | ~~3~4 周~~ | Phase 0, 1 | ✅ 100% | +| [02_input_layer.md](base/02_input_layer.md) | 输入抽象层 | 1~2 周 | Phase 0, 1 | ⬜ 0% | +| [03_simulator.md](base/03_simulator.md) | 多平台模拟器 | 2~3 周 | Phase 0, 2 | ⬜ 0% | +| [04_testing.md](base/04_testing.md) | 测试体系 | 贯穿全程 | 所有阶段 | 🚧 55% | +| [99_ui_material_framework.md](base/99_ui_material_framework.md) | UI Material Framework | 持续迭代 | Phase 0-3 | 🚧 95% | +| desktop/ | Desktop 模块 (显示后端+窗口管理) | 持续迭代 | Phase 0-6 | 🚧 90% | + +## 状态图例 + +- ⬜ **待开始** (Todo) - 尚未开始的任务 +- 🚧 **进行中** (In Progress) - 正在开发的任务 +- ✅ **已完成** (Done) - 已完成的任务 +- ⚠️ **已废弃** (Deprecated) - 不再需要的任务 +- 🔄 **阻塞中** (Blocked) - 被依赖阻塞的任务 ## 项目状态报告 @@ -24,10 +34,6 @@ description: "- 综合报告: PROJECTSTATUSREPORT.md" - **整体完成度**: 约 75% - **显示后端详情**: [done/14_display_backend_status.md](done/14_display_backend_status.md) — Windows + WSL X11 后端已完成 -## 详细信息 - -请查看 [TODO 索引](./) 获取完整的 TODO 看板信息。 - --- *最后更新: 2026-03-30* diff --git a/scripts/README.md b/scripts/README.md index 319f089d7..4ac046a25 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -162,4 +162,6 @@ bash scripts/develop/remove_trailing_space.sh .\scripts\develop\remove_trailing_space.ps1 ``` +> 完整脚本文档: [document/scripts/](../document/scripts/) + diff --git a/scripts/lib/README.md b/scripts/lib/README.md index 7c82cd842..ad8e3c2d4 100644 --- a/scripts/lib/README.md +++ b/scripts/lib/README.md @@ -366,3 +366,5 @@ eval "$(get_ini_config "$CONFIG_FILE")" | 日志函数重复 | 25 次 | 1 次 | -96% | | 配置解析重复 | 15 次 | 1 次 | -93% | | 总重复代码 | ~2,400 行 | ~600 行 | -75% | + +> 完整库文档: [document/scripts/lib/](../../../document/scripts/lib/) diff --git a/scripts/release/hooks/README.md b/scripts/release/hooks/README.md index 3223d4706..58aaa502f 100644 --- a/scripts/release/hooks/README.md +++ b/scripts/release/hooks/README.md @@ -31,3 +31,5 @@ bash scripts/release/hooks/install_hooks.sh ## 详细文档 完整使用指南请参考: [document/release_rule/git_hooks_guide.md](../../../document/release_rule/git_hooks_guide.md) + +> 完整脚本文档: [document/scripts/release/hooks/](../../../document/scripts/release/hooks/) From ea1df7af4dd3b7c33c012a7120c0314a26a53c0c Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Sun, 24 May 2026 07:34:25 +0800 Subject: [PATCH 04/18] simplified: remove the old documentations --- document/status/current.md | 43 -------------------------------------- document/status/index.md | 10 --------- 2 files changed, 53 deletions(-) delete mode 100644 document/status/current.md delete mode 100644 document/status/index.md diff --git a/document/status/current.md b/document/status/current.md deleted file mode 100644 index d204d3fff..000000000 --- a/document/status/current.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: CFDesktop 当前状态 -description: "最后更新: 2026-05-22,- 本地 可发现 47 个 CTest 测试。" ---- - -# CFDesktop 当前状态 - -> 最后更新: 2026-05-22 - -## 基本信息 - -| 项目 | 当前值 | -|------|--------| -| 版本 | 0.18.0 | -| 语言标准 | C++23 | -| UI 框架 | Qt 6.8.x | -| 构建系统 | CMake + Docker build helpers | -| 文档系统 | VitePress | -| 当前开发分支 | develop | - -## 当前基线 - -- 本地 `out/build_develop/test` 可发现 47 个 CTest 测试。 -- 最近一次本地验证结果: 47/47 通过。 -- VitePress 文档构建命令 `pnpm build` 可通过。 -- 自动生成的 `document/api/**` 暂不纳入 VitePress 主站,避免旧 Doxybook2 生成页影响构建。 - -## 当前主线 - -短期建议优先推进“可见可用桌面闭环”: - -1. 状态栏 -2. 任务栏/导航栏 -3. 应用启动器 -4. 窗口管理可见联动 - -HWTier、InputManager、RenderBackend、Wayland/EGLFS 后端仍然重要,但当前不应阻塞桌面可演示版本。 - -## 已知需要收敛的地方 - -- 历史文档中仍有 `0.13.1`、C++17、MkDocs 等旧描述。 -- `document/todo/done/` 是历史状态归档,不应作为当前事实源。 -- API 自动文档二期需要重新选择发布方式:Doxygen HTML 独立发布,或修复 Doxybook2 Markdown 链接后再纳入主站。 diff --git a/document/status/index.md b/document/status/index.md deleted file mode 100644 index b5259660b..000000000 --- a/document/status/index.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: 项目状态 -description: 本节记录 CFDesktop 的当前事实状态。后续开发和 AI 辅助分析应优先读取这里,而不是旧的历 ---- - -# 项目状态 - -本节记录 CFDesktop 的当前事实状态。后续开发和 AI 辅助分析应优先读取这里,而不是旧的历史状态报告。 - -- [当前状态](current.md) From 77740f0f14d488c72affa048a4309c8ea2af7025 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Mon, 15 Jun 2026 19:12:42 +0800 Subject: [PATCH 05/18] docs: establish multi-layer documentation truth-source architecture Restore a single source of truth for project status and conventions, and fix the broken doc links left by prior "remove old documentations" cleanups. - Restore document/status/current.md as the single progress truth source (qualitative status, no percentages); converge README/CLAUDE.md/todo to it - Add /status slash command (first cognitive/orientation command) - Make AGENT.md the single convention source; CLAUDE.md (@AGENT.md) and AGENTS.md become thin Claude/Codex entry points referencing it - Share the AI hub via git (was gitignored): track .claude/commands/, settings.json, CLAUDE.md, AGENTS.md; keep MEMORY.md/BLUEPRINT.md/ settings.local.json private - Fix ~25 broken doc links (deleted done/*_status.md, missing milestone_01, MaterialRules.md, quick_start path) - Dispatch upper layers (current.md, AGENT.md) to HandBook/scripts as the detail truth source --- .claude/commands/architecture.md | 97 +++++++++ .claude/commands/cross-platform.md | 116 +++++++++++ .claude/commands/docs.md | 108 ++++++++++ .claude/commands/next-step.md | 75 +++++++ .claude/commands/optimize.md | 93 +++++++++ .claude/commands/review.md | 123 +++++++++++ .claude/commands/status.md | 72 +++++++ .claude/commands/testing.md | 129 ++++++++++++ .claude/settings.json | 11 + .gitignore | 9 +- AGENT.md | 193 ++++++++++++++---- AGENTS.md | 28 +++ CLAUDE.md | 16 ++ README.md | 55 +---- document/development/02_quick_start.md | 2 +- document/status/current.md | 66 ++++++ document/todo/base/02_input_layer.md | 4 +- document/todo/base/03_simulator.md | 2 +- document/todo/base/04_testing.md | 4 +- .../todo/base/99_ui_material_framework.md | 2 +- document/todo/desktop/06_infrastructure.md | 6 +- document/todo/desktop/07_render_backend.md | 4 +- document/todo/desktop/08_p1_controls.md | 4 +- document/todo/desktop/09_window_manager.md | 4 +- document/todo/desktop/index.md | 8 +- .../todo/desktop/milestone_00_overview.md | 2 +- .../todo/desktop/milestone_02_status_bar.md | 2 +- document/todo/index.md | 28 ++- 28 files changed, 1136 insertions(+), 127 deletions(-) create mode 100644 .claude/commands/architecture.md create mode 100644 .claude/commands/cross-platform.md create mode 100644 .claude/commands/docs.md create mode 100644 .claude/commands/next-step.md create mode 100644 .claude/commands/optimize.md create mode 100644 .claude/commands/review.md create mode 100644 .claude/commands/status.md create mode 100644 .claude/commands/testing.md create mode 100644 .claude/settings.json create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 document/status/current.md diff --git a/.claude/commands/architecture.md b/.claude/commands/architecture.md new file mode 100644 index 000000000..8eee9f286 --- /dev/null +++ b/.claude/commands/architecture.md @@ -0,0 +1,97 @@ +# /architecture — 架构一致性守护 + +检查代码是否符合 CFDesktop 三层架构规则。 + +## 层级定义 + +### Layer 1: base/ (基础层) +- **路径**: `base/` +- **CMake targets**: `cfbase`, `cfbase_headers`, `cfbase_cpu`, `cfbase_memory`, `cfbase_gpu`, `cfbase_network`, `cfbase_console` +- **允许依赖**: Qt6::Core, OS APIs (POSIX, Win32) +- **职责**: 硬件探测、工具库、平台抽象 +- **禁止**: 不可感知 UI 或桌面环境概念 + +### Layer 2: ui/ (UI 框架层) +- **路径**: `ui/` +- **CMake targets**: `cfui`, `cf_ui_base`, `cf_ui_core`, `cf_ui_widget_material`, `cf_ui_components_material` +- **允许依赖**: Qt6::Core, Qt6::Gui, CFDesktop::base +- **职责**: Material Design 3 组件、主题引擎、动画框架 +- **禁止**: 不可感知桌面环境或窗口管理概念 + +### Layer 3: desktop/ (桌面环境层) +- **路径**: `desktop/` +- **CMake targets**: `CFDesktop_shared`, `CFDesktopMain`, `CFDesktopUi`, `cffilesystem`, `cfconfig`, `cflogger` +- **允许依赖**: Qt6::Core, Qt6::Gui, Qt6::Widgets, cfbase, cflogger, cfui +- **职责**: 桌面环境实现、窗口管理、显示后端、配置管理 + +## 依赖规则 (STRICT) + +``` +base/ → 禁止 include ui/ 或 desktop/ 的任何头文件 +ui/ → 禁止 include desktop/ 的任何头文件 +desktop/ → 可以 include ui/ 和 base/ 的头文件 +``` + +## 检查流程 + +### Step 1: 确定审查范围 + +根据用户指定的模块/文件/目录,确定需要检查的文件列表。 + +### Step 2: 检查 #include 指令 + +对每个源文件和头文件: +1. 提取所有 `#include` 行 +2. 将每个 include 映射到其所属层级 (base/ui/desktop/外部) +3. 标记任何向上依赖(低层级 include 高层级) + +**检测模式**: +- base 层违规: `#include` 匹配 `ui/` 或 `desktop/` 或 `cfui` 或 `CFDesktop` +- ui 层违规: `#include` 匹配 `desktop/` 或 `CFDesktop_shared` + +### Step 3: 检查 CMake 依赖 + +对每个 `CMakeLists.txt`: +1. 检查 `target_link_libraries` 只引用同层或更低层 +2. base: 只允许 Qt 和系统库 +3. ui: Qt + CFDesktop::base +4. desktop: Qt + cfbase + cfui + 自身静态库 + +### Step 4: 检查接口驱动设计 + +- 抽象接口应在 `components/` 或 `include/` 目录 +- 具体实现在 `platform/` 或 `private/` 目录 +- 运行时平台选择使用工厂模式 +- 行为变化使用策略模式 + +### Step 5: 检查平台抽象隔离 + +平台特定代码必须隔离在 `private/_impl/` 目录中: +- `base/system/*/private/linux_impl/` — Linux 实现 +- `base/system/*/private/win_impl/` — Windows 实现 +- `desktop/ui/platform/windows/` — Windows 桌面后端 +- `desktop/ui/platform/linux_wsl/` — Linux/WSL 后端 + +共享代码(ui/core, ui/widget, base/include)中不得出现平台特定代码。 + +## 输出格式 (中文) + +```markdown +# 架构一致性报告: + +## 依赖关系图 +(ASCII 图示实际依赖关系) + +## 违规项 +| 文件 | 行号 | 违规类型 | 说明 | +|------|------|----------|------| + +## 符合项 +(确认观察到的正确模式) + +## 建议修改 +(针对每个违规的具体修复方案) +``` + +如果无违规: +> 架构一致性检查通过,未发现层级依赖违规。 diff --git a/.claude/commands/cross-platform.md b/.claude/commands/cross-platform.md new file mode 100644 index 000000000..d68f0b6bb --- /dev/null +++ b/.claude/commands/cross-platform.md @@ -0,0 +1,116 @@ +# /cross-platform — 跨平台兼容性检查 + +检查代码的跨平台兼容性,确保正确的平台抽象和隔离。 + +## 触发方式 + +- `/cross-platform <模块路径>` — 检查跨平台兼容性 +- Review 流程内部调用 + +## 支持平台 + +| 平台 | API | 编译器 | +|------|-----|--------| +| Windows 10/11 | Win32, DWM | MSVC, MinGW | +| Linux/WSL | POSIX, X11, Wayland | GCC, Clang | +| Embedded ARM | EGLFS, LinuxFB (规划中) | GCC交叉编译 | + +## 检查流程 + +### Step 1: 条件编译检查 + +#### 合法的平台宏 (优先使用) +- `Q_OS_WIN` — Windows 平台 (Qt 宏,**首选**) +- `Q_OS_LINUX` — Linux 平台 (Qt 宏,**首选**) +- `Q_OS_MAC` — macOS 平台 (Qt 宏) +- `_WIN32` / `_MSC_VER` — Windows 特定低层代码(可接受) +- `WIN32` — CMake 级别(可接受) + +#### 禁止的宏 +- `__linux__` — 应使用 `Q_OS_LINUX` +- `__APPLE__` — 应使用 `Q_OS_MAC` +- `#ifdef _WIN64` — 应使用 `Q_OS_WIN` + +#### 路径处理 +- 硬编码路径分隔符 (`"\"` 或 `"/"`) — 应使用 `QDir`/`QFileInfo` +- 硬编码行尾 — 应使用 `QFile` 自动处理 +- 硬编码临时目录 — 应使用 `QStandardPaths` + +### Step 2: 平台抽象目录结构检查 + +验证平台特定代码的隔离模式: + +**正确模式** (base/system/): +``` +base/system/cpu/ +├── cfcpu.h ← 公共头文件(平台无关接口) +├── cfcpu.cpp ← 公共实现(调用 host.h) +└── private/ + ├── cpu_host.h ← 平台选择器 + ├── linux_impl/ ← Linux 实现 + │ └── cpu_linux.cpp + └── win_impl/ ← Windows 实现 + └── cpu_win.cpp +``` + +**正确模式** (desktop/ui/platform/): +``` +desktop/ui/platform/ +├── interface.h ← 平台无关接口 +├── windows/ ← Windows 实现 +└── linux_wsl/ ← Linux/WSL 实现 +``` + +### Step 3: 共享层纯度检查 + +以下位置**禁止**包含平台特定代码: +- `ui/core/` — 主题引擎 +- `ui/widget/` — MD3 控件 +- `ui/components/` — 动画框架 +- `base/include/base/` — 工具头文件 + +平台特定代码**必须**隔离在: +- `base/system/*/private/` +- `desktop/ui/platform/windows/` +- `desktop/ui/platform/linux_wsl/` + +### Step 4: DLL 导出宏检查 + +验证正确的导出宏使用: +- base: `CF_BASE_EXPORT` +- ui: `CF_UI_EXPORT` +- desktop: `CF_DESKTOP_EXPORT` + +每个导出宏应处理 Windows `__declspec` 和 Linux `__attribute__((visibility))`。 + +### Step 5: 输出报告 (中文) + +```markdown +# 跨平台兼容性报告: + +## 条件编译检查 +### 使用的平台宏 +| 文件 | 行号 | 宏 | 合规 | +|------|------|-----|------| + +### 不符合规范的宏 +(需修改的项,含建议替换) + +## 平台抽象层检查 +### 正确隔离的代码 +- ... + +### 泄漏到共享层的平台代码 +- ... + +## 路径处理检查 +- 合规项: ... +- 违规项: ... + +## DLL 导出检查 +- 合规项: ... +- 缺失导出宏: ... + +## 修复建议 +(按优先级排列的具体修改方案) +``` diff --git a/.claude/commands/docs.md b/.claude/commands/docs.md new file mode 100644 index 000000000..ba4acc989 --- /dev/null +++ b/.claude/commands/docs.md @@ -0,0 +1,108 @@ +# /docs — 文档审查与补全 + +审查指定模块的文档准确性,补全缺失文档。 + +## 触发方式 + +- `/docs <模块路径>` — 审查模块文档 +- 用户说"审查/改进 XXX 模块文档" + +## 三大文档源 + +1. **Doxygen 源码注释** — 在 `.h`/`.hpp` 文件中的 `/** */` 或 `///` 注释 +2. **MkDocs 文档** — `document/HandBook/` 和 `document/design_stage/` 目录 +3. **模块 README** — 各模块目录下的 README 文件 + +## 审查流程 + +### Step 1: 确定审查范围 + +- 用户指定模块、文件或目录 +- 列出头文件和源文件清单 +- 识别对应的文档文件(`document/HandBook/` 下) + +### Step 2: Doxygen 合规性检查 + +参考规范:`document/DOXYGEN_REQUEST.md`(权威标准) + +对每个头文件检查: + +#### 文件级检查 +- `@file` 头部存在且路径正确 +- `@brief`, `@author`, `@date`, `@version`, `@since`, `@ingroup` 标签完整 +- 行宽 ≤ 100 字符 + +#### 类型注释检查 +- 每个公共 `class`/`struct`/`enum` 前有 `@brief` +- `@details` 描述生命周期和所有权(如有) +- `@ingroup` 模块归属正确 + +#### 函数注释检查 +- `@brief` 使用第三人称现在时 +- `@param` 包含方向标记 `[in]/[out]/[in,out]` +- `@param` 顺序与函数签名一致 +- 非 void 函数有 `@return`,void 函数**没有** `@return` +- `@throws`, `@note`, `@warning`, `@since`, `@ingroup` 标签存在 +- 模板参数有 `@tparam` + +#### 风格一致性检查 +- 同一文件内统一使用 `/** */` 或 `///` +- 行宽 ≤ 100 字符 +- 无第一人称("we", "I", "our") + +### Step 3: 代码 vs 文档准确性 + +对每个已文档化的 API: +1. `@param` 名称是否匹配函数签名 +2. `@return` 是否匹配实际返回类型 +3. `@throws` 是否匹配实际异常行为 +4. 描述的行为是否匹配实现 +5. 标记所有 `@note FIXME` 条目 + +### Step 4: MkDocs 文档检查 + +1. 验证 `document/` 文件引用的路径是否存在 +2. 架构描述是否匹配当前代码结构 +3. 文档中的代码示例是否可编译且为最新 +4. `document/design_stage/` 的完成度描述是否准确 + +### Step 5: 输出报告 (中文) + +```markdown +# 文档审查报告: + +## Doxygen 合规性 +### 合规文件 +- `path/to/file.h` — 完全合规 + +### 违规文件 +- `path/to/file.h` — 具体违规项: + 1. 缺少文件级 @file 头 + 2. 函数 foo() 缺少 @param 方向标记 + 3. ... + +### 缺失文档的公共 API +- `Class::method()` — 完全无文档 +- `Class::anotherMethod()` — 仅有 @brief,缺少标签 + +## 文档准确性 +### 不一致列表 +| 文件 | API | 文档描述 | 实际行为 | +|------|-----|----------|----------| + +## MkDocs 文档状态 +- 过时内容: ... +- 缺失页面: ... +- 路径失效引用: ... + +## 修复优先级 +1. **[高]** ... +2. **[中]** ... +3. **[低]** ... +``` + +## 集成现有基建 + +- Doxygen 修复遵循 `AGENT.md` 的 5 步流程 +- 验证运行 `python3 scripts/doxygen/lint.py` +- MkDocs 导航结构参考 `mkdocs.yml` diff --git a/.claude/commands/next-step.md b/.claude/commands/next-step.md new file mode 100644 index 000000000..07f8a7356 --- /dev/null +++ b/.claude/commands/next-step.md @@ -0,0 +1,75 @@ +# /next-step — 开发指引 + +根据项目阶段文档和当前进度,推荐下一步开发任务。 + +## 触发方式 + +- `/next-step` — 推荐下一步开发任务 +- 用户说"下一步开发什么" + +## 工作流程 + +### Step 1: 评估当前状态 + +1. 读取 `document/status/current.md`(项目进度唯一事实来源)获取当前进度与下一步路线 +2. 识别哪些 Phase / 模块未完成 + +### Step 2: 映射设计文档 + +对每个未完成阶段,读取对应的设计文档获取具体任务列表: + +| Phase | 设计文档 | +|-------|----------| +| 1 | `document/design_stage/01_phase1_hardware_probe.md` | +| 2 | `document/design_stage/02_phase2_base_library.md` | +| 3 | `document/design_stage/03_phase3_input_layer.md` | +| 6 | `document/design_stage/04_phase6_simulator.md` | +| 8 | `document/design_stage/05_phase8_testing.md` | + +### Step 3: 优先排序 + +按以下顺序推荐: + +1. **阻塞依赖项** — 其他模块依赖此任务才能推进 +2. **接近完成的模块** (90%+) — 收尾效率最高 +3. **当前活跃阶段** — 保持开发连续性 +4. **测试覆盖缺口** — 为已完成的代码补充测试 + +### Step 4: 输出格式 (中文) + +对每个推荐任务: + +```markdown +## 推荐任务 N: <任务名称> + +**优先级**: 高/中/低 +**所属阶段**: Phase X — <阶段名> + +### 目标文件 +- `path/to/file.h` — 修改说明 +- `path/to/file.cpp` — 修改说明 +- `path/to/new_file.h` — 新建说明 + +### 改动要点 +1. ... +2. ... + +### 风险评估 +- **风险等级**: 低/中/高 +- **风险说明**: ... +- **缓解措施**: ... + +### 依赖关系 +- 前置依赖: ... +- 后续可解锁: ... + +### 参考文档 +- `document/design_stage/XX_phaseX.md` — 具体章节 +``` + +## 规则 + +- 推荐前验证目标文件是否存在(避免推荐已完成的任务) +- 不推荐违反层级依赖规则的任务 +- 每次推荐 2-4 个任务,按优先级排序 +- 如果有近完成的模块,优先推荐收尾 diff --git a/.claude/commands/optimize.md b/.claude/commands/optimize.md new file mode 100644 index 000000000..e20b98fed --- /dev/null +++ b/.claude/commands/optimize.md @@ -0,0 +1,93 @@ +# /optimize — 代码优化 + +基于 C++23 和现代 C++ 工程实践,对代码进行逼近零开销的小改动优化。 + +## 触发方式 + +- `/optimize <文件/函数>` — 优化指定代码 +- Review 输出对接 — 用户基于审查报告请求优化 + +## 硬约束 (MUST) + +- 每项优化改动 **1-5 行** +- **禁止**改变公共 API 签名(破坏 ABI) +- **禁止**改变行为(只做优化) +- **禁止**大规模重构 +- **必须**通过 `-Wall -Wextra -Wpedantic` 编译 +- **必须**通过现有测试 +- 每文件每轮最多 **10 项优化** +- 不确定时加 `@note FIXME` 而非猜测 + +## 优化流程 + +### Step 1: 读取目标代码 + +- 读取指定文件完整内容 +- 读取相关头文件和测试文件 +- 理解类层次和使用上下文 + +### Step 2: 优化检查清单 + +#### 2.1 constexpr 提升 +- 编译期可计算的函数 → `constexpr` +- 魔数 → `constexpr` 常量 +- 类型特征和辅助工具 → `constexpr` 优先于模板元编程 +- `if constexpr` 替代运行时分支(类型分发场景) + +#### 2.2 移动语义 +- 按值返回 vs 显式 `std::move`(NRVO 场景不要 move,会阻碍优化) +- Sink 参数用按值传递 + `std::move` 进成员 +- 构造函数中用 `std::move` 初始化成员 +- **不要**对返回局部变量 `std::move`(阻碍 RVO/NRVO) + +#### 2.3 零开销抽象 +- 编译期可确定的多态 → 模板参数替代虚分派 +- 小函数隐式内联(头文件定义) +- CRTP 替代虚分派(静态多态) +- 固定大小容器 → `std::array` 替代 `std::vector` + +#### 2.4 [[nodiscard]] 标注 +- 所有 const getter +- 返回所有权的工厂函数 +- 返回 `expected` 或错误码的函数 +- 忽略返回值会导致 bug 的函数 + +#### 2.5 智能指针优化 +- 独占所有权 → `std::unique_ptr`(零开销) +- 共享所有权 → `std::shared_ptr`(仅在必要时) +- `std::make_unique` / `std::make_shared` 优于裸 `new` +- 观察者模式 → 裸指针/引用,不用智能指针 + +#### 2.6 Qt 特定优化 +- `QString` → 使用 `QStringView` / `qsizetype` +- Signal/Slot → 新式连接 (`&Class::method`) +- 避免 `QString::fromStdString` 不必要转换 +- 像素操作 → `QImage` 优于 `QPixmap` +- `Q_FOREACH` → 范围 for 循环 + +### Step 3: 输出格式 (中文) + +```markdown +# 优化建议: + +## 优化 1: <标题> +- **类别**: constexpr/移动语义/零开销/[[nodiscard]]/智能指针/Qt +- **当前代码**: + ```cpp + // file:line + ``` +- **优化后代码**: + ```cpp + // 修改后 + ``` +- **原理**: ... +- **影响范围**: 低/中 +- **验证方法**: 编译 + 运行测试 + +## 优化 2: ... + +## 总结 +- 优化项数: N +- 预估性能提升: ... +- 风险等级: 整体低/中 +``` diff --git a/.claude/commands/review.md b/.claude/commands/review.md new file mode 100644 index 000000000..6190b34ff --- /dev/null +++ b/.claude/commands/review.md @@ -0,0 +1,123 @@ +# /review — 代码审查 + +对指定模块进行结构化代码审查,输出中文评审报告。 + +## 触发方式 + +- `/review <模块/文件路径>` — 审查指定模块或文件 +- 用户说"审查 XXX 子模块" + +## 审查流程 + +### Step 1: 确认审查范围 + +1. 根据用户指定,搜索相关源文件 (`.h`, `.hpp`, `.cpp`) +2. 列出完整文件清单(含行数) +3. **等待用户确认**后再继续 +4. 如果范围过大(>20 个文件),建议拆分为子模块逐个审查 + +### Step 2: 读取全部代码 + +- 读取范围内所有 `.h`, `.hpp`, `.cpp` 文件 +- 读取对应 `CMakeLists.txt` 了解构建上下文 +- 查找并读取对应测试文件(`test/` 目录下) + +### Step 3: 五维分析 + +#### 3.1 性能关注点 + +- 不必要的拷贝(缺少 `std::move`,大类型按值传递) +- 多余的 `QString`/`QByteArray` 转换 +- 过多的堆分配(优先栈分配、`std::array`、small buffer optimization) +- 虚调度开销(可用静态分派替代的场景) +- Signal/Slot 连接类型(auto vs direct vs queued 的合理性) +- 循环中的重复计算 +- 不必要的 `Q_OBJECT` 宏(非 QObject 子类使用) + +#### 3.2 模块间依赖检查 + +参考 `.claude/commands/architecture.md` 的层级规则: +- base 不得 include ui/desktop +- ui 不得 include desktop +- CMake `target_link_libraries` 依赖方向正确 + +#### 3.3 类级耦合分析 + +| 检查项 | 阈值 | 说明 | +|--------|------|------| +| 继承深度 | > 3 | 标记过深的继承链 | +| friend 使用 | 任何 | 需要合理理由,否则标记 | +| Signal 数量 | > 10 | 过多 Signal 可能是设计异味 | +| 类行数 | > 500 | 考虑拆分为多个类 | +| 公共方法数 | > 30 | 检查是否违反接口隔离原则 | +| 依赖类数量 | > 8 | 高耦合信号 | + +#### 3.4 C++23 现代性 + +- 缺少 `[[nodiscard]]`(const getter、工厂函数、返回 `expected` 的函数) +- 可 `constexpr` 化但未标注的函数和变量 +- 裸指针(可用智能指针或引用替代的场景) +- 缺少 `override`/`final`(重写虚方法时) +- 缺少 `noexcept`(不抛异常的移动构造/赋值) +- 可用 `std::span` 替代指针+长度参数 +- 可用 `using enum` 简化枚举使用 +- 可用结构化绑定简化返回值处理 + +#### 3.5 文档完整性 + +对照 `document/DOXYGEN_REQUEST.md` 规范: +- 未文档化的公共 API +- `@param` 方向标记缺失 +- `@return` 与实际返回类型不匹配 +- 风格不一致(`/** */` 和 `///` 混用) + +### Step 4: 输出报告 (中文) + +```markdown +# 代码审查报告: + +## 审查范围 +| 文件 | 类型 | 行数 | +|------|------|------| +| ... | .h/.cpp | N | + +模块层次位置: base/ui/desktop — Layer N + +## 性能关注点 +1. **[严重/中等/建议]** 文件:行号 — 问题描述 + +## 依赖违规 +(层级违规详情,或 "无违规") + +## 类级耦合分析 +- 继承深度: ... +- friend 使用: ... +- Signal/Slot 合理性: ... +- 类职责评估: ... +- 上帝类检测: ... + +## 现代 C++ 改进建议 +1. **[类别]** 文件:行号 — 具体建议 + +## 文档完整性 +- 缺失文档的公共 API: ... +- Doxygen 标签问题: ... + +## 综合评估 +- 健康度评分: X/10 +- 主要风险: ... +- 优先改进项: + 1. ... + 2. ... + 3. ... + +## 改进指南 +(可对接 /optimize 的具体改进项列表) +``` + +## 规则 + +- 始终读取实际代码,不做假设 +- 引用具体文件路径和行号 +- 区分严重问题、中等问题和建议 +- 报告中的严重问题必须附带修复方案 diff --git a/.claude/commands/status.md b/.claude/commands/status.md new file mode 100644 index 000000000..17fe00c14 --- /dev/null +++ b/.claude/commands/status.md @@ -0,0 +1,72 @@ +# /status — 项目现状快照 + +合成并输出项目当前状态的精炼摘要,让任何人或新 AI 会话 **30 秒看懂**「现在到哪了、下一步干什么」。 + +## 触发方式 + +- `/status` — 输出项目现状快照 +- 用户说「项目现状 / 现在什么进度 / 快速了解项目 / onboarding / 帮我上手」 + +## 工作流程(全部自动合成,禁止臆造) + +### Step 1: 读取进度真相源 + +1. 读取 `document/status/current.md`(项目进度的唯一事实来源) +2. 抽取:当前阶段、三层进度状态、Phase 状态、下一步路线 + +### Step 2: 采集近期变更 + +```bash +git log -10 --oneline # 最近 10 次提交 +git rev-parse --abbrev-ref HEAD # 当前分支 +``` + +### Step 3: 验证架构健康度 + +```bash +grep -r '#include.*\(ui/\|desktop/\)' base/ # 期望 0(base 不可向上依赖) +grep -r '#include.*desktop/' ui/ # 期望 0(ui 不可依赖 desktop) +``` + +- 任一返回非 0 = 三层依赖违规,必须在输出中标注 ⚠️ 并列出违规文件。 + +### Step 4: 合成输出 + +按下方「输出格式」拼装,保持一屏可读。 + +## 输出格式(中文) + +```markdown +# CFDesktop 现状快照 + +**版本 / 分支**: 0.19.0 / +**校准**: + +## 当前阶段 +<取自 current.md「当前阶段」> + +## 进度状态(定性) +### 三层架构 + +### Phase +<取自 current.md> + +## 下一步路线 +<取自 current.md「下一步路线」,列出 MS2-MS5 与首要任务> + +## 最近变更(最近 10 提交) + + +## 架构健康度 +- 三层依赖: ✅ 0 违规 / ❌ N 处违规(列出文件) + +## 新人入口 +<指向 README / CLAUDE.md / current.md 下一步路线> +``` + +## 规则 + +- **数据来源强制**:所有状态必须取自 `document/status/current.md` 或实时 `git` / `grep`,**禁止凭记忆臆造**,**禁止编造百分比**(current.md 本身用定性状态)。 +- **断链报警**:若 `current.md` 导航表指向的文件不存在,必须在输出中以 ⚠️ 显式报警(这正是本命令要守护的「进度漂移 / 指针断裂」问题)。 +- **精炼优先**:一屏可读,不展开 `CLAUDE.md` 的构建命令细节,指向即可。 +- **只读操作**:本命令只读取与合成,不修改任何文件。 diff --git a/.claude/commands/testing.md b/.claude/commands/testing.md new file mode 100644 index 000000000..e7e779dc6 --- /dev/null +++ b/.claude/commands/testing.md @@ -0,0 +1,129 @@ +# /testing — 测试覆盖建议 + +为新增或修改的代码建议测试用例,参考现有测试模式。 + +## 触发方式 + +- `/testing <模块路径>` — 建议测试用例 +- `/next-step` 或 `/optimize` 衔接 + +## 工作流程 + +### Step 1: 分析目标代码 + +1. 读取源文件,识别公共 API 接口: + - 公共方法(public methods) + - 信号和槽(signals/slots) + - 枚举和常量 + - 模板函数/类 + +2. 识别边界情况: + - 空输入、零值、最大值 + - 无效输入、越界访问 + - 资源限制(内存、文件句柄) + +3. 识别线程安全问题: + - 锁自由数据结构 + - 异步操作 + - 共享状态访问 + +### Step 2: 检查现有测试覆盖 + +1. 在 `test///` 查找已有测试文件 +2. 读取已有测试,理解当前覆盖范围 +3. 识别未覆盖的代码路径 + +### Step 3: 参考现有测试模式 + +**文件模式** (`test///_test.cpp`): +```cpp +/** + * @file _test.cpp + * @brief Test coverage for . + */ +#include +#include ".h" + +TEST(Suite, BasicFunctionality) { + // Arrange + // Act + // Assert +} +``` + +**CMake 模式** (`test//CMakeLists.txt`): +```cmake +add_gtest_executable(_test + /_test.cpp + LINK_LIBRARIES GTest::gtest GTest::gtest_main + LABELS "module;unit;component" +) +``` + +### Step 4: 测试建议分级 + +#### P0 — 必须有 (Critical Path) +- 构造函数/析构函数基本功能 +- 主要用例(happy path) +- 错误处理(无效输入) + +#### P1 — 应该有 (Boundary) +- 空输入、零值、最大值 +- Off-by-one 边界 +- 资源限制 + +#### P2 — 可选有 (Integration/Regression) +- Qt 框架交互(Signal/Slot) +- 跨组件交互 +- 线程安全测试 +- 性能回归测试 + +### Step 5: 输出格式 (中文) + +```markdown +# 测试建议: + +## 现有测试状态 +- 已有测试文件: (列出或 "无") +- 覆盖率评估: X% +- 未覆盖的关键路径: ... + +## 建议新增测试 + +### P0 — 必须 +```cpp +// TEST(Suite, ConstructorInitializesCorrectly) +TEST(Suite, ConstructorInitializesCorrectly) { + // Arrange & Act + obj{/* params */}; + // Assert + EXPECT_EQ(obj.(), expected); +} +``` + +### P1 — 应该 +(测试用例骨架) + +### P2 — 可选 +(测试用例骨架) + +## 测试文件位置 +- 建议路径: `test///_test.cpp` +- CMakeLists.txt 修改: +```cmake +add_gtest_executable(_test ...) +``` + +## 预估工作量 +- P0 测试数量: N +- P1 测试数量: N +- P2 测试数量: N +- 预估耗时: X minutes +``` + +## 模块特定注意事项 + +- **base/ 工具库**: header-only,链接 `cfbase_headers` + `GTest` +- **ui/ 控件**: 需要 `QApplication`,链接 `cfui` + `Qt6::Widgets` + `Qt6::Gui` +- **desktop/**: 需要完整 `CFDesktop_shared` + `Qt6::Widgets` +- **Signal/Slot 测试**: 需要 `Qt6::Test`,使用 `QSignalSpy` diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..baef58202 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(bash scripts/build_helpers/linux_configure.sh)", + "Bash(bash scripts/build_helpers/linux_fast_develop_build.sh)", + "Bash(bash scripts/build_helpers/linux_develop_build.sh)", + "Bash(bash scripts/build_helpers/linux_run_tests.sh)", + "Bash(python3 scripts/doxygen/lint.py)" + ] + } +} diff --git a/.gitignore b/.gitignore index e8c058458..a45cf02ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,15 @@ # ignores .cache .ccache/ -.claude -CLAUDE.md + +# AI agent — personal/private config (NOT shared). Shared AI assets +# (AGENT.md / CLAUDE.md / AGENTS.md and .claude/commands/) ARE tracked. +.claude/settings.local.json MEMORY.md +BLUEPRINT.md # aqtinstall log aqtinstall.log -# privates -BLUEPRINT.md # native builds out/ diff --git a/AGENT.md b/AGENT.md index 036484912..43d966680 100644 --- a/AGENT.md +++ b/AGENT.md @@ -1,51 +1,160 @@ -# AGENT.md — Project Conventions for AI Agents +# AGENT.md — CFDesktop Project Conventions for AI Agents -## Build System - -- **Language**: C++23 / CMake (minimum 3.16), Qt 6 -- **Build directory**: `out/build_develop/` -- **Configure**: `bash scripts/build_helpers/linux_configure.sh` -- **Build (no re-configure)**: `bash scripts/build_helpers/linux_fast_develop_build.sh` -- **Build (full clean + build + tests)**: `bash scripts/build_helpers/linux_develop_build.sh` - -## Doxygen Fix Workflow - -### 1. Read the spec - -Read `document/DOXYGEN_REQUEST.md` in full — it is the authoritative Doxygen style guide. - -### 2. Read the violations +> **Single source of truth** for project conventions, shared across all AI agents +> (Claude Code, OpenAI Codex CLI). Tool-specific entry points — `CLAUDE.md`, `AGENTS.md` — +> reference this file and add only tool-specific notes. **Edit conventions here; do not +> duplicate them** in the entry points. -Read `FAILED_DOXYGEN.md` for the current list of violations, grouped by file. +## Project Overview -### 3. Read the linter +- **Name**: CFDesktop — Cross-platform Desktop Environment Framework +- **Version**: 0.19.0 +- **Tech**: C++23 / CMake 3.16+ / Qt 6.8.3 / Material Design 3 +- **Targets**: Windows 10/11, Linux/WSL, Embedded ARM +- **Repo**: https://github.com/Charliechen114514/CFDesktop +- **Current focus / progress**: [`document/status/current.md`](document/status/current.md) — single source of truth for status -Skim `scripts/doxygen/lint.py` to understand the exact checks (file header, function blocks, return tags, param directions, language rules, etc.). +## Architecture: Three-Layer Strict Dependency -### 4. Fix by file - -For each flagged file, read the source, then: - -1. **File header** — add `/** @file ... */` at top if missing (see DOXYGEN_REQUEST.md Section 2). -2. **Type comments** — add `/** @brief ... */` before any undocumented public enum/struct/class. -3. **Function comments** — add a Doxygen block before each flagged function. Key rules: - - Every `@param` needs a direction: `[in]`, `[out]`, or `[in,out]`. - - Non-void functions **must** have `@return`. Void functions **must not**. - - Tags to always include: `@brief`, `@throws` (or `None`), `@note` (or `None`), `@warning` (or `None`), `@since` (`N/A`), `@ingroup` (`none`). -4. **Style consistency** — use `/** */` block style or `///` line style consistently within a file. -5. **Language** — third-person present tense only. No "will", "we", "I", "our", "my". - -### 5. Validate - -```bash -python3 scripts/doxygen/lint.py +``` +desktop/ (Layer 3) → ui/ (Layer 2) → base/ (Layer 1) → Qt/OS API ``` -Iterate up to 3 passes if violations remain. +**Rules (STRICT single-direction):** +- `base/` MUST NOT `#include` from `ui/` or `desktop/` +- `ui/` MUST NOT `#include` from `desktop/` +- `desktop/` MAY `#include` from `ui/` and `base/` +- Verify: `grep -r '#include.*\(ui/\|desktop/\)' base/` must return nothing +- Verify: `grep -r '#include.*desktop/' ui/` must return nothing -## Key Constraints +## Build System -- Only edit Doxygen comments — never change code logic. -- All comments in English. -- Comment lines must be ≤ 100 characters. -- When uncertain about behavior, use `@note FIXME: ...` rather than guessing. +| Action | Command | +|--------|---------| +| Configure | `bash scripts/build_helpers/linux_configure.sh` | +| Fast build | `bash scripts/build_helpers/linux_fast_develop_build.sh` | +| Full build | `bash scripts/build_helpers/linux_develop_build.sh` | +| Run tests | `bash scripts/build_helpers/linux_run_tests.sh` | +| Doxygen lint | `python3 scripts/doxygen/lint.py` | +| Build dir | `out/build_develop/` | + +Windows equivalents use `.ps1` scripts in the same directory. + +## Code Style + +- **Formatter**: LLVM-based `.clang-format`, 100 char line width, 4-space indent +- **Classes**: PascalCase (`ThemeManager`) +- **Methods**: camelCase (`setThemeTo`) +- **Files**: snake_case (`theme_manager.h`) +- **Namespace**: `cf::` for base utilities +- **Export macros**: `CF_BASE_EXPORT`, `CF_UI_EXPORT`, `CF_DESKTOP_EXPORT` +- **Shared libs**: `cfbase` (DLL), `cfui` (DLL), `CFDesktop_shared` (DLL) +- **CMake targets**: `cfbase_*`, `cf_ui_*`, `cf_desktop_*` + +## Module Map + +### base/ → cfbase +Hardware probes and foundation utilities. +- `system/cpu/` — CPU detection (features, freq, temp, usage) +- `system/memory/` — RAM detection (physical, virtual, process) +- `system/gpu/` — GPU detection and capabilities +- `system/network/` — Network interface and connectivity +- `system/hardware_tier/` — Hardware tier assessment (CPU/GPU/Memory/Display scoring, capability flags, policy-based override) +- `device/console/` — Console device abstraction with policy chains +- `include/base/` — Header-only utilities (scope_guard, singleton, factory, weak_ptr, expected, policy_chain) + +### ui/ → cfui +Material Design 3 UI framework (5-layer pipeline): +- Layer 1: Math & Utility (`CFColor`, `GeometryHelper`, `Easing`, `DevicePixel`) +- Layer 2: Theme Engine (`ThemeManager`, `MaterialFactory`, token system) +- Layer 3: Animation Engine (`CFMaterialAnimationFactory`, animation strategies) +- Layer 4: Material Behavior (`StateMachine`, `RippleHelper`, `MdElevationController`) +- Layer 5: Widget Adapter (19 MD3 widgets: Button, TextField, Slider, etc.) + +### desktop/ → CFDesktop_shared +Desktop environment implementation. +- `main/` — DAG-based initialization chain + entry point +- `base/config_manager/` — 4-layer ConfigStore (Temp/App/User/System) with JSON backend +- `base/logger/` — Async multi-sink logging (lock-free MPSC queue) +- `ui/components/` — Core interfaces (`IWindow`, `IDisplayServerBackend`) +- `ui/platform/` — Platform backends (Windows, WSL X11, Wayland planned) + +## Phase System + +Per-phase **progress status** (done / in-progress / not-started) lives in +[`document/status/current.md`](document/status/current.md) — the single source of truth. +This table is only a Phase index (no percentages). + +| Phase | Description | +|-------|-------------| +| 0 | Project skeleton | +| 1 | Hardware probe | +| 2 | Base library | +| 3 | Input abstraction | +| 4 | Multi-platform simulator | +| 6 | UI framework + controls | +| 8 | Testing | + +Reference design docs: `document/design_stage/` (Phase → design-doc mapping in `/next-step`). + +## Doxygen Conventions + +- **Spec**: `document/DOXYGEN_REQUEST.md` — authoritative style guide +- **Linter**: `scripts/doxygen/lint.py` — automated validation +- **Rules**: Third-person present tense, `@param` directions `[in]/[out]/[in,out]`, consistent `/** */` or `///` per file +- **Tags required**: `@brief`, `@param`, `@return`, `@throws`, `@note`, `@warning`, `@since`, `@ingroup` + +### Doxygen Fix Workflow + +1. **Read the spec** — `document/DOXYGEN_REQUEST.md` in full. +2. **Read the violations** — `FAILED_DOXYGEN.md` for the current list, grouped by file. +3. **Read the linter** — skim `scripts/doxygen/lint.py` to understand the exact checks (file header, function blocks, return tags, param directions, language rules). +4. **Fix by file** — for each flagged file: + 1. **File header** — add `/** @file ... */` at top if missing. + 2. **Type comments** — add `/** @brief ... */` before undocumented public enum/struct/class. + 3. **Function comments** — add a Doxygen block before each flagged function: + - Every `@param` needs a direction: `[in]`, `[out]`, or `[in,out]`. + - Non-void functions **must** have `@return`. Void functions **must not**. + - Always include: `@brief`, `@throws` (or `None`), `@note` (or `None`), `@warning` (or `None`), `@since` (`N/A`), `@ingroup` (`none`). + 4. **Style consistency** — use `/** */` or `///` consistently within a file. + 5. **Language** — third-person present tense only. No "will", "we", "I", "our", "my". +5. **Validate** — `python3 scripts/doxygen/lint.py`. Iterate up to 3 passes. + +**Constraints**: Only edit Doxygen comments — never change code logic. All comments in English. Comment lines ≤ 100 chars. When uncertain about behavior, use `@note FIXME: ...` rather than guessing. + +## Testing Conventions + +- **Framework**: GoogleTest v1.14.0 +- **Pattern**: `test///_test.cpp` +- **CMake helper**: `add_gtest_executable()` +- **Labels**: `"module;unit;component"` +- **Qt signal tests**: link `Qt6::Test`, use `QSignalSpy` + +## Slash Commands (Claude Code) + +Reusable workflows live in [`.claude/commands/`](.claude/commands/): + +| Command | Purpose | +|---------|---------| +| `/status` | Project status snapshot (source: `current.md` + git + dependency check) | +| `/next-step` | Recommend next dev task from phase docs | +| `/review` | Code review (performance, coupling, docs) | +| `/optimize` | C++23 zero-overhead optimization | +| `/docs` | Documentation accuracy review | +| `/architecture` | Three-layer dependency guard | +| `/cross-platform` | Platform compatibility check | +| `/testing` | Test coverage suggestions | + +(Codex CLI users see `AGENTS.md` for the equivalent checklist.) + +## Documentation Map + +| Want to know | See | +|---|---| +| Current progress / next steps (single source) | [`document/status/current.md`](document/status/current.md) | +| Project intro (for humans) | [`README.md`](README.md) | +| Component / API usage details (single source) | [`document/HandBook/`](document/HandBook/) | +| Script tooling docs (single source) | [`document/scripts/`](document/scripts/) | +| Per-phase design details | [`document/design_stage/`](document/design_stage/) | +| Module TODO boards | [`document/todo/`](document/todo/) | +| Completed-phase archive | [`document/todo/done/SUMMARY.md`](document/todo/done/SUMMARY.md) | diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..d2ca7e862 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,28 @@ +# AGENTS.md — Codex CLI Entry Point + +> Full project conventions live in [AGENT.md](AGENT.md) — the single source of truth +> shared with Claude Code. This file adds **only** Codex-specific notes; it does **not** +> duplicate conventions. + +## At a glance + +- **Stack**: C++23 / CMake 3.16+ / Qt 6.8 / Material Design 3 (Windows, Linux/WSL, embedded ARM) +- **Configure**: `bash scripts/build_helpers/linux_configure.sh` +- **Fast build**: `bash scripts/build_helpers/linux_fast_develop_build.sh` +- **Full build + tests**: `bash scripts/build_helpers/linux_develop_build.sh` +- **Run tests**: `bash scripts/build_helpers/linux_run_tests.sh` +- **Current progress / next steps**: [`document/status/current.md`](document/status/current.md) + +## Read first + +Before working in this repo, read [AGENT.md](AGENT.md) for: + +- the **three-layer strict dependency** rules (`base` → `ui` → `desktop`) and how to verify them with `grep`, +- the module map, code style, and phase system, +- the **Doxygen conventions** and the 5-step fix workflow, +- testing conventions. + +## Codex-specific + +- Codex has no `.claude/commands/` equivalent. Use the "Slash Commands" table in [AGENT.md](AGENT.md) as a **checklist** of review / optimize / docs workflows to invoke manually when relevant. +- Respect the same dependency and Doxygen constraints documented in AGENT.md — they apply identically here. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..2d2667cd0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,16 @@ +# CLAUDE.md — Claude Code Entry Point + +> Project conventions live in **AGENT.md** — the single source of truth shared with +> Codex CLI. It is imported below. This file adds **only** Claude-specific notes; do not +> duplicate conventions here. + +@AGENT.md + +## Claude-specific + +- **Slash commands**: see `.claude/commands/` (indexed in AGENT.md). Use `/status` for an instant project snapshot, `/next-step` for the next task, `/architecture` to guard the three-layer rules. +- **Settings**: `.claude/settings.json` holds the shared permission allowlist; personal overrides go in `.claude/settings.local.json` (git-ignored). +- **Workflow output language**: Chinese (technical terms remain in English). +- **Code comments**: English, per the Doxygen spec in AGENT.md. +- **Commit messages**: English. +- **Documentation under `document/`**: Chinese with English technical terms. diff --git a/README.md b/README.md index 2ba6ae1c9..012ececde 100644 --- a/README.md +++ b/README.md @@ -33,56 +33,23 @@ --- -## 项目进度 +## AI Agent 支持 + +本项目内置 AI agent 协作支持(**Claude Code** 与 **Codex CLI**),项目约定集中在 [`AGENT.md`](AGENT.md)(单一来源,`CLAUDE.md` / `AGENTS.md` 为各工具入口)。使用 AI 时据此快速理解架构与规范;**不使用 AI 可安全忽略** `AGENT.md` / `CLAUDE.md` / `AGENTS.md` 与 `.claude/` 目录,它们不影响构建。 -### 已完成 ✅ +--- -| 阶段 | 模块 | 完成度 | 说明 | -|:---|:---|:---:|:---| -| Phase 0 | 工程骨架 | 100% | CMake 构建系统、代码规范、CI/CD、Docker 多架构构建 | -| Phase 1 | 硬件探针 | 90% | CPU/Memory/GPU/网络检测完成,缺HWTier/Policy | -| Phase 2 | Base 库核心 | 85% | ConfigStore(85%)、Logger(90%)、DPI基础转换(ui/base)、ASCII Art、File Operations | -| Phase 5 | 测试体系 | 65% | Google Test/CTest 集成,本地基线 47 个测试通过 | -| Phase 6 | UI 框架核心 | 95% | Material Design 3 分层架构 (Layer 1-4 全部完成),缺布局/手势 | -| Phase 6 | P0 核心控件 | 100% | Button, TextField, TextArea, Label, CheckBox, RadioButton, GroupBox | -| Phase 6 | P1 控件 | 100% | Slider, ProgressBar, Switch, ToggleButton, etc. (12个) | -| Desktop | 桌面模块 | 90% | 配置中心、日志系统、启动初始化、文件操作、显示后端架构 | -| Display Backend | Windows+WSL X11 | 70% | Windows 后端(100%)、WSL X11 后端(100%),Wayland/嵌入式待实现 | +## 项目进度 -### 进行中 🚧 +> 项目进度以 [`document/status/current.md`](document/status/current.md) 为唯一事实来源。以下为对外概览,可能滞后,精确状态请查阅该文件。 -| 阶段 | 模块 | 完成度 | 说明 | -|:---|:---|:---:|:---| -| Phase 2 | 配置日志增强 | 80% | DPI基础转换已有(ui/base),缺自动检测/版本控制/迁移/验证/网络日志 | -| Desktop | 渲染后端实现 | 30% | RenderBackend 接口设计完成,具体实现待开发 | +**当前焦点**:跑通「看到桌面 → 点图标 → 开应用」最小闭环。 -### 待开始 ⬜ +- ✅ **base** 硬件探针层(含 HWTier)完成 +- 🚧 **ui / desktop** 核心就绪,最小闭环控件与后端开发中 +- ⬜ 输入抽象、模拟器、Wayland / 嵌入式后端待开始 -| 阶段 | 模块 | 说明 | -|:---|:---|:---| -| Phase 1 | 硬件探针完善 | HWTier 档位、CapabilityPolicy | -| Phase 6 | 布局系统 | Grid、Stack、ConstraintLayout | -| Phase 6 | 手势识别 | 触摸/手势统一接口 | -| Phase 3 | 输入抽象层 | 触摸/按键/旋钮/手势统一接口 | -| Phase 4 | 多平台模拟器 | 开发调试用模拟器、设备配置、DPI 注入 | -| Display Backend | Wayland/嵌入式 | Wayland 合成器后端、EGLFS/LinuxFB 直驱后端 | -| Phase 6 | P2 控件 | 27个高级控件 (DatePicker, MenuBar, Dialog, etc.) | -| Phase 6 | P3 控件 | 25个专业控件 (SplitView, ChartView, etc.) | -| Phase 5 | 测试完善 | desktop 模块、性能基准、UI 自动化 | -| 文档 | VitePress 重排 | 已迁移到 VitePress,API 自动文档二期处理 | - -### 快速统计 (2026-03-27 更新) - -| 类别 | 完成度 | 说明 | -|:---|:---:|:---| -| UI 控件 (P0+P1) | 100% (19个) | 完整实现 | -| UI 控件 (P2+P3) | 0% (52个) | 待开发 | -| 文档覆盖 | 60% | ~268个文档 | -| 示例覆盖 | 50% | ~80个示例 | -| 测试覆盖 | 65% | CTest 47 项通过,P0/P1 Widget 已有基础测试 | - -📋 **完整待办清单**: [document/todo/](document/todo/) -📊 **当前状态**: [document/status/current.md](document/status/current.md) +下一步路线与各 Phase 细节见 [`document/status/current.md`](document/status/current.md),待办清单见 [`document/todo/`](document/todo/)。 --- diff --git a/document/development/02_quick_start.md b/document/development/02_quick_start.md index 234902354..3d49c7ee2 100644 --- a/document/development/02_quick_start.md +++ b/document/development/02_quick_start.md @@ -310,7 +310,7 @@ cat scripts/docker/logger/ci_build_*.log | tail -50 如果遇到本文未涵盖的问题: -1. 查看[设计文档](../../design_stage/)获取详细信息 +1. 查看[设计文档](../design_stage/)获取详细信息 2. 参考[构建系统文档](03_build_system.md) 3. 在 [GitHub](https://github.com/Awesome-Embedded-Learning-Studio/CFDesktop/issues) 上提交 issue diff --git a/document/status/current.md b/document/status/current.md new file mode 100644 index 000000000..1e1e94e42 --- /dev/null +++ b/document/status/current.md @@ -0,0 +1,66 @@ + +--- +title: CFDesktop 当前项目状态 +description: CFDesktop 项目进度的唯一事实来源与全局导航。 +--- + +# CFDesktop 当前项目状态 + +> **校准日期**:2026-06-15 | **版本**:0.19.0 +> **本文件是项目进度的唯一事实来源(single source of truth)。** 其他位置一律指向此处,勿另行手抄。 + +## 项目导航(各信息去哪看) + +| 想了解 | 去看 | +|:---|:---| +| 项目定位 / 对外介绍 | [`README.md`](../../README.md) | +| 架构规则 / 构建命令 / 编码规范 | [`AGENT.md`](../../AGENT.md) | +| 组件 / API 用法(细节真相源) | [`document/HandBook/`](../HandBook/) | +| 脚本工具说明(细节真相源) | [`document/scripts/`](../scripts/) | +| 各 Phase 设计细节 | [`document/design_stage/`](../design_stage/) | +| 各模块待办清单 | [`document/todo/`](../todo/) | +| 已完成阶段历史归档 | [`document/todo/done/SUMMARY.md`](../todo/done/SUMMARY.md) | + +## 当前阶段 + +**一句话**:跑通「看到桌面 → 点图标 → 开应用」最小闭环。 + +## 进度状态(定性) + +### 三层架构 + +- **base**(`cfbase`):✅ 完成。硬件探针(CPU/Memory/GPU/Network) + HWTier 分级 + console + 工具库就绪。 +- **ui**(`cfui`):🚧 进行中。MD3 五层 pipeline + P0/P1 控件(19 个)完成;布局 / 手势 / P2-P3 控件待做。 +- **desktop**(`CFDesktop_shared`):🚧 进行中。DAG 初始化 / 4 层 ConfigStore / 异步 Logger / 窗口骨架就绪;Windows / WSL X11 后端完成;Wayland / 嵌入式待做。 + +### Phase + +- Phase 0 / 1(骨架 / 硬件探针含 HWTier):✅ 完成 +- Phase 2(基础库):🚧 进行中,接近完成 +- Phase 3(输入抽象)/ Phase 4(模拟器):⬜ 待开始 +- Phase 6(UI 框架 + 控件):P0/P1 ✅ · P2/P3 ⬜ +- Phase 8(测试):🚧 进行中 + +## 下一步路线(最小闭环先行) + +1. **MS2 状态栏**(顶部时间 + 系统图标)— ⬜ 待开始 · *首个落地任务*(`IStatusBar` / `PanelManager` / `ThemeEngine` 已就绪) +2. **MS3 任务栏**(底部居中图标条 + hover 动画)— ⬜ 待开始 +3. **MS4 应用启动器**(应用网格 + QProcess 启动)— ⬜ 待开始 +4. **MS5 窗口管理**(窗口装饰 + 任务栏联动)— ⬜ 待开始 + +闭环达成后按需推进:HWTier 策略引擎、CrashHandler、IPC、EGLFS 嵌入式后端、输入抽象层、P2/P3 控件。 + +## 最近里程碑(git 可证) + +- **2026-06**:`refactor: refactor the ui subsystem`;`hwtier system enabled`;文档清理 +- **已达成**:Milestone 1「桌面骨架可见」;Phase 0 / 1 / 2 / A(CI) / 6 / G(Widget) / H(显示后端)(详见 [SUMMARY.md](../todo/done/SUMMARY.md)) + +## 新人入门 + +1. [`README.md`](../../README.md) — 项目定位 +2. [`AGENT.md`](../../AGENT.md) — 构建命令(configure / fast build / run tests) +3. 本文件「下一步路线」— 取首个任务 MS2 上手 diff --git a/document/todo/base/02_input_layer.md b/document/todo/base/02_input_layer.md index aad1d07b4..934bd8702 100644 --- a/document/todo/base/02_input_layer.md +++ b/document/todo/base/02_input_layer.md @@ -209,8 +209,8 @@ description: "预计周期: 1~2 周,依赖阶段: Phase 0, Phase 1, Phase 2" ## 六、相关文档 - 设计文档: [../../design_stage/03_phase3_input_layer.md](../../design_stage/03_phase3_input_layer.md) -- 依赖: [工程骨架状态](../done/00_project_skeleton_status.md), [硬件探针状态](../done/01_hardware_probe_status.md) -- Base库已完成: [done/02_base_library_status.md](../done/02_base_library_status.md) +- 依赖: [工程骨架状态](../done/SUMMARY.md), [硬件探针状态](../done/SUMMARY.md) +- Base库已完成: [done/SUMMARY.md) --- diff --git a/document/todo/base/03_simulator.md b/document/todo/base/03_simulator.md index 77fa73ac5..6b855446c 100644 --- a/document/todo/base/03_simulator.md +++ b/document/todo/base/03_simulator.md @@ -264,7 +264,7 @@ description: "预计周期: 2~3 周,依赖阶段: Phase 0, Phase 2, Phase 3" ## 七、相关文档 - 设计文档: [../../design_stage/04_phase6_simulator.md](../../design_stage/04_phase6_simulator.md) -- 依赖: [工程骨架状态](../done/00_project_skeleton_status.md), [02_input_layer.md](02_input_layer.md) +- 依赖: [工程骨架状态](../done/SUMMARY.md), [02_input_layer.md](02_input_layer.md) --- diff --git a/document/todo/base/04_testing.md b/document/todo/base/04_testing.md index 4f4fcd6ea..f8277067b 100644 --- a/document/todo/base/04_testing.md +++ b/document/todo/base/04_testing.md @@ -9,7 +9,7 @@ description: "预计周期: 贯穿全程,依赖阶段: 所有阶段" > **预计周期**: 贯穿全程 > **依赖阶段**: 所有阶段 > **目标交付物**: 单元测试框架、集成测试、UI 自动化、CI/CD 配置 -> **完成归档**: [document/todo/done/05_testing_status.md](../done/05_testing_status.md) +> **完成归档**: [document/todo/done/SUMMARY.md) --- @@ -237,7 +237,7 @@ description: "预计周期: 贯穿全程,依赖阶段: 所有阶段" - 设计文档: [../../design_stage/05_phase8_testing.md](../../design_stage/05_phase8_testing.md) - 依赖: 所有其他阶段文档 -- 测试状态: [../done/05_testing_status.md](../done/05_testing_status.md) +- 测试状态: [../done/SUMMARY.md) --- diff --git a/document/todo/base/99_ui_material_framework.md b/document/todo/base/99_ui_material_framework.md index 4cb50321c..b3feb272e 100644 --- a/document/todo/base/99_ui_material_framework.md +++ b/document/todo/base/99_ui_material_framework.md @@ -189,7 +189,7 @@ Layer 1: Core Math & Utility Layer (math_helper, color, geometry, ...) ## 五、相关文档 -- 依赖: [工程骨架状态](../done/00_project_skeleton_status.md), [02_input_layer.md](02_input_layer.md) +- 依赖: [工程骨架状态](../done/SUMMARY.md), [02_input_layer.md](02_input_layer.md) --- diff --git a/document/todo/desktop/06_infrastructure.md b/document/todo/desktop/06_infrastructure.md index 43267cd39..a825778bf 100644 --- a/document/todo/desktop/06_infrastructure.md +++ b/document/todo/desktop/06_infrastructure.md @@ -8,14 +8,14 @@ description: "状态: 🚧 部分完成 (~50%),预计周期: 4~5 周" > **状态**: 🚧 部分完成 (~50%) > **预计周期**: 4~5 周 > **依赖阶段**: Phase 1, Phase 2, Phase 3 -> **已完成归档**: [done/06_infrastructure_status.md](../done/06_infrastructure_status.md) +> **已完成归档**: [done/SUMMARY.md) --- ## 已完成模块 > GPU 检测器、Network 检测器、ConfigStore、Logger 已完成。 -> 详细状态请参考: [done/06_infrastructure_status.md](../done/06_infrastructure_status.md) +> 详细状态请参考: [done/SUMMARY.md) --- @@ -277,7 +277,7 @@ description: "状态: 🚧 部分完成 (~50%),预计周期: 4~5 周" ## 五、相关文档 - 设计文档: [../desktop/summary.md](summary.md) Phase A 节 -- 依赖: [硬件探针状态](../done/01_hardware_probe_status.md), [Base库状态](../done/02_base_library_status.md) +- 依赖: [硬件探针状态](../done/SUMMARY.md), [Base库状态](../done/SUMMARY.md) - CMake 配置: `../../cmake/` (参考现有模块集成方式) --- diff --git a/document/todo/desktop/07_render_backend.md b/document/todo/desktop/07_render_backend.md index 49067ebd9..548c516b6 100644 --- a/document/todo/desktop/07_render_backend.md +++ b/document/todo/desktop/07_render_backend.md @@ -8,13 +8,13 @@ description: "状态: 🚧 部分完成 (接口已实现,具体后端待开发 > **状态**: 🚧 部分完成 (接口已实现,具体后端待开发) > **预计周期**: 2 周 > **依赖阶段**: Phase 6 -> **已完成归档**: [done/14_display_backend_status.md](../done/14_display_backend_status.md) +> **已完成归档**: [done/SUMMARY.md) ## 已完成模块 > RenderBackend 抽象接口、BackendCapabilities、RenderBackendFactory 已实现。 > 显示后端架构 (IDisplayServerBackend)、Windows 后端、WSL X11 后端已完成。 -> 详细状态请参考: [done/14_display_backend_status.md](../done/14_display_backend_status.md) +> 详细状态请参考: [done/SUMMARY.md) --- diff --git a/document/todo/desktop/08_p1_controls.md b/document/todo/desktop/08_p1_controls.md index 5114276c8..1f810e631 100644 --- a/document/todo/desktop/08_p1_controls.md +++ b/document/todo/desktop/08_p1_controls.md @@ -7,7 +7,7 @@ description: "完成日期: 2026-03-18,详细完成状态: done/13widgetappsst > **状态**: ✅ 已完成 > **完成日期**: 2026-03-18 -> **详细完成状态**: [done/13_widget_apps_status.md](../done/13_widget_apps_status.md) +> **详细完成状态**: [done/SUMMARY.md) --- @@ -27,7 +27,7 @@ description: "完成日期: 2026-03-18,详细完成状态: done/13widgetappsst ## 相关文档 -- 完成归档: [done/13_widget_apps_status.md](../done/13_widget_apps_status.md) +- 完成归档: [done/SUMMARY.md) - 设计文档: [summary.md](summary.md) Phase C 节 - Material Design 3 规范: https://m3.material.io/ diff --git a/document/todo/desktop/09_window_manager.md b/document/todo/desktop/09_window_manager.md index ab029fe13..51003d9d7 100644 --- a/document/todo/desktop/09_window_manager.md +++ b/document/todo/desktop/09_window_manager.md @@ -8,13 +8,13 @@ description: "状态: 🚧 部分完成,依赖阶段: Phase 6, Phase 7" > **状态**: 🚧 部分完成 > **预计周期**: 3 周 > **依赖阶段**: Phase 6, Phase 7 -> **已完成归档**: [done/14_display_backend_status.md](../done/14_display_backend_status.md) +> **已完成归档**: [done/SUMMARY.md) ## 已完成模块 > 基础 WindowManager (弱引用模式)、基础 IWindow 接口、IWindowBackend 接口已完成。 > Windows 和 WSL X11 平台后端已实现。 -> 详细状态: [done/14_display_backend_status.md](../done/14_display_backend_status.md) +> 详细状态: [done/SUMMARY.md) ### 已实现 - [x] `WindowManager` 窗口注册/查询/关闭/置顶 (弱引用模式) diff --git a/document/todo/desktop/index.md b/document/todo/desktop/index.md index e904ac574..87ae5a614 100644 --- a/document/todo/desktop/index.md +++ b/document/todo/desktop/index.md @@ -33,7 +33,7 @@ description: 桌面本体 (Desktop Shell) 开发规划 | 文件 | 里程碑 | 核心交付 | 状态 | |------|--------|----------|------| | [milestone_00_overview.md](milestone_00_overview.md) | 总览 | 依赖关系 + 可复用资产 + 可跳过模块 | 📋 参考 | -| [milestone_01_desktop_skeleton.md](milestone_01_desktop_skeleton.md) | MS1: 桌面骨架 | 壁纸/背景 + 面板布局 | ⬜ 待开始 | +| [../done/SUMMARY.md](../done/SUMMARY.md) | MS1: 桌面骨架 | 壁纸/背景 + 面板布局 | ⬜ 待开始 | | [milestone_02_status_bar.md](milestone_02_status_bar.md) | MS2: 状态栏 | 顶部时间+系统图标 | ⬜ 待开始 | | [milestone_03_taskbar.md](milestone_03_taskbar.md) | MS3: 任务栏 | 底部居中图标+hover动画 | ⬜ 待开始 | | [milestone_04_app_launcher.md](milestone_04_app_launcher.md) | MS4: 应用启动器 | 弹出应用网格+进程启动 | ⬜ 待开始 | @@ -45,9 +45,9 @@ description: 桌面本体 (Desktop Shell) 开发规划 ## 已完成归档 详见 [../done/](../done/) 目录,特别是: -- [../done/14_display_backend_status.md](../done/14_display_backend_status.md) — 显示后端完成状态 -- [../done/13_widget_apps_status.md](../done/13_widget_apps_status.md) — Widget + 控件完成状态 -- [../done/06_infrastructure_status.md](../done/06_infrastructure_status.md) — 基础设施完成状态 +- [../done/SUMMARY.md) — 显示后端完成状态 +- [../done/SUMMARY.md) — Widget + 控件完成状态 +- [../done/SUMMARY.md) — 基础设施完成状态 --- diff --git a/document/todo/desktop/milestone_00_overview.md b/document/todo/desktop/milestone_00_overview.md index 9ac74fa0d..e08ec7e51 100644 --- a/document/todo/desktop/milestone_00_overview.md +++ b/document/todo/desktop/milestone_00_overview.md @@ -14,7 +14,7 @@ description: "创建日期: 2026-03-31,目标: 将当前空白桌面推进到 | # | 里程碑 | 状态 | 核心交付 | 预计工作量 | |---|--------|------|----------|-----------| -| MS1 | [桌面骨架可见](done/milestone_01_desktop_skeleton.md) | ✅ | 壁纸/背景 + 正确的面板布局计算 | 1-2 天 | +| MS1 | [桌面骨架可见](../done/SUMMARY.md) | ✅ | 壁纸/背景 + 正确的面板布局计算 | 1-2 天 | | MS2 | [状态栏](milestone_02_status_bar.md) | ⬜ | 顶部时间+系统图标面板 | 3-5 天 | | MS3 | [任务栏/导航栏](milestone_03_taskbar.md) | ⬜ | 底部任务栏 (居中图标) | 5-7 天 | | MS4 | [应用启动器](milestone_04_app_launcher.md) | ⬜ | 应用网格弹窗 + 外部进程启动 | 5-7 天 | diff --git a/document/todo/desktop/milestone_02_status_bar.md b/document/todo/desktop/milestone_02_status_bar.md index 6c34bdb1c..e208fb471 100644 --- a/document/todo/desktop/milestone_02_status_bar.md +++ b/document/todo/desktop/milestone_02_status_bar.md @@ -7,7 +7,7 @@ description: "预计周期: 3-5 天,前置依赖: Milestone 1: 桌面骨架可 > **状态**: ⬜ 待开始 > **预计周期**: 3-5 天 -> **前置依赖**: [Milestone 1: 桌面骨架可见](milestone_01_desktop_skeleton.md) +> **前置依赖**: [Milestone 1: 桌面骨架可见](../done/SUMMARY.md) > **目标**: 屏幕顶部出现一条状态栏,显示时间、基础系统图标 --- diff --git a/document/todo/index.md b/document/todo/index.md index 8903b9e18..671f7d3ef 100644 --- a/document/todo/index.md +++ b/document/todo/index.md @@ -5,20 +5,18 @@ description: 本目录包含 CFDesktop 项目各模块的详细 TODO 清单, # CFDesktop 项目 TODO 看板 -本目录包含 CFDesktop 项目各模块的详细 TODO 清单,基于 `design_stage` 设计文档和 `MaterialRules.md` 架构规范整理而成。 +本目录包含 CFDesktop 项目各模块的详细 TODO 清单,基于 [`design_stage`](../design_stage/) 设计文档与 [`AGENT.md`](../../AGENT.md) 架构规范整理而成。 ## 模块索引 -| TODO 文件 | 模块 | 预计周期 | 依赖 | 状态 | -|----------|------|---------|------|------| -| [done/00_project_skeleton_status.md](done/00_project_skeleton_status.md) | 工程骨架搭建 | 1~2 周 | - | ✅ 100% | -| [done/01_hardware_probe_status.md](done/01_hardware_probe_status.md) | 硬件探针与能力分级 | 2~3 周 | Phase 0 | 🚧 90% | -| ~~[done/02_base_library_status.md](done/02_base_library_status.md)~~ | ~~Base 库核心~~ | ~~3~4 周~~ | Phase 0, 1 | ✅ 100% | -| [02_input_layer.md](base/02_input_layer.md) | 输入抽象层 | 1~2 周 | Phase 0, 1 | ⬜ 0% | -| [03_simulator.md](base/03_simulator.md) | 多平台模拟器 | 2~3 周 | Phase 0, 2 | ⬜ 0% | -| [04_testing.md](base/04_testing.md) | 测试体系 | 贯穿全程 | 所有阶段 | 🚧 55% | -| [99_ui_material_framework.md](base/99_ui_material_framework.md) | UI Material Framework | 持续迭代 | Phase 0-3 | 🚧 95% | -| desktop/ | Desktop 模块 (显示后端+窗口管理) | 持续迭代 | Phase 0-6 | 🚧 90% | +| 模块 | 待办清单 | 预计周期 | 依赖 | 状态 | +|------|---------|---------|------|------| +| 工程骨架 / 硬件探针 / Base 库 | [`done/SUMMARY.md`](done/SUMMARY.md)(归档) | — | — | ✅ 完成 | +| 输入抽象层 | [`base/02_input_layer.md`](base/02_input_layer.md) | 1~2 周 | Phase 0, 1 | ⬜ 待开始 | +| 多平台模拟器 | [`base/03_simulator.md`](base/03_simulator.md) | 2~3 周 | Phase 0, 2 | ⬜ 待开始 | +| 测试体系 | [`base/04_testing.md`](base/04_testing.md) | 贯穿全程 | 所有阶段 | 🚧 进行中 | +| UI Material Framework | [`base/99_ui_material_framework.md`](base/99_ui_material_framework.md) | 持续迭代 | Phase 0-3 | 🚧 进行中 | +| Desktop 模块(显示后端 + 窗口管理) | [`desktop/`](desktop/) | 持续迭代 | Phase 0-6 | 🚧 进行中 | ## 状态图例 @@ -30,10 +28,10 @@ description: 本目录包含 CFDesktop 项目各模块的详细 TODO 清单, ## 项目状态报告 -- **综合报告**: [PROJECT_STATUS_REPORT.md](done/PROJECT_STATUS_REPORT.md) -- **整体完成度**: 约 75% -- **显示后端详情**: [done/14_display_backend_status.md](done/14_display_backend_status.md) — Windows + WSL X11 后端已完成 +> 项目进度以 [`../status/current.md`](../status/current.md) 为唯一事实来源。本看板仅维护各模块的待办清单,不再重复声明整体完成度。 + +已完成阶段的归档见 [`done/SUMMARY.md`](done/SUMMARY.md)。 --- -*最后更新: 2026-03-30* +*看板最后更新: 2026-06-15* From f7aa68150ecdf1fb414e3685c75c633bb6fe4504 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Mon, 15 Jun 2026 19:47:22 +0800 Subject: [PATCH 06/18] ci: reusable pre-commit hooks (core.hooksPath) + layer dependency check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Point git core.hooksPath at scripts/release/hooks/ (version-controlled); cmake/install_hooks.cmake sets it at configure time so hooks work right after clone — no manual install_hooks.sh needed - Rename pre-commit.sample/pre-push.sample -> pre-commit/pre-push; CRLF -> LF - Add three-layer dependency check to pre-commit (blocks base->ui/desktop, ui->desktop) — rules were documented in AGENT.md but not enforced on commit - Make PROJECT_ROOT position-independent (git rev-parse --show-toplevel); required after moving hooks from .git/hooks (2 levels deep) to scripts/release/hooks (3 levels) — without it every check silently ran in the wrong cwd and no-op'd - Add .clang-tidy naming config (config-only; not enforced in pre-commit/CI) - Document the full discipline in AGENT.md "Code Quality Discipline" --- .clang-tidy | 87 +++++ AGENT.md | 21 ++ CMakeLists.txt | 3 + cmake/install_hooks.cmake | 20 ++ scripts/release/hooks/install_hooks.sh | 14 +- .../hooks/{pre-commit.sample => pre-commit} | 308 ++++++++++-------- .../hooks/{pre-push.sample => pre-push} | 4 +- 7 files changed, 314 insertions(+), 143 deletions(-) create mode 100644 .clang-tidy create mode 100644 cmake/install_hooks.cmake rename scripts/release/hooks/{pre-commit.sample => pre-commit} (74%) mode change 100644 => 100755 rename scripts/release/hooks/{pre-push.sample => pre-push} (95%) mode change 100644 => 100755 diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 000000000..fe9d38414 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,87 @@ +# .clang-tidy — naming & style checks for clangd / manual clang-tidy. +# +# Status: CONFIG-ONLY. Not run in pre-commit or CI (see AGENT.md "Code Quality +# Discipline"). clangd reads this automatically for in-editor hints; to silence +# naming noise temporarily, comment out `readability-identifier-naming` in Checks. +# Naming rules mirror AGENT.md "Code Style": PascalCase types, camelCase methods, +# snake_case files/variables, UPPER_CASE constants, lower_case namespaces. +--- +Checks: > + -*, + readability-identifier-naming, + bugprone-assert-side-effect, + bugprone-bool-pointer-implicit-conversion, + bugprone-copy-constructor-init, + bugprone-dangling-handle, + bugprone-fold-init-type, + bugprone-forwarding-reference-overload, + bugprone-inaccurate-cast, + bugprone-incorrect-roundings, + bugprone-infinite-loop, + bugprone-integer-division, + bugprone-misplaced-widening-cast, + bugprone-move-forwarding-reference, + bugprone-multiple-statement-macro, + bugprone-string-constructor, + bugprone-suspicious-enum-usage, + bugprone-suspicious-memset-usage, + bugprone-undefined-memory-manipulation, + bugprone-use-after-move, + bugprone-virtual-near-miss, + modernize-deprecated-headers, + modernize-loop-convert, + modernize-make-shared, + modernize-make-unique, + modernize-pass-by-value, + modernize-redundant-void-arg, + modernize-replace-auto-ptr, + modernize-replace-random-shuffle, + modernize-return-braced-init-list, + modernize-shrink-to-fit, + modernize-unary-static-assert, + modernize-use-bool-literals, + modernize-use-default-member-init, + modernize-use-emplace, + modernize-use-equals-default, + modernize-use-equals-delete, + modernize-use-noexcept, + modernize-use-nullptr, + modernize-use-override, + modernize-use-transparent-functors, + modernize-use-using, + readability-braces-around-statements, + readability-container-size-empty, + readability-delete-null-pointer, + readability-else-after-return, + readability-function-size, + readability-non-const-parameter, + readability-qualified-auto, + readability-redundant-control-flow, + readability-redundant-string-cstr, + readability-redundant-string-init, + readability-simplify-boolean-expr, + readability-static-definition-in-anonymous-namespace, + readability-string-compare, + readability-uniqueptr-delete-release, + readability-uppercase-literal-suffix, +WarningsAsErrors: '' +HeaderFilterRegex: '.*' +CheckOptions: + - { key: readability-identifier-naming.ClassCase, value: CamelCase } + - { key: readability-identifier-naming.StructCase, value: CamelCase } + - { key: readability-identifier-naming.UnionCase, value: CamelCase } + - { key: readability-identifier-naming.EnumCase, value: CamelCase } + - { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE } + - { key: readability-identifier-naming.TypedefCase, value: CamelCase } + - { key: readability-identifier-naming.TypeAliasCase, value: CamelCase } + - { key: readability-identifier-naming.FunctionCase, value: camelBack } + - { key: readability-identifier-naming.MethodCase, value: camelBack } + - { key: readability-identifier-naming.ParameterCase, value: camelBack } + - { key: readability-identifier-naming.VariableCase, value: camelBack } + - { key: readability-identifier-naming.PrivateMemberSuffix, value: _ } + - { key: readability-identifier-naming.PrivateMemberCase, value: camelBack } + - { key: readability-identifier-naming.ConstantCase, value: UPPER_CASE } + - { key: readability-identifier-naming.ConstantPrefix, value: k } + - { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE } + - { key: readability-identifier-naming.NamespaceCase, value: lower_case } + - { key: readability-identifier-naming.FileIgnoringCase, value: lower_case } diff --git a/AGENT.md b/AGENT.md index 43d966680..6b1db4b27 100644 --- a/AGENT.md +++ b/AGENT.md @@ -130,6 +130,27 @@ Reference design docs: `document/design_stage/` (Phase → design-doc mapping in - **Labels**: `"module;unit;component"` - **Qt signal tests**: link `Qt6::Test`, use `QSignalSpy` +## Code Quality Discipline + +Automated checks enforce the conventions above. They run on commit via the +pre-commit hook and in-editor via clangd. + +| Concern | Authority | Enforced by | +|---|---|---| +| Code format | [`.clang-format`](.clang-format) | pre-commit (clang-format, auto-formats staged files) | +| Doxygen comments | [`document/DOXYGEN_REQUEST.md`](document/DOXYGEN_REQUEST.md) | pre-commit (`scripts/doxygen/lint.py`) | +| Three-layer dependency | Architecture rules above | pre-commit (blocks `base→ui/desktop`, `ui→desktop`) | +| Naming | [`.clang-tidy`](.clang-tidy) | clangd / manual (config-only; **not** in pre-commit/CI) | + +**Hook setup**: `git config core.hooksPath scripts/release/hooks` is set +**automatically** at configure time by `cmake/install_hooks.cmake` — no manual +install needed after `clone`. Hooks live in [`scripts/release/hooks/`](scripts/release/hooks/) +(version-controlled). Bypass with `git commit --no-verify`. + +**HandBook sync (manual discipline)**: when changing a public API or component, +update the corresponding page in [`document/HandBook/`](document/HandBook/) (the +detail truth source). Not automated — a maintainer responsibility. + ## Slash Commands (Claude Code) Reusable workflows live in [`.claude/commands/`](.claude/commands/): diff --git a/CMakeLists.txt b/CMakeLists.txt index bae2d248e..88fb969fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,9 @@ project(CFDesktop # ============================================================ include(cmake/check_pre_configure.cmake) +# Configure git hooks (core.hooksPath) so pre-commit/pre-push work after configure +include(cmake/install_hooks.cmake) + # ============================================================ # Generate Meta Information Headers # ============================================================ diff --git a/cmake/install_hooks.cmake b/cmake/install_hooks.cmake new file mode 100644 index 000000000..c814c5b46 --- /dev/null +++ b/cmake/install_hooks.cmake @@ -0,0 +1,20 @@ +# cmake/install_hooks.cmake +# ============================================================================= +# Configure-time: point `git core.hooksPath` at the version-controlled hooks +# directory so pre-commit / pre-push take effect right after `cmake configure`. +# No manual `install_hooks.sh` needed — the hooks live in the repo and git runs +# them directly. Idempotent and non-fatal (skips if not a git repo). +# ============================================================================= +if(EXISTS "${CMAKE_SOURCE_DIR}/.git") + execute_process( + COMMAND git config core.hooksPath scripts/release/hooks + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + RESULT_VARIABLE _cf_hooks_result + OUTPUT_QUIET ERROR_QUIET + ) + if(_cf_hooks_result EQUAL 0) + message(STATUS "Git hooks: core.hooksPath -> scripts/release/hooks (auto-configured)") + else() + message(STATUS "Git hooks: skipped (git config unavailable; non-fatal)") + endif() +endif() diff --git a/scripts/release/hooks/install_hooks.sh b/scripts/release/hooks/install_hooks.sh index b79cda9d7..3e6dc0b11 100755 --- a/scripts/release/hooks/install_hooks.sh +++ b/scripts/release/hooks/install_hooks.sh @@ -5,6 +5,8 @@ # # 用法: bash scripts/release/hooks/install_hooks.sh # +# 推荐:直接 `cmake configure` 即自动设置 core.hooksPath,无需本脚本。 +# 本脚本为 fallback(复制钩子到 .git/hooks/),仅用于不跑 cmake 的场景。 # 此脚本将钩子安装到 .git/hooks/ 目录 # ============================================================================= @@ -131,17 +133,17 @@ log_info "安装 Git Hooks..." echo "" # 安装 pre-commit -if [[ -f "$HOOKS_SOURCE_DIR/pre-commit.sample" ]]; then - install_hook "pre-commit.sample" "pre-commit" +if [[ -f "$HOOKS_SOURCE_DIR/pre-commit" ]]; then + install_hook "pre-commit" "pre-commit" else - log_warning "pre-commit.sample 不存在,跳过" + log_warning "pre-commit 不存在,跳过" fi # 安装 pre-push -if [[ -f "$HOOKS_SOURCE_DIR/pre-push.sample" ]]; then - install_hook "pre-push.sample" "pre-push" +if [[ -f "$HOOKS_SOURCE_DIR/pre-push" ]]; then + install_hook "pre-push" "pre-push" else - log_warning "pre-push.sample 不存在,跳过" + log_warning "pre-push 不存在,跳过" fi # ============================================================================= diff --git a/scripts/release/hooks/pre-commit.sample b/scripts/release/hooks/pre-commit old mode 100644 new mode 100755 similarity index 74% rename from scripts/release/hooks/pre-commit.sample rename to scripts/release/hooks/pre-commit index 804e17f5d..0684bf52b --- a/scripts/release/hooks/pre-commit.sample +++ b/scripts/release/hooks/pre-commit @@ -1,136 +1,172 @@ -#!/bin/bash -# ============================================================================= -# Git Pre-Commit Hook - 本地代码质量检查 -# ============================================================================= -# -# 安装方法: bash scripts/release/hooks/install_hooks.sh -# -# 检查内容: -# 1. C++代码格式(需要clang-format) -# 2. Doxygen注释检查(需要Python) -# -# 绕过方法: git commit --no-verify -m "message" -# ============================================================================= - -# ============================================================================= -# 颜色定义 -# ============================================================================= -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -# ============================================================================= -# 日志函数 -# ============================================================================= -log_info() { - echo -e "${BLUE}>>>${NC} $1" -} - -log_success() { - echo -e "${GREEN}✓${NC} $1" -} - -log_warning() { - echo -e "${YELLOW}⚠${NC} $1" -} - -log_error() { - echo -e "${RED}✗${NC} $1" -} - -# ============================================================================= -# 获取项目根目录 -# ============================================================================= -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" - -# ============================================================================= -# 1. C++ 格式化(如果 clang-format 可用) -# ============================================================================= -if command -v clang-format >/dev/null 2>&1; then - cd "$PROJECT_ROOT" || exit 1 - - # 获取暂存的 C/C++ 文件 - STAGED_CPP_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(cpp|cc|cxx|h|hpp|hxx)$' || true) - - if [ -n "$STAGED_CPP_FILES" ]; then - log_info "格式化暂存的 C/C++ 文件..." - - # 对每个暂存的 C/C++ 文件运行 clang-format - echo "$STAGED_CPP_FILES" | while IFS= read -r file; do - [ -z "$file" ] && continue - [ ! -f "$file" ] && continue - - # 格式化文件 - clang-format -i "$file" 2>/dev/null || true - done - - # 重新添加格式化后的文件到暂存区 - echo "$STAGED_CPP_FILES" | while IFS= read -r file; do - [ -z "$file" ] && continue - git add "$file" 2>/dev/null || true - done - - log_success "C++ 文件已格式化" - fi -fi - -# ============================================================================= -# 2. Doxygen 注释检查(如果 Python 可用) -# ============================================================================= -PYTHON_CMD="" -if command -v python3 >/dev/null 2>&1; then - PYTHON_CMD="python3" -elif command -v python >/dev/null 2>&1; then - PYTHON_CMD="python" -fi - -if [ -n "$PYTHON_CMD" ]; then - cd "$PROJECT_ROOT" || exit 1 - - # 检查是否有暂存的头文件 - STAGED_HEADER_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(h|hpp|hxx)$' || true) - - if [ -n "$STAGED_HEADER_FILES" ]; then - log_info "检查 Doxygen 注释..." - - # 运行 doxygen lint - # 捕获输出和退出码 - LINT_OUTPUT=$("$PYTHON_CMD" "$PROJECT_ROOT/scripts/doxygen/lint.py" 2>&1) - LINT_EXIT_CODE=$? - - if [ $LINT_EXIT_CODE -ne 0 ]; then - # 检查输出中是否包含 "FAILED" 字样来确认真正的失败 - if echo "$LINT_OUTPUT" | grep -q "FAILED"; then - log_error "Doxygen 注释检查失败" - echo "" - echo "发现的违规已写入: FAILED_DOXYGEN.md" - echo "" - echo "修复方法:" - echo " 1. 查看 FAILED_DOXYGEN.md 了解详细问题" - echo " 2. 参考 document/DOXYGEN_REQUEST.md 添加/修复 Doxygen 注释" - echo "" - echo -e "${YELLOW}提示: 使用 --no-verify 可跳过此检查(不推荐)${NC}" - exit 1 - else - # 输出中有 "All Doxygen checks passed" 但退出码非零 - # 这可能是 Windows + Git bash 的兼容性问题,忽略 - log_success "Doxygen 注释检查通过" - fi - else - log_success "Doxygen 注释检查通过" - fi - fi -else - log_warning "未找到 Python,跳过 Doxygen 注释检查" -fi - -# ============================================================================= -# 完成 -# ============================================================================= -echo "" -log_success "Pre-Commit 检查全部通过" -echo "" -exit 0 +#!/bin/bash +# ============================================================================= +# Git Pre-Commit Hook - 本地代码质量检查 +# ============================================================================= +# +# 安装方法: bash scripts/release/hooks/install_hooks.sh +# +# 检查内容: +# 1. C++代码格式(需要clang-format) +# 2. Doxygen注释检查(需要Python) +# +# 绕过方法: git commit --no-verify -m "message" +# ============================================================================= + +# ============================================================================= +# 颜色定义 +# ============================================================================= +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# ============================================================================= +# 日志函数 +# ============================================================================= +log_info() { + echo -e "${BLUE}>>>${NC} $1" +} + +log_success() { + echo -e "${GREEN}✓${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}⚠${NC} $1" +} + +log_error() { + echo -e "${RED}✗${NC} $1" +} + +# ============================================================================= +# 获取项目根目录 +# ============================================================================= +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Position-independent: works under core.hooksPath (scripts/release/hooks/) or .git/hooks/ +PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" +[ -z "$PROJECT_ROOT" ] && PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# ============================================================================= +# 1. C++ 格式化(如果 clang-format 可用) +# ============================================================================= +if command -v clang-format >/dev/null 2>&1; then + cd "$PROJECT_ROOT" || exit 1 + + # 获取暂存的 C/C++ 文件 + STAGED_CPP_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(cpp|cc|cxx|h|hpp|hxx)$' || true) + + if [ -n "$STAGED_CPP_FILES" ]; then + log_info "格式化暂存的 C/C++ 文件..." + + # 对每个暂存的 C/C++ 文件运行 clang-format + echo "$STAGED_CPP_FILES" | while IFS= read -r file; do + [ -z "$file" ] && continue + [ ! -f "$file" ] && continue + + # 格式化文件 + clang-format -i "$file" 2>/dev/null || true + done + + # 重新添加格式化后的文件到暂存区 + echo "$STAGED_CPP_FILES" | while IFS= read -r file; do + [ -z "$file" ] && continue + git add "$file" 2>/dev/null || true + done + + log_success "C++ 文件已格式化" + fi +fi + +# ============================================================================= +# 2. Doxygen 注释检查(如果 Python 可用) +# ============================================================================= +PYTHON_CMD="" +if command -v python3 >/dev/null 2>&1; then + PYTHON_CMD="python3" +elif command -v python >/dev/null 2>&1; then + PYTHON_CMD="python" +fi + +if [ -n "$PYTHON_CMD" ]; then + cd "$PROJECT_ROOT" || exit 1 + + # 检查是否有暂存的头文件 + STAGED_HEADER_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(h|hpp|hxx)$' || true) + + if [ -n "$STAGED_HEADER_FILES" ]; then + log_info "检查 Doxygen 注释..." + + # 运行 doxygen lint + # 捕获输出和退出码 + LINT_OUTPUT=$("$PYTHON_CMD" "$PROJECT_ROOT/scripts/doxygen/lint.py" 2>&1) + LINT_EXIT_CODE=$? + + if [ $LINT_EXIT_CODE -ne 0 ]; then + # 检查输出中是否包含 "FAILED" 字样来确认真正的失败 + if echo "$LINT_OUTPUT" | grep -q "FAILED"; then + log_error "Doxygen 注释检查失败" + echo "" + echo "发现的违规已写入: FAILED_DOXYGEN.md" + echo "" + echo "修复方法:" + echo " 1. 查看 FAILED_DOXYGEN.md 了解详细问题" + echo " 2. 参考 document/DOXYGEN_REQUEST.md 添加/修复 Doxygen 注释" + echo "" + echo -e "${YELLOW}提示: 使用 --no-verify 可跳过此检查(不推荐)${NC}" + exit 1 + else + # 输出中有 "All Doxygen checks passed" 但退出码非零 + # 这可能是 Windows + Git bash 的兼容性问题,忽略 + log_success "Doxygen 注释检查通过" + fi + else + log_success "Doxygen 注释检查通过" + fi + fi +else + log_warning "未找到 Python,跳过 Doxygen 注释检查" +fi + +# ============================================================================= +# 3. 三层依赖检查(base 不可依赖 ui/desktop;ui 不可依赖 desktop) +# ============================================================================= +cd "$PROJECT_ROOT" || exit 1 +log_info "检查三层依赖..." +LAYER_ERR=0 +while IFS= read -r file; do + [ -z "$file" ] && continue + [ ! -f "$file" ] && continue + case "$file" in + base/*) + if grep -nE '#include[[:space:]]+[<"]?(ui/|desktop/)' "$file" >/dev/null 2>&1; then + log_error "base 层违规: $file 引用了 ui/ 或 desktop/" + grep -nE '#include[[:space:]]+[<"]?(ui/|desktop/)' "$file" + LAYER_ERR=1 + fi + ;; + ui/*) + if grep -nE '#include[[:space:]]+[<"]?desktop/' "$file" >/dev/null 2>&1; then + log_error "ui 层违规: $file 引用了 desktop/" + grep -nE '#include[[:space:]]+[<"]?desktop/' "$file" + LAYER_ERR=1 + fi + ;; + esac +done <<< "$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(cpp|cc|cxx|h|hpp|hxx)$' || true)" +if [ "$LAYER_ERR" -ne 0 ]; then + echo "" + echo "三层依赖规则: base ↛ ui/desktop, ui ↛ desktop(见 AGENT.md Architecture)" + echo "提示: 使用 --no-verify 可跳过(不推荐)" + exit 1 +fi +log_success "三层依赖检查通过" + +# ============================================================================= +# 完成 +# ============================================================================= +echo "" +log_success "Pre-Commit 检查全部通过" +echo "" +exit 0 diff --git a/scripts/release/hooks/pre-push.sample b/scripts/release/hooks/pre-push old mode 100644 new mode 100755 similarity index 95% rename from scripts/release/hooks/pre-push.sample rename to scripts/release/hooks/pre-push index 1cfe14dfc..6ec88b891 --- a/scripts/release/hooks/pre-push.sample +++ b/scripts/release/hooks/pre-push @@ -44,7 +44,9 @@ log_error() { # Project root # ============================================================================= SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +# Position-independent: works under core.hooksPath (scripts/release/hooks/) or .git/hooks/ +PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" +[ -z "$PROJECT_ROOT" ] && PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" cd "$PROJECT_ROOT" # ============================================================================= From d6388a2e859e6bd9bfe2b78b888502d70d06e4c6 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Mon, 15 Jun 2026 19:57:25 +0800 Subject: [PATCH 07/18] fix: align hook install scripts with renamed hooks - install_hooks.ps1: reference pre-commit/pre-push directly (was *.sample, which no longer exists after the core.hooksPath rename); add core.hooksPath recommendation in header - install_hooks.sh: rename local sample_name -> source_name (hooks are no longer .sample files) --- scripts/release/hooks/install_hooks.ps1 | 6 ++++-- scripts/release/hooks/install_hooks.sh | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/release/hooks/install_hooks.ps1 b/scripts/release/hooks/install_hooks.ps1 index a6c2a27c4..950dc127a 100644 --- a/scripts/release/hooks/install_hooks.ps1 +++ b/scripts/release/hooks/install_hooks.ps1 @@ -4,6 +4,8 @@ # # 用法: .\scripts\release\hooks\install_hooks.ps1 # +# 推荐:直接 cmake configure 即自动设置 core.hooksPath,无需本脚本。 +# 本脚本为 fallback(复制钩子到 .git/hooks/),仅用于不跑 cmake 的场景。 # 此脚本将钩子安装到 .git/hooks/ 目录 # ============================================================================= @@ -155,10 +157,10 @@ Log-Info "安装 Git Hooks..." Write-Host "" # 安装 pre-commit -Install-Hook "pre-commit.sample" "pre-commit" +Install-Hook "pre-commit" "pre-commit" # 安装 pre-push -Install-Hook "pre-push.sample" "pre-push" +Install-Hook "pre-push" "pre-push" # ============================================================================= # 完成 diff --git a/scripts/release/hooks/install_hooks.sh b/scripts/release/hooks/install_hooks.sh index 3e6dc0b11..d3ef8aac7 100755 --- a/scripts/release/hooks/install_hooks.sh +++ b/scripts/release/hooks/install_hooks.sh @@ -111,9 +111,9 @@ backup_existing_hooks() { # 安装单个钩子 # ============================================================================= install_hook() { - local sample_name="$1" + local source_name="$1" local hook_name="$2" - local source_hook="$HOOKS_SOURCE_DIR/$sample_name" + local source_hook="$HOOKS_SOURCE_DIR/$source_name" local target_hook="$HOOKS_TARGET_DIR/$hook_name" # 备份现有钩子 From 3ebe63098319f74f2111d1a78be984e04cbc92d5 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Mon, 15 Jun 2026 20:11:56 +0800 Subject: [PATCH 08/18] docs: finalize naming & Doxygen conventions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .clang-tidy: fix naming to match the actual codebase — private members use the m_ prefix (was wrongly configured as _ suffix); constants are plain UPPER_CASE (drop the invented k prefix) - AGENT.md: document the deliberate Doxygen two-tier model — lint.py is the enforced floor (codebase passes), DOXYGEN_REQUEST.md is the aspirational target (class @code, @author/@date) that does not block commits --- .clang-tidy | 5 ++--- AGENT.md | 5 +++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index fe9d38414..aa1a60524 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -78,10 +78,9 @@ CheckOptions: - { key: readability-identifier-naming.MethodCase, value: camelBack } - { key: readability-identifier-naming.ParameterCase, value: camelBack } - { key: readability-identifier-naming.VariableCase, value: camelBack } - - { key: readability-identifier-naming.PrivateMemberSuffix, value: _ } + - { key: readability-identifier-naming.PrivateMemberPrefix, value: m_ } - { key: readability-identifier-naming.PrivateMemberCase, value: camelBack } - - { key: readability-identifier-naming.ConstantCase, value: UPPER_CASE } - - { key: readability-identifier-naming.ConstantPrefix, value: k } + - { key: readability-identifier-naming.ConstantCase, value: UPPER_CASE } - { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE } - { key: readability-identifier-naming.NamespaceCase, value: lower_case } - { key: readability-identifier-naming.FileIgnoringCase, value: lower_case } diff --git a/AGENT.md b/AGENT.md index 6b1db4b27..17135cb9f 100644 --- a/AGENT.md +++ b/AGENT.md @@ -142,6 +142,11 @@ pre-commit hook and in-editor via clangd. | Three-layer dependency | Architecture rules above | pre-commit (blocks `base→ui/desktop`, `ui→desktop`) | | Naming | [`.clang-tidy`](.clang-tidy) | clangd / manual (config-only; **not** in pre-commit/CI) | +**Doxygen — two tiers** (deliberate): + +- **Enforced floor**: [`scripts/doxygen/lint.py`](scripts/doxygen/lint.py) (runs in pre-commit) — `@file` header, `@brief`, `@param` direction, `@return`, `@throws`, `@since`, `@ingroup`, enum/struct docs, function blocks, ≤100-char lines. The codebase currently passes this. +- **Aspirational target**: [`document/DOXYGEN_REQUEST.md`](document/DOXYGEN_REQUEST.md) adds stricter items (class `@code` example, `@author`/`@date`) that lint does **not** check. New code should aim for these; they won't block commits. + **Hook setup**: `git config core.hooksPath scripts/release/hooks` is set **automatically** at configure time by `cmake/install_hooks.cmake` — no manual install needed after `clone`. Hooks live in [`scripts/release/hooks/`](scripts/release/hooks/) From fbe4fab62bd1010a3655632cd38888d72c254b12 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Mon, 15 Jun 2026 20:18:25 +0800 Subject: [PATCH 09/18] docs: add Coding Taste section to AGENT.md Distill the codebase's actual style preferences (data-backed) so new code and AI tools match the project: prefer cf::expected for errors (reduce exceptions), unique_ptr + make_unique ownership (shared_ptr only for shared ownership), heavy auto/constexpr/string_view, interface-driven design with strict override, and Qt without Q_DISABLE_COPY. These are preferences, not lint-enforced. --- AGENT.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/AGENT.md b/AGENT.md index 17135cb9f..19c10d73a 100644 --- a/AGENT.md +++ b/AGENT.md @@ -51,6 +51,28 @@ Windows equivalents use `.ps1` scripts in the same directory. - **Shared libs**: `cfbase` (DLL), `cfui` (DLL), `CFDesktop_shared` (DLL) - **CMake targets**: `cfbase_*`, `cf_ui_*`, `cf_desktop_*` +## Coding Taste + +Hard rules live in Code Style above; these are *style preferences* distilled from +the codebase — write new code to match. Not lint-enforced; they keep the codebase +coherent and help AI tools generate on-style code. + +- **Error handling**: prefer [`cf::expected`](base/include/base/) for failable + operations in new code; reduce exceptions. Existing exception-based code is + tolerated. +- **Ownership**: default to `std::unique_ptr` + `std::make_unique`; use + `std::shared_ptr` only for genuine shared ownership; raw `new` is discouraged + (acceptable for Qt parent-ownership like `new QWidget(parent)`, not general + allocation). +- **Modern C++ (heavily used — match this)**: `auto` for local deduction, + `constexpr` liberally, `std::string_view` for non-owning string params. + `concepts` / `std::span` are early-stage — adoptable, not yet idiomatic. +- **Interface-driven design**: cross-layer seams are pure-virtual interfaces + (`IWindow`, `IStatusBar`, `IPanel`…) with implementations in `platform/` / + `private/`; always mark overrides with `override`. `final` is not enforced. +- **Qt**: use `Q_OBJECT` / `emit` normally; `Q_DISABLE_COPY` is not used in this + project (prefer explicit `= delete` if copy-protection is needed). + ## Module Map ### base/ → cfbase From 07703c8f15da32c9cc176f802753a7cc3380c407 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Mon, 15 Jun 2026 22:40:53 +0800 Subject: [PATCH 10/18] improved status bar --- desktop/CMakeLists.txt | 8 +- desktop/main.cpp | 6 +- desktop/ui/CFDesktopEntity.cpp | 7 + desktop/ui/components/CMakeLists.txt | 1 + desktop/ui/components/PanelManager.cpp | 1 + .../ui/components/statusbar/CMakeLists.txt | 21 ++ desktop/ui/components/statusbar/IStatusBar.h | 103 +++++- .../ui/components/statusbar/icons/LICENSE.md | 15 + .../ui/components/statusbar/icons/battery.png | Bin 0 -> 320 bytes .../ui/components/statusbar/icons/signal.png | Bin 0 -> 182 bytes .../ui/components/statusbar/icons/volume.png | Bin 0 -> 696 bytes .../ui/components/statusbar/icons/wifi.png | Bin 0 -> 1217 bytes .../ui/components/statusbar/status_bar.cpp | 342 ++++++++++++++++++ desktop/ui/components/statusbar/status_bar.h | 278 ++++++++++++++ .../components/statusbar/statusbar_icons.qrc | 8 + document/status/current.md | 2 +- .../todo/desktop/milestone_02_status_bar.md | 29 +- 17 files changed, 809 insertions(+), 12 deletions(-) create mode 100644 desktop/ui/components/statusbar/icons/LICENSE.md create mode 100644 desktop/ui/components/statusbar/icons/battery.png create mode 100644 desktop/ui/components/statusbar/icons/signal.png create mode 100644 desktop/ui/components/statusbar/icons/volume.png create mode 100644 desktop/ui/components/statusbar/icons/wifi.png create mode 100644 desktop/ui/components/statusbar/status_bar.cpp create mode 100644 desktop/ui/components/statusbar/status_bar.h create mode 100644 desktop/ui/components/statusbar/statusbar_icons.qrc diff --git a/desktop/CMakeLists.txt b/desktop/CMakeLists.txt index 0b0f6fcee..a238d3585 100644 --- a/desktop/CMakeLists.txt +++ b/desktop/CMakeLists.txt @@ -75,10 +75,14 @@ target_link_libraries(CFDesktop_shared PUBLIC Qt6::Widgets ) -# Link shared library dependencies (NOT in whole-archive) -target_link_libraries(CFDesktop_shared PRIVATE +# Link shared library dependencies (NOT in whole-archive). +# cfui is PUBLIC so the thin EXE can include the QApplication subclass +# (MaterialApplication) it instantiates in main.cpp. +target_link_libraries(CFDesktop_shared +PRIVATE cfbase cflogger +PUBLIC cfui ) diff --git a/desktop/main.cpp b/desktop/main.cpp index 3baf5cb9e..a3c2afabf 100644 --- a/desktop/main.cpp +++ b/desktop/main.cpp @@ -1,7 +1,9 @@ #include "desktop_entry.h" -#include +#include "ui/widget/material/application/material_application.h" int main(int argc, char* argv[]) { - QApplication cf_desktop_app(argc, argv); + // MaterialApplication registers the MD3 themes (light/dark) into ThemeManager + // on construction, so panels and widgets resolve real theme tokens at runtime. + cf::ui::widget::material::MaterialApplication cf_desktop_app(argc, argv); return cf::desktop::run_desktop_session(); } diff --git a/desktop/ui/CFDesktopEntity.cpp b/desktop/ui/CFDesktopEntity.cpp index 8fbae72a8..665b4f5c1 100644 --- a/desktop/ui/CFDesktopEntity.cpp +++ b/desktop/ui/CFDesktopEntity.cpp @@ -8,6 +8,7 @@ #include "components/DisplayServerBackendFactory.h" #include "components/IDisplayServerBackend.h" #include "components/PanelManager.h" +#include "components/statusbar/status_bar.h" #include "platform/DesktopPropertyStrategyFactory.h" #include "platform/display_backend_helper.h" #include "platform/shell_layer_helper.h" @@ -100,6 +101,12 @@ CFDesktopEntity::RunsSetupResult CFDesktopEntity::run_init(RunsSetupMethod m) { QObject::connect(panel_mgr, &PanelManager::availableGeometryChanged, desktop_entity_, [shell](const QRect& r) { shell->onAvailableGeometryChanged(r); }); + // ── Status bar: top-edge panel (clock + system icons) ── + auto* status_bar = new cf::desktop::desktop_component::StatusBar(desktop_entity_); + panel_mgr->registerPanel(status_bar->GetWeak()); + status_bar->show(); + panel_mgr->relayout(); + // Show the desktop full-screen desktop_entity_->showFullScreen(); diff --git a/desktop/ui/components/CMakeLists.txt b/desktop/ui/components/CMakeLists.txt index f5cfb8795..0c9f1c081 100644 --- a/desktop/ui/components/CMakeLists.txt +++ b/desktop/ui/components/CMakeLists.txt @@ -31,5 +31,6 @@ PUBLIC PRIVATE cfdesktop_shell_layer_impl cfdesktop_wallpaper + cfdesktop_statusbar Qt6::Core Qt6::Gui Qt6::Widgets ) diff --git a/desktop/ui/components/PanelManager.cpp b/desktop/ui/components/PanelManager.cpp index 774e381c5..ea0a2d7ca 100644 --- a/desktop/ui/components/PanelManager.cpp +++ b/desktop/ui/components/PanelManager.cpp @@ -19,6 +19,7 @@ PanelManager::RegisterFeedback PanelManager::registerPanel(WeakPtr panel return RegisterFeedback::DuplicatePanel; } + panels.push_back(panel); return RegisterFeedback::OK; } diff --git a/desktop/ui/components/statusbar/CMakeLists.txt b/desktop/ui/components/statusbar/CMakeLists.txt index e69de29bb..f7be5a730 100644 --- a/desktop/ui/components/statusbar/CMakeLists.txt +++ b/desktop/ui/components/statusbar/CMakeLists.txt @@ -0,0 +1,21 @@ +# Status bar implementation (QWidget-based top-edge panel). +add_library(cfdesktop_statusbar STATIC + status_bar.cpp + statusbar_icons.qrc +) + +target_include_directories(cfdesktop_statusbar PUBLIC + $ + $ + $ +) + +target_link_libraries( + cfdesktop_statusbar +PUBLIC + cfui # ThemeManager, color/typography tokens, ICFTheme +PRIVATE + Qt6::Widgets + cfbase # WeakPtr / WeakPtrFactory + cflogger # Diagnostic logging for icon-mask loading +) diff --git a/desktop/ui/components/statusbar/IStatusBar.h b/desktop/ui/components/statusbar/IStatusBar.h index 54992a703..482faf83f 100644 --- a/desktop/ui/components/statusbar/IStatusBar.h +++ b/desktop/ui/components/statusbar/IStatusBar.h @@ -2,13 +2,14 @@ * @file IStatusBar.h * @brief Abstract status bar panel interface. * - * IStatusBar extends IPanel to define the status bar contract. - * Concrete implementations provide the actual status bar widget - * and content. + * IStatusBar extends IPanel to define the status bar contract: a panel + * anchored to the top edge that renders the clock and system icons. + * Concrete implementations (e.g. StatusBar) provide the widget and the + * behavior behind these accessors. * * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) * @date 2026-03-29 - * @version 0.1 + * @version 0.2 * @since 0.11 * @ingroup components */ @@ -18,15 +19,105 @@ #include "components/IPanel.h" namespace cf::desktop::desktop_component { +/** + * @brief Visual layout variant for a status bar. + * + * Controls how the clock and system icons are arranged. + * + * @ingroup components + */ +enum class StatusBarStyle { + Centered, ///< Clock centered, minimal iconry. + Split ///< Clock and icons distributed to the side regions. +}; + /** * @brief Abstract status bar panel. * - * Concrete implementations provide a status bar that attaches - * to a screen edge and participates in the panel layout. + * Concrete implementations provide a status bar that attaches to a screen + * edge and participates in the panel layout. The visibility accessors let + * callers toggle the clock and the system-icon region independently. * * @ingroup components */ class IStatusBar : public IPanel { public: + /** + * @brief Shows or hides the clock region. + * + * @param[in] visible true to show the clock, false to hide it. + * + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup components + */ + virtual void setTimeVisible(bool visible) = 0; + + /** + * @brief Reports whether the clock region is shown. + * + * @return true when the clock is visible, false otherwise. + * + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup components + */ + virtual bool isTimeVisible() const = 0; + + /** + * @brief Shows or hides the system-icon region. + * + * @param[in] visible true to show system icons, false to hide them. + * + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup components + */ + virtual void setIconsVisible(bool visible) = 0; + + /** + * @brief Reports whether the system-icon region is shown. + * + * @return true when system icons are visible, false otherwise. + * + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup components + */ + virtual bool isIconsVisible() const = 0; + + /** + * @brief Selects the clock/icon layout variant. + * + * @param[in] style The layout style to apply. + * + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup components + */ + virtual void setStyle(StatusBarStyle style) = 0; + + /** + * @brief Returns the active layout variant. + * + * @return The current status bar style. + * + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup components + */ + virtual StatusBarStyle style() const = 0; }; } // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/statusbar/icons/LICENSE.md b/desktop/ui/components/statusbar/icons/LICENSE.md new file mode 100644 index 000000000..c6af3523f --- /dev/null +++ b/desktop/ui/components/statusbar/icons/LICENSE.md @@ -0,0 +1,15 @@ +# Status Bar Icons — Attribution + +The status bar icons (`signal.png`, `battery.png`, `wifi.png`, `volume.png`) are +sourced from **Icons8** (https://icons8.com). + +- Source set: Icons8 line-style glyphs (50px, monochrome black on transparent). +- License: Icons8 Free License — https://icons8.com/license +- Requirement: Free usage requires attribution / a link back to icons8.com. + +These PNGs are used as monochrome alpha masks: at runtime `StatusBar` recolors +them to the active Material theme's `onSurfaceVariant` token, so they follow +Light/Dark theme switches with a single asset per icon. + +> NOTE: If this project is distributed without an Icons8 attribution elsewhere +> (e.g. README, About dialog), keep this file in place to satisfy the license. diff --git a/desktop/ui/components/statusbar/icons/battery.png b/desktop/ui/components/statusbar/icons/battery.png new file mode 100644 index 0000000000000000000000000000000000000000..9dae88956ad347e4ca32873b21b5287f0586b747 GIT binary patch literal 320 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-&H|6fVg?3oVGw3ym^DWNDEP_K z#WAE}&f6KWT!#z^cC49h{H6%sTW`vq+_IRu zW>>S>`d!Mq>krO~bKY|``rFnEKb{(2zf(FP{eqyLZu|v7Cy=B6wD9l0XwJ;??Rq58 O_Y9t{elF{r5}E*)j((&7 literal 0 HcmV?d00001 diff --git a/desktop/ui/components/statusbar/icons/signal.png b/desktop/ui/components/statusbar/icons/signal.png new file mode 100644 index 0000000000000000000000000000000000000000..6345c679ad8ac31bedb9a3c50b25d255359e49bd GIT binary patch literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-&H|6fVg?3oVGw3ym^DWND45~t z;uumf=k57}oDBgChcCR@UvkIc%ZG(tOnbjOUcFqi<0p^NG{<8`CnDG+Pb(xJ=PBBu zz+xFJ&@@Mr!_lly@=2;Ahtq`JO#&?n^II&+&M4%_8goP+=Q(uEuz5o%glpd@!W^4< WVEV$gN3p#D7B&gi{E{G!hVO#6r+Y zEEEe1QyDBSjVA=e));~se-zqSC}^o52#R{jkrXOXK~eEXvhv_9EMazbFME48v)mV^ z+4pv5KC^FT-poK(U3sMqz$su6_zKJbeR!raa0>|5lnZzUaoYePL>dvVtcN zcPtQLDFj47K_CJO0ukWhzz%!>*$CWQ2Skw2F%N(_V5A)on=(MyK>C4Y_4x)I${XT- z28g84+7SuYRp4x1;gl63?HDLUXXr_^p%+-J|hKdiWPqhUQ%?$Qq`d9t|yVAaN z$x`u<`n+p1NVN0bYD6UJv!!B5$I;$tCCC=wJaEL4-j{(F`rgSjAZaRY1w{0Fdm_&o zKqaPa2h>OQH9anZNTZGdFZKB!V4tCF5)pOv+3O-ms9xZ*+CBk;2A?UlPrD2f_WEPB zPa5oFYJ225h-h5x&kVLP2>rYbdi(wk6Z<2fo-%=C14@vS<7h{ zT!y(>1dwo`&J_V9gt=A(kTA?{SRj7lkxIbz7?9_9rV=o307-T1>6&`> z0~Tnl@u&;V1?~gh1NH;uDu?lS7r0lvYmS?Nb->|>B0mA^ftA4Bz)WBo&;*>W(WHMf zMYB?Ceva@uEEzZDt^8PE4)C=Tb`sbEJOs4mAmei2VW3;QgPgB`yQD)^xDoi)F+Ko1 zqm!)_rUTE)r-EGbs~d*GY_ja%+pwK}Bg2PtfMvihP7m{=NtH6V75G&Pj{?tE;DF`> z*8`6MZvkHd{V`FiUvozDsMgrpG)w?q1dd53+t@8x%Dm!ibQ^F*3jNvgoezP(tB6>K zfDhygXQjw1$!CI(t_Dtv5{(Bw710Ucsg#qN3cLXP5pjOA<2Aq%+4J<6sMV^uC8F(# zIQQ?efGH`5{j_v6`i;{46wKzn(ZpZC?FmfI*NqLh4SxYt6%hJ8YFYMREW;yaK;0OKt4w#)G{5+ZEOTY%8FD7dB zNhym$x5;m^^4MPKun)LSx^33Ke(C-nw2C-HtRwoid@?aX&>6s7-P-XXM67?b>AB(+ zhbEOWp?ulqHfA~}mI|Q=G{<`~!9LFf9&sBlYa`~ctK^KOn5b3K+$!Z0zxnUQGj2l- z(j^3g{n9lRS1akMEP|j&@Ys3rej5j%jDxC33Im+;uxyO z7*)i3oHeYG%t~~t7#9Froe6cyyz<64CEH+f-IARPEJN>fbxv9p+NfIXHK0rD4r%Pv zzgJZ&S@cQ5Y*+8?k`7g)!xe0nZR60YL>3}lCF6(^Yf|J{ExKbVJ~L?CsZeQ*S48Y} zt(!ZpQ(vyk+ooG;)OWk)x?H_vYu||2{t7o|vU9~eCw!?A<}O#Zyd25CS(v0#54UZ+ zW{G#G}R!J?N zUPsrhwIc$PUGZqQaZg4Iv_T!kaC7^_qwX-pUYAmT>dlaaYuttvpkD`wgUT~M_$!v3*5=!(D1#7Z> zCWZGZ>rZiSjjmyCy;j|~4PA~tjnJjBxlx4)u2E1{6ELf5GGfuJ1lXI>H5@f)8Dx21 fpg@5F{{#FBcO9on3#cRm00000NkvXXu0mjfX9P!e literal 0 HcmV?d00001 diff --git a/desktop/ui/components/statusbar/status_bar.cpp b/desktop/ui/components/statusbar/status_bar.cpp new file mode 100644 index 000000000..cc07abbd3 --- /dev/null +++ b/desktop/ui/components/statusbar/status_bar.cpp @@ -0,0 +1,342 @@ +/** + * @file status_bar.cpp + * @brief Concrete status bar panel implementation. + * + * Renders the clock and system-icon glyphs with Material Design 3 polish: a + * tonal elevation surface, an in-band soft shadow at the bottom seam, refined + * vector icons, and a boot fade-in. All rendering is QPainter-native so it + * builds for the embedded target without extra Qt modules. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-15 + * @version 0.2 + * @since 0.19 + * @ingroup components + */ + +#include "status_bar.h" + +#include "base/color.h" +#include "base/device_pixel.h" +#include "cflog.h" +#include "core/theme_manager.h" +#include "core/token/material_scheme/cfmaterial_token_literals.h" +#include "core/token/typography/cfmaterial_typography_token_literals.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +// Q_INIT_RESOURCE must run at global scope: the rcc-generated registration +// function lives in the global namespace, but the macro's extern declaration +// is emitted in the surrounding scope, so calling it from inside +// cf::desktop::desktop_component would look up a namespaced symbol that +// doesn't exist and fail to link. +static void registerStatusbarIconsResource() { + Q_INIT_RESOURCE(statusbar_icons); +} + +namespace cf::desktop::desktop_component { + +using cf::desktop::PanelPosition; +using cf::ui::base::CFColor; +using cf::ui::base::device::CanvasUnitHelper; +using namespace cf::ui::core::token::literals; + +namespace { +// Fallback palette used when no theme is available (mirrors MD3 light). +constexpr int kBarHeight = 48; ///< Status bar thickness in device pixels. +constexpr int kSideMarginDp = 16; ///< Horizontal padding for clock/icons (dp). +constexpr int kIconGapDp = 12; ///< Spacing between adjacent icons (dp). +constexpr int kIconSizeDp = 16; ///< Icon cell edge length (dp). +constexpr int kTimeGapDp = 10; ///< Gap between the time and the icon cluster (dp). +constexpr qreal kShadowBandDp = 5.0; ///< In-band elevation shadow height (dp). + +// Icon kinds and their compiled-resource mask paths. Indices align with +// StatusBar::icon_masks_[]. A missing mask leaves a visible gap in the bar +// (no silent fallback) so a broken resource stays obvious. +enum class StatusIcon { Signal = 0, Battery = 1, Wifi = 2, Volume = 3 }; +constexpr int kStatusIconCount = 4; +constexpr const char* const kIconMaskPaths[kStatusIconCount] = { + ":/cfdesktop/statusbar/signal.png", // Signal + ":/cfdesktop/statusbar/battery.png", // Battery + ":/cfdesktop/statusbar/wifi.png", // Wifi + ":/cfdesktop/statusbar/volume.png", // Volume +}; +} // namespace + +StatusBar::StatusBar(QWidget* parent) + : QWidget(parent), timer_(new QTimer(this)), cached_time_(currentTimeText()), + cached_date_(currentDateText()) { + setAttribute(Qt::WA_OpaquePaintEvent); + setAutoFillBackground(false); + setFixedHeight(kBarHeight); + setupUi(); + applyTheme(); + loadIconMasks(); + startFadeIn(); +} + +StatusBar::~StatusBar() = default; + +void StatusBar::setupUi() { + connect(timer_, &QTimer::timeout, this, &StatusBar::onTimeout); + timer_->start(1000); + + // React to theme switches (ThemeManager is the canonical source). + connect(&cf::ui::core::ThemeManager::instance(), &cf::ui::core::ThemeManager::themeChanged, + this, [this](const cf::ui::core::ICFTheme&) { applyTheme(); }); +} + +void StatusBar::applyTheme() { + try { + auto& tm = cf::ui::core::ThemeManager::instance(); + const auto& theme = tm.theme(tm.currentThemeName()); + auto& cs = theme.color_scheme(); + background_color_ = cs.queryColor(SURFACE); + foreground_color_ = cs.queryColor(ON_SURFACE); + icon_color_ = cs.queryColor(ON_SURFACE_VARIANT); + divider_color_ = cs.queryColor(OUTLINE_VARIANT); + clock_font_ = theme.font_type().queryTargetFont(TYPOGRAPHY_TITLE_MEDIUM); + // MD3 elevation tonal lift: lighten the surface tone a few steps for the + // top of the gradient, so the bar reads as raised above the shell. + const CFColor base(background_color_); + surface_top_color_ = + CFColor(base.hue(), base.chroma(), std::clamp(base.tone() + 3.0f, 0.0f, 100.0f)) + .native_color(); + } catch (...) { + // Fallback palette when no theme is registered yet. + background_color_ = QColor(0xF7, 0xF5, 0xF3); + surface_top_color_ = QColor(0xFF, 0xFF, 0xFF); + foreground_color_ = QColor(0x1C, 0x1B, 0x1F); + icon_color_ = QColor(0x49, 0x45, 0x4E); + divider_color_ = QColor(0xCA, 0xC4, 0xD0); + clock_font_ = font(); + clock_font_.setPixelSize(15); + } + update(); +} + +void StatusBar::startFadeIn() { + auto* fade = new QVariantAnimation(this); + fade->setDuration(250); + fade->setStartValue(qreal(0)); + fade->setEndValue(qreal(1)); + fade->setEasingCurve(QEasingCurve::OutCubic); + connect(fade, &QVariantAnimation::valueChanged, this, [this](const QVariant& v) { + fade_opacity_ = v.toReal(); + update(); + }); + fade->start(QAbstractAnimation::DeleteWhenStopped); +} + +void StatusBar::loadIconMasks() { + // Qt resources compiled into a STATIC library can be stripped by the linker + // because nothing references the registration symbol. Forcing registration + // keeps the object, so the ":/cfdesktop/statusbar/*.png" lookups succeed. + registerStatusbarIconsResource(); + + int loaded = 0; + std::string missing; + for (int i = 0; i < kStatusIconCount; ++i) { + icon_masks_[i] = QPixmap(QString::fromLatin1(kIconMaskPaths[i])); + if (icon_masks_[i].isNull()) { + missing += missing.empty() ? "" : ", "; + missing += kIconMaskPaths[i]; + } else { + ++loaded; + } + } + if (missing.empty()) { + cf::log::infoftag("StatusBar", "loaded {}/{} icon masks", loaded, kStatusIconCount); + } else { + // Fail loud: a missing mask now leaves a visible gap in the bar rather + // than a silent substitute, and this warning names exactly what is gone. + cf::log::warningftag("StatusBar", "loaded {}/{} icon masks; missing: {}", loaded, + kStatusIconCount, missing); + } +} + +QPixmap StatusBar::tintedPixmap(const QPixmap& mask, const QColor& color) { + if (mask.isNull()) { + return {}; + } + QPixmap out(mask.size()); + out.setDevicePixelRatio(mask.devicePixelRatio()); + out.fill(Qt::transparent); + QPainter tp(&out); + tp.drawPixmap(0, 0, mask); + // Replace every non-transparent pixel's color with `color`, keeping the + // mask's alpha shape. Works for any single-color source (black or white). + tp.setCompositionMode(QPainter::CompositionMode_SourceIn); + tp.fillRect(out.rect(), color); + tp.end(); + return out; +} + +QString StatusBar::currentTimeText() const { + return QDateTime::currentDateTime().toString(QStringLiteral("HH:mm")); +} + +QString StatusBar::currentDateText() const { + return QDateTime::currentDateTime().toString(QStringLiteral("yyyy/MM/dd")); +} + +void StatusBar::onTimeout() { + const QString nextTime = currentTimeText(); + const QString nextDate = currentDateText(); + if (nextTime != cached_time_ || nextDate != cached_date_) { + cached_time_ = nextTime; + cached_date_ = nextDate; + update(); + } +} + +// -- IPanel ---------------------------------------------------------------- +PanelPosition StatusBar::position() const { + return PanelPosition::Top; +} + +int StatusBar::priority() const { + return 100; +} + +int StatusBar::preferredSize() const { + return kBarHeight; +} + +QWidget* StatusBar::widget() const { + return const_cast(this); +} + +// -- IStatusBar ------------------------------------------------------------ +void StatusBar::setTimeVisible(bool visible) { + if (time_visible_ != visible) { + time_visible_ = visible; + update(); + } +} + +bool StatusBar::isTimeVisible() const { + return time_visible_; +} + +void StatusBar::setIconsVisible(bool visible) { + if (icons_visible_ != visible) { + icons_visible_ = visible; + update(); + } +} + +bool StatusBar::isIconsVisible() const { + return icons_visible_; +} + +void StatusBar::setStyle(StatusBarStyle s) { + if (style_ != s) { + style_ = s; + update(); + } +} + +StatusBarStyle StatusBar::style() const { + return style_; +} + +// -- Painting -------------------------------------------------------------- +void StatusBar::paintEvent(QPaintEvent* /*event*/) { + const CanvasUnitHelper h(devicePixelRatioF()); + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing, true); + p.setRenderHint(QPainter::SmoothPixmapTransform, true); + p.setOpacity(fade_opacity_); + + // Tonal elevation surface: gentle vertical gradient (lifted top -> surface). + QLinearGradient surface(0, 0, 0, height()); + surface.setColorAt(0.0, surface_top_color_); + surface.setColorAt(1.0, background_color_); + p.fillRect(rect(), surface); + + // Clock region: date on the left, time on the right just before the icons + // (Split) or centered (Centered). The icon-cluster left edge anchors the time. + const qreal sideMargin = h.dpToPx(kSideMarginDp); + const qreal iconSize = h.dpToPx(kIconSizeDp); + const qreal iconGap = h.dpToPx(kIconGapDp); + const qreal clusterWidth = icons_visible_ ? (4 * iconSize + 3 * iconGap) : 0; + const qreal iconClusterLeft = width() - sideMargin - clusterWidth; + + if (time_visible_) { + p.setPen(foreground_color_); + p.setFont(clock_font_); + const int sm = static_cast(sideMargin); + + // Date: far left. + p.drawText(QRect(sm, 0, width(), height()), Qt::AlignVCenter | Qt::AlignLeft, cached_date_); + + // Time: right-aligned to the icon cluster (Split) or centered (Centered). + if (style_ == StatusBarStyle::Centered) { + p.drawText(rect(), Qt::AlignCenter, cached_time_); + } else { + const int timeRight = static_cast(iconClusterLeft - h.dpToPx(kTimeGapDp)); + p.drawText(QRect(sm, 0, timeRight - sm, height()), Qt::AlignVCenter | Qt::AlignRight, + cached_time_); + } + } + + // System-icon cluster (right-aligned): signal, battery, wifi, volume. + if (icons_visible_) { + const int y = static_cast((height() - iconSize) / 2.0); + qreal x = width() - sideMargin; + auto cell = [&](qreal rightEdge) { + return QRectF(rightEdge - iconSize, y, iconSize, iconSize); + }; + + // PNG mask tinted to the theme. A missing mask leaves a visible gap — + // no silent fallback, so a broken resource stays obvious. + auto drawStatusIcon = [&](StatusIcon kind, qreal rightEdge) { + const QPixmap& mask = icon_masks_[static_cast(kind)]; + if (mask.isNull()) { + return; + } + p.drawPixmap(cell(rightEdge).toRect(), tintedPixmap(mask, icon_color_)); + }; + + drawStatusIcon(StatusIcon::Volume, x); + x -= (iconSize + iconGap); + drawStatusIcon(StatusIcon::Wifi, x); + x -= (iconSize + iconGap); + drawStatusIcon(StatusIcon::Battery, x); + x -= (iconSize + iconGap); + drawStatusIcon(StatusIcon::Signal, x); + } + + // In-band soft elevation shadow at the bottom seam (PanelManager locks the + // widget height to preferredSize, so the cast shadow is drawn in-band). + const qreal shadowH = h.dpToPx(kShadowBandDp); + QLinearGradient shadow(0, height() - shadowH, 0, height()); + shadow.setColorAt(0.0, QColor(0, 0, 0, 0)); + shadow.setColorAt(1.0, QColor(0, 0, 0, 26)); // ~10% alpha + p.fillRect(QRectF(0, height() - shadowH, width(), shadowH), shadow); + + // Horizontally-faded hairline (less "ruled" than a hard full-width line). + QColor lineMid = divider_color_; + lineMid.setAlphaF(0.45); + QColor lineEdge = divider_color_; + lineEdge.setAlphaF(0.0); + QLinearGradient hairline(0, 0, width(), 0); + hairline.setColorAt(0.0, lineEdge); + hairline.setColorAt(0.08, lineMid); + hairline.setColorAt(0.92, lineMid); + hairline.setColorAt(1.0, lineEdge); + p.setPen(QPen(hairline, h.dpToPx(1))); + p.drawLine(QPointF(0, height() - h.dpToPx(0.5)), QPointF(width(), height() - h.dpToPx(0.5))); +} + +} // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/statusbar/status_bar.h b/desktop/ui/components/statusbar/status_bar.h new file mode 100644 index 000000000..bcab51ce5 --- /dev/null +++ b/desktop/ui/components/statusbar/status_bar.h @@ -0,0 +1,278 @@ +/** + * @file status_bar.h + * @brief Concrete status bar panel. + * + * StatusBar is the top-edge panel implementation behind IStatusBar. It + * renders the clock and a row of system-icon glyphs, follows the active + * Material theme, and updates the clock once per second. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-15 + * @version 0.1 + * @since 0.19 + * @ingroup components + */ + +#pragma once + +#include "IStatusBar.h" +#include "base/weak_ptr/weak_ptr.h" +#include "base/weak_ptr/weak_ptr_factory.h" + +#include +#include +#include +#include + +class QTimer; + +namespace cf::desktop::desktop_component { + +/** + * @brief QWidget-based status bar. + * + * Renders the clock and system-icon glyphs directly in paintEvent so the + * active Material theme fully controls colors and typography. A one-second + * timer keeps the clock current. + * + * @ingroup components + */ +class StatusBar final : public QWidget, public IStatusBar { + Q_OBJECT + public: + /** + * @brief Constructs the status bar. + * + * @param[in] parent Owning widget (typically the desktop surface). + * + * @throws None + * @note Starts the clock timer and applies the current theme. + * @warning None + * @since 0.19 + * @ingroup components + */ + explicit StatusBar(QWidget* parent = nullptr); + + /** + * @brief Destructs the status bar. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + ~StatusBar() override; + + // -- IPanel --------------------------------------------------------------- + /** + * @brief Returns the anchoring edge. + * + * @return Always PanelPosition::Top. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + PanelPosition position() const override; + + /** + * @brief Returns the layout priority. + * + * @return Priority value (outermost on the edge). + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + int priority() const override; + + /** + * @brief Returns the bar thickness in device pixels. + * + * @return Preferred height in device pixels. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + int preferredSize() const override; + + /** + * @brief Returns the widget positioned by the layout engine. + * + * @return This status bar widget. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + QWidget* widget() const override; + + // -- IStatusBar ----------------------------------------------------------- + /** + * @brief Shows or hides the clock. + * + * @param[in] visible true to show the clock, false to hide it. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + void setTimeVisible(bool visible) override; + + /** + * @brief Reports clock visibility. + * + * @return true when the clock is visible. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + bool isTimeVisible() const override; + + /** + * @brief Shows or hides the system-icon cluster. + * + * @param[in] visible true to show icons, false to hide them. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + void setIconsVisible(bool visible) override; + + /** + * @brief Reports icon-cluster visibility. + * + * @return true when system icons are visible. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + bool isIconsVisible() const override; + + /** + * @brief Selects the clock/icon layout variant. + * + * @param[in] style The layout style to apply. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + void setStyle(StatusBarStyle style) override; + + /** + * @brief Returns the active layout variant. + * + * @return The current status bar style. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + StatusBarStyle style() const override; + + /** + * @brief Returns a weak reference to this status bar. + * + * @return WeakPtr valid for this instance's lifetime. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + WeakPtr GetWeak() const { return weak_factory_.GetWeakPtr(); } + + protected: + /** + * @brief Paints the background, clock, and icon glyphs. + * + * @param[in] event The paint event descriptor. + * + * @throws None + * @note Theme colors and fonts are resolved in applyTheme(). + * @warning None + * @since 0.19 + * @ingroup components + */ + void paintEvent(QPaintEvent* event) override; + + private slots: + /// @brief Refreshes the clock text each second. + void onTimeout(); + + private: + /// @brief Wires signals and starts the clock timer. + void setupUi(); + /// @brief Resolves theme colors and typography, then repaints. + void applyTheme(); + /// @brief Returns the formatted current time string (HH:mm). + QString currentTimeText() const; + /// @brief Returns the formatted current date string (yyyy/MM/dd). + QString currentDateText() const; + /// @brief Plays the boot fade-in (opacity 0 -> 1). + void startFadeIn(); + /// @brief Loads monochrome icon masks from compiled resources (qrc). + /// @note Masks stay null when the resources are absent; paintEvent then + /// falls back to the hand-drawn vector glyphs. + void loadIconMasks(); + /// @brief Recolors a monochrome mask to a target color, preserving alpha. + /// @param[in] mask Single-color icon on a transparent background. + /// @param[in] color Target fill color. + /// @return Tinted pixmap matching the mask's alpha shape. + static QPixmap tintedPixmap(const QPixmap& mask, const QColor& color); + + /// One-second clock timer. Ownership: this widget (Qt parent). + QTimer* timer_{nullptr}; + bool time_visible_{true}; + bool icons_visible_{true}; + StatusBarStyle style_{StatusBarStyle::Split}; + QString cached_time_; + QString cached_date_; + + // Resolved theme values (refreshed by applyTheme()). + QColor background_color_; + QColor surface_top_color_; ///< HCT tone-lifted surface, for the top of the gradient. + QColor foreground_color_; + QColor icon_color_; + QColor divider_color_; + QFont clock_font_; + + /// Boot fade-in opacity in [0, 1]; applied as painter opacity. + qreal fade_opacity_{0.0}; + + /// Cached monochrome icon masks (index = StatusIcon). Null entries fall + /// back to vector drawing in paintEvent. + QPixmap icon_masks_[4]; + + /// Weak pointer factory (must be the last member). + mutable cf::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/statusbar/statusbar_icons.qrc b/desktop/ui/components/statusbar/statusbar_icons.qrc new file mode 100644 index 000000000..535b3ffae --- /dev/null +++ b/desktop/ui/components/statusbar/statusbar_icons.qrc @@ -0,0 +1,8 @@ + + + icons/signal.png + icons/battery.png + icons/wifi.png + icons/volume.png + + diff --git a/document/status/current.md b/document/status/current.md index 1e1e94e42..bcbacf11b 100644 --- a/document/status/current.md +++ b/document/status/current.md @@ -47,7 +47,7 @@ description: CFDesktop 项目进度的唯一事实来源与全局导航。 ## 下一步路线(最小闭环先行) -1. **MS2 状态栏**(顶部时间 + 系统图标)— ⬜ 待开始 · *首个落地任务*(`IStatusBar` / `PanelManager` / `ThemeEngine` 已就绪) +1. **MS2 状态栏**(顶部时间 + 系统图标)— ✅ 功能落地(`StatusBar` 实现 + 注册 PanelManager + 主题跟随 + MD3 美化;offscreen 启动通过,待真机视觉确认) 2. **MS3 任务栏**(底部居中图标条 + hover 动画)— ⬜ 待开始 3. **MS4 应用启动器**(应用网格 + QProcess 启动)— ⬜ 待开始 4. **MS5 窗口管理**(窗口装饰 + 任务栏联动)— ⬜ 待开始 diff --git a/document/todo/desktop/milestone_02_status_bar.md b/document/todo/desktop/milestone_02_status_bar.md index e208fb471..a6aa11672 100644 --- a/document/todo/desktop/milestone_02_status_bar.md +++ b/document/todo/desktop/milestone_02_status_bar.md @@ -5,11 +5,38 @@ description: "预计周期: 3-5 天,前置依赖: Milestone 1: 桌面骨架可 # Milestone 2: 状态栏 -> **状态**: ⬜ 待开始 +> **状态**: ✅ 功能落地(v1 基础 + v2 MD3 美化) > **预计周期**: 3-5 天 > **前置依赖**: [Milestone 1: 桌面骨架可见](../done/SUMMARY.md) > **目标**: 屏幕顶部出现一条状态栏,显示时间、基础系统图标 +## 实现结果与偏差(2026-06-15) + +**已交付**:`StatusBar` 顶层面板,注册到 PanelManager 作为 Top edge panel, +顶部一条 48dp 状态栏,显示时间 (HH:MM)、系统图标簇(信号/电池/WiFi/音量), +背景与图标跟随 ThemeManager 主题;offscreen 启动通过、Doxygen lint 通过。 + +**与原计划的偏差**(实现时发现真实代码与本文档措辞不符,已按代码库实际 API 落地): + +1. **`PanelManager::registerPanel()` 原有 bug**:只校验、不把 panel 存入 `panels` + 向量,导致面板注册后永远不会被布局。已修复(`panels.push_back(panel)`)。 +2. **主题 API 名称不同**:实际接口是 `ICFTheme::color_scheme()` / `font_type()` + (非本文档写的 `colorScheme()` / `typography()`);且**无 `surfaceContainer` token**, + 实际用 `SURFACE` / `ON_SURFACE` / `ON_SURFACE_VARIANT` / `OUTLINE_VARIANT`。 +3. **主题管线原本未接通**:`main.cpp` 用裸 `QApplication`,而 Material 主题只在 + 构造 `MaterialApplication` 时才注册到 ThemeManager。已将 `main.cpp` 切到 + `MaterialApplication`,主题在运行时真正可用(顺带让全部 MD3 控件拿到真实主题色)。 +4. **v2 MD3 美化**(嵌入式安全,纯 QPainter,不引入 `Qt6::Svg` 依赖): + HCT 色调抬升的垂直渐变表面 + 底部接缝处 in-band 软阴影 + 横向渐隐发丝线 + + 开机 250ms 淡入。图标用 **icons8 单色 PNG 蒙版 + 运行时 `SourceIn` 染色** + 跟随主题(嵌入式只需 Qt GUI,无需 Svg 模块;资源经 qrc 编译进二进制, + 无文件系统依赖)。**无静默兜底**:mask 加载失败则该图标留可见空白并打 + WARNING 日志(不替换掩盖)。资源用 `Q_INIT_RESOURCE`(全局作用域 helper + 包裹,因该宏不能在命名空间内调用)防止静态库 qrc 被链接器剥离。 + 图标来源/署名见 `desktop/ui/components/statusbar/icons/LICENSE.md`。 + +**待人工确认**:真机/WSLg 显示下的视觉效果(本地无显示设备,仅 offscreen 验证不崩)。 + --- ## 一、阶段目标 From e5f9c6c77bcc129d89a134ef19a3aa20a14573f7 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Tue, 16 Jun 2026 14:37:21 +0800 Subject: [PATCH 11/18] feat: fix the mspc queue issue --- base/include/base/lockfree/mpsc_queue.hpp | 38 ++++--- .../logger/src/async_queue/async_queue.cpp | 29 +++-- .../notes/07-Logger-Flush-Lost-Wakeup-Hang.md | 105 ++++++++++++++++++ document/notes/index.md | 13 +++ 4 files changed, 164 insertions(+), 21 deletions(-) create mode 100644 document/notes/07-Logger-Flush-Lost-Wakeup-Hang.md diff --git a/base/include/base/lockfree/mpsc_queue.hpp b/base/include/base/lockfree/mpsc_queue.hpp index 354c2a415..48f48562f 100644 --- a/base/include/base/lockfree/mpsc_queue.hpp +++ b/base/include/base/lockfree/mpsc_queue.hpp @@ -179,10 +179,11 @@ template class MpscQueue { * @ingroup base_lockfree */ bool tryPop(T& out) noexcept { - Cell* cell = &buffer_[readPos_ & (Capacity - 1)]; + const size_type rp = readPos_.load(std::memory_order_relaxed); + Cell* cell = &buffer_[rp & (Capacity - 1)]; size_type seq = cell->sequence.load(std::memory_order_acquire); - if (seq != readPos_ + 1) { + if (seq != rp + 1) { return false; // Empty } @@ -190,10 +191,13 @@ template class MpscQueue { cell->ptr()->~T(); // Destroy the object in the cell // Publish slot availability for next round - // Set to readPos + Capacity so producer at pos = readPos + Capacity can use it - cell->sequence.store(readPos_ + Capacity, std::memory_order_release); + // Set to rp + Capacity so producer at pos = rp + Capacity can use it + cell->sequence.store(rp + Capacity, std::memory_order_release); - ++readPos_; + // Advance the consumer cursor with release ordering so that producer threads + // reading the approximate size()/empty() observe a consistent value instead of + // racing with this write. + readPos_.store(rp + 1, std::memory_order_release); return true; } @@ -263,19 +267,20 @@ template class MpscQueue { size_type popped = 0; for (; popped < max_count; ++popped) { - Cell* cell = &buffer_[readPos_ & (Capacity - 1)]; + const size_type rp = readPos_.load(std::memory_order_relaxed); + Cell* cell = &buffer_[rp & (Capacity - 1)]; size_type seq = cell->sequence.load(std::memory_order_acquire); - if (seq != readPos_ + 1) { + if (seq != rp + 1) { break; // No more available } out[popped] = std::move(*cell->ptr()); cell->ptr()->~T(); - cell->sequence.store(readPos_ + Capacity, std::memory_order_release); + cell->sequence.store(rp + Capacity, std::memory_order_release); - ++readPos_; + readPos_.store(rp + 1, std::memory_order_release); } return popped; @@ -294,7 +299,10 @@ template class MpscQueue { * This is an approximate check, not a thread-safe guarantee. * @ingroup base_lockfree */ - bool empty() const noexcept { return readPos_ >= writePos_.load(std::memory_order_acquire); } + bool empty() const noexcept { + return readPos_.load(std::memory_order_acquire) >= + writePos_.load(std::memory_order_acquire); + } /** * @brief Gets the approximate current size. @@ -307,10 +315,11 @@ template class MpscQueue { */ size_type size() const noexcept { size_type writePos = writePos_.load(std::memory_order_acquire); - if (writePos < readPos_) { + size_type rp = readPos_.load(std::memory_order_acquire); + if (writePos < rp) { return 0; } - size_type size = writePos - readPos_; + size_type size = writePos - rp; return size > Capacity ? Capacity : size; } @@ -349,10 +358,11 @@ template class MpscQueue { std::array buffer_; ///< Ring buffer storage std::atomic writePos_{0}; ///< Current write position (multi-producer) - size_type readPos_{0}; ///< Current read position (single-consumer) + std::atomic readPos_{0}; ///< Current read position (single-consumer) // Padding to prevent false sharing between producer and consumer - char padding_[64 - sizeof(std::atomic) - sizeof(size_type) - sizeof(buffer_) % 64]; + char padding_[64 - sizeof(std::atomic) - sizeof(std::atomic) - + sizeof(buffer_) % 64]; }; } // namespace lockfree diff --git a/desktop/base/logger/src/async_queue/async_queue.cpp b/desktop/base/logger/src/async_queue/async_queue.cpp index 753a6e083..fcd91e124 100644 --- a/desktop/base/logger/src/async_queue/async_queue.cpp +++ b/desktop/base/logger/src/async_queue/async_queue.cpp @@ -19,9 +19,16 @@ void AsyncPostQueue::start() { void AsyncPostQueue::stop() { if (running_.exchange(false)) { cv_.notify_all(); - flush_completed_.store(flush_token_.load(std::memory_order_acquire), - std::memory_order_release); - flush_completed_cv_.notify_all(); + // Publish the final completed token and wake blocked flush_sync() callers under + // flush_completed_mu_ for the same lost-wakeup reason as the worker loop. The wait + // predicate also returns once running_ is false, but only if the caller is actually + // woken, so the notify must not race past an about-to-sleep caller. + { + std::lock_guard lock(flush_completed_mu_); + flush_completed_.store(flush_token_.load(std::memory_order_acquire), + std::memory_order_release); + flush_completed_cv_.notify_all(); + } if (worker_thread_.joinable()) { worker_thread_.join(); } @@ -136,11 +143,19 @@ void AsyncPostQueue::worker_loop() { sink->flush(); } } - // Update completed token to current flush_token_ - // This unblocks all flush_sync() calls with tokens <= this value + // Update the completed token to the current flush_token_, unblocking every + // flush_sync() caller whose token is <= this value. flush_completed_mu_ MUST be + // held across the store and the notify: flush_sync() blocks on + // flush_completed_cv_ with no timeout, so a notify that races past a caller + // which has already evaluated its predicate (and is about to enter the futex) + // is lost forever and hangs that caller. Holding the lock serialises this + // update against the caller's predicate check, closing the lost-wakeup window. uint64_t current_token = flush_token_.load(std::memory_order_acquire); - flush_completed_.store(current_token, std::memory_order_release); - flush_completed_cv_.notify_all(); + { + std::lock_guard lock(flush_completed_mu_); + flush_completed_.store(current_token, std::memory_order_release); + flush_completed_cv_.notify_all(); + } } std::unique_lock lock(wakeMu_); diff --git a/document/notes/07-Logger-Flush-Lost-Wakeup-Hang.md b/document/notes/07-Logger-Flush-Lost-Wakeup-Hang.md new file mode 100644 index 000000000..e622e63f8 --- /dev/null +++ b/document/notes/07-Logger-Flush-Lost-Wakeup-Hang.md @@ -0,0 +1,105 @@ +--- +title: Logger Flush 丢失唤醒死锁 +description: 异步日志 logger_concurrency_test 在 clang/CI 下极端偶然卡死的根因定位与修复——flush_completed_cv_ 丢失唤醒(主因)与 MpscQueue readPos_ 数据竞争(次因) +--- + +# Logger Flush 丢失唤醒死锁 -- 排查与修复记录 + +## 背景 + +`logger_concurrency_test` 在 clang 构建(尤其 CI 高负载)下**极端偶然**地卡死:CTest 输出停在 `Start 11: logger_concurrency_test`,进程永不返回。直接怀疑死锁,但常规手段碰壁—— + +- 隔离运行该二进制 **23 次全部通过**(fast 配置:2 线程 × 50 条,队列 65536 永不满); +- 单核 `taskset`、填满队列的 stress 配置也无法在几次内复现; +- 静态读代码看不出任何 AB-BA 锁环。 + +一句话定性:**不是确定性死锁,是时序敏感的罕见竞态**。这类 bug 不能靠"多跑几次碰运气"定位,必须换工具。 + +## 排查方法论(可复用) + +定位罕见并发 bug 的三件套,按顺序用: + +1. **ThreadSanitizer(TSan)** —— 不依赖时序,基于 happens-before **确定性**报数据竞争与锁环。先确认"确有并发缺陷",再缩小范围。本例 TSan 当场抓到一处 `readPos_` 数据竞争,并(因 TSan 改变线程调度)顺带暴露了卡死。 + +2. **脱离重依赖的最小探针** —— logger 源码不依赖 Qt(`LogRecord` 是纯 `std`),于是用 `clang++` 直接编译 3 个 `.cpp` + 一个压测驱动,绕过整个 CMake/Qt 构建链。压测驱动刻意制造最坏组合:多生产者 + 一个**紧循环调 `flush_sync()`** 的 flusher 线程。复现率从"23 次不卡"提升到"约 1/3 卡死"。 + +3. **SIGABRT 看门狗 + gdb 作父进程** —— 罕见卡死无法 `gdb -p` 附加(ptrace_scope=1、无 sudo)。解法:探针内置看门狗线程,8 秒无进展则 `raise(SIGABRT)`;在 gdb 下 `run` 因信号自动中断,随后的 `-ex "thread apply all bt"` 便能在**卡死现场**抓全部线程栈。 + +最后用**逐事件插桩**(flush_sync 入口/出口、worker 完成事件、worker 心跳)打印到 stderr,在卡死时读末尾几行,精确还原控制流。 + +## 根因:`flush_completed_cv_` 丢失唤醒 + +`AsyncPostQueue::flush_sync()` 用 token 机制等待 flush 完成: + +```cpp +// flush_sync() —— 唯一无超时的阻塞点 +uint64_t my_token = flush_token_.fetch_add(1, acq_rel) + 1; +flush_requested_.store(true, release); +cv_.notify_one(); +std::unique_lock lock(flush_completed_mu_); +flush_completed_cv_.wait(lock, [my_token] { // 无 timeout + return flush_completed_.load(acquire) >= my_token || !running_; +}); +``` + +修复前,worker 完成 flush 时**未持锁**就 notify: + +```cpp +// worker_loop(修复前) +flush_completed_.store(current_token, release); +flush_completed_cv_.notify_all(); // ← 未持有 flush_completed_mu_ +``` + +致命窗口:flusher 已判定 predicate 为 false(`completed < my_token`)、尚未进入 futex 期间,worker 的 `notify_all` 正好飞过 —— 没人在 futex 里,通知**丢失**。flusher 随即进入 futex 永久阻塞,而该 `wait` **没有超时兜底**。 + +逐事件追踪抓到的现场(token 627): + +``` +[F-in ] tk=627 (token=627 comp=626) ← flusher 进入 wait(626>=627 false) +[W-done] comp=627 (flag was 1) ← worker 已置 completed=627 并 notify_all(未持锁) +[W-beat] qsize=0 fr=0 …8 秒… ← 再无 [F-out] tk=627,flusher 永久卡死 +``` + +**指纹**:`flush_token_ == flush_completed_`(token 已完成)却仍卡在 `wait`。这看似自相矛盾——predicate 明明为真——但 `wait(lock, pred)` 只在**被唤醒或伪唤醒时**才重判 predicate;notify 已丢,永远不重判。worker 之所以之后 `fr=0`,是因为 flusher 卡死、发不出 #628。 + +> 为何 clang/CI 更易触发:纯粹是该窗口的时序敏感,CI 高负载让 worker 与 flusher 的调度更容易撞进这个窗口,非编译器 bug。 + +## 修复:持锁 notify 关闭窗口 + +`pthread_cond_wait` 的语义保证"**原子地释放用户锁 + 登记为等待者**"。因此,只要 notify 方**持有同一把锁**,waiter 要么还没进 `wait`(之后取锁、重判 predicate 为真即返回),要么已在 futex(被这次 notify 唤醒、重判为真返回)。两个分支都不会丢。 + +关键决策: + +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| worker/stop 持 `flush_completed_mu_` 做 `store + notify_all` | 关闭丢失唤醒窗口,根治 | 给 `flush_completed_cv_.wait` 加超时:只是用轮询掩盖竞态(违背项目"no silent fallbacks"),且仍可能漏判 | +| notify 在锁内(而非解锁后) | `pthread_cond_wait` 原子解锁+登记语义要求 notify 持锁才能保证不丢;flush 是低频路径,唤醒后短暂等锁的开销可忽略 | 解锁后 notify:仍残留更小的丢唤醒窗口 | +| 不改 worker 唤醒路径(`cv_`/`wakeMu_`) | worker 用 `wait_for(10ms)` 有超时自愈,丢唤醒最多 10ms 延迟,非死锁 | 给 `submit` 热路径加 `wakeMu_` 锁竞争:得不偿失 | + +代码见 [async_queue.cpp](../../desktop/base/logger/src/async_queue/async_queue.cpp) 的 `worker_loop()` 与 `stop()`。 + +## 次因:`MpscQueue::readPos_` 数据竞争 + +TSan 顺带抓到的独立缺陷:`readPos_` 原为**非原子** `size_type`(Vyukov 队列的单消费者私有位),却被生产者经 `size()` 读取(`submit()` 的 drop 策略用它)。worker 写、生产者读,无同步 → 数据竞争(UB)。x86-64 上对齐 8 字节不会撕裂,故不致死锁,只影响 drop 判定的准确性,但是真实的并发 UB。 + +修复:`readPos_` 改为 `std::atomic`,消费者 `relaxed` 读 / `release` 推进,生产者 `size()`/`empty()` 用 `acquire` 读。代码见 [mpsc_queue.hpp](../../base/include/base/lockfree/mpsc_queue.hpp)。 + +## 验证 + +| 验证项 | 修复前 | 修复后 | +|--------|--------|--------| +| 独立探针(clang `-O2`)猛跑 | ~1/3 卡死 | 80 次零卡死 | +| TSan 探针 | 数据竞争 + 卡死 | **完全干净**,三次全到 `ALL DONE` | +| 项目 4 个 logger 测试 ×6 | — | 全通过 | +| `mpsc_queue_test` ×8 | — | 全通过 | +| clang `-Werror` 语法检查 | — | 通过 | + +两处改动共 +45/−21 行,均在并发核心,附详细注释说明"为何必须持锁 notify"。 + +## 可复用的经验 + +- **罕见竞态别靠多跑**:先用 TSan 拿确定性的 happens-before 证据,再缩小范围。 +- **绕过重依赖做最小探针**:能脱离 Qt/CMake 直接 `clang++` 编译的子系统,复现迭代快一个数量级。 +- **看门狗 + gdb 父进程**是无 sudo/受限 ptrace 环境下抓"卡死现场全栈"的通用套路。 +- **`condition_variable` notify 不持锁**是经典坑:带 predicate 的 `wait` 能挡住"notify 先于 wait 进入"的常见情况,但挡不住"notify 落在 pred 判定与 futex 进入之间"的窗口——只要 `wait` 无超时,这个窗口就是永久死锁。持锁 notify 是根治。 +- **`token == completed` 却卡在 wait** 是丢失唤醒的典型指纹:值已更新,但唤醒已丢,predicate 不再被重判。 diff --git a/document/notes/index.md b/document/notes/index.md index 4a1e84f4f..b16e0d5d9 100644 --- a/document/notes/index.md +++ b/document/notes/index.md @@ -89,6 +89,19 @@ description: 本文档系列详细介绍了桌面应用程序中窗口行为建 --- +### [07 - Logger Flush 丢失唤醒死锁](07-Logger-Flush-Lost-Wakeup-Hang.md) + +记录 `logger_concurrency_test` 在 clang/CI 下极端偶然卡死的完整排查与修复,包括: + +- 罕见竞态的定位方法论(TSan 确定性取证 → 脱离 Qt 的最小探针 → SIGABRT 看门狗 + gdb 父进程抓栈 → 逐事件插桩) +- 根因:`flush_completed_cv_` 丢失唤醒(worker notify 未持锁 + wait 无超时) +- 次因:`MpscQueue::readPos_` 数据竞争 +- 修复(持锁 notify)与验证矩阵 + +**适用场景**:排查 `condition_variable` 卡死、无超时 wait 的丢失唤醒、或需要复现罕见并发 bug 时参考。 + +--- + ## 架构概览 ```text From 3a4903096776e23004d30e6f435b9426a9e091c9 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Tue, 16 Jun 2026 21:51:54 +0800 Subject: [PATCH 12/18] feat: complete minimal desktop closed-loop (taskbar, launcher, window tracking) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the last three milestones of the see-desktop -> tap-icon -> launch-app loop with taskbar running indicators. MS3 taskbar: taskbar/{app_entry,taskbar_icon,centered_taskbar} — bottom-edge IPanel, centered icon tiles with hover zoom, self-drawn ripple, running dot; registered via PanelManager. MS4 launcher: launcher/app_launch_service — QProcess::startDetached (splitCommand for args), returns cf::expected, failures logged and never swallowed; taskbar appClicked wired to launch. MS5 window tracking: window_info.h + IWindow::pid() (WSLX11Window reads _NET_WM_PID); WindowManager tracks windows and emits windowAppeared/Disappeared(pid) via QObject::destroyed (window_gone weak is already expired); taskbar indicator lights on matching window appear/gone (PID match; reliable for direct launches like xterm). Verified: fast build, doxygen lint, clang-format, offscreen launch. --- desktop/ui/CFDesktopEntity.cpp | 71 ++++++ desktop/ui/components/CMakeLists.txt | 4 + desktop/ui/components/IWindow.h | 7 + desktop/ui/components/WindowManager.cpp | 32 +++ desktop/ui/components/WindowManager.h | 21 ++ desktop/ui/components/launcher/CMakeLists.txt | 18 ++ .../launcher/app_launch_service.cpp | 56 +++++ .../components/launcher/app_launch_service.h | 69 ++++++ desktop/ui/components/taskbar/CMakeLists.txt | 20 ++ desktop/ui/components/taskbar/app_entry.h | 63 +++++ .../components/taskbar/centered_taskbar.cpp | 150 ++++++++++++ .../ui/components/taskbar/centered_taskbar.h | 213 +++++++++++++++++ .../ui/components/taskbar/taskbar_icon.cpp | 220 +++++++++++++++++ desktop/ui/components/taskbar/taskbar_icon.h | 226 ++++++++++++++++++ desktop/ui/components/window_info.h | 53 ++++ .../ui/platform/linux_wsl/wsl_x11_window.cpp | 23 +- .../ui/platform/linux_wsl/wsl_x11_window.h | 11 + document/status/current.md | 6 +- 18 files changed, 1259 insertions(+), 4 deletions(-) create mode 100644 desktop/ui/components/launcher/CMakeLists.txt create mode 100644 desktop/ui/components/launcher/app_launch_service.cpp create mode 100644 desktop/ui/components/launcher/app_launch_service.h create mode 100644 desktop/ui/components/taskbar/CMakeLists.txt create mode 100644 desktop/ui/components/taskbar/app_entry.h create mode 100644 desktop/ui/components/taskbar/centered_taskbar.cpp create mode 100644 desktop/ui/components/taskbar/centered_taskbar.h create mode 100644 desktop/ui/components/taskbar/taskbar_icon.cpp create mode 100644 desktop/ui/components/taskbar/taskbar_icon.h create mode 100644 desktop/ui/components/window_info.h diff --git a/desktop/ui/CFDesktopEntity.cpp b/desktop/ui/CFDesktopEntity.cpp index 665b4f5c1..a8ff0008f 100644 --- a/desktop/ui/CFDesktopEntity.cpp +++ b/desktop/ui/CFDesktopEntity.cpp @@ -8,11 +8,15 @@ #include "components/DisplayServerBackendFactory.h" #include "components/IDisplayServerBackend.h" #include "components/PanelManager.h" +#include "components/WindowManager.h" +#include "components/launcher/app_launch_service.h" #include "components/statusbar/status_bar.h" +#include "components/taskbar/centered_taskbar.h" #include "platform/DesktopPropertyStrategyFactory.h" #include "platform/display_backend_helper.h" #include "platform/shell_layer_helper.h" #include "qt_format.h" +#include #include namespace cf::desktop { @@ -105,8 +109,75 @@ CFDesktopEntity::RunsSetupResult CFDesktopEntity::run_init(RunsSetupMethod m) { auto* status_bar = new cf::desktop::desktop_component::StatusBar(desktop_entity_); panel_mgr->registerPanel(status_bar->GetWeak()); status_bar->show(); + + // ── Window manager: track external windows and link them to the taskbar ── + // app_pid maps a launched app_id to its process id so window tracking can + // match external windows back to a taskbar entry. + auto app_pid = std::make_shared>(); + auto* window_mgr = new cf::desktop::WindowManager(desktop_entity_); + if (display_backend_) { + auto window_backend = display_backend_->windowBackend(); + if (window_backend) { + window_mgr->setBackend(window_backend); + } + } + + // ── Taskbar: bottom-edge panel (centered app icons) ── + // apps is captured by the click handler to resolve app_id -> exec_command. + const QList apps = + cf::desktop::desktop_component::defaultApps(); + auto* taskbar = new cf::desktop::desktop_component::CenteredTaskbar(desktop_entity_); + taskbar->setApps(apps); + panel_mgr->registerPanel(taskbar->GetWeak()); + QObject::connect(taskbar, &cf::desktop::desktop_component::CenteredTaskbar::appClicked, this, + [apps, app_pid](const QString& app_id) { + QString exec; + for (const auto& app : apps) { + if (app.app_id == app_id) { + exec = app.exec_command; + break; + } + } + if (exec.isEmpty()) { + cf::log::warningftag("CFDesktopEntity", "No exec for app_id '{}'", + app_id.toStdString()); + return; + } + const auto launched = + cf::desktop::desktop_component::AppLaunchService::launch(exec); + if (launched.has_value()) { + (*app_pid)[app_id] = *launched; + } + }); + taskbar->show(); panel_mgr->relayout(); + // ── WM ↔ Taskbar: light a running indicator when a window appears/gone ── + QObject::connect(window_mgr, &cf::desktop::WindowManager::windowAppeared, this, + [app_pid, taskbar](qint64 pid) { + if (pid == 0) { + return; + } + for (auto it = app_pid->begin(); it != app_pid->end(); ++it) { + if (it.value() == pid) { + taskbar->updateRunningState(it.key(), true); + return; + } + } + }); + QObject::connect(window_mgr, &cf::desktop::WindowManager::windowDisappeared, this, + [app_pid, taskbar](qint64 pid) { + if (pid == 0) { + return; + } + for (auto it = app_pid->begin(); it != app_pid->end(); ++it) { + if (it.value() == pid) { + taskbar->updateRunningState(it.key(), false); + return; + } + } + }); + // Show the desktop full-screen desktop_entity_->showFullScreen(); diff --git a/desktop/ui/components/CMakeLists.txt b/desktop/ui/components/CMakeLists.txt index 0c9f1c081..af0ec7478 100644 --- a/desktop/ui/components/CMakeLists.txt +++ b/desktop/ui/components/CMakeLists.txt @@ -6,6 +6,8 @@ log_info("Dekstop UI" "Start UI Components Configurations") add_subdirectory(wallpaper) add_subdirectory(shell_layer_impl) add_subdirectory(statusbar) +add_subdirectory(taskbar) +add_subdirectory(launcher) # Create interface library for convenience add_library(cf_desktop_components STATIC) @@ -32,5 +34,7 @@ PRIVATE cfdesktop_shell_layer_impl cfdesktop_wallpaper cfdesktop_statusbar + cfdesktop_taskbar + cfdesktop_launcher Qt6::Core Qt6::Gui Qt6::Widgets ) diff --git a/desktop/ui/components/IWindow.h b/desktop/ui/components/IWindow.h index 6a0a72dd4..810c800a5 100644 --- a/desktop/ui/components/IWindow.h +++ b/desktop/ui/components/IWindow.h @@ -73,6 +73,13 @@ class IWindow : public QObject { */ virtual void raise() = 0; + /** + * @brief Returns the owning process id of this window. + * + * @return The process id, or 0 when the backend cannot determine it. + */ + virtual qint64 pid() const { return 0; } + /** * @brief Creates a weak pointer to this window. * diff --git a/desktop/ui/components/WindowManager.cpp b/desktop/ui/components/WindowManager.cpp index dc3933b55..8d74f705d 100644 --- a/desktop/ui/components/WindowManager.cpp +++ b/desktop/ui/components/WindowManager.cpp @@ -7,6 +7,9 @@ WindowManager::WindowManager(QObject* parent) : QObject(parent) {} void WindowManager::setBackend(WeakPtr backend) { window_backend_ = std::move(backend); + if (auto* backend_raw = window_backend_.Get()) { + connect(backend_raw, &IWindowBackend::window_came, this, &WindowManager::onWindowCame); + } } WeakPtr WindowManager::create_window(const win_id_t& win_id) { @@ -49,4 +52,33 @@ bool WindowManager::raise_a_window(WeakPtr window) { return true; } +void WindowManager::onWindowCame(WeakPtr window) { + if (!window) { + return; + } + auto* window_raw = window.Get(); + if (window_raw == nullptr) { + return; + } + const win_id_t id = window_raw->windowID(); + WindowInfo info; + info.window_id = id; + info.title = window_raw->title(); + info.pid = window_raw->pid(); + info.geometry = window_raw->geometry(); + info.state = WindowState::Normal; + window_infos_[id] = info; + + // The backend emits window_gone after the IWindow object is already + // destroyed (its weak reference expires), so that signal cannot carry the + // id. Watch QObject::destroyed instead: it fires while the entry is still + // identifiable via the captured id. + connect(window_raw, &QObject::destroyed, this, [this, id, pid = info.pid]() { + window_infos_.erase(id); + emit windowDisappeared(pid); + }); + + emit windowAppeared(info.pid); +} + } // namespace cf::desktop diff --git a/desktop/ui/components/WindowManager.h b/desktop/ui/components/WindowManager.h index 4a5cb2333..d0a1c71a5 100644 --- a/desktop/ui/components/WindowManager.h +++ b/desktop/ui/components/WindowManager.h @@ -16,6 +16,7 @@ #pragma once #include "IWindow.h" #include "base/weak_ptr/weak_ptr.h" +#include "window_info.h" #include #include @@ -88,10 +89,30 @@ class WindowManager : public QObject { */ bool raise_a_window(WeakPtr window); + signals: + /** + * @brief Emitted when a tracked window appears. + * + * @param[in] pid Owning process id (0 if unknown). + */ + void windowAppeared(qint64 pid); + + /** + * @brief Emitted when a tracked window disappears. + * + * @param[in] pid Owning process id captured at appearance. + */ + void windowDisappeared(qint64 pid); + private: + /// @brief Records a newly appeared window and watches its destruction. + void onWindowCame(WeakPtr window); + /// Weak reference to the window backend. Ownership: external. WeakPtr window_backend_{nullptr}; /// Tracked windows keyed by window ID (weak references only). Ownership: backend. std::unordered_map, QStringHash> windows_; + /// Observed WindowInfo keyed by window ID. + std::unordered_map window_infos_; }; } // namespace cf::desktop diff --git a/desktop/ui/components/launcher/CMakeLists.txt b/desktop/ui/components/launcher/CMakeLists.txt new file mode 100644 index 000000000..f2e57d575 --- /dev/null +++ b/desktop/ui/components/launcher/CMakeLists.txt @@ -0,0 +1,18 @@ +# App launcher service (QProcess-based external application launching). +add_library(cfdesktop_launcher STATIC + app_launch_service.cpp +) + +target_include_directories(cfdesktop_launcher PUBLIC + $ + $ + $ +) + +target_link_libraries( + cfdesktop_launcher +PRIVATE + Qt6::Core # QProcess, QStringList + cfbase # cf::expected + cflogger # Diagnostic logging for launch success/failure +) diff --git a/desktop/ui/components/launcher/app_launch_service.cpp b/desktop/ui/components/launcher/app_launch_service.cpp new file mode 100644 index 000000000..d0b14e7a2 --- /dev/null +++ b/desktop/ui/components/launcher/app_launch_service.cpp @@ -0,0 +1,56 @@ +/** + * @file app_launch_service.cpp + * @brief External application launch implementation. + * + * Splits the command string with QProcess::splitCommand (so quoted arguments + * and URLs survive), then starts the program detached. The shell is bypassed + * on purpose: a detached process is a direct child, which keeps launches + * predictable and avoids an extra shell process. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-16 + * @version 0.2 + * @since 0.19 + * @ingroup components + */ + +#include "app_launch_service.h" + +#include "cflog.h" + +#include +#include + +namespace cf::desktop::desktop_component { + +namespace { +constexpr const char* kTag = "AppLaunchService"; +} + +cf::expected AppLaunchService::launch(const QString& exec_command) { + if (exec_command.trimmed().isEmpty()) { + cf::log::warningftag(kTag, "launch skipped: empty exec_command"); + return cf::unexpected(AppLaunchError::Empty); + } + + const QStringList parts = QProcess::splitCommand(exec_command); + if (parts.isEmpty()) { + cf::log::warningftag(kTag, "launch skipped: unparsable command '{}'", + exec_command.toStdString()); + return cf::unexpected(AppLaunchError::Unparsable); + } + + const QString program = parts.first(); + const QStringList args = parts.mid(1); + + qint64 pid = 0; + if (QProcess::startDetached(program, args, QString{}, &pid)) { + cf::log::infoftag(kTag, "launched '{}' (pid {})", exec_command.toStdString(), pid); + return pid; + } + + cf::log::errorftag(kTag, "failed to launch '{}'", exec_command.toStdString()); + return cf::unexpected(AppLaunchError::Failed); +} + +} // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/launcher/app_launch_service.h b/desktop/ui/components/launcher/app_launch_service.h new file mode 100644 index 000000000..5ebc1cdc2 --- /dev/null +++ b/desktop/ui/components/launcher/app_launch_service.h @@ -0,0 +1,69 @@ +/** + * @file app_launch_service.h + * @brief Launches external applications via QProcess. + * + * AppLaunchService detaches an external process for a given command string, + * splitting it into program + arguments so commands such as "xdg-open ." or + * "xdg-open https://example.com" resolve correctly. launch() returns the + * process id on success, or an AppLaunchError on failure; failures are logged + * and never silently swallowed. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-16 + * @version 0.2 + * @since 0.19 + * @ingroup components + */ + +#pragma once + +#include "base/expected/expected.hpp" + +#include + +namespace cf::desktop::desktop_component { + +/** + * @brief Reason an application launch did not produce a process. + * + * @ingroup components + */ +enum class AppLaunchError { + Empty, ///< Command was empty or only whitespace. + Unparsable, ///< Command could not be split into program + arguments. + Failed ///< The process could not be started. +}; + +/** + * @brief Stateless service that launches external applications. + * + * @ingroup components + */ +class AppLaunchService { + public: + /// @brief Deleted; AppLaunchService is static-only. + AppLaunchService() = delete; + + /** + * @brief Detaches an external process for a command string. + * + * Splits the command into a program and its arguments, then starts it + * detached so CFDesktop is not the parent and the launched app outlives + * the shell. + * + * @param[in] exec_command The command string (program plus arguments). + * + * @return The started process id, or an AppLaunchError. Empty or + * unparsable commands and launch failures are logged before the + * error is returned; they are never hidden. + * + * @throws None + * @note Failures log a warning or error naming the command. + * @warning None + * @since 0.19 + * @ingroup components + */ + static cf::expected launch(const QString& exec_command); +}; + +} // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/taskbar/CMakeLists.txt b/desktop/ui/components/taskbar/CMakeLists.txt new file mode 100644 index 000000000..4bd46f0fb --- /dev/null +++ b/desktop/ui/components/taskbar/CMakeLists.txt @@ -0,0 +1,20 @@ +# Taskbar implementation (QWidget-based bottom-edge panel). +add_library(cfdesktop_taskbar STATIC + centered_taskbar.cpp + taskbar_icon.cpp +) + +target_include_directories(cfdesktop_taskbar PUBLIC + $ + $ + $ +) + +target_link_libraries( + cfdesktop_taskbar +PUBLIC + cfui # ThemeManager, color/typography tokens, ICFTheme +PRIVATE + Qt6::Widgets + cfbase # WeakPtr / WeakPtrFactory +) diff --git a/desktop/ui/components/taskbar/app_entry.h b/desktop/ui/components/taskbar/app_entry.h new file mode 100644 index 000000000..466f8c631 --- /dev/null +++ b/desktop/ui/components/taskbar/app_entry.h @@ -0,0 +1,63 @@ +/** + * @file app_entry.h + * @brief Application entry data model for the taskbar. + * + * AppEntry describes a single launchable application shown as an icon in the + * CenteredTaskbar. defaultApps() returns a small set of placeholder entries + * (file manager, terminal, settings, browser) used until a real app registry + * exists. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-16 + * @version 0.1 + * @since 0.19 + * @ingroup components + */ + +#pragma once + +#include +#include + +namespace cf::desktop::desktop_component { + +/** + * @brief Describes one launchable application for the taskbar. + * + * @ingroup components + */ +struct AppEntry { + QString app_id; ///< Stable unique identifier (e.g. "terminal"). + QString display_name; ///< Human-readable label (initial is drawn on the tile). + QString icon_path; ///< Optional icon resource path (empty -> use initial). + QString exec_command; ///< Launch command consumed later by QProcess. + bool is_running{false}; ///< Whether the app currently has a live window. +}; + +/** + * @brief Returns the default placeholder application set. + * + * @return A list of sample AppEntry values (files, terminal, settings, + * browser). Their exec_command values are placeholders, not yet + * wired to real launching. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ +inline QList defaultApps() { + return { + {QStringLiteral("files"), QStringLiteral("Files"), QString{}, QStringLiteral("xdg-open ."), + false}, + {QStringLiteral("terminal"), QStringLiteral("Terminal"), QString{}, QStringLiteral("xterm"), + false}, + {QStringLiteral("settings"), QStringLiteral("Settings"), QString{}, + QStringLiteral("cfdesktop-settings"), false}, + {QStringLiteral("browser"), QStringLiteral("Browser"), QString{}, + QStringLiteral("xdg-open https://example.com"), false}, + }; +} + +} // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/taskbar/centered_taskbar.cpp b/desktop/ui/components/taskbar/centered_taskbar.cpp new file mode 100644 index 000000000..28e0059ab --- /dev/null +++ b/desktop/ui/components/taskbar/centered_taskbar.cpp @@ -0,0 +1,150 @@ +/** + * @file centered_taskbar.cpp + * @brief Centered taskbar panel implementation. + * + * Implements the bottom-edge panel: a centered row of TaskbarIcon tiles laid + * out with stretchers on both sides, painted on a translucent Material + * surface with a horizontally-faded top hairline (mirroring the status bar + * seam). Clicks on tiles are forwarded as appClicked(app_id). + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-16 + * @version 0.1 + * @since 0.19 + * @ingroup components + */ + +#include "centered_taskbar.h" + +#include "taskbar_icon.h" + +#include "core/theme_manager.h" +#include "core/token/material_scheme/cfmaterial_token_literals.h" + +#include +#include +#include +#include +#include + +namespace cf::desktop::desktop_component { + +using cf::desktop::PanelPosition; +using namespace cf::ui::core::token::literals; + +namespace { +constexpr int kTaskbarHeight = 64; ///< Bar thickness (px). +constexpr int kSideMargin = 12; ///< Horizontal padding (px). +constexpr int kTopBottomMargin = 4; ///< Vertical padding (px). +constexpr int kIconSpacing = 8; ///< Gap between tiles (px). +constexpr qreal kSurfaceAlpha = 0.92; ///< Surface fill opacity. +} // namespace + +CenteredTaskbar::CenteredTaskbar(QWidget* parent) : QWidget(parent) { + setAttribute(Qt::WA_OpaquePaintEvent); + setAutoFillBackground(false); + setFixedHeight(kTaskbarHeight); + setupUi(); + applyTheme(); +} + +CenteredTaskbar::~CenteredTaskbar() = default; + +void CenteredTaskbar::setupUi() { + layout_ = new QHBoxLayout(this); + layout_->setContentsMargins(kSideMargin, kTopBottomMargin, kSideMargin, kTopBottomMargin); + layout_->setSpacing(kIconSpacing); + + // React to theme switches (ThemeManager is the canonical source). + connect(&cf::ui::core::ThemeManager::instance(), &cf::ui::core::ThemeManager::themeChanged, + this, [this](const cf::ui::core::ICFTheme&) { applyTheme(); }); +} + +void CenteredTaskbar::applyTheme() { + try { + auto& tm = cf::ui::core::ThemeManager::instance(); + const auto& theme = tm.theme(tm.currentThemeName()); + auto& cs = theme.color_scheme(); + background_color_ = cs.queryColor(SURFACE); + divider_color_ = cs.queryColor(OUTLINE_VARIANT); + } catch (...) { + // Fallback palette when no theme is registered yet. + background_color_ = QColor(0xF7, 0xF5, 0xF3); + divider_color_ = QColor(0xCA, 0xC4, 0xD0); + } + update(); +} + +// -- IPanel ---------------------------------------------------------------- +PanelPosition CenteredTaskbar::position() const { + return PanelPosition::Bottom; +} + +int CenteredTaskbar::priority() const { + return 100; +} + +int CenteredTaskbar::preferredSize() const { + return kTaskbarHeight; +} + +QWidget* CenteredTaskbar::widget() const { + return const_cast(this); +} + +// -- Taskbar API ----------------------------------------------------------- +void CenteredTaskbar::setApps(const QList& apps) { + // Clear existing items and their widgets. + while (layout_->count() != 0) { + QLayoutItem* item = layout_->takeAt(0); + if (item->widget() != nullptr) { + item->widget()->deleteLater(); + } + delete item; + } + icons_.clear(); + + // Center the tile row between two stretchers. + layout_->addStretch(); + for (const auto& app : apps) { + auto* icon = new TaskbarIcon(app, this); + connect(icon, &TaskbarIcon::clicked, this, &CenteredTaskbar::appClicked); + layout_->addWidget(icon); + icons_.append(icon); + } + layout_->addStretch(); +} + +void CenteredTaskbar::updateRunningState(const QString& app_id, bool running) { + for (auto* icon : icons_) { + if (icon != nullptr && icon->appId() == app_id) { + icon->setRunning(running); + } + } +} + +// -- Painting -------------------------------------------------------------- +void CenteredTaskbar::paintEvent(QPaintEvent* /*event*/) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing, true); + + // Translucent surface. + QColor bg = background_color_; + bg.setAlphaF(kSurfaceAlpha); + p.fillRect(rect(), bg); + + // Horizontally-faded top hairline, mirroring the status bar seam. + QColor lineMid = divider_color_; + lineMid.setAlphaF(0.45); + QColor lineEdge = divider_color_; + lineEdge.setAlphaF(0.0); + QLinearGradient hairline(0, 0, width(), 0); + hairline.setColorAt(0.0, lineEdge); + hairline.setColorAt(0.08, lineMid); + hairline.setColorAt(0.92, lineMid); + hairline.setColorAt(1.0, lineEdge); + p.setPen(QPen(hairline, 1)); + p.drawLine(QPointF(0, 0.5), QPointF(width(), 0.5)); +} + +} // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/taskbar/centered_taskbar.h b/desktop/ui/components/taskbar/centered_taskbar.h new file mode 100644 index 000000000..218a1c720 --- /dev/null +++ b/desktop/ui/components/taskbar/centered_taskbar.h @@ -0,0 +1,213 @@ +/** + * @file centered_taskbar.h + * @brief Bottom-edge centered taskbar panel. + * + * CenteredTaskbar is the bottom-edge panel implementation behind IPanel. It + * lays out TaskbarIcon tiles in a centered row, paints a translucent Material + * surface with a top divider, follows the active theme, and forwards tile + * clicks as appClicked(app_id). + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-16 + * @version 0.1 + * @since 0.19 + * @ingroup components + */ + +#pragma once + +#include "app_entry.h" +#include "base/weak_ptr/weak_ptr.h" +#include "base/weak_ptr/weak_ptr_factory.h" +#include "components/IPanel.h" + +#include +#include +#include + +class QHBoxLayout; + +namespace cf::desktop::desktop_component { + +class TaskbarIcon; + +/** + * @brief QWidget-based centered taskbar. + * + * Implements the bottom-edge panel contract: a centered row of application + * tiles on a translucent surface. Forwards clicks via appClicked() and is + * registered with PanelManager like the status bar. + * + * @ingroup components + */ +class CenteredTaskbar final : public QWidget, public cf::desktop::IPanel { + Q_OBJECT + public: + /** + * @brief Constructs the taskbar. + * + * @param[in] parent Owning widget (typically the desktop surface). + * + * @throws None + * @note Builds the layout and applies the current theme. + * @warning None + * @since 0.19 + * @ingroup components + */ + explicit CenteredTaskbar(QWidget* parent = nullptr); + + /** + * @brief Destructs the taskbar. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + ~CenteredTaskbar() override; + + // -- IPanel --------------------------------------------------------------- + /** + * @brief Returns the anchoring edge. + * + * @return Always PanelPosition::Bottom. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + cf::desktop::PanelPosition position() const override; + + /** + * @brief Returns the layout priority. + * + * @return Priority value (outermost on the edge). + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + int priority() const override; + + /** + * @brief Returns the bar thickness in pixels. + * + * @return Preferred height in pixels. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + int preferredSize() const override; + + /** + * @brief Returns the widget positioned by the layout engine. + * + * @return This taskbar widget. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + QWidget* widget() const override; + + // -- Taskbar API ---------------------------------------------------------- + /** + * @brief Rebuilds the tile row from an application list. + * + * @param[in] apps The applications to show, centered in the bar. + * + * @throws None + * @note Replaces any previously shown tiles. + * @warning None + * @since 0.19 + * @ingroup components + */ + void setApps(const QList& apps); + + /** + * @brief Updates the running indicator for one application. + * + * @param[in] app_id The application identifier to update. + * @param[in] running true to mark it running. + * + * @throws None + * @note No-op when app_id is not shown. + * @warning None + * @since 0.19 + * @ingroup components + */ + void updateRunningState(const QString& app_id, bool running); + + /** + * @brief Returns a weak reference to this taskbar. + * + * @return WeakPtr valid for this instance's lifetime. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + WeakPtr GetWeak() const { return weak_factory_.GetWeakPtr(); } + + signals: + /** + * @brief Emitted when a tile is clicked. + * + * @param[in] app_id The clicked application identifier. + * + * @since 0.19 + * @ingroup components + */ + void appClicked(const QString& app_id); + + /** + * @brief Emitted to request the application launcher (reserved for MS4). + * + * @since 0.19 + * @ingroup components + */ + void launcherRequested(); + + protected: + /** + * @brief Paints the translucent surface and top divider. + * + * @param[in] event The paint event descriptor. + * + * @throws None + * @note Theme colors are resolved in applyTheme(). + * @warning None + * @since 0.19 + * @ingroup components + */ + void paintEvent(QPaintEvent* event) override; + + private: + /// @brief Creates the centered row layout. + void setupUi(); + /// @brief Resolves theme colors, then repaints. + void applyTheme(); + + QHBoxLayout* layout_{nullptr}; ///< Centered tile row. Ownership: this widget. + QList icons_; ///< Current tiles. Ownership: Qt parented. + + QColor background_color_; ///< Surface fill for the bar. + QColor divider_color_; ///< Top hairline divider color. + + /// Weak pointer factory (must be the last member). + mutable cf::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/taskbar/taskbar_icon.cpp b/desktop/ui/components/taskbar/taskbar_icon.cpp new file mode 100644 index 000000000..759bde570 --- /dev/null +++ b/desktop/ui/components/taskbar/taskbar_icon.cpp @@ -0,0 +1,220 @@ +/** + * @file taskbar_icon.cpp + * @brief Single application icon widget implementation. + * + * Renders one launchable application as a rounded-square tile bearing its + * initial. The tile zooms on hover (QVariantAnimation), plays a self-drawn + * press ripple, and shows a running-state indicator dot. All rendering is + * QPainter-native, mirroring the status bar so it builds everywhere. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-16 + * @version 0.1 + * @since 0.19 + * @ingroup components + */ + +#include "taskbar_icon.h" + +#include "core/theme_manager.h" +#include "core/token/material_scheme/cfmaterial_token_literals.h" +#include "core/token/typography/cfmaterial_typography_token_literals.h" + +#include +#include +#include +#include +#include +#include + +#include + +namespace cf::desktop::desktop_component { + +using namespace cf::ui::core::token::literals; + +namespace { +constexpr int kCellSize = 56; ///< Tile widget edge length (px). +constexpr qreal kIconBase = 36.0; ///< Resting tile square edge (px). +constexpr qreal kHoverScale = 1.2; ///< Scale factor when hovered. +constexpr qreal kIconRadius = 10.0; ///< Tile corner radius (px). +constexpr qreal kDotRadius = 2.5; ///< Running-indicator dot radius (px). +constexpr qreal kDotOffset = 6.0; ///< Dot offset below the tile (px). +constexpr int kLabelPixelSize = 18; ///< Initial letter font size (px). +constexpr int kHoverDurationMs = 150; ///< Hover zoom duration (ms). +constexpr int kRippleDurationMs = 350; ///< Ripple expansion duration (ms). +constexpr int kRippleAlpha = 90; ///< Peak ripple overlay alpha. +constexpr int kHoverOverlayAlpha = 24; ///< Hover state-layer alpha. +} // namespace + +TaskbarIcon::TaskbarIcon(AppEntry entry, QWidget* parent) + : QWidget(parent), entry_(std::move(entry)) { + setFixedSize(kCellSize, kCellSize); + setCursor(Qt::PointingHandCursor); + setAutoFillBackground(false); + setupAnimations(); + applyTheme(); +} + +TaskbarIcon::~TaskbarIcon() = default; + +void TaskbarIcon::setEntry(const AppEntry& entry) { + entry_ = entry; + update(); +} + +void TaskbarIcon::setRunning(bool running) { + if (running_ != running) { + running_ = running; + update(); + } +} + +QSize TaskbarIcon::sizeHint() const { + return {kCellSize, kCellSize}; +} + +// -- Painting -------------------------------------------------------------- +void TaskbarIcon::paintEvent(QPaintEvent* /*event*/) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing, true); + + const QRectF cell = rect(); + const qreal edge = kIconBase * hover_scale_; + const QPointF c = cell.center(); + const QRectF tile(c.x() - edge / 2.0, c.y() - edge / 2.0, edge, edge); + + p.setPen(Qt::NoPen); + + // Tile body. + p.setBrush(tile_color_); + p.drawRoundedRect(tile, kIconRadius, kIconRadius); + + // Hover state overlay (MD3 state layer), shown only while zoomed in. + if (hover_scale_ > 1.001) { + p.setBrush(QColor(foreground_color_.red(), foreground_color_.green(), + foreground_color_.blue(), kHoverOverlayAlpha)); + p.drawRoundedRect(tile, kIconRadius, kIconRadius); + } + + // Press ripple: an expanding circle that fades out. + if (rippling_) { + const qreal maxRadius = std::hypot(cell.width(), cell.height()) / 2.0; + const qreal radius = ripple_progress_ * maxRadius; + const int alpha = static_cast((1.0 - ripple_progress_) * kRippleAlpha); + p.setBrush(QColor(foreground_color_.red(), foreground_color_.green(), + foreground_color_.blue(), alpha)); + p.drawEllipse(ripple_center_, radius, radius); + } + + // Initial letter. + p.setPen(foreground_color_); + p.setFont(label_font_); + const QString letter = entry_.display_name.isEmpty() + ? QStringLiteral("?") + : QString(entry_.display_name.at(0)).toUpper(); + p.drawText(tile, Qt::AlignCenter, letter); + + // Running indicator dot near the tile bottom. + if (running_) { + const QPointF dot(c.x(), tile.bottom() + kDotOffset); + p.setPen(Qt::NoPen); + p.setBrush(indicator_color_); + p.drawEllipse(dot, kDotRadius, kDotRadius); + } +} + +// -- Interaction ----------------------------------------------------------- +void TaskbarIcon::enterEvent(QEnterEvent* /*event*/) { + startHover(true); +} + +void TaskbarIcon::leaveEvent(QEvent* /*event*/) { + startHover(false); + if (rippling_) { + ripple_anim_->stop(); + rippling_ = false; + } + update(); +} + +void TaskbarIcon::mousePressEvent(QMouseEvent* event) { + if (event->button() == Qt::LeftButton) { + startRipple(event->position()); + } + QWidget::mousePressEvent(event); +} + +void TaskbarIcon::mouseReleaseEvent(QMouseEvent* event) { + if (event->button() == Qt::LeftButton && rect().contains(event->position().toPoint())) { + emit clicked(entry_.app_id); + } + QWidget::mouseReleaseEvent(event); +} + +// -- Internal -------------------------------------------------------------- +void TaskbarIcon::applyTheme() { + try { + auto& tm = cf::ui::core::ThemeManager::instance(); + const auto& theme = tm.theme(tm.currentThemeName()); + auto& cs = theme.color_scheme(); + tile_color_ = cs.queryColor(SURFACE_VARIANT); + foreground_color_ = cs.queryColor(ON_SURFACE); + indicator_color_ = cs.queryColor(ON_SURFACE_VARIANT); + label_font_ = theme.font_type().queryTargetFont(TYPOGRAPHY_TITLE_MEDIUM); + label_font_.setPixelSize(kLabelPixelSize); + } catch (...) { + // Fallback palette when no theme is registered yet. + tile_color_ = QColor(0xE7, 0xE0, 0xEC); + foreground_color_ = QColor(0x1C, 0x1B, 0x1F); + indicator_color_ = QColor(0x49, 0x45, 0x4E); + label_font_ = font(); + label_font_.setPixelSize(kLabelPixelSize); + } + update(); +} + +void TaskbarIcon::startHover(bool entering) { + hover_anim_->stop(); + hover_anim_->setStartValue(hover_scale_); + hover_anim_->setEndValue(entering ? qreal(kHoverScale) : qreal(1.0)); + hover_anim_->start(); +} + +void TaskbarIcon::startRipple(const QPointF& center) { + ripple_center_ = center; + ripple_progress_ = 0.0; + rippling_ = true; + ripple_anim_->stop(); + ripple_anim_->setStartValue(qreal(0.0)); + ripple_anim_->setEndValue(qreal(1.0)); + ripple_anim_->start(); +} + +void TaskbarIcon::setupAnimations() { + hover_anim_ = new QVariantAnimation(this); + hover_anim_->setDuration(kHoverDurationMs); + hover_anim_->setEasingCurve(QEasingCurve::OutCubic); + connect(hover_anim_, &QVariantAnimation::valueChanged, this, [this](const QVariant& v) { + hover_scale_ = v.toReal(); + update(); + }); + + ripple_anim_ = new QVariantAnimation(this); + ripple_anim_->setDuration(kRippleDurationMs); + ripple_anim_->setEasingCurve(QEasingCurve::OutQuad); + connect(ripple_anim_, &QVariantAnimation::valueChanged, this, [this](const QVariant& v) { + ripple_progress_ = v.toReal(); + update(); + }); + connect(ripple_anim_, &QVariantAnimation::finished, this, [this]() { + rippling_ = false; + update(); + }); + + // Follow live theme switches (ThemeManager is the canonical source). + connect(&cf::ui::core::ThemeManager::instance(), &cf::ui::core::ThemeManager::themeChanged, + this, [this](const cf::ui::core::ICFTheme&) { applyTheme(); }); +} + +} // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/taskbar/taskbar_icon.h b/desktop/ui/components/taskbar/taskbar_icon.h new file mode 100644 index 000000000..d46fda3b1 --- /dev/null +++ b/desktop/ui/components/taskbar/taskbar_icon.h @@ -0,0 +1,226 @@ +/** + * @file taskbar_icon.h + * @brief Single application icon widget for the centered taskbar. + * + * TaskbarIcon renders one launchable application as a rounded-square tile + * bearing the app's initial. It zooms in on hover, plays a self-drawn press + * ripple, and shows a running-state indicator dot. It emits clicked(app_id) + * on a left-button release inside the tile. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-16 + * @version 0.1 + * @since 0.19 + * @ingroup components + */ + +#pragma once + +#include "app_entry.h" + +#include +#include +#include +#include +#include + +class QEnterEvent; +class QEvent; +class QMouseEvent; +class QVariantAnimation; + +namespace cf::desktop::desktop_component { + +/** + * @brief One application tile shown in the centered taskbar. + * + * Paints a rounded-square glyph tile that zooms on hover, shows a press + * ripple, exposes a running indicator, and emits clicked() on release. All + * colors and typography follow the active Material theme. + * + * @ingroup components + */ +class TaskbarIcon final : public QWidget { + Q_OBJECT + public: + /** + * @brief Constructs the icon for a given application entry. + * + * @param[in] entry The application this tile represents. + * @param[in] parent Owning widget (the taskbar). + * + * @throws None + * @note Resolves theme colors and starts idle at scale 1.0. + * @warning None + * @since 0.19 + * @ingroup components + */ + explicit TaskbarIcon(AppEntry entry, QWidget* parent = nullptr); + + /** + * @brief Destructs the icon. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + ~TaskbarIcon() override; + + /** + * @brief Replaces the application entry backing this tile. + * + * @param[in] entry The new application entry. + * + * @throws None + * @note Triggers a repaint. + * @warning None + * @since 0.19 + * @ingroup components + */ + void setEntry(const AppEntry& entry); + + /** + * @brief Shows or hides the running indicator. + * + * @param[in] running true to show the indicator dot. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + void setRunning(bool running); + + /** + * @brief Returns this tile's application identifier. + * + * @return The app_id of the backing entry. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + const QString& appId() const noexcept { return entry_.app_id; } + + /** + * @brief Returns the preferred tile size. + * + * @return A fixed square size hint. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + QSize sizeHint() const override; + + signals: + /** + * @brief Emitted when the tile is clicked (left-button release inside). + * + * @param[in] app_id The application identifier of this tile. + * + * @since 0.19 + * @ingroup components + */ + void clicked(const QString& app_id); + + protected: + /** + * @brief Paints the tile, overlay, ripple, initial, and indicator. + * + * @param[in] event The paint event descriptor. + * + * @throws None + * @note Theme values are resolved in applyTheme(). + * @warning None + * @since 0.19 + * @ingroup components + */ + void paintEvent(QPaintEvent* event) override; + + /** + * @brief Starts the zoom-in animation on mouse enter. + * + * @param[in] event The enter event descriptor. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + void enterEvent(QEnterEvent* event) override; + + /** + * @brief Reverts the zoom and cancels the ripple on mouse leave. + * + * @param[in] event The leave event descriptor. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + void leaveEvent(QEvent* event) override; + + /** + * @brief Begins a ripple at the press point. + * + * @param[in] event The mouse press event descriptor. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + void mousePressEvent(QMouseEvent* event) override; + + /** + * @brief Emits clicked() when released inside the tile. + * + * @param[in] event The mouse release event descriptor. + * + * @throws None + * @note None + * @warning None + * @since 0.19 + * @ingroup components + */ + void mouseReleaseEvent(QMouseEvent* event) override; + + private: + /// @brief Resolves theme colors and typography, then repaints. + void applyTheme(); + /// @brief Animates the hover scale toward the resting or hovered value. + void startHover(bool entering); + /// @brief Starts an expanding ripple from a center point. + void startRipple(const QPointF& center); + /// @brief Creates the hover and ripple animations and wires them. + void setupAnimations(); + + AppEntry entry_; ///< Backing application entry. + bool running_{false}; ///< Whether the running indicator shows. + qreal hover_scale_{1.0}; ///< Current tile scale (1.0 idle, >1 hover). + qreal ripple_progress_{0.0}; ///< Ripple expansion in [0, 1]. + QPointF ripple_center_; ///< Ripple origin in local coordinates. + bool rippling_{false}; ///< Whether a ripple is animating. + + QColor tile_color_; ///< Tile fill (surface variant). + QColor foreground_color_; ///< Initial text color (on surface). + QColor indicator_color_; ///< Running dot color (on surface variant). + QFont label_font_; ///< Font used for the initial letter. + + QVariantAnimation* hover_anim_{nullptr}; ///< Zoom-in/out animation. + QVariantAnimation* ripple_anim_{nullptr}; ///< Ripple expansion animation. +}; + +} // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/window_info.h b/desktop/ui/components/window_info.h new file mode 100644 index 000000000..5bba75636 --- /dev/null +++ b/desktop/ui/components/window_info.h @@ -0,0 +1,53 @@ +/** + * @file window_info.h + * @brief Tracked-window data model for WindowManager. + * + * WindowInfo captures the observed state of an external window (title, pid, + * geometry, lifecycle state) so the shell can reason about windows without + * owning them. WindowState models the minimal lifecycle the tracker needs. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-16 + * @version 0.1 + * @since 0.19 + * @ingroup components + */ + +#pragma once + +#include +#include + +namespace cf::desktop { + +/** + * @brief Lifecycle state of a tracked window. + * + * Only Normal and Closed are exercised by the tracker today; the remaining + * values are reserved for future window-management work. + * + * @ingroup components + */ +enum class WindowState { + Normal, ///< Visible, resting state. + Minimized, ///< Hidden to the taskbar (reserved). + Maximized, ///< Filling the work area (reserved). + Fullscreen, ///< Full-screen (reserved). + Closing, ///< Close requested, not yet gone (reserved). + Closed ///< Window is gone. +}; + +/** + * @brief Snapshot of an observed external window. + * + * @ingroup components + */ +struct WindowInfo { + QString window_id; ///< Platform window identifier (cf IWindow::windowID). + QString title; ///< Last observed window title. + qint64 pid{0}; ///< Owning process id (0 if unknown). + QRect geometry; ///< Last observed geometry (device px). + WindowState state{WindowState::Normal}; ///< Lifecycle state. +}; + +} // namespace cf::desktop diff --git a/desktop/ui/platform/linux_wsl/wsl_x11_window.cpp b/desktop/ui/platform/linux_wsl/wsl_x11_window.cpp index d0c29862a..03dc88bb8 100644 --- a/desktop/ui/platform/linux_wsl/wsl_x11_window.cpp +++ b/desktop/ui/platform/linux_wsl/wsl_x11_window.cpp @@ -23,7 +23,9 @@ namespace cf::desktop::backend::wsl { WSLX11Window::WSLX11Window(xcb_connection_t* conn, xcb_window_t root, xcb_window_t win, const XcbAtoms& atoms, QObject* parent) - : IWindow(parent), conn_(conn), root_(root), window_(win), atoms_(atoms) {} + : IWindow(parent), conn_(conn), root_(root), window_(win), atoms_(atoms) { + pid_ = readPid(); +} WSLX11Window::~WSLX11Window() = default; @@ -174,6 +176,25 @@ void WSLX11Window::raise() { xcb_flush(conn_); } +qint64 WSLX11Window::pid() const { + return pid_; +} + +qint64 WSLX11Window::readPid() const { + if (!conn_ || window_ == XCB_WINDOW_NONE || atoms_.net_wm_pid == XCB_ATOM_NONE) { + return 0; + } + xcb_get_property_cookie_t cookie = + xcb_get_property(conn_, 0, window_, atoms_.net_wm_pid, XCB_ATOM_CARDINAL, 0, 1); + xcb_get_property_reply_t* reply = xcb_get_property_reply(conn_, cookie, nullptr); + qint64 pid = 0; + if (reply && reply->type != XCB_ATOM_NONE && xcb_get_property_value_length(reply) >= 4) { + pid = static_cast(*static_cast(xcb_get_property_value(reply))); + } + free(reply); + return pid; +} + } // namespace cf::desktop::backend::wsl #endif // CFDESKTOP_OS_LINUX diff --git a/desktop/ui/platform/linux_wsl/wsl_x11_window.h b/desktop/ui/platform/linux_wsl/wsl_x11_window.h index 79edebdfe..bd2fdd32d 100644 --- a/desktop/ui/platform/linux_wsl/wsl_x11_window.h +++ b/desktop/ui/platform/linux_wsl/wsl_x11_window.h @@ -97,6 +97,12 @@ class WSLX11Window : public IWindow { */ void raise() override; + /** + * @brief Returns the owning process id read from _NET_WM_PID. + * @return The process id, or 0 when unavailable. + */ + qint64 pid() const override; + // ── X11-specific ────────────────────────────────────── /** @@ -114,6 +120,11 @@ class WSLX11Window : public IWindow { xcb_window_t window_; /// Cached atoms for property queries. XcbAtoms atoms_; + /// Owning process id, read once from _NET_WM_PID at construction. + qint64 pid_{0}; + + /// @brief Queries _NET_WM_PID once; returns 0 when unavailable. + qint64 readPid() const; }; } // namespace cf::desktop::backend::wsl diff --git a/document/status/current.md b/document/status/current.md index bcbacf11b..90c05bb7b 100644 --- a/document/status/current.md +++ b/document/status/current.md @@ -48,9 +48,9 @@ description: CFDesktop 项目进度的唯一事实来源与全局导航。 ## 下一步路线(最小闭环先行) 1. **MS2 状态栏**(顶部时间 + 系统图标)— ✅ 功能落地(`StatusBar` 实现 + 注册 PanelManager + 主题跟随 + MD3 美化;offscreen 启动通过,待真机视觉确认) -2. **MS3 任务栏**(底部居中图标条 + hover 动画)— ⬜ 待开始 -3. **MS4 应用启动器**(应用网格 + QProcess 启动)— ⬜ 待开始 -4. **MS5 窗口管理**(窗口装饰 + 任务栏联动)— ⬜ 待开始 +2. **MS3 任务栏**(底部居中图标条 + hover 动画)— 🚧 进行中(最小切片跑通:`CenteredTaskbar` 注册 Bottom 面板 + `TaskbarIcon` 居中图标/hover 放大/自绘 ripple/运行指示器,构建通过;点击反馈先打 log,待接 MS4 真启动) +3. **MS4 应用启动器**(应用网格 + QProcess 启动)— 🚧 进行中(最小启动闭环跑通:`AppLaunchService` 用 `QProcess::startDetached` 启动 taskbar 图标对应应用,点 Files/Browser 可真打开;开始菜单弹窗网格待做) +4. **MS5 窗口管理**(窗口装饰 + 任务栏联动)— 🚧 进行中(追踪+联动切片跑通:`WindowManager` 追踪外部窗口 + `IWindow::pid()` + Taskbar 运行指示器联动;靠 PID 匹配,直接启动(xterm)可靠、间接启动(xdg-open)受限;窗口装饰/操作因 WSL X11 客户端架构不可行,暂跳过) 闭环达成后按需推进:HWTier 策略引擎、CrashHandler、IPC、EGLFS 嵌入式后端、输入抽象层、P2/P3 控件。 From 03acbc16043ea6ac090d9ab80beb4ab42cf0fcfa Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Thu, 18 Jun 2026 11:09:11 +0800 Subject: [PATCH 13/18] feat: extract foundation utils into aex submodule (Phase 1) Move the Qt-free, header-only foundation utilities out of base/ into the new aex org-level library (third_party/aex submodule @ v0.1.0): - Migrated to aex: expected, scope_guard, weak_ptr, singleton, factory, lockfree, hash, policy_chain, indexed_vector, span, helpers, macro (+ macros.h). Namespace cf:: -> aex:: in the extracted library. - Retained in base/: hardware probes (system/, device/, utils/) and the windows/linux platform helpers + export.h (consumed by probes). - CFDesktop consumes aex via setup_third_party(); cfbase and its sub-modules link aex::aex. Foundation references across base/desktop/ ui/test/example rewritten to aex:: (cf::desktop/log/config/ui/asciiart stay). - base/ unit tests + example/base/common policy_chain example moved to aex. - .gitignore: un-ignore tracked submodule path (third_party/aex). aex standalone: 231/231 tests pass. CFDesktop: build green, 40/40 tests. --- .gitignore | 4 + .gitmodules | 3 + CMakeLists.txt | 10 + base/CMakeLists.txt | 7 +- base/device/console/console_helper.h | 4 +- base/device/console/impl/console_platform.cpp | 2 +- base/device/console/impl/console_platform.h | 4 +- .../console/impl/unix/console_unix_impl.cpp | 4 +- .../console/impl/win/console_win_impl.cpp | 2 +- base/include/base/expected/expected.hpp | 1667 ----------------- base/include/base/factory/plain_factory.hpp | 92 - .../base/factory/registered_factory.hpp | 174 -- .../base/factory/smartptr_plain_factory.hpp | 129 -- base/include/base/hash/constexpr_fnv1a.hpp | 195 -- base/include/base/helpers/once_init.hpp | 227 --- .../base/indexed_vector/indexed_vector.hpp | 1027 ---------- base/include/base/lockfree/mpsc_queue.hpp | 369 ---- base/include/base/macro/build_type.h | 24 - base/include/base/macro/plain_property.h | 32 - base/include/base/macro/system_judge.h | 48 - base/include/base/macros.h | 13 - .../base/policy_chain/policy_chain.hpp | 335 ---- base/include/base/scope_guard/scope_guard.hpp | 144 -- .../base/singleton/simple_singleton.hpp | 76 - base/include/base/singleton/singleton.hpp | 119 -- base/include/base/span/span.h | 312 --- .../weak_ptr/private/weak_ptr_internals.h | 101 - base/include/base/weak_ptr/weak_ptr.h | 413 ---- base/include/base/weak_ptr/weak_ptr_factory.h | 157 -- base/include/base/windows/co_helper.hpp | 18 +- base/include/base/windows/common.h | 2 +- base/include/system/cpu/cfcpu.h | 6 +- base/include/system/cpu/cfcpu_bonus.h | 12 +- base/include/system/cpu/cfcpu_profile.h | 6 +- base/include/system/gpu/gpu.h | 4 +- .../system/hardware_tier/hardware_tier.h | 7 +- base/include/system/network/network.h | 4 +- base/system/cpu/CMakeLists.txt | 2 + base/system/cpu/cfcpu.cpp | 10 +- base/system/cpu/cfcpu_bonus.cpp | 20 +- base/system/cpu/cfcpu_profile.cpp | 4 +- .../cpu/private/linux_impl/cpu_bonus.cpp | 2 +- .../system/cpu/private/linux_impl/cpu_bonus.h | 4 +- .../cpu/private/linux_impl/cpu_features.h | 2 +- .../cpu/private/linux_impl/cpu_info.cpp | 4 +- base/system/cpu/private/linux_impl/cpu_info.h | 6 +- .../cpu/private/linux_impl/cpu_profile.cpp | 2 +- .../cpu/private/linux_impl/cpu_profile.h | 6 +- .../system/cpu/private/win_impl/cpu_bonus.cpp | 9 +- base/system/cpu/private/win_impl/cpu_bonus.h | 4 +- .../cpu/private/win_impl/cpu_features.cpp | 2 +- .../cpu/private/win_impl/cpu_features.h | 2 +- base/system/cpu/private/win_impl/cpu_info.cpp | 30 +- base/system/cpu/private/win_impl/cpu_info.h | 6 +- .../cpu/private/win_impl/cpu_profile.cpp | 2 +- .../system/cpu/private/win_impl/cpu_profile.h | 6 +- base/system/gpu/CMakeLists.txt | 2 + base/system/gpu/gpu.cpp | 6 +- .../gpu/private/linux_impl/gpu_info.cpp | 2 +- base/system/hardware_tier/CMakeLists.txt | 3 + base/system/hardware_tier/hardware_tier.cpp | 12 +- base/system/memory/CMakeLists.txt | 2 + base/system/memory/memory_info.cpp | 4 +- base/system/network/CMakeLists.txt | 3 + base/system/network/network.cpp | 2 +- .../base/config_manager/include/cfconfig.hpp | 6 +- .../base/logger/include/cflog/cflog_level.hpp | 2 +- .../base/logger/src/async_queue/async_queue.h | 4 +- .../cfpath/desktop_main_path_resolvers.h | 8 +- .../early_session/early_handle/early_handle.h | 6 +- .../desktop_backbone_init.cpp | 3 +- .../desktop_backbone_init.h | 2 +- .../main/init/early_gain/early_pass_stage.h | 4 +- .../main/init/gui_progress/gui_init_stage.h | 4 +- desktop/main/init/init_session_chain.cpp | 8 +- desktop/main/init/init_session_chain.h | 16 +- desktop/main/init/init_settings.h | 4 +- desktop/main/init/init_stage.h | 16 +- desktop/ui/CFDesktop.h | 20 +- desktop/ui/CFDesktopEntity.cpp | 19 +- desktop/ui/CFDesktopEntity.h | 10 +- desktop/ui/CFDesktopProxy.cpp | 2 +- desktop/ui/CFDesktopProxy.h | 6 +- desktop/ui/base/qt_backend.h | 2 +- .../components/DisplayServerBackendFactory.h | 6 +- desktop/ui/components/IDisplayServerBackend.h | 8 +- desktop/ui/components/IShellLayerStrategy.h | 4 +- desktop/ui/components/IWindow.h | 10 +- desktop/ui/components/IWindowBackend.h | 21 +- desktop/ui/components/PanelManager.cpp | 8 +- desktop/ui/components/PanelManager.h | 8 +- desktop/ui/components/WindowManager.cpp | 12 +- desktop/ui/components/WindowManager.h | 22 +- desktop/ui/components/launcher/CMakeLists.txt | 2 +- .../launcher/app_launch_service.cpp | 8 +- .../components/launcher/app_launch_service.h | 4 +- .../DefaultShellLayerStrategy.cpp | 3 +- .../DefaultShellLayerStrategy.h | 6 +- .../WallpaperShellLayerStrategy.cpp | 7 +- .../WallpaperShellLayerStrategy.h | 2 +- .../shell_layer_impl/WidgetShellLayer.cpp | 2 +- .../shell_layer_impl/WidgetShellLayer.h | 10 +- .../shell_layer_impl/wallpaper_src_chain.cpp | 6 +- .../shell_layer_impl/wallpaper_src_chain.h | 8 +- desktop/ui/components/statusbar/status_bar.h | 10 +- .../ui/components/taskbar/centered_taskbar.h | 10 +- .../wallpaper/ImageWallPaperLayer.cpp | 2 +- .../wallpaper/ImageWallPaperLayer.h | 2 +- .../wallpaper/WallPaperAccessStorage.cpp | 12 +- .../wallpaper/WallPaperAccessStorage.h | 22 +- .../components/wallpaper/WallPaperToken.cpp | 8 +- .../ui/components/wallpaper/WallPaperToken.h | 6 +- .../ui/platform/IDesktopDisplaySizeStrategy.h | 8 +- .../ui/platform/linux_wsl/linux_wsl_factory.h | 6 +- .../wsl_x11_display_server_backend.cpp | 6 +- .../wsl_x11_display_server_backend.h | 4 +- .../ui/platform/linux_wsl/wsl_x11_window.h | 2 +- .../linux_wsl/wsl_x11_window_backend.cpp | 8 +- .../linux_wsl/wsl_x11_window_backend.h | 8 +- .../windows_display_server_backend.cpp | 6 +- .../windows/windows_display_server_backend.h | 4 +- .../windows/windows_display_size_policy.cpp | 2 +- desktop/ui/platform/windows/windows_factory.h | 6 +- desktop/ui/platform/windows/windows_window.h | 2 +- .../windows/windows_window_backend.cpp | 8 +- .../platform/windows/windows_window_backend.h | 8 +- example/base/CMakeLists.txt | 3 +- example/base/common/CMakeLists.txt | 22 - example/base/common/example_policy_chain.cpp | 150 -- test/CMakeLists.txt | 4 +- test/base/CMakeLists.txt | 81 - test/base/expected/expected_test.cpp | 721 ------- test/base/hash/constexpr_fnv1a_test.cpp | 202 -- .../indexed_vector/indexed_vector_test.cpp | 609 ------ test/base/lockfree/mpsc_queue_test.cpp | 426 ----- test/base/policy_chain/policy_chain_test.cpp | 464 ----- test/base/scope_guard/scope_guard_test.cpp | 656 ------- test/base/weak_ptr/weak_ptr_test.cpp | 279 --- test/desktop/init/init_session_chain_test.cpp | 20 +- .../logger_error_handling_test.cpp | 2 +- third_party/aex | 1 + ui/components/animation.h | 8 +- ui/components/animation_factory_manager.h | 22 +- ui/components/animation_group.h | 14 +- .../material/cfmaterial_animation_factory.cpp | 34 +- .../material/cfmaterial_animation_factory.h | 46 +- .../material/cfmaterial_fade_animation.h | 14 +- .../material/cfmaterial_property_animation.h | 10 +- .../material/cfmaterial_scale_animation.h | 14 +- .../material/cfmaterial_slide_animation.h | 14 +- ui/core/material/material_factory.cpp | 8 +- ui/core/material/material_factory.hpp | 4 +- ui/core/theme_manager.h | 6 +- ui/widget/application_support/application.cpp | 4 +- ui/widget/application_support/application.h | 16 +- .../material/base/elevation_controller.cpp | 4 +- .../material/base/elevation_controller.h | 10 +- ui/widget/material/base/focus_ring.cpp | 4 +- ui/widget/material/base/focus_ring.h | 9 +- .../material/base/material_widget_base.cpp | 2 +- .../material/base/material_widget_base.h | 2 +- ui/widget/material/base/ripple_helper.cpp | 6 +- ui/widget/material/base/ripple_helper.h | 6 +- ui/widget/material/base/state_machine.cpp | 4 +- ui/widget/material/base/state_machine.h | 10 +- .../material/widget/checkbox/checkbox.cpp | 2 +- .../material/widget/comboBox/combobox.cpp | 6 +- .../material/widget/groupbox/groupbox.cpp | 966 +++++----- ui/widget/material/widget/groupbox/groupbox.h | 554 +++--- ui/widget/material/widget/label/label.h | 644 +++---- .../widget/progressbar/progressbar.cpp | 4 +- .../widget/radiobutton/radiobutton.cpp | 2 +- .../material/widget/scrollview/scrollview.cpp | 2 +- .../material/widget/scrollview/scrollview.h | 4 +- ui/widget/material/widget/switch/switch.cpp | 2 +- .../widget/tabview/private/materialtabbar.cpp | 2 +- .../widget/tabview/private/materialtabbar.h | 4 +- .../material/widget/textarea/textarea.cpp | 2 +- .../material/widget/textfield/textfield.cpp | 4 +- 179 files changed, 1632 insertions(+), 10838 deletions(-) create mode 100644 .gitmodules delete mode 100644 base/include/base/expected/expected.hpp delete mode 100644 base/include/base/factory/plain_factory.hpp delete mode 100644 base/include/base/factory/registered_factory.hpp delete mode 100644 base/include/base/factory/smartptr_plain_factory.hpp delete mode 100644 base/include/base/hash/constexpr_fnv1a.hpp delete mode 100644 base/include/base/helpers/once_init.hpp delete mode 100644 base/include/base/indexed_vector/indexed_vector.hpp delete mode 100644 base/include/base/lockfree/mpsc_queue.hpp delete mode 100644 base/include/base/macro/build_type.h delete mode 100644 base/include/base/macro/plain_property.h delete mode 100644 base/include/base/macro/system_judge.h delete mode 100644 base/include/base/macros.h delete mode 100644 base/include/base/policy_chain/policy_chain.hpp delete mode 100644 base/include/base/scope_guard/scope_guard.hpp delete mode 100644 base/include/base/singleton/simple_singleton.hpp delete mode 100644 base/include/base/singleton/singleton.hpp delete mode 100644 base/include/base/span/span.h delete mode 100644 base/include/base/weak_ptr/private/weak_ptr_internals.h delete mode 100644 base/include/base/weak_ptr/weak_ptr.h delete mode 100644 base/include/base/weak_ptr/weak_ptr_factory.h delete mode 100644 example/base/common/CMakeLists.txt delete mode 100644 example/base/common/example_policy_chain.cpp delete mode 100644 test/base/CMakeLists.txt delete mode 100644 test/base/expected/expected_test.cpp delete mode 100644 test/base/hash/constexpr_fnv1a_test.cpp delete mode 100644 test/base/indexed_vector/indexed_vector_test.cpp delete mode 100644 test/base/lockfree/mpsc_queue_test.cpp delete mode 100644 test/base/policy_chain/policy_chain_test.cpp delete mode 100644 test/base/scope_guard/scope_guard_test.cpp delete mode 100644 test/base/weak_ptr/weak_ptr_test.cpp create mode 160000 third_party/aex diff --git a/.gitignore b/.gitignore index a45cf02ff..5e3fcc80d 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,11 @@ document/api/ .vscode # third_party dependencies (downloaded during configuration) +# FetchContent-downloaded deps stay ignored; tracked submodules are re-included +# so their gitlink + .gitmodules are committed. third_party/*/ +!third_party/aex/ +!third_party/QuarkWidgets/ !third_party/.gitkeep __pycache__/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..251ea9965 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "third_party/aex"] + path = third_party/aex + url = https://github.com/Awesome-Embedded-Learning-Studio/aex.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 88fb969fe..ecab9b9b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,16 @@ cf_generate_desktop_settings() # Apply desktop root overlay (needs CFDESKTOP_DEFAULT_ROOT from above) cf_apply_desktop_root_overlay() +# ============================================================ +# Third-party: aex (foundation utils, header-only). MUST precede base — +# the slimmed cfbase links aex::aex for expected/scope_guard/macro. +# ============================================================ +include(cmake/third_party_helper.cmake) +setup_third_party( + NAME aex + GIT_REPOSITORY https://github.com/Awesome-Embedded-Learning-Studio/aex.git + GIT_TAG v0.1.0) + # Log base module start log_module_start("base") add_subdirectory(base) diff --git a/base/CMakeLists.txt b/base/CMakeLists.txt index 58a8485b4..b592043b1 100644 --- a/base/CMakeLists.txt +++ b/base/CMakeLists.txt @@ -2,12 +2,16 @@ cmake_minimum_required(VERSION 3.16) # ============================================================ -# Header-only library for base/include (utilities, hash, expected, etc.) +# Header-only exposure for base/include (system probes + platform helpers). +# Foundation utilities (expected, scope_guard, weak_ptr, macro, ...) were +# extracted to the aex submodule; retained headers (e.g. windows/co_helper.hpp) +# include them, so aex::aex is propagated here. # ============================================================ add_library(cfbase_headers INTERFACE) target_include_directories(cfbase_headers INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include ) +target_link_libraries(cfbase_headers INTERFACE aex::aex) add_library(CFDesktop::base_headers ALIAS cfbase_headers) # Include subdirectory CMakeLists (static libraries) @@ -60,6 +64,7 @@ target_include_directories(cfbase PUBLIC # Link dependencies target_link_libraries(cfbase PUBLIC Qt6::Core + aex::aex ) # Platform-specific link libraries diff --git a/base/device/console/console_helper.h b/base/device/console/console_helper.h index a1e1d4581..1ec4894a0 100644 --- a/base/device/console/console_helper.h +++ b/base/device/console/console_helper.h @@ -1,6 +1,6 @@ #pragma once +#include "aex/policy_chain/policy_chain.hpp" #include "base/export.h" -#include "base/policy_chain/policy_chain.hpp" #include "console_defines.h" #include #include @@ -8,7 +8,7 @@ namespace cf::base::device::console { -using cf::PolicyChain; +using aex::PolicyChain; /** * @file console_helper.h diff --git a/base/device/console/impl/console_platform.cpp b/base/device/console/impl/console_platform.cpp index e39cf76e6..96704a8a6 100644 --- a/base/device/console/impl/console_platform.cpp +++ b/base/device/console/impl/console_platform.cpp @@ -1,5 +1,5 @@ #include "console_platform.h" -#include "base/macros.h" +#include "aex/macros.h" #include #include diff --git a/base/device/console/impl/console_platform.h b/base/device/console/impl/console_platform.h index 8a1b1853c..cd14bd6e2 100644 --- a/base/device/console/impl/console_platform.h +++ b/base/device/console/impl/console_platform.h @@ -1,6 +1,6 @@ #pragma once #include "../console_defines.h" -#include "base/policy_chain/policy_chain.hpp" +#include "aex/policy_chain/policy_chain.hpp" #include namespace cf::base::device::console { @@ -19,7 +19,7 @@ static constexpr console::console_size_t INVALID_ONE{-1, -1}; * @ingroup device */ -using cf::PolicyChain; +using aex::PolicyChain; /** * @brief Creates the platform-specific console size policy chain. diff --git a/base/device/console/impl/unix/console_unix_impl.cpp b/base/device/console/impl/unix/console_unix_impl.cpp index cc1b602d8..58d3454d0 100644 --- a/base/device/console/impl/unix/console_unix_impl.cpp +++ b/base/device/console/impl/unix/console_unix_impl.cpp @@ -1,4 +1,4 @@ -#include "base/macro/system_judge.h" +#include "aex/macro/system_judge.h" #ifndef CFDESKTOP_OS_LINUX # error "Unexpected Includes As This File is using for Linux/Unix Implementations" @@ -13,7 +13,7 @@ # include namespace cf::base::device::impl::Unix { console::console_size_t console_size() { - struct winsize console_window_size {}; + struct winsize console_window_size{}; const auto result = ioctl(STDOUT_FILENO, TIOCGWINSZ, &console_window_size); if (result == 0) { return {console_window_size.ws_col, console_window_size.ws_row}; diff --git a/base/device/console/impl/win/console_win_impl.cpp b/base/device/console/impl/win/console_win_impl.cpp index 004ebec4d..10af5ae42 100644 --- a/base/device/console/impl/win/console_win_impl.cpp +++ b/base/device/console/impl/win/console_win_impl.cpp @@ -1,4 +1,4 @@ -#include "base/macro/system_judge.h" +#include "aex/macro/system_judge.h" #ifndef CFDESKTOP_OS_WINDOWS # error "Unexpected Includes As This File is using for Windows Implementations" diff --git a/base/include/base/expected/expected.hpp b/base/include/base/expected/expected.hpp deleted file mode 100644 index c6b66efe1..000000000 --- a/base/include/base/expected/expected.hpp +++ /dev/null @@ -1,1667 +0,0 @@ -/** - * @file base/include/base/expected/expected.hpp - * @brief Provides a C++17 implementation of std::expected (C++23). - * - * Offers a type-safe error handling mechanism that uses values instead of - * exceptions. Compatible with compilers that do not yet support C++23. - * - * @author Charliechen114514 - * @date 2026-02-22 - * @version 0.1 - * @since 0.1 - * @ingroup base_utilities - */ -#pragma once - -#include -#include -#include -#include - -namespace cf { - -/** - * @brief Exception thrown when accessing the value of an expected that - * contains an error. - * - * @ingroup base_utilities - */ -template class bad_expected_access; - -/** - * @brief Specialization of bad_expected_access for void error type. - * - * @ingroup base_utilities - */ -template <> class bad_expected_access : public std::exception { - public: - /** - * @brief Returns the explanatory string. - * - * @return Pointer to "bad_expected_access" string. - * - * @throws None. - * - * @since 0.1 - * @ingroup base_utilities - */ - const char* what() const noexcept override { return "bad_expected_access"; } -}; - -/** - * @brief Exception thrown when accessing the value of an expected in error - * state. - * - * @tparam E Error type stored in the expected. - * - * @ingroup base_utilities - */ -template class bad_expected_access : public bad_expected_access { - public: - /** - * @brief Constructs the exception with an error value. - * - * @param[in] e Error value to store. - * - * @ingroup base_utilities - */ - explicit bad_expected_access(E e) : error_(std::move(e)) {} - - /** - * @brief Returns a const reference to the stored error. - * - * @return Const reference to the error. - * - * @throws None. - * - * @ingroup base_utilities - */ - const E& error() const& noexcept { return error_; } - - /** - * @brief Returns a reference to the stored error. - * - * @return Reference to the error. - * - * @throws None. - * - * @ingroup base_utilities - */ - E& error() & noexcept { return error_; } - - /** - * @brief Returns a const rvalue reference to the stored error. - * - * @return Const rvalue reference to the error. - * - * @throws None. - * - * @ingroup base_utilities - */ - const E&& error() const&& noexcept { return std::move(error_); } - - /** - * @brief Returns an rvalue reference to the stored error. - * - * @return Rvalue reference to the error. - * - * @throws None. - * - * @ingroup base_utilities - */ - E&& error() && noexcept { return std::move(error_); } - - private: - /// Stored error value. Ownership: owner. - E error_; -}; - -/** - * @brief Wrapper type for constructing expected objects in error state. - * - * @tparam E Error type to wrap. - * - * @ingroup base_utilities - */ -template class unexpected { - static_assert(!std::is_void_v, "E must not be void"); - static_assert(!std::is_reference_v, "E must not be a reference"); - - public: - /// Deleted default constructor. - unexpected() = delete; - - /** - * @brief Constructs unexpected from an error value. - * - * @tparam Err Forwarded error type. - * @param[in] e Error value to wrap. - * - * @ingroup base_utilities - */ - template , unexpected> && - std::is_constructible_v>> - constexpr explicit unexpected(Err&& e) : error_(std::forward(e)) {} - - /** - * @brief Constructs unexpected in-place with arguments. - * - * @tparam Args Constructor argument types. - * @param[in] args Arguments to forward to E constructor. - * - * @ingroup base_utilities - */ - template >> - constexpr explicit unexpected(std::in_place_t, Args&&... args) - : error_(std::forward(args)...) {} - - /** - * @brief Constructs unexpected in-place with initializer list. - * - * @tparam U Element type of initializer list. - * @tparam Args Constructor argument types. - * @param[in] il Initializer list. - * @param[in] args Additional arguments. - * - * @ingroup base_utilities - */ - template < - typename U, typename... Args, - typename = std::enable_if_t&, Args...>>> - constexpr explicit unexpected(std::in_place_t, std::initializer_list il, Args&&... args) - : error_(il, std::forward(args)...) {} - - /** - * @brief Returns const reference to the error. - * - * @return Const reference to the error. - * - * @throws None. - * - * @ingroup base_utilities - */ - constexpr const E& error() const& noexcept { return error_; } - - /** - * @brief Returns reference to the error. - * - * @return Reference to the error. - * - * @throws None. - * - * @ingroup base_utilities - */ - constexpr E& error() & noexcept { return error_; } - - /** - * @brief Returns const rvalue reference to the error. - * - * @return Const rvalue reference to the error. - * - * @throws None. - * - * @ingroup base_utilities - */ - constexpr const E&& error() const&& noexcept { return std::move(error_); } - - /** - * @brief Returns rvalue reference to the error. - * - * @return Rvalue reference to the error. - * - * @throws None. - * - * @ingroup base_utilities - */ - constexpr E&& error() && noexcept { return std::move(error_); } - - /** - * @brief Swaps the error with another unexpected. - * - * @param[in,out] other Other unexpected to swap with. - * - * @throws None if E is nothrow swappable. - * - * @note None. - * - * @warning None. - * - * @since 0.1 - * @ingroup base_utilities - */ - void swap(unexpected& other) noexcept(std::is_nothrow_swappable_v) { - using std::swap; - swap(error_, other.error_); - } - - /** - * @brief Compares two unexpected objects for equality. - * - * @tparam E2 Error type of the other unexpected. - * @param[in] a First unexpected. - * @param[in] b Second unexpected. - * - * @return true if errors are equal, false otherwise. - * - * @throws None. - * - * @ingroup base_utilities - */ - template - friend constexpr bool operator==(const unexpected& a, const unexpected& b) { - return a.error_ == b.error(); - } - - private: - /// Stored error value. Ownership: owner. - E error_; -}; - -/// Deduction guide for unexpected template constructor. -/// @cond DeductionGuides -template unexpected(E) -> unexpected; -/// @endcond - -/** - * @brief Tag type for constructing expected in error state. - * - * @ingroup base_utilities - */ -struct unexpect_t { - /** - * @brief Default constructor. - * - * @ingroup base_utilities - */ - explicit unexpect_t() = default; -}; - -/// Global unexpect tag instance. -inline constexpr unexpect_t unexpect{}; - -/** - * @brief A wrapper that contains either a value or an error. - * - * Provides type-safe error handling without exceptions. The object either - * contains a value of type T or an error of type E. - * - * @tparam T Value type. - * @tparam E Error type. - * - * @ingroup base_utilities - */ -template class expected { - static_assert(!std::is_void_v, "E must not be void"); - static_assert(!std::is_reference_v, - "T must not be a reference (use T* or std::reference_wrapper)"); - static_assert(!std::is_reference_v, "E must not be a reference"); - - /** - * @brief Internal storage union for value or error. - * - * Discriminated union that holds either the value or the error. - * Only one member is active at any time. - * - * @note Trivial (no explicit initialization). - * - * @warning Direct access to members is unsafe; use has_val_ to - * determine which member is active. - * - * @since 0.1 - * @ingroup base_utilities - * @internal - */ - union Storage { - T val; ///< Value storage. - - E err; ///< Error storage. - - /** - * @brief Default constructor. - * - * @throws None. - * - * @since 0.1 - * @ingroup base_utilities - */ - Storage() {} - - /** - * @brief Destructor. - * - * @throws None. - * - * @since 0.1 - * @ingroup base_utilities - */ - ~Storage() {} - }; - - Storage storage_; ///< Internal storage. - bool has_val_; ///< Indicates whether value is present. - - /** - * @brief Destroys the currently stored value or error. - * - * @throws None. - * - * @ingroup base_utilities - */ - void destroy() noexcept { - if (has_val_) - storage_.val.~T(); - else - storage_.err.~E(); - } - - public: - using value_type = T; ///< Value type alias. - using error_type = E; ///< Error type alias. - using unexpected_type = unexpected; ///< Unexpected type alias. - - /** - * @brief Default constructor. Value-initializes T. - * - * @throws May throw if T default constructor throws. - * - * @ingroup base_utilities - */ - constexpr expected() noexcept(std::is_nothrow_default_constructible_v) : has_val_(true) { - ::new (&storage_.val) T(); - } - - /** - * @brief Copy constructor. - * - * @param[in] o Expected to copy from. - * - * @throws May throw if T or E copy constructor throws. - * - * @ingroup base_utilities - */ - expected(const expected& o) : has_val_(o.has_val_) { - if (has_val_) - ::new (&storage_.val) T(o.storage_.val); - else - ::new (&storage_.err) E(o.storage_.err); - } - - /** - * @brief Move constructor. - * - * @param[in] o Expected to move from. - * - * @throws May throw if T or E move constructor throws. - * - * @ingroup base_utilities - */ - expected(expected&& o) noexcept(std::is_nothrow_move_constructible_v && - std::is_nothrow_move_constructible_v) - : has_val_(o.has_val_) { - if (has_val_) - ::new (&storage_.val) T(std::move(o.storage_.val)); - else - ::new (&storage_.err) E(std::move(o.storage_.err)); - } - - /** - * @brief Converting constructor from value. - * - * @tparam U Forwarded value type. - * @param[in] v Value to construct with. - * - * @throws May throw if T constructor throws. - * - * @ingroup base_utilities - */ - template , expected> && - !std::is_same_v, std::in_place_t> && - !std::is_same_v, unexpect_t> && - std::is_constructible_v>> - constexpr expected(U&& v) : has_val_(true) { - ::new (&storage_.val) T(std::forward(v)); - } - - /** - * @brief Constructor from unexpected. - * - * @tparam G Forwarded error type. - * @param[in] u Unexpected value containing error. - * - * @throws May throw if E constructor throws. - * - * @ingroup base_utilities - */ - template >> - constexpr expected(const unexpected& u) : has_val_(false) { - ::new (&storage_.err) E(u.error()); - } - - /** - * @brief Constructor from unexpected rvalue. - * - * @tparam G Forwarded error type. - * @param[in] u Unexpected value containing error. - * - * @throws May throw if E constructor throws. - * - * @ingroup base_utilities - */ - template >> - constexpr expected(unexpected&& u) : has_val_(false) { - ::new (&storage_.err) E(std::move(u.error())); - } - - /** - * @brief In-place constructor for value. - * - * @tparam Args Constructor argument types. - * @param[in] args Arguments to forward to T constructor. - * - * @throws May throw if T constructor throws. - * - * @ingroup base_utilities - */ - template >> - constexpr explicit expected(std::in_place_t, Args&&... args) : has_val_(true) { - ::new (&storage_.val) T(std::forward(args)...); - } - - /** - * @brief Constructor for in-place error construction. - * - * @tparam Args Constructor argument types. - * @param[in] args Arguments to forward to E constructor. - * - * @throws May throw if E constructor throws. - * - * @ingroup base_utilities - */ - template >> - constexpr explicit expected(unexpect_t, Args&&... args) : has_val_(false) { - ::new (&storage_.err) E(std::forward(args)...); - } - - /** - * @brief Destructor. Destroys contained value or error. - * - * @throws None. - * - * @ingroup base_utilities - */ - ~expected() { destroy(); } - - /** - * @brief Copy assignment operator. - * - * @param[in] o Expected to copy from. - * - * @return Reference to this expected. - * - * @throws May throw if assignment or construction throws. - * - * @ingroup base_utilities - */ - expected& operator=(const expected& o) { - if (this == &o) - return *this; - if (has_val_ && o.has_val_) { - storage_.val = o.storage_.val; - } else if (!has_val_ && !o.has_val_) { - storage_.err = o.storage_.err; - } else { - destroy(); - has_val_ = o.has_val_; - if (has_val_) - ::new (&storage_.val) T(o.storage_.val); - else - ::new (&storage_.err) E(o.storage_.err); - } - return *this; - } - - /** - * @brief Move assignment operator. - * - * @param[in] o Expected to move from. - * - * @return Reference to this expected. - * - * @throws May throw if assignment or construction throws. - * - * @ingroup base_utilities - */ - expected& operator=(expected&& o) noexcept(std::is_nothrow_move_assignable_v && - std::is_nothrow_move_assignable_v && - std::is_nothrow_move_constructible_v && - std::is_nothrow_move_constructible_v) { - if (this == &o) - return *this; - if (has_val_ && o.has_val_) { - storage_.val = std::move(o.storage_.val); - } else if (!has_val_ && !o.has_val_) { - storage_.err = std::move(o.storage_.err); - } else { - destroy(); - has_val_ = o.has_val_; - if (has_val_) - ::new (&storage_.val) T(std::move(o.storage_.val)); - else - ::new (&storage_.err) E(std::move(o.storage_.err)); - } - return *this; - } - - /** - * @brief Assignment from value. - * - * @tparam U Forwarded value type. - * @param[in] v Value to assign. - * - * @return Reference to this expected. - * - * @throws May throw if assignment or construction throws. - * - * @ingroup base_utilities - */ - template - std::enable_if_t, expected> && std::is_constructible_v && - std::is_assignable_v, - expected&> - operator=(U&& v) { - if (has_val_) { - storage_.val = std::forward(v); - } else { - destroy(); - ::new (&storage_.val) T(std::forward(v)); - has_val_ = true; - } - return *this; - } - - /** - * @brief Assignment from unexpected. - * - * @tparam G Forwarded error type. - * @param[in] u Unexpected value containing error. - * - * @return Reference to this expected. - * - * @throws May throw if assignment or construction throws. - * - * @ingroup base_utilities - */ - template expected& operator=(const unexpected& u) { - if (!has_val_) { - storage_.err = u.error(); - } else { - destroy(); - ::new (&storage_.err) E(u.error()); - has_val_ = false; - } - return *this; - } - - /** - * @brief Assignment from unexpected rvalue. - * - * @tparam G Forwarded error type. - * @param[in] u Unexpected value containing error. - * - * @return Reference to this expected. - * - * @throws May throw if assignment or construction throws. - * - * @ingroup base_utilities - */ - template expected& operator=(unexpected&& u) { - if (!has_val_) { - storage_.err = std::move(u.error()); - } else { - destroy(); - ::new (&storage_.err) E(std::move(u.error())); - has_val_ = false; - } - return *this; - } - - /** - * @brief Checks if the expected contains a value. - * - * @return true if contains a value, false if contains error. - * - * @throws None. - * - * @ingroup base_utilities - */ - constexpr bool has_value() const noexcept { return has_val_; } - - /** - * @brief Boolean conversion operator. - * - * @return true if contains a value, false if contains error. - * - * @throws None. - * - * @ingroup base_utilities - */ - constexpr explicit operator bool() const noexcept { return has_val_; } - - /** - * @brief Pointer operator. - * - * @return Pointer to the contained value. - * - * @warning Undefined behavior if expected does not contain a value. - * - * @throws None. - * - * @ingroup base_utilities - */ - constexpr T* operator->() noexcept { return &storage_.val; } - - /** - * @brief Const pointer operator. - * - * @return Pointer to the contained value. - * - * @warning Undefined behavior if expected does not contain a value. - * - * @throws None. - * - * @ingroup base_utilities - */ - constexpr const T* operator->() const noexcept { return &storage_.val; } - - /** - * @brief Dereference operator. - * - * @return Reference to the contained value. - * - * @warning Undefined behavior if expected does not contain a value. - * - * @throws None. - * - * @ingroup base_utilities - */ - constexpr T& operator*() & noexcept { return storage_.val; } - - /** - * @brief Const dereference operator. - * - * @return Const reference to the contained value. - * - * @warning Undefined behavior if expected does not contain a value. - * - * @throws None. - * - * @ingroup base_utilities - */ - constexpr const T& operator*() const& noexcept { return storage_.val; } - - /** - * @brief Rvalue dereference operator. - * - * @return Rvalue reference to the contained value. - * - * @warning Undefined behavior if expected does not contain a value. - * - * @throws None. - * - * @ingroup base_utilities - */ - constexpr T&& operator*() && noexcept { return std::move(storage_.val); } - - /** - * @brief Const rvalue dereference operator. - * - * @return Const rvalue reference to the contained value. - * - * @warning Undefined behavior if expected does not contain a value. - * - * @throws None. - * - * @ingroup base_utilities - */ - constexpr const T&& operator*() const&& noexcept { return std::move(storage_.val); } - - /** - * @brief Returns the contained value. - * - * @return Reference to the contained value. - * - * @throws bad_expected_access if expected contains an error. - * - * @warning Throws bad_expected_access if this expected does not - * contain a value. - * - * @ingroup base_utilities - */ - constexpr T& value() & { - if (!has_val_) - throw bad_expected_access(storage_.err); - return storage_.val; - } - - /** - * @brief Returns the contained value (const lvalue). - * - * @return Const reference to the contained value. - * - * @throws bad_expected_access if expected contains an error. - * - * @warning Throws bad_expected_access if this expected does not - * contain a value. - * - * @ingroup base_utilities - */ - constexpr const T& value() const& { - if (!has_val_) - throw bad_expected_access(storage_.err); - return storage_.val; - } - - /** - * @brief Returns the contained value (rvalue). - * - * @return Rvalue reference to the contained value. - * - * @throws bad_expected_access if expected contains an error. - * - * @warning Throws bad_expected_access if this expected does not - * contain a value. - * - * @ingroup base_utilities - */ - constexpr T&& value() && { - if (!has_val_) - throw bad_expected_access(std::move(storage_.err)); - return std::move(storage_.val); - } - - /** - * @brief Returns the contained value (const rvalue). - * - * @return Const rvalue reference to the contained value. - * - * @throws bad_expected_access if expected contains an error. - * - * @warning Throws bad_expected_access if this expected does not - * contain a value. - * - * @ingroup base_utilities - */ - constexpr const T&& value() const&& { - if (!has_val_) - throw bad_expected_access(std::move(storage_.err)); - return std::move(storage_.val); - } - - /** - * @brief Returns the contained error. - * - * @return Reference to the contained error. - * - * @throws None. - * - * @warning The error is only valid if has_value() returns false. - * - * @ingroup base_utilities - */ - constexpr E& error() & noexcept { return storage_.err; } - - /** - * @brief Returns the contained error (const lvalue). - * - * @return Const reference to the contained error. - * - * @throws None. - * - * @warning The error is only valid if has_value() returns false. - * - * @ingroup base_utilities - */ - constexpr const E& error() const& noexcept { return storage_.err; } - - /** - * @brief Returns the contained error (rvalue). - * - * @return Rvalue reference to the contained error. - * - * @throws None. - * - * @warning The error is only valid if has_value() returns false. - * - * @ingroup base_utilities - */ - constexpr E&& error() && noexcept { return std::move(storage_.err); } - - /** - * @brief Returns the contained error (const rvalue). - * - * @return Const rvalue reference to the contained error. - * - * @throws None. - * - * @warning The error is only valid if has_value() returns false. - * - * @ingroup base_utilities - */ - constexpr const E&& error() const&& noexcept { return std::move(storage_.err); } - - /** - * @brief Returns the contained value or a default value. - * - * @tparam U Default value type. - * @param[in] default_val Default value to return if in error state. - * - * @return The contained value or the default value. - * - * @throws None. - * - * @ingroup base_utilities - */ - template constexpr T value_or(U&& default_val) const& { - return has_val_ ? storage_.val : static_cast(std::forward(default_val)); - } - - /** - * @brief Returns the contained value or a default value (rvalue). - * - * @tparam U Default value type. - * @param[in] default_val Default value to return if in error state. - * - * @return The contained value or the default value. - * - * @throws None. - * - * @ingroup base_utilities - */ - template constexpr T value_or(U&& default_val) && { - return has_val_ ? std::move(storage_.val) : static_cast(std::forward(default_val)); - } - - /** - * @brief Returns the contained error or a default error. - * - * @tparam U Default error type. - * @param[in] default_err Default error to return if has value. - * - * @return The contained error or the default error. - * - * @throws None. - * - * @ingroup base_utilities - */ - template constexpr E error_or(U&& default_err) const& { - return !has_val_ ? storage_.err : static_cast(std::forward(default_err)); - } - - /** - * @brief Returns the contained error or a default error (rvalue). - * - * @tparam U Default error type. - * @param[in] default_err Default error to return if has value. - * - * @return The contained error or the default error. - * - * @throws None. - * - * @ingroup base_utilities - */ - template constexpr E error_or(U&& default_err) && { - return !has_val_ ? std::move(storage_.err) : static_cast(std::forward(default_err)); - } - - /** - * @brief Monadic operation: chains if value is present. - * - * @tparam F Callable type. - * @param[in] f Function to call if value is present. - * - * @return Result of f(value) or error. - * - * @throws None. - * - * @ingroup base_utilities - */ - template auto and_then(F&& f) & { - using Ret = std::invoke_result_t; - if (has_val_) - return std::forward(f)(storage_.val); - else - return Ret(unexpect, storage_.err); - } - - /** - * @brief Monadic operation: chains if value is present (const). - * - * @tparam F Callable type. - * @param[in] f Function to call if value is present. - * - * @return Result of f(value) or error. - * - * @throws None. - * - * @ingroup base_utilities - */ - template auto and_then(F&& f) const& { - using Ret = std::invoke_result_t; - if (has_val_) - return std::forward(f)(storage_.val); - else - return Ret(unexpect, storage_.err); - } - - /** - * @brief Monadic operation: chains if value is present (rvalue). - * - * @tparam F Callable type. - * @param[in] f Function to call if value is present. - * - * @return Result of f(value) or error. - * - * @throws None. - * - * @ingroup base_utilities - */ - template auto and_then(F&& f) && { - using Ret = std::invoke_result_t; - if (has_val_) - return std::forward(f)(std::move(storage_.val)); - else - return Ret(unexpect, std::move(storage_.err)); - } - - /** - * @brief Monadic operation: chains if error is present. - * - * @tparam F Callable type. - * @param[in] f Function to call if error is present. - * - * @return value or result of f(error). - * - * @throws None. - * - * @ingroup base_utilities - */ - template auto or_else(F&& f) & { - using Ret = std::invoke_result_t; - if (!has_val_) - return std::forward(f)(storage_.err); - else - return Ret(storage_.val); - } - - /** - * @brief Monadic operation: chains if error is present (const). - * - * @tparam F Callable type. - * @param[in] f Function to call if error is present. - * - * @return value or result of f(error). - * - * @throws None. - * - * @ingroup base_utilities - */ - template auto or_else(F&& f) const& { - using Ret = std::invoke_result_t; - if (!has_val_) - return std::forward(f)(storage_.err); - else - return Ret(storage_.val); - } - - /** - * @brief Monadic operation: chains if error is present (rvalue). - * - * @tparam F Callable type. - * @param[in] f Function to call if error is present. - * - * @return value or result of f(error). - * - * @throws None. - * - * @ingroup base_utilities - */ - template auto or_else(F&& f) && { - using Ret = std::invoke_result_t; - if (!has_val_) - return std::forward(f)(std::move(storage_.err)); - else - return Ret(std::move(storage_.val)); - } - - /** - * @brief Monadic operation: transforms the value. - * - * @tparam F Callable type. - * @param[in] f Function to apply to value. - * - * @return expected with transformed value or error. - * - * @throws None. - * - * @ingroup base_utilities - */ - template auto transform(F&& f) & { - using U = std::invoke_result_t; - if (has_val_) - return expected(std::forward(f)(storage_.val)); - else - return expected(unexpect, storage_.err); - } - - /** - * @brief Monadic operation: transforms the value (const). - * - * @tparam F Callable type. - * @param[in] f Function to apply to value. - * - * @return expected with transformed value or error. - * - * @throws None. - * - * @ingroup base_utilities - */ - template auto transform(F&& f) const& { - using U = std::invoke_result_t; - if (has_val_) - return expected(std::forward(f)(storage_.val)); - else - return expected(unexpect, storage_.err); - } - - /** - * @brief Monadic operation: transforms the value (rvalue). - * - * @tparam F Callable type. - * @param[in] f Function to apply to value. - * - * @return expected with transformed value or error. - * - * @throws None. - * - * @ingroup base_utilities - */ - template auto transform(F&& f) && { - using U = std::invoke_result_t; - if (has_val_) - return expected(std::forward(f)(std::move(storage_.val))); - else - return expected(unexpect, std::move(storage_.err)); - } - - /** - * @brief Monadic operation: transforms the error. - * - * @tparam F Callable type. - * @param[in] f Function to apply to error. - * - * @return expected with value or transformed error. - * - * @throws None. - * - * @ingroup base_utilities - */ - template auto transform_error(F&& f) & { - using G = std::invoke_result_t; - if (!has_val_) - return expected(unexpect, std::forward(f)(storage_.err)); - else - return expected(storage_.val); - } - - /** - * @brief Monadic operation: transforms the error (const). - * - * @tparam F Callable type. - * @param[in] f Function to apply to error. - * - * @return expected with value or transformed error. - * - * @throws None. - * - * @ingroup base_utilities - */ - template auto transform_error(F&& f) const& { - using G = std::invoke_result_t; - if (!has_val_) - return expected(unexpect, std::forward(f)(storage_.err)); - else - return expected(storage_.val); - } - - /** - * @brief Monadic operation: transforms the error (rvalue). - * - * @tparam F Callable type. - * @param[in] f Function to apply to error. - * - * @return expected with value or transformed error. - * - * @throws None. - * - * @ingroup base_utilities - */ - template auto transform_error(F&& f) && { - using G = std::invoke_result_t; - if (!has_val_) - return expected(unexpect, std::forward(f)(std::move(storage_.err))); - else - return expected(std::move(storage_.val)); - } - - /** - * @brief Equality comparison with another expected. - * - * @tparam T2 Other value type. - * @tparam E2 Other error type. - * @param[in] a First expected. - * @param[in] b Second expected. - * - * @return true if equal, false otherwise. - * - * @throws None. - * - * @ingroup base_utilities - */ - template - friend constexpr bool operator==(const expected& a, const expected& b) { - if (a.has_val_ != b.has_value()) - return false; - if (a.has_val_) - return *a == *b; - return a.error() == b.error(); - } - - /** - * @brief Equality comparison with a value. - * - * @tparam T2 Value type to compare with. - * @param[in] a Expected. - * @param[in] v Value to compare. - * - * @return true if expected contains value equal to v. - * - * @throws None. - * - * @ingroup base_utilities - */ - template friend constexpr bool operator==(const expected& a, const T2& v) { - return a.has_val_ && (*a == v); - } - - /** - * @brief Equality comparison with unexpected. - * - * @tparam E2 Error type of unexpected. - * @param[in] a Expected. - * @param[in] u Unexpected to compare. - * - * @return true if expected contains matching error. - * - * @throws None. - * - * @ingroup base_utilities - */ - template - friend constexpr bool operator==(const expected& a, const unexpected& u) { - return !a.has_val_ && (a.error() == u.error()); - } - - /** - * @brief Inequality comparison with another expected. - * - * @tparam T2 Other value type. - * @tparam E2 Other error type. - * @param[in] a First expected. - * @param[in] b Second expected. - * - * @return true if not equal, false otherwise. - * - * @throws None. - * - * @ingroup base_utilities - */ - template - friend constexpr bool operator!=(const expected& a, const expected& b) { - return !(a == b); - } - - /** - * @brief Inequality comparison with a value. - * - * @tparam T2 Value type to compare with. - * @param[in] a Expected. - * @param[in] v Value to compare. - * - * @return true if not equal, false otherwise. - * - * @throws None. - * - * @ingroup base_utilities - */ - template friend constexpr bool operator!=(const expected& a, const T2& v) { - return !(a == v); - } - - /** - * @brief Inequality comparison with unexpected. - * - * @tparam E2 Error type of unexpected. - * @param[in] a Expected. - * @param[in] u Unexpected to compare. - * - * @return true if not equal, false otherwise. - * - * @throws None. - * - * @ingroup base_utilities - */ - template - friend constexpr bool operator!=(const expected& a, const unexpected& u) { - return !(a == u); - } - - /** - * @brief Swaps the contents with another expected. - * - * @param[in,out] o Expected to swap with. - * - * @throws None if types are nothrow move constructible and swappable. - * - * @note None. - * - * @warning None. - * - * @since 0.1 - * @ingroup base_utilities - */ - void swap(expected& o) noexcept(std::is_nothrow_move_constructible_v && - std::is_nothrow_swappable_v && - std::is_nothrow_move_constructible_v && - std::is_nothrow_swappable_v) { - using std::swap; - if (has_val_ && o.has_val_) { - swap(storage_.val, o.storage_.val); - } else if (!has_val_ && !o.has_val_) { - swap(storage_.err, o.storage_.err); - } else { - // Swap between value-holding and error-holding expected - if (has_val_) { - // this has value, o has error - T tmp_val(std::move(storage_.val)); - storage_.val.~T(); - - ::new (&storage_.err) E(std::move(o.storage_.err)); - o.storage_.err.~E(); - - ::new (&o.storage_.val) T(std::move(tmp_val)); - // tmp_val is destroyed here - - has_val_ = false; - o.has_val_ = true; - } else { - // this has error, o has value - E tmp_err(std::move(storage_.err)); - storage_.err.~E(); - - ::new (&storage_.val) T(std::move(o.storage_.val)); - o.storage_.val.~T(); - - ::new (&o.storage_.err) E(std::move(tmp_err)); - // tmp_err is destroyed here - - has_val_ = true; - o.has_val_ = false; - } - } - } - - /** - * @brief Non-member swap function. - * - * @param[in,out] a First expected. - * @param[in,out] b Second expected. - * - * @throws None if types are nothrow move constructible and swappable. - * - * @note None. - * - * @warning None. - * - * @since 0.1 - * @ingroup base_utilities - */ - friend void swap(expected& a, expected& b) noexcept(noexcept(a.swap(b))) { a.swap(b); } -}; - -/** - * @brief Specialization of expected for void value type. - * - * This specialization represents operations that may fail but do not - * produce a value on success. - * - * @tparam E Error type. - * - * @ingroup base_utilities - */ -template class expected { - static_assert(!std::is_void_v, "E must not be void"); - - /// Internal storage union. - union Storage { - E err; ///< Error storage. - Storage() {} - ~Storage() {} - }; - Storage storage_; ///< Internal storage. - bool has_val_; ///< Indicates whether in value (void) state. - - public: - using value_type = void; ///< Value type alias. - using error_type = E; ///< Error type alias. - using unexpected_type = unexpected; ///< Unexpected type alias. - - /** - * @brief Default constructor. Creates expected in success state. - * - * @throws None. - * - * @ingroup base_utilities - */ - constexpr expected() noexcept : has_val_(true) {} - - /** - * @brief Copy constructor. - * - * @param[in] o Expected to copy from. - * - * @throws May throw if E copy constructor throws. - * - * @ingroup base_utilities - */ - expected(const expected& o) : has_val_(o.has_val_) { - if (!has_val_) - ::new (&storage_.err) E(o.storage_.err); - } - - /** - * @brief Move constructor. - * - * @param[in] o Expected to move from. - * - * @throws May throw if E move constructor throws. - * - * @ingroup base_utilities - */ - expected(expected&& o) noexcept(std::is_nothrow_move_constructible_v) - : has_val_(o.has_val_) { - if (!has_val_) - ::new (&storage_.err) E(std::move(o.storage_.err)); - } - - /** - * @brief Constructor from unexpected. - * - * @tparam G Forwarded error type. - * @param[in] u Unexpected value containing error. - * - * @throws May throw if E constructor throws. - * - * @ingroup base_utilities - */ - template constexpr expected(const unexpected& u) : has_val_(false) { - ::new (&storage_.err) E(u.error()); - } - - /** - * @brief Constructor from unexpected rvalue. - * - * @tparam G Forwarded error type. - * @param[in] u Unexpected value containing error. - * - * @throws May throw if E constructor throws. - * - * @ingroup base_utilities - */ - template constexpr expected(unexpected&& u) : has_val_(false) { - ::new (&storage_.err) E(std::move(u.error())); - } - - /** - * @brief In-place constructor for success state. - * - * @throws None. - * - * @ingroup base_utilities - */ - constexpr explicit expected(std::in_place_t) noexcept : has_val_(true) {} - - /** - * @brief Constructor for in-place error construction. - * - * @tparam Args Constructor argument types. - * @param[in] args Arguments to forward to E constructor. - * - * @throws May throw if E constructor throws. - * - * @ingroup base_utilities - */ - template constexpr explicit expected(unexpect_t, Args&&... args) - : has_val_(false) { - ::new (&storage_.err) E(std::forward(args)...); - } - - /** - * @brief Destructor. - * - * @throws None. - * - * @ingroup base_utilities - */ - ~expected() { - if (!has_val_) - storage_.err.~E(); - } - - /** - * @brief Copy assignment operator. - * - * @param[in] o Expected to copy from. - * - * @return Reference to this expected. - * - * @throws May throw if E assignment or construction throws. - * - * @ingroup base_utilities - */ - expected& operator=(const expected& o) { - if (has_val_ && o.has_val_) { - } else if (!has_val_ && !o.has_val_) { - storage_.err = o.storage_.err; - } else if (has_val_) { - ::new (&storage_.err) E(o.storage_.err); - has_val_ = false; - } else { - storage_.err.~E(); - has_val_ = true; - } - return *this; - } - - /** - * @brief Checks if the expected is in success state. - * - * @return true if in success state, false if in error state. - * - * @throws None. - * - * @ingroup base_utilities - */ - constexpr bool has_value() const noexcept { return has_val_; } - - /** - * @brief Boolean conversion operator. - * - * @return true if in success state, false if in error state. - * - * @throws None. - * - * @ingroup base_utilities - */ - constexpr explicit operator bool() const noexcept { return has_val_; } - - /** - * @brief Returns void if in success state. - * - * @throws bad_expected_access if in error state. - * - * @warning Throws bad_expected_access if this expected contains - * an error. - * - * @ingroup base_utilities - */ - constexpr void value() const { - if (!has_val_) - throw bad_expected_access(storage_.err); - } - - /** - * @brief Returns the contained error. - * - * @return Reference to the contained error. - * - * @throws None. - * - * @warning The error is only valid if has_value() returns false. - * - * @ingroup base_utilities - */ - constexpr E& error() & noexcept { return storage_.err; } - - /** - * @brief Returns the contained error (const lvalue). - * - * @return Const reference to the contained error. - * - * @throws None. - * - * @warning The error is only valid if has_value() returns false. - * - * @ingroup base_utilities - */ - constexpr const E& error() const& noexcept { return storage_.err; } - - /** - * @brief Returns the contained error (rvalue). - * - * @return Rvalue reference to the contained error. - * - * @throws None. - * - * @warning The error is only valid if has_value() returns false. - * - * @ingroup base_utilities - */ - constexpr E&& error() && noexcept { return std::move(storage_.err); } - - /** - * @brief Returns the contained error (const rvalue). - * - * @return Const rvalue reference to the contained error. - * - * @throws None. - * - * @warning The error is only valid if has_value() returns false. - * - * @ingroup base_utilities - */ - constexpr const E&& error() const&& noexcept { return std::move(storage_.err); } - - /** - * @brief Monadic operation: chains if in success state. - * - * @tparam F Callable type. - * @param[in] f Function to call if in success state. - * - * @return Result of f() or error. - * - * @throws None. - * - * @ingroup base_utilities - */ - template auto and_then(F&& f) & { - using Ret = std::invoke_result_t; - if (has_val_) - return std::forward(f)(); - else - return Ret(unexpect, storage_.err); - } - - /** - * @brief Monadic operation: chains if in success state (rvalue). - * - * @tparam F Callable type. - * @param[in] f Function to call if in success state. - * - * @return Result of f() or error. - * - * @throws None. - * - * @ingroup base_utilities - */ - template auto and_then(F&& f) && { - using Ret = std::invoke_result_t; - if (has_val_) - return std::forward(f)(); - else - return Ret(unexpect, std::move(storage_.err)); - } - - /** - * @brief Monadic operation: transforms the success state. - * - * @tparam F Callable type. - * @param[in] f Function to apply. - * - * @return expected with transformed value or error. - * - * @throws None. - * - * @ingroup base_utilities - */ - template auto transform(F&& f) & { - using U = std::invoke_result_t; - if (has_val_) - return expected(std::forward(f)()); - else - return expected(unexpect, storage_.err); - } - - /** - * @brief Monadic operation: transforms the success state (rvalue). - * - * @tparam F Callable type. - * @param[in] f Function to apply. - * - * @return expected with transformed value or error. - * - * @throws None. - * - * @ingroup base_utilities - */ - template auto transform(F&& f) && { - using U = std::invoke_result_t; - if (has_val_) - return expected(std::forward(f)()); - else - return expected(unexpect, std::move(storage_.err)); - } -}; - -} // namespace cf diff --git a/base/include/base/factory/plain_factory.hpp b/base/include/base/factory/plain_factory.hpp deleted file mode 100644 index c5468f404..000000000 --- a/base/include/base/factory/plain_factory.hpp +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @file plain_factory.hpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Provides plain pointer factory templates for cross-ABI compatibility. - * - * Defines factory templates that create objects using raw pointers. Useful for - * scenarios requiring ABI stability across compilation boundaries. - * - * @version 0.1 - * @date 2026-03-27 - * @copyright Copyright (c) 2026 - * @ingroup base_factory - */ -#pragma once -#include "base/singleton/simple_singleton.hpp" -#include -namespace cf { - -/** - * @brief Factory that creates objects as raw pointers. - * - * Provides a virtual interface for constructing objects with raw pointers. - * Intended for cross-ABI scenarios where smart pointers may not be suitable. - * - * @tparam Result Type of object to create. Must be constructible from Args. - * @tparam Args Argument types forwarded to Result constructor. - * - * @note None - * @warning None - * @since 0.1 - * @ingroup base_factory - * - * @code - * PlainFactory factory; - * MyClass* obj = factory.make(42, 3.14); - * @endcode - */ -template struct PlainFactory { - /** - * @brief Destroys the factory instance. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup base_factory - */ - virtual ~PlainFactory() = default; - - /** - * @brief Creates a new Result object using raw pointer. - * - * Constructs a Result instance by forwarding arguments to its constructor. - * Caller owns the returned pointer and is responsible for deletion. - * - * @param[in] args Arguments forwarded to Result constructor. - * - * @return Pointer to newly created Result object. Caller owns. - * - * @throws None - * @note Caller must delete the returned pointer to avoid memory leak. - * @warning Raw pointer ownership; consider smart pointers for new code. - * @since 0.1 - * @ingroup base_factory - */ - Result* make(Args&&... args) { return new Result(std::forward(args)...); } -}; - -/** - * @brief Singleton variant of PlainFactory for static access. - * - * Combines PlainFactory with SimpleSingleton to provide a globally - * accessible factory instance using raw pointers. - * - * @tparam Result Type of object to create. Must be constructible from Args. - * @tparam Args Argument types forwarded to Result constructor. - * - * @note None - * @warning None - * @since 0.1 - * @ingroup base_factory - * - * @code - * auto& factory = StaticPlainFactory::instance(); - * MyClass* obj = factory.make(42); - * @endcode - */ -template struct StaticPlainFactory - : public PlainFactory, - public SimpleSingleton> {}; - -} // namespace cf diff --git a/base/include/base/factory/registered_factory.hpp b/base/include/base/factory/registered_factory.hpp deleted file mode 100644 index 1419bb8a9..000000000 --- a/base/include/base/factory/registered_factory.hpp +++ /dev/null @@ -1,174 +0,0 @@ -/** - * @file registered_factory.hpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Provides registered factory templates for abstract interface creation. - * - * Defines factory templates that create objects via registered creator functions, - * rather than by directly calling constructors. This is the dual of - * SmartPtrPlainFactory: it works for abstract interfaces where direct - * construction is impossible. - * - * @version 0.1 - * @date 2026-03-30 - * @copyright Copyright (c) 2026 - * @ingroup base_factory - */ -#pragma once - -#include "base/singleton/simple_singleton.hpp" -#include -#include - -namespace cf { - -/** - * @brief Factory that creates objects via a registered creator function. - * - * Unlike SmartPtrPlainFactory which directly calls Result(args...), this - * factory delegates creation to a std::function registered at startup. - * This makes it suitable for abstract interfaces where the concrete type - * is determined by the platform. - * - * @tparam Interface The abstract base type to create. - * - * @note None - * @warning None - * @since N/A - * @ingroup base_factory - * - * @code - * RegisteredFactory factory; - * factory.register_creator([](){ return new ConcreteImpl; }); - * auto obj = factory.make_unique(); - * @endcode - */ -template class RegisteredFactory { - public: - /// Function type that creates a raw pointer to Interface. - using Creator = std::function; - - /// Function type that destroys a raw pointer to Interface. - using Destroyer = std::function; - - /// Convenience alias for the unique_ptr type returned by make_unique(). - using unique_ptr_type = std::unique_ptr; - - RegisteredFactory() = default; - virtual ~RegisteredFactory() = default; - - /** - * @brief Registers a creator/destroyer pair. - * - * Should be called once during application startup by the - * platform-specific initialization code. - * - * @param[in] creator Function that returns a new Interface*. - * @param[in] destroyer Optional custom deleter. If null, default delete is used. - * - * @throws None - * @note Calling this multiple times replaces the previous registration. - * @warning None - * @since 0.1 - * @ingroup base_factory - */ - void register_creator(Creator creator, Destroyer destroyer = nullptr) { - creator_ = std::move(creator); - destroyer_ = std::move(destroyer); - } - - /** - * @brief Creates an Interface as std::unique_ptr with custom deleter support. - * - * Returns nullptr if no creator has been registered or if the creator - * returns nullptr. - * - * @return std::unique_ptr owning the newly created object, or nullptr. - * - * @throws None - * @note If a destroyer was registered, it is used as the deleter. - * Otherwise, a default delete deleter is used. - * @warning None - * @since 0.1 - * @ingroup base_factory - */ - std::unique_ptr make_unique() { - if (!creator_) - return nullptr; - Interface* raw = creator_(); - if (!raw) - return nullptr; - if (destroyer_) { - return std::unique_ptr(raw, destroyer_); - } - return std::unique_ptr(raw, [](Interface* p) { delete p; }); - } - - /** - * @brief Creates an Interface as std::shared_ptr. - * - * Returns nullptr if no creator has been registered or if the creator - * returns nullptr. - * - * @return std::shared_ptr sharing ownership, or nullptr. - * - * @throws None - * @note If a destroyer was registered, it is used as the deleter. - * @warning None - * @since 0.1 - * @ingroup base_factory - */ - std::shared_ptr make_shared() { - if (!creator_) - return nullptr; - Interface* raw = creator_(); - if (!raw) - return nullptr; - if (destroyer_) { - auto d = destroyer_; - return std::shared_ptr(raw, [d](Interface* p) { d(p); }); - } - return std::shared_ptr(raw); - } - - /** - * @brief Checks whether a creator has been registered. - * - * @return true if a non-empty creator is registered. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup base_factory - */ - bool has_creator() const { return static_cast(creator_); } - - protected: - Creator creator_; - Destroyer destroyer_; -}; - -/** - * @brief Singleton variant of RegisteredFactory for static access. - * - * Combines RegisteredFactory with SimpleSingleton to provide a - * globally accessible factory instance that uses registered creators. - * - * @tparam Interface Type of object to create. - * - * @note None - * @warning None - * @since N/A - * @ingroup base_factory - * - * @code - * using MyFactory = StaticRegisteredFactory; - * MyFactory::instance().register_creator([](){ return new Concrete; }); - * auto obj = MyFactory::instance().make_unique(); - * @endcode - */ -template class StaticRegisteredFactory - : public RegisteredFactory, - public SimpleSingleton> {}; - -} // namespace cf diff --git a/base/include/base/factory/smartptr_plain_factory.hpp b/base/include/base/factory/smartptr_plain_factory.hpp deleted file mode 100644 index 79e3da56f..000000000 --- a/base/include/base/factory/smartptr_plain_factory.hpp +++ /dev/null @@ -1,129 +0,0 @@ -/** - * @file smartptr_plain_factory.hpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Provides smart pointer factory templates for automatic lifetime - * management. - * - * Defines factory templates that create objects using std::unique_ptr and - * std::shared_ptr. Eliminates the need for manual memory management. - * - * @version 0.1 - * @date 2026-03-27 - * @copyright Copyright (c) 2026 - * @ingroup base_factory - */ -#pragma once - -#include "base/singleton/simple_singleton.hpp" -#include -namespace cf { - -/** - * @brief Factory that creates objects using smart pointers. - * - * Provides methods for constructing objects with automatic lifetime - * management via std::unique_ptr and std::shared_ptr. - * - * @tparam Result Type of object to create. Must be constructible from Args. - * @tparam Args Argument types forwarded to Result constructor. - * - * @note None - * @warning None - * @since 0.1 - * @ingroup base_factory - * - * @code - * SmartPtrPlainFactory factory; - * auto unique = factory.make_unique(42); - * auto shared = factory.make_shared(42); - * @endcode - */ -template struct SmartPtrPlainFactory { - public: - /** - * @brief Default constructs the factory. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup base_factory - */ - SmartPtrPlainFactory() = default; - - /** - * @brief Destroys the factory instance. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup base_factory - */ - virtual ~SmartPtrPlainFactory() = default; - - /** - * @brief Creates a new Result object as std::unique_ptr. - * - * Constructs a Result instance with unique ownership by forwarding - * arguments to its constructor. - * - * @param[in] args Arguments forwarded to Result constructor. - * - * @return std::unique_ptr owning the newly created object. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup base_factory - */ - std::unique_ptr make_unique(Args&&... args) { - return std::make_unique(std::forward(args)...); - } - - /** - * @brief Creates a new Result object as std::shared_ptr. - * - * Constructs a Result instance with shared ownership by forwarding - * arguments to its constructor. - * - * @param[in] args Arguments forwarded to Result constructor. - * - * @return std::shared_ptr sharing ownership of the new object. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup base_factory - */ - std::shared_ptr make_shared(Args&&... args) { - return std::make_shared(std::forward(args)...); - } -}; - -/** - * @brief Singleton variant of SmartPtrPlainFactory for static access. - * - * Combines SmartPtrPlainFactory with SimpleSingleton to provide a - * globally accessible factory instance using smart pointers. - * - * @tparam Result Type of object to create. Must be constructible from Args. - * @tparam Args Argument types forwarded to Result constructor. - * - * @note None - * @warning None - * @since 0.1 - * @ingroup base_factory - * - * @code - * auto& factory = StaticSmartPtrPlainFactory::instance(); - * auto obj = factory.make_unique(42); - * @endcode - */ -template struct StaticSmartPtrPlainFactory - : public SmartPtrPlainFactory, - public SimpleSingleton> {}; - -} // namespace cf diff --git a/base/include/base/hash/constexpr_fnv1a.hpp b/base/include/base/hash/constexpr_fnv1a.hpp deleted file mode 100644 index c96c7b660..000000000 --- a/base/include/base/hash/constexpr_fnv1a.hpp +++ /dev/null @@ -1,195 +0,0 @@ -/** - * @file constexpr_fnv1a.hpp - * @brief Compile-time FNV-1a hash for string interning. - * - * Provides constexpr FNV-1a hashing algorithm for compile-time - * string hash computation. Supports both 32-bit and 64-bit variants. - * - * @author Charliechen114514 - * @date 2026-02-25 - * @version 0.1 - * @since 0.1 - * @ingroup base_hash - */ -#pragma once - -#include -#include - -namespace cf::hash { - -/** - * @brief FNV-1a 64-bit offset basis. - * - * @since 0.1 - * @ingroup base_hash - * @internal - */ -inline constexpr uint64_t fnv1a64_offset_basis = 14695981039346656037ULL; - -/** - * @brief FNV-1a 64-bit prime. - * - * @since 0.1 - * @ingroup base_hash - * @internal - */ -inline constexpr uint64_t fnv1a64_prime = 1099511628211ULL; - -/** - * @brief FNV-1a 32-bit offset basis. - * - * @since 0.1 - * @ingroup base_hash - * @internal - */ -inline constexpr uint32_t fnv1a32_offset_basis = 2166136261U; - -/** - * @brief FNV-1a 32-bit prime. - * - * @since 0.1 - * @ingroup base_hash - * @internal - */ -inline constexpr uint32_t fnv1a32_prime = 16777619U; - -/** - * @brief FNV-1a 64-bit hash constexpr implementation. - * - * Computes FNV-1a hash at compile time for string literals. - * The FNV-1a algorithm provides excellent distribution and - * minimal collisions for string identifiers. - * - * @param[in] str Null-terminated string to hash. - * @param[in] seed Starting hash value (default: FNV offset basis). - * - * @return 64-bit hash value. - * - * @throws None. - * - * @note The hash is computed at compile time for string literals. - * - * @warning Different inputs may produce the same hash (collision). - * Use string comparison as fallback when hashes match. - * - * @since 0.1 - * @ingroup base_hash - * - * @code - * constexpr uint64_t h1 = fnv1a64("TokenA"); // Compile-time - * constexpr uint64_t h2 = fnv1a64("TokenB"); // Different value - * static_assert(h1 != h2, "Hash collision!"); - * @endcode - */ -constexpr uint64_t fnv1a64(const char* str, uint64_t seed = fnv1a64_offset_basis) { - return (*str == 0) ? seed - : fnv1a64(str + 1, (seed ^ static_cast(*str)) * fnv1a64_prime); -} - -/** - * @brief FNV-1a 64-bit hash for std::string_view (runtime/constexpr). - * - * @param[in] sv String view to hash. - * @param[in] seed Starting hash value (default: FNV offset basis). - * - * @return 64-bit hash value. - * - * @throws None. - * - * @note This function can be evaluated at compile time if the - * input is a constant expression. - * - * @warning Different inputs may produce the same hash (collision). - * Use string comparison as fallback when hashes match. - * - * @since 0.1 - * @ingroup base_hash - */ -constexpr uint64_t fnv1a64(std::string_view sv, uint64_t seed = fnv1a64_offset_basis) { - uint64_t result = seed; - for (char c : sv) { - result = (result ^ static_cast(c)) * fnv1a64_prime; - } - return result; -} - -/** - * @brief FNV-1a 32-bit hash constexpr implementation. - * - * @param[in] str Null-terminated string to hash. - * @param[in] seed Starting hash value (default: FNV offset basis). - * - * @return 32-bit hash value. - * - * @throws None. - * - * @note The hash is computed at compile time for string literals. - * - * @warning Different inputs may produce the same hash (collision). - * Use string comparison as fallback when hashes match. - * - * @since 0.1 - * @ingroup base_hash - */ -constexpr uint32_t fnv1a32(const char* str, uint32_t seed = fnv1a32_offset_basis) { - return (*str == 0) ? seed - : fnv1a32(str + 1, (seed ^ static_cast(*str)) * fnv1a32_prime); -} - -/** - * @brief FNV-1a 32-bit hash for std::string_view. - * - * @param[in] sv String view to hash. - * @param[in] seed Starting hash value (default: FNV offset basis). - * - * @return 32-bit hash value. - * - * @throws None. - * - * @note This function can be evaluated at compile time if the - * input is a constant expression. - * - * @warning Different inputs may produce the same hash (collision). - * Use string comparison as fallback when hashes match. - * - * @since 0.1 - * @ingroup base_hash - */ -constexpr uint32_t fnv1a32(std::string_view sv, uint32_t seed = fnv1a32_offset_basis) { - uint32_t result = seed; - for (char c : sv) { - result = (result ^ static_cast(c)) * fnv1a32_prime; - } - return result; -} - -/** - * @brief User-defined literal for compile-time FNV-1a 64-bit hashing. - * - * Enables syntax: `"TokenName"_hash` for compile-time hash computation. - * - * @param[in] str Null-terminated string to hash. - * @param[in] len Length of the string (unused, required by UDL syntax). - * - * @return 64-bit hash value. - * - * @throws None. - * - * @note This function is constexpr and evaluated at compile time. - * - * @warning None. - * - * @since 0.1 - * @ingroup base_hash - * - * @code - * constexpr uint64_t h = "MyToken"_hash; // Compile-time hash - * static_assert(h == fnv1a64("MyToken"), "Hash must match"); - * @endcode - */ -constexpr uint64_t operator""_hash(const char* str, size_t) { - return fnv1a64(str); -} - -} // namespace cf::hash diff --git a/base/include/base/helpers/once_init.hpp b/base/include/base/helpers/once_init.hpp deleted file mode 100644 index 758b67994..000000000 --- a/base/include/base/helpers/once_init.hpp +++ /dev/null @@ -1,227 +0,0 @@ -/** - * @file base/include/base/helpers/once_init.hpp - * @brief Provides thread-safe lazy initialization utilities. - * - * Implements the CallOnceInit template for one-time resource - * initialization with thread safety and forced re-initialization - * support. - * - * @author Charliechen114514 - * @date 2026-02-21 - * @version 0.1 - * @since 0.1 - * @ingroup base_helpers - */ -#pragma once -#include -#include - -namespace cf { - -/** - * @brief Thread-safe lazy initialization template. - * - * Provides a base class for implementing lazy-initialized resources - * with thread-safe initialization and forced re-initialization - * capabilities. Derived classes must implement the initialization - * logic. - * - * @tparam Resources Type of the resource to be lazily initialized. - * - * @ingroup base_helpers - * - * @note Not thread-safe during force_reinit() calls. External - * synchronization is required if concurrent re-initialization - * is possible. - * - * @code - * class MyInitializer : public cf::CallOnceInit { - * protected: - * bool init_resources() override { - * // Perform initialization - * resource.data = fetch_data(); - * return true; - * } - * bool force_do_reinit() override { - * // Perform re-initialization - * resource.data = fetch_data(); - * return true; - * } - * }; - * @endcode - */ -template struct CallOnceInit { - public: - /** - * @brief Constructs with forwarded arguments for the resource. - * - * Initializes the resource with the provided arguments and marks - * it as initialized without calling init_resources(). - * - * @tparam Args Types of arguments to forward to Resources constructor. - * @param[in] args Arguments to forward to the Resources constructor. - * - * @throws May throw if Resources constructor throws. - * - * @note None. - * - * @warning None. - * - * @since 0.1 - * @ingroup base_helpers - */ - template explicit CallOnceInit(Args&&... args) - : resource(std::forward(args)...), initialized(true) {} - - /** - * @brief Default constructor. Does not initialize the resource. - * - * The resource is initialized on the first call to - * get_resources(). - * - * @throws None. - * - * @note None. - * - * @warning None. - * - * @since 0.1 - * @ingroup base_helpers - */ - CallOnceInit() = default; - - /** - * @brief Retrieves the resource, initializing if necessary. - * - * If the resource has not been initialized, this method triggers - * thread-safe initialization using std::call_once. Subsequent - * calls return the cached resource. - * - * @return Reference to the initialized resource. - * - * @throws May throw if init_resources() throws. - * - * @note Thread-safe for initialization. Concurrent calls - * block until initialization completes. - * - * @warning Concurrent re-initialization via force_reinit() - * requires external synchronization. - * - * @since 0.1 - * @ingroup base_helpers - */ - Resources& get_resources() { - if (!initialized) { - std::call_once(init_flag, [this]() { - init_resources(); - initialized = true; - }); - } - return resource; - } - - /** - * @brief Forces re-initialization of the resource. - * - * Locks the internal mutex and calls force_do_reinit() to - * re-initialize the resource, even if it was already initialized. - * - * @throws May throw if force_do_reinit() throws. - * - * @note Blocks until the internal mutex is acquired. - * - * @warning Not thread-safe with respect to concurrent - * get_resources() calls. External synchronization is - * required. - * - * @since 0.1 - * @ingroup base_helpers - */ - void force_reinit() { - std::lock_guard lock(force_mtx); - force_do_reinit(); - initialized = true; - } - - /** - * @brief Forces re-initialization with new arguments. - * - * Locks the internal mutex, replaces the resource with a new - * instance constructed from the provided arguments, and marks - * it as initialized. - * - * @tparam Args Types of arguments to forward to Resources constructor. - * @param[in] args Arguments to forward to the Resources constructor. - * - * @throws May throw if Resources constructor throws. - * - * @note Blocks until the internal mutex is acquired. - * - * @warning Not thread-safe with respect to concurrent - * get_resources() calls. External synchronization is - * required. - * - * @since 0.1 - * @ingroup base_helpers - */ - template void force_reinit(Args&&... args) { - std::lock_guard lock(force_mtx); - resource = Resources(std::forward(args)...); - initialized = true; - } - - protected: - /// The managed resource. Ownership: owner. - Resources resource; - - /** - * @brief Performs the actual resource initialization. - * - * Called during first access to initialize the resource. - * Implementations should populate the `resource` member. - * - * @return true if initialization succeeded, false otherwise. - * - * @throws Implementation-defined exceptions. - * - * @note Called at most once during the lifetime of the object - * unless force_reinit() is called. - * - * @warning None. - * - * @since 0.1 - * @ingroup base_helpers - */ - virtual bool init_resources() = 0; - - /** - * @brief Performs forced re-initialization. - * - * Called when force_reinit() is invoked to re-initialize - * the resource. - * - * @return true if re-initialization succeeded, false otherwise. - * - * @throws Implementation-defined exceptions. - * - * @note May be called multiple times. - * - * @warning None. - * - * @since 0.1 - * @ingroup base_helpers - */ - virtual bool force_do_reinit() = 0; - - private: - /// Ensures one-time initialization. Ownership: owner. - std::once_flag init_flag; - - /// Protects force_reinit() operations. Ownership: owner. - std::mutex force_mtx; - - /// Tracks initialization state. Ownership: owner. - bool initialized{false}; -}; - -} // namespace cf diff --git a/base/include/base/indexed_vector/indexed_vector.hpp b/base/include/base/indexed_vector/indexed_vector.hpp deleted file mode 100644 index 068e8b199..000000000 --- a/base/include/base/indexed_vector/indexed_vector.hpp +++ /dev/null @@ -1,1027 +0,0 @@ -/** - * @file base/include/base/indexed_vector/indexed_vector.hpp - * @brief Provides a cursor-enhanced vector container. - * - * Implements a vector wrapper with an embedded cursor (position indicator) - * and configurable indexing modes: Fixed (cursor locked), Listed (linear, - * throws on bounds), and Recycled (circular wrap-around). Suitable for - * playlist, image browser, and similar navigation scenarios. - * - * @author Charliechen114514 - * @date 2026-04-09 - * @version 0.1 - * @since 0.1 - * @ingroup base_containers - */ - -#pragma once - -#include -#include -#include -#include -#include - -namespace cf { - -/** - * @brief Controls how the cursor navigates an indexed_vector. - * - * @ingroup base_containers - */ -enum class IndexingMode { - Fixed, ///< Cursor is locked; next/prev are no-ops. - Listed, ///< Linear traversal; throws on out-of-bounds. - Recycled ///< Circular traversal; wraps at both ends. -}; - -/** - * @brief A cursor-enhanced vector with configurable navigation modes. - * - * Wraps a @c std::vector and adds an embedded cursor that tracks the - * "current" element. Three indexing modes control how @c next() and - * @c prev() behave: - * - * - @c IndexingMode::Fixed: cursor is locked; @c next()/@c prev() are no-ops. - * - @c IndexingMode::Listed: linear traversal; throws @c std::out_of_range on bounds. - * - @c IndexingMode::Recycled: circular traversal; wraps around at both ends. - * - * The default mode can be set at compile time via the @p DefaultMode - * template parameter, and changed at runtime with @c set_mode(). - * - * @tparam T Element type. - * @tparam DefaultMode Compile-time default indexing mode (Listed if omitted). - * - * @ingroup base_containers - * - * @code - * cf::indexed_vector playlist{1, 2, 3, 4, 5}; - * playlist.set_cursor(2); // cursor -> 3 - * playlist.next(); // cursor -> 4 - * - * // Listed mode: next() at end throws - * playlist.set_mode(cf::IndexingMode::Recycled); - * playlist.set_cursor(4); - * playlist.next(); // wraps to cursor 0 - * @endcode - */ -template class indexed_vector { - public: - // ========================================================================= - // Type aliases (delegate to std::vector) - // ========================================================================= - - using value_type = T; - using allocator_type = typename std::vector::allocator_type; - using size_type = std::size_t; - using difference_type = std::ptrdiff_t; - using reference = T&; - using const_reference = const T&; - using pointer = T*; - using const_pointer = const T*; - using iterator = typename std::vector::iterator; - using const_iterator = typename std::vector::const_iterator; - using reverse_iterator = typename std::vector::reverse_iterator; - using const_reverse_iterator = typename std::vector::const_reverse_iterator; - - // ========================================================================= - // Constructors / Destructor / Assignment - // ========================================================================= - - /** - * @brief Default-constructs an empty indexed_vector. - * - * Cursor is 0, mode is @p DefaultMode. - * - * @since 0.1 - */ - indexed_vector() : mode_(DefaultMode) {} - - /** - * @brief Constructs with @p count default-inserted elements. - * @param count Number of elements. - * - * @since 0.1 - */ - explicit indexed_vector(size_type count) : data_(count), mode_(DefaultMode) {} - - /** - * @brief Constructs with @p count copies of @p value. - * @param count Number of elements. - * @param value Value to copy. - * - * @since 0.1 - */ - indexed_vector(size_type count, const T& value) : data_(count, value), mode_(DefaultMode) {} - - /** - * @brief Constructs from iterator range [first, last). - * @tparam InputIt Input iterator type. - * @param first Start of range. - * @param last End of range. - * - * @since 0.1 - */ - template indexed_vector(InputIt first, InputIt last) - : data_(first, last), mode_(DefaultMode) {} - - /** - * @brief Constructs from initializer list. - * @param init Initializer list. - * - * @since 0.1 - */ - indexed_vector(std::initializer_list init) : data_(init), mode_(DefaultMode) {} - - /** @brief Copy constructor. @since 0.1 */ - indexed_vector(const indexed_vector&) = default; - /** @brief Move constructor. @since 0.1 */ - indexed_vector(indexed_vector&&) noexcept = default; - /** @brief Copy assignment. @since 0.1 */ - indexed_vector& operator=(const indexed_vector&) = default; - /** @brief Move assignment. @since 0.1 */ - indexed_vector& operator=(indexed_vector&&) noexcept = default; - /** @brief Destructor. @since 0.1 */ - ~indexed_vector() = default; - - // ========================================================================= - // Cursor API - // ========================================================================= - - /** - * @brief Returns the current cursor position. - * @return Cursor index (0 on empty vector). - * - * @throws None. - * - * @since 0.1 - */ - size_type cursor() const noexcept { return cursor_; } - - /** - * @brief Sets the cursor to @p pos. - * @param[in] pos New cursor position. - * - * @throws std::out_of_range If @p pos >= size() (including empty vector). - * - * @since 0.1 - */ - void set_cursor(size_type pos) { - if (pos >= data_.size()) { - throw std::out_of_range("indexed_vector::set_cursor: pos out of range"); - } - cursor_ = pos; - } - - /** - * @brief Returns a reference to the element at the cursor. - * @return Reference to current element. - * - * @throws std::out_of_range If the vector is empty. - * - * @since 0.1 - */ - reference at_cursor() { - if (data_.empty()) { - throw std::out_of_range("indexed_vector::at_cursor: vector is empty"); - } - return data_[cursor_]; - } - - /** - * @brief Returns a const reference to the element at the cursor. - * @return Const reference to current element. - * - * @throws std::out_of_range If the vector is empty. - * - * @since 0.1 - */ - const_reference at_cursor() const { - if (data_.empty()) { - throw std::out_of_range("indexed_vector::at_cursor: vector is empty"); - } - return data_[cursor_]; - } - - /** - * @brief Advances the cursor forward. - * - * Behavior depends on the current mode: - * - @c Fixed: No-op. - * - @c Listed: Increments cursor; throws if already at end. - * - @c Recycled: Increments cursor; wraps to 0 at end. - * - * @throws std::out_of_range (Listed) cursor at end or vector empty; - * (Recycled) vector empty. - * - * @since 0.1 - */ - void next() { - switch (mode_) { - case IndexingMode::Fixed: - return; - case IndexingMode::Listed: - if (data_.empty() || cursor_ + 1 >= data_.size()) { - throw std::out_of_range("indexed_vector::next: cursor at end"); - } - ++cursor_; - return; - case IndexingMode::Recycled: - if (data_.empty()) { - throw std::out_of_range("indexed_vector::next: vector is empty"); - } - cursor_ = (cursor_ + 1) % data_.size(); - return; - } - } - - /** - * @brief Moves the cursor backward. - * - * Behavior depends on the current mode: - * - @c Fixed: No-op. - * - @c Listed: Decrements cursor; throws if already at start. - * - @c Recycled: Decrements cursor; wraps to end at start. - * - * @throws std::out_of_range (Listed) cursor at start or vector empty; - * (Recycled) vector empty. - * - * @since 0.1 - */ - void prev() { - switch (mode_) { - case IndexingMode::Fixed: - return; - case IndexingMode::Listed: - if (data_.empty() || cursor_ == 0) { - throw std::out_of_range("indexed_vector::prev: cursor at beginning"); - } - --cursor_; - return; - case IndexingMode::Recycled: - if (data_.empty()) { - throw std::out_of_range("indexed_vector::prev: vector is empty"); - } - cursor_ = (cursor_ == 0) ? data_.size() - 1 : cursor_ - 1; - return; - } - } - - /** - * @brief Checks whether a next element is reachable. - * @return @c true if next() would succeed. - * - * @throws None. - * - * @since 0.1 - */ - bool has_next() const noexcept { - switch (mode_) { - case IndexingMode::Fixed: - return false; - case IndexingMode::Listed: - return cursor_ + 1 < data_.size(); - case IndexingMode::Recycled: - return !data_.empty(); - } - return false; - } - - /** - * @brief Checks whether a previous element is reachable. - * @return @c true if prev() would succeed. - * - * @throws None. - * - * @since 0.1 - */ - bool has_prev() const noexcept { - switch (mode_) { - case IndexingMode::Fixed: - return false; - case IndexingMode::Listed: - return cursor_ > 0; - case IndexingMode::Recycled: - return !data_.empty(); - } - return false; - } - - // ========================================================================= - // Mode API - // ========================================================================= - - /** - * @brief Returns the current indexing mode. - * @return Current mode. - * - * @throws None. - * - * @since 0.1 - */ - IndexingMode mode() const noexcept { return mode_; } - - /** - * @brief Changes the indexing mode at runtime. - * @param[in] mode New mode. - * - * @note Does not affect cursor position or vector contents. - * - * @throws None. - * - * @since 0.1 - */ - void set_mode(IndexingMode mode) noexcept { mode_ = mode; } - - // ========================================================================= - // Element access (passthrough) - // ========================================================================= - - reference operator[](size_type pos) { return data_[pos]; } - const_reference operator[](size_type pos) const { return data_[pos]; } - - /** - * @brief Accesses element at @p pos with bounds checking. - * - * @param[in] pos Position of the element. - * - * @return Reference to the element. - * - * @throws std::out_of_range If @p pos >= size(). - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - reference at(size_type pos) { return data_.at(pos); } - - /** - * @brief Accesses element at @p pos with bounds checking (const). - * - * @param[in] pos Position of the element. - * - * @return Const reference to the element. - * - * @throws std::out_of_range If @p pos >= size(). - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - const_reference at(size_type pos) const { return data_.at(pos); } - - /** - * @brief Returns a reference to the first element. - * - * @return Reference to the first element. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - reference front() { return data_.front(); } - - /** - * @brief Returns a const reference to the first element. - * - * @return Const reference to the first element. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - const_reference front() const { return data_.front(); } - - /** - * @brief Returns a reference to the last element. - * - * @return Reference to the last element. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - reference back() { return data_.back(); } - - /** - * @brief Returns a const reference to the last element. - * - * @return Const reference to the last element. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - const_reference back() const { return data_.back(); } - - /** - * @brief Returns a pointer to the underlying element storage. - * - * @return Pointer to the first element. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - T* data() noexcept { return data_.data(); } - - /** - * @brief Returns a const pointer to the underlying element storage. - * - * @return Const pointer to the first element. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - const T* data() const noexcept { return data_.data(); } - - // ========================================================================= - // Iterators (passthrough) - // ========================================================================= - - /** - * @brief Returns an iterator to the beginning. - * - * @return Mutable iterator to the first element. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - iterator begin() noexcept { return data_.begin(); } - - /** - * @brief Returns a const iterator to the beginning. - * - * @return Const iterator to the first element. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - const_iterator begin() const noexcept { return data_.begin(); } - - /** - * @brief Returns a const iterator to the beginning. - * - * @return Const iterator to the first element. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - const_iterator cbegin() const noexcept { return data_.cbegin(); } - - /** - * @brief Returns an iterator to the end. - * - * @return Mutable iterator past the last element. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - iterator end() noexcept { return data_.end(); } - - /** - * @brief Returns a const iterator to the end. - * - * @return Const iterator past the last element. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - const_iterator end() const noexcept { return data_.end(); } - - /** - * @brief Returns a const iterator to the end. - * - * @return Const iterator past the last element. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - const_iterator cend() const noexcept { return data_.cend(); } - - /** - * @brief Returns a reverse iterator to the beginning. - * - * @return Mutable reverse iterator to the last element. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - reverse_iterator rbegin() noexcept { return data_.rbegin(); } - - /** - * @brief Returns a const reverse iterator to the beginning. - * - * @return Const reverse iterator to the last element. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - const_reverse_iterator rbegin() const noexcept { return data_.rbegin(); } - - /** - * @brief Returns a const reverse iterator to the beginning. - * - * @return Const reverse iterator to the last element. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - const_reverse_iterator crbegin() const noexcept { return data_.crbegin(); } - - /** - * @brief Returns a reverse iterator to the end. - * - * @return Mutable reverse iterator past the first element. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - reverse_iterator rend() noexcept { return data_.rend(); } - - /** - * @brief Returns a const reverse iterator to the end. - * - * @return Const reverse iterator past the first element. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - const_reverse_iterator rend() const noexcept { return data_.rend(); } - - /** - * @brief Returns a const reverse iterator to the end. - * - * @return Const reverse iterator past the first element. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - const_reverse_iterator crend() const noexcept { return data_.crend(); } - - // ========================================================================= - // Capacity (passthrough) - // ========================================================================= - - /** - * @brief Checks whether the container is empty. - * - * @return @c true if the container is empty. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - bool empty() const noexcept { return data_.empty(); } - - /** - * @brief Returns the number of elements. - * - * @return Current number of elements. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - size_type size() const noexcept { return data_.size(); } - - /** - * @brief Returns the maximum number of elements possible. - * - * @return Maximum possible number of elements. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - size_type max_size() const noexcept { return data_.max_size(); } - - /** - * @brief Returns the current allocated capacity. - * - * @return Number of elements that can be held. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - size_type capacity() const noexcept { return data_.capacity(); } - - /** - * @brief Reserves storage for at least @p new_cap elements. - * - * @param[in] new_cap New capacity to reserve. - * - * @throws std::length_error If @p new_cap > max_size(). - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - void reserve(size_type new_cap) { data_.reserve(new_cap); } - - /** - * @brief Reduces capacity to fit the current size. - * - * @throws None. - * - * @note None. - * @warning None. - * @since 0.1 - * @ingroup base_containers - */ - void shrink_to_fit() { data_.shrink_to_fit(); } - - // ========================================================================= - // Modifiers (with cursor adjustment) - // ========================================================================= - - /** - * @brief Clears all elements and resets the cursor to 0. - * - * @since 0.1 - */ - void clear() { - data_.clear(); - cursor_ = 0; - } - - /** - * @brief Appends an element to the end. Cursor is not affected. - * @param[in] value Value to copy. - * - * @since 0.1 - */ - void push_back(const T& value) { data_.push_back(value); } - - /** - * @brief Appends an element to the end. Cursor is not affected. - * @param[in] value Value to move. - * - * @since 0.1 - */ - void push_back(T&& value) { data_.push_back(std::move(value)); } - - /** - * @brief Constructs an element in-place at the end. Cursor is not affected. - * @tparam Args Constructor argument types. - * @param[in] args Arguments to forward. - * @return Reference to the inserted element. - * - * @since 0.1 - */ - template reference emplace_back(Args&&... args) { - return data_.emplace_back(std::forward(args)...); - } - - /** - * @brief Removes the last element. Adjusts cursor if necessary. - * - * If the cursor was on the last element, it moves back one position. - * If the vector becomes empty, cursor resets to 0. - * - * @since 0.1 - */ - void pop_back() { - if (data_.size() <= 1) { - data_.pop_back(); - cursor_ = 0; - return; - } - if (cursor_ == data_.size() - 1) { - cursor_ = data_.size() - 2; - } - data_.pop_back(); - } - - /** - * @brief Inserts an element before @p pos. Adjusts cursor if needed. - * @param[in] pos Iterator before which to insert. - * @param[in] value Value to copy. - * @return Iterator pointing to the inserted element. - * - * @since 0.1 - */ - iterator insert(const_iterator pos, const T& value) { - auto idx = static_cast(pos - data_.cbegin()); - auto it = data_.insert(pos, value); - if (idx <= cursor_) { - ++cursor_; - } - return it; - } - - /** - * @brief Inserts an element before @p pos. Adjusts cursor if needed. - * @param[in] pos Iterator before which to insert. - * @param[in] value Value to move. - * @return Iterator pointing to the inserted element. - * - * @since 0.1 - */ - iterator insert(const_iterator pos, T&& value) { - auto idx = static_cast(pos - data_.cbegin()); - auto it = data_.insert(pos, std::move(value)); - if (idx <= cursor_) { - ++cursor_; - } - return it; - } - - /** - * @brief Inserts @p count copies of @p value before @p pos. - * @param[in] pos Iterator before which to insert. - * @param[in] count Number of copies. - * @param[in] value Value to copy. - * @return Iterator pointing to the first inserted element. - * - * @since 0.1 - */ - iterator insert(const_iterator pos, size_type count, const T& value) { - auto idx = static_cast(pos - data_.cbegin()); - auto it = data_.insert(pos, count, value); - if (idx <= cursor_) { - cursor_ += count; - } - return it; - } - - /** - * @brief Inserts elements from range [first, last) before @p pos. - * @tparam InputIt Input iterator type. - * @param[in] pos Iterator before which to insert. - * @param[in] first Start of range. - * @param[in] last End of range. - * @return Iterator pointing to the first inserted element. - * - * @since 0.1 - */ - template iterator insert(const_iterator pos, InputIt first, InputIt last) { - auto idx = static_cast(pos - data_.cbegin()); - auto size_before = data_.size(); - auto it = data_.insert(pos, first, last); - auto inserted = data_.size() - size_before; - if (idx <= cursor_) { - cursor_ += inserted; - } - return it; - } - - /** - * @brief Inserts elements from initializer list before @p pos. - * @param[in] pos Iterator before which to insert. - * @param[in] ilist Initializer list. - * @return Iterator pointing to the first inserted element. - * - * @since 0.1 - */ - iterator insert(const_iterator pos, std::initializer_list ilist) { - auto idx = static_cast(pos - data_.cbegin()); - auto size_before = data_.size(); - auto it = data_.insert(pos, ilist); - auto inserted = data_.size() - size_before; - if (idx <= cursor_) { - cursor_ += inserted; - } - return it; - } - - /** - * @brief Constructs an element in-place before @p pos. - * @tparam Args Constructor argument types. - * @param[in] pos Iterator before which to insert. - * @param[in] args Arguments to forward. - * @return Iterator pointing to the inserted element. - * - * @since 0.1 - */ - template iterator emplace(const_iterator pos, Args&&... args) { - auto idx = static_cast(pos - data_.cbegin()); - auto it = data_.emplace(pos, std::forward(args)...); - if (idx <= cursor_) { - ++cursor_; - } - return it; - } - - /** - * @brief Erases the element at @p pos. Adjusts cursor if needed. - * @param[in] pos Iterator to the element to erase. - * @return Iterator following the erased element. - * - * @since 0.1 - */ - iterator erase(const_iterator pos) { - auto idx = static_cast(pos - data_.cbegin()); - auto it = data_.erase(pos); - adjust_cursor_on_erase(idx, 1); - return it; - } - - /** - * @brief Erases elements in range [first, last). Adjusts cursor if needed. - * @param[in] first Start of range. - * @param[in] last End of range. - * @return Iterator following the last erased element. - * - * @since 0.1 - */ - iterator erase(const_iterator first, const_iterator last) { - auto erased_start = static_cast(first - data_.cbegin()); - auto erased_count = static_cast(last - first); - auto it = data_.erase(first, last); - adjust_cursor_on_erase(erased_start, erased_count); - return it; - } - - /** - * @brief Resizes the container. Adjusts cursor if shrunk. - * @param[in] count New size. - * - * @since 0.1 - */ - void resize(size_type count) { - data_.resize(count); - clamp_cursor(); - } - - /** - * @brief Resizes the container. Adjusts cursor if shrunk. - * @param[in] count New size. - * @param[in] value Value to fill new elements with. - * - * @since 0.1 - */ - void resize(size_type count, const T& value) { - data_.resize(count, value); - clamp_cursor(); - } - - /** - * @brief Swaps contents with another indexed_vector. - * @param[in,out] other The other indexed_vector. - * - * @since 0.1 - */ - void swap(indexed_vector& other) noexcept { - using std::swap; - swap(data_, other.data_); - swap(cursor_, other.cursor_); - swap(mode_, other.mode_); - } - - // ========================================================================= - // Non-member functions (friend) - // ========================================================================= - - /** - * @brief Equality comparison. - * @return @c true if contents, cursor, and mode are equal. - * - * @since 0.1 - * @ingroup base_containers - */ - friend bool operator==(const indexed_vector& a, const indexed_vector& b) { - return a.data_ == b.data_ && a.cursor_ == b.cursor_ && a.mode_ == b.mode_; - } - - /** - * @brief Inequality comparison. - * @return @c true if contents, cursor, or mode differ. - * - * @since 0.1 - * @ingroup base_containers - */ - friend bool operator!=(const indexed_vector& a, const indexed_vector& b) { return !(a == b); } - - /** - * @brief Swaps two indexed_vector instances. - * - * @since 0.1 - * @ingroup base_containers - */ - friend void swap(indexed_vector& a, indexed_vector& b) noexcept { a.swap(b); } - - private: - std::vector data_; - size_type cursor_ = 0; - IndexingMode mode_ = DefaultMode; - - /** - * @brief Adjusts cursor after an erase operation. - * @param erased_start Index where erasure began. - * @param erased_count Number of elements erased. - * - * @since 0.1 - */ - void adjust_cursor_on_erase(size_type erased_start, size_type erased_count) { - size_type erased_end = erased_start + erased_count; - if (cursor_ >= erased_start && cursor_ < erased_end) { - // Cursor was in the erased range — point to erase start. - cursor_ = erased_start; - } else if (cursor_ >= erased_end) { - // Cursor was after the erased range — shift back. - cursor_ -= erased_count; - } - // else: cursor was before erased range — no adjustment. - - clamp_cursor(); - } - - /** - * @brief Clamps cursor to valid range [0, size). Resets to 0 if empty. - * - * @since 0.1 - */ - void clamp_cursor() { - if (data_.empty()) { - cursor_ = 0; - } else if (cursor_ >= data_.size()) { - cursor_ = data_.size() - 1; - } - } -}; - -} // namespace cf diff --git a/base/include/base/lockfree/mpsc_queue.hpp b/base/include/base/lockfree/mpsc_queue.hpp deleted file mode 100644 index 48f48562f..000000000 --- a/base/include/base/lockfree/mpsc_queue.hpp +++ /dev/null @@ -1,369 +0,0 @@ -/** - * @file base/include/base/lockfree/mpsc_queue.hpp - * @brief Multi-Producer Single-Consumer (MPSC) lock-free queue implementation. - * - * Provides a fixed-capacity ring buffer optimized for MPSC scenarios. - * Suitable for high-throughput logging, event processing, and task queues. - * Uses index-based operations to avoid ABA problems and maintain cache-friendly behavior. - * - * @author Charliechen114514 - * @date 2026-03-16 - * @version 0.1 - * @since 0.1 - * @ingroup base_lockfree - */ - -#pragma once - -#include -#include -#include - -#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) -# include -#endif - -#include "base/span/span.h" - -namespace cf { -namespace lockfree { - -namespace detail { - -/** - * @brief Issues a short processor-friendly spin-wait hint. - * - * Uses the platform pause intrinsic on x86 targets and falls back to a compiler - * signal fence on other architectures. - * - * @throws None. - * @note Intended for tight lock-free retry loops. - * @warning Does not yield the current thread or provide scheduling fairness. - * @since 0.1 - * @ingroup base_lockfree - */ -inline void cpu_relax() noexcept { -#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) - _mm_pause(); -#elif (defined(__x86_64__) || defined(__i386__)) && (defined(__GNUC__) || defined(__clang__)) - __builtin_ia32_pause(); -#else - std::atomic_signal_fence(std::memory_order_seq_cst); -#endif -} - -} // namespace detail - -/** - * @brief Multi-Producer Single-Consumer lock-free queue. - * - * Implements a fixed-capacity ring buffer optimized for MPSC scenarios. - * Multiple producer threads can safely call push operations concurrently, - * while only one consumer thread should call pop operations. - * - * @tparam T Element type. Must be move-constructible and move-assignable. - * @tparam Capacity Queue capacity. Must be a power of 2 for efficient indexing. - * - * @ingroup base_lockfree - * - * @note Thread-safe for producers. NOT thread-safe for multiple consumers. - * - * @code - * cf::lockfree::MpscQueue queue; - * - * // Producer thread(s): - * queue.tryPush(42); - * - * // Consumer thread: - * int value; - * if (queue.tryPop(value)) { - * // Process value - * } - * @endcode - */ -template class MpscQueue { - static_assert((Capacity & (Capacity - 1)) == 0, "Capacity must be a power of 2"); - - public: - using value_type = T; - using size_type = size_t; - - /** - * @brief Default constructor. - * - * Initializes an empty queue. All slots are pre-allocated and ready for use. - * - * @ingroup base_lockfree - */ - MpscQueue() noexcept { - // Initialize sequence for each cell - // Each cell starts with sequence == its index - for (size_type i = 0; i < Capacity; ++i) { - buffer_[i].sequence.store(i, std::memory_order_relaxed); - } - } - - /** - * @brief Destructor. - * - * Destroys any remaining elements in the queue. - * - * @warning Only the consumer thread should destroy the queue. - * @ingroup base_lockfree - */ - ~MpscQueue() noexcept { - // Destroy any remaining elements - while (!empty()) { - T dummy; - tryPop(dummy); - } - } - - // Non-copyable, non-movable - MpscQueue(const MpscQueue&) = delete; - MpscQueue& operator=(const MpscQueue&) = delete; - MpscQueue(MpscQueue&&) = delete; - MpscQueue& operator=(MpscQueue&&) = delete; - - // ======================================================================== - // Core Operations - // ======================================================================== - - /** - * @brief Attempts to push a single value into the queue. - * - * Thread-safe: Multiple producers can call this method concurrently. - * Spin-waits if the queue is full. - * - * @param[in] value Value to push (moved into the queue). - * @return true if successful (always returns true). - * - * @note Uses release semantics on success to ensure proper publication. - * @ingroup base_lockfree - */ - bool tryPush(T&& value) noexcept { - Cell* cell; - size_type pos = writePos_.fetch_add(1, std::memory_order_relaxed); - cell = &buffer_[pos & (Capacity - 1)]; - - size_type seq = cell->sequence.load(std::memory_order_acquire); - - // For MPSC: wait for slot to be available - // The slot is available when seq == pos - // If seq < pos, it means another producer is using this slot (wait) - // If seq > pos, it means consumer hasn't caught up (queue full) - // Uses a simple approach: just wait without timeout for MPSC - while (seq != pos) { - detail::cpu_relax(); - seq = cell->sequence.load(std::memory_order_acquire); - } - - // Construct in-place - new (cell->ptr()) T(std::move(value)); - - // Publish with release semantics - cell->sequence.store(pos + 1, std::memory_order_release); - - return true; - } - - /** - * @brief Attempts to pop a single value from the queue. - * - * NOT thread-safe for consumers: Only one consumer thread should call this. - * - * @param[out] out Reference to store the popped value. - * @return true if successful, false if queue is empty. - * - * @note Uses acquire semantics to synchronize with producers. - * @ingroup base_lockfree - */ - bool tryPop(T& out) noexcept { - const size_type rp = readPos_.load(std::memory_order_relaxed); - Cell* cell = &buffer_[rp & (Capacity - 1)]; - - size_type seq = cell->sequence.load(std::memory_order_acquire); - if (seq != rp + 1) { - return false; // Empty - } - - out = std::move(*cell->ptr()); - cell->ptr()->~T(); // Destroy the object in the cell - - // Publish slot availability for next round - // Set to rp + Capacity so producer at pos = rp + Capacity can use it - cell->sequence.store(rp + Capacity, std::memory_order_release); - - // Advance the consumer cursor with release ordering so that producer threads - // reading the approximate size()/empty() observe a consistent value instead of - // racing with this write. - readPos_.store(rp + 1, std::memory_order_release); - return true; - } - - // ======================================================================== - // Batch Operations - // ======================================================================== - - /** - * @brief Attempts to push multiple values in one operation. - * - * More efficient than individual pushes due to: - * - Single sequence number fetch - * - Better cache locality - * - * Thread-safe: Multiple producers can call this concurrently. - * - * @param[in] values Span of values to push. - * @return Number of values actually pushed (0 to values.size()). - * - * @ingroup base_lockfree - */ - size_type tryPushBatch(cf::span values) noexcept { - if (values.empty()) { - return 0; - } - - const size_type count = values.size(); - const size_type start = writePos_.fetch_add(count, std::memory_order_relaxed); - - // For each slot, wait for it to become available, then write - for (size_type i = 0; i < count; ++i) { - size_type pos = start + i; - Cell* cell = &buffer_[pos & (Capacity - 1)]; - - size_type seq = cell->sequence.load(std::memory_order_acquire); - // Wait for this slot to become available (like tryPush) - while (seq != pos) { - detail::cpu_relax(); - seq = cell->sequence.load(std::memory_order_acquire); - } - - // Slot is available, write the value - new (cell->ptr()) T(std::move(values[i])); - cell->sequence.store(pos + 1, std::memory_order_release); - } - - return count; - } - - /** - * @brief Attempts to pop multiple values in one operation. - * - * Critical for worker thread efficiency: - * - Reduces per-item overhead - * - Amortizes synchronization cost - * - Better cache utilization - * - * NOT thread-safe for consumers: Only one consumer thread should call this. - * - * @param[out] out Output buffer to store popped values. - * @param[in] max_count Maximum number of values to pop. - * @return Number of values actually popped. - * - * @ingroup base_lockfree - */ - size_type tryPopBatch(T* out, size_type max_count) noexcept { - size_type popped = 0; - - for (; popped < max_count; ++popped) { - const size_type rp = readPos_.load(std::memory_order_relaxed); - Cell* cell = &buffer_[rp & (Capacity - 1)]; - - size_type seq = cell->sequence.load(std::memory_order_acquire); - if (seq != rp + 1) { - break; // No more available - } - - out[popped] = std::move(*cell->ptr()); - cell->ptr()->~T(); - - cell->sequence.store(rp + Capacity, std::memory_order_release); - - readPos_.store(rp + 1, std::memory_order_release); - } - - return popped; - } - - // ======================================================================== - // Capacity and State Queries - // ======================================================================== - - /** - * @brief Checks if the queue is approximately empty. - * - * @return true if the queue appears empty, false otherwise. - * - * @warning May return stale data in concurrent scenarios. - * This is an approximate check, not a thread-safe guarantee. - * @ingroup base_lockfree - */ - bool empty() const noexcept { - return readPos_.load(std::memory_order_acquire) >= - writePos_.load(std::memory_order_acquire); - } - - /** - * @brief Gets the approximate current size. - * - * @return Approximate number of elements in the queue. - * - * @warning Expensive operation. May return stale data in concurrent scenarios. - * Use sparingly, primarily for monitoring/debugging. - * @ingroup base_lockfree - */ - size_type size() const noexcept { - size_type writePos = writePos_.load(std::memory_order_acquire); - size_type rp = readPos_.load(std::memory_order_acquire); - if (writePos < rp) { - return 0; - } - size_type size = writePos - rp; - return size > Capacity ? Capacity : size; - } - - /** - * @brief Gets the maximum capacity of the queue. - * - * @return Maximum number of elements the queue can hold. - * - * @ingroup base_lockfree - */ - static constexpr size_type capacity() noexcept { return Capacity; } - - private: - /** - * @brief Internal cell structure for ring buffer storage. - * - * Each cell contains a sequence number for synchronization - * and raw storage for the element. - */ - struct Cell { - std::atomic sequence; ///< Sequence number for synchronization - alignas(alignof(T)) unsigned char storage[sizeof(T)]; ///< Raw storage for T - - /** - * @brief Gets a typed pointer to the storage. - * @return Pointer to the stored element. - */ - T* ptr() noexcept { return reinterpret_cast(storage); } - - /** - * @brief Gets a const typed pointer to the storage. - * @return Const pointer to the stored element. - */ - const T* ptr() const noexcept { return reinterpret_cast(storage); } - }; - - std::array buffer_; ///< Ring buffer storage - std::atomic writePos_{0}; ///< Current write position (multi-producer) - std::atomic readPos_{0}; ///< Current read position (single-consumer) - - // Padding to prevent false sharing between producer and consumer - char padding_[64 - sizeof(std::atomic) - sizeof(std::atomic) - - sizeof(buffer_) % 64]; -}; - -} // namespace lockfree -} // namespace cf diff --git a/base/include/base/macro/build_type.h b/base/include/base/macro/build_type.h deleted file mode 100644 index 163df53c9..000000000 --- a/base/include/base/macro/build_type.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -/** - * @file base/include/base/macro/build_type.h - * @brief Defines build type detection macros for the CFDesktop project. - * - * Provides macros that indicate the current build configuration (Debug, - * Develop, or Release) based on preprocessor definitions. - * - * @author N/A - * @date N/A - * @version N/A - * @since N/A - * @ingroup base_macro - */ - -// CFDESKTOP_DEBUG_BUILD is defined when build type is Debug -#if defined(_CFDESKTOPDEBUG) -# define CFDESKTOP_DEBUG_BUILD -#endif - -#if defined(NDEBUG) -# define CFDESKTOP_DEVELOP_BUILD -#endif \ No newline at end of file diff --git a/base/include/base/macro/plain_property.h b/base/include/base/macro/plain_property.h deleted file mode 100644 index aef53320f..000000000 --- a/base/include/base/macro/plain_property.h +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @file base/include/base/macro/plain_property.h - * @brief Macro for generating simple property getters and setters. - * - * Defines the CF_PLAIN_PROPERTY macro which generates getter and setter - * methods for a class member variable with a default value. - * - * @author N/A - * @date N/A - * @version N/A - * @since N/A - * @ingroup none - */ -#pragma once - -#ifndef CF_PLAIN_PROPERTY -# define CF_PLAIN_PROPERTY(val_type, val_name, default_value) \ - public: \ - val_type& get_##val_name() { \ - return val_name; \ - } \ - const val_type& get_##val_name##_const() const { \ - return val_name; \ - } \ - void set_##val_name(const val_type& v) { \ - val_name = v; \ - } \ - \ - private: \ - val_type val_name{default_value}; - -#endif \ No newline at end of file diff --git a/base/include/base/macro/system_judge.h b/base/include/base/macro/system_judge.h deleted file mode 100644 index 97c50d858..000000000 --- a/base/include/base/macro/system_judge.h +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @file base/include/base/macro/system_judge.h - * @brief Defines platform and architecture detection macros. - * - * Provides compile-time macros for detecting the operating system - * and CPU architecture. - * - * @author Charliechen114514 - * @date 2026-02-22 - * @version 0.1 - * @since 0.1 - * @ingroup base_macros - */ -#pragma once - -/* ==================== Operating System Detection ==================== */ - -// Windows platform detection -#if defined(_WIN32) || defined(_WIN64) -# define CFDESKTOP_OS_WINDOWS -#endif - -// Linux platform detection -#if defined(__linux__) -# define CFDESKTOP_OS_LINUX -#endif - -// WSL Specific (set by CMake when WSL is detected) -#ifdef CFDESKTOP_OS_WSL -# define CFDESKTOP_OS_WSL_ENABLED -#endif - -/* ==================== Architecture Detection ==================== */ - -// x86_64 (AMD64) detection -#if defined(__x86_64__) || defined(_M_X64) || defined(__amd64__) -# define CFDESKTOP_ARCH_X86_64 -#endif - -// ARM64 detection -#if defined(__aarch64__) || defined(_M_ARM64) -# define CFDESKTOP_ARCH_ARM64 -#endif - -// ARM32 detection -#if defined(__arm__) || defined(_M_ARM) -# define CFDESKTOP_ARCH_ARM32 -#endif diff --git a/base/include/base/macros.h b/base/include/base/macros.h deleted file mode 100644 index e3ca35ce2..000000000 --- a/base/include/base/macros.h +++ /dev/null @@ -1,13 +0,0 @@ -/** - * @file macros.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Macros helps to unified the base level - * @version 0.1 - * @date 2026-02-21 - * - * @copyright Copyright (c) 2026 - * - */ -#pragma once - -#include "macro/system_judge.h" //< see what os diff --git a/base/include/base/policy_chain/policy_chain.hpp b/base/include/base/policy_chain/policy_chain.hpp deleted file mode 100644 index 85b0dd50e..000000000 --- a/base/include/base/policy_chain/policy_chain.hpp +++ /dev/null @@ -1,335 +0,0 @@ -#pragma once - -/** - * @file base/include/base/policy_chain/policy_chain.hpp - * @brief Provides a chain-of-responsibility pattern implementation with fallback. - * - * Defines PolicyChain, PolicyChainBuilder, and factory functions for executing - * a sequence of callable policies in order, falling back to the next policy if - * the previous one returns std::nullopt. - * - * @author CFDesktop Team - * @date 2026-03-16 - * @version 0.13.1 - * @since 0.13.0 - * @ingroup base_policy_chain - */ - -#include -#include -#include -#include -#include -#include -#include - -namespace cf { - -namespace policy_chain_detail { - -#if defined(__clang__) && (__clang_major__ < 19) -# define CF_POLICY_CHAIN_INVOKE_BARRIER __attribute__((noinline, optnone)) -#else -# define CF_POLICY_CHAIN_INVOKE_BARRIER -#endif - -// Clang 18 can miscompile inlined std::optional-returning fallback chains under -O3. -template [[nodiscard]] CF_POLICY_CHAIN_INVOKE_BARRIER auto -invoke_policy(Policy const& policy, CallArgs&&... args) - -> decltype(policy(std::forward(args)...)) { - return policy(std::forward(args)...); -} - -#undef CF_POLICY_CHAIN_INVOKE_BARRIER - -} // namespace policy_chain_detail - -/** - * @brief PolicyChain with fallback mechanism - * - * Executes functions in chain order (head first). - * If a function returns std::nullopt, falls back to the next. - * Stops at first successful result (returns a value). - * - * @tparam T The value type wrapped in std::optional - * @tparam Args Argument types for the policies - * - * @example - * @code - * // Method 1: Direct construction - * PolicyChain chain; - * chain.add_front([](const std::string& s) -> std::optional { - * if (!s.empty()) return std::stoi(s); - * return std::nullopt; - * }); - * - * // Method 2: Using builder - * auto chain = make_policy_chain( - * [](const std::string& s) -> std::optional { return s.length(); }, - * [](const std::string& s) -> std::optional { - * if (!s.empty()) return std::stoi(s); - * return std::nullopt; - * } - * ); - * - * // Execute - * if (auto result = chain.execute("42")) { - * std::cout << "Result: " << *result << std::endl; - * } - * @endcode - */ -template class PolicyChain { - public: - using PolicyType = std::function(Args...)>; - using ValueType = Ret; - using SizeType = std::size_t; - - PolicyChain() = default; - PolicyChain(const PolicyChain&) = default; - PolicyChain& operator=(const PolicyChain&) = default; - PolicyChain(PolicyChain&&) noexcept = default; - PolicyChain& operator=(PolicyChain&&) noexcept = default; - - /** - * @brief Add a policy to the front of the chain (highest priority) - */ - template void add_front(Policy&& policy) { - policies_.emplace_front(std::forward(policy)); - } - - /** - * @brief Add a policy to the back of the chain (lowest priority) - */ - template void add_back(Policy&& policy) { - policies_.emplace_back(std::forward(policy)); - } - - /** - * @brief Execute policies in chain order until one succeeds - * @return First non-null result, or std::nullopt if all fail - */ - [[nodiscard]] std::optional execute(Args... args) const { - for (const auto& policy : policies_) { - auto result = policy_chain_detail::invoke_policy(policy, args...); - if (result.has_value()) { - return std::move(result); - } - } - return std::nullopt; - } - - /** - * @brief Execute policies in chain order (operator() overload) - */ - [[nodiscard]] std::optional operator()(Args... args) const { return execute(args...); } - - /** - * @brief Remove all policies - */ - void clear() { policies_.clear(); } - - /** - * @brief Check if chain is empty - */ - [[nodiscard]] bool empty() const { return policies_.empty(); } - - /** - * @brief Get number of policies - */ - [[nodiscard]] SizeType size() const { return policies_.size(); } - - /** - * @brief Get iterator to beginning (for range-based for) - */ - [[nodiscard]] auto begin() const { return policies_.begin(); } - - /** - * @brief Get iterator to end - */ - [[nodiscard]] auto end() const { return policies_.end(); } - - private: - struct PolicyConcept { - virtual ~PolicyConcept() = default; - [[nodiscard]] virtual std::optional invoke(Args... args) const = 0; - [[nodiscard]] virtual std::unique_ptr clone() const = 0; - }; - - template struct PolicyModel final : PolicyConcept { - explicit PolicyModel(Policy policy) : policy_(std::move(policy)) {} - - [[nodiscard]] std::optional invoke(Args... args) const override { - auto result = std::invoke(policy_, args...); -#if defined(__clang__) && (__clang_major__ < 19) - // Clang 18 can mispack disengaged small std::optional returns under -O3. - if (!result.has_value()) { - return std::nullopt; - } -#endif - return result; - } - - [[nodiscard]] std::unique_ptr clone() const override { - return std::make_unique(policy_); - } - - mutable Policy policy_; - }; - - class PolicyEntry { - public: - template explicit PolicyEntry(Policy&& policy) - : policy_(make_policy_model(std::forward(policy))) {} - - PolicyEntry(const PolicyEntry& other) : policy_(other.policy_->clone()) {} - PolicyEntry(PolicyEntry&&) noexcept = default; - - PolicyEntry& operator=(const PolicyEntry& other) { - if (this != &other) { - policy_ = other.policy_->clone(); - } - return *this; - } - - PolicyEntry& operator=(PolicyEntry&&) noexcept = default; - - [[nodiscard]] std::optional operator()(Args... args) const { - return policy_->invoke(args...); - } - - private: - template - [[nodiscard]] static std::unique_ptr make_policy_model(Policy&& policy) { - using StoredPolicy = std::decay_t; - static_assert(std::is_copy_constructible_v, - "PolicyChain policies must be copy constructible"); - return std::make_unique>(std::forward(policy)); - } - - std::unique_ptr policy_; - }; - - std::list policies_; -}; - -// ============================================================================ -// Factory Functions -// ============================================================================ - -/** - * @brief Create a PolicyChain with fluent builder API - * - * Policies are executed in order (first = highest priority). - * - * @tparam T Return value type - * @tparam Args Argument types - * @tparam Policies Types of the policy callables - * - * @example - * @code - * auto chain = make_policy_chain( - * [](int x) -> std::optional { - * if (x > 0) return x * 2; - * return std::nullopt; - * }, - * [](int x) -> std::optional { - * if (x < 0) return -x; - * return std::nullopt; - * }, - * [](int) -> std::optional { - * return 0; // Default fallback - * } - * ); - * - * auto result = chain.execute(-5); // Returns 5 - * @endcode - */ -template -[[nodiscard]] auto make_policy_chain(Policies&&... policies) { - PolicyChain chain; - (chain.add_back(std::forward(policies)), ...); - return chain; -} - -/** - * @brief Builder for creating PolicyChain with fluent API - * - * @example - * @code - * auto chain = policy_chain_builder() - * .then([](const std::string& s) -> std::optional { - * if (!s.empty()) return std::stoi(s); - * return std::nullopt; - * }) - * .then([](const std::string& s) -> std::optional { - * return static_cast(s.length()); - * }) - * .then([](const std::string&) -> std::optional { - * return 0; // Default - * }) - * .build(); - * @endcode - */ -template class PolicyChainBuilder { - public: - using PolicyType = std::function(Args...)>; - - PolicyChainBuilder() = default; - - /** - * @brief Adds a policy to the chain. - * - * Adds the policy to the back of the chain with the lowest priority. - * Policies are executed in the order they are added. - * - * @param[in] policy The policy callable to add to the chain. - * @return Reference to this builder for method chaining. - * @throws None - * @note The policy is moved into the chain. - * @warning None - * @since 0.13.0 - * @ingroup base_policy_chain - */ - template PolicyChainBuilder& then(Policy&& policy) { - chain_.add_back(std::forward(policy)); - return *this; - } - - /** - * @brief Build the PolicyChain - */ - [[nodiscard]] PolicyChain build() && { return std::move(chain_); } - - /** - * @brief Build and return by copy - */ - [[nodiscard]] PolicyChain build() const& { return chain_; } - - private: - PolicyChain chain_; -}; - -/** - * @brief Create a PolicyChain builder - * - * @tparam T Return value type - * @tparam Args Argument types - * - * @example - * @code - * auto chain = policy_chain_builder() - * .then([](int x) -> std::optional { - * if (x > 0) return x * 2; - * return std::nullopt; - * }) - * .then([](int x) -> std::optional { - * return 0; - * }) - * .build(); - * @endcode - */ -template [[nodiscard]] auto policy_chain_builder() { - return PolicyChainBuilder{}; -} - -} // namespace cf diff --git a/base/include/base/scope_guard/scope_guard.hpp b/base/include/base/scope_guard/scope_guard.hpp deleted file mode 100644 index 934073bc9..000000000 --- a/base/include/base/scope_guard/scope_guard.hpp +++ /dev/null @@ -1,144 +0,0 @@ -/** - * @file base/include/base/scope_guard/scope_guard.hpp - * @brief Provides RAII-style scope guard for automatic cleanup. - * - * Implements a scope guard that executes a provided function upon - * destruction, ensuring cleanup operations are performed even when - * exceptions occur. - * - * @author Charliechen114514 - * @date 2026-02-21 - * @version 0.1 - * @since 0.1 - * @ingroup base_utilities - */ -#pragma once -#include - -namespace cf { - -/** - * @brief RAII scope guard for automatic cleanup operations. - * - * Executes a provided function upon destruction unless explicitly - * dismissed. Useful for ensuring resources are released or - * cleanup operations are performed when leaving a scope. - * - * @ingroup base_utilities - * - * @note The guard is not movable or copyable. This design ensures - * cleanup operations are executed exactly once. - * - * @warning The stored function is invoked during destruction. Any - * exceptions thrown by the function propagate during - * stack unwinding. - * - * @code - * { - * FILE* f = fopen("data.txt", "r"); - * cf::ScopeGuard guard([&f]() { fclose(f); }); - * - * // Use the file... - * // File is closed automatically when leaving scope. - * } - * @endcode - */ -class ScopeGuard { - public: - /** - * @brief Constructs a scope guard with a cleanup function. - * - * @param[in] f Function to execute on destruction. Must be callable - * with no arguments. - * - * @throws May throw if function copy/move constructor throws. - * - * @note None. - * - * @warning None. - * - * @since 0.1 - * @ingroup base_utilities - */ - template explicit ScopeGuard(F&& f) : fn_(std::forward(f)) {} - - /** - * @brief Destroys the scope guard and executes the cleanup function. - * - * The cleanup function is executed only if the guard has not been - * dismissed. - * - * @throws May propagate exceptions thrown by the cleanup function. - * - * @note None. - * - * @warning Any exceptions thrown by the cleanup function - * propagate during stack unwinding. - * - * @since 0.1 - * @ingroup base_utilities - */ - ~ScopeGuard() noexcept(false) { - if (active_) - fn_(); - } - - /** - * @brief Copy constructor is deleted. - * - * @since 0.1 - * @ingroup base_utilities - */ - ScopeGuard(const ScopeGuard&) = delete; - - /** - * @brief Copy assignment operator is deleted. - * - * @since 0.1 - * @ingroup base_utilities - */ - ScopeGuard& operator=(const ScopeGuard&) = delete; - - /** - * @brief Move constructor is deleted. - * - * @since 0.1 - * @ingroup base_utilities - */ - ScopeGuard(ScopeGuard&&) = delete; - - /** - * @brief Move assignment operator is deleted. - * - * @since 0.1 - * @ingroup base_utilities - */ - ScopeGuard& operator=(ScopeGuard&&) = delete; - - /** - * @brief Dismisses the scope guard. - * - * Prevents the cleanup function from being executed upon - * destruction. Irreversible once called. - * - * @throws None. - * - * @note Once dismissed, the cleanup function cannot be - * re-activated. - * - * @warning None. - * - * @since 0.1 - * @ingroup base_utilities - */ - void dismiss() noexcept { active_ = false; } - - private: - /// The cleanup function to execute on destruction. Ownership: owner. - std::function fn_; - - /// Indicates whether the guard is still active. Ownership: owner. - bool active_ = true; -}; - -} // namespace cf diff --git a/base/include/base/singleton/simple_singleton.hpp b/base/include/base/singleton/simple_singleton.hpp deleted file mode 100644 index 91e3f204d..000000000 --- a/base/include/base/singleton/simple_singleton.hpp +++ /dev/null @@ -1,76 +0,0 @@ -/** - * @file base/include/base/singleton/simple_singleton.hpp - * @brief Simple thread-safe singleton using Meyer's singleton pattern. - * - * Provides a minimal singleton implementation that leverages C++11 guarantees - * for thread-safe static local variable initialization. - * - * @author CFDesktop Team - * @date 2026-02-20 - * @version 0.13.1 - * @since 0.1 - * @ingroup base_singleton - */ - -#pragma once - -namespace cf { - -/** - * @brief Simple thread-safe singleton using Meyer's singleton pattern. - * - * Provides a minimal singleton implementation that leverages C++11 guarantees - * for thread-safe static local variable initialization. The instance is created - * on first access and destroyed on application exit. - * - * @tparam SingleTargetClass Type of the singleton instance. - * - * @ingroup base_singleton - * - * @note Thread-safe. C++11 guarantees thread-safe initialization of function-local - * static variables. - * - * @warning None - * - * @code - * class MyClass { - * public: - * void doSomething() {} - * }; - * - * using MySingleton = SimpleSingleton; - * MySingleton::instance().doSomething(); - * @endcode - */ -template class SimpleSingleton { - public: - /** - * @brief Returns the singleton instance. - * - * Creates the instance on first call, subsequent calls return the same - * instance. - * - * @return Reference to the singleton instance. - * @throws None - * @note Thread-safe initialization guaranteed by C++11 standard. - * @warning None - * @since 0.1 - * @ingroup base_singleton - */ - static SingleTargetClass& instance() { - static SingleTargetClass target; - return target; - } - - protected: - SimpleSingleton() = default; - ~SimpleSingleton() = default; - - private: - SimpleSingleton(const SimpleSingleton&) = delete; - SimpleSingleton& operator=(const SimpleSingleton&) = delete; - SimpleSingleton(SimpleSingleton&&) = delete; - SimpleSingleton& operator=(SimpleSingleton&&) = delete; -}; - -} // namespace cf diff --git a/base/include/base/singleton/singleton.hpp b/base/include/base/singleton/singleton.hpp deleted file mode 100644 index 01d8c9623..000000000 --- a/base/include/base/singleton/singleton.hpp +++ /dev/null @@ -1,119 +0,0 @@ -/** - * @file base/include/base/singleton/singleton.hpp - * @brief Thread-safe singleton with explicit initialization. - * - * Provides a singleton implementation that requires explicit initialization - * via init() before accessing the instance. Uses std::call_once for - * thread-safe initialization. - * - * @author CFDesktop Team - * @date 2026-02-20 - * @version 0.13.1 - * @since 0.1 - * @ingroup base_singleton - */ - -#pragma once -#include -#include -#include - -namespace cf { - -/** - * @brief Thread-safe singleton with explicit initialization. - * - * Provides a singleton implementation that requires explicit initialization - * via init() before accessing the instance. This pattern allows for - * parameterized construction and explicit control over initialization timing. - * - * @tparam T Type of the singleton instance. - * - * @ingroup base_singleton - * - * @note Thread-safe. Uses std::call_once for initialization. - * - * @warning Accessing instance() before calling init() throws std::logic_error. - * - * @code - * class MyClass { - * public: - * MyClass(int value) : value_(value) {} - * void doSomething() {} - * private: - * int value_; - * }; - * - * using MySingleton = Singleton; - * MySingleton::init(42); - * MySingleton::instance().doSomething(); - * @endcode - */ -template class Singleton { - public: - /** - * @brief Initializes the singleton with the provided arguments. - * - * Constructs the singleton instance once, subsequent calls return the - * existing instance. Thread-safe via std::call_once. - * - * @tparam Args Types of constructor arguments. - * @param[in] args Arguments to forward to T's constructor. - * @return Reference to the initialized singleton instance. - * @throws None - * @note Thread-safe initialization guaranteed by std::call_once. - * @warning Calling init() twice has no effect; the first instance is kept. - * @since N/A - * @ingroup none - */ - template static T& init(Args&&... args) { - std::call_once(flag_, [&] { instance_.reset(new T(std::forward(args)...)); }); - return *instance_; - } - - /** - * @brief Returns the singleton instance. - * - * Requires that init() has been called first. - * - * @return Reference to the singleton instance. - * @throws std::logic_error if init() has not been called. - * @note None - * @warning Must call init() before accessing instance(). - * @since 0.1 - * @ingroup base_singleton - */ - static T& instance() { - if (!instance_) { - throw std::logic_error("Singleton not initialized. Call init() first."); - } - return *instance_; - } - - /** - * @brief Resets the singleton instance. - * - * Destroys the current instance and allows re-initialization via init(). - * - * @throws None - * @note After calling reset(), init() must be called again before - * accessing instance(). - * @warning None - * @since 0.1 - * @ingroup base_singleton - */ - static void reset() { instance_.reset(); } - - Singleton(const Singleton&) = delete; - Singleton& operator=(const Singleton&) = delete; - - protected: - Singleton() = default; - virtual ~Singleton() = default; - - private: - inline static std::unique_ptr instance_ = nullptr; - inline static std::once_flag flag_; -}; - -} // namespace cf diff --git a/base/include/base/span/span.h b/base/include/base/span/span.h deleted file mode 100644 index 0deddd199..000000000 --- a/base/include/base/span/span.h +++ /dev/null @@ -1,312 +0,0 @@ -/** - * @file base/include/base/span/span.h - * @brief Provides a C++17 implementation of std::span (C++20). - * - * Offers a zero-overhead view into contiguous sequences of elements. - * Supports C arrays, std::vector, and std::array as underlying storage. - * - * @author Charliechen114514 - * @date 2026-02-22 - * @version 0.1 - * @since 0.1 - * @ingroup base_containers - */ -#pragma once - -#include -#include -#include - -namespace cf { - -/** - * @brief A non-owning view into a contiguous sequence of elements. - * - * Provides a lightweight, bounds-checked (when using methods) interface - * to access elements without owning the underlying data. Similar to - * std::span from C++20 but implemented for C++17 compatibility. - * - * @tparam T Element type. Must be trivially copyable. - * - * @ingroup base_containers - * - * @code - * std::vector vec = {1, 2, 3, 4, 5}; - * cf::span s = vec; - * int first = s[0]; // Access first element - * @endcode - */ -template class span { - T* data_; - size_t size_; - - public: - /** - * @brief Default constructor. Creates an empty span. - * - * @ingroup base_containers - */ - constexpr span() noexcept : data_(nullptr), size_(0) {} - - /** - * @brief Constructs a span from a pointer and size. - * - * @param[in] data Pointer to the first element. Must be non-null if - * size > 0. - * @param[in] size Number of elements in the sequence. - * - * @ingroup base_containers - */ - constexpr span(T* data, size_t size) noexcept : data_(data), size_(size) {} - - /** - * @brief Constructs a span from a C array. - * - * @tparam N Array size deduced from the input. - * @param[in] arr C array to view. - * - * @ingroup base_containers - */ - template constexpr span(T (&arr)[N]) noexcept : data_(arr), size_(N) {} - - /** - * @brief Constructs a span from a std::vector. - * - * @param[in] vec Vector to view. Does not take ownership. - * - * @ingroup base_containers - */ - constexpr span(std::vector& vec) noexcept : data_(vec.data()), size_(vec.size()) {} - - /** - * @brief Constructs a const span from a const std::vector. - * - * @tparam U Template parameter to enable only for const T. - * @param[in] vec Const vector to view. Does not take ownership. - * - * @ingroup base_containers - */ - template ::value, int> = 0> - constexpr span(const std::vector>& vec) noexcept - : data_(vec.data()), size_(vec.size()) {} - - /** - * @brief Constructs a span from a std::array. - * - * @tparam N Array size. - * @param[in] arr Array to view. - * - * @ingroup base_containers - */ - template constexpr span(std::array& arr) noexcept - : data_(arr.data()), size_(N) {} - - /** - * @brief Constructs a const span from a const std::array. - * - * @tparam N Array size. - * @tparam U Template parameter to enable only for const T. - * @param[in] arr Const array to view. - * - * @ingroup base_containers - */ - template ::value, int> = 0> - constexpr span(const std::array, N>& arr) noexcept - : data_(arr.data()), size_(N) {} - - /** - * @brief Default copy constructor. - * - * @ingroup base_containers - */ - constexpr span(const span& other) noexcept = default; - - /** - * @brief Default copy assignment operator. - * - * @ingroup base_containers - */ - constexpr span& operator=(const span& other) noexcept = default; - - /** - * @brief Returns pointer to the first element. - * - * @return Pointer to the underlying data, or nullptr if empty. - * - * @ingroup base_containers - */ - constexpr T* data() const noexcept { return data_; } - - /** - * @brief Returns the number of elements in the span. - * - * @return Number of elements in the sequence. - * - * @ingroup base_containers - */ - constexpr size_t size() const noexcept { return size_; } - - /** - * @brief Checks if the span is empty. - * - * @return true if size() == 0, false otherwise. - * - * @ingroup base_containers - */ - constexpr bool empty() const noexcept { return size_ == 0; } - - /** - * @brief Accesses element at specified index. - * - * @param[in] index Zero-based element index. - * - * @return Reference to the element at the specified index. - * - * @warning No bounds checking is performed. Caller must ensure - * index < size(). - * - * @ingroup base_containers - */ - constexpr T& operator[](size_t index) const noexcept { return data_[index]; } - - /** - * @brief Returns the first element. - * - * @return Reference to the first element. - * - * @throws None. - * - * @note None. - * - * @warning Undefined behavior if the span is empty. - * - * @since 0.1 - * @ingroup base_containers - */ - constexpr T& front() const noexcept { return data_[0]; } - - /** - * @brief Returns the last element. - * - * @return Reference to the last element. - * - * @throws None. - * - * @note None. - * - * @warning Undefined behavior if the span is empty. - * - * @since 0.1 - * @ingroup base_containers - */ - constexpr T& back() const noexcept { return data_[size_ - 1]; } - - /** - * @brief Returns iterator to the first element. - * - * @return Pointer to the first element (begin iterator). - * - * @throws None. - * - * @note None. - * - * @since 0.1 - * @ingroup base_containers - */ - constexpr T* begin() const noexcept { return data_; } - - /** - * @brief Returns iterator to one past the last element. - * - * @return Pointer to one past the last element (end iterator). - * - * @throws None. - * - * @note None. - * - * @since 0.1 - * @ingroup base_containers - */ - constexpr T* end() const noexcept { return data_ + size_; } - - /** - * @brief Creates a span of the first `count` elements. - * - * @param[in] count Number of elements to include. - * - * @return New span containing the first `count` elements. - * - * @throws None. - * - * @note None. - * - * @warning Undefined behavior if count > size(). - * - * @since 0.1 - * @ingroup base_containers - */ - constexpr span first(size_t count) const noexcept { return span(data_, count); } - - /** - * @brief Creates a span of the last `count` elements. - * - * @param[in] count Number of elements to include. - * - * @return New span containing the last `count` elements. - * - * @throws None. - * - * @note None. - * - * @warning Undefined behavior if count > size(). - * - * @since 0.1 - * @ingroup base_containers - */ - constexpr span last(size_t count) const noexcept { - return span(data_ + size_ - count, count); - } - - /** - * @brief Creates a subspan starting at `offset`. - * - * @param[in] offset Starting element index. - * @param[in] count Number of elements to include. If -1 (default), - * includes all elements from offset to end. - * - * @return New span containing the specified elements. - * - * @throws None. - * - * @note None. - * - * @warning Undefined behavior if offset + count > size(). - * - * @since 0.1 - * @ingroup base_containers - */ - constexpr span subspan(size_t offset, - size_t count = static_cast(-1)) const noexcept { - if (count == static_cast(-1)) { - count = size_ - offset; - } - return span(data_ + offset, count); - } -}; - -// Type deduction guides -/// @cond DeductionGuides -template span(T*, size_t) -> span; - -template span(T (&)[N]) -> span; - -template span(std::vector&) -> span; - -template span(const std::vector&) -> span; - -template span(std::array&) -> span; - -template span(const std::array&) -> span; -/// @endcond - -} // namespace cf diff --git a/base/include/base/weak_ptr/private/weak_ptr_internals.h b/base/include/base/weak_ptr/private/weak_ptr_internals.h deleted file mode 100644 index 2353ee867..000000000 --- a/base/include/base/weak_ptr/private/weak_ptr_internals.h +++ /dev/null @@ -1,101 +0,0 @@ -/** - * @file base/include/base/weak_ptr/private/weak_ptr_internals.h - * @brief Internal implementation details for WeakPtr system. - * - * This header contains the internal implementation details for the WeakPtr - * system. External code should not directly include or depend on this file. - * WeakPtr and WeakPtrFactory share the WeakReferenceFlag to track liveness. - * - * @author N/A - * @date N/A - * @version N/A - * @since N/A - * @ingroup none - */ - -#pragma once - -#include -#include - -namespace cf { -namespace internal { - -/** - * @brief Shared flag tracking WeakPtr liveness state. - * - * The Owner (WeakPtrFactory) and all WeakPtr instances share the same - * WeakReferenceFlag instance. When the owner is destroyed (or explicitly - * calls InvalidateWeakPtrs), the alive_ flag is set to false, causing all - * WeakPtr::Get() calls to return nullptr. - * - * @ingroup none - * - * @note The alive_ flag uses std::atomic for thread-safety. - * Invalidate() and IsAlive() are thread-safe operations. - * - * @warning WeakPtr should only be used on the same thread where it was - * created. The "check + dereference" two-step operation between - * IsAlive() and actual access is not atomic. - * - * @code - * // Internal use only - created by WeakPtrFactory - * auto flag = std::make_shared(); - * if (flag->IsAlive()) { - * // Safe to access - * } - * flag->Invalidate(); // All WeakPtrs now return nullptr - * @endcode - */ -class WeakReferenceFlag { - public: - /// @brief Default constructor: creates a flag in the alive state. - WeakReferenceFlag() = default; - - /// @brief Copy constructor: deleted. Flag is shared via shared_ptr. - WeakReferenceFlag(const WeakReferenceFlag&) = delete; - - /// @brief Copy assignment: deleted. - WeakReferenceFlag& operator=(const WeakReferenceFlag&) = delete; - - /// @brief Move constructor: deleted. - WeakReferenceFlag(WeakReferenceFlag&&) = delete; - - /// @brief Move assignment: deleted. - WeakReferenceFlag& operator=(WeakReferenceFlag&&) = delete; - - /** - * @brief Checks if the owner is still alive. - * - * @return true if the owner is alive, false if invalidated. - * @throws None - * @note Uses memory_order_acquire for proper synchronization. - * @warning None - * @since N/A - * @ingroup none - */ - [[nodiscard]] bool IsAlive() const noexcept { return alive_.load(std::memory_order_acquire); } - - /** - * @brief Marks the flag as invalid. - * - * After calling this method, all existing and future IsAlive() calls - * returns false. - * - * @throws None - * @note Uses memory_order_release for proper synchronization. - * @warning None - * @since N/A - * @ingroup none - */ - void Invalidate() noexcept { alive_.store(false, std::memory_order_release); } - - private: - std::atomic alive_{true}; -}; - -/// @brief Shared pointer type for WeakReferenceFlag. -using WeakReferenceFlagPtr = std::shared_ptr; - -} // namespace internal -} // namespace cf diff --git a/base/include/base/weak_ptr/weak_ptr.h b/base/include/base/weak_ptr/weak_ptr.h deleted file mode 100644 index 67ce3c72c..000000000 --- a/base/include/base/weak_ptr/weak_ptr.h +++ /dev/null @@ -1,413 +0,0 @@ -/** - * @file base/include/base/weak_ptr/weak_ptr.h - * @brief Non-owning weak reference for exclusively owned resources. - * - * Unlike std::weak_ptr, WeakPtr does not participate in reference counting. - * The resource is exclusively owned by an owner (typically holding - * WeakPtrFactory), and WeakPtr acts as a "claim ticket" that becomes - * invalid when the owner disappears. - * - * @author N/A - * @date N/A - * @version N/A - * @since N/A - * @ingroup none - */ - -#pragma once - -#include "private/weak_ptr_internals.h" - -#include -#include - -namespace cf { - -// Forward declaration -template class WeakPtrFactory; - -/** - * @brief Non-owning weak reference for exclusively owned resources. - * - * @details Unlike std::weak_ptr, WeakPtr does not participate in reference - * counting. The resource is exclusively owned by an owner (typically - * holding WeakPtrFactory), and WeakPtr acts as a "claim ticket" that - * becomes invalid when the owner disappears. - * - * @tparam T Type of the referenced object. - * - * @ingroup none - * - * @note Supports covariance: WeakPtr can be implicitly converted - * to WeakPtr. - * - * @warning WeakPtr should only be used on the same thread (sequence) where it - * was created. The "check + dereference" two-step operation is not - * atomic. - * - * @code - * class ThemeManager { - * public: - * ... - * private: - * WeakPtrFactory weak_factory_{this}; // Declare last - * }; - * - * WeakPtr ref = tm.GetWeakPtr(); - * if (ref) { // or ref.IsValid() - * ref->ApplyTheme(); // Safe access - * } - * @endcode - */ -template class WeakPtr { - public: - // ------------------------------------------------------------ - // Construction - // ------------------------------------------------------------ - - /** - * @brief Default constructor: creates an empty weak reference. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - constexpr WeakPtr() noexcept = default; - - /** - * @brief Constructs an empty weak reference from nullptr. - * @param[in] nullptr_t The nullptr literal. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - constexpr WeakPtr(std::nullptr_t) noexcept {} - - /** - * @brief Copy constructor. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - WeakPtr(const WeakPtr&) noexcept = default; - - /** - * @brief Copy assignment operator. - * @param[in] other Source weak pointer. - * @return Reference to this WeakPtr. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - WeakPtr& operator=(const WeakPtr&) noexcept = default; - - /** - * @brief Move constructor. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - WeakPtr(WeakPtr&&) noexcept = default; - - /** - * @brief Move assignment operator. - * @param[in] other Source weak pointer. - * @return Reference to this WeakPtr. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - WeakPtr& operator=(WeakPtr&&) noexcept = default; - - /** - * @brief Covariant copy constructor from WeakPtr to WeakPtr. - * - * @tparam U Derived type that is convertible to T. - * @param[in] other Source weak pointer. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - template >> - WeakPtr(const WeakPtr& other) noexcept : ptr_(other.ptr_), flag_(other.flag_) {} - - /** - * @brief Covariant copy assignment from WeakPtr to WeakPtr. - * - * @tparam U Derived type that is convertible to T. - * @param[in] other Source weak pointer. - * @return Reference to this WeakPtr. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - template >> - WeakPtr& operator=(const WeakPtr& other) noexcept { - ptr_ = other.ptr_; - flag_ = other.flag_; - return *this; - } - - /** - * @brief Destructor: does not affect Owner lifetime. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - ~WeakPtr() = default; - - // ------------------------------------------------------------ - // Access - // ------------------------------------------------------------ - - /** - * @brief Gets the raw pointer if the owner is still alive. - * - * @return Raw pointer to the owned object, or nullptr if owner is gone. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - [[nodiscard]] T* Get() const noexcept { - if (flag_ && flag_->IsAlive()) - return ptr_; - return nullptr; - } - - /** - * @brief Dereferences the weak pointer. - * - * @return Raw pointer to the owned object. - * @throws None (assertion failure if invalid). - * @note Asserts that the WeakPtr is valid before dereferencing. - * @warning Undefined behavior if called on an invalid WeakPtr. - * @since N/A - * @ingroup none - */ - T* operator->() const noexcept { - assert(IsValid() && "Dereferencing an invalid WeakPtr"); - return ptr_; - } - - /** - * @brief Dereferences the weak pointer. - * - * @return Reference to the owned object. - * @throws None (assertion failure if invalid). - * @note Asserts that the WeakPtr is valid before dereferencing. - * @warning Undefined behavior if called on an invalid WeakPtr. - * @since N/A - * @ingroup none - */ - T& operator*() const noexcept { - assert(IsValid() && "Dereferencing an invalid WeakPtr"); - return *ptr_; - } - - // ------------------------------------------------------------ - // State Query - // ------------------------------------------------------------ - - /** - * @brief Checks if the owner is still alive. - * - * @return true if the owner is alive, false otherwise. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - [[nodiscard]] bool IsValid() const noexcept { return Get() != nullptr; } - - /** - * @brief Boolean conversion operator for validity check. - * - * @return true if the owner is alive, false otherwise. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - explicit operator bool() const noexcept { return IsValid(); } - - // ------------------------------------------------------------ - // Reset - // ------------------------------------------------------------ - - /** - * @brief Resets the weak pointer to empty state. - * - * @throws None - * @note After reset, IsValid() returns false. - * @warning None - * @since N/A - * @ingroup none - */ - void Reset() noexcept { - ptr_ = nullptr; - flag_ = nullptr; - } - - // ------------------------------------------------------------ - // Comparison - // ------------------------------------------------------------ - - /** - * @brief Equality comparison with another WeakPtr. - * - * @param[in] other Other WeakPtr to compare with. - * @return true if both weak pointers point to the same object. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - bool operator==(const WeakPtr& other) const noexcept { return Get() == other.Get(); } - - /** - * @brief Inequality comparison with another WeakPtr. - * - * @param[in] other Other WeakPtr to compare with. - * @return true if weak pointers point to different objects. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - bool operator!=(const WeakPtr& other) const noexcept { return !(*this == other); } - - /** - * @brief Equality comparison with nullptr. - * - * @return true if the WeakPtr is invalid (empty). - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - bool operator==(std::nullptr_t) const noexcept { return !IsValid(); } - - /** - * @brief Inequality comparison with nullptr. - * - * @return true if the WeakPtr is valid. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - bool operator!=(std::nullptr_t) const noexcept { return IsValid(); } - - // ------------------------------------------------------------ - // Dynamic Cast (Base -> Derived conversion) - // ------------------------------------------------------------ - - /** - * @brief Dynamically converts WeakPtr to WeakPtr. - * - * @details Performs a dynamic_cast on the underlying pointer. - * Returns a valid WeakPtr if the cast succeeds, - * otherwise returns an invalid WeakPtr. - * - * @tparam Source Source type (base class). - * @param[in] other WeakPtr to the source object. - * - * @return WeakPtr pointing to the same object if - * the object is of type Derived, otherwise invalid. - * - * @throws None - * @note The returned WeakPtr shares the same weak reference - * flag as the source. - * @warning Always check the returned WeakPtr's validity before use. - * @since N/A - * @ingroup none - * - * @code - * WeakPtr basePtr = ...; - * auto derivedPtr = WeakPtr::DynamicCast(basePtr); - * if (derivedPtr) { - * derivedPtr->DerivedMethod(); - * } - * @endcode - */ - template static WeakPtr DynamicCast(const WeakPtr& other) noexcept { - if (!other.flag_ || !other.flag_->IsAlive()) { - return WeakPtr(); - } - T* casted = dynamic_cast(other.ptr_); - if (casted) { - return WeakPtr(casted, other.flag_); - } - return WeakPtr(); - } - - private: - // Only WeakPtrFactory can construct valid WeakPtr instances - - /** - * @brief Private constructor for creating valid WeakPtr instances. - * - * @param[in] ptr Raw pointer to the owned object. - * @param[in] flag Weak reference flag for lifetime tracking. - * @throws None - * @note Only accessible by WeakPtrFactory. - * @warning None - * @since N/A - * @ingroup none - */ - explicit WeakPtr(T* ptr, internal::WeakReferenceFlagPtr flag) noexcept - : ptr_(ptr), flag_(std::move(flag)) {} - - /// Raw pointer to the owned object. Ownership: observer; may be nullptr. - T* ptr_ = nullptr; - - /// Weak reference flag for lifetime tracking. Ownership: shared; may be nullptr. - internal::WeakReferenceFlagPtr flag_ = nullptr; - - /** - * @brief Friend declaration for covariance support. - * - * @tparam U Type parameter for the befriended WeakPtr. - * - * @since N/A - * @ingroup none - */ - template friend class WeakPtr; - - /** - * @brief Friend declaration for factory access. - * - * @tparam U Type parameter for the befriended WeakPtrFactory. - * - * @since N/A - * @ingroup none - */ - template friend class WeakPtrFactory; -}; - -} // namespace cf diff --git a/base/include/base/weak_ptr/weak_ptr_factory.h b/base/include/base/weak_ptr/weak_ptr_factory.h deleted file mode 100644 index da306c89b..000000000 --- a/base/include/base/weak_ptr/weak_ptr_factory.h +++ /dev/null @@ -1,157 +0,0 @@ -/** - * @file base/include/base/weak_ptr/weak_ptr_factory.h - * @brief Factory for producing WeakPtr instances for an exclusive owner. - * - * WeakPtrFactory should be declared as a member variable of the owner class, - * as the last member to ensure it is destroyed first (C++ destroys members - * in reverse declaration order). - * - * @author N/A - * @date N/A - * @version N/A - * @since N/A - * @ingroup none - */ - -#pragma once - -#include "private/weak_ptr_internals.h" -#include "weak_ptr.h" - -#include -#include - -namespace cf { - -/** - * @brief Factory for producing WeakPtr instances for an exclusive owner. - * - * WeakPtrFactory should be declared as a member variable of the owner class, - * as the last member to ensure it is destroyed first (C++ destroys members - * in reverse declaration order). - * - * @tparam T Type of the owner class. - * - * @ingroup none - * - * @note Not copyable or movable. GetWeakPtr() should be called on the same - * thread where the WeakPtr is used. - * - * @warning Declare WeakPtrFactory as the LAST member of the owner class so it - * is destroyed first, invalidating all weak pointers before any other - * members are destroyed. - * - * @code - * class Foo { - * public: - * void doSomething() {} - * - * private: - * SomeResource resource_; // Destroyed first - * WeakPtrFactory weak_factory_{this}; // Destroyed last - * }; - * - * Foo foo; - * WeakPtr ref = foo.weak_factory_.GetWeakPtr(); - * if (ref) { - * ref->doSomething(); - * } - * @endcode - */ -template class WeakPtrFactory { - public: - // ------------------------------------------------------------ - // Construction / Destruction - // ------------------------------------------------------------ - - /** - * @brief Constructs a WeakPtrFactory for the given owner. - * - * @param[in] owner Raw pointer to the owner object. Must not be nullptr. - * @throws None (assertion failure if owner is nullptr). - * @note The factory does not take ownership; the owner must - * outlive the factory or explicitly manage lifetime. - * @warning None - * @since N/A - * @ingroup none - */ - explicit WeakPtrFactory(T* owner) noexcept - : owner_(owner), flag_(std::make_shared()) { - assert(owner_ && "WeakPtrFactory owner must not be null"); - } - - /// @brief Destructor: automatically invalidates all issued WeakPtr instances. - ~WeakPtrFactory() { InvalidateWeakPtrs(); } - - /// @brief Copy constructor: deleted. - WeakPtrFactory(const WeakPtrFactory&) = delete; - - /// @brief Copy assignment: deleted. - WeakPtrFactory& operator=(const WeakPtrFactory&) = delete; - - /// @brief Move constructor: deleted. - WeakPtrFactory(WeakPtrFactory&&) = delete; - - /// @brief Move assignment: deleted. - WeakPtrFactory& operator=(WeakPtrFactory&&) = delete; - - // ------------------------------------------------------------ - // Core Interface - // ------------------------------------------------------------ - - /** - * @brief Creates a WeakPtr pointing to the owner. - * - * @return WeakPtr that can be used to safely access the owner. - * @throws None (assertion failure if called after InvalidateWeakPtrs()). - * @note The returned WeakPtr does not extend the owner's lifetime. - * @warning Do not call after InvalidateWeakPtrs() has been called. - * @since N/A - * @ingroup none - */ - [[nodiscard]] WeakPtr GetWeakPtr() const noexcept { - assert(flag_->IsAlive() && "GetWeakPtr() called after InvalidateWeakPtrs()"); - return WeakPtr(owner_, flag_); - } - - /** - * @brief Invalidates all previously issued WeakPtr instances. - * - * After calling this method, all existing WeakPtr instances return - * nullptr from Get(). The owner remains alive and new WeakPtr instances - * can be created via GetWeakPtr(). - * - * @throws None - * @note New WeakPtr instances can be created after invalidation. - * @warning None - * @since N/A - * @ingroup none - */ - void InvalidateWeakPtrs() noexcept { - if (flag_->IsAlive()) { - flag_->Invalidate(); - // Allocate a new flag to support continued use after invalidation - flag_ = std::make_shared(); - } - } - - /** - * @brief Checks if any WeakPtr instances are currently held externally. - * - * @return true if external WeakPtr instances exist, false otherwise. - * @throws None - * @note Uses the shared_ptr use_count to determine if any WeakPtr - * instances are holding the flag. - * @warning None - * @since N/A - * @ingroup none - */ - [[nodiscard]] bool HasWeakPtrs() const noexcept { return flag_.use_count() > 1; } - - private: - T* owner_; - // mutable allows GetWeakPtr() to be const - mutable internal::WeakReferenceFlagPtr flag_; -}; - -} // namespace cf diff --git a/base/include/base/windows/co_helper.hpp b/base/include/base/windows/co_helper.hpp index b6e5438d0..2ba4508bf 100644 --- a/base/include/base/windows/co_helper.hpp +++ b/base/include/base/windows/co_helper.hpp @@ -12,8 +12,8 @@ * @ingroup base_windows */ #pragma once -#include "../expected/expected.hpp" -#include "../scope_guard/scope_guard.hpp" +#include "aex/expected/expected.hpp" +#include "aex/scope_guard/scope_guard.hpp" #include "common.h" #include @@ -36,7 +36,7 @@ namespace cf { template class COMHelper { public: /// Type alias for COM operation function. - using ContextFunction = std::function()>; + using ContextFunction = std::function()>; /** * @brief Executes a COM operation with single-threaded initialization. @@ -55,22 +55,22 @@ template class COMHelper { * * @ingroup base_windows */ - static cf::expected + static aex::expected RunComInterfacesOnce(ContextFunction f, DWORD coinitFlag = COINIT_APARTMENTTHREADED) { HRESULT hr = ::CoInitializeEx(nullptr, coinitFlag); if (FAILED(hr)) { - return cf::unexpected(static_cast(hr)); + return aex::unexpected(static_cast(hr)); } - cf::ScopeGuard guard([]() { ::CoUninitialize(); }); + aex::ScopeGuard guard([]() { ::CoUninitialize(); }); hr = CoInitializeSecurity(nullptr, -1, nullptr, nullptr, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_NONE, nullptr); if (FAILED(hr)) { - return cf::unexpected(static_cast(hr)); + return aex::unexpected(static_cast(hr)); } - cf::expected result = f(); + aex::expected result = f(); return result; } @@ -88,7 +88,7 @@ template class COMHelper { * * @ingroup base_windows */ - static cf::expected RunComInterfacesMTA(ContextFunction f) { + static aex::expected RunComInterfacesMTA(ContextFunction f) { return RunComInterfacesOnce(std::move(f), COINIT_MULTITHREADED); } }; diff --git a/base/include/base/windows/common.h b/base/include/base/windows/common.h index 952511d39..520c3929f 100644 --- a/base/include/base/windows/common.h +++ b/base/include/base/windows/common.h @@ -9,7 +9,7 @@ * @ingroup base_windows */ #pragma once -#include "../macros.h" +#include "aex/macros.h" // Ensure this file is only included on Windows platforms #ifndef CFDESKTOP_OS_WINDOWS diff --git a/base/include/system/cpu/cfcpu.h b/base/include/system/cpu/cfcpu.h index 94fc12d74..ff33a64bb 100644 --- a/base/include/system/cpu/cfcpu.h +++ b/base/include/system/cpu/cfcpu.h @@ -12,7 +12,7 @@ * @ingroup system_cpu */ #pragma once -#include "base/expected/expected.hpp" +#include "aex/expected/expected.hpp" #include "base/export.h" #include @@ -53,7 +53,7 @@ struct CPUInfoView { * @param[in] force_refresh If true, forces re-querying CPU information. * Default is false. * - * @return `expected` containing CPU + * @return `aex::expected` containing CPU * information on success, or an error type on failure. * * @throws None (error reporting via expected type). @@ -68,6 +68,6 @@ struct CPUInfoView { * @since 0.1 * @ingroup system_cpu */ -CF_BASE_EXPORT expected getCPUInfo(bool force_refresh = false); +CF_BASE_EXPORT aex::expected getCPUInfo(bool force_refresh = false); } // namespace cf diff --git a/base/include/system/cpu/cfcpu_bonus.h b/base/include/system/cpu/cfcpu_bonus.h index 2b02fe048..3a15d870f 100644 --- a/base/include/system/cpu/cfcpu_bonus.h +++ b/base/include/system/cpu/cfcpu_bonus.h @@ -14,9 +14,9 @@ */ #pragma once -#include "base/expected/expected.hpp" +#include "aex/expected/expected.hpp" +#include "aex/span/span.h" #include "base/export.h" -#include "base/span/span.h" #include #include #include @@ -44,10 +44,10 @@ enum class CPUBonusInfoViewError { */ struct CPUBonusInfoView { /// CPU feature flags (e.g., "neon", "aes", "avx2"). - cf::span features; + aex::span features; /// Cache sizes in kilobytes. Order: L1, L2, L3 (if available). - cf::span cache_size; + aex::span cache_size; /// Indicates whether CPU uses big.LITTLE or DynamIQ architecture. bool has_big_little = false; @@ -75,7 +75,7 @@ struct CPUBonusInfoView { * @param[in] force_refresh If true, forces re-querying CPU information. * Default is false. * - * @return `expected` + * @return `aex::expected` * containing extended CPU information on success, or an * error type on failure. * @@ -92,7 +92,7 @@ struct CPUBonusInfoView { * @since 0.1 * @ingroup system_cpu */ -CF_BASE_EXPORT expected +CF_BASE_EXPORT aex::expected getCPUBonusInfo(bool force_refresh = false); } // namespace cf diff --git a/base/include/system/cpu/cfcpu_profile.h b/base/include/system/cpu/cfcpu_profile.h index 2921d6009..897e625c8 100644 --- a/base/include/system/cpu/cfcpu_profile.h +++ b/base/include/system/cpu/cfcpu_profile.h @@ -12,7 +12,7 @@ * @ingroup system_cpu */ #pragma once -#include "base/expected/expected.hpp" +#include "aex/expected/expected.hpp" #include "base/export.h" #include @@ -52,7 +52,7 @@ struct CPUProfileInfo { * function performs a real-time query each time it is called and * does not cache results. * - * @return `expected` containing + * @return `aex::expected` containing * CPU performance data on success, or an error type on * failure. * @@ -67,6 +67,6 @@ struct CPUProfileInfo { * @since 0.1 * @ingroup system_cpu */ -CF_BASE_EXPORT expected getCPUProfileInfo(); +CF_BASE_EXPORT aex::expected getCPUProfileInfo(); } // namespace cf diff --git a/base/include/system/gpu/gpu.h b/base/include/system/gpu/gpu.h index bac7f41f3..31ae273c6 100644 --- a/base/include/system/gpu/gpu.h +++ b/base/include/system/gpu/gpu.h @@ -13,7 +13,7 @@ */ #pragma once -#include "base/expected/expected.hpp" +#include "aex/expected/expected.hpp" #include "base/export.h" #include #include @@ -129,6 +129,6 @@ struct GpuDisplayInfo { * @since N/A * @ingroup none */ -cf::expected CF_BASE_EXPORT getGpuDisplayInfo() noexcept; +aex::expected CF_BASE_EXPORT getGpuDisplayInfo() noexcept; } // namespace cf diff --git a/base/include/system/hardware_tier/hardware_tier.h b/base/include/system/hardware_tier/hardware_tier.h index 1d052bb6e..4c4e5ef52 100644 --- a/base/include/system/hardware_tier/hardware_tier.h +++ b/base/include/system/hardware_tier/hardware_tier.h @@ -14,7 +14,7 @@ */ #pragma once -#include "base/expected/expected.hpp" +#include "aex/expected/expected.hpp" #include "base/export.h" #include "system/hardware_tier/hardware_tier_data.h" @@ -137,7 +137,7 @@ CF_BASE_EXPORT void clearDeviceConfigOverride(); * @since 0.19 * @ingroup system_hardware_tier */ -CF_BASE_EXPORT expected +CF_BASE_EXPORT aex::expected assessHardware(bool force_refresh = false); /** @@ -151,6 +151,7 @@ assessHardware(bool force_refresh = false); * @since 0.19 * @ingroup system_hardware_tier */ -CF_BASE_EXPORT expected getHardwareTierCapabilities(); +CF_BASE_EXPORT aex::expected +getHardwareTierCapabilities(); } // namespace cf diff --git a/base/include/system/network/network.h b/base/include/system/network/network.h index 3be414187..7d22fd5e8 100644 --- a/base/include/system/network/network.h +++ b/base/include/system/network/network.h @@ -12,7 +12,7 @@ * @ingroup none */ #pragma once -#include "base/expected/expected.hpp" +#include "aex/expected/expected.hpp" #include "base/export.h" #include #include @@ -377,7 +377,7 @@ enum class NetworkQueryError { OK }; * @since N/A * @ingroup none */ -cf::expected CF_BASE_EXPORT getNetworkInfo() noexcept; +aex::expected CF_BASE_EXPORT getNetworkInfo() noexcept; /** * @brief Returns the human-readable name for an interface type. diff --git a/base/system/cpu/CMakeLists.txt b/base/system/cpu/CMakeLists.txt index 9f46a60c3..d992cee7a 100644 --- a/base/system/cpu/CMakeLists.txt +++ b/base/system/cpu/CMakeLists.txt @@ -7,6 +7,8 @@ set_target_properties(cfbase_cpu PROPERTIES ) # Define export macro since this will be linked into cfbase shared library target_compile_definitions(cfbase_cpu PRIVATE CFBASE_EXPORTS) +# Foundation utils (aex) + base/include platform headers +target_link_libraries(cfbase_cpu PRIVATE cfbase_headers) target_include_directories(cfbase_cpu PUBLIC $ diff --git a/base/system/cpu/cfcpu.cpp b/base/system/cpu/cfcpu.cpp index 20021784a..d6e1dc7e0 100644 --- a/base/system/cpu/cfcpu.cpp +++ b/base/system/cpu/cfcpu.cpp @@ -14,8 +14,8 @@ */ #include "system/cpu/cfcpu.h" -#include "base/helpers/once_init.hpp" -#include "base/macro/system_judge.h" +#include "aex/helpers/once_init.hpp" +#include "aex/macro/system_judge.h" #include "private/cpu_host.h" #ifdef CFDESKTOP_OS_WINDOWS @@ -25,7 +25,7 @@ #endif namespace { -class CPUHostInfoIniter : public cf::CallOnceInit { +class CPUHostInfoIniter : public aex::CallOnceInit { public: cf::CPUInfoErrorType error() const { return error_code; } @@ -50,14 +50,14 @@ static CPUHostInfoIniter cpu_initer; } // namespace namespace cf { -expected getCPUInfo(bool force_refresh) { +aex::expected getCPUInfo(bool force_refresh) { if (force_refresh) { cpu_initer.force_reinit(); } auto& result = cpu_initer.get_resources(); if (cpu_initer.error() != cf::CPUInfoErrorType::CPU_QUERY_NOERROR) { - return cf::unexpected(cpu_initer.error()); + return aex::unexpected(cpu_initer.error()); } // Convert CPUInfoHost to CPUInfoView diff --git a/base/system/cpu/cfcpu_bonus.cpp b/base/system/cpu/cfcpu_bonus.cpp index 7b33cf9ad..d5201b5eb 100644 --- a/base/system/cpu/cfcpu_bonus.cpp +++ b/base/system/cpu/cfcpu_bonus.cpp @@ -1,7 +1,7 @@ #include "system/cpu/cfcpu_bonus.h" -#include "base/helpers/once_init.hpp" -#include "base/macro/system_judge.h" -#include "base/span/span.h" +#include "aex/helpers/once_init.hpp" +#include "aex/macro/system_judge.h" +#include "aex/span/span.h" #include "private/cpu_host.h" #include #include @@ -14,7 +14,7 @@ #endif namespace { -class CPUBonusInfoInit : public cf::CallOnceInit { +class CPUBonusInfoInit : public aex::CallOnceInit { public: cf::CPUBonusInfoViewError error() const { return error_code; } @@ -35,35 +35,35 @@ class CPUBonusInfoInit : public cf::CallOnceInit { static CPUBonusInfoInit bonusInfoInit; -cf::span toView(const std::vector& src_array) { +aex::span toView(const std::vector& src_array) { static std::vector views; views.clear(); views.reserve(src_array.size()); for (const auto& item : src_array) { views.push_back(item); } - return cf::span(views.data(), views.size()); + return aex::span(views.data(), views.size()); } } // namespace namespace cf { -expected getCPUBonusInfo(bool force_refresh) { +aex::expected getCPUBonusInfo(bool force_refresh) { if (force_refresh) { bonusInfoInit.force_reinit(); } auto& result = bonusInfoInit.get_resources(); if (bonusInfoInit.error() != cf::CPUBonusInfoViewError::NoError) { - return cf::unexpected(bonusInfoInit.error()); + return aex::unexpected(bonusInfoInit.error()); } // Convert CPUBonusInfoHost to CPUBonusInfoView CPUBonusInfoView view; view.features = toView(result.features); - view.cache_size = cf::span(result.cache_size.data(), result.cache_size.size()); + view.cache_size = aex::span(result.cache_size.data(), result.cache_size.size()); view.has_big_little = result.has_big_little; view.big_core_count = result.big_core_count; @@ -74,4 +74,4 @@ expected getCPUBonusInfo(bool force_ref return view; } -} // namespace cf \ No newline at end of file +} // namespace cf diff --git a/base/system/cpu/cfcpu_profile.cpp b/base/system/cpu/cfcpu_profile.cpp index a0a8adc9e..3e094a46d 100644 --- a/base/system/cpu/cfcpu_profile.cpp +++ b/base/system/cpu/cfcpu_profile.cpp @@ -1,5 +1,5 @@ #include "system/cpu/cfcpu_profile.h" -#include "base/macro/system_judge.h" +#include "aex/macro/system_judge.h" #ifdef CFDESKTOP_OS_WINDOWS # include "private/win_impl/cpu_profile.h" @@ -7,7 +7,7 @@ # include "private/linux_impl/cpu_profile.h" #endif namespace cf { -expected getCPUProfileInfo() { +aex::expected getCPUProfileInfo() { return query_cpu_profile_info(); } diff --git a/base/system/cpu/private/linux_impl/cpu_bonus.cpp b/base/system/cpu/private/linux_impl/cpu_bonus.cpp index 5103ad260..f38612cdb 100644 --- a/base/system/cpu/private/linux_impl/cpu_bonus.cpp +++ b/base/system/cpu/private/linux_impl/cpu_bonus.cpp @@ -195,7 +195,7 @@ std::optional readCpuTemperature() noexcept { } // namespace -cf::expected query_cpu_bonus_info(cf::CPUBonusInfoHost& bonus) { +aex::expected query_cpu_bonus_info(cf::CPUBonusInfoHost& bonus) { // Get CPU features query_cpu_features(bonus.features); diff --git a/base/system/cpu/private/linux_impl/cpu_bonus.h b/base/system/cpu/private/linux_impl/cpu_bonus.h index fc34cd6df..5973c8c82 100644 --- a/base/system/cpu/private/linux_impl/cpu_bonus.h +++ b/base/system/cpu/private/linux_impl/cpu_bonus.h @@ -11,7 +11,7 @@ #pragma once #include "../cpu_host.h" -#include "base/expected/expected.hpp" +#include "aex/expected/expected.hpp" #include "system/cpu/cfcpu_bonus.h" -cf::expected query_cpu_bonus_info(cf::CPUBonusInfoHost& bonus); +aex::expected query_cpu_bonus_info(cf::CPUBonusInfoHost& bonus); diff --git a/base/system/cpu/private/linux_impl/cpu_features.h b/base/system/cpu/private/linux_impl/cpu_features.h index e002e79e3..30fb1094d 100644 --- a/base/system/cpu/private/linux_impl/cpu_features.h +++ b/base/system/cpu/private/linux_impl/cpu_features.h @@ -27,4 +27,4 @@ * @since 0.1 * @ingroup system_cpu */ -void query_cpu_features(std::vector& feats); \ No newline at end of file +void query_cpu_features(std::vector& feats); diff --git a/base/system/cpu/private/linux_impl/cpu_info.cpp b/base/system/cpu/private/linux_impl/cpu_info.cpp index cc987b1da..4c16fb739 100644 --- a/base/system/cpu/private/linux_impl/cpu_info.cpp +++ b/base/system/cpu/private/linux_impl/cpu_info.cpp @@ -16,10 +16,10 @@ #include #include -cf::expected query_cpu_basic_info(cf::CPUInfoHost& hostInfo) { +aex::expected query_cpu_basic_info(cf::CPUInfoHost& hostInfo) { std::ifstream cpuinfo("/proc/cpuinfo"); if (!cpuinfo.is_open()) { - return cf::unexpected(cf::CPUInfoErrorType::CPU_QUERY_GENERAL_FAILED); + return aex::unexpected(cf::CPUInfoErrorType::CPU_QUERY_GENERAL_FAILED); } std::string line; diff --git a/base/system/cpu/private/linux_impl/cpu_info.h b/base/system/cpu/private/linux_impl/cpu_info.h index dab4ee711..3a0c6e981 100644 --- a/base/system/cpu/private/linux_impl/cpu_info.h +++ b/base/system/cpu/private/linux_impl/cpu_info.h @@ -11,13 +11,13 @@ #pragma once #include "../cpu_host.h" -#include "base/expected/expected.hpp" +#include "aex/expected/expected.hpp" #include "system/cpu/cfcpu.h" /** * @brief Internal Windows Query for basic CPU information * * @param hostInfo Output parameter containing model, manufacturer, and architecture - * @return cf::expected + * @return aex::expected */ -cf::expected query_cpu_basic_info(cf::CPUInfoHost& hostInfo); +aex::expected query_cpu_basic_info(cf::CPUInfoHost& hostInfo); diff --git a/base/system/cpu/private/linux_impl/cpu_profile.cpp b/base/system/cpu/private/linux_impl/cpu_profile.cpp index b06a2974f..e721c64be 100644 --- a/base/system/cpu/private/linux_impl/cpu_profile.cpp +++ b/base/system/cpu/private/linux_impl/cpu_profile.cpp @@ -82,7 +82,7 @@ float getCpuUsage() noexcept { } // namespace -cf::expected query_cpu_profile_info() { +aex::expected query_cpu_profile_info() { cf::CPUProfileInfo profile_info{}; // Get logical and physical core counts from /proc/cpuinfo diff --git a/base/system/cpu/private/linux_impl/cpu_profile.h b/base/system/cpu/private/linux_impl/cpu_profile.h index b3cd39298..666409e39 100644 --- a/base/system/cpu/private/linux_impl/cpu_profile.h +++ b/base/system/cpu/private/linux_impl/cpu_profile.h @@ -10,12 +10,12 @@ */ #pragma once -#include "base/expected/expected.hpp" +#include "aex/expected/expected.hpp" #include "system/cpu/cfcpu_profile.h" /** * @brief Internal Windows Query for CPU profile information * - * @return cf::expected + * @return aex::expected */ -cf::expected query_cpu_profile_info(); \ No newline at end of file +aex::expected query_cpu_profile_info(); diff --git a/base/system/cpu/private/win_impl/cpu_bonus.cpp b/base/system/cpu/private/win_impl/cpu_bonus.cpp index c3aba20b6..4a62c8593 100644 --- a/base/system/cpu/private/win_impl/cpu_bonus.cpp +++ b/base/system/cpu/private/win_impl/cpu_bonus.cpp @@ -1,13 +1,13 @@ #include "cpu_bonus.h" #include "cpu_features.h" #include "cpu_host.h" -#include #include +#include // MinGW may not have EfficiencyClass in PROCESSOR_RELATIONSHIP (Windows 10 1903+) #ifndef PROCESSOR_RELATIONSHIP_EFFICIENCY_CLASS // Offset of EfficiencyClass in PROCESSOR_RELATIONSHIP structure -#define PROCESSOR_RELATIONSHIP_EFFICIENCY_CLASS 20 +# define PROCESSOR_RELATIONSHIP_EFFICIENCY_CLASS 20 #endif namespace { @@ -16,7 +16,8 @@ namespace { inline BYTE GetProcessorEfficiencyClass(const PROCESSOR_RELATIONSHIP* proc) { // Access EfficiencyClass by offset since MinGW may not have the field // The EfficiencyClass is at offset 20 (after GroupCount and GroupMask array) - return *reinterpret_cast(reinterpret_cast(proc) + PROCESSOR_RELATIONSHIP_EFFICIENCY_CLASS); + return *reinterpret_cast(reinterpret_cast(proc) + + PROCESSOR_RELATIONSHIP_EFFICIENCY_CLASS); } void filledCache(cf::CPUBonusInfoHost& host) { @@ -75,7 +76,7 @@ void getCPUEffecientClass(cf::CPUBonusInfoHost& host) { } // namespace -cf::expected query_cpu_bonus_info(cf::CPUBonusInfoHost& bonus) { +aex::expected query_cpu_bonus_info(cf::CPUBonusInfoHost& bonus) { query_cpu_features(bonus.features); filledCache(bonus); bonus.temperature = {}; // Not accessible in Windows diff --git a/base/system/cpu/private/win_impl/cpu_bonus.h b/base/system/cpu/private/win_impl/cpu_bonus.h index fc34cd6df..5973c8c82 100644 --- a/base/system/cpu/private/win_impl/cpu_bonus.h +++ b/base/system/cpu/private/win_impl/cpu_bonus.h @@ -11,7 +11,7 @@ #pragma once #include "../cpu_host.h" -#include "base/expected/expected.hpp" +#include "aex/expected/expected.hpp" #include "system/cpu/cfcpu_bonus.h" -cf::expected query_cpu_bonus_info(cf::CPUBonusInfoHost& bonus); +aex::expected query_cpu_bonus_info(cf::CPUBonusInfoHost& bonus); diff --git a/base/system/cpu/private/win_impl/cpu_features.cpp b/base/system/cpu/private/win_impl/cpu_features.cpp index c08c3c6f4..4dd97df73 100644 --- a/base/system/cpu/private/win_impl/cpu_features.cpp +++ b/base/system/cpu/private/win_impl/cpu_features.cpp @@ -34,7 +34,7 @@ void addFeatureIfSupported(bool condition, const char* name, std::vector + * @return aex::expected */ void query_cpu_features(std::vector& feats) { int cpuInfo[4] = {}; diff --git a/base/system/cpu/private/win_impl/cpu_features.h b/base/system/cpu/private/win_impl/cpu_features.h index 2aa0751da..c490d43bd 100644 --- a/base/system/cpu/private/win_impl/cpu_features.h +++ b/base/system/cpu/private/win_impl/cpu_features.h @@ -27,4 +27,4 @@ * @since 0.1 * @ingroup system_cpu */ -void query_cpu_features(std::vector& feats); \ No newline at end of file +void query_cpu_features(std::vector& feats); diff --git a/base/system/cpu/private/win_impl/cpu_info.cpp b/base/system/cpu/private/win_impl/cpu_info.cpp index b14e73fdf..d0c3ce4f4 100644 --- a/base/system/cpu/private/win_impl/cpu_info.cpp +++ b/base/system/cpu/private/win_impl/cpu_info.cpp @@ -8,7 +8,7 @@ * @copyright Copyright (c) 2026 * */ -#include "base/scope_guard/scope_guard.hpp" +#include "aex/scope_guard/scope_guard.hpp" #include "base/windows/co_helper.hpp" #include "system/cpu/cfcpu.h" #include "system/cpu/private/cpu_host.h" @@ -22,7 +22,7 @@ namespace { using namespace cf; // Helper function to query a single WMI property -cf::expected +aex::expected queryWMIProperty(IWbemServices* pSvc, const std::wstring& className, const std::wstring& property) { IEnumWbemClassObject* pEnumerator = nullptr; @@ -35,11 +35,11 @@ queryWMIProperty(IWbemServices* pSvc, const std::wstring& className, const std:: &pEnumerator); if (FAILED(hres)) { - return cf::unexpected(CPUInfoErrorType::CPU_QUERY_GENERAL_FAILED); + return aex::unexpected(CPUInfoErrorType::CPU_QUERY_GENERAL_FAILED); } // Ensure enumerator is released - cf::ScopeGuard enumeratorGuard([&pEnumerator]() { + aex::ScopeGuard enumeratorGuard([&pEnumerator]() { if (pEnumerator) { pEnumerator->Release(); } @@ -50,10 +50,10 @@ queryWMIProperty(IWbemServices* pSvc, const std::wstring& className, const std:: hres = pEnumerator->Next(static_cast(WBEM_INFINITE), 1, &pclsObj, &uReturn); if (uReturn == 0 || FAILED(hres)) { - return cf::unexpected(CPUInfoErrorType::CPU_QUERY_GENERAL_FAILED); + return aex::unexpected(CPUInfoErrorType::CPU_QUERY_GENERAL_FAILED); } - cf::ScopeGuard classObjGuard([&pclsObj]() { + aex::ScopeGuard classObjGuard([&pclsObj]() { if (pclsObj) { pclsObj->Release(); } @@ -64,7 +64,7 @@ queryWMIProperty(IWbemServices* pSvc, const std::wstring& className, const std:: hres = pclsObj->Get(property.c_str(), 0, &vtProp, 0, 0); - cf::ScopeGuard variantGuard([&vtProp]() { VariantClear(&vtProp); }); + aex::ScopeGuard variantGuard([&vtProp]() { VariantClear(&vtProp); }); if (SUCCEEDED(hres)) { // Handle different variant types @@ -87,7 +87,7 @@ queryWMIProperty(IWbemServices* pSvc, const std::wstring& className, const std:: return result; } - return cf::unexpected(CPUInfoErrorType::CPU_QUERY_GENERAL_FAILED); + return aex::unexpected(CPUInfoErrorType::CPU_QUERY_GENERAL_FAILED); } // Convert Architecture value to string @@ -116,8 +116,8 @@ std::string architectureToString(UINT16 archValue) { } // namespace -cf::expected query_cpu_basic_info(cf::CPUInfoHost& hostInfo) { - using CpuInfoQueryExpected = cf::expected; +aex::expected query_cpu_basic_info(cf::CPUInfoHost& hostInfo) { + using CpuInfoQueryExpected = aex::expected; return cf::COMHelper::RunComInterfacesMTA( [&hostInfo]() -> CpuInfoQueryExpected { @@ -127,10 +127,10 @@ cf::expected query_cpu_basic_info(cf::CPUInfoHost& hostI IID_IWbemLocator, reinterpret_cast(&pLoc)); if (FAILED(hres)) { - return cf::unexpected(CPUInfoErrorType::CPU_QUERY_GENERAL_FAILED); + return aex::unexpected(CPUInfoErrorType::CPU_QUERY_GENERAL_FAILED); } - cf::ScopeGuard locGuard([&pLoc]() { + aex::ScopeGuard locGuard([&pLoc]() { if (pLoc) { pLoc->Release(); } @@ -142,10 +142,10 @@ cf::expected query_cpu_basic_info(cf::CPUInfoHost& hostI pLoc->ConnectServer(_bstr_t(L"ROOT\\CIMV2"), nullptr, nullptr, 0, 0, 0, 0, &pSvc); if (FAILED(hres)) { - return cf::unexpected(CPUInfoErrorType::CPU_QUERY_GENERAL_FAILED); + return aex::unexpected(CPUInfoErrorType::CPU_QUERY_GENERAL_FAILED); } - cf::ScopeGuard svcGuard([&pSvc]() { + aex::ScopeGuard svcGuard([&pSvc]() { if (pSvc) { pSvc->Release(); } @@ -157,7 +157,7 @@ cf::expected query_cpu_basic_info(cf::CPUInfoHost& hostI EOAC_NONE); if (FAILED(hres)) { - return cf::unexpected(CPUInfoErrorType::CPU_QUERY_GENERAL_FAILED); + return aex::unexpected(CPUInfoErrorType::CPU_QUERY_GENERAL_FAILED); } // Query CPU information diff --git a/base/system/cpu/private/win_impl/cpu_info.h b/base/system/cpu/private/win_impl/cpu_info.h index dab4ee711..3a0c6e981 100644 --- a/base/system/cpu/private/win_impl/cpu_info.h +++ b/base/system/cpu/private/win_impl/cpu_info.h @@ -11,13 +11,13 @@ #pragma once #include "../cpu_host.h" -#include "base/expected/expected.hpp" +#include "aex/expected/expected.hpp" #include "system/cpu/cfcpu.h" /** * @brief Internal Windows Query for basic CPU information * * @param hostInfo Output parameter containing model, manufacturer, and architecture - * @return cf::expected + * @return aex::expected */ -cf::expected query_cpu_basic_info(cf::CPUInfoHost& hostInfo); +aex::expected query_cpu_basic_info(cf::CPUInfoHost& hostInfo); diff --git a/base/system/cpu/private/win_impl/cpu_profile.cpp b/base/system/cpu/private/win_impl/cpu_profile.cpp index a21ad9d29..cc5743ec7 100644 --- a/base/system/cpu/private/win_impl/cpu_profile.cpp +++ b/base/system/cpu/private/win_impl/cpu_profile.cpp @@ -52,7 +52,7 @@ float getCpuUsage() { } // namespace -cf::expected query_cpu_profile_info() { +aex::expected query_cpu_profile_info() { cf::CPUProfileInfo profile_info{}; // logical cnt diff --git a/base/system/cpu/private/win_impl/cpu_profile.h b/base/system/cpu/private/win_impl/cpu_profile.h index b3cd39298..666409e39 100644 --- a/base/system/cpu/private/win_impl/cpu_profile.h +++ b/base/system/cpu/private/win_impl/cpu_profile.h @@ -10,12 +10,12 @@ */ #pragma once -#include "base/expected/expected.hpp" +#include "aex/expected/expected.hpp" #include "system/cpu/cfcpu_profile.h" /** * @brief Internal Windows Query for CPU profile information * - * @return cf::expected + * @return aex::expected */ -cf::expected query_cpu_profile_info(); \ No newline at end of file +aex::expected query_cpu_profile_info(); diff --git a/base/system/gpu/CMakeLists.txt b/base/system/gpu/CMakeLists.txt index ef4e5e01d..743fe5925 100644 --- a/base/system/gpu/CMakeLists.txt +++ b/base/system/gpu/CMakeLists.txt @@ -10,6 +10,8 @@ set_target_properties(cfbase_gpu PROPERTIES ) # Define export macro since this will be linked into cfbase shared library target_compile_definitions(cfbase_gpu PRIVATE CFBASE_EXPORTS) +# Foundation utils (aex) + base/include platform headers +target_link_libraries(cfbase_gpu PRIVATE cfbase_headers) target_include_directories(cfbase_gpu PUBLIC $ diff --git a/base/system/gpu/gpu.cpp b/base/system/gpu/gpu.cpp index 9dfd45a54..b8ecfbc84 100644 --- a/base/system/gpu/gpu.cpp +++ b/base/system/gpu/gpu.cpp @@ -1,6 +1,6 @@ #include "system/gpu/gpu.h" -#include "base/macro/system_judge.h" -#include "include/base/expected/expected.hpp" +#include "aex/expected/expected.hpp" +#include "aex/macro/system_judge.h" // Platform-specific includes #ifdef CFDESKTOP_OS_WINDOWS @@ -155,7 +155,7 @@ EnvironmentScore GPUEnvProber::calcScore(const GPUInfo& g, const DisplayInfo& d) return s; } -cf::expected getGpuDisplayInfo() noexcept { +aex::expected getGpuDisplayInfo() noexcept { GpuDisplayInfo info; info.gpu = GPUEnvProber::probeGPU(); info.display = GPUEnvProber::probeDisplay(); diff --git a/base/system/gpu/private/linux_impl/gpu_info.cpp b/base/system/gpu/private/linux_impl/gpu_info.cpp index fe53457fc..ddb4ddc32 100644 --- a/base/system/gpu/private/linux_impl/gpu_info.cpp +++ b/base/system/gpu/private/linux_impl/gpu_info.cpp @@ -556,4 +556,4 @@ DisplayInfoHost query_display_info_linux() { return d; } -} // namespace cf \ No newline at end of file +} // namespace cf diff --git a/base/system/hardware_tier/CMakeLists.txt b/base/system/hardware_tier/CMakeLists.txt index 602ee0f9e..f4e3e7428 100644 --- a/base/system/hardware_tier/CMakeLists.txt +++ b/base/system/hardware_tier/CMakeLists.txt @@ -8,6 +8,9 @@ set_target_properties(cfbase_hardware_tier PROPERTIES target_compile_definitions(cfbase_hardware_tier PRIVATE CFBASE_EXPORTS) +# Foundation utils (aex) + base/include platform headers +target_link_libraries(cfbase_hardware_tier PRIVATE cfbase_headers) + target_include_directories(cfbase_hardware_tier PUBLIC $ diff --git a/base/system/hardware_tier/hardware_tier.cpp b/base/system/hardware_tier/hardware_tier.cpp index beee9dc0a..98b6bc463 100644 --- a/base/system/hardware_tier/hardware_tier.cpp +++ b/base/system/hardware_tier/hardware_tier.cpp @@ -128,7 +128,7 @@ void clearDeviceConfigOverride() { invalidateCache(); } -expected assessHardware(bool force_refresh) { +aex::expected assessHardware(bool force_refresh) { if (force_refresh) { invalidateCache(); } @@ -162,13 +162,13 @@ expected assessHardware(bool force_re // ── Stage 1: Collect ── if (!reg.collector) { - return cf::unexpected(HardwareTierError::CollectionFailed); + return aex::unexpected(HardwareTierError::CollectionFailed); } HardwareData data = reg.collector->collect(); // ── Stage 2: Score ── if (!reg.cpu_scorer || !reg.gpu_scorer || !reg.memory_scorer || !reg.display_scorer) { - return cf::unexpected(HardwareTierError::ScoringFailed); + return aex::unexpected(HardwareTierError::ScoringFailed); } result.cpu.value = reg.cpu_scorer->score(data); @@ -178,7 +178,7 @@ expected assessHardware(bool force_re // ── Stage 3: Assess ── if (!reg.assessor) { - return cf::unexpected(HardwareTierError::AssessmentFailed); + return aex::unexpected(HardwareTierError::AssessmentFailed); } result.tier = reg.assessor->assess(result.cpu.value, result.gpu.value, result.memory.value, result.display.value); @@ -196,10 +196,10 @@ expected assessHardware(bool force_re return result; } -expected getHardwareTierCapabilities() { +aex::expected getHardwareTierCapabilities() { std::lock_guard lock(g_cache_mutex); if (!g_cached.valid) { - return cf::unexpected(HardwareTierError::PolicyFailed); + return aex::unexpected(HardwareTierError::PolicyFailed); } return g_cached.capabilities; } diff --git a/base/system/memory/CMakeLists.txt b/base/system/memory/CMakeLists.txt index 72bd41dfd..c8b8c56e9 100644 --- a/base/system/memory/CMakeLists.txt +++ b/base/system/memory/CMakeLists.txt @@ -4,6 +4,8 @@ set_target_properties(cfbase_memory PROPERTIES ) # Define export macro since this will be linked into cfbase shared library target_compile_definitions(cfbase_memory PRIVATE CFBASE_EXPORTS) +# Foundation utils (aex) + base/include platform headers +target_link_libraries(cfbase_memory PRIVATE cfbase_headers) target_include_directories(cfbase_memory PUBLIC $ diff --git a/base/system/memory/memory_info.cpp b/base/system/memory/memory_info.cpp index 5e204f21c..2d5675ac7 100644 --- a/base/system/memory/memory_info.cpp +++ b/base/system/memory/memory_info.cpp @@ -14,7 +14,7 @@ */ #include "system/memory/memory_info.h" -#include "base/macro/system_judge.h" +#include "aex/macro/system_judge.h" #ifdef CFDESKTOP_OS_WINDOWS # include "private/win_impl/memory_info.h" @@ -32,4 +32,4 @@ void getSystemMemoryInfo(MemoryInfo& info) { #endif } -} // namespace cf \ No newline at end of file +} // namespace cf diff --git a/base/system/network/CMakeLists.txt b/base/system/network/CMakeLists.txt index 3f84e9ee0..edb9c35b1 100644 --- a/base/system/network/CMakeLists.txt +++ b/base/system/network/CMakeLists.txt @@ -4,6 +4,9 @@ add_library(cfbase_network STATIC target_compile_definitions(cfbase_network PRIVATE CFBASE_EXPORTS) +# Foundation utils (aex) + base/include platform headers +target_link_libraries(cfbase_network PRIVATE cfbase_headers) + target_link_libraries(cfbase_network PUBLIC Qt6::Core PRIVATE Qt6::Network diff --git a/base/system/network/network.cpp b/base/system/network/network.cpp index f357e9e9a..7d093caee 100644 --- a/base/system/network/network.cpp +++ b/base/system/network/network.cpp @@ -249,7 +249,7 @@ static NetworkStatus buildNetworkStatus() { return s; } -cf::expected getNetworkInfo() noexcept { +aex::expected getNetworkInfo() noexcept { NetworkInfo result; try { diff --git a/desktop/base/config_manager/include/cfconfig.hpp b/desktop/base/config_manager/include/cfconfig.hpp index c30d87264..3bf3dcf81 100644 --- a/desktop/base/config_manager/include/cfconfig.hpp +++ b/desktop/base/config_manager/include/cfconfig.hpp @@ -11,7 +11,7 @@ #pragma once -#include "base/singleton/simple_singleton.hpp" +#include "aex/singleton/simple_singleton.hpp" #include "cfconfig/cfconfig_domain_handle.h" #include "cfconfig/cfconfig_result.h" #include "cfconfig/cfconfig_watcher.h" @@ -35,7 +35,7 @@ class IConfigStorePathProvider; // Path provider forward declaration * change monitoring, and persistence capabilities. * * @note Thread-safe: All API calls can be used in multi-threaded environments - * @note Singleton pattern: Use ConfigStore::instance() to access + * @note aex::Singleton pattern: Use ConfigStore::instance() to access * @note Auto-initialization: Loads from paths on first access * * @code @@ -56,7 +56,7 @@ class IConfigStorePathProvider; // Path provider forward declaration * ConfigStore::instance().sync(SyncMethod::Async); * @endcode */ -class ConfigStore : public SimpleSingleton { +class ConfigStore : public aex::SimpleSingleton { public: ConfigStore(); ~ConfigStore(); diff --git a/desktop/base/logger/include/cflog/cflog_level.hpp b/desktop/base/logger/include/cflog/cflog_level.hpp index 20f0f80ca..a1b36e7cc 100644 --- a/desktop/base/logger/include/cflog/cflog_level.hpp +++ b/desktop/base/logger/include/cflog/cflog_level.hpp @@ -12,7 +12,7 @@ * @ingroup cflog */ #pragma once -#include "base/macro/build_type.h" +#include "aex/macro/build_type.h" #include namespace cf::log { diff --git a/desktop/base/logger/src/async_queue/async_queue.h b/desktop/base/logger/src/async_queue/async_queue.h index b5d1a5eae..20ec52cca 100644 --- a/desktop/base/logger/src/async_queue/async_queue.h +++ b/desktop/base/logger/src/async_queue/async_queue.h @@ -13,7 +13,7 @@ */ #pragma once -#include "base/lockfree/mpsc_queue.hpp" +#include "aex/lockfree/mpsc_queue.hpp" #include "cflog/cflog_level.hpp" #include "cflog/cflog_record.h" #include "cflog/cflog_sink.h" @@ -242,7 +242,7 @@ class AsyncPostQueue { std::mutex errorMu_; ///< Mutex for error queue access. // Normal queue (lockfree MPSC, bounded, drop when full) - cf::lockfree::MpscQueue normalQueue_; ///< Normal queue. + aex::lockfree::MpscQueue normalQueue_; ///< Normal queue. std::atomic normalQueueOverflow_{0}; ///< Count of dropped normal messages. // Notification diff --git a/desktop/base/path/include/cfpath/desktop_main_path_resolvers.h b/desktop/base/path/include/cfpath/desktop_main_path_resolvers.h index c9fa53e40..b8bf36cda 100644 --- a/desktop/base/path/include/cfpath/desktop_main_path_resolvers.h +++ b/desktop/base/path/include/cfpath/desktop_main_path_resolvers.h @@ -13,7 +13,7 @@ */ #pragma once -#include "base/singleton/singleton.hpp" +#include "aex/singleton/singleton.hpp" #include #include @@ -26,12 +26,12 @@ namespace cf::desktop::path { * * @ingroup desktop_base */ -class DesktopMainPathProvider : public cf::Singleton { +class DesktopMainPathProvider : public aex::Singleton { public: /** - * @brief Allows Singleton base to access private constructor. + * @brief Allows aex::Singleton base to access private constructor. */ - friend class cf::Singleton; + friend class aex::Singleton; /// @brief Single source of truth for all path types. /// Add new entries here — enum, string table, and count are derived automatically. #define CF_PATH_TYPES \ diff --git a/desktop/main/early_session/early_handle/early_handle.h b/desktop/main/early_session/early_handle/early_handle.h index a159c4a77..48d263cdd 100644 --- a/desktop/main/early_session/early_handle/early_handle.h +++ b/desktop/main/early_session/early_handle/early_handle.h @@ -12,14 +12,14 @@ * @ingroup early_session */ #pragma once -#include "base/singleton/simple_singleton.hpp" +#include "aex/singleton/simple_singleton.hpp" #include "settings/early_settings.h" #include class QWidget; namespace cf::desktop::early_stage { /** - * @brief Singleton handle for managing early desktop settings. + * @brief aex::Singleton handle for managing early desktop settings. * * Provides centralized access to early configuration settings throughout the * desktop initialization process. @@ -33,7 +33,7 @@ namespace cf::desktop::early_stage { * const auto& settings = handle.early_settings(); * @endcode */ -class EarlyHandle : public SimpleSingleton { +class EarlyHandle : public aex::SimpleSingleton { public: EarlyHandle() = default; /** diff --git a/desktop/main/init/desktop_backbone_init/desktop_backbone_init.cpp b/desktop/main/init/desktop_backbone_init/desktop_backbone_init.cpp index 223ab6530..cf90c7286 100644 --- a/desktop/main/init/desktop_backbone_init/desktop_backbone_init.cpp +++ b/desktop/main/init/desktop_backbone_init/desktop_backbone_init.cpp @@ -6,7 +6,8 @@ namespace cf::desktop::init_session { -std::vector> DesktopBackboneSetupStage::request_before_actions_init() const { +std::vector> +DesktopBackboneSetupStage::request_before_actions_init() const { const auto chain_ref = InitSessionChain::GetChainRef(); if (!chain_ref) { return {}; diff --git a/desktop/main/init/desktop_backbone_init/desktop_backbone_init.h b/desktop/main/init/desktop_backbone_init/desktop_backbone_init.h index a7fab1c4b..73b9c5809 100644 --- a/desktop/main/init/desktop_backbone_init/desktop_backbone_init.h +++ b/desktop/main/init/desktop_backbone_init/desktop_backbone_init.h @@ -24,7 +24,7 @@ class DesktopBackboneSetupStage : public IInitStage { * @return Compile-time constant stage name. */ std::string_view name() const noexcept override { return DESKTOP_BACKBONE_SETUP; } - std::vector> request_before_actions_init() const override; + std::vector> request_before_actions_init() const override; /** * @brief Executes the backbone setup initialization logic. diff --git a/desktop/main/init/early_gain/early_pass_stage.h b/desktop/main/init/early_gain/early_pass_stage.h index 3efcf4686..3139499fe 100644 --- a/desktop/main/init/early_gain/early_pass_stage.h +++ b/desktop/main/init/early_gain/early_pass_stage.h @@ -58,7 +58,9 @@ class EarlyTransferStage : public IInitStage { * @since N/A * @ingroup none */ - std::vector> request_before_actions_init() const override { return {}; } + std::vector> request_before_actions_init() const override { + return {}; + } /** * @brief Executes the early transfer stage session. diff --git a/desktop/main/init/gui_progress/gui_init_stage.h b/desktop/main/init/gui_progress/gui_init_stage.h index bd9573149..e04020394 100644 --- a/desktop/main/init/gui_progress/gui_init_stage.h +++ b/desktop/main/init/gui_progress/gui_init_stage.h @@ -62,7 +62,9 @@ class GuiLogoBootStage : public IInitStage { * @since N/A * @ingroup desktop_init */ - std::vector> request_before_actions_init() const override { return {}; } + std::vector> request_before_actions_init() const override { + return {}; + } /** * @brief Executes the GUI boot progress initialization. diff --git a/desktop/main/init/init_session_chain.cpp b/desktop/main/init/init_session_chain.cpp index 93e6238fe..38268272e 100644 --- a/desktop/main/init/init_session_chain.cpp +++ b/desktop/main/init/init_session_chain.cpp @@ -41,12 +41,12 @@ IInitStage::StageResult IInitStage::StageResult::critical_failed(const QString& // IInitStage 默认实现 // ============================================================================ -std::vector> IInitStage::request_before_actions_init() const { +std::vector> IInitStage::request_before_actions_init() const { return {}; // 默认无依赖 } namespace { -cf::WeakPtr g_chain_ref; +aex::WeakPtr g_chain_ref; } // namespace InitSessionChain::InitSessionChain(QObject* parent) : QObject(parent), weak_ptr_factory_(this) {} @@ -58,7 +58,7 @@ InitSessionChain::~InitSessionChain() { g_chain_ref.Reset(); } -cf::WeakPtr InitSessionChain::GetChainRef() { +aex::WeakPtr InitSessionChain::GetChainRef() { return g_chain_ref; } @@ -83,7 +83,7 @@ void InitSessionChain::reset() { last_debug_string.clear(); } -cf::WeakPtr InitSessionChain::find_stage(std::string_view stage_name) const { +aex::WeakPtr InitSessionChain::find_stage(std::string_view stage_name) const { for (const auto& stage : stages) { if (stage->name() == stage_name) { return stage->get_weak_ptr(); diff --git a/desktop/main/init/init_session_chain.h b/desktop/main/init/init_session_chain.h index 0b591f8e1..b70990cf1 100644 --- a/desktop/main/init/init_session_chain.h +++ b/desktop/main/init/init_session_chain.h @@ -9,8 +9,8 @@ * */ #pragma once -#include "base/weak_ptr/weak_ptr.h" -#include "base/weak_ptr/weak_ptr_factory.h" +#include "aex/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr_factory.h" #include "boot_stage_info.h" #include "init_stage.h" #include @@ -36,9 +36,9 @@ class InitSessionChain : public QObject { ~InitSessionChain(); /** - * @brief Gets the global Chain's WeakPtr reference (singleton pattern) + * @brief Gets the global Chain's aex::WeakPtr reference (singleton pattern) */ - static cf::WeakPtr GetChainRef(); + static aex::WeakPtr GetChainRef(); /** * @brief Adds an initialization stage to the end of the chain @@ -70,11 +70,11 @@ class InitSessionChain : public QObject { void reset(); /** - * @brief Finds a Stage by name and returns its WeakPtr + * @brief Finds a Stage by name and returns its aex::WeakPtr * @param[in] stage_name Stage name - * @return Valid WeakPtr if found, invalid WeakPtr otherwise + * @return Valid aex::WeakPtr if found, invalid aex::WeakPtr otherwise */ - cf::WeakPtr find_stage(std::string_view stage_name) const; + aex::WeakPtr find_stage(std::string_view stage_name) const; /** * @brief Gets the number of stages in the chain @@ -155,7 +155,7 @@ class InitSessionChain : public QObject { bool has_run{false}; bool is_valid{true}; // Whether the dependency graph is valid (no circular dependencies) - cf::WeakPtrFactory weak_ptr_factory_; + aex::WeakPtrFactory weak_ptr_factory_; }; } // namespace cf::desktop::init_session diff --git a/desktop/main/init/init_settings.h b/desktop/main/init/init_settings.h index 4f9076d04..04da4fafa 100644 --- a/desktop/main/init/init_settings.h +++ b/desktop/main/init/init_settings.h @@ -13,7 +13,7 @@ * @ingroup init_session */ #pragma once -#include "base/singleton/simple_singleton.hpp" +#include "aex/singleton/simple_singleton.hpp" #include #include @@ -41,7 +41,7 @@ namespace cf::desktop::init_session { * QWidget* widget = initInfo.unlockBootWidget(); * @endcode */ -class InitInfoHandle : public SimpleSingleton { +class InitInfoHandle : public aex::SimpleSingleton { public: ~InitInfoHandle(); /** diff --git a/desktop/main/init/init_stage.h b/desktop/main/init/init_stage.h index 61532c235..bfaff220e 100644 --- a/desktop/main/init/init_stage.h +++ b/desktop/main/init/init_stage.h @@ -1,6 +1,6 @@ #pragma once -#include "base/weak_ptr/weak_ptr.h" -#include "base/weak_ptr/weak_ptr_factory.h" +#include "aex/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr_factory.h" #include #include #include @@ -152,7 +152,7 @@ class IInitStage { * @since N/A * @ingroup desktop_init */ - virtual std::vector> request_before_actions_init() const; + virtual std::vector> request_before_actions_init() const; /** * @brief Executes the initialization logic for this stage. @@ -167,18 +167,18 @@ class IInitStage { /** * @brief Gets a weak pointer to this stage. - * @return WeakPtr pointing to this stage, or invalid if unsupported. + * @return aex::WeakPtr pointing to this stage, or invalid if unsupported. * @throws None - * @note Default returns an invalid WeakPtr. Concrete stages with a - * WeakPtrFactory should override this method. + * @note Default returns an invalid aex::WeakPtr. Concrete stages with a + * aex::WeakPtrFactory should override this method. * @warning None * @since N/A * @ingroup desktop_init */ - virtual WeakPtr get_weak_ptr() const { return nullptr; } + virtual aex::WeakPtr get_weak_ptr() const { return nullptr; } private: - WeakPtrFactory weak_ptr_factory; + aex::WeakPtrFactory weak_ptr_factory; }; } // namespace cf::desktop::init_session diff --git a/desktop/ui/CFDesktop.h b/desktop/ui/CFDesktop.h index b9f9b24fd..0dc4af230 100644 --- a/desktop/ui/CFDesktop.h +++ b/desktop/ui/CFDesktop.h @@ -11,8 +11,8 @@ #pragma once #include "../export.h" #include "CFDesktopProxy.h" -#include "base/weak_ptr/weak_ptr.h" -#include "base/weak_ptr/weak_ptr_factory.h" +#include "aex/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr_factory.h" #include namespace cf::desktop { @@ -70,8 +70,18 @@ class CF_DESKTOP_EXPORT CFDesktop final : public QWidget { */ bool component_available(const DesktopComponent d = DesktopComponent::Common) const noexcept; - // Make Weak Reference - WeakPtr GetWeak() const { return weak_ptr_factory_.GetWeakPtr(); } + /** + * @brief Obtains a weak reference to this desktop instance. + * + * @return WeakPtr pointing to this CFDesktop; invalidated on destruction. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup desktop_ui + */ + aex::WeakPtr GetWeak() const { return weak_ptr_factory_.GetWeakPtr(); } private: /* Managed by Resources */ @@ -94,6 +104,6 @@ class CF_DESKTOP_EXPORT CFDesktop final : public QWidget { void resizeEvent(QResizeEvent* event) override; private: - cf::WeakPtrFactory weak_ptr_factory_; + aex::WeakPtrFactory weak_ptr_factory_; }; } // namespace cf::desktop diff --git a/desktop/ui/CFDesktopEntity.cpp b/desktop/ui/CFDesktopEntity.cpp index a8ff0008f..57708f4ee 100644 --- a/desktop/ui/CFDesktopEntity.cpp +++ b/desktop/ui/CFDesktopEntity.cpp @@ -3,7 +3,7 @@ #include "CFDesktopWindowProxy.h" #include "IDesktopDisplaySizeStrategy.h" #include "IDesktopPropertyStrategy.h" -#include "base/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr.h" #include "cflog.h" #include "components/DisplayServerBackendFactory.h" #include "components/IDisplayServerBackend.h" @@ -71,14 +71,15 @@ CFDesktopEntity::RunsSetupResult CFDesktopEntity::run_init(RunsSetupMethod m) { auto wb = display_backend_->windowBackend(); if (wb) { auto* raw = wb.Get(); - QObject::connect(raw, &IWindowBackend::window_came, this, [](WeakPtr win) { - if (win) { - cf::log::traceftag("CFDesktopEntity", "External window detected: {}", - win->title().toStdString()); - } - }); + QObject::connect( + raw, &IWindowBackend::window_came, this, [](aex::WeakPtr win) { + if (win) { + cf::log::traceftag("CFDesktopEntity", "External window detected: {}", + win->title().toStdString()); + } + }); QObject::connect(raw, &IWindowBackend::window_gone, this, - [](WeakPtr /*win*/) { + [](aex::WeakPtr /*win*/) { cf::log::traceftag("CFDesktopEntity", "External window gone"); }); } @@ -185,7 +186,7 @@ CFDesktopEntity::RunsSetupResult CFDesktopEntity::run_init(RunsSetupMethod m) { return RunsSetupResult::OK; } -WeakPtr CFDesktopEntity::desktop_widget() const { +aex::WeakPtr CFDesktopEntity::desktop_widget() const { return desktop_entity_->GetWeak(); } diff --git a/desktop/ui/CFDesktopEntity.h b/desktop/ui/CFDesktopEntity.h index 86481697e..f28c9634a 100644 --- a/desktop/ui/CFDesktopEntity.h +++ b/desktop/ui/CFDesktopEntity.h @@ -16,8 +16,8 @@ #pragma once #include "../export.h" -#include "base/factory/registered_factory.hpp" -#include "base/weak_ptr/weak_ptr.h" +#include "aex/factory/registered_factory.hpp" +#include "aex/weak_ptr/weak_ptr.h" #include #include @@ -101,14 +101,14 @@ class CF_DESKTOP_EXPORT CFDesktopEntity : public QObject { /** * @brief Gets a weak pointer to the desktop widget. * - * @return WeakPtr referencing the desktop widget; may be empty. + * @return aex::WeakPtr referencing the desktop widget; may be empty. * @throws None * @note Returns observer reference; caller does not own the widget. * @warning None * @since N/A * @ingroup none */ - WeakPtr desktop_widget() const; + aex::WeakPtr desktop_widget() const; /** * @brief Destroys the CFDesktopEntity instance. @@ -159,7 +159,7 @@ class CF_DESKTOP_EXPORT CFDesktopEntity : public QObject { std::unique_ptr platform_factory_; /// @brief Display server backend. Ownership: owner. - cf::RegisteredFactory::unique_ptr_type display_backend_; + aex::RegisteredFactory::unique_ptr_type display_backend_; private: /// @brief Global singleton instance of CFDesktopEntity. Ownership: owner. diff --git a/desktop/ui/CFDesktopProxy.cpp b/desktop/ui/CFDesktopProxy.cpp index 40b92d5b9..d3216627e 100644 --- a/desktop/ui/CFDesktopProxy.cpp +++ b/desktop/ui/CFDesktopProxy.cpp @@ -3,6 +3,6 @@ namespace cf::desktop { -CFDesktopProxy::CFDesktopProxy(WeakPtr desktop) : desktop_(desktop) {} +CFDesktopProxy::CFDesktopProxy(aex::WeakPtr desktop) : desktop_(desktop) {} } // namespace cf::desktop diff --git a/desktop/ui/CFDesktopProxy.h b/desktop/ui/CFDesktopProxy.h index 2cb0e187e..2573c4677 100644 --- a/desktop/ui/CFDesktopProxy.h +++ b/desktop/ui/CFDesktopProxy.h @@ -14,7 +14,7 @@ #pragma once #include "../export.h" #include "CFDesktop.h" -#include "base/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr.h" #include namespace cf::desktop { @@ -71,10 +71,10 @@ class CF_DESKTOP_EXPORT CFDesktopProxy { * @since 0.1 * @ingroup desktop_ui */ - CFDesktopProxy(WeakPtr desktop); + CFDesktopProxy(aex::WeakPtr desktop); /// @brief Weak reference to the desktop instance. Ownership: observer. - WeakPtr desktop_; + aex::WeakPtr desktop_; }; } // namespace cf::desktop diff --git a/desktop/ui/base/qt_backend.h b/desktop/ui/base/qt_backend.h index 8d8a9c459..41764ec5d 100644 --- a/desktop/ui/base/qt_backend.h +++ b/desktop/ui/base/qt_backend.h @@ -13,7 +13,7 @@ */ #pragma once -#include "base/macro/system_judge.h" +#include "aex/macro/system_judge.h" namespace cf::desktop::backend { #ifdef CFDESKTOP_OS_LINUX diff --git a/desktop/ui/components/DisplayServerBackendFactory.h b/desktop/ui/components/DisplayServerBackendFactory.h index 8555769ca..94e624147 100644 --- a/desktop/ui/components/DisplayServerBackendFactory.h +++ b/desktop/ui/components/DisplayServerBackendFactory.h @@ -10,20 +10,20 @@ #pragma once #include "IDisplayServerBackend.h" -#include "base/factory/registered_factory.hpp" +#include "aex/factory/registered_factory.hpp" namespace cf::desktop { /** * @brief Factory for creating platform-specific display server backends. * - * Inherits from StaticRegisteredFactory, which + * Inherits from aex::StaticRegisteredFactory, which * delegates creation to a std::function registered at startup by the * platform-specific module. * * Usage: * auto backend = DisplayServerBackendFactory::instance().make_unique(); */ -class DisplayServerBackendFactory : public StaticRegisteredFactory {}; +class DisplayServerBackendFactory : public aex::StaticRegisteredFactory {}; } // namespace cf::desktop diff --git a/desktop/ui/components/IDisplayServerBackend.h b/desktop/ui/components/IDisplayServerBackend.h index 45860a2f9..42828ab15 100644 --- a/desktop/ui/components/IDisplayServerBackend.h +++ b/desktop/ui/components/IDisplayServerBackend.h @@ -26,7 +26,7 @@ #include "IWindow.h" #include "IWindowBackend.h" -#include "base/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr.h" #include #include @@ -147,7 +147,7 @@ class IDisplayServerBackend : public QObject { * @return Weak pointer to the IWindowBackend, or null if not * yet initialized. */ - virtual WeakPtr windowBackend() = 0; + virtual aex::WeakPtr windowBackend() = 0; /** * @brief Returns the list of output rectangles (screens). @@ -173,7 +173,7 @@ class IDisplayServerBackend : public QObject { * * @param[in] window Weak reference to the new window. */ - void externalWindowAppeared(WeakPtr window); + void externalWindowAppeared(aex::WeakPtr window); /** * @brief Emitted when an external application window disappears. @@ -182,7 +182,7 @@ class IDisplayServerBackend : public QObject { * * @param[in] window Weak reference to the disappearing window. */ - void externalWindowDisappeared(WeakPtr window); + void externalWindowDisappeared(aex::WeakPtr window); }; } // namespace cf::desktop diff --git a/desktop/ui/components/IShellLayerStrategy.h b/desktop/ui/components/IShellLayerStrategy.h index f019708b3..bdcdbe16a 100644 --- a/desktop/ui/components/IShellLayerStrategy.h +++ b/desktop/ui/components/IShellLayerStrategy.h @@ -14,7 +14,7 @@ */ #pragma once -#include "base/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr.h" #include #include #include @@ -42,7 +42,7 @@ class IShellLayerStrategy { * @param[in] layer Weak reference to the shell layer. * @param[in] wm Weak reference to the window manager. */ - virtual void activate(WeakPtr layer, WeakPtr wm) = 0; + virtual void activate(aex::WeakPtr layer, aex::WeakPtr wm) = 0; /** * @brief Deactivates the strategy and releases resources. diff --git a/desktop/ui/components/IWindow.h b/desktop/ui/components/IWindow.h index 810c800a5..1bb923969 100644 --- a/desktop/ui/components/IWindow.h +++ b/desktop/ui/components/IWindow.h @@ -15,8 +15,8 @@ #pragma once #include "WindowDefine.h" -#include "base/weak_ptr/weak_ptr.h" -#include "base/weak_ptr/weak_ptr_factory.h" +#include "aex/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr_factory.h" #include namespace cf::desktop { @@ -83,9 +83,9 @@ class IWindow : public QObject { /** * @brief Creates a weak pointer to this window. * - * @return A WeakPtr that references this IWindow instance. + * @return A aex::WeakPtr that references this IWindow instance. */ - WeakPtr make_weak() const { return weak_ptr_factory_.GetWeakPtr(); } + aex::WeakPtr make_weak() const { return weak_ptr_factory_.GetWeakPtr(); } signals: @@ -112,6 +112,6 @@ class IWindow : public QObject { private: /// Factory for creating weak pointers. Ownership: this instance. - WeakPtrFactory weak_ptr_factory_; + aex::WeakPtrFactory weak_ptr_factory_; }; } // namespace cf::desktop diff --git a/desktop/ui/components/IWindowBackend.h b/desktop/ui/components/IWindowBackend.h index ba0f3b352..af5c7d4cc 100644 --- a/desktop/ui/components/IWindowBackend.h +++ b/desktop/ui/components/IWindowBackend.h @@ -16,8 +16,8 @@ #pragma once #include "../render/backend_capabilities.h" #include "IWindow.h" -#include "base/weak_ptr/weak_ptr.h" -#include "base/weak_ptr/weak_ptr_factory.h" +#include "aex/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr_factory.h" #include namespace cf::desktop { @@ -38,17 +38,17 @@ class IWindowBackend : public QObject { virtual ~IWindowBackend() = default; - virtual WeakPtr createWindow(const QString& appId) = 0; + virtual aex::WeakPtr createWindow(const QString& appId) = 0; /** * @brief Destroys the given window and releases resources. * * @param[in] window Weak reference to the window to destroy. */ - virtual void destroyWindow(WeakPtr window) = 0; + virtual void destroyWindow(aex::WeakPtr window) = 0; /// Referenced windows held by the backend. - virtual QList> windows() const = 0; + virtual QList> windows() const = 0; /** * @brief Queries the capabilities of this window backend. @@ -65,10 +65,10 @@ class IWindowBackend : public QObject { /** * @brief Returns a weak reference to this backend. * - * @return A WeakPtr referencing this IWindowBackend instance. + * @return A aex::WeakPtr referencing this IWindowBackend instance. * @since 0.11 */ - WeakPtr make_weak() const { return weak_ptr_factory_.GetWeakPtr(); } + aex::WeakPtr make_weak() const { return weak_ptr_factory_.GetWeakPtr(); } signals: /** @@ -76,16 +76,17 @@ class IWindowBackend : public QObject { * * @param[in] ref Weak reference to the newly created window. */ - void window_came(WeakPtr ref); + void window_came(aex::WeakPtr ref); /** * @brief Emitted when a window is destroyed and about to be released. * * @param[in] ref Weak reference to the destroyed window. */ - void window_gone(WeakPtr ref); + void window_gone(aex::WeakPtr ref); private: - mutable WeakPtrFactory weak_ptr_factory_{const_cast(this)}; + mutable aex::WeakPtrFactory weak_ptr_factory_{ + const_cast(this)}; }; } // namespace cf::desktop diff --git a/desktop/ui/components/PanelManager.cpp b/desktop/ui/components/PanelManager.cpp index ea0a2d7ca..88be5fe8f 100644 --- a/desktop/ui/components/PanelManager.cpp +++ b/desktop/ui/components/PanelManager.cpp @@ -8,7 +8,7 @@ namespace cf::desktop { PanelManager::PanelManager(QWidget* host, QObject* parent) : QObject{parent}, host_(host) {} PanelManager::~PanelManager() = default; -PanelManager::RegisterFeedback PanelManager::registerPanel(WeakPtr panel) { +PanelManager::RegisterFeedback PanelManager::registerPanel(aex::WeakPtr panel) { if (!panel) { return RegisterFeedback::InvalidPanel; } @@ -23,7 +23,7 @@ PanelManager::RegisterFeedback PanelManager::registerPanel(WeakPtr panel return RegisterFeedback::OK; } -PanelManager::UnRegisterFeedback PanelManager::unregisterPanel(WeakPtr panel) { +PanelManager::UnRegisterFeedback PanelManager::unregisterPanel(aex::WeakPtr panel) { if (!panel) { return UnRegisterFeedback::UnknownPanel; } @@ -50,7 +50,7 @@ void PanelManager::relayout() { QRect available = host_->rect(); // ── Collect panels by edge (skip invalid / zero-size) ── - std::vector> tops, bottoms, lefts, rights; + std::vector> tops, bottoms, lefts, rights; for (const auto& p : panels) { if (!p || !p->widget() || p->preferredSize() <= 0) continue; @@ -71,7 +71,7 @@ void PanelManager::relayout() { } // Higher priority → placed first (outermost on that edge) - auto byPriority = [](const WeakPtr& a, const WeakPtr& b) { + auto byPriority = [](const aex::WeakPtr& a, const aex::WeakPtr& b) { return a->priority() > b->priority(); }; std::sort(tops.begin(), tops.end(), byPriority); diff --git a/desktop/ui/components/PanelManager.h b/desktop/ui/components/PanelManager.h index 45f0fe573..2530bb04d 100644 --- a/desktop/ui/components/PanelManager.h +++ b/desktop/ui/components/PanelManager.h @@ -15,7 +15,7 @@ #pragma once -#include "base/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr.h" #include #include #include @@ -62,7 +62,7 @@ class PanelManager : public QObject { * * @return Registration feedback indicating success or failure. */ - virtual RegisterFeedback registerPanel(WeakPtr panel); + virtual RegisterFeedback registerPanel(aex::WeakPtr panel); /** * @brief Unregisters a panel from the layout engine. @@ -71,7 +71,7 @@ class PanelManager : public QObject { * * @return Unregistration feedback indicating success or failure. */ - virtual UnRegisterFeedback unregisterPanel(WeakPtr panel); + virtual UnRegisterFeedback unregisterPanel(aex::WeakPtr panel); /** * @brief Returns the available geometry for the shell layer. @@ -100,7 +100,7 @@ class PanelManager : public QObject { /// Host widget for panel positioning. Ownership: external. QWidget* host_{nullptr}; /// Registered panels (weak references). Ownership: external. - std::vector> panels; + std::vector> panels; private: /// Cached available geometry after layout computation. diff --git a/desktop/ui/components/WindowManager.cpp b/desktop/ui/components/WindowManager.cpp index 8d74f705d..e2bcbbe8a 100644 --- a/desktop/ui/components/WindowManager.cpp +++ b/desktop/ui/components/WindowManager.cpp @@ -5,14 +5,14 @@ namespace cf::desktop { WindowManager::WindowManager(QObject* parent) : QObject(parent) {} -void WindowManager::setBackend(WeakPtr backend) { +void WindowManager::setBackend(aex::WeakPtr backend) { window_backend_ = std::move(backend); if (auto* backend_raw = window_backend_.Get()) { connect(backend_raw, &IWindowBackend::window_came, this, &WindowManager::onWindowCame); } } -WeakPtr WindowManager::create_window(const win_id_t& win_id) { +aex::WeakPtr WindowManager::create_window(const win_id_t& win_id) { if (windows_.find(win_id) != windows_.end()) return nullptr; @@ -27,7 +27,7 @@ WeakPtr WindowManager::create_window(const win_id_t& win_id) { return window; } -WeakPtr WindowManager::find_window(const win_id_t& win_id) const { +aex::WeakPtr WindowManager::find_window(const win_id_t& win_id) const { auto it = windows_.find(win_id); if (it == windows_.end()) return nullptr; @@ -38,21 +38,21 @@ WeakPtr WindowManager::find_window(const win_id_t& win_id) const { return it->second; } -bool WindowManager::request_close_window(WeakPtr window) { +bool WindowManager::request_close_window(aex::WeakPtr window) { if (!window) return false; window->requestClose(); return true; } -bool WindowManager::raise_a_window(WeakPtr window) { +bool WindowManager::raise_a_window(aex::WeakPtr window) { if (!window) return false; window->raise(); return true; } -void WindowManager::onWindowCame(WeakPtr window) { +void WindowManager::onWindowCame(aex::WeakPtr window) { if (!window) { return; } diff --git a/desktop/ui/components/WindowManager.h b/desktop/ui/components/WindowManager.h index d0a1c71a5..df7c1977e 100644 --- a/desktop/ui/components/WindowManager.h +++ b/desktop/ui/components/WindowManager.h @@ -15,7 +15,7 @@ #pragma once #include "IWindow.h" -#include "base/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr.h" #include "window_info.h" #include @@ -47,7 +47,7 @@ class WindowManager : public QObject { * * @param[in] backend Weak reference to the window backend. */ - void setBackend(WeakPtr backend); + void setBackend(aex::WeakPtr backend); /** * @brief Creates and registers a new window with the given ID. @@ -58,18 +58,18 @@ class WindowManager : public QObject { * * @param[in] win_id Unique identifier for the new window. * - * @return WeakPtr to the created window, or nullptr if duplicate. + * @return aex::WeakPtr to the created window, or nullptr if duplicate. */ - WeakPtr create_window(const win_id_t& win_id); + aex::WeakPtr create_window(const win_id_t& win_id); /** * @brief Finds a tracked window by its unique ID. * * @param[in] win_id The window identifier to look up. * - * @return WeakPtr to the window, or nullptr if not found / expired. + * @return aex::WeakPtr to the window, or nullptr if not found / expired. */ - WeakPtr find_window(const win_id_t& win_id) const; + aex::WeakPtr find_window(const win_id_t& win_id) const; /** * @brief Requests a graceful close for the given window. @@ -78,7 +78,7 @@ class WindowManager : public QObject { * * @return True if the close request was dispatched, false otherwise. */ - bool request_close_window(WeakPtr window); + bool request_close_window(aex::WeakPtr window); /** * @brief Raises the given window to the top of the stacking order. @@ -87,7 +87,7 @@ class WindowManager : public QObject { * * @return True if the raise request was dispatched, false otherwise. */ - bool raise_a_window(WeakPtr window); + bool raise_a_window(aex::WeakPtr window); signals: /** @@ -106,12 +106,12 @@ class WindowManager : public QObject { private: /// @brief Records a newly appeared window and watches its destruction. - void onWindowCame(WeakPtr window); + void onWindowCame(aex::WeakPtr window); /// Weak reference to the window backend. Ownership: external. - WeakPtr window_backend_{nullptr}; + aex::WeakPtr window_backend_{nullptr}; /// Tracked windows keyed by window ID (weak references only). Ownership: backend. - std::unordered_map, QStringHash> windows_; + std::unordered_map, QStringHash> windows_; /// Observed WindowInfo keyed by window ID. std::unordered_map window_infos_; }; diff --git a/desktop/ui/components/launcher/CMakeLists.txt b/desktop/ui/components/launcher/CMakeLists.txt index f2e57d575..7cc60a1f4 100644 --- a/desktop/ui/components/launcher/CMakeLists.txt +++ b/desktop/ui/components/launcher/CMakeLists.txt @@ -13,6 +13,6 @@ target_link_libraries( cfdesktop_launcher PRIVATE Qt6::Core # QProcess, QStringList - cfbase # cf::expected + cfbase # aex::expected (propagated via cfbase -> aex::aex) cflogger # Diagnostic logging for launch success/failure ) diff --git a/desktop/ui/components/launcher/app_launch_service.cpp b/desktop/ui/components/launcher/app_launch_service.cpp index d0b14e7a2..1ae0b1373 100644 --- a/desktop/ui/components/launcher/app_launch_service.cpp +++ b/desktop/ui/components/launcher/app_launch_service.cpp @@ -27,17 +27,17 @@ namespace { constexpr const char* kTag = "AppLaunchService"; } -cf::expected AppLaunchService::launch(const QString& exec_command) { +aex::expected AppLaunchService::launch(const QString& exec_command) { if (exec_command.trimmed().isEmpty()) { cf::log::warningftag(kTag, "launch skipped: empty exec_command"); - return cf::unexpected(AppLaunchError::Empty); + return aex::unexpected(AppLaunchError::Empty); } const QStringList parts = QProcess::splitCommand(exec_command); if (parts.isEmpty()) { cf::log::warningftag(kTag, "launch skipped: unparsable command '{}'", exec_command.toStdString()); - return cf::unexpected(AppLaunchError::Unparsable); + return aex::unexpected(AppLaunchError::Unparsable); } const QString program = parts.first(); @@ -50,7 +50,7 @@ cf::expected AppLaunchService::launch(const QString& exe } cf::log::errorftag(kTag, "failed to launch '{}'", exec_command.toStdString()); - return cf::unexpected(AppLaunchError::Failed); + return aex::unexpected(AppLaunchError::Failed); } } // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/launcher/app_launch_service.h b/desktop/ui/components/launcher/app_launch_service.h index 5ebc1cdc2..57714db25 100644 --- a/desktop/ui/components/launcher/app_launch_service.h +++ b/desktop/ui/components/launcher/app_launch_service.h @@ -17,7 +17,7 @@ #pragma once -#include "base/expected/expected.hpp" +#include "aex/expected/expected.hpp" #include @@ -63,7 +63,7 @@ class AppLaunchService { * @since 0.19 * @ingroup components */ - static cf::expected launch(const QString& exec_command); + static aex::expected launch(const QString& exec_command); }; } // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/shell_layer_impl/DefaultShellLayerStrategy.cpp b/desktop/ui/components/shell_layer_impl/DefaultShellLayerStrategy.cpp index b17fa0eae..9970fd0a3 100644 --- a/desktop/ui/components/shell_layer_impl/DefaultShellLayerStrategy.cpp +++ b/desktop/ui/components/shell_layer_impl/DefaultShellLayerStrategy.cpp @@ -10,7 +10,8 @@ DefaultShellLayerStrategy::DefaultShellLayerStrategy() { DefaultShellLayerStrategy::~DefaultShellLayerStrategy() = default; -void DefaultShellLayerStrategy::activate(WeakPtr layer, WeakPtr wm) { +void DefaultShellLayerStrategy::activate(aex::WeakPtr layer, + aex::WeakPtr wm) { log::trace("DefaultShellStrategy activated"); layer_ = layer; window_manager_ = wm; diff --git a/desktop/ui/components/shell_layer_impl/DefaultShellLayerStrategy.h b/desktop/ui/components/shell_layer_impl/DefaultShellLayerStrategy.h index 298de1c5a..73e1b472c 100644 --- a/desktop/ui/components/shell_layer_impl/DefaultShellLayerStrategy.h +++ b/desktop/ui/components/shell_layer_impl/DefaultShellLayerStrategy.h @@ -39,7 +39,7 @@ class DefaultShellLayerStrategy : public IShellLayerStrategy { * @param[in] layer Weak reference to the shell layer. * @param[in] wm Weak reference to the window manager. */ - void activate(WeakPtr layer, WeakPtr wm) override; + void activate(aex::WeakPtr layer, aex::WeakPtr wm) override; /** * @brief Deactivates the strategy and releases held references. @@ -55,7 +55,7 @@ class DefaultShellLayerStrategy : public IShellLayerStrategy { void onGeometryChanged(const QRect& available) override; private: - WeakPtr layer_; - WeakPtr window_manager_; + aex::WeakPtr layer_; + aex::WeakPtr window_manager_; }; } // namespace cf::desktop diff --git a/desktop/ui/components/shell_layer_impl/WallpaperShellLayerStrategy.cpp b/desktop/ui/components/shell_layer_impl/WallpaperShellLayerStrategy.cpp index ebe6e7d5c..a44f8a4e0 100644 --- a/desktop/ui/components/shell_layer_impl/WallpaperShellLayerStrategy.cpp +++ b/desktop/ui/components/shell_layer_impl/WallpaperShellLayerStrategy.cpp @@ -17,8 +17,8 @@ struct WallpaperShellLayerStrategy::Private { std::unique_ptr wallpaper_layer; QImage cached_scaled_image; QRect current_geometry; - WeakPtr layer; - WeakPtr window_manager; + aex::WeakPtr layer; + aex::WeakPtr window_manager; }; // ============================================================ @@ -37,7 +37,8 @@ WallpaperShellLayerStrategy::WallpaperShellLayerStrategy( WallpaperShellLayerStrategy::~WallpaperShellLayerStrategy() = default; -void WallpaperShellLayerStrategy::activate(WeakPtr layer, WeakPtr wm) { +void WallpaperShellLayerStrategy::activate(aex::WeakPtr layer, + aex::WeakPtr wm) { log::traceftag("WallpaperShellLayerStrategy", "Activated"); d->layer = layer; d->window_manager = wm; diff --git a/desktop/ui/components/shell_layer_impl/WallpaperShellLayerStrategy.h b/desktop/ui/components/shell_layer_impl/WallpaperShellLayerStrategy.h index 43e89b04c..75754171f 100644 --- a/desktop/ui/components/shell_layer_impl/WallpaperShellLayerStrategy.h +++ b/desktop/ui/components/shell_layer_impl/WallpaperShellLayerStrategy.h @@ -66,7 +66,7 @@ class WallpaperShellLayerStrategy : public IShellLayerStrategy { * @since 0.15 * @ingroup components */ - void activate(WeakPtr layer, WeakPtr wm) override; + void activate(aex::WeakPtr layer, aex::WeakPtr wm) override; /** * @brief Deactivates the strategy and releases resources. diff --git a/desktop/ui/components/shell_layer_impl/WidgetShellLayer.cpp b/desktop/ui/components/shell_layer_impl/WidgetShellLayer.cpp index 5dfcb8636..3b21e78d8 100644 --- a/desktop/ui/components/shell_layer_impl/WidgetShellLayer.cpp +++ b/desktop/ui/components/shell_layer_impl/WidgetShellLayer.cpp @@ -25,7 +25,7 @@ void WidgetShellLayer::setStrategy(std::unique_ptr strategy strategy_ = std::move(strategy); if (strategy_) { // Activate with our weak ref; WindowManager not available at this layer - strategy_->activate(GetWeak(), WeakPtr{}); + strategy_->activate(GetWeak(), aex::WeakPtr{}); } } diff --git a/desktop/ui/components/shell_layer_impl/WidgetShellLayer.h b/desktop/ui/components/shell_layer_impl/WidgetShellLayer.h index 63a2c2956..249dc22d6 100644 --- a/desktop/ui/components/shell_layer_impl/WidgetShellLayer.h +++ b/desktop/ui/components/shell_layer_impl/WidgetShellLayer.h @@ -16,8 +16,8 @@ #pragma once #include "../IShellLayer.h" -#include "base/weak_ptr/weak_ptr.h" -#include "base/weak_ptr/weak_ptr_factory.h" +#include "aex/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr_factory.h" #include #include namespace cf::desktop { @@ -64,9 +64,9 @@ class WidgetShellLayer : public QWidget, public IShellLayer { /** * @brief Returns a weak pointer to this shell layer. * - * @return A WeakPtr referencing this instance. + * @return A aex::WeakPtr referencing this instance. */ - WeakPtr GetWeak() const { return weak_factory_.GetWeakPtr(); } + aex::WeakPtr GetWeak() const { return weak_factory_.GetWeakPtr(); } // -- IShellLayer ---------------------------------------------------------- /** @@ -101,7 +101,7 @@ class WidgetShellLayer : public QWidget, public IShellLayer { std::unique_ptr strategy_; /// Weak pointer factory (must be last member). - mutable cf::WeakPtrFactory weak_factory_{this}; + mutable aex::WeakPtrFactory weak_factory_{this}; }; } // namespace cf::desktop diff --git a/desktop/ui/components/shell_layer_impl/wallpaper_src_chain.cpp b/desktop/ui/components/shell_layer_impl/wallpaper_src_chain.cpp index 55df389e5..ecab23e1b 100644 --- a/desktop/ui/components/shell_layer_impl/wallpaper_src_chain.cpp +++ b/desktop/ui/components/shell_layer_impl/wallpaper_src_chain.cpp @@ -1,5 +1,5 @@ #include "wallpaper_src_chain.h" -#include "base/policy_chain/policy_chain.hpp" +#include "aex/policy_chain/policy_chain.hpp" #include "cfconfig.hpp" #include "cflog.h" #include "cfpath/desktop_main_path_resolvers.h" @@ -7,8 +7,8 @@ #include namespace cf::desktop::wallpaper { -PolicyChain WallpaperImages() { - return policy_chain_builder() +aex::PolicyChain WallpaperImages() { + return aex::policy_chain_builder() .then([]() -> std::optional { // Policy 1: Load from ConfigStore wallpaper domain log::trace("Scanning from the config file to load wallpaper"); diff --git a/desktop/ui/components/shell_layer_impl/wallpaper_src_chain.h b/desktop/ui/components/shell_layer_impl/wallpaper_src_chain.h index c5b380969..dee7e3b7c 100644 --- a/desktop/ui/components/shell_layer_impl/wallpaper_src_chain.h +++ b/desktop/ui/components/shell_layer_impl/wallpaper_src_chain.h @@ -2,7 +2,7 @@ * @file desktop/ui/components/shell_layer_impl/wallpaper_src_chain.h * @brief Provides the wallpaper image source chain. * - * Defines the function that produces a PolicyChain of wallpaper + * Defines the function that produces a aex::PolicyChain of wallpaper * image paths for the shell layer. * * @author N/A @@ -13,14 +13,14 @@ */ #pragma once -#include "base/policy_chain/policy_chain.hpp" +#include "aex/policy_chain/policy_chain.hpp" #include namespace cf::desktop::wallpaper { /** * @brief Get the Wallpaper Load Image from here * - * @return PolicyChain + * @return aex::PolicyChain */ -PolicyChain WallpaperImages(); +aex::PolicyChain WallpaperImages(); } // namespace cf::desktop::wallpaper diff --git a/desktop/ui/components/statusbar/status_bar.h b/desktop/ui/components/statusbar/status_bar.h index bcab51ce5..b203452a6 100644 --- a/desktop/ui/components/statusbar/status_bar.h +++ b/desktop/ui/components/statusbar/status_bar.h @@ -16,8 +16,8 @@ #pragma once #include "IStatusBar.h" -#include "base/weak_ptr/weak_ptr.h" -#include "base/weak_ptr/weak_ptr_factory.h" +#include "aex/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr_factory.h" #include #include @@ -199,7 +199,7 @@ class StatusBar final : public QWidget, public IStatusBar { /** * @brief Returns a weak reference to this status bar. * - * @return WeakPtr valid for this instance's lifetime. + * @return aex::WeakPtr valid for this instance's lifetime. * * @throws None * @note None @@ -207,7 +207,7 @@ class StatusBar final : public QWidget, public IStatusBar { * @since 0.19 * @ingroup components */ - WeakPtr GetWeak() const { return weak_factory_.GetWeakPtr(); } + aex::WeakPtr GetWeak() const { return weak_factory_.GetWeakPtr(); } protected: /** @@ -272,7 +272,7 @@ class StatusBar final : public QWidget, public IStatusBar { QPixmap icon_masks_[4]; /// Weak pointer factory (must be the last member). - mutable cf::WeakPtrFactory weak_factory_{this}; + mutable aex::WeakPtrFactory weak_factory_{this}; }; } // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/taskbar/centered_taskbar.h b/desktop/ui/components/taskbar/centered_taskbar.h index 218a1c720..c0c5e7b31 100644 --- a/desktop/ui/components/taskbar/centered_taskbar.h +++ b/desktop/ui/components/taskbar/centered_taskbar.h @@ -16,9 +16,9 @@ #pragma once +#include "aex/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr_factory.h" #include "app_entry.h" -#include "base/weak_ptr/weak_ptr.h" -#include "base/weak_ptr/weak_ptr_factory.h" #include "components/IPanel.h" #include @@ -151,7 +151,7 @@ class CenteredTaskbar final : public QWidget, public cf::desktop::IPanel { /** * @brief Returns a weak reference to this taskbar. * - * @return WeakPtr valid for this instance's lifetime. + * @return aex::WeakPtr valid for this instance's lifetime. * * @throws None * @note None @@ -159,7 +159,7 @@ class CenteredTaskbar final : public QWidget, public cf::desktop::IPanel { * @since 0.19 * @ingroup components */ - WeakPtr GetWeak() const { return weak_factory_.GetWeakPtr(); } + aex::WeakPtr GetWeak() const { return weak_factory_.GetWeakPtr(); } signals: /** @@ -207,7 +207,7 @@ class CenteredTaskbar final : public QWidget, public cf::desktop::IPanel { QColor divider_color_; ///< Top hairline divider color. /// Weak pointer factory (must be the last member). - mutable cf::WeakPtrFactory weak_factory_{this}; + mutable aex::WeakPtrFactory weak_factory_{this}; }; } // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/wallpaper/ImageWallPaperLayer.cpp b/desktop/ui/components/wallpaper/ImageWallPaperLayer.cpp index a4fc7a830..b12bdfda2 100644 --- a/desktop/ui/components/wallpaper/ImageWallPaperLayer.cpp +++ b/desktop/ui/components/wallpaper/ImageWallPaperLayer.cpp @@ -96,7 +96,7 @@ void ImageWallPaperLayer::setBackgroundColor(const QColor& color) { d->background_color = color; } -bool ImageWallPaperLayer::loadImageFromToken(const WeakPtr& token) { +bool ImageWallPaperLayer::loadImageFromToken(const aex::WeakPtr& token) { if (!token) { return false; } diff --git a/desktop/ui/components/wallpaper/ImageWallPaperLayer.h b/desktop/ui/components/wallpaper/ImageWallPaperLayer.h index 102f6f8f2..7f2de2f7b 100644 --- a/desktop/ui/components/wallpaper/ImageWallPaperLayer.h +++ b/desktop/ui/components/wallpaper/ImageWallPaperLayer.h @@ -172,7 +172,7 @@ class ImageWallPaperLayer : public WallPaperLayer { * @param[in] token Weak reference to the wallpaper token. * @return True if the image was loaded successfully. */ - bool loadImageFromToken(const WeakPtr& token); + bool loadImageFromToken(const aex::WeakPtr& token); struct Private; std::unique_ptr d; diff --git a/desktop/ui/components/wallpaper/WallPaperAccessStorage.cpp b/desktop/ui/components/wallpaper/WallPaperAccessStorage.cpp index 05601fa67..a5b7e9441 100644 --- a/desktop/ui/components/wallpaper/WallPaperAccessStorage.cpp +++ b/desktop/ui/components/wallpaper/WallPaperAccessStorage.cpp @@ -4,7 +4,7 @@ namespace cf::desktop::wallpaper { -WeakPtr WallPaperAccessStorage::toNext(bool move_cursor) { +aex::WeakPtr WallPaperAccessStorage::toNext(bool move_cursor) { try { if (move_cursor) wallpaper_images.next(); @@ -15,7 +15,7 @@ WeakPtr WallPaperAccessStorage::toNext(bool move_cursor) { } } -WeakPtr WallPaperAccessStorage::toPrev(bool move_cursor) { +aex::WeakPtr WallPaperAccessStorage::toPrev(bool move_cursor) { try { if (move_cursor) wallpaper_images.prev(); @@ -26,7 +26,7 @@ WeakPtr WallPaperAccessStorage::toPrev(bool move_cursor) { } } -WeakPtr WallPaperAccessStorage::to(const wallpaper_token_id_t& token) { +aex::WeakPtr WallPaperAccessStorage::to(const wallpaper_token_id_t& token) { auto it = std::find_if(wallpaper_images.begin(), wallpaper_images.end(), [&token](const auto& ptr) { return ptr->id() == token; }); if (it == wallpaper_images.end()) @@ -35,13 +35,13 @@ WeakPtr WallPaperAccessStorage::to(const wallpaper_token_id_t& t return (*it)->getWeakPtr(); } -WeakPtr WallPaperAccessStorage::at(const wallpaper_token_id_t& token) { +aex::WeakPtr WallPaperAccessStorage::at(const wallpaper_token_id_t& token) { auto it = std::find_if(wallpaper_images.begin(), wallpaper_images.end(), [&token](const auto& ptr) { return ptr->id() == token; }); - return it != wallpaper_images.end() ? (*it)->getWeakPtr() : WeakPtr{}; + return it != wallpaper_images.end() ? (*it)->getWeakPtr() : aex::WeakPtr{}; } -WeakPtr WallPaperAccessStorage::current() const { +aex::WeakPtr WallPaperAccessStorage::current() const { if (wallpaper_images.empty()) { return {}; } diff --git a/desktop/ui/components/wallpaper/WallPaperAccessStorage.h b/desktop/ui/components/wallpaper/WallPaperAccessStorage.h index b7b2d4511..0904239b0 100644 --- a/desktop/ui/components/wallpaper/WallPaperAccessStorage.h +++ b/desktop/ui/components/wallpaper/WallPaperAccessStorage.h @@ -14,8 +14,8 @@ #pragma once #include "WallPaperToken.h" -#include "base/indexed_vector/indexed_vector.hpp" -#include "base/weak_ptr/weak_ptr.h" +#include "aex/indexed_vector/indexed_vector.hpp" +#include "aex/weak_ptr/weak_ptr.h" #include #include #include @@ -24,7 +24,7 @@ namespace cf::desktop::wallpaper { /** * @brief Indexed storage for wallpaper tokens with cursor navigation. * - * Wraps an indexed_vector of unique WallPaperToken instances and provides + * Wraps an aex::indexed_vector of unique WallPaperToken instances and provides * sequential/random access with overflow signaling via Qt signals. * * @note None. @@ -44,19 +44,19 @@ class WallPaperAccessStorage : public QObject { /** * @brief move the index to the next one * - * @return WeakPtr + * @return aex::WeakPtr */ - WeakPtr toNext(bool move_cursor = false); - WeakPtr toPrev(bool move_cursor = false); - WeakPtr to(const wallpaper_token_id_t& token); - WeakPtr at(const wallpaper_token_id_t& token); + aex::WeakPtr toNext(bool move_cursor = false); + aex::WeakPtr toPrev(bool move_cursor = false); + aex::WeakPtr to(const wallpaper_token_id_t& token); + aex::WeakPtr at(const wallpaper_token_id_t& token); /** * @brief Returns the token at the current cursor position. * - * @return WeakPtr to current token, or null if storage is empty. + * @return aex::WeakPtr to current token, or null if storage is empty. */ - WeakPtr current() const; + aex::WeakPtr current() const; /** * @brief Appends a wallpaper token to the storage. @@ -155,6 +155,6 @@ class WallPaperAccessStorage : public QObject { void indexed_overflow(const OverFlowType t); private: - cf::indexed_vector> wallpaper_images; + aex::indexed_vector> wallpaper_images; }; } // namespace cf::desktop::wallpaper diff --git a/desktop/ui/components/wallpaper/WallPaperToken.cpp b/desktop/ui/components/wallpaper/WallPaperToken.cpp index 2a004a618..8f2cc96be 100644 --- a/desktop/ui/components/wallpaper/WallPaperToken.cpp +++ b/desktop/ui/components/wallpaper/WallPaperToken.cpp @@ -1,5 +1,5 @@ #include "WallPaperToken.h" -#include "base/weak_ptr/weak_ptr_factory.h" +#include "aex/weak_ptr/weak_ptr_factory.h" #include #include @@ -7,7 +7,7 @@ namespace cf::desktop::wallpaper { // ============================================================ // WallPaperToken::Private -// WeakPtrFactory placed LAST (destroyed first, invalidates +// aex::WeakPtrFactory placed LAST (destroyed first, invalidates // weak refs before other members). // ============================================================ struct WallPaperToken::Private { @@ -19,7 +19,7 @@ struct WallPaperToken::Private { QString description; SourceType source_type; - WeakPtrFactory weak_ptr_factory; + aex::WeakPtrFactory weak_ptr_factory; Private(const QString& path, SourceType type, const QString& name, const QString& auth, const QString& desc, WallPaperToken* owner) @@ -62,7 +62,7 @@ bool operator==(const WallPaperToken& lh, const WallPaperToken& rh) { return lh.d->id == rh.d->id; } -WeakPtr WallPaperToken::getWeakPtr() const { +aex::WeakPtr WallPaperToken::getWeakPtr() const { return d->weak_ptr_factory.GetWeakPtr(); } diff --git a/desktop/ui/components/wallpaper/WallPaperToken.h b/desktop/ui/components/wallpaper/WallPaperToken.h index bd02ceeb8..ca6a481fb 100644 --- a/desktop/ui/components/wallpaper/WallPaperToken.h +++ b/desktop/ui/components/wallpaper/WallPaperToken.h @@ -13,8 +13,8 @@ */ #pragma once -#include "base/factory/smartptr_plain_factory.hpp" -#include "base/weak_ptr/weak_ptr.h" +#include "aex/factory/smartptr_plain_factory.hpp" +#include "aex/weak_ptr/weak_ptr.h" #include #include #include @@ -136,7 +136,7 @@ class WallPaperToken { * @ingroup wallpaper */ SourceType sourceType() const; - WeakPtr getWeakPtr() const; + aex::WeakPtr getWeakPtr() const; friend bool operator==(const WallPaperToken& lh, const WallPaperToken& rh); diff --git a/desktop/ui/platform/IDesktopDisplaySizeStrategy.h b/desktop/ui/platform/IDesktopDisplaySizeStrategy.h index adc461810..a8eb9875a 100644 --- a/desktop/ui/platform/IDesktopDisplaySizeStrategy.h +++ b/desktop/ui/platform/IDesktopDisplaySizeStrategy.h @@ -15,8 +15,8 @@ #pragma once #include "../../../export.h" #include "IDesktopPropertyStrategy.h" -#include "base/weak_ptr/weak_ptr.h" -#include "base/weak_ptr/weak_ptr_factory.h" +#include "aex/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr_factory.h" #include class QWidget; @@ -83,7 +83,7 @@ class CF_DESKTOP_EXPORT IDesktopDisplaySizeStrategy : public IDesktopPropertyStr * @since 0.1 * @ingroup none */ - WeakPtr GetOne() { return weak_factory_ptr_.GetWeakPtr(); } + aex::WeakPtr GetOne() { return weak_factory_ptr_.GetWeakPtr(); } /** * @brief Applies the strategy behavior to the specified widget. @@ -123,7 +123,7 @@ class CF_DESKTOP_EXPORT IDesktopDisplaySizeStrategy : public IDesktopPropertyStr * * Ownership: owner. Manages the weak reference lifecycle. */ - WeakPtrFactory weak_factory_ptr_; + aex::WeakPtrFactory weak_factory_ptr_; }; } // namespace cf::desktop::platform_strategy diff --git a/desktop/ui/platform/linux_wsl/linux_wsl_factory.h b/desktop/ui/platform/linux_wsl/linux_wsl_factory.h index 5c53f5171..6d2c9e503 100644 --- a/desktop/ui/platform/linux_wsl/linux_wsl_factory.h +++ b/desktop/ui/platform/linux_wsl/linux_wsl_factory.h @@ -14,7 +14,7 @@ #pragma once #include "IDesktopPropertyStrategy.h" -#include "base/singleton/simple_singleton.hpp" +#include "aex/singleton/simple_singleton.hpp" #include namespace cf::desktop::platform_strategy::wsl { @@ -28,7 +28,7 @@ class DisplaySizePolicyMaker; * Creates and manages the lifecycle of desktop property strategy instances * specifically for WSL environments. * - * @note Inherits singleton semantics from SimpleSingleton base. + * @note Inherits singleton semantics from aex::SimpleSingleton base. * * @ingroup platform_wsl * @@ -37,7 +37,7 @@ class DisplaySizePolicyMaker; * auto* strategy = factory.create(StrategyType::WSL); * @endcode */ -class WSLDeskProStrategyFactory : public SimpleSingleton { +class WSLDeskProStrategyFactory : public aex::SimpleSingleton { public: /** * @brief Constructs a new WSLDeskProStrategyFactory instance. diff --git a/desktop/ui/platform/linux_wsl/wsl_x11_display_server_backend.cpp b/desktop/ui/platform/linux_wsl/wsl_x11_display_server_backend.cpp index d1a1eadb0..b6d71e432 100644 --- a/desktop/ui/platform/linux_wsl/wsl_x11_display_server_backend.cpp +++ b/desktop/ui/platform/linux_wsl/wsl_x11_display_server_backend.cpp @@ -77,10 +77,10 @@ bool WSLDisplayServerBackend::initialize(int argc, char** argv) { // Forward external-window signals. connect(window_backend_.get(), &IWindowBackend::window_came, this, - [this](WeakPtr win) { emit externalWindowAppeared(win); }); + [this](aex::WeakPtr win) { emit externalWindowAppeared(win); }); connect(window_backend_.get(), &IWindowBackend::window_gone, this, - [this](WeakPtr win) { emit externalWindowDisappeared(win); }); + [this](aex::WeakPtr win) { emit externalWindowDisappeared(win); }); // Monitor screen geometry changes. auto* guiApp = static_cast(QGuiApplication::instance()); @@ -130,7 +130,7 @@ int WSLDisplayServerBackend::runEventLoop() { return QApplication::exec(); } -WeakPtr WSLDisplayServerBackend::windowBackend() { +aex::WeakPtr WSLDisplayServerBackend::windowBackend() { if (!window_backend_) { return {}; } diff --git a/desktop/ui/platform/linux_wsl/wsl_x11_display_server_backend.h b/desktop/ui/platform/linux_wsl/wsl_x11_display_server_backend.h index 3821b03b9..db8b8d2a9 100644 --- a/desktop/ui/platform/linux_wsl/wsl_x11_display_server_backend.h +++ b/desktop/ui/platform/linux_wsl/wsl_x11_display_server_backend.h @@ -9,7 +9,7 @@ */ #pragma once -#include "base/macro/system_judge.h" +#include "aex/macro/system_judge.h" #ifdef CFDESKTOP_OS_LINUX @@ -67,7 +67,7 @@ class WSLDisplayServerBackend : public IDisplayServerBackend { * @return Exit code from the event loop. */ int runEventLoop() override; - WeakPtr windowBackend() override; + aex::WeakPtr windowBackend() override; QList outputs() const override; private: diff --git a/desktop/ui/platform/linux_wsl/wsl_x11_window.h b/desktop/ui/platform/linux_wsl/wsl_x11_window.h index bd2fdd32d..09a7fd432 100644 --- a/desktop/ui/platform/linux_wsl/wsl_x11_window.h +++ b/desktop/ui/platform/linux_wsl/wsl_x11_window.h @@ -12,7 +12,7 @@ */ #pragma once -#include "base/macro/system_judge.h" +#include "aex/macro/system_judge.h" #ifdef CFDESKTOP_OS_LINUX diff --git a/desktop/ui/platform/linux_wsl/wsl_x11_window_backend.cpp b/desktop/ui/platform/linux_wsl/wsl_x11_window_backend.cpp index 839bf88cc..41f0c5a06 100644 --- a/desktop/ui/platform/linux_wsl/wsl_x11_window_backend.cpp +++ b/desktop/ui/platform/linux_wsl/wsl_x11_window_backend.cpp @@ -121,13 +121,13 @@ void WSLX11WindowBackend::stopTracking() { // IWindowBackend interface // ────────────────────────────────────────────────────────── -WeakPtr WSLX11WindowBackend::createWindow(const QString& appId) { +aex::WeakPtr WSLX11WindowBackend::createWindow(const QString& appId) { Q_UNUSED(appId); cf::log::warningftag("WSLWindowBackend", "createWindow() not supported in X11 tracking mode"); return {}; } -void WSLX11WindowBackend::destroyWindow(WeakPtr window) { +void WSLX11WindowBackend::destroyWindow(aex::WeakPtr window) { if (!window) { return; } @@ -138,8 +138,8 @@ void WSLX11WindowBackend::destroyWindow(WeakPtr window) { unregisterWindow(ww->nativeHandle()); } -QList> WSLX11WindowBackend::windows() const { - QList> result; +QList> WSLX11WindowBackend::windows() const { + QList> result; result.reserve(static_cast(tracked_windows_.size())); for (const auto& [win, ptr] : tracked_windows_) { result.append(ptr->make_weak()); diff --git a/desktop/ui/platform/linux_wsl/wsl_x11_window_backend.h b/desktop/ui/platform/linux_wsl/wsl_x11_window_backend.h index 0ab162a35..bc3c849e6 100644 --- a/desktop/ui/platform/linux_wsl/wsl_x11_window_backend.h +++ b/desktop/ui/platform/linux_wsl/wsl_x11_window_backend.h @@ -13,7 +13,7 @@ */ #pragma once -#include "base/macro/system_judge.h" +#include "aex/macro/system_judge.h" #ifdef CFDESKTOP_OS_LINUX @@ -59,15 +59,15 @@ class WSLX11WindowBackend : public IWindowBackend { // ── IWindowBackend interface ────────────────────────── - WeakPtr createWindow(const QString& appId) override; + aex::WeakPtr createWindow(const QString& appId) override; /** * @brief Destroys a previously tracked window. * @param[in] window Weak reference to the window to destroy. */ - void destroyWindow(WeakPtr window) override; + void destroyWindow(aex::WeakPtr window) override; - QList> windows() const override; + QList> windows() const override; /** * @brief Returns the rendering capabilities of this backend. diff --git a/desktop/ui/platform/windows/windows_display_server_backend.cpp b/desktop/ui/platform/windows/windows_display_server_backend.cpp index a2d40a99f..f92325723 100644 --- a/desktop/ui/platform/windows/windows_display_server_backend.cpp +++ b/desktop/ui/platform/windows/windows_display_server_backend.cpp @@ -67,10 +67,10 @@ bool WindowsDisplayServerBackend::initialize(int argc, char** argv) { // Forward external-window signals. connect(window_backend_.get(), &IWindowBackend::window_came, this, - [this](WeakPtr win) { emit externalWindowAppeared(win); }); + [this](aex::WeakPtr win) { emit externalWindowAppeared(win); }); connect(window_backend_.get(), &IWindowBackend::window_gone, this, - [this](WeakPtr win) { emit externalWindowDisappeared(win); }); + [this](aex::WeakPtr win) { emit externalWindowDisappeared(win); }); // Monitor screen geometry changes. auto* guiApp = static_cast(QGuiApplication::instance()); @@ -113,7 +113,7 @@ int WindowsDisplayServerBackend::runEventLoop() { return QApplication::exec(); } -WeakPtr WindowsDisplayServerBackend::windowBackend() { +aex::WeakPtr WindowsDisplayServerBackend::windowBackend() { if (!window_backend_) { return {}; } diff --git a/desktop/ui/platform/windows/windows_display_server_backend.h b/desktop/ui/platform/windows/windows_display_server_backend.h index 71d21d237..8eaeb4858 100644 --- a/desktop/ui/platform/windows/windows_display_server_backend.h +++ b/desktop/ui/platform/windows/windows_display_server_backend.h @@ -9,7 +9,7 @@ */ #pragma once -#include "base/macro/system_judge.h" +#include "aex/macro/system_judge.h" #ifdef CFDESKTOP_OS_WINDOWS @@ -67,7 +67,7 @@ class WindowsDisplayServerBackend : public IDisplayServerBackend { */ int runEventLoop() override; - WeakPtr windowBackend() override; + aex::WeakPtr windowBackend() override; QList outputs() const override; private: diff --git a/desktop/ui/platform/windows/windows_display_size_policy.cpp b/desktop/ui/platform/windows/windows_display_size_policy.cpp index f86cc85c4..f3d740dd9 100644 --- a/desktop/ui/platform/windows/windows_display_size_policy.cpp +++ b/desktop/ui/platform/windows/windows_display_size_policy.cpp @@ -1,6 +1,6 @@ #include "windows_display_size_policy.h" #include "IDesktopDisplaySizeStrategy.h" -#include "base/macro/system_judge.h" +#include "aex/macro/system_judge.h" #include "cflog.h" #include #include diff --git a/desktop/ui/platform/windows/windows_factory.h b/desktop/ui/platform/windows/windows_factory.h index 03373b576..4215e6737 100644 --- a/desktop/ui/platform/windows/windows_factory.h +++ b/desktop/ui/platform/windows/windows_factory.h @@ -15,7 +15,7 @@ #pragma once #include "../../../export.h" #include "IDesktopPropertyStrategy.h" -#include "base/singleton/simple_singleton.hpp" +#include "aex/singleton/simple_singleton.hpp" #include namespace cf::desktop::platform_strategy::windows { @@ -29,7 +29,7 @@ class WindowsDisplaySizePolicy; * Creates and manages the lifecycle of desktop property strategy instances * specifically for Windows environments. * - * @note Inherits singleton semantics from SimpleSingleton base. + * @note Inherits singleton semantics from aex::SimpleSingleton base. * * @ingroup platform_windows * @@ -39,7 +39,7 @@ class WindowsDisplaySizePolicy; * @endcode */ class CF_DESKTOP_EXPORT WindowsDeskProStrategyFactory - : public SimpleSingleton { + : public aex::SimpleSingleton { public: /** * @brief Constructs a new WindowsDeskProStrategyFactory instance. diff --git a/desktop/ui/platform/windows/windows_window.h b/desktop/ui/platform/windows/windows_window.h index 917d6d5a1..7bd274607 100644 --- a/desktop/ui/platform/windows/windows_window.h +++ b/desktop/ui/platform/windows/windows_window.h @@ -12,7 +12,7 @@ */ #pragma once -#include "base/macro/system_judge.h" +#include "aex/macro/system_judge.h" #ifdef CFDESKTOP_OS_WINDOWS diff --git a/desktop/ui/platform/windows/windows_window_backend.cpp b/desktop/ui/platform/windows/windows_window_backend.cpp index 4796d6ca3..abd2d9ddd 100644 --- a/desktop/ui/platform/windows/windows_window_backend.cpp +++ b/desktop/ui/platform/windows/windows_window_backend.cpp @@ -142,7 +142,7 @@ void WindowsWindowBackend::stopTracking() { // IWindowBackend interface // ────────────────────────────────────────────────────────── -WeakPtr WindowsWindowBackend::createWindow(const QString& appId) { +aex::WeakPtr WindowsWindowBackend::createWindow(const QString& appId) { // For the pseudo-desktop, internal window creation is not yet // implemented. External windows are discovered via hooks. Q_UNUSED(appId); @@ -150,7 +150,7 @@ WeakPtr WindowsWindowBackend::createWindow(const QString& appId) { return {}; } -void WindowsWindowBackend::destroyWindow(WeakPtr window) { +void WindowsWindowBackend::destroyWindow(aex::WeakPtr window) { // For external windows, "destroy" means stop tracking — // we don't actually close the window. if (!window) { @@ -163,8 +163,8 @@ void WindowsWindowBackend::destroyWindow(WeakPtr window) { unregisterWindow(ww->nativeHandle()); } -QList> WindowsWindowBackend::windows() const { - QList> result; +QList> WindowsWindowBackend::windows() const { + QList> result; result.reserve(static_cast(tracked_windows_.size())); for (const auto& [hwnd, win] : tracked_windows_) { result.append(win->make_weak()); diff --git a/desktop/ui/platform/windows/windows_window_backend.h b/desktop/ui/platform/windows/windows_window_backend.h index d357eb514..c76663ae4 100644 --- a/desktop/ui/platform/windows/windows_window_backend.h +++ b/desktop/ui/platform/windows/windows_window_backend.h @@ -13,7 +13,7 @@ */ #pragma once -#include "base/macro/system_judge.h" +#include "aex/macro/system_judge.h" #ifdef CFDESKTOP_OS_WINDOWS @@ -57,16 +57,16 @@ class WindowsWindowBackend : public IWindowBackend { // ── IWindowBackend interface ────────────────────────── - WeakPtr createWindow(const QString& appId) override; + aex::WeakPtr createWindow(const QString& appId) override; /** * @brief Destroys the given window and removes it from tracking. * * @param[in] window Weak reference to the window to destroy. */ - void destroyWindow(WeakPtr window) override; + void destroyWindow(aex::WeakPtr window) override; - QList> windows() const override; + QList> windows() const override; /** * @brief Returns the capabilities of this Windows backend. diff --git a/example/base/CMakeLists.txt b/example/base/CMakeLists.txt index bc2602a49..bb27eec21 100644 --- a/example/base/CMakeLists.txt +++ b/example/base/CMakeLists.txt @@ -1,5 +1,4 @@ -log_info("Example Base" "Start From Common Examples") -add_subdirectory(common) +# NOTE: the policy_chain common example now lives in the aex submodule. log_info("Example Base" "Start From System Configurations") add_subdirectory(system) diff --git a/example/base/common/CMakeLists.txt b/example/base/common/CMakeLists.txt deleted file mode 100644 index e7dad1f05..000000000 --- a/example/base/common/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ -log_info("Example Base Common" "Creating PolicyChain Example") - -# PolicyChain example executable -add_executable(example_policy_chain - example_policy_chain.cpp -) - -target_link_libraries(example_policy_chain - PRIVATE - cfbase -) - -# Set C++20 for this example -target_compile_features(example_policy_chain PRIVATE cxx_std_20) - -# Set output to examples directory -cf_set_example_output_dir(example_policy_chain "base") - -# Register for launcher generation (Windows) -cf_register_example_launcher(example_policy_chain "base") - -log_info("Example Base Common" "PolicyChain Example Created!") diff --git a/example/base/common/example_policy_chain.cpp b/example/base/common/example_policy_chain.cpp deleted file mode 100644 index ae0ed1c4d..000000000 --- a/example/base/common/example_policy_chain.cpp +++ /dev/null @@ -1,150 +0,0 @@ -#include "base/policy_chain/policy_chain.hpp" -#include -#include -#include - -void example_basic_usage() { - std::cout << "=== Basic Usage ===" << std::endl; - - // Create a chain with multiple fallback policies - cf::PolicyChain chain; - - // Policy 1 (highest priority): return 2x if input > 0 - chain.add_front([](int x) -> std::optional { - if (x > 0) - return x * 2; - return std::nullopt; - }); - - // Policy 2: return -x if input < 0 - chain.add_front([](int x) -> std::optional { - if (x < 0) - return -x; - return std::nullopt; - }); - - // Policy 3 (lowest priority): return 0 as default - chain.add_back([](int) -> std::optional { return 0; }); - - // Test cases - assert(chain.execute(5) == 10); // Policy 1: 5 * 2 = 10 - assert(chain.execute(-3) == 3); // Policy 2: -(-3) = 3 - assert(chain.execute(0) == 0); // Policy 3: default fallback - - // Using operator() - assert(chain(10) == 20); - - std::cout << "All assertions passed!" << std::endl; -} - -void example_factory_function() { - std::cout << "\n=== Using make_policy_chain ===" << std::endl; - - auto chain = cf::make_policy_chain( - [](const std::string& s) -> std::optional { - // Try to parse as integer - try { - return std::stoi(s); - } catch (...) { - return std::nullopt; - } - }, - [](const std::string& s) -> std::optional { - // Fallback: return string length - if (s.empty()) { - return {}; - } - return static_cast(s.length()); - }, - [](const std::string&) -> std::optional { - // Default fallback - return -1; - }); - - assert(chain.execute("123") == 123); // Parses to 123 - assert(chain.execute("hello") == 5); // Length is 5 - assert(chain.execute("") == -1); // Default fallback - - std::cout << "All assertions passed!" << std::endl; -} - -void example_builder() { - std::cout << "\n=== Using Builder API ===" << std::endl; - - auto chain = cf::policy_chain_builder() - .then([](double x) -> std::optional { - // Try to round to nearest int - if (x >= 0.0) - return static_cast(x + 0.5); - return std::nullopt; - }) - .then([](double x) -> std::optional { - // Fallback: truncate - return static_cast(x); - }) - .then([](double) -> std::optional { - // Default - return 0; - }) - .build(); - - assert(chain.execute(3.7) == 4); // Round: 3.7 -> 4 - assert(chain.execute(-2.3) == -2); // Truncate: -2.3 -> -2 - - std::cout << "All assertions passed!" << std::endl; -} - -void example_multiple_args() { - std::cout << "\n=== Multiple Arguments ===" << std::endl; - - auto chain = cf::make_policy_chain( - [](int a, int b) -> std::optional { - if (b != 0) - return a / b; - return std::nullopt; - }, - [](int a, int) -> std::optional { return a * 2; }, - [](int, int) -> std::optional { return -1; }); - - assert(chain.execute(10, 2) == 5); // Division: 10 / 2 = 5 - assert(chain.execute(10, 0) == 20); // Fallback: 10 * 2 = 20 - - std::cout << "All assertions passed!" << std::endl; -} - -void example_void_output() { - std::cout << "\n=== Void-like Output (bool) ===" << std::endl; - - // Using std::optional for success/failure with fallback - auto chain = cf::make_policy_chain( - [](int x) -> std::optional { - if (x > 100) - return true; - return std::nullopt; - }, - [](int x) -> std::optional { - if (x > 50) - return false; - return std::nullopt; - }, - [](int) -> std::optional { - return false; // Default - }); - - assert(chain.execute(150) == true); // First policy matches - assert(chain.execute(75) == false); // Second policy matches - assert(chain.execute(10) == false); // Default fallback - - std::cout << "All assertions passed!" << std::endl; -} - -int main() { - example_basic_usage(); - example_factory_function(); - example_builder(); - example_multiple_args(); - example_void_output(); - - std::cout << "\n=== All examples completed successfully! ===" << std::endl; - return 0; -} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bfa3e563f..b2dd81b8a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -35,8 +35,8 @@ else() set(QT_TEST_AVAILABLE FALSE) endif() -# Add base unit tests subdirectory -add_subdirectory(base) +# NOTE: base foundation unit tests now live in the aex submodule +# (third_party/aex/test) and run from aex's own standalone build. # Add config manager tests subdirectory add_subdirectory(config_manager) diff --git a/test/base/CMakeLists.txt b/test/base/CMakeLists.txt deleted file mode 100644 index 0271e0c59..000000000 --- a/test/base/CMakeLists.txt +++ /dev/null @@ -1,81 +0,0 @@ -# Unit tests for base library components using GoogleTest -include(add_gtest_executable) - -log_info("base_tests" "Configured base library unit tests:") - -# ============================================================================= -# Base library unit tests - expected_test -# ============================================================================= -add_gtest_executable( - TEST_NAME expected_test - SOURCE_FILE expected/expected_test.cpp - LINK_LIBRARIES CFDesktop::base_headers;Qt6::Core;GTest::gtest;GTest::gtest_main - LABELS "base;unit;expected" - LOG_MODULE base_tests -) - -# ============================================================================= -# Base library unit tests - scope_guard_test -# ============================================================================= -add_gtest_executable( - TEST_NAME scope_guard_test - SOURCE_FILE scope_guard/scope_guard_test.cpp - LINK_LIBRARIES CFDesktop::base_headers;Qt6::Core;GTest::gtest;GTest::gtest_main - LABELS "base;unit;scope_guard" - LOG_MODULE base_tests -) - -# ============================================================================= -# Base library unit tests - constexpr_fnv1a_test -# ============================================================================= -add_gtest_executable( - TEST_NAME constexpr_fnv1a_test - SOURCE_FILE hash/constexpr_fnv1a_test.cpp - LINK_LIBRARIES CFDesktop::base_headers;Qt6::Core;GTest::gtest;GTest::gtest_main - LABELS "base;unit;hash" - LOG_MODULE base_tests -) - -# ============================================================================= -# Base library unit tests - weak_ptr_test -# ============================================================================= -add_gtest_executable( - TEST_NAME weak_ptr_test - SOURCE_FILE weak_ptr/weak_ptr_test.cpp - LINK_LIBRARIES CFDesktop::base_headers;Qt6::Core;GTest::gtest;GTest::gtest_main - LABELS "base;unit;weak_ptr" - LOG_MODULE base_tests -) - -# ============================================================================= -# Base library unit tests - mpsc_queue_test -# ============================================================================= -add_gtest_executable( - TEST_NAME mpsc_queue_test - SOURCE_FILE lockfree/mpsc_queue_test.cpp - LINK_LIBRARIES CFDesktop::base_headers;Qt6::Core;GTest::gtest;GTest::gtest_main - LABELS "base;unit;lockfree" - LOG_MODULE base_tests -) - -# ============================================================================= -# Base library unit tests - policy_chain_test -# ============================================================================= -add_gtest_executable( - TEST_NAME policy_chain_test - SOURCE_FILE policy_chain/policy_chain_test.cpp - LINK_LIBRARIES CFDesktop::base_headers;Qt6::Core;GTest::gtest;GTest::gtest_main - LABELS "base;unit;policy_chain" - LOG_MODULE base_tests -) - -# ============================================================================= -# Base library unit tests - indexed_vector_test -# ============================================================================= -add_gtest_executable( - TEST_NAME indexed_vector_test - SOURCE_FILE indexed_vector/indexed_vector_test.cpp - LINK_LIBRARIES CFDesktop::base_headers;Qt6::Core;GTest::gtest;GTest::gtest_main - LABELS "base;unit;indexed_vector" - LOG_MODULE base_tests -) diff --git a/test/base/expected/expected_test.cpp b/test/base/expected/expected_test.cpp deleted file mode 100644 index 263d9e27a..000000000 --- a/test/base/expected/expected_test.cpp +++ /dev/null @@ -1,721 +0,0 @@ -/** - * @file expected_test.cpp - * @brief Comprehensive unit tests for cf::expected using GoogleTest - * - * Test Coverage: - * 1. Construction and Destruction - * 2. Value and Error Access - * 3. Assignment Operators - * 4. Monadic Operations (and_then, or_else, transform, transform_error) - * 5. Comparison Operations - * 6. Swap Operations - * 7. Exception Handling (bad_expected_access) - * 8. Type Traits and Compile-time Properties - * 9. Move Semantics - * 10. Const Correctness - * 11. expected Specialization - */ - -#include "base/expected/expected.hpp" -#include -#include -#include -#include -#include - -// ============================================================================= -// Test Suite 1: Construction and Destruction -// ============================================================================= - -TEST(ExpectedTest, DefaultConstructor) { - cf::expected e1; - EXPECT_TRUE(e1.has_value()); - EXPECT_EQ(*e1, 0); - - cf::expected e2; - EXPECT_TRUE(e2.has_value()); - EXPECT_TRUE(e2->empty()); -} - -TEST(ExpectedTest, ValueConstructor) { - cf::expected e1(42); - EXPECT_TRUE(e1.has_value()); - EXPECT_EQ(*e1, 42); - - // Implicit conversion - cf::expected e2 = 100; - EXPECT_EQ(*e2, 100); - - // Move-only type - cf::expected, std::string> e3(std::make_unique(999)); - EXPECT_TRUE(e3.has_value()); - EXPECT_EQ(**e3, 999); -} - -TEST(ExpectedTest, ErrorConstructor) { - cf::expected e1(cf::unexpected("error")); - EXPECT_FALSE(e1.has_value()); - EXPECT_EQ(e1.error(), "error"); - - // Implicit conversion - cf::expected e2 = cf::unexpected("failure"); - EXPECT_FALSE(e2.has_value()); - EXPECT_EQ(e2.error(), "failure"); -} - -TEST(ExpectedTest, CopyConstructor) { - // Copy with value - cf::expected e1(42); - cf::expected e2(e1); - EXPECT_TRUE(e2.has_value()); - EXPECT_EQ(*e2, 42); - EXPECT_EQ(*e1, 42); // Original unchanged - - // Copy with error - cf::expected e3(cf::unexpected("error")); - cf::expected e4(e3); - EXPECT_FALSE(e4.has_value()); - EXPECT_EQ(e4.error(), "error"); -} - -TEST(ExpectedTest, MoveConstructor) { - cf::expected e1(std::string("hello")); - cf::expected e2(std::move(e1)); - EXPECT_TRUE(e2.has_value()); - EXPECT_EQ(*e2, "hello"); - - cf::expected e3(cf::unexpected("error")); - cf::expected e4(std::move(e3)); - EXPECT_FALSE(e4.has_value()); - EXPECT_EQ(e4.error(), "error"); -} - -TEST(ExpectedTest, InPlaceConstructors) { - cf::expected e1(std::in_place, 5, 'a'); - EXPECT_TRUE(e1.has_value()); - EXPECT_EQ(*e1, "aaaaa"); - - cf::expected e2(cf::unexpect, 3, 'b'); - EXPECT_FALSE(e2.has_value()); - EXPECT_EQ(e2.error(), "bbb"); -} - -// ============================================================================= -// Test Suite 2: Value and Error Access -// ============================================================================= - -TEST(ExpectedTest, OperatorArrow) { - struct S { - int value = 42; - int get() const { return value; } - }; - - cf::expected e(std::in_place); - EXPECT_EQ(e->value, 42); - EXPECT_EQ(e->get(), 42); - - const cf::expected ce(std::in_place); - EXPECT_EQ(ce->value, 42); -} - -TEST(ExpectedTest, OperatorDereference) { - cf::expected e(42); - EXPECT_EQ(*e, 42); - - cf::expected e2(std::string("hello")); - std::string moved = *std::move(e2); - EXPECT_EQ(moved, "hello"); -} - -TEST(ExpectedTest, ValueMethod) { - cf::expected e(42); - EXPECT_EQ(e.value(), 42); - - cf::expected e_err(cf::unexpected("error")); - EXPECT_THROW(e_err.value(), cf::bad_expected_access); - - // Check exception content - try { - e_err.value(); - } catch (const cf::bad_expected_access& ex) { - EXPECT_EQ(ex.error(), "error"); - EXPECT_STREQ(ex.what(), "bad_expected_access"); - } -} - -TEST(ExpectedTest, ErrorMethod) { - cf::expected e_err(cf::unexpected("error")); - EXPECT_EQ(e_err.error(), "error"); - - cf::expected e_void(cf::unexpected(42)); - EXPECT_EQ(e_void.error(), 42); -} - -TEST(ExpectedTest, ValueOr) { - cf::expected e(42); - EXPECT_EQ(e.value_or(-1), 42); - - cf::expected e_err(cf::unexpected("error")); - EXPECT_EQ(e_err.value_or(-1), -1); -} - -TEST(ExpectedTest, ErrorOr) { - cf::expected e(42); - EXPECT_EQ(e.error_or("default"), "default"); - - cf::expected e_err(cf::unexpected("error")); - EXPECT_EQ(e_err.error_or("default"), "error"); -} - -TEST(ExpectedTest, HasValueAndBoolConversion) { - cf::expected e_val(42); - EXPECT_TRUE(e_val.has_value()); - EXPECT_TRUE(e_val); - EXPECT_TRUE(static_cast(e_val)); - - cf::expected e_err(cf::unexpected("error")); - EXPECT_FALSE(e_err.has_value()); - EXPECT_FALSE(e_err); -} - -// ============================================================================= -// Test Suite 3: Assignment Operators -// ============================================================================= - -TEST(ExpectedTest, CopyAssignment) { - // Value to value - cf::expected e1(10); - cf::expected e2(20); - e1 = e2; - EXPECT_EQ(*e1, 20); - EXPECT_EQ(*e2, 20); - - // Error to error - cf::expected e3(cf::unexpected("err1")); - cf::expected e4(cf::unexpected("err2")); - e3 = e4; - EXPECT_EQ(e3.error(), "err2"); - - // Value to error (state change) - cf::expected e5(cf::unexpected("error")); - cf::expected e6(42); - e5 = e6; - EXPECT_TRUE(e5.has_value()); - EXPECT_EQ(*e5, 42); - - // Error to value (state change) - cf::expected e7(42); - cf::expected e8(cf::unexpected("error")); - e7 = e8; - EXPECT_FALSE(e7.has_value()); - EXPECT_EQ(e7.error(), "error"); - - // Self assignment - cf::expected e9(99); - e9 = e9; - EXPECT_EQ(*e9, 99); -} - -TEST(ExpectedTest, MoveAssignment) { - cf::expected e1(std::string("hello")); - cf::expected e2(std::string("world")); - e1 = std::move(e2); - EXPECT_EQ(*e1, "world"); - - cf::expected e3(cf::unexpected("err1")); - cf::expected e4(cf::unexpected("err2")); - e3 = std::move(e4); - EXPECT_EQ(e3.error(), "err2"); -} - -TEST(ExpectedTest, ValueAssignment) { - cf::expected e(cf::unexpected("error")); - e = 42; - EXPECT_TRUE(e.has_value()); - EXPECT_EQ(*e, 42); - - e = 100; - EXPECT_EQ(*e, 100); -} - -TEST(ExpectedTest, UnexpectedAssignment) { - cf::expected e(42); - e = cf::unexpected("error"); - EXPECT_FALSE(e.has_value()); - EXPECT_EQ(e.error(), "error"); -} - -// ============================================================================= -// Test Suite 4: Monadic Operations -// ============================================================================= - -TEST(ExpectedTest, AndThen) { - auto add_one = [](int x) -> cf::expected { return x + 1; }; - - cf::expected e1(5); - auto result1 = e1.and_then(add_one); - EXPECT_TRUE(result1.has_value()); - EXPECT_EQ(*result1, 6); - - cf::expected e2(cf::unexpected("error")); - auto result2 = e2.and_then(add_one); - EXPECT_FALSE(result2.has_value()); - EXPECT_EQ(result2.error(), "error"); - - // Chaining - auto multiply_by_two = [](int x) -> cf::expected { return x * 2; }; - - cf::expected e3(3); - auto result3 = e3.and_then(add_one).and_then(multiply_by_two); - EXPECT_EQ(*result3, 8); // (3+1)*2 - - auto always_error = [](int) -> cf::expected { - return cf::unexpected("chain error"); - }; - - auto result4 = e3.and_then(add_one).and_then(always_error); - EXPECT_FALSE(result4.has_value()); - EXPECT_EQ(result4.error(), "chain error"); -} - -TEST(ExpectedTest, OrElse) { - auto recover = [](const std::string& err) -> cf::expected { return -1; }; - - cf::expected e1(cf::unexpected("error")); - auto result1 = e1.or_else(recover); - EXPECT_TRUE(result1.has_value()); - EXPECT_EQ(*result1, -1); - - cf::expected e2(42); - auto result2 = e2.or_else(recover); - EXPECT_TRUE(result2.has_value()); - EXPECT_EQ(*result2, 42); - - auto convert_error = [](const std::string& err) -> cf::expected { - return cf::unexpected(static_cast(err.size())); - }; - - cf::expected e3(cf::unexpected("test")); - auto result3 = e3.or_else(convert_error); - EXPECT_FALSE(result3.has_value()); - EXPECT_EQ(result3.error(), 4); -} - -TEST(ExpectedTest, Transform) { - auto square = [](int x) { return x * x; }; - - cf::expected e1(5); - auto result1 = e1.transform(square); - EXPECT_TRUE(result1.has_value()); - EXPECT_EQ(*result1, 25); - - cf::expected e2(cf::unexpected("error")); - auto result2 = e2.transform(square); - EXPECT_FALSE(result2.has_value()); - EXPECT_EQ(result2.error(), "error"); - - // Type change - auto to_string = [](int x) { return "value: " + std::to_string(x); }; - - cf::expected e3(42); - auto result3 = e3.transform(to_string); - EXPECT_TRUE(result3.has_value()); - EXPECT_EQ(*result3, "value: 42"); -} - -TEST(ExpectedTest, TransformError) { - auto prepend_prefix = [](const std::string& err) { return "Error: " + err; }; - - cf::expected e1(cf::unexpected("test")); - auto result1 = e1.transform_error(prepend_prefix); - EXPECT_FALSE(result1.has_value()); - EXPECT_EQ(result1.error(), "Error: test"); - - cf::expected e2(42); - auto result2 = e2.transform_error(prepend_prefix); - EXPECT_TRUE(result2.has_value()); - EXPECT_EQ(*result2, 42); - - auto to_error_code = [](const std::string& err) { return static_cast(err.size()); }; - - cf::expected e3(cf::unexpected("error")); - auto result3 = e3.transform_error(to_error_code); - EXPECT_FALSE(result3.has_value()); - EXPECT_EQ(result3.error(), 5); -} - -// ============================================================================= -// Test Suite 5: Comparison Operations -// ============================================================================= - -TEST(ExpectedTest, EqualityComparison) { - cf::expected e1(42); - cf::expected e2(42); - cf::expected e3(43); - cf::expected e4(cf::unexpected("error")); - cf::expected e5(cf::unexpected("error")); - cf::expected e6(cf::unexpected("other")); - - EXPECT_EQ(e1, e2); - EXPECT_NE(e1, e3); - EXPECT_NE(e1, e4); - EXPECT_EQ(e4, e5); - EXPECT_NE(e4, e6); - - // Comparison with value - EXPECT_EQ(e1, 42); - EXPECT_NE(e1, 43); - - // Comparison with unexpected - EXPECT_EQ(e4, cf::unexpected("error")); - EXPECT_NE(e1, cf::unexpected("error")); -} - -// ============================================================================= -// Test Suite 6: Swap Operations -// ============================================================================= - -TEST(ExpectedTest, SwapValueValue) { - cf::expected e1(10); - cf::expected e2(20); - - e1.swap(e2); - - EXPECT_EQ(*e1, 20); - EXPECT_EQ(*e2, 10); - EXPECT_TRUE(e1.has_value() && e2.has_value()); -} - -TEST(ExpectedTest, SwapErrorError) { - cf::expected e1(cf::unexpected("error1")); - cf::expected e2(cf::unexpected("error2")); - - e1.swap(e2); - - EXPECT_EQ(e1.error(), "error2"); - EXPECT_EQ(e2.error(), "error1"); -} - -TEST(ExpectedTest, SwapValueError) { - cf::expected e1(std::string("value")); - cf::expected e2(cf::unexpected(42)); - - e1.swap(e2); - - EXPECT_FALSE(e1.has_value()); - EXPECT_EQ(e1.error(), 42); - EXPECT_TRUE(e2.has_value()); - EXPECT_EQ(*e2, "value"); -} - -TEST(ExpectedTest, StdSwap) { - cf::expected e1(10); - cf::expected e2(20); - - std::swap(e1, e2); - - EXPECT_EQ(*e1, 20); - EXPECT_EQ(*e2, 10); -} - -TEST(ExpectedTest, SelfSwap) { - cf::expected e(42); - e.swap(e); - EXPECT_TRUE(e.has_value()); - EXPECT_EQ(*e, 42); -} - -// ============================================================================= -// Test Suite 7: Exception Handling -// ============================================================================= - -TEST(ExpectedTest, BadExpectedAccess) { - cf::expected e(cf::unexpected("error message")); - - EXPECT_THROW(e.value(), cf::bad_expected_access); - - try { - e.value(); - } catch (const cf::bad_expected_access& ex) { - EXPECT_STREQ(ex.what(), "bad_expected_access"); - EXPECT_EQ(ex.error(), "error message"); - } -} - -TEST(ExpectedTest, ExceptionInMonadicOps) { - auto throwing_func = [](int) -> cf::expected { - throw std::runtime_error("user exception"); - }; - - cf::expected e(42); - EXPECT_THROW(e.and_then(throwing_func), std::runtime_error); -} - -// ============================================================================= -// Test Suite 8: Type Traits and Compile-time Properties -// ============================================================================= - -TEST(ExpectedTest, TypeTraits) { - using E = cf::expected; - - EXPECT_TRUE((std::is_copy_constructible_v)); - EXPECT_TRUE((std::is_move_constructible_v)); - EXPECT_TRUE((std::is_copy_assignable_v)); - EXPECT_TRUE((std::is_move_assignable_v)); - - EXPECT_TRUE((std::is_same_v)); - EXPECT_TRUE((std::is_same_v)); - EXPECT_TRUE((std::is_same_v>)); -} - -TEST(ExpectedTest, NoexceptSpecifications) { - cf::expected e(42); - - EXPECT_TRUE(noexcept(e.has_value())); - EXPECT_TRUE(noexcept(static_cast(e))); - EXPECT_TRUE(noexcept(e.error())); -} - -// ============================================================================= -// Test Suite 9: Move Semantics and Resource Management -// ============================================================================= - -TEST(ExpectedTest, MoveOnlyTypes) { - // unique_ptr as value - cf::expected, std::string> e1(std::make_unique(42)); - EXPECT_TRUE(e1.has_value()); - EXPECT_EQ(**e1, 42); - - cf::expected, std::string> e2(std::move(e1)); - EXPECT_TRUE(e2.has_value()); - EXPECT_EQ(**e2, 42); - - // unique_ptr as error - cf::expected> e3( - cf::unexpected(std::make_unique("error"))); - EXPECT_FALSE(e3.has_value()); - EXPECT_EQ(*e3.error(), "error"); -} - -TEST(ExpectedTest, ResourceManagement) { - // Use namespace-level counters instead of static members in local struct - static int constructions = 0; - static int destructions = 0; - - struct Counter { - int value; - - Counter(int v) : value(v) { constructions++; } - ~Counter() { destructions++; } - Counter(const Counter& other) : value(other.value) { constructions++; } - Counter(Counter&& other) noexcept : value(other.value) { constructions++; } - Counter& operator=(const Counter&) = default; - Counter& operator=(Counter&&) noexcept = default; - }; - - constructions = 0; - destructions = 0; - - { - cf::expected e(42); - EXPECT_GT(constructions, 0); - } - - EXPECT_GT(destructions, 0); -} - -// ============================================================================= -// Test Suite 10: Const Correctness -// ============================================================================= - -TEST(ExpectedTest, ConstExpectedAccess) { - const cf::expected e(42); - - EXPECT_TRUE(e.has_value()); - EXPECT_EQ(*e, 42); - EXPECT_EQ(e.value(), 42); - EXPECT_EQ(*e.operator->(), 42); -} - -TEST(ExpectedTest, ConstErrorAccess) { - const cf::expected e(cf::unexpected("error")); - - EXPECT_FALSE(e.has_value()); - EXPECT_EQ(e.error(), "error"); -} - -TEST(ExpectedTest, ConstMonadicOperations) { - const cf::expected e(42); - - auto square = [](int x) { return x * x; }; - auto result = e.transform(square); - - EXPECT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1764); -} - -// ============================================================================= -// Test Suite 11: expected Specialization -// ============================================================================= - -TEST(ExpectedVoidTest, DefaultConstructor) { - cf::expected e; - EXPECT_TRUE(e.has_value()); - EXPECT_NO_THROW(e.value()); -} - -TEST(ExpectedVoidTest, FromUnexpected) { - cf::expected e(cf::unexpected("error")); - EXPECT_FALSE(e.has_value()); - EXPECT_EQ(e.error(), "error"); - EXPECT_THROW(e.value(), cf::bad_expected_access); -} - -TEST(ExpectedVoidTest, Copy) { - cf::expected e1; - cf::expected e2 = e1; - EXPECT_TRUE(e2.has_value()); - - cf::expected e3(cf::unexpected("err")); - cf::expected e4 = e3; - EXPECT_FALSE(e4.has_value()); - EXPECT_EQ(e4.error(), "err"); -} - -TEST(ExpectedVoidTest, Assignment) { - cf::expected e1; - cf::expected e2(cf::unexpected("err")); - - e1 = e2; - EXPECT_FALSE(e1.has_value()); - EXPECT_EQ(e1.error(), "err"); -} - -TEST(ExpectedVoidTest, AndThen) { - cf::expected e; - - auto return_int = []() -> cf::expected { return 42; }; - - auto result = e.and_then(return_int); - EXPECT_TRUE(result.has_value()); - EXPECT_EQ(*result, 42); - - cf::expected e_err(cf::unexpected("error")); - auto result_err = e_err.and_then(return_int); - EXPECT_FALSE(result_err.has_value()); - EXPECT_EQ(result_err.error(), "error"); -} - -TEST(ExpectedVoidTest, Transform) { - cf::expected e; - - auto return_int = []() { return 42; }; - auto result = e.transform(return_int); - - EXPECT_TRUE(result.has_value()); - EXPECT_EQ(*result, 42); - - cf::expected e_err(cf::unexpected("error")); - auto result_err = e_err.transform(return_int); - - EXPECT_FALSE(result_err.has_value()); - EXPECT_EQ(result_err.error(), "error"); -} - -// ============================================================================= -// Test Suite 12: Edge Cases and Corner Cases -// ============================================================================= - -TEST(ExpectedEdgeCases, EmptyStringValue) { - cf::expected e(""); - EXPECT_TRUE(e.has_value()); - EXPECT_EQ(e->size(), 0); -} - -TEST(ExpectedEdgeCases, EmptyStringError) { - cf::expected e(cf::unexpected("")); - EXPECT_FALSE(e.has_value()); - EXPECT_EQ(e.error().size(), 0); -} - -TEST(ExpectedEdgeCases, ZeroValues) { - cf::expected e(0); - EXPECT_TRUE(e.has_value()); - EXPECT_EQ(*e, 0); - - cf::expected e2(cf::unexpected(0)); - EXPECT_FALSE(e2.has_value()); - EXPECT_EQ(e2.error(), 0); -} - -TEST(ExpectedEdgeCases, BooleanValues) { - cf::expected e_true(true); - cf::expected e_false(false); - - EXPECT_TRUE(e_true.has_value()); - EXPECT_TRUE(*e_true); - - EXPECT_TRUE(e_false.has_value()); - EXPECT_FALSE(*e_false); - - // bool() checks has_value(), not the contained bool - EXPECT_TRUE(static_cast(e_true)); - EXPECT_TRUE(static_cast(e_false)); -} - -TEST(ExpectedEdgeCases, LargeObjects) { - std::vector large_vec(1000); - for (size_t i = 0; i < large_vec.size(); ++i) { - large_vec[i] = static_cast(i); - } - - cf::expected, std::string> e(std::move(large_vec)); - - EXPECT_TRUE(e.has_value()); - EXPECT_EQ(e->size(), 1000); - EXPECT_EQ((*e)[500], 500); -} - -TEST(ExpectedEdgeCases, MultipleStateChanges) { - cf::expected e(1); - - e = cf::unexpected("error1"); - EXPECT_FALSE(e.has_value()); - - e = 2; - EXPECT_TRUE(e.has_value()); - EXPECT_EQ(*e, 2); - - e = cf::unexpected("error2"); - EXPECT_FALSE(e.has_value()); - EXPECT_EQ(e.error(), "error2"); - - e = 3; - EXPECT_TRUE(e.has_value()); - EXPECT_EQ(*e, 3); -} - -TEST(ExpectedEdgeCases, StructErrorType) { - struct ErrorInfo { - int code; - std::string message; - bool operator==(const ErrorInfo& other) const { - return code == other.code && message == other.message; - } - }; - - cf::expected e(cf::unexpected(ErrorInfo{404, "Not Found"})); - - EXPECT_FALSE(e.has_value()); - EXPECT_EQ(e.error().code, 404); - EXPECT_EQ(e.error().message, "Not Found"); -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/base/hash/constexpr_fnv1a_test.cpp b/test/base/hash/constexpr_fnv1a_test.cpp deleted file mode 100644 index e2f67e0b2..000000000 --- a/test/base/hash/constexpr_fnv1a_test.cpp +++ /dev/null @@ -1,202 +0,0 @@ -/** - * @file constexpr_fnv1a_test.cpp - * @brief Unit tests for compile-time FNV-1a hash - * - * Test Coverage: - * 1. Compile-time hash computation - * 2. User-defined literal "_hash" - * 3. String_view hash - * 4. Hash distribution (different strings, different hashes) - * 5. 32-bit variant - */ - -#include "base/hash/constexpr_fnv1a.hpp" -#include -#include -#include - -using namespace cf::hash; - -// ============================================================================= -// Test Suite 1: Compile-time Hash (64-bit) -// ============================================================================= - -TEST(Fnv1a64Test, CompileTimeHash) { - constexpr uint64_t h1 = fnv1a64("TokenA"); - constexpr uint64_t h2 = fnv1a64("TokenB"); - constexpr uint64_t h3 = fnv1a64("TokenA"); // Same as h1 - - EXPECT_NE(h1, h2); - EXPECT_EQ(h1, h3); -} - -TEST(Fnv1a64Test, UserDefinedLiteral) { - constexpr uint64_t h1 = "testToken"_hash; - constexpr uint64_t h2 = fnv1a64("testToken"); - - EXPECT_EQ(h1, h2); -} - -TEST(Fnv1a64Test, LiteralIsConstexpr) { - // This should compile if the literal is truly constexpr - constexpr uint64_t h = "constexprTest"_hash; - static_assert(h > 0, "Hash should be non-zero"); - EXPECT_GT(h, 0); -} - -TEST(Fnv1a64Test, StringViewHash) { - std::string_view sv = "myToken"; - constexpr uint64_t h1 = "myToken"_hash; - uint64_t h2 = fnv1a64(sv); - - EXPECT_EQ(h1, h2); -} - -TEST(Fnv1a64Test, StringHash) { - std::string s = "myString"; - uint64_t h1 = fnv1a64(s); - uint64_t h2 = fnv1a64(std::string_view(s)); - - EXPECT_EQ(h1, h2); -} - -TEST(Fnv1a64Test, DifferentStringsHaveDifferentHashes) { - constexpr uint64_t h1 = "userId"_hash; - constexpr uint64_t h2 = "userName"_hash; - constexpr uint64_t h3 = "settings"_hash; - constexpr uint64_t h4 = "counter"_hash; - constexpr uint64_t h5 = "config"_hash; - - // All hashes should be different - EXPECT_NE(h1, h2); - EXPECT_NE(h1, h3); - EXPECT_NE(h1, h4); - EXPECT_NE(h1, h5); - EXPECT_NE(h2, h3); - EXPECT_NE(h2, h4); - EXPECT_NE(h2, h5); - EXPECT_NE(h3, h4); - EXPECT_NE(h3, h5); - EXPECT_NE(h4, h5); -} - -TEST(Fnv1a64Test, EmptyString) { - constexpr uint64_t h_empty = ""_hash; - EXPECT_GT(h_empty, 0); - - uint64_t h_empty_sv = fnv1a64(std::string_view("")); - EXPECT_EQ(h_empty, h_empty_sv); -} - -TEST(Fnv1a64Test, SingleCharacter) { - constexpr uint64_t h_a = "a"_hash; - constexpr uint64_t h_b = "b"_hash; - constexpr uint64_t h_A = "A"_hash; - - EXPECT_NE(h_a, h_b); - EXPECT_NE(h_a, h_A); -} - -TEST(Fnv1a64Test, SimilarStringsDifferentHashes) { - constexpr uint64_t h1 = "test"_hash; - constexpr uint64_t h2 = "Test"_hash; - constexpr uint64_t h3 = "TEST"_hash; - constexpr uint64_t h4 = "test "_hash; // trailing space - constexpr uint64_t h5 = " test"_hash; // leading space - - EXPECT_NE(h1, h2); - EXPECT_NE(h1, h3); - EXPECT_NE(h2, h3); - EXPECT_NE(h1, h4); - EXPECT_NE(h1, h5); -} - -TEST(Fnv1a64Test, LongString) { - // Test with a longer string - std::string long_str(1000, 'a'); // 1000 'a' characters - uint64_t h = fnv1a64(long_str); - - EXPECT_GT(h, 0); - - // Different length should give different hash - std::string longer_str(1001, 'a'); - uint64_t h2 = fnv1a64(longer_str); - EXPECT_NE(h, h2); -} - -TEST(Fnv1a64Test, CustomSeed) { - constexpr uint64_t seed = 12345; - constexpr uint64_t h1 = fnv1a64("test", seed); - constexpr uint64_t h2 = fnv1a64("test", seed); - constexpr uint64_t h3 = fnv1a64("test"); // default seed - - EXPECT_EQ(h1, h2); - EXPECT_NE(h1, h3); -} - -// ============================================================================= -// Test Suite 2: 32-bit Variant -// ============================================================================= - -TEST(Fnv1a32Test, CompileTimeHash32) { - constexpr uint32_t h1 = fnv1a32("TokenA"); - constexpr uint32_t h2 = fnv1a32("TokenB"); - constexpr uint32_t h3 = fnv1a32("TokenA"); - - EXPECT_NE(h1, h2); - EXPECT_EQ(h1, h3); -} - -TEST(Fnv1a32Test, StringViewHash32) { - std::string_view sv = "myToken32"; - uint32_t h1 = fnv1a32(sv); - uint32_t h2 = fnv1a32("myToken32"); - - EXPECT_EQ(h1, h2); -} - -TEST(Fnv1a32Test, DifferentFrom64Bit) { - constexpr uint64_t h64 = "test"_hash; - constexpr uint32_t h32 = fnv1a32("test"); - - // 32-bit hash should be different from (at least part of) 64-bit - // (they use different constants so results will differ) - EXPECT_NE(static_cast(h64), h32); -} - -// ============================================================================= -// Test Suite 3: Hash Distribution -// ============================================================================= - -TEST(HashDistribution, NoCollisionsForCommonTokens) { - // Test a set of common token-like names - const char* token_names[] = { - "userId", "userName", "userEmail", "settings", "config", "preferences", "token", - "session", "cache", "request", "response", "error", "id", "name", - "value", "type", "true", "false", "null", "width", "height", - "x", "y", "red", "green", "blue", "alpha"}; - - constexpr size_t num_names = sizeof(token_names) / sizeof(token_names[0]); - uint64_t hashes[num_names]; - - for (size_t i = 0; i < num_names; ++i) { - hashes[i] = fnv1a64(token_names[i]); - } - - // Check no duplicates - for (size_t i = 0; i < num_names; ++i) { - for (size_t j = i + 1; j < num_names; ++j) { - EXPECT_NE(hashes[i], hashes[j]) - << "Collision between " << token_names[i] << " and " << token_names[j]; - } - } -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/base/indexed_vector/indexed_vector_test.cpp b/test/base/indexed_vector/indexed_vector_test.cpp deleted file mode 100644 index aeb107407..000000000 --- a/test/base/indexed_vector/indexed_vector_test.cpp +++ /dev/null @@ -1,609 +0,0 @@ -/** - * @file test/base/indexed_vector/indexed_vector_test.cpp - * @brief Unit tests for cf::indexed_vector. - * - * @author Charliechen114514 - * @date 2026-04-09 - * @version 0.1 - * @since 0.1 - */ - -#include - -#include -#include -#include -#include - -// ============================================================================= -// Test Suite 1: Construction -// ============================================================================= - -TEST(IndexedVectorConstruction, DefaultConstructor) { - cf::indexed_vector v; - EXPECT_TRUE(v.empty()); - EXPECT_EQ(v.cursor(), 0u); - EXPECT_EQ(v.mode(), cf::IndexingMode::Listed); -} - -TEST(IndexedVectorConstruction, CountConstructor) { - cf::indexed_vector v(5); - EXPECT_EQ(v.size(), 5u); - EXPECT_EQ(v.cursor(), 0u); -} - -TEST(IndexedVectorConstruction, CountValueConstructor) { - cf::indexed_vector v(3, 42); - EXPECT_EQ(v.size(), 3u); - for (auto& elem : v) { - EXPECT_EQ(elem, 42); - } -} - -TEST(IndexedVectorConstruction, InitializerListConstructor) { - cf::indexed_vector v{1, 2, 3, 4, 5}; - EXPECT_EQ(v.size(), 5u); - EXPECT_EQ(v[0], 1); - EXPECT_EQ(v[4], 5); - EXPECT_EQ(v.cursor(), 0u); -} - -TEST(IndexedVectorConstruction, CopyConstructor) { - cf::indexed_vector original{10, 20, 30}; - original.set_cursor(2); - original.set_mode(cf::IndexingMode::Recycled); - - cf::indexed_vector copy(original); - EXPECT_EQ(copy.size(), 3u); - EXPECT_EQ(copy.cursor(), 2u); - EXPECT_EQ(copy.mode(), cf::IndexingMode::Recycled); - EXPECT_EQ(copy[1], 20); -} - -TEST(IndexedVectorConstruction, MoveConstructor) { - cf::indexed_vector original{10, 20, 30}; - original.set_cursor(1); - - cf::indexed_vector moved(std::move(original)); - EXPECT_EQ(moved.size(), 3u); - EXPECT_EQ(moved.cursor(), 1u); - EXPECT_EQ(moved[0], 10); -} - -// ============================================================================= -// Test Suite 2: Cursor Access -// ============================================================================= - -TEST(IndexedVectorCursorAccess, CursorDefaultZero) { - cf::indexed_vector v{1, 2, 3}; - EXPECT_EQ(v.cursor(), 0u); -} - -TEST(IndexedVectorCursorAccess, SetCursorValid) { - cf::indexed_vector v{1, 2, 3, 4, 5}; - v.set_cursor(0); - EXPECT_EQ(v.cursor(), 0u); - v.set_cursor(4); - EXPECT_EQ(v.cursor(), 4u); - v.set_cursor(2); - EXPECT_EQ(v.cursor(), 2u); -} - -TEST(IndexedVectorCursorAccess, SetCursorOutOfRange) { - cf::indexed_vector v{1, 2, 3}; - EXPECT_THROW(v.set_cursor(3), std::out_of_range); - EXPECT_THROW(v.set_cursor(100), std::out_of_range); -} - -TEST(IndexedVectorCursorAccess, AtCursorAccess) { - cf::indexed_vector v{10, 20, 30}; - v.set_cursor(1); - EXPECT_EQ(v.at_cursor(), 20); - v.at_cursor() = 99; - EXPECT_EQ(v[1], 99); -} - -TEST(IndexedVectorCursorAccess, AtCursorConst) { - const cf::indexed_vector v{10, 20, 30}; - // cursor is 0 by default - EXPECT_EQ(v.at_cursor(), 10); -} - -TEST(IndexedVectorCursorAccess, AtCursorEmptyThrows) { - cf::indexed_vector v; - EXPECT_THROW(v.at_cursor(), std::out_of_range); -} - -// ============================================================================= -// Test Suite 3: Fixed Mode Behavior -// ============================================================================= - -TEST(IndexedVectorFixedMode, NextNoOp) { - cf::indexed_vector v{1, 2, 3}; - v.set_mode(cf::IndexingMode::Fixed); - v.set_cursor(1); - v.next(); - EXPECT_EQ(v.cursor(), 1u); -} - -TEST(IndexedVectorFixedMode, PrevNoOp) { - cf::indexed_vector v{1, 2, 3}; - v.set_mode(cf::IndexingMode::Fixed); - v.set_cursor(1); - v.prev(); - EXPECT_EQ(v.cursor(), 1u); -} - -TEST(IndexedVectorFixedMode, HasNextHasPrevFalse) { - cf::indexed_vector v{1, 2, 3}; - v.set_mode(cf::IndexingMode::Fixed); - v.set_cursor(1); - EXPECT_FALSE(v.has_next()); - EXPECT_FALSE(v.has_prev()); -} - -TEST(IndexedVectorFixedMode, SetCursorStillWorks) { - cf::indexed_vector v{1, 2, 3}; - v.set_mode(cf::IndexingMode::Fixed); - v.set_cursor(2); - EXPECT_EQ(v.at_cursor(), 3); -} - -// ============================================================================= -// Test Suite 4: Listed Mode Behavior -// ============================================================================= - -TEST(IndexedVectorListedMode, NextAdvances) { - cf::indexed_vector v{1, 2, 3}; - EXPECT_EQ(v.cursor(), 0u); - v.next(); - EXPECT_EQ(v.cursor(), 1u); - v.next(); - EXPECT_EQ(v.cursor(), 2u); -} - -TEST(IndexedVectorListedMode, PrevRetreats) { - cf::indexed_vector v{1, 2, 3}; - v.set_cursor(2); - v.prev(); - EXPECT_EQ(v.cursor(), 1u); - v.prev(); - EXPECT_EQ(v.cursor(), 0u); -} - -TEST(IndexedVectorListedMode, NextAtEndThrows) { - cf::indexed_vector v{1, 2, 3}; - v.set_cursor(2); - EXPECT_THROW(v.next(), std::out_of_range); -} - -TEST(IndexedVectorListedMode, PrevAtStartThrows) { - cf::indexed_vector v{1, 2, 3}; - EXPECT_THROW(v.prev(), std::out_of_range); -} - -TEST(IndexedVectorListedMode, HasNextHasPrev) { - cf::indexed_vector v{1, 2, 3}; - v.set_cursor(1); - EXPECT_TRUE(v.has_next()); - EXPECT_TRUE(v.has_prev()); - - v.set_cursor(0); - EXPECT_TRUE(v.has_next()); - EXPECT_FALSE(v.has_prev()); - - v.set_cursor(2); - EXPECT_FALSE(v.has_next()); - EXPECT_TRUE(v.has_prev()); -} - -TEST(IndexedVectorListedMode, TraversalFullRange) { - cf::indexed_vector v{10, 20, 30}; - EXPECT_EQ(v.at_cursor(), 10); - v.next(); - EXPECT_EQ(v.at_cursor(), 20); - v.next(); - EXPECT_EQ(v.at_cursor(), 30); - v.prev(); - EXPECT_EQ(v.at_cursor(), 20); - v.prev(); - EXPECT_EQ(v.at_cursor(), 10); -} - -// ============================================================================= -// Test Suite 5: Recycled Mode Behavior -// ============================================================================= - -TEST(IndexedVectorRecycledMode, NextWraps) { - cf::indexed_vector v{1, 2, 3}; - v.set_mode(cf::IndexingMode::Recycled); - v.set_cursor(2); - v.next(); - EXPECT_EQ(v.cursor(), 0u); -} - -TEST(IndexedVectorRecycledMode, PrevWraps) { - cf::indexed_vector v{1, 2, 3}; - v.set_mode(cf::IndexingMode::Recycled); - v.prev(); - EXPECT_EQ(v.cursor(), 2u); -} - -TEST(IndexedVectorRecycledMode, SingleElementRecycled) { - cf::indexed_vector v{42}; - v.set_mode(cf::IndexingMode::Recycled); - v.next(); - EXPECT_EQ(v.cursor(), 0u); - EXPECT_EQ(v.at_cursor(), 42); - v.prev(); - EXPECT_EQ(v.cursor(), 0u); - EXPECT_EQ(v.at_cursor(), 42); -} - -TEST(IndexedVectorRecycledMode, RecycledEmptyThrows) { - cf::indexed_vector v; - v.set_mode(cf::IndexingMode::Recycled); - EXPECT_THROW(v.next(), std::out_of_range); - EXPECT_THROW(v.prev(), std::out_of_range); -} - -TEST(IndexedVectorRecycledMode, HasNextHasPrevAlwaysTrue) { - cf::indexed_vector v{1, 2, 3}; - v.set_mode(cf::IndexingMode::Recycled); - v.set_cursor(0); - EXPECT_TRUE(v.has_next()); - EXPECT_TRUE(v.has_prev()); - v.set_cursor(2); - EXPECT_TRUE(v.has_next()); - EXPECT_TRUE(v.has_prev()); -} - -// ============================================================================= -// Test Suite 6: Mode Switching -// ============================================================================= - -TEST(IndexedVectorModeSwitching, SetModeChangesBehavior) { - cf::indexed_vector v{1, 2, 3}; - v.set_cursor(2); - - // Listed: next throws - EXPECT_THROW(v.next(), std::out_of_range); - - // Switch to Recycled: next wraps - v.set_mode(cf::IndexingMode::Recycled); - v.next(); - EXPECT_EQ(v.cursor(), 0u); -} - -TEST(IndexedVectorModeSwitching, ModeReturnsCurrent) { - cf::indexed_vector v; - EXPECT_EQ(v.mode(), cf::IndexingMode::Listed); - v.set_mode(cf::IndexingMode::Recycled); - EXPECT_EQ(v.mode(), cf::IndexingMode::Recycled); - v.set_mode(cf::IndexingMode::Fixed); - EXPECT_EQ(v.mode(), cf::IndexingMode::Fixed); -} - -TEST(IndexedVectorModeSwitching, SetModePreservesCursor) { - cf::indexed_vector v{1, 2, 3}; - v.set_cursor(2); - v.set_mode(cf::IndexingMode::Recycled); - EXPECT_EQ(v.cursor(), 2u); -} - -TEST(IndexedVectorModeSwitching, CompileTimeDefault) { - cf::indexed_vector v_default; - EXPECT_EQ(v_default.mode(), cf::IndexingMode::Listed); - - cf::indexed_vector v_recycled; - EXPECT_EQ(v_recycled.mode(), cf::IndexingMode::Recycled); - - cf::indexed_vector v_fixed; - EXPECT_EQ(v_fixed.mode(), cf::IndexingMode::Fixed); -} - -// ============================================================================= -// Test Suite 7: Cursor on Erase -// ============================================================================= - -TEST(IndexedVectorCursorOnErase, EraseBeforeCursor) { - cf::indexed_vector v{10, 20, 30, 40}; - v.set_cursor(3); - v.erase(v.cbegin() + 1); // erase 20 - EXPECT_EQ(v.cursor(), 2u); // shifted from 3 to 2 - EXPECT_EQ(v.at_cursor(), 40); -} - -TEST(IndexedVectorCursorOnErase, EraseAfterCursor) { - cf::indexed_vector v{10, 20, 30, 40}; - v.set_cursor(1); - v.erase(v.cbegin() + 3); // erase 40 - EXPECT_EQ(v.cursor(), 1u); - EXPECT_EQ(v.at_cursor(), 20); -} - -TEST(IndexedVectorCursorOnErase, EraseAtCursor) { - cf::indexed_vector v{10, 20, 30, 40}; - v.set_cursor(1); - v.erase(v.cbegin() + 1); // erase 20 - EXPECT_EQ(v.cursor(), 1u); // stays at index 1, now points to 30 - EXPECT_EQ(v.at_cursor(), 30); -} - -TEST(IndexedVectorCursorOnErase, EraseLastElementAtCursor) { - cf::indexed_vector v{10, 20, 30}; - v.set_cursor(2); - v.erase(v.cbegin() + 2); // erase 30 - EXPECT_EQ(v.cursor(), 1u); // clamped back - EXPECT_EQ(v.at_cursor(), 20); -} - -TEST(IndexedVectorCursorOnErase, EraseRangeBeforeCursor) { - cf::indexed_vector v{10, 20, 30, 40, 50}; - v.set_cursor(4); - v.erase(v.cbegin() + 1, v.cbegin() + 3); // erase 20, 30 - EXPECT_EQ(v.cursor(), 2u); // 4 - 2 = 2 - EXPECT_EQ(v.at_cursor(), 50); -} - -TEST(IndexedVectorCursorOnErase, EraseRangeIncludingCursor) { - cf::indexed_vector v{10, 20, 30, 40, 50}; - v.set_cursor(2); - v.erase(v.cbegin() + 1, v.cbegin() + 4); // erase 20, 30, 40 - EXPECT_EQ(v.cursor(), 1u); // set to erase start, clamped - EXPECT_EQ(v.at_cursor(), 50); -} - -TEST(IndexedVectorCursorOnErase, PopBackAtCursor) { - cf::indexed_vector v{10, 20, 30}; - v.set_cursor(2); - v.pop_back(); - EXPECT_EQ(v.cursor(), 1u); - EXPECT_EQ(v.at_cursor(), 20); -} - -TEST(IndexedVectorCursorOnErase, PopBackNotAtCursor) { - cf::indexed_vector v{10, 20, 30}; - v.set_cursor(0); - v.pop_back(); - EXPECT_EQ(v.cursor(), 0u); - EXPECT_EQ(v.at_cursor(), 10); -} - -TEST(IndexedVectorCursorOnErase, PopBackLastElement) { - cf::indexed_vector v{42}; - v.pop_back(); - EXPECT_TRUE(v.empty()); - EXPECT_EQ(v.cursor(), 0u); -} - -// ============================================================================= -// Test Suite 8: Cursor on Insert -// ============================================================================= - -TEST(IndexedVectorCursorOnInsert, InsertBeforeCursor) { - cf::indexed_vector v{10, 30, 40}; - v.set_cursor(2); - v.insert(v.cbegin() + 1, 20); // insert before cursor - EXPECT_EQ(v.cursor(), 3u); - EXPECT_EQ(v.at_cursor(), 40); -} - -TEST(IndexedVectorCursorOnInsert, InsertAfterCursor) { - cf::indexed_vector v{10, 20, 40}; - v.set_cursor(1); - v.insert(v.cbegin() + 3, 30); // insert after cursor - EXPECT_EQ(v.cursor(), 1u); - EXPECT_EQ(v.at_cursor(), 20); -} - -TEST(IndexedVectorCursorOnInsert, PushBackNoEffect) { - cf::indexed_vector v{1, 2, 3}; - v.set_cursor(1); - v.push_back(4); - EXPECT_EQ(v.cursor(), 1u); - EXPECT_EQ(v.at_cursor(), 2); -} - -TEST(IndexedVectorCursorOnInsert, EmplaceBackNoEffect) { - cf::indexed_vector v{"a", "b", "c"}; - v.set_cursor(1); - v.emplace_back("d"); - EXPECT_EQ(v.cursor(), 1u); - EXPECT_EQ(v.at_cursor(), "b"); -} - -// ============================================================================= -// Test Suite 9: Cursor on Clear and Resize -// ============================================================================= - -TEST(IndexedVectorCursorClearResize, ClearResetsCursor) { - cf::indexed_vector v{1, 2, 3}; - v.set_cursor(2); - v.clear(); - EXPECT_EQ(v.cursor(), 0u); - EXPECT_TRUE(v.empty()); -} - -TEST(IndexedVectorCursorClearResize, ResizeSmallerAdjustsCursor) { - cf::indexed_vector v{1, 2, 3, 4, 5}; - v.set_cursor(4); - v.resize(3); - EXPECT_EQ(v.cursor(), 2u); // clamped to new_size - 1 -} - -TEST(IndexedVectorCursorClearResize, ResizeLargerNoEffect) { - cf::indexed_vector v{1, 2, 3}; - v.set_cursor(1); - v.resize(10); - EXPECT_EQ(v.cursor(), 1u); -} - -TEST(IndexedVectorCursorClearResize, ResizeToZeroResetsCursor) { - cf::indexed_vector v{1, 2, 3}; - v.set_cursor(2); - v.resize(0); - EXPECT_EQ(v.cursor(), 0u); - EXPECT_TRUE(v.empty()); -} - -// ============================================================================= -// Test Suite 10: Empty Vector Edge Cases -// ============================================================================= - -TEST(IndexedVectorEmptyVector, EmptyVectorNextThrows) { - cf::indexed_vector v; - EXPECT_THROW(v.next(), std::out_of_range); -} - -TEST(IndexedVectorEmptyVector, EmptyVectorPrevThrows) { - cf::indexed_vector v; - EXPECT_THROW(v.prev(), std::out_of_range); -} - -TEST(IndexedVectorEmptyVector, EmptyVectorAtCursorThrows) { - cf::indexed_vector v; - EXPECT_THROW(v.at_cursor(), std::out_of_range); -} - -TEST(IndexedVectorEmptyVector, EmptyVectorSetCursorThrows) { - cf::indexed_vector v; - EXPECT_THROW(v.set_cursor(0), std::out_of_range); -} - -TEST(IndexedVectorEmptyVector, EmptyVectorHasNextPrev) { - cf::indexed_vector v; - EXPECT_FALSE(v.has_next()); - EXPECT_FALSE(v.has_prev()); -} - -// ============================================================================= -// Test Suite 11: Vector API Passthrough -// ============================================================================= - -TEST(IndexedVectorPassthrough, OperatorBracket) { - cf::indexed_vector v{10, 20, 30}; - EXPECT_EQ(v[0], 10); - EXPECT_EQ(v[2], 30); - v[1] = 99; - EXPECT_EQ(v[1], 99); -} - -TEST(IndexedVectorPassthrough, AtMethod) { - cf::indexed_vector v{10, 20, 30}; - EXPECT_EQ(v.at(1), 20); - EXPECT_THROW(v.at(3), std::out_of_range); -} - -TEST(IndexedVectorPassthrough, FrontBack) { - cf::indexed_vector v{10, 20, 30}; - EXPECT_EQ(v.front(), 10); - EXPECT_EQ(v.back(), 30); -} - -TEST(IndexedVectorPassthrough, IteratorRange) { - cf::indexed_vector v{1, 2, 3}; - std::vector copy(v.begin(), v.end()); - EXPECT_EQ(copy.size(), 3u); - EXPECT_EQ(copy[0], 1); - EXPECT_EQ(copy[2], 3); -} - -TEST(IndexedVectorPassthrough, CapacityMethods) { - cf::indexed_vector v; - EXPECT_TRUE(v.empty()); - v.push_back(1); - EXPECT_EQ(v.size(), 1u); - v.reserve(100); - EXPECT_GE(v.capacity(), 100u); - EXPECT_EQ(v.max_size(), v.max_size()); // just verify it compiles -} - -TEST(IndexedVectorPassthrough, Swap) { - cf::indexed_vector a{1, 2}; - a.set_cursor(1); - cf::indexed_vector b{3, 4, 5}; - b.set_cursor(2); - b.set_mode(cf::IndexingMode::Recycled); - - a.swap(b); - EXPECT_EQ(a.size(), 3u); - EXPECT_EQ(a.cursor(), 2u); - EXPECT_EQ(a.mode(), cf::IndexingMode::Recycled); - EXPECT_EQ(b.size(), 2u); - EXPECT_EQ(b.cursor(), 1u); -} - -// ============================================================================= -// Test Suite 12: Comparison -// ============================================================================= - -TEST(IndexedVectorComparison, EqualWhenSame) { - cf::indexed_vector a{1, 2, 3}; - cf::indexed_vector b{1, 2, 3}; - EXPECT_EQ(a, b); -} - -TEST(IndexedVectorComparison, NotEqualDifferentCursor) { - cf::indexed_vector a{1, 2, 3}; - cf::indexed_vector b{1, 2, 3}; - b.set_cursor(2); - EXPECT_NE(a, b); -} - -TEST(IndexedVectorComparison, NotEqualDifferentContent) { - cf::indexed_vector a{1, 2, 3}; - cf::indexed_vector b{1, 2, 4}; - EXPECT_NE(a, b); -} - -TEST(IndexedVectorComparison, NotEqualDifferentMode) { - cf::indexed_vector a{1, 2, 3}; - cf::indexed_vector b{1, 2, 3}; - b.set_mode(cf::IndexingMode::Recycled); - EXPECT_NE(a, b); -} - -// ============================================================================= -// Test Suite 13: Full navigation scenario -// ============================================================================= - -TEST(IndexedVectorScenario, PlaylistNavigation) { - cf::indexed_vector playlist{"song_a", "song_b", "song_c", "song_d"}; - - // Start at first song - EXPECT_EQ(playlist.at_cursor(), "song_a"); - - // Skip to next - playlist.next(); - EXPECT_EQ(playlist.at_cursor(), "song_b"); - - // Skip to last - playlist.next(); - playlist.next(); - EXPECT_EQ(playlist.at_cursor(), "song_d"); - EXPECT_FALSE(playlist.has_next()); - - // Enable loop mode - playlist.set_mode(cf::IndexingMode::Recycled); - playlist.next(); - EXPECT_EQ(playlist.at_cursor(), "song_a"); - - // Go backwards wraps to last - playlist.prev(); - EXPECT_EQ(playlist.at_cursor(), "song_d"); -} - -TEST(IndexedVectorScenario, ImageBrowserWithDeletion) { - cf::indexed_vector images{100, 200, 300, 400}; - images.set_cursor(1); // viewing image 200 - - // Delete current image - images.erase(images.cbegin() + 1); - EXPECT_EQ(images.at_cursor(), 300); // now viewing 300 - - // Delete last image while cursor is on last - images.set_cursor(2); // viewing 400 - images.pop_back(); - EXPECT_EQ(images.at_cursor(), 300); // cursor backed up -} diff --git a/test/base/lockfree/mpsc_queue_test.cpp b/test/base/lockfree/mpsc_queue_test.cpp deleted file mode 100644 index 3018763c8..000000000 --- a/test/base/lockfree/mpsc_queue_test.cpp +++ /dev/null @@ -1,426 +0,0 @@ -/** - * @file mpsc_queue_test.cpp - * @brief Unit tests for MPSC lock-free queue - * - * Test Coverage: - * 1. Single-threaded basic operations (push/pop) - * 2. Empty and full queue behavior - * 3. Batch operations - * 4. Multi-producer single-consumer concurrency - * 5. Move semantics - * 6. Various types (int, struct, std::string) - */ - -#include "base/lockfree/mpsc_queue.hpp" -#include -#include -#include -#include -#include -#include - -using namespace cf::lockfree; - -// ============================================================================= -// Test Types -// ============================================================================= - -struct TestStruct { - int a; - int b; - - TestStruct() : a(0), b(0) {} - TestStruct(int x, int y) : a(x), b(y) {} - - bool operator==(const TestStruct& other) const { return a == other.a && b == other.b; } -}; - -// ============================================================================= -// Test Suite 1: Single-Threaded Basic Operations -// ============================================================================= - -TEST(MpscQueueTest, ConstructAndDestruct) { - MpscQueue queue; - EXPECT_TRUE(queue.empty()); - EXPECT_EQ(queue.size(), 0); - EXPECT_EQ(queue.capacity(), 4); -} - -TEST(MpscQueueTest, PushAndPopSingleElement) { - MpscQueue queue; - - EXPECT_TRUE(queue.tryPush(42)); - EXPECT_FALSE(queue.empty()); - EXPECT_EQ(queue.size(), 1); - - int value; - EXPECT_TRUE(queue.tryPop(value)); - EXPECT_EQ(value, 42); - EXPECT_TRUE(queue.empty()); -} - -TEST(MpscQueueTest, PushPopMultipleElements) { - MpscQueue queue; - - EXPECT_TRUE(queue.tryPush(1)); - EXPECT_TRUE(queue.tryPush(2)); - EXPECT_TRUE(queue.tryPush(3)); - - EXPECT_EQ(queue.size(), 3); - - int value; - EXPECT_TRUE(queue.tryPop(value)); - EXPECT_EQ(value, 1); - - EXPECT_TRUE(queue.tryPop(value)); - EXPECT_EQ(value, 2); - - EXPECT_TRUE(queue.tryPop(value)); - EXPECT_EQ(value, 3); - - EXPECT_TRUE(queue.empty()); -} - -TEST(MpscQueueTest, PopFromEmptyQueue) { - MpscQueue queue; - - int value; - EXPECT_FALSE(queue.tryPop(value)); - EXPECT_TRUE(queue.empty()); -} - -TEST(MpscQueueTest, PushToFullQueue) { - MpscQueue queue; - - EXPECT_TRUE(queue.tryPush(1)); - EXPECT_TRUE(queue.tryPush(2)); - EXPECT_TRUE(queue.tryPush(3)); - EXPECT_TRUE(queue.tryPush(4)); - - // Queue is full with 4 elements, need to consume before pushing more - EXPECT_EQ(queue.size(), 4); - - // Pop one element - int value; - EXPECT_TRUE(queue.tryPop(value)); - EXPECT_EQ(value, 1); - - // Now we can push again - EXPECT_TRUE(queue.tryPush(5)); - EXPECT_EQ(queue.size(), 4); -} - -TEST(MpscQueueTest, RoundtripElements) { - MpscQueue queue; // Larger capacity to fit all elements - - for (int i = 0; i < 100; ++i) { - EXPECT_TRUE(queue.tryPush(int{i})); - } - - int value; - for (int i = 0; i < 100; ++i) { - EXPECT_TRUE(queue.tryPop(value)); - EXPECT_EQ(value, i); - } -} - -// ============================================================================= -// Test Suite 2: Type Tests -// ============================================================================= - -TEST(MpscQueueTest, StructType) { - MpscQueue queue; - - EXPECT_TRUE(queue.tryPush(TestStruct(10, 20))); - - TestStruct value; - EXPECT_TRUE(queue.tryPop(value)); - EXPECT_EQ(value.a, 10); - EXPECT_EQ(value.b, 20); -} - -TEST(MpscQueueTest, StringType) { - MpscQueue queue; - - EXPECT_TRUE(queue.tryPush("hello")); - - std::string value; - EXPECT_TRUE(queue.tryPop(value)); - EXPECT_EQ(value, "hello"); -} - -TEST(MpscQueueTest, MoveSemantics) { - MpscQueue queue; - - std::string str = "move test"; - EXPECT_TRUE(queue.tryPush(std::move(str))); - - // Original string should be in moved-from state - EXPECT_TRUE(str.empty() || !str.empty()); // Either state is valid - - std::string value; - EXPECT_TRUE(queue.tryPop(value)); - EXPECT_EQ(value, "move test"); -} - -// ============================================================================= -// Test Suite 3: Batch Operations -// ============================================================================= - -TEST(MpscQueueTest, PushBatchEmpty) { - MpscQueue queue; - std::vector values; - - size_t pushed = queue.tryPushBatch(cf::span(values)); - EXPECT_EQ(pushed, 0); -} - -TEST(MpscQueueTest, PushBatchMultiple) { - MpscQueue queue; - std::vector values = {1, 2, 3, 4, 5}; - - size_t pushed = queue.tryPushBatch(cf::span(values)); - EXPECT_EQ(pushed, 5); - EXPECT_EQ(queue.size(), 5); - - int value; - for (int expected : values) { - EXPECT_TRUE(queue.tryPop(value)); - EXPECT_EQ(value, expected); - } -} - -TEST(MpscQueueTest, PopBatchEmpty) { - MpscQueue queue; - - int buffer[10]; - size_t popped = queue.tryPopBatch(buffer, 10); - EXPECT_EQ(popped, 0); -} - -TEST(MpscQueueTest, PopBatchMultiple) { - MpscQueue queue; - - for (int i = 0; i < 5; ++i) { - queue.tryPush(int{i}); - } - - int buffer[10]; - size_t popped = queue.tryPopBatch(buffer, 10); - EXPECT_EQ(popped, 5); - - for (size_t i = 0; i < popped; ++i) { - EXPECT_EQ(buffer[i], static_cast(i)); - } -} - -TEST(MpscQueueTest, PopBatchPartial) { - MpscQueue queue; - - for (int i = 0; i < 3; ++i) { - queue.tryPush(int{i}); - } - - int buffer[10]; - size_t popped = queue.tryPopBatch(buffer, 10); - EXPECT_EQ(popped, 3); - - EXPECT_EQ(buffer[0], 0); - EXPECT_EQ(buffer[1], 1); - EXPECT_EQ(buffer[2], 2); -} - -// ============================================================================= -// Test Suite 4: Concurrency Tests -// ============================================================================= - -TEST(MpscQueueTest, SingleProducerSingleConsumer) { - MpscQueue queue; - constexpr int numItems = 10000; - - // Producer thread - std::thread producer([&]() { - for (int i = 0; i < numItems; ++i) { - while (!queue.tryPush(int{i})) { - std::this_thread::yield(); - } - } - }); - - // Consumer thread - std::thread consumer([&]() { - int received = 0; - int value; - while (received < numItems) { - if (queue.tryPop(value)) { - EXPECT_EQ(value, received); - ++received; - } else { - std::this_thread::yield(); - } - } - }); - - producer.join(); - consumer.join(); - - EXPECT_TRUE(queue.empty()); -} - -TEST(MpscQueueTest, MultiProducerSingleConsumer) { - MpscQueue queue; - constexpr int numProducers = 4; - constexpr int itemsPerProducer = 2500; - constexpr int totalItems = numProducers * itemsPerProducer; - - std::atomic receivedCount{0}; - std::atomic sum{0}; - - // Producer threads - std::vector producers; - for (int p = 0; p < numProducers; ++p) { - producers.emplace_back([&, p]() { - for (int i = 0; i < itemsPerProducer; ++i) { - int value = p * 1000 + i; - while (!queue.tryPush(int{value})) { - std::this_thread::yield(); - } - } - }); - } - - // Consumer thread - std::thread consumer([&]() { - int value; - while (receivedCount.load() < totalItems) { - if (queue.tryPop(value)) { - sum.fetch_add(value); - receivedCount.fetch_add(1); - } else { - std::this_thread::yield(); - } - } - }); - - for (auto& t : producers) { - t.join(); - } - consumer.join(); - - EXPECT_EQ(receivedCount.load(), totalItems); - - // Calculate expected sum - int expectedSum = 0; - for (int p = 0; p < numProducers; ++p) { - for (int i = 0; i < itemsPerProducer; ++i) { - expectedSum += p * 1000 + i; - } - } - EXPECT_EQ(sum.load(), expectedSum); -} - -TEST(MpscQueueTest, StressTestMultiProducer) { - auto queue = std::make_unique>(); - constexpr int numProducers = 8; - constexpr int itemsPerProducer = 5000; - constexpr int totalItems = numProducers * itemsPerProducer; - - std::atomic receivedCount{0}; - - // Producer threads - std::vector producers; - for (int p = 0; p < numProducers; ++p) { - producers.emplace_back([&, p]() { - for (int i = 0; i < itemsPerProducer; ++i) { - while (!queue->tryPush(p * itemsPerProducer + i)) { - std::this_thread::yield(); - } - } - }); - } - - // Consumer thread - std::thread consumer([&]() { - int value; - while (receivedCount.load() < totalItems) { - if (queue->tryPop(value)) { - receivedCount.fetch_add(1); - } else { - std::this_thread::yield(); - } - } - }); - - for (auto& t : producers) { - t.join(); - } - consumer.join(); - - EXPECT_EQ(receivedCount.load(), totalItems); - EXPECT_TRUE(queue->empty()); -} - -// ============================================================================= -// Test Suite 5: Edge Cases -// ============================================================================= - -TEST(MpscQueueTest, CapacityIsPowerOfTwo) { - MpscQueue q1; // 2^0 - MpscQueue q2; // 2^1 - MpscQueue q4; // 2^2 - MpscQueue q8; // 2^3 - MpscQueue q16; // 2^4 - auto q1024 = std::make_unique>(); - auto q65536 = std::make_unique>(); - - EXPECT_EQ(q1.capacity(), 1); - EXPECT_EQ(q2.capacity(), 2); - EXPECT_EQ(q4.capacity(), 4); - EXPECT_EQ(q8.capacity(), 8); - EXPECT_EQ(q16.capacity(), 16); - EXPECT_EQ(q1024->capacity(), 1024); - EXPECT_EQ(q65536->capacity(), 65536); -} - -TEST(MpscQueueTest, LargeQueue) { - auto largeQueue = std::make_unique>(); - ; - - // Fill partially - for (int i = 0; i < 1000; ++i) { - EXPECT_TRUE(largeQueue->tryPush(int{i})); - } - - EXPECT_EQ(largeQueue->size(), 1000); - - // Drain - int value; - for (int i = 0; i < 1000; ++i) { - EXPECT_TRUE(largeQueue->tryPop(value)); - EXPECT_EQ(value, i); - } - - EXPECT_TRUE(largeQueue->empty()); -} - -TEST(MpscQueueTest, AlternatingPushPop) { - MpscQueue queue; - - for (int round = 0; round < 10; ++round) { - EXPECT_TRUE(queue.tryPush(int{round})); - int value; - EXPECT_TRUE(queue.tryPop(value)); - EXPECT_EQ(value, round); - } - - EXPECT_TRUE(queue.empty()); -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/base/policy_chain/policy_chain_test.cpp b/test/base/policy_chain/policy_chain_test.cpp deleted file mode 100644 index fe7b7a754..000000000 --- a/test/base/policy_chain/policy_chain_test.cpp +++ /dev/null @@ -1,464 +0,0 @@ -/** - * @file policy_chain_test.cpp - * @brief Comprehensive unit tests for cf::PolicyChain using GoogleTest - * - * Test Coverage: - * 1. Basic Construction and Operations - * 2. Fallback Mechanism - * 3. Multiple Arguments - * 4. Factory Functions - * 5. Builder API - * 6. Edge Cases - * 7. Empty Chain Behavior - */ - -#include "base/policy_chain/policy_chain.hpp" -#include -#include -#include -#include - -// ============================================================================= -// Test Suite 1: Basic Construction and Operations -// ============================================================================= - -TEST(PolicyChainTest, DefaultConstructor) { - cf::PolicyChain chain; - - EXPECT_TRUE(chain.empty()); - EXPECT_EQ(chain.size(), 0); - EXPECT_FALSE(chain.execute(42).has_value()); -} - -TEST(PolicyChainTest, AddFront) { - cf::PolicyChain chain; - - chain.add_front([](int x) -> std::optional { return x * 2; }); - - EXPECT_FALSE(chain.empty()); - EXPECT_EQ(chain.size(), 1); - EXPECT_EQ(chain.execute(5), 10); -} - -TEST(PolicyChainTest, AddBack) { - cf::PolicyChain chain; - - chain.add_back([](int x) -> std::optional { return x * 2; }); - - EXPECT_FALSE(chain.empty()); - EXPECT_EQ(chain.size(), 1); - EXPECT_EQ(chain.execute(5), 10); -} - -TEST(PolicyChainTest, Clear) { - cf::PolicyChain chain; - - chain.add_back([](int) -> std::optional { return 1; }); - chain.add_back([](int) -> std::optional { return 2; }); - - EXPECT_EQ(chain.size(), 2); - - chain.clear(); - - EXPECT_TRUE(chain.empty()); - EXPECT_EQ(chain.size(), 0); - EXPECT_FALSE(chain.execute(0).has_value()); -} - -TEST(PolicyChainTest, FunctionCallOperator) { - cf::PolicyChain chain; - - chain.add_back([](int x) -> std::optional { return x + 10; }); - - // Test operator() overload - auto result = chain(5); - EXPECT_TRUE(result.has_value()); - EXPECT_EQ(*result, 15); -} - -// ============================================================================= -// Test Suite 2: Fallback Mechanism -// ============================================================================= - -TEST(PolicyChainTest, Fallback_SecondPolicy) { - cf::PolicyChain chain; - - // First policy fails for negative input - chain.add_front([](int x) -> std::optional { - if (x > 0) - return x * 2; - return std::nullopt; - }); - - // Second policy handles negative input - chain.add_back([](int x) -> std::optional { - if (x < 0) - return -x; - return std::nullopt; - }); - - EXPECT_EQ(chain.execute(5), 10); // First policy - EXPECT_EQ(chain.execute(-3), 3); // Fallback to second policy -} - -TEST(PolicyChainTest, Fallback_MultiplePolicies) { - cf::PolicyChain chain; - - chain.add_front([](int x) -> std::optional { - if (x > 100) - return x; - return std::nullopt; - }); - - chain.add_back([](int x) -> std::optional { - if (x > 50) - return x * 2; - return std::nullopt; - }); - - chain.add_back([](int x) -> std::optional { - if (x > 0) - return x + 10; - return std::nullopt; - }); - - chain.add_back([](int) -> std::optional { - return 0; // Default fallback - }); - - EXPECT_EQ(chain.execute(150), 150); // First policy - EXPECT_EQ(chain.execute(75), 150); // Second policy: 75 * 2 - EXPECT_EQ(chain.execute(25), 35); // Third policy: 25 + 10 - EXPECT_EQ(chain.execute(-5), 0); // Fourth policy (default) -} - -TEST(PolicyChainTest, Fallback_AllFail) { - cf::PolicyChain chain; - - chain.add_front([](int) -> std::optional { return std::nullopt; }); - - chain.add_back([](int) -> std::optional { return std::nullopt; }); - - auto result = chain.execute(42); - EXPECT_FALSE(result.has_value()); -} - -TEST(PolicyChainTest, Fallback_Order_AddFront) { - cf::PolicyChain chain; - - chain.add_front([](int) -> std::optional { return 1; }); - chain.add_front([](int) -> std::optional { return 2; }); - chain.add_front([](int) -> std::optional { return 3; }); - - // Last add_front is at the head (highest priority) - EXPECT_EQ(chain.execute(0), 3); -} - -TEST(PolicyChainTest, Fallback_Order_AddBack) { - cf::PolicyChain chain; - - chain.add_back([](int) -> std::optional { return 1; }); - chain.add_back([](int) -> std::optional { return 2; }); - chain.add_back([](int) -> std::optional { return 3; }); - - // First add_back is at the head (highest priority) - EXPECT_EQ(chain.execute(0), 1); -} - -// ============================================================================= -// Test Suite 3: Multiple Arguments -// ============================================================================= - -TEST(PolicyChainTest, MultipleArguments_TwoArgs) { - cf::PolicyChain chain; - - chain.add_back([](int a, int b) -> std::optional { - if (b != 0) - return a / b; - return std::nullopt; - }); - - chain.add_back([](int a, int) -> std::optional { return a * 2; }); - - EXPECT_EQ(chain.execute(10, 2), 5); - EXPECT_EQ(chain.execute(10, 0), 20); -} - -TEST(PolicyChainTest, MultipleArguments_ThreeArgs) { - cf::PolicyChain chain; - - chain.add_back([](int a, int b, int c) -> std::optional { return a + b + c; }); - - EXPECT_EQ(chain.execute(1, 2, 3), 6); -} - -TEST(PolicyChainTest, MultipleArguments_StringRef) { - cf::PolicyChain chain; - - chain.add_back([](const std::string& s) -> std::optional { - if (!s.empty()) - return static_cast(s.length()); - return std::nullopt; - }); - - chain.add_back([](const std::string&) -> std::optional { return -1; }); - - EXPECT_EQ(chain.execute("hello"), 5); - EXPECT_EQ(chain.execute(""), -1); -} - -// ============================================================================= -// Test Suite 4: Factory Functions -// ============================================================================= - -TEST(PolicyChainTest, MakePolicyChain_Basic) { - auto chain = cf::make_policy_chain( - [](int x) -> std::optional { - if (x > 0) - return x * 2; - return std::nullopt; - }, - [](int x) -> std::optional { return -x; }); - - EXPECT_EQ(chain.execute(5), 10); - EXPECT_EQ(chain.execute(-3), 3); -} - -TEST(PolicyChainTest, MakePolicyChain_MultiplePolicies) { - auto chain = cf::make_policy_chain( - [](const std::string& s) -> std::optional { - try { - return std::stoi(s); - } catch (...) { - return std::nullopt; - } - }, - [](const std::string& s) -> std::optional { - if (!s.empty()) - return static_cast(s.length()); - return std::nullopt; - }, - [](const std::string&) -> std::optional { return -1; }); - - EXPECT_EQ(chain.execute("123"), 123); - EXPECT_EQ(chain.execute("hello"), 5); - EXPECT_EQ(chain.execute(""), -1); -} - -// ============================================================================= -// Test Suite 5: Builder API -// ============================================================================= - -TEST(PolicyChainTest, Builder_Basic) { - auto chain = cf::policy_chain_builder() - .then([](int x) -> std::optional { - if (x > 0) - return x * 2; - return std::nullopt; - }) - .then([](int x) -> std::optional { return -x; }) - .build(); - - EXPECT_EQ(chain.execute(5), 10); - EXPECT_EQ(chain.execute(-3), 3); -} - -TEST(PolicyChainTest, Builder_ChainedThen) { - auto chain = cf::policy_chain_builder() - .then([](double x) -> std::optional { - if (x >= 0.0) - return static_cast(x + 0.5); - return std::nullopt; - }) - .then([](double x) -> std::optional { return static_cast(x); }) - .then([](double) -> std::optional { return 0; }) - .build(); - - EXPECT_EQ(chain.execute(3.7), 4); - EXPECT_EQ(chain.execute(-2.3), -2); -} - -TEST(PolicyChainTest, Builder_MoveBuild) { - auto builder = - cf::policy_chain_builder().then([](int x) -> std::optional { return x; }); - - auto chain = std::move(builder).build(); - EXPECT_EQ(chain.execute(42), 42); -} - -TEST(PolicyChainTest, Builder_CopyBuild) { - auto builder = - cf::policy_chain_builder().then([](int x) -> std::optional { return x; }); - - auto chain = builder.build(); - EXPECT_EQ(chain.execute(42), 42); -} - -// ============================================================================= -// Test Suite 6: Edge Cases -// ============================================================================= - -TEST(PolicyChainEdgeCases, EmptyChain) { - cf::PolicyChain chain; - - auto result = chain.execute(42); - EXPECT_FALSE(result.has_value()); -} - -TEST(PolicyChainEdgeCases, SinglePolicy) { - cf::PolicyChain chain; - - chain.add_back([](int x) -> std::optional { return x + 1; }); - - EXPECT_EQ(chain.execute(5), 6); -} - -TEST(PolicyChainEdgeCases, OptionalBool) { - cf::PolicyChain chain; - - chain.add_back([](int x) -> std::optional { - if (x > 100) - return true; - return std::nullopt; - }); - - chain.add_back([](int x) -> std::optional { - if (x > 50) - return false; - return std::nullopt; - }); - - chain.add_back([](int) -> std::optional { return false; }); - - EXPECT_EQ(chain.execute(150), true); - EXPECT_EQ(chain.execute(75), false); - EXPECT_EQ(chain.execute(10), false); -} - -TEST(PolicyChainEdgeCases, MoveOnlyReturnType) { - cf::PolicyChain, int> chain; - - chain.add_back( - [](int x) -> std::optional> { return std::make_unique(x * 2); }); - - auto result = chain.execute(5); - EXPECT_TRUE(result.has_value()); - EXPECT_EQ(**result, 10); -} - -TEST(PolicyChainEdgeCases, StringReturnType) { - cf::PolicyChain chain; - - chain.add_back([](int x) -> std::optional { - if (x > 0) - return "positive"; - return std::nullopt; - }); - - chain.add_back([](int x) -> std::optional { - if (x < 0) - return "negative"; - return std::nullopt; - }); - - chain.add_back([](int) -> std::optional { return "zero"; }); - - EXPECT_EQ(chain.execute(5), "positive"); - EXPECT_EQ(chain.execute(-5), "negative"); - EXPECT_EQ(chain.execute(0), "zero"); -} - -TEST(PolicyChainEdgeCases, ConstPolicyChain) { - cf::PolicyChain chain; - - chain.add_back([](int x) -> std::optional { return x * 2; }); - - const auto& const_chain = chain; - auto result = const_chain.execute(5); - EXPECT_EQ(result, 10); -} - -TEST(PolicyChainEdgeCases, PolicyWithSideEffects) { - int call_count = 0; - - cf::PolicyChain chain; - - chain.add_back([&call_count](int x) -> std::optional { - ++call_count; - if (x > 0) - return x; - return std::nullopt; - }); - - chain.add_back([&call_count](int x) -> std::optional { - ++call_count; - return -x; - }); - - call_count = 0; - EXPECT_EQ(chain.execute(5), 5); - EXPECT_EQ(call_count, 1); // Only first policy called - - call_count = 0; - EXPECT_EQ(chain.execute(-5), 5); - EXPECT_EQ(call_count, 2); // Both policies called -} - -// ============================================================================= -// Test Suite 7: Range-based Iteration -// ============================================================================= - -TEST(PolicyChainTest, RangeBasedFor) { - cf::PolicyChain chain; - - chain.add_back([](int) -> std::optional { return 1; }); - chain.add_back([](int) -> std::optional { return 2; }); - chain.add_back([](int) -> std::optional { return 3; }); - - int count = 0; - for (const auto& policy : chain) { - (void)policy; // Suppress unused warning - ++count; - } - - EXPECT_EQ(count, 3); -} - -TEST(PolicyChainTest, BeginEnd) { - cf::PolicyChain chain; - - chain.add_back([](int) -> std::optional { return 1; }); - chain.add_back([](int) -> std::optional { return 2; }); - - auto begin = chain.begin(); - auto end = chain.end(); - - EXPECT_NE(begin, end); - EXPECT_EQ(chain.size(), std::distance(begin, end)); -} - -// ============================================================================= -// Test Suite 8: Type Traits -// ============================================================================= - -TEST(PolicyChainTest, TypeTraits) { - using Chain = cf::PolicyChain; - - EXPECT_TRUE((std::is_copy_constructible_v)); - EXPECT_TRUE((std::is_move_constructible_v)); - EXPECT_TRUE((std::is_copy_assignable_v)); - EXPECT_TRUE((std::is_move_assignable_v)); - - EXPECT_TRUE((std::is_same_v)); - EXPECT_TRUE((std::is_same_v(const std::string&)>>)); -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/base/scope_guard/scope_guard_test.cpp b/test/base/scope_guard/scope_guard_test.cpp deleted file mode 100644 index f96cfb276..000000000 --- a/test/base/scope_guard/scope_guard_test.cpp +++ /dev/null @@ -1,656 +0,0 @@ -/** - * @file scope_guard_test.cpp - * @brief Comprehensive unit tests for cf::ScopeGuard using GoogleTest - * - * Test Coverage: - * 1. Basic Functionality - cleanup on scope exit - * 2. dismiss() - canceling cleanup - * 3. Non-copyable and Non-movable Guarantees - * 4. Exception Safety - * 5. Multiple Guards in Same Scope - * 6. Move-only Capture in Lambda - * 7. Order of Destruction (LIFO) - * 8. Interaction with Control Flow (return, break, continue, throw) - * 9. Empty/No-op Functions - * 10. Complex Capture Scenarios - * 11. Resource Management Patterns - * 12. Edge Cases - * 13. Real-world Usage Patterns - */ - -#include "base/scope_guard/scope_guard.hpp" -#include -#include -#include -#include -#include - -// ============================================================================= -// Test Suite 1: Basic Functionality -// ============================================================================= - -TEST(ScopeGuardTest, BasicCleanup) { - int counter = 0; - - { - cf::ScopeGuard guard([&counter]() { counter = 42; }); - } - - EXPECT_EQ(counter, 42); -} - -TEST(ScopeGuardTest, CleanupExecutesOnce) { - int counter = 0; - - { - cf::ScopeGuard guard([&counter]() { counter++; }); - } - - EXPECT_EQ(counter, 1); -} - -TEST(ScopeGuardTest, ModifiesCapturedVariable) { - std::string result = "initial"; - - { - cf::ScopeGuard guard([&result]() { result = "cleaned"; }); - } - - EXPECT_EQ(result, "cleaned"); -} - -TEST(ScopeGuardTest, MultipleActionsInLambda) { - int a = 0, b = 0, c = 0; - - { - cf::ScopeGuard guard([&a, &b, &c]() { - a = 1; - b = 2; - c = 3; - }); - } - - EXPECT_EQ(a, 1); - EXPECT_EQ(b, 2); - EXPECT_EQ(c, 3); -} - -// ============================================================================= -// Test Suite 2: dismiss() Functionality -// ============================================================================= - -TEST(ScopeGuardTest, DismissPreventsCleanup) { - int counter = 0; - - { - cf::ScopeGuard guard([&counter]() { counter = 42; }); - guard.dismiss(); - } - - EXPECT_EQ(counter, 0); -} - -TEST(ScopeGuardTest, DismissMultipleTimes) { - int counter = 0; - - { - cf::ScopeGuard guard([&counter]() { counter = 42; }); - guard.dismiss(); - guard.dismiss(); // Safe to call multiple times - } - - EXPECT_EQ(counter, 0); -} - -TEST(ScopeGuardTest, DismissConditional) { - int counter = 0; - bool condition = true; - - { - cf::ScopeGuard guard([&counter]() { counter = 42; }); - if (condition) { - guard.dismiss(); - } - } - - EXPECT_EQ(counter, 0); -} - -// ============================================================================= -// Test Suite 3: Non-copyable and Non-movable Guarantees -// ============================================================================= - -TEST(ScopeGuardTest, IsNotCopyConstructible) { - // Compile-time test: ScopeGuard should not be copy constructible - EXPECT_FALSE((std::is_copy_constructible_v)); - EXPECT_FALSE((std::is_copy_assignable_v)); -} - -// ============================================================================= -// Test Suite 4: Exception Safety -// ============================================================================= - -TEST(ScopeGuardTest, CleanupWhenExceptionThrown) { - int counter = 0; - - try { - cf::ScopeGuard guard([&counter]() { counter = 42; }); - throw std::runtime_error("test exception"); - } catch (...) { - // Exception caught - } - - EXPECT_EQ(counter, 42); -} - -TEST(ScopeGuardTest, CleanupFunctionThrows) { - // When cleanup throws, it should propagate - EXPECT_THROW({ cf::ScopeGuard guard([]() { throw std::runtime_error("cleanup error"); }); }, - std::runtime_error); -} - -TEST(ScopeGuardTest, ExceptionInGuardedCode) { - int cleanup_called = 0; - - try { - cf::ScopeGuard guard([&cleanup_called]() { cleanup_called++; }); - throw std::runtime_error("error"); - } catch (...) { - // Exception from the guarded code - } - - EXPECT_EQ(cleanup_called, 1); -} - -// ============================================================================= -// Test Suite 5: Multiple Guards in Same Scope -// ============================================================================= - -TEST(ScopeGuardTest, MultipleGuardsAllExecute) { - int counter1 = 0, counter2 = 0, counter3 = 0; - - { - cf::ScopeGuard guard1([&counter1]() { counter1 = 1; }); - cf::ScopeGuard guard2([&counter2]() { counter2 = 2; }); - cf::ScopeGuard guard3([&counter3]() { counter3 = 3; }); - } - - EXPECT_EQ(counter1, 1); - EXPECT_EQ(counter2, 2); - EXPECT_EQ(counter3, 3); -} - -TEST(ScopeGuardTest, MultipleGuardsWithSomeDismissed) { - int counter1 = 0, counter2 = 0, counter3 = 0; - - { - cf::ScopeGuard guard1([&counter1]() { counter1 = 1; }); - cf::ScopeGuard guard2([&counter2]() { counter2 = 2; }); - cf::ScopeGuard guard3([&counter3]() { counter3 = 3; }); - - guard2.dismiss(); - } - - EXPECT_EQ(counter1, 1); - EXPECT_EQ(counter2, 0); - EXPECT_EQ(counter3, 3); -} - -// ============================================================================= -// Test Suite 6: Move-only Capture in Lambda -// ============================================================================= -// NOTE: Current ScopeGuard implementation uses std::function which requires -// copyable callables. Move-only captures like unique_ptr by value are not -// supported. Capturing by reference works fine. - -TEST(ScopeGuardTest, CaptureUniquePtrByReference) { - auto ptr = std::make_unique(42); - int result = 0; - - { - cf::ScopeGuard guard([&ptr, &result]() { - if (ptr) - result = *ptr; - }); - } - - EXPECT_EQ(result, 42); -} - -// ============================================================================= -// Test Suite 7: Order of Destruction (LIFO) -// ============================================================================= - -TEST(ScopeGuardTest, LIFODestructionOrder) { - std::vector order; - - { - cf::ScopeGuard guard1([&order]() { order.push_back(1); }); - cf::ScopeGuard guard2([&order]() { order.push_back(2); }); - cf::ScopeGuard guard3([&order]() { order.push_back(3); }); - } - - ASSERT_EQ(order.size(), 3); - EXPECT_EQ(order[0], 3); // Last created, first destroyed - EXPECT_EQ(order[1], 2); - EXPECT_EQ(order[2], 1); // First created, last destroyed -} - -TEST(ScopeGuardTest, NestedScopesOrder) { - std::vector order; - - { - cf::ScopeGuard outer1([&order]() { order.push_back(1); }); - - { - cf::ScopeGuard inner([&order]() { order.push_back(2); }); - } // inner executes here - - cf::ScopeGuard outer2([&order]() { order.push_back(3); }); - } // outer2 executes, then outer1 - - ASSERT_EQ(order.size(), 3); - EXPECT_EQ(order[0], 2); // Inner scope guard executes first - EXPECT_EQ(order[1], 3); - EXPECT_EQ(order[2], 1); -} - -// ============================================================================= -// Test Suite 8: Interaction with Control Flow -// ============================================================================= - -TEST(ScopeGuardTest, WithEarlyReturn) { - int counter = 0; - - auto func = [&counter]() -> void { - cf::ScopeGuard guard([&counter]() { counter++; }); - return; // Early return - }; - - func(); - - EXPECT_EQ(counter, 1); -} - -TEST(ScopeGuardTest, WithMultipleReturns) { - int counter = 0; - - auto func = [&counter](bool condition) -> void { - cf::ScopeGuard guard([&counter]() { counter++; }); - if (condition) { - return; - } - counter = 100; // Won't execute if condition is true - }; - - func(true); - EXPECT_EQ(counter, 1); - - counter = 0; - func(false); - EXPECT_EQ(counter, 101); // 100 from code + 1 from cleanup -} - -TEST(ScopeGuardTest, WithBreakStatement) { - int counter = 0; - - for (int i = 0; i < 10; ++i) { - cf::ScopeGuard guard([&counter]() { counter++; }); - if (i == 2) - break; - } - - EXPECT_EQ(counter, 3); // Iterations 0, 1, 2 -} - -TEST(ScopeGuardTest, WithContinueStatement) { - int counter = 0; - - for (int i = 0; i < 3; ++i) { - cf::ScopeGuard guard([&counter]() { counter++; }); - if (i < 2) - continue; - } - - EXPECT_EQ(counter, 3); // All 3 iterations -} - -TEST(ScopeGuardTest, WithGotoStatement) { - int counter = 0; - - auto func = [&counter]() -> void { - cf::ScopeGuard guard([&counter]() { counter++; }); - goto end; - counter = 100; // Skipped - end:; - }; - - func(); - - EXPECT_EQ(counter, 1); -} - -// ============================================================================= -// Test Suite 9: Empty/No-op Functions -// ============================================================================= - -TEST(ScopeGuardTest, EmptyLambda) { - // Empty lambda should be safe - { - cf::ScopeGuard guard([]() {}); - } - SUCCEED(); -} - -TEST(ScopeGuardTest, NoopGuard) { - int counter = 0; - - { - cf::ScopeGuard guard([&counter]() { - // Do nothing - }); - } - - EXPECT_EQ(counter, 0); -} - -// ============================================================================= -// Test Suite 10: Complex Capture Scenarios -// ============================================================================= - -TEST(ScopeGuardTest, CaptureThisPointer) { - struct MyClass { - int value = 0; - - void method() { - cf::ScopeGuard guard([this]() { this->value = 42; }); - } - }; - - MyClass obj; - obj.method(); - - EXPECT_EQ(obj.value, 42); -} - -TEST(ScopeGuardTest, CaptureByValueAndReference) { - int a = 1, b = 2; - int result_a = 0, result_b = 0; - - { - cf::ScopeGuard guard([a, &b, &result_a, &result_b]() { - result_a = a; - result_b = b; - b = 999; // Modify reference - }); - } - - EXPECT_EQ(result_a, 1); - EXPECT_EQ(result_b, 2); - EXPECT_EQ(b, 999); -} - -TEST(ScopeGuardTest, MixedCaptureWithInitializer) { - int x = 10; - int result = 0; - - { - cf::ScopeGuard guard([y = x + 5, &result]() { result = y; }); - } - - EXPECT_EQ(result, 15); -} - -TEST(ScopeGuardTest, CaptureConstReference) { - const int value = 42; - int result = 0; - - { - cf::ScopeGuard guard([&value, &result]() { result = value; }); - } - - EXPECT_EQ(result, 42); -} - -// ============================================================================= -// Test Suite 11: Resource Management Patterns -// ============================================================================= - -TEST(ScopeGuardTest, FileHandlePattern) { - bool closed = false; - - { - cf::ScopeGuard close_guard([&closed]() { closed = true; }); - EXPECT_FALSE(closed); - } - - EXPECT_TRUE(closed); -} - -TEST(ScopeGuardTest, RollbackPattern) { - int state = 0; - - { - cf::ScopeGuard rollback([&state]() { state = 0; }); - - state = 100; - - // Commit successful - dismiss rollback - rollback.dismiss(); - } - - EXPECT_EQ(state, 100); -} - -TEST(ScopeGuardTest, RollbackOnException) { - int state = 0; - - try { - cf::ScopeGuard rollback([&state]() { state = 0; }); - - state = 100; - - throw std::runtime_error("operation failed"); - - // rollback.dismiss(); // Never reached - } catch (...) { - // Exception handled - } - - EXPECT_EQ(state, 0); -} - -// ============================================================================= -// Test Suite 12: Edge Cases -// ============================================================================= - -TEST(ScopeGuardTest, GuardInLoop) { - int counter = 0; - - for (int i = 0; i < 5; ++i) { - cf::ScopeGuard guard([&counter]() { counter++; }); - } - - EXPECT_EQ(counter, 5); -} - -TEST(ScopeGuardTest, GuardWithStaticVariable) { - static int counter = 0; - - { - cf::ScopeGuard guard([]() { counter++; }); - } - - EXPECT_EQ(counter, 1); - counter = 0; // Reset -} - -TEST(ScopeGuardTest, LargeLambdaCapture) { - std::vector large_vec(1000); - for (size_t i = 0; i < large_vec.size(); ++i) { - large_vec[i] = static_cast(i); - } - - int sum = 0; - - { - cf::ScopeGuard guard([large_vec, &sum]() { - for (int v : large_vec) { - sum += v; - } - }); - } - - int expected = 999 * 1000 / 2; // Sum of 0 to 999 - EXPECT_EQ(sum, expected); -} - -TEST(ScopeGuardTest, NestedScopeGuards) { - int outer = 0, inner = 0; - - { - cf::ScopeGuard outer_guard([&outer]() { outer = 1; }); - - { - cf::ScopeGuard inner_guard([&inner]() { inner = 2; }); - } - - EXPECT_EQ(inner, 2); - EXPECT_EQ(outer, 0); - } - - EXPECT_EQ(outer, 1); -} - -TEST(ScopeGuardTest, StdFunctionCompatibility) { - int counter = 0; - - std::function func = [&counter]() { counter = 42; }; - - { cf::ScopeGuard guard(func); } - - EXPECT_EQ(counter, 42); -} - -// ============================================================================= -// Test Suite 13: Real-world Usage Patterns -// ============================================================================= - -TEST(ScopeGuardTest, LockGuardPattern) { - bool locked = true; - - { - cf::ScopeGuard unlock_guard([&locked]() { locked = false; }); - EXPECT_TRUE(locked); - } - - EXPECT_FALSE(locked); -} - -TEST(ScopeGuardTest, PointerResetPattern) { - int* ptr = new int(42); - bool deleted = false; - - { - cf::ScopeGuard delete_guard([&ptr, &deleted]() { - delete ptr; - ptr = nullptr; - deleted = true; - }); - - EXPECT_FALSE(deleted); - EXPECT_NE(ptr, nullptr); - } - - EXPECT_TRUE(deleted); - EXPECT_EQ(ptr, nullptr); -} - -TEST(ScopeGuardTest, TransactionPattern) { - struct Transaction { - bool committed = false; - bool rolled_back = false; - - void commit() { committed = true; } - void rollback() { rolled_back = true; } - }; - - Transaction txn; - - { - cf::ScopeGuard guard([&txn]() { - if (!txn.committed) { - txn.rollback(); - } - }); - - txn.commit(); - guard.dismiss(); - } - - EXPECT_TRUE(txn.committed); - EXPECT_FALSE(txn.rolled_back); -} - -TEST(ScopeGuardTest, TransactionFailureRollback) { - struct Transaction { - bool committed = false; - bool rolled_back = false; - - void commit() { committed = true; } - void rollback() { rolled_back = true; } - }; - - Transaction txn; - - { - cf::ScopeGuard guard([&txn]() { - if (!txn.committed) { - txn.rollback(); - } - }); - - // Simulate failure - don't commit - // guard.dismiss(); // Not called - } - - EXPECT_FALSE(txn.committed); - EXPECT_TRUE(txn.rolled_back); -} - -TEST(ScopeGuardTest, BufferRestorePattern) { - int original_value = 10; - int current_value = original_value; - - { - cf::ScopeGuard restore( - [¤t_value, original_value]() { current_value = original_value; }); - - current_value = 999; - EXPECT_EQ(current_value, 999); - } - - EXPECT_EQ(current_value, original_value); -} - -// ============================================================================= -// Test: ScopeGuard with std::function -// ============================================================================= - -TEST(ScopeGuardTest, ConstructFromStdFunction) { - int counter = 0; - std::function func = [&counter]() { counter++; }; - - { cf::ScopeGuard guard(func); } - - EXPECT_EQ(counter, 1); -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/base/weak_ptr/weak_ptr_test.cpp b/test/base/weak_ptr/weak_ptr_test.cpp deleted file mode 100644 index f2a82398c..000000000 --- a/test/base/weak_ptr/weak_ptr_test.cpp +++ /dev/null @@ -1,279 +0,0 @@ -/** - * @file weak_ptr_test.cpp - * @brief Comprehensive unit tests for cf::WeakPtr using GoogleTest - * - * Test Coverage: - * 1. Default Construction - * 2. Validity While Owner Alive - * 3. Invalidation After Owner Destroyed - * 4. Multiple WeakPtrs - * 5. Manual Invalidation - * 6. HasWeakPtrs Tracking - * 7. Mutation Through WeakPtr - * 8. Copy and Move Semantics - * 9. Reset Operation - * 10. Covariant Conversion (Derived to Base) - * 11. Integration Patterns - */ - -#include "base/weak_ptr/weak_ptr.h" -#include "base/weak_ptr/weak_ptr_factory.h" -#include -#include - -using namespace cf; - -// ============================================================================= -// Test Helper Classes -// ============================================================================= - -class Resource { - public: - explicit Resource(std::string name) : name_(std::move(name)) {} - - const std::string& name() const { return name_; } - void Rename(std::string s) { name_ = std::move(s); } - - WeakPtr GetWeakPtr() { return weak_factory_.GetWeakPtr(); } - bool HasWeakPtrs() const { return weak_factory_.HasWeakPtrs(); } - void InvalidateWeakPtrs() { weak_factory_.InvalidateWeakPtrs(); } - - private: - std::string name_; - WeakPtrFactory weak_factory_{this}; // Must be last member -}; - -// Base/Derived classes for covariant tests -struct Base { - virtual ~Base() = default; - int x = 1; -}; - -struct Derived : Base { - int y = 2; -}; - -// ============================================================================= -// Test Suite 1: Default Construction -// ============================================================================= - -TEST(WeakPtrTest, DefaultConstructedWeakPtrIsInvalid) { - WeakPtr wp; - EXPECT_FALSE(wp.IsValid()); - EXPECT_FALSE(static_cast(wp)); - EXPECT_EQ(wp.Get(), nullptr); - EXPECT_EQ(wp, nullptr); -} - -// ============================================================================= -// Test Suite 2: Validity While Owner Alive -// ============================================================================= - -TEST(WeakPtrTest, WeakPtrIsValidWhileOwnerAlive) { - Resource res("theme"); - WeakPtr wp = res.GetWeakPtr(); - - EXPECT_TRUE(wp.IsValid()); - EXPECT_TRUE(static_cast(wp)); - EXPECT_EQ(wp.Get(), &res); - EXPECT_EQ(wp->name(), "theme"); - EXPECT_EQ((*wp).name(), "theme"); -} - -// ============================================================================= -// Test Suite 3: Invalidation After Owner Destroyed -// ============================================================================= - -TEST(WeakPtrTest, WeakPtrInvalidatedAfterOwnerDestroyed) { - WeakPtr wp; - { - Resource res("temp"); - wp = res.GetWeakPtr(); - EXPECT_TRUE(wp.IsValid()); - } // res destroyed - EXPECT_FALSE(wp.IsValid()); - EXPECT_EQ(wp.Get(), nullptr); -} - -// ============================================================================= -// Test Suite 4: Multiple WeakPtrs -// ============================================================================= - -TEST(WeakPtrTest, MultipleWeakPtrsAllInvalidatedTogether) { - WeakPtr wp1, wp2, wp3; - { - Resource res("multi"); - wp1 = res.GetWeakPtr(); - wp2 = res.GetWeakPtr(); - wp3 = wp1; // Copy - EXPECT_TRUE(wp1 && wp2 && wp3); - } - EXPECT_FALSE(wp1.IsValid()); - EXPECT_FALSE(wp2.IsValid()); - EXPECT_FALSE(wp3.IsValid()); -} - -// ============================================================================= -// Test Suite 5: Manual Invalidation -// ============================================================================= - -TEST(WeakPtrTest, ManualInvalidateWeakPtrs) { - Resource res("manual"); - WeakPtr wp = res.GetWeakPtr(); - EXPECT_TRUE(wp.IsValid()); - - res.InvalidateWeakPtrs(); - EXPECT_FALSE(wp.IsValid()); // Old references invalidated - - // Owner still alive, can get new valid WeakPtr - WeakPtr wp2 = res.GetWeakPtr(); - EXPECT_TRUE(wp2.IsValid()); -} - -// ============================================================================= -// Test Suite 6: HasWeakPtrs Tracking -// ============================================================================= - -TEST(WeakPtrTest, HasWeakPtrsTracksOutstandingRefs) { - Resource res("tracker"); - EXPECT_FALSE(res.HasWeakPtrs()); - - { - WeakPtr wp = res.GetWeakPtr(); - EXPECT_TRUE(res.HasWeakPtrs()); - } // wp destroyed - - EXPECT_FALSE(res.HasWeakPtrs()); -} - -// ============================================================================= -// Test Suite 7: Mutation Through WeakPtr -// ============================================================================= - -TEST(WeakPtrTest, WeakPtrCanMutateThroughOwner) { - Resource res("old"); - WeakPtr wp = res.GetWeakPtr(); - - wp->Rename("new"); - EXPECT_EQ(res.name(), "new"); -} - -// ============================================================================= -// Test Suite 8: Copy and Move Semantics -// ============================================================================= - -TEST(WeakPtrTest, WeakPtrCopyAndMove) { - Resource res("copy_move"); - WeakPtr wp1 = res.GetWeakPtr(); - - // Copy - WeakPtr wp2 = wp1; - EXPECT_TRUE(wp2.IsValid()); - EXPECT_EQ(wp2.Get(), &res); - - // Move - WeakPtr wp3 = std::move(wp2); - EXPECT_TRUE(wp3.IsValid()); - // wp2 after move is null (implementation detail) -} - -// ============================================================================= -// Test Suite 9: Reset Operation -// ============================================================================= - -TEST(WeakPtrTest, WeakPtrResetMakesItNull) { - Resource res("reset_test"); - WeakPtr wp = res.GetWeakPtr(); - EXPECT_TRUE(wp.IsValid()); - - wp.Reset(); - EXPECT_FALSE(wp.IsValid()); - EXPECT_EQ(wp.Get(), nullptr); -} - -// ============================================================================= -// Test Suite 10: Covariant Conversion -// ============================================================================= - -TEST(WeakPtrTest, CovariantConversionDerivedToBase) { - Derived d; - WeakPtrFactory factory(&d); - - WeakPtr derived_wp = factory.GetWeakPtr(); - WeakPtr base_wp = derived_wp; // Implicit upcast - - EXPECT_TRUE(base_wp.IsValid()); - EXPECT_EQ(base_wp->x, 1); -} - -TEST(WeakPtrTest, CovariantInvalidationFollowsOwner) { - WeakPtr base_wp; - { - Derived d; - WeakPtrFactory factory(&d); - WeakPtr derived_wp = factory.GetWeakPtr(); - base_wp = derived_wp; - EXPECT_TRUE(base_wp.IsValid()); - } - EXPECT_FALSE(base_wp.IsValid()); -} - -// ============================================================================= -// Test Suite 11: Integration Patterns -// ============================================================================= - -class ThemeManager { - public: - static ThemeManager& Instance() { - static ThemeManager inst; - return inst; - } - - WeakPtr GetWeakPtr() { return weak_factory_.GetWeakPtr(); } - - std::string current_theme = "light"; - - ThemeManager(const ThemeManager&) = delete; - ThemeManager& operator=(const ThemeManager&) = delete; - - private: - ThemeManager() = default; - WeakPtrFactory weak_factory_{this}; -}; - -class ToolBar { - public: - explicit ToolBar(WeakPtr tm) : theme_(std::move(tm)) {} - - std::string Render() { - if (!theme_) { - return "[ToolBar] ThemeManager gone, skip render."; - } - return "[ToolBar] Rendering with theme: " + theme_->current_theme; - } - - private: - WeakPtr theme_; -}; - -TEST(WeakPtrTest, ThemeManagerIntegration) { - auto& tm = ThemeManager::Instance(); - ToolBar tb(tm.GetWeakPtr()); - - EXPECT_EQ(tb.Render(), "[ToolBar] Rendering with theme: light"); - tm.current_theme = "dark"; - EXPECT_EQ(tb.Render(), "[ToolBar] Rendering with theme: dark"); - - // Reset local copy only - tm.GetWeakPtr().Reset(); - EXPECT_EQ(tb.Render(), "[ToolBar] Rendering with theme: dark"); -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/desktop/init/init_session_chain_test.cpp b/test/desktop/init/init_session_chain_test.cpp index d74a8d6e3..6c7fe5e2b 100644 --- a/test/desktop/init/init_session_chain_test.cpp +++ b/test/desktop/init/init_session_chain_test.cpp @@ -14,7 +14,7 @@ * @version 1.0 */ -#include "base/weak_ptr/weak_ptr_factory.h" +#include "aex/weak_ptr/weak_ptr_factory.h" #include "init_session_chain.h" #include "init_stage.h" #include @@ -42,7 +42,7 @@ namespace test { * Features: * - Configurable name, return status, message, and error detail * - Tracks execution count - * - Supports dependency management via WeakPtr + * - Supports dependency management via aex::WeakPtr */ class MockInitStage : public IInitStage { public: @@ -59,7 +59,7 @@ class MockInitStage : public IInitStage { return StageResult{status_code_, message_, error_detail_}; } - std::vector> request_before_actions_init() const override { + std::vector> request_before_actions_init() const override { return dependencies_; } @@ -71,7 +71,7 @@ class MockInitStage : public IInitStage { error_detail_ = detail; } - void add_dependency(WeakPtr dep) { dependencies_.push_back(std::move(dep)); } + void add_dependency(aex::WeakPtr dep) { dependencies_.push_back(std::move(dep)); } void clear_dependencies() { dependencies_.clear(); } @@ -84,7 +84,7 @@ class MockInitStage : public IInitStage { IInitStage::StatusCode status_code_; QString message_; QString error_detail_; - std::vector> dependencies_; + std::vector> dependencies_; int execution_count{0}; }; @@ -125,10 +125,10 @@ class InitSessionChainTest : public ::testing::Test { // For tests that need dependency setup, store stages with their factories struct StageWithFactory { std::unique_ptr stage; - std::unique_ptr> factory; + std::unique_ptr> factory; - cf::WeakPtr GetWeakPtr() const { - return factory ? factory->GetWeakPtr() : cf::WeakPtr(); + aex::WeakPtr GetWeakPtr() const { + return factory ? factory->GetWeakPtr() : aex::WeakPtr(); } }; @@ -137,7 +137,7 @@ class InitSessionChainTest : public ::testing::Test { CreateStageWithFactory(std::string_view name, IInitStage::StatusCode code = IInitStage::StatusCode::OK) { auto stage = std::make_unique(name, code); - auto factory = std::make_unique>(stage.get()); + auto factory = std::make_unique>(stage.get()); return StageWithFactory{std::move(stage), std::move(factory)}; } @@ -660,7 +660,7 @@ TEST(StageResultTest, BoolOperator_ReturnsTrueOnlyForOk) { } // ============================================================================= -// Test Suite 7: Singleton Access +// Test Suite 7: aex::Singleton Access // ============================================================================= TEST_F(InitSessionChainTest, GetChainRef_InitiallyReturnsNull) { diff --git a/test/logger/error_handling/logger_error_handling_test.cpp b/test/logger/error_handling/logger_error_handling_test.cpp index 737f46915..749ff9af2 100644 --- a/test/logger/error_handling/logger_error_handling_test.cpp +++ b/test/logger/error_handling/logger_error_handling_test.cpp @@ -11,7 +11,7 @@ #include "../mock/mock_sink.h" #include "../test_config.h" -#include "base/singleton/simple_singleton.hpp" +#include "aex/singleton/simple_singleton.hpp" #include "cflog.h" #include "cflog/cflog.hpp" #include "cflog/cflog_level.hpp" diff --git a/third_party/aex b/third_party/aex new file mode 160000 index 000000000..79628175f --- /dev/null +++ b/third_party/aex @@ -0,0 +1 @@ +Subproject commit 79628175fc89046dd89f46ee7a36b7bd2c80dcf5 diff --git a/ui/components/animation.h b/ui/components/animation.h index c1758fecf..108bf0e35 100644 --- a/ui/components/animation.h +++ b/ui/components/animation.h @@ -15,7 +15,7 @@ */ #pragma once -#include "base/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr.h" #include "export.h" #include #include @@ -127,9 +127,9 @@ class CF_UI_EXPORT ICFAbstractAnimation : public QObject { * @brief Gets a weak pointer to this animation. * * @details Each concrete animation class must implement this using - * its WeakPtrFactory. + * its aex::WeakPtrFactory. * - * @return WeakPtr to this animation. + * @return aex::WeakPtr to this animation. * * @throws None * @note None @@ -137,7 +137,7 @@ class CF_UI_EXPORT ICFAbstractAnimation : public QObject { * @since 0.1 * @ingroup ui_components */ - virtual cf::WeakPtr GetWeakPtr() = 0; + virtual aex::WeakPtr GetWeakPtr() = 0; /** * @brief Gets the enabled state of the animation. diff --git a/ui/components/animation_factory_manager.h b/ui/components/animation_factory_manager.h index 92404a476..ef2ab3b3e 100644 --- a/ui/components/animation_factory_manager.h +++ b/ui/components/animation_factory_manager.h @@ -13,14 +13,14 @@ * and supports both string-based and typed animation registration. * * The manager maintains ownership of created animations and provides - * WeakPtr access to users for safe, non-owning references. + * aex::WeakPtr access to users for safe, non-owning references. * * @ingroup ui_components */ #pragma once +#include "aex/weak_ptr/weak_ptr.h" #include "animation.h" -#include "base/weak_ptr/weak_ptr.h" #include #include @@ -45,14 +45,14 @@ using AnimationCreator = std::function; * - Token-based animation lookup (e.g., "md.animation.fadeIn") * - Type-safe animation registration * - Global and per-animation enable/disable - * - WeakPtr ownership model for safe access + * - aex::WeakPtr ownership model for safe access * - * Animations are owned by the manager and accessed via WeakPtr. + * Animations are owned by the manager and accessed via aex::WeakPtr. * This ensures proper lifecycle management and prevents dangling * pointers when the manager is destroyed. * * @note Implementations should be thread-safe for concurrent reads. - * @warning Destroying the manager invalidates all WeakPtr references. + * @warning Destroying the manager invalidates all aex::WeakPtr references. * @since 0.1 * @ingroup ui_components * @@ -108,7 +108,7 @@ class CF_UI_EXPORT ICFAnimationManagerFactory : public QObject { }; Q_ENUM(RegisteredResult) - virtual cf::WeakPtr GetWeakPtr() = 0; + virtual aex::WeakPtr GetWeakPtr() = 0; // ========================================================================= // Animation Registration // ========================================================================= @@ -180,10 +180,10 @@ class CF_UI_EXPORT ICFAnimationManagerFactory : public QObject { * * @param[in] name Animation name or token. * - * @return WeakPtr to the animation, or invalid WeakPtr if not found. + * @return aex::WeakPtr to the animation, or invalid aex::WeakPtr if not found. * * @throws None - * @note The returned WeakPtr may become invalid if the manager + * @note The returned aex::WeakPtr may become invalid if the manager * is destroyed. Always check validity before use. * @warning The manager owns the animation; do not delete it manually. * @since 0.1 @@ -196,7 +196,7 @@ class CF_UI_EXPORT ICFAnimationManagerFactory : public QObject { * } * @endcode */ - virtual cf::WeakPtr getAnimation(const char* name) = 0; + virtual aex::WeakPtr getAnimation(const char* name) = 0; // ========================================================================= // Global Settings // ========================================================================= @@ -225,7 +225,7 @@ class CF_UI_EXPORT ICFAnimationManagerFactory : public QObject { * @brief Set enabled state for a specific animation. * * @details Controls whether a specific animation is allowed to run. - * When disabled, getAnimation() returns invalid WeakPtr. + * When disabled, getAnimation() returns invalid aex::WeakPtr. * * @param[in] which Animation name. * @param[in] enabled true to enable, false to disable. @@ -260,7 +260,7 @@ class CF_UI_EXPORT ICFAnimationManagerFactory : public QObject { /** * @brief Set enabled state for all animations. * - * @details When disabled, getAnimation() returns invalid WeakPtr + * @details When disabled, getAnimation() returns invalid aex::WeakPtr * for all animations. Existing running animations continue * until completion. * diff --git a/ui/components/animation_group.h b/ui/components/animation_group.h index 8dfc0805c..d653959f0 100644 --- a/ui/components/animation_group.h +++ b/ui/components/animation_group.h @@ -15,8 +15,8 @@ */ #pragma once +#include "aex/weak_ptr/weak_ptr.h" #include "animation.h" -#include "base/weak_ptr/weak_ptr.h" #include "export.h" #include #include @@ -48,7 +48,7 @@ class CF_UI_EXPORT ICFAnimationGroup : public ICFAbstractAnimation { /** * @brief Adds an animation to the group. * - * @param[in] animation WeakPtr to the animation to add. + * @param[in] animation aex::WeakPtr to the animation to add. * * @throws None * @note None @@ -56,12 +56,14 @@ class CF_UI_EXPORT ICFAnimationGroup : public ICFAbstractAnimation { * @since 0.1 * @ingroup ui_components */ - void addAnimation(cf::WeakPtr animation) { animations.insert(animation); } + void addAnimation(aex::WeakPtr animation) { + animations.insert(animation); + } /** * @brief Removes an animation from the group. * - * @param[in] animation WeakPtr to the animation to remove. + * @param[in] animation aex::WeakPtr to the animation to remove. * * @throws None * @note None @@ -69,12 +71,12 @@ class CF_UI_EXPORT ICFAnimationGroup : public ICFAbstractAnimation { * @since 0.1 * @ingroup ui_components */ - void removeAnimation(cf::WeakPtr animation) { + void removeAnimation(aex::WeakPtr animation) { animations.erase(animation); } private: - std::unordered_set> animations; + std::unordered_set> animations; }; } // namespace cf::ui::components diff --git a/ui/components/material/cfmaterial_animation_factory.cpp b/ui/components/material/cfmaterial_animation_factory.cpp index fd7c313ec..2d0b51a9f 100644 --- a/ui/components/material/cfmaterial_animation_factory.cpp +++ b/ui/components/material/cfmaterial_animation_factory.cpp @@ -51,18 +51,18 @@ CFMaterialAnimationFactory::~CFMaterialAnimationFactory() { // Public Methods // ============================================================================= -cf::WeakPtr +aex::WeakPtr CFMaterialAnimationFactory::getAnimation(const char* animationToken) { // Check global enabled state if (!globalEnabled_) { - return cf::WeakPtr(); + return aex::WeakPtr(); } // Check if animation already exists auto it = animations_.find(animationToken); if (it != animations_.end()) { - // Return existing WeakPtr from the animation + // Return existing aex::WeakPtr from the animation return it->second->GetWeakPtr(); } @@ -71,7 +71,7 @@ CFMaterialAnimationFactory::getAnimation(const char* animationToken) { // Check if token was found if (descriptor.animationType == nullptr) { - return cf::WeakPtr(); + return aex::WeakPtr(); } // Apply strategy @@ -79,7 +79,7 @@ CFMaterialAnimationFactory::getAnimation(const char* animationToken) { // Check if animation should be enabled if (!shouldEnableAnimation(nullptr)) { - return cf::WeakPtr(); + return aex::WeakPtr(); } // Create animation based on type @@ -94,7 +94,7 @@ CFMaterialAnimationFactory::getAnimation(const char* animationToken) { animation = createScaleAnimation(descriptor, nullptr); } - // Store and return WeakPtr + // Store and return aex::WeakPtr if (animation) { const char* tokenKey = descriptor.motionToken; ICFAbstractAnimation* rawPtr = animation.get(); @@ -103,16 +103,16 @@ CFMaterialAnimationFactory::getAnimation(const char* animationToken) { return rawPtr->GetWeakPtr(); } - return cf::WeakPtr(); + return aex::WeakPtr(); } -cf::WeakPtr +aex::WeakPtr CFMaterialAnimationFactory::createAnimation(const AnimationDescriptor& descriptor, QWidget* targetWidget, QObject* owner) { // Check global enabled state if (!globalEnabled_) { - return cf::WeakPtr(); + return aex::WeakPtr(); } // Apply strategy @@ -120,7 +120,7 @@ CFMaterialAnimationFactory::createAnimation(const AnimationDescriptor& descripto // Check if animation should be enabled if (!shouldEnableAnimation(targetWidget)) { - return cf::WeakPtr(); + return aex::WeakPtr(); } // Generate a unique key for this animation @@ -149,7 +149,7 @@ CFMaterialAnimationFactory::createAnimation(const AnimationDescriptor& descripto animation = createScaleAnimation(adjustedDescriptor, targetWidget); } - // Store and return WeakPtr + // Store and return aex::WeakPtr if (animation) { ICFAbstractAnimation* rawPtr = animation.get(); animations_[key] = std::move(animation); @@ -167,7 +167,7 @@ CFMaterialAnimationFactory::createAnimation(const AnimationDescriptor& descripto return rawPtr->GetWeakPtr(); } - return cf::WeakPtr(); + return aex::WeakPtr(); } void CFMaterialAnimationFactory::setStrategy(std::unique_ptr strategy) { @@ -315,18 +315,18 @@ bool CFMaterialAnimationFactory::shouldEnableAnimation(QWidget* widget) const { return globalEnabled_; } -cf::WeakPtr CFMaterialAnimationFactory::createPropertyAnimation( +aex::WeakPtr CFMaterialAnimationFactory::createPropertyAnimation( float* value, float from, float to, int durationMs, cf::ui::base::Easing::Type easing, QWidget* targetWidget, QObject* owner) { // Check global enabled state if (!globalEnabled_) { - return cf::WeakPtr(); + return aex::WeakPtr(); } // Check if animation should be enabled if (!shouldEnableAnimation(targetWidget)) { - return cf::WeakPtr(); + return aex::WeakPtr(); } // Generate a unique key for this animation @@ -352,7 +352,7 @@ cf::WeakPtr CFMaterialAnimationFactory::createPropertyAnim anim->setTargetWidget(targetWidget); } - // Store and return WeakPtr + // Store and return aex::WeakPtr if (anim) { ICFAbstractAnimation* rawPtr = anim.get(); animations_[key] = std::move(anim); @@ -370,7 +370,7 @@ cf::WeakPtr CFMaterialAnimationFactory::createPropertyAnim return rawPtr->GetWeakPtr(); } - return cf::WeakPtr(); + return aex::WeakPtr(); } } // namespace cf::ui::components::material diff --git a/ui/components/material/cfmaterial_animation_factory.h b/ui/components/material/cfmaterial_animation_factory.h index ff865927a..c51de0e97 100644 --- a/ui/components/material/cfmaterial_animation_factory.h +++ b/ui/components/material/cfmaterial_animation_factory.h @@ -17,11 +17,11 @@ * - Token-based animation retrieval (e.g., "md.animation.fadeIn") * - Automatic mapping to MotionSpec for timing and easing * - Strategy pattern for widget-specific behavior - * - WeakPtr ownership model (factory owns, users hold weak references) + * - aex::WeakPtr ownership model (factory owns, users hold weak references) * - Global enable/disable for performance and accessibility * * The factory maintains exclusive ownership of all created animations - * via unique_ptr, while users receive WeakPtr references. This ensures + * via unique_ptr, while users receive aex::WeakPtr references. This ensures * proper lifecycle management and prevents dangling pointers. * * @ingroup ui_components_material @@ -29,10 +29,10 @@ #pragma once #include "../animation.h" +#include "aex/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr_factory.h" #include "animation_factory_manager.h" #include "base/easing.h" -#include "base/weak_ptr/weak_ptr.h" -#include "base/weak_ptr/weak_ptr_factory.h" #include "cfmaterial_animation_strategy.h" #include "core/theme.h" #include "export.h" @@ -53,7 +53,7 @@ class CFMaterialScaleAnimation; * * @details Creates and manages animations following Material Design 3 * motion specifications. The factory maintains exclusive ownership - * of created animations (via unique_ptr) and provides WeakPtr + * of created animations (via unique_ptr) and provides aex::WeakPtr * access to users. * * Animation lifecycle: @@ -62,7 +62,7 @@ class CFMaterialScaleAnimation; * 3. Strategy adjusts descriptor (if set) * 4. Factory creates animation instance * 5. Factory stores animation (owns it) - * 6. Factory returns WeakPtr to user + * 6. Factory returns aex::WeakPtr to user * * Token resolution: * - "md.animation.fadeIn" → Fade animation, shortEnter timing @@ -70,9 +70,9 @@ class CFMaterialScaleAnimation; * - "md.animation.scaleUp" → Scale animation, shortEnter timing * * @note Thread-safe for concurrent reads. - * @warning Animations are owned by the factory; WeakPtr may become + * @warning Animations are owned by the factory; aex::WeakPtr may become * invalid if the factory is destroyed. - * @throws None (all errors return invalid WeakPtr) + * @throws None (all errors return invalid aex::WeakPtr) * @since 0.1 * @ingroup ui_components_material * @@ -134,7 +134,7 @@ class CF_UI_EXPORT CFMaterialAnimationFactory : public ICFAnimationManagerFactor /** * @brief Destructor. * - * @details All owned animations are destroyed. Any WeakPtr + * @details All owned animations are destroyed. Any aex::WeakPtr * returned by this factory becomes invalid. * * @since 0.1 @@ -147,7 +147,7 @@ class CF_UI_EXPORT CFMaterialAnimationFactory : public ICFAnimationManagerFactor CFMaterialAnimationFactory(CFMaterialAnimationFactory&&) = delete; CFMaterialAnimationFactory& operator=(CFMaterialAnimationFactory&&) = delete; - cf::WeakPtr GetWeakPtr() override { + aex::WeakPtr GetWeakPtr() override { return weak_factory_.GetWeakPtr(); } @@ -196,12 +196,12 @@ class CF_UI_EXPORT CFMaterialAnimationFactory : public ICFAnimationManagerFactor * - "md.animation.scaleUp" → Scale animation, shortEnter timing * * If an animation with the given token already exists, - * the existing animation's WeakPtr is returned. + * the existing animation's aex::WeakPtr is returned. * Otherwise, a new animation is created and stored. * * @param animationToken Token name (e.g., "md.animation.fadeIn"). * - * @return WeakPtr to the animation, or invalid WeakPtr if: + * @return aex::WeakPtr to the animation, or invalid aex::WeakPtr if: * - Token is not found in mapping * - Animation type is not supported * - Global enabled is false @@ -209,7 +209,7 @@ class CF_UI_EXPORT CFMaterialAnimationFactory : public ICFAnimationManagerFactor * * @throws None * @note If the animation doesn't exist, a new animation is created. - * @warning The returned WeakPtr may become invalid if the factory + * @warning The returned aex::WeakPtr may become invalid if the factory * is destroyed. Always check validity before use. * @since 0.1 * @ingroup ui_components_material @@ -222,7 +222,7 @@ class CF_UI_EXPORT CFMaterialAnimationFactory : public ICFAnimationManagerFactor * } * @endcode */ - cf::WeakPtr getAnimation(const char* animationToken) override; + aex::WeakPtr getAnimation(const char* animationToken) override; /** * @brief Create an animation from a descriptor. @@ -239,7 +239,7 @@ class CF_UI_EXPORT CFMaterialAnimationFactory : public ICFAnimationManagerFactor * The animation applies to this widget. * @param[in] owner Optional owner QObject for memory management. * - * @return WeakPtr to the created animation, or invalid WeakPtr if: + * @return aex::WeakPtr to the created animation, or invalid aex::WeakPtr if: * - Animation type is not supported * - Global enabled is false * - Strategy disables animation @@ -256,9 +256,9 @@ class CF_UI_EXPORT CFMaterialAnimationFactory : public ICFAnimationManagerFactor * if (anim) anim->start(); * @endcode */ - cf::WeakPtr createAnimation(const AnimationDescriptor& descriptor, - QWidget* targetWidget = nullptr, - QObject* owner = nullptr); + aex::WeakPtr createAnimation(const AnimationDescriptor& descriptor, + QWidget* targetWidget = nullptr, + QObject* owner = nullptr); /** * @brief Create a property animation for a float value. @@ -278,7 +278,7 @@ class CF_UI_EXPORT CFMaterialAnimationFactory : public ICFAnimationManagerFactor * @param[in] easing Easing type for the animation. * @param[in] targetWidget Optional target widget for repaint notifications. * - * @return WeakPtr to the created animation, or invalid WeakPtr if: + * @return aex::WeakPtr to the created animation, or invalid aex::WeakPtr if: * - Global enabled is false * - Strategy disables animation * @@ -296,7 +296,7 @@ class CF_UI_EXPORT CFMaterialAnimationFactory : public ICFAnimationManagerFactor * if (anim) anim->start(); * @endcode */ - cf::WeakPtr createPropertyAnimation( + aex::WeakPtr createPropertyAnimation( float* value, float from, float to, int durationMs, cf::ui::base::Easing::Type easing = cf::ui::base::Easing::Type::EmphasizedDecelerate, QWidget* targetWidget = nullptr, QObject* owner = nullptr); @@ -339,8 +339,8 @@ class CF_UI_EXPORT CFMaterialAnimationFactory : public ICFAnimationManagerFactor /** * @brief Set the global enabled state for all animations. * - * @details When disabled, getAnimation() returns invalid WeakPtr - * and createAnimation() returns invalid WeakPtr. + * @details When disabled, getAnimation() returns invalid aex::WeakPtr + * and createAnimation() returns invalid aex::WeakPtr. * * This is useful for: * - Performance optimization during heavy processing @@ -542,7 +542,7 @@ class CF_UI_EXPORT CFMaterialAnimationFactory : public ICFAnimationManagerFactor */ bool shouldEnableAnimation(QWidget* widget) const; - cf::WeakPtrFactory weak_factory_{this}; + aex::WeakPtrFactory weak_factory_{this}; }; } // namespace cf::ui::components::material diff --git a/ui/components/material/cfmaterial_fade_animation.h b/ui/components/material/cfmaterial_fade_animation.h index 7561e35af..cb765d8f8 100644 --- a/ui/components/material/cfmaterial_fade_animation.h +++ b/ui/components/material/cfmaterial_fade_animation.h @@ -19,8 +19,8 @@ */ #pragma once -#include "base/weak_ptr/weak_ptr.h" -#include "base/weak_ptr/weak_ptr_factory.h" +#include "aex/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr_factory.h" #include "components/timing_animation.h" #include "core/motion_spec.h" #include "export.h" @@ -52,7 +52,7 @@ namespace cf::ui::components::material { * // Create a fade-in animation * auto& motionSpec = theme.motion_spec(); * auto fadeAnim = std::make_unique( - * cf::WeakPtr(&motionSpec), + * aex::WeakPtr(&motionSpec), * this); * fadeAnim->setRange(0.0f, 1.0f); // Fade from transparent to opaque * fadeAnim->setTargetWidget(myWidget); @@ -199,11 +199,11 @@ class CF_UI_EXPORT CFMaterialFadeAnimation : public ICFTimingAnimation { /** * @brief Get a weak pointer to this animation. * - * @return WeakPtr that can be used to safely access this animation. + * @return aex::WeakPtr that can be used to safely access this animation. * * @since 0.1 */ - cf::WeakPtr GetWeakPtr() override { return weak_factory_.GetWeakPtr(); } + aex::WeakPtr GetWeakPtr() override { return weak_factory_.GetWeakPtr(); } private: /// Current opacity value (0.0 to 1.0) @@ -254,9 +254,9 @@ class CF_UI_EXPORT CFMaterialFadeAnimation : public ICFTimingAnimation { */ float calculateEasedProgress(float linearProgress) const; - /// WeakPtrFactory for creating weak pointers to this animation + /// aex::WeakPtrFactory for creating weak pointers to this animation /// Must be the last member to ensure it's destroyed first - cf::WeakPtrFactory weak_factory_{this}; + aex::WeakPtrFactory weak_factory_{this}; }; } // namespace cf::ui::components::material diff --git a/ui/components/material/cfmaterial_property_animation.h b/ui/components/material/cfmaterial_property_animation.h index 448960c76..0d9fa2d7a 100644 --- a/ui/components/material/cfmaterial_property_animation.h +++ b/ui/components/material/cfmaterial_property_animation.h @@ -21,9 +21,9 @@ */ #pragma once +#include "aex/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr_factory.h" #include "base/easing.h" -#include "base/weak_ptr/weak_ptr.h" -#include "base/weak_ptr/weak_ptr_factory.h" #include "components/timing_animation.h" #include "core/motion_spec.h" #include "export.h" @@ -159,7 +159,7 @@ class CF_UI_EXPORT CFMaterialPropertyAnimation : public ICFAbstractAnimation { * @since 0.1 * @ingroup ui_components_material */ - cf::WeakPtr GetWeakPtr() override { return weak_factory_.GetWeakPtr(); } + aex::WeakPtr GetWeakPtr() override { return weak_factory_.GetWeakPtr(); } // ========================================================================= // Property-Specific Methods @@ -249,9 +249,9 @@ class CF_UI_EXPORT CFMaterialPropertyAnimation : public ICFAbstractAnimation { */ int calculateInterval() const { return static_cast(1000.0f / m_targetFps); } - /// WeakPtrFactory for creating weak pointers to this animation + /// aex::WeakPtrFactory for creating weak pointers to this animation /// Must be the last member to ensure it's destroyed first - cf::WeakPtrFactory weak_factory_{this}; + aex::WeakPtrFactory weak_factory_{this}; }; } // namespace cf::ui::components::material diff --git a/ui/components/material/cfmaterial_scale_animation.h b/ui/components/material/cfmaterial_scale_animation.h index 085cd5332..c82442024 100644 --- a/ui/components/material/cfmaterial_scale_animation.h +++ b/ui/components/material/cfmaterial_scale_animation.h @@ -19,8 +19,8 @@ */ #pragma once -#include "base/weak_ptr/weak_ptr.h" -#include "base/weak_ptr/weak_ptr_factory.h" +#include "aex/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr_factory.h" #include "components/timing_animation.h" #include "core/motion_spec.h" #include "export.h" @@ -52,7 +52,7 @@ namespace cf::ui::components::material { * // Create a scale-up animation * auto& motionSpec = theme.motion_spec(); * auto scaleAnim = std::make_unique( - * cf::WeakPtr(&motionSpec), + * aex::WeakPtr(&motionSpec), * this); * scaleAnim->setRange(0.8f, 1.0f); // Scale from 80% to 100% * scaleAnim->setTargetWidget(myWidget); @@ -224,11 +224,11 @@ class CF_UI_EXPORT CFMaterialScaleAnimation : public ICFTimingAnimation { /** * @brief Get a weak pointer to this animation. * - * @return WeakPtr that can be used to safely access this animation. + * @return aex::WeakPtr that can be used to safely access this animation. * * @since 0.1 */ - cf::WeakPtr GetWeakPtr() override { return weak_factory_.GetWeakPtr(); } + aex::WeakPtr GetWeakPtr() override { return weak_factory_.GetWeakPtr(); } private: /// Current scale value (1.0 = normal size) @@ -283,9 +283,9 @@ class CF_UI_EXPORT CFMaterialScaleAnimation : public ICFTimingAnimation { */ float calculateEasedProgress(float linearProgress) const; - /// WeakPtrFactory for creating weak pointers to this animation + /// aex::WeakPtrFactory for creating weak pointers to this animation /// Must be the last member to ensure it's destroyed first - cf::WeakPtrFactory weak_factory_{this}; + aex::WeakPtrFactory weak_factory_{this}; }; } // namespace cf::ui::components::material diff --git a/ui/components/material/cfmaterial_slide_animation.h b/ui/components/material/cfmaterial_slide_animation.h index 56e61daf8..e64abc0bd 100644 --- a/ui/components/material/cfmaterial_slide_animation.h +++ b/ui/components/material/cfmaterial_slide_animation.h @@ -19,8 +19,8 @@ */ #pragma once -#include "base/weak_ptr/weak_ptr.h" -#include "base/weak_ptr/weak_ptr_factory.h" +#include "aex/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr_factory.h" #include "components/timing_animation.h" #include "core/motion_spec.h" #include "export.h" @@ -68,7 +68,7 @@ enum class SlideDirection { * // Create a slide-up animation * auto& motionSpec = theme.motion_spec(); * auto slideAnim = std::make_unique( - * cf::WeakPtr(&motionSpec), + * aex::WeakPtr(&motionSpec), * SlideDirection::Up, * this); * slideAnim->setDistance(100.0f); // Slide 100 pixels @@ -273,11 +273,11 @@ class CF_UI_EXPORT CFMaterialSlideAnimation : public ICFTimingAnimation { /** * @brief Get a weak pointer to this animation. * - * @return WeakPtr that can be used to safely access this animation. + * @return aex::WeakPtr that can be used to safely access this animation. * * @since 0.1 */ - cf::WeakPtr GetWeakPtr() override { return weak_factory_.GetWeakPtr(); } + aex::WeakPtr GetWeakPtr() override { return weak_factory_.GetWeakPtr(); } private: /// Current offset value in pixels @@ -335,9 +335,9 @@ class CF_UI_EXPORT CFMaterialSlideAnimation : public ICFTimingAnimation { */ float calculateEasedProgress(float linearProgress) const; - /// WeakPtrFactory for creating weak pointers to this animation + /// aex::WeakPtrFactory for creating weak pointers to this animation /// Must be the last member to ensure it's destroyed first - cf::WeakPtrFactory weak_factory_{this}; + aex::WeakPtrFactory weak_factory_{this}; }; } // namespace cf::ui::components::material diff --git a/ui/core/material/material_factory.cpp b/ui/core/material/material_factory.cpp index 5b68a156c..aadc1e9db 100644 --- a/ui/core/material/material_factory.cpp +++ b/ui/core/material/material_factory.cpp @@ -183,14 +183,14 @@ Result fromJson(const QByteArray& json, bool isDark) { QJsonDocument doc = QJsonDocument::fromJson(json, &parseError); if (parseError.error != QJsonParseError::NoError) { - return ::cf::unexpected( + return ::aex::unexpected( MaterialSchemeError{MaterialSchemeError::Kind::InvalidJson, "Failed to parse JSON: " + parseError.errorString().toStdString()}); } if (!doc.isObject()) { - return ::cf::unexpected(MaterialSchemeError{MaterialSchemeError::Kind::InvalidJson, - "Root element must be an object"}); + return ::aex::unexpected(MaterialSchemeError{MaterialSchemeError::Kind::InvalidJson, + "Root element must be an object"}); } QJsonObject root = doc.object(); @@ -245,7 +245,7 @@ Result fromJson(const QByteArray& json, bool isDark) { if (colors.contains(qKey)) { QString colorStr = colors[qKey].toString(); if (!colorStr.startsWith('#')) { - return ::cf::unexpected(MaterialSchemeError{ + return ::aex::unexpected(MaterialSchemeError{ MaterialSchemeError::Kind::InvalidColorFormat, "Invalid color format for " + key + ": " + colorStr.toStdString()}); } diff --git a/ui/core/material/material_factory.hpp b/ui/core/material/material_factory.hpp index e48e3c653..89e6a5693 100644 --- a/ui/core/material/material_factory.hpp +++ b/ui/core/material/material_factory.hpp @@ -14,8 +14,8 @@ #include #include "../../export.h" +#include "aex/expected/expected.hpp" #include "base/color.h" -#include "base/expected/expected.hpp" #include "cfmaterial_fonttype.h" #include "cfmaterial_motion.h" #include "cfmaterial_radius_scale.h" @@ -76,7 +76,7 @@ struct MaterialSchemeError { * * @since 0.1 */ -using Result = cf::expected; +using Result = aex::expected; // ============================================================================= // Factory Functions diff --git a/ui/core/theme_manager.h b/ui/core/theme_manager.h index 41ff91ca4..e2e738a89 100644 --- a/ui/core/theme_manager.h +++ b/ui/core/theme_manager.h @@ -1,6 +1,6 @@ /** * @file ui/core/theme_manager.h - * @brief Singleton manager for CF UI theme registration and application. + * @brief aex::Singleton manager for CF UI theme registration and application. * * ThemeManager manages theme factory registration, theme creation, and * application of themes to widgets. Emits signals when the theme changes. @@ -26,14 +26,14 @@ namespace cf::ui::core { /** - * @brief Singleton manager for CF UI theme registration and application. + * @brief aex::Singleton manager for CF UI theme registration and application. * * ThemeManager manages theme factory registration, theme creation, and * application of themes to widgets. Emits signals when the theme changes. * * @ingroup none * - * @note Singleton instance accessed via instance(). + * @note aex::Singleton instance accessed via instance(). * * @warning None * diff --git a/ui/widget/application_support/application.cpp b/ui/widget/application_support/application.cpp index 9e178b74c..9a4f89682 100644 --- a/ui/widget/application_support/application.cpp +++ b/ui/widget/application_support/application.cpp @@ -44,7 +44,7 @@ core::ThemeManager* Application::themeManager() { return &core::ThemeManager::instance(); } -cf::WeakPtr Application::animationFactory() { +aex::WeakPtr Application::animationFactory() { if (auto* app = instance()) { return app->animationFactory_->GetWeakPtr(); } @@ -73,7 +73,7 @@ const core::ICFTheme& Application::currentTheme() const { return tm.theme(name); } -cf::WeakPtr +aex::WeakPtr Application::animation(const std::string& animationToken) { if (animationFactory_) { return animationFactory_->getAnimation(animationToken.c_str()); diff --git a/ui/widget/application_support/application.h b/ui/widget/application_support/application.h index 8b09d9911..a1aa7fb6b 100644 --- a/ui/widget/application_support/application.h +++ b/ui/widget/application_support/application.h @@ -33,9 +33,9 @@ namespace cf::ui::widget::application_support { * CFMaterialAnimationFactory. Replaces standard QApplication in main(). * * @note Thread-safe for concurrent reads. - * @warning The animation factory is owned by Application; WeakPtr may become + * @warning The animation factory is owned by Application; aex::WeakPtr may become * invalid if the application is destroyed. - * @throws None (all errors return invalid WeakPtr or throw from ThemeManager) + * @throws None (all errors return invalid aex::WeakPtr or throw from ThemeManager) * @since 0.1 * @ingroup ui_widget_application_support * @@ -125,12 +125,12 @@ class CF_UI_EXPORT Application : public QApplication { /** * @brief Get the animation factory. * - * @return WeakPtr to the animation factory, or invalid WeakPtr if + * @return aex::WeakPtr to the animation factory, or invalid aex::WeakPtr if * Application instance doesn't exist. * * @since 0.1 */ - static cf::WeakPtr animationFactory(); + static aex::WeakPtr animationFactory(); // ======================================================================== // Theme Access (Token-based) @@ -190,13 +190,13 @@ class CF_UI_EXPORT Application : public QApplication { * * @param[in] animationToken Animation identifier (e.g., "md.animation.fadeIn"). * - * @return WeakPtr to the animation, or invalid WeakPtr if: + * @return aex::WeakPtr to the animation, or invalid aex::WeakPtr if: * - Token is not found in mapping * - Animation type is not supported * - Animations are globally disabled * * @throws None - * @note The returned WeakPtr may become invalid if the factory + * @note The returned aex::WeakPtr may become invalid if the factory * is destroyed or the theme changes. * @warning Always check validity before use. * @since 0.1 @@ -210,12 +210,12 @@ class CF_UI_EXPORT Application : public QApplication { * } * @endcode */ - cf::WeakPtr animation(const std::string& animationToken); + aex::WeakPtr animation(const std::string& animationToken); /** * @brief Set global animation enabled state. * - * @details When disabled, animation() returns invalid WeakPtr. + * @details When disabled, animation() returns invalid aex::WeakPtr. * Existing animations continue to run; only new creations are affected. * * @param[in] enabled true to enable animations, false to disable. diff --git a/ui/widget/material/base/elevation_controller.cpp b/ui/widget/material/base/elevation_controller.cpp index 9356761f5..3304915dd 100644 --- a/ui/widget/material/base/elevation_controller.cpp +++ b/ui/widget/material/base/elevation_controller.cpp @@ -44,11 +44,11 @@ using namespace cf::ui::math; /** * @brief Constructor - initializes elevation controller. * - * @param factory WeakPtr to animation factory for elevation transitions. + * @param factory aex::WeakPtr to animation factory for elevation transitions. * @param parent QObject parent for memory management. */ MdElevationController::MdElevationController( - cf::WeakPtr factory, QObject* parent) + aex::WeakPtr factory, QObject* parent) : QObject(parent), m_currentLevel(0.0f), m_targetLevel(0), m_animator(factory) {} /** diff --git a/ui/widget/material/base/elevation_controller.h b/ui/widget/material/base/elevation_controller.h index 93b22cc41..f7aa47136 100644 --- a/ui/widget/material/base/elevation_controller.h +++ b/ui/widget/material/base/elevation_controller.h @@ -41,7 +41,7 @@ class CF_UI_EXPORT MdElevationController : public QObject { /** * @brief Constructor with animation factory. * - * @param[in] factory WeakPtr to the animation factory. + * @param[in] factory aex::WeakPtr to the animation factory. * @param[in] parent QObject parent. * * @throws None @@ -51,7 +51,7 @@ class CF_UI_EXPORT MdElevationController : public QObject { * @ingroup ui_widget_material_base */ explicit MdElevationController( - cf::WeakPtr factory, + aex::WeakPtr factory, QObject* parent = nullptr); /** @@ -234,11 +234,11 @@ class CF_UI_EXPORT MdElevationController : public QObject { int m_targetLevel = 0; float m_lightSourceAngle = 15.0f; bool m_isPressed = false; - float m_currentPressOffset = 0.0f; ///< Animated press offset value - cf::WeakPtr m_animator; + float m_currentPressOffset = 0.0f; ///< Animated press offset value + aex::WeakPtr m_animator; /// Reference to the currently running press offset animation - cf::WeakPtr m_pressOffsetAnimation; + aex::WeakPtr m_pressOffsetAnimation; /** * @brief Cancels the currently running press offset animation. diff --git a/ui/widget/material/base/focus_ring.cpp b/ui/widget/material/base/focus_ring.cpp index 81995e126..694dd46a4 100644 --- a/ui/widget/material/base/focus_ring.cpp +++ b/ui/widget/material/base/focus_ring.cpp @@ -30,11 +30,11 @@ using namespace cf::ui::base; /** * @brief Constructor - initializes focus indicator. * - * @param factory WeakPtr to animation factory for fade animation. + * @param factory aex::WeakPtr to animation factory for fade animation. * @param parent QObject parent for memory management. */ MdFocusIndicator::MdFocusIndicator( - cf::WeakPtr factory, QObject* parent) + aex::WeakPtr factory, QObject* parent) : QObject(parent), m_progress(0.0f), m_animator(factory) {} // ============================================================================ diff --git a/ui/widget/material/base/focus_ring.h b/ui/widget/material/base/focus_ring.h index 99b598efc..0752a3f3c 100644 --- a/ui/widget/material/base/focus_ring.h +++ b/ui/widget/material/base/focus_ring.h @@ -35,7 +35,7 @@ class CF_UI_EXPORT MdFocusIndicator : public QObject { /** * @brief Constructor with animation factory. * - * @param[in] factory WeakPtr to the animation factory. + * @param[in] factory aex::WeakPtr to the animation factory. * @param[in] parent QObject parent. * * @throws None @@ -44,8 +44,9 @@ class CF_UI_EXPORT MdFocusIndicator : public QObject { * @since N/A * @ingroup ui_widget_material_base */ - explicit MdFocusIndicator(cf::WeakPtr factory, - QObject* parent = nullptr); + explicit MdFocusIndicator( + aex::WeakPtr factory, + QObject* parent = nullptr); /** * @brief Handles focus in event. @@ -87,6 +88,6 @@ class CF_UI_EXPORT MdFocusIndicator : public QObject { private: float m_progress = 0.0f; - cf::WeakPtr m_animator; + aex::WeakPtr m_animator; }; } // namespace cf::ui::widget::material::base diff --git a/ui/widget/material/base/material_widget_base.cpp b/ui/widget/material/base/material_widget_base.cpp index 742cff4fb..11e991274 100644 --- a/ui/widget/material/base/material_widget_base.cpp +++ b/ui/widget/material/base/material_widget_base.cpp @@ -12,7 +12,7 @@ namespace cf::ui::widget::material::base { MaterialWidgetBase::MaterialWidgetBase(QWidget* owner, const Config& config) : m_owner(owner) { - auto factory = cf::WeakPtr::DynamicCast( + auto factory = aex::WeakPtr::DynamicCast( application_support::Application::animationFactory()); m_stateMachine = new StateMachine(factory, owner); diff --git a/ui/widget/material/base/material_widget_base.h b/ui/widget/material/base/material_widget_base.h index 4a332971a..61f40fdf4 100644 --- a/ui/widget/material/base/material_widget_base.h +++ b/ui/widget/material/base/material_widget_base.h @@ -10,7 +10,7 @@ #pragma once -#include "base/include/base/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr.h" #include "components/material/cfmaterial_animation_factory.h" #include "export.h" #include "focus_ring.h" diff --git a/ui/widget/material/base/ripple_helper.cpp b/ui/widget/material/base/ripple_helper.cpp index 47fd84fa8..972b68ba5 100644 --- a/ui/widget/material/base/ripple_helper.cpp +++ b/ui/widget/material/base/ripple_helper.cpp @@ -28,10 +28,10 @@ using namespace cf::ui::components; /** * @brief Constructor - initializes ripple controller. * - * @param factory WeakPtr to animation factory for ripple animations. + * @param factory aex::WeakPtr to animation factory for ripple animations. * @param parent QObject parent for memory management. */ -RippleHelper::RippleHelper(cf::WeakPtr factory, +RippleHelper::RippleHelper(aex::WeakPtr factory, QObject* parent) : QObject(parent), m_mode(Mode::Bounded), m_color(Qt::black), m_animator(factory) {} @@ -193,7 +193,7 @@ void RippleHelper::onCancel() { namespace { // Material Design 3 ripple fixed opacity (12% as per MD3 specs) constexpr float RIPPLE_FIXED_OPACITY = 0.12f; -} +} // namespace void RippleHelper::paint(QPainter* painter, const QPainterPath& clipPath) { if (m_ripples.isEmpty() || !painter) { diff --git a/ui/widget/material/base/ripple_helper.h b/ui/widget/material/base/ripple_helper.h index 41b0ed8e1..6fb374bc1 100644 --- a/ui/widget/material/base/ripple_helper.h +++ b/ui/widget/material/base/ripple_helper.h @@ -51,7 +51,7 @@ class CF_UI_EXPORT RippleHelper : public QObject { /** * @brief Constructor with animation factory. * - * @param[in] factory WeakPtr to the animation factory. + * @param[in] factory aex::WeakPtr to the animation factory. * @param[in] parent QObject parent. * * @throws None @@ -60,7 +60,7 @@ class CF_UI_EXPORT RippleHelper : public QObject { * @since N/A * @ingroup ui_widget_material_base */ - explicit RippleHelper(cf::WeakPtr factory, + explicit RippleHelper(aex::WeakPtr factory, QObject* parent); /** @@ -177,7 +177,7 @@ class CF_UI_EXPORT RippleHelper : public QObject { QList m_ripples; Mode m_mode = Mode::Bounded; cf::ui::base::CFColor m_color; - cf::WeakPtr m_animator; + aex::WeakPtr m_animator; float maxRadius(const QRectF& rect, const QPointF& center) const; }; diff --git a/ui/widget/material/base/state_machine.cpp b/ui/widget/material/base/state_machine.cpp index 73416a6ea..23989a2fa 100644 --- a/ui/widget/material/base/state_machine.cpp +++ b/ui/widget/material/base/state_machine.cpp @@ -33,10 +33,10 @@ using namespace cf::ui::components; /** * @brief Constructor - initializes state machine with animation factory. * - * @param factory WeakPtr to animation factory for creating transitions. + * @param factory aex::WeakPtr to animation factory for creating transitions. * @param parent QObject parent for memory management. */ -StateMachine::StateMachine(cf::WeakPtr factory, +StateMachine::StateMachine(aex::WeakPtr factory, QObject* parent) : QObject(parent), m_state(State::StateNormal), m_opacity(0.0f) { m_animator = factory; diff --git a/ui/widget/material/base/state_machine.h b/ui/widget/material/base/state_machine.h index 6b22c89e8..6cf1c5168 100644 --- a/ui/widget/material/base/state_machine.h +++ b/ui/widget/material/base/state_machine.h @@ -12,7 +12,7 @@ * @ingroup ui_widget_material_base */ #pragma once -#include "base/include/base/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr.h" #include "components/material/cfmaterial_animation_factory.h" #include "export.h" #include @@ -52,7 +52,7 @@ class CF_UI_EXPORT StateMachine : public QObject { /** * @brief Constructor with animation factory. * - * @param[in] factory WeakPtr to the animation factory. + * @param[in] factory aex::WeakPtr to the animation factory. * @param[in] parent QObject parent. * * @throws None @@ -61,7 +61,7 @@ class CF_UI_EXPORT StateMachine : public QObject { * @since N/A * @ingroup ui_widget_material_base */ - explicit StateMachine(cf::WeakPtr factory, + explicit StateMachine(aex::WeakPtr factory, QObject* parent); /** @@ -319,9 +319,9 @@ class CF_UI_EXPORT StateMachine : public QObject { States m_state = State::StateNormal; float m_opacity = 0.0f; - cf::WeakPtr m_animator; + aex::WeakPtr m_animator; /// Reference to the currently running opacity animation - cf::WeakPtr m_currentAnimation; + aex::WeakPtr m_currentAnimation; }; } // namespace cf::ui::widget::material::base diff --git a/ui/widget/material/widget/checkbox/checkbox.cpp b/ui/widget/material/widget/checkbox/checkbox.cpp index 29f68b68e..07894b408 100644 --- a/ui/widget/material/widget/checkbox/checkbox.cpp +++ b/ui/widget/material/widget/checkbox/checkbox.cpp @@ -102,7 +102,7 @@ void CheckBox::updateAnimationProgress(float progress, bool checked) { void CheckBox::startCheckMarkAnimation(float target) { float fromValue = m_checkAnimationProgress; - auto factory = cf::WeakPtr::DynamicCast( + auto factory = aex::WeakPtr::DynamicCast( application_support::Application::animationFactory()); if (!factory) { diff --git a/ui/widget/material/widget/comboBox/combobox.cpp b/ui/widget/material/widget/comboBox/combobox.cpp index 4186ae564..afe194d5c 100644 --- a/ui/widget/material/widget/comboBox/combobox.cpp +++ b/ui/widget/material/widget/comboBox/combobox.cpp @@ -13,11 +13,11 @@ */ #include "combobox.h" +#include "aex/weak_ptr/weak_ptr.h" #include "application_support/application.h" #include "base/device_pixel.h" #include "base/easing.h" #include "base/geometry_helper.h" -#include "base/include/base/weak_ptr/weak_ptr.h" #include "components/material/cfmaterial_animation_factory.h" #include "components/material/cfmaterial_property_animation.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" @@ -146,7 +146,7 @@ void ComboBox::changeEvent(QEvent* event) { void ComboBox::showPopup() { // Get animation factory locally for custom arrow animation - auto factory = cf::WeakPtr::DynamicCast( + auto factory = aex::WeakPtr::DynamicCast( application_support::Application::animationFactory()); // Start arrow rotation animation @@ -228,7 +228,7 @@ void ComboBox::hidePopup() { } // Get animation factory locally for custom arrow animation - auto factory = cf::WeakPtr::DynamicCast( + auto factory = aex::WeakPtr::DynamicCast( application_support::Application::animationFactory()); // Reset arrow rotation animation diff --git a/ui/widget/material/widget/groupbox/groupbox.cpp b/ui/widget/material/widget/groupbox/groupbox.cpp index ca6619b21..bb5a9a4e0 100644 --- a/ui/widget/material/widget/groupbox/groupbox.cpp +++ b/ui/widget/material/widget/groupbox/groupbox.cpp @@ -1,483 +1,483 @@ -/** - * @file groupbox.cpp - * @brief Material Design 3 GroupBox Implementation - * - * Implements a Material Design 3 group box with rounded corners, - * optional elevation shadows, and theme-aware colors. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "groupbox.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "base/geometry_helper.h" -#include "cfmaterial_animation_factory.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/elevation_controller.h" - -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::base::geometry; -using namespace cf::ui::components; -using namespace cf::ui::components::material; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::material::base; -using namespace cf::ui::widget::application_support; - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -GroupBox::GroupBox(QWidget* parent) : QGroupBox(parent), m_cornerRadius(-1.0f), m_hasBorder(true) { - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_elevation = new MdElevationController(m_animationFactory, this); - - // Set initial elevation (default: level 1 for subtle depth) - m_elevation->setElevation(1); - - // Enable custom drawing without automatic clipping - setAttribute(Qt::WA_OpaquePaintEvent, false); - setAttribute(Qt::WA_TranslucentBackground, false); - - // Disable automatic background drawing to avoid double-draw issues - setAttribute(Qt::WA_StyledBackground, false); - - // Check if title exists - m_hasTitle = !title().isEmpty(); - - // Set initial content margins to ensure proper layout - updateContentMargins(); -} - -GroupBox::GroupBox(const QString& title, QWidget* parent) : GroupBox(parent) { - setTitle(title); - m_hasTitle = !title.isEmpty(); - // Update margins since we now have a title - updateContentMargins(); -} - -GroupBox::~GroupBox() { - // Components are parented to this, Qt will delete them automatically -} - -// ============================================================================ -// Property Getters/Setters -// ============================================================================ - -int GroupBox::elevation() const { - return m_elevation ? m_elevation->elevation() : 0; -} - -void GroupBox::setElevation(int level) { - if (m_elevation) { - m_elevation->setElevation(level); - updateContentMargins(); // Shadow margin changed, update layout - update(); - } -} - -float GroupBox::cornerRadius() const { - // If corner radius is not explicitly set (negative), use default value - if (m_cornerRadius < 0.0f) { - // Use Material Design ShapeSmall (8dp) as default - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(8.0f); - } - return m_cornerRadius; -} - -void GroupBox::setCornerRadius(float radius) { - if (m_cornerRadius != radius) { - m_cornerRadius = radius; - update(); - } -} - -bool GroupBox::hasBorder() const { - return m_hasBorder; -} - -void GroupBox::setHasBorder(bool hasBorder) { - if (m_hasBorder != hasBorder) { - m_hasBorder = hasBorder; - update(); - } -} - -// ============================================================================ -// Size Hints -// ============================================================================ - -QSize GroupBox::sizeHint() const { - // QGroupBox's sizeHint already includes our contentsMargins, - // which we've set to include shadow and title space in updateContentMargins() - return QGroupBox::sizeHint(); -} - -QSize GroupBox::minimumSizeHint() const { - // QGroupBox's minimumSizeHint already includes our contentsMargins - return QGroupBox::minimumSizeHint(); -} - -// ============================================================================ -// Color Access Methods -// ============================================================================ - -namespace { -// Fallback colors when theme is not available -inline CFColor fallbackSurface() { - return CFColor(253, 253, 253); -} // Surface -inline CFColor fallbackSurfaceVariant() { - return CFColor(231, 224, 236); -} // Surface Variant -inline CFColor fallbackOutline() { - return CFColor(120, 124, 132); -} // Outline -inline CFColor fallbackOnSurface() { - return CFColor(27, 27, 31); -} // On Surface -} // namespace - -CFColor GroupBox::surfaceColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(SURFACE)); - } catch (...) { - return fallbackSurface(); - } -} - -CFColor GroupBox::outlineColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOutline(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(OUTLINE)); - } catch (...) { - return fallbackOutline(); - } -} - -CFColor GroupBox::titleColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - return fallbackOnSurface(); - } -} - -QFont GroupBox::titleFont() const { - auto* app = Application::instance(); - if (!app) { - // Fallback to system font with reasonable size - QFont font = QGroupBox::font(); - font.setPixelSize(14); - font.setWeight(QFont::Medium); - return font; - } - - try { - const auto& theme = app->currentTheme(); - auto& fontType = theme.font_type(); - return fontType.queryTargetFont("titleSmall"); - } catch (...) { - QFont font = QGroupBox::font(); - font.setPixelSize(14); - font.setWeight(QFont::Medium); - return font; - } -} - -// ============================================================================ -// Child Event -// ============================================================================ - -void GroupBox::childEvent(QChildEvent* event) { - QGroupBox::childEvent(event); - // Update when children change to ensure proper redraw - if (event->type() == QChildEvent::ChildAdded || event->type() == QChildEvent::ChildRemoved) { - updateGeometry(); - update(); - } -} - -// ============================================================================ -// Change Event -// ============================================================================ - -void GroupBox::changeEvent(QEvent* event) { - QGroupBox::changeEvent(event); - if (event->type() == QEvent::WindowTitleChange) { - m_hasTitle = !title().isEmpty(); - updateContentMargins(); - update(); - } -} - -// ============================================================================ -// Resize Event -// ============================================================================ - -void GroupBox::resizeEvent(QResizeEvent* event) { - QGroupBox::resizeEvent(event); - // Update content margins on resize to ensure proper layout - updateContentMargins(); -} - -// ============================================================================ -// Layout Helpers -// ============================================================================ - -void GroupBox::updateContentMargins() { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Calculate shadow margin (extra space needed for shadow rendering) - QMarginsF shadowMargin = this->shadowMargin(); - int shadowLeft = int(std::ceil(shadowMargin.left())); - int shadowTop = int(std::ceil(shadowMargin.top())); - int shadowRight = int(std::ceil(shadowMargin.right())); - int shadowBottom = int(std::ceil(shadowMargin.bottom())); - - // Calculate title area height (space needed for title at the top) - int titleTopSpace = 0; - if (m_hasTitle) { - QFont font = titleFont(); - QFontMetrics fm(font); - float titleTopPadding = helper.dpToPx(8.0f); // Distance from top of contentRect - float titleHeight = fm.height(); - float titleBottomGap = helper.dpToPx(4.0f); // Extra gap below title - titleTopSpace = int(std::ceil(titleTopPadding + titleHeight + titleBottomGap)); - } - - // Additional padding to keep content away from rounded corners - // This prevents child widgets from drawing into the corner curve area - float radius = cornerRadius(); - int cornerAvoidance = int(std::ceil(radius * 0.6f)); // Keep 60% of radius as clearance - - // Standard content padding - int contentPadding = int(std::ceil(helper.dpToPx(16.0f))); - - // Set content margins: - // - Left/Right: shadow margin + corner avoidance + standard content padding - // - Top: shadow margin + title space + standard content padding - // - Bottom: shadow margin + standard content padding - setContentsMargins( - shadowLeft + cornerAvoidance + contentPadding, shadowTop + titleTopSpace + contentPadding, - shadowRight + cornerAvoidance + contentPadding, shadowBottom + contentPadding); -} - -// ============================================================================ -// Paint Event -// ============================================================================ - -void GroupBox::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - // Calculate content area (inset to make room for shadow) - QMarginsF margin = shadowMargin(); - QRectF contentRect = - QRectF(rect()).adjusted(margin.left(), margin.top(), -margin.right(), -margin.bottom()); - - // Create shape path (rounded corners) from contentRect - QPainterPath shape = roundedRect(contentRect, cornerRadius()); - - // Step 1: Draw shadow (outside contentRect) - drawShadow(p, contentRect, shape); - - // Step 2: Draw background - drawBackground(p, shape); - - // Step 3: Draw border (with title gap if has title) - if (m_hasBorder) { - drawBorder(p, contentRect, shape); - } - - // Step 4: Draw title (straddling the top border) - if (m_hasTitle) { - drawTitle(p, contentRect); - } -} - -// ============================================================================ -// Drawing Helpers -// ============================================================================ - -// Calculate shadow margin (extra space needed for shadow) -QMarginsF GroupBox::shadowMargin() const { - if (!m_elevation || m_elevation->elevation() <= 0) { - return QMarginsF(0, 0, 0, 0); - } - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - // According to elevation level, calculate margin dynamically - // Level 1: blur=2dp, offset=1dp, max offset ~1.5dp - // Reserve margin = offset + blur/2 - int level = m_elevation->elevation(); - float margin = helper.dpToPx(1.0f + level * 1.5f); - return QMarginsF(margin, margin, margin, margin); -} - -void GroupBox::drawShadow(QPainter& p, const QRectF& contentRect, const QPainterPath& shape) { - if (m_elevation && m_elevation->elevation() > 0) { - m_elevation->paintShadow(&p, shape); - } -} - -void GroupBox::drawBackground(QPainter& p, const QPainterPath& shape) { - CFColor bg = surfaceColor(); - - // Group box background is typically surface color - p.fillPath(shape, bg.native_color()); -} - -void GroupBox::drawBorder(QPainter& p, const QRectF& contentRect, const QPainterPath& shape) { - QColor color = outlineColor().native_color(); - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - p.save(); - - // Use 0.5px inset for border (QPen strokes are center-aligned) - float inset = 0.5f; - - // Create inset path for border - QRectF shapeBounds = shape.boundingRect(); - QRectF insetRect = shapeBounds.adjusted(inset, inset, -inset, -inset); - float adjustedRadius = std::max(0.0f, cornerRadius() - inset); - QPainterPath insetShape = roundedRect(insetRect, adjustedRadius); - - QPen pen(color, 1.0); // 1px width - pen.setCosmetic(true); // Keep consistent 1px width across DPI - p.setPen(pen); - p.setBrush(Qt::NoBrush); - - // If has title, we need to clear the border where title sits - if (m_hasTitle) { - QRectF tArea = titleArea(); - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float titleGapExtend = helper.dpToPx(6.0f); - - QRectF gapRect = tArea.adjusted(-titleGapExtend, -helper.dpToPx(2.0f), titleGapExtend, - helper.dpToPx(2.0f)); - - QRegion fullRegion(rect()); - QRegion gapRegion(gapRect.toRect()); - p.setClipRegion(fullRegion - gapRegion); - - p.drawPath(insetShape); - - p.setClipping(false); // 恢复 - } else { - p.drawPath(insetShape); - } - - p.restore(); -} - -void GroupBox::drawTitle(QPainter& p, const QRectF& contentRect) { - QString titleText = title(); - if (titleText.isEmpty()) { - return; - } - - // Get title font and color - QFont font = titleFont(); - QFontMetrics fm(font); - CFColor titleC = titleColor(); - - p.save(); - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float titlePadding = helper.dpToPx(16.0f); - float titleTopPadding = helper.dpToPx(8.0f); // Distance from top of contentRect - float titleHeight = fm.height(); - - // Title is positioned fully inside the contentRect, at the top - // The border and background start below the title area - float titleY = contentRect.top() + titleTopPadding; - - QRectF textRect(contentRect.left() + titlePadding, titleY, - contentRect.width() - titlePadding * 2, titleHeight); - - // Draw title text - p.setFont(font); - QColor textColor = titleC.native_color(); - if (!isEnabled()) { - textColor.setAlphaF(0.38f); - } - p.setPen(textColor); - - p.drawText(textRect, Qt::AlignLeft | Qt::AlignTop, titleText); - - p.restore(); -} - -// ============================================================================ -// Title Area Calculation -// ============================================================================ - -QRectF GroupBox::titleArea() const { - QString titleText = title(); - if (titleText.isEmpty()) { - return QRectF(); - } - - QFont font = titleFont(); - QFontMetrics fm(font); - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float titlePadding = helper.dpToPx(16.0f); - float titleTopPadding = helper.dpToPx(8.0f); - float titleTextWidth = fm.horizontalAdvance(titleText); - float titleHeight = fm.height(); - - QMarginsF margin = shadowMargin(); - QRectF contentRect = - QRectF(rect()).adjusted(margin.left(), margin.top(), -margin.right(), -margin.bottom()); - - // Title area matches drawTitle() position (inside contentRect at top) - float titleY = contentRect.top() + titleTopPadding; - - return QRectF(contentRect.left() + titlePadding, titleY, titleTextWidth, titleHeight); -} - -} // namespace cf::ui::widget::material +/** + * @file groupbox.cpp + * @brief Material Design 3 GroupBox Implementation + * + * Implements a Material Design 3 group box with rounded corners, + * optional elevation shadows, and theme-aware colors. + * + * @author CFDesktop Team + * @date 2026-03-01 + * @version 0.1 + * @since 0.1 + * @ingroup ui_widget_material + */ + +#include "groupbox.h" +#include "application_support/application.h" +#include "base/device_pixel.h" +#include "base/geometry_helper.h" +#include "cfmaterial_animation_factory.h" +#include "core/token/material_scheme/cfmaterial_token_literals.h" +#include "widget/material/base/elevation_controller.h" + +#include +#include +#include +#include +#include + +namespace cf::ui::widget::material { + +using namespace cf::ui::base; +using namespace cf::ui::base::device; +using namespace cf::ui::base::geometry; +using namespace cf::ui::components; +using namespace cf::ui::components::material; +using namespace cf::ui::core; +using namespace cf::ui::core::token::literals; +using namespace cf::ui::widget::material::base; +using namespace cf::ui::widget::application_support; + +// ============================================================================ +// Constructor / Destructor +// ============================================================================ + +GroupBox::GroupBox(QWidget* parent) : QGroupBox(parent), m_cornerRadius(-1.0f), m_hasBorder(true) { + // Get animation factory from Application + m_animationFactory = + aex::WeakPtr::DynamicCast(Application::animationFactory()); + + // Initialize behavior components + m_elevation = new MdElevationController(m_animationFactory, this); + + // Set initial elevation (default: level 1 for subtle depth) + m_elevation->setElevation(1); + + // Enable custom drawing without automatic clipping + setAttribute(Qt::WA_OpaquePaintEvent, false); + setAttribute(Qt::WA_TranslucentBackground, false); + + // Disable automatic background drawing to avoid double-draw issues + setAttribute(Qt::WA_StyledBackground, false); + + // Check if title exists + m_hasTitle = !title().isEmpty(); + + // Set initial content margins to ensure proper layout + updateContentMargins(); +} + +GroupBox::GroupBox(const QString& title, QWidget* parent) : GroupBox(parent) { + setTitle(title); + m_hasTitle = !title.isEmpty(); + // Update margins since we now have a title + updateContentMargins(); +} + +GroupBox::~GroupBox() { + // Components are parented to this, Qt will delete them automatically +} + +// ============================================================================ +// Property Getters/Setters +// ============================================================================ + +int GroupBox::elevation() const { + return m_elevation ? m_elevation->elevation() : 0; +} + +void GroupBox::setElevation(int level) { + if (m_elevation) { + m_elevation->setElevation(level); + updateContentMargins(); // Shadow margin changed, update layout + update(); + } +} + +float GroupBox::cornerRadius() const { + // If corner radius is not explicitly set (negative), use default value + if (m_cornerRadius < 0.0f) { + // Use Material Design ShapeSmall (8dp) as default + CanvasUnitHelper helper(qApp->devicePixelRatio()); + return helper.dpToPx(8.0f); + } + return m_cornerRadius; +} + +void GroupBox::setCornerRadius(float radius) { + if (m_cornerRadius != radius) { + m_cornerRadius = radius; + update(); + } +} + +bool GroupBox::hasBorder() const { + return m_hasBorder; +} + +void GroupBox::setHasBorder(bool hasBorder) { + if (m_hasBorder != hasBorder) { + m_hasBorder = hasBorder; + update(); + } +} + +// ============================================================================ +// Size Hints +// ============================================================================ + +QSize GroupBox::sizeHint() const { + // QGroupBox's sizeHint already includes our contentsMargins, + // which we've set to include shadow and title space in updateContentMargins() + return QGroupBox::sizeHint(); +} + +QSize GroupBox::minimumSizeHint() const { + // QGroupBox's minimumSizeHint already includes our contentsMargins + return QGroupBox::minimumSizeHint(); +} + +// ============================================================================ +// Color Access Methods +// ============================================================================ + +namespace { +// Fallback colors when theme is not available +inline CFColor fallbackSurface() { + return CFColor(253, 253, 253); +} // Surface +inline CFColor fallbackSurfaceVariant() { + return CFColor(231, 224, 236); +} // Surface Variant +inline CFColor fallbackOutline() { + return CFColor(120, 124, 132); +} // Outline +inline CFColor fallbackOnSurface() { + return CFColor(27, 27, 31); +} // On Surface +} // namespace + +CFColor GroupBox::surfaceColor() const { + auto* app = Application::instance(); + if (!app) { + return fallbackSurface(); + } + + try { + const auto& theme = app->currentTheme(); + auto& colorScheme = theme.color_scheme(); + return CFColor(colorScheme.queryColor(SURFACE)); + } catch (...) { + return fallbackSurface(); + } +} + +CFColor GroupBox::outlineColor() const { + auto* app = Application::instance(); + if (!app) { + return fallbackOutline(); + } + + try { + const auto& theme = app->currentTheme(); + auto& colorScheme = theme.color_scheme(); + return CFColor(colorScheme.queryColor(OUTLINE)); + } catch (...) { + return fallbackOutline(); + } +} + +CFColor GroupBox::titleColor() const { + auto* app = Application::instance(); + if (!app) { + return fallbackOnSurface(); + } + + try { + const auto& theme = app->currentTheme(); + auto& colorScheme = theme.color_scheme(); + return CFColor(colorScheme.queryColor(ON_SURFACE)); + } catch (...) { + return fallbackOnSurface(); + } +} + +QFont GroupBox::titleFont() const { + auto* app = Application::instance(); + if (!app) { + // Fallback to system font with reasonable size + QFont font = QGroupBox::font(); + font.setPixelSize(14); + font.setWeight(QFont::Medium); + return font; + } + + try { + const auto& theme = app->currentTheme(); + auto& fontType = theme.font_type(); + return fontType.queryTargetFont("titleSmall"); + } catch (...) { + QFont font = QGroupBox::font(); + font.setPixelSize(14); + font.setWeight(QFont::Medium); + return font; + } +} + +// ============================================================================ +// Child Event +// ============================================================================ + +void GroupBox::childEvent(QChildEvent* event) { + QGroupBox::childEvent(event); + // Update when children change to ensure proper redraw + if (event->type() == QChildEvent::ChildAdded || event->type() == QChildEvent::ChildRemoved) { + updateGeometry(); + update(); + } +} + +// ============================================================================ +// Change Event +// ============================================================================ + +void GroupBox::changeEvent(QEvent* event) { + QGroupBox::changeEvent(event); + if (event->type() == QEvent::WindowTitleChange) { + m_hasTitle = !title().isEmpty(); + updateContentMargins(); + update(); + } +} + +// ============================================================================ +// Resize Event +// ============================================================================ + +void GroupBox::resizeEvent(QResizeEvent* event) { + QGroupBox::resizeEvent(event); + // Update content margins on resize to ensure proper layout + updateContentMargins(); +} + +// ============================================================================ +// Layout Helpers +// ============================================================================ + +void GroupBox::updateContentMargins() { + CanvasUnitHelper helper(qApp->devicePixelRatio()); + + // Calculate shadow margin (extra space needed for shadow rendering) + QMarginsF shadowMargin = this->shadowMargin(); + int shadowLeft = int(std::ceil(shadowMargin.left())); + int shadowTop = int(std::ceil(shadowMargin.top())); + int shadowRight = int(std::ceil(shadowMargin.right())); + int shadowBottom = int(std::ceil(shadowMargin.bottom())); + + // Calculate title area height (space needed for title at the top) + int titleTopSpace = 0; + if (m_hasTitle) { + QFont font = titleFont(); + QFontMetrics fm(font); + float titleTopPadding = helper.dpToPx(8.0f); // Distance from top of contentRect + float titleHeight = fm.height(); + float titleBottomGap = helper.dpToPx(4.0f); // Extra gap below title + titleTopSpace = int(std::ceil(titleTopPadding + titleHeight + titleBottomGap)); + } + + // Additional padding to keep content away from rounded corners + // This prevents child widgets from drawing into the corner curve area + float radius = cornerRadius(); + int cornerAvoidance = int(std::ceil(radius * 0.6f)); // Keep 60% of radius as clearance + + // Standard content padding + int contentPadding = int(std::ceil(helper.dpToPx(16.0f))); + + // Set content margins: + // - Left/Right: shadow margin + corner avoidance + standard content padding + // - Top: shadow margin + title space + standard content padding + // - Bottom: shadow margin + standard content padding + setContentsMargins( + shadowLeft + cornerAvoidance + contentPadding, shadowTop + titleTopSpace + contentPadding, + shadowRight + cornerAvoidance + contentPadding, shadowBottom + contentPadding); +} + +// ============================================================================ +// Paint Event +// ============================================================================ + +void GroupBox::paintEvent(QPaintEvent* event) { + Q_UNUSED(event) + + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + // Calculate content area (inset to make room for shadow) + QMarginsF margin = shadowMargin(); + QRectF contentRect = + QRectF(rect()).adjusted(margin.left(), margin.top(), -margin.right(), -margin.bottom()); + + // Create shape path (rounded corners) from contentRect + QPainterPath shape = roundedRect(contentRect, cornerRadius()); + + // Step 1: Draw shadow (outside contentRect) + drawShadow(p, contentRect, shape); + + // Step 2: Draw background + drawBackground(p, shape); + + // Step 3: Draw border (with title gap if has title) + if (m_hasBorder) { + drawBorder(p, contentRect, shape); + } + + // Step 4: Draw title (straddling the top border) + if (m_hasTitle) { + drawTitle(p, contentRect); + } +} + +// ============================================================================ +// Drawing Helpers +// ============================================================================ + +// Calculate shadow margin (extra space needed for shadow) +QMarginsF GroupBox::shadowMargin() const { + if (!m_elevation || m_elevation->elevation() <= 0) { + return QMarginsF(0, 0, 0, 0); + } + + CanvasUnitHelper helper(qApp->devicePixelRatio()); + // According to elevation level, calculate margin dynamically + // Level 1: blur=2dp, offset=1dp, max offset ~1.5dp + // Reserve margin = offset + blur/2 + int level = m_elevation->elevation(); + float margin = helper.dpToPx(1.0f + level * 1.5f); + return QMarginsF(margin, margin, margin, margin); +} + +void GroupBox::drawShadow(QPainter& p, const QRectF& contentRect, const QPainterPath& shape) { + if (m_elevation && m_elevation->elevation() > 0) { + m_elevation->paintShadow(&p, shape); + } +} + +void GroupBox::drawBackground(QPainter& p, const QPainterPath& shape) { + CFColor bg = surfaceColor(); + + // Group box background is typically surface color + p.fillPath(shape, bg.native_color()); +} + +void GroupBox::drawBorder(QPainter& p, const QRectF& contentRect, const QPainterPath& shape) { + QColor color = outlineColor().native_color(); + if (!isEnabled()) { + color.setAlphaF(0.38f); + } + + p.save(); + + // Use 0.5px inset for border (QPen strokes are center-aligned) + float inset = 0.5f; + + // Create inset path for border + QRectF shapeBounds = shape.boundingRect(); + QRectF insetRect = shapeBounds.adjusted(inset, inset, -inset, -inset); + float adjustedRadius = std::max(0.0f, cornerRadius() - inset); + QPainterPath insetShape = roundedRect(insetRect, adjustedRadius); + + QPen pen(color, 1.0); // 1px width + pen.setCosmetic(true); // Keep consistent 1px width across DPI + p.setPen(pen); + p.setBrush(Qt::NoBrush); + + // If has title, we need to clear the border where title sits + if (m_hasTitle) { + QRectF tArea = titleArea(); + CanvasUnitHelper helper(qApp->devicePixelRatio()); + float titleGapExtend = helper.dpToPx(6.0f); + + QRectF gapRect = tArea.adjusted(-titleGapExtend, -helper.dpToPx(2.0f), titleGapExtend, + helper.dpToPx(2.0f)); + + QRegion fullRegion(rect()); + QRegion gapRegion(gapRect.toRect()); + p.setClipRegion(fullRegion - gapRegion); + + p.drawPath(insetShape); + + p.setClipping(false); // 恢复 + } else { + p.drawPath(insetShape); + } + + p.restore(); +} + +void GroupBox::drawTitle(QPainter& p, const QRectF& contentRect) { + QString titleText = title(); + if (titleText.isEmpty()) { + return; + } + + // Get title font and color + QFont font = titleFont(); + QFontMetrics fm(font); + CFColor titleC = titleColor(); + + p.save(); + + CanvasUnitHelper helper(qApp->devicePixelRatio()); + float titlePadding = helper.dpToPx(16.0f); + float titleTopPadding = helper.dpToPx(8.0f); // Distance from top of contentRect + float titleHeight = fm.height(); + + // Title is positioned fully inside the contentRect, at the top + // The border and background start below the title area + float titleY = contentRect.top() + titleTopPadding; + + QRectF textRect(contentRect.left() + titlePadding, titleY, + contentRect.width() - titlePadding * 2, titleHeight); + + // Draw title text + p.setFont(font); + QColor textColor = titleC.native_color(); + if (!isEnabled()) { + textColor.setAlphaF(0.38f); + } + p.setPen(textColor); + + p.drawText(textRect, Qt::AlignLeft | Qt::AlignTop, titleText); + + p.restore(); +} + +// ============================================================================ +// Title Area Calculation +// ============================================================================ + +QRectF GroupBox::titleArea() const { + QString titleText = title(); + if (titleText.isEmpty()) { + return QRectF(); + } + + QFont font = titleFont(); + QFontMetrics fm(font); + CanvasUnitHelper helper(qApp->devicePixelRatio()); + float titlePadding = helper.dpToPx(16.0f); + float titleTopPadding = helper.dpToPx(8.0f); + float titleTextWidth = fm.horizontalAdvance(titleText); + float titleHeight = fm.height(); + + QMarginsF margin = shadowMargin(); + QRectF contentRect = + QRectF(rect()).adjusted(margin.left(), margin.top(), -margin.right(), -margin.bottom()); + + // Title area matches drawTitle() position (inside contentRect at top) + float titleY = contentRect.top() + titleTopPadding; + + return QRectF(contentRect.left() + titlePadding, titleY, titleTextWidth, titleHeight); +} + +} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/groupbox/groupbox.h b/ui/widget/material/widget/groupbox/groupbox.h index 54aabe288..eaedbd431 100644 --- a/ui/widget/material/widget/groupbox/groupbox.h +++ b/ui/widget/material/widget/groupbox/groupbox.h @@ -1,277 +1,277 @@ -/** - * @file ui/widget/material/widget/groupbox/groupbox.h - * @brief Material Design 3 GroupBox widget. - * - * Implements Material Design 3 group box with rounded corners, optional - * elevation shadows, and theme-aware colors. Provides a container for - * grouping related widgets with a title. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include - -#include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" -#include "export.h" -#include -#include - -namespace cf::ui::widget::material { - -// Forward declarations -namespace base { -class MdElevationController; -} // namespace base - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 GroupBox widget. - * - * @details Implements Material Design 3 group box with rounded corners, - * optional elevation shadows, and theme-aware colors. Provides - * a container for grouping related widgets with a title. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT GroupBox : public QGroupBox { - Q_OBJECT - Q_PROPERTY(int elevation READ elevation WRITE setElevation) - Q_PROPERTY(float cornerRadius READ cornerRadius WRITE setCornerRadius) - Q_PROPERTY(bool hasBorder READ hasBorder WRITE setHasBorder) - - public: - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit GroupBox(QWidget* parent = nullptr); - - /** - * @brief Constructor with title. - * - * @param[in] title Group box title. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit GroupBox(const QString& title, QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~GroupBox() override; - - /** - * @brief Gets the elevation level. - * - * @return Elevation level (0-5). - * - * @throws None - * @note Material Design defines 6 standard levels. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - int elevation() const; - - /** - * @brief Sets the elevation level. - * - * @param[in] level Elevation level (0-5). - * - * @throws None - * @note Affects shadow rendering. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setElevation(int level); - - /** - * @brief Gets the effective corner radius. - * - * @return Corner radius in pixels. Returns 0 for default (auto-calculate). - * - * @throws None - * @note Default is based on Material Design shape scale (ShapeSmall). - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - float cornerRadius() const; - - /** - * @brief Sets the corner radius. - * - * @param[in] radius Corner radius in pixels. - * - * @throws None - * @note Value is clamped to valid range. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setCornerRadius(float radius); - - /** - * @brief Gets whether the group box has a border. - * - * @return true if border is enabled, false otherwise. - * - * @throws None - * @note Default is true. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool hasBorder() const; - - /** - * @brief Sets whether the group box has a border. - * - * @param[in] hasBorder true to enable border, false to disable. - * - * @throws None - * @note When disabled, only shadow is drawn (if elevation > 0). - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setHasBorder(bool hasBorder); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the group box. - * - * @throws None - * @note Based on content and margins. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures minimum content size. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - protected: - /** - * @brief Paints the group box. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements Material Design paint pipeline. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles child event. - * - * @param[in] event Child event. - * - * @throws None - * @note Used to detect layout changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void childEvent(QChildEvent* event) override; - - /** - * @brief Handles widget change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates title tracking when title changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Handles resize event. - * - * @param[in] event Resize event. - * - * @throws None - * @note Updates content margins on resize. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void resizeEvent(QResizeEvent* event) override; - - private: - // Layout helpers - void updateContentMargins(); - - // Drawing helpers - QMarginsF shadowMargin() const; - void drawShadow(QPainter& p, const QRectF& contentRect, const QPainterPath& shape); - void drawBackground(QPainter& p, const QPainterPath& shape); - void drawBorder(QPainter& p, const QRectF& contentRect, const QPainterPath& shape); - void drawTitle(QPainter& p, const QRectF& contentRect); - - // Color access methods - CFColor surfaceColor() const; - CFColor outlineColor() const; - CFColor titleColor() const; - QFont titleFont() const; - - // Calculate title area for background masking - QRectF titleArea() const; - - // Behavior components - cf::WeakPtr m_animationFactory; - base::MdElevationController* m_elevation; - - // Properties - float m_cornerRadius; - bool m_hasBorder; - bool m_hasTitle; -}; - -} // namespace cf::ui::widget::material +/** + * @file ui/widget/material/widget/groupbox/groupbox.h + * @brief Material Design 3 GroupBox widget. + * + * Implements Material Design 3 group box with rounded corners, optional + * elevation shadows, and theme-aware colors. Provides a container for + * grouping related widgets with a title. + * + * @author CFDesktop Team + * @date 2026-03-01 + * @version 0.1 + * @since 0.1 + * @ingroup ui_widget_material_widget + */ +#pragma once + +#include + +#include "aex/weak_ptr/weak_ptr.h" +#include "base/color.h" +#include "cfmaterial_animation_factory.h" +#include "export.h" +#include +#include + +namespace cf::ui::widget::material { + +// Forward declarations +namespace base { +class MdElevationController; +} // namespace base + +using CFColor = cf::ui::base::CFColor; + +/** + * @brief Material Design 3 GroupBox widget. + * + * @details Implements Material Design 3 group box with rounded corners, + * optional elevation shadows, and theme-aware colors. Provides + * a container for grouping related widgets with a title. + * + * @since 0.1 + * @ingroup ui_widget_material_widget + */ +class CF_UI_EXPORT GroupBox : public QGroupBox { + Q_OBJECT + Q_PROPERTY(int elevation READ elevation WRITE setElevation) + Q_PROPERTY(float cornerRadius READ cornerRadius WRITE setCornerRadius) + Q_PROPERTY(bool hasBorder READ hasBorder WRITE setHasBorder) + + public: + /** + * @brief Constructor. + * + * @param[in] parent QObject parent. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + explicit GroupBox(QWidget* parent = nullptr); + + /** + * @brief Constructor with title. + * + * @param[in] title Group box title. + * @param[in] parent QObject parent. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + explicit GroupBox(const QString& title, QWidget* parent = nullptr); + + /** + * @brief Destructor. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + ~GroupBox() override; + + /** + * @brief Gets the elevation level. + * + * @return Elevation level (0-5). + * + * @throws None + * @note Material Design defines 6 standard levels. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + int elevation() const; + + /** + * @brief Sets the elevation level. + * + * @param[in] level Elevation level (0-5). + * + * @throws None + * @note Affects shadow rendering. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setElevation(int level); + + /** + * @brief Gets the effective corner radius. + * + * @return Corner radius in pixels. Returns 0 for default (auto-calculate). + * + * @throws None + * @note Default is based on Material Design shape scale (ShapeSmall). + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + float cornerRadius() const; + + /** + * @brief Sets the corner radius. + * + * @param[in] radius Corner radius in pixels. + * + * @throws None + * @note Value is clamped to valid range. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setCornerRadius(float radius); + + /** + * @brief Gets whether the group box has a border. + * + * @return true if border is enabled, false otherwise. + * + * @throws None + * @note Default is true. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + bool hasBorder() const; + + /** + * @brief Sets whether the group box has a border. + * + * @param[in] hasBorder true to enable border, false to disable. + * + * @throws None + * @note When disabled, only shadow is drawn (if elevation > 0). + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setHasBorder(bool hasBorder); + + /** + * @brief Gets the recommended size. + * + * @return Recommended size for the group box. + * + * @throws None + * @note Based on content and margins. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QSize sizeHint() const override; + + /** + * @brief Gets the minimum recommended size. + * + * @return Minimum recommended size. + * + * @throws None + * @note Ensures minimum content size. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QSize minimumSizeHint() const override; + + protected: + /** + * @brief Paints the group box. + * + * @param[in] event Paint event. + * + * @throws None + * @note Implements Material Design paint pipeline. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void paintEvent(QPaintEvent* event) override; + + /** + * @brief Handles child event. + * + * @param[in] event Child event. + * + * @throws None + * @note Used to detect layout changes. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void childEvent(QChildEvent* event) override; + + /** + * @brief Handles widget change event. + * + * @param[in] event Change event. + * + * @throws None + * @note Updates title tracking when title changes. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void changeEvent(QEvent* event) override; + + /** + * @brief Handles resize event. + * + * @param[in] event Resize event. + * + * @throws None + * @note Updates content margins on resize. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void resizeEvent(QResizeEvent* event) override; + + private: + // Layout helpers + void updateContentMargins(); + + // Drawing helpers + QMarginsF shadowMargin() const; + void drawShadow(QPainter& p, const QRectF& contentRect, const QPainterPath& shape); + void drawBackground(QPainter& p, const QPainterPath& shape); + void drawBorder(QPainter& p, const QRectF& contentRect, const QPainterPath& shape); + void drawTitle(QPainter& p, const QRectF& contentRect); + + // Color access methods + CFColor surfaceColor() const; + CFColor outlineColor() const; + CFColor titleColor() const; + QFont titleFont() const; + + // Calculate title area for background masking + QRectF titleArea() const; + + // Behavior components + aex::WeakPtr m_animationFactory; + base::MdElevationController* m_elevation; + + // Properties + float m_cornerRadius; + bool m_hasBorder; + bool m_hasTitle; +}; + +} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/label/label.h b/ui/widget/material/widget/label/label.h index 055e4d0d2..e256a6426 100644 --- a/ui/widget/material/widget/label/label.h +++ b/ui/widget/material/widget/label/label.h @@ -1,322 +1,322 @@ -/** - * @file ui/widget/material/widget/label/label.h - * @brief Material Design 3 Label widget. - * - * Implements Material Design 3 label with support for multiple typography - * styles (Display, Headline, Title, Body, Label), color variants (OnSurface, - * OnSurfaceVariant, Primary, Secondary, etc.), and theme integration. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "export.h" - -#include -#include - -namespace cf::ui::widget::material { - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Typography style for Material Design 3 labels. - * - * @details Maps to Material Design 3 Type Scale with 15 styles across - * 5 categories: Display, Headline, Title, Body, Label. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -enum class TypographyStyle { - // Display styles (57sp, 45sp, 36sp) - for hero content - DisplayLarge, - DisplayMedium, - DisplaySmall, - - // Headline styles (32sp, 28sp, 24sp) - for app bar text - HeadlineLarge, - HeadlineMedium, - HeadlineSmall, - - // Title styles (22sp, 16sp, 14sp) - for section headings - TitleLarge, - TitleMedium, - TitleSmall, - - // Body styles (16sp, 14sp, 12sp) - for main content - BodyLarge, - BodyMedium, - BodySmall, - - // Label styles (14sp, 12sp, 11sp) - for secondary info - LabelLarge, - LabelMedium, - LabelSmall -}; - -/** - * @brief Color variant for Material Design 3 labels. - * - * @details Controls which color token is used for text rendering. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -enum class LabelColorVariant { - OnSurface, ///< Default on-surface color - OnSurfaceVariant, ///< Variant on-surface color - Primary, ///< Primary brand color - OnPrimary, ///< Text on primary color - Secondary, ///< Secondary brand color - OnSecondary, ///< Text on secondary color - Error, ///< Error color - OnError, ///< Text on error color - InverseSurface, ///< Inverted surface color - InverseOnSurface ///< Text on inverted surface -}; - -/** - * @brief Material Design 3 Label widget. - * - * @details Implements Material Design 3 label with: - * - 15 typography styles from MD3 Type Scale - * - 9 color variants for semantic coloring - * - Theme-aware color and font resolution - * - Disabled state with opacity - * - Automatic theme change handling - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT Label : public QLabel { - Q_OBJECT - Q_PROPERTY(TypographyStyle typographyStyle READ typographyStyle WRITE setTypographyStyle) - Q_PROPERTY(LabelColorVariant colorVariant READ colorVariant WRITE setColorVariant) - Q_PROPERTY(bool autoHiding READ autoHiding WRITE setAutoHiding) - - public: - /** - * @brief Constructor with text and style. - * - * @param[in] text Label text content. - * @param[in] style Typography style. - * @param[in] parent QObject parent. - * - * @throws None - * @note Default style is BodyMedium. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit Label(const QString& text = QString(), - TypographyStyle style = TypographyStyle::BodyMedium, QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~Label() override; - - /** - * @brief Gets the typography style. - * - * @return Current typography style. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - TypographyStyle typographyStyle() const; - - /** - * @brief Sets the typography style. - * - * @param[in] style Typography style to apply. - * - * @throws None - * @note Updates font and triggers repaint. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setTypographyStyle(TypographyStyle style); - - /** - * @brief Gets the color variant. - * - * @return Current color variant. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - LabelColorVariant colorVariant() const; - - /** - * @brief Sets the color variant. - * - * @param[in] variant Color variant to apply. - * - * @throws None - * @note Updates text color and triggers repaint. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setColorVariant(LabelColorVariant variant); - - /** - * @brief Gets whether auto-hiding is enabled. - * - * @return true if label hides when empty, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool autoHiding() const; - - /** - * @brief Sets whether auto-hiding is enabled. - * - * @param[in] enabled true to hide when text is empty, false otherwise. - * - * @throws None - * @note When enabled, label becomes hidden when text is empty. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setAutoHiding(bool enabled); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the label. - * - * @throws None - * @note Based on text, font, and word wrap setting. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures minimum touch target size for interactive labels. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - protected: - /** - * @brief Paints the label. - * - * @param[in] event Paint event. - * - * @throws None - * @note Renders text with theme-aware color and font. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - private: - /** - * @brief Updates the label's appearance. - * - * @throws None - * @note Refreshes font, color, and auto-hiding state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void updateAppearance(); - - /** - * @brief Gets the text color based on current variant. - * - * @return Color for text rendering. - * - * @throws None - * @note Respects enabled state and theme. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - CFColor textColor() const; - - /** - * @brief Gets the font for current typography style. - * - * @return Font matching the current style. - * - * @throws None - * @note Queries theme for typography token. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QFont typographyFont() const; - - /** - * @brief Gets the typography token name for a style. - * - * @param[in] style Typography style. - * @return Token name string. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - static const char* typographyTokenName(TypographyStyle style); - - TypographyStyle typographyStyle_; - LabelColorVariant colorVariant_; - bool autoHiding_; - mutable CFColor cachedColor_; ///< Cached color to avoid repeated theme queries - mutable bool colorCacheValid_; ///< Whether the cached color is valid -}; - -} // namespace cf::ui::widget::material +/** + * @file ui/widget/material/widget/label/label.h + * @brief Material Design 3 Label widget. + * + * Implements Material Design 3 label with support for multiple typography + * styles (Display, Headline, Title, Body, Label), color variants (OnSurface, + * OnSurfaceVariant, Primary, Secondary, etc.), and theme integration. + * + * @author CFDesktop Team + * @date 2026-03-01 + * @version 0.1 + * @since 0.1 + * @ingroup ui_widget_material_widget + */ +#pragma once + +#include "aex/weak_ptr/weak_ptr.h" +#include "base/color.h" +#include "export.h" + +#include +#include + +namespace cf::ui::widget::material { + +using CFColor = cf::ui::base::CFColor; + +/** + * @brief Typography style for Material Design 3 labels. + * + * @details Maps to Material Design 3 Type Scale with 15 styles across + * 5 categories: Display, Headline, Title, Body, Label. + * + * @since 0.1 + * @ingroup ui_widget_material_widget + */ +enum class TypographyStyle { + // Display styles (57sp, 45sp, 36sp) - for hero content + DisplayLarge, + DisplayMedium, + DisplaySmall, + + // Headline styles (32sp, 28sp, 24sp) - for app bar text + HeadlineLarge, + HeadlineMedium, + HeadlineSmall, + + // Title styles (22sp, 16sp, 14sp) - for section headings + TitleLarge, + TitleMedium, + TitleSmall, + + // Body styles (16sp, 14sp, 12sp) - for main content + BodyLarge, + BodyMedium, + BodySmall, + + // Label styles (14sp, 12sp, 11sp) - for secondary info + LabelLarge, + LabelMedium, + LabelSmall +}; + +/** + * @brief Color variant for Material Design 3 labels. + * + * @details Controls which color token is used for text rendering. + * + * @since 0.1 + * @ingroup ui_widget_material_widget + */ +enum class LabelColorVariant { + OnSurface, ///< Default on-surface color + OnSurfaceVariant, ///< Variant on-surface color + Primary, ///< Primary brand color + OnPrimary, ///< Text on primary color + Secondary, ///< Secondary brand color + OnSecondary, ///< Text on secondary color + Error, ///< Error color + OnError, ///< Text on error color + InverseSurface, ///< Inverted surface color + InverseOnSurface ///< Text on inverted surface +}; + +/** + * @brief Material Design 3 Label widget. + * + * @details Implements Material Design 3 label with: + * - 15 typography styles from MD3 Type Scale + * - 9 color variants for semantic coloring + * - Theme-aware color and font resolution + * - Disabled state with opacity + * - Automatic theme change handling + * + * @since 0.1 + * @ingroup ui_widget_material_widget + */ +class CF_UI_EXPORT Label : public QLabel { + Q_OBJECT + Q_PROPERTY(TypographyStyle typographyStyle READ typographyStyle WRITE setTypographyStyle) + Q_PROPERTY(LabelColorVariant colorVariant READ colorVariant WRITE setColorVariant) + Q_PROPERTY(bool autoHiding READ autoHiding WRITE setAutoHiding) + + public: + /** + * @brief Constructor with text and style. + * + * @param[in] text Label text content. + * @param[in] style Typography style. + * @param[in] parent QObject parent. + * + * @throws None + * @note Default style is BodyMedium. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + explicit Label(const QString& text = QString(), + TypographyStyle style = TypographyStyle::BodyMedium, QWidget* parent = nullptr); + + /** + * @brief Destructor. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + ~Label() override; + + /** + * @brief Gets the typography style. + * + * @return Current typography style. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + TypographyStyle typographyStyle() const; + + /** + * @brief Sets the typography style. + * + * @param[in] style Typography style to apply. + * + * @throws None + * @note Updates font and triggers repaint. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setTypographyStyle(TypographyStyle style); + + /** + * @brief Gets the color variant. + * + * @return Current color variant. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + LabelColorVariant colorVariant() const; + + /** + * @brief Sets the color variant. + * + * @param[in] variant Color variant to apply. + * + * @throws None + * @note Updates text color and triggers repaint. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setColorVariant(LabelColorVariant variant); + + /** + * @brief Gets whether auto-hiding is enabled. + * + * @return true if label hides when empty, false otherwise. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + bool autoHiding() const; + + /** + * @brief Sets whether auto-hiding is enabled. + * + * @param[in] enabled true to hide when text is empty, false otherwise. + * + * @throws None + * @note When enabled, label becomes hidden when text is empty. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setAutoHiding(bool enabled); + + /** + * @brief Gets the recommended size. + * + * @return Recommended size for the label. + * + * @throws None + * @note Based on text, font, and word wrap setting. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QSize sizeHint() const override; + + /** + * @brief Gets the minimum recommended size. + * + * @return Minimum recommended size. + * + * @throws None + * @note Ensures minimum touch target size for interactive labels. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QSize minimumSizeHint() const override; + + protected: + /** + * @brief Paints the label. + * + * @param[in] event Paint event. + * + * @throws None + * @note Renders text with theme-aware color and font. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void paintEvent(QPaintEvent* event) override; + + /** + * @brief Handles widget state change event. + * + * @param[in] event Change event. + * + * @throws None + * @note Updates appearance based on state changes. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void changeEvent(QEvent* event) override; + + private: + /** + * @brief Updates the label's appearance. + * + * @throws None + * @note Refreshes font, color, and auto-hiding state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void updateAppearance(); + + /** + * @brief Gets the text color based on current variant. + * + * @return Color for text rendering. + * + * @throws None + * @note Respects enabled state and theme. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + CFColor textColor() const; + + /** + * @brief Gets the font for current typography style. + * + * @return Font matching the current style. + * + * @throws None + * @note Queries theme for typography token. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QFont typographyFont() const; + + /** + * @brief Gets the typography token name for a style. + * + * @param[in] style Typography style. + * @return Token name string. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + static const char* typographyTokenName(TypographyStyle style); + + TypographyStyle typographyStyle_; + LabelColorVariant colorVariant_; + bool autoHiding_; + mutable CFColor cachedColor_; ///< Cached color to avoid repeated theme queries + mutable bool colorCacheValid_; ///< Whether the cached color is valid +}; + +} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/progressbar/progressbar.cpp b/ui/widget/material/widget/progressbar/progressbar.cpp index 59e6fab15..2ef64f055 100644 --- a/ui/widget/material/widget/progressbar/progressbar.cpp +++ b/ui/widget/material/widget/progressbar/progressbar.cpp @@ -14,11 +14,11 @@ */ #include "progressbar.h" +#include "aex/weak_ptr/weak_ptr.h" #include "application_support/application.h" #include "base/device_pixel.h" #include "base/easing.h" #include "base/geometry_helper.h" -#include "base/include/base/weak_ptr/weak_ptr.h" #include "components/material/cfmaterial_animation_factory.h" #include "components/material/cfmaterial_property_animation.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" @@ -227,7 +227,7 @@ QRectF ProgressBar::trackRect() const { void ProgressBar::startIndeterminateAnimation() { // Get animation factory locally for custom indeterminate animation - auto factory = cf::WeakPtr::DynamicCast( + auto factory = aex::WeakPtr::DynamicCast( application_support::Application::animationFactory()); if (!factory || m_indeterminateAnimating) { diff --git a/ui/widget/material/widget/radiobutton/radiobutton.cpp b/ui/widget/material/widget/radiobutton/radiobutton.cpp index 573e0d209..e65e8b7d3 100644 --- a/ui/widget/material/widget/radiobutton/radiobutton.cpp +++ b/ui/widget/material/widget/radiobutton/radiobutton.cpp @@ -370,7 +370,7 @@ void RadioButton::startInnerCircleAnimation(bool checked) { float targetScale = checked ? INNER_CIRCLE_SCALE_CHECKED : INNER_CIRCLE_SCALE_UNCHECKED; float fromScale = m_innerCircleScale; - auto factory = cf::WeakPtr::DynamicCast( + auto factory = aex::WeakPtr::DynamicCast( Application::animationFactory()); if (!factory) { diff --git a/ui/widget/material/widget/scrollview/scrollview.cpp b/ui/widget/material/widget/scrollview/scrollview.cpp index f8a9824c7..cb1bd04ed 100644 --- a/ui/widget/material/widget/scrollview/scrollview.cpp +++ b/ui/widget/material/widget/scrollview/scrollview.cpp @@ -105,7 +105,7 @@ class ScrollbarOverlay : public QWidget { ScrollView::ScrollView(QWidget* parent) : QScrollArea(parent) { // Get animation factory from Application m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); + aex::WeakPtr::DynamicCast(Application::animationFactory()); // Use AsNeeded as default policy for custom scrollbars setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); diff --git a/ui/widget/material/widget/scrollview/scrollview.h b/ui/widget/material/widget/scrollview/scrollview.h index 21cd09a43..9aa538b5c 100644 --- a/ui/widget/material/widget/scrollview/scrollview.h +++ b/ui/widget/material/widget/scrollview/scrollview.h @@ -14,8 +14,8 @@ */ #pragma once +#include "aex/weak_ptr/weak_ptr.h" #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" #include "cfmaterial_animation_factory.h" #include "export.h" #include @@ -387,7 +387,7 @@ class CF_UI_EXPORT ScrollView : public QScrollArea { void stopFadeTimer(); // Behavior components - cf::WeakPtr m_animationFactory; + aex::WeakPtr m_animationFactory; // Scrollbar state bool m_scrollbarFadeEnabled = true; diff --git a/ui/widget/material/widget/switch/switch.cpp b/ui/widget/material/widget/switch/switch.cpp index fff18c516..f181041f3 100644 --- a/ui/widget/material/widget/switch/switch.cpp +++ b/ui/widget/material/widget/switch/switch.cpp @@ -332,7 +332,7 @@ QRectF Switch::textRect() const { // ============================================================================ void Switch::startThumbPositionAnimation(float target) { - auto factory = cf::WeakPtr::DynamicCast( + auto factory = aex::WeakPtr::DynamicCast( application_support::Application::animationFactory()); if (!factory) { diff --git a/ui/widget/material/widget/tabview/private/materialtabbar.cpp b/ui/widget/material/widget/tabview/private/materialtabbar.cpp index adaa9e08d..48a9eca86 100644 --- a/ui/widget/material/widget/tabview/private/materialtabbar.cpp +++ b/ui/widget/material/widget/tabview/private/materialtabbar.cpp @@ -257,7 +257,7 @@ void MaterialTabBar::animateIndicatorTo(int index) { m_lastIndex = index; auto factory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); + aex::WeakPtr::DynamicCast(Application::animationFactory()); if (!factory || !factory->isAllEnabled()) { m_indicatorPosition = static_cast(tabRect(index).left()); update(); diff --git a/ui/widget/material/widget/tabview/private/materialtabbar.h b/ui/widget/material/widget/tabview/private/materialtabbar.h index 254d1012e..90bea76de 100644 --- a/ui/widget/material/widget/tabview/private/materialtabbar.h +++ b/ui/widget/material/widget/tabview/private/materialtabbar.h @@ -20,7 +20,7 @@ #include "base/device_pixel.h" -#include "base/include/base/weak_ptr/weak_ptr.h" +#include "aex/weak_ptr/weak_ptr.h" #include "material/widget/button/button.h" #include "widget/material/base/material_widget_base.h" @@ -383,7 +383,7 @@ class MaterialTabBar : public QTabBar { QSet m_closeableTabs; // Set of closeable tab indices - cf::WeakPtr m_indicatorAnimation; + aex::WeakPtr m_indicatorAnimation; base::MaterialWidgetBase m_material; }; diff --git a/ui/widget/material/widget/textarea/textarea.cpp b/ui/widget/material/widget/textarea/textarea.cpp index 05feff4e6..d6ebc703a 100644 --- a/ui/widget/material/widget/textarea/textarea.cpp +++ b/ui/widget/material/widget/textarea/textarea.cpp @@ -817,7 +817,7 @@ void TextArea::animateFloatingTo(bool floating) { return; } - auto factory = cf::WeakPtr::DynamicCast( + auto factory = aex::WeakPtr::DynamicCast( application_support::Application::animationFactory()); if (!factory) { diff --git a/ui/widget/material/widget/textfield/textfield.cpp b/ui/widget/material/widget/textfield/textfield.cpp index 4c4d6ad41..97fb8ef8a 100644 --- a/ui/widget/material/widget/textfield/textfield.cpp +++ b/ui/widget/material/widget/textfield/textfield.cpp @@ -14,10 +14,10 @@ */ #include "textfield.h" +#include "aex/weak_ptr/weak_ptr.h" #include "application_support/application.h" #include "base/device_pixel.h" #include "base/geometry_helper.h" -#include "base/include/base/weak_ptr/weak_ptr.h" #include "cfmaterial_animation_factory.h" #include "components/material/cfmaterial_property_animation.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" @@ -1054,7 +1054,7 @@ void TextField::animateFloatingTo(bool floating) { } // Get animation factory locally for custom property animations - auto factory = cf::WeakPtr::DynamicCast( + auto factory = aex::WeakPtr::DynamicCast( application_support::Application::animationFactory()); if (!factory) { From 8941016bb03bc25758333eeb56762793cacbfc64 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Fri, 19 Jun 2026 08:58:53 +0800 Subject: [PATCH 14/18] feat: extract ui into QuarkWidgets submodule (Phase 2) Move the entire ui/ tree out of CFDesktop into the new QuarkWidgets org-level Qt6 Material 3 widget library (third_party/QuarkWidgets submodule @ v0.1.0): - Migrated to QuarkWidgets: ui/ (base/core/components/widget/models), test/ui (36 files), example/{gui,ui} (80 files). Namespace cf::ui:: -> qw::, export macro CF_UI_EXPORT -> QW_EXPORT, targets cfui -> quarkwidgets and cf_ui_* -> qw_ui_* (alias QuarkWidgets::quarkwidgets). - CFDesktop consumes QuarkWidgets via setup_third_party(); removed add_subdirectory(ui). desktop links QuarkWidgets::quarkwidgets; cf::ui references in desktop rewritten to qw. desktop include style ("core/theme_manager.h", "ui/widget/...") preserved via QuarkWidgets' PUBLIC include dirs. - Removed ui/, test/ui, example/{gui,ui} from CFDesktop. QuarkWidgets standalone: 500/500 tests pass. CFDesktop: build green, 10/10 tests (ui tests now run from QuarkWidgets). --- .gitmodules | 3 + CMakeLists.txt | 11 +- desktop/CMakeLists.txt | 4 +- desktop/main.cpp | 2 +- desktop/ui/CMakeLists.txt | 2 +- .../shell_layer_impl/CMakeLists.txt | 2 +- .../ui/components/statusbar/CMakeLists.txt | 2 +- .../ui/components/statusbar/status_bar.cpp | 12 +- desktop/ui/components/taskbar/CMakeLists.txt | 2 +- .../components/taskbar/centered_taskbar.cpp | 8 +- .../ui/components/taskbar/taskbar_icon.cpp | 8 +- .../ui/components/wallpaper/CMakeLists.txt | 2 +- desktop/ui/platform/CMakeLists.txt | 2 +- example/CMakeLists.txt | 3 +- example/gui/CMakeLists.txt | 4 - example/gui/material/CMakeLists.txt | 6 - .../material_color_scheme/CMakeLists.txt | 26 - .../MaterialColorSchemeMainWindow.cpp | 572 --------- .../MaterialColorSchemeMainWindow.h | 204 ---- .../material/material_color_scheme/main.cpp | 30 - .../material_motion_spec/CMakeLists.txt | 26 - .../MaterialMotionSpecMainWindow.cpp | 674 ---------- .../MaterialMotionSpecMainWindow.h | 230 ---- .../material/material_motion_spec/main.cpp | 30 - .../material_radius_scale/CMakeLists.txt | 26 - .../MaterialRadiusScaleMainWindow.cpp | 172 --- .../MaterialRadiusScaleMainWindow.h | 84 -- .../material/material_radius_scale/main.cpp | 30 - .../material_typography/CMakeLists.txt | 26 - .../MaterialTypographyMainWindow.cpp | 459 ------- .../MaterialTypographyMainWindow.h | 154 --- .../gui/material/material_typography/main.cpp | 31 - example/gui/theme/CMakeLists.txt | 40 - example/gui/theme/ColorSchemePage.cpp | 400 ------ example/gui/theme/ColorSchemePage.h | 130 -- .../gui/theme/MaterialGalleryMainWindow.cpp | 274 ----- example/gui/theme/MaterialGalleryMainWindow.h | 124 -- example/gui/theme/MotionSpecPage.cpp | 558 --------- example/gui/theme/MotionSpecPage.h | 177 --- example/gui/theme/RadiusScalePage.cpp | 217 ---- example/gui/theme/RadiusScalePage.h | 73 -- example/gui/theme/ThemePageWidget.cpp | 16 - example/gui/theme/ThemePageWidget.h | 46 - example/gui/theme/ThemeSidebar.cpp | 176 --- example/gui/theme/ThemeSidebar.h | 105 -- example/gui/theme/ToastWidget.cpp | 82 -- example/gui/theme/ToastWidget.h | 33 - example/gui/theme/TypographyPage.cpp | 420 ------- example/gui/theme/TypographyPage.h | 124 -- example/gui/theme/main.cpp | 27 - example/ui/CMakeLists.txt | 3 - example/ui/widget/CMakeLists.txt | 2 - .../ui/widget/material_example/CMakeLists.txt | 62 - .../MaterialGalleryWindow.cpp | 185 --- .../material_example/MaterialGalleryWindow.h | 38 - .../material_example/demos/ButtonDemo.cpp | 275 ----- .../material_example/demos/ButtonDemo.h | 41 - .../material_example/demos/CheckBoxDemo.cpp | 378 ------ .../material_example/demos/CheckBoxDemo.h | 47 - .../material_example/demos/ComboBoxDemo.cpp | 213 ---- .../material_example/demos/ComboBoxDemo.h | 42 - .../demos/DoubleSpinBoxDemo.cpp | 390 ------ .../demos/DoubleSpinBoxDemo.h | 48 - .../material_example/demos/GroupBoxDemo.cpp | 310 ----- .../material_example/demos/GroupBoxDemo.h | 49 - .../material_example/demos/LabelDemo.cpp | 322 ----- .../widget/material_example/demos/LabelDemo.h | 61 - .../material_example/demos/ListViewDemo.cpp | 219 ---- .../material_example/demos/ListViewDemo.h | 40 - .../demos/ProgressBarDemo.cpp | 268 ---- .../material_example/demos/ProgressBarDemo.h | 66 - .../demos/RadioButtonDemo.cpp | 255 ---- .../material_example/demos/RadioButtonDemo.h | 41 - .../material_example/demos/ScrollViewDemo.cpp | 316 ----- .../material_example/demos/ScrollViewDemo.h | 38 - .../material_example/demos/SeparatorDemo.cpp | 163 --- .../material_example/demos/SeparatorDemo.h | 37 - .../material_example/demos/SliderDemo.cpp | 250 ---- .../material_example/demos/SliderDemo.h | 44 - .../material_example/demos/SpinBoxDemo.cpp | 305 ----- .../material_example/demos/SpinBoxDemo.h | 40 - .../material_example/demos/SwitchDemo.cpp | 188 --- .../material_example/demos/SwitchDemo.h | 41 - .../material_example/demos/TabViewDemo.cpp | 272 ----- .../material_example/demos/TabViewDemo.h | 38 - .../material_example/demos/TableViewDemo.cpp | 427 ------- .../material_example/demos/TableViewDemo.h | 48 - .../material_example/demos/TextAreaDemo.cpp | 261 ---- .../material_example/demos/TextAreaDemo.h | 41 - .../material_example/demos/TextFieldDemo.cpp | 265 ---- .../material_example/demos/TextFieldDemo.h | 38 - .../material_example/demos/TreeViewDemo.cpp | 340 ------ .../material_example/demos/TreeViewDemo.h | 50 - example/ui/widget/material_example/main.cpp | 33 - test/CMakeLists.txt | 3 +- test/ui/CMakeLists.txt | 5 - test/ui/base/CMakeLists.txt | 67 - test/ui/base/color_helper_test.cpp | 422 ------- test/ui/base/color_test.cpp | 384 ------ test/ui/base/device_pixel_test.cpp | 404 ------ test/ui/base/easing_test.cpp | 422 ------- test/ui/base/geometry_helper_test.cpp | 390 ------ test/ui/base/math_helper_test.cpp | 411 ------- test/ui/components/CMakeLists.txt | 63 - .../components/elevation_controller_test.cpp | 323 ----- test/ui/components/focus_ring_test.cpp | 234 ---- test/ui/components/painter_layer_test.cpp | 81 -- test/ui/components/ripple_helper_test.cpp | 279 ----- test/ui/components/state_machine_test.cpp | 185 --- test/ui/core/CMakeLists.txt | 1 - test/ui/widget/CMakeLists.txt | 221 ---- test/ui/widget/button_test.cpp | 187 --- test/ui/widget/checkbox_test.cpp | 152 --- test/ui/widget/combobox_test.cpp | 118 -- test/ui/widget/doublespinbox_test.cpp | 106 -- test/ui/widget/groupbox_test.cpp | 129 -- test/ui/widget/label_test.cpp | 170 --- test/ui/widget/listview_test.cpp | 70 -- test/ui/widget/progressbar_test.cpp | 123 -- test/ui/widget/radiobutton_test.cpp | 135 -- test/ui/widget/scrollview_test.cpp | 88 -- test/ui/widget/separator_test.cpp | 99 -- test/ui/widget/slider_test.cpp | 124 -- test/ui/widget/spinbox_test.cpp | 111 -- test/ui/widget/switch_test.cpp | 112 -- test/ui/widget/tableview_test.cpp | 55 - test/ui/widget/tabview_test.cpp | 117 -- test/ui/widget/textarea_test.cpp | 153 --- test/ui/widget/textfield_test.cpp | 184 --- test/ui/widget/treeview_test.cpp | 65 - test/ui/widget/widget_test_helper.h | 117 -- third_party/QuarkWidgets | 1 + ui/.gitignore | 3 - ui/CMakeLists.txt | 83 -- ui/base/CMakeLists.txt | 27 - ui/base/color.cpp | 301 ----- ui/base/color.h | 262 ---- ui/base/color_helper.cpp | 124 -- ui/base/color_helper.h | 78 -- ui/base/device_pixel.cpp | 50 - ui/base/device_pixel.h | 137 --- ui/base/easing.cpp | 77 -- ui/base/easing.h | 128 -- ui/base/geometry_helper.cpp | 126 -- ui/base/geometry_helper.h | 86 -- ui/base/math_helper.cpp | 136 --- ui/base/math_helper.h | 162 --- ui/base/qt_format.h | 201 --- ui/components/CMakeLists.txt | 34 - ui/components/animation.cpp | 41 - ui/components/animation.h | 267 ---- ui/components/animation_factory_manager.cpp | 23 - ui/components/animation_factory_manager.h | 320 ----- ui/components/animation_group.h | 82 -- ui/components/material/CMakeLists.txt | 32 - .../material/cfmaterial_animation_factory.cpp | 376 ------ .../material/cfmaterial_animation_factory.h | 548 --------- .../cfmaterial_animation_strategy.cpp | 43 - .../material/cfmaterial_animation_strategy.h | 301 ----- .../material/cfmaterial_fade_animation.cpp | 212 ---- .../material/cfmaterial_fade_animation.h | 262 ---- .../cfmaterial_property_animation.cpp | 194 --- .../material/cfmaterial_property_animation.h | 257 ---- .../material/cfmaterial_scale_animation.cpp | 221 ---- .../material/cfmaterial_scale_animation.h | 291 ----- .../material/cfmaterial_slide_animation.cpp | 214 ---- .../material/cfmaterial_slide_animation.h | 343 ------ .../material/token/animation_token_literals.h | 198 --- .../material/token/animation_token_mapping.h | 292 ----- ui/components/spring_animation.h | 124 -- ui/components/timing_animation.cpp | 23 - ui/components/timing_animation.h | 100 -- ui/core/CMakeLists.txt | 37 - ui/core/color_scheme.h | 88 -- ui/core/font_type.h | 52 - ui/core/material/CMakeLists.txt | 30 - ui/core/material/cfmaterial_fonttype.cpp | 116 -- ui/core/material/cfmaterial_fonttype.h | 133 -- ui/core/material/cfmaterial_motion.cpp | 76 -- ui/core/material/cfmaterial_motion.h | 352 ------ ui/core/material/cfmaterial_radius_scale.cpp | 35 - ui/core/material/cfmaterial_radius_scale.h | 92 -- ui/core/material/cfmaterial_scheme.cpp | 45 - ui/core/material/cfmaterial_scheme.h | 120 -- ui/core/material/cfmaterial_theme.cpp | 27 - ui/core/material/cfmaterial_theme.h | 87 -- ui/core/material/material_factory.cpp | 540 -------- ui/core/material/material_factory.hpp | 244 ---- ui/core/material/material_factory_class.h | 97 -- ui/core/motion_spec.h | 65 - ui/core/radius_scale.h | 53 - ui/core/theme.h | 118 -- ui/core/theme_factory.h | 97 -- ui/core/theme_manager.cpp | 96 -- ui/core/theme_manager.h | 200 --- .../cfmaterial_token_literals.h | 280 ----- .../motion/cfmaterial_motion_token_literals.h | 190 --- .../cfmaterial_radius_scale_literals.h | 92 -- .../token/theme_name/material_theme_name.h | 28 - .../cfmaterial_typography_token_literals.h | 301 ----- ui/export.h | 40 - ui/models/CMakeLists.txt | 12 - ui/widget/CMakeLists.txt | 92 -- ui/widget/application_support/application.cpp | 188 --- ui/widget/application_support/application.h | 431 ------- .../application/material_application.cpp | 49 - .../application/material_application.h | 98 -- .../material/base/elevation_controller.cpp | 376 ------ .../material/base/elevation_controller.h | 275 ----- ui/widget/material/base/focus_ring.cpp | 158 --- ui/widget/material/base/focus_ring.h | 93 -- .../material/base/material_widget_base.cpp | 95 -- .../material/base/material_widget_base.h | 214 ---- ui/widget/material/base/painter_layer.cpp | 56 - ui/widget/material/base/painter_layer.h | 94 -- ui/widget/material/base/ripple_helper.cpp | 247 ---- ui/widget/material/base/ripple_helper.h | 185 --- ui/widget/material/base/state_machine.cpp | 250 ---- ui/widget/material/base/state_machine.h | 327 ----- ui/widget/material/widget/button/button.cpp | 599 --------- ui/widget/material/widget/button/button.h | 398 ------ .../material/widget/checkbox/checkbox.cpp | 605 --------- ui/widget/material/widget/checkbox/checkbox.h | 340 ------ .../material/widget/comboBox/combobox.cpp | 603 --------- ui/widget/material/widget/comboBox/combobox.h | 294 ----- .../widget/doublespinbox/doublespinbox.cpp | 655 ---------- .../widget/doublespinbox/doublespinbox.h | 272 ----- .../material/widget/groupbox/groupbox.cpp | 483 -------- ui/widget/material/widget/groupbox/groupbox.h | 277 ----- ui/widget/material/widget/label/label.cpp | 433 ------- ui/widget/material/widget/label/label.h | 322 ----- .../material/widget/listview/listview.cpp | 686 ----------- ui/widget/material/widget/listview/listview.h | 352 ------ .../widget/progressbar/progressbar.cpp | 432 ------- .../material/widget/progressbar/progressbar.h | 200 --- .../widget/radiobutton/radiobutton.cpp | 587 --------- .../material/widget/radiobutton/radiobutton.h | 340 ------ .../material/widget/scrollview/scrollview.cpp | 918 -------------- .../material/widget/scrollview/scrollview.h | 421 ------- .../material/widget/separator/separator.cpp | 208 ---- .../material/widget/separator/separator.h | 194 --- ui/widget/material/widget/slider/slider.cpp | 483 -------- ui/widget/material/widget/slider/slider.h | 254 ---- ui/widget/material/widget/spinbox/spinbox.cpp | 689 ----------- ui/widget/material/widget/spinbox/spinbox.h | 271 ---- ui/widget/material/widget/switch/switch.cpp | 500 -------- ui/widget/material/widget/switch/switch.h | 288 ----- .../material/widget/tableview/tableview.cpp | 547 --------- .../material/widget/tableview/tableview.h | 414 ------- .../widget/tabview/private/materialtabbar.cpp | 503 -------- .../widget/tabview/private/materialtabbar.h | 390 ------ ui/widget/material/widget/tabview/tabview.cpp | 89 -- ui/widget/material/widget/tabview/tabview.h | 192 --- .../material/widget/textarea/textarea.cpp | 871 ------------- ui/widget/material/widget/textarea/textarea.h | 533 -------- .../material/widget/textfield/textfield.cpp | 1085 ----------------- .../material/widget/textfield/textfield.h | 528 -------- .../treeview/private/treeviewdelegate.cpp | 727 ----------- .../treeview/private/treeviewdelegate.h | 238 ---- .../material/widget/treeview/treeview.cpp | 452 ------- ui/widget/material/widget/treeview/treeview.h | 410 ------- 261 files changed, 35 insertions(+), 51568 deletions(-) delete mode 100644 example/gui/CMakeLists.txt delete mode 100644 example/gui/material/CMakeLists.txt delete mode 100644 example/gui/material/material_color_scheme/CMakeLists.txt delete mode 100644 example/gui/material/material_color_scheme/MaterialColorSchemeMainWindow.cpp delete mode 100644 example/gui/material/material_color_scheme/MaterialColorSchemeMainWindow.h delete mode 100644 example/gui/material/material_color_scheme/main.cpp delete mode 100644 example/gui/material/material_motion_spec/CMakeLists.txt delete mode 100644 example/gui/material/material_motion_spec/MaterialMotionSpecMainWindow.cpp delete mode 100644 example/gui/material/material_motion_spec/MaterialMotionSpecMainWindow.h delete mode 100644 example/gui/material/material_motion_spec/main.cpp delete mode 100644 example/gui/material/material_radius_scale/CMakeLists.txt delete mode 100644 example/gui/material/material_radius_scale/MaterialRadiusScaleMainWindow.cpp delete mode 100644 example/gui/material/material_radius_scale/MaterialRadiusScaleMainWindow.h delete mode 100644 example/gui/material/material_radius_scale/main.cpp delete mode 100644 example/gui/material/material_typography/CMakeLists.txt delete mode 100644 example/gui/material/material_typography/MaterialTypographyMainWindow.cpp delete mode 100644 example/gui/material/material_typography/MaterialTypographyMainWindow.h delete mode 100644 example/gui/material/material_typography/main.cpp delete mode 100644 example/gui/theme/CMakeLists.txt delete mode 100644 example/gui/theme/ColorSchemePage.cpp delete mode 100644 example/gui/theme/ColorSchemePage.h delete mode 100644 example/gui/theme/MaterialGalleryMainWindow.cpp delete mode 100644 example/gui/theme/MaterialGalleryMainWindow.h delete mode 100644 example/gui/theme/MotionSpecPage.cpp delete mode 100644 example/gui/theme/MotionSpecPage.h delete mode 100644 example/gui/theme/RadiusScalePage.cpp delete mode 100644 example/gui/theme/RadiusScalePage.h delete mode 100644 example/gui/theme/ThemePageWidget.cpp delete mode 100644 example/gui/theme/ThemePageWidget.h delete mode 100644 example/gui/theme/ThemeSidebar.cpp delete mode 100644 example/gui/theme/ThemeSidebar.h delete mode 100644 example/gui/theme/ToastWidget.cpp delete mode 100644 example/gui/theme/ToastWidget.h delete mode 100644 example/gui/theme/TypographyPage.cpp delete mode 100644 example/gui/theme/TypographyPage.h delete mode 100644 example/gui/theme/main.cpp delete mode 100644 example/ui/CMakeLists.txt delete mode 100644 example/ui/widget/CMakeLists.txt delete mode 100644 example/ui/widget/material_example/CMakeLists.txt delete mode 100644 example/ui/widget/material_example/MaterialGalleryWindow.cpp delete mode 100644 example/ui/widget/material_example/MaterialGalleryWindow.h delete mode 100644 example/ui/widget/material_example/demos/ButtonDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/ButtonDemo.h delete mode 100644 example/ui/widget/material_example/demos/CheckBoxDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/CheckBoxDemo.h delete mode 100644 example/ui/widget/material_example/demos/ComboBoxDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/ComboBoxDemo.h delete mode 100644 example/ui/widget/material_example/demos/DoubleSpinBoxDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/DoubleSpinBoxDemo.h delete mode 100644 example/ui/widget/material_example/demos/GroupBoxDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/GroupBoxDemo.h delete mode 100644 example/ui/widget/material_example/demos/LabelDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/LabelDemo.h delete mode 100644 example/ui/widget/material_example/demos/ListViewDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/ListViewDemo.h delete mode 100644 example/ui/widget/material_example/demos/ProgressBarDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/ProgressBarDemo.h delete mode 100644 example/ui/widget/material_example/demos/RadioButtonDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/RadioButtonDemo.h delete mode 100644 example/ui/widget/material_example/demos/ScrollViewDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/ScrollViewDemo.h delete mode 100644 example/ui/widget/material_example/demos/SeparatorDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/SeparatorDemo.h delete mode 100644 example/ui/widget/material_example/demos/SliderDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/SliderDemo.h delete mode 100644 example/ui/widget/material_example/demos/SpinBoxDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/SpinBoxDemo.h delete mode 100644 example/ui/widget/material_example/demos/SwitchDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/SwitchDemo.h delete mode 100644 example/ui/widget/material_example/demos/TabViewDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/TabViewDemo.h delete mode 100644 example/ui/widget/material_example/demos/TableViewDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/TableViewDemo.h delete mode 100644 example/ui/widget/material_example/demos/TextAreaDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/TextAreaDemo.h delete mode 100644 example/ui/widget/material_example/demos/TextFieldDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/TextFieldDemo.h delete mode 100644 example/ui/widget/material_example/demos/TreeViewDemo.cpp delete mode 100644 example/ui/widget/material_example/demos/TreeViewDemo.h delete mode 100644 example/ui/widget/material_example/main.cpp delete mode 100644 test/ui/CMakeLists.txt delete mode 100644 test/ui/base/CMakeLists.txt delete mode 100644 test/ui/base/color_helper_test.cpp delete mode 100644 test/ui/base/color_test.cpp delete mode 100644 test/ui/base/device_pixel_test.cpp delete mode 100644 test/ui/base/easing_test.cpp delete mode 100644 test/ui/base/geometry_helper_test.cpp delete mode 100644 test/ui/base/math_helper_test.cpp delete mode 100644 test/ui/components/CMakeLists.txt delete mode 100644 test/ui/components/elevation_controller_test.cpp delete mode 100644 test/ui/components/focus_ring_test.cpp delete mode 100644 test/ui/components/painter_layer_test.cpp delete mode 100644 test/ui/components/ripple_helper_test.cpp delete mode 100644 test/ui/components/state_machine_test.cpp delete mode 100644 test/ui/core/CMakeLists.txt delete mode 100644 test/ui/widget/CMakeLists.txt delete mode 100644 test/ui/widget/button_test.cpp delete mode 100644 test/ui/widget/checkbox_test.cpp delete mode 100644 test/ui/widget/combobox_test.cpp delete mode 100644 test/ui/widget/doublespinbox_test.cpp delete mode 100644 test/ui/widget/groupbox_test.cpp delete mode 100644 test/ui/widget/label_test.cpp delete mode 100644 test/ui/widget/listview_test.cpp delete mode 100644 test/ui/widget/progressbar_test.cpp delete mode 100644 test/ui/widget/radiobutton_test.cpp delete mode 100644 test/ui/widget/scrollview_test.cpp delete mode 100644 test/ui/widget/separator_test.cpp delete mode 100644 test/ui/widget/slider_test.cpp delete mode 100644 test/ui/widget/spinbox_test.cpp delete mode 100644 test/ui/widget/switch_test.cpp delete mode 100644 test/ui/widget/tableview_test.cpp delete mode 100644 test/ui/widget/tabview_test.cpp delete mode 100644 test/ui/widget/textarea_test.cpp delete mode 100644 test/ui/widget/textfield_test.cpp delete mode 100644 test/ui/widget/treeview_test.cpp delete mode 100644 test/ui/widget/widget_test_helper.h create mode 160000 third_party/QuarkWidgets delete mode 100644 ui/.gitignore delete mode 100644 ui/CMakeLists.txt delete mode 100644 ui/base/CMakeLists.txt delete mode 100644 ui/base/color.cpp delete mode 100644 ui/base/color.h delete mode 100644 ui/base/color_helper.cpp delete mode 100644 ui/base/color_helper.h delete mode 100644 ui/base/device_pixel.cpp delete mode 100644 ui/base/device_pixel.h delete mode 100644 ui/base/easing.cpp delete mode 100644 ui/base/easing.h delete mode 100644 ui/base/geometry_helper.cpp delete mode 100644 ui/base/geometry_helper.h delete mode 100644 ui/base/math_helper.cpp delete mode 100644 ui/base/math_helper.h delete mode 100644 ui/base/qt_format.h delete mode 100644 ui/components/CMakeLists.txt delete mode 100644 ui/components/animation.cpp delete mode 100644 ui/components/animation.h delete mode 100644 ui/components/animation_factory_manager.cpp delete mode 100644 ui/components/animation_factory_manager.h delete mode 100644 ui/components/animation_group.h delete mode 100644 ui/components/material/CMakeLists.txt delete mode 100644 ui/components/material/cfmaterial_animation_factory.cpp delete mode 100644 ui/components/material/cfmaterial_animation_factory.h delete mode 100644 ui/components/material/cfmaterial_animation_strategy.cpp delete mode 100644 ui/components/material/cfmaterial_animation_strategy.h delete mode 100644 ui/components/material/cfmaterial_fade_animation.cpp delete mode 100644 ui/components/material/cfmaterial_fade_animation.h delete mode 100644 ui/components/material/cfmaterial_property_animation.cpp delete mode 100644 ui/components/material/cfmaterial_property_animation.h delete mode 100644 ui/components/material/cfmaterial_scale_animation.cpp delete mode 100644 ui/components/material/cfmaterial_scale_animation.h delete mode 100644 ui/components/material/cfmaterial_slide_animation.cpp delete mode 100644 ui/components/material/cfmaterial_slide_animation.h delete mode 100644 ui/components/material/token/animation_token_literals.h delete mode 100644 ui/components/material/token/animation_token_mapping.h delete mode 100644 ui/components/spring_animation.h delete mode 100644 ui/components/timing_animation.cpp delete mode 100644 ui/components/timing_animation.h delete mode 100644 ui/core/CMakeLists.txt delete mode 100644 ui/core/color_scheme.h delete mode 100644 ui/core/font_type.h delete mode 100644 ui/core/material/CMakeLists.txt delete mode 100644 ui/core/material/cfmaterial_fonttype.cpp delete mode 100644 ui/core/material/cfmaterial_fonttype.h delete mode 100644 ui/core/material/cfmaterial_motion.cpp delete mode 100644 ui/core/material/cfmaterial_motion.h delete mode 100644 ui/core/material/cfmaterial_radius_scale.cpp delete mode 100644 ui/core/material/cfmaterial_radius_scale.h delete mode 100644 ui/core/material/cfmaterial_scheme.cpp delete mode 100644 ui/core/material/cfmaterial_scheme.h delete mode 100644 ui/core/material/cfmaterial_theme.cpp delete mode 100644 ui/core/material/cfmaterial_theme.h delete mode 100644 ui/core/material/material_factory.cpp delete mode 100644 ui/core/material/material_factory.hpp delete mode 100644 ui/core/material/material_factory_class.h delete mode 100644 ui/core/motion_spec.h delete mode 100644 ui/core/radius_scale.h delete mode 100644 ui/core/theme.h delete mode 100644 ui/core/theme_factory.h delete mode 100644 ui/core/theme_manager.cpp delete mode 100644 ui/core/theme_manager.h delete mode 100644 ui/core/token/material_scheme/cfmaterial_token_literals.h delete mode 100644 ui/core/token/motion/cfmaterial_motion_token_literals.h delete mode 100644 ui/core/token/radius_scale/cfmaterial_radius_scale_literals.h delete mode 100644 ui/core/token/theme_name/material_theme_name.h delete mode 100644 ui/core/token/typography/cfmaterial_typography_token_literals.h delete mode 100644 ui/export.h delete mode 100644 ui/models/CMakeLists.txt delete mode 100644 ui/widget/CMakeLists.txt delete mode 100644 ui/widget/application_support/application.cpp delete mode 100644 ui/widget/application_support/application.h delete mode 100644 ui/widget/material/application/material_application.cpp delete mode 100644 ui/widget/material/application/material_application.h delete mode 100644 ui/widget/material/base/elevation_controller.cpp delete mode 100644 ui/widget/material/base/elevation_controller.h delete mode 100644 ui/widget/material/base/focus_ring.cpp delete mode 100644 ui/widget/material/base/focus_ring.h delete mode 100644 ui/widget/material/base/material_widget_base.cpp delete mode 100644 ui/widget/material/base/material_widget_base.h delete mode 100644 ui/widget/material/base/painter_layer.cpp delete mode 100644 ui/widget/material/base/painter_layer.h delete mode 100644 ui/widget/material/base/ripple_helper.cpp delete mode 100644 ui/widget/material/base/ripple_helper.h delete mode 100644 ui/widget/material/base/state_machine.cpp delete mode 100644 ui/widget/material/base/state_machine.h delete mode 100644 ui/widget/material/widget/button/button.cpp delete mode 100644 ui/widget/material/widget/button/button.h delete mode 100644 ui/widget/material/widget/checkbox/checkbox.cpp delete mode 100644 ui/widget/material/widget/checkbox/checkbox.h delete mode 100644 ui/widget/material/widget/comboBox/combobox.cpp delete mode 100644 ui/widget/material/widget/comboBox/combobox.h delete mode 100644 ui/widget/material/widget/doublespinbox/doublespinbox.cpp delete mode 100644 ui/widget/material/widget/doublespinbox/doublespinbox.h delete mode 100644 ui/widget/material/widget/groupbox/groupbox.cpp delete mode 100644 ui/widget/material/widget/groupbox/groupbox.h delete mode 100644 ui/widget/material/widget/label/label.cpp delete mode 100644 ui/widget/material/widget/label/label.h delete mode 100644 ui/widget/material/widget/listview/listview.cpp delete mode 100644 ui/widget/material/widget/listview/listview.h delete mode 100644 ui/widget/material/widget/progressbar/progressbar.cpp delete mode 100644 ui/widget/material/widget/progressbar/progressbar.h delete mode 100644 ui/widget/material/widget/radiobutton/radiobutton.cpp delete mode 100644 ui/widget/material/widget/radiobutton/radiobutton.h delete mode 100644 ui/widget/material/widget/scrollview/scrollview.cpp delete mode 100644 ui/widget/material/widget/scrollview/scrollview.h delete mode 100644 ui/widget/material/widget/separator/separator.cpp delete mode 100644 ui/widget/material/widget/separator/separator.h delete mode 100644 ui/widget/material/widget/slider/slider.cpp delete mode 100644 ui/widget/material/widget/slider/slider.h delete mode 100644 ui/widget/material/widget/spinbox/spinbox.cpp delete mode 100644 ui/widget/material/widget/spinbox/spinbox.h delete mode 100644 ui/widget/material/widget/switch/switch.cpp delete mode 100644 ui/widget/material/widget/switch/switch.h delete mode 100644 ui/widget/material/widget/tableview/tableview.cpp delete mode 100644 ui/widget/material/widget/tableview/tableview.h delete mode 100644 ui/widget/material/widget/tabview/private/materialtabbar.cpp delete mode 100644 ui/widget/material/widget/tabview/private/materialtabbar.h delete mode 100644 ui/widget/material/widget/tabview/tabview.cpp delete mode 100644 ui/widget/material/widget/tabview/tabview.h delete mode 100644 ui/widget/material/widget/textarea/textarea.cpp delete mode 100644 ui/widget/material/widget/textarea/textarea.h delete mode 100644 ui/widget/material/widget/textfield/textfield.cpp delete mode 100644 ui/widget/material/widget/textfield/textfield.h delete mode 100644 ui/widget/material/widget/treeview/private/treeviewdelegate.cpp delete mode 100644 ui/widget/material/widget/treeview/private/treeviewdelegate.h delete mode 100644 ui/widget/material/widget/treeview/treeview.cpp delete mode 100644 ui/widget/material/widget/treeview/treeview.h diff --git a/.gitmodules b/.gitmodules index 251ea9965..062e21bdc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "third_party/aex"] path = third_party/aex url = https://github.com/Awesome-Embedded-Learning-Studio/aex.git +[submodule "third_party/QuarkWidgets"] + path = third_party/QuarkWidgets + url = https://github.com/Awesome-Embedded-Learning-Studio/QuarkWidgets.git diff --git a/CMakeLists.txt b/CMakeLists.txt index ecab9b9b3..c26ac2f39 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,11 +58,12 @@ add_subdirectory(base) # Log base module end log_module_end("base") -# Log base module start -log_module_start("ui") -add_subdirectory(ui) -# Log base module end -log_module_end("ui") +# NOTE: ui/ was extracted to the QuarkWidgets submodule (Phase 2). It is consumed +# below via setup_third_party() before desktop, which links QuarkWidgets::quarkwidgets. +setup_third_party( + NAME QuarkWidgets + GIT_REPOSITORY https://github.com/Awesome-Embedded-Learning-Studio/QuarkWidgets.git + GIT_TAG v0.1.0) log_module_start("desktop") add_subdirectory(desktop) diff --git a/desktop/CMakeLists.txt b/desktop/CMakeLists.txt index a238d3585..4da6b4764 100644 --- a/desktop/CMakeLists.txt +++ b/desktop/CMakeLists.txt @@ -76,14 +76,14 @@ target_link_libraries(CFDesktop_shared PUBLIC ) # Link shared library dependencies (NOT in whole-archive). -# cfui is PUBLIC so the thin EXE can include the QApplication subclass +# QuarkWidgets::quarkwidgets is PUBLIC so the thin EXE can include the QApplication subclass # (MaterialApplication) it instantiates in main.cpp. target_link_libraries(CFDesktop_shared PRIVATE cfbase cflogger PUBLIC - cfui + QuarkWidgets::quarkwidgets ) # Platform-specific link libraries (from base) diff --git a/desktop/main.cpp b/desktop/main.cpp index a3c2afabf..5c0823682 100644 --- a/desktop/main.cpp +++ b/desktop/main.cpp @@ -4,6 +4,6 @@ int main(int argc, char* argv[]) { // MaterialApplication registers the MD3 themes (light/dark) into ThemeManager // on construction, so panels and widgets resolve real theme tokens at runtime. - cf::ui::widget::material::MaterialApplication cf_desktop_app(argc, argv); + qw::widget::material::MaterialApplication cf_desktop_app(argc, argv); return cf::desktop::run_desktop_session(); } diff --git a/desktop/ui/CMakeLists.txt b/desktop/ui/CMakeLists.txt index 521fba630..e3419575b 100644 --- a/desktop/ui/CMakeLists.txt +++ b/desktop/ui/CMakeLists.txt @@ -35,6 +35,6 @@ PRIVATE cf_desktop_ui_widget # Link widget submodule cf_desktop_ui_platform # Link platform submodule cf_desktop_render # Link render backend abstraction - cf_ui_base # For qt_format.h (std::formatter) + QuarkWidgets::quarkwidgets # For qt_format.h (std::formatter) ) diff --git a/desktop/ui/components/shell_layer_impl/CMakeLists.txt b/desktop/ui/components/shell_layer_impl/CMakeLists.txt index ddd682810..094dd356e 100644 --- a/desktop/ui/components/shell_layer_impl/CMakeLists.txt +++ b/desktop/ui/components/shell_layer_impl/CMakeLists.txt @@ -17,7 +17,7 @@ target_link_libraries( Qt6::Widgets cflogger cfbase - cf_ui_base + QuarkWidgets::quarkwidgets cfdesktop_wallpaper cfpath cfconfig diff --git a/desktop/ui/components/statusbar/CMakeLists.txt b/desktop/ui/components/statusbar/CMakeLists.txt index f7be5a730..349ecadc7 100644 --- a/desktop/ui/components/statusbar/CMakeLists.txt +++ b/desktop/ui/components/statusbar/CMakeLists.txt @@ -13,7 +13,7 @@ target_include_directories(cfdesktop_statusbar PUBLIC target_link_libraries( cfdesktop_statusbar PUBLIC - cfui # ThemeManager, color/typography tokens, ICFTheme + QuarkWidgets::quarkwidgets # ThemeManager, color/typography tokens, ICFTheme PRIVATE Qt6::Widgets cfbase # WeakPtr / WeakPtrFactory diff --git a/desktop/ui/components/statusbar/status_bar.cpp b/desktop/ui/components/statusbar/status_bar.cpp index cc07abbd3..c55d7de60 100644 --- a/desktop/ui/components/statusbar/status_bar.cpp +++ b/desktop/ui/components/statusbar/status_bar.cpp @@ -46,9 +46,9 @@ static void registerStatusbarIconsResource() { namespace cf::desktop::desktop_component { using cf::desktop::PanelPosition; -using cf::ui::base::CFColor; -using cf::ui::base::device::CanvasUnitHelper; -using namespace cf::ui::core::token::literals; +using qw::base::CFColor; +using qw::base::device::CanvasUnitHelper; +using namespace qw::core::token::literals; namespace { // Fallback palette used when no theme is available (mirrors MD3 light). @@ -91,13 +91,13 @@ void StatusBar::setupUi() { timer_->start(1000); // React to theme switches (ThemeManager is the canonical source). - connect(&cf::ui::core::ThemeManager::instance(), &cf::ui::core::ThemeManager::themeChanged, - this, [this](const cf::ui::core::ICFTheme&) { applyTheme(); }); + connect(&qw::core::ThemeManager::instance(), &qw::core::ThemeManager::themeChanged, this, + [this](const qw::core::ICFTheme&) { applyTheme(); }); } void StatusBar::applyTheme() { try { - auto& tm = cf::ui::core::ThemeManager::instance(); + auto& tm = qw::core::ThemeManager::instance(); const auto& theme = tm.theme(tm.currentThemeName()); auto& cs = theme.color_scheme(); background_color_ = cs.queryColor(SURFACE); diff --git a/desktop/ui/components/taskbar/CMakeLists.txt b/desktop/ui/components/taskbar/CMakeLists.txt index 4bd46f0fb..6447189be 100644 --- a/desktop/ui/components/taskbar/CMakeLists.txt +++ b/desktop/ui/components/taskbar/CMakeLists.txt @@ -13,7 +13,7 @@ target_include_directories(cfdesktop_taskbar PUBLIC target_link_libraries( cfdesktop_taskbar PUBLIC - cfui # ThemeManager, color/typography tokens, ICFTheme + QuarkWidgets::quarkwidgets # ThemeManager, color/typography tokens, ICFTheme PRIVATE Qt6::Widgets cfbase # WeakPtr / WeakPtrFactory diff --git a/desktop/ui/components/taskbar/centered_taskbar.cpp b/desktop/ui/components/taskbar/centered_taskbar.cpp index 28e0059ab..f2a59be05 100644 --- a/desktop/ui/components/taskbar/centered_taskbar.cpp +++ b/desktop/ui/components/taskbar/centered_taskbar.cpp @@ -30,7 +30,7 @@ namespace cf::desktop::desktop_component { using cf::desktop::PanelPosition; -using namespace cf::ui::core::token::literals; +using namespace qw::core::token::literals; namespace { constexpr int kTaskbarHeight = 64; ///< Bar thickness (px). @@ -56,13 +56,13 @@ void CenteredTaskbar::setupUi() { layout_->setSpacing(kIconSpacing); // React to theme switches (ThemeManager is the canonical source). - connect(&cf::ui::core::ThemeManager::instance(), &cf::ui::core::ThemeManager::themeChanged, - this, [this](const cf::ui::core::ICFTheme&) { applyTheme(); }); + connect(&qw::core::ThemeManager::instance(), &qw::core::ThemeManager::themeChanged, this, + [this](const qw::core::ICFTheme&) { applyTheme(); }); } void CenteredTaskbar::applyTheme() { try { - auto& tm = cf::ui::core::ThemeManager::instance(); + auto& tm = qw::core::ThemeManager::instance(); const auto& theme = tm.theme(tm.currentThemeName()); auto& cs = theme.color_scheme(); background_color_ = cs.queryColor(SURFACE); diff --git a/desktop/ui/components/taskbar/taskbar_icon.cpp b/desktop/ui/components/taskbar/taskbar_icon.cpp index 759bde570..6de893509 100644 --- a/desktop/ui/components/taskbar/taskbar_icon.cpp +++ b/desktop/ui/components/taskbar/taskbar_icon.cpp @@ -31,7 +31,7 @@ namespace cf::desktop::desktop_component { -using namespace cf::ui::core::token::literals; +using namespace qw::core::token::literals; namespace { constexpr int kCellSize = 56; ///< Tile widget edge length (px). @@ -155,7 +155,7 @@ void TaskbarIcon::mouseReleaseEvent(QMouseEvent* event) { // -- Internal -------------------------------------------------------------- void TaskbarIcon::applyTheme() { try { - auto& tm = cf::ui::core::ThemeManager::instance(); + auto& tm = qw::core::ThemeManager::instance(); const auto& theme = tm.theme(tm.currentThemeName()); auto& cs = theme.color_scheme(); tile_color_ = cs.queryColor(SURFACE_VARIANT); @@ -213,8 +213,8 @@ void TaskbarIcon::setupAnimations() { }); // Follow live theme switches (ThemeManager is the canonical source). - connect(&cf::ui::core::ThemeManager::instance(), &cf::ui::core::ThemeManager::themeChanged, - this, [this](const cf::ui::core::ICFTheme&) { applyTheme(); }); + connect(&qw::core::ThemeManager::instance(), &qw::core::ThemeManager::themeChanged, this, + [this](const qw::core::ICFTheme&) { applyTheme(); }); } } // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/wallpaper/CMakeLists.txt b/desktop/ui/components/wallpaper/CMakeLists.txt index c5dc9a667..28a6eb6d5 100644 --- a/desktop/ui/components/wallpaper/CMakeLists.txt +++ b/desktop/ui/components/wallpaper/CMakeLists.txt @@ -14,5 +14,5 @@ target_link_libraries(cfdesktop_wallpaper PRIVATE Qt6::Gui cflogger cfbase - cf_ui_base + QuarkWidgets::quarkwidgets ) \ No newline at end of file diff --git a/desktop/ui/platform/CMakeLists.txt b/desktop/ui/platform/CMakeLists.txt index 568a50c60..f3cf0302c 100644 --- a/desktop/ui/platform/CMakeLists.txt +++ b/desktop/ui/platform/CMakeLists.txt @@ -78,7 +78,7 @@ PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets - cf_ui_base # For qt_format.h (std::formatter) + QuarkWidgets::quarkwidgets # For qt_format.h (std::formatter) ) if(UNIX AND NOT APPLE) diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 0677f0ef3..177dd4bfa 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -1,7 +1,6 @@ log_info("Example" "Managing Example Components") add_subdirectory(base) -add_subdirectory(ui) -add_subdirectory(gui) +# NOTE: ui + gui examples now live in the QuarkWidgets submodule. add_subdirectory(desktop) diff --git a/example/gui/CMakeLists.txt b/example/gui/CMakeLists.txt deleted file mode 100644 index 7e21363a8..000000000 --- a/example/gui/CMakeLists.txt +++ /dev/null @@ -1,4 +0,0 @@ -log_info("Example GUI" "Managing GUI Examples") - -add_subdirectory(material) -add_subdirectory(theme) diff --git a/example/gui/material/CMakeLists.txt b/example/gui/material/CMakeLists.txt deleted file mode 100644 index 4d0edec67..000000000 --- a/example/gui/material/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -log_info("Example GUI Material" "Managing Material Examples") - -add_subdirectory(material_color_scheme) -add_subdirectory(material_typography) -add_subdirectory(material_radius_scale) -add_subdirectory(material_motion_spec) diff --git a/example/gui/material/material_color_scheme/CMakeLists.txt b/example/gui/material/material_color_scheme/CMakeLists.txt deleted file mode 100644 index b391c8712..000000000 --- a/example/gui/material/material_color_scheme/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -# Material Color Scheme Gallery Example -log_info("Material Color Scheme" "Building Color Gallery Example") - -add_executable(material_color_scheme_gallery - main.cpp - MaterialColorSchemeMainWindow.h - MaterialColorSchemeMainWindow.cpp -) - -target_link_libraries(material_color_scheme_gallery PRIVATE - Qt6::Widgets - Qt6::Core - Qt6::Gui - cfui -) - -# Set output directory to examples/gui/ -cf_set_example_output_dir(material_color_scheme_gallery "gui") -cf_register_example_launcher(material_color_scheme_gallery "gui") - -# Set as GUI application on Windows (hide console window) -if(WIN32) - set_target_properties(material_color_scheme_gallery PROPERTIES - WIN32_EXECUTABLE TRUE - ) -endif() diff --git a/example/gui/material/material_color_scheme/MaterialColorSchemeMainWindow.cpp b/example/gui/material/material_color_scheme/MaterialColorSchemeMainWindow.cpp deleted file mode 100644 index 443d6dbae..000000000 --- a/example/gui/material/material_color_scheme/MaterialColorSchemeMainWindow.cpp +++ /dev/null @@ -1,572 +0,0 @@ -/** - * @file MaterialColorSchemeMainWindow.cpp - * @brief Material Design 3 Color Scheme Gallery - Implementation - */ - -#include "MaterialColorSchemeMainWindow.h" -#include "material/material_factory.hpp" -#include "ui/core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -// Global namespace alias for token literals -namespace token_literals = ::cf::ui::core::token::literals; - -namespace cf::ui::gallery { - -// ============================================================================= -// Static Token Lists -// ============================================================================= - -const QStringList MaterialColorSchemeMainWindow::PRIMARY_TOKENS = { - token_literals::PRIMARY, token_literals::ON_PRIMARY, token_literals::PRIMARY_CONTAINER, - token_literals::ON_PRIMARY_CONTAINER}; - -const QStringList MaterialColorSchemeMainWindow::SECONDARY_TOKENS = { - token_literals::SECONDARY, token_literals::ON_SECONDARY, token_literals::SECONDARY_CONTAINER, - token_literals::ON_SECONDARY_CONTAINER}; - -const QStringList MaterialColorSchemeMainWindow::TERTIARY_TOKENS = { - token_literals::TERTIARY, token_literals::ON_TERTIARY, token_literals::TERTIARY_CONTAINER, - token_literals::ON_TERTIARY_CONTAINER}; - -const QStringList MaterialColorSchemeMainWindow::ERROR_TOKENS = { - token_literals::ERROR, token_literals::ON_ERROR, token_literals::ERROR_CONTAINER, - token_literals::ON_ERROR_CONTAINER}; - -const QStringList MaterialColorSchemeMainWindow::SURFACE_TOKENS = { - token_literals::BACKGROUND, token_literals::ON_BACKGROUND, token_literals::SURFACE, - token_literals::ON_SURFACE, token_literals::SURFACE_VARIANT, token_literals::ON_SURFACE_VARIANT, - token_literals::OUTLINE, token_literals::OUTLINE_VARIANT}; - -const QStringList MaterialColorSchemeMainWindow::UTILITY_TOKENS = { - token_literals::SHADOW, token_literals::SCRIM, token_literals::INVERSE_SURFACE, - token_literals::INVERSE_ON_SURFACE, token_literals::INVERSE_PRIMARY}; - -// ============================================================================= -// Contrast Pair Mapping -// ============================================================================= - -QString MaterialColorSchemeMainWindow::getContrastPair(const QString& token) const { - static const QMap pairs = { - {"md.primary", "md.onPrimary"}, - {"md.onPrimary", "md.primary"}, - {"md.primaryContainer", "md.onPrimaryContainer"}, - {"md.onPrimaryContainer", "md.primaryContainer"}, - {"md.secondary", "md.onSecondary"}, - {"md.onSecondary", "md.secondary"}, - {"md.secondaryContainer", "md.onSecondaryContainer"}, - {"md.onSecondaryContainer", "md.secondaryContainer"}, - {"md.tertiary", "md.onTertiary"}, - {"md.onTertiary", "md.tertiary"}, - {"md.tertiaryContainer", "md.onTertiaryContainer"}, - {"md.onTertiaryContainer", "md.tertiaryContainer"}, - {"md.error", "md.onError"}, - {"md.onError", "md.error"}, - {"md.errorContainer", "md.onErrorContainer"}, - {"md.onErrorContainer", "md.errorContainer"}, - {"md.background", "md.onBackground"}, - {"md.onBackground", "md.background"}, - {"md.surface", "md.onSurface"}, - {"md.onSurface", "md.surface"}, - {"md.surfaceVariant", "md.onSurfaceVariant"}, - {"md.onSurfaceVariant", "md.surfaceVariant"}, - {"md.outline", "md.surface"}, - {"md.outlineVariant", "md.surface"}, - {"md.shadow", "md.surface"}, - {"md.scrim", "md.surface"}, - {"md.inverseSurface", "md.inverseOnSurface"}, - {"md.inverseOnSurface", "md.inverseSurface"}, - {"md.inversePrimary", "md.surface"}}; - return pairs.value(token, "md.surface"); -} - -float MaterialColorSchemeMainWindow::calculateContrastRatio(const QColor& fg, - const QColor& bg) const { - // WCAG 2.1 relative luminance calculation - auto luminance = [](const QColor& c) -> float { - auto toLinear = [](float v) -> float { - v /= 255.0f; - return v <= 0.03928f ? v / 12.92f : std::pow((v + 0.055f) / 1.055f, 2.4f); - }; - float r = toLinear(c.red()); - float g = toLinear(c.green()); - float b = toLinear(c.blue()); - return 0.2126f * r + 0.7152f * g + 0.0722f * b; - }; - - float l1 = luminance(fg); - float l2 = luminance(bg); - float lighter = std::max(l1, l2); - float darker = std::min(l1, l2); - return (lighter + 0.05f) / (darker + 0.05f); -} - -// ============================================================================= -// ToastWidget Implementation -// ============================================================================= - -ToastWidget::ToastWidget(const QString& message, QWidget* parent) - : QWidget(parent), message_(message) { - setAttribute(Qt::WA_TransparentForMouseEvents, false); - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); - setAttribute(Qt::WA_TranslucentBackground); -} - -void ToastWidget::show(int durationMs) { - // Calculate size and position - QFont font("Segoe UI", 10); - QFontMetrics fm(font); - int padding = 20; - int textWidth = fm.horizontalAdvance(message_); - int width = textWidth + padding * 2; - int height = fm.height() + padding * 2; - - setFixedSize(width, height); - - // Center at bottom of parent - if (parentWidget()) { - QPoint pos = parentWidget()->mapToGlobal(parentWidget()->rect().bottomLeft()); - int x = pos.x() + (parentWidget()->width() - width) / 2; - int y = pos.y() - height - 20; - move(x, y); - } - - QWidget::show(); - - // Auto hide after duration - QTimer::singleShot(durationMs, this, [this]() { - // Fade out animation - QPropertyAnimation* anim = new QPropertyAnimation(this, "windowOpacity"); - anim->setDuration(300); - anim->setStartValue(1.0); - anim->setEndValue(0.0); - connect(anim, &QPropertyAnimation::finished, this, &QWidget::hide); - connect(anim, &QPropertyAnimation::finished, anim, &QObject::deleteLater); - anim->start(QAbstractAnimation::DeleteWhenStopped); - }); -} - -void ToastWidget::paintEvent(QPaintEvent*) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - // Semi-transparent black background - QPainterPath path; - path.addRoundedRect(rect(), 12, 12); - painter.fillPath(path, QColor(0, 0, 0, 200)); - - // White text - painter.setPen(Qt::white); - QFont font("Segoe UI", 10); - painter.setFont(font); - painter.drawText(rect(), Qt::AlignCenter, message_); -} - -// ============================================================================= -// ColorCardWidget Implementation -// ============================================================================= - -ColorCardWidget::ColorCardWidget(const QString& tokenName, const QColor& color, - const QString& contrastInfo, QWidget* parent) - : QWidget(parent), tokenName_(tokenName), color_(color), contrastInfo_(contrastInfo) { - hexValue_ = color.name().toUpper(); - setMinimumSize(160, 180); - setMaximumSize(200, 220); - setCursor(Qt::PointingHandCursor); -} - -void ColorCardWidget::updateColor(const QColor& color, const QString& contrastInfo) { - color_ = color; - hexValue_ = color.name().toUpper(); - contrastInfo_ = contrastInfo; - update(); -} - -void ColorCardWidget::paintEvent(QPaintEvent*) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QRectF r = rect().adjusted(2, 2, -2, -2); - qreal radius = 12; - - // Get current theme colors for card background - cf::ui::core::MaterialColorScheme* scheme = nullptr; - // We'll get this from parent during update - QColor bgColor = isHovered_ ? QColor(250, 250, 250) : QColor(245, 245, 245); - QColor borderColor = QColor(220, 220, 220); - - // Card background with elevation - QPainterPath path; - path.addRoundedRect(r, radius, radius); - - // Shadow for elevation effect - if (isHovered_) { - // Higher elevation when hovered - painter.setPen(Qt::NoPen); - painter.setBrush(QColor(0, 0, 0, 20)); - painter.drawPath(path.translated(0, 2)); - } - - painter.setPen(QPen(borderColor, 1)); - painter.setBrush(bgColor); - painter.drawPath(path); - - // Color swatch - qreal swatchSize = std::min(r.width(), r.height() * 0.35); - QRectF swatchRect(r.center().x() - swatchSize / 2, r.top() + 15, swatchSize, swatchSize); - - QPainterPath swatchPath; - swatchPath.addRoundedRect(swatchRect, 8, 8); - painter.setPen(Qt::NoPen); - painter.setBrush(color_); - painter.drawPath(swatchPath); - - // Token name - QFont nameFont("Segoe UI", 9, QFont::Medium); - painter.setFont(nameFont); - painter.setPen(QColor(60, 60, 60)); - QRectF nameRect = r.adjusted(10, swatchRect.bottom() + 10, -10, 0); - painter.drawText(nameRect, Qt::AlignTop | Qt::AlignHCenter, tokenName_); - - // Separator line - qreal lineY = nameRect.top() + QFontMetrics(nameFont).height() + 8; - QPen linePen(QColor(200, 200, 200), 1); - painter.setPen(linePen); - painter.drawLine(QPointF(r.left() + 20, lineY), QPointF(r.right() - 20, lineY)); - - // HEX value - QFont hexFont("Consolas", 10); - painter.setFont(hexFont); - painter.setPen(QColor(40, 40, 40)); - QRectF hexRect = r.adjusted(10, lineY + 5, -10, 0); - painter.drawText(hexRect, Qt::AlignTop | Qt::AlignHCenter, hexValue_); - - // Contrast info with color coding - QFont contrastFont("Segoe UI", 8); - painter.setFont(contrastFont); - - // Parse contrast value - float contrast = contrastInfo_.split(": ").last().toFloat(); - QColor contrastColor; - if (contrast < 3.0f) { - contrastColor = QColor(220, 50, 50); // Red - fail - } else if (contrast < 4.5f) { - contrastColor = QColor(220, 150, 50); // Yellow - large text only - } else { - contrastColor = QColor(50, 180, 80); // Green - pass - } - - painter.setPen(contrastColor); - QRectF contrastRect = - r.adjusted(10, hexRect.top() + QFontMetrics(hexFont).height() + 3, -10, -10); - painter.drawText(contrastRect, Qt::AlignTop | Qt::AlignHCenter, contrastInfo_); - - // Click hint on hover - if (isHovered_) { - QFont hintFont("Segoe UI", 8); - painter.setFont(hintFont); - painter.setPen(QColor(100, 100, 100)); - painter.drawText(r.adjusted(10, 0, -10, -5), Qt::AlignBottom | Qt::AlignHCenter, - "Click to copy"); - } -} - -void ColorCardWidget::enterEvent(QEnterEvent*) { - isHovered_ = true; - update(); -} - -void ColorCardWidget::leaveEvent(QEvent*) { - isHovered_ = false; - update(); -} - -void ColorCardWidget::mousePressEvent(QMouseEvent*) { - emit clicked(hexValue_); -} - -// ============================================================================= -// ThemeSwitch Implementation -// ============================================================================= - -ThemeSwitch::ThemeSwitch(QWidget* parent) : QWidget(parent) { - setFixedSize(60, 32); - - animation_ = new QPropertyAnimation(this, "knobPosition", this); - animation_->setDuration(200); - animation_->setEasingCurve(QEasingCurve::OutCubic); -} - -void ThemeSwitch::setDark(bool dark) { - if (isDark_ != dark) { - isDark_ = dark; - float targetPos = dark ? 1.0f : 0.0f; - animation_->stop(); - animation_->setStartValue(knobPosition_); - animation_->setEndValue(targetPos); - animation_->start(); - emit themeChanged(isDark_); - } -} - -void ThemeSwitch::setKnobPosition(float pos) { - knobPosition_ = qBound(0.0f, pos, 1.0f); - update(); - emit knobPositionChanged(); -} - -void ThemeSwitch::paintEvent(QPaintEvent*) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QRectF r = rect(); - qreal radius = r.height() / 2; - - // Track background - QColor trackColor = isDark_ ? QColor(103, 80, 164) : QColor(180, 180, 180); - QPainterPath trackPath; - trackPath.addRoundedRect(r, radius, radius); - painter.setPen(Qt::NoPen); - painter.setBrush(trackColor); - painter.drawPath(trackPath); - - // Knob - qreal knobDiameter = r.height() - 6; - qreal knobX = 3 + knobPosition_ * (r.width() - knobDiameter - 6); - QRectF knobRect(knobX, 3, knobDiameter, knobDiameter); - - QPainterPath knobPath; - knobPath.addEllipse(knobRect); - - // Knob shadow - painter.setPen(Qt::NoPen); - painter.setBrush(QColor(0, 0, 0, 30)); - painter.drawEllipse(knobRect.translated(0, 1)); - - // Knob - painter.setBrush(Qt::white); - painter.drawPath(knobPath); - - // Icon on knob (sun or moon) - painter.setPen(isDark_ ? QColor(60, 60, 60) : QColor(255, 200, 50)); - QFont iconFont("Segoe UI Emoji", 12); - painter.setFont(iconFont); - QString icon = isDark_ ? "🌙" : "☀️"; - painter.drawText(knobRect, Qt::AlignCenter, icon); -} - -void ThemeSwitch::mousePressEvent(QMouseEvent*) { - setDark(!isDark_); -} - -// ============================================================================= -// MaterialColorSchemeMainWindow Implementation -// ============================================================================= - -MaterialColorSchemeMainWindow::MaterialColorSchemeMainWindow(QWidget* parent) - : QMainWindow(parent) { - - // Create color schemes - lightScheme_ = cf::ui::core::material::light(); - darkScheme_ = cf::ui::core::material::dark(); - - setupUI(); - createHeader(); - createColorGroups(); - updateWindowTheme(); -} - -MaterialColorSchemeMainWindow::~MaterialColorSchemeMainWindow() = default; - -void MaterialColorSchemeMainWindow::setupUI() { - // Window setup - setWindowTitle("Material Color Scheme Gallery"); - resize(1200, 800); - setMinimumSize(800, 600); - - // Central widget - centralWidget_ = new QWidget(this); - setCentralWidget(centralWidget_); - - mainLayout_ = new QVBoxLayout(centralWidget_); - mainLayout_->setContentsMargins(20, 20, 20, 20); - mainLayout_->setSpacing(20); - - // Header - headerLayout_ = new QHBoxLayout(); - headerLayout_->setSpacing(16); - mainLayout_->addLayout(headerLayout_); - - // Scroll area for color cards - scrollArea_ = new QScrollArea(this); - scrollArea_->setWidgetResizable(true); - scrollArea_->setFrameShape(QFrame::NoFrame); - scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scrollContent_ = new QWidget(); - scrollLayout_ = new QVBoxLayout(scrollContent_); - scrollLayout_->setSpacing(30); - - colorGridLayout_ = new QGridLayout(); - colorGridLayout_->setSpacing(16); - colorGridLayout_->setContentsMargins(0, 0, 0, 0); - scrollLayout_->addLayout(colorGridLayout_); - scrollLayout_->addStretch(); - - scrollArea_->setWidget(scrollContent_); - mainLayout_->addWidget(scrollArea_); - - // Toast - toast_ = new ToastWidget("", this); - toast_->hide(); -} - -void MaterialColorSchemeMainWindow::createHeader() { - // Title - titleLabel_ = new QLabel("Material Color Scheme Gallery", this); - QFont titleFont("Segoe UI", 18, QFont::Bold); - titleLabel_->setFont(titleFont); - headerLayout_->addWidget(titleLabel_); - headerLayout_->addStretch(); - - // Theme label - themeLabel_ = new QLabel("☀️ Light", this); - QFont themeFont("Segoe UI", 11); - themeLabel_->setFont(themeFont); - headerLayout_->addWidget(themeLabel_); - - // Theme switch - themeSwitch_ = new ThemeSwitch(this); - headerLayout_->addWidget(themeSwitch_); - - connect(themeSwitch_, &ThemeSwitch::themeChanged, this, - &MaterialColorSchemeMainWindow::onThemeChanged); -} - -void MaterialColorSchemeMainWindow::createColorGroups() { - int row = 0; - - createColorGroup("Primary Colors", colorGridLayout_, row, PRIMARY_TOKENS); - createColorGroup("Secondary Colors", colorGridLayout_, row, SECONDARY_TOKENS); - createColorGroup("Tertiary Colors", colorGridLayout_, row, TERTIARY_TOKENS); - createColorGroup("Error Colors", colorGridLayout_, row, ERROR_TOKENS); - createColorGroup("Surface Colors", colorGridLayout_, row, SURFACE_TOKENS); - createColorGroup("Utility Colors", colorGridLayout_, row, UTILITY_TOKENS); -} - -void MaterialColorSchemeMainWindow::createColorGroup(const QString& title, QGridLayout* layout, - int& row, const QStringList& tokens) { - // Group title - QLabel* titleLabel = new QLabel(title, scrollContent_); - QFont titleFont("Segoe UI", 14, QFont::Bold); - titleLabel->setFont(titleFont); - layout->addWidget(titleLabel, row++, 0, 1, -1, Qt::AlignLeft); - - // Calculate column count and start new row for cards - int col = 0; - layout->setRowStretch(row, 0); - - for (const QString& token : tokens) { - QColor color = lightScheme_.queryColor(token.toStdString().c_str()); - - // Calculate contrast - QString pairToken = getContrastPair(token); - QColor pairColor = lightScheme_.queryColor(pairToken.toStdString().c_str()); - float contrast = calculateContrastRatio(color, pairColor); - QString contrastInfo = QString("⚡ 对比度: %1").arg(contrast, 0, 'f', 2); - - auto* card = new ColorCardWidget(token, color, contrastInfo, scrollContent_); - layout->addWidget(card, row, col++); - - ColorCardInfo info{card, token}; - colorCards_.append(info); - - connect(card, &ColorCardWidget::clicked, this, - &MaterialColorSchemeMainWindow::onColorCardClicked); - } - - row++; -} - -void MaterialColorSchemeMainWindow::updateAllColors() { - auto& scheme = isDarkTheme_ ? darkScheme_ : lightScheme_; - - for (auto& info : colorCards_) { - QColor color = scheme.queryColor(info.token.toStdString().c_str()); - QString pairToken = getContrastPair(info.token); - QColor pairColor = scheme.queryColor(pairToken.toStdString().c_str()); - float contrast = calculateContrastRatio(color, pairColor); - QString contrastInfo = QString("⚡ 对比度: %1").arg(contrast, 0, 'f', 2); - info.widget->updateColor(color, contrastInfo); - } -} - -void MaterialColorSchemeMainWindow::updateWindowTheme() { - auto& scheme = isDarkTheme_ ? darkScheme_ : lightScheme_; - - // Update background colors - QColor bg = scheme.queryColor("md.background"); - QColor surface = scheme.queryColor("md.surface"); - QColor onSurface = scheme.queryColor("md.onSurface"); - - centralWidget_->setAutoFillBackground(true); - QPalette pal = centralWidget_->palette(); - pal.setColor(QPalette::Window, bg); - centralWidget_->setPalette(pal); - - scrollContent_->setAutoFillBackground(true); - pal = scrollContent_->palette(); - pal.setColor(QPalette::Window, bg); - scrollContent_->setPalette(pal); - - // Update text colors - titleLabel_->setStyleSheet(QString("color: %1").arg(onSurface.name())); - themeLabel_->setStyleSheet(QString("color: %1").arg(onSurface.name())); - - // Update theme label text - themeLabel_->setText(isDarkTheme_ ? "🌙 Dark" : "☀️ Light"); - - // Update color cards - updateAllColors(); -} - -int MaterialColorSchemeMainWindow::calculateColumnCount() const { - int width = scrollArea_->width(); - if (width < 800) - return 2; - if (width < 1200) - return 3; - return 4; -} - -void MaterialColorSchemeMainWindow::resizeEvent(QResizeEvent* event) { - QMainWindow::resizeEvent(event); - // Could trigger column count recalculation here -} - -void MaterialColorSchemeMainWindow::onThemeChanged(bool isDark) { - isDarkTheme_ = isDark; - updateWindowTheme(); -} - -void MaterialColorSchemeMainWindow::onColorCardClicked(const QString& hexValue) { - QClipboard* clipboard = QGuiApplication::clipboard(); - clipboard->setText(hexValue); - showToast(QString("已复制: %1").arg(hexValue)); -} - -void MaterialColorSchemeMainWindow::showToast(const QString& message) { - delete toast_; - toast_ = new ToastWidget(message, this); - toast_->show(); -} - -} // namespace cf::ui::gallery diff --git a/example/gui/material/material_color_scheme/MaterialColorSchemeMainWindow.h b/example/gui/material/material_color_scheme/MaterialColorSchemeMainWindow.h deleted file mode 100644 index 6063182db..000000000 --- a/example/gui/material/material_color_scheme/MaterialColorSchemeMainWindow.h +++ /dev/null @@ -1,204 +0,0 @@ -/** - * @file MaterialColorSchemeMainWindow.h - * @brief Material Design 3 Color Scheme Gallery - Main Window - * - * A visual gallery to display all Material Design 3 color tokens - * with light/dark theme switching support. - * - * @author CFDesktop Team - * @date 2026-02-25 - * @version 0.1 - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ui/core/material/cfmaterial_scheme.h" - -namespace cf::ui::gallery { - -/** - * @brief Toast notification widget for displaying temporary messages. - * - * Shows a semi-transparent notification at the bottom of the window - * that automatically fades out after 2 seconds. - */ -class ToastWidget : public QWidget { - Q_OBJECT - public: - explicit ToastWidget(const QString& message, QWidget* parent = nullptr); - - void show(int durationMs = 2000); - - protected: - void paintEvent(QPaintEvent* event) override; - - private: - QString message_; -}; - -/** - * @brief Color card widget displaying a single color token. - * - * Shows: - * - Color preview swatch - * - Token name (e.g., "md.primary") - * - HEX color value - * - Contrast ratio with paired color - * - * Click to copy HEX value to clipboard. - */ -class ColorCardWidget : public QWidget { - Q_OBJECT - public: - explicit ColorCardWidget(const QString& tokenName, const QColor& color, - const QString& contrastInfo, QWidget* parent = nullptr); - - void updateColor(const QColor& color, const QString& contrastInfo); - - signals: - void clicked(const QString& hexValue); - - protected: - void paintEvent(QPaintEvent* event) override; - void enterEvent(QEnterEvent* event) override; - void leaveEvent(QEvent* event) override; - void mousePressEvent(QMouseEvent* event) override; - - private: - QString tokenName_; - QColor color_; - QString hexValue_; - QString contrastInfo_; - bool isHovered_ = false; -}; - -/** - * @brief Theme toggle switch widget. - * - * A mobile-style toggle switch for switching between light and dark themes. - */ -class ThemeSwitch : public QWidget { - Q_OBJECT - Q_PROPERTY( - float knobPosition READ knobPosition WRITE setKnobPosition NOTIFY knobPositionChanged) - - public: - explicit ThemeSwitch(QWidget* parent = nullptr); - - bool isDark() const { return isDark_; } - void setDark(bool dark); - - float knobPosition() const { return knobPosition_; } - void setKnobPosition(float pos); - - signals: - void themeChanged(bool isDark); - void knobPositionChanged(); - - protected: - void paintEvent(QPaintEvent* event) override; - void mousePressEvent(QMouseEvent* event) override; - - private: - bool isDark_ = false; - float knobPosition_ = 0.0f; - QPropertyAnimation* animation_; -}; - -/** - * @brief Main window for the Material Color Scheme Gallery. - * - * Displays all 26 Material Design 3 color tokens organized in groups: - * - Primary Colors (4) - * - Secondary Colors (4) - * - Tertiary Colors (4) - * - Error Colors (4) - * - Surface Colors (8) - * - Utility Colors (5) - */ -class MaterialColorSchemeMainWindow : public QMainWindow { - Q_OBJECT - - public: - explicit MaterialColorSchemeMainWindow(QWidget* parent = nullptr); - ~MaterialColorSchemeMainWindow() override; - - protected: - void resizeEvent(QResizeEvent* event) override; - - private: - void setupUI(); - void createHeader(); - void createColorGroups(); - void updateAllColors(); - void updateWindowTheme(); - int calculateColumnCount() const; - - // Color group creation helpers - void createColorGroup(const QString& title, QGridLayout* layout, int& row, - const QStringList& tokens); - - // Calculate WCAG contrast ratio - float calculateContrastRatio(const QColor& fg, const QColor& bg) const; - - // Get contrast pair for a token - QString getContrastPair(const QString& token) const; - - // Show toast notification - void showToast(const QString& message); - - private slots: - void onThemeChanged(bool isDark); - void onColorCardClicked(const QString& hexValue); - - private: - // Color schemes - cf::ui::core::MaterialColorScheme lightScheme_; - cf::ui::core::MaterialColorScheme darkScheme_; - bool isDarkTheme_ = false; - - // UI components - QWidget* centralWidget_; - QVBoxLayout* mainLayout_; - QHBoxLayout* headerLayout_; - QScrollArea* scrollArea_; - QWidget* scrollContent_; - QVBoxLayout* scrollLayout_; - QGridLayout* colorGridLayout_; - - // Header components - QLabel* titleLabel_; - QLabel* themeLabel_; - ThemeSwitch* themeSwitch_; - - // Toast - ToastWidget* toast_; - - // Color cards storage for theme updates - struct ColorCardInfo { - ColorCardWidget* widget; - QString token; - }; - QList colorCards_; - - // All 26 color tokens - static const QStringList PRIMARY_TOKENS; - static const QStringList SECONDARY_TOKENS; - static const QStringList TERTIARY_TOKENS; - static const QStringList ERROR_TOKENS; - static const QStringList SURFACE_TOKENS; - static const QStringList UTILITY_TOKENS; -}; - -} // namespace cf::ui::gallery diff --git a/example/gui/material/material_color_scheme/main.cpp b/example/gui/material/material_color_scheme/main.cpp deleted file mode 100644 index 3dd2369e0..000000000 --- a/example/gui/material/material_color_scheme/main.cpp +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @file main.cpp - * @brief Material Color Scheme Gallery - Main Entry Point - * - * Entry point for the Material Design 3 Color Scheme Gallery application. - * Displays all 26 color tokens with light/dark theme switching support. - * - * @author CFDesktop Team - * @date 2026-02-25 - * @version 0.1 - */ - -#include "MaterialColorSchemeMainWindow.h" - -#include - -int main(int argc, char* argv[]) { - QApplication app(argc, argv); - - // Set application metadata - app.setApplicationName("Material Color Scheme Gallery"); - app.setApplicationVersion("0.1"); - app.setOrganizationName("CFDesktop"); - - // Create and show main window - cf::ui::gallery::MaterialColorSchemeMainWindow window; - window.show(); - - return app.exec(); -} diff --git a/example/gui/material/material_motion_spec/CMakeLists.txt b/example/gui/material/material_motion_spec/CMakeLists.txt deleted file mode 100644 index 504cd6663..000000000 --- a/example/gui/material/material_motion_spec/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -# Material Motion Spec Gallery Example -log_info("Material Motion Spec" "Building Motion Spec Gallery Example") - -add_executable(material_motion_spec_gallery - main.cpp - MaterialMotionSpecMainWindow.h - MaterialMotionSpecMainWindow.cpp -) - -target_link_libraries(material_motion_spec_gallery PRIVATE - Qt6::Widgets - Qt6::Core - Qt6::Gui - cfui -) - -# Set output directory to examples/gui/ -cf_set_example_output_dir(material_motion_spec_gallery "gui") -cf_register_example_launcher(material_motion_spec_gallery "gui") - -# Set as GUI application on Windows (hide console window) -if(WIN32) - set_target_properties(material_motion_spec_gallery PROPERTIES - WIN32_EXECUTABLE TRUE - ) -endif() diff --git a/example/gui/material/material_motion_spec/MaterialMotionSpecMainWindow.cpp b/example/gui/material/material_motion_spec/MaterialMotionSpecMainWindow.cpp deleted file mode 100644 index 84356a411..000000000 --- a/example/gui/material/material_motion_spec/MaterialMotionSpecMainWindow.cpp +++ /dev/null @@ -1,674 +0,0 @@ -/** - * @file MaterialMotionSpecMainWindow.cpp - * @brief Material Design 3 Motion Spec Gallery - Implementation - */ - -#include "MaterialMotionSpecMainWindow.h" -#include "material/material_factory.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace { -using namespace cf::ui; -QString easingTypeToString(base::Easing::Type type) { - switch (type) { - case base::Easing::Type::Emphasized: - return "Emphasized"; - case base::Easing::Type::EmphasizedDecelerate: - return "EmphasizedDecelerate"; - case base::Easing::Type::EmphasizedAccelerate: - return "EmphasizedAccelerate"; - case base::Easing::Type::Standard: - return "Standard"; - case base::Easing::Type::StandardDecelerate: - return "StandardDecelerate"; - case base::Easing::Type::StandardAccelerate: - return "StandardAccelerate"; - case base::Easing::Type::Linear: - return "Linear"; - default: - return "Unknown"; - } -} -} // namespace - -namespace cf::ui::gallery { - -// ============================================================================= -// MotionPreviewWidget Implementation -// ============================================================================= - -MotionPreviewWidget::MotionPreviewWidget(const core::MotionSpec& spec, const QString& name, - QWidget* parent) - : QWidget(parent), spec_(spec), name_(name) { - setMinimumHeight(120); - setMaximumHeight(180); - - timer_ = new QTimer(this); - connect(timer_, &QTimer::timeout, this, &MotionPreviewWidget::updateAnimation); - - resetAnimation(); -} - -void MotionPreviewWidget::setProgress(float progress) { - progress_ = qBound(0.0f, progress, 1.0f); - - // Calculate ball position based on eased progress - float eased = calculateEasedProgress(progress_); - QRectF r = rect().adjusted(40, 40, -40, -40); - ballPosition_ = QPointF(r.left() + eased * r.width(), r.center().y()); - - update(); - emit progressChanged(); -} - -float MotionPreviewWidget::calculateEasedProgress(float linearProgress) const { - QEasingCurve curve = spec_.toEasingCurve(); - return curve.valueForProgress(linearProgress); -} - -void MotionPreviewWidget::startAnimation() { - isAnimating_ = true; - elapsed_ = 0.0f; - progress_ = 0.0f; - - // Timer runs at 60fps (16ms interval) - timer_->start(16); -} - -void MotionPreviewWidget::resetAnimation() { - isAnimating_ = false; - timer_->stop(); - progress_ = 0.0f; - elapsed_ = 0.0f; - ballPosition_ = QPointF(rect().left() + 40, rect().center().y()); - update(); -} - -void MotionPreviewWidget::updateAnimation() { - if (!isAnimating_) - return; - - // Increment elapsed time - elapsed_ += 16.0f; // 16ms per frame - - if (elapsed_ >= spec_.durationMs) { - elapsed_ = spec_.durationMs; - setProgress(1.0f); - isAnimating_ = false; - timer_->stop(); - emit animationFinished(); - } else { - float linearProgress = elapsed_ / spec_.durationMs; - setProgress(linearProgress); - } -} - -void MotionPreviewWidget::paintEvent(QPaintEvent*) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QRectF r = rect().adjusted(20, 20, -20, -20); - - // Background track - QColor trackColor = isDarkTheme_ ? QColor(60, 60, 60) : QColor(220, 220, 220); - QPainterPath trackPath; - trackPath.addRoundedRect(r, 8, 8); - painter.setPen(Qt::NoPen); - painter.setBrush(trackColor); - painter.drawPath(trackPath); - - // Progress fill (with easing curve color) - QEasingCurve curve = spec_.toEasingCurve(); - QColor progressColor; - switch (spec_.easing) { - case base::Easing::Type::Emphasized: - case base::Easing::Type::EmphasizedDecelerate: - progressColor = QColor(103, 80, 164); // Primary purple - break; - case base::Easing::Type::EmphasizedAccelerate: - progressColor = QColor(155, 93, 175); // Tertiary purple - break; - case base::Easing::Type::Standard: - case base::Easing::Type::StandardDecelerate: - case base::Easing::Type::StandardAccelerate: - progressColor = QColor(98, 91, 113); // Secondary - break; - case base::Easing::Type::Linear: - progressColor = QColor(98, 91, 113); - break; - default: - progressColor = QColor(103, 80, 164); - break; - } - - // Calculate eased progress for fill width - float eased = calculateEasedProgress(progress_); - float fillWidth = r.width() * eased; - - if (fillWidth > 0) { - QRectF fillRect(r.left(), r.top(), fillWidth, r.height()); - QPainterPath fillPath; - fillPath.addRoundedRect(fillRect, 8, 8); - - // Use a gradient for the progress fill - QLinearGradient gradient(r.left(), 0, r.left() + fillWidth, 0); - gradient.setColorAt(0, progressColor); - gradient.setColorAt(1, progressColor.lighter(120)); - painter.setBrush(gradient); - painter.drawPath(fillPath); - } - - // Animated ball - float ballRadius = 12; - QPainterPath ballPath; - ballPath.addEllipse(ballPosition_, ballRadius, ballRadius); - - // Ball shadow - painter.setPen(Qt::NoPen); - painter.setBrush(QColor(0, 0, 0, 50)); - painter.drawEllipse(ballPosition_ + QPointF(0, 2), ballRadius, ballRadius); - - // Ball gradient - QRadialGradient ballGradient(ballPosition_, ballRadius); - ballGradient.setColorAt(0, QColor(255, 255, 255)); - ballGradient.setColorAt(1, QColor(220, 220, 220)); - painter.setBrush(ballGradient); - painter.drawPath(ballPath); - - // Time markers - QFont markerFont("Segoe UI", 8); - painter.setFont(markerFont); - QColor markerColor = isDarkTheme_ ? QColor(180, 180, 180) : QColor(100, 100, 100); - painter.setPen(markerColor); - - painter.drawText(QRectF(r.left(), r.bottom() + 5, 40, 20), Qt::AlignCenter, "0ms"); - painter.drawText(QRectF(r.right() - 40, r.bottom() + 5, 40, 20), Qt::AlignCenter, - QString("%1ms").arg(spec_.durationMs)); - - // Current time label - if (isAnimating_ || progress_ > 0) { - int currentTime = static_cast(elapsed_); - painter.drawText(QRectF(r.center().x() - 30, r.top() - 20, 60, 20), Qt::AlignCenter, - QString("%1ms").arg(currentTime)); - } -} - -void MotionPreviewWidget::resizeEvent(QResizeEvent* event) { - QWidget::resizeEvent(event); - resetAnimation(); -} - -// ============================================================================= -// MotionCardWidget Implementation -// ============================================================================= - -MotionCardWidget::MotionCardWidget(const core::MotionSpec& spec, const QString& name, - const QString& description, QWidget* parent) - : QWidget(parent), spec_(spec), name_(name), description_(description) { - setMinimumSize(280, 200); - setMaximumSize(320, 240); - setCursor(Qt::PointingHandCursor); - - // Default theme colors (light) - backgroundColor_ = QColor(250, 250, 250); - surfaceColor_ = QColor(245, 245, 245); - onSurfaceColor_ = QColor(60, 60, 60); - - updateCurvePath(); -} - -void MotionCardWidget::updateCurvePath() { - // Generate easing curve visualization points - curvePath_ = QPainterPath(); - QRectF curveRect(10, 10, 80, 40); - curvePath_.moveTo(curveRect.bottomLeft()); - - QEasingCurve curve = spec_.toEasingCurve(); - for (int i = 0; i <= 20; i++) { - float t = i / 20.0f; - float value = curve.valueForProgress(t); - QPointF point(curveRect.left() + t * curveRect.width(), - curveRect.bottom() - value * curveRect.height()); - curvePath_.lineTo(point); - } -} - -void MotionCardWidget::setThemeColors(const QColor& background, const QColor& surface, - const QColor& onSurface) { - backgroundColor_ = background; - surfaceColor_ = surface; - onSurfaceColor_ = onSurface; - update(); -} - -void MotionCardWidget::paintEvent(QPaintEvent*) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QRectF r = rect().adjusted(4, 4, -4, -4); - float radius = 16; - - // Card background with elevation - QPainterPath path; - path.addRoundedRect(r, radius, radius); - - // Shadow when hovered - if (isHovered_) { - painter.setPen(Qt::NoPen); - painter.setBrush(QColor(0, 0, 0, 15)); - painter.drawPath(path.translated(0, 3)); - } - - // Card background - QColor bgColor = isHovered_ ? surfaceColor_.lighter(105) : surfaceColor_; - painter.setPen(QPen(QColor(200, 200, 200), 1)); - painter.setBrush(bgColor); - painter.drawPath(path); - - // Title - QFont titleFont("Segoe UI", 12, QFont::Bold); - painter.setFont(titleFont); - painter.setPen(onSurfaceColor_); - QRectF titleRect = r.adjusted(16, 16, -16, 0); - painter.drawText(titleRect, Qt::AlignLeft | Qt::AlignTop, name_); - - // Description - QFont descFont("Segoe UI", 9); - painter.setFont(descFont); - QColor descColor = onSurfaceColor_; - descColor.setAlpha(180); - painter.setPen(descColor); - QRectF descRect = titleRect.adjusted(0, QFontMetrics(titleFont).height() + 4, 0, 0); - painter.drawText(descRect, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, description_); - - // Spec info (duration + easing) - QFont infoFont("Consolas", 10); - painter.setFont(infoFont); - QColor infoColor = QColor(103, 80, 164); // Primary purple - painter.setPen(infoColor); - - QString specText = - QString("⏱ %1ms • %2").arg(spec_.durationMs).arg(easingTypeToString(spec_.easing)); - - QRectF specRect = r.adjusted(16, 0, -16, -50); - painter.drawText(specRect, Qt::AlignLeft | Qt::AlignBottom, specText); - - // Easing curve preview - QRectF curveBox(r.right() - 100, r.top() + 16, 84, 48); - QPainterPath curveBoxPath; - curveBoxPath.addRoundedRect(curveBox, 8, 8); - - // Curve box background - painter.setPen(Qt::NoPen); - painter.setBrush(isDarkTheme_ ? QColor(40, 40, 40) : QColor(235, 235, 235)); - painter.drawPath(curveBoxPath); - - // Draw the curve - painter.setPen(QPen(QColor(103, 80, 164), 2)); - painter.setBrush(Qt::NoBrush); - painter.drawPath(curvePath_); - - // Play button hint - if (isHovered_) { - QFont hintFont("Segoe UI", 9); - painter.setFont(hintFont); - painter.setPen(onSurfaceColor_); - painter.drawText(r.adjusted(16, 0, -16, -16), Qt::AlignBottom | Qt::AlignHCenter, - "▶ 点击预览动画"); - } -} - -void MotionCardWidget::enterEvent(QEnterEvent*) { - isHovered_ = true; - update(); -} - -void MotionCardWidget::leaveEvent(QEvent*) { - isHovered_ = false; - update(); -} - -void MotionCardWidget::mousePressEvent(QMouseEvent*) { - emit playRequested(spec_); -} - -// ============================================================================= -// ToastWidget Implementation -// ============================================================================= - -ToastWidget::ToastWidget(const QString& message, QWidget* parent) - : QWidget(parent), message_(message) { - setAttribute(Qt::WA_TransparentForMouseEvents, false); - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); - setAttribute(Qt::WA_TranslucentBackground); -} - -void ToastWidget::show(int durationMs) { - QFont font("Segoe UI", 10); - QFontMetrics fm(font); - int padding = 20; - int textWidth = fm.horizontalAdvance(message_); - int width = textWidth + padding * 2; - int height = fm.height() + padding * 2; - - setFixedSize(width, height); - - if (parentWidget()) { - QPoint pos = parentWidget()->mapToGlobal(parentWidget()->rect().bottomLeft()); - int x = pos.x() + (parentWidget()->width() - width) / 2; - int y = pos.y() - height - 20; - move(x, y); - } - - QWidget::show(); - - QTimer::singleShot(durationMs, this, [this]() { - QPropertyAnimation* anim = new QPropertyAnimation(this, "windowOpacity"); - anim->setDuration(300); - anim->setStartValue(1.0); - anim->setEndValue(0.0); - connect(anim, &QPropertyAnimation::finished, this, &QWidget::hide); - connect(anim, &QPropertyAnimation::finished, anim, &QObject::deleteLater); - anim->start(QAbstractAnimation::DeleteWhenStopped); - }); -} - -void ToastWidget::paintEvent(QPaintEvent*) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QPainterPath path; - path.addRoundedRect(rect(), 12, 12); - painter.fillPath(path, QColor(0, 0, 0, 200)); - - painter.setPen(Qt::white); - QFont font("Segoe UI", 10); - painter.setFont(font); - painter.drawText(rect(), Qt::AlignCenter, message_); -} - -// ============================================================================= -// MaterialMotionSpecMainWindow Implementation -// ============================================================================= - -MaterialMotionSpecMainWindow::MaterialMotionSpecMainWindow(QWidget* parent) : QMainWindow(parent) { - - // Initialize motion presets - motionPresets_ = { - {"Short Enter", "小元素入场动画 (如按钮、图标)", core::MotionPresets::shortEnter()}, - {"Short Exit", "小元素离场动画", core::MotionPresets::shortExit()}, - {"Medium Enter", "中等元素入场动画 (如卡片、列表项)", core::MotionPresets::mediumEnter()}, - {"Medium Exit", "中等元素离场动画", core::MotionPresets::mediumExit()}, - {"Long Enter", "大元素入场动画 (如对话框、页面)", core::MotionPresets::longEnter()}, - {"Long Exit", "大元素离场动画", core::MotionPresets::longExit()}, - {"State Change", "状态层动画 (hover、focus)", core::MotionPresets::stateChange()}, - {"Ripple Expand", "涟漪扩散动画", core::MotionPresets::rippleExpand()}, - {"Ripple Fade", "涟漪淡出动画", core::MotionPresets::rippleFade()}}; - - setupUI(); - createHeader(); - createPreviewSection(); - createMotionCards(); - updateWindowTheme(); -} - -MaterialMotionSpecMainWindow::~MaterialMotionSpecMainWindow() = default; - -void MaterialMotionSpecMainWindow::setupUI() { - setWindowTitle("Material Motion Spec Gallery"); - resize(1200, 900); - setMinimumSize(900, 700); - - centralWidget_ = new QWidget(this); - setCentralWidget(centralWidget_); - - mainLayout_ = new QVBoxLayout(centralWidget_); - mainLayout_->setContentsMargins(20, 20, 20, 20); - mainLayout_->setSpacing(20); - - // Header - headerLayout_ = new QHBoxLayout(); - headerLayout_->setSpacing(16); - mainLayout_->addLayout(headerLayout_); - - // Preview frame - previewFrame_ = new QFrame(this); - previewFrame_->setFrameStyle(QFrame::StyledPanel); - previewLayout_ = new QVBoxLayout(previewFrame_); - previewLayout_->setContentsMargins(16, 16, 16, 16); - mainLayout_->addWidget(previewFrame_); - - // Scroll area for motion cards - scrollArea_ = new QScrollArea(this); - scrollArea_->setWidgetResizable(true); - scrollArea_->setFrameShape(QFrame::NoFrame); - scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scrollContent_ = new QWidget(); - scrollLayout_ = new QVBoxLayout(scrollContent_); - scrollLayout_->setSpacing(20); - - cardsLayout_ = new QGridLayout(); - cardsLayout_->setSpacing(16); - cardsLayout_->setContentsMargins(0, 0, 0, 0); - scrollLayout_->addLayout(cardsLayout_); - scrollLayout_->addStretch(); - - scrollArea_->setWidget(scrollContent_); - mainLayout_->addWidget(scrollArea_); - - // Toast - toast_ = new ToastWidget("", this); - toast_->hide(); -} - -void MaterialMotionSpecMainWindow::createHeader() { - // Title - titleLabel_ = new QLabel("Material Motion Spec Gallery", this); - QFont titleFont("Segoe UI", 18, QFont::Bold); - titleLabel_->setFont(titleFont); - headerLayout_->addWidget(titleLabel_); - headerLayout_->addStretch(); - - // Theme toggle button - themeButton_ = new QPushButton("🌙 Dark Mode", this); - themeButton_->setCheckable(true); - themeButton_->setFixedSize(120, 36); - connect(themeButton_, &QPushButton::clicked, this, - &MaterialMotionSpecMainWindow::onThemeToggle); - headerLayout_->addWidget(themeButton_); -} - -void MaterialMotionSpecMainWindow::createPreviewSection() { - // Preview title - previewLabel_ = new QLabel("动画预览区 - 点击下方卡片播放", this); - QFont previewTitleFont("Segoe UI", 14, QFont::Medium); - previewLabel_->setFont(previewTitleFont); - previewLayout_->addWidget(previewLabel_); - - // Preview widget container - previewContainer_ = new QHBoxLayout(); - - // Preview widget - previewWidget_ = new MotionPreviewWidget(core::MotionPresets::shortEnter(), "shortEnter", this); - previewContainer_->addWidget(previewWidget_, 1); - - // Info panel - QVBoxLayout* infoPanel = new QVBoxLayout(); - - // Speed control - QLabel* speedLabel = new QLabel("播放速度:", this); - infoPanel->addWidget(speedLabel); - - speedCombo_ = new QComboBox(this); - speedCombo_->addItem("1x (正常)", 1); - speedCombo_->addItem("0.5x (慢放)", 2); - speedCombo_->addItem("0.25x (极慢)", 4); - speedCombo_->setCurrentIndex(0); - connect(speedCombo_, QOverload::of(&QComboBox::currentIndexChanged), this, - &MaterialMotionSpecMainWindow::onSpeedChanged); - infoPanel->addWidget(speedCombo_); - - infoPanel->addStretch(); - - // Spec info - previewInfoLabel_ = new QLabel(this); - previewInfoLabel_->setWordWrap(true); - QFont infoFont("Consolas", 10); - previewInfoLabel_->setFont(infoFont); - infoPanel->addWidget(previewInfoLabel_); - - previewContainer_->addLayout(infoPanel, 1); - previewLayout_->addLayout(previewContainer_); - - // Update initial info - onPlayRequested(core::MotionPresets::shortEnter()); -} - -void MaterialMotionSpecMainWindow::createMotionCards() { - int row = 0, col = 0; - int maxCols = 3; - - for (const auto& preset : motionPresets_) { - auto* card = - new MotionCardWidget(preset.spec, preset.name, preset.description, scrollContent_); - - cardsLayout_->addWidget(card, row, col); - motionCards_.append(card); - - connect(card, &MotionCardWidget::playRequested, this, - &MaterialMotionSpecMainWindow::onPlayRequested); - - col++; - if (col >= maxCols) { - col = 0; - row++; - } - } -} - -void MaterialMotionSpecMainWindow::updateWindowTheme() { - auto scheme = isDarkTheme_ ? cf::ui::core::material::dark() : cf::ui::core::material::light(); - - QColor bg = scheme.queryColor("md.background"); - QColor surface = scheme.queryColor("md.surface"); - QColor onSurface = scheme.queryColor("md.onSurface"); - QColor primary = scheme.queryColor("md.primary"); - - centralWidget_->setAutoFillBackground(true); - QPalette pal = centralWidget_->palette(); - pal.setColor(QPalette::Window, bg); - centralWidget_->setPalette(pal); - - scrollContent_->setAutoFillBackground(true); - pal = scrollContent_->palette(); - pal.setColor(QPalette::Window, bg); - scrollContent_->setPalette(pal); - - titleLabel_->setStyleSheet(QString("color: %1").arg(onSurface.name())); - previewLabel_->setStyleSheet(QString("color: %1").arg(onSurface.name())); - - // Update preview frame - previewFrame_->setStyleSheet( - QString("QFrame { background-color: %1; border-radius: 12px; border: 1px solid %2; }") - .arg(surface.name()) - .arg(scheme.queryColor("md.outlineVariant").name())); - - // Update all cards - for (auto* card : motionCards_) { - card->setThemeColors(bg, surface, onSurface); - } - - // Update theme button - themeButton_->setText(isDarkTheme_ ? "☀️ Light Mode" : "🌙 Dark Mode"); -} - -QString MaterialMotionSpecMainWindow::easingTypeToString(base::Easing::Type type) const { - switch (type) { - case base::Easing::Type::Emphasized: - return "Emphasized"; - case base::Easing::Type::EmphasizedDecelerate: - return "EmphasizedDecelerate"; - case base::Easing::Type::EmphasizedAccelerate: - return "EmphasizedAccelerate"; - case base::Easing::Type::Standard: - return "Standard"; - case base::Easing::Type::StandardDecelerate: - return "StandardDecelerate"; - case base::Easing::Type::StandardAccelerate: - return "StandardAccelerate"; - case base::Easing::Type::Linear: - return "Linear"; - default: - return "Unknown"; - } -} - -void MaterialMotionSpecMainWindow::onPlayRequested(const core::MotionSpec& spec) { - // Find preset name - QString presetName; - for (const auto& preset : motionPresets_) { - if (preset.spec.durationMs == spec.durationMs && preset.spec.easing == spec.easing) { - presetName = preset.name; - break; - } - } - - // Create a copy with adjusted duration for animation speed - int speedMultiplier = speedCombo_->currentData().toInt(); - core::MotionSpec adjustedSpec = spec; - adjustedSpec.durationMs = spec.durationMs * speedMultiplier; - - // Update the existing preview widget with new spec - if (previewWidget_) { - previewWidget_->setDarkTheme(isDarkTheme_); - previewWidget_->updateSpec(adjustedSpec); - } - - // Update info label - QString infoText = QString("动画名称: %1\n" - "持续时间: %2ms\n" - "缓动类型: %3\n" - "延迟: %4ms") - .arg(presetName) - .arg(adjustedSpec.durationMs) - .arg(easingTypeToString(spec.easing)) - .arg(spec.delayMs); - - previewInfoLabel_->setText(infoText); - - // Start animation - previewWidget_->startAnimation(); -} - -void MaterialMotionSpecMainWindow::onAnimationFinished() { - // Could add replay button or auto-replay functionality here -} - -void MaterialMotionSpecMainWindow::onThemeToggle() { - isDarkTheme_ = themeButton_->isChecked(); - updateWindowTheme(); -} - -void MaterialMotionSpecMainWindow::onSpeedChanged(int index) { - Q_UNUSED(index); - // Speed will be applied on next play -} - -void MaterialMotionSpecMainWindow::resizeEvent(QResizeEvent* event) { - QMainWindow::resizeEvent(event); -} - -} // namespace cf::ui::gallery diff --git a/example/gui/material/material_motion_spec/MaterialMotionSpecMainWindow.h b/example/gui/material/material_motion_spec/MaterialMotionSpecMainWindow.h deleted file mode 100644 index 4e7728a43..000000000 --- a/example/gui/material/material_motion_spec/MaterialMotionSpecMainWindow.h +++ /dev/null @@ -1,230 +0,0 @@ -/** - * @file MaterialMotionSpecMainWindow.h - * @brief Material Design 3 Motion Spec Gallery - Main Window - * - * A visual gallery to display all Material Design 3 motion presets - * with interactive animation demonstrations and easing curve visualization. - * - * @author CFDesktop Team - * @date 2026-02-26 - * @version 0.1 - */ - -#pragma once - -#include "ui/core/material/cfmaterial_motion.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cf::ui::gallery { - -/** - * @brief Animation preview widget. - * - * Shows a visual representation of the motion spec with an animated ball - * that demonstrates the easing curve and duration. - */ -class MotionPreviewWidget : public QWidget { - Q_OBJECT - Q_PROPERTY(float progress READ progress WRITE setProgress NOTIFY progressChanged) - - public: - explicit MotionPreviewWidget(const cf::ui::core::MotionSpec& spec, const QString& name, - QWidget* parent = nullptr); - - float progress() const { return progress_; } - void setProgress(float progress); - - void startAnimation(); - void resetAnimation(); - void setDarkTheme(bool dark) { - isDarkTheme_ = dark; - update(); - } - void updateSpec(const cf::ui::core::MotionSpec& spec) { - spec_ = spec; - resetAnimation(); - } - - signals: - void progressChanged(); - void animationFinished(); - - protected: - void paintEvent(QPaintEvent* event) override; - void resizeEvent(QResizeEvent* event) override; - - private: - void updateAnimation(); - float calculateEasedProgress(float linearProgress) const; - - cf::ui::core::MotionSpec spec_; - QString name_; - float progress_ = 0.0f; - float elapsed_ = 0.0f; - QTimer* timer_; - bool isAnimating_ = false; - bool isDarkTheme_ = false; - - // Ball position cache - QPointF ballPosition_; -}; - -/** - * @brief Motion spec card widget. - * - * Displays information about a single motion preset: - * - Name and description - * - Duration and easing type - * - Visual easing curve preview - * - Play button for animation demo - */ -class MotionCardWidget : public QWidget { - Q_OBJECT - - public: - explicit MotionCardWidget(const cf::ui::core::MotionSpec& spec, const QString& name, - const QString& description, QWidget* parent = nullptr); - - void setThemeColors(const QColor& background, const QColor& surface, const QColor& onSurface); - - signals: - void playRequested(const cf::ui::core::MotionSpec& spec); - - protected: - void paintEvent(QPaintEvent* event) override; - void enterEvent(QEnterEvent* event) override; - void leaveEvent(QEvent* event) override; - void mousePressEvent(QMouseEvent* event) override; - - private: - cf::ui::core::MotionSpec spec_; - QString name_; - QString description_; - bool isHovered_ = false; - bool isDarkTheme_ = false; - - // Theme colors - QColor backgroundColor_; - QColor surfaceColor_; - QColor onSurfaceColor_; - - // Easing curve points cache - void updateCurvePath(); - QPainterPath curvePath_; -}; - -/** - * @brief Toast notification widget for displaying temporary messages. - */ -class ToastWidget : public QWidget { - Q_OBJECT - public: - explicit ToastWidget(const QString& message, QWidget* parent = nullptr); - void show(int durationMs = 2000); - - protected: - void paintEvent(QPaintEvent* event) override; - - private: - QString message_; -}; - -/** - * @brief Main window for the Material Motion Spec Gallery. - * - * Displays all 9 Material Design 3 motion presets: - * - Short Enter/Exit (200/150ms) - * - Medium Enter/Exit (300/250ms) - * - Long Enter/Exit (450/400ms) - * - State Change (200ms) - * - Ripple Expand/Fade (400/150ms) - */ -class MaterialMotionSpecMainWindow : public QMainWindow { - Q_OBJECT - - public: - explicit MaterialMotionSpecMainWindow(QWidget* parent = nullptr); - ~MaterialMotionSpecMainWindow() override; - - protected: - void resizeEvent(QResizeEvent* event) override; - - private: - void setupUI(); - void createHeader(); - void createMotionCards(); - void createPreviewSection(); - void updateWindowTheme(); - - QString easingTypeToString(cf::ui::base::Easing::Type type) const; - - private slots: - void onPlayRequested(const cf::ui::core::MotionSpec& spec); - void onAnimationFinished(); - void onThemeToggle(); - void onSpeedChanged(int index); - - private: - // Motion scheme - cf::ui::core::MaterialMotionScheme motionScheme_; - - // UI components - QWidget* centralWidget_; - QVBoxLayout* mainLayout_; - QHBoxLayout* headerLayout_; - QScrollArea* scrollArea_; - QWidget* scrollContent_; - QVBoxLayout* scrollLayout_; - QGridLayout* cardsLayout_; - QFrame* previewFrame_; - QVBoxLayout* previewLayout_; - - // Header components - QLabel* titleLabel_; - QLabel* themeLabel_; - QPushButton* themeButton_; - - // Preview section - QLabel* previewLabel_; - QHBoxLayout* previewContainer_; - MotionPreviewWidget* previewWidget_; - QLabel* previewInfoLabel_; - QComboBox* speedCombo_; - - // Theme - bool isDarkTheme_ = false; - - // Toast - ToastWidget* toast_; - - // All 9 motion presets info - struct MotionPresetInfo { - QString name; - QString description; - cf::ui::core::MotionSpec spec; - }; - QList motionPresets_; - QList motionCards_; - - // Current animation - QPropertyAnimation* currentAnimation_ = nullptr; -}; - -} // namespace cf::ui::gallery diff --git a/example/gui/material/material_motion_spec/main.cpp b/example/gui/material/material_motion_spec/main.cpp deleted file mode 100644 index 0667e430d..000000000 --- a/example/gui/material/material_motion_spec/main.cpp +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @file main.cpp - * @brief Material Motion Spec Gallery - Main Entry Point - * - * Entry point for the Material Design 3 Motion Spec Gallery application. - * Displays all 9 motion presets with interactive animation demonstrations. - * - * @author CFDesktop Team - * @date 2026-02-26 - * @version 0.1 - */ - -#include "MaterialMotionSpecMainWindow.h" - -#include - -int main(int argc, char* argv[]) { - QApplication app(argc, argv); - - // Set application metadata - app.setApplicationName("Material Motion Spec Gallery"); - app.setApplicationVersion("0.1"); - app.setOrganizationName("CFDesktop"); - - // Create and show main window - cf::ui::gallery::MaterialMotionSpecMainWindow window; - window.show(); - - return app.exec(); -} diff --git a/example/gui/material/material_radius_scale/CMakeLists.txt b/example/gui/material/material_radius_scale/CMakeLists.txt deleted file mode 100644 index 553451f56..000000000 --- a/example/gui/material/material_radius_scale/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -# Material Radius Scale Gallery Example -log_info("Material Radius Scale" "Building Radius Scale Gallery Example") - -add_executable(material_radius_scale_gallery - main.cpp - MaterialRadiusScaleMainWindow.h - MaterialRadiusScaleMainWindow.cpp -) - -target_link_libraries(material_radius_scale_gallery PRIVATE - Qt6::Widgets - Qt6::Core - Qt6::Gui - cfui -) - -# Set output directory to examples/gui/ -cf_set_example_output_dir(material_radius_scale_gallery "gui") -cf_register_example_launcher(material_radius_scale_gallery "gui") - -# Set as GUI application on Windows (hide console window) -if(WIN32) - set_target_properties(material_radius_scale_gallery PROPERTIES - WIN32_EXECUTABLE TRUE - ) -endif() diff --git a/example/gui/material/material_radius_scale/MaterialRadiusScaleMainWindow.cpp b/example/gui/material/material_radius_scale/MaterialRadiusScaleMainWindow.cpp deleted file mode 100644 index fae3aa117..000000000 --- a/example/gui/material/material_radius_scale/MaterialRadiusScaleMainWindow.cpp +++ /dev/null @@ -1,172 +0,0 @@ -/** - * @file MaterialRadiusScaleMainWindow.cpp - * @brief Material Design 3 Radius Scale Gallery - Implementation - * - * @author CFDesktop Team - * @date 2026-02-26 - * @version 0.1 - */ - -#include "MaterialRadiusScaleMainWindow.h" - -#include "ui/core/material/material_factory.hpp" -#include "ui/core/token/radius_scale/cfmaterial_radius_scale_literals.h" -#include -#include -#include - -// Global namespace alias for token literals -namespace token_literals = ::cf::ui::core::token::literals; - -namespace cf::ui::gallery { - -// ============================================================================= -// RadiusPreviewWidget Implementation -// ============================================================================= - -RadiusPreviewWidget::RadiusPreviewWidget(const QString& name, float radiusDp, - const QColor& accentColor, QWidget* parent) - : QWidget(parent), name_(name), radiusDp_(radiusDp), accentColor_(accentColor) { - setMinimumSize(200, 180); -} - -void RadiusPreviewWidget::paintEvent(QPaintEvent*) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QRectF r = rect().adjusted(10, 10, -10, -10); - - // 卡片背景 - QPainterPath cardPath; - cardPath.addRoundedRect(r, 12, 12); - painter.setPen(QPen(QColor(200, 200, 200), 1)); - painter.setBrush(QColor(250, 250, 250)); - painter.drawPath(cardPath); - - // 圆角预览区域 - QRectF previewRect = r.adjusted(20, 50, -20, -50); - - // 根据 dp 计算 px (假设 1dp = 1px 用于预览) - float radiusPx = radiusDp_; - - QPainterPath previewPath; - previewPath.addRoundedRect(previewRect, radiusPx, radiusPx); - - // 绘制带圆角的矩形 - painter.setPen(Qt::NoPen); - painter.setBrush(accentColor_); - painter.drawPath(previewPath); - - // 标题 - QFont nameFont("Segoe UI", 12, QFont::Bold); - painter.setFont(nameFont); - painter.setPen(QColor(60, 60, 60)); - painter.drawText(QRectF(r.left(), r.top() + 10, r.width(), 25), Qt::AlignCenter, name_); - - // dp 值标签 - QFont dpFont("Consolas", 11); - painter.setFont(dpFont); - painter.setPen(QColor(100, 100, 100)); - QString dpText = QString("%1 dp").arg(radiusDp_, 0, 'f', 0); - painter.drawText(QRectF(previewRect.left(), previewRect.bottom() + 8, previewRect.width(), 20), - Qt::AlignCenter, dpText); -} - -// ============================================================================= -// MaterialRadiusScaleMainWindow Implementation -// ============================================================================= - -MaterialRadiusScaleMainWindow::MaterialRadiusScaleMainWindow(QWidget* parent) - : QMainWindow(parent), radiusScale_(cf::ui::core::material::defaultRadiusScale()) { - - setupUI(); - createHeader(); - createRadiusCards(); -} - -void MaterialRadiusScaleMainWindow::setupUI() { - // Window setup - setWindowTitle("Material Radius Scale Gallery"); - resize(1000, 700); - setMinimumSize(600, 400); - - // Central widget - centralWidget_ = new QWidget(this); - setCentralWidget(centralWidget_); - - mainLayout_ = new QVBoxLayout(centralWidget_); - mainLayout_->setContentsMargins(20, 20, 20, 20); - mainLayout_->setSpacing(16); - - // Header - auto* headerLayout = new QHBoxLayout(); - headerLayout->setSpacing(16); - mainLayout_->addLayout(headerLayout); - - auto* titleLabel = new QLabel("Material Radius Scale Gallery", this); - QFont titleFont("Segoe UI", 18, QFont::Bold); - titleLabel->setFont(titleFont); - headerLayout->addWidget(titleLabel); - headerLayout->addStretch(); - - auto* subtitleLabel = new QLabel("Material Design 3 Corner Radius Tokens", this); - QFont subtitleFont("Segoe UI", 11); - subtitleLabel->setFont(subtitleFont); - subtitleLabel->setStyleSheet("color: #666;"); - headerLayout->addWidget(subtitleLabel); - - // Scroll area for cards - scrollArea_ = new QScrollArea(this); - scrollArea_->setWidgetResizable(true); - scrollArea_->setFrameShape(QFrame::NoFrame); - scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scrollContent_ = new QWidget(); - auto* scrollLayout = new QVBoxLayout(scrollContent_); - scrollLayout->setSpacing(20); - - cardsLayout_ = new QGridLayout(); - cardsLayout_->setSpacing(16); - cardsLayout_->setContentsMargins(0, 0, 0, 0); - scrollLayout->addLayout(cardsLayout_); - scrollLayout->addStretch(); - - scrollArea_->setWidget(scrollContent_); - mainLayout_->addWidget(scrollArea_); -} - -void MaterialRadiusScaleMainWindow::createHeader() { - // Header already created in setupUI -} - -void MaterialRadiusScaleMainWindow::createRadiusCards() { - struct RadiusInfo { - const char* token; - const char* name; - QColor color; - }; - - static const RadiusInfo radii[] = { - {token_literals::CORNER_NONE, "Corner None", QColor(100, 100, 100)}, - {token_literals::CORNER_EXTRA_SMALL, "Corner Extra Small", QColor(103, 80, 164)}, - {token_literals::CORNER_SMALL, "Corner Small", QColor(103, 80, 164)}, - {token_literals::CORNER_MEDIUM, "Corner Medium", QColor(103, 80, 164)}, - {token_literals::CORNER_LARGE, "Corner Large", QColor(103, 80, 164)}, - {token_literals::CORNER_EXTRA_LARGE, "Corner Extra Large", QColor(103, 80, 164)}, - {token_literals::CORNER_EXTRA_EXTRA_LARGE, "Corner Extra Extra Large", - QColor(103, 80, 164)}}; - - int row = 0, col = 0; - for (const auto& info : radii) { - float radius = radiusScale_.queryRadiusScale(info.token); - auto* card = new RadiusPreviewWidget(info.name, radius, info.color, scrollContent_); - cardsLayout_->addWidget(card, row, col++); - - if (col >= 4) { - col = 0; - row++; - } - } -} - -} // namespace cf::ui::gallery diff --git a/example/gui/material/material_radius_scale/MaterialRadiusScaleMainWindow.h b/example/gui/material/material_radius_scale/MaterialRadiusScaleMainWindow.h deleted file mode 100644 index ea3dd79da..000000000 --- a/example/gui/material/material_radius_scale/MaterialRadiusScaleMainWindow.h +++ /dev/null @@ -1,84 +0,0 @@ -/** - * @file MaterialRadiusScaleMainWindow.h - * @brief Material Design 3 Radius Scale Gallery - Main Window - * - * A visual gallery to display all Material Design 3 corner radius tokens - * with visual preview cards. - * - * @author CFDesktop Team - * @date 2026-02-26 - * @version 0.1 - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "ui/core/material/cfmaterial_radius_scale.h" - -namespace cf::ui::gallery { - -/** - * @brief 圆角预览卡片 - 显示一个带圆角的矩形 - * - * 每个卡片展示: - * - 圆角级别的名称(如 "Corner Small") - * - 带对应圆角的预览矩形 - * - dp 值标签(如 "8 dp") - */ -class RadiusPreviewWidget : public QWidget { - Q_OBJECT - public: - explicit RadiusPreviewWidget(const QString& name, float radiusDp, const QColor& accentColor, - QWidget* parent = nullptr); - - protected: - void paintEvent(QPaintEvent* event) override; - - private: - QString name_; - float radiusDp_; - QColor accentColor_; -}; - -/** - * @brief 主窗口 - 展示所有 7 个圆角级别 - * - * 显示 Material Design 3 的所有圆角半径: - * - Corner None (0dp) - * - Corner Extra Small (4dp) - * - Corner Small (8dp) - * - Corner Medium (12dp) - * - Corner Large (16dp) - * - Corner Extra Large (28dp) - * - Corner Extra Extra Large (32dp) - */ -class MaterialRadiusScaleMainWindow : public QMainWindow { - Q_OBJECT - - public: - explicit MaterialRadiusScaleMainWindow(QWidget* parent = nullptr); - ~MaterialRadiusScaleMainWindow() override = default; - - private: - void setupUI(); - void createHeader(); - void createRadiusCards(); - - private: - cf::ui::core::MaterialRadiusScale radiusScale_; - - QWidget* centralWidget_; - QVBoxLayout* mainLayout_; - QScrollArea* scrollArea_; - QWidget* scrollContent_; - QGridLayout* cardsLayout_; -}; - -} // namespace cf::ui::gallery diff --git a/example/gui/material/material_radius_scale/main.cpp b/example/gui/material/material_radius_scale/main.cpp deleted file mode 100644 index 1abb9e1aa..000000000 --- a/example/gui/material/material_radius_scale/main.cpp +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @file main.cpp - * @brief Material Radius Scale Gallery - Main Entry Point - * - * Entry point for the Material Design 3 Radius Scale Gallery application. - * Displays all 7 corner radius tokens with visual preview. - * - * @author CFDesktop Team - * @date 2026-02-26 - * @version 0.1 - */ - -#include "MaterialRadiusScaleMainWindow.h" - -#include - -int main(int argc, char* argv[]) { - QApplication app(argc, argv); - - // Set application metadata - app.setApplicationName("Material Radius Scale Gallery"); - app.setApplicationVersion("0.1"); - app.setOrganizationName("CFDesktop"); - - // Create and show main window - cf::ui::gallery::MaterialRadiusScaleMainWindow window; - window.show(); - - return app.exec(); -} diff --git a/example/gui/material/material_typography/CMakeLists.txt b/example/gui/material/material_typography/CMakeLists.txt deleted file mode 100644 index a4c03e20a..000000000 --- a/example/gui/material/material_typography/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -# Material Typography Gallery Example -log_info("Material Typography" "Building Typography Gallery Example") - -add_executable(material_typography_gallery - main.cpp - MaterialTypographyMainWindow.h - MaterialTypographyMainWindow.cpp -) - -target_link_libraries(material_typography_gallery PRIVATE - Qt6::Widgets - Qt6::Core - Qt6::Gui - cfui -) - -# Set output directory to examples/gui/ -cf_set_example_output_dir(material_typography_gallery "gui") -cf_register_example_launcher(material_typography_gallery "gui") - -# Set as GUI application on Windows (hide console window) -if(WIN32) - set_target_properties(material_typography_gallery PROPERTIES - WIN32_EXECUTABLE TRUE - ) -endif() diff --git a/example/gui/material/material_typography/MaterialTypographyMainWindow.cpp b/example/gui/material/material_typography/MaterialTypographyMainWindow.cpp deleted file mode 100644 index d24522e7c..000000000 --- a/example/gui/material/material_typography/MaterialTypographyMainWindow.cpp +++ /dev/null @@ -1,459 +0,0 @@ -/** - * @file MaterialTypographyMainWindow.cpp - * @brief Material Design 3 Typography Gallery - Implementation - */ - -#include "MaterialTypographyMainWindow.h" -#include "material/material_factory.hpp" -#include "ui/core/token/typography/cfmaterial_typography_token_literals.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -// Global namespace alias for token literals -namespace token_literals = ::cf::ui::core::token::literals; - -namespace cf::ui::gallery { - -// ============================================================================= -// Static Token Lists -// ============================================================================= - -const QStringList MaterialTypographyMainWindow::DISPLAY_TOKENS = { - token_literals::TYPOGRAPHY_DISPLAY_LARGE, token_literals::TYPOGRAPHY_DISPLAY_MEDIUM, - token_literals::TYPOGRAPHY_DISPLAY_SMALL}; - -const QStringList MaterialTypographyMainWindow::HEADLINE_TOKENS = { - token_literals::TYPOGRAPHY_HEADLINE_LARGE, token_literals::TYPOGRAPHY_HEADLINE_MEDIUM, - token_literals::TYPOGRAPHY_HEADLINE_SMALL}; - -const QStringList MaterialTypographyMainWindow::TITLE_TOKENS = { - token_literals::TYPOGRAPHY_TITLE_LARGE, token_literals::TYPOGRAPHY_TITLE_MEDIUM, - token_literals::TYPOGRAPHY_TITLE_SMALL}; - -const QStringList MaterialTypographyMainWindow::BODY_TOKENS = { - token_literals::TYPOGRAPHY_BODY_LARGE, token_literals::TYPOGRAPHY_BODY_MEDIUM, - token_literals::TYPOGRAPHY_BODY_SMALL}; - -const QStringList MaterialTypographyMainWindow::LABEL_TOKENS = { - token_literals::TYPOGRAPHY_LABEL_LARGE, token_literals::TYPOGRAPHY_LABEL_MEDIUM, - token_literals::TYPOGRAPHY_LABEL_SMALL}; - -// ============================================================================= -// ToastWidget Implementation -// ============================================================================= - -ToastWidget::ToastWidget(const QString& message, QWidget* parent) - : QWidget(parent), message_(message) { - setAttribute(Qt::WA_TransparentForMouseEvents, false); - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); - setAttribute(Qt::WA_TranslucentBackground); -} - -void ToastWidget::show(int durationMs) { - // Calculate size and position - QFont font("Segoe UI", 10); - QFontMetrics fm(font); - int padding = 20; - int textWidth = fm.horizontalAdvance(message_); - int width = textWidth + padding * 2; - int height = fm.height() + padding * 2; - - setFixedSize(width, height); - - // Center at bottom of parent - if (parentWidget()) { - QPoint pos = parentWidget()->mapToGlobal(parentWidget()->rect().bottomLeft()); - int x = pos.x() + (parentWidget()->width() - width) / 2; - int y = pos.y() - height - 20; - move(x, y); - } - - QWidget::show(); - - // Auto hide after duration - QTimer::singleShot(durationMs, this, [this]() { - // Fade out animation - QPropertyAnimation* anim = new QPropertyAnimation(this, "windowOpacity"); - anim->setDuration(300); - anim->setStartValue(1.0); - anim->setEndValue(0.0); - connect(anim, &QPropertyAnimation::finished, this, [this]() { - hide(); - deleteLater(); - }); - anim->start(QPropertyAnimation::DeleteWhenStopped); - }); -} - -void ToastWidget::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - // Rounded rectangle background - QRectF rect = this->rect().adjusted(2, 2, -2, -2); - QPainterPath path; - path.addRoundedRect(rect, 8, 8); - - // Semi-transparent black background - QColor bgColor(0, 0, 0, 200); - painter.fillPath(path, bgColor); - - // White text - painter.setPen(Qt::white); - QFont font("Segoe UI", 10); - painter.setFont(font); - - QRect textRect = rect.toRect(); - painter.drawText(textRect, Qt::AlignCenter, message_); -} - -// ============================================================================= -// FontCardWidget Implementation -// ============================================================================= - -FontCardWidget::FontCardWidget(const QString& tokenName, const QFont& font, float lineHeight, - const QString& previewText, QWidget* parent) - : QWidget(parent), tokenName_(tokenName), font_(font), lineHeight_(lineHeight), - previewText_(previewText) { - cssStyle_ = generateCssStyle(); - setCursor(Qt::PointingHandCursor); - // Calculate minimum height based on font size - QFontMetrics fm(font_); - int minHeight = qMax(120, fm.height() * 3 + 60); // Enough space for preview + props - setMinimumHeight(minHeight); -} - -void FontCardWidget::updateFont(const QFont& font, float lineHeight) { - font_ = font; - lineHeight_ = lineHeight; - cssStyle_ = generateCssStyle(); - update(); -} - -QString FontCardWidget::generateCssStyle() const { - QString weightStr; - switch (font_.weight()) { - case QFont::Thin: - weightStr = "100"; - break; - case QFont::ExtraLight: - weightStr = "200"; - break; - case QFont::Light: - weightStr = "300"; - break; - case QFont::Normal: - weightStr = "400"; - break; - case QFont::Medium: - weightStr = "500"; - break; - case QFont::DemiBold: - weightStr = "600"; - break; - case QFont::Bold: - weightStr = "700"; - break; - case QFont::ExtraBold: - weightStr = "800"; - break; - case QFont::Black: - weightStr = "900"; - break; - default: - weightStr = "400"; - break; - } - - return QString("font-family: '%1'; font-size: %2sp; font-weight: %3; line-height: %4sp;") - .arg(font_.family()) - .arg(font_.pointSizeF()) - .arg(weightStr) - .arg(lineHeight_); -} - -QSize FontCardWidget::sizeHint() const { - QFontMetrics fm(font_); - QFont tokenFont("Segoe UI", 24); - QFontMetrics tokenFm(tokenFont); - - int tokenHeight = tokenFm.height(); - int propsHeight = fm.height() + 8; - int padding = 16 * 2; // Both sides - int spacing = 8; // Between elements - - int height = tokenHeight + spacing + fm.height() * 2 + spacing + propsHeight + padding; - int width = 200; // Minimum width - - return QSize(width, height); -} - -void FontCardWidget::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QRectF cardRect = rect().adjusted(4, 4, -4, -4); - - // Card background with shadow effect when hovered - if (isHovered_) { - QPainterPath path; - path.addRoundedRect(cardRect, 12, 12); - - // Shadow - QPainterPath shadowPath = path.translated(0, 2); - QColor shadowColor(0, 0, 0, 20); - painter.fillPath(shadowPath, shadowColor); - - // Border - QPen borderPen(QColor(100, 100, 255), 2); - painter.setPen(borderPen); - painter.drawPath(path); - } - - // Background fill - QPainterPath path; - path.addRoundedRect(cardRect, 12, 12); - painter.fillPath(path, QColor(250, 250, 250)); - - // Content padding - const int padding = 16; - QRectF contentRect = cardRect.adjusted(padding, padding, -padding, -padding); - - // Calculate heights dynamically - QFont tokenFont("Segoe UI", 24); - QFontMetrics tokenFm(tokenFont); - int tokenHeight = tokenFm.height(); - int tokenY = contentRect.top(); - - // Token name (small, gray) - painter.setPen(QColor(120, 120, 120)); - painter.setFont(tokenFont); - painter.drawText(contentRect.adjusted(0, tokenY, 0, 0), Qt::AlignLeft, tokenName_); - - // Font preview - calculate available space - QFontMetrics fm(font_); - int propsHeight = fm.height() + 8; // Props at bottom - int availableHeight = static_cast(contentRect.height()) - tokenHeight - propsHeight - 20; - int previewHeight = qMax(fm.height() * 2, availableHeight); - - QRectF previewRect(contentRect.left(), tokenY + tokenHeight + 8, contentRect.width(), - static_cast(previewHeight)); - - painter.setFont(font_); - painter.setPen(QColor(30, 30, 30)); - - // Draw preview text with elision - QString wrappedText = - fm.elidedText(previewText_, Qt::ElideRight, static_cast(previewRect.width())); - painter.drawText(previewRect, Qt::AlignLeft | Qt::AlignVCenter, wrappedText); - - // Font properties - fixed distance below preview - painter.setPen(QColor(100, 100, 100)); - QFont propsFont("Segoe UI", 9); - painter.setFont(propsFont); - - QString props = - QString("%1sp / %2sp / W%3").arg(font_.pointSizeF()).arg(lineHeight_).arg(font_.weight()); - - QRectF propsRect = contentRect; - propsRect.setBottom(cardRect.bottom() - 9); - painter.drawText(propsRect, Qt::AlignLeft | Qt::AlignTop, props); - - // "Click to copy" hint - if (isHovered_) { - painter.setPen(QColor(100, 100, 255)); - painter.drawText(propsRect, Qt::AlignRight | Qt::AlignTop, "点击复制 CSS"); - } -} - -void FontCardWidget::enterEvent(QEnterEvent* event) { - Q_UNUSED(event) - isHovered_ = true; - update(); -} - -void FontCardWidget::leaveEvent(QEvent* event) { - Q_UNUSED(event) - isHovered_ = false; - update(); -} - -void FontCardWidget::mousePressEvent(QMouseEvent* event) { - if (event->button() == Qt::LeftButton) { - emit clicked(cssStyle_); - } -} - -// ============================================================================= -// MaterialTypographyMainWindow Implementation -// ============================================================================= - -MaterialTypographyMainWindow::MaterialTypographyMainWindow(QWidget* parent) : QMainWindow(parent) { - // Create typography - typography_ = cf::ui::core::material::defaultTypography(); - - setupUI(); - createFontGroups(); -} - -MaterialTypographyMainWindow::~MaterialTypographyMainWindow() = default; - -void MaterialTypographyMainWindow::setupUI() { - // Central widget - centralWidget_ = new QWidget(this); - setCentralWidget(centralWidget_); - - // Main layout - mainLayout_ = new QVBoxLayout(centralWidget_); - mainLayout_->setContentsMargins(20, 20, 20, 20); - mainLayout_->setSpacing(16); - - // Header - createHeader(); - - // Scroll area for font cards - scrollArea_ = new QScrollArea(this); - scrollArea_->setWidgetResizable(true); - scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea_->setFrameShape(QFrame::NoFrame); - - scrollContent_ = new QWidget(); - scrollLayout_ = new QVBoxLayout(scrollContent_); - scrollLayout_->setContentsMargins(0, 0, 0, 0); - scrollLayout_->setSpacing(24); - - // Grid layout for font cards - fontGridLayout_ = new QGridLayout(); - fontGridLayout_->setSpacing(16); - scrollLayout_->addLayout(fontGridLayout_); - - // Add stretch at bottom - scrollLayout_->addStretch(); - - scrollArea_->setWidget(scrollContent_); - mainLayout_->addWidget(scrollArea_, 1); - - // Background color - QPalette pal = palette(); - pal.setColor(QPalette::Window, QColor(245, 245, 245)); - setPalette(pal); -} - -void MaterialTypographyMainWindow::createHeader() { - headerLayout_ = new QHBoxLayout(); - headerLayout_->setSpacing(16); - - // Title - titleLabel_ = new QLabel("Material Design 3 Type Scale", this); - QFont titleFont("Segoe UI", 24, QFont::Bold); - titleLabel_->setFont(titleFont); - titleLabel_->setStyleSheet("QLabel { color: #1C1B1F; }"); - - headerLayout_->addWidget(titleLabel_); - headerLayout_->addStretch(); - - mainLayout_->addLayout(headerLayout_); -} - -QString MaterialTypographyMainWindow::getPreviewText(const QString& token) const { - if (token.contains("Display")) { - return "Ag 你好世界 Display"; - } else if (token.contains("Headline")) { - return "标题文字 Headline 大标题"; - } else if (token.contains("Title")) { - return "标题文字 Title 标题"; - } else if (token.contains("Body")) { - return "正文内容 The quick brown fox"; - } else if (token.contains("Label")) { - return "标签 LABEL 按钮 Button"; - } - return "Preview Text 预览"; -} - -void MaterialTypographyMainWindow::createFontGroups() { - int row = 0; - createFontGroup("Display Styles", fontGridLayout_, row, DISPLAY_TOKENS); - createFontGroup("Headline Styles", fontGridLayout_, row, HEADLINE_TOKENS); - createFontGroup("Title Styles", fontGridLayout_, row, TITLE_TOKENS); - createFontGroup("Body Styles", fontGridLayout_, row, BODY_TOKENS); - createFontGroup("Label Styles", fontGridLayout_, row, LABEL_TOKENS); -} - -void MaterialTypographyMainWindow::createFontGroup(const QString& title, QGridLayout* layout, - int& row, const QStringList& tokens) { - // Section title label - QLabel* titleLabel = new QLabel(title); - QFont titleFont("Segoe UI", 14, QFont::DemiBold); - titleLabel->setFont(titleFont); - titleLabel->setStyleSheet("QLabel { color: #49454F; padding: 8px 0px; }"); - - layout->addWidget(titleLabel, row++, 0, 1, -1); - - // Calculate column count - int cols = calculateColumnCount(); - - // Create font cards - int col = 0; - for (const QString& token : tokens) { - QFont font = typography_.queryTargetFont(token.toUtf8().constData()); - float lineHeight = typography_.getLineHeight(token.toUtf8().constData()); - QString previewText = getPreviewText(token); - - FontCardWidget* card = new FontCardWidget(token, font, lineHeight, previewText); - - // Connect click signal - connect(card, &FontCardWidget::clicked, this, - &MaterialTypographyMainWindow::onFontCardClicked); - - // Store card info - fontCards_.append({card, token}); - - layout->addWidget(card, row, col); - - col++; - if (col >= cols) { - col = 0; - row++; - } - } - - if (col > 0) { - row++; - } - - // Add spacing after group - row++; -} - -int MaterialTypographyMainWindow::calculateColumnCount() const { - int width = scrollArea_->width(); - if (width < 800) - return 2; - if (width < 1200) - return 3; - return 4; -} - -void MaterialTypographyMainWindow::showToast(const QString& message) { - ToastWidget* toast = new ToastWidget(message, this); - toast->show(); -} - -void MaterialTypographyMainWindow::onFontCardClicked(const QString& cssStyle) { - // Copy to clipboard - QApplication::clipboard()->setText(cssStyle); - showToast("CSS 样式已复制到剪贴板"); -} - -} // namespace cf::ui::gallery diff --git a/example/gui/material/material_typography/MaterialTypographyMainWindow.h b/example/gui/material/material_typography/MaterialTypographyMainWindow.h deleted file mode 100644 index efec0fa0e..000000000 --- a/example/gui/material/material_typography/MaterialTypographyMainWindow.h +++ /dev/null @@ -1,154 +0,0 @@ -/** - * @file MaterialTypographyMainWindow.h - * @brief Material Design 3 Typography Gallery - Main Window - * - * A visual gallery to display all Material Design 3 Type Scale tokens - * with 15 font styles organized in 5 categories. - * - * @author CFDesktop Team - * @date 2026-02-26 - * @version 0.1 - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ui/core/material/cfmaterial_fonttype.h" - -namespace cf::ui::gallery { - -/** - * @brief Toast notification widget for displaying temporary messages. - * - * Shows a semi-transparent notification at the bottom of the window - * that automatically fades out after 2 seconds. - */ -class ToastWidget : public QWidget { - Q_OBJECT - public: - explicit ToastWidget(const QString& message, QWidget* parent = nullptr); - - void show(int durationMs = 2000); - - protected: - void paintEvent(QPaintEvent* event) override; - - private: - QString message_; -}; - -/** - * @brief Font card widget displaying a single typography token. - * - * Shows: - * - Font preview text - * - Token name (e.g., "md.typography.displayLarge") - * - Font properties (size, weight, line-height) - * - * Click to copy CSS style to clipboard. - */ -class FontCardWidget : public QWidget { - Q_OBJECT - public: - explicit FontCardWidget(const QString& tokenName, const QFont& font, float lineHeight, - const QString& previewText, QWidget* parent = nullptr); - - void updateFont(const QFont& font, float lineHeight); - - QSize sizeHint() const override; - - signals: - void clicked(const QString& cssStyle); - - protected: - void paintEvent(QPaintEvent* event) override; - void enterEvent(QEnterEvent* event) override; - void leaveEvent(QEvent* event) override; - void mousePressEvent(QMouseEvent* event) override; - - private: - QString generateCssStyle() const; - - QString tokenName_; - QFont font_; - float lineHeight_; - QString previewText_; - QString cssStyle_; - bool isHovered_ = false; -}; - -/** - * @brief Main window for the Material Typography Gallery. - * - * Displays all 15 Material Design 3 Type Scale tokens organized in groups: - * - Display Styles (3) - * - Headline Styles (3) - * - Title Styles (3) - * - Body Styles (3) - * - Label Styles (3) - */ -class MaterialTypographyMainWindow : public QMainWindow { - Q_OBJECT - - public: - explicit MaterialTypographyMainWindow(QWidget* parent = nullptr); - ~MaterialTypographyMainWindow() override; - - private: - void setupUI(); - void createHeader(); - void createFontGroups(); - int calculateColumnCount() const; - - void createFontGroup(const QString& title, QGridLayout* layout, int& row, - const QStringList& tokens); - - QString getPreviewText(const QString& token) const; - void showToast(const QString& message); - - private slots: - void onFontCardClicked(const QString& cssStyle); - - private: - cf::ui::core::MaterialTypography typography_; - - // UI components - QWidget* centralWidget_; - QVBoxLayout* mainLayout_; - QHBoxLayout* headerLayout_; - QScrollArea* scrollArea_; - QWidget* scrollContent_; - QVBoxLayout* scrollLayout_; - QGridLayout* fontGridLayout_; - - // Header components - QLabel* titleLabel_; - - // Toast - ToastWidget* toast_; - - // Font cards storage - struct FontCardInfo { - FontCardWidget* widget; - QString token; - }; - QList fontCards_; - - // 15 typography tokens grouped by category - static const QStringList DISPLAY_TOKENS; - static const QStringList HEADLINE_TOKENS; - static const QStringList TITLE_TOKENS; - static const QStringList BODY_TOKENS; - static const QStringList LABEL_TOKENS; -}; - -} // namespace cf::ui::gallery diff --git a/example/gui/material/material_typography/main.cpp b/example/gui/material/material_typography/main.cpp deleted file mode 100644 index e32889eb6..000000000 --- a/example/gui/material/material_typography/main.cpp +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @file main.cpp - * @brief Material Typography Gallery - Main Entry Point - * - * Entry point for the Material Design 3 Typography Gallery application. - * Displays all 15 Type Scale tokens organized in 5 categories. - * - * @author CFDesktop Team - * @date 2026-02-26 - * @version 0.1 - */ - -#include "MaterialTypographyMainWindow.h" - -#include - -int main(int argc, char* argv[]) { - QApplication app(argc, argv); - - // Set application metadata - app.setApplicationName("Material Typography Gallery"); - app.setApplicationVersion("0.1"); - app.setOrganizationName("CFDesktop"); - - // Create and show main window - cf::ui::gallery::MaterialTypographyMainWindow window; - window.resize(1200, 800); - window.show(); - - return app.exec(); -} diff --git a/example/gui/theme/CMakeLists.txt b/example/gui/theme/CMakeLists.txt deleted file mode 100644 index 191a59089..000000000 --- a/example/gui/theme/CMakeLists.txt +++ /dev/null @@ -1,40 +0,0 @@ -# Material Gallery Example -log_info("Material Gallery" "Building Material Gallery Example") - -add_executable(material_gallery - main.cpp - MaterialGalleryMainWindow.h - MaterialGalleryMainWindow.cpp - ThemeSidebar.h - ThemeSidebar.cpp - ThemePageWidget.h - ThemePageWidget.cpp - ToastWidget.h - ToastWidget.cpp - ColorSchemePage.h - ColorSchemePage.cpp - MotionSpecPage.h - MotionSpecPage.cpp - RadiusScalePage.h - RadiusScalePage.cpp - TypographyPage.h - TypographyPage.cpp -) - -target_link_libraries(material_gallery PRIVATE - Qt6::Widgets - Qt6::Core - Qt6::Gui - cfui -) - -# 设置输出目录到 examples/gui/ -cf_set_example_output_dir(material_gallery "gui") -cf_register_example_launcher(material_gallery "gui") - -# Windows 下设置为 GUI 应用(不显示控制台窗口) -if(WIN32) - set_target_properties(material_gallery PROPERTIES - WIN32_EXECUTABLE TRUE - ) -endif() diff --git a/example/gui/theme/ColorSchemePage.cpp b/example/gui/theme/ColorSchemePage.cpp deleted file mode 100644 index 2ea2a3779..000000000 --- a/example/gui/theme/ColorSchemePage.cpp +++ /dev/null @@ -1,400 +0,0 @@ -/** - * @file ColorSchemePage.cpp - * @brief Color Scheme page - Implementation - * - * @author CFDesktop Team - * @date 2026-02-28 - * @version 0.1 - */ - -#include "ColorSchemePage.h" -#include "ToastWidget.h" -#include "ui/core/material/cfmaterial_scheme.h" -#include "ui/core/material/material_factory.hpp" -#include "ui/core/theme.h" -#include "ui/core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -// Global namespace alias for token literals -namespace token_literals = ::cf::ui::core::token::literals; - -namespace cf::ui::gallery { - -// ============================================================================= -// Static Token Lists -// ============================================================================= - -const QStringList ColorSchemePage::PRIMARY_TOKENS = { - token_literals::PRIMARY, token_literals::ON_PRIMARY, token_literals::PRIMARY_CONTAINER, - token_literals::ON_PRIMARY_CONTAINER}; - -const QStringList ColorSchemePage::SECONDARY_TOKENS = { - token_literals::SECONDARY, token_literals::ON_SECONDARY, token_literals::SECONDARY_CONTAINER, - token_literals::ON_SECONDARY_CONTAINER}; - -const QStringList ColorSchemePage::TERTIARY_TOKENS = { - token_literals::TERTIARY, token_literals::ON_TERTIARY, token_literals::TERTIARY_CONTAINER, - token_literals::ON_TERTIARY_CONTAINER}; - -const QStringList ColorSchemePage::ERROR_TOKENS = {token_literals::ERROR, token_literals::ON_ERROR, - token_literals::ERROR_CONTAINER, - token_literals::ON_ERROR_CONTAINER}; - -const QStringList ColorSchemePage::SURFACE_TOKENS = { - token_literals::BACKGROUND, token_literals::ON_BACKGROUND, token_literals::SURFACE, - token_literals::ON_SURFACE, token_literals::SURFACE_VARIANT, token_literals::ON_SURFACE_VARIANT, - token_literals::OUTLINE, token_literals::OUTLINE_VARIANT}; - -const QStringList ColorSchemePage::UTILITY_TOKENS = { - token_literals::SHADOW, token_literals::SCRIM, token_literals::INVERSE_SURFACE, - token_literals::INVERSE_ON_SURFACE, token_literals::INVERSE_PRIMARY}; - -// ============================================================================= -// ColorCardWidget Implementation -// ============================================================================= - -ColorCardWidget::ColorCardWidget(const QString& tokenName, const QColor& color, - const QString& contrastInfo, QWidget* parent) - : QWidget(parent), tokenName_(tokenName), color_(color), contrastInfo_(contrastInfo) { - hexValue_ = color.name().toUpper(); - setMinimumSize(140, 160); - setMaximumSize(180, 200); - setCursor(Qt::PointingHandCursor); -} - -void ColorCardWidget::updateColor(const QColor& color, const QString& contrastInfo) { - color_ = color; - hexValue_ = color.name().toUpper(); - contrastInfo_ = contrastInfo; - update(); -} - -void ColorCardWidget::paintEvent(QPaintEvent*) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QRectF r = rect().adjusted(2, 2, -2, -2); - qreal radius = 12; - - // Card background with elevation - QPainterPath path; - path.addRoundedRect(r, radius, radius); - - // Shadow for elevation effect - if (isHovered_) { - painter.setPen(Qt::NoPen); - painter.setBrush(QColor(0, 0, 0, 20)); - painter.drawPath(path.translated(0, 2)); - } - - painter.setPen(QPen(QColor(200, 200, 200), 1)); - painter.setBrush(QColor(250, 250, 250)); - painter.drawPath(path); - - // Color swatch - qreal swatchSize = std::min(r.width(), r.height() * 0.35); - QRectF swatchRect(r.center().x() - swatchSize / 2, r.top() + 12, swatchSize, swatchSize); - - QPainterPath swatchPath; - swatchPath.addRoundedRect(swatchRect, 8, 8); - painter.setPen(Qt::NoPen); - painter.setBrush(color_); - painter.drawPath(swatchPath); - - // Token name - QFont nameFont("Segoe UI", 8, QFont::Medium); - painter.setFont(nameFont); - painter.setPen(QColor(60, 60, 60)); - QRectF nameRect = r.adjusted(8, swatchRect.bottom() + 8, -8, 0); - painter.drawText(nameRect, Qt::AlignTop | Qt::AlignHCenter, tokenName_); - - // Separator line - qreal lineY = nameRect.top() + QFontMetrics(nameFont).height() + 6; - QPen linePen(QColor(200, 200, 200), 1); - painter.setPen(linePen); - painter.drawLine(QPointF(r.left() + 16, lineY), QPointF(r.right() - 16, lineY)); - - // HEX value - QFont hexFont("Consolas", 9); - painter.setFont(hexFont); - painter.setPen(QColor(40, 40, 40)); - QRectF hexRect = r.adjusted(8, lineY + 4, -8, 0); - painter.drawText(hexRect, Qt::AlignTop | Qt::AlignHCenter, hexValue_); - - // Contrast info with color coding - QFont contrastFont("Segoe UI", 7); - painter.setFont(contrastFont); - - // Parse contrast value - float contrast = contrastInfo_.split(": ").last().toFloat(); - QColor contrastColor; - if (contrast < 3.0f) { - contrastColor = QColor(220, 50, 50); - } else if (contrast < 4.5f) { - contrastColor = QColor(220, 150, 50); - } else { - contrastColor = QColor(50, 180, 80); - } - - painter.setPen(contrastColor); - QRectF contrastRect = r.adjusted(8, hexRect.top() + QFontMetrics(hexFont).height() + 2, -8, -8); - painter.drawText(contrastRect, Qt::AlignTop | Qt::AlignHCenter, contrastInfo_); - - // Click hint on hover - if (isHovered_) { - QFont hintFont("Segoe UI", 7); - painter.setFont(hintFont); - painter.setPen(QColor(100, 100, 100)); - painter.drawText(r.adjusted(8, 0, -8, -4), Qt::AlignBottom | Qt::AlignHCenter, - "Click to copy"); - } -} - -void ColorCardWidget::enterEvent(QEnterEvent*) { - isHovered_ = true; - update(); -} - -void ColorCardWidget::leaveEvent(QEvent*) { - isHovered_ = false; - update(); -} - -void ColorCardWidget::mousePressEvent(QMouseEvent*) { - emit clicked(hexValue_); -} - -// ============================================================================= -// ColorSchemePage Implementation -// ============================================================================= - -ColorSchemePage::ColorSchemePage(QWidget* parent) : ThemePageWidget(parent) { - - // Default card colors (light theme) - cardBgColor_ = QColor(250, 250, 250); - cardBorderColor_ = QColor(220, 220, 220); - - setupUI(); - // Note: createColorGroups will be called after theme is set -} - -void ColorSchemePage::setupUI() { - auto* mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->setSpacing(0); - - // Scroll area for color cards - scrollArea_ = new QScrollArea(this); - scrollArea_->setWidgetResizable(true); - scrollArea_->setFrameShape(QFrame::NoFrame); - scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scrollContent_ = new QWidget(); - scrollLayout_ = new QVBoxLayout(scrollContent_); - scrollLayout_->setSpacing(24); - scrollLayout_->setContentsMargins(24, 24, 24, 24); - - colorGridLayout_ = new QGridLayout(); - colorGridLayout_->setSpacing(12); - colorGridLayout_->setContentsMargins(0, 0, 0, 0); - scrollLayout_->addLayout(colorGridLayout_); - scrollLayout_->addStretch(); - - scrollArea_->setWidget(scrollContent_); - mainLayout->addWidget(scrollArea_); - - // Toast - toast_ = new ToastWidget("", this); - toast_->hide(); -} - -void ColorSchemePage::createColorGroups() { - int row = 0; - - createColorGroup("Primary Colors 主色", colorGridLayout_, row, PRIMARY_TOKENS); - createColorGroup("Secondary Colors 副色", colorGridLayout_, row, SECONDARY_TOKENS); - createColorGroup("Tertiary Colors 第三色", colorGridLayout_, row, TERTIARY_TOKENS); - createColorGroup("Error Colors 错误色", colorGridLayout_, row, ERROR_TOKENS); - createColorGroup("Surface Colors 表面色", colorGridLayout_, row, SURFACE_TOKENS); - createColorGroup("Utility Colors 工具色", colorGridLayout_, row, UTILITY_TOKENS); -} - -void ColorSchemePage::createColorGroup(const QString& title, QGridLayout* layout, int& row, - const QStringList& tokens) { - // Get color scheme from theme - if (!theme_) - return; - auto& colorScheme = const_cast( - static_cast(theme_->color_scheme())); - - // Group title - QLabel* titleLabel = new QLabel(title, scrollContent_); - QFont titleFont("Segoe UI", 13, QFont::Bold); - titleLabel->setFont(titleFont); - titleLabel->setStyleSheet("QLabel { color: #49454F; padding: 4px 0px; }"); - layout->addWidget(titleLabel, row++, 0, 1, -1, Qt::AlignLeft); - - // Calculate column count - int col = 0; - int maxCols = 4; - layout->setRowStretch(row, 0); - - for (const QString& token : tokens) { - QColor color = colorScheme.queryColor(token.toStdString().c_str()); - - // Calculate contrast - QString pairToken = getContrastPair(token); - QColor pairColor = colorScheme.queryColor(pairToken.toStdString().c_str()); - float contrast = calculateContrastRatio(color, pairColor); - QString contrastInfo = QString("⚡ %1").arg(contrast, 0, 'f', 2); - - auto* card = new ColorCardWidget(token, color, contrastInfo, scrollContent_); - layout->addWidget(card, row, col++); - - ColorCardInfo info{card, token}; - colorCards_.append(info); - - connect(card, &ColorCardWidget::clicked, this, &ColorSchemePage::onColorCardClicked); - - if (col >= maxCols) { - col = 0; - row++; - } - } - - if (col > 0) { - row++; - } - - // Add spacing after group - row++; -} - -QString ColorSchemePage::getContrastPair(const QString& token) const { - static const QMap pairs = { - {"md.primary", "md.onPrimary"}, - {"md.onPrimary", "md.primary"}, - {"md.primaryContainer", "md.onPrimaryContainer"}, - {"md.onPrimaryContainer", "md.primaryContainer"}, - {"md.secondary", "md.onSecondary"}, - {"md.onSecondary", "md.secondary"}, - {"md.secondaryContainer", "md.onSecondaryContainer"}, - {"md.onSecondaryContainer", "md.secondaryContainer"}, - {"md.tertiary", "md.onTertiary"}, - {"md.onTertiary", "md.tertiary"}, - {"md.tertiaryContainer", "md.onTertiaryContainer"}, - {"md.onTertiaryContainer", "md.tertiaryContainer"}, - {"md.error", "md.onError"}, - {"md.onError", "md.error"}, - {"md.errorContainer", "md.onErrorContainer"}, - {"md.onErrorContainer", "md.errorContainer"}, - {"md.background", "md.onBackground"}, - {"md.onBackground", "md.background"}, - {"md.surface", "md.onSurface"}, - {"md.onSurface", "md.surface"}, - {"md.surfaceVariant", "md.onSurfaceVariant"}, - {"md.onSurfaceVariant", "md.surfaceVariant"}, - {"md.outline", "md.surface"}, - {"md.outlineVariant", "md.surface"}, - {"md.shadow", "md.surface"}, - {"md.scrim", "md.surface"}, - {"md.inverseSurface", "md.inverseOnSurface"}, - {"md.inverseOnSurface", "md.inverseSurface"}, - {"md.inversePrimary", "md.surface"}}; - return pairs.value(token, "md.surface"); -} - -float ColorSchemePage::calculateContrastRatio(const QColor& fg, const QColor& bg) const { - // WCAG 2.1 relative luminance calculation - auto luminance = [](const QColor& c) -> float { - auto toLinear = [](float v) -> float { - v /= 255.0f; - return v <= 0.03928f ? v / 12.92f : std::pow((v + 0.055f) / 1.055f, 2.4f); - }; - float r = toLinear(c.red()); - float g = toLinear(c.green()); - float b = toLinear(c.blue()); - return 0.2126f * r + 0.7152f * g + 0.0722f * b; - }; - - float l1 = luminance(fg); - float l2 = luminance(bg); - float lighter = std::max(l1, l2); - float darker = std::min(l1, l2); - return (lighter + 0.05f) / (darker + 0.05f); -} - -void ColorSchemePage::updateAllColors() { - if (!theme_) - return; - auto& colorScheme = const_cast( - static_cast(theme_->color_scheme())); - - for (auto& info : colorCards_) { - QColor color = colorScheme.queryColor(info.token.toStdString().c_str()); - QString pairToken = getContrastPair(info.token); - QColor pairColor = colorScheme.queryColor(pairToken.toStdString().c_str()); - float contrast = calculateContrastRatio(color, pairColor); - QString contrastInfo = QString("⚡ %1").arg(contrast, 0, 'f', 2); - info.widget->updateColor(color, contrastInfo); - } -} - -void ColorSchemePage::updateWindowTheme() { - if (!theme_) - return; - auto& colorScheme = const_cast( - static_cast(theme_->color_scheme())); - - // Update background colors - QColor bg = colorScheme.queryColor("md.background"); - QColor onSurface = colorScheme.queryColor("md.onSurface"); - - scrollContent_->setAutoFillBackground(true); - QPalette pal = scrollContent_->palette(); - pal.setColor(QPalette::Window, bg); - scrollContent_->setPalette(pal); -} - -void ColorSchemePage::applyTheme(const cf::ui::core::ICFTheme& theme) { - // Store theme pointer - theme_ = &theme; - - // Clear existing cards and recreate with new theme - colorCards_.clear(); - - // Clear layout items - this also deletes the widgets (Qt takes ownership) - while (colorGridLayout_->count()) { - QLayoutItem* item = colorGridLayout_->takeAt(0); - if (item->widget()) { - delete item->widget(); - } - delete item; - } - createColorGroups(); - - updateAllColors(); - updateWindowTheme(); -} - -void ColorSchemePage::onColorCardClicked(const QString& hexValue) { - QClipboard* clipboard = QGuiApplication::clipboard(); - clipboard->setText(hexValue); - showToast(QString("已复制: %1").arg(hexValue)); -} - -void ColorSchemePage::showToast(const QString& message) { - delete toast_; - toast_ = new ToastWidget(message, this); - toast_->show(); -} - -} // namespace cf::ui::gallery diff --git a/example/gui/theme/ColorSchemePage.h b/example/gui/theme/ColorSchemePage.h deleted file mode 100644 index 595976fc4..000000000 --- a/example/gui/theme/ColorSchemePage.h +++ /dev/null @@ -1,130 +0,0 @@ -/** - * @file ColorSchemePage.h - * @brief Color Scheme page for the Material Gallery - * - * @author CFDesktop Team - * @date 2026-02-28 - * @version 0.1 - */ - -#pragma once - -#include "ThemePageWidget.h" -#include "ToastWidget.h" -#include "ui/core/material/cfmaterial_scheme.h" -#include -#include -#include -#include - -namespace cf::ui::core { -struct ICFTheme; -} - -namespace cf::ui::gallery { - -/** - * @brief Color card widget displaying a single color token. - */ -class ColorCardWidget : public QWidget { - Q_OBJECT - public: - explicit ColorCardWidget(const QString& tokenName, const QColor& color, - const QString& contrastInfo, QWidget* parent = nullptr); - - void updateColor(const QColor& color, const QString& contrastInfo); - - signals: - void clicked(const QString& hexValue); - - protected: - void paintEvent(QPaintEvent* event) override; - void enterEvent(QEnterEvent* event) override; - void leaveEvent(QEvent* event) override; - void mousePressEvent(QMouseEvent* event) override; - - private: - QString tokenName_; - QColor color_; - QString hexValue_; - QString contrastInfo_; - bool isHovered_ = false; -}; - -/** - * @brief Color Scheme page - displays all 26 Material Design 3 color tokens. - * - * Organized in 6 groups: - * - Primary Colors (4) - * - Secondary Colors (4) - * - Tertiary Colors (4) - * - Error Colors (4) - * - Surface Colors (8) - * - Utility Colors (5) - */ -class ColorSchemePage : public ThemePageWidget { - Q_OBJECT - - public: - explicit ColorSchemePage(QWidget* parent = nullptr); - ~ColorSchemePage() override = default; - - QString pageTitle() const override { return "色彩方案"; } - void applyTheme(const cf::ui::core::ICFTheme& theme) override; - - private: - void setupUI(); - void createColorGroups(); - void updateAllColors(); - void updateWindowTheme(); - - // Color group creation helpers - void createColorGroup(const QString& title, QGridLayout* layout, int& row, - const QStringList& tokens); - - // Calculate WCAG contrast ratio - float calculateContrastRatio(const QColor& fg, const QColor& bg) const; - - // Get contrast pair for a token - QString getContrastPair(const QString& token) const; - - // Show toast notification - void showToast(const QString& message); - - private slots: - void onColorCardClicked(const QString& hexValue); - - private: - // Theme pointer for accessing color scheme - const cf::ui::core::ICFTheme* theme_ = nullptr; - - // UI components - QScrollArea* scrollArea_; - QWidget* scrollContent_; - QVBoxLayout* scrollLayout_; - QGridLayout* colorGridLayout_; - - // Toast - ToastWidget* toast_; - - // Color cards storage for theme updates - struct ColorCardInfo { - ColorCardWidget* widget; - QString token; - }; - QList colorCards_; - - // Theme colors for card background - QColor cardBgColor_; - QColor cardBorderColor_; - - // All 26 color tokens - static const QStringList PRIMARY_TOKENS; - static const QStringList SECONDARY_TOKENS; - static const QStringList TERTIARY_TOKENS; - static const QStringList ERROR_TOKENS; - static const QStringList SURFACE_TOKENS; - static const QStringList UTILITY_TOKENS; -}; - -} // namespace cf::ui::gallery diff --git a/example/gui/theme/MaterialGalleryMainWindow.cpp b/example/gui/theme/MaterialGalleryMainWindow.cpp deleted file mode 100644 index b60ff0ea3..000000000 --- a/example/gui/theme/MaterialGalleryMainWindow.cpp +++ /dev/null @@ -1,274 +0,0 @@ -/** - * @file MaterialGalleryMainWindow.cpp - * @brief Main window for the Material Design 3 Gallery - Implementation - * - * @author CFDesktop Team - * @date 2026-02-28 - * @version 0.1 - */ - -#include "MaterialGalleryMainWindow.h" -#include "ColorSchemePage.h" -#include "MotionSpecPage.h" -#include "RadiusScalePage.h" -#include "ThemePageWidget.h" -#include "ThemeSidebar.h" -#include "TypographyPage.h" - -#include "ui/core/material/cfmaterial_scheme.h" -#include "ui/core/material/material_factory.hpp" -#include "ui/core/material/material_factory_class.h" -#include "ui/core/theme_factory.h" -#include "ui/core/theme_manager.h" -#include "ui/core/token/theme_name/material_theme_name.h" - -#include -#include - -// Global namespace alias for token literals -namespace token_literals = ::cf::ui::core::token::literals; - -namespace cf::ui::gallery { - -// ============================================================================= -// ThemeSwitch Implementation -// ============================================================================= - -ThemeSwitch::ThemeSwitch(QWidget* parent) : QWidget(parent) { - setFixedSize(56, 28); - - animation_ = new QPropertyAnimation(this, "knobPosition", this); - animation_->setDuration(200); - animation_->setEasingCurve(QEasingCurve::OutCubic); -} - -void ThemeSwitch::setDark(bool dark) { - if (isDark_ != dark) { - isDark_ = dark; - float targetPos = dark ? 1.0f : 0.0f; - animation_->stop(); - animation_->setStartValue(knobPosition_); - animation_->setEndValue(targetPos); - animation_->start(); - emit themeChanged(isDark_); - } -} - -void ThemeSwitch::setKnobPosition(float pos) { - knobPosition_ = qBound(0.0f, pos, 1.0f); - update(); - emit knobPositionChanged(); -} - -void ThemeSwitch::paintEvent(QPaintEvent*) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QRectF r = rect(); - qreal radius = r.height() / 2; - - // Track background - QColor trackColor = isDark_ ? QColor(103, 80, 164) : QColor(180, 180, 180); - QPainterPath trackPath; - trackPath.addRoundedRect(r, radius, radius); - painter.setPen(Qt::NoPen); - painter.setBrush(trackColor); - painter.drawPath(trackPath); - - // Knob - qreal knobDiameter = r.height() - 6; - qreal knobX = 3 + knobPosition_ * (r.width() - knobDiameter - 6); - QRectF knobRect(knobX, 3, knobDiameter, knobDiameter); - - QPainterPath knobPath; - knobPath.addEllipse(knobRect); - - // Knob shadow - painter.setPen(Qt::NoPen); - painter.setBrush(QColor(0, 0, 0, 30)); - painter.drawEllipse(knobRect.translated(0, 1)); - - // Knob - painter.setBrush(Qt::white); - painter.drawPath(knobPath); - - // Icon on knob (sun or moon) - painter.setPen(isDark_ ? QColor(60, 60, 60) : QColor(255, 200, 50)); - QFont iconFont("Segoe UI Emoji", 11); - painter.setFont(iconFont); - QString icon = isDark_ ? "🌙" : "☀️"; - painter.drawText(knobRect, Qt::AlignCenter, icon); -} - -void ThemeSwitch::mousePressEvent(QMouseEvent*) { - setDark(!isDark_); -} - -// ============================================================================= -// MaterialGalleryMainWindow Implementation -// ============================================================================= - -MaterialGalleryMainWindow::MaterialGalleryMainWindow(QWidget* parent) : QMainWindow(parent) { - - setupThemeManager(); - setupUI(); - createHeader(); - - // Create pages and add to stacked widget - colorSchemePage_ = new ColorSchemePage(this); - motionSpecPage_ = new MotionSpecPage(this); - radiusScalePage_ = new RadiusScalePage(this); - typographyPage_ = new TypographyPage(this); - - contentStack_->addWidget(colorSchemePage_); - contentStack_->addWidget(motionSpecPage_); - contentStack_->addWidget(radiusScalePage_); - contentStack_->addWidget(typographyPage_); - - // Set initial page - contentStack_->setCurrentIndex(0); - - themeManager_->setThemeTo(token_literals::MATERIAL_THEME_LIGHT); - - // Apply initial theme - if (themeManager_) { - auto& theme = themeManager_->theme(token_literals::MATERIAL_THEME_LIGHT); - applyThemeToAllPages(theme); - sidebar_->applyTheme(theme); - } -} - -MaterialGalleryMainWindow::~MaterialGalleryMainWindow() { - // Cleanup is handled by Qt's parent-child system -} - -void MaterialGalleryMainWindow::setupThemeManager() { - themeManager_ = &core::ThemeManager::instance(); - - // Register MaterialFactory for both light and dark themes - auto installMaterialTheme = []() { return std::make_unique(); }; - // Register both theme names - the same factory handles both - themeManager_->insert_one(token_literals::MATERIAL_THEME_LIGHT, installMaterialTheme); - themeManager_->insert_one(token_literals::MATERIAL_THEME_DARK, installMaterialTheme); - - // Install main window for theme updates - themeManager_->install_widget(this); - - // Connect themeChanged signal - connect(themeManager_, &core::ThemeManager::themeChanged, this, - &MaterialGalleryMainWindow::onThemeManagerChanged); -} - -void MaterialGalleryMainWindow::setupUI() { - // Window setup - setWindowTitle("Material Design 3 Gallery"); - resize(1200, 800); - setMinimumSize(900, 600); - - // Central widget - centralWidget_ = new QWidget(this); - setCentralWidget(centralWidget_); - - // Main horizontal layout (sidebar + content) - mainLayout_ = new QHBoxLayout(centralWidget_); - mainLayout_->setContentsMargins(0, 0, 0, 0); - mainLayout_->setSpacing(0); - - // Sidebar - sidebar_ = new ThemeSidebar(this); - mainLayout_->addWidget(sidebar_); - - // Connect sidebar signal - connect(sidebar_, &ThemeSidebar::tabClicked, this, &MaterialGalleryMainWindow::onTabClicked); - - // Content area (vertical layout with header + stacked widget) - QWidget* contentWidget = new QWidget(this); - contentLayout_ = new QVBoxLayout(contentWidget); - contentLayout_->setContentsMargins(0, 0, 0, 0); - contentLayout_->setSpacing(0); - - mainLayout_->addWidget(contentWidget, 1); // Stretch factor 1 - - // Content stack for page switching - contentStack_ = new QStackedWidget(this); - contentLayout_->addWidget(contentStack_, 1); // Stretch factor 1 -} - -void MaterialGalleryMainWindow::createHeader() { - // Header layout - headerLayout_ = new QHBoxLayout(); - headerLayout_->setContentsMargins(20, 16, 20, 16); - headerLayout_->setSpacing(16); - contentLayout_->addLayout(headerLayout_); - - // Title - titleLabel_ = new QLabel("Material Design 3 Gallery", this); - QFont titleFont("Segoe UI", 16, QFont::Bold); - titleLabel_->setFont(titleFont); - titleLabel_->setStyleSheet("QLabel { color: #1C1B1F; }"); - headerLayout_->addWidget(titleLabel_); - headerLayout_->addStretch(); - - // Theme label - themeLabel_ = new QLabel("☀️ Light", this); - QFont themeFont("Segoe UI", 10); - themeLabel_->setFont(themeFont); - themeLabel_->setStyleSheet("QLabel { color: #49454F; }"); - headerLayout_->addWidget(themeLabel_); - - // Theme switch - themeSwitch_ = new ThemeSwitch(this); - headerLayout_->addWidget(themeSwitch_); - - connect(themeSwitch_, &ThemeSwitch::themeChanged, this, - &MaterialGalleryMainWindow::onThemeChanged); -} - -void MaterialGalleryMainWindow::applyThemeToAllPages(const core::ICFTheme& theme) { - colorSchemePage_->applyTheme(theme); - motionSpecPage_->applyTheme(theme); - radiusScalePage_->applyTheme(theme); - typographyPage_->applyTheme(theme); - sidebar_->applyTheme(theme); - - // Update header colors - auto& colorScheme = static_cast(theme.color_scheme()); - QColor onSurface = colorScheme.queryColor("md.onSurface"); - QColor bg = colorScheme.queryColor("md.background"); - - titleLabel_->setStyleSheet(QString("QLabel { color: %1; }").arg(onSurface.name())); - themeLabel_->setStyleSheet(QString("QLabel { color: %1; }").arg(onSurface.name())); - - // Update central widget background - centralWidget_->setAutoFillBackground(true); - QPalette pal = centralWidget_->palette(); - pal.setColor(QPalette::Window, bg); - centralWidget_->setPalette(pal); -} - -void MaterialGalleryMainWindow::onTabClicked(int index) { - contentStack_->setCurrentIndex(index); -} - -void MaterialGalleryMainWindow::onThemeChanged(bool isDark) { - isDarkTheme_ = isDark; - - // Update theme label - themeLabel_->setText(isDark ? "🌙 Dark" : "☀️ Light"); - - // Switch theme via ThemeManager - const char* themeName = - isDark ? token_literals::MATERIAL_THEME_DARK : token_literals::MATERIAL_THEME_LIGHT; - themeManager_->setThemeTo(themeName); -} - -void MaterialGalleryMainWindow::onThemeManagerChanged(const core::ICFTheme& theme) { - applyThemeToAllPages(theme); -} - -void MaterialGalleryMainWindow::resizeEvent(QResizeEvent* event) { - QMainWindow::resizeEvent(event); - // Handle responsive layout if needed -} - -} // namespace cf::ui::gallery diff --git a/example/gui/theme/MaterialGalleryMainWindow.h b/example/gui/theme/MaterialGalleryMainWindow.h deleted file mode 100644 index 233607b84..000000000 --- a/example/gui/theme/MaterialGalleryMainWindow.h +++ /dev/null @@ -1,124 +0,0 @@ -/** - * @file MaterialGalleryMainWindow.h - * @brief Main window for the Material Design 3 Gallery - * - * @author CFDesktop Team - * @date 2026-02-28 - * @version 0.1 - */ - -#pragma once - -#include -#include -#include -#include -#include -#include - -namespace cf::ui::core { -struct ICFTheme; -class ThemeManager; -} // namespace cf::ui::core - -namespace cf::ui::gallery { - -// Forward declarations -class ThemeSidebar; -class ThemePageWidget; -class ColorSchemePage; -class MotionSpecPage; -class RadiusScalePage; -class TypographyPage; - -/** - * @brief Theme toggle switch widget (header). - */ -class ThemeSwitch : public QWidget { - Q_OBJECT - Q_PROPERTY( - float knobPosition READ knobPosition WRITE setKnobPosition NOTIFY knobPositionChanged) - - public: - explicit ThemeSwitch(QWidget* parent = nullptr); - - bool isDark() const { return isDark_; } - void setDark(bool dark); - - float knobPosition() const { return knobPosition_; } - void setKnobPosition(float pos); - - signals: - void themeChanged(bool isDark); - void knobPositionChanged(); - - protected: - void paintEvent(QPaintEvent* event) override; - void mousePressEvent(QMouseEvent* event) override; - - private: - bool isDark_ = false; - float knobPosition_ = 0.0f; - QPropertyAnimation* animation_; -}; - -/** - * @brief Main window for the Material Design 3 Gallery. - * - * Features: - * - Left sidebar with 4 tab items (Color Scheme, Motion Spec, Radius Scale, Typography) - * - Content area with QStackedWidget for switching between pages - * - Theme toggle switch in header for light/dark theme switching - * - ThemeManager integration for global theme management - */ -class MaterialGalleryMainWindow : public QMainWindow { - Q_OBJECT - - public: - explicit MaterialGalleryMainWindow(QWidget* parent = nullptr); - ~MaterialGalleryMainWindow() override; - - protected: - void resizeEvent(QResizeEvent* event) override; - - private: - void setupUI(); - void createHeader(); - void setupThemeManager(); - void applyThemeToAllPages(const cf::ui::core::ICFTheme& theme); - - private slots: - void onTabClicked(int index); - void onThemeChanged(bool isDark); - void onThemeManagerChanged(const cf::ui::core::ICFTheme& theme); - - private: - // Theme Manager - cf::ui::core::ThemeManager* themeManager_ = nullptr; - bool isDarkTheme_ = false; - - // UI components - QWidget* centralWidget_; - QHBoxLayout* mainLayout_; - QVBoxLayout* contentLayout_; - QHBoxLayout* headerLayout_; - - // Sidebar - ThemeSidebar* sidebar_; - - // Header components - QLabel* titleLabel_; - QLabel* themeLabel_; - ThemeSwitch* themeSwitch_; - - // Content area - QStackedWidget* contentStack_; - - // Pages - ColorSchemePage* colorSchemePage_; - MotionSpecPage* motionSpecPage_; - RadiusScalePage* radiusScalePage_; - TypographyPage* typographyPage_; -}; - -} // namespace cf::ui::gallery diff --git a/example/gui/theme/MotionSpecPage.cpp b/example/gui/theme/MotionSpecPage.cpp deleted file mode 100644 index 6b1dd32c9..000000000 --- a/example/gui/theme/MotionSpecPage.cpp +++ /dev/null @@ -1,558 +0,0 @@ -/** - * @file MotionSpecPage.cpp - * @brief Motion Spec page - Implementation - * - * @author CFDesktop Team - * @date 2026-02-28 - * @version 0.1 - */ - -#include "MotionSpecPage.h" -#include "ui/core/material/cfmaterial_motion.h" -#include "ui/core/material/cfmaterial_scheme.h" -#include "ui/core/theme.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace { - -QString easingTypeToString(cf::ui::base::Easing::Type type) { - switch (type) { - case cf::ui::base::Easing::Type::Emphasized: - return "Emphasized"; - case cf::ui::base::Easing::Type::EmphasizedDecelerate: - return "EmphasizedDecelerate"; - case cf::ui::base::Easing::Type::EmphasizedAccelerate: - return "EmphasizedAccelerate"; - case cf::ui::base::Easing::Type::Standard: - return "Standard"; - case cf::ui::base::Easing::Type::StandardDecelerate: - return "StandardDecelerate"; - case cf::ui::base::Easing::Type::StandardAccelerate: - return "StandardAccelerate"; - case cf::ui::base::Easing::Type::Linear: - return "Linear"; - default: - return "Unknown"; - } -} - -} // namespace - -namespace cf::ui::gallery { - -// ============================================================================= -// MotionPreviewWidget Implementation -// ============================================================================= - -MotionPreviewWidget::MotionPreviewWidget(const core::MotionSpec& spec, const QString& name, - QWidget* parent) - : QWidget(parent), spec_(spec), name_(name) { - setMinimumHeight(100); - setMaximumHeight(160); - - timer_ = new QTimer(this); - connect(timer_, &QTimer::timeout, this, &MotionPreviewWidget::updateAnimation); - - resetAnimation(); -} - -void MotionPreviewWidget::setProgress(float progress) { - progress_ = qBound(0.0f, progress, 1.0f); - - float eased = calculateEasedProgress(progress_); - QRectF r = rect().adjusted(40, 30, -40, -30); - ballPosition_ = QPointF(r.left() + eased * r.width(), r.center().y()); - - update(); - emit progressChanged(); -} - -float MotionPreviewWidget::calculateEasedProgress(float linearProgress) const { - QEasingCurve curve = spec_.toEasingCurve(); - return curve.valueForProgress(linearProgress); -} - -void MotionPreviewWidget::startAnimation() { - isAnimating_ = true; - elapsed_ = 0.0f; - progress_ = 0.0f; - timer_->start(16); -} - -void MotionPreviewWidget::resetAnimation() { - isAnimating_ = false; - timer_->stop(); - progress_ = 0.0f; - elapsed_ = 0.0f; - ballPosition_ = QPointF(rect().left() + 40, rect().center().y()); - update(); -} - -void MotionPreviewWidget::updateAnimation() { - if (!isAnimating_) - return; - - elapsed_ += 16.0f; - - if (elapsed_ >= spec_.durationMs) { - elapsed_ = spec_.durationMs; - setProgress(1.0f); - isAnimating_ = false; - timer_->stop(); - emit animationFinished(); - } else { - float linearProgress = elapsed_ / spec_.durationMs; - setProgress(linearProgress); - } -} - -void MotionPreviewWidget::paintEvent(QPaintEvent*) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QRectF r = rect().adjusted(20, 20, -20, -20); - - // Background track - QColor trackColor = isDarkTheme_ ? QColor(60, 60, 60) : QColor(220, 220, 220); - QPainterPath trackPath; - trackPath.addRoundedRect(r, 8, 8); - painter.setPen(Qt::NoPen); - painter.setBrush(trackColor); - painter.drawPath(trackPath); - - // Progress fill - QEasingCurve curve = spec_.toEasingCurve(); - QColor progressColor; - switch (spec_.easing) { - case base::Easing::Type::Emphasized: - case base::Easing::Type::EmphasizedDecelerate: - progressColor = QColor(103, 80, 164); - break; - case base::Easing::Type::EmphasizedAccelerate: - progressColor = QColor(155, 93, 175); - break; - case base::Easing::Type::Standard: - case base::Easing::Type::StandardDecelerate: - case base::Easing::Type::StandardAccelerate: - progressColor = QColor(98, 91, 113); - break; - case base::Easing::Type::Linear: - progressColor = QColor(98, 91, 113); - break; - default: - progressColor = QColor(103, 80, 164); - break; - } - - float eased = calculateEasedProgress(progress_); - float fillWidth = r.width() * eased; - - if (fillWidth > 0) { - QRectF fillRect(r.left(), r.top(), fillWidth, r.height()); - QPainterPath fillPath; - fillPath.addRoundedRect(fillRect, 8, 8); - - QLinearGradient gradient(r.left(), 0, r.left() + fillWidth, 0); - gradient.setColorAt(0, progressColor); - gradient.setColorAt(1, progressColor.lighter(120)); - painter.setBrush(gradient); - painter.drawPath(fillPath); - } - - // Animated ball - float ballRadius = 10; - QPainterPath ballPath; - ballPath.addEllipse(ballPosition_, ballRadius, ballRadius); - - painter.setPen(Qt::NoPen); - painter.setBrush(QColor(0, 0, 0, 50)); - painter.drawEllipse(ballPosition_ + QPointF(0, 2), ballRadius, ballRadius); - - QRadialGradient ballGradient(ballPosition_, ballRadius); - ballGradient.setColorAt(0, QColor(255, 255, 255)); - ballGradient.setColorAt(1, QColor(220, 220, 220)); - painter.setBrush(ballGradient); - painter.drawPath(ballPath); - - // Time markers - QFont markerFont("Segoe UI", 8); - painter.setFont(markerFont); - QColor markerColor = isDarkTheme_ ? QColor(180, 180, 180) : QColor(100, 100, 100); - painter.setPen(markerColor); - - painter.drawText(QRectF(r.left(), r.bottom() + 5, 40, 20), Qt::AlignCenter, "0ms"); - painter.drawText(QRectF(r.right() - 40, r.bottom() + 5, 40, 20), Qt::AlignCenter, - QString("%1ms").arg(spec_.durationMs)); - - // Current time label - if (isAnimating_ || progress_ > 0) { - int currentTime = static_cast(elapsed_); - painter.drawText(QRectF(r.center().x() - 30, r.top() - 18, 60, 20), Qt::AlignCenter, - QString("%1ms").arg(currentTime)); - } -} - -void MotionPreviewWidget::resizeEvent(QResizeEvent* event) { - QWidget::resizeEvent(event); - resetAnimation(); -} - -void MotionPreviewWidget::updateSpec(const core::MotionSpec& spec) { - spec_ = spec; - resetAnimation(); -} - -// ============================================================================= -// MotionCardWidget Implementation -// ============================================================================= - -MotionCardWidget::MotionCardWidget(const core::MotionSpec& spec, const QString& name, - const QString& description, QWidget* parent) - : QWidget(parent), spec_(spec), name_(name), description_(description) { - setMinimumSize(240, 180); - setMaximumSize(280, 220); - setCursor(Qt::PointingHandCursor); - - backgroundColor_ = QColor(250, 250, 250); - surfaceColor_ = QColor(245, 245, 245); - onSurfaceColor_ = QColor(60, 60, 60); - - updateCurvePath(); -} - -void MotionCardWidget::updateCurvePath() { - curvePath_ = QPainterPath(); - QRectF curveRect(10, 10, 80, 40); - curvePath_.moveTo(curveRect.bottomLeft()); - - QEasingCurve curve = spec_.toEasingCurve(); - for (int i = 0; i <= 20; i++) { - float t = i / 20.0f; - float value = curve.valueForProgress(t); - QPointF point(curveRect.left() + t * curveRect.width(), - curveRect.bottom() - value * curveRect.height()); - curvePath_.lineTo(point); - } -} - -void MotionCardWidget::setThemeColors(const QColor& background, const QColor& surface, - const QColor& onSurface) { - backgroundColor_ = background; - surfaceColor_ = surface; - onSurfaceColor_ = onSurface; - update(); -} - -void MotionCardWidget::paintEvent(QPaintEvent*) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QRectF r = rect().adjusted(4, 4, -4, -4); - float radius = 16; - - // Card background with elevation - QPainterPath path; - path.addRoundedRect(r, radius, radius); - - if (isHovered_) { - painter.setPen(Qt::NoPen); - painter.setBrush(QColor(0, 0, 0, 15)); - painter.drawPath(path.translated(0, 3)); - } - - QColor bgColor = isHovered_ ? surfaceColor_.lighter(105) : surfaceColor_; - painter.setPen(QPen(QColor(200, 200, 200), 1)); - painter.setBrush(bgColor); - painter.drawPath(path); - - // Title - QFont titleFont("Segoe UI", 11, QFont::Bold); - painter.setFont(titleFont); - painter.setPen(onSurfaceColor_); - QRectF titleRect = r.adjusted(14, 14, -14, 0); - painter.drawText(titleRect, Qt::AlignLeft | Qt::AlignTop, name_); - - // Description - QFont descFont("Segoe UI", 8); - painter.setFont(descFont); - QColor descColor = onSurfaceColor_; - descColor.setAlpha(180); - painter.setPen(descColor); - QRectF descRect = titleRect.adjusted(0, QFontMetrics(titleFont).height() + 4, 0, 0); - painter.drawText(descRect, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, description_); - - // Spec info - QFont infoFont("Consolas", 9); - painter.setFont(infoFont); - QColor infoColor = QColor(103, 80, 164); - painter.setPen(infoColor); - - QString specText = - QString("⏱ %1ms • %2").arg(spec_.durationMs).arg(easingTypeToString(spec_.easing)); - - QRectF specRect = r.adjusted(14, 0, -14, -45); - painter.drawText(specRect, Qt::AlignLeft | Qt::AlignBottom, specText); - - // Easing curve preview - QRectF curveBox(r.right() - 90, r.top() + 14, 76, 44); - QPainterPath curveBoxPath; - curveBoxPath.addRoundedRect(curveBox, 8, 8); - - painter.setPen(Qt::NoPen); - painter.setBrush(isDarkTheme_ ? QColor(40, 40, 40) : QColor(235, 235, 235)); - painter.drawPath(curveBoxPath); - - painter.setPen(QPen(QColor(103, 80, 164), 2)); - painter.setBrush(Qt::NoBrush); - painter.drawPath(curvePath_); - - // Play button hint - if (isHovered_) { - QFont hintFont("Segoe UI", 8); - painter.setFont(hintFont); - painter.setPen(onSurfaceColor_); - painter.drawText(r.adjusted(14, 0, -14, -14), Qt::AlignBottom | Qt::AlignHCenter, - "▶ 点击预览动画"); - } -} - -void MotionCardWidget::enterEvent(QEnterEvent*) { - isHovered_ = true; - update(); -} - -void MotionCardWidget::leaveEvent(QEvent*) { - isHovered_ = false; - update(); -} - -void MotionCardWidget::mousePressEvent(QMouseEvent*) { - emit playRequested(spec_); -} - -// ============================================================================= -// MotionSpecPage Implementation -// ============================================================================= - -MotionSpecPage::MotionSpecPage(QWidget* parent) : ThemePageWidget(parent) { - // Initialize motion presets - motionPresets_ = { - {"Short Enter", "小元素入场 (按钮、图标)", core::MotionPresets::shortEnter()}, - {"Short Exit", "小元素离场", core::MotionPresets::shortExit()}, - {"Medium Enter", "中等元素入场 (卡片、列表)", core::MotionPresets::mediumEnter()}, - {"Medium Exit", "中等元素离场", core::MotionPresets::mediumExit()}, - {"Long Enter", "大元素入场 (对话框、页面)", core::MotionPresets::longEnter()}, - {"Long Exit", "大元素离场", core::MotionPresets::longExit()}, - {"State Change", "状态层动画 (hover、focus)", core::MotionPresets::stateChange()}, - {"Ripple Expand", "涟漪扩散动画", core::MotionPresets::rippleExpand()}, - {"Ripple Fade", "涟漪淡出动画", core::MotionPresets::rippleFade()}}; - - setupUI(); - createPreviewSection(); - createMotionCards(); -} - -void MotionSpecPage::setupUI() { - auto* mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->setSpacing(16); - - // Preview frame - previewFrame_ = new QFrame(this); - previewFrame_->setFrameStyle(QFrame::NoFrame); - previewLayout_ = new QVBoxLayout(previewFrame_); - previewLayout_->setContentsMargins(20, 16, 20, 16); - mainLayout->addWidget(previewFrame_); - - // Scroll area for motion cards - scrollArea_ = new QScrollArea(this); - scrollArea_->setWidgetResizable(true); - scrollArea_->setFrameShape(QFrame::NoFrame); - scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scrollContent_ = new QWidget(); - scrollLayout_ = new QVBoxLayout(scrollContent_); - scrollLayout_->setSpacing(16); - scrollLayout_->setContentsMargins(20, 0, 20, 20); - - cardsLayout_ = new QGridLayout(); - cardsLayout_->setSpacing(12); - cardsLayout_->setContentsMargins(0, 0, 0, 0); - scrollLayout_->addLayout(cardsLayout_); - scrollLayout_->addStretch(); - - scrollArea_->setWidget(scrollContent_); - mainLayout->addWidget(scrollArea_); -} - -void MotionSpecPage::createPreviewSection() { - // Preview title - previewLabel_ = new QLabel("动画预览区 - 点击下方卡片播放", this); - QFont previewTitleFont("Segoe UI", 13, QFont::Medium); - previewLabel_->setFont(previewTitleFont); - previewLabel_->setStyleSheet("QLabel { color: #49454F; }"); - previewLayout_->addWidget(previewLabel_); - - // Preview widget container - previewContainer_ = new QHBoxLayout(); - - // Preview widget - previewWidget_ = new MotionPreviewWidget(core::MotionPresets::shortEnter(), "shortEnter", this); - previewContainer_->addWidget(previewWidget_, 1); - - // Info panel - QVBoxLayout* infoPanel = new QVBoxLayout(); - - // Speed control - QLabel* speedLabel = new QLabel("播放速度:", this); - speedLabel->setStyleSheet("QLabel { color: #49454F; }"); - infoPanel->addWidget(speedLabel); - - speedCombo_ = new QComboBox(this); - speedCombo_->addItem("1x (正常)", 1); - speedCombo_->addItem("0.5x (慢放)", 2); - speedCombo_->addItem("0.25x (极慢)", 4); - speedCombo_->setCurrentIndex(0); - connect(speedCombo_, QOverload::of(&QComboBox::currentIndexChanged), this, - &MotionSpecPage::onSpeedChanged); - infoPanel->addWidget(speedCombo_); - - infoPanel->addStretch(); - - // Spec info - previewInfoLabel_ = new QLabel(this); - previewInfoLabel_->setWordWrap(true); - QFont infoFont("Consolas", 9); - previewInfoLabel_->setFont(infoFont); - previewInfoLabel_->setStyleSheet("QLabel { color: #49454F; }"); - infoPanel->addWidget(previewInfoLabel_); - - previewContainer_->addLayout(infoPanel, 1); - previewLayout_->addLayout(previewContainer_); - - // Update initial info - onPlayRequested(core::MotionPresets::shortEnter()); -} - -void MotionSpecPage::createMotionCards() { - int row = 0, col = 0; - int maxCols = 3; - - for (const auto& preset : motionPresets_) { - auto* card = - new MotionCardWidget(preset.spec, preset.name, preset.description, scrollContent_); - - cardsLayout_->addWidget(card, row, col); - motionCards_.append(card); - - connect(card, &MotionCardWidget::playRequested, this, &MotionSpecPage::onPlayRequested); - - col++; - if (col >= maxCols) { - col = 0; - row++; - } - } -} - -void MotionSpecPage::updateWindowTheme() { - // Background update is handled by applyTheme -} - -QString MotionSpecPage::easingTypeToString(base::Easing::Type type) const { - return ::easingTypeToString(type); -} - -void MotionSpecPage::onPlayRequested(const core::MotionSpec& spec) { - // Find preset name - QString presetName; - for (const auto& preset : motionPresets_) { - if (preset.spec.durationMs == spec.durationMs && preset.spec.easing == spec.easing) { - presetName = preset.name; - break; - } - } - - // Create a copy with adjusted duration for animation speed - int speedMultiplier = speedCombo_->currentData().toInt(); - core::MotionSpec adjustedSpec = spec; - adjustedSpec.durationMs = spec.durationMs * speedMultiplier; - - // Update the existing preview widget with new spec - if (previewWidget_) { - previewWidget_->setDarkTheme(isDarkTheme_); - previewWidget_->updateSpec(adjustedSpec); - } - - // Update info label - QString infoText = QString("动画名称: %1\n" - "持续时间: %2ms\n" - "缓动类型: %3\n" - "延迟: %4ms") - .arg(presetName) - .arg(adjustedSpec.durationMs) - .arg(easingTypeToString(spec.easing)) - .arg(spec.delayMs); - - previewInfoLabel_->setText(infoText); - - // Start animation - previewWidget_->startAnimation(); -} - -void MotionSpecPage::onAnimationFinished() { - // Could add replay functionality here -} - -void MotionSpecPage::onSpeedChanged(int index) { - Q_UNUSED(index); - // Speed will be applied on next play -} - -void MotionSpecPage::applyTheme(const core::ICFTheme& theme) { - auto& colorScheme = static_cast(theme.color_scheme()); - - QColor bg = colorScheme.queryColor("md.background"); - QColor surface = colorScheme.queryColor("md.surface"); - QColor onSurface = colorScheme.queryColor("md.onSurface"); - - // Update background - scrollContent_->setAutoFillBackground(true); - QPalette pal = scrollContent_->palette(); - pal.setColor(QPalette::Window, bg); - scrollContent_->setPalette(pal); - - previewLabel_->setStyleSheet(QString("QLabel { color: %1; }").arg(onSurface.name())); - previewInfoLabel_->setStyleSheet(QString("QLabel { color: %1; }").arg(onSurface.name())); - - // Update preview frame - previewFrame_->setStyleSheet( - QString("QFrame { background-color: %1; border-radius: 12px; border: 1px solid %2; }") - .arg(surface.name()) - .arg(colorScheme.queryColor("md.outlineVariant").name())); - - // Update all cards - for (auto* card : motionCards_) { - card->setThemeColors(bg, surface, onSurface); - } - - isDarkTheme_ = (bg.value() < 128); - previewWidget_->setDarkTheme(isDarkTheme_); - - for (auto* card : motionCards_) { - card->setDarkTheme(isDarkTheme_); - } -} - -} // namespace cf::ui::gallery diff --git a/example/gui/theme/MotionSpecPage.h b/example/gui/theme/MotionSpecPage.h deleted file mode 100644 index a427a947a..000000000 --- a/example/gui/theme/MotionSpecPage.h +++ /dev/null @@ -1,177 +0,0 @@ -/** - * @file MotionSpecPage.h - * @brief Motion Spec page for the Material Gallery - * - * @author CFDesktop Team - * @date 2026-02-28 - * @version 0.1 - */ - -#pragma once - -#include "ThemePageWidget.h" -#include "ui/base/easing.h" -#include "ui/core/material/cfmaterial_motion.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cf::ui::core { -struct ICFTheme; -} - -namespace cf::ui::gallery { - -/** - * @brief Animation preview widget. - */ -class MotionPreviewWidget : public QWidget { - Q_OBJECT - Q_PROPERTY(float progress READ progress WRITE setProgress NOTIFY progressChanged) - - public: - explicit MotionPreviewWidget(const cf::ui::core::MotionSpec& spec, const QString& name, - QWidget* parent = nullptr); - - float progress() const { return progress_; } - void setProgress(float progress); - - void startAnimation(); - void resetAnimation(); - void setDarkTheme(bool dark) { - isDarkTheme_ = dark; - update(); - } - void updateSpec(const cf::ui::core::MotionSpec& spec); - - signals: - void progressChanged(); - void animationFinished(); - - protected: - void paintEvent(QPaintEvent* event) override; - void resizeEvent(QResizeEvent* event) override; - - private: - void updateAnimation(); - float calculateEasedProgress(float linearProgress) const; - - cf::ui::core::MotionSpec spec_; - QString name_; - float progress_ = 0.0f; - float elapsed_ = 0.0f; - QTimer* timer_; - bool isAnimating_ = false; - bool isDarkTheme_ = false; - QPointF ballPosition_; -}; - -/** - * @brief Motion spec card widget. - */ -class MotionCardWidget : public QWidget { - Q_OBJECT - - public: - explicit MotionCardWidget(const cf::ui::core::MotionSpec& spec, const QString& name, - const QString& description, QWidget* parent = nullptr); - - void setThemeColors(const QColor& background, const QColor& surface, const QColor& onSurface); - void setDarkTheme(bool dark) { - isDarkTheme_ = dark; - update(); - } - - signals: - void playRequested(const cf::ui::core::MotionSpec& spec); - - protected: - void paintEvent(QPaintEvent* event) override; - void enterEvent(QEnterEvent* event) override; - void leaveEvent(QEvent* event) override; - void mousePressEvent(QMouseEvent* event) override; - - private: - void updateCurvePath(); - - cf::ui::core::MotionSpec spec_; - QString name_; - QString description_; - bool isHovered_ = false; - bool isDarkTheme_ = false; - - // Theme colors - QColor backgroundColor_; - QColor surfaceColor_; - QColor onSurfaceColor_; - - QPainterPath curvePath_; -}; - -/** - * @brief Motion Spec page - displays all 9 Material Design 3 motion presets. - */ -class MotionSpecPage : public ThemePageWidget { - Q_OBJECT - - public: - explicit MotionSpecPage(QWidget* parent = nullptr); - ~MotionSpecPage() override = default; - - QString pageTitle() const override { return "动效规范"; } - void applyTheme(const cf::ui::core::ICFTheme& theme) override; - - private: - void setupUI(); - void createPreviewSection(); - void createMotionCards(); - void updateWindowTheme(); - - QString easingTypeToString(cf::ui::base::Easing::Type type) const; - - private slots: - void onPlayRequested(const cf::ui::core::MotionSpec& spec); - void onAnimationFinished(); - void onSpeedChanged(int index); - - private: - // UI components - QScrollArea* scrollArea_; - QWidget* scrollContent_; - QVBoxLayout* scrollLayout_; - QGridLayout* cardsLayout_; - QFrame* previewFrame_; - QVBoxLayout* previewLayout_; - - // Preview section - QLabel* previewLabel_; - QHBoxLayout* previewContainer_; - MotionPreviewWidget* previewWidget_; - QLabel* previewInfoLabel_; - QComboBox* speedCombo_; - - // Theme - bool isDarkTheme_ = false; - - // All 9 motion presets info - struct MotionPresetInfo { - QString name; - QString description; - cf::ui::core::MotionSpec spec; - }; - QList motionPresets_; - QList motionCards_; - - // Current animation - QPropertyAnimation* currentAnimation_ = nullptr; -}; - -} // namespace cf::ui::gallery diff --git a/example/gui/theme/RadiusScalePage.cpp b/example/gui/theme/RadiusScalePage.cpp deleted file mode 100644 index 67f87a88a..000000000 --- a/example/gui/theme/RadiusScalePage.cpp +++ /dev/null @@ -1,217 +0,0 @@ -/** - * @file RadiusScalePage.cpp - * @brief Radius Scale page - Implementation - * - * @author CFDesktop Team - * @date 2026-02-28 - * @version 0.1 - */ - -#include "RadiusScalePage.h" -#include "ui/core/material/cfmaterial_radius_scale.h" -#include "ui/core/material/cfmaterial_scheme.h" -#include "ui/core/material/material_factory.hpp" -#include "ui/core/theme.h" -#include "ui/core/token/radius_scale/cfmaterial_radius_scale_literals.h" - -#include -#include -#include - -// Global namespace alias for token literals -namespace token_literals = ::cf::ui::core::token::literals; - -namespace cf::ui::gallery { - -// ============================================================================= -// RadiusPreviewWidget Implementation -// ============================================================================= - -RadiusPreviewWidget::RadiusPreviewWidget(const QString& name, float radiusDp, - const QColor& accentColor, QWidget* parent) - : QWidget(parent), name_(name), radiusDp_(radiusDp), accentColor_(accentColor) { - setMinimumSize(180, 160); -} - -void RadiusPreviewWidget::paintEvent(QPaintEvent*) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QRectF r = rect().adjusted(10, 10, -10, -10); - - // Card background - QPainterPath cardPath; - cardPath.addRoundedRect(r, 12, 12); - painter.setPen(QPen(QColor(200, 200, 200), 1)); - painter.setBrush(QColor(250, 250, 250)); - painter.drawPath(cardPath); - - // Radius preview area - QRectF previewRect = r.adjusted(20, 45, -20, -45); - - // Calculate px from dp (1dp = 1px for preview) - float radiusPx = radiusDp_; - - QPainterPath previewPath; - previewPath.addRoundedRect(previewRect, radiusPx, radiusPx); - - // Draw rounded rectangle with accent color - painter.setPen(Qt::NoPen); - painter.setBrush(accentColor_); - painter.drawPath(previewPath); - - // Title - QFont nameFont("Segoe UI", 11, QFont::Bold); - painter.setFont(nameFont); - painter.setPen(QColor(60, 60, 60)); - painter.drawText(QRectF(r.left(), r.top() + 8, r.width(), 25), Qt::AlignCenter, name_); - - // dp value label - QFont dpFont("Consolas", 10); - painter.setFont(dpFont); - painter.setPen(QColor(100, 100, 100)); - QString dpText = QString("%1 dp").arg(radiusDp_, 0, 'f', 0); - painter.drawText(QRectF(previewRect.left(), previewRect.bottom() + 6, previewRect.width(), 20), - Qt::AlignCenter, dpText); -} - -// ============================================================================= -// RadiusScalePage Implementation -// ============================================================================= - -RadiusScalePage::RadiusScalePage(QWidget* parent) : ThemePageWidget(parent) { - // Default colors (light theme) - cardBgColor_ = QColor(250, 250, 250); - cardBorderColor_ = QColor(200, 200, 200); - textColor_ = QColor(60, 60, 60); - - setupUI(); - // Note: createRadiusCards will be called after theme is set -} - -void RadiusScalePage::setupUI() { - auto* mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->setSpacing(0); - - // Scroll area for cards - scrollArea_ = new QScrollArea(this); - scrollArea_->setWidgetResizable(true); - scrollArea_->setFrameShape(QFrame::NoFrame); - scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scrollContent_ = new QWidget(); - auto* scrollLayout = new QVBoxLayout(scrollContent_); - scrollLayout->setSpacing(16); - scrollLayout->setContentsMargins(24, 24, 24, 24); - - cardsLayout_ = new QGridLayout(); - cardsLayout_->setSpacing(12); - cardsLayout_->setContentsMargins(0, 0, 0, 0); - scrollLayout->addLayout(cardsLayout_); - scrollLayout->addStretch(); - - scrollArea_->setWidget(scrollContent_); - mainLayout->addWidget(scrollArea_); -} - -void RadiusScalePage::createRadiusCards() { - if (!theme_) - return; - auto& radiusScale = const_cast( - static_cast(theme_->radius_scale())); - auto& colorScheme = const_cast( - static_cast(theme_->color_scheme())); - - struct RadiusInfo { - const char* token; - const char* name; - QColor color; - }; - - static const RadiusInfo radii[] = { - {token_literals::CORNER_NONE, "Corner None", QColor(100, 100, 100)}, - {token_literals::CORNER_EXTRA_SMALL, "Corner Extra Small", QColor(103, 80, 164)}, - {token_literals::CORNER_SMALL, "Corner Small", QColor(103, 80, 164)}, - {token_literals::CORNER_MEDIUM, "Corner Medium", QColor(103, 80, 164)}, - {token_literals::CORNER_LARGE, "Corner Large", QColor(103, 80, 164)}, - {token_literals::CORNER_EXTRA_LARGE, "Corner Extra Large", QColor(103, 80, 164)}, - {token_literals::CORNER_EXTRA_EXTRA_LARGE, "Corner Extra Extra Large", - QColor(103, 80, 164)}}; - - int row = 0, col = 0; - int maxCols = 4; - - for (const auto& info : radii) { - float radius = radiusScale.queryRadiusScale(info.token); - auto* card = new RadiusPreviewWidget(info.name, radius, info.color, scrollContent_); - cardsLayout_->addWidget(card, row, col++); - - if (col >= maxCols) { - col = 0; - row++; - } - } -} - -void RadiusScalePage::applyTheme(const cf::ui::core::ICFTheme& theme) { - // Store theme pointer - theme_ = &theme; - - auto& colorScheme = const_cast( - static_cast(theme.color_scheme())); - auto& radiusScale = const_cast( - static_cast(theme.radius_scale())); - - // Update colors - QColor bg = colorScheme.queryColor("md.background"); - QColor onSurface = colorScheme.queryColor("md.onSurface"); - QColor primary = colorScheme.queryColor("md.primary"); - - // Update background - scrollContent_->setAutoFillBackground(true); - QPalette pal = scrollContent_->palette(); - pal.setColor(QPalette::Window, bg); - scrollContent_->setPalette(pal); - - // Recreate cards with new theme - // Clear existing layout - while (cardsLayout_->count()) { - auto* item = cardsLayout_->takeAt(0); - if (item->widget()) { - delete item->widget(); - } - delete item; - } - - // Recreate cards - struct RadiusInfo { - const char* token; - const char* name; - }; - - static const RadiusInfo radii[] = { - {token_literals::CORNER_NONE, "Corner None"}, - {token_literals::CORNER_EXTRA_SMALL, "Corner Extra Small"}, - {token_literals::CORNER_SMALL, "Corner Small"}, - {token_literals::CORNER_MEDIUM, "Corner Medium"}, - {token_literals::CORNER_LARGE, "Corner Large"}, - {token_literals::CORNER_EXTRA_LARGE, "Corner Extra Large"}, - {token_literals::CORNER_EXTRA_EXTRA_LARGE, "Corner Extra Extra Large"}}; - - int row = 0, col = 0; - int maxCols = 4; - - for (const auto& info : radii) { - float radius = radiusScale.queryRadiusScale(info.token); - auto* card = new RadiusPreviewWidget(info.name, radius, primary, scrollContent_); - cardsLayout_->addWidget(card, row, col++); - - if (col >= maxCols) { - col = 0; - row++; - } - } -} - -} // namespace cf::ui::gallery diff --git a/example/gui/theme/RadiusScalePage.h b/example/gui/theme/RadiusScalePage.h deleted file mode 100644 index ebd67eae0..000000000 --- a/example/gui/theme/RadiusScalePage.h +++ /dev/null @@ -1,73 +0,0 @@ -/** - * @file RadiusScalePage.h - * @brief Radius Scale page for the Material Gallery - * - * @author CFDesktop Team - * @date 2026-02-28 - * @version 0.1 - */ - -#pragma once - -#include "ThemePageWidget.h" -#include "ui/core/material/cfmaterial_radius_scale.h" -#include -#include -#include - -namespace cf::ui::core { -struct ICFTheme; -} - -namespace cf::ui::gallery { - -/** - * @brief Corner radius preview card widget. - */ -class RadiusPreviewWidget : public QWidget { - Q_OBJECT - public: - explicit RadiusPreviewWidget(const QString& name, float radiusDp, const QColor& accentColor, - QWidget* parent = nullptr); - - protected: - void paintEvent(QPaintEvent* event) override; - - private: - QString name_; - float radiusDp_; - QColor accentColor_; -}; - -/** - * @brief Radius Scale page - displays all 7 Material Design 3 corner radius levels. - */ -class RadiusScalePage : public ThemePageWidget { - Q_OBJECT - - public: - explicit RadiusScalePage(QWidget* parent = nullptr); - ~RadiusScalePage() override = default; - - QString pageTitle() const override { return "圆角规范"; } - void applyTheme(const cf::ui::core::ICFTheme& theme) override; - - private: - void setupUI(); - void createRadiusCards(); - - private: - const cf::ui::core::ICFTheme* theme_ = nullptr; - - // UI components - QScrollArea* scrollArea_; - QWidget* scrollContent_; - QGridLayout* cardsLayout_; - - // Theme colors - QColor cardBgColor_; - QColor cardBorderColor_; - QColor textColor_; -}; - -} // namespace cf::ui::gallery diff --git a/example/gui/theme/ThemePageWidget.cpp b/example/gui/theme/ThemePageWidget.cpp deleted file mode 100644 index 42077e269..000000000 --- a/example/gui/theme/ThemePageWidget.cpp +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @file ThemePageWidget.cpp - * @brief Base class for all theme gallery pages - Implementation - * - * @author CFDesktop Team - * @date 2026-02-28 - * @version 0.1 - */ - -#include "ThemePageWidget.h" - -namespace cf::ui::gallery { - -ThemePageWidget::ThemePageWidget(QWidget* parent) : QWidget(parent) {} - -} // namespace cf::ui::gallery diff --git a/example/gui/theme/ThemePageWidget.h b/example/gui/theme/ThemePageWidget.h deleted file mode 100644 index acefb81bb..000000000 --- a/example/gui/theme/ThemePageWidget.h +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @file ThemePageWidget.h - * @brief Base class for all theme gallery pages - * - * @author CFDesktop Team - * @date 2026-02-28 - * @version 0.1 - */ - -#pragma once - -#include - -namespace cf::ui::core { -struct ICFTheme; -} - -namespace cf::ui::gallery { - -/** - * @brief Base class for all content pages in the Material Gallery. - * - * Provides a common interface for all pages that can be switched via the sidebar. - * Each page is responsible for displaying its own content and responding to theme changes. - */ -class ThemePageWidget : public QWidget { - Q_OBJECT - - public: - explicit ThemePageWidget(QWidget* parent = nullptr); - ~ThemePageWidget() override = default; - - /** - * @brief Get the display title of this page - * @return Page title for display purposes - */ - virtual QString pageTitle() const = 0; - - /** - * @brief Apply a new theme to this page - * @param theme The theme to apply - */ - virtual void applyTheme(const cf::ui::core::ICFTheme& theme) = 0; -}; - -} // namespace cf::ui::gallery diff --git a/example/gui/theme/ThemeSidebar.cpp b/example/gui/theme/ThemeSidebar.cpp deleted file mode 100644 index 5b4faa165..000000000 --- a/example/gui/theme/ThemeSidebar.cpp +++ /dev/null @@ -1,176 +0,0 @@ -/** - * @file ThemeSidebar.cpp - * @brief Left sidebar with tab items for navigation - Implementation - * - * @author CFDesktop Team - * @date 2026-02-28 - * @version 0.1 - */ - -#include "ThemeSidebar.h" -#include "ui/core/theme.h" -#include -#include -#include -#include - -namespace cf::ui::gallery { - -ThemeSidebar::ThemeSidebar(QWidget* parent) : QWidget(parent) { - setMinimumWidth(200); - setMaximumWidth(280); - - // Initialize tabs with icons + text - tabs_ = {{"🎨", "色彩方案", "Color Scheme"}, - {"⚡", "动效规范", "Motion Spec"}, - {"🔲", "圆角规范", "Radius Scale"}, - {"🔤", "字体规范", "Typography"}}; - - // Default colors (light theme) - backgroundColor_ = QColor(245, 245, 245); - activeColor_ = QColor(103, 80, 164); - hoverColor_ = QColor(230, 230, 235); - textColor_ = QColor(60, 60, 60); - iconColor_ = QColor(60, 60, 60); - borderColor_ = QColor(220, 220, 220); - - setMouseTracking(true); -} - -void ThemeSidebar::setActiveIndex(int index) { - if (index >= 0 && index < tabs_.size() && index != activeIndex_) { - activeIndex_ = index; - update(); - } -} - -void ThemeSidebar::applyTheme(const cf::ui::core::ICFTheme& theme) { - auto& colorScheme = theme.color_scheme(); - - // Get colors from theme - backgroundColor_ = colorScheme.queryColor("md.surface"); - auto primaryColor = colorScheme.queryColor("md.primary"); - auto onSurfaceColor = colorScheme.queryColor("md.onSurface"); - auto surfaceVariantColor = colorScheme.queryColor("md.surfaceVariant"); - - activeColor_ = primaryColor; - hoverColor_ = surfaceVariantColor; - textColor_ = onSurfaceColor; - iconColor_ = onSurfaceColor; - borderColor_ = colorScheme.queryColor("md.outlineVariant"); - - update(); -} - -void ThemeSidebar::paintEvent(QPaintEvent*) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QRectF r = rect(); - - // Background - painter.fillRect(r, backgroundColor_); - - // Border line on right side - painter.setPen(QPen(borderColor_, 1)); - painter.drawLine(r.right() - 1, r.top(), r.right() - 1, r.bottom()); - - // Draw tabs - for (int i = 0; i < tabs_.size(); ++i) { - QRect tabRect = this->tabRect(i); - bool isActive = (i == activeIndex_); - bool isHovered = (i == hoverIndex_); - - // Tab background - if (isActive) { - // Active indicator on left edge - QPainterPath indicatorPath; - qreal indicatorWidth = 4; - indicatorPath.addRoundedRect( - QRectF(tabRect.left(), tabRect.top() + 8, indicatorWidth, tabRect.height() - 16), 2, - 2); - painter.fillPath(indicatorPath, activeColor_); - - // Active background - QPainterPath bgPath; - bgPath.addRoundedRect(tabRect.adjusted(4, 4, -4, -4), 8, 8); - painter.fillPath(bgPath, hoverColor_); - } else if (isHovered) { - QPainterPath bgPath; - bgPath.addRoundedRect(tabRect.adjusted(4, 4, -4, -4), 8, 8); - painter.fillPath(bgPath, hoverColor_); - } - - // Icon and text - const TabItem& tab = tabs_[i]; - - // Icon - QFont iconFont("Segoe UI Emoji", 20); - painter.setFont(iconFont); - painter.setPen(iconColor_); - - QRectF iconRect(tabRect.left() + 20, tabRect.center().y() - 12, 32, 32); - painter.drawText(iconRect, Qt::AlignCenter, tab.icon); - - // Text label - QFont textFont("Segoe UI", 11, isActive ? QFont::Bold : QFont::Normal); - painter.setFont(textFont); - painter.setPen(isActive ? activeColor_ : textColor_); - - QRectF textRect(iconRect.right() + 12, tabRect.top() + 10, - tabRect.width() - iconRect.width() - 40, tabRect.height() - 20); - painter.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, tab.label); - } -} - -void ThemeSidebar::mousePressEvent(QMouseEvent* event) { - int index = tabAtPosition(event->pos()); - if (index >= 0) { - setActiveIndex(index); - emit tabClicked(index); - } -} - -void ThemeSidebar::enterEvent(QEnterEvent*) { - // Track mouse for hover effects -} - -void ThemeSidebar::leaveEvent(QEvent*) { - if (hoverIndex_ >= 0) { - hoverIndex_ = -1; - update(); - } -} - -void ThemeSidebar::mouseMoveEvent(QMouseEvent* event) { - int index = tabAtPosition(event->pos()); - if (index != hoverIndex_) { - hoverIndex_ = index; - update(); - } -} - -int ThemeSidebar::tabAtPosition(const QPoint& pos) const { - for (int i = 0; i < tabs_.size(); ++i) { - if (tabRect(i).contains(pos)) { - return i; - } - } - return -1; -} - -QRect ThemeSidebar::tabRect(int index) const { - if (index < 0 || index >= tabs_.size()) { - return QRect(); - } - - int tabHeight = 60; - int topMargin = 20; - int spacing = 8; - - int y = topMargin + index * (tabHeight + spacing); - - return QRect(0, y, width(), tabHeight); -} - -} // namespace cf::ui::gallery diff --git a/example/gui/theme/ThemeSidebar.h b/example/gui/theme/ThemeSidebar.h deleted file mode 100644 index c70337206..000000000 --- a/example/gui/theme/ThemeSidebar.h +++ /dev/null @@ -1,105 +0,0 @@ -/** - * @file ThemeSidebar.h - * @brief Left sidebar with tab items for navigation - * - * @author CFDesktop Team - * @date 2026-02-28 - * @version 0.1 - */ - -#pragma once - -#include -#include - -namespace cf::ui::core { -struct ICFTheme; -} - -namespace cf::ui::gallery { - -/** - * @brief Left sidebar navigation widget with clickable tab items. - * - * Displays 4 tab items with emoji icons + text labels: - * - 🎨 Color Scheme (色彩方案) - * - ⚡ Motion Spec (动效规范) - * - 🔲 Radius Scale (圆角规范) - * - 🔤 Typography (字体规范) - * - * Provides visual feedback for active/hover states. - */ -class ThemeSidebar : public QWidget { - Q_OBJECT - - public: - explicit ThemeSidebar(QWidget* parent = nullptr); - ~ThemeSidebar() override = default; - - /** - * @brief Set the active tab index - * @param index Tab index (0-3) - */ - void setActiveIndex(int index); - - /** - * @brief Get the current active tab index - * @return Active tab index - */ - int activeIndex() const { return activeIndex_; } - - /** - * @brief Apply theme colors to the sidebar - * @param theme Theme to apply - */ - void applyTheme(const cf::ui::core::ICFTheme& theme); - - signals: - /** - * @brief Emitted when a tab is clicked - * @param index The clicked tab index - */ - void tabClicked(int index); - - protected: - void paintEvent(QPaintEvent* event) override; - void mousePressEvent(QMouseEvent* event) override; - void enterEvent(QEnterEvent* event) override; - void leaveEvent(QEvent* event) override; - void mouseMoveEvent(QMouseEvent* event) override; - - private: - struct TabItem { - QString icon; // Emoji icon - QString label; // Text label (Chinese) - QString labelEn; // English label (optional) - }; - - /** - * @brief Get the tab item at a given position - * @param pos Position in widget coordinates - * @return Tab index, or -1 if not on a tab - */ - int tabAtPosition(const QPoint& pos) const; - - /** - * @brief Get the bounding rect for a tab item - * @param index Tab index - * @return Rectangle for the tab - */ - QRect tabRect(int index) const; - - QList tabs_; - int activeIndex_ = 0; - int hoverIndex_ = -1; - - // Theme colors - QColor backgroundColor_; - QColor activeColor_; - QColor hoverColor_; - QColor textColor_; - QColor iconColor_; - QColor borderColor_; -}; - -} // namespace cf::ui::gallery diff --git a/example/gui/theme/ToastWidget.cpp b/example/gui/theme/ToastWidget.cpp deleted file mode 100644 index 6951d7f06..000000000 --- a/example/gui/theme/ToastWidget.cpp +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @file ToastWidget.cpp - * @brief Toast notification widget implementation - * - * @author CFDesktop Team - * @date 2026-02-28 - * @version 0.1 - */ - -#include "ToastWidget.h" - -#include -#include -#include -#include -#include -#include - -namespace cf::ui::gallery { - -ToastWidget::ToastWidget(const QString& message, QWidget* parent) - : QWidget(parent), message_(message) { - setAttribute(Qt::WA_TransparentForMouseEvents, false); - setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); - setAttribute(Qt::WA_TranslucentBackground); -} - -void ToastWidget::show(int durationMs) { - QFont font("Segoe UI", 10); - QFontMetrics fm(font); - int padding = 20; - int textWidth = fm.horizontalAdvance(message_); - int width = textWidth + padding * 2; - int height = fm.height() + padding * 2; - - setFixedSize(width, height); - - if (parentWidget()) { - QPoint pos = parentWidget()->mapToGlobal(parentWidget()->rect().bottomLeft()); - int x = pos.x() + (parentWidget()->width() - width) / 2; - int y = pos.y() - height - 20; - move(x, y); - } - - QWidget::show(); - - QTimer::singleShot(durationMs, this, [this]() { - QPropertyAnimation* anim = new QPropertyAnimation(this, "windowOpacity"); - anim->setDuration(300); - anim->setStartValue(1.0); - anim->setEndValue(0.0); - connect(anim, &QPropertyAnimation::finished, this, [this]() { - hide(); - deleteLater(); - }); - anim->start(QPropertyAnimation::DeleteWhenStopped); - }); -} - -void ToastWidget::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QRectF r = rect(); - - // Background with rounded corners - QPainterPath bgPath; - bgPath.addRoundedRect(r, 8, 8); - painter.setPen(Qt::NoPen); - painter.setBrush(QColor(60, 60, 60, 230)); - painter.drawPath(bgPath); - - // Text - QFont font("Segoe UI", 10); - painter.setFont(font); - painter.setPen(Qt::white); - painter.drawText(r, Qt::AlignCenter, message_); -} - -} // namespace cf::ui::gallery diff --git a/example/gui/theme/ToastWidget.h b/example/gui/theme/ToastWidget.h deleted file mode 100644 index 35915f934..000000000 --- a/example/gui/theme/ToastWidget.h +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @file ToastWidget.h - * @brief Toast notification widget for displaying temporary messages. - * - * @author CFDesktop Team - * @date 2026-02-28 - * @version 0.1 - */ - -#pragma once - -#include -#include - -namespace cf::ui::gallery { - -/** - * @brief Toast notification widget for displaying temporary messages. - */ -class ToastWidget : public QWidget { - Q_OBJECT - public: - explicit ToastWidget(const QString& message, QWidget* parent = nullptr); - void show(int durationMs = 2000); - - protected: - void paintEvent(QPaintEvent* event) override; - - private: - QString message_; -}; - -} // namespace cf::ui::gallery diff --git a/example/gui/theme/TypographyPage.cpp b/example/gui/theme/TypographyPage.cpp deleted file mode 100644 index 2df681e52..000000000 --- a/example/gui/theme/TypographyPage.cpp +++ /dev/null @@ -1,420 +0,0 @@ -/** - * @file TypographyPage.cpp - * @brief Typography page - Implementation - * - * @author CFDesktop Team - * @date 2026-02-28 - * @version 0.1 - */ - -#include "TypographyPage.h" -#include "ToastWidget.h" -#include "ui/core/material/cfmaterial_fonttype.h" -#include "ui/core/material/cfmaterial_scheme.h" -#include "ui/core/material/material_factory.hpp" -#include "ui/core/theme.h" -#include "ui/core/token/typography/cfmaterial_typography_token_literals.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// Global namespace alias for token literals -namespace token_literals = ::cf::ui::core::token::literals; - -namespace cf::ui::gallery { - -// ============================================================================= -// Static Token Lists -// ============================================================================= - -const QStringList TypographyPage::DISPLAY_TOKENS = {token_literals::TYPOGRAPHY_DISPLAY_LARGE, - token_literals::TYPOGRAPHY_DISPLAY_MEDIUM, - token_literals::TYPOGRAPHY_DISPLAY_SMALL}; - -const QStringList TypographyPage::HEADLINE_TOKENS = {token_literals::TYPOGRAPHY_HEADLINE_LARGE, - token_literals::TYPOGRAPHY_HEADLINE_MEDIUM, - token_literals::TYPOGRAPHY_HEADLINE_SMALL}; - -const QStringList TypographyPage::TITLE_TOKENS = {token_literals::TYPOGRAPHY_TITLE_LARGE, - token_literals::TYPOGRAPHY_TITLE_MEDIUM, - token_literals::TYPOGRAPHY_TITLE_SMALL}; - -const QStringList TypographyPage::BODY_TOKENS = {token_literals::TYPOGRAPHY_BODY_LARGE, - token_literals::TYPOGRAPHY_BODY_MEDIUM, - token_literals::TYPOGRAPHY_BODY_SMALL}; - -const QStringList TypographyPage::LABEL_TOKENS = {token_literals::TYPOGRAPHY_LABEL_LARGE, - token_literals::TYPOGRAPHY_LABEL_MEDIUM, - token_literals::TYPOGRAPHY_LABEL_SMALL}; - -// ============================================================================= -// FontCardWidget Implementation -// ============================================================================= - -FontCardWidget::FontCardWidget(const QString& tokenName, const QFont& font, float lineHeight, - const QString& previewText, QWidget* parent) - : QWidget(parent), tokenName_(tokenName), font_(font), lineHeight_(lineHeight), - previewText_(previewText) { - cssStyle_ = generateCssStyle(); - setCursor(Qt::PointingHandCursor); - - QFontMetrics fm(font_); - int minHeight = qMax(100, fm.height() * 2 + 50); - setMinimumHeight(minHeight); -} - -void FontCardWidget::updateFont(const QFont& font, float lineHeight) { - font_ = font; - lineHeight_ = lineHeight; - cssStyle_ = generateCssStyle(); - update(); -} - -QString FontCardWidget::generateCssStyle() const { - QString weightStr; - switch (font_.weight()) { - case QFont::Thin: - weightStr = "100"; - break; - case QFont::ExtraLight: - weightStr = "200"; - break; - case QFont::Light: - weightStr = "300"; - break; - case QFont::Normal: - weightStr = "400"; - break; - case QFont::Medium: - weightStr = "500"; - break; - case QFont::DemiBold: - weightStr = "600"; - break; - case QFont::Bold: - weightStr = "700"; - break; - case QFont::ExtraBold: - weightStr = "800"; - break; - case QFont::Black: - weightStr = "900"; - break; - default: - weightStr = "400"; - break; - } - - return QString("font-family: '%1'; font-size: %2sp; font-weight: %3; line-height: %4sp;") - .arg(font_.family()) - .arg(font_.pointSizeF()) - .arg(weightStr) - .arg(lineHeight_); -} - -QSize FontCardWidget::sizeHint() const { - QFontMetrics fm(font_); - QFont tokenFont("Segoe UI", 20); - QFontMetrics tokenFm(tokenFont); - - int tokenHeight = tokenFm.height(); - int propsHeight = fm.height() + 8; - int padding = 14 * 2; - int spacing = 8; - - int height = tokenHeight + spacing + fm.height() * 2 + spacing + propsHeight + padding; - int width = 180; - - return QSize(width, height); -} - -void FontCardWidget::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QRectF cardRect = rect().adjusted(4, 4, -4, -4); - - // Card background with shadow effect when hovered - if (isHovered_) { - QPainterPath path; - path.addRoundedRect(cardRect, 12, 12); - - QPainterPath shadowPath = path.translated(0, 2); - QColor shadowColor(0, 0, 0, 20); - painter.fillPath(shadowPath, shadowColor); - - QPen borderPen(QColor(100, 100, 255), 2); - painter.setPen(borderPen); - painter.drawPath(path); - } - - // Background fill - QPainterPath path; - path.addRoundedRect(cardRect, 12, 12); - painter.fillPath(path, QColor(250, 250, 250)); - - // Content padding - const int padding = 14; - QRectF contentRect = cardRect.adjusted(padding, padding, -padding, -padding); - - // Calculate heights - QFont tokenFont("Segoe UI", 20); - QFontMetrics tokenFm(tokenFont); - int tokenHeight = tokenFm.height(); - int tokenY = contentRect.top(); - - // Token name - painter.setPen(QColor(120, 120, 120)); - painter.setFont(tokenFont); - painter.drawText(contentRect.adjusted(0, tokenY, 0, 0), Qt::AlignLeft, tokenName_); - - // Font preview - QFontMetrics fm(font_); - int propsHeight = fm.height() + 8; - int availableHeight = static_cast(contentRect.height()) - tokenHeight - propsHeight - 15; - int previewHeight = qMax(fm.height() * 2, availableHeight); - - QRectF previewRect(contentRect.left(), tokenY + tokenHeight + 6, contentRect.width(), - static_cast(previewHeight)); - - painter.setFont(font_); - painter.setPen(QColor(30, 30, 30)); - - QString wrappedText = - fm.elidedText(previewText_, Qt::ElideRight, static_cast(previewRect.width())); - painter.drawText(previewRect, Qt::AlignLeft | Qt::AlignVCenter, wrappedText); - - // Font properties - painter.setPen(QColor(100, 100, 100)); - QFont propsFont("Segoe UI", 8); - painter.setFont(propsFont); - - QString props = - QString("%1sp / %2sp / W%3").arg(font_.pointSizeF()).arg(lineHeight_).arg(font_.weight()); - - QRectF propsRect = contentRect; - propsRect.setBottom(cardRect.bottom() - 7); - painter.drawText(propsRect, Qt::AlignLeft | Qt::AlignTop, props); - - // "Click to copy" hint - if (isHovered_) { - painter.setPen(QColor(100, 100, 255)); - painter.drawText(propsRect, Qt::AlignRight | Qt::AlignTop, "点击复制 CSS"); - } -} - -void FontCardWidget::enterEvent(QEnterEvent* event) { - Q_UNUSED(event) - isHovered_ = true; - update(); -} - -void FontCardWidget::leaveEvent(QEvent* event) { - Q_UNUSED(event) - isHovered_ = false; - update(); -} - -void FontCardWidget::mousePressEvent(QMouseEvent* event) { - if (event->button() == Qt::LeftButton) { - emit clicked(cssStyle_); - } -} - -// ============================================================================= -// TypographyPage Implementation -// ============================================================================= - -TypographyPage::TypographyPage(QWidget* parent) : ThemePageWidget(parent) { - // Default colors (light theme) - cardBgColor_ = QColor(250, 250, 250); - cardBorderColor_ = QColor(200, 200, 200); - textColor_ = QColor(60, 60, 60); - - setupUI(); - // Note: createFontGroups will be called after theme is set -} - -void TypographyPage::setupUI() { - auto* mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->setSpacing(0); - - // Scroll area for font cards - scrollArea_ = new QScrollArea(this); - scrollArea_->setWidgetResizable(true); - scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea_->setFrameShape(QFrame::NoFrame); - - scrollContent_ = new QWidget(); - scrollLayout_ = new QVBoxLayout(scrollContent_); - scrollLayout_->setContentsMargins(0, 0, 0, 0); - scrollLayout_->setSpacing(20); - - // Grid layout for font cards - fontGridLayout_ = new QGridLayout(); - fontGridLayout_->setSpacing(12); - fontGridLayout_->setContentsMargins(24, 24, 24, 24); - scrollLayout_->addLayout(fontGridLayout_); - - // Add stretch at bottom - scrollLayout_->addStretch(); - - scrollArea_->setWidget(scrollContent_); - mainLayout->addWidget(scrollArea_, 1); - - // Background color - QPalette pal = palette(); - pal.setColor(QPalette::Window, QColor(245, 245, 245)); - setPalette(pal); - - // Toast - toast_ = new ToastWidget("", this); - toast_->hide(); -} - -QString TypographyPage::getPreviewText(const QString& token) const { - if (token.contains("Display")) { - return "Ag 你好世界 Display"; - } else if (token.contains("Headline")) { - return "标题文字 Headline 大标题"; - } else if (token.contains("Title")) { - return "标题文字 Title 标题"; - } else if (token.contains("Body")) { - return "正文内容 The quick brown fox"; - } else if (token.contains("Label")) { - return "标签 LABEL 按钮 Button"; - } - return "Preview Text 预览"; -} - -void TypographyPage::createFontGroups() { - int row = 0; - createFontGroup("Display Styles 展示样式", fontGridLayout_, row, DISPLAY_TOKENS); - createFontGroup("Headline Styles 标题样式", fontGridLayout_, row, HEADLINE_TOKENS); - createFontGroup("Title Styles 小标题样式", fontGridLayout_, row, TITLE_TOKENS); - createFontGroup("Body Styles 正文样式", fontGridLayout_, row, BODY_TOKENS); - createFontGroup("Label Styles 标签样式", fontGridLayout_, row, LABEL_TOKENS); -} - -void TypographyPage::createFontGroup(const QString& title, QGridLayout* layout, int& row, - const QStringList& tokens) { - // Get typography from theme - if (!theme_) - return; - auto& typography = const_cast( - static_cast(theme_->font_type())); - - // Section title label - QLabel* titleLabel = new QLabel(title); - QFont titleFont("Segoe UI", 13, QFont::DemiBold); - titleLabel->setFont(titleFont); - titleLabel->setStyleSheet("QLabel { color: #49454F; padding: 4px 0px; }"); - - layout->addWidget(titleLabel, row++, 0, 1, -1); - - // Calculate column count - int cols = calculateColumnCount(); - - // Create font cards - int col = 0; - for (const QString& token : tokens) { - QFont font = typography.queryTargetFont(token.toUtf8().constData()); - float lineHeight = typography.getLineHeight(token.toUtf8().constData()); - QString previewText = getPreviewText(token); - - FontCardWidget* card = new FontCardWidget(token, font, lineHeight, previewText); - - // Connect click signal - connect(card, &FontCardWidget::clicked, this, &TypographyPage::onFontCardClicked); - - // Store card info - fontCards_.append({card, token}); - - layout->addWidget(card, row, col); - - col++; - if (col >= cols) { - col = 0; - row++; - } - } - - if (col > 0) { - row++; - } - - // Add spacing after group - row++; -} - -int TypographyPage::calculateColumnCount() const { - int width = scrollArea_->width(); - if (width < 800) - return 2; - if (width < 1200) - return 3; - return 4; -} - -void TypographyPage::showToast(const QString& message) { - ToastWidget* toast = new ToastWidget(message, this); - toast->show(); -} - -void TypographyPage::onFontCardClicked(const QString& cssStyle) { - QApplication::clipboard()->setText(cssStyle); - showToast("CSS 样式已复制到剪贴板"); -} - -void TypographyPage::applyTheme(const cf::ui::core::ICFTheme& theme) { - auto& colorScheme = static_cast(theme.color_scheme()); - - // Store theme pointer - theme_ = &theme; - - // Update colors - QColor bg = colorScheme.queryColor("md.background"); - QColor onSurface = colorScheme.queryColor("md.onSurface"); - - // Update background - scrollContent_->setAutoFillBackground(true); - QPalette pal = scrollContent_->palette(); - pal.setColor(QPalette::Window, bg); - scrollContent_->setPalette(pal); - - // Clear existing cards and recreate with new theme - for (auto& info : fontCards_) { - delete info.widget; - } - fontCards_.clear(); - // Clear layout items - while (fontGridLayout_->count()) { - QLayoutItem* item = fontGridLayout_->takeAt(0); - if (item->widget()) { - item->widget()->setParent(nullptr); - } - delete item; - } - createFontGroups(); - - // Update font cards with new typography - auto& typography = const_cast( - static_cast(theme.font_type())); - for (auto& info : fontCards_) { - QFont font = typography.queryTargetFont(info.token.toUtf8().constData()); - float lineHeight = typography.getLineHeight(info.token.toUtf8().constData()); - info.widget->updateFont(font, lineHeight); - } -} - -} // namespace cf::ui::gallery diff --git a/example/gui/theme/TypographyPage.h b/example/gui/theme/TypographyPage.h deleted file mode 100644 index a4ab58ca5..000000000 --- a/example/gui/theme/TypographyPage.h +++ /dev/null @@ -1,124 +0,0 @@ -/** - * @file TypographyPage.h - * @brief Typography page for the Material Gallery - * - * @author CFDesktop Team - * @date 2026-02-28 - * @version 0.1 - */ - -#pragma once - -#include "ThemePageWidget.h" -#include "ToastWidget.h" -#include "ui/core/material/cfmaterial_fonttype.h" -#include -#include -#include -#include - -namespace cf::ui::core { -struct ICFTheme; -} - -namespace cf::ui::gallery { - -/** - * @brief Font card widget displaying a single typography token. - */ -class FontCardWidget : public QWidget { - Q_OBJECT - public: - explicit FontCardWidget(const QString& tokenName, const QFont& font, float lineHeight, - const QString& previewText, QWidget* parent = nullptr); - - void updateFont(const QFont& font, float lineHeight); - QSize sizeHint() const override; - - signals: - void clicked(const QString& cssStyle); - - protected: - void paintEvent(QPaintEvent* event) override; - void enterEvent(QEnterEvent* event) override; - void leaveEvent(QEvent* event) override; - void mousePressEvent(QMouseEvent* event) override; - - private: - QString generateCssStyle() const; - - QString tokenName_; - QFont font_; - float lineHeight_; - QString previewText_; - QString cssStyle_; - bool isHovered_ = false; -}; - -/** - * @brief Typography page - displays all 15 Material Design 3 typography tokens. - * - * Organized in 5 categories: - * - Display Styles (3) - * - Headline Styles (3) - * - Title Styles (3) - * - Body Styles (3) - * - Label Styles (3) - */ -class TypographyPage : public ThemePageWidget { - Q_OBJECT - - public: - explicit TypographyPage(QWidget* parent = nullptr); - ~TypographyPage() override = default; - - QString pageTitle() const override { return "字体规范"; } - void applyTheme(const cf::ui::core::ICFTheme& theme) override; - - private: - void setupUI(); - void createFontGroups(); - int calculateColumnCount() const; - - void createFontGroup(const QString& title, QGridLayout* layout, int& row, - const QStringList& tokens); - - QString getPreviewText(const QString& token) const; - void showToast(const QString& message); - - private slots: - void onFontCardClicked(const QString& cssStyle); - - private: - const cf::ui::core::ICFTheme* theme_ = nullptr; - - // UI components - QScrollArea* scrollArea_; - QWidget* scrollContent_; - QVBoxLayout* scrollLayout_; - QGridLayout* fontGridLayout_; - - // Toast - ToastWidget* toast_; - - // Font cards storage - struct FontCardInfo { - FontCardWidget* widget; - QString token; - }; - QList fontCards_; - - // 15 typography tokens grouped by category - static const QStringList DISPLAY_TOKENS; - static const QStringList HEADLINE_TOKENS; - static const QStringList TITLE_TOKENS; - static const QStringList BODY_TOKENS; - static const QStringList LABEL_TOKENS; - - // Theme colors - QColor cardBgColor_; - QColor cardBorderColor_; - QColor textColor_; -}; - -} // namespace cf::ui::gallery diff --git a/example/gui/theme/main.cpp b/example/gui/theme/main.cpp deleted file mode 100644 index 20261a766..000000000 --- a/example/gui/theme/main.cpp +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @file main.cpp - * @brief Material Design 3 Gallery - Main Entry Point - * - * @author CFDesktop Team - * @date 2026-02-28 - * @version 0.1 - */ - -#include "MaterialGalleryMainWindow.h" - -#include - -int main(int argc, char* argv[]) { - QApplication app(argc, argv); - - // Set application metadata - app.setApplicationName("Material Design 3 Gallery"); - app.setApplicationVersion("0.1"); - app.setOrganizationName("CFDesktop"); - - // Create and show main window - cf::ui::gallery::MaterialGalleryMainWindow mainWindow; - mainWindow.show(); - - return app.exec(); -} diff --git a/example/ui/CMakeLists.txt b/example/ui/CMakeLists.txt deleted file mode 100644 index f322769cc..000000000 --- a/example/ui/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -log_info("Example UI" "Managing UI Widget Examples") - -add_subdirectory(widget) diff --git a/example/ui/widget/CMakeLists.txt b/example/ui/widget/CMakeLists.txt deleted file mode 100644 index 403edc0f1..000000000 --- a/example/ui/widget/CMakeLists.txt +++ /dev/null @@ -1,2 +0,0 @@ -log_info("Example UI Widget" "Managing Widget Examples") -add_subdirectory(material_example) diff --git a/example/ui/widget/material_example/CMakeLists.txt b/example/ui/widget/material_example/CMakeLists.txt deleted file mode 100644 index 28804b171..000000000 --- a/example/ui/widget/material_example/CMakeLists.txt +++ /dev/null @@ -1,62 +0,0 @@ -log_info("Material Widget Gallery" "Building Material Widget Gallery") - -add_executable(material_widget_gallery - main.cpp - MaterialGalleryWindow.h - MaterialGalleryWindow.cpp - demos/ButtonDemo.cpp - demos/ButtonDemo.h - demos/CheckBoxDemo.cpp - demos/CheckBoxDemo.h - demos/ComboBoxDemo.cpp - demos/ComboBoxDemo.h - demos/GroupBoxDemo.cpp - demos/GroupBoxDemo.h - demos/LabelDemo.cpp - demos/LabelDemo.h - demos/ProgressBarDemo.cpp - demos/ProgressBarDemo.h - demos/RadioButtonDemo.cpp - demos/RadioButtonDemo.h - demos/TextAreaDemo.cpp - demos/TextAreaDemo.h - demos/TextFieldDemo.cpp - demos/TextFieldDemo.h - demos/SliderDemo.cpp - demos/SliderDemo.h - demos/SwitchDemo.cpp - demos/SwitchDemo.h - demos/SpinBoxDemo.cpp - demos/SpinBoxDemo.h - demos/DoubleSpinBoxDemo.cpp - demos/DoubleSpinBoxDemo.h - demos/SeparatorDemo.cpp - demos/SeparatorDemo.h - demos/ScrollViewDemo.cpp - demos/ScrollViewDemo.h - demos/ListViewDemo.cpp - demos/ListViewDemo.h - demos/TableViewDemo.cpp - demos/TableViewDemo.h - demos/TabViewDemo.cpp - demos/TabViewDemo.h - demos/TreeViewDemo.cpp - demos/TreeViewDemo.h -) - -target_link_libraries(material_widget_gallery PRIVATE - Qt6::Widgets - Qt6::Core - Qt6::Gui - cfui -) - -# 设置输出目录到 examples/ui/ -cf_set_example_output_dir(material_widget_gallery "ui") -cf_register_example_launcher(material_widget_gallery "ui") - -if(WIN32) - set_target_properties(material_widget_gallery PROPERTIES - WIN32_EXECUTABLE TRUE - ) -endif() diff --git a/example/ui/widget/material_example/MaterialGalleryWindow.cpp b/example/ui/widget/material_example/MaterialGalleryWindow.cpp deleted file mode 100644 index 74f619f9b..000000000 --- a/example/ui/widget/material_example/MaterialGalleryWindow.cpp +++ /dev/null @@ -1,185 +0,0 @@ -/** - * @file MaterialGalleryWindow.cpp - * @brief Material Design 3 Widget Gallery Main Window Implementation - */ - -#include "MaterialGalleryWindow.h" - -// Demo includes -#include "demos/ButtonDemo.h" -#include "demos/CheckBoxDemo.h" -#include "demos/ComboBoxDemo.h" -#include "demos/DoubleSpinBoxDemo.h" -#include "demos/GroupBoxDemo.h" -#include "demos/LabelDemo.h" -#include "demos/ListViewDemo.h" -#include "demos/ProgressBarDemo.h" -#include "demos/RadioButtonDemo.h" -#include "demos/ScrollViewDemo.h" -#include "demos/SeparatorDemo.h" -#include "demos/SliderDemo.h" -#include "demos/SpinBoxDemo.h" -#include "demos/SwitchDemo.h" -#include "demos/TabViewDemo.h" -#include "demos/TableViewDemo.h" -#include "demos/TextAreaDemo.h" -#include "demos/TextFieldDemo.h" -#include "demos/TreeViewDemo.h" - -#include - -namespace cf::ui::example { - -MaterialGalleryWindow::MaterialGalleryWindow(QWidget* parent) - : QMainWindow(parent), centralWidget_(nullptr), mainLayout_(nullptr), navList_(nullptr), - contentStack_(nullptr), splitter_(nullptr) { - setupUI(); - createNavigation(); - registerDemos(); - - setWindowTitle("Material Design 3 Widget Gallery"); - resize(1200, 800); -} - -MaterialGalleryWindow::~MaterialGalleryWindow() = default; - -void MaterialGalleryWindow::setupUI() { - // Create central widget - centralWidget_ = new QWidget(this); - setCentralWidget(centralWidget_); - - // Create main layout - mainLayout_ = new QHBoxLayout(centralWidget_); - mainLayout_->setContentsMargins(0, 0, 0, 0); - mainLayout_->setSpacing(0); - - // Create splitter for resizable navigation - splitter_ = new QSplitter(Qt::Horizontal, centralWidget_); - mainLayout_->addWidget(splitter_); -} - -void MaterialGalleryWindow::createNavigation() { - // Create left navigation list - navList_ = new QListWidget(splitter_); - navList_->setMinimumWidth(180); - navList_->setMaximumWidth(250); - navList_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); - - // Create right content stack - contentStack_ = new QStackedWidget(splitter_); - - // Add widgets to splitter - splitter_->addWidget(navList_); - splitter_->addWidget(contentStack_); - - // Set initial splitter sizes (nav:content = 200:1000) - splitter_->setSizes({200, 1000}); - splitter_->setStretchFactor(0, 0); - splitter_->setStretchFactor(1, 1); - - // Connect navigation to content stack - connect(navList_, &QListWidget::currentRowChanged, contentStack_, - &QStackedWidget::setCurrentIndex); -} - -void MaterialGalleryWindow::registerDemos() { - // 1. Button - auto* buttonDemo = new ButtonDemo(contentStack_); - navList_->addItem(buttonDemo->title()); - contentStack_->addWidget(buttonDemo); - - // 2. CheckBox - auto* checkBoxDemo = new CheckBoxDemo(contentStack_); - navList_->addItem(checkBoxDemo->title()); - contentStack_->addWidget(checkBoxDemo); - - // 3. ComboBox - auto* comboBoxDemo = new ComboBoxDemo(contentStack_); - navList_->addItem(comboBoxDemo->title()); - contentStack_->addWidget(comboBoxDemo); - - // 4. GroupBox - auto* groupBoxDemo = new GroupBoxDemo(contentStack_); - navList_->addItem(groupBoxDemo->title()); - contentStack_->addWidget(groupBoxDemo); - - // 5. Label - auto* labelDemo = new LabelDemo(contentStack_); - navList_->addItem(labelDemo->title()); - contentStack_->addWidget(labelDemo); - - // 6. ProgressBar - auto* progressBarDemo = new ProgressBarDemo(contentStack_); - navList_->addItem(progressBarDemo->title()); - contentStack_->addWidget(progressBarDemo); - - // 7. RadioButton - auto* radioButtonDemo = new RadioButtonDemo(contentStack_); - navList_->addItem(radioButtonDemo->title()); - contentStack_->addWidget(radioButtonDemo); - - // 8. Slider - auto* sliderDemo = new SliderDemo(contentStack_); - navList_->addItem(sliderDemo->title()); - contentStack_->addWidget(sliderDemo); - - // 9. Switch - auto* switchDemo = new SwitchDemo(contentStack_); - navList_->addItem(switchDemo->title()); - contentStack_->addWidget(switchDemo); - - // 10. TextArea - auto* textAreaDemo = new TextAreaDemo(contentStack_); - navList_->addItem(textAreaDemo->title()); - contentStack_->addWidget(textAreaDemo); - - // 11. TextField - auto* textFieldDemo = new TextFieldDemo(contentStack_); - navList_->addItem(textFieldDemo->title()); - contentStack_->addWidget(textFieldDemo); - - // 12. SpinBox - auto* spinBoxDemo = new SpinBoxDemo(contentStack_); - navList_->addItem(spinBoxDemo->title()); - contentStack_->addWidget(spinBoxDemo); - - // 13. DoubleSpinBox - auto* doubleSpinBoxDemo = new DoubleSpinBoxDemo(contentStack_); - navList_->addItem(doubleSpinBoxDemo->title()); - contentStack_->addWidget(doubleSpinBoxDemo); - - // 14. Separator - auto* separatorDemo = new SeparatorDemo(contentStack_); - navList_->addItem(separatorDemo->title()); - contentStack_->addWidget(separatorDemo); - - // 15. ScrollView - auto* scrollViewDemo = new ScrollViewDemo(contentStack_); - navList_->addItem(scrollViewDemo->title()); - contentStack_->addWidget(scrollViewDemo); - - // 16. ListView - auto* listViewDemo = new ListViewDemo(contentStack_); - navList_->addItem(listViewDemo->title()); - contentStack_->addWidget(listViewDemo); - - // 17. TableView - auto* tableViewDemo = new TableViewDemo(contentStack_); - navList_->addItem(tableViewDemo->title()); - contentStack_->addWidget(tableViewDemo); - - // 18. TabView - auto* tabViewDemo = new TabViewDemo(contentStack_); - navList_->addItem(tabViewDemo->title()); - contentStack_->addWidget(tabViewDemo); - - // 19. TreeView - auto* treeViewDemo = new TreeViewDemo(contentStack_); - navList_->addItem(treeViewDemo->title()); - contentStack_->addWidget(treeViewDemo); - - // Select first item - navList_->setCurrentRow(0); -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/MaterialGalleryWindow.h b/example/ui/widget/material_example/MaterialGalleryWindow.h deleted file mode 100644 index a5e887491..000000000 --- a/example/ui/widget/material_example/MaterialGalleryWindow.h +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @file MaterialGalleryWindow.h - * @brief Material Design 3 Widget Gallery Main Window - */ - -#pragma once - -#include -#include -#include -#include -#include -#include - -namespace cf::ui::example { - -class MaterialGalleryWindow : public QMainWindow { - Q_OBJECT - - public: - explicit MaterialGalleryWindow(QWidget* parent = nullptr); - ~MaterialGalleryWindow() override; - - QStackedWidget* contentStack() { return contentStack_; } - - private: - void setupUI(); - void createNavigation(); - void registerDemos(); - - QWidget* centralWidget_; - QHBoxLayout* mainLayout_; - QListWidget* navList_; - QStackedWidget* contentStack_; - QSplitter* splitter_; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/ButtonDemo.cpp b/example/ui/widget/material_example/demos/ButtonDemo.cpp deleted file mode 100644 index 5d5f35cfe..000000000 --- a/example/ui/widget/material_example/demos/ButtonDemo.cpp +++ /dev/null @@ -1,275 +0,0 @@ -/** - * @file ButtonDemo.cpp - * @brief Material Design 3 Button Demo - Implementation - */ - -#include "ButtonDemo.h" - -#include "ui/widget/material/widget/button/button.h" - -#include -#include -#include -#include -#include -#include -#include - -using namespace cf::ui::widget::material; -using ButtonVariant = cf::ui::widget::material::Button::ButtonVariant; - -namespace cf::ui::example { - -ButtonDemo::ButtonDemo(QWidget* parent) : QWidget(parent) { - - setupUI(); - createButtonVariantsSection(); - createButtonStatesSection(); - createButtonWithIconSection(); - createElevationSection(); - createInteractionDemoSection(); -} - -void ButtonDemo::setupUI() { - layout_ = new QVBoxLayout(this); - layout_->setContentsMargins(24, 24, 24, 24); - layout_->setSpacing(16); - - // Scroll area for content - QScrollArea* scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scrollContent_ = new QWidget(); - QVBoxLayout* scrollLayout = new QVBoxLayout(scrollContent_); - scrollLayout->setSpacing(24); - scrollLayout->setContentsMargins(0, 0, 0, 0); - - scrollArea->setWidget(scrollContent_); - layout_->addWidget(scrollArea, 1); -} - -void ButtonDemo::createButtonVariantsSection() { - QGroupBox* groupBox = new QGroupBox("Button Variants (按钮变体)", this); - QGridLayout* gridLayout = new QGridLayout(groupBox); - gridLayout->setSpacing(16); - gridLayout->setContentsMargins(16, 24, 16, 16); - - // Create 5 button variants in 2x3 grid layout - // Row 1, Col 0: Filled Button - QLabel* filledLabel = new QLabel("Filled", this); - filledLabel->setAlignment(Qt::AlignCenter); - Button* filledButton = new Button("Filled", ButtonVariant::Filled, this); - gridLayout->addWidget(filledLabel, 0, 0); - gridLayout->addWidget(filledButton, 1, 0); - - // Row 1, Col 1: Tonal Button - QLabel* tonalLabel = new QLabel("Tonal", this); - tonalLabel->setAlignment(Qt::AlignCenter); - Button* tonalButton = new Button("Tonal", ButtonVariant::Tonal, this); - gridLayout->addWidget(tonalLabel, 0, 1); - gridLayout->addWidget(tonalButton, 1, 1); - - // Row 1, Col 2: Outlined Button - QLabel* outlinedLabel = new QLabel("Outlined", this); - outlinedLabel->setAlignment(Qt::AlignCenter); - Button* outlinedButton = new Button("Outlined", ButtonVariant::Outlined, this); - gridLayout->addWidget(outlinedLabel, 0, 2); - gridLayout->addWidget(outlinedButton, 1, 2); - - // Row 2, Col 0: Text Button - QLabel* textLabel = new QLabel("Text", this); - textLabel->setAlignment(Qt::AlignCenter); - Button* textButton = new Button("Text", ButtonVariant::Text, this); - gridLayout->addWidget(textLabel, 2, 0); - gridLayout->addWidget(textButton, 3, 0); - - // Row 2, Col 1: Elevated Button - QLabel* elevatedLabel = new QLabel("Elevated", this); - elevatedLabel->setAlignment(Qt::AlignCenter); - Button* elevatedButton = new Button("Elevated", ButtonVariant::Elevated, this); - gridLayout->addWidget(elevatedLabel, 2, 1); - gridLayout->addWidget(elevatedButton, 3, 1); - - // Add stretch to empty cell (Row 2, Col 2) - gridLayout->setRowStretch(4, 1); - gridLayout->setColumnStretch(0, 1); - gridLayout->setColumnStretch(1, 1); - gridLayout->setColumnStretch(2, 1); - - scrollContent_->layout()->addWidget(groupBox); -} - -void ButtonDemo::createButtonStatesSection() { - QGroupBox* groupBox = createGroupBox("Button States (按钮状态)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Normal (enabled) buttons - QHBoxLayout* normalLayout = new QHBoxLayout(); - QLabel* normalLabel = new QLabel("Normal:", this); - normalLabel->setMinimumWidth(120); - Button* normalButton = new Button("Enabled Button", ButtonVariant::Filled, this); - normalLayout->addWidget(normalLabel); - normalLayout->addWidget(normalButton); - normalLayout->addStretch(); - layout->addLayout(normalLayout); - - // Disabled buttons - QHBoxLayout* disabledLayout = new QHBoxLayout(); - QLabel* disabledLabel = new QLabel("Disabled:", this); - disabledLabel->setMinimumWidth(120); - Button* disabledFilled = new Button("Disabled Filled", ButtonVariant::Filled, this); - disabledFilled->setEnabled(false); - Button* disabledOutlined = new Button("Disabled Outlined", ButtonVariant::Outlined, this); - disabledOutlined->setEnabled(false); - Button* disabledText = new Button("Disabled Text", ButtonVariant::Text, this); - disabledText->setEnabled(false); - disabledLayout->addWidget(disabledLabel); - disabledLayout->addWidget(disabledFilled); - disabledLayout->addWidget(disabledOutlined); - disabledLayout->addWidget(disabledText); - disabledLayout->addStretch(); - layout->addLayout(disabledLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void ButtonDemo::createButtonWithIconSection() { - QGroupBox* groupBox = createGroupBox("Buttons with Icons (带图标的按钮)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Filled with icon - QHBoxLayout* filledIconLayout = new QHBoxLayout(); - QLabel* filledIconLabel = new QLabel("Filled + Icon:", this); - filledIconLabel->setMinimumWidth(120); - Button* filledIconButton = new Button("Save", ButtonVariant::Filled, this); - // Use standard Qt icon as example - filledIconButton->setIcon(QApplication::style()->standardIcon(QStyle::SP_DialogSaveButton)); - filledIconLayout->addWidget(filledIconLabel); - filledIconLayout->addWidget(filledIconButton); - filledIconLayout->addStretch(); - layout->addLayout(filledIconLayout); - - // Outlined with icon - QHBoxLayout* outlinedIconLayout = new QHBoxLayout(); - QLabel* outlinedIconLabel = new QLabel("Outlined + Icon:", this); - outlinedIconLabel->setMinimumWidth(120); - Button* outlinedIconButton = new Button("Open", ButtonVariant::Outlined, this); - outlinedIconButton->setIcon(QApplication::style()->standardIcon(QStyle::SP_DialogOpenButton)); - outlinedIconLayout->addWidget(outlinedIconLabel); - outlinedIconLayout->addWidget(outlinedIconButton); - outlinedIconLayout->addStretch(); - layout->addLayout(outlinedIconLayout); - - // Tonal with icon - QHBoxLayout* tonalIconLayout = new QHBoxLayout(); - QLabel* tonalIconLabel = new QLabel("Tonal + Icon:", this); - tonalIconLabel->setMinimumWidth(120); - Button* tonalIconButton = new Button("Delete", ButtonVariant::Tonal, this); - tonalIconButton->setIcon(QApplication::style()->standardIcon(QStyle::SP_TrashIcon)); - tonalIconLayout->addWidget(tonalIconLabel); - tonalIconLayout->addWidget(tonalIconButton); - tonalIconLayout->addStretch(); - layout->addLayout(tonalIconLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void ButtonDemo::createElevationSection() { - QGroupBox* groupBox = createGroupBox("Elevation Levels (阴影级别)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Elevation 0 (default) - QHBoxLayout* e0Layout = new QHBoxLayout(); - QLabel* e0Label = new QLabel("Elevation 0:", this); - e0Label->setMinimumWidth(120); - Button* e0Button = new Button("Level 0", ButtonVariant::Elevated, this); - e0Button->setElevation(0); - e0Layout->addWidget(e0Label); - e0Layout->addWidget(e0Button); - e0Layout->addStretch(); - layout->addLayout(e0Layout); - - // Elevation 1 - QHBoxLayout* e1Layout = new QHBoxLayout(); - QLabel* e1Label = new QLabel("Elevation 1:", this); - e1Label->setMinimumWidth(120); - Button* e1Button = new Button("Level 1", ButtonVariant::Elevated, this); - e1Button->setElevation(1); - e1Layout->addWidget(e1Label); - e1Layout->addWidget(e1Button); - e1Layout->addStretch(); - layout->addLayout(e1Layout); - - // Elevation 2 - QHBoxLayout* e2Layout = new QHBoxLayout(); - QLabel* e2Label = new QLabel("Elevation 2:", this); - e2Label->setMinimumWidth(120); - Button* e2Button = new Button("Level 2", ButtonVariant::Elevated, this); - e2Button->setElevation(2); - e2Layout->addWidget(e2Label); - e2Layout->addWidget(e2Button); - e2Layout->addStretch(); - layout->addLayout(e2Layout); - - // Elevation 3 - QHBoxLayout* e3Layout = new QHBoxLayout(); - QLabel* e3Label = new QLabel("Elevation 3:", this); - e3Label->setMinimumWidth(120); - Button* e3Button = new Button("Level 3", ButtonVariant::Elevated, this); - e3Button->setElevation(3); - e3Layout->addWidget(e3Label); - e3Layout->addWidget(e3Button); - e3Layout->addStretch(); - layout->addLayout(e3Layout); - - // Elevation 4 - QHBoxLayout* e4Layout = new QHBoxLayout(); - QLabel* e4Label = new QLabel("Elevation 4:", this); - e4Label->setMinimumWidth(120); - Button* e4Button = new Button("Level 4", ButtonVariant::Elevated, this); - e4Button->setElevation(4); - e4Layout->addWidget(e4Label); - e4Layout->addWidget(e4Button); - e4Layout->addStretch(); - layout->addLayout(e4Layout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void ButtonDemo::createInteractionDemoSection() { - QGroupBox* groupBox = createGroupBox("Button Interaction (按钮交互)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Click counter demo - QHBoxLayout* counterLayout = new QHBoxLayout(); - QLabel* hintLabel = new QLabel("Click the button to test interaction:", this); - hintLabel->setMinimumWidth(200); - Button* demoButton = new Button("Click me (0)", ButtonVariant::Filled, this); - connect(demoButton, &Button::clicked, this, &ButtonDemo::onDemoButtonClicked); - counterLayout->addWidget(hintLabel); - counterLayout->addWidget(demoButton); - counterLayout->addStretch(); - layout->addLayout(counterLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void ButtonDemo::onDemoButtonClicked() { - clickCount_++; - Button* button = qobject_cast(sender()); - if (button) { - button->setText(QString("Click me (%1)").arg(clickCount_)); - } -} - -QGroupBox* ButtonDemo::createGroupBox(const QString& title) { - QGroupBox* groupBox = new QGroupBox(title, this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - return groupBox; -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/ButtonDemo.h b/example/ui/widget/material_example/demos/ButtonDemo.h deleted file mode 100644 index 2f0ed1be3..000000000 --- a/example/ui/widget/material_example/demos/ButtonDemo.h +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @file ButtonDemo.h - * @brief Material Design 3 Button Demo - Widget Component - */ - -#pragma once - -#include -#include -#include -class QGroupBox; - -namespace cf::ui::example { - -class ButtonDemo : public QWidget { - Q_OBJECT - - public: - explicit ButtonDemo(QWidget* parent = nullptr); - ~ButtonDemo() override = default; - - QString title() const { return "Button"; } - QString description() const { return "Material Design 3 Button Component"; } - - private: - void setupUI(); - void createButtonVariantsSection(); - void createButtonStatesSection(); - void createButtonWithIconSection(); - void createElevationSection(); - void createInteractionDemoSection(); - QGroupBox* createGroupBox(const QString& title); - - void onDemoButtonClicked(); - - QWidget* scrollContent_; - QVBoxLayout* layout_; - int clickCount_ = 0; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/CheckBoxDemo.cpp b/example/ui/widget/material_example/demos/CheckBoxDemo.cpp deleted file mode 100644 index 5cc3d51af..000000000 --- a/example/ui/widget/material_example/demos/CheckBoxDemo.cpp +++ /dev/null @@ -1,378 +0,0 @@ -/** - * @file CheckBoxDemo.cpp - * @brief Material Design 3 CheckBox Demo - Implementation - */ - -#include "CheckBoxDemo.h" - -#include "ui/widget/material/widget/checkbox/checkbox.h" - -#include -#include -#include -#include -#include -#include -#include - -using namespace cf::ui::widget::material; - -namespace cf::ui::example { - -CheckBoxDemo::CheckBoxDemo(QWidget* parent) : QWidget(parent) { - - setupUI(); - createCheckboxStatesSection(); - createTristateSection(); - createErrorStateSection(); - createDisabledSection(); - createInteractiveDemoSection(); - createCustomLabelsSection(); -} - -void CheckBoxDemo::setupUI() { - layout_ = new QVBoxLayout(this); - layout_->setContentsMargins(24, 24, 24, 24); - layout_->setSpacing(16); - - // Scroll area for content - QScrollArea* scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scrollContent_ = new QWidget(); - QVBoxLayout* scrollLayout = new QVBoxLayout(scrollContent_); - scrollLayout->setSpacing(24); - scrollLayout->setContentsMargins(0, 0, 0, 0); - - scrollArea->setWidget(scrollContent_); - layout_->addWidget(scrollArea, 1); -} - -void CheckBoxDemo::createCheckboxStatesSection() { - QGroupBox* groupBox = new QGroupBox("Checkbox States (复选框状态)", this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - - // Unchecked - QHBoxLayout* uncheckedLayout = new QHBoxLayout(); - CheckBox* uncheckedBox = new CheckBox("Unchecked (未选中)", this); - uncheckedLayout->addWidget(uncheckedBox); - uncheckedLayout->addStretch(); - layout->addLayout(uncheckedLayout); - - // Checked - QHBoxLayout* checkedLayout = new QHBoxLayout(); - CheckBox* checkedBox = new CheckBox("Checked (已选中)", this); - checkedBox->setChecked(true); - checkedLayout->addWidget(checkedBox); - checkedLayout->addStretch(); - layout->addLayout(checkedLayout); - - // Programmatically set checked - QHBoxLayout* progLayout = new QHBoxLayout(); - CheckBox* progBox = new CheckBox("Click to toggle (点击切换)", this); - progLayout->addWidget(progBox); - progLayout->addStretch(); - layout->addLayout(progLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void CheckBoxDemo::createTristateSection() { - QGroupBox* groupBox = new QGroupBox("Tristate Checkbox (三态复选框)", this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - - // Tristate checkbox - QHBoxLayout* tristateLayout = new QHBoxLayout(); - CheckBox* tristateBox = new CheckBox("Tristate (三态)", this); - tristateBox->setTristate(true); - tristateBox->setCheckState(Qt::PartiallyChecked); - tristateLayout->addWidget(tristateBox); - tristateLayout->addStretch(); - layout->addLayout(tristateLayout); - - // Parent-child checkboxes example - QLabel* hintLabel = new QLabel("Parent-Child Example (父子级联动):", this); - layout->addWidget(hintLabel); - - QHBoxLayout* parentLayout = new QHBoxLayout(); - CheckBox* parentBox = new CheckBox("Select All (全选)", this); - parentBox->setTristate(true); - parentLayout->addWidget(parentBox); - parentLayout->addStretch(); - layout->addLayout(parentLayout); - - QHBoxLayout* child1Layout = new QHBoxLayout(); - CheckBox* child1Box = new CheckBox("Option 1 (选项 1)", this); - child1Layout->addSpacing(24); - child1Layout->addWidget(child1Box); - child1Layout->addStretch(); - layout->addLayout(child1Layout); - - QHBoxLayout* child2Layout = new QHBoxLayout(); - CheckBox* child2Box = new CheckBox("Option 2 (选项 2)", this); - child2Layout->addSpacing(24); - child2Layout->addWidget(child2Box); - child2Layout->addStretch(); - layout->addLayout(child2Layout); - - QHBoxLayout* child3Layout = new QHBoxLayout(); - CheckBox* child3Box = new CheckBox("Option 3 (选项 3)", this); - child3Layout->addSpacing(24); - child3Layout->addWidget(child3Box); - child3Layout->addStretch(); - layout->addLayout(child3Layout); - - // Connect parent-child logic - connect(parentBox, &CheckBox::checkStateChanged, this, - [child1Box, child2Box, child3Box](Qt::CheckState state) { - if (state == Qt::Checked) { - child1Box->setChecked(true); - child2Box->setChecked(true); - child3Box->setChecked(true); - } else if (state == Qt::Unchecked) { - child1Box->setChecked(false); - child2Box->setChecked(false); - child3Box->setChecked(false); - } - }); - - auto updateParentState = [parentBox, child1Box, child2Box, child3Box]() { - int checkedCount = 0; - if (child1Box->isChecked()) - checkedCount++; - if (child2Box->isChecked()) - checkedCount++; - if (child3Box->isChecked()) - checkedCount++; - - if (checkedCount == 0) { - parentBox->setCheckState(Qt::Unchecked); - } else if (checkedCount == 3) { - parentBox->setCheckState(Qt::Checked); - } else { - parentBox->setCheckState(Qt::PartiallyChecked); - } - }; - - connect(child1Box, &CheckBox::checkStateChanged, parentBox, - [updateParentState]() { updateParentState(); }); - connect(child2Box, &CheckBox::checkStateChanged, parentBox, - [updateParentState]() { updateParentState(); }); - connect(child3Box, &CheckBox::checkStateChanged, parentBox, - [updateParentState]() { updateParentState(); }); - - scrollContent_->layout()->addWidget(groupBox); -} - -void CheckBoxDemo::createErrorStateSection() { - QGroupBox* groupBox = new QGroupBox("Error State (错误状态)", this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - - // Error checkbox - QHBoxLayout* errorLayout = new QHBoxLayout(); - CheckBox* errorBox = new CheckBox("I agree to the terms (同意条款)", this); - errorBox->setError(true); - errorLayout->addWidget(errorBox); - errorLayout->addStretch(); - layout->addLayout(errorLayout); - - // Toggle error state - QHBoxLayout* toggleLayout = new QHBoxLayout(); - CheckBox* toggleBox = new CheckBox("Toggle error state (切换错误状态)", this); - connect(toggleBox, &CheckBox::clicked, this, &CheckBoxDemo::onErrorToggleClicked); - toggleLayout->addWidget(toggleBox); - toggleLayout->addStretch(); - layout->addLayout(toggleLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void CheckBoxDemo::createDisabledSection() { - QGroupBox* groupBox = new QGroupBox("Disabled State (禁用状态)", this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - - // Disabled unchecked - QHBoxLayout* disabledUncheckedLayout = new QHBoxLayout(); - CheckBox* disabledUnchecked = new CheckBox("Disabled unchecked (禁用未选)", this); - disabledUnchecked->setEnabled(false); - disabledUncheckedLayout->addWidget(disabledUnchecked); - disabledUncheckedLayout->addStretch(); - layout->addLayout(disabledUncheckedLayout); - - // Disabled checked - QHBoxLayout* disabledCheckedLayout = new QHBoxLayout(); - CheckBox* disabledChecked = new CheckBox("Disabled checked (禁用已选)", this); - disabledChecked->setEnabled(false); - disabledChecked->setChecked(true); - disabledCheckedLayout->addWidget(disabledChecked); - disabledCheckedLayout->addStretch(); - layout->addLayout(disabledCheckedLayout); - - // Disabled indeterminate - QHBoxLayout* disabledIndeterminateLayout = new QHBoxLayout(); - CheckBox* disabledIndeterminate = new CheckBox("Disabled indeterminate (禁用半选)", this); - disabledIndeterminate->setEnabled(false); - disabledIndeterminate->setTristate(true); - disabledIndeterminate->setCheckState(Qt::PartiallyChecked); - disabledIndeterminateLayout->addWidget(disabledIndeterminate); - disabledIndeterminateLayout->addStretch(); - layout->addLayout(disabledIndeterminateLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void CheckBoxDemo::createInteractiveDemoSection() { - QGroupBox* groupBox = new QGroupBox("Interactive Demo (交互演示)", this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - - statusLabel_ = new QLabel("Status: No checkboxes selected", this); - QFont statusFont("Segoe UI", 10); - statusLabel_->setFont(statusFont); - layout->addWidget(statusLabel_); - - QHBoxLayout* termsLayout = new QHBoxLayout(); - CheckBox* termsBox = new CheckBox("I accept the Terms of Service (接受服务条款)", this); - connect(termsBox, &CheckBox::clicked, this, &CheckBoxDemo::onTermsCheckboxClicked); - termsLayout->addWidget(termsBox); - termsLayout->addStretch(); - layout->addLayout(termsLayout); - - QHBoxLayout* privacyLayout = new QHBoxLayout(); - CheckBox* privacyBox = new CheckBox("I accept the Privacy Policy (接受隐私政策)", this); - connect(privacyBox, &CheckBox::clicked, this, &CheckBoxDemo::onTermsCheckboxClicked); - privacyLayout->addWidget(privacyBox); - privacyLayout->addStretch(); - layout->addLayout(privacyLayout); - - QHBoxLayout* acceptLayout = new QHBoxLayout(); - CheckBox* acceptBox = new CheckBox("Enable submit button (启用提交按钮)", this); - connect(acceptBox, &CheckBox::clicked, this, &CheckBoxDemo::onAcceptCheckboxClicked); - acceptLayout->addWidget(acceptBox); - acceptLayout->addStretch(); - layout->addLayout(acceptLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void CheckBoxDemo::createCustomLabelsSection() { - QGroupBox* groupBox = new QGroupBox("Various Labels (各种标签)", this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - - // Short label - QHBoxLayout* shortLayout = new QHBoxLayout(); - CheckBox* shortBox = new CheckBox("Short", this); - shortLayout->addWidget(shortBox); - shortLayout->addStretch(); - layout->addLayout(shortLayout); - - // Long label - QHBoxLayout* longLayout = new QHBoxLayout(); - CheckBox* longBox = - new CheckBox("This is a very long checkbox label that demonstrates text wrapping " - "or truncation behavior when the text is too long to fit in a single line.", - this); - longLayout->addWidget(longBox); - longLayout->addStretch(); - layout->addLayout(longLayout); - - // Checkbox without text - QHBoxLayout* noTextLayout = new QHBoxLayout(); - CheckBox* noTextBox = new CheckBox("", this); - QLabel* noTextLabel = new QLabel("Checkbox without text (无文本):", this); - noTextLayout->addWidget(noTextLabel); - noTextLayout->addWidget(noTextBox); - noTextLayout->addStretch(); - layout->addLayout(noTextLayout); - - // Newsletter subscription example - QHBoxLayout* newsletterLayout = new QHBoxLayout(); - CheckBox* newsletterBox = new CheckBox( - "Subscribe to our newsletter for updates and special offers (订阅我们的新闻通讯)", this); - connect(newsletterBox, &CheckBox::clicked, this, &CheckBoxDemo::onNewsletterCheckboxClicked); - newsletterLayout->addWidget(newsletterBox); - newsletterLayout->addStretch(); - layout->addLayout(newsletterLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void CheckBoxDemo::onAcceptCheckboxClicked(bool checked) { - if (checked) { - statusLabel_->setText("Status: Submit button enabled"); - statusLabel_->setStyleSheet("color: green;"); - } else { - statusLabel_->setText("Status: Submit button disabled"); - statusLabel_->setStyleSheet("color: red;"); - } -} - -void CheckBoxDemo::onTermsCheckboxClicked(bool checked) { - Q_UNUSED(checked) - // Check if both terms and privacy are checked - CheckBox* termsBox = qobject_cast(sender()); - CheckBox* privacyBox = nullptr; - - // Find the privacy checkbox - QList allCheckboxes = findChildren(); - for (CheckBox* box : allCheckboxes) { - if (box->text().contains("Privacy Policy")) { - privacyBox = box; - break; - } - } - - if (termsBox && privacyBox) { - if (termsBox->isChecked() && privacyBox->isChecked()) { - statusLabel_->setText("Status: All terms accepted!"); - statusLabel_->setStyleSheet("color: green;"); - } else { - statusLabel_->setText("Status: Please accept all terms"); - statusLabel_->setStyleSheet("color: orange;"); - } - } -} - -void CheckBoxDemo::onNewsletterCheckboxClicked(bool checked) { - if (checked) { - statusLabel_->setText("Status: You are now subscribed!"); - statusLabel_->setStyleSheet("color: blue;"); - } else { - statusLabel_->setText("Status: Subscription cancelled"); - statusLabel_->setStyleSheet(""); - } -} - -void CheckBoxDemo::onErrorToggleClicked(bool checked) { - // Find and toggle error state on the agreement checkbox - QList allCheckboxes = findChildren(); - for (CheckBox* box : allCheckboxes) { - if (box->text().contains("I agree to the terms")) { - box->setError(checked); - break; - } - } -} - -QGroupBox* CheckBoxDemo::createGroupBox(const QString& title) { - QGroupBox* groupBox = new QGroupBox(title, this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - return groupBox; -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/CheckBoxDemo.h b/example/ui/widget/material_example/demos/CheckBoxDemo.h deleted file mode 100644 index 9bec784a5..000000000 --- a/example/ui/widget/material_example/demos/CheckBoxDemo.h +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @file CheckBoxDemo.h - * @brief Material Design 3 CheckBox Demo - Widget Component - */ - -#pragma once - -#include -#include -#include -#include -#include -class QGroupBox; - -namespace cf::ui::example { - -class CheckBoxDemo : public QWidget { - Q_OBJECT - - public: - explicit CheckBoxDemo(QWidget* parent = nullptr); - ~CheckBoxDemo() override = default; - - QString title() const { return "CheckBox"; } - QString description() const { return "Material Design 3 CheckBox Component"; } - - private: - void setupUI(); - void createCheckboxStatesSection(); - void createTristateSection(); - void createErrorStateSection(); - void createDisabledSection(); - void createInteractiveDemoSection(); - void createCustomLabelsSection(); - QGroupBox* createGroupBox(const QString& title); - - void onAcceptCheckboxClicked(bool checked); - void onTermsCheckboxClicked(bool checked); - void onNewsletterCheckboxClicked(bool checked); - void onErrorToggleClicked(bool checked); - - QWidget* scrollContent_; - QVBoxLayout* layout_; - QLabel* statusLabel_; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/ComboBoxDemo.cpp b/example/ui/widget/material_example/demos/ComboBoxDemo.cpp deleted file mode 100644 index 7cb79021c..000000000 --- a/example/ui/widget/material_example/demos/ComboBoxDemo.cpp +++ /dev/null @@ -1,213 +0,0 @@ -/** - * @file ComboBoxDemo.cpp - * @brief Material Design 3 ComboBox Demo - Implementation - */ - -#include "ComboBoxDemo.h" - -#include "ui/widget/material/widget/comboBox/combobox.h" - -#include -#include -#include -#include -#include -#include - -using namespace cf::ui::widget::material; - -namespace cf::ui::example { - -ComboBoxDemo::ComboBoxDemo(QWidget* parent) : QWidget(parent) { - - setupUI(); - createVariantsSection(); - createItemsSection(); - createStatesSection(); - createInteractiveDemoSection(); -} - -void ComboBoxDemo::setupUI() { - layout_ = new QVBoxLayout(this); - layout_->setContentsMargins(24, 24, 24, 24); - layout_->setSpacing(16); - - // Scroll area for content - QScrollArea* scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scrollContent_ = new QWidget(); - QVBoxLayout* scrollLayout = new QVBoxLayout(scrollContent_); - scrollLayout->setSpacing(24); - scrollLayout->setContentsMargins(0, 0, 0, 0); - - scrollArea->setWidget(scrollContent_); - layout_->addWidget(scrollArea, 1); -} - -void ComboBoxDemo::createVariantsSection() { - QGroupBox* groupBox = new QGroupBox("ComboBox Variants (变体)", this); - QGridLayout* gridLayout = new QGridLayout(groupBox); - gridLayout->setSpacing(16); - gridLayout->setContentsMargins(16, 24, 16, 16); - - // Filled - QLabel* filledLabel = new QLabel("Filled:", this); - ComboBox* filledCombo = new ComboBox(this); - filledCombo->addItem("Option 1"); - filledCombo->addItem("Option 2"); - filledCombo->addItem("Option 3"); - filledCombo->setCurrentIndex(0); - gridLayout->addWidget(filledLabel, 0, 0); - gridLayout->addWidget(filledCombo, 0, 1); - - // Outlined - QLabel* outlinedLabel = new QLabel("Outlined:", this); - ComboBox* outlinedCombo = new ComboBox(this); - outlinedCombo->setVariant(ComboBox::ComboBoxVariant::Outlined); - outlinedCombo->addItem("Option A"); - outlinedCombo->addItem("Option B"); - outlinedCombo->addItem("Option C"); - outlinedCombo->setCurrentIndex(0); - gridLayout->addWidget(outlinedLabel, 1, 0); - gridLayout->addWidget(outlinedCombo, 1, 1); - - gridLayout->setRowStretch(2, 1); - scrollContent_->layout()->addWidget(groupBox); -} - -void ComboBoxDemo::createItemsSection() { - QGroupBox* groupBox = createGroupBox("Different Item Counts (不同项目数)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Single item - QHBoxLayout* singleLayout = new QHBoxLayout(); - QLabel* singleLabel = new QLabel("Single:", this); - singleLabel->setMinimumWidth(100); - ComboBox* singleCombo = new ComboBox(this); - singleCombo->addItem("Only Option"); - singleLayout->addWidget(singleLabel); - singleLayout->addWidget(singleCombo); - singleLayout->addStretch(); - layout->addLayout(singleLayout); - - // Few items - QHBoxLayout* fewLayout = new QHBoxLayout(); - QLabel* fewLabel = new QLabel("Few items:", this); - fewLabel->setMinimumWidth(100); - ComboBox* fewCombo = new ComboBox(this); - fewCombo->addItem("Apple"); - fewCombo->addItem("Banana"); - fewCombo->addItem("Cherry"); - fewLayout->addWidget(fewLabel); - fewLayout->addWidget(fewCombo); - fewLayout->addStretch(); - layout->addLayout(fewLayout); - - // Many items - QHBoxLayout* manyLayout = new QHBoxLayout(); - QLabel* manyLabel = new QLabel("Many items:", this); - manyLabel->setMinimumWidth(100); - ComboBox* manyCombo = new ComboBox(this); - for (int i = 1; i <= 20; ++i) { - manyCombo->addItem(QString("Item %1").arg(i)); - } - manyCombo->setCurrentIndex(5); - manyLayout->addWidget(manyLabel); - manyLayout->addWidget(manyCombo); - manyLayout->addStretch(); - layout->addLayout(manyLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void ComboBoxDemo::createStatesSection() { - QGroupBox* groupBox = createGroupBox("ComboBox States (状态)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Normal - QHBoxLayout* normalLayout = new QHBoxLayout(); - QLabel* normalLabel = new QLabel("Normal:", this); - normalLabel->setMinimumWidth(100); - ComboBox* normalCombo = new ComboBox(this); - normalCombo->addItem("Enabled"); - normalCombo->addItem("Option 2"); - normalLayout->addWidget(normalLabel); - normalLayout->addWidget(normalCombo); - normalLayout->addStretch(); - layout->addLayout(normalLayout); - - // Disabled - QHBoxLayout* disabledLayout = new QHBoxLayout(); - QLabel* disabledLabel = new QLabel("Disabled:", this); - disabledLabel->setMinimumWidth(100); - ComboBox* disabledCombo = new ComboBox(this); - disabledCombo->addItem("Disabled"); - disabledCombo->addItem("Option 2"); - disabledCombo->setEnabled(false); - disabledLayout->addWidget(disabledLabel); - disabledLayout->addWidget(disabledCombo); - disabledLayout->addStretch(); - layout->addLayout(disabledLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void ComboBoxDemo::createInteractiveDemoSection() { - QGroupBox* groupBox = createGroupBox("Interactive Demo (交互演示)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - QHBoxLayout* demoLayout = new QHBoxLayout(); - QLabel* demoLabel = new QLabel("Choose a fruit:", this); - demoLabel->setMinimumWidth(100); - - ComboBox* demoCombo = new ComboBox(this); - demoCombo->addItem("Apple"); - demoCombo->addItem("Banana"); - demoCombo->addItem("Cherry"); - demoCombo->addItem("Date"); - demoCombo->addItem("Elderberry"); - connect(demoCombo, QOverload::of(&ComboBox::currentIndexChanged), this, - &ComboBoxDemo::onDemoComboBoxChanged); - - demoLayout->addWidget(demoLabel); - demoLayout->addWidget(demoCombo); - demoLayout->addStretch(); - layout->addLayout(demoLayout); - - // Selection display - selectionLabel_ = new QLabel("Selected: Apple", this); - QFont selectionFont("Segoe UI", 10, QFont::Bold); - selectionLabel_->setFont(selectionFont); - QHBoxLayout* selectionLayout = new QHBoxLayout(); - selectionLayout->addSpacing(100); - selectionLayout->addWidget(selectionLabel_); - selectionLayout->addStretch(); - layout->addLayout(selectionLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void ComboBoxDemo::onComboBoxChanged(int index) { - Q_UNUSED(index) - // Could add more interactive feedback here -} - -void ComboBoxDemo::onDemoComboBoxChanged(int index) { - ComboBox* combo = qobject_cast(sender()); - if (combo && index >= 0) { - selectionLabel_->setText(QString("Selected: %1").arg(combo->currentText())); - } -} - -QGroupBox* ComboBoxDemo::createGroupBox(const QString& title) { - QGroupBox* groupBox = new QGroupBox(title, this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - return groupBox; -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/ComboBoxDemo.h b/example/ui/widget/material_example/demos/ComboBoxDemo.h deleted file mode 100644 index a36c2c6a3..000000000 --- a/example/ui/widget/material_example/demos/ComboBoxDemo.h +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @file ComboBoxDemo.h - * @brief Material Design 3 ComboBox Demo - Widget Component - */ - -#pragma once - -#include -#include -#include -#include -#include - -namespace cf::ui::example { - -class ComboBoxDemo : public QWidget { - Q_OBJECT - - public: - explicit ComboBoxDemo(QWidget* parent = nullptr); - ~ComboBoxDemo() override = default; - - QString title() const { return "ComboBox"; } - QString description() const { return "Material Design 3 ComboBox Component"; } - - private: - void setupUI(); - void createVariantsSection(); - void createItemsSection(); - void createStatesSection(); - void createInteractiveDemoSection(); - QGroupBox* createGroupBox(const QString& title); - - void onComboBoxChanged(int index); - void onDemoComboBoxChanged(int index); - - QWidget* scrollContent_; - QVBoxLayout* layout_; - QLabel* selectionLabel_; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/DoubleSpinBoxDemo.cpp b/example/ui/widget/material_example/demos/DoubleSpinBoxDemo.cpp deleted file mode 100644 index 30151cbbc..000000000 --- a/example/ui/widget/material_example/demos/DoubleSpinBoxDemo.cpp +++ /dev/null @@ -1,390 +0,0 @@ -/** - * @file DoubleSpinBoxDemo.cpp - * @brief Material Design 3 DoubleSpinBox Demo - Implementation - */ - -#include "DoubleSpinBoxDemo.h" - -#include "ui/widget/material/widget/doublespinbox/doublespinbox.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace cf::ui::widget::material; - -namespace cf::ui::example { - -DoubleSpinBoxDemo::DoubleSpinBoxDemo(QWidget* parent) : QWidget(parent) { - - setupUI(); - createVariantsSection(); - createStatesSection(); - createPrecisionSection(); - createValueRangeSection(); - createStepSizeSection(); - createInteractionDemoSection(); -} - -void DoubleSpinBoxDemo::setupUI() { - layout_ = new QVBoxLayout(this); - layout_->setContentsMargins(24, 24, 24, 24); - layout_->setSpacing(16); - - // Scroll area for content - QScrollArea* scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scrollContent_ = new QWidget(); - QVBoxLayout* scrollLayout = new QVBoxLayout(scrollContent_); - scrollLayout->setSpacing(24); - scrollLayout->setContentsMargins(0, 0, 0, 0); - - scrollArea->setWidget(scrollContent_); - layout_->addWidget(scrollArea, 1); -} - -void DoubleSpinBoxDemo::createVariantsSection() { - QGroupBox* groupBox = new QGroupBox("Basic Variants (基本变体)", this); - QGridLayout* gridLayout = new QGridLayout(groupBox); - gridLayout->setSpacing(16); - gridLayout->setContentsMargins(16, 24, 16, 16); - - // Default DoubleSpinBox - QLabel* defaultLabel = new QLabel("Default:", this); - DoubleSpinBox* defaultSpinBox = new DoubleSpinBox(this); - defaultSpinBox->setRange(0.0, 100.0); - defaultSpinBox->setValue(50.0); - gridLayout->addWidget(defaultLabel, 0, 0); - gridLayout->addWidget(defaultSpinBox, 0, 1); - - // With prefix - QLabel* prefixLabel = new QLabel("With Prefix:", this); - DoubleSpinBox* prefixSpinBox = new DoubleSpinBox(this); - prefixSpinBox->setRange(0.0, 1000.0); - prefixSpinBox->setValue(100.0); - prefixSpinBox->setPrefix("$"); - gridLayout->addWidget(prefixLabel, 1, 0); - gridLayout->addWidget(prefixSpinBox, 1, 1); - - // With suffix - QLabel* suffixLabel = new QLabel("With Suffix:", this); - DoubleSpinBox* suffixSpinBox = new DoubleSpinBox(this); - suffixSpinBox->setRange(0.0, 100.0); - suffixSpinBox->setValue(25.5); - suffixSpinBox->setSuffix(" %"); - gridLayout->addWidget(suffixLabel, 2, 0); - gridLayout->addWidget(suffixSpinBox, 2, 1); - - // With both prefix and suffix - QLabel* bothLabel = new QLabel("Prefix + Suffix:", this); - DoubleSpinBox* bothSpinBox = new DoubleSpinBox(this); - bothSpinBox->setRange(-100.0, 100.0); - bothSpinBox->setValue(0.0); - bothSpinBox->setPrefix("+/- "); - bothSpinBox->setSuffix(" deg"); - gridLayout->addWidget(bothLabel, 3, 0); - gridLayout->addWidget(bothSpinBox, 3, 1); - - gridLayout->setColumnStretch(1, 1); - scrollContent_->layout()->addWidget(groupBox); -} - -void DoubleSpinBoxDemo::createStatesSection() { - QGroupBox* groupBox = createGroupBox("States (状态)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Normal (enabled) state - QHBoxLayout* normalLayout = new QHBoxLayout(); - QLabel* normalLabel = new QLabel("Normal (Enabled):", this); - normalLabel->setMinimumWidth(150); - DoubleSpinBox* normalSpinBox = new DoubleSpinBox(this); - normalSpinBox->setRange(0.0, 100.0); - normalSpinBox->setValue(50.0); - normalLayout->addWidget(normalLabel); - normalLayout->addWidget(normalSpinBox); - normalLayout->addStretch(); - layout->addLayout(normalLayout); - - // Disabled state - QHBoxLayout* disabledLayout = new QHBoxLayout(); - QLabel* disabledLabel = new QLabel("Disabled:", this); - disabledLabel->setMinimumWidth(150); - DoubleSpinBox* disabledSpinBox = new DoubleSpinBox(this); - disabledSpinBox->setRange(0.0, 100.0); - disabledSpinBox->setValue(50.0); - disabledSpinBox->setEnabled(false); - disabledLayout->addWidget(disabledLabel); - disabledLayout->addWidget(disabledSpinBox); - disabledLayout->addStretch(); - layout->addLayout(disabledLayout); - - // Read-only state - QHBoxLayout* readOnlyLayout = new QHBoxLayout(); - QLabel* readOnlyLabel = new QLabel("Read-only:", this); - readOnlyLabel->setMinimumWidth(150); - DoubleSpinBox* readOnlySpinBox = new DoubleSpinBox(this); - readOnlySpinBox->setRange(0.0, 100.0); - readOnlySpinBox->setValue(75.0); - readOnlySpinBox->setReadOnly(true); - readOnlyLayout->addWidget(readOnlyLabel); - readOnlyLayout->addWidget(readOnlySpinBox); - readOnlyLayout->addStretch(); - layout->addLayout(readOnlyLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void DoubleSpinBoxDemo::createPrecisionSection() { - QGroupBox* groupBox = new QGroupBox("Decimal Precision (小数精度)", this); - QGridLayout* gridLayout = new QGridLayout(groupBox); - gridLayout->setSpacing(12); - gridLayout->setContentsMargins(16, 24, 16, 16); - - // No decimals (0) - QLabel* intLabel = new QLabel("Integer (0 decimals):", this); - DoubleSpinBox* intSpinBox = new DoubleSpinBox(this); - intSpinBox->setRange(0.0, 1000.0); - intSpinBox->setValue(50); - intSpinBox->setDecimals(0); - gridLayout->addWidget(intLabel, 0, 0); - gridLayout->addWidget(intSpinBox, 0, 1); - - // 1 decimal place - QLabel* oneDecimalLabel = new QLabel("1 decimal place:", this); - DoubleSpinBox* oneDecimalSpinBox = new DoubleSpinBox(this); - oneDecimalSpinBox->setRange(0.0, 100.0); - oneDecimalSpinBox->setValue(50.5); - oneDecimalSpinBox->setDecimals(1); - gridLayout->addWidget(oneDecimalLabel, 1, 0); - gridLayout->addWidget(oneDecimalSpinBox, 1, 1); - - // 2 decimal places (default) - QLabel* twoDecimalLabel = new QLabel("2 decimal places:", this); - DoubleSpinBox* twoDecimalSpinBox = new DoubleSpinBox(this); - twoDecimalSpinBox->setRange(0.0, 100.0); - twoDecimalSpinBox->setValue(50.25); - twoDecimalSpinBox->setDecimals(2); - gridLayout->addWidget(twoDecimalLabel, 2, 0); - gridLayout->addWidget(twoDecimalSpinBox, 2, 1); - - // 3 decimal places - QLabel* threeDecimalLabel = new QLabel("3 decimal places:", this); - DoubleSpinBox* threeDecimalSpinBox = new DoubleSpinBox(this); - threeDecimalSpinBox->setRange(0.0, 100.0); - threeDecimalSpinBox->setValue(50.125); - threeDecimalSpinBox->setDecimals(3); - gridLayout->addWidget(threeDecimalLabel, 3, 0); - gridLayout->addWidget(threeDecimalSpinBox, 3, 1); - - // 4 decimal places - QLabel* fourDecimalLabel = new QLabel("4 decimal places:", this); - DoubleSpinBox* fourDecimalSpinBox = new DoubleSpinBox(this); - fourDecimalSpinBox->setRange(0.0, 100.0); - fourDecimalSpinBox->setValue(50.0625); - fourDecimalSpinBox->setDecimals(4); - gridLayout->addWidget(fourDecimalLabel, 4, 0); - gridLayout->addWidget(fourDecimalSpinBox, 4, 1); - - gridLayout->setColumnStretch(1, 1); - scrollContent_->layout()->addWidget(groupBox); -} - -void DoubleSpinBoxDemo::createValueRangeSection() { - QGroupBox* groupBox = new QGroupBox("Value Ranges (数值范围)", this); - QGridLayout* gridLayout = new QGridLayout(groupBox); - gridLayout->setSpacing(12); - gridLayout->setContentsMargins(16, 24, 16, 16); - - // 0 to 1 range (normalized) - QLabel* normalizedLabel = new QLabel("Normalized [0, 1]:", this); - DoubleSpinBox* normalizedSpinBox = new DoubleSpinBox(this); - normalizedSpinBox->setRange(0.0, 1.0); - normalizedSpinBox->setValue(0.5); - normalizedSpinBox->setDecimals(3); - normalizedSpinBox->setSingleStep(0.01); - gridLayout->addWidget(normalizedLabel, 0, 0); - gridLayout->addWidget(normalizedSpinBox, 0, 1); - - // Negative to positive range - QLabel* centeredLabel = new QLabel("Centered [-100, 100]:", this); - DoubleSpinBox* centeredSpinBox = new DoubleSpinBox(this); - centeredSpinBox->setRange(-100.0, 100.0); - centeredSpinBox->setValue(0.0); - gridLayout->addWidget(centeredLabel, 1, 0); - gridLayout->addWidget(centeredSpinBox, 1, 1); - - // Large range - QLabel* largeLabel = new QLabel("Large range [0, 10000]:", this); - DoubleSpinBox* largeSpinBox = new DoubleSpinBox(this); - largeSpinBox->setRange(0.0, 10000.0); - largeSpinBox->setValue(5000.0); - largeSpinBox->setSingleStep(100.0); - gridLayout->addWidget(largeLabel, 2, 0); - gridLayout->addWidget(largeSpinBox, 2, 1); - - // Small fractional range - QLabel* smallLabel = new QLabel("Fractional [0, 0.1]:", this); - DoubleSpinBox* smallSpinBox = new DoubleSpinBox(this); - smallSpinBox->setRange(0.0, 0.1); - smallSpinBox->setValue(0.05); - smallSpinBox->setDecimals(4); - smallSpinBox->setSingleStep(0.001); - gridLayout->addWidget(smallLabel, 3, 0); - gridLayout->addWidget(smallSpinBox, 3, 1); - - gridLayout->setColumnStretch(1, 1); - scrollContent_->layout()->addWidget(groupBox); -} - -void DoubleSpinBoxDemo::createStepSizeSection() { - QGroupBox* groupBox = new QGroupBox("Step Sizes (步长)", this); - QGridLayout* gridLayout = new QGridLayout(groupBox); - gridLayout->setSpacing(12); - gridLayout->setContentsMargins(16, 24, 16, 16); - - // Small step (0.01) - QLabel* smallStepLabel = new QLabel("Small step (0.01):", this); - DoubleSpinBox* smallStepSpinBox = new DoubleSpinBox(this); - smallStepSpinBox->setRange(0.0, 10.0); - smallStepSpinBox->setValue(5.0); - smallStepSpinBox->setDecimals(2); - smallStepSpinBox->setSingleStep(0.01); - gridLayout->addWidget(smallStepLabel, 0, 0); - gridLayout->addWidget(smallStepSpinBox, 0, 1); - - // Medium step (0.5) - QLabel* mediumStepLabel = new QLabel("Medium step (0.5):", this); - DoubleSpinBox* mediumStepSpinBox = new DoubleSpinBox(this); - mediumStepSpinBox->setRange(0.0, 100.0); - mediumStepSpinBox->setValue(50.0); - mediumStepSpinBox->setDecimals(1); - mediumStepSpinBox->setSingleStep(0.5); - gridLayout->addWidget(mediumStepLabel, 1, 0); - gridLayout->addWidget(mediumStepSpinBox, 1, 1); - - // Large step (10.0) - QLabel* largeStepLabel = new QLabel("Large step (10.0):", this); - DoubleSpinBox* largeStepSpinBox = new DoubleSpinBox(this); - largeStepSpinBox->setRange(0.0, 1000.0); - largeStepSpinBox->setValue(500.0); - largeStepSpinBox->setSingleStep(10.0); - gridLayout->addWidget(largeStepLabel, 2, 0); - gridLayout->addWidget(largeStepSpinBox, 2, 1); - - // Very large step (100.0) - QLabel* veryLargeStepLabel = new QLabel("Very large step (100.0):", this); - DoubleSpinBox* veryLargeStepSpinBox = new DoubleSpinBox(this); - veryLargeStepSpinBox->setRange(0.0, 10000.0); - veryLargeStepSpinBox->setValue(5000.0); - veryLargeStepSpinBox->setSingleStep(100.0); - gridLayout->addWidget(veryLargeStepLabel, 3, 0); - gridLayout->addWidget(veryLargeStepSpinBox, 3, 1); - - gridLayout->setColumnStretch(1, 1); - scrollContent_->layout()->addWidget(groupBox); -} - -void DoubleSpinBoxDemo::createInteractionDemoSection() { - QGroupBox* groupBox = createGroupBox("Interaction Demo (交互演示)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Value tracking demo - QHBoxLayout* valueLayout = new QHBoxLayout(); - QLabel* valueLabel = new QLabel("Adjust value:", this); - valueLabel->setMinimumWidth(150); - DoubleSpinBox* valueSpinBox = new DoubleSpinBox(this); - valueSpinBox->setRange(0.0, 100.0); - valueSpinBox->setValue(50.0); - valueSpinBox->setSuffix(" units"); - connect(valueSpinBox, QOverload::of(&DoubleSpinBox::valueChanged), this, - &DoubleSpinBoxDemo::onValueValueChanged); - valueDisplayLabel_ = new QLabel("Current value: 50.00 units", this); - valueDisplayLabel_->setMinimumWidth(150); - valueLayout->addWidget(valueLabel); - valueLayout->addWidget(valueSpinBox); - valueLayout->addWidget(valueDisplayLabel_); - valueLayout->addStretch(); - layout->addLayout(valueLayout); - - // Price demo - QHBoxLayout* priceLayout = new QHBoxLayout(); - QLabel* priceLabel = new QLabel("Enter price:", this); - priceLabel->setMinimumWidth(150); - DoubleSpinBox* priceSpinBox = new DoubleSpinBox(this); - priceSpinBox->setRange(0.0, 999999.99); - priceSpinBox->setValue(99.99); - priceSpinBox->setPrefix("$"); - priceSpinBox->setDecimals(2); - connect(priceSpinBox, QOverload::of(&DoubleSpinBox::valueChanged), this, - &DoubleSpinBoxDemo::onPriceValueChanged); - priceDisplayLabel_ = new QLabel("Price: $99.99", this); - priceDisplayLabel_->setMinimumWidth(150); - priceLayout->addWidget(priceLabel); - priceLayout->addWidget(priceSpinBox); - priceLayout->addWidget(priceDisplayLabel_); - priceLayout->addStretch(); - layout->addLayout(priceLayout); - - // Temperature demo - QHBoxLayout* tempLayout = new QHBoxLayout(); - QLabel* tempLabel = new QLabel("Temperature:", this); - tempLabel->setMinimumWidth(150); - DoubleSpinBox* tempSpinBox = new DoubleSpinBox(this); - tempSpinBox->setRange(-273.15, 1000.0); - tempSpinBox->setValue(20.0); - tempSpinBox->setDecimals(1); - tempSpinBox->setSuffix(" °C"); - connect(tempSpinBox, QOverload::of(&DoubleSpinBox::valueChanged), this, - &DoubleSpinBoxDemo::onTemperatureValueChanged); - temperatureDisplayLabel_ = new QLabel("20.0 °C - Room temperature", this); - temperatureDisplayLabel_->setMinimumWidth(200); - tempLayout->addWidget(tempLabel); - tempLayout->addWidget(tempSpinBox); - tempLayout->addWidget(temperatureDisplayLabel_); - tempLayout->addStretch(); - layout->addLayout(tempLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void DoubleSpinBoxDemo::onValueValueChanged(double value) { - valueDisplayLabel_->setText(QString("Current value: %1 units").arg(value, 0, 'f', 2)); -} - -void DoubleSpinBoxDemo::onPriceValueChanged(double value) { - priceDisplayLabel_->setText(QString("Price: $%1").arg(value, 0, 'f', 2)); -} - -void DoubleSpinBoxDemo::onTemperatureValueChanged(double value) { - QString status; - if (value < 0) { - status = "Below freezing"; - } else if (value < 15) { - status = "Cold"; - } else if (value < 25) { - status = "Room temperature"; - } else if (value < 35) { - status = "Warm"; - } else { - status = "Hot"; - } - temperatureDisplayLabel_->setText(QString("%1 °C - %2").arg(value, 0, 'f', 1).arg(status)); -} - -QGroupBox* DoubleSpinBoxDemo::createGroupBox(const QString& title) { - QGroupBox* groupBox = new QGroupBox(title, this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - return groupBox; -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/DoubleSpinBoxDemo.h b/example/ui/widget/material_example/demos/DoubleSpinBoxDemo.h deleted file mode 100644 index 4209d384b..000000000 --- a/example/ui/widget/material_example/demos/DoubleSpinBoxDemo.h +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @file DoubleSpinBoxDemo.h - * @brief Material Design 3 DoubleSpinBox Demo - Widget Component - */ - -#pragma once - -#include -#include -#include -class QGroupBox; -class QVBoxLayout; -class QLabel; - -namespace cf::ui::example { - -class DoubleSpinBoxDemo : public QWidget { - Q_OBJECT - - public: - explicit DoubleSpinBoxDemo(QWidget* parent = nullptr); - ~DoubleSpinBoxDemo() override = default; - - QString title() const { return "DoubleSpinBox"; } - QString description() const { return "Material Design 3 DoubleSpinBox Component"; } - - private: - void setupUI(); - void createVariantsSection(); - void createStatesSection(); - void createPrecisionSection(); - void createValueRangeSection(); - void createStepSizeSection(); - void createInteractionDemoSection(); - QGroupBox* createGroupBox(const QString& title); - - void onValueValueChanged(double value); - void onPriceValueChanged(double value); - void onTemperatureValueChanged(double value); - - QWidget* scrollContent_; - QVBoxLayout* layout_; - QLabel* valueDisplayLabel_; - QLabel* priceDisplayLabel_; - QLabel* temperatureDisplayLabel_; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/GroupBoxDemo.cpp b/example/ui/widget/material_example/demos/GroupBoxDemo.cpp deleted file mode 100644 index 3f925d5bb..000000000 --- a/example/ui/widget/material_example/demos/GroupBoxDemo.cpp +++ /dev/null @@ -1,310 +0,0 @@ -/** - * @file GroupBoxDemo.cpp - * @brief Material Design 3 GroupBox Demo - Implementation - */ - -#include "GroupBoxDemo.h" - -#include "ui/widget/material/widget/button/button.h" -#include "ui/widget/material/widget/groupbox/groupbox.h" - -#include -#include -#include -#include -#include -#include - -using namespace cf::ui::widget::material; -using ButtonVariant = cf::ui::widget::material::Button::ButtonVariant; - -namespace cf::ui::example { - -GroupBoxDemo::GroupBoxDemo(QWidget* parent) : QWidget(parent) { - - setupUI(); - createBasicGroupBoxSection(); - createElevationSection(); - createBorderSection(); - createCornerRadiusSection(); - createContentExamplesSection(); - createNestedGroupBoxSection(); - - // Add stretch at the end of scroll content - layout_->addStretch(); -} - -void GroupBoxDemo::setupUI() { - // Scroll area for content - QScrollArea* scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - // Apply margins - scrollArea->setContentsMargins(24, 24, 24, 24); - - scrollContent_ = new QWidget(); - layout_ = new QVBoxLayout(scrollContent_); - layout_->setSpacing(24); - layout_->setContentsMargins(0, 0, 0, 0); - - scrollArea->setWidget(scrollContent_); - - // Main layout - QVBoxLayout* mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->addWidget(scrollArea, 1); -} - -void GroupBoxDemo::createBasicGroupBoxSection() { - GroupBox* groupBox = new GroupBox("Basic GroupBox (基本分组框)", scrollContent_); - QVBoxLayout* gbLayout = new QVBoxLayout(groupBox); - gbLayout->setSpacing(12); - gbLayout->setContentsMargins(16, 32, 16, 16); - - // Description label - QLabel* descLabel = - new QLabel("This is a basic Material Design 3 GroupBox with default settings.\n" - "It features rounded corners, a subtle elevation shadow, and a border.", - groupBox); - descLabel->setWordWrap(true); - gbLayout->addWidget(descLabel); - - // Example content - QHBoxLayout* hLayout = new QHBoxLayout(); - Button* button1 = new Button("Button 1", ButtonVariant::Filled, groupBox); - Button* button2 = new Button("Button 2", ButtonVariant::Outlined, groupBox); - hLayout->addWidget(button1); - hLayout->addWidget(button2); - hLayout->addStretch(); - gbLayout->addLayout(hLayout); - - layout_->addWidget(groupBox); -} - -void GroupBoxDemo::createElevationSection() { - // Section title - QLabel* sectionLabel = new QLabel("Elevation Levels (阴影级别)", scrollContent_); - QFont sectionFont("Segoe UI", 14, QFont::Bold); - sectionLabel->setFont(sectionFont); - layout_->addWidget(sectionLabel); - - // Grid layout for different elevations - QWidget* gridWidget = new QWidget(scrollContent_); - gridWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - QGridLayout* gridLayout = new QGridLayout(gridWidget); - gridLayout->setSpacing(16); - - // Add column stretch for even distribution - gridLayout->setColumnStretch(0, 1); - gridLayout->setColumnStretch(1, 1); - - // Create group boxes with different elevation levels - for (int level = 0; level <= 5; ++level) { - GroupBox* gb = new GroupBox(QString("Elevation %1").arg(level), gridWidget); - gb->setElevation(level); - - QVBoxLayout* gbLayout = new QVBoxLayout(gb); - QLabel* label = new QLabel(QString("Shadow level: %1").arg(level), gb); - gbLayout->addWidget(label); - - int row = level / 2; - int col = level % 2; - gridLayout->addWidget(gb, row, col); - } - - layout_->addWidget(gridWidget); -} - -void GroupBoxDemo::createBorderSection() { - // Section title - QLabel* sectionLabel = new QLabel("Border Variants (边框样式)", scrollContent_); - QFont sectionFont("Segoe UI", 14, QFont::Bold); - sectionLabel->setFont(sectionFont); - layout_->addWidget(sectionLabel); - - // Horizontal layout - QWidget* hWidget = new QWidget(scrollContent_); - QHBoxLayout* hLayout = new QHBoxLayout(hWidget); - hLayout->setSpacing(16); - - // With border (default) - GroupBox* withBorder = new GroupBox("With Border", hWidget); - withBorder->setHasBorder(true); - withBorder->setElevation(1); - QVBoxLayout* wbLayout = new QVBoxLayout(withBorder); - QLabel* wbLabel = new QLabel("This group box has a border.", withBorder); - wbLayout->addWidget(wbLabel); - hLayout->addWidget(withBorder); - - // Without border - GroupBox* noBorder = new GroupBox("Without Border", hWidget); - noBorder->setHasBorder(false); - noBorder->setElevation(2); - QVBoxLayout* nbLayout = new QVBoxLayout(noBorder); - QLabel* nbLabel = new QLabel("This group box has no border,\nonly elevation shadow.", noBorder); - nbLabel->setWordWrap(true); - nbLayout->addWidget(nbLabel); - hLayout->addWidget(noBorder); - - hLayout->addStretch(); - layout_->addWidget(hWidget); -} - -void GroupBoxDemo::createCornerRadiusSection() { - // Section title - QLabel* sectionLabel = new QLabel("Corner Radius (圆角半径)", scrollContent_); - QFont sectionFont("Segoe UI", 14, QFont::Bold); - sectionLabel->setFont(sectionFont); - layout_->addWidget(sectionLabel); - - // Horizontal layout - QWidget* hWidget = new QWidget(scrollContent_); - QHBoxLayout* hLayout = new QHBoxLayout(hWidget); - hLayout->setSpacing(16); - - // Small corner radius - GroupBox* smallRadius = new GroupBox("Small Radius (4dp)", hWidget); - smallRadius->setCornerRadius(4.0f); - smallRadius->setElevation(1); - QVBoxLayout* srLayout = new QVBoxLayout(smallRadius); - srLayout->addWidget(new QLabel("Subtle rounding", smallRadius)); - hLayout->addWidget(smallRadius); - - // Medium corner radius - GroupBox* mediumRadius = new GroupBox("Medium Radius (12dp)", hWidget); - mediumRadius->setCornerRadius(12.0f); - mediumRadius->setElevation(1); - QVBoxLayout* mrLayout = new QVBoxLayout(mediumRadius); - mrLayout->addWidget(new QLabel("Medium rounding", mediumRadius)); - hLayout->addWidget(mediumRadius); - - // Large corner radius - GroupBox* largeRadius = new GroupBox("Large Radius (20dp)", hWidget); - largeRadius->setCornerRadius(20.0f); - largeRadius->setElevation(1); - QVBoxLayout* lrLayout = new QVBoxLayout(largeRadius); - lrLayout->addWidget(new QLabel("Pronounced rounding", largeRadius)); - hLayout->addWidget(largeRadius); - - hLayout->addStretch(); - layout_->addWidget(hWidget); -} - -void GroupBoxDemo::createContentExamplesSection() { - // Section title - QLabel* sectionLabel = new QLabel("Content Examples (内容示例)", scrollContent_); - QFont sectionFont("Segoe UI", 14, QFont::Bold); - sectionLabel->setFont(sectionFont); - layout_->addWidget(sectionLabel); - - // Form controls example - GroupBox* formGroup = new GroupBox("Form Controls (表单控件)", scrollContent_); - formGroup->setElevation(1); - QGridLayout* formLayout = new QGridLayout(formGroup); - formLayout->setSpacing(12); - formLayout->setContentsMargins(16, 32, 16, 16); - - // Text input - formLayout->addWidget(new QLabel("Name:", formGroup), 0, 0); - formLayout->addWidget(new QLineEdit(formGroup), 0, 1); - - // Checkboxes - formLayout->addWidget(new QLabel("Options:", formGroup), 1, 0); - QVBoxLayout* checkLayout = new QVBoxLayout(); - checkLayout->addWidget(new QCheckBox("Enable feature A", formGroup)); - checkLayout->addWidget(new QCheckBox("Enable feature B", formGroup)); - formLayout->addLayout(checkLayout, 1, 1); - - // Radio buttons - formLayout->addWidget(new QLabel("Mode:", formGroup), 2, 0); - QVBoxLayout* radioLayout = new QVBoxLayout(); - QRadioButton* simpleModeRadio = new QRadioButton("Simple mode", formGroup); - radioLayout->addWidget(simpleModeRadio); - radioLayout->addWidget(new QRadioButton("Advanced mode", formGroup)); - simpleModeRadio->setChecked(true); // Select first - formLayout->addLayout(radioLayout, 2, 1); - - // Slider - formLayout->addWidget(new QLabel("Value:", formGroup), 3, 0); - QSlider* slider = new QSlider(Qt::Horizontal, formGroup); - slider->setRange(0, 100); - slider->setValue(50); - formLayout->addWidget(slider, 3, 1); - - // Spin box - formLayout->addWidget(new QLabel("Quantity:", formGroup), 4, 0); - formLayout->addWidget(new QSpinBox(formGroup), 4, 1); - - // Buttons - QHBoxLayout* buttonLayout = new QHBoxLayout(); - buttonLayout->addWidget(new Button("Submit", ButtonVariant::Filled, formGroup)); - buttonLayout->addWidget(new Button("Cancel", ButtonVariant::Outlined, formGroup)); - buttonLayout->addStretch(); - formLayout->addLayout(buttonLayout, 5, 0, 1, 2); - - layout_->addWidget(formGroup); - - // Settings example - GroupBox* settingsGroup = new GroupBox("Settings (设置)", scrollContent_); - settingsGroup->setElevation(2); - QVBoxLayout* settingsLayout = new QVBoxLayout(settingsGroup); - settingsLayout->setSpacing(12); - settingsLayout->setContentsMargins(16, 32, 16, 16); - - settingsLayout->addWidget(new QCheckBox("Show notifications", settingsGroup)); - settingsLayout->addWidget(new QCheckBox("Auto-save changes", settingsGroup)); - settingsLayout->addWidget(new QCheckBox("Enable dark mode", settingsGroup)); - settingsLayout->addWidget(new QCheckBox("Remember my preferences", settingsGroup)); - - settingsLayout->addStretch(); - - QHBoxLayout* actionLayout = new QHBoxLayout(); - actionLayout->addStretch(); - actionLayout->addWidget(new Button("Apply Settings", ButtonVariant::Tonal, settingsGroup)); - settingsLayout->addLayout(actionLayout); - - layout_->addWidget(settingsGroup); -} - -void GroupBoxDemo::createNestedGroupBoxSection() { - // Section title - QLabel* sectionLabel = new QLabel("Nested GroupBoxes (嵌套分组框)", scrollContent_); - QFont sectionFont("Segoe UI", 14, QFont::Bold); - sectionLabel->setFont(sectionFont); - layout_->addWidget(sectionLabel); - - // Outer group box - GroupBox* outerGroup = new GroupBox("Outer GroupBox", scrollContent_); - outerGroup->setElevation(1); - QVBoxLayout* outerLayout = new QVBoxLayout(outerGroup); - outerLayout->setSpacing(16); - outerLayout->setContentsMargins(16, 32, 16, 16); - - QLabel* outerLabel = - new QLabel("This is the outer group box containing nested group boxes:", outerGroup); - outerLayout->addWidget(outerLabel); - - // Inner group box 1 - GroupBox* inner1 = new GroupBox("Inner Group 1", outerGroup); - inner1->setElevation(2); - QVBoxLayout* inner1Layout = new QVBoxLayout(inner1); - inner1Layout->addWidget(new QLabel("Content in nested group box 1", inner1)); - outerLayout->addWidget(inner1); - - // Inner group box 2 - GroupBox* inner2 = new GroupBox("Inner Group 2", outerGroup); - inner2->setElevation(2); - QVBoxLayout* inner2Layout = new QVBoxLayout(inner2); - inner2Layout->addWidget(new QLabel("Content in nested group box 2", inner2)); - QHBoxLayout* buttonLayout = new QHBoxLayout(); - buttonLayout->addWidget(new Button("OK", ButtonVariant::Filled, inner2)); - buttonLayout->addWidget(new Button("Close", ButtonVariant::Text, inner2)); - inner2Layout->addLayout(buttonLayout); - outerLayout->addWidget(inner2); - - layout_->addWidget(outerGroup); -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/GroupBoxDemo.h b/example/ui/widget/material_example/demos/GroupBoxDemo.h deleted file mode 100644 index 03f7c0495..000000000 --- a/example/ui/widget/material_example/demos/GroupBoxDemo.h +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @file GroupBoxDemo.h - * @brief Material Design 3 GroupBox Demo Widget - */ - -#pragma once - -#include -#include -#include -#include - -namespace cf::ui::example { - -/** - * @brief Demo widget for Material Design 3 GroupBox component. - * - * Displays various GroupBox configurations: - * - Different elevation levels (0-5) - * - With and without border - * - Different corner radius values - * - Content examples with various widgets - * - Nested group boxes - */ -class GroupBoxDemo : public QWidget { - Q_OBJECT - - public: - explicit GroupBoxDemo(QWidget* parent = nullptr); - ~GroupBoxDemo() override = default; - - QString title() const { return "GroupBox"; } - QString description() const { return "Material Design 3 GroupBox Component"; } - - private: - void setupUI(); - void createBasicGroupBoxSection(); - void createElevationSection(); - void createBorderSection(); - void createCornerRadiusSection(); - void createContentExamplesSection(); - void createNestedGroupBoxSection(); - - private: - QWidget* scrollContent_; - QVBoxLayout* layout_; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/LabelDemo.cpp b/example/ui/widget/material_example/demos/LabelDemo.cpp deleted file mode 100644 index edce306fc..000000000 --- a/example/ui/widget/material_example/demos/LabelDemo.cpp +++ /dev/null @@ -1,322 +0,0 @@ -/** - * @file LabelDemo.cpp - * @brief Material Design 3 Label Demo - Implementation - */ - -#include "LabelDemo.h" - -#include "ui/widget/material/widget/label/label.h" -#include -#include -#include - -using namespace cf::ui::widget::material; -using TypographyStyle = cf::ui::widget::material::TypographyStyle; -using LabelColorVariant = cf::ui::widget::material::LabelColorVariant; - -namespace cf::ui::example { - -LabelDemo::LabelDemo(QWidget* parent) : QWidget(parent) { - - setupUI(); - createDisplaySection(); - createHeadlineSection(); - createTitleSection(); - createBodySection(); - createLabelSection(); - createColorVariantsSection(); - createStatesSection(); - createFeaturesSection(); - - // Add stretch at the end -} - -void LabelDemo::setupUI() { - // Scroll area for content - QScrollArea* scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - // Apply margins - scrollArea->setContentsMargins(24, 24, 24, 24); - - scrollContent_ = new QWidget(); - layout_ = new QVBoxLayout(scrollContent_); - layout_->setSpacing(24); - layout_->setContentsMargins(0, 0, 0, 0); - - scrollArea->setWidget(scrollContent_); - - // Main layout - QVBoxLayout* mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->addWidget(scrollArea, 1); -} - -void LabelDemo::createDisplaySection() { - QGroupBox* groupBox = createGroupBox("Display Styles (展示样式)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Display Large - 57sp - Label* displayLarge = new Label("Display Large", TypographyStyle::DisplayLarge, scrollContent_); - displayLarge->setColorVariant(LabelColorVariant::OnSurface); - layout->addWidget(displayLarge); - - // Display Medium - 45sp - Label* displayMedium = - new Label("Display Medium", TypographyStyle::DisplayMedium, scrollContent_); - displayMedium->setColorVariant(LabelColorVariant::OnSurface); - layout->addWidget(displayMedium); - - // Display Small - 36sp - Label* displaySmall = new Label("Display Small", TypographyStyle::DisplaySmall, scrollContent_); - displaySmall->setColorVariant(LabelColorVariant::OnSurface); - layout->addWidget(displaySmall); - - layout_->addWidget(groupBox); -} - -void LabelDemo::createHeadlineSection() { - QGroupBox* groupBox = createGroupBox("Headline Styles (标题样式)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Headline Large - 32sp - Label* headlineLarge = - new Label("Headline Large", TypographyStyle::HeadlineLarge, scrollContent_); - headlineLarge->setColorVariant(LabelColorVariant::OnSurface); - layout->addWidget(headlineLarge); - - // Headline Medium - 28sp - Label* headlineMedium = - new Label("Headline Medium", TypographyStyle::HeadlineMedium, scrollContent_); - headlineMedium->setColorVariant(LabelColorVariant::OnSurface); - layout->addWidget(headlineMedium); - - // Headline Small - 24sp - Label* headlineSmall = - new Label("Headline Small", TypographyStyle::HeadlineSmall, scrollContent_); - headlineSmall->setColorVariant(LabelColorVariant::OnSurface); - layout->addWidget(headlineSmall); - - layout_->addWidget(groupBox); -} - -void LabelDemo::createTitleSection() { - QGroupBox* groupBox = createGroupBox("Title Styles (标题样式)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Title Large - 22sp - Label* titleLarge = new Label("Title Large", TypographyStyle::TitleLarge, scrollContent_); - titleLarge->setColorVariant(LabelColorVariant::OnSurface); - layout->addWidget(titleLarge); - - // Title Medium - 16sp - Label* titleMedium = new Label("Title Medium", TypographyStyle::TitleMedium, scrollContent_); - titleMedium->setColorVariant(LabelColorVariant::OnSurface); - layout->addWidget(titleMedium); - - // Title Small - 14sp - Label* titleSmall = new Label("Title Small", TypographyStyle::TitleSmall, scrollContent_); - titleSmall->setColorVariant(LabelColorVariant::OnSurface); - layout->addWidget(titleSmall); - - layout_->addWidget(groupBox); -} - -void LabelDemo::createBodySection() { - QGroupBox* groupBox = createGroupBox("Body Styles (正文样式)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Body Large - 16sp - Label* bodyLarge = - new Label("Body Large - Main content text", TypographyStyle::BodyLarge, scrollContent_); - bodyLarge->setColorVariant(LabelColorVariant::OnSurface); - layout->addWidget(bodyLarge); - - // Body Medium - 14sp - Label* bodyMedium = new Label("Body Medium - Default content text", TypographyStyle::BodyMedium, - scrollContent_); - bodyMedium->setColorVariant(LabelColorVariant::OnSurface); - layout->addWidget(bodyMedium); - - // Body Small - 12sp - Label* bodySmall = new Label("Body Small - Secondary content text", TypographyStyle::BodySmall, - scrollContent_); - bodySmall->setColorVariant(LabelColorVariant::OnSurfaceVariant); - layout->addWidget(bodySmall); - - layout_->addWidget(groupBox); -} - -void LabelDemo::createLabelSection() { - QGroupBox* groupBox = createGroupBox("Label Styles (标签样式)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Label Large - 14sp - Label* labelLarge = - new Label("Label Large - Button text", TypographyStyle::LabelLarge, scrollContent_); - labelLarge->setColorVariant(LabelColorVariant::Primary); - layout->addWidget(labelLarge); - - // Label Medium - 12sp - Label* labelMedium = - new Label("Label Medium - Caption text", TypographyStyle::LabelMedium, scrollContent_); - labelMedium->setColorVariant(LabelColorVariant::OnSurfaceVariant); - layout->addWidget(labelMedium); - - // Label Small - 11sp - Label* labelSmall = - new Label("Label Small - Helper text", TypographyStyle::LabelSmall, scrollContent_); - labelSmall->setColorVariant(LabelColorVariant::OnSurfaceVariant); - layout->addWidget(labelSmall); - - layout_->addWidget(groupBox); -} - -void LabelDemo::createColorVariantsSection() { - QGroupBox* groupBox = createGroupBox("Color Variants (颜色变体)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Create a container widget for the grid - QWidget* gridContainer = new QWidget(groupBox); - QGridLayout* gridLayout = new QGridLayout(gridContainer); - gridLayout->setSpacing(12); - gridLayout->setContentsMargins(0, 0, 0, 0); - - // Row 1: OnSurface variants - gridLayout->addWidget(new QLabel("OnSurface:", gridContainer), 0, 0); - Label* onSurface = new Label("Default Text", TypographyStyle::BodyMedium, gridContainer); - onSurface->setColorVariant(LabelColorVariant::OnSurface); - gridLayout->addWidget(onSurface, 0, 1); - - gridLayout->addWidget(new QLabel("OnSurfaceVariant:", gridContainer), 0, 2); - Label* onSurfaceVariant = - new Label("Secondary Text", TypographyStyle::BodyMedium, gridContainer); - onSurfaceVariant->setColorVariant(LabelColorVariant::OnSurfaceVariant); - gridLayout->addWidget(onSurfaceVariant, 0, 3); - - // Row 2: Primary variants - gridLayout->addWidget(new QLabel("Primary:", gridContainer), 1, 0); - Label* primary = new Label("Primary Color", TypographyStyle::BodyMedium, gridContainer); - primary->setColorVariant(LabelColorVariant::Primary); - gridLayout->addWidget(primary, 1, 1); - - gridLayout->addWidget(new QLabel("OnPrimary:", gridContainer), 1, 2); - Label* onPrimary = new Label("On Primary", TypographyStyle::BodyMedium, gridContainer); - onPrimary->setColorVariant(LabelColorVariant::OnPrimary); - gridLayout->addWidget(onPrimary, 1, 3); - - // Row 3: Secondary variants - gridLayout->addWidget(new QLabel("Secondary:", gridContainer), 2, 0); - Label* secondary = new Label("Secondary", TypographyStyle::BodyMedium, gridContainer); - secondary->setColorVariant(LabelColorVariant::Secondary); - gridLayout->addWidget(secondary, 2, 1); - - gridLayout->addWidget(new QLabel("OnSecondary:", gridContainer), 2, 2); - Label* onSecondary = new Label("On Secondary", TypographyStyle::BodyMedium, gridContainer); - onSecondary->setColorVariant(LabelColorVariant::OnSecondary); - gridLayout->addWidget(onSecondary, 2, 3); - - // Row 4: Error variants - gridLayout->addWidget(new QLabel("Error:", gridContainer), 3, 0); - Label* error = new Label("Error Message", TypographyStyle::BodyMedium, gridContainer); - error->setColorVariant(LabelColorVariant::Error); - gridLayout->addWidget(error, 3, 1); - - gridLayout->addWidget(new QLabel("OnError:", gridContainer), 3, 2); - Label* onError = new Label("On Error", TypographyStyle::BodyMedium, gridContainer); - onError->setColorVariant(LabelColorVariant::OnError); - gridLayout->addWidget(onError, 3, 3); - - layout->addWidget(gridContainer); - layout_->addWidget(groupBox); -} - -void LabelDemo::createStatesSection() { - QGroupBox* groupBox = createGroupBox("States (状态)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Enabled state - QHBoxLayout* enabledLayout = new QHBoxLayout(); - QLabel* enabledLabel = new QLabel("Enabled:", scrollContent_); - enabledLabel->setMinimumWidth(100); - Label* enabledLabel2 = - new Label("This label is enabled", TypographyStyle::BodyMedium, scrollContent_); - enabledLabel2->setColorVariant(LabelColorVariant::OnSurface); - enabledLayout->addWidget(enabledLabel); - enabledLayout->addWidget(enabledLabel2); - enabledLayout->addStretch(); - layout->addLayout(enabledLayout); - - // Disabled state - QHBoxLayout* disabledLayout = new QHBoxLayout(); - QLabel* disabledLabel = new QLabel("Disabled:", scrollContent_); - disabledLabel->setMinimumWidth(100); - Label* disabledLabel2 = - new Label("This label is disabled", TypographyStyle::BodyMedium, scrollContent_); - disabledLabel2->setColorVariant(LabelColorVariant::OnSurface); - disabledLabel2->setEnabled(false); - disabledLayout->addWidget(disabledLabel); - disabledLayout->addWidget(disabledLabel2); - disabledLayout->addStretch(); - layout->addLayout(disabledLayout); - - layout_->addWidget(groupBox); -} - -void LabelDemo::createFeaturesSection() { - QGroupBox* groupBox = createGroupBox("Features (特性)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Auto-hiding demo - QHBoxLayout* autoHideLayout = new QHBoxLayout(); - QLabel* autoHideLabel = new QLabel("Auto-Hiding:", scrollContent_); - autoHideLabel->setMinimumWidth(120); - Label* autoHideLabel2 = - new Label("Visible (has text)", TypographyStyle::BodyMedium, scrollContent_); - autoHideLabel2->setAutoHiding(true); - autoHideLayout->addWidget(autoHideLabel); - autoHideLayout->addWidget(autoHideLabel2); - autoHideLayout->addStretch(); - layout->addLayout(autoHideLayout); - - // Empty label with auto-hiding - QHBoxLayout* emptyLayout = new QHBoxLayout(); - QLabel* emptyLabel = new QLabel("Empty (Hidden):", scrollContent_); - emptyLabel->setMinimumWidth(120); - Label* emptyAutoHideLabel = new Label("", TypographyStyle::BodyMedium, scrollContent_); - emptyAutoHideLabel->setAutoHiding(true); - emptyAutoHideLabel->setColorVariant(LabelColorVariant::OnSurfaceVariant); - emptyLayout->addWidget(emptyLabel); - emptyLayout->addWidget(emptyAutoHideLabel); - emptyLayout->addStretch(); - layout->addLayout(emptyLayout); - - // Word wrap demo - QHBoxLayout* wrapLayout = new QHBoxLayout(); - QLabel* wrapLabel = new QLabel("Word Wrap:", scrollContent_); - wrapLabel->setMinimumWidth(120); - Label* wrapLabel2 = - new Label("This is a long text that will wrap when the label width is constrained", - TypographyStyle::BodyMedium, scrollContent_); - wrapLabel2->setColorVariant(LabelColorVariant::OnSurface); - wrapLabel2->setWordWrap(true); - wrapLabel2->setMaximumWidth(300); - wrapLayout->addWidget(wrapLabel); - wrapLayout->addWidget(wrapLabel2); - wrapLayout->addStretch(); - layout->addLayout(wrapLayout); - - layout_->addWidget(groupBox); -} - -QGroupBox* LabelDemo::createGroupBox(const QString& title) { - QGroupBox* groupBox = new QGroupBox(title, scrollContent_); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(8); - layout->setContentsMargins(16, 24, 16, 16); - return groupBox; -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/LabelDemo.h b/example/ui/widget/material_example/demos/LabelDemo.h deleted file mode 100644 index c6eb416c6..000000000 --- a/example/ui/widget/material_example/demos/LabelDemo.h +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @file LabelDemo.h - * @brief Material Design 3 Label Demo Widget - */ - -#pragma once - -#include -#include -#include -#include -#include -class QGroupBox; - -namespace cf::ui::example { - -/** - * @brief Demo widget for Material Design 3 Label component. - * - * Displays all 15 Material Design 3 typography styles: - * - Display Large/Medium/Small (hero content) - * - Headline Large/Medium/Small (app bar text) - * - Title Large/Medium/Small (section headings) - * - Body Large/Medium/Small (main content) - * - Label Large/Medium/Small (secondary information) - * - * Also demonstrates: - * - Different color variants (OnSurface, Primary, Error, etc.) - * - Disabled state - * - Auto-hiding behavior - * - Word wrap functionality - */ -class LabelDemo : public QWidget { - Q_OBJECT - - public: - explicit LabelDemo(QWidget* parent = nullptr); - ~LabelDemo() override = default; - - QString title() const { return "Label"; } - QString description() const { return "Material Design 3 Typography Component"; } - - private: - void setupUI(); - void createDisplaySection(); - void createHeadlineSection(); - void createTitleSection(); - void createBodySection(); - void createLabelSection(); - void createColorVariantsSection(); - void createStatesSection(); - void createFeaturesSection(); - - QGroupBox* createGroupBox(const QString& title); - - private: - QWidget* scrollContent_; - QVBoxLayout* layout_; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/ListViewDemo.cpp b/example/ui/widget/material_example/demos/ListViewDemo.cpp deleted file mode 100644 index db6c5a4a5..000000000 --- a/example/ui/widget/material_example/demos/ListViewDemo.cpp +++ /dev/null @@ -1,219 +0,0 @@ -/** - * @file ListViewDemo.cpp - * @brief Material Design 3 ListView Demo - Implementation - */ - -#include "ListViewDemo.h" - -#include "ui/widget/material/widget/listview/listview.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace cf::ui::widget::material; -using ItemHeight = cf::ui::widget::material::ListView::ItemHeight; - -namespace cf::ui::example { - -ListViewDemo::ListViewDemo(QWidget* parent) : QWidget(parent) { - - setupUI(); - createBasicListSection(); - createSingleSelectionSection(); - createMultiSelectionSection(); - createWithIconsSection(); - createDifferentItemHeightsSection(); -} - -void ListViewDemo::setupUI() { - layout_ = new QVBoxLayout(this); - layout_->setContentsMargins(24, 24, 24, 24); - layout_->setSpacing(16); - - // Scroll area for content - QScrollArea* scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scrollContent_ = new QWidget(); - QVBoxLayout* scrollLayout = new QVBoxLayout(scrollContent_); - scrollLayout->setSpacing(24); - scrollLayout->setContentsMargins(0, 0, 0, 0); - - scrollArea->setWidget(scrollContent_); - layout_->addWidget(scrollArea, 1); -} - -void ListViewDemo::createBasicListSection() { - QGroupBox* groupBox = createGroupBox("Basic List (基础列表)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Basic list view - ListView* listView = new ListView(this); - listView->setMinimumHeight(200); - listView->setShowSeparator(true); - - QStringListModel* model = new QStringListModel(this); - model->setStringList({"Item 1", "Item 2", "Item 3", "Item 4", "Item 5"}); - listView->setModel(model); - - layout->addWidget(listView); - scrollContent_->layout()->addWidget(groupBox); -} - -void ListViewDemo::createSingleSelectionSection() { - QGroupBox* groupBox = createGroupBox("Single Selection (单选列表)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Single selection list view - ListView* listView = new ListView(this); - listView->setMinimumHeight(200); - listView->setShowSeparator(true); - listView->setSelectionMode(QAbstractItemView::SingleSelection); - - QStringListModel* model = new QStringListModel(this); - model->setStringList({"Option A", "Option B", "Option C", "Option D", "Option E"}); - listView->setModel(model); - - layout->addWidget(listView); - - // Info label - QLabel* infoLabel = - new QLabel("Click an item to select it (only one item can be selected)", this); - infoLabel->setWordWrap(true); - layout->addWidget(infoLabel); - - scrollContent_->layout()->addWidget(groupBox); -} - -void ListViewDemo::createMultiSelectionSection() { - QGroupBox* groupBox = createGroupBox("Multi Selection (多选列表)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Multi selection list view - ListView* listView = new ListView(this); - listView->setMinimumHeight(200); - listView->setShowSeparator(true); - listView->setSelectionMode(QAbstractItemView::MultiSelection); - - QStringListModel* model = new QStringListModel(this); - model->setStringList( - {"Selection 1", "Selection 2", "Selection 3", "Selection 4", "Selection 5", "Selection 6"}); - listView->setModel(model); - - layout->addWidget(listView); - - // Info label - QLabel* infoLabel = - new QLabel("Click multiple items to select them (Ctrl+Click for individual items)", this); - infoLabel->setWordWrap(true); - layout->addWidget(infoLabel); - - scrollContent_->layout()->addWidget(groupBox); -} - -void ListViewDemo::createWithIconsSection() { - QGroupBox* groupBox = createGroupBox("List with Icons (带图标的列表)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // List view with icons - ListView* listView = new ListView(this); - listView->setMinimumHeight(200); - listView->setShowSeparator(true); - - QStandardItemModel* model = new QStandardItemModel(this); - - // Add items with icons - QList items; - items << new QStandardItem(QApplication::style()->standardIcon(QStyle::SP_DirIcon), - "Documents"); - items << new QStandardItem(QApplication::style()->standardIcon(QStyle::SP_FileIcon), "Files"); - items << new QStandardItem(QApplication::style()->standardIcon(QStyle::SP_ComputerIcon), - "Computer"); - items << new QStandardItem(QApplication::style()->standardIcon(QStyle::SP_DriveHDIcon), - "Hard Drive"); - items << new QStandardItem(QApplication::style()->standardIcon(QStyle::SP_DriveCDIcon), - "CD-ROM"); - - model->appendColumn(items); - listView->setModel(model); - - layout->addWidget(listView); - scrollContent_->layout()->addWidget(groupBox); -} - -void ListViewDemo::createDifferentItemHeightsSection() { - QGroupBox* groupBox = new QGroupBox("Different Item Heights (不同项目高度)", this); - QGridLayout* gridLayout = new QGridLayout(groupBox); - gridLayout->setSpacing(16); - gridLayout->setContentsMargins(16, 24, 16, 16); - - // Single Line items (56dp) - QLabel* singleLineLabel = new QLabel("Single Line (56dp):", this); - ListView* singleLineList = new ListView(this); - singleLineList->setMinimumHeight(180); - singleLineList->setShowSeparator(true); - singleLineList->setItemHeight(ItemHeight::SingleLine); - - QStringListModel* singleLineModel = new QStringListModel(this); - singleLineModel->setStringList({"Item One", "Item Two", "Item Three"}); - singleLineList->setModel(singleLineModel); - - gridLayout->addWidget(singleLineLabel, 0, 0); - gridLayout->addWidget(singleLineList, 1, 0); - - // Two Line items (72dp) - QLabel* twoLineLabel = new QLabel("Two Line (72dp):", this); - ListView* twoLineList = new ListView(this); - twoLineList->setMinimumHeight(180); - twoLineList->setShowSeparator(true); - twoLineList->setItemHeight(ItemHeight::TwoLine); - - QStringListModel* twoLineModel = new QStringListModel(this); - twoLineModel->setStringList({"Primary text\nSecondary text description", - "Another primary\nMore secondary info", - "Third primary\nFinal secondary line"}); - twoLineList->setModel(twoLineModel); - - gridLayout->addWidget(twoLineLabel, 0, 1); - gridLayout->addWidget(twoLineList, 1, 1); - - // Three Line items (88dp) - QLabel* threeLineLabel = new QLabel("Three Line (88dp):", this); - ListView* threeLineList = new ListView(this); - threeLineList->setMinimumHeight(180); - threeLineList->setShowSeparator(true); - threeLineList->setItemHeight(ItemHeight::ThreeLine); - - QStringListModel* threeLineModel = new QStringListModel(this); - threeLineModel->setStringList({"Primary heading\nSecondary description\nThird line of text", - "Another heading\nMore description here\nExtra detail line", - "Last heading\nFinal description\nConcluding line"}); - threeLineList->setModel(threeLineModel); - - gridLayout->addWidget(threeLineLabel, 2, 0); - gridLayout->addWidget(threeLineList, 3, 0); - - gridLayout->setColumnStretch(0, 1); - gridLayout->setColumnStretch(1, 1); - - scrollContent_->layout()->addWidget(groupBox); -} - -QGroupBox* ListViewDemo::createGroupBox(const QString& title) { - QGroupBox* groupBox = new QGroupBox(title, this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - return groupBox; -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/ListViewDemo.h b/example/ui/widget/material_example/demos/ListViewDemo.h deleted file mode 100644 index a0fa3e7e0..000000000 --- a/example/ui/widget/material_example/demos/ListViewDemo.h +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @file ListViewDemo.h - * @brief Material Design 3 ListView Demo - Widget Component - */ - -#pragma once - -#include -#include -#include -class QGroupBox; -class QStringListModel; -class QStandardItemModel; - -namespace cf::ui::example { - -class ListViewDemo : public QWidget { - Q_OBJECT - - public: - explicit ListViewDemo(QWidget* parent = nullptr); - ~ListViewDemo() override = default; - - QString title() const { return "ListView"; } - QString description() const { return "Material Design 3 ListView Component"; } - - private: - void setupUI(); - void createBasicListSection(); - void createSingleSelectionSection(); - void createMultiSelectionSection(); - void createWithIconsSection(); - void createDifferentItemHeightsSection(); - QGroupBox* createGroupBox(const QString& title); - - QWidget* scrollContent_; - QVBoxLayout* layout_; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/ProgressBarDemo.cpp b/example/ui/widget/material_example/demos/ProgressBarDemo.cpp deleted file mode 100644 index f75bfba30..000000000 --- a/example/ui/widget/material_example/demos/ProgressBarDemo.cpp +++ /dev/null @@ -1,268 +0,0 @@ -/** - * @file ProgressBarDemo.cpp - * @brief Material Design 3 ProgressBar Demo - Implementation - */ - -#include "ProgressBarDemo.h" - -#include "ui/widget/material/widget/progressbar/progressbar.h" - -#include -#include -#include - -using namespace cf::ui::widget::material; - -namespace cf::ui::example { - -ProgressBarDemo::ProgressBarDemo(QWidget* parent) : QWidget(parent) { - - setupUI(); - createDeterminateSection(); - createIndeterminateSection(); - createStatesSection(); - createInteractiveDemoSection(); - - // Add stretch at the end -} - -ProgressBarDemo::~ProgressBarDemo() { - if (progressTimer_) { - progressTimer_->stop(); - } -} - -void ProgressBarDemo::setupUI() { - // Scroll area for content - QScrollArea* scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - // Apply margins - scrollArea->setContentsMargins(24, 24, 24, 24); - - scrollContent_ = new QWidget(); - layout_ = new QVBoxLayout(scrollContent_); - layout_->setSpacing(24); - layout_->setContentsMargins(0, 0, 0, 0); - - scrollArea->setWidget(scrollContent_); - - // Main layout - QVBoxLayout* mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->addWidget(scrollArea, 1); -} - -void ProgressBarDemo::createDeterminateSection() { - QGroupBox* groupBox = createGroupBox("Determinate Progress Bars (确定进度条)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // 0% - QHBoxLayout* layout0 = new QHBoxLayout(); - QLabel* label0 = new QLabel("0%:", scrollContent_); - label0->setMinimumWidth(80); - ProgressBar* pb0 = new ProgressBar(scrollContent_); - pb0->setValue(0); - pb0->setTextVisible(true); - layout0->addWidget(label0); - layout0->addWidget(pb0); - layout->addLayout(layout0); - - // 25% - QHBoxLayout* layout25 = new QHBoxLayout(); - QLabel* label25 = new QLabel("25%:", scrollContent_); - label25->setMinimumWidth(80); - ProgressBar* pb25 = new ProgressBar(scrollContent_); - pb25->setValue(25); - pb25->setTextVisible(true); - layout25->addWidget(label25); - layout25->addWidget(pb25); - layout->addLayout(layout25); - - // 50% - QHBoxLayout* layout50 = new QHBoxLayout(); - QLabel* label50 = new QLabel("50%:", scrollContent_); - label50->setMinimumWidth(80); - ProgressBar* pb50 = new ProgressBar(scrollContent_); - pb50->setValue(50); - pb50->setTextVisible(true); - layout50->addWidget(label50); - layout50->addWidget(pb50); - layout->addLayout(layout50); - - // 75% - QHBoxLayout* layout75 = new QHBoxLayout(); - QLabel* label75 = new QLabel("75%:", scrollContent_); - label75->setMinimumWidth(80); - ProgressBar* pb75 = new ProgressBar(scrollContent_); - pb75->setValue(75); - pb75->setTextVisible(true); - layout75->addWidget(label75); - layout75->addWidget(pb75); - layout->addLayout(layout75); - - // 100% - QHBoxLayout* layout100 = new QHBoxLayout(); - QLabel* label100 = new QLabel("100%:", scrollContent_); - label100->setMinimumWidth(80); - ProgressBar* pb100 = new ProgressBar(scrollContent_); - pb100->setValue(100); - pb100->setTextVisible(true); - layout100->addWidget(label100); - layout100->addWidget(pb100); - layout->addLayout(layout100); - - layout_->addWidget(groupBox); -} - -void ProgressBarDemo::createIndeterminateSection() { - QGroupBox* groupBox = createGroupBox("Indeterminate Progress Bar (不确定进度条)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - QHBoxLayout* hLayout = new QHBoxLayout(); - QLabel* label = new QLabel("Loading...", scrollContent_); - label->setMinimumWidth(80); - - // Indeterminate: min=0, max=0 - ProgressBar* pb = new ProgressBar(scrollContent_); - pb->setRange(0, 0); - pb->setTextVisible(false); - - hLayout->addWidget(label); - hLayout->addWidget(pb); - layout->addLayout(hLayout); - - layout_->addWidget(groupBox); -} - -void ProgressBarDemo::createStatesSection() { - QGroupBox* groupBox = createGroupBox("Progress Bar States (进度条状态)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Normal (enabled) - QHBoxLayout* normalLayout = new QHBoxLayout(); - QLabel* normalLabel = new QLabel("Normal:", scrollContent_); - normalLabel->setMinimumWidth(80); - ProgressBar* normalPb = new ProgressBar(scrollContent_); - normalPb->setValue(60); - normalLayout->addWidget(normalLabel); - normalLayout->addWidget(normalPb); - layout->addLayout(normalLayout); - - // Disabled - QHBoxLayout* disabledLayout = new QHBoxLayout(); - QLabel* disabledLabel = new QLabel("Disabled:", scrollContent_); - disabledLabel->setMinimumWidth(80); - ProgressBar* disabledPb = new ProgressBar(scrollContent_); - disabledPb->setValue(60); - disabledPb->setEnabled(false); - disabledLayout->addWidget(disabledLabel); - disabledLayout->addWidget(disabledPb); - layout->addLayout(disabledLayout); - - // Without text - QHBoxLayout* noTextLayout = new QHBoxLayout(); - QLabel* noTextLabel = new QLabel("No Text:", scrollContent_); - noTextLabel->setMinimumWidth(80); - ProgressBar* noTextPb = new ProgressBar(scrollContent_); - noTextPb->setValue(80); - noTextPb->setTextVisible(false); - noTextLayout->addWidget(noTextLabel); - noTextLayout->addWidget(noTextPb); - layout->addLayout(noTextLayout); - - layout_->addWidget(groupBox); -} - -void ProgressBarDemo::createInteractiveDemoSection() { - QGroupBox* groupBox = createGroupBox("Interactive Demo (交互演示)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Progress bar - demoProgressBar_ = new ProgressBar(scrollContent_); - demoProgressBar_->setValue(0); - demoProgressBar_->setTextVisible(true); - layout->addWidget(demoProgressBar_); - - // Control buttons - QHBoxLayout* buttonLayout = new QHBoxLayout(); - - QPushButton* decrementBtn = new QPushButton("-10", scrollContent_); - QPushButton* incrementBtn = new QPushButton("+10", scrollContent_); - QPushButton* resetBtn = new QPushButton("Reset", scrollContent_); - - connect(decrementBtn, &QPushButton::clicked, this, &ProgressBarDemo::onDecrementClicked); - connect(incrementBtn, &QPushButton::clicked, this, &ProgressBarDemo::onIncrementClicked); - connect(resetBtn, &QPushButton::clicked, this, &ProgressBarDemo::onResetClicked); - - buttonLayout->addStretch(); - buttonLayout->addWidget(decrementBtn); - buttonLayout->addWidget(incrementBtn); - buttonLayout->addWidget(resetBtn); - buttonLayout->addStretch(); - - layout->addLayout(buttonLayout); - - // Auto-progress button and timer - QHBoxLayout* autoLayout = new QHBoxLayout(); - QPushButton* autoBtn = new QPushButton("Start Auto Progress", scrollContent_); - connect(autoBtn, &QPushButton::clicked, this, [this, autoBtn]() { - if (!progressTimer_) { - progressTimer_ = new QTimer(this); - connect(progressTimer_, &QTimer::timeout, this, &ProgressBarDemo::updateProgress); - } - - if (progressTimer_->isActive()) { - progressTimer_->stop(); - autoBtn->setText("Start Auto Progress"); - } else { - progressTimer_->start(50); // Update every 50ms - autoBtn->setText("Stop Auto Progress"); - } - }); - - autoLayout->addStretch(); - autoLayout->addWidget(autoBtn); - autoLayout->addStretch(); - layout->addLayout(autoLayout); - - layout_->addWidget(groupBox); -} - -void ProgressBarDemo::onIncrementClicked() { - demoValue_ = qMin(demoValue_ + 10, 100); - demoProgressBar_->setValue(demoValue_); -} - -void ProgressBarDemo::onDecrementClicked() { - demoValue_ = qMax(demoValue_ - 10, 0); - demoProgressBar_->setValue(demoValue_); -} - -void ProgressBarDemo::onResetClicked() { - demoValue_ = 0; - demoProgressBar_->setValue(0); - if (progressTimer_) { - progressTimer_->stop(); - } -} - -void ProgressBarDemo::updateProgress() { - demoValue_ += 1; - if (demoValue_ > 100) { - demoValue_ = 0; - } - demoProgressBar_->setValue(demoValue_); -} - -QGroupBox* ProgressBarDemo::createGroupBox(const QString& title) { - QGroupBox* groupBox = new QGroupBox(title, scrollContent_); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - return groupBox; -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/ProgressBarDemo.h b/example/ui/widget/material_example/demos/ProgressBarDemo.h deleted file mode 100644 index 6a5d71bb4..000000000 --- a/example/ui/widget/material_example/demos/ProgressBarDemo.h +++ /dev/null @@ -1,66 +0,0 @@ -/** - * @file ProgressBarDemo.h - * @brief Material Design 3 ProgressBar Demo Widget - */ - -#pragma once - -#include -#include -#include -#include -#include - -class QGroupBox; - -namespace cf::ui::widget::material { -class ProgressBar; -} - -namespace cf::ui::example { - -/** - * @brief Demo widget for Material Design 3 ProgressBar component. - * - * Displays: - * - Determinate progress bars with different values - * - Indeterminate progress bar (animated) - * - Disabled state - * - Interactive demo with increment/decrement buttons - */ -class ProgressBarDemo : public QWidget { - Q_OBJECT - - public: - explicit ProgressBarDemo(QWidget* parent = nullptr); - ~ProgressBarDemo() override; - - QString title() const { return "ProgressBar"; } - QString description() const { return "Material Design 3 Progress Indicator Component"; } - - private: - void setupUI(); - void createDeterminateSection(); - void createIndeterminateSection(); - void createStatesSection(); - void createInteractiveDemoSection(); - - QGroupBox* createGroupBox(const QString& title); - - private slots: - void onIncrementClicked(); - void onDecrementClicked(); - void onResetClicked(); - void updateProgress(); - - private: - QWidget* scrollContent_; - QVBoxLayout* layout_; - - // Interactive demo - widget::material::ProgressBar* demoProgressBar_; - QTimer* progressTimer_{nullptr}; - int demoValue_ = 0; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/RadioButtonDemo.cpp b/example/ui/widget/material_example/demos/RadioButtonDemo.cpp deleted file mode 100644 index d08ae4408..000000000 --- a/example/ui/widget/material_example/demos/RadioButtonDemo.cpp +++ /dev/null @@ -1,255 +0,0 @@ -/** - * @file RadioButtonDemo.cpp - * @brief Material Design 3 RadioButton Demo - Implementation - */ - -#include "RadioButtonDemo.h" - -#include "ui/widget/material/widget/radiobutton/radiobutton.h" - -#include -#include -#include -#include -#include -#include - -using namespace cf::ui::widget::material; - -namespace cf::ui::example { - -RadioButtonDemo::RadioButtonDemo(QWidget* parent) : QWidget(parent) { - setupUI(); - createBasicRadioSection(); - createRadioStatesSection(); - createButtonGroupSection(); - createErrorStateSection(); - createInteractionDemoSection(); -} - -void RadioButtonDemo::setupUI() { - layout_ = new QVBoxLayout(this); - layout_->setContentsMargins(16, 16, 16, 16); - layout_->setSpacing(16); - - scrollContent_ = new QWidget(); - QVBoxLayout* scrollLayout = new QVBoxLayout(scrollContent_); - scrollLayout->setSpacing(24); - scrollLayout->setContentsMargins(0, 0, 0, 0); - - QScrollArea* scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea->setWidget(scrollContent_); - - layout_->addWidget(scrollArea, 1); -} - -void RadioButtonDemo::createBasicRadioSection() { - QGroupBox* groupBox = new QGroupBox("Basic Radio Buttons (基本单选按钮)", this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - - QButtonGroup* buttonGroup = new QButtonGroup(this); - - RadioButton* option1 = new RadioButton("Option 1 - First choice", this); - option1->setChecked(true); - buttonGroup->addButton(option1, 1); - layout->addWidget(option1); - - RadioButton* option2 = new RadioButton("Option 2 - Second choice", this); - buttonGroup->addButton(option2, 2); - layout->addWidget(option2); - - RadioButton* option3 = new RadioButton("Option 3 - Third choice", this); - buttonGroup->addButton(option3, 3); - layout->addWidget(option3); - - scrollContent_->layout()->addWidget(groupBox); -} - -void RadioButtonDemo::createRadioStatesSection() { - QGroupBox* groupBox = createGroupBox("Radio Button States (按钮状态)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - QHBoxLayout* normalLayout = new QHBoxLayout(); - QLabel* normalLabel = new QLabel("Normal:", this); - normalLabel->setMinimumWidth(120); - - QButtonGroup* normalGroup = new QButtonGroup(this); - RadioButton* normalRadio1 = new RadioButton("Enabled A", this); - RadioButton* normalRadio2 = new RadioButton("Enabled B", this); - normalRadio1->setChecked(true); - normalGroup->addButton(normalRadio1); - normalGroup->addButton(normalRadio2); - - normalLayout->addWidget(normalLabel); - normalLayout->addWidget(normalRadio1); - normalLayout->addWidget(normalRadio2); - normalLayout->addStretch(); - layout->addLayout(normalLayout); - - QHBoxLayout* disabledLayout = new QHBoxLayout(); - QLabel* disabledLabel = new QLabel("Disabled:", this); - disabledLabel->setMinimumWidth(120); - - QButtonGroup* disabledGroup = new QButtonGroup(this); - RadioButton* disabledRadio1 = new RadioButton("Disabled Option 1", this); - RadioButton* disabledRadio2 = new RadioButton("Disabled Option 2", this); - disabledRadio1->setChecked(true); - disabledRadio1->setEnabled(false); - disabledRadio2->setEnabled(false); - disabledGroup->addButton(disabledRadio1); - disabledGroup->addButton(disabledRadio2); - - disabledLayout->addWidget(disabledLabel); - disabledLayout->addWidget(disabledRadio1); - disabledLayout->addWidget(disabledRadio2); - disabledLayout->addStretch(); - layout->addLayout(disabledLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void RadioButtonDemo::createButtonGroupSection() { - QGroupBox* groupBox = createGroupBox("Independent Button Groups (独立按钮组)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - QLabel* group1Label = new QLabel("Select Color:", this); - layout->addWidget(group1Label); - - QHBoxLayout* group1Layout = new QHBoxLayout(); - QButtonGroup* colorGroup = new QButtonGroup(this); - RadioButton* redRadio = new RadioButton("Red", this); - RadioButton* greenRadio = new RadioButton("Green", this); - RadioButton* blueRadio = new RadioButton("Blue", this); - redRadio->setChecked(true); - colorGroup->addButton(redRadio); - colorGroup->addButton(greenRadio); - colorGroup->addButton(blueRadio); - group1Layout->addWidget(redRadio); - group1Layout->addWidget(greenRadio); - group1Layout->addWidget(blueRadio); - group1Layout->addStretch(); - layout->addLayout(group1Layout); - - layout->addSpacing(16); - - QLabel* group2Label = new QLabel("Select Size:", this); - layout->addWidget(group2Label); - - QHBoxLayout* group2Layout = new QHBoxLayout(); - QButtonGroup* sizeGroup = new QButtonGroup(this); - RadioButton* smallRadio = new RadioButton("Small", this); - RadioButton* mediumRadio = new RadioButton("Medium", this); - RadioButton* largeRadio = new RadioButton("Large", this); - mediumRadio->setChecked(true); - sizeGroup->addButton(smallRadio); - sizeGroup->addButton(mediumRadio); - sizeGroup->addButton(largeRadio); - group2Layout->addWidget(smallRadio); - group2Layout->addWidget(mediumRadio); - group2Layout->addWidget(largeRadio); - group2Layout->addStretch(); - layout->addLayout(group2Layout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void RadioButtonDemo::createErrorStateSection() { - QGroupBox* groupBox = createGroupBox("Error State (错误状态)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - QHBoxLayout* errorLayout = new QHBoxLayout(); - QLabel* errorLabel = new QLabel("Error State:", this); - errorLabel->setMinimumWidth(120); - - QButtonGroup* errorGroup = new QButtonGroup(this); - RadioButton* errorRadio1 = new RadioButton("Invalid Choice", this); - RadioButton* errorRadio2 = new RadioButton("Another Invalid", this); - errorRadio1->setError(true); - errorRadio2->setError(true); - errorRadio1->setChecked(true); - errorGroup->addButton(errorRadio1); - errorGroup->addButton(errorRadio2); - - errorLayout->addWidget(errorLabel); - errorLayout->addWidget(errorRadio1); - errorLayout->addWidget(errorRadio2); - errorLayout->addStretch(); - layout->addLayout(errorLayout); - - QHBoxLayout* toggleLayout = new QHBoxLayout(); - QLabel* toggleLabel = new QLabel("Toggle Error:", this); - toggleLabel->setMinimumWidth(120); - - QButtonGroup* toggleGroup = new QButtonGroup(this); - RadioButton* toggleRadio1 = new RadioButton("Valid Option", this); - RadioButton* toggleRadio2 = new RadioButton("Error Option", this); - toggleRadio1->setChecked(true); - toggleGroup->addButton(toggleRadio1, 1); - toggleGroup->addButton(toggleRadio2, 2); - - toggleLayout->addWidget(toggleLabel); - toggleLayout->addWidget(toggleRadio1); - toggleLayout->addWidget(toggleRadio2); - toggleLayout->addStretch(); - layout->addLayout(toggleLayout); - - connect(toggleGroup, QOverload::of(&QButtonGroup::buttonClicked), this, - [toggleRadio1, toggleRadio2](QAbstractButton* button) { - bool isError = (button == toggleRadio2); - toggleRadio1->setError(isError); - toggleRadio2->setError(isError); - }); - - scrollContent_->layout()->addWidget(groupBox); -} - -void RadioButtonDemo::createInteractionDemoSection() { - QGroupBox* groupBox = createGroupBox("Interaction Demo (交互演示)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - selectionLabel_ = new QLabel("Selected: Option 1", this); - selectionLabel_->setMinimumWidth(200); - QFont labelFont = selectionLabel_->font(); - labelFont.setBold(true); - selectionLabel_->setFont(labelFont); - layout->addWidget(selectionLabel_); - - QButtonGroup* demoGroup = new QButtonGroup(this); - - RadioButton* demoOption1 = new RadioButton("Option 1", this); - RadioButton* demoOption2 = new RadioButton("Option 2", this); - RadioButton* demoOption3 = new RadioButton("Option 3", this); - demoOption1->setChecked(true); - - demoGroup->addButton(demoOption1, 1); - demoGroup->addButton(demoOption2, 2); - demoGroup->addButton(demoOption3, 3); - - layout->addWidget(demoOption1); - layout->addWidget(demoOption2); - layout->addWidget(demoOption3); - - connect(demoGroup, QOverload::of(&QButtonGroup::buttonClicked), this, - [this](QAbstractButton* button) { - selectionLabel_->setText(QString("Selected: %1").arg(button->text())); - }); - - scrollContent_->layout()->addWidget(groupBox); - - static_cast(scrollContent_->layout())->addStretch(); -} - -QGroupBox* RadioButtonDemo::createGroupBox(const QString& title) { - QGroupBox* groupBox = new QGroupBox(title, this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - return groupBox; -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/RadioButtonDemo.h b/example/ui/widget/material_example/demos/RadioButtonDemo.h deleted file mode 100644 index f0fbd1d04..000000000 --- a/example/ui/widget/material_example/demos/RadioButtonDemo.h +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @file RadioButtonDemo.h - * @brief Material Design 3 RadioButton Demo Widget - */ - -#pragma once - -#include -#include -#include -#include -#include - -namespace cf::ui::example { - -class RadioButtonDemo : public QWidget { - Q_OBJECT - - public: - explicit RadioButtonDemo(QWidget* parent = nullptr); - ~RadioButtonDemo() override = default; - - QString title() const { return "RadioButton"; } - QString description() const { return "Material Design 3 RadioButton Component"; } - - private: - void setupUI(); - void createBasicRadioSection(); - void createRadioStatesSection(); - void createButtonGroupSection(); - void createErrorStateSection(); - void createInteractionDemoSection(); - - QGroupBox* createGroupBox(const QString& title); - - QWidget* scrollContent_; - QVBoxLayout* layout_; - QLabel* selectionLabel_; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/ScrollViewDemo.cpp b/example/ui/widget/material_example/demos/ScrollViewDemo.cpp deleted file mode 100644 index 37e138bce..000000000 --- a/example/ui/widget/material_example/demos/ScrollViewDemo.cpp +++ /dev/null @@ -1,316 +0,0 @@ -/** - * @file ScrollViewDemo.cpp - * @brief Material Design 3 ScrollView Demo - Implementation - */ - -#include "ScrollViewDemo.h" - -#include "ui/widget/material/widget/scrollview/scrollview.h" - -#include -#include -#include -#include -#include -#include - -using namespace cf::ui::widget::material; - -namespace cf::ui::example { - -ScrollViewDemo::ScrollViewDemo(QWidget* parent) : QWidget(parent) { - setupUI(); - createVerticalScrollSection(); - createHorizontalScrollSection(); - createBothDirectionsSection(); - createScrollBarPoliciesSection(); - createContentExamplesSection(); - - // Add stretch at the end of scroll content to push sections to top - static_cast(scrollContent_->layout())->addStretch(); -} - -void ScrollViewDemo::setupUI() { - layout_ = new QVBoxLayout(this); - layout_->setContentsMargins(0, 0, 0, 0); - layout_->setSpacing(0); - - // Scroll area for demo content - ScrollView* scrollArea = new ScrollView(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scrollContent_ = new QWidget(); - QVBoxLayout* scrollLayout = new QVBoxLayout(scrollContent_); - scrollLayout->setSpacing(24); - scrollLayout->setContentsMargins(24, 24, 24, 24); - scrollLayout->setSizeConstraint(QLayout::SetMinAndMaxSize); - - scrollArea->setWidget(scrollContent_); - layout_->addWidget(scrollArea, 1); -} - -void ScrollViewDemo::createVerticalScrollSection() { - QGroupBox* groupBox = new QGroupBox("Vertical Scroll (垂直滚动)", this); - QVBoxLayout* groupLayout = new QVBoxLayout(groupBox); - groupLayout->setSpacing(12); - groupLayout->setContentsMargins(16, 24, 16, 16); - - // Create a ScrollView with vertical scrolling - ScrollView* scrollView = new ScrollView(this); - scrollView->setWidgetResizable(true); - scrollView->setFixedHeight(200); - scrollView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - - // Create content that requires vertical scrolling - QWidget* content = new QWidget(); - QVBoxLayout* contentLayout = new QVBoxLayout(content); - contentLayout->setSpacing(8); - - for (int i = 1; i <= 20; ++i) { - QLabel* label = new QLabel(QString("Vertical Content Item %1").arg(i), this); - label->setStyleSheet("padding: 8px; background-color: #f0f0f0; border-radius: 4px;"); - contentLayout->addWidget(label); - } - contentLayout->addStretch(); - - scrollView->setWidget(content); - groupLayout->addWidget(scrollView); - - scrollContent_->layout()->addWidget(groupBox); -} - -void ScrollViewDemo::createHorizontalScrollSection() { - QGroupBox* groupBox = new QGroupBox("Horizontal Scroll (水平滚动)", this); - QVBoxLayout* groupLayout = new QVBoxLayout(groupBox); - groupLayout->setSpacing(12); - groupLayout->setContentsMargins(16, 24, 16, 16); - - // Create a ScrollView with horizontal scrolling - ScrollView* scrollView = new ScrollView(this); - scrollView->setWidgetResizable(true); - scrollView->setFixedHeight(120); - scrollView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); - scrollView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - // Create content that requires horizontal scrolling - QWidget* content = new QWidget(); - content->setFixedHeight(80); - QHBoxLayout* contentLayout = new QHBoxLayout(content); - contentLayout->setSpacing(8); - - for (int i = 1; i <= 15; ++i) { - QLabel* label = new QLabel(QString("H%1").arg(i), this); - label->setAlignment(Qt::AlignCenter); - label->setFixedSize(60, 60); - label->setStyleSheet("background-color: #e0e0e0; border-radius: 8px; font-weight: bold;"); - contentLayout->addWidget(label); - } - - scrollView->setWidget(content); - groupLayout->addWidget(scrollView); - - scrollContent_->layout()->addWidget(groupBox); -} - -void ScrollViewDemo::createBothDirectionsSection() { - QGroupBox* groupBox = new QGroupBox("Both Directions (双向滚动)", this); - QVBoxLayout* groupLayout = new QVBoxLayout(groupBox); - groupLayout->setSpacing(12); - groupLayout->setContentsMargins(16, 24, 16, 16); - - // Create a ScrollView with both horizontal and vertical scrolling - ScrollView* scrollView = new ScrollView(this); - scrollView->setWidgetResizable(true); - scrollView->setFixedSize(400, 200); - scrollView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); - scrollView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - - // Create a grid of content - QWidget* content = new QWidget(); - QGridLayout* contentLayout = new QGridLayout(content); - contentLayout->setSpacing(8); - contentLayout->setContentsMargins(8, 8, 8, 8); - - for (int row = 0; row < 10; ++row) { - for (int col = 0; col < 8; ++col) { - QLabel* label = new QLabel(QString("%1,%2").arg(row + 1).arg(col + 1), this); - label->setAlignment(Qt::AlignCenter); - label->setFixedSize(80, 50); - label->setStyleSheet("background-color: #d0d0d0; border-radius: 4px;"); - contentLayout->addWidget(label, row, col); - } - } - - scrollView->setWidget(content); - groupLayout->addWidget(scrollView); - - scrollContent_->layout()->addWidget(groupBox); -} - -void ScrollViewDemo::createScrollBarPoliciesSection() { - QGroupBox* groupBox = new QGroupBox("Scroll Bar Policies (滚动条策略)", this); - QGridLayout* gridLayout = new QGridLayout(groupBox); - gridLayout->setSpacing(16); - gridLayout->setContentsMargins(16, 24, 16, 16); - - // Always On - QLabel* alwaysOnLabel = new QLabel("ScrollBarAlwaysOn:", this); - ScrollView* alwaysOnScroll = new ScrollView(this); - alwaysOnScroll->setWidgetResizable(true); - alwaysOnScroll->setFixedSize(180, 100); - alwaysOnScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - alwaysOnScroll->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); - alwaysOnScroll->setWidget(createLargeContentWidget(Qt::Vertical)); - - // As Needed - QLabel* asNeededLabel = new QLabel("ScrollBarAsNeeded:", this); - ScrollView* asNeededScroll = new ScrollView(this); - asNeededScroll->setWidgetResizable(true); - asNeededScroll->setFixedSize(180, 100); - asNeededScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - asNeededScroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - asNeededScroll->setWidget(createLargeContentWidget(Qt::Vertical)); - - // Always Off - QLabel* alwaysOffLabel = new QLabel("ScrollBarAlwaysOff:", this); - ScrollView* alwaysOffScroll = new ScrollView(this); - alwaysOffScroll->setWidgetResizable(true); - alwaysOffScroll->setFixedSize(180, 100); - alwaysOffScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - alwaysOffScroll->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - alwaysOffScroll->setWidget(createLargeContentWidget(Qt::Vertical)); - - // Fade effect enabled - QLabel* fadeEnabledLabel = new QLabel("Fade Effect:", this); - ScrollView* fadeScroll = new ScrollView(this); - fadeScroll->setWidgetResizable(true); - fadeScroll->setFixedSize(180, 100); - fadeScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - fadeScroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - fadeScroll->setScrollbarFadeEnabled(true); - fadeScroll->setWidget(createLargeContentWidget(Qt::Vertical)); - - // Layout arrangement - gridLayout->addWidget(alwaysOnLabel, 0, 0); - gridLayout->addWidget(alwaysOnScroll, 1, 0); - gridLayout->addWidget(asNeededLabel, 0, 1); - gridLayout->addWidget(asNeededScroll, 1, 1); - gridLayout->addWidget(alwaysOffLabel, 0, 2); - gridLayout->addWidget(alwaysOffScroll, 1, 2); - gridLayout->addWidget(fadeEnabledLabel, 0, 3); - gridLayout->addWidget(fadeScroll, 1, 3); - - gridLayout->setColumnStretch(0, 1); - gridLayout->setColumnStretch(1, 1); - gridLayout->setColumnStretch(2, 1); - gridLayout->setColumnStretch(3, 1); - - scrollContent_->layout()->addWidget(groupBox); -} - -void ScrollViewDemo::createContentExamplesSection() { - QGroupBox* groupBox = new QGroupBox("Content Examples (内容示例)", this); - QVBoxLayout* groupLayout = new QVBoxLayout(groupBox); - groupLayout->setSpacing(12); - groupLayout->setContentsMargins(16, 24, 16, 16); - - // Text content example - QHBoxLayout* textLayout = new QHBoxLayout(); - QLabel* textLabel = new QLabel("Text Content:", this); - textLabel->setMinimumWidth(120); - - ScrollView* textScroll = new ScrollView(this); - textScroll->setWidgetResizable(true); - textScroll->setFixedSize(400, 120); - - QWidget* textContent = new QWidget(); - QVBoxLayout* textContentLayout = new QVBoxLayout(textContent); - QLabel* longTextLabel = new QLabel( - "This is an example of long text content in a ScrollView. " - "ScrollViews are perfect for displaying content that exceeds the available screen space. " - "Material Design 3 ScrollViews feature smooth scrolling, custom-styled scrollbars, " - "and optional fade effects. The scrollbar automatically appears when content overflows " - "and can be configured with different policies. You can scroll vertically, horizontally, " - "or in both directions depending on your content requirements.", - this); - longTextLabel->setWordWrap(true); - longTextLabel->setStyleSheet("padding: 12px;"); - textContentLayout->addWidget(longTextLabel); - textScroll->setWidget(textContent); - - textLayout->addWidget(textLabel); - textLayout->addWidget(textScroll); - textLayout->addStretch(); - groupLayout->addLayout(textLayout); - - // Widget grid example - QHBoxLayout* widgetLayout = new QHBoxLayout(); - QLabel* widgetLabel = new QLabel("Widget Grid:", this); - widgetLabel->setMinimumWidth(120); - - ScrollView* widgetScroll = new ScrollView(this); - widgetScroll->setWidgetResizable(true); - widgetScroll->setFixedSize(400, 120); - - QWidget* widgetContent = new QWidget(); - QGridLayout* widgetContentLayout = new QGridLayout(widgetContent); - widgetContentLayout->setSpacing(8); - - for (int i = 0; i < 12; ++i) { - QPushButton* btn = new QPushButton(QString("Button %1").arg(i + 1), this); - btn->setFixedSize(100, 36); - int row = i / 4; - int col = i % 4; - widgetContentLayout->addWidget(btn, row, col); - } - - widgetScroll->setWidget(widgetContent); - - widgetLayout->addWidget(widgetLabel); - widgetLayout->addWidget(widgetScroll); - widgetLayout->addStretch(); - groupLayout->addLayout(widgetLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -QWidget* ScrollViewDemo::createLargeContentWidget(Qt::Orientation orientation) { - QWidget* content = new QWidget(); - - if (orientation == Qt::Vertical) { - QVBoxLayout* layout = new QVBoxLayout(content); - layout->setSpacing(4); - for (int i = 1; i <= 10; ++i) { - QLabel* label = new QLabel(QString("Item %1").arg(i), this); - label->setStyleSheet("padding: 6px; background-color: #e8e8e8; border-radius: 3px;"); - layout->addWidget(label); - } - layout->addStretch(); - } else { - content->setFixedHeight(50); - QHBoxLayout* layout = new QHBoxLayout(content); - layout->setSpacing(4); - for (int i = 1; i <= 10; ++i) { - QLabel* label = new QLabel(QString("%1").arg(i), this); - label->setFixedSize(40, 40); - label->setAlignment(Qt::AlignCenter); - label->setStyleSheet("background-color: #e8e8e8; border-radius: 3px;"); - layout->addWidget(label); - } - } - - return content; -} - -QGroupBox* ScrollViewDemo::createGroupBox(const QString& title) { - QGroupBox* groupBox = new QGroupBox(title, this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - return groupBox; -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/ScrollViewDemo.h b/example/ui/widget/material_example/demos/ScrollViewDemo.h deleted file mode 100644 index 3164beceb..000000000 --- a/example/ui/widget/material_example/demos/ScrollViewDemo.h +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @file ScrollViewDemo.h - * @brief Material Design 3 ScrollView Demo - Widget Component - */ - -#pragma once - -#include -#include -class QGroupBox; - -namespace cf::ui::example { - -class ScrollViewDemo : public QWidget { - Q_OBJECT - - public: - explicit ScrollViewDemo(QWidget* parent = nullptr); - ~ScrollViewDemo() override = default; - - QString title() const { return "ScrollView"; } - QString description() const { return "Material Design 3 ScrollView Component"; } - - private: - void setupUI(); - void createVerticalScrollSection(); - void createHorizontalScrollSection(); - void createBothDirectionsSection(); - void createScrollBarPoliciesSection(); - void createContentExamplesSection(); - QGroupBox* createGroupBox(const QString& title); - QWidget* createLargeContentWidget(Qt::Orientation orientation); - - QWidget* scrollContent_; - QVBoxLayout* layout_; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/SeparatorDemo.cpp b/example/ui/widget/material_example/demos/SeparatorDemo.cpp deleted file mode 100644 index 6e610cd6c..000000000 --- a/example/ui/widget/material_example/demos/SeparatorDemo.cpp +++ /dev/null @@ -1,163 +0,0 @@ -/** - * @file SeparatorDemo.cpp - * @brief Material Design 3 Separator Demo - Implementation - */ - -#include "SeparatorDemo.h" - -#include "ui/widget/material/widget/separator/separator.h" - -#include -#include -#include -#include -#include - -using namespace cf::ui::widget::material; - -namespace cf::ui::example { - -SeparatorDemo::SeparatorDemo(QWidget* parent) : QWidget(parent) { - - setupUI(); - createHorizontalSeparatorSection(); - createVerticalSeparatorSection(); - createSeparatorModesSection(); - createInListSection(); -} - -void SeparatorDemo::setupUI() { - layout_ = new QVBoxLayout(this); - layout_->setContentsMargins(24, 24, 24, 24); - layout_->setSpacing(16); - - // Scroll area for content - QScrollArea* scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scrollContent_ = new QWidget(); - QVBoxLayout* scrollLayout = new QVBoxLayout(scrollContent_); - scrollLayout->setSpacing(24); - scrollLayout->setContentsMargins(0, 0, 0, 0); - - scrollArea->setWidget(scrollContent_); - layout_->addWidget(scrollArea, 1); -} - -void SeparatorDemo::createHorizontalSeparatorSection() { - QGroupBox* groupBox = createGroupBox("Horizontal Separator (水平分隔符)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Basic horizontal separator - QLabel* label1 = new QLabel("Content above the separator", groupBox); - layout->addWidget(label1); - - Separator* separator1 = new Separator(Qt::Horizontal, groupBox); - layout->addWidget(separator1); - - QLabel* label2 = new QLabel("Content below the separator", groupBox); - layout->addWidget(label2); - - scrollContent_->layout()->addWidget(groupBox); -} - -void SeparatorDemo::createVerticalSeparatorSection() { - QGroupBox* groupBox = new QGroupBox("Vertical Separator (垂直分隔符)", this); - QHBoxLayout* layout = new QHBoxLayout(groupBox); - - layout->setSpacing(16); - layout->setContentsMargins(16, 24, 16, 16); - - // Left content - QLabel* leftLabel = new QLabel("Left Item", groupBox); - layout->addWidget(leftLabel); - - // Vertical separator - Separator* separator = new Separator(Qt::Vertical, groupBox); - separator->setMinimumHeight(60); - layout->addWidget(separator); - - // Right content - QLabel* rightLabel = new QLabel("Right Item", groupBox); - layout->addWidget(rightLabel); - - layout->addStretch(); - - scrollContent_->layout()->addWidget(groupBox); -} - -void SeparatorDemo::createSeparatorModesSection() { - QGroupBox* groupBox = createGroupBox("Separator Modes (分隔符模式)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // FullBleed mode - QLabel* fullBleedLabel = new QLabel("FullBleed Mode (full width/height):", groupBox); - layout->addWidget(fullBleedLabel); - - Separator* fullBleedSep = new Separator(Qt::Horizontal, groupBox); - fullBleedSep->setMode(SeparatorMode::FullBleed); - layout->addWidget(fullBleedSep); - - // Inset mode - QLabel* insetLabel = new QLabel("Inset Mode (16dp margin on both sides):", groupBox); - layout->addWidget(insetLabel); - - Separator* insetSep = new Separator(Qt::Horizontal, groupBox); - insetSep->setMode(SeparatorMode::Inset); - layout->addWidget(insetSep); - - // MiddleInset mode - QLabel* middleInsetLabel = - new QLabel("MiddleInset Mode (16dp margin on leading side):", groupBox); - layout->addWidget(middleInsetLabel); - - Separator* middleInsetSep = new Separator(Qt::Horizontal, groupBox); - middleInsetSep->setMode(SeparatorMode::MiddleInset); - layout->addWidget(middleInsetSep); - - scrollContent_->layout()->addWidget(groupBox); -} - -void SeparatorDemo::createInListSection() { - QGroupBox* groupBox = createGroupBox("Separators in List (列表中的分隔符)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Simulate a list with separators - QLabel* item1 = new QLabel("List Item 1", groupBox); - layout->addWidget(item1); - - Separator* sep1 = new Separator(Qt::Horizontal, groupBox); - sep1->setMode(SeparatorMode::Inset); - layout->addWidget(sep1); - - QLabel* item2 = new QLabel("List Item 2", groupBox); - layout->addWidget(item2); - - Separator* sep2 = new Separator(Qt::Horizontal, groupBox); - sep2->setMode(SeparatorMode::Inset); - layout->addWidget(sep2); - - QLabel* item3 = new QLabel("List Item 3", groupBox); - layout->addWidget(item3); - - Separator* sep3 = new Separator(Qt::Horizontal, groupBox); - sep3->setMode(SeparatorMode::Inset); - layout->addWidget(sep3); - - QLabel* item4 = new QLabel("List Item 4", groupBox); - layout->addWidget(item4); - - scrollContent_->layout()->addWidget(groupBox); -} - -QGroupBox* SeparatorDemo::createGroupBox(const QString& title) { - QGroupBox* groupBox = new QGroupBox(title, this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - return groupBox; -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/SeparatorDemo.h b/example/ui/widget/material_example/demos/SeparatorDemo.h deleted file mode 100644 index 32c46bbb0..000000000 --- a/example/ui/widget/material_example/demos/SeparatorDemo.h +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @file SeparatorDemo.h - * @brief Material Design 3 Separator Demo - Widget Component - */ - -#pragma once - -#include -#include -#include -class QGroupBox; - -namespace cf::ui::example { - -class SeparatorDemo : public QWidget { - Q_OBJECT - - public: - explicit SeparatorDemo(QWidget* parent = nullptr); - ~SeparatorDemo() override = default; - - QString title() const { return "Separator"; } - QString description() const { return "Material Design 3 Separator Component"; } - - private: - void setupUI(); - void createHorizontalSeparatorSection(); - void createVerticalSeparatorSection(); - void createSeparatorModesSection(); - void createInListSection(); - QGroupBox* createGroupBox(const QString& title); - - QWidget* scrollContent_; - QVBoxLayout* layout_; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/SliderDemo.cpp b/example/ui/widget/material_example/demos/SliderDemo.cpp deleted file mode 100644 index 91ef48fba..000000000 --- a/example/ui/widget/material_example/demos/SliderDemo.cpp +++ /dev/null @@ -1,250 +0,0 @@ -/** - * @file SliderDemo.cpp - * @brief Material Design 3 Slider Demo - Implementation - */ - -#include "SliderDemo.h" - -#include "ui/widget/material/widget/slider/slider.h" - -#include -#include -#include -#include - -using namespace cf::ui::widget::material; - -namespace cf::ui::example { - -SliderDemo::SliderDemo(QWidget* parent) : QWidget(parent) { - setupUI(); - createHorizontalSection(); - createVerticalSection(); - createStatesSection(); - createTickMarksSection(); - createInteractiveDemoSection(); -} - -void SliderDemo::setupUI() { - layout_ = new QVBoxLayout(this); - layout_->setContentsMargins(16, 16, 16, 16); - layout_->setSpacing(16); - - scrollContent_ = new QWidget(); - QVBoxLayout* scrollLayout = new QVBoxLayout(scrollContent_); - scrollLayout->setSpacing(24); - scrollLayout->setContentsMargins(0, 0, 0, 0); - - QScrollArea* scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea->setWidget(scrollContent_); - - layout_->addWidget(scrollArea, 1); -} - -void SliderDemo::createHorizontalSection() { - QGroupBox* groupBox = createGroupBox("Horizontal Sliders (水平滑块)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - QHBoxLayout* layout1 = new QHBoxLayout(); - QLabel* label1 = new QLabel("0 - 100:", this); - label1->setMinimumWidth(80); - Slider* slider1 = new Slider(Qt::Horizontal, this); - slider1->setRange(0, 100); - slider1->setValue(50); - layout1->addWidget(label1); - layout1->addWidget(slider1); - layout->addLayout(layout1); - - QHBoxLayout* layout2 = new QHBoxLayout(); - QLabel* label2 = new QLabel("0 - 255:", this); - label2->setMinimumWidth(80); - Slider* slider2 = new Slider(Qt::Horizontal, this); - slider2->setRange(0, 255); - slider2->setValue(128); - layout2->addWidget(label2); - layout2->addWidget(slider2); - layout->addLayout(layout2); - - QHBoxLayout* layout3 = new QHBoxLayout(); - QLabel* label3 = new QLabel("Volume:", this); - label3->setMinimumWidth(80); - Slider* slider3 = new Slider(Qt::Horizontal, this); - slider3->setRange(0, 100); - slider3->setValue(70); - layout3->addWidget(label3); - layout3->addWidget(slider3); - layout->addLayout(layout3); - - scrollContent_->layout()->addWidget(groupBox); -} - -void SliderDemo::createVerticalSection() { - QGroupBox* groupBox = createGroupBox("Vertical Sliders (垂直滑块)"); - QHBoxLayout* layout = static_cast(groupBox->layout()); - - QVBoxLayout* vLayout1 = new QVBoxLayout(); - Slider* vSlider1 = new Slider(Qt::Vertical, this); - vSlider1->setRange(0, 100); - vSlider1->setValue(30); - vSlider1->setMinimumSize(60, 200); - QLabel* vLabel1 = new QLabel("30", this); - vLabel1->setAlignment(Qt::AlignCenter); - vLayout1->addWidget(vSlider1); - vLayout1->addWidget(vLabel1); - layout->addLayout(vLayout1); - - QVBoxLayout* vLayout2 = new QVBoxLayout(); - Slider* vSlider2 = new Slider(Qt::Vertical, this); - vSlider2->setRange(0, 100); - vSlider2->setValue(70); - vSlider2->setMinimumSize(60, 200); - QLabel* vLabel2 = new QLabel("70", this); - vLabel2->setAlignment(Qt::AlignCenter); - vLayout2->addWidget(vSlider2); - vLayout2->addWidget(vLabel2); - layout->addLayout(vLayout2); - - QVBoxLayout* vLayout3 = new QVBoxLayout(); - Slider* vSlider3 = new Slider(Qt::Vertical, this); - vSlider3->setRange(0, 100); - vSlider3->setValue(50); - vSlider3->setMinimumSize(60, 200); - QLabel* vLabel3 = new QLabel("50", this); - vLabel3->setAlignment(Qt::AlignCenter); - vLayout3->addWidget(vSlider3); - vLayout3->addWidget(vLabel3); - layout->addLayout(vLayout3); - - layout->addStretch(); - - scrollContent_->layout()->addWidget(groupBox); -} - -void SliderDemo::createStatesSection() { - QGroupBox* groupBox = createGroupBox("Slider States (滑块状态)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - QHBoxLayout* normalLayout = new QHBoxLayout(); - QLabel* normalLabel = new QLabel("Normal:", this); - normalLabel->setMinimumWidth(80); - Slider* normalSlider = new Slider(Qt::Horizontal, this); - normalSlider->setRange(0, 100); - normalSlider->setValue(60); - normalLayout->addWidget(normalLabel); - normalLayout->addWidget(normalSlider); - layout->addLayout(normalLayout); - - QHBoxLayout* disabledLayout = new QHBoxLayout(); - QLabel* disabledLabel = new QLabel("Disabled:", this); - disabledLabel->setMinimumWidth(80); - Slider* disabledSlider = new Slider(Qt::Horizontal, this); - disabledSlider->setRange(0, 100); - disabledSlider->setValue(60); - disabledSlider->setEnabled(false); - disabledLayout->addWidget(disabledLabel); - disabledLayout->addWidget(disabledSlider); - layout->addLayout(disabledLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void SliderDemo::createTickMarksSection() { - QGroupBox* groupBox = createGroupBox("Tick Marks (刻度)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - QHBoxLayout* ticksBelowLayout = new QHBoxLayout(); - QLabel* ticksBelowLabel = new QLabel("Ticks Below:", this); - ticksBelowLabel->setMinimumWidth(100); - Slider* ticksBelowSlider = new Slider(Qt::Horizontal, this); - ticksBelowSlider->setRange(0, 100); - ticksBelowSlider->setValue(25); - ticksBelowSlider->setTickPosition(QSlider::TicksBelow); - ticksBelowSlider->setTickInterval(10); - ticksBelowLayout->addWidget(ticksBelowLabel); - ticksBelowLayout->addWidget(ticksBelowSlider); - layout->addLayout(ticksBelowLayout); - - QHBoxLayout* ticksAboveLayout = new QHBoxLayout(); - QLabel* ticksAboveLabel = new QLabel("Ticks Above:", this); - ticksAboveLabel->setMinimumWidth(100); - Slider* ticksAboveSlider = new Slider(Qt::Horizontal, this); - ticksAboveSlider->setRange(0, 100); - ticksAboveSlider->setValue(75); - ticksAboveSlider->setTickPosition(QSlider::TicksAbove); - ticksAboveSlider->setTickInterval(25); - ticksAboveLayout->addWidget(ticksAboveLabel); - ticksAboveLayout->addWidget(ticksAboveSlider); - layout->addLayout(ticksAboveLayout); - - QHBoxLayout* bothLayout = new QHBoxLayout(); - QLabel* bothLabel = new QLabel("Both Sides:", this); - bothLabel->setMinimumWidth(100); - Slider* bothSlider = new Slider(Qt::Horizontal, this); - bothSlider->setRange(0, 10); - bothSlider->setValue(5); - bothSlider->setTickPosition(QSlider::TicksBothSides); - bothSlider->setTickInterval(1); - bothLayout->addWidget(bothLabel); - bothLayout->addWidget(bothSlider); - layout->addLayout(bothLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void SliderDemo::createInteractiveDemoSection() { - QGroupBox* groupBox = createGroupBox("Interactive Demo (交互演示)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - QHBoxLayout* hSliderLayout = new QHBoxLayout(); - QLabel* hLabel = new QLabel("Horizontal:", this); - hLabel->setMinimumWidth(80); - Slider* hSlider = new Slider(Qt::Horizontal, this); - hSlider->setRange(0, 100); - hSlider->setValue(50); - connect(hSlider, &Slider::valueChanged, this, &SliderDemo::onHorizontalSliderChanged); - horizontalValueLabel_ = new QLabel("Value: 50", this); - hSliderLayout->addWidget(hLabel); - hSliderLayout->addWidget(hSlider); - hSliderLayout->addWidget(horizontalValueLabel_); - layout->addLayout(hSliderLayout); - - QHBoxLayout* vSliderLayout = new QHBoxLayout(); - QLabel* vLabel = new QLabel("Vertical:", this); - vLabel->setMinimumWidth(80); - Slider* vSlider = new Slider(Qt::Vertical, this); - vSlider->setRange(0, 100); - vSlider->setValue(30); - vSlider->setMinimumSize(60, 150); - connect(vSlider, &Slider::valueChanged, this, &SliderDemo::onVerticalSliderChanged); - verticalValueLabel_ = new QLabel("Value: 30", this); - vSliderLayout->addWidget(vLabel); - vSliderLayout->addWidget(vSlider); - vSliderLayout->addWidget(verticalValueLabel_); - vSliderLayout->addStretch(); - layout->addLayout(vSliderLayout); - - scrollContent_->layout()->addWidget(groupBox); - - static_cast(scrollContent_->layout())->addStretch(); -} - -void SliderDemo::onHorizontalSliderChanged(int value) { - horizontalValueLabel_->setText(QString("Value: %1").arg(value)); -} - -void SliderDemo::onVerticalSliderChanged(int value) { - verticalValueLabel_->setText(QString("Value: %1").arg(value)); -} - -QGroupBox* SliderDemo::createGroupBox(const QString& title) { - QGroupBox* groupBox = new QGroupBox(title, this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - return groupBox; -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/SliderDemo.h b/example/ui/widget/material_example/demos/SliderDemo.h deleted file mode 100644 index 1fdfd48a1..000000000 --- a/example/ui/widget/material_example/demos/SliderDemo.h +++ /dev/null @@ -1,44 +0,0 @@ -/** - * @file SliderDemo.h - * @brief Material Design 3 Slider Demo Widget - */ - -#pragma once - -#include -#include -#include -#include - -namespace cf::ui::example { - -class SliderDemo : public QWidget { - Q_OBJECT - - public: - explicit SliderDemo(QWidget* parent = nullptr); - ~SliderDemo() override = default; - - QString title() const { return "Slider"; } - QString description() const { return "Material Design 3 Slider Component"; } - - private: - void setupUI(); - void createHorizontalSection(); - void createVerticalSection(); - void createStatesSection(); - void createTickMarksSection(); - void createInteractiveDemoSection(); - - QGroupBox* createGroupBox(const QString& title); - - void onHorizontalSliderChanged(int value); - void onVerticalSliderChanged(int value); - - QWidget* scrollContent_; - QVBoxLayout* layout_; - QLabel* horizontalValueLabel_; - QLabel* verticalValueLabel_; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/SpinBoxDemo.cpp b/example/ui/widget/material_example/demos/SpinBoxDemo.cpp deleted file mode 100644 index a6bea51d5..000000000 --- a/example/ui/widget/material_example/demos/SpinBoxDemo.cpp +++ /dev/null @@ -1,305 +0,0 @@ -/** - * @file SpinBoxDemo.cpp - * @brief Material Design 3 SpinBox Demo - Implementation - */ - -#include "SpinBoxDemo.h" - -#include "ui/widget/material/widget/spinbox/spinbox.h" - -#include -#include -#include -#include -#include -#include -#include - -using namespace cf::ui::widget::material; - -namespace cf::ui::example { - -SpinBoxDemo::SpinBoxDemo(QWidget* parent) : QWidget(parent) { - - setupUI(); - createSpinBoxVariantsSection(); - createSpinBoxStatesSection(); - createValueRangesSection(); - createStepSizesSection(); - createInteractionDemoSection(); -} - -void SpinBoxDemo::setupUI() { - layout_ = new QVBoxLayout(this); - layout_->setContentsMargins(24, 24, 24, 24); - layout_->setSpacing(16); - - // Scroll area for content - QScrollArea* scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scrollContent_ = new QWidget(); - QVBoxLayout* scrollLayout = new QVBoxLayout(scrollContent_); - scrollLayout->setSpacing(24); - scrollLayout->setContentsMargins(0, 0, 0, 0); - - scrollArea->setWidget(scrollContent_); - layout_->addWidget(scrollArea, 1); -} - -void SpinBoxDemo::createSpinBoxVariantsSection() { - QGroupBox* groupBox = new QGroupBox("SpinBox Variants (数字输入框变体)", this); - QGridLayout* gridLayout = new QGridLayout(groupBox); - gridLayout->setSpacing(16); - gridLayout->setContentsMargins(16, 24, 16, 16); - - // Row 0: Basic SpinBox - QLabel* basicLabel = new QLabel("Basic", this); - basicLabel->setAlignment(Qt::AlignCenter); - SpinBox* basicSpinBox = new SpinBox(this); - basicSpinBox->setRange(0, 100); - basicSpinBox->setValue(50); - gridLayout->addWidget(basicLabel, 0, 0); - gridLayout->addWidget(basicSpinBox, 1, 0); - - // Row 1: SpinBox with minimum value - QLabel* minLabel = new QLabel("Minimum", this); - minLabel->setAlignment(Qt::AlignCenter); - SpinBox* minSpinBox = new SpinBox(this); - minSpinBox->setRange(-100, 100); - minSpinBox->setValue(-50); - gridLayout->addWidget(minLabel, 0, 1); - gridLayout->addWidget(minSpinBox, 1, 1); - - // Row 2: SpinBox with maximum value - QLabel* maxLabel = new QLabel("Maximum", this); - maxLabel->setAlignment(Qt::AlignCenter); - SpinBox* maxSpinBox = new SpinBox(this); - maxSpinBox->setRange(0, 1000); - maxSpinBox->setValue(999); - gridLayout->addWidget(maxLabel, 0, 2); - gridLayout->addWidget(maxSpinBox, 1, 2); - - gridLayout->setColumnStretch(0, 1); - gridLayout->setColumnStretch(1, 1); - gridLayout->setColumnStretch(2, 1); - - scrollContent_->layout()->addWidget(groupBox); -} - -void SpinBoxDemo::createSpinBoxStatesSection() { - QGroupBox* groupBox = createGroupBox("SpinBox States (数字输入框状态)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Normal (enabled) spinbox - QHBoxLayout* normalLayout = new QHBoxLayout(); - QLabel* normalLabel = new QLabel("Normal:", this); - normalLabel->setMinimumWidth(120); - SpinBox* normalSpinBox = new SpinBox(this); - normalSpinBox->setRange(0, 100); - normalSpinBox->setValue(42); - normalLayout->addWidget(normalLabel); - normalLayout->addWidget(normalSpinBox); - normalLayout->addStretch(); - layout->addLayout(normalLayout); - - // Disabled spinbox - QHBoxLayout* disabledLayout = new QHBoxLayout(); - QLabel* disabledLabel = new QLabel("Disabled:", this); - disabledLabel->setMinimumWidth(120); - SpinBox* disabledSpinBox = new SpinBox(this); - disabledSpinBox->setRange(0, 100); - disabledSpinBox->setValue(50); - disabledSpinBox->setEnabled(false); - disabledLayout->addWidget(disabledLabel); - disabledLayout->addWidget(disabledSpinBox); - disabledLayout->addStretch(); - layout->addLayout(disabledLayout); - - // Read-only spinbox - QHBoxLayout* readOnlyLayout = new QHBoxLayout(); - QLabel* readOnlyLabel = new QLabel("Read-only:", this); - readOnlyLabel->setMinimumWidth(120); - SpinBox* readOnlySpinBox = new SpinBox(this); - readOnlySpinBox->setRange(0, 100); - readOnlySpinBox->setValue(75); - readOnlySpinBox->setReadOnly(true); - readOnlyLayout->addWidget(readOnlyLabel); - readOnlyLayout->addWidget(readOnlySpinBox); - readOnlyLayout->addStretch(); - layout->addLayout(readOnlyLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void SpinBoxDemo::createValueRangesSection() { - QGroupBox* groupBox = createGroupBox("Value Ranges (数值范围)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Small range - QHBoxLayout* smallLayout = new QHBoxLayout(); - QLabel* smallLabel = new QLabel("Small (0-10):", this); - smallLabel->setMinimumWidth(150); - SpinBox* smallSpinBox = new SpinBox(this); - smallSpinBox->setRange(0, 10); - smallSpinBox->setValue(5); - smallLayout->addWidget(smallLabel); - smallLayout->addWidget(smallSpinBox); - smallLayout->addStretch(); - layout->addLayout(smallLayout); - - // Medium range - QHBoxLayout* mediumLayout = new QHBoxLayout(); - QLabel* mediumLabel = new QLabel("Medium (0-100):", this); - mediumLabel->setMinimumWidth(150); - SpinBox* mediumSpinBox = new SpinBox(this); - mediumSpinBox->setRange(0, 100); - mediumSpinBox->setValue(50); - mediumLayout->addWidget(mediumLabel); - mediumLayout->addWidget(mediumSpinBox); - mediumLayout->addStretch(); - layout->addLayout(mediumLayout); - - // Large range - QHBoxLayout* largeLayout = new QHBoxLayout(); - QLabel* largeLabel = new QLabel("Large (0-1000):", this); - largeLabel->setMinimumWidth(150); - SpinBox* largeSpinBox = new SpinBox(this); - largeSpinBox->setRange(0, 1000); - largeSpinBox->setValue(500); - largeLayout->addWidget(largeLabel); - largeLayout->addWidget(largeSpinBox); - largeLayout->addStretch(); - layout->addLayout(largeLayout); - - // Negative range - QHBoxLayout* negativeLayout = new QHBoxLayout(); - QLabel* negativeLabel = new QLabel("Negative (-100 to 100):", this); - negativeLabel->setMinimumWidth(150); - SpinBox* negativeSpinBox = new SpinBox(this); - negativeSpinBox->setRange(-100, 100); - negativeSpinBox->setValue(0); - negativeLayout->addWidget(negativeLabel); - negativeLayout->addWidget(negativeSpinBox); - negativeLayout->addStretch(); - layout->addLayout(negativeLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void SpinBoxDemo::createStepSizesSection() { - QGroupBox* groupBox = createGroupBox("Step Sizes (步长)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Single step - QHBoxLayout* singleLayout = new QHBoxLayout(); - QLabel* singleLabel = new QLabel("Step 1:", this); - singleLabel->setMinimumWidth(150); - SpinBox* singleSpinBox = new SpinBox(this); - singleSpinBox->setRange(0, 100); - singleSpinBox->setValue(50); - singleSpinBox->setSingleStep(1); - singleLayout->addWidget(singleLabel); - singleLayout->addWidget(singleSpinBox); - singleLayout->addStretch(); - layout->addLayout(singleLayout); - - // Step of 5 - QHBoxLayout* step5Layout = new QHBoxLayout(); - QLabel* step5Label = new QLabel("Step 5:", this); - step5Label->setMinimumWidth(150); - SpinBox* step5SpinBox = new SpinBox(this); - step5SpinBox->setRange(0, 100); - step5SpinBox->setValue(50); - step5SpinBox->setSingleStep(5); - step5Layout->addWidget(step5Label); - step5Layout->addWidget(step5SpinBox); - step5Layout->addStretch(); - layout->addLayout(step5Layout); - - // Step of 10 - QHBoxLayout* step10Layout = new QHBoxLayout(); - QLabel* step10Label = new QLabel("Step 10:", this); - step10Label->setMinimumWidth(150); - SpinBox* step10SpinBox = new SpinBox(this); - step10SpinBox->setRange(0, 200); - step10SpinBox->setValue(100); - step10SpinBox->setSingleStep(10); - step10Layout->addWidget(step10Label); - step10Layout->addWidget(step10SpinBox); - step10Layout->addStretch(); - layout->addLayout(step10Layout); - - // Step of 100 - QHBoxLayout* step100Layout = new QHBoxLayout(); - QLabel* step100Label = new QLabel("Step 100:", this); - step100Label->setMinimumWidth(150); - SpinBox* step100SpinBox = new SpinBox(this); - step100SpinBox->setRange(0, 10000); - step100SpinBox->setValue(5000); - step100SpinBox->setSingleStep(100); - step100Layout->addWidget(step100Label); - step100Layout->addWidget(step100SpinBox); - step100Layout->addStretch(); - layout->addLayout(step100Layout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void SpinBoxDemo::createInteractionDemoSection() { - QGroupBox* groupBox = createGroupBox("SpinBox Interaction (数字输入框交互)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Value change demo - QHBoxLayout* valueLayout = new QHBoxLayout(); - QLabel* hintLabel = new QLabel("Adjust value to see changes:", this); - hintLabel->setMinimumWidth(200); - SpinBox* demoSpinBox = new SpinBox(this); - demoSpinBox->setRange(0, 100); - demoSpinBox->setValue(50); - connect(demoSpinBox, QOverload::of(&QSpinBox::valueChanged), this, - &SpinBoxDemo::onDemoSpinBoxValueChanged); - valueLayout->addWidget(hintLabel); - valueLayout->addWidget(demoSpinBox); - valueLayout->addStretch(); - layout->addLayout(valueLayout); - - // Status label - QHBoxLayout* statusLayout = new QHBoxLayout(); - QLabel* statusLabel = new QLabel("Current value:", this); - statusLabel->setMinimumWidth(200); - QLabel* valueDisplayLabel = new QLabel("50", this); - valueDisplayLabel->setObjectName("valueDisplay"); - valueDisplayLabel->setStyleSheet("QLabel#valueDisplay { font-weight: bold; color: #6750A4; }"); - statusLayout->addWidget(statusLabel); - statusLayout->addWidget(valueDisplayLabel); - statusLayout->addStretch(); - layout->addLayout(statusLayout); - - // Store the label for updating - demoSpinBox->setProperty("valueLabel", QVariant::fromValue(valueDisplayLabel)); - - scrollContent_->layout()->addWidget(groupBox); -} - -void SpinBoxDemo::onDemoSpinBoxValueChanged(int value) { - SpinBox* spinBox = qobject_cast(sender()); - if (spinBox) { - QLabel* valueLabel = spinBox->property("valueLabel").value(); - if (valueLabel) { - valueLabel->setText(QString::number(value)); - } - } -} - -QGroupBox* SpinBoxDemo::createGroupBox(const QString& title) { - QGroupBox* groupBox = new QGroupBox(title, this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - return groupBox; -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/SpinBoxDemo.h b/example/ui/widget/material_example/demos/SpinBoxDemo.h deleted file mode 100644 index 6ad2090ce..000000000 --- a/example/ui/widget/material_example/demos/SpinBoxDemo.h +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @file SpinBoxDemo.h - * @brief Material Design 3 SpinBox Demo - Widget Component - */ - -#pragma once - -#include -#include -#include -class QGroupBox; - -namespace cf::ui::example { - -class SpinBoxDemo : public QWidget { - Q_OBJECT - - public: - explicit SpinBoxDemo(QWidget* parent = nullptr); - ~SpinBoxDemo() override = default; - - QString title() const { return "SpinBox"; } - QString description() const { return "Material Design 3 SpinBox Component"; } - - private: - void setupUI(); - void createSpinBoxVariantsSection(); - void createSpinBoxStatesSection(); - void createValueRangesSection(); - void createStepSizesSection(); - void createInteractionDemoSection(); - QGroupBox* createGroupBox(const QString& title); - - void onDemoSpinBoxValueChanged(int value); - - QWidget* scrollContent_; - QVBoxLayout* layout_; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/SwitchDemo.cpp b/example/ui/widget/material_example/demos/SwitchDemo.cpp deleted file mode 100644 index ce6d5d330..000000000 --- a/example/ui/widget/material_example/demos/SwitchDemo.cpp +++ /dev/null @@ -1,188 +0,0 @@ -/** - * @file SwitchDemo.cpp - * @brief Material Design 3 Switch Demo Implementation - */ - -#include "SwitchDemo.h" - -#include "ui/widget/material/widget/switch/switch.h" - -#include -#include -#include -#include - -using namespace cf::ui::widget::material; - -namespace cf::ui::example { - -static QLabel* statusLabel = nullptr; - -SwitchDemo::SwitchDemo(QWidget* parent) - : QWidget(parent), scrollContent_(nullptr), layout_(nullptr) { - setupUI(); - createBasicStatesSection(); - createWithLabelsSection(); - createDisabledSection(); - createInteractiveDemoSection(); -} - -void SwitchDemo::setupUI() { - layout_ = new QVBoxLayout(this); - layout_->setContentsMargins(16, 16, 16, 16); - layout_->setSpacing(16); - - // Create scroll area - auto* scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - // Create scroll content - scrollContent_ = new QWidget(); - auto* scrollLayout = new QVBoxLayout(scrollContent_); - scrollLayout->setSpacing(24); - scrollLayout->setContentsMargins(8, 8, 8, 8); - scrollLayout->addStretch(); - - scrollArea->setWidget(scrollContent_); - layout_->addWidget(scrollArea, 1); -} - -void SwitchDemo::createBasicStatesSection() { - QGroupBox* groupBox = createGroupBox("Basic States (基本状态)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Unchecked - QHBoxLayout* uncheckedLayout = new QHBoxLayout(); - Switch* uncheckedSwitch = new Switch(this); - uncheckedSwitch->setChecked(false); - QLabel* uncheckedLabel = new QLabel("Off", this); - uncheckedLayout->addWidget(uncheckedSwitch); - uncheckedLayout->addWidget(uncheckedLabel); - uncheckedLayout->addStretch(); - layout->addLayout(uncheckedLayout); - - // Checked - QHBoxLayout* checkedLayout = new QHBoxLayout(); - Switch* checkedSwitch = new Switch(this); - checkedSwitch->setChecked(true); - QLabel* checkedLabel = new QLabel("On", this); - checkedLayout->addWidget(checkedSwitch); - checkedLayout->addWidget(checkedLabel); - checkedLayout->addStretch(); - layout->addLayout(checkedLayout); - - // Insert before stretch - auto* scrollLayout = static_cast(scrollContent_->layout()); - scrollLayout->insertWidget(scrollLayout->count() - 1, groupBox); -} - -void SwitchDemo::createWithLabelsSection() { - QGroupBox* groupBox = createGroupBox("Switches with Labels (带标签)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Wi-Fi example - QHBoxLayout* wifiLayout = new QHBoxLayout(); - Switch* wifiSwitch = new Switch("Wi-Fi", this); - wifiSwitch->setChecked(true); - wifiLayout->addWidget(wifiSwitch); - wifiLayout->addStretch(); - layout->addLayout(wifiLayout); - - // Bluetooth example - QHBoxLayout* btLayout = new QHBoxLayout(); - Switch* btSwitch = new Switch("Bluetooth", this); - btSwitch->setChecked(false); - btLayout->addWidget(btSwitch); - btLayout->addStretch(); - layout->addLayout(btLayout); - - // Notifications example - QHBoxLayout* notifLayout = new QHBoxLayout(); - Switch* notifSwitch = new Switch("Notifications", this); - notifSwitch->setChecked(true); - notifLayout->addWidget(notifSwitch); - notifLayout->addStretch(); - layout->addLayout(notifLayout); - - // Insert before stretch - auto* scrollLayout = static_cast(scrollContent_->layout()); - scrollLayout->insertWidget(scrollLayout->count() - 1, groupBox); -} - -void SwitchDemo::createDisabledSection() { - QGroupBox* groupBox = createGroupBox("Disabled States (禁用状态)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Disabled unchecked - QHBoxLayout* disabledUncheckedLayout = new QHBoxLayout(); - Switch* disabledUncheckedSwitch = new Switch("Disabled (Off)", this); - disabledUncheckedSwitch->setChecked(false); - disabledUncheckedSwitch->setEnabled(false); - disabledUncheckedLayout->addWidget(disabledUncheckedSwitch); - disabledUncheckedLayout->addStretch(); - layout->addLayout(disabledUncheckedLayout); - - // Disabled checked - QHBoxLayout* disabledCheckedLayout = new QHBoxLayout(); - Switch* disabledCheckedSwitch = new Switch("Disabled (On)", this); - disabledCheckedSwitch->setChecked(true); - disabledCheckedSwitch->setEnabled(false); - disabledCheckedLayout->addWidget(disabledCheckedSwitch); - disabledCheckedLayout->addStretch(); - layout->addLayout(disabledCheckedLayout); - - // Insert before stretch - auto* scrollLayout = static_cast(scrollContent_->layout()); - scrollLayout->insertWidget(scrollLayout->count() - 1, groupBox); -} - -void SwitchDemo::createInteractiveDemoSection() { - QGroupBox* groupBox = createGroupBox("Interactive Demo (交互演示)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Interactive switch - QHBoxLayout* demoLayout = new QHBoxLayout(); - Switch* demoSwitch = new Switch("Enable Feature", this); - connect(demoSwitch, &Switch::toggled, this, &SwitchDemo::onSwitchToggled); - demoLayout->addWidget(demoSwitch); - demoLayout->addStretch(); - layout->addLayout(demoLayout); - - // Status label - statusLabel = new QLabel("Status: Off", this); - QFont statusFont("Segoe UI", 10, QFont::Bold); - statusLabel->setFont(statusFont); - QHBoxLayout* statusLayout = new QHBoxLayout(); - statusLayout->addSpacing(8); // Align with switch - statusLayout->addWidget(statusLabel); - statusLayout->addStretch(); - layout->addLayout(statusLayout); - - // Insert before stretch - auto* scrollLayout = static_cast(scrollContent_->layout()); - scrollLayout->insertWidget(scrollLayout->count() - 1, groupBox); -} - -void SwitchDemo::onSwitchToggled(bool checked) { - if (statusLabel) { - if (checked) { - statusLabel->setText("Status: On"); - statusLabel->setStyleSheet("color: green;"); - } else { - statusLabel->setText("Status: Off"); - statusLabel->setStyleSheet("color: gray;"); - } - } -} - -QGroupBox* SwitchDemo::createGroupBox(const QString& title) { - QGroupBox* groupBox = new QGroupBox(title, this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - return groupBox; -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/SwitchDemo.h b/example/ui/widget/material_example/demos/SwitchDemo.h deleted file mode 100644 index e9af1a611..000000000 --- a/example/ui/widget/material_example/demos/SwitchDemo.h +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @file SwitchDemo.h - * @brief Material Design 3 Switch Demo - Widget Component - */ - -#pragma once - -#include -#include -#include -#include - -namespace cf::ui::example { - -class SwitchDemo : public QWidget { - Q_OBJECT - - public: - explicit SwitchDemo(QWidget* parent = nullptr); - ~SwitchDemo() override = default; - - QString title() const { return "Switch"; } - QString description() const { return "Material Design 3 Switch Component"; } - - private: - void setupUI(); - void createBasicStatesSection(); - void createWithLabelsSection(); - void createDisabledSection(); - void createInteractiveDemoSection(); - - QGroupBox* createGroupBox(const QString& title); - - QWidget* scrollContent_; - QVBoxLayout* layout_; - - private slots: - void onSwitchToggled(bool checked); -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/TabViewDemo.cpp b/example/ui/widget/material_example/demos/TabViewDemo.cpp deleted file mode 100644 index 924d8c6c3..000000000 --- a/example/ui/widget/material_example/demos/TabViewDemo.cpp +++ /dev/null @@ -1,272 +0,0 @@ -/** - * @file TabViewDemo.cpp - * @brief Material Design 3 TabView Demo - Implementation - */ - -#include "TabViewDemo.h" - -#include "ui/widget/material/widget/tabview/tabview.h" - -#include -#include -#include -#include -#include -#include -#include - -using namespace cf::ui::widget::material; - -namespace cf::ui::example { - -TabViewDemo::TabViewDemo(QWidget* parent) : QWidget(parent) { - - setupUI(); - createBasicTabsSection(); - createTabsWithIconsSection(); - createCloseableTabsSection(); - createTabPositionSection(); - createDisabledTabsSection(); -} - -void TabViewDemo::setupUI() { - layout_ = new QVBoxLayout(this); - layout_->setContentsMargins(24, 24, 24, 24); - layout_->setSpacing(16); - - // Scroll area for content - QScrollArea* scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scrollContent_ = new QWidget(); - QVBoxLayout* scrollLayout = new QVBoxLayout(scrollContent_); - scrollLayout->setSpacing(24); - scrollLayout->setContentsMargins(0, 0, 0, 0); - - scrollArea->setWidget(scrollContent_); - layout_->addWidget(scrollArea, 1); -} - -void TabViewDemo::createBasicTabsSection() { - QGroupBox* groupBox = new QGroupBox("Basic Tabs (基本选项卡)", this); - QVBoxLayout* groupLayout = new QVBoxLayout(groupBox); - groupLayout->setSpacing(12); - groupLayout->setContentsMargins(16, 24, 16, 16); - - // Basic TabView with text labels - TabView* tabView = new TabView(this); - - // Add tabs with basic content - QWidget* homeTab = new QWidget(); - QVBoxLayout* homeLayout = new QVBoxLayout(homeTab); - homeLayout->addWidget(new QLabel("Home Tab Content\n\nThis is the home page content.")); - homeLayout->addStretch(); - tabView->addTab(homeTab, "Home"); - - QWidget* profileTab = new QWidget(); - QVBoxLayout* profileLayout = new QVBoxLayout(profileTab); - profileLayout->addWidget( - new QLabel("Profile Tab Content\n\nUser profile information goes here.")); - profileLayout->addStretch(); - tabView->addTab(profileTab, "Profile"); - - QWidget* settingsTab = new QWidget(); - QVBoxLayout* settingsLayout = new QVBoxLayout(settingsTab); - settingsLayout->addWidget(new QLabel("Settings Tab Content\n\nConfigure your settings here.")); - settingsLayout->addStretch(); - tabView->addTab(settingsTab, "Settings"); - - tabView->setMinimumHeight(200); - groupLayout->addWidget(tabView); - - scrollContent_->layout()->addWidget(groupBox); -} - -void TabViewDemo::createTabsWithIconsSection() { - QGroupBox* groupBox = new QGroupBox("Tabs with Icons (带图标的选项卡)", this); - QVBoxLayout* groupLayout = new QVBoxLayout(groupBox); - groupLayout->setSpacing(12); - groupLayout->setContentsMargins(16, 24, 16, 16); - - // TabView with icons - TabView* tabView = new TabView(this); - - // Add tabs with icons - QWidget* filesTab = new QWidget(); - QVBoxLayout* filesLayout = new QVBoxLayout(filesTab); - filesLayout->addWidget(new QLabel("Files Tab\n\nBrowse and manage your files.")); - filesLayout->addStretch(); - tabView->addTab(filesTab, QApplication::style()->standardIcon(QStyle::SP_FileIcon), "Files"); - - QWidget* editTab = new QWidget(); - QVBoxLayout* editLayout = new QVBoxLayout(editTab); - editLayout->addWidget(new QLabel("Edit Tab\n\nEdit your content here.")); - editLayout->addStretch(); - tabView->addTab(editTab, QApplication::style()->standardIcon(QStyle::SP_FileDialogDetailedView), - "Edit"); - - QWidget* viewTab = new QWidget(); - QVBoxLayout* viewLayout = new QVBoxLayout(viewTab); - viewLayout->addWidget(new QLabel("View Tab\n\nAdjust view settings.")); - viewLayout->addStretch(); - tabView->addTab(viewTab, QApplication::style()->standardIcon(QStyle::SP_BrowserReload), "View"); - - tabView->setMinimumHeight(200); - groupLayout->addWidget(tabView); - - scrollContent_->layout()->addWidget(groupBox); -} - -void TabViewDemo::createCloseableTabsSection() { - QGroupBox* groupBox = new QGroupBox("Closeable Tabs (可关闭的选项卡)", this); - QVBoxLayout* groupLayout = new QVBoxLayout(groupBox); - groupLayout->setSpacing(12); - groupLayout->setContentsMargins(16, 24, 16, 16); - - // TabView with closeable tabs - TabView* tabView = new TabView(this); - - // Helper lambda to create closeable tab content - auto createCloseableTab = [](const QString& name) -> QWidget* { - QWidget* tab = new QWidget(); - QVBoxLayout* layout = new QVBoxLayout(tab); - layout->addWidget(new QLabel(QString("Tab: %1\n\nThis tab can be closed.").arg(name))); - layout->addStretch(); - return tab; - }; - - // Add closeable tabs - tabView->addTab(createCloseableTab("Document 1"), "Document 1"); - tabView->addTab(createCloseableTab("Document 2"), "Document 2"); - tabView->addTab(createCloseableTab("Document 3"), "Document 3"); - - // Set tabs closeable using our custom close button - for (int i = 0; i < tabView->count(); ++i) { - tabView->setTabCloseable(i, true); - } - - tabView->setMinimumHeight(200); - groupLayout->addWidget(tabView); - - // Hint label - QLabel* hintLabel = new QLabel("Note: Click the '×' button on each tab to close it.", this); - hintLabel->setStyleSheet("color: gray; font-size: 11px;"); - groupLayout->addWidget(hintLabel); - - scrollContent_->layout()->addWidget(groupBox); -} - -void TabViewDemo::createTabPositionSection() { - QGroupBox* groupBox = new QGroupBox("Tab Position (选项卡位置)", this); - QVBoxLayout* groupLayout = new QVBoxLayout(groupBox); - groupLayout->setSpacing(12); - groupLayout->setContentsMargins(16, 24, 16, 16); - - // Top position (default) - QHBoxLayout* topLayout = new QHBoxLayout(); - QLabel* topLabel = new QLabel("Top:", this); - topLabel->setMinimumWidth(80); - TabView* topTabView = new TabView(this); - topTabView->addTab(new QLabel("Top Position Content"), "Tab 1"); - topTabView->addTab(new QLabel("Another Tab"), "Tab 2"); - topTabView->setTabPosition(QTabWidget::North); - topTabView->setMinimumHeight(120); - topLayout->addWidget(topLabel); - topLayout->addWidget(topTabView); - groupLayout->addLayout(topLayout); - - // Bottom position - QHBoxLayout* bottomLayout = new QHBoxLayout(); - QLabel* bottomLabel = new QLabel("Bottom:", this); - bottomLabel->setMinimumWidth(80); - TabView* bottomTabView = new TabView(this); - bottomTabView->addTab(new QLabel("Bottom Position Content"), "Tab 1"); - bottomTabView->addTab(new QLabel("Another Tab"), "Tab 2"); - bottomTabView->setTabPosition(QTabWidget::South); - bottomTabView->setMinimumHeight(120); - bottomLayout->addWidget(bottomLabel); - bottomLayout->addWidget(bottomTabView); - groupLayout->addLayout(bottomLayout); - - // Left position - QHBoxLayout* leftLayout = new QHBoxLayout(); - QLabel* leftLabel = new QLabel("Left:", this); - leftLabel->setMinimumWidth(80); - TabView* leftTabView = new TabView(this); - leftTabView->addTab(new QLabel("Left\nPosition\nContent"), "Tab 1"); - leftTabView->addTab(new QLabel("Another\nTab"), "Tab 2"); - leftTabView->setTabPosition(QTabWidget::West); - leftTabView->setMinimumHeight(120); - leftLayout->addWidget(leftLabel); - leftLayout->addWidget(leftTabView); - groupLayout->addLayout(leftLayout); - - // Right position - QHBoxLayout* rightLayout = new QHBoxLayout(); - QLabel* rightLabel = new QLabel("Right:", this); - rightLabel->setMinimumWidth(80); - TabView* rightTabView = new TabView(this); - rightTabView->addTab(new QLabel("Right\nPosition\nContent"), "Tab 1"); - rightTabView->addTab(new QLabel("Another\nTab"), "Tab 2"); - rightTabView->setTabPosition(QTabWidget::East); - rightTabView->setMinimumHeight(120); - rightLayout->addWidget(rightLabel); - rightLayout->addWidget(rightTabView); - groupLayout->addLayout(rightLayout); - - scrollContent_->layout()->addWidget(groupBox); -} - -void TabViewDemo::createDisabledTabsSection() { - QGroupBox* groupBox = new QGroupBox("Disabled Tabs (禁用的选项卡)", this); - QVBoxLayout* groupLayout = new QVBoxLayout(groupBox); - groupLayout->setSpacing(12); - groupLayout->setContentsMargins(16, 24, 16, 16); - - // TabView with disabled tabs - TabView* tabView = new TabView(this); - - // Add tabs - QWidget* activeTab = new QWidget(); - QVBoxLayout* activeLayout = new QVBoxLayout(activeTab); - activeLayout->addWidget(new QLabel("Active Tab\n\nThis tab is enabled.")); - activeLayout->addStretch(); - tabView->addTab(activeTab, "Active"); - - QWidget* disabledTab = new QWidget(); - QVBoxLayout* disabledLayout = new QVBoxLayout(disabledTab); - disabledLayout->addWidget(new QLabel("Disabled Tab\n\nThis tab is disabled.")); - disabledLayout->addStretch(); - int disabledIndex = tabView->addTab(disabledTab, "Disabled"); - - QWidget* anotherActiveTab = new QWidget(); - QVBoxLayout* anotherLayout = new QVBoxLayout(anotherActiveTab); - anotherLayout->addWidget(new QLabel("Another Active\n\nThis tab is also enabled.")); - anotherLayout->addStretch(); - tabView->addTab(anotherActiveTab, "Active 2"); - - // Disable the second tab - tabView->setTabEnabled(disabledIndex, false); - - tabView->setMinimumHeight(200); - groupLayout->addWidget(tabView); - - // Hint label - QLabel* hintLabel = new QLabel("Note: The 'Disabled' tab cannot be selected.", this); - hintLabel->setStyleSheet("color: gray; font-size: 11px;"); - groupLayout->addWidget(hintLabel); - - scrollContent_->layout()->addWidget(groupBox); -} - -QGroupBox* TabViewDemo::createGroupBox(const QString& title) { - QGroupBox* groupBox = new QGroupBox(title, this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - return groupBox; -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/TabViewDemo.h b/example/ui/widget/material_example/demos/TabViewDemo.h deleted file mode 100644 index 74309834e..000000000 --- a/example/ui/widget/material_example/demos/TabViewDemo.h +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @file TabViewDemo.h - * @brief Material Design 3 TabView Demo - Widget Component - */ - -#pragma once - -#include -#include -#include -class QGroupBox; - -namespace cf::ui::example { - -class TabViewDemo : public QWidget { - Q_OBJECT - - public: - explicit TabViewDemo(QWidget* parent = nullptr); - ~TabViewDemo() override = default; - - QString title() const { return "TabView"; } - QString description() const { return "Material Design 3 TabView Component"; } - - private: - void setupUI(); - void createBasicTabsSection(); - void createTabsWithIconsSection(); - void createCloseableTabsSection(); - void createTabPositionSection(); - void createDisabledTabsSection(); - QGroupBox* createGroupBox(const QString& title); - - QWidget* scrollContent_; - QVBoxLayout* layout_; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/TableViewDemo.cpp b/example/ui/widget/material_example/demos/TableViewDemo.cpp deleted file mode 100644 index 23b54ec68..000000000 --- a/example/ui/widget/material_example/demos/TableViewDemo.cpp +++ /dev/null @@ -1,427 +0,0 @@ -/** - * @file TableViewDemo.cpp - * @brief Material Design 3 TableView Demo - Implementation - */ - -#include "TableViewDemo.h" - -#include "ui/widget/material/widget/tableview/tableview.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace cf::ui::widget::material; - -namespace cf::ui::example { - -TableViewDemo::TableViewDemo(QWidget* parent) : QWidget(parent) { - - setupUI(); - createBasicTableSection(); - createSortingSection(); - createColumnHeadersSection(); - createSelectionModesSection(); - createEditableCellsSection(); -} - -void TableViewDemo::setupUI() { - layout_ = new QVBoxLayout(this); - layout_->setContentsMargins(24, 24, 24, 24); - layout_->setSpacing(16); - - // Scroll area for content - QScrollArea* scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); - - scrollContent_ = new QWidget(); - QVBoxLayout* scrollLayout = new QVBoxLayout(scrollContent_); - scrollLayout->setSpacing(24); - scrollLayout->setContentsMargins(0, 0, 0, 0); - - scrollArea->setWidget(scrollContent_); - layout_->addWidget(scrollArea, 1); -} - -void TableViewDemo::createBasicTableSection() { - QGroupBox* groupBox = createGroupBox("Basic Table (基础表格)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Basic table view - TableView* tableView = new TableView(this); - tableView->setMinimumHeight(250); - tableView->setShowHeader(true); - tableView->setGridStyle(TableGridStyle::Both); - tableView->setAlternatingRowColors(true); - tableView->setRowHeight(TableRowHeight::Standard); - - basicTableModel_ = new QStandardItemModel(this); - basicTableModel_->setColumnCount(4); - basicTableModel_->setHorizontalHeaderLabels({"Name", "Age", "Department", "Status"}); - - // Add sample data - QList row1; - row1 << new QStandardItem("Alice Johnson") << new QStandardItem("28") - << new QStandardItem("Engineering") << new QStandardItem("Active"); - basicTableModel_->appendRow(row1); - - QList row2; - row2 << new QStandardItem("Bob Smith") << new QStandardItem("35") - << new QStandardItem("Marketing") << new QStandardItem("Active"); - basicTableModel_->appendRow(row2); - - QList row3; - row3 << new QStandardItem("Carol Williams") << new QStandardItem("42") - << new QStandardItem("Finance") << new QStandardItem("On Leave"); - basicTableModel_->appendRow(row3); - - QList row4; - row4 << new QStandardItem("David Brown") << new QStandardItem("31") - << new QStandardItem("Engineering") << new QStandardItem("Active"); - basicTableModel_->appendRow(row4); - - QList row5; - row5 << new QStandardItem("Eve Davis") << new QStandardItem("26") << new QStandardItem("Sales") - << new QStandardItem("Active"); - basicTableModel_->appendRow(row5); - - tableView->setModel(basicTableModel_); - - // Adjust column widths - tableView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); - tableView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); - tableView->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); - tableView->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents); - - layout->addWidget(tableView); - scrollContent_->layout()->addWidget(groupBox); -} - -void TableViewDemo::createSortingSection() { - QGroupBox* groupBox = createGroupBox("Sortable Columns (可排序列)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Sortable table view - TableView* tableView = new TableView(this); - tableView->setMinimumHeight(250); - tableView->setShowHeader(true); - tableView->setGridStyle(TableGridStyle::Horizontal); - tableView->setAlternatingRowColors(true); - - sortableModel_ = new QStandardItemModel(this); - sortableModel_->setColumnCount(4); - sortableModel_->setHorizontalHeaderLabels({"Product", "Price", "Stock", "Rating"}); - - sortableModel_->setSortRole(Qt::DisplayRole); - - // Add sample data with sortable values - QList row1; - row1 << new QStandardItem("Laptop") << new QStandardItem("999") << new QStandardItem("45") - << new QStandardItem("4.5"); - sortableModel_->appendRow(row1); - - QList row2; - row2 << new QStandardItem("Mouse") << new QStandardItem("25") << new QStandardItem("120") - << new QStandardItem("4.2"); - sortableModel_->appendRow(row2); - - QList row3; - row3 << new QStandardItem("Keyboard") << new QStandardItem("75") << new QStandardItem("85") - << new QStandardItem("4.7"); - sortableModel_->appendRow(row3); - - QList row4; - row4 << new QStandardItem("Monitor") << new QStandardItem("350") << new QStandardItem("30") - << new QStandardItem("4.3"); - sortableModel_->appendRow(row4); - - QList row5; - row5 << new QStandardItem("Headphones") << new QStandardItem("150") << new QStandardItem("60") - << new QStandardItem("4.6"); - sortableModel_->appendRow(row5); - - tableView->setModel(sortableModel_); - tableView->setSortingEnabled(true); - - // Enable sortable indicator - QHeaderView* header = tableView->horizontalHeader(); - header->setSectionsClickable(true); - header->setSortIndicatorShown(true); - - QLabel* infoLabel = - new QLabel("Click column headers to sort by that column (按列标题排序)", this); - infoLabel->setWordWrap(true); - layout->addWidget(infoLabel); - - layout->addWidget(tableView); - scrollContent_->layout()->addWidget(groupBox); -} - -void TableViewDemo::createColumnHeadersSection() { - QGroupBox* groupBox = new QGroupBox("Column Headers (列标题配置)", this); - QGridLayout* gridLayout = new QGridLayout(groupBox); - gridLayout->setSpacing(16); - gridLayout->setContentsMargins(16, 24, 16, 16); - - // Table with custom header styling - QLabel* customHeaderLabel = new QLabel("Custom Header Style:", this); - TableView* customHeaderTable = new TableView(this); - customHeaderTable->setMinimumHeight(200); - customHeaderTable->setShowHeader(true); - customHeaderTable->setGridStyle(TableGridStyle::Both); - - headerModel_ = new QStandardItemModel(this); - headerModel_->setColumnCount(3); - headerModel_->setHorizontalHeaderLabels({"ID", "Task Name", "Priority"}); - - QList row1; - row1 << new QStandardItem("001") << new QStandardItem("Design Review") - << new QStandardItem("High"); - headerModel_->appendRow(row1); - - QList row2; - row2 << new QStandardItem("002") << new QStandardItem("Code Review") - << new QStandardItem("Medium"); - headerModel_->appendRow(row2); - - QList row3; - row3 << new QStandardItem("003") << new QStandardItem("Testing") << new QStandardItem("Low"); - headerModel_->appendRow(row3); - - customHeaderTable->setModel(headerModel_); - - gridLayout->addWidget(customHeaderLabel, 0, 0); - gridLayout->addWidget(customHeaderTable, 1, 0); - - // Table without header - QLabel* noHeaderLabel = new QLabel("No Header (无标题):", this); - TableView* noHeaderTable = new TableView(this); - noHeaderTable->setMinimumHeight(200); - noHeaderTable->setShowHeader(false); - noHeaderTable->setGridStyle(TableGridStyle::Horizontal); - - QStandardItemModel* noHeaderModel = new QStandardItemModel(this); - noHeaderModel->setColumnCount(2); - - QList nhRow1; - nhRow1 << new QStandardItem("Apple") << new QStandardItem("$1.50"); - noHeaderModel->appendRow(nhRow1); - - QList nhRow2; - nhRow2 << new QStandardItem("Banana") << new QStandardItem("$0.75"); - noHeaderModel->appendRow(nhRow2); - - QList nhRow3; - nhRow3 << new QStandardItem("Orange") << new QStandardItem("$1.25"); - noHeaderModel->appendRow(nhRow3); - - noHeaderTable->setModel(noHeaderModel); - - gridLayout->addWidget(noHeaderLabel, 0, 1); - gridLayout->addWidget(noHeaderTable, 1, 1); - - gridLayout->setColumnStretch(0, 1); - gridLayout->setColumnStretch(1, 1); - - scrollContent_->layout()->addWidget(groupBox); -} - -void TableViewDemo::createSelectionModesSection() { - QGroupBox* groupBox = new QGroupBox("Selection Modes (选择模式)", this); - QGridLayout* gridLayout = new QGridLayout(groupBox); - gridLayout->setSpacing(16); - gridLayout->setContentsMargins(16, 24, 16, 16); - - // Single selection - QLabel* singleLabel = new QLabel("Single Selection (单选):", this); - TableView* singleSelectTable = new TableView(this); - singleSelectTable->setMinimumHeight(200); - singleSelectTable->setSelectionMode(QAbstractItemView::SingleSelection); - singleSelectTable->setSelectionBehavior(QAbstractItemView::SelectRows); - singleSelectTable->setShowHeader(true); - singleSelectTable->setGridStyle(TableGridStyle::Both); - - singleSelectModel_ = new QStandardItemModel(this); - singleSelectModel_->setColumnCount(3); - singleSelectModel_->setHorizontalHeaderLabels({"First Name", "Last Name", "Email"}); - - QList ssRow1; - ssRow1 << new QStandardItem("John") << new QStandardItem("Doe") - << new QStandardItem("john@example.com"); - singleSelectModel_->appendRow(ssRow1); - - QList ssRow2; - ssRow2 << new QStandardItem("Jane") << new QStandardItem("Smith") - << new QStandardItem("jane@example.com"); - singleSelectModel_->appendRow(ssRow2); - - QList ssRow3; - ssRow3 << new QStandardItem("Bob") << new QStandardItem("Jones") - << new QStandardItem("bob@example.com"); - singleSelectModel_->appendRow(ssRow3); - - singleSelectTable->setModel(singleSelectModel_); - - gridLayout->addWidget(singleLabel, 0, 0); - gridLayout->addWidget(singleSelectTable, 1, 0); - - // Multi selection - QLabel* multiLabel = new QLabel("Multi Selection (多选):", this); - TableView* multiSelectTable = new TableView(this); - multiSelectTable->setMinimumHeight(200); - multiSelectTable->setSelectionMode(QAbstractItemView::MultiSelection); - multiSelectTable->setSelectionBehavior(QAbstractItemView::SelectRows); - multiSelectTable->setShowHeader(true); - multiSelectTable->setGridStyle(TableGridStyle::Both); - - multiSelectModel_ = new QStandardItemModel(this); - multiSelectModel_->setColumnCount(3); - multiSelectModel_->setHorizontalHeaderLabels({"Item", "Quantity", "Location"}); - - QList msRow1; - msRow1 << new QStandardItem("Widget A") << new QStandardItem("10") - << new QStandardItem("Warehouse 1"); - multiSelectModel_->appendRow(msRow1); - - QList msRow2; - msRow2 << new QStandardItem("Widget B") << new QStandardItem("25") - << new QStandardItem("Warehouse 2"); - multiSelectModel_->appendRow(msRow2); - - QList msRow3; - msRow3 << new QStandardItem("Widget C") << new QStandardItem("15") - << new QStandardItem("Warehouse 1"); - multiSelectModel_->appendRow(msRow3); - - QList msRow4; - msRow4 << new QStandardItem("Widget D") << new QStandardItem("30") - << new QStandardItem("Warehouse 3"); - multiSelectModel_->appendRow(msRow4); - - multiSelectTable->setModel(multiSelectModel_); - - gridLayout->addWidget(multiLabel, 2, 0); - gridLayout->addWidget(multiSelectTable, 3, 0); - - // Extended selection (Ctrl+Click, Shift+Click) - QLabel* extendedLabel = new QLabel("Extended Selection (扩展选择):", this); - TableView* extendedSelectTable = new TableView(this); - extendedSelectTable->setMinimumHeight(200); - extendedSelectTable->setSelectionMode(QAbstractItemView::ExtendedSelection); - extendedSelectTable->setSelectionBehavior(QAbstractItemView::SelectRows); - extendedSelectTable->setShowHeader(true); - extendedSelectTable->setGridStyle(TableGridStyle::Both); - - QStandardItemModel* extendedModel = new QStandardItemModel(this); - extendedModel->setColumnCount(2); - extendedModel->setHorizontalHeaderLabels({"Project", "Status"}); - - QList esRow1; - esRow1 << new QStandardItem("Alpha") << new QStandardItem("Complete"); - extendedModel->appendRow(esRow1); - - QList esRow2; - esRow2 << new QStandardItem("Beta") << new QStandardItem("In Progress"); - extendedModel->appendRow(esRow2); - - QList esRow3; - esRow3 << new QStandardItem("Gamma") << new QStandardItem("Planning"); - extendedModel->appendRow(esRow3); - - extendedSelectTable->setModel(extendedModel); - - gridLayout->addWidget(extendedLabel, 0, 1); - gridLayout->addWidget(extendedSelectTable, 1, 1); - - gridLayout->setColumnStretch(0, 1); - gridLayout->setColumnStretch(1, 1); - - scrollContent_->layout()->addWidget(groupBox); -} - -void TableViewDemo::createEditableCellsSection() { - QGroupBox* groupBox = createGroupBox("Editable Cells (可编辑单元格)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Editable table view - TableView* tableView = new TableView(this); - tableView->setMinimumHeight(250); - tableView->setShowHeader(true); - tableView->setGridStyle(TableGridStyle::Both); - tableView->setAlternatingRowColors(true); - - editableModel_ = new QStandardItemModel(this); - editableModel_->setColumnCount(4); - editableModel_->setHorizontalHeaderLabels({"First Name", "Last Name", "Role", "Notes"}); - - // Add sample data - QList row1; - row1 << new QStandardItem("Sarah") << new QStandardItem("Connor") - << new QStandardItem("Developer") << new QStandardItem("Frontend specialist"); - editableModel_->appendRow(row1); - - QList row2; - row2 << new QStandardItem("Mike") << new QStandardItem("Ross") << new QStandardItem("Designer") - << new QStandardItem("UI/UX expert"); - editableModel_->appendRow(row2); - - QList row3; - row3 << new QStandardItem("Lisa") << new QStandardItem("Wong") << new QStandardItem("Manager") - << new QStandardItem("Team lead"); - editableModel_->appendRow(row3); - - QList row4; - row4 << new QStandardItem("Tom") << new QStandardItem("Anderson") - << new QStandardItem("Developer") << new QStandardItem("Backend specialist"); - editableModel_->appendRow(row4); - - // Make all items editable - for (int row = 0; row < editableModel_->rowCount(); ++row) { - for (int col = 0; col < editableModel_->columnCount(); ++col) { - QStandardItem* item = editableModel_->item(row, col); - if (item) { - item->setFlags(item->flags() | Qt::ItemIsEditable); - } - } - } - - tableView->setModel(editableModel_); - tableView->setEditTriggers(QAbstractItemView::DoubleClicked | - QAbstractItemView::EditKeyPressed); - - QLabel* infoLabel = new QLabel("Double-click any cell or press F2 to edit. Changes apply to " - "the model. (双击单元格或按F2编辑)", - this); - infoLabel->setWordWrap(true); - layout->addWidget(infoLabel); - - layout->addWidget(tableView); - scrollContent_->layout()->addWidget(groupBox); -} - -void TableViewDemo::onSortIndicatorChanged(int logicalIndex, Qt::SortOrder order) { - Q_UNUSED(logicalIndex); - Q_UNUSED(order); - // Handle sort indicator changes if needed -} - -void TableViewDemo::onSelectionChanged() { - // Handle selection changes if needed -} - -QGroupBox* TableViewDemo::createGroupBox(const QString& title) { - QGroupBox* groupBox = new QGroupBox(title, this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - return groupBox; -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/TableViewDemo.h b/example/ui/widget/material_example/demos/TableViewDemo.h deleted file mode 100644 index e7c5c8a07..000000000 --- a/example/ui/widget/material_example/demos/TableViewDemo.h +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @file TableViewDemo.h - * @brief Material Design 3 TableView Demo - Widget Component - */ - -#pragma once - -#include -#include -#include -class QGroupBox; -class QStandardItemModel; - -namespace cf::ui::example { - -class TableViewDemo : public QWidget { - Q_OBJECT - - public: - explicit TableViewDemo(QWidget* parent = nullptr); - ~TableViewDemo() override = default; - - QString title() const { return "TableView"; } - QString description() const { return "Material Design 3 TableView Component"; } - - private: - void setupUI(); - void createBasicTableSection(); - void createSortingSection(); - void createColumnHeadersSection(); - void createSelectionModesSection(); - void createEditableCellsSection(); - QGroupBox* createGroupBox(const QString& title); - - void onSortIndicatorChanged(int logicalIndex, Qt::SortOrder order); - void onSelectionChanged(); - - QWidget* scrollContent_; - QVBoxLayout* layout_; - QStandardItemModel* basicTableModel_; - QStandardItemModel* sortableModel_; - QStandardItemModel* headerModel_; - QStandardItemModel* singleSelectModel_; - QStandardItemModel* multiSelectModel_; - QStandardItemModel* editableModel_; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/TextAreaDemo.cpp b/example/ui/widget/material_example/demos/TextAreaDemo.cpp deleted file mode 100644 index 128aeaf46..000000000 --- a/example/ui/widget/material_example/demos/TextAreaDemo.cpp +++ /dev/null @@ -1,261 +0,0 @@ -/** - * @file TextAreaDemo.cpp - * @brief Material Design 3 TextArea Demo - Implementation - */ - -#include "TextAreaDemo.h" - -#include "ui/widget/material/widget/textarea/textarea.h" -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace cf::ui::widget::material; -using TextAreaVariant = cf::ui::widget::material::TextArea::TextAreaVariant; - -namespace cf::ui::example { - -TextAreaDemo::TextAreaDemo(QWidget* parent) : QWidget(parent) { - setupUI(); - createTextAreaVariantsSection(); - createTextAreaStatesSection(); - createCharacterCounterSection(); - createMinMaxLinesSection(); - createInteractionDemoSection(); - - // Add stretch at the end -} - -void TextAreaDemo::setupUI() { - // Main layout - layout_ = new QVBoxLayout(this); - layout_->setContentsMargins(16, 16, 16, 16); - layout_->setSpacing(16); - - // Scroll area for content - QScrollArea* scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scrollContent_ = new QWidget(); - QVBoxLayout* scrollLayout = new QVBoxLayout(scrollContent_); - scrollLayout->setSpacing(24); - scrollLayout->setContentsMargins(0, 0, 0, 0); - - scrollArea->setWidget(scrollContent_); - layout_->addWidget(scrollArea, 1); - - // Use scrollLayout for adding content - layout_ = scrollLayout; -} - -void TextAreaDemo::createTextAreaVariantsSection() { - QGroupBox* groupBox = new QGroupBox("TextArea Variants (文本域变体)", this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(16); - layout->setContentsMargins(16, 24, 16, 16); - - // Filled TextArea - QHBoxLayout* filledLayout = new QHBoxLayout(); - QLabel* filledLabel = new QLabel("Filled:", this); - filledLabel->setMinimumWidth(100); - TextArea* filledTextArea = new TextArea(TextAreaVariant::Filled, this); - filledTextArea->setLabel("Label"); - filledTextArea->setPlaceholderText("Enter text here..."); - filledTextArea->setMinLines(3); - filledLayout->addWidget(filledLabel); - filledLayout->addWidget(filledTextArea, 1); - layout->addLayout(filledLayout); - - // Outlined TextArea - QHBoxLayout* outlinedLayout = new QHBoxLayout(); - QLabel* outlinedLabel = new QLabel("Outlined:", this); - outlinedLabel->setMinimumWidth(100); - TextArea* outlinedTextArea = new TextArea(TextAreaVariant::Outlined, this); - outlinedTextArea->setLabel("Label"); - outlinedTextArea->setPlaceholderText("Enter text here..."); - outlinedTextArea->setMinLines(3); - outlinedLayout->addWidget(outlinedLabel); - outlinedLayout->addWidget(outlinedTextArea, 1); - layout->addLayout(outlinedLayout); - - layout_->addWidget(groupBox); -} - -void TextAreaDemo::createTextAreaStatesSection() { - QGroupBox* groupBox = createGroupBox("TextArea States (文本域状态)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Normal (enabled) text areas - QHBoxLayout* normalLayout = new QHBoxLayout(); - QLabel* normalLabel = new QLabel("Normal:", this); - normalLabel->setMinimumWidth(100); - TextArea* normalTextArea = new TextArea("Enabled text area", TextAreaVariant::Filled, this); - normalTextArea->setLabel("Message"); - normalTextArea->setMinLines(2); - normalLayout->addWidget(normalLabel); - normalLayout->addWidget(normalTextArea, 1); - layout->addLayout(normalLayout); - - // Disabled text areas - QHBoxLayout* disabledLayout = new QHBoxLayout(); - QLabel* disabledLabel = new QLabel("Disabled:", this); - disabledLabel->setMinimumWidth(100); - TextArea* disabledFilled = - new TextArea("Disabled filled text area", TextAreaVariant::Filled, this); - disabledFilled->setLabel("Message"); - disabledFilled->setEnabled(false); - disabledFilled->setMinLines(2); - TextArea* disabledOutlined = - new TextArea("Disabled outlined text area", TextAreaVariant::Outlined, this); - disabledOutlined->setLabel("Message"); - disabledOutlined->setEnabled(false); - disabledOutlined->setMinLines(2); - disabledLayout->addWidget(disabledLabel); - disabledLayout->addWidget(disabledFilled, 1); - disabledLayout->addWidget(disabledOutlined, 1); - layout->addLayout(disabledLayout); - - // Error state - QHBoxLayout* errorLayout = new QHBoxLayout(); - QLabel* errorLabel = new QLabel("Error:", this); - errorLabel->setMinimumWidth(100); - TextArea* errorTextArea = new TextArea(TextAreaVariant::Outlined, this); - errorTextArea->setLabel("Email"); - errorTextArea->setErrorText("Please enter a valid email address"); - errorTextArea->setMinLines(2); - errorLayout->addWidget(errorLabel); - errorLayout->addWidget(errorTextArea, 1); - layout->addLayout(errorLayout); - - layout_->addWidget(groupBox); -} - -void TextAreaDemo::createCharacterCounterSection() { - QGroupBox* groupBox = createGroupBox("Character Counter (字符计数器)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // TextArea with character counter - QHBoxLayout* counterLayout = new QHBoxLayout(); - QLabel* counterLabel = new QLabel("With Counter:", this); - counterLabel->setMinimumWidth(100); - TextArea* counterTextArea = new TextArea(TextAreaVariant::Filled, this); - counterTextArea->setLabel("Bio"); - counterTextArea->setPlaceholderText("Tell us about yourself..."); - counterTextArea->setMaxLength(150); - counterTextArea->setShowCharacterCounter(true); - counterTextArea->setMinLines(3); - counterLayout->addWidget(counterLabel); - counterLayout->addWidget(counterTextArea, 1); - layout->addLayout(counterLayout); - - layout_->addWidget(groupBox); -} - -void TextAreaDemo::createMinMaxLinesSection() { - QGroupBox* groupBox = createGroupBox("Min/Max Lines (最小/最大行数)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Fixed 3 lines - QHBoxLayout* fixed3Layout = new QHBoxLayout(); - QLabel* fixed3Label = new QLabel("Fixed 3 lines:", this); - fixed3Label->setMinimumWidth(100); - TextArea* fixed3TextArea = new TextArea(TextAreaVariant::Outlined, this); - fixed3TextArea->setLabel("Short description"); - fixed3TextArea->setMinLines(3); - fixed3TextArea->setMaxLines(3); - fixed3TextArea->setPlaceholderText("Exactly 3 lines visible..."); - fixed3Layout->addWidget(fixed3Label); - fixed3Layout->addWidget(fixed3TextArea, 1); - layout->addLayout(fixed3Layout); - - // Auto-grow (1-5 lines) - QHBoxLayout* autoGrowLayout = new QHBoxLayout(); - QLabel* autoGrowLabel = new QLabel("Auto-grow (1-5):", this); - autoGrowLabel->setMinimumWidth(100); - TextArea* autoGrowTextArea = new TextArea(TextAreaVariant::Filled, this); - autoGrowTextArea->setLabel("Description"); - autoGrowTextArea->setMinLines(1); - autoGrowTextArea->setMaxLines(5); - autoGrowTextArea->setPlaceholderText("Starts at 1 line, grows up to 5 lines..."); - autoGrowLayout->addWidget(autoGrowLabel); - autoGrowLayout->addWidget(autoGrowTextArea, 1); - layout->addLayout(autoGrowLayout); - - // Multi-line (5 lines min) - QHBoxLayout* multiLineLayout = new QHBoxLayout(); - QLabel* multiLineLabel = new QLabel("Multi-line (5+):", this); - multiLineLabel->setMinimumWidth(100); - TextArea* multiLineTextArea = new TextArea(TextAreaVariant::Outlined, this); - multiLineTextArea->setLabel("Comments"); - multiLineTextArea->setHelperText("Share your thoughts with us"); - multiLineTextArea->setMinLines(5); - multiLineTextArea->setPlaceholderText("Type your message here..."); - multiLineLayout->addWidget(multiLineLabel); - multiLineLayout->addWidget(multiLineTextArea, 1); - layout->addLayout(multiLineLayout); - - layout_->addWidget(groupBox); -} - -void TextAreaDemo::createInteractionDemoSection() { - QGroupBox* groupBox = createGroupBox("Interaction Demo (交互演示)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Live character count demo - QHBoxLayout* demoLayout = new QHBoxLayout(); - QLabel* demoLabel = new QLabel("Type to test:", this); - demoLabel->setMinimumWidth(100); - TextArea* demoTextArea = new TextArea(TextAreaVariant::Filled, this); - demoTextArea->setLabel("Your Message"); - demoTextArea->setMaxLength(200); - demoTextArea->setShowCharacterCounter(true); - demoTextArea->setMinLines(4); - demoTextArea->setPlaceholderText("Start typing to see the character counter update..."); - demoLayout->addWidget(demoLabel); - demoLayout->addWidget(demoTextArea, 1); - layout->addLayout(demoLayout); - - // Character count display - QHBoxLayout* countDisplayLayout = new QHBoxLayout(); - QLabel* countLabel = new QLabel("Character count:", this); - countLabel->setMinimumWidth(100); - charCountLabel_ = new QLabel("0 / 200", this); - QFont countFont("Segoe UI", 12, QFont::Bold); - charCountLabel_->setFont(countFont); - countDisplayLayout->addWidget(countLabel); - countDisplayLayout->addWidget(charCountLabel_); - countDisplayLayout->addStretch(); - layout->addLayout(countDisplayLayout); - - // Connect text changed signal - connect(demoTextArea, &QTextEdit::textChanged, this, &TextAreaDemo::onDemoTextChanged); - - layout_->addWidget(groupBox); -} - -void TextAreaDemo::onDemoTextChanged() { - // Update character count display - TextArea* textArea = qobject_cast(sender()); - if (textArea && charCountLabel_) { - int count = textArea->toPlainText().length(); - int max = textArea->maxLength(); - charCountLabel_->setText(QString("%1 / %2").arg(count).arg(max)); - } -} - -QGroupBox* TextAreaDemo::createGroupBox(const QString& title) { - QGroupBox* groupBox = new QGroupBox(title, this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - return groupBox; -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/TextAreaDemo.h b/example/ui/widget/material_example/demos/TextAreaDemo.h deleted file mode 100644 index c18cec392..000000000 --- a/example/ui/widget/material_example/demos/TextAreaDemo.h +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @file TextAreaDemo.h - * @brief Material Design 3 TextArea Demo Widget - */ - -#pragma once -#include -#include -#include -#include - -class QGroupBox; - -namespace cf::ui::example { - -class TextAreaDemo : public QWidget { - Q_OBJECT - public: - explicit TextAreaDemo(QWidget* parent = nullptr); - ~TextAreaDemo() override = default; - - QString title() const { return "TextArea"; } - QString description() const { return "Material Design 3 TextArea Component"; } - - private: - void setupUI(); - void createTextAreaVariantsSection(); - void createTextAreaStatesSection(); - void createCharacterCounterSection(); - void createMinMaxLinesSection(); - void createInteractionDemoSection(); - - QGroupBox* createGroupBox(const QString& title); - void onDemoTextChanged(); - - QWidget* scrollContent_; - QVBoxLayout* layout_; - QLabel* charCountLabel_; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/TextFieldDemo.cpp b/example/ui/widget/material_example/demos/TextFieldDemo.cpp deleted file mode 100644 index 7be709474..000000000 --- a/example/ui/widget/material_example/demos/TextFieldDemo.cpp +++ /dev/null @@ -1,265 +0,0 @@ -/** - * @file TextFieldDemo.cpp - * @brief Material Design 3 TextField Demo - Implementation - */ - -#include "TextFieldDemo.h" - -#include "ui/widget/material/widget/textfield/textfield.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace cf::ui::widget::material; -using TextFieldVariant = cf::ui::widget::material::TextField::TextFieldVariant; - -namespace cf::ui::example { - -TextFieldDemo::TextFieldDemo(QWidget* parent) : QWidget(parent) { - setupUI(); - createTextFieldVariantsSection(); - createTextFieldStatesSection(); - createTextFieldWithIconsSection(); - createTextFieldWithErrorSection(); - createTextFieldAdvancedSection(); - - // Add stretch at the end -} - -void TextFieldDemo::setupUI() { - // Main layout - layout_ = new QVBoxLayout(this); - layout_->setContentsMargins(16, 16, 16, 16); - layout_->setSpacing(16); - - // Scroll area for content - QScrollArea* scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scrollContent_ = new QWidget(); - QVBoxLayout* scrollLayout = new QVBoxLayout(scrollContent_); - scrollLayout->setSpacing(24); - scrollLayout->setContentsMargins(0, 0, 0, 0); - - scrollArea->setWidget(scrollContent_); - layout_->addWidget(scrollArea, 1); - - // Use scrollLayout for adding content - layout_ = scrollLayout; -} - -void TextFieldDemo::createTextFieldVariantsSection() { - QGroupBox* groupBox = new QGroupBox("TextField Variants (文本框变体)", this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(16); - layout->setContentsMargins(16, 24, 16, 16); - - // Filled TextField - QHBoxLayout* filledLayout = new QHBoxLayout(); - QLabel* filledLabel = new QLabel("Filled:", this); - filledLabel->setMinimumWidth(120); - TextField* filledField = new TextField("Filled text", TextFieldVariant::Filled, this); - filledField->setLabel("Label"); - connect(filledField, &QLineEdit::textChanged, this, &TextFieldDemo::onTextChanged); - filledLayout->addWidget(filledLabel); - filledLayout->addWidget(filledField); - filledLayout->addStretch(); - layout->addLayout(filledLayout); - - // Outlined TextField - QHBoxLayout* outlinedLayout = new QHBoxLayout(); - QLabel* outlinedLabel = new QLabel("Outlined:", this); - outlinedLabel->setMinimumWidth(120); - TextField* outlinedField = new TextField("Outlined text", TextFieldVariant::Outlined, this); - outlinedField->setLabel("Label"); - connect(outlinedField, &QLineEdit::textChanged, this, &TextFieldDemo::onTextChanged); - outlinedLayout->addWidget(outlinedLabel); - outlinedLayout->addWidget(outlinedField); - outlinedLayout->addStretch(); - layout->addLayout(outlinedLayout); - - layout_->addWidget(groupBox); -} - -void TextFieldDemo::createTextFieldStatesSection() { - QGroupBox* groupBox = createGroupBox("TextField States (文本框状态)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Normal (enabled) text fields - QHBoxLayout* normalLayout = new QHBoxLayout(); - QLabel* normalLabel = new QLabel("Normal:", this); - normalLabel->setMinimumWidth(120); - TextField* normalField = new TextField(TextFieldVariant::Filled, this); - normalField->setLabel("Enter text"); - normalLayout->addWidget(normalLabel); - normalLayout->addWidget(normalField); - normalLayout->addStretch(); - layout->addLayout(normalLayout); - - // Disabled text fields - QHBoxLayout* disabledLayout = new QHBoxLayout(); - QLabel* disabledLabel = new QLabel("Disabled:", this); - disabledLabel->setMinimumWidth(120); - TextField* disabledFilled = new TextField("Disabled text", TextFieldVariant::Filled, this); - disabledFilled->setLabel("Label"); - disabledFilled->setEnabled(false); - TextField* disabledOutlined = new TextField("Disabled text", TextFieldVariant::Outlined, this); - disabledOutlined->setLabel("Label"); - disabledOutlined->setEnabled(false); - disabledLayout->addWidget(disabledLabel); - disabledLayout->addWidget(disabledFilled); - disabledLayout->addWidget(disabledOutlined); - disabledLayout->addStretch(); - layout->addLayout(disabledLayout); - - // Read-only text fields - QHBoxLayout* readonlyLayout = new QHBoxLayout(); - QLabel* readonlyLabel = new QLabel("Read-only:", this); - readonlyLabel->setMinimumWidth(120); - TextField* readonlyField = new TextField("Read-only text", TextFieldVariant::Filled, this); - readonlyField->setLabel("Label"); - readonlyField->setReadOnly(true); - readonlyLayout->addWidget(readonlyLabel); - readonlyLayout->addWidget(readonlyField); - readonlyLayout->addStretch(); - layout->addLayout(readonlyLayout); - - layout_->addWidget(groupBox); -} - -void TextFieldDemo::createTextFieldWithIconsSection() { - QGroupBox* groupBox = createGroupBox("Text Fields with Icons (带图标的文本框)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Filled with prefix icon - QHBoxLayout* filledPrefixLayout = new QHBoxLayout(); - QLabel* filledPrefixLabel = new QLabel("Filled + Prefix:", this); - filledPrefixLabel->setMinimumWidth(120); - TextField* filledPrefixField = new TextField(TextFieldVariant::Filled, this); - filledPrefixField->setLabel("Search"); - filledPrefixField->setPrefixIcon(QApplication::style()->standardIcon(QStyle::SP_FileIcon)); - filledPrefixLayout->addWidget(filledPrefixLabel); - filledPrefixLayout->addWidget(filledPrefixField); - filledPrefixLayout->addStretch(); - layout->addLayout(filledPrefixLayout); - - // Outlined with suffix icon - QHBoxLayout* outlinedSuffixLayout = new QHBoxLayout(); - QLabel* outlinedSuffixLabel = new QLabel("Outlined + Suffix:", this); - outlinedSuffixLabel->setMinimumWidth(120); - TextField* outlinedSuffixField = new TextField(TextFieldVariant::Outlined, this); - outlinedSuffixField->setLabel("Email"); - outlinedSuffixField->setSuffixIcon( - QApplication::style()->standardIcon(QStyle::SP_DialogOkButton)); - outlinedSuffixLayout->addWidget(outlinedSuffixLabel); - outlinedSuffixLayout->addWidget(outlinedSuffixField); - outlinedSuffixLayout->addStretch(); - layout->addLayout(outlinedSuffixLayout); - - // Password field - QHBoxLayout* passwordLayout = new QHBoxLayout(); - QLabel* passwordLabel = new QLabel("Password:", this); - passwordLabel->setMinimumWidth(120); - TextField* passwordField = new TextField(TextFieldVariant::Filled, this); - passwordField->setLabel("Password"); - passwordField->setEchoMode(TextField::Password); - passwordField->setPrefixIcon( - QApplication::style()->standardIcon(QStyle::SP_DialogCancelButton)); - passwordLayout->addWidget(passwordLabel); - passwordLayout->addWidget(passwordField); - passwordLayout->addStretch(); - layout->addLayout(passwordLayout); - - layout_->addWidget(groupBox); -} - -void TextFieldDemo::createTextFieldWithErrorSection() { - QGroupBox* groupBox = createGroupBox("Helper Text & Error (辅助文本与错误)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // With helper text - QHBoxLayout* helperLayout = new QHBoxLayout(); - QLabel* helperLabel = new QLabel("Helper text:", this); - helperLabel->setMinimumWidth(120); - TextField* helperField = new TextField(TextFieldVariant::Filled, this); - helperField->setLabel("Username"); - helperField->setHelperText("Enter your username"); - helperLayout->addWidget(helperLabel); - helperLayout->addWidget(helperField); - helperLayout->addStretch(); - layout->addLayout(helperLayout); - - // With error text - QHBoxLayout* errorLayout = new QHBoxLayout(); - QLabel* errorLabel = new QLabel("Error state:", this); - errorLabel->setMinimumWidth(120); - TextField* errorField = new TextField("invalid-email", TextFieldVariant::Outlined, this); - errorField->setLabel("Email"); - errorField->setErrorText("Please enter a valid email address"); - errorLayout->addWidget(errorLabel); - errorLayout->addWidget(errorField); - errorLayout->addStretch(); - layout->addLayout(errorLayout); - - layout_->addWidget(groupBox); -} - -void TextFieldDemo::createTextFieldAdvancedSection() { - QGroupBox* groupBox = createGroupBox("Advanced Features (高级功能)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - // Character counter - QHBoxLayout* counterLayout = new QHBoxLayout(); - QLabel* counterLabel = new QLabel("Character limit:", this); - counterLabel->setMinimumWidth(120); - TextField* counterField = new TextField(TextFieldVariant::Outlined, this); - counterField->setLabel("Bio"); - counterField->setHelperText("Tell us about yourself"); - counterField->setMaxLength(50); - counterField->setShowCharacterCounter(true); - counterLayout->addWidget(counterLabel); - counterLayout->addWidget(counterField); - counterLayout->addStretch(); - layout->addLayout(counterLayout); - - // Multi-line simulation (text area hint) - QHBoxLayout* multilineLayout = new QHBoxLayout(); - QLabel* multilineLabel = new QLabel("TextArea hint:", this); - multilineLabel->setMinimumWidth(120); - TextField* multilineField = - new TextField("This is a single-line TextField. For multi-line input, use TextArea.", - TextFieldVariant::Filled, this); - multilineField->setLabel("Description"); - multilineField->setHelperText("Enter a description (multi-line not supported in TextField)"); - multilineLayout->addWidget(multilineLabel); - multilineLayout->addWidget(multilineField); - multilineLayout->addStretch(); - layout->addLayout(multilineLayout); - - layout_->addWidget(groupBox); -} - -void TextFieldDemo::onTextChanged(const QString& text) { - // Example handler for text changes - Q_UNUSED(text) - // Could update UI based on text content -} - -QGroupBox* TextFieldDemo::createGroupBox(const QString& title) { - QGroupBox* groupBox = new QGroupBox(title, this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - return groupBox; -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/TextFieldDemo.h b/example/ui/widget/material_example/demos/TextFieldDemo.h deleted file mode 100644 index 10d930776..000000000 --- a/example/ui/widget/material_example/demos/TextFieldDemo.h +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @file TextFieldDemo.h - * @brief Material Design 3 TextField Demo Widget - */ - -#pragma once -#include -#include -#include -#include - -namespace cf::ui::example { - -class TextFieldDemo : public QWidget { - Q_OBJECT - public: - explicit TextFieldDemo(QWidget* parent = nullptr); - ~TextFieldDemo() override = default; - - QString title() const { return "TextField"; } - QString description() const { return "Material Design 3 TextField Component"; } - - private: - void setupUI(); - void createTextFieldVariantsSection(); - void createTextFieldStatesSection(); - void createTextFieldWithIconsSection(); - void createTextFieldWithErrorSection(); - void createTextFieldAdvancedSection(); - - QGroupBox* createGroupBox(const QString& title); - void onTextChanged(const QString& text); - - QWidget* scrollContent_; - QVBoxLayout* layout_; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/TreeViewDemo.cpp b/example/ui/widget/material_example/demos/TreeViewDemo.cpp deleted file mode 100644 index d8a86d11c..000000000 --- a/example/ui/widget/material_example/demos/TreeViewDemo.cpp +++ /dev/null @@ -1,340 +0,0 @@ -/** - * @file TreeViewDemo.cpp - * @brief Material Design 3 TreeView Demo - Implementation - */ - -#include "TreeViewDemo.h" - -#include "ui/widget/material/widget/treeview/treeview.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace cf::ui::widget::material; - -namespace cf::ui::example { - -TreeViewDemo::TreeViewDemo(QWidget* parent) : QWidget(parent) { - - setupUI(); - createBasicTreeSection(); - createNestedItemsSection(); - createExpandCollapseSection(); - createSelectionModesSection(); - createIconsInTreeSection(); -} - -void TreeViewDemo::setupUI() { - layout_ = new QVBoxLayout(this); - layout_->setContentsMargins(24, 24, 24, 24); - layout_->setSpacing(16); - - // Scroll area for content - QScrollArea* scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scrollContent_ = new QWidget(); - QVBoxLayout* scrollLayout = new QVBoxLayout(scrollContent_); - scrollLayout->setSpacing(24); - scrollLayout->setContentsMargins(0, 0, 0, 0); - - scrollArea->setWidget(scrollContent_); - layout_->addWidget(scrollArea, 1); -} - -void TreeViewDemo::createBasicTreeSection() { - QGroupBox* groupBox = new QGroupBox("Basic Tree (基本树)", this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - - basicTreeModel_ = new QStandardItemModel(this); - basicTreeModel_->setHorizontalHeaderLabels(QStringList() << "Name" << "Type"); - - // Root items - QStandardItem* root1 = new QStandardItem("Documents"); - QStandardItem* root1Type = new QStandardItem("Folder"); - root1->appendRow(QList() - << new QStandardItem("Report.pdf") << new QStandardItem("PDF")); - root1->appendRow(QList() - << new QStandardItem("Presentation.pptx") << new QStandardItem("PowerPoint")); - basicTreeModel_->appendRow(QList() << root1 << root1Type); - - QStandardItem* root2 = new QStandardItem("Pictures"); - QStandardItem* root2Type = new QStandardItem("Folder"); - root2->appendRow(QList() - << new QStandardItem("Photo1.jpg") << new QStandardItem("JPEG")); - root2->appendRow(QList() - << new QStandardItem("Photo2.png") << new QStandardItem("PNG")); - basicTreeModel_->appendRow(QList() << root2 << root2Type); - - QStandardItem* root3 = new QStandardItem("Music"); - QStandardItem* root3Type = new QStandardItem("Folder"); - basicTreeModel_->appendRow(QList() << root3 << root3Type); - - TreeView* treeView = new TreeView(this); - treeView->setModel(basicTreeModel_); - treeView->setMinimumHeight(200); - - layout->addWidget(treeView); - scrollContent_->layout()->addWidget(groupBox); -} - -void TreeViewDemo::createNestedItemsSection() { - QGroupBox* groupBox = createGroupBox("Nested Items (嵌套项目)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - nestedTreeModel_ = new QStandardItemModel(this); - nestedTreeModel_->setHorizontalHeaderLabels(QStringList() << "Project Structure"); - - // Create deeply nested structure - QStandardItem* projectRoot = new QStandardItem("MyProject"); - nestedTreeModel_->appendRow(projectRoot); - - QStandardItem* srcFolder = new QStandardItem("src"); - projectRoot->appendRow(srcFolder); - - QStandardItem* coreFolder = new QStandardItem("core"); - srcFolder->appendRow(coreFolder); - coreFolder->appendRow(new QStandardItem("engine.cpp")); - coreFolder->appendRow(new QStandardItem("renderer.cpp")); - - QStandardItem* uiFolder = new QStandardItem("ui"); - srcFolder->appendRow(uiFolder); - uiFolder->appendRow(new QStandardItem("main_window.cpp")); - uiFolder->appendRow(new QStandardItem("widgets.cpp")); - - QStandardItem* componentsFolder = new QStandardItem("components"); - uiFolder->appendRow(componentsFolder); - componentsFolder->appendRow(new QStandardItem("button.cpp")); - componentsFolder->appendRow(new QStandardItem("treeview.cpp")); - - QStandardItem* resourcesFolder = new QStandardItem("resources"); - projectRoot->appendRow(resourcesFolder); - resourcesFolder->appendRow(new QStandardItem("icons/")); - resourcesFolder->appendRow(new QStandardItem("styles/")); - - QStandardItem* buildFolder = new QStandardItem("build"); - projectRoot->appendRow(buildFolder); - buildFolder->appendRow(new QStandardItem("debug/")); - buildFolder->appendRow(new QStandardItem("release/")); - - TreeView* treeView = new TreeView(this); - treeView->setModel(nestedTreeModel_); - treeView->setMinimumHeight(250); - treeView->expandAll(); - - layout->addWidget(treeView); - scrollContent_->layout()->addWidget(groupBox); -} - -void TreeViewDemo::createExpandCollapseSection() { - QGroupBox* groupBox = createGroupBox("Expand/Collapse (展开/折叠)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - expandTreeModel_ = new QStandardItemModel(this); - expandTreeModel_->setHorizontalHeaderLabels(QStringList() << "Categories"); - - // Create expandable categories - QStandardItem* electronics = new QStandardItem("Electronics (电子产品)"); - electronics->appendRow(new QStandardItem("Laptops (笔记本电脑)")); - electronics->appendRow(new QStandardItem("Smartphones (智能手机)")); - electronics->appendRow(new QStandardItem("Tablets (平板电脑)")); - expandTreeModel_->appendRow(electronics); - - QStandardItem* clothing = new QStandardItem("Clothing (服装)"); - clothing->appendRow(new QStandardItem("Shirts (衬衫)")); - clothing->appendRow(new QStandardItem("Pants (裤子)")); - clothing->appendRow(new QStandardItem("Shoes (鞋子)")); - expandTreeModel_->appendRow(clothing); - - QStandardItem* books = new QStandardItem("Books (书籍)"); - books->appendRow(new QStandardItem("Fiction (小说)")); - books->appendRow(new QStandardItem("Non-Fiction (非小说)")); - books->appendRow(new QStandardItem("Technical (技术)")); - expandTreeModel_->appendRow(books); - - TreeView* treeView = new TreeView(this); - treeView->setModel(expandTreeModel_); - treeView->setMinimumHeight(200); - - // Control buttons - QHBoxLayout* buttonLayout = new QHBoxLayout(); - QPushButton* expandAllBtn = new QPushButton("Expand All (全部展开)", this); - QPushButton* collapseAllBtn = new QPushButton("Collapse All (全部折叠)", this); - - connect(expandAllBtn, &QPushButton::clicked, this, &TreeViewDemo::onExpandAll); - connect(collapseAllBtn, &QPushButton::clicked, this, &TreeViewDemo::onCollapseAll); - - buttonLayout->addWidget(expandAllBtn); - buttonLayout->addWidget(collapseAllBtn); - buttonLayout->addStretch(); - - layout->addWidget(treeView); - layout->addLayout(buttonLayout); - scrollContent_->layout()->addWidget(groupBox); -} - -void TreeViewDemo::createSelectionModesSection() { - QGroupBox* groupBox = createGroupBox("Selection Modes (选择模式)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - selectionTreeModel_ = new QStandardItemModel(this); - selectionTreeModel_->setHorizontalHeaderLabels(QStringList() << "Items"); - - // Add sample items - QStandardItem* item1 = new QStandardItem("Item 1"); - item1->appendRow(new QStandardItem("Subitem 1.1")); - item1->appendRow(new QStandardItem("Subitem 1.2")); - selectionTreeModel_->appendRow(item1); - - QStandardItem* item2 = new QStandardItem("Item 2"); - item2->appendRow(new QStandardItem("Subitem 2.1")); - selectionTreeModel_->appendRow(item2); - - QStandardItem* item3 = new QStandardItem("Item 3"); - selectionTreeModel_->appendRow(item3); - - QGridLayout* gridLayout = new QGridLayout(); - - // Single Selection - TreeView* singleSelectTree = new TreeView(this); - singleSelectTree->setModel(selectionTreeModel_); - singleSelectTree->setSelectionMode(QAbstractItemView::SingleSelection); - singleSelectTree->setMinimumHeight(150); - - QLabel* singleLabel = new QLabel("Single Selection:\n(单选)", this); - singleLabel->setAlignment(Qt::AlignCenter); - - // Multi Selection - TreeView* multiSelectTree = new TreeView(this); - multiSelectTree->setModel(selectionTreeModel_); - multiSelectTree->setSelectionMode(QAbstractItemView::MultiSelection); - multiSelectTree->setMinimumHeight(150); - - QLabel* multiLabel = new QLabel("Multi Selection:\n(多选)", this); - multiLabel->setAlignment(Qt::AlignCenter); - - // Extended Selection - TreeView* extendedSelectTree = new TreeView(this); - extendedSelectTree->setModel(selectionTreeModel_); - extendedSelectTree->setSelectionMode(QAbstractItemView::ExtendedSelection); - extendedSelectTree->setMinimumHeight(150); - - QLabel* extendedLabel = new QLabel("Extended Selection:\n(扩展选择)", this); - extendedLabel->setAlignment(Qt::AlignCenter); - - gridLayout->addWidget(singleLabel, 0, 0); - gridLayout->addWidget(singleSelectTree, 1, 0); - gridLayout->addWidget(multiLabel, 0, 1); - gridLayout->addWidget(multiSelectTree, 1, 1); - gridLayout->addWidget(extendedLabel, 0, 2); - gridLayout->addWidget(extendedSelectTree, 1, 2); - - layout->addLayout(gridLayout); - scrollContent_->layout()->addWidget(groupBox); -} - -void TreeViewDemo::createIconsInTreeSection() { - QGroupBox* groupBox = createGroupBox("Icons in Tree (带图标的树)"); - QVBoxLayout* layout = static_cast(groupBox->layout()); - - iconTreeModel_ = new QStandardItemModel(this); - iconTreeModel_->setHorizontalHeaderLabels(QStringList() << "File Explorer"); - - // Get standard icons - QIcon folderIcon = QApplication::style()->standardIcon(QStyle::SP_DirIcon); - QIcon fileIcon = QApplication::style()->standardIcon(QStyle::SP_FileIcon); - QIcon computerIcon = QApplication::style()->standardIcon(QStyle::SP_ComputerIcon); - QIcon driveIcon = QApplication::style()->standardIcon(QStyle::SP_DriveHDIcon); - - // Create items with icons - QStandardItem* myComputer = new QStandardItem(computerIcon, "My Computer"); - iconTreeModel_->appendRow(myComputer); - - QStandardItem* driveC = new QStandardItem(driveIcon, "Local Disk (C:)"); - myComputer->appendRow(driveC); - - QStandardItem* programFiles = new QStandardItem(folderIcon, "Program Files"); - driveC->appendRow(programFiles); - programFiles->appendRow(new QStandardItem(fileIcon, "Application.exe")); - programFiles->appendRow(new QStandardItem(fileIcon, "Tool.exe")); - - QStandardItem* users = new QStandardItem(folderIcon, "Users"); - driveC->appendRow(users); - users->appendRow(new QStandardItem(folderIcon, "Documents")); - users->appendRow(new QStandardItem(folderIcon, "Pictures")); - - QStandardItem* driveD = new QStandardItem(driveIcon, "Data Disk (D:)"); - myComputer->appendRow(driveD); - - QStandardItem* projects = new QStandardItem(folderIcon, "Projects"); - driveD->appendRow(projects); - projects->appendRow(new QStandardItem(fileIcon, "project1.cpp")); - projects->appendRow(new QStandardItem(fileIcon, "project2.cpp")); - projects->appendRow(new QStandardItem(fileIcon, "README.md")); - - TreeView* treeView = new TreeView(this); - treeView->setModel(iconTreeModel_); - treeView->setMinimumHeight(250); - treeView->expandAll(); - - layout->addWidget(treeView); - scrollContent_->layout()->addWidget(groupBox); -} - -void TreeViewDemo::onSelectionChanged() { - // Handle selection changes if needed -} - -void TreeViewDemo::onExpandAll() { - QObject* senderObj = sender(); - if (senderObj) { - QWidget* widget = qobject_cast(senderObj->parent()); - if (widget) { - TreeView* treeView = widget->findChild(); - if (treeView) { - treeView->expandAll(); - } - } - } -} - -void TreeViewDemo::onCollapseAll() { - QObject* senderObj = sender(); - if (senderObj) { - QWidget* widget = qobject_cast(senderObj->parent()); - if (widget) { - TreeView* treeView = widget->findChild(); - if (treeView) { - treeView->collapseAll(); - } - } - } -} - -void TreeViewDemo::onAddItem() { - // Placeholder for adding items -} - -void TreeViewDemo::onRemoveItem() { - // Placeholder for removing items -} - -QGroupBox* TreeViewDemo::createGroupBox(const QString& title) { - QGroupBox* groupBox = new QGroupBox(title, this); - QVBoxLayout* layout = new QVBoxLayout(groupBox); - layout->setSpacing(12); - layout->setContentsMargins(16, 24, 16, 16); - return groupBox; -} - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/demos/TreeViewDemo.h b/example/ui/widget/material_example/demos/TreeViewDemo.h deleted file mode 100644 index 0e87f74a9..000000000 --- a/example/ui/widget/material_example/demos/TreeViewDemo.h +++ /dev/null @@ -1,50 +0,0 @@ -/** - * @file TreeViewDemo.h - * @brief Material Design 3 TreeView Demo - Widget Component - */ - -#pragma once - -#include -#include -#include -class QGroupBox; -class QStandardItemModel; - -namespace cf::ui::example { - -class TreeViewDemo : public QWidget { - Q_OBJECT - - public: - explicit TreeViewDemo(QWidget* parent = nullptr); - ~TreeViewDemo() override = default; - - QString title() const { return "TreeView"; } - QString description() const { return "Material Design 3 TreeView Component"; } - - private: - void setupUI(); - void createBasicTreeSection(); - void createNestedItemsSection(); - void createExpandCollapseSection(); - void createSelectionModesSection(); - void createIconsInTreeSection(); - QGroupBox* createGroupBox(const QString& title); - - void onSelectionChanged(); - void onExpandAll(); - void onCollapseAll(); - void onAddItem(); - void onRemoveItem(); - - QWidget* scrollContent_; - QVBoxLayout* layout_; - QStandardItemModel* basicTreeModel_; - QStandardItemModel* nestedTreeModel_; - QStandardItemModel* expandTreeModel_; - QStandardItemModel* selectionTreeModel_; - QStandardItemModel* iconTreeModel_; -}; - -} // namespace cf::ui::example diff --git a/example/ui/widget/material_example/main.cpp b/example/ui/widget/material_example/main.cpp deleted file mode 100644 index b37f47a61..000000000 --- a/example/ui/widget/material_example/main.cpp +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @file main.cpp - * @brief Material Widget Gallery - Main Entry Point - * - * Entry point for the Material Design 3 Widget Gallery application. - * Displays all Material Design widgets with various states and configurations. - * - * @author CFDesktop Team - * @date 2026-03-18 - * @version 0.1 - */ - -#include "MaterialGalleryWindow.h" -#include "ui/widget/material/application/material_application.h" - -#include - -using namespace cf::ui::widget::material; - -int main(int argc, char* argv[]) { - MaterialApplication app(argc, argv); - - // Set application metadata - app.setApplicationName("Material Gallery"); - app.setApplicationVersion("0.1"); - app.setOrganizationName("CFDesktop"); - - // Create and show main window - cf::ui::example::MaterialGalleryWindow window; - window.show(); - - return app.exec(); -} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b2dd81b8a..829eb2946 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -47,8 +47,7 @@ add_subdirectory(logger) # Add System Test add_subdirectory(system) -# Add ui tests -add_subdirectory(ui) +# NOTE: ui tests now live in the QuarkWidgets submodule (third_party/QuarkWidgets/test). # Add desktop tests add_subdirectory(desktop) diff --git a/test/ui/CMakeLists.txt b/test/ui/CMakeLists.txt deleted file mode 100644 index d1987fb48..000000000 --- a/test/ui/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -# UI tests -add_subdirectory(base) -add_subdirectory(core) -add_subdirectory(components) -add_subdirectory(widget) diff --git a/test/ui/base/CMakeLists.txt b/test/ui/base/CMakeLists.txt deleted file mode 100644 index 26aee8218..000000000 --- a/test/ui/base/CMakeLists.txt +++ /dev/null @@ -1,67 +0,0 @@ -# ============================================================================= -# ui/base tests - math_helper_test -# ============================================================================= -add_gtest_executable( - TEST_NAME math_helper_test - SOURCE_FILE math_helper_test.cpp - LINK_LIBRARIES UI::base;GTest::gtest;GTest::gtest_main - LABELS "ui;base;math" - LOG_MODULE ui_base_tests -) - -# ============================================================================= -# ui/base tests - color_test -# ============================================================================= -add_gtest_executable( - TEST_NAME color_test - SOURCE_FILE color_test.cpp - LINK_LIBRARIES UI::base;GTest::gtest;GTest::gtest_main - LABELS "ui;base;color" - LOG_MODULE ui_base_tests -) - -# ============================================================================= -# ui/base tests - color_helper_test -# ============================================================================= -add_gtest_executable( - TEST_NAME color_helper_test - SOURCE_FILE color_helper_test.cpp - LINK_LIBRARIES UI::base;GTest::gtest;GTest::gtest_main - LABELS "ui;base;color" - LOG_MODULE ui_base_tests -) - -# ============================================================================= -# ui/base tests - device_pixel_test -# ============================================================================= -add_gtest_executable( - TEST_NAME device_pixel_test - SOURCE_FILE device_pixel_test.cpp - LINK_LIBRARIES UI::base;GTest::gtest;GTest::gtest_main - LABELS "ui;base;device" - LOG_MODULE ui_base_tests -) - -# ============================================================================= -# ui/base tests - geometry_helper_test -# ============================================================================= -add_gtest_executable( - TEST_NAME geometry_helper_test - SOURCE_FILE geometry_helper_test.cpp - LINK_LIBRARIES UI::base;GTest::gtest;GTest::gtest_main - LABELS "ui;base;geometry" - LOG_MODULE ui_base_tests -) - -# ============================================================================= -# ui/base tests - easing_test -# ============================================================================= -add_gtest_executable( - TEST_NAME easing_test - SOURCE_FILE easing_test.cpp - LINK_LIBRARIES UI::base;GTest::gtest;GTest::gtest_main - LABELS "ui;base;easing" - LOG_MODULE ui_base_tests -) - -log_info("UI_Base_Tests" "Configured ui/base tests") diff --git a/test/ui/base/color_helper_test.cpp b/test/ui/base/color_helper_test.cpp deleted file mode 100644 index d4e77ab33..000000000 --- a/test/ui/base/color_helper_test.cpp +++ /dev/null @@ -1,422 +0,0 @@ -/** - * @file color_helper_test.cpp - * @brief Comprehensive unit tests for cf::ui::base color helper functions - * - * Test Coverage: - * 1. blend() - Color blending with ratio - * 2. elevationOverlay() - Material Design elevation overlay - * 3. contrastRatio() - WCAG contrast ratio calculation - * 4. tonalPalette() - Tonal palette generation from key color - */ - -#include "ui/base/color_helper.h" -#include - -// ============================================================================= -// Test Suite 1: blend() -// ============================================================================= - -TEST(ColorHelperTest, Blend_Ratio0_ReturnsBase) { - cf::ui::base::CFColor base(QColor(255, 0, 0)); // Red - cf::ui::base::CFColor overlay(QColor(0, 0, 255)); // Blue - - cf::ui::base::CFColor result = cf::ui::base::blend(base, overlay, 0.0f); - - EXPECT_EQ(result.native_color(), base.native_color()); -} - -TEST(ColorHelperTest, Blend_Ratio1_ReturnsOverlay) { - cf::ui::base::CFColor base(QColor(255, 0, 0)); // Red - cf::ui::base::CFColor overlay(QColor(0, 0, 255)); // Blue - - cf::ui::base::CFColor result = cf::ui::base::blend(base, overlay, 1.0f); - - EXPECT_EQ(result.native_color(), overlay.native_color()); -} - -TEST(ColorHelperTest, Blend_Ratio05_ReturnsMidpoint) { - cf::ui::base::CFColor base(QColor(0, 0, 0)); // Black - cf::ui::base::CFColor overlay(QColor(255, 255, 255)); // White - - cf::ui::base::CFColor result = cf::ui::base::blend(base, overlay, 0.5f); - - // Should be gray - EXPECT_EQ(result.native_color(), QColor(128, 128, 128)); -} - -TEST(ColorHelperTest, Blend_RedAndWhite) { - cf::ui::base::CFColor base(QColor(255, 0, 0)); // Red - cf::ui::base::CFColor overlay(QColor(255, 255, 255)); // White - - cf::ui::base::CFColor result = cf::ui::base::blend(base, overlay, 0.5f); - - // Pink-ish - EXPECT_GT(result.native_color().red(), 200); - EXPECT_GT(result.native_color().green(), 100); - EXPECT_GT(result.native_color().blue(), 100); -} - -TEST(ColorHelperTest, Blend_ClampsRatioAbove1) { - cf::ui::base::CFColor base(QColor(0, 0, 0)); // Black - cf::ui::base::CFColor overlay(QColor(255, 255, 255)); // White - - cf::ui::base::CFColor result = cf::ui::base::blend(base, overlay, 1.5f); - - EXPECT_EQ(result.native_color(), overlay.native_color()); -} - -TEST(ColorHelperTest, Blend_ClampsRatioBelow0) { - cf::ui::base::CFColor base(QColor(0, 0, 0)); // Black - cf::ui::base::CFColor overlay(QColor(255, 255, 255)); // White - - cf::ui::base::CFColor result = cf::ui::base::blend(base, overlay, -0.5f); - - EXPECT_EQ(result.native_color(), base.native_color()); -} - -TEST(ColorHelperTest, Blend_WithAlpha) { - cf::ui::base::CFColor base(QColor(255, 0, 0, 255)); // Opaque red - cf::ui::base::CFColor overlay(QColor(0, 0, 255, 128)); // Semi-transparent blue - - cf::ui::base::CFColor result = cf::ui::base::blend(base, overlay, 0.5f); - - // Alpha should also be blended - EXPECT_GT(result.native_color().alpha(), 128); - EXPECT_LT(result.native_color().alpha(), 255); -} - -TEST(ColorHelperTest, Blend_SameColor) { - cf::ui::base::CFColor base(QColor(100, 100, 100)); - cf::ui::base::CFColor overlay(QColor(100, 100, 100)); - - cf::ui::base::CFColor result = cf::ui::base::blend(base, overlay, 0.5f); - - EXPECT_EQ(result.native_color(), base.native_color()); -} - -TEST(ColorHelperTest, Blend_MultipleRatios) { - cf::ui::base::CFColor base(QColor(0, 0, 0)); - cf::ui::base::CFColor overlay(QColor(100, 0, 0)); - - auto r1 = cf::ui::base::blend(base, overlay, 0.25f); - auto r2 = cf::ui::base::blend(base, overlay, 0.5f); - auto r3 = cf::ui::base::blend(base, overlay, 0.75f); - - EXPECT_LT(r1.native_color().red(), r2.native_color().red()); - EXPECT_LT(r2.native_color().red(), r3.native_color().red()); -} - -// ============================================================================= -// Test Suite 2: elevationOverlay() -// ============================================================================= - -TEST(ColorHelperTest, ElevationOverlay_Level0_NoOverlay) { - cf::ui::base::CFColor surface(QColor(200, 200, 200)); - cf::ui::base::CFColor primary(QColor(100, 100, 255)); - - cf::ui::base::CFColor result = cf::ui::base::elevationOverlay(surface, primary, 0); - - // Level 0 should have no overlay (alpha = 0.00) - EXPECT_EQ(result.native_color(), surface.native_color()); -} - -TEST(ColorHelperTest, ElevationOverlay_PositiveLevel_Darkens) { - cf::ui::base::CFColor surface(QColor(255, 255, 255)); // White - cf::ui::base::CFColor primary(QColor(0, 0, 0)); // Black - - cf::ui::base::CFColor result = cf::ui::base::elevationOverlay(surface, primary, 5); - - // Should be darker than white - int total = - result.native_color().red() + result.native_color().green() + result.native_color().blue(); - EXPECT_LT(total, 765); // 765 = 255+255+255 -} - -TEST(ColorHelperTest, ElevationOverlay_ClampsNegativeElevation) { - cf::ui::base::CFColor surface(QColor(200, 200, 200)); - cf::ui::base::CFColor primary(QColor(100, 100, 100)); - - cf::ui::base::CFColor result = cf::ui::base::elevationOverlay(surface, primary, -10); - - // Should be clamped to level 0 - EXPECT_EQ(result.native_color(), surface.native_color()); -} - -TEST(ColorHelperTest, ElevationOverlay_ClampsElevationAbove5) { - cf::ui::base::CFColor surface(QColor(200, 200, 200)); - cf::ui::base::CFColor primary(QColor(0, 0, 0)); - - cf::ui::base::CFColor result1 = cf::ui::base::elevationOverlay(surface, primary, 5); - cf::ui::base::CFColor result2 = cf::ui::base::elevationOverlay(surface, primary, 10); - - // Both should be clamped to level 5 - EXPECT_EQ(result1.native_color(), result2.native_color()); -} - -TEST(ColorHelperTest, ElevationOverlay_IncreasingLevels) { - cf::ui::base::CFColor surface(QColor(255, 255, 255)); - cf::ui::base::CFColor primary(QColor(0, 0, 0)); - - auto level1 = cf::ui::base::elevationOverlay(surface, primary, 1); - auto level3 = cf::ui::base::elevationOverlay(surface, primary, 3); - auto level5 = cf::ui::base::elevationOverlay(surface, primary, 5); - - // Higher elevation = more overlay effect (darker) - int l1_total = - level1.native_color().red() + level1.native_color().green() + level1.native_color().blue(); - int l3_total = - level3.native_color().red() + level3.native_color().green() + level3.native_color().blue(); - int l5_total = - level5.native_color().red() + level5.native_color().green() + level5.native_color().blue(); - - EXPECT_GT(l1_total, l3_total); - EXPECT_GT(l3_total, l5_total); -} - -TEST(ColorHelperTest, ElevationOverlay_AlphaValues) { - // Verify alpha values match Material Design spec - cf::ui::base::CFColor surface(QColor(255, 255, 255)); - cf::ui::base::CFColor primary(QColor(0, 0, 0)); - - // Level 5 should have 0.17 alpha - cf::ui::base::CFColor result = cf::ui::base::elevationOverlay(surface, primary, 5); - - // Result should be 17% black blended with white - // Expected value: 255 * (1 - 0.17) = 211.65 ≈ 212 - EXPECT_NEAR(result.native_color().red(), 212, 2); -} - -// ============================================================================= -// Test Suite 3: contrastRatio() -// ============================================================================= - -TEST(ColorHelperTest, ContrastRatio_BlackWhite) { - cf::ui::base::CFColor black(Qt::black); - cf::ui::base::CFColor white(Qt::white); - - float ratio = cf::ui::base::contrastRatio(black, white); - - // Maximum contrast ratio is approximately 21:1 - EXPECT_NEAR(ratio, 21.0f, 1.0f); -} - -TEST(ColorHelperTest, ContrastRatio_SameColor) { - cf::ui::base::CFColor color(QColor(128, 128, 128)); - - float ratio = cf::ui::base::contrastRatio(color, color); - - EXPECT_FLOAT_EQ(ratio, 1.0f); -} - -TEST(ColorHelperTest, ContrastRatio_DarkGrayWhite) { - cf::ui::base::CFColor dark(QColor(50, 50, 50)); - cf::ui::base::CFColor white(Qt::white); - - float ratio = cf::ui::base::contrastRatio(dark, white); - - // Should be high contrast - EXPECT_GT(ratio, 10.0f); -} - -TEST(ColorHelperTest, ContrastRatio_RedGreen) { - cf::ui::base::CFColor red(Qt::red); - cf::ui::base::CFColor green(Qt::green); - - float ratio = cf::ui::base::contrastRatio(red, green); - - // Red and green have similar luminance, so contrast is low - EXPECT_LT(ratio, 3.0f); -} - -TEST(ColorHelperTest, ContrastRatio_WCAG_AA) { - // WCAG AA requires 4.5:1 for normal text - cf::ui::base::CFColor dark(QColor(30, 30, 30)); - cf::ui::base::CFColor light(QColor(240, 240, 240)); - - float ratio = cf::ui::base::contrastRatio(dark, light); - - EXPECT_GT(ratio, 4.5f); -} - -TEST(ColorHelperTest, ContrastRatio_WCAG_AAA) { - // WCAG AAA requires 7:1 for normal text - cf::ui::base::CFColor black(Qt::black); - cf::ui::base::CFColor white(Qt::white); - - float ratio = cf::ui::base::contrastRatio(black, white); - - EXPECT_GT(ratio, 7.0f); -} - -TEST(ColorHelperTest, ContrastRatio_Symmetric) { - cf::ui::base::CFColor color1(QColor(100, 50, 50)); - cf::ui::base::CFColor color2(QColor(200, 200, 200)); - - float ratio1 = cf::ui::base::contrastRatio(color1, color2); - float ratio2 = cf::ui::base::contrastRatio(color2, color1); - - EXPECT_FLOAT_EQ(ratio1, ratio2); -} - -TEST(ColorHelperTest, ContrastRatio_LightOnDark) { - cf::ui::base::CFColor dark(QColor(20, 20, 20)); - cf::ui::base::CFColor light(QColor(230, 230, 230)); - - float ratio = cf::ui::base::contrastRatio(dark, light); - - // Should exceed WCAG AA - EXPECT_GT(ratio, 4.5f); -} - -TEST(ColorHelperTest, ContrastRatio_GrayOnWhite) { - cf::ui::base::CFColor gray(QColor(120, 120, 120)); - cf::ui::base::CFColor white(Qt::white); - - float ratio = cf::ui::base::contrastRatio(gray, white); - - // Gray on white typically fails WCAG AA - EXPECT_LT(ratio, 4.5f); -} - -// ============================================================================= -// Test Suite 4: tonalPalette() -// ============================================================================= - -TEST(ColorHelperTest, TonalPalette_Size) { - cf::ui::base::CFColor keyColor(QColor(100, 150, 200)); - - QList palette = cf::ui::base::tonalPalette(keyColor); - - EXPECT_EQ(palette.size(), 13); -} - -TEST(ColorHelperTest, TonalPalette_ToneValues) { - cf::ui::base::CFColor keyColor(QColor(100, 150, 200)); - - QList palette = cf::ui::base::tonalPalette(keyColor); - - // Check standard tonal values: 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100 - EXPECT_NEAR(palette[0].tone(), 0.0f, 1.0f); - EXPECT_NEAR(palette[1].tone(), 10.0f, 1.0f); - EXPECT_NEAR(palette[5].tone(), 50.0f, 1.0f); - EXPECT_NEAR(palette[12].tone(), 100.0f, 1.0f); -} - -TEST(ColorHelperTest, TonalPalette_PreservesHue) { - cf::ui::base::CFColor keyColor(180.0f, 50.0f, 50.0f); - - QList palette = cf::ui::base::tonalPalette(keyColor); - - // All colors should have the same hue - for (const auto& color : palette) { - EXPECT_NEAR(color.hue(), 180.0f, 5.0f); - } -} - -TEST(ColorHelperTest, TonalPalette_PreservesChroma) { - cf::ui::base::CFColor keyColor(180.0f, 50.0f, 50.0f); - - QList palette = cf::ui::base::tonalPalette(keyColor); - - // All colors should have approximately the same chroma - // (may vary slightly due to tone constraints) - for (const auto& color : palette) { - EXPECT_GT(color.chroma(), 30.0f); - EXPECT_LT(color.chroma(), 70.0f); - } -} - -TEST(ColorHelperTest, TonalPalette_LightToDark) { - cf::ui::base::CFColor keyColor(180.0f, 50.0f, 50.0f); - - QList palette = cf::ui::base::tonalPalette(keyColor); - - // Tone should increase from index 0 to 12 - for (int i = 1; i < palette.size(); ++i) { - EXPECT_GT(palette[i].tone(), palette[i - 1].tone()); - } -} - -TEST(ColorHelperTest, TonalPalette_FirstColorIsNearBlack) { - cf::ui::base::CFColor keyColor(QColor(100, 150, 200)); - - QList palette = cf::ui::base::tonalPalette(keyColor); - - EXPECT_NEAR(palette[0].tone(), 0.0f, 1.0f); - int total = palette[0].native_color().red() + palette[0].native_color().green() + - palette[0].native_color().blue(); - EXPECT_LT(total, 50); -} - -TEST(ColorHelperTest, TonalPalette_LastColorIsNearWhite) { - cf::ui::base::CFColor keyColor(QColor(100, 150, 200)); - - QList palette = cf::ui::base::tonalPalette(keyColor); - - EXPECT_NEAR(palette[12].tone(), 100.0f, 1.0f); - int total = palette[12].native_color().red() + palette[12].native_color().green() + - palette[12].native_color().blue(); - EXPECT_GT(total, 700); -} - -TEST(ColorHelperTest, TonalPalette_MidTone) { - cf::ui::base::CFColor keyColor(180.0f, 50.0f, 50.0f); - - QList palette = cf::ui::base::tonalPalette(keyColor); - - // Index 5 should be tone 50 - EXPECT_NEAR(palette[5].tone(), 50.0f, 2.0f); -} - -TEST(ColorHelperTest, TonalPalette_DifferentKeyColors) { - cf::ui::base::CFColor red(0.0f, 80.0f, 50.0f); - cf::ui::base::CFColor blue(240.0f, 80.0f, 50.0f); - - QList redPalette = cf::ui::base::tonalPalette(red); - QList bluePalette = cf::ui::base::tonalPalette(blue); - - // Palettes should be different - EXPECT_NE(redPalette[6].native_color(), bluePalette[6].native_color()); -} - -// ============================================================================= -// Test Suite 5: Integration Tests -// ============================================================================= - -TEST(ColorHelperIntegration, BlendThenContrast) { - cf::ui::base::CFColor black(Qt::black); - cf::ui::base::CFColor white(Qt::white); - - auto blended = cf::ui::base::blend(black, white, 0.5f); - - // Contrast with white should be lower than black-white - float originalContrast = cf::ui::base::contrastRatio(black, white); - float blendedContrast = cf::ui::base::contrastRatio(blended, white); - - EXPECT_LT(blendedContrast, originalContrast); -} - -TEST(ColorHelperIntegration, ElevationThenContrast) { - cf::ui::base::CFColor surface(QColor(240, 240, 240)); - cf::ui::base::CFColor primary(QColor(30, 30, 30)); - - auto elevated = cf::ui::base::elevationOverlay(surface, primary, 3); - - // Elevated surface should have less contrast with primary - float originalContrast = cf::ui::base::contrastRatio(surface, primary); - float elevatedContrast = cf::ui::base::contrastRatio(elevated, primary); - - EXPECT_LT(elevatedContrast, originalContrast); -} - -TEST(ColorHelperIntegration, TonalPaletteThenContrast) { - cf::ui::base::CFColor keyColor(180.0f, 50.0f, 50.0f); - auto palette = cf::ui::base::tonalPalette(keyColor); - - // First and last colors should have high contrast - float contrast = cf::ui::base::contrastRatio(palette[0], palette[12]); - - EXPECT_GT(contrast, 10.0f); -} - -// No main() needed - using GTest::gtest_main diff --git a/test/ui/base/color_test.cpp b/test/ui/base/color_test.cpp deleted file mode 100644 index 15fbb8b68..000000000 --- a/test/ui/base/color_test.cpp +++ /dev/null @@ -1,384 +0,0 @@ -/** - * @file color_test.cpp - * @brief Comprehensive unit tests for cf::ui::base::CFColor class - * - * Test Coverage: - * 1. Construction (default, from QColor, from hex string, from HCT) - * 2. HCT component accessors (hue, chroma, tone) - * 3. Native color access - * 4. Relative luminance calculation (WCAG 2.1) - * 5. HCT-RGB conversion consistency - * 6. Invalid input handling - * 7. Boundary value clamping - */ - -#include "ui/base/color.h" -#include -#include - -// ============================================================================= -// Test Suite 1: Default Construction -// ============================================================================= - -TEST(CFColorTest, DefaultConstructor) { - cf::ui::base::CFColor color; - - // Default color should be black (invalid QColor defaults to black) - EXPECT_EQ(color.native_color(), Qt::black); - - // HCT values for black - EXPECT_FLOAT_EQ(color.hue(), 0.0f); - EXPECT_FLOAT_EQ(color.chroma(), 0.0f); - EXPECT_FLOAT_EQ(color.tone(), 0.0f); -} - -// ============================================================================= -// Test Suite 2: Construction from QColor -// ============================================================================= - -TEST(CFColorTest, ConstructFromQColor_Black) { - QColor qColor(Qt::black); - cf::ui::base::CFColor color(qColor); - - EXPECT_EQ(color.native_color(), Qt::black); - EXPECT_FLOAT_EQ(color.tone(), 0.0f); -} - -TEST(CFColorTest, ConstructFromQColor_White) { - QColor qColor(Qt::white); - cf::ui::base::CFColor color(qColor); - - EXPECT_EQ(color.native_color(), Qt::white); - EXPECT_FLOAT_EQ(color.tone(), 100.0f); -} - -TEST(CFColorTest, ConstructFromQColor_Red) { - QColor qColor(Qt::red); - cf::ui::base::CFColor color(qColor); - - EXPECT_EQ(color.native_color(), Qt::red); - EXPECT_GT(color.chroma(), 0.0f); // Red has high chroma -} - -TEST(CFColorTest, ConstructFromQColor_Green) { - QColor qColor(Qt::green); - cf::ui::base::CFColor color(qColor); - - EXPECT_EQ(color.native_color(), Qt::green); - EXPECT_GT(color.chroma(), 0.0f); -} - -TEST(CFColorTest, ConstructFromQColor_Blue) { - QColor qColor(Qt::blue); - cf::ui::base::CFColor color(qColor); - - EXPECT_EQ(color.native_color(), Qt::blue); - EXPECT_GT(color.chroma(), 0.0f); -} - -TEST(CFColorTest, ConstructFromQColor_Gray) { - QColor qColor(128, 128, 128); - cf::ui::base::CFColor color(qColor); - - EXPECT_EQ(color.native_color(), QColor(128, 128, 128)); - EXPECT_NEAR(color.chroma(), 0.0f, 1.0f); // Gray has low chroma - EXPECT_GT(color.tone(), 40.0f); - EXPECT_LT(color.tone(), 60.0f); -} - -TEST(CFColorTest, ConstructFromQColor_WithAlpha) { - QColor qColor(255, 0, 0, 128); // Semi-transparent red - cf::ui::base::CFColor color(qColor); - - EXPECT_EQ(color.native_color(), QColor(255, 0, 0, 128)); -} - -// ============================================================================= -// Test Suite 3: Construction from Hex String -// ============================================================================= - -TEST(CFColorTest, ConstructFromHex_SixDigit) { - cf::ui::base::CFColor color("#FF0000"); - EXPECT_EQ(color.native_color(), QColor(255, 0, 0)); -} - -TEST(CFColorTest, ConstructFromHex_EightDigit) { - cf::ui::base::CFColor color("#80FF0000"); // #AARRGGBB - EXPECT_EQ(color.native_color(), QColor(255, 0, 0, 128)); // Note: order is #AARRGGBB -} - -TEST(CFColorTest, ConstructFromHex_White) { - cf::ui::base::CFColor color("#FFFFFF"); - EXPECT_EQ(color.native_color(), Qt::white); - EXPECT_FLOAT_EQ(color.tone(), 100.0f); -} - -TEST(CFColorTest, ConstructFromHex_Black) { - cf::ui::base::CFColor color("#000000"); - EXPECT_EQ(color.native_color(), Qt::black); - EXPECT_FLOAT_EQ(color.tone(), 0.0f); -} - -TEST(CFColorTest, ConstructFromHex_Gray) { - cf::ui::base::CFColor color("#808080"); - EXPECT_EQ(color.native_color(), QColor(128, 128, 128)); -} - -TEST(CFColorTest, ConstructFromHex_WithWhitespace) { - cf::ui::base::CFColor color(" #FF0000 "); - EXPECT_EQ(color.native_color(), QColor(255, 0, 0)); -} - -TEST(CFColorTest, ConstructFromHex_NoHashSign) { - // Invalid format - should return black - cf::ui::base::CFColor color("FF0000"); - EXPECT_EQ(color.native_color(), Qt::black); -} - -TEST(CFColorTest, ConstructFromHex_InvalidLength) { - // Invalid format - should return black - cf::ui::base::CFColor color("#FFF"); - EXPECT_EQ(color.native_color(), Qt::black); -} - -TEST(CFColorTest, ConstructFromHex_EmptyString) { - cf::ui::base::CFColor color(""); - EXPECT_EQ(color.native_color(), Qt::black); -} - -// ============================================================================= -// Test Suite 4: Construction from HCT Values -// ============================================================================= - -TEST(CFColorTest, ConstructFromHCT_Basic) { - cf::ui::base::CFColor color(180.0f, 50.0f, 50.0f); // Cyan-ish - - EXPECT_FLOAT_EQ(color.hue(), 180.0f); - EXPECT_FLOAT_EQ(color.chroma(), 50.0f); - EXPECT_FLOAT_EQ(color.tone(), 50.0f); -} - -TEST(CFColorTest, ConstructFromHCT_Clamping) { - // Values should be clamped to valid ranges - cf::ui::base::CFColor color(500.0f, 200.0f, 150.0f); - - EXPECT_FLOAT_EQ(color.hue(), 360.0f); // Clamped to 360 - EXPECT_FLOAT_EQ(color.chroma(), 150.0f); // Clamped to 150 - EXPECT_FLOAT_EQ(color.tone(), 100.0f); // Clamped to 100 -} - -TEST(CFColorTest, ConstructFromHCT_NegativeValues) { - cf::ui::base::CFColor color(-10.0f, -5.0f, -10.0f); - - EXPECT_FLOAT_EQ(color.hue(), 0.0f); // Clamped to 0 - EXPECT_FLOAT_EQ(color.chroma(), 0.0f); // Clamped to 0 - EXPECT_FLOAT_EQ(color.tone(), 0.0f); // Clamped to 0 -} - -TEST(CFColorTest, ConstructFromHCT_ZeroChroma_Grayscale) { - cf::ui::base::CFColor color(0.0f, 0.0f, 50.0f); // Gray - - EXPECT_NEAR(color.chroma(), 0.0f, 0.1f); - EXPECT_NEAR(color.tone(), 50.0f, 1.0f); -} - -TEST(CFColorTest, ConstructFromHCT_Red) { - cf::ui::base::CFColor color(0.0f, 100.0f, 50.0f); // Red - - EXPECT_NEAR(color.hue(), 0.0f, 5.0f); - EXPECT_GT(color.native_color().red(), 200); // High red component -} - -TEST(CFColorTest, ConstructFromHCT_Green) { - cf::ui::base::CFColor color(120.0f, 80.0f, 50.0f); // Green - - EXPECT_NEAR(color.hue(), 120.0f, 10.0f); - EXPECT_GT(color.native_color().green(), 150); -} - -TEST(CFColorTest, ConstructFromHCT_Blue) { - cf::ui::base::CFColor color(240.0f, 80.0f, 50.0f); // Blue - - EXPECT_NEAR(color.hue(), 240.0f, 10.0f); - EXPECT_GT(color.native_color().blue(), 150); -} - -TEST(CFColorTest, ConstructFromHCT_LightTone) { - cf::ui::base::CFColor color(0.0f, 50.0f, 90.0f); // Light color - - EXPECT_GT(color.tone(), 85.0f); - // Light colors have higher RGB values - int total = - color.native_color().red() + color.native_color().green() + color.native_color().blue(); - EXPECT_GT(total, 500); -} - -TEST(CFColorTest, ConstructFromHCT_DarkTone) { - cf::ui::base::CFColor color(0.0f, 50.0f, 10.0f); // Dark color - - EXPECT_LT(color.tone(), 15.0f); - // Dark colors have lower RGB values - int total = - color.native_color().red() + color.native_color().green() + color.native_color().blue(); - EXPECT_LT(total, 150); -} - -// ============================================================================= -// Test Suite 5: Relative Luminance -// ============================================================================= - -TEST(CFColorTest, RelativeLuminance_Black) { - cf::ui::base::CFColor color(Qt::black); - EXPECT_FLOAT_EQ(color.relativeLuminance(), 0.0f); -} - -TEST(CFColorTest, RelativeLuminance_White) { - cf::ui::base::CFColor color(Qt::white); - EXPECT_FLOAT_EQ(color.relativeLuminance(), 1.0f); -} - -TEST(CFColorTest, RelativeLuminance_Gray) { - cf::ui::base::CFColor color(128, 128, 128); - float lum = color.relativeLuminance(); - EXPECT_GT(lum, 0.1f); - EXPECT_LT(lum, 0.4f); -} - -TEST(CFColorTest, RelativeLuminance_Red) { - cf::ui::base::CFColor color(Qt::red); - float lum = color.relativeLuminance(); - // Red has lower perceived brightness - EXPECT_GT(lum, 0.1f); - EXPECT_LT(lum, 0.4f); -} - -TEST(CFColorTest, RelativeLuminance_Green) { - cf::ui::base::CFColor color(Qt::green); - float lum = color.relativeLuminance(); - // Green has high perceived brightness - EXPECT_GT(lum, 0.5f); - EXPECT_LT(lum, 0.9f); -} - -TEST(CFColorTest, RelativeLuminance_Blue) { - cf::ui::base::CFColor color(Qt::blue); - float lum = color.relativeLuminance(); - // Blue has low perceived brightness - EXPECT_GT(lum, 0.05f); - EXPECT_LT(lum, 0.2f); -} - -TEST(CFColorTest, RelativeLuminance_WCAGFormula) { - // Verify WCAG 2.1 formula: 0.2126*R + 0.7152*G + 0.0722*B - // For pure white (1,1,1): 0.2126 + 0.7152 + 0.0722 = 1.0 - cf::ui::base::CFColor color(Qt::white); - EXPECT_NEAR(color.relativeLuminance(), 1.0f, 0.001f); -} - -// ============================================================================= -// Test Suite 6: HCT-RGB Round-trip Conversion -// ============================================================================= - -TEST(CFColorTest, RoundTrip_HCTtoRGBtoHCT) { - // Create color from HCT - cf::ui::base::CFColor original(180.0f, 60.0f, 50.0f); - float origHue = original.hue(); - float origChroma = original.chroma(); - float origTone = original.tone(); - - // Create new color from RGB - cf::ui::base::CFColor fromRgb(original.native_color()); - - // HCT values should be approximately preserved - EXPECT_NEAR(fromRgb.hue(), origHue, 15.0f); - EXPECT_NEAR(fromRgb.chroma(), origChroma, 20.0f); - EXPECT_NEAR(fromRgb.tone(), origTone, 15.0f); -} - -TEST(CFColorTest, RoundTrip_RGBtoHCTtoRGB) { - // Create color from RGB - QColor originalRgb(100, 150, 200); - cf::ui::base::CFColor original(originalRgb); - - // Create new color from HCT - cf::ui::base::CFColor fromHct(original.hue(), original.chroma(), original.tone()); - - // RGB values should be approximately preserved - EXPECT_NEAR(fromHct.native_color().red(), originalRgb.red(), 30); - EXPECT_NEAR(fromHct.native_color().green(), originalRgb.green(), 30); - EXPECT_NEAR(fromHct.native_color().blue(), originalRgb.blue(), 30); -} - -// ============================================================================= -// Test Suite 7: Edge Cases and Special Colors -// ============================================================================= - -TEST(CFColorTest, Hue_BoundaryValues) { - cf::ui::base::CFColor color1(0.0f, 50.0f, 50.0f); - EXPECT_FLOAT_EQ(color1.hue(), 0.0f); - - cf::ui::base::CFColor color2(360.0f, 50.0f, 50.0f); - EXPECT_FLOAT_EQ(color2.hue(), 360.0f); -} - -TEST(CFColorTest, Chroma_BoundaryValues) { - cf::ui::base::CFColor color1(180.0f, 0.0f, 50.0f); - EXPECT_FLOAT_EQ(color1.chroma(), 0.0f); - - cf::ui::base::CFColor color2(180.0f, 150.0f, 50.0f); - EXPECT_FLOAT_EQ(color2.chroma(), 150.0f); -} - -TEST(CFColorTest, Tone_BoundaryValues) { - cf::ui::base::CFColor color1(180.0f, 50.0f, 0.0f); - EXPECT_FLOAT_EQ(color1.tone(), 0.0f); - - cf::ui::base::CFColor color2(180.0f, 50.0f, 100.0f); - EXPECT_FLOAT_EQ(color2.tone(), 100.0f); -} - -TEST(CFColorTest, NativeColor_ReturnsQColor) { - QColor qColor(123, 45, 67); - cf::ui::base::CFColor color(qColor); - - EXPECT_EQ(color.native_color(), qColor); -} - -TEST(CFColorTest, CopyConstructor) { - cf::ui::base::CFColor original("#FF00FF"); - cf::ui::base::CFColor copy(original); - - EXPECT_EQ(copy.native_color(), original.native_color()); - EXPECT_FLOAT_EQ(copy.hue(), original.hue()); - EXPECT_FLOAT_EQ(copy.chroma(), original.chroma()); - EXPECT_FLOAT_EQ(copy.tone(), original.tone()); -} - -// ============================================================================= -// Test Suite 8: Color Consistency -// ============================================================================= - -TEST(CFColorTest, MultipleConstructorsSameColor) { - // Create same color using different methods - cf::ui::base::CFColor fromHex("#FF0000"); - cf::ui::base::CFColor fromQColor(QColor(255, 0, 0)); - - // RGB values should match - EXPECT_EQ(fromHex.native_color().red(), fromQColor.native_color().red()); - EXPECT_EQ(fromHex.native_color().green(), fromQColor.native_color().green()); - EXPECT_EQ(fromHex.native_color().blue(), fromQColor.native_color().blue()); -} - -TEST(CFColorTest, HCTRangeValidation) { - // Test that HCT values are always in valid range - cf::ui::base::CFColor color(500.0f, -50.0f, 150.0f); - - EXPECT_GE(color.hue(), 0.0f); - EXPECT_LE(color.hue(), 360.0f); - EXPECT_GE(color.chroma(), 0.0f); - EXPECT_LE(color.chroma(), 150.0f); - EXPECT_GE(color.tone(), 0.0f); - EXPECT_LE(color.tone(), 100.0f); -} - -// No main() needed - using GTest::gtest_main diff --git a/test/ui/base/device_pixel_test.cpp b/test/ui/base/device_pixel_test.cpp deleted file mode 100644 index 3f48f2920..000000000 --- a/test/ui/base/device_pixel_test.cpp +++ /dev/null @@ -1,404 +0,0 @@ -/** - * @file device_pixel_test.cpp - * @brief Comprehensive unit tests for cf::ui::base::device::CanvasUnitHelper - * - * Test Coverage: - * 1. Construction with different device pixel ratios - * 2. dpToPx() - Device-independent pixels to physical pixels - * 3. spToPx() - Scalable pixels to physical pixels - * 4. pxToDp() - Physical pixels to device-independent pixels - * 5. dpi() - DPI calculation - * 6. breakPoint() - Responsive breakpoint categorization - */ - -#include "ui/base/device_pixel.h" -#include - -// ============================================================================= -// Test Suite 1: Construction -// ============================================================================= - -TEST(CanvasUnitHelperTest, Construction_StandardDPR) { - cf::ui::base::device::CanvasUnitHelper helper(1.0); - EXPECT_FLOAT_EQ(helper.dpi(), 96.0); -} - -TEST(CanvasUnitHelperTest, Construction_RetinaDPR) { - cf::ui::base::device::CanvasUnitHelper helper(2.0); - EXPECT_FLOAT_EQ(helper.dpi(), 192.0); -} - -TEST(CanvasUnitHelperTest, Construction_HighDPR) { - cf::ui::base::device::CanvasUnitHelper helper(3.0); - EXPECT_FLOAT_EQ(helper.dpi(), 288.0); -} - -TEST(CanvasUnitHelperTest, Construction_FractionalDPR) { - cf::ui::base::device::CanvasUnitHelper helper(1.5); - EXPECT_FLOAT_EQ(helper.dpi(), 144.0); -} - -// ============================================================================= -// Test Suite 2: dpToPx() -// ============================================================================= - -TEST(CanvasUnitHelperTest, DpToPx_DPR1) { - cf::ui::base::device::CanvasUnitHelper helper(1.0); - - EXPECT_DOUBLE_EQ(helper.dpToPx(16.0), 16.0); - EXPECT_DOUBLE_EQ(helper.dpToPx(0.0), 0.0); - EXPECT_DOUBLE_EQ(helper.dpToPx(100.0), 100.0); -} - -TEST(CanvasUnitHelperTest, DpToPx_DPR2) { - cf::ui::base::device::CanvasUnitHelper helper(2.0); - - EXPECT_DOUBLE_EQ(helper.dpToPx(16.0), 32.0); - EXPECT_DOUBLE_EQ(helper.dpToPx(0.0), 0.0); - EXPECT_DOUBLE_EQ(helper.dpToPx(100.0), 200.0); -} - -TEST(CanvasUnitHelperTest, DpToPx_DPR3) { - cf::ui::base::device::CanvasUnitHelper helper(3.0); - - EXPECT_DOUBLE_EQ(helper.dpToPx(16.0), 48.0); - EXPECT_DOUBLE_EQ(helper.dpToPx(8.0), 24.0); -} - -TEST(CanvasUnitHelperTest, DpToPx_FractionalDPR) { - cf::ui::base::device::CanvasUnitHelper helper(1.5); - - EXPECT_DOUBLE_EQ(helper.dpToPx(16.0), 24.0); - EXPECT_DOUBLE_EQ(helper.dpToPx(10.0), 15.0); -} - -TEST(CanvasUnitHelperTest, DpToPx_FractionalDp) { - cf::ui::base::device::CanvasUnitHelper helper(2.0); - - EXPECT_DOUBLE_EQ(helper.dpToPx(16.5), 33.0); - EXPECT_DOUBLE_EQ(helper.dpToPx(0.5), 1.0); -} - -TEST(CanvasUnitHelperTest, DpToPx_NegativeDp) { - cf::ui::base::device::CanvasUnitHelper helper(2.0); - - EXPECT_DOUBLE_EQ(helper.dpToPx(-16.0), -32.0); -} - -// ============================================================================= -// Test Suite 3: spToPx() -// ============================================================================= - -TEST(CanvasUnitHelperTest, SpToPx_DPR1) { - cf::ui::base::device::CanvasUnitHelper helper(1.0); - - EXPECT_DOUBLE_EQ(helper.spToPx(14.0), 14.0); - EXPECT_DOUBLE_EQ(helper.spToPx(16.0), 16.0); - EXPECT_DOUBLE_EQ(helper.spToPx(20.0), 20.0); -} - -TEST(CanvasUnitHelperTest, SpToPx_DPR2) { - cf::ui::base::device::CanvasUnitHelper helper(2.0); - - EXPECT_DOUBLE_EQ(helper.spToPx(14.0), 28.0); - EXPECT_DOUBLE_EQ(helper.spToPx(16.0), 32.0); - EXPECT_DOUBLE_EQ(helper.spToPx(20.0), 40.0); -} - -TEST(CanvasUnitHelperTest, SpToPx_SameAsDp) { - cf::ui::base::device::CanvasUnitHelper helper(2.0); - - // sp and dp should behave the same - double dpResult = helper.dpToPx(16.0); - double spResult = helper.spToPx(16.0); - - EXPECT_DOUBLE_EQ(dpResult, spResult); -} - -TEST(CanvasUnitHelperTest, SpToPx_FontSizes) { - cf::ui::base::device::CanvasUnitHelper helper(2.0); - - // Common font sizes - EXPECT_DOUBLE_EQ(helper.spToPx(12.0), 24.0); - EXPECT_DOUBLE_EQ(helper.spToPx(14.0), 28.0); - EXPECT_DOUBLE_EQ(helper.spToPx(16.0), 32.0); - EXPECT_DOUBLE_EQ(helper.spToPx(18.0), 36.0); - EXPECT_DOUBLE_EQ(helper.spToPx(24.0), 48.0); -} - -// ============================================================================= -// Test Suite 4: pxToDp() -// ============================================================================= - -TEST(CanvasUnitHelperTest, PxToDp_DPR1) { - cf::ui::base::device::CanvasUnitHelper helper(1.0); - - EXPECT_DOUBLE_EQ(helper.pxToDp(16.0), 16.0); - EXPECT_DOUBLE_EQ(helper.pxToDp(32.0), 32.0); - EXPECT_DOUBLE_EQ(helper.pxToDp(100.0), 100.0); -} - -TEST(CanvasUnitHelperTest, PxToDp_DPR2) { - cf::ui::base::device::CanvasUnitHelper helper(2.0); - - EXPECT_DOUBLE_EQ(helper.pxToDp(32.0), 16.0); - EXPECT_DOUBLE_EQ(helper.pxToDp(48.0), 24.0); - EXPECT_DOUBLE_EQ(helper.pxToDp(200.0), 100.0); -} - -TEST(CanvasUnitHelperTest, PxToDp_DPR3) { - cf::ui::base::device::CanvasUnitHelper helper(3.0); - - EXPECT_DOUBLE_EQ(helper.pxToDp(48.0), 16.0); - EXPECT_DOUBLE_EQ(helper.pxToDp(150.0), 50.0); -} - -TEST(CanvasUnitHelperTest, PxToDp_RoundTrip) { - cf::ui::base::device::CanvasUnitHelper helper(2.0); - - double originalDp = 16.0; - double px = helper.dpToPx(originalDp); - double convertedBack = helper.pxToDp(px); - - EXPECT_DOUBLE_EQ(convertedBack, originalDp); -} - -TEST(CanvasUnitHelperTest, PxToDp_Zero) { - cf::ui::base::device::CanvasUnitHelper helper(2.0); - - EXPECT_DOUBLE_EQ(helper.pxToDp(0.0), 0.0); -} - -TEST(CanvasUnitHelperTest, PxToDp_Negative) { - cf::ui::base::device::CanvasUnitHelper helper(2.0); - - EXPECT_DOUBLE_EQ(helper.pxToDp(-32.0), -16.0); -} - -// ============================================================================= -// Test Suite 5: dpi() -// ============================================================================= - -TEST(CanvasUnitHelperTest, Dpi_Standard96) { - cf::ui::base::device::CanvasUnitHelper helper(1.0); - EXPECT_DOUBLE_EQ(helper.dpi(), 96.0); -} - -TEST(CanvasUnitHelperTest, Dpi_Retina192) { - cf::ui::base::device::CanvasUnitHelper helper(2.0); - EXPECT_DOUBLE_EQ(helper.dpi(), 192.0); -} - -TEST(CanvasUnitHelperTest, Dpi_SuperRetina288) { - cf::ui::base::device::CanvasUnitHelper helper(3.0); - EXPECT_DOUBLE_EQ(helper.dpi(), 288.0); -} - -TEST(CanvasUnitHelperTest, Dpi_Formula) { - // DPI = 96 * devicePixelRatio - for (qreal dpr = 0.5; dpr <= 4.0; dpr += 0.5) { - cf::ui::base::device::CanvasUnitHelper helper(dpr); - EXPECT_DOUBLE_EQ(helper.dpi(), 96.0 * dpr); - } -} - -// ============================================================================= -// Test Suite 6: breakPoint() -// ============================================================================= - -TEST(CanvasUnitHelperTest, BreakPoint_Compact) { - cf::ui::base::device::CanvasUnitHelper helper(1.0); - - EXPECT_EQ(helper.breakPoint(300.0), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Compact); - EXPECT_EQ(helper.breakPoint(599.0), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Compact); - EXPECT_EQ(helper.breakPoint(599.99), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Compact); -} - -TEST(CanvasUnitHelperTest, BreakPoint_Medium) { - cf::ui::base::device::CanvasUnitHelper helper(1.0); - - EXPECT_EQ(helper.breakPoint(600.0), cf::ui::base::device::CanvasUnitHelper::BreakPoint::Medium); - EXPECT_EQ(helper.breakPoint(700.0), cf::ui::base::device::CanvasUnitHelper::BreakPoint::Medium); - EXPECT_EQ(helper.breakPoint(839.0), cf::ui::base::device::CanvasUnitHelper::BreakPoint::Medium); - EXPECT_EQ(helper.breakPoint(839.99), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Medium); -} - -TEST(CanvasUnitHelperTest, BreakPoint_Expanded) { - cf::ui::base::device::CanvasUnitHelper helper(1.0); - - EXPECT_EQ(helper.breakPoint(840.0), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Expanded); - EXPECT_EQ(helper.breakPoint(1000.0), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Expanded); - EXPECT_EQ(helper.breakPoint(1920.0), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Expanded); -} - -TEST(CanvasUnitHelperTest, BreakPoint_BoundaryValues) { - cf::ui::base::device::CanvasUnitHelper helper(1.0); - - // Exactly at boundaries - EXPECT_EQ(helper.breakPoint(600.0), cf::ui::base::device::CanvasUnitHelper::BreakPoint::Medium); - EXPECT_EQ(helper.breakPoint(840.0), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Expanded); - - // Just below boundaries - EXPECT_EQ(helper.breakPoint(599.99), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Compact); - EXPECT_EQ(helper.breakPoint(839.99), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Medium); - - // Just above boundaries - EXPECT_EQ(helper.breakPoint(600.01), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Medium); - EXPECT_EQ(helper.breakPoint(840.01), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Expanded); -} - -TEST(CanvasUnitHelperTest, BreakPoint_NegativeWidth) { - cf::ui::base::device::CanvasUnitHelper helper(1.0); - - EXPECT_EQ(helper.breakPoint(-100.0), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Compact); -} - -TEST(CanvasUnitHelperTest, BreakPoint_Zero) { - cf::ui::base::device::CanvasUnitHelper helper(1.0); - - EXPECT_EQ(helper.breakPoint(0.0), cf::ui::base::device::CanvasUnitHelper::BreakPoint::Compact); -} - -// ============================================================================= -// Test Suite 7: Material Design Breakpoint Specifications -// ============================================================================= - -TEST(MaterialDesignBreakpoints, Compact_Range) { - // Compact: < 600dp - cf::ui::base::device::CanvasUnitHelper helper(1.0); - - // Typical phone widths - EXPECT_EQ(helper.breakPoint(360.0), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Compact); - EXPECT_EQ(helper.breakPoint(375.0), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Compact); - EXPECT_EQ(helper.breakPoint(414.0), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Compact); -} - -TEST(MaterialDesignBreakpoints, Medium_Range) { - // Medium: 600dp - 839dp (foldables, tablets) - cf::ui::base::device::CanvasUnitHelper helper(1.0); - - EXPECT_EQ(helper.breakPoint(600.0), cf::ui::base::device::CanvasUnitHelper::BreakPoint::Medium); - EXPECT_EQ(helper.breakPoint(720.0), cf::ui::base::device::CanvasUnitHelper::BreakPoint::Medium); - EXPECT_EQ(helper.breakPoint(839.0), cf::ui::base::device::CanvasUnitHelper::BreakPoint::Medium); -} - -TEST(MaterialDesignBreakpoints, Expanded_Range) { - // Expanded: >= 840dp (desktops) - cf::ui::base::device::CanvasUnitHelper helper(1.0); - - EXPECT_EQ(helper.breakPoint(840.0), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Expanded); - EXPECT_EQ(helper.breakPoint(1024.0), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Expanded); - EXPECT_EQ(helper.breakPoint(1280.0), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Expanded); - EXPECT_EQ(helper.breakPoint(1920.0), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Expanded); -} - -// ============================================================================= -// Test Suite 8: Integration Tests -// ============================================================================= - -TEST(CanvasUnitHelperIntegration, ResponsiveLayout_Calculation) { - // Simulate responsive layout calculation - cf::ui::base::device::CanvasUnitHelper helper(2.0); - - // Physical width of 720px at 2x DPR = 360dp (Compact) - double widthDp = helper.pxToDp(720.0); - EXPECT_EQ(helper.breakPoint(widthDp), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Compact); - - // Physical width of 1400px at 2x DPR = 700dp (Medium) - widthDp = helper.pxToDp(1400.0); - EXPECT_EQ(helper.breakPoint(widthDp), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Medium); - - // Physical width of 2000px at 2x DPR = 1000dp (Expanded) - widthDp = helper.pxToDp(2000.0); - EXPECT_EQ(helper.breakPoint(widthDp), - cf::ui::base::device::CanvasUnitHelper::BreakPoint::Expanded); -} - -TEST(CanvasUnitHelperIntegration, FontScaling) { - // Font should scale properly across different DPRs - qreal baseSizeSp = 16.0; - - cf::ui::base::device::CanvasUnitHelper helper1x(1.0); - cf::ui::base::device::CanvasUnitHelper helper2x(2.0); - cf::ui::base::device::CanvasUnitHelper helper3x(3.0); - - EXPECT_DOUBLE_EQ(helper1x.spToPx(baseSizeSp), 16.0); - EXPECT_DOUBLE_EQ(helper2x.spToPx(baseSizeSp), 32.0); - EXPECT_DOUBLE_EQ(helper3x.spToPx(baseSizeSp), 48.0); -} - -TEST(CanvasUnitHelperIntegration, LayoutMargins) { - // Test typical layout margin (16dp) at different DPRs - qreal marginDp = 16.0; - - cf::ui::base::device::CanvasUnitHelper helper1x(1.0); - cf::ui::base::device::CanvasUnitHelper helper2x(2.0); - cf::ui::base::device::CanvasUnitHelper helper3x(3.0); - - EXPECT_DOUBLE_EQ(helper1x.dpToPx(marginDp), 16.0); - EXPECT_DOUBLE_EQ(helper2x.dpToPx(marginDp), 32.0); - EXPECT_DOUBLE_EQ(helper3x.dpToPx(marginDp), 48.0); -} - -// ============================================================================= -// Test Suite 9: Edge Cases -// ============================================================================= - -TEST(CanvasUnitHelperEdgeCases, VerySmallDPR) { - cf::ui::base::device::CanvasUnitHelper helper(0.5); - - EXPECT_DOUBLE_EQ(helper.dpToPx(16.0), 8.0); - EXPECT_DOUBLE_EQ(helper.pxToDp(8.0), 16.0); - EXPECT_DOUBLE_EQ(helper.dpi(), 48.0); -} - -TEST(CanvasUnitHelperEdgeCases, LargeDPR) { - cf::ui::base::device::CanvasUnitHelper helper(4.0); - - EXPECT_DOUBLE_EQ(helper.dpToPx(16.0), 64.0); - EXPECT_DOUBLE_EQ(helper.pxToDp(64.0), 16.0); - EXPECT_DOUBLE_EQ(helper.dpi(), 384.0); -} - -TEST(CanvasUnitHelperEdgeCases, VeryLargeValue) { - cf::ui::base::device::CanvasUnitHelper helper(2.0); - - EXPECT_DOUBLE_EQ(helper.dpToPx(10000.0), 20000.0); -} - -TEST(CanvasUnitHelperEdgeCases, VerySmallValue) { - cf::ui::base::device::CanvasUnitHelper helper(2.0); - - EXPECT_DOUBLE_EQ(helper.dpToPx(0.1), 0.2); -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/base/easing_test.cpp b/test/ui/base/easing_test.cpp deleted file mode 100644 index ceb7287a2..000000000 --- a/test/ui/base/easing_test.cpp +++ /dev/null @@ -1,422 +0,0 @@ -/** - * @file easing_test.cpp - * @brief Comprehensive unit tests for cf::ui::base::Easing namespace - * - * Test Coverage: - * 1. fromEasingType() - Converting Type enum to QEasingCurve - * 2. custom() - Creating custom cubic bezier easing curves - * 3. Spring presets (springGentle, springBouncy, springStiff) - * 4. QEasingCurve validation - */ - -#include "ui/base/easing.h" -#include - -// ============================================================================= -// Test Suite 1: fromEasingType() - Type enum to QEasingCurve conversion -// ============================================================================= - -TEST(EasingTest, FromEasingType_Emphasized) { - QEasingCurve curve = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::Emphasized); - - EXPECT_EQ(curve.type(), QEasingCurve::BezierSpline); - EXPECT_FLOAT_EQ(curve.valueForProgress(0.0), 0.0); - EXPECT_FLOAT_EQ(curve.valueForProgress(1.0), 1.0); -} - -TEST(EasingTest, FromEasingType_EmphasizedDecelerate) { - QEasingCurve curve = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::EmphasizedDecelerate); - - EXPECT_EQ(curve.type(), QEasingCurve::BezierSpline); - EXPECT_FLOAT_EQ(curve.valueForProgress(0.0), 0.0); - EXPECT_FLOAT_EQ(curve.valueForProgress(1.0), 1.0); -} - -TEST(EasingTest, FromEasingType_EmphasizedAccelerate) { - QEasingCurve curve = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::EmphasizedAccelerate); - - EXPECT_EQ(curve.type(), QEasingCurve::BezierSpline); - EXPECT_FLOAT_EQ(curve.valueForProgress(0.0), 0.0); - EXPECT_FLOAT_EQ(curve.valueForProgress(1.0), 1.0); -} - -TEST(EasingTest, FromEasingType_Standard) { - QEasingCurve curve = cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::Standard); - - EXPECT_EQ(curve.type(), QEasingCurve::BezierSpline); - EXPECT_FLOAT_EQ(curve.valueForProgress(0.0), 0.0); - EXPECT_FLOAT_EQ(curve.valueForProgress(1.0), 1.0); -} - -TEST(EasingTest, FromEasingType_StandardDecelerate) { - QEasingCurve curve = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::StandardDecelerate); - - EXPECT_EQ(curve.type(), QEasingCurve::BezierSpline); - EXPECT_FLOAT_EQ(curve.valueForProgress(0.0), 0.0); - EXPECT_FLOAT_EQ(curve.valueForProgress(1.0), 1.0); -} - -TEST(EasingTest, FromEasingType_StandardAccelerate) { - QEasingCurve curve = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::StandardAccelerate); - - EXPECT_EQ(curve.type(), QEasingCurve::BezierSpline); - EXPECT_FLOAT_EQ(curve.valueForProgress(0.0), 0.0); - EXPECT_FLOAT_EQ(curve.valueForProgress(1.0), 1.0); -} - -TEST(EasingTest, FromEasingType_Linear) { - QEasingCurve curve = cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::Linear); - - EXPECT_EQ(curve.type(), QEasingCurve::BezierSpline); - EXPECT_FLOAT_EQ(curve.valueForProgress(0.0), 0.0); - EXPECT_FLOAT_EQ(curve.valueForProgress(1.0), 1.0); - - // Linear should have value approximately equal to progress at all points - EXPECT_NEAR(curve.valueForProgress(0.5), 0.5, 0.01f); - EXPECT_NEAR(curve.valueForProgress(0.25), 0.25, 0.01f); - EXPECT_NEAR(curve.valueForProgress(0.75), 0.75, 0.01f); -} - -// ============================================================================= -// Test Suite 2: Material Design easing curve characteristics -// ============================================================================= - -TEST(MaterialDesignEasing, Emphasized_IsEaseInOut) { - // Emphasized: cubic-bezier(0.2, 0, 0, 1.0) - QEasingCurve curve = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::Emphasized); - - // At 0.5 progress, should be greater than 0.5 (quick rise after slow start) - float value = curve.valueForProgress(0.5); - EXPECT_GT(value, 0.5f); - EXPECT_LT(value, 1.0f); -} - -TEST(MaterialDesignEasing, EmphasizedDecelerate_IsEaseOut) { - // EmphasizedDecelerate: cubic-bezier(0.05, 0.7, 0.1, 1.0) - // Fast start, slow end - QEasingCurve curve = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::EmphasizedDecelerate); - - // At 0.5 progress, should be greater than 0.5 (fast start) - float value = curve.valueForProgress(0.5); - EXPECT_GT(value, 0.5f); -} - -TEST(MaterialDesignEasing, EmphasizedAccelerate_IsEaseIn) { - // EmphasizedAccelerate: cubic-bezier(0.3, 0, 0.8, 0.15) - // Slow start, fast end - QEasingCurve curve = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::EmphasizedAccelerate); - - // At 0.5 progress, should be less than 0.5 (slow start) - float value = curve.valueForProgress(0.5); - EXPECT_LT(value, 0.5f); -} - -TEST(MaterialDesignEasing, Standard_IsSimilarToEmphasized) { - // Standard: cubic-bezier(0.2, 0, 0, 1.0) - same as Emphasized - QEasingCurve emphasized = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::Emphasized); - QEasingCurve standard = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::Standard); - - // Should be the same curve - for (int i = 0; i <= 10; ++i) { - float t = i / 10.0f; - EXPECT_FLOAT_EQ(emphasized.valueForProgress(t), standard.valueForProgress(t)); - } -} - -TEST(MaterialDesignEasing, StandardDecelerate_IsPureEaseOut) { - // StandardDecelerate: cubic-bezier(0, 0, 0, 1.0) - // This is actually a pure ease-out curve - QEasingCurve curve = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::StandardDecelerate); - - EXPECT_FLOAT_EQ(curve.valueForProgress(0.0), 0.0); - EXPECT_FLOAT_EQ(curve.valueForProgress(1.0), 1.0); -} - -TEST(MaterialDesignEasing, StandardAccelerate_IsPureEaseIn) { - // StandardAccelerate: cubic-bezier(0.3, 0, 1, 1) - // Pure ease-in curve - QEasingCurve curve = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::StandardAccelerate); - - EXPECT_FLOAT_EQ(curve.valueForProgress(0.0), 0.0); - EXPECT_FLOAT_EQ(curve.valueForProgress(1.0), 1.0); - - // At 0.5, should be significantly less than 0.5 (ease-in starts slow) - float value = curve.valueForProgress(0.5); - EXPECT_LT(value, 0.4f); -} - -// ============================================================================= -// Test Suite 3: custom() - Custom cubic bezier curves -// ============================================================================= - -TEST(CustomEasing, BasicBezier) { - QEasingCurve curve = cf::ui::base::Easing::custom(0.25f, 0.1f, 0.25f, 1.0f); - - EXPECT_EQ(curve.type(), QEasingCurve::BezierSpline); - EXPECT_FLOAT_EQ(curve.valueForProgress(0.0), 0.0); - EXPECT_FLOAT_EQ(curve.valueForProgress(1.0), 1.0); -} - -TEST(CustomEasing, EaseInQuad) { - // Parabolic ease-in: (0.5, 0, 1, 1) - QEasingCurve curve = cf::ui::base::Easing::custom(0.5f, 0.0f, 1.0f, 1.0f); - - EXPECT_FLOAT_EQ(curve.valueForProgress(0.0), 0.0); - EXPECT_FLOAT_EQ(curve.valueForProgress(1.0), 1.0); - - // At t=0.5, value should be around 0.25 (quadratic) - float value = curve.valueForProgress(0.5); - EXPECT_LT(value, 0.5f); -} - -TEST(CustomEasing, EaseOutQuad) { - // Parabolic ease-out: (0, 0, 0.5, 1) - QEasingCurve curve = cf::ui::base::Easing::custom(0.0f, 0.0f, 0.5f, 1.0f); - - EXPECT_FLOAT_EQ(curve.valueForProgress(0.0), 0.0); - EXPECT_FLOAT_EQ(curve.valueForProgress(1.0), 1.0); - - // At t=0.5, value should be around 0.75 (quadratic) - float value = curve.valueForProgress(0.5); - EXPECT_GT(value, 0.5f); -} - -TEST(CustomEasing, ExtremeControlPoints) { - // Test with extreme control point values - QEasingCurve curve = cf::ui::base::Easing::custom(0.0f, -0.5f, 1.0f, 1.5f); - - EXPECT_FLOAT_EQ(curve.valueForProgress(0.0), 0.0); - EXPECT_FLOAT_EQ(curve.valueForProgress(1.0), 1.0); -} - -// ============================================================================= -// Test Suite 4: Spring presets -// ============================================================================= - -TEST(SpringPresets, SpringGentle) { - cf::ui::base::Easing::SpringPreset preset = cf::ui::base::Easing::springGentle(); - - EXPECT_FLOAT_EQ(preset.stiffness, 120.0f); - EXPECT_FLOAT_EQ(preset.damping, 20.0f); -} - -TEST(SpringPresets, SpringBouncy) { - cf::ui::base::Easing::SpringPreset preset = cf::ui::base::Easing::springBouncy(); - - EXPECT_FLOAT_EQ(preset.stiffness, 200.0f); - EXPECT_FLOAT_EQ(preset.damping, 10.0f); -} - -TEST(SpringPresets, SpringStiff) { - cf::ui::base::Easing::SpringPreset preset = cf::ui::base::Easing::springStiff(); - - EXPECT_FLOAT_EQ(preset.stiffness, 400.0f); - EXPECT_FLOAT_EQ(preset.damping, 30.0f); -} - -TEST(SpringPresets, GentleHasLowestStiffness) { - auto gentle = cf::ui::base::Easing::springGentle(); - auto bouncy = cf::ui::base::Easing::springBouncy(); - auto stiff = cf::ui::base::Easing::springStiff(); - - EXPECT_LT(gentle.stiffness, bouncy.stiffness); - EXPECT_LT(bouncy.stiffness, stiff.stiffness); -} - -TEST(SpringPresets, BouncyHasLowestDamping) { - auto gentle = cf::ui::base::Easing::springGentle(); - auto bouncy = cf::ui::base::Easing::springBouncy(); - auto stiff = cf::ui::base::Easing::springStiff(); - - EXPECT_LT(bouncy.damping, gentle.damping); - EXPECT_LT(bouncy.damping, stiff.damping); -} - -// ============================================================================= -// Test Suite 5: QEasingCurve validation -// ============================================================================= - -TEST(EasingCurveValidation, AllTypes_StartAtZero) { - auto types = {cf::ui::base::Easing::Type::Emphasized, - cf::ui::base::Easing::Type::EmphasizedDecelerate, - cf::ui::base::Easing::Type::EmphasizedAccelerate, - cf::ui::base::Easing::Type::Standard, - cf::ui::base::Easing::Type::StandardDecelerate, - cf::ui::base::Easing::Type::StandardAccelerate, - cf::ui::base::Easing::Type::Linear}; - - for (auto type : types) { - QEasingCurve curve = cf::ui::base::Easing::fromEasingType(type); - EXPECT_FLOAT_EQ(curve.valueForProgress(0.0), 0.0) << "Easing curve should start at 0"; - } -} - -TEST(EasingCurveValidation, AllTypes_EndAtOne) { - auto types = {cf::ui::base::Easing::Type::Emphasized, - cf::ui::base::Easing::Type::EmphasizedDecelerate, - cf::ui::base::Easing::Type::EmphasizedAccelerate, - cf::ui::base::Easing::Type::Standard, - cf::ui::base::Easing::Type::StandardDecelerate, - cf::ui::base::Easing::Type::StandardAccelerate, - cf::ui::base::Easing::Type::Linear}; - - for (auto type : types) { - QEasingCurve curve = cf::ui::base::Easing::fromEasingType(type); - EXPECT_FLOAT_EQ(curve.valueForProgress(1.0), 1.0) << "Easing curve should end at 1"; - } -} - -TEST(EasingCurveValidation, AllTypes_Monotonic) { - // Easing curves should be monotonically increasing - auto types = {cf::ui::base::Easing::Type::Emphasized, - cf::ui::base::Easing::Type::EmphasizedDecelerate, - cf::ui::base::Easing::Type::EmphasizedAccelerate, - cf::ui::base::Easing::Type::Standard, - cf::ui::base::Easing::Type::StandardDecelerate, - cf::ui::base::Easing::Type::StandardAccelerate, - cf::ui::base::Easing::Type::Linear}; - - for (auto type : types) { - QEasingCurve curve = cf::ui::base::Easing::fromEasingType(type); - - float prev = curve.valueForProgress(0.0); - for (int i = 1; i <= 10; ++i) { - float t = i / 10.0f; - float curr = curve.valueForProgress(t); - EXPECT_GE(curr, prev) << "Easing curve should be monotonically increasing"; - prev = curr; - } - } -} - -TEST(EasingCurveValidation, LinearIsPerfectlyLinear) { - QEasingCurve curve = cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::Linear); - - for (int i = 0; i <= 10; ++i) { - float t = i / 10.0f; - // Use EXPECT_NEAR for bezier curve floating point tolerance - EXPECT_NEAR(curve.valueForProgress(t), t, 0.01f); - } -} - -// ============================================================================= -// Test Suite 6: Common animation progress values -// ============================================================================= - -TEST(EasingProgressValues, AtZeroProgress) { - auto types = {cf::ui::base::Easing::Type::Emphasized, cf::ui::base::Easing::Type::Linear, - cf::ui::base::Easing::Type::Standard}; - - for (auto type : types) { - QEasingCurve curve = cf::ui::base::Easing::fromEasingType(type); - EXPECT_FLOAT_EQ(curve.valueForProgress(0.0), 0.0); - } -} - -TEST(EasingProgressValues, AtQuarterProgress) { - // Different easing types should produce different values - QEasingCurve linear = cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::Linear); - QEasingCurve easeIn = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::EmphasizedAccelerate); - QEasingCurve easeOut = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::EmphasizedDecelerate); - - float t = 0.25f; - float linearValue = linear.valueForProgress(t); - float easeInValue = easeIn.valueForProgress(t); - float easeOutValue = easeOut.valueForProgress(t); - - EXPECT_NEAR(linearValue, 0.25f, 0.01f); - // Ease-in should be slower at the start - EXPECT_LT(easeInValue, linearValue); - // Ease-out should be faster at the start - EXPECT_GT(easeOutValue, linearValue); -} - -TEST(EasingProgressValues, AtHalfProgress) { - QEasingCurve linear = cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::Linear); - QEasingCurve emphasized = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::Emphasized); - - float t = 0.5f; - float linearValue = linear.valueForProgress(t); - float emphasizedValue = emphasized.valueForProgress(t); - - EXPECT_NEAR(linearValue, 0.5f, 0.01f); - // Emphasized curve (0.2, 0, 0, 1.0) rises quickly after initial slow start - // At t=0.5, the value is actually around 0.88 due to the curve shape - EXPECT_GT(emphasizedValue, 0.5f); - EXPECT_LT(emphasizedValue, 1.0f); -} - -TEST(EasingProgressValues, AtThreeQuarterProgress) { - QEasingCurve linear = cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::Linear); - QEasingCurve easeIn = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::EmphasizedAccelerate); - QEasingCurve easeOut = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::EmphasizedDecelerate); - - float t = 0.75f; - float linearValue = linear.valueForProgress(t); - float easeInValue = easeIn.valueForProgress(t); - float easeOutValue = easeOut.valueForProgress(t); - - EXPECT_NEAR(linearValue, 0.75f, 0.01f); - // Ease-in should still be catching up - EXPECT_LT(easeInValue, linearValue); - // Ease-out should be slowing down - EXPECT_GT(easeOutValue, linearValue); -} - -// ============================================================================= -// Test Suite 7: Comparison between easing types -// ============================================================================= - -TEST(EasingComparison, AccelerateVsDecelerate) { - QEasingCurve accelerate = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::EmphasizedAccelerate); - QEasingCurve decelerate = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::EmphasizedDecelerate); - - // At early progress, accelerate should be slower - float earlyT = 0.25f; - EXPECT_LT(accelerate.valueForProgress(earlyT), decelerate.valueForProgress(earlyT)); - - // At late progress, accelerate should have caught up and be faster - float lateT = 0.75f; - EXPECT_LT(accelerate.valueForProgress(lateT), decelerate.valueForProgress(lateT)); -} - -TEST(EasingComparison, LinearVsEmphasized) { - QEasingCurve linear = cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::Linear); - QEasingCurve emphasized = - cf::ui::base::Easing::fromEasingType(cf::ui::base::Easing::Type::Emphasized); - - // At very start (t=0.01), emphasized is still behind linear - float startT = 0.01f; - EXPECT_LT(emphasized.valueForProgress(startT), linear.valueForProgress(startT)); - - // At end, emphasized should catch up - EXPECT_FLOAT_EQ(emphasized.valueForProgress(1.0), linear.valueForProgress(1.0)); -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/base/geometry_helper_test.cpp b/test/ui/base/geometry_helper_test.cpp deleted file mode 100644 index 22cc6f8c5..000000000 --- a/test/ui/base/geometry_helper_test.cpp +++ /dev/null @@ -1,390 +0,0 @@ -/** - * @file geometry_helper_test.cpp - * @brief Comprehensive unit tests for cf::ui::base::geometry helper functions - * - * Test Coverage: - * 1. roundedRect() with ShapeScale enum values - * 2. roundedRect() with uniform radius - * 3. roundedRect() with individual corner radii - * 4. QPainterPath validation - */ - -#include "ui/base/geometry_helper.h" -#include -#include -#include - -// ============================================================================= -// Test Suite 1: roundedRect with ShapeScale -// ============================================================================= - -TEST(GeometryHelperTest, RoundedRect_ShapeNone) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = - cf::ui::base::geometry::roundedRect(rect, cf::ui::base::geometry::ShapeScale::ShapeNone); - - EXPECT_FALSE(path.isEmpty()); - // Should be a rectangle (no rounding) - EXPECT_TRUE(path.toFillPolygon().count() >= 4); -} - -TEST(GeometryHelperTest, RoundedRect_ShapeExtraSmall) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = cf::ui::base::geometry::roundedRect( - rect, cf::ui::base::geometry::ShapeScale::ShapeExtraSmall); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_ShapeSmall) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = - cf::ui::base::geometry::roundedRect(rect, cf::ui::base::geometry::ShapeScale::ShapeSmall); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_ShapeMedium) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = - cf::ui::base::geometry::roundedRect(rect, cf::ui::base::geometry::ShapeScale::ShapeMedium); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_ShapeLarge) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = - cf::ui::base::geometry::roundedRect(rect, cf::ui::base::geometry::ShapeScale::ShapeLarge); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_ShapeExtraLarge) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = cf::ui::base::geometry::roundedRect( - rect, cf::ui::base::geometry::ShapeScale::ShapeExtraLarge); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_ShapeFull) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = - cf::ui::base::geometry::roundedRect(rect, cf::ui::base::geometry::ShapeScale::ShapeFull); - - EXPECT_FALSE(path.isEmpty()); - // Full rounded should create a capsule shape -} - -TEST(GeometryHelperTest, RoundedRect_ShapeFull_Square) { - QRectF rect(0, 0, 100, 100); - QPainterPath path = - cf::ui::base::geometry::roundedRect(rect, cf::ui::base::geometry::ShapeScale::ShapeFull); - - EXPECT_FALSE(path.isEmpty()); - // A square with full rounding should become a circle -} - -// ============================================================================= -// Test Suite 2: roundedRect with uniform radius -// ============================================================================= - -TEST(GeometryHelperTest, RoundedRect_UniformRadius_Zero) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 0.0f); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_UniformRadius_Small) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 5.0f); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_UniformRadius_Medium) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 15.0f); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_UniformRadius_Large) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 50.0f); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_UniformRadius_HalfHeight) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 25.0f); - - EXPECT_FALSE(path.isEmpty()); - // Half the height creates a capsule shape -} - -TEST(GeometryHelperTest, RoundedRect_UniformRadius_Excessive) { - QRectF rect(0, 0, 100, 50); - // Radius larger than half the height - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 100.0f); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_UniformRadius_Square_Circle) { - QRectF rect(0, 0, 100, 100); - // Radius equal to half the width/height creates a circle - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 50.0f); - - EXPECT_FALSE(path.isEmpty()); -} - -// ============================================================================= -// Test Suite 3: roundedRect with individual corner radii -// ============================================================================= - -TEST(GeometryHelperTest, RoundedRect_IndividualCorners_AllZero) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 0.0f, 0.0f, 0.0f, 0.0f); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_IndividualCorners_AllSame) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 10.0f, 10.0f, 10.0f, 10.0f); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_IndividualCorners_TopLeftOnly) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 15.0f, 0.0f, 0.0f, 0.0f); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_IndividualCorners_TopRightOnly) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 0.0f, 15.0f, 0.0f, 0.0f); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_IndividualCorners_BottomLeftOnly) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 0.0f, 0.0f, 15.0f, 0.0f); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_IndividualCorners_BottomRightOnly) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 0.0f, 0.0f, 0.0f, 15.0f); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_IndividualCorners_TopRounded) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 15.0f, 15.0f, 0.0f, 0.0f); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_IndividualCorners_BottomRounded) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 0.0f, 0.0f, 15.0f, 15.0f); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_IndividualCorners_Asymmetric) { - QRectF rect(0, 0, 100, 50); - // Each corner has different radius - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 5.0f, 10.0f, 15.0f, 20.0f); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperTest, RoundedRect_IndividualCorners_LargeRadii) { - QRectF rect(0, 0, 100, 50); - // Large radii that may exceed bounds - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 50.0f, 50.0f, 50.0f, 50.0f); - - EXPECT_FALSE(path.isEmpty()); -} - -// ============================================================================= -// Test Suite 4: QPainterPath validation -// ============================================================================= - -TEST(GeometryHelperPathValidation, PathIsNotEmpty) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = - cf::ui::base::geometry::roundedRect(rect, cf::ui::base::geometry::ShapeScale::ShapeMedium); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperPathValidation, PathIsClosed) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 10.0f); - - // The path should form a closed shape - EXPECT_TRUE(path.toFillPolygon().isClosed()); -} - -TEST(GeometryHelperPathValidation, PathContainsOriginalRect) { - QRectF rect(0, 0, 100, 50); - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 10.0f); - - // The rounded rect should contain the center of the original rect - EXPECT_TRUE(path.contains(rect.center())); -} - -TEST(GeometryHelperPathValidation, PathBoundingBox) { - QRectF rect(10, 20, 100, 50); - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 10.0f); - - QRectF boundingRect = path.boundingRect(); - - // Bounding rect should be approximately the same as original - EXPECT_NEAR(boundingRect.x(), rect.x(), 1.0); - EXPECT_NEAR(boundingRect.y(), rect.y(), 1.0); - EXPECT_NEAR(boundingRect.width(), rect.width(), 1.0); - EXPECT_NEAR(boundingRect.height(), rect.height(), 1.0); -} - -// ============================================================================= -// Test Suite 5: Material Design shape scale values -// ============================================================================= - -TEST(MaterialDesignShapeScale, RadiusValues) { - // Test that each shape scale produces the expected radius - // This is a conceptual test - actual values depend on implementation - - QRectF rect(0, 0, 100, 50); - - // ShapeNone should produce sharp corners (like a rectangle) - QPainterPath nonePath = - cf::ui::base::geometry::roundedRect(rect, cf::ui::base::geometry::ShapeScale::ShapeNone); - - // ShapeFull should produce maximum rounded corners - QPainterPath fullPath = - cf::ui::base::geometry::roundedRect(rect, cf::ui::base::geometry::ShapeScale::ShapeFull); - - // Both paths should be valid - EXPECT_FALSE(nonePath.isEmpty()); - EXPECT_FALSE(fullPath.isEmpty()); - - // The full rounded path should be "rounder" - this is hard to test directly - // but we can check that both are different - // (Their bounding rects might differ slightly due to corner rendering) -} - -// ============================================================================= -// Test Suite 6: Different rectangle sizes -// ============================================================================= - -TEST(GeometryHelperRectSizes, VerySmallRect) { - QRectF rect(0, 0, 5, 5); - QPainterPath path = - cf::ui::base::geometry::roundedRect(rect, cf::ui::base::geometry::ShapeScale::ShapeSmall); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperRectSizes, VeryWideRect) { - QRectF rect(0, 0, 500, 20); - QPainterPath path = - cf::ui::base::geometry::roundedRect(rect, cf::ui::base::geometry::ShapeScale::ShapeSmall); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperRectSizes, VeryTallRect) { - QRectF rect(0, 0, 20, 500); - QPainterPath path = - cf::ui::base::geometry::roundedRect(rect, cf::ui::base::geometry::ShapeScale::ShapeSmall); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperRectSizes, NegativeCoordinates) { - QRectF rect(-50, -50, 100, 100); - QPainterPath path = - cf::ui::base::geometry::roundedRect(rect, cf::ui::base::geometry::ShapeScale::ShapeMedium); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperRectSizes, OffsetRect) { - QRectF rect(100, 200, 50, 50); - QPainterPath path = - cf::ui::base::geometry::roundedRect(rect, cf::ui::base::geometry::ShapeScale::ShapeMedium); - - EXPECT_FALSE(path.isEmpty()); - EXPECT_TRUE(path.contains(rect.center())); -} - -// ============================================================================= -// Test Suite 7: Consistency between overloads -// ============================================================================= - -TEST(GeometryHelperConsistency, ShapeScaleVsUniformRadius) { - QRectF rect(0, 0, 100, 50); - - // ShapeSmall is defined as 8dp radius - QPainterPath shapeScalePath = - cf::ui::base::geometry::roundedRect(rect, cf::ui::base::geometry::ShapeScale::ShapeSmall); - QPainterPath uniformPath = cf::ui::base::geometry::roundedRect(rect, 8.0f); - - // Both should produce similar (but not necessarily identical) results - // The exact behavior depends on Qt's rounded rect implementation - EXPECT_FALSE(shapeScalePath.isEmpty()); - EXPECT_FALSE(uniformPath.isEmpty()); -} - -TEST(GeometryHelperConsistency, SameInputSameOutput) { - QRectF rect(0, 0, 100, 50); - - // Call the same function twice with same input - QPainterPath path1 = cf::ui::base::geometry::roundedRect(rect, 10.0f); - QPainterPath path2 = cf::ui::base::geometry::roundedRect(rect, 10.0f); - - // Should produce identical results - // (This tests function purity) - EXPECT_EQ(path1.fillRule(), path2.fillRule()); -} - -// ============================================================================= -// Test Suite 8: Special cases -// ============================================================================= - -TEST(GeometryHelperSpecialCases, ZeroSizeRect) { - QRectF rect(0, 0, 0, 0); - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 10.0f); - - // Qt handles zero-size rectangles gracefully -} - -TEST(GeometryHelperSpecialCases, NegativeRadius) { - QRectF rect(0, 0, 100, 50); - // Qt may clamp or ignore negative radii - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, -10.0f); - - EXPECT_FALSE(path.isEmpty()); -} - -TEST(GeometryHelperSpecialCases, FloatPrecision) { - QRectF rect(0.5, 0.5, 99.9, 49.9); - QPainterPath path = cf::ui::base::geometry::roundedRect(rect, 9.9f); - - EXPECT_FALSE(path.isEmpty()); -} - -// No main() needed - using GTest::gtest_main diff --git a/test/ui/base/math_helper_test.cpp b/test/ui/base/math_helper_test.cpp deleted file mode 100644 index 8558f1163..000000000 --- a/test/ui/base/math_helper_test.cpp +++ /dev/null @@ -1,411 +0,0 @@ -/** - * @file math_helper_test.cpp - * @brief Comprehensive unit tests for cf::ui::math helper functions - * - * Test Coverage: - * 1. lerp - Linear interpolation - * 2. clamp - Value clamping - * 3. remap - Value range remapping - * 4. cubicBezier - Cubic Bezier curve evaluation - * 5. springStep - Spring physics simulation - * 6. lerpAngle - Angle interpolation with 0-360 boundary handling - */ - -#include "ui/base/math_helper.h" -#include -#include - -// ============================================================================= -// Test Suite 1: lerp (Linear Interpolation) -// ============================================================================= - -TEST(MathHelperTest, Lerp_T0_ReturnsA) { - EXPECT_FLOAT_EQ(cf::ui::math::lerp(10.0f, 20.0f, 0.0f), 10.0f); - EXPECT_FLOAT_EQ(cf::ui::math::lerp(-5.0f, 5.0f, 0.0f), -5.0f); -} - -TEST(MathHelperTest, Lerp_T1_ReturnsB) { - EXPECT_FLOAT_EQ(cf::ui::math::lerp(10.0f, 20.0f, 1.0f), 20.0f); - EXPECT_FLOAT_EQ(cf::ui::math::lerp(-5.0f, 5.0f, 1.0f), 5.0f); -} - -TEST(MathHelperTest, Lerp_T05_ReturnsMidpoint) { - EXPECT_FLOAT_EQ(cf::ui::math::lerp(0.0f, 10.0f, 0.5f), 5.0f); - EXPECT_FLOAT_EQ(cf::ui::math::lerp(10.0f, 20.0f, 0.5f), 15.0f); - EXPECT_FLOAT_EQ(cf::ui::math::lerp(-10.0f, 10.0f, 0.5f), 0.0f); -} - -TEST(MathHelperTest, Lerp_NegativeT) { - // t < 0 extrapolates beyond a - EXPECT_FLOAT_EQ(cf::ui::math::lerp(10.0f, 20.0f, -0.5f), 5.0f); - EXPECT_FLOAT_EQ(cf::ui::math::lerp(0.0f, 10.0f, -1.0f), -10.0f); -} - -TEST(MathHelperTest, Lerp_TGreaterThan1) { - // t > 1 extrapolates beyond b - EXPECT_FLOAT_EQ(cf::ui::math::lerp(10.0f, 20.0f, 1.5f), 25.0f); - EXPECT_FLOAT_EQ(cf::ui::math::lerp(0.0f, 10.0f, 2.0f), 20.0f); -} - -TEST(MathHelperTest, Lerp_ReverseOrder) { - // When a > b, interpolation works in reverse - EXPECT_FLOAT_EQ(cf::ui::math::lerp(20.0f, 10.0f, 0.5f), 15.0f); - EXPECT_FLOAT_EQ(cf::ui::math::lerp(20.0f, 10.0f, 0.0f), 20.0f); - EXPECT_FLOAT_EQ(cf::ui::math::lerp(20.0f, 10.0f, 1.0f), 10.0f); -} - -TEST(MathHelperTest, Lerp_EqualEndpoints) { - EXPECT_FLOAT_EQ(cf::ui::math::lerp(5.0f, 5.0f, 0.5f), 5.0f); - EXPECT_FLOAT_EQ(cf::ui::math::lerp(5.0f, 5.0f, 0.0f), 5.0f); - EXPECT_FLOAT_EQ(cf::ui::math::lerp(5.0f, 5.0f, 1.0f), 5.0f); -} - -// ============================================================================= -// Test Suite 2: clamp -// ============================================================================= - -TEST(MathHelperTest, Clamp_ValueInRange) { - EXPECT_FLOAT_EQ(cf::ui::math::clamp(5.0f, 0.0f, 10.0f), 5.0f); - EXPECT_FLOAT_EQ(cf::ui::math::clamp(0.5f, 0.0f, 1.0f), 0.5f); - EXPECT_FLOAT_EQ(cf::ui::math::clamp(-5.0f, -10.0f, 0.0f), -5.0f); -} - -TEST(MathHelperTest, _Clamp_ValueBelowMin) { - EXPECT_FLOAT_EQ(cf::ui::math::clamp(-5.0f, 0.0f, 10.0f), 0.0f); - EXPECT_FLOAT_EQ(cf::ui::math::clamp(2.0f, 5.0f, 10.0f), 5.0f); - EXPECT_FLOAT_EQ(cf::ui::math::clamp(-100.0f, -50.0f, 50.0f), -50.0f); -} - -TEST(MathHelperTest, Clamp_ValueAboveMax) { - EXPECT_FLOAT_EQ(cf::ui::math::clamp(15.0f, 0.0f, 10.0f), 10.0f); - EXPECT_FLOAT_EQ(cf::ui::math::clamp(100.0f, 0.0f, 50.0f), 50.0f); - EXPECT_FLOAT_EQ(cf::ui::math::clamp(60.0f, -50.0f, 50.0f), 50.0f); -} - -TEST(MathHelperTest, _Clamp_BoundaryValues) { - EXPECT_FLOAT_EQ(cf::ui::math::clamp(0.0f, 0.0f, 10.0f), 0.0f); - EXPECT_FLOAT_EQ(cf::ui::math::clamp(10.0f, 0.0f, 10.0f), 10.0f); - EXPECT_FLOAT_EQ(cf::ui::math::clamp(-5.0f, -5.0f, 5.0f), -5.0f); - EXPECT_FLOAT_EQ(cf::ui::math::clamp(5.0f, -5.0f, 5.0f), 5.0f); -} - -TEST(MathHelperTest, Clamp_NegativeRange) { - EXPECT_FLOAT_EQ(cf::ui::math::clamp(-15.0f, -20.0f, -10.0f), -15.0f); - EXPECT_FLOAT_EQ(cf::ui::math::clamp(-25.0f, -20.0f, -10.0f), -20.0f); - EXPECT_FLOAT_EQ(cf::ui::math::clamp(-5.0f, -20.0f, -10.0f), -10.0f); -} - -TEST(MathHelperTest, Clamp_EqualMinMax) { - EXPECT_FLOAT_EQ(cf::ui::math::clamp(5.0f, 10.0f, 10.0f), 10.0f); - EXPECT_FLOAT_EQ(cf::ui::math::clamp(10.0f, 10.0f, 10.0f), 10.0f); - EXPECT_FLOAT_EQ(cf::ui::math::clamp(15.0f, 10.0f, 10.0f), 10.0f); -} - -// ============================================================================= -// Test Suite 3: remap -// ============================================================================= - -TEST(MathHelperTest, Remap_Basic) { - EXPECT_FLOAT_EQ(cf::ui::math::remap(5.0f, 0.0f, 10.0f, 0.0f, 100.0f), 50.0f); - EXPECT_FLOAT_EQ(cf::ui::math::remap(0.0f, 0.0f, 10.0f, 0.0f, 100.0f), 0.0f); - EXPECT_FLOAT_EQ(cf::ui::math::remap(10.0f, 0.0f, 10.0f, 0.0f, 100.0f), 100.0f); -} - -TEST(MathHelperTest, Remap_ReverseInputRange) { - EXPECT_FLOAT_EQ(cf::ui::math::remap(7.5f, 10.0f, 0.0f, 0.0f, 100.0f), 25.0f); - EXPECT_FLOAT_EQ(cf::ui::math::remap(10.0f, 10.0f, 0.0f, 0.0f, 100.0f), 0.0f); - EXPECT_FLOAT_EQ(cf::ui::math::remap(0.0f, 10.0f, 0.0f, 0.0f, 100.0f), 100.0f); -} - -TEST(MathHelperTest, Remap_ReverseOutputRange) { - EXPECT_FLOAT_EQ(cf::ui::math::remap(5.0f, 0.0f, 10.0f, 100.0f, 0.0f), 50.0f); - EXPECT_FLOAT_EQ(cf::ui::math::remap(0.0f, 0.0f, 10.0f, 100.0f, 0.0f), 100.0f); - EXPECT_FLOAT_EQ(cf::ui::math::remap(10.0f, 0.0f, 10.0f, 100.0f, 0.0f), 0.0f); -} - -TEST(MathHelperTest, Remap_DifferentScaleFactors) { - EXPECT_FLOAT_EQ(cf::ui::math::remap(0.5f, 0.0f, 1.0f, 0.0f, 1000.0f), 500.0f); - EXPECT_FLOAT_EQ(cf::ui::math::remap(50.0f, 0.0f, 100.0f, -100.0f, 100.0f), 0.0f); -} - -TEST(MathHelperTest, Remap_OutOfInputRange) { - // Values outside input range are extrapolated - EXPECT_FLOAT_EQ(cf::ui::math::remap(-5.0f, 0.0f, 10.0f, 0.0f, 100.0f), -50.0f); - EXPECT_FLOAT_EQ(cf::ui::math::remap(15.0f, 0.0f, 10.0f, 0.0f, 100.0f), 150.0f); -} - -TEST(MathHelperTest, Remap_ZeroInputRange_ValueAtMin) { - // When input range is zero, value at or below min returns output max - EXPECT_FLOAT_EQ(cf::ui::math::remap(5.0f, 5.0f, 5.0f, 0.0f, 100.0f), 100.0f); - EXPECT_FLOAT_EQ(cf::ui::math::remap(3.0f, 5.0f, 5.0f, 0.0f, 100.0f), 100.0f); -} - -TEST(MathHelperTest, Remap_ZeroInputRange_ValueAboveMin) { - // When input range is zero, value above min returns output min - EXPECT_FLOAT_EQ(cf::ui::math::remap(6.0f, 5.0f, 5.0f, 0.0f, 100.0f), 0.0f); - EXPECT_FLOAT_EQ(cf::ui::math::remap(10.0f, 5.0f, 5.0f, 0.0f, 100.0f), 0.0f); -} - -TEST(MathHelperTest, Remap_NegativeRanges) { - EXPECT_FLOAT_EQ(cf::ui::math::remap(-5.0f, -10.0f, 0.0f, -100.0f, 0.0f), -50.0f); - EXPECT_FLOAT_EQ(cf::ui::math::remap(-10.0f, -10.0f, 0.0f, -100.0f, 0.0f), -100.0f); - EXPECT_FLOAT_EQ(cf::ui::math::remap(0.0f, -10.0f, 0.0f, -100.0f, 0.0f), 0.0f); -} - -// ============================================================================= -// Test Suite 4: cubicBezier -// ============================================================================= - -TEST(MathHelperTest, CubicBezier_T0_Returns0) { - EXPECT_FLOAT_EQ(cf::ui::math::cubicBezier(0.2f, 0.0f, 0.0f, 1.0f, 0.0f), 0.0f); - EXPECT_FLOAT_EQ(cf::ui::math::cubicBezier(0.5f, 0.5f, 0.5f, 0.5f, 0.0f), 0.0f); -} - -TEST(MathHelperTest, CubicBezier_T1_Returns1) { - EXPECT_FLOAT_EQ(cf::ui::math::cubicBezier(0.2f, 0.0f, 0.0f, 1.0f, 1.0f), 1.0f); - EXPECT_FLOAT_EQ(cf::ui::math::cubicBezier(0.5f, 0.5f, 0.5f, 0.5f, 1.0f), 1.0f); -} - -TEST(MathHelperTest, CubicBezier_Linear) { - // Linear bezier: (0,0) to (1,1) with control points at (0,0) and (1,1) is linear - // Actually with P1=(0,0), P2=(1,1), the curve is y=x - float tolerance = 0.001f; - for (int i = 0; i <= 10; ++i) { - float t = i / 10.0f; - EXPECT_NEAR(cf::ui::math::cubicBezier(0.0f, 0.0f, 1.0f, 1.0f, t), t, tolerance); - } -} - -TEST(MathHelperTest, CubicBezier_EaseIn) { - // Ease in: slow start, fast end - // At t=0.5, should be less than 0.5 - float result = cf::ui::math::cubicBezier(0.5f, 0.0f, 1.0f, 1.0f, 0.5f); - EXPECT_LT(result, 0.5f); -} - -TEST(MathHelperTest, CubicBezier_EaseOut) { - // Ease out: fast start, slow end - // At t=0.5, should be greater than 0.5 - float result = cf::ui::math::cubicBezier(0.0f, 0.0f, 0.5f, 1.0f, 0.5f); - EXPECT_GT(result, 0.5f); -} - -TEST(MathHelperTest, CubicBezier_MaterialDesignStandard) { - // Material Design Standard: cubic-bezier(0.2, 0, 0, 1) - // This is an ease-out curve (fast start, slow end) - // At t=0.5, y should be > 0.8 (curve rises quickly then plateaus) - float result = cf::ui::math::cubicBezier(0.2f, 0.0f, 0.0f, 1.0f, 0.5f); - EXPECT_GT(result, 0.8f); // Ease-out rises quickly - EXPECT_LT(result, 0.95f); // But shouldn't reach near-1 yet -} - -TEST(MathHelperTest, CubicBezier_Monotonicity) { - // For proper easing curves, output should increase with t - float prev = cf::ui::math::cubicBezier(0.2f, 0.0f, 0.0f, 1.0f, 0.0f); - for (int i = 1; i <= 10; ++i) { - float t = i / 10.0f; - float curr = cf::ui::math::cubicBezier(0.2f, 0.0f, 0.0f, 1.0f, t); - EXPECT_GE(curr, prev); - prev = curr; - } -} - -// ============================================================================= -// Test Suite 5: springStep -// ============================================================================= - -TEST(SpringStepTest, BasicSpringBehavior) { - // Spring should move towards target - auto [pos, vel] = cf::ui::math::springStep(0.0f, 0.0f, 10.0f, 100.0f, 10.0f, 0.016f); - EXPECT_GT(pos, 0.0f); // Position should increase - EXPECT_GT(vel, 0.0f); // Velocity should increase -} - -TEST(SpringStepTest, SpringAtTarget_NoMovement) { - // Already at target with zero velocity should stay - auto [pos, vel] = cf::ui::math::springStep(10.0f, 0.0f, 10.0f, 100.0f, 10.0f, 0.016f); - EXPECT_FLOAT_EQ(pos, 10.0f); - EXPECT_FLOAT_EQ(vel, 0.0f); -} - -TEST(SpringStepTest, SpringOvershoot) { - // With low damping, spring should overshoot - float stiffness = 200.0f; - float damping = 5.0f; - float dt = 0.016f; - - float pos = 0.0f; - float vel = 0.0f; - float target = 100.0f; - - bool overshot = false; - for (int i = 0; i < 100; ++i) { - auto result = cf::ui::math::springStep(pos, vel, target, stiffness, damping, dt); - pos = result.first; - vel = result.second; - if (pos > target) { - overshot = true; - break; - } - } - EXPECT_TRUE(overshot); -} - -TEST(SpringStepTest, SpringConvergence) { - // With proper damping, spring should converge - float stiffness = 200.0f; - float damping = 20.0f; - float dt = 0.016f; - - float pos = 0.0f; - float vel = 0.0f; - float target = 100.0f; - - // Simulate for 3 seconds - for (int i = 0; i < 187; ++i) { - auto result = cf::ui::math::springStep(pos, vel, target, stiffness, damping, dt); - pos = result.first; - vel = result.second; - } - - // Should be close to target - EXPECT_NEAR(pos, target, 1.0f); - // Velocity should be small - EXPECT_NEAR(std::abs(vel), 0.0f, 5.0f); -} - -TEST(SpringStepTest, SpringVelocityDecay) { - // High damping should cause velocity to decay quickly - float stiffness = 100.0f; - float damping = 50.0f; - float dt = 0.016f; - - float pos = 0.0f; - float vel = 100.0f; // Initial velocity toward target - float target = 10.0f; - - auto [pos1, vel1] = cf::ui::math::springStep(pos, vel, target, stiffness, damping, dt); - EXPECT_LT(std::abs(vel1), std::abs(vel)); -} - -TEST(SpringStepTest, SpringZeroTimeStep) { - // dt=0 should not change state - auto [pos, vel] = cf::ui::math::springStep(5.0f, 10.0f, 100.0f, 100.0f, 10.0f, 0.0f); - EXPECT_FLOAT_EQ(pos, 5.0f); - EXPECT_FLOAT_EQ(vel, 10.0f); -} - -TEST(SpringStepTest, SpringNegativeStiffness) { - // Negative stiffness should move away from target (unstable) - auto [pos, vel] = cf::ui::math::springStep(50.0f, 0.0f, 100.0f, -100.0f, 10.0f, 0.016f); - EXPECT_LT(pos, 50.0f); // Should move away from target -} - -TEST(SpringStepTest, SpringHighDamping) { - // High damping should prevent overshoot - float stiffness = 400.0f; - float damping = 50.0f; - float dt = 0.016f; - - float pos = 0.0f; - float vel = 0.0f; - float target = 100.0f; - - bool overshot = false; - for (int i = 0; i < 200; ++i) { - auto result = cf::ui::math::springStep(pos, vel, target, stiffness, damping, dt); - pos = result.first; - vel = result.second; - if (pos > target) { - overshot = true; - break; - } - } - EXPECT_FALSE(overshot); -} - -// ============================================================================= -// Test Suite 6: lerpAngle -// ============================================================================= - -TEST(LerpAngleTest, NormalInterpolation) { - // Normal angle interpolation - EXPECT_NEAR(cf::ui::math::lerpAngle(0.0f, 90.0f, 0.5f), 45.0f, 0.01f); - EXPECT_NEAR(cf::ui::math::lerpAngle(0.0f, 180.0f, 0.5f), 90.0f, 0.01f); -} - -TEST(LerpAngleTest, T0_ReturnsStartAngle) { - EXPECT_FLOAT_EQ(cf::ui::math::lerpAngle(45.0f, 135.0f, 0.0f), 45.0f); - EXPECT_FLOAT_EQ(cf::ui::math::lerpAngle(180.0f, 270.0f, 0.0f), 180.0f); -} - -TEST(LerpAngleTest, T1_ReturnsEndAngle) { - EXPECT_NEAR(cf::ui::math::lerpAngle(45.0f, 135.0f, 1.0f), 135.0f, 0.01f); - EXPECT_NEAR(cf::ui::math::lerpAngle(180.0f, 270.0f, 1.0f), 270.0f, 0.01f); -} - -TEST(LerpAngleTest, CrossZeroBoundaryForward) { - // Interpolating from 350° to 10° should go through 0° - float result = cf::ui::math::lerpAngle(350.0f, 10.0f, 0.5f); - // Should be close to 0° (or 360°) - EXPECT_TRUE(result < 10.0f || result > 350.0f); -} - -TEST(LerpAngleTest, CrossZeroBoundaryBackward) { - // Interpolating from 10° to 350° should go the short way through 0° - float result = cf::ui::math::lerpAngle(10.0f, 350.0f, 0.5f); - // Should be close to 0° or 360° - EXPECT_TRUE(result < 20.0f || result > 340.0f); -} - -TEST(LerpAngleTest, ShortestPath180Degrees) { - // 0° to 180° - either direction is the same - float result = cf::ui::math::lerpAngle(0.0f, 180.0f, 0.5f); - EXPECT_NEAR(result, 90.0f, 0.1f); -} - -TEST(LerpAngleTest, LargeAngleDifference) { - // Interpolating across large angles - float result = cf::ui::math::lerpAngle(30.0f, 330.0f, 0.5f); - // Should take shortest path through 0° - EXPECT_NEAR(result, 0.0f, 10.0f); -} - -TEST(LerpAngleTest, NegativeAngles) { - // Negative angles should work - float result = cf::ui::math::lerpAngle(-90.0f, 90.0f, 0.5f); - EXPECT_NEAR(result, 0.0f, 0.1f); -} - -TEST(LerpAngleTest, SameAngle) { - EXPECT_NEAR(cf::ui::math::lerpAngle(45.0f, 45.0f, 0.5f), 45.0f, 0.01f); - EXPECT_NEAR(cf::ui::math::lerpAngle(0.0f, 0.0f, 0.5f), 0.0f, 0.01f); -} - -TEST(LerpAngleTest, ClockwiseRotation) { - // Small clockwise rotation - float result = cf::ui::math::lerpAngle(0.0f, 45.0f, 0.5f); - EXPECT_NEAR(result, 22.5f, 0.1f); -} - -TEST(LerpAngleTest, CounterClockwiseRotation) { - // Counter-clockwise rotation (short path) - float result = cf::ui::math::lerpAngle(45.0f, 0.0f, 0.5f); - // Should go through 22.5° - EXPECT_NEAR(result, 22.5f, 0.1f); -} - -TEST(LerpAngleTest, Extrapolation) { - // t > 1 should extrapolate - float result = cf::ui::math::lerpAngle(0.0f, 90.0f, 1.5f); - EXPECT_NEAR(result, 135.0f, 0.1f); -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/components/CMakeLists.txt b/test/ui/components/CMakeLists.txt deleted file mode 100644 index 5a29bf321..000000000 --- a/test/ui/components/CMakeLists.txt +++ /dev/null @@ -1,63 +0,0 @@ -# ============================================================================= -# Layer 4: Material Behavior Layer Tests -# ============================================================================= - -# Common link libraries for component tests (use cfui shared library) -set(COMPONENT_TEST_LIBS "cfui;Qt6::Widgets;GTest::gtest;GTest::gtest_main") - -# ============================================================================= -# state_machine_test -# ============================================================================= -add_gtest_executable( - TEST_NAME state_machine_test - SOURCE_FILE state_machine_test.cpp - LINK_LIBRARIES ${COMPONENT_TEST_LIBS} - LABELS "ui;components;layer4;state" - LOG_MODULE ui_components_tests -) - -# ============================================================================= -# ripple_helper_test -# ============================================================================= -add_gtest_executable( - TEST_NAME ripple_helper_test - SOURCE_FILE ripple_helper_test.cpp - LINK_LIBRARIES ${COMPONENT_TEST_LIBS} - LABELS "ui;components;layer4;ripple" - LOG_MODULE ui_components_tests -) - -# ============================================================================= -# elevation_controller_test -# ============================================================================= -add_gtest_executable( - TEST_NAME elevation_controller_test - SOURCE_FILE elevation_controller_test.cpp - LINK_LIBRARIES ${COMPONENT_TEST_LIBS} - LABELS "ui;components;layer4;elevation" - LOG_MODULE ui_components_tests -) - -# ============================================================================= -# focus_ring_test -# ============================================================================= -add_gtest_executable( - TEST_NAME focus_ring_test - SOURCE_FILE focus_ring_test.cpp - LINK_LIBRARIES ${COMPONENT_TEST_LIBS} - LABELS "ui;components;layer4;focus" - LOG_MODULE ui_components_tests -) - -# ============================================================================= -# painter_layer_test -# ============================================================================= -add_gtest_executable( - TEST_NAME painter_layer_test - SOURCE_FILE painter_layer_test.cpp - LINK_LIBRARIES ${COMPONENT_TEST_LIBS} - LABELS "ui;components;layer4;painter" - LOG_MODULE ui_components_tests -) - -log_info("UI_Components_Tests" "Configured ui/components tests") diff --git a/test/ui/components/elevation_controller_test.cpp b/test/ui/components/elevation_controller_test.cpp deleted file mode 100644 index af6b399c3..000000000 --- a/test/ui/components/elevation_controller_test.cpp +++ /dev/null @@ -1,323 +0,0 @@ -/** - * @file elevation_controller_test.cpp - * @brief Unit tests for MdElevationController (Material Behavior Layer) - * - * Test Coverage: - * 1. Elevation level setting - * 2. Shadow parameter calculation (via paint output) - * 3. Tonal overlay calculation - * 4. Level clamping - */ - -#include "color.h" -#include "ui/widget/material/base/elevation_controller.h" -#include -#include -#include -#include - -using namespace cf::ui::widget::material::base; -using CFColor = cf::ui::base::CFColor; - -// ============================================================================= -// Test Helper: Count Non-Transparent Pixels -// ============================================================================= - -/** - * @brief Helper function to count non-transparent pixels in an image. - * Used to verify paint behavior. - */ -static int countNonTransparentPixels(const QImage& image) { - int count = 0; - for (int y = 0; y < image.height(); ++y) { - for (int x = 0; x < image.width(); ++x) { - QRgb pixel = image.pixel(x, y); - if (qAlpha(pixel) > 0) { - ++count; - } - } - } - return count; -} - -/** - * @brief Helper to count pixels with alpha > threshold. - * Useful for detecting semi-transparent shadow pixels. - */ -static int countSemiTransparentPixels(const QImage& image, int alphaThreshold = 10) { - int count = 0; - for (int y = 0; y < image.height(); ++y) { - for (int x = 0; x < image.width(); ++x) { - QRgb pixel = image.pixel(x, y); - if (qAlpha(pixel) > alphaThreshold) { - ++count; - } - } - } - return count; -} - -// ============================================================================= -// Test Suite 1: Elevation Configuration -// ============================================================================= - -TEST(ElevationControllerTest, InitialElevation_IsZero) { - MdElevationController controller(nullptr, nullptr); - EXPECT_EQ(controller.elevation(), 0); -} - -TEST(ElevationControllerTest, SetElevation_UpdatesLevel) { - MdElevationController controller(nullptr, nullptr); - controller.setElevation(3); - EXPECT_EQ(controller.elevation(), 3); -} - -TEST(ElevationControllerTest, SetElevation_ClampsToFive) { - MdElevationController controller(nullptr, nullptr); - controller.setElevation(10); - EXPECT_EQ(controller.elevation(), 5); // Should be clamped -} - -TEST(ElevationControllerTest, SetElevation_ClampsToZero) { - MdElevationController controller(nullptr, nullptr); - controller.setElevation(-5); - EXPECT_EQ(controller.elevation(), 0); // Should be clamped -} - -// ============================================================================= -// Test Suite 2: Shadow Parameters (via Paint Output) -// ============================================================================= - -TEST(ElevationControllerTest, PaintShadow_Level0_NoShadow) { - MdElevationController controller(nullptr, nullptr); - controller.setElevation(0); - - QImage image(100, 100, QImage::Format_ARGB32); - image.fill(Qt::transparent); - - QPainter painter(&image); - QPainterPath shape; - shape.addRect(30, 30, 40, 40); - - controller.paintShadow(&painter, shape); - painter.end(); - - // Level 0 should have minimal or no shadow - // Shadow should be mostly transparent - int semiTransparent = countSemiTransparentPixels(image, 5); - EXPECT_LT(semiTransparent, 10) << "Level 0 should have minimal shadow"; -} - -TEST(ElevationControllerTest, PaintShadow_Level5_HasVisibleShadow) { - MdElevationController controller(nullptr, nullptr); - controller.setElevation(5); - - QImage image(100, 100, QImage::Format_ARGB32); - image.fill(Qt::transparent); - - QPainter painter(&image); - QPainterPath shape; - shape.addRect(30, 30, 40, 40); - - controller.paintShadow(&painter, shape); - painter.end(); - - // Level 5 should have a visible shadow - int nonTransparent = countNonTransparentPixels(image); - EXPECT_GT(nonTransparent, 0) << "Level 5 should have visible shadow"; -} - -TEST(ElevationControllerTest, PaintShadow_HigherLevel_MoreShadowPixels) { - MdElevationController controllerLow(nullptr, nullptr); - controllerLow.setElevation(1); - - MdElevationController controllerHigh(nullptr, nullptr); - controllerHigh.setElevation(5); - - QImage image1(100, 100, QImage::Format_ARGB32); - image1.fill(Qt::transparent); - { - QPainter painter(&image1); - QPainterPath shape; - shape.addRect(30, 30, 40, 40); - controllerLow.paintShadow(&painter, shape); - } - - QImage image2(100, 100, QImage::Format_ARGB32); - image2.fill(Qt::transparent); - { - QPainter painter(&image2); - QPainterPath shape; - shape.addRect(30, 30, 40, 40); - controllerHigh.paintShadow(&painter, shape); - } - - // Higher elevation should produce more shadow pixels (larger blur radius) - int pixelsLevel1 = countSemiTransparentPixels(image1, 5); - int pixelsLevel5 = countSemiTransparentPixels(image2, 5); - - EXPECT_GT(pixelsLevel5, pixelsLevel1) - << "Higher elevation should produce more shadow pixels due to larger blur"; -} - -TEST(ElevationControllerTest, PaintShadow_NullPainter_DoesNotCrash) { - MdElevationController controller(nullptr, nullptr); - controller.setElevation(5); - - QPainterPath shape; - shape.addRect(30, 30, 40, 40); - - // Should not crash with null painter - controller.paintShadow(nullptr, shape); -} - -TEST(ElevationControllerTest, PaintShadow_ComplexShape_HandlesCorrectly) { - MdElevationController controller(nullptr, nullptr); - controller.setElevation(3); - - QImage image(100, 100, QImage::Format_ARGB32); - image.fill(Qt::transparent); - - QPainter painter(&image); - // Create a more complex shape (rounded rect) - QPainterPath shape; - shape.addRoundedRect(30, 30, 40, 40, 8, 8); - - controller.paintShadow(&painter, shape); - painter.end(); - - // Should paint shadow for complex shapes too - EXPECT_GT(countNonTransparentPixels(image), 0); -} - -// ============================================================================= -// Test Suite 3: Tonal Overlay -// ============================================================================= - -TEST(ElevationControllerTest, TonalOverlay_Level0_ReturnsSurface) { - MdElevationController controller(nullptr, nullptr); - controller.setElevation(0); - - CFColor surface(200, 200, 200); - CFColor primary(100, 100, 255); - - CFColor result = controller.tonalOverlay(surface, primary); - // Level 0 should return surface unchanged - EXPECT_EQ(result.native_color(), surface.native_color()); -} - -TEST(ElevationControllerTest, TonalOverlay_HighElevation_BlendsWithPrimary) { - MdElevationController controller(nullptr, nullptr); - controller.setElevation(5); - - CFColor surface(200, 200, 200); - CFColor primary(100, 100, 255); - - CFColor result = controller.tonalOverlay(surface, primary); - - // Result should differ from surface at higher elevation - // (blends in primary color for tonal elevation effect) - QColor resultColor = result.native_color(); - QColor surfaceColor = surface.native_color(); - - // At least one color component should be different - bool differs = - (resultColor.red() != surfaceColor.red() || resultColor.green() != surfaceColor.green() || - resultColor.blue() != surfaceColor.blue()); - - EXPECT_TRUE(differs) << "Higher elevation should modify surface color"; -} - -TEST(ElevationControllerTest, TonalOverlay_IncreasingElevation_MoreBlending) { - MdElevationController controllerLow(nullptr, nullptr); - controllerLow.setElevation(1); - - MdElevationController controllerHigh(nullptr, nullptr); - controllerHigh.setElevation(5); - - CFColor surface(200, 200, 200); - CFColor primary(100, 100, 255); - - CFColor resultLow = controllerLow.tonalOverlay(surface, primary); - CFColor resultHigh = controllerHigh.tonalOverlay(surface, primary); - - // Higher elevation should result in more blending (closer to primary) - QColor lowColor = resultLow.native_color(); - QColor highColor = resultHigh.native_color(); - QColor surfaceColor = surface.native_color(); - QColor primaryColor = primary.native_color(); - - // Calculate distance from surface - int lowDist = std::abs(lowColor.red() - surfaceColor.red()) + - std::abs(lowColor.green() - surfaceColor.green()) + - std::abs(lowColor.blue() - surfaceColor.blue()); - - int highDist = std::abs(highColor.red() - surfaceColor.red()) + - std::abs(highColor.green() - surfaceColor.green()) + - std::abs(highColor.blue() - surfaceColor.blue()); - - EXPECT_GE(highDist, lowDist) << "Higher elevation should blend more primary color"; -} - -// ============================================================================= -// Test Suite 4: Animation -// ============================================================================= - -TEST(ElevationControllerTest, AnimateTo_DoesNotCrash) { - MdElevationController controller(nullptr, nullptr); - controller.setElevation(0); - - cf::ui::core::MotionSpec spec; - spec.durationMs = 200; - spec.easing = cf::ui::base::Easing::Type::Standard; - - controller.animateTo(3, spec); - // Should handle gracefully even without animation factory - SUCCEED(); -} - -TEST(ElevationControllerTest, AnimateTo_BeyondRange_ClampsTarget) { - MdElevationController controller(nullptr, nullptr); - controller.setElevation(0); - - cf::ui::core::MotionSpec spec; - spec.durationMs = 200; - spec.easing = cf::ui::base::Easing::Type::Standard; - - controller.animateTo(10, spec); - // Target should be clamped to 5 - // (Actual elevation may not change without animation factory) - SUCCEED(); -} - -// ============================================================================= -// Test Suite 5: Edge Cases -// ============================================================================= - -TEST(ElevationControllerTest, SetSameElevation_DoesNotTriggerAnimation) { - MdElevationController controller(nullptr, nullptr); - controller.setElevation(3); - - int before = controller.elevation(); - controller.setElevation(3); - - EXPECT_EQ(controller.elevation(), before); -} - -TEST(ElevationControllerTest, NegativeElevation_ClampsToZero) { - MdElevationController controller(nullptr, nullptr); - controller.setElevation(-1); - EXPECT_EQ(controller.elevation(), 0); - - controller.setElevation(-100); - EXPECT_EQ(controller.elevation(), 0); -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/components/focus_ring_test.cpp b/test/ui/components/focus_ring_test.cpp deleted file mode 100644 index 7722a0202..000000000 --- a/test/ui/components/focus_ring_test.cpp +++ /dev/null @@ -1,234 +0,0 @@ -/** - * @file focus_ring_test.cpp - * @brief Unit tests for MdFocusIndicator (Material Behavior Layer) - * - * Test Coverage: - * 1. Focus state changes (via paint output) - * 2. Progress animation behavior - * 3. Paint method behavior with QImage verification - */ - -#include "color.h" -#include "ui/widget/material/base/focus_ring.h" -#include -#include -#include -#include - -using namespace cf::ui::widget::material::base; -using CFColor = cf::ui::base::CFColor; - -// ============================================================================= -// Test Helper: Count Non-Transparent Pixels -// ============================================================================= - -/** - * @brief Helper function to count non-transparent pixels in an image. - * Used to verify paint behavior. - */ -static int countNonTransparentPixels(const QImage& image) { - int count = 0; - for (int y = 0; y < image.height(); ++y) { - for (int x = 0; x < image.width(); ++x) { - QRgb pixel = image.pixel(x, y); - if (qAlpha(pixel) > 0) { - ++count; - } - } - } - return count; -} - -// ============================================================================= -// Test Suite 1: Focus Events (via Paint Output) -// ============================================================================= - -TEST(FocusRingTest, InitialState_PaintDoesNothing) { - MdFocusIndicator indicator(nullptr, nullptr); - - QImage image(100, 100, QImage::Format_ARGB32); - image.fill(Qt::transparent); - - QPainter painter(&image); - QPainterPath shape; - shape.addRect(20, 20, 60, 60); - - indicator.paint(&painter, shape, CFColor(100, 150, 255)); - painter.end(); - - // Without focus, progress is 0, so paint should do nothing - EXPECT_EQ(countNonTransparentPixels(image), 0); -} - -TEST(FocusRingTest, OnFocusIn_PaintsFocusRing) { - MdFocusIndicator indicator(nullptr, nullptr); - indicator.onFocusIn(); - - QImage image(100, 100, QImage::Format_ARGB32); - image.fill(Qt::transparent); - - QPainter painter(&image); - QPainterPath shape; - shape.addRect(20, 20, 60, 60); - - indicator.paint(&painter, shape, CFColor(100, 150, 255)); - painter.end(); - - // With focus, should paint the focus ring - int nonTransparent = countNonTransparentPixels(image); - EXPECT_GT(nonTransparent, 0) << "Focus ring should be painted after onFocusIn"; -} - -TEST(FocusRingTest, OnFocusOut_RemovesFocusRing) { - MdFocusIndicator indicator(nullptr, nullptr); - indicator.onFocusIn(); - indicator.onFocusOut(); - - QImage image(100, 100, QImage::Format_ARGB32); - image.fill(Qt::transparent); - - QPainter painter(&image); - QPainterPath shape; - shape.addRect(20, 20, 60, 60); - - indicator.paint(&painter, shape, CFColor(100, 150, 255)); - painter.end(); - - // After focus out, progress should be 0 again - EXPECT_EQ(countNonTransparentPixels(image), 0); -} - -// ============================================================================= -// Test Suite 2: Multiple Focus Cycles -// ============================================================================= - -TEST(FocusRingTest, MultipleFocusCycles_PaintsCorrectly) { - MdFocusIndicator indicator(nullptr, nullptr); - - // First focus cycle - indicator.onFocusIn(); - - QImage image1(100, 100, QImage::Format_ARGB32); - image1.fill(Qt::transparent); - { - QPainter painter(&image1); - QPainterPath shape; - shape.addRect(20, 20, 60, 60); - indicator.paint(&painter, shape, CFColor(100, 150, 255)); - } - int pixelsAfterFirstFocus = countNonTransparentPixels(image1); - - indicator.onFocusOut(); - - QImage image2(100, 100, QImage::Format_ARGB32); - image2.fill(Qt::transparent); - { - QPainter painter(&image2); - QPainterPath shape; - shape.addRect(20, 20, 60, 60); - indicator.paint(&painter, shape, CFColor(100, 150, 255)); - } - EXPECT_EQ(countNonTransparentPixels(image2), 0) << "Should be clear after first focus out"; - - // Second focus cycle - indicator.onFocusIn(); - - QImage image3(100, 100, QImage::Format_ARGB32); - image3.fill(Qt::transparent); - { - QPainter painter(&image3); - QPainterPath shape; - shape.addRect(20, 20, 60, 60); - indicator.paint(&painter, shape, CFColor(100, 150, 255)); - } - int pixelsAfterSecondFocus = countNonTransparentPixels(image3); - - // Both focus states should paint the same amount - EXPECT_EQ(pixelsAfterFirstFocus, pixelsAfterSecondFocus) - << "Multiple focus cycles should produce consistent paint output"; - - indicator.onFocusOut(); - - QImage image4(100, 100, QImage::Format_ARGB32); - image4.fill(Qt::transparent); - { - QPainter painter(&image4); - QPainterPath shape; - shape.addRect(20, 20, 60, 60); - indicator.paint(&painter, shape, CFColor(100, 150, 255)); - } - EXPECT_EQ(countNonTransparentPixels(image4), 0) << "Should be clear after second focus out"; -} - -// ============================================================================= -// Test Suite 3: Edge Cases -// ============================================================================= - -TEST(FocusRingTest, Paint_NullPainter_DoesNotCrash) { - MdFocusIndicator indicator(nullptr, nullptr); - indicator.onFocusIn(); - - QPainterPath shape; - shape.addRect(20, 20, 60, 60); - - // Should not crash with null painter - indicator.paint(nullptr, shape, CFColor(100, 150, 255)); -} - -TEST(FocusRingTest, Paint_WithDifferentColors_AppliesColor) { - MdFocusIndicator indicator(nullptr, nullptr); - indicator.onFocusIn(); - - // Test with red color - QImage image1(100, 100, QImage::Format_ARGB32); - image1.fill(Qt::transparent); - { - QPainter painter(&image1); - QPainterPath shape; - shape.addRect(20, 20, 60, 60); - indicator.paint(&painter, shape, CFColor(255, 0, 0)); - } - - // Find a red pixel (with some tolerance for alpha) - bool foundRed = false; - for (int y = 0; y < image1.height() && !foundRed; ++y) { - for (int x = 0; x < image1.width() && !foundRed; ++x) { - QRgb pixel = image1.pixel(x, y); - if (qAlpha(pixel) > 0) { - // Check if red channel is dominant - if (qRed(pixel) > qGreen(pixel) && qRed(pixel) > qBlue(pixel)) { - foundRed = true; - } - } - } - } - EXPECT_TRUE(foundRed) << "Painted ring should have red color component"; -} - -TEST(FocusRingTest, Paint_ComplexShape_HandlesCorrectly) { - MdFocusIndicator indicator(nullptr, nullptr); - indicator.onFocusIn(); - - QImage image(100, 100, QImage::Format_ARGB32); - image.fill(Qt::transparent); - - QPainter painter(&image); - // Create a more complex shape (rounded rect) - QPainterPath shape; - shape.addRoundedRect(20, 20, 60, 60, 10, 10); - - indicator.paint(&painter, shape, CFColor(100, 150, 255)); - painter.end(); - - // Should paint something for complex shapes too - EXPECT_GT(countNonTransparentPixels(image), 0); -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/components/painter_layer_test.cpp b/test/ui/components/painter_layer_test.cpp deleted file mode 100644 index ae779f66c..000000000 --- a/test/ui/components/painter_layer_test.cpp +++ /dev/null @@ -1,81 +0,0 @@ -/** - * @file painter_layer_test.cpp - * @brief Unit tests for PainterLayer (Material Behavior Layer) - * - * Test Coverage: - * 1. Color and opacity setting - * 2. Paint method behavior - * 3. Edge cases (zero opacity, null painter) - */ - -#include "ui/widget/material/base/painter_layer.h" -#include - -using namespace cf::ui::widget::material::base; -using CFColor = cf::ui::base::CFColor; -// ============================================================================= -// Test Suite 1: Configuration -// ============================================================================= - -TEST(PainterLayerTest, Constructor_InitializesDefaultValues) { - PainterLayer layer(nullptr); - // Should initialize with valid defaults - // Opacity should be non-negative - // Color should be valid -} - -TEST(PainterLayerTest, SetColor_UpdatesCachedColor) { - PainterLayer layer(nullptr); - CFColor color(Qt::red); - layer.setColor(color); - // Color should be stored for painting -} - -TEST(PainterLayerTest, SetOpacity_UpdatesOpacity) { - PainterLayer layer(nullptr); - layer.setOpacity(0.5f); - // Opacity should be stored for painting -} - -TEST(PainterLayerTest, SetZeroOpacity_Valid) { - PainterLayer layer(nullptr); - layer.setOpacity(0.0f); - // Zero opacity is valid (layer becomes invisible) -} - -TEST(PainterLayerTest, SetFullOpacity_Valid) { - PainterLayer layer(nullptr); - layer.setOpacity(1.0f); - // Full opacity is valid (layer is fully visible) -} - -// ============================================================================= -// Test Suite 2: Edge Cases -// ============================================================================= - -TEST(PainterLayerTest, SetNegativeOpacity_ClampsOrAccepted) { - PainterLayer layer(nullptr); - layer.setOpacity(-0.5f); - // Behavior depends on implementation: - // - May clamp to 0.0 - // - May accept negative values - // Either is acceptable as long as it doesn't crash -} - -TEST(PainterLayerTest, SetOpacityAboveOne_ClampsOrAccepted) { - PainterLayer layer(nullptr); - layer.setOpacity(1.5f); - // Behavior depends on implementation: - // - May clamp to 1.0 - // - May accept values > 1 - // Either is acceptable -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/components/ripple_helper_test.cpp b/test/ui/components/ripple_helper_test.cpp deleted file mode 100644 index 7efe6064e..000000000 --- a/test/ui/components/ripple_helper_test.cpp +++ /dev/null @@ -1,279 +0,0 @@ -/** - * @file ripple_helper_test.cpp - * @brief Unit tests for RippleHelper (Material Behavior Layer) - * - * Test Coverage: - * 1. Ripple creation on press - * 2. Ripple lifecycle (expand, fade, remove) - * 3. Max radius calculation - * 4. Mode switching (bounded/unbounded) - * 5. Paint output verification - */ - -#include "color.h" -#include "ui/widget/material/base/ripple_helper.h" -#include -#include -#include -#include - -using namespace cf::ui::widget::material::base; -using CFColor = cf::ui::base::CFColor; - -// ============================================================================= -// Test Helper: Count Non-Transparent Pixels -// ============================================================================= - -/** - * @brief Helper function to count non-transparent pixels in an image. - * Used to verify paint behavior. - */ -static int countNonTransparentPixels(const QImage& image) { - int count = 0; - for (int y = 0; y < image.height(); ++y) { - for (int x = 0; x < image.width(); ++x) { - QRgb pixel = image.pixel(x, y); - if (qAlpha(pixel) > 0) { - ++count; - } - } - } - return count; -} - -// ============================================================================= -// Test Suite 1: Ripple Creation -// ============================================================================= - -TEST(RippleHelperTest, InitialState_NoActiveRipples) { - RippleHelper ripple(nullptr, nullptr); - EXPECT_FALSE(ripple.hasActiveRipple()); -} - -TEST(RippleHelperTest, OnPress_WithoutFactory_NoRipplesCreated) { - RippleHelper ripple(nullptr, nullptr); - QRectF widgetRect(0, 0, 100, 100); - ripple.onPress(QPoint(50, 50), widgetRect); - - // Without animation factory, onPress returns early and doesn't create ripples - EXPECT_FALSE(ripple.hasActiveRipple()) << "No factory means no ripples created"; -} - -TEST(RippleHelperTest, OnRelease_WithoutFactory_DoesNotCrash) { - RippleHelper ripple(nullptr, nullptr); - QRectF widgetRect(0, 0, 100, 100); - ripple.onPress(QPoint(50, 50), widgetRect); - ripple.onRelease(); - - // Without factory, no ripples are created, so onRelease has nothing to do - // This test verifies the interface doesn't crash - EXPECT_FALSE(ripple.hasActiveRipple()) << "No ripples without factory"; -} - -TEST(RippleHelperTest, OnCancel_ClearsRipplesImmediately) { - RippleHelper ripple(nullptr, nullptr); - QRectF widgetRect(0, 0, 100, 100); - - // Without animation factory, onPress doesn't create ripples - // So we just verify onCancel doesn't crash when called - ripple.onPress(QPoint(50, 50), widgetRect); - EXPECT_FALSE(ripple.hasActiveRipple()) << "No factory means no ripples created"; - - // onCancel should clear any existing ripples (none in this case) - ripple.onCancel(); - EXPECT_FALSE(ripple.hasActiveRipple()) << "Cancel clears all ripples"; - - // The test verifies the interface works correctly - SUCCEED(); -} - -// ============================================================================= -// Test Suite 2: Mode Switching -// ============================================================================= - -TEST(RippleHelperTest, DefaultMode_IsBounded) { - RippleHelper ripple(nullptr, nullptr); - // Default should be bounded (clipped to widget) - // We verify the interface exists and default mode doesn't crash - SUCCEED(); -} - -TEST(RippleHelperTest, SetMode_Unbounded_DoesNotCrash) { - RippleHelper ripple(nullptr, nullptr); - ripple.setMode(RippleHelper::Mode::Unbounded); - // Mode should be switchable - SUCCEED(); -} - -TEST(RippleHelperTest, SetMode_Bounded_DoesNotCrash) { - RippleHelper ripple(nullptr, nullptr); - ripple.setMode(RippleHelper::Mode::Bounded); - SUCCEED(); -} - -// ============================================================================= -// Test Suite 3: Color Configuration -// ============================================================================= - -TEST(RippleHelperTest, SetColor_DoesNotCrash) { - RippleHelper ripple(nullptr, nullptr); - CFColor color(Qt::red); - ripple.setColor(color); - // Color should be stored for painting - SUCCEED(); -} - -// ============================================================================= -// Test Suite 4: Paint Output Verification -// ============================================================================= - -TEST(RippleHelperTest, Paint_Initially_DoesNothing) { - RippleHelper ripple(nullptr, nullptr); - - QImage image(100, 100, QImage::Format_ARGB32); - image.fill(Qt::white); - - QPainter painter(&image); - QPainterPath clipPath; - clipPath.addRect(0, 0, 100, 100); - - ripple.paint(&painter, clipPath); - painter.end(); - - // Without active ripples, paint should not modify the image - // (Image should still be all white) - int nonWhitePixels = 0; - for (int y = 0; y < image.height(); ++y) { - for (int x = 0; x < image.width(); ++x) { - QRgb pixel = image.pixel(x, y); - if (pixel != qRgb(255, 255, 255)) { - ++nonWhitePixels; - } - } - } - EXPECT_EQ(nonWhitePixels, 0) << "No ripples should be painted without onPress"; -} - -TEST(RippleHelperTest, Paint_AfterCancel_DoesNothing) { - RippleHelper ripple(nullptr, nullptr); - - QRectF widgetRect(0, 0, 100, 100); - ripple.onPress(QPoint(50, 50), widgetRect); - ripple.onCancel(); - - QImage image(100, 100, QImage::Format_ARGB32); - image.fill(Qt::white); - - QPainter painter(&image); - QPainterPath clipPath; - clipPath.addRect(0, 0, 100, 100); - - ripple.paint(&painter, clipPath); - painter.end(); - - // After cancel, no ripples should be painted - int nonWhitePixels = 0; - for (int y = 0; y < image.height(); ++y) { - for (int x = 0; x < image.width(); ++x) { - QRgb pixel = image.pixel(x, y); - if (pixel != qRgb(255, 255, 255)) { - ++nonWhitePixels; - } - } - } - EXPECT_EQ(nonWhitePixels, 0) << "No ripples should be painted after onCancel"; -} - -TEST(RippleHelperTest, Paint_WithColor_AppliesColor) { - RippleHelper ripple(nullptr, nullptr); - - // Set a specific color (red) - ripple.setColor(CFColor(255, 0, 0)); - - QRectF widgetRect(0, 0, 100, 100); - ripple.onPress(QPoint(50, 50), widgetRect); - - QImage image(100, 100, QImage::Format_ARGB32); - image.fill(Qt::white); - - QPainter painter(&image); - QPainterPath clipPath; - clipPath.addRect(0, 0, 100, 100); - - ripple.paint(&painter, clipPath); - painter.end(); - - // Verify that the color is applied (without animation, it should paint directly) - // At least some pixels should have been modified - int modifiedPixels = 0; - for (int y = 0; y < image.height(); ++y) { - for (int x = 0; x < image.width(); ++x) { - QRgb pixel = image.pixel(x, y); - if (pixel != qRgb(255, 255, 255)) { - ++modifiedPixels; - } - } - } - // Without animation factory, the ripple should be drawn directly - // (or not at all depending on implementation - this test documents behavior) -} - -// ============================================================================= -// Test Suite 5: Max Radius Calculation -// ============================================================================= - -TEST(RippleHelperTest, MaxRadius_CornerToCorner) { - // For a 100x100 rect, max radius from center (50,50) - // should be distance to farthest corner - QRectF widgetRect(0, 0, 100, 100); - QPointF center(50, 50); - - float expected = std::hypot(50.0, 50.0); // Distance to corner - float tolerance = 1.0f; - - // Verify the calculation is approximately sqrt(2)*50 - EXPECT_GT(expected, 70.0f); // Should be > 70 - EXPECT_LT(expected, 72.0f); // Should be < 72 (sqrt(2)*50 ≈ 70.71) -} - -TEST(RippleHelperTest, MaxRadius_OffCenter_CalculatesCorrectly) { - // Test max radius from off-center position - QRectF widgetRect(0, 0, 100, 100); - QPointF center(25, 50); // Left of center - - // Distance to farthest corner (100, 100) - float expected = std::hypot(75.0, 50.0); // Distance to corner (100, 100) - - EXPECT_GT(expected, 90.0f); // Should be > 90 - EXPECT_LT(expected, 91.0f); // Should be < 91 (sqrt(75^2+50^2) ≈ 90.14) -} - -// ============================================================================= -// Test Suite 6: Multiple Press Events -// ============================================================================= - -TEST(RippleHelperTest, MultiplePresses_WithoutFactory_HandlesCorrectly) { - RippleHelper ripple(nullptr, nullptr); - QRectF widgetRect(0, 0, 100, 100); - - // Multiple press events - without factory, no ripples are created - ripple.onPress(QPoint(30, 30), widgetRect); - ripple.onPress(QPoint(50, 50), widgetRect); - ripple.onPress(QPoint(70, 70), widgetRect); - - // Should handle multiple presses without crashing - EXPECT_FALSE(ripple.hasActiveRipple()) << "No ripples created without factory"; - - // onCancel should still work correctly - ripple.onCancel(); - EXPECT_FALSE(ripple.hasActiveRipple()) << "Still no ripples after cancel"; -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/components/state_machine_test.cpp b/test/ui/components/state_machine_test.cpp deleted file mode 100644 index e6f3cb695..000000000 --- a/test/ui/components/state_machine_test.cpp +++ /dev/null @@ -1,185 +0,0 @@ -/** - * @file state_machine_test.cpp - * @brief Unit tests for StateMachine (Material Behavior Layer) - * - * Test Coverage: - * 1. State transitions and flags - * 2. Opacity calculation for each state - * 3. State priority handling - * 4. Signal emission - */ - -#include "ui/widget/material/base/state_machine.h" -#include - -using namespace cf::ui::widget::material::base; - -// ============================================================================= -// Test Suite 1: State Transitions -// ============================================================================= - -TEST(StateMachineTest, InitialState_IsNormal) { - // Create state machine with null factory (for testing without animation) - StateMachine sm(nullptr, nullptr); - EXPECT_EQ(sm.currentState(), StateMachine::State::StateNormal); - EXPECT_FALSE(sm.hasState(StateMachine::State::StateHovered)); - EXPECT_FALSE(sm.hasState(StateMachine::State::StatePressed)); -} - -TEST(StateMachineTest, OnHoverEnter_SetsHoveredState) { - StateMachine sm(nullptr, nullptr); - sm.onHoverEnter(); - EXPECT_TRUE(sm.hasState(StateMachine::State::StateHovered)); -} - -TEST(StateMachineTest, OnHoverLeave_ClearsHoveredState) { - StateMachine sm(nullptr, nullptr); - sm.onHoverEnter(); - sm.onHoverLeave(); - EXPECT_FALSE(sm.hasState(StateMachine::State::StateHovered)); -} - -TEST(StateMachineTest, OnPress_SetsPressedState) { - StateMachine sm(nullptr, nullptr); - sm.onPress(QPoint(10, 10)); - EXPECT_TRUE(sm.hasState(StateMachine::State::StatePressed)); -} - -TEST(StateMachineTest, OnRelease_ClearsPressedState) { - StateMachine sm(nullptr, nullptr); - sm.onPress(QPoint(10, 10)); - sm.onRelease(); - EXPECT_FALSE(sm.hasState(StateMachine::State::StatePressed)); -} - -TEST(StateMachineTest, OnFocusIn_SetsFocusedState) { - StateMachine sm(nullptr, nullptr); - sm.onFocusIn(); - EXPECT_TRUE(sm.hasState(StateMachine::State::StateFocused)); -} - -TEST(StateMachineTest, OnFocusOut_ClearsFocusedState) { - StateMachine sm(nullptr, nullptr); - sm.onFocusIn(); - sm.onFocusOut(); - EXPECT_FALSE(sm.hasState(StateMachine::State::StateFocused)); -} - -TEST(StateMachineTest, OnDisable_SetsDisabledState) { - StateMachine sm(nullptr, nullptr); - sm.onDisable(); - EXPECT_TRUE(sm.hasState(StateMachine::State::StateDisabled)); -} - -TEST(StateMachineTest, OnEnable_ClearsDisabledState) { - StateMachine sm(nullptr, nullptr); - sm.onDisable(); - sm.onEnable(); - EXPECT_FALSE(sm.hasState(StateMachine::State::StateDisabled)); -} - -TEST(StateMachineTest, OnCheckedChanged_SetsCheckedState) { - StateMachine sm(nullptr, nullptr); - sm.onCheckedChanged(true); - EXPECT_TRUE(sm.hasState(StateMachine::State::StateChecked)); -} - -TEST(StateMachineTest, OnCheckedChangedFalse_ClearsCheckedState) { - StateMachine sm(nullptr, nullptr); - sm.onCheckedChanged(true); - sm.onCheckedChanged(false); - EXPECT_FALSE(sm.hasState(StateMachine::State::StateChecked)); -} - -// ============================================================================= -// Test Suite 2: Combined States -// ============================================================================= - -TEST(StateMachineTest, HoverAndPressed_CanCoexist) { - StateMachine sm(nullptr, nullptr); - sm.onHoverEnter(); - sm.onPress(QPoint(10, 10)); - EXPECT_TRUE(sm.hasState(StateMachine::State::StateHovered)); - EXPECT_TRUE(sm.hasState(StateMachine::State::StatePressed)); -} - -TEST(StateMachineTest, FocusAndHover_CanCoexist) { - StateMachine sm(nullptr, nullptr); - sm.onFocusIn(); - sm.onHoverEnter(); - EXPECT_TRUE(sm.hasState(StateMachine::State::StateFocused)); - EXPECT_TRUE(sm.hasState(StateMachine::State::StateHovered)); -} - -// ============================================================================= -// Test Suite 3: Opacity Calculation -// ============================================================================= - -TEST(StateMachineTest, Opacity_NormalState_IsZero) { - StateMachine sm(nullptr, nullptr); - EXPECT_FLOAT_EQ(sm.stateLayerOpacity(), 0.0f); -} - -TEST(StateMachineTest, Opacity_HoveredState_Is008) { - StateMachine sm(nullptr, nullptr); - sm.onHoverEnter(); - // Without animation factory, opacity should be set directly - EXPECT_FLOAT_EQ(sm.stateLayerOpacity(), 0.08f); -} - -TEST(StateMachineTest, Opacity_PressedState_Is012) { - StateMachine sm(nullptr, nullptr); - sm.onPress(QPoint(10, 10)); - EXPECT_FLOAT_EQ(sm.stateLayerOpacity(), 0.12f); -} - -TEST(StateMachineTest, Opacity_FocusedState_Is012) { - StateMachine sm(nullptr, nullptr); - sm.onFocusIn(); - EXPECT_FLOAT_EQ(sm.stateLayerOpacity(), 0.12f); -} - -TEST(StateMachineTest, Opacity_CheckedState_Is008) { - StateMachine sm(nullptr, nullptr); - sm.onCheckedChanged(true); - EXPECT_FLOAT_EQ(sm.stateLayerOpacity(), 0.08f); -} - -TEST(StateMachineTest, Opacity_DisabledState_IsZero) { - StateMachine sm(nullptr, nullptr); - sm.onHoverEnter(); - sm.onDisable(); - // Disabled overrides all other states - EXPECT_FLOAT_EQ(sm.stateLayerOpacity(), 0.0f); -} - -// ============================================================================= -// Test Suite 4: State Priority -// ============================================================================= - -TEST(StateMachineTest, Priority_PressedOverHovered) { - StateMachine sm(nullptr, nullptr); - sm.onHoverEnter(); - sm.onPress(QPoint(10, 10)); - // Pressed has higher priority than hovered - EXPECT_FLOAT_EQ(sm.stateLayerOpacity(), 0.12f); // Pressed value -} - -TEST(StateMachineTest, Priority_DisabledOverAll) { - StateMachine sm(nullptr, nullptr); - sm.onHoverEnter(); - sm.onPress(QPoint(10, 10)); - sm.onFocusIn(); - sm.onDisable(); - // Disabled overrides everything - EXPECT_TRUE(sm.hasState(StateMachine::State::StateDisabled)); -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/core/CMakeLists.txt b/test/ui/core/CMakeLists.txt deleted file mode 100644 index f4cec64e4..000000000 --- a/test/ui/core/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -log_info("UI_Core_Tests" "Configured ui/core tests") diff --git a/test/ui/widget/CMakeLists.txt b/test/ui/widget/CMakeLists.txt deleted file mode 100644 index 5f21c6b4e..000000000 --- a/test/ui/widget/CMakeLists.txt +++ /dev/null @@ -1,221 +0,0 @@ -# ============================================================================= -# Widget Tests - Material Design 3 Widget Unit Tests -# ============================================================================= - -# Common link libraries for widget tests -# Note: Use cfui shared library to match __declspec(dllimport) in headers. -# No gtest_main — each test provides its own main() with QGuiApplication -set(WIDGET_TEST_LIBS - cfui;Qt6::Widgets;GTest::gtest -) - -# ============================================================================= -# Button -# ============================================================================= -add_gtest_executable( - TEST_NAME button_test - SOURCE_FILE button_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;button" - LOG_MODULE ui_widget_tests -) - -# ============================================================================= -# CheckBox -# ============================================================================= -add_gtest_executable( - TEST_NAME checkbox_test - SOURCE_FILE checkbox_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;checkbox" - LOG_MODULE ui_widget_tests -) - -# ============================================================================= -# RadioButton -# ============================================================================= -add_gtest_executable( - TEST_NAME radiobutton_test - SOURCE_FILE radiobutton_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;radiobutton" - LOG_MODULE ui_widget_tests -) - -# ============================================================================= -# Switch -# ============================================================================= -add_gtest_executable( - TEST_NAME switch_test - SOURCE_FILE switch_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;switch" - LOG_MODULE ui_widget_tests -) - -# ============================================================================= -# Slider -# ============================================================================= -add_gtest_executable( - TEST_NAME slider_test - SOURCE_FILE slider_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;slider" - LOG_MODULE ui_widget_tests -) - -# ============================================================================= -# TextField -# ============================================================================= -add_gtest_executable( - TEST_NAME textfield_test - SOURCE_FILE textfield_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;textfield" - LOG_MODULE ui_widget_tests -) - -# ============================================================================= -# TextArea -# ============================================================================= -add_gtest_executable( - TEST_NAME textarea_test - SOURCE_FILE textarea_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;textarea" - LOG_MODULE ui_widget_tests -) - -# ============================================================================= -# Label -# ============================================================================= -add_gtest_executable( - TEST_NAME label_test - SOURCE_FILE label_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;label" - LOG_MODULE ui_widget_tests -) - -# ============================================================================= -# ProgressBar -# ============================================================================= -add_gtest_executable( - TEST_NAME progressbar_test - SOURCE_FILE progressbar_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;progressbar" - LOG_MODULE ui_widget_tests -) - -# ============================================================================= -# ComboBox -# ============================================================================= -add_gtest_executable( - TEST_NAME combobox_test - SOURCE_FILE combobox_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;combobox" - LOG_MODULE ui_widget_tests -) - -# ============================================================================= -# GroupBox -# ============================================================================= -add_gtest_executable( - TEST_NAME groupbox_test - SOURCE_FILE groupbox_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;groupbox" - LOG_MODULE ui_widget_tests -) - -# ============================================================================= -# ListView -# ============================================================================= -add_gtest_executable( - TEST_NAME listview_test - SOURCE_FILE listview_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;listview" - LOG_MODULE ui_widget_tests -) - -# ============================================================================= -# TableView -# ============================================================================= -add_gtest_executable( - TEST_NAME tableview_test - SOURCE_FILE tableview_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;tableview" - LOG_MODULE ui_widget_tests -) - -# ============================================================================= -# TreeView -# ============================================================================= -add_gtest_executable( - TEST_NAME treeview_test - SOURCE_FILE treeview_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;treeview" - LOG_MODULE ui_widget_tests -) - -# ============================================================================= -# TabView -# ============================================================================= -add_gtest_executable( - TEST_NAME tabview_test - SOURCE_FILE tabview_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;tabview" - LOG_MODULE ui_widget_tests -) - -# ============================================================================= -# ScrollView -# ============================================================================= -add_gtest_executable( - TEST_NAME scrollview_test - SOURCE_FILE scrollview_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;scrollview" - LOG_MODULE ui_widget_tests -) - -# ============================================================================= -# Separator -# ============================================================================= -add_gtest_executable( - TEST_NAME separator_test - SOURCE_FILE separator_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;separator" - LOG_MODULE ui_widget_tests -) - -# ============================================================================= -# SpinBox -# ============================================================================= -add_gtest_executable( - TEST_NAME spinbox_test - SOURCE_FILE spinbox_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;spinbox" - LOG_MODULE ui_widget_tests -) - -# ============================================================================= -# DoubleSpinBox -# ============================================================================= -add_gtest_executable( - TEST_NAME doublespinbox_test - SOURCE_FILE doublespinbox_test.cpp - LINK_LIBRARIES ${WIDGET_TEST_LIBS} - LABELS "ui;widget;doublespinbox" - LOG_MODULE ui_widget_tests -) - -log_info("UI_Widget_Tests" "Configured ui/widget tests") diff --git a/test/ui/widget/button_test.cpp b/test/ui/widget/button_test.cpp deleted file mode 100644 index a41ca7c7b..000000000 --- a/test/ui/widget/button_test.cpp +++ /dev/null @@ -1,187 +0,0 @@ -/** - * @file button_test.cpp - * @brief Unit tests for Material Design 3 Button widget. - * - * Test Coverage: - * 1. Constructor variants and defaults - * 2. Property setters/getters (variant, elevation, icon, enabled, press effect, light source) - * 3. Size hints (sizeHint, minimumSizeHint) - * 4. Paint behavior across variants, enabled/disabled states, and with icons - */ - -#include "widget_test_helper.h" - -#include "ui/widget/material/widget/button/button.h" - -#include -#include -#include - -using namespace cf::ui::widget::material; - -// ============================================================================= -// Test Suite A: Constructors -// ============================================================================= - -TEST(ButtonTest, A01_DefaultConstructor_IsFilled) { - Button button; - EXPECT_EQ(button.variant(), Button::ButtonVariant::Filled); -} - -TEST(ButtonTest, A02_ConstructWithText) { - Button button("Hello"); - EXPECT_EQ(button.text(), "Hello"); -} - -TEST(ButtonTest, A03_ConstructWithVariant) { - Button tonalBtn("Tonal", Button::ButtonVariant::Tonal); - EXPECT_EQ(tonalBtn.variant(), Button::ButtonVariant::Tonal); - - Button outlinedBtn("Outlined", Button::ButtonVariant::Outlined); - EXPECT_EQ(outlinedBtn.variant(), Button::ButtonVariant::Outlined); - - Button textBtn("Text", Button::ButtonVariant::Text); - EXPECT_EQ(textBtn.variant(), Button::ButtonVariant::Text); - - Button elevatedBtn("Elevated", Button::ButtonVariant::Elevated); - EXPECT_EQ(elevatedBtn.variant(), Button::ButtonVariant::Elevated); -} - -// ============================================================================= -// Test Suite B: Property Setters / Getters -// ============================================================================= - -TEST(ButtonTest, B01_SetVariant) { - Button button; - - button.setVariant(Button::ButtonVariant::Filled); - EXPECT_EQ(button.variant(), Button::ButtonVariant::Filled); - - button.setVariant(Button::ButtonVariant::Tonal); - EXPECT_EQ(button.variant(), Button::ButtonVariant::Tonal); - - button.setVariant(Button::ButtonVariant::Outlined); - EXPECT_EQ(button.variant(), Button::ButtonVariant::Outlined); - - button.setVariant(Button::ButtonVariant::Text); - EXPECT_EQ(button.variant(), Button::ButtonVariant::Text); - - button.setVariant(Button::ButtonVariant::Elevated); - EXPECT_EQ(button.variant(), Button::ButtonVariant::Elevated); -} - -TEST(ButtonTest, B02_SetElevation) { - Button button; - button.setElevation(3); - EXPECT_EQ(button.elevation(), 3); - - button.setElevation(0); - EXPECT_EQ(button.elevation(), 0); - - button.setElevation(5); - EXPECT_EQ(button.elevation(), 5); -} - -TEST(ButtonTest, B03_SetLeadingIcon) { - Button button; - EXPECT_NO_FATAL_FAILURE(button.setLeadingIcon(QIcon())); -} - -TEST(ButtonTest, B04_EnableDisable) { - Button button; - EXPECT_TRUE(button.isEnabled()); - - button.setEnabled(false); - EXPECT_FALSE(button.isEnabled()); - - button.setEnabled(true); - EXPECT_TRUE(button.isEnabled()); -} - -TEST(ButtonTest, B05_PressEffectToggle) { - Button button; - EXPECT_TRUE(button.pressEffectEnabled()); - - button.setPressEffectEnabled(false); - EXPECT_FALSE(button.pressEffectEnabled()); - - button.setPressEffectEnabled(true); - EXPECT_TRUE(button.pressEffectEnabled()); -} - -TEST(ButtonTest, B06_SetLightSourceAngle) { - Button button; - button.setLightSourceAngle(45.0f); - EXPECT_FLOAT_EQ(button.lightSourceAngle(), 45.0f); - - button.setLightSourceAngle(-30.0f); - EXPECT_FLOAT_EQ(button.lightSourceAngle(), -30.0f); - - button.setLightSourceAngle(0.0f); - EXPECT_FLOAT_EQ(button.lightSourceAngle(), 0.0f); -} - -// ============================================================================= -// Test Suite C: Size Hints -// ============================================================================= - -TEST(ButtonTest, C01_SizeHint_NotZero) { - Button button("Click Me"); - EXPECT_GT(button.sizeHint().width(), 0); - EXPECT_GT(button.sizeHint().height(), 0); -} - -TEST(ButtonTest, C02_MinimumSizeHint_NotZero) { - Button button("Click Me"); - EXPECT_GT(button.minimumSizeHint().width(), 0); - EXPECT_GT(button.minimumSizeHint().height(), 0); -} - -// ============================================================================= -// Test Suite D: Paint Behavior -// ============================================================================= - -TEST(ButtonTest, D01_Paint_AllVariants_NoCrash) { - const Button::ButtonVariant variants[] = { - Button::ButtonVariant::Filled, Button::ButtonVariant::Tonal, - Button::ButtonVariant::Outlined, Button::ButtonVariant::Text, - Button::ButtonVariant::Elevated}; - - for (auto v : variants) { - Button btn("Test", v); - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&btn, QSize(200, 48))); - } -} - -TEST(ButtonTest, D02_Paint_EnabledDisabled_DifferentPixels) { - Button enabledBtn("Test"); - enabledBtn.setEnabled(true); - - Button disabledBtn("Test"); - disabledBtn.setEnabled(false); - - QImage enabledImage = widget_test::renderWidgetToImage(&enabledBtn, QSize(200, 48)); - QImage disabledImage = widget_test::renderWidgetToImage(&disabledBtn, QSize(200, 48)); - - EXPECT_TRUE(widget_test::imagesDiffer(enabledImage, disabledImage)) - << "Enabled and disabled buttons should produce different visual output"; -} - -TEST(ButtonTest, D03_Paint_WithIcon_NoCrash) { - Button button("Icon Button"); - button.setIcon(QIcon()); - - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&button, QSize(200, 48))); -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/checkbox_test.cpp b/test/ui/widget/checkbox_test.cpp deleted file mode 100644 index f9f389207..000000000 --- a/test/ui/widget/checkbox_test.cpp +++ /dev/null @@ -1,152 +0,0 @@ -/** - * @file checkbox_test.cpp - * @brief Unit tests for Material Design 3 CheckBox widget. - * - * Test Coverage: - * 1. Constructor variants and defaults - * 2. Property setters/getters (checked, check state, error, enabled) - * 3. Size hints (sizeHint, minimumSizeHint) - * 4. Paint behavior across checked/unchecked, indeterminate, and error states - */ - -#include "widget_test_helper.h" - -#include "ui/widget/material/widget/checkbox/checkbox.h" - -#include -#include - -using namespace cf::ui::widget::material; - -// ============================================================================= -// Test Suite A: Constructors -// ============================================================================= - -TEST(CheckBoxTest, A01_DefaultConstructor_Unchecked) { - CheckBox cb; - EXPECT_FALSE(cb.isChecked()); -} - -TEST(CheckBoxTest, A02_ConstructWithText) { - CheckBox cb("Label"); - EXPECT_EQ(cb.text(), "Label"); -} - -// ============================================================================= -// Test Suite B: Property Setters / Getters -// ============================================================================= - -TEST(CheckBoxTest, B01_SetChecked_TrueFalse) { - CheckBox cb; - EXPECT_FALSE(cb.isChecked()); - - cb.setChecked(true); - EXPECT_TRUE(cb.isChecked()); - - cb.setChecked(false); - EXPECT_FALSE(cb.isChecked()); -} - -TEST(CheckBoxTest, B02_SetCheckState_ThreeStates) { - CheckBox cb; - - cb.setCheckState(Qt::Unchecked); - EXPECT_EQ(cb.checkState(), Qt::Unchecked); - - cb.setCheckState(Qt::PartiallyChecked); - EXPECT_EQ(cb.checkState(), Qt::PartiallyChecked); - - cb.setCheckState(Qt::Checked); - EXPECT_EQ(cb.checkState(), Qt::Checked); -} - -TEST(CheckBoxTest, B03_SetError) { - CheckBox cb; - EXPECT_FALSE(cb.hasError()); - - cb.setError(true); - EXPECT_TRUE(cb.hasError()); - - cb.setError(false); - EXPECT_FALSE(cb.hasError()); -} - -TEST(CheckBoxTest, B04_EnableDisable) { - CheckBox cb; - EXPECT_TRUE(cb.isEnabled()); - - cb.setEnabled(false); - EXPECT_FALSE(cb.isEnabled()); - - cb.setEnabled(true); - EXPECT_TRUE(cb.isEnabled()); -} - -// ============================================================================= -// Test Suite C: Size Hints -// ============================================================================= - -TEST(CheckBoxTest, C01_SizeHint_NotZero) { - CheckBox cb("Option"); - EXPECT_GT(cb.sizeHint().width(), 0); - EXPECT_GT(cb.sizeHint().height(), 0); -} - -TEST(CheckBoxTest, C02_MinimumSizeHint_NotZero) { - CheckBox cb("Option"); - EXPECT_GT(cb.minimumSizeHint().width(), 0); - EXPECT_GT(cb.minimumSizeHint().height(), 0); -} - -// ============================================================================= -// Test Suite D: Paint Behavior -// ============================================================================= - -TEST(CheckBoxTest, D01_Paint_CheckedUnchecked_DifferentPixels) { - CheckBox uncheckedCb("Option"); - uncheckedCb.setChecked(false); - - CheckBox checkedCb("Option"); - checkedCb.setChecked(true); - - QImage uncheckedImage = widget_test::renderWidgetToImage(&uncheckedCb, QSize(200, 48)); - QImage checkedImage = widget_test::renderWidgetToImage(&checkedCb, QSize(200, 48)); - - EXPECT_TRUE(widget_test::imagesDiffer(uncheckedImage, checkedImage)) - << "Checked and unchecked checkboxes should produce different visual " - "output"; -} - -TEST(CheckBoxTest, D02_Paint_Indeterminate_NoCrash) { - CheckBox cb("Maybe"); - cb.setCheckState(Qt::PartiallyChecked); - - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&cb, QSize(200, 48))); -} - -TEST(CheckBoxTest, D03_Paint_ErrorState_DifferentPixels) { - CheckBox normalCb("Field"); - normalCb.setError(false); - - CheckBox errorCb("Field"); - errorCb.setError(true); - - QImage normalImage = widget_test::renderWidgetToImage(&normalCb, QSize(200, 48)); - QImage errorImage = widget_test::renderWidgetToImage(&errorCb, QSize(200, 48)); - - EXPECT_TRUE(widget_test::imagesDiffer(normalImage, errorImage)) - << "Error and normal state checkboxes should produce different visual " - "output"; -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/combobox_test.cpp b/test/ui/widget/combobox_test.cpp deleted file mode 100644 index 5cb0acddf..000000000 --- a/test/ui/widget/combobox_test.cpp +++ /dev/null @@ -1,118 +0,0 @@ -/** - * @file combobox_test.cpp - * @brief Unit tests for Material Design 3 ComboBox widget. - */ - -#include "widget_test_helper.h" - -#include -#include - -#include "ui/widget/material/widget/comboBox/combobox.h" - -using namespace cf::ui::widget::material; - -// --- A: Construction --- - -TEST(ComboBoxTest, A01_DefaultConstructor) { - ComboBox cb; - EXPECT_NO_FATAL_FAILURE(cb.count()); - EXPECT_TRUE(cb.isEnabled()); -} - -// --- B: Property setters --- - -TEST(ComboBoxTest, B01_AddItems) { - ComboBox cb; - cb.addItems({"A", "B", "C"}); - EXPECT_EQ(cb.count(), 3); -} - -TEST(ComboBoxTest, B02_SetCurrentIndex) { - ComboBox cb; - cb.addItems({"A", "B", "C"}); - cb.setCurrentIndex(1); - EXPECT_EQ(cb.currentIndex(), 1); -} - -TEST(ComboBoxTest, B03_EnableDisable) { - ComboBox cb; - cb.setEnabled(true); - EXPECT_TRUE(cb.isEnabled()); - - cb.setEnabled(false); - EXPECT_FALSE(cb.isEnabled()); -} - -TEST(ComboBoxTest, B04_SetVariant) { - ComboBox cb; - cb.setVariant(ComboBox::ComboBoxVariant::Filled); - EXPECT_EQ(cb.variant(), ComboBox::ComboBoxVariant::Filled); - - cb.setVariant(ComboBox::ComboBoxVariant::Outlined); - EXPECT_EQ(cb.variant(), ComboBox::ComboBoxVariant::Outlined); -} - -// --- C: Size hints --- - -TEST(ComboBoxTest, C01_SizeHint_NotZero) { - ComboBox cb; - cb.addItems({"A", "B", "C"}); - QSize hint = cb.sizeHint(); - EXPECT_FALSE(hint.isNull()); -} - -TEST(ComboBoxTest, C02_MinimumSizeHint_NotZero) { - ComboBox cb; - cb.addItems({"A", "B", "C"}); - QSize hint = cb.minimumSizeHint(); - EXPECT_GT(hint.width(), 0); - EXPECT_GT(hint.height(), 0); -} - -// --- D: Paint --- - -TEST(ComboBoxTest, D01_Paint_WithItems_NoCrash) { - ComboBox cb; - cb.addItems({"A", "B", "C"}); - cb.setCurrentIndex(0); - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&cb, QSize(200, 48))); -} - -TEST(ComboBoxTest, D02_Paint_Variant_DifferentPixels) { - ComboBox filledCb; - filledCb.addItems({"A", "B", "C"}); - filledCb.setVariant(ComboBox::ComboBoxVariant::Filled); - filledCb.setCurrentIndex(0); - QImage filledImage = widget_test::renderWidgetToImage(&filledCb, QSize(200, 48)); - - ComboBox outlinedCb; - outlinedCb.addItems({"A", "B", "C"}); - outlinedCb.setVariant(ComboBox::ComboBoxVariant::Outlined); - outlinedCb.setCurrentIndex(0); - QImage outlinedImage = widget_test::renderWidgetToImage(&outlinedCb, QSize(200, 48)); - - EXPECT_TRUE(widget_test::imagesDiffer(filledImage, outlinedImage)) - << "Filled and Outlined ComboBox variants should produce different visual output"; -} - -TEST(ComboBoxTest, D03_Paint_DifferentIndex_DifferentPixels) { - ComboBox cb; - cb.addItems({"Short", "A Much Longer Item Name", "C"}); - cb.setCurrentIndex(0); - QImage img0 = widget_test::renderWidgetToImage(&cb, QSize(200, 48)); - - cb.setCurrentIndex(1); - QImage img1 = widget_test::renderWidgetToImage(&cb, QSize(200, 48)); - - EXPECT_TRUE(widget_test::imagesDiffer(img0, img1)) - << "ComboBox with different selected items should produce different visual output"; -} - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/doublespinbox_test.cpp b/test/ui/widget/doublespinbox_test.cpp deleted file mode 100644 index aa7daa33b..000000000 --- a/test/ui/widget/doublespinbox_test.cpp +++ /dev/null @@ -1,106 +0,0 @@ -/** - * @file doublespinbox_test.cpp - * @brief Unit tests for Material Design 3 DoubleSpinBox widget. - */ - -#include "widget_test_helper.h" - -#include -#include - -#include "ui/widget/material/widget/doublespinbox/doublespinbox.h" - -using namespace cf::ui::widget::material; - -// --- A: Construction --- - -TEST(DoubleSpinBoxTest, A01_DefaultConstructor) { - DoubleSpinBox dsb; - EXPECT_NO_FATAL_FAILURE(dsb.value()); - EXPECT_TRUE(dsb.isEnabled()); -} - -// --- B: Property setters --- - -TEST(DoubleSpinBoxTest, B01_SetRange) { - DoubleSpinBox dsb; - dsb.setRange(0.0, 10.0); - EXPECT_DOUBLE_EQ(dsb.minimum(), 0.0); - EXPECT_DOUBLE_EQ(dsb.maximum(), 10.0); -} - -TEST(DoubleSpinBoxTest, B02_SetValue) { - DoubleSpinBox dsb; - dsb.setValue(3.14); - EXPECT_DOUBLE_EQ(dsb.value(), 3.14); -} - -TEST(DoubleSpinBoxTest, B03_SetDecimals) { - DoubleSpinBox dsb; - dsb.setDecimals(2); - EXPECT_EQ(dsb.decimals(), 2); -} - -TEST(DoubleSpinBoxTest, B04_EnableDisable) { - DoubleSpinBox dsb; - dsb.setEnabled(true); - EXPECT_TRUE(dsb.isEnabled()); - - dsb.setEnabled(false); - EXPECT_FALSE(dsb.isEnabled()); -} - -TEST(DoubleSpinBoxTest, B05_SetPrefixSuffix) { - DoubleSpinBox dsb; - dsb.setPrefix("$"); - dsb.setSuffix(" kg"); - EXPECT_EQ(dsb.prefix(), "$"); - EXPECT_EQ(dsb.suffix(), " kg"); -} - -// --- C: Size hints --- - -TEST(DoubleSpinBoxTest, C01_SizeHint_NotZero) { - DoubleSpinBox dsb; - QSize hint = dsb.sizeHint(); - EXPECT_FALSE(hint.isNull()); -} - -TEST(DoubleSpinBoxTest, C02_MinimumSizeHint_NotZero) { - DoubleSpinBox dsb; - dsb.setValue(3.14); - EXPECT_GT(dsb.minimumSizeHint().width(), 0); - EXPECT_GT(dsb.minimumSizeHint().height(), 0); -} - -// --- D: Paint --- - -TEST(DoubleSpinBoxTest, D01_Paint_WithValue_NoCrash) { - DoubleSpinBox dsb; - dsb.setRange(0.0, 10.0); - dsb.setValue(3.14); - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&dsb, QSize(120, 48))); -} - -TEST(DoubleSpinBoxTest, D02_Paint_DifferentValues_DifferentPixels) { - DoubleSpinBox dsbLow; - dsbLow.setRange(0.0, 10.0); - dsbLow.setValue(0.0); - QImage imgLow = widget_test::renderWidgetToImage(&dsbLow, QSize(120, 48)); - - DoubleSpinBox dsbHigh; - dsbHigh.setRange(0.0, 10.0); - dsbHigh.setValue(9.99); - QImage imgHigh = widget_test::renderWidgetToImage(&dsbHigh, QSize(120, 48)); - - EXPECT_TRUE(widget_test::imagesDiffer(imgLow, imgHigh)) - << "DoubleSpinBox at value=0 and value=9.99 should produce different visual output"; -} - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/groupbox_test.cpp b/test/ui/widget/groupbox_test.cpp deleted file mode 100644 index a4a562c11..000000000 --- a/test/ui/widget/groupbox_test.cpp +++ /dev/null @@ -1,129 +0,0 @@ -/** - * @file groupbox_test.cpp - * @brief Unit tests for Material Design 3 GroupBox widget. - */ - -#include "widget_test_helper.h" - -#include -#include - -#include "ui/widget/material/widget/groupbox/groupbox.h" - -using namespace cf::ui::widget::material; - -// --- A: Construction --- - -TEST(GroupBoxTest, A01_DefaultConstructor) { - GroupBox gb; - EXPECT_NO_FATAL_FAILURE(gb.title()); - EXPECT_TRUE(gb.isEnabled()); -} - -TEST(GroupBoxTest, A02_ConstructWithTitle) { - GroupBox gb("Title"); - EXPECT_EQ(gb.title(), "Title"); -} - -// --- B: Property setters --- - -TEST(GroupBoxTest, B01_SetTitle) { - GroupBox gb; - gb.setTitle("New Title"); - EXPECT_EQ(gb.title(), "New Title"); -} - -TEST(GroupBoxTest, B02_EnableDisable) { - GroupBox gb; - gb.setEnabled(true); - EXPECT_TRUE(gb.isEnabled()); - - gb.setEnabled(false); - EXPECT_FALSE(gb.isEnabled()); -} - -TEST(GroupBoxTest, B03_SetElevation) { - GroupBox gb; - gb.setElevation(0); - EXPECT_EQ(gb.elevation(), 0); - - gb.setElevation(3); - EXPECT_EQ(gb.elevation(), 3); - - gb.setElevation(5); - EXPECT_EQ(gb.elevation(), 5); -} - -TEST(GroupBoxTest, B04_SetCornerRadius) { - GroupBox gb; - gb.setCornerRadius(4.0f); - EXPECT_FLOAT_EQ(gb.cornerRadius(), 4.0f); - - gb.setCornerRadius(16.0f); - EXPECT_FLOAT_EQ(gb.cornerRadius(), 16.0f); -} - -TEST(GroupBoxTest, B05_SetHasBorder) { - GroupBox gb; - gb.setHasBorder(true); - EXPECT_TRUE(gb.hasBorder()); - - gb.setHasBorder(false); - EXPECT_FALSE(gb.hasBorder()); -} - -// --- C: Size hints --- - -TEST(GroupBoxTest, C01_SizeHint_NotZero) { - GroupBox gb; - gb.setTitle("Group"); - QSize hint = gb.sizeHint(); - EXPECT_FALSE(hint.isNull()); -} - -TEST(GroupBoxTest, C02_MinimumSizeHint_NotZero) { - GroupBox gb; - gb.setTitle("Group"); - EXPECT_GT(gb.minimumSizeHint().width(), 0); - EXPECT_GT(gb.minimumSizeHint().height(), 0); -} - -// --- D: Paint --- - -TEST(GroupBoxTest, D01_Paint_WithTitle_NoCrash) { - GroupBox gb; - gb.setTitle("My Group"); - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&gb, QSize(200, 100))); -} - -TEST(GroupBoxTest, D02_Paint_WithWithoutTitle_DifferentPixels) { - GroupBox untitledGb; - QImage untitledImage = widget_test::renderWidgetToImage(&untitledGb, QSize(200, 100)); - - GroupBox titledGb("My Group"); - QImage titledImage = widget_test::renderWidgetToImage(&titledGb, QSize(200, 100)); - - EXPECT_TRUE(widget_test::imagesDiffer(untitledImage, titledImage)) - << "GroupBox with and without title should produce different visual output"; -} - -TEST(GroupBoxTest, D03_Paint_Elevation_DifferentPixels) { - GroupBox lowGb("Shadow"); - lowGb.setElevation(0); - QImage lowImage = widget_test::renderWidgetToImage(&lowGb, QSize(200, 100)); - - GroupBox highGb("Shadow"); - highGb.setElevation(4); - QImage highImage = widget_test::renderWidgetToImage(&highGb, QSize(200, 100)); - - EXPECT_TRUE(widget_test::imagesDiffer(lowImage, highImage)) - << "GroupBox with different elevations should produce different visual output"; -} - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/label_test.cpp b/test/ui/widget/label_test.cpp deleted file mode 100644 index f5b97031c..000000000 --- a/test/ui/widget/label_test.cpp +++ /dev/null @@ -1,170 +0,0 @@ -/** - * @file label_test.cpp - * @brief Unit tests for Label (Material Widget Layer) - * - * Test Coverage: - * 1. Default and text constructors - * 2. Set text - * 3. Enable/disable behavior - * 4. SizeHint non-zero (with text set) - * 5. Paint with text produces visible pixels - * 6. Different text produces different sizeHint values - */ - -#include "ui/widget/material/widget/label/label.h" -#include "widget_test_helper.h" -#include -#include - -using namespace cf::ui::widget::material; - -// ============================================================================= -// Test Suite A: Construction -// ============================================================================= - -TEST(LabelTest, A01_DefaultConstructor) { - Label label; - EXPECT_TRUE(label.text().isEmpty()); -} - -TEST(LabelTest, A02_ConstructWithText) { - Label label("Hello"); - EXPECT_EQ(label.text(), "Hello"); -} - -// ============================================================================= -// Test Suite B: State Changes -// ============================================================================= - -TEST(LabelTest, B01_SetText) { - Label label; - label.setText("World"); - EXPECT_EQ(label.text(), "World"); -} - -TEST(LabelTest, B02_EnableDisable) { - Label label("Test"); - EXPECT_TRUE(label.isEnabled()); - - label.setEnabled(false); - EXPECT_FALSE(label.isEnabled()); - - label.setEnabled(true); - EXPECT_TRUE(label.isEnabled()); -} - -TEST(LabelTest, B03_SetTypographyStyle) { - Label label; - label.setTypographyStyle(TypographyStyle::DisplayLarge); - EXPECT_EQ(label.typographyStyle(), TypographyStyle::DisplayLarge); - - label.setTypographyStyle(TypographyStyle::HeadlineMedium); - EXPECT_EQ(label.typographyStyle(), TypographyStyle::HeadlineMedium); - - label.setTypographyStyle(TypographyStyle::TitleSmall); - EXPECT_EQ(label.typographyStyle(), TypographyStyle::TitleSmall); - - label.setTypographyStyle(TypographyStyle::BodyLarge); - EXPECT_EQ(label.typographyStyle(), TypographyStyle::BodyLarge); - - label.setTypographyStyle(TypographyStyle::LabelSmall); - EXPECT_EQ(label.typographyStyle(), TypographyStyle::LabelSmall); -} - -TEST(LabelTest, B04_SetColorVariant) { - Label label("Text"); - label.setColorVariant(LabelColorVariant::Primary); - EXPECT_EQ(label.colorVariant(), LabelColorVariant::Primary); - - label.setColorVariant(LabelColorVariant::OnSurface); - EXPECT_EQ(label.colorVariant(), LabelColorVariant::OnSurface); - - label.setColorVariant(LabelColorVariant::Error); - EXPECT_EQ(label.colorVariant(), LabelColorVariant::Error); - - label.setColorVariant(LabelColorVariant::InverseSurface); - EXPECT_EQ(label.colorVariant(), LabelColorVariant::InverseSurface); -} - -TEST(LabelTest, B05_SetAutoHiding) { - Label label("Text"); - EXPECT_FALSE(label.autoHiding()); - - label.setAutoHiding(true); - EXPECT_TRUE(label.autoHiding()); - - label.setAutoHiding(false); - EXPECT_FALSE(label.autoHiding()); -} - -// ============================================================================= -// Test Suite C: Size Hints -// ============================================================================= - -TEST(LabelTest, C01_SizeHint_NotZero) { - Label label("Sample Text"); - EXPECT_GT(label.sizeHint().width(), 0); - EXPECT_GT(label.sizeHint().height(), 0); -} - -TEST(LabelTest, C02_MinimumSizeHint_NotZero) { - Label label("Sample"); - EXPECT_GT(label.minimumSizeHint().width(), 0); - EXPECT_GT(label.minimumSizeHint().height(), 0); -} - -// ============================================================================= -// Test Suite D: Paint Verification -// ============================================================================= - -TEST(LabelTest, D01_Paint_WithText_NoCrash) { - Label label("Rendered Text"); - widget_test::verifyPaintsPixels(&label, QSize(200, 48)); -} - -TEST(LabelTest, D02_Paint_DifferentText_DifferentSize) { - Label shortLabel("Hi"); - QSize shortSize = shortLabel.sizeHint(); - - Label longLabel("This is a much longer text string for size comparison"); - QSize longSize = longLabel.sizeHint(); - - EXPECT_NE(shortSize.width(), longSize.width()) - << "Short and long text labels should have different sizeHint widths"; -} - -TEST(LabelTest, D03_Paint_TypographyStyles_NoCrash) { - const TypographyStyle styles[] = {TypographyStyle::DisplayLarge, - TypographyStyle::HeadlineMedium, TypographyStyle::TitleSmall, - TypographyStyle::BodyMedium, TypographyStyle::LabelLarge}; - for (auto style : styles) { - Label label("Test Text"); - label.setTypographyStyle(style); - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&label, QSize(200, 48))); - } -} - -TEST(LabelTest, D04_Paint_DifferentTypography_DifferentSize) { - Label displayLabel("Text"); - displayLabel.setTypographyStyle(TypographyStyle::DisplayLarge); - QSize displaySize = displayLabel.sizeHint(); - - Label bodyLabel("Text"); - bodyLabel.setTypographyStyle(TypographyStyle::BodySmall); - QSize bodySize = bodyLabel.sizeHint(); - - EXPECT_NE(displaySize.height(), bodySize.height()) - << "Display and Body typography should have different heights"; -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/listview_test.cpp b/test/ui/widget/listview_test.cpp deleted file mode 100644 index 02104546a..000000000 --- a/test/ui/widget/listview_test.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "widget_test_helper.h" - -#include -#include -#include - -#include "ui/widget/material/widget/listview/listview.h" - -using namespace cf::ui::widget::material; - -TEST(ListView, A01_DefaultConstructor) { - ListView lv; - EXPECT_TRUE(lv.isEnabled()); -} - -TEST(ListView, B01_SetModel) { - ListView lv; - QStringListModel model(QStringList{"A", "B", "C"}); - lv.setModel(&model); - EXPECT_EQ(lv.model(), &model); -} - -TEST(ListView, B02_EnableDisable) { - ListView lv; - lv.setEnabled(true); - EXPECT_TRUE(lv.isEnabled()); - - lv.setEnabled(false); - EXPECT_FALSE(lv.isEnabled()); - - lv.setEnabled(true); - EXPECT_TRUE(lv.isEnabled()); -} - -TEST(ListView, B03_SetCurrentIndex) { - ListView lv; - QStringListModel model(QStringList{"A", "B", "C"}); - lv.setModel(&model); - lv.setCurrentIndex(model.index(1, 0)); - EXPECT_EQ(lv.currentIndex().row(), 1); -} - -TEST(ListView, C01_SizeHint_Valid) { - ListView lv; - QSize hint = lv.sizeHint(); - EXPECT_GT(hint.width(), 0); - EXPECT_GT(hint.height(), 0); -} - -TEST(ListView, D01_Paint_WithModel_NoCrash) { - ListView lv; - QStringListModel model(QStringList{"A", "B", "C"}); - lv.setModel(&model); - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&lv, QSize(200, 200))); -} - -TEST(ListView, D02_Paint_WithItems_PaintsPixels) { - ListView lv; - QStringListModel model(QStringList{"Item1", "Item2", "Item3"}); - lv.setModel(&model); - widget_test::verifyPaintsPixels(&lv, QSize(200, 200)); -} - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/progressbar_test.cpp b/test/ui/widget/progressbar_test.cpp deleted file mode 100644 index 48afac5f2..000000000 --- a/test/ui/widget/progressbar_test.cpp +++ /dev/null @@ -1,123 +0,0 @@ -/** - * @file progressbar_test.cpp - * @brief Unit tests for ProgressBar (Material Widget Layer) - * - * Test Coverage: - * 1. Default constructor - * 2. Set range - * 3. Set value - * 4. Enable/disable behavior - * 5. SizeHint non-zero - * 6. Paint output differs between progress values - */ - -#include "ui/widget/material/widget/progressbar/progressbar.h" -#include "widget_test_helper.h" -#include -#include - -using namespace cf::ui::widget::material; - -// ============================================================================= -// Test Suite A: Construction -// ============================================================================= - -TEST(ProgressBarTest, A01_DefaultConstructor) { - ProgressBar pb; - // Verify constructed without crash - EXPECT_TRUE(pb.isEnabled()); -} - -// ============================================================================= -// Test Suite B: State Changes -// ============================================================================= - -TEST(ProgressBarTest, B01_SetRange) { - ProgressBar pb; - pb.setRange(0, 100); - EXPECT_EQ(pb.minimum(), 0); - EXPECT_EQ(pb.maximum(), 100); -} - -TEST(ProgressBarTest, B02_SetValue) { - ProgressBar pb; - pb.setRange(0, 100); - pb.setValue(50); - EXPECT_EQ(pb.value(), 50); -} - -TEST(ProgressBarTest, B03_EnableDisable) { - ProgressBar pb; - EXPECT_TRUE(pb.isEnabled()); - - pb.setEnabled(false); - EXPECT_FALSE(pb.isEnabled()); - - pb.setEnabled(true); - EXPECT_TRUE(pb.isEnabled()); -} - -// ============================================================================= -// Test Suite C: Size Hints -// ============================================================================= - -TEST(ProgressBarTest, C01_SizeHint_NotZero) { - ProgressBar pb; - EXPECT_GT(pb.sizeHint().width(), 0); - EXPECT_GT(pb.sizeHint().height(), 0); -} - -TEST(ProgressBarTest, C02_MinimumSizeHint_NotZero) { - ProgressBar pb; - EXPECT_GT(pb.minimumSizeHint().width(), 0); - EXPECT_GT(pb.minimumSizeHint().height(), 0); -} - -// ============================================================================= -// Test Suite D: Paint Verification -// ============================================================================= - -TEST(ProgressBarTest, D01_Paint_DifferentValues_DifferentPixels) { - ProgressBar pbLow; - pbLow.setRange(0, 100); - pbLow.setValue(0); - QImage imgLow = widget_test::renderWidgetToImage(&pbLow, QSize(200, 48)); - - ProgressBar pbHigh; - pbHigh.setRange(0, 100); - pbHigh.setValue(100); - QImage imgHigh = widget_test::renderWidgetToImage(&pbHigh, QSize(200, 48)); - - EXPECT_TRUE(widget_test::imagesDiffer(imgLow, imgHigh)) - << "ProgressBar at value=0 and value=100 should produce different visual output"; -} - -TEST(ProgressBarTest, D02_Paint_EnabledDisabled_DifferentPixels) { - ProgressBar enabledPb; - enabledPb.setRange(0, 100); - enabledPb.setValue(50); - enabledPb.setEnabled(true); - - ProgressBar disabledPb; - disabledPb.setRange(0, 100); - disabledPb.setValue(50); - disabledPb.setEnabled(false); - - QImage enabledImage = widget_test::renderWidgetToImage(&enabledPb, QSize(200, 48)); - QImage disabledImage = widget_test::renderWidgetToImage(&disabledPb, QSize(200, 48)); - - EXPECT_TRUE(widget_test::imagesDiffer(enabledImage, disabledImage)) - << "Enabled and disabled ProgressBar should produce different visual output"; -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/radiobutton_test.cpp b/test/ui/widget/radiobutton_test.cpp deleted file mode 100644 index 55fa315e2..000000000 --- a/test/ui/widget/radiobutton_test.cpp +++ /dev/null @@ -1,135 +0,0 @@ -/** - * @file radiobutton_test.cpp - * @brief Unit tests for RadioButton (Material Widget Layer) - * - * Test Coverage: - * 1. Default and text constructors - * 2. Checked state toggling - * 3. Enable/disable behavior - * 4. SizeHint non-zero - * 5. Paint output differs between checked and unchecked states - */ - -#include "ui/widget/material/widget/radiobutton/radiobutton.h" -#include "widget_test_helper.h" -#include -#include - -using namespace cf::ui::widget::material; - -// ============================================================================= -// Test Suite A: Construction -// ============================================================================= - -TEST(RadioButtonTest, A01_DefaultConstructor) { - RadioButton rb; - EXPECT_FALSE(rb.isChecked()); -} - -TEST(RadioButtonTest, A02_ConstructWithText) { - RadioButton rb("Option"); - EXPECT_EQ(rb.text(), "Option"); -} - -// ============================================================================= -// Test Suite B: State Changes -// ============================================================================= - -TEST(RadioButtonTest, B01_SetChecked) { - RadioButton rb; - EXPECT_FALSE(rb.isChecked()); - - rb.setChecked(true); - EXPECT_TRUE(rb.isChecked()); -} - -TEST(RadioButtonTest, B02_EnableDisable) { - RadioButton rb; - EXPECT_TRUE(rb.isEnabled()); - - rb.setEnabled(false); - EXPECT_FALSE(rb.isEnabled()); - - rb.setEnabled(true); - EXPECT_TRUE(rb.isEnabled()); -} - -TEST(RadioButtonTest, B03_SetError) { - RadioButton rb; - EXPECT_FALSE(rb.hasError()); - - rb.setError(true); - EXPECT_TRUE(rb.hasError()); - - rb.setError(false); - EXPECT_FALSE(rb.hasError()); -} - -TEST(RadioButtonTest, B04_PressEffectToggle) { - RadioButton rb; - EXPECT_TRUE(rb.pressEffectEnabled()); - - rb.setPressEffectEnabled(false); - EXPECT_FALSE(rb.pressEffectEnabled()); - - rb.setPressEffectEnabled(true); - EXPECT_TRUE(rb.pressEffectEnabled()); -} - -// ============================================================================= -// Test Suite C: Size Hints -// ============================================================================= - -TEST(RadioButtonTest, C01_SizeHint_NotZero) { - RadioButton rb; - EXPECT_GT(rb.sizeHint().width(), 0); - EXPECT_GT(rb.sizeHint().height(), 0); -} - -TEST(RadioButtonTest, C02_MinimumSizeHint_NotZero) { - RadioButton rb("Option"); - EXPECT_GT(rb.minimumSizeHint().width(), 0); - EXPECT_GT(rb.minimumSizeHint().height(), 0); -} - -// ============================================================================= -// Test Suite D: Paint Verification -// ============================================================================= - -TEST(RadioButtonTest, D01_Paint_CheckedUnchecked_DifferentPixels) { - RadioButton rbUnchecked; - QImage imgUnchecked = widget_test::renderWidgetToImage(&rbUnchecked, QSize(200, 48)); - - RadioButton rbChecked; - rbChecked.setChecked(true); - QImage imgChecked = widget_test::renderWidgetToImage(&rbChecked, QSize(200, 48)); - - EXPECT_TRUE(widget_test::imagesDiffer(imgUnchecked, imgChecked)) - << "Checked and unchecked RadioButton should produce different visual output"; -} - -TEST(RadioButtonTest, D02_Paint_ErrorState_DifferentPixels) { - RadioButton normalRb("Option"); - normalRb.setError(false); - - RadioButton errorRb("Option"); - errorRb.setError(true); - - QImage normalImage = widget_test::renderWidgetToImage(&normalRb, QSize(200, 48)); - QImage errorImage = widget_test::renderWidgetToImage(&errorRb, QSize(200, 48)); - - EXPECT_TRUE(widget_test::imagesDiffer(normalImage, errorImage)) - << "Error and normal RadioButton should produce different visual output"; -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/scrollview_test.cpp b/test/ui/widget/scrollview_test.cpp deleted file mode 100644 index 62d7e019e..000000000 --- a/test/ui/widget/scrollview_test.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include "widget_test_helper.h" - -#include -#include -#include - -#include "ui/widget/material/widget/scrollview/scrollview.h" - -using namespace cf::ui::widget::material; - -TEST(ScrollView, A01_DefaultConstructor) { - ScrollView sv; - EXPECT_TRUE(sv.isEnabled()); -} - -TEST(ScrollView, B01_SetWidget) { - ScrollView sv; - QLabel* label = new QLabel("This is a very long text that should require scrolling " - "to see all the content within the scroll view widget."); - sv.setWidget(label); - EXPECT_EQ(sv.widget(), label); -} - -TEST(ScrollView, B02_EnableDisable) { - ScrollView sv; - sv.setEnabled(true); - EXPECT_TRUE(sv.isEnabled()); - - sv.setEnabled(false); - EXPECT_FALSE(sv.isEnabled()); - - sv.setEnabled(true); - EXPECT_TRUE(sv.isEnabled()); -} - -TEST(ScrollView, B03_SetScrollbarFadeEnabled) { - ScrollView sv; - sv.setScrollbarFadeEnabled(false); - EXPECT_FALSE(sv.scrollbarFadeEnabled()); - - sv.setScrollbarFadeEnabled(true); - EXPECT_TRUE(sv.scrollbarFadeEnabled()); -} - -TEST(ScrollView, B04_SetScrollbarFadeDelay) { - ScrollView sv; - sv.setScrollbarFadeDelay(1000); - EXPECT_EQ(sv.scrollbarFadeDelay(), 1000); -} - -TEST(ScrollView, B05_SetScrollbarHoverExpansion) { - ScrollView sv; - sv.setScrollbarHoverExpansion(false); - EXPECT_FALSE(sv.scrollbarHoverExpansion()); - - sv.setScrollbarHoverExpansion(true); - EXPECT_TRUE(sv.scrollbarHoverExpansion()); -} - -TEST(ScrollView, C01_SizeHint_Valid) { - ScrollView sv; - QSize hint = sv.sizeHint(); - EXPECT_GT(hint.width(), 0); - EXPECT_GT(hint.height(), 0); -} - -TEST(ScrollView, D01_Paint_WithContent_NoCrash) { - ScrollView sv; - QLabel* label = new QLabel("This is a very long text that should require scrolling " - "to see all the content within the scroll view widget."); - sv.setWidget(label); - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&sv, QSize(200, 200))); -} - -TEST(ScrollView, D02_Paint_WithContent_PaintsPixels) { - ScrollView sv; - QLabel* label = new QLabel("Content for scroll view testing"); - sv.setWidget(label); - widget_test::verifyPaintsPixels(&sv, QSize(200, 200)); -} - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/separator_test.cpp b/test/ui/widget/separator_test.cpp deleted file mode 100644 index d76e95209..000000000 --- a/test/ui/widget/separator_test.cpp +++ /dev/null @@ -1,99 +0,0 @@ -/** - * @file separator_test.cpp - * @brief Unit tests for Material Design 3 Separator widget. - */ - -#include "widget_test_helper.h" - -#include -#include - -#include "ui/widget/material/widget/separator/separator.h" - -using namespace cf::ui::widget::material; - -// --- A: Construction --- - -TEST(SeparatorTest, A01_DefaultConstructor) { - Separator sep; - EXPECT_NO_FATAL_FAILURE(sep.mode()); - EXPECT_TRUE(sep.isEnabled()); -} - -// --- B: Property setters --- - -TEST(SeparatorTest, B01_EnableDisable) { - Separator sep; - sep.setEnabled(true); - EXPECT_TRUE(sep.isEnabled()); - - sep.setEnabled(false); - EXPECT_FALSE(sep.isEnabled()); -} - -TEST(SeparatorTest, B02_SetMode) { - Separator sep; - sep.setMode(SeparatorMode::FullBleed); - EXPECT_EQ(sep.mode(), SeparatorMode::FullBleed); - - sep.setMode(SeparatorMode::Inset); - EXPECT_EQ(sep.mode(), SeparatorMode::Inset); - - sep.setMode(SeparatorMode::MiddleInset); - EXPECT_EQ(sep.mode(), SeparatorMode::MiddleInset); -} - -TEST(SeparatorTest, B03_SetOrientation) { - Separator sep; - sep.setOrientation(Qt::Horizontal); - EXPECT_EQ(sep.orientation(), Qt::Horizontal); - - sep.setOrientation(Qt::Vertical); - EXPECT_EQ(sep.orientation(), Qt::Vertical); -} - -// --- C: Size hints --- - -TEST(SeparatorTest, C01_SizeHint_NotZero) { - Separator sep; - QSize hint = sep.sizeHint(); - EXPECT_FALSE(hint.isNull()); -} - -TEST(SeparatorTest, C02_SizeHint_Horizontal_Vertical) { - Separator hSep(Qt::Horizontal); - EXPECT_GT(hSep.sizeHint().width(), 0); - - Separator vSep(Qt::Vertical); - EXPECT_GT(vSep.sizeHint().height(), 0); -} - -// --- D: Paint --- - -TEST(SeparatorTest, D01_Paint_NoCrash) { - Separator sep; - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&sep, QSize(200, 2))); -} - -TEST(SeparatorTest, D02_Paint_AllModes_NoCrash) { - const SeparatorMode modes[] = {SeparatorMode::FullBleed, SeparatorMode::Inset, - SeparatorMode::MiddleInset}; - for (auto mode : modes) { - Separator sep(Qt::Horizontal); - sep.setMode(mode); - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&sep, QSize(200, 4))); - } -} - -TEST(SeparatorTest, D03_Paint_Vertical_NoCrash) { - Separator sep(Qt::Vertical); - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&sep, QSize(4, 200))); -} - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/slider_test.cpp b/test/ui/widget/slider_test.cpp deleted file mode 100644 index b097fb315..000000000 --- a/test/ui/widget/slider_test.cpp +++ /dev/null @@ -1,124 +0,0 @@ -/** - * @file slider_test.cpp - * @brief Unit tests for Slider (Material Widget Layer) - * - * Test Coverage: - * 1. Default constructor - * 2. Set range - * 3. Set value - * 4. Enable/disable behavior - * 5. SizeHint non-zero - * 6. Paint output differs between slider values - */ - -#include "ui/widget/material/widget/slider/slider.h" -#include "widget_test_helper.h" -#include -#include - -using namespace cf::ui::widget::material; - -// ============================================================================= -// Test Suite A: Construction -// ============================================================================= - -TEST(SliderTest, A01_DefaultConstructor) { - Slider slider; - // Verify constructed without crash - EXPECT_TRUE(slider.isEnabled()); -} - -TEST(SliderTest, A02_ConstructWithOrientation) { - Slider hSlider(Qt::Horizontal); - EXPECT_EQ(hSlider.orientation(), Qt::Horizontal); - - Slider vSlider(Qt::Vertical); - EXPECT_EQ(vSlider.orientation(), Qt::Vertical); -} - -// ============================================================================= -// Test Suite B: State Changes -// ============================================================================= - -TEST(SliderTest, B01_SetRange) { - Slider slider; - slider.setRange(0, 100); - EXPECT_EQ(slider.minimum(), 0); - EXPECT_EQ(slider.maximum(), 100); -} - -TEST(SliderTest, B02_SetValue) { - Slider slider; - slider.setRange(0, 100); - slider.setValue(50); - EXPECT_EQ(slider.value(), 50); -} - -TEST(SliderTest, B03_EnableDisable) { - Slider slider; - EXPECT_TRUE(slider.isEnabled()); - - slider.setEnabled(false); - EXPECT_FALSE(slider.isEnabled()); - - slider.setEnabled(true); - EXPECT_TRUE(slider.isEnabled()); -} - -TEST(SliderTest, B04_SetValue_BoundaryClamped) { - Slider slider; - slider.setRange(0, 100); - - slider.setValue(-10); - EXPECT_GE(slider.value(), 0) << "Value below minimum should be clamped"; - - slider.setValue(200); - EXPECT_LE(slider.value(), 100) << "Value above maximum should be clamped"; -} - -// ============================================================================= -// Test Suite C: Size Hints -// ============================================================================= - -TEST(SliderTest, C01_SizeHint_NotZero) { - Slider slider; - EXPECT_GT(slider.sizeHint().width(), 0); - EXPECT_GT(slider.sizeHint().height(), 0); -} - -TEST(SliderTest, C02_MinimumSizeHint_NotZero) { - Slider slider; - EXPECT_GT(slider.minimumSizeHint().width(), 0); - EXPECT_GT(slider.minimumSizeHint().height(), 0); -} - -// ============================================================================= -// Test Suite D: Paint Verification -// ============================================================================= - -TEST(SliderTest, D01_Paint_DifferentValues_DifferentPixels) { - Slider sliderLow; - sliderLow.setRange(0, 100); - sliderLow.setValue(0); - QImage imgLow = widget_test::renderWidgetToImage(&sliderLow, QSize(200, 48)); - - Slider sliderHigh; - sliderHigh.setRange(0, 100); - sliderHigh.setValue(100); - QImage imgHigh = widget_test::renderWidgetToImage(&sliderHigh, QSize(200, 48)); - - EXPECT_TRUE(widget_test::imagesDiffer(imgLow, imgHigh)) - << "Slider at value=0 and value=100 should produce different visual output"; -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/spinbox_test.cpp b/test/ui/widget/spinbox_test.cpp deleted file mode 100644 index bc8e061e4..000000000 --- a/test/ui/widget/spinbox_test.cpp +++ /dev/null @@ -1,111 +0,0 @@ -/** - * @file spinbox_test.cpp - * @brief Unit tests for Material Design 3 SpinBox widget. - */ - -#include "widget_test_helper.h" - -#include -#include - -#include "ui/widget/material/widget/spinbox/spinbox.h" - -using namespace cf::ui::widget::material; - -// --- A: Construction --- - -TEST(SpinBoxTest, A01_DefaultConstructor) { - SpinBox sb; - EXPECT_NO_FATAL_FAILURE(sb.value()); - EXPECT_TRUE(sb.isEnabled()); -} - -// --- B: Property setters --- - -TEST(SpinBoxTest, B01_SetRange) { - SpinBox sb; - sb.setRange(0, 100); - EXPECT_EQ(sb.minimum(), 0); - EXPECT_EQ(sb.maximum(), 100); -} - -TEST(SpinBoxTest, B02_SetValue) { - SpinBox sb; - sb.setValue(42); - EXPECT_EQ(sb.value(), 42); -} - -TEST(SpinBoxTest, B03_SetPrefixSuffix) { - SpinBox sb; - sb.setPrefix("$"); - sb.setSuffix("px"); - EXPECT_EQ(sb.prefix(), "$"); - EXPECT_EQ(sb.suffix(), "px"); -} - -TEST(SpinBoxTest, B04_EnableDisable) { - SpinBox sb; - sb.setEnabled(true); - EXPECT_TRUE(sb.isEnabled()); - - sb.setEnabled(false); - EXPECT_FALSE(sb.isEnabled()); -} - -TEST(SpinBoxTest, B05_SetValue_BoundaryClamped) { - SpinBox sb; - sb.setRange(0, 100); - - sb.setValue(-10); - EXPECT_GE(sb.value(), 0) << "Value below minimum should be clamped"; - - sb.setValue(200); - EXPECT_LE(sb.value(), 100) << "Value above maximum should be clamped"; -} - -// --- C: Size hints --- - -TEST(SpinBoxTest, C01_SizeHint_NotZero) { - SpinBox sb; - QSize hint = sb.sizeHint(); - EXPECT_FALSE(hint.isNull()); -} - -TEST(SpinBoxTest, C02_MinimumSizeHint_NotZero) { - SpinBox sb; - sb.setValue(42); - EXPECT_GT(sb.minimumSizeHint().width(), 0); - EXPECT_GT(sb.minimumSizeHint().height(), 0); -} - -// --- D: Paint --- - -TEST(SpinBoxTest, D01_Paint_WithValue_NoCrash) { - SpinBox sb; - sb.setRange(0, 100); - sb.setValue(42); - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&sb, QSize(120, 48))); -} - -TEST(SpinBoxTest, D02_Paint_DifferentValues_DifferentPixels) { - SpinBox sbLow; - sbLow.setRange(0, 100); - sbLow.setValue(0); - QImage imgLow = widget_test::renderWidgetToImage(&sbLow, QSize(120, 48)); - - SpinBox sbHigh; - sbHigh.setRange(0, 100); - sbHigh.setValue(99); - QImage imgHigh = widget_test::renderWidgetToImage(&sbHigh, QSize(120, 48)); - - EXPECT_TRUE(widget_test::imagesDiffer(imgLow, imgHigh)) - << "SpinBox at value=0 and value=99 should produce different visual output"; -} - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/switch_test.cpp b/test/ui/widget/switch_test.cpp deleted file mode 100644 index 318218ddb..000000000 --- a/test/ui/widget/switch_test.cpp +++ /dev/null @@ -1,112 +0,0 @@ -/** - * @file switch_test.cpp - * @brief Unit tests for Material Design 3 Switch widget. - * - * Test Coverage: - * 1. Constructor variants and defaults - * 2. Property setters/getters (checked, enabled) - * 3. Size hints with reasonable dimension checks - * 4. Paint behavior for on/off states and thumb position verification - */ - -#include "widget_test_helper.h" - -#include "ui/widget/material/widget/switch/switch.h" - -#include -#include - -using namespace cf::ui::widget::material; - -// ============================================================================= -// Test Suite A: Constructors -// ============================================================================= - -TEST(SwitchTest, A01_DefaultConstructor_Unchecked) { - Switch sw; - EXPECT_FALSE(sw.isChecked()); -} - -TEST(SwitchTest, A02_ConstructWithText) { - Switch sw("Toggle"); - EXPECT_EQ(sw.text(), "Toggle"); -} - -// ============================================================================= -// Test Suite B: Property Setters / Getters -// ============================================================================= - -TEST(SwitchTest, B01_SetChecked) { - Switch sw; - EXPECT_FALSE(sw.isChecked()); - - sw.setChecked(true); - EXPECT_TRUE(sw.isChecked()); - - sw.setChecked(false); - EXPECT_FALSE(sw.isChecked()); -} - -TEST(SwitchTest, B02_EnableDisable) { - Switch sw; - EXPECT_TRUE(sw.isEnabled()); - - sw.setEnabled(false); - EXPECT_FALSE(sw.isEnabled()); - - sw.setEnabled(true); - EXPECT_TRUE(sw.isEnabled()); -} - -// ============================================================================= -// Test Suite C: Size Hints -// ============================================================================= - -TEST(SwitchTest, C01_SizeHint_NotZero) { - Switch sw; - EXPECT_GT(sw.sizeHint().width(), 0); - EXPECT_GT(sw.sizeHint().height(), 0); - EXPECT_GE(sw.sizeHint().width(), 40); - EXPECT_GE(sw.sizeHint().height(), 20); -} - -// ============================================================================= -// Test Suite D: Paint Behavior -// ============================================================================= - -TEST(SwitchTest, D01_Paint_OnOff_DifferentPixels) { - Switch offSwitch; - offSwitch.setChecked(false); - - Switch onSwitch; - onSwitch.setChecked(true); - - QImage offImage = widget_test::renderWidgetToImage(&offSwitch, QSize(120, 48)); - QImage onImage = widget_test::renderWidgetToImage(&onSwitch, QSize(120, 48)); - - EXPECT_TRUE(widget_test::imagesDiffer(offImage, onImage)) - << "On and off switch states should produce different visual output"; -} - -TEST(SwitchTest, D02_Paint_ThumbPosition) { - Switch sw; - sw.setChecked(false); - - QImage image = widget_test::renderWidgetToImage(&sw, QSize(120, 48)); - int nonTransparent = widget_test::countNonTransparentPixels(image); - - // Verify the switch renders visible content - EXPECT_GT(nonTransparent, 0) << "Switch should render visible pixels"; -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/tableview_test.cpp b/test/ui/widget/tableview_test.cpp deleted file mode 100644 index 68a4b5c64..000000000 --- a/test/ui/widget/tableview_test.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include "widget_test_helper.h" - -#include -#include -#include - -#include "ui/widget/material/widget/tableview/tableview.h" - -using namespace cf::ui::widget::material; - -TEST(TableView, A01_DefaultConstructor) { - TableView tv; - EXPECT_TRUE(tv.isEnabled()); -} - -TEST(TableView, B01_SetModel) { - TableView tv; - QStandardItemModel model(3, 2); - tv.setModel(&model); - EXPECT_EQ(tv.model(), &model); -} - -TEST(TableView, B02_EnableDisable) { - TableView tv; - tv.setEnabled(true); - EXPECT_TRUE(tv.isEnabled()); - - tv.setEnabled(false); - EXPECT_FALSE(tv.isEnabled()); - - tv.setEnabled(true); - EXPECT_TRUE(tv.isEnabled()); -} - -TEST(TableView, C01_SizeHint_Valid) { - TableView tv; - QSize hint = tv.sizeHint(); - EXPECT_GT(hint.width(), 0); - EXPECT_GT(hint.height(), 0); -} - -TEST(TableView, D01_Paint_WithModel_NoCrash) { - TableView tv; - QStandardItemModel model(3, 2); - tv.setModel(&model); - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&tv, QSize(300, 200))); -} - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/tabview_test.cpp b/test/ui/widget/tabview_test.cpp deleted file mode 100644 index 015b1cd5a..000000000 --- a/test/ui/widget/tabview_test.cpp +++ /dev/null @@ -1,117 +0,0 @@ -#include "widget_test_helper.h" - -#include -#include - -#include "ui/widget/material/widget/tabview/tabview.h" - -using namespace cf::ui::widget::material; - -TEST(TabView, A01_DefaultConstructor) { - TabView tv; - EXPECT_EQ(tv.count(), 0); -} - -TEST(TabView, B01_AddTab) { - TabView tv; - tv.addTab(new QWidget(), "Tab1"); - tv.addTab(new QWidget(), "Tab2"); - tv.addTab(new QWidget(), "Tab3"); - EXPECT_EQ(tv.count(), 3); -} - -TEST(TabView, B02_SetCurrentIndex) { - TabView tv; - tv.addTab(new QWidget(), "Tab1"); - tv.addTab(new QWidget(), "Tab2"); - tv.addTab(new QWidget(), "Tab3"); - tv.setCurrentIndex(1); - EXPECT_EQ(tv.currentIndex(), 1); -} - -TEST(TabView, B03_EnableDisable) { - TabView tv; - tv.setEnabled(true); - EXPECT_TRUE(tv.isEnabled()); - - tv.setEnabled(false); - EXPECT_FALSE(tv.isEnabled()); - - tv.setEnabled(true); - EXPECT_TRUE(tv.isEnabled()); -} - -TEST(TabView, B04_SetTabHeight) { - TabView tv; - tv.setTabHeight(48); - EXPECT_EQ(tv.tabHeight(), 48); -} - -TEST(TabView, B05_SetTabMinWidth) { - TabView tv; - tv.setTabMinWidth(80); - EXPECT_EQ(tv.tabMinWidth(), 80); -} - -TEST(TabView, B06_SetShowIndicator) { - TabView tv; - tv.setShowIndicator(true); - EXPECT_TRUE(tv.showIndicator()); - - tv.setShowIndicator(false); - EXPECT_FALSE(tv.showIndicator()); -} - -TEST(TabView, B07_SetTabCloseable) { - TabView tv; - tv.addTab(new QWidget(), "Tab1"); - tv.addTab(new QWidget(), "Tab2"); - EXPECT_NO_FATAL_FAILURE(tv.setTabCloseable(0, true)); - EXPECT_NO_FATAL_FAILURE(tv.setTabCloseable(1, false)); -} - -TEST(TabView, C01_SizeHint_Valid) { - TabView tv; - QSize hint = tv.sizeHint(); - EXPECT_GT(hint.width(), 0); - EXPECT_GT(hint.height(), 0); -} - -TEST(TabView, C02_MinimumSizeHint_NotZero) { - TabView tv; - tv.addTab(new QWidget(), "Tab1"); - EXPECT_GT(tv.minimumSizeHint().width(), 0); - EXPECT_GT(tv.minimumSizeHint().height(), 0); -} - -TEST(TabView, D01_Paint_WithTabs_NoCrash) { - TabView tv; - tv.addTab(new QWidget(), "Tab1"); - tv.addTab(new QWidget(), "Tab2"); - tv.addTab(new QWidget(), "Tab3"); - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&tv, QSize(300, 200))); -} - -TEST(TabView, D02_Paint_DifferentIndex_DifferentPixels) { - TabView tv; - tv.addTab(new QWidget(), "First"); - tv.addTab(new QWidget(), "Second"); - tv.resize(300, 200); - - tv.setCurrentIndex(0); - QImage img0 = widget_test::renderWidgetToImage(&tv, QSize(300, 200)); - - tv.setCurrentIndex(1); - QImage img1 = widget_test::renderWidgetToImage(&tv, QSize(300, 200)); - - EXPECT_TRUE(widget_test::imagesDiffer(img0, img1)) - << "TabView with different active tabs should produce different visual output"; -} - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/textarea_test.cpp b/test/ui/widget/textarea_test.cpp deleted file mode 100644 index ee420bdf7..000000000 --- a/test/ui/widget/textarea_test.cpp +++ /dev/null @@ -1,153 +0,0 @@ -/** - * @file textarea_test.cpp - * @brief Unit tests for Material Design 3 TextArea widget. - */ - -#include "widget_test_helper.h" - -#include -#include - -#include "ui/widget/material/widget/textarea/textarea.h" - -using namespace cf::ui::widget::material; - -// --- A: Construction --- - -TEST(TextAreaTest, A01_DefaultConstructor) { - TextArea ta; - EXPECT_NO_FATAL_FAILURE(ta.toPlainText()); - EXPECT_TRUE(ta.isEnabled()); -} - -// --- B: Property setters --- - -TEST(TextAreaTest, B01_SetPlainText) { - TextArea ta; - ta.setPlainText("hello"); - EXPECT_EQ(ta.toPlainText(), "hello"); -} - -TEST(TextAreaTest, B02_SetReadOnly) { - TextArea ta; - ta.setReadOnly(true); - EXPECT_TRUE(ta.isReadOnly()); -} - -TEST(TextAreaTest, B03_EnableDisable) { - TextArea ta; - ta.setEnabled(true); - EXPECT_TRUE(ta.isEnabled()); - - ta.setEnabled(false); - EXPECT_FALSE(ta.isEnabled()); -} - -// --- C: Size hints --- - -TEST(TextAreaTest, C01_SizeHint_Valid) { - TextArea ta; - QSize hint = ta.sizeHint(); - EXPECT_GT(hint.width(), 0); - EXPECT_GT(hint.height(), 0); -} - -// --- D: Paint --- - -TEST(TextAreaTest, D01_Paint_WithText_NoCrash) { - TextArea ta; - ta.setPlainText("Hello World"); - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&ta, QSize(200, 100))); -} - -TEST(TextAreaTest, B04_SetVariant) { - TextArea ta; - ta.setVariant(TextArea::TextAreaVariant::Filled); - EXPECT_EQ(ta.variant(), TextArea::TextAreaVariant::Filled); - - ta.setVariant(TextArea::TextAreaVariant::Outlined); - EXPECT_EQ(ta.variant(), TextArea::TextAreaVariant::Outlined); -} - -TEST(TextAreaTest, B05_SetLabel) { - TextArea ta; - ta.setLabel("Description"); - EXPECT_EQ(ta.label(), "Description"); -} - -TEST(TextAreaTest, B06_SetHelperText) { - TextArea ta; - ta.setHelperText("Enter a description"); - EXPECT_EQ(ta.helperText(), "Enter a description"); -} - -TEST(TextAreaTest, B07_SetErrorText) { - TextArea ta; - ta.setErrorText("Description is required"); - EXPECT_EQ(ta.errorText(), "Description is required"); -} - -TEST(TextAreaTest, B08_SetMaxLength) { - TextArea ta; - ta.setMaxLength(500); - EXPECT_EQ(ta.maxLength(), 500); -} - -TEST(TextAreaTest, B09_ShowCharacterCounter) { - TextArea ta; - ta.setShowCharacterCounter(true); - EXPECT_TRUE(ta.showCharacterCounter()); - - ta.setShowCharacterCounter(false); - EXPECT_FALSE(ta.showCharacterCounter()); -} - -TEST(TextAreaTest, B10_SetMinLines) { - TextArea ta; - ta.setMinLines(3); - EXPECT_EQ(ta.minLines(), 3); -} - -TEST(TextAreaTest, B11_SetMaxLines) { - TextArea ta; - ta.setMaxLines(8); - EXPECT_EQ(ta.maxLines(), 8); -} - -TEST(TextAreaTest, C02_MinimumSizeHint_NotZero) { - TextArea ta; - QSize hint = ta.minimumSizeHint(); - EXPECT_GT(hint.width(), 0); - EXPECT_GT(hint.height(), 0); -} - -TEST(TextAreaTest, D02_Paint_EnabledDisabled_DifferentPixels) { - TextArea enabledTa; - enabledTa.setPlainText("Test content"); - enabledTa.setEnabled(true); - - TextArea disabledTa; - disabledTa.setPlainText("Test content"); - disabledTa.setEnabled(false); - - QImage enabledImage = widget_test::renderWidgetToImage(&enabledTa, QSize(200, 100)); - QImage disabledImage = widget_test::renderWidgetToImage(&disabledTa, QSize(200, 100)); - - EXPECT_TRUE(widget_test::imagesDiffer(enabledImage, disabledImage)) - << "Enabled and disabled TextArea should produce different visual output"; -} - -TEST(TextAreaTest, D03_Paint_WithLabel_NoCrash) { - TextArea ta; - ta.setLabel("Comments"); - ta.setPlainText("Some text here"); - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&ta, QSize(200, 100))); -} - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/textfield_test.cpp b/test/ui/widget/textfield_test.cpp deleted file mode 100644 index f83f60e11..000000000 --- a/test/ui/widget/textfield_test.cpp +++ /dev/null @@ -1,184 +0,0 @@ -/** - * @file textfield_test.cpp - * @brief Unit tests for Material Design 3 TextField widget. - */ - -#include "widget_test_helper.h" - -#include -#include - -#include "ui/widget/material/widget/textfield/textfield.h" - -using namespace cf::ui::widget::material; - -// --- A: Construction --- - -TEST(TextFieldTest, A01_DefaultConstructor) { - TextField tf; - EXPECT_NO_FATAL_FAILURE(tf.text()); - EXPECT_TRUE(tf.isEnabled()); -} - -// --- B: Property setters --- - -TEST(TextFieldTest, B01_SetText) { - TextField tf; - tf.setText("hello"); - EXPECT_EQ(tf.text(), "hello"); -} - -TEST(TextFieldTest, B02_SetPlaceholderText) { - TextField tf; - tf.setPlaceholderText("Enter..."); - EXPECT_EQ(tf.placeholderText(), "Enter..."); -} - -TEST(TextFieldTest, B03_SetReadOnly) { - TextField tf; - tf.setReadOnly(true); - EXPECT_TRUE(tf.isReadOnly()); -} - -TEST(TextFieldTest, B04_EnableDisable) { - TextField tf; - tf.setEnabled(true); - EXPECT_TRUE(tf.isEnabled()); - - tf.setEnabled(false); - EXPECT_FALSE(tf.isEnabled()); -} - -// --- C: Size hints --- - -TEST(TextFieldTest, C01_SizeHint_NotZero) { - TextField tf; - QSize hint = tf.sizeHint(); - EXPECT_FALSE(hint.isNull()); -} - -// --- D: Paint --- - -TEST(TextFieldTest, D01_Paint_WithText_NoCrash) { - TextField tf; - tf.setText("Hello World"); - widget_test::verifyPaintsPixels(&tf, QSize(200, 56)); -} - -TEST(TextFieldTest, D02_Paint_EmptyVsText_DifferentPixels) { - TextField tf; - - // Render empty - QImage emptyImage = widget_test::renderWidgetToImage(&tf, QSize(200, 56)); - - // Render with text - tf.setText("Hello World"); - QImage textImage = widget_test::renderWidgetToImage(&tf, QSize(200, 56)); - - EXPECT_TRUE(widget_test::imagesDiffer(emptyImage, textImage)) - << "Empty and text renders should produce different visual output"; -} - -TEST(TextFieldTest, B05_SetVariant) { - TextField tf; - tf.setVariant(TextField::TextFieldVariant::Filled); - EXPECT_EQ(tf.variant(), TextField::TextFieldVariant::Filled); - - tf.setVariant(TextField::TextFieldVariant::Outlined); - EXPECT_EQ(tf.variant(), TextField::TextFieldVariant::Outlined); -} - -TEST(TextFieldTest, B06_SetLabel) { - TextField tf; - tf.setLabel("Username"); - EXPECT_EQ(tf.label(), "Username"); - - tf.setLabel(""); - EXPECT_TRUE(tf.label().isEmpty()); -} - -TEST(TextFieldTest, B07_SetHelperText) { - TextField tf; - tf.setHelperText("Enter your username"); - EXPECT_EQ(tf.helperText(), "Enter your username"); -} - -TEST(TextFieldTest, B08_SetErrorText) { - TextField tf; - tf.setErrorText("Field is required"); - EXPECT_EQ(tf.errorText(), "Field is required"); -} - -TEST(TextFieldTest, B09_SetMaxLength) { - TextField tf; - tf.setMaxLength(50); - EXPECT_EQ(tf.maxLength(), 50); -} - -TEST(TextFieldTest, B10_ShowCharacterCounter) { - TextField tf; - EXPECT_FALSE(tf.showCharacterCounter()); - - tf.setShowCharacterCounter(true); - EXPECT_TRUE(tf.showCharacterCounter()); - - tf.setShowCharacterCounter(false); - EXPECT_FALSE(tf.showCharacterCounter()); -} - -TEST(TextFieldTest, B11_SetPrefixSuffixIcon) { - TextField tf; - EXPECT_NO_FATAL_FAILURE(tf.setPrefixIcon(QIcon())); - EXPECT_NO_FATAL_FAILURE(tf.setSuffixIcon(QIcon())); -} - -TEST(TextFieldTest, C02_MinimumSizeHint_NotZero) { - TextField tf; - tf.setText("Sample"); - QSize hint = tf.minimumSizeHint(); - EXPECT_GT(hint.width(), 0); - EXPECT_GT(hint.height(), 0); -} - -TEST(TextFieldTest, D03_Paint_EnabledDisabled_DifferentPixels) { - TextField enabledTf; - enabledTf.setText("Test"); - enabledTf.setEnabled(true); - - TextField disabledTf; - disabledTf.setText("Test"); - disabledTf.setEnabled(false); - - QImage enabledImage = widget_test::renderWidgetToImage(&enabledTf, QSize(200, 56)); - QImage disabledImage = widget_test::renderWidgetToImage(&disabledTf, QSize(200, 56)); - - EXPECT_TRUE(widget_test::imagesDiffer(enabledImage, disabledImage)) - << "Enabled and disabled TextField should produce different visual output"; -} - -TEST(TextFieldTest, D04_Paint_Variant_DifferentPixels) { - TextField filledTf("Text", TextField::TextFieldVariant::Filled); - - TextField outlinedTf("Text", TextField::TextFieldVariant::Outlined); - - QImage filledImage = widget_test::renderWidgetToImage(&filledTf, QSize(200, 56)); - QImage outlinedImage = widget_test::renderWidgetToImage(&outlinedTf, QSize(200, 56)); - - EXPECT_TRUE(widget_test::imagesDiffer(filledImage, outlinedImage)) - << "Filled and Outlined TextField variants should produce different visual output"; -} - -TEST(TextFieldTest, D05_Paint_WithLabel_NoCrash) { - TextField tf; - tf.setLabel("Email"); - tf.setText("user@example.com"); - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&tf, QSize(200, 56))); -} - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/treeview_test.cpp b/test/ui/widget/treeview_test.cpp deleted file mode 100644 index 5b43a03bf..000000000 --- a/test/ui/widget/treeview_test.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "widget_test_helper.h" - -#include -#include -#include - -#include "ui/widget/material/widget/treeview/treeview.h" - -using namespace cf::ui::widget::material; - -TEST(TreeView, A01_DefaultConstructor) { - TreeView tv; - EXPECT_TRUE(tv.isEnabled()); -} - -TEST(TreeView, B01_SetModel) { - TreeView tv; - QStandardItemModel model; - QStandardItem* root = model.invisibleRootItem(); - QStandardItem* parentItem = new QStandardItem("Parent"); - root->appendRow(parentItem); - parentItem->appendRow(new QStandardItem("Child1")); - parentItem->appendRow(new QStandardItem("Child2")); - tv.setModel(&model); - EXPECT_EQ(tv.model(), &model); -} - -TEST(TreeView, B02_EnableDisable) { - TreeView tv; - tv.setEnabled(true); - EXPECT_TRUE(tv.isEnabled()); - - tv.setEnabled(false); - EXPECT_FALSE(tv.isEnabled()); - - tv.setEnabled(true); - EXPECT_TRUE(tv.isEnabled()); -} - -TEST(TreeView, C01_SizeHint_Valid) { - TreeView tv; - QSize hint = tv.sizeHint(); - EXPECT_GT(hint.width(), 0); - EXPECT_GT(hint.height(), 0); -} - -TEST(TreeView, D01_Paint_WithModel_NoCrash) { - TreeView tv; - QStandardItemModel model; - QStandardItem* root = model.invisibleRootItem(); - QStandardItem* parentItem = new QStandardItem("Parent"); - root->appendRow(parentItem); - parentItem->appendRow(new QStandardItem("Child1")); - parentItem->appendRow(new QStandardItem("Child2")); - tv.setModel(&model); - EXPECT_NO_FATAL_FAILURE(widget_test::renderWidgetToImage(&tv, QSize(200, 200))); -} - -int main(int argc, char* argv[]) { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureFontsAvailable(); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/ui/widget/widget_test_helper.h b/test/ui/widget/widget_test_helper.h deleted file mode 100644 index b82ad090c..000000000 --- a/test/ui/widget/widget_test_helper.h +++ /dev/null @@ -1,117 +0,0 @@ -/** - * @file widget_test_helper.h - * @brief Common test utilities for Material Design 3 widget tests. - * - * Provides offscreen rendering helpers and pixel verification utilities - * for testing widget paint behavior without a display server. - */ - -#pragma once - -#include -#include -#include -#include - -/** - * @brief Ensure fonts are available for offscreen rendering. - * - * Qt6 no longer ships bundled fonts. In offscreen QPA mode on Windows, - * set QT_QPA_FONTDIR to the system fonts directory so text renders properly. - * Must be called before QApplication is constructed. - */ -inline void ensureFontsAvailable() { -#ifdef _WIN32 - qputenv("QT_QPA_FONTDIR", "C:/Windows/Fonts"); -#endif -} - -namespace widget_test { - -/** - * @brief Count non-transparent pixels in an image. - * Used to verify that paint operations produced visible output. - */ -inline int countNonTransparentPixels(const QImage& image) { - int count = 0; - for (int y = 0; y < image.height(); ++y) { - for (int x = 0; x < image.width(); ++x) { - QRgb pixel = image.pixel(x, y); - if (qAlpha(pixel) > 0) { - ++count; - } - } - } - return count; -} - -/** - * @brief Check if the image contains any pixel with the specified color (with tolerance). - * @param image The image to search. - * @param color The target color to find. - * @param tolerance Per-channel tolerance (default 20). - */ -inline bool hasPixelWithColor(const QImage& image, QRgb color, int tolerance = 20) { - for (int y = 0; y < image.height(); ++y) { - for (int x = 0; x < image.width(); ++x) { - QRgb pixel = image.pixel(x, y); - if (qAlpha(pixel) > 0 && qAbs(qRed(pixel) - qRed(color)) <= tolerance && - qAbs(qGreen(pixel) - qGreen(color)) <= tolerance && - qAbs(qBlue(pixel) - qBlue(color)) <= tolerance) { - return true; - } - } - } - return false; -} - -/** - * @brief Render a widget to an offscreen QImage. - * - * Resizes the widget, renders it to a transparent ARGB32 image. - * Requires QGuiApplication to exist (offscreen platform). - */ -inline QImage renderWidgetToImage(QWidget* widget, const QSize& size) { - QImage image(size, QImage::Format_ARGB32); - image.fill(Qt::transparent); - widget->resize(size); - widget->show(); - QCoreApplication::processEvents(); - widget->render(&image); - widget->hide(); - return image; -} - -/** - * @brief Verify that a widget paints non-transparent pixels. - * - * Renders the widget and asserts that at least one non-transparent - * pixel was produced. - */ -inline void verifyPaintsPixels(QWidget* widget, const QSize& size) { - QImage image = renderWidgetToImage(widget, size); - int nonTransparent = countNonTransparentPixels(image); - EXPECT_GT(nonTransparent, 0) << "Widget should paint non-transparent pixels"; -} - -/** - * @brief Check if two images have different pixel content. - * - * Compares every pixel. Returns true if at least one pixel differs - * (comparing RGBA channels). This is useful for verifying that widget - * state changes (enabled/disabled, checked/unchecked) produce visual changes. - */ -inline bool imagesDiffer(const QImage& a, const QImage& b) { - if (a.size() != b.size()) - return true; - for (int y = 0; y < a.height(); ++y) { - for (int x = 0; x < a.width(); ++x) { - if (a.pixel(x, y) != b.pixel(x, y)) { - return true; - } - } - } - return false; -} - -} // namespace widget_test diff --git a/third_party/QuarkWidgets b/third_party/QuarkWidgets new file mode 160000 index 000000000..30b7936ea --- /dev/null +++ b/third_party/QuarkWidgets @@ -0,0 +1 @@ +Subproject commit 30b7936eac42563af2878c41272b2f32a07daedf diff --git a/ui/.gitignore b/ui/.gitignore deleted file mode 100644 index 26bfe1df9..000000000 --- a/ui/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# AI Helps -MaterialRules.md -HELPER.md \ No newline at end of file diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt deleted file mode 100644 index d70b0baee..000000000 --- a/ui/CMakeLists.txt +++ /dev/null @@ -1,83 +0,0 @@ -project(UI DESCRIPTION "CFDesktop UI Components" LANGUAGES CXX) - -log_info("UI" "Start UI Components Configurations") - -# All sources under ui/ are compiled into cfui.dll, so define cfui_EXPORTS -# so that CF_UI_EXPORT expands to __declspec(dllexport) instead of dllimport. -add_compile_definitions(cfui_EXPORTS) - -# Base Supports the UI Components Needs -add_subdirectory(base) - -# Core Provides the UI Components Supports -add_subdirectory(core) - -# Components Provides animation and UI component implementations -add_subdirectory(components) - -# Widget Supports the Middle Level Widgets Abstractions -add_subdirectory(widget) - -# Models will provide SDK Like Models for programmers -# Quick Start Apps -add_subdirectory(models) - -# ============================================================ -# Unified UI Library - Static Library -# ============================================================ - -add_library(cfui SHARED) - -set(CFUI_STATIC_LIBS - cf_ui_base - cf_ui_core - cf_ui_core_material - cf_ui_widget_material - cf_ui_application_support - cf_ui_components_material - cf_ui_components -) - -# Force all objects into cfui.dll (PRIVATE: only affects cfui's own link step). -# cfui has no own sources, so the linker would otherwise skip static lib objects. -# Note: cf_ui_widget is an INTERFACE library — use its actual static lib deps instead. -if(MSVC) - target_link_libraries(cfui PRIVATE ${CFUI_STATIC_LIBS}) - foreach(_cfui_module IN LISTS CFUI_STATIC_LIBS) - target_link_options(cfui PRIVATE "/WHOLEARCHIVE:$") - endforeach() -elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" AND NOT APPLE) - target_link_libraries(cfui PRIVATE - -Wl,--whole-archive - ${CFUI_STATIC_LIBS} - -Wl,--no-whole-archive - ) -else() - target_link_libraries(cfui PRIVATE ${CFUI_STATIC_LIBS}) -endif() - -# Set include directories -target_include_directories(cfui PUBLIC - $ - $ - $ - $ - $ - $ - $ -) - -# Link Qt libraries and base library (SHARED cfbase.dll) -target_link_libraries(cfui PUBLIC - Qt6::Core - Qt6::Gui - CFDesktop::base -) - -# Add alias for consistent naming -add_library(UI::cfui ALIAS cfui) -# For backward compatibility -add_library(UI::cf_ui ALIAS cfui) -add_library(UI::base ALIAS cfui) - -log_info("UI" "Start UI Components Configurations Done") diff --git a/ui/base/CMakeLists.txt b/ui/base/CMakeLists.txt deleted file mode 100644 index bab0818e2..000000000 --- a/ui/base/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ -project(ui_base LANGUAGES CXX) - -log_info("UI_Base" "Start Configure the UI Base") - -# STATIC library for linking into cfui.dll -add_library(cf_ui_base STATIC - math_helper.cpp - color_helper.cpp - color.cpp - device_pixel.cpp - geometry_helper.cpp - easing.cpp -) - -# Add include directories so headers can be found with ui/base/ prefix -# We need to add the ui parent directory so that ui/base/math_helper.h works -target_include_directories(cf_ui_base PUBLIC - $ - $ - $ - $ -) - -# Link Qt for compile dependencies (headers, etc.) -target_link_libraries(cf_ui_base PUBLIC Qt6::Core Qt6::Gui) - -log_info("UI_Base" "the UI Base Configuration Done") diff --git a/ui/base/color.cpp b/ui/base/color.cpp deleted file mode 100644 index e9e954ad8..000000000 --- a/ui/base/color.cpp +++ /dev/null @@ -1,301 +0,0 @@ -/** - * @file color.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief - * @version 0.1 - * @date 2026-02-23 - * - * @copyright Copyright (c) 2026 - * - */ -#include "color.h" -#include "math_helper.h" -#include -#include - -namespace cf::ui::base { - -// ============================================================================ -// Helper functions (internal) -// ============================================================================ - -namespace { -// sRGB to linear RGB conversion (gamma correction) -float toLinear(float c) { - if (c <= 0.04045f) { - return c / 12.92f; - } - return std::pow((c + 0.055f) / 1.055f, 2.4f); -} - -// Linear RGB to sRGB conversion -float toGamma(float c) { - if (c <= 0.0031308f) { - return c * 12.92f; - } - return 1.055f * std::pow(c, 1.0f / 2.4f) - 0.055f; -} - -// RGB to HSL conversion -struct HSL { - float h; // 0-360 - float s; // 0-1 - float l; // 0-1 -}; - -HSL rgbToHsl(float r, float g, float b) { - float maxVal = std::max({r, g, b}); - float minVal = std::min({r, g, b}); - float delta = maxVal - minVal; - - HSL hsl; - hsl.l = (maxVal + minVal) / 2.0f; - - if (delta < 0.0001f) { - hsl.h = 0.0f; - hsl.s = 0.0f; - } else { - if (hsl.l < 0.5f) { - hsl.s = delta / (maxVal + minVal); - } else { - hsl.s = delta / (2.0f - maxVal - minVal); - } - - if (maxVal == r) { - hsl.h = 60.0f * std::fmod((g - b) / delta, 6.0f); - } else if (maxVal == g) { - hsl.h = 60.0f * ((b - r) / delta + 2.0f); - } else { - hsl.h = 60.0f * ((r - g) / delta + 4.0f); - } - } - - if (hsl.h < 0.0f) - hsl.h += 360.0f; - return hsl; -} - -// HSL to RGB conversion -void hslToRgb(float h, float s, float l, float& outR, float& outG, float& outB) { - if (s < 0.0001f) { - outR = outG = outB = l; - return; - } - - float q = l < 0.5f ? l * (1.0f + s) : l + s - l * s; - float p = 2.0f * l - q; - - auto hueToRgb = [p, q](float t) -> float { - if (t < 0.0f) - t += 1.0f; - if (t > 1.0f) - t -= 1.0f; - if (t < 1.0f / 6.0f) - return p + (q - p) * 6.0f * t; - if (t < 1.0f / 2.0f) - return q; - if (t < 2.0f / 3.0f) - return p + (q - p) * (2.0f / 3.0f - t) * 6.0f; - return p; - }; - - outR = hueToRgb((h / 360.0f) + 1.0f / 3.0f); - outG = hueToRgb(h / 360.0f); - outB = hueToRgb((h / 360.0f) - 1.0f / 3.0f); -} - -// HCT to HSL approximation -// H and C are preserved, Tone maps to Lightness with adjustment -void hctToHsl(float h, float c, float tone, float& outH, float& outS, float& outL) { - // Simplified: H is same, Tone affects L, C affects S - outH = h; - - // Tone (0-100) to Lightness (0-1) - roughly linear but with adjustment - float t = math::clamp(tone, 0.0f, 100.0f) / 100.0f; - - // Chroma affects saturation - // Higher chroma = higher saturation for mid tones - // At very low/high tones, saturation is naturally limited - float chromaFactor = math::clamp(c / 100.0f, 0.0f, 1.5f); - - // Saturation peaks at mid tones - float toneSaturationFactor = 1.0f - std::abs(t - 0.5f) * 2.0f; // 1 at mid, 0 at extremes - toneSaturationFactor = - toneSaturationFactor * toneSaturationFactor; // Square for smoother falloff - - outS = chromaFactor * (0.3f + 0.7f * toneSaturationFactor); - outS = math::clamp(outS, 0.0f, 1.0f); - - // Lightness is primarily tone, but slightly affected by chroma - // Higher chroma colors appear slightly darker at same tone - float chromaDarkening = chromaFactor * 0.05f; - outL = math::clamp(t - chromaDarkening, 0.0f, 1.0f); -} - -// RGB to HCT approximation -void rgbToHct(float r, float g, float b, float& outH, float& outC, float& outT) { - HSL hsl = rgbToHsl(r, g, b); - - // H is same as HSL hue - outH = hsl.h; - - // Tone is primarily lightness, scaled to 0-100 - // Adjust for perceived brightness (green appears brighter than blue) - float perceivedLightness = 0.299f * r + 0.587f * g + 0.114f * b; - outT = perceivedLightness * 100.0f; - - // Chroma is derived from saturation, but adjusted - // At extreme tones, same saturation gives lower perceived chroma - float toneFactor = 1.0f - std::abs(hsl.l - 0.5f) * 1.5f; - toneFactor = math::clamp(toneFactor, 0.2f, 1.0f); - outC = (hsl.s / toneFactor) * 100.0f; - outC = math::clamp(outC, 0.0f, 150.0f); -} - -} // anonymous namespace - -// ============================================================================ -// CFColor Implementation -// ============================================================================ - -CFColor::CFColor(int r, int g, int b) : internal_color(r, g, b) { - // Cache HCT values - float rf = internal_color.redF(); - float gf = internal_color.greenF(); - float bf = internal_color.blueF(); - rgbToHct(rf, gf, bf, m_hue, m_chroma, m_tone); -} - -CFColor::CFColor(const QColor& native) : internal_color(native) { - // Cache HCT values - float r = internal_color.redF(); - float g = internal_color.greenF(); - float b = internal_color.blueF(); - rgbToHct(r, g, b, m_hue, m_chroma, m_tone); -} - -namespace { - -// Helper: parse hex byte string to integer -int parseHexByte(const QString& s, int pos) { - return s.mid(pos, 2).toInt(nullptr, 16); -} - -} // anonymous namespace - -CFColor::CFColor(const char* hex) { - if (!hex) { - // Null pointer, use black - internal_color = Qt::black; - m_hue = 0.0f; - m_chroma = 0.0f; - m_tone = 0.0f; - return; - } - // Convert to QString and use that logic - QString hexStr(hex); - QString trimmed = hexStr.trimmed(); - if (!trimmed.startsWith('#')) { - // Invalid format, use black - internal_color = Qt::black; - m_hue = 0.0f; - m_chroma = 0.0f; - m_tone = 0.0f; - return; - } - - trimmed.remove(0, 1); // Remove '#' - - bool ok; - if (trimmed.length() == 6) { - // #RRGGBB format - internal_color = - QColor(parseHexByte(trimmed, 0), parseHexByte(trimmed, 2), parseHexByte(trimmed, 4)); - } else if (trimmed.length() == 8) { - // #AARRGGBB format - internal_color = QColor(parseHexByte(trimmed, 2), parseHexByte(trimmed, 4), - parseHexByte(trimmed, 6), parseHexByte(trimmed, 0)); - } else { - // Invalid format, use black - internal_color = Qt::black; - } - - // Cache HCT values - float r = internal_color.redF(); - float g = internal_color.greenF(); - float b = internal_color.blueF(); - rgbToHct(r, g, b, m_hue, m_chroma, m_tone); -} - -CFColor::CFColor(const QString& hex) { - QString trimmed = hex.trimmed(); - if (!trimmed.startsWith('#')) { - // Invalid format, use black - internal_color = Qt::black; - m_hue = 0.0f; - m_chroma = 0.0f; - m_tone = 0.0f; - return; - } - - trimmed.remove(0, 1); // Remove '#' - - bool ok; - if (trimmed.length() == 6) { - // #RRGGBB format - internal_color = - QColor(parseHexByte(trimmed, 0), parseHexByte(trimmed, 2), parseHexByte(trimmed, 4)); - } else if (trimmed.length() == 8) { - // #AARRGGBB format - internal_color = QColor(parseHexByte(trimmed, 2), parseHexByte(trimmed, 4), - parseHexByte(trimmed, 6), parseHexByte(trimmed, 0)); - } else { - // Invalid format, use black - internal_color = Qt::black; - } - - // Cache HCT values - float r = internal_color.redF(); - float g = internal_color.greenF(); - float b = internal_color.blueF(); - rgbToHct(r, g, b, m_hue, m_chroma, m_tone); -} - -CFColor::CFColor(float hue, float chroma, float tone) { - m_hue = math::clamp(hue, 0.0f, 360.0f); - m_chroma = math::clamp(chroma, 0.0f, 150.0f); - m_tone = math::clamp(tone, 0.0f, 100.0f); - - // Convert HCT to RGB via HSL - float h, s, l; - hctToHsl(m_hue, m_chroma, m_tone, h, s, l); - - float r, g, b; - hslToRgb(h, s, l, r, g, b); - - internal_color = QColor::fromRgbF(math::clamp(r, 0.0f, 1.0f), math::clamp(g, 0.0f, 1.0f), - math::clamp(b, 0.0f, 1.0f)); -} - -float CFColor::relativeLuminance() const { - // WCAG 2.1 relative luminance formula - float r = toLinear(internal_color.redF()); - float g = toLinear(internal_color.greenF()); - float b = toLinear(internal_color.blueF()); - - return 0.2126f * r + 0.7152f * g + 0.0722f * b; -} - -float CFColor::hue() const { - return m_hue; -} - -float CFColor::chroma() const { - return m_chroma; -} - -float CFColor::tone() const { - return m_tone; -} - -} // namespace cf::ui::base diff --git a/ui/base/color.h b/ui/base/color.h deleted file mode 100644 index 939d788a4..000000000 --- a/ui/base/color.h +++ /dev/null @@ -1,262 +0,0 @@ -/** - * @file ui/base/color.h - * @brief Enhanced color representation with HCT color space support. - * - * Provides the CFColor class which extends QColor with HCT (Hue-Chroma-Tone) - * color space for Material Design 3 color system compatibility. - * - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @date 2026-02-23 - * @version 0.1 - * @since 0.1 - * @ingroup ui - */ - -#pragma once -#include "export.h" -#include -#include - -namespace cf::ui::base { - -/** - * @brief Enhanced color class with HCT color space support. - * - * Extends QColor with HCT (Hue-Chroma-Tone) color space, enabling - * Material Design 3 color theming and dynamic color schemes. - * Maintains both the native QColor representation and cached HCT values. - * - * @note Not thread-safe unless externally synchronized. - * @ingroup ui - * - * @code - * // Create from hex string - * CFColor color("#FF0000"); - * - * // Create from HCT values - * CFColor blue(240.0f, 80.0f, 50.0f); - * - * // Access HCT components - * float h = color.hue(); - * float c = color.chroma(); - * float t = color.tone(); - * @endcode - */ -class CF_UI_EXPORT CFColor { - public: - /** - * @brief Default constructor. - * - * Creates a black color with HCT values (0, 0, 0). - * - * @throws None. - * - * @note None. - * - * @warning None. - * - * @since 0.1 - * @ingroup ui - */ - CFColor() : internal_color(Qt::black) {} - - /** - * @brief Constructs from RGB values. - * - * Creates a CFColor from RGB integer values (0-255 range), - * automatically computing and caching the HCT values. - * - * @param[in] r Red component (0-255). - * @param[in] g Green component (0-255). - * @param[in] b Blue component (0-255). - * - * @throws None. - * - * @note None. - * - * @warning None. - * - * @since 0.1 - * @ingroup ui - */ - CFColor(int r, int g, int b); - - /** - * @brief Constructs from a QColor. - * - * Creates a CFColor from a native QColor, automatically computing - * and caching the HCT values. - * - * @param[in] native Source QColor. - * - * @throws None. - * - * @note None. - * - * @warning None. - * - * @since 0.1 - * @ingroup ui - */ - CFColor(const QColor& native); - - /** - * @brief Constructs from a hexadecimal color string. - * - * Parses a hex string in "#RRGGBB" or "#AARRGGBB" format and creates - * a CFColor. Invalid formats default to black. - * - * @param[in] hex Hex color string. Valid formats: "#RRGGBB", "#AARRGGBB". - * - * @throws None. - * - * @note Explicit to avoid ambiguity with QColor(const char*). - * - * @warning Invalid hex strings produce a black color. - * - * @since 0.1 - * @ingroup ui - */ - explicit CFColor(const QString& hex); - - /** - * @brief Constructs from a C-string hexadecimal color. - * - * Parses a hex string in "#RRGGBB" or "#AARRGGBB" format. - * - * @param[in] hex Hex color string. Valid formats: "#RRGGBB", "#AARRGGBB". - * - * @throws None. - * - * @note None. - * - * @warning Invalid hex strings produce a black color. - * - * @since 0.1 - * @ingroup ui - */ - CFColor(const char* hex); - - /** - * @brief Constructs from HCT color space values. - * - * Creates a color from Hue-Chroma-Tone values, automatically converting - * to RGB and caching the result. Values are clamped to valid ranges. - * - * @param[in] hue Hue component in degrees. Valid range: [0.0, 360.0]. - * @param[in] chroma Chroma component (color intensity). Valid range: [0.0, 150.0]. - * @param[in] tone Tone component (lightness). Valid range: [0.0, 100.0]. - * - * @throws None. - * - * @note Values outside valid ranges are clamped. - * - * @warning None. - * - * @since 0.1 - * @ingroup ui - */ - CFColor(float hue, float chroma, float tone); - - /** - * @brief Computes the WCAG relative luminance. - * - * Returns the relative luminance according to WCAG 2.1 specification, - * used for contrast ratio calculations. Range: [0.0, 1.0]. - * - * @return Relative luminance in range [0.0, 1.0]. - * @throws None - * @note Uses linear RGB conversion with gamma correction. - * @warning None - * @since 0.1 - */ - float relativeLuminance() const; - - /** - * @brief Returns the hue component. - * - * @return Hue in degrees, range [0.0, 360.0]. - * @throws None - * @note None - * @warning None - * @since 0.1 - */ - float hue() const; - - /** - * @brief Returns the chroma component. - * - * @return Chroma (color intensity), range [0.0, 150.0]. - * @throws None - * @note None - * @warning None - * @since 0.1 - */ - float chroma() const; - - /** - * @brief Returns the tone component. - * - * @return Tone (lightness), range [0.0, 100.0]. - * @throws None - * @note None - * @warning None - * @since 0.1 - */ - float tone() const; - - /** - * @brief Returns the native QColor representation. - * - * @return Native QColor. Ownership: observer. - * @throws None - * @note The returned QColor is a copy, not a reference. - * @warning None - * @since 0.1 - */ - QColor native_color() const { return internal_color; } - - /** - * @brief Copy constructor. - * - * @param[in] other Source color. - * @throws None - * @note None - * @warning None - * @since 0.1 - */ - CFColor(const CFColor& other) = default; - - /** - * @brief Copy assignment operator. - * - * Copies both the native QColor and the cached HCT values to ensure - * consistency after assignment. - * - * @param[in] other Source color. - * @return Reference to this color. - * @throws None - * @note Self-assignment is safely handled. - * @warning None - * @since 0.1 - */ - CFColor& operator=(const CFColor& other) { - if (this != &other) { - internal_color = other.internal_color; - m_hue = other.m_hue; - m_chroma = other.m_chroma; - m_tone = other.m_tone; - } - return *this; - } - - private: - QColor internal_color; ///< Native QColor representation. - - // Cached HCT values (computed from RGB in constructors) - float m_hue = 0.0f; ///< Cached hue component. Range: [0.0, 360.0]. - float m_chroma = 0.0f; ///< Cached chroma component. Range: [0.0, 150.0]. - float m_tone = 0.0f; ///< Cached tone component. Range: [0.0, 100.0]. -}; - -} // namespace cf::ui::base diff --git a/ui/base/color_helper.cpp b/ui/base/color_helper.cpp deleted file mode 100644 index 87db85661..000000000 --- a/ui/base/color_helper.cpp +++ /dev/null @@ -1,124 +0,0 @@ -/** - * @file color_helper.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Color helper functions for Material Design 3 - * @version 0.1 - * @date 2026-02-23 - * - * @copyright Copyright (c) 2026 - * - */ -#include "color_helper.h" -#include "math_helper.h" - -namespace cf::ui::base { - -// ============================================================================ -// Helper functions (internal) -// ============================================================================ - -namespace { -// Elevation overlay alpha values for elevation levels 0-5 -// Material Design 3 specification -constexpr float ELEVATION_ALPHA[] = { - 0.00f, // Level 0 - 0.05f, // Level 1 - 0.08f, // Level 2 - 0.11f, // Level 3 - 0.14f, // Level 4 - 0.17f // Level 5 -}; - -// Tonal palette tone values (13 tones from key color) -constexpr float TONAL_VALUES[] = { - 0.0f, // Near black - 10.0f, // Very dark - 20.0f, // Dark - 30.0f, // Dark medium - 40.0f, // Medium dark - 50.0f, // Mid - 60.0f, // Mid light - 70.0f, // Medium light - 80.0f, // Light - 90.0f, // Very light - 95.0f, // Pale - 99.0f, // Near white - 100.0f // Pure white -}; -constexpr int TONAL_COUNT = sizeof(TONAL_VALUES) / sizeof(TONAL_VALUES[0]); - -} // anonymous namespace - -// ============================================================================ -// Color Helper Functions -// ============================================================================ - -CFColor blend(const CFColor& base, CFColor& overlay, float ratio) { - // Clamp ratio to [0, 1] - float t = math::clamp(ratio, 0.0f, 1.0f); - - // Get RGB components - float baseR = base.native_color().redF(); - float baseG = base.native_color().greenF(); - float baseB = base.native_color().blueF(); - float baseA = base.native_color().alphaF(); - - float overlayR = overlay.native_color().redF(); - float overlayG = overlay.native_color().greenF(); - float overlayB = overlay.native_color().blueF(); - float overlayA = overlay.native_color().alphaF(); - - // Linear interpolation for each component - float r = math::lerp(baseR, overlayR, t); - float g = math::lerp(baseG, overlayG, t); - float b = math::lerp(baseB, overlayB, t); - float a = math::lerp(baseA, overlayA, t); - - // Convert to integer RGB for consistent comparison - int rInt = static_cast(r * 255.0f + 0.5f); - int gInt = static_cast(g * 255.0f + 0.5f); - int bInt = static_cast(b * 255.0f + 0.5f); - int aInt = static_cast(a * 255.0f + 0.5f); - - return CFColor(QColor(rInt, gInt, bInt, aInt)); -} - -CFColor elevationOverlay(CFColor& surface, CFColor& primary, int elevation) { - // Clamp elevation to [0, 5] - int level = math::clamp(elevation, 0, 5); - float alpha = ELEVATION_ALPHA[level]; - - // Blend primary color over surface with elevation alpha - return blend(surface, primary, alpha); -} - -float contrastRatio(CFColor& a, CFColor& b) { - // WCAG 2.1 contrast ratio formula - // ratio = (L1 + 0.05) / (L2 + 0.05) - // where L1 is the lighter (higher luminance) and L2 is the darker - - float lumA = a.relativeLuminance(); - float lumB = b.relativeLuminance(); - - float lighter = std::max(lumA, lumB); - float darker = std::min(lumA, lumB); - - return (lighter + 0.05f) / (darker + 0.05f); -} - -QList tonalPalette(CFColor keyColor) { - QList palette; - - // Get HCT values from key color - float hue = keyColor.hue(); - float chroma = keyColor.chroma(); - - // Generate tonal palette by varying tone while keeping hue and chroma constant - for (int i = 0; i < TONAL_COUNT; ++i) { - palette.append(CFColor(hue, chroma, TONAL_VALUES[i])); - } - - return palette; -} - -} // namespace cf::ui::base diff --git a/ui/base/color_helper.h b/ui/base/color_helper.h deleted file mode 100644 index bf5cacd37..000000000 --- a/ui/base/color_helper.h +++ /dev/null @@ -1,78 +0,0 @@ -/** - * @file ui/base/color_helper.h - * @brief Color utility functions for blending, contrast, and tonal palettes. - * - * Provides helper functions for color manipulation including blending, - * elevation overlays, contrast ratio calculation, and tonal palette generation. - * - * @author Charliechen114514 - * @date 2026-02-23 - * @version 0.1 - * @since 0.1 - * @ingroup ui - */ -#pragma once -#include "color.h" -#include "export.h" -#include - -namespace cf::ui::base { - -/** - * @brief Blends two colors with a specified ratio. - * - * @param[in] base The base color to blend. - * @param[in] overlay The overlay color to blend onto the base. - * @param[in] ratio Blending ratio between 0.0 and 1.0. - * @return Blended color result. - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui - */ -CF_UI_EXPORT CFColor blend(const CFColor& base, CFColor& overlay, float ratio); - -/** - * @brief Applies elevation overlay to a surface color. - * - * @param[in] surface The base surface color. - * @param[in] primary The primary color for overlay. - * @param[in] elevation Elevation level in dp units. - * @return Color with elevation overlay applied. - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui - */ -CF_UI_EXPORT CFColor elevationOverlay(CFColor& surface, CFColor& primary, int elevation); - -/** - * @brief Calculates the contrast ratio between two colors. - * - * @param[in] a First color. - * @param[in] b Second color. - * @return Contrast ratio value (typically 1.0 to 21.0). - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui - */ -CF_UI_EXPORT float contrastRatio(CFColor& a, CFColor& b); - -/** - * @brief Generates a tonal palette from a key color. - * - * @param[in] keyColor The base key color for palette generation. - * @return List of generated tonal colors. - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui - */ -CF_UI_EXPORT QList tonalPalette(CFColor keyColor); - -} // namespace cf::ui::base diff --git a/ui/base/device_pixel.cpp b/ui/base/device_pixel.cpp deleted file mode 100644 index f27bf83cc..000000000 --- a/ui/base/device_pixel.cpp +++ /dev/null @@ -1,50 +0,0 @@ -/** - * @file device_pixel.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Device Pixel Ratio Helper for UI Units Conversion - * @version 0.1 - * @date 2026-02-23 - * - * @copyright Copyright (c) 2026 - * - */ -#include "device_pixel.h" - -namespace cf::ui::base { -namespace device { - -CanvasUnitHelper::CanvasUnitHelper(const qreal devicePixelRatio) - : devicePixelRatio(devicePixelRatio) {} - -qreal CanvasUnitHelper::dpToPx(qreal dp) const { - // Density-independent pixel to physical pixel - return dp * devicePixelRatio; -} - -qreal CanvasUnitHelper::spToPx(qreal sp) const { - // Scale-independent pixel to physical pixel (for fonts) - return sp * devicePixelRatio; -} - -qreal CanvasUnitHelper::pxToDp(qreal px) const { - // Physical pixel to density-independent pixel - return devicePixelRatio > 0 ? px / devicePixelRatio : px; -} - -qreal CanvasUnitHelper::dpi() const { - // Standard DPI is 96, scale by device pixel ratio - return 96.0 * devicePixelRatio; -} - -CanvasUnitHelper::BreakPoint CanvasUnitHelper::breakPoint(qreal widthDp) { - if (widthDp < 600.0) { - return BreakPoint::Compact; - } - if (widthDp < 840.0) { - return BreakPoint::Medium; - } - return BreakPoint::Expanded; -} - -} // namespace device -} // namespace cf::ui::base diff --git a/ui/base/device_pixel.h b/ui/base/device_pixel.h deleted file mode 100644 index 06006bb34..000000000 --- a/ui/base/device_pixel.h +++ /dev/null @@ -1,137 +0,0 @@ -/** - * @file ui/base/device_pixel.h - * @brief Device pixel ratio conversion utilities for canvas units. - * - * Provides helper structures and functions for converting between device-independent - * pixels (dp), scalable pixels (sp), and physical pixels based on device pixel ratio. - * - * @author Charliechen114514 - * @date 2026-02-23 - * @version 0.1 - * @since 0.1 - * @ingroup ui - */ -#pragma once -#include "export.h" -#include - -namespace cf::ui::base { -namespace device { - -/** - * @brief Helper for converting between canvas units and device pixels. - * - * Provides conversion functions for device-independent pixels (dp), - * scalable pixels (sp), and physical pixels based on the device pixel ratio. - * - * @note Not thread-safe unless externally synchronized. - * - * @code - * CanvasUnitHelper helper(2.0); - * qreal pixels = helper.dpToPx(16.0); // Converts 16dp to pixels - * @endcode - * - * @ingroup ui - */ -struct CF_UI_EXPORT CanvasUnitHelper { - /** - * @brief Constructs a CanvasUnitHelper with the specified device pixel ratio. - * - * @param[in] devicePixelRatio The device pixel ratio (e.g., 1.0, 2.0, 3.0). - * - * @return None (constructor). - * - * @throws None. - * - * @note None. - * - * @warning None. - * - * @since 0.1 - * @ingroup ui - */ - CanvasUnitHelper(const qreal devicePixelRatio); - - /** - * @brief Converts device-independent pixels to physical pixels. - * - * @param[in] dp Value in device-independent pixels. - * @return Value in physical pixels. - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui - */ - qreal dpToPx(qreal dp) const; - - /** - * @brief Converts scalable pixels to physical pixels. - * - * @param[in] sp Value in scalable pixels. - * @return Value in physical pixels. - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui - */ - qreal spToPx(qreal sp) const; - - /** - * @brief Converts physical pixels to device-independent pixels. - * - * @param[in] px Value in physical pixels. - * @return Value in device-independent pixels. - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui - */ - qreal pxToDp(qreal px) const; - - /** - * @brief Returns the device pixel ratio. - * - * @return The device pixel ratio. - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui - */ - qreal dpi() const; - - /** - * @brief Window width breakpoints for responsive layouts. - * - * Defines breakpoints for different screen size categories - * following Material Design guidelines. - * - * @ingroup ui - */ - enum class BreakPoint { - Compact, ///< Compact width: < 600dp - Medium, ///< Medium width: 600dp - 839dp - Expanded ///< Expanded width: >= 840dp - }; - - /** - * @brief Determines the breakpoint category for a given width. - * - * @param[in] widthDp Width in device-independent pixels. - * @return The breakpoint category for the specified width. - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui - */ - BreakPoint breakPoint(qreal widthDp); - - private: - qreal devicePixelRatio; ///< Device pixel ratio used for conversions. -}; -} // namespace device -} // namespace cf::ui::base diff --git a/ui/base/easing.cpp b/ui/base/easing.cpp deleted file mode 100644 index 82774a4ed..000000000 --- a/ui/base/easing.cpp +++ /dev/null @@ -1,77 +0,0 @@ -/** - * @file easing.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design Easing Curves - * @version 0.1 - * @date 2026-02-23 - * - * @copyright Copyright (c) 2026 - * - */ -#include "easing.h" -#include - -namespace cf::ui::base { -namespace Easing { - -QEasingCurve fromEasingType(const Type t) { - switch (t) { - case Type::Emphasized: - // cubic-bezier(0.2, 0, 0, 1.0) 进出均平滑 - return custom(0.2f, 0.0f, 0.0f, 1.0f); - - case Type::EmphasizedDecelerate: - // cubic-bezier(0.05, 0.7, 0.1, 1.0) 入场动画 - return custom(0.05f, 0.7f, 0.1f, 1.0f); - - case Type::EmphasizedAccelerate: - // cubic-bezier(0.3, 0, 0.8, 0.15) 离场动画 - return custom(0.3f, 0.0f, 0.8f, 0.15f); - - case Type::Standard: - // cubic-bezier(0.2, 0, 0, 1.0) - return custom(0.2f, 0.0f, 0.0f, 1.0f); - - case Type::StandardDecelerate: - // cubic-bezier(0, 0, 0, 1.0) - return custom(0.0f, 0.0f, 0.0f, 1.0f); - - case Type::StandardAccelerate: - // cubic-bezier(0.3, 0, 1, 1) - return custom(0.3f, 0.0f, 1.0f, 1.0f); - - case Type::Linear: - // cubic-bezier(0, 0, 1, 1) - return custom(0.0f, 0.0f, 1.0f, 1.0f); - - default: - return custom(0.2f, 0.0f, 0.0f, 1.0f); - } -} - -QEasingCurve custom(float x1, float y1, float x2, float y2) { - // QEasingCurve::BezierType requires control points as std::pair - // The cubic bezier curve is defined with P0=(0,0), P1=(x1,y1), P2=(x2,y2), P3=(1,1) - QEasingCurve curve(QEasingCurve::BezierSpline); - curve.addCubicBezierSegment(QPointF(x1, y1), QPointF(x2, y2), QPointF(1.0, 1.0)); - return curve; -} - -SpringPreset springGentle() { - // Gentle spring: 低刚度,中等阻尼,用于轻微的弹性效果 - return {120.0f, 20.0f}; -} - -SpringPreset springBouncy() { - // Bouncy spring: 中等刚度,低阻尼,产生明显的回弹效果 - return {200.0f, 10.0f}; -} - -SpringPreset springStiff() { - // Stiff spring: 高刚度,高阻尼,快速响应无回弹 - return {400.0f, 30.0f}; -} - -} // namespace Easing - -} // namespace cf::ui::base diff --git a/ui/base/easing.h b/ui/base/easing.h deleted file mode 100644 index e862d6de4..000000000 --- a/ui/base/easing.h +++ /dev/null @@ -1,128 +0,0 @@ -/** - * @file ui/base/easing.h - * @brief Easing curves and spring animation presets. - * - * Provides convenient access to predefined easing curves and spring - * physics presets for smooth animations following Material Design motion - * principles. - * - * @author Charliechen114514 - * @date 2026-02-23 - * @version 0.1 - * @since 0.1 - * @ingroup ui - */ -#pragma once -#include "export.h" -#include - -namespace cf::ui::base { -/** - * @brief Easing curves and spring presets for animations. - * - * Provides convenient access to predefined easing curves following - * Material Design motion principles. - * - * @ingroup ui - */ -namespace Easing { - -/** - * @brief Predefined easing curve types. - * - * Defines standard easing curves used for animations following - * Material Design motion specifications. - * - * @ingroup ui - */ -enum class Type { - Emphasized, ///< Emphasized easing with acceleration and deceleration. - EmphasizedDecelerate, ///< Emphasized deceleration easing. - EmphasizedAccelerate, ///< Emphasized acceleration easing. - Standard, ///< Standard easing with acceleration and deceleration. - StandardDecelerate, ///< Standard deceleration easing. - StandardAccelerate, ///< Standard acceleration easing. - Linear ///< Linear easing (no acceleration). -}; - -/** - * @brief Converts an easing type to a QEasingCurve. - * - * @param[in] t The easing type to convert. - * @return QEasingCurve corresponding to the specified type. - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui - */ -CF_UI_EXPORT QEasingCurve fromEasingType(const Type t); - -/** - * @brief Creates a custom cubic bezier easing curve. - * - * @param[in] x1 First control point X coordinate (0.0 to 1.0). - * @param[in] y1 First control point Y coordinate. - * @param[in] x2 Second control point X coordinate (0.0 to 1.0). - * @param[in] y2 Second control point Y coordinate. - * @return Custom QEasingCurve with the specified control points. - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui - */ -CF_UI_EXPORT QEasingCurve custom(float x1, float y1, float x2, float y2); - -/** - * @brief Spring physics preset for animations. - * - * Defines stiffness and damping parameters for spring-based - * animations. - * - * @ingroup ui - */ -struct CF_UI_EXPORT SpringPreset { - float stiffness; ///< Spring stiffness coefficient. - float damping; ///< Spring damping coefficient. -}; - -/** - * @brief Returns a gentle spring preset. - * - * @return SpringPreset with gentle stiffness and damping. - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui - */ -CF_UI_EXPORT SpringPreset springGentle(); - -/** - * @brief Returns a bouncy spring preset. - * - * @return SpringPreset with high bounciness. - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui - */ -CF_UI_EXPORT SpringPreset springBouncy(); - -/** - * @brief Returns a stiff spring preset. - * - * @return SpringPreset with high stiffness. - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui - */ -CF_UI_EXPORT SpringPreset springStiff(); - -} // namespace Easing - -} // namespace cf::ui::base diff --git a/ui/base/geometry_helper.cpp b/ui/base/geometry_helper.cpp deleted file mode 100644 index d21b51f89..000000000 --- a/ui/base/geometry_helper.cpp +++ /dev/null @@ -1,126 +0,0 @@ -/** - * @file geometry_helper.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Geometry Helpers for UI Widgets (Rounded Rects) - * @version 0.1 - * @date 2026-02-23 - * - * @copyright Copyright (c) 2026 - * - */ -#include "geometry_helper.h" -#include - -namespace cf::ui::base { -namespace geometry { - -// Shape scale to dp radius mapping (Material Design specification) -namespace { -constexpr float SHAPE_NONE_RADIUS = 0.0f; -constexpr float SHAPE_EXTRA_SMALL_RADIUS = 4.0f; -constexpr float SHAPE_SMALL_RADIUS = 8.0f; -constexpr float SHAPE_MEDIUM_RADIUS = 12.0f; -constexpr float SHAPE_LARGE_RADIUS = 16.0f; -constexpr float SHAPE_EXTRA_LARGE_RADIUS = 28.0f; -} // namespace - -static float radiusForScale(ShapeScale scale) { - switch (scale) { - case ShapeScale::ShapeNone: - return SHAPE_NONE_RADIUS; - case ShapeScale::ShapeExtraSmall: - return SHAPE_EXTRA_SMALL_RADIUS; - case ShapeScale::ShapeSmall: - return SHAPE_SMALL_RADIUS; - case ShapeScale::ShapeMedium: - return SHAPE_MEDIUM_RADIUS; - case ShapeScale::ShapeLarge: - return SHAPE_LARGE_RADIUS; - case ShapeScale::ShapeExtraLarge: - return SHAPE_EXTRA_LARGE_RADIUS; - case ShapeScale::ShapeFull: - return 0.0f; // Handled separately as percentage - } - return 0.0f; -} - -QPainterPath roundedRect(const QRectF& rect, ShapeScale scale) { - QPainterPath path; - - if (scale == ShapeScale::ShapeFull) { - // Full rounded: capsule/circle shape (50% of height) - float radius = static_cast(rect.height()) * 0.5f; - path.addRoundedRect(rect, radius, radius); - } else { - float radius = radiusForScale(scale); - path.addRoundedRect(rect, radius, radius); - } - - return path; -} - -QPainterPath roundedRect(const QRectF& rect, float radius) { - QPainterPath path; - path.addRoundedRect(rect, radius, radius); - return path; -} - -QPainterPath roundedRect(const QRectF& rect, float topLeft, float topRight, float bottomLeft, - float bottomRight) { - // Custom rounded rect with asymmetric corner radii - QPainterPath path; - - float w = static_cast(rect.width()); - float h = static_cast(rect.height()); - float x = static_cast(rect.x()); - float y = static_cast(rect.y()); - - // Start from top-left edge (after corner arc) - path.moveTo(x + topLeft, y); - - // Top edge - path.lineTo(x + w - topRight, y); - - // Top-right corner - if (topRight > 0) { - path.quadTo(x + w, y, x + w, y + topRight); - } else { - path.lineTo(x + w, y); - } - - // Right edge - path.lineTo(x + w, y + h - bottomRight); - - // Bottom-right corner - if (bottomRight > 0) { - path.quadTo(x + w, y + h, x + w - bottomRight, y + h); - } else { - path.lineTo(x + w, y + h); - } - - // Bottom edge - path.lineTo(x + bottomLeft, y + h); - - // Bottom-left corner - if (bottomLeft > 0) { - path.quadTo(x, y + h, x, y + h - bottomLeft); - } else { - path.lineTo(x, y + h); - } - - // Left edge - path.lineTo(x, y + topLeft); - - // Top-left corner - if (topLeft > 0) { - path.quadTo(x, y, x + topLeft, y); - } else { - path.lineTo(x, y); - } - - path.closeSubpath(); - return path; -} - -} // namespace geometry -} // namespace cf::ui::base diff --git a/ui/base/geometry_helper.h b/ui/base/geometry_helper.h deleted file mode 100644 index 6b72a3aa9..000000000 --- a/ui/base/geometry_helper.h +++ /dev/null @@ -1,86 +0,0 @@ -/** - * @file ui/base/geometry_helper.h - * @brief Geometry helpers for creating rounded rectangles. - * - * Provides utilities for creating rounded rectangles with various - * corner radius options following Material Design shape scales. - * - * @author Charliechen114514 - * @date 2026-02-23 - * @version 0.1 - * @since 0.1 - * @ingroup ui - */ -#pragma once -#include "export.h" -#include - -namespace cf::ui::base { -namespace geometry { - -/** - * @brief Material Design shape scale categories. - * - * Defines standard corner radius values following Material Design - * shape specifications. - * - * @ingroup ui - */ -enum class ShapeScale { - ShapeNone, ///< No rounding (0dp). - ShapeExtraSmall, ///< Extra small rounding (4dp). - ShapeSmall, ///< Small rounding (8dp). - ShapeMedium, ///< Medium rounding (12dp). - ShapeLarge, ///< Large rounding (16dp). - ShapeExtraLarge, ///< Extra large rounding (28dp). - ShapeFull ///< Full rounding (50% of size). -}; - -/** - * @brief Creates a rounded rectangle path with Material Design scale. - * - * @param[in] rect The base rectangle to round. - * @param[in] scale The Material Design shape scale for corner radius. - * @return QPainterPath with rounded rectangle. - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui - */ -CF_UI_EXPORT QPainterPath roundedRect(const QRectF& rect, ShapeScale scale); - -/** - * @brief Creates a rounded rectangle path with uniform corner radius. - * - * @param[in] rect The base rectangle to round. - * @param[in] radius Uniform corner radius in pixels. - * @return QPainterPath with rounded rectangle. - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui - */ -CF_UI_EXPORT QPainterPath roundedRect(const QRectF& rect, float radius); - -/** - * @brief Creates a rounded rectangle path with individual corner radii. - * - * @param[in] rect The base rectangle to round. - * @param[in] topLeft Top-left corner radius in pixels. - * @param[in] topRight Top-right corner radius in pixels. - * @param[in] bottomLeft Bottom-left corner radius in pixels. - * @param[in] bottomRight Bottom-right corner radius in pixels. - * @return QPainterPath with rounded rectangle. - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui - */ -CF_UI_EXPORT QPainterPath roundedRect(const QRectF& rect, float topLeft, float topRight, - float bottomLeft, float bottomRight); - -} // namespace geometry -} // namespace cf::ui::base diff --git a/ui/base/math_helper.cpp b/ui/base/math_helper.cpp deleted file mode 100644 index cfead2192..000000000 --- a/ui/base/math_helper.cpp +++ /dev/null @@ -1,136 +0,0 @@ -/** - * @file math_helper.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Math Helpers for the UI Widgets - * @version 0.1 - * @date 2026-02-23 - * - * @copyright Copyright (c) 2026 - * - */ -#include "math_helper.h" -#include - -namespace cf::ui { -namespace math { - -float lerp(float a, float b, float t) { - return a + t * (b - a); -} - -float clamp(float value, float min, float max) { - if (value < min) - return min; - if (value > max) - return max; - return value; -} - -float remap(float value, float inMin, float inMax, float outMin, float outMax) { - // Handle edge case where input range is zero - // When input range is zero, value at or below min returns output max, value above min returns - // output min - if (inMax == inMin) { - return (value <= inMin) ? outMax : outMin; - } - return outMin + (value - inMin) * (outMax - outMin) / (inMax - inMin); -} - -float cubicBezier(float x1, float y1, float x2, float y2, float t) { - // For proper CSS cubic-bezier behavior, we need to solve x(t) = input_time - // and return y(t). This is a simplified implementation that handles - // common cases correctly. - - // Special case: linear bezier - returns input directly - if (std::abs(x1 - y1) < 0.001f && std::abs(x2 - y2) < 0.001f) { - return t; - } - - // For other cases, use Newton's method to solve for the parameter - // that gives x(t) = input, then return y(t) - // Start with initial guess equal to input - float guess = t; - - // Newton iteration: guess_{n+1} = guess_n - (x(guess_n) - t) / x'(guess_n) - for (int i = 0; i < 8; ++i) { - float mt = 1.0f - guess; - float mt2 = mt * mt; - float t2 = guess * guess; - - // x(guess) = 3(1-t)²t*x1 + 3(1-t)t²*x2 + t³ - float x_guess = 3.0f * mt2 * guess * x1 + 3.0f * mt * t2 * x2 + guess * t2; - - // x'(guess) = 3(1-t)²*x1 + 6(1-t)t*(x2-x1) + 3t²*(1-x2) - float x_derivative = - 3.0f * mt2 * x1 + 6.0f * mt * guess * (x2 - x1) + 3.0f * t2 * (1.0f - x2); - - float delta = (x_guess - t) / x_derivative; - guess -= delta; - - if (std::abs(delta) < 0.0001f) { - break; - } - } - - // Clamp guess to [0, 1] - if (guess < 0.0f) - guess = 0.0f; - if (guess > 1.0f) - guess = 1.0f; - - // Compute y at the solved parameter - float mt = 1.0f - guess; - float mt2 = mt * mt; - float t2 = guess * guess; - - float y = 3.0f * mt2 * guess * y1 + 3.0f * mt * t2 * y2 + guess * t2; - return y; -} - -std::pair springStep(float position, float velocity, float target, float stiffness, - float damping, float dt) { - // Damped spring using semi-implicit Euler integration - float acceleration = stiffness * (target - position) - damping * velocity; - float newVelocity = velocity + acceleration * dt; - float newPosition = position + newVelocity * dt; - return {newPosition, newVelocity}; -} - -float lerpAngle(float a, float b, float t) { - constexpr float PI = 3.14159265358979323846f; - constexpr float TWO_PI = 2.0f * PI; - constexpr float DEG_TO_RAD = PI / 180.0f; - constexpr float RAD_TO_DEG = 180.0f / PI; - - float aRad = a * DEG_TO_RAD; - float bRad = b * DEG_TO_RAD; - - // Calculate the shortest path difference from a to b - float diff = bRad - aRad; - - // Normalize to [-PI, PI] - while (diff <= -PI) - diff += TWO_PI; - while (diff > PI) - diff -= TWO_PI; - - // Handle the exact 180 degree case - prefer positive direction - if (std::abs(diff - PI) < 0.0001f) { - diff = PI; - } else if (std::abs(diff + PI) < 0.0001f) { - diff = PI; - } - - float result = aRad + diff * t; - - // Normalize result to [0, TWO_PI) - while (result < 0) - result += TWO_PI; - while (result >= TWO_PI) - result -= TWO_PI; - - return result * RAD_TO_DEG; -} - -} // namespace math -} // namespace cf::ui \ No newline at end of file diff --git a/ui/base/math_helper.h b/ui/base/math_helper.h deleted file mode 100644 index 5814bc034..000000000 --- a/ui/base/math_helper.h +++ /dev/null @@ -1,162 +0,0 @@ -/** - * @file ui/base/math_helper.h - * @brief Mathematical utility functions for UI animations and transitions. - * - * Provides interpolation, clamping, remapping, cubic Bezier curves, - * spring physics simulation, and angle interpolation functions for UI components. - * - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @date 2026-02-23 - * @version 0.1 - * @since 0.1 - * @ingroup ui - */ - -#pragma once -#include "export.h" -#include - -namespace cf::ui { - -/** - * @brief Mathematical utilities for UI animations and transitions. - * - * Provides functions for value interpolation, clamping, remapping, - * cubic Bezier easing curves, spring physics simulation, and angle - * interpolation with boundary handling. - * - * @note None of these functions perform thread synchronization. - * @ingroup ui - */ -namespace math { - -/** - * @brief Performs linear interpolation between two values. - * - * Computes the linear interpolation between `a` and `b` using parameter `t`. - * When `t` is 0, returns `a`; when `t` is 1, returns `b`. Values outside - * [0, 1] produce extrapolated results. - * - * @param[in] a Start value. - * @param[in] b End value. - * @param[in] t Interpolation parameter. Valid range: [0.0, 1.0] for normal - * interpolation; values outside produce extrapolation. - * @return Interpolated value between `a` and `b`. - * @throws None - * @note Commonly used for smooth transitions and animations. - * @warning No bounds checking on `t`; extrapolation occurs for t < 0 or t > 1. - * @since 0.1 - * @ingroup ui - */ -CF_UI_EXPORT float lerp(float a, float b, float t); - -/** - * @brief Clamps a value to the specified range. - * - * Constrains `value` to the inclusive range [`min`, `max`]. If `value` - * is less than `min`, returns `min`; if greater than `max`, returns `max`. - * - * @param[in] value Value to clamp. - * @param[in] min Minimum bound (inclusive). - * @param[in] max Maximum bound (inclusive). - * @return Clamped value in range [`min`, `max`]. - * @throws None - * @note This function does not validate that min <= max. - * @warning Behavior is undefined if `min` > `max`. - * @since 0.1 - * @ingroup ui - */ -CF_UI_EXPORT float clamp(float value, float min, float max); - -/** - * @brief Remaps a value from one range to another. - * - * Transforms `value` from the input range [`inMin`, `inMax`] to the - * output range [`outMin`, `outMax`]. Preserves relative position within - * each range. - * - * @param[in] value Input value to remap. - * @param[in] inMin Input range minimum. - * @param[in] inMax Input range maximum. - * @param[in] outMin Output range minimum. - * @param[in] outMax Output range maximum. - * @return Remapped value in output range. - * @throws None - * @note If `inMin == inMax`, returns `outMax` when value >= inMin, - * otherwise returns `outMin`. - * @warning Behavior is undefined if `inMin == inMax` and `value` is NaN. - * @since 0.1 - * @ingroup ui - */ -CF_UI_EXPORT float remap(float value, float inMin, float inMax, float outMin, float outMax); - -/** - * @brief Evaluates a cubic Bezier easing curve. - * - * Computes the y-value at parameter `t` for a cubic Bezier curve with - * fixed endpoints P0=(0,0) and P3=(1,1), and control points P1=(x1,y1) - * and P2=(x2,y2). Used for custom easing functions in animations. - * - * @param[in] x1 First control point X coordinate. Valid range: [0.0, 1.0]. - * @param[in] y1 First control point Y coordinate. No range restriction. - * @param[in] x2 Second control point X coordinate. Valid range: [0.0, 1.0]. - * @param[in] y2 Second control point Y coordinate. No range restriction. - * @param[in] t Curve parameter. Valid range: [0.0, 1.0]. - * @return Y-value along the Bezier curve at parameter `t`. - * @throws None - * @note This is the explicit formula for y(t), not a solver for x(t). - * For animation easing, typically assumes x(t) ≈ t. - * @warning Values of `t` outside [0.0, 1.0] produce extrapolated results. - * @since 0.1 - * @ingroup ui - */ -CF_UI_EXPORT float cubicBezier(float x1, float y1, float x2, float y2, float t); - -/** - * @brief Performs a single step of spring physics simulation. - * - * Simulates one time step of a damped spring using semi-implicit Euler - * integration. Returns the new position and velocity after applying - * spring forces toward the target. - * - * @param[in] position Current position. - * @param[in] velocity Current velocity. - * @param[in] target Target position the spring pulls toward. - * @param[in] stiffness Spring stiffness coefficient. Higher values create - * stronger, faster springs. Typical range: 100-500. - * @param[in] damping Spring damping coefficient. Lower values create more - * oscillation. Typical range: 5-30. - * @param[in] dt Time step in seconds. Typical value: 1/60 (60 FPS). - * @return Pair containing [new_position, new_velocity]. - * @throws None - * @note Stability requires `dt` to be small relative to - * stiffness. Large `stiffness` * `dt` may cause instability. - * @warning Negative `stiffness` creates an unstable (repulsive) spring. - * @since 0.1 - * @ingroup ui - */ -CF_UI_EXPORT std::pair springStep(float position, float velocity, float target, - float stiffness, float damping, float dt); - -/** - * @brief Performs linear interpolation between two angles. - * - * Interpolates between angles `a` and `b` using parameter `t`, handling - * the wraparound at 0°/360°. Always takes the shortest path around the - * circle, whether clockwise or counter-clockwise. - * - * @param[in] a Start angle in degrees. No range restriction. - * @param[in] b End angle in degrees. No range restriction. - * @param[in] t Interpolation parameter. Valid range: [0.0, 1.0]. - * @return Interpolated angle in degrees. - * @throws None - * @note The result is normalized to [0, 360) range. - * @warning None - * @since 0.1 - * @ingroup ui - */ -CF_UI_EXPORT float lerpAngle(float a, float b, float t); - -} // namespace math - -} // namespace cf::ui diff --git a/ui/base/qt_format.h b/ui/base/qt_format.h deleted file mode 100644 index 9bc9f3518..000000000 --- a/ui/base/qt_format.h +++ /dev/null @@ -1,201 +0,0 @@ -/** - * @file ui/base/qt_format.h - * @brief std::formatter specializations for Qt geometry types. - * - * Provides formatters for QPoint, QPointF, QSize, QSizeF, QRect, QRectF, - * QLine, QLineF, QMargins, and QMarginsF so they can be used directly - * with std::format and the project's logging macros (log::traceftag, etc.). - * - * @author Charliechen114514 - * @date 2026-03-31 - * @version 0.14 - * @since 0.14 - * @ingroup ui - */ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -// ============================================================================ -// QString -// ============================================================================ - -/** - * @brief std::formatter specialization for QString. - * - * Formats a QString by converting to UTF-8 std::string. - * - * @ingroup ui - */ -template <> struct std::formatter : std::formatter { - auto format(const QString& s, std::format_context& ctx) const { - return std::formatter::format(s.toStdString(), ctx); - } -}; - -// ============================================================================ -// Point types -// ============================================================================ - -/** - * @brief std::formatter specialization for QPoint. - * - * Formats a QPoint as "QPoint(x, y)". - * - * @ingroup ui - */ -template <> struct std::formatter : std::formatter { - auto format(const QPoint& p, std::format_context& ctx) const { - return std::formatter::format(std::format("QPoint({}, {})", p.x(), p.y()), - ctx); - } -}; - -/** - * @brief std::formatter specialization for QPointF. - * - * Formats a QPointF as "QPointF(x, y)". - * - * @ingroup ui - */ -template <> struct std::formatter : std::formatter { - auto format(const QPointF& p, std::format_context& ctx) const { - return std::formatter::format(std::format("QPointF({}, {})", p.x(), p.y()), - ctx); - } -}; - -// ============================================================================ -// Size types -// ============================================================================ - -/** - * @brief std::formatter specialization for QSize. - * - * Formats a QSize as "QSize(width, height)". - * - * @ingroup ui - */ -template <> struct std::formatter : std::formatter { - auto format(const QSize& s, std::format_context& ctx) const { - return std::formatter::format( - std::format("QSize({}, {})", s.width(), s.height()), ctx); - } -}; - -/** - * @brief std::formatter specialization for QSizeF. - * - * Formats a QSizeF as "QSizeF(width, height)". - * - * @ingroup ui - */ -template <> struct std::formatter : std::formatter { - auto format(const QSizeF& s, std::format_context& ctx) const { - return std::formatter::format( - std::format("QSizeF({}, {})", s.width(), s.height()), ctx); - } -}; - -// ============================================================================ -// Rectangle types -// ============================================================================ - -/** - * @brief std::formatter specialization for QRect. - * - * Formats a QRect as "QRect(x, y, width, height)". - * - * @ingroup ui - */ -template <> struct std::formatter : std::formatter { - auto format(const QRect& r, std::format_context& ctx) const { - return std::formatter::format( - std::format("QRect({}, {}, {}, {})", r.x(), r.y(), r.width(), r.height()), ctx); - } -}; - -/** - * @brief std::formatter specialization for QRectF. - * - * Formats a QRectF as "QRectF(x, y, width, height)". - * - * @ingroup ui - */ -template <> struct std::formatter : std::formatter { - auto format(const QRectF& r, std::format_context& ctx) const { - return std::formatter::format( - std::format("QRectF({}, {}, {}, {})", r.x(), r.y(), r.width(), r.height()), ctx); - } -}; - -// ============================================================================ -// Line types -// ============================================================================ - -/** - * @brief std::formatter specialization for QLine. - * - * Formats a QLine as "QLine(x1, y1, x2, y2)". - * - * @ingroup ui - */ -template <> struct std::formatter : std::formatter { - auto format(const QLine& l, std::format_context& ctx) const { - return std::formatter::format( - std::format("QLine({}, {}, {}, {})", l.x1(), l.y1(), l.x2(), l.y2()), ctx); - } -}; - -/** - * @brief std::formatter specialization for QLineF. - * - * Formats a QLineF as "QLineF(x1, y1, x2, y2)". - * - * @ingroup ui - */ -template <> struct std::formatter : std::formatter { - auto format(const QLineF& l, std::format_context& ctx) const { - return std::formatter::format( - std::format("QLineF({}, {}, {}, {})", l.x1(), l.y1(), l.x2(), l.y2()), ctx); - } -}; - -// ============================================================================ -// Margin types -// ============================================================================ - -/** - * @brief std::formatter specialization for QMargins. - * - * Formats a QMargins as "QMargins(left, top, right, bottom)". - * - * @ingroup ui - */ -template <> struct std::formatter : std::formatter { - auto format(const QMargins& m, std::format_context& ctx) const { - return std::formatter::format( - std::format("QMargins({}, {}, {}, {})", m.left(), m.top(), m.right(), m.bottom()), ctx); - } -}; - -/** - * @brief std::formatter specialization for QMarginsF. - * - * Formats a QMarginsF as "QMarginsF(left, top, right, bottom)". - * - * @ingroup ui - */ -template <> struct std::formatter : std::formatter { - auto format(const QMarginsF& m, std::format_context& ctx) const { - return std::formatter::format( - std::format("QMarginsF({}, {}, {}, {})", m.left(), m.top(), m.right(), m.bottom()), - ctx); - } -}; diff --git a/ui/components/CMakeLists.txt b/ui/components/CMakeLists.txt deleted file mode 100644 index 4a9ee210f..000000000 --- a/ui/components/CMakeLists.txt +++ /dev/null @@ -1,34 +0,0 @@ -project(ui_components LANGUAGES CXX) - -log_info("UI_Components" "Start Configure the UI Components") - -# ============================================================ -# Base Animation Library - STATIC library for cfui.dll -# Contains base animation interfaces and implementations -# ============================================================ - -add_library(cf_ui_components STATIC - animation.cpp - animation_factory_manager.cpp - timing_animation.cpp -) - -# Add include directories -target_include_directories(cf_ui_components PUBLIC - $ - $ -) - -# Link Qt libraries -target_link_libraries(cf_ui_components PUBLIC - Qt6::Core - Qt6::Gui - CFDesktop::base_headers - cf_ui_base -) - -# Include subdirectory CMakeLists (STATIC libraries) -# Material depends on cf_ui_components, so it must come after -add_subdirectory(material) - -log_info("UI_Components" "the UI Components Configuration Done") diff --git a/ui/components/animation.cpp b/ui/components/animation.cpp deleted file mode 100644 index 5f19b84c9..000000000 --- a/ui/components/animation.cpp +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @file animation.cpp - * @brief Implementation file for ICFAbstractAnimation to enable MOC processing - * - * This file exists to trigger Qt's Meta-Object Compiler (MOC) for the - * ICFAbstractAnimation class which uses Q_OBJECT. - */ - -#include "animation.h" - -namespace cf::ui::components { - -// ============================================================================= -// Protected Methods -// ============================================================================= - -void ICFAbstractAnimation::setTargetFps(float fps) { - if (fps > 0.0f) { - targetFps_ = fps; - } -} - -int ICFAbstractAnimation::calculateInterval() const { - return static_cast(1000.0f / targetFps_); -} - -// ============================================================================= -// Constructor / Destructor -// ============================================================================= - -ICFAbstractAnimation::ICFAbstractAnimation(QObject* parent) : QObject(parent) { - driven_internal_timer = new QTimer(this); - // Connect timer timeout to tick slot for driving animation updates - connect(driven_internal_timer, &QTimer::timeout, this, [this]() { - if (m_state == State::Running) { - tick(calculateInterval()); - } - }); -} - -} // namespace cf::ui::components diff --git a/ui/components/animation.h b/ui/components/animation.h deleted file mode 100644 index 108bf0e35..000000000 --- a/ui/components/animation.h +++ /dev/null @@ -1,267 +0,0 @@ -/** - * @file animation.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Abstract Animation Interface - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Defines the base animation interface for all animation types. - * Provides common animation states, direction control, and lifecycle methods. - * - * @ingroup ui_components - */ -#pragma once - -#include "aex/weak_ptr/weak_ptr.h" -#include "export.h" -#include -#include - -namespace cf::ui::components { - -/** - * @brief Abstract animation base class. - * - * @details Provides the common interface for all animations including - * state management, direction control, and lifecycle methods. - * - * @since 0.1 - * @ingroup ui_components - */ -class CF_UI_EXPORT ICFAbstractAnimation : public QObject { - Q_OBJECT - public: - friend class ICFAnimationManagerFactory; - - explicit ICFAbstractAnimation(QObject* parent = nullptr); - - /** - * @brief Animation states. - * - * @since 0.1 - * @ingroup ui_components - */ - enum class State { Idle, Running, Paused, Finished }; - Q_ENUM(State) - - /** - * @brief Animation playback direction. - * - * @since 0.1 - * @ingroup ui_components - */ - enum class Direction { Forward, Backward }; - Q_ENUM(Direction) - - /** - * @brief Starts the animation in the specified direction. - * - * @param[in] dir Direction to play the animation (default: Forward). - * - * @throws None - * @note If already running, this may restart the animation. - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - virtual void start(Direction dir = Direction::Forward) = 0; - - /** - * @brief Pauses the animation. - * - * @throws None - * @note Does nothing if the animation is not running. - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - virtual void pause() = 0; - - /** - * @brief Stops the animation and resets to initial state. - * - * @throws None - * @note Emits the stopped signal. - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - virtual void stop() = 0; - - /** - * @brief Reverses the animation direction. - * - * @details Stops the current session and plays in opposite direction. - * - * @throws None - * @note Emits the reversed signal. - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - virtual void reverse() = 0; - - /** - * @brief Updates the animation state. - * - * @details Called every frame by the animator. Subclasses implement - * specific interpolation logic. - * - * @param[in] dt Time interval since the last call (milliseconds). - * - * @return true if animation continues, false if finished. - * - * @throws None - * @note Implementations should update m_progress and apply - * the interpolated value to the target. - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - virtual bool tick(int dt) = 0; - - /** - * @brief Gets a weak pointer to this animation. - * - * @details Each concrete animation class must implement this using - * its aex::WeakPtrFactory. - * - * @return aex::WeakPtr to this animation. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - virtual aex::WeakPtr GetWeakPtr() = 0; - - /** - * @brief Gets the enabled state of the animation. - * - * @return true if animation is enabled, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - bool getEnabled() const { return enabled; } - - /** - * @brief Set the target FPS for this animation. - * - * @details Sets the desired frame rate for this animation's timer updates. - * This affects the tick interval for this animation instance. - * - * @param[in] fps Target frames per second (e.g., 60.0f). - * - * @throws None - * @note Default is 60.0f. Takes effect on the next animation start. - * @warning None - * @since 0.1 - * @ingroup ui_components - * - * @code - * animation->setTargetFps(30.0f); // 30 FPS (lower CPU usage) - * @endcode - */ - void setTargetFps(float fps); - - /** - * @brief Calculate the timer interval based on target FPS. - * - * @details Returns the interval in milliseconds for the timer - * based on the current target FPS setting. - * - * @return Timer interval in milliseconds. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - int calculateInterval() const; - - signals: - /** - * @brief Signal emitted when animation starts. - * - * @since 0.1 - * @ingroup ui_components - */ - void started(); - - /** - * @brief Signal emitted when animation is paused. - * - * @since 0.1 - * @ingroup ui_components - */ - void paused(); - - /** - * @brief Signal emitted when animation stops. - * - * @since 0.1 - * @ingroup ui_components - */ - void stopped(); - - /** - * @brief Signal emitted when animation reverses direction. - * - * @since 0.1 - * @ingroup ui_components - */ - void reversed(); - - /** - * @brief Signal emitted when animation finishes. - * - * @since 0.1 - * @ingroup ui_components - */ - void finished(); - - /** - * @brief Signal emitted when animation progress changes. - * - * @param[in] progress Current progress value (0.0 to 1.0). - * - * @since 0.1 - * @ingroup ui_components - */ - void progressChanged(float progress); - - protected: - QTimer* driven_internal_timer{nullptr}; - float m_progress = 0.0f; - State m_state = State::Idle; - - /// Target FPS for this animation (default 60.0f) - float targetFps_ = 60.0f; - - /** - * @brief Sets the enabled state of the animation. - * - * @param[in] enabled true to enable, false to disable. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - void setEnabled(bool enabled) { this->enabled = enabled; } - - private: - bool enabled; -}; - -} // namespace cf::ui::components diff --git a/ui/components/animation_factory_manager.cpp b/ui/components/animation_factory_manager.cpp deleted file mode 100644 index 7e8249bb0..000000000 --- a/ui/components/animation_factory_manager.cpp +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @file animation_factory_manager.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Animation Factory Manager Interface Implementation - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Implementation file for ICFAnimationManagerFactory interface. - * This file is required for Qt's MOC (Meta-Object Compiler) to generate - * the meta-object code (vtable, staticMetaObject, etc.) for interfaces - * with Q_OBJECT macro. - */ - -#include "animation_factory_manager.h" - -namespace cf::ui::components { - -ICFAnimationManagerFactory::ICFAnimationManagerFactory(QObject* parent) : QObject(parent) {} - -} // namespace cf::ui::components diff --git a/ui/components/animation_factory_manager.h b/ui/components/animation_factory_manager.h deleted file mode 100644 index ef2ab3b3e..000000000 --- a/ui/components/animation_factory_manager.h +++ /dev/null @@ -1,320 +0,0 @@ -/** - * @file animation_factory_manager.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Animation Factory Manager Interface - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Defines the interface for animation factory managers that create and - * manage animations. This interface provides token-based animation lookup - * and supports both string-based and typed animation registration. - * - * The manager maintains ownership of created animations and provides - * aex::WeakPtr access to users for safe, non-owning references. - * - * @ingroup ui_components - */ -#pragma once - -#include "aex/weak_ptr/weak_ptr.h" -#include "animation.h" -#include -#include - -namespace cf::ui::components { - -/** - * @brief Animation creator function type. - * - * @details Function signature for creating animations dynamically. - * Used with registerAnimationCreator for type-safe animation - * registration. - * - * @since 0.1 - * @ingroup ui_components - */ -using AnimationCreator = std::function; - -/** - * @brief Animation Factory Manager Interface. - * - * @details Manages creation and lifecycle of animations with support for: - * - Token-based animation lookup (e.g., "md.animation.fadeIn") - * - Type-safe animation registration - * - Global and per-animation enable/disable - * - aex::WeakPtr ownership model for safe access - * - * Animations are owned by the manager and accessed via aex::WeakPtr. - * This ensures proper lifecycle management and prevents dangling - * pointers when the manager is destroyed. - * - * @note Implementations should be thread-safe for concurrent reads. - * @warning Destroying the manager invalidates all aex::WeakPtr references. - * @since 0.1 - * @ingroup ui_components - * - * @code - * // Example usage with MaterialAnimationFactory - * MaterialAnimationFactory factory(theme); - * auto anim = factory.getAnimation("md.animation.fadeIn"); - * if (anim) { - * anim->start(); - * } - * @endcode - */ -class CF_UI_EXPORT ICFAnimationManagerFactory : public QObject { - Q_OBJECT - - public: - /** - * @brief Constructor with parent. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - explicit ICFAnimationManagerFactory(QObject* parent); - - /** - * @brief Virtual destructor. - * - * @since 0.1 - */ - ~ICFAnimationManagerFactory() override = default; - - // ========================================================================= - // Enumeration Types - // ========================================================================= - - /** - * @brief Registration result enumeration. - * - * @details Possible results from animation registration operations. - * - * @since 0.1 - * @ingroup ui_components - */ - enum class RegisteredResult { - OK, ///< Registration successful - DUP_NAME, ///< Animation with this name already exists - UNSUPPORT_TYPE ///< Animation type is not supported - }; - Q_ENUM(RegisteredResult) - - virtual aex::WeakPtr GetWeakPtr() = 0; - // ========================================================================= - // Animation Registration - // ========================================================================= - - /** - * @brief Register an animation type by name. - * - * @details Associates a name with an animation type for later - * retrieval. The type string is used to determine - * which animation class to instantiate. - * - * @param[in] name Unique name for the animation (e.g., "fadeIn"). - * @param[in] type Animation type identifier (e.g., "fade", "slide"). - * - * @return Registration result indicating success or failure reason. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - * - * @code - * manager->registerOneAnimation("buttonPress", "fade"); - * @endcode - */ - virtual RegisteredResult registerOneAnimation(const QString& name, const QString& type) = 0; - - /** - * @brief Register an animation with a creator function. - * - * @details Associates a name with a function that creates animation - * instances. This provides type-safe registration for - * custom animation types. - * - * @param[in] name Unique name for the animation. - * @param[in] creator Function that creates the animation instance. - * - * @return Registration result indicating success or failure reason. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - * - * @code - * manager->registerAnimationCreator("customFade", - * [](QObject* parent) { - * return new CFMaterialFadeAnimation(motionSpec, parent); - * }); - * @endcode - */ - virtual RegisteredResult registerAnimationCreator(const QString& name, - AnimationCreator creator) = 0; - - // ========================================================================= - // Animation Retrieval - // ========================================================================= - - /** - * @brief Get or create an animation by name. - * - * @details Retrieves an existing animation or creates a new one - * based on the registered name/token. - * - * Supports both custom registered names and Material Design - * tokens (e.g., "md.animation.fadeIn"). - * - * @param[in] name Animation name or token. - * - * @return aex::WeakPtr to the animation, or invalid aex::WeakPtr if not found. - * - * @throws None - * @note The returned aex::WeakPtr may become invalid if the manager - * is destroyed. Always check validity before use. - * @warning The manager owns the animation; do not delete it manually. - * @since 0.1 - * @ingroup ui_components - * - * @code - * auto anim = manager->getAnimation("md.animation.fadeIn"); - * if (anim) { - * anim->start(); - * } - * @endcode - */ - virtual aex::WeakPtr getAnimation(const char* name) = 0; - // ========================================================================= - // Global Settings - // ========================================================================= - - /** - * @brief Set the target FPS for animations. - * - * @details Sets the desired frame rate for animation updates. - * This affects the tick interval for all animations. - * - * @param[in] fps Target frames per second (e.g., 60.0f). - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - * - * @code - * manager->setTargetFps(60.0f); // 60 FPS - * @endcode - */ - void setTargetFps(const float fps); - - /** - * @brief Set enabled state for a specific animation. - * - * @details Controls whether a specific animation is allowed to run. - * When disabled, getAnimation() returns invalid aex::WeakPtr. - * - * @param[in] which Animation name. - * @param[in] enabled true to enable, false to disable. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - * - * @code - * manager->setTargetEnabled("md.animation.fadeIn", false); - * @endcode - */ - void setTargetEnabled(const QString& which, const bool enabled); - - /** - * @brief Check if a specific animation is enabled. - * - * @param[in] which Animation name. - * - * @return true if enabled, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - bool targetEnabled(const QString& which); - - /** - * @brief Set enabled state for all animations. - * - * @details When disabled, getAnimation() returns invalid aex::WeakPtr - * for all animations. Existing running animations continue - * until completion. - * - * @param[in] enabled true to enable all, false to disable all. - * - * @throws None - * @note Existing running animations continue until completion. - * @warning None - * @since 0.1 - * @ingroup ui_components - * - * @code - * // Disable all animations during heavy processing - * manager->setEnabledAll(false); - * // ... do heavy work ... - * manager->setEnabledAll(true); - * @endcode - */ - virtual void setEnabledAll(bool enabled) = 0; - - /** - * @brief Check if all animations are enabled. - * - * @return true if all animations are enabled, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - virtual bool isAllEnabled() = 0; - - signals: - /** - * @brief Signal emitted when an animation is registered. - * - * @param[in] name The name of the registered animation. - * - * @since 0.1 - * @ingroup ui_components - */ - void animationRegistered(const QString& name); - - /** - * @brief Signal emitted when an animation's enabled state changes. - * - * @param[in] name The name of the animation. - * @param[in] enabled The new enabled state. - * - * @since 0.1 - * @ingroup ui_components - */ - void animationEnabledChanged(const QString& name, bool enabled); -}; - -} // namespace cf::ui::components diff --git a/ui/components/animation_group.h b/ui/components/animation_group.h deleted file mode 100644 index d653959f0..000000000 --- a/ui/components/animation_group.h +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @file animation_group.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Animation Group Interface - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Defines the animation group interface for running multiple animations - * together either in parallel or sequentially. - * - * @ingroup ui_components - */ -#pragma once - -#include "aex/weak_ptr/weak_ptr.h" -#include "animation.h" -#include "export.h" -#include -#include - -namespace cf::ui::components { - -/** - * @brief Animation group for combining multiple animations. - * - * @details Groups can run animations in parallel or sequential mode. - * - * @since 0.1 - * @ingroup ui_components - */ -class CF_UI_EXPORT ICFAnimationGroup : public ICFAbstractAnimation { - Q_OBJECT - public: - /** - * @brief Animation group execution mode. - * - * @since 0.1 - * @ingroup ui_components - */ - enum class Mode { Parallel, Sequential }; - Q_ENUM(Mode); - - ICFAnimationGroup(QObject* parent = nullptr) : ICFAbstractAnimation(parent) {} - - /** - * @brief Adds an animation to the group. - * - * @param[in] animation aex::WeakPtr to the animation to add. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - void addAnimation(aex::WeakPtr animation) { - animations.insert(animation); - } - - /** - * @brief Removes an animation from the group. - * - * @param[in] animation aex::WeakPtr to the animation to remove. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - void removeAnimation(aex::WeakPtr animation) { - animations.erase(animation); - } - - private: - std::unordered_set> animations; -}; - -} // namespace cf::ui::components diff --git a/ui/components/material/CMakeLists.txt b/ui/components/material/CMakeLists.txt deleted file mode 100644 index ed622ba37..000000000 --- a/ui/components/material/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -project(ui_components_material LANGUAGES CXX) - -log_info("UI_Components_Material" "Start Configure the UI Components Material") - -# STATIC library for linking into cfui.dll -add_library(cf_ui_components_material STATIC - cfmaterial_fade_animation.cpp - cfmaterial_slide_animation.cpp - cfmaterial_scale_animation.cpp - cfmaterial_property_animation.cpp - cfmaterial_animation_factory.cpp - cfmaterial_animation_strategy.cpp -) - -# Add include directories -target_include_directories(cf_ui_components_material PUBLIC - $ - $ - $ -) - -# Link Qt for compile dependencies and base animation library -target_link_libraries(cf_ui_components_material PUBLIC - Qt6::Core - Qt6::Gui - Qt6::Widgets - CFDesktop::base_headers - cf_ui_base - cf_ui_components -) - -log_info("UI_Components_Material" "the UI Components Material Configuration Done") diff --git a/ui/components/material/cfmaterial_animation_factory.cpp b/ui/components/material/cfmaterial_animation_factory.cpp deleted file mode 100644 index 2d0b51a9f..000000000 --- a/ui/components/material/cfmaterial_animation_factory.cpp +++ /dev/null @@ -1,376 +0,0 @@ -/** - * @file cfmaterial_animation_factory.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Animation Factory Implementation - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Implements the CFMaterialAnimationFactory class for creating and managing - * Material Design 3 animations with token-based lookup and strategy - * pattern customization. - */ - -#include "cfmaterial_animation_factory.h" -#include "animation_factory_manager.h" -#include "cfmaterial_fade_animation.h" -#include "cfmaterial_property_animation.h" -#include "cfmaterial_scale_animation.h" -#include "cfmaterial_slide_animation.h" -#include "token/animation_token_mapping.h" -#include - -namespace cf::ui::components::material { - -using namespace cf::ui::core; -using namespace token_literals; - -// ============================================================================= -// Constructor / Destructor -// ============================================================================= - -CFMaterialAnimationFactory::CFMaterialAnimationFactory(const ICFTheme& theme, - std::unique_ptr strategy, - QObject* parent) - : ICFAnimationManagerFactory(parent), theme_(theme), strategy_(std::move(strategy)), - globalEnabled_(true) { - // Use default strategy if none provided - if (!strategy_) { - strategy_ = std::make_unique(); - } -} - -CFMaterialAnimationFactory::~CFMaterialAnimationFactory() { - // All owned animations will be automatically destroyed - animations_.clear(); -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -aex::WeakPtr -CFMaterialAnimationFactory::getAnimation(const char* animationToken) { - - // Check global enabled state - if (!globalEnabled_) { - return aex::WeakPtr(); - } - - // Check if animation already exists - auto it = animations_.find(animationToken); - if (it != animations_.end()) { - // Return existing aex::WeakPtr from the animation - return it->second->GetWeakPtr(); - } - - // Resolve token to descriptor - AnimationDescriptor descriptor = resolveToken(animationToken); - - // Check if token was found - if (descriptor.animationType == nullptr) { - return aex::WeakPtr(); - } - - // Apply strategy - descriptor = applyStrategy(descriptor, nullptr); - - // Check if animation should be enabled - if (!shouldEnableAnimation(nullptr)) { - return aex::WeakPtr(); - } - - // Create animation based on type - std::unique_ptr animation; - const char* type = descriptor.animationType; - - if (std::strcmp(type, "fade") == 0) { - animation = createFadeAnimation(descriptor, nullptr); - } else if (std::strcmp(type, "slide") == 0) { - animation = createSlideAnimation(descriptor, nullptr); - } else if (std::strcmp(type, "scale") == 0) { - animation = createScaleAnimation(descriptor, nullptr); - } - - // Store and return aex::WeakPtr - if (animation) { - const char* tokenKey = descriptor.motionToken; - ICFAbstractAnimation* rawPtr = animation.get(); - animations_[tokenKey] = std::move(animation); - emit animationCreated(QString::fromUtf8(tokenKey)); - return rawPtr->GetWeakPtr(); - } - - return aex::WeakPtr(); -} - -aex::WeakPtr -CFMaterialAnimationFactory::createAnimation(const AnimationDescriptor& descriptor, - QWidget* targetWidget, QObject* owner) { - - // Check global enabled state - if (!globalEnabled_) { - return aex::WeakPtr(); - } - - // Apply strategy - AnimationDescriptor adjustedDescriptor = applyStrategy(descriptor, targetWidget); - - // Check if animation should be enabled - if (!shouldEnableAnimation(targetWidget)) { - return aex::WeakPtr(); - } - - // Generate a unique key for this animation - // Priority: owner > targetWidget, ensuring each caller has its own cached instance - QObject* keyObject = owner ? owner : targetWidget; - std::string key = adjustedDescriptor.motionToken; - key += "_"; - key += std::to_string(reinterpret_cast(keyObject)); - - // Check if animation already exists in cache - auto it = animations_.find(key); - if (it != animations_.end()) { - // Return cached instance - return it->second->GetWeakPtr(); - } - - // Create animation based on type - std::unique_ptr animation; - const char* type = adjustedDescriptor.animationType; - - if (std::strcmp(type, "fade") == 0) { - animation = createFadeAnimation(adjustedDescriptor, targetWidget); - } else if (std::strcmp(type, "slide") == 0) { - animation = createSlideAnimation(adjustedDescriptor, targetWidget); - } else if (std::strcmp(type, "scale") == 0) { - animation = createScaleAnimation(adjustedDescriptor, targetWidget); - } - - // Store and return aex::WeakPtr - if (animation) { - ICFAbstractAnimation* rawPtr = animation.get(); - animations_[key] = std::move(animation); - - // Monitor owner/targetWidget destruction to auto-cleanup cache - // This prevents memory leaks when widgets are destroyed - if (owner) { - connect(owner, &QObject::destroyed, this, [this, key]() { animations_.erase(key); }); - } else if (targetWidget) { - connect(targetWidget, &QObject::destroyed, this, - [this, key]() { animations_.erase(key); }); - } - - emit animationCreated(QString::fromUtf8(key.c_str())); - return rawPtr->GetWeakPtr(); - } - - return aex::WeakPtr(); -} - -void CFMaterialAnimationFactory::setStrategy(std::unique_ptr strategy) { - - strategy_ = std::move(strategy); - if (!strategy_) { - strategy_ = std::make_unique(); - } -} - -void CFMaterialAnimationFactory::setEnabledAll(bool enabled) { - if (globalEnabled_ != enabled) { - globalEnabled_ = enabled; - emit animationEnabledChanged(enabled); - } -} - -void CFMaterialAnimationFactory::setTargetFps(const float fps) { - if (fps > 0.0f) { - targetFps_ = fps; - // Update interval for all existing animations - for (auto& [name, animation] : animations_) { - if (animation) { - animation->setTargetFps(fps); - } - } - } -} - -ICFAnimationManagerFactory::RegisteredResult -CFMaterialAnimationFactory::registerOneAnimation(const QString& name, const QString& type) { - // Material Design factory uses predefined token mappings - // Custom registrations are stored in a separate map - (void)name; // TODO: Implement custom animation registration - (void)type; // TODO: Implement custom animation registration - return ICFAnimationManagerFactory::RegisteredResult::UNSUPPORT_TYPE; -} - -ICFAnimationManagerFactory::RegisteredResult -CFMaterialAnimationFactory::registerAnimationCreator(const QString& name, - AnimationCreator creator) { - // Material Design factory uses predefined token mappings - // Custom registrations are stored in a separate map - (void)name; // TODO: Implement custom animation registration - (void)creator; // TODO: Implement custom animation registration - return ICFAnimationManagerFactory::RegisteredResult::UNSUPPORT_TYPE; -} - -// ============================================================================= -// Private Methods -// ============================================================================= - -AnimationDescriptor CFMaterialAnimationFactory::resolveToken(const char* token) { - const auto* mapping = findTokenMapping(token); - if (mapping) { - return AnimationDescriptor(mapping->animationType, mapping->motionToken, mapping->property, - mapping->defaultFrom, mapping->defaultTo, - 0 // delayMs - ); - } - - // Return empty descriptor if token not found - return AnimationDescriptor(); -} - -std::unique_ptr -CFMaterialAnimationFactory::createFadeAnimation(const AnimationDescriptor& desc, QWidget* widget) { - - // Get motion spec from theme - auto& motionSpec = theme_.motion_spec(); - - // Query duration and easing from motion token - int duration = motionSpec.queryDuration(desc.motionToken); - int easing = motionSpec.queryEasing(desc.motionToken); - - // Create fade animation with raw pointer (lifetime guaranteed by theme_) - auto anim = std::make_unique(&motionSpec, nullptr); - anim->setRange(desc.fromValue, desc.toValue); - anim->setTargetFps(targetFps_); - if (widget) { - anim->setTargetWidget(widget); - } - return anim; -} - -std::unique_ptr -CFMaterialAnimationFactory::createSlideAnimation(const AnimationDescriptor& desc, QWidget* widget) { - - // Get motion spec from theme - auto& motionSpec = theme_.motion_spec(); - - // Query duration and easing from motion token - int duration = motionSpec.queryDuration(desc.motionToken); - int easing = motionSpec.queryEasing(desc.motionToken); - - // Determine slide direction from property - SlideDirection direction = SlideDirection::Up; - if (std::strcmp(desc.property, "positionX") == 0) { - direction = SlideDirection::Right; - } - - // Create slide animation with raw pointer (lifetime guaranteed by theme_) - auto anim = std::make_unique(&motionSpec, direction, nullptr); - anim->setRange(desc.fromValue, desc.toValue); - anim->setTargetFps(targetFps_); - if (widget) { - anim->setTargetWidget(widget); - } - return anim; -} - -std::unique_ptr -CFMaterialAnimationFactory::createScaleAnimation(const AnimationDescriptor& desc, QWidget* widget) { - - // Get motion spec from theme - auto& motionSpec = theme_.motion_spec(); - - // Query duration and easing from motion token - int duration = motionSpec.queryDuration(desc.motionToken); - int easing = motionSpec.queryEasing(desc.motionToken); - - // Create scale animation with raw pointer (lifetime guaranteed by theme_) - auto anim = std::make_unique(&motionSpec, nullptr); - anim->setRange(desc.fromValue, desc.toValue); - anim->setTargetFps(targetFps_); - if (widget) { - anim->setTargetWidget(widget); - } - return anim; -} - -AnimationDescriptor CFMaterialAnimationFactory::applyStrategy(const AnimationDescriptor& descriptor, - QWidget* widget) { - - if (strategy_) { - return strategy_->adjust(descriptor, widget); - } - return descriptor; -} - -bool CFMaterialAnimationFactory::shouldEnableAnimation(QWidget* widget) const { - if (strategy_) { - return strategy_->shouldEnable(widget); - } - return globalEnabled_; -} - -aex::WeakPtr CFMaterialAnimationFactory::createPropertyAnimation( - float* value, float from, float to, int durationMs, cf::ui::base::Easing::Type easing, - QWidget* targetWidget, QObject* owner) { - - // Check global enabled state - if (!globalEnabled_) { - return aex::WeakPtr(); - } - - // Check if animation should be enabled - if (!shouldEnableAnimation(targetWidget)) { - return aex::WeakPtr(); - } - - // Generate a unique key for this animation - // Priority: owner > targetWidget, ensuring each caller has its own cached instance - QObject* keyObject = owner ? owner : targetWidget; - std::string key = "property_"; - key += std::to_string(reinterpret_cast(value)); - key += "_"; - key += std::to_string(reinterpret_cast(keyObject)); - - // Check if animation already exists in cache - auto it = animations_.find(key); - if (it != animations_.end()) { - // Return cached instance - return it->second->GetWeakPtr(); - } - - // Create property animation - auto anim = - std::make_unique(value, from, to, durationMs, easing, nullptr); - anim->setTargetFps(targetFps_); - if (targetWidget) { - anim->setTargetWidget(targetWidget); - } - - // Store and return aex::WeakPtr - if (anim) { - ICFAbstractAnimation* rawPtr = anim.get(); - animations_[key] = std::move(anim); - - // Monitor owner/targetWidget destruction to auto-cleanup cache - // This prevents memory leaks when widgets are destroyed - if (owner) { - connect(owner, &QObject::destroyed, this, [this, key]() { animations_.erase(key); }); - } else if (targetWidget) { - connect(targetWidget, &QObject::destroyed, this, - [this, key]() { animations_.erase(key); }); - } - - emit animationCreated(QString::fromUtf8(key.c_str())); - return rawPtr->GetWeakPtr(); - } - - return aex::WeakPtr(); -} - -} // namespace cf::ui::components::material diff --git a/ui/components/material/cfmaterial_animation_factory.h b/ui/components/material/cfmaterial_animation_factory.h deleted file mode 100644 index c51de0e97..000000000 --- a/ui/components/material/cfmaterial_animation_factory.h +++ /dev/null @@ -1,548 +0,0 @@ -/** - * @file cfmaterial_animation_factory.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Animation Factory - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * MaterialAnimationFactory creates and manages animations following - * Material Design 3 motion specifications. It uses a strategy pattern - * to allow widget-specific customization while maintaining a simple - * token-based API for users. - * - * Key features: - * - Token-based animation retrieval (e.g., "md.animation.fadeIn") - * - Automatic mapping to MotionSpec for timing and easing - * - Strategy pattern for widget-specific behavior - * - aex::WeakPtr ownership model (factory owns, users hold weak references) - * - Global enable/disable for performance and accessibility - * - * The factory maintains exclusive ownership of all created animations - * via unique_ptr, while users receive aex::WeakPtr references. This ensures - * proper lifecycle management and prevents dangling pointers. - * - * @ingroup ui_components_material - */ -#pragma once - -#include "../animation.h" -#include "aex/weak_ptr/weak_ptr.h" -#include "aex/weak_ptr/weak_ptr_factory.h" -#include "animation_factory_manager.h" -#include "base/easing.h" -#include "cfmaterial_animation_strategy.h" -#include "core/theme.h" -#include "export.h" -#include -#include -#include -#include - -namespace cf::ui::components::material { - -// Forward declarations -class CFMaterialFadeAnimation; -class CFMaterialSlideAnimation; -class CFMaterialScaleAnimation; - -/** - * @brief Material Design 3 Animation Factory. - * - * @details Creates and manages animations following Material Design 3 - * motion specifications. The factory maintains exclusive ownership - * of created animations (via unique_ptr) and provides aex::WeakPtr - * access to users. - * - * Animation lifecycle: - * 1. User calls getAnimation("md.animation.fadeIn") - * 2. Factory resolves token to AnimationDescriptor via token mapping - * 3. Strategy adjusts descriptor (if set) - * 4. Factory creates animation instance - * 5. Factory stores animation (owns it) - * 6. Factory returns aex::WeakPtr to user - * - * Token resolution: - * - "md.animation.fadeIn" → Fade animation, shortEnter timing - * - "md.animation.slideUp" → Slide animation, mediumEnter timing - * - "md.animation.scaleUp" → Scale animation, shortEnter timing - * - * @note Thread-safe for concurrent reads. - * @warning Animations are owned by the factory; aex::WeakPtr may become - * invalid if the factory is destroyed. - * @throws None (all errors return invalid aex::WeakPtr) - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * // Initialization (usually done once per application) - * using namespace cf::ui::components::material; - * using namespace cf::ui::core; - * - * MaterialFactory themeFactory; - * auto theme = themeFactory.fromName("theme.material.light"); - * auto factory = std::make_unique(*theme); - * factory->setStrategy(std::make_unique()); - * - * // Basic usage - get animation by token - * auto fadeIn = factory->getAnimation("md.animation.fadeIn"); - * if (fadeIn) { - * fadeIn->setTargetWidget(myWidget); - * fadeIn->start(); - * } - * - * // Create with custom descriptor - * AnimationDescriptor desc{"fade", "md.motion.longEnter", "opacity", 0.0f, 1.0f}; - * auto customFade = factory->createAnimation(desc, myWidget); - * - * // Enable/disable all animations - * factory->setEnabledAll(false); // Disable animations for performance - * @endcode - */ -class CF_UI_EXPORT CFMaterialAnimationFactory : public ICFAnimationManagerFactory { - Q_OBJECT - - public: - /** - * @brief Constructor with theme reference. - * - * @details The theme reference must remain valid for the lifetime - * of the factory. It is used to query MotionSpec values - * (duration, easing) for animations. - * - * @param[in] theme Reference to the Material Design theme. - * @param[in] strategy Optional strategy for widget-specific behavior. - * If nullptr, DefaultAnimationStrategy is used. - * @param[in] parent QObject parent. - * - * @throws None - * @note If strategy is nullptr, DefaultAnimationStrategy is used. - * @warning The theme must outlive this factory. - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * CFMaterialAnimationFactory factory(*theme); - * @endcode - */ - explicit CFMaterialAnimationFactory(const core::ICFTheme& theme, - std::unique_ptr strategy = nullptr, - QObject* parent = nullptr); - - /** - * @brief Destructor. - * - * @details All owned animations are destroyed. Any aex::WeakPtr - * returned by this factory becomes invalid. - * - * @since 0.1 - */ - ~CFMaterialAnimationFactory() override; - - // Non-copyable, non-movable - CFMaterialAnimationFactory(const CFMaterialAnimationFactory&) = delete; - CFMaterialAnimationFactory& operator=(const CFMaterialAnimationFactory&) = delete; - CFMaterialAnimationFactory(CFMaterialAnimationFactory&&) = delete; - CFMaterialAnimationFactory& operator=(CFMaterialAnimationFactory&&) = delete; - - aex::WeakPtr GetWeakPtr() override { - return weak_factory_.GetWeakPtr(); - } - - // Implement pure virtual functions from ICFAnimationManagerFactory - /** - * @brief Register an animation type by name. - * - * @param[in] name Unique name for the animation. - * @param[in] type Animation type identifier. - * - * @return Registration result indicating success or failure reason. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - ICFAnimationManagerFactory::RegisteredResult registerOneAnimation(const QString& name, - const QString& type) override; - - /** - * @brief Register an animation with a creator function. - * - * @param[in] name Unique name for the animation. - * @param[in] creator Function that creates the animation instance. - * - * @return Registration result indicating success or failure reason. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - ICFAnimationManagerFactory::RegisteredResult - registerAnimationCreator(const QString& name, AnimationCreator creator) override; - - /** - * @brief Get or create an animation by token name. - * - * @details This is the primary user API for retrieving animations. - * Tokens are resolved through the token mapping system: - * - "md.animation.fadeIn" → Fade animation, shortEnter timing - * - "md.animation.slideUp" → Slide animation, mediumEnter timing - * - "md.animation.scaleUp" → Scale animation, shortEnter timing - * - * If an animation with the given token already exists, - * the existing animation's aex::WeakPtr is returned. - * Otherwise, a new animation is created and stored. - * - * @param animationToken Token name (e.g., "md.animation.fadeIn"). - * - * @return aex::WeakPtr to the animation, or invalid aex::WeakPtr if: - * - Token is not found in mapping - * - Animation type is not supported - * - Global enabled is false - * - Strategy disables animation - * - * @throws None - * @note If the animation doesn't exist, a new animation is created. - * @warning The returned aex::WeakPtr may become invalid if the factory - * is destroyed. Always check validity before use. - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * auto anim = factory->getAnimation("md.animation.fadeIn"); - * if (anim) { - * anim->setTargetWidget(myWidget); - * anim->start(); - * } - * @endcode - */ - aex::WeakPtr getAnimation(const char* animationToken) override; - - /** - * @brief Create an animation from a descriptor. - * - * @details Creates a new animation instance based on the descriptor. - * The strategy (if set) is applied before creation. - * - * Unlike getAnimation(), this method always creates a new - * animation instance, even if one with the same configuration - * already exists. - * - * @param[in] descriptor Animation configuration descriptor. - * @param[in] targetWidget Optional target widget for the animation. - * The animation applies to this widget. - * @param[in] owner Optional owner QObject for memory management. - * - * @return aex::WeakPtr to the created animation, or invalid aex::WeakPtr if: - * - Animation type is not supported - * - Global enabled is false - * - Strategy disables animation - * - * @throws None - * @note The factory takes ownership of the created animation. - * @warning None - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * AnimationDescriptor desc{"fade", "md.motion.longEnter", "opacity", 0.0f, 1.0f}; - * auto anim = factory->createAnimation(desc, myWidget); - * if (anim) anim->start(); - * @endcode - */ - aex::WeakPtr createAnimation(const AnimationDescriptor& descriptor, - QWidget* targetWidget = nullptr, - QObject* owner = nullptr); - - /** - * @brief Create a property animation for a float value. - * - * @details Creates a property animation that directly animates a float - * value from a start value to an end value. This is useful for - * simple property animations like floating labels, scale, etc. - * - * Unlike createAnimation(), this method always creates a new - * animation instance and does not use token-based lookup. - * - * @param[in] value Pointer to the float property to animate. - * Must remain valid for the lifetime of the animation. - * @param[in] from Start value of the animation. - * @param[in] to End value of the animation. - * @param[in] durationMs Duration of the animation in milliseconds. - * @param[in] easing Easing type for the animation. - * @param[in] targetWidget Optional target widget for repaint notifications. - * - * @return aex::WeakPtr to the created animation, or invalid aex::WeakPtr if: - * - Global enabled is false - * - Strategy disables animation - * - * @throws None - * @note The factory takes ownership of the created animation. - * @warning The value pointer must remain valid during animation. - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * float scale = 0.0f; - * auto anim = factory->createPropertyAnimation(&scale, 0.0f, 1.0f, 200, - * Easing::Type::EmphasizedDecelerate, - * this); - * if (anim) anim->start(); - * @endcode - */ - aex::WeakPtr createPropertyAnimation( - float* value, float from, float to, int durationMs, - cf::ui::base::Easing::Type easing = cf::ui::base::Easing::Type::EmphasizedDecelerate, - QWidget* targetWidget = nullptr, QObject* owner = nullptr); - - /** - * @brief Set the animation strategy for this factory. - * - * @details The strategy is applied to all animations created after - * this call. Existing animations are not affected. - * - * Strategies allow widget-specific customization: - * - Buttons: shorter animations - * - Dialogs: longer animations - * - Lists: staggered animations - * - * @param[in] strategy Unique pointer to the strategy (takes ownership). - * Pass nullptr to use DefaultAnimationStrategy. - * - * @throws None - * @note Pass nullptr to use the default strategy. - * @warning The factory takes ownership of the strategy. - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * factory->setStrategy(std::make_unique()); - * @endcode - */ - void setStrategy(std::unique_ptr strategy); - - /** - * @brief Get the current animation strategy. - * - * @return Pointer to the current strategy (may be nullptr). - * - * @since 0.1 - */ - AnimationStrategy* strategy() const { return strategy_.get(); } - - /** - * @brief Set the global enabled state for all animations. - * - * @details When disabled, getAnimation() returns invalid aex::WeakPtr - * and createAnimation() returns invalid aex::WeakPtr. - * - * This is useful for: - * - Performance optimization during heavy processing - * - Accessibility (respect "reduce motion" settings) - * - User preference (animation toggle) - * - * @param[in] enabled true to enable animations, false to disable. - * - * @throws None - * @note This affects all animation creation methods. - * @warning Existing animations continue to run; only new creations - * are affected. - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * // Disable animations during heavy processing - * factory->setEnabledAll(false); - * // ... do heavy work ... - * factory->setEnabledAll(true); - * @endcode - */ - void setEnabledAll(bool enabled) override; - - /** - * @brief Check if animations are globally enabled. - * - * @return true if animations are enabled, false otherwise. - * - * @since 0.1 - */ - bool isAllEnabled() override { return globalEnabled_; } - - /** - * @brief Set the target FPS for animations. - * - * @details Sets the desired frame rate for animation updates. - * This affects the tick interval for all animations created - * by this factory, including existing animations. - * - * @param[in] fps Target frames per second (e.g., 60.0f). - * - * @throws None - * @note Default is 60.0f. Changes take effect immediately. - * @warning None - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * factory->setTargetFps(30.0f); // 30 FPS (lower CPU usage) - * factory->setTargetFps(120.0f); // 120 FPS (smoother animation) - * @endcode - */ - void setTargetFps(const float fps); - - /** - * @brief Get the associated theme. - * - * @return Reference to the theme. - * - * @warning The theme must outlive this factory. - * @since 0.1 - */ - const core::ICFTheme& theme() const { return theme_; } - - /** - * @brief Get the number of animations managed by this factory. - * - * @return Number of owned animations. - * - * @since 0.1 - */ - size_t animationCount() const { return animations_.size(); } - - /** - * @brief Get the target FPS for animations. - * - * @details Returns the current target frame rate for animation updates. - * This value is used by all animations created by this factory. - * - * @return Target frames per second (default: 60.0f). - * - * @throws None - * @note Use setTargetFps() to change this value. - * @warning None - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * float fps = factory->getTargetFps(); // Returns 60.0f by default - * @endcode - */ - float getTargetFps() const { return targetFps_; } - - signals: - /** - * @brief Signal emitted when an animation is created. - * - * @param[in] token The animation token that was created. - * - * @since 0.1 - * @ingroup ui_components_material - */ - void animationCreated(const QString& token); - - /** - * @brief Signal emitted when global enabled state changes. - * - * @param[in] enabled The new enabled state. - * - * @since 0.1 - * @ingroup ui_components_material - */ - void animationEnabledChanged(bool enabled); - - private: - /// Reference to the theme for MotionSpec queries - const core::ICFTheme& theme_; - - /// Animation strategy for widget-specific behavior - std::unique_ptr strategy_; - - /// Global enabled state - bool globalEnabled_ = true; - - /// Target FPS for animations (default 60.0f) - float targetFps_ = 60.0f; - - /// Owned animations (unique ownership) - /// Key: token name, Value: owned animation instance - std::unordered_map> animations_; - - /** - * @brief Resolve a token to an AnimationDescriptor. - * - * @details Looks up the token in the mapping table and returns - * the corresponding AnimationDescriptor. - * - * @param token Animation token name. - * - * @return AnimationDescriptor, or empty descriptor with nullptr values - * if token is not found. - */ - AnimationDescriptor resolveToken(const char* token); - - /** - * @brief Create a fade animation. - * - * @param desc Animation descriptor. - * @param widget Target widget. - * - * @return Unique pointer to the created animation, or nullptr on failure. - */ - std::unique_ptr createFadeAnimation(const AnimationDescriptor& desc, - QWidget* widget); - - /** - * @brief Create a slide animation. - * - * @param desc Animation descriptor. - * @param widget Target widget. - * - * @return Unique pointer to the created animation, or nullptr on failure. - */ - std::unique_ptr createSlideAnimation(const AnimationDescriptor& desc, - QWidget* widget); - - /** - * @brief Create a scale animation. - * - * @param desc Animation descriptor. - * @param widget Target widget. - * - * @return Unique pointer to the created animation, or nullptr on failure. - */ - std::unique_ptr createScaleAnimation(const AnimationDescriptor& desc, - QWidget* widget); - - /** - * @brief Apply the strategy to a descriptor. - * - * @details Calls strategy->adjust() if a strategy is set. - * - * @param descriptor The descriptor to adjust. - * @param widget The target widget. - * - * @return Adjusted descriptor. - */ - AnimationDescriptor applyStrategy(const AnimationDescriptor& descriptor, QWidget* widget); - - /** - * @brief Check if animation should be enabled. - * - * @details Checks global enabled state and strategy. - * - * @param widget The target widget. - * - * @return true if animation should be enabled. - */ - bool shouldEnableAnimation(QWidget* widget) const; - - aex::WeakPtrFactory weak_factory_{this}; -}; - -} // namespace cf::ui::components::material diff --git a/ui/components/material/cfmaterial_animation_strategy.cpp b/ui/components/material/cfmaterial_animation_strategy.cpp deleted file mode 100644 index 9937a4e12..000000000 --- a/ui/components/material/cfmaterial_animation_strategy.cpp +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @file cfmaterial_animation_strategy.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Animation Strategy Implementation - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Implements the AnimationStrategy interface methods. - */ - -#include "cfmaterial_animation_strategy.h" - -namespace cf::ui::components::material { - -// ============================================================================= -// AnimationStrategy -// ============================================================================= - -bool AnimationStrategy::shouldEnable(QWidget* widget) const { - (void)widget; // Suppress unused parameter warning - return globalEnabled_; -} - -void AnimationStrategy::setGlobalEnabled(bool enabled) { - globalEnabled_ = enabled; -} - -// ============================================================================= -// DefaultAnimationStrategy -// ============================================================================= - -AnimationDescriptor DefaultAnimationStrategy::adjust(const AnimationDescriptor& descriptor, - QWidget* widget) { - - // Return descriptor unchanged - this is a no-op strategy - (void)widget; // Suppress unused parameter warning - return descriptor; -} - -} // namespace cf::ui::components::material diff --git a/ui/components/material/cfmaterial_animation_strategy.h b/ui/components/material/cfmaterial_animation_strategy.h deleted file mode 100644 index fdf607286..000000000 --- a/ui/components/material/cfmaterial_animation_strategy.h +++ /dev/null @@ -1,301 +0,0 @@ -/** - * @file cfmaterial_animation_strategy.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Animation Strategy Interface for Material Design 3 - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Defines the strategy interface for animation customization based on - * widget types. This allows different widget types to have specialized - * animation behaviors while maintaining a consistent factory interface. - * - * The Animation Strategy pattern enables: - * - Widget-specific animation parameter adjustments - * - Runtime animation type overrides - * - Conditional animation enable/disable logic - * - Reusable customization logic across different widget types - * - * @ingroup ui_components_material - */ -#pragma once - -#include "export.h" -#include - -namespace cf::ui::components::material { - -/** - * @brief Animation descriptor structure. - * - * @details Contains all information needed to create an animation: - * - Animation type (fade, slide, scale, rotate, spring) - * - Motion specification token for timing/easing - * - Target property to animate - * - Value range - * - Optional delay - * - * This structure is passed to AnimationStrategy for potential - * modification before animation creation. - * - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * // Create a fade-in animation descriptor - * AnimationDescriptor desc; - * desc.animationType = "fade"; - * desc.motionToken = "md.motion.shortEnter"; - * desc.property = "opacity"; - * desc.fromValue = 0.0f; - * desc.toValue = 1.0f; - * desc.delayMs = 0; - * @endcode - */ -struct CF_UI_EXPORT AnimationDescriptor { - /// Animation type: "fade", "slide", "scale", "rotate", "spring" - const char* animationType; - - /// Motion spec token: e.g., "md.motion.shortEnter" - const char* motionToken; - - /// Property to animate: "opacity", "positionX", "positionY", "scale" - const char* property; - - /// Start value for the animation - float fromValue; - - /// End value for the animation - float toValue; - - /// Delay before animation starts (milliseconds) - int delayMs = 0; - - /** - * @brief Default constructor. - * - * @since 0.1 - */ - AnimationDescriptor() = default; - - /** - * @brief Construct with all fields. - * - * @param[in] type Animation type. - * @param[in] motion Motion spec token. - * @param[in] prop Property name. - * @param[in] from Start value. - * @param[in] to End value. - * @param[in] delay Delay in milliseconds. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * AnimationDescriptor desc("fade", "md.motion.longEnter", "opacity", 0.0f, 1.0f); - * @endcode - */ - AnimationDescriptor(const char* type, const char* motion, const char* prop, float from, - float to, int delay = 0) - : animationType(type), motionToken(motion), property(prop), fromValue(from), toValue(to), - delayMs(delay) {} -}; - -/** - * @brief Abstract strategy interface for animation customization. - * - * @details The Animation Strategy pattern allows different widget types - * to customize animation behavior without modifying the factory. - * Each strategy can: - * - Adjust animation parameters (duration, easing, values) - * - Override animation types for specific widgets - * - Enable/disable animations based on conditions - * - * Strategies are applied during animation creation, before - * the actual animation object is instantiated. This allows - * for runtime customization based on widget state, system - * settings, or other contextual factors. - * - * @note Strategies should be stateless and reusable across widgets. - * Do not store widget pointers in strategies; use only the - * provided widget parameter during the adjust() call. - * @warning Do not store the widget pointer passed to adjust(); it may - * become invalid after the call returns. - * @throws None (all methods are noexcept safe) - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * // Custom strategy for buttons - * class ButtonStrategy : public AnimationStrategy { - * public: - * AnimationDescriptor adjust(const AnimationDescriptor& desc, - * QWidget* widget) override { - * // Buttons use shorter animations - * AnimationDescriptor adjusted = desc; - * if (strcmp(desc.motionToken, "md.motion.mediumEnter") == 0) { - * adjusted.motionToken = "md.motion.shortEnter"; - * } - * return adjusted; - * } - * }; - * - * // Usage - * factory->setStrategy(std::make_unique()); - * @endcode - */ -class CF_UI_EXPORT AnimationStrategy { - public: - /** - * @brief Virtual destructor. - * - * @since 0.1 - */ - virtual ~AnimationStrategy() = default; - - /** - * @brief Adjust animation descriptor for a specific widget. - * - * @details Called by the factory before creating an animation. - * Subclasses can modify any field in the descriptor to - * customize animation behavior. - * - * Common adjustments include: - * - Changing motion token (duration/easing) - * - Substituting animation type (slide → fade) - * - Modifying value ranges - * - Adding delays - * - * @param[in] descriptor Original animation descriptor. - * @param[in] widget Target widget (may be nullptr). - * - * @return Adjusted animation descriptor. - * - * @throws None - * @note The default implementation returns the descriptor unchanged. - * @warning Do not store the widget pointer; it may be invalid after - * this call returns. - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * AnimationDescriptor adjust(const AnimationDescriptor& desc, - * QWidget* widget) override { - * AnimationDescriptor result = desc; - * // Reduce animation duration for small widgets - * if (widget && widget->width() < 100) { - * result.motionToken = "md.motion.shortEnter"; - * } - * return result; - * } - * @endcode - */ - virtual AnimationDescriptor adjust(const AnimationDescriptor& descriptor, QWidget* widget) = 0; - - /** - * @brief Check if animations should be enabled for a widget. - * - * @details Allows strategies to conditionally disable animations - * based on widget state or system settings. This can be - * used for accessibility, performance, or user preference. - * - * Common reasons to disable: - * - System accessibility setting (reduce motion) - * - Low-performance mode - * - Widget-specific conditions - * - * @param[in] widget Target widget (may be nullptr). - * - * @return true if animation should be enabled, false otherwise. - * - * @throws None - * @note The default implementation returns the global enabled state. - * @warning None - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * bool shouldEnable(QWidget* widget) const override { - * // Disable animations in low-performance mode - * if (isLowPerformanceMode()) return false; - * return globalEnabled_; - * } - * @endcode - */ - virtual bool shouldEnable(QWidget* widget) const; - - /** - * @brief Set the global enabled state for this strategy. - * - * @details This affects all subsequent shouldEnable() calls. - * Individual widget checks can still override this by - * returning false in their implementation. - * - * @param[in] enabled true to enable animations, false to disable. - * - * @throws None - * @note This affects all subsequent shouldEnable() calls. - * @warning None - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * // Disable animations during heavy processing - * strategy->setGlobalEnabled(false); - * // ... do heavy work ... - * strategy->setGlobalEnabled(true); - * @endcode - */ - void setGlobalEnabled(bool enabled); - - /** - * @brief Get the global enabled state. - * - * @return true if globally enabled, false otherwise. - * - * @since 0.1 - */ - bool globalEnabled() const { return globalEnabled_; } - - protected: - /// Global enabled state for animations using this strategy. - bool globalEnabled_ = true; -}; - -/** - * @brief Default animation strategy implementation. - * - * @details Provides a no-op adjustment that returns the descriptor - * unchanged. Used when no specific strategy is needed. - * - * This is the default strategy used by MaterialAnimationFactory - * when no custom strategy is provided via setStrategy(). - * - * @since 0.1 - * @ingroup ui_components_material - */ -class CF_UI_EXPORT DefaultAnimationStrategy : public AnimationStrategy { - public: - /** - * @brief Returns the descriptor unchanged. - * - * @param[in] descriptor The animation descriptor. - * @param[in] widget The target widget (unused). - * - * @return The original descriptor, unmodified. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - AnimationDescriptor adjust(const AnimationDescriptor& descriptor, QWidget* widget) override; -}; - -} // namespace cf::ui::components::material diff --git a/ui/components/material/cfmaterial_fade_animation.cpp b/ui/components/material/cfmaterial_fade_animation.cpp deleted file mode 100644 index 5c6424d2a..000000000 --- a/ui/components/material/cfmaterial_fade_animation.cpp +++ /dev/null @@ -1,212 +0,0 @@ -/** - * @file cfmaterial_fade_animation.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Fade Animation Implementation - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Implements the CFMaterialFadeAnimation class for opacity-based animations - * following Material Design 3 motion specifications. - */ - -#include "cfmaterial_fade_animation.h" -#include - -namespace cf::ui::components::material { - -// ============================================================================= -// Constructor / Destructor -// ============================================================================= - -CFMaterialFadeAnimation::CFMaterialFadeAnimation(cf::ui::core::IMotionSpec* spec, QObject* parent) - : ICFTimingAnimation(spec, parent), currentOpacity_(1.0f), targetWidget_(nullptr), - opacityEffect_(nullptr), ownsOpacityEffect_(false), durationMs_(200), delayMs_(0), - elapsedTime_(0) {} - -CFMaterialFadeAnimation::~CFMaterialFadeAnimation() { - // Clean up opacity effect if we own it - if (opacityEffect_ && ownsOpacityEffect_ && targetWidget_) { - targetWidget_->setGraphicsEffect(nullptr); - } - delete opacityEffect_; -} - -// ============================================================================= -// ICFAbstractAnimation Interface -// ============================================================================= - -void CFMaterialFadeAnimation::start(Direction dir) { - if (m_state == State::Running) { - return; // Already running - } - - // Get duration from motion spec if available - if (motion_spec_) { - // Duration would be queried from the motion token name - // For now, use default - durationMs_ = 200; // shortEnter default - } - - elapsedTime_ = 0; - m_state = State::Running; - - if (dir == Direction::Forward) { - currentOpacity_ = m_from; - } else { - currentOpacity_ = m_to; - } - - emit started(); - - // Apply initial opacity - applyOpacity(currentOpacity_); - - // Start the internal timer to drive animation - if (driven_internal_timer) { - driven_internal_timer->start(calculateInterval()); - } -} - -void CFMaterialFadeAnimation::pause() { - if (m_state == State::Running) { - m_state = State::Paused; - if (driven_internal_timer) { - driven_internal_timer->stop(); - } - emit paused(); - } -} - -void CFMaterialFadeAnimation::stop() { - m_state = State::Idle; - if (driven_internal_timer) { - driven_internal_timer->stop(); - } - elapsedTime_ = 0; - - // Reset to initial value - currentOpacity_ = m_from; - applyOpacity(currentOpacity_); - - emit stopped(); -} - -void CFMaterialFadeAnimation::reverse() { - if (m_state == State::Running) { - // Calculate remaining progress and reverse from there - float progress = static_cast(elapsedTime_) / durationMs_; - progress = 1.0f - progress; // Reverse progress - - elapsedTime_ = static_cast(progress * durationMs_); - } else { - // Start in reverse direction - start(Direction::Backward); - } - - emit reversed(); -} - -bool CFMaterialFadeAnimation::tick(int dt) { - if (m_state != State::Running) { - return false; - } - - elapsedTime_ += dt; - - // Check if we're still in delay period - if (elapsedTime_ < delayMs_) { - return true; // Still waiting - } - - // Calculate actual animation time (subtract delay) - int animTime = elapsedTime_ - delayMs_; - - // Calculate progress - float progress = static_cast(animTime) / durationMs_; - - // Clamp progress - if (progress > 1.0f) { - progress = 1.0f; - } - - // Apply easing - float easedProgress = calculateEasedProgress(progress); - - // Calculate current opacity based on range - currentOpacity_ = m_from + (m_to - m_from) * easedProgress; - - // Apply opacity - applyOpacity(currentOpacity_); - - // Emit progress signal - emit progressChanged(easedProgress); - - // Check if animation is complete - if (progress >= 1.0f) { - m_state = State::Finished; - emit finished(); - return false; - } - - return true; -} - -// ============================================================================= -// Fade-Specific Methods -// ============================================================================= - -void CFMaterialFadeAnimation::setTargetWidget(QWidget* widget) { - targetWidget_ = widget; - - // Set up opacity effect for the new widget - if (widget) { - ensureOpacityEffect(); - } -} - -void CFMaterialFadeAnimation::applyOpacity(float opacity) { - if (!targetWidget_) { - return; - } - - ensureOpacityEffect(); - - if (opacityEffect_) { - opacityEffect_->setOpacity(qBound(0.0, static_cast(opacity), 1.0)); - - // Trigger update - if (targetWidget_) { - targetWidget_->update(); - } - } -} - -void CFMaterialFadeAnimation::ensureOpacityEffect() { - if (!targetWidget_) { - return; - } - - // Check if widget already has an opacity effect - opacityEffect_ = qobject_cast(targetWidget_->graphicsEffect()); - - if (!opacityEffect_) { - // Create new opacity effect - opacityEffect_ = new QGraphicsOpacityEffect(targetWidget_); - opacityEffect_->setOpacity(currentOpacity_); - targetWidget_->setGraphicsEffect(opacityEffect_); - ownsOpacityEffect_ = true; - } else { - ownsOpacityEffect_ = false; - } -} - -float CFMaterialFadeAnimation::calculateEasedProgress(float linearProgress) const { - // For now, use simple linear easing - // TODO: Integrate with MaterialMotionScheme for proper easing curves - return linearProgress; -} - -} // namespace cf::ui::components::material diff --git a/ui/components/material/cfmaterial_fade_animation.h b/ui/components/material/cfmaterial_fade_animation.h deleted file mode 100644 index cb765d8f8..000000000 --- a/ui/components/material/cfmaterial_fade_animation.h +++ /dev/null @@ -1,262 +0,0 @@ -/** - * @file cfmaterial_fade_animation.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Fade Animation - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Implements a fade animation that follows Material Design 3 motion - * specifications. Animates the opacity of a widget from a start value - * to an end value using the specified timing and easing curve. - * - * This animation integrates with the MaterialMotionScheme to obtain - * duration and easing values based on the motion token (e.g., "md.motion.shortEnter"). - * - * @ingroup ui_components_material - */ -#pragma once - -#include "aex/weak_ptr/weak_ptr.h" -#include "aex/weak_ptr/weak_ptr_factory.h" -#include "components/timing_animation.h" -#include "core/motion_spec.h" -#include "export.h" -#include -#include - -namespace cf::ui::components::material { - -/** - * @brief Material Design 3 Fade Animation. - * - * @details Animates the opacity of a widget from a start value to an end - * value following Material Design 3 motion specifications. - * - * The animation uses: - * - Duration from the MotionSpec token - * - Easing curve from the MotionSpec token - * - Optional delay before starting - * - * Opacity is applied via QGraphicsOpacityEffect for widgets - * that support it. - * - * @note The target widget must remain valid during animation. - * @warning Changing the target widget during animation may cause issues. - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * // Create a fade-in animation - * auto& motionSpec = theme.motion_spec(); - * auto fadeAnim = std::make_unique( - * aex::WeakPtr(&motionSpec), - * this); - * fadeAnim->setRange(0.0f, 1.0f); // Fade from transparent to opaque - * fadeAnim->setTargetWidget(myWidget); - * fadeAnim->start(); - * - * // Connect to progress signal - * connect(fadeAnim.get(), &ICFAbstractAnimation::progressChanged, - * this, [](float progress) { - * qDebug() << "Fade progress:" << progress; - * }); - * @endcode - */ -class CF_UI_EXPORT CFMaterialFadeAnimation : public ICFTimingAnimation { - Q_OBJECT - - public: - /** - * @brief Constructor with motion spec. - * - * @param[in] spec Raw pointer to the motion spec for timing/easing. - * Must remain valid for the lifetime of this animation. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - explicit CFMaterialFadeAnimation(cf::ui::core::IMotionSpec* spec, QObject* parent = nullptr); - - /** - * @brief Destructor. - * - * @since 0.1 - */ - ~CFMaterialFadeAnimation() override; - - // Non-copyable, non-movable - CFMaterialFadeAnimation(const CFMaterialFadeAnimation&) = delete; - CFMaterialFadeAnimation& operator=(const CFMaterialFadeAnimation&) = delete; - CFMaterialFadeAnimation(CFMaterialFadeAnimation&&) = delete; - CFMaterialFadeAnimation& operator=(CFMaterialFadeAnimation&&) = delete; - - // ========================================================================= - // ICFAbstractAnimation Interface - // ========================================================================= - - /** - * @brief Start the fade animation. - * - * @param[in] dir Direction to play (Forward = fade in, Backward = fade out). - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - void start(Direction dir = Direction::Forward) override; - - /** - * @brief Pause the animation. - * - * @since 0.1 - */ - void pause() override; - - /** - * @brief Stop the animation and reset to initial state. - * - * @since 0.1 - */ - void stop() override; - - /** - * @brief Reverse the animation direction. - * - * @since 0.1 - */ - void reverse() override; - - /** - * @brief Update animation state. - * - * @param[in] dt Time elapsed since last tick (milliseconds). - * - * @return true if animation is still running, false if finished. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - bool tick(int dt) override; - - // ========================================================================= - // ICFTimingAnimation Interface - // ========================================================================= - - /** - * @brief Get the current opacity value. - * - * @return Current opacity (0.0 = transparent, 1.0 = opaque). - * - * @since 0.1 - */ - float currentValue() const override { return currentOpacity_; } - - // ========================================================================= - // Fade-Specific Methods - // ========================================================================= - - /** - * @brief Set the target widget for the fade animation. - * - * @details The widget's opacity is animated. The widget must - * remain valid for the duration of the animation. - * - * @param[in] widget Target widget (may be nullptr). - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * fadeAnim->setTargetWidget(myLabel); - * @endcode - */ - void setTargetWidget(QWidget* widget); - - /** - * @brief Get the target widget. - * - * @return Target widget, or nullptr if not set. - * - * @since 0.1 - */ - QWidget* targetWidget() const { return targetWidget_; } - - /** - * @brief Get a weak pointer to this animation. - * - * @return aex::WeakPtr that can be used to safely access this animation. - * - * @since 0.1 - */ - aex::WeakPtr GetWeakPtr() override { return weak_factory_.GetWeakPtr(); } - - private: - /// Current opacity value (0.0 to 1.0) - float currentOpacity_ = 1.0f; - - /// Target widget to animate - QWidget* targetWidget_ = nullptr; - - /// Graphics effect for opacity (created/owned by this animation) - QGraphicsOpacityEffect* opacityEffect_ = nullptr; - - /// Whether the animation owns the opacity effect - bool ownsOpacityEffect_ = false; - - /// Duration in milliseconds (from motion spec) - int durationMs_ = 200; - - /// Delay in milliseconds before starting - int delayMs_ = 0; - - /// Elapsed time (including delay) - int elapsedTime_ = 0; - - /** - * @brief Apply the current opacity to the target widget. - * - * @param opacity Opacity value (0.0 to 1.0). - * - * @since 0.1 - */ - void applyOpacity(float opacity); - - /** - * @brief Ensure the target widget has an opacity effect. - * - * @since 0.1 - */ - void ensureOpacityEffect(); - - /** - * @brief Calculate eased progress based on timing function. - * - * @param linearProgress Linear progress (0.0 to 1.0). - * - * @return Eased progress (0.0 to 1.0). - * - * @since 0.1 - */ - float calculateEasedProgress(float linearProgress) const; - - /// aex::WeakPtrFactory for creating weak pointers to this animation - /// Must be the last member to ensure it's destroyed first - aex::WeakPtrFactory weak_factory_{this}; -}; - -} // namespace cf::ui::components::material diff --git a/ui/components/material/cfmaterial_property_animation.cpp b/ui/components/material/cfmaterial_property_animation.cpp deleted file mode 100644 index 6ee8a00ec..000000000 --- a/ui/components/material/cfmaterial_property_animation.cpp +++ /dev/null @@ -1,194 +0,0 @@ -/** - * @file cfmaterial_property_animation.cpp - * @brief Material Design 3 Property Animation Implementation - * @version 0.1 - * @date 2026-03-01 - * - * @copyright Copyright (c) 2026 - * - * @details - * Implements the CFMaterialPropertyAnimation class for float property - * animations following Material Design 3 motion specifications. - */ - -#include "cfmaterial_property_animation.h" -#include "base/easing.h" -#include - -namespace cf::ui::components::material { - -// ============================================================================= -// Constructor / Destructor -// ============================================================================= - -CFMaterialPropertyAnimation::CFMaterialPropertyAnimation( - float* value, float from, float to, int durationMs, base::Easing::Type easing, QObject* parent) - : ICFAbstractAnimation(parent) - , m_value(value) - , m_from(from) - , m_to(to) - , m_durationMs(durationMs) - , m_easing(easing) - , m_elapsed(0) - , m_targetWidget(nullptr) - , m_timer(new QTimer(this)) { - - // Setup timer with dynamic interval - m_timer->setInterval(calculateInterval()); - connect(m_timer, &QTimer::timeout, this, &CFMaterialPropertyAnimation::onTimerTick); - - // Set initial value - if (m_value) { - *m_value = from; - } -} - -CFMaterialPropertyAnimation::~CFMaterialPropertyAnimation() { - if (m_timer) { - m_timer->stop(); - } -} - -// ============================================================================= -// ICFAbstractAnimation Interface -// ============================================================================= - -void CFMaterialPropertyAnimation::start(Direction dir) { - if (m_state == State::Running) { - return; // Already running - } - - m_elapsed = 0; - m_state = State::Running; - - // Set initial value based on direction - if (m_value) { - if (dir == Direction::Forward) { - *m_value = m_from; - } else { - *m_value = m_to; - } - } - - emit started(); - - // Update target widget - if (m_targetWidget) { - m_targetWidget->update(); - } - - // Start the internal timer (60 FPS ~ 16ms) - if (m_timer) { - m_timer->start(); - } -} - -void CFMaterialPropertyAnimation::pause() { - if (m_state == State::Running) { - m_state = State::Paused; - if (m_timer) { - m_timer->stop(); - } - emit paused(); - } -} - -void CFMaterialPropertyAnimation::stop() { - m_state = State::Idle; - if (m_timer) { - m_timer->stop(); - } - m_elapsed = 0; - - // Reset to initial value - if (m_value) { - *m_value = m_from; - } - - if (m_targetWidget) { - m_targetWidget->update(); - } - - emit stopped(); -} - -void CFMaterialPropertyAnimation::reverse() { - if (m_state == State::Running) { - // Calculate remaining progress and reverse from there - float progress = static_cast(m_elapsed) / m_durationMs; - progress = 1.0f - progress; // Reverse progress - - m_elapsed = static_cast(progress * m_durationMs); - - // Swap from/to for reverse - std::swap(m_from, m_to); - } else { - // Start in reverse direction - start(Direction::Backward); - } - - emit reversed(); -} - -bool CFMaterialPropertyAnimation::tick(int dt) { - if (m_state != State::Running) { - return false; - } - - m_elapsed += dt; - - // Calculate progress - float progress = static_cast(m_elapsed) / m_durationMs; - - // Clamp progress - if (progress > 1.0f) { - progress = 1.0f; - } - - // Apply easing - QEasingCurve easingCurve = base::Easing::fromEasingType(m_easing); - float easedProgress = static_cast(easingCurve.valueForProgress(progress)); - - // Calculate current value based on range - if (m_value) { - *m_value = m_from + (m_to - m_from) * easedProgress; - } - - // Emit progress signal - emit progressChanged(easedProgress); - - // Update target widget - if (m_targetWidget) { - m_targetWidget->update(); - } - - // Check if animation is complete - if (progress >= 1.0f) { - m_state = State::Finished; - if (m_timer) { - m_timer->stop(); - } - emit finished(); - return false; - } - - return true; -} - -// ============================================================================= -// Property-Specific Methods -// ============================================================================= - -void CFMaterialPropertyAnimation::setTargetWidget(QWidget* widget) { - m_targetWidget = widget; -} - -// ============================================================================= -// Private Slots -// ============================================================================= - -void CFMaterialPropertyAnimation::onTimerTick() { - tick(calculateInterval()); -} - -} // namespace cf::ui::components::material diff --git a/ui/components/material/cfmaterial_property_animation.h b/ui/components/material/cfmaterial_property_animation.h deleted file mode 100644 index 0d9fa2d7a..000000000 --- a/ui/components/material/cfmaterial_property_animation.h +++ /dev/null @@ -1,257 +0,0 @@ -/** - * @file cfmaterial_property_animation.h - * @brief Material Design 3 Property Animation - * @version 0.1 - * @date 2026-03-01 - * - * @copyright Copyright (c) 2026 - * - * @details - * Implements a property animation that follows Material Design 3 motion - * specifications. Animates a float property from a start value to an end value - * using the specified duration and easing curve. - * - * This animation is designed for simple property animations like: - * - Floating label progress (0.0 to 1.0) - * - Scale animations - * - Opacity transitions - * - Any float property animation - * - * @ingroup ui_components_material - */ -#pragma once - -#include "aex/weak_ptr/weak_ptr.h" -#include "aex/weak_ptr/weak_ptr_factory.h" -#include "base/easing.h" -#include "components/timing_animation.h" -#include "core/motion_spec.h" -#include "export.h" -#include - -namespace cf::ui::components::material { - -/** - * @brief Material Design 3 Property Animation. - * - * @details Animates a float property from a start value to an end value - * following Material Design 3 motion specifications. - * - * The animation uses: - * - Duration specified in milliseconds - * - Easing curve for smooth motion - * - Direct property update via pointer - * - * @note The property pointer must remain valid during animation. - * @warning None - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * // Create a property animation - * float scale = 0.0f; - * auto propAnim = std::make_unique( - * &scale, 0.0f, 1.0f, 200, Easing::Type::EmphasizedDecelerate, this); - * - * // Connect to finished signal - * connect(propAnim.get(), &CFMaterialPropertyAnimation::finished, - * this, [&scale]() { - * qDebug() << "Final scale:" << scale; - * }); - * - * // Start the animation - * propAnim->start(); - * @endcode - */ -class CF_UI_EXPORT CFMaterialPropertyAnimation : public ICFAbstractAnimation { - Q_OBJECT - - public: - /** - * @brief Constructor with property pointer. - * - * @param[in] value Pointer to the float property to animate. - * Must remain valid for the lifetime of this animation. - * @param[in] from Start value of the animation. - * @param[in] to End value of the animation. - * @param[in] durationMs Duration of the animation in milliseconds. - * @param[in] easing Easing type for the animation. - * @param[in] parent QObject parent. - * - * @throws None - * @note The value pointer must remain valid for the lifetime - * of this animation. - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - CFMaterialPropertyAnimation( - float* value, float from, float to, int durationMs, - base::Easing::Type easing = base::Easing::Type::EmphasizedDecelerate, - QObject* parent = nullptr); - - /** - * @brief Destructor. - * - * @since 0.1 - */ - ~CFMaterialPropertyAnimation() override; - - // Non-copyable, non-movable - CFMaterialPropertyAnimation(const CFMaterialPropertyAnimation&) = delete; - CFMaterialPropertyAnimation& operator=(const CFMaterialPropertyAnimation&) = delete; - CFMaterialPropertyAnimation(CFMaterialPropertyAnimation&&) = delete; - CFMaterialPropertyAnimation& operator=(CFMaterialPropertyAnimation&&) = delete; - - // ========================================================================= - // ICFAbstractAnimation Interface - // ========================================================================= - - /** - * @brief Start the animation. - * - * @param[in] dir Direction to play the animation. - * - * @since 0.1 - */ - void start(Direction dir = Direction::Forward) override; - - /** - * @brief Pause the animation. - * - * @since 0.1 - */ - void pause() override; - - /** - * @brief Stop the animation and reset to initial state. - * - * @since 0.1 - */ - void stop() override; - - /** - * @brief Reverse the animation direction. - * - * @since 0.1 - */ - void reverse() override; - - /** - * @brief Update animation state by delta time. - * - * @param[in] dt Delta time in milliseconds. - * - * @return true if animation is still running, false if finished. - * - * @since 0.1 - */ - bool tick(int dt) override; - - /** - * @brief Gets a weak pointer to this animation. - * - * @return Weak pointer to this animation instance. - * - * @throws None - * @note Provides safe access to potentially destroyed animation. - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - aex::WeakPtr GetWeakPtr() override { return weak_factory_.GetWeakPtr(); } - - // ========================================================================= - // Property-Specific Methods - // ========================================================================= - - /** - * @brief Set the target widget for repaint notifications. - * - * @details The widget receives update() calls during animation - * to trigger repaints. - * - * @param[in] widget Target widget (may be nullptr). - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - void setTargetWidget(QWidget* widget); - - /** - * @brief Get the current value. - * - * @return Current animated value. - * - * @since 0.1 - */ - float currentValue() const { return m_value ? *m_value : 0.0f; } - - /** - * @brief Set the value range for the animation. - * - * @details Allows reusing the same animation object with different - * start/end values. Call before start() to update the range. - * - * @param[in] from Start value of the animation. - * @param[in] to End value of the animation. - * - * @since 0.1 - */ - void setRange(float from, float to) { - m_from = from; - m_to = to; - } - - private slots: - /** - * @brief Internal timer tick handler. - * - * @since 0.1 - */ - void onTimerTick(); - - private: - /// Pointer to the float property being animated - float* m_value; - - /// Start value - float m_from; - - /// End value - float m_to; - - /// Duration in milliseconds - int m_durationMs; - - /// Easing type - base::Easing::Type m_easing; - - /// Elapsed time in milliseconds - int m_elapsed; - - /// Target widget for repaint notifications - QWidget* m_targetWidget; - - /// Internal timer - QTimer* m_timer; - - /// Target FPS for this animation (default 60.0f) - float m_targetFps = 60.0f; - - /** - * @brief Calculate the timer interval based on target FPS. - * - * @return Timer interval in milliseconds. - */ - int calculateInterval() const { return static_cast(1000.0f / m_targetFps); } - - /// aex::WeakPtrFactory for creating weak pointers to this animation - /// Must be the last member to ensure it's destroyed first - aex::WeakPtrFactory weak_factory_{this}; -}; - -} // namespace cf::ui::components::material diff --git a/ui/components/material/cfmaterial_scale_animation.cpp b/ui/components/material/cfmaterial_scale_animation.cpp deleted file mode 100644 index da68bd346..000000000 --- a/ui/components/material/cfmaterial_scale_animation.cpp +++ /dev/null @@ -1,221 +0,0 @@ -/** - * @file cfmaterial_scale_animation.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Scale Animation Implementation - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Implements the CFMaterialScaleAnimation class for size-based animations - * following Material Design 3 motion specifications. - */ - -#include "cfmaterial_scale_animation.h" -#include -#include - -namespace cf::ui::components::material { - -// ============================================================================= -// Constructor / Destructor -// ============================================================================= - -CFMaterialScaleAnimation::CFMaterialScaleAnimation(cf::ui::core::IMotionSpec* spec, QObject* parent) - : ICFTimingAnimation(spec, parent), currentScale_(1.0f), targetWidget_(nullptr), - scaleFromCenter_(true), durationMs_(200), delayMs_(0), elapsedTime_(0) {} - -CFMaterialScaleAnimation::~CFMaterialScaleAnimation() { - // Restore original geometry if widget still exists - if (targetWidget_ && !originalGeometry_.isEmpty()) { - targetWidget_->setGeometry(originalGeometry_); - } -} - -// ============================================================================= -// ICFAbstractAnimation Interface -// ============================================================================= - -void CFMaterialScaleAnimation::start(Direction dir) { - if (m_state == State::Running) { - return; // Already running - } - - if (!targetWidget_) { - qWarning() << "CFMaterialScaleAnimation::start: No target widget set"; - return; - } - - // Store original geometry - originalGeometry_ = targetWidget_->geometry(); - - // Get duration from motion spec if available - if (motion_spec_) { - // Duration would be queried from the motion token name - // For now, use default - durationMs_ = 200; // shortEnter default - } - - elapsedTime_ = 0; - m_state = State::Running; - - // Set initial scale - if (dir == Direction::Forward) { - currentScale_ = m_from; - } else { - currentScale_ = m_to; - } - - emit started(); - - // Apply initial scale - applyScale(currentScale_); - - // Start the internal timer to drive animation - if (driven_internal_timer) { - driven_internal_timer->start(calculateInterval()); - } -} - -void CFMaterialScaleAnimation::pause() { - if (m_state == State::Running) { - m_state = State::Paused; - if (driven_internal_timer) { - driven_internal_timer->stop(); - } - emit paused(); - } -} - -void CFMaterialScaleAnimation::stop() { - m_state = State::Idle; - if (driven_internal_timer) { - driven_internal_timer->stop(); - } - elapsedTime_ = 0; - - // Reset to original geometry - currentScale_ = m_from; - if (targetWidget_ && !originalGeometry_.isEmpty()) { - targetWidget_->setGeometry(originalGeometry_); - } - - emit stopped(); -} - -void CFMaterialScaleAnimation::reverse() { - if (m_state == State::Running) { - // Calculate remaining progress and reverse from there - float progress = static_cast(elapsedTime_) / durationMs_; - progress = 1.0f - progress; // Reverse progress - - elapsedTime_ = static_cast(progress * durationMs_); - } else { - // Start in reverse direction - start(Direction::Backward); - } - - emit reversed(); -} - -bool CFMaterialScaleAnimation::tick(int dt) { - if (m_state != State::Running || !targetWidget_) { - return false; - } - - elapsedTime_ += dt; - - // Check if we're still in delay period - if (elapsedTime_ < delayMs_) { - return true; // Still waiting - } - - // Calculate actual animation time (subtract delay) - int animTime = elapsedTime_ - delayMs_; - - // Calculate progress - float progress = static_cast(animTime) / durationMs_; - - // Clamp progress - if (progress > 1.0f) { - progress = 1.0f; - } - - // Apply easing - float easedProgress = calculateEasedProgress(progress); - - // Calculate current scale based on range - currentScale_ = m_from + (m_to - m_from) * easedProgress; - - // Apply scale - applyScale(currentScale_); - - // Emit progress signal - emit progressChanged(easedProgress); - - // Check if animation is complete - if (progress >= 1.0f) { - m_state = State::Finished; - emit finished(); - return false; - } - - return true; -} - -// ============================================================================= -// Scale-Specific Methods -// ============================================================================= - -void CFMaterialScaleAnimation::setTargetWidget(QWidget* widget) { - targetWidget_ = widget; -} - -void CFMaterialScaleAnimation::applyScale(float scale) { - if (!targetWidget_ || originalGeometry_.isEmpty()) { - return; - } - - // Calculate scaled geometry - QRect scaledGeometry = calculateScaledGeometry(scale); - - // Apply new geometry - targetWidget_->setGeometry(scaledGeometry); -} - -QRect CFMaterialScaleAnimation::calculateScaledGeometry(float scale) const { - if (originalGeometry_.isEmpty()) { - return QRect(); - } - - // Calculate new size - int newWidth = static_cast(originalGeometry_.width() * scale); - int newHeight = static_cast(originalGeometry_.height() * scale); - - // Ensure minimum size - newWidth = qMax(1, newWidth); - newHeight = qMax(1, newHeight); - - if (scaleFromCenter_) { - // Calculate position to maintain center point - int widthDiff = newWidth - originalGeometry_.width(); - int heightDiff = newHeight - originalGeometry_.height(); - - int newX = originalGeometry_.x() - widthDiff / 2; - int newY = originalGeometry_.y() - heightDiff / 2; - - return QRect(newX, newY, newWidth, newHeight); - } else { - // Scale from top-left corner - return QRect(originalGeometry_.x(), originalGeometry_.y(), newWidth, newHeight); - } -} - -float CFMaterialScaleAnimation::calculateEasedProgress(float linearProgress) const { - // For now, use simple linear easing - // TODO: Integrate with MaterialMotionScheme for proper easing curves - return linearProgress; -} - -} // namespace cf::ui::components::material diff --git a/ui/components/material/cfmaterial_scale_animation.h b/ui/components/material/cfmaterial_scale_animation.h deleted file mode 100644 index c82442024..000000000 --- a/ui/components/material/cfmaterial_scale_animation.h +++ /dev/null @@ -1,291 +0,0 @@ -/** - * @file cfmaterial_scale_animation.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Scale Animation - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Implements a scale animation that follows Material Design 3 motion - * specifications. Animates the size of a widget from a start scale - * to an end scale using the specified timing and easing curve. - * - * This animation integrates with the MaterialMotionScheme to obtain - * duration and easing values based on the motion token (e.g., "md.motion.shortEnter"). - * - * @ingroup ui_components_material - */ -#pragma once - -#include "aex/weak_ptr/weak_ptr.h" -#include "aex/weak_ptr/weak_ptr_factory.h" -#include "components/timing_animation.h" -#include "core/motion_spec.h" -#include "export.h" -#include -#include - -namespace cf::ui::components::material { - -/** - * @brief Material Design 3 Scale Animation. - * - * @details Animates the scale of a widget from a start value to an end - * value following Material Design 3 motion specifications. - * - * The animation uses: - * - Duration from the MotionSpec token - * - Easing curve from the MotionSpec token - * - Optional delay before starting - * - * Scale is applied via QWidget::setGeometry() with adjusted - * size and position to maintain the widget's center point. - * - * @note The target widget must remain valid during animation. - * @warning Changing the target widget during animation may cause issues. - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * // Create a scale-up animation - * auto& motionSpec = theme.motion_spec(); - * auto scaleAnim = std::make_unique( - * aex::WeakPtr(&motionSpec), - * this); - * scaleAnim->setRange(0.8f, 1.0f); // Scale from 80% to 100% - * scaleAnim->setTargetWidget(myWidget); - * scaleAnim->start(); - * @endcode - */ -class CF_UI_EXPORT CFMaterialScaleAnimation : public ICFTimingAnimation { - Q_OBJECT - - public: - /** - * @brief Constructor with motion spec. - * - * @param[in] spec Raw pointer to the motion spec for timing/easing. - * Must remain valid for the lifetime of this animation. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - explicit CFMaterialScaleAnimation(cf::ui::core::IMotionSpec* spec, QObject* parent = nullptr); - - /** - * @brief Destructor. - * - * @since 0.1 - */ - ~CFMaterialScaleAnimation() override; - - // Non-copyable, non-movable - CFMaterialScaleAnimation(const CFMaterialScaleAnimation&) = delete; - CFMaterialScaleAnimation& operator=(const CFMaterialScaleAnimation&) = delete; - CFMaterialScaleAnimation(CFMaterialScaleAnimation&&) = delete; - CFMaterialScaleAnimation& operator=(CFMaterialScaleAnimation&&) = delete; - - // ========================================================================= - // ICFAbstractAnimation Interface - // ========================================================================= - - /** - * @brief Start the scale animation. - * - * @param[in] dir Direction to play. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - void start(Direction dir = Direction::Forward) override; - - /** - * @brief Pause the animation. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - void pause() override; - - /** - * @brief Stop the animation and reset to initial state. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - void stop() override; - - /** - * @brief Reverse the animation direction. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - void reverse() override; - - /** - * @brief Update animation state. - * - * @param[in] dt Time elapsed since last tick (milliseconds). - * - * @return true if animation is still running, false if finished. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - bool tick(int dt) override; - - // ========================================================================= - // ICFTimingAnimation Interface - // ========================================================================= - - /** - * @brief Get the current scale value. - * - * @return Current scale (1.0 = normal size). - * - * @since 0.1 - */ - float currentValue() const override { return currentScale_; } - - // ========================================================================= - // Scale-Specific Methods - // ========================================================================= - - /** - * @brief Set the target widget for the scale animation. - * - * @param[in] widget Target widget (may be nullptr). - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - void setTargetWidget(QWidget* widget); - - /** - * @brief Get the target widget. - * - * @return Target widget, or nullptr if not set. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - QWidget* targetWidget() const { return targetWidget_; } - - /** - * @brief Set whether to scale from center. - * - * @param[in] center If true, widget scales from its center point. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - void setScaleFromCenter(bool center) { scaleFromCenter_ = center; } - - /** - * @brief Check if scaling from center. - * - * @return true if scaling from center. - * - * @since 0.1 - */ - bool scaleFromCenter() const { return scaleFromCenter_; } - - /** - * @brief Get a weak pointer to this animation. - * - * @return aex::WeakPtr that can be used to safely access this animation. - * - * @since 0.1 - */ - aex::WeakPtr GetWeakPtr() override { return weak_factory_.GetWeakPtr(); } - - private: - /// Current scale value (1.0 = normal size) - float currentScale_ = 1.0f; - - /// Target widget to animate - QWidget* targetWidget_ = nullptr; - - /// Original geometry of the widget (stored at animation start) - QRect originalGeometry_; - - /// Whether to scale from the center point - bool scaleFromCenter_ = true; - - /// Duration in milliseconds - int durationMs_ = 200; - - /// Delay in milliseconds before starting - int delayMs_ = 0; - - /// Elapsed time (including delay) - int elapsedTime_ = 0; - - /** - * @brief Apply the current scale to the target widget. - * - * @param scale Scale value (1.0 = normal size). - * - * @since 0.1 - */ - void applyScale(float scale); - - /** - * @brief Calculate the geometry for a given scale value. - * - * @param scale Scale value. - * - * @return QRect representing the scaled geometry. - * - * @since 0.1 - */ - QRect calculateScaledGeometry(float scale) const; - - /** - * @brief Calculate eased progress based on timing function. - * - * @param linearProgress Linear progress (0.0 to 1.0). - * - * @return Eased progress (0.0 to 1.0). - * - * @since 0.1 - */ - float calculateEasedProgress(float linearProgress) const; - - /// aex::WeakPtrFactory for creating weak pointers to this animation - /// Must be the last member to ensure it's destroyed first - aex::WeakPtrFactory weak_factory_{this}; -}; - -} // namespace cf::ui::components::material diff --git a/ui/components/material/cfmaterial_slide_animation.cpp b/ui/components/material/cfmaterial_slide_animation.cpp deleted file mode 100644 index d97f83ea6..000000000 --- a/ui/components/material/cfmaterial_slide_animation.cpp +++ /dev/null @@ -1,214 +0,0 @@ -/** - * @file cfmaterial_slide_animation.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Slide Animation Implementation - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Implements the CFMaterialSlideAnimation class for position-based animations - * following Material Design 3 motion specifications. - */ - -#include "cfmaterial_slide_animation.h" -#include "base/easing.h" -#include -#include - -namespace cf::ui::components::material { - -// ============================================================================= -// Constructor / Destructor -// ============================================================================= - -CFMaterialSlideAnimation::CFMaterialSlideAnimation(cf::ui::core::IMotionSpec* spec, - SlideDirection direction, QObject* parent) - : ICFTimingAnimation(spec, parent), currentOffset_(0.0f), targetWidget_(nullptr), - direction_(direction), distance_(100.0f), durationMs_(300), delayMs_(0), elapsedTime_(0) {} - -CFMaterialSlideAnimation::~CFMaterialSlideAnimation() { - // Restore original position if widget still exists - if (targetWidget_ && !originalPosition_.isNull()) { - targetWidget_->move(originalPosition_); - } -} - -// ============================================================================= -// ICFAbstractAnimation Interface -// ============================================================================= - -void CFMaterialSlideAnimation::start(Direction dir) { - if (m_state == State::Running) { - return; // Already running - } - - if (!targetWidget_) { - qWarning() << "CFMaterialSlideAnimation::start: No target widget set"; - return; - } - - // Store original position - originalPosition_ = targetWidget_->pos(); - - // Get duration from motion spec if available - if (motion_spec_) { - // Duration would be queried from the motion token name - // For now, use default based on direction - durationMs_ = 300; // mediumEnter default - } - - elapsedTime_ = 0; - m_state = State::Running; - - // Set initial offset - if (dir == Direction::Forward) { - currentOffset_ = m_from; - } else { - currentOffset_ = m_to; - } - - emit started(); - - // Apply initial offset - applyOffset(currentOffset_); - - // Start the internal timer to drive animation - if (driven_internal_timer) { - driven_internal_timer->start(calculateInterval()); - } -} - -void CFMaterialSlideAnimation::pause() { - if (m_state == State::Running) { - m_state = State::Paused; - if (driven_internal_timer) { - driven_internal_timer->stop(); - } - emit paused(); - } -} - -void CFMaterialSlideAnimation::stop() { - m_state = State::Idle; - if (driven_internal_timer) { - driven_internal_timer->stop(); - } - elapsedTime_ = 0; - - // Reset to original position - currentOffset_ = m_from; - if (targetWidget_ && !originalPosition_.isNull()) { - targetWidget_->move(originalPosition_); - } - - emit stopped(); -} - -void CFMaterialSlideAnimation::reverse() { - if (m_state == State::Running) { - // Calculate remaining progress and reverse from there - float progress = static_cast(elapsedTime_) / durationMs_; - progress = 1.0f - progress; // Reverse progress - - elapsedTime_ = static_cast(progress * durationMs_); - } else { - // Start in reverse direction - start(Direction::Backward); - } - - emit reversed(); -} - -bool CFMaterialSlideAnimation::tick(int dt) { - if (m_state != State::Running || !targetWidget_) { - return false; - } - - elapsedTime_ += dt; - - // Check if we're still in delay period - if (elapsedTime_ < delayMs_) { - return true; // Still waiting - } - - // Calculate actual animation time (subtract delay) - int animTime = elapsedTime_ - delayMs_; - - // Calculate progress - float progress = static_cast(animTime) / durationMs_; - - // Clamp progress - if (progress > 1.0f) { - progress = 1.0f; - } - - // Apply easing - float easedProgress = calculateEasedProgress(progress); - - // Calculate current offset based on range - currentOffset_ = m_from + (m_to - m_from) * easedProgress; - - // Apply offset - applyOffset(currentOffset_); - - // Emit progress signal - emit progressChanged(easedProgress); - - // Check if animation is complete - if (progress >= 1.0f) { - m_state = State::Finished; - emit finished(); - return false; - } - - return true; -} - -// ============================================================================= -// Slide-Specific Methods -// ============================================================================= - -void CFMaterialSlideAnimation::setTargetWidget(QWidget* widget) { - targetWidget_ = widget; -} - -void CFMaterialSlideAnimation::applyOffset(float offset) { - if (!targetWidget_ || originalPosition_.isNull()) { - return; - } - - // Calculate offset point - QPoint offsetPoint = calculateOffsetPoint(offset); - - // Apply new position - targetWidget_->move(originalPosition_ + offsetPoint); -} - -QPoint CFMaterialSlideAnimation::calculateOffsetPoint(float offset) const { - switch (direction_) { - case SlideDirection::Up: - return QPoint(0, static_cast(-offset)); // Move up (negative Y) - - case SlideDirection::Down: - return QPoint(0, static_cast(offset)); // Move down (positive Y) - - case SlideDirection::Left: - return QPoint(static_cast(-offset), 0); // Move left (negative X) - - case SlideDirection::Right: - return QPoint(static_cast(offset), 0); // Move right (positive X) - - default: - return QPoint(); - } -} - -float CFMaterialSlideAnimation::calculateEasedProgress(float linearProgress) const { - // For now, use simple linear easing - // TODO: Integrate with MaterialMotionScheme for proper easing curves - return linearProgress; -} - -} // namespace cf::ui::components::material diff --git a/ui/components/material/cfmaterial_slide_animation.h b/ui/components/material/cfmaterial_slide_animation.h deleted file mode 100644 index e64abc0bd..000000000 --- a/ui/components/material/cfmaterial_slide_animation.h +++ /dev/null @@ -1,343 +0,0 @@ -/** - * @file cfmaterial_slide_animation.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Slide Animation - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Implements a slide animation that follows Material Design 3 motion - * specifications. Animates the position of a widget from a start offset - * to an end offset using the specified timing and easing curve. - * - * This animation integrates with the MaterialMotionScheme to obtain - * duration and easing values based on the motion token (e.g., "md.motion.mediumEnter"). - * - * @ingroup ui_components_material - */ -#pragma once - -#include "aex/weak_ptr/weak_ptr.h" -#include "aex/weak_ptr/weak_ptr_factory.h" -#include "components/timing_animation.h" -#include "core/motion_spec.h" -#include "export.h" -#include -#include - -namespace cf::ui::components::material { - -/** - * @brief Slide direction enumeration. - * - * @details Defines the four cardinal directions for slide animations. - * - * @since 0.1 - * @ingroup ui_components_material - */ -enum class SlideDirection { - Up, ///< Slide upward (widget moves from bottom to top) - Down, ///< Slide downward (widget moves from top to bottom) - Left, ///< Slide leftward (widget moves from right to left) - Right ///< Slide rightward (widget moves from left to right) -}; - -/** - * @brief Material Design 3 Slide Animation. - * - * @details Animates the position of a widget from a start offset to an - * end offset following Material Design 3 motion specifications. - * - * The animation uses: - * - Duration from the MotionSpec token - * - Easing curve from the MotionSpec token - * - Optional delay before starting - * - * Position is applied via widget movement using setGeometry() - * or pos(). The original position is stored and restored after - * animation completion. - * - * @note The target widget must remain valid during animation. - * @warning Changing the target widget during animation may cause issues. - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * // Create a slide-up animation - * auto& motionSpec = theme.motion_spec(); - * auto slideAnim = std::make_unique( - * aex::WeakPtr(&motionSpec), - * SlideDirection::Up, - * this); - * slideAnim->setDistance(100.0f); // Slide 100 pixels - * slideAnim->setTargetWidget(myWidget); - * slideAnim->start(); - * @endcode - */ -class CF_UI_EXPORT CFMaterialSlideAnimation : public ICFTimingAnimation { - Q_OBJECT - - public: - /** - * @brief Constructor with motion spec and direction. - * - * @param[in] spec Raw pointer to the motion spec for timing/easing. - * Must remain valid for the lifetime of this animation. - * @param[in] direction Direction of the slide animation. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - explicit CFMaterialSlideAnimation(cf::ui::core::IMotionSpec* spec, SlideDirection direction, - QObject* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - ~CFMaterialSlideAnimation() override; - - // Non-copyable, non-movable - CFMaterialSlideAnimation(const CFMaterialSlideAnimation&) = delete; - CFMaterialSlideAnimation& operator=(const CFMaterialSlideAnimation&) = delete; - CFMaterialSlideAnimation(CFMaterialSlideAnimation&&) = delete; - CFMaterialSlideAnimation& operator=(CFMaterialSlideAnimation&&) = delete; - - // ========================================================================= - // ICFAbstractAnimation Interface - // ========================================================================= - - /** - * @brief Start the slide animation. - * - * @param[in] dir Direction to play. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - void start(Direction dir = Direction::Forward) override; - - /** - * @brief Pause the animation. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - void pause() override; - - /** - * @brief Stop the animation and reset to initial state. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - void stop() override; - - /** - * @brief Reverse the animation direction. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - void reverse() override; - - /** - * @brief Update animation state. - * - * @param[in] dt Time elapsed since last tick (milliseconds). - * - * @return true if animation is still running, false if finished. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - bool tick(int dt) override; - - // ========================================================================= - // ICFTimingAnimation Interface - // ========================================================================= - - /** - * @brief Get the current offset value. - * - * @return Current offset in pixels. - * - * @since 0.1 - */ - float currentValue() const override { return currentOffset_; } - - // ========================================================================= - // Slide-Specific Methods - // ========================================================================= - - /** - * @brief Set the target widget for the slide animation. - * - * @param[in] widget Target widget (may be nullptr). - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - void setTargetWidget(QWidget* widget); - - /** - * @brief Get the target widget. - * - * @return Target widget, or nullptr if not set. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - QWidget* targetWidget() const { return targetWidget_; } - - /** - * @brief Set the slide distance in pixels. - * - * @param[in] distance Distance to slide in pixels. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - void setDistance(float distance) { distance_ = distance; } - - /** - * @brief Get the slide distance. - * - * @return Distance in pixels. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - float distance() const { return distance_; } - - /** - * @brief Set the slide direction. - * - * @param[in] direction Direction to slide. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - void setDirection(SlideDirection direction) { direction_ = direction; } - - /** - * @brief Get the slide direction. - * - * @return Current slide direction. - * - * @since 0.1 - */ - SlideDirection direction() const { return direction_; } - - /** - * @brief Get a weak pointer to this animation. - * - * @return aex::WeakPtr that can be used to safely access this animation. - * - * @since 0.1 - */ - aex::WeakPtr GetWeakPtr() override { return weak_factory_.GetWeakPtr(); } - - private: - /// Current offset value in pixels - float currentOffset_ = 0.0f; - - /// Target widget to animate - QWidget* targetWidget_ = nullptr; - - /// Original position of the widget (stored at animation start) - QPoint originalPosition_; - - /// Direction of the slide - SlideDirection direction_ = SlideDirection::Up; - - /// Distance to slide in pixels - float distance_ = 100.0f; - - /// Duration in milliseconds - int durationMs_ = 300; - - /// Delay in milliseconds before starting - int delayMs_ = 0; - - /// Elapsed time (including delay) - int elapsedTime_ = 0; - - /** - * @brief Apply the current offset to the target widget. - * - * @param offset Offset value in pixels. - * - * @since 0.1 - */ - void applyOffset(float offset); - - /** - * @brief Calculate the offset point for a given offset value. - * - * @param offset Offset distance in pixels. - * - * @return QPoint representing the offset. - * - * @since 0.1 - */ - QPoint calculateOffsetPoint(float offset) const; - - /** - * @brief Calculate eased progress based on timing function. - * - * @param linearProgress Linear progress (0.0 to 1.0). - * - * @return Eased progress (0.0 to 1.0). - * - * @since 0.1 - */ - float calculateEasedProgress(float linearProgress) const; - - /// aex::WeakPtrFactory for creating weak pointers to this animation - /// Must be the last member to ensure it's destroyed first - aex::WeakPtrFactory weak_factory_{this}; -}; - -} // namespace cf::ui::components::material diff --git a/ui/components/material/token/animation_token_literals.h b/ui/components/material/token/animation_token_literals.h deleted file mode 100644 index 6d6fe4cf0..000000000 --- a/ui/components/material/token/animation_token_literals.h +++ /dev/null @@ -1,198 +0,0 @@ -/** - * @file animation_token_literals.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Animation Token String Literals - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Compile-time string constants for Material Design 3 animation tokens. - * These literals are used throughout the animation system to ensure - * consistency and avoid string duplication. - * - * Animation tokens follow the naming pattern: "md.animation." - * where type is "fade", "slide", "scale", etc. and direction is "In", "Out". - * - * @ingroup ui_components_material - */ -#pragma once - -#include - -namespace cf::ui::components::material::token_literals { - -// ============================================================================= -// Animation Token Literals - Fade Animations -// ============================================================================= - -/** - * @brief Fade-in animation token. - * - * @details Opacity animation from 0 to 1. Uses shortEnter timing by default. - * - * @since 0.1 - * @ingroup ui_components_material - */ -inline constexpr const char ANIMATION_FADE_IN[] = "md.animation.fadeIn"; - -/** - * @brief Fade-out animation token. - * - * @details Opacity animation from 1 to 0. Uses shortExit timing by default. - * - * @since 0.1 - * @ingroup ui_components_material - */ -inline constexpr const char ANIMATION_FADE_OUT[] = "md.animation.fadeOut"; - -// ============================================================================= -// Animation Token Literals - Slide Animations -// ============================================================================= - -/** - * @brief Slide-up animation token. - * - * @details Position animation sliding upward. Uses mediumEnter timing. - * - * @since 0.1 - * @ingroup ui_components_material - */ -inline constexpr const char ANIMATION_SLIDE_UP[] = "md.animation.slideUp"; - -/** - * @brief Slide-down animation token. - * - * @details Position animation sliding downward. Uses mediumExit timing. - * - * @since 0.1 - * @ingroup ui_components_material - */ -inline constexpr const char ANIMATION_SLIDE_DOWN[] = "md.animation.slideDown"; - -/** - * @brief Slide-left animation token. - * - * @details Position animation sliding left. Uses mediumEnter timing. - * - * @since 0.1 - * @ingroup ui_components_material - */ -inline constexpr const char ANIMATION_SLIDE_LEFT[] = "md.animation.slideLeft"; - -/** - * @brief Slide-right animation token. - * - * @details Position animation sliding right. Uses mediumEnter timing. - * - * @since 0.1 - * @ingroup ui_components_material - */ -inline constexpr const char ANIMATION_SLIDE_RIGHT[] = "md.animation.slideRight"; - -// ============================================================================= -// Animation Token Literals - Scale Animations -// ============================================================================= - -/** - * @brief Scale-up animation token. - * - * @details Size animation growing from smaller to normal. Uses shortEnter timing. - * - * @since 0.1 - * @ingroup ui_components_material - */ -inline constexpr const char ANIMATION_SCALE_UP[] = "md.animation.scaleUp"; - -/** - * @brief Scale-down animation token. - * - * @details Size animation shrinking from normal to smaller. Uses shortExit timing. - * - * @since 0.1 - * @ingroup ui_components_material - */ -inline constexpr const char ANIMATION_SCALE_DOWN[] = "md.animation.scaleDown"; - -// ============================================================================= -// Animation Token Literals - Rotate Animations -// ============================================================================= - -/** - * @brief Rotate-in animation token. - * - * @details Rotation animation for entering elements. Uses mediumEnter timing. - * - * @since 0.1 - * @ingroup ui_components_material - */ -inline constexpr const char ANIMATION_ROTATE_IN[] = "md.animation.rotateIn"; - -/** - * @brief Rotate-out animation token. - * - * @details Rotation animation for exiting elements. Uses mediumExit timing. - * - * @since 0.1 - * @ingroup ui_components_material - */ -inline constexpr const char ANIMATION_ROTATE_OUT[] = "md.animation.rotateOut"; - -// ============================================================================= -// Animation Token Literals - Ripple Animations -// ============================================================================= - -/** - * @brief Ripple expand animation token. - * - * @details Ripple effect radius expansion from 0 to maxRadius. Uses rippleExpand timing. - * - * @since 0.1 - * @ingroup ui_components_material - */ -inline constexpr const char ANIMATION_RIPPLE_EXPAND[] = "md.animation.rippleExpand"; - -/** - * @brief Ripple fade animation token. - * - * @details Ripple effect opacity fade-out from 1 to 0. Uses rippleFade timing. - * - * @since 0.1 - * @ingroup ui_components_material - */ -inline constexpr const char ANIMATION_RIPPLE_FADE[] = "md.animation.rippleFade"; - -// ============================================================================= -// All Animation Tokens Array (for iteration) -// ============================================================================= - -/** - * @brief Array containing all Material Design 3 animation token literals. - * - * @details Ordered by category: Fade (2), Slide (4), Scale (2), Rotate (2). - * - * @since 0.1 - * @ingroup ui_components_material - */ -inline constexpr const char* const ALL_ANIMATION_TOKENS[] = { - // Fade - ANIMATION_FADE_IN, ANIMATION_FADE_OUT, - // Slide - ANIMATION_SLIDE_UP, ANIMATION_SLIDE_DOWN, ANIMATION_SLIDE_LEFT, ANIMATION_SLIDE_RIGHT, - // Scale - ANIMATION_SCALE_UP, ANIMATION_SCALE_DOWN, - // Rotate - ANIMATION_ROTATE_IN, ANIMATION_ROTATE_OUT, - // Ripple - ANIMATION_RIPPLE_EXPAND, ANIMATION_RIPPLE_FADE}; - -/** - * @brief Total count of Material Design 3 animation tokens. - * - * @since 0.1 - * @ingroup ui_components_material - */ -inline constexpr size_t ANIMATION_TOKEN_COUNT = 12; - -} // namespace cf::ui::components::material::token_literals diff --git a/ui/components/material/token/animation_token_mapping.h b/ui/components/material/token/animation_token_mapping.h deleted file mode 100644 index 6e806ca4e..000000000 --- a/ui/components/material/token/animation_token_mapping.h +++ /dev/null @@ -1,292 +0,0 @@ -/** - * @file animation_token_mapping.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Animation Token to MotionSpec Mapping - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Defines the mapping between animation tokens and their corresponding - * MotionSpec configurations. This enables the factory to resolve a - * token like "md.animation.fadeIn" to a complete AnimationDescriptor - * with animation type, motion spec, property, and default values. - * - * The mapping follows Material Design 3 motion principles: - * - Fade: 200ms enter, 150ms exit - * - Slide: 300ms enter, 250ms exit - * - Scale: 200ms enter, 150ms exit - * - Rotate: 300ms enter, 250ms exit - * - * @ingroup ui_components_material - */ -#pragma once - -#include "animation_token_literals.h" -#include - -namespace cf::ui::components::material::token_literals { - -// ============================================================================= -// Animation Token → MotionSpec Mapping Structure -// ============================================================================= - -/** - * @brief Animation token mapping entry. - * - * @details Defines how an animation token maps to a concrete animation - * configuration. This includes: - * - The animation token string - * - The animation type (fade, slide, scale, etc.) - * - The motion spec token for timing/easing - * - The property to animate - * - Default start and end values - * - * @since 0.1 - * @ingroup ui_components_material - */ -struct AnimationTokenMapping { - /// The animation token (e.g., "md.animation.fadeIn") - const char* animationToken; - - /// Animation type: "fade", "slide", "scale", "rotate" - const char* animationType; - - /// Motion spec token: e.g., "md.motion.shortEnter" - const char* motionToken; - - /// Property to animate: "opacity", "positionX", "positionY", "scale" - const char* property; - - /// Default start value for the animation - float defaultFrom; - - /// Default end value for the animation - float defaultTo; - - /** - * @brief Check if this mapping matches a given token. - * - * @param[in] token The token string to compare. - * - * @return true if the animationToken matches, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ - bool matches(const char* token) const { - return animationToken && token && std::strcmp(animationToken, token) == 0; - } -}; - -// ============================================================================= -// Animation Token Mapping Table -// ============================================================================= - -/** - * @brief Complete animation token mapping table. - * - * @details Defines all predefined animation tokens and their - * corresponding configurations. This table is used by - * MaterialAnimationFactory to resolve tokens to descriptors. - * - * The table is ordered by animation category for easier lookup: - * - Fade animations (2 entries) - * - Slide animations (4 entries) - * - Scale animations (2 entries) - * - Rotate animations (2 entries) - * - Ripple animations (2 entries) - * - * @since 0.1 - * @ingroup ui_components_material - */ -inline constexpr AnimationTokenMapping TOKEN_MAPPINGS[] = { - // ========================================================================= - // Fade Animations - // ========================================================================= - { - ANIMATION_FADE_IN, // "md.animation.fadeIn" - "fade", // Animation type - "md.motion.shortEnter", // Motion spec (200ms, EmphasizedDecelerate) - "opacity", // Property to animate - 0.0f, // Start value (transparent) - 1.0f // End value (fully visible) - }, - { - ANIMATION_FADE_OUT, // "md.animation.fadeOut" - "fade", // Animation type - "md.motion.shortExit", // Motion spec (150ms, EmphasizedAccelerate) - "opacity", // Property to animate - 1.0f, // Start value (fully visible) - 0.0f // End value (transparent) - }, - - // ========================================================================= - // Slide Animations - // ========================================================================= - { - ANIMATION_SLIDE_UP, // "md.animation.slideUp" - "slide", // Animation type - "md.motion.mediumEnter", // Motion spec (300ms, EmphasizedDecelerate) - "positionY", // Property to animate - 100.0f, // Start value (offset downward) - 0.0f // End value (normal position) - }, - { - ANIMATION_SLIDE_DOWN, // "md.animation.slideDown" - "slide", // Animation type - "md.motion.mediumExit", // Motion spec (250ms, EmphasizedAccelerate) - "positionY", // Property to animate - 0.0f, // Start value (normal position) - 100.0f // End value (offset downward) - }, - { - ANIMATION_SLIDE_LEFT, // "md.animation.slideLeft" - "slide", // Animation type - "md.motion.mediumEnter", // Motion spec (300ms, EmphasizedDecelerate) - "positionX", // Property to animate - 100.0f, // Start value (offset right) - 0.0f // End value (normal position) - }, - { - ANIMATION_SLIDE_RIGHT, // "md.animation.slideRight" - "slide", // Animation type - "md.motion.mediumEnter", // Motion spec (300ms, EmphasizedDecelerate) - "positionX", // Property to animate - -100.0f, // Start value (offset left) - 0.0f // End value (normal position) - }, - - // ========================================================================= - // Scale Animations - // ========================================================================= - { - ANIMATION_SCALE_UP, // "md.animation.scaleUp" - "scale", // Animation type - "md.motion.shortEnter", // Motion spec (200ms, EmphasizedDecelerate) - "scale", // Property to animate - 0.8f, // Start value (smaller) - 1.0f // End value (normal size) - }, - { - ANIMATION_SCALE_DOWN, // "md.animation.scaleDown" - "scale", // Animation type - "md.motion.shortExit", // Motion spec (150ms, EmphasizedAccelerate) - "scale", // Property to animate - 1.0f, // Start value (normal size) - 0.8f // End value (smaller) - }, - - // ========================================================================= - // Rotate Animations - // ========================================================================= - { - ANIMATION_ROTATE_IN, // "md.animation.rotateIn" - "rotate", // Animation type - "md.motion.mediumEnter", // Motion spec (300ms, EmphasizedDecelerate) - "rotation", // Property to animate - -45.0f, // Start value (rotated counter-clockwise) - 0.0f // End value (normal rotation) - }, - { - ANIMATION_ROTATE_OUT, // "md.animation.rotateOut" - "rotate", // Animation type - "md.motion.mediumExit", // Motion spec (250ms, EmphasizedAccelerate) - "rotation", // Property to animate - 0.0f, // Start value (normal rotation) - 45.0f // End value (rotated clockwise) - }, - - // ========================================================================= - // Ripple Animations - // ========================================================================= - { - ANIMATION_RIPPLE_EXPAND, // "md.animation.rippleExpand" - "fade", // Animation type (uses progress for radius) - "rippleExpand", // Motion spec (400ms, Standard) - "progress", // Property to animate (used as radius multiplier) - 0.0f, // Start value (no ripple) - 1.0f // End value (full radius) - }, - { - ANIMATION_RIPPLE_FADE, // "md.animation.rippleFade" - "fade", // Animation type - "rippleFade", // Motion spec (150ms, Linear) - "opacity", // Property to animate - 1.0f, // Start value (fully visible) - 0.0f // End value (invisible) - }, -}; - -/** - * @brief Total number of animation token mappings. - * - * @since 0.1 - * @ingroup ui_components_material - */ -inline constexpr size_t TOKEN_MAPPING_COUNT = - sizeof(TOKEN_MAPPINGS) / sizeof(AnimationTokenMapping); - -// ============================================================================= -// Token Resolution Functions -// ============================================================================= - -/** - * @brief Find a mapping entry by animation token. - * - * @details Performs a linear search through the mapping table. - * Returns nullptr if the token is not found. - * - * @param token The animation token to look up. - * - * @return Pointer to the matching mapping entry, or nullptr if not found. - * - * @throws None - * @note Linear search is acceptable for small tables (< 100 entries). - * @warning The returned pointer is valid only as long as TOKEN_MAPPINGS - * is accessible (meaning: program lifetime). - * @since 0.1 - * @ingroup ui_components_material - * - * @code - * const auto* mapping = findTokenMapping("md.animation.fadeIn"); - * if (mapping) { - * // mapping->animationType == "fade" - * // mapping->motionToken == "md.motion.shortEnter" - * // mapping->property == "opacity" - * // mapping->defaultFrom == 0.0f - * // mapping->defaultTo == 1.0f - * } - * @endcode - */ -inline const AnimationTokenMapping* findTokenMapping(const char* token) { - for (size_t i = 0; i < TOKEN_MAPPING_COUNT; ++i) { - if (TOKEN_MAPPINGS[i].matches(token)) { - return &TOKEN_MAPPINGS[i]; - } - } - return nullptr; -} - -/** - * @brief Check if an animation token exists in the mapping table. - * - * @param[in] token The animation token to check. - * - * @return true if the token is found, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components_material - */ -inline bool hasTokenMapping(const char* token) { - return findTokenMapping(token) != nullptr; -} - -} // namespace cf::ui::components::material::token_literals diff --git a/ui/components/spring_animation.h b/ui/components/spring_animation.h deleted file mode 100644 index ac0236039..000000000 --- a/ui/components/spring_animation.h +++ /dev/null @@ -1,124 +0,0 @@ -/** - * @file spring_animation.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Spring-based Animation Interface - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Defines the spring-based animation interface that uses spring physics - * for natural motion. This provides animations that follow spring dynamics - * rather than fixed timing curves. - * - * @ingroup ui_components - */ -#pragma once - -#include "animation.h" -#include "base/easing.h" -#include - -namespace cf::ui::components { - -/** - * @brief Spring-based animation interface. - * - * @details Base class for animations that use spring physics for - * natural, bouncy motion following Material Design 3. - * - * @since 0.1 - * @ingroup ui_components - */ -class CF_UI_EXPORT ICFSpringAnimation : public ICFAbstractAnimation { - Q_OBJECT - public: - /** - * @brief Constructor with spring easing preset. - * - * @param[in] easing Spring preset for physics parameters. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - ICFSpringAnimation(const base::Easing::SpringPreset& easing, QObject* parent = nullptr) - : ICFAbstractAnimation(parent) { - easing_ = easing; - } - - /** - * @brief Sets the target value for the spring animation. - * - * @param[in] target Target value to animate toward. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - virtual void setTarget(float target) { m_target = target; } - - /** - * @brief Sets the initial velocity for the spring animation. - * - * @param[in] velocity Initial velocity value. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - virtual void setInitialVelocity(float velocity) { m_velocity = velocity; } - - /** - * @brief Gets the current animated value. - * - * @return Current animated value. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - virtual float currentValue() const = 0; - - /** - * @brief Updates the spring animation state. - * - * @details Uses springStep for physics-based interpolation. - * - * @param[in] dt Time elapsed since last tick (milliseconds). - * - * @return true if animation continues, false if finished. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - bool tick(int dt) override; // Using springStep - - protected: - /// Spring easing preset containing physics parameters (stiffness, damping). - base::Easing::SpringPreset easing_; - - /// Current position value in the spring animation. - float m_position = 0.0f; - - /// Current velocity value in the spring animation. - float m_velocity = 0.0f; - - /// Target value for the spring animation to settle at. - float m_target = 1.0f; -}; - -} // namespace cf::ui::components diff --git a/ui/components/timing_animation.cpp b/ui/components/timing_animation.cpp deleted file mode 100644 index ccf5554c2..000000000 --- a/ui/components/timing_animation.cpp +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @file timing_animation.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Timing-based Animation Interface Implementation - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Implementation file for ICFTimingAnimation interface. - * This file is required for Qt's MOC (Meta-Object Compiler) to generate - * the meta-object code for the Q_OBJECT interface. - */ - -#include "timing_animation.h" - -namespace cf::ui::components { - -ICFTimingAnimation::ICFTimingAnimation(cf::ui::core::IMotionSpec* spec, QObject* parent) - : ICFAbstractAnimation(parent), motion_spec_(spec) {} - -} // namespace cf::ui::components diff --git a/ui/components/timing_animation.h b/ui/components/timing_animation.h deleted file mode 100644 index fc5e479ad..000000000 --- a/ui/components/timing_animation.h +++ /dev/null @@ -1,100 +0,0 @@ -/** - * @file timing_animation.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Timing-based Animation Interface - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Defines the timing-based animation interface that uses motion spec - * for duration and easing values. This is the base class for animations - * that follow Material Design 3 motion specifications. - * - * @ingroup ui_components - */ -#pragma once - -#include "animation.h" -#include "core/motion_spec.h" -#include - -namespace cf::ui::components { - -// Forward declaration -namespace core { -struct IMotionSpec; -} - -/** - * @brief Timing-based animation interface. - * - * @details Base class for animations that use MotionSpec for timing - * and easing configuration following Material Design 3. - * - * NOTE: The motion_spec pointer must remain valid for the - * lifetime of this animation. This is guaranteed when the - * animation is created by CFMaterialAnimationFactory, which - * holds a reference to the theme that owns the motion spec. - * - * @since 0.1 - * @ingroup ui_components - */ -class CF_UI_EXPORT ICFTimingAnimation : public ICFAbstractAnimation { - Q_OBJECT - public: - /** - * @brief Constructor with motion specification. - * - * @param[in] spec Raw pointer to the motion spec for timing/easing. - * Must remain valid for the lifetime of this animation. - * @param[in] parent QObject parent. - * - * @throws None - * @note The motion_spec pointer must remain valid for the lifetime - * of this animation. - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - explicit ICFTimingAnimation(cf::ui::core::IMotionSpec* spec, QObject* parent = nullptr); - - /** - * @brief Sets the value range for the animation. - * - * @param[in] from Start value of the animation. - * @param[in] to End value of the animation. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - virtual void setRange(float from, float to) { - m_from = from; - m_to = to; - } - - /** - * @brief Gets the current animated value. - * - * @return Current animated value. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_components - */ - virtual float currentValue() const = 0; - - protected: - cf::ui::core::IMotionSpec* motion_spec_ = nullptr; - float m_from = 0.0f; - float m_to = 1.0f; - int m_elapsed = 0; -}; - -} // namespace cf::ui::components diff --git a/ui/core/CMakeLists.txt b/ui/core/CMakeLists.txt deleted file mode 100644 index 57c7f3917..000000000 --- a/ui/core/CMakeLists.txt +++ /dev/null @@ -1,37 +0,0 @@ -project(ui_core LANGUAGES CXX) - -log_info("UI_Core" "Start Configure the UI Core") - -# Include subdirectory CMakeLists (STATIC libraries) -add_subdirectory(material) - -# ============================================================ -# Unified UI Core Library - STATIC library for cfui.dll -# ============================================================ - -# STATIC library for linking into cfui.dll -# Note: color_scheme.h is a header-only interface file -add_library(cf_ui_core STATIC - theme_manager.cpp -) - -# Link against material sub-module and base -target_link_libraries(cf_ui_core PUBLIC - cf_ui_core_material - cf_ui_base -) - -# Add include directories for header-only files -target_include_directories(cf_ui_core PUBLIC - $ - $ -) - -# Link Qt for compile dependencies -target_link_libraries(cf_ui_core PUBLIC - Qt6::Core - Qt6::Gui - CFDesktop::base_headers -) - -log_info("UI_Core" "the UI Core Configuration Done") diff --git a/ui/core/color_scheme.h b/ui/core/color_scheme.h deleted file mode 100644 index 09dea8af2..000000000 --- a/ui/core/color_scheme.h +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @file color_scheme.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Interface for color scheme queries. - * @version 0.1 - * @date 2026-02-25 - * @since 0.1 - * @ingroup ui_core - * @copyright Copyright (c) 2026 - * - */ -#pragma once -#include "export.h" -#include - -namespace cf::ui::core { - -/** - * @brief Interface for color scheme queries. - * - * Defines the contract for color scheme implementations that provide - * color values by name. Used by Material Design 3 color system - * implementations. - * - * @note Implementations are expected to be thread-safe if used - * across multiple threads. - * - * @warning None. - * - * @since 0.1 - * @ingroup ui_core - */ -struct CF_UI_EXPORT ICFColorScheme { - /** - * @brief Virtual destructor. - * - * @throws None. - * - * @note None. - * - * @warning None. - * - * @since 0.1 - * @ingroup ui_core - */ - virtual ~ICFColorScheme() = default; - - /** - * @brief Queries a color by name. - * - * @param[in] name Color token name (e.g., "md.primary"). - * - * @return Reference to the QColor associated with the name. - * - * @throws None. - * - * @note The returned reference is valid for the lifetime of - * the color scheme object. - * - * @warning None. - * - * @since 0.1 - * @ingroup ui_core - */ - virtual QColor& queryExpectedColor(const char* name) = 0; - - /** - * @brief Convenience wrapper for queryExpectedColor. - * - * @param[in] name Color token name (e.g., "md.primary"). - * - * @return Copy of the QColor associated with the name. - * - * @throws None. - * - * @note Returns a copy rather than a reference for convenience. - * - * @warning None. - * - * @since 0.1 - * @ingroup ui_core - */ - QColor queryColor(const char* name) const { - return const_cast(this)->queryExpectedColor(name); - } -}; - -} // namespace cf::ui::core diff --git a/ui/core/font_type.h b/ui/core/font_type.h deleted file mode 100644 index 465ce2471..000000000 --- a/ui/core/font_type.h +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @file ui/core/font_type.h - * @brief Defines the IFontType interface for querying font styles. - * - * Provides an abstract interface for querying QFont objects by name. - * - * @author Charliechen114514 - * @date 2026-02-26 - * @version N/A - * @since N/A - * @ingroup ui_core - */ - -#pragma once -#include "export.h" -#include - -namespace cf::ui::core { - -/** - * @brief Abstract interface for querying font styles by name. - * - * Defines the contract for font type providers that return QFont - * objects based on style names. - * - * @ingroup ui_core - * - * @code - * IFontType* fontType = getFontType(); - * QFont font = fontType->queryTargetFont("bodyLarge"); - * @endcode - */ -struct CF_UI_EXPORT IFontType { - /// @brief Virtual destructor. - virtual ~IFontType() = default; - - /** - * @brief Query a font style by name. - * - * @param[in] name Font style name (e.g., "bodyLarge", "headlineMedium"). - * @return QFont object corresponding to the requested style. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_core - */ - virtual QFont queryTargetFont(const char* name) = 0; -}; - -} // namespace cf::ui::core diff --git a/ui/core/material/CMakeLists.txt b/ui/core/material/CMakeLists.txt deleted file mode 100644 index a72b44cc7..000000000 --- a/ui/core/material/CMakeLists.txt +++ /dev/null @@ -1,30 +0,0 @@ -project(ui_core_material LANGUAGES CXX) - -log_info("UI_Core_Material" "Start Configure the UI Core Material") - -# STATIC library for linking into cfui.dll -add_library(cf_ui_core_material STATIC - cfmaterial_scheme.cpp - cfmaterial_fonttype.cpp - cfmaterial_radius_scale.cpp - cfmaterial_motion.cpp - cfmaterial_theme.cpp - material_factory.cpp -) - -# Add include directories -target_include_directories(cf_ui_core_material PUBLIC - $ - $ - $ - $ -) - -# Link Qt for compile dependencies -target_link_libraries(cf_ui_core_material PUBLIC - Qt6::Core - Qt6::Gui - CFDesktop::base_headers -) - -log_info("UI_Core_Material" "the UI Core Material Configuration Done") diff --git a/ui/core/material/cfmaterial_fonttype.cpp b/ui/core/material/cfmaterial_fonttype.cpp deleted file mode 100644 index 03da6a8d6..000000000 --- a/ui/core/material/cfmaterial_fonttype.cpp +++ /dev/null @@ -1,116 +0,0 @@ -/** - * @file cfmaterial_fonttype.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Typography Implementation - * @version 0.1 - * @date 2026-02-26 - * - * @copyright Copyright (c) 2026 - * - */ - -#include "cfmaterial_fonttype.h" - -namespace cf::ui::core { - -namespace detail { - -inline QString systemDefaultFont() { -#ifdef Q_OS_WIN - return "Segoe UI"; -#elif defined(Q_OS_MACOS) - return ".SF NS Text"; -#else - return "Ubuntu"; -#endif -} - -inline QFont createFont(int sizeSp, QFont::Weight weight, bool italic = false) { - QFont font(systemDefaultFont()); - font.setStyleHint(QFont::SansSerif); - font.setWeight(weight); - font.setItalic(italic); - font.setPointSizeF(sizeSp); - return font; -} - -} // namespace detail - -MaterialTypography::MaterialTypography() { - fonts_.reserve(15); - line_heights_.reserve(15); - - // Display Styles - fonts_["md.typography.displayLarge"] = detail::createFont(57, QFont::Normal); - fonts_["md.typography.displayMedium"] = detail::createFont(45, QFont::Normal); - fonts_["md.typography.displaySmall"] = detail::createFont(36, QFont::Normal); - line_heights_["md.lineHeight.displayLarge"] = 64.0f; - line_heights_["md.lineHeight.displayMedium"] = 52.0f; - line_heights_["md.lineHeight.displaySmall"] = 44.0f; - - // Headline Styles - fonts_["md.typography.headlineLarge"] = detail::createFont(32, QFont::Normal); - fonts_["md.typography.headlineMedium"] = detail::createFont(28, QFont::Normal); - fonts_["md.typography.headlineSmall"] = detail::createFont(24, QFont::Normal); - line_heights_["md.lineHeight.headlineLarge"] = 40.0f; - line_heights_["md.lineHeight.headlineMedium"] = 36.0f; - line_heights_["md.lineHeight.headlineSmall"] = 32.0f; - - // Title Styles - fonts_["md.typography.titleLarge"] = detail::createFont(22, QFont::Medium); - fonts_["md.typography.titleMedium"] = detail::createFont(16, QFont::Medium); - fonts_["md.typography.titleSmall"] = detail::createFont(14, QFont::Medium); - line_heights_["md.lineHeight.titleLarge"] = 28.0f; - line_heights_["md.lineHeight.titleMedium"] = 24.0f; - line_heights_["md.lineHeight.titleSmall"] = 20.0f; - - // Body Styles - fonts_["md.typography.bodyLarge"] = detail::createFont(16, QFont::Normal); - fonts_["md.typography.bodyMedium"] = detail::createFont(14, QFont::Normal); - fonts_["md.typography.bodySmall"] = detail::createFont(12, QFont::Normal); - line_heights_["md.lineHeight.bodyLarge"] = 24.0f; - line_heights_["md.lineHeight.bodyMedium"] = 20.0f; - line_heights_["md.lineHeight.bodySmall"] = 16.0f; - - // Label Styles - fonts_["md.typography.labelLarge"] = detail::createFont(14, QFont::Medium); - fonts_["md.typography.labelMedium"] = detail::createFont(12, QFont::Medium); - fonts_["md.typography.labelSmall"] = detail::createFont(11, QFont::Medium); - line_heights_["md.lineHeight.labelLarge"] = 20.0f; - line_heights_["md.lineHeight.labelMedium"] = 16.0f; - line_heights_["md.lineHeight.labelSmall"] = 16.0f; -} - -QFont MaterialTypography::queryTargetFont(const char* name) { - auto it = fonts_.find(name); - if (it != fonts_.end()) { - return it->second; - } - QFont fallback(detail::systemDefaultFont()); - fallback.setPointSizeF(14); - return fallback; -} - -float MaterialTypography::getLineHeight(const char* styleName) const { - std::string key = styleName; - const std::string prefix = "md.typography."; - const std::string lineHeightPrefix = "md.lineHeight."; - - size_t pos = key.find(prefix); - if (pos == 0) { - key.replace(0, prefix.length(), lineHeightPrefix); - } - - auto it = line_heights_.find(key); - return it != line_heights_.end() ? it->second : 0.0f; -} - -void MaterialTypography::setFont(const std::string& name, const QFont& font) { - fonts_[name] = font; -} - -void MaterialTypography::setLineHeight(const std::string& name, float lineHeight) { - line_heights_[name] = lineHeight; -} - -} // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_fonttype.h b/ui/core/material/cfmaterial_fonttype.h deleted file mode 100644 index e9d58e6d8..000000000 --- a/ui/core/material/cfmaterial_fonttype.h +++ /dev/null @@ -1,133 +0,0 @@ -/** - * @file cfmaterial_fonttype.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Typography - * @version 0.1 - * @date 2026-02-26 - * - * @copyright Copyright (c) 2026 - * - * @details - * Implements the complete Material Design 3 Type Scale system with 15 styles. - * - * Font selection: - * - Windows: Segoe UI (Chinese fallback to Microsoft YaHei UI) - * - macOS: .SF NS Text (Chinese fallback to PingFang SC) - * - Linux: Ubuntu (Chinese fallback to Noto Sans CJK SC) - */ - -#pragma once - -#include -#include -#include - -#include "../export.h" -#include "font_type.h" - -namespace cf::ui::core { - -/** - * @brief Material Design 3 Typography. - * - * @details Implements the complete Material Design 3 Type Scale system with 15 styles. - * - * ### Type Scale Specifications - * - * | Category | Name | Size | Weight | Line Height | - * |----------|------|------|--------|-------------| - * | Display | displayLarge | 57sp | 400 | 64sp | - * | Display | displayMedium | 45sp | 400 | 52sp | - * | Display | displaySmall | 36sp | 400 | 44sp | - * | Headline | headlineLarge | 32sp | 400 | 40sp | - * | Headline | headlineMedium | 28sp | 400 | 36sp | - * | Headline | headlineSmall | 24sp | 400 | 32sp | - * | Title | titleLarge | 22sp | 500 | 28sp | - * | Title | titleMedium | 16sp | 500 | 24sp | - * | Title | titleSmall | 14sp | 500 | 20sp | - * | Body | bodyLarge | 16sp | 400 | 24sp | - * | Body | bodyMedium | 14sp | 400 | 20sp | - * | Body | bodySmall | 12sp | 400 | 16sp | - * | Label | labelLarge | 14sp | 500 | 20sp | - * | Label | labelMedium | 12sp | 500 | 16sp | - * | Label | labelSmall | 11sp | 500 | 16sp | - * - * @note Font selection: Windows uses Segoe UI, macOS uses .SF NS Text, - * Linux uses Ubuntu. Chinese fallbacks are platform-dependent. - * @warning None - * @throws None - * @since 0.1 - * @ingroup ui_core - * - * @code - * MaterialTypography typography; - * QFont font = typography.queryTargetFont("md.typography.bodyLarge"); - * @endcode - */ -class CF_UI_EXPORT MaterialTypography : public IFontType { - public: - MaterialTypography(); - ~MaterialTypography() override = default; - - // Non-copyable, movable - MaterialTypography(const MaterialTypography&) = delete; - MaterialTypography& operator=(const MaterialTypography&) = delete; - MaterialTypography(MaterialTypography&&) noexcept = default; - MaterialTypography& operator=(MaterialTypography&&) noexcept = default; - - /** - * @brief Implement interface: query font by name. - * - * @param[in] name Font style name (e.g., "md.typography.displayLarge"). - * @return QFont object (copy). - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_core - */ - QFont queryTargetFont(const char* name) override; - - /** - * @brief Get line height for a specific font style. - * - * @param[in] styleName Font style name. - * @return Line height value in sp, returns 0 if not found. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_core - */ - float getLineHeight(const char* styleName) const; - - /** - * @brief Register a font by name. - * - * @param[in] name Font style name (e.g., "md.typography.displayLarge"). - * @param[in] font QFont value to register. - * - * @since 0.1 - * @ingroup ui_core - */ - void setFont(const std::string& name, const QFont& font); - - /** - * @brief Register a line height by name. - * - * @param[in] name Line height token name (e.g., "md.lineHeight.displayLarge"). - * @param[in] lineHeight Line height value in sp. - * - * @since 0.1 - * @ingroup ui_core - */ - void setLineHeight(const std::string& name, float lineHeight); - - private: - std::unordered_map fonts_; - std::unordered_map line_heights_; -}; - -} // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_motion.cpp b/ui/core/material/cfmaterial_motion.cpp deleted file mode 100644 index 5be27f0d4..000000000 --- a/ui/core/material/cfmaterial_motion.cpp +++ /dev/null @@ -1,76 +0,0 @@ -/** - * @file cfmaterial_motion.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Motion Implementation - * @version 0.1 - * @date 2026-02-26 - * - * @copyright Copyright (c) 2026 - * - * @details - * Implements MaterialMotionScheme with motion preset registration and query methods. - * All motion presets follow Material Design 3 motion specifications. - */ -#include "cfmaterial_motion.h" - -namespace cf::ui::core { - -MaterialMotionScheme::MaterialMotionScheme() { - // Durations (Material Design 3 Motion specifications) - durations_["md.motion.shortEnter.duration"] = 200; - durations_["md.motion.shortExit.duration"] = 150; - durations_["md.motion.mediumEnter.duration"] = 300; - durations_["md.motion.mediumExit.duration"] = 250; - durations_["md.motion.longEnter.duration"] = 450; - durations_["md.motion.longExit.duration"] = 400; - durations_["md.motion.stateChange.duration"] = 200; - durations_["md.motion.rippleExpand.duration"] = 400; - durations_["md.motion.rippleFade.duration"] = 150; - - // Easing types - using EType = cf::ui::base::Easing::Type; - easings_["md.motion.shortEnter.easing"] = static_cast(EType::EmphasizedDecelerate); - easings_["md.motion.shortExit.easing"] = static_cast(EType::EmphasizedAccelerate); - easings_["md.motion.mediumEnter.easing"] = static_cast(EType::EmphasizedDecelerate); - easings_["md.motion.mediumExit.easing"] = static_cast(EType::EmphasizedAccelerate); - easings_["md.motion.longEnter.easing"] = static_cast(EType::Emphasized); - easings_["md.motion.longExit.easing"] = static_cast(EType::Emphasized); - easings_["md.motion.stateChange.easing"] = static_cast(EType::Standard); - easings_["md.motion.rippleExpand.easing"] = static_cast(EType::Standard); - easings_["md.motion.rippleFade.easing"] = static_cast(EType::Linear); -} - -int MaterialMotionScheme::queryDuration(const char* name) { - std::string fullName = std::string("md.motion.") + name + ".duration"; - auto it = durations_.find(fullName); - return it != durations_.end() ? it->second : 200; -} - -int MaterialMotionScheme::queryEasing(const char* name) { - std::string fullName = std::string("md.motion.") + name + ".easing"; - auto it = easings_.find(fullName); - return it != easings_.end() ? it->second - : static_cast(cf::ui::base::Easing::Type::Standard); -} - -int MaterialMotionScheme::queryDelay(const char* name) { - (void)name; - return 0; -} - -MotionSpec MaterialMotionScheme::getMotionSpec(const char* name) { - auto it = spec_cache_.find(name); - if (it != spec_cache_.end()) { - return it->second; - } - - MotionSpec spec; - spec.durationMs = queryDuration(name); - spec.easing = static_cast(queryEasing(name)); - spec.delayMs = queryDelay(name); - - spec_cache_[name] = spec; - return spec; -} - -} // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_motion.h b/ui/core/material/cfmaterial_motion.h deleted file mode 100644 index 42420c983..000000000 --- a/ui/core/material/cfmaterial_motion.h +++ /dev/null @@ -1,352 +0,0 @@ -/** - * @file cfmaterial_motion.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Motion Scheme - * @version 0.1 - * @date 2026-02-26 - * - * @copyright Copyright (c) 2026 - * - * @details - * Implements the complete Material Design 3 motion system with duration, - * easing, and delay specifications. - */ -#pragma once -#include -#include -#include - -#include "../motion_spec.h" -#include "base/easing.h" -#include "export.h" - -namespace cf::ui::core { - -// ============================================================================= -// Motion Data Structure -// ============================================================================= - -/** - * @brief Motion specification structure. - * - * Describes a complete animation specification with duration, - * easing curve, and optional delay. - * - * @since 0.1 - * @ingroup ui_core - */ -struct MotionSpec { - int durationMs; ///< Duration in milliseconds - cf::ui::base::Easing::Type easing; ///< Easing type - int delayMs = 0; ///< Delay in milliseconds - - /** - * @brief Convert easing type to QEasingCurve. - * - * @return QEasingCurve corresponding to the easing type. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_core - */ - QEasingCurve toEasingCurve() const { return cf::ui::base::Easing::fromEasingType(easing); } - - /** - * @brief Equality comparison operator. - * - * @since 0.1 - */ - bool operator==(const MotionSpec& other) const { - return durationMs == other.durationMs && easing == other.easing && delayMs == other.delayMs; - } - - /** - * @brief Inequality comparison operator. - * - * @since 0.1 - */ - bool operator!=(const MotionSpec& other) const { return !(*this == other); } -}; - -// ============================================================================= -// Motion Presets -// ============================================================================= - -/** - * @brief Static motion preset functions. - * - * Provides convenient access to predefined motion specifications - * following Material Design 3 motion principles. - * - * @since 0.1 - * @ingroup ui_core - */ -struct MotionPresets { - /** - * @brief Short enter motion preset. - * - * Duration: 200ms, Easing: EmphasizedDecelerate - * - * @return MotionSpec with duration 200ms and EmphasizedDecelerate easing. - * - * @since 0.1 - * @ingroup ui_core - */ - static MotionSpec shortEnter() { - return {200, cf::ui::base::Easing::Type::EmphasizedDecelerate, 0}; - } - - /** - * @brief Short exit motion preset. - * - * Duration: 150ms, Easing: EmphasizedAccelerate - * - * @return MotionSpec with duration 150ms and EmphasizedAccelerate easing. - * - * @since 0.1 - * @ingroup ui_core - */ - static MotionSpec shortExit() { - return {150, cf::ui::base::Easing::Type::EmphasizedAccelerate, 0}; - } - - /** - * @brief Medium enter motion preset. - * - * Duration: 300ms, Easing: EmphasizedDecelerate - * - * @return MotionSpec with duration 300ms and EmphasizedDecelerate easing. - * - * @since 0.1 - * @ingroup ui_core - */ - static MotionSpec mediumEnter() { - return {300, cf::ui::base::Easing::Type::EmphasizedDecelerate, 0}; - } - - /** - * @brief Medium exit motion preset. - * - * Duration: 250ms, Easing: EmphasizedAccelerate - * - * @return MotionSpec with duration 250ms and EmphasizedAccelerate easing. - * - * @since 0.1 - * @ingroup ui_core - */ - static MotionSpec mediumExit() { - return {250, cf::ui::base::Easing::Type::EmphasizedAccelerate, 0}; - } - - /** - * @brief Long enter motion preset. - * - * Duration: 450ms, Easing: Emphasized - * - * @return MotionSpec with duration 450ms and Emphasized easing. - * - * @since 0.1 - * @ingroup ui_core - */ - static MotionSpec longEnter() { return {450, cf::ui::base::Easing::Type::Emphasized, 0}; } - - /** - * @brief Long exit motion preset. - * - * Duration: 400ms, Easing: Emphasized - * - * @return MotionSpec with duration 400ms and Emphasized easing. - * - * @since 0.1 - * @ingroup ui_core - */ - static MotionSpec longExit() { return {400, cf::ui::base::Easing::Type::Emphasized, 0}; } - - /** - * @brief State change motion preset. - * - * Duration: 200ms, Easing: Standard - * - * @return MotionSpec with duration 200ms and Standard easing. - * - * @since 0.1 - * @ingroup ui_core - */ - static MotionSpec stateChange() { return {200, cf::ui::base::Easing::Type::Standard, 0}; } - - /** - * @brief Ripple expand motion preset. - * - * Duration: 400ms, Easing: Standard - * - * @return MotionSpec with duration 400ms and Standard easing. - * - * @since 0.1 - * @ingroup ui_core - */ - static MotionSpec rippleExpand() { return {400, cf::ui::base::Easing::Type::Standard, 0}; } - - /** - * @brief Ripple fade motion preset. - * - * Duration: 150ms, Easing: Linear - * - * @return MotionSpec with duration 150ms and Linear easing. - * - * @since 0.1 - * @ingroup ui_core - */ - static MotionSpec rippleFade() { return {150, cf::ui::base::Easing::Type::Linear, 0}; } -}; - -// ============================================================================= -// Material Motion Scheme -// ============================================================================= - -/** - * @brief Material Design 3 Motion Scheme. - * - * @details Implements the complete Material Design 3 motion system with - * duration and easing specifications. - * - * Factory functions are available in the cf::ui::core::material namespace. - * - * @note None - * @warning None - * @throws None - * @since 0.1 - * @ingroup ui_core - * - * @code - * #include "material_factory.hpp" - * - * auto motionScheme = cf::ui::core::material::motion(); - * int duration = motionScheme.queryDuration("shortEnter"); // 200 - * @endcode - */ -class CF_UI_EXPORT MaterialMotionScheme : public IMotionSpec { - public: - MaterialMotionScheme(); - ~MaterialMotionScheme() override = default; - - // Non-copyable, movable - MaterialMotionScheme(const MaterialMotionScheme&) = delete; - MaterialMotionScheme& operator=(const MaterialMotionScheme&) = delete; - MaterialMotionScheme(MaterialMotionScheme&&) noexcept = default; - MaterialMotionScheme& operator=(MaterialMotionScheme&&) noexcept = default; - - /** - * @brief Query a motion duration by name. - * - * @param[in] name Motion preset name (e.g., "shortEnter"). - * - * @return Duration in milliseconds. - * - * @throws None. - * - * @note Returns 0 if preset not found. - * - * @warning None. - * - * @since 0.1 - * @ingroup ui_core - */ - int queryDuration(const char* name) override; - - /** - * @brief Query a motion easing type by name. - * - * @param[in] name Motion preset name. - * - * @return Easing type as int (cf::ui::base::Easing::Type). - * - * @throws None. - * - * @note Returns 0 (Linear) if preset not found. - * - * @warning None. - * - * @since 0.1 - * @ingroup ui_core - */ - int queryEasing(const char* name) override; - - /** - * @brief Query a motion delay by name. - * - * @param[in] name Motion preset name. - * - * @return Delay in milliseconds (always returns 0 for standard presets). - * - * @throws None. - * - * @note Returns 0 if preset not found. - * - * @warning None. - * - * @since 0.1 - * @ingroup ui_core - */ - int queryDelay(const char* name) override; - - /** - * @brief Get a complete MotionSpec by name. - * - * @param[in] name Motion preset name (e.g., "shortEnter"). - * - * @return MotionSpec structure with duration, easing, delay. - * - * @throws None. - * - * @note Returns default MotionSpec (0ms, Linear, 0delay) if not found. - * - * @warning None. - * - * @since 0.1 - * @ingroup ui_core - */ - MotionSpec getMotionSpec(const char* name); - - /** - * @brief Motion presets group structure. - * - * Contains all predefined motion specifications. - * - * @since 0.1 - */ - struct MotionPresetsGroup { - MotionSpec shortEnter; ///< Short enter motion preset. - MotionSpec shortExit; ///< Short exit motion preset. - MotionSpec mediumEnter; ///< Medium enter motion preset. - MotionSpec mediumExit; ///< Medium exit motion preset. - MotionSpec longEnter; ///< Long enter motion preset. - MotionSpec longExit; ///< Long exit motion preset. - MotionSpec stateChange; ///< State change motion preset. - MotionSpec rippleExpand; ///< Ripple expand motion preset. - MotionSpec rippleFade; ///< Ripple fade motion preset. - }; - - /** - * @brief Get all motion presets as a group. - * - * @return MotionPresetsGroup containing all predefined motion specs. - * - * @since 0.1 - */ - [[nodiscard]] MotionPresetsGroup presets() const { - return MotionPresetsGroup{MotionPresets::shortEnter(), MotionPresets::shortExit(), - MotionPresets::mediumEnter(), MotionPresets::mediumExit(), - MotionPresets::longEnter(), MotionPresets::longExit(), - MotionPresets::stateChange(), MotionPresets::rippleExpand(), - MotionPresets::rippleFade()}; - } - - private: - std::unordered_map durations_; - std::unordered_map easings_; - mutable std::unordered_map spec_cache_; -}; - -} // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_radius_scale.cpp b/ui/core/material/cfmaterial_radius_scale.cpp deleted file mode 100644 index a76446d38..000000000 --- a/ui/core/material/cfmaterial_radius_scale.cpp +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @file cfmaterial_radius_scale.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Radius Scale Implementation - * @version 0.1 - * @date 2026-02-26 - * - * @copyright Copyright (c) 2026 - * - */ - -#include "cfmaterial_radius_scale.h" - -namespace cf::ui::core { - -MaterialRadiusScale::MaterialRadiusScale() { - radii_["md.shape.cornerNone"] = 0.0f; - radii_["md.shape.cornerExtraSmall"] = 4.0f; - radii_["md.shape.cornerSmall"] = 8.0f; - radii_["md.shape.cornerMedium"] = 12.0f; - radii_["md.shape.cornerLarge"] = 16.0f; - radii_["md.shape.cornerExtraLarge"] = 28.0f; - radii_["md.shape.cornerExtraExtraLarge"] = 32.0f; -} - -float MaterialRadiusScale::queryRadiusScale(const char* name) { - auto it = radii_.find(name); - return it != radii_.end() ? it->second : 0.0f; -} - -void MaterialRadiusScale::setRadius(const std::string& name, float radius) { - radii_[name] = radius; -} - -} // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_radius_scale.h b/ui/core/material/cfmaterial_radius_scale.h deleted file mode 100644 index 1e9e2efdd..000000000 --- a/ui/core/material/cfmaterial_radius_scale.h +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @file cfmaterial_radius_scale.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Radius Scale - * @version 0.1 - * @date 2026-02-26 - * - * @copyright Copyright (c) 2026 - * - * @details - * Implements the complete Material Design 3 corner radius system with 7 scales. - */ - -#pragma once - -#include -#include - -#include "../export.h" -#include "radius_scale.h" - -namespace cf::ui::core { - -/** - * @brief Material Design 3 Radius Scale. - * - * @details Implements the complete Material Design 3 Corner Radius system with 7 scales. - * - * ### Radius Scale Specifications - * - * | Token | Name | Value (dp) | Usage | - * |-------|------|------------|-------| - * | cornerNone | cornerNone | 0dp | No corner radius | - * | cornerExtraSmall | cornerExtraSmall | 4dp | Chips, small cards | - * | cornerSmall | cornerSmall | 8dp | Text fields, checkboxes | - * | cornerMedium | cornerMedium | 12dp | Cards | - * | cornerLarge | cornerLarge | 16dp | Alert dialogs | - * | cornerExtraLarge | cornerExtraLarge | 28dp | FAB, modals | - * | cornerExtraExtraLarge | cornerExtraExtraLarge | 32dp | Drawers | - * - * @note None - * @warning None - * @throws None - * @since 0.1 - * @ingroup ui_core - * - * @code - * MaterialRadiusScale radiusScale; - * float radius = radiusScale.queryRadiusScale("md.shape.cornerSmall"); - * @endcode - */ -class CF_UI_EXPORT MaterialRadiusScale : public IRadiusScale { - public: - MaterialRadiusScale(); - ~MaterialRadiusScale() override = default; - - // Non-copyable, movable - MaterialRadiusScale(const MaterialRadiusScale&) = delete; - MaterialRadiusScale& operator=(const MaterialRadiusScale&) = delete; - MaterialRadiusScale(MaterialRadiusScale&&) noexcept = default; - MaterialRadiusScale& operator=(MaterialRadiusScale&&) noexcept = default; - - /** - * @brief Implement interface: query corner radius by name. - * - * @param[in] name Radius style name (e.g., "md.shape.cornerSmall"). - * @return Radius value in dp, returns 0 if not found. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_core - */ - float queryRadiusScale(const char* name) override; - - /** - * @brief Register a radius by name. - * - * @param[in] name Radius token name (e.g., "md.shape.cornerSmall"). - * @param[in] radius Radius value in dp. - * - * @since 0.1 - * @ingroup ui_core - */ - void setRadius(const std::string& name, float radius); - - private: - std::unordered_map radii_; -}; - -} // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_scheme.cpp b/ui/core/material/cfmaterial_scheme.cpp deleted file mode 100644 index fd2a78b1e..000000000 --- a/ui/core/material/cfmaterial_scheme.cpp +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @file cfmaterial_scheme.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Color Scheme implementation - * @version 0.1 - * @date 2026-02-25 - * - * @copyright Copyright (c) 2026 - * - */ - -#include "cfmaterial_scheme.h" - -namespace cf::ui::core { - -MaterialColorScheme::MaterialColorScheme() { - colors_.reserve(32); -} - -QColor& MaterialColorScheme::queryExpectedColor(const char* name) { - auto it = colors_.find(name); - if (it != colors_.end()) { - return it->second; - } - static QColor fallback(Qt::black); - return fallback; -} - -QColor MaterialColorScheme::queryColor(const char* name) const { - auto it = colors_.find(name); - if (it != colors_.end()) { - return it->second; - } - return QColor(Qt::black); -} - -void MaterialColorScheme::setColor(const std::string& name, const QColor& color) { - colors_[name] = color; -} - -bool MaterialColorScheme::hasColor(const std::string& name) const { - return colors_.find(name) != colors_.end(); -} - -} // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_scheme.h b/ui/core/material/cfmaterial_scheme.h deleted file mode 100644 index 3f6f5d958..000000000 --- a/ui/core/material/cfmaterial_scheme.h +++ /dev/null @@ -1,120 +0,0 @@ -/** - * @file cfmaterial_scheme.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Color Scheme - * @version 0.1 - * @date 2026-02-25 - * - * @copyright Copyright (c) 2026 - * - */ -#pragma once -#include -#include -#include - -#include "../../export.h" -#include "color_scheme.h" - -namespace cf::ui::core { - -/** - * @brief Material Design 3 Color Scheme. - * - * @details Implements the complete Material Design 3 color system with 26 color tokens. - * Colors are stored in a flat map keyed by string token names. - * - * Factory functions are available in the cf::ui::core::material namespace. - * - * @note None. - * @warning None. - * @throws None. - * @since 0.1 - * @ingroup ui_core - * - * @code - * #include "material_factory.hpp" - * - * // Create a light theme - * auto lightScheme = cf::ui::core::material::light(); - * - * // Query colors by name - * QColor primary = lightScheme.queryExpectedColor("md.primary"); - * @endcode - */ -class CF_UI_EXPORT MaterialColorScheme : public ICFColorScheme { - public: - MaterialColorScheme(); - ~MaterialColorScheme() override = default; - - // Non-copyable, movable - MaterialColorScheme(const MaterialColorScheme&) = delete; - MaterialColorScheme& operator=(const MaterialColorScheme&) = delete; - MaterialColorScheme(MaterialColorScheme&&) noexcept = default; - MaterialColorScheme& operator=(MaterialColorScheme&&) noexcept = default; - - /** - * @brief Query a color by name. - * - * @param[in] name Color token name (e.g., "md.primary"). - * - * @return Reference to the QColor. - * - * @throws None. - * - * @note Returns a reference that is valid for the lifetime - * of the color scheme. - * - * @warning None. - * - * @since 0.1 - * @ingroup ui_core - */ - QColor& queryExpectedColor(const char* name) override; - - /** - * @brief Query a color by name (const overload). - * - * @param[in] name Color token name. - * - * @return Copy of the QColor. - * - * @throws None. - * - * @note Returns a default black color if not found. - * - * @warning None. - * - * @since 0.1 - * @ingroup ui_core - */ - QColor queryColor(const char* name) const; - - /** - * @brief Register a color by name. - * - * @param[in] name Color token name (e.g., "md.primary"). - * @param[in] color Color value to register. - * - * @since 0.1 - * @ingroup ui_core - */ - void setColor(const std::string& name, const QColor& color); - - /** - * @brief Check if a color token exists. - * - * @param[in] name Color token name. - * - * @return true if the token exists. - * - * @since 0.1 - * @ingroup ui_core - */ - bool hasColor(const std::string& name) const; - - private: - std::unordered_map colors_; -}; - -} // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_theme.cpp b/ui/core/material/cfmaterial_theme.cpp deleted file mode 100644 index 984415887..000000000 --- a/ui/core/material/cfmaterial_theme.cpp +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @file cfmaterial_theme.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Theme Implementation - * @version 0.1 - * @date 2026-02-27 - * - * @copyright Copyright (c) 2026 - * - */ - -#include "cfmaterial_theme.h" - -namespace cf::ui::core { - -MaterialTheme::MaterialTheme(std::unique_ptr color_scheme, - std::unique_ptr font_type, - std::unique_ptr radius_scale, - std::unique_ptr motion_spec) { - // Initialize base class protected members - color_scheme_ = std::move(color_scheme); - font_type_ = std::move(font_type); - radius_scale_ = std::move(radius_scale); - motion_spec_ = std::move(motion_spec); -} - -} // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_theme.h b/ui/core/material/cfmaterial_theme.h deleted file mode 100644 index bf62cc0b3..000000000 --- a/ui/core/material/cfmaterial_theme.h +++ /dev/null @@ -1,87 +0,0 @@ -/** - * @file cfmaterial_theme.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Theme - Complete Theme Implementation - * @version 0.1 - * @date 2026-02-27 - * - * @copyright Copyright (c) 2026 - * - * @details - * Implements ICFTheme interface for Material Design 3, combining: - * - MaterialColorScheme for colors - * - MaterialTypography for fonts - * - MaterialRadiusScale for corner radii - * - MaterialMotionScheme for animations - */ - -#pragma once - -#include "../export.h" -#include "../theme.h" -#include "cfmaterial_fonttype.h" -#include "cfmaterial_motion.h" -#include "cfmaterial_radius_scale.h" -#include "cfmaterial_scheme.h" - -namespace cf::ui::core { - -// Forward declaration for friend access -class MaterialFactory; - -/** - * @brief Material Design 3 Theme Implementation. - * - * @details Complete theme implementation combining all Material Design 3 - * components: color scheme, typography, radius scale, and motion specs. - * - * Only MaterialFactory can create instances (friend class). - * - * @note None. - * @warning None. - * @throws None. - * @since 0.1 - * @ingroup ui_core - * - * @code - * // Created via MaterialFactory - * MaterialFactory factory; - * auto theme = factory.fromName("theme.material.light"); - * - * // Access theme components - * auto& colors = theme->color_scheme(); - * auto& fonts = theme->font_type(); - * auto& radius = theme->radius_scale(); - * auto& motion = theme->motion_spec(); - * @endcode - */ -class CF_UI_EXPORT MaterialTheme : public ICFTheme { - friend class MaterialFactory; - - public: - ~MaterialTheme() override = default; - - // Non-copyable, non-movable (due to unique_ptr members in base class) - MaterialTheme(const MaterialTheme&) = delete; - MaterialTheme& operator=(const MaterialTheme&) = delete; - MaterialTheme(MaterialTheme&&) noexcept = delete; - MaterialTheme& operator=(MaterialTheme&&) noexcept = delete; - - protected: - /** - * @brief Construct MaterialTheme with all components. - * - * @param color_scheme Color scheme (light or dark). - * @param font_type Typography scale. - * @param radius_scale Corner radius scale. - * @param motion_spec Motion/animation specifications. - * - * @since 0.1 - */ - MaterialTheme(std::unique_ptr color_scheme, - std::unique_ptr font_type, - std::unique_ptr radius_scale, - std::unique_ptr motion_spec); -}; - -} // namespace cf::ui::core diff --git a/ui/core/material/material_factory.cpp b/ui/core/material/material_factory.cpp deleted file mode 100644 index aadc1e9db..000000000 --- a/ui/core/material/material_factory.cpp +++ /dev/null @@ -1,540 +0,0 @@ -/** - * @file material_factory.cpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Color Scheme Factory Implementation - * @version 0.1 - * @date 2026-02-25 - * - * @copyright Copyright (c) 2026 - * - */ - -#include "material_factory.hpp" - -#include -#include -#include - -#include "base/color.h" -#include "base/color_helper.h" -#include "cfmaterial_fonttype.h" -#include "cfmaterial_motion.h" -#include "cfmaterial_radius_scale.h" -namespace cf::ui::core::material { - -namespace detail { - -// ============================================================================= -// Material Design 3 Default Color Values -// ============================================================================= - -/** - * @brief Material Design 3 Light Theme Colors (Baseline Purple) - * - * Official MD3 baseline colors for light theme. - * Source: https://m3.material.io/styles/color/the-color-system/tokens - */ -struct LightColors { - static constexpr const char* primary = "#6750A4"; - static constexpr const char* onPrimary = "#FFFFFF"; - static constexpr const char* primaryContainer = "#EADDFF"; - static constexpr const char* onPrimaryContainer = "#21005D"; - - static constexpr const char* secondary = "#625B71"; - static constexpr const char* onSecondary = "#FFFFFF"; - static constexpr const char* secondaryContainer = "#E8DEF8"; - static constexpr const char* onSecondaryContainer = "#1D192B"; - - static constexpr const char* tertiary = "#7D5260"; - static constexpr const char* onTertiary = "#FFFFFF"; - static constexpr const char* tertiaryContainer = "#FFD8E4"; - static constexpr const char* onTertiaryContainer = "#31111D"; - - static constexpr const char* error = "#B3261E"; - static constexpr const char* onError = "#FFFFFF"; - static constexpr const char* errorContainer = "#F9DEDC"; - static constexpr const char* onErrorContainer = "#410E0B"; - - static constexpr const char* background = "#FFFBFE"; - static constexpr const char* onBackground = "#1C1B1F"; - static constexpr const char* surface = "#FFFBFE"; - static constexpr const char* onSurface = "#1C1B1F"; - static constexpr const char* surfaceVariant = "#E7E0EC"; - static constexpr const char* onSurfaceVariant = "#49454F"; - static constexpr const char* outline = "#79747E"; - static constexpr const char* outlineVariant = "#CAC4D0"; - - static constexpr const char* shadow = "#000000"; - static constexpr const char* scrim = "#000000"; - static constexpr const char* inverseSurface = "#313033"; - static constexpr const char* inverseOnSurface = "#F4EFF4"; - static constexpr const char* inversePrimary = "#D0BCFF"; -}; - -/** - * @brief Material Design 3 Dark Theme Colors (Baseline Purple) - * - * Official MD3 baseline colors for dark theme. - */ -struct DarkColors { - static constexpr const char* primary = "#D0BCFF"; - static constexpr const char* onPrimary = "#381E72"; - static constexpr const char* primaryContainer = "#4F378B"; - static constexpr const char* onPrimaryContainer = "#EADDFF"; - - static constexpr const char* secondary = "#CCC2DC"; - static constexpr const char* onSecondary = "#332D41"; - static constexpr const char* secondaryContainer = "#4A4458"; - static constexpr const char* onSecondaryContainer = "#E8DEF8"; - - static constexpr const char* tertiary = "#EFB8C8"; - static constexpr const char* onTertiary = "#492532"; - static constexpr const char* tertiaryContainer = "#633B48"; - static constexpr const char* onTertiaryContainer = "#FFD8E4"; - - static constexpr const char* error = "#F2B8B5"; - static constexpr const char* onError = "#601410"; - static constexpr const char* errorContainer = "#8C1D18"; - static constexpr const char* onErrorContainer = "#F9DEDC"; - - static constexpr const char* background = "#1C1B1F"; - static constexpr const char* onBackground = "#E6E1E5"; - static constexpr const char* surface = "#1C1B1F"; - static constexpr const char* onSurface = "#E6E1E5"; - static constexpr const char* surfaceVariant = "#49454F"; - static constexpr const char* onSurfaceVariant = "#CAC4D0"; - static constexpr const char* outline = "#938F99"; - static constexpr const char* outlineVariant = "#49454F"; - - static constexpr const char* shadow = "#000000"; - static constexpr const char* scrim = "#000000"; - static constexpr const char* inverseSurface = "#E6E1E5"; - static constexpr const char* inverseOnSurface = "#313033"; - static constexpr const char* inversePrimary = "#6750A4"; -}; - -// ============================================================================= -// Color Registration Helpers -// ============================================================================= - -template inline void registerAllColors(MaterialColorScheme& scheme) { - // Primary colors - scheme.setColor("md.primary", QColor(ColorDefs::primary)); - scheme.setColor("md.onPrimary", QColor(ColorDefs::onPrimary)); - scheme.setColor("md.primaryContainer", QColor(ColorDefs::primaryContainer)); - scheme.setColor("md.onPrimaryContainer", QColor(ColorDefs::onPrimaryContainer)); - - // Secondary colors - scheme.setColor("md.secondary", QColor(ColorDefs::secondary)); - scheme.setColor("md.onSecondary", QColor(ColorDefs::onSecondary)); - scheme.setColor("md.secondaryContainer", QColor(ColorDefs::secondaryContainer)); - scheme.setColor("md.onSecondaryContainer", QColor(ColorDefs::onSecondaryContainer)); - - // Tertiary colors - scheme.setColor("md.tertiary", QColor(ColorDefs::tertiary)); - scheme.setColor("md.onTertiary", QColor(ColorDefs::onTertiary)); - scheme.setColor("md.tertiaryContainer", QColor(ColorDefs::tertiaryContainer)); - scheme.setColor("md.onTertiaryContainer", QColor(ColorDefs::onTertiaryContainer)); - - // Error colors - scheme.setColor("md.error", QColor(ColorDefs::error)); - scheme.setColor("md.onError", QColor(ColorDefs::onError)); - scheme.setColor("md.errorContainer", QColor(ColorDefs::errorContainer)); - scheme.setColor("md.onErrorContainer", QColor(ColorDefs::onErrorContainer)); - - // Surface colors - scheme.setColor("md.background", QColor(ColorDefs::background)); - scheme.setColor("md.onBackground", QColor(ColorDefs::onBackground)); - scheme.setColor("md.surface", QColor(ColorDefs::surface)); - scheme.setColor("md.onSurface", QColor(ColorDefs::onSurface)); - scheme.setColor("md.surfaceVariant", QColor(ColorDefs::surfaceVariant)); - scheme.setColor("md.onSurfaceVariant", QColor(ColorDefs::onSurfaceVariant)); - scheme.setColor("md.outline", QColor(ColorDefs::outline)); - scheme.setColor("md.outlineVariant", QColor(ColorDefs::outlineVariant)); - - // Utility colors - scheme.setColor("md.shadow", QColor(ColorDefs::shadow)); - scheme.setColor("md.scrim", QColor(ColorDefs::scrim)); - scheme.setColor("md.inverseSurface", QColor(ColorDefs::inverseSurface)); - scheme.setColor("md.inverseOnSurface", QColor(ColorDefs::inverseOnSurface)); - scheme.setColor("md.inversePrimary", QColor(ColorDefs::inversePrimary)); -} - -} // namespace detail - -// ============================================================================= -// Factory Function Implementations -// ============================================================================= - -MaterialColorScheme light() { - MaterialColorScheme scheme; - detail::registerAllColors(scheme); - return scheme; -} - -MaterialColorScheme dark() { - MaterialColorScheme scheme; - detail::registerAllColors(scheme); - return scheme; -} - -Result fromJson(const QByteArray& json, bool isDark) { - QJsonParseError parseError; - QJsonDocument doc = QJsonDocument::fromJson(json, &parseError); - - if (parseError.error != QJsonParseError::NoError) { - return ::aex::unexpected( - MaterialSchemeError{MaterialSchemeError::Kind::InvalidJson, - "Failed to parse JSON: " + parseError.errorString().toStdString()}); - } - - if (!doc.isObject()) { - return ::aex::unexpected(MaterialSchemeError{MaterialSchemeError::Kind::InvalidJson, - "Root element must be an object"}); - } - - QJsonObject root = doc.object(); - - // Support both direct color values and nested "schemes" structure - QJsonObject colors; - if (root.contains("schemes")) { - QJsonObject schemes = root["schemes"].toObject(); - QString schemeKey = isDark ? "dark" : "light"; - colors = schemes[schemeKey].toObject(); - } else { - colors = root; - } - - MaterialColorScheme scheme; - - // Map of MD3 color names to registry keys - const std::unordered_map colorMapping = { - {"primary", "md.primary"}, - {"onPrimary", "md.onPrimary"}, - {"primaryContainer", "md.primaryContainer"}, - {"onPrimaryContainer", "md.onPrimaryContainer"}, - {"secondary", "md.secondary"}, - {"onSecondary", "md.onSecondary"}, - {"secondaryContainer", "md.secondaryContainer"}, - {"onSecondaryContainer", "md.onSecondaryContainer"}, - {"tertiary", "md.tertiary"}, - {"onTertiary", "md.onTertiary"}, - {"tertiaryContainer", "md.tertiaryContainer"}, - {"onTertiaryContainer", "md.onTertiaryContainer"}, - {"error", "md.error"}, - {"onError", "md.onError"}, - {"errorContainer", "md.errorContainer"}, - {"onErrorContainer", "md.onErrorContainer"}, - {"background", "md.background"}, - {"onBackground", "md.onBackground"}, - {"surface", "md.surface"}, - {"onSurface", "md.onSurface"}, - {"surfaceVariant", "md.surfaceVariant"}, - {"onSurfaceVariant", "md.onSurfaceVariant"}, - {"outline", "md.outline"}, - {"outlineVariant", "md.outlineVariant"}, - {"shadow", "md.shadow"}, - {"scrim", "md.scrim"}, - {"inverseSurface", "md.inverseSurface"}, - {"inverseOnSurface", "md.inverseOnSurface"}, - {"inversePrimary", "md.inversePrimary"}}; - - // Parse and register colors - for (const auto& [key, registryKey] : colorMapping) { - QString qKey = QString::fromStdString(key); - if (colors.contains(qKey)) { - QString colorStr = colors[qKey].toString(); - if (!colorStr.startsWith('#')) { - return ::aex::unexpected(MaterialSchemeError{ - MaterialSchemeError::Kind::InvalidColorFormat, - "Invalid color format for " + key + ": " + colorStr.toStdString()}); - } - scheme.setColor(registryKey, QColor(colorStr)); - } - } - - return scheme; -} - -MaterialColorScheme fromKeyColor(cf::ui::base::CFColor keyColor, bool isDark) { - using ::cf::ui::base::tonalPalette; - - MaterialColorScheme scheme; - - // Generate tonal palette from key color - QList tonal = tonalPalette(keyColor); - - // Helper to get color from tonal palette by index - auto getTone = [&tonal](int index) -> cf::ui::base::CFColor { - if (index >= 0 && index < tonal.size()) { - return tonal[index]; - } - return cf::ui::base::CFColor("#808080"); // Fallback gray - }; - - if (!isDark) { - // Light scheme generation - scheme.setColor("md.primary", getTone(4).native_color()); - scheme.setColor("md.onPrimary", QColor("#FFFFFF")); - scheme.setColor("md.primaryContainer", getTone(9).native_color()); - scheme.setColor("md.onPrimaryContainer", getTone(0).native_color()); - - // Secondary: Use complementary hue - cf::ui::base::CFColor secondaryKey(std::fmod(keyColor.hue() + 60.0f, 360.0f), - keyColor.chroma() * 0.8f, keyColor.tone()); - QList secondaryPalette = tonalPalette(secondaryKey); - scheme.setColor("md.secondary", secondaryPalette[5].native_color()); - scheme.setColor("md.onSecondary", QColor("#FFFFFF")); - scheme.setColor("md.secondaryContainer", secondaryPalette[9].native_color()); - scheme.setColor("md.onSecondaryContainer", secondaryPalette[0].native_color()); - - // Tertiary: Use complementary hue - cf::ui::base::CFColor tertiaryKey(std::fmod(keyColor.hue() + 120.0f, 360.0f), - keyColor.chroma() * 0.6f, keyColor.tone()); - QList tertiaryPalette = tonalPalette(tertiaryKey); - scheme.setColor("md.tertiary", tertiaryPalette[5].native_color()); - scheme.setColor("md.onTertiary", QColor("#FFFFFF")); - scheme.setColor("md.tertiaryContainer", tertiaryPalette[9].native_color()); - scheme.setColor("md.onTertiaryContainer", tertiaryPalette[0].native_color()); - - // Error colors (fixed red-based) - scheme.setColor("md.error", QColor("#B3261E")); - scheme.setColor("md.onError", QColor("#FFFFFF")); - scheme.setColor("md.errorContainer", QColor("#F9DEDC")); - scheme.setColor("md.onErrorContainer", QColor("#410E0B")); - - // Surface colors - scheme.setColor("md.background", QColor("#FFFBFE")); - scheme.setColor("md.onBackground", QColor("#1C1B1F")); - scheme.setColor("md.surface", QColor("#FFFBFE")); - scheme.setColor("md.onSurface", QColor("#1C1B1F")); - scheme.setColor("md.surfaceVariant", getTone(11).native_color()); - scheme.setColor("md.onSurfaceVariant", getTone(2).native_color()); - scheme.setColor("md.outline", getTone(7).native_color()); - scheme.setColor("md.outlineVariant", getTone(10).native_color()); - - // Utility colors - scheme.setColor("md.shadow", QColor("#000000")); - scheme.setColor("md.scrim", QColor("#000000")); - scheme.setColor("md.inverseSurface", QColor("#313033")); - scheme.setColor("md.inverseOnSurface", QColor("#F4EFF4")); - scheme.setColor("md.inversePrimary", getTone(8).native_color()); - } else { - // Dark scheme generation - scheme.setColor("md.primary", getTone(8).native_color()); - scheme.setColor("md.onPrimary", getTone(0).native_color()); - scheme.setColor("md.primaryContainer", getTone(3).native_color()); - scheme.setColor("md.onPrimaryContainer", getTone(11).native_color()); - - // Secondary - cf::ui::base::CFColor secondaryKey(std::fmod(keyColor.hue() + 60.0f, 360.0f), - keyColor.chroma() * 0.8f, keyColor.tone()); - QList secondaryPalette = tonalPalette(secondaryKey); - scheme.setColor("md.secondary", secondaryPalette[8].native_color()); - scheme.setColor("md.onSecondary", secondaryPalette[0].native_color()); - scheme.setColor("md.secondaryContainer", secondaryPalette[3].native_color()); - scheme.setColor("md.onSecondaryContainer", secondaryPalette[11].native_color()); - - // Tertiary - cf::ui::base::CFColor tertiaryKey(std::fmod(keyColor.hue() + 120.0f, 360.0f), - keyColor.chroma() * 0.6f, keyColor.tone()); - QList tertiaryPalette = tonalPalette(tertiaryKey); - scheme.setColor("md.tertiary", tertiaryPalette[8].native_color()); - scheme.setColor("md.onTertiary", tertiaryPalette[0].native_color()); - scheme.setColor("md.tertiaryContainer", tertiaryPalette[3].native_color()); - scheme.setColor("md.onTertiaryContainer", tertiaryPalette[11].native_color()); - - // Error colors (fixed red-based for dark) - scheme.setColor("md.error", QColor("#F2B8B5")); - scheme.setColor("md.onError", QColor("#601410")); - scheme.setColor("md.errorContainer", QColor("#8C1D18")); - scheme.setColor("md.onErrorContainer", QColor("#F9DEDC")); - - // Surface colors - scheme.setColor("md.background", QColor("#1C1B1F")); - scheme.setColor("md.onBackground", QColor("#E6E1E5")); - scheme.setColor("md.surface", QColor("#1C1B1F")); - scheme.setColor("md.onSurface", QColor("#E6E1E5")); - scheme.setColor("md.surfaceVariant", getTone(2).native_color()); - scheme.setColor("md.onSurfaceVariant", getTone(10).native_color()); - scheme.setColor("md.outline", getTone(5).native_color()); - scheme.setColor("md.outlineVariant", getTone(2).native_color()); - - // Utility colors - scheme.setColor("md.shadow", QColor("#000000")); - scheme.setColor("md.scrim", QColor("#000000")); - scheme.setColor("md.inverseSurface", QColor("#E6E1E5")); - scheme.setColor("md.inverseOnSurface", QColor("#313033")); - scheme.setColor("md.inversePrimary", getTone(4).native_color()); - } - - return scheme; -} - -QByteArray toJson(const MaterialColorScheme& scheme) { - QJsonObject lightScheme; - - // Helper to get color string from scheme - auto getColorString = [&scheme](const char* key) -> QString { - QColor color = scheme.queryColor(key); - return color.isValid() ? color.name() : "#000000"; - }; - - // Build scheme object - lightScheme["primary"] = getColorString("md.primary"); - lightScheme["onPrimary"] = getColorString("md.onPrimary"); - lightScheme["primaryContainer"] = getColorString("md.primaryContainer"); - lightScheme["onPrimaryContainer"] = getColorString("md.onPrimaryContainer"); - lightScheme["secondary"] = getColorString("md.secondary"); - lightScheme["onSecondary"] = getColorString("md.onSecondary"); - lightScheme["secondaryContainer"] = getColorString("md.secondaryContainer"); - lightScheme["onSecondaryContainer"] = getColorString("md.onSecondaryContainer"); - lightScheme["tertiary"] = getColorString("md.tertiary"); - lightScheme["onTertiary"] = getColorString("md.onTertiary"); - lightScheme["tertiaryContainer"] = getColorString("md.tertiaryContainer"); - lightScheme["onTertiaryContainer"] = getColorString("md.onTertiaryContainer"); - lightScheme["error"] = getColorString("md.error"); - lightScheme["onError"] = getColorString("md.onError"); - lightScheme["errorContainer"] = getColorString("md.errorContainer"); - lightScheme["onErrorContainer"] = getColorString("md.onErrorContainer"); - lightScheme["background"] = getColorString("md.background"); - lightScheme["onBackground"] = getColorString("md.onBackground"); - lightScheme["surface"] = getColorString("md.surface"); - lightScheme["onSurface"] = getColorString("md.onSurface"); - lightScheme["surfaceVariant"] = getColorString("md.surfaceVariant"); - lightScheme["onSurfaceVariant"] = getColorString("md.onSurfaceVariant"); - lightScheme["outline"] = getColorString("md.outline"); - lightScheme["outlineVariant"] = getColorString("md.outlineVariant"); - lightScheme["shadow"] = getColorString("md.shadow"); - lightScheme["scrim"] = getColorString("md.scrim"); - lightScheme["inverseSurface"] = getColorString("md.inverseSurface"); - lightScheme["inverseOnSurface"] = getColorString("md.inverseOnSurface"); - lightScheme["inversePrimary"] = getColorString("md.inversePrimary"); - - QJsonObject schemes; - schemes["light"] = lightScheme; - - QJsonObject root; - root["schemes"] = schemes; - - QJsonDocument doc(root); - return doc.toJson(QJsonDocument::Indented); -} - -// ============================================================================= -// Typography Factory Functions -// ============================================================================= - -MaterialTypography defaultTypography() { - return MaterialTypography(); -} - -// ============================================================================= -// Radius Scale Factory Functions -// ============================================================================= - -MaterialRadiusScale defaultRadiusScale() { - return MaterialRadiusScale(); -} - -// ============================================================================= -// Motion Factory Functions -// ============================================================================= - -MaterialMotionScheme motion() { - return MaterialMotionScheme(); -} - -} // namespace cf::ui::core::material - -// ============================================================================= -// MaterialFactory Class Implementation -// ============================================================================= - -#include "../token/theme_name/material_theme_name.h" -#include "cfmaterial_theme.h" -#include "material_factory_class.h" - -namespace cf::ui::core { - -std::unique_ptr MaterialFactory::fromName(const char* name) { - using namespace token::literals; - - // Check for light theme - if (std::strcmp(name, MATERIAL_THEME_LIGHT) == 0) { - auto color_scheme = std::make_unique(material::light()); - auto font_type = std::make_unique(material::defaultTypography()); - auto radius_scale = std::make_unique(material::defaultRadiusScale()); - auto motion_spec = std::make_unique(material::motion()); - - return std::unique_ptr( - new MaterialTheme(std::move(color_scheme), std::move(font_type), - std::move(radius_scale), std::move(motion_spec))); - } - - // Check for dark theme - if (std::strcmp(name, MATERIAL_THEME_DARK) == 0) { - auto color_scheme = std::make_unique(material::dark()); - auto font_type = std::make_unique(material::defaultTypography()); - auto radius_scale = std::make_unique(material::defaultRadiusScale()); - auto motion_spec = std::make_unique(material::motion()); - - return std::unique_ptr( - new MaterialTheme(std::move(color_scheme), std::move(font_type), - std::move(radius_scale), std::move(motion_spec))); - } - - // Unknown theme name - return nullptr; -} - -std::unique_ptr MaterialFactory::fromJson(const QByteArray& json) { - // Try to parse as light theme first, then dark if needed - auto lightResult = material::fromJson(json, false); - if (lightResult) { - auto color_scheme = std::make_unique(std::move(*lightResult)); - auto font_type = std::make_unique(material::defaultTypography()); - auto radius_scale = std::make_unique(material::defaultRadiusScale()); - auto motion_spec = std::make_unique(material::motion()); - - return std::unique_ptr( - new MaterialTheme(std::move(color_scheme), std::move(font_type), - std::move(radius_scale), std::move(motion_spec))); - } - - // If light parsing failed, try dark - auto darkResult = material::fromJson(json, true); - if (darkResult) { - auto color_scheme = std::make_unique(std::move(*darkResult)); - auto font_type = std::make_unique(material::defaultTypography()); - auto radius_scale = std::make_unique(material::defaultRadiusScale()); - auto motion_spec = std::make_unique(material::motion()); - - return std::unique_ptr( - new MaterialTheme(std::move(color_scheme), std::move(font_type), - std::move(radius_scale), std::move(motion_spec))); - } - - // Parsing failed for both light and dark - return nullptr; -} - -QByteArray MaterialFactory::toJson(ICFTheme* raw_theme) { - if (!raw_theme) { - return QByteArray(); - } - - // Get the color scheme from the theme - auto& color_scheme = raw_theme->color_scheme(); - - // Try to cast to MaterialColorScheme - auto* material_scheme = dynamic_cast(&color_scheme); - if (material_scheme) { - return material::toJson(*material_scheme); - } - - // Not a MaterialColorScheme, return empty JSON - return QByteArray(); -} - -} // namespace cf::ui::core diff --git a/ui/core/material/material_factory.hpp b/ui/core/material/material_factory.hpp deleted file mode 100644 index 89e6a5693..000000000 --- a/ui/core/material/material_factory.hpp +++ /dev/null @@ -1,244 +0,0 @@ -/** - * @file material_factory.hpp - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Color Scheme Factory Functions - * @version 0.1 - * @date 2026-02-25 - * - * @copyright Copyright (c) 2026 - * - */ - -#pragma once - -#include - -#include "../../export.h" -#include "aex/expected/expected.hpp" -#include "base/color.h" -#include "cfmaterial_fonttype.h" -#include "cfmaterial_motion.h" -#include "cfmaterial_radius_scale.h" -#include "cfmaterial_scheme.h" - -namespace cf::ui::core::material { - -// ============================================================================= -// Error Types -// ============================================================================= - -/** - * @brief Error type for material scheme operations. - * - * @since 0.1 - * @ingroup ui_core - */ -struct MaterialSchemeError { - /** - * @brief Error kind enumeration. - * - * @since 0.1 - */ - enum class Kind { - InvalidJson, ///< JSON parsing failed - MissingColor, ///< Required color not found in JSON - InvalidColorFormat, ///< Color value format is invalid - GenerationFailed ///< Dynamic color generation failed - } kind = Kind::InvalidJson; - - /** - * @brief Human-readable error message. - * - * @since 0.1 - */ - std::string message; - - /** - * @brief Default constructor. - * - * @since 0.1 - */ - MaterialSchemeError() = default; - - /** - * @brief Construct from kind and message. - * - * @param k Error kind. - * @param msg Error message. - * - * @since 0.1 - */ - MaterialSchemeError(Kind k, std::string msg) : kind(k), message(std::move(msg)) {} -}; - -/** - * @brief Result type for factory functions. - * - * @since 0.1 - */ -using Result = aex::expected; - -// ============================================================================= -// Factory Functions -// ============================================================================= - -/** - * @brief Creates Material Design 3 default light theme. - * - * Uses the official Material Design 3 baseline purple color scheme. - * - * @return MaterialColorScheme with light theme colors. - * - * @since 0.1 - * @ingroup ui_core - * - * @code - * auto lightScheme = cf::ui::core::material::light(); - * QColor primary = lightScheme.queryExpectedColor("md.primary"); - * @endcode - */ -CF_UI_EXPORT MaterialColorScheme light(); - -/** - * @brief Creates Material Design 3 default dark theme. - * - * Uses the official Material Design 3 baseline purple color scheme - * adapted for dark mode. - * - * @return MaterialColorScheme with dark theme colors. - * - * @since 0.1 - * @ingroup ui_core - */ -CF_UI_EXPORT MaterialColorScheme dark(); - -/** - * @brief Creates color scheme from Material Theme Builder JSON. - * - * Supports the Material Theme Builder export format with the following - * structure: - * - * @code - * { - * "schemes": { - * "light": { "primary": "#6750A4", ... }, - * "dark": { "primary": "#D0BCFF", ... } - * } - * } - * @endcode - * - * Also supports direct color values: - * - * @code - * { - * "primary": "#6750A4", - * "onPrimary": "#FFFFFF", - * ... - * } - * @endcode - * - * @param[in] json JSON export from Material Theme Builder. - * @param[in] isDark Whether to use light or dark scheme from JSON (default: false). - * - * @return MaterialColorScheme parsed from JSON, or error. - * - * @since 0.1 - * @ingroup ui_core - */ -CF_UI_EXPORT Result fromJson(const QByteArray& json, bool isDark = false); - -/** - * @brief Creates dynamic color scheme from a key color. - * - * Implements Material You dynamic color generation using the - * HCT color space and tonal palette algorithm. - * - * @param[in] keyColor Source color for generating the scheme. - * @param[in] isDark Whether to generate a dark scheme (default: false). - * - * @return MaterialColorScheme generated from the key color. - * - * @since 0.1 - * @ingroup ui_core - */ -CF_UI_EXPORT MaterialColorScheme fromKeyColor(cf::ui::base::CFColor keyColor, bool isDark = false); - -/** - * @brief Converts a MaterialColorScheme to JSON format. - * - * Exports the scheme in Material Theme Builder compatible format. - * - * @param[in] scheme The color scheme to export. - * - * @return JSON representation of the color scheme. - * - * @since 0.1 - * @ingroup ui_core - */ -CF_UI_EXPORT QByteArray toJson(const MaterialColorScheme& scheme); - -// ============================================================================= -// Typography Factory Functions -// ============================================================================= - -/** - * @brief Creates default Material Design 3 typography scale. - * - * Uses system default sans-serif font with MD3 specified sizes and weights. - * - * @return MaterialTypography with default configuration. - * - * @since 0.1 - * @ingroup ui_core - * - * @code - * auto typography = cf::ui::core::material::defaultTypography(); - * QFont titleFont = typography.queryTargetFont("md.typography.titleLarge"); - * @endcode - */ -CF_UI_EXPORT MaterialTypography defaultTypography(); - -// ============================================================================= -// Radius Scale Factory Functions -// ============================================================================= - -/** - * @brief Creates default Material Design 3 radius scale. - * - * Uses Material Design 3 specified corner radius values. - * - * @return MaterialRadiusScale with default configuration. - * - * @since 0.1 - * @ingroup ui_core - * - * @code - * auto radiusScale = cf::ui::core::material::defaultRadiusScale(); - * float smallRadius = radiusScale.queryRadiusScale("md.shape.cornerSmall"); // 8.0f - * @endcode - */ -CF_UI_EXPORT MaterialRadiusScale defaultRadiusScale(); - -// ============================================================================= -// Motion Factory Functions -// ============================================================================= - -/** - * @brief Creates default Material Design 3 motion scheme. - * - * Uses Material Design 3 specified duration and easing values for animations. - * - * @return MaterialMotionScheme with default configuration. - * - * @since 0.1 - * @ingroup ui_core - * - * @code - * auto motionScheme = cf::ui::core::material::motion(); - * int duration = motionScheme.queryDuration("shortEnter"); // 200 - * auto spec = motionScheme.getMotionSpec("mediumExit"); - * @endcode - */ -CF_UI_EXPORT MaterialMotionScheme motion(); - -} // namespace cf::ui::core::material diff --git a/ui/core/material/material_factory_class.h b/ui/core/material/material_factory_class.h deleted file mode 100644 index 726a51f9d..000000000 --- a/ui/core/material/material_factory_class.h +++ /dev/null @@ -1,97 +0,0 @@ -/** - * @file material_factory_class.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Theme Factory - * @version 0.1 - * @date 2026-02-27 - * - * @copyright Copyright (c) 2026 - * - * @details - * MaterialFactory implements ThemeFactory interface for Material Design 3. - * Supports creating themes by name (light/dark) and from JSON. - */ - -#pragma once - -#include -#include - -#include "../export.h" -#include "../theme_factory.h" - -namespace cf::ui::core { - -/** - * @brief Material Design 3 Theme Factory. - * - * @details Implements ThemeFactory for Material Design 3 themes. - * Supports: - * - Creating predefined themes by name: "theme.material.light", "theme.material.dark" - * - Creating themes from Material Theme Builder JSON export - * - Exporting themes to JSON format - * - * @note None. - * @warning None. - * @throws None. - * @since 0.1 - * @ingroup ui_core - * - * @code - * MaterialFactory factory; - * - * // Create by name - * auto lightTheme = factory.fromName("theme.material.light"); - * auto darkTheme = factory.fromName("theme.material.dark"); - * - * // Create from JSON - * auto fromJson = factory.fromJson(jsonBytes); - * - * // Export to JSON - * auto json = factory.toJson(lightTheme.get()); - * @endcode - */ -class CF_UI_EXPORT MaterialFactory : public ThemeFactory { - public: - ~MaterialFactory() override = default; - - /** - * @brief Create theme by name. - * - * @param[in] name Theme name: - * - "theme.material.light": Light theme - * - "theme.material.dark": Dark theme - * - * @return Unique pointer to ICFTheme, or nullptr if name not recognized. - * - * @since 0.1 - * @ingroup ui_core - */ - std::unique_ptr fromName(const char* name) override; - - /** - * @brief Create theme from Material Theme Builder JSON. - * - * @param[in] json JSON export from Material Theme Builder. - * - * @return Unique pointer to ICFTheme, or nullptr if parsing fails. - * - * @since 0.1 - * @ingroup ui_core - */ - std::unique_ptr fromJson(const QByteArray& json) override; - - /** - * @brief Export theme to JSON format. - * - * @param[in] raw_theme Pointer to ICFTheme (must be MaterialTheme). - * - * @return JSON representation of the theme. - * - * @since 0.1 - * @ingroup ui_core - */ - QByteArray toJson(ICFTheme* raw_theme) override; -}; - -} // namespace cf::ui::core diff --git a/ui/core/motion_spec.h b/ui/core/motion_spec.h deleted file mode 100644 index 6d0a8231c..000000000 --- a/ui/core/motion_spec.h +++ /dev/null @@ -1,65 +0,0 @@ -/** - * @file motion_spec.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Motion specification interface for Material Design animations - * @version 0.1 - * @date 2026-02-26 - * - * @copyright Copyright (c) 2026 - * - * @details - * Defines the interface for motion/animation specifications following - * Material Design 3 motion principles. This interface provides a way to - * query animation duration, easing, and delay values by token name. - */ -#pragma once -#include "export.h" -#include - -namespace cf::ui::core { - -/** - * @brief Motion specification interface. - * - * Defines the contract for querying motion/animation specifications. - * Implementations should provide access to Material Design 3 motion - * presets with proper duration, easing, and delay values. - * - * @since 0.1 - * @ingroup ui_core - */ -struct CF_UI_EXPORT IMotionSpec { - virtual ~IMotionSpec() = default; - - /** - * @brief Query motion duration by token name. - * - * @param[in] name Motion token name (e.g., "md.motion.shortEnter"). - * @return Duration in milliseconds. - * - * @since 0.1 - */ - virtual int queryDuration(const char* name) = 0; - - /** - * @brief Query motion easing type by token name. - * - * @param[in] name Motion token name. - * @return Easing type enum value (as int for cross-language compatibility). - * - * @since 0.1 - */ - virtual int queryEasing(const char* name) = 0; - - /** - * @brief Query motion delay by token name. - * - * @param[in] name Motion token name. - * @return Delay in milliseconds (default 0). - * - * @since 0.1 - */ - virtual int queryDelay(const char* name) = 0; -}; - -} // namespace cf::ui::core diff --git a/ui/core/radius_scale.h b/ui/core/radius_scale.h deleted file mode 100644 index 28e6a0787..000000000 --- a/ui/core/radius_scale.h +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @file ui/core/radius_scale.h - * @brief Defines the IRadiusScale interface for querying corner radius values. - * - * Provides an abstract interface for querying radius scale values - * by name, typically used for Material Design 3 corner specifications. - * - * @author Charliechen114514 - * @date 2026-02-26 - * @version N/A - * @since N/A - * @ingroup ui_core - */ - -#pragma once -#include "export.h" - -namespace cf::ui::core { - -/** - * @brief Abstract interface for querying corner radius scales by name. - * - * Defines the contract for radius scale providers that return float - * radius values based on style names (e.g., "cornerSmall", "cornerLarge"). - * - * @ingroup ui_core - * - * @code - * IRadiusScale* radiusScale = getRadiusScale(); - * float radius = radiusScale->queryRadiusScale("cornerSmall"); - * @endcode - */ -struct CF_UI_EXPORT IRadiusScale { - /// @brief Virtual destructor. - virtual ~IRadiusScale() = default; - - /** - * @brief Query a corner radius value by name. - * - * @param[in] name Radius scale name (e.g., "cornerSmall", "cornerLarge"). - * @return Radius value in density-independent pixels (dp). - * Returns 0 if the name is not found. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_core - */ - virtual float queryRadiusScale(const char* name) = 0; -}; - -} // namespace cf::ui::core diff --git a/ui/core/theme.h b/ui/core/theme.h deleted file mode 100644 index 277c24645..000000000 --- a/ui/core/theme.h +++ /dev/null @@ -1,118 +0,0 @@ -/** - * @file ui/core/theme.h - * @brief Interface for CF UI theme components. - * - * Defines the ICFTheme interface which provides access to color schemes, - * motion specifications, radius scales, and font types for UI theming. - * - * @author N/A - * @date N/A - * @version N/A - * @since N/A - * @ingroup none - */ - -#pragma once - -#include "color_scheme.h" -#include "export.h" -#include "font_type.h" -#include "motion_spec.h" -#include "radius_scale.h" -#include - -namespace cf::ui::core { - -/** - * @brief Interface for CF UI theme components. - * - * ICFTheme provides access to the core theme components including color - * schemes, motion specifications, radius scales, and font types. Instances - * should only be created by ThemeFactory and accessed by reference. - * - * @ingroup none - * - * @note Theme instances should be accessed by reference. Only ThemeFactory - * should create instances. - * - * @warning Direct construction is not supported; use ThemeFactory instead. - * - * @code - * const ICFTheme& theme = ThemeManager::instance().theme("default"); - * const auto& colors = theme.color_scheme(); - * @endcode - */ -struct CF_UI_EXPORT ICFTheme { - public: - /// @brief ThemeFactory has exclusive creation access. - friend class ThemeFactory; - - /// @brief Virtual destructor. - virtual ~ICFTheme() = default; - - /** - * @brief Gets the color scheme component. - * - * @return Reference to the color scheme. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - ICFColorScheme& color_scheme() const { return *color_scheme_; } - - /** - * @brief Gets the motion specification component. - * - * @return Reference to the motion specification. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - IMotionSpec& motion_spec() const { return *motion_spec_; } - - /** - * @brief Gets the radius scale component. - * - * @return Reference to the radius scale. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - IRadiusScale& radius_scale() const { return *radius_scale_; } - - /** - * @brief Gets the font type component. - * - * @return Reference to the font type. - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - IFontType& font_type() const { return *font_type_; } - - protected: - /// @brief Default constructor: protected for factory-only creation. - ICFTheme() = default; - - /// @brief Color scheme component. Ownership: owner. - std::unique_ptr color_scheme_; - - /// @brief Motion specification component. Ownership: owner. - std::unique_ptr motion_spec_; - - /// @brief Radius scale component. Ownership: owner. - std::unique_ptr radius_scale_; - - /// @brief Font type component. Ownership: owner. - std::unique_ptr font_type_; -}; - -} // namespace cf::ui::core diff --git a/ui/core/theme_factory.h b/ui/core/theme_factory.h deleted file mode 100644 index f84975868..000000000 --- a/ui/core/theme_factory.h +++ /dev/null @@ -1,97 +0,0 @@ -/** - * @file ui/core/theme_factory.h - * @brief Abstract factory for creating and serializing CF UI themes. - * - * Defines the ThemeFactory interface for creating ICFTheme instances - * from names or JSON data, and for serializing themes to JSON format. - * - * @author N/A - * @date N/A - * @version N/A - * @since N/A - * @ingroup none - */ - -#pragma once - -#include "export.h" -#include "theme.h" -#include -#include - -namespace cf::ui::core { - -/** - * @brief Abstract factory for creating and serializing CF UI themes. - * - * ThemeFactory provides an interface for creating ICFTheme instances from - * theme names or JSON data, and for serializing existing themes to JSON. - * Implementations of this interface handle specific theme formats. - * - * @ingroup none - * - * @note None - * - * @warning None - * - * @code - * class MyThemeFactory : public ThemeFactory { - * public: - * std::unique_ptr fromName(const char* name) override { - * // Create theme by name - * } - * std::unique_ptr fromJson(const QByteArray& json) override { - * // Parse JSON and create theme - * } - * QByteArray toJson(ICFTheme* raw_theme) override { - * // Serialize theme to JSON - * } - * }; - * @endcode - */ -class CF_UI_EXPORT ThemeFactory { - public: - /// @brief Virtual destructor. - virtual ~ThemeFactory() = default; - - /** - * @brief Creates a theme from its name. - * - * @param[in] name Name identifier of the theme to create. - * @return Unique pointer to the created theme, or nullptr on error. - * @throws May throw exceptions depending on implementation. - * @note Implementation-specific behavior for unknown names. - * @warning None - * @since N/A - * @ingroup none - */ - virtual std::unique_ptr fromName(const char* name) = 0; - - /** - * @brief Creates a theme from JSON data. - * - * @param[in] json JSON byte array containing theme definition. - * @return Unique pointer to the created theme, or nullptr on error. - * @throws May throw exceptions depending on implementation. - * @note Implementation-specific JSON format validation. - * @warning None - * @since N/A - * @ingroup none - */ - virtual std::unique_ptr fromJson(const QByteArray& json) = 0; - - /** - * @brief Serializes a theme to JSON format. - * - * @param[in] raw_theme Pointer to the theme to serialize. - * @return JSON byte array representing the theme. - * @throws May throw exceptions depending on implementation. - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - virtual QByteArray toJson(ICFTheme* raw_theme) = 0; -}; - -} // namespace cf::ui::core diff --git a/ui/core/theme_manager.cpp b/ui/core/theme_manager.cpp deleted file mode 100644 index 49c355e4a..000000000 --- a/ui/core/theme_manager.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "theme_manager.h" - -#include -#include -#include -#include - -namespace cf::ui::core { - -ThemeManager::ThemeManager(QObject* parent) : QObject(parent) {} - -ThemeManager& ThemeManager::instance() { - static ThemeManager instance; - return instance; -} - -const ICFTheme& ThemeManager::theme(const std::string& name) const { - auto it = factories_.find(name); - if (it == factories_.end()) { - // Return a pointer indicator for "not found" - caller should check factories_ first - // Or we can throw an exception - throw std::runtime_error("Theme factory not found: " + name); - } - - // Create or get cached theme instance - auto cache_it = theme_cache_.find(name); - if (cache_it != theme_cache_.end()) { - return *cache_it->second; - } - - // const_cast is safe here as we're just populating the cache - auto& factory = it->second; - auto theme_ptr = factory->fromName(name.c_str()); - if (!theme_ptr) { - throw std::runtime_error("Failed to create theme: " + name); - } - - auto& non_const = const_cast(this)->theme_cache_[name]; - non_const = std::move(theme_ptr); - return *non_const; -} - -bool ThemeManager::insert_one(const std::string& name, InstallerMaker make_one) { - if (factories_.find(name) != factories_.end()) { - return false; // Already exists - } - auto factory = make_one(); - if (!factory) { - return false; - } - factories_[name] = std::move(factory); - return true; -} - -void ThemeManager::remove_one(const std::string& name) { - factories_.erase(name); - theme_cache_.erase(name); -} - -void ThemeManager::install_widget(QWidget* w) { - if (!w) - return; - installed_widget.insert(w); -} - -void ThemeManager::remove_widget(QWidget* w) { - installed_widget.erase(w); -} - -void ThemeManager::setThemeTo(const std::string& name, bool doBroadcast) { - auto it = factories_.find(name); - if (it == factories_.end()) { - return; // Theme not found - } - - // Get or create the theme - auto cache_it = theme_cache_.find(name); - if (cache_it == theme_cache_.end()) { - auto theme_ptr = it->second->fromName(name.c_str()); - if (!theme_ptr) { - return; // Failed to create theme - } - cache_it = theme_cache_.emplace(name, std::move(theme_ptr)).first; - } - - current_theme_name_ = name; - // Emit signal - if (doBroadcast) { - emit themeChanged(*cache_it->second); - } -} - -const std::string& ThemeManager::currentThemeName() const { - return current_theme_name_; -} -} // namespace cf::ui::core diff --git a/ui/core/theme_manager.h b/ui/core/theme_manager.h deleted file mode 100644 index e2e738a89..000000000 --- a/ui/core/theme_manager.h +++ /dev/null @@ -1,200 +0,0 @@ -/** - * @file ui/core/theme_manager.h - * @brief aex::Singleton manager for CF UI theme registration and application. - * - * ThemeManager manages theme factory registration, theme creation, and - * application of themes to widgets. Emits signals when the theme changes. - * - * @author N/A - * @date N/A - * @version N/A - * @since N/A - * @ingroup none - */ - -#pragma once -#include "export.h" -#include "theme.h" -#include "theme_factory.h" -#include -#include -#include -#include -#include -#include - -namespace cf::ui::core { - -/** - * @brief aex::Singleton manager for CF UI theme registration and application. - * - * ThemeManager manages theme factory registration, theme creation, and - * application of themes to widgets. Emits signals when the theme changes. - * - * @ingroup none - * - * @note aex::Singleton instance accessed via instance(). - * - * @warning None - * - * @code - * auto& manager = ThemeManager::instance(); - * manager.insert_one("my_theme", []() { return std::make_unique(); }); - * manager.install_widget(my_widget); - * manager.setThemeTo("my_theme"); - * @endcode - */ -class CF_UI_EXPORT ThemeManager : public QObject { - Q_OBJECT - public: - /** - * @brief Gets the singleton ThemeManager instance. - * - * @return Reference to the singleton instance. - * @throws None - * @note Thread-safe initialization. - * @warning None - * @since N/A - * @ingroup none - */ - static ThemeManager& instance(); - - /** - * @brief Gets a theme by name. - * - * @param[in] name Name of the theme to retrieve. - * @return Reference to the requested theme. - * @throws May throw if theme not found. - * @note Theme is created on first access and cached. - * @warning Theme must have been registered via insert_one(). - * @since N/A - * @ingroup none - */ - const ICFTheme& theme(const std::string& name) const; - - /// @brief Function type for creating ThemeFactory instances. - using InstallerMaker = std::function()>; - - /** - * @brief Registers a theme factory with a name. - * - * @param[in] name Name identifier for the theme. - * @param[in] make_one Factory function that creates ThemeFactory instances. - * @return true if registration succeeded, false on duplicate name. - * @throws None - * @note The factory function is called when creating themes. - * @warning None - * @since N/A - * @ingroup none - */ - bool insert_one(const std::string& name, InstallerMaker make_one); - - /** - * @brief Removes a theme factory by name. - * - * @param[in] name Name of the theme to remove. - * @throws None - * @note Does not affect currently active theme. - * @warning None - * @since N/A - * @ingroup none - */ - void remove_one(const std::string& name); - - /** - * @brief Installs a widget for theme updates. - * - * @param[in] w Pointer to the widget to install. - * @throws None - * @note Installed widgets receive theme change notifications. - * @warning None - * @since N/A - * @ingroup none - */ - void install_widget(QWidget* w); - - /** - * @brief Removes a widget from theme updates. - * - * @param[in] w Pointer to the widget to remove. - * @throws None - * @note The widget no longer receives theme change notifications. - * @warning None - * @since N/A - * @ingroup none - */ - void remove_widget(QWidget* w); - - /** - * @brief Sets the current active theme. - * - * @param[in] name Name of the theme to activate. - * @throws None - * @note Emits themeChanged signal after successful theme change. - * @warning Theme must have been registered via insert_one(). - * @since N/A - * @ingroup none - */ - void setThemeTo(const std::string& name, bool doBroadcast = true); - - /** - * @brief Get the current active theme name. - * - * @return Reference to the current theme name. - * @throws None - * @note Returns empty string if no theme has been set. - * @warning None - * @since N/A - * @ingroup none - */ - const std::string& currentThemeName() const; - - signals: - /** - * @brief Signal emitted when the theme changes. - * - * @param[in] new_theme Reference to the newly activated theme. - * - * @note None - * @warning None - * @since N/A - * @ingroup none - */ - void themeChanged(const ICFTheme& new_theme); - - private: - /** - * @brief Constructs the ThemeManager. - * - * @param[in] parent Optional parent QObject. - * @throws None - * @note Private constructor for singleton pattern. - * @warning None - * @since N/A - * @ingroup none - */ - ThemeManager(QObject* parent = nullptr); - - /// @brief Destructor. - ~ThemeManager() override = default; - - /// @brief Copy constructor: deleted. - ThemeManager(const ThemeManager&) = delete; - - /// @brief Copy assignment: deleted. - ThemeManager& operator=(const ThemeManager&) = delete; - - /// @brief Set of widgets receiving theme updates. Ownership: observer. - std::unordered_set installed_widget; - - /// @brief Map of registered theme factories. Ownership: owner. - std::unordered_map> factories_; - - /// @brief Cache of created theme instances. Ownership: owner. - std::unordered_map> theme_cache_; - - /// @brief Name of the currently active theme. - std::string current_theme_name_; -}; - -} // namespace cf::ui::core diff --git a/ui/core/token/material_scheme/cfmaterial_token_literals.h b/ui/core/token/material_scheme/cfmaterial_token_literals.h deleted file mode 100644 index 5fb4f47fe..000000000 --- a/ui/core/token/material_scheme/cfmaterial_token_literals.h +++ /dev/null @@ -1,280 +0,0 @@ -/** - * @file cfmaterial_token_literals.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Color Token String Literals - * @version 0.1 - * @date 2026-02-25 - * - * @copyright Copyright (c) 2026 - * - * @details - * Compile-time string constants for Material Design 3 color tokens. - * These literals are used throughout the color system to ensure - * consistency and avoid string duplication. - * - * @ingroup ui_core - */ -#pragma once - -#include - -namespace cf::ui::core::token::literals { - -// ============================================================================= -// Primary Color Token Literals -// ============================================================================= - -/** - * @brief Primary color token string literal. - * - * Used as the main branding color for key components. - */ -inline constexpr const char PRIMARY[] = "md.primary"; - -/** - * @brief On-primary color token string literal. - * - * Text and icons drawn on top of primary color. - */ -inline constexpr const char ON_PRIMARY[] = "md.onPrimary"; - -/** - * @brief Primary container color token string literal. - * - * Tonal surface variant containing primary-colored content. - */ -inline constexpr const char PRIMARY_CONTAINER[] = "md.primaryContainer"; - -/** - * @brief On-primary-container color token string literal. - * - * Text and icons drawn on top of primary container color. - */ -inline constexpr const char ON_PRIMARY_CONTAINER[] = "md.onPrimaryContainer"; - -// ============================================================================= -// Secondary Color Token Literals -// ============================================================================= - -/** - * @brief Secondary color token string literal. - * - * Alternative to primary color for less prominent components. - */ -inline constexpr const char SECONDARY[] = "md.secondary"; - -/** - * @brief On-secondary color token string literal. - * - * Text and icons drawn on top of secondary color. - */ -inline constexpr const char ON_SECONDARY[] = "md.onSecondary"; - -/** - * @brief Secondary container color token string literal. - * - * Tonal surface variant containing secondary-colored content. - */ -inline constexpr const char SECONDARY_CONTAINER[] = "md.secondaryContainer"; - -/** - * @brief On-secondary-container color token string literal. - * - * Text and icons drawn on top of secondary container color. - */ -inline constexpr const char ON_SECONDARY_CONTAINER[] = "md.onSecondaryContainer"; - -// ============================================================================= -// Tertiary Color Token Literals -// ============================================================================= - -/** - * @brief Tertiary color token string literal. - * - * Used for contrasting accents and balance in the UI. - */ -inline constexpr const char TERTIARY[] = "md.tertiary"; - -/** - * @brief On-tertiary color token string literal. - * - * Text and icons drawn on top of tertiary color. - */ -inline constexpr const char ON_TERTIARY[] = "md.onTertiary"; - -/** - * @brief Tertiary container color token string literal. - * - * Tonal surface variant containing tertiary-colored content. - */ -inline constexpr const char TERTIARY_CONTAINER[] = "md.tertiaryContainer"; - -/** - * @brief On-tertiary-container color token string literal. - * - * Text and icons drawn on top of tertiary container color. - */ -inline constexpr const char ON_TERTIARY_CONTAINER[] = "md.onTertiaryContainer"; - -// ============================================================================= -// Error Color Token Literals -// ============================================================================= - -/** - * @brief Error color token string literal. - * - * Used for error states and destructive actions. - */ -inline constexpr const char ERROR[] = "md.error"; - -/** - * @brief On-error color token string literal. - * - * Text and icons drawn on top of error color. - */ -inline constexpr const char ON_ERROR[] = "md.onError"; - -/** - * @brief Error container color token string literal. - * - * Tonal surface variant containing error-colored content. - */ -inline constexpr const char ERROR_CONTAINER[] = "md.errorContainer"; - -/** - * @brief On-error-container color token string literal. - * - * Text and icons drawn on top of error container color. - */ -inline constexpr const char ON_ERROR_CONTAINER[] = "md.onErrorContainer"; - -// ============================================================================= -// Surface Color Token Literals -// ============================================================================= - -/** - * @brief Background color token string literal. - * - * The base color of the application background. - */ -inline constexpr const char BACKGROUND[] = "md.background"; - -/** - * @brief On-background color token string literal. - * - * Text and icons drawn on top of background color. - */ -inline constexpr const char ON_BACKGROUND[] = "md.onBackground"; - -/** - * @brief Surface color token string literal. - * - * Color for surfaces such as cards, sheets, and menus. - */ -inline constexpr const char SURFACE[] = "md.surface"; - -/** - * @brief On-surface color token string literal. - * - * Text and icons drawn on top of surface color. - */ -inline constexpr const char ON_SURFACE[] = "md.onSurface"; - -/** - * @brief Surface variant color token string literal. - * - * Variant of surface color for subtle differentiation. - */ -inline constexpr const char SURFACE_VARIANT[] = "md.surfaceVariant"; - -/** - * @brief On-surface-variant color token string literal. - * - * Text and icons drawn on top of surface variant color. - */ -inline constexpr const char ON_SURFACE_VARIANT[] = "md.onSurfaceVariant"; - -/** - * @brief Outline color token string literal. - * - * Color for borders and outlines. - */ -inline constexpr const char OUTLINE[] = "md.outline"; - -/** - * @brief Outline variant color token string literal. - * - * Variant of outline color for subtle differentiation. - */ -inline constexpr const char OUTLINE_VARIANT[] = "md.outlineVariant"; - -// ============================================================================= -// Utility Color Token Literals -// ============================================================================= - -/** - * @brief Shadow color token string literal. - * - * Color for drop shadows. - */ -inline constexpr const char SHADOW[] = "md.shadow"; - -/** - * @brief Scrim color token string literal. - * - * Color for overlay scrim. - */ -inline constexpr const char SCRIM[] = "md.scrim"; - -/** - * @brief Inverse surface color token string literal. - * - * Inverted surface color for special UI scenarios. - */ -inline constexpr const char INVERSE_SURFACE[] = "md.inverseSurface"; - -/** - * @brief Inverse-on-surface color token string literal. - * - * Text and icons drawn on top of inverse surface color. - */ -inline constexpr const char INVERSE_ON_SURFACE[] = "md.inverseOnSurface"; - -/** - * @brief Inverse primary color token string literal. - * - * Inverted primary color for special UI scenarios. - */ -inline constexpr const char INVERSE_PRIMARY[] = "md.inversePrimary"; - -// ============================================================================= -// All Tokens Array (for iteration) -// ============================================================================= - -/** - * @brief Array containing all 26 Material Design 3 color token literals. - * - * Ordered by group: Primary (4), Secondary (4), Tertiary (4), - * Error (4), Surface (8), Utility (5). - */ -inline constexpr const char* const ALL_TOKENS[] = { - // Primary - PRIMARY, ON_PRIMARY, PRIMARY_CONTAINER, ON_PRIMARY_CONTAINER, - // Secondary - SECONDARY, ON_SECONDARY, SECONDARY_CONTAINER, ON_SECONDARY_CONTAINER, - // Tertiary - TERTIARY, ON_TERTIARY, TERTIARY_CONTAINER, ON_TERTIARY_CONTAINER, - // Error - ERROR, ON_ERROR, ERROR_CONTAINER, ON_ERROR_CONTAINER, - // Surface - BACKGROUND, ON_BACKGROUND, SURFACE, ON_SURFACE, SURFACE_VARIANT, ON_SURFACE_VARIANT, OUTLINE, - OUTLINE_VARIANT, - // Utility - SHADOW, SCRIM, INVERSE_SURFACE, INVERSE_ON_SURFACE, INVERSE_PRIMARY}; - -/** - * @brief Total count of Material Design 3 color tokens (auto-calculated). - */ -inline constexpr size_t TOKEN_COUNT = sizeof(ALL_TOKENS) / sizeof(ALL_TOKENS[0]); - -} // namespace cf::ui::core::token::literals diff --git a/ui/core/token/motion/cfmaterial_motion_token_literals.h b/ui/core/token/motion/cfmaterial_motion_token_literals.h deleted file mode 100644 index bb1034210..000000000 --- a/ui/core/token/motion/cfmaterial_motion_token_literals.h +++ /dev/null @@ -1,190 +0,0 @@ -/** - * @file cfmaterial_motion_token_literals.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Motion Token String Literals - * @version 0.1 - * @date 2026-02-26 - * - * @copyright Copyright (c) 2026 - * - * @details - * Compile-time string constants for Material Design 3 motion tokens. - * These literals are used throughout the motion system to ensure - * consistency and avoid string duplication. - * - * @ingroup ui_core - */ -#pragma once - -#include - -namespace cf::ui::core::token::literals { - -// ============================================================================= -// Motion Duration Token Literals -// ============================================================================= - -/** - * @brief Short enter motion duration token. - * - * Duration: 200ms - * Used for: Small elements entering the screen - */ -inline constexpr const char MOTION_SHORT_ENTER_DURATION[] = "md.motion.shortEnter.duration"; - -/** - * @brief Short exit motion duration token. - * - * Duration: 150ms - * Used for: Small elements exiting the screen - */ -inline constexpr const char MOTION_SHORT_EXIT_DURATION[] = "md.motion.shortExit.duration"; - -/** - * @brief Medium enter motion duration token. - * - * Duration: 300ms - * Used for: Medium-sized elements entering the screen - */ -inline constexpr const char MOTION_MEDIUM_ENTER_DURATION[] = "md.motion.mediumEnter.duration"; - -/** - * @brief Medium exit motion duration token. - * - * Duration: 250ms - * Used for: Medium-sized elements exiting the screen - */ -inline constexpr const char MOTION_MEDIUM_EXIT_DURATION[] = "md.motion.mediumExit.duration"; - -/** - * @brief Long enter motion duration token. - * - * Duration: 450ms - * Used for: Large elements entering the screen - */ -inline constexpr const char MOTION_LONG_ENTER_DURATION[] = "md.motion.longEnter.duration"; - -/** - * @brief Long exit motion duration token. - * - * Duration: 400ms - * Used for: Large elements exiting the screen - */ -inline constexpr const char MOTION_LONG_EXIT_DURATION[] = "md.motion.longExit.duration"; - -/** - * @brief State change motion duration token. - * - * Duration: 200ms - * Used for: State layer animations - */ -inline constexpr const char MOTION_STATE_CHANGE_DURATION[] = "md.motion.stateChange.duration"; - -/** - * @brief Ripple expand motion duration token. - * - * Duration: 400ms - * Used for: Ripple effect expansion - */ -inline constexpr const char MOTION_RIPPLE_EXPAND_DURATION[] = "md.motion.rippleExpand.duration"; - -/** - * @brief Ripple fade motion duration token. - * - * Duration: 150ms - * Used for: Ripple effect fade out - */ -inline constexpr const char MOTION_RIPPLE_FADE_DURATION[] = "md.motion.rippleFade.duration"; - -// ============================================================================= -// Motion Easing Token Literals -// ============================================================================= - -/** - * @brief Short enter motion easing token. - * - * Easing: EmphasizedDecelerate - */ -inline constexpr const char MOTION_SHORT_ENTER_EASING[] = "md.motion.shortEnter.easing"; - -/** - * @brief Short exit motion easing token. - * - * Easing: EmphasizedAccelerate - */ -inline constexpr const char MOTION_SHORT_EXIT_EASING[] = "md.motion.shortExit.easing"; - -/** - * @brief Medium enter motion easing token. - * - * Easing: EmphasizedDecelerate - */ -inline constexpr const char MOTION_MEDIUM_ENTER_EASING[] = "md.motion.mediumEnter.easing"; - -/** - * @brief Medium exit motion easing token. - * - * Easing: EmphasizedAccelerate - */ -inline constexpr const char MOTION_MEDIUM_EXIT_EASING[] = "md.motion.mediumExit.easing"; - -/** - * @brief Long enter motion easing token. - * - * Easing: Emphasized - */ -inline constexpr const char MOTION_LONG_ENTER_EASING[] = "md.motion.longEnter.easing"; - -/** - * @brief Long exit motion easing token. - * - * Easing: Emphasized - */ -inline constexpr const char MOTION_LONG_EXIT_EASING[] = "md.motion.longExit.easing"; - -/** - * @brief State change motion easing token. - * - * Easing: Standard - */ -inline constexpr const char MOTION_STATE_CHANGE_EASING[] = "md.motion.stateChange.easing"; - -/** - * @brief Ripple expand motion easing token. - * - * Easing: Standard - */ -inline constexpr const char MOTION_RIPPLE_EXPAND_EASING[] = "md.motion.rippleExpand.easing"; - -/** - * @brief Ripple fade motion easing token. - * - * Easing: Linear - */ -inline constexpr const char MOTION_RIPPLE_FADE_EASING[] = "md.motion.rippleFade.easing"; - -// ============================================================================= -// All Motion Tokens Array (for iteration) -// ============================================================================= - -/** - * @brief Array containing all Material Design 3 motion token literals. - * - * Ordered by category: Durations (9), Easing (9). - */ -inline constexpr const char* const ALL_MOTION_TOKENS[] = { - // Durations - MOTION_SHORT_ENTER_DURATION, MOTION_SHORT_EXIT_DURATION, MOTION_MEDIUM_ENTER_DURATION, - MOTION_MEDIUM_EXIT_DURATION, MOTION_LONG_ENTER_DURATION, MOTION_LONG_EXIT_DURATION, - MOTION_STATE_CHANGE_DURATION, MOTION_RIPPLE_EXPAND_DURATION, MOTION_RIPPLE_FADE_DURATION, - // Easing - MOTION_SHORT_ENTER_EASING, MOTION_SHORT_EXIT_EASING, MOTION_MEDIUM_ENTER_EASING, - MOTION_MEDIUM_EXIT_EASING, MOTION_LONG_ENTER_EASING, MOTION_LONG_EXIT_EASING, - MOTION_STATE_CHANGE_EASING, MOTION_RIPPLE_EXPAND_EASING, MOTION_RIPPLE_FADE_EASING}; - -/** - * @brief Total count of Material Design 3 motion tokens. - */ -inline constexpr size_t MOTION_TOKEN_COUNT = 18; - -} // namespace cf::ui::core::token::literals diff --git a/ui/core/token/radius_scale/cfmaterial_radius_scale_literals.h b/ui/core/token/radius_scale/cfmaterial_radius_scale_literals.h deleted file mode 100644 index f3bd85a56..000000000 --- a/ui/core/token/radius_scale/cfmaterial_radius_scale_literals.h +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @file cfmaterial_radius_scale_literals.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Radius Scale Token String Literals - * @version 0.1 - * @date 2026-02-26 - * - * @copyright Copyright (c) 2026 - * - * @details - * Compile-time string constants for Material Design 3 radius scale tokens. - * These literals are used throughout the shape system to ensure - * consistency and avoid string duplication. - * - * @ingroup ui_core - */ -#pragma once - -#include - -namespace cf::ui::core::token::literals { - -// ============================================================================= -// Corner Radius Token Literals -// ============================================================================= - -/** - * @brief Corner None token string literal. - * - * Value: 0dp - No corner radius. - */ -inline constexpr const char CORNER_NONE[] = "md.shape.cornerNone"; - -/** - * @brief Corner Extra Small token string literal. - * - * Value: 4dp - Used for chips, small cards. - */ -inline constexpr const char CORNER_EXTRA_SMALL[] = "md.shape.cornerExtraSmall"; - -/** - * @brief Corner Small token string literal. - * - * Value: 8dp - Used for text fields, checkboxes. - */ -inline constexpr const char CORNER_SMALL[] = "md.shape.cornerSmall"; - -/** - * @brief Corner Medium token string literal. - * - * Value: 12dp - Used for cards. - */ -inline constexpr const char CORNER_MEDIUM[] = "md.shape.cornerMedium"; - -/** - * @brief Corner Large token string literal. - * - * Value: 16dp - Used for alert dialogs. - */ -inline constexpr const char CORNER_LARGE[] = "md.shape.cornerLarge"; - -/** - * @brief Corner Extra Large token string literal. - * - * Value: 28dp - Used for FAB, modals. - */ -inline constexpr const char CORNER_EXTRA_LARGE[] = "md.shape.cornerExtraLarge"; - -/** - * @brief Corner Extra Extra Large token string literal. - * - * Value: 32dp - Used for drawers. - */ -inline constexpr const char CORNER_EXTRA_EXTRA_LARGE[] = "md.shape.cornerExtraExtraLarge"; - -// ============================================================================= -// All Tokens Array (for iteration) -// ============================================================================= - -/** - * @brief Array containing all 7 Material Design 3 radius scale token literals. - */ -inline constexpr const char* const ALL_RADIUS_TOKENS[] = { - CORNER_NONE, CORNER_EXTRA_SMALL, CORNER_SMALL, CORNER_MEDIUM, CORNER_LARGE, - CORNER_EXTRA_LARGE, CORNER_EXTRA_EXTRA_LARGE}; - -/** - * @brief Total count of Material Design 3 radius scale tokens. - */ -inline constexpr size_t RADIUS_TOKEN_COUNT = 7; - -} // namespace cf::ui::core::token::literals diff --git a/ui/core/token/theme_name/material_theme_name.h b/ui/core/token/theme_name/material_theme_name.h deleted file mode 100644 index 4fe609a31..000000000 --- a/ui/core/token/theme_name/material_theme_name.h +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @file ui/core/token/theme_name/material_theme_name.h - * @brief Literal constants for Material theme names. - * - * Provides string literal constants for identifying Material Design - * themes in the token system. - * - * @author N/A - * @date N/A - * @version N/A - * @since N/A - * @ingroup none - */ - -#pragma once - -namespace cf::ui::core::token::literals { - -/// @brief Material theme default name identifier. -inline constexpr const char MATERIAL_THEME_NAME[] = "theme.md.material"; - -/// @brief Material theme light variant name identifier. -inline constexpr const char MATERIAL_THEME_LIGHT[] = "theme.material.light"; - -/// @brief Material theme dark variant name identifier. -inline constexpr const char MATERIAL_THEME_DARK[] = "theme.material.dark"; - -} // namespace cf::ui::core::token::literals diff --git a/ui/core/token/typography/cfmaterial_typography_token_literals.h b/ui/core/token/typography/cfmaterial_typography_token_literals.h deleted file mode 100644 index a06fe3dc4..000000000 --- a/ui/core/token/typography/cfmaterial_typography_token_literals.h +++ /dev/null @@ -1,301 +0,0 @@ -/** - * @file cfmaterial_typography_token_literals.h - * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Typography Token String Literals - * @version 0.1 - * @date 2026-02-26 - * - * @copyright Copyright (c) 2026 - * - * @details - * Compile-time string constants for Material Design 3 typography tokens. - * These literals are used throughout the typography system to ensure - * consistency and avoid string duplication. - * - * @ingroup ui_core - */ -#pragma once - -#include - -namespace cf::ui::core::token::literals { - -// ============================================================================= -// Display Typography Token Literals -// ============================================================================= - -/** - * @brief Display Large typography token string literal. - * - * Used for hero content, size: 57sp, weight: 400, line-height: 64sp. - */ -inline constexpr const char TYPOGRAPHY_DISPLAY_LARGE[] = "md.typography.displayLarge"; - -/** - * @brief Display Medium typography token string literal. - * - * Used for hero content, size: 45sp, weight: 400, line-height: 52sp. - */ -inline constexpr const char TYPOGRAPHY_DISPLAY_MEDIUM[] = "md.typography.displayMedium"; - -/** - * @brief Display Small typography token string literal. - * - * Used for hero content, size: 36sp, weight: 400, line-height: 44sp. - */ -inline constexpr const char TYPOGRAPHY_DISPLAY_SMALL[] = "md.typography.displaySmall"; - -// ============================================================================= -// Headline Typography Token Literals -// ============================================================================= - -/** - * @brief Headline Large typography token string literal. - * - * Used for app bar important text, size: 32sp, weight: 400, line-height: 40sp. - */ -inline constexpr const char TYPOGRAPHY_HEADLINE_LARGE[] = "md.typography.headlineLarge"; - -/** - * @brief Headline Medium typography token string literal. - * - * Used for app bar important text, size: 28sp, weight: 400, line-height: 36sp. - */ -inline constexpr const char TYPOGRAPHY_HEADLINE_MEDIUM[] = "md.typography.headlineMedium"; - -/** - * @brief Headline Small typography token string literal. - * - * Used for app bar important text, size: 24sp, weight: 400, line-height: 32sp. - */ -inline constexpr const char TYPOGRAPHY_HEADLINE_SMALL[] = "md.typography.headlineSmall"; - -// ============================================================================= -// Title Typography Token Literals -// ============================================================================= - -/** - * @brief Title Large typography token string literal. - * - * Used for section headings, size: 22sp, weight: 500, line-height: 28sp. - */ -inline constexpr const char TYPOGRAPHY_TITLE_LARGE[] = "md.typography.titleLarge"; - -/** - * @brief Title Medium typography token string literal. - * - * Used for section headings, size: 16sp, weight: 500, line-height: 24sp. - */ -inline constexpr const char TYPOGRAPHY_TITLE_MEDIUM[] = "md.typography.titleMedium"; - -/** - * @brief Title Small typography token string literal. - * - * Used for section headings, size: 14sp, weight: 500, line-height: 20sp. - */ -inline constexpr const char TYPOGRAPHY_TITLE_SMALL[] = "md.typography.titleSmall"; - -// ============================================================================= -// Body Typography Token Literals -// ============================================================================= - -/** - * @brief Body Large typography token string literal. - * - * Used for main content, size: 16sp, weight: 400, line-height: 24sp. - */ -inline constexpr const char TYPOGRAPHY_BODY_LARGE[] = "md.typography.bodyLarge"; - -/** - * @brief Body Medium typography token string literal. - * - * Used for main content, size: 14sp, weight: 400, line-height: 20sp. - */ -inline constexpr const char TYPOGRAPHY_BODY_MEDIUM[] = "md.typography.bodyMedium"; - -/** - * @brief Body Small typography token string literal. - * - * Used for main content, size: 12sp, weight: 400, line-height: 16sp. - */ -inline constexpr const char TYPOGRAPHY_BODY_SMALL[] = "md.typography.bodySmall"; - -// ============================================================================= -// Label Typography Token Literals -// ============================================================================= - -/** - * @brief Label Large typography token string literal. - * - * Used for secondary information, size: 14sp, weight: 500, line-height: 20sp. - */ -inline constexpr const char TYPOGRAPHY_LABEL_LARGE[] = "md.typography.labelLarge"; - -/** - * @brief Label Medium typography token string literal. - * - * Used for secondary information, size: 12sp, weight: 500, line-height: 16sp. - */ -inline constexpr const char TYPOGRAPHY_LABEL_MEDIUM[] = "md.typography.labelMedium"; - -/** - * @brief Label Small typography token string literal. - * - * Used for secondary information, size: 11sp, weight: 500, line-height: 16sp. - */ -inline constexpr const char TYPOGRAPHY_LABEL_SMALL[] = "md.typography.labelSmall"; - -// ============================================================================= -// Line Height Token Literals -// ============================================================================= - -/** - * @brief Display Large line-height token string literal. - * - * Value: 64sp - */ -inline constexpr const char LINEHEIGHT_DISPLAY_LARGE[] = "md.lineHeight.displayLarge"; - -/** - * @brief Display Medium line-height token string literal. - * - * Value: 52sp - */ -inline constexpr const char LINEHEIGHT_DISPLAY_MEDIUM[] = "md.lineHeight.displayMedium"; - -/** - * @brief Display Small line-height token string literal. - * - * Value: 44sp - */ -inline constexpr const char LINEHEIGHT_DISPLAY_SMALL[] = "md.lineHeight.displaySmall"; - -/** - * @brief Headline Large line-height token string literal. - * - * Value: 40sp - */ -inline constexpr const char LINEHEIGHT_HEADLINE_LARGE[] = "md.lineHeight.headlineLarge"; - -/** - * @brief Headline Medium line-height token string literal. - * - * Value: 36sp - */ -inline constexpr const char LINEHEIGHT_HEADLINE_MEDIUM[] = "md.lineHeight.headlineMedium"; - -/** - * @brief Headline Small line-height token string literal. - * - * Value: 32sp - */ -inline constexpr const char LINEHEIGHT_HEADLINE_SMALL[] = "md.lineHeight.headlineSmall"; - -/** - * @brief Title Large line-height token string literal. - * - * Value: 28sp - */ -inline constexpr const char LINEHEIGHT_TITLE_LARGE[] = "md.lineHeight.titleLarge"; - -/** - * @brief Title Medium line-height token string literal. - * - * Value: 24sp - */ -inline constexpr const char LINEHEIGHT_TITLE_MEDIUM[] = "md.lineHeight.titleMedium"; - -/** - * @brief Title Small line-height token string literal. - * - * Value: 20sp - */ -inline constexpr const char LINEHEIGHT_TITLE_SMALL[] = "md.lineHeight.titleSmall"; - -/** - * @brief Body Large line-height token string literal. - * - * Value: 24sp - */ -inline constexpr const char LINEHEIGHT_BODY_LARGE[] = "md.lineHeight.bodyLarge"; - -/** - * @brief Body Medium line-height token string literal. - * - * Value: 20sp - */ -inline constexpr const char LINEHEIGHT_BODY_MEDIUM[] = "md.lineHeight.bodyMedium"; - -/** - * @brief Body Small line-height token string literal. - * - * Value: 16sp - */ -inline constexpr const char LINEHEIGHT_BODY_SMALL[] = "md.lineHeight.bodySmall"; - -/** - * @brief Label Large line-height token string literal. - * - * Value: 20sp - */ -inline constexpr const char LINEHEIGHT_LABEL_LARGE[] = "md.lineHeight.labelLarge"; - -/** - * @brief Label Medium line-height token string literal. - * - * Value: 16sp - */ -inline constexpr const char LINEHEIGHT_LABEL_MEDIUM[] = "md.lineHeight.labelMedium"; - -/** - * @brief Label Small line-height token string literal. - * - * Value: 16sp - */ -inline constexpr const char LINEHEIGHT_LABEL_SMALL[] = "md.lineHeight.labelSmall"; - -// ============================================================================= -// All Tokens Array (for iteration) -// ============================================================================= - -/** - * @brief Array containing all 15 Material Design 3 typography token literals. - */ -inline constexpr const char* const ALL_TYPOGRAPHY_TOKENS[] = { - // Display - TYPOGRAPHY_DISPLAY_LARGE, TYPOGRAPHY_DISPLAY_MEDIUM, TYPOGRAPHY_DISPLAY_SMALL, - // Headline - TYPOGRAPHY_HEADLINE_LARGE, TYPOGRAPHY_HEADLINE_MEDIUM, TYPOGRAPHY_HEADLINE_SMALL, - // Title - TYPOGRAPHY_TITLE_LARGE, TYPOGRAPHY_TITLE_MEDIUM, TYPOGRAPHY_TITLE_SMALL, - // Body - TYPOGRAPHY_BODY_LARGE, TYPOGRAPHY_BODY_MEDIUM, TYPOGRAPHY_BODY_SMALL, - // Label - TYPOGRAPHY_LABEL_LARGE, TYPOGRAPHY_LABEL_MEDIUM, TYPOGRAPHY_LABEL_SMALL}; - -/** - * @brief Array containing all 15 Material Design 3 line-height token literals. - */ -inline constexpr const char* const ALL_LINEHEIGHT_TOKENS[] = { - // Display - LINEHEIGHT_DISPLAY_LARGE, LINEHEIGHT_DISPLAY_MEDIUM, LINEHEIGHT_DISPLAY_SMALL, - // Headline - LINEHEIGHT_HEADLINE_LARGE, LINEHEIGHT_HEADLINE_MEDIUM, LINEHEIGHT_HEADLINE_SMALL, - // Title - LINEHEIGHT_TITLE_LARGE, LINEHEIGHT_TITLE_MEDIUM, LINEHEIGHT_TITLE_SMALL, - // Body - LINEHEIGHT_BODY_LARGE, LINEHEIGHT_BODY_MEDIUM, LINEHEIGHT_BODY_SMALL, - // Label - LINEHEIGHT_LABEL_LARGE, LINEHEIGHT_LABEL_MEDIUM, LINEHEIGHT_LABEL_SMALL}; - -/** - * @brief Total count of Material Design 3 typography tokens. - */ -inline constexpr size_t TYPOGRAPHY_TOKEN_COUNT = 15; - -/** - * @brief Total count of Material Design 3 line-height tokens. - */ -inline constexpr size_t LINEHEIGHT_TOKEN_COUNT = 15; - -} // namespace cf::ui::core::token::literals diff --git a/ui/export.h b/ui/export.h deleted file mode 100644 index ffd3945e1..000000000 --- a/ui/export.h +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @file export.h - * @brief CF UI Library export/import macros - * - * Defines CF_UI_EXPORT macro for controlling symbol visibility in shared libraries. - * - When building cfui.dll/so, symbols are exported - * - When using cfui.dll/so, symbols are imported - * - * @copyright Copyright (c) 2026 - */ -#pragma once - -/** - * @def CF_UI_EXPORT - * @brief Export/import macro for CF UI Library symbols - * - * Usage: - * class CF_UI_EXPORT MyClass { ... }; - * - * CF_UI_EXPORT void myFunction(); - * - * Platform behavior: - * - Windows: Uses __declspec(dllexport/dllimport) - * - Linux/GCC: Uses __attribute__((visibility("default"))) - * - * CMake automatically defines CFUI_EXPORTS when building the shared library. - * This follows the standard CMake convention: _EXPORTS is defined - * when compiling sources that are part of a SHARED library target. - */ - -#if defined(_WIN32) || defined(_MSC_VER) -# if defined(cfui_EXPORTS) || defined(CFUI_EXPORTS) -# define CF_UI_EXPORT __declspec(dllexport) -# else -# define CF_UI_EXPORT __declspec(dllimport) -# endif -#else -// Linux/Unix: same attribute for both building and using -# define CF_UI_EXPORT __attribute__((visibility("default"))) -#endif diff --git a/ui/models/CMakeLists.txt b/ui/models/CMakeLists.txt deleted file mode 100644 index 23951df71..000000000 --- a/ui/models/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -project(ui_models LANGUAGES CXX) - -log_info("UI_Model" "Start Configure the UI Model") - -# Placeholder for future UI model components -# This will provide SDK-like models for programmers to quickly start apps -add_library(cf_ui_models INTERFACE) - -# Add alias for consistent naming -add_library(UI::cf_ui_models ALIAS cf_ui_models) - -log_info("UI_Model" "the UI Model Configuration Done") \ No newline at end of file diff --git a/ui/widget/CMakeLists.txt b/ui/widget/CMakeLists.txt deleted file mode 100644 index 223927064..000000000 --- a/ui/widget/CMakeLists.txt +++ /dev/null @@ -1,92 +0,0 @@ -project(ui_widget LANGUAGES CXX) - -log_info("UI_Widget" "Start Configure the UI Widget") - - -add_library(cf_ui_application_support STATIC - application_support/application.cpp -) - -target_include_directories(cf_ui_application_support PUBLIC - $ - $ -) - -target_link_libraries(cf_ui_application_support PUBLIC - Qt6::Core - Qt6::Gui - Qt6::Widgets - CFDesktop::base_headers - cf_ui_base - cf_ui_components - cf_ui_components_material - cf_ui_core -) - -add_library(cf_ui_widget_material STATIC - material/application/material_application.cpp - material/base/state_machine.cpp - material/base/ripple_helper.cpp - material/base/elevation_controller.cpp - material/base/focus_ring.cpp - material/base/painter_layer.cpp - material/base/material_widget_base.cpp - material/widget/button/button.cpp - material/widget/checkbox/checkbox.cpp - material/widget/comboBox/combobox.cpp - material/widget/groupbox/groupbox.cpp - material/widget/label/label.cpp - material/widget/progressbar/progressbar.cpp - material/widget/radiobutton/radiobutton.cpp - material/widget/slider/slider.cpp - material/widget/switch/switch.cpp - material/widget/textarea/textarea.cpp - material/widget/textfield/textfield.cpp - material/widget/spinbox/spinbox.cpp - material/widget/doublespinbox/doublespinbox.cpp - material/widget/listview/listview.cpp - material/widget/scrollview/scrollview.cpp - material/widget/separator/separator.cpp - material/widget/tableview/tableview.cpp - material/widget/tabview/tabview.cpp - material/widget/tabview/private/materialtabbar.cpp - material/widget/treeview/treeview.cpp - material/widget/treeview/private/treeviewdelegate.cpp -) - -# Add include directories -target_include_directories(cf_ui_widget_material PUBLIC - $ - $ -) - -# Link Qt libraries -target_link_libraries(cf_ui_widget_material PUBLIC - Qt6::Core - Qt6::Gui - Qt6::Widgets - CFDesktop::base_headers - cf_ui_base - cf_ui_components - cf_ui_components_material - cf_ui_core - cf_ui_application_support -) - -# ============================================================ -# Unified UI Widget Library - INTERFACE library -# ============================================================ - -add_library(cf_ui_widget INTERFACE) - -target_include_directories(cf_ui_widget INTERFACE - $ - $ -) - -target_link_libraries(cf_ui_widget INTERFACE - cf_ui_widget_material - cf_ui_application_support -) - -log_info("UI_Widget" "the UI Widget Configuration Done") diff --git a/ui/widget/application_support/application.cpp b/ui/widget/application_support/application.cpp deleted file mode 100644 index 9a4f89682..000000000 --- a/ui/widget/application_support/application.cpp +++ /dev/null @@ -1,188 +0,0 @@ -/** - * @file ui/widget/application_support/application.cpp - * @author N/A - * @brief CF Desktop Application class implementation - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - */ - -#include "application.h" -#include - -namespace cf::ui::widget::application_support { - -Application::Application(int& argc, char** argv) : QApplication(argc, argv), initialized_(false) { - // Connect to ThemeManager's themeChanged signal - auto& tm = core::ThemeManager::instance(); - connect(&tm, &core::ThemeManager::themeChanged, this, &Application::onThemeManagerChanged); -} - -Application::~Application() { - // Cleanup: animationFactory_ must be destroyed before theme cleanup - // (since it holds references to the theme) - animationFactory_.reset(); -} - -void Application::init() { - if (initialized_) { - throw std::runtime_error( - "Application::init() already called. Initialization can only be done once."); - } - - // Initialize animation factory with current theme - initializeAnimationFactory(); - initialized_ = true; -} - -Application* Application::instance() { - return qobject_cast(QApplication::instance()); -} - -core::ThemeManager* Application::themeManager() { - return &core::ThemeManager::instance(); -} - -aex::WeakPtr Application::animationFactory() { - if (auto* app = instance()) { - return app->animationFactory_->GetWeakPtr(); - } - return {}; -} - -const core::ICFTheme& Application::theme(const std::string& themeToken) const { - return core::ThemeManager::instance().theme(themeToken); -} - -void Application::setTheme(const std::string& themeToken) { - core::ThemeManager::instance().setThemeTo(themeToken); -} - -const core::ICFTheme& Application::currentTheme() const { - auto& tm = core::ThemeManager::instance(); - const auto& name = tm.currentThemeName(); - - if (name.empty()) { - // If no theme has been set, throw exception - // TODO: Support dynamic loading of default themes - throw std::runtime_error("No theme set. Use MaterialApplication for automatic theme " - "registration, or call setTheme() before accessing themes."); - } - - return tm.theme(name); -} - -aex::WeakPtr -Application::animation(const std::string& animationToken) { - if (animationFactory_) { - return animationFactory_->getAnimation(animationToken.c_str()); - } - return {}; -} - -void Application::setAnimationsEnabled(bool enabled) { - if (animationFactory_) { - animationFactory_->setEnabledAll(enabled); - emit animationsEnabledChanged(enabled); - } -} - -bool Application::animationsEnabled() const { - return animationFactory_ ? animationFactory_->isAllEnabled() : false; -} - -void Application::initializeAnimationFactory() { - auto& tm = core::ThemeManager::instance(); - const auto& themeName = tm.currentThemeName(); - const auto& currentTheme = this->currentTheme(); - - animationFactory_ = createAnimationFactory(themeName, currentTheme, this); -} - -void Application::onThemeManagerChanged(const core::ICFTheme& newTheme) { - // Recreate animation factory with new theme - // This is necessary because factories store a reference to theme - auto oldEnabled = animationsEnabled(); - auto& tm = core::ThemeManager::instance(); - const auto& themeName = tm.currentThemeName(); - - animationFactory_ = createAnimationFactory(themeName, newTheme, this); - - animationFactory_->setEnabledAll(oldEnabled); - - // Forward the signal - emit themeChanged(newTheme); -} - -// ============================================================================= -// Animation Factory Registry -// ============================================================================= - -std::unordered_map& -Application::animationFactoryRegistry() { - static std::unordered_map registry; - return registry; -} - -bool Application::registerAnimationFactoryType(const std::string& themePrefix, - AnimationFactoryMaker maker) { - auto& registry = animationFactoryRegistry(); - - if (registry.find(themePrefix) != registry.end()) { - return false; // Already registered - } - - registry[themePrefix] = std::move(maker); - return true; -} - -void Application::unregisterAnimationFactoryType(const std::string& themePrefix) { - auto& registry = animationFactoryRegistry(); - registry.erase(themePrefix); -} - -bool Application::registerAnimationFactory(const std::string& themePrefix, - AnimationFactoryMaker maker) { - return registerAnimationFactoryType(themePrefix, std::move(maker)); -} - -void Application::unregisterAnimationFactory(const std::string& themePrefix) { - unregisterAnimationFactoryType(themePrefix); -} - -std::unique_ptr -Application::createAnimationFactory(const std::string& themeName, const core::ICFTheme& theme, - QObject* parent) { - auto& registry = animationFactoryRegistry(); - - // Find the best matching prefix - // themeName format: "theme.material.light", "theme.fluent.dark" - // We try to match "theme.material", "theme.fluent", etc. - - // Split by '.' and check for prefixes - // Try longest prefixes first (most specific) - std::string bestMatch; - for (const auto& [prefix, maker] : registry) { - if (themeName.size() >= prefix.size() && themeName.compare(0, prefix.size(), prefix) == 0) { - // Check if this is a better (longer) match - if (prefix.size() > bestMatch.size()) { - bestMatch = prefix; - } - } - } - - if (!bestMatch.empty()) { - // Found a matching factory - return registry[bestMatch](theme, parent); - } - - // No matching factory found - throw exception - // TODO: Support dynamic loading of animation factories - throw std::runtime_error( - "No animation factory registered for theme '" + themeName + - "'. Register an animation factory for this theme prefix, " - "or use MaterialApplication which includes default Material factory support."); -} - -} // namespace cf::ui::widget::application_support diff --git a/ui/widget/application_support/application.h b/ui/widget/application_support/application.h deleted file mode 100644 index a1aa7fb6b..000000000 --- a/ui/widget/application_support/application.h +++ /dev/null @@ -1,431 +0,0 @@ -/** - * @file ui/widget/application_support/application.h - * @author N/A - * @brief CF Desktop Application class with integrated theme and animation support - * @version 0.1 - * @date 2026-02-28 - * - * @copyright Copyright (c) 2026 - * - * @details - * Application extends QApplication to provide unified access to ThemeManager - * and CFMaterialAnimationFactory. It replaces the standard QApplication in main() - * and provides token-based APIs for theme and animation access. - */ - -#pragma once - -#include "components/animation_factory_manager.h" -#include "core/theme_manager.h" -#include "export.h" -#include -#include -#include -#include -#include - -namespace cf::ui::widget::application_support { - -/** - * @brief Application class with integrated theme and animation management. - * - * @details Extends QApplication to provide unified access to ThemeManager and - * CFMaterialAnimationFactory. Replaces standard QApplication in main(). - * - * @note Thread-safe for concurrent reads. - * @warning The animation factory is owned by Application; aex::WeakPtr may become - * invalid if the application is destroyed. - * @throws None (all errors return invalid aex::WeakPtr or throw from ThemeManager) - * @since 0.1 - * @ingroup ui_widget_application_support - * - * @code - * int main(int argc, char* argv[]) { - * using namespace cf::ui::widget::application_support; - * - * Application app(argc, argv); - * app.setTheme("theme.material.light"); - * - * MyWidget w; - * w.show(); - * - * return app.exec(); - * } - * - * // Access animations from anywhere - * auto fadeIn = Application::animation("md.animation.fadeIn"); - * if (fadeIn) { - * fadeIn->setTargetWidget(myWidget); - * fadeIn->start(); - * } - * @endcode - */ -class CF_UI_EXPORT Application : public QApplication { - Q_OBJECT - - public: - /** - * @brief Constructor with standard QApplication arguments. - * - * @param[in] argc Argument count (reference for Qt compatibility). - * @param[in] argv Argument values. - * - * @throws None - * @note Initializes animation factory with current theme. - * @warning None - * @since 0.1 - * @ingroup ui_widget_application_support - * - * @code - * Application app(argc, argv); - * @endcode - */ - Application(int& argc, char** argv); - - /** - * @brief Destructor. - * - * @details All owned resources are cleaned up. The animation factory - * is destroyed before theme cleanup (since it holds theme references). - * - * @since 0.1 - */ - ~Application() override; - - // Non-copyable, non-movable - Application(const Application&) = delete; - Application& operator=(const Application&) = delete; - Application(Application&&) = delete; - Application& operator=(Application&&) = delete; - - // ======================================================================== - // Global Access (Static Methods - Convenience API) - // ======================================================================== - - /** - * @brief Get the singleton Application instance. - * - * @return Pointer to Application instance, or nullptr if not created. - * - * @since 0.1 - */ - static Application* instance(); - - /** - * @brief Get the ThemeManager singleton. - * - * @details Convenience method equivalent to ThemeManager::instance(). - * - * @return Pointer to ThemeManager. - * - * @since 0.1 - */ - static core::ThemeManager* themeManager(); - - /** - * @brief Get the animation factory. - * - * @return aex::WeakPtr to the animation factory, or invalid aex::WeakPtr if - * Application instance doesn't exist. - * - * @since 0.1 - */ - static aex::WeakPtr animationFactory(); - - // ======================================================================== - // Theme Access (Token-based) - // ======================================================================== - - /** - * @brief Get a theme by token name. - * - * @param[in] themeToken Theme identifier (e.g., "theme.material.light"). - * - * @return Reference to the theme. - * @throws May throw if theme not found. - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_application_support - */ - const core::ICFTheme& theme(const std::string& themeToken) const; - - /** - * @brief Set the active theme by token. - * - * @param[in] themeToken Theme identifier. - * - * @throws None - * @note Emits themeChanged signal after successful theme change. - * @warning Theme must have been registered via ThemeManager::insert_one(). - * @since 0.1 - * @ingroup ui_widget_application_support - * - * @code - * app.setTheme("theme.material.dark"); - * @endcode - */ - void setTheme(const std::string& themeToken); - - /** - * @brief Get the current active theme. - * - * @return Reference to the current theme. - * @throws May throw if no theme is set. - * - * @since 0.1 - */ - const core::ICFTheme& currentTheme() const; - - // ======================================================================== - // Animation Access (Token-based) - // ======================================================================== - - /** - * @brief Get an animation by token name. - * - * @details Retrieves an animation from the animation factory using - * token-based lookup. Tokens are resolved through the - * token mapping system. - * - * @param[in] animationToken Animation identifier (e.g., "md.animation.fadeIn"). - * - * @return aex::WeakPtr to the animation, or invalid aex::WeakPtr if: - * - Token is not found in mapping - * - Animation type is not supported - * - Animations are globally disabled - * - * @throws None - * @note The returned aex::WeakPtr may become invalid if the factory - * is destroyed or the theme changes. - * @warning Always check validity before use. - * @since 0.1 - * @ingroup ui_widget_application_support - * - * @code - * auto anim = Application::animation("md.animation.fadeIn"); - * if (anim) { - * anim->setTargetWidget(myWidget); - * anim->start(); - * } - * @endcode - */ - aex::WeakPtr animation(const std::string& animationToken); - - /** - * @brief Set global animation enabled state. - * - * @details When disabled, animation() returns invalid aex::WeakPtr. - * Existing animations continue to run; only new creations are affected. - * - * @param[in] enabled true to enable animations, false to disable. - * - * @throws None - * @note Emits animationsEnabledChanged signal. - * @warning None - * @since 0.1 - * @ingroup ui_widget_application_support - * - * @code - * // Disable animations during heavy processing - * Application::setAnimationsEnabled(false); - * // ... do heavy work ... - * Application::setAnimationsEnabled(true); - * @endcode - */ - void setAnimationsEnabled(bool enabled); - - /** - * @brief Check if animations are globally enabled. - * - * @return true if animations are enabled, false otherwise. - * - * @since 0.1 - */ - bool animationsEnabled() const; - - // ======================================================================== - // Animation Factory Registration - // ======================================================================== - - /** - * @brief Animation factory creator function type. - * - * @details Function signature for creating animation factories dynamically. - * Used with registerAnimationFactoryType for registering new - * animation factory implementations. - * - * @since 0.1 - * @ingroup ui_widget_application_support - */ - using AnimationFactoryMaker = - std::function(const core::ICFTheme&, - QObject*)>; - - /** - * @brief Register an animation factory type for a theme prefix. - * - * @details Associates a theme prefix (e.g., "theme.material") with a - * factory creation function. When a theme matching this prefix - * is activated, the registered factory is used. - * - * @param[in] themePrefix Theme prefix to match (e.g., "theme.material"). - * This matches all themes starting with this prefix - * (e.g., "theme.material.light", "theme.material.dark"). - * @param[in] maker Factory function that creates the animation factory. - * - * @return true if registration succeeded, false if prefix already exists. - * - * @throws None - * @note The default Material factory is pre-registered. - * @warning Register with a unique prefix to avoid conflicts. - * @since 0.1 - * @ingroup ui_widget_application_support - * - * @code - * // Register a Fluent-style animation factory - * Application::registerAnimationFactoryType("theme.fluent", - * [](const ICFTheme& theme, QObject* parent) { - * return std::make_unique(theme, parent); - * }); - * - * // Now when theme "theme.fluent.dark" is active, the Fluent factory is used - * app.setTheme("theme.fluent.dark"); - * @endcode - */ - static bool registerAnimationFactoryType(const std::string& themePrefix, - AnimationFactoryMaker maker); - - /** - * @brief Unregister an animation factory type. - * - * @details Removes a previously registered animation factory for the - * given theme prefix. If a theme with this prefix is currently - * active, the factory is replaced with the default. - * - * @param[in] themePrefix Theme prefix to unregister. - * - * @throws None - * @note The default Material factory cannot be unregistered. - * @warning None - * @since 0.1 - * @ingroup ui_widget_application_support - * - * @code - * Application::unregisterAnimationFactoryType("theme.fluent"); - * @endcode - */ - static void unregisterAnimationFactoryType(const std::string& themePrefix); - - signals: - /** - * @brief Signal emitted when the theme changes. - * - * @param[in] newTheme Reference to the newly activated theme. - * - * @note Forwards ThemeManager::themeChanged signal. - * @warning None - * @since 0.1 - * @ingroup ui_widget_application_support - */ - void themeChanged(const core::ICFTheme& newTheme); - - /** - * @brief Signal emitted when animation enabled state changes. - * - * @param[in] enabled The new enabled state. - * - * @since 0.1 - * @ingroup ui_widget_application_support - */ - void animationsEnabledChanged(bool enabled); - - protected: - /** - * @brief Initialize the application. - * - * @details Called by constructor after base class setup. - * Derived classes can override this to register themes - * before calling base init(). - * - * @note The default implementation initializes animation factory. - * @warning When overriding, always call base init() at the end. - * @since 0.1 - * @ingroup ui_widget_application_support - * - * @code - * class MyApplication : public Application { - * protected: - * void init() override { - * // Register themes first - * registerMyThemes(); - * - * // Then call base init - * Application::init(); - * } - * }; - * @endcode - */ - virtual void init(); - - private: - /** - * @brief Initialize the default animation factory. - * - * @details Creates the appropriate animation factory based on current theme. - */ - void initializeAnimationFactory(); - - /** - * @brief Register an animation factory (internal). - * - * @param themePrefix Theme prefix to match. - * @param maker Factory function. - * @return true if registration succeeded. - */ - bool registerAnimationFactory(const std::string& themePrefix, AnimationFactoryMaker maker); - - /** - * @brief Unregister an animation factory (internal). - * - * @param themePrefix Theme prefix to unregister. - */ - void unregisterAnimationFactory(const std::string& themePrefix); - - /** - * @brief Create animation factory based on theme name. - * - * @details Extracts the prefix from themeName and looks up a matching - * registered factory. If no match is found, uses the default - * Material factory. - * - * @param themeName Full theme name (e.g., "theme.material.light"). - * @param theme Reference to the theme. - * @param parent QObject parent. - * @return Unique pointer to the created factory. - */ - std::unique_ptr - createAnimationFactory(const std::string& themeName, const core::ICFTheme& theme, - QObject* parent); - - /** - * @brief Handle theme change from ThemeManager. - * - * @details Updates animation factory to use new theme. The factory is - * recreated because it holds a reference to the theme. - * - * @param newTheme The newly activated theme. - */ - void onThemeManagerChanged(const core::ICFTheme& newTheme); - - /// Animation factory owned by Application (base type for polymorphism) - std::unique_ptr animationFactory_; - - /// Tracks whether init() has been called - bool initialized_; - - /// Static registry of animation factory makers (shared across all instances) - static std::unordered_map& animationFactoryRegistry(); -}; - -} // namespace cf::ui::widget::application_support diff --git a/ui/widget/material/application/material_application.cpp b/ui/widget/material/application/material_application.cpp deleted file mode 100644 index 85388d8d0..000000000 --- a/ui/widget/material/application/material_application.cpp +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @file material_application.cpp - * @brief Material Design Application class implementation - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - */ - -#include "material_application.h" -#include "components/material/cfmaterial_animation_factory.h" -#include "ui/core/material/material_factory_class.h" -#include "ui/core/token/theme_name/material_theme_name.h" - -namespace cf::ui::widget::material { - -namespace token_literals = ::cf::ui::core::token::literals; - -MaterialApplication::MaterialApplication(int& argc, char** argv) - : application_support::Application(argc, argv) { - // Call init to register themes and complete initialization - // (Derived class constructor body can safely call virtual init()) - init(); -} - -MaterialApplication::~MaterialApplication() = default; - -void MaterialApplication::init() { - // Register Material animation factory FIRST (before theme registration) - // This must be registered before calling Application::init() which - // tries to create the animation factory - Application::registerAnimationFactoryType("theme.material", [](const core::ICFTheme& theme, - QObject* parent) { - return std::make_unique(theme, nullptr, - parent); - }); - - // Register Material Design themes - auto* themeManager = Application::themeManager(); - auto installMaterialTheme = []() { return std::make_unique(); }; - themeManager->insert_one(token_literals::MATERIAL_THEME_LIGHT, installMaterialTheme); - themeManager->insert_one(token_literals::MATERIAL_THEME_DARK, installMaterialTheme); - themeManager->setThemeTo(DEFAULT_THEME, false); - - // Then call base class init to complete initialization - Application::init(); -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/application/material_application.h b/ui/widget/material/application/material_application.h deleted file mode 100644 index 92fd396b0..000000000 --- a/ui/widget/material/application/material_application.h +++ /dev/null @@ -1,98 +0,0 @@ -/** - * @file material_application.h - * @brief Material Design Application class - * - * Application class pre-configured for Material Design 3 theming. - * Automatically registers Material Design light and dark themes. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - */ - -#pragma once - -#include "token/theme_name/material_theme_name.h" -#include "ui/export.h" -#include "ui/widget/application_support/application.h" -#include - -namespace cf::ui::widget::material { - -/** - * @brief Material Design 3 Application class. - * - * @details Extends Application to provide Material Design 3 theme support. - * Automatically registers Material Design light and dark themes - * on construction. - * - * @note This class registers the material themes automatically. - * @warning Use this instead of Application for Material Design apps. - * @throws May throw if theme registration fails. - * @since 0.1 - * @ingroup ui_widget_material - * - * @code - * int main(int argc, char* argv[]) { - * using namespace cf::ui::widget::material; - * - * MaterialApplication app(argc, argv); - * app.setTheme("theme.material.light"); - * - * MyWidget w; - * w.show(); - * - * return app.exec(); - * } - * @endcode - */ -class CF_UI_EXPORT MaterialApplication : public application_support::Application { - Q_OBJECT - - public: - static constexpr const char* DEFAULT_THEME = - cf::ui::core::token::literals::MATERIAL_THEME_LIGHT; - /** - * @brief Constructor with standard QApplication arguments. - * - * @param argc Argument count (reference for Qt compatibility). - * @param argv Argument values. - * - * @throws May throw if theme registration fails. - * @note Automatically registers Material Design themes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material - * - * @code - * MaterialApplication app(argc, argv); - * @endcode - */ - MaterialApplication(int& argc, char** argv); - - /** - * @brief Destructor. - * - * @since 0.1 - */ - ~MaterialApplication() override; - - // Non-copyable, non-movable - MaterialApplication(const MaterialApplication&) = delete; - MaterialApplication& operator=(const MaterialApplication&) = delete; - MaterialApplication(MaterialApplication&&) = delete; - MaterialApplication& operator=(MaterialApplication&&) = delete; - - protected: - /** - * @brief Register Material themes and initialize. - * - * @details Overrides Application::init() to register Material Design themes - * before calling base initialization. - * - * @since 0.1 - */ - void init() override; -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/base/elevation_controller.cpp b/ui/widget/material/base/elevation_controller.cpp deleted file mode 100644 index 3304915dd..000000000 --- a/ui/widget/material/base/elevation_controller.cpp +++ /dev/null @@ -1,376 +0,0 @@ -/** - * @file elevation_controller.cpp - * @brief Material Design Elevation Controller Implementation - * - * Manages Material elevation levels and renders corresponding shadows. - * Supports animated transitions between elevation levels. - * - * Material Elevation Levels (dp → shadow params): - * Level 0: 0dp - No shadow - * Level 1: 1dp - blur=2px, offset=1px, opacity=0.15 - * Level 2: 3dp - blur=4px, offset=2px, opacity=0.20 - * Level 3: 6dp - blur=8px, offset=4px, opacity=0.25 - * Level 4: 8dp - blur=12px, offset=6px, opacity=0.30 - * Level 5: 12dp - blur=16px, offset=8px, opacity=0.35 - * - * @author Material Design Framework Team - * @date 2026-02-28 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "elevation_controller.h" -#include "base/color_helper.h" -#include "base/device_pixel.h" -#include "base/math_helper.h" -#include "components/animation.h" -#include "components/material/cfmaterial_animation_factory.h" -#include "components/material/cfmaterial_animation_strategy.h" -#include "components/timing_animation.h" - -#include -#include -#include -#include -#include // For std::tan, std::floor, std::ceil - -namespace cf::ui::widget::material::base { - -using namespace cf::ui::components::material; -using namespace cf::ui::base; -using namespace cf::ui::math; - -/** - * @brief Constructor - initializes elevation controller. - * - * @param factory aex::WeakPtr to animation factory for elevation transitions. - * @param parent QObject parent for memory management. - */ -MdElevationController::MdElevationController( - aex::WeakPtr factory, QObject* parent) - : QObject(parent), m_currentLevel(0.0f), m_targetLevel(0), m_animator(factory) {} - -/** - * @brief Destructor - cancels any running animation. - */ -MdElevationController::~MdElevationController() { - cancelCurrentAnimation(); -} - -// ============================================================================ -// Configuration -// ============================================================================ - -void MdElevationController::setElevation(int level) { - level = clamp(level, 0, 5); - m_targetLevel = level; - m_currentLevel = static_cast(level); -} - -void MdElevationController::setLightSourceAngle(float degrees) { - m_lightSourceAngle = degrees; -} - -int MdElevationController::elevation() const { - return m_targetLevel; -} - -void MdElevationController::setPressed(bool pressed) { - if (m_isPressed == pressed) { - return; - } - - m_isPressed = pressed; - - // Calculate target press offset - device::CanvasUnitHelper helper(qApp ? qApp->devicePixelRatio() : 1.0); - float targetOffset = 0.0f; - if (pressed) { - targetOffset = helper.dpToPx(static_cast(m_targetLevel) * 2.0f); - } - - // Animate to the new offset - animatePressOffsetTo(targetOffset); -} - -float MdElevationController::pressOffset() const { - // Return the animated press offset value - return m_currentPressOffset; -} - -/** - * @brief Cancels the currently running press offset animation. - * - * Disconnects all signals and stops the animation to prevent multiple - * animations from competing to update m_currentPressOffset. - */ -void MdElevationController::cancelCurrentAnimation() { - if (m_pressOffsetAnimation) { - auto* anim = m_pressOffsetAnimation.Get(); - if (anim) { - // Disconnect all signals from this animation to this object - disconnect(anim, &components::ICFAbstractAnimation::progressChanged, this, nullptr); - disconnect(anim, &components::ICFAbstractAnimation::finished, this, nullptr); - // Stop the animation - anim->stop(); - } - m_pressOffsetAnimation = nullptr; - } -} - -/** - * @brief Slot called when the press offset animation finishes. - * - * Clears the animation reference to allow new animations to start. - */ -void MdElevationController::onAnimationFinished() { - m_pressOffsetAnimation = nullptr; -} - -void MdElevationController::animatePressOffsetTo(float to) { - auto* factory = m_animator.Get(); - if (!factory || !factory->isAllEnabled()) { - // Direct set if animations disabled - m_currentPressOffset = to; - emit pressOffsetChanged(); - return; - } - - // CRITICAL FIX: Cancel any currently running animation before starting a new one. - // This prevents multiple animations from competing to update m_currentPressOffset, - // which causes visual glitches during rapid press/release events. - cancelCurrentAnimation(); - - // Start animation from current press offset value - float from = m_currentPressOffset; - - // Create custom animation descriptor with longer duration for smoother press effect - // Using "md.motion.longEnter" for slower, more noticeable animation - AnimationDescriptor desc( - "fade", // Animation type - "md.motion.longEnter", // Motion spec (longer duration for smooth press) - "opacity", // Property (we'll override with setRange) - from, // Start value - to // End value - ); - - // Create animation from descriptor - auto anim = factory->createAnimation(desc, nullptr, this); - if (!anim) { - // Fallback: direct set if animation creation fails - m_currentPressOffset = to; - emit pressOffsetChanged(); - return; - } - - // Save animation reference for cancellation - m_pressOffsetAnimation = anim; - - // Get raw pointer and set range if it's a timing animation - auto* rawAnim = anim.Get(); - auto* timingAnim = static_cast(rawAnim); - if (timingAnim) { - timingAnim->setRange(from, to); - } - - // Connect progress signal - // Note: Qt::UniqueConnection cannot be used with lambdas, but cancelCurrentAnimation() - // disconnects all signals before starting a new animation, preventing duplicates - // - // CRITICAL: progressChanged emits 0-1 normalized progress, NOT actual offset values. - // We must interpolate between from and to to get the actual offset. - connect(rawAnim, &components::ICFAbstractAnimation::progressChanged, this, - [this, from, to](float progress) { - // progress is 0-1, interpolate to get actual offset value - m_currentPressOffset = from + (to - from) * progress; - emit pressOffsetChanged(); - }); - - // Connect finished signal to clear the animation reference - connect(rawAnim, &components::ICFAbstractAnimation::finished, this, - &MdElevationController::onAnimationFinished, Qt::UniqueConnection); - - // Start animation - rawAnim->start(components::ICFAbstractAnimation::Direction::Forward); -} - -void MdElevationController::animateTo(int level, const core::MotionSpec& spec) { - level = clamp(level, 0, 5); - - auto* factory = m_animator.Get(); - if (!factory || !factory->isAllEnabled()) { - // Direct set if animations disabled - m_targetLevel = level; - m_currentLevel = static_cast(level); - return; - } - - // Create custom timing animation for elevation transition - // Note: This requires the factory to support custom animation creation - // For now, use direct set with smooth transition - int oldLevel = m_targetLevel; - m_targetLevel = level; - - // Simple interpolation - in production, use proper animation - // The actual animation would be handled by the widget's paint loop - // reading m_currentLevel - Q_UNUSED(spec); - Q_UNUSED(oldLevel); -} - -// ============================================================================ -// Shadow Calculation -// ============================================================================ - -/** - * @brief Calculates shadow parameters for a given elevation level. - * - * Uses linear interpolation between predefined levels for smooth - * elevation transitions. - * - * @param level Elevation level (can be fractional for animation). - * @return Shadow parameters for rendering. - */ -MdElevationController::ShadowParams MdElevationController::paramsForLevel(float level) const { - // Predefined parameters for integer levels - struct LevelParams { - float blurRadius; - float offsetY; // Vertical offset (positive =向下) - float opacity; - }; - - static constexpr LevelParams levelParams[] = { - {0.0f, 0.0f, 0.00f}, // Level 0 - {2.0f, 1.0f, 0.15f}, // Level 1 - {4.0f, 2.0f, 0.20f}, // Level 2 - {8.0f, 4.0f, 0.25f}, // Level 3 - {12.0f, 6.0f, 0.30f}, // Level 4 - {16.0f, 8.0f, 0.35f} // Level 5 - }; - - // Clamp level to valid range - level = clamp(level, 0.0f, 5.0f); - - // Interpolate between levels - int lowerLevel = static_cast(std::floor(level)); - int upperLevel = static_cast(std::ceil(level)); - float t = level - lowerLevel; - - // Handle edge case where level is exactly at an integer - if (upperLevel == lowerLevel) { - upperLevel = std::min(5, lowerLevel + 1); - t = 0.0f; - } - - const LevelParams& lower = levelParams[lowerLevel]; - const LevelParams& upper = levelParams[upperLevel]; - - ShadowParams result; - result.blurRadius = lerp(lower.blurRadius, upper.blurRadius, t); - result.offsetY = lerp(lower.offsetY, upper.offsetY, t); - result.opacity = lerp(lower.opacity, upper.opacity, t); - - // Calculate horizontal offset based on light source angle - // angle in degrees: 正值 = 光源从左侧, 负值 = 光源从右侧 - // Material Design 默认 15 度 = 光源从左上方照射 - float angleRad = m_lightSourceAngle * 3.14159265f / 180.0f; - // offsetX与offsetY成比例,模拟光线方向 - // 光源从左上方来,阴影向右下方投射(offsetX 为正) - result.offsetX = result.offsetY * std::tan(angleRad); - - // 按压时阴影缩小并靠近 - if (m_isPressed) { - result.offsetX *= 0.5f; - result.offsetY *= 0.5f; - result.blurRadius *= 0.7f; - } - - return result; -} - -// ============================================================================ -// Painting -// ============================================================================ - -/** - * @brief Paints shadow for the given shape. - * - * Uses layered shadow rendering for optimal performance. - * Each layer is drawn with decreasing offset and blur. - * - * @param painter QPainter instance (must be active). - * @param shape Path to draw shadow for. - */ -void MdElevationController::paintShadow(QPainter* painter, const QPainterPath& shape) { - if (!painter || m_currentLevel <= 0.0f) { - return; - } - - ShadowParams params = paramsForLevel(m_currentLevel); - - // Convert dp to pixels - use actual device pixel ratio from QApplication - device::CanvasUnitHelper helper(qApp ? qApp->devicePixelRatio() : 1.0); - float blurRadius = helper.dpToPx(params.blurRadius); - float offsetX = helper.dpToPx(params.offsetX); - float offsetY = helper.dpToPx(params.offsetY); - - painter->save(); - - // Get shape bounding rect for shadow rendering - QRectF bounds = shape.boundingRect(); - - // Create shadow color (black with opacity) - QColor shadowColor(0, 0, 0, static_cast(params.opacity * 255)); - - // Draw shadow layers with light source offset - // Main shadow layer - QPainterPath shadowPath = shape; - shadowPath.translate(offsetX, offsetY); - - painter->setPen(Qt::NoPen); - painter->setBrush(shadowColor); - painter->drawPath(shadowPath); - - // Additional layers for blur approximation - int layers = static_cast(std::ceil(blurRadius / 2.0f)); - for (int i = 1; i <= layers && i <= 3; ++i) { - float layerOpacity = params.opacity * (1.0f - static_cast(i) / (layers + 1)); - float layerOffsetScale = 1.0f + (blurRadius * i / layers) * 0.3f / offsetY; - float layerOffsetX = offsetX * layerOffsetScale; - float layerOffsetY = offsetY * layerOffsetScale; - - QColor layerColor(0, 0, 0, static_cast(layerOpacity * 255)); - QPainterPath layerPath = shape; - layerPath.translate(layerOffsetX, layerOffsetY); - - painter->setBrush(layerColor); - painter->drawPath(layerPath); - } - - painter->restore(); -} - -/** - * @brief Calculates tonal overlay color for dark theme elevation. - * - * In dark theme, elevation is indicated by adding tonal color - * from the primary palette to the surface. - * - * @param surface Base surface color. - * @param primary Primary color for tonal overlay. - * @return Tonal overlay color. - */ -CFColor MdElevationController::tonalOverlay(CFColor surface, CFColor primary) const { - if (m_currentLevel <= 0.0f) { - return surface; - } - - // Calculate tonal amount based on elevation - float tonalAmount = m_currentLevel / 10.0f; // 0.0 to 0.5 - - // Blend surface with primary - return blend(surface, primary, tonalAmount); -} - -} // namespace cf::ui::widget::material::base diff --git a/ui/widget/material/base/elevation_controller.h b/ui/widget/material/base/elevation_controller.h deleted file mode 100644 index f7aa47136..000000000 --- a/ui/widget/material/base/elevation_controller.h +++ /dev/null @@ -1,275 +0,0 @@ -/** - * @file ui/widget/material/base/elevation_controller.h - * @brief Material Design elevation controller for shadow rendering. - * - * Manages elevation levels and shadow rendering for Material Design widgets. - * Provides animated transitions between elevation levels and tonal overlay - * colors for dark theme support. - * - * @author N/A - * @date N/A - * @version N/A - * @since N/A - * @ingroup ui_widget_material_base - */ -#pragma once -#include "color.h" -#include "components/material/cfmaterial_animation_factory.h" -#include "core/material/cfmaterial_motion.h" -#include - -class QPainter; -class QPainterPath; -class QApplication; - -namespace cf::ui::widget::material::base { -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design elevation controller. - * - * @details Manages elevation levels and shadow rendering for Material Design - * widgets. Provides animated transitions between elevation levels - * and tonal overlay colors for dark theme support. - * - * @since N/A - * @ingroup ui_widget_material_base - */ -class CF_UI_EXPORT MdElevationController : public QObject { - Q_OBJECT - public: - /** - * @brief Constructor with animation factory. - * - * @param[in] factory aex::WeakPtr to the animation factory. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - explicit MdElevationController( - aex::WeakPtr factory, - QObject* parent = nullptr); - - /** - * @brief Sets the base elevation level. - * - * @param[in] level Elevation level (0-5). - * - * @throws None - * @note Material Design defines 6 standard levels (0-5). - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void setElevation(int level); - - /** - * @brief Gets the current elevation level. - * - * @return Current elevation level (0-5). - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - int elevation() const; - - /** - * @brief Sets the light source angle. - * - * @param[in] degrees Angle in degrees (positive = right, negative = left). - * - * @throws None - * @note Material Design default is approximately 15 degrees - * (light from top-left). - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void setLightSourceAngle(float degrees); - - /** - * @brief Gets the light source angle. - * - * @return Light source angle in degrees. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - float lightSourceAngle() const { return m_lightSourceAngle; } - - /** - * @brief Animates to a new elevation level. - * - * @param[in] level Target elevation level. - * @param[in] spec Motion specification for the animation. - * - * @throws None - * @note Used for dynamic elevation changes like FAB press. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void animateTo(int level, const core::MotionSpec& spec); - - /** - * @brief Paints the shadow for a given shape. - * - * @param[in] painter QPainter to render with. - * @param[in] shape Shape path to render shadow for. - * - * @throws None - * @note Call in paintEvent before drawing the background. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void paintShadow(QPainter* painter, const QPainterPath& shape); - - /** - * @brief Gets the tonal overlay color for dark theme. - * - * @param[in] surface Base surface color. - * @param[in] primary Primary color for overlay calculation. - * - * @return Tonal overlay color. - * - * @throws None - * @note Used in dark theme to indicate elevation level. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - CFColor tonalOverlay(CFColor surface, CFColor primary) const; - - /** - * @brief Sets the pressed state. - * - * @param[in] pressed true to set pressed state, false otherwise. - * - * @throws None - * @note Pressed state increases elevation visually. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void setPressed(bool pressed); - - /** - * @brief Animates the press offset to a target value. - * - * @param[in] to Target press offset value. - * - * @throws None - * @note Uses the animation factory for smooth transition. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void animatePressOffsetTo(float to); - - /** - * @brief Gets the pressed state. - * - * @return true if pressed, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - bool isPressed() const { return m_isPressed; } - - /** - * @brief Gets the press offset for the current elevation. - * - * @return Press offset in pixels. - * - * @throws None - * @note Returns the animated press offset value. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - float pressOffset() const; - - /** - * @brief Destructor. - * - * @details Cancels any running animation before destruction. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - ~MdElevationController(); - - signals: - /** - * @brief Emitted when the press offset changes during animation. - * - * @throws None - * @note Connect to this signal to trigger repaint when - * press offset animates. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void pressOffsetChanged(); - - private: - float m_currentLevel = 0.0f; - int m_targetLevel = 0; - float m_lightSourceAngle = 15.0f; - bool m_isPressed = false; - float m_currentPressOffset = 0.0f; ///< Animated press offset value - aex::WeakPtr m_animator; - - /// Reference to the currently running press offset animation - aex::WeakPtr m_pressOffsetAnimation; - - /** - * @brief Cancels the currently running press offset animation. - * - * @throws None - * @note Disconnects all signals and stops the animation. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void cancelCurrentAnimation(); - - /** - * @brief Slot called when the press offset animation finishes. - * - * @details Clears the animation reference. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void onAnimationFinished(); - - struct ShadowParams { - float blurRadius; - float offsetX; - float offsetY; - float opacity; - }; - ShadowParams paramsForLevel(float level) const; -}; -} // namespace cf::ui::widget::material::base diff --git a/ui/widget/material/base/focus_ring.cpp b/ui/widget/material/base/focus_ring.cpp deleted file mode 100644 index 694dd46a4..000000000 --- a/ui/widget/material/base/focus_ring.cpp +++ /dev/null @@ -1,158 +0,0 @@ -/** - * @file focus_ring.cpp - * @brief Material Design Focus Indicator Implementation - * - * Draws focus ring following Material Design 3 specifications. - * Ring width: 3dp, inset: 3dp from widget boundary. - * - * @author Material Design Framework Team - * @date 2026-02-28 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "focus_ring.h" -#include "base/device_pixel.h" -#include "components/material/cfmaterial_animation_factory.h" -#include "components/timing_animation.h" - -#include -#include -#include - -namespace cf::ui::widget::material::base { - -using namespace cf::ui::components::material; -using namespace cf::ui::components; -using namespace cf::ui::base; - -/** - * @brief Constructor - initializes focus indicator. - * - * @param factory aex::WeakPtr to animation factory for fade animation. - * @param parent QObject parent for memory management. - */ -MdFocusIndicator::MdFocusIndicator( - aex::WeakPtr factory, QObject* parent) - : QObject(parent), m_progress(0.0f), m_animator(factory) {} - -// ============================================================================ -// Event Handlers -// ============================================================================ - -void MdFocusIndicator::onFocusIn() { - auto* factory = m_animator.Get(); - if (!factory || !factory->isAllEnabled()) { - // Direct set if animations disabled - m_progress = 1.0f; - return; - } - - // Get fade animation for ring appearance - auto anim = factory->getAnimation("md.animation.fadeIn"); - if (!anim) { - m_progress = 1.0f; - return; - } - - // Get raw pointer and set range if it's a timing animation - auto* rawAnim = anim.Get(); - auto* timingAnim = static_cast(rawAnim); - if (timingAnim) { - timingAnim->setRange(0.0f, 1.0f); - } - - connect(rawAnim, &components::ICFAbstractAnimation::progressChanged, this, - [this](float progress) { m_progress = progress; }); - - rawAnim->start(components::ICFAbstractAnimation::Direction::Forward); -} - -void MdFocusIndicator::onFocusOut() { - auto* factory = m_animator.Get(); - if (!factory || !factory->isAllEnabled()) { - // Direct set if animations disabled - m_progress = 0.0f; - return; - } - - // Get fade animation for ring disappearance - auto anim = factory->getAnimation("md.animation.fadeOut"); - if (!anim) { - m_progress = 0.0f; - return; - } - - // Get raw pointer and set range if it's a timing animation - auto* rawAnim = anim.Get(); - auto* timingAnim = static_cast(rawAnim); - if (timingAnim) { - timingAnim->setRange(1.0f, 0.0f); - } - - connect(rawAnim, &components::ICFAbstractAnimation::progressChanged, this, - [this](float progress) { m_progress = progress; }); - - rawAnim->start(components::ICFAbstractAnimation::Direction::Forward); -} - -// ============================================================================ -// Painting -// ============================================================================ - -/** - * @brief Paints the focus ring. - * - * The ring is drawn outside the widget shape with: - * - Width: 3dp - * - Inset from boundary: 3dp - * - Opacity based on animation progress - * - * @param painter QPainter instance (must be active). - * @param shape Widget shape path for focus ring outline. - * @param indicatorColor Ring color (typically onSurface or similar). - */ -void MdFocusIndicator::paint(QPainter* painter, const QPainterPath& shape, - const CFColor& indicatorColor) { - if (m_progress <= 0.0f || !painter) { - return; - } - - // Convert dp to pixels - // Note: Using device pixel ratio 1.0 for now. In production, get from QApplication. - device::CanvasUnitHelper helper(1.0); - float ringWidth = helper.dpToPx(3.0f); // 3dp ring width - float inset = helper.dpToPx(3.0f); // 3dp inset - - painter->save(); - - // Create stroker for the ring - QPainterPathStroker stroker; - stroker.setWidth(ringWidth); - stroker.setCapStyle(Qt::SquareCap); - stroker.setJoinStyle(Qt::MiterJoin); - - // Create offset path (inset from widget boundary) - QPainterPathStroker insetStroker; - insetStroker.setWidth(inset * 2); // Inset on both sides - QPainterPath innerShape = insetStroker.createStroke(shape); - // Subtract to get the inset outline - QPainterPath outlinePath = shape.subtracted(innerShape); - - // Create the ring path - QPainterPath ringPath = stroker.createStroke(outlinePath); - - // Apply opacity based on animation progress - QColor color = indicatorColor.native_color(); - color.setAlphaF(color.alphaF() * m_progress); - - // Draw the ring - painter->setPen(Qt::NoPen); - painter->setBrush(color); - painter->drawPath(ringPath); - - painter->restore(); -} - -} // namespace cf::ui::widget::material::base diff --git a/ui/widget/material/base/focus_ring.h b/ui/widget/material/base/focus_ring.h deleted file mode 100644 index 0752a3f3c..000000000 --- a/ui/widget/material/base/focus_ring.h +++ /dev/null @@ -1,93 +0,0 @@ -/** - * @file ui/widget/material/base/focus_ring.h - * @brief Material Design focus indicator for keyboard navigation. - * - * Manages the focus ring indicator for Material Design widgets. - * Provides animated entrance/exit when widgets gain or lose focus. - * - * @author N/A - * @date N/A - * @version N/A - * @since N/A - * @ingroup ui_widget_material_base - */ -#pragma once -#include "color.h" -#include "components/material/cfmaterial_animation_factory.h" -#include -class QPainter; -class QPainterPath; - -namespace cf::ui::widget::material::base { - -/** - * @brief Material Design focus indicator. - * - * @details Manages the focus ring indicator for keyboard navigation. - * Provides animated entrance/exit when widgets gain or lose focus. - * - * @since N/A - * @ingroup ui_widget_material_base - */ -class CF_UI_EXPORT MdFocusIndicator : public QObject { - Q_OBJECT - public: - /** - * @brief Constructor with animation factory. - * - * @param[in] factory aex::WeakPtr to the animation factory. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - explicit MdFocusIndicator( - aex::WeakPtr factory, - QObject* parent = nullptr); - - /** - * @brief Handles focus in event. - * - * @throws None - * @note Starts the focus entrance animation. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void onFocusIn(); - - /** - * @brief Handles focus out event. - * - * @throws None - * @note Starts the focus exit animation. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void onFocusOut(); - - /** - * @brief Paints the focus indicator. - * - * @param[in] painter QPainter to render with. - * @param[in] shape Shape path to render focus ring for. - * @param[in] indicatorColor Color for the focus indicator. - * - * @throws None - * @note Call in paintEvent at the top layer. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void paint(QPainter* painter, const QPainterPath& shape, - const cf::ui::base::CFColor& indicatorColor); - - private: - float m_progress = 0.0f; - aex::WeakPtr m_animator; -}; -} // namespace cf::ui::widget::material::base diff --git a/ui/widget/material/base/material_widget_base.cpp b/ui/widget/material/base/material_widget_base.cpp deleted file mode 100644 index 11e991274..000000000 --- a/ui/widget/material/base/material_widget_base.cpp +++ /dev/null @@ -1,95 +0,0 @@ -/** - * @file material_widget_base.cpp - * @brief Common Material Design widget behavior composition helper. - * - * @ingroup ui_widget_material_base - */ - -#include "material_widget_base.h" - -#include "application_support/application.h" - -namespace cf::ui::widget::material::base { - -MaterialWidgetBase::MaterialWidgetBase(QWidget* owner, const Config& config) : m_owner(owner) { - auto factory = aex::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - m_stateMachine = new StateMachine(factory, owner); - - if (config.useRipple) { - m_ripple = new RippleHelper(factory, owner); - m_ripple->setMode(config.rippleMode); - connect(m_ripple, &RippleHelper::repaintNeeded, owner, - static_cast(&QWidget::update)); - } - - if (config.useElevation) { - m_elevation = new MdElevationController(factory, owner); - m_elevation->setElevation(config.initialElevation); - connect(m_elevation, &MdElevationController::pressOffsetChanged, owner, - static_cast(&QWidget::update)); - } - - if (config.useFocusIndicator) { - m_focusIndicator = new MdFocusIndicator(factory, owner); - } - - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, owner, - static_cast(&QWidget::update)); -} - -void MaterialWidgetBase::onEnterEvent() { - m_stateMachine->onHoverEnter(); - m_owner->update(); -} - -void MaterialWidgetBase::onLeaveEvent() { - m_stateMachine->onHoverLeave(); - if (m_ripple) - m_ripple->onCancel(); - m_owner->update(); -} - -void MaterialWidgetBase::onMousePress(const QPoint& pos, const QRectF& bounds) { - m_stateMachine->onPress(pos); - if (m_ripple) - m_ripple->onPress(pos, bounds); - if (m_elevation) - m_elevation->setPressed(true); - m_owner->update(); -} - -void MaterialWidgetBase::onMouseRelease() { - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); - if (m_elevation) - m_elevation->setPressed(false); - m_owner->update(); -} - -void MaterialWidgetBase::onFocusIn() { - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); - m_owner->update(); -} - -void MaterialWidgetBase::onFocusOut() { - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); - m_owner->update(); -} - -void MaterialWidgetBase::onEnabledChange(bool enabled) { - if (enabled) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - m_owner->update(); -} - -} // namespace cf::ui::widget::material::base diff --git a/ui/widget/material/base/material_widget_base.h b/ui/widget/material/base/material_widget_base.h deleted file mode 100644 index 61f40fdf4..000000000 --- a/ui/widget/material/base/material_widget_base.h +++ /dev/null @@ -1,214 +0,0 @@ -/** - * @file material_widget_base.h - * @brief Common Material Design widget behavior composition helper. - * - * Encapsulates the shared initialization, signal connections, and event - * forwarding used by all interactive Material Design 3 widgets. - * - * @ingroup ui_widget_material_base - */ - -#pragma once - -#include "aex/weak_ptr/weak_ptr.h" -#include "components/material/cfmaterial_animation_factory.h" -#include "export.h" -#include "focus_ring.h" -#include "ripple_helper.h" -#include "state_machine.h" -#include "widget/material/base/elevation_controller.h" - -#include -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material::base { - -/** - * @brief Composition helper for Material Design widget behavior. - * - * @details Encapsulates the common initialization, signal connections, - * and event forwarding shared by all interactive Material Design 3 - * widgets. Uses a Config struct to control which helper components - * are created. - * - * This is a "has-a" composition, not inheritance, because widgets - * inherit from different Qt base classes (QPushButton, QCheckBox, - * QSlider, etc.). - * - * @note The owner QWidget must remain valid for the lifetime of this object. - * @warning Do not use with non-interactive widgets (Label, Separator). - * @since 0.1 - * @ingroup ui_widget_material_base - * - * @code - * // In widget constructor: - * m_material(this, MaterialWidgetBase::Config{ - * .useElevation = true, - * .initialElevation = 2 - * }); - * - * // In event handlers: - * void MyWidget::enterEvent(QEnterEvent* event) { - * QPushButton::enterEvent(event); - * m_material.onEnterEvent(); - * } - * @endcode - */ - -/** - * @brief Configuration for MaterialWidgetBase helper creation. - * - * @since 0.1 - * @ingroup ui_widget_material_base - */ -struct MaterialWidgetBaseConfig { - bool useRipple = true; ///< Create RippleHelper. - bool useElevation = false; ///< Create MdElevationController. - bool useFocusIndicator = true; ///< Create MdFocusIndicator. - RippleHelper::Mode rippleMode = RippleHelper::Mode::Bounded; ///< Ripple mode. - int initialElevation = 0; ///< Initial elevation level. -}; - -class CF_UI_EXPORT MaterialWidgetBase : public QObject { - Q_OBJECT - public: - using Config = MaterialWidgetBaseConfig; - - /** - * @brief Construct and initialize helper components. - * - * @param[in] owner The parent widget (must not be null). - * @param[in] config Configuration controlling which helpers to create. - * - * @throws None - * @note Connects repaint signals to owner's update() slot. - * @warning owner must remain valid for the lifetime of this object. - * @since 0.1 - * @ingroup ui_widget_material_base - */ - MaterialWidgetBase(QWidget* owner, const Config& config = Config{}); - - ~MaterialWidgetBase() override = default; - - // Non-copyable, movable - MaterialWidgetBase(const MaterialWidgetBase&) = delete; - MaterialWidgetBase& operator=(const MaterialWidgetBase&) = delete; - - // --- Event forwarding --- - - /** - * @brief Forward hover enter to helpers and trigger repaint. - * - * @since 0.1 - * @ingroup ui_widget_material_base - */ - void onEnterEvent(); - - /** - * @brief Forward hover leave to helpers and trigger repaint. - * - * @since 0.1 - * @ingroup ui_widget_material_base - */ - void onLeaveEvent(); - - /** - * @brief Forward mouse press to helpers and trigger repaint. - * - * @param[in] pos Mouse position relative to the widget. - * @param[in] bounds Widget bounds for ripple calculation. - * - * @since 0.1 - * @ingroup ui_widget_material_base - */ - void onMousePress(const QPoint& pos, const QRectF& bounds); - - /** - * @brief Forward mouse release to helpers and trigger repaint. - * - * @since 0.1 - * @ingroup ui_widget_material_base - */ - void onMouseRelease(); - - /** - * @brief Forward focus-in to helpers and trigger repaint. - * - * @since 0.1 - * @ingroup ui_widget_material_base - */ - void onFocusIn(); - - /** - * @brief Forward focus-out to helpers and trigger repaint. - * - * @since 0.1 - * @ingroup ui_widget_material_base - */ - void onFocusOut(); - - /** - * @brief Forward enabled-state change to helpers and trigger repaint. - * - * @param[in] enabled Whether the widget is now enabled. - * - * @since 0.1 - * @ingroup ui_widget_material_base - */ - void onEnabledChange(bool enabled); - - // --- Accessors --- - - /** - * @brief Get the state machine helper. - * - * @return Pointer to StateMachine, or nullptr if not created. - * - * @since 0.1 - * @ingroup ui_widget_material_base - */ - StateMachine* stateMachine() const { return m_stateMachine; } - - /** - * @brief Get the ripple helper. - * - * @return Pointer to RippleHelper, or nullptr if not created. - * - * @since 0.1 - * @ingroup ui_widget_material_base - */ - RippleHelper* ripple() const { return m_ripple; } - - /** - * @brief Get the elevation controller. - * - * @return Pointer to MdElevationController, or nullptr if not created. - * - * @since 0.1 - * @ingroup ui_widget_material_base - */ - MdElevationController* elevation() const { return m_elevation; } - - /** - * @brief Get the focus indicator. - * - * @return Pointer to MdFocusIndicator, or nullptr if not created. - * - * @since 0.1 - * @ingroup ui_widget_material_base - */ - MdFocusIndicator* focusIndicator() const { return m_focusIndicator; } - - private: - QWidget* m_owner; - StateMachine* m_stateMachine = nullptr; - RippleHelper* m_ripple = nullptr; - MdElevationController* m_elevation = nullptr; - MdFocusIndicator* m_focusIndicator = nullptr; -}; - -} // namespace cf::ui::widget::material::base diff --git a/ui/widget/material/base/painter_layer.cpp b/ui/widget/material/base/painter_layer.cpp deleted file mode 100644 index 854bdc488..000000000 --- a/ui/widget/material/base/painter_layer.cpp +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @file painter_layer.cpp - * @brief Material Design Painter Layer Implementation - * - * Provides basic color and opacity overlay rendering for state layers. - * This is the simplest component in Layer 4, serving as the foundation - * for more complex behavior components. - * - * @author Material Design Framework Team - * @date 2026-02-28 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "painter_layer.h" -#include -#include - -namespace cf::ui::widget::material::base { - -/** - * @brief Constructor - initializes default values. - * - * @param parent QObject parent for memory management. - */ -PainterLayer::PainterLayer(QObject* parent) - : QObject(parent), cached_color_(Qt::black), opacity_(1.0f) {} - -/** - * @brief Paints the layer with current color and opacity. - * - * This method fills the clip path with the cached color, - * applying the current opacity value. Used in paintEvent - * of Material widgets. - * - * @param painter QPainter instance (must be active). - * @param clipPath Path to clip the drawing area. - */ -void PainterLayer::paint(QPainter* painter, const QPainterPath& clipPath) { - if (!painter || opacity_ <= 0.0f) { - return; - } - - painter->save(); - painter->setClipPath(clipPath); - - // Convert CFColor to QColor and apply opacity - QColor color = cached_color_.native_color(); - color.setAlphaF(color.alphaF() * opacity_); - - painter->fillPath(clipPath, color); - painter->restore(); -} - -} // namespace cf::ui::widget::material::base diff --git a/ui/widget/material/base/painter_layer.h b/ui/widget/material/base/painter_layer.h deleted file mode 100644 index 791a32137..000000000 --- a/ui/widget/material/base/painter_layer.h +++ /dev/null @@ -1,94 +0,0 @@ -/** - * @file ui/widget/material/base/painter_layer.h - * @brief Painter layer for Material Design widget rendering. - * - * Provides a layer abstraction for painting operations with color - * and opacity control. Used for layered rendering in Material Design widgets. - * - * @author N/A - * @date N/A - * @version N/A - * @since N/A - * @ingroup ui_widget_material_base - */ -#pragma once -#include "color.h" -#include "export.h" -#include -class QPainter; -class QPainterPath; - -namespace cf::ui::widget::material::base { - -/** - * @brief Painter layer for Material Design widget rendering. - * - * @details Provides a layer abstraction for painting operations with color - * and opacity control. Used for layered rendering in Material Design - * widgets. - * - * @since N/A - * @ingroup ui_widget_material_base - */ -class CF_UI_EXPORT PainterLayer : public QObject { - Q_OBJECT - public: - /** - * @brief Constructor with parent. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - explicit PainterLayer(QObject* parent); - - /** - * @brief Sets the layer color. - * - * @param[in] color Color to use for this layer. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void setColor(const cf::ui::base::CFColor& color) { cached_color_ = color; } - - /** - * @brief Sets the layer opacity. - * - * @param[in] opacity Opacity value (0.0 = transparent, 1.0 = opaque). - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void setOpacity(float opacity) { opacity_ = opacity; } - - /** - * @brief Paints the layer. - * - * @param[in] painter QPainter to render with. - * @param[in] clipPath Clipping path for the layer. - * - * @throws None - * @note Call in paintEvent. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void paint(QPainter* painter, const QPainterPath& clipPath); - - protected: - cf::ui::base::CFColor cached_color_; - float opacity_; -}; - -} // namespace cf::ui::widget::material::base diff --git a/ui/widget/material/base/ripple_helper.cpp b/ui/widget/material/base/ripple_helper.cpp deleted file mode 100644 index 972b68ba5..000000000 --- a/ui/widget/material/base/ripple_helper.cpp +++ /dev/null @@ -1,247 +0,0 @@ -/** - * @file ripple_helper.cpp - * @brief Material Design Ripple Helper Implementation - * - * Manages ripple effect lifecycle and rendering following Material Design 3. - * Ripples expand from touch point with fade-out on release. - * - * @author Material Design Framework Team - * @date 2026-02-28 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "ripple_helper.h" -#include "components/material/cfmaterial_animation_factory.h" -#include "components/timing_animation.h" - -#include -#include -#include - -namespace cf::ui::widget::material::base { - -using namespace cf::ui::components::material; -using namespace cf::ui::components; - -/** - * @brief Constructor - initializes ripple controller. - * - * @param factory aex::WeakPtr to animation factory for ripple animations. - * @param parent QObject parent for memory management. - */ -RippleHelper::RippleHelper(aex::WeakPtr factory, - QObject* parent) - : QObject(parent), m_mode(Mode::Bounded), m_color(Qt::black), m_animator(factory) {} - -// ============================================================================ -// Configuration -// ============================================================================ - -void RippleHelper::setMode(Mode mode) { - m_mode = mode; -} - -void RippleHelper::setColor(const cf::ui::base::CFColor& color) { - m_color = color; -} - -// ============================================================================ -// Event Handlers -// ============================================================================ - -/** - * @brief Calculates maximum ripple radius. - * - * Uses diagonal length from touch point to farthest corner. - * - * @param rect Widget bounding rectangle. - * @param center Touch point (ripple center). - * @return Maximum radius in pixels. - */ -float RippleHelper::maxRadius(const QRectF& rect, const QPointF& center) const { - // Calculate distance to each corner - QPointF topLeft = rect.topLeft(); - QPointF topRight = rect.topRight(); - QPointF bottomLeft = rect.bottomLeft(); - QPointF bottomRight = rect.bottomRight(); - - float d1 = std::hypot(center.x() - topLeft.x(), center.y() - topLeft.y()); - float d2 = std::hypot(center.x() - topRight.x(), center.y() - topRight.y()); - float d3 = std::hypot(center.x() - bottomLeft.x(), center.y() - bottomLeft.y()); - float d4 = std::hypot(center.x() - bottomRight.x(), center.y() - bottomRight.y()); - - return std::max({d1, d2, d3, d4}); -} - -void RippleHelper::onPress(const QPoint& pos, const QRectF& widgetRect) { - // Performance mode check - auto* factory = m_animator.Get(); - if (!factory || !factory->isAllEnabled()) { - return; // Skip ripples if animations disabled - } - - // Cancel any existing ripples before starting a new one - // This prevents visual artifacts from rapid clicks - onCancel(); - - // Create new ripple - MdRipple ripple; - ripple.center = QPointF(pos); - ripple.radius = 0.0f; - ripple.opacity = 1.0f; - ripple.releasing = false; - - // Calculate final radius - ripple.maxRadius = maxRadius(widgetRect, ripple.center); - - // Store ripple - m_ripples.append(ripple); - - // Start expand animation - auto anim = factory->getAnimation("md.animation.rippleExpand"); - if (anim) { - // Use index to track which ripple this animation controls - int index = m_ripples.size() - 1; - - // Get raw pointer and set range if it's a timing animation - auto* rawAnim = anim.Get(); - auto* timingAnim = static_cast(rawAnim); - if (timingAnim) { - timingAnim->setRange(0.0f, 1.0f); - } - - connect(rawAnim, &components::ICFAbstractAnimation::progressChanged, this, - [this, index](float progress) { - if (index >= 0 && index < m_ripples.size()) { - m_ripples[index].radius = m_ripples[index].maxRadius * progress; - emit repaintNeeded(); - } - }); - - connect(rawAnim, &components::ICFAbstractAnimation::finished, this, [this, index]() { - // Animation done but ripple may still be in releasing state - emit repaintNeeded(); - }); - - rawAnim->start(components::ICFAbstractAnimation::Direction::Forward); - } - - emit repaintNeeded(); -} - -void RippleHelper::onRelease() { - // Trigger fade-out for all non-releasing ripples - auto* factory = m_animator.Get(); - if (!factory) - return; - - for (int i = 0; i < m_ripples.size(); ++i) { - if (!m_ripples[i].releasing) { - m_ripples[i].releasing = true; - - // Start fade animation - auto anim = factory->getAnimation("md.animation.rippleFade"); - if (anim) { - int index = i; // Capture for lambda - - // Get raw pointer and set range if it's a timing animation - auto* rawAnim = anim.Get(); - auto* timingAnim = static_cast(rawAnim); - if (timingAnim) { - timingAnim->setRange(1.0f, 0.0f); - } - - connect(rawAnim, &components::ICFAbstractAnimation::progressChanged, this, - [this, index](float progress) { - if (index >= 0 && index < m_ripples.size()) { - m_ripples[index].opacity = progress; - emit repaintNeeded(); - } - }); - - connect(rawAnim, &components::ICFAbstractAnimation::finished, this, - [this, index]() { - // Remove finished ripple - if (index >= 0 && index < m_ripples.size()) { - m_ripples.removeAt(index); - emit repaintNeeded(); - } - }); - - rawAnim->start(components::ICFAbstractAnimation::Direction::Forward); - } - } - } - - emit repaintNeeded(); -} - -void RippleHelper::onCancel() { - // Remove all active ripples immediately - if (!m_ripples.isEmpty()) { - m_ripples.clear(); - emit repaintNeeded(); - } -} - -// ============================================================================ -// Painting -// ============================================================================ - -namespace { -// Material Design 3 ripple fixed opacity (12% as per MD3 specs) -constexpr float RIPPLE_FIXED_OPACITY = 0.12f; -} // namespace - -void RippleHelper::paint(QPainter* painter, const QPainterPath& clipPath) { - if (m_ripples.isEmpty() || !painter) { - return; - } - - painter->save(); - - // Apply clipping for bounded mode - if (m_mode == Mode::Bounded) { - painter->setClipPath(clipPath); - } - - // Draw each ripple - for (const auto& ripple : m_ripples) { - if (ripple.radius <= 0.0f || ripple.opacity <= 0.0f) { - continue; - } - - // Create radial gradient for smooth ripple edge - QRadialGradient gradient(ripple.center, ripple.radius); - - QColor color = m_color.native_color(); - // Apply fixed ripple opacity (Material Design 3 spec) - // The ripple.opacity controls the fade-out animation, while RIPPLE_FIXED_OPACITY - // ensures the ripple always has the correct visual strength - color.setAlphaF(RIPPLE_FIXED_OPACITY * ripple.opacity); - - // Center is solid, edge fades slightly - gradient.setColorAt(0.0f, color); - gradient.setColorAt(0.7f, color); - gradient.setColorAt(1.0f, QColor(color.red(), color.green(), color.blue(), 0)); - - painter->setBrush(QBrush(gradient)); - painter->setPen(Qt::NoPen); - - painter->drawEllipse(ripple.center, ripple.radius, ripple.radius); - } - - painter->restore(); -} - -// ============================================================================ -// State Query -// ============================================================================ - -bool RippleHelper::hasActiveRipple() const { - return !m_ripples.isEmpty(); -} - -} // namespace cf::ui::widget::material::base diff --git a/ui/widget/material/base/ripple_helper.h b/ui/widget/material/base/ripple_helper.h deleted file mode 100644 index 6fb374bc1..000000000 --- a/ui/widget/material/base/ripple_helper.h +++ /dev/null @@ -1,185 +0,0 @@ -/** - * @file ui/widget/material/base/ripple_helper.h - * @brief Material Design ripple effect helper. - * - * Manages ripple effects for Material Design widgets. Provides - * animated ripple propagation on press/release interactions with - * bounded and unbounded modes. - * - * @author N/A - * @date N/A - * @version N/A - * @since N/A - * @ingroup ui_widget_material_base - */ -#pragma once -#include "color.h" -#include "components/material/cfmaterial_animation_factory.h" -#include -#include -class QPainterPath; - -namespace cf::ui::widget::material::base { - -/** - * @brief Material Design ripple data structure. - * - * @since N/A - * @ingroup ui_widget_material_base - */ -struct MdRipple { - QPointF center; - float radius; - float opacity; - bool releasing; - float maxRadius; -}; - -/** - * @brief Material Design ripple effect helper. - * - * @details Manages ripple effects for Material Design widgets. Provides - * animated ripple propagation on press/release interactions with - * bounded and unbounded modes. - * - * @since N/A - * @ingroup ui_widget_material_base - */ -class CF_UI_EXPORT RippleHelper : public QObject { - Q_OBJECT - public: - /** - * @brief Constructor with animation factory. - * - * @param[in] factory aex::WeakPtr to the animation factory. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - explicit RippleHelper(aex::WeakPtr factory, - QObject* parent); - - /** - * @brief Ripple rendering mode. - * - * @since N/A - * @ingroup ui_widget_material_base - */ - enum class Mode { - Bounded, ///< Clipped by widget bounds. - Unbounded ///< Not clipped by widget bounds. - }; - Q_ENUM(Mode) - - /** - * @brief Sets the ripple rendering mode. - * - * @param[in] mode Rendering mode to use. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void setMode(Mode mode); - - /** - * @brief Sets the ripple color. - * - * @param[in] color Ripple color (typically the state color). - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void setColor(const cf::ui::base::CFColor& color); - - /** - * @brief Handles press event. - * - * @param[in] pos Press position coordinates. - * @param[in] widgetRect Widget bounding rectangle. - * - * @throws None - * @note Creates a new ripple at the press position. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void onPress(const QPoint& pos, const QRectF& widgetRect); - - /** - * @brief Handles release event. - * - * @throws None - * @note Starts ripple fade-out animation. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void onRelease(); - - /** - * @brief Handles cancel event. - * - * @throws None - * @note Cancels unreleased ripples (e.g., when mouse leaves widget). - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void onCancel(); - - /** - * @brief Paints the ripples. - * - * @param[in] painter QPainter to render with. - * @param[in] clipPath Clipping path for bounded mode. - * - * @throws None - * @note Call in paintEvent. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void paint(QPainter* painter, const QPainterPath& clipPath); - - /** - * @brief Checks if there are active ripples. - * - * @return true if any ripples are active, false otherwise. - * - * @throws None - * @note Used to determine if repaint is needed. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - bool hasActiveRipple() const; - - signals: - /** - * @brief Signal emitted when repaint is needed. - * - * @since N/A - * @ingroup ui_widget_material_base - */ - void repaintNeeded(); - - private: - QList m_ripples; - Mode m_mode = Mode::Bounded; - cf::ui::base::CFColor m_color; - aex::WeakPtr m_animator; - - float maxRadius(const QRectF& rect, const QPointF& center) const; -}; - -} // namespace cf::ui::widget::material::base diff --git a/ui/widget/material/base/state_machine.cpp b/ui/widget/material/base/state_machine.cpp deleted file mode 100644 index 23989a2fa..000000000 --- a/ui/widget/material/base/state_machine.cpp +++ /dev/null @@ -1,250 +0,0 @@ -/** - * @file state_machine.cpp - * @brief Material Design State Machine Implementation - * - * Manages visual state transitions for Material widgets, driving - * StateLayer opacity animations following Material Design 3 specifications. - * - * State opacity values (Material Design 3): - * - Normal: 0.00 - * - Hovered: 0.08 - * - Pressed: 0.12 - * - Focused: 0.12 - * - Dragged: 0.16 - * - Disabled: 0.00 - * - * @author Material Design Framework Team - * @date 2026-02-28 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "state_machine.h" -#include "components/animation.h" -#include "components/material/cfmaterial_animation_factory.h" -#include "components/material/cfmaterial_animation_strategy.h" - -namespace cf::ui::widget::material::base { - -using namespace cf::ui::components::material; -using namespace cf::ui::components; - -/** - * @brief Constructor - initializes state machine with animation factory. - * - * @param factory aex::WeakPtr to animation factory for creating transitions. - * @param parent QObject parent for memory management. - */ -StateMachine::StateMachine(aex::WeakPtr factory, - QObject* parent) - : QObject(parent), m_state(State::StateNormal), m_opacity(0.0f) { - m_animator = factory; -} - -/** - * @brief Destructor - cancels any running animation. - */ -StateMachine::~StateMachine() { - cancelCurrentAnimation(); -} - -/** - * @brief Calculates target opacity for a given state. - * - * Follows Material Design 3 state layer opacity specifications. - * - * @param s The state to calculate opacity for. - * @return Target opacity value [0.0, 1.0]. - */ -float StateMachine::targetOpacityForState(States s) const { - // Priority order: Disabled > Pressed > Dragged > Focused > Hovered > Normal - if (s & State::StateDisabled) { - return 0.0f; - } - if (s & State::StatePressed) { - return 0.12f; - } - if (s & State::StateDragged) { - return 0.16f; - } - if (s & State::StateFocused) { - return 0.12f; - } - if (s & State::StateHovered) { - return 0.08f; - } - if (s & State::StateChecked) { - return 0.08f; - } - - return 0.0f; -} - -/** - * @brief Cancels the currently running opacity animation. - * - * Disconnects all signals and stops the animation to prevent multiple - * animations from competing to update m_opacity. - * - * IMPORTANT: After stopping, reset m_opacity to the correct target value - * for the current state. This is necessary because the progress signal - * carries 0-1 progress, not the actual opacity value. - */ -void StateMachine::cancelCurrentAnimation() { - if (m_currentAnimation) { - auto* anim = m_currentAnimation.Get(); - if (anim) { - disconnect(anim, &components::ICFAbstractAnimation::progressChanged, this, nullptr); - disconnect(anim, &components::ICFAbstractAnimation::finished, this, nullptr); - anim->stop(); - } - m_currentAnimation = nullptr; - } - m_opacity = targetOpacityForState(m_state); - emit stateLayerOpacityChanged(m_opacity); -} - -/** - * @brief Slot called when the current animation finishes. - * - * Clears the animation reference and ensures m_opacity is set to the - * correct target value for the current state. - */ -void StateMachine::onAnimationFinished() { - m_currentAnimation = nullptr; - float targetOpacity = targetOpacityForState(m_state); - if (m_opacity != targetOpacity) { - m_opacity = targetOpacity; - emit stateLayerOpacityChanged(m_opacity); - } -} - -/** - * @brief Starts opacity transition animation. - * - * Creates a fade animation using the animation factory. - * Cancels any currently running animation before starting a new one. - * - * @param to Target opacity value. - */ -void StateMachine::animateOpacityTo(float to) { - auto* factory = m_animator.Get(); - if (!factory || !factory->isAllEnabled()) { - m_opacity = to; - emit stateLayerOpacityChanged(m_opacity); - return; - } - - cancelCurrentAnimation(); - - float from = m_opacity; - - AnimationDescriptor desc("fade", "md.motion.shortEnter", "opacity", from, to); - - auto anim = factory->createAnimation(desc, nullptr, this); - if (!anim) { - m_opacity = to; - emit stateLayerOpacityChanged(m_opacity); - return; - } - - m_currentAnimation = anim; - auto* rawAnim = anim.Get(); - - connect(rawAnim, &components::ICFAbstractAnimation::progressChanged, this, - [this, from, to](float progress) { - m_opacity = from + (to - from) * progress; - emit stateLayerOpacityChanged(m_opacity); - }); - - connect(rawAnim, &components::ICFAbstractAnimation::finished, this, - &StateMachine::onAnimationFinished, Qt::UniqueConnection); - - rawAnim->start(components::ICFAbstractAnimation::Direction::Forward); -} - -// ============================================================================ -// State Transition -// ============================================================================ - -void StateMachine::transitionTo(States newState) { - if (m_state == newState) { - return; - } - States oldState = m_state; - m_state = newState; - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); -} - -// ============================================================================ -// Event Handlers -// ============================================================================ - -void StateMachine::onHoverEnter() { - if (m_state & State::StateDisabled) - return; - transitionTo(m_state | State::StateHovered); -} - -void StateMachine::onHoverLeave() { - transitionTo(m_state & ~static_cast(State::StateHovered)); -} - -void StateMachine::onPress(const QPoint& pos) { - Q_UNUSED(pos) - if (m_state & State::StateDisabled) - return; - transitionTo(m_state | State::StatePressed); -} - -void StateMachine::onRelease() { - transitionTo(m_state & ~static_cast(State::StatePressed)); -} - -void StateMachine::onFocusIn() { - if (m_state & State::StateDisabled) - return; - transitionTo(m_state | State::StateFocused); -} - -void StateMachine::onFocusOut() { - transitionTo(m_state & ~static_cast(State::StateFocused)); -} - -void StateMachine::onEnable() { - transitionTo(m_state & ~static_cast(State::StateDisabled)); -} - -void StateMachine::onDisable() { - transitionTo(m_state | State::StateDisabled); -} - -void StateMachine::onCheckedChanged(bool checked) { - if (m_state & State::StateDisabled) - return; - if (checked) { - transitionTo(m_state | State::StateChecked); - } else { - transitionTo(m_state & ~static_cast(State::StateChecked)); - } -} - -// ============================================================================ -// State Queries -// ============================================================================ - -StateMachine::States StateMachine::currentState() const { - return m_state; -} - -bool StateMachine::hasState(State s) const { - return (m_state & s) != States(); -} - -float StateMachine::stateLayerOpacity() const { - return m_opacity; -} - -} // namespace cf::ui::widget::material::base diff --git a/ui/widget/material/base/state_machine.h b/ui/widget/material/base/state_machine.h deleted file mode 100644 index 6cf1c5168..000000000 --- a/ui/widget/material/base/state_machine.h +++ /dev/null @@ -1,327 +0,0 @@ -/** - * @file ui/widget/material/base/state_machine.h - * @brief Material Design state machine for widget interaction states. - * - * Manages Material Design widget states including hover, pressed, focused, - * disabled, and checked states with animated state layer transitions. - * - * @author N/A - * @date N/A - * @version N/A - * @since N/A - * @ingroup ui_widget_material_base - */ -#pragma once -#include "aex/weak_ptr/weak_ptr.h" -#include "components/material/cfmaterial_animation_factory.h" -#include "export.h" -#include - -namespace cf::ui::widget::material::base { - -/** - * @brief Material Design state machine. - * - * @details Manages Material Design widget states including hover, pressed, - * focused, disabled, and checked states with animated state layer - * transitions. - * - * @since N/A - * @ingroup ui_widget_material_base - */ -class CF_UI_EXPORT StateMachine : public QObject { - Q_OBJECT - public: - /** - * @brief Widget interaction states. - * - * @since N/A - * @ingroup ui_widget_material_base - */ - enum class State { - StateNormal = 0x00, ///< Normal state. - StateHovered = 0x01, ///< Mouse hover state. - StatePressed = 0x02, ///< Mouse pressed state. - StateFocused = 0x04, ///< Keyboard focused state. - StateDisabled = 0x08, ///< Disabled state. - StateChecked = 0x10, ///< Checked state. - StateDragged = 0x20 ///< Dragged state. - }; - Q_DECLARE_FLAGS(States, State) - - /** - * @brief Constructor with animation factory. - * - * @param[in] factory aex::WeakPtr to the animation factory. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - explicit StateMachine(aex::WeakPtr factory, - QObject* parent); - - /** - * @brief Handles hover enter event. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void onHoverEnter(); - - /** - * @brief Handles hover leave event. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void onHoverLeave(); - - /** - * @brief Handles press event. - * - * @param[in] pos Press position coordinates. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void onPress(const QPoint& pos); - - /** - * @brief Handles release event. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void onRelease(); - - /** - * @brief Handles focus in event. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void onFocusIn(); - - /** - * @brief Handles focus out event. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void onFocusOut(); - - /** - * @brief Handles enable event. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void onEnable(); - - /** - * @brief Handles disable event. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void onDisable(); - - /** - * @brief Handles checked state change event. - * - * @param[in] checked true if widget is checked, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void onCheckedChanged(bool checked); - - /** - * @brief Destructor. - * - * @details Cancels any running animation before destruction. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - ~StateMachine(); - - /** - * @brief Gets the current states. - * - * @return Current widget states. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - States currentState() const; - - /** - * @brief Checks if a specific state is active. - * - * @param[in] s State to check. - * - * @return true if the state is active, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - bool hasState(State s) const; - - /** - * @brief Gets the state layer opacity. - * - * @return Current state layer opacity (0.0 to 1.0). - * - * @throws None - * @note Used for Material Design state overlay rendering. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - float stateLayerOpacity() const; - - signals: - /** - * @brief Signal emitted when state changes. - * - * @param[in] newState New widget states. - * @param[in] oldState Previous widget states. - * - * @since N/A - * @ingroup ui_widget_material_base - */ - void stateChanged(States newState, States oldState); - - /** - * @brief Signal emitted when state layer opacity changes. - * - * @param[in] opacity New opacity value. - * - * @since N/A - * @ingroup ui_widget_material_base - */ - void stateLayerOpacityChanged(float opacity); - - private: - /** - * @brief Animates the state layer opacity to a target value. - * - * @details Creates and starts an opacity animation from the current - * opacity value to the target value. Cancels any running - * animation before starting a new one. - * - * @param[in] to Target opacity value (0.0 to 1.0). - * - * @throws None - * @note Uses the animation factory to create a fade animation. - * @warning The animation reference is stored and can be cancelled. - * @since N/A - * @ingroup ui_widget_material_base - */ - void animateOpacityTo(float to); - - /** - * @brief Cancels the currently running opacity animation. - * - * @throws None - * @note Disconnects all signals and stops the animation. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void cancelCurrentAnimation(); - - /** - * @brief Transitions to a new state if different from current. - * - * @details Compares new state with current state. If different, - * updates the state, emits stateChanged, and animates - * opacity to the target value for the new state. - * - * @param[in] newState The target state to transition to. - * - * @throws None - * @note No-op if newState equals current state. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void transitionTo(States newState); - - /** - * @brief Slot called when the current animation finishes. - * - * @details Clears the animation reference to allow new animations. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - void onAnimationFinished(); - - /** - * @brief Calculates the target opacity for a given widget state. - * - * @param[in] s Widget state combination. - * - * @return Target opacity value (0.0 to 1.0). - * - * @throws None - * @note Higher priority states take precedence. - * @warning None - * @since N/A - * @ingroup ui_widget_material_base - */ - float targetOpacityForState(States s) const; - - States m_state = State::StateNormal; - float m_opacity = 0.0f; - aex::WeakPtr m_animator; - - /// Reference to the currently running opacity animation - aex::WeakPtr m_currentAnimation; -}; -} // namespace cf::ui::widget::material::base diff --git a/ui/widget/material/widget/button/button.cpp b/ui/widget/material/widget/button/button.cpp deleted file mode 100644 index a188a5994..000000000 --- a/ui/widget/material/widget/button/button.cpp +++ /dev/null @@ -1,599 +0,0 @@ -/** - * @file button.cpp - * @brief Material Design 3 Button Implementation - * - * Implements a Material Design 3 button with 5 variants: Filled, Tonal, - * Outlined, Text, and Elevated. Supports ripple effects, state layers, - * elevation shadows, and focus indicators. - * - * @author Material Design Framework Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "button.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "base/geometry_helper.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::base::geometry; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::material::base; -using namespace cf::ui::widget::application_support; - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -Button::Button(ButtonVariant variant, QWidget* parent) - : QPushButton(parent), m_material(this, - MaterialWidgetBase::Config{ - .useElevation = true, - .initialElevation = 2, - }), - variant_(variant) { - setFont(labelFont()); -} - -Button::Button(const QString& text, ButtonVariant variant, QWidget* parent) - : Button(variant, parent) { - setText(text); -} - -Button::~Button() { - // Components are parented to this, Qt will delete them automatically -} - -// ============================================================================ -// Event Handlers (moved from inline in header) -// ============================================================================ - -void Button::enterEvent(QEnterEvent* event) { - QPushButton::enterEvent(event); - m_material.onEnterEvent(); -} - -void Button::leaveEvent(QEvent* event) { - QPushButton::leaveEvent(event); - m_material.onLeaveEvent(); -} - -void Button::mousePressEvent(QMouseEvent* event) { - QPushButton::mousePressEvent(event); - if (m_pressEffectEnabled) { - m_material.onMousePress(event->pos(), rect()); - } else { - m_material.stateMachine()->onPress(event->pos()); - if (m_material.ripple()) - m_material.ripple()->onPress(event->pos(), rect()); - update(); - } -} - -void Button::mouseReleaseEvent(QMouseEvent* event) { - QPushButton::mouseReleaseEvent(event); - if (m_pressEffectEnabled) { - m_material.onMouseRelease(); - } else { - m_material.stateMachine()->onRelease(); - if (m_material.ripple()) - m_material.ripple()->onRelease(); - update(); - } -} - -void Button::focusInEvent(QFocusEvent* event) { - QPushButton::focusInEvent(event); - m_material.onFocusIn(); -} - -void Button::focusOutEvent(QFocusEvent* event) { - QPushButton::focusOutEvent(event); - m_material.onFocusOut(); -} - -void Button::changeEvent(QEvent* event) { - QPushButton::changeEvent(event); - if (event->type() == QEvent::EnabledChange) { - m_material.onEnabledChange(isEnabled()); - } -} - -// ============================================================================ -// Property Getters/Setters -// ============================================================================ - -int Button::elevation() const { - return m_material.elevation() ? m_material.elevation()->elevation() : 0; -} - -void Button::setElevation(int level) { - if (m_material.elevation()) { - m_material.elevation()->setElevation(level); - update(); - } -} - -void Button::setLightSourceAngle(float degrees) { - if (m_material.elevation()) { - m_material.elevation()->setLightSourceAngle(degrees); - update(); - } -} - -float Button::lightSourceAngle() const { - return m_material.elevation() ? m_material.elevation()->lightSourceAngle() : 15.0f; -} - -void Button::setLeadingIcon(const QIcon& icon) { - leadingIcon_ = icon; - updateGeometry(); - update(); -} - -Button::ButtonVariant Button::variant() const { - return variant_; -} - -void Button::setVariant(ButtonVariant variant) { - if (variant_ != variant) { - variant_ = variant; - // 变体改变时保持 elevation 不变(统一使用 level 2) - updateGeometry(); - update(); - } -} - -bool Button::pressEffectEnabled() const { - return m_pressEffectEnabled; -} - -void Button::setPressEffectEnabled(bool enabled) { - if (m_pressEffectEnabled != enabled) { - m_pressEffectEnabled = enabled; - update(); - } -} - -// ============================================================================ -// Size Hints -// ============================================================================ - -QSize Button::sizeHint() const { - // Material Design button specifications: - // - Height: 40dp (content area) - // - Horizontal padding: 24dp - // - Icon size: 18dp, 8dp gap from text - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // 内容区域高度 - float contentHeight = helper.dpToPx(40.0f); - float hPadding = helper.dpToPx(24.0f); - float iconWidth = 0; - float iconGap = 0; - - if (!leadingIcon_.isNull()) { - iconWidth = helper.dpToPx(18.0f); - iconGap = helper.dpToPx(8.0f); - } - - float textWidth = fontMetrics().horizontalAdvance(text()); - float contentWidth = hPadding * 2 + iconWidth + iconGap + textWidth; - - // 加上阴影边距 - QMarginsF margin = shadowMargin(); - float totalWidth = contentWidth + margin.left() + margin.right(); - float totalHeight = contentHeight + margin.top() + margin.bottom(); - - return QSize(int(std::ceil(totalWidth)), int(std::ceil(totalHeight))); -} - -QSize Button::minimumSizeHint() const { - // Minimum width respects padding and some text - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // 内容区域高度 - float contentHeight = helper.dpToPx(40.0f); - float hPadding = helper.dpToPx(24.0f); - float iconWidth = leadingIcon_.isNull() ? 0 : helper.dpToPx(18.0f); - float iconGap = leadingIcon_.isNull() ? 0 : helper.dpToPx(8.0f); - - // Minimum text width (approximately 3 characters) - float minTextWidth = fontMetrics().horizontalAdvance("MMM"); - float contentWidth = hPadding * 2 + iconWidth + iconGap + minTextWidth; - - // 加上阴影边距 - QMarginsF margin = shadowMargin(); - float totalWidth = contentWidth + margin.left() + margin.right(); - float totalHeight = contentHeight + margin.top() + margin.bottom(); - - return QSize(int(std::ceil(totalWidth)), int(std::ceil(totalHeight))); -} - -// ============================================================================ -// Color Access Methods -// ============================================================================ - -namespace { -// Fallback colors when theme is not available -inline CFColor fallbackPrimary() { - return CFColor(103, 80, 164); -} // Purple 700 -inline CFColor fallbackOnPrimary() { - return CFColor(255, 255, 255); -} // White -inline CFColor fallbackPrimaryContainer() { - return CFColor(234, 221, 255); -} -inline CFColor fallbackOnPrimaryContainer() { - return CFColor(29, 25, 67); -} -inline CFColor fallbackSecondary() { - return CFColor(101, 163, 207); -} // Light Blue -inline CFColor fallbackSecondaryContainer() { - return CFColor(179, 218, 255); -} -inline CFColor fallbackOnSecondaryContainer() { - return CFColor(0, 46, 73); -} -inline CFColor fallbackOutline() { - return CFColor(120, 124, 132); -} // Outline -inline CFColor fallbackSurface() { - return CFColor(253, 253, 253); -} // Surface -inline CFColor fallbackOnSurface() { - return CFColor(27, 27, 31); -} // On Surface -} // namespace - -CFColor Button::containerColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - - switch (variant_) { - case ButtonVariant::Filled: - return CFColor(colorScheme.queryColor(PRIMARY)); - case ButtonVariant::Tonal: - return CFColor(colorScheme.queryColor(SECONDARY_CONTAINER)); - case ButtonVariant::Outlined: - case ButtonVariant::Text: - return CFColor(colorScheme.queryColor(SURFACE)); - case ButtonVariant::Elevated: - return CFColor(colorScheme.queryColor(SURFACE)); - } - } catch (...) { - // Fallback if theme access fails - } - - switch (variant_) { - case ButtonVariant::Filled: - return fallbackPrimary(); - case ButtonVariant::Tonal: - return fallbackSecondaryContainer(); - case ButtonVariant::Outlined: - case ButtonVariant::Text: - case ButtonVariant::Elevated: - return fallbackSurface(); - } - return fallbackSurface(); -} - -CFColor Button::labelColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - - switch (variant_) { - case ButtonVariant::Filled: - return CFColor(colorScheme.queryColor(ON_PRIMARY)); - case ButtonVariant::Tonal: - return CFColor(colorScheme.queryColor(ON_SECONDARY_CONTAINER)); - case ButtonVariant::Outlined: - case ButtonVariant::Text: - case ButtonVariant::Elevated: - return CFColor(colorScheme.queryColor(PRIMARY)); - } - } catch (...) { - // Fallback if theme access fails - } - - switch (variant_) { - case ButtonVariant::Filled: - return fallbackOnPrimary(); - case ButtonVariant::Tonal: - return fallbackOnPrimaryContainer(); - case ButtonVariant::Outlined: - case ButtonVariant::Text: - case ButtonVariant::Elevated: - return fallbackPrimary(); - } - return fallbackOnSurface(); -} - -CFColor Button::stateLayerColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - - // State layer color is the same as label color for all variants - return labelColor(); - } catch (...) { - return labelColor(); - } -} - -CFColor Button::outlineColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOutline(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(OUTLINE)); - } catch (...) { - return fallbackOutline(); - } -} - -float Button::cornerRadius() const { - // Full rounded corners (50% of height) - return height() / 2.0f; -} - -QFont Button::labelFont() const { - auto* app = Application::instance(); - if (!app) { - // Fallback to system font with reasonable size - QFont font = QPushButton::font(); - font.setPixelSize(14); - font.setWeight(QFont::Medium); - return font; - } - - try { - const auto& theme = app->currentTheme(); - auto& fontType = theme.font_type(); - return fontType.queryTargetFont("labelLarge"); - } catch (...) { - QFont font = QPushButton::font(); - font.setPixelSize(14); - font.setWeight(QFont::Medium); - return font; - } -} - -// ============================================================================ -// Paint Event -// ============================================================================ - -void Button::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - // 计算按压偏移 - float pressOffset = 0.0f; - if (m_pressEffectEnabled && m_material.elevation()) { - pressOffset = m_material.elevation()->pressOffset(); - } - - // Calculate content area (inset to make room for shadow) - QMarginsF margin = shadowMargin(); - QRectF contentRect = - QRectF(rect()).adjusted(margin.left(), margin.top(), -margin.right(), -margin.bottom()); - - // 应用按压偏移 - 整体向下平移,而不是压缩顶部 - contentRect.translate(0, pressOffset); - - // Create shape path (full rounded corners) using content area - QPainterPath shape = roundedRect(contentRect, cornerRadius()); - - // Step 1: Draw shadow (Elevated variant only) - uses contentRect - drawShadow(p, contentRect, shape); - - // Step 2: Draw background - drawBackground(p, shape); - - // Step 3: Draw state layer - drawStateLayer(p, shape); - - // Step 4: Draw ripple - drawRipple(p, shape); - - // Step 5: Draw outline (Outlined variant only) - if (variant_ == ButtonVariant::Outlined) { - drawOutline(p, shape); - } - - // Step 6: Draw content (icon + text) - drawContent(p, contentRect); - - // Step 7: Draw focus indicator - drawFocusIndicator(p, shape); -} - -// ============================================================================ -// Drawing Helpers -// ============================================================================ - -// Calculate shadow margin (extra space needed for shadow) -QMarginsF Button::shadowMargin() const { - if (!m_material.elevation() || m_material.elevation()->elevation() <= 0) { - return QMarginsF(0, 0, 0, 0); - } - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - // 根据 elevation 级别动态计算边距 - // Level 2: blur=4dp, offset=2dp, 最大偏移约 3dp - // 预留边距 = offset + blur/2,更精确的阴影空间 - int level = m_material.elevation()->elevation(); - float margin = helper.dpToPx(2.0f + level * 1.5f); // level 2: 约 5dp - return QMarginsF(margin, margin, margin, margin); -} - -void Button::drawShadow(QPainter& p, const QRectF& contentRect, const QPainterPath& shape) { - if (m_material.elevation() && m_material.elevation()->elevation() > 0) { - m_material.elevation()->paintShadow(&p, shape); - } -} - -void Button::drawBackground(QPainter& p, const QPainterPath& shape) { - CFColor bg = containerColor(); - - // Handle disabled state - if (!isEnabled()) { - QColor color = bg.native_color(); - color.setAlphaF(0.38f); // 38% opacity for disabled - p.fillPath(shape, color); - return; - } - - // Text variant has transparent background - if (variant_ == ButtonVariant::Text) { - return; - } - - p.fillPath(shape, bg.native_color()); -} - -void Button::drawStateLayer(QPainter& p, const QPainterPath& shape) { - if (!isEnabled() || !m_material.stateMachine()) { - return; - } - - float opacity = m_material.stateMachine()->stateLayerOpacity(); - if (opacity <= 0.0f) { - return; - } - - CFColor stateColor = stateLayerColor(); - QColor color = stateColor.native_color(); - color.setAlphaF(color.alphaF() * opacity); - - p.fillPath(shape, color); -} - -void Button::drawRipple(QPainter& p, const QPainterPath& shape) { - if (m_material.ripple()) { - // Set ripple color based on label color (content color) - m_material.ripple()->setColor(labelColor()); - m_material.ripple()->paint(&p, shape); - } -} - -void Button::drawOutline(QPainter& p, const QPainterPath& shape) { - // 使用 1px inset(而不是 dp)让 outline 刚好在边缘内侧 - // QPen 描边是居中对齐的,所以 0.5px 在内,0.5px 在外 - float inset = 0.5f; - - QColor color = outlineColor().native_color(); - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - p.save(); - - // Create inset path for outline - QRectF shapeBounds = shape.boundingRect(); - QRectF insetRect = shapeBounds.adjusted(inset, inset, -inset, -inset); - float adjustedRadius = std::max(0.0f, cornerRadius() - inset); - QPainterPath insetShape = roundedRect(insetRect, adjustedRadius); - - QPen pen(color, 1.0); // 1px width - pen.setCosmetic(true); // Keep consistent 1px width across DPI - p.setPen(pen); - p.setBrush(Qt::NoBrush); - p.drawPath(insetShape); - - p.restore(); -} - -void Button::drawContent(QPainter& p, const QRectF& contentRect) { - CFColor textColor = labelColor(); - if (!isEnabled()) { - QColor color = textColor.native_color(); - color.setAlphaF(0.38f); - p.setPen(color); - } else { - p.setPen(textColor.native_color()); - } - - // Use button's font - QFont font = labelFont(); - p.setFont(font); - - // Use contentRect passed in (already inset for shadow margin) - QRectF textArea = contentRect; - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float hPadding = helper.dpToPx(24.0f); - textArea.adjust(hPadding, 0, -hPadding, 0); - - // Calculate icon and text positions - float iconWidth = 0; - float iconGap = 0; - - if (!leadingIcon_.isNull()) { - iconWidth = helper.dpToPx(18.0f); - iconGap = helper.dpToPx(8.0f); - } - - float textWidth = fontMetrics().horizontalAdvance(text()); - float totalContentWidth = iconWidth + iconGap + textWidth; - float startX = textArea.left() + (textArea.width() - totalContentWidth) / 2.0f; - float centerY = textArea.center().y(); - - // Draw icon - if (!leadingIcon_.isNull()) { - float iconSize = helper.dpToPx(18.0f); - QRectF iconRect(startX, centerY - iconSize / 2, iconSize, iconSize); - leadingIcon_.paint(&p, iconRect.toRect()); - startX += iconWidth + iconGap; - } - - // Draw text - QRectF textRect(startX, textArea.top(), textWidth, textArea.height()); - p.drawText(textRect, Qt::AlignCenter, text()); -} - -void Button::drawFocusIndicator(QPainter& p, const QPainterPath& shape) { - if (m_material.focusIndicator()) { - m_material.focusIndicator()->paint(&p, shape, labelColor()); - } -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/button/button.h b/ui/widget/material/widget/button/button.h deleted file mode 100644 index 79b1b8207..000000000 --- a/ui/widget/material/widget/button/button.h +++ /dev/null @@ -1,398 +0,0 @@ -/** - * @file ui/widget/material/widget/button/button.h - * @brief Material Design 3 Button widget. - * - * Implements Material Design 3 button with support for filled, tonal, - * outlined, text, and elevated variants. Includes ripple effects, state - * layers, elevation shadows, and focus indicators. - * - * @author N/A - * @date N/A - * @version N/A - * @since N/A - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include - -#include "base/color.h" -#include "export.h" -#include "widget/material/base/material_widget_base.h" -#include -#include -#include - -namespace cf::ui::widget::material { - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 Button widget. - * - * @details Implements Material Design 3 button with support for filled, - * tonal, outlined, text, and elevated variants. Includes ripple - * effects, state layers, elevation shadows, and focus indicators. - * - * @since N/A - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT Button : public QPushButton { - Q_OBJECT - Q_PROPERTY(ButtonVariant variant READ variant WRITE setVariant) - Q_PROPERTY(int elevation READ elevation WRITE setElevation) - Q_PROPERTY(bool pressEffectEnabled READ pressEffectEnabled WRITE setPressEffectEnabled) - - public: - /** - * @brief Button visual variant. - * - * @since N/A - * @ingroup ui_widget_material_widget - */ - enum class ButtonVariant { Filled, Tonal, Outlined, Text, Elevated }; - Q_ENUM(ButtonVariant); - - /** - * @brief Constructor with variant. - * - * @param[in] variant Button visual variant. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - explicit Button(ButtonVariant variant = ButtonVariant::Filled, QWidget* parent = nullptr); - - /** - * @brief Constructor with text and variant. - * - * @param[in] text Button text label. - * @param[in] variant Button visual variant. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - explicit Button(const QString& text, ButtonVariant variant = ButtonVariant::Filled, - QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - ~Button() override; - - /** - * @brief Gets the elevation level. - * - * @return Elevation level (0-5). - * - * @throws None - * @note Material Design defines 6 standard levels. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - int elevation() const; - - /** - * @brief Sets the elevation level. - * - * @param[in] level Elevation level (0-5). - * - * @throws None - * @note Affects shadow rendering. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void setElevation(int level); - - /** - * @brief Sets the light source angle for shadow. - * - * @param[in] degrees Angle in degrees (-90 to 90). - * - * @throws None - * @note Default is 15 degrees (light from top-left). - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void setLightSourceAngle(float degrees); - - /** - * @brief Gets the light source angle. - * - * @return Light source angle in degrees. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - float lightSourceAngle() const; - - /** - * @brief Sets the leading icon. - * - * @param[in] icon Icon to display before text. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void setLeadingIcon(const QIcon& icon); - - /** - * @brief Redirects setIcon to setLeadingIcon for convenience. - * - * @param[in] icon Icon to display before text. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void setIcon(const QIcon& icon) { setLeadingIcon(icon); } - - /** - * @brief Gets the current icon. - * - * @return Current icon. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - QIcon icon() const { return leadingIcon_; } - - /** - * @brief Gets whether press effect is enabled. - * - * @return true if press effect is enabled, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - bool pressEffectEnabled() const; - - /** - * @brief Sets whether press effect is enabled. - * - * @param[in] enabled true to enable press effect, false to disable. - * - * @throws None - * @note Press effect includes elevation change and ripple. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void setPressEffectEnabled(bool enabled); - - /** - * @brief Gets the button variant. - * - * @return Current button variant. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - ButtonVariant variant() const; - - /** - * @brief Sets the button variant. - * - * @param[in] variant Button variant to use. - * - * @throws None - * @note Changing variant updates the visual appearance. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void setVariant(ButtonVariant variant); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the button. - * - * @throws None - * @note Based on text, icon, and padding. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures touch target size requirements. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - protected: - /** - * @brief Paints the button. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements 7-step Material Design paint pipeline. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles mouse enter event. - * - * @param[in] event Enter event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void enterEvent(QEnterEvent* event) override; - - /** - * @brief Handles mouse leave event. - * - * @param[in] event Leave event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void leaveEvent(QEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple and press state. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates press state. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Shows focus indicator. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Hides focus indicator. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - private: - // Drawing helpers - 7-step paint pipeline - QMarginsF shadowMargin() const; - void drawShadow(QPainter& p, const QRectF& contentRect, const QPainterPath& shape); - void drawBackground(QPainter& p, const QPainterPath& shape); - void drawStateLayer(QPainter& p, const QPainterPath& shape); - void drawRipple(QPainter& p, const QPainterPath& shape); - void drawOutline(QPainter& p, const QPainterPath& shape); - void drawContent(QPainter& p, const QRectF& contentRect); - void drawFocusIndicator(QPainter& p, const QPainterPath& shape); - - // Color access methods - CFColor containerColor() const; - CFColor labelColor() const; - CFColor stateLayerColor() const; - CFColor outlineColor() const; - float cornerRadius() const; - QFont labelFont() const; - - // Behavior components - base::MaterialWidgetBase m_material; - - ButtonVariant variant_; - QIcon leadingIcon_; - bool m_pressEffectEnabled = true; -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/checkbox/checkbox.cpp b/ui/widget/material/widget/checkbox/checkbox.cpp deleted file mode 100644 index 07894b408..000000000 --- a/ui/widget/material/widget/checkbox/checkbox.cpp +++ /dev/null @@ -1,605 +0,0 @@ -/** - * @file checkbox.cpp - * @brief Material Design 3 CheckBox Implementation - * - * Implements a Material Design 3 checkbox with three states: unchecked, checked, - * and indeterminate. Supports ripple effects, state layers, and focus indicators. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "checkbox.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "base/easing.h" -#include "base/geometry_helper.h" -#include "components/material/cfmaterial_property_animation.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::base::geometry; -using namespace cf::ui::components::material; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::material::base; - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -CheckBox::CheckBox(QWidget* parent) - : QCheckBox(parent), m_material(this, MaterialWidgetBase::Config{.useElevation = false}) { - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - - if (checkState() == Qt::Checked) { - m_checkAnimationProgress = 1.0f; - m_material.stateMachine()->onCheckedChanged(true); - } else if (checkState() == Qt::PartiallyChecked) { - m_checkAnimationProgress = 1.0f; - } - - setCursor(Qt::PointingHandCursor); -} - -CheckBox::CheckBox(const QString& text, QWidget* parent) : CheckBox(parent) { - setText(text); -} - -void CheckBox::setChecked(bool checked) { - if (isChecked() == checked) { - return; - } - QCheckBox::setChecked(checked); - updateAnimationProgress(checked ? 1.0f : 0.0f, checked); -} - -void CheckBox::setCheckState(Qt::CheckState state) { - if (checkState() == state) { - return; - } - QCheckBox::setCheckState(state); - - float progress = 0.0f; - bool isCheckedState = false; - switch (state) { - case Qt::Unchecked: - progress = 0.0f; - isCheckedState = false; - break; - case Qt::PartiallyChecked: - progress = 1.0f; // Full progress for complete indeterminate mark - isCheckedState = false; - break; - case Qt::Checked: - progress = 1.0f; - isCheckedState = true; - break; - } - updateAnimationProgress(progress, isCheckedState); -} - -void CheckBox::updateAnimationProgress(float progress, bool checked) { - m_checkAnimationProgress = progress; - if (m_material.stateMachine()) { - m_material.stateMachine()->onCheckedChanged(checked); - } - update(); -} - -void CheckBox::startCheckMarkAnimation(float target) { - float fromValue = m_checkAnimationProgress; - - auto factory = aex::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - if (!factory) { - m_checkAnimationProgress = target; - update(); - return; - } - - auto anim = - factory->createPropertyAnimation(&m_checkAnimationProgress, fromValue, target, 300, - cf::ui::base::Easing::Type::EmphasizedDecelerate, this); - - if (anim) { - if (auto* propAnim = dynamic_cast(anim.Get())) { - propAnim->setRange(fromValue, target); - } - anim->start(); - } else { - m_checkAnimationProgress = target; - update(); - } -} - -CheckBox::~CheckBox() { - // Components are parented to this, Qt will delete them automatically -} - -// ============================================================================ -// Event Handlers -// ============================================================================ - -void CheckBox::enterEvent(QEnterEvent* event) { - QCheckBox::enterEvent(event); - m_material.onEnterEvent(); -} - -void CheckBox::leaveEvent(QEvent* event) { - QCheckBox::leaveEvent(event); - m_material.onLeaveEvent(); -} - -void CheckBox::mousePressEvent(QMouseEvent* event) { - QCheckBox::mousePressEvent(event); - m_material.onMousePress(event->pos(), checkboxRect()); -} - -void CheckBox::mouseReleaseEvent(QMouseEvent* event) { - QCheckBox::mouseReleaseEvent(event); - m_material.onMouseRelease(); -} - -void CheckBox::focusInEvent(QFocusEvent* event) { - QCheckBox::focusInEvent(event); - m_material.onFocusIn(); -} - -void CheckBox::focusOutEvent(QFocusEvent* event) { - QCheckBox::focusOutEvent(event); - m_material.onFocusOut(); -} - -void CheckBox::changeEvent(QEvent* event) { - QCheckBox::changeEvent(event); - if (event->type() == QEvent::EnabledChange) { - m_material.onEnabledChange(isEnabled()); - } -} - -void CheckBox::nextCheckState() { - Qt::CheckState oldState = checkState(); - QCheckBox::nextCheckState(); - Qt::CheckState newState = checkState(); - - float newTarget = 0.0f; - bool checked = false; - - switch (newState) { - case Qt::Unchecked: - newTarget = 0.0f; - checked = false; - break; - case Qt::PartiallyChecked: - newTarget = 1.0f; - checked = false; - break; - case Qt::Checked: - newTarget = 1.0f; - checked = true; - break; - } - - if (m_material.stateMachine()) { - m_material.stateMachine()->onCheckedChanged(checked); - } - - startCheckMarkAnimation(newTarget); -} - -bool CheckBox::hitButton(const QPoint& pos) const { - return rect().contains(pos); -} - -// ============================================================================ -// Property Getters/Setters -// ============================================================================ - -bool CheckBox::hasError() const { - return m_error; -} - -void CheckBox::setError(bool error) { - if (m_error != error) { - m_error = error; - emit errorChanged(error); - update(); - } -} - -// ============================================================================ -// Size Hints -// ============================================================================ - -QSize CheckBox::sizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - float leftPadding = helper.dpToPx(12.0f); - float rightPadding = helper.dpToPx(12.0f); - float boxSize = helper.dpToPx(18.0f); - float spacing = helper.dpToPx(12.0f); - float textWidth = text().isEmpty() ? 0.0f : fontMetrics().horizontalAdvance(text()); - - float width = leftPadding + boxSize + spacing + textWidth + rightPadding; - float minWidth = helper.dpToPx(48.0f); - width = std::max(width, minWidth); - - float height = helper.dpToPx(48.0f); // Fixed height for touch target - - return QSize(int(std::ceil(width)), int(std::ceil(height))); -} - -QSize CheckBox::minimumSizeHint() const { - return sizeHint(); -} - -// ============================================================================ -// Color Access Methods -// ============================================================================ - -namespace { -inline CFColor fallbackPrimary() { - return CFColor(103, 80, 164); -} // Purple 700 -inline CFColor fallbackOnSurface() { - return CFColor(27, 27, 31); -} // On Surface -inline CFColor fallbackError() { - return CFColor(186, 26, 26); -} // Error -inline CFColor fallbackOutline() { - return CFColor(120, 124, 132); -} // Outline -} // namespace - -CFColor CheckBox::checkmarkColor() const { - auto* app = application_support::Application::instance(); - if (!app) { - return m_error ? fallbackError() : fallbackPrimary(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - - if (m_error) { - return CFColor(colorScheme.queryColor(ERROR)); - } - - if (isChecked() || checkState() == Qt::PartiallyChecked) { - return CFColor(colorScheme.queryColor(PRIMARY)); - } - return CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - return m_error ? fallbackError() : fallbackPrimary(); - } -} - -CFColor CheckBox::markDrawColor() const { - auto* app = application_support::Application::instance(); - if (!app) { - return CFColor(255, 255, 255); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_PRIMARY)); - } catch (...) { - return CFColor(255, 255, 255); - } -} - -CFColor CheckBox::borderColor() const { - auto* app = application_support::Application::instance(); - if (!app) { - return m_error ? fallbackError() : fallbackOutline(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - - if (m_error) { - return CFColor(colorScheme.queryColor(ERROR)); - } - - if (isChecked() || checkState() == Qt::PartiallyChecked) { - return CFColor(colorScheme.queryColor(PRIMARY)); - } - - return CFColor(colorScheme.queryColor(OUTLINE)); - } catch (...) { - return m_error ? fallbackError() : fallbackOutline(); - } -} - -CFColor CheckBox::backgroundColor() const { - auto* app = application_support::Application::instance(); - if (!app) { - return CFColor(Qt::transparent); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - - if (isChecked() || checkState() == Qt::PartiallyChecked) { - return CFColor(colorScheme.queryColor(PRIMARY)); - } - return CFColor(Qt::transparent); - } catch (...) { - return CFColor(Qt::transparent); - } -} - -CFColor CheckBox::stateLayerColor() const { - return checkmarkColor(); -} - -float CheckBox::cornerRadius() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(2.0f); -} - -float CheckBox::checkboxSize() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(18.0f); -} - -float CheckBox::strokeWidth() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(2.0f); -} - -// ============================================================================ -// Layout Helpers -// ============================================================================ - -QRectF CheckBox::checkboxRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - float boxSize = checkboxSize(); - float y = (height() - boxSize) / 2.0f; - float x = helper.dpToPx(12.0f); - - return QRectF(x, y, boxSize, boxSize); -} - -QRectF CheckBox::textRect() const { - QRectF box = checkboxRect(); - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - float spacing = helper.dpToPx(12.0f); - float x = box.right() + spacing; - float y = 0; - float w = width() - x; - float h = height(); - - return QRectF(x, y, w, h); -} - -// ============================================================================ -// Paint Event -// ============================================================================ - -void CheckBox::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - QRectF box = checkboxRect(); - - // Step 1: Draw background (for checked/indeterminate) - drawBackground(p, box); - - // Step 2: Draw border - drawBorder(p, box); - - // Step 3: Draw check mark or indeterminate mark - Qt::CheckState state = checkState(); - if (state == Qt::PartiallyChecked) { - drawIndeterminateMark(p, box); - } else if (state == Qt::Checked) { - drawCheckMark(p, box); - } - - // Step 4: Draw ripple - drawRipple(p, box); - - // Step 5: Draw text - if (!text().isEmpty()) { - drawText(p, textRect()); - } - - // Step 6: Draw focus indicator - drawFocusIndicator(p, box); -} - -// ============================================================================ -// Drawing Helpers -// ============================================================================ - -void CheckBox::drawBackground(QPainter& p, const QRectF& rect) { - if (checkState() == Qt::Unchecked) { - return; - } - - CFColor bgColor = backgroundColor(); - QColor color = bgColor.native_color(); - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - QPainterPath shape = roundedRect(rect, cornerRadius()); - p.fillPath(shape, color); -} - -void CheckBox::drawBorder(QPainter& p, const QRectF& rect) { - CFColor bColor = borderColor(); - QColor color = bColor.native_color(); - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - if (m_material.stateMachine() && isEnabled()) { - float opacity = m_material.stateMachine()->stateLayerOpacity(); - if (opacity > 0.0f) { - CFColor stateColor = stateLayerColor(); - QColor stateQColor = stateColor.native_color(); - stateQColor.setAlphaF(opacity); - - int r = int(color.red() * (1.0f - opacity) + stateQColor.red() * opacity); - int g = int(color.green() * (1.0f - opacity) + stateQColor.green() * opacity); - int b = int(color.blue() * (1.0f - opacity) + stateQColor.blue() * opacity); - color = QColor(r, g, b, color.alpha()); - } - } - - p.save(); - - float inset = strokeWidth() / 2.0f; - QRectF insetRect = rect.adjusted(inset, inset, -inset, -inset); - float adjustedRadius = std::max(0.0f, cornerRadius() - inset); - QPainterPath shape = roundedRect(insetRect, adjustedRadius); - - QPen pen(color, strokeWidth()); - pen.setCosmetic(false); - p.setPen(pen); - p.setBrush(Qt::NoBrush); - p.drawPath(shape); - - p.restore(); -} - -void CheckBox::drawCheckMark(QPainter& p, const QRectF& rect) { - CFColor cmColor = markDrawColor(); - QColor color = cmColor.native_color(); - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - p.save(); - QPen pen(color, strokeWidth() * 0.6f); - pen.setCapStyle(Qt::RoundCap); - pen.setJoinStyle(Qt::RoundJoin); - p.setPen(pen); - - float w = rect.width(); - float h = rect.height(); - - float left = rect.left() + w * 0.25f; - float bottom = rect.top() + h * 0.55f; - - float centerX = rect.left() + w * 0.45f; - float centerY = rect.top() + h * 0.70f; - - float right = rect.left() + w * 0.75f; - float top = rect.top() + h * 0.35f; - - float progress = m_checkAnimationProgress; - - if (progress > 0.0f) { - float segment1Progress = std::min(progress * 2.0f, 1.0f); - float currentX = left + (centerX - left) * segment1Progress; - float currentY = bottom + (centerY - bottom) * segment1Progress; - p.drawLine(QPointF(left, bottom), QPointF(currentX, currentY)); - } - - if (progress > 0.5f) { - float segment2Progress = (progress - 0.5f) * 2.0f; - float currentX = centerX + (right - centerX) * segment2Progress; - float currentY = centerY + (top - centerY) * segment2Progress; - p.drawLine(QPointF(centerX, centerY), QPointF(currentX, currentY)); - } - - p.restore(); -} - -void CheckBox::drawIndeterminateMark(QPainter& p, const QRectF& rect) { - CFColor cmColor = markDrawColor(); - QColor color = cmColor.native_color(); - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - p.save(); - QPen pen(color, strokeWidth() * 0.8f); - pen.setCapStyle(Qt::RoundCap); - p.setPen(pen); - - float w = rect.width(); - float h = rect.height(); - - float margin = w * 0.25f; - float y = rect.top() + h / 2.0f; - float x1 = rect.left() + margin; - float x2 = rect.right() - margin; - - float progress = m_checkAnimationProgress; - - if (progress > 0.0f) { - float currentX = x1 + (x2 - x1) * progress; - p.drawLine(QPointF(x1, y), QPointF(currentX, y)); - } - - p.restore(); -} - -void CheckBox::drawRipple(QPainter& p, const QRectF& rect) { - if (m_material.ripple()) { - m_material.ripple()->setColor(stateLayerColor()); - - QPainterPath clipPath = roundedRect(rect, cornerRadius()); - m_material.ripple()->paint(&p, clipPath); - } -} - -void CheckBox::drawText(QPainter& p, const QRectF& rect) { - CFColor textColor = checkmarkColor(); - - if (!isEnabled()) { - QColor color = textColor.native_color(); - color.setAlphaF(0.38f); - p.setPen(color); - } else { - p.setPen(textColor.native_color()); - } - - p.setFont(font()); - - QRectF textBounds = rect.adjusted(0, 2, 0, -2); - p.drawText(textBounds, Qt::AlignLeft | Qt::AlignVCenter, text()); -} - -void CheckBox::drawFocusIndicator(QPainter& p, const QRectF& rect) { - if (m_material.focusIndicator()) { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float margin = helper.dpToPx(4.0f); - QRectF focusRect = rect.adjusted(-margin, -margin, margin, margin); - - QPainterPath shape = roundedRect(focusRect, cornerRadius() + margin); - m_material.focusIndicator()->paint(&p, shape, checkmarkColor()); - } -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/checkbox/checkbox.h b/ui/widget/material/widget/checkbox/checkbox.h deleted file mode 100644 index 0046ae386..000000000 --- a/ui/widget/material/widget/checkbox/checkbox.h +++ /dev/null @@ -1,340 +0,0 @@ -/** - * @file ui/widget/material/widget/checkbox/checkbox.h - * @brief Material Design 3 CheckBox widget. - * - * Implements Material Design 3 checkbox with support for unchecked, checked, - * and indeterminate states. Includes ripple effects, state layers, and focus - * indicators following Material Design 3 specifications. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include "base/color.h" -#include "export.h" -#include "widget/material/base/material_widget_base.h" -#include -#include - -namespace cf::ui::widget::material { - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 CheckBox widget. - * - * @details Implements Material Design 3 checkbox with support for unchecked, - * checked, and indeterminate states. Includes ripple effects, state - * layers, and focus indicators following Material Design 3 - * specifications. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT CheckBox : public QCheckBox { - Q_OBJECT - Q_PROPERTY(bool error READ hasError WRITE setError NOTIFY errorChanged) - - public: - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit CheckBox(QWidget* parent = nullptr); - - /** - * @brief Constructor with text. - * - * @param[in] text CheckBox text label. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit CheckBox(const QString& text, QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~CheckBox() override; - - /** - * @brief Sets the checked state. - * - * @param[in] checked true to check, false to uncheck. - * - * @throws None - * @note Updates animation progress for correct rendering. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setChecked(bool checked); - - /** - * @brief Sets the check state. - * - * @param[in] state The check state to set. - * - * @throws None - * @note Updates animation progress for correct rendering. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setCheckState(Qt::CheckState state); - - /** - * @brief Gets whether the checkbox is in error state. - * - * @return true if error state is active, false otherwise. - * - * @throws None - * @note Error state affects the border color. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool hasError() const; - - /** - * @brief Sets the error state. - * - * @param[in] error true to set error state, false to clear. - * - * @throws None - * @note Error state uses error color for the border. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setError(bool error); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the checkbox. - * - * @throws None - * @note Based on icon size, text, and spacing. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures touch target size requirements. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - signals: - /** - * @brief Signal emitted when error state changes. - * - * @param[in] error The new error state. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void errorChanged(bool error); - - protected: - /** - * @brief Paints the checkbox. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements Material Design paint pipeline. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles mouse enter event. - * - * @param[in] event Enter event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void enterEvent(QEnterEvent* event) override; - - /** - * @brief Handles mouse leave event. - * - * @param[in] event Leave event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void leaveEvent(QEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple and press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Handles check state change event. - * - * @param[in] event State change event. - * - * @throws None - * @note Triggers check mark animation. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void nextCheckState() override; - - /** - * @brief Determines if a point is within the clickable button area. - * - * @param[in] pos The point to check, in widget coordinates. - * @return true if the point is within the clickable area, false otherwise. - * - * @throws None - * @note Overrides QAbstractButton behavior to make entire widget clickable. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool hitButton(const QPoint& pos) const override; - - private: - // Drawing helpers - Material Design paint pipeline - QRectF checkboxRect() const; - QRectF textRect() const; - void drawBackground(QPainter& p, const QRectF& rect); - void drawBorder(QPainter& p, const QRectF& rect); - void drawCheckMark(QPainter& p, const QRectF& rect); - void drawIndeterminateMark(QPainter& p, const QRectF& rect); - void drawRipple(QPainter& p, const QRectF& rect); - void drawText(QPainter& p, const QRectF& rect); - void drawFocusIndicator(QPainter& p, const QRectF& rect); - - // Animation helper - void updateAnimationProgress(float progress, bool checked); - void startCheckMarkAnimation(float target); - - // Color access methods - CFColor checkmarkColor() const; - CFColor markDrawColor() const; // Color for check/indeterminate mark on background - CFColor borderColor() const; - CFColor backgroundColor() const; - CFColor stateLayerColor() const; - float cornerRadius() const; - - // Helper to get checkbox size in pixels - float checkboxSize() const; - float strokeWidth() const; - - // Behavior components - base::MaterialWidgetBase m_material; - - // Check mark animation progress (0.0 to 1.0) - float m_checkAnimationProgress = 0.0f; - - // Error state - bool m_error = false; -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/comboBox/combobox.cpp b/ui/widget/material/widget/comboBox/combobox.cpp deleted file mode 100644 index afe194d5c..000000000 --- a/ui/widget/material/widget/comboBox/combobox.cpp +++ /dev/null @@ -1,603 +0,0 @@ -/** - * @file combobox.cpp - * @brief Material Design 3 ComboBox Implementation - * - * Implements a Material Design 3 combo box (dropdown) with filled and - * outlined variants, animated dropdown arrow, and custom list styling. - * - * @author CFDesktop Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "combobox.h" -#include "aex/weak_ptr/weak_ptr.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "base/easing.h" -#include "base/geometry_helper.h" -#include "components/material/cfmaterial_animation_factory.h" -#include "components/material/cfmaterial_property_animation.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::base::geometry; -using namespace cf::ui::components; -using namespace cf::ui::components::material; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::material::base; - -// ============================================================================ -// Constants -// ============================================================================ - -namespace { -// Material Design 3 ComboBox specifications (in dp) -constexpr float FIELD_HEIGHT_DP = 56.0f; -constexpr float ARROW_SIZE_DP = 24.0f; -constexpr float ARROW_ICON_SIZE_DP = 12.0f; -constexpr float H_PADDING_DP = 16.0f; -constexpr float OUTLINE_WIDTH_DP = 1.0f; -constexpr float CORNER_RADIUS_DP = 4.0f; -constexpr float FOCUS_RING_MARGIN_DP = 4.0f; -constexpr float MIN_WIDTH_DP = 120.0f; -constexpr float DEFAULT_WIDTH_DP = 200.0f; -} // namespace - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -ComboBox::ComboBox(QWidget* parent) - : QComboBox(parent), variant_(ComboBoxVariant::Filled), - m_material(this, base::MaterialWidgetBase::Config{ - .useRipple = true, - .useElevation = false, - .useFocusIndicator = true, - }) { - // Set size policy - setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); - - // Set minimum contents length for proper sizing - setMinimumContentsLength(1); - - // Initialize popup animation - m_popupAnimation = new QPropertyAnimation(this); - m_popupAnimation->setEasingCurve(QEasingCurve::OutCubic); - m_popupAnimation->setDuration(250); - - // Set default cursor - setCursor(Qt::PointingHandCursor); -} - -ComboBox::~ComboBox() { - // Components are parented to this, Qt will delete them automatically -} - -ComboBox::ComboBoxVariant ComboBox::variant() const { - return variant_; -} - -void ComboBox::setVariant(ComboBoxVariant variant) { - if (variant_ != variant) { - variant_ = variant; - update(); - } -} - -// ============================================================================ -// Event Handlers -// ============================================================================ - -void ComboBox::enterEvent(QEnterEvent* event) { - QComboBox::enterEvent(event); - m_material.onEnterEvent(); -} - -void ComboBox::leaveEvent(QEvent* event) { - QComboBox::leaveEvent(event); - m_material.onLeaveEvent(); -} - -void ComboBox::mousePressEvent(QMouseEvent* event) { - QComboBox::mousePressEvent(event); - m_material.onMousePress(event->pos(), fieldRect()); -} - -void ComboBox::mouseReleaseEvent(QMouseEvent* event) { - QComboBox::mouseReleaseEvent(event); - m_material.onMouseRelease(); -} - -void ComboBox::focusInEvent(QFocusEvent* event) { - QComboBox::focusInEvent(event); - m_material.onFocusIn(); -} - -void ComboBox::focusOutEvent(QFocusEvent* event) { - QComboBox::focusOutEvent(event); - m_material.onFocusOut(); -} - -void ComboBox::changeEvent(QEvent* event) { - QComboBox::changeEvent(event); - if (event->type() == QEvent::EnabledChange) { - m_material.onEnabledChange(isEnabled()); - } -} - -void ComboBox::showPopup() { - // Get animation factory locally for custom arrow animation - auto factory = aex::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - // Start arrow rotation animation - if (factory) { - auto anim = factory->createPropertyAnimation(&m_arrowRotation, 0.0f, 180.0f, 200, - cf::ui::base::Easing::Type::Standard, this); - if (anim) { - // IMPORTANT: Update range to fix cached animation's stale from/to values - if (auto* propAnim = - dynamic_cast( - anim.Get())) { - propAnim->setRange(0.0f, 180.0f); - } - anim->start(); - } else { - m_arrowRotation = 180.0f; - } - } else { - m_arrowRotation = 180.0f; - } - - // Apply custom styling to the list view - QListView* listView = qobject_cast(view()); - if (listView) { - listView->setStyleSheet("QListView {" - " background-color: palette(base);" - " border: 1px solid palette(mid);" - " padding: 4px;" - "}" - "QListView::item {" - " padding: 12px 16px;" - " min-height: 40px;" - "}" - "QListView::item:hover {" - " background-color: palette(highlight);" - "}" - "QListView::item:selected {" - " background-color: palette(highlight);" - "}"); - } - - // First show the popup normally - QComboBox::showPopup(); - - // Get the popup container - if (QWidget* container = findChild(QLatin1String("QComboBoxPrivateContainer"))) { - m_popupContainer = container; - - // Store the target height - m_targetPopupHeight = container->height(); - - // Set initial height for drawer animation (start from 0) - container->setFixedHeight(0); - - // Stop any existing animation - if (m_popupAnimation) { - m_popupAnimation->stop(); - } - - // Setup drawer animation - expand from 0 to target height - m_popupAnimation->setTargetObject(container); - m_popupAnimation->setPropertyName("geometry"); - m_popupAnimation->setStartValue( - QRect(container->x(), container->y(), container->width(), 0)); - - // Calculate target position (drawer should expand downward from combo box bottom) - QRect targetRect = container->geometry(); - targetRect.setHeight(m_targetPopupHeight); - m_popupAnimation->setEndValue(targetRect); - - m_popupAnimation->start(QAbstractAnimation::DeleteWhenStopped); - } -} - -void ComboBox::hidePopup() { - // Stop any running popup animation - if (m_popupAnimation && m_popupAnimation->state() == QPropertyAnimation::Running) { - m_popupAnimation->stop(); - } - - // Get animation factory locally for custom arrow animation - auto factory = aex::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - // Reset arrow rotation animation - if (factory) { - auto anim = factory->createPropertyAnimation(&m_arrowRotation, m_arrowRotation, 0.0f, 150, - cf::ui::base::Easing::Type::Standard, this); - if (anim) { - // IMPORTANT: Update range to fix cached animation's stale from/to values - if (auto* propAnim = - dynamic_cast( - anim.Get())) { - propAnim->setRange(m_arrowRotation, 0.0f); - } - anim->start(); - } else { - m_arrowRotation = 0.0f; - } - } else { - m_arrowRotation = 0.0f; - } - - QComboBox::hidePopup(); - m_popupContainer = nullptr; -} - -// ============================================================================ -// Size Hints -// ============================================================================ - -QSize ComboBox::sizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Material Design 3 combo box specifications: - // - Field height: 56dp - // - Horizontal padding: 16dp - // - Arrow size: 24dp - // - Minimum width: based on text content - - float hPadding = helper.dpToPx(H_PADDING_DP); - float arrowWidth = helper.dpToPx(ARROW_SIZE_DP); - float textWidth = - fontMetrics().horizontalAdvance(currentText()) + hPadding * 2 + arrowWidth + 20; - - float height = helper.dpToPx(FIELD_HEIGHT_DP); - float minWidth = helper.dpToPx(DEFAULT_WIDTH_DP); - - return QSize(int(std::ceil(qMax(textWidth, minWidth))), int(std::ceil(height))); -} - -QSize ComboBox::minimumSizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - float height = helper.dpToPx(FIELD_HEIGHT_DP); - float minWidth = helper.dpToPx(MIN_WIDTH_DP); - - return QSize(int(std::ceil(minWidth)), int(std::ceil(height))); -} - -// ============================================================================ -// Color Access Methods -// ============================================================================ - -namespace { -// Fallback colors when theme is not available -inline CFColor fallbackPrimary() { - return CFColor(103, 80, 164); // Purple 700 -} -inline CFColor fallbackOnSurface() { - return CFColor(27, 27, 31); // On Surface -} -inline CFColor fallbackSurface() { - return CFColor(232, 226, 232); // Surface -} -inline CFColor fallbackOutline() { - return CFColor(120, 124, 132); // Outline -} -inline CFColor fallbackOnPrimaryContainer() { - return CFColor(233, 227, 235); // On Primary Container -} -inline CFColor fallbackPrimaryContainer() { - return CFColor(225, 218, 237); // Primary Container -} -} // namespace - -CFColor ComboBox::containerColor() const { - auto* app = application_support::Application::instance(); - if (!app) { - return variant_ == ComboBoxVariant::Filled ? fallbackPrimaryContainer() : fallbackSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - - if (variant_ == ComboBoxVariant::Filled) { - return CFColor(colorScheme.queryColor(PRIMARY_CONTAINER)); - } - return CFColor(colorScheme.queryColor(SURFACE)); - } catch (...) { - return variant_ == ComboBoxVariant::Filled ? fallbackPrimaryContainer() : fallbackSurface(); - } -} - -CFColor ComboBox::labelColor() const { - auto* app = application_support::Application::instance(); - if (!app) { - return variant_ == ComboBoxVariant::Filled ? fallbackOnPrimaryContainer() - : fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - - if (variant_ == ComboBoxVariant::Filled) { - return CFColor(colorScheme.queryColor(ON_PRIMARY_CONTAINER)); - } - return CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - return variant_ == ComboBoxVariant::Filled ? fallbackOnPrimaryContainer() - : fallbackOnSurface(); - } -} - -CFColor ComboBox::outlineColor() const { - auto* app = application_support::Application::instance(); - if (!app) { - return fallbackOutline(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(OUTLINE)); - } catch (...) { - return fallbackOutline(); - } -} - -CFColor ComboBox::stateLayerColor() const { - return labelColor(); -} - -CFColor ComboBox::arrowColor() const { - return labelColor(); -} - -float ComboBox::cornerRadius() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(CORNER_RADIUS_DP); -} - -float ComboBox::arrowRotation() const { - return m_arrowRotation; -} - -// ============================================================================ -// Layout Helpers -// ============================================================================ - -QRectF ComboBox::fieldRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float height = helper.dpToPx(FIELD_HEIGHT_DP); - return QRectF(0, 0, width(), height); -} - -QRectF ComboBox::textRect() const { - QRectF field = fieldRect(); - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float hPadding = helper.dpToPx(H_PADDING_DP); - float arrowWidth = helper.dpToPx(ARROW_SIZE_DP); - - float x = field.left() + hPadding; - float y = field.top(); - float w = field.width() - 2 * hPadding - arrowWidth - hPadding; - float h = field.height(); - - return QRectF(x, y, w, h); -} - -QRectF ComboBox::arrowRect() const { - QRectF field = fieldRect(); - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float arrowSize = helper.dpToPx(ARROW_SIZE_DP); - - float x = field.right() - arrowSize - helper.dpToPx(H_PADDING_DP); - float y = (field.height() - arrowSize) / 2.0f; - - return QRectF(x, y, arrowSize, arrowSize); -} - -// ============================================================================ -// Paint Event -// ============================================================================ - -void ComboBox::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - QRectF field = fieldRect(); - QPainterPath shape = roundedRect(field, cornerRadius()); - - // Step 1: Draw background (for filled variant) - if (variant_ == ComboBoxVariant::Filled) { - drawBackground(p, shape); - } - - // Step 2: Draw outline (for outlined variant or all) - drawOutline(p, shape); - - // Step 3: Draw state layer - drawStateLayer(p, shape); - - // Step 4: Draw ripple - drawRipple(p, shape); - - // Step 5: Draw text - drawText(p, textRect()); - - // Step 6: Draw arrow - drawArrow(p, arrowRect()); - - // Step 7: Draw focus indicator - drawFocusIndicator(p, shape); -} - -// ============================================================================ -// Drawing Helpers -// ============================================================================ - -void ComboBox::drawBackground(QPainter& p, const QPainterPath& shape) { - if (variant_ != ComboBoxVariant::Filled) { - return; - } - - CFColor bgColor = containerColor(); - QColor color = bgColor.native_color(); - - // Handle disabled state - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - p.fillPath(shape, color); -} - -void ComboBox::drawOutline(QPainter& p, const QPainterPath& shape) { - CFColor oColor = outlineColor(); - QColor color = oColor.native_color(); - - // Handle disabled state - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - // For filled variant, outline is only visible when hovered/focused - if (variant_ == ComboBoxVariant::Filled) { - if (m_material.stateMachine() && isEnabled()) { - float opacity = m_material.stateMachine()->stateLayerOpacity(); - if (opacity > 0.0f) { - color.setAlphaF(opacity); - } else { - return; // No outline when not interacting - } - } else { - return; - } - } - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float inset = helper.dpToPx(OUTLINE_WIDTH_DP) / 2.0f; - QPainterPath insetShape = - roundedRect(fieldRect().adjusted(inset, inset, -inset, -inset), cornerRadius() - inset); - - QPen pen(color, helper.dpToPx(OUTLINE_WIDTH_DP)); - pen.setCosmetic(false); - p.setPen(pen); - p.setBrush(Qt::NoBrush); - p.drawPath(insetShape); -} - -void ComboBox::drawStateLayer(QPainter& p, const QPainterPath& shape) { - if (!m_material.stateMachine() || !isEnabled()) { - return; - } - - float opacity = m_material.stateMachine()->stateLayerOpacity(); - if (opacity <= 0.0f) { - return; - } - - CFColor stateColor = stateLayerColor(); - QColor stateQColor = stateColor.native_color(); - stateQColor.setAlphaF(opacity * 0.08f); // State layer has reduced opacity - - p.fillPath(shape, stateQColor); -} - -void ComboBox::drawRipple(QPainter& p, const QPainterPath& shape) { - if (m_material.ripple()) { - // Update ripple color based on current state - m_material.ripple()->setColor(stateLayerColor()); - - m_material.ripple()->paint(&p, shape); - } -} - -void ComboBox::drawText(QPainter& p, const QRectF& rect) { - CFColor textColor = labelColor(); - QColor color = textColor.native_color(); - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - p.setPen(color); - p.setFont(font()); - - // Draw text vertically centered, left aligned - QRectF textBounds = rect.adjusted(0, 2, 0, -2); - p.drawText(textBounds, Qt::AlignLeft | Qt::AlignVCenter, currentText()); -} - -void ComboBox::drawArrow(QPainter& p, const QRectF& rect) { - CFColor arrowColor = this->arrowColor(); - QColor color = arrowColor.native_color(); - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - QPen pen(color, 2); - pen.setCapStyle(Qt::RoundCap); - pen.setJoinStyle(Qt::RoundJoin); - p.setPen(pen); - - // Draw dropdown arrow (downward pointing triangle) - float cx = rect.center().x(); - float cy = rect.center().y(); - float size = 5.0f; // Arrow size - - // Apply rotation transformation - p.save(); - p.translate(cx, cy); - p.rotate(arrowRotation()); - - // Draw arrow as three connected lines - QPointF points[3] = {QPointF(-size, -size / 2.0f), QPointF(0, size / 2.0f), - QPointF(size, -size / 2.0f)}; - - for (int i = 0; i < 2; ++i) { - p.drawLine(points[i], points[i + 1]); - } - - p.restore(); -} - -void ComboBox::drawFocusIndicator(QPainter& p, const QPainterPath& shape) { - if (m_material.focusIndicator()) { - // Expand rect slightly for focus ring - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float margin = helper.dpToPx(FOCUS_RING_MARGIN_DP); - QRectF field = fieldRect(); - QRectF focusRect = field.adjusted(-margin, -margin, margin, margin); - - QPainterPath focusShape = roundedRect(focusRect, cornerRadius() + margin); - m_material.focusIndicator()->paint(&p, focusShape, containerColor()); - } -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/comboBox/combobox.h b/ui/widget/material/widget/comboBox/combobox.h deleted file mode 100644 index f23b13d99..000000000 --- a/ui/widget/material/widget/comboBox/combobox.h +++ /dev/null @@ -1,294 +0,0 @@ -/** - * @file ui/widget/material/widget/comboBox/combobox.h - * @brief Material Design 3 ComboBox widget. - * - * Implements Material Design 3 combo box (dropdown) with filled and - * outlined variants, animated dropdown arrow, and custom list styling. - * - * @author CFDesktop Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include "base/color.h" -#include "export.h" -#include "widget/material/base/material_widget_base.h" -#include -#include -#include - -namespace cf::ui::widget::material { - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 ComboBox widget. - * - * @details Implements Material Design 3 combo box (dropdown) with filled - * and outlined variants, animated dropdown arrow, and custom list styling. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT ComboBox : public QComboBox { - Q_OBJECT - - public: - /** - * @brief ComboBox visual variant. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - enum class ComboBoxVariant { - Filled, ///< Filled background with border - Outlined ///< Outlined border only - }; - Q_ENUM(ComboBoxVariant) - - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note Defaults to Filled variant. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit ComboBox(QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~ComboBox() override; - - /** - * @brief Gets the combo box variant. - * - * @return Current combo box variant. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ComboBoxVariant variant() const; - - /** - * @brief Sets the combo box variant. - * - * @param[in] variant ComboBox variant to use. - * - * @throws None - * @note Changing variant updates the visual appearance. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setVariant(ComboBoxVariant variant); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the combo box. - * - * @throws None - * @note Based on Material Design specifications. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures touch target size requirements. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - protected: - /** - * @brief Paints the combo box. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements Material Design paint pipeline. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles mouse enter event. - * - * @param[in] event Enter event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void enterEvent(QEnterEvent* event) override; - - /** - * @brief Handles mouse leave event. - * - * @param[in] event Leave event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void leaveEvent(QEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple and press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Shows the popup list. - * - * @throws None - * @note Override to apply custom styling to popup. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void showPopup() override; - - /** - * @brief Hides the popup list. - * - * @throws None - * @note Override to reset arrow animation. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void hidePopup() override; - - private: - // Drawing helpers - Material Design paint pipeline - QRectF fieldRect() const; - QRectF textRect() const; - QRectF arrowRect() const; - void drawBackground(QPainter& p, const QPainterPath& shape); - void drawOutline(QPainter& p, const QPainterPath& shape); - void drawStateLayer(QPainter& p, const QPainterPath& shape); - void drawRipple(QPainter& p, const QPainterPath& shape); - void drawText(QPainter& p, const QRectF& rect); - void drawArrow(QPainter& p, const QRectF& rect); - void drawFocusIndicator(QPainter& p, const QPainterPath& shape); - - // Color access methods - CFColor containerColor() const; - CFColor labelColor() const; - CFColor outlineColor() const; - CFColor stateLayerColor() const; - CFColor arrowColor() const; - float cornerRadius() const; - - // Helper to get arrow rotation angle - float arrowRotation() const; - - // Behavior components - base::MaterialWidgetBase m_material; - - // Variant - ComboBoxVariant variant_; - - // Arrow rotation (0 = down, 180 = up) - float m_arrowRotation = 0.0f; - - // Drawer animation - QPropertyAnimation* m_popupAnimation = nullptr; - QWidget* m_popupContainer = nullptr; - int m_targetPopupHeight = 0; -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/doublespinbox/doublespinbox.cpp b/ui/widget/material/widget/doublespinbox/doublespinbox.cpp deleted file mode 100644 index ab8cea448..000000000 --- a/ui/widget/material/widget/doublespinbox/doublespinbox.cpp +++ /dev/null @@ -1,655 +0,0 @@ -/** - * @file doublespinbox.cpp - * @brief Material Design 3 DoubleSpinBox Implementation - * - * Implements a Material Design 3 double spin box with floating-point input - * support. Features increment/decrement buttons, outline style, focus - * indicator, and state layer effects. - * - * @author CFDesktop Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "doublespinbox.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "base/geometry_helper.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::base::geometry; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::material::base; -using namespace cf::ui::widget::application_support; - -// ============================================================================ -// Constants (Material Design 3 specifications) -// ============================================================================ - -namespace { -// Fallback colors when theme is not available -inline CFColor fallbackSurface() { - return CFColor(253, 253, 253); -} // Surface -inline CFColor fallbackOnSurface() { - return CFColor(27, 27, 31); -} // On Surface -inline CFColor fallbackOnSurfaceVariant() { - return CFColor(75, 75, 80); -} // On Surface Variant -inline CFColor fallbackOutline() { - return CFColor(120, 124, 132); -} // Outline -inline CFColor fallbackPrimary() { - return CFColor(103, 80, 164); -} // Purple 700 -} // namespace - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -DoubleSpinBox::DoubleSpinBox(QWidget* parent) - : QDoubleSpinBox(parent), m_material(this, - MaterialWidgetBase::Config{ - .useRipple = true, - .useElevation = true, - .useFocusIndicator = true, - .initialElevation = 0, - }), - m_hoveringIncrementButton(false), m_hoveringDecrementButton(false), - m_pressingIncrementButton(false), m_pressingDecrementButton(false) { - - // Disable native frame and background - setFrame(false); - setAttribute(Qt::WA_TranslucentBackground); - // Hide the native up/down buttons - we'll draw our own Material Design buttons - // Keyboard up/down arrow keys will still work - setButtonSymbols(QDoubleSpinBox::NoButtons); - - // Make internal lineEdit transparent and borderless so it doesn't cover our custom drawing - if (lineEdit()) { - lineEdit()->setFrame(false); - lineEdit()->setAttribute(Qt::WA_TranslucentBackground); - lineEdit()->setStyleSheet("QLineEdit { background: transparent; border: none; }"); - // Set alignment to right-align numbers (standard for numeric input) - lineEdit()->setAlignment(Qt::AlignRight); - } - - // Set default font - setFont(textFont()); - - // Set cursor - setCursor(Qt::IBeamCursor); - - // Set initial text color for lineEdit - updateTextColor(); -} - -DoubleSpinBox::~DoubleSpinBox() { - // Components are parented to this, Qt will delete them automatically -} - -// ============================================================================ -// Event Handlers -// ============================================================================ - -void DoubleSpinBox::enterEvent(QEnterEvent* event) { - QDoubleSpinBox::enterEvent(event); - m_material.onEnterEvent(); -} - -void DoubleSpinBox::leaveEvent(QEvent* event) { - QDoubleSpinBox::leaveEvent(event); - m_material.onLeaveEvent(); - - // Reset button hover states - m_hoveringIncrementButton = false; - m_hoveringDecrementButton = false; -} - -void DoubleSpinBox::mousePressEvent(QMouseEvent* event) { - // Check if button area was clicked - if (isOverButtons(event->pos())) { - if (isOverIncrementButton(event->pos())) { - m_pressingIncrementButton = true; - // Trigger increment - stepUp(); - } else if (isOverDecrementButton(event->pos())) { - m_pressingDecrementButton = true; - // Trigger decrement - stepDown(); - } - update(); - return; - } - - QDoubleSpinBox::mousePressEvent(event); - m_material.onMousePress(event->pos(), rect()); -} - -void DoubleSpinBox::mouseReleaseEvent(QMouseEvent* event) { - // Reset button press states - if (m_pressingIncrementButton || m_pressingDecrementButton) { - m_pressingIncrementButton = false; - m_pressingDecrementButton = false; - update(); - return; - } - - QDoubleSpinBox::mouseReleaseEvent(event); - m_material.onMouseRelease(); -} - -void DoubleSpinBox::mouseMoveEvent(QMouseEvent* event) { - QDoubleSpinBox::mouseMoveEvent(event); - updateButtonHoverState(event->pos()); -} - -void DoubleSpinBox::focusInEvent(QFocusEvent* event) { - QDoubleSpinBox::focusInEvent(event); - m_material.onFocusIn(); -} - -void DoubleSpinBox::focusOutEvent(QFocusEvent* event) { - QDoubleSpinBox::focusOutEvent(event); - m_material.onFocusOut(); -} - -void DoubleSpinBox::changeEvent(QEvent* event) { - QDoubleSpinBox::changeEvent(event); - if (event->type() == QEvent::EnabledChange) { - m_material.onEnabledChange(isEnabled()); - updateTextColor(); - update(); - } -} - -void DoubleSpinBox::resizeEvent(QResizeEvent* event) { - QDoubleSpinBox::resizeEvent(event); - // Constrain internal lineEdit to text area only, so it doesn't cover button area - if (lineEdit()) { - lineEdit()->setGeometry(textRect().toRect()); - } -} - -// ============================================================================ -// Paint Event -// ============================================================================ - -void DoubleSpinBox::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - QRectF content = contentRect(); - QPainterPath shape = shapePath(); - - // Step 1: Draw background - drawBackground(p, shape); - - // Step 2: Draw state layer - drawStateLayer(p, shape); - - // Step 3: Draw ripple - drawRipple(p, shape); - - // Step 4: Draw outline - drawOutline(p, shape); - - // Note: Text is drawn by the internal lineEdit widget, not here - - // Step 5: Draw buttons - drawButtons(p, incrementButtonRect().united(decrementButtonRect())); - - // Step 6: Draw focus indicator - drawFocusIndicator(p, shape); -} - -// ============================================================================ -// Size Hints -// ============================================================================ - -QSize DoubleSpinBox::sizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Material Design spin box specifications: - // - Height: 56dp - // - Horizontal padding: 16dp - // - Button width: 40dp - float height = helper.dpToPx(56.0f); - float hPadding = helper.dpToPx(16.0f); - float buttonWidth = helper.dpToPx(40.0f); - - // Estimate text width based on decimals and range - QFontMetricsF fm(font()); - QString sampleText = QString("-%1.%2") - .arg(qAbs(maximum()), 0, 'f', decimals()) - .arg(QString('0').repeated(decimals())); - float textWidth = fm.horizontalAdvance(sampleText); - - float contentWidth = hPadding * 2 + textWidth + buttonWidth; - - return QSize(int(std::ceil(contentWidth)), int(std::ceil(height))); -} - -QSize DoubleSpinBox::minimumSizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - float height = helper.dpToPx(56.0f); - float minWidth = helper.dpToPx(120.0f); // Minimum usable width - - return QSize(int(std::ceil(minWidth)), int(std::ceil(height))); -} - -// ============================================================================ -// Color Access Methods -// ============================================================================ - -CFColor DoubleSpinBox::containerColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(SURFACE)); - } catch (...) { - return fallbackSurface(); - } -} - -CFColor DoubleSpinBox::textColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - return fallbackOnSurface(); - } -} - -CFColor DoubleSpinBox::stateLayerColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - return fallbackOnSurface(); - } -} - -CFColor DoubleSpinBox::outlineColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOutline(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(OUTLINE)); - } catch (...) { - return fallbackOutline(); - } -} - -CFColor DoubleSpinBox::focusOutlineColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackPrimary(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(PRIMARY)); - } catch (...) { - return fallbackPrimary(); - } -} - -CFColor DoubleSpinBox::buttonIconColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurfaceVariant(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE_VARIANT)); - } catch (...) { - return fallbackOnSurfaceVariant(); - } -} - -float DoubleSpinBox::cornerRadius() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - // Small corner radius (4dp) as per Material Design 3 - return helper.dpToPx(4.0f); -} - -QFont DoubleSpinBox::textFont() const { - auto* app = Application::instance(); - if (!app) { - QFont font = QDoubleSpinBox::font(); - font.setPixelSize(16); - return font; - } - - try { - const auto& theme = app->currentTheme(); - auto& fontType = theme.font_type(); - return fontType.queryTargetFont("bodyLarge"); - } catch (...) { - QFont font = QDoubleSpinBox::font(); - font.setPixelSize(16); - return font; - } -} - -// ============================================================================ -// Layout Helpers -// ============================================================================ - -QRectF DoubleSpinBox::contentRect() const { - return QRectF(rect()); -} - -QRectF DoubleSpinBox::textRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - QRectF content = contentRect(); - float hPadding = helper.dpToPx(16.0f); - float buttonWidth = helper.dpToPx(40.0f); - - float left = hPadding; - float top = 0; - float availableWidth = content.width() - hPadding * 2 - buttonWidth; - float height = content.height(); - - return QRectF(left, top, availableWidth, height); -} - -QRectF DoubleSpinBox::incrementButtonRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - QRectF content = contentRect(); - float buttonWidth = helper.dpToPx(40.0f); - - float right = content.width(); - float buttonHeight = content.height() / 2.0f; - - return QRectF(right - buttonWidth, 0, buttonWidth, buttonHeight); -} - -QRectF DoubleSpinBox::decrementButtonRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - QRectF content = contentRect(); - float buttonWidth = helper.dpToPx(40.0f); - float buttonHeight = content.height() / 2.0f; - - float right = content.width(); - float top = buttonHeight; - - return QRectF(right - buttonWidth, top, buttonWidth, buttonHeight); -} - -QPainterPath DoubleSpinBox::shapePath() const { - QRectF content = contentRect(); - return roundedRect(content, cornerRadius()); -} - -// ============================================================================ -// Button Helpers -// ============================================================================ - -bool DoubleSpinBox::isOverIncrementButton(const QPoint& pos) const { - return incrementButtonRect().contains(pos); -} - -bool DoubleSpinBox::isOverDecrementButton(const QPoint& pos) const { - return decrementButtonRect().contains(pos); -} - -bool DoubleSpinBox::isOverButtons(const QPoint& pos) const { - return isOverIncrementButton(pos) || isOverDecrementButton(pos); -} - -void DoubleSpinBox::updateButtonHoverState(const QPoint& pos) { - bool wasHoveringIncrement = m_hoveringIncrementButton; - bool wasHoveringDecrement = m_hoveringDecrementButton; - - m_hoveringIncrementButton = isOverIncrementButton(pos); - m_hoveringDecrementButton = isOverDecrementButton(pos); - - if (wasHoveringIncrement != m_hoveringIncrementButton || - wasHoveringDecrement != m_hoveringDecrementButton) { - update(); - } -} - -// ============================================================================ -// Drawing Helpers -// ============================================================================ - -void DoubleSpinBox::drawBackground(QPainter& p, const QPainterPath& shape) { - CFColor bg = containerColor(); - - // Handle disabled state - if (!isEnabled()) { - QColor color = bg.native_color(); - color.setAlphaF(0.38f); // 38% opacity for disabled - p.fillPath(shape, color); - return; - } - - p.fillPath(shape, bg.native_color()); -} - -void DoubleSpinBox::drawStateLayer(QPainter& p, const QPainterPath& shape) { - if (!isEnabled() || !m_material.stateMachine()) { - return; - } - - float opacity = m_material.stateMachine()->stateLayerOpacity(); - if (opacity <= 0.0f) { - return; - } - - CFColor stateColor = stateLayerColor(); - QColor color = stateColor.native_color(); - color.setAlphaF(color.alphaF() * opacity); - - p.fillPath(shape, color); -} - -void DoubleSpinBox::drawRipple(QPainter& p, const QPainterPath& shape) { - if (m_material.ripple()) { - // Set ripple color based on text color - m_material.ripple()->setColor(textColor()); - m_material.ripple()->paint(&p, shape); - } -} - -void DoubleSpinBox::drawOutline(QPainter& p, const QPainterPath& shape) { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float inset = 0.5f; - - QColor color; - if (hasFocus()) { - color = focusOutlineColor().native_color(); - } else { - color = outlineColor().native_color(); - } - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - p.save(); - - // Create inset path for outline - QRectF shapeBounds = shape.boundingRect(); - QRectF insetRect = shapeBounds.adjusted(inset, inset, -inset, -inset); - float adjustedRadius = std::max(0.0f, cornerRadius() - inset); - QPainterPath insetShape = roundedRect(insetRect, adjustedRadius); - - // Calculate outline width - float baseWidth = helper.dpToPx(1.0f); - float activeWidth = helper.dpToPx(2.0f); - float currentWidth = hasFocus() ? activeWidth : baseWidth; - - QPen pen(color, currentWidth); - pen.setCosmetic(true); // Keep consistent width across DPI - p.setPen(pen); - p.setBrush(Qt::NoBrush); - p.drawPath(insetShape); - - p.restore(); -} - -void DoubleSpinBox::drawText(QPainter& p, const QRectF& textRect) { - // The internal lineEdit handles text display and cursor rendering. - // This function is now a no-op - color updates are handled by updateTextColor(). - Q_UNUSED(p) - Q_UNUSED(textRect) -} - -void DoubleSpinBox::updateTextColor() { - // Update the lineEdit text color based on current state - if (lineEdit()) { - QPalette pal = lineEdit()->palette(); - QColor textColorVal = textColor().native_color(); - if (!isEnabled()) { - textColorVal.setAlphaF(0.38f); - } - pal.setColor(QPalette::Text, textColorVal); - pal.setColor(QPalette::WindowText, textColorVal); - lineEdit()->setPalette(pal); - } -} - -void DoubleSpinBox::drawButtons(QPainter& p, const QRectF& buttonRect) { - Q_UNUSED(buttonRect) - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Draw increment button - QRectF incRect = incrementButtonRect(); - if (!incRect.isEmpty()) { - p.save(); - - // Hover overlay for increment button - if (m_hoveringIncrementButton || m_pressingIncrementButton) { - QColor overlay = buttonIconColor().native_color(); - overlay.setAlphaF(m_pressingIncrementButton ? 0.12f : 0.08f); - p.fillRect(incRect, overlay); - } - - // Draw plus icon - QColor iconColor = buttonIconColor().native_color(); - if (!isEnabled()) { - iconColor.setAlphaF(0.38f); - } - - p.setPen(QPen(iconColor, helper.dpToPx(2.0f))); - p.setRenderHint(QPainter::Antialiasing); - - float cx = incRect.center().x(); - float cy = incRect.center().y(); - float size = helper.dpToPx(16.0f); - float half = size / 2.0f; - - // Draw plus sign - p.drawLine(QPointF(cx - half, cy), QPointF(cx + half, cy)); // Horizontal - p.drawLine(QPointF(cx, cy - half), QPointF(cx, cy + half)); // Vertical - - p.restore(); - } - - // Draw decrement button - QRectF decRect = decrementButtonRect(); - if (!decRect.isEmpty()) { - p.save(); - - // Hover overlay for decrement button - if (m_hoveringDecrementButton || m_pressingDecrementButton) { - QColor overlay = buttonIconColor().native_color(); - overlay.setAlphaF(m_pressingDecrementButton ? 0.12f : 0.08f); - p.fillRect(decRect, overlay); - } - - // Draw minus icon - QColor iconColor = buttonIconColor().native_color(); - if (!isEnabled()) { - iconColor.setAlphaF(0.38f); - } - - p.setPen(QPen(iconColor, helper.dpToPx(2.0f))); - p.setRenderHint(QPainter::Antialiasing); - - float cx = decRect.center().x(); - float cy = decRect.center().y(); - float size = helper.dpToPx(16.0f); - float half = size / 2.0f; - - // Draw minus sign (horizontal only) - p.drawLine(QPointF(cx - half, cy), QPointF(cx + half, cy)); - - p.restore(); - } - - // Draw separator line between buttons - p.save(); - QColor separatorColor = outlineColor().native_color(); - if (!isEnabled()) { - separatorColor.setAlphaF(0.38f); - } - - float separatorX = incrementButtonRect().left(); - float separatorY = incrementButtonRect().bottom(); - float separatorWidth = helper.dpToPx(1.0f); - QRectF separatorRect(separatorX, separatorY - separatorWidth / 2, incrementButtonRect().width(), - separatorWidth); - - p.fillRect(separatorRect, separatorColor); - p.restore(); -} - -void DoubleSpinBox::drawFocusIndicator(QPainter& p, const QPainterPath& shape) { - if (m_material.focusIndicator() && hasFocus()) { - m_material.focusIndicator()->paint(&p, shape, focusOutlineColor()); - } -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/doublespinbox/doublespinbox.h b/ui/widget/material/widget/doublespinbox/doublespinbox.h deleted file mode 100644 index 41c5a1bca..000000000 --- a/ui/widget/material/widget/doublespinbox/doublespinbox.h +++ /dev/null @@ -1,272 +0,0 @@ -/** - * @file ui/widget/material/widget/doublespinbox/doublespinbox.h - * @brief Material Design 3 DoubleSpinBox widget. - * - * Implements Material Design 3 double spin box with support for floating-point - * input, increment/decrement buttons, outline style, focus indicator, and - * state layer effects. - * - * @author CFDesktop Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include -#include - -#include "base/color.h" -#include "export.h" -#include "widget/material/base/material_widget_base.h" -#include -#include - -namespace cf::ui::widget::material { - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 DoubleSpinBox widget. - * - * @details Implements Material Design 3 double spin box with support for - * floating-point input, increment/decrement buttons, outline style, - * focus indicator, and state layer effects following Material Design - * 3 specifications. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT DoubleSpinBox : public QDoubleSpinBox { - Q_OBJECT - - public: - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit DoubleSpinBox(QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~DoubleSpinBox() override; - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the spin box. - * - * @throws None - * @note Based on content width, button width, and padding. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures touch target size requirements. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - protected: - /** - * @brief Paints the spin box. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements 7-step Material Design paint pipeline. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles mouse enter event. - * - * @param[in] event Enter event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void enterEvent(QEnterEvent* event) override; - - /** - * @brief Handles mouse leave event. - * - * @param[in] event Leave event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void leaveEvent(QEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple and press state for button area. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse move event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Tracks hover state over button areas. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseMoveEvent(QMouseEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Handles widget resize event. - * - * @param[in] event Resize event. - * - * @throws None - * @note Constrains internal lineEdit to text area only. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void resizeEvent(QResizeEvent* event) override; - - private: - // Drawing helpers - 7-step paint pipeline - void drawBackground(QPainter& p, const QPainterPath& shape); - void drawStateLayer(QPainter& p, const QPainterPath& shape); - void drawRipple(QPainter& p, const QPainterPath& shape); - void drawOutline(QPainter& p, const QPainterPath& shape); - void drawText(QPainter& p, const QRectF& textRect); - void drawButtons(QPainter& p, const QRectF& buttonRect); - void drawFocusIndicator(QPainter& p, const QPainterPath& shape); - - // Layout helpers - QRectF contentRect() const; - QRectF textRect() const; - QRectF incrementButtonRect() const; - QRectF decrementButtonRect() const; - QPainterPath shapePath() const; - - // Button helpers - bool isOverIncrementButton(const QPoint& pos) const; - bool isOverDecrementButton(const QPoint& pos) const; - bool isOverButtons(const QPoint& pos) const; - void updateButtonHoverState(const QPoint& pos); - - // Update lineEdit text color based on current state - void updateTextColor(); - - // Color access methods - CFColor containerColor() const; - CFColor textColor() const; - CFColor stateLayerColor() const; - CFColor outlineColor() const; - CFColor focusOutlineColor() const; - CFColor buttonIconColor() const; - float cornerRadius() const; - QFont textFont() const; - - // Behavior components - base::MaterialWidgetBase m_material; - - // Internal state - bool m_hoveringIncrementButton; - bool m_hoveringDecrementButton; - bool m_pressingIncrementButton; - bool m_pressingDecrementButton; -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/groupbox/groupbox.cpp b/ui/widget/material/widget/groupbox/groupbox.cpp deleted file mode 100644 index bb5a9a4e0..000000000 --- a/ui/widget/material/widget/groupbox/groupbox.cpp +++ /dev/null @@ -1,483 +0,0 @@ -/** - * @file groupbox.cpp - * @brief Material Design 3 GroupBox Implementation - * - * Implements a Material Design 3 group box with rounded corners, - * optional elevation shadows, and theme-aware colors. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "groupbox.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "base/geometry_helper.h" -#include "cfmaterial_animation_factory.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/elevation_controller.h" - -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::base::geometry; -using namespace cf::ui::components; -using namespace cf::ui::components::material; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::material::base; -using namespace cf::ui::widget::application_support; - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -GroupBox::GroupBox(QWidget* parent) : QGroupBox(parent), m_cornerRadius(-1.0f), m_hasBorder(true) { - // Get animation factory from Application - m_animationFactory = - aex::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_elevation = new MdElevationController(m_animationFactory, this); - - // Set initial elevation (default: level 1 for subtle depth) - m_elevation->setElevation(1); - - // Enable custom drawing without automatic clipping - setAttribute(Qt::WA_OpaquePaintEvent, false); - setAttribute(Qt::WA_TranslucentBackground, false); - - // Disable automatic background drawing to avoid double-draw issues - setAttribute(Qt::WA_StyledBackground, false); - - // Check if title exists - m_hasTitle = !title().isEmpty(); - - // Set initial content margins to ensure proper layout - updateContentMargins(); -} - -GroupBox::GroupBox(const QString& title, QWidget* parent) : GroupBox(parent) { - setTitle(title); - m_hasTitle = !title.isEmpty(); - // Update margins since we now have a title - updateContentMargins(); -} - -GroupBox::~GroupBox() { - // Components are parented to this, Qt will delete them automatically -} - -// ============================================================================ -// Property Getters/Setters -// ============================================================================ - -int GroupBox::elevation() const { - return m_elevation ? m_elevation->elevation() : 0; -} - -void GroupBox::setElevation(int level) { - if (m_elevation) { - m_elevation->setElevation(level); - updateContentMargins(); // Shadow margin changed, update layout - update(); - } -} - -float GroupBox::cornerRadius() const { - // If corner radius is not explicitly set (negative), use default value - if (m_cornerRadius < 0.0f) { - // Use Material Design ShapeSmall (8dp) as default - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(8.0f); - } - return m_cornerRadius; -} - -void GroupBox::setCornerRadius(float radius) { - if (m_cornerRadius != radius) { - m_cornerRadius = radius; - update(); - } -} - -bool GroupBox::hasBorder() const { - return m_hasBorder; -} - -void GroupBox::setHasBorder(bool hasBorder) { - if (m_hasBorder != hasBorder) { - m_hasBorder = hasBorder; - update(); - } -} - -// ============================================================================ -// Size Hints -// ============================================================================ - -QSize GroupBox::sizeHint() const { - // QGroupBox's sizeHint already includes our contentsMargins, - // which we've set to include shadow and title space in updateContentMargins() - return QGroupBox::sizeHint(); -} - -QSize GroupBox::minimumSizeHint() const { - // QGroupBox's minimumSizeHint already includes our contentsMargins - return QGroupBox::minimumSizeHint(); -} - -// ============================================================================ -// Color Access Methods -// ============================================================================ - -namespace { -// Fallback colors when theme is not available -inline CFColor fallbackSurface() { - return CFColor(253, 253, 253); -} // Surface -inline CFColor fallbackSurfaceVariant() { - return CFColor(231, 224, 236); -} // Surface Variant -inline CFColor fallbackOutline() { - return CFColor(120, 124, 132); -} // Outline -inline CFColor fallbackOnSurface() { - return CFColor(27, 27, 31); -} // On Surface -} // namespace - -CFColor GroupBox::surfaceColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(SURFACE)); - } catch (...) { - return fallbackSurface(); - } -} - -CFColor GroupBox::outlineColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOutline(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(OUTLINE)); - } catch (...) { - return fallbackOutline(); - } -} - -CFColor GroupBox::titleColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - return fallbackOnSurface(); - } -} - -QFont GroupBox::titleFont() const { - auto* app = Application::instance(); - if (!app) { - // Fallback to system font with reasonable size - QFont font = QGroupBox::font(); - font.setPixelSize(14); - font.setWeight(QFont::Medium); - return font; - } - - try { - const auto& theme = app->currentTheme(); - auto& fontType = theme.font_type(); - return fontType.queryTargetFont("titleSmall"); - } catch (...) { - QFont font = QGroupBox::font(); - font.setPixelSize(14); - font.setWeight(QFont::Medium); - return font; - } -} - -// ============================================================================ -// Child Event -// ============================================================================ - -void GroupBox::childEvent(QChildEvent* event) { - QGroupBox::childEvent(event); - // Update when children change to ensure proper redraw - if (event->type() == QChildEvent::ChildAdded || event->type() == QChildEvent::ChildRemoved) { - updateGeometry(); - update(); - } -} - -// ============================================================================ -// Change Event -// ============================================================================ - -void GroupBox::changeEvent(QEvent* event) { - QGroupBox::changeEvent(event); - if (event->type() == QEvent::WindowTitleChange) { - m_hasTitle = !title().isEmpty(); - updateContentMargins(); - update(); - } -} - -// ============================================================================ -// Resize Event -// ============================================================================ - -void GroupBox::resizeEvent(QResizeEvent* event) { - QGroupBox::resizeEvent(event); - // Update content margins on resize to ensure proper layout - updateContentMargins(); -} - -// ============================================================================ -// Layout Helpers -// ============================================================================ - -void GroupBox::updateContentMargins() { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Calculate shadow margin (extra space needed for shadow rendering) - QMarginsF shadowMargin = this->shadowMargin(); - int shadowLeft = int(std::ceil(shadowMargin.left())); - int shadowTop = int(std::ceil(shadowMargin.top())); - int shadowRight = int(std::ceil(shadowMargin.right())); - int shadowBottom = int(std::ceil(shadowMargin.bottom())); - - // Calculate title area height (space needed for title at the top) - int titleTopSpace = 0; - if (m_hasTitle) { - QFont font = titleFont(); - QFontMetrics fm(font); - float titleTopPadding = helper.dpToPx(8.0f); // Distance from top of contentRect - float titleHeight = fm.height(); - float titleBottomGap = helper.dpToPx(4.0f); // Extra gap below title - titleTopSpace = int(std::ceil(titleTopPadding + titleHeight + titleBottomGap)); - } - - // Additional padding to keep content away from rounded corners - // This prevents child widgets from drawing into the corner curve area - float radius = cornerRadius(); - int cornerAvoidance = int(std::ceil(radius * 0.6f)); // Keep 60% of radius as clearance - - // Standard content padding - int contentPadding = int(std::ceil(helper.dpToPx(16.0f))); - - // Set content margins: - // - Left/Right: shadow margin + corner avoidance + standard content padding - // - Top: shadow margin + title space + standard content padding - // - Bottom: shadow margin + standard content padding - setContentsMargins( - shadowLeft + cornerAvoidance + contentPadding, shadowTop + titleTopSpace + contentPadding, - shadowRight + cornerAvoidance + contentPadding, shadowBottom + contentPadding); -} - -// ============================================================================ -// Paint Event -// ============================================================================ - -void GroupBox::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - // Calculate content area (inset to make room for shadow) - QMarginsF margin = shadowMargin(); - QRectF contentRect = - QRectF(rect()).adjusted(margin.left(), margin.top(), -margin.right(), -margin.bottom()); - - // Create shape path (rounded corners) from contentRect - QPainterPath shape = roundedRect(contentRect, cornerRadius()); - - // Step 1: Draw shadow (outside contentRect) - drawShadow(p, contentRect, shape); - - // Step 2: Draw background - drawBackground(p, shape); - - // Step 3: Draw border (with title gap if has title) - if (m_hasBorder) { - drawBorder(p, contentRect, shape); - } - - // Step 4: Draw title (straddling the top border) - if (m_hasTitle) { - drawTitle(p, contentRect); - } -} - -// ============================================================================ -// Drawing Helpers -// ============================================================================ - -// Calculate shadow margin (extra space needed for shadow) -QMarginsF GroupBox::shadowMargin() const { - if (!m_elevation || m_elevation->elevation() <= 0) { - return QMarginsF(0, 0, 0, 0); - } - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - // According to elevation level, calculate margin dynamically - // Level 1: blur=2dp, offset=1dp, max offset ~1.5dp - // Reserve margin = offset + blur/2 - int level = m_elevation->elevation(); - float margin = helper.dpToPx(1.0f + level * 1.5f); - return QMarginsF(margin, margin, margin, margin); -} - -void GroupBox::drawShadow(QPainter& p, const QRectF& contentRect, const QPainterPath& shape) { - if (m_elevation && m_elevation->elevation() > 0) { - m_elevation->paintShadow(&p, shape); - } -} - -void GroupBox::drawBackground(QPainter& p, const QPainterPath& shape) { - CFColor bg = surfaceColor(); - - // Group box background is typically surface color - p.fillPath(shape, bg.native_color()); -} - -void GroupBox::drawBorder(QPainter& p, const QRectF& contentRect, const QPainterPath& shape) { - QColor color = outlineColor().native_color(); - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - p.save(); - - // Use 0.5px inset for border (QPen strokes are center-aligned) - float inset = 0.5f; - - // Create inset path for border - QRectF shapeBounds = shape.boundingRect(); - QRectF insetRect = shapeBounds.adjusted(inset, inset, -inset, -inset); - float adjustedRadius = std::max(0.0f, cornerRadius() - inset); - QPainterPath insetShape = roundedRect(insetRect, adjustedRadius); - - QPen pen(color, 1.0); // 1px width - pen.setCosmetic(true); // Keep consistent 1px width across DPI - p.setPen(pen); - p.setBrush(Qt::NoBrush); - - // If has title, we need to clear the border where title sits - if (m_hasTitle) { - QRectF tArea = titleArea(); - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float titleGapExtend = helper.dpToPx(6.0f); - - QRectF gapRect = tArea.adjusted(-titleGapExtend, -helper.dpToPx(2.0f), titleGapExtend, - helper.dpToPx(2.0f)); - - QRegion fullRegion(rect()); - QRegion gapRegion(gapRect.toRect()); - p.setClipRegion(fullRegion - gapRegion); - - p.drawPath(insetShape); - - p.setClipping(false); // 恢复 - } else { - p.drawPath(insetShape); - } - - p.restore(); -} - -void GroupBox::drawTitle(QPainter& p, const QRectF& contentRect) { - QString titleText = title(); - if (titleText.isEmpty()) { - return; - } - - // Get title font and color - QFont font = titleFont(); - QFontMetrics fm(font); - CFColor titleC = titleColor(); - - p.save(); - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float titlePadding = helper.dpToPx(16.0f); - float titleTopPadding = helper.dpToPx(8.0f); // Distance from top of contentRect - float titleHeight = fm.height(); - - // Title is positioned fully inside the contentRect, at the top - // The border and background start below the title area - float titleY = contentRect.top() + titleTopPadding; - - QRectF textRect(contentRect.left() + titlePadding, titleY, - contentRect.width() - titlePadding * 2, titleHeight); - - // Draw title text - p.setFont(font); - QColor textColor = titleC.native_color(); - if (!isEnabled()) { - textColor.setAlphaF(0.38f); - } - p.setPen(textColor); - - p.drawText(textRect, Qt::AlignLeft | Qt::AlignTop, titleText); - - p.restore(); -} - -// ============================================================================ -// Title Area Calculation -// ============================================================================ - -QRectF GroupBox::titleArea() const { - QString titleText = title(); - if (titleText.isEmpty()) { - return QRectF(); - } - - QFont font = titleFont(); - QFontMetrics fm(font); - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float titlePadding = helper.dpToPx(16.0f); - float titleTopPadding = helper.dpToPx(8.0f); - float titleTextWidth = fm.horizontalAdvance(titleText); - float titleHeight = fm.height(); - - QMarginsF margin = shadowMargin(); - QRectF contentRect = - QRectF(rect()).adjusted(margin.left(), margin.top(), -margin.right(), -margin.bottom()); - - // Title area matches drawTitle() position (inside contentRect at top) - float titleY = contentRect.top() + titleTopPadding; - - return QRectF(contentRect.left() + titlePadding, titleY, titleTextWidth, titleHeight); -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/groupbox/groupbox.h b/ui/widget/material/widget/groupbox/groupbox.h deleted file mode 100644 index eaedbd431..000000000 --- a/ui/widget/material/widget/groupbox/groupbox.h +++ /dev/null @@ -1,277 +0,0 @@ -/** - * @file ui/widget/material/widget/groupbox/groupbox.h - * @brief Material Design 3 GroupBox widget. - * - * Implements Material Design 3 group box with rounded corners, optional - * elevation shadows, and theme-aware colors. Provides a container for - * grouping related widgets with a title. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include - -#include "aex/weak_ptr/weak_ptr.h" -#include "base/color.h" -#include "cfmaterial_animation_factory.h" -#include "export.h" -#include -#include - -namespace cf::ui::widget::material { - -// Forward declarations -namespace base { -class MdElevationController; -} // namespace base - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 GroupBox widget. - * - * @details Implements Material Design 3 group box with rounded corners, - * optional elevation shadows, and theme-aware colors. Provides - * a container for grouping related widgets with a title. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT GroupBox : public QGroupBox { - Q_OBJECT - Q_PROPERTY(int elevation READ elevation WRITE setElevation) - Q_PROPERTY(float cornerRadius READ cornerRadius WRITE setCornerRadius) - Q_PROPERTY(bool hasBorder READ hasBorder WRITE setHasBorder) - - public: - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit GroupBox(QWidget* parent = nullptr); - - /** - * @brief Constructor with title. - * - * @param[in] title Group box title. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit GroupBox(const QString& title, QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~GroupBox() override; - - /** - * @brief Gets the elevation level. - * - * @return Elevation level (0-5). - * - * @throws None - * @note Material Design defines 6 standard levels. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - int elevation() const; - - /** - * @brief Sets the elevation level. - * - * @param[in] level Elevation level (0-5). - * - * @throws None - * @note Affects shadow rendering. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setElevation(int level); - - /** - * @brief Gets the effective corner radius. - * - * @return Corner radius in pixels. Returns 0 for default (auto-calculate). - * - * @throws None - * @note Default is based on Material Design shape scale (ShapeSmall). - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - float cornerRadius() const; - - /** - * @brief Sets the corner radius. - * - * @param[in] radius Corner radius in pixels. - * - * @throws None - * @note Value is clamped to valid range. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setCornerRadius(float radius); - - /** - * @brief Gets whether the group box has a border. - * - * @return true if border is enabled, false otherwise. - * - * @throws None - * @note Default is true. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool hasBorder() const; - - /** - * @brief Sets whether the group box has a border. - * - * @param[in] hasBorder true to enable border, false to disable. - * - * @throws None - * @note When disabled, only shadow is drawn (if elevation > 0). - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setHasBorder(bool hasBorder); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the group box. - * - * @throws None - * @note Based on content and margins. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures minimum content size. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - protected: - /** - * @brief Paints the group box. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements Material Design paint pipeline. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles child event. - * - * @param[in] event Child event. - * - * @throws None - * @note Used to detect layout changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void childEvent(QChildEvent* event) override; - - /** - * @brief Handles widget change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates title tracking when title changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Handles resize event. - * - * @param[in] event Resize event. - * - * @throws None - * @note Updates content margins on resize. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void resizeEvent(QResizeEvent* event) override; - - private: - // Layout helpers - void updateContentMargins(); - - // Drawing helpers - QMarginsF shadowMargin() const; - void drawShadow(QPainter& p, const QRectF& contentRect, const QPainterPath& shape); - void drawBackground(QPainter& p, const QPainterPath& shape); - void drawBorder(QPainter& p, const QRectF& contentRect, const QPainterPath& shape); - void drawTitle(QPainter& p, const QRectF& contentRect); - - // Color access methods - CFColor surfaceColor() const; - CFColor outlineColor() const; - CFColor titleColor() const; - QFont titleFont() const; - - // Calculate title area for background masking - QRectF titleArea() const; - - // Behavior components - aex::WeakPtr m_animationFactory; - base::MdElevationController* m_elevation; - - // Properties - float m_cornerRadius; - bool m_hasBorder; - bool m_hasTitle; -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/label/label.cpp b/ui/widget/material/widget/label/label.cpp deleted file mode 100644 index ffa91eb41..000000000 --- a/ui/widget/material/widget/label/label.cpp +++ /dev/null @@ -1,433 +0,0 @@ -/** - * @file label.cpp - * @brief Material Design 3 Label Implementation - * - * Implements a Material Design 3 label with 15 typography styles, - * 9 color variants, and full theme integration. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "label.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "core/token/typography/cfmaterial_typography_token_literals.h" - -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base::device; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::application_support; - -namespace { -// Fallback pixel sizes matching Material Design 3 Type Scale -inline int fallbackPixelSize(TypographyStyle style) { - switch (style) { - case TypographyStyle::DisplayLarge: - return 57; - case TypographyStyle::DisplayMedium: - return 45; - case TypographyStyle::DisplaySmall: - return 36; - - case TypographyStyle::HeadlineLarge: - return 32; - case TypographyStyle::HeadlineMedium: - return 28; - case TypographyStyle::HeadlineSmall: - return 24; - - case TypographyStyle::TitleLarge: - return 22; - case TypographyStyle::TitleMedium: - return 16; - case TypographyStyle::TitleSmall: - return 14; - - case TypographyStyle::BodyLarge: - return 16; - case TypographyStyle::BodyMedium: - return 14; - case TypographyStyle::BodySmall: - return 12; - - case TypographyStyle::LabelLarge: - return 14; - case TypographyStyle::LabelMedium: - return 12; - case TypographyStyle::LabelSmall: - return 11; - } - return 14; -} - -// Fallback colors when theme is not available -inline CFColor fallbackOnSurface() { - return CFColor(27, 27, 31); -} // On Surface -inline CFColor fallbackOnSurfaceVariant() { - return CFColor(74, 69, 78); -} // On Surface Variant -inline CFColor fallbackPrimary() { - return CFColor(103, 80, 164); -} // Purple 700 -inline CFColor fallbackOnPrimary() { - return CFColor(255, 255, 255); -} // White -inline CFColor fallbackSecondary() { - return CFColor(101, 163, 207); -} // Light Blue -inline CFColor fallbackOnSecondary() { - return CFColor(255, 255, 255); -} // White -inline CFColor fallbackError() { - return CFColor(186, 26, 26); -} // Error -inline CFColor fallbackOnError() { - return CFColor(255, 255, 255); -} // White -inline CFColor fallbackInverseSurface() { - return CFColor(49, 48, 51); -} // Inverse Surface -inline CFColor fallbackInverseOnSurface() { - return CFColor(236, 236, 240); -} // Inverse On Surface -} // namespace - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -Label::Label(const QString& text, TypographyStyle style, QWidget* parent) - : QLabel(text, parent), typographyStyle_(style), colorVariant_(LabelColorVariant::OnSurface), - autoHiding_(false), colorCacheValid_(false) { - updateAppearance(); -} - -Label::~Label() = default; - -// ============================================================================ -// Property Getters/Setters -// ============================================================================ - -TypographyStyle Label::typographyStyle() const { - return typographyStyle_; -} - -void Label::setTypographyStyle(TypographyStyle style) { - if (typographyStyle_ != style) { - typographyStyle_ = style; - colorCacheValid_ = false; // Invalidate color cache - updateAppearance(); - updateGeometry(); - } -} - -LabelColorVariant Label::colorVariant() const { - return colorVariant_; -} - -void Label::setColorVariant(LabelColorVariant variant) { - if (colorVariant_ != variant) { - colorVariant_ = variant; - colorCacheValid_ = false; // Invalidate color cache - updateAppearance(); - } -} - -bool Label::autoHiding() const { - return autoHiding_; -} - -void Label::setAutoHiding(bool enabled) { - if (autoHiding_ != enabled) { - autoHiding_ = enabled; - updateAppearance(); - } -} - -// ============================================================================ -// Size Hints -// ============================================================================ - -QSize Label::sizeHint() const { - QFontMetrics fm(font()); - QString txt = text(); - - // Handle empty text - if (txt.isEmpty()) { - return QLabel::sizeHint(); - } - - // Calculate based on word wrap setting - if (wordWrap()) { - int width = fm.horizontalAdvance(txt); - // Approximate height with line wrapping - int height = fm.height() * 2 + fm.leading(); - // Use a reasonable default width constraint - int maxWidth = 300; - if (width > maxWidth) { - int lines = (width + maxWidth - 1) / maxWidth; - height = fm.height() * lines + fm.leading() * (lines - 1); - width = maxWidth; - } - return QSize(width, height); - } - - return QSize(fm.horizontalAdvance(txt), fm.height()); -} - -QSize Label::minimumSizeHint() const { - QFontMetrics fm(font()); - // Minimum: at least one character or empty placeholder - int minWidth = fm.horizontalAdvance("M"); - return QSize(minWidth, fm.height()); -} - -// ============================================================================ -// Event Handlers -// ============================================================================ - -void Label::paintEvent(QPaintEvent* event) { - Q_UNUSED(event); - - QString txt = text(); - if (autoHiding_ && txt.isEmpty()) { - return; // Don't render anything if auto-hiding and empty - } - - // Draw text manually with theme-aware color - QPainter p(this); - p.setPen(textColor().native_color()); - p.setFont(font()); - - QRect contentRect = contentsRect(); - Qt::Alignment align = alignment(); - - // Draw text with proper alignment and word wrap - int flags = align; - if (wordWrap()) { - flags |= Qt::TextWordWrap; - } - - p.drawText(contentRect, flags, txt); -} - -void Label::changeEvent(QEvent* event) { - QLabel::changeEvent(event); - - if (event->type() == QEvent::EnabledChange) { - // Update appearance when enabled state changes - colorCacheValid_ = false; // Invalidate color cache - updateAppearance(); - } -} - -// ============================================================================ -// Private Helpers -// ============================================================================ - -void Label::updateAppearance() { - // Update font from theme - setFont(typographyFont()); - - // Update stylesheet is not used - we handle color in paintEvent - // Update auto-hiding state - if (autoHiding_) { - setVisible(!text().isEmpty()); - } else { - setVisible(true); - } - - update(); -} - -CFColor Label::textColor() const { - // Return cached color if valid - if (colorCacheValid_) { - return cachedColor_; - } - - auto* app = Application::instance(); - if (!app) { - cachedColor_ = fallbackOnSurface(); - colorCacheValid_ = true; - return cachedColor_; - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - - const char* token = nullptr; - switch (colorVariant_) { - case LabelColorVariant::OnSurface: - token = ON_SURFACE; - break; - case LabelColorVariant::OnSurfaceVariant: - token = ON_SURFACE_VARIANT; - break; - case LabelColorVariant::Primary: - token = PRIMARY; - break; - case LabelColorVariant::OnPrimary: - token = ON_PRIMARY; - break; - case LabelColorVariant::Secondary: - token = SECONDARY; - break; - case LabelColorVariant::OnSecondary: - token = ON_SECONDARY; - break; - case LabelColorVariant::Error: - token = ERROR; - break; - case LabelColorVariant::OnError: - token = ON_ERROR; - break; - case LabelColorVariant::InverseSurface: - token = INVERSE_SURFACE; - break; - case LabelColorVariant::InverseOnSurface: - token = INVERSE_ON_SURFACE; - break; - } - - if (token) { - CFColor color = CFColor(colorScheme.queryColor(token)); - - // Apply disabled state opacity - if (!isEnabled()) { - QColor c = color.native_color(); - c.setAlphaF(0.38f); // 38% opacity for disabled - cachedColor_ = CFColor(c); - } else { - cachedColor_ = color; - } - colorCacheValid_ = true; - return cachedColor_; - } - } catch (...) { - // Fallback if theme access fails - } - - // Fallback colors - CFColor fallback; - switch (colorVariant_) { - case LabelColorVariant::OnSurface: - fallback = fallbackOnSurface(); - break; - case LabelColorVariant::OnSurfaceVariant: - fallback = fallbackOnSurfaceVariant(); - break; - case LabelColorVariant::Primary: - fallback = fallbackPrimary(); - break; - case LabelColorVariant::OnPrimary: - fallback = fallbackOnPrimary(); - break; - case LabelColorVariant::Secondary: - fallback = fallbackSecondary(); - break; - case LabelColorVariant::OnSecondary: - fallback = fallbackOnSecondary(); - break; - case LabelColorVariant::Error: - fallback = fallbackError(); - break; - case LabelColorVariant::OnError: - fallback = fallbackOnError(); - break; - case LabelColorVariant::InverseSurface: - fallback = fallbackInverseSurface(); - break; - case LabelColorVariant::InverseOnSurface: - fallback = fallbackInverseOnSurface(); - break; - } - - // Apply disabled state to fallback - if (!isEnabled()) { - QColor c = fallback.native_color(); - c.setAlphaF(0.38f); - cachedColor_ = CFColor(c); - } else { - cachedColor_ = fallback; - } - colorCacheValid_ = true; - return cachedColor_; -} - -QFont Label::typographyFont() const { - auto* app = Application::instance(); - if (!app) { - // Fallback to system font with size based on typography style - QFont font = QLabel::font(); - font.setPixelSize(fallbackPixelSize(typographyStyle_)); - font.setWeight(QFont::Normal); - return font; - } - - try { - const auto& theme = app->currentTheme(); - auto& fontType = theme.font_type(); - const char* tokenName = typographyTokenName(typographyStyle_); - return fontType.queryTargetFont(tokenName); - } catch (...) { - QFont font = QLabel::font(); - font.setPixelSize(fallbackPixelSize(typographyStyle_)); - font.setWeight(QFont::Normal); - return font; - } -} - -const char* Label::typographyTokenName(TypographyStyle style) { - switch (style) { - case TypographyStyle::DisplayLarge: - return TYPOGRAPHY_DISPLAY_LARGE; - case TypographyStyle::DisplayMedium: - return TYPOGRAPHY_DISPLAY_MEDIUM; - case TypographyStyle::DisplaySmall: - return TYPOGRAPHY_DISPLAY_SMALL; - - case TypographyStyle::HeadlineLarge: - return TYPOGRAPHY_HEADLINE_LARGE; - case TypographyStyle::HeadlineMedium: - return TYPOGRAPHY_HEADLINE_MEDIUM; - case TypographyStyle::HeadlineSmall: - return TYPOGRAPHY_HEADLINE_SMALL; - - case TypographyStyle::TitleLarge: - return TYPOGRAPHY_TITLE_LARGE; - case TypographyStyle::TitleMedium: - return TYPOGRAPHY_TITLE_MEDIUM; - case TypographyStyle::TitleSmall: - return TYPOGRAPHY_TITLE_SMALL; - - case TypographyStyle::BodyLarge: - return TYPOGRAPHY_BODY_LARGE; - case TypographyStyle::BodyMedium: - return TYPOGRAPHY_BODY_MEDIUM; - case TypographyStyle::BodySmall: - return TYPOGRAPHY_BODY_SMALL; - - case TypographyStyle::LabelLarge: - return TYPOGRAPHY_LABEL_LARGE; - case TypographyStyle::LabelMedium: - return TYPOGRAPHY_LABEL_MEDIUM; - case TypographyStyle::LabelSmall: - return TYPOGRAPHY_LABEL_SMALL; - } - return TYPOGRAPHY_BODY_MEDIUM; -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/label/label.h b/ui/widget/material/widget/label/label.h deleted file mode 100644 index e256a6426..000000000 --- a/ui/widget/material/widget/label/label.h +++ /dev/null @@ -1,322 +0,0 @@ -/** - * @file ui/widget/material/widget/label/label.h - * @brief Material Design 3 Label widget. - * - * Implements Material Design 3 label with support for multiple typography - * styles (Display, Headline, Title, Body, Label), color variants (OnSurface, - * OnSurfaceVariant, Primary, Secondary, etc.), and theme integration. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include "aex/weak_ptr/weak_ptr.h" -#include "base/color.h" -#include "export.h" - -#include -#include - -namespace cf::ui::widget::material { - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Typography style for Material Design 3 labels. - * - * @details Maps to Material Design 3 Type Scale with 15 styles across - * 5 categories: Display, Headline, Title, Body, Label. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -enum class TypographyStyle { - // Display styles (57sp, 45sp, 36sp) - for hero content - DisplayLarge, - DisplayMedium, - DisplaySmall, - - // Headline styles (32sp, 28sp, 24sp) - for app bar text - HeadlineLarge, - HeadlineMedium, - HeadlineSmall, - - // Title styles (22sp, 16sp, 14sp) - for section headings - TitleLarge, - TitleMedium, - TitleSmall, - - // Body styles (16sp, 14sp, 12sp) - for main content - BodyLarge, - BodyMedium, - BodySmall, - - // Label styles (14sp, 12sp, 11sp) - for secondary info - LabelLarge, - LabelMedium, - LabelSmall -}; - -/** - * @brief Color variant for Material Design 3 labels. - * - * @details Controls which color token is used for text rendering. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -enum class LabelColorVariant { - OnSurface, ///< Default on-surface color - OnSurfaceVariant, ///< Variant on-surface color - Primary, ///< Primary brand color - OnPrimary, ///< Text on primary color - Secondary, ///< Secondary brand color - OnSecondary, ///< Text on secondary color - Error, ///< Error color - OnError, ///< Text on error color - InverseSurface, ///< Inverted surface color - InverseOnSurface ///< Text on inverted surface -}; - -/** - * @brief Material Design 3 Label widget. - * - * @details Implements Material Design 3 label with: - * - 15 typography styles from MD3 Type Scale - * - 9 color variants for semantic coloring - * - Theme-aware color and font resolution - * - Disabled state with opacity - * - Automatic theme change handling - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT Label : public QLabel { - Q_OBJECT - Q_PROPERTY(TypographyStyle typographyStyle READ typographyStyle WRITE setTypographyStyle) - Q_PROPERTY(LabelColorVariant colorVariant READ colorVariant WRITE setColorVariant) - Q_PROPERTY(bool autoHiding READ autoHiding WRITE setAutoHiding) - - public: - /** - * @brief Constructor with text and style. - * - * @param[in] text Label text content. - * @param[in] style Typography style. - * @param[in] parent QObject parent. - * - * @throws None - * @note Default style is BodyMedium. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit Label(const QString& text = QString(), - TypographyStyle style = TypographyStyle::BodyMedium, QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~Label() override; - - /** - * @brief Gets the typography style. - * - * @return Current typography style. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - TypographyStyle typographyStyle() const; - - /** - * @brief Sets the typography style. - * - * @param[in] style Typography style to apply. - * - * @throws None - * @note Updates font and triggers repaint. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setTypographyStyle(TypographyStyle style); - - /** - * @brief Gets the color variant. - * - * @return Current color variant. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - LabelColorVariant colorVariant() const; - - /** - * @brief Sets the color variant. - * - * @param[in] variant Color variant to apply. - * - * @throws None - * @note Updates text color and triggers repaint. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setColorVariant(LabelColorVariant variant); - - /** - * @brief Gets whether auto-hiding is enabled. - * - * @return true if label hides when empty, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool autoHiding() const; - - /** - * @brief Sets whether auto-hiding is enabled. - * - * @param[in] enabled true to hide when text is empty, false otherwise. - * - * @throws None - * @note When enabled, label becomes hidden when text is empty. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setAutoHiding(bool enabled); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the label. - * - * @throws None - * @note Based on text, font, and word wrap setting. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures minimum touch target size for interactive labels. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - protected: - /** - * @brief Paints the label. - * - * @param[in] event Paint event. - * - * @throws None - * @note Renders text with theme-aware color and font. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - private: - /** - * @brief Updates the label's appearance. - * - * @throws None - * @note Refreshes font, color, and auto-hiding state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void updateAppearance(); - - /** - * @brief Gets the text color based on current variant. - * - * @return Color for text rendering. - * - * @throws None - * @note Respects enabled state and theme. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - CFColor textColor() const; - - /** - * @brief Gets the font for current typography style. - * - * @return Font matching the current style. - * - * @throws None - * @note Queries theme for typography token. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QFont typographyFont() const; - - /** - * @brief Gets the typography token name for a style. - * - * @param[in] style Typography style. - * @return Token name string. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - static const char* typographyTokenName(TypographyStyle style); - - TypographyStyle typographyStyle_; - LabelColorVariant colorVariant_; - bool autoHiding_; - mutable CFColor cachedColor_; ///< Cached color to avoid repeated theme queries - mutable bool colorCacheValid_; ///< Whether the cached color is valid -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/listview/listview.cpp b/ui/widget/material/widget/listview/listview.cpp deleted file mode 100644 index 9679338a4..000000000 --- a/ui/widget/material/widget/listview/listview.cpp +++ /dev/null @@ -1,686 +0,0 @@ -/** - * @file listview.cpp - * @brief Material Design 3 ListView Implementation - * - * Implements a Material Design 3 list view with support for single-line, - * two-line, and three-line list items. Supports ripple effects, state layers, - * separators, and focus indicators. - * - * @author Material Design Framework Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -// Force PCH rebuild: Added internal delegate for item size control -#include "listview.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "base/geometry_helper.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -// ============================================================================ -// Private Delegate for Item Size Control -// ============================================================================ - -/** - * @brief Internal delegate for controlling item sizes in ListView. - * - * This delegate provides custom size hints for list items based on the - * configured ItemHeight setting. The default delegate would otherwise use - * QFontMetrics-based sizing which doesn't respect our Material Design - * specifications. - * - * @note This class is defined in the .cpp file and is not part of the - * public API. It is only used internally by ListView. - */ -class ListViewDelegate : public QStyledItemDelegate { - public: - explicit ListViewDelegate(QObject* parent = nullptr) - : QStyledItemDelegate(parent), m_itemHeightPx(56.0f) {} - - void setItemHeight(float heightPx) { m_itemHeightPx = heightPx; } - - QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override { - Q_UNUSED(option); - Q_UNUSED(index); - - // Return fixed height based on ItemHeight setting - // Width is left to the view's discretion (suggested: 200dp) - return QSize(-1, static_cast(m_itemHeightPx)); - } - - private: - float m_itemHeightPx; -}; - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::base::geometry; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::material::base; -using namespace cf::ui::widget::application_support; - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -ListView::ListView(QWidget* parent) - : QListView(parent), m_material(this, - MaterialWidgetBase::Config{ - .useRipple = true, - .useElevation = false, - .useFocusIndicator = true, - }), - m_itemHeight(ItemHeight::SingleLine), m_showSeparator(true), m_rippleEnabled(true), - m_hoveredIndex(-1), m_pressedIndex(-1), m_delegate(nullptr) { - // Initialize and set delegate for item size control - m_delegate = new ListViewDelegate(this); - setItemDelegate(m_delegate); - - // Configure viewport for mouse tracking - viewport()->setMouseTracking(true); - - // Set default selection mode - setSelectionMode(QAbstractItemView::SingleSelection); - setSelectionBehavior(QAbstractItemView::SelectRows); - - // Set item height - setItemHeight(m_itemHeight); - - // Disable default styling - setAttribute(Qt::WA_StyledBackground, false); -} - -ListView::~ListView() { - // Components are parented to this, Qt will delete them automatically -} - -// ============================================================================ -// Event Handlers -// ============================================================================ - -void ListView::enterEvent(QEnterEvent* event) { - QListView::enterEvent(event); - m_material.onEnterEvent(); -} - -void ListView::leaveEvent(QEvent* event) { - QListView::leaveEvent(event); - m_material.onLeaveEvent(); - m_hoveredIndex = -1; - update(); -} - -void ListView::mousePressEvent(QMouseEvent* event) { - QListView::mousePressEvent(event); - - // Map viewport position to model index - QModelIndex index = indexAt(event->pos()); - if (index.isValid() && m_rippleEnabled) { - m_pressedIndex = index.row(); - m_pressPosition = event->pos(); - - m_material.onMousePress(event->pos(), visualItemRect(index)); - } - update(); -} - -void ListView::mouseReleaseEvent(QMouseEvent* event) { - QListView::mouseReleaseEvent(event); - - if (m_pressedIndex >= 0) { - m_material.onMouseRelease(); - m_pressedIndex = -1; - } - update(); -} - -void ListView::focusInEvent(QFocusEvent* event) { - QListView::focusInEvent(event); - m_material.onFocusIn(); -} - -void ListView::focusOutEvent(QFocusEvent* event) { - QListView::focusOutEvent(event); - m_material.onFocusOut(); -} - -void ListView::changeEvent(QEvent* event) { - QListView::changeEvent(event); - if (event->type() == QEvent::EnabledChange) { - m_material.onEnabledChange(isEnabled()); - } -} - -void ListView::resizeEvent(QResizeEvent* event) { - QListView::resizeEvent(event); - update(); -} - -// ============================================================================ -// Property Getters/Setters -// ============================================================================ - -int ListView::itemHeight() const { - return static_cast(m_itemHeight); -} - -void ListView::setItemHeight(ItemHeight height) { - if (m_itemHeight != height) { - m_itemHeight = height; - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float itemHeightPx = helper.dpToPx(static_cast(itemHeight())); - - // Update delegate with new height - if (m_delegate) { - m_delegate->setItemHeight(itemHeightPx); - } - - setUniformItemSizes(true); - // Force update of item size through delegate size hint - updateGeometry(); - update(); - } -} - -void ListView::setItemHeightInt(int height) { - // Convert int to ItemHeight enum - if (height == static_cast(ItemHeight::SingleLine) || - height == static_cast(ItemHeight::TwoLine) || - height == static_cast(ItemHeight::ThreeLine)) { - setItemHeight(static_cast(height)); - } -} - -bool ListView::showSeparator() const { - return m_showSeparator; -} - -void ListView::setShowSeparator(bool show) { - if (m_showSeparator != show) { - m_showSeparator = show; - update(); - } -} - -bool ListView::rippleEnabled() const { - return m_rippleEnabled; -} - -void ListView::setRippleEnabled(bool enabled) { - if (m_rippleEnabled != enabled) { - m_rippleEnabled = enabled; - update(); - } -} - -// ============================================================================ -// Size Hints -// ============================================================================ - -QSize ListView::sizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float itemHeightPx = helper.dpToPx(static_cast(itemHeight())); - int itemCount = model() ? model()->rowCount() : 0; - - // Default width: 200dp - float defaultWidth = helper.dpToPx(200.0f); - - // Minimum height: at least one item row - float totalHeight = itemHeightPx * (itemCount > 0 ? itemCount : 1); - - return QSize(static_cast(defaultWidth), static_cast(totalHeight)); -} - -// ============================================================================ -// Color Access Methods -// ============================================================================ - -namespace { -// Fallback colors when theme is not available -inline CFColor fallbackSurface() { - return CFColor(253, 253, 253); -} // Surface -inline CFColor fallbackOnSurface() { - return CFColor(27, 27, 31); -} // On Surface -inline CFColor fallbackOnSurfaceVariant() { - return CFColor(73, 69, 79); -} // On Surface Variant -inline CFColor fallbackSurfaceVariant() { - return CFColor(231, 224, 236); -} // Surface Variant -inline CFColor fallbackOutlineVariant() { - return CFColor(187, 186, 189); -} // Outline Variant -} // namespace - -CFColor ListView::backgroundColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(SURFACE)); - } catch (...) { - return fallbackSurface(); - } -} - -CFColor ListView::textColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - return fallbackOnSurface(); - } -} - -CFColor ListView::secondaryTextColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurfaceVariant(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE_VARIANT)); - } catch (...) { - return fallbackOnSurfaceVariant(); - } -} - -CFColor ListView::selectedBackgroundColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackSurfaceVariant(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(SURFACE_VARIANT)); - } catch (...) { - return fallbackSurfaceVariant(); - } -} - -CFColor ListView::selectedTextColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - return fallbackOnSurface(); - } -} - -CFColor ListView::separatorColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOutlineVariant(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(OUTLINE_VARIANT)); - } catch (...) { - return fallbackOutlineVariant(); - } -} - -CFColor ListView::stateLayerColor() const { - // State layer uses onSurface color with varying opacity - return textColor(); -} - -float ListView::cornerRadius() const { - // ListView items use small rounded corners (8dp) - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(8.0f); -} - -QFont ListView::itemFont() const { - auto* app = Application::instance(); - if (!app) { - QFont font = QListView::font(); - font.setPixelSize(16); - font.setWeight(QFont::Normal); - return font; - } - - try { - const auto& theme = app->currentTheme(); - auto& fontType = theme.font_type(); - return fontType.queryTargetFont("bodyLarge"); - } catch (...) { - QFont font = QListView::font(); - font.setPixelSize(16); - font.setWeight(QFont::Normal); - return font; - } -} - -QFont ListView::secondaryFont() const { - auto* app = Application::instance(); - if (!app) { - QFont font = QListView::font(); - font.setPixelSize(14); - font.setWeight(QFont::Normal); - return font; - } - - try { - const auto& theme = app->currentTheme(); - auto& fontType = theme.font_type(); - return fontType.queryTargetFont("bodyMedium"); - } catch (...) { - QFont font = QListView::font(); - font.setPixelSize(14); - font.setWeight(QFont::Normal); - return font; - } -} - -// ============================================================================ -// Item State Helpers -// ============================================================================ - -bool ListView::isIndexHovered(int index) const { - return m_hoveredIndex == index && isEnabled(); -} - -bool ListView::isIndexPressed(int index) const { - return m_pressedIndex == index && isEnabled(); -} - -bool ListView::isIndexSelected(int index) const { - if (!selectionModel()) { - return false; - } - QModelIndex modelIndex = model()->index(index, 0); - return selectionModel()->isSelected(modelIndex); -} - -QRectF ListView::visualItemRect(const QModelIndex& index) const { - if (!index.isValid()) { - return QRectF(); - } - QRect rect = visualRect(index); - return QRectF(rect); -} - -// ============================================================================ -// Paint Event -// ============================================================================ - -void ListView::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter p(viewport()); - p.setRenderHint(QPainter::Antialiasing); - - QRectF viewportRect = QRectF(viewport()->rect()); - - // Draw viewport background - drawViewportBackground(p, viewportRect); - - // Update hovered index - QPoint mousePos = viewport()->mapFromGlobal(QCursor::pos()); - QModelIndex hoveredIndex = indexAt(mousePos); - m_hoveredIndex = hoveredIndex.isValid() ? hoveredIndex.row() : -1; - - // Get visible items - QModelIndex topLeft = indexAt(viewportRect.topLeft().toPoint()); - QModelIndex bottomRight = indexAt(viewportRect.bottomRight().toPoint()); - - if (!topLeft.isValid()) { - return; - } - - if (!bottomRight.isValid()) { - bottomRight = model()->index(model()->rowCount() - 1, 0); - } - - // Draw each visible item - for (int row = topLeft.row(); row <= bottomRight.row(); ++row) { - QModelIndex index = model()->index(row, 0); - QRectF itemRect = visualItemRect(index); - - // Skip if item is outside viewport - if (!itemRect.intersects(viewportRect)) { - continue; - } - - // Draw item background (for selected state) - drawItemBackground(p, itemRect, row); - - // Draw state layer (hover/pressed) - drawItemStateLayer(p, itemRect, row); - - // Draw ripple effect - drawItemRipple(p, itemRect, row); - - // Draw item content (text, icons) - drawItemContent(p, itemRect, row); - - // Draw separator (except for last item) - if (m_showSeparator && row < model()->rowCount() - 1) { - drawSeparator(p, itemRect); - } - - // Draw focus indicator if this is the current index - if (hasFocus() && currentIndex().row() == row) { - drawFocusIndicator(p, itemRect, row); - } - } -} - -// ============================================================================ -// Drawing Helpers -// ============================================================================ - -void ListView::drawViewportBackground(QPainter& p, const QRectF& viewportRect) { - CFColor bg = backgroundColor(); - p.fillRect(viewportRect, bg.native_color()); -} - -void ListView::drawItemBackground(QPainter& p, const QRectF& itemRect, int index) { - if (isIndexSelected(index)) { - CFColor selectedBg = selectedBackgroundColor(); - QPainterPath shape = roundedRect(itemRect, 0.0f); // No rounding for item background - p.fillPath(shape, selectedBg.native_color()); - } -} - -void ListView::drawItemStateLayer(QPainter& p, const QRectF& itemRect, int index) { - if (!isEnabled() || !m_material.stateMachine()) { - return; - } - - // Only draw state layer for hovered or pressed items - if (!isIndexHovered(index) && !isIndexPressed(index)) { - return; - } - - float opacity = m_material.stateMachine()->stateLayerOpacity(); - if (opacity <= 0.0f) { - return; - } - - CFColor stateColor = stateLayerColor(); - QColor color = stateColor.native_color(); - color.setAlphaF(color.alphaF() * opacity); - - QPainterPath shape = roundedRect(itemRect, 0.0f); - p.fillPath(shape, color); -} - -void ListView::drawItemRipple(QPainter& p, const QRectF& itemRect, int index) { - if (!m_rippleEnabled || !m_material.ripple()) { - return; - } - - m_material.ripple()->setColor(textColor()); - - // Create a clip path for the item - QPainterPath clipPath; - clipPath.addRect(itemRect); - - p.save(); - p.setClipPath(clipPath); - m_material.ripple()->paint(&p, clipPath); - p.restore(); -} - -void ListView::drawItemContent(QPainter& p, const QRectF& itemRect, int index) { - QModelIndex modelIndex = model()->index(index, 0); - if (!modelIndex.isValid()) { - return; - } - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float horizontalPadding = helper.dpToPx(16.0f); - float iconSize = helper.dpToPx(24.0f); - float iconSpacing = helper.dpToPx(16.0f); - - QRectF contentRect = itemRect.adjusted(horizontalPadding, 0, -horizontalPadding, 0); - - // Determine text color based on state - CFColor itemTextColor; - if (isIndexSelected(index)) { - itemTextColor = selectedTextColor(); - } else { - itemTextColor = textColor(); - } - - // Handle disabled state - if (!isEnabled()) { - QColor color = itemTextColor.native_color(); - color.setAlphaF(0.38f); - p.setPen(color); - } else { - p.setPen(itemTextColor.native_color()); - } - - // Check for icon/decoration - QVariant decorationVariant = modelIndex.data(Qt::DecorationRole); - QIcon icon; - if (!decorationVariant.isNull() && decorationVariant.canConvert()) { - icon = decorationVariant.value(); - } - - // Get text - QString text = modelIndex.data(Qt::DisplayRole).toString(); - - // Calculate layout - float currentX = contentRect.left(); - float centerY = contentRect.center().y(); - - // Draw icon if present - if (!icon.isNull()) { - QRectF iconRect(currentX, centerY - iconSize / 2, iconSize, iconSize); - QIcon::Mode mode = isEnabled() ? QIcon::Normal : QIcon::Disabled; - icon.paint(&p, iconRect.toRect(), Qt::AlignCenter, mode); - currentX += iconSize + iconSpacing; - } - - // Draw text - QFont font = itemFont(); - p.setFont(font); - - QRectF textRect(currentX, contentRect.top(), contentRect.right() - currentX, - contentRect.height()); - - Qt::Alignment alignment = Qt::AlignLeft | Qt::AlignVCenter; - p.drawText(textRect, alignment, text); - - // Draw secondary text if available (for two-line items) - QVariant secondaryVariant = modelIndex.data(Qt::UserRole + 1); - if (secondaryVariant.isValid()) { - QString secondaryText = secondaryVariant.toString(); - QFont secFont = secondaryFont(); - p.setFont(secFont); - - CFColor secColor = secondaryTextColor(); - if (!isEnabled()) { - QColor color = secColor.native_color(); - color.setAlphaF(0.38f); - p.setPen(color); - } else { - p.setPen(secColor.native_color()); - } - - // Position secondary text below primary text - QFontMetrics primaryMetrics(font); - float primaryHeight = primaryMetrics.boundingRect(text).height(); - float secondaryTop = textRect.top() + primaryHeight + helper.dpToPx(4.0f); - - QRectF secondaryRect(textRect.left(), secondaryTop, textRect.width(), - textRect.bottom() - secondaryTop); - p.drawText(secondaryRect, alignment, secondaryText); - } -} - -void ListView::drawSeparator(QPainter& p, const QRectF& itemRect) { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float separatorHeight = helper.dpToPx(1.0f); - float horizontalPadding = helper.dpToPx(16.0f); - - CFColor sepColor = separatorColor(); - QColor color = sepColor.native_color(); - color.setAlphaF(color.alphaF() * 0.12f); // 12% opacity for separators - - p.setPen(QPen(color, separatorHeight)); - p.setBrush(Qt::NoBrush); - - float y = itemRect.bottom() - separatorHeight / 2; - float left = itemRect.left() + horizontalPadding; - float right = itemRect.right() - horizontalPadding; - - p.drawLine(QPointF(left, y), QPointF(right, y)); -} - -void ListView::drawFocusIndicator(QPainter& p, const QRectF& itemRect, int index) { - if (!m_material.focusIndicator()) { - return; - } - - // Create a path for the focus indicator around the item - QPainterPath shape; - shape.addRect(itemRect); - - // Use the text color as the focus indicator color - m_material.focusIndicator()->paint(&p, shape, textColor()); -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/listview/listview.h b/ui/widget/material/widget/listview/listview.h deleted file mode 100644 index 454e58dd5..000000000 --- a/ui/widget/material/widget/listview/listview.h +++ /dev/null @@ -1,352 +0,0 @@ -/** - * @file ui/widget/material/widget/listview/listview.h - * @brief Material Design 3 ListView widget. - * - * Implements Material Design 3 list view with support for single-line, - * two-line, and three-line list items. Includes ripple effects, state - * layers, separators, and focus indicators. - * - * @author N/A - * @date N/A - * @version N/A - * @since N/A - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include -#include -#include - -#include "base/color.h" -#include "export.h" -#include "widget/material/base/material_widget_base.h" - -namespace cf::ui::widget::material { - -class ListViewDelegate; - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 ListView widget. - * - * @details Implements Material Design 3 list view with support for single-line, - * two-line, and three-line list items. Includes ripple effects, state - * layers, separators, and focus indicators. - * - * @since N/A - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT ListView : public QListView { - Q_OBJECT - Q_PROPERTY(int itemHeight READ itemHeight WRITE setItemHeightInt) - Q_PROPERTY(bool showSeparator READ showSeparator WRITE setShowSeparator) - Q_PROPERTY(bool rippleEnabled READ rippleEnabled WRITE setRippleEnabled) - - public: - /** - * @brief List item height specification. - * - * @since N/A - * @ingroup ui_widget_material_widget - */ - enum class ItemHeight { - SingleLine = 56, ///< 56dp for single-line items - TwoLine = 72, ///< 72dp for two-line items - ThreeLine = 88 ///< 88dp for three-line items - }; - Q_ENUM(ItemHeight); - - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - explicit ListView(QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - ~ListView() override; - - /** - * @brief Gets the item height in dp. - * - * @return Item height in device-independent pixels. - * - * @throws None - * @note Returns the integer value of ItemHeight enum. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - int itemHeight() const; - - /** - * @brief Sets the item height. - * - * @param[in] height Item height type. - * - * @throws None - * @note Updates the uniform item size. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void setItemHeight(ItemHeight height); - - /** - * @brief Sets the item height (int version for Q_PROPERTY). - * - * @param[in] height Item height value. - * - * @throws None - * @note Converts int to ItemHeight enum. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void setItemHeightInt(int height); - - /** - * @brief Gets whether separators are shown. - * - * @return true if separators are shown, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - bool showSeparator() const; - - /** - * @brief Sets whether to show separators. - * - * @param[in] show true to show separators, false to hide. - * - * @throws None - * @note Separators are drawn between items. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void setShowSeparator(bool show); - - /** - * @brief Gets whether ripple effect is enabled. - * - * @return true if ripple is enabled, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - bool rippleEnabled() const; - - /** - * @brief Sets whether ripple effect is enabled. - * - * @param[in] enabled true to enable ripple, false to disable. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void setRippleEnabled(bool enabled); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the list view. - * - * @throws None - * @note Based on item count and item height. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - protected: - /** - * @brief Paints the list view. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements Material Design paint pipeline for viewport. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles viewport mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple and updates state. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles viewport mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates state. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles viewport mouse enter event. - * - * @param[in] event Enter event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void enterEvent(QEnterEvent* event) override; - - /** - * @brief Handles viewport mouse leave event. - * - * @param[in] event Leave event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void leaveEvent(QEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Shows focus indicator. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Hides focus indicator. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Handles viewport resize event. - * - * @param[in] event Resize event. - * - * @throws None - * @note Updates layout. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void resizeEvent(QResizeEvent* event) override; - - private: - // Drawing helpers - Material Design paint pipeline - void drawViewportBackground(QPainter& p, const QRectF& viewportRect); - void drawItemBackground(QPainter& p, const QRectF& itemRect, int index); - void drawItemStateLayer(QPainter& p, const QRectF& itemRect, int index); - void drawItemRipple(QPainter& p, const QRectF& itemRect, int index); - void drawItemContent(QPainter& p, const QRectF& itemRect, int index); - void drawSeparator(QPainter& p, const QRectF& itemRect); - void drawFocusIndicator(QPainter& p, const QRectF& itemRect, int index); - - // Item state helpers - bool isIndexHovered(int index) const; - bool isIndexPressed(int index) const; - bool isIndexSelected(int index) const; - QRectF visualItemRect(const QModelIndex& index) const; - - // Color access methods - CFColor backgroundColor() const; - CFColor textColor() const; - CFColor secondaryTextColor() const; - CFColor selectedBackgroundColor() const; - CFColor selectedTextColor() const; - CFColor separatorColor() const; - CFColor stateLayerColor() const; - float cornerRadius() const; - QFont itemFont() const; - QFont secondaryFont() const; - - // Behavior components - base::MaterialWidgetBase m_material; - - // Item properties - ItemHeight m_itemHeight; - bool m_showSeparator; - bool m_rippleEnabled; - - // Hover state tracking - int m_hoveredIndex; - int m_pressedIndex; - QPoint m_pressPosition; - - // Delegate for item size control (defined in .cpp) - ListViewDelegate* m_delegate; -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/progressbar/progressbar.cpp b/ui/widget/material/widget/progressbar/progressbar.cpp deleted file mode 100644 index 2ef64f055..000000000 --- a/ui/widget/material/widget/progressbar/progressbar.cpp +++ /dev/null @@ -1,432 +0,0 @@ -/** - * @file progressbar.cpp - * @brief Material Design 3 ProgressBar Implementation - * - * Implements a Material Design 3 progress bar with determinate and - * indeterminate modes. Supports smooth animations, state layers, - * and focus indicators. - * - * @author CFDesktop Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "progressbar.h" -#include "aex/weak_ptr/weak_ptr.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "base/easing.h" -#include "base/geometry_helper.h" -#include "components/material/cfmaterial_animation_factory.h" -#include "components/material/cfmaterial_property_animation.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::base::geometry; -using namespace cf::ui::components; -using namespace cf::ui::components::material; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::material::base; - -// ============================================================================ -// Constants -// ============================================================================ - -namespace { -// Material Design 3 ProgressBar specifications (in dp) -constexpr float TRACK_HEIGHT_DP = 8.0f; -constexpr float TEXT_TO_TRACK_GAP_DP = 4.0f; -constexpr float FOCUS_RING_MARGIN_DP = 4.0f; -} // namespace - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -ProgressBar::ProgressBar(QWidget* parent) - : QProgressBar(parent), m_material(this, base::MaterialWidgetBase::Config{ - .useRipple = false, - .useElevation = false, - .useFocusIndicator = true, - }) { - // Set size policy - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - - // Start indeterminate animation if in indeterminate mode - if (minimum() == 0 && maximum() == 0) { - startIndeterminateAnimation(); - } -} - -ProgressBar::~ProgressBar() { - stopIndeterminateAnimation(); - // Components are parented to this, Qt will delete them automatically -} - -// ============================================================================ -// Event Handlers -// ============================================================================ - -void ProgressBar::enterEvent(QEnterEvent* event) { - QProgressBar::enterEvent(event); - m_material.onEnterEvent(); -} - -void ProgressBar::leaveEvent(QEvent* event) { - QProgressBar::leaveEvent(event); - m_material.onLeaveEvent(); -} - -void ProgressBar::focusInEvent(QFocusEvent* event) { - QProgressBar::focusInEvent(event); - m_material.onFocusIn(); -} - -void ProgressBar::focusOutEvent(QFocusEvent* event) { - QProgressBar::focusOutEvent(event); - m_material.onFocusOut(); -} - -void ProgressBar::changeEvent(QEvent* event) { - QProgressBar::changeEvent(event); - if (event->type() == QEvent::EnabledChange) { - m_material.onEnabledChange(isEnabled()); - } -} - -// ============================================================================ -// Size Hints -// ============================================================================ - -QSize ProgressBar::sizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Material Design 3 progress bar specifications: - // - Track height: 4dp - // - Touch target: 48dp (for accessibility) - // - Minimum width: 280dp - - float minHeight = helper.dpToPx(48.0f); // Touch target - float minWidth = helper.dpToPx(280.0f); - - return QSize(int(std::ceil(minWidth)), int(std::ceil(minHeight))); -} - -QSize ProgressBar::minimumSizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - float minHeight = helper.dpToPx(48.0f); - float minWidth = helper.dpToPx(200.0f); - - return QSize(int(std::ceil(minWidth)), int(std::ceil(minHeight))); -} - -// ============================================================================ -// Color Access Methods -// ============================================================================ - -namespace { -// Fallback colors when theme is not available -inline CFColor fallbackSurface() { - return CFColor(232, 226, 232); // Surface container -} -inline CFColor fallbackPrimary() { - return CFColor(103, 80, 164); // Purple 700 -} -inline CFColor fallbackOnSurface() { - return CFColor(27, 27, 31); // On Surface -} -} // namespace - -CFColor ProgressBar::trackColor() const { - auto* app = application_support::Application::instance(); - if (!app) { - return fallbackSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - // Use SURFACE_VARIANT for track background (subtle contrast from main surface) - return CFColor(colorScheme.queryColor(SURFACE_VARIANT)); - } catch (...) { - return fallbackSurface(); - } -} - -CFColor ProgressBar::fillColor() const { - auto* app = application_support::Application::instance(); - if (!app) { - return fallbackPrimary(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(PRIMARY)); - } catch (...) { - return fallbackPrimary(); - } -} - -CFColor ProgressBar::textColor() const { - auto* app = application_support::Application::instance(); - if (!app) { - return fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - return fallbackOnSurface(); - } -} - -CFColor ProgressBar::stateLayerColor() const { - return fillColor(); -} - -float ProgressBar::cornerRadius() const { - // Progress bar uses full rounding (pill shape) - return trackHeight() / 2.0f; -} - -float ProgressBar::trackHeight() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(TRACK_HEIGHT_DP); -} - -// ============================================================================ -// Layout Helpers -// ============================================================================ - -QRectF ProgressBar::trackRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float h = trackHeight(); - float y = (height() - h) / 2.0f; - return QRectF(0, y, width(), h); -} - -// ============================================================================ -// Indeterminate Animation -// ============================================================================ - -void ProgressBar::startIndeterminateAnimation() { - // Get animation factory locally for custom indeterminate animation - auto factory = aex::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - if (!factory || m_indeterminateAnimating) { - return; - } - - m_indeterminateAnimating = true; - - // Create a looping animation for indeterminate mode - // Using a property animation on m_indeterminatePosition - auto anim = factory->createPropertyAnimation(&m_indeterminatePosition, 0.0f, 1.0f, 1500, - cf::ui::base::Easing::Type::Linear, this); - - if (anim) { - // IMPORTANT: Update range to fix cached animation's stale from/to values - if (auto* propAnim = - dynamic_cast( - anim.Get())) { - propAnim->setRange(0.0f, 1.0f); - } - // Connect to finished signal to restart the animation - connect(anim.Get(), &ICFAbstractAnimation::finished, this, [this]() { - m_indeterminatePosition = 0.0f; - startIndeterminateAnimation(); - }); - anim->start(); - } -} - -void ProgressBar::stopIndeterminateAnimation() { - m_indeterminateAnimating = false; - m_indeterminatePosition = 0.0f; -} - -void ProgressBar::updateIndeterminatePosition() { - // This is called by the animation system - update(); -} - -// ============================================================================ -// Paint Event -// ============================================================================ - -void ProgressBar::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - QRectF track = trackRect(); - - // Step 1: Draw background track - drawBackground(p, track); - - // Step 2: Draw progress fill - if (minimum() == 0 && maximum() == 0) { - drawIndeterminate(p, track); - } else { - drawFill(p, track); - } - - // Step 3: Draw text (if visible) - if (isTextVisible()) { - drawText(p, rect()); - } - - // Step 4: Draw focus indicator - drawFocusIndicator(p, track); -} - -// ============================================================================ -// Drawing Helpers -// ============================================================================ - -void ProgressBar::drawBackground(QPainter& p, const QRectF& rect) { - CFColor bgColor = trackColor(); - QColor color = bgColor.native_color(); - - // Handle disabled state - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - QPainterPath shape = roundedRect(rect, cornerRadius()); - p.fillPath(shape, color); -} - -void ProgressBar::drawFill(QPainter& p, const QRectF& trackRect) { - CFColor fillColor = this->fillColor(); - QColor color = fillColor.native_color(); - - // Handle disabled state - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - // Handle state layer overlay - if (m_material.stateMachine() && isEnabled()) { - float opacity = m_material.stateMachine()->stateLayerOpacity(); - if (opacity > 0.0f) { - CFColor stateColor = stateLayerColor(); - QColor stateQColor = stateColor.native_color(); - stateQColor.setAlphaF(opacity); - - // Blend state color with fill color - int r = int(color.red() * (1.0f - opacity) + stateQColor.red() * opacity); - int g = int(color.green() * (1.0f - opacity) + stateQColor.green() * opacity); - int b = int(color.blue() * (1.0f - opacity) + stateQColor.blue() * opacity); - color = QColor(r, g, b, color.alpha()); - } - } - - // Calculate fill width based on value - int minVal = minimum(); - int maxVal = maximum(); - int curVal = value(); - - if (maxVal > minVal) { - double ratio = static_cast(curVal - minVal) / (maxVal - minVal); - double fillWidth = trackRect.width() * ratio; - - QRectF fillRect = trackRect; - fillRect.setWidth(fillWidth); - - QPainterPath shape = roundedRect(fillRect, cornerRadius()); - p.fillPath(shape, color); - } -} - -void ProgressBar::drawIndeterminate(QPainter& p, const QRectF& trackRect) { - CFColor fillColor = this->fillColor(); - QColor color = fillColor.native_color(); - - // Handle disabled state - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - // Indeterminate animation: a segment that moves across the track - // The segment is 1/3 of the track width - double segmentWidth = trackRect.width() / 3.0; - - // Calculate position based on animation progress - // The segment moves from -segmentWidth to trackRect.width() - double xPos = -segmentWidth + (trackRect.width() + segmentWidth) * m_indeterminatePosition; - - QRectF fillRect = trackRect; - fillRect.setX(xPos); - fillRect.setWidth(segmentWidth); - - // Clip to track bounds - QPainterPath clipPath = roundedRect(trackRect, cornerRadius()); - p.setClipPath(clipPath); - - QPainterPath shape = roundedRect(fillRect, cornerRadius()); - p.fillPath(shape, color); - - p.setClipping(false); -} - -void ProgressBar::drawText(QPainter& p, const QRectF& rect) { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - CFColor tColor = textColor(); - QColor color = tColor.native_color(); - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - p.setPen(color); - p.setFont(font()); - - // Draw text centered above the track - QString text = this->text(); - if (text.isEmpty()) { - int minVal = minimum(); - int maxVal = maximum(); - int curVal = value(); - - if (maxVal > minVal) { - int percentage = static_cast((curVal - minVal) * 100 / (maxVal - minVal)); - text = QString("%1%").arg(percentage); - } - } - - QRectF textRect = rect.adjusted(0, 0, 0, -trackHeight() - helper.dpToPx(TEXT_TO_TRACK_GAP_DP)); - p.drawText(textRect, Qt::AlignCenter, text); -} - -void ProgressBar::drawFocusIndicator(QPainter& p, const QRectF& rect) { - if (m_material.focusIndicator()) { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float margin = helper.dpToPx(FOCUS_RING_MARGIN_DP); - QRectF focusRect = rect.adjusted(-margin, -margin, margin, margin); - - QPainterPath shape = roundedRect(focusRect, cornerRadius() + margin); - m_material.focusIndicator()->paint(&p, shape, fillColor()); - } -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/progressbar/progressbar.h b/ui/widget/material/widget/progressbar/progressbar.h deleted file mode 100644 index 05a6324ac..000000000 --- a/ui/widget/material/widget/progressbar/progressbar.h +++ /dev/null @@ -1,200 +0,0 @@ -/** - * @file ui/widget/material/widget/progressBar/progressbar.h - * @brief Material Design 3 ProgressBar widget. - * - * Implements Material Design 3 progress bar with determinate and - * indeterminate modes. Includes smooth animations, state layer, - * and focus indicators following Material Design 3 specifications. - * - * @author CFDesktop Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include "base/color.h" -#include "export.h" -#include "widget/material/base/material_widget_base.h" -#include -#include - -namespace cf::ui::widget::material { - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 ProgressBar widget. - * - * @details Implements Material Design 3 progress bar with determinate and - * indeterminate modes. Includes smooth animations, state layer, - * and focus indicators following Material Design 3 specifications. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT ProgressBar : public QProgressBar { - Q_OBJECT - - public: - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit ProgressBar(QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~ProgressBar() override; - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the progress bar. - * - * @throws None - * @note Based on Material Design 4dp height with padding. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures touch target size requirements. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - protected: - /** - * @brief Paints the progress bar. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements Material Design paint pipeline. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles mouse enter event. - * - * @param[in] event Enter event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void enterEvent(QEnterEvent* event) override; - - /** - * @brief Handles mouse leave event. - * - * @param[in] event Leave event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void leaveEvent(QEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - private: - // Drawing helpers - Material Design paint pipeline - QRectF trackRect() const; - void drawBackground(QPainter& p, const QRectF& rect); - void drawFill(QPainter& p, const QRectF& trackRect); - void drawIndeterminate(QPainter& p, const QRectF& trackRect); - void drawText(QPainter& p, const QRectF& rect); - void drawFocusIndicator(QPainter& p, const QRectF& rect); - - // Color access methods - CFColor trackColor() const; - CFColor fillColor() const; - CFColor textColor() const; - CFColor stateLayerColor() const; - float cornerRadius() const; - float trackHeight() const; - - // Indeterminate animation - void startIndeterminateAnimation(); - void stopIndeterminateAnimation(); - void updateIndeterminatePosition(); - - // Behavior components - base::MaterialWidgetBase m_material; - - // Indeterminate animation state (0.0 to 1.0) - float m_indeterminatePosition = 0.0f; - bool m_indeterminateAnimating = false; -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/radiobutton/radiobutton.cpp b/ui/widget/material/widget/radiobutton/radiobutton.cpp deleted file mode 100644 index e65e8b7d3..000000000 --- a/ui/widget/material/widget/radiobutton/radiobutton.cpp +++ /dev/null @@ -1,587 +0,0 @@ -/** - * @file radiobutton.cpp - * @brief Material Design 3 RadioButton Implementation - * - * Implements a Material Design 3 radio button with circular selection area, - * inner circle scale animation, ripple effects, and focus indicators. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "radiobutton.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "base/easing.h" -#include "components/material/cfmaterial_property_animation.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::components::material; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::material::base; -using namespace cf::ui::widget::application_support; - -// Material Design 3 specifications for RadioButton -namespace { -constexpr float RADIO_SIZE_DP = 20.0f; // Outer ring diameter -constexpr float INNER_CIRCLE_SIZE_DP = 10.0f; // Inner circle diameter (50% of outer) -constexpr float TOUCH_TARGET_DP = 48.0f; // Minimum touch target size -constexpr float TEXT_SPACING_DP = 8.0f; // Spacing between radio and text -constexpr float STROKE_WIDTH_DP = 2.0f; // Outer ring stroke width - -constexpr float INNER_CIRCLE_SCALE_UNCHECKED = 0.0f; -constexpr float INNER_CIRCLE_SCALE_CHECKED = 1.0f; -} // namespace - -// Fallback colors when theme is not available -namespace { -inline CFColor fallbackPrimary() { - return CFColor(103, 80, 164); -} // Purple 700 -inline CFColor fallbackOnPrimary() { - return CFColor(255, 255, 255); -} // White -inline CFColor fallbackOutline() { - return CFColor(120, 124, 132); -} // Outline -inline CFColor fallbackOnSurface() { - return CFColor(27, 27, 31); -} // On Surface -inline CFColor fallbackError() { - return CFColor(186, 26, 26); -} // Error red -} // namespace - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -RadioButton::RadioButton(QWidget* parent) - : QRadioButton(parent), m_material(this, MaterialWidgetBase::Config{.useElevation = false}) { - setFont(labelFont()); - m_innerCircleScale = isChecked() ? INNER_CIRCLE_SCALE_CHECKED : INNER_CIRCLE_SCALE_UNCHECKED; -} - -RadioButton::RadioButton(const QString& text, QWidget* parent) : RadioButton(parent) { - setText(text); -} - -RadioButton::~RadioButton() { - // Components are parented to this, Qt will delete them automatically -} - -// ============================================================================ -// Event Handlers -// ============================================================================ - -void RadioButton::enterEvent(QEnterEvent* event) { - QRadioButton::enterEvent(event); - m_material.onEnterEvent(); -} - -void RadioButton::leaveEvent(QEvent* event) { - QRadioButton::leaveEvent(event); - m_material.onLeaveEvent(); -} - -void RadioButton::mousePressEvent(QMouseEvent* event) { - QRadioButton::mousePressEvent(event); - if (m_pressEffectEnabled) { - QRectF radioRect = calculateRadioRect(); - m_material.onMousePress(event->pos(), radioRect.united(calculateTextRect(radioRect))); - } else { - m_material.stateMachine()->onPress(event->pos()); - update(); - } -} - -void RadioButton::mouseReleaseEvent(QMouseEvent* event) { - QRadioButton::mouseReleaseEvent(event); - if (m_pressEffectEnabled) { - m_material.onMouseRelease(); - } else { - m_material.stateMachine()->onRelease(); - if (m_material.ripple()) - m_material.ripple()->onRelease(); - update(); - } -} - -void RadioButton::focusInEvent(QFocusEvent* event) { - QRadioButton::focusInEvent(event); - m_material.onFocusIn(); -} - -void RadioButton::focusOutEvent(QFocusEvent* event) { - QRadioButton::focusOutEvent(event); - m_material.onFocusOut(); -} - -void RadioButton::changeEvent(QEvent* event) { - QRadioButton::changeEvent(event); - if (event->type() == QEvent::EnabledChange) { - m_material.onEnabledChange(isEnabled()); - } -} - -void RadioButton::nextCheckState() { - bool wasChecked = isChecked(); - QRadioButton::nextCheckState(); - bool isCheckedNow = isChecked(); - - if (wasChecked != isCheckedNow) { - if (m_material.stateMachine()) { - m_material.stateMachine()->onCheckedChanged(isCheckedNow); - } - startInnerCircleAnimation(isCheckedNow); - } -} - -void RadioButton::setChecked(bool checked) { - bool wasChecked = isChecked(); - if (wasChecked == checked) { - return; - } - - QRadioButton::setChecked(checked); - bool isCheckedNow = isChecked(); - - if (wasChecked != isCheckedNow) { - if (m_material.stateMachine()) { - m_material.stateMachine()->onCheckedChanged(isCheckedNow); - } - m_innerCircleScale = - isCheckedNow ? INNER_CIRCLE_SCALE_CHECKED : INNER_CIRCLE_SCALE_UNCHECKED; - update(); - } -} - -// ============================================================================ -// Property Getters/Setters -// ============================================================================ - -bool RadioButton::hasError() const { - return m_hasError; -} - -void RadioButton::setError(bool error) { - if (m_hasError != error) { - m_hasError = error; - update(); - } -} - -bool RadioButton::pressEffectEnabled() const { - return m_pressEffectEnabled; -} - -void RadioButton::setPressEffectEnabled(bool enabled) { - if (m_pressEffectEnabled != enabled) { - m_pressEffectEnabled = enabled; - update(); - } -} - -bool RadioButton::hitButton(const QPoint& pos) const { - return rect().contains(pos); -} - -// ============================================================================ -// Size Hints -// ============================================================================ - -QSize RadioButton::sizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - float leftPadding = helper.dpToPx(12.0f); - float radioSize = helper.dpToPx(RADIO_SIZE_DP); - float textSpacing = helper.dpToPx(TEXT_SPACING_DP); - - float textWidth = text().isEmpty() ? 0.0f : fontMetrics().horizontalAdvance(text()); - - float totalWidth = leftPadding + radioSize + textSpacing + textWidth; - float height = helper.dpToPx(TOUCH_TARGET_DP); - - return QSize(int(std::ceil(totalWidth)), int(std::ceil(height))); -} - -QSize RadioButton::minimumSizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - float leftPadding = helper.dpToPx(12.0f); - float radioSize = helper.dpToPx(RADIO_SIZE_DP); - float textSpacing = helper.dpToPx(TEXT_SPACING_DP); - float minTextWidth = text().isEmpty() ? 0 : fontMetrics().horizontalAdvance("M"); - - float totalWidth = leftPadding + radioSize + textSpacing + minTextWidth; - float height = helper.dpToPx(TOUCH_TARGET_DP); - - return QSize(int(std::ceil(totalWidth)), int(std::ceil(height))); -} - -// ============================================================================ -// Color Access Methods -// ============================================================================ - -CFColor RadioButton::radioColor() const { - if (m_hasError) { - return errorColor(); - } - - if (isChecked()) { - auto* app = Application::instance(); - if (!app) { - return fallbackPrimary(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(PRIMARY)); - } catch (...) { - return fallbackPrimary(); - } - } else { - auto* app = Application::instance(); - if (!app) { - return fallbackOutline(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(OUTLINE)); - } catch (...) { - return fallbackOutline(); - } - } -} - -CFColor RadioButton::onRadioColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnPrimary(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_PRIMARY)); - } catch (...) { - return fallbackOnPrimary(); - } -} - -CFColor RadioButton::stateLayerColor() const { - if (isChecked()) { - return radioColor(); - } else { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - return fallbackOnSurface(); - } - } -} - -CFColor RadioButton::errorColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackError(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ERROR)); - } catch (...) { - return fallbackError(); - } -} - -QFont RadioButton::labelFont() const { - auto* app = Application::instance(); - if (!app) { - QFont font = QRadioButton::font(); - font.setPixelSize(14); - font.setWeight(QFont::Normal); - return font; - } - - try { - const auto& theme = app->currentTheme(); - auto& fontType = theme.font_type(); - return fontType.queryTargetFont("bodyLarge"); - } catch (...) { - QFont font = QRadioButton::font(); - font.setPixelSize(14); - font.setWeight(QFont::Normal); - return font; - } -} - -// ============================================================================ -// Layout Calculations -// ============================================================================ - -QRectF RadioButton::calculateRadioRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float radioSize = helper.dpToPx(RADIO_SIZE_DP); - float y = (height() - radioSize) / 2.0f; - float x = helper.dpToPx(12.0f); - - return QRectF(x, y, radioSize, radioSize); -} - -QRectF RadioButton::calculateTextRect(const QRectF& radioRect) const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float textSpacing = helper.dpToPx(TEXT_SPACING_DP); - - float x = radioRect.right() + textSpacing; - return QRectF(x, 0.0f, width() - x, height()); -} - -// ============================================================================ -// Animation Helpers -// ============================================================================ - -void RadioButton::startInnerCircleAnimation(bool checked) { - float targetScale = checked ? INNER_CIRCLE_SCALE_CHECKED : INNER_CIRCLE_SCALE_UNCHECKED; - float fromScale = m_innerCircleScale; - - auto factory = aex::WeakPtr::DynamicCast( - Application::animationFactory()); - - if (!factory) { - m_innerCircleScale = targetScale; - update(); - return; - } - - auto anim = - factory->createPropertyAnimation(&m_innerCircleScale, fromScale, targetScale, 200, - cf::ui::base::Easing::Type::EmphasizedDecelerate, this); - - if (anim) { - if (auto* propAnim = dynamic_cast(anim.Get())) { - propAnim->setRange(fromScale, targetScale); - } - anim->start(); - } else { - m_innerCircleScale = targetScale; - update(); - } -} - -// ============================================================================= -// Paint Event -// ============================================================================= - -void RadioButton::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - QRectF radioRect = calculateRadioRect(); - QRectF textRect = calculateTextRect(radioRect); - - // Step 1: Draw state layer (behind everything) - drawStateLayer(p, radioRect); - - // Step 2: Draw ripple - drawRipple(p, radioRect); - - // Step 3: Draw outer ring - drawOuterRing(p, radioRect); - - // Step 4: Draw inner circle (when checked) - drawInnerCircle(p, radioRect); - - // Step 5: Draw text label - drawText(p, textRect); - - // Step 6: Draw focus indicator - drawFocusIndicator(p, radioRect); -} - -// ============================================================================ -// Drawing Helpers -// ============================================================================ - -void RadioButton::drawStateLayer(QPainter& p, const QRectF& radioRect) { - if (!isEnabled() || !m_material.stateMachine()) { - return; - } - - float opacity = m_material.stateMachine()->stateLayerOpacity(); - if (opacity <= 0.0f) { - return; - } - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float stateLayerSize = helper.dpToPx(RADIO_SIZE_DP * 2.0f); // 40dp - QPointF center = radioRect.center(); - QRectF stateLayerRect(center.x() - stateLayerSize / 2.0f, center.y() - stateLayerSize / 2.0f, - stateLayerSize, stateLayerSize); - - QPainterPath circlePath; - circlePath.addEllipse(stateLayerRect); - - CFColor stateColor = stateLayerColor(); - QColor color = stateColor.native_color(); - color.setAlphaF(color.alphaF() * opacity); - - p.fillPath(circlePath, color); -} - -void RadioButton::drawRipple(QPainter& p, const QRectF& radioRect) { - if (!m_material.ripple() || !m_pressEffectEnabled) { - return; - } - - m_material.ripple()->setColor(stateLayerColor()); - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float stateLayerSize = helper.dpToPx(RADIO_SIZE_DP * 2.0f); // 40dp - QPointF center = radioRect.center(); - QRectF stateLayerRect(center.x() - stateLayerSize / 2.0f, center.y() - stateLayerSize / 2.0f, - stateLayerSize, stateLayerSize); - - QPainterPath clipPath; - clipPath.addEllipse(stateLayerRect); - - m_material.ripple()->paint(&p, clipPath); -} - -void RadioButton::drawOuterRing(QPainter& p, const QRectF& radioRect) { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float strokeWidth = helper.dpToPx(STROKE_WIDTH_DP); - - CFColor ringColor = radioColor(); - QColor color = ringColor.native_color(); - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - p.save(); - - float inset = strokeWidth / 2.0f; - QRectF insetRect = radioRect.adjusted(inset, inset, -inset, -inset); - - QPainterPath ringPath; - ringPath.addEllipse(insetRect); - - QPen pen(color, strokeWidth); - pen.setCosmetic(false); - p.setPen(pen); - p.setBrush(Qt::NoBrush); - - p.drawPath(ringPath); - - p.restore(); -} - -void RadioButton::drawInnerCircle(QPainter& p, const QRectF& radioRect) { - if (!isChecked() && m_innerCircleScale <= 0.01f) { - return; - } - - float outerRadius = radioRect.width() / 2.0f; - float innerRadius = outerRadius * 0.5f; - float scaledRadius = innerRadius * m_innerCircleScale; - - QPointF center = radioRect.center(); - - p.save(); - - QPainterPath innerCirclePath; - innerCirclePath.addEllipse(center, scaledRadius, scaledRadius); - - CFColor fillColor = radioColor(); - QColor color = fillColor.native_color(); - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - p.fillPath(innerCirclePath, color); - - p.restore(); -} - -void RadioButton::drawText(QPainter& p, const QRectF& textRect) { - if (text().isEmpty()) { - return; - } - - CFColor textColor; - if (m_hasError) { - textColor = errorColor(); - } else { - auto* app = Application::instance(); - if (!app) { - textColor = fallbackOnSurface(); - } else { - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - textColor = CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - textColor = fallbackOnSurface(); - } - } - } - - if (!isEnabled()) { - QColor color = textColor.native_color(); - color.setAlphaF(0.38f); - p.setPen(color); - } else { - p.setPen(textColor.native_color()); - } - - QFont font = labelFont(); - p.setFont(font); - - p.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, text()); -} - -void RadioButton::drawFocusIndicator(QPainter& p, const QRectF& radioRect) { - if (!m_material.focusIndicator()) { - return; - } - - QPainterPath focusPath; - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float focusPadding = helper.dpToPx(4.0f); - focusPath.addEllipse( - radioRect.adjusted(-focusPadding, -focusPadding, focusPadding, focusPadding)); - - CFColor indicatorColor = radioColor(); - m_material.focusIndicator()->paint(&p, focusPath, indicatorColor); -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/radiobutton/radiobutton.h b/ui/widget/material/widget/radiobutton/radiobutton.h deleted file mode 100644 index 714ac9f87..000000000 --- a/ui/widget/material/widget/radiobutton/radiobutton.h +++ /dev/null @@ -1,340 +0,0 @@ -/** - * @file ui/widget/material/widget/radiobutton/radiobutton.h - * @brief Material Design 3 RadioButton widget. - * - * Implements Material Design 3 radio button with circular selection area, - * inner circle scale animation, ripple effects, and focus indicators. - * Supports mutual exclusion through QButtonGroup integration. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include "base/color.h" -#include "export.h" -#include "widget/material/base/material_widget_base.h" -#include -#include - -namespace cf::ui::widget::material { - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 RadioButton widget. - * - * @details Implements Material Design 3 radio button with circular selection - * area, inner circle scale animation, ripple effects, and focus - * indicators. Supports mutual exclusion through QButtonGroup - * integration. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT RadioButton : public QRadioButton { - Q_OBJECT - Q_PROPERTY(bool error READ hasError WRITE setError DESIGNABLE true SCRIPTABLE true) - Q_PROPERTY(bool pressEffectEnabled READ pressEffectEnabled WRITE setPressEffectEnabled) - - public: - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit RadioButton(QWidget* parent = nullptr); - - /** - * @brief Constructor with text. - * - * @param[in] text Button text label. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit RadioButton(const QString& text, QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~RadioButton() override; - - /** - * @brief Gets whether the radio button is in error state. - * - * @return true if in error state, false otherwise. - * - * @throws None - * @note Error state displays with error color theme. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool hasError() const; - - /** - * @brief Sets the error state. - * - * @param[in] error true to set error state, false otherwise. - * - * @throws None - * @note Error state displays with error color theme. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setError(bool error); - - /** - * @brief Gets whether press effect is enabled. - * - * @return true if press effect is enabled, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool pressEffectEnabled() const; - - /** - * @brief Sets whether press effect is enabled. - * - * @param[in] enabled true to enable press effect, false to disable. - * - * @throws None - * @note Press effect includes ripple animation. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setPressEffectEnabled(bool enabled); - - /** - * @brief Sets the checked state of the radio button. - * - * @param[in] checked true to check, false to uncheck. - * - * @throws None - * @note Override to sync inner circle scale with checked state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setChecked(bool checked); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the radio button. - * - * @throws None - * @note Based on text and touch target requirements. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures touch target size requirements (48dp). - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - /** - * @brief Handles hit test for mouse events. - * - * @param[in] pos Mouse position. - * - * @return true if the position is within the clickable area. - * - * @throws None - * @note Entire widget area is clickable. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool hitButton(const QPoint& pos) const override; - - protected: - /** - * @brief Paints the radio button. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements Material Design paint pipeline. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles mouse enter event. - * - * @param[in] event Enter event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void enterEvent(QEnterEvent* event) override; - - /** - * @brief Handles mouse leave event. - * - * @param[in] event Leave event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void leaveEvent(QEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple and press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Handles check state change event. - * - * @param[in] checked Check state. - * - * @throws None - * @note Triggers inner circle scale animation. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void nextCheckState() override; - - private: - // Drawing helpers - Material Design paint pipeline - void drawStateLayer(QPainter& p, const QRectF& radioRect); - void drawRipple(QPainter& p, const QRectF& radioRect); - void drawOuterRing(QPainter& p, const QRectF& radioRect); - void drawInnerCircle(QPainter& p, const QRectF& radioRect); - void drawText(QPainter& p, const QRectF& textRect); - void drawFocusIndicator(QPainter& p, const QRectF& radioRect); - - // Color access methods - CFColor radioColor() const; - CFColor onRadioColor() const; - CFColor stateLayerColor() const; - CFColor errorColor() const; - QFont labelFont() const; - - // Layout calculations - QRectF calculateRadioRect() const; - QRectF calculateTextRect(const QRectF& radioRect) const; - - // Animation helpers - void startInnerCircleAnimation(bool checked); - - // Behavior components - base::MaterialWidgetBase m_material; - - // Animation state - float m_innerCircleScale = 0.0f; - - // Properties - bool m_hasError = false; - bool m_pressEffectEnabled = true; -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/scrollview/scrollview.cpp b/ui/widget/material/widget/scrollview/scrollview.cpp deleted file mode 100644 index cb1bd04ed..000000000 --- a/ui/widget/material/widget/scrollview/scrollview.cpp +++ /dev/null @@ -1,918 +0,0 @@ -/** - * @file scrollview.cpp - * @brief Material Design 3 ScrollView Implementation - * - * Implements a Material Design 3 scroll area with custom scrollbars, - * fade effects, and theme integration. - * - * @author Material Design Framework Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "scrollview.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "base/geometry_helper.h" -#include "cfmaterial_animation_factory.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::base::geometry; -using namespace cf::ui::components; -using namespace cf::ui::components::material; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::application_support; - -// ============================================================================ -// Internal ScrollbarOverlay Widget -// ============================================================================ - -/** - * @brief Internal overlay widget for drawing custom scrollbars. - * Defined in .cpp to keep implementation details hidden. - * This overlay is transparent to mouse events - it only draws scrollbars. - * Scrollbar interaction is handled by ScrollView's eventFilter. - */ -class ScrollbarOverlay : public QWidget { - public: - ScrollbarOverlay(QWidget* viewportParent, ScrollView* scrollView) - : QWidget(viewportParent), m_scrollView(scrollView) { - // Completely transparent to mouse events - let events pass through to content - setAttribute(Qt::WA_TransparentForMouseEvents, true); - setAttribute(Qt::WA_NoSystemBackground); - } - - void paintEvent(QPaintEvent* event) override { - Q_UNUSED(event); - if (!m_scrollView) - return; - - m_scrollView->updateWidthAnimation(); - m_scrollView->updateOpacityAnimation(); - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - // Check both policy and maximum for vertical scrollbar - auto vPolicy = m_scrollView->verticalScrollBarPolicy(); - bool shouldShowVertical = - (vPolicy == Qt::ScrollBarAlwaysOn) || - (vPolicy != Qt::ScrollBarAlwaysOff && m_scrollView->verticalScrollBar()->maximum() > 0); - - // Check both policy and maximum for horizontal scrollbar - auto hPolicy = m_scrollView->horizontalScrollBarPolicy(); - bool shouldShowHorizontal = (hPolicy == Qt::ScrollBarAlwaysOn) || - (hPolicy != Qt::ScrollBarAlwaysOff && - m_scrollView->horizontalScrollBar()->maximum() > 0); - - if (shouldShowVertical) { - m_scrollView->drawVerticalScrollbar(p); - } - if (shouldShowHorizontal) { - m_scrollView->drawHorizontalScrollbar(p); - } - } - - ScrollView* scrollView() const { return m_scrollView; } - - private: - ScrollView* m_scrollView; -}; - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -ScrollView::ScrollView(QWidget* parent) : QScrollArea(parent) { - // Get animation factory from Application - m_animationFactory = - aex::WeakPtr::DynamicCast(Application::animationFactory()); - - // Use AsNeeded as default policy for custom scrollbars - setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); - setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - - // Explicitly hide native scrollbars - horizontalScrollBar()->hide(); - verticalScrollBar()->hide(); - - // Configure scroll step size - horizontalScrollBar()->setSingleStep(60); - verticalScrollBar()->setSingleStep(60); - - // Reserve space for custom scrollbars at right and bottom - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float scrollbarSpace = helper.dpToPx(20.0f); - setViewportMargins(0, 0, int(scrollbarSpace), int(scrollbarSpace)); - - // Enable mouse tracking on ScrollView - setMouseTracking(true); - viewport()->setMouseTracking(true); - - // Create overlay widget as child of viewport so it sits above - // the scrolling content and receives mouse events correctly. - m_overlay = new ScrollbarOverlay(viewport(), this); - m_overlay->setStyleSheet("background: rgba(255,0,0,0.06);"); - m_overlay->setGeometry(viewport()->rect()); - m_overlay->raise(); - m_overlay->show(); - - // Make sure we know when the viewport resizes/moves so we keep overlay aligned - viewport()->installEventFilter(this); - - // Start animation timer for smooth transitions - m_animationTimer.start(); - - // Initialize width - m_currentWidth = helper.dpToPx(12.0f); - m_targetWidth = m_currentWidth; -} - -ScrollView::~ScrollView() { - stopFadeTimer(); -} - -// ============================================================================ -// Event Handlers -// ============================================================================ - -void ScrollView::enterEvent(QEnterEvent* event) { - QScrollArea::enterEvent(event); - m_isHovering = true; - showScrollbars(); -} - -void ScrollView::leaveEvent(QEvent* event) { - QScrollArea::leaveEvent(event); - m_isHovering = false; - if (m_scrollbarFadeEnabled) { - startFadeTimer(); - } -} - -void ScrollView::resizeEvent(QResizeEvent* event) { - QScrollArea::resizeEvent(event); - if (m_overlay) { - // Keep overlay exactly over the viewport - m_overlay->setGeometry(viewport()->rect()); - m_overlay->raise(); - } -} - -void ScrollView::scrollContentsBy(int dx, int dy) { - QScrollArea::scrollContentsBy(dx, dy); - - // Mark as scrolling and show scrollbars - m_isScrolling = (dx != 0 || dy != 0); - showScrollbars(); - - // Restart fade timer - if (m_scrollbarFadeEnabled) { - startFadeTimer(); - } -} - -void ScrollView::changeEvent(QEvent* event) { - QScrollArea::changeEvent(event); - if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) { - if (m_overlay) - m_overlay->update(); // Redraw with new theme colors - } -} - -void ScrollView::mousePressEvent(QMouseEvent* event) { - QScrollArea::mousePressEvent(event); -} - -void ScrollView::mouseMoveEvent(QMouseEvent* event) { - QScrollArea::mouseMoveEvent(event); -} - -void ScrollView::mouseReleaseEvent(QMouseEvent* event) { - QScrollArea::mouseReleaseEvent(event); -} - -int ScrollView::deltaPosToScroll(int delta, float maxThumbTravel, int range) { - if (maxThumbTravel <= 0 || range <= 0) - return 0; - return int(delta * range / maxThumbTravel); -} - -void ScrollView::timerEvent(QTimerEvent* event) { - if (event->timerId() == m_fadeTimerId) { - stopFadeTimer(); - hideScrollbars(); - } - QScrollArea::timerEvent(event); -} - -bool ScrollView::eventFilter(QObject* obj, QEvent* ev) { - if (obj == viewport()) { - if (ev->type() == QEvent::Resize || ev->type() == QEvent::Move) { - if (m_overlay) { - m_overlay->setGeometry(viewport()->rect()); - m_overlay->raise(); - m_overlay->update(); - } - return false; - } - - // Handle mouse events for scrollbar interaction - switch (ev->type()) { - case QEvent::MouseButtonPress: - case QEvent::MouseButtonRelease: - case QEvent::MouseMove: { - auto* mouseEvent = static_cast(ev); - // Map to viewport coordinates (viewport is parent of overlay) - QPoint pos = mouseEvent->pos(); - - // Check if over scrollbar - bool overScrollbar = - isPointOverVerticalThumb(pos) || isPointOverHorizontalThumb(pos) || - isPointOverVerticalTrack(pos) || isPointOverHorizontalTrack(pos); - - // Always handle mouse events when dragging, otherwise only when over scrollbar - if (m_isDraggingThumb || overScrollbar) { - handleScrollbarMouseEvent(mouseEvent, pos); - return true; // Eat the event - } - - // Not over scrollbar - reset hover state if needed - if (ev->type() == QEvent::MouseMove) { - bool wasHoveringVertical = - m_isHoveringVerticalThumb || m_isHoveringVerticalTrack; - bool wasHoveringHorizontal = - m_isHoveringHorizontalThumb || m_isHoveringHorizontalTrack; - - m_isHoveringVerticalThumb = false; - m_isHoveringHorizontalThumb = false; - m_isHoveringVerticalTrack = false; - m_isHoveringHorizontalTrack = false; - - if (wasHoveringVertical || wasHoveringHorizontal) { - setScrollbarState(ScrollbarState::Idle); - viewport()->setCursor(Qt::ArrowCursor); - } - } - break; - } - case QEvent::Leave: { - // End drag if active - if (m_isDraggingThumb) { - m_isDraggingThumb = false; - setScrollbarState(ScrollbarState::Idle); - viewport()->setCursor(Qt::ArrowCursor); - } - // Reset hover state - bool wasHovering = m_isHoveringVerticalThumb || m_isHoveringHorizontalThumb || - m_isHoveringVerticalTrack || m_isHoveringHorizontalTrack; - if (wasHovering) { - m_isHoveringVerticalThumb = false; - m_isHoveringHorizontalThumb = false; - m_isHoveringVerticalTrack = false; - m_isHoveringHorizontalTrack = false; - setScrollbarState(ScrollbarState::Idle); - } - break; - } - default: - break; - } - } - return QScrollArea::eventFilter(obj, ev); -} - -// ============================================================================ -// Scrollbar Mouse Event Handler -// ============================================================================ - -void ScrollView::handleScrollbarMouseEvent(QMouseEvent* event, const QPoint& pos) { - switch (event->type()) { - case QEvent::MouseButtonPress: { - if (event->button() == Qt::LeftButton) { - if (isPointOverVerticalThumb(pos)) { - m_isDraggingThumb = true; - m_dragOrientation = Qt::Vertical; - m_dragScrollValue = verticalScrollBar()->value(); - m_dragStartPosition = pos.y(); - setScrollbarState(ScrollbarState::Dragged); - return; - } - if (isPointOverHorizontalThumb(pos)) { - m_isDraggingThumb = true; - m_dragOrientation = Qt::Horizontal; - m_dragScrollValue = horizontalScrollBar()->value(); - m_dragStartPosition = pos.x(); - setScrollbarState(ScrollbarState::Dragged); - return; - } - if (isPointOverVerticalTrack(pos)) { - QRectF thumbRect = verticalScrollbarThumbRect(); - if (pos.y() < thumbRect.top()) { - verticalScrollBar()->triggerAction(QScrollBar::SliderPageStepSub); - } else if (pos.y() > thumbRect.bottom()) { - verticalScrollBar()->triggerAction(QScrollBar::SliderPageStepAdd); - } - return; - } - if (isPointOverHorizontalTrack(pos)) { - QRectF thumbRect = horizontalScrollbarThumbRect(); - if (pos.x() < thumbRect.left()) { - horizontalScrollBar()->triggerAction(QScrollBar::SliderPageStepSub); - } else if (pos.x() > thumbRect.right()) { - horizontalScrollBar()->triggerAction(QScrollBar::SliderPageStepAdd); - } - return; - } - } - break; - } - case QEvent::MouseMove: { - bool needsUpdate = false; - - if (m_isDraggingThumb) { - if (m_dragOrientation == Qt::Vertical) { - int deltaY = pos.y() - m_dragStartPosition; - QRectF thumbRect = verticalScrollbarThumbRect(); - QRectF trackRect = verticalScrollbarTrackRect(); - - float maxThumbTravel = trackRect.height() - thumbRect.height(); - if (maxThumbTravel > 0) { - int range = verticalScrollBar()->maximum() - verticalScrollBar()->minimum(); - int newValue = m_dragScrollValue + - int(deltaPosToScroll(deltaY, maxThumbTravel, range)); - verticalScrollBar()->setValue(newValue); - } - } else { - int deltaX = pos.x() - m_dragStartPosition; - QRectF thumbRect = horizontalScrollbarThumbRect(); - QRectF trackRect = horizontalScrollbarTrackRect(); - - float maxThumbTravel = trackRect.width() - thumbRect.width(); - if (maxThumbTravel > 0) { - int range = - horizontalScrollBar()->maximum() - horizontalScrollBar()->minimum(); - int newValue = m_dragScrollValue + - int(deltaPosToScroll(deltaX, maxThumbTravel, range)); - horizontalScrollBar()->setValue(newValue); - } - } - return; - } - - // Update hover state - bool wasHoveringVertical = m_isHoveringVerticalThumb || m_isHoveringVerticalTrack; - bool wasHoveringHorizontal = m_isHoveringHorizontalThumb || m_isHoveringHorizontalTrack; - - m_isHoveringVerticalThumb = isPointOverVerticalThumb(pos); - m_isHoveringHorizontalThumb = isPointOverHorizontalThumb(pos); - m_isHoveringVerticalTrack = isPointOverVerticalTrack(pos); - m_isHoveringHorizontalTrack = isPointOverHorizontalTrack(pos); - - bool isHoveringVertical = m_isHoveringVerticalThumb || m_isHoveringVerticalTrack; - bool isHoveringHorizontal = m_isHoveringHorizontalThumb || m_isHoveringHorizontalTrack; - - if ((wasHoveringVertical != isHoveringVertical) || - (wasHoveringHorizontal != isHoveringHorizontal)) { - needsUpdate = true; - } - - if (m_isHoveringVerticalThumb || m_isHoveringHorizontalThumb) { - viewport()->setCursor(Qt::PointingHandCursor); - } else { - viewport()->setCursor(Qt::ArrowCursor); - } - - if (m_scrollbarHoverExpansion) { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - if (isHoveringVertical || isHoveringHorizontal) { - m_targetWidth = helper.dpToPx(16.0f); - } else { - m_targetWidth = helper.dpToPx(12.0f); - } - needsUpdate = true; - } - - if (needsUpdate) { - updateWidthAnimation(); - } - break; - } - case QEvent::MouseButtonRelease: { - if (m_isDraggingThumb && event->button() == Qt::LeftButton) { - m_isDraggingThumb = false; - - // Update hover state based on current mouse position - m_isHoveringVerticalThumb = isPointOverVerticalThumb(pos); - m_isHoveringHorizontalThumb = isPointOverHorizontalThumb(pos); - m_isHoveringVerticalTrack = isPointOverVerticalTrack(pos); - m_isHoveringHorizontalTrack = isPointOverHorizontalTrack(pos); - - bool isOver = m_isHoveringVerticalThumb || m_isHoveringHorizontalThumb || - m_isHoveringVerticalTrack || m_isHoveringHorizontalTrack; - - if (isOver) { - setScrollbarState(ScrollbarState::Hovered); - viewport()->setCursor(Qt::PointingHandCursor); - } else { - setScrollbarState(ScrollbarState::Idle); - viewport()->setCursor(Qt::ArrowCursor); - } - - // Ensure animation update happens after state change - updateWidthAnimation(); - if (m_overlay) - m_overlay->update(); - } - break; - } - default: - break; - } -} - -// ============================================================================ -// Paint Event -// ============================================================================ - -void ScrollView::paintEvent(QPaintEvent* event) { - // Base class draws viewport contents - QScrollArea::paintEvent(event); - // Scrollbars are drawn by the overlay widget -} - -// ============================================================================ -// Drawing Helpers -// ============================================================================ - -void ScrollView::drawVerticalScrollbar(QPainter& p) { - QRectF trackRect = verticalScrollbarTrackRect(); - drawScrollbarTrack(p, trackRect, Qt::Vertical); - - QRectF thumbRect = verticalScrollbarThumbRect(); - if (!thumbRect.isEmpty()) { - drawScrollbarThumb(p, thumbRect, Qt::Vertical); - } -} - -void ScrollView::drawHorizontalScrollbar(QPainter& p) { - QRectF trackRect = horizontalScrollbarTrackRect(); - drawScrollbarTrack(p, trackRect, Qt::Horizontal); - - QRectF thumbRect = horizontalScrollbarThumbRect(); - if (!thumbRect.isEmpty()) { - drawScrollbarThumb(p, thumbRect, Qt::Horizontal); - } -} - -void ScrollView::drawScrollbarTrack(QPainter& p, const QRectF& trackRect, - Qt::Orientation orientation) { - Q_UNUSED(orientation) - - CFColor trackColor = scrollbarTrackColor(); - QColor color = trackColor.native_color(); - color.setAlphaF(color.alphaF() * 0.5f); // Semi-transparent - - QPainterPath path = roundedRect(trackRect, scrollBarCornerRadius()); - p.fillPath(path, color); -} - -void ScrollView::drawScrollbarThumb(QPainter& p, const QRectF& thumbRect, - Qt::Orientation orientation) { - Q_UNUSED(orientation) - - if (thumbRect.isEmpty()) - return; - - // Get current state - ScrollbarState state = scrollbarState(); - float opacity = m_scrollbarOpacity; // Use animated opacity value, not the binary function - - // Draw state layer for hovered/dragged states - if (state == ScrollbarState::Hovered || state == ScrollbarState::Dragged) { - float stateLayerOpacity = (state == ScrollbarState::Dragged) ? 0.12f : 0.08f; - drawStateLayer(p, thumbRect, scrollbarStateLayerColor().native_color(), stateLayerOpacity); - } - - // Draw main thumb with Material Design colors - CFColor thumbColor = scrollbarThumbColor(); - QColor color = thumbColor.native_color(); - color.setAlphaF(color.alphaF() * opacity); - - QPainterPath path = roundedRect(thumbRect, scrollBarCornerRadius()); - p.fillPath(path, color); - - // Draw subtle inner highlight for dragged state - if (state == ScrollbarState::Dragged) { - QColor highlightColor(255, 255, 255, 30); - QPainterPath highlightPath = - roundedRect(thumbRect.adjusted(1, 1, -1, -1), scrollBarCornerRadius() * 0.8f); - p.fillPath(highlightPath, highlightColor); - } -} - -// ============================================================================ -// Scrollbar Geometry -// ============================================================================ - -QRectF ScrollView::verticalScrollbarTrackRect() const { - float w = scrollBarWidth(); - float h = scrollBarWidth(); // Same width for horizontal scrollbar - // Anchor to right edge of viewport, leave space for horizontal scrollbar at bottom - return QRectF(viewport()->width() - w, 0, w, viewport()->height() - h); -} - -QRectF ScrollView::verticalScrollbarThumbRect() const { - QScrollBar* scrollbar = verticalScrollBar(); - if (!scrollbar || scrollbar->maximum() <= 0) { - return QRectF(); - } - - QRectF trackRect = verticalScrollbarTrackRect(); - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Calculate thumb position based on scroll range - int range = scrollbar->maximum() - scrollbar->minimum(); - if (range <= 0) { - return QRectF(); - } - - float value = scrollbar->value() - scrollbar->minimum(); - float pageRatio = float(scrollbar->pageStep()) / float(range + scrollbar->pageStep()); - - float thumbHeight = std::max(qreal(helper.dpToPx(24.0f)), trackRect.height() * pageRatio); - float maxThumbY = trackRect.height() - thumbHeight; - float thumbY = trackRect.top() + (value / range) * maxThumbY; - - return QRectF(trackRect.left(), thumbY, trackRect.width(), thumbHeight); -} - -QRectF ScrollView::horizontalScrollbarTrackRect() const { - float w = scrollBarWidth(); - float h = scrollBarWidth(); - // Anchor to bottom edge of viewport, leave space for vertical scrollbar at right - return QRectF(0, viewport()->height() - h, viewport()->width() - w, h); -} - -QRectF ScrollView::horizontalScrollbarThumbRect() const { - QScrollBar* scrollbar = horizontalScrollBar(); - if (!scrollbar || scrollbar->maximum() <= 0) { - return QRectF(); - } - - QRectF trackRect = horizontalScrollbarTrackRect(); - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Calculate thumb position based on scroll range - int range = scrollbar->maximum() - scrollbar->minimum(); - if (range <= 0) { - return QRectF(); - } - - float value = scrollbar->value() - scrollbar->minimum(); - float pageRatio = float(scrollbar->pageStep()) / float(range + scrollbar->pageStep()); - - float thumbWidth = std::max(qreal(helper.dpToPx(24.0f)), trackRect.width() * pageRatio); - float maxThumbX = trackRect.width() - thumbWidth; - float thumbX = trackRect.left() + (value / range) * maxThumbX; - - return QRectF(thumbX, trackRect.top(), thumbWidth, trackRect.height()); -} - -// ============================================================================ -// Color Access Methods -// ============================================================================ - -namespace { -// Fallback colors when theme is not available -inline CFColor fallbackSurface() { - return CFColor(253, 253, 253); -} // Surface -inline CFColor fallbackSurfaceVariant() { - return CFColor(231, 224, 236); -} // Surface Variant -inline CFColor fallbackOnSurfaceVariant() { - return CFColor(49, 48, 51); -} // On Surface Variant -} // namespace - -CFColor ScrollView::backgroundColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(SURFACE)); - } catch (...) { - return fallbackSurface(); - } -} - -CFColor ScrollView::scrollbarTrackColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackSurfaceVariant(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(SURFACE_VARIANT)); - } catch (...) { - return fallbackSurfaceVariant(); - } -} - -CFColor ScrollView::scrollbarThumbColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurfaceVariant(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE_VARIANT)); - } catch (...) { - return fallbackOnSurfaceVariant(); - } -} - -// ============================================================================ -// Unit Conversion Helpers -// ============================================================================ - -float ScrollView::scrollBarWidth() const { - // Use current animated width for smooth transitions - return m_currentWidth; -} - -float ScrollView::scrollBarMargin() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(4.0f); // 4dp margin -} - -float ScrollView::scrollBarCornerRadius() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(8.0f); // 8dp corner radius -} - -float ScrollView::scrollBarOpacity() const { - // Return the animated opacity value for any external consumers - return m_scrollbarOpacity; -} - -// ============================================================================ -// Fade Effect Helpers -// ============================================================================ - -void ScrollView::showScrollbars() { - // Set target opacity and let animation handle the transition - m_targetOpacity = 1.0f; - if (m_overlay) - m_overlay->update(); -} - -void ScrollView::hideScrollbars() { - // Set target opacity and let animation handle the transition - if (!m_isHovering && !m_isScrolling) { - m_targetOpacity = 0.4f; - if (m_overlay) - m_overlay->update(); - } -} - -void ScrollView::startFadeTimer() { - stopFadeTimer(); - m_fadeTimerId = startTimer(m_scrollbarFadeDelay); -} - -void ScrollView::stopFadeTimer() { - if (m_fadeTimerId != 0) { - killTimer(m_fadeTimerId); - m_fadeTimerId = 0; - } -} - -// ============================================================================ -// Property Getters/Setters -// ============================================================================ - -bool ScrollView::scrollbarFadeEnabled() const { - return m_scrollbarFadeEnabled; -} - -void ScrollView::setScrollbarFadeEnabled(bool enabled) { - if (m_scrollbarFadeEnabled != enabled) { - m_scrollbarFadeEnabled = enabled; - if (!enabled) { - stopFadeTimer(); - showScrollbars(); - } - if (m_overlay) - m_overlay->update(); - } -} - -int ScrollView::scrollbarFadeDelay() const { - return m_scrollbarFadeDelay; -} - -void ScrollView::setScrollbarFadeDelay(int delay) { - if (m_scrollbarFadeDelay != delay && delay >= 0) { - m_scrollbarFadeDelay = delay; - } -} - -// ============================================================================ -// Size Hint -// ============================================================================ - -QSize ScrollView::sizeHint() const { - QSize hint = QScrollArea::sizeHint(); - - // Add space for scrollbars - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float scrollbarWidth = scrollBarWidth() + scrollBarMargin() * 2; - - // Only add scrollbar width if we expect scrollbars to be visible - if (verticalScrollBarPolicy() != Qt::ScrollBarAlwaysOff) { - hint.rwidth() += int(std::ceil(scrollbarWidth)); - } - if (horizontalScrollBarPolicy() != Qt::ScrollBarAlwaysOff) { - hint.rheight() += int(std::ceil(scrollbarWidth)); - } - - return hint; -} - -// ============================================================================ -// New Material Design 3 Methods -// ============================================================================ - -void ScrollView::drawStateLayer(QPainter& p, const QRectF& rect, const QColor& color, - float opacity) { - if (rect.isEmpty() || opacity <= 0.0f) - return; - - QColor stateColor = color; - stateColor.setAlphaF(opacity); - - QPainterPath path = roundedRect(rect, scrollBarCornerRadius()); - p.fillPath(path, stateColor); -} - -CFColor ScrollView::scrollbarStateLayerColor() const { - // Use primary color for state layer, or fall back to on surface variant - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurfaceVariant(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(PRIMARY)); - } catch (...) { - return fallbackOnSurfaceVariant(); - } -} - -float ScrollView::scrollBarExpandedWidth() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(16.0f); // 16dp when hovered per Material Design -} - -float ScrollView::currentScrollBarWidth() const { - return m_currentWidth; -} - -ScrollbarState ScrollView::scrollbarState() const { - if (m_isDraggingThumb) { - return ScrollbarState::Dragged; - } - if (m_isHoveringVerticalThumb || m_isHoveringHorizontalThumb || m_isHoveringVerticalTrack || - m_isHoveringHorizontalTrack) { - return ScrollbarState::Hovered; - } - return ScrollbarState::Idle; -} - -void ScrollView::setScrollbarState(ScrollbarState state) { - // Update opacity target based on state - switch (state) { - case ScrollbarState::Idle: - m_targetOpacity = 0.4f; - if (m_scrollbarHoverExpansion) { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - m_targetWidth = helper.dpToPx(12.0f); - } - break; - case ScrollbarState::Hovered: - case ScrollbarState::Dragged: - m_targetOpacity = 1.0f; - if (m_scrollbarHoverExpansion) { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - m_targetWidth = helper.dpToPx(16.0f); - } - break; - } - if (m_overlay) - m_overlay->update(); -} - -bool ScrollView::isPointOverVerticalThumb(const QPoint& pos) const { - if (verticalScrollBar()->maximum() <= 0) { - return false; - } - QRectF thumbRect = verticalScrollbarThumbRect(); - // Add some padding for easier interaction - QRectF hitRect = thumbRect.adjusted(-4, -4, 4, 4); - return hitRect.contains(pos); -} - -bool ScrollView::isPointOverHorizontalThumb(const QPoint& pos) const { - if (horizontalScrollBar()->maximum() <= 0) { - return false; - } - QRectF thumbRect = horizontalScrollbarThumbRect(); - // Add some padding for easier interaction - QRectF hitRect = thumbRect.adjusted(-4, -4, 4, 4); - return hitRect.contains(pos); -} - -bool ScrollView::isPointOverVerticalTrack(const QPoint& pos) const { - if (verticalScrollBar()->maximum() <= 0) { - return false; - } - QRectF trackRect = verticalScrollbarTrackRect(); - return trackRect.contains(pos); -} - -bool ScrollView::isPointOverHorizontalTrack(const QPoint& pos) const { - if (horizontalScrollBar()->maximum() <= 0) { - return false; - } - QRectF trackRect = horizontalScrollbarTrackRect(); - return trackRect.contains(pos); -} - -void ScrollView::updateWidthAnimation() { - // Smoothly interpolate current width towards target - float diff = m_targetWidth - m_currentWidth; - if (std::abs(diff) > 0.5f) { - m_currentWidth += diff * ANIMATION_SPEED_WIDTH; - if (m_overlay) - m_overlay->update(); - } else { - m_currentWidth = m_targetWidth; - } -} - -void ScrollView::updateOpacityAnimation() { - // Smoothly interpolate current opacity towards target - float diff = m_targetOpacity - m_scrollbarOpacity; - if (std::abs(diff) > 0.01f) { - m_scrollbarOpacity += diff * ANIMATION_SPEED_OPACITY; - if (m_overlay) - m_overlay->update(); - } else { - m_scrollbarOpacity = m_targetOpacity; - } -} - -bool ScrollView::scrollbarHoverExpansion() const { - return m_scrollbarHoverExpansion; -} - -void ScrollView::setScrollbarHoverExpansion(bool enabled) { - if (m_scrollbarHoverExpansion != enabled) { - m_scrollbarHoverExpansion = enabled; - if (m_overlay) - m_overlay->update(); - } -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/scrollview/scrollview.h b/ui/widget/material/widget/scrollview/scrollview.h deleted file mode 100644 index 9aa538b5c..000000000 --- a/ui/widget/material/widget/scrollview/scrollview.h +++ /dev/null @@ -1,421 +0,0 @@ -/** - * @file ui/widget/material/widget/scrollview/scrollview.h - * @brief Material Design 3 ScrollView widget. - * - * Implements Material Design 3 scroll area with custom scrollbars, - * fade effects, and theme integration. Supports both horizontal and - * vertical scrolling with proper Material Design styling. - * - * @author Material Design Framework Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include "aex/weak_ptr/weak_ptr.h" -#include "base/color.h" -#include "cfmaterial_animation_factory.h" -#include "export.h" -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using CFColor = cf::ui::base::CFColor; - -// Forward declaration of internal overlay class -class ScrollbarOverlay; - -/** - * @brief Scrollbar interaction states for Material Design 3. - * - * Defines the various visual states a scrollbar can be in - * following Material Design 3 state layer specifications. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -enum class ScrollbarState { - /** Idle state - 40% opacity, 12dp width */ - Idle, - /** Hovered state - 100% opacity, 16dp width */ - Hovered, - /** Dragged/Pressed state - 100% opacity, 16dp width, state layer overlay */ - Dragged -}; - -/** - * @brief Material Design 3 ScrollView widget. - * - * @details Implements Material Design 3 scroll area with custom scrollbars - * following Material Design 3 specifications. Features include: - * - Custom scrollbar drawing (12dp width) - * - Scrollbar fade in/out effects - * - Proper Material Design colors - * - Support for both horizontal and vertical scrollbars - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT ScrollView : public QScrollArea { - Q_OBJECT - Q_PROPERTY(bool scrollbarFadeEnabled READ scrollbarFadeEnabled WRITE setScrollbarFadeEnabled) - Q_PROPERTY(int scrollbarFadeDelay READ scrollbarFadeDelay WRITE setScrollbarFadeDelay) - Q_PROPERTY( - bool scrollbarHoverExpansion READ scrollbarHoverExpansion WRITE setScrollbarHoverExpansion) - - public: - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note Initializes with default Material Design styling. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit ScrollView(QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note Components are parented to this; Qt deletes them. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~ScrollView() override; - - /** - * @brief Gets whether scrollbar fade effect is enabled. - * - * @return true if fade effect is enabled, false otherwise. - * - * @throws None - * @note Fade effect hides scrollbars when not scrolling. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool scrollbarFadeEnabled() const; - - /** - * @brief Sets whether scrollbar fade effect is enabled. - * - * @param[in] enabled true to enable fade effect, false to disable. - * - * @throws None - * @note When enabled, scrollbars fade out after inactivity. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setScrollbarFadeEnabled(bool enabled); - - /** - * @brief Gets the scrollbar fade delay in milliseconds. - * - * @return Fade delay in milliseconds. - * - * @throws None - * @note Default is 500ms. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - int scrollbarFadeDelay() const; - - /** - * @brief Sets the scrollbar fade delay in milliseconds. - * - * @param[in] delay Fade delay in milliseconds. - * - * @throws None - * @note Scrollbars fade after this period of inactivity. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setScrollbarFadeDelay(int delay); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the scroll view. - * - * @throws None - * @note Based on content size and scrollbar space. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets whether scrollbar hover expansion is enabled. - * - * @return true if hover expansion is enabled, false otherwise. - * - * @throws None - * @note When enabled, scrollbar expands from 12dp to 16dp on hover. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool scrollbarHoverExpansion() const; - - /** - * @brief Sets whether scrollbar hover expansion is enabled. - * - * @param[in] enabled true to enable hover expansion, false to disable. - * - * @throws None - * @note Follows Material Design 3 hover interaction pattern. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setScrollbarHoverExpansion(bool enabled); - - protected: - /** - * @brief Paints the scroll view and custom scrollbars. - * - * @param[in] event Paint event. - * - * @throws None - * @note Completely overrides default scrollbar drawing. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles resize event. - * - * @param[in] event Resize event. - * - * @throws None - * @note Updates scrollbar geometry. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void resizeEvent(QResizeEvent* event) override; - - /** - * @brief Handles scroll event. - * - * @param[in] event Scroll event. - * - * @throws None - * @note Shows scrollbars and resets fade timer. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void scrollContentsBy(int dx, int dy) override; - - /** - * @brief Handles mouse enter event. - * - * @param[in] event Enter event. - * - * @throws None - * @note Shows scrollbars on hover. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void enterEvent(QEnterEvent* event) override; - - /** - * @brief Handles mouse leave event. - * - * @param[in] event Leave event. - * - * @throws None - * @note Starts fade timer when leaving widget. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void leaveEvent(QEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse press event. - * - * @throws None - * @note Detects scrollbar thumb drag start. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse move event. - * - * @param[in] event Mouse move event. - * - * @throws None - * @note Tracks hover over scrollbars and thumb drag. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseMoveEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse release event. - * - * @throws None - * @note Ends scrollbar thumb drag. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Timer event for scrollbar fade effect. - * - * @param[in] event Timer event. - * - * @throws None - * @note Handles fade animation timer. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void timerEvent(QTimerEvent* event) override; - - /** - * @brief Event filter for viewport resize/move tracking. - * - * @param[in] obj Watched object. - * @param[in] ev Event. - * @return true if event was handled, false otherwise. - * - * @throws None - * @note Keeps overlay synchronized with viewport geometry. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool eventFilter(QObject* obj, QEvent* ev) override; - - private: - // Internal overlay mouse event handlers (called by ScrollbarOverlay) - void overlayMousePressEvent(QMouseEvent* event); - void overlayMouseMoveEvent(QMouseEvent* event); - void overlayMouseReleaseEvent(QMouseEvent* event); - // Handle scrollbar mouse events from eventFilter - void handleScrollbarMouseEvent(QMouseEvent* event, const QPoint& pos); - // Drawing helpers - void drawVerticalScrollbar(QPainter& p); - void drawHorizontalScrollbar(QPainter& p); - void drawScrollbarTrack(QPainter& p, const QRectF& trackRect, Qt::Orientation orientation); - void drawScrollbarThumb(QPainter& p, const QRectF& thumbRect, Qt::Orientation orientation); - void drawStateLayer(QPainter& p, const QRectF& rect, const QColor& color, float opacity); - - // Scrollbar geometry calculations (using absolute coordinates) - QRectF verticalScrollbarTrackRect() const; - QRectF verticalScrollbarThumbRect() const; - QRectF horizontalScrollbarTrackRect() const; - QRectF horizontalScrollbarThumbRect() const; - - // Color access methods - CFColor backgroundColor() const; - CFColor scrollbarTrackColor() const; - CFColor scrollbarThumbColor() const; - CFColor scrollbarStateLayerColor() const; - - // Unit conversion helpers - float scrollBarWidth() const; - float scrollBarMargin() const; - float scrollBarCornerRadius() const; - float scrollBarOpacity() const; - float scrollBarExpandedWidth() const; - float currentScrollBarWidth() const; - - // State helpers - ScrollbarState scrollbarState() const; - void setScrollbarState(ScrollbarState state); - bool isPointOverVerticalThumb(const QPoint& pos) const; - bool isPointOverHorizontalThumb(const QPoint& pos) const; - bool isPointOverVerticalTrack(const QPoint& pos) const; - bool isPointOverHorizontalTrack(const QPoint& pos) const; - - // Animation helpers - void updateWidthAnimation(); - void updateOpacityAnimation(); - int deltaPosToScroll(int delta, float maxThumbTravel, int range); - friend class ScrollbarOverlay; - // Internal overlay widget (opaque pointer, defined in .cpp) - ScrollbarOverlay* m_overlay = nullptr; - - // Fade effect helpers - void showScrollbars(); - void hideScrollbars(); - void startFadeTimer(); - void stopFadeTimer(); - - // Behavior components - aex::WeakPtr m_animationFactory; - - // Scrollbar state - bool m_scrollbarFadeEnabled = true; - bool m_scrollbarHoverExpansion = true; - int m_scrollbarFadeDelay = 500; // milliseconds - int m_fadeTimerId = 0; - float m_scrollbarOpacity = 0.4f; // Initial opacity (idle state) - float m_targetOpacity = 0.4f; - float m_currentWidth = 12.0f; // Current animated width in dp - float m_targetWidth = 12.0f; // Target width in dp - bool m_isHovering = false; - bool m_isScrolling = false; - bool m_isDraggingThumb = false; - Qt::Orientation m_dragOrientation = Qt::Vertical; - int m_dragStartPosition = 0; - int m_dragScrollValue = 0; - - // Hover state tracking - bool m_isHoveringVerticalThumb = false; - bool m_isHoveringHorizontalThumb = false; - bool m_isHoveringVerticalTrack = false; - bool m_isHoveringHorizontalTrack = false; - - // Animation state - QElapsedTimer m_animationTimer; - static constexpr int ANIMATION_FRAME_MS = 16; // ~60fps - static constexpr float ANIMATION_SPEED_WIDTH = 0.3f; // Width interpolation factor - static constexpr float ANIMATION_SPEED_OPACITY = 0.2f; // Opacity interpolation factor -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/separator/separator.cpp b/ui/widget/material/widget/separator/separator.cpp deleted file mode 100644 index 21af90973..000000000 --- a/ui/widget/material/widget/separator/separator.cpp +++ /dev/null @@ -1,208 +0,0 @@ -/** - * @file separator.cpp - * @brief Material Design 3 Separator Implementation - * - * Implements a Material Design 3 separator (divider) with support for - * horizontal and vertical orientations. The separator is drawn as a 1dp - * line using OUTLINE color with 40% opacity for better visibility. Supports three - * spacing modes: Full-bleed, Inset, and Middle-inset. - * - * @author Material Design Framework Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "separator.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::application_support; - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -Separator::Separator(Qt::Orientation orientation, QWidget* parent) - : QFrame(parent), mode_(SeparatorMode::FullBleed), orientation_(orientation) { - setFrameShape(QFrame::NoFrame); - setLineWidth(0); - setMidLineWidth(0); -} - -Separator::~Separator() { - // Qt will handle cleanup of child objects -} - -// ============================================================================ -// Property Getters/Setters -// ============================================================================ - -SeparatorMode Separator::mode() const { - return mode_; -} - -void Separator::setMode(SeparatorMode mode) { - if (mode_ != mode) { - mode_ = mode; - update(); - } -} - -Qt::Orientation Separator::orientation() const { - return orientation_; -} - -void Separator::setOrientation(Qt::Orientation orientation) { - if (orientation_ != orientation) { - orientation_ = orientation; - updateGeometry(); - update(); - } -} - -// ============================================================================ -// Size Hints -// ============================================================================ - -QSize Separator::sizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Material Design separator thickness is 1dp - float thickness = helper.dpToPx(1.0f); - - if (orientation() == Qt::Horizontal) { - // Horizontal separator: fixed height (1dp), flexible width - return QSize(1, static_cast(std::ceil(thickness))); - } else { - // Vertical separator: fixed width (1dp), flexible height - return QSize(static_cast(std::ceil(thickness)), 1); - } -} - -// ============================================================================ -// Color Access -// ============================================================================ - -namespace { -// Fallback color when theme is not available -inline CFColor fallbackOutlineVariant() { - return CFColor(120, 124, 132); -} // Outline gray - -} // namespace - -CFColor Separator::separatorColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOutlineVariant(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - CFColor color = CFColor(colorScheme.queryColor(OUTLINE)); - QColor result = color.native_color(); - result.setAlphaF(0.90f); - return CFColor(result); - } catch (...) { - QColor color = fallbackOutlineVariant().native_color(); - color.setAlphaF(0.90f); - return CFColor(color); - } -} - -// ============================================================================ -// Drawing Helpers -// ============================================================================ - -QRectF Separator::separatorRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - const float thickness = helper.dpToPx(1.0f); - const float insetDp = helper.dpToPx(16.0f); - - QRectF widgetRect = QRectF(rect()); - - if (orientation() == Qt::Horizontal) { - // Horizontal separator - float yPos = widgetRect.center().y() - thickness / 2.0f; - float xPos = widgetRect.left(); - float width = widgetRect.width(); - - switch (mode_) { - case SeparatorMode::FullBleed: - // Span entire width - break; - case SeparatorMode::Inset: - // 16dp margin on both sides - xPos += insetDp; - width -= insetDp * 2.0f; - break; - case SeparatorMode::MiddleInset: - // 16dp margin on leading (left) side - xPos += insetDp; - width -= insetDp; - break; - } - - return QRectF(xPos, yPos, std::max(0.0f, width), thickness); - - } else { - // Vertical separator - float xPos = widgetRect.center().x() - thickness / 2.0f; - float yPos = widgetRect.top(); - float height = widgetRect.height(); - - switch (mode_) { - case SeparatorMode::FullBleed: - // Span entire height - break; - case SeparatorMode::Inset: - // 16dp margin on both sides - yPos += insetDp; - height -= insetDp * 2.0f; - break; - case SeparatorMode::MiddleInset: - // 16dp margin on leading (top) side - yPos += insetDp; - height -= insetDp; - break; - } - - return QRectF(xPos, yPos, thickness, std::max(0.0f, height)); - } -} - -// ============================================================================ -// Paint Event -// ============================================================================ - -void Separator::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter p(this); - // No antialiasing needed for a straight line - p.setRenderHint(QPainter::Antialiasing, false); - - QRectF lineRect = separatorRect(); - if (lineRect.width() <= 0 || lineRect.height() <= 0) { - return; // Nothing to draw - } - - CFColor color = separatorColor(); - p.fillRect(lineRect, color.native_color()); -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/separator/separator.h b/ui/widget/material/widget/separator/separator.h deleted file mode 100644 index 52d9c181c..000000000 --- a/ui/widget/material/widget/separator/separator.h +++ /dev/null @@ -1,194 +0,0 @@ -/** - * @file ui/widget/material/widget/separator/separator.h - * @brief Material Design 3 Separator widget. - * - * Implements Material Design 3 separator (divider) with support for - * horizontal and vertical orientations, full-bleed, inset, and - * middle-inset spacing modes. - * - * @author N/A - * @date N/A - * @version N/A - * @since N/A - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include "base/color.h" -#include "export.h" -#include -#include - -namespace cf::ui::widget::material { - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Separator spacing mode. - * - * @details Defines the inset/spacing behavior of the separator line. - * Full-bleed spans the entire width/height, Inset adds margins - * on both sides, and Middle-inset adds margin on one side. - * - * @since N/A - * @ingroup ui_widget_material_widget - */ -enum class SeparatorMode { - FullBleed, ///< Spans entire width/height - Inset, ///< 16dp margin on both sides - MiddleInset ///< 16dp margin on leading/start side -}; - -/** - * @brief Material Design 3 Separator widget. - * - * @details Implements Material Design 3 separator (divider) with support - * for horizontal and vertical orientations. The separator is - * drawn as a 1dp line using OUTLINE color with 40% - * opacity for better visibility. Supports three spacing modes: Full-bleed, Inset, - * and Middle-inset. - * - * @since N/A - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT Separator : public QFrame { - Q_OBJECT - Q_PROPERTY(SeparatorMode mode READ mode WRITE setMode) - - public: - /** - * @brief Constructor with orientation. - * - * @param[in] orientation Qt::Horizontal or Qt::Vertical. - * @param[in] parent QObject parent. - * - * @throws None - * @note Defaults to Full-bleed mode. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - explicit Separator(Qt::Orientation orientation = Qt::Horizontal, QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - ~Separator() override; - - /** - * @brief Gets the spacing mode. - * - * @return Current separator mode. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - SeparatorMode mode() const; - - /** - * @brief Sets the spacing mode. - * - * @param[in] mode Separator spacing mode. - * - * @throws None - * @note Changing mode triggers a repaint. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void setMode(SeparatorMode mode); - - /** - * @brief Gets the orientation. - * - * @return Current orientation (Horizontal or Vertical). - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - Qt::Orientation orientation() const; - - /** - * @brief Sets the orientation. - * - * @param[in] orientation Qt::Horizontal or Qt::Vertical. - * - * @throws None - * @note Changing orientation triggers a repaint. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void setOrientation(Qt::Orientation orientation); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the separator. - * - * @throws None - * @note Thickness is 1dp, length expands to parent. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - protected: - /** - * @brief Paints the separator. - * - * @param[in] event Paint event. - * - * @throws None - * @note Draws a 1dp line with proper inset based on mode. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - private: - /** - * @brief Gets the separator color. - * - * @return Outline variant color with 12% opacity. - * - * @throws None - * @note None - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - CFColor separatorColor() const; - - /** - * @brief Calculates the separator line rect. - * - * @return Rectangle for drawing the separator line. - * - * @throws None - * @note Applies inset based on mode and orientation. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - QRectF separatorRect() const; - - SeparatorMode mode_; - Qt::Orientation orientation_; -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/slider/slider.cpp b/ui/widget/material/widget/slider/slider.cpp deleted file mode 100644 index 6da1aef2b..000000000 --- a/ui/widget/material/widget/slider/slider.cpp +++ /dev/null @@ -1,483 +0,0 @@ -/** - * @file slider.cpp - * @brief Material Design 3 Slider Implementation - * - * Implements a Material Design 3 slider with horizontal/vertical orientations, - * active/inactive track portions, thumb with elevation, tick marks, and state layers. - * - * @author CFDesktop Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "slider.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "base/geometry_helper.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::base::geometry; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::material::base; - -// ============================================================================ -// Constants -// ============================================================================ - -namespace { -constexpr float TRACK_HEIGHT_DP = 8.0f; -constexpr float THUMB_DIAMETER_DP = 20.0f; -constexpr float TOUCH_TARGET_DP = 48.0f; -constexpr float TICK_MARK_LENGTH_DP = 8.0f; -constexpr float TICK_MARK_WIDTH_DP = 2.0f; -constexpr float TICK_MARK_TO_TRACK_GAP_DP = 4.0f; -constexpr float FOCUS_RING_MARGIN_DP = 4.0f; -} // namespace - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -Slider::Slider(QWidget* parent) - : QSlider(Qt::Horizontal, parent), - m_material(this, MaterialWidgetBase::Config{.useElevation = true, .initialElevation = 1}) { - if (orientation() == Qt::Horizontal) { - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - } else { - setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); - } - setCursor(Qt::PointingHandCursor); -} - -Slider::Slider(Qt::Orientation orientation, QWidget* parent) - : QSlider(orientation, parent), - m_material(this, MaterialWidgetBase::Config{.useElevation = true, .initialElevation = 1}) { - if (orientation == Qt::Horizontal) { - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - } else { - setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); - } - setCursor(Qt::PointingHandCursor); -} - -Slider::~Slider() { - // Components are parented to this, Qt will delete them automatically -} - -// ============================================================================ -// Event Handlers -// ============================================================================ - -void Slider::enterEvent(QEnterEvent* event) { - QSlider::enterEvent(event); - m_material.onEnterEvent(); -} - -void Slider::leaveEvent(QEvent* event) { - QSlider::leaveEvent(event); - m_material.onLeaveEvent(); -} - -void Slider::mousePressEvent(QMouseEvent* event) { - QSlider::mousePressEvent(event); - m_lastPressPos = event->pos(); - m_material.onMousePress(event->pos(), thumbRect()); -} - -void Slider::mouseReleaseEvent(QMouseEvent* event) { - QSlider::mouseReleaseEvent(event); - m_material.onMouseRelease(); -} - -void Slider::mouseMoveEvent(QMouseEvent* event) { - QSlider::mouseMoveEvent(event); -} - -void Slider::focusInEvent(QFocusEvent* event) { - QSlider::focusInEvent(event); - m_material.onFocusIn(); -} - -void Slider::focusOutEvent(QFocusEvent* event) { - QSlider::focusOutEvent(event); - m_material.onFocusOut(); -} - -void Slider::changeEvent(QEvent* event) { - QSlider::changeEvent(event); - if (event->type() == QEvent::EnabledChange) { - m_material.onEnabledChange(isEnabled()); - } -} - -// ============================================================================ -// Size Hints -// ============================================================================ - -QSize Slider::sizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float touchTarget = helper.dpToPx(TOUCH_TARGET_DP); - - if (orientation() == Qt::Horizontal) { - return QSize(200, int(std::ceil(touchTarget))); - } else { - return QSize(int(std::ceil(touchTarget)), 200); - } -} - -QSize Slider::minimumSizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float touchTarget = helper.dpToPx(TOUCH_TARGET_DP); - - if (orientation() == Qt::Horizontal) { - return QSize(120, int(std::ceil(touchTarget))); - } else { - return QSize(int(std::ceil(touchTarget)), 120); - } -} - -// ============================================================================ -// Color Access Methods -// ============================================================================ - -namespace { -inline CFColor fallbackPrimary() { - return CFColor(103, 80, 164); -} -inline CFColor fallbackSurfaceVariant() { - return CFColor(230, 225, 229); -} -inline CFColor fallbackOnPrimary() { - return CFColor(255, 255, 255); -} -inline CFColor fallbackOutline() { - return CFColor(120, 124, 132); -} -} // namespace - -CFColor Slider::activeTrackColor() const { - auto* app = application_support::Application::instance(); - if (!app) { - return fallbackPrimary(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(PRIMARY)); - } catch (...) { - return fallbackPrimary(); - } -} - -CFColor Slider::inactiveTrackColor() const { - auto* app = application_support::Application::instance(); - if (!app) { - return isEnabled() ? fallbackSurfaceVariant() : fallbackOutline(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - if (isEnabled()) { - return CFColor(colorScheme.queryColor(SURFACE_VARIANT)); - } - return CFColor(colorScheme.queryColor(OUTLINE)); - } catch (...) { - return isEnabled() ? fallbackSurfaceVariant() : fallbackOutline(); - } -} - -CFColor Slider::thumbColor() const { - auto* app = application_support::Application::instance(); - if (!app) { - return fallbackPrimary(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(PRIMARY)); - } catch (...) { - return fallbackPrimary(); - } -} - -CFColor Slider::stateLayerColor() const { - return activeTrackColor(); -} - -float Slider::trackHeight() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(TRACK_HEIGHT_DP); -} - -float Slider::thumbRadius() const { - return thumbDiameter() / 2.0f; -} - -float Slider::thumbDiameter() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(THUMB_DIAMETER_DP); -} - -// ============================================================================ -// Layout Helpers -// ============================================================================ - -float Slider::thumbPosition() const { - int minVal = minimum(); - int maxVal = maximum(); - int curVal = value(); - - if (maxVal <= minVal) { - return 0.0f; - } - - return static_cast(curVal - minVal) / static_cast(maxVal - minVal); -} - -QRectF Slider::trackRect() const { - float tHeight = trackHeight(); - float tDiameter = thumbDiameter(); - - if (orientation() == Qt::Horizontal) { - float y = (height() - tHeight) / 2.0f; - float thumbRadius = tDiameter / 2.0f; - float x = thumbRadius; - float w = width() - 2.0f * thumbRadius; - return QRectF(x, y, w, tHeight); - } else { - float x = (width() - tHeight) / 2.0f; - float thumbRadius = tDiameter / 2.0f; - float y = thumbRadius; - float h = height() - 2.0f * thumbRadius; - return QRectF(x, y, tHeight, h); - } -} - -QRectF Slider::activeTrackRect() const { - QRectF track = trackRect(); - float ratio = thumbPosition(); - - if (orientation() == Qt::Horizontal) { - float thumbCenter = track.left() + track.width() * ratio; - float activeWidth = thumbCenter - track.left(); - return QRectF(track.left(), track.top(), activeWidth, track.height()); - } else { - float thumbCenter = track.bottom() - track.height() * ratio; - float activeHeight = track.bottom() - thumbCenter; - return QRectF(track.left(), thumbCenter, track.width(), activeHeight); - } -} - -QRectF Slider::thumbRect() const { - QRectF track = trackRect(); - float ratio = thumbPosition(); - float tDiameter = thumbDiameter(); - - if (orientation() == Qt::Horizontal) { - float thumbCenter = track.left() + track.width() * ratio; - float x = thumbCenter - tDiameter / 2.0f; - float y = (height() - tDiameter) / 2.0f; - return QRectF(x, y, tDiameter, tDiameter); - } else { - float thumbCenter = track.bottom() - track.height() * ratio; - float x = (width() - tDiameter) / 2.0f; - float y = thumbCenter - tDiameter / 2.0f; - return QRectF(x, y, tDiameter, tDiameter); - } -} - -// ============================================================================ -// Paint Event -// ============================================================================ - -void Slider::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - QRectF track = trackRect(); - QRectF thumb = thumbRect(); - - // Draw inactive track - if (orientation() == Qt::Horizontal) { - qreal rightWidth = track.right() - thumb.right(); - if (rightWidth > 0) { - QRectF rightInactive(thumb.right(), track.top(), rightWidth, track.height()); - drawInactiveTrack(p, rightInactive); - } - } else { - qreal bottomHeight = track.bottom() - thumb.bottom(); - if (bottomHeight > 0) { - QRectF bottomInactive(track.left(), thumb.bottom(), track.width(), bottomHeight); - drawInactiveTrack(p, bottomInactive); - } - - qreal topHeight = thumb.top() - track.top(); - if (topHeight > 0) { - QRectF topInactive(track.left(), track.top(), track.width(), topHeight); - drawInactiveTrack(p, topInactive); - } - } - - // Draw active track - QRectF activeRect = activeTrackRect(); - if (!activeRect.isEmpty()) { - drawActiveTrack(p, activeRect); - } - - // Draw tick marks - if (tickPosition() != QSlider::NoTicks) { - drawTickMarks(p, track); - } - - // Draw thumb - drawThumb(p, thumb); - - // Draw ripple - drawRipple(p, thumb); - - // Draw focus indicator - drawFocusIndicator(p, track); -} - -// ============================================================================ -// Drawing Helpers -// ============================================================================ - -void Slider::drawInactiveTrack(QPainter& p, const QRectF& rect) { - CFColor tColor = inactiveTrackColor(); - QColor color = tColor.native_color(); - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - QPainterPath shape = roundedRect(rect, trackHeight() / 2.0f); - p.fillPath(shape, color); -} - -void Slider::drawActiveTrack(QPainter& p, const QRectF& rect) { - if (rect.width() <= 0 || rect.height() <= 0) { - return; - } - - CFColor tColor = activeTrackColor(); - QColor color = tColor.native_color(); - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - if (m_material.stateMachine() && isEnabled()) { - float opacity = m_material.stateMachine()->stateLayerOpacity(); - if (opacity > 0.0f) { - CFColor stateColor = stateLayerColor(); - QColor stateQColor = stateColor.native_color(); - stateQColor.setAlphaF(opacity); - - int r = int(color.red() * (1.0f - opacity) + stateQColor.red() * opacity); - int g = int(color.green() * (1.0f - opacity) + stateQColor.green() * opacity); - int b = int(color.blue() * (1.0f - opacity) + stateQColor.blue() * opacity); - color = QColor(r, g, b, color.alpha()); - } - } - - QPainterPath shape = roundedRect(rect, trackHeight() / 2.0f); - p.fillPath(shape, color); -} - -void Slider::drawTickMarks(QPainter& p, const QRectF& trackRect) { - CFColor tColor = inactiveTrackColor(); - QColor color = tColor.native_color(); - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - CanvasUnitHelper helper(qApp->devicePixelRatio()); - p.setPen(QPen(color, helper.dpToPx(TICK_MARK_WIDTH_DP))); - - float tickLength = helper.dpToPx(TICK_MARK_LENGTH_DP); - int tickInterval = this->tickInterval(); - if (tickInterval <= 0) { - tickInterval = 1; - } - - int minVal = minimum(); - int maxVal = maximum(); - int numTicks = (maxVal - minVal) / tickInterval; - - if (orientation() == Qt::Horizontal) { - float tickY = trackRect.bottom() + helper.dpToPx(TICK_MARK_TO_TRACK_GAP_DP); - - for (int i = 0; i <= numTicks; ++i) { - float ratio = static_cast(i) / static_cast(numTicks); - float x = trackRect.left() + trackRect.width() * ratio; - p.drawLine(QPointF(x, tickY), QPointF(x, tickY + tickLength)); - } - } else { - float tickX = trackRect.right() + helper.dpToPx(TICK_MARK_TO_TRACK_GAP_DP); - - for (int i = 0; i <= numTicks; ++i) { - float ratio = static_cast(i) / static_cast(numTicks); - float y = trackRect.bottom() - trackRect.height() * ratio; - p.drawLine(QPointF(tickX, y), QPointF(tickX + tickLength, y)); - } - } -} - -void Slider::drawThumb(QPainter& p, const QRectF& rect) { - if (m_material.elevation() && isEnabled()) { - QPainterPath thumbShape = roundedRect(rect, thumbRadius()); - m_material.elevation()->paintShadow(&p, thumbShape); - } - - CFColor tColor = thumbColor(); - QColor color = tColor.native_color(); - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - QPainterPath shape = roundedRect(rect, thumbRadius()); - p.fillPath(shape, color); -} - -void Slider::drawRipple(QPainter& p, const QRectF& rect) { - if (m_material.ripple()) { - m_material.ripple()->setColor(stateLayerColor()); - - QPainterPath clipPath = roundedRect(rect, thumbRadius()); - m_material.ripple()->paint(&p, clipPath); - } -} - -void Slider::drawFocusIndicator(QPainter& p, const QRectF& trackRect) { - if (m_material.focusIndicator()) { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float margin = helper.dpToPx(FOCUS_RING_MARGIN_DP); - - QRectF focusRect = trackRect.adjusted(-margin, -margin, margin, margin); - - QPainterPath shape = roundedRect(focusRect, trackHeight() / 2.0f + margin); - m_material.focusIndicator()->paint(&p, shape, activeTrackColor()); - } -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/slider/slider.h b/ui/widget/material/widget/slider/slider.h deleted file mode 100644 index e45e4dfe8..000000000 --- a/ui/widget/material/widget/slider/slider.h +++ /dev/null @@ -1,254 +0,0 @@ -/** - * @file ui/widget/material/widget/slider/slider.h - * @brief Material Design 3 Slider widget. - * - * Implements Material Design 3 slider with horizontal/vertical orientations, - * active/inactive track portions, thumb with elevation, tick marks, and - * state layers. - * - * @author CFDesktop Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include "base/color.h" -#include "export.h" -#include "widget/material/base/material_widget_base.h" -#include -#include - -namespace cf::ui::widget::material { - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 Slider widget. - * - * @details Implements Material Design 3 slider with horizontal/vertical - * orientations, active/inactive track portions, thumb with elevation, - * tick marks, and state layers. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT Slider : public QSlider { - Q_OBJECT - - public: - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note Defaults to horizontal orientation. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit Slider(QWidget* parent = nullptr); - - /** - * @brief Constructor with orientation. - * - * @param[in] orientation Qt::Horizontal or Qt::Vertical. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit Slider(Qt::Orientation orientation, QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~Slider() override; - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the slider. - * - * @throws None - * @note Based on Material Design specifications. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures touch target size requirements. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - protected: - /** - * @brief Paints the slider. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements Material Design paint pipeline. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles mouse enter event. - * - * @param[in] event Enter event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void enterEvent(QEnterEvent* event) override; - - /** - * @brief Handles mouse leave event. - * - * @param[in] event Leave event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void leaveEvent(QEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple and press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse move event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates drag state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseMoveEvent(QMouseEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - private: - // Drawing helpers - Material Design paint pipeline - QRectF trackRect() const; - QRectF activeTrackRect() const; - QRectF thumbRect() const; - void drawInactiveTrack(QPainter& p, const QRectF& rect); - void drawActiveTrack(QPainter& p, const QRectF& rect); - void drawTickMarks(QPainter& p, const QRectF& trackRect); - void drawThumb(QPainter& p, const QRectF& rect); - void drawRipple(QPainter& p, const QRectF& rect); - void drawFocusIndicator(QPainter& p, const QRectF& trackRect); - - // Color access methods - CFColor activeTrackColor() const; - CFColor inactiveTrackColor() const; - CFColor thumbColor() const; - CFColor stateLayerColor() const; - float trackHeight() const; - float thumbRadius() const; - float thumbDiameter() const; - - // Helper to calculate thumb position - float thumbPosition() const; - - // Behavior components - base::MaterialWidgetBase m_material; - - // Track last press position for ripple - QPoint m_lastPressPos; -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/spinbox/spinbox.cpp b/ui/widget/material/widget/spinbox/spinbox.cpp deleted file mode 100644 index aff2865fe..000000000 --- a/ui/widget/material/widget/spinbox/spinbox.cpp +++ /dev/null @@ -1,689 +0,0 @@ -/** - * @file spinbox.cpp - * @brief Material Design 3 SpinBox Implementation - * - * Implements a Material Design 3 spin box with outline style, increment/decrement - * buttons, focus indicator, and state layer effects. - * - * @author CFDesktop Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "spinbox.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "base/geometry_helper.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::base::geometry; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::material::base; -using namespace cf::ui::widget::application_support; - -// ============================================================================ -// Material Design 3 SpinBox Specifications -// ============================================================================ -namespace { -// Size specifications in dp -constexpr float kHeightDp = 56.0f; // Total height -constexpr float kHorizontalPaddingDp = 16.0f; // Left/right padding -constexpr float kButtonWidthDp = 40.0f; // Increment/decrement button width -constexpr float kButtonSpacingDp = 0.0f; // Spacing between stacked buttons -constexpr float kCornerRadiusDp = 4.0f; // ShapeSmall corner radius -constexpr float kOutlineWidthDp = 1.0f; // Outline border width - -// Icon specifications -constexpr float kIconSizeDp = 24.0f; // Button icon size -constexpr float kIconStrokeWidth = 2.5f; // Icon stroke width -} // namespace - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -SpinBox::SpinBox(QWidget* parent) - : QSpinBox(parent), m_material(this, - MaterialWidgetBase::Config{ - .useRipple = true, - .useElevation = true, - .useFocusIndicator = true, - .initialElevation = 0, - }), - m_hoveringIncrementButton(false), m_hoveringDecrementButton(false), - m_pressingIncrementButton(false), m_pressingDecrementButton(false) { - - // Disable native frame and background - setFrame(false); - setAttribute(Qt::WA_TranslucentBackground); - - // Hide the native up/down buttons - we'll draw our own Material Design buttons - // Keyboard up/down arrow keys will still work - setButtonSymbols(QSpinBox::NoButtons); - - // Make internal lineEdit transparent and borderless so it doesn't cover our custom drawing - if (lineEdit()) { - lineEdit()->setFrame(false); - lineEdit()->setAttribute(Qt::WA_TranslucentBackground); - lineEdit()->setStyleSheet("QLineEdit { background: transparent; border: none; }"); - // Set alignment to right-align numbers (standard for numeric input) - lineEdit()->setAlignment(Qt::AlignRight); - } - - // Set default font - setFont(textFont()); - - // Set cursor - setCursor(Qt::IBeamCursor); - - // Set initial text color for lineEdit - updateTextColor(); -} - -SpinBox::~SpinBox() { - // Components are parented to this, Qt will delete them automatically -} - -// ============================================================================ -// Event Handlers -// ============================================================================ - -void SpinBox::enterEvent(QEnterEvent* event) { - QSpinBox::enterEvent(event); - m_material.onEnterEvent(); -} - -void SpinBox::leaveEvent(QEvent* event) { - QSpinBox::leaveEvent(event); - m_material.onLeaveEvent(); - - // Reset button hover state - m_hoveringIncrementButton = false; - m_hoveringDecrementButton = false; -} - -void SpinBox::mousePressEvent(QMouseEvent* event) { - QPoint pos = event->pos(); - - // Check if button area was clicked first - if (isOverButtons(pos)) { - if (isOverIncrementButton(pos)) { - m_pressingIncrementButton = true; - // Trigger increment - stepUp(); - } else if (isOverDecrementButton(pos)) { - m_pressingDecrementButton = true; - // Trigger decrement - stepDown(); - } - update(); - return; - } - - // Handle text area click - QSpinBox::mousePressEvent(event); - m_material.onMousePress(pos, rect()); -} - -void SpinBox::mouseReleaseEvent(QMouseEvent* event) { - // Reset button press states - if (m_pressingIncrementButton || m_pressingDecrementButton) { - m_pressingIncrementButton = false; - m_pressingDecrementButton = false; - update(); - return; - } - - QSpinBox::mouseReleaseEvent(event); - m_material.onMouseRelease(); -} - -void SpinBox::mouseMoveEvent(QMouseEvent* event) { - QSpinBox::mouseMoveEvent(event); - updateButtonHoverState(event->pos()); -} - -void SpinBox::focusInEvent(QFocusEvent* event) { - QSpinBox::focusInEvent(event); - m_material.onFocusIn(); -} - -void SpinBox::focusOutEvent(QFocusEvent* event) { - QSpinBox::focusOutEvent(event); - m_material.onFocusOut(); -} - -void SpinBox::changeEvent(QEvent* event) { - QSpinBox::changeEvent(event); - if (event->type() == QEvent::EnabledChange) { - m_material.onEnabledChange(isEnabled()); - updateTextColor(); - update(); - } -} - -void SpinBox::resizeEvent(QResizeEvent* event) { - QSpinBox::resizeEvent(event); - // Constrain internal lineEdit to text area only, so it doesn't cover button area - if (lineEdit()) { - lineEdit()->setGeometry(textRect().toRect()); - } -} - -// ============================================================================ -// Size Hints -// ============================================================================ - -QSize SpinBox::sizeHint() const { - // Material Design spin box specifications: - // - Height: 56dp - // - Horizontal padding: 16dp - // - Button width: 40dp (total 40dp for vertical stack) - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - float height = helper.dpToPx(kHeightDp); - float hPadding = helper.dpToPx(kHorizontalPaddingDp); - float buttonWidth = helper.dpToPx(kButtonWidthDp); - - // Calculate text width based on maximum possible value - QString maxText = textFromValue(maximum()); - float textWidth = fontMetrics().horizontalAdvance(maxText); - - // Total width = padding + text + padding + button area + padding - float totalWidth = hPadding + textWidth + hPadding + buttonWidth + hPadding; - - return QSize(int(std::ceil(totalWidth)), int(std::ceil(height))); -} - -QSize SpinBox::minimumSizeHint() const { - // Minimum width based on a reasonable range of values - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - float height = helper.dpToPx(kHeightDp); - float hPadding = helper.dpToPx(kHorizontalPaddingDp); - float buttonWidth = helper.dpToPx(kButtonWidthDp); - - // Minimum text width (approximately 3-4 digits) - float minTextWidth = fontMetrics().horizontalAdvance("1000"); - float totalWidth = hPadding + minTextWidth + hPadding + buttonWidth + hPadding; - - return QSize(int(std::ceil(totalWidth)), int(std::ceil(height))); -} - -// ============================================================================ -// Layout Helpers -// ============================================================================ - -QRectF SpinBox::contentRect() const { - // Return the full widget rect as content rect - return QRectF(rect()); -} - -QRectF SpinBox::textRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - float hPadding = helper.dpToPx(kHorizontalPaddingDp); - float buttonWidth = helper.dpToPx(kButtonWidthDp); - - QRectF content = contentRect(); - float left = content.left() + hPadding; - float width = content.width() - hPadding * 2 - buttonWidth; - - return QRectF(left, content.top(), width, content.height()); -} - -QRectF SpinBox::incrementButtonRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - float buttonWidth = helper.dpToPx(kButtonWidthDp); - float halfButtonHeight = contentRect().height() / 2.0f; - - QRectF content = contentRect(); - float left = content.right() - buttonWidth; - - // Increment button is the top half - return QRectF(left, content.top(), buttonWidth, halfButtonHeight); -} - -QRectF SpinBox::decrementButtonRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - float buttonWidth = helper.dpToPx(kButtonWidthDp); - float halfButtonHeight = contentRect().height() / 2.0f; - - QRectF content = contentRect(); - float left = content.right() - buttonWidth; - - // Decrement button is the bottom half - return QRectF(left, content.top() + halfButtonHeight, buttonWidth, halfButtonHeight); -} - -QPainterPath SpinBox::shapePath() const { - QRectF content = contentRect(); - float radius = cornerRadius(); - return roundedRect(content, radius); -} - -// ============================================================================ -// Button Helpers -// ============================================================================ - -bool SpinBox::isOverIncrementButton(const QPoint& pos) const { - return incrementButtonRect().contains(pos); -} - -bool SpinBox::isOverDecrementButton(const QPoint& pos) const { - return decrementButtonRect().contains(pos); -} - -bool SpinBox::isOverButtons(const QPoint& pos) const { - return isOverIncrementButton(pos) || isOverDecrementButton(pos); -} - -void SpinBox::updateButtonHoverState(const QPoint& pos) { - bool wasHoveringIncrement = m_hoveringIncrementButton; - bool wasHoveringDecrement = m_hoveringDecrementButton; - - m_hoveringIncrementButton = isOverIncrementButton(pos); - m_hoveringDecrementButton = isOverDecrementButton(pos); - - if (wasHoveringIncrement != m_hoveringIncrementButton || - wasHoveringDecrement != m_hoveringDecrementButton) { - update(); - } -} - -// ============================================================================ -// Color Access Methods -// ============================================================================ - -namespace { -// Fallback colors when theme is not available -inline CFColor fallbackSurface() { - return CFColor(253, 253, 253); -} // Surface -inline CFColor fallbackOnSurface() { - return CFColor(27, 27, 31); -} // On Surface -inline CFColor fallbackOutline() { - return CFColor(120, 124, 132); -} // Outline -inline CFColor fallbackPrimary() { - return CFColor(103, 80, 164); -} // Purple 700 -inline CFColor fallbackOnSurfaceVariant() { - return CFColor(73, 69, 79); -} // On Surface Variant -} // namespace - -CFColor SpinBox::containerColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(SURFACE)); - } catch (...) { - return fallbackSurface(); - } -} - -CFColor SpinBox::textColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - return fallbackOnSurface(); - } -} - -CFColor SpinBox::stateLayerColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - return fallbackOnSurface(); - } -} - -CFColor SpinBox::outlineColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOutline(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(OUTLINE)); - } catch (...) { - return fallbackOutline(); - } -} - -CFColor SpinBox::focusOutlineColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackPrimary(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(PRIMARY)); - } catch (...) { - return fallbackPrimary(); - } -} - -CFColor SpinBox::buttonIconColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurfaceVariant(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE_VARIANT)); - } catch (...) { - return fallbackOnSurfaceVariant(); - } -} - -float SpinBox::cornerRadius() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(kCornerRadiusDp); -} - -QFont SpinBox::textFont() const { - auto* app = Application::instance(); - if (!app) { - // Fallback to system font with reasonable size - QFont font = QSpinBox::font(); - font.setPixelSize(16); - return font; - } - - try { - const auto& theme = app->currentTheme(); - auto& fontType = theme.font_type(); - return fontType.queryTargetFont("bodyLarge"); - } catch (...) { - QFont font = QSpinBox::font(); - font.setPixelSize(16); - return font; - } -} - -// ============================================================================ -// Paint Event -// ============================================================================ - -void SpinBox::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - QRectF content = contentRect(); - QPainterPath shape = shapePath(); - - // Step 1: Draw background - drawBackground(p, shape); - - // Step 2: Draw state layer (hover/pressed overlay) - drawStateLayer(p, shape); - - // Step 3: Draw ripple - drawRipple(p, shape); - - // Step 4: Draw outline - drawOutline(p, shape); - - // Note: Text is drawn by the internal lineEdit widget, not here - - // Step 5: Draw increment/decrement buttons - drawButtons(p, QRectF(incrementButtonRect().left(), incrementButtonRect().top(), - decrementButtonRect().width(), - incrementButtonRect().height() + decrementButtonRect().height())); - - // Step 6: Draw focus indicator - drawFocusIndicator(p, shape); -} - -// ============================================================================ -// Drawing Helpers -// ============================================================================ - -void SpinBox::drawBackground(QPainter& p, const QPainterPath& shape) { - CFColor bg = containerColor(); - - // Handle disabled state - if (!isEnabled()) { - QColor color = bg.native_color(); - p.fillPath(shape, color); - return; - } - - p.fillPath(shape, bg.native_color()); -} - -void SpinBox::drawStateLayer(QPainter& p, const QPainterPath& shape) { - if (!isEnabled() || !m_material.stateMachine()) { - return; - } - - float opacity = m_material.stateMachine()->stateLayerOpacity(); - if (opacity <= 0.0f) { - return; - } - - CFColor stateColor = stateLayerColor(); - QColor color = stateColor.native_color(); - color.setAlphaF(color.alphaF() * opacity); - - p.fillPath(shape, color); -} - -void SpinBox::drawRipple(QPainter& p, const QPainterPath& shape) { - if (m_material.ripple()) { - // Set ripple color based on text color - m_material.ripple()->setColor(textColor()); - m_material.ripple()->paint(&p, shape); - } -} - -void SpinBox::drawOutline(QPainter& p, const QPainterPath& shape) { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float inset = 0.5f; - - QColor color; - if (hasFocus()) { - color = focusOutlineColor().native_color(); - } else { - color = outlineColor().native_color(); - } - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - p.save(); - - // Create inset path for outline - QRectF shapeBounds = shape.boundingRect(); - QRectF insetRect = shapeBounds.adjusted(inset, inset, -inset, -inset); - float adjustedRadius = std::max(0.0f, cornerRadius() - inset); - QPainterPath insetShape = roundedRect(insetRect, adjustedRadius); - - // Calculate outline width - float baseWidth = helper.dpToPx(1.0f); - float activeWidth = helper.dpToPx(2.0f); - float currentWidth = hasFocus() ? activeWidth : baseWidth; - - QPen pen(color, currentWidth); - pen.setCosmetic(true); // Keep consistent width across DPI - p.setPen(pen); - p.setBrush(Qt::NoBrush); - p.drawPath(insetShape); - - p.restore(); -} - -void SpinBox::drawText(QPainter& p, const QRectF& textRect) { - // The internal lineEdit handles text display and cursor rendering. - // This function is now a no-op - color updates are handled by updateTextColor(). - Q_UNUSED(p) - Q_UNUSED(textRect) -} - -void SpinBox::updateTextColor() { - // Update the lineEdit text color based on current state - if (lineEdit()) { - QPalette pal = lineEdit()->palette(); - QColor textColorVal = textColor().native_color(); - if (!isEnabled()) { - textColorVal.setAlphaF(0.38f); - } - pal.setColor(QPalette::Text, textColorVal); - pal.setColor(QPalette::WindowText, textColorVal); - lineEdit()->setPalette(pal); - } -} - -void SpinBox::drawButtons(QPainter& p, const QRectF& buttonRect) { - Q_UNUSED(buttonRect) - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Draw increment button (up arrow) - QRectF incRect = incrementButtonRect(); - if (!incRect.isEmpty()) { - p.save(); - - // Hover overlay for increment button - if (m_hoveringIncrementButton || m_pressingIncrementButton) { - QColor overlay = buttonIconColor().native_color(); - overlay.setAlphaF(m_pressingIncrementButton ? 0.12f : 0.08f); - p.fillRect(incRect, overlay); - } - - // Draw up arrow icon - QColor iconColor = buttonIconColor().native_color(); - if (!isEnabled()) { - iconColor.setAlphaF(0.38f); - } - - p.setPen(QPen(iconColor, helper.dpToPx(2.0f))); - p.setRenderHint(QPainter::Antialiasing); - - float cx = incRect.center().x(); - float cy = incRect.center().y(); - float size = helper.dpToPx(12.0f); // Arrow size - float half = size / 2.0f; - - // Draw up arrow (triangle) - QPainterPath upArrow; - upArrow.moveTo(cx, cy - half); // Top point - upArrow.lineTo(cx - half, cy + half * 0.5f); // Bottom left - upArrow.lineTo(cx + half, cy + half * 0.5f); // Bottom right - upArrow.closeSubpath(); - - p.fillPath(upArrow, iconColor); - - p.restore(); - } - - // Draw decrement button (down arrow) - QRectF decRect = decrementButtonRect(); - if (!decRect.isEmpty()) { - p.save(); - - // Hover overlay for decrement button - if (m_hoveringDecrementButton || m_pressingDecrementButton) { - QColor overlay = buttonIconColor().native_color(); - overlay.setAlphaF(m_pressingDecrementButton ? 0.12f : 0.08f); - p.fillRect(decRect, overlay); - } - - // Draw down arrow icon - QColor iconColor = buttonIconColor().native_color(); - if (!isEnabled()) { - iconColor.setAlphaF(0.38f); - } - - p.setPen(QPen(iconColor, helper.dpToPx(2.0f))); - p.setRenderHint(QPainter::Antialiasing); - - float cx = decRect.center().x(); - float cy = decRect.center().y(); - float size = helper.dpToPx(12.0f); // Arrow size - float half = size / 2.0f; - - // Draw down arrow (triangle) - QPainterPath downArrow; - downArrow.moveTo(cx, cy + half); // Bottom point - downArrow.lineTo(cx - half, cy - half * 0.5f); // Top left - downArrow.lineTo(cx + half, cy - half * 0.5f); // Top right - downArrow.closeSubpath(); - - p.fillPath(downArrow, iconColor); - - p.restore(); - } - - // Draw separator line between buttons - p.save(); - QColor separatorColor = outlineColor().native_color(); - if (!isEnabled()) { - separatorColor.setAlphaF(0.38f); - } - - float separatorX = incrementButtonRect().left(); - float separatorY = incrementButtonRect().bottom(); - float separatorWidth = helper.dpToPx(1.0f); - QRectF separatorRect(separatorX, separatorY - separatorWidth / 2, incrementButtonRect().width(), - separatorWidth); - - p.fillRect(separatorRect, separatorColor); - p.restore(); -} - -void SpinBox::drawFocusIndicator(QPainter& p, const QPainterPath& shape) { - if (m_material.focusIndicator() && hasFocus()) { - m_material.focusIndicator()->paint(&p, shape, focusOutlineColor()); - } -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/spinbox/spinbox.h b/ui/widget/material/widget/spinbox/spinbox.h deleted file mode 100644 index 0ffabc0e4..000000000 --- a/ui/widget/material/widget/spinbox/spinbox.h +++ /dev/null @@ -1,271 +0,0 @@ -/** - * @file ui/widget/material/widget/spinbox/spinbox.h - * @brief Material Design 3 SpinBox widget. - * - * Implements Material Design 3 spin box with support for integer input, - * increment/decrement buttons, outline style, focus indicator, and - * state layer effects. - * - * @author CFDesktop Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include -#include - -#include "base/color.h" -#include "export.h" -#include "widget/material/base/material_widget_base.h" -#include -#include - -namespace cf::ui::widget::material { - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 SpinBox widget. - * - * @details Implements Material Design 3 spin box with support for integer - * input, increment/decrement buttons, outline style, focus indicator, - * and state layer effects following Material Design 3 specifications. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT SpinBox : public QSpinBox { - Q_OBJECT - - public: - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit SpinBox(QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~SpinBox() override; - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the spin box. - * - * @throws None - * @note Based on content width, button width, and padding. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures touch target size requirements. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - protected: - /** - * @brief Paints the spin box. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements 7-step Material Design paint pipeline. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles mouse enter event. - * - * @param[in] event Enter event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void enterEvent(QEnterEvent* event) override; - - /** - * @brief Handles mouse leave event. - * - * @param[in] event Leave event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void leaveEvent(QEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple and press state for button area. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse move event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Tracks hover state over button areas. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseMoveEvent(QMouseEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Handles widget resize event. - * - * @param[in] event Resize event. - * - * @throws None - * @note Constrains internal lineEdit to text area only. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void resizeEvent(QResizeEvent* event) override; - - private: - // Drawing helpers - 7-step paint pipeline - void drawBackground(QPainter& p, const QPainterPath& shape); - void drawStateLayer(QPainter& p, const QPainterPath& shape); - void drawRipple(QPainter& p, const QPainterPath& shape); - void drawOutline(QPainter& p, const QPainterPath& shape); - void drawText(QPainter& p, const QRectF& textRect); - void drawButtons(QPainter& p, const QRectF& buttonRect); - void drawFocusIndicator(QPainter& p, const QPainterPath& shape); - - // Layout helpers - QRectF contentRect() const; - QRectF textRect() const; - QRectF incrementButtonRect() const; - QRectF decrementButtonRect() const; - QPainterPath shapePath() const; - - // Button helpers - bool isOverIncrementButton(const QPoint& pos) const; - bool isOverDecrementButton(const QPoint& pos) const; - bool isOverButtons(const QPoint& pos) const; - void updateButtonHoverState(const QPoint& pos); - - // Update lineEdit text color based on current state - void updateTextColor(); - - // Color access methods - CFColor containerColor() const; - CFColor textColor() const; - CFColor stateLayerColor() const; - CFColor outlineColor() const; - CFColor focusOutlineColor() const; - CFColor buttonIconColor() const; - float cornerRadius() const; - QFont textFont() const; - - // Behavior components - base::MaterialWidgetBase m_material; - - // Internal state - bool m_hoveringIncrementButton; - bool m_hoveringDecrementButton; - bool m_pressingIncrementButton; - bool m_pressingDecrementButton; -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/switch/switch.cpp b/ui/widget/material/widget/switch/switch.cpp deleted file mode 100644 index f181041f3..000000000 --- a/ui/widget/material/widget/switch/switch.cpp +++ /dev/null @@ -1,500 +0,0 @@ -/** - * @file switch.cpp - * @brief Material Design 3 Switch Implementation - * - * Implements a Material Design 3 switch (toggle) with animated thumb - * position, track color transitions, state layers, and focus indicators. - * - * @author CFDesktop Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "switch.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "base/easing.h" -#include "base/geometry_helper.h" -#include "components/material/cfmaterial_property_animation.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::base::geometry; -using namespace cf::ui::components::material; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::material::base; - -// ============================================================================ -// Constants -// ============================================================================ - -namespace { -constexpr float TRACK_WIDTH_DP = 52.0f; -constexpr float TRACK_HEIGHT_DP = 32.0f; -constexpr float THUMB_DIAMETER_DP = 16.0f; -constexpr float THUMB_MARGIN_DP = 8.0f; -constexpr float TEXT_SPACING_DP = 12.0f; -constexpr float TOUCH_TARGET_DP = 48.0f; -} // namespace - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -Switch::Switch(QWidget* parent) - : QCheckBox(parent), - m_material(this, MaterialWidgetBase::Config{.useElevation = true, .initialElevation = 1}) { - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - - m_thumbPosition = isChecked() ? 1.0f : 0.0f; - if (isChecked()) { - m_material.stateMachine()->onCheckedChanged(true); - } - - setCursor(Qt::PointingHandCursor); -} - -Switch::Switch(const QString& text, QWidget* parent) : Switch(parent) { - setText(text); -} - -Switch::~Switch() { - // Components are parented to this, Qt will delete them automatically -} - -void Switch::setChecked(bool checked) { - if (isChecked() == checked) { - return; - } - QCheckBox::setChecked(checked); - - if (!m_inNextCheckState) { - if (m_material.stateMachine()) { - m_material.stateMachine()->onCheckedChanged(checked); - } - m_thumbPosition = checked ? 1.0f : 0.0f; - update(); - } -} - -// ============================================================================ -// Event Handlers -// ============================================================================ - -void Switch::enterEvent(QEnterEvent* event) { - QCheckBox::enterEvent(event); - m_material.onEnterEvent(); -} - -void Switch::leaveEvent(QEvent* event) { - QCheckBox::leaveEvent(event); - m_material.onLeaveEvent(); -} - -void Switch::mousePressEvent(QMouseEvent* event) { - QCheckBox::mousePressEvent(event); - m_material.onMousePress(event->pos(), trackRect()); -} - -void Switch::mouseReleaseEvent(QMouseEvent* event) { - QCheckBox::mouseReleaseEvent(event); - m_material.onMouseRelease(); -} - -void Switch::focusInEvent(QFocusEvent* event) { - QCheckBox::focusInEvent(event); - m_material.onFocusIn(); -} - -void Switch::focusOutEvent(QFocusEvent* event) { - QCheckBox::focusOutEvent(event); - m_material.onFocusOut(); -} - -void Switch::changeEvent(QEvent* event) { - QCheckBox::changeEvent(event); - if (event->type() == QEvent::EnabledChange) { - m_material.onEnabledChange(isEnabled()); - } -} - -void Switch::nextCheckState() { - m_inNextCheckState = true; - QCheckBox::nextCheckState(); - m_inNextCheckState = false; - - if (m_material.stateMachine()) { - m_material.stateMachine()->onCheckedChanged(isChecked()); - } - - startThumbPositionAnimation(isChecked() ? 1.0f : 0.0f); -} - -bool Switch::hitButton(const QPoint& pos) const { - return rect().contains(pos); -} - -// ============================================================================ -// Size Hints -// ============================================================================ - -QSize Switch::sizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - float trackW = trackWidth(); - float spacing = helper.dpToPx(TEXT_SPACING_DP); - float textWidth = text().isEmpty() ? 0.0f : fontMetrics().horizontalAdvance(text()); - - float width = trackW + spacing + textWidth; - float minWidth = helper.dpToPx(TOUCH_TARGET_DP); - width = std::max(width, minWidth); - - float height = helper.dpToPx(TOUCH_TARGET_DP); - - return QSize(int(std::ceil(width)), int(std::ceil(height))); -} - -QSize Switch::minimumSizeHint() const { - return sizeHint(); -} - -// ============================================================================ -// Color Access Methods -// ============================================================================ - -namespace { -inline CFColor fallbackPrimary() { - return CFColor(103, 80, 164); -} -inline CFColor fallbackOutline() { - return CFColor(120, 124, 132); -} -inline CFColor fallbackSurface() { - return CFColor(232, 226, 232); -} -inline CFColor fallbackOnPrimary() { - return CFColor(255, 255, 255); -} -inline CFColor fallbackOnSurface() { - return CFColor(29, 27, 32); -} -} // namespace - -CFColor Switch::trackColor() const { - auto* app = application_support::Application::instance(); - if (!app) { - return isChecked() ? fallbackPrimary() : fallbackOutline(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - - if (isChecked()) { - return CFColor(colorScheme.queryColor(PRIMARY)); - } - return CFColor(colorScheme.queryColor(OUTLINE)); - } catch (...) { - return isChecked() ? fallbackPrimary() : fallbackOutline(); - } -} - -CFColor Switch::thumbColor() const { - auto* app = application_support::Application::instance(); - if (!app) { - return isChecked() ? fallbackOnPrimary() : fallbackSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - - if (isChecked()) { - return CFColor(colorScheme.queryColor(ON_PRIMARY)); - } - return CFColor(colorScheme.queryColor(SURFACE)); - } catch (...) { - return isChecked() ? fallbackOnPrimary() : fallbackSurface(); - } -} - -CFColor Switch::thumbIconColor() const { - auto* app = application_support::Application::instance(); - if (!app) { - return fallbackPrimary(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(PRIMARY)); - } catch (...) { - return fallbackPrimary(); - } -} - -CFColor Switch::stateLayerColor() const { - return trackColor(); -} - -CFColor Switch::labelColor() const { - auto* app = application_support::Application::instance(); - if (!app) { - return fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - return fallbackOnSurface(); - } -} - -float Switch::trackCornerRadius() const { - return trackHeight() / 2.0f; -} - -float Switch::thumbRadius() const { - return thumbDiameter() / 2.0f; -} - -// ============================================================================ -// Dimension Helpers -// ============================================================================ - -float Switch::trackWidth() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(TRACK_WIDTH_DP); -} - -float Switch::trackHeight() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(TRACK_HEIGHT_DP); -} - -float Switch::thumbDiameter() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(THUMB_DIAMETER_DP); -} - -// ============================================================================ -// Layout Helpers -// ============================================================================ - -QRectF Switch::trackRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float trackH = trackHeight(); - float y = (height() - trackH) / 2.0f; - float x = 0; - return QRectF(x, y, trackWidth(), trackH); -} - -QRectF Switch::thumbRect() const { - QRectF track = trackRect(); - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float margin = helper.dpToPx(THUMB_MARGIN_DP); - - float maxTravel = track.width() - 2.0f * margin - thumbDiameter(); - float currentX = track.left() + margin + maxTravel * m_thumbPosition; - float y = track.top() + (track.height() - thumbDiameter()) / 2.0f; - - return QRectF(currentX, y, thumbDiameter(), thumbDiameter()); -} - -QRectF Switch::textRect() const { - QRectF track = trackRect(); - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float spacing = helper.dpToPx(TEXT_SPACING_DP); - float x = track.right() + spacing; - float y = 0; - float w = width() - x; - float h = height(); - - return QRectF(x, y, w, h); -} - -// ============================================================================ -// Animation Helpers -// ============================================================================ - -void Switch::startThumbPositionAnimation(float target) { - auto factory = aex::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - if (!factory) { - m_thumbPosition = target; - update(); - return; - } - - auto anim = factory->createPropertyAnimation(&m_thumbPosition, m_thumbPosition, target, 200, - cf::ui::base::Easing::Type::Standard, this); - - if (anim) { - if (auto* propAnim = dynamic_cast(anim.Get())) { - propAnim->setRange(m_thumbPosition, target); - } - anim->start(); - } else { - m_thumbPosition = target; - update(); - } -} - -// ============================================================================ -// Paint Event -// ============================================================================ - -void Switch::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - QRectF track = trackRect(); - - // Step 1: Draw track background - drawTrack(p, track); - - // Step 2: Draw ripple - drawRipple(p, track); - - // Step 3: Draw thumb - drawThumb(p, thumbRect()); - - // Step 4: Draw text - if (!text().isEmpty()) { - drawText(p, textRect()); - } - - // Step 5: Draw focus indicator - drawFocusIndicator(p, track); -} - -// ============================================================================ -// Drawing Helpers -// ============================================================================ - -void Switch::drawTrack(QPainter& p, const QRectF& rect) { - CFColor tColor = trackColor(); - QColor color = tColor.native_color(); - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - if (m_material.stateMachine() && isEnabled()) { - float opacity = m_material.stateMachine()->stateLayerOpacity(); - if (opacity > 0.0f) { - CFColor stateColor = stateLayerColor(); - QColor stateQColor = stateColor.native_color(); - stateQColor.setAlphaF(opacity); - - if (!isChecked()) { - auto* app = application_support::Application::instance(); - if (app) { - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - QColor surface = CFColor(colorScheme.queryColor(SURFACE)).native_color(); - int r = int(surface.red() * (1.0f - opacity) + stateQColor.red() * opacity); - int g = - int(surface.green() * (1.0f - opacity) + stateQColor.green() * opacity); - int b = - int(surface.blue() * (1.0f - opacity) + stateQColor.blue() * opacity); - color = QColor(r, g, b, color.alpha()); - } catch (...) { - int r = int(color.red() * (1.0f - opacity) + stateQColor.red() * opacity); - int g = - int(color.green() * (1.0f - opacity) + stateQColor.green() * opacity); - int b = int(color.blue() * (1.0f - opacity) + stateQColor.blue() * opacity); - color = QColor(r, g, b, color.alpha()); - } - } else { - int r = int(color.red() * (1.0f - opacity) + stateQColor.red() * opacity); - int g = int(color.green() * (1.0f - opacity) + stateQColor.green() * opacity); - int b = int(color.blue() * (1.0f - opacity) + stateQColor.blue() * opacity); - color = QColor(r, g, b, color.alpha()); - } - } else { - int r = int(color.red() * (1.0f - opacity) + stateQColor.red() * opacity); - int g = int(color.green() * (1.0f - opacity) + stateQColor.green() * opacity); - int b = int(color.blue() * (1.0f - opacity) + stateQColor.blue() * opacity); - color = QColor(r, g, b, color.alpha()); - } - } - } - - QPainterPath shape = roundedRect(rect, trackCornerRadius()); - p.fillPath(shape, color); -} - -void Switch::drawThumb(QPainter& p, const QRectF& rect) { - if (m_material.elevation() && isEnabled()) { - QPainterPath thumbShape = roundedRect(rect, thumbRadius()); - m_material.elevation()->paintShadow(&p, thumbShape); - } - - CFColor tColor = thumbColor(); - QColor color = tColor.native_color(); - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - QPainterPath shape = roundedRect(rect, thumbRadius()); - p.fillPath(shape, color); -} - -void Switch::drawRipple(QPainter& p, const QRectF& rect) { - if (m_material.ripple()) { - m_material.ripple()->setColor(stateLayerColor()); - - QPainterPath clipPath = roundedRect(rect, trackCornerRadius()); - m_material.ripple()->paint(&p, clipPath); - } -} - -void Switch::drawText(QPainter& p, const QRectF& rect) { - CFColor textColor = labelColor(); - - if (!isEnabled()) { - QColor color = textColor.native_color(); - color.setAlphaF(0.38f); - p.setPen(color); - } else { - p.setPen(textColor.native_color()); - } - - p.setFont(font()); - - QRectF textBounds = rect.adjusted(0, 2, 0, -2); - p.drawText(textBounds, Qt::AlignLeft | Qt::AlignVCenter, text()); -} - -void Switch::drawFocusIndicator(QPainter& p, const QRectF& rect) { - if (m_material.focusIndicator()) { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float margin = helper.dpToPx(4.0f); - QRectF focusRect = rect.adjusted(-margin, -margin, margin, margin); - - QPainterPath shape = roundedRect(focusRect, trackCornerRadius() + margin); - m_material.focusIndicator()->paint(&p, shape, trackColor()); - } -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/switch/switch.h b/ui/widget/material/widget/switch/switch.h deleted file mode 100644 index 0f0f98382..000000000 --- a/ui/widget/material/widget/switch/switch.h +++ /dev/null @@ -1,288 +0,0 @@ -/** - * @file ui/widget/material/widget/switch/switch.h - * @brief Material Design 3 Switch widget. - * - * Implements Material Design 3 switch (toggle) with animated thumb position, - * track color transitions, and state layers. Provides an alternative to - * checkboxes for binary on/off settings. - * - * @author CFDesktop Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include "base/color.h" -#include "export.h" -#include "widget/material/base/material_widget_base.h" -#include -#include - -namespace cf::ui::widget::material { - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 Switch widget. - * - * @details Implements Material Design 3 switch (toggle) with animated thumb - * position, track color transitions, and state layers. Provides an - * alternative to checkboxes for binary on/off settings. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT Switch : public QCheckBox { - Q_OBJECT - - public: - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit Switch(QWidget* parent = nullptr); - - /** - * @brief Constructor with text. - * - * @param[in] text Switch text label. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit Switch(const QString& text, QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~Switch() override; - - /** - * @brief Sets the checked state. - * - * @param[in] checked true to check, false to uncheck. - * - * @throws None - * @note Updates thumb position animation. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setChecked(bool checked); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the switch. - * - * @throws None - * @note Based on Material Design 52x32dp switch track. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures touch target size requirements. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - protected: - /** - * @brief Paints the switch. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements Material Design paint pipeline. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles mouse enter event. - * - * @param[in] event Enter event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void enterEvent(QEnterEvent* event) override; - - /** - * @brief Handles mouse leave event. - * - * @param[in] event Leave event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void leaveEvent(QEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple and press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Handles check state change event. - * - * @param[in] event State change event. - * - * @throws None - * @note Triggers thumb position animation. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void nextCheckState() override; - - /** - * @brief Determines if a point is within the clickable button area. - * - * @param[in] pos The point to check, in widget coordinates. - * @return true if the point is within the clickable area, false otherwise. - * - * @throws None - * @note Makes entire widget clickable. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool hitButton(const QPoint& pos) const override; - - private: - // Drawing helpers - Material Design paint pipeline - QRectF trackRect() const; - QRectF thumbRect() const; - QRectF textRect() const; - void drawTrack(QPainter& p, const QRectF& rect); - void drawThumb(QPainter& p, const QRectF& rect); - void drawRipple(QPainter& p, const QRectF& rect); - void drawText(QPainter& p, const QRectF& rect); - void drawFocusIndicator(QPainter& p, const QRectF& rect); - - // Animation helper - void startThumbPositionAnimation(float target); - - // Color access methods - CFColor trackColor() const; - CFColor thumbColor() const; - CFColor thumbIconColor() const; - CFColor stateLayerColor() const; - CFColor labelColor() const; - float trackCornerRadius() const; - float thumbRadius() const; - - // Helper to get dimensions in pixels - float trackWidth() const; - float trackHeight() const; - float thumbDiameter() const; - - // Behavior components - base::MaterialWidgetBase m_material; - - // Thumb position animation progress (0.0 = unchecked/left, 1.0 = checked/right) - float m_thumbPosition = 0.0f; - // Guard flag: true when in nextCheckState(), prevents setChecked from - // snapping position so animation can handle the transition smoothly - bool m_inNextCheckState = false; -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/tableview/tableview.cpp b/ui/widget/material/widget/tableview/tableview.cpp deleted file mode 100644 index a60f8acf2..000000000 --- a/ui/widget/material/widget/tableview/tableview.cpp +++ /dev/null @@ -1,547 +0,0 @@ -/** - * @file tableview.cpp - * @brief Material Design 3 TableView Implementation - * - * Implements a Material Design 3 table view with custom header rendering, - * grid lines, row selection effects, and Material Design color tokens. - * - * @author Material Design Framework Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "tableview.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::material::base; -using namespace cf::ui::widget::application_support; - -// ============================================================================ -// Constants -// ============================================================================ - -namespace { -constexpr float kHeaderHeightDp = 48.0f; -constexpr float kRowHeightCompactDp = 48.0f; -constexpr float kRowHeightStandardDp = 56.0f; -constexpr float kSelectedRowAlpha = 0.12f; // 12% opacity -constexpr float kHoverAlpha = 0.08f; // 8% opacity - -// Fallback colors -inline CFColor fallbackOnSurface() { - return CFColor(27, 27, 31); -} -inline CFColor fallbackOutlineVariant() { - return CFColor(188, 188, 188); -} -inline CFColor fallbackPrimaryContainer() { - return CFColor(234, 221, 255); -} -} // namespace - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -TableView::TableView(QWidget* parent) - : QTableView(parent), m_material(this, - MaterialWidgetBase::Config{ - .useRipple = true, - .useElevation = false, - .useFocusIndicator = true, - }), - rowHeight_(TableRowHeight::Standard), gridStyle_(TableGridStyle::Horizontal), - showHeader_(true), alternatingRowColors_(false), rippleEnabled_(true), - m_hasValidPressPos(false), m_hoveredRow(-1), m_pressedRow(-1) { - // Configure default QTableView properties for Material rendering - setAttribute(Qt::WA_Hover, true); - setMouseTracking(true); - - // Disable default Qt styling - we handle all rendering - setStyleSheet("QTableView { border: none; background: transparent; }"); - - // Configure header - auto* header = horizontalHeader(); - if (header) { - header->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter); - header->setStretchLastSection(false); - header->setSectionsMovable(true); - header->setSectionsClickable(true); - header->setSortIndicatorShown(true); - } - - auto* vHeader = verticalHeader(); - if (vHeader) { - vHeader->setVisible(false); - vHeader->setDefaultSectionSize(static_cast(rowHeightValue())); - } - - // Enable alternating row colors by default - QTableView::setAlternatingRowColors(alternatingRowColors_); -} - -TableView::~TableView() { - // Components are parented to this, Qt will delete them automatically -} - -// ============================================================================ -// Property Getters/Setters -// ============================================================================ - -TableRowHeight TableView::rowHeight() const { - return rowHeight_; -} - -void TableView::setRowHeight(TableRowHeight height) { - if (rowHeight_ != height) { - rowHeight_ = height; - auto* vHeader = verticalHeader(); - if (vHeader) { - vHeader->setDefaultSectionSize(static_cast(rowHeightValue())); - } - updateGeometry(); - update(); - } -} - -TableGridStyle TableView::gridStyle() const { - return gridStyle_; -} - -void TableView::setGridStyle(TableGridStyle style) { - if (gridStyle_ != style) { - gridStyle_ = style; - update(); - } -} - -bool TableView::showHeader() const { - return showHeader_; -} - -void TableView::setShowHeader(bool show) { - if (showHeader_ != show) { - showHeader_ = show; - auto* header = horizontalHeader(); - if (header) { - header->setVisible(show); - } - updateGeometry(); - update(); - } -} - -bool TableView::alternatingRowColors() const { - return alternatingRowColors_; -} - -void TableView::setAlternatingRowColors(bool enabled) { - if (alternatingRowColors_ != enabled) { - alternatingRowColors_ = enabled; - // Also set the base class property for internal state tracking - QTableView::setAlternatingRowColors(enabled); - update(); - } -} - -bool TableView::rippleEnabled() const { - return rippleEnabled_; -} - -void TableView::setRippleEnabled(bool enabled) { - if (rippleEnabled_ != enabled) { - rippleEnabled_ = enabled; - if (!enabled && m_material.ripple()) { - m_material.ripple()->onCancel(); - } - } -} - -// ============================================================================ -// Size Hints -// ============================================================================ - -QSize TableView::sizeHint() const { - // Calculate size based on content - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - int rowCount = model() ? model()->rowCount() : 0; - int columnCount = model() ? model()->columnCount() : 0; - - float width = 0; - auto* hHeader = horizontalHeader(); - if (hHeader && columnCount > 0) { - for (int i = 0; i < columnCount; ++i) { - width += hHeader->sectionSize(i); - } - } - if (width < helper.dpToPx(200.0f)) { - width = helper.dpToPx(200.0f); - } - - float height = 0; - if (showHeader_ && hHeader) { - height += headerHeightValue(); - } - if (rowCount > 0) { - height += rowCount * rowHeightValue(); - } else { - // Show at least 5 rows for empty tables - height += 5 * rowHeightValue(); - } - - return QSize(int(std::ceil(width)), int(std::ceil(height))); -} - -QSize TableView::minimumSizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Minimum width for 2 columns - float minWidth = helper.dpToPx(200.0f); - - // Minimum height for header + 3 rows - float minHeight = headerHeightValue() + 3 * rowHeightValue(); - - return QSize(int(std::ceil(minWidth)), int(std::ceil(minHeight))); -} - -// ============================================================================ -// Event Handlers -// ============================================================================ - -void TableView::paintEvent(QPaintEvent* event) { - // First, let Qt draw the base table content (cells, selection, etc.) - // This uses the item delegate to render each cell - QTableView::paintEvent(event); - - // Then, overlay our Material Design custom rendering on the viewport - QPainter p(viewport()); - p.setRenderHint(QPainter::Antialiasing); - - QRectF viewportRect = viewport()->rect(); - - // Draw hover state overlay (semi-transparent) - if (model() && m_hoveredRow >= 0) { - QRect rowRect = visualRect(model()->index(m_hoveredRow, 0)); - if (rowRect.intersects(viewportRect.toRect())) { - rowRect.setLeft(0); - rowRect.setWidth(viewportRect.width()); - CFColor hoverColor = onSurfaceColor(); - QColor color = hoverColor.native_color(); - color.setAlphaF(kHoverAlpha); - p.fillRect(rowRect, color); - } - } - - // Draw ripple effects - if (rippleEnabled_ && m_material.ripple() && (m_pressedRow >= 0 || m_hoveredRow >= 0)) { - int row = (m_pressedRow >= 0) ? m_pressedRow : m_hoveredRow; - if (row >= 0) { - QRect rowRect = visualRect(model()->index(row, 0)); - rowRect.setLeft(0); - rowRect.setWidth(viewportRect.width()); - QPainterPath clipPath; - clipPath.addRect(rowRect); - m_material.ripple()->setColor(onSurfaceColor()); - m_material.ripple()->paint(&p, clipPath); - } - } - - // Draw grid lines - if (gridStyle_ != TableGridStyle::None) { - drawGridLines(p, viewportRect); - } - - // Draw focus indicator around the actual content area - if (hasFocus() && m_material.focusIndicator() && model()) { - // Calculate actual content bounds - QRectF contentRect = viewportRect; - - // Get the actual row and column count - int rowCount = model()->rowCount(); - int columnCount = model()->columnCount(); - - if (rowCount > 0 && columnCount > 0) { - // Get bottom-right corner of the last visible cell - QModelIndex lastIndex = model()->index(rowCount - 1, columnCount - 1); - QRect lastCellRect = visualRect(lastIndex); - if (lastCellRect.isValid()) { - qreal contentWidth = static_cast(lastCellRect.right() + 1); - qreal contentHeight = static_cast(lastCellRect.bottom() + 1); - contentRect.setWidth(qMin(contentWidth, viewportRect.width())); - contentRect.setHeight(qMin(contentHeight, viewportRect.height())); - } - } - - drawFocusIndicator(p, contentRect); - } -} - -void TableView::mousePressEvent(QMouseEvent* event) { - QTableView::mousePressEvent(event); - - if (event->button() == Qt::LeftButton) { - m_pressPos = event->pos(); - m_hasValidPressPos = true; - - // Convert to viewport coordinates: event->pos() is in view coords, indexAt() needs viewport - // coords - QPoint viewportPos = viewport()->mapFrom(this, event->pos()); - QModelIndex index = indexAt(viewportPos); - int row = index.row(); - if (row >= 0) { - m_pressedRow = row; - if (m_material.stateMachine()) { - m_material.stateMachine()->onPress(event->pos()); - } - if (m_material.ripple() && rippleEnabled_) { - QRect rowRect = visualRect(model()->index(row, 0)); - rowRect.setLeft(0); - rowRect.setWidth(viewport()->width()); - // Ripple expects viewport coordinates since rowRect is in viewport coords - m_material.ripple()->onPress(viewportPos, rowRect); - } - update(); - } - } -} - -void TableView::mouseReleaseEvent(QMouseEvent* event) { - QTableView::mouseReleaseEvent(event); - - if (m_pressedRow >= 0) { - if (m_material.stateMachine()) { - m_material.stateMachine()->onRelease(); - } - if (m_material.ripple() && rippleEnabled_) { - m_material.ripple()->onRelease(); - } - m_pressedRow = -1; - m_hasValidPressPos = false; - update(); - } -} - -void TableView::mouseMoveEvent(QMouseEvent* event) { - QTableView::mouseMoveEvent(event); - - // Convert to viewport coordinates: event->pos() is in view coords, indexAt() needs viewport - // coords - QPoint viewportPos = viewport()->mapFrom(this, event->pos()); - QModelIndex index = indexAt(viewportPos); - int row = index.row(); - - if (m_hoveredRow != row) { - m_hoveredRow = row; - if (row >= 0 && m_material.stateMachine()) { - m_material.stateMachine()->onHoverEnter(); - } - update(); - } -} - -void TableView::enterEvent(QEnterEvent* event) { - QTableView::enterEvent(event); - m_material.onEnterEvent(); -} - -void TableView::leaveEvent(QEvent* event) { - QTableView::leaveEvent(event); - m_material.onLeaveEvent(); - m_hoveredRow = -1; - m_pressedRow = -1; - update(); -} - -void TableView::focusInEvent(QFocusEvent* event) { - QTableView::focusInEvent(event); - m_material.onFocusIn(); -} - -void TableView::focusOutEvent(QFocusEvent* event) { - QTableView::focusOutEvent(event); - m_material.onFocusOut(); -} - -void TableView::changeEvent(QEvent* event) { - QTableView::changeEvent(event); - if (event->type() == QEvent::EnabledChange) { - m_material.onEnabledChange(isEnabled()); - } -} - -// ============================================================================ -// Drawing Helpers -// ============================================================================ - -void TableView::drawGridLines(QPainter& p, const QRectF& viewportRect) { - if (!model()) - return; - - p.save(); - - CFColor gridColor = outlineVariantColor(); - QPen pen(gridColor.native_color(), 1.0); - pen.setCosmetic(true); - p.setPen(pen); - - // Draw vertical lines - if (gridStyle_ == TableGridStyle::Vertical || gridStyle_ == TableGridStyle::Both) { - int columnCount = model()->columnCount(); - for (int col = 0; col < columnCount; ++col) { - if (!isColumnHidden(col)) { - float x = columnViewportPosition(col) + columnWidth(col); - p.drawLine(QPointF(x, viewportRect.top()), QPointF(x, viewportRect.bottom())); - } - } - } - - // Draw horizontal lines - use actual row positions from visualRect - if (gridStyle_ == TableGridStyle::Horizontal || gridStyle_ == TableGridStyle::Both) { - int rowCount = model()->rowCount(); - for (int row = 0; row <= rowCount; ++row) { - if (row < rowCount && !isRowHidden(row)) { - QRect rowRect = visualRect(model()->index(row, 0)); - // Draw line at bottom of each row - float y = rowRect.bottom(); - p.drawLine(QPointF(viewportRect.left(), y), QPointF(viewportRect.right(), y)); - } else if (row == rowCount) { - // Draw line after the last visible row - // Find the last visible row and draw below it - for (int r = rowCount - 1; r >= 0; --r) { - if (!isRowHidden(r)) { - QRect rowRect = visualRect(model()->index(r, 0)); - float y = rowRect.bottom(); - p.drawLine(QPointF(viewportRect.left(), y), - QPointF(viewportRect.right(), y)); - break; - } - } - } - } - } - - p.restore(); -} - -void TableView::drawFocusIndicator(QPainter& p, const QRectF& rect) { - if (m_material.focusIndicator()) { - // Create focus indicator path (inset to account for both ring width and spacing) - // Ring width: 3dp, Additional inset: 3dp = Total 6dp - QPainterPath path; - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float inset = helper.dpToPx(2.0f); - QRectF focusRect = rect.adjusted(-inset, -inset, inset, inset); - path.addRoundedRect(focusRect, 4, 4); - - m_material.focusIndicator()->paint(&p, path, primaryContainerColor()); - } -} - -// ============================================================================ -// Row State Helpers -// ============================================================================ - -bool TableView::isRowHovered(int row) const { - return m_hoveredRow == row; -} - -bool TableView::isRowPressed(int row) const { - return m_pressedRow == row; -} - -bool TableView::isRowSelected(int row) const { - if (!selectionModel() || !model()) { - return false; - } - QModelIndex index = model()->index(row, 0); - return selectionModel()->isSelected(index); -} - -// ============================================================================ -// Layout Helpers -// ============================================================================ - -float TableView::rowHeightValue() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - switch (rowHeight_) { - case TableRowHeight::Compact: - return helper.dpToPx(kRowHeightCompactDp); - case TableRowHeight::Standard: - return helper.dpToPx(kRowHeightStandardDp); - } - return helper.dpToPx(kRowHeightStandardDp); -} - -float TableView::headerHeightValue() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(kHeaderHeightDp); -} - -// ============================================================================ -// Color Access Methods -// ============================================================================ - -CFColor TableView::onSurfaceColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - return fallbackOnSurface(); - } -} - -CFColor TableView::outlineVariantColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOutlineVariant(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(OUTLINE_VARIANT)); - } catch (...) { - return fallbackOutlineVariant(); - } -} - -CFColor TableView::primaryContainerColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackPrimaryContainer(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(PRIMARY_CONTAINER)); - } catch (...) { - return fallbackPrimaryContainer(); - } -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/tableview/tableview.h b/ui/widget/material/widget/tableview/tableview.h deleted file mode 100644 index a8e2e902e..000000000 --- a/ui/widget/material/widget/tableview/tableview.h +++ /dev/null @@ -1,414 +0,0 @@ -/** - * @file ui/widget/material/widget/tableview/tableview.h - * @brief Material Design 3 TableView widget. - * - * Implements Material Design 3 table view for two-dimensional data display. - * Features custom header rendering, grid lines, row selection with ripple, - * sort indicators, column resize feedback, and Material Design color tokens. - * - * @author Material Design Framework Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include "base/color.h" -#include "export.h" -#include "widget/material/base/material_widget_base.h" -#include -#include - -namespace cf::ui::widget::material { - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Table row height mode. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -enum class TableRowHeight { - Compact, ///< 48dp height for dense content - Standard ///< 56dp height for standard content -}; - -/** - * @brief Grid line style. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -enum class TableGridStyle { - None, ///< No grid lines - Horizontal, ///< Horizontal lines only - Vertical, ///< Vertical lines only - Both ///< Both horizontal and vertical lines -}; - -/** - * @brief Material Design 3 TableView widget. - * - * @details Implements Material Design 3 table view with two-dimensional - * data display support. Features include: - * - Custom table header rendering with 48dp height - * - Grid lines with OutlineVariant color - * - Row selection with PrimaryContainer overlay (12% opacity) - * - Ripple effects on row interaction - * - Sort indicators for sortable columns - * - Column resize visual feedback - * - Alternating row colors (SurfaceVariant 5% opacity) - * - Material Design 3 color tokens - * - Focus indicators for keyboard navigation - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT TableView : public QTableView { - Q_OBJECT - Q_PROPERTY(TableRowHeight rowHeight READ rowHeight WRITE setRowHeight) - Q_PROPERTY(TableGridStyle gridStyle READ gridStyle WRITE setGridStyle) - Q_PROPERTY(bool showHeader READ showHeader WRITE setShowHeader) - Q_PROPERTY(bool alternatingRowColors READ alternatingRowColors WRITE setAlternatingRowColors) - Q_PROPERTY(bool rippleEnabled READ rippleEnabled WRITE setRippleEnabled) - - public: - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note Defaults to Standard row height and Both grid style. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit TableView(QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note Components are parented to this, Qt deletes them. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~TableView() override; - - /** - * @brief Gets the row height mode. - * - * @return Current row height mode. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - TableRowHeight rowHeight() const; - - /** - * @brief Sets the row height mode. - * - * @param[in] height Row height mode to use. - * - * @throws None - * @note Triggers layout update. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setRowHeight(TableRowHeight height); - - /** - * @brief Gets the grid line style. - * - * @return Current grid line style. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - TableGridStyle gridStyle() const; - - /** - * @brief Sets the grid line style. - * - * @param[in] style Grid line style to use. - * - * @throws None - * @note Triggers repaint. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setGridStyle(TableGridStyle style); - - /** - * @brief Gets whether the header is shown. - * - * @return true if header is visible, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool showHeader() const; - - /** - * @brief Sets whether to show the header. - * - * @param[in] show true to show header, false to hide. - * - * @throws None - * @note Triggers layout update. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setShowHeader(bool show); - - /** - * @brief Gets whether alternating row colors are enabled. - * - * @return true if alternating row colors are enabled. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool alternatingRowColors() const; - - /** - * @brief Sets whether to use alternating row colors. - * - * @param[in] enabled true to enable alternating colors, false to disable. - * - * @throws None - * @note Triggers repaint. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setAlternatingRowColors(bool enabled); - - /** - * @brief Gets whether ripple effect is enabled. - * - * @return true if ripple is enabled, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool rippleEnabled() const; - - /** - * @brief Sets whether ripple effect is enabled. - * - * @param[in] enabled true to enable ripple, false to disable. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setRippleEnabled(bool enabled); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the table view. - * - * @throws None - * @note Based on content and minimum dimensions. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum size hint. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures minimum content visibility. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - protected: - /** - * @brief Paints the table view. - * - * @param[in] event Paint event. - * - * @throws None - * @note Completely overrides default table rendering. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple and state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse move event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Tracks hover state for row highlighting. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseMoveEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse enter event. - * - * @param[in] event Enter event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void enterEvent(QEnterEvent* event) override; - - /** - * @brief Handles mouse leave event. - * - * @param[in] event Leave event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void leaveEvent(QEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - private: - // Drawing helpers - viewport-based rendering - void drawGridLines(QPainter& p, const QRectF& viewportRect); - void drawFocusIndicator(QPainter& p, const QRectF& rect); - - // Row state helpers - bool isRowHovered(int row) const; - bool isRowPressed(int row) const; - bool isRowSelected(int row) const; - - // Layout helpers - float rowHeightValue() const; - float headerHeightValue() const; - - // Color access methods - CFColor onSurfaceColor() const; - CFColor outlineVariantColor() const; - CFColor primaryContainerColor() const; - - // Behavior components - base::MaterialWidgetBase m_material; - - // Properties - TableRowHeight rowHeight_; - TableGridStyle gridStyle_; - bool showHeader_; - bool alternatingRowColors_; - bool rippleEnabled_; - - // Cached press position for ripple - QPoint m_pressPos; - bool m_hasValidPressPos; - - // Hover/pressed row tracking - int m_hoveredRow; - int m_pressedRow; -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/tabview/private/materialtabbar.cpp b/ui/widget/material/widget/tabview/private/materialtabbar.cpp deleted file mode 100644 index 48a9eca86..000000000 --- a/ui/widget/material/widget/tabview/private/materialtabbar.cpp +++ /dev/null @@ -1,503 +0,0 @@ -/** - * @file materialtabbar.cpp - * @brief Material Design 3 TabBar Implementation - * - * @author Material Design Framework Team - * @date 2026-03-18 - * @version 0.1 - * @ingroup ui_widget_material - */ - -#include "materialtabbar.h" -#include "../tabview.h" - -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::components::material; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::material::base; -using namespace cf::ui::widget::application_support; - -namespace { -constexpr int DEFAULT_TAB_HEIGHT_DP = 48; -constexpr int DEFAULT_TAB_MIN_WIDTH_DP = 120; -constexpr int INDICATOR_HEIGHT_DP = 3; - -inline CFColor fallbackSurface() { - return CFColor(253, 253, 253); -} -inline CFColor fallbackOnSurfaceVariant() { - return CFColor(117, 117, 117); -} -inline CFColor fallbackPrimary() { - return CFColor(103, 80, 164); -} -} // namespace - -// ============================================================================ -// MaterialTabBar Implementation -// ============================================================================ - -MaterialTabBar::MaterialTabBar(TabView* parent) - : QTabBar(parent), m_tabView(parent), m_tabHeightDp(DEFAULT_TAB_HEIGHT_DP), - m_tabMinWidthDp(DEFAULT_TAB_MIN_WIDTH_DP), m_showIndicator(true), m_indicatorPosition(0.0f), - m_indicatorTargetPosition(0.0f), m_lastIndex(-1), m_hoveredIndex(-1), m_pressedIndex(-1), - m_closeButtonHoveredIndex(-1), m_material(this, base::MaterialWidgetBase::Config{ - .useRipple = true, - .useElevation = false, - .useFocusIndicator = true, - }) { - - setDrawBase(false); - setDocumentMode(true); - setExpanding(false); - setUsesScrollButtons(true); - setElideMode(Qt::ElideRight); - setMouseTracking(true); - - connect(this, &QTabBar::currentChanged, this, &MaterialTabBar::animateIndicatorTo); -} - -MaterialTabBar::~MaterialTabBar() { - // Components are parented to this, Qt will delete them -} - -void MaterialTabBar::setTabHeightDp(int height) { - if (m_tabHeightDp != height) { - m_tabHeightDp = height; - updateGeometry(); - } -} - -void MaterialTabBar::setTabMinWidthDp(int width) { - if (m_tabMinWidthDp != width) { - m_tabMinWidthDp = width; - updateGeometry(); - } -} - -void MaterialTabBar::setShowIndicator(bool show) { - if (m_showIndicator != show) { - m_showIndicator = show; - update(); - } -} - -void MaterialTabBar::setTabCloseable(int index, bool closeable) { - if (closeable) { - m_closeableTabs.insert(index); - } else { - m_closeableTabs.remove(index); - } - updateGeometry(); - update(); -} - -bool MaterialTabBar::isTabCloseable(int index) const { - return m_closeableTabs.contains(index); -} - -QSize MaterialTabBar::tabSizeHint(int index) const { - Q_UNUSED(index) - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float minWidth = helper.dpToPx(static_cast(m_tabMinWidthDp)); - float height = helper.dpToPx(static_cast(m_tabHeightDp)); - - return QSize(static_cast(std::ceil(minWidth)), static_cast(std::ceil(height))); -} - -void MaterialTabBar::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - p.fillRect(rect(), backgroundColor().native_color()); - - for (int i = 0; i < count(); ++i) { - drawTab(p, i); - } - - if (m_showIndicator) { - drawIndicator(p); - } -} - -void MaterialTabBar::drawTab(QPainter& p, int index) { - QRect tabRect = this->tabRect(index); - if (tabRect.isEmpty()) { - return; - } - - drawTabStateLayer(p, tabRect, index); - drawTabContent(p, tabRect, index); - - if (index == currentIndex() && hasFocus() && m_material.focusIndicator()) { - QPainterPath shape; - shape.addRoundedRect(tabRect.adjusted(2, 2, -2, -2), 4, 4); - m_material.focusIndicator()->paint(&p, shape, focusIndicatorColor()); - } -} - -void MaterialTabBar::drawTabStateLayer(QPainter& p, const QRect& tabRect, int index) { - if (!isEnabled() || !m_material.stateMachine()) { - return; - } - - bool isHovered = (index == m_hoveredIndex); - bool isPressed = (index == m_pressedIndex); - bool isSelected = (index == currentIndex()); - - if (!isHovered && !isPressed && !isSelected) { - return; - } - - float opacity = isPressed ? 0.12f : (isHovered ? 0.08f : 0.08f); - - if (opacity <= 0.0f) { - return; - } - - CFColor stateColor = stateLayerColor(); - QColor color = stateColor.native_color(); - color.setAlphaF(color.alphaF() * opacity); - p.fillRect(tabRect, color); -} - -void MaterialTabBar::drawTabContent(QPainter& p, const QRect& tabRect, int index) { - CFColor textColor = (index == currentIndex()) ? selectedTabTextColor() : tabTextColor(); - - if (!isEnabled()) { - QColor color = textColor.native_color(); - color.setAlphaF(0.38f); - p.setPen(color); - } else { - p.setPen(textColor.native_color()); - } - - QFont font = tabFont(); - p.setFont(font); - - QString text = this->tabText(index); - QRect textRect = tabRect.adjusted(16, 0, -16, 0); - - QIcon icon = this->tabIcon(index); - if (!icon.isNull()) { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float iconSize = helper.dpToPx(24.0f); - float iconGap = helper.dpToPx(8.0f); - - QRect iconRect(textRect.left(), textRect.center().y() - iconSize / 2, - static_cast(iconSize), static_cast(iconSize)); - icon.paint(&p, iconRect); - textRect.setLeft(iconRect.right() + static_cast(iconGap)); - } - - // Handle closeable tabs - reserve space for close button - if (isTabCloseable(index)) { - QRect closeRect = closeButtonRect(index); - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float closeGap = helper.dpToPx(8.0f); - textRect.setRight(closeRect.left() - static_cast(closeGap)); - drawCloseIcon(p, closeRect, index); - } - - QString elidedText = fontMetrics().elidedText(text, Qt::ElideRight, textRect.width()); - p.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, elidedText); -} - -void MaterialTabBar::drawIndicator(QPainter& p) { - if (count() == 0) { - return; - } - - updateIndicatorPosition(); - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float indicatorHeight = helper.dpToPx(static_cast(INDICATOR_HEIGHT_DP)); - int currentIndexVal = currentIndex(); - - if (currentIndexVal < 0 || currentIndexVal >= count()) { - return; - } - - QRect currentTabRect = tabRect(currentIndexVal); - float indicatorWidth = currentTabRect.width() - 32; - float indicatorX = currentTabRect.left() + 16; - float indicatorY = height() - indicatorHeight; - - QRectF indicatorRect(indicatorX, indicatorY, indicatorWidth, indicatorHeight); - - float radius = indicatorHeight / 2.0f; - QPainterPath path; - path.addRoundedRect(indicatorRect, radius, radius); - p.fillPath(path, indicatorColor().native_color()); -} - -void MaterialTabBar::animateIndicatorTo(int index) { - if (index < 0 || index >= count()) { - return; - } - - int oldIndex = m_lastIndex; - m_lastIndex = index; - - auto factory = - aex::WeakPtr::DynamicCast(Application::animationFactory()); - if (!factory || !factory->isAllEnabled()) { - m_indicatorPosition = static_cast(tabRect(index).left()); - update(); - return; - } - - m_indicatorPosition = static_cast(tabRect(index).left()); - update(); -} - -void MaterialTabBar::updateIndicatorPosition() { - if (!m_indicatorAnimation || !m_indicatorAnimation.Get()) { - int current = currentIndex(); - if (current >= 0 && current < count()) { - m_indicatorPosition = static_cast(tabRect(current).left()); - } - } -} - -QRect MaterialTabBar::closeButtonRect(int index) const { - if (index < 0 || index >= count()) { - return QRect(); - } - - QRect tabRect = this->tabRect(index); - if (tabRect.isEmpty()) { - return QRect(); - } - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float closeSize = helper.dpToPx(18.0f); - float rightMargin = helper.dpToPx(8.0f); - - int closeX = tabRect.right() - static_cast(closeSize) - static_cast(rightMargin); - int closeY = tabRect.center().y() - static_cast(closeSize / 2); - - return QRect(closeX, closeY, static_cast(closeSize), static_cast(closeSize)); -} - -bool MaterialTabBar::isOverCloseButton(int index, const QPoint& pos) const { - if (!isTabCloseable(index)) { - return false; - } - QRect closeRect = closeButtonRect(index); - return closeRect.contains(pos); -} - -void MaterialTabBar::updateCloseButtonHover(const QPoint& pos) { - int newCloseHoverIndex = -1; - - for (int i = 0; i < count(); ++i) { - if (isOverCloseButton(i, pos)) { - newCloseHoverIndex = i; - break; - } - } - - if (newCloseHoverIndex != m_closeButtonHoveredIndex) { - m_closeButtonHoveredIndex = newCloseHoverIndex; - update(); - } -} - -void MaterialTabBar::drawCloseIcon(QPainter& p, const QRect& closeRect, int index) { - bool isHovered = (index == m_closeButtonHoveredIndex); - bool isSelected = (index == currentIndex()); - - CFColor baseColor = isSelected ? selectedTabTextColor() : tabTextColor(); - QColor color = baseColor.native_color(); - - if (isHovered) { - color.setAlphaF(0.87f); - } else { - color.setAlphaF(0.6f); - } - - p.setPen(QPen(color, 1.5)); - - float pad = closeRect.width() * 0.25f; - - QPointF p1(closeRect.left() + pad, closeRect.top() + pad); - QPointF p2(closeRect.right() - pad, closeRect.bottom() - pad); - - QPointF p3(closeRect.right() - pad, closeRect.top() + pad); - QPointF p4(closeRect.left() + pad, closeRect.bottom() - pad); - - p.drawLine(p1, p2); - p.drawLine(p3, p4); -} - -void MaterialTabBar::enterEvent(QEnterEvent* event) { - QTabBar::enterEvent(event); - m_material.onEnterEvent(); - update(); -} - -void MaterialTabBar::leaveEvent(QEvent* event) { - QTabBar::leaveEvent(event); - m_material.onLeaveEvent(); - m_hoveredIndex = -1; - m_closeButtonHoveredIndex = -1; - update(); -} - -void MaterialTabBar::mouseMoveEvent(QMouseEvent* event) { - int index = tabAt(event->pos()); - if (index != m_hoveredIndex) { - m_hoveredIndex = index; - update(); - } - - // Track close button hover - updateCloseButtonHover(event->pos()); - - QTabBar::mouseMoveEvent(event); -} - -void MaterialTabBar::mousePressEvent(QMouseEvent* event) { - int index = tabAt(event->pos()); - if (index >= 0) { - m_pressedIndex = index; - m_material.onMousePress(event->pos(), QRectF(tabRect(index))); - } - QTabBar::mousePressEvent(event); - update(); -} - -void MaterialTabBar::mouseReleaseEvent(QMouseEvent* event) { - m_pressedIndex = -1; - m_material.onMouseRelease(); - - // Check if close button was clicked - int index = tabAt(event->pos()); - if (index >= 0 && isOverCloseButton(index, event->pos())) { - emit tabCloseRequested(index); - update(); - return; // Don't process as tab click - } - - QTabBar::mouseReleaseEvent(event); - update(); -} - -void MaterialTabBar::focusInEvent(QFocusEvent* event) { - QTabBar::focusInEvent(event); - m_material.onFocusIn(); - update(); -} - -void MaterialTabBar::focusOutEvent(QFocusEvent* event) { - QTabBar::focusOutEvent(event); - m_material.onFocusOut(); - update(); -} - -void MaterialTabBar::changeEvent(QEvent* event) { - QTabBar::changeEvent(event); - if (event->type() == QEvent::EnabledChange) { - m_material.onEnabledChange(isEnabled()); - update(); - } -} - -CFColor MaterialTabBar::backgroundColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackSurface(); - } - try { - const auto& theme = app->currentTheme(); - return CFColor(theme.color_scheme().queryColor(SURFACE)); - } catch (...) { - return fallbackSurface(); - } -} - -CFColor MaterialTabBar::tabTextColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurfaceVariant(); - } - try { - const auto& theme = app->currentTheme(); - return CFColor(theme.color_scheme().queryColor(ON_SURFACE_VARIANT)); - } catch (...) { - return fallbackOnSurfaceVariant(); - } -} - -CFColor MaterialTabBar::selectedTabTextColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackPrimary(); - } - try { - const auto& theme = app->currentTheme(); - return CFColor(theme.color_scheme().queryColor(PRIMARY)); - } catch (...) { - return fallbackPrimary(); - } -} - -CFColor MaterialTabBar::indicatorColor() const { - return selectedTabTextColor(); -} - -CFColor MaterialTabBar::stateLayerColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurfaceVariant(); - } - try { - const auto& theme = app->currentTheme(); - return CFColor(theme.color_scheme().queryColor(ON_SURFACE)); - } catch (...) { - return fallbackOnSurfaceVariant(); - } -} - -CFColor MaterialTabBar::focusIndicatorColor() const { - return selectedTabTextColor(); -} - -QFont MaterialTabBar::tabFont() const { - auto* app = Application::instance(); - if (!app) { - QFont font = QTabBar::font(); - font.setPixelSize(14); - font.setWeight(QFont::Medium); - return font; - } - try { - const auto& theme = app->currentTheme(); - return theme.font_type().queryTargetFont("labelLarge"); - } catch (...) { - QFont font = QTabBar::font(); - font.setPixelSize(14); - font.setWeight(QFont::Medium); - return font; - } -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/tabview/private/materialtabbar.h b/ui/widget/material/widget/tabview/private/materialtabbar.h deleted file mode 100644 index 90bea76de..000000000 --- a/ui/widget/material/widget/tabview/private/materialtabbar.h +++ /dev/null @@ -1,390 +0,0 @@ -/** - * @file materialtabbar.h - * @brief Material Design 3 TabBar - Internal Implementation - * - * Internal TabBar implementation for TabView. Not exposed in public API. - * - * @author Material Design Framework Team - * @date 2026-03-18 - * @version 0.1 - * @ingroup ui_widget_material - */ - -#pragma once - -#include -#include -#include -#include -#include - -#include "base/device_pixel.h" - -#include "aex/weak_ptr/weak_ptr.h" -#include "material/widget/button/button.h" -#include "widget/material/base/material_widget_base.h" - -namespace cf::ui::widget::material { - -// Forward declarations -class TabView; - -using CFColor = base::CFColor; -/** - * @brief Internal Material TabBar implementation. - * - * Not exported - used only by TabView internally. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class MaterialTabBar : public QTabBar { - Q_OBJECT - - public: - /** - * @brief Constructor. - * - * @param[in] parent Parent TabView widget. - * - * @throws None - * @note Initializes with default Material Design styling. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit MaterialTabBar(TabView* parent); - - /** - * @brief Destructor. - * - * @throws None - * @note Components are parented to this; Qt deletes them. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~MaterialTabBar() override; - - /** - * @brief Gets the tab height. - * - * @return Tab height in device-independent pixels. - * - * @throws None - * @note Default follows Material Design 3 specifications. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - int tabHeightDp() const { return m_tabHeightDp; } - - /** - * @brief Sets the tab height. - * - * @param[in] height Tab height in device-independent pixels. - * - * @throws None - * @note Triggers layout update. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setTabHeightDp(int height); - - /** - * @brief Gets the minimum tab width. - * - * @return Minimum tab width in device-independent pixels. - * - * @throws None - * @note Default follows Material Design 3 specifications. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - int tabMinWidthDp() const { return m_tabMinWidthDp; } - - /** - * @brief Sets the minimum tab width. - * - * @param[in] width Minimum tab width in device-independent pixels. - * - * @throws None - * @note Triggers layout update. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setTabMinWidthDp(int width); - - /** - * @brief Gets whether the selection indicator is shown. - * - * @return true if indicator is visible, false otherwise. - * - * @throws None - * @note The indicator slides to the selected tab. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool showIndicator() const { return m_showIndicator; } - - /** - * @brief Sets whether the selection indicator is shown. - * - * @param[in] show true to show indicator, false to hide. - * - * @throws None - * @note Triggers repaint. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setShowIndicator(bool show); - - /** - * @brief Sets whether a tab can be closed. - * - * @param[in] index Tab index. - * @param[in] closeable true to make tab closeable, false otherwise. - * - * @throws None - * @note Closeable tabs display a close button. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setTabCloseable(int index, bool closeable); - - /** - * @brief Gets whether a tab can be closed. - * - * @param[in] index Tab index. - * @return true if tab is closeable, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool isTabCloseable(int index) const; - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the tab bar. - * - * @throws None - * @note Calculates based on tab height and content. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - inline QSize sizeHint() const override { - ::cf::ui::base::device::CanvasUnitHelper helper(qApp->devicePixelRatio()); - float height = helper.dpToPx(static_cast(m_tabHeightDp)); - QSize baseSize = QTabBar::sizeHint(); - return QSize(baseSize.width(), static_cast(std::ceil(height))); - } - - signals: - /** - * @brief Signal emitted when a tab close button is clicked. - * - * @param[in] index Index of the tab to close. - * - * @note Connect to this signal to handle tab closure. - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void tabCloseRequested(int index); - - protected: - /** - * @brief Gets the size hint for a specific tab. - * - * @param[in] index Tab index. - * @return Size hint for the tab. - * - * @throws None - * @note Considers minimum width and content size. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize tabSizeHint(int index) const override; - - /** - * @brief Paints the tab bar. - * - * @param[in] event Paint event. - * - * @throws None - * @note Draws tabs and selection indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles mouse enter event. - * - * @param[in] event Enter event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void enterEvent(QEnterEvent* event) override; - - /** - * @brief Handles mouse leave event. - * - * @param[in] event Leave event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void leaveEvent(QEvent* event) override; - - /** - * @brief Handles mouse move event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Tracks hover for close buttons. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseMoveEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Detects clicks on close buttons. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - private: - /** - * @brief Draws a single tab. - * - * @param[in] p QPainter to render with. - * @param[in] index Tab index to draw. - * - * @throws None - * @note Draws tab background, text, and close button if applicable. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void drawTab(QPainter& p, int index); - void drawTabStateLayer(QPainter& p, const QRect& tabRect, int index); - void drawTabContent(QPainter& p, const QRect& tabRect, int index); - void drawCloseIcon(QPainter& p, const QRect& closeRect, int index); - void drawIndicator(QPainter& p); - - void animateIndicatorTo(int index); - void updateIndicatorPosition(); - - QRect closeButtonRect(int index) const; - bool isOverCloseButton(int index, const QPoint& pos) const; - void updateCloseButtonHover(const QPoint& pos); - - // Color access - CFColor backgroundColor() const; - CFColor tabTextColor() const; - CFColor selectedTabTextColor() const; - CFColor indicatorColor() const; - CFColor stateLayerColor() const; - CFColor focusIndicatorColor() const; - QFont tabFont() const; - - TabView* m_tabView; - int m_tabHeightDp; - int m_tabMinWidthDp; - bool m_showIndicator; - - float m_indicatorPosition; - float m_indicatorTargetPosition; - int m_lastIndex; - int m_hoveredIndex; - int m_pressedIndex; - int m_closeButtonHoveredIndex; // Index of tab whose close button is hovered - - QSet m_closeableTabs; // Set of closeable tab indices - - aex::WeakPtr m_indicatorAnimation; - base::MaterialWidgetBase m_material; -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/tabview/tabview.cpp b/ui/widget/material/widget/tabview/tabview.cpp deleted file mode 100644 index a9ff0d203..000000000 --- a/ui/widget/material/widget/tabview/tabview.cpp +++ /dev/null @@ -1,89 +0,0 @@ -/** - * @file tabview.cpp - * @brief Material Design 3 TabView Implementation - * - * @author Material Design Framework Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "tabview.h" -#include "private/materialtabbar.h" - -#include - -namespace cf::ui::widget::material { - -namespace { -constexpr int DEFAULT_TAB_HEIGHT_DP = 48; -constexpr int DEFAULT_TAB_MIN_WIDTH_DP = 120; -} // namespace - -// ============================================================================ -// TabView Implementation -// ============================================================================ - -TabView::TabView(QWidget* parent) : QTabWidget(parent), m_tabBar(nullptr) { - - m_tabBar = new MaterialTabBar(this); - setTabBar(m_tabBar); - - setTabPosition(QTabWidget::North); - setDocumentMode(true); - - // Connect close button signal - connect(m_tabBar, &MaterialTabBar::tabCloseRequested, this, [this](int index) { - removeTab(index); - emit tabCloseRequested(index); - }); -} - -TabView::~TabView() { - // TabBar is parented to this, Qt will delete it -} - -int TabView::tabHeight() const { - return m_tabBar ? m_tabBar->tabHeightDp() : DEFAULT_TAB_HEIGHT_DP; -} - -void TabView::setTabHeight(int height) { - if (m_tabBar) { - m_tabBar->setTabHeightDp(height); - } -} - -int TabView::tabMinWidth() const { - return m_tabBar ? m_tabBar->tabMinWidthDp() : DEFAULT_TAB_MIN_WIDTH_DP; -} - -void TabView::setTabMinWidth(int width) { - if (m_tabBar) { - m_tabBar->setTabMinWidthDp(width); - } -} - -bool TabView::showIndicator() const { - return m_tabBar ? m_tabBar->showIndicator() : true; -} - -void TabView::setShowIndicator(bool show) { - if (m_tabBar) { - m_tabBar->setShowIndicator(show); - } -} - -void TabView::setTabCloseable(int index, bool closeable) { - if (m_tabBar) { - m_tabBar->setTabCloseable(index, closeable); - } -} - -QSize TabView::sizeHint() const { - QSize contentSize = QTabWidget::sizeHint(); - int tabBarHeight = m_tabBar ? m_tabBar->sizeHint().height() : 48; - return QSize(contentSize.width(), contentSize.height() + tabBarHeight); -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/tabview/tabview.h b/ui/widget/material/widget/tabview/tabview.h deleted file mode 100644 index 6edb607e1..000000000 --- a/ui/widget/material/widget/tabview/tabview.h +++ /dev/null @@ -1,192 +0,0 @@ -/** - * @file ui/widget/material/widget/tabview/tabview.h - * @brief Material Design 3 TabView widget. - * - * Implements Material Design 3 tab widget with support for tab labels, - * selection indicator with sliding animation, and tab scrolling. Includes - * state layers, focus indicators, and Material Design 3 styling. - * - * @author N/A - * @date N/A - * @version N/A - * @since N/A - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include -#include - -#include "base/color.h" -#include "export.h" - -namespace cf::ui::widget::material { - -using CFColor = cf::ui::base::CFColor; - -// Forward declaration for internal TabBar implementation -class MaterialTabBar; - -/** - * @brief Material Design 3 TabView widget. - * - * @details Implements Material Design 3 tab widget with support for tab labels, - * selection indicator with sliding animation, and tab scrolling. - * The TabBar implementation is internal and not exposed. - * - * @since N/A - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT TabView : public QTabWidget { - Q_OBJECT - Q_PROPERTY(int tabHeight READ tabHeight WRITE setTabHeight) - Q_PROPERTY(int tabMinWidth READ tabMinWidth WRITE setTabMinWidth) - Q_PROPERTY(bool showIndicator READ showIndicator WRITE setShowIndicator) - - public: - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note Initializes with default Material Design styling. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - explicit TabView(QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note Components are parented to this; Qt deletes them. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - ~TabView() override; - - /** - * @brief Gets the tab height. - * - * @return Tab height in device-independent pixels. - * - * @throws None - * @note Default height follows Material Design 3 specifications. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - int tabHeight() const; - - /** - * @brief Sets the tab height. - * - * @param[in] height Tab height in device-independent pixels. - * - * @throws None - * @note Triggers layout update. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void setTabHeight(int height); - - /** - * @brief Gets the minimum tab width. - * - * @return Minimum tab width in device-independent pixels. - * - * @throws None - * @note Default width follows Material Design 3 specifications. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - int tabMinWidth() const; - - /** - * @brief Sets the minimum tab width. - * - * @param[in] width Minimum tab width in device-independent pixels. - * - * @throws None - * @note Triggers layout update. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void setTabMinWidth(int width); - - /** - * @brief Gets whether the selection indicator is shown. - * - * @return true if indicator is visible, false otherwise. - * - * @throws None - * @note The indicator slides to the selected tab. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - bool showIndicator() const; - - /** - * @brief Sets whether the selection indicator is shown. - * - * @param[in] show true to show indicator, false to hide. - * - * @throws None - * @note Triggers repaint. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void setShowIndicator(bool show); - - /** - * @brief Sets whether a tab can be closed. - * - * @param[in] index Tab index. - * @param[in] closeable true to make tab closeable, false otherwise. - * - * @throws None - * @note Closeable tabs display a close button. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - void setTabCloseable(int index, bool closeable); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the tab view. - * - * @throws None - * @note Based on content and tab dimensions. - * @warning None - * @since N/A - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - signals: - /** - * @brief Signal emitted when a tab close button is clicked. - * - * @param[in] index Index of the tab to close. - * - * @note Connect to this signal to handle tab closure. - * @since N/A - * @ingroup ui_widget_material_widget - */ - void tabCloseRequested(int index); - - private: - MaterialTabBar* m_tabBar; // Internal TabBar implementation -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/textarea/textarea.cpp b/ui/widget/material/widget/textarea/textarea.cpp deleted file mode 100644 index d6ebc703a..000000000 --- a/ui/widget/material/widget/textarea/textarea.cpp +++ /dev/null @@ -1,871 +0,0 @@ -/** - * @file textarea.cpp - * @brief Material Design 3 TextArea Implementation - * - * Implements a Material Design 3 text area with filled and outlined variants. - * Supports floating labels, character counter, helper/error text, and - * multi-line text input with auto-resize support. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "textarea.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "base/geometry_helper.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::base::geometry; -using namespace cf::ui::components; -using namespace cf::ui::components::material; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::material::base; -using namespace cf::ui::widget::application_support; - -// ============================================================================ -// Constants (Material Design 3 specifications) -// ============================================================================ - -namespace { -// Fallback colors when theme is not available -inline CFColor fallbackSurface() { - return CFColor(253, 253, 253); -} // Surface -inline CFColor fallbackOnSurface() { - return CFColor(27, 27, 31); -} // On Surface -inline CFColor fallbackOnSurfaceVariant() { - return CFColor(75, 75, 80); -} // On Surface Variant -inline CFColor fallbackOutline() { - return CFColor(120, 124, 132); -} // Outline -inline CFColor fallbackError() { - return CFColor(186, 26, 26); -} // Error -inline CFColor fallbackPrimary() { - return CFColor(103, 80, 164); -} // Purple 700 -} // namespace - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -TextArea::TextArea(TextAreaVariant variant, QWidget* parent) - : QTextEdit(parent), m_material(this, - MaterialWidgetBase::Config{ - .useRipple = true, - .useElevation = false, - .useFocusIndicator = true, - }), - m_variant(variant), m_showCharacterCounter(false), m_maxLength(0), m_minLines(1), - m_maxLines(0), m_isFloating(false), m_hasError(false), m_floatingProgress(0.0f), - m_updatingGeometry(false) { - - // Disable native frame - setFrameStyle(QFrame::NoFrame); - // Set viewport margins for custom drawing space above text - setViewportMargins(0, 0, 0, 0); - setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - - // Connect text change signal - connect(this, &QTextEdit::textChanged, this, &TextArea::textChanged); - - // Set default font - setFont(inputFont()); - - // Set cursor - setCursor(Qt::IBeamCursor); - - // Disable tab focus for proper multi-line editing - setTabChangesFocus(false); - - // Configure viewport for proper background rendering - // (background appears below text, overlays are drawn in paintEvent) - if (viewport()) { - viewport()->setAutoFillBackground(true); - QPalette pal = viewport()->palette(); - pal.setColor(QPalette::Base, containerColor().native_color()); - viewport()->setPalette(pal); - } -} - -TextArea::TextArea(const QString& text, TextAreaVariant variant, QWidget* parent) - : TextArea(variant, parent) { - setText(text); -} - -TextArea::~TextArea() { - // Components are parented to this, Qt will delete them automatically -} - -// ============================================================================ -// Event Handlers -// ============================================================================ - -void TextArea::mousePressEvent(QMouseEvent* event) { - QTextEdit::mousePressEvent(event); - m_material.onMousePress(event->pos(), rect()); -} - -void TextArea::mouseReleaseEvent(QMouseEvent* event) { - QTextEdit::mouseReleaseEvent(event); - m_material.onMouseRelease(); -} - -void TextArea::focusInEvent(QFocusEvent* event) { - QTextEdit::focusInEvent(event); - m_material.onFocusIn(); - updateFloatingState(true); -} - -void TextArea::focusOutEvent(QFocusEvent* event) { - QTextEdit::focusOutEvent(event); - m_material.onFocusOut(); - updateFloatingState(!toPlainText().isEmpty()); -} - -void TextArea::changeEvent(QEvent* event) { - QTextEdit::changeEvent(event); - if (event->type() == QEvent::EnabledChange) { - m_material.onEnabledChange(isEnabled()); - } -} - -void TextArea::resizeEvent(QResizeEvent* event) { - QTextEdit::resizeEvent(event); - update(); -} - -void TextArea::textChanged() { - // Enforce max length - if (m_maxLength > 0) { - QString text = toPlainText(); - if (text.length() > m_maxLength) { - // Truncate to max length - text = text.left(m_maxLength); - m_updatingGeometry = true; - setPlainText(text); - m_updatingGeometry = false; - - // Move cursor to end - QTextCursor cursor = textCursor(); - cursor.movePosition(QTextCursor::End); - setTextCursor(cursor); - } - } - - // Update floating state based on content - updateFloatingState(hasFocus() || !toPlainText().isEmpty()); - - // Update character counter visibility - if (m_showCharacterCounter) { - update(); - } - - // Auto-resize based on line count - if (!m_updatingGeometry) { - updateGeometryForLines(); - } -} - -void TextArea::keyPressEvent(QKeyEvent* event) { - // Block Enter key if maxLines is set and already at limit - if (m_maxLines > 0 && (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)) { - if (document()->blockCount() >= m_maxLines) { - // Ignore the Enter key - return; - } - } - - QTextEdit::keyPressEvent(event); -} - -void TextArea::paintEvent(QPaintEvent* event) { - // 1) First let base class render text content to viewport - QTextEdit::paintEvent(event); - - // 2) Then draw overlays (label, outline, ripple, focus, helper, counter) on viewport - QWidget* vp = viewport(); - if (!vp) { - return; // extreme edge case guard - } - - QPainter p(vp); - if (!p.isActive()) { - qWarning() << "TextArea::paintEvent: painter not active on viewport()"; - return; - } - p.setRenderHint(QPainter::Antialiasing); - - const QRectF field = fieldRect(); - const QRectF helper = helperTextRect(); - - // Draw order: outline/ripple/focus/helper/counter (overlay) - drawOutline(p, field); - - // Ensure ripple/focus routines check painter active internally - if (p.isActive()) { - drawRipple(p, field); - drawFocusIndicator(p, field); - } - - drawHelperText(p, helper); - if (m_showCharacterCounter) - drawCharacterCounter(p, helper); - - // p destructor will end painting -} - -// ============================================================================ -// Property Getters/Setters -// ============================================================================ - -TextArea::TextAreaVariant TextArea::variant() const { - return m_variant; -} - -void TextArea::setVariant(TextAreaVariant variant) { - if (m_variant != variant) { - m_variant = variant; - updateGeometry(); - update(); - } -} - -QString TextArea::label() const { - return m_label; -} - -void TextArea::setLabel(const QString& label) { - if (m_label != label) { - m_label = label; - updateGeometry(); - update(); - } -} - -QString TextArea::helperText() const { - return m_helperText; -} - -void TextArea::setHelperText(const QString& text) { - if (m_helperText != text) { - m_helperText = text; - updateGeometry(); - update(); - } -} - -QString TextArea::errorText() const { - return m_errorText; -} - -void TextArea::setErrorText(const QString& text) { - if (m_errorText != text) { - m_errorText = text; - m_hasError = !text.isEmpty(); - updateGeometry(); - update(); - } -} - -bool TextArea::isFloating() const { - return m_isFloating; -} - -bool TextArea::showCharacterCounter() const { - return m_showCharacterCounter; -} - -void TextArea::setShowCharacterCounter(bool show) { - if (m_showCharacterCounter != show) { - m_showCharacterCounter = show; - updateGeometry(); - update(); - } -} - -int TextArea::maxLength() const { - return m_maxLength; -} - -void TextArea::setMaxLength(int length) { - if (m_maxLength != length) { - m_maxLength = length; - update(); - } -} - -int TextArea::minLines() const { - return m_minLines; -} - -void TextArea::setMinLines(int lines) { - if (m_minLines != lines && lines > 0) { - m_minLines = lines; - updateGeometry(); - update(); - } -} - -int TextArea::maxLines() const { - return m_maxLines; -} - -void TextArea::setMaxLines(int lines) { - if (m_maxLines != lines && lines >= 0) { - m_maxLines = lines; - updateGeometry(); - update(); - } -} - -// ============================================================================ -// Size Hints -// ============================================================================ - -QSize TextArea::sizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Calculate line height - QFont f = inputFont(); - QFontMetricsF fm(f); - float lineHeight = fm.height() + helper.dpToPx(8.0f); // Line spacing - - // Calculate height based on line count - int lineCount = qMax(m_minLines, document()->blockCount()); - if (m_maxLines > 0) { - lineCount = qMin(lineCount, m_maxLines); - } - - float contentHeight = lineHeight * lineCount; - float labelHeight = m_label.isEmpty() ? 0 : helper.dpToPx(16.0f); - float vPadding = helper.dpToPx(16.0f); // Top and bottom padding - float fieldHeight = contentHeight + vPadding * 2; - - // Add space for floating label (it overlays content) - float totalHeight = fieldHeight; - - // Add helper text height - float helperHeight = m_helperText.isEmpty() && m_errorText.isEmpty() ? 0 : helper.dpToPx(16.0f); - totalHeight += helperHeight; - - // Minimum width for touch target - float minWidth = helper.dpToPx(280.0f); - float contentWidth = helper.dpToPx(280.0f); // Default width - - return QSize(int(std::ceil(contentWidth)), int(std::ceil(totalHeight))); -} - -QSize TextArea::minimumSizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - QFont f = inputFont(); - QFontMetricsF fm(f); - float lineHeight = fm.height() + helper.dpToPx(8.0f); - - float contentHeight = lineHeight * m_minLines; - float vPadding = helper.dpToPx(16.0f); - float fieldHeight = contentHeight + vPadding * 2; - - float helperHeight = m_helperText.isEmpty() && m_errorText.isEmpty() ? 0 : helper.dpToPx(16.0f); - float totalHeight = fieldHeight + helperHeight; - - // Minimum width for usability - float minWidth = helper.dpToPx(200.0f); - - return QSize(int(std::ceil(minWidth)), int(std::ceil(totalHeight))); -} - -// ============================================================================ -// Color Access Methods -// ============================================================================ - -CFColor TextArea::containerColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(SURFACE)); - } catch (...) { - return fallbackSurface(); - } -} - -CFColor TextArea::onContainerColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurfaceVariant(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE_VARIANT)); - } catch (...) { - return fallbackOnSurfaceVariant(); - } -} - -CFColor TextArea::labelColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurfaceVariant(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE_VARIANT)); - } catch (...) { - return fallbackOnSurfaceVariant(); - } -} - -CFColor TextArea::inputTextColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - return fallbackOnSurface(); - } -} - -CFColor TextArea::outlineColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOutline(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(OUTLINE)); - } catch (...) { - return fallbackOutline(); - } -} - -CFColor TextArea::focusOutlineColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackPrimary(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(PRIMARY)); - } catch (...) { - return fallbackPrimary(); - } -} - -CFColor TextArea::errorColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackError(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ERROR)); - } catch (...) { - return fallbackError(); - } -} - -CFColor TextArea::helperTextColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurfaceVariant(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE_VARIANT)); - } catch (...) { - return fallbackOnSurfaceVariant(); - } -} - -float TextArea::cornerRadius() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - // Small corner radius (4dp) - return helper.dpToPx(4.0f); -} - -QFont TextArea::inputFont() const { - auto* app = Application::instance(); - if (!app) { - QFont font = QTextEdit::font(); - font.setPixelSize(16); - return font; - } - - try { - const auto& theme = app->currentTheme(); - auto& fontType = theme.font_type(); - return fontType.queryTargetFont("bodyLarge"); - } catch (...) { - QFont font = QTextEdit::font(); - font.setPixelSize(16); - return font; - } -} - -QFont TextArea::labelFont() const { - auto* app = Application::instance(); - if (!app) { - QFont font = QTextEdit::font(); - font.setPixelSize(16); - return font; - } - - try { - const auto& theme = app->currentTheme(); - auto& fontType = theme.font_type(); - return fontType.queryTargetFont("bodyLarge"); - } catch (...) { - QFont font = QTextEdit::font(); - font.setPixelSize(16); - return font; - } -} - -QFont TextArea::helperFont() const { - auto* app = Application::instance(); - if (!app) { - QFont font = QTextEdit::font(); - font.setPixelSize(12); - return font; - } - - try { - const auto& theme = app->currentTheme(); - auto& fontType = theme.font_type(); - return fontType.queryTargetFont("bodySmall"); - } catch (...) { - QFont font = QTextEdit::font(); - font.setPixelSize(12); - return font; - } -} - -// ============================================================================ -// Layout Helpers -// ============================================================================ - -QRectF TextArea::fieldRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Calculate height based on content - QFont f = inputFont(); - QFontMetricsF fm(f); - float lineHeight = fm.height() + helper.dpToPx(8.0f); - - int lineCount = qMax(m_minLines, document()->blockCount()); - if (m_maxLines > 0) { - lineCount = qMin(lineCount, m_maxLines); - } - - float contentHeight = lineHeight * lineCount; - float vPadding = helper.dpToPx(16.0f); - float fieldHeight = contentHeight + vPadding * 2; - - return QRectF(0, 0, width(), fieldHeight); -} - -QRectF TextArea::textRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - QRectF field = fieldRect(); - float hPadding = helper.dpToPx(16.0f); - - // Account for floating label - float topMargin = helper.dpToPx(16.0f); - if (m_isFloating && !m_label.isEmpty()) { - topMargin += helper.dpToPx(8.0f); - } - - float left = hPadding; - float top = topMargin; - float availableWidth = field.width() - hPadding * 2; - float availableHeight = field.height() - topMargin - helper.dpToPx(16.0f); - - return QRectF(left, top, availableWidth, availableHeight); -} - -QRectF TextArea::helperTextRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - QRectF field = fieldRect(); - float helperHeight = helper.dpToPx(16.0f); - float top = field.bottom(); - - return QRectF(field.left(), top, field.width(), helperHeight); -} - -// ============================================================================ -// Drawing Helpers -// ============================================================================ - -void TextArea::drawBackground(QPainter& p, const QRectF& fieldRect) { - if (m_variant == TextAreaVariant::Outlined) { - return; // Outlined variant has no background - } - - // Filled variant background - CFColor bg = containerColor(); - if (!isEnabled()) { - QColor color = bg.native_color(); - color.setAlphaF(0.38f); - p.fillRect(fieldRect, color); - return; - } - - p.fillRect(fieldRect, bg.native_color()); -} - -void TextArea::drawOutline(QPainter& p, const QRectF& fieldRect) { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float radius = cornerRadius(); - - if (m_variant == TextAreaVariant::Filled) { - // Filled variant: draw bottom border only - float lineWidth = helper.dpToPx(1.0f); - - // Use active width when focused - float activeWidth = helper.dpToPx(2.0f); - float currentWidth = hasFocus() ? activeWidth : lineWidth; - - // Determine color - QColor color; - if (m_hasError) { - color = errorColor().native_color(); - } else if (hasFocus()) { - color = focusOutlineColor().native_color(); - } else { - color = outlineColor().native_color(); - } - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - // Draw bottom line - p.fillRect(QRectF(fieldRect.left(), fieldRect.bottom() - currentWidth, fieldRect.width(), - currentWidth), - color); - } else { - // Outlined variant: draw rounded rectangle - QPainterPath shape = roundedRect(fieldRect, radius); - - // Determine color - QColor color; - if (m_hasError) { - color = errorColor().native_color(); - } else if (hasFocus()) { - color = focusOutlineColor().native_color(); - } else { - color = outlineColor().native_color(); - } - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - // Calculate outline width - float baseWidth = helper.dpToPx(1.0f); - float activeWidth = helper.dpToPx(2.0f); - float currentWidth = hasFocus() ? activeWidth : baseWidth; - - QPen pen(color, currentWidth); - pen.setCosmetic(true); - p.setPen(pen); - p.setBrush(Qt::NoBrush); - p.drawPath(shape); - } -} - -void TextArea::drawHelperText(QPainter& p, const QRectF& helperRect) { - QString text = m_hasError ? m_errorText : m_helperText; - if (text.isEmpty()) { - return; - } - - p.save(); - - QFont f = helperFont(); - p.setFont(f); - - QColor textColor = m_hasError ? errorColor().native_color() : helperTextColor().native_color(); - if (!isEnabled()) { - textColor.setAlphaF(0.38f); - } - p.setPen(textColor); - - p.drawText(helperRect, Qt::AlignLeft | Qt::AlignVCenter, text); - - p.restore(); -} - -void TextArea::drawCharacterCounter(QPainter& p, const QRectF& helperRect) { - if (!m_showCharacterCounter) { - return; - } - - p.save(); - - QFont f = helperFont(); - p.setFont(f); - - QString counterText = QString("%1/%2").arg(toPlainText().length()).arg(m_maxLength); - QFontMetricsF fm(f); - float textWidth = fm.horizontalAdvance(counterText); - - QColor textColor = helperTextColor().native_color(); - if (toPlainText().length() > m_maxLength) { - textColor = errorColor().native_color(); - } - if (!isEnabled()) { - textColor.setAlphaF(0.38f); - } - p.setPen(textColor); - - QRectF counterRect(helperRect.right() - textWidth, helperRect.top(), textWidth, - helperRect.height()); - p.drawText(counterRect, Qt::AlignRight | Qt::AlignVCenter, counterText); - - p.restore(); -} - -void TextArea::drawFocusIndicator(QPainter& p, const QRectF& fieldRect) { - if (!p.isActive()) - return; // guard - if (m_material.focusIndicator() && hasFocus()) { - QPainterPath shape = roundedRect(fieldRect, cornerRadius()); - m_material.focusIndicator()->paint(&p, shape, focusOutlineColor()); - } -} - -void TextArea::drawRipple(QPainter& p, const QRectF& fieldRect) { - if (!p.isActive()) - return; // guard - if (m_material.ripple()) { - QPainterPath shape; - if (m_variant == TextAreaVariant::Outlined) { - shape = roundedRect(fieldRect, cornerRadius()); - } else { - // For filled variant, clip to background - shape.addRect(fieldRect); - } - m_material.ripple()->paint(&p, shape); - } -} - -// ============================================================================ -// Animation Helpers -// ============================================================================ - -void TextArea::updateFloatingState(bool shouldFloat) { - if (m_isFloating != shouldFloat) { - m_isFloating = shouldFloat; - emit floatingChanged(m_isFloating); - } - animateFloatingTo(shouldFloat); -} - -void TextArea::animateFloatingTo(bool floating) { - float target = floating ? 1.0f : 0.0f; - - // Skip animation if already at the target state - if (qFuzzyCompare(m_floatingProgress, target)) { - return; - } - - auto factory = aex::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - if (!factory) { - m_floatingProgress = target; - update(); - return; - } - - // Create property animation for floating progress (same approach as TextField) - auto anim = - factory->createPropertyAnimation(&m_floatingProgress, m_floatingProgress, target, 200, - cf::ui::base::Easing::Type::EmphasizedDecelerate, this); - - if (anim) { - anim->start(); - } else { - m_floatingProgress = target; - update(); - } -} - -void TextArea::updateGeometryForLines() { - // Calculate new height based on content - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - QFont f = inputFont(); - QFontMetricsF fm(f); - float lineHeight = fm.height() + helper.dpToPx(8.0f); - - // Calculate line count - int lineCount = qMax(m_minLines, document()->blockCount()); - if (m_maxLines > 0) { - lineCount = qMin(lineCount, m_maxLines); - } - - float contentHeight = lineHeight * lineCount; - float vPadding = helper.dpToPx(16.0f); - float fieldHeight = contentHeight + vPadding * 2; - - // Add helper text height - float helperHeight = m_helperText.isEmpty() && m_errorText.isEmpty() ? 0 : helper.dpToPx(16.0f); - float totalHeight = fieldHeight + helperHeight; - - // Set the new height - int newHeight = int(std::ceil(totalHeight)); - if (height() != newHeight) { - setFixedHeight(newHeight); - } -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/textarea/textarea.h b/ui/widget/material/widget/textarea/textarea.h deleted file mode 100644 index 2f7f38981..000000000 --- a/ui/widget/material/widget/textarea/textarea.h +++ /dev/null @@ -1,533 +0,0 @@ -/** - * @file ui/widget/material/widget/textarea/textarea.h - * @brief Material Design 3 TextArea widget. - * - * Implements Material Design 3 text area with support for filled and outlined - * variants. Includes floating labels, character counter, helper/error text, - * and multi-line text input. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include -#include -#include -#include -#include - -#include "base/color.h" -#include "export.h" -#include "widget/material/base/material_widget_base.h" - -namespace cf::ui::widget::material { - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 TextArea widget. - * - * @details Implements Material Design 3 text area with support for filled - * and outlined variants. Includes floating labels, character counter, - * helper/error text, and multi-line text input with auto-resize support. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT TextArea : public QTextEdit { - Q_OBJECT - Q_PROPERTY(TextAreaVariant variant READ variant WRITE setVariant) - Q_PROPERTY(QString label READ label WRITE setLabel) - Q_PROPERTY(QString helperText READ helperText WRITE setHelperText) - Q_PROPERTY(QString errorText READ errorText WRITE setErrorText) - Q_PROPERTY(int maxLength READ maxLength WRITE setMaxLength) - Q_PROPERTY(bool showCharacterCounter READ showCharacterCounter WRITE setShowCharacterCounter) - Q_PROPERTY(bool isFloating READ isFloating NOTIFY floatingChanged) - Q_PROPERTY(int minLines READ minLines WRITE setMinLines) - Q_PROPERTY(int maxLines READ maxLines WRITE setMaxLines) - - public: - /** - * @brief TextArea visual variant. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - enum class TextAreaVariant { Filled, Outlined }; - Q_ENUM(TextAreaVariant); - - /** - * @brief Constructor with variant. - * - * @param[in] variant TextArea visual variant. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit TextArea(TextAreaVariant variant = TextAreaVariant::Filled, QWidget* parent = nullptr); - - /** - * @brief Constructor with text and variant. - * - * @param[in] text Initial text content. - * @param[in] variant TextArea visual variant. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit TextArea(const QString& text, TextAreaVariant variant = TextAreaVariant::Filled, - QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~TextArea() override; - - /** - * @brief Gets the text area variant. - * - * @return Current text area variant. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - TextAreaVariant variant() const; - - /** - * @brief Sets the text area variant. - * - * @param[in] variant TextArea variant to use. - * - * @throws None - * @note Changing variant updates the visual appearance. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setVariant(TextAreaVariant variant); - - /** - * @brief Gets the label text. - * - * @return Current label text. - * - * @throws None - * @note The label floats when the field has focus or content. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QString label() const; - - /** - * @brief Sets the label text. - * - * @param[in] label Label text to display. - * - * @throws None - * @note The label floats when the field has focus or content. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setLabel(const QString& label); - - /** - * @brief Gets the helper text. - * - * @return Current helper text. - * - * @throws None - * @note Helper text is displayed below the text area. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QString helperText() const; - - /** - * @brief Sets the helper text. - * - * @param[in] text Helper text to display. - * - * @throws None - * @note Helper text is displayed below the text area. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setHelperText(const QString& text); - - /** - * @brief Gets the error text. - * - * @return Current error text. - * - * @throws None - * @note Error text takes precedence over helper text. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QString errorText() const; - - /** - * @brief Sets the error text. - * - * @param[in] text Error text to display. Empty string clears error state. - * - * @throws None - * @note Error text takes precedence over helper text. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setErrorText(const QString& text); - - /** - * @brief Checks if the label is in floating state. - * - * @return true if label is floating, false otherwise. - * - * @throws None - * @note Label floats when field has focus or content. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool isFloating() const; - - /** - * @brief Gets whether character counter is shown. - * - * @return true if character counter is visible, false otherwise. - * - * @throws None - * @note Character counter is shown in the helper text area. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool showCharacterCounter() const; - - /** - * @brief Sets whether character counter is shown. - * - * @param[in] show true to show character counter, false to hide. - * - * @throws None - * @note Character counter is shown in the helper text area. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setShowCharacterCounter(bool show); - - /** - * @brief Gets the maximum length. - * - * @return Maximum character length. - * - * @throws None - * @note 0 means no maximum. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - int maxLength() const; - - /** - * @brief Sets the maximum length. - * - * @param[in] length Maximum character length. 0 means no maximum. - * - * @throws None - * @note 0 means no maximum. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setMaxLength(int length); - - /** - * @brief Gets the minimum number of visible lines. - * - * @return Minimum number of lines. - * - * @throws None - * @note Default is 1. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - int minLines() const; - - /** - * @brief Sets the minimum number of visible lines. - * - * @param[in] lines Minimum number of visible lines. - * - * @throws None - * @note Affects the minimum height of the text area. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setMinLines(int lines); - - /** - * @brief Gets the maximum number of visible lines. - * - * @return Maximum number of lines, or 0 for unlimited. - * - * @throws None - * @note 0 means no maximum (scrollbar appears). - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - int maxLines() const; - - /** - * @brief Sets the maximum number of visible lines. - * - * @param[in] lines Maximum number of visible lines, or 0 for unlimited. - * - * @throws None - * @note When set, the text area grows up to this many lines. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setMaxLines(int lines); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the text area. - * - * @throws None - * @note Based on label, padding, helper text, and line count. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures touch target size requirements. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - signals: - /** - * @brief Signal emitted when floating state changes. - * - * @param[in] floating true if label is floating, false otherwise. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void floatingChanged(bool floating); - - protected: - /** - * @brief Paints the text area. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements Material Design paint pipeline. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles resize event. - * - * @param[in] event Resize event. - * - * @throws None - * @note Updates internal layout calculations. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void resizeEvent(QResizeEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple effect. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates ripple state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Updates floating label state and shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Updates floating label state and hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Handles key press event. - * - * @param[in] event Key event. - * - * @throws None - * @note Blocks Enter key when maxLines limit is reached. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void keyPressEvent(QKeyEvent* event) override; - - /** - * @brief Handles text change event. - * - * @param[in] text New text content. - * - * @throws None - * @note Updates floating label state and character counter. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void textChanged(); - - private: - // Drawing helpers - Material Design paint pipeline - void drawBackground(QPainter& p, const QRectF& fieldRect); - void drawOutline(QPainter& p, const QRectF& fieldRect); - void drawHelperText(QPainter& p, const QRectF& helperRect); - void drawCharacterCounter(QPainter& p, const QRectF& helperRect); - void drawFocusIndicator(QPainter& p, const QRectF& fieldRect); - void drawRipple(QPainter& p, const QRectF& fieldRect); - - // Layout helpers - QRectF fieldRect() const; - QRectF textRect() const; - QRectF helperTextRect() const; - - // Animation helpers - void updateFloatingState(bool shouldFloat); - void animateFloatingTo(bool floating); - void updateGeometryForLines(); - - // Color access methods - CFColor containerColor() const; - CFColor onContainerColor() const; - CFColor labelColor() const; - CFColor inputTextColor() const; - CFColor outlineColor() const; - CFColor focusOutlineColor() const; - CFColor errorColor() const; - CFColor helperTextColor() const; - float cornerRadius() const; - QFont inputFont() const; - QFont labelFont() const; - QFont helperFont() const; - - // Behavior components - base::MaterialWidgetBase m_material; - - // Properties - TextAreaVariant m_variant; - QString m_label; - QString m_helperText; - QString m_errorText; - bool m_showCharacterCounter; - int m_maxLength; - int m_minLines; - int m_maxLines; - - // Internal state - bool m_isFloating; - bool m_hasError; - float m_floatingProgress; // 0.0 = resting, 1.0 = floating - bool m_updatingGeometry; -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/textfield/textfield.cpp b/ui/widget/material/widget/textfield/textfield.cpp deleted file mode 100644 index 97fb8ef8a..000000000 --- a/ui/widget/material/widget/textfield/textfield.cpp +++ /dev/null @@ -1,1085 +0,0 @@ -/** - * @file textfield.cpp - * @brief Material Design 3 TextField Implementation - * - * Implements a Material Design 3 text field with filled and outlined variants. - * Supports floating labels, prefix/suffix icons, character counter, helper/error - * text, and password mode. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "textfield.h" -#include "aex/weak_ptr/weak_ptr.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "base/geometry_helper.h" -#include "cfmaterial_animation_factory.h" -#include "components/material/cfmaterial_property_animation.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::base::geometry; -using namespace cf::ui::components::material; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::material::base; -using namespace cf::ui::widget::application_support; - -// ============================================================================ -// Constants (Material Design 3 specifications) -// ============================================================================ - -namespace { -// Fallback colors when theme is not available -inline CFColor fallbackSurface() { - return CFColor(253, 253, 253); -} // Surface -inline CFColor fallbackOnSurface() { - return CFColor(27, 27, 31); -} // On Surface -inline CFColor fallbackOnSurfaceVariant() { - return CFColor(75, 75, 80); -} // On Surface Variant -inline CFColor fallbackOutline() { - return CFColor(120, 124, 132); -} // Outline -inline CFColor fallbackError() { - return CFColor(186, 26, 26); -} // Error -inline CFColor fallbackPrimary() { - return CFColor(103, 80, 164); -} // Purple 700 -} // namespace - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -TextField::TextField(TextFieldVariant variant, QWidget* parent) - : QLineEdit(parent), m_material(this, - MaterialWidgetBase::Config{ - .useRipple = true, - .useElevation = false, - .useFocusIndicator = true, - }), - m_variant(variant), m_showCharacterCounter(false), m_maxLength(0), m_isFloating(false), - m_hasError(false), m_floatingProgress(0.0f), m_outlineWidth(1.0f), - m_hoveringClearButton(false), m_pressingClearButton(false) { - - // Disable native frame and background - setFrame(false); - setAttribute(Qt::WA_TranslucentBackground); - setTextMargins(0, 0, 0, 0); - - // Connect text change signal - connect(this, &QLineEdit::textChanged, this, &TextField::textChanged); - - // Set default font - setFont(inputFont()); - - // Set cursor - setCursor(Qt::IBeamCursor); -} - -TextField::TextField(const QString& text, TextFieldVariant variant, QWidget* parent) - : TextField(variant, parent) { - setText(text); -} - -TextField::~TextField() { - // Components are parented to this, Qt will delete them automatically -} - -// ============================================================================ -// Event Handlers -// ============================================================================ - -void TextField::mousePressEvent(QMouseEvent* event) { - // Check if clear button was clicked - QRectF clearRect = clearButtonRect(); - if (!text().isEmpty() && clearRect.contains(event->pos())) { - m_pressingClearButton = true; - update(); - return; - } - - QLineEdit::mousePressEvent(event); - if (m_material.stateMachine()) - m_material.stateMachine()->onPress(event->pos()); - if (m_material.ripple()) - m_material.ripple()->onPress(event->pos(), rect()); - update(); -} - -void TextField::mouseReleaseEvent(QMouseEvent* event) { - // Check if clear button was released - if (m_pressingClearButton) { - m_pressingClearButton = false; - QRectF clearRect = clearButtonRect(); - if (clearRect.contains(event->pos())) { - clear(); - } - update(); - return; - } - - QLineEdit::mouseReleaseEvent(event); - if (m_material.stateMachine()) - m_material.stateMachine()->onRelease(); - if (m_material.ripple()) - m_material.ripple()->onRelease(); - update(); -} - -void TextField::focusInEvent(QFocusEvent* event) { - QLineEdit::focusInEvent(event); - if (m_material.stateMachine()) - m_material.stateMachine()->onFocusIn(); - if (m_material.focusIndicator()) - m_material.focusIndicator()->onFocusIn(); - updateFloatingState(true); - update(); -} - -void TextField::focusOutEvent(QFocusEvent* event) { - QLineEdit::focusOutEvent(event); - if (m_material.stateMachine()) - m_material.stateMachine()->onFocusOut(); - if (m_material.focusIndicator()) - m_material.focusIndicator()->onFocusOut(); - updateFloatingState(!text().isEmpty()); - update(); -} - -void TextField::changeEvent(QEvent* event) { - QLineEdit::changeEvent(event); - if (event->type() == QEvent::EnabledChange) { - m_material.onEnabledChange(isEnabled()); - update(); - } -} - -void TextField::resizeEvent(QResizeEvent* event) { - QLineEdit::resizeEvent(event); - update(); -} - -void TextField::textChanged(const QString& text) { - // Update floating state based on content - updateFloatingState(hasFocus() || !text.isEmpty()); - - // Update character counter visibility - if (m_showCharacterCounter) { - update(); - } -} - -void TextField::paintEvent(QPaintEvent* event) { - Q_UNUSED(event) - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - // Calculate layout rectangles - QRectF field = fieldRect(); - QRectF helper = helperTextRect(); - - // Step 1: Draw background (Filled variant only) - drawBackground(p, field); - - // Step 2: Draw outline (Outlined variant and Filled active state) - drawOutline(p, field); - - // Step 3: Draw label (floating or resting) - drawLabel(p, field); - - // Step 4: Draw prefix icon - drawPrefixIcon(p, textRect()); - - // Step 5: Draw text content - drawText(p, textRect()); - - // Step 6: Draw suffix icon - drawSuffixIcon(p, textRect()); - - // Step 7: Draw clear button - drawClearButton(p, textRect()); - - // Step 8: Draw ripple - drawRipple(p, field); - - // Step 9: Draw focus indicator - drawFocusIndicator(p, field); - - // Step 10: Draw helper/error text - drawHelperText(p, helper); - - // Step 11: Draw character counter - if (m_showCharacterCounter) { - drawCharacterCounter(p, helper); - } -} - -// ============================================================================ -// Property Getters/Setters -// ============================================================================ - -TextField::TextFieldVariant TextField::variant() const { - return m_variant; -} - -void TextField::setVariant(TextFieldVariant variant) { - if (m_variant != variant) { - m_variant = variant; - updateGeometry(); - update(); - } -} - -QString TextField::label() const { - return m_label; -} - -void TextField::setLabel(const QString& label) { - if (m_label != label) { - m_label = label; - updateGeometry(); - update(); - } -} - -QString TextField::helperText() const { - return m_helperText; -} - -void TextField::setHelperText(const QString& text) { - if (m_helperText != text) { - m_helperText = text; - updateGeometry(); - update(); - } -} - -QString TextField::errorText() const { - return m_errorText; -} - -void TextField::setErrorText(const QString& text) { - if (m_errorText != text) { - m_errorText = text; - m_hasError = !text.isEmpty(); - updateGeometry(); - update(); - } -} - -bool TextField::isFloating() const { - return m_isFloating; -} - -QIcon TextField::prefixIcon() const { - return m_prefixIcon; -} - -void TextField::setPrefixIcon(const QIcon& icon) { - if (m_prefixIcon.cacheKey() != icon.cacheKey()) { - m_prefixIcon = icon; - updateGeometry(); - update(); - } -} - -QIcon TextField::suffixIcon() const { - return m_suffixIcon; -} - -void TextField::setSuffixIcon(const QIcon& icon) { - if (m_suffixIcon.cacheKey() != icon.cacheKey()) { - m_suffixIcon = icon; - updateGeometry(); - update(); - } -} - -bool TextField::showCharacterCounter() const { - return m_showCharacterCounter; -} - -void TextField::setShowCharacterCounter(bool show) { - if (m_showCharacterCounter != show) { - m_showCharacterCounter = show; - updateGeometry(); - update(); - } -} - -int TextField::maxLength() const { - return m_maxLength; -} - -void TextField::setMaxLength(int length) { - if (m_maxLength != length) { - m_maxLength = length; - QLineEdit::setMaxLength(length > 0 ? length : 32767); - update(); - } -} - -// ============================================================================ -// Size Hints -// ============================================================================ - -QSize TextField::sizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Material Design text field specifications: - // - Height: 56dp (filled), 56dp (outlined) - // - Label height: 16dp - // - Text height: 24dp - // - Helper text height: 16dp - float fieldHeight = helper.dpToPx(56.0f); - float helperHeight = m_helperText.isEmpty() && m_errorText.isEmpty() ? 0 : helper.dpToPx(16.0f); - float totalHeight = fieldHeight + helperHeight; - - // Minimum width for touch target - float minWidth = helper.dpToPx(280.0f); - float contentWidth = helper.dpToPx(280.0f); // Default width - - return QSize(int(std::ceil(contentWidth)), int(std::ceil(totalHeight))); -} - -QSize TextField::minimumSizeHint() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - float fieldHeight = helper.dpToPx(56.0f); - float helperHeight = m_helperText.isEmpty() && m_errorText.isEmpty() ? 0 : helper.dpToPx(16.0f); - float totalHeight = fieldHeight + helperHeight; - - // Minimum width for usability - float minWidth = helper.dpToPx(200.0f); - - return QSize(int(std::ceil(minWidth)), int(std::ceil(totalHeight))); -} - -// ============================================================================ -// Color Access Methods -// ============================================================================ - -CFColor TextField::containerColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(SURFACE)); - } catch (...) { - return fallbackSurface(); - } -} - -CFColor TextField::onContainerColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurfaceVariant(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE_VARIANT)); - } catch (...) { - return fallbackOnSurfaceVariant(); - } -} - -CFColor TextField::labelColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurfaceVariant(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE_VARIANT)); - } catch (...) { - return fallbackOnSurfaceVariant(); - } -} - -CFColor TextField::inputTextColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - return fallbackOnSurface(); - } -} - -CFColor TextField::outlineColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOutline(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(OUTLINE)); - } catch (...) { - return fallbackOutline(); - } -} - -CFColor TextField::focusOutlineColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackPrimary(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(PRIMARY)); - } catch (...) { - return fallbackPrimary(); - } -} - -CFColor TextField::errorColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackError(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ERROR)); - } catch (...) { - return fallbackError(); - } -} - -CFColor TextField::helperTextColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurfaceVariant(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE_VARIANT)); - } catch (...) { - return fallbackOnSurfaceVariant(); - } -} - -float TextField::cornerRadius() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - // Small corner radius (4dp) - return helper.dpToPx(4.0f); -} - -QFont TextField::inputFont() const { - auto* app = Application::instance(); - if (!app) { - QFont font = QLineEdit::font(); - font.setPixelSize(16); - return font; - } - - try { - const auto& theme = app->currentTheme(); - auto& fontType = theme.font_type(); - return fontType.queryTargetFont("bodyLarge"); - } catch (...) { - QFont font = QLineEdit::font(); - font.setPixelSize(16); - return font; - } -} - -QFont TextField::labelFont() const { - auto* app = Application::instance(); - if (!app) { - QFont font = QLineEdit::font(); - font.setPixelSize(16); - return font; - } - - try { - const auto& theme = app->currentTheme(); - auto& fontType = theme.font_type(); - return fontType.queryTargetFont("bodyLarge"); - } catch (...) { - QFont font = QLineEdit::font(); - font.setPixelSize(16); - return font; - } -} - -QFont TextField::helperFont() const { - auto* app = Application::instance(); - if (!app) { - QFont font = QLineEdit::font(); - font.setPixelSize(12); - return font; - } - - try { - const auto& theme = app->currentTheme(); - auto& fontType = theme.font_type(); - return fontType.queryTargetFont("bodySmall"); - } catch (...) { - QFont font = QLineEdit::font(); - font.setPixelSize(12); - return font; - } -} - -// ============================================================================ -// Layout Helpers -// ============================================================================ - -QRectF TextField::fieldRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float fieldHeight = helper.dpToPx(56.0f); - return QRectF(0, 0, width(), fieldHeight); -} - -QRectF TextField::textRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - QRectF field = fieldRect(); - float hPadding = helper.dpToPx(16.0f); - - // Account for prefix icon - float prefixWidth = 0; - if (!m_prefixIcon.isNull()) { - prefixWidth = helper.dpToPx(24.0f) + helper.dpToPx(12.0f); // icon + gap - } - - // Account for suffix icon - float suffixWidth = 0; - if (!m_suffixIcon.isNull()) { - suffixWidth = helper.dpToPx(24.0f) + helper.dpToPx(12.0f); // icon + gap - } - - // Account for clear button - float clearWidth = 0; - if (!text().isEmpty()) { - clearWidth = helper.dpToPx(24.0f) + helper.dpToPx(8.0f); // button + gap - } - - // For floating label, adjust top margin - float topMargin = m_isFloating ? helper.dpToPx(16.0f) : helper.dpToPx(28.0f); - - float left = hPadding + prefixWidth; - float top = topMargin; - float availableWidth = field.width() - hPadding * 2 - prefixWidth - suffixWidth - clearWidth; - float height = helper.dpToPx(24.0f); // Text line height - - return QRectF(left, top, availableWidth, height); -} - -QRectF TextField::helperTextRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - QRectF field = fieldRect(); - float helperHeight = helper.dpToPx(16.0f); - float top = field.bottom(); - - return QRectF(field.left(), top, field.width(), helperHeight); -} - -QRectF TextField::prefixIconRect() const { - if (m_prefixIcon.isNull()) { - return QRectF(); - } - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - QRectF field = fieldRect(); - float hPadding = helper.dpToPx(16.0f); - float iconSize = helper.dpToPx(24.0f); - float centerY = field.center().y(); - - return QRectF(hPadding, centerY - iconSize / 2, iconSize, iconSize); -} - -QRectF TextField::suffixIconRect() const { - if (m_suffixIcon.isNull()) { - return QRectF(); - } - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - QRectF field = fieldRect(); - float hPadding = helper.dpToPx(16.0f); - float iconSize = helper.dpToPx(24.0f); - float gap = helper.dpToPx(12.0f); - float centerY = field.center().y(); - - // Account for clear button - float clearOffset = 0; - if (!text().isEmpty()) { - clearOffset = helper.dpToPx(24.0f) + helper.dpToPx(8.0f); - } - - float right = field.width() - hPadding - clearOffset; - - return QRectF(right - iconSize, centerY - iconSize / 2, iconSize, iconSize); -} - -QRectF TextField::clearButtonRect() const { - if (text().isEmpty()) { - return QRectF(); - } - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - QRectF field = fieldRect(); - float hPadding = helper.dpToPx(16.0f); - float iconSize = helper.dpToPx(24.0f); - float centerY = field.center().y(); - - float right = field.width() - hPadding; - - return QRectF(right - iconSize, centerY - iconSize / 2, iconSize, iconSize); -} - -// ============================================================================ -// Drawing Helpers -// ============================================================================ - -void TextField::drawBackground(QPainter& p, const QRectF& fieldRect) { - if (m_variant == TextFieldVariant::Outlined) { - return; // Outlined variant has no background - } - - // Filled variant background - CFColor bg = containerColor(); - if (!isEnabled()) { - QColor color = bg.native_color(); - color.setAlphaF(0.38f); - p.fillRect(fieldRect, color); - return; - } - - p.fillRect(fieldRect, bg.native_color()); -} - -void TextField::drawOutline(QPainter& p, const QRectF& fieldRect) { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float radius = cornerRadius(); - - if (m_variant == TextFieldVariant::Filled) { - // Filled variant: draw bottom line only - float lineWidth = helper.dpToPx(1.0f); - - // Use active width when focused or floating - float activeWidth = helper.dpToPx(2.0f); - float currentWidth = lineWidth; - if (hasFocus() || m_isFloating) { - currentWidth = lineWidth + (activeWidth - lineWidth) * m_floatingProgress; - } - - // Determine color - QColor color; - if (m_hasError) { - color = errorColor().native_color(); - } else if (hasFocus()) { - color = focusOutlineColor().native_color(); - } else { - color = outlineColor().native_color(); - } - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - // Draw bottom line - p.fillRect(QRectF(fieldRect.left(), fieldRect.bottom() - currentWidth, fieldRect.width(), - currentWidth), - color); - } else { - // Outlined variant: draw rounded rectangle with gap for label - QPainterPath shape = roundedRect(fieldRect, radius); - - // Determine color - QColor color; - if (m_hasError) { - color = errorColor().native_color(); - } else if (hasFocus()) { - color = focusOutlineColor().native_color(); - } else { - color = outlineColor().native_color(); - } - - if (!isEnabled()) { - color.setAlphaF(0.38f); - } - - // Calculate outline width - float baseWidth = helper.dpToPx(1.0f); - float activeWidth = helper.dpToPx(2.0f); - float currentWidth = hasFocus() ? activeWidth : baseWidth; - - QPen pen(color, currentWidth); - pen.setCosmetic(true); - p.setPen(pen); - p.setBrush(Qt::NoBrush); - p.drawPath(shape); - } -} - -void TextField::drawLabel(QPainter& p, const QRectF& fieldRect) { - if (m_label.isEmpty()) { - return; - } - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - QFont labelF = labelFont(); - QFontMetricsF fm(labelF); - - // Calculate label position based on floating state - float floatingScale = 0.75f; // 75% of normal size - float floatingY = helper.dpToPx(12.0f); // Top position when floating - float restingY = fieldRect.center().y(); // Center when resting - - // Interpolate between resting and floating positions - float currentY = restingY + (floatingY - restingY) * m_floatingProgress; - float currentScale = 1.0f - (1.0f - floatingScale) * m_floatingProgress; - - // Calculate horizontal position - float leftMargin = helper.dpToPx(16.0f); - - // For outlined variant with focus, add horizontal padding - if (m_variant == TextFieldVariant::Outlined && hasFocus()) { - leftMargin += helper.dpToPx(4.0f); - } - - // Account for prefix icon - if (!m_prefixIcon.isNull()) { - leftMargin += helper.dpToPx(24.0f) + helper.dpToPx(12.0f); - } - - // For filled variant, create background gap when floating - if (m_variant == TextFieldVariant::Filled && m_floatingProgress > 0.5f) { - float bgWidth = fm.horizontalAdvance(m_label) * currentScale + helper.dpToPx(8.0f); - QRectF bgRect(leftMargin - helper.dpToPx(4.0f), - floatingY - fm.height() * currentScale / 2 - helper.dpToPx(2.0f), bgWidth, - fm.height() * currentScale + helper.dpToPx(4.0f)); - p.fillRect(bgRect, containerColor().native_color()); - } - - p.save(); - - // Set font and color - // Get the actual font height since pixelSize() may return 0 if pointSize is used - float baseFontSize = labelF.pixelSize(); - if (baseFontSize <= 0) { - baseFontSize = fm.height(); - } - int scaledSize = static_cast(baseFontSize * currentScale); - if (scaledSize < 1) { - scaledSize = 1; // Ensure minimum valid size - } - labelF.setPixelSize(scaledSize); - p.setFont(labelF); - - QColor labelColor; - if (m_hasError) { - labelColor = errorColor().native_color(); - } else if (hasFocus() || m_isFloating) { - labelColor = focusOutlineColor().native_color(); - } else { - labelColor = this->labelColor().native_color(); - } - - if (!isEnabled()) { - labelColor.setAlphaF(0.38f); - } - - p.setPen(labelColor); - - // Draw label - QRectF textRect(leftMargin, currentY - fm.height() * currentScale / 2, - fieldRect.width() - leftMargin * 2, fm.height() * currentScale); - p.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, m_label); - - p.restore(); -} - -void TextField::drawText(QPainter& p, const QRectF& textRect) { - // Use QLineEdit's default text rendering but clip to our text rect - p.save(); - p.setClipRect(textRect); - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - QFont f = inputFont(); - p.setFont(f); - - QColor textColor = inputTextColor().native_color(); - if (!isEnabled()) { - textColor.setAlphaF(0.38f); - } - p.setPen(textColor); - - // Get display text - QString displayText = text(); - if (displayText.isEmpty() && placeholderText().isEmpty() == false) { - // Show placeholder - QColor placeholderColor = onContainerColor().native_color(); - placeholderColor.setAlphaF(0.6f); - p.setPen(placeholderColor); - displayText = placeholderText(); - } - - // Draw text - QRectF adjustedRect = textRect; - adjustedRect.setHeight(QFontMetricsF(f).height()); - - // Handle password mode - if (echoMode() == Password) { - displayText = QString(displayText.length(), QChar(0x2022)); // Bullet character - } - - p.drawText(adjustedRect, Qt::AlignLeft | Qt::AlignTop, displayText); - - // Draw cursor - if (hasFocus() && cursorPosition() >= 0) { - QFontMetricsF fm(f); - int cursorPos = qBound(0, cursorPosition(), displayText.length()); - float cursorX = textRect.left() + fm.horizontalAdvance(displayText.left(cursorPos)); - - float cursorWidth = helper.dpToPx(1.0f); - QRectF cursorRect(cursorX, adjustedRect.top(), cursorWidth, adjustedRect.height()); - - p.fillRect(cursorRect, focusOutlineColor().native_color()); - } - - p.restore(); -} - -void TextField::drawPrefixIcon(QPainter& p, const QRectF& textRect) { - if (m_prefixIcon.isNull()) { - return; - } - - QRectF iconRect = prefixIconRect(); - if (iconRect.isEmpty()) { - return; - } - - QColor iconColor = onContainerColor().native_color(); - if (!isEnabled()) { - iconColor.setAlphaF(0.38f); - } - - QIcon::Mode mode = isEnabled() ? QIcon::Normal : QIcon::Disabled; - m_prefixIcon.paint(&p, iconRect.toRect(), Qt::AlignCenter, mode); -} - -void TextField::drawSuffixIcon(QPainter& p, const QRectF& textRect) { - if (m_suffixIcon.isNull()) { - return; - } - - QRectF iconRect = suffixIconRect(); - if (iconRect.isEmpty()) { - return; - } - - QIcon::Mode mode = isEnabled() ? QIcon::Normal : QIcon::Disabled; - m_suffixIcon.paint(&p, iconRect.toRect(), Qt::AlignCenter, mode); -} - -void TextField::drawClearButton(QPainter& p, const QRectF& textRect) { - Q_UNUSED(textRect) - if (text().isEmpty()) { - return; - } - - QRectF iconRect = clearButtonRect(); - if (iconRect.isEmpty()) { - return; - } - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - p.save(); - - // Draw clear "X" icon - QColor iconColor = onContainerColor().native_color(); - if (m_hoveringClearButton || m_pressingClearButton) { - iconColor = iconColor.lighter(120); - } - if (!isEnabled()) { - iconColor.setAlphaF(0.38f); - } - - p.setPen(QPen(iconColor, helper.dpToPx(2.0f))); - p.setRenderHint(QPainter::Antialiasing); - - float cx = iconRect.center().x(); - float cy = iconRect.center().y(); - float size = helper.dpToPx(18.0f); - float half = size / 2; - - p.drawLine(QPointF(cx - half, cy - half), QPointF(cx + half, cy + half)); - p.drawLine(QPointF(cx + half, cy - half), QPointF(cx - half, cy + half)); - - p.restore(); -} - -void TextField::drawHelperText(QPainter& p, const QRectF& helperRect) { - QString text = m_hasError ? m_errorText : m_helperText; - if (text.isEmpty()) { - return; - } - - p.save(); - - QFont f = helperFont(); - p.setFont(f); - - QColor textColor = m_hasError ? errorColor().native_color() : helperTextColor().native_color(); - if (!isEnabled()) { - textColor.setAlphaF(0.38f); - } - p.setPen(textColor); - - p.drawText(helperRect, Qt::AlignLeft | Qt::AlignVCenter, text); - - p.restore(); -} - -void TextField::drawCharacterCounter(QPainter& p, const QRectF& helperRect) { - if (!m_showCharacterCounter) { - return; - } - - p.save(); - - QFont f = helperFont(); - p.setFont(f); - - QString counterText = QString("%1/%2").arg(text().length()).arg(m_maxLength); - QFontMetricsF fm(f); - float textWidth = fm.horizontalAdvance(counterText); - - QColor textColor = helperTextColor().native_color(); - if (text().length() > m_maxLength) { - textColor = errorColor().native_color(); - } - if (!isEnabled()) { - textColor.setAlphaF(0.38f); - } - p.setPen(textColor); - - QRectF counterRect(helperRect.right() - textWidth, helperRect.top(), textWidth, - helperRect.height()); - p.drawText(counterRect, Qt::AlignRight | Qt::AlignVCenter, counterText); - - p.restore(); -} - -void TextField::drawFocusIndicator(QPainter& p, const QRectF& fieldRect) { - if (m_material.focusIndicator() && hasFocus()) { - QPainterPath shape = roundedRect(fieldRect, cornerRadius()); - m_material.focusIndicator()->paint(&p, shape, focusOutlineColor()); - } -} - -void TextField::drawRipple(QPainter& p, const QRectF& fieldRect) { - if (m_material.ripple()) { - QPainterPath shape; - if (m_variant == TextFieldVariant::Outlined) { - shape = roundedRect(fieldRect, cornerRadius()); - } else { - // For filled variant, clip to background - shape.addRect(fieldRect); - } - m_material.ripple()->paint(&p, shape); - } -} - -// ============================================================================ -// Animation Helpers -// ============================================================================ - -void TextField::updateFloatingState(bool shouldFloat) { - if (m_isFloating != shouldFloat) { - m_isFloating = shouldFloat; - emit floatingChanged(m_isFloating); - } - animateFloatingTo(shouldFloat); -} - -void TextField::animateFloatingTo(bool floating) { - float target = floating ? 1.0f : 0.0f; - - // Skip animation if already at the target state - if (qFuzzyCompare(m_floatingProgress, target)) { - return; - } - - // Get animation factory locally for custom property animations - auto factory = aex::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - if (!factory) { - m_floatingProgress = target; - update(); - return; - } - - // Create property animation for floating progress - auto anim = - factory->createPropertyAnimation(&m_floatingProgress, m_floatingProgress, target, 200, - cf::ui::base::Easing::Type::EmphasizedDecelerate, this); - - if (anim) { - // IMPORTANT: Update range to fix cached animation's stale from/to values - if (auto* propAnim = - dynamic_cast( - anim.Get())) { - propAnim->setRange(m_floatingProgress, target); - } - anim->start(); - } else { - m_floatingProgress = target; - update(); - } -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/textfield/textfield.h b/ui/widget/material/widget/textfield/textfield.h deleted file mode 100644 index 92e08f538..000000000 --- a/ui/widget/material/widget/textfield/textfield.h +++ /dev/null @@ -1,528 +0,0 @@ -/** - * @file ui/widget/material/widget/textfield/textfield.h - * @brief Material Design 3 TextField widget. - * - * Implements Material Design 3 text field with support for filled and outlined - * variants. Includes floating labels, prefix/suffix icons, character counter, - * helper/error text, and password mode. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include -#include -#include - -#include "base/color.h" -#include "export.h" -#include "widget/material/base/material_widget_base.h" - -namespace cf::ui::widget::material { - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 TextField widget. - * - * @details Implements Material Design 3 text field with support for filled - * and outlined variants. Includes floating labels, prefix/suffix icons, - * character counter, helper/error text, and password mode. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT TextField : public QLineEdit { - Q_OBJECT - Q_PROPERTY(TextFieldVariant variant READ variant WRITE setVariant) - Q_PROPERTY(QString label READ label WRITE setLabel) - Q_PROPERTY(QString helperText READ helperText WRITE setHelperText) - Q_PROPERTY(QString errorText READ errorText WRITE setErrorText) - Q_PROPERTY(int maxLength READ maxLength WRITE setMaxLength) - Q_PROPERTY(bool showCharacterCounter READ showCharacterCounter WRITE setShowCharacterCounter) - Q_PROPERTY(bool isFloating READ isFloating NOTIFY floatingChanged) - Q_PROPERTY(QIcon prefixIcon READ prefixIcon WRITE setPrefixIcon) - Q_PROPERTY(QIcon suffixIcon READ suffixIcon WRITE setSuffixIcon) - - public: - /** - * @brief TextField visual variant. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - enum class TextFieldVariant { Filled, Outlined }; - Q_ENUM(TextFieldVariant); - - /** - * @brief Constructor with variant. - * - * @param[in] variant TextField visual variant. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit TextField(TextFieldVariant variant = TextFieldVariant::Filled, - QWidget* parent = nullptr); - - /** - * @brief Constructor with text and variant. - * - * @param[in] text Initial text content. - * @param[in] variant TextField visual variant. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit TextField(const QString& text, TextFieldVariant variant = TextFieldVariant::Filled, - QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~TextField() override; - - /** - * @brief Gets the text field variant. - * - * @return Current text field variant. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - TextFieldVariant variant() const; - - /** - * @brief Sets the text field variant. - * - * @param[in] variant TextField variant to use. - * - * @throws None - * @note Changing variant updates the visual appearance. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setVariant(TextFieldVariant variant); - - /** - * @brief Gets the label text. - * - * @return Current label text. - * - * @throws None - * @note The label floats when the field has focus or content. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QString label() const; - - /** - * @brief Sets the label text. - * - * @param[in] label Label text to display. - * - * @throws None - * @note The label floats when the field has focus or content. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setLabel(const QString& label); - - /** - * @brief Gets the helper text. - * - * @return Current helper text. - * - * @throws None - * @note Helper text is displayed below the text field. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QString helperText() const; - - /** - * @brief Sets the helper text. - * - * @param[in] text Helper text to display. - * - * @throws None - * @note Helper text is displayed below the text field. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setHelperText(const QString& text); - - /** - * @brief Gets the error text. - * - * @return Current error text. - * - * @throws None - * @note Error text takes precedence over helper text. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QString errorText() const; - - /** - * @brief Sets the error text. - * - * @param[in] text Error text to display. Empty string clears error state. - * - * @throws None - * @note Error text takes precedence over helper text. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setErrorText(const QString& text); - - /** - * @brief Checks if the label is in floating state. - * - * @return true if label is floating, false otherwise. - * - * @throws None - * @note Label floats when field has focus or content. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool isFloating() const; - - /** - * @brief Gets the prefix icon. - * - * @return Current prefix icon. - * - * @throws None - * @note Prefix icon is displayed at the start of the field. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QIcon prefixIcon() const; - - /** - * @brief Sets the prefix icon. - * - * @param[in] icon Icon to display at the start of the field. - * - * @throws None - * @note Prefix icon is displayed at the start of the field. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setPrefixIcon(const QIcon& icon); - - /** - * @brief Gets the suffix icon. - * - * @return Current suffix icon. - * - * @throws None - * @note Suffix icon is displayed at the end of the field. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QIcon suffixIcon() const; - - /** - * @brief Sets the suffix icon. - * - * @param[in] icon Icon to display at the end of the field. - * - * @throws None - * @note Suffix icon is displayed at the end of the field. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setSuffixIcon(const QIcon& icon); - - /** - * @brief Gets whether character counter is shown. - * - * @return true if character counter is visible, false otherwise. - * - * @throws None - * @note Character counter is shown in the helper text area. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool showCharacterCounter() const; - - /** - * @brief Sets whether character counter is shown. - * - * @param[in] show true to show character counter, false to hide. - * - * @throws None - * @note Character counter is shown in the helper text area. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setShowCharacterCounter(bool show); - - /** - * @brief Gets the maximum length. - * - * @return Maximum character length. - * - * @throws None - * @note 0 means no maximum. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - int maxLength() const; - - /** - * @brief Sets the maximum length. - * - * @param[in] length Maximum character length. 0 means no maximum. - * - * @throws None - * @note 0 means no maximum. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setMaxLength(int length); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the text field. - * - * @throws None - * @note Based on label, icons, padding, and helper text. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures touch target size requirements. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - signals: - /** - * @brief Signal emitted when floating state changes. - * - * @param[in] floating true if label is floating, false otherwise. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void floatingChanged(bool floating); - - protected: - /** - * @brief Paints the text field. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements Material Design paint pipeline. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles resize event. - * - * @param[in] event Resize event. - * - * @throws None - * @note Updates internal layout calculations. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void resizeEvent(QResizeEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple effect. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates ripple state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Updates floating label state and shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Updates floating label state and hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Handles text change event. - * - * @param[in] text New text content. - * - * @throws None - * @note Updates floating label state and character counter. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void textChanged(const QString& text); - - private: - // Drawing helpers - Material Design paint pipeline - void drawBackground(QPainter& p, const QRectF& fieldRect); - void drawOutline(QPainter& p, const QRectF& fieldRect); - void drawLabel(QPainter& p, const QRectF& fieldRect); - void drawText(QPainter& p, const QRectF& textRect); - void drawPrefixIcon(QPainter& p, const QRectF& textRect); - void drawSuffixIcon(QPainter& p, const QRectF& textRect); - void drawClearButton(QPainter& p, const QRectF& textRect); - void drawHelperText(QPainter& p, const QRectF& helperRect); - void drawCharacterCounter(QPainter& p, const QRectF& helperRect); - void drawFocusIndicator(QPainter& p, const QRectF& fieldRect); - void drawRipple(QPainter& p, const QRectF& fieldRect); - - // Layout helpers - QRectF fieldRect() const; - QRectF textRect() const; - QRectF helperTextRect() const; - QRectF prefixIconRect() const; - QRectF suffixIconRect() const; - QRectF clearButtonRect() const; - - // Animation helpers - void updateFloatingState(bool shouldFloat); - void animateFloatingTo(bool floating); - - // Color access methods - CFColor containerColor() const; - CFColor onContainerColor() const; - CFColor labelColor() const; - CFColor inputTextColor() const; - CFColor outlineColor() const; - CFColor focusOutlineColor() const; - CFColor errorColor() const; - CFColor helperTextColor() const; - float cornerRadius() const; - QFont inputFont() const; - QFont labelFont() const; - QFont helperFont() const; - - // Behavior components - base::MaterialWidgetBase m_material; - - // Properties - TextFieldVariant m_variant; - QString m_label; - QString m_helperText; - QString m_errorText; - QIcon m_prefixIcon; - QIcon m_suffixIcon; - bool m_showCharacterCounter; - int m_maxLength; - - // Internal state - bool m_isFloating; - bool m_hasError; - float m_floatingProgress; // 0.0 = resting, 1.0 = floating - float m_outlineWidth; // For animating outline width - bool m_hoveringClearButton; - bool m_pressingClearButton; -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/treeview/private/treeviewdelegate.cpp b/ui/widget/material/widget/treeview/private/treeviewdelegate.cpp deleted file mode 100644 index 4097801b4..000000000 --- a/ui/widget/material/widget/treeview/private/treeviewdelegate.cpp +++ /dev/null @@ -1,727 +0,0 @@ -/** - * @file treeviewdelegate.cpp - * @brief Material Design 3 TreeView Delegate Implementation - * - * Implements custom rendering and size hints for TreeView items. - * - * @author Material Design Framework Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "treeviewdelegate.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "base/geometry_helper.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" - -#include -#include -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::base::geometry; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::application_support; - -// ============================================================================ -// Fallback Colors -// ============================================================================ - -namespace { -inline CFColor fallbackSurface() { - return CFColor(253, 253, 253); -} -inline CFColor fallbackOnSurface() { - return CFColor(27, 27, 31); -} -inline CFColor fallbackOnSurfaceVariant() { - return CFColor(115, 119, 128); -} -inline CFColor fallbackPrimaryContainer() { - return CFColor(234, 221, 255); -} -inline CFColor fallbackOutlineVariant() { - return CFColor(120, 124, 132); -} -} // namespace - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -TreeViewItemDelegate::TreeViewItemDelegate(QObject* parent) - : QStyledItemDelegate(parent), m_itemHeight(TreeItemHeight::Standard), - m_indentStyle(TreeIndentStyle::Material), m_showTreeLines(true) { - - // Calculate initial sizes - CanvasUnitHelper helper(qApp->devicePixelRatio()); - m_itemHeightPx = helper.dpToPx(56.0f); // Standard height - m_indentPx = helper.dpToPx(28.0f); // Material indent (28dp per level) -} - -TreeViewItemDelegate::~TreeViewItemDelegate() { - // Qt handles cleanup -} - -// ============================================================================ -// QStyledItemDelegate Interface -// ============================================================================ - -void TreeViewItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, - const QModelIndex& index) const { - if (!index.isValid() || !painter) { - return; - } - - painter->setRenderHint(QPainter::Antialiasing); - - // Get the view for tree-specific operations - const QTreeView* view = qobject_cast(option.widget); - if (!view) { - // Fallback to default painting if not a TreeView - QStyledItemDelegate::paint(painter, option, index); - return; - } - - QRectF itemRect = option.rect; - - // Check if this index is hovered by accessing the view's hovered index property - // The view stores the hovered index as a dynamic property - QStyleOptionViewItem opt = option; - QVariant hoveredVariant = view->property("_q_hoveredIndex"); - if (hoveredVariant.isValid()) { - QPersistentModelIndex hoveredIndex = hoveredVariant.value(); - if (hoveredIndex.isValid() && hoveredIndex == index) { - opt.state |= QStyle::State_MouseOver; - } else { - opt.state &= ~QStyle::State_MouseOver; - } - } else { - // Fallback: check cursor position - QPoint cursorPos = view->viewport()->mapFromGlobal(QCursor::pos()); - QModelIndex indexAtCursor = view->indexAt(cursorPos); - if (indexAtCursor.isValid() && indexAtCursor == index) { - opt.state |= QStyle::State_MouseOver; - } else { - opt.state &= ~QStyle::State_MouseOver; - } - } - - // Apply custom background from model if present - QVariant bgVar = index.data(Qt::BackgroundRole); - if (bgVar.isValid()) { - QBrush bgBrush = bgVar.value(); - painter->fillRect(itemRect, bgBrush); - } - - // 1. Draw background (selection, hover, state layer) - drawBackground(painter, opt, index); - - // 2. Draw tree lines if enabled - if (m_showTreeLines && m_indentStyle == TreeIndentStyle::Material) { - drawTreeLines(painter, index, itemRect, view); - } - - // 3. Calculate content area with indentation - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float hPadding = horizontalPadding(); - int depth = getDepth(index, view); - float indent = m_indentPx; - float iconSz = iconSize(); - float checkGap = helper.dpToPx(8.0f); - float checkSize = helper.dpToPx(18.0f); - - // Account for expand/collapse icon space - float contentLeft = hPadding + (indent * depth); - if (index.model()->hasChildren(index)) { - contentLeft += iconSz; - } - - // Account for checkbox space if present - QVariant checkVar = index.data(Qt::CheckStateRole); - bool hasCheckbox = checkVar.isValid(); - if (hasCheckbox) { - contentLeft += checkSize + checkGap; - } - - QRectF contentRect = itemRect.adjusted(contentLeft, 0, 0, 0); - - // 4. Draw expand/collapse icon - if (index.model()->hasChildren(index)) { - QRectF expandRect(hPadding + (indent * depth), itemRect.center().y() - iconSz / 2.0f, - iconSz, iconSz); - - bool expanded = view->isExpanded(index); - CFColor iconColor = onSurfaceVariantColor(); - drawExpandCollapseIcon(painter, expandRect, expanded, iconColor); - } - - // 5. Draw checkbox if present - if (hasCheckbox) { - float checkX = hPadding + (indent * depth); - if (index.model()->hasChildren(index)) { - checkX += iconSz; - } - QRectF checkRect(checkX, itemRect.center().y() - checkSize / 2.0f, checkSize, checkSize); - Qt::CheckState checkState = static_cast(checkVar.toInt()); - drawCheckBox(painter, checkRect, checkState, opt); - } - - // 6. Draw item content (text and decoration) - drawItemContent(painter, contentRect, opt, index); -} - -QSize TreeViewItemDelegate::sizeHint(const QStyleOptionViewItem& option, - const QModelIndex& index) const { - if (!index.isValid()) { - return QSize(-1, static_cast(std::ceil(m_itemHeightPx))); - } - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float hPadding = horizontalPadding(); - float iconSz = iconSize(); - float iconGap = helper.dpToPx(12.0f); - float checkGap = helper.dpToPx(8.0f); - float checkSize = helper.dpToPx(18.0f); - - // Get tree view for depth calculation - const QTreeView* view = qobject_cast(option.widget); - int depth = 0; - if (view) { - depth = getDepth(index, view); - } - - // Calculate base width from indentation - float width = hPadding + (m_indentPx * depth); - - // Add expand/collapse icon space if item has children - if (index.model()->hasChildren(index)) { - width += iconSz; - } - - // Add checkbox space if present - QVariant checkVar = index.data(Qt::CheckStateRole); - if (checkVar.isValid()) { - width += checkSize + checkGap; - } - - // Add decoration icon space if present - QVariant decoration = index.data(Qt::DecorationRole); - if (decoration.isValid()) { - QIcon icon = decoration.value(); - if (!icon.isNull()) { - width += iconSz + iconGap; - } - } - - // Get font (use custom font if provided) - QFont font = option.font; - QVariant fontVar = index.data(Qt::FontRole); - if (fontVar.isValid()) { - font = fontVar.value(); - } - if (font.pixelSize() <= 0) { - font.setPixelSize(static_cast(helper.dpToPx(14.0f))); - } - - // Calculate text width - use full text, not elided - QString text = index.data(Qt::DisplayRole).toString(); - QFontMetrics fm(font); - width += fm.horizontalAdvance(text); - - // Add right padding - width += hPadding; - - return QSize(static_cast(std::ceil(width)), static_cast(std::ceil(m_itemHeightPx))); -} - -// ============================================================================ -// Property Setters -// ============================================================================ - -void TreeViewItemDelegate::setItemHeight(TreeItemHeight height) { - if (m_itemHeight != height) { - m_itemHeight = height; - CanvasUnitHelper helper(qApp->devicePixelRatio()); - if (height == TreeItemHeight::Compact) { - m_itemHeightPx = helper.dpToPx(48.0f); - } else { - m_itemHeightPx = helper.dpToPx(56.0f); - } - } -} - -void TreeViewItemDelegate::setIndentStyle(TreeIndentStyle style) { - if (m_indentStyle != style) { - m_indentStyle = style; - CanvasUnitHelper helper(qApp->devicePixelRatio()); - if (style == TreeIndentStyle::Material) { - m_indentPx = helper.dpToPx(28.0f); - } else { - m_indentPx = helper.dpToPx(20.0f); - } - } -} - -void TreeViewItemDelegate::setShowTreeLines(bool show) { - m_showTreeLines = show; -} - -float TreeViewItemDelegate::itemHeightValue() const { - return m_itemHeightPx; -} - -float TreeViewItemDelegate::indentPerLevel() const { - return m_indentPx; -} - -// ============================================================================ -// Drawing Helpers -// ============================================================================ - -void TreeViewItemDelegate::drawBackground(QPainter* painter, const QStyleOptionViewItem& option, - const QModelIndex& index) const { - QRectF itemRect = option.rect; - bool selected = option.state & QStyle::State_Selected; - bool hovered = (option.state & QStyle::State_MouseOver); - bool enabled = option.state & QStyle::State_Enabled; - - if (selected) { - // Draw selected background with rounded corners - CFColor containerColor = primaryContainerColor(); - QColor bgColor = containerColor.native_color(); - bgColor.setAlphaF(0.12f); // 12% opacity - - QPainterPath path = roundedRect(itemRect, 8.0f); // ShapeSmall - painter->fillPath(path, bgColor); - } else if (hovered && enabled) { - // Draw hover state - CFColor onSurface = onSurfaceColor(); - QColor hoverColor = onSurface.native_color(); - hoverColor.setAlphaF(0.08f); // 8% opacity - - QPainterPath path = roundedRect(itemRect, 8.0f); - painter->fillPath(path, hoverColor); - } -} - -void TreeViewItemDelegate::drawTreeLines(QPainter* painter, const QModelIndex& index, - const QRectF& itemRect, const QTreeView* view) const { - if (!m_showTreeLines || m_indentStyle != TreeIndentStyle::Material) { - return; - } - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float lineThickness = helper.dpToPx(1.0f); - float indent = m_indentPx; - float hPadding = horizontalPadding(); - float iconSz = iconSize(); - - CFColor lineColor = outlineVariantColor(); - QColor lineQtColor = lineColor.native_color(); - lineQtColor.setAlphaF(0.12f); // 12% opacity for tree lines - - painter->save(); - painter->setPen(QPen(lineQtColor, lineThickness)); - painter->setRenderHint(QPainter::Antialiasing, false); - - int depth = getDepth(index, view); - - // Draw vertical lines for each ancestor level - // Only draw lines that should continue (not for last siblings) - for (int level = 0; level < depth; level++) { - if (!shouldContinueLineAtLevel(index, level, view)) { - continue; - } - - float x = hPadding + (indent * level) + iconSz / 2.0f; - float y1 = itemRect.top(); - float y2 = itemRect.bottom(); - painter->drawLine(QPointF(x, y1), QPointF(x, y2)); - } - - // Draw horizontal connector line to current item - if (depth > 0) { - float x1 = hPadding + (indent * (depth - 1)) + iconSz / 2.0f; - float x2 = hPadding + (indent * depth) + iconSz / 2.0f; - float y = itemRect.center().y(); - painter->drawLine(QPointF(x1, y), QPointF(x2, y)); - } - - painter->restore(); -} - -void TreeViewItemDelegate::drawExpandCollapseIcon(QPainter* painter, const QRectF& iconRect, - bool expanded, const CFColor& color) const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float strokeThickness = helper.dpToPx(2.0f); - - painter->setPen(QPen(color.native_color(), strokeThickness)); - painter->setRenderHint(QPainter::Antialiasing); - - QPointF center = iconRect.center(); - float iconSz = iconSize(); - float halfSize = iconSz * 0.25f; - - // Draw chevron (right arrow pointing down when expanded) - QPainterPath path; - if (expanded) { - // Downward chevron - path.moveTo(center.x() - halfSize, center.y() - halfSize / 2.0f); - path.lineTo(center.x(), center.y() + halfSize / 2.0f); - path.lineTo(center.x() + halfSize, center.y() - halfSize / 2.0f); - } else { - // Right chevron - path.moveTo(center.x() - halfSize / 2.0f, center.y() - halfSize); - path.lineTo(center.x() + halfSize / 2.0f, center.y()); - path.lineTo(center.x() - halfSize / 2.0f, center.y() + halfSize); - } - - painter->drawPath(path); -} - -void TreeViewItemDelegate::drawCheckBox(QPainter* painter, const QRectF& checkRect, - Qt::CheckState state, - const QStyleOptionViewItem& option) const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float strokeThickness = helper.dpToPx(2.0f); - float cornerRadius = helper.dpToPx(2.0f); - - painter->setRenderHint(QPainter::Antialiasing); - - // Get checkbox color - CFColor boxColor = onSurfaceVariantColor(); - CFColor checkColorCf = onSurfaceColor(); - - bool enabled = option.state & QStyle::State_Enabled; - QColor borderColor = boxColor.native_color(); - QColor checkColor = checkColorCf.native_color(); - - if (!enabled) { - borderColor.setAlpha(100); - checkColor.setAlpha(100); - } - - // Draw checkbox border - QPainterPath boxPath; - boxPath.addRoundedRect(checkRect, cornerRadius, cornerRadius); - - painter->setPen(QPen(borderColor, strokeThickness)); - painter->setBrush(Qt::NoBrush); - painter->drawPath(boxPath); - - // Draw checkmark or partial state - if (state == Qt::Checked) { - // Draw checkmark - QPainterPath checkPath; - float w = checkRect.width(); - float h = checkRect.height(); - float inset = helper.dpToPx(4.0f); - float midY = checkRect.center().y(); - - // Checkmark path - checkPath.moveTo(checkRect.left() + inset, midY); - checkPath.lineTo(checkRect.center().x() - helper.dpToPx(1.0f), checkRect.bottom() - inset); - checkPath.lineTo(checkRect.right() - inset, checkRect.top() + inset); - - painter->setPen( - QPen(checkColor, strokeThickness, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); - painter->setRenderHint(QPainter::Antialiasing); - painter->drawPath(checkPath); - } else if (state == Qt::PartiallyChecked) { - // Draw minus sign for partial state - float hPadding = helper.dpToPx(4.0f); - float lineY = checkRect.center().y(); - painter->setPen(QPen(checkColor, strokeThickness, Qt::SolidLine, Qt::RoundCap)); - painter->drawLine(QPointF(checkRect.left() + hPadding, lineY), - QPointF(checkRect.right() - hPadding, lineY)); - } -} - -void TreeViewItemDelegate::drawItemContent(QPainter* painter, const QRectF& contentRect, - const QStyleOptionViewItem& option, - const QModelIndex& index) const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float iconSz = helper.dpToPx(24.0f); - float iconGap = helper.dpToPx(12.0f); - - // Get text color - check for custom foreground color first - QVariant fgVar = index.data(Qt::ForegroundRole); - QColor textColor; - bool enabled = option.state & QStyle::State_Enabled; - - if (fgVar.isValid()) { - QBrush fgBrush = fgVar.value(); - textColor = fgBrush.color(); - if (!enabled) { - textColor.setAlphaF(textColor.alphaF() * 0.38f); - } - } else { - CFColor defaultColor = onSurfaceColor(); - textColor = defaultColor.native_color(); - if (!enabled) { - textColor.setAlphaF(0.38f); - } - } - painter->setPen(textColor); - - // Get font - check for custom font first - QFont font = painter->font(); - QVariant fontVar = index.data(Qt::FontRole); - if (fontVar.isValid()) { - font = fontVar.value(); - } else { - font.setPixelSize(static_cast(helper.dpToPx(14.0f))); - } - painter->setFont(font); - - // Check for decoration icon - QVariant decoration = index.data(Qt::DecorationRole); - bool hasIcon = false; - if (decoration.isValid()) { - QIcon icon = decoration.value(); - hasIcon = !icon.isNull(); - } - - // Get text alignment - check for custom alignment - Qt::Alignment alignment = Qt::AlignLeft | Qt::AlignVCenter; - QVariant alignVar = index.data(Qt::TextAlignmentRole); - if (alignVar.isValid()) { - alignment = static_cast(alignVar.toInt()); - } - - // Calculate available width for text - float availableTextWidth = contentRect.width(); - float currentX = contentRect.left(); - if (hasIcon && (alignment & Qt::AlignLeft)) { - availableTextWidth -= (iconSz + iconGap); - } - - // Get display text - QString text = index.data(Qt::DisplayRole).toString(); - - QFontMetrics fm(font); - - // Draw decoration icon first (left side) - if (hasIcon && (alignment & Qt::AlignLeft)) { - QIcon icon = decoration.value(); - QRectF iconRect(currentX, contentRect.center().y() - iconSz / 2.0f, iconSz, iconSz); - icon.paint(painter, iconRect.toRect()); - currentX += iconSz + iconGap; - availableTextWidth = contentRect.right() - currentX; - } - - // Draw text - NOT elided, show full text - QRectF textRect(currentX, contentRect.top(), availableTextWidth, contentRect.height()); - painter->drawText(textRect, alignment, text); - - // Draw decoration icon on right side if right-aligned - if (hasIcon && (alignment & Qt::AlignRight)) { - QIcon icon = decoration.value(); - float textWidth = fm.horizontalAdvance(text); - float iconX = contentRect.left() + textWidth + iconGap; - QRectF iconRect(iconX, contentRect.center().y() - iconSz / 2.0f, iconSz, iconSz); - icon.paint(painter, iconRect.toRect()); - } -} - -// ============================================================================ -// Tree Structure Helpers -// ============================================================================ - -int TreeViewItemDelegate::getDepth(const QModelIndex& index, const QTreeView* view) const { - if (!index.isValid()) { - return 0; - } - - // Count ancestors by walking up the model hierarchy - // This is more reliable than using visualRect.x() which can be affected - // by Qt's internal layout calculations - int depth = 0; - QModelIndex current = index; - while (current.isValid()) { - current = current.parent(); - if (current.isValid()) { - depth++; - } - } - return depth; -} - -bool TreeViewItemDelegate::isLastSibling(const QModelIndex& index, const QTreeView* view) const { - if (!index.isValid() || !view) { - return true; - } - - QModelIndex parent = index.parent(); - // Use custom property instead of view->rootIsDecorated() (non-virtual) - bool rootDecorated = view->property("_q_rootIsDecorated").toBool(); - if (!parent.isValid() && !rootDecorated) { - // For root items without decoration, consider row count - const QAbstractItemModel* model = index.model(); - if (model && index.row() == model->rowCount() - 1) { - return true; - } - return false; - } - - // Check if this is the last child of its parent - if (!parent.isValid()) { - // Top level item - const QAbstractItemModel* model = index.model(); - if (!model) { - return true; - } - return index.row() == model->rowCount() - 1; - } - - return index.row() == parent.model()->rowCount(parent) - 1; -} - -bool TreeViewItemDelegate::shouldContinueLineAtLevel(const QModelIndex& index, int level, - const QTreeView* view) const { - // Determine if the tree line should continue at this level - // The line continues if the ancestor at this level is NOT the last sibling - - if (!index.isValid() || !view) { - return false; - } - - // Walk up the tree to find the ancestor at the given level - QModelIndex current = index; - int currentDepth = getDepth(index, view); - - // We need to go up (currentDepth - level) levels - int levelsUp = currentDepth - level; - if (levelsUp <= 0) { - return false; - } - - for (int i = 0; i < levelsUp && current.isValid(); i++) { - current = current.parent(); - } - - if (!current.isValid()) { - return false; - } - - // The line continues if this ancestor is NOT the last sibling - return !isLastSibling(current, view); -} - -// ============================================================================ -// Layout Helpers -// ============================================================================ - -float TreeViewItemDelegate::horizontalPadding() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(16.0f); -} - -float TreeViewItemDelegate::iconSize() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - return helper.dpToPx(24.0f); -} - -// ============================================================================ -// Color Access Methods -// ============================================================================ - -CFColor TreeViewItemDelegate::surfaceColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(SURFACE)); - } catch (...) { - return fallbackSurface(); - } -} - -CFColor TreeViewItemDelegate::onSurfaceColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - return fallbackOnSurface(); - } -} - -CFColor TreeViewItemDelegate::onSurfaceVariantColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurfaceVariant(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE_VARIANT)); - } catch (...) { - return fallbackOnSurfaceVariant(); - } -} - -CFColor TreeViewItemDelegate::primaryContainerColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackPrimaryContainer(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(PRIMARY_CONTAINER)); - } catch (...) { - return fallbackPrimaryContainer(); - } -} - -CFColor TreeViewItemDelegate::outlineVariantColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOutlineVariant(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(OUTLINE_VARIANT)); - } catch (...) { - return fallbackOutlineVariant(); - } -} - -// ============================================================================ -// Public Accessors for TreeView Hit Testing -// ============================================================================ - -float TreeViewItemDelegate::horizontalPaddingValue() const { - return horizontalPadding(); -} - -float TreeViewItemDelegate::iconSizeValue() const { - return iconSize(); -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/treeview/private/treeviewdelegate.h b/ui/widget/material/widget/treeview/private/treeviewdelegate.h deleted file mode 100644 index ec2239609..000000000 --- a/ui/widget/material/widget/treeview/private/treeviewdelegate.h +++ /dev/null @@ -1,238 +0,0 @@ -/** - * @file treeviewdelegate.h - * @brief Material Design 3 TreeView Delegate - * - * Provides custom rendering and size hints for TreeView items following - * Material Design 3 specifications. Handles tree lines, expand/collapse - * icons, item backgrounds, and proper indentation. - * - * @author Material Design Framework Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include "../treeview.h" -#include "base/color.h" -#include "export.h" -#include -#include - -class QTreeView; - -namespace cf::ui::widget::material { - -// TreeItemHeight and TreeIndentStyle are defined in treeview.h - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 TreeView item delegate. - * - * @details Handles rendering of individual tree items including: - * - Background and selection states - * - Tree connection lines - * - Expand/collapse icons - * - Text and decoration icons - * - Hover and pressed states - * - Proper indentation per level - * - * The delegate does NOT handle: - * - Ripple effects (handled by TreeView) - * - Focus indicators (handled by TreeView) - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT TreeViewItemDelegate : public QStyledItemDelegate { - Q_OBJECT - - public: - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note Defaults to Standard item height and Material indent style. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit TreeViewItemDelegate(QObject* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~TreeViewItemDelegate() override; - - /** - * @brief Paints the tree item. - * - * @param[in] painter QPainter to render with. - * @param[in] option Style options for the item. - * @param[in] index Model index for the item. - * - * @throws None - * @note Called by Qt's view framework for each visible item. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paint(QPainter* painter, const QStyleOptionViewItem& option, - const QModelIndex& index) const override; - - /** - * @brief Gets the size hint for the item. - * - * @param[in] option Style options for the item. - * @param[in] index Model index for the item. - * - * @return Size hint for the item. - * - * @throws None - * @note Height is fixed based on itemHeight setting. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; - - /** - * @brief Sets the item height mode. - * - * @param[in] height Item height mode to use. - * - * @throws None - * @note Triggers layout update. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setItemHeight(TreeItemHeight height); - - /** - * @brief Sets the indentation style. - * - * @param[in] style Indentation style to use. - * - * @throws None - * @note Triggers repaint. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setIndentStyle(TreeIndentStyle style); - - /** - * @brief Sets whether tree connection lines are shown. - * - * @param[in] show true to show tree lines, false otherwise. - * - * @throws None - * @note Triggers repaint. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setShowTreeLines(bool show); - - /** - * @brief Gets the current item height in pixels. - * - * @return Item height in device pixels. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - float itemHeightValue() const; - - /** - * @brief Gets the indentation per level in pixels. - * - * @return Indentation width in device pixels. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - float indentPerLevel() const; - - /** - * @brief Gets the horizontal padding in pixels. - * - * @return Horizontal padding in device pixels. - * - * @throws None - * @note Used by TreeView for expand/collapse hit testing. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - float horizontalPaddingValue() const; - - /** - * @brief Gets the expand/collapse icon size in pixels. - * - * @return Icon size in device pixels. - * - * @throws None - * @note Used by TreeView for expand/collapse hit testing. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - float iconSizeValue() const; - - private: - // Drawing helpers - void drawBackground(QPainter* painter, const QStyleOptionViewItem& option, - const QModelIndex& index) const; - void drawTreeLines(QPainter* painter, const QModelIndex& index, const QRectF& itemRect, - const QTreeView* view) const; - void drawExpandCollapseIcon(QPainter* painter, const QRectF& iconRect, bool expanded, - const CFColor& color) const; - void drawCheckBox(QPainter* painter, const QRectF& checkRect, Qt::CheckState state, - const QStyleOptionViewItem& option) const; - void drawItemContent(QPainter* painter, const QRectF& contentRect, - const QStyleOptionViewItem& option, const QModelIndex& index) const; - - // Tree structure helpers - int getDepth(const QModelIndex& index, const QTreeView* view) const; - bool isLastSibling(const QModelIndex& index, const QTreeView* view) const; - bool shouldContinueLineAtLevel(const QModelIndex& index, int level, - const QTreeView* view) const; - - // Layout helpers - float horizontalPadding() const; - float iconSize() const; - - // Color access methods - CFColor surfaceColor() const; - CFColor onSurfaceColor() const; - CFColor onSurfaceVariantColor() const; - CFColor primaryContainerColor() const; - CFColor outlineVariantColor() const; - - // Properties - TreeItemHeight m_itemHeight; - TreeIndentStyle m_indentStyle; - bool m_showTreeLines; - float m_itemHeightPx; - float m_indentPx; -}; - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/treeview/treeview.cpp b/ui/widget/material/widget/treeview/treeview.cpp deleted file mode 100644 index 1ae19d81c..000000000 --- a/ui/widget/material/widget/treeview/treeview.cpp +++ /dev/null @@ -1,452 +0,0 @@ -/** - * @file treeview.cpp - * @brief Material Design 3 TreeView Implementation - * - * Implements a Material Design 3 tree view with hierarchical data display. - * Features expand/collapse animations, tree connection lines, proper - * indentation, and Material Design color tokens. - * - * Rendering is delegated to TreeViewItemDelegate following Qt's Model/View - * architecture. This class handles events, state management, and coordinates - * between the view and delegate. - * - * @author Material Design Framework Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material - */ - -#include "treeview.h" -#include "application_support/application.h" -#include "base/device_pixel.h" -#include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "private/treeviewdelegate.h" - -#include -#include -#include -#include - -namespace cf::ui::widget::material { - -using namespace cf::ui::base; -using namespace cf::ui::base::device; -using namespace cf::ui::core; -using namespace cf::ui::core::token::literals; -using namespace cf::ui::widget::material::base; -using namespace cf::ui::widget::application_support; - -// ============================================================================ -// Fallback Colors -// ============================================================================ - -namespace { -inline CFColor fallbackSurface() { - return CFColor(253, 253, 253); -} -inline CFColor fallbackOnSurface() { - return CFColor(27, 27, 31); -} -} // namespace - -// ============================================================================ -// Constructor / Destructor -// ============================================================================ - -TreeView::TreeView(QWidget* parent) - : QTreeView(parent), m_material(this, - MaterialWidgetBase::Config{ - .useRipple = true, - .useElevation = false, - .useFocusIndicator = true, - }), - m_itemHeight(TreeItemHeight::Standard), m_indentStyle(TreeIndentStyle::Material), - m_showTreeLines(true), m_rootIsDecorated(true) { - // Initialize and set the delegate for item rendering - m_delegate = new TreeViewItemDelegate(this); - setItemDelegate(m_delegate); - - // Configure tree view for Material appearance - setHeaderHidden(true); - // Disable Qt's default branch decoration to avoid duplicate arrow rendering - // Our delegate handles drawing expand/collapse icons - // IMPORTANT: Call parent class method directly to bypass our override - QTreeView::setRootIsDecorated(false); - setIndentation(static_cast(std::ceil(m_delegate->indentPerLevel()))); - setUniformRowHeights(true); - - // Expose root decorated state to delegate via property - setProperty("_q_rootIsDecorated", m_rootIsDecorated); - - // Disable default frame - setFrameShape(QFrame::NoFrame); - setFrameShadow(QFrame::Plain); - - // Enable mouse tracking for hover effects - viewport()->setMouseTracking(true); - - // Set viewport margins for padding - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float vPadding = helper.dpToPx(8.0f); - setViewportMargins(0, static_cast(vPadding), 0, static_cast(vPadding)); - - // Set item height through delegate - setItemHeight(m_itemHeight); -} - -TreeView::~TreeView() { - // Components are parented to this, Qt will delete them automatically -} - -// ============================================================================ -// Property Getters/Setters -// ============================================================================ - -TreeItemHeight TreeView::itemHeight() const { - return m_itemHeight; -} - -void TreeView::setItemHeight(TreeItemHeight height) { - if (m_itemHeight != height) { - m_itemHeight = height; - // Update delegate with new height - if (m_delegate) { - m_delegate->setItemHeight(height); - } - setUniformRowHeights(true); - doItemsLayout(); - update(); - } -} - -TreeIndentStyle TreeView::indentStyle() const { - return m_indentStyle; -} - -void TreeView::setIndentStyle(TreeIndentStyle style) { - if (m_indentStyle != style) { - m_indentStyle = style; - if (m_delegate) { - m_delegate->setIndentStyle(style); - } - setIndentation(static_cast(std::ceil(m_delegate->indentPerLevel()))); - update(); - } -} - -bool TreeView::showTreeLines() const { - return m_showTreeLines; -} - -void TreeView::setShowTreeLines(bool show) { - if (m_showTreeLines != show) { - m_showTreeLines = show; - if (m_delegate) { - m_delegate->setShowTreeLines(show); - } - update(); - } -} - -bool TreeView::rootIsDecorated() const { - return m_rootIsDecorated; -} - -void TreeView::setRootIsDecorated(bool decorated) { - if (m_rootIsDecorated != decorated) { - m_rootIsDecorated = decorated; - // Always keep Qt's default disabled - our delegate handles drawing icons - // This must be called to prevent Qt from reserving space for branch indicators - QTreeView::setRootIsDecorated(false); - // Expose state to delegate via property - setProperty("_q_rootIsDecorated", m_rootIsDecorated); - update(); - } -} - -// ============================================================================ -// Size Hints -// ============================================================================ - -QSize TreeView::sizeHint() const { - // Minimum visible area: show at least 3 items - float itemH = itemHeightValue(); - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float minWidth = helper.dpToPx(200.0f); - float minHeight = itemH * 3.0f; - - return QSize(static_cast(std::ceil(minWidth)), static_cast(std::ceil(minHeight))); -} - -QSize TreeView::minimumSizeHint() const { - // Show at least 2 items - float itemH = itemHeightValue(); - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float minWidth = helper.dpToPx(150.0f); - float minHeight = itemH * 2.0f; - - return QSize(static_cast(std::ceil(minWidth)), static_cast(std::ceil(minHeight))); -} - -// ============================================================================ -// Layout Helpers -// ============================================================================ - -float TreeView::itemHeightValue() const { - return m_delegate ? m_delegate->itemHeightValue() : 56.0f; -} - -// ============================================================================ -// Color Access Methods -// ============================================================================ - -CFColor TreeView::surfaceColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(SURFACE)); - } catch (...) { - return fallbackSurface(); - } -} - -CFColor TreeView::onSurfaceColor() const { - auto* app = Application::instance(); - if (!app) { - return fallbackOnSurface(); - } - - try { - const auto& theme = app->currentTheme(); - auto& colorScheme = theme.color_scheme(); - return CFColor(colorScheme.queryColor(ON_SURFACE)); - } catch (...) { - return fallbackOnSurface(); - } -} - -// ============================================================================ -// Event Handlers -// ============================================================================ - -void TreeView::enterEvent(QEnterEvent* event) { - QTreeView::enterEvent(event); - m_material.onEnterEvent(); - // Initialize hovered index on enter - QPoint localPos = viewport()->mapFromGlobal(QCursor::pos()); - m_hoveredIndex = indexAt(localPos); - setProperty("_q_hoveredIndex", QVariant::fromValue(m_hoveredIndex)); - update(); -} - -void TreeView::leaveEvent(QEvent* event) { - QTreeView::leaveEvent(event); - m_material.onLeaveEvent(); - // Clear hovered index - if (m_hoveredIndex.isValid()) { - QPersistentModelIndex oldIndex = m_hoveredIndex; - m_hoveredIndex = QPersistentModelIndex(); - setProperty("_q_hoveredIndex", QVariant()); - update(oldIndex); - } -} - -void TreeView::mouseMoveEvent(QMouseEvent* event) { - QTreeView::mouseMoveEvent(event); - - // Map to viewport coordinates - QPoint viewportPos = viewport()->mapFromParent(event->pos()); - - // Update hovered index - QModelIndex newIndex = indexAt(viewportPos); - if (newIndex != m_hoveredIndex) { - // Store old index for update - QPersistentModelIndex oldIndex = m_hoveredIndex; - m_hoveredIndex = newIndex; - - // Set property for delegate to access - setProperty("_q_hoveredIndex", QVariant::fromValue(m_hoveredIndex)); - - // Update both old and new regions - if (oldIndex.isValid()) { - update(oldIndex); - } - if (m_hoveredIndex.isValid()) { - update(m_hoveredIndex); - } - } -} - -void TreeView::mousePressEvent(QMouseEvent* event) { - // Map to viewport coordinates for hit testing - QPoint viewportPos = viewport()->mapFromParent(event->pos()); - QModelIndex index = indexAt(viewportPos); - - // Check if click is on expand/collapse icon - if (index.isValid() && model() && model()->hasChildren(index)) { - // Replicate the delegate's expand icon position calculation - QRect itemRect = visualRect(index); // viewport coordinates - float hPadding = m_delegate->horizontalPaddingValue(); - float iconSz = m_delegate->iconSizeValue(); - float indent = m_delegate->indentPerLevel(); - - // Calculate depth by walking up the model hierarchy - int depth = 0; - QModelIndex current = index; - while (current.parent().isValid()) { - current = current.parent(); - depth++; - } - - // Calculate expand icon rect (must match delegate's paint logic) - QRectF expandRect(hPadding + (indent * depth), itemRect.center().y() - iconSz / 2.0f, - iconSz, iconSz); - - if (expandRect.contains(viewportPos)) { - // Click is on expand/collapse icon - manually toggle expansion - if (isExpanded(index)) { - collapse(index); - } else { - expand(index); - } - - // Still handle ripple/state effects - if (m_material.stateMachine()) { - m_material.stateMachine()->onPress(event->pos()); - } - if (m_material.ripple()) { - // Use viewport coordinates for ripple (painter draws on viewport) - m_material.ripple()->onPress(viewportPos, QRectF(itemRect)); - // Store clip rect for ripple animation - m_rippleClipRect = QRectF(itemRect); - } - m_pressedIndex = index; - update(); - return; // Don't call base class - avoid Qt's incorrect hit detection - } - } - - // Non-icon area: use normal flow (selection, ripple, etc.) - QTreeView::mousePressEvent(event); - - if (m_material.stateMachine()) { - m_material.stateMachine()->onPress(event->pos()); - } - - // Track pressed index for ripple - QModelIndex pressedIndex = indexAt(viewportPos); - if (pressedIndex.isValid()) { - m_pressedIndex = pressedIndex; - m_pressPosition = event->pos(); - - if (m_material.ripple()) { - QRect itemRect = visualRect(pressedIndex); - // Use viewport coordinates for ripple (painter draws on viewport) - m_material.ripple()->onPress(viewportPos, QRectF(itemRect)); - // Store clip rect for ripple animation (so it continues after release) - m_rippleClipRect = QRectF(itemRect); - } - } - update(); -} - -void TreeView::mouseReleaseEvent(QMouseEvent* event) { - QTreeView::mouseReleaseEvent(event); - - if (m_material.stateMachine()) { - m_material.stateMachine()->onRelease(); - } - if (m_material.ripple()) { - m_material.ripple()->onRelease(); - } - m_pressedIndex = QPersistentModelIndex(); - update(); -} - -void TreeView::focusInEvent(QFocusEvent* event) { - QTreeView::focusInEvent(event); - m_material.onFocusIn(); -} - -void TreeView::focusOutEvent(QFocusEvent* event) { - QTreeView::focusOutEvent(event); - m_material.onFocusOut(); -} - -void TreeView::changeEvent(QEvent* event) { - QTreeView::changeEvent(event); - if (event->type() == QEvent::EnabledChange) { - m_material.onEnabledChange(isEnabled()); - } -} - -void TreeView::rowsInserted(const QModelIndex& parent, int start, int end) { - QTreeView::rowsInserted(parent, start, end); - // Ensure layout is updated after model changes - doItemsLayout(); -} - -// ============================================================================ -// Draw Branches Override -// ============================================================================ - -void TreeView::drawBranches(QPainter* painter, const QRect& rect, const QModelIndex& index) const { - // Completely disable Qt's default branch drawing - // Our delegate handles all expand/collapse icon rendering - Q_UNUSED(painter); - Q_UNUSED(rect); - Q_UNUSED(index); -} - -// ============================================================================ -// Paint Event -// ============================================================================ - -void TreeView::paintEvent(QPaintEvent* event) { - // Draw viewport background - QPainter p(viewport()); - p.setRenderHint(QPainter::Antialiasing); - p.fillRect(viewport()->rect(), surfaceColor().native_color()); - - // Let Qt handle tree rendering via delegate - // This calls the delegate's paint() method for each visible item - QTreeView::paintEvent(event); - - // Draw ripple if active (overlay on top of items) - // RippleHelper manages its own state - always try to paint - if (m_material.ripple() && !m_rippleClipRect.isEmpty()) { - m_material.ripple()->setColor(onSurfaceColor()); - QPainterPath clipPath; - clipPath.addRect(m_rippleClipRect); - p.save(); - p.setClipPath(clipPath); - m_material.ripple()->paint(&p, clipPath); - p.restore(); - - // Clear clip rect when ripple is no longer active - if (!m_material.ripple()->hasActiveRipple()) { - m_rippleClipRect = QRectF(); - } - } - - // Draw focus indicator on current index if focused - if (m_material.focusIndicator() && hasFocus()) { - QModelIndex current = currentIndex(); - if (current.isValid()) { - QRect itemRect = visualRect(current); - if (itemRect.intersects(viewport()->rect())) { - QPainterPath shape; - shape.addRect(itemRect); - m_material.focusIndicator()->paint(&p, shape, onSurfaceColor()); - } - } - } -} - -} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/treeview/treeview.h b/ui/widget/material/widget/treeview/treeview.h deleted file mode 100644 index f04527dc9..000000000 --- a/ui/widget/material/widget/treeview/treeview.h +++ /dev/null @@ -1,410 +0,0 @@ -/** - * @file ui/widget/material/widget/treeview/treeview.h - * @brief Material Design 3 TreeView widget. - * - * Implements Material Design 3 tree view for hierarchical data display. - * Features expand/collapse animations, tree connection lines, proper - * indentation, and Material Design color tokens. - * - * @author Material Design Framework Team - * @date 2026-03-18 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include "base/color.h" -#include "export.h" -#include "widget/material/base/material_widget_base.h" -#include -#include -#include - -namespace cf::ui::widget::material { - -class TreeViewItemDelegate; - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Tree item height mode. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -enum class TreeItemHeight { - Compact, ///< 48dp height for dense content - Standard ///< 56dp height for standard content -}; - -/** - * @brief Tree indentation style. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -enum class TreeIndentStyle { - Material, ///< 56dp per level with guide lines - Classic ///< Traditional nested indentation -}; - -/** - * @brief Material Design 3 TreeView widget. - * - * @details Implements Material Design 3 tree view with hierarchical data - * display support. Features include: - * - Expand/collapse with animation support - * - Tree connection lines for visual hierarchy - * - Proper indentation per level - * - Ripple effects on item interaction - * - Material Design 3 color tokens - * - Focus indicators for keyboard navigation - * - * Rendering is handled by TreeViewItemDelegate following Qt's - * Model/View architecture. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT TreeView : public QTreeView { - Q_OBJECT - Q_PROPERTY(TreeItemHeight itemHeight READ itemHeight WRITE setItemHeight) - Q_PROPERTY(TreeIndentStyle indentStyle READ indentStyle WRITE setIndentStyle) - Q_PROPERTY(bool showTreeLines READ showTreeLines WRITE setShowTreeLines) - Q_PROPERTY(bool rootIsDecorated READ rootIsDecorated WRITE setRootIsDecorated) - - public: - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note Defaults to Standard item height and Material indent style. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit TreeView(QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note Components are parented to this, Qt deletes them. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~TreeView() override; - - /** - * @brief Gets the item height mode. - * - * @return Current item height mode. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - TreeItemHeight itemHeight() const; - - /** - * @brief Sets the item height mode. - * - * @param[in] height Item height mode to use. - * - * @throws None - * @note Triggers layout update. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setItemHeight(TreeItemHeight height); - - /** - * @brief Gets the indentation style. - * - * @return Current indentation style. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - TreeIndentStyle indentStyle() const; - - /** - * @brief Sets the indentation style. - * - * @param[in] style Indentation style to use. - * - * @throws None - * @note Triggers repaint. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setIndentStyle(TreeIndentStyle style); - - /** - * @brief Gets whether tree connection lines are shown. - * - * @return true if tree lines are visible, false otherwise. - * - * @throws None - * @note Tree lines show hierarchy between parent and child items. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool showTreeLines() const; - - /** - * @brief Sets whether tree connection lines are shown. - * - * @param[in] show true to show tree lines, false otherwise. - * - * @throws None - * @note Triggers repaint. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setShowTreeLines(bool show); - - /** - * @brief Gets whether root items are decorated. - * - * @return true if root items show expand/collapse controls. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool rootIsDecorated() const; - - /** - * @brief Sets whether root items are decorated. - * - * @param[in] decorated true to decorate root items, false otherwise. - * - * @throws None - * @note Triggers repaint. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setRootIsDecorated(bool decorated); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the tree view. - * - * @throws None - * @note Based on content and minimum item dimensions. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum size hint. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures minimum content visibility. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - protected: - /** - * @brief Paints the tree view. - * - * @param[in] event Paint event. - * - * @throws None - * @note Draws background, then lets Qt handle tree rendering via delegate. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles viewport mouse move event for hover tracking. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates hovered index for delegate. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseMoveEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple and state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse enter event. - * - * @param[in] event Enter event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void enterEvent(QEnterEvent* event) override; - - /** - * @brief Handles mouse leave event. - * - * @param[in] event Leave event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void leaveEvent(QEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Handles rows inserted event for layout update. - * - * @param[in] parent Parent model index. - * @param[in] start Start row. - * @param[in] end End row. - * - * @throws None - * @note Ensures proper layout after model changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void rowsInserted(const QModelIndex& parent, int start, int end) override; - - /** - * @brief Draws tree branch indicators. - * - * @param[in] painter QPainter to render with. - * @param[in] rect Rectangle for the branch area. - * @param[in] index Model index for the item. - * - * @throws None - * @note Empty implementation - delegate handles all expand/collapse icon rendering. - * This prevents Qt from drawing default branch indicators that would - * conflict with the custom delegate rendering. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void drawBranches(QPainter* painter, const QRect& rect, - const QModelIndex& index) const override; - - private: - // Layout helpers - float itemHeightValue() const; - - // Color access methods (for background and ripple) - CFColor surfaceColor() const; - CFColor onSurfaceColor() const; - - // Behavior components - base::MaterialWidgetBase m_material; - - // Delegate for item rendering - TreeViewItemDelegate* m_delegate; - - // Properties - TreeItemHeight m_itemHeight; - TreeIndentStyle m_indentStyle; - bool m_showTreeLines; - bool m_rootIsDecorated; - - // Per-index state tracking for proper hover/ripple - QPersistentModelIndex m_hoveredIndex; - QPersistentModelIndex m_pressedIndex; - QPoint m_pressPosition; - QRectF m_rippleClipRect; // Store the item rect where ripple started -}; - -} // namespace cf::ui::widget::material From fa201365f8fafe15e2b476acb311a4b41b46f143 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Fri, 19 Jun 2026 09:07:13 +0800 Subject: [PATCH 15/18] ci: fetch submodules recursively on build jobs cpp-check.yml's linux-gcc / linux-clang / windows-msvc checkouts now use actions/checkout submodules: recursive so third_party/aex and third_party/QuarkWidgets are populated at checkout time. Previously they relied on setup_third_party's FetchContent fallback (slow, and requires the submodule repos to be pushed + network at configure). --- .github/workflows/cpp-check.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/cpp-check.yml b/.github/workflows/cpp-check.yml index ccaabcbf8..c4b5d665a 100644 --- a/.github/workflows/cpp-check.yml +++ b/.github/workflows/cpp-check.yml @@ -67,6 +67,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: recursive - name: Normalize Docker image name run: echo "GHCR_IMAGE=$(echo '${{ env.GHCR_IMAGE }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" @@ -167,6 +169,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: recursive - name: Normalize Docker image name run: echo "GHCR_IMAGE=$(echo '${{ env.GHCR_IMAGE }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" @@ -273,6 +277,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: recursive - name: Setup MSVC developer environment uses: ilammy/msvc-dev-cmd@v1 From e10d15bf86ceb0bf7523380a0aec05fa88eaf49d Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Fri, 19 Jun 2026 09:32:21 +0800 Subject: [PATCH 16/18] refactor: seperate and simplified --- third_party/QuarkWidgets | 2 +- third_party/aex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/third_party/QuarkWidgets b/third_party/QuarkWidgets index 30b7936ea..73a3d1d8a 160000 --- a/third_party/QuarkWidgets +++ b/third_party/QuarkWidgets @@ -1 +1 @@ -Subproject commit 30b7936eac42563af2878c41272b2f32a07daedf +Subproject commit 73a3d1d8a6f65ae76a08009b524280b563888337 diff --git a/third_party/aex b/third_party/aex index 79628175f..11ea60a26 160000 --- a/third_party/aex +++ b/third_party/aex @@ -1 +1 @@ -Subproject commit 79628175fc89046dd89f46ee7a36b7bd2c80dcf5 +Subproject commit 11ea60a26c1aef3d1173f4b7f609746aa8b13042 From 85c491ad2f5b529aa2856078a2b02c78fd8a37c4 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Fri, 26 Jun 2026 08:38:39 +0800 Subject: [PATCH 17/18] aligned documents --- document/status/current.md | 10 +++++---- document/todo/desktop/06_infrastructure.md | 2 ++ .../todo/desktop/milestone_00_overview.md | 22 +++++++++---------- .../desktop/milestone_05_window_management.md | 4 +++- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/document/status/current.md b/document/status/current.md index 90c05bb7b..24d9fd53f 100644 --- a/document/status/current.md +++ b/document/status/current.md @@ -10,7 +10,7 @@ description: CFDesktop 项目进度的唯一事实来源与全局导航。 # CFDesktop 当前项目状态 -> **校准日期**:2026-06-15 | **版本**:0.19.0 +> **校准日期**:2026-06-26 | **版本**:0.19.0 > **本文件是项目进度的唯一事实来源(single source of truth)。** 其他位置一律指向此处,勿另行手抄。 ## 项目导航(各信息去哪看) @@ -48,11 +48,13 @@ description: CFDesktop 项目进度的唯一事实来源与全局导航。 ## 下一步路线(最小闭环先行) 1. **MS2 状态栏**(顶部时间 + 系统图标)— ✅ 功能落地(`StatusBar` 实现 + 注册 PanelManager + 主题跟随 + MD3 美化;offscreen 启动通过,待真机视觉确认) -2. **MS3 任务栏**(底部居中图标条 + hover 动画)— 🚧 进行中(最小切片跑通:`CenteredTaskbar` 注册 Bottom 面板 + `TaskbarIcon` 居中图标/hover 放大/自绘 ripple/运行指示器,构建通过;点击反馈先打 log,待接 MS4 真启动) +2. **MS3 任务栏**(底部居中图标条 + hover 动画)— ✅ 最小切片完成(`CenteredTaskbar` 注册 Bottom 面板 + `TaskbarIcon` 居中图标/hover 放大/自绘 ripple/运行指示器;`appClicked` 已接 `AppLaunchService::launch` 真启动并记 PID,运行指示器经 `WindowManager` 窗口追踪联动——见 `desktop/ui/CFDesktopEntity.cpp:133-180`) 3. **MS4 应用启动器**(应用网格 + QProcess 启动)— 🚧 进行中(最小启动闭环跑通:`AppLaunchService` 用 `QProcess::startDetached` 启动 taskbar 图标对应应用,点 Files/Browser 可真打开;开始菜单弹窗网格待做) -4. **MS5 窗口管理**(窗口装饰 + 任务栏联动)— 🚧 进行中(追踪+联动切片跑通:`WindowManager` 追踪外部窗口 + `IWindow::pid()` + Taskbar 运行指示器联动;靠 PID 匹配,直接启动(xterm)可靠、间接启动(xdg-open)受限;窗口装饰/操作因 WSL X11 客户端架构不可行,暂跳过) +4. **MS5 窗口管理**(窗口装饰 + 任务栏联动)— 🚧 进行中(追踪+联动切片跑通:`WindowManager` 追踪外部窗口 + `IWindow::pid()` + Taskbar 运行指示器联动;靠 PID 匹配,直接启动(xterm)可靠、间接启动(xdg-open)受限;**窗口装饰已定策→overlay 渲染**〔策略 A:CFDesktop 自绘 overlay 层呈现标题栏+控制按钮作纯视觉指示,因 WSL X11 客户端模式下外部窗口由 XWayland 管理无法直接装饰;X11 WM〔B〕/ Wayland Compositor〔C〕延后到 EGLFS/Wayland 后端,详见 [milestone_05](../todo/desktop/milestone_05_window_management.md):157-160〕) -闭环达成后按需推进:HWTier 策略引擎、CrashHandler、IPC、EGLFS 嵌入式后端、输入抽象层、P2/P3 控件。 +闭环达成后按需推进:CrashHandler、IPC、EGLFS 嵌入式后端、输入抽象层、P2/P3 控件。 + +> **HWTier 优先级决策(2026-06-26)**:检测 + 评分 + 策略覆写**已完成**(Phase 1,`base/system/hardware_tier/`:`IHardwareCollector / Scorer / Assessor / Policy` 全套就绪)。故 [`summary.md`](../todo/desktop/summary.md) 与 [`06_infrastructure.md`](../todo/desktop/06_infrastructure.md) 里「HWTier 0% 🔴、上线前必做」的表述**已过时**——那针对的是 embedded/production 上线。当前 demo / 可见桌面路线:**CapabilityPolicy 策略引擎**(把档位转成动效/渲染/内存降级策略并接入 Shell)**延后**,开发期一律按 High Tier;待 embedded pivot 再接。 ## 最近里程碑(git 可证) diff --git a/document/todo/desktop/06_infrastructure.md b/document/todo/desktop/06_infrastructure.md index a825778bf..d57294691 100644 --- a/document/todo/desktop/06_infrastructure.md +++ b/document/todo/desktop/06_infrastructure.md @@ -23,6 +23,8 @@ description: "状态: 🚧 部分完成 (~50%),预计周期: 4~5 周" ### HWTier 分级系统 +> **状态订正(2026-06-26)**:HWTier **检测 + 评分 + 档位覆写已完成**(Phase 1,见 `base/system/hardware_tier/` 的 `IHardwareCollector/Scorer/Assessor/Policy`,以及 [`current.md`](../../status/current.md))。下列 Day 1-2 任务实际已完成;**CapabilityPolicy 策略引擎**(Day 5,档位→动效/渲染/内存降级并接入 Shell)在 demo / 可见桌面路线**延后**(开发期按 High Tier),待 embedded/production pivot 再做。本节「必做」语义针对 embedded 上线,不阻塞当前 demo 路线。 + #### Day 1-2: HWTier 分级系统 - [ ] 定义 HWTier 枚举 - [ ] `enum class Tier { Low, Mid, High, Unknown }` diff --git a/document/todo/desktop/milestone_00_overview.md b/document/todo/desktop/milestone_00_overview.md index e08ec7e51..4b255fb8d 100644 --- a/document/todo/desktop/milestone_00_overview.md +++ b/document/todo/desktop/milestone_00_overview.md @@ -15,11 +15,11 @@ description: "创建日期: 2026-03-31,目标: 将当前空白桌面推进到 | # | 里程碑 | 状态 | 核心交付 | 预计工作量 | |---|--------|------|----------|-----------| | MS1 | [桌面骨架可见](../done/SUMMARY.md) | ✅ | 壁纸/背景 + 正确的面板布局计算 | 1-2 天 | -| MS2 | [状态栏](milestone_02_status_bar.md) | ⬜ | 顶部时间+系统图标面板 | 3-5 天 | -| MS3 | [任务栏/导航栏](milestone_03_taskbar.md) | ⬜ | 底部任务栏 (居中图标) | 5-7 天 | -| MS4 | [应用启动器](milestone_04_app_launcher.md) | ⬜ | 应用网格弹窗 + 外部进程启动 | 5-7 天 | -| MS5 | [窗口管理可见](milestone_05_window_management.md) | ⬜ | 窗口装饰 + 最小化/最大化/关闭 + 任务栏联动 | 7-10 天 | -| MS6 | [小组件+控制中心](milestone_06_widget_control_center.md) | ⬜ | 桌面时钟 + 下拉控制面板 + 主题切换 | 7-10 天 | +| MS2 | [状态栏](milestone_02_status_bar.md) | ✅ 功能落地 | 顶部时间+系统图标面板 | 3-5 天 | +| MS3 | [任务栏/导航栏](milestone_03_taskbar.md) | ✅ 最小切片完成 | 底部任务栏 (居中图标) | 5-7 天 | +| MS4 | [应用启动器](milestone_04_app_launcher.md) | 🚧 闭环跑通·网格待做 | 应用网格弹窗 + 外部进程启动 | 5-7 天 | +| MS5 | [窗口管理可见](milestone_05_window_management.md) | 🚧 追踪联动跑通·装饰待做 | 窗口装饰 + 最小化/最大化/关闭 + 任务栏联动 | 7-10 天 | +| MS6 | [小组件+控制中心](milestone_06_widget_control_center.md) | ⬜ 待开始 | 桌面时钟 + 下拉控制面板 + 主题切换 | 7-10 天 | --- @@ -28,15 +28,15 @@ description: "创建日期: 2026-03-31,目标: 将当前空白桌面推进到 ```text MS1: 桌面骨架 (壁纸+布局) ✅ 已完成 │ - ├──→ MS2: 状态栏 (时间+图标) + ├──→ MS2: 状态栏 (时间+图标) ✅ 功能落地 │ │ - │ ├──→ MS3: 任务栏/导航栏 + │ ├──→ MS3: 任务栏/导航栏 ✅ 最小切片完成 │ │ │ - │ │ ├──→ MS4: 应用启动器 + │ │ ├──→ MS4: 应用启动器 🚧 闭环跑通(网格待做) │ │ │ - │ │ └──→ MS5: 窗口管理可见 + │ │ └──→ MS5: 窗口管理可见 🚧 追踪联动跑通(装饰待做) │ │ - │ └──→ MS6: 小组件+控制中心 (可与 MS3/4/5 并行) + │ └──→ MS6: 小组件+控制中心 (可与 MS3/4/5 并行) ⬜ ```bash **关键路径 (最快通路)**: MS1 → MS2 → MS3 → MS4 → MS5 @@ -80,4 +80,4 @@ MS1: 桌面骨架 (壁纸+布局) ✅ 已完成 --- -*最后更新: 2026-04-09* +*最后更新: 2026-06-26* diff --git a/document/todo/desktop/milestone_05_window_management.md b/document/todo/desktop/milestone_05_window_management.md index 7800d6f40..d6ff5deb9 100644 --- a/document/todo/desktop/milestone_05_window_management.md +++ b/document/todo/desktop/milestone_05_window_management.md @@ -5,10 +5,12 @@ description: "预计周期: 7-10 天,前置依赖: Milestone 3: 任务栏 (任 # Milestone 5: 窗口管理可见 -> **状态**: ⬜ 待开始 +> **状态**: 🚧 进行中(追踪 + 任务栏联动切片跑通;窗口装饰走 overlay 渲染〔策略 A〕待实现) > **预计周期**: 7-10 天 > **前置依赖**: [Milestone 3: 任务栏](milestone_03_taskbar.md) (任务栏联动) > **目标**: 外部应用窗口能被管理——有窗口装饰,支持最小化/最大化/关闭,任务栏同步显示运行中应用 +> +> **决策(2026-06-26)**:窗口装饰采用**策略 A — overlay 渲染**为初期路径(CFDesktop 自绘层呈现标题栏 + 控制按钮,纯视觉指示;WSL X11 客户端模式下外部窗口由 XWayland 管理无法直接装饰)。策略 B(X11 WM)/ 策略 C(Wayland Compositor)延后到 EGLFS/Wayland 后端阶段。 --- From 93f8d6998c20f4aacf481599d6563157c7a0585c Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Fri, 26 Jun 2026 09:10:03 +0800 Subject: [PATCH 18/18] fix(cmake): replace stale cfui target ref after QuarkWidgets extraction The QuarkWidgets extraction (Phase 2) renamed the cfui shared library to quarkwidgets (alias QuarkWidgets::quarkwidgets). test/desktop/init/CMakeLists.txt still referenced $ in the WIN32 post-build DLL-copy block, which broke CMake configure on Windows MSVC (generator-expression eval failed; WIN32-only so Linux was unaffected). Use $. Also fix the stale cfui in the ExampleLauncher.cmake usage comment. aex is INTERFACE (header-only), no DLL to copy. --- cmake/ExampleLauncher.cmake | 2 +- test/desktop/init/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/ExampleLauncher.cmake b/cmake/ExampleLauncher.cmake index a5ba9c725..3cd5b5251 100644 --- a/cmake/ExampleLauncher.cmake +++ b/cmake/ExampleLauncher.cmake @@ -141,7 +141,7 @@ endfunction() # # 用法: # cf_add_example_with_launcher(my_example "ui" main.cpp widget.cpp) -# target_link_libraries(my_example PRIVATE cfui Qt6::Widgets) +# target_link_libraries(my_example PRIVATE QuarkWidgets::quarkwidgets Qt6::Widgets) # macro(cf_add_example_with_launcher TARGET_NAME CATEGORY) # 创建可执行文件 diff --git a/test/desktop/init/CMakeLists.txt b/test/desktop/init/CMakeLists.txt index c3e66b328..567cbdea8 100644 --- a/test/desktop/init/CMakeLists.txt +++ b/test/desktop/init/CMakeLists.txt @@ -57,7 +57,7 @@ if(WIN32) $ $ COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ + $ $ COMMENT "Copying shared libraries to test output directory" )