diff --git a/.github/workflows/cpp-check.yml b/.github/workflows/cpp-check.yml new file mode 100644 index 000000000..817a4f0ad --- /dev/null +++ b/.github/workflows/cpp-check.yml @@ -0,0 +1,68 @@ +name: C++ Check + +on: + pull_request: + branches: + - main + - release/* + types: + - opened + - synchronize + - reopened + workflow_dispatch: + +permissions: + contents: read + +jobs: + linux-build-test: + name: Linux build and tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build Docker image + run: | + docker build \ + --platform linux/amd64 \ + --build-arg QT_ARCH=linux_gcc_64 \ + -f scripts/docker/Dockerfile.build \ + -t cfdesktop-build \ + . + + - name: Configure, build, and test + run: | + docker run --rm \ + --platform linux/amd64 \ + -e QT_QPA_PLATFORM=offscreen \ + -v "$PWD:/project" \ + -w /project \ + cfdesktop-build \ + bash -lc "bash scripts/build_helpers/linux_develop_build.sh ci -c build_ci_config.ini && bash scripts/build_helpers/linux_run_tests.sh ci -c build_ci_config.ini" + + docs-build: + name: VitePress docs build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.33.3 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build docs + run: pnpm build diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 811b36509..6f794f941 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,94 +1,61 @@ -# 工作流名称 -name: 自动部署 MkDocs +name: Deploy VitePress -# 触发条件:推送到 main 分支 on: push: branches: - main - - # 允许手动触发 workflow_dispatch: -# 设置权限 permissions: - contents: write # 允许推送到 gh-pages 分支 + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false jobs: - deploy: + build: runs-on: ubuntu-latest - + steps: - # 1. 检出代码 - - name: 检出仓库 + - name: Checkout uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 with: - fetch-depth: 0 # 获取完整历史,显示文章修改时间 - - # 2. 设置 Python - - name: 设置 Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - # cache: 'pip' # 缓存依赖,加速构建 - - # 3. 安装依赖 - - name: 安装依赖 - run: | - pip install mkdocs-material - pip install mkdocs-awesome-pages-plugin - pip install mkdocs-git-revision-date-localized-plugin - - # 4. 安装 Doxygen - - name: 安装 Doxygen - run: | - sudo apt-get update - sudo apt-get install -y doxygen + version: 10.33.3 - # 5. 安装 doxybook2 - - name: 安装 doxybook2 - run: | - # 下载最新版本的 doxybook2 - DOXYBOOK2_VERSION="1.5.0" - wget https://github.com/matusnovak/doxybook2/releases/download/v${DOXYBOOK2_VERSION}/doxybook2-linux-amd64-v${DOXYBOOK2_VERSION}.zip - unzip doxybook2-linux-amd64-v${DOXYBOOK2_VERSION}.zip - sudo mv bin/doxybook2 /usr/local/bin/ - sudo chmod +x /usr/local/bin/doxybook2 - doxybook2 --version + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm - # 6. 生成 API 文档(Doxygen → Markdown) - - name: 生成 API 文档 - run: | - doxygen Doxyfile - rm -rf document/api - mkdir -p document/api - doxybook2 --input ./xml \ - --output ./document/api \ - --config doxybook.json + - name: Setup Pages + uses: actions/configure-pages@v5 - # 6.5. 为自动生成的 API 文档配置导航 - - name: 配置 API 导航 - run: | - cat > document/api/.pages <<'EOF' - title: API 自动文档 - icon: material/file-document - nav: - - 命名空间: Namespaces - - 类: Classes - - 文件: Files - EOF + - name: Install dependencies + run: pnpm install --frozen-lockfile - # 6.6. 修复 doxybook2 生成的相对链接路径 - - name: 修复 API 文档链接 - run: python3 scripts/document/fix_doxybook_links.py + - name: Build docs + run: pnpm build - # 7. 构建网站 - - name: 构建网站 - run: mkdocs build --clean - - # 8. 自动部署到 gh-pages 分支(这一步会自动触发 GitHub Pages) - - name: 部署到 GitHub Pages - uses: peaceiris/actions-gh-pages@v4 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 with: - github_token: ${{ secrets.GITHUB_TOKEN }} # GitHub 自动提供的 token - publish_dir: ./site # MkDocs 构建输出目录 \ No newline at end of file + path: site/.vitepress/dist + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/docs-check.yml b/.github/workflows/docs-check.yml new file mode 100644 index 000000000..0fbf0b512 --- /dev/null +++ b/.github/workflows/docs-check.yml @@ -0,0 +1,41 @@ +name: Docs Check + +on: + pull_request: + branches: + - develop + types: + - opened + - synchronize + - reopened + - labeled + +permissions: + contents: read + +jobs: + vitepress: + name: VitePress build + if: contains(github.event.pull_request.labels.*.name, 'build-doc') + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.33.3 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build docs + run: pnpm build diff --git a/.gitignore b/.gitignore index 2dff386ea..adb742025 100644 --- a/.gitignore +++ b/.gitignore @@ -1,31 +1,38 @@ -# ignores -.cache -.claude - -# aqtinstall log -aqtinstall.log -# privates -BLUEPRINT.md - -# native builds -out/ - -# Documents summons -xml/ -site/ -document/api/ - -# .vscode -.vscode - -# third_party dependencies (downloaded during configuration) -third_party/*/ -!third_party/.gitkeep - -__pycache__/ - -# Python virtual environment -.venv/ - -# Docker build logs -scripts/docker/logger/ \ No newline at end of file +# ignores +.cache +.claude +CLAUDE.md +MEMORY.md + +# aqtinstall log +aqtinstall.log +# privates +BLUEPRINT.md + +# native builds +out/ + +# Documents summons +xml/ +document/api/ + +# .vscode +.vscode + +# third_party dependencies (downloaded during configuration) +third_party/*/ +!third_party/.gitkeep + +__pycache__/ + +# Python virtual environment +.venv/ + +# Docker build logs +scripts/docker/logger/ + +# Node/VitePress +node_modules/ +site/.vitepress/cache/ +site/.vitepress/dist/ +site/.vitepress/.temp diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 000000000..036484912 --- /dev/null +++ b/AGENT.md @@ -0,0 +1,51 @@ +# AGENT.md — 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 + +Read `FAILED_DOXYGEN.md` for the current list of violations, 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, etc.). + +### 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 +``` + +Iterate up to 3 passes if violations remain. + +## Key Constraints + +- 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. diff --git a/PROMPT_FRONTMATTER_AND_CODEBLOCKS.md b/PROMPT_FRONTMATTER_AND_CODEBLOCKS.md new file mode 100644 index 000000000..d4e83aff7 --- /dev/null +++ b/PROMPT_FRONTMATTER_AND_CODEBLOCKS.md @@ -0,0 +1,158 @@ +# CFDesktop 文档收尾:Frontmatter 补充 + 代码块语言标签统一 + +## 项目背景 + +CFDesktop 是一个 C++23/Qt6/CMake 跨平台桌面框架,文档站使用 VitePress 构建,源文件在 `document/` 目录下。此前已完成一轮文档一致性修复(英文→中文翻译、C++17 过时引用修正、占位内容清理、重复目录删除、遗留 MkDocs CSS 删除)。 + +现在需要完成两项收尾工作。 + +--- + +## 任务一:为所有非占位 .md 文件补充 VitePress YAML frontmatter + +### 规则 + +每个 `.md` 文件**开头**必须添加如下 frontmatter(如果没有的话): + +```yaml +--- +title: 文档标题 +description: 一句话描述页面内容 +--- +``` + +**要求:** +- `title`:取文件中第一个 `#` 标题的内容(去掉 markdown 标记),中文标题保持中文 +- `description`:根据文件内容写一句简短中文描述(20-50 字),包含英文技术术语 +- 如果文件已有 frontmatter,**不要重复添加**,检查是否缺少 `title` 或 `description`,补齐即可 +- `index.md` 文件通常标题是目录名描述,也需要加 +- **跳过** `document/api/` 目录(这是 Doxygen 自动生成的,不在 VitePress 构建范围内) +- `---` 是 YAML frontmatter 边界标记,和文件正文中的 `---`(水平分割线)不冲突——frontmatter 的 `---` 必须出现在文件的**最开始**两行 + +### 判断是否已有 frontmatter + +如果文件以 `---` 开头(第一行就是 `---`),则已有 frontmatter,检查并补齐字段。 +如果文件以 `#` 或其他内容开头,则没有 frontmatter,需要添加。 + +### 示例 + +修改前: +```markdown +# 快速开始指南 + +> 30 分钟内搭建 CFDesktop 开发环境 +``` + +修改后: +```markdown +--- +title: 快速开始指南 +description: 30 分钟搭建 CFDesktop 开发环境,涵盖克隆仓库、Docker 构建、运行示例和测试 +--- + +# 快速开始指南 + +> 30 分钟内搭建 CFDesktop 开发环境 +``` + +### 需要处理的目录 + +``` +document/development/ (10 个文件) +document/design_stage/ (10 个文件) +document/HandBook/ (~150 个文件,排除 api/ 子目录) +document/notes/ (7 个文件) +document/ci/ (5 个文件) +document/todo/ (~37 个文件) +document/optimize/ (2 个文件) +document/release_rule/ (2 个文件) +document/scripts/ (~59 个文件) +document/status/ (2 个文件) +document/index.md (首页) +``` + +**跳过:** `document/api/` 目录(自动生成,不在 VitePress 范围内) + +--- + +## 任务二:统一代码块语言标签 + +### 问题 + +文档中有 336 处代码块使用了无语言标签的 ` ``` ` 开关,导致 VitePress 无法正确进行语法高亮。 + +### 规则 + +将所有无语言标签的代码块根据内容加上正确的标签: + +| 内容类型 | 标签 | +|---------|------| +| C++ 代码 | `cpp` | +| CMake 代码 | `cmake` | +| Bash/Shell 命令 | `bash` | +| PowerShell 命令 | `powershell` | +| INI 配置 | `ini` | +| JSON | `json` | +| YAML | `yaml` | +| 纯文本输出/目录树 | `text` | +| Diff | `diff` | +| Python | `python` | + +**判断方法:** +- 看代码块内容判断语言 +- 目录树结构(有 `├──`、`└──` 等)用 `text` +- 终端命令输出(有 `$` 提示符)用 `bash` +- 纯文本日志/输出用 `text` +- 如果实在无法判断,用 `text` + +### 示例 + +修改前: +```` +``` +cmake_minimum_required(VERSION 3.16) +project(CFDesktop) +``` +```` + +修改后: +```` +```cmake +cmake_minimum_required(VERSION 3.16) +project(CFDesktop) +``` +```` + +### 注意 + +- **只修改无标签的代码块**(即开头的 ` ``` ` 后面什么都没有的) +- 已有语言标签的代码块(如 ` ```cpp `、` ```bash `)**不要动** +- 代码块内的内容**不要修改**,只添加语言标签 + +--- + +## 文档约定 + +- 文档语言:中文 + 英文技术术语 +- 项目版本:0.18.0 +- C++ 标准:C++23(不是 C++17) +- Qt 版本:6.8.3 +- CMake 最低版本:3.16 +- 三层架构:base/ → ui/ → desktop/(严格单向依赖) + +## 验证 + +完成所有修改后,运行以下验证: + +```bash +# 检查是否还有无标签的代码块(应该大幅减少) +grep -rn '^```$' document/ --include='*.md' | grep -v 'document/api/' | wc -l + +# 检查所有非 api 目录的 .md 文件是否都有 frontmatter +find document/ -name '*.md' -not -path 'document/api/*' | while read f; do + head -1 "$f" | grep -q '^---$' || echo "MISSING FRONTMATTER: $f" +done + +# 检查文档站能否正常构建 +cd /home/charliechen/CFDesktop && pnpm build +``` diff --git a/README.md b/README.md index 83292769b..d701455f5 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ | 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 | 测试体系 | 55% | Google Test 集成,base/logger/ui基础有覆盖 | +| 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个) | @@ -69,7 +69,7 @@ | Phase 6 | P2 控件 | 27个高级控件 (DatePicker, MenuBar, Dialog, etc.) | | Phase 6 | P3 控件 | 25个专业控件 (SplitView, ChartView, etc.) | | Phase 5 | 测试完善 | desktop 模块、性能基准、UI 自动化 | -| 文档 | API/示例补充 | 约40%文档缺失,50%示例缺失 | +| 文档 | VitePress 重排 | 已迁移到 VitePress,API 自动文档二期处理 | ### 快速统计 (2026-03-27 更新) @@ -79,10 +79,10 @@ | UI 控件 (P2+P3) | 0% (52个) | 待开发 | | 文档覆盖 | 60% | ~268个文档 | | 示例覆盖 | 50% | ~80个示例 | -| 测试覆盖 | 55% | Base/Logger良好,UI控件缺失 | +| 测试覆盖 | 65% | CTest 47 项通过,P0/P1 Widget 已有基础测试 | -📋 **完整待办清单**: [TODO.md](TODO.md) -📊 **详细状态报告**: [document/todo/done/PROJECT_STATUS_REPORT.md](document/todo/done/PROJECT_STATUS_REPORT.md) +📋 **完整待办清单**: [document/todo/](document/todo/) +📊 **当前状态**: [document/status/current.md](document/status/current.md) --- @@ -150,7 +150,7 @@ CFDesktop 通过 `IDisplayServerBackend` 接口抽象了三种显示模式,使 - **框架**: Qt 6.8.3 - **构建**: CMake 3.16+, Ninja - **测试**: Google Test -- **文档**: MkDocs + Doxygen +- **文档**: VitePress + Doxygen --- @@ -198,14 +198,22 @@ cd CFDesktop 📚 **项目文档站**: [https://awesome-embedded-learning-studio.github.io/CFDesktop/](https://awesome-embedded-learning-studio.github.io/CFDesktop/) +本地预览文档: + +```bash +pnpm install +pnpm dev +pnpm build +``` + ### 开发文档 | 文档 | 说明 | 链接 | |:---|:---|:---| | 开发环境设置 | 前置要求、快速开始、构建系统 | [development/](https://awesome-embedded-learning-studio.github.io/CFDesktop/development/) | -| API 参考手册 | 基础库 API 文档 | [HandBook/api/](https://awesome-embedded-learning-studio.github.io/CFDesktop/HandBook/api/) | +| 当前状态 | 开发重启事实源 | [status/current](https://awesome-embedded-learning-studio.github.io/CFDesktop/status/current) | | UI 框架文档 | Material Design 实现架构 | [HandBook/ui/](https://awesome-embedded-learning-studio.github.io/CFDesktop/HandBook/ui/) | -| 代码示例 | 各模块使用示例 | [example/](https://awesome-embedded-learning-studio.github.io/CFDesktop/examples/) | +| AI 辅助开发 | 通用 AI 协作指南 | [development/ai-assistant-guide](https://awesome-embedded-learning-studio.github.io/CFDesktop/development/ai-assistant-guide) | ### 设计文档 @@ -217,9 +225,9 @@ cd CFDesktop ### TODO 跟踪 -- [**待办清单**](TODO.md) - 按优先级分类的待办事项 (P0-P3) +- [**待办清单**](document/todo/) - 按优先级分类的待办事项 - [任务看板](document/todo/README.md) - 当前开发任务列表 -- [状态报告](document/todo/done/PROJECT_STATUS_REPORT.md) - 项目整体进度报告 +- [当前状态](document/status/current.md) - 项目重启事实源 --- diff --git a/cmake/meta_info/desktop_settings.template.h.in b/cmake/meta_info/desktop_settings.template.h.in index ffdd3176c..bace44470 100644 --- a/cmake/meta_info/desktop_settings.template.h.in +++ b/cmake/meta_info/desktop_settings.template.h.in @@ -20,8 +20,17 @@ static constexpr const char* EARLY_SETTINGS_TEMPLATE = R"({ "dirent": "logger/" }, "desktop": { - "root": "@CFDESKTOP_DEFAULT_ROOT@" + "root": "@CFDESKTOP_DEFAULT_ROOT@", + "config_folder": "settings/desktop/" } } )"; + +/// @brief Default wallpaper config template written to /wallpaper.json. +static constexpr const char* WALLPAPER_CONFIG_TEMPLATE = R"({ + "source_path": "", + "scaling": "fill", + "background_color": "#1c1b1f" +} +)"; } // namespace cf::desktop::early_stage diff --git a/desktop/base/CMakeLists.txt b/desktop/base/CMakeLists.txt index 001176c55..8245e3286 100644 --- a/desktop/base/CMakeLists.txt +++ b/desktop/base/CMakeLists.txt @@ -1,3 +1,5 @@ +add_subdirectory(fundamental) +add_subdirectory(path) add_subdirectory(logger) add_subdirectory(file_operations) add_subdirectory(config_manager) diff --git a/desktop/base/config_manager/CMakeLists.txt b/desktop/base/config_manager/CMakeLists.txt index ce6f0becd..4b30312af 100644 --- a/desktop/base/config_manager/CMakeLists.txt +++ b/desktop/base/config_manager/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(cfconfig PRIVATE src/cfconfig.cpp src/cfconfig_path_provider.cpp src/impl/config_impl.cpp + src/impl/config_domain.cpp src/impl/config_key.cpp src/impl/config_backend_factory.cpp src/impl/qsettings_backend.cpp diff --git a/desktop/base/config_manager/include/cfconfig.hpp b/desktop/base/config_manager/include/cfconfig.hpp index f07ea2ac7..c30d87264 100644 --- a/desktop/base/config_manager/include/cfconfig.hpp +++ b/desktop/base/config_manager/include/cfconfig.hpp @@ -12,6 +12,7 @@ #pragma once #include "base/singleton/simple_singleton.hpp" +#include "cfconfig/cfconfig_domain_handle.h" #include "cfconfig/cfconfig_result.h" #include "cfconfig/cfconfig_watcher.h" #include "cfconfig_key.h" @@ -81,6 +82,23 @@ class ConfigStore : public SimpleSingleton { */ void initialize(std::shared_ptr path_provider); + /** + * @brief Get a handle to a named config domain. + * + * Each domain has its own set of backend files per layer. + * The "default" domain is used by all non-domain operations. + * Domains are lazily created on first access. + * + * @param[in] name Domain name (e.g., "wallpaper", "ui", "network"). + * @return ConfigDomainHandle scoped to the named domain. + * + * @code + * auto wallpaper = ConfigStore::instance().domain("wallpaper"); + * wallpaper.set(KeyView{.group="source", .key="path"}, std::string("/pics/bg.jpg")); + * @endcode + */ + ConfigDomainHandle domain(const std::string& name); + /* ========== Query operations ========== */ /** @@ -421,3 +439,6 @@ RegisterResult ConfigStore::register_key(const Key& key, const Value& init_value } } // namespace cf::config + +// Include ConfigDomainHandle template implementations (needs detail::any_cast above) +#include "impl/config_domain_handle_impl.h" diff --git a/desktop/base/config_manager/include/cfconfig/cfconfig_domain_handle.h b/desktop/base/config_manager/include/cfconfig/cfconfig_domain_handle.h new file mode 100644 index 000000000..ba355fe21 --- /dev/null +++ b/desktop/base/config_manager/include/cfconfig/cfconfig_domain_handle.h @@ -0,0 +1,165 @@ +/** + * @file cfconfig_domain_handle.h + * @brief Public handle for a named config domain. + * + * Provides the same query/set/watch API as ConfigStore but scoped to + * a single named domain. Obtained via ConfigStore::domain("name"). + * + * @date 2026-04-12 + * @version 1.0 + */ + +#pragma once + +#include "cfconfig/cfconfig_result.h" +#include "cfconfig/cfconfig_watcher.h" +#include "cfconfig_key.h" +#include "cfconfig_layer.h" +#include "cfconfig_notify_policy.h" +#include +#include +#include +#include + +namespace cf::config { + +class ConfigStoreImpl; + +/** + * @brief Handle to a named config domain. + * + * Mirrors the ConfigStore API surface but scoped to one domain. + * Obtain via ConfigStore::domain("name"). + * + * @note Lightweight: holds only a shared_ptr to the impl and a domain name. + * @note Thread-safe: operations are thread-safe via the underlying ConfigDomain. + * + * @code + * auto wallpaper = ConfigStore::instance().domain("wallpaper"); + * wallpaper.set(KeyView{.group="source", .key="path"}, std::string("/pics/bg.jpg"), Layer::App); + * auto path = wallpaper.query(KeyView{.group="source", .key="path"}, ""); + * @endcode + */ +class ConfigDomainHandle { + public: + /* ========== Query operations ========== */ + + template [[nodiscard]] std::optional query(const KeyView key); + + template + [[nodiscard]] Value query(const KeyView key, const Value& default_value); + + template + [[nodiscard]] std::optional query(const KeyView key, Layer layer); + + template + [[nodiscard]] Value query(const KeyView key, Layer layer, const Value& default_value); + + [[nodiscard]] bool has_key(const KeyView key); + [[nodiscard]] bool has_key(const KeyView key, Layer layer); + + /* ========== Write operations ========== */ + + template + [[nodiscard]] bool set(const KeyView key, const Value& v, Layer layer = Layer::App, + NotifyPolicy notify_policy = NotifyPolicy::Immediate); + + template + [[nodiscard]] RegisterResult register_key(const Key& key, const Value& init_value, + Layer layer = Layer::App, + NotifyPolicy notify_policy = NotifyPolicy::Immediate); + + [[nodiscard]] UnRegisterResult + unregister_key(const Key& key, Layer layer = Layer::App, + NotifyPolicy notify_policy = NotifyPolicy::Immediate); + + /** + * @brief Clears all configuration values in this domain. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + void clear(); + /** + * @brief Clears configuration values in the specified layer. + * @param[in] layer The configuration layer to clear. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + void clear_layer(Layer layer); + + /* ========== Watcher operations ========== */ + + /** + * @brief Registers a watcher for keys matching a pattern. + * @param[in] key_pattern Glob-like pattern to match key names. + * @param[in] callback Callback invoked on matching key changes. + * @param[in] policy Notification policy for the watcher. + * @return Handle identifying this watcher registration. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + WatcherHandle watch(const std::string& key_pattern, Watcher callback, + NotifyPolicy policy = NotifyPolicy::Immediate); + /** + * @brief Removes a previously registered watcher. + * @param[in] handle Handle returned by watch(). + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + void unwatch(WatcherHandle handle); + [[nodiscard]] NotifyResult notify(); + [[nodiscard]] std::size_t pending_changes() const; + + /* ========== Persistence operations ========== */ + + /** + * @brief Persists configuration changes to disk asynchronously. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + void sync(); + /** + * @brief Reloads configuration values from disk. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + void reload(); + + /** + * @brief Gets the domain name this handle is bound to. + * @return Reference to the domain name string. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + const std::string& domain_name() const; + + private: + friend class ConfigStore; + ConfigDomainHandle(std::shared_ptr impl, const std::string& domain_name); + + std::shared_ptr impl_; + std::string domain_name_; +}; + +} // namespace cf::config 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 491ee2e01..6fda4b58c 100644 --- a/desktop/base/config_manager/include/cfconfig/cfconfig_path_provider.h +++ b/desktop/base/config_manager/include/cfconfig/cfconfig_path_provider.h @@ -109,6 +109,24 @@ class IConfigStorePathProvider { * @ingroup none */ virtual bool is_layer_enabled(int layer_index) const = 0; + + /** + * @brief Get the full file path for a named domain in a specific layer. + * + * Used by non-default config domains to resolve their file paths. + * Return empty QString to disable that layer for the domain. + * + * @param[in] layer_index 0=System, 1=User, 2=App (Temp is memory-only). + * @param[in] domain_name Name of the config domain. + * @return Full file path for the domain's config file in that layer. + * @throws None + * @since 1.1 + */ + virtual QString domain_path(int layer_index, const QString& domain_name) const { + (void)layer_index; + (void)domain_name; + return QString(); + } }; /** @@ -235,6 +253,19 @@ class DesktopConfigStorePathProvider : public IConfigStorePathProvider { */ bool is_layer_enabled(int layer_index) const override; + /** + * @brief Get file path for a named domain in a specific layer. + * + * For "default" domain, returns the original single-file paths. + * For other domains, returns {base_dir}/{domain_name}.ini per layer. + * + * @param[in] layer_index 0=System, 1=User, 2=App. + * @param[in] domain_name Name of the config domain. + * @return Full file path for the domain. + * @since 1.1 + */ + QString domain_path(int layer_index, const QString& domain_name) const override; + private: QString system_path_; QString user_dir_; diff --git a/desktop/base/config_manager/include/impl/config_domain.h b/desktop/base/config_manager/include/impl/config_domain.h new file mode 100644 index 000000000..10a8f9a0e --- /dev/null +++ b/desktop/base/config_manager/include/impl/config_domain.h @@ -0,0 +1,358 @@ +/** + * @file desktop/base/config_manager/include/impl/config_domain.h + * @brief Per-domain configuration storage engine. + * + * Each ConfigDomain owns its own backends, cache, watchers, and dirty flags, + * providing complete isolation between configuration domains. + * + * @author N/A + * @date 2026-04-12 + * @version 1.0 + * @since N/A + * @ingroup none + */ + +#pragma once + +#include "cfconfig/cfconfig_path_provider.h" +#include "cfconfig/cfconfig_result.h" +#include "cfconfig/cfconfig_watcher.h" +#include "cfconfig_key.h" +#include "cfconfig_layer.h" +#include "cfconfig_notify_policy.h" +#include "impl/config_backend.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cf::config { + +/** + * @brief Watcher entry for pattern matching. + */ +struct WatcherEntry { + std::string pattern; + Watcher callback; + NotifyPolicy policy; + WatcherHandle handle; +}; + +/** + * @brief Pending change for manual notification. + */ +struct PendingChange { + Key key; + std::any old_value; + std::any new_value; + Layer from_layer; +}; + +/** + * @brief Deferred watcher event for callback execution after lock release. + */ +struct DeferredWatcherEvent { + Watcher callback; + Key key; + std::any old_value; + std::any new_value; + Layer from_layer; + bool has_old_value; + bool has_new_value; +}; + +/** + * @brief Per-domain configuration storage engine. + * + * Manages four-layer configuration storage, caching, watchers, + * and persistence operations for a single named domain. + * + * @note Thread-safe for all operations. + * @note Not part of the public API. + * + * @since N/A + * @ingroup none + */ +class ConfigDomain { + public: + /** + * @brief Construct with path provider and domain name. + * + * For domain "default", uses the original path provider methods + * (system_path, user_dir/filename, app_dir/filename). + * For other domains, uses domain_path() from the provider. + * + * @param[in] path_provider Path provider for config file locations. + * @param[in] domain_name Name of this domain. + */ + ConfigDomain(std::shared_ptr path_provider, + const std::string& domain_name); + + ~ConfigDomain(); + + ConfigDomain(const ConfigDomain&) = delete; + ConfigDomain& operator=(const ConfigDomain&) = delete; + ConfigDomain(ConfigDomain&&) = delete; + ConfigDomain& operator=(ConfigDomain&&) = delete; + + /* ========== Query operations ========== */ + + /** + * @brief Queries a configuration value by key with a fallback. + * @param[in] key The configuration key to look up. + * @param[in] default_value Value returned if the key is absent. + * @return The stored value, or default_value if not found. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + std::any query(const std::string& key, const std::any& default_value); + + /** + * @brief Queries a configuration value from a specific layer. + * @param[in] key The configuration key to look up. + * @param[in] layer The configuration layer to query. + * @return The stored value, or empty std::any if not found. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + std::any query(const std::string& key, Layer layer); + + /** + * @brief Checks whether a key exists across all layers. + * @param[in] key The configuration key to check. + * @return true if the key exists, false otherwise. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + bool has_key(const std::string& key); + + /** + * @brief Checks whether a key exists in a specific layer. + * @param[in] key The configuration key to check. + * @param[in] layer The configuration layer to check. + * @return true if the key exists in the layer, false otherwise. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + bool has_key(const std::string& key, Layer layer); + + /* ========== Write operations ========== */ + + /** + * @brief Sets a configuration value in the specified layer. + * @param[in] key The configuration key to set. + * @param[in] value The value to store. + * @param[in] layer The target configuration layer. + * @param[in] notify_policy How and when to notify watchers. + * @return true if the value was set successfully. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + bool set(const std::string& key, const std::any& value, Layer layer, + NotifyPolicy notify_policy); + + /** + * @brief Registers a new configuration key with an initial value. + * @param[in] key The key descriptor to register. + * @param[in] init_value The initial value for the key. + * @param[in] layer The layer to store the initial value in. + * @param[in] notify_policy How and when to notify watchers. + * @return Result indicating success or the reason for failure. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + RegisterResult register_key(const Key& key, const std::any& init_value, Layer layer, + NotifyPolicy notify_policy); + + /** + * @brief Unregisters a previously registered configuration key. + * @param[in] key The key descriptor to unregister. + * @param[in] layer The layer to remove the key from. + * @param[in] notify_policy How and when to notify watchers. + * @return Result indicating success or the reason for failure. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + UnRegisterResult unregister_key(const Key& key, Layer layer, NotifyPolicy notify_policy); + + /** + * @brief Clears all configuration values across all layers. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + void clear(); + + /** + * @brief Clears all configuration values in the specified layer. + * @param[in] layer The configuration layer to clear. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + void clear_layer(Layer layer); + + /* ========== Watcher operations ========== */ + + /** + * @brief Registers a watcher for keys matching a pattern. + * @param[in] pattern Glob-like pattern to match key names. + * @param[in] callback Callback invoked on matching key changes. + * @param[in] policy Notification policy for the watcher. + * @return Handle identifying this watcher registration. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + WatcherHandle watch(const std::string& pattern, Watcher callback, NotifyPolicy policy); + + /** + * @brief Removes a previously registered watcher. + * @param[in] handle Handle returned by watch(). + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + void unwatch(WatcherHandle handle); + + /** + * @brief Manually triggers pending watcher notifications. + * @return Result indicating how many watchers were notified. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + NotifyResult notify(); + + /** + * @brief Returns the number of pending, unnotified changes. + * @return Count of pending changes. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + std::size_t pending_changes() const; + + /* ========== Persistence operations ========== */ + + /** + * @brief Persists dirty layers to disk. + * @param[in] async If true, performs the write asynchronously. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + void sync(bool async); + + /** + * @brief Reloads all layers from their persistent storage. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + void reload(); + + /** + * @brief Gets the domain name. + * @return Reference to the domain name string. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + const std::string& domain_name() const { return domain_name_; } + + private: + /** + * @brief Tests whether a key matches a glob-like pattern. + * @param[in] pattern The glob pattern to test against. + * @param[in] key The key string to test. + * @return true if the key matches the pattern. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + static bool match_pattern(const std::string& pattern, const std::string& key); + IConfigBackend* get_backend(Layer layer); + void mark_dirty(Layer layer); + static QVariant anyToQVariant(const std::any& value); + + bool set_impl(const std::string& key, const std::any& value, Layer layer, + NotifyPolicy notify_policy); + RegisterResult register_key_impl(const Key& key, const std::any& init_value, Layer layer, + NotifyPolicy notify_policy); + UnRegisterResult unregister_key_impl(const Key& key, Layer layer, NotifyPolicy notify_policy); + void clear_layer_impl(Layer layer); + void clear_impl(); + + void trigger_watchers(const Key& key, const std::any* old_value, const std::any* new_value, + Layer layer); + void execute_deferred_watchers(); + + private: + mutable std::shared_mutex mutex_; + std::mutex deferred_mutex_; + + std::shared_ptr path_provider_; + std::string domain_name_; + + std::unordered_map cache_; + + std::unique_ptr settings_system_; + std::unique_ptr settings_user_; + std::unique_ptr settings_app_; + + std::array dirty_flags_{false, false, false, false}; + + std::vector watchers_; + std::atomic next_handle_{1}; + + std::vector pending_changes_; + std::vector deferred_events_; +}; + +} // namespace cf::config diff --git a/desktop/base/config_manager/include/impl/config_domain_handle_impl.h b/desktop/base/config_manager/include/impl/config_domain_handle_impl.h new file mode 100644 index 000000000..295941b4c --- /dev/null +++ b/desktop/base/config_manager/include/impl/config_domain_handle_impl.h @@ -0,0 +1,188 @@ +/** + * @file config_domain_handle_impl.h + * @brief Template method implementations for ConfigDomainHandle. + * + * @date 2026-04-12 + * @version 1.0 + */ + +#pragma once + +#include "cfconfig/cfconfig_domain_handle.h" +#include "cfconfig_key.h" +#include "impl/config_impl.h" +#include +#include + +namespace cf::config { + +// Forward declaration of the any_cast helper in cfconfig.hpp +namespace detail { +/** + * @brief Extracts a typed value from std::any with a fallback. + * + * Attempts to cast the stored value to type T. Returns the default + * if the stored type does not match. + * + * @tparam T The target type to extract. + * @param[in] value The std::any value to cast. + * @param[in] default_value Fallback returned when cast fails. + * @return The extracted value, or default_value on failure. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ +template T any_cast(const std::any& value, const T& default_value); +} // namespace detail + +inline ConfigDomainHandle::ConfigDomainHandle(std::shared_ptr impl, + const std::string& domain_name) + : impl_(std::move(impl)), domain_name_(domain_name) {} + +inline const std::string& ConfigDomainHandle::domain_name() const { + return domain_name_; +} + +inline void ConfigDomainHandle::clear() { + impl_->get_domain(domain_name_)->clear(); +} + +inline void ConfigDomainHandle::clear_layer(Layer layer) { + impl_->get_domain(domain_name_)->clear_layer(layer); +} + +inline WatcherHandle ConfigDomainHandle::watch(const std::string& key_pattern, Watcher callback, + NotifyPolicy policy) { + return impl_->get_domain(domain_name_)->watch(key_pattern, std::move(callback), policy); +} + +inline void ConfigDomainHandle::unwatch(WatcherHandle handle) { + impl_->get_domain(domain_name_)->unwatch(handle); +} + +inline NotifyResult ConfigDomainHandle::notify() { + return impl_->get_domain(domain_name_)->notify(); +} + +inline std::size_t ConfigDomainHandle::pending_changes() const { + return impl_->get_domain(domain_name_)->pending_changes(); +} + +inline void ConfigDomainHandle::sync() { + impl_->get_domain(domain_name_)->sync(true); +} + +inline void ConfigDomainHandle::reload() { + impl_->get_domain(domain_name_)->reload(); +} + +inline bool ConfigDomainHandle::has_key(const KeyView key) { + KeyHelper helper; + Key k; + if (!helper.fromKeyViewToKey(key, k)) { + return false; + } + return impl_->get_domain(domain_name_)->has_key(k.full_key); +} + +inline bool ConfigDomainHandle::has_key(const KeyView key, Layer layer) { + KeyHelper helper; + Key k; + if (!helper.fromKeyViewToKey(key, k)) { + return false; + } + return impl_->get_domain(domain_name_)->has_key(k.full_key, layer); +} + +inline UnRegisterResult ConfigDomainHandle::unregister_key(const Key& key, Layer layer, + NotifyPolicy notify_policy) { + return impl_->get_domain(domain_name_)->unregister_key(key, layer, notify_policy); +} + +// ============================================================================ +// Template implementations +// ============================================================================ + +template std::optional ConfigDomainHandle::query(const KeyView key) { + KeyHelper helper; + Key k; + if (!helper.fromKeyViewToKey(key, k)) { + return std::nullopt; + } + + std::any result = impl_->get_domain(domain_name_)->query(k.full_key, std::any()); + if (result.type() == typeid(void)) { + return std::nullopt; + } + + Value value = detail::any_cast(result, Value{}); + return value; +} + +template +Value ConfigDomainHandle::query(const KeyView key, const Value& default_value) { + KeyHelper helper; + Key k; + if (!helper.fromKeyViewToKey(key, k)) { + return default_value; + } + + std::any result = impl_->get_domain(domain_name_)->query(k.full_key, std::any()); + return detail::any_cast(result, default_value); +} + +template +std::optional ConfigDomainHandle::query(const KeyView key, Layer layer) { + KeyHelper helper; + Key k; + if (!helper.fromKeyViewToKey(key, k)) { + return std::nullopt; + } + + std::any result = impl_->get_domain(domain_name_)->query(k.full_key, layer); + if (result.type() == typeid(void)) { + return std::nullopt; + } + + Value value = detail::any_cast(result, Value{}); + return value; +} + +template +Value ConfigDomainHandle::query(const KeyView key, Layer layer, const Value& default_value) { + KeyHelper helper; + Key k; + if (!helper.fromKeyViewToKey(key, k)) { + return default_value; + } + + std::any result = impl_->get_domain(domain_name_)->query(k.full_key, layer); + if (result.type() == typeid(void)) { + return default_value; + } + + return detail::any_cast(result, default_value); +} + +template bool ConfigDomainHandle::set(const KeyView key, const Value& v, + Layer layer, NotifyPolicy notify_policy) { + KeyHelper helper; + Key k; + if (!helper.fromKeyViewToKey(key, k)) { + return false; + } + + std::any value = v; + return impl_->get_domain(domain_name_)->set(k.full_key, value, layer, notify_policy); +} + +template +RegisterResult ConfigDomainHandle::register_key(const Key& key, const Value& init_value, + Layer layer, NotifyPolicy notify_policy) { + std::any value = init_value; + return impl_->get_domain(domain_name_)->register_key(key, value, layer, notify_policy); +} + +} // namespace cf::config diff --git a/desktop/base/config_manager/include/impl/config_impl.h b/desktop/base/config_manager/include/impl/config_impl.h index fa9851f0f..6a0042e5b 100644 --- a/desktop/base/config_manager/include/impl/config_impl.h +++ b/desktop/base/config_manager/include/impl/config_impl.h @@ -1,12 +1,17 @@ /** - * @file config_impl.h - * @brief Internal implementation of ConfigStore (Pimpl). + * @file desktop/base/config_manager/include/impl/config_impl.h + * @brief Internal implementation of ConfigStore (Pimpl). * - * Provides the ConfigStoreImpl class that manages configuration storage, - * caching, layer management, and watcher notifications. + * ConfigStoreImpl acts as a multi-domain container. Each domain is a + * ConfigDomain instance with its own backends, cache, and watchers. + * Existing ConfigStore methods delegate to the "default" domain for + * backward compatibility. * - * @date 2026-03-17 - * @version 1.0 + * @author N/A + * @date 2026-03-17 + * @version 2.0 + * @since N/A + * @ingroup none */ #pragma once @@ -18,6 +23,7 @@ #include "cfconfig_layer.h" #include "cfconfig_notify_policy.h" #include "impl/config_backend.h" +#include "impl/config_domain.h" #include #include #include @@ -30,299 +36,273 @@ namespace cf::config { /** - * @brief Watcher entry for pattern matching. - */ -struct WatcherEntry { - std::string pattern; ///< Pattern to match (supports * wildcard) - Watcher callback; ///< Callback function - NotifyPolicy policy; ///< When to trigger this watcher - WatcherHandle handle; ///< Unique handle for this watcher -}; - -/** - * @brief Pending change for manual notification. - */ -struct PendingChange { - Key key; - std::any old_value; - std::any new_value; - Layer from_layer; -}; - -/** - * @brief Deferred watcher event for callback execution after lock release. - */ -struct DeferredWatcherEvent { - Watcher callback; - Key key; - std::any old_value; - std::any new_value; - Layer from_layer; - bool has_old_value; - bool has_new_value; -}; - -/** - * @brief Internal implementation of ConfigStore. + * @brief Internal implementation of ConfigStore. + * + * Manages multiple named ConfigDomain instances and dispatches + * all operations to the appropriate domain. * - * Manages four-layer configuration storage, caching, watchers, - * and persistence operations. + * @note Thread-safe for all operations. + * @note Not part of the public API. * - * @note Thread-safe for all operations. - * @note Not part of the public API. + * @since N/A + * @ingroup none */ class ConfigStoreImpl { public: /** - * @brief Constructs a new ConfigStoreImpl with default path provider. - * - * Initializes backends for each layer using DesktopConfigStorePathProvider: - * - System: /etc/cfdesktop/system.ini (read-write, may not exist) - * - User: ~/.config/cfdesktop/user.ini (read-write, created if needed) - * - App: config/app.ini (read-write, relative path) - * - Temp: Memory only + * @brief Default-constructs a ConfigStoreImpl. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none */ ConfigStoreImpl(); - /** - * @brief Constructs a new ConfigStoreImpl with custom path provider. - * - * @param[in] path_provider Custom path provider for config file locations. - * Allows tests and other projects to customize paths. + * @brief Constructs with a custom path provider. + * @param[in] path_provider Path provider for config file locations. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none */ explicit ConfigStoreImpl(std::shared_ptr path_provider); - /** - * @brief Destroys the ConfigStoreImpl. - * - * Syncs all dirty layers before destruction. + * @brief Destructs the ConfigStoreImpl and releases all domains. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none */ ~ConfigStoreImpl(); - // Delete copy and move ConfigStoreImpl(const ConfigStoreImpl&) = delete; ConfigStoreImpl& operator=(const ConfigStoreImpl&) = delete; ConfigStoreImpl(ConfigStoreImpl&&) = delete; ConfigStoreImpl& operator=(ConfigStoreImpl&&) = delete; - /* ========== Query operations ========== */ + /* ========== Domain management ========== */ /** - * @brief Query a configuration value with a fallback default. + * @brief Get or lazily create a named domain. * - * Searches all layers (merged view) for the given key and returns - * the first match found. If the key does not exist in any layer, - * @p default_value is returned instead. + * Thread-safe. Uses double-checked locking for fast path. * + * @param[in] name Domain name. + * @return Pointer to the domain (never null after creation). + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + ConfigDomain* get_domain(const std::string& name); + + /** + * @brief Get the default domain (for backward-compat fast path). + * @return Pointer to the default domain. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none + */ + ConfigDomain* default_domain() const; + + /* ========== Delegated operations (default domain) ========== */ + + /** + * @brief Queries a configuration value by key with a fallback. * @param[in] key The configuration key to look up. - * @param[in] default_value The value to return if the key is not found. - * - * @return The queried value, or @p default_value if the key is absent. + * @param[in] default_value Value returned if the key is absent. + * @return The stored value, or default_value if not found. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none */ std::any query(const std::string& key, const std::any& default_value); /** - * @brief Query a configuration value from a specific layer. - * + * @brief Queries a configuration value from a specific layer. * @param[in] key The configuration key to look up. - * @param[in] layer The layer to search. - * - * @return The value stored in the specified layer. + * @param[in] layer The configuration layer to query. + * @return The stored value, or empty std::any if not found. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none */ std::any query(const std::string& key, Layer layer); /** - * @brief Check whether a key exists in any layer. - * + * @brief Checks whether a key exists across all layers. * @param[in] key The configuration key to check. - * - * @return True if the key exists in at least one layer, false otherwise. + * @return true if the key exists, false otherwise. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none */ bool has_key(const std::string& key); /** - * @brief Check whether a key exists in a specific layer. - * + * @brief Checks whether a key exists in a specific layer. * @param[in] key The configuration key to check. - * @param[in] layer The layer to search. - * - * @return True if the key exists in the specified layer, false otherwise. + * @param[in] layer The configuration layer to check. + * @return true if the key exists in the layer, false otherwise. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none */ bool has_key(const std::string& key, Layer layer); - /* ========== Write operations ========== */ - /** - * @brief Set a key-value pair in the specified layer. - * + * @brief Sets a configuration value in the specified layer. * @param[in] key The configuration key to set. * @param[in] value The value to store. - * @param[in] layer The target layer. - * @param[in] notify_policy Controls when watchers are notified. - * - * @return True if the value was set successfully, false otherwise. + * @param[in] layer The target configuration layer. + * @param[in] notify_policy How and when to notify watchers. + * @return true if the value was set successfully. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none */ bool set(const std::string& key, const std::any& value, Layer layer, NotifyPolicy notify_policy); /** - * @brief Register a new key with an initial value in a layer. - * + * @brief Registers a new configuration key with an initial value. * @param[in] key The key descriptor to register. * @param[in] init_value The initial value for the key. - * @param[in] layer The target layer. - * @param[in] notify_policy Controls when watchers are notified. - * - * @return The result of the registration attempt. + * @param[in] layer The layer to store the initial value in. + * @param[in] notify_policy How and when to notify watchers. + * @return Result indicating success or the reason for failure. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none */ RegisterResult register_key(const Key& key, const std::any& init_value, Layer layer, NotifyPolicy notify_policy); /** - * @brief Unregister a key from a layer. - * + * @brief Unregisters a previously registered configuration key. * @param[in] key The key descriptor to unregister. * @param[in] layer The layer to remove the key from. - * @param[in] notify_policy Controls when watchers are notified. - * - * @return The result of the unregistration attempt. + * @param[in] notify_policy How and when to notify watchers. + * @return Result indicating success or the reason for failure. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none */ UnRegisterResult unregister_key(const Key& key, Layer layer, NotifyPolicy notify_policy); /** - * @brief Clear all layers and cache. + * @brief Clears all configuration values across all layers. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none */ void clear(); /** - * @brief Clear a specific layer. - * - * @param[in] layer The layer to clear. + * @brief Clears all configuration values in the specified layer. + * @param[in] layer The configuration layer to clear. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none */ void clear_layer(Layer layer); - /* ========== Watcher operations ========== */ - /** - * @brief Register a watcher callback for keys matching a pattern. - * - * @param[in] pattern The key pattern to watch (supports * wildcard). - * @param[in] callback The function to call when a matching key changes. - * @param[in] policy When to trigger this watcher. - * - * @return A handle that can be used to remove the watcher later. + * @brief Registers a watcher for keys matching a pattern. + * @param[in] pattern Glob-like pattern to match key names. + * @param[in] callback Callback invoked on matching key changes. + * @param[in] policy Notification policy for the watcher. + * @return Handle identifying this watcher registration. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none */ WatcherHandle watch(const std::string& pattern, Watcher callback, NotifyPolicy policy); /** - * @brief Remove a previously registered watcher. - * - * @param[in] handle The watcher handle returned by watch(). + * @brief Removes a previously registered watcher. + * @param[in] handle Handle returned by watch(). + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none */ void unwatch(WatcherHandle handle); /** - * @brief Manually trigger all pending watcher notifications. - * - * @return The result of the notification dispatch. + * @brief Manually triggers pending watcher notifications. + * @return Result indicating how many watchers were notified. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none */ NotifyResult notify(); /** - * @brief Get the number of pending changes awaiting notification. - * - * @return The count of pending changes. + * @brief Returns the number of pending, unnotified changes. + * @return Count of pending changes. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none */ std::size_t pending_changes() const; - /* ========== Persistence operations ========== */ - /** - * @brief Persist all dirty layers to disk. - * - * @param[in] async If true, perform the write asynchronously; otherwise - * block until complete. + * @brief Persists dirty layers to disk. + * @param[in] async If true, performs the write asynchronously. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none */ void sync(bool async); /** - * @brief Re-read all layers from disk, discarding in-memory changes. + * @brief Reloads all layers from their persistent storage. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup none */ void reload(); private: - /** - * @brief Match a key against a pattern. - * - * Supports * wildcard matching. - */ - static bool match_pattern(const std::string& pattern, const std::string& key); - - /** - * @brief Get backend for a layer. - * - * Returns nullptr for Temp layer (memory only). - */ - IConfigBackend* get_backend(Layer layer); - - /** - * @brief Mark a layer as dirty (needs syncing). - */ - void mark_dirty(Layer layer); - - /** - * @brief Convert std::any to QVariant for backend storage. - */ - static QVariant anyToQVariant(const std::any& value); - - /* ========== Internal lock-free implementations ========== */ - bool set_impl(const std::string& key, const std::any& value, Layer layer, - NotifyPolicy notify_policy); - RegisterResult register_key_impl(const Key& key, const std::any& init_value, Layer layer, - NotifyPolicy notify_policy); - UnRegisterResult unregister_key_impl(const Key& key, Layer layer, NotifyPolicy notify_policy); - void clear_layer_impl(Layer layer); - void clear_impl(); - - /** - * @brief Trigger watchers for a key change (called with lock held). - */ - void trigger_watchers(const Key& key, const std::any* old_value, const std::any* new_value, - Layer layer); - - /** - * @brief Execute deferred watcher callbacks (called without lock). - */ - void execute_deferred_watchers(); - - private: - // Thread safety mutable std::shared_mutex mutex_; - std::mutex deferred_mutex_; ///< Mutex for deferred watcher events - - // Path provider for config file locations std::shared_ptr path_provider_; - - // Cache for all layers (Temp is cache-only) - std::unordered_map cache_; - - // Layer storage (format-agnostic backends) - std::unique_ptr settings_system_; - std::unique_ptr settings_user_; - std::unique_ptr settings_app_; - - // Dirty flags for each layer - std::array dirty_flags_{false, false, false, false}; - - // Watchers - std::vector watchers_; - std::atomic next_handle_{1}; - - // Pending changes for Manual notification - std::vector pending_changes_; - - // Deferred watcher events (executed after lock release) - std::vector deferred_events_; + std::unordered_map> domains_; }; } // namespace cf::config diff --git a/desktop/base/config_manager/src/cfconfig.cpp b/desktop/base/config_manager/src/cfconfig.cpp index 22fee2a95..b472cbbe2 100644 --- a/desktop/base/config_manager/src/cfconfig.cpp +++ b/desktop/base/config_manager/src/cfconfig.cpp @@ -106,4 +106,8 @@ void ConfigStore::reload() { impl->reload(); } +ConfigDomainHandle ConfigStore::domain(const std::string& name) { + return ConfigDomainHandle(impl, name); +} + } // namespace cf::config diff --git a/desktop/base/config_manager/src/cfconfig_path_provider.cpp b/desktop/base/config_manager/src/cfconfig_path_provider.cpp index 7ded64e54..9dc484302 100644 --- a/desktop/base/config_manager/src/cfconfig_path_provider.cpp +++ b/desktop/base/config_manager/src/cfconfig_path_provider.cpp @@ -57,4 +57,33 @@ bool DesktopConfigStorePathProvider::is_layer_enabled(int layer_index) const { return true; } +QString DesktopConfigStorePathProvider::domain_path(int layer_index, + const QString& domain_name) const { + if (domain_name == "default") { + switch (layer_index) { + case 0: + return system_path_; + case 1: + return user_dir_ + "/" + user_filename_; + case 2: + return app_dir_ + "/" + app_filename_; + } + return QString(); + } + + // Named domain: one file per domain in each layer's directory + QString filename = domain_name + ".json"; + switch (layer_index) { + case 0: { + QString dir = QFileInfo(system_path_).absolutePath(); + return dir + "/" + filename; + } + case 1: + return user_dir_ + "/" + filename; + case 2: + return app_dir_ + "/" + filename; + } + return QString(); +} + } // namespace cf::config diff --git a/desktop/base/config_manager/src/impl/config_domain.cpp b/desktop/base/config_manager/src/impl/config_domain.cpp new file mode 100644 index 000000000..1902d8cf1 --- /dev/null +++ b/desktop/base/config_manager/src/impl/config_domain.cpp @@ -0,0 +1,571 @@ +/** + * @file config_domain.cpp + * @brief Per-domain configuration storage engine implementation. + * + * @date 2026-04-12 + * @version 1.0 + */ + +#include "impl/config_domain.h" +#include "cfconfig/cfconfig_path_provider.h" +#include "cfconfig/cfconfig_result.h" +#include "cfconfig_key.h" +#include "impl/config_backend_factory.h" +#include +#include +#include +#include +#include +#include +#include + +namespace cf::config { + +namespace detail { + +bool match_pattern(const std::string& pattern, const std::string& key) { + std::string regex_pattern = pattern; + size_t pos = 0; + while ((pos = regex_pattern.find_first_of("^.${}+|()[]\\", pos)) != std::string::npos) { + regex_pattern.insert(pos, "\\"); + pos += 2; + } + pos = 0; + while ((pos = regex_pattern.find('*', pos)) != std::string::npos) { + regex_pattern.replace(pos, 1, ".*"); + pos += 2; + } + regex_pattern = "^" + regex_pattern + "$"; + + try { + std::regex re(regex_pattern); + return std::regex_match(key, re); + } catch (const std::regex_error&) { + return false; + } +} + +} // namespace detail + +// ============================================================================ +// Constructor / Destructor +// ============================================================================ + +ConfigDomain::ConfigDomain(std::shared_ptr path_provider, + const std::string& domain_name) + : path_provider_(std::move(path_provider)), domain_name_(domain_name) { + + QString domain_qname = QString::fromStdString(domain_name_); + + if (domain_name == "default") { + // Backward-compatible: use the original path provider methods + QString systemPath = path_provider_->system_path(); + if (!systemPath.isEmpty() && QFileInfo::exists(systemPath)) { + settings_system_ = createBackend(systemPath); + } + + QString userDir = path_provider_->user_dir(); + if (!userDir.isEmpty()) { + QDir().mkpath(userDir); + QString userFilePath = userDir + "/" + path_provider_->user_filename(); + settings_user_ = createBackend(userFilePath); + } + + QString appDir = path_provider_->app_dir(); + if (!appDir.isEmpty()) { + QDir().mkpath(appDir); + QString appFilePath = appDir + "/" + path_provider_->app_filename(); + settings_app_ = createBackend(appFilePath); + } + } else { + // Named domain: use domain_path() from provider + // System layer (index 0) + QString sysPath = path_provider_->domain_path(0, domain_qname); + if (!sysPath.isEmpty() && QFileInfo::exists(sysPath)) { + settings_system_ = createBackend(sysPath); + } + + // User layer (index 1) + QString userPath = path_provider_->domain_path(1, domain_qname); + if (!userPath.isEmpty()) { + QDir().mkpath(QFileInfo(userPath).absolutePath()); + settings_user_ = createBackend(userPath); + } + + // App layer (index 2) + QString appPath = path_provider_->domain_path(2, domain_qname); + if (!appPath.isEmpty()) { + QDir().mkpath(QFileInfo(appPath).absolutePath()); + settings_app_ = createBackend(appPath); + } + } +} + +ConfigDomain::~ConfigDomain() { + sync(false); +} + +// ============================================================================ +// Query operations +// ============================================================================ + +std::any ConfigDomain::query(const std::string& key, const std::any& default_value) { + std::shared_lock lock(mutex_); + + auto it = cache_.find(key); + if (it != cache_.end()) { + return it->second; + } + + QString qkey = QString::fromStdString(key); + + if (settings_app_) { + QVariant value = settings_app_->value(qkey); + if (!value.isNull()) { + cache_[key] = value; + return value; + } + } + + if (settings_user_) { + QVariant value = settings_user_->value(qkey); + if (!value.isNull()) { + cache_[key] = value; + return value; + } + } + + if (settings_system_) { + QVariant value = settings_system_->value(qkey); + if (!value.isNull()) { + cache_[key] = value; + return value; + } + } + + return default_value; +} + +std::any ConfigDomain::query(const std::string& key, Layer layer) { + std::shared_lock lock(mutex_); + + if (layer == Layer::Temp) { + auto it = cache_.find(key); + if (it != cache_.end()) { + return it->second; + } + return std::any(); + } + + IConfigBackend* backend = get_backend(layer); + if (!backend) { + return std::any(); + } + + QString qkey = QString::fromStdString(key); + QVariant value = backend->value(qkey); + + if (value.isNull()) { + return std::any(); + } + + return value; +} + +bool ConfigDomain::has_key(const std::string& key) { + std::shared_lock lock(mutex_); + + if (cache_.find(key) != cache_.end()) { + return true; + } + + QString qkey = QString::fromStdString(key); + + if (settings_app_ && settings_app_->contains(qkey)) { + return true; + } + if (settings_user_ && settings_user_->contains(qkey)) { + return true; + } + if (settings_system_ && settings_system_->contains(qkey)) { + return true; + } + + return false; +} + +bool ConfigDomain::has_key(const std::string& key, Layer layer) { + std::shared_lock lock(mutex_); + + if (layer == Layer::Temp) { + return cache_.find(key) != cache_.end(); + } + + IConfigBackend* backend = get_backend(layer); + if (!backend) { + return false; + } + + QString qkey = QString::fromStdString(key); + return backend->contains(qkey); +} + +// ============================================================================ +// Write operations (public - acquire locks) +// ============================================================================ + +bool ConfigDomain::set(const std::string& key, const std::any& value, Layer layer, + NotifyPolicy notify_policy) { + std::unique_lock lock(mutex_); + bool result = set_impl(key, value, layer, notify_policy); + if (result) { + lock.unlock(); + execute_deferred_watchers(); + } + return result; +} + +RegisterResult ConfigDomain::register_key(const Key& key, const std::any& init_value, Layer layer, + NotifyPolicy notify_policy) { + std::unique_lock lock(mutex_); + RegisterResult result = register_key_impl(key, init_value, layer, notify_policy); + lock.unlock(); + execute_deferred_watchers(); + return result; +} + +UnRegisterResult ConfigDomain::unregister_key(const Key& key, Layer layer, + NotifyPolicy notify_policy) { + std::unique_lock lock(mutex_); + UnRegisterResult result = unregister_key_impl(key, layer, notify_policy); + lock.unlock(); + execute_deferred_watchers(); + return result; +} + +void ConfigDomain::clear() { + std::unique_lock lock(mutex_); + clear_impl(); + lock.unlock(); + execute_deferred_watchers(); +} + +void ConfigDomain::clear_layer(Layer layer) { + std::unique_lock lock(mutex_); + clear_layer_impl(layer); + lock.unlock(); + execute_deferred_watchers(); +} + +// ============================================================================ +// Write operations (internal lock-free implementations) +// ============================================================================ + +bool ConfigDomain::set_impl(const std::string& key, const std::any& value, Layer layer, + NotifyPolicy notify_policy) { + std::any old_value; + bool had_old = false; + auto it = cache_.find(key); + if (it != cache_.end()) { + old_value = it->second; + had_old = true; + } + + if (layer == Layer::Temp) { + cache_[key] = value; + + if (notify_policy == NotifyPolicy::Immediate) { + Key k{.full_key = key, .full_description = key}; + trigger_watchers(k, had_old ? &old_value : nullptr, &value, layer); + } else { + pending_changes_.push_back({Key{.full_key = key, .full_description = key}, + had_old ? old_value : std::any(), value, layer}); + } + return true; + } + + IConfigBackend* backend = get_backend(layer); + if (!backend) { + return false; + } + + QString qkey = QString::fromStdString(key); + + if (!had_old && backend->contains(qkey)) { + old_value = backend->value(qkey); + had_old = true; + } + + QVariant qvalue = anyToQVariant(value); + + backend->setValue(qkey, qvalue); + cache_[key] = value; + mark_dirty(layer); + + if (notify_policy == NotifyPolicy::Immediate) { + Key k{.full_key = key, .full_description = key}; + trigger_watchers(k, had_old ? &old_value : nullptr, &value, layer); + } else { + pending_changes_.push_back({Key{.full_key = key, .full_description = key}, + had_old ? old_value : std::any(), value, layer}); + } + + return true; +} + +RegisterResult ConfigDomain::register_key_impl(const Key& key, const std::any& init_value, + Layer layer, NotifyPolicy notify_policy) { + if (cache_.find(key.full_key) != cache_.end()) { + return RegisterResult::KeyAlreadyIn; + } + + if (layer != Layer::Temp) { + IConfigBackend* backend = get_backend(layer); + if (backend) { + QString qkey = QString::fromStdString(key.full_key); + if (backend->contains(qkey)) { + return RegisterResult::KeyAlreadyIn; + } + } + } + + set_impl(key.full_key, init_value, layer, notify_policy); + return RegisterResult::KeyRegisteredSuccess; +} + +UnRegisterResult ConfigDomain::unregister_key_impl(const Key& key, Layer layer, + NotifyPolicy notify_policy) { + bool existed = false; + std::any old_value; + + auto it = cache_.find(key.full_key); + if (it != cache_.end()) { + old_value = it->second; + cache_.erase(it); + existed = true; + } + + if (layer != Layer::Temp) { + IConfigBackend* backend = get_backend(layer); + if (backend) { + QString qkey = QString::fromStdString(key.full_key); + if (backend->contains(qkey)) { + if (!existed) { + old_value = backend->value(qkey); + } + backend->remove(qkey); + existed = true; + mark_dirty(layer); + } + } + } + + if (!existed) { + return UnRegisterResult::KeyUnexisted; + } + + if (notify_policy == NotifyPolicy::Immediate) { + trigger_watchers(key, &old_value, nullptr, layer); + } else { + pending_changes_.push_back({key, old_value, std::any(), layer}); + } + + return UnRegisterResult::KeyUnRegisteredSuccess; +} + +void ConfigDomain::clear_impl() { + cache_.clear(); + + if (settings_system_) { + settings_system_->clear(); + } + if (settings_user_) { + settings_user_->clear(); + } + if (settings_app_) { + settings_app_->clear(); + } + + dirty_flags_.fill(true); + pending_changes_.clear(); +} + +void ConfigDomain::clear_layer_impl(Layer layer) { + if (layer == Layer::Temp) { + cache_.clear(); + return; + } + + IConfigBackend* backend = get_backend(layer); + if (backend) { + backend->clear(); + mark_dirty(layer); + } +} + +// ============================================================================ +// Watcher operations +// ============================================================================ + +WatcherHandle ConfigDomain::watch(const std::string& pattern, Watcher callback, + NotifyPolicy policy) { + std::unique_lock lock(mutex_); + WatcherHandle handle = next_handle_.fetch_add(1); + watchers_.push_back({pattern, std::move(callback), policy, handle}); + return handle; +} + +void ConfigDomain::unwatch(WatcherHandle handle) { + std::unique_lock lock(mutex_); + auto it = std::remove_if(watchers_.begin(), watchers_.end(), + [handle](const WatcherEntry& e) { return e.handle == handle; }); + watchers_.erase(it, watchers_.end()); +} + +NotifyResult ConfigDomain::notify() { + std::unique_lock lock(mutex_); + + if (pending_changes_.empty()) { + return NotifyResult::NothingWorthNotify; + } + + for (const auto& change : pending_changes_) { + trigger_watchers(change.key, + change.old_value.type() == typeid(void) ? nullptr : &change.old_value, + change.new_value.type() == typeid(void) ? nullptr : &change.new_value, + change.from_layer); + } + + pending_changes_.clear(); + lock.unlock(); + + execute_deferred_watchers(); + return NotifyResult::NotifySuccess; +} + +std::size_t ConfigDomain::pending_changes() const { + std::shared_lock lock(mutex_); + return pending_changes_.size(); +} + +// ============================================================================ +// Persistence operations +// ============================================================================ + +void ConfigDomain::sync(bool async) { + (void)async; + + std::shared_lock lock(mutex_); + + if (settings_system_ && dirty_flags_[0]) { + settings_system_->sync(); + dirty_flags_[0] = false; + } + if (settings_user_ && dirty_flags_[1]) { + settings_user_->sync(); + dirty_flags_[1] = false; + } + if (settings_app_ && dirty_flags_[2]) { + settings_app_->sync(); + dirty_flags_[2] = false; + } +} + +void ConfigDomain::reload() { + std::unique_lock lock(mutex_); + + cache_.clear(); + pending_changes_.clear(); + + if (settings_system_) { + settings_system_->reload(); + } + if (settings_user_) { + settings_user_->reload(); + } + if (settings_app_) { + settings_app_->reload(); + } +} + +// ============================================================================ +// Private helpers +// ============================================================================ + +bool ConfigDomain::match_pattern(const std::string& pattern, const std::string& key) { + return detail::match_pattern(pattern, key); +} + +IConfigBackend* ConfigDomain::get_backend(Layer layer) { + switch (layer) { + case Layer::System: + return settings_system_.get(); + case Layer::User: + return settings_user_.get(); + case Layer::App: + return settings_app_.get(); + case Layer::Temp: + return nullptr; + } + return nullptr; +} + +QVariant ConfigDomain::anyToQVariant(const std::any& value) { + if (value.type() == typeid(int)) { + return QVariant::fromValue(std::any_cast(value)); + } else if (value.type() == typeid(double)) { + return QVariant::fromValue(std::any_cast(value)); + } else if (value.type() == typeid(bool)) { + return QVariant::fromValue(std::any_cast(value)); + } else if (value.type() == typeid(std::string)) { + return QString::fromStdString(std::any_cast(value)); + } else { + try { + return std::any_cast(value); + } catch (const std::bad_any_cast&) { + return QVariant(); + } + } +} + +void ConfigDomain::trigger_watchers(const Key& key, const std::any* old_value, + const std::any* new_value, Layer layer) { + for (const auto& watcher : watchers_) { + if (detail::match_pattern(watcher.pattern, key.full_key)) { + DeferredWatcherEvent event; + event.callback = watcher.callback; + event.key = key; + event.from_layer = layer; + event.has_old_value = (old_value != nullptr); + event.has_new_value = (new_value != nullptr); + if (old_value) + event.old_value = *old_value; + if (new_value) + event.new_value = *new_value; + deferred_events_.push_back(std::move(event)); + } + } +} + +void ConfigDomain::execute_deferred_watchers() { + std::vector events; + { + std::lock_guard lock(deferred_mutex_); + events = std::move(deferred_events_); + deferred_events_.clear(); + } + + for (const auto& event : events) { + event.callback(event.key, event.has_old_value ? &event.old_value : nullptr, + event.has_new_value ? &event.new_value : nullptr, event.from_layer); + } +} + +void ConfigDomain::mark_dirty(Layer layer) { + int index = static_cast(layer); + if (index >= 0 && index < 4) { + dirty_flags_[index] = true; + } +} + +} // namespace cf::config diff --git a/desktop/base/config_manager/src/impl/config_impl.cpp b/desktop/base/config_manager/src/impl/config_impl.cpp index a56dcd431..08c8ea33a 100644 --- a/desktop/base/config_manager/src/impl/config_impl.cpp +++ b/desktop/base/config_manager/src/impl/config_impl.cpp @@ -2,8 +2,10 @@ * @file config_impl.cpp * @brief Internal implementation of ConfigStore (Pimpl). * + * ConfigStoreImpl delegates all per-domain operations to ConfigDomain instances. + * * @date 2026-03-17 - * @version 1.0 + * @version 2.0 */ #include "impl/config_impl.h" @@ -14,47 +16,11 @@ #include #include #include -#include #include #include -#include namespace cf::config { -namespace detail { - -/** - * @brief Match a key pattern against a key. - * Supports * wildcard. - */ -bool match_pattern(const std::string& pattern, const std::string& key) { - // Convert glob pattern to regex - std::string regex_pattern = pattern; - // Escape special regex characters except * - size_t pos = 0; - while ((pos = regex_pattern.find_first_of("^.${}+|()[]\\", pos)) != std::string::npos) { - regex_pattern.insert(pos, "\\"); - pos += 2; - } - // Replace * with .* - pos = 0; - while ((pos = regex_pattern.find('*', pos)) != std::string::npos) { - regex_pattern.replace(pos, 1, ".*"); - pos += 2; - } - // Anchor to start and end - regex_pattern = "^" + regex_pattern + "$"; - - try { - std::regex re(regex_pattern); - return std::regex_match(key, re); - } catch (const std::regex_error&) { - return false; - } -} - -} // namespace detail - // ============================================================================ // Constructor / Destructor // ============================================================================ @@ -64,532 +30,127 @@ ConfigStoreImpl::ConfigStoreImpl() ConfigStoreImpl::ConfigStoreImpl(std::shared_ptr path_provider) : path_provider_(std::move(path_provider)) { - // System layer: use path provider - QString systemPath = path_provider_->system_path(); - if (!systemPath.isEmpty() && QFileInfo::exists(systemPath)) { - settings_system_ = createBackend(systemPath); - } - - // User layer: use path provider - QString userDir = path_provider_->user_dir(); - if (!userDir.isEmpty()) { - QDir().mkpath(userDir); - QString userFilePath = userDir + "/" + path_provider_->user_filename(); - settings_user_ = createBackend(userFilePath); - } - - // App layer: use path provider - QString appDir = path_provider_->app_dir(); - if (!appDir.isEmpty()) { - QDir().mkpath(appDir); - QString appFilePath = appDir + "/" + path_provider_->app_filename(); - settings_app_ = createBackend(appFilePath); - } + // Always create the default domain eagerly + domains_.emplace("default", std::make_unique(path_provider_, "default")); } ConfigStoreImpl::~ConfigStoreImpl() { - // Sync all dirty layers - sync(false); + // Sync all domains + std::shared_lock lock(mutex_); + for (auto& [name, domain] : domains_) { + (void)name; + domain->sync(false); + } } // ============================================================================ -// Query operations +// Domain management // ============================================================================ -std::any ConfigStoreImpl::query(const std::string& key, const std::any& default_value) { - std::shared_lock lock(mutex_); - - // Check cache first (Temp layer) - auto it = cache_.find(key); - if (it != cache_.end()) { - return it->second; - } - - QString qkey = QString::fromStdString(key); - - // Query App layer - if (settings_app_) { - QVariant value = settings_app_->value(qkey); - if (!value.isNull()) { - cache_[key] = value; - return value; - } - } - - // Query User layer - if (settings_user_) { - QVariant value = settings_user_->value(qkey); - if (!value.isNull()) { - cache_[key] = value; - return value; +ConfigDomain* ConfigStoreImpl::get_domain(const std::string& name) { + // Fast path: shared lock for lookup + { + std::shared_lock lock(mutex_); + auto it = domains_.find(name); + if (it != domains_.end()) { + return it->second.get(); } } - // Query System layer - if (settings_system_) { - QVariant value = settings_system_->value(qkey); - if (!value.isNull()) { - cache_[key] = value; - return value; - } + // Slow path: unique lock for creation (double-checked) + std::unique_lock lock(mutex_); + auto it = domains_.find(name); + if (it != domains_.end()) { + return it->second.get(); } - return default_value; + auto [ins_it, ok] = + domains_.emplace(name, std::make_unique(path_provider_, name)); + return ins_it->second.get(); } -std::any ConfigStoreImpl::query(const std::string& key, Layer layer) { - std::shared_lock lock(mutex_); - - // Temp layer - check cache - if (layer == Layer::Temp) { - auto it = cache_.find(key); - if (it != cache_.end()) { - return it->second; - } - return std::any(); - } - - IConfigBackend* backend = get_backend(layer); - if (!backend) { - return std::any(); - } +ConfigDomain* ConfigStoreImpl::default_domain() const { + // Default domain is always created in constructor, no lock needed + return domains_.at("default").get(); +} - QString qkey = QString::fromStdString(key); - QVariant value = backend->value(qkey); +// ============================================================================ +// Delegated operations (default domain) +// ============================================================================ - if (value.isNull()) { - return std::any(); - } +std::any ConfigStoreImpl::query(const std::string& key, const std::any& default_value) { + return default_domain()->query(key, default_value); +} - return value; +std::any ConfigStoreImpl::query(const std::string& key, Layer layer) { + return default_domain()->query(key, layer); } bool ConfigStoreImpl::has_key(const std::string& key) { - std::shared_lock lock(mutex_); - - // Check cache (Temp layer) - if (cache_.find(key) != cache_.end()) { - return true; - } - - QString qkey = QString::fromStdString(key); - - // Check App layer - if (settings_app_) { - if (settings_app_->contains(qkey)) { - return true; - } - } - - // Check User layer - if (settings_user_) { - if (settings_user_->contains(qkey)) { - return true; - } - } - - // Check System layer - if (settings_system_) { - if (settings_system_->contains(qkey)) { - return true; - } - } - - return false; + return default_domain()->has_key(key); } bool ConfigStoreImpl::has_key(const std::string& key, Layer layer) { - std::shared_lock lock(mutex_); - - if (layer == Layer::Temp) { - return cache_.find(key) != cache_.end(); - } - - IConfigBackend* backend = get_backend(layer); - if (!backend) { - return false; - } - - QString qkey = QString::fromStdString(key); - return backend->contains(qkey); + return default_domain()->has_key(key, layer); } -// ============================================================================ -// Write operations (public - acquire locks) -// ============================================================================ - bool ConfigStoreImpl::set(const std::string& key, const std::any& value, Layer layer, NotifyPolicy notify_policy) { - std::unique_lock lock(mutex_); - bool result = set_impl(key, value, layer, notify_policy); - if (result) { - lock.unlock(); - execute_deferred_watchers(); - } - return result; + return default_domain()->set(key, value, layer, notify_policy); } RegisterResult ConfigStoreImpl::register_key(const Key& key, const std::any& init_value, Layer layer, NotifyPolicy notify_policy) { - std::unique_lock lock(mutex_); - RegisterResult result = register_key_impl(key, init_value, layer, notify_policy); - lock.unlock(); - execute_deferred_watchers(); - return result; + return default_domain()->register_key(key, init_value, layer, notify_policy); } UnRegisterResult ConfigStoreImpl::unregister_key(const Key& key, Layer layer, NotifyPolicy notify_policy) { - std::unique_lock lock(mutex_); - UnRegisterResult result = unregister_key_impl(key, layer, notify_policy); - lock.unlock(); - execute_deferred_watchers(); - return result; + return default_domain()->unregister_key(key, layer, notify_policy); } void ConfigStoreImpl::clear() { - std::unique_lock lock(mutex_); - clear_impl(); - lock.unlock(); - execute_deferred_watchers(); -} - -void ConfigStoreImpl::clear_layer(Layer layer) { - std::unique_lock lock(mutex_); - clear_layer_impl(layer); - lock.unlock(); - execute_deferred_watchers(); -} - -// ============================================================================ -// Write operations (internal lock-free implementations) -// ============================================================================ - -bool ConfigStoreImpl::set_impl(const std::string& key, const std::any& value, Layer layer, - NotifyPolicy notify_policy) { - // Store old value for notification - std::any old_value; - bool had_old = false; - auto it = cache_.find(key); - if (it != cache_.end()) { - old_value = it->second; - had_old = true; - } - - // Temp layer - just update cache - if (layer == Layer::Temp) { - cache_[key] = value; - - if (notify_policy == NotifyPolicy::Immediate) { - Key k{.full_key = key, .full_description = key}; - trigger_watchers(k, had_old ? &old_value : nullptr, &value, layer); - } else { - pending_changes_.push_back({Key{.full_key = key, .full_description = key}, - had_old ? old_value : std::any(), value, layer}); - } - return true; - } - - // Other layers - update backend - IConfigBackend* backend = get_backend(layer); - if (!backend) { - return false; - } - - QString qkey = QString::fromStdString(key); - - // Get old value from backend if not in cache - if (!had_old && backend->contains(qkey)) { - old_value = backend->value(qkey); - had_old = true; - } - - // Convert std::any to QVariant and store - QVariant qvalue = anyToQVariant(value); - - backend->setValue(qkey, qvalue); - cache_[key] = value; - mark_dirty(layer); - - // Notify watchers - if (notify_policy == NotifyPolicy::Immediate) { - Key k{.full_key = key, .full_description = key}; - trigger_watchers(k, had_old ? &old_value : nullptr, &value, layer); - } else { - pending_changes_.push_back({Key{.full_key = key, .full_description = key}, - had_old ? old_value : std::any(), value, layer}); - } - - return true; -} - -RegisterResult ConfigStoreImpl::register_key_impl(const Key& key, const std::any& init_value, - Layer layer, NotifyPolicy notify_policy) { - // Check if key already exists in cache - if (cache_.find(key.full_key) != cache_.end()) { - return RegisterResult::KeyAlreadyIn; - } - - // Check backend for non-Temp layers - if (layer != Layer::Temp) { - IConfigBackend* backend = get_backend(layer); - if (backend) { - QString qkey = QString::fromStdString(key.full_key); - if (backend->contains(qkey)) { - return RegisterResult::KeyAlreadyIn; - } - } - } - - // Register the key using internal set - set_impl(key.full_key, init_value, layer, notify_policy); - return RegisterResult::KeyRegisteredSuccess; -} - -UnRegisterResult ConfigStoreImpl::unregister_key_impl(const Key& key, Layer layer, - NotifyPolicy notify_policy) { - bool existed = false; - std::any old_value; - - // Check and remove from cache - auto it = cache_.find(key.full_key); - if (it != cache_.end()) { - old_value = it->second; - cache_.erase(it); - existed = true; - } - - // Remove from backend for non-Temp layers - if (layer != Layer::Temp) { - IConfigBackend* backend = get_backend(layer); - if (backend) { - QString qkey = QString::fromStdString(key.full_key); - if (backend->contains(qkey)) { - if (!existed) { - old_value = backend->value(qkey); - } - backend->remove(qkey); - existed = true; - mark_dirty(layer); - } - } - } - - if (!existed) { - return UnRegisterResult::KeyUnexisted; - } - - // Notify watchers - if (notify_policy == NotifyPolicy::Immediate) { - trigger_watchers(key, &old_value, nullptr, layer); - } else { - pending_changes_.push_back({key, old_value, std::any(), layer}); - } - - return UnRegisterResult::KeyUnRegisteredSuccess; -} - -void ConfigStoreImpl::clear_impl() { - cache_.clear(); - - if (settings_system_) { - settings_system_->clear(); - } - if (settings_user_) { - settings_user_->clear(); - } - if (settings_app_) { - settings_app_->clear(); + std::shared_lock lock(mutex_); + for (auto& [name, domain] : domains_) { + (void)name; + domain->clear(); } - - dirty_flags_.fill(true); - pending_changes_.clear(); } -void ConfigStoreImpl::clear_layer_impl(Layer layer) { - if (layer == Layer::Temp) { - cache_.clear(); - return; - } - - IConfigBackend* backend = get_backend(layer); - if (backend) { - backend->clear(); - mark_dirty(layer); - } +void ConfigStoreImpl::clear_layer(Layer layer) { + default_domain()->clear_layer(layer); } -// ============================================================================ -// Watcher operations -// ============================================================================ - WatcherHandle ConfigStoreImpl::watch(const std::string& pattern, Watcher callback, NotifyPolicy policy) { - std::unique_lock lock(mutex_); - WatcherHandle handle = next_handle_.fetch_add(1); - watchers_.push_back({pattern, std::move(callback), policy, handle}); - return handle; + return default_domain()->watch(pattern, std::move(callback), policy); } void ConfigStoreImpl::unwatch(WatcherHandle handle) { - std::unique_lock lock(mutex_); - auto it = std::remove_if(watchers_.begin(), watchers_.end(), - [handle](const WatcherEntry& e) { return e.handle == handle; }); - watchers_.erase(it, watchers_.end()); + default_domain()->unwatch(handle); } NotifyResult ConfigStoreImpl::notify() { - std::unique_lock lock(mutex_); - - if (pending_changes_.empty()) { - return NotifyResult::NothingWorthNotify; - } - - // Collect all watcher events - for (const auto& change : pending_changes_) { - trigger_watchers(change.key, - change.old_value.type() == typeid(void) ? nullptr : &change.old_value, - change.new_value.type() == typeid(void) ? nullptr : &change.new_value, - change.from_layer); - } - - pending_changes_.clear(); - lock.unlock(); - - execute_deferred_watchers(); - return NotifyResult::NotifySuccess; + return default_domain()->notify(); } std::size_t ConfigStoreImpl::pending_changes() const { - std::shared_lock lock(mutex_); - return pending_changes_.size(); + return default_domain()->pending_changes(); } -// ============================================================================ -// Persistence operations -// ============================================================================ - void ConfigStoreImpl::sync(bool async) { - // Note: async parameter is reserved for future implementation - // For now, all syncs are synchronous - std::shared_lock lock(mutex_); - - if (settings_system_ && dirty_flags_[0]) { - settings_system_->sync(); - dirty_flags_[0] = false; + for (auto& [name, domain] : domains_) { + (void)name; + domain->sync(async); } - if (settings_user_ && dirty_flags_[1]) { - settings_user_->sync(); - dirty_flags_[1] = false; - } - if (settings_app_ && dirty_flags_[2]) { - settings_app_->sync(); - dirty_flags_[2] = false; - } - // Temp layer (index 3) doesn't need syncing } void ConfigStoreImpl::reload() { - std::unique_lock lock(mutex_); - - // Clear cache (this clears the Temp layer) - cache_.clear(); - pending_changes_.clear(); - - // Reload backends from disk - if (settings_system_) { - settings_system_->reload(); - } - if (settings_user_) { - settings_user_->reload(); - } - if (settings_app_) { - settings_app_->reload(); - } -} - -// ============================================================================ -// Private helpers -// ============================================================================ - -bool ConfigStoreImpl::match_pattern(const std::string& pattern, const std::string& key) { - return detail::match_pattern(pattern, key); -} - -IConfigBackend* ConfigStoreImpl::get_backend(Layer layer) { - switch (layer) { - case Layer::System: - return settings_system_.get(); - case Layer::User: - return settings_user_.get(); - case Layer::App: - return settings_app_.get(); - case Layer::Temp: - return nullptr; // Temp layer is memory only - } - return nullptr; -} - -QVariant ConfigStoreImpl::anyToQVariant(const std::any& value) { - if (value.type() == typeid(int)) { - return QVariant::fromValue(std::any_cast(value)); - } else if (value.type() == typeid(double)) { - return QVariant::fromValue(std::any_cast(value)); - } else if (value.type() == typeid(bool)) { - return QVariant::fromValue(std::any_cast(value)); - } else if (value.type() == typeid(std::string)) { - return QString::fromStdString(std::any_cast(value)); - } else { - // Try QVariant directly - try { - return std::any_cast(value); - } catch (const std::bad_any_cast&) { - return QVariant(); - } - } -} - -void ConfigStoreImpl::trigger_watchers(const Key& key, const std::any* old_value, - const std::any* new_value, Layer layer) { - // We're already under lock - collect events for deferred execution - for (const auto& watcher : watchers_) { - if (detail::match_pattern(watcher.pattern, key.full_key)) { - DeferredWatcherEvent event; - event.callback = watcher.callback; - event.key = key; - event.from_layer = layer; - event.has_old_value = (old_value != nullptr); - event.has_new_value = (new_value != nullptr); - if (old_value) - event.old_value = *old_value; - if (new_value) - event.new_value = *new_value; - deferred_events_.push_back(std::move(event)); - } - } -} - -void ConfigStoreImpl::execute_deferred_watchers() { - // Execute deferred callbacks without holding the main mutex - std::vector events; - { - std::lock_guard lock(deferred_mutex_); - events = std::move(deferred_events_); - deferred_events_.clear(); - } - - // Now execute callbacks without any lock held - for (const auto& event : events) { - event.callback(event.key, event.has_old_value ? &event.old_value : nullptr, - event.has_new_value ? &event.new_value : nullptr, event.from_layer); - } -} - -void ConfigStoreImpl::mark_dirty(Layer layer) { - int index = static_cast(layer); - if (index >= 0 && index < 4) { - dirty_flags_[index] = true; + std::shared_lock lock(mutex_); + for (auto& [name, domain] : domains_) { + (void)name; + domain->reload(); } } diff --git a/desktop/base/file_operations/CMakeLists.txt b/desktop/base/file_operations/CMakeLists.txt index eac51f8eb..efca98236 100644 --- a/desktop/base/file_operations/CMakeLists.txt +++ b/desktop/base/file_operations/CMakeLists.txt @@ -4,6 +4,7 @@ add_library(cffilesystem STATIC) # Sources target_sources(cffilesystem PRIVATE file_op.cpp + filter_target.cpp ) # Include directories diff --git a/desktop/base/file_operations/filter_target.cpp b/desktop/base/file_operations/filter_target.cpp new file mode 100644 index 000000000..ec5079f33 --- /dev/null +++ b/desktop/base/file_operations/filter_target.cpp @@ -0,0 +1,72 @@ +#include "filter_target.h" +#include +#include +#include + +namespace cf::desktop::base::filesystem { + +namespace { + +class FilterPool { + public: + static FilterPool& instance() { + static FilterPool pool; + return pool; + } + + QString filter_string(FilterType type) const { return m_filterStrings.value(type); } + + QStringList& filter_list(FilterType type) { return m_filterLists[type]; } + + private: + FilterPool() { + // Pictures + m_filterLists[FilterType::Pictures] = {"*.png", "*.jpg", "*.jpeg", "*.bmp", "*.gif", + "*.webp", "*.svg", "*.tiff", "*.ico"}; + m_filterStrings[FilterType::Pictures] = + QString("Images (%1)").arg(m_filterLists[FilterType::Pictures].join(' ')); + + // AllFiles (files only) + m_filterLists[FilterType::AllFiles] = {"*"}; + m_filterStrings[FilterType::AllFiles] = QString("All Files (*)"); + + // All (files + directories) + m_filterLists[FilterType::All] = {"*"}; + m_filterStrings[FilterType::All] = QString("All (*)"); + + // Dirent + m_filterLists[FilterType::Dirent] = {}; + m_filterStrings[FilterType::Dirent] = QString("Directories"); + } + + QMap m_filterLists; + QMap m_filterStrings; +}; + +} // namespace + +QString request_filter(const FilterType what) { + return FilterPool::instance().filter_string(what); +} + +QStringList& request_filterlist(const FilterType what) { + return FilterPool::instance().filter_list(what); +} + +QStringList filter_target(const QString& dirent_path, const QStringList& filters) { + QStringList result; + QDir dir(dirent_path); + + if (!dir.exists()) { + return result; + } + + const QFileInfoList entries = dir.entryInfoList(filters, QDir::Files | QDir::NoDotAndDotDot); + for (const auto& entry : entries) { + result.append(entry.absoluteFilePath()); + } + + return result; +} + +} // namespace cf::desktop::base::filesystem diff --git a/desktop/base/file_operations/filter_target.h b/desktop/base/file_operations/filter_target.h new file mode 100644 index 000000000..d3af46c4f --- /dev/null +++ b/desktop/base/file_operations/filter_target.h @@ -0,0 +1,43 @@ +/** + * @file desktop/base/file_operations/filter_target.h + * @brief File-system filtering utilities for directory traversal. + * + * Provides filter types and functions for listing files in a directory + * that match specific criteria (e.g., pictures, all files). + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-04-09 + * @version 0.1 + * @since 0.1 + * @ingroup filesystem + */ + +#pragma once +#include + +namespace cf::desktop::base::filesystem { + +/** + * @brief File filter categories for directory traversal. + * + * @ingroup filesystem + */ +enum class FilterType { + Pictures, ///< Common image file extensions. + AllFiles, ///< All regular files. + All, ///< All entries including hidden files. + Dirent ///< Directory entries only. +}; + +QString request_filter(const FilterType what); +QStringList& request_filterlist(const FilterType what); + +/** + * @brief Get the file associate the filters + * + * @param dirent_path + * @param filters + * @return QStringList + */ +QStringList filter_target(const QString& dirent_path, const QStringList& filters); +} // namespace cf::desktop::base::filesystem diff --git a/desktop/base/fundamental/CMakeLists.txt b/desktop/base/fundamental/CMakeLists.txt new file mode 100644 index 000000000..e6c3f0d47 --- /dev/null +++ b/desktop/base/fundamental/CMakeLists.txt @@ -0,0 +1,17 @@ +# Fundamental module — logging helpers and basic utilities +add_library(cffundamental STATIC) + +target_sources(cffundamental PRIVATE + src/log_helper.cpp +) + +target_include_directories(cffundamental PUBLIC + $ +) + +target_link_libraries(cffundamental PUBLIC + cflogger + Qt6::Core +) + +add_library(CFDesktop::fundamental ALIAS cffundamental) diff --git a/desktop/main/log/log_helper.h b/desktop/base/fundamental/include/cffundamental/log_helper.h similarity index 85% rename from desktop/main/log/log_helper.h rename to desktop/base/fundamental/include/cffundamental/log_helper.h index 6b8b5fe4a..1bebbf46c 100644 --- a/desktop/main/log/log_helper.h +++ b/desktop/base/fundamental/include/cffundamental/log_helper.h @@ -1,5 +1,5 @@ /** - * @file desktop/main/log/log_helper.h + * @file desktop/base/fundamental/include/cffundamental/log_helper.h * @brief Provides logging helper functions for the desktop module. * * Contains utility functions for generating log filenames and related @@ -9,7 +9,7 @@ * @date * @version * @since N/A - * @ingroup desktop_main + * @ingroup desktop_base */ #pragma once @@ -27,7 +27,7 @@ namespace cf::desktop::logger_helper { * @note None * @warning None * @since N/A - * @ingroup desktop_main + * @ingroup desktop_base */ QString log_filename(); diff --git a/desktop/main/log/log_helper.cpp b/desktop/base/fundamental/src/log_helper.cpp similarity index 73% rename from desktop/main/log/log_helper.cpp rename to desktop/base/fundamental/src/log_helper.cpp index 148738ba6..963719f2f 100644 --- a/desktop/main/log/log_helper.cpp +++ b/desktop/base/fundamental/src/log_helper.cpp @@ -1,4 +1,4 @@ -#include "log_helper.h" +#include "cffundamental/log_helper.h" #include namespace cf::desktop::logger_helper { @@ -8,4 +8,4 @@ QString log_filename() { return QString("cfdesktop_%1.log").arg(datetime); } -} // namespace cf::desktop::logger_helper \ No newline at end of file +} // namespace cf::desktop::logger_helper diff --git a/desktop/base/path/CMakeLists.txt b/desktop/base/path/CMakeLists.txt new file mode 100644 index 000000000..7b1ba1cff --- /dev/null +++ b/desktop/base/path/CMakeLists.txt @@ -0,0 +1,19 @@ +# Path module — desktop path resolution utilities +add_library(cfpath STATIC) + +target_sources(cfpath PRIVATE + src/desktop_main_path_resolvers.cpp + src/desktop_self_path_resolvers.cpp +) + +target_include_directories(cfpath PUBLIC + $ +) + +target_link_libraries(cfpath PUBLIC + CFDesktop::base + cflogger + Qt6::Core +) + +add_library(CFDesktop::path ALIAS cfpath) diff --git a/desktop/main/path/desktop_main_path_resolvers.h b/desktop/base/path/include/cfpath/desktop_main_path_resolvers.h similarity index 92% rename from desktop/main/path/desktop_main_path_resolvers.h rename to desktop/base/path/include/cfpath/desktop_main_path_resolvers.h index f891bf7a4..c9fa53e40 100644 --- a/desktop/main/path/desktop_main_path_resolvers.h +++ b/desktop/base/path/include/cfpath/desktop_main_path_resolvers.h @@ -1,5 +1,5 @@ /** - * @file desktop/main/path/desktop_main_path_resolvers.h + * @file desktop/base/path/include/cfpath/desktop_main_path_resolvers.h * @brief Provides path resolution for desktop main directories and resources. * * Contains DesktopMainPathProvider class for managing and resolving paths @@ -9,7 +9,7 @@ * @date * @version * @since N/A - * @ingroup desktop_main + * @ingroup desktop_base */ #pragma once @@ -24,7 +24,7 @@ namespace cf::desktop::path { * Manages resolution of standard desktop directory paths including Home, * Desktop, Documents, Downloads, Music, Pictures, Videos, Apps, and Runtime. * - * @ingroup desktop_main + * @ingroup desktop_base */ class DesktopMainPathProvider : public cf::Singleton { public: @@ -50,7 +50,7 @@ class DesktopMainPathProvider : public cf::Singleton { * * Defines enumeration values for common desktop directory types. * - * @ingroup desktop_main + * @ingroup desktop_base */ enum class PathType { #define X(name) name, @@ -81,7 +81,7 @@ class DesktopMainPathProvider : public cf::Singleton { * @note None * @warning None * @since N/A - * @ingroup desktop_main + * @ingroup desktop_base */ bool setup(); @@ -96,7 +96,7 @@ class DesktopMainPathProvider : public cf::Singleton { * @note None * @warning None * @since N/A - * @ingroup desktop_main + * @ingroup desktop_base */ QString absolutePath(const PathType p); @@ -127,10 +127,10 @@ class DesktopMainPathProvider : public cf::Singleton { * @note None * @warning None * @since N/A - * @ingroup desktop_main + * @ingroup desktop_base */ DesktopMainPathProvider(const QString& desktop_active_root); /// @brief View of the root directory path. Ownership: observer. QString root{}; }; -} // namespace cf::desktop::path \ No newline at end of file +} // namespace cf::desktop::path diff --git a/desktop/main/path/desktop_self_path_resolvers.h b/desktop/base/path/include/cfpath/desktop_self_path_resolvers.h similarity index 80% rename from desktop/main/path/desktop_self_path_resolvers.h rename to desktop/base/path/include/cfpath/desktop_self_path_resolvers.h index e31d6a315..97cf57e2e 100644 --- a/desktop/main/path/desktop_self_path_resolvers.h +++ b/desktop/base/path/include/cfpath/desktop_self_path_resolvers.h @@ -1,5 +1,5 @@ /** - * @file desktop/main/path/desktop_self_path_resolvers.h + * @file desktop/base/path/include/cfpath/desktop_self_path_resolvers.h * @brief Provides path resolution utilities for desktop self-referential paths. * * Contains declarations for resolving paths related to the desktop @@ -9,7 +9,7 @@ * @date * @version * @since N/A - * @ingroup desktop_main + * @ingroup desktop_base */ #pragma once diff --git a/desktop/base/path/include/cfpath/desktop_settings_path.h b/desktop/base/path/include/cfpath/desktop_settings_path.h new file mode 100644 index 000000000..9a3321643 --- /dev/null +++ b/desktop/base/path/include/cfpath/desktop_settings_path.h @@ -0,0 +1,14 @@ +/** + * @file desktop/base/path/include/cfpath/desktop_settings_path.h + * @brief Desktop settings path constants. + * + * Provides path declarations for desktop settings storage. + * + * @author N/A + * @date N/A + * @version N/A + * @since N/A + * @ingroup none + */ + +#pragma once diff --git a/desktop/main/path/desktop_main_path_resolvers.cpp b/desktop/base/path/src/desktop_main_path_resolvers.cpp similarity index 96% rename from desktop/main/path/desktop_main_path_resolvers.cpp rename to desktop/base/path/src/desktop_main_path_resolvers.cpp index 3850cb7f8..e82e29df8 100644 --- a/desktop/main/path/desktop_main_path_resolvers.cpp +++ b/desktop/base/path/src/desktop_main_path_resolvers.cpp @@ -1,4 +1,4 @@ -#include "desktop_main_path_resolvers.h" +#include "cfpath/desktop_main_path_resolvers.h" #include "cflog.h" #include diff --git a/desktop/base/path/src/desktop_self_path_resolvers.cpp b/desktop/base/path/src/desktop_self_path_resolvers.cpp new file mode 100644 index 000000000..498c59cc8 --- /dev/null +++ b/desktop/base/path/src/desktop_self_path_resolvers.cpp @@ -0,0 +1,3 @@ +#include "cfpath/desktop_self_path_resolvers.h" + +namespace cf::desktop::path {} diff --git a/desktop/main/CMakeLists.txt b/desktop/main/CMakeLists.txt index 4bf8ba8be..ebb0d2310 100644 --- a/desktop/main/CMakeLists.txt +++ b/desktop/main/CMakeLists.txt @@ -1,7 +1,6 @@ log_info("Desktop Main" "Creating unified CFDesktopMain library") # Collect source variables from subdirectories -add_subdirectory(log) add_subdirectory(early_session/impl) # CFDesktopEarlySession sources (merged from early_session/CMakeLists.txt) set(EARLY_SESSION_SOURCES @@ -20,13 +19,12 @@ set(INIT_SOURCES init/gui_progress/gui_init_stage.cpp init/early_gain/early_pass_stage.cpp init/desktop_backbone_init/desktop_backbone_init.cpp + init/desktop_config_init/desktop_config_init.cpp ) add_library(CFDesktopMain STATIC) target_sources(CFDesktopMain PRIVATE - path/desktop_main_path_resolvers.cpp - path/desktop_self_path_resolvers.cpp ${CMAKE_CURRENT_SOURCE_DIR}/desktop_entry.cpp ${EARLY_SESSION_SOURCES} ${INIT_SOURCES} @@ -42,7 +40,7 @@ target_include_directories(CFDesktopMain PUBLIC ) target_link_libraries(CFDesktopMain PUBLIC - cfbase cflogger cfasciiart cffilesystem CFDesktopLog + cfbase cflogger cfasciiart cffilesystem cffundamental cfpath cfconfig cf_desktop_ui_widget_init_session Qt6::Core Qt6::Gui Qt6::Widgets ) diff --git a/desktop/main/early_session/CMakeLists.txt b/desktop/main/early_session/CMakeLists.txt index b0678f47e..da8698761 100644 --- a/desktop/main/early_session/CMakeLists.txt +++ b/desktop/main/early_session/CMakeLists.txt @@ -17,9 +17,9 @@ target_include_directories(CFDesktopEarlySession PUBLIC # Link against cfbase to access device headers and other base utilities # Link against cflogger to access cflog.h -# Link against CFDesktopLog for log_helper functions +# Link against cffundamental for log_helper functions target_link_libraries(CFDesktopEarlySession PUBLIC - cfbase cflogger cfasciiart cffilesystem CFDesktopLog + cfbase cflogger cfasciiart cffilesystem cffundamental Qt6::Core Qt6::Gui Qt6::Widgets) diff --git a/desktop/main/early_session/settings/early_settings.cpp b/desktop/main/early_session/settings/early_settings.cpp index d19d42d33..dfa237eba 100644 --- a/desktop/main/early_session/settings/early_settings.cpp +++ b/desktop/main/early_session/settings/early_settings.cpp @@ -1,6 +1,6 @@ #include "settings/early_settings.h" +#include "cffundamental/log_helper.h" #include "file_op.h" -#include "log/log_helper.h" #include #include #include @@ -71,6 +71,20 @@ QString EarlySettings::get_desktop_root() const { return desktop_root; } +QString EarlySettings::get_config_folder() const { + if (!valid() || !early_settings_obj_) { + return {}; + } + + if (!config_folder_cache.isEmpty()) { + return config_folder_cache; + } + + config_folder_cache = + (*early_settings_obj_)["desktop"].toObject()["config_folder"].toString("settings/desktop/"); + return config_folder_cache; +} + std::unique_ptr EarlySettings::unlock_early_settings() { return std::move(early_settings_obj_); } diff --git a/desktop/main/early_session/settings/early_settings.h b/desktop/main/early_session/settings/early_settings.h index 232fc003f..68db82132 100644 --- a/desktop/main/early_session/settings/early_settings.h +++ b/desktop/main/early_session/settings/early_settings.h @@ -75,6 +75,13 @@ class EarlySettings { */ QString get_desktop_root() const; + /** + * @brief Gets the desktop configuration folder path (relative to app runtime dir). + * + * @return Relative config folder path, defaults to "settings/desktop/". + */ + QString get_config_folder() const; + /** * @brief Unlock the Early Settings, used in releasing the sessions. * @@ -90,6 +97,9 @@ class EarlySettings { /// @brief Cached Desktop Root; only changes when the desktop path is switched. mutable QString desktop_root{}; + /// @brief Cached config folder path. + mutable QString config_folder_cache{}; + /// @brief Parsed JSON object containing the configuration data. Ownership: owner. std::unique_ptr early_settings_obj_; 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 884963b8f..223ab6530 100644 --- a/desktop/main/init/desktop_backbone_init/desktop_backbone_init.cpp +++ b/desktop/main/init/desktop_backbone_init/desktop_backbone_init.cpp @@ -1,8 +1,8 @@ #include "desktop_backbone_init.h" +#include "cfpath/desktop_main_path_resolvers.h" #include "init_session_chain.h" #include "init_settings.h" #include "init_stage_name.h" -#include "path/desktop_main_path_resolvers.h" namespace cf::desktop::init_session { diff --git a/desktop/main/init/desktop_config_init/desktop_config_init.cpp b/desktop/main/init/desktop_config_init/desktop_config_init.cpp new file mode 100644 index 000000000..e305252fa --- /dev/null +++ b/desktop/main/init/desktop_config_init/desktop_config_init.cpp @@ -0,0 +1,65 @@ +#include "desktop_config_init.h" + +#include "cfconfig.hpp" +#include "cfconfig/cfconfig_path_provider.h" +#include "cflog.h" +#include "desktop_settings.h" +#include "file_op.h" +#include "init_settings.h" + +#include + +namespace cf::desktop::init_session { + +DesktopConfigInitStage::StageResult DesktopConfigInitStage::run_session() { + // 1. Read config_folder from early settings stored in InitInfoHandle + const auto& root_pos = InitInfoHandle::instance().root_position(); + if (root_pos.isEmpty()) { + return StageResult::critical_failed("DesktopConfigInit", "No root position available"); + } + + // Compute absolute config folder path: app_runtime_dir() + config_folder + const auto runtime = base::filesystem::app_runtime_dir(); + + // Read config_folder from early settings via InitInfoHandle + QString config_folder_rel = InitInfoHandle::instance().config_folder(); + if (config_folder_rel.isEmpty()) { + // When Empty, Desktop should be sucked + return StageResult::critical_failed("DesktopConfigInit", + "Config Folder is empty, this is not expected!"); + } + + QString config_folder_abs = base::filesystem::concat_filepath(runtime, config_folder_rel); + + // 2. Ensure directory exists + if (!base::filesystem::exsited(config_folder_abs)) { + if (!base::filesystem::create_anyway(config_folder_abs)) { + return StageResult::critical_failed( + "DesktopConfigInit", "Failed to create config directory: " + config_folder_abs); + } + } + + log::infoftag("DesktopConfigInit", "Config folder: {}", config_folder_abs.toStdString()); + + // 3. Create default wallpaper.json if not exists + QString wallpaper_path = config_folder_abs + "/wallpaper.json"; + if (!QFile::exists(wallpaper_path)) { + QFile file(wallpaper_path); + if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { + file.write(early_stage::WALLPAPER_CONFIG_TEMPLATE); + file.close(); + log::infoftag("DesktopConfigInit", "Created default wallpaper.json"); + } + } + + // 4. Initialize ConfigStore with path provider pointing to config folder + auto provider = std::make_shared(config_folder_abs); + cf::config::ConfigStore::instance().initialize(provider); + + log::infoftag("DesktopConfigInit", "ConfigStore initialized with app_dir: {}", + config_folder_abs.toStdString()); + + return StageResult::ok("Desktop config initialized"); +} + +} // namespace cf::desktop::init_session diff --git a/desktop/main/init/desktop_config_init/desktop_config_init.h b/desktop/main/init/desktop_config_init/desktop_config_init.h new file mode 100644 index 000000000..275342e20 --- /dev/null +++ b/desktop/main/init/desktop_config_init/desktop_config_init.h @@ -0,0 +1,47 @@ +/** + * @file desktop/main/init/desktop_config_init/desktop_config_init.h + * @brief Desktop component configuration initialization stage. + * + * Initializes ConfigStore with the desktop config folder path read from + * early settings, and creates default component config files if needed. + * + * @ingroup init_session + */ +#pragma once + +#include "init_stage.h" +#include "init_stage_name.h" + +namespace cf::desktop::init_session { + +/** + * @brief Desktop component configuration initialization stage. + * + * Initializes the ConfigStore with desktop-specific paths and creates + * default component configuration files when needed. + * + * @note None + * @since N/A + * @ingroup init_session + */ +class DesktopConfigInitStage : public IInitStage { + public: + std::string_view name() const noexcept override { return DESKTOP_CONFIG_INIT; } + + /** + * @brief Executes the configuration initialization. + * + * Reads the desktop config folder path from early settings, then + * initializes the ConfigStore and creates default config files. + * + * @return StageResult indicating success or failure. + * @throws None + * @note None + * @warning None + * @since N/A + * @ingroup init_session + */ + StageResult run_session() override; +}; + +} // namespace cf::desktop::init_session diff --git a/desktop/main/init/init_session.cpp b/desktop/main/init/init_session.cpp index dd88008cf..42f0b66e0 100644 --- a/desktop/main/init/init_session.cpp +++ b/desktop/main/init/init_session.cpp @@ -1,5 +1,6 @@ #include "cflog.h" #include "desktop_backbone_init/desktop_backbone_init.h" +#include "desktop_config_init/desktop_config_init.h" #include "early_gain/early_pass_stage.h" #include "gui_progress/gui_init_stage.h" #include "init_session_chain.h" @@ -14,6 +15,7 @@ void RunStageInit() { initSessionChain.add_stage_back(std::make_unique()); initSessionChain.add_stage_back(std::make_unique()); initSessionChain.add_stage_back(std::make_unique()); + initSessionChain.add_stage_back(std::make_unique()); } void ReleaseStageInitOldResources() { diff --git a/desktop/main/init/init_settings.cpp b/desktop/main/init/init_settings.cpp index 23c51ba5f..a0845b8a7 100644 --- a/desktop/main/init/init_settings.cpp +++ b/desktop/main/init/init_settings.cpp @@ -32,4 +32,11 @@ QString InitInfoHandle::root_position() const { return (*early_settings_)["desktop"].toObject()["root"].toString(); } +QString InitInfoHandle::config_folder() const { + if (!early_settings_) { + return {}; + } + return (*early_settings_)["desktop"].toObject()["config_folder"].toString("settings/desktop/"); +} + } // namespace cf::desktop::init_session diff --git a/desktop/main/init/init_settings.h b/desktop/main/init/init_settings.h index 91dce9630..4f9076d04 100644 --- a/desktop/main/init/init_settings.h +++ b/desktop/main/init/init_settings.h @@ -98,6 +98,15 @@ class InitInfoHandle : public SimpleSingleton { */ QString root_position() const; + /** + * @brief Get the desktop config folder path (relative to app runtime dir). + * + * Reads desktop.config_folder from early settings, defaults to "settings/desktop/". + * + * @return QString + */ + QString config_folder() const; + private: /// @brief Pointer to the boot widget. Ownership: owner; may be nullptr. QWidget* boot_widget_{nullptr}; diff --git a/desktop/main/init/init_stage_name.h b/desktop/main/init/init_stage_name.h index a75d930bb..8e725b6f3 100644 --- a/desktop/main/init/init_stage_name.h +++ b/desktop/main/init/init_stage_name.h @@ -11,4 +11,5 @@ namespace cf::desktop::init_session { static constexpr const char* CONFIG_PASS_STAGE = "Early Transform"; static constexpr const char* DESKTOP_BACKBONE_SETUP = "Desktop Backbone setup"; +static constexpr const char* DESKTOP_CONFIG_INIT = "Desktop Config Init"; } // namespace cf::desktop::init_session diff --git a/desktop/main/log/CMakeLists.txt b/desktop/main/log/CMakeLists.txt deleted file mode 100644 index 94a15baee..000000000 --- a/desktop/main/log/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# Making Components with early sessions - -add_library(CFDesktopLog STATIC) - -target_sources(CFDesktopLog PRIVATE log_helper.cpp) - -target_include_directories(CFDesktopLog PUBLIC - $ -) - -target_link_libraries(CFDesktopLog PUBLIC cflogger Qt6::Core) diff --git a/desktop/main/path/CMakeLists.txt b/desktop/main/path/CMakeLists.txt deleted file mode 100644 index 807b01a17..000000000 --- a/desktop/main/path/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -# Making Components with early sessions - -add_library(CFDesktopPathSettings STATIC) - -target_sources(CFDesktopPathSettings PRIVATE - desktop_main_path_resolvers.cpp - desktop_self_path_resolvers.cpp) - -target_include_directories(CFDesktopPathSettings PUBLIC - $ -) - -target_link_libraries(CFDesktopPathSettings PUBLIC cflogger Qt6::Core) diff --git a/desktop/main/path/desktop_self_path_resolvers.cpp b/desktop/main/path/desktop_self_path_resolvers.cpp deleted file mode 100644 index 003932ca9..000000000 --- a/desktop/main/path/desktop_self_path_resolvers.cpp +++ /dev/null @@ -1,3 +0,0 @@ -#include "desktop_self_path_resolvers.h" - -namespace cf::desktop::path {} \ No newline at end of file diff --git a/desktop/ui/CFDesktopEntity.cpp b/desktop/ui/CFDesktopEntity.cpp index 63d6b66c4..8fbae72a8 100644 --- a/desktop/ui/CFDesktopEntity.cpp +++ b/desktop/ui/CFDesktopEntity.cpp @@ -38,7 +38,7 @@ CFDesktopEntity::CFDesktopEntity() } CFDesktopEntity::~CFDesktopEntity() { - log::tracef("Dekstop Entuty is released"); + log::tracef("Desktop Entity is released, and never available again!"); } void CFDesktopEntity::release() { diff --git a/desktop/ui/components/shell_layer_impl/CMakeLists.txt b/desktop/ui/components/shell_layer_impl/CMakeLists.txt index e3cef7f38..ddd682810 100644 --- a/desktop/ui/components/shell_layer_impl/CMakeLists.txt +++ b/desktop/ui/components/shell_layer_impl/CMakeLists.txt @@ -3,7 +3,8 @@ add_library(cfdesktop_shell_layer_impl STATIC WidgetShellLayer.cpp DefaultShellLayerStrategy.cpp WallpaperShellLayerStrategy.cpp - ../wallpaper/wallpaper_setup.cpp + wallpaper_setup.cpp + wallpaper_src_chain.cpp ) target_include_directories(cfdesktop_shell_layer_impl PUBLIC @@ -18,4 +19,7 @@ target_link_libraries( cfbase cf_ui_base cfdesktop_wallpaper + cfpath + cfconfig + cffilesystem ) \ No newline at end of file diff --git a/desktop/ui/components/shell_layer_impl/wallpaper_setup.cpp b/desktop/ui/components/shell_layer_impl/wallpaper_setup.cpp new file mode 100644 index 000000000..91b6a6260 --- /dev/null +++ b/desktop/ui/components/shell_layer_impl/wallpaper_setup.cpp @@ -0,0 +1,42 @@ +#include "wallpaper_setup.h" +#include "cflog.h" +#include "filter_target.h" +#include "shell_layer_impl/WallpaperShellLayerStrategy.h" +#include "wallpaper/ImageWallPaperLayer.h" +#include "wallpaper/WallPaperAccessStorage.h" +#include "wallpaper/WallPaperToken.h" +#include "wallpaper_src_chain.h" + +namespace cf::desktop::wallpaper { + +namespace { +/** + * @brief Wallpaper Layer Inits + * + * @return std::unique_ptr + */ +std::unique_ptr make_layer() { + using namespace base::filesystem; + + auto layer = std::make_unique(); + // Resolve wallpaper pictures through policy chain + auto picture_dirent = WallpaperImages().execute(); + if (!picture_dirent.has_value() || picture_dirent->isEmpty()) { + return layer; // return out! + } + + auto pictures = filter_target(picture_dirent.value(), request_filterlist(FilterType::Pictures)); + auto storage = std::make_unique(); + storage->addTokens(WallPaperTokenFactory::fromFiles(pictures)); + log::tracef("Initialized with {} files", storage->size()); + layer->setTokenStorage(std::move(storage)); + return layer; +} + +} // namespace + +std::unique_ptr create_wallpaper_strategy() { + return std::make_unique(make_layer()); +} + +} // namespace cf::desktop::wallpaper diff --git a/desktop/ui/components/wallpaper/wallpaper_setup.h b/desktop/ui/components/shell_layer_impl/wallpaper_setup.h similarity index 100% rename from desktop/ui/components/wallpaper/wallpaper_setup.h rename to desktop/ui/components/shell_layer_impl/wallpaper_setup.h diff --git a/desktop/ui/components/shell_layer_impl/wallpaper_src_chain.cpp b/desktop/ui/components/shell_layer_impl/wallpaper_src_chain.cpp new file mode 100644 index 000000000..55df389e5 --- /dev/null +++ b/desktop/ui/components/shell_layer_impl/wallpaper_src_chain.cpp @@ -0,0 +1,34 @@ +#include "wallpaper_src_chain.h" +#include "base/policy_chain/policy_chain.hpp" +#include "cfconfig.hpp" +#include "cflog.h" +#include "cfpath/desktop_main_path_resolvers.h" +#include +#include + +namespace cf::desktop::wallpaper { +PolicyChain WallpaperImages() { + return policy_chain_builder() + .then([]() -> std::optional { + // Policy 1: Load from ConfigStore wallpaper domain + log::trace("Scanning from the config file to load wallpaper"); + auto wp = cf::config::ConfigStore::instance().domain("wallpaper"); + auto path = wp.query( + cf::config::KeyView{.group = "wallpaper", .key = "source_path"}, ""); + if (!path.empty()) { + QString qpath = QString::fromStdString(path); + log::tracef("scan out the dirent: {}", qpath.toStdString()); + if (QFileInfo::exists(qpath)) { + return qpath; + } + } + return std::nullopt; + }) + .then([]() -> std::optional { + /* If not, we use the pictures in Picture Dirent */ + return path::DesktopMainPathProvider::instance().absolutePath( + path::DesktopMainPathProvider::PathType::Pictures); + }) + .build(); +} +} // namespace cf::desktop::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 new file mode 100644 index 000000000..c5b380969 --- /dev/null +++ b/desktop/ui/components/shell_layer_impl/wallpaper_src_chain.h @@ -0,0 +1,26 @@ +/** + * @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 + * image paths for the shell layer. + * + * @author N/A + * @date N/A + * @version N/A + * @since N/A + * @ingroup none + */ + +#pragma once +#include "base/policy_chain/policy_chain.hpp" +#include + +namespace cf::desktop::wallpaper { +/** + * @brief Get the Wallpaper Load Image from here + * + * @return PolicyChain + */ +PolicyChain WallpaperImages(); +} // namespace cf::desktop::wallpaper diff --git a/desktop/ui/components/wallpaper/ImageWallPaperLayer.cpp b/desktop/ui/components/wallpaper/ImageWallPaperLayer.cpp index ff3151f8c..a4fc7a830 100644 --- a/desktop/ui/components/wallpaper/ImageWallPaperLayer.cpp +++ b/desktop/ui/components/wallpaper/ImageWallPaperLayer.cpp @@ -39,6 +39,10 @@ void ImageWallPaperLayer::setTokenStorage(std::unique_ptrstorage; // OK, Deref +} + bool ImageWallPaperLayer::showNextOne() { if (!d->storage) { return false; diff --git a/desktop/ui/components/wallpaper/ImageWallPaperLayer.h b/desktop/ui/components/wallpaper/ImageWallPaperLayer.h index 3ea7e601f..102f6f8f2 100644 --- a/desktop/ui/components/wallpaper/ImageWallPaperLayer.h +++ b/desktop/ui/components/wallpaper/ImageWallPaperLayer.h @@ -44,6 +44,20 @@ class ImageWallPaperLayer : public WallPaperLayer { void setTokenStorage(std::unique_ptr storage) override; + /** + * @brief Returns a reference to the current token storage. + * + * @return Reference to the active WallPaperAccessStorage. + * + * @throws None. + * + * @note None. + * @warning None. + * @since 0.15 + * @ingroup wallpaper + */ + WallPaperAccessStorage& tokenStorage() const override; + /** * @brief Switches to the next wallpaper in the collection. * diff --git a/desktop/ui/components/wallpaper/WallPaperAccessStorage.cpp b/desktop/ui/components/wallpaper/WallPaperAccessStorage.cpp index 3531b470c..05601fa67 100644 --- a/desktop/ui/components/wallpaper/WallPaperAccessStorage.cpp +++ b/desktop/ui/components/wallpaper/WallPaperAccessStorage.cpp @@ -52,6 +52,12 @@ void WallPaperAccessStorage::addToken(std::unique_ptr token) { wallpaper_images.push_back(std::move(token)); } +void WallPaperAccessStorage::addTokens(std::vector> tokens) { + wallpaper_images.reserve(wallpaper_images.size() + tokens.size()); + for (auto& t : tokens) + wallpaper_images.push_back(std::move(t)); +} + void WallPaperAccessStorage::insertToken(size_t index, std::unique_ptr token) { wallpaper_images.insert(wallpaper_images.begin() + static_cast(index), std::move(token)); @@ -65,4 +71,8 @@ size_t WallPaperAccessStorage::indexOf(const wallpaper_token_id_t& token) const return static_cast(it - wallpaper_images.begin()); } +size_t WallPaperAccessStorage::size() const { + return wallpaper_images.size(); +} + } // namespace cf::desktop::wallpaper diff --git a/desktop/ui/components/wallpaper/WallPaperAccessStorage.h b/desktop/ui/components/wallpaper/WallPaperAccessStorage.h index 91bb58470..b7b2d4511 100644 --- a/desktop/ui/components/wallpaper/WallPaperAccessStorage.h +++ b/desktop/ui/components/wallpaper/WallPaperAccessStorage.h @@ -74,6 +74,23 @@ class WallPaperAccessStorage : public QObject { */ void addToken(std::unique_ptr token); + /** + * @brief Appends multiple wallpaper tokens to the storage. + * + * Ownership of all tokens transfers to this storage. + * + * @param[in] tokens The wallpaper tokens to add. Moved from; empty on + * return. + * + * @throws None. + * + * @note None. + * @warning None. + * @since 0.18 + * @ingroup wallpaper + */ + void addTokens(std::vector> tokens); + /** * @brief Inserts a wallpaper token at the given index. * @@ -107,6 +124,20 @@ class WallPaperAccessStorage : public QObject { */ size_t indexOf(const wallpaper_token_id_t& token) const; + /** + * @brief Returns the number of wallpaper tokens in the storage. + * + * @return Number of stored tokens. + * + * @throws None. + * + * @note None. + * @warning None. + * @since 0.15 + * @ingroup wallpaper + */ + size_t size() const; + enum class OverFlowType { OverFlow, UnderFlow }; signals: /** diff --git a/desktop/ui/components/wallpaper/WallPaperLayer.h b/desktop/ui/components/wallpaper/WallPaperLayer.h index 93debd073..8a986db3d 100644 --- a/desktop/ui/components/wallpaper/WallPaperLayer.h +++ b/desktop/ui/components/wallpaper/WallPaperLayer.h @@ -59,6 +59,13 @@ class WallPaperLayer { */ virtual void setTokenStorage(std::unique_ptr storage) = 0; + /** + * @brief Get the token storage from wallpaper Layers + * + * @return WallPaperAccessStorage& + */ + virtual WallPaperAccessStorage& tokenStorage() const = 0; + /** * @brief Switches to the next wallpaper in the collection. * diff --git a/desktop/ui/components/wallpaper/WallPaperToken.cpp b/desktop/ui/components/wallpaper/WallPaperToken.cpp index 6c05862bd..2a004a618 100644 --- a/desktop/ui/components/wallpaper/WallPaperToken.cpp +++ b/desktop/ui/components/wallpaper/WallPaperToken.cpp @@ -1,5 +1,6 @@ #include "WallPaperToken.h" #include "base/weak_ptr/weak_ptr_factory.h" +#include #include namespace cf::desktop::wallpaper { @@ -65,19 +66,18 @@ WeakPtr WallPaperToken::getWeakPtr() const { return d->weak_ptr_factory.GetWeakPtr(); } -// ============================================================ -// WallPaperTokenFactory (no Pimpl — only 2 fields) -// ============================================================ - -WallPaperTokenFactory& WallPaperTokenFactory::fromFile(const QString& path) { - static WallPaperTokenFactory factory; - factory.stored_path_ = path; - factory.stored_type_ = WallPaperToken::SourceType::File; - return factory; +WallPaperTokenFactory::WallPaperToken_t WallPaperTokenFactory::fromFile(const QString& path) { + return std::unique_ptr( + new WallPaperToken(path, WallPaperToken::SourceType::File)); } -std::unique_ptr WallPaperTokenFactory::create() { - return std::unique_ptr(new WallPaperToken(stored_path_, stored_type_)); +std::vector +WallPaperTokenFactory::fromFiles(const QStringList& paths) { + std::vector v; + for (const auto& p : paths) { + v.emplace_back(std::move(fromFile(p))); + } + return v; } } // namespace cf::desktop::wallpaper diff --git a/desktop/ui/components/wallpaper/WallPaperToken.h b/desktop/ui/components/wallpaper/WallPaperToken.h index d0ef08f9e..bd02ceeb8 100644 --- a/desktop/ui/components/wallpaper/WallPaperToken.h +++ b/desktop/ui/components/wallpaper/WallPaperToken.h @@ -17,6 +17,7 @@ #include "base/weak_ptr/weak_ptr.h" #include #include +#include namespace cf::desktop::wallpaper { @@ -169,12 +170,38 @@ class WallPaperToken { class WallPaperTokenFactory { public: - static WallPaperTokenFactory& fromFile(const QString& path); - std::unique_ptr create(); + using WallPaperToken_t = std::unique_ptr; - private: - QString stored_path_; - WallPaperToken::SourceType stored_type_{WallPaperToken::SourceType::File}; + /** + * @brief Creates a wallpaper token from a file path. + * + * @param[in] path Filesystem path to the wallpaper image. + * + * @return A unique_ptr to the newly created WallPaperToken. + * + * @throws None. + * + * @note None. + * @warning None. + * @since 0.15 + * @ingroup wallpaper + */ + static WallPaperToken_t fromFile(const QString& path); + /** + * @brief Creates wallpaper tokens from multiple file paths. + * + * @param[in] paths List of filesystem paths to wallpaper images. + * + * @return Vector of unique_ptrs to newly created tokens. + * + * @throws None. + * + * @note None. + * @warning None. + * @since 0.15 + * @ingroup wallpaper + */ + static std::vector fromFiles(const QStringList& paths); }; } // namespace cf::desktop::wallpaper diff --git a/desktop/ui/components/wallpaper/wallpaper_setup.cpp b/desktop/ui/components/wallpaper/wallpaper_setup.cpp deleted file mode 100644 index 0859ea79d..000000000 --- a/desktop/ui/components/wallpaper/wallpaper_setup.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "wallpaper/wallpaper_setup.h" -#include "shell_layer_impl/WallpaperShellLayerStrategy.h" -#include "wallpaper/ImageWallPaperLayer.h" - -namespace cf::desktop::wallpaper { - -namespace { - -std::unique_ptr make_layer() { - return std::make_unique(); -} - -} // namespace - -std::unique_ptr create_wallpaper_strategy() { - return std::make_unique(make_layer()); -} - -} // namespace cf::desktop::wallpaper diff --git a/desktop/ui/platform/linux_wsl/linux_wsl_platform.cpp b/desktop/ui/platform/linux_wsl/linux_wsl_platform.cpp index 74085c446..8324ba25f 100644 --- a/desktop/ui/platform/linux_wsl/linux_wsl_platform.cpp +++ b/desktop/ui/platform/linux_wsl/linux_wsl_platform.cpp @@ -2,7 +2,7 @@ #include "IDesktopPropertyStrategy.h" #include "components/IShellLayerStrategy.h" #include "components/shell_layer_impl/WidgetShellLayer.h" -#include "components/wallpaper/wallpaper_setup.h" +#include "components/shell_layer_impl/wallpaper_setup.h" #include "display_backend_helper.h" #include "linux_wsl_factory.h" #include "platform_helper.h" diff --git a/desktop/ui/platform/windows/windows_platform.cpp b/desktop/ui/platform/windows/windows_platform.cpp index 77ec211a8..1da6a9813 100644 --- a/desktop/ui/platform/windows/windows_platform.cpp +++ b/desktop/ui/platform/windows/windows_platform.cpp @@ -2,7 +2,7 @@ #include "IDesktopPropertyStrategy.h" #include "components/IShellLayerStrategy.h" #include "components/shell_layer_impl/WidgetShellLayer.h" -#include "components/wallpaper/wallpaper_setup.h" +#include "components/shell_layer_impl/wallpaper_setup.h" #include "display_backend_helper.h" #include "platform_helper.h" #include "shell_layer_helper.h" diff --git a/document/.pages b/document/.pages deleted file mode 100644 index 56f65c34d..000000000 --- a/document/.pages +++ /dev/null @@ -1,12 +0,0 @@ -title: 首页 -nav: - - HandBook - - Development: development - - Desktop: desktop - - CI/CD: ci - - Scripts: scripts - - Design Stage: design_stage - - Notes: notes - - TODO: todo - - Optimize: optimize - - Release Rule: release_rule diff --git a/document/DOXYGEN_REQUEST.md b/document/DOXYGEN_REQUEST.md index dc1a556ce..44e283816 100644 --- a/document/DOXYGEN_REQUEST.md +++ b/document/DOXYGEN_REQUEST.md @@ -1,390 +1,395 @@ -# prompt.md — Doxygen comment generation specification (English) - -> Purpose: a strict, machine-readable yet human-friendly prompt that instructs an AI to generate idiomatic, high-quality Doxygen comments for modern C++ code (C, C++20/23, templates, concepts, macros, embedded/OS code). Use this file as the contract that the generator must follow. -> Tone: concise technical (LLVM/Qt-like), third-person present tense, with small tasteful flourishes allowed to avoid robotic dryness — but **never** introduce ambiguity or invent behavior. - ---- - -## 1 — Scope & high-level rules (MUST / MUST NOT) - -* **MUST** produce Doxygen comments in English. -* **MUST** use **third-person present tense** (e.g., “Initializes the cache.”). -* **MUST NOT** use first-person (“we”, “I”, “our”) except within `@note` when explicitly allowed for brief clarifications. -* **MUST** support both block style (`/** ... */`) and line style (`/// ...`) comments. - - * **MUST** be **consistent per file**: choose either block or line style within that file and use it for all declarations in that file. -* **MUST** include a **file-level** Doxygen header at the top of every source and header file. -* **MUST** document **public** and **protected** APIs. Private functions may be documented but are optional. -* **MUST** document **every enum, struct, class, union, typedef, and non-static data member**. -* **MUST** document **every public and protected function**; constructors, destructors, and operators included. -* **MUST** include the following tags for every function (if not applicable, write `@return None` or `@throws None` as described below): - `@brief`, `@param`, `@return`, `@note`, `@warning`, `@throws`, `@since`, `@ingroup`. -* **MUST** include parameter **direction** for every `@param`: `@param[in]`, `@param[out]`, or `@param[in,out]`. -* **MUST** list `@param` entries in the **same order** as the function signature. -* **MUST** explain **units and ranges** for numeric parameters where applicable (e.g., “milliseconds”, “bytes”, “MHz”). -* **MUST** document **default values** for parameters when the code provides defaults. -* **MUST** include `@return` for non-void functions. - **MUST NOT** include `@return` for `void` functions (explicitly forbidden). -* **MUST** include `@since` (use provided release/version or `since: N/A` if unknown). -* **MUST** include `@ingroup` when the symbol belongs to a module or logical group; otherwise set `@ingroup none`. -* **MUST** document template parameters (`@tparam`) and concept constraints when generating comments for templates. -* **MUST** include thread-safety statements **only if present in the code or explicitly stated in repo metadata**. **Do not assume thread safety**. -* **MUST** be conservative with `@throws` — only list exceptions observable in the function body or documented upstream; if none, write `@throws None`. -* **MUST** avoid inventing behavioral guarantees not reflected by the code. If uncertain, **add a `FIXME` note** (see Error Handling section). -* **MUST** follow the style/length/formatting rules in Section 4. - ---- - -## 2 — File-level header (required) - -Every file must start with a file-level block like: - -```cpp -/** - * @file relative/path/to/file.h - * @brief One-sentence summary of the file's responsibility. - * - * Longer one-paragraph description (optional, max 2–3 short sentences). - * - * @author - * @date - * @version - * @since - * @ingroup - */ -``` - -* Fill `@author`, `@date`, `@version` from git metadata if available; otherwise set to `"N/A"`. -* Keep the file-level description concise (≤ 2–3 short sentences). - ---- - -## 3 — Function & method comment template (MUST follow) - -* Use this canonical template. If a tag is not applicable, include it with `None` as the value (so the presence of the tag is guaranteed and the output is machine-validateable). - -Block style example: - -```cpp -/** - * @brief Short description in third-person present tense. - * - * Detailed description (optional). If present, keep to a few short sentences. - * - * @param[in] name Description. Specify units and valid range when applicable. - * @param[out] out Description. If pointer, explain ownership semantics. - * @param[in,out] buf Description. Mention buffer length and units if relevant. - * @tparam T Description of template parameter T (if applicable). - * @return Describe return value. If not applicable (void), omit @return. - * @throws List exceptions thrown, or `None`. - * @note Short clarifications, constraints, or references. - * @warning Short warnings (e.g., reentrancy, performance). - * @since Version or "N/A". - * @ingroup Module name or "none". - */ -``` - -Line-style equivalent: - -```cpp -/// @brief Short description. -/// @details Optional extended description in third-person present tense. -/// @param[in] name Description... -/// @return Description... -``` - -* **MUST** include `@tparam` for templates. -* **MUST** include `@throws` (or `@throws None`). -* **MUST** include `@note` (or `@note None`) and `@warning` (or `@warning None`). -* **MUST** not use `@return` for `void` functions. - ---- - -## 4 — Formatting constraints (MUST) - -* **Line width**: every line inside the comment must be **≤ 100 characters**. -* **Whitespace & alignment**: - - * If using block style (`/** ... */`) the leading `*` must be vertically aligned (common column) for the entire block. - * If using line style (`///`) each `///` must start at the same column for that file. -* **Padding & blank lines**: - - * Single blank line allowed between `@brief` paragraph and the tag block. - * No more than one consecutive blank line inside a comment block. -* **Sentence style**: start with a capital letter; end with a period. -* **Examples**: Use `@code` / `@endcode` for short usage examples (max 6 lines). -* **Language mechanics**: use simple present tense verbs (e.g., “Returns”, “Initializes”, “Parses”). -* **Maximum tag line length**: the text following a tag should be wrapped so that the whole line remains ≤100 chars. -* **Consistency**: within a file, keep tag ordering stable: `@brief` → extended desc (optional) → `@param` / `@tparam` (in signature order) → `@return` → `@throws` → `@note` → `@warning` → `@since` → `@ingroup`. - ---- - -## 5 — Special cases & advanced language features - -### Templates / Concepts - -* **MUST** annotate template parameters with `@tparam` and describe constraints. -* For constrained templates, explicitly state concept requirements in `@tparam` (e.g., `@tparam T Must satisfy std::integral`). - -### Constexpr / inline / noexcept - -* Document behavior and compile-time guarantees. E.g., `@note This function is constexpr and may be evaluated at compile time.` - -### Operators / Conversions - -* Use `@brief` to state semantic meaning; include `@note` for subtlety (e.g., cost, aliasing). - -### Macros - -* Document the macro’s purpose, expected arguments, side effects, and recommended alternatives (if any). - -### Exceptions - -* Only document exceptions that can be thrown directly or via called functions that are not `noexcept`. If none, `@throws None`. - ---- - -## 6 — Class & struct documentation (MUST) - -Each class/struct must have: - -* `@brief` one-line design purpose. -* `@details` short paragraph for lifetime, ownership, and common usage. -* `@note` thread-safety note (only if known). -* `@code` example that demonstrates typical usage (required, max 6 lines). -* Document **public/protected** member functions and **all non-static data members** (brief for members, fuller for functions). - -Class example: - -```cpp -/** - * @brief Lightweight ring buffer for bytes. - * - * Provides single-producer single-consumer semantics. Does not allocate after - * construction. - * - * @note Not thread-safe unless externally synchronized. - * - * @ingroup utils - * - * @code - * RingBuffer rb(1024); - * rb.push(...); - * @endcode - */ -class RingBuffer { ... }; -``` - ---- - -## 7 — Enums & constants (MUST) - -* Every `enum` and `enum class` must have: - - * `@brief` describing the enum's role. - * Each enumerator must have a one-line trailing comment or an `@enum` block listing names and meanings. -* Constants (`constexpr`, `const`) must have a `@brief` and unit (if numeric). - -Example enumerator: - -```cpp -/** - * @brief Device power states. - * @ingroup driver - */ -enum class PowerState { - Off, ///< Power is off. - Sleep, ///< Low-power sleep mode. - On ///< Fully powered. -}; -``` - ---- - -## 8 — Member variable documentation (MUST) - -* **MUST** document every non-static data member. -* Specify ownership (`owner`, `observer`, `borrowed`), lifetime, and whether it may be `nullptr`. -* Keep member comments short (one sentence). - -Example: - -```cpp -/// @brief Pointer to underlying device context. Ownership: observer; may be nullptr. -DeviceContext* ctx_; -``` - ---- - -## 9 — Tags that must always appear and how to handle “not applicable” - -Because the downstream tooling expects a predictable set of tags, **always include** the following for functions and classes: - -* `@brief` (never `None`) -* `@param` (for each parameter; if none, omit the block) -* `@return` (for non-void); **do not** add for void -* `@throws` — use `@throws None` if none. -* `@note` — use `@note None` if none. -* `@warning` — use `@warning None` if none. -* `@since` — `N/A` if unknown. -* `@ingroup` — `none` if not part of a group. - ---- - -## 10 — Error handling & ambiguous code (MUST) - -If the generator cannot determine meaning from code: - -* **Prefer conservative language** (e.g., “May return -1 on error.” → only if code shows that). -* **Add a `FIXME` line** in the `@note` with a short reason why the annotation is uncertain and what is needed (e.g., “FIXME: ownership unclear — confirm by inspecting caller or README”). -* **Do not invent behavior** (e.g., do not claim that a function throws `std::runtime_error` unless visible). -* If the code uses custom error codes rather than exceptions, document the error-code set and mapping where visible; otherwise `@throws None` and `@note FIXME: error-code mapping unclear.` - ---- - -## 11 — Allowable creative flourishes - -* Short, tasteful explanatory text in `@note` is allowed to reduce dryness, but **must** be factual and concise. -* No rhetorical or marketing language. Keep it professional. -* Examples and short code snippets are encouraged for classes and public APIs. - ---- - -## 12 — Validation rules (automatic checks — MUST run after generation) - -The generator must pass the following checks. If any check fails, the generator must **rewrite** the comment until all pass. - -1. **File header present**: regex `(?s)/\*\*.*@file\s+.+\*/` at file start. -2. **Per-file style consistency**: all comment blocks use `/**` or all `///`. -3. **Required tags presence**: each function/class comment contains `@brief` and `@since` and `@ingroup`. -4. **Parameter order & presence**: number and names of `@param` entries match function signature in order. - - * Validate with a parser or a conservative regex-based check. -5. **@param directions present**: every `@param` has `[in]`, `[out]`, or `[in,out]`. -6. **Void functions must NOT contain `@return`**: fail if `@return` appears in void function. -7. **Line length**: no line inside comment > 100 chars. -8. **Star / prefix alignment**: for block style, each interior line must start with `*` (space-star-space) after the opening `/**`. -9. **Enumerations & members documented**: every `enum`/`struct`/non-static member must have an adjacent doc comment. -10. **Template params annotated**: presence of `@tparam` for templates. -11. **@throws presence**: must exist (with `None` if not applicable). -12. **No first-person**: fail if comment contains `\b(we|our|I|my)\b` outside quoted text. -13. **Third-person present tense**: spot-check verbs like “initializes”, “returns”, “parses”; if glaringly different (e.g., “will initialize”), flag and rewrite. -14. **FIXME handling**: if `FIXME` is present, ensure it's in `@note` and explains the ambiguity. - -Provide exact failure messages for each check so the generator can iterate. - ---- - -## 13 — Good / Bad examples - -### Good (function) - -```cpp -/** - * @brief Parses a little-endian unsigned integer from `buf`. - * - * Parses exactly `len` bytes and returns the resulting value. If `len` is 0, - * behavior is undefined. - * - * @param[in] buf Pointer to input bytes. Must be non-null. - * @param[in] len Number of bytes to parse. Unit: bytes. Valid range: 1..8. - * @return Parsed 64-bit unsigned integer. - * @throws None - * @note This function does not perform bounds checking on the - * caller-provided buffer. - * @warning Behavior is undefined for len==0. - * @since 1.2.0 - * @ingroup util - */ -uint64_t parse_le_uint(const uint8_t* buf, size_t len); -``` - -### Bad (function) - -```cpp -/** Parses bytes into a number. This function will parse and return value. */ -uint64_t parse_le_uint(const uint8_t* buf, size_t len); -``` - -* Issues: first-person / future tense; no tags; no units; no param directions; too short; possibly misleading. - ---- - -## 14 — Implementation notes for the generator (instructions for the AI implementing this prompt) - -* Parse the source file to extract declarations and signatures before generating comments. Prefer an AST when available; if not, use robust regexes with conservative fallbacks. -* For each symbol: - - * Generate tags following the canonical ordering (Section 4). - * Fill `@throws`, `@note`, `@warning` with `None` if inapplicable. - * For templates, include `@tparam` and describe constraints. - * For each parameter, inspect type to infer direction: - - * pointer to non-const ⇒ likely `@param[out]` or `@param[in,out]` depending on name and usage (if unknown, prefer `in,out` and add a `FIXME`). - * const pointer/reference ⇒ `@param[in]`. - * return by non-const reference ⇒ usually `@param[out]` for the parameter and `@return` for function when appropriate — be conservative, and add `FIXME` if ambiguous. -* Ownership language: if variable name contains `owner`/`owns`/`unique_ptr`/`std::unique_ptr`, document as `owner`. If raw pointer and no visible ownership, document as `observer`. -* Thread-safety: include explicit statement only when code or repo docs indicate it. Otherwise include `@note Not thread-safe unless externally synchronized.` where appropriate. -* When inferring error codes or exceptional behavior, prefer **not** to claim exceptions. If the code returns sentinel values (e.g., `-1`), document the sentinel only if visible in the implementation or header comment. -* Do not consult external knowledge to invent behavior. Use only repository artifacts and inline code context. - ---- - -## 15 — Git metadata & author fields - -* If the agent has access to git via an external tool, populate: - - * `@author` ← git `--format="%an"` - * `@date` ← date of last commit touching the file (ISO 8601) - * `@version` ← last tag or short commit hash -* If no git info available, set fields to `"N/A"`. - ---- - -## 16 — Output quality & rewrite policy (strictness) - -* This prompt is **strong-constraint**: any deviation from mandatory checks must trigger an automatic rewrite. -* The generator should attempt up to **3** automatic refinement passes per file to satisfy the validation rules before returning results to the user. -* If, after 3 passes, the code still fails validation due to ambiguous code, include `FIXME` notes and a short summary explaining what could not be determined and why. - ---- - -## 17 — Example workflow for the AI consumer (summary) - -1. Parse file → collect symbols and signatures. -2. Generate file-level header. -3. For each symbol generate comment block per the templates. -4. Run validation checks (Section 12). -5. If checks fail, rewrite (up to 3 passes). If still failing, add `FIXME` and explanation. -6. Return the modified file with comments inserted, and a short machine-readable report of checks (JSON recommended). - ---- - -## 18 — Machine-readable report format (recommended) - -Return an object with fields: - -```json -{ - "file": "path/to/file.h", - "generated_comments": N, - "errors": [ - {"symbol": "foo()", "check": "line_length", "message": "line > 100 chars"} - ], - "fixme_count": M -} -``` - ---- - -## 19 — Quick checklist for reviewers (human) - -* Is `@brief` clear and in present tense? -* Do `@param` entries match signature order and include direction? -* Is `@return` present only for non-void functions? -* Are units / ranges specified where applicable? -* Are enums, structs, and members documented? -* Are lines ≤ 100 chars and stars aligned? -* Are any `FIXME` notes present? If so, are they justified? - +--- +title: "prompt.md — Doxygen comment generation specification (English)" +description: "Doxygen 注释生成规范,定义 AI 生成 C++ 代码文档的格式和风格要求" +--- + +# prompt.md — Doxygen comment generation specification (English) + +> Purpose: a strict, machine-readable yet human-friendly prompt that instructs an AI to generate idiomatic, high-quality Doxygen comments for modern C++ code (C, C++20/23, templates, concepts, macros, embedded/OS code). Use this file as the contract that the generator must follow. +> Tone: concise technical (LLVM/Qt-like), third-person present tense, with small tasteful flourishes allowed to avoid robotic dryness — but **never** introduce ambiguity or invent behavior. + +--- + +## 1 — Scope & high-level rules (MUST / MUST NOT) + +* **MUST** produce Doxygen comments in English. +* **MUST** use **third-person present tense** (e.g., “Initializes the cache.”). +* **MUST NOT** use first-person (“we”, “I”, “our”) except within `@note` when explicitly allowed for brief clarifications. +* **MUST** support both block style (`/** ... */`) and line style (`/// ...`) comments. + + * **MUST** be **consistent per file**: choose either block or line style within that file and use it for all declarations in that file. +* **MUST** include a **file-level** Doxygen header at the top of every source and header file. +* **MUST** document **public** and **protected** APIs. Private functions may be documented but are optional. +* **MUST** document **every enum, struct, class, union, typedef, and non-static data member**. +* **MUST** document **every public and protected function**; constructors, destructors, and operators included. +* **MUST** include the following tags for every function (if not applicable, write `@return None` or `@throws None` as described below): + `@brief`, `@param`, `@return`, `@note`, `@warning`, `@throws`, `@since`, `@ingroup`. +* **MUST** include parameter **direction** for every `@param`: `@param[in]`, `@param[out]`, or `@param[in,out]`. +* **MUST** list `@param` entries in the **same order** as the function signature. +* **MUST** explain **units and ranges** for numeric parameters where applicable (e.g., “milliseconds”, “bytes”, “MHz”). +* **MUST** document **default values** for parameters when the code provides defaults. +* **MUST** include `@return` for non-void functions. + **MUST NOT** include `@return` for `void` functions (explicitly forbidden). +* **MUST** include `@since` (use provided release/version or `since: N/A` if unknown). +* **MUST** include `@ingroup` when the symbol belongs to a module or logical group; otherwise set `@ingroup none`. +* **MUST** document template parameters (`@tparam`) and concept constraints when generating comments for templates. +* **MUST** include thread-safety statements **only if present in the code or explicitly stated in repo metadata**. **Do not assume thread safety**. +* **MUST** be conservative with `@throws` — only list exceptions observable in the function body or documented upstream; if none, write `@throws None`. +* **MUST** avoid inventing behavioral guarantees not reflected by the code. If uncertain, **add a `FIXME` note** (see Error Handling section). +* **MUST** follow the style/length/formatting rules in Section 4. + +--- + +## 2 — File-level header (required) + +Every file must start with a file-level block like: + +```cpp +/** + * @file relative/path/to/file.h + * @brief One-sentence summary of the file's responsibility. + * + * Longer one-paragraph description (optional, max 2–3 short sentences). + * + * @author + * @date + * @version + * @since + * @ingroup + */ +```yaml + +* Fill `@author`, `@date`, `@version` from git metadata if available; otherwise set to `"N/A"`. +* Keep the file-level description concise (≤ 2–3 short sentences). + +--- + +## 3 — Function & method comment template (MUST follow) + +* Use this canonical template. If a tag is not applicable, include it with `None` as the value (so the presence of the tag is guaranteed and the output is machine-validateable). + +Block style example: + +```cpp +/** + * @brief Short description in third-person present tense. + * + * Detailed description (optional). If present, keep to a few short sentences. + * + * @param[in] name Description. Specify units and valid range when applicable. + * @param[out] out Description. If pointer, explain ownership semantics. + * @param[in,out] buf Description. Mention buffer length and units if relevant. + * @tparam T Description of template parameter T (if applicable). + * @return Describe return value. If not applicable (void), omit @return. + * @throws List exceptions thrown, or `None`. + * @note Short clarifications, constraints, or references. + * @warning Short warnings (e.g., reentrancy, performance). + * @since Version or "N/A". + * @ingroup Module name or "none". + */ +```text + +Line-style equivalent: + +```cpp +/// @brief Short description. +/// @details Optional extended description in third-person present tense. +/// @param[in] name Description... +/// @return Description... +```yaml + +* **MUST** include `@tparam` for templates. +* **MUST** include `@throws` (or `@throws None`). +* **MUST** include `@note` (or `@note None`) and `@warning` (or `@warning None`). +* **MUST** not use `@return` for `void` functions. + +--- + +## 4 — Formatting constraints (MUST) + +* **Line width**: every line inside the comment must be **≤ 100 characters**. +* **Whitespace & alignment**: + + * If using block style (`/** ... */`) the leading `*` must be vertically aligned (common column) for the entire block. + * If using line style (`///`) each `///` must start at the same column for that file. +* **Padding & blank lines**: + + * Single blank line allowed between `@brief` paragraph and the tag block. + * No more than one consecutive blank line inside a comment block. +* **Sentence style**: start with a capital letter; end with a period. +* **Examples**: Use `@code` / `@endcode` for short usage examples (max 6 lines). +* **Language mechanics**: use simple present tense verbs (e.g., “Returns”, “Initializes”, “Parses”). +* **Maximum tag line length**: the text following a tag should be wrapped so that the whole line remains ≤100 chars. +* **Consistency**: within a file, keep tag ordering stable: `@brief` → extended desc (optional) → `@param` / `@tparam` (in signature order) → `@return` → `@throws` → `@note` → `@warning` → `@since` → `@ingroup`. + +--- + +## 5 — Special cases & advanced language features + +### Templates / Concepts + +* **MUST** annotate template parameters with `@tparam` and describe constraints. +* For constrained templates, explicitly state concept requirements in `@tparam` (e.g., `@tparam T Must satisfy std::integral`). + +### Constexpr / inline / noexcept + +* Document behavior and compile-time guarantees. E.g., `@note This function is constexpr and may be evaluated at compile time.` + +### Operators / Conversions + +* Use `@brief` to state semantic meaning; include `@note` for subtlety (e.g., cost, aliasing). + +### Macros + +* Document the macro’s purpose, expected arguments, side effects, and recommended alternatives (if any). + +### Exceptions + +* Only document exceptions that can be thrown directly or via called functions that are not `noexcept`. If none, `@throws None`. + +--- + +## 6 — Class & struct documentation (MUST) + +Each class/struct must have: + +* `@brief` one-line design purpose. +* `@details` short paragraph for lifetime, ownership, and common usage. +* `@note` thread-safety note (only if known). +* `@code` example that demonstrates typical usage (required, max 6 lines). +* Document **public/protected** member functions and **all non-static data members** (brief for members, fuller for functions). + +Class example: + +```cpp +/** + * @brief Lightweight ring buffer for bytes. + * + * Provides single-producer single-consumer semantics. Does not allocate after + * construction. + * + * @note Not thread-safe unless externally synchronized. + * + * @ingroup utils + * + * @code + * RingBuffer rb(1024); + * rb.push(...); + * @endcode + */ +class RingBuffer { ... }; +```yaml + +--- + +## 7 — Enums & constants (MUST) + +* Every `enum` and `enum class` must have: + + * `@brief` describing the enum's role. + * Each enumerator must have a one-line trailing comment or an `@enum` block listing names and meanings. +* Constants (`constexpr`, `const`) must have a `@brief` and unit (if numeric). + +Example enumerator: + +```cpp +/** + * @brief Device power states. + * @ingroup driver + */ +enum class PowerState { + Off, ///< Power is off. + Sleep, ///< Low-power sleep mode. + On ///< Fully powered. +}; +```yaml + +--- + +## 8 — Member variable documentation (MUST) + +* **MUST** document every non-static data member. +* Specify ownership (`owner`, `observer`, `borrowed`), lifetime, and whether it may be `nullptr`. +* Keep member comments short (one sentence). + +Example: + +```cpp +/// @brief Pointer to underlying device context. Ownership: observer; may be nullptr. +DeviceContext* ctx_; +```yaml + +--- + +## 9 — Tags that must always appear and how to handle “not applicable” + +Because the downstream tooling expects a predictable set of tags, **always include** the following for functions and classes: + +* `@brief` (never `None`) +* `@param` (for each parameter; if none, omit the block) +* `@return` (for non-void); **do not** add for void +* `@throws` — use `@throws None` if none. +* `@note` — use `@note None` if none. +* `@warning` — use `@warning None` if none. +* `@since` — `N/A` if unknown. +* `@ingroup` — `none` if not part of a group. + +--- + +## 10 — Error handling & ambiguous code (MUST) + +If the generator cannot determine meaning from code: + +* **Prefer conservative language** (e.g., “May return -1 on error.” → only if code shows that). +* **Add a `FIXME` line** in the `@note` with a short reason why the annotation is uncertain and what is needed (e.g., “FIXME: ownership unclear — confirm by inspecting caller or README”). +* **Do not invent behavior** (e.g., do not claim that a function throws `std::runtime_error` unless visible). +* If the code uses custom error codes rather than exceptions, document the error-code set and mapping where visible; otherwise `@throws None` and `@note FIXME: error-code mapping unclear.` + +--- + +## 11 — Allowable creative flourishes + +* Short, tasteful explanatory text in `@note` is allowed to reduce dryness, but **must** be factual and concise. +* No rhetorical or marketing language. Keep it professional. +* Examples and short code snippets are encouraged for classes and public APIs. + +--- + +## 12 — Validation rules (automatic checks — MUST run after generation) + +The generator must pass the following checks. If any check fails, the generator must **rewrite** the comment until all pass. + +1. **File header present**: regex `(?s)/\*\*.*@file\s+.+\*/` at file start. +2. **Per-file style consistency**: all comment blocks use `/**` or all `///`. +3. **Required tags presence**: each function/class comment contains `@brief` and `@since` and `@ingroup`. +4. **Parameter order & presence**: number and names of `@param` entries match function signature in order. + + * Validate with a parser or a conservative regex-based check. +5. **@param directions present**: every `@param` has `[in]`, `[out]`, or `[in,out]`. +6. **Void functions must NOT contain `@return`**: fail if `@return` appears in void function. +7. **Line length**: no line inside comment > 100 chars. +8. **Star / prefix alignment**: for block style, each interior line must start with `*` (space-star-space) after the opening `/**`. +9. **Enumerations & members documented**: every `enum`/`struct`/non-static member must have an adjacent doc comment. +10. **Template params annotated**: presence of `@tparam` for templates. +11. **@throws presence**: must exist (with `None` if not applicable). +12. **No first-person**: fail if comment contains `\b(we|our|I|my)\b` outside quoted text. +13. **Third-person present tense**: spot-check verbs like “initializes”, “returns”, “parses”; if glaringly different (e.g., “will initialize”), flag and rewrite. +14. **FIXME handling**: if `FIXME` is present, ensure it's in `@note` and explains the ambiguity. + +Provide exact failure messages for each check so the generator can iterate. + +--- + +## 13 — Good / Bad examples + +### Good (function) + +```cpp +/** + * @brief Parses a little-endian unsigned integer from `buf`. + * + * Parses exactly `len` bytes and returns the resulting value. If `len` is 0, + * behavior is undefined. + * + * @param[in] buf Pointer to input bytes. Must be non-null. + * @param[in] len Number of bytes to parse. Unit: bytes. Valid range: 1..8. + * @return Parsed 64-bit unsigned integer. + * @throws None + * @note This function does not perform bounds checking on the + * caller-provided buffer. + * @warning Behavior is undefined for len==0. + * @since 1.2.0 + * @ingroup util + */ +uint64_t parse_le_uint(const uint8_t* buf, size_t len); +```text + +### Bad (function) + +```cpp +/** Parses bytes into a number. This function will parse and return value. */ +uint64_t parse_le_uint(const uint8_t* buf, size_t len); +```yaml + +* Issues: first-person / future tense; no tags; no units; no param directions; too short; possibly misleading. + +--- + +## 14 — Implementation notes for the generator (instructions for the AI implementing this prompt) + +* Parse the source file to extract declarations and signatures before generating comments. Prefer an AST when available; if not, use robust regexes with conservative fallbacks. +* For each symbol: + + * Generate tags following the canonical ordering (Section 4). + * Fill `@throws`, `@note`, `@warning` with `None` if inapplicable. + * For templates, include `@tparam` and describe constraints. + * For each parameter, inspect type to infer direction: + + * pointer to non-const ⇒ likely `@param[out]` or `@param[in,out]` depending on name and usage (if unknown, prefer `in,out` and add a `FIXME`). + * const pointer/reference ⇒ `@param[in]`. + * return by non-const reference ⇒ usually `@param[out]` for the parameter and `@return` for function when appropriate — be conservative, and add `FIXME` if ambiguous. +* Ownership language: if variable name contains `owner`/`owns`/`unique_ptr`/`std::unique_ptr`, document as `owner`. If raw pointer and no visible ownership, document as `observer`. +* Thread-safety: include explicit statement only when code or repo docs indicate it. Otherwise include `@note Not thread-safe unless externally synchronized.` where appropriate. +* When inferring error codes or exceptional behavior, prefer **not** to claim exceptions. If the code returns sentinel values (e.g., `-1`), document the sentinel only if visible in the implementation or header comment. +* Do not consult external knowledge to invent behavior. Use only repository artifacts and inline code context. + +--- + +## 15 — Git metadata & author fields + +* If the agent has access to git via an external tool, populate: + + * `@author` ← git `--format="%an"` + * `@date` ← date of last commit touching the file (ISO 8601) + * `@version` ← last tag or short commit hash +* If no git info available, set fields to `"N/A"`. + +--- + +## 16 — Output quality & rewrite policy (strictness) + +* This prompt is **strong-constraint**: any deviation from mandatory checks must trigger an automatic rewrite. +* The generator should attempt up to **3** automatic refinement passes per file to satisfy the validation rules before returning results to the user. +* If, after 3 passes, the code still fails validation due to ambiguous code, include `FIXME` notes and a short summary explaining what could not be determined and why. + +--- + +## 17 — Example workflow for the AI consumer (summary) + +1. Parse file → collect symbols and signatures. +2. Generate file-level header. +3. For each symbol generate comment block per the templates. +4. Run validation checks (Section 12). +5. If checks fail, rewrite (up to 3 passes). If still failing, add `FIXME` and explanation. +6. Return the modified file with comments inserted, and a short machine-readable report of checks (JSON recommended). + +--- + +## 18 — Machine-readable report format (recommended) + +Return an object with fields: + +```json +{ + "file": "path/to/file.h", + "generated_comments": N, + "errors": [ + {"symbol": "foo()", "check": "line_length", "message": "line > 100 chars"} + ], + "fixme_count": M +} +```yaml + +--- + +## 19 — Quick checklist for reviewers (human) + +* Is `@brief` clear and in present tense? +* Do `@param` entries match signature order and include direction? +* Is `@return` present only for non-void functions? +* Are units / ranges specified where applicable? +* Are enums, structs, and members documented? +* Are lines ≤ 100 chars and stars aligned? +* Are any `FIXME` notes present? If so, are they justified? + --- \ No newline at end of file diff --git a/document/HandBook/.pages b/document/HandBook/.pages deleted file mode 100644 index adbe901e0..000000000 --- a/document/HandBook/.pages +++ /dev/null @@ -1,9 +0,0 @@ -title: 开发手册 -icon: material/book-open-page-variant -nav: - - 基础工具库: base - - 桌面模块: desktop - - UI 框架: ui - - API 参考: api - - 平台实现: implementation - - 示例: examples diff --git a/document/HandBook/api/.pages b/document/HandBook/api/.pages deleted file mode 100644 index 836d1c135..000000000 --- a/document/HandBook/api/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: API 参考 -icon: material/api -nav: - - system diff --git a/document/HandBook/api/system/.pages b/document/HandBook/api/system/.pages deleted file mode 100644 index b1392c022..000000000 --- a/document/HandBook/api/system/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: 系统信息 -nav: - - CPU: cpu - - 内存: memory diff --git a/document/HandBook/api/system/cpu/.pages b/document/HandBook/api/system/cpu/.pages deleted file mode 100644 index cdf7015cf..000000000 --- a/document/HandBook/api/system/cpu/.pages +++ /dev/null @@ -1,6 +0,0 @@ -title: CPU -nav: - - 概览: overview.md - - CFCpu: cfcpu.md - - CFCpu Bonus: cfcpu_bonus.md - - 性能分析: cfcpu_profile.md diff --git a/document/HandBook/api/system/memory/.pages b/document/HandBook/api/system/memory/.pages deleted file mode 100644 index 082af6ce1..000000000 --- a/document/HandBook/api/system/memory/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: 内存 -nav: - - index.md - - 内存信息: memory_info.md diff --git a/document/HandBook/base/.pages b/document/HandBook/base/.pages deleted file mode 100644 index b85f86d59..000000000 --- a/document/HandBook/base/.pages +++ /dev/null @@ -1,18 +0,0 @@ -title: 基础工具库 -icon: material/toolbox -nav: - - 总览: overview.md - - expected 错误处理: expected.md - - span 视图: span.md - - 宏工具: macros.md - - 宏定义: macro - - Linux 工具: linux - - 单例模式: singleton.md - - 作用域守卫: scope_guard.md - - 工厂模式: factory.md - - 哈希: hash.md - - MPSC 队列: mpsc_queue.md - - 一次初始化: once_init.md - - 策略链: policy_chain.md - - 弱引用: weak_ptr.md - - 弱引用工厂: weak_ptr_factory.md diff --git a/document/HandBook/base/expected.md b/document/HandBook/base/expected.md index 7f2205d42..8a00c1191 100644 --- a/document/HandBook/base/expected.md +++ b/document/HandBook/base/expected.md @@ -1,263 +1,268 @@ -# expected - 错误处理 - -`expected` 是 C++23 引入的错误处理模板,我们提供了一份 C++17 实现。核心思想很简单——用返回值显式表示"可能失败的操作",而不是靠异常把错误往外扔。这个设计特别适合我们这种需要跨平台、甚至在嵌入式环境下跑的代码,毕竟很多嵌入式编译器压根不支持异常,而且开启异常后二进制体积会显著膨胀。 - -## 为什么需要 expected - -传统的 C++ 错误处理无非就几条路:抛异常、返回错误码、用输出参数。但这几条路都有自己的问题。 - -```cpp -// 方案一:异常 -// 问题:很多环境禁用异常,且异常的开销不明确 -std::ifstream open_file(const std::string& path) { - std::ifstream file(path); - if (!file) { - throw std::runtime_error("Failed to open file"); - } - return file; -} - -// 方案二:错误码 -// 问题:错误码可能被忽略,且无法携带返回值 -bool open_file(const std::string& path, std::ifstream& out) { - std::ifstream file(path); - if (!file) return false; - out = std::move(file); - return true; -} -// 调用方可能忘记检查返回值 -open_file("data.txt", file); // 忘记检查,静默失败 - -// 方案三:expected -// 问题:几乎没有,错误必须被显式处理 -cf::expected open_file(const std::string& path) { - std::ifstream file(path); - if (!file) { - return cf::unexpected("Failed to open file: " + path); - } - return file; -} -``` - -`expected` 强制调用者处理错误——你想拿到值,就必须先检查有没有错误。而且类型系统会帮你看住:一个 `expected` 要么包含 `int`,要么包含 `ErrorCode`,不可能同时存在或都不存在。 - -## 基本用法 - -创建一个 `expected` 有两种方式:直接返回值,或者返回 `unexpected`: - -```cpp -#include "base/expected/expected.hpp" - -enum class ParseError { InvalidInput, Overflow }; - -cf::expected parse_number(std::string_view str) { - if (str.empty()) { - return cf::unexpected(ParseError::InvalidInput); - } - - try { - return std::stoi(std::string(str)); - } catch (const std::out_of_range&) { - return cf::unexpected(ParseError::Overflow); - } -} -``` - -调用方需要检查结果: - -```cpp -auto result = parse_number("42"); - -if (result.has_value()) { - std::cout << "解析成功: " << result.value() << std::endl; -} else { - switch (result.error()) { - case ParseError::InvalidInput: - std::cout << "输入为空" << std::endl; - break; - case ParseError::Overflow: - std::cout << "数值溢出" << std::endl; - break; - } -} -``` - -⚠️ 如果不检查直接调用 `value()`,会抛出 `bad_expected_access` 异常。但这个异常是你在"错误地使用 expected"时才抛出的,和业务逻辑异常是两码事。正常流程下,`expected` 的使用是不抛异常的。 - -## 值访问方式 - -`expected` 提供了多种访问值的方式,取决于你想怎么处理错误情况: - -```cpp -cf::expected result = /* ... */; - -// 方式一:先检查再访问 -if (result) { - int value = *result; // operator* - int value2 = result.value(); // value() 方法 -} - -// 方式二:提供默认值 -int value = result.value_or(-1); // 如果是错误状态,返回 -1 - -// 方式三:直接访问(如果确实是错误状态,会抛异常) -int value = result.value(); // 可能抛 bad_expected_access -``` - -`operator*` 和 `operator->` 的行为类似指针,但不做边界检查——如果 `expected` 处于错误状态,调用它们的后果是未定义行为。这和原生指针的越界访问一样,性能优先,安全你自己负责。 - -## void 特化 - -有些操作没有返回值,只需要表示成功或失败。这时可以用 `expected`: - -```cpp -cf::expected save_config(const std::string& path) { - std::ofstream file(path); - if (!file) { - return cf::unexpected("无法打开文件"); - } - // 写入配置... - return {}; // 成功,返回空 expected -} - -// 调用 -auto result = save_config("config.txt"); -if (!result) { - std::cerr << "保存失败: " << result.error() << std::endl; -} -``` - -`expected` 的"值"是虚拟的,成功状态下没有实际数据存储,只有一个标志位。这意味着它的内存开销比 `expected` 小——只需要存一个 `bool` 和可能的 `E`。 - -## 单态操作 - -C++23 的 `expected` 带来了函数式风格的链式调用,我们也实现了这些接口。`and_then`、`or_else`、`transform` 和 `transform_error` 让你可以把多个可能失败的操作串起来,而不需要嵌套的 `if-else`: - -```cpp -// and_then: 如果当前有值,用这个值调用函数,返回新的 expected -// 如果当前是错误,直接传播错误 -cf::expected read_user_id(int user_id) { - return parse_number(std::to_string(user_id)) - .and_then([](int id) { - return fetch_user_name(id); // 返回 expected - }); -} - -// transform: 如果当前有值,应用函数转换值,错误保持不变 -cf::expected result = - parse_number("42") - .transform([](int value) { - return "数字是: " + std::to_string(value); - }); -// 结果: expected,值为 "数字是: 42" - -// or_else: 如果当前是错误,用这个错误调用恢复函数 -cf::expected result = - parse_number("invalid") - .or_else([](ParseError err) { - return cf::expected{0}; // 提供默认值 - }); -// 结果: expected,值为 0 - -// transform_error: 转换错误类型,值保持不变 -cf::expected result = - parse_number("42") - .transform_error([](ParseError err) { - return "解析错误: " + std::to_string(static_cast(err)); - }); -``` - -这些操作的组合可以实现复杂的错误处理逻辑,而且代码是线性的,不是嵌套的: - -```cpp -// 不用 monadic 操作 -auto result1 = parse_number(input); -if (!result1) { - return result1; -} -auto result2 = fetch_user(*result1); -if (!result2) { - return result2; -} -auto result3 = calculate_score(*result2); -if (!result3) { - return result3; -} -return result3; - -// 用 monadic 操作 -return parse_number(input) - .and_then(fetch_user) - .and_then(calculate_score); -``` - -## 与异常的对比 - -`expected` 不是要完全替代异常,而是在特定场景下提供一种更可控的错误处理方式。异常适合"真正的异常情况"——那些理论上不应该发生、发生了最好让程序停下来思考的事情。而 `expected` 适合"预期的错误"——输入格式不对、文件不存在、网络超时这些正常流程中可能出现的情况。 - -从性能角度看,`expected` 的优势在于: -- 成功路径上没有额外开销(不需要 try-catch 的栈展开机制) -- 错误处理的成本是显式的(构造一个 `E` 类型的对象) -- 二进制体积更小(不需要异常元数据) - -从代码可读性角度看,`expected` 的优势在于: -- 错误处理路径在类型系统中是可见的 -- 不容易忘记处理错误(虽然还是可以忽略,但至少需要显式地忽略) -- 错误类型是类型签名的一部分,不需要去翻文档才知道可能出什么错 - -## 性能考虑 - -`expected` 的内存布局是一个联合体加上一个标志位: - -```cpp -union { - T value; - E error; -} storage_; -bool has_value_; -``` - -大小是 `max(sizeof(T), sizeof(E)) + sizeof(bool)`,对齐后可能会有一点 padding。如果你在意内存占用,可以让错误类型尽量小——比如用 `enum` 代替 `std::string`。 - -另一个需要考虑的是移动语义。`expected` 支持移动,而且移动操作是 `noexcept` 的(如果 T 和 E 的移动构造是 `noexcept` 的)。这意味着你可以放心地在容器里存 `expected`,不用担心异常安全的问题。 - -## 注意事项 - -使用 `expected` 时有几个常见的坑: - -第一,不要在错误类型里存大对象。`std::string` 作为错误类型很方便,但每次返回错误都要拷贝字符串。如果是频繁调用的底层函数,考虑用错误码或错误码加上一个小型消息缓冲区。 - -第二,`unexpected` 不能被忽略,但可以没有被检查。如果你写 `parse_number("42");` 而不接收返回值,编译器不会警告你(除非你开启了特定的警告)。这一点和 `[[nodiscard]]` 不同——我们确实想让某些情况下错误被静默忽略成为可能。 - -第三,`operator*` 和 `operator->` 不做检查。如果你在错误状态下调用它们,结果是未定义行为。这个设计是有意为之的——`expected` 的使用场景里,调用方通常已经用 `has_value()` 检查过状态了,再做一次检查就是纯粹的开销。 - -## 与 std::expected 的兼容性 - -我们的实现尽量保持与 C++23 `std::expected` 的接口一致。如果将来编译器升级到 C++23,你可以相对平滑地迁移: - -```cpp -// 现在 -namespace cf { - template - class expected { /* ... */ }; -} - -// 将来(假设 C++23) -namespace std { - template - class expected { /* ... */ }; -} - -// 迁移时只需要把 cf::expected 替换成 std::expected -// 类型别名可以帮助过渡 -template -using expected = std::expected; -``` - -当然,我们还是建议直接用 `cf::expected`,这样可以保持代码的跨平台兼容性,而且我们可以根据自己的需求定制实现。 - -## 相关文档 - -- [span - 容器视图](./span.md) -- [ScopeGuard - 资源管理](./scope_guard.md) -- [基础工具类概述](./overview.md) +--- +title: "expected - 错误处理" +description: 是 C++23 引入的错误处理模板,我们提供了一份 backport 实现,确保在所有目标编译器上可 +--- + +# expected - 错误处理 + +`expected` 是 C++23 引入的错误处理模板,我们提供了一份 backport 实现,确保在所有目标编译器上可用。核心思想很简单——用返回值显式表示"可能失败的操作",而不是靠异常把错误往外扔。这个设计特别适合我们这种需要跨平台、甚至在嵌入式环境下跑的代码,毕竟很多嵌入式编译器压根不支持异常,而且开启异常后二进制体积会显著膨胀。 + +## 为什么需要 expected + +传统的 C++ 错误处理无非就几条路:抛异常、返回错误码、用输出参数。但这几条路都有自己的问题。 + +```cpp +// 方案一:异常 +// 问题:很多环境禁用异常,且异常的开销不明确 +std::ifstream open_file(const std::string& path) { + std::ifstream file(path); + if (!file) { + throw std::runtime_error("Failed to open file"); + } + return file; +} + +// 方案二:错误码 +// 问题:错误码可能被忽略,且无法携带返回值 +bool open_file(const std::string& path, std::ifstream& out) { + std::ifstream file(path); + if (!file) return false; + out = std::move(file); + return true; +} +// 调用方可能忘记检查返回值 +open_file("data.txt", file); // 忘记检查,静默失败 + +// 方案三:expected +// 问题:几乎没有,错误必须被显式处理 +cf::expected open_file(const std::string& path) { + std::ifstream file(path); + if (!file) { + return cf::unexpected("Failed to open file: " + path); + } + return file; +} +```text + +`expected` 强制调用者处理错误——你想拿到值,就必须先检查有没有错误。而且类型系统会帮你看住:一个 `expected` 要么包含 `int`,要么包含 `ErrorCode`,不可能同时存在或都不存在。 + +## 基本用法 + +创建一个 `expected` 有两种方式:直接返回值,或者返回 `unexpected`: + +```cpp +#include "base/expected/expected.hpp" + +enum class ParseError { InvalidInput, Overflow }; + +cf::expected parse_number(std::string_view str) { + if (str.empty()) { + return cf::unexpected(ParseError::InvalidInput); + } + + try { + return std::stoi(std::string(str)); + } catch (const std::out_of_range&) { + return cf::unexpected(ParseError::Overflow); + } +} +```text + +调用方需要检查结果: + +```cpp +auto result = parse_number("42"); + +if (result.has_value()) { + std::cout << "解析成功: " << result.value() << std::endl; +} else { + switch (result.error()) { + case ParseError::InvalidInput: + std::cout << "输入为空" << std::endl; + break; + case ParseError::Overflow: + std::cout << "数值溢出" << std::endl; + break; + } +} +```text + +⚠️ 如果不检查直接调用 `value()`,会抛出 `bad_expected_access` 异常。但这个异常是你在"错误地使用 expected"时才抛出的,和业务逻辑异常是两码事。正常流程下,`expected` 的使用是不抛异常的。 + +## 值访问方式 + +`expected` 提供了多种访问值的方式,取决于你想怎么处理错误情况: + +```cpp +cf::expected result = /* ... */; + +// 方式一:先检查再访问 +if (result) { + int value = *result; // operator* + int value2 = result.value(); // value() 方法 +} + +// 方式二:提供默认值 +int value = result.value_or(-1); // 如果是错误状态,返回 -1 + +// 方式三:直接访问(如果确实是错误状态,会抛异常) +int value = result.value(); // 可能抛 bad_expected_access +```cpp + +`operator*` 和 `operator->` 的行为类似指针,但不做边界检查——如果 `expected` 处于错误状态,调用它们的后果是未定义行为。这和原生指针的越界访问一样,性能优先,安全你自己负责。 + +## void 特化 + +有些操作没有返回值,只需要表示成功或失败。这时可以用 `expected`: + +```cpp +cf::expected save_config(const std::string& path) { + std::ofstream file(path); + if (!file) { + return cf::unexpected("无法打开文件"); + } + // 写入配置... + return {}; // 成功,返回空 expected +} + +// 调用 +auto result = save_config("config.txt"); +if (!result) { + std::cerr << "保存失败: " << result.error() << std::endl; +} +```text + +`expected` 的"值"是虚拟的,成功状态下没有实际数据存储,只有一个标志位。这意味着它的内存开销比 `expected` 小——只需要存一个 `bool` 和可能的 `E`。 + +## 单态操作 + +C++23 的 `expected` 带来了函数式风格的链式调用,我们也实现了这些接口。`and_then`、`or_else`、`transform` 和 `transform_error` 让你可以把多个可能失败的操作串起来,而不需要嵌套的 `if-else`: + +```cpp +// and_then: 如果当前有值,用这个值调用函数,返回新的 expected +// 如果当前是错误,直接传播错误 +cf::expected read_user_id(int user_id) { + return parse_number(std::to_string(user_id)) + .and_then([](int id) { + return fetch_user_name(id); // 返回 expected + }); +} + +// transform: 如果当前有值,应用函数转换值,错误保持不变 +cf::expected result = + parse_number("42") + .transform([](int value) { + return "数字是: " + std::to_string(value); + }); +// 结果: expected,值为 "数字是: 42" + +// or_else: 如果当前是错误,用这个错误调用恢复函数 +cf::expected result = + parse_number("invalid") + .or_else([](ParseError err) { + return cf::expected{0}; // 提供默认值 + }); +// 结果: expected,值为 0 + +// transform_error: 转换错误类型,值保持不变 +cf::expected result = + parse_number("42") + .transform_error([](ParseError err) { + return "解析错误: " + std::to_string(static_cast(err)); + }); +```text + +这些操作的组合可以实现复杂的错误处理逻辑,而且代码是线性的,不是嵌套的: + +```cpp +// 不用 monadic 操作 +auto result1 = parse_number(input); +if (!result1) { + return result1; +} +auto result2 = fetch_user(*result1); +if (!result2) { + return result2; +} +auto result3 = calculate_score(*result2); +if (!result3) { + return result3; +} +return result3; + +// 用 monadic 操作 +return parse_number(input) + .and_then(fetch_user) + .and_then(calculate_score); +```text + +## 与异常的对比 + +`expected` 不是要完全替代异常,而是在特定场景下提供一种更可控的错误处理方式。异常适合"真正的异常情况"——那些理论上不应该发生、发生了最好让程序停下来思考的事情。而 `expected` 适合"预期的错误"——输入格式不对、文件不存在、网络超时这些正常流程中可能出现的情况。 + +从性能角度看,`expected` 的优势在于: +- 成功路径上没有额外开销(不需要 try-catch 的栈展开机制) +- 错误处理的成本是显式的(构造一个 `E` 类型的对象) +- 二进制体积更小(不需要异常元数据) + +从代码可读性角度看,`expected` 的优势在于: +- 错误处理路径在类型系统中是可见的 +- 不容易忘记处理错误(虽然还是可以忽略,但至少需要显式地忽略) +- 错误类型是类型签名的一部分,不需要去翻文档才知道可能出什么错 + +## 性能考虑 + +`expected` 的内存布局是一个联合体加上一个标志位: + +```cpp +union { + T value; + E error; +} storage_; +bool has_value_; +```cpp + +大小是 `max(sizeof(T), sizeof(E)) + sizeof(bool)`,对齐后可能会有一点 padding。如果你在意内存占用,可以让错误类型尽量小——比如用 `enum` 代替 `std::string`。 + +另一个需要考虑的是移动语义。`expected` 支持移动,而且移动操作是 `noexcept` 的(如果 T 和 E 的移动构造是 `noexcept` 的)。这意味着你可以放心地在容器里存 `expected`,不用担心异常安全的问题。 + +## 注意事项 + +使用 `expected` 时有几个常见的坑: + +第一,不要在错误类型里存大对象。`std::string` 作为错误类型很方便,但每次返回错误都要拷贝字符串。如果是频繁调用的底层函数,考虑用错误码或错误码加上一个小型消息缓冲区。 + +第二,`unexpected` 不能被忽略,但可以没有被检查。如果你写 `parse_number("42");` 而不接收返回值,编译器不会警告你(除非你开启了特定的警告)。这一点和 `[[nodiscard]]` 不同——我们确实想让某些情况下错误被静默忽略成为可能。 + +第三,`operator*` 和 `operator->` 不做检查。如果你在错误状态下调用它们,结果是未定义行为。这个设计是有意为之的——`expected` 的使用场景里,调用方通常已经用 `has_value()` 检查过状态了,再做一次检查就是纯粹的开销。 + +## 与 std::expected 的兼容性 + +我们的实现尽量保持与 C++23 `std::expected` 的接口一致。如果将来编译器升级到 C++23,你可以相对平滑地迁移: + +```cpp +// 现在 +namespace cf { + template + class expected { /* ... */ }; +} + +// 将来(假设 C++23) +namespace std { + template + class expected { /* ... */ }; +} + +// 迁移时只需要把 cf::expected 替换成 std::expected +// 类型别名可以帮助过渡 +template +using expected = std::expected; +```text + +当然,我们还是建议直接用 `cf::expected`,这样可以保持代码的跨平台兼容性,而且我们可以根据自己的需求定制实现。 + +## 相关文档 + +- [span - 容器视图](./span.md) +- [ScopeGuard - 资源管理](./scope_guard.md) +- [基础工具类概述](./overview.md) diff --git a/document/HandBook/base/factory.md b/document/HandBook/base/factory.md index 0706ca797..df0b719ac 100644 --- a/document/HandBook/base/factory.md +++ b/document/HandBook/base/factory.md @@ -1,3 +1,8 @@ +--- +title: "factory - 工厂模式" +description: 命名空间下提供了一组工厂模板,覆盖了从裸指针到智能指针、从直接构造到注册创建的多种场景。所有工厂都是 +--- + # factory - 工厂模式 `cf` 命名空间下提供了一组工厂模板,覆盖了从裸指针到智能指针、从直接构造到注册创建的多种场景。所有工厂都是头文件模板,无需额外编译单元,且设计上尽量保持 ABI 友好。 @@ -28,7 +33,7 @@ cf::PlainFactory factory; Widget* w = factory.make(10, 20); // 使用 w... delete w; // 调用方负责删除 -``` +```text `PlainFactory` 的设计目标之一是跨 ABI 边界。动态库导出的接口通常不适合直接返回 `std::unique_ptr`(不同编译器/标准库的 `unique_ptr` 布局可能不同),但裸指针永远兼容。 @@ -44,7 +49,7 @@ using WidgetFactory = cf::StaticPlainFactory; // 在任何地方 auto& factory = WidgetFactory::instance(); Widget* w = factory.make(10, 20); -``` +```text `StaticPlainFactory` 继承了 `PlainFactory` 和 `SimpleSingleton`,线程安全的单例由 Meyer's Singleton 保证。 @@ -66,7 +71,7 @@ auto unique_svc = factory.make_unique("Logger"); // std::unique_ptr auto shared_svc = factory.make_shared("Config"); // std::shared_ptr // 不需要手动 delete -``` +```text ### StaticSmartPtrPlainFactory - 单例版智能指针工厂 @@ -74,7 +79,7 @@ auto shared_svc = factory.make_shared("Config"); // std::shared_ptr using ServiceFactory = cf::StaticSmartPtrPlainFactory; auto svc = ServiceFactory::instance().make_unique("MyService"); -``` +```text ## RegisteredFactory - 注册式工厂 @@ -112,7 +117,7 @@ renderer_factory.register_creator([]() -> IRenderer* { // 创建 auto renderer = renderer_factory.make_unique(); renderer->draw(); -``` +```text ### 自定义删除器 @@ -123,7 +128,7 @@ renderer_factory.register_creator( []() -> IRenderer* { return new VulkanRenderer; }, [](IRenderer* p) { /* 自定义清理逻辑 */ delete p; } ); -``` +```text ### StaticRegisteredFactory - 单例版注册式工厂 @@ -137,7 +142,7 @@ RendererFactory::instance().register_creator([]() -> IRenderer* { // 使用 auto renderer = RendererFactory::instance().make_unique(); -``` +```text ### 检查注册状态 @@ -147,7 +152,7 @@ if (RendererFactory::instance().has_creator()) { } else { // 没有注册任何 creator,无法创建 } -``` +```bash ## 线程安全说明 diff --git a/document/HandBook/base/hash.md b/document/HandBook/base/hash.md index ab46438f0..08c2badbc 100644 --- a/document/HandBook/base/hash.md +++ b/document/HandBook/base/hash.md @@ -1,3 +1,8 @@ +--- +title: "hash (FNV-1a) - 编译期哈希" +description: "命名空间提供了 FNV-1a(Fowler-Noll-Vo 1a)哈希算法的 实现,支持在编译期计" +--- + # hash (FNV-1a) - 编译期哈希 `cf::hash` 命名空间提供了 FNV-1a(Fowler-Noll-Vo 1a)哈希算法的 `constexpr` 实现,支持在编译期计算字符串的哈希值。FNV-1a 以其实现简单、分布均匀和极低的碰撞率而闻名,非常适合用于字符串标识符的快速比较。 @@ -17,7 +22,7 @@ switch (fnv1a64(token)) { case fnv1a64("START"): /* ... */ break; case fnv1a64("STOP"): /* ... */ break; } -``` +```text 编译器会在编译期算出 `fnv1a64("START")` 的值,switch 语句变成了一组整数比较,非常高效。 @@ -38,7 +43,7 @@ static_assert(h1 != h2, "Different strings must produce different hashes"); // 运行时使用 std::string_view std::string_view sv = "some_string"; uint64_t h3 = fnv1a64(sv); -``` +```text ### 32 位哈希 @@ -48,7 +53,7 @@ constexpr uint32_t h32 = fnv1a32("TokenA"); // 运行时 uint32_t h = fnv1a32(std::string_view("dynamic")); -``` +```text 32 位版本适用于内存受限的场景,但碰撞概率比 64 位高。如果哈希表不大(几千个条目以内),32 位通常够用。 @@ -59,7 +64,7 @@ uint32_t h = fnv1a32(std::string_view("dynamic")); constexpr uint64_t h1 = fnv1a64("data"); // 默认种子 constexpr uint64_t h2 = fnv1a64("data", 12345678ULL); // 自定义种子 // h1 != h2,相同输入不同种子产生不同哈希 -``` +```text ### 用户自定义字面量 @@ -69,7 +74,7 @@ using namespace cf::hash; // "_hash" 后缀,编译期计算 constexpr uint64_t h = "MyToken"_hash; static_assert(h == fnv1a64("MyToken"), "UDL must match function call"); -``` +```text `_hash` 字面量让代码更简洁,特别是在 switch-case 里: @@ -81,19 +86,19 @@ switch (fnv1a64(cmd)) { case "save"_hash: save_file(); break; default: unknown_cmd(); break; } -``` +```text ## FNV-1a 算法 FNV-1a 的核心逻辑极其简单: -``` +```text hash = offset_basis for each byte in input: hash = hash XOR byte hash = hash * prime return hash -``` +```bash 参数值: @@ -133,7 +138,7 @@ if (it != hashmap.end()) { // 确认是真正的匹配 } } -``` +```text 3. **不要用于安全场景**:FNV-1a 不是加密哈希,不应该用于密码存储、完整性校验等安全场景。 diff --git a/document/HandBook/base/index.md b/document/HandBook/base/index.md index 0508d99ff..2d287fb33 100644 --- a/document/HandBook/base/index.md +++ b/document/HandBook/base/index.md @@ -1,3 +1,8 @@ +--- +title: 基础工具库 +description: 零 Qt 依赖的跨平台工具集。设计原则:标准库特性的降级实现和便利封装,方便在无 Qt 环境下复用。 +--- + # 基础工具库 零 Qt 依赖的跨平台工具集。设计原则:标准库特性的降级实现和便利封装,方便在无 Qt 环境下复用。 diff --git a/document/HandBook/base/linux/.pages b/document/HandBook/base/linux/.pages deleted file mode 100644 index f586d1213..000000000 --- a/document/HandBook/base/linux/.pages +++ /dev/null @@ -1,3 +0,0 @@ -title: Linux 工具 -nav: - - proc 解析器: proc_parser.md diff --git a/document/HandBook/base/linux/index.md b/document/HandBook/base/linux/index.md index 89c6764f0..d56b9151f 100644 --- a/document/HandBook/base/linux/index.md +++ b/document/HandBook/base/linux/index.md @@ -1,10 +1,11 @@ -# linux - -> Welcome to the linux section. +--- +title: Linux 工具 +description: 本章节包含 库中 Linux 平台特有的工具实现,涵盖基于 Linux 系统调用和 文件系统的底 +--- -## Overview +# Linux 工具 -Documentation and resources for linux. +本章节包含 `cfbase` 库中 Linux 平台特有的工具实现,涵盖基于 Linux 系统调用和 `/proc` 文件系统的底层工具函数。这些工具为硬件探测模块提供平台相关的系统信息采集能力。 --- diff --git a/document/HandBook/base/linux/proc_parser.md b/document/HandBook/base/linux/proc_parser.md index 8327bbd30..bdd63480f 100644 --- a/document/HandBook/base/linux/proc_parser.md +++ b/document/HandBook/base/linux/proc_parser.md @@ -1,141 +1,146 @@ -# proc_parser - Linux 文件解析工具 - -Linux 下的硬件信息大多藏在 `/proc` 和 `/sys` 伪文件系统里。这些文件格式固定但处理起来很繁琐,而且容易出错——比如字段分隔符可能是冒号加空格,也可能是制表符。`proc_parser` 提供了一套专门处理这些格式的工具,用 `string_view` 避免拷贝,而且不抛异常。 - -## 解析 cpuinfo - -`/proc/cpuinfo` 是 CPU 信息的核心来源,每行格式是 `key: value`。`parse_cpuinfo_field()` 提取指定字段的值: - -```cpp -#include "base/linux/proc_parser.h" - -std::string line = "model name\t: Intel(R) Core(TM) i7-9750H"; -auto model = cf::parse_cpuinfo_field(line, "model name"); -// model == "Intel(R) Core(TM) i7-9750H" - -// 字段不存在时返回空视图 -auto missing = cf::parse_cpuinfo_field(line, "vendor_id"); -// missing.data() == nullptr -``` - -实际使用时通常是逐行读取: - -```cpp -std::ifstream cpuinfo("/proc/cpuinfo"); -std::string line; - -while (std::getline(cpuinfo, line)) { - auto model = cf::parse_cpuinfo_field(line, "model name"); - if (!model.empty()) { - std::cout << "CPU: " << model << std::endl; - break; // 找到就行,不需要继续 - } - - auto vendor = cf::parse_cpuinfo_field(line, "vendor_id"); - if (!vendor.empty()) { - std::cout << "厂商: " << vendor << std::endl; - } -} -``` - -## 字符串去空格 - -这些文件里经常有多余的空格,`trim_whitespace()` 系列函数可以清理: - -```cpp -std::string_view sv = " hello world "; -auto trimmed = cf::trim_whitespace(sv); // "hello world" -auto left_trimmed = cf::ltrim_whitespace(sv); // "hello world " -auto right_trimmed = cf::rtrim_whitespace(sv);// " hello world" -``` - -## 解析缓存大小 - -缓存大小用的是人类可读的格式——`32K`、`1M`、`2G`。`parse_cache_size()` 统一转换成 KB: - -```cpp -auto size1 = cf::parse_cache_size("32K"); // 32 -auto size2 = cf::parse_cache_size("1M"); // 1024 -auto size3 = cf::parse_cache_size("2G"); // 2097152 -auto invalid = cf::parse_cache_size("xyz"); // std::nullopt -``` - -这在读取 `/sys/devices/system/cpu/cpu0/cache/index*/size` 时特别有用: - -```cpp -auto l1_size = cf::read_uint32_file("/sys/devices/system/cpu/cpu0/cache/index0/size"); -auto size_kb = cf::parse_cache_size(l1_size); -if (size_kb) { - std::cout << "L1 缓存: " << *size_kb << " KB" << std::endl; -} -``` - -## 解析数字 - -`parse_uint32()` 和 `parse_hex_uint32()` 把字符串转成数字,失败时返回 `std::nullopt` 而不是抛异常: - -```cpp -auto value = cf::parse_uint32("4096"); // 4096 -auto hex1 = cf::parse_hex_uint32("0x41"); // 65 -auto hex2 = cf::parse_hex_uint32("FF"); // 255 -auto invalid = cf::parse_uint32("abc"); // std::nullopt -``` - -十六进制解析在处理 ARM 的 `CPU implementer` 字段时很常见: - -```cpp -auto impl = cf::parse_cpuinfo_field(line, "CPU implementer"); -if (auto impl_val = cf::parse_hex_uint32(impl)) { - auto vendor = cf::arm_implementer_to_vendor(*impl_val); - // impl_val = 0x41 -> vendor = "ARM" -} -``` - -## 读取文件 - -`read_uint32_file()` 直接读取单个数字值的文件,这在 `/sys` 下很常见——很多文件就只有一个数字: - -```cpp -auto max_freq = cf::read_uint32_file( - "/sys/devices/system/cpu/cpu0/cpufreq/max_freq" -); -if (max_freq.has_value()) { - std::cout << "最大频率: " << *max_freq << " kHz" << std::endl; -} -``` - -⚠️ 这个函数会执行文件 I/O,可能因为权限或文件不存在而失败。返回 `std::nullopt` 时可以判断是文件问题还是解析失败。 - -## ARM 厂商映射 - -ARM 的 `CPU implementer` 是个十六进制 ID,需要映射到厂商名称: - -```cpp -auto vendor = cf::arm_implementer_to_vendor(0x41); // "ARM" -auto vendor2 = cf::arm_implementer_to_vendor(0x51); // "Qualcomm" -auto vendor3 = cf::arm_implementer_to_vendor(0x69); // "Intel" -auto unknown = cf::arm_implementer_to_vendor(0xFF); // "Unknown" -``` - -支持的厂商 ID 包括 ARM (0x41)、Broadcom (0x42)、Qualcomm (0x51)、NVIDIA (0x4E) 等。 - -## 生命周期注意 - -所有返回 `string_view` 的函数,结果都依赖于输入数据的生命周期。不要这样做: - -```cpp -std::string_view bad() { - std::string line = "model name: Intel"; - return cf::parse_cpuinfo_field(line, "model name"); // 危险!line 会被销毁 -} - -std::string_view good(const std::string& line) { - return cf::parse_cpuinfo_field(line, "model name"); // OK,调用方保证 line 有效 -} -``` - -## 相关文档 - -- [基础工具类概述](../overview.md) -- [Linux 平台实现](../../implementation/linux/cpu_implementation.md) -- [CPU 模块概述](../../api/system/cpu/overview.md) +--- +title: "procparser - Linux 文件解析工具" +description: Linux 下的硬件信息大多藏在 和 伪文件系统里。这些文件格式固定但处理起来很繁琐,而且容易出 +--- + +# proc_parser - Linux 文件解析工具 + +Linux 下的硬件信息大多藏在 `/proc` 和 `/sys` 伪文件系统里。这些文件格式固定但处理起来很繁琐,而且容易出错——比如字段分隔符可能是冒号加空格,也可能是制表符。`proc_parser` 提供了一套专门处理这些格式的工具,用 `string_view` 避免拷贝,而且不抛异常。 + +## 解析 cpuinfo + +`/proc/cpuinfo` 是 CPU 信息的核心来源,每行格式是 `key: value`。`parse_cpuinfo_field()` 提取指定字段的值: + +```cpp +#include "base/linux/proc_parser.h" + +std::string line = "model name\t: Intel(R) Core(TM) i7-9750H"; +auto model = cf::parse_cpuinfo_field(line, "model name"); +// model == "Intel(R) Core(TM) i7-9750H" + +// 字段不存在时返回空视图 +auto missing = cf::parse_cpuinfo_field(line, "vendor_id"); +// missing.data() == nullptr +```text + +实际使用时通常是逐行读取: + +```cpp +std::ifstream cpuinfo("/proc/cpuinfo"); +std::string line; + +while (std::getline(cpuinfo, line)) { + auto model = cf::parse_cpuinfo_field(line, "model name"); + if (!model.empty()) { + std::cout << "CPU: " << model << std::endl; + break; // 找到就行,不需要继续 + } + + auto vendor = cf::parse_cpuinfo_field(line, "vendor_id"); + if (!vendor.empty()) { + std::cout << "厂商: " << vendor << std::endl; + } +} +```text + +## 字符串去空格 + +这些文件里经常有多余的空格,`trim_whitespace()` 系列函数可以清理: + +```cpp +std::string_view sv = " hello world "; +auto trimmed = cf::trim_whitespace(sv); // "hello world" +auto left_trimmed = cf::ltrim_whitespace(sv); // "hello world " +auto right_trimmed = cf::rtrim_whitespace(sv);// " hello world" +```text + +## 解析缓存大小 + +缓存大小用的是人类可读的格式——`32K`、`1M`、`2G`。`parse_cache_size()` 统一转换成 KB: + +```cpp +auto size1 = cf::parse_cache_size("32K"); // 32 +auto size2 = cf::parse_cache_size("1M"); // 1024 +auto size3 = cf::parse_cache_size("2G"); // 2097152 +auto invalid = cf::parse_cache_size("xyz"); // std::nullopt +```text + +这在读取 `/sys/devices/system/cpu/cpu0/cache/index*/size` 时特别有用: + +```cpp +auto l1_size = cf::read_uint32_file("/sys/devices/system/cpu/cpu0/cache/index0/size"); +auto size_kb = cf::parse_cache_size(l1_size); +if (size_kb) { + std::cout << "L1 缓存: " << *size_kb << " KB" << std::endl; +} +```text + +## 解析数字 + +`parse_uint32()` 和 `parse_hex_uint32()` 把字符串转成数字,失败时返回 `std::nullopt` 而不是抛异常: + +```cpp +auto value = cf::parse_uint32("4096"); // 4096 +auto hex1 = cf::parse_hex_uint32("0x41"); // 65 +auto hex2 = cf::parse_hex_uint32("FF"); // 255 +auto invalid = cf::parse_uint32("abc"); // std::nullopt +```text + +十六进制解析在处理 ARM 的 `CPU implementer` 字段时很常见: + +```cpp +auto impl = cf::parse_cpuinfo_field(line, "CPU implementer"); +if (auto impl_val = cf::parse_hex_uint32(impl)) { + auto vendor = cf::arm_implementer_to_vendor(*impl_val); + // impl_val = 0x41 -> vendor = "ARM" +} +```text + +## 读取文件 + +`read_uint32_file()` 直接读取单个数字值的文件,这在 `/sys` 下很常见——很多文件就只有一个数字: + +```cpp +auto max_freq = cf::read_uint32_file( + "/sys/devices/system/cpu/cpu0/cpufreq/max_freq" +); +if (max_freq.has_value()) { + std::cout << "最大频率: " << *max_freq << " kHz" << std::endl; +} +```text + +⚠️ 这个函数会执行文件 I/O,可能因为权限或文件不存在而失败。返回 `std::nullopt` 时可以判断是文件问题还是解析失败。 + +## ARM 厂商映射 + +ARM 的 `CPU implementer` 是个十六进制 ID,需要映射到厂商名称: + +```cpp +auto vendor = cf::arm_implementer_to_vendor(0x41); // "ARM" +auto vendor2 = cf::arm_implementer_to_vendor(0x51); // "Qualcomm" +auto vendor3 = cf::arm_implementer_to_vendor(0x69); // "Intel" +auto unknown = cf::arm_implementer_to_vendor(0xFF); // "Unknown" +```text + +支持的厂商 ID 包括 ARM (0x41)、Broadcom (0x42)、Qualcomm (0x51)、NVIDIA (0x4E) 等。 + +## 生命周期注意 + +所有返回 `string_view` 的函数,结果都依赖于输入数据的生命周期。不要这样做: + +```cpp +std::string_view bad() { + std::string line = "model name: Intel"; + return cf::parse_cpuinfo_field(line, "model name"); // 危险!line 会被销毁 +} + +std::string_view good(const std::string& line) { + return cf::parse_cpuinfo_field(line, "model name"); // OK,调用方保证 line 有效 +} +```text + +## 相关文档 + +- [基础工具类概述](../overview.md) +- [Linux 平台实现](../../implementation/linux/cpu_implementation.md) +- [CPU 模块概述](../../api/system/cpu/overview.md) diff --git a/document/HandBook/base/macro/.pages b/document/HandBook/base/macro/.pages deleted file mode 100644 index 67894b440..000000000 --- a/document/HandBook/base/macro/.pages +++ /dev/null @@ -1,5 +0,0 @@ -title: 宏定义 -nav: - - 概览: index.md - - Plain Property: plain_property.md - - 系统判断: system_judge.md diff --git a/document/HandBook/base/macro/index.md b/document/HandBook/base/macro/index.md index 59f927b73..73b3693f7 100644 --- a/document/HandBook/base/macro/index.md +++ b/document/HandBook/base/macro/index.md @@ -1,10 +1,11 @@ -# macro - -> Welcome to the macro section. +--- +title: 宏定义系统 +description: 本章节介绍 库的宏定义系统,涵盖平台检测宏(Windows/Linux/macOS)、架构检测宏( +--- -## Overview +# 宏定义系统 -Documentation and resources for macro. +本章节介绍 `cfbase` 库的宏定义系统,涵盖平台检测宏(Windows/Linux/macOS)、架构检测宏(x86/ARM)以及编译器特性检测宏等预处理器工具。宏定义系统为跨平台编译和条件包含提供统一的基础设施。 --- diff --git a/document/HandBook/base/macro/plain_property.md b/document/HandBook/base/macro/plain_property.md index 6e620ad4b..5ac5d4ed1 100644 --- a/document/HandBook/base/macro/plain_property.md +++ b/document/HandBook/base/macro/plain_property.md @@ -1,133 +1,138 @@ -# plain_property - 简单属性宏 - -`CF_PLAIN_PROPERTY` 是一个用来自动生成 getter 和 setter 方法的宏,减少手写样板代码的重复劳动。我们经常需要一些简单的数据持有类,它们只包含一些带默认值的成员变量和对应的访问器——这种代码写多了既无聊又容易出错,用宏来生成是更务实的选择。 - -## 基本用法 - -在类中使用 `CF_PLAIN_PROPERTY` 宏时,它会展开为一个私有成员变量和三个公共方法: - -```cpp -#include "base/macro/plain_property.h" - -class WidgetConfig { - // 生成: int width{800}; - // int& get_width(); - // const int& get_width_const() const; - // void set_width(const int&); - CF_PLAIN_PROPERTY(int, width, 800) - - // 生成: std::string title{"Untitled"}; - // ... - CF_PLAIN_PROPERTY(std::string, title, "Untitled") - -public: - // 其他成员方法... -}; -``` - -## 宏展开结果 - -`CF_PLAIN_PROPERTY(val_type, val_name, default_value)` 展开后的代码如下: - -```cpp -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}; -``` - -注意成员变量直接在 `private` 区域声明,而访问器在 `public` 区域。这是因为宏本身包含了访问修饰符,所以使用时不需要额外考虑这些。 - -## 使用示例 - -```cpp -#include "base/macro/plain_property.h" -#include -#include - -class AppConfig { - CF_PLAIN_PROPERTY(int, max_connections, 10) - CF_PLAIN_PROPERTY(std::string, host, "localhost") - CF_PLAIN_PROPERTY(bool, debug_mode, false) -}; - -int main() { - AppConfig config; - - // 使用 setter 修改值 - config.set_max_connections(100); - config.set_host("192.168.1.1"); - config.set_debug_mode(true); - - // 使用 getter 读取值 - std::cout << "Host: " << config.get_host() << std::endl; - std::cout << "Max connections: " << config.get_max_connections() << std::endl; - - // const getter 可以在 const 上下文调用 - const AppConfig& const_config = config; - std::cout << "Debug: " << std::boolalpha << (const_config.get_debug_mode_const()) << std::endl; - - return 0; -} -``` - -## 方法命名 - -宏生成的三个方法遵循特定命名规则: - -- `get_()`:返回非 const 引用,允许修改 -- `get__const()`:返回 const 引用,用于只读访问 -- `set_(const T&)`:设置新值 - -两个 getter 的区别在于 const 正确性。非 const 对象可以调用任何一个,const 对象只能调用 `_const` 版本。这个设计虽然有点冗余,但避免了在模板代码里处理 const 重载的麻烦。 - -## 设计动机 - -选择用宏而不是 C++ 的属性语法(如 `property` 关键字)是因为后者不是标准 C++ 的一部分,只有 MSVC 支持非标准的扩展。用宏虽然可读性稍差,但能保证跨编译器兼容。 - -另一个考虑是代码生成的一致性。手写 getter/setter 时容易漏掉 const 版本,或者命名不统一。宏强制了一种固定模式,减少了人为错误的可能性。 - -## 注意事项 - -⚠️ setter 使用 const 引用传参,对于基础类型(int、bool 等)来说不是最高效的选择。如果你对性能非常敏感,或者这些属性在热路径上被频繁修改,可能需要手写专门的版本。 - -⚠️ 宏展开会插入访问修饰符,如果在类的中间使用,会改变后续成员的访问级别。建议把所有 `CF_PLAIN_PROPERTY` 调用集中在类定义的开头或结尾,避免和手写的 public/private 区域混在一起: - -```cpp -// 好的做法:宏集中使用 -class Good { - CF_PLAIN_PROPERTY(int, x, 0) - CF_PLAIN_PROPERTY(int, y, 0) - -public: - void method(); - -private: - int internal_; -}; - -// 不好的做法:宏和手写访问修饰符混用 -class Bad { -public: - void method(); - CF_PLAIN_PROPERTY(int, x, 0) // 会插入 public:/private:,破坏原有结构 -}; -``` - -## 适用场景 - -`CF_PLAIN_PROPERTY` 适合用于简单的配置类、数据传输对象(DTO)或状态持有类。如果属性需要额外的验证逻辑、触发通知或线程安全保证,还是手写实现更合适。 - -## 相关文档 - -- [system_judge - 系统判断宏](./system_judge.md) -- [macros - 宏定义系统](../macros.md) +--- +title: "plainproperty - 简单属性宏" +description: 是一个用来自动生成 getter 和 setter 方法的宏,减少手写样板代码的重复劳动。我们经常需 +--- + +# plain_property - 简单属性宏 + +`CF_PLAIN_PROPERTY` 是一个用来自动生成 getter 和 setter 方法的宏,减少手写样板代码的重复劳动。我们经常需要一些简单的数据持有类,它们只包含一些带默认值的成员变量和对应的访问器——这种代码写多了既无聊又容易出错,用宏来生成是更务实的选择。 + +## 基本用法 + +在类中使用 `CF_PLAIN_PROPERTY` 宏时,它会展开为一个私有成员变量和三个公共方法: + +```cpp +#include "base/macro/plain_property.h" + +class WidgetConfig { + // 生成: int width{800}; + // int& get_width(); + // const int& get_width_const() const; + // void set_width(const int&); + CF_PLAIN_PROPERTY(int, width, 800) + + // 生成: std::string title{"Untitled"}; + // ... + CF_PLAIN_PROPERTY(std::string, title, "Untitled") + +public: + // 其他成员方法... +}; +```text + +## 宏展开结果 + +`CF_PLAIN_PROPERTY(val_type, val_name, default_value)` 展开后的代码如下: + +```cpp +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}; +```text + +注意成员变量直接在 `private` 区域声明,而访问器在 `public` 区域。这是因为宏本身包含了访问修饰符,所以使用时不需要额外考虑这些。 + +## 使用示例 + +```cpp +#include "base/macro/plain_property.h" +#include +#include + +class AppConfig { + CF_PLAIN_PROPERTY(int, max_connections, 10) + CF_PLAIN_PROPERTY(std::string, host, "localhost") + CF_PLAIN_PROPERTY(bool, debug_mode, false) +}; + +int main() { + AppConfig config; + + // 使用 setter 修改值 + config.set_max_connections(100); + config.set_host("192.168.1.1"); + config.set_debug_mode(true); + + // 使用 getter 读取值 + std::cout << "Host: " << config.get_host() << std::endl; + std::cout << "Max connections: " << config.get_max_connections() << std::endl; + + // const getter 可以在 const 上下文调用 + const AppConfig& const_config = config; + std::cout << "Debug: " << std::boolalpha << (const_config.get_debug_mode_const()) << std::endl; + + return 0; +} +```text + +## 方法命名 + +宏生成的三个方法遵循特定命名规则: + +- `get_()`:返回非 const 引用,允许修改 +- `get__const()`:返回 const 引用,用于只读访问 +- `set_(const T&)`:设置新值 + +两个 getter 的区别在于 const 正确性。非 const 对象可以调用任何一个,const 对象只能调用 `_const` 版本。这个设计虽然有点冗余,但避免了在模板代码里处理 const 重载的麻烦。 + +## 设计动机 + +选择用宏而不是 C++ 的属性语法(如 `property` 关键字)是因为后者不是标准 C++ 的一部分,只有 MSVC 支持非标准的扩展。用宏虽然可读性稍差,但能保证跨编译器兼容。 + +另一个考虑是代码生成的一致性。手写 getter/setter 时容易漏掉 const 版本,或者命名不统一。宏强制了一种固定模式,减少了人为错误的可能性。 + +## 注意事项 + +⚠️ setter 使用 const 引用传参,对于基础类型(int、bool 等)来说不是最高效的选择。如果你对性能非常敏感,或者这些属性在热路径上被频繁修改,可能需要手写专门的版本。 + +⚠️ 宏展开会插入访问修饰符,如果在类的中间使用,会改变后续成员的访问级别。建议把所有 `CF_PLAIN_PROPERTY` 调用集中在类定义的开头或结尾,避免和手写的 public/private 区域混在一起: + +```cpp +// 好的做法:宏集中使用 +class Good { + CF_PLAIN_PROPERTY(int, x, 0) + CF_PLAIN_PROPERTY(int, y, 0) + +public: + void method(); + +private: + int internal_; +}; + +// 不好的做法:宏和手写访问修饰符混用 +class Bad { +public: + void method(); + CF_PLAIN_PROPERTY(int, x, 0) // 会插入 public:/private:,破坏原有结构 +}; +```text + +## 适用场景 + +`CF_PLAIN_PROPERTY` 适合用于简单的配置类、数据传输对象(DTO)或状态持有类。如果属性需要额外的验证逻辑、触发通知或线程安全保证,还是手写实现更合适。 + +## 相关文档 + +- [system_judge - 系统判断宏](./system_judge.md) +- [macros - 宏定义系统](../macros.md) diff --git a/document/HandBook/base/macro/system_judge.md b/document/HandBook/base/macro/system_judge.md index 9fbd5f8d1..00359ec94 100644 --- a/document/HandBook/base/macro/system_judge.md +++ b/document/HandBook/base/macro/system_judge.md @@ -1,122 +1,127 @@ -# system_judge - 系统判断宏 - -`system_judge.h` 定义了平台和架构检测的底层宏,是整个项目跨平台支持的基础。这个文件的设计原则很简单——只负责检测,不做任何决策,把判断结果以宏的形式暴露给上层使用。 - -## 操作系统检测 - -我们支持三个主流桌面操作系统,检测基于编译器预定义宏: - -```cpp -// Windows 平台检测(包括 32 位和 64 位) -#if defined(_WIN32) || defined(_WIN64) -# define CFDESKTOP_OS_WINDOWS -#endif - -// Linux 平台检测 -#if defined(__linux__) -# define CFDESKTOP_OS_LINUX -#endif -``` - -Windows 的检测用了两个宏是因为 `_WIN32` 在 64 位 Windows 上也会被定义,而 `_WIN64` 只在 64 位上定义。用 `||` 连接可以覆盖所有情况。Linux 的 `__linux__` 则是所有类 Linux 系统通用的宏,如果你需要区分发行版,得在运行时检查 `/etc/os-release`。 - -## 架构检测 - -CPU 架构检测主要服务于性能优化和 SIMD 指令使用: - -```cpp -// x86-64 (AMD64) 检测 -#if defined(__x86_64__) || defined(_M_X64) || defined(__amd64__) -# define CFDESKTOP_ARCH_X86_64 -#endif - -// ARM64 检测 -#if defined(__aarch64__) || defined(_M_ARM64) -# define CFDESKTOP_ARCH_ARM64 -#endif - -// ARM32 检测 -#if defined(__arm__) || defined(_M_ARM) -# define CFDESKTOP_ARCH_ARM32 -#endif -``` - -这里的 `__x86_64__` 和 `__aarch64__` 是 GCC/Clang 的约定,`_M_X64`、`_M_ARM64`、`_M_ARM` 是 MSVC 的约定。用多个宏检查是因为不同编译器的命名习惯不一样。 - -## 宏定义规则 - -每个宏定义都遵循相同的模式——检测到条件后定义一个不带值的标准宏: - -```cpp -#if defined() -# define CFDESKTOP__ -#endif -``` - -不带值的设计是有意为之的。我们只需要知道"是不是这个平台",不需要传递额外信息。如果需要细分版本(比如 Windows 10 vs Windows 11),应该在运行时检测,而不是用编译时宏。 - -## 使用示例 - -```cpp -#include "base/macro/system_judge.h" - -// 根据平台选择不同的实现 -#if defined(CFDESKTOP_OS_WINDOWS) - // Windows 实现 -#elif defined(CFDESKTOP_OS_LINUX) - // Linux 实现 -#else - #error "Unsupported platform" -#endif - -// 根据架构选择优化路径 -#if defined(CFDESKTOP_ARCH_X86_64) - // 使用 AVX2 指令集 -#elif defined(CFDESKTOP_ARCH_ARM64) - // 使用 NEON 指令集 -#endif -``` - -⚠️ 记得在 `#else` 或 `#elif` 分支加上 `#error`,避免在不支持的平台上静默编译通过,结果运行时出错。 - -## 编译器兼容性 - -这些宏在主流编译器上都能工作: - -| 编译器 | 平台宏支持 | 架构宏支持 | -|--------|-----------|-----------| -| MSVC | 完全支持 | 完全支持 | -| GCC | 完全支持 | 完全支持 | -| Clang | 完全支持 | 完全支持 | -| MinGW | 完全支持 | 完全支持 | - -如果你用了其他编译器(比如 Intel ICC),可能需要额外添加检测逻辑。 - -## 扩展新平台 - -添加新平台支持时,需要在两个地方修改代码: - -```cpp -// 1. 在 system_judge.h 添加检测 -#if defined(__NEW_OS__) -# define CFDESKTOP_OS_NEW_OS -#endif - -// 2. 在对应实现文件添加平台特定代码 -#if defined(CFDESKTOP_OS_NEW_OS) - // 新平台实现 -#endif -``` - -记得同步更新文档和测试,确保 CI 环境能在新平台上正常运行。 - -## 常见问题 - -为什么不用 C++20 的 `#ifdef`?因为我们需要支持 C++17,而且 `std::is_constant_evaluated()` 只能判断是否在常量求值上下文,不能区分平台。 - -为什么不用 CMake 的 `CMAKE_SYSTEM_NAME`?因为那是构建系统的信息,不能直接在代码里用。我们用宏是为了在预处理阶段就知道平台,实现条件编译。 - -## 相关文档 - -- [macros - 宏定义系统](../macros.md) -- [基础工具类概述](../overview.md) +--- +title: "systemjudge - 系统判断宏" +description: 定义了平台和架构检测的底层宏,是整个项目跨平台支持的基础。这个文件的设计原则很简单——只负责检测,不 +--- + +# system_judge - 系统判断宏 + +`system_judge.h` 定义了平台和架构检测的底层宏,是整个项目跨平台支持的基础。这个文件的设计原则很简单——只负责检测,不做任何决策,把判断结果以宏的形式暴露给上层使用。 + +## 操作系统检测 + +我们支持三个主流桌面操作系统,检测基于编译器预定义宏: + +```cpp +// Windows 平台检测(包括 32 位和 64 位) +#if defined(_WIN32) || defined(_WIN64) +# define CFDESKTOP_OS_WINDOWS +#endif + +// Linux 平台检测 +#if defined(__linux__) +# define CFDESKTOP_OS_LINUX +#endif +```text + +Windows 的检测用了两个宏是因为 `_WIN32` 在 64 位 Windows 上也会被定义,而 `_WIN64` 只在 64 位上定义。用 `||` 连接可以覆盖所有情况。Linux 的 `__linux__` 则是所有类 Linux 系统通用的宏,如果你需要区分发行版,得在运行时检查 `/etc/os-release`。 + +## 架构检测 + +CPU 架构检测主要服务于性能优化和 SIMD 指令使用: + +```cpp +// x86-64 (AMD64) 检测 +#if defined(__x86_64__) || defined(_M_X64) || defined(__amd64__) +# define CFDESKTOP_ARCH_X86_64 +#endif + +// ARM64 检测 +#if defined(__aarch64__) || defined(_M_ARM64) +# define CFDESKTOP_ARCH_ARM64 +#endif + +// ARM32 检测 +#if defined(__arm__) || defined(_M_ARM) +# define CFDESKTOP_ARCH_ARM32 +#endif +```text + +这里的 `__x86_64__` 和 `__aarch64__` 是 GCC/Clang 的约定,`_M_X64`、`_M_ARM64`、`_M_ARM` 是 MSVC 的约定。用多个宏检查是因为不同编译器的命名习惯不一样。 + +## 宏定义规则 + +每个宏定义都遵循相同的模式——检测到条件后定义一个不带值的标准宏: + +```cpp +#if defined() +# define CFDESKTOP__ +#endif +```text + +不带值的设计是有意为之的。我们只需要知道"是不是这个平台",不需要传递额外信息。如果需要细分版本(比如 Windows 10 vs Windows 11),应该在运行时检测,而不是用编译时宏。 + +## 使用示例 + +```cpp +#include "base/macro/system_judge.h" + +// 根据平台选择不同的实现 +#if defined(CFDESKTOP_OS_WINDOWS) + // Windows 实现 +#elif defined(CFDESKTOP_OS_LINUX) + // Linux 实现 +#else + #error "Unsupported platform" +#endif + +// 根据架构选择优化路径 +#if defined(CFDESKTOP_ARCH_X86_64) + // 使用 AVX2 指令集 +#elif defined(CFDESKTOP_ARCH_ARM64) + // 使用 NEON 指令集 +#endif +```bash + +⚠️ 记得在 `#else` 或 `#elif` 分支加上 `#error`,避免在不支持的平台上静默编译通过,结果运行时出错。 + +## 编译器兼容性 + +这些宏在主流编译器上都能工作: + +| 编译器 | 平台宏支持 | 架构宏支持 | +|--------|-----------|-----------| +| MSVC | 完全支持 | 完全支持 | +| GCC | 完全支持 | 完全支持 | +| Clang | 完全支持 | 完全支持 | +| MinGW | 完全支持 | 完全支持 | + +如果你用了其他编译器(比如 Intel ICC),可能需要额外添加检测逻辑。 + +## 扩展新平台 + +添加新平台支持时,需要在两个地方修改代码: + +```cpp +// 1. 在 system_judge.h 添加检测 +#if defined(__NEW_OS__) +# define CFDESKTOP_OS_NEW_OS +#endif + +// 2. 在对应实现文件添加平台特定代码 +#if defined(CFDESKTOP_OS_NEW_OS) + // 新平台实现 +#endif +```text + +记得同步更新文档和测试,确保 CI 环境能在新平台上正常运行。 + +## 常见问题 + +为什么不用 C++20 的 `#ifdef`?因为 `std::is_constant_evaluated()` 只能判断是否在常量求值上下文,不能区分平台。 + +为什么不用 CMake 的 `CMAKE_SYSTEM_NAME`?因为那是构建系统的信息,不能直接在代码里用。我们用宏是为了在预处理阶段就知道平台,实现条件编译。 + +## 相关文档 + +- [macros - 宏定义系统](../macros.md) +- [基础工具类概述](../overview.md) diff --git a/document/HandBook/base/macros.md b/document/HandBook/base/macros.md index d6c8236b0..afbd88788 100644 --- a/document/HandBook/base/macros.md +++ b/document/HandBook/base/macros.md @@ -1,99 +1,104 @@ -# macros - 宏定义系统 - -`macros.h` 是 CFDesktop 项目的宏定义入口文件,本身不直接定义宏,而是引入 `system_judge.h` 来完成平台检测。这个设计把平台相关的宏定义集中管理,避免在代码里到处散落 `#ifdef` 检查。 - -## 平台检测 - -项目目前支持 Windows 和 Linux 两个桌面平台,通过 `system_judge.h` 自动检测: - -```cpp -#include "base/macros.h" - -// 编译时会自动定义以下宏之一 -#ifdef CFDESKTOP_OS_WINDOWS - // Windows 特定代码 -#elif defined(CFDESKTOP_OS_LINUX) - // Linux 特定代码 -#endif -``` - -检测逻辑基于编译器预定义宏。Windows 平台检查 `_WIN32` 或 `_WIN64`,Linux 平台检查 `__linux__`。这些是主流编译器(MSVC、GCC、Clang)都遵守的约定。 - -## 架构检测 - -除了操作系统,我们还关心 CPU 架构,特别是在做性能优化或 SIMD 操作时: - -```cpp -#ifdef CFDESKTOP_ARCH_X86_64 - // x86-64 (AMD64) 特定代码 -#elif defined(CFDESKTOP_ARCH_ARM64) - // ARM64 特定代码 -#elif defined(CFDESKTOP_ARCH_ARM32) - // ARM32 特定代码 -#endif -``` - -x86-64 检查 `__x86_64__`、`_M_X64` 或 `__amd64__`,ARM64 检查 `__aarch64__` 或 `_M_ARM64`,ARM32 检查 `__arm__` 或 `_M_ARM`。这覆盖了我们项目目标平台的所有主流架构。 - -## 使用场景 - -平台检测宏最常见的用途是条件编译和平台特定代码隔离: - -```cpp -// 场景一:条件编译 -std::string get_config_path() { -#ifdef CFDESKTOP_OS_WINDOWS - return getenv("APPDATA") + std::string("/myapp/config"); -#elif defined(CFDESKTOP_OS_LINUX) - return getenv("HOME") + std::string("/.config/myapp"); -#endif -} - -// 场景二:平台特定头文件 -#include "base/macros.h" - -#ifdef CFDESKTOP_OS_WINDOWS - #include -#elif defined(CFDESKTOP_OS_LINUX) - #include -#endif - -// 场景三:不同平台的实现细节 -void set_thread_priority(int priority) { -#ifdef CFDESKTOP_OS_WINDOWS - SetThreadPriority(GetCurrentThread(), priority); -#elif defined(CFDESKTOP_OS_LINUX) - // Linux 实现省略... -#endif -} -``` - -## 命名约定 - -所有自定义宏都带 `CFDESKTOP_` 前缀,避免和第三方库或系统宏冲突。如果你需要添加新的平台检测宏,记得遵守这个约定。 - -## 注意事项 - -使用宏检测有几个需要注意的地方。第一,这些宏是编译时确定的,不能在运行时切换。如果你需要同一个二进制在多个平台运行,那得用动态加载或抽象工厂模式。 - -第二,不要过度使用平台宏。大部分跨平台差异应该通过封装类或函数来处理,而不是到处写 `#ifdef`。我们这套基础工具库的目的就是帮你隔离这些差异,让上层业务代码不需要关心平台细节。 - -第三,宏定义在预处理阶段生效,IDE 的跳转和重构工具通常不会跟踪它们。如果你在重构时修改了宏名字,记得全局搜索替换,不要只信任 IDE 的引用查找。 - -## 扩展指南 - -如果需要支持新的平台或架构,在 `system_judge.h` 中添加相应的检测逻辑: - -```cpp -// 添加新平台的示例 -#if defined(__NEW_OS__) -# define CFDESKTOP_OS_NEW_OS -#endif -``` - -然后记得在对应的地方添加平台特定实现,并写测试确保在新平台上能正确编译和运行。 - -## 相关文档 - -- [system_judge - 系统判断宏](./macro/system_judge.md) -- [基础工具类概述](./overview.md) +--- +title: "macros - 宏定义系统" +description: 是 CFDesktop 项目的宏定义入口文件,本身不直接定义宏,而是引入 来完成平台检测。这个设计 +--- + +# macros - 宏定义系统 + +`macros.h` 是 CFDesktop 项目的宏定义入口文件,本身不直接定义宏,而是引入 `system_judge.h` 来完成平台检测。这个设计把平台相关的宏定义集中管理,避免在代码里到处散落 `#ifdef` 检查。 + +## 平台检测 + +项目目前支持 Windows 和 Linux 两个桌面平台,通过 `system_judge.h` 自动检测: + +```cpp +#include "base/macros.h" + +// 编译时会自动定义以下宏之一 +#ifdef CFDESKTOP_OS_WINDOWS + // Windows 特定代码 +#elif defined(CFDESKTOP_OS_LINUX) + // Linux 特定代码 +#endif +```text + +检测逻辑基于编译器预定义宏。Windows 平台检查 `_WIN32` 或 `_WIN64`,Linux 平台检查 `__linux__`。这些是主流编译器(MSVC、GCC、Clang)都遵守的约定。 + +## 架构检测 + +除了操作系统,我们还关心 CPU 架构,特别是在做性能优化或 SIMD 操作时: + +```cpp +#ifdef CFDESKTOP_ARCH_X86_64 + // x86-64 (AMD64) 特定代码 +#elif defined(CFDESKTOP_ARCH_ARM64) + // ARM64 特定代码 +#elif defined(CFDESKTOP_ARCH_ARM32) + // ARM32 特定代码 +#endif +```text + +x86-64 检查 `__x86_64__`、`_M_X64` 或 `__amd64__`,ARM64 检查 `__aarch64__` 或 `_M_ARM64`,ARM32 检查 `__arm__` 或 `_M_ARM`。这覆盖了我们项目目标平台的所有主流架构。 + +## 使用场景 + +平台检测宏最常见的用途是条件编译和平台特定代码隔离: + +```cpp +// 场景一:条件编译 +std::string get_config_path() { +#ifdef CFDESKTOP_OS_WINDOWS + return getenv("APPDATA") + std::string("/myapp/config"); +#elif defined(CFDESKTOP_OS_LINUX) + return getenv("HOME") + std::string("/.config/myapp"); +#endif +} + +// 场景二:平台特定头文件 +#include "base/macros.h" + +#ifdef CFDESKTOP_OS_WINDOWS + #include +#elif defined(CFDESKTOP_OS_LINUX) + #include +#endif + +// 场景三:不同平台的实现细节 +void set_thread_priority(int priority) { +#ifdef CFDESKTOP_OS_WINDOWS + SetThreadPriority(GetCurrentThread(), priority); +#elif defined(CFDESKTOP_OS_LINUX) + // Linux 实现省略... +#endif +} +```text + +## 命名约定 + +所有自定义宏都带 `CFDESKTOP_` 前缀,避免和第三方库或系统宏冲突。如果你需要添加新的平台检测宏,记得遵守这个约定。 + +## 注意事项 + +使用宏检测有几个需要注意的地方。第一,这些宏是编译时确定的,不能在运行时切换。如果你需要同一个二进制在多个平台运行,那得用动态加载或抽象工厂模式。 + +第二,不要过度使用平台宏。大部分跨平台差异应该通过封装类或函数来处理,而不是到处写 `#ifdef`。我们这套基础工具库的目的就是帮你隔离这些差异,让上层业务代码不需要关心平台细节。 + +第三,宏定义在预处理阶段生效,IDE 的跳转和重构工具通常不会跟踪它们。如果你在重构时修改了宏名字,记得全局搜索替换,不要只信任 IDE 的引用查找。 + +## 扩展指南 + +如果需要支持新的平台或架构,在 `system_judge.h` 中添加相应的检测逻辑: + +```cpp +// 添加新平台的示例 +#if defined(__NEW_OS__) +# define CFDESKTOP_OS_NEW_OS +#endif +```text + +然后记得在对应的地方添加平台特定实现,并写测试确保在新平台上能正确编译和运行。 + +## 相关文档 + +- [system_judge - 系统判断宏](./macro/system_judge.md) +- [基础工具类概述](./overview.md) diff --git a/document/HandBook/base/mpsc_queue.md b/document/HandBook/base/mpsc_queue.md index aab3d6ce0..912c38e51 100644 --- a/document/HandBook/base/mpsc_queue.md +++ b/document/HandBook/base/mpsc_queue.md @@ -1,3 +1,8 @@ +--- +title: "MpscQueue - 无锁多生产者单消费者队列" +description: 是一个固定容量的环形缓冲区无锁队列,专为多生产者单消费者(MPSC)场景设计。它的典型应用场景是高性 +--- + # MpscQueue - 无锁多生产者单消费者队列 `cf::lockfree::MpscQueue` 是一个固定容量的环形缓冲区无锁队列,专为多生产者单消费者(MPSC)场景设计。它的典型应用场景是高性能日志系统、事件分发和任务队列。 @@ -26,7 +31,7 @@ int value; if (queue.tryPop(value)) { std::cout << "Got: " << value << std::endl; // Got: 42 } -``` +```text ### 批量操作 @@ -44,7 +49,7 @@ size_t popped = queue.tryPopBatch(buffer, 32); for (size_t i = 0; i < popped; ++i) { process(buffer[i]); } -``` +```text ### 容量查询 @@ -57,7 +62,7 @@ size_t approx_size = queue.size(); // 是否为空(近似值) bool empty = queue.empty(); -``` +```text ## 容量要求 @@ -67,7 +72,7 @@ bool empty = queue.empty(); cf::lockfree::MpscQueue q1; // OK: 1024 = 2^10 cf::lockfree::MpscQueue q2; // OK: 2048 = 2^11 cf::lockfree::MpscQueue q3; // 编译错误:static_assert 失败 -``` +```text 选择容量时应该考虑最坏情况下的生产速率和消费速率之差。如果生产者短暂 burst 产生了很多数据,队列需要有足够的缓冲空间。 @@ -113,7 +118,7 @@ while (seq != pos) { #endif seq = cell->sequence.load(std::memory_order_acquire); } -``` +```text 这意味着如果消费者跟不上,生产者线程会被阻塞在自旋中。如果你的场景可能出现持续的生产过剩,需要在上层做背压控制。 @@ -124,7 +129,7 @@ struct Cell { std::atomic sequence; // 序列号 alignas(alignof(T)) unsigned char storage[sizeof(T)]; // 原始存储 }; -``` +```text 每个槽位的存储是 `alignas(T)` 的原始字节数组,通过 placement new 构造对象。这样避免了不必要的默认构造,也支持没有默认构造函数的类型。 @@ -132,7 +137,7 @@ struct Cell { ```cpp char padding_[64 - sizeof(readPos_) - sizeof(writePos_) - sizeof(buffer_) % 64]; -``` +```bash ## 线程安全 diff --git a/document/HandBook/base/once_init.md b/document/HandBook/base/once_init.md index 63c1d7338..a9a41a78c 100644 --- a/document/HandBook/base/once_init.md +++ b/document/HandBook/base/once_init.md @@ -1,159 +1,164 @@ -# CallOnceInit - 懒加载初始化 - -`CallOnceInit` 是线程安全的懒加载模板,资源只在首次访问时初始化一次。我们用它来缓存那些获取开销大但不会变化的数据——比如 CPU 型号、内存大小这些系统信息。 - -## 基本用法 - -继承 `CallOnceInit<资源类型>`,然后重写 `init_resources()` 方法实现初始化逻辑: - -```cpp -#include "base/helpers/once_init.hpp" - -class CPUInfoCache : public cf::CallOnceInit { -protected: - bool init_resources() override { - // 执行实际的初始化 - resource.model = queryModel(); - resource.arch = queryArch(); - resource.manufacturer = queryManufacturer(); - return true; // 返回 true 表示初始化成功 - } - - bool force_do_reinit() override { - // 强制重新初始化时的逻辑 - return init_resources(); - } -}; -``` - -首次调用 `get_resources()` 时,`init_resources()` 会被自动调用。后续调用直接返回缓存的资源,不会再执行初始化。 - -## 使用示例 - -创建全局或静态实例,然后通过 `get_resources()` 访问: - -```cpp -static CPUInfoCache g_cpu_cache; - -void print_cpu_info() { - // 首次调用会执行初始化 - auto& info = g_cpu_cache.get_resources(); - - std::cout << "CPU: " << info.model << std::endl; - std::cout << "架构: " << info.arch << std::endl; - - // 后续调用返回缓存,不会重复查询 - auto& info2 = g_cpu_cache.get_resources(); - assert(&info == &info2); // 同一个对象 -} -``` - -## 重新初始化 - -如果需要强制刷新缓存,调用 `force_reinit()`: - -```cpp -// 强制重新初始化 -g_cpu_cache.force_reinit(); -``` - -带参数的版本可以在重新初始化时传入新参数: - -```cpp -class ConfigCache : public cf::CallOnceInit { -protected: - bool init_resources() override { - resource = load_default_config(); - return true; - } - - bool force_do_reinit() override { - // 使用新参数重新加载 - resource = load_config(new_config_path); - return true; - } - -private: - std::string new_config_path; -}; - -// 使用 -config.force_reinit("/etc/app/new_config.json"); -``` - -⚠️ `force_reinit()` 不是线程安全的。如果可能和 `get_resources()` 并发调用,需要自己加锁保护。 - -## 常见场景 - -### 缓存系统信息 - -```cpp -class SystemInfoCache : public cf::CallOnceInit { -protected: - bool init_resources() override { - resource.cpu_count = std::thread::hardware_concurrency(); - resource.total_memory = get_total_memory(); - return true; - } -}; - -SystemInfoCache sys_info; -auto& info = sys_info.get_resources(); -std::cout << "CPU 核心数: " << info.cpu_count << std::endl; -``` - -### 延迟加载配置 - -```cpp -class ConfigCache : public cf::CallOnceInit { -protected: - bool init_resources() override { - resource = load_config_file("/etc/app/config.json"); - return resource.valid(); - } - - bool force_do_reinit() override { - // 配置文件可能被外部修改,支持重新加载 - resource = load_config_file("/etc/app/config.json"); - return resource.valid(); - } -}; - -ConfigCache config; -if (config.get_resources().debug_mode) { - // ... -} -``` - -## 线程安全保证 - -`get_resources()` 是线程安全的。多个线程同时首次调用时,只有一个线程会执行 `init_resources()`,其他线程会等待完成: - -```cpp -// 以下代码是安全的 -std::thread t1([&]() { auto& info = cache.get_resources(); }); -std::thread t2([&]() { auto& info = cache.get_resources(); }); - -t1.join(); -t2.join(); -// init_resources() 只被执行一次 -``` - -但 `force_reinit()` 不是线程安全的。如果需要在线程间重新初始化,必须加锁: - -```cpp -std::mutex cache_mutex; -std::lock_guard lock(cache_mutex); -cache.force_reinit(); // 现在安全了 -``` - -## 注意事项 - -如果 `init_resources()` 返回 `false`,下次调用 `get_resources()` 会重试。这使得初始化失败可以恢复,但也意味着失败的初始化会被反复尝试——如果失败是不可恢复的,最好在初始化逻辑里处理。 - -另外,`CallOnceInit` 没有虚析构函数,这不是 bug,而是有意为之。我们不需要多态删除,避免虚函数开销。 - -## 相关文档 - -- [ScopeGuard - 资源管理](./scope_guard.md) -- [基础工具类概述](./overview.md) +--- +title: "CallOnceInit - 懒加载初始化" +description: 是线程安全的懒加载模板,资源只在首次访问时初始化一次。我们用它来缓存那些获取开销大但不会变化的数据— +--- + +# CallOnceInit - 懒加载初始化 + +`CallOnceInit` 是线程安全的懒加载模板,资源只在首次访问时初始化一次。我们用它来缓存那些获取开销大但不会变化的数据——比如 CPU 型号、内存大小这些系统信息。 + +## 基本用法 + +继承 `CallOnceInit<资源类型>`,然后重写 `init_resources()` 方法实现初始化逻辑: + +```cpp +#include "base/helpers/once_init.hpp" + +class CPUInfoCache : public cf::CallOnceInit { +protected: + bool init_resources() override { + // 执行实际的初始化 + resource.model = queryModel(); + resource.arch = queryArch(); + resource.manufacturer = queryManufacturer(); + return true; // 返回 true 表示初始化成功 + } + + bool force_do_reinit() override { + // 强制重新初始化时的逻辑 + return init_resources(); + } +}; +```text + +首次调用 `get_resources()` 时,`init_resources()` 会被自动调用。后续调用直接返回缓存的资源,不会再执行初始化。 + +## 使用示例 + +创建全局或静态实例,然后通过 `get_resources()` 访问: + +```cpp +static CPUInfoCache g_cpu_cache; + +void print_cpu_info() { + // 首次调用会执行初始化 + auto& info = g_cpu_cache.get_resources(); + + std::cout << "CPU: " << info.model << std::endl; + std::cout << "架构: " << info.arch << std::endl; + + // 后续调用返回缓存,不会重复查询 + auto& info2 = g_cpu_cache.get_resources(); + assert(&info == &info2); // 同一个对象 +} +```text + +## 重新初始化 + +如果需要强制刷新缓存,调用 `force_reinit()`: + +```cpp +// 强制重新初始化 +g_cpu_cache.force_reinit(); +```text + +带参数的版本可以在重新初始化时传入新参数: + +```cpp +class ConfigCache : public cf::CallOnceInit { +protected: + bool init_resources() override { + resource = load_default_config(); + return true; + } + + bool force_do_reinit() override { + // 使用新参数重新加载 + resource = load_config(new_config_path); + return true; + } + +private: + std::string new_config_path; +}; + +// 使用 +config.force_reinit("/etc/app/new_config.json"); +```text + +⚠️ `force_reinit()` 不是线程安全的。如果可能和 `get_resources()` 并发调用,需要自己加锁保护。 + +## 常见场景 + +### 缓存系统信息 + +```cpp +class SystemInfoCache : public cf::CallOnceInit { +protected: + bool init_resources() override { + resource.cpu_count = std::thread::hardware_concurrency(); + resource.total_memory = get_total_memory(); + return true; + } +}; + +SystemInfoCache sys_info; +auto& info = sys_info.get_resources(); +std::cout << "CPU 核心数: " << info.cpu_count << std::endl; +```text + +### 延迟加载配置 + +```cpp +class ConfigCache : public cf::CallOnceInit { +protected: + bool init_resources() override { + resource = load_config_file("/etc/app/config.json"); + return resource.valid(); + } + + bool force_do_reinit() override { + // 配置文件可能被外部修改,支持重新加载 + resource = load_config_file("/etc/app/config.json"); + return resource.valid(); + } +}; + +ConfigCache config; +if (config.get_resources().debug_mode) { + // ... +} +```text + +## 线程安全保证 + +`get_resources()` 是线程安全的。多个线程同时首次调用时,只有一个线程会执行 `init_resources()`,其他线程会等待完成: + +```cpp +// 以下代码是安全的 +std::thread t1([&]() { auto& info = cache.get_resources(); }); +std::thread t2([&]() { auto& info = cache.get_resources(); }); + +t1.join(); +t2.join(); +// init_resources() 只被执行一次 +```text + +但 `force_reinit()` 不是线程安全的。如果需要在线程间重新初始化,必须加锁: + +```cpp +std::mutex cache_mutex; +std::lock_guard lock(cache_mutex); +cache.force_reinit(); // 现在安全了 +```text + +## 注意事项 + +如果 `init_resources()` 返回 `false`,下次调用 `get_resources()` 会重试。这使得初始化失败可以恢复,但也意味着失败的初始化会被反复尝试——如果失败是不可恢复的,最好在初始化逻辑里处理。 + +另外,`CallOnceInit` 没有虚析构函数,这不是 bug,而是有意为之。我们不需要多态删除,避免虚函数开销。 + +## 相关文档 + +- [ScopeGuard - 资源管理](./scope_guard.md) +- [基础工具类概述](./overview.md) diff --git a/document/HandBook/base/overview.md b/document/HandBook/base/overview.md index bc64b28d0..4a0466b81 100644 --- a/document/HandBook/base/overview.md +++ b/document/HandBook/base/overview.md @@ -1,147 +1,152 @@ -# 基础工具库 - -基础工具库是 CFDesktop 最底层的模块,提供了一套零 Qt 依赖的跨平台工具。选择把这些工具独立出来,是因为上层 UI 框架可能需要在不同的上下文中复用——比如单元测试里不想链接整个 Qt 库,或者某些底层模块只需要一个轻量的错误处理机制。 - -这些工具大多是标准库特性的"降级实现"或"便利封装"。项目基于 C++17,但我们需要 C++20 的 `std::span` 和 C++23 的 `std::expected`,所以自己实现了一份。另外也有一些实战中总结的实用工具,比如 RAII 风格的资源管理。 - -## 错误处理 - -`expected` 是函数式错误处理的模板,用返回值显式表示可能的失败,而不是靠异常。选择这个方案主要考虑嵌入式环境通常禁用异常,而且显式的错误类型让调用者更清楚怎么处理。 - -```cpp -#include "base/expected/expected.hpp" - -enum class ErrorCode { InvalidInput, NotFound }; - -cf::expected parseNumber(std::string str) { - if (str.empty()) { - return cf::unexpected(ErrorCode::InvalidInput); - } - return std::stoi(str); -} - -// 使用时 -auto result = parseNumber("42"); -if (result.has_value()) { - std::cout << "值: " << result.value() << std::endl; -} else { - // 根据 error() 的值决定怎么恢复 -} -``` - -## 容器视图 - -`span` 提供对连续序列的非拥有视图,避免不必要的拷贝。函数参数用 `span` 替代 `const vector&`,调用方可以用数组、`vector`、`array` 等任何容器传入,灵活性高很多。 - -```cpp -#include "base/span/span.h" - -void process(cf::span data); - -std::vector vec = {1, 2, 3}; -int arr[] = {1, 2, 3}; - -process(vec); // OK -process(arr); // 也 OK -``` - -## 资源管理 - -`ScopeGuard` 确保 RAII 风格的清理操作在作用域结束时执行,即使中途抛出异常。这在管理 C 接口资源时特别有用——比如打开的文件句柄、分配的内存,或者需要手动释放的 COM 对象。 - -```cpp -#include "base/scope_guard/scope_guard.hpp" - -{ - FILE* f = fopen("data.txt", "r"); - cf::ScopeGuard guard([&f]() { fclose(f); }); - - // 使用文件,无论中间发生什么,离开作用域都会自动关闭 -} -``` - -## 懒加载初始化 - -`CallOnceInit` 是线程安全的懒加载模板,用 `std::call_once` 确保只初始化一次。我们用它来缓存系统信息——CPU 型号、内存大小这些东西不会变,每次都查太浪费。 - -```cpp -#include "base/helpers/once_init.hpp" - -class CPUInfoCache : public cf::CallOnceInit { -protected: - bool init_resources() override { - resource.model = queryModel(); - resource.arch = queryArch(); - return true; - } -}; - -// 首次调用 get_resources() 时才执行初始化 -auto& info = cache.get_resources(); -``` - -⚠️ `force_reinit()` 不是线程安全的,如果需要在运行时重新初始化,记得自己加锁。 - -## Linux 文件解析 - -`proc_parser` 提供了一套解析 `/proc` 和 `/sys` 伪文件系统的工具。Linux 下查硬件信息基本都要读这些文件,格式是固定的但处理起来很繁琐。这套工具用 `string_view` 避免拷贝,而且不抛异常。 - -```cpp -#include "base/linux/proc_parser.h" - -// 从 cpuinfo 行中提取字段 -std::string_view model = cf::parse_cpuinfo_field(line, "model name"); - -// 直接读单个数字值的文件 -auto freq = cf::read_uint32_file("/sys/devices/system/cpu/cpu0/cpufreq/max_freq"); -``` - -## 弱引用 - -`WeakPtr` 是一套非拥有的弱引用机制,和 `std::weak_ptr` 有本质区别。假设对象有唯一的拥有者,弱引用只是一个"取票凭证"——拥有者销毁后,所有凭证自动失效。配合 `WeakPtrFactory` 使用,为对象提供弱引用支持。 - -```cpp -#include "base/weak_ptr/weak_ptr_factory.h" - -class ThemeManager { -private: - cf::WeakPtrFactory weak_factory_{this}; // 最后一个成员 - -public: - cf::WeakPtr GetWeakPtr() { - return weak_factory_.GetWeakPtr(); - } -}; - -// 使用方 -auto weak = manager.GetWeakPtr(); -if (weak) { - weak->ApplyTheme(); // 安全访问 -} -``` - -## 平台检测 - -宏定义系统提供编译时的平台和架构检测。`CFDESKTOP_OS_WINDOWS`、`CFDESKTOP_OS_LINUX` 等宏在预处理阶段就知道目标平台,实现条件编译。 - -```cpp -#include "base/macros.h" - -#if defined(CFDESKTOP_OS_WINDOWS) - // Windows 特定代码 -#elif defined(CFDESKTOP_OS_LINUX) - // Linux 特定代码 -#endif -``` - -## 相关文档 - -- [expected 详解](./expected.md) -- [span 详解](./span.md) -- [ScopeGuard 详解](./scope_guard.md) -- [weak_ptr 详解](./weak_ptr.md) -- [weak_ptr_factory 详解](./weak_ptr_factory.md) -- [macros 详解](./macros.md) -- [system_judge 详解](./macro/system_judge.md) -- [CallOnceInit 详解](./once_init.md) -- [proc_parser 详解](../linux/proc_parser/) +--- +title: 基础工具库 +description: 基础工具库是 CFDesktop 最底层的模块,提供了一套零 Qt 依赖的跨平台工具。选择把这些工具 +--- + +# 基础工具库 + +基础工具库是 CFDesktop 最底层的模块,提供了一套零 Qt 依赖的跨平台工具。选择把这些工具独立出来,是因为上层 UI 框架可能需要在不同的上下文中复用——比如单元测试里不想链接整个 Qt 库,或者某些底层模块只需要一个轻量的错误处理机制。 + +这些工具大多是标准库特性的"降级实现"或"便利封装"。项目基于 C++23,部分编译器尚未完整支持 `std::expected` 等特性,所以自己实现了一份 backport。另外也有一些实战中总结的实用工具,比如 RAII 风格的资源管理。 + +## 错误处理 + +`expected` 是函数式错误处理的模板,用返回值显式表示可能的失败,而不是靠异常。选择这个方案主要考虑嵌入式环境通常禁用异常,而且显式的错误类型让调用者更清楚怎么处理。 + +```cpp +#include "base/expected/expected.hpp" + +enum class ErrorCode { InvalidInput, NotFound }; + +cf::expected parseNumber(std::string str) { + if (str.empty()) { + return cf::unexpected(ErrorCode::InvalidInput); + } + return std::stoi(str); +} + +// 使用时 +auto result = parseNumber("42"); +if (result.has_value()) { + std::cout << "值: " << result.value() << std::endl; +} else { + // 根据 error() 的值决定怎么恢复 +} +```text + +## 容器视图 + +`span` 提供对连续序列的非拥有视图,避免不必要的拷贝。函数参数用 `span` 替代 `const vector&`,调用方可以用数组、`vector`、`array` 等任何容器传入,灵活性高很多。 + +```cpp +#include "base/span/span.h" + +void process(cf::span data); + +std::vector vec = {1, 2, 3}; +int arr[] = {1, 2, 3}; + +process(vec); // OK +process(arr); // 也 OK +```text + +## 资源管理 + +`ScopeGuard` 确保 RAII 风格的清理操作在作用域结束时执行,即使中途抛出异常。这在管理 C 接口资源时特别有用——比如打开的文件句柄、分配的内存,或者需要手动释放的 COM 对象。 + +```cpp +#include "base/scope_guard/scope_guard.hpp" + +{ + FILE* f = fopen("data.txt", "r"); + cf::ScopeGuard guard([&f]() { fclose(f); }); + + // 使用文件,无论中间发生什么,离开作用域都会自动关闭 +} +```text + +## 懒加载初始化 + +`CallOnceInit` 是线程安全的懒加载模板,用 `std::call_once` 确保只初始化一次。我们用它来缓存系统信息——CPU 型号、内存大小这些东西不会变,每次都查太浪费。 + +```cpp +#include "base/helpers/once_init.hpp" + +class CPUInfoCache : public cf::CallOnceInit { +protected: + bool init_resources() override { + resource.model = queryModel(); + resource.arch = queryArch(); + return true; + } +}; + +// 首次调用 get_resources() 时才执行初始化 +auto& info = cache.get_resources(); +```text + +⚠️ `force_reinit()` 不是线程安全的,如果需要在运行时重新初始化,记得自己加锁。 + +## Linux 文件解析 + +`proc_parser` 提供了一套解析 `/proc` 和 `/sys` 伪文件系统的工具。Linux 下查硬件信息基本都要读这些文件,格式是固定的但处理起来很繁琐。这套工具用 `string_view` 避免拷贝,而且不抛异常。 + +```cpp +#include "base/linux/proc_parser.h" + +// 从 cpuinfo 行中提取字段 +std::string_view model = cf::parse_cpuinfo_field(line, "model name"); + +// 直接读单个数字值的文件 +auto freq = cf::read_uint32_file("/sys/devices/system/cpu/cpu0/cpufreq/max_freq"); +```text + +## 弱引用 + +`WeakPtr` 是一套非拥有的弱引用机制,和 `std::weak_ptr` 有本质区别。假设对象有唯一的拥有者,弱引用只是一个"取票凭证"——拥有者销毁后,所有凭证自动失效。配合 `WeakPtrFactory` 使用,为对象提供弱引用支持。 + +```cpp +#include "base/weak_ptr/weak_ptr_factory.h" + +class ThemeManager { +private: + cf::WeakPtrFactory weak_factory_{this}; // 最后一个成员 + +public: + cf::WeakPtr GetWeakPtr() { + return weak_factory_.GetWeakPtr(); + } +}; + +// 使用方 +auto weak = manager.GetWeakPtr(); +if (weak) { + weak->ApplyTheme(); // 安全访问 +} +```text + +## 平台检测 + +宏定义系统提供编译时的平台和架构检测。`CFDESKTOP_OS_WINDOWS`、`CFDESKTOP_OS_LINUX` 等宏在预处理阶段就知道目标平台,实现条件编译。 + +```cpp +#include "base/macros.h" + +#if defined(CFDESKTOP_OS_WINDOWS) + // Windows 特定代码 +#elif defined(CFDESKTOP_OS_LINUX) + // Linux 特定代码 +#endif +```text + +## 相关文档 + +- [expected 详解](./expected.md) +- [span 详解](./span.md) +- [ScopeGuard 详解](./scope_guard.md) +- [weak_ptr 详解](./weak_ptr.md) +- [weak_ptr_factory 详解](./weak_ptr_factory.md) +- [macros 详解](./macros.md) +- [system_judge 详解](./macro/system_judge.md) +- [CallOnceInit 详解](./once_init.md) +- [proc_parser 详解](../linux/proc_parser/) diff --git a/document/HandBook/base/policy_chain.md b/document/HandBook/base/policy_chain.md index 5cb228aef..a82e31f70 100644 --- a/document/HandBook/base/policy_chain.md +++ b/document/HandBook/base/policy_chain.md @@ -1,3 +1,8 @@ +--- +title: "PolicyChain - 策略链" +description: 是责任链模式(Chain of Responsibility)的一种实现,按优先级顺序执行一系列策略 +--- + # PolicyChain - 策略链 `cf::PolicyChain` 是责任链模式(Chain of Responsibility)的一种实现,按优先级顺序执行一系列策略函数。如果某个策略返回了有效值(非 `std::nullopt`),链就停止;如果返回 `std::nullopt`,则尝试下一个策略。这种模式特别适合"多种可能的解决方案,优先使用第一个有效的"场景。 @@ -19,7 +24,7 @@ std::optional get_font(const std::string& name) { if (auto f = try_builtin(name)) return f; return std::nullopt; } -``` +```text 这看起来还好,但当策略数量增加、策略需要动态注册时,硬编码的 if-else 就不够灵活了。PolicyChain 把这些策略变成可组合、可动态管理的链。 @@ -47,7 +52,7 @@ auto result = chain.execute("42"); if (result) { std::cout << "Result: " << *result << std::endl; // Result: 42 } -``` +```text ### 使用工厂函数 @@ -68,7 +73,7 @@ auto chain = cf::make_policy_chain( auto r1 = chain.execute("hello"); // 返回 std::optional(5) — stoi 失败但 length 可用 auto r2 = chain.execute("123"); // 返回 std::optional(123) — stoi 成功 auto r3 = chain.execute(""); // 返回 std::optional(0) — 兜底策略 -``` +```text `make_policy_chain` 按参数顺序添加策略,第一个参数优先级最高。 @@ -92,7 +97,7 @@ auto chain = cf::policy_chain_builder() auto r1 = chain.execute(5); // 10 — 第一个策略处理 auto r2 = chain.execute(-3); // 3 — 第二个策略处理 auto r3 = chain.execute(0); // 0 — 兜底策略 -``` +```text Builder 的 `then` 按调用顺序添加策略,先添加的优先级高。 @@ -119,7 +124,7 @@ template class PolicyChain { [[nodiscard]] bool empty() const; [[nodiscard]] SizeType size() const; }; -``` +```text ### 工厂函数和 Builder @@ -131,7 +136,7 @@ auto make_policy_chain(Policies&&... policies); // Builder 创建器 template auto policy_chain_builder(); -``` +```text ## 执行语义 @@ -148,7 +153,7 @@ if (result) { } else { // 所有策略都未能处理 } -``` +```text ## 典型使用场景 @@ -170,7 +175,7 @@ auto renderer_chain = cf::make_policy_chain( ); auto renderer = renderer_chain.execute(); -``` +```text ### 配置值解析 @@ -186,7 +191,7 @@ auto config_chain = cf::policy_chain_builder() return get_default(key); // 最后默认值 }) .build(); -``` +```text ### 资源加载 @@ -202,7 +207,7 @@ auto loader_chain = cf::make_policy_chain( return try_load_from_network(path); } ); -``` +```text ## 线程安全 diff --git a/document/HandBook/base/scope_guard.md b/document/HandBook/base/scope_guard.md index 02268b243..d9c07baa9 100644 --- a/document/HandBook/base/scope_guard.md +++ b/document/HandBook/base/scope_guard.md @@ -1,270 +1,275 @@ -# scope_guard - 作用域守卫 - -`ScopeGuard` 是 RAII(Resource Acquisition Is Initialization)模式的轻量级实现,确保一段代码在作用域结束时执行,无论是因为正常返回还是抛出异常。这个看似简单的工具在实际代码里非常好用——它能把"清理资源"和"业务逻辑"分开,让代码更清晰,也更容易避免资源泄漏。 - -## 为什么需要 ScopeGuard - -考虑一个需要手动管理的资源: - -```cpp -// 没有 ScopeGuard 的写法 -void process_file(const std::string& path) { - FILE* f = fopen(path.c_str(), "r"); - if (!f) return; - - void* buffer = malloc(1024); - if (!buffer) { - fclose(f); // 别忘了关闭文件 - return; - } - - if (some_condition) { - free(buffer); // 别忘了释放内存 - fclose(f); // 别忘了关闭文件 - return; - } - - // 更多代码... - - free(buffer); // 三个出口,三个地方写清理代码 - fclose(f); -} -``` - -每个可能的返回路径都要记得清理所有资源,漏一个就泄漏。用 `ScopeGuard` 就简单多了: - -```cpp -// 有 ScopeGuard 的写法 -void process_file(const std::string& path) { - FILE* f = fopen(path.c_str(), "r"); - if (!f) return; - cf::ScopeGuard close_file([&f]() { fclose(f); }); - - void* buffer = malloc(1024); - if (!buffer) return; - cf::ScopeGuard free_buffer([&buffer]() { free(buffer); }); - - if (some_condition) return; // 自动清理 - - // 更多代码... -} -``` - -无论从哪个路径退出,`ScopeGuard` 都会执行对应的清理代码。你不需要在每个返回点都写一遍,也不容易漏。 - -## 基本用法 - -`ScopeGuard` 接受一个可调用对象(通常是 lambda),在销毁时执行: - -```cpp -#include "base/scope_guard/scope_guard.hpp" - -{ - int counter = 0; - - cf::ScopeGuard guard([&counter]() { - counter = 42; - }); - - // 做一些事情... - // 离开作用域时 counter 变成 42 -} -``` - -lambda 按引用捕获 `counter`,所以在守卫内部可以修改它。你也可以按值捕获,看具体需求。 - -## 取消防护 - -有时候你不想让守卫执行清理代码,可以调用 `dismiss()`: - -```cpp -void save_config(const std::string& path) { - std::string temp_path = path + ".tmp"; - - // 创建临时文件 - FILE* f = fopen(temp_path.c_str(), "w"); - cf::ScopeGuard cleanup([&temp_path]() { - // 失败时删除临时文件 - std::remove(temp_path.c_str()); - }); - - // 写入配置... - - // 原子重命名 - if (std::rename(temp_path.c_str(), path.c_str()) == 0) { - cleanup.dismiss(); // 成功,不需要删除临时文件 - } -} -``` - -`dismiss()` 是不可逆的,一旦调用就不能再恢复。多次调用 `dismiss()` 是安全的,不会有额外效果。 - -## 多个守卫的执行顺序 - -同一个作用域可以有多个 `ScopeGuard`,它们按照**创建相反的顺序**执行: - -```cpp -{ - 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); }); -} -// order = {3, 2, 1} -``` - -这和 C++ 局部变量的析构顺序一致——后创建的先析构。这个顺序很重要,如果多个守卫之间有依赖,你需要知道哪个先执行。比如先分配的资源应该后释放(LIFO),正好符合这个顺序。 - -## 异常安全 - -`ScopeGuard` 的清理代码在异常抛出时也会执行: - -```cpp -try { - cf::ScopeGuard guard([]() { - printf("Cleanup executed\n"); - }); - - throw std::runtime_error("error"); - // guard 仍然会执行 -} catch (...) { - // 异常被捕获,但清理已经执行 -} -``` - -⚠️ 如果清理代码本身抛出异常,这个异常会传播出去。如果在栈展开过程中(已经有一个异常在处理)清理代码又抛出异常,程序会调用 `std::terminate`。所以确保清理代码不会抛异常,或者把可能抛异常的代码用 `try-catch` 包起来。 - -## 典型使用场景 - -### 文件句柄管理 - -```cpp -void read_config(const std::string& path) { - FILE* f = fopen(path.c_str(), "r"); - if (!f) return; - cf::ScopeGuard close_file([f]() { fclose(f); }); - - // 使用文件... - // 离开作用域自动关闭 -} -``` - -### 状态回滚 - -```cpp -void update_state(State& s) { - State backup = s; - - cf::ScopeGuard rollback([&s, backup]() { - s = backup; // 失败时恢复 - }); - - // 尝试修改状态... - - rollback.dismiss(); // 成功,不需要回滚 -} -``` - -### 锁的释放 - -```cpp -void critical_section() { - mutex.lock(); - cf::ScopeGuard unlock([&mutex]() { mutex.unlock(); }); - - // 临界区代码... -} -``` - -当然更推荐直接用 `std::lock_guard` 或 `std::unique_lock`,但 `ScopeGuard` 可以处理更复杂的场景。 - -### 恢复修改过的变量 - -```cpp -void process_item(Item& item) { - auto original_priority = item.priority(); - - cf::ScopeGuard restore([&item, original_priority]() { - item.set_priority(original_priority); - }); - - item.set_priority(Priority::High); - - // 临时提高优先级处理... - - // 离开作用域自动恢复 -} -``` - -## 限制和注意事项 - -`ScopeGuard` 不可复制也不可移动: - -```cpp -cf::ScopeGuard guard1([]() {}); - -cf::ScopeGuard guard2 = guard1; // 编译错误 -cf::ScopeGuard guard3 = std::move(guard1); // 编译错误 -``` - -这个设计是为了确保清理代码只执行一次。如果允许复制,同一个守卫可能被复制到多个地方,不清楚应该由谁负责清理。如果允许移动,移动后原守卫的清理代码就不应该再执行,但这会让语义变得复杂。 - -另一个限制是内部使用 `std::function` 存储,所以 lambda 必须可复制。这意味着你不能按值捕获只能移动的类型(如 `std::unique_ptr`)。解决方法是按引用捕获,或者用 `std::shared_ptr` 包装。 - -```cpp -// 这样不行 -auto ptr = std::make_unique(42); -cf::ScopeGuard guard([ptr]() {}); // 编译错误 - -// 可以这样 -auto ptr = std::make_unique(42); -cf::ScopeGuard guard([&ptr]() {}); // 按引用捕获 -``` - -## 性能考虑 - -`ScopeGuard` 的开销主要是 `std::function` 的类型擦除。一个守卫对象通常占用 32-64 字节(取决于平台),构造和析构各有一次虚函数调用(通过 `std::function` 的机制)。 - -在绝大多数场景下这个开销是可以忽略的。如果你在极度性能敏感的代码里使用,而且清理代码是固定的,可以考虑写一个专门的 RAII 类。但 99% 的情况下,`ScopeGuard` 的便利性远大于这点开销。 - -## 控制流交互 - -`ScopeGuard` 和各种控制流都能正确配合: - -```cpp -// early return -for (int i = 0; i < 10; ++i) { - cf::ScopeGuard guard([]() { /* cleanup */ }); - if (i == 5) return; // 仍然执行清理 -} - -// break/continue -for (int i = 0; i < 10; ++i) { - cf::ScopeGuard guard([i]() { printf("%d\n", i); }); - if (i == 5) break; // 守卫仍然执行 -} -// 输出: 0 1 2 3 4 5 - -// goto -{ - cf::ScopeGuard guard([]() { /* cleanup */ }); - goto end; -end:; -} -// 守卫仍然执行 -``` - -无论控制流怎么跳转,只要离开了守卫所在的作用域,清理代码就会执行。这得益于 C++ 的 RAII 机制——析构函数总会在作用域结束时被调用。 - -## 设计决策 - -我们没有使用更复杂的实现(比如支持 dismiss 后重新激活,或者支持移动),是因为简单够用。`ScopeGuard` 的定位就是"创建时注册,销毁时执行",额外的功能只会增加理解和维护成本。 - -也不提供类似 `SCOPE_EXIT` 宏的实现。宏确实可以让代码更紧凑,但会引入一些问题:调试时栈追踪不清楚,命名空间污染,以及宏展开的意外行为。显式的变量声明虽然多打几个字,但意图更清晰。 - -## 相关文档 - -- [weak_ptr - 弱引用指针](./weak_ptr.md) -- [expected - 错误处理](./expected.md) -- [基础工具类概述](./overview.md) +--- +title: "scopeguard - 作用域守卫" +description: 是 RAII(Resource Acquisition Is Initialization)模式的轻 +--- + +# scope_guard - 作用域守卫 + +`ScopeGuard` 是 RAII(Resource Acquisition Is Initialization)模式的轻量级实现,确保一段代码在作用域结束时执行,无论是因为正常返回还是抛出异常。这个看似简单的工具在实际代码里非常好用——它能把"清理资源"和"业务逻辑"分开,让代码更清晰,也更容易避免资源泄漏。 + +## 为什么需要 ScopeGuard + +考虑一个需要手动管理的资源: + +```cpp +// 没有 ScopeGuard 的写法 +void process_file(const std::string& path) { + FILE* f = fopen(path.c_str(), "r"); + if (!f) return; + + void* buffer = malloc(1024); + if (!buffer) { + fclose(f); // 别忘了关闭文件 + return; + } + + if (some_condition) { + free(buffer); // 别忘了释放内存 + fclose(f); // 别忘了关闭文件 + return; + } + + // 更多代码... + + free(buffer); // 三个出口,三个地方写清理代码 + fclose(f); +} +```text + +每个可能的返回路径都要记得清理所有资源,漏一个就泄漏。用 `ScopeGuard` 就简单多了: + +```cpp +// 有 ScopeGuard 的写法 +void process_file(const std::string& path) { + FILE* f = fopen(path.c_str(), "r"); + if (!f) return; + cf::ScopeGuard close_file([&f]() { fclose(f); }); + + void* buffer = malloc(1024); + if (!buffer) return; + cf::ScopeGuard free_buffer([&buffer]() { free(buffer); }); + + if (some_condition) return; // 自动清理 + + // 更多代码... +} +```text + +无论从哪个路径退出,`ScopeGuard` 都会执行对应的清理代码。你不需要在每个返回点都写一遍,也不容易漏。 + +## 基本用法 + +`ScopeGuard` 接受一个可调用对象(通常是 lambda),在销毁时执行: + +```cpp +#include "base/scope_guard/scope_guard.hpp" + +{ + int counter = 0; + + cf::ScopeGuard guard([&counter]() { + counter = 42; + }); + + // 做一些事情... + // 离开作用域时 counter 变成 42 +} +```text + +lambda 按引用捕获 `counter`,所以在守卫内部可以修改它。你也可以按值捕获,看具体需求。 + +## 取消防护 + +有时候你不想让守卫执行清理代码,可以调用 `dismiss()`: + +```cpp +void save_config(const std::string& path) { + std::string temp_path = path + ".tmp"; + + // 创建临时文件 + FILE* f = fopen(temp_path.c_str(), "w"); + cf::ScopeGuard cleanup([&temp_path]() { + // 失败时删除临时文件 + std::remove(temp_path.c_str()); + }); + + // 写入配置... + + // 原子重命名 + if (std::rename(temp_path.c_str(), path.c_str()) == 0) { + cleanup.dismiss(); // 成功,不需要删除临时文件 + } +} +```text + +`dismiss()` 是不可逆的,一旦调用就不能再恢复。多次调用 `dismiss()` 是安全的,不会有额外效果。 + +## 多个守卫的执行顺序 + +同一个作用域可以有多个 `ScopeGuard`,它们按照**创建相反的顺序**执行: + +```cpp +{ + 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); }); +} +// order = {3, 2, 1} +```text + +这和 C++ 局部变量的析构顺序一致——后创建的先析构。这个顺序很重要,如果多个守卫之间有依赖,你需要知道哪个先执行。比如先分配的资源应该后释放(LIFO),正好符合这个顺序。 + +## 异常安全 + +`ScopeGuard` 的清理代码在异常抛出时也会执行: + +```cpp +try { + cf::ScopeGuard guard([]() { + printf("Cleanup executed\n"); + }); + + throw std::runtime_error("error"); + // guard 仍然会执行 +} catch (...) { + // 异常被捕获,但清理已经执行 +} +```text + +⚠️ 如果清理代码本身抛出异常,这个异常会传播出去。如果在栈展开过程中(已经有一个异常在处理)清理代码又抛出异常,程序会调用 `std::terminate`。所以确保清理代码不会抛异常,或者把可能抛异常的代码用 `try-catch` 包起来。 + +## 典型使用场景 + +### 文件句柄管理 + +```cpp +void read_config(const std::string& path) { + FILE* f = fopen(path.c_str(), "r"); + if (!f) return; + cf::ScopeGuard close_file([f]() { fclose(f); }); + + // 使用文件... + // 离开作用域自动关闭 +} +```text + +### 状态回滚 + +```cpp +void update_state(State& s) { + State backup = s; + + cf::ScopeGuard rollback([&s, backup]() { + s = backup; // 失败时恢复 + }); + + // 尝试修改状态... + + rollback.dismiss(); // 成功,不需要回滚 +} +```text + +### 锁的释放 + +```cpp +void critical_section() { + mutex.lock(); + cf::ScopeGuard unlock([&mutex]() { mutex.unlock(); }); + + // 临界区代码... +} +```text + +当然更推荐直接用 `std::lock_guard` 或 `std::unique_lock`,但 `ScopeGuard` 可以处理更复杂的场景。 + +### 恢复修改过的变量 + +```cpp +void process_item(Item& item) { + auto original_priority = item.priority(); + + cf::ScopeGuard restore([&item, original_priority]() { + item.set_priority(original_priority); + }); + + item.set_priority(Priority::High); + + // 临时提高优先级处理... + + // 离开作用域自动恢复 +} +```text + +## 限制和注意事项 + +`ScopeGuard` 不可复制也不可移动: + +```cpp +cf::ScopeGuard guard1([]() {}); + +cf::ScopeGuard guard2 = guard1; // 编译错误 +cf::ScopeGuard guard3 = std::move(guard1); // 编译错误 +```text + +这个设计是为了确保清理代码只执行一次。如果允许复制,同一个守卫可能被复制到多个地方,不清楚应该由谁负责清理。如果允许移动,移动后原守卫的清理代码就不应该再执行,但这会让语义变得复杂。 + +另一个限制是内部使用 `std::function` 存储,所以 lambda 必须可复制。这意味着你不能按值捕获只能移动的类型(如 `std::unique_ptr`)。解决方法是按引用捕获,或者用 `std::shared_ptr` 包装。 + +```cpp +// 这样不行 +auto ptr = std::make_unique(42); +cf::ScopeGuard guard([ptr]() {}); // 编译错误 + +// 可以这样 +auto ptr = std::make_unique(42); +cf::ScopeGuard guard([&ptr]() {}); // 按引用捕获 +```text + +## 性能考虑 + +`ScopeGuard` 的开销主要是 `std::function` 的类型擦除。一个守卫对象通常占用 32-64 字节(取决于平台),构造和析构各有一次虚函数调用(通过 `std::function` 的机制)。 + +在绝大多数场景下这个开销是可以忽略的。如果你在极度性能敏感的代码里使用,而且清理代码是固定的,可以考虑写一个专门的 RAII 类。但 99% 的情况下,`ScopeGuard` 的便利性远大于这点开销。 + +## 控制流交互 + +`ScopeGuard` 和各种控制流都能正确配合: + +```cpp +// early return +for (int i = 0; i < 10; ++i) { + cf::ScopeGuard guard([]() { /* cleanup */ }); + if (i == 5) return; // 仍然执行清理 +} + +// break/continue +for (int i = 0; i < 10; ++i) { + cf::ScopeGuard guard([i]() { printf("%d\n", i); }); + if (i == 5) break; // 守卫仍然执行 +} +// 输出: 0 1 2 3 4 5 + +// goto +{ + cf::ScopeGuard guard([]() { /* cleanup */ }); + goto end; +end:; +} +// 守卫仍然执行 +```text + +无论控制流怎么跳转,只要离开了守卫所在的作用域,清理代码就会执行。这得益于 C++ 的 RAII 机制——析构函数总会在作用域结束时被调用。 + +## 设计决策 + +我们没有使用更复杂的实现(比如支持 dismiss 后重新激活,或者支持移动),是因为简单够用。`ScopeGuard` 的定位就是"创建时注册,销毁时执行",额外的功能只会增加理解和维护成本。 + +也不提供类似 `SCOPE_EXIT` 宏的实现。宏确实可以让代码更紧凑,但会引入一些问题:调试时栈追踪不清楚,命名空间污染,以及宏展开的意外行为。显式的变量声明虽然多打几个字,但意图更清晰。 + +## 相关文档 + +- [weak_ptr - 弱引用指针](./weak_ptr.md) +- [expected - 错误处理](./expected.md) +- [基础工具类概述](./overview.md) diff --git a/document/HandBook/base/singleton.md b/document/HandBook/base/singleton.md index c97406239..fe89380dd 100644 --- a/document/HandBook/base/singleton.md +++ b/document/HandBook/base/singleton.md @@ -1,3 +1,8 @@ +--- +title: "Singleton - 单例模式" +description: 命名空间提供了两种单例实现: 和 。两者的区别在于初始化方式—— 需要显式调用 并支持参数化构造, +--- + # Singleton - 单例模式 `cf` 命名空间提供了两种单例实现:`Singleton` 和 `SimpleSingleton`。两者的区别在于初始化方式——`Singleton` 需要显式调用 `init()` 并支持参数化构造,而 `SimpleSingleton` 利用 Meyer's Singleton 模式自动初始化,要求目标类有默认构造函数。 @@ -30,7 +35,7 @@ using LoggerSingleton = cf::SimpleSingleton; // 在任何地方 LoggerSingleton::instance().log("Hello"); -``` +```text 不需要手动初始化,第一次调用 `instance()` 时自动构造。后续调用返回同一个实例。 @@ -50,7 +55,7 @@ private: // 使用 auto& mgr = WindowManager::instance(); mgr.create_window("Main"); -``` +```text 注意把构造函数设为 `private` 或 `protected`,并声明 `SimpleSingleton` 为友元。 @@ -88,7 +93,7 @@ DBSingleton::init("host=localhost port=5432", 10); // 使用 DBSingleton::instance().query("SELECT * FROM users"); -``` +```text ### 初始化时机控制 @@ -110,7 +115,7 @@ int main() { // 阶段 3:运行 run_app(); } -``` +```text 如果调用 `instance()` 之前没有调用 `init()`,会抛出 `std::logic_error`: @@ -121,7 +126,7 @@ try { } catch (const std::logic_error& e) { // "Singleton not initialized. Call init() first." } -``` +```text ### 重置单例 @@ -137,7 +142,7 @@ void test_something() { // 下次使用前需要重新 init cf::Singleton::init("production_config.json"); } -``` +```text `reset()` 之后必须重新调用 `init()` 才能使用 `instance()`。 @@ -148,7 +153,7 @@ void test_something() { ```cpp cf::Singleton::init("config1.json"); // 生效 cf::Singleton::init("config2.json"); // 被忽略,仍使用 config1 -``` +```bash ## 两种单例对比 diff --git a/document/HandBook/base/span.md b/document/HandBook/base/span.md index 0b1904a29..67504e060 100644 --- a/document/HandBook/base/span.md +++ b/document/HandBook/base/span.md @@ -1,152 +1,157 @@ -# span - 容器视图 - -`span` 是 C++20 引入的容器视图类,提供对连续序列的非拥有访问。我们需要在 C++17 环境下使用,所以自己实现了一份。核心思想很简单——只持有指针和长度,不管理数据生命周期——这使得 `span` 可以零拷贝地"切分"任何连续容器。 - -## 为什么需要 span - -考虑一个函数需要接收整型数据的场景: - -```cpp -// 只能接受 vector -void process(const std::vector& data); - -// 只能接受 C 数组(但会退化成指针,丢失长度) -void process(int* data, size_t size); -``` - -第一种限制了调用方必须用 `vector`,第二种需要手动传长度而且容易出错。用 `span` 就没有这些问题: - -```cpp -// 可以接受任何容器 -void process(cf::span data); - -std::vector vec = {1, 2, 3}; -std::array arr = {1, 2, 3}; -int c_arr[] = {1, 2, 3}; - -process(vec); // OK -process(arr); // OK -process(c_arr); // OK -``` - -## 构造方式 - -`span` 可以从任何连续容器构造,编译器会自动推导大小: - -```cpp -#include "base/span/span.h" - -// 从 C 数组构造 -int arr[] = {1, 2, 3, 4, 5}; -cf::span s1 = arr; // 自动推导长度 - -// 从 vector 构造 -std::vector vec = {1, 2, 3}; -cf::span s2 = vec; - -// 从 array 构造 -std::array arr2 = {1, 2, 3, 4}; -cf::span s3 = arr2; - -// 手动指定指针和长度 -cf::span s4(vec.data(), vec.size()); -``` - -## 元素访问 - -访问元素的方式和标准容器一样,支持 `operator[]`、`front()`、`back()`: - -```cpp -cf::span s = /* ... */; - -int first = s[0]; // 下标访问 -int first2 = s.front(); // 首元素 -int last = s.back(); // 末元素 -int* ptr = s.data(); // 底层指针 -``` - -⚠️ `operator[]` 不做边界检查,越界访问是未定义行为。如果需要安全检查,标准库提供了 `at()` 方法,但我们的实现里为了性能省略了。 - -## 切片操作 - -`span` 最强大的功能是切片——可以轻松获取子视图而不拷贝数据: - -```cpp -cf::span s = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; - -// 前三个元素 -auto first_three = s.first(3); // {1, 2, 3} - -// 后三个元素 -auto last_three = s.last(3); // {8, 9, 10} - -// 中间的切片 -auto middle = s.subspan(2, 4); // {3, 4, 5, 6} - -// 从位置 2 到末尾 -auto tail = s.subspan(2); // {3, 4, 5, 6, 7, 8, 9, 10} -``` - -切片返回的新 `span` 仍指向原始数据,只是起始位置和长度不同。这意味着切片操作是 O(1) 的,没有任何拷贝开销。 - -## 函数参数 - -用 `span` 做函数参数是最常见的使用场景,特别是处理二进制数据或网络协议时: - -```cpp -// 写一个处理网络包的函数 -void process_packet(cf::span packet) { - if (packet.size() < 4) return; // 包头至少 4 字节 - - auto header = packet.first(4); - auto payload = packet.subspan(4); - - // 处理... -} - -// 调用方可以传入任何缓冲区 -std::vector buffer = read_from_socket(); -process_packet(buffer); - -uint8_t stack_buf[256]; -size_t received = recv(sock, stack_buf, 256, 0); -process_packet(cf::span(stack_buf, received)); -``` - -## const 正确性 - -`span` 表示只读视图,`span` 表示可写视图。选择正确的类型可以避免意外修改: - -```cpp -void read_only(cf::span data); -void read_write(cf::span data); - -const std::vector vec = {1, 2, 3}; - -read_only(vec); // OK -read_write(vec); // 编译错误 -``` - -## 生命周期陷阱 - -`span` 不拥有数据,只是一个"窗口"。如果底层数据被销毁,`span` 就会变成悬空引用: - -```cpp -cf::span get_bad_span() { - std::vector vec = {1, 2, 3}; - return vec; // 警告:vec 会被销毁,返回的 span 悬空 -} - -// 正确的做法 -cf::span get_good_span(const std::vector& vec) { - return vec; // OK,调用方保证 vec 有效 -} -``` - -这个坑在异步代码里特别容易出现——如果在一个线程里创建 `span`,另一个线程里使用,必须确保原始数据的生命周期足够长。 - -## 相关文档 - -- [expected - 错误处理](./expected.md) -- [ScopeGuard - 资源管理](./scope_guard.md) -- [基础工具类概述](./overview.md) +--- +title: "span - 容器视图" +description: 是 C++20 引入的容器视图类,提供对连续序列的非拥有访问。我们提供了一份 backport 实现 +--- + +# span - 容器视图 + +`span` 是 C++20 引入的容器视图类,提供对连续序列的非拥有访问。我们提供了一份 backport 实现,确保在所有目标平台上可用。核心思想很简单——只持有指针和长度,不管理数据生命周期——这使得 `span` 可以零拷贝地"切分"任何连续容器。 + +## 为什么需要 span + +考虑一个函数需要接收整型数据的场景: + +```cpp +// 只能接受 vector +void process(const std::vector& data); + +// 只能接受 C 数组(但会退化成指针,丢失长度) +void process(int* data, size_t size); +```text + +第一种限制了调用方必须用 `vector`,第二种需要手动传长度而且容易出错。用 `span` 就没有这些问题: + +```cpp +// 可以接受任何容器 +void process(cf::span data); + +std::vector vec = {1, 2, 3}; +std::array arr = {1, 2, 3}; +int c_arr[] = {1, 2, 3}; + +process(vec); // OK +process(arr); // OK +process(c_arr); // OK +```text + +## 构造方式 + +`span` 可以从任何连续容器构造,编译器会自动推导大小: + +```cpp +#include "base/span/span.h" + +// 从 C 数组构造 +int arr[] = {1, 2, 3, 4, 5}; +cf::span s1 = arr; // 自动推导长度 + +// 从 vector 构造 +std::vector vec = {1, 2, 3}; +cf::span s2 = vec; + +// 从 array 构造 +std::array arr2 = {1, 2, 3, 4}; +cf::span s3 = arr2; + +// 手动指定指针和长度 +cf::span s4(vec.data(), vec.size()); +```text + +## 元素访问 + +访问元素的方式和标准容器一样,支持 `operator[]`、`front()`、`back()`: + +```cpp +cf::span s = /* ... */; + +int first = s[0]; // 下标访问 +int first2 = s.front(); // 首元素 +int last = s.back(); // 末元素 +int* ptr = s.data(); // 底层指针 +```text + +⚠️ `operator[]` 不做边界检查,越界访问是未定义行为。如果需要安全检查,标准库提供了 `at()` 方法,但我们的实现里为了性能省略了。 + +## 切片操作 + +`span` 最强大的功能是切片——可以轻松获取子视图而不拷贝数据: + +```cpp +cf::span s = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + +// 前三个元素 +auto first_three = s.first(3); // {1, 2, 3} + +// 后三个元素 +auto last_three = s.last(3); // {8, 9, 10} + +// 中间的切片 +auto middle = s.subspan(2, 4); // {3, 4, 5, 6} + +// 从位置 2 到末尾 +auto tail = s.subspan(2); // {3, 4, 5, 6, 7, 8, 9, 10} +```text + +切片返回的新 `span` 仍指向原始数据,只是起始位置和长度不同。这意味着切片操作是 O(1) 的,没有任何拷贝开销。 + +## 函数参数 + +用 `span` 做函数参数是最常见的使用场景,特别是处理二进制数据或网络协议时: + +```cpp +// 写一个处理网络包的函数 +void process_packet(cf::span packet) { + if (packet.size() < 4) return; // 包头至少 4 字节 + + auto header = packet.first(4); + auto payload = packet.subspan(4); + + // 处理... +} + +// 调用方可以传入任何缓冲区 +std::vector buffer = read_from_socket(); +process_packet(buffer); + +uint8_t stack_buf[256]; +size_t received = recv(sock, stack_buf, 256, 0); +process_packet(cf::span(stack_buf, received)); +```text + +## const 正确性 + +`span` 表示只读视图,`span` 表示可写视图。选择正确的类型可以避免意外修改: + +```cpp +void read_only(cf::span data); +void read_write(cf::span data); + +const std::vector vec = {1, 2, 3}; + +read_only(vec); // OK +read_write(vec); // 编译错误 +```text + +## 生命周期陷阱 + +`span` 不拥有数据,只是一个"窗口"。如果底层数据被销毁,`span` 就会变成悬空引用: + +```cpp +cf::span get_bad_span() { + std::vector vec = {1, 2, 3}; + return vec; // 警告:vec 会被销毁,返回的 span 悬空 +} + +// 正确的做法 +cf::span get_good_span(const std::vector& vec) { + return vec; // OK,调用方保证 vec 有效 +} +```text + +这个坑在异步代码里特别容易出现——如果在一个线程里创建 `span`,另一个线程里使用,必须确保原始数据的生命周期足够长。 + +## 相关文档 + +- [expected - 错误处理](./expected.md) +- [ScopeGuard - 资源管理](./scope_guard.md) +- [基础工具类概述](./overview.md) diff --git a/document/HandBook/base/weak_ptr.md b/document/HandBook/base/weak_ptr.md index e919f45dd..a01799d90 100644 --- a/document/HandBook/base/weak_ptr.md +++ b/document/HandBook/base/weak_ptr.md @@ -1,248 +1,253 @@ -# weak_ptr - 非拥有弱引用指针 - -`WeakPtr` 是一套非拥有的弱引用机制,和 `std::weak_ptr` 有本质区别。标准库的 `weak_ptr` 依赖引用计数,配合 `shared_ptr` 使用,而我们的 `WeakPtr` 假设对象有唯一的拥有者,它只是一个"取票凭证"——拥有者销毁后,所有凭证自动失效。 - -## 为什么需要 WeakPtr - -标准库的 `weak_ptr` 在某些场景下过于重量级。你需要把对象用 `shared_ptr` 管理,即使对象本身有明确的生命周期所有者。这会带来额外的引用计数开销,而且容易误用——比如两个 `shared_ptr` 互相持有对方的 `weak_ptr`,本意是想打破循环,结果导致对象永远不被释放。 - -我们的设计更简单直接:对象有明确的拥有者(通常是栈上的对象或者唯一的 `unique_ptr`),其他地方只持有弱引用。拥有者销毁时,所有弱引用自动失效,不需要引用计数参与。 - -## 基本用法 - -`WeakPtr` 需要配合 `WeakPtrFactory` 使用。在拥有者类中声明一个工厂成员,然后通过它创建弱引用: - -```cpp -#include "base/weak_ptr/weak_ptr_factory.h" - -class ThemeManager { -public: - void ApplyTheme() { /* ... */ } - - // 提供获取弱引用的接口 - cf::WeakPtr GetWeakPtr() { - return weak_factory_.GetWeakPtr(); - } - -private: - // 重要:必须是最后一个成员变量 - cf::WeakPtrFactory weak_factory_{this}; -}; - -// 使用方 -ThemeManager manager; -auto weak_ref = manager.GetWeakPtr(); - -if (weak_ref) { // 检查对象是否存活 - weak_ref->ApplyTheme(); -} -``` - -⚠️ `WeakPtrFactory` 必须声明为类的最后一个成员。C++ 按声明顺序的逆序销毁成员,这样可以确保工厂先失效,其他成员的析构函数中如果持有弱引用也能正确检测到失效。 - -## 访问方式 - -`WeakPtr` 提供了多种访问对象的方式: - -```cpp -cf::WeakPtr weak = /* ... */; - -// 方式一:显式检查后访问 -if (weak.IsValid()) { // 或 if (weak) - weak->DoSomething(); // 安全访问 -} - -// 方式二:获取原始指针 -if (MyClass* ptr = weak.Get()) { - ptr->DoSomething(); -} - -// 方式三:直接解引用(会断言,仅确定对象存在时使用) -*weak; // 如果无效会触发 assert -weak->Method(); // 同上 -``` - -直接解引用会触发断言,这是有意为之的设计。如果你用了 `operator->` 或 `operator*`,说明你已经确定对象存在,不会再检查。如果你不敢确定,应该用 `Get()` 或 `IsValid()` 先检查。 - -## 生命周期管理 - -`WeakPtr` 不参与对象的生命周期管理,它只是一个观察者: - -```cpp -{ - MyClass obj; - auto weak = obj.GetWeakPtr(); - - // 对象存活 - assert(weak.IsValid()); - -} // obj 销毁,weak 自动失效 - -assert(!weak.IsValid()); -assert(weak.Get() == nullptr); -``` - -这个设计避免了 `shared_ptr` 的隐式生命周期延长问题。持有 `WeakPtr` 不会阻止对象被销毁,这也是它和 `std::weak_ptr` 的核心区别之一。 - -## 类型转换 - -`WeakPtr` 支持协变类型转换,子类到父类的转换是隐式的: - -```cpp -class Base { -public: - virtual ~Base() = default; - void BaseMethod() {} -}; - -class Derived : public Base { -public: - void DerivedMethod() {} -}; - -Derived derived; -cf::WeakPtr derived_weak = derived.GetWeakPtr(); - -// 隐式向上转换 -cf::WeakPtr base_weak = derived_weak; -if (base_weak) { - base_weak->BaseMethod(); // OK -} - -// 显式向下转换(使用 DynamicCast) -cf::WeakPtr derived_again = - cf::WeakPtr::DynamicCast(base_weak); -if (derived_again) { - derived_again->DerivedMethod(); -} -``` - -`DynamicCast` 会在运行时检查类型,如果转换失败返回无效的 `WeakPtr`。这个操作不是免费的,但比直接 `dynamic_cast` 原始指针要安全,因为转换失败得到的是空指针而不是未定义行为。 - -## 线程安全考虑 - -`WeakPtr` **不是线程安全的**,应该在同一个线程(或序列)中使用。"检查有效性"和"访问对象"这两步操作不是原子的: - -```cpp -// 危险:多线程环境下 -// 线程 1 -if (weak.IsValid()) { // 检查通过 - // 线程 2 在这里销毁了对象 - weak->Method(); // 未定义行为! -} - -// 正确做法:在单线程序列中使用 -// 或者用其他同步机制保护整个检查+访问过程 -``` - -这个限制和 `std::weak_ptr::lock()` 不一样。标准库的 `lock()` 是原子的,可以返回一个 `shared_ptr` 保证对象在使用期间存活。我们选择不提供这个功能,是因为项目里的使用场景大多是单线程的,不需要这个开销。 - -## 手动失效 - -`WeakPtrFactory` 提供了手动使所有弱引用失效的接口: - -```cpp -class MyClass { -public: - void InvalidateAllRefs() { - weak_factory_.InvalidateWeakPtrs(); - } - - cf::WeakPtr GetWeakPtr() { - return weak_factory_.GetWeakPtr(); - } - -private: - cf::WeakPtrFactory weak_factory_{this}; -}; - -// 使用 -MyClass obj; -auto weak1 = obj.GetWeakPtr(); -auto weak2 = obj.GetWeakPtr(); - -assert(weak1.IsValid()); // 有效 - -obj.InvalidateAllRefs(); // 手动失效 - -assert(!weak1.IsValid()); // 失效 -assert(!weak2.IsValid()); // 失效 - -// 失效后仍然可以创建新的弱引用 -auto weak3 = obj.GetWeakPtr(); -assert(weak3.IsValid()); // 新的弱引用有效 -``` - -这个功能在某些场景下很有用,比如你想显式通知所有观察者对象不再可用,但又不想真的销毁对象。注意失效后创建的新弱引用是有效的,因为工厂内部会分配新的标志位。 - -## 检查外部引用 - -`WeakPtrFactory::HasWeakPtrs()` 可以检查是否有外部持有的弱引用: - -```cpp -class MyClass { -private: - cf::WeakPtrFactory weak_factory_{this}; - -public: - bool HasExternalReferences() const { - return weak_factory_.HasWeakPtrs(); - } -}; -``` - -这个接口通过 `shared_ptr::use_count()` 实现,所以是 O(1) 的。如果 `use_count() > 1`,说明除了工厂自己外,还有其他地方持有弱引用标志位。这个功能在调试或内存泄漏排查时有用。 - -## 与 std::weak_ptr 的对比 - -| 特性 | cf::WeakPtr | std::weak_ptr | -|------|-------------|---------------| -| 所有权模型 | 独占拥有者 | 引用计数 | -| 性能开销 | 极小 | 较小(引用计数) | -| 线程安全 | 不保证 | lock() 原子操作 | -| 使用场景 | 明确生命周期的对象 | 共享所有权 | -| 依赖工厂 | 需要 WeakPtrFactory | 需要 shared_ptr | - -选择 `cf::WeakPtr` 的场景通常满足这些条件:对象有明确的拥有者、不需要跨线程共享、想避免引用计数开销。如果你的对象本身就需要用 `shared_ptr` 管理,那直接用 `std::weak_ptr` 更合适。 - -## 常见陷阱 - -第一个陷阱是在 `WeakPtrFactory` 之前声明其他成员。如果这些成员的析构函数里尝试访问弱引用,会看到对象已经"失效",因为工厂先销毁了: - -```cpp -// 错误示例 -class Bad { -public: - Bad() : weak_factory_(this) {} - -private: - SomeResource resource_; // 先销毁 - cf::WeakPtrFactory weak_factory_{this}; // 后销毁 -}; - -// resource_ 的析构函数中,如果持有 Bad 的 WeakPtr,会看到失效 -``` - -第二个陷阱是忘记检查有效性直接访问。这在异步代码里特别容易出现,因为回调执行时对象可能已经被销毁: - -```cpp -// 危险 -auto weak = obj.GetWeakPtr(); -post_task([weak]() { - weak->Method(); // 如果 obj 已经被销毁,这里是 UB -}); - -// 正确 -auto weak = obj.GetWeakPtr(); -post_task([weak]() { - if (weak) { - weak->Method(); - } -}); -``` - -## 相关文档 - -- [weak_ptr_factory - 弱引用工厂](./weak_ptr_factory.md) -- [ScopeGuard - 作用域守卫](./scope_guard.md) -- [基础工具类概述](./overview.md) +--- +title: "weakptr - 非拥有弱引用指针" +description: 是一套非拥有的弱引用机制,和 有本质区别。标准库的 依赖引用计数,配合 使用,而我们的 假设 +--- + +# weak_ptr - 非拥有弱引用指针 + +`WeakPtr` 是一套非拥有的弱引用机制,和 `std::weak_ptr` 有本质区别。标准库的 `weak_ptr` 依赖引用计数,配合 `shared_ptr` 使用,而我们的 `WeakPtr` 假设对象有唯一的拥有者,它只是一个"取票凭证"——拥有者销毁后,所有凭证自动失效。 + +## 为什么需要 WeakPtr + +标准库的 `weak_ptr` 在某些场景下过于重量级。你需要把对象用 `shared_ptr` 管理,即使对象本身有明确的生命周期所有者。这会带来额外的引用计数开销,而且容易误用——比如两个 `shared_ptr` 互相持有对方的 `weak_ptr`,本意是想打破循环,结果导致对象永远不被释放。 + +我们的设计更简单直接:对象有明确的拥有者(通常是栈上的对象或者唯一的 `unique_ptr`),其他地方只持有弱引用。拥有者销毁时,所有弱引用自动失效,不需要引用计数参与。 + +## 基本用法 + +`WeakPtr` 需要配合 `WeakPtrFactory` 使用。在拥有者类中声明一个工厂成员,然后通过它创建弱引用: + +```cpp +#include "base/weak_ptr/weak_ptr_factory.h" + +class ThemeManager { +public: + void ApplyTheme() { /* ... */ } + + // 提供获取弱引用的接口 + cf::WeakPtr GetWeakPtr() { + return weak_factory_.GetWeakPtr(); + } + +private: + // 重要:必须是最后一个成员变量 + cf::WeakPtrFactory weak_factory_{this}; +}; + +// 使用方 +ThemeManager manager; +auto weak_ref = manager.GetWeakPtr(); + +if (weak_ref) { // 检查对象是否存活 + weak_ref->ApplyTheme(); +} +```text + +⚠️ `WeakPtrFactory` 必须声明为类的最后一个成员。C++ 按声明顺序的逆序销毁成员,这样可以确保工厂先失效,其他成员的析构函数中如果持有弱引用也能正确检测到失效。 + +## 访问方式 + +`WeakPtr` 提供了多种访问对象的方式: + +```cpp +cf::WeakPtr weak = /* ... */; + +// 方式一:显式检查后访问 +if (weak.IsValid()) { // 或 if (weak) + weak->DoSomething(); // 安全访问 +} + +// 方式二:获取原始指针 +if (MyClass* ptr = weak.Get()) { + ptr->DoSomething(); +} + +// 方式三:直接解引用(会断言,仅确定对象存在时使用) +*weak; // 如果无效会触发 assert +weak->Method(); // 同上 +```cpp + +直接解引用会触发断言,这是有意为之的设计。如果你用了 `operator->` 或 `operator*`,说明你已经确定对象存在,不会再检查。如果你不敢确定,应该用 `Get()` 或 `IsValid()` 先检查。 + +## 生命周期管理 + +`WeakPtr` 不参与对象的生命周期管理,它只是一个观察者: + +```cpp +{ + MyClass obj; + auto weak = obj.GetWeakPtr(); + + // 对象存活 + assert(weak.IsValid()); + +} // obj 销毁,weak 自动失效 + +assert(!weak.IsValid()); +assert(weak.Get() == nullptr); +```text + +这个设计避免了 `shared_ptr` 的隐式生命周期延长问题。持有 `WeakPtr` 不会阻止对象被销毁,这也是它和 `std::weak_ptr` 的核心区别之一。 + +## 类型转换 + +`WeakPtr` 支持协变类型转换,子类到父类的转换是隐式的: + +```cpp +class Base { +public: + virtual ~Base() = default; + void BaseMethod() {} +}; + +class Derived : public Base { +public: + void DerivedMethod() {} +}; + +Derived derived; +cf::WeakPtr derived_weak = derived.GetWeakPtr(); + +// 隐式向上转换 +cf::WeakPtr base_weak = derived_weak; +if (base_weak) { + base_weak->BaseMethod(); // OK +} + +// 显式向下转换(使用 DynamicCast) +cf::WeakPtr derived_again = + cf::WeakPtr::DynamicCast(base_weak); +if (derived_again) { + derived_again->DerivedMethod(); +} +```text + +`DynamicCast` 会在运行时检查类型,如果转换失败返回无效的 `WeakPtr`。这个操作不是免费的,但比直接 `dynamic_cast` 原始指针要安全,因为转换失败得到的是空指针而不是未定义行为。 + +## 线程安全考虑 + +`WeakPtr` **不是线程安全的**,应该在同一个线程(或序列)中使用。"检查有效性"和"访问对象"这两步操作不是原子的: + +```cpp +// 危险:多线程环境下 +// 线程 1 +if (weak.IsValid()) { // 检查通过 + // 线程 2 在这里销毁了对象 + weak->Method(); // 未定义行为! +} + +// 正确做法:在单线程序列中使用 +// 或者用其他同步机制保护整个检查+访问过程 +```text + +这个限制和 `std::weak_ptr::lock()` 不一样。标准库的 `lock()` 是原子的,可以返回一个 `shared_ptr` 保证对象在使用期间存活。我们选择不提供这个功能,是因为项目里的使用场景大多是单线程的,不需要这个开销。 + +## 手动失效 + +`WeakPtrFactory` 提供了手动使所有弱引用失效的接口: + +```cpp +class MyClass { +public: + void InvalidateAllRefs() { + weak_factory_.InvalidateWeakPtrs(); + } + + cf::WeakPtr GetWeakPtr() { + return weak_factory_.GetWeakPtr(); + } + +private: + cf::WeakPtrFactory weak_factory_{this}; +}; + +// 使用 +MyClass obj; +auto weak1 = obj.GetWeakPtr(); +auto weak2 = obj.GetWeakPtr(); + +assert(weak1.IsValid()); // 有效 + +obj.InvalidateAllRefs(); // 手动失效 + +assert(!weak1.IsValid()); // 失效 +assert(!weak2.IsValid()); // 失效 + +// 失效后仍然可以创建新的弱引用 +auto weak3 = obj.GetWeakPtr(); +assert(weak3.IsValid()); // 新的弱引用有效 +```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`,说明除了工厂自己外,还有其他地方持有弱引用标志位。这个功能在调试或内存泄漏排查时有用。 + +## 与 std::weak_ptr 的对比 + +| 特性 | cf::WeakPtr | std::weak_ptr | +|------|-------------|---------------| +| 所有权模型 | 独占拥有者 | 引用计数 | +| 性能开销 | 极小 | 较小(引用计数) | +| 线程安全 | 不保证 | lock() 原子操作 | +| 使用场景 | 明确生命周期的对象 | 共享所有权 | +| 依赖工厂 | 需要 WeakPtrFactory | 需要 shared_ptr | + +选择 `cf::WeakPtr` 的场景通常满足这些条件:对象有明确的拥有者、不需要跨线程共享、想避免引用计数开销。如果你的对象本身就需要用 `shared_ptr` 管理,那直接用 `std::weak_ptr` 更合适。 + +## 常见陷阱 + +第一个陷阱是在 `WeakPtrFactory` 之前声明其他成员。如果这些成员的析构函数里尝试访问弱引用,会看到对象已经"失效",因为工厂先销毁了: + +```cpp +// 错误示例 +class Bad { +public: + Bad() : weak_factory_(this) {} + +private: + SomeResource resource_; // 先销毁 + cf::WeakPtrFactory weak_factory_{this}; // 后销毁 +}; + +// resource_ 的析构函数中,如果持有 Bad 的 WeakPtr,会看到失效 +```text + +第二个陷阱是忘记检查有效性直接访问。这在异步代码里特别容易出现,因为回调执行时对象可能已经被销毁: + +```cpp +// 危险 +auto weak = obj.GetWeakPtr(); +post_task([weak]() { + weak->Method(); // 如果 obj 已经被销毁,这里是 UB +}); + +// 正确 +auto weak = obj.GetWeakPtr(); +post_task([weak]() { + if (weak) { + weak->Method(); + } +}); +```text + +## 相关文档 + +- [weak_ptr_factory - 弱引用工厂](./weak_ptr_factory.md) +- [ScopeGuard - 作用域守卫](./scope_guard.md) +- [基础工具类概述](./overview.md) diff --git a/document/HandBook/base/weak_ptr_factory.md b/document/HandBook/base/weak_ptr_factory.md index b0b2d109b..0d29e4fe5 100644 --- a/document/HandBook/base/weak_ptr_factory.md +++ b/document/HandBook/base/weak_ptr_factory.md @@ -1,233 +1,238 @@ -# weak_ptr_factory - 弱引用工厂 - -`WeakPtrFactory` 是弱引用机制的核心工厂类,负责创建和管理某个对象的弱引用。每个需要对外提供弱引用的对象都应该持有一个 `WeakPtrFactory` 成员,把它作为接口的守门员。 - -## 基本用法 - -在类中添加弱引用支持很简单,声明一个 `WeakPtrFactory` 成员即可: - -```cpp -#include "base/weak_ptr/weak_ptr_factory.h" - -class NetworkManager { -public: - void SendRequest(const std::string& url) { - // 发送请求... - } - - // 对外提供弱引用获取接口 - cf::WeakPtr GetWeakPtr() { - return weak_factory_.GetWeakPtr(); - } - -private: - // 必须是最后一个成员变量 - cf::WeakPtrFactory weak_factory_{this}; -}; -``` - -构造时传入 `this` 指针,工厂会记住对象的位置。每次调用 `GetWeakPtr()` 就会创建一个新的弱引用,指向同一个对象。 - -## 成员变量顺序 - -⚠️ 这是使用 `WeakPtrFactory` 最重要的规则:**必须声明为类的最后一个成员**。 - -原因在于 C++ 的析构顺序——成员变量按照声明相反的顺序销毁。工厂最后声明,所以最先销毁,这样其他成员析构时如果持有自己的弱引用,能正确检测到失效: - -```cpp -class MyClass { -private: - // 这些成员先销毁 - std::vector callbacks_; - std::mutex mutex_; - - // 工厂最后销毁 - cf::WeakPtrFactory weak_factory_{this}; -}; -``` - -如果把工厂放在中间,某些成员析构时可能仍然检测到弱引用"有效",然后尝试访问已经被析构的部分对象,后果是未定义行为。 - -## 创建弱引用 - -通过 `GetWeakPtr()` 创建弱引用: - -```cpp -MyClass obj; - -// 创建多个弱引用 -auto weak1 = obj.GetWeakPtr(); -auto weak2 = obj.GetWeakPtr(); -auto weak3 = obj.GetWeakPtr(); - -// 所有弱引用都指向同一个对象 -assert(weak1.Get() == weak2.Get()); -assert(weak2.Get() == weak3.Get()); -``` - -每次调用都创建一个新的 `WeakPtr` 对象,但它们共享同一个内部的"存活标志"。对象销毁或调用 `InvalidateWeakPtrs()` 后,所有弱引用同时失效。 - -## 手动失效 - -有时候你想显式让所有弱引用失效,而不是真的销毁对象: - -```cpp -class ConfigManager { -public: - void Reload() { - // 让所有旧的弱引用失效 - weak_factory_.InvalidateWeakPtrs(); - - // 重新加载配置... - } - - cf::WeakPtr GetWeakPtr() { - return weak_factory_.GetWeakPtr(); - } - -private: - cf::WeakPtrFactory weak_factory_{this}; -}; -``` - -`InvalidateWeakPtrs()` 会把当前的存活标志设为失效,然后分配一个新的。失效前创建的所有弱引用都会变成无效,失效后调用 `GetWeakPtr()` 得到的新弱引用使用新的标志,所以是有效的。 - -这个功能在对象"重置"或"重启"时很有用——你想通知所有观察者旧的状态已经不可用,但对象本身还在。 - -## 检查外部引用 - -`HasWeakPtrs()` 可以检查是否有外部持有的弱引用: - -```cpp -class Service { -public: - void Shutdown() { - if (weak_factory_.HasWeakPtrs()) { - // 通知所有持有弱引用的地方 - NotifyShutdown(); - } - // 实际关闭服务... - } - -private: - cf::WeakPtrFactory weak_factory_{this}; -}; -``` - -这个接口通过内部的 `shared_ptr` 引用计数实现,所以是 O(1) 的。如果 `use_count() > 1`,说明有外部弱引用存在。注意这个检查不是实时的——调用完 `HasWeakPtrs()` 后,其他线程可能立即创建或销毁弱引用。 - -## 不可复制不可移动 - -`WeakPtrFactory` 禁止复制和移动: - -```cpp -class MyClass { -private: - cf::WeakPtrFactory weak_factory_{this}; -}; - -MyClass a; -MyClass b = a; // 编译错误:WeakPtrFactory 不可复制 -MyClass c = std::move(a); // 编译错误:WeakPtrFactory 不可移动 -``` - -这个设计是有意为之的。工厂和对象的生命周期绑定在一起,复制或移动会破坏这个关系。如果你确实需要移动对象,得先清理所有弱引用,但这个场景在我们的使用中极少出现,干脆直接禁了。 - -## 常见使用模式 - -### 回调管理 - -```cpp -class AsyncWorker { -public: - using Callback = std::function; - - void DoWork(Callback callback) { - // 保存弱引用而不是裸指针 - callbacks_.push_back([weak = GetWeakPtr(), callback](int result) { - if (weak) { // 检查对象是否存活 - callback(result); - } - }); - - // 实际的工作... - } - -private: - std::vector callbacks_; - cf::WeakPtrFactory weak_factory_{this}; -}; -``` - -### 观察者模式 - -```cpp -class Subject { -public: - void AddObserver(cf::WeakPtr observer) { - observers_.push_back(observer); - } - - void NotifyEvent() { - // 自动清理已失效的观察者 - auto it = std::remove_if(observers_.begin(), observers_.end(), - [](const auto& weak) { return !weak.IsValid(); }); - observers_.erase(it, observers_.end()); - - // 通知剩余的观察者 - for (auto& weak : observers_) { - if (weak) { - weak->OnEvent(); - } - } - } - -private: - std::vector> observers_; - cf::WeakPtrFactory weak_factory_{this}; -}; -``` - -### 延迟销毁检查 - -```cpp -class ResourceManager { -public: - void UnloadResource(const std::string& id) { - // 先让弱引用失效 - weak_factory_.InvalidateWeakPtrs(); - - // 检查是否有地方还在使用 - if (weak_factory_.HasWeakPtrs()) { - // 有外部引用,延迟销毁 - ScheduleDeferredUnload(id); - } else { - // 立即销毁 - resources_.erase(id); - } - } - -private: - cf::WeakPtrFactory weak_factory_{this}; -}; -``` - -## 注意事项 - -第一,不要在构造函数里就把 `this` 传给其他地方。对象构造完成前,其他成员还没初始化,通过弱引用访问会导致未定义行为。`WeakPtrFactory` 的构造函数里有断言检查 `this` 非空,但不会检查构造是否完成。 - -第二,不要在析构函数里调用 `GetWeakPtr()`。对象已经在销毁过程中,创建新的弱引用没有意义。如果你确实需要,重新考虑设计——为什么在析构函数里还要传递自己的引用? - -第三,`InvalidateWeakPtrs()` 不是原子的。多线程环境下如果一边失效一边创建新的弱引用,可能会出现新创建的弱引用意外失效。如果有这种需求,得自己加锁保护。 - -## 与标准库的对比 - -`WeakPtrFactory` 没有标准库的直接对应物。`std::enable_shared_from_this` 提供了类似的功能,但它是配合 `shared_ptr` 使用的,而我们假设对象有明确的唯一拥有者。 - -如果你需要在已有代码中引入 `WeakPtrFactory`,最简单的迁移方式是把原来持有裸指针或引用的地方改成持有 `WeakPtr`。这通常不需要大幅改动调用代码,只需要在使用前检查有效性。 - -## 相关文档 - -- [weak_ptr - 弱引用指针](./weak_ptr.md) -- [ScopeGuard - 作用域守卫](./scope_guard.md) -- [基础工具类概述](./overview.md) +--- +title: "weakptrfactory - 弱引用工厂" +description: 是弱引用机制的核心工厂类,负责创建和管理某个对象的弱引用。每个需要对外提供弱引用的对象都应该持有一个 +--- + +# weak_ptr_factory - 弱引用工厂 + +`WeakPtrFactory` 是弱引用机制的核心工厂类,负责创建和管理某个对象的弱引用。每个需要对外提供弱引用的对象都应该持有一个 `WeakPtrFactory` 成员,把它作为接口的守门员。 + +## 基本用法 + +在类中添加弱引用支持很简单,声明一个 `WeakPtrFactory` 成员即可: + +```cpp +#include "base/weak_ptr/weak_ptr_factory.h" + +class NetworkManager { +public: + void SendRequest(const std::string& url) { + // 发送请求... + } + + // 对外提供弱引用获取接口 + cf::WeakPtr GetWeakPtr() { + return weak_factory_.GetWeakPtr(); + } + +private: + // 必须是最后一个成员变量 + cf::WeakPtrFactory weak_factory_{this}; +}; +```text + +构造时传入 `this` 指针,工厂会记住对象的位置。每次调用 `GetWeakPtr()` 就会创建一个新的弱引用,指向同一个对象。 + +## 成员变量顺序 + +⚠️ 这是使用 `WeakPtrFactory` 最重要的规则:**必须声明为类的最后一个成员**。 + +原因在于 C++ 的析构顺序——成员变量按照声明相反的顺序销毁。工厂最后声明,所以最先销毁,这样其他成员析构时如果持有自己的弱引用,能正确检测到失效: + +```cpp +class MyClass { +private: + // 这些成员先销毁 + std::vector callbacks_; + std::mutex mutex_; + + // 工厂最后销毁 + cf::WeakPtrFactory weak_factory_{this}; +}; +```text + +如果把工厂放在中间,某些成员析构时可能仍然检测到弱引用"有效",然后尝试访问已经被析构的部分对象,后果是未定义行为。 + +## 创建弱引用 + +通过 `GetWeakPtr()` 创建弱引用: + +```cpp +MyClass obj; + +// 创建多个弱引用 +auto weak1 = obj.GetWeakPtr(); +auto weak2 = obj.GetWeakPtr(); +auto weak3 = obj.GetWeakPtr(); + +// 所有弱引用都指向同一个对象 +assert(weak1.Get() == weak2.Get()); +assert(weak2.Get() == weak3.Get()); +```text + +每次调用都创建一个新的 `WeakPtr` 对象,但它们共享同一个内部的"存活标志"。对象销毁或调用 `InvalidateWeakPtrs()` 后,所有弱引用同时失效。 + +## 手动失效 + +有时候你想显式让所有弱引用失效,而不是真的销毁对象: + +```cpp +class ConfigManager { +public: + void Reload() { + // 让所有旧的弱引用失效 + weak_factory_.InvalidateWeakPtrs(); + + // 重新加载配置... + } + + cf::WeakPtr GetWeakPtr() { + return weak_factory_.GetWeakPtr(); + } + +private: + cf::WeakPtrFactory weak_factory_{this}; +}; +```text + +`InvalidateWeakPtrs()` 会把当前的存活标志设为失效,然后分配一个新的。失效前创建的所有弱引用都会变成无效,失效后调用 `GetWeakPtr()` 得到的新弱引用使用新的标志,所以是有效的。 + +这个功能在对象"重置"或"重启"时很有用——你想通知所有观察者旧的状态已经不可用,但对象本身还在。 + +## 检查外部引用 + +`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()` 后,其他线程可能立即创建或销毁弱引用。 + +## 不可复制不可移动 + +`WeakPtrFactory` 禁止复制和移动: + +```cpp +class MyClass { +private: + cf::WeakPtrFactory weak_factory_{this}; +}; + +MyClass a; +MyClass b = a; // 编译错误:WeakPtrFactory 不可复制 +MyClass c = std::move(a); // 编译错误:WeakPtrFactory 不可移动 +```text + +这个设计是有意为之的。工厂和对象的生命周期绑定在一起,复制或移动会破坏这个关系。如果你确实需要移动对象,得先清理所有弱引用,但这个场景在我们的使用中极少出现,干脆直接禁了。 + +## 常见使用模式 + +### 回调管理 + +```cpp +class AsyncWorker { +public: + using Callback = std::function; + + void DoWork(Callback callback) { + // 保存弱引用而不是裸指针 + callbacks_.push_back([weak = GetWeakPtr(), callback](int result) { + if (weak) { // 检查对象是否存活 + callback(result); + } + }); + + // 实际的工作... + } + +private: + std::vector callbacks_; + cf::WeakPtrFactory weak_factory_{this}; +}; +```text + +### 观察者模式 + +```cpp +class Subject { +public: + void AddObserver(cf::WeakPtr observer) { + observers_.push_back(observer); + } + + void NotifyEvent() { + // 自动清理已失效的观察者 + auto it = std::remove_if(observers_.begin(), observers_.end(), + [](const auto& weak) { return !weak.IsValid(); }); + observers_.erase(it, observers_.end()); + + // 通知剩余的观察者 + for (auto& weak : observers_) { + if (weak) { + weak->OnEvent(); + } + } + } + +private: + std::vector> observers_; + cf::WeakPtrFactory weak_factory_{this}; +}; +```text + +### 延迟销毁检查 + +```cpp +class ResourceManager { +public: + void UnloadResource(const std::string& id) { + // 先让弱引用失效 + weak_factory_.InvalidateWeakPtrs(); + + // 检查是否有地方还在使用 + if (weak_factory_.HasWeakPtrs()) { + // 有外部引用,延迟销毁 + ScheduleDeferredUnload(id); + } else { + // 立即销毁 + resources_.erase(id); + } + } + +private: + cf::WeakPtrFactory weak_factory_{this}; +}; +```text + +## 注意事项 + +第一,不要在构造函数里就把 `this` 传给其他地方。对象构造完成前,其他成员还没初始化,通过弱引用访问会导致未定义行为。`WeakPtrFactory` 的构造函数里有断言检查 `this` 非空,但不会检查构造是否完成。 + +第二,不要在析构函数里调用 `GetWeakPtr()`。对象已经在销毁过程中,创建新的弱引用没有意义。如果你确实需要,重新考虑设计——为什么在析构函数里还要传递自己的引用? + +第三,`InvalidateWeakPtrs()` 不是原子的。多线程环境下如果一边失效一边创建新的弱引用,可能会出现新创建的弱引用意外失效。如果有这种需求,得自己加锁保护。 + +## 与标准库的对比 + +`WeakPtrFactory` 没有标准库的直接对应物。`std::enable_shared_from_this` 提供了类似的功能,但它是配合 `shared_ptr` 使用的,而我们假设对象有明确的唯一拥有者。 + +如果你需要在已有代码中引入 `WeakPtrFactory`,最简单的迁移方式是把原来持有裸指针或引用的地方改成持有 `WeakPtr`。这通常不需要大幅改动调用代码,只需要在使用前检查有效性。 + +## 相关文档 + +- [weak_ptr - 弱引用指针](./weak_ptr.md) +- [ScopeGuard - 作用域守卫](./scope_guard.md) +- [基础工具类概述](./overview.md) diff --git a/document/HandBook/desktop/.pages b/document/HandBook/desktop/.pages deleted file mode 100644 index 4041a4d88..000000000 --- a/document/HandBook/desktop/.pages +++ /dev/null @@ -1,3 +0,0 @@ -title: 桌面模块 -nav: - - 基础组件: base diff --git a/document/HandBook/desktop/base/.pages b/document/HandBook/desktop/base/.pages deleted file mode 100644 index 1e3e430e8..000000000 --- a/document/HandBook/desktop/base/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: 基础组件 -nav: - - 配置管理器: config_manager - - 日志系统: logger 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 d544fb143..01ce124c5 100644 --- a/document/HandBook/desktop/base/config_manager/01-quick-start.md +++ b/document/HandBook/desktop/base/config_manager/01-quick-start.md @@ -1,3 +1,8 @@ +--- +title: 快速入门 +description: 本文档将引导您从零开始使用 ConfigStore 配置管理中心。 +--- + # 快速入门 本文档将引导您从零开始使用 ConfigStore 配置管理中心。 @@ -12,7 +17,7 @@ #include // Qt 字符串类型 using namespace cf::config; -``` +```text ### 获取单例实例 @@ -21,7 +26,7 @@ ConfigStore 使用单例模式,首次访问时自动初始化: ```cpp // 获取单例实例 auto& config = ConfigStore::instance(); -``` +```text ### 自定义路径配置 @@ -59,7 +64,7 @@ public: // 在首次使用前初始化 auto provider = std::make_shared(); ConfigStore::instance().initialize(provider); -``` +```text ## 基础操作 @@ -93,7 +98,7 @@ bool auto_save = ConfigStore::instance().query( KeyView{.group = "app", .key = "auto_save"}, true ); -``` +```text #### Optional 查询 @@ -116,7 +121,7 @@ if (auto value = ConfigStore::instance().query( KeyView{.group = "network", .key = "port"})) { std::cout << "端口: " << *value << std::endl; } -``` +```text #### 指定层查询 @@ -132,7 +137,7 @@ auto user_theme = ConfigStore::instance().query( if (user_theme.has_value()) { std::cout << "用户设置的主题: " << user_theme.value() << std::endl; } -``` +```text ### 写入配置 @@ -164,7 +169,7 @@ ConfigStore::instance().set( KeyView{.group = "app", .key = "auto_save"}, false ); -``` +```text #### 选择目标层 @@ -184,7 +189,7 @@ ConfigStore::instance().set( std::string("abc123"), Layer::Temp // 写入 Temp 层,重启后丢失 ); -``` +```text #### 通知策略 @@ -214,7 +219,7 @@ ConfigStore::instance().set( ); // 批量操作完成后,一次性触发所有 Watcher ConfigStore::instance().notify(); -``` +```text ### 键管理 @@ -242,7 +247,7 @@ if (result == RegisterResult::KeyRegisteredSuccess) { } else { std::cout << "键已存在" << std::endl; } -``` +```text #### 注销键 @@ -263,7 +268,7 @@ auto result = ConfigStore::instance().unregister_key( if (result == UnRegisterResult::KeyUnRegisteredSuccess) { std::cout << "键注销成功" << std::endl; } -``` +```text #### 检查键存在 @@ -278,7 +283,7 @@ bool in_user = ConfigStore::instance().has_key( KeyView{.group = "app", .key = "theme"}, Layer::User ); -``` +```text ## 监听配置变更 @@ -298,7 +303,7 @@ auto handle = ConfigStore::instance().watch( } } ); -``` +```text ### 通配符监听 @@ -320,7 +325,7 @@ auto theme_watcher = ConfigStore::instance().watch( std::cout << "主题配置变更: " << k.full_key << std::endl; } ); -``` +```text ### 取消监听 @@ -330,7 +335,7 @@ WatcherHandle handle = ConfigStore::instance().watch("app.*", callback); // 取消监听 ConfigStore::instance().unwatch(handle); -``` +```text ### 手动通知模式 @@ -350,7 +355,7 @@ ConfigStore::instance().set(KeyView{.group = "batch", .key = "b"}, 2, // 手动触发通知 ConfigStore::instance().notify(); -``` +```text ## 持久化操作 @@ -362,7 +367,7 @@ ConfigStore::instance().sync(SyncMethod::Async); // 同步同步(阻塞直到写入完成) ConfigStore::instance().sync(SyncMethod::Sync); -``` +```text ### 重新加载配置 @@ -370,7 +375,7 @@ ConfigStore::instance().sync(SyncMethod::Sync); // 从磁盘重新加载所有配置 // 注意:这会清空 Temp 层的所有临时配置 ConfigStore::instance().reload(); -``` +```text ### 查看待写入变更 @@ -378,7 +383,7 @@ ConfigStore::instance().reload(); // 获取待同步的变更数量 size_t pending = ConfigStore::instance().pending_changes(); std::cout << "待同步变更数: " << pending << std::endl; -``` +```text ## 完整示例:应用程序配置管理 @@ -512,7 +517,7 @@ int main() { return 0; } -``` +```text ## 下一步 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 051834a24..71ac45326 100644 --- a/document/HandBook/desktop/base/config_manager/02-best-practices.md +++ b/document/HandBook/desktop/base/config_manager/02-best-practices.md @@ -1,3 +1,8 @@ +--- +title: ConfigStore 最佳实践 +description: ConfigStore 提供四层存储架构,每一层都有其特定的使用场景: +--- + # ConfigStore 最佳实践 ## 文档信息 @@ -17,18 +22,18 @@ ConfigStore 使用点分隔的分层键名结构,建议采用 2-4 层的命名深度: -``` +```bash [level1].[level2].[level3].[level4] | | | | | | | +-- 具体属性名 (必需) | | +----------- 子模块 (可选) | +-------------------- 功能模块 (必需) +----------------------------- 命名空间/域 (必需) -``` +```text **推荐示例:** -``` +```text app.theme.name # 应用主题名称 app.theme.dark_mode # 主题深色模式 ui.window.width # 窗口宽度 @@ -38,7 +43,7 @@ network.proxy.host # 代理主机地址 network.proxy.port # 代理端口 editor.font.size # 编辑器字体大小 editor.font.family # 编辑器字体族 -``` +```bash ### 1.2 命名约定 @@ -57,7 +62,7 @@ editor.font.family # 编辑器字体族 为避免键名冲突,推荐为不同模块或组件使用命名空间前缀: -``` +```text # 为每个子系统预留独立的命名空间 app.* # 应用程序级配置 ui.* # 用户界面配置 @@ -65,7 +70,7 @@ editor.* # 编辑器配置 network.* # 网络配置 database.* # 数据库配置 logger.* # 日志配置 -``` +```text **常见命名空间:** @@ -81,7 +86,7 @@ namespace ConfigKeys { inline constexpr KeyView UI_WINDOW_WIDTH{.group = "ui.window", .key = "width"}; inline constexpr KeyView EDITOR_FONT_SIZE{.group = "editor.font", .key = "size"}; } -``` +```yaml --- @@ -91,7 +96,7 @@ ConfigStore 提供四层存储架构,每一层都有其特定的使用场景 ### 2.1 层级优先级图 -``` +```bash +-----------------------------+ | Temp 层 (优先级 3) | 内存临时数据,进程重启后丢失 +-----------------------------+ @@ -110,7 +115,7 @@ ConfigStore 提供四层存储架构,每一层都有其特定的使用场景 +-----------------------------+ | System 层 (优先级 0) | 系统默认配置,管理员维护 +-----------------------------+ -``` +```text ### 2.2 System 层:系统默认 @@ -151,7 +156,7 @@ void init_system_defaults() { store.sync(SyncMethod::Sync); // 同步写入 } -``` +```text ### 2.3 User 层:用户偏好 @@ -201,7 +206,7 @@ UserPreferences load_user_preferences() { return prefs; } -``` +```text ### 2.4 App 层:运行时状态 @@ -253,7 +258,7 @@ void restore_window_state(QWidget* window) { window->showMaximized(); } } -``` +```text ### 2.5 Temp 层:临时数据 @@ -305,7 +310,7 @@ void enable_debug_mode_temporarily() { // 不会持久化到磁盘 } -``` +```bash ### 2.6 层级选择决策表 @@ -340,7 +345,7 @@ if (auto v = store.query(KeyView{.group = "app", .key = "timeout"}, Layer:: } else if (auto v = store.query(KeyView{.group = "app", .key = "timeout"}, Layer::App)) { value = *v; } // ... -``` +```text **2. 批量读取配置** @@ -371,7 +376,7 @@ struct AppSettings { // 使用缓存的配置 auto settings = AppSettings::load(); apply_settings(settings); -``` +```text **3. 避免频繁查询同一配置** @@ -421,7 +426,7 @@ private: // 处理缩放变化 } }; -``` +```text ### 3.2 批量操作建议 @@ -446,7 +451,7 @@ void update_multiple_settings() { // 一次性同步到磁盘 store.sync(SyncMethod::Async); } -``` +```text ### 3.3 异步持久化的使用 @@ -493,7 +498,7 @@ void on_config_changed() { void on_application_exit() { ConfigSaver::instance().save_immediately(); // 退出时立即保存 } -``` +```yaml --- @@ -528,7 +533,7 @@ void worker_thread(int id) { // 使用配置... } } -``` +```text ### 4.2 避免死锁的注意事项 @@ -561,7 +566,7 @@ void main_loop() { ConfigStore::instance().set(KeyView{.group = "app", .key = "changed"}, true); } } -``` +```text **2. 持有其他锁时避免调用 ConfigStore** @@ -588,7 +593,7 @@ class MyClass { ConfigStore::instance().set(...); } }; -``` +```text ### 4.3 Watcher 回调中的注意事项 @@ -673,7 +678,7 @@ private: std::queue event_queue_; bool stop_ = false; }; -``` +```yaml --- @@ -724,7 +729,7 @@ public: return value; } }; -``` +```text ### 5.2 键验证的处理 @@ -764,7 +769,7 @@ public: return ConfigStore::instance().set(kv, value); } }; -``` +```text ### 5.3 优雅降级策略 @@ -841,7 +846,7 @@ public: } }; }; -``` +```yaml --- @@ -1070,7 +1075,7 @@ void use_app_config() { new_theme.dark_mode = true; config.set_theme(new_theme); } -``` +```text ### 6.2 模式二:热重载支持 @@ -1212,7 +1217,7 @@ void initialize_application() { ConfigHotReload::instance().start_watching(); #endif } -``` +```text ### 6.3 模式三:配置迁移 @@ -1390,7 +1395,7 @@ void check_and_migrate_config() { } } } -``` +```text ### 6.4 模式四:多实例隔离(使用自定义路径提供者) @@ -1570,7 +1575,7 @@ private: std::unordered_map> user_stores_; }; -``` +```bash --- @@ -1607,7 +1612,7 @@ auto handle = store.watch("user.theme.*", [](auto... args) { // 处理变化 }); store.unwatch(handle); -``` +```yaml ### A.3 错误处理检查清单 diff --git a/document/HandBook/desktop/base/config_manager/03-faq.md b/document/HandBook/desktop/base/config_manager/03-faq.md index aae12815e..523043189 100644 --- a/document/HandBook/desktop/base/config_manager/03-faq.md +++ b/document/HandBook/desktop/base/config_manager/03-faq.md @@ -1,3 +1,8 @@ +--- +title: ConfigStore 常见问题与故障排查 +description: 本文档列出了 ConfigStore 使用中的常见问题、故障排查方法和调试技巧。 +--- + # ConfigStore 常见问题与故障排查 本文档列出了 ConfigStore 使用中的常见问题、故障排查方法和调试技巧。 @@ -30,7 +35,7 @@ ConfigStore::instance().sync(SyncMethod::Sync); // 同步写入 // 或使用异步同步(推荐) ConfigStore::instance().sync(SyncMethod::Async); // 不阻塞调用方 -``` +```text #### 可能原因 2:层级优先级问题 @@ -55,7 +60,7 @@ auto system_theme = ConfigStore::instance().query( Layer::System, "" ); -``` +```text #### 可能原因 3:KeyView 转换失败 @@ -69,7 +74,7 @@ ConfigStore::instance().set(invalid_kv, "value"); // 返回 false // 解决方案:使用合法字符 KeyView valid_kv{.group = "app_theme", .key = "name"}; // 仅字母、数字、下划线 ConfigStore::instance().set(valid_kv, "value"); // 成功 -``` +```yaml --- @@ -99,7 +104,7 @@ ConfigStore::instance().set( "https://custom.api.com", Layer::User ); -``` +```text #### 方法 2:自定义路径提供者 @@ -140,7 +145,7 @@ public: // 初始化时使用 ConfigStore::instance().initialize(std::make_shared()); -``` +```text #### 方法 3:使用环境变量 @@ -186,7 +191,7 @@ public: ConfigStore::instance().initialize( std::make_shared(QString::fromStdString(config_dir)) ); -``` +```yaml --- @@ -210,7 +215,7 @@ ConfigStore::instance().set(KeyView{.group = "app", .key = "theme"}, "dark"); // 解决方案:手动触发通知 ConfigStore::instance().set(KeyView{.group = "app", .key = "theme"}, "dark", Layer::App, NotifyPolicy::Manual); ConfigStore::instance().notify(); // 触发所有 Manual Watcher -``` +```text #### 原因 2:键模式不匹配 @@ -234,7 +239,7 @@ ConfigStore::instance().watch( "app.*", // 匹配 app 下的所有键 callback ); -``` +```text #### 原因 3:Watcher 被提前取消 @@ -263,7 +268,7 @@ public: ConfigStore::instance().unwatch(theme_watcher_); } }; -``` +```yaml --- @@ -278,7 +283,7 @@ ConfigStore::instance().set(KeyView{.group = "test", .key = "value"}, std::strin // 尝试读取为 int,但字符串无法直接转换 auto result = ConfigStore::instance().query(KeyView{.group = "test", .key = "value"}, 0); // 可能返回默认值 0 -``` +```text #### 解决方案 1:先读取字符串再转换 @@ -295,7 +300,7 @@ if (!str_value.empty()) { // 处理转换错误 } } -``` +```text #### 解决方案 2:使用 QVariant 兼容的类型 @@ -307,7 +312,7 @@ ConfigStore::instance().set(KeyView{.group = "test", .key = "value"}, 123); // int value = ConfigStore::instance().query( KeyView{.group = "test", .key = "value"}, 0 ); // 正确返回 123 -``` +```text #### 解决方案 3:使用 std::any 处理多种类型 @@ -320,7 +325,7 @@ if (value.type() == typeid(int)) { std::string str_value = std::any_cast(value); // 手动转换... } -``` +```yaml --- @@ -337,7 +342,7 @@ copy C:\OldPath\config.ini %APPDATA%\MyApp\user.ini # macOS cp /old/path/config.plist ~/Library/Preferences/com.myapp.plist -``` +```text #### 方法 2:使用 ConfigStore API 迁移 @@ -378,7 +383,7 @@ void migrate_old_config(const std::string& old_file_path) { // 同步到磁盘 ConfigStore::instance().sync(SyncMethod::Sync); } -``` +```text #### 方法 3:映射旧键名到新键名 @@ -424,7 +429,7 @@ void migrate_with_mapping(const std::string& old_file) { ConfigStore::instance().sync(SyncMethod::Sync); } -``` +```yaml --- @@ -445,7 +450,7 @@ ConfigStore::instance().reload(); // Temp 层被清空 // 3. 调用 clear_layer(Layer::Temp) ConfigStore::instance().clear_layer(Layer::Temp); // Temp 层数据被清空 -``` +```text #### 保留场景 @@ -461,7 +466,7 @@ auto value = ConfigStore::instance().query( // sync() 不会清空 Temp 层 ConfigStore::instance().sync(SyncMethod::Sync); // Temp 层数据仍然存在 -``` +```text #### 最佳实践 @@ -496,7 +501,7 @@ ConfigStore::instance().set( "dark", Layer::User // 使用 User 或 App 层 ); -``` +```yaml --- @@ -532,7 +537,7 @@ ConfigStore::instance().set( // 批量修改完成后,一次性触发 ConfigStore::instance().notify(); // 所有 Watcher 被触发一次 -``` +```text #### 方法 2:使用事务模式 @@ -565,7 +570,7 @@ public: // 事务结束,自动触发通知 } -``` +```text #### 方法 3:先取消 Watcher,批量修改后再添加 @@ -585,7 +590,7 @@ ConfigStore::instance().set(KeyView{.group = "batch", .key = "c"}, 3); // 重新添加 Watcher watcher_handle = ConfigStore::instance().watch("batch.*", callback); -``` +```bash --- @@ -668,7 +673,7 @@ void check_config_files() { } } } -``` +```yaml --- @@ -676,7 +681,7 @@ void check_config_files() { ### 问题诊断流程 -``` +```bash 配置读取异常 | +---------------+---------------+ @@ -705,7 +710,7 @@ void check_config_files() { +-----------+-------+ | 解决问题 -``` +```text ### 详细诊断步骤 @@ -749,7 +754,7 @@ void diagnose_key(const KeyView& kv) { // 使用 diagnose_key(KeyView{.group = "app.theme", .key = "name"}); -``` +```text #### 步骤 2:检查配置文件 @@ -804,7 +809,7 @@ void diagnose_config_files() { std::cout << " 修改时间: " << file.lastModified().toString().toStdString() << std::endl; } } -``` +```text #### 步骤 3:检查 Watcher 状态 @@ -847,7 +852,7 @@ void diagnose_watchers() { ConfigStore::instance().unwatch(handle); ConfigStore::instance().clear_layer(Layer::Temp); } -``` +```text ### 日志输出分析方法 @@ -887,7 +892,7 @@ auto theme = LoggingConfigStore::query( "default", Layer::User ); -``` +```text #### 检查 pending_changes @@ -908,7 +913,7 @@ void monitor_pending_changes() { size_t after_notify = ConfigStore::instance().pending_changes(); std::cout << "notify 后待写入变更数: " << after_notify << std::endl; } -``` +```text ### 调试技巧 @@ -973,7 +978,7 @@ void dump_group(QSettings& settings, const QString& group, int indent) { settings.endGroup(); } } -``` +```text #### 技巧 2:验证类型转换 @@ -992,7 +997,7 @@ void test_type_conversion(const KeyView& kv) { std::cout << " as double: " << as_double << std::endl; std::cout << " as bool: " << (as_bool ? "true" : "false") << std::endl; } -``` +```text #### 技巧 3:Watcher 性能分析 @@ -1054,7 +1059,7 @@ auto handle = watcher->install(); // ... 运行一段时间 ... watcher->print_stats(); -``` +```yaml --- @@ -1078,7 +1083,7 @@ ConfigStore::instance().set( KeyView{.group = "app_short", .key = "long_group_key"}, "value" ); -``` +```text #### 问题 2:注册表权限 @@ -1114,7 +1119,7 @@ bool check_registry_access() { return false; } -``` +```text #### 问题 3:INI 格式与注册表格式差异 @@ -1159,7 +1164,7 @@ public: // QSettings 会根据路径扩展名自动选择格式 // .ini -> INI 格式 // 无扩展名 -> 注册表(Windows) -``` +```text ### Linux @@ -1189,7 +1194,7 @@ void ensure_config_directory(const std::string& path) { // 使用 ensure_config_directory("~/.config/cfdesktop/user.ini"); -``` +```text #### 问题 2:System 层路径需要 root 权限 @@ -1198,7 +1203,7 @@ ensure_config_directory("~/.config/cfdesktop/user.ini"); sudo touch /etc/cfdesktop/system.ini sudo chown $USER:$USER /etc/cfdesktop/system.ini # 或者将应用配置放在用户目录 -``` +```text #### 问题 3:INI 文件编码 @@ -1213,7 +1218,7 @@ ConfigStore::instance().set( u8"应用程序标题", // UTF-8 字符串字面量 Layer::User ); -``` +```text ### macOS @@ -1244,7 +1249,7 @@ void import_from_plist(const std::string& plist_path) { ConfigStore::instance().set(kv, value.toString().toStdString(), Layer::User); } } -``` +```text #### 问题 2:macOS 特殊目录 @@ -1282,7 +1287,7 @@ public: return true; } }; -``` +```yaml --- @@ -1324,7 +1329,7 @@ void monitor_memory_usage() { // 方案 3:限制缓存大小(需要修改 ConfigStore 实现) // 在 ConfigStoreImpl 中添加 LRU 缓存 -``` +```text ### 读写缓慢 @@ -1372,7 +1377,7 @@ void benchmark_config_operations() { // 清理 ConfigStore::instance().clear_layer(Layer::Temp); } -``` +```text #### 优化建议 @@ -1390,7 +1395,7 @@ for (int i = 0; i < 1000; ++i) { } ConfigStore::instance().notify(); ConfigStore::instance().sync(SyncMethod::Async); // 异步同步 -``` +```text 2. **使用 Temp 层存储频繁变化的配置** ```cpp @@ -1410,7 +1415,7 @@ ConfigStore::instance().set( Layer::App ); ConfigStore::instance().sync(SyncMethod::Sync); -``` +```text ### Watcher 性能问题 @@ -1426,7 +1431,7 @@ ConfigStore::instance().watch( // 这会阻塞所有后续的配置操作 } ); -``` +```text #### 解决方案 @@ -1512,7 +1517,7 @@ private: // 使用 auto debounced_watcher = std::make_unique("app.*", std::chrono::milliseconds(100)); debounced_watcher->install(); -``` +```yaml --- diff --git a/document/HandBook/desktop/base/config_manager/04-architecture.md b/document/HandBook/desktop/base/config_manager/04-architecture.md index 466097288..96b983c60 100644 --- a/document/HandBook/desktop/base/config_manager/04-architecture.md +++ b/document/HandBook/desktop/base/config_manager/04-architecture.md @@ -1,3 +1,8 @@ +--- +title: ConfigStore 架构详解 +description: 本文档面向库开发者和维护者,深入解析 ConfigStore 的实现架构、设计理念和扩展机制。 +--- + # ConfigStore 架构详解 本文档面向库开发者和维护者,深入解析 ConfigStore 的实现架构、设计理念和扩展机制。 @@ -8,7 +13,7 @@ ConfigStore 采用四层优先级架构,实现了配置的层次化管理和灵活覆盖: -``` +```bash +-----------------------------------------------+ | Temp Layer (最高优先级) | | - 仅内存存储,进程退出后丢失 | @@ -26,7 +31,7 @@ ConfigStore 采用四层优先级架构,实现了配置的层次化管理和 | - 系统级配置,/etc/cfdesktop/system.ini | | - 全局默认配置,只读或需要特权写入 | +-----------------------------------------------+ -``` +```cpp **查询顺序(优先级从高到低)**:Temp -> App -> User -> System @@ -40,7 +45,7 @@ ConfigStore 采用四层优先级架构,实现了配置的层次化管理和 ConfigStore 使用 Pimpl(Pointer to Implementation)模式实现接口与实现分离: -``` +```cpp +------------------+ +------------------------+ | ConfigStore | | ConfigStoreImpl | | (公共接口层) | --------> | (实现层) | @@ -50,7 +55,7 @@ ConfigStore 使用 Pimpl(Pointer to Implementation)模式实现接口与实 | - 单例继承 | | - Watcher 机制 | | | | - 线程同步 | +------------------+ +------------------------+ -``` +```text **优势**: 1. **ABI 稳定性**:实现变更不影响公共头文件,无需重新编译依赖代码 @@ -71,7 +76,7 @@ class SimpleSingleton { return target; } }; -``` +```text **特点**: - **线程安全**:C++11 标准保证静态局部变量初始化的线程安全性 @@ -108,7 +113,7 @@ template [[nodiscard]] RegisterResult register_key(const Key& key, const Value& init_value, Layer layer = Layer::App, NotifyPolicy notify_policy = NotifyPolicy::Immediate); -``` +```text **类型转换机制**(`detail::any_cast`): - 直接类型匹配:`std::any` 直接包含目标类型 @@ -150,13 +155,13 @@ private: std::vector pending_changes_; std::vector deferred_events_; }; -``` +```text **内部方法架构**: 每个公共方法都有对应的 `_impl` 版本,用于已持锁场景: -``` +```cpp 公共方法 (获取锁) 内部方法 (无锁,调用者必须持锁) +------------------+ +------------------------+ | set() | | set_impl() | @@ -165,7 +170,7 @@ private: | clear() | | clear_impl() | | clear_layer() | | clear_layer_impl() | +------------------+ +------------------------+ -``` +```text 这种设计避免了在已持锁场景下的重复加锁,提高了效率。 @@ -185,7 +190,7 @@ public: virtual QString app_filename() const = 0; virtual bool is_layer_enabled(int layer_index) const = 0; }; -``` +```bash **默认实现**:`DesktopConfigStorePathProvider` @@ -212,7 +217,7 @@ struct Key { std::string full_key; // 完整键,如 "app.theme.name" std::string full_description; // 完整描述 }; -``` +```text **转换逻辑**: @@ -222,7 +227,7 @@ struct Key { // Key -> KeyView "app.theme.name" => group="app.theme", key="name" -``` +```text **验证规则**(`default_policy`): - 只允许字母、数字、下划线和点号 @@ -232,7 +237,7 @@ struct Key { ### 3.1 读取流程 -``` +```cpp 用户调用 ConfigStore::query() | v @@ -270,11 +275,11 @@ struct Key { | v 返回给用户 -``` +```text ### 3.2 写入流程 -``` +```cpp 用户调用 ConfigStore::set() | v @@ -316,11 +321,11 @@ struct Key { | v 返回结果 -``` +```text ### 3.3 Watcher 触发机制 -``` +```cpp 配置变更发生 | v @@ -348,7 +353,7 @@ struct Key { | v 完成 -``` +```text **延迟回调机制的关键**: 1. 在主锁内收集事件(避免回调中死锁) @@ -357,7 +362,7 @@ struct Key { ### 3.4 四层优先级查询图 -``` +```cpp 查询 "app.theme.name" | +----------------+----------------+ @@ -376,7 +381,7 @@ struct Key { | | | | v v v v 返回值 -------> 下层 ---------> 下层 --------> 默认值 -``` +```text ## 4. 线程安全 @@ -392,7 +397,7 @@ std::shared_lock lock(mutex_); // query(), has_key() // 写操作:独占锁,独占访问 std::unique_lock lock(mutex_); // set(), register_key(), etc. -``` +```bash **并发场景分析**: @@ -439,7 +444,7 @@ void execute_deferred_watchers() { event.callback(...); // 安全执行,无主锁 } } -``` +```text **锁分离设计**: - `mutex_`:保护配置数据和 watcher 列表 @@ -451,7 +456,7 @@ void execute_deferred_watchers() { ```cpp std::atomic next_handle_{1}; // WatcherHandle 分配无需加锁 -``` +```text **内存序保证**: - 默认使用 `memory_order_seq_cst` @@ -492,7 +497,7 @@ class ConfigStore { public: void set_key_helper(std::unique_ptr helper); }; -``` +```text ### 5.2 自定义路径提供者 @@ -538,13 +543,13 @@ public: private: QString base_dir_; }; -``` +```text **使用方式**: ```cpp auto test_provider = std::make_shared("/tmp/test_config"); cf::config::ConfigStore::instance().initialize(test_provider); -``` +```text ### 5.3 扩展存储后端 @@ -583,7 +588,7 @@ class ConfigStoreImpl { void unwatch(WatcherHandle handle); NotifyResult notify(); }; -``` +```text **注意事项**: 1. 保持与现有 ConfigStoreImpl 相同的接口签名 @@ -688,7 +693,7 @@ bool exists = ConfigStore::instance().has_key(key_view); // 检查特定层 bool exists_in_app = ConfigStore::instance().has_key(key_view, Layer::App); -``` +```text ### 8.2 日志建议 @@ -742,7 +747,7 @@ private: // 使用方式 auto mock_provider = std::make_shared("/tmp/test_config"); cf::config::ConfigStore::instance().initialize(mock_provider); -``` +```yaml **单元测试**:参考 `test/config_manager/config_store_test.cpp` diff --git a/document/HandBook/desktop/base/config_manager/README.md b/document/HandBook/desktop/base/config_manager/README.md index 538ffc8bb..88cfefbe0 100644 --- a/document/HandBook/desktop/base/config_manager/README.md +++ b/document/HandBook/desktop/base/config_manager/README.md @@ -1,3 +1,8 @@ +--- +title: "ConfigStore - 配置管理中心" +description: ConfigStore 是 CFDesktop 桌面框架的配置管理中心,提供分层存储、结构化键名管理 +--- + # ConfigStore - 配置管理中心 ## 简介 @@ -31,7 +36,7 @@ int width = ConfigStore::instance().query( // 设置配置 ConfigStore::instance().set( KeyView{.group = "ui", .key = "width"}, 1024); -``` +```text 支持的基础类型: - `int` / `unsigned` / `long` 等整数类型 @@ -51,7 +56,7 @@ auto handle = ConfigStore::instance().watch( std::cout << "UI 配置变更: " << k.full_key << std::endl; } ); -``` +```text 支持两种通知策略: - **Immediate**: 每次变更立即触发 Watcher @@ -99,7 +104,7 @@ int main() { return 0; } -``` +```bash ## 文档导航 @@ -117,7 +122,7 @@ int main() { ```bash cd build/example/desktop/base/config_manager ./example_usage -``` +```text ## 依赖模块 diff --git a/document/HandBook/desktop/base/config_manager/index.md b/document/HandBook/desktop/base/config_manager/index.md index d6b480baf..e4bcf7851 100644 --- a/document/HandBook/desktop/base/config_manager/index.md +++ b/document/HandBook/desktop/base/config_manager/index.md @@ -1,10 +1,11 @@ -# config_manager - -> Welcome to the config_manager section. +--- +title: ConfigStore 手册 +description: ConfigStore 是 CFDesktop 的四层优先级配置管理系统,本手册详细介绍其存储层级( +--- -## Overview +# ConfigStore 手册 -Documentation and resources for config_manager. +ConfigStore 是 CFDesktop 的四层优先级配置管理系统,本手册详细介绍其存储层级(Temp / App / User / System)的使用方式、JSON 配置文件格式、热加载机制以及自定义配置项的扩展方法。 --- diff --git a/document/HandBook/desktop/base/index.md b/document/HandBook/desktop/base/index.md index 166eabd94..4a6e9d4da 100644 --- a/document/HandBook/desktop/base/index.md +++ b/document/HandBook/desktop/base/index.md @@ -1,10 +1,11 @@ -# base - -> Welcome to the base section. +--- +title: 基础组件 +description: 本目录包含桌面环境层基础组件的使用手册,包括 CFLogger 异步日志系统和 ConfigStor +--- -## Overview +# 基础组件 -Documentation and resources for base. +本目录包含桌面环境层基础组件的使用手册,包括 CFLogger 异步日志系统和 ConfigStore 四层配置管理系统的详细操作指南与接口参考。 --- diff --git a/document/HandBook/desktop/base/logger/advanced_usage.md b/document/HandBook/desktop/base/logger/advanced_usage.md index 968017e2e..cc86e894e 100644 --- a/document/HandBook/desktop/base/logger/advanced_usage.md +++ b/document/HandBook/desktop/base/logger/advanced_usage.md @@ -1,3 +1,8 @@ +--- +title: 高级用法 +description: 本文档介绍 CFLogger 的高级功能,包括自定义 Sink、Formatter 以及运行时配置。 +--- + # 高级用法 本文档介绍 CFLogger 的高级功能,包括自定义 Sink、Formatter 以及运行时配置。 @@ -23,7 +28,7 @@ #include "cflog/cflog_format_factory.h" #include "cflog/formatter/console_formatter.h" #include "cflog/sinks/console_sink.h" -``` +```text ### 获取 Logger 实例 @@ -31,7 +36,7 @@ using namespace cf::log; auto& logger = Logger::instance(); -``` +```text ### 基本用法 @@ -45,7 +50,7 @@ logger.setMininumLevel(level::INFO); // 刷新 logger.flush(); // 异步 logger.flush_sync(); // 同步 -``` +```text ## 自定义 Sink @@ -84,7 +89,7 @@ private: std::string prefix_; std::ostream& output_stream_ = std::cout; }; -``` +```text ### 使用自定义 Sink @@ -97,7 +102,7 @@ my_sink->setFormat(std::make_shared()); // 添加到 Logger Logger::instance().add_sink(my_sink); -``` +```text ### 网络 Sink 示例 @@ -137,7 +142,7 @@ private: asio::ip::tcp::socket socket_; std::vector messages_; }; -``` +```text ## 自定义 Formatter @@ -166,7 +171,7 @@ public: bool configurable() const override { return false; } }; -``` +```text ### 可配置的 Formatter @@ -214,7 +219,7 @@ public: private: std::shared_ptr config_; }; -``` +```text ## FormatterFactory 使用 @@ -242,7 +247,7 @@ auto cached = factory.get_or_create("console"); // 清除缓存 factory.clear_cache(); -``` +```text ### 运行时切换格式 @@ -262,7 +267,7 @@ sink->setFormat(factory.create("simple")); // 运行时切换到详细格式 sink->setFormat(factory.create("verbose")); -``` +```text ## 动态 Sink 管理 @@ -274,7 +279,7 @@ void enable_file_logging(const std::string& path) { file_sink->setFormat(std::make_shared()); Logger::instance().add_sink(file_sink); } -``` +```text ### 运行时移除 Sink @@ -282,13 +287,13 @@ void enable_file_logging(const std::string& path) { void disable_file_logging(FileSink* sink) { Logger::instance().remove_sink(sink); } -``` +```text ### 清除所有 Sink ```cpp Logger::instance().clear_sinks(); -``` +```text ## 运行时配置修改 @@ -300,7 +305,7 @@ Logger::instance().setMininumLevel(level::INFO); // 晚上调试时使用 DEBUG 级别 Logger::instance().setMininumLevel(level::DEBUG); -``` +```text ### 动态修改 Formatter 配置 @@ -315,7 +320,7 @@ config->disable(FormatterFlag::COLOR); config->set_timestamp_format("%Y-%m-%d %H:%M:%S"); formatter->set_config(config); -``` +```text ## 多环境配置 @@ -340,7 +345,7 @@ void setup_dev_logging() { Logger::instance().add_sink(console); Logger::instance().setMininumLevel(level::TRACE); } -``` +```text ### 生产环境配置 @@ -375,7 +380,7 @@ void setup_prod_logging() { Logger::instance().add_sink(error_file); Logger::instance().setMininumLevel(level::INFO); } -``` +```text ## 完整示例 @@ -445,7 +450,7 @@ int main(int argc, char** argv) { Logger::instance().flush_sync(); return 0; } -``` +```text ## 下一步 diff --git a/document/HandBook/desktop/base/logger/basic_logging.md b/document/HandBook/desktop/base/logger/basic_logging.md index d4148af45..b8c6d2451 100644 --- a/document/HandBook/desktop/base/logger/basic_logging.md +++ b/document/HandBook/desktop/base/logger/basic_logging.md @@ -1,3 +1,8 @@ +--- +title: 基础日志详解 +description: 本文档详细介绍 CFLogger 的简单 API 使用方法。 +--- + # 基础日志详解 本文档详细介绍 CFLogger 的简单 API 使用方法。 @@ -8,7 +13,7 @@ ```cpp #include "cflog/cflog.h" -``` +```text ## 日志函数详解 @@ -18,7 +23,7 @@ void trace(std::string_view msg, std::string_view tag = "CFLog", std::source_location loc = std::source_location::current()); -``` +```text **用途**:记录最详细的输出信息,通常用于追踪函数调用流程。 @@ -30,7 +35,7 @@ void process_request(const std::string& request) { // 处理请求... trace("请求处理完成", "HTTP"); } -``` +```text **建议**: - 生产环境通常设置为禁用 @@ -43,7 +48,7 @@ void process_request(const std::string& request) { void debug(std::string_view msg, std::string_view tag = "CFLog", std::source_location loc = std::source_location::current()); -``` +```text **用途**:记录调试信息,帮助开发者理解程序运行状态。 @@ -55,7 +60,7 @@ void connect_database(const std::string& url) { // 连接逻辑... debug("数据库连接成功", "Database"); } -``` +```text **建议**: - 开发环境默认级别 @@ -68,7 +73,7 @@ void connect_database(const std::string& url) { void info(std::string_view msg, std::string_view tag = "CFLog", std::source_location loc = std::source_location::current()); -``` +```text **用途**:记录程序正常运行的重要信息。 @@ -78,7 +83,7 @@ void info(std::string_view msg, info("应用程序启动", "App"); info("配置文件加载完成", "Config"); info("服务器启动,监听端口 8080", "Server"); -``` +```text **建议**: - 生产环境推荐的最低级别 @@ -91,7 +96,7 @@ info("服务器启动,监听端口 8080", "Server"); void warning(std::string_view msg, std::string_view tag = "CFLog", std::source_location loc = std::source_location::current()); -``` +```text **用途**:记录潜在的问题,不会影响程序继续运行。 @@ -104,7 +109,7 @@ void load_config(const std::string& path) { // 使用默认配置... } } -``` +```text **建议**: - 记录可恢复的异常情况 @@ -116,7 +121,7 @@ void load_config(const std::string& path) { ```cpp void error(std::string_view msg, std::source_location loc = std::source_location::current()); -``` +```text **用途**:记录错误和异常情况。 @@ -128,7 +133,7 @@ void save_file(const std::string& path) { error("文件保存失败: " + path, "FileIO"); } } -``` +```text **重要特性**: - **ERROR 日志永不丢失**:即使队列满,ERROR 日志也会被保留 @@ -141,7 +146,7 @@ void save_file(const std::string& path) { ```cpp void set_level(level lvl); -``` +```text 设置全局最低日志级别,低于此级别的日志将被过滤。 @@ -165,7 +170,7 @@ int main() { flush(); return 0; } -``` +```bash ### 不同环境的推荐级别 @@ -187,7 +192,7 @@ info("用户登录成功", "Auth"); info("查询数据库", "Database"); warning("缓存未命中", "Cache"); error("连接超时", "Network"); -``` +```bash ### 推荐的标签命名 @@ -214,7 +219,7 @@ error("连接超时", "Network"); ```cpp void flush(); -``` +```text 异步刷新,请求工作线程处理队列,立即返回。 @@ -222,7 +227,7 @@ void flush(); info("重要操作开始"); do_something(); flush(); // 确保日志写入 -``` +```text **适用场景**: - 需要确保日志及时写入 @@ -232,7 +237,7 @@ flush(); // 确保日志写入 ```cpp void flush_sync(); // 高级 API -``` +```text 同步刷新,等待所有日志写入完成。 @@ -240,7 +245,7 @@ void flush_sync(); // 高级 API #include "cflog/cflog.hpp" Logger::instance().flush_sync(); -``` +```text **适用场景**: - 程序退出前 @@ -292,7 +297,7 @@ int main() { return 0; } -``` +```bash ## 与高级 API 的对比 diff --git a/document/HandBook/desktop/base/logger/best_practices.md b/document/HandBook/desktop/base/logger/best_practices.md index aa79e4f76..81d2f6009 100644 --- a/document/HandBook/desktop/base/logger/best_practices.md +++ b/document/HandBook/desktop/base/logger/best_practices.md @@ -1,3 +1,8 @@ +--- +title: 最佳实践 +description: 本文档总结了使用 CFLogger 的推荐做法和常见模式。 +--- + # 最佳实践 本文档总结了使用 CFLogger 的推荐做法和常见模式。 @@ -34,7 +39,7 @@ void process_user_login(const std::string& username) { trace("参数 username = " + username, "Auth"); // ... 过度日志 } -``` +```text ## 标签使用 @@ -50,7 +55,7 @@ info("缓存更新", "cache"); // 小写也可以,保持一致 info("连接成功", "db"); // 过于简短 info("请求处理", "HttpRequestHandler"); // 过于详细 info("缓存更新", "c"); // 意义不明 -``` +```text ### 按模块划分标签 @@ -75,7 +80,7 @@ public: debug("发送数据: " + data, "Network"); } }; -``` +```bash ### 常用标签建议 @@ -104,7 +109,7 @@ warning("查询耗时 " + std::to_string(duration_ms) + "ms 超过阈值", "Data error("文件打开失败", "FileIO"); info("用户登录", "Auth"); warning("查询慢", "Database"); -``` +```text ### 结构化消息 @@ -115,7 +120,7 @@ info("请求处理 | method=POST | path=/api/users | duration=50ms | status=200" // ✅ 使用键值对 error("数据库错误 | code=" + std::to_string(err.code) + " | msg=" + err.message + " | query=" + query, "Database"); -``` +```text ### 避免敏感信息 @@ -126,7 +131,7 @@ info("用户登录: user=admin&password=123456", "Auth"); // ✅ 脱敏处理 info("用户登录: user=admin&password=****", "Auth"); info("信用卡支付: ****-****-****-" + last4, "Payment"); -``` +```text ## 性能考虑 @@ -148,7 +153,7 @@ void process_data(const std::vector& items) { } info("完成处理 " + std::to_string(items.size()) + " 个项目"); } -``` +```text ### 延迟计算 @@ -160,7 +165,7 @@ trace("调试信息: " + expensive_computation()); // 即使 TRACE 被过滤也 if (should_log(level::TRACE)) { trace("调试信息: " + expensive_computation()); } -``` +```text ### 字符串拼接 @@ -174,7 +179,7 @@ info(msg); // ✅ 单次拼接 info("用户: " + username + ", 操作: " + action); -``` +```text ## 线程安全 @@ -197,7 +202,7 @@ int main() { t.join(); } } -``` +```text ### 共享资源的日志 @@ -215,7 +220,7 @@ private: std::mutex mutex_; int counter_ = 0; }; -``` +```text ## 错误处理 @@ -237,7 +242,7 @@ try { error("数据库连接失败 | url=" + url + " | error=" + std::string(e.what()), "Database"); } -``` +```text ### 关键操作前后 @@ -254,7 +259,7 @@ void save_to_file(const std::string& path, const Data& data) { throw; } } -``` +```text ## 启动和关闭 @@ -275,7 +280,7 @@ int main(int argc, char** argv) { return 0; } -``` +```text ### 应用关闭 @@ -291,7 +296,7 @@ int main(int argc, char** argv) { return 0; } -``` +```text ## 配置管理 @@ -319,7 +324,7 @@ void setup_logging(Environment env) { break; } } -``` +```text ### 动态调整 @@ -339,7 +344,7 @@ public: Logger::instance().setMininumLevel(static_cast(new_level)); } }; -``` +```text ## 日志轮转 @@ -356,7 +361,7 @@ class DailyFileSink : public ISink { // 每天创建新文件 // app_20260316.log, app_20260317.log, ... }; -``` +```text ## 单元测试 @@ -373,7 +378,7 @@ TEST(MyTest, TestSomething) { // 测试结束恢复 Logger::instance().setMininumLevel(level::WARNING); } -``` +```text ### Mock Sink @@ -404,7 +409,7 @@ TEST(LoggingTest, ErrorLogged) { ASSERT_FALSE(mock->messages.empty()); ASSERT_TRUE(mock->messages[0].find("Test error") != std::string::npos); } -``` +```text ## 检查清单 diff --git a/document/HandBook/desktop/base/logger/configuration.md b/document/HandBook/desktop/base/logger/configuration.md index 8c5a73f35..88ec43efb 100644 --- a/document/HandBook/desktop/base/logger/configuration.md +++ b/document/HandBook/desktop/base/logger/configuration.md @@ -1,3 +1,8 @@ +--- +title: 配置选项 +description: 本文档详细介绍 CFLogger 的所有配置选项。 +--- + # 配置选项 本文档详细介绍 CFLogger 的所有配置选项。 @@ -14,7 +19,7 @@ enum class level { WARNING, // 警告信息 ERROR // 错误信息 }; -``` +```text ### 设置最低级别 @@ -24,13 +29,13 @@ set_level(level::INFO); // 高级 API Logger::instance().setMininumLevel(level::INFO); -``` +```text ### 级别关系 -``` +```text TRACE (0) < DEBUG (1) < INFO (2) < WARNING (3) < ERROR (4) -``` +```bash 设置某级别后,只有该级别及以上的日志会被记录: @@ -61,7 +66,7 @@ void setup_logging_by_env() { Logger::instance().setMininumLevel(lvl); } -``` +```text ## FormatterFlag 配置 @@ -78,7 +83,7 @@ enum FormatterFlag : uint32_t { MESSAGE = 1 << 5, // 消息内容 COLOR = 1 << 6, // ANSI 颜色 }; -``` +```text ### 预设组合 @@ -86,7 +91,7 @@ enum FormatterFlag : uint32_t { MINIMAL = LEVEL | MESSAGE; DEFAULT = TIMESTAMP | LEVEL | TAG | SOURCE_LOCATION | MESSAGE; VERBOSE = TIMESTAMP | LEVEL | TAG | THREAD_ID | SOURCE_LOCATION | MESSAGE; -``` +```bash ### 输出组件示例 @@ -118,7 +123,7 @@ if (config->is_enabled(FormatterFlag::COLOR)) { config->set_flags(FormatterFlag::MINIMAL); formatter->set_config(config); -``` +```text ### 位运算组合 @@ -131,7 +136,7 @@ auto flags = FormatterFlag::DEFAULT & ~FormatterFlag::SOURCE_LOCATION; // 添加某个标志 auto flags = FormatterFlag::MINIMAL | FormatterFlag::TIMESTAMP; -``` +```text ## 时间戳格式配置 @@ -140,7 +145,7 @@ auto flags = FormatterFlag::MINIMAL | FormatterFlag::TIMESTAMP; ```cpp auto config = std::make_shared(); config->set_timestamp_format("%Y-%m-%d %H:%M:%S"); -``` +```bash ### 常用格式 @@ -195,7 +200,7 @@ formatter->set_config(config); // 方法3:使用 FileFormatter(自动忽略颜色) auto formatter = std::make_shared(); -``` +```text ## 队列配置 @@ -204,7 +209,7 @@ auto formatter = std::make_shared(); ```cpp // AsyncPostQueue 中的常量 static constexpr size_t kMaxNormalQueueSize = 65536; // 2^16 -``` +```bash 这是编译时常量,运行时不可修改。 @@ -233,7 +238,7 @@ void monitor_queue() { last_count = current_count; } } -``` +```text ## 文件 Sink 配置 @@ -244,7 +249,7 @@ enum class OpenMode { Append, // 追加到文件末尾 Truncate // 覆盖现有文件 }; -``` +```text ### 使用示例 @@ -254,7 +259,7 @@ auto sink = std::make_shared("app.log"); // 覆盖模式(测试环境) auto sink = std::make_shared("app.log", OpenMode::Truncate); -``` +```text ## 多环境配置 @@ -283,7 +288,7 @@ void setup_dev_environment() { Logger::instance().add_sink(console); Logger::instance().setMininumLevel(level::TRACE); } -``` +```text ### 测试环境 @@ -305,7 +310,7 @@ void setup_test_environment() { Logger::instance().add_sink(console); Logger::instance().setMininumLevel(level::DEBUG); } -``` +```text ### 生产环境 @@ -344,7 +349,7 @@ void setup_production_environment() { Logger::instance().add_sink(error_log); Logger::instance().setMininumLevel(level::INFO); } -``` +```text ## 配置文件示例 @@ -377,7 +382,7 @@ void setup_production_environment() { ] } } -``` +```text ### 命令行参数 @@ -426,7 +431,7 @@ void parse_command_line_args(int argc, char** argv) { Logger::instance().add_sink(file_sink); } } -``` +```text ## 配置最佳实践 diff --git a/document/HandBook/desktop/base/logger/formatters.md b/document/HandBook/desktop/base/logger/formatters.md index 839f5e46b..da22b8c64 100644 --- a/document/HandBook/desktop/base/logger/formatters.md +++ b/document/HandBook/desktop/base/logger/formatters.md @@ -1,3 +1,8 @@ +--- +title: 格式化器 (Formatter) 详解 +description: Formatter 负责将 LogRecord 转换为可读的文本格式。 +--- + # 格式化器 (Formatter) 详解 Formatter 负责将 LogRecord 转换为可读的文本格式。 @@ -6,13 +11,13 @@ Formatter 负责将 LogRecord 转换为可读的文本格式。 ### 什么是 Formatter? -``` +```text LogRecord (结构化数据) ↓ Formatter (格式化器) ↓ std::string (可读文本) -``` +```bash ### 内置 Formatter @@ -38,7 +43,7 @@ enum FormatterFlag : uint32_t { MESSAGE = 1 << 5, // 消息内容 COLOR = 1 << 6, // ANSI 颜色 }; -``` +```text ### 预设组合 @@ -51,7 +56,7 @@ DEFAULT = TIMESTAMP | LEVEL | TAG | SOURCE_LOCATION | MESSAGE // 详细:包含所有组件 VERBOSE = TIMESTAMP | LEVEL | TAG | THREAD_ID | SOURCE_LOCATION | MESSAGE -``` +```bash ### 输出示例 @@ -72,7 +77,7 @@ using namespace cf::log; // 使用默认配置 auto formatter = std::make_shared(); -``` +```text ### 自定义配置 @@ -86,7 +91,7 @@ auto minimal = std::make_shared( auto verbose = std::make_shared( FormatterFlag::VERBOSE | FormatterFlag::COLOR ); -``` +```text ### 运行时修改配置 @@ -104,7 +109,7 @@ config->enable(FormatterFlag::THREAD_ID); config->set_timestamp_format("%Y-%m-%d %H:%M:%S"); formatter->set_config(config); -``` +```bash ### ANSI 颜色映射 @@ -118,7 +123,7 @@ formatter->set_config(config); ### 输出示例 -``` +```text [14:23:45] [INFO] [CFLog] 应用启动 ^^^^^^ 绿色 @@ -127,7 +132,7 @@ formatter->set_config(config); [14:23:47] [ERROR] [Network] 连接失败 ^^^^^ 红色 -``` +```text ## FileFormatter @@ -139,7 +144,7 @@ formatter->set_config(config); using namespace cf::log; auto formatter = std::make_shared(); -``` +```text ### 特点 @@ -153,7 +158,7 @@ FileFormatter 与 AsciiColorFormatter 基本相同,但: auto formatter = std::make_shared( FormatterFlag::DEFAULT | FormatterFlag::COLOR // COLOR 被忽略 ); -``` +```text ## DefaultFormatter @@ -165,7 +170,7 @@ auto formatter = std::make_shared( #include "cflog/formatter/default_formatter.h" auto formatter = std::make_shared(); -``` +```text **输出**:只包含 `LogRecord.msg` @@ -186,7 +191,7 @@ auto config = std::make_shared( FormatterFlag::MINIMAL, "%H:%M:%S" // 时间格式 ); -``` +```text ### 线程安全操作 @@ -204,7 +209,7 @@ if (config->is_enabled(FormatterFlag::COLOR)) { // 设置所有标志 config->set_flags(FormatterFlag::VERBOSE); -``` +```bash ### 时间戳格式 @@ -233,7 +238,7 @@ public: return false; // 不支持配置 } }; -``` +```text ### 可配置自定义 @@ -283,7 +288,7 @@ private: return ""; } }; -``` +```text ## 常见格式示例 @@ -311,7 +316,7 @@ private: return ""; } }; -``` +```text ### Syslog 格式 @@ -342,7 +347,7 @@ private: return ""; } }; -``` +```text ## 使用 FormatterFactory @@ -361,7 +366,7 @@ auto formatter = factory.create("custom"); // 获取或创建(带缓存) auto cached = factory.get_or_create("custom"); -``` +```bash ## 选择建议 diff --git a/document/HandBook/desktop/base/logger/index.md b/document/HandBook/desktop/base/logger/index.md index bde4f3ed1..8c20fda03 100644 --- a/document/HandBook/desktop/base/logger/index.md +++ b/document/HandBook/desktop/base/logger/index.md @@ -1,10 +1,11 @@ -# logger - -> Welcome to the logger section. +--- +title: CFLogger 手册 +description: CFLogger 是 CFDesktop 的高性能异步日志系统,本手册详细介绍其初始化配置、日志级别 +--- -## Overview +# CFLogger 手册 -Documentation and resources for logger. +CFLogger 是 CFDesktop 的高性能异步日志系统,本手册详细介绍其初始化配置、日志级别设置、自定义 Sink 开发以及与桌面环境集成的最佳实践。 --- diff --git a/document/HandBook/desktop/base/logger/overview.md b/document/HandBook/desktop/base/logger/overview.md index a92599544..5846c192d 100644 --- a/document/HandBook/desktop/base/logger/overview.md +++ b/document/HandBook/desktop/base/logger/overview.md @@ -1,3 +1,8 @@ +--- +title: CFLogger 使用手册 +description: 欢迎使用 CFLogger 使用手册!CFLogger 是 CFDesktop 框架的异步日志系统, +--- + # CFLogger 使用手册 欢迎使用 CFLogger 使用手册!CFLogger 是 CFDesktop 框架的异步日志系统,专为现代桌面应用程序设计。 @@ -53,7 +58,7 @@ int main() { info("Hello, CFLogger!"); return 0; } -``` +```text ### 典型配置 @@ -74,21 +79,21 @@ void init_logger() { Logger::instance().setMininumLevel(level::INFO); } -``` +```text ## 日志输出示例 ### 默认格式输出 -``` +```text [14:23:45] [INFO] [CFLog] Application started [14:23:46] [WARNING] [Config] Config file not found [14:23:47] [ERROR] [Network] Connection failed -``` +```text ### 彩色控制台输出 -``` +```text [14:23:45] [INFO] [CFLog] Application started ^^^^^^ 绿色 @@ -97,7 +102,7 @@ void init_logger() { [14:23:47] [ERROR] [Network] Connection failed ^^^^^ 红色 -``` +```bash ## 手册结构 @@ -117,7 +122,7 @@ void init_logger() { ## 学习路径 -``` +```text 初学者路径: overview → quick_start → basic_logging → best_practices @@ -126,7 +131,7 @@ overview → advanced_usage → formatters → sinks → configuration 深入路径: overview → architecture → performance → troubleshooting -``` +```text ## 相关资源 @@ -135,7 +140,7 @@ overview → architecture → performance → troubleshooting ## 版本要求 -- C++17 或更高 +- C++23 或更高 - 支持的编译器:GCC 9+, Clang 10+, MSVC 2019+ - 支持的平台:Linux, Windows, macOS diff --git a/document/HandBook/desktop/base/logger/performance.md b/document/HandBook/desktop/base/logger/performance.md index e93a71deb..575d47764 100644 --- a/document/HandBook/desktop/base/logger/performance.md +++ b/document/HandBook/desktop/base/logger/performance.md @@ -1,3 +1,8 @@ +--- +title: 性能优化 +description: 本文档介绍 CFLogger 的性能特性和优化建议。 +--- + # 性能优化 本文档介绍 CFLogger 的性能特性和优化建议。 @@ -28,13 +33,13 @@ - 线程数: 16 - 每线程日志数: 10000 - 队列洪泛数: 70000 -``` +```text ## 架构性能优势 ### 异步处理 -``` +```text 同步日志 (阻塞): [调用线程] → [格式化] → [写入磁盘] → [返回] ↑_________阻塞________^ @@ -43,7 +48,7 @@ [调用线程] → [入队] → [立即返回] ↓ [工作线程] → [格式化] → [写入磁盘] -``` +```text **优势**: - 调用线程不会被 I/O 阻塞 @@ -56,7 +61,7 @@ CFLogger 使用无锁 MPSC(多生产者单消费者)队列: ```cpp cf::lockfree::MpscQueue normalQueue_; -``` +```text **优势**: - 多线程写入无需互斥锁 @@ -67,7 +72,7 @@ cf::lockfree::MpscQueue normalQueue_; ```cpp void submit(LogRecord record); // 按值传递 -``` +```text LogRecord 使用移动语义,避免字符串拷贝: @@ -75,7 +80,7 @@ LogRecord 使用移动语义,避免字符串拷贝: LogRecord record; record.msg = "很长的日志消息..."; async_queue_.submit(std::move(record)); // 移动,不拷贝 -``` +```text ## 性能影响因素 @@ -91,7 +96,7 @@ trace("这条消息不会被记录"); // 只做一次原子比较 // ❌ 高开销 set_level(level::TRACE); trace("这条消息会被记录"); // 入队、格式化、写入 -``` +```bash ### 消息大小 @@ -111,7 +116,7 @@ trace("完整响应: " + huge_json_response); // ✅ 好 trace("响应大小: " + std::to_string(response.size()) + " bytes"); debug("响应内容: " + response.substr(0, 100) + "..."); -``` +```text ### 格式化器复杂度 @@ -128,7 +133,7 @@ auto formatter = std::make_shared( auto formatter = std::make_shared( FormatterFlag::VERBOSE ); -``` +```bash ### Sink 类型 @@ -149,7 +154,7 @@ auto formatter = std::make_shared( #else Logger::instance().setMininumLevel(level::INFO); #endif -``` +```text ### 2. 避免热路径过度日志 @@ -165,7 +170,7 @@ for (int i = 0; i < 1000000; ++i) { // 处理... } debug("完成处理 1000000 个项目"); -``` +```text ### 3. 延迟计算 @@ -177,7 +182,7 @@ trace("详细信息: " + expensive_function()); if (Logger::instance().getMininumLevel() <= level::TRACE) { trace("详细信息: " + expensive_function()); } -``` +```text ### 4. 简化格式 @@ -192,7 +197,7 @@ if (Logger::instance().getMininumLevel() <= level::TRACE) { FormatterFlag::VERBOSE | FormatterFlag::COLOR ); #endif -``` +```text ### 5. 批量刷新 @@ -208,7 +213,7 @@ for (int i = 0; i < 1000; ++i) { info("项目 " + std::to_string(i)); } Logger::instance().flush(); // 最后统一刷新 -``` +```text ### 6. 监控队列溢出 @@ -226,13 +231,13 @@ void monitor_queue_overflow() { last_check = now; } } -``` +```text ## 内存使用 ### 内存估算 -``` +```text 每个 LogRecord 大小: - level: 4 字节 - tag: 约 32 字节 @@ -244,7 +249,7 @@ void monitor_queue_overflow() { 队列容量:65,536 条 队列内存:约 14 MB -``` +```text ### 内存优化 @@ -256,7 +261,7 @@ void log_message(std::string_view msg) { // ✅ 避免大量临时对象 info("数据: " + data.to_string()); // data.to_string() 返回临时字符串 -``` +```text ## 线程扩展性 @@ -264,13 +269,13 @@ info("数据: " + data.to_string()); // data.to_string() 返回临时字符串 CFLogger 在多线程环境下表现良好: -``` +```text 单线程: 10,000 条/秒 2 线程: 20,000 条/秒 4 线程: 40,000 条/秒 8 线程: 80,000 条/秒 16 线程: 120,000 条/秒 (开始饱和) -``` +```text ### 瓶颈分析 @@ -312,7 +317,7 @@ private: std::chrono::steady_clock::time_point start_time_; size_t log_count_ = 0; }; -``` +```text ### 使用示例 @@ -327,7 +332,7 @@ for (int i = 0; i < 10000; ++i) { Logger::instance().flush_sync(); monitor.report(); -``` +```text ## 性能检查清单 @@ -366,7 +371,7 @@ void setup_high_performance_logging() { // 较高的日志级别 Logger::instance().setMininumLevel(level::WARNING); } -``` +```text ### 调试配置 @@ -390,7 +395,7 @@ void setup_verbose_logging() { // 最低级别 Logger::instance().setMininumLevel(level::TRACE); } -``` +```text ## 下一步 diff --git a/document/HandBook/desktop/base/logger/quick_start.md b/document/HandBook/desktop/base/logger/quick_start.md index 4274b489b..bc4d4a491 100644 --- a/document/HandBook/desktop/base/logger/quick_start.md +++ b/document/HandBook/desktop/base/logger/quick_start.md @@ -1,3 +1,8 @@ +--- +title: 快速入门 +description: 欢迎使用 CFLogger!本指南将帮助你在 5 分钟内上手使用 CFLogger。 +--- + # 快速入门 欢迎使用 CFLogger!本指南将帮助你在 5 分钟内上手使用 CFLogger。 @@ -10,13 +15,13 @@ CFLogger 提供两套 API:简单 API 和高级 API。 ```cpp #include "cflog/cflog.h" -``` +```text ### 高级 API(需要更多控制) ```cpp #include "cflog/cflog.hpp" -``` +```text ## 第二步:Hello World @@ -36,13 +41,13 @@ int main() { return 0; } -``` +```text 编译运行后,你将看到: -``` +```text [INFO] Hello, CFLogger! -``` +```text ## 第三步:使用不同日志级别 @@ -63,7 +68,7 @@ int main() { flush(); return 0; } -``` +```bash ### 日志级别对照表 @@ -97,17 +102,17 @@ int main() { flush(); return 0; } -``` +```text ### 级别过滤规则 -``` +```text 设置为 TRACE:显示所有日志 设置为 DEBUG:显示 DEBUG, INFO, WARNING, ERROR 设置为 INFO: 显示 INFO, WARNING, ERROR 设置为 WARNING:显示 WARNING, ERROR 设置为 ERROR:只显示 ERROR -``` +```text ## 第五步:使用标签组织日志 @@ -128,7 +133,7 @@ int main() { flush(); return 0; } -``` +```bash ### 常用标签建议 @@ -191,7 +196,7 @@ int main() { Logger::instance().flush_sync(); // 等待写入完成 return 0; } -``` +```text ## 完整示例 @@ -234,7 +239,7 @@ int main() { return 0; } -``` +```text ## CMake 配置 @@ -249,7 +254,7 @@ add_executable(my_app main.cpp) # 链接 CFLogger target_link_libraries(my_app PRIVATE CFDesktop::logger) -``` +```text ## 编译运行 @@ -262,7 +267,7 @@ cmake --build build # 运行 ./build/my_app -``` +```yaml ## 常见问题 diff --git a/document/HandBook/desktop/base/logger/sinks.md b/document/HandBook/desktop/base/logger/sinks.md index 086a18245..e99cb776c 100644 --- a/document/HandBook/desktop/base/logger/sinks.md +++ b/document/HandBook/desktop/base/logger/sinks.md @@ -1,3 +1,8 @@ +--- +title: Sink 详解 +description: Sink 决定日志的输出目标。CFLogger 提供了 ConsoleSink 和 FileSink +--- + # Sink 详解 Sink 决定日志的输出目标。CFLogger 提供了 ConsoleSink 和 FileSink,并支持自定义 Sink。 @@ -6,7 +11,7 @@ Sink 决定日志的输出目标。CFLogger 提供了 ConsoleSink 和 FileSink ### 什么是 Sink? -``` +```text LogRecord (日志记录) ↓ Formatter (格式化) @@ -16,7 +21,7 @@ LogRecord (日志记录) Sink (输出目标) ↓ 实际存储位置 -``` +```text ### Sink 职责 @@ -39,7 +44,7 @@ auto console_sink = std::make_shared(); console_sink->setFormat(std::make_shared()); Logger::instance().add_sink(console_sink); -``` +```text **特点**: - 线程安全 @@ -67,7 +72,7 @@ auto file_sink = std::make_shared( file_sink->setFormat(std::make_shared()); Logger::instance().add_sink(file_sink); -``` +```bash **特点**: - 线程安全 @@ -108,7 +113,7 @@ void setup_logging() { file_sink->setFormat(factory.create("file")); Logger::instance().add_sink(file_sink); } -``` +```text ### 分级输出(普通日志 vs 错误日志) @@ -132,7 +137,7 @@ void setup_split_logging() { ); // 实际使用需要自定义 Sink 来过滤 ERROR 级别 } -``` +```text ## 自定义 Sink @@ -170,7 +175,7 @@ protected: std::shared_ptr formatter_; }; -``` +```text ### 旋转文件 Sink @@ -256,7 +261,7 @@ private: size_t file_index_; std::ofstream file_; }; -``` +```text ### 过滤 Sink @@ -289,7 +294,7 @@ private: std::shared_ptr wrapped_sink_; level min_level_; }; -``` +```text ### 统计 Sink @@ -328,7 +333,7 @@ private: mutable std::mutex mutex_; std::map counts_; }; -``` +```text ### 网络 Sink @@ -444,7 +449,7 @@ private: std::string sending_; std::mutex queue_mutex_; }; -``` +```text ## Sink 最佳实践 @@ -464,7 +469,7 @@ public: private: std::mutex mutex_; }; -``` +```text ### 2. 错误处理 @@ -480,7 +485,7 @@ bool write(const LogRecord& record) override { return false; } } -``` +```text ### 3. 资源清理 @@ -495,7 +500,7 @@ bool write(const LogRecord& record) override { socket_.close(); } } -``` +```text ## 使用示例 @@ -544,7 +549,7 @@ void setup_comprehensive_logging() { logger.setMininumLevel(level::INFO); } -``` +```text ## 下一步 diff --git a/document/HandBook/desktop/base/logger/troubleshooting.md b/document/HandBook/desktop/base/logger/troubleshooting.md index b145f23ac..bb413cacc 100644 --- a/document/HandBook/desktop/base/logger/troubleshooting.md +++ b/document/HandBook/desktop/base/logger/troubleshooting.md @@ -1,3 +1,8 @@ +--- +title: 故障排除 +description: 本文档列出 CFLogger 使用中的常见问题和解决方案。 +--- + # 故障排除 本文档列出 CFLogger 使用中的常见问题和解决方案。 @@ -23,7 +28,7 @@ int main() { flush(); // 或 Logger::instance().flush_sync() return 0; } -``` +```text **原因 2:日志级别设置过高** @@ -35,7 +40,7 @@ info("这条不会显示"); // INFO < WARNING // ✅ 调整级别 set_level(level::INFO); info("这条会显示"); -``` +```text **原因 3:没有添加 Sink** @@ -47,7 +52,7 @@ Logger::instance().log(level::INFO, "Hello", "Tag", {}); auto sink = std::make_shared(); Logger::instance().add_sink(sink); Logger::instance().log(level::INFO, "Hello", "Tag", {}); -``` +```text ### Q2: 程序崩溃后日志丢失 @@ -72,7 +77,7 @@ int main() { Logger::instance().flush_sync(); // 确保所有日志写入 return 0; } -``` +```text ### Q3: 队列溢出导致日志丢失 @@ -83,7 +88,7 @@ size_t overflow = Logger::instance().get_normal_queue_overflow(); if (overflow > 0) { warning("队列溢出,丢失 " + std::to_string(overflow) + " 条日志"); } -``` +```text #### 解决方法 @@ -95,7 +100,7 @@ Logger::instance().setMininumLevel(level::WARNING); // 减少详细日志 // trace() → debug() → info() -``` +```text **方法 2:批量日志** @@ -111,7 +116,7 @@ for (int i = 0; i < 10000; ++i) { // 处理... } trace("完成处理 10000 个项目"); -``` +```text **方法 3:异步处理加速** @@ -120,7 +125,7 @@ trace("完成处理 10000 个项目"); ```cpp // 使用更快的 Sink // 考虑使用内存缓冲 + 批量写入 -``` +```text ### Q4: 颜色显示异常 @@ -128,9 +133,9 @@ trace("完成处理 10000 个项目"); 在文件中看到 ANSI 转义码: -``` +```text [14:23:45] [INFO] [CFLog] ^[[92m消息^[[0m -``` +```text #### 原因 @@ -153,15 +158,15 @@ auto file_formatter = std::make_shared( auto config = std::make_shared(FormatterFlag::DEFAULT); config->disable(FormatterFlag::COLOR); formatter->set_config(config); -``` +```text ### Q5: 编译错误 #### 问题:找不到头文件 -``` +```text fatal error: cflog/cflog.h: No such file or directory -``` +```text #### 解决方法 @@ -176,13 +181,13 @@ target_link_libraries(my_app PRIVATE CFDesktop::logger) target_include_directories(my_app PRIVATE ${CMAKE_SOURCE_DIR}/desktop/base/logger/include ) -``` +```text #### 问题:链接错误 -``` +```text undefined reference to `cf::log::info(std::string_view, ...)` -``` +```text #### 解决方法 @@ -192,7 +197,7 @@ target_link_libraries(my_app PRIVATE CFDesktop::logger) # 检查 logger 库是否被构建 # 确保 CMake 选项 CFDESKTOP_BUILD_LOGGER 为 ON -``` +```text ### Q6: 多线程日志混乱 @@ -219,7 +224,7 @@ public: private: std::mutex mutex_; }; -``` +```text ### Q7: 性能问题 @@ -250,7 +255,7 @@ public: private: std::chrono::steady_clock::time_point start_; }; -``` +```text #### 解决方法 @@ -277,7 +282,7 @@ size_t overflow = Logger::instance().get_normal_queue_overflow(); if (overflow > 0) { // 日志产生速度 > 消费速度 } -``` +```text **原因 2:自定义 Sink 泄漏** @@ -293,7 +298,7 @@ public: private: std::vector messages_; // 从不清理 }; -``` +```text **解决方法** @@ -315,7 +320,7 @@ private: size_t max_size_ = 1000; std::mutex mutex_; }; -``` +```text ### Q9: 文件打开失败 @@ -347,7 +352,7 @@ void ensure_log_directory(const std::string& log_path) { // 使用 ensure_log_directory("/var/log/myapp/app.log"); auto sink = std::make_shared("/var/log/myapp/app.log"); -``` +```text ### Q10: 时间戳不正确 @@ -366,7 +371,7 @@ auto sink = std::make_shared("/var/log/myapp/app.log"); auto config = std::make_shared(); config->set_timestamp_format("%Y-%m-%d %H:%M:%S %z"); // 添加时区 formatter->set_config(config); -``` +```text ## 调试技巧 @@ -380,7 +385,7 @@ Logger::instance().setMininumLevel(level::TRACE); auto formatter = std::make_shared( FormatterFlag::VERBOSE | FormatterFlag::COLOR ); -``` +```text ### 监控队列状态 @@ -414,7 +419,7 @@ private: std::thread thread_; size_t last_overflow_ = 0; }; -``` +```text ### 捕获异常 @@ -433,7 +438,7 @@ public: } } }; -``` +```text ## 获取帮助 @@ -463,7 +468,7 @@ grep "\[Network\]" app.log # 实时过滤 tail -f app.log | grep ERROR -``` +```text ## 下一步 diff --git a/document/HandBook/desktop/index.md b/document/HandBook/desktop/index.md index bfafefa61..0533327bd 100644 --- a/document/HandBook/desktop/index.md +++ b/document/HandBook/desktop/index.md @@ -1,10 +1,11 @@ -# desktop - -> Welcome to the desktop section. +--- +title: 桌面环境手册 +description: 本目录包含 CFDesktop 桌面环境层各组件的使用手册,涵盖 CFLogger 日志系统的配置与 +--- -## Overview +# 桌面环境手册 -Documentation and resources for desktop. +本目录包含 CFDesktop 桌面环境层各组件的使用手册,涵盖 CFLogger 日志系统的配置与使用、ConfigStore 配置管理系统的接口说明等,帮助开发者快速上手桌面环境的核心功能模块。 --- diff --git a/document/HandBook/examples/.pages b/document/HandBook/examples/.pages deleted file mode 100644 index c9a472ae4..000000000 --- a/document/HandBook/examples/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: 示例 -icon: material/code-tags -nav: - - CPU 信息示例: cpu_info_example.md diff --git a/document/HandBook/examples/cpu_info_example.md b/document/HandBook/examples/cpu_info_example.md index 00e054ad8..a634cfdf7 100644 --- a/document/HandBook/examples/cpu_info_example.md +++ b/document/HandBook/examples/cpu_info_example.md @@ -1,193 +1,198 @@ -# CPU 信息查询示例 - -## 简介 - -本文档展示了如何使用 CFDesktop CPU 模块查询各种 CPU 信息。示例代码基于 `example/base/system/example_cpu_info.cpp`。 - -## 完整示例代码 - -```cpp -#include "system/cpu/cfcpu.h" -#include "system/cpu/cfcpu_bonus.h" -#include "system/cpu/cfcpu_profile.h" -#include - -int main() { - using namespace cf; - - // ===== 1. 查询基础信息 ===== - auto cpuInfo = getCPUInfo(); - if (!cpuInfo.has_value()) { - std::cerr << "无法获取 CPU 基础信息" << std::endl; - return 1; - } - - std::cout << "=== CPU 基础信息 ===" << std::endl; - std::cout << " 型号: " << std::string(cpuInfo->model) << std::endl; - std::cout << " 架构: " << std::string(cpuInfo->arch) << std::endl; - std::cout << " 制造商: " << std::string(cpuInfo->manufacturer) << std::endl; - - // ===== 2. 查询性能信息 ===== - auto profileInfo = getCPUProfileInfo(); - if (!profileInfo.has_value()) { - std::cerr << "无法获取 CPU 性能信息" << std::endl; - return 1; - } - - std::cout << "\n=== CPU 性能信息 ===" << std::endl; - std::cout << " 逻辑线程: " << profileInfo->logical_cnt << std::endl; - std::cout << " 物理核心: " << profileInfo->physical_cnt << std::endl; - std::cout << " 当前频率: " << profileInfo->current_frequecy << " MHz" << std::endl; - std::cout << " 最大频率: " << profileInfo->max_frequency << " MHz" << std::endl; - std::cout << " 使用率: " << profileInfo->cpu_usage_percentage << "%" << std::endl; - - // ===== 3. 查询扩展信息 ===== - auto bonusInfo = getCPUBonusInfo(); - if (!bonusInfo.has_value()) { - std::cerr << "无法获取 CPU 扩展信息" << std::endl; - return 1; - } - - std::cout << "\n=== CPU 扩展信息 ===" << std::endl; - - // 打印 CPU 特性 - std::cout << " 特性: "; - for (const auto& feature : bonusInfo->features) { - std::cout << std::string(feature) << " "; - } - std::cout << std::endl; - - // 打印缓存大小 - std::cout << " 缓存: "; - for (const auto& cache : bonusInfo->cache_size) { - std::cout << cache << "KB "; - } - std::cout << std::endl; - - // 打印大小核信息 - if (bonusInfo->has_big_little) { - std::cout << " 大小核架构: 是" << std::endl; - std::cout << " 大核: " << bonusInfo->big_core_count << std::endl; - std::cout << " 小核: " << bonusInfo->little_core_count << std::endl; - } else { - std::cout << " 大小核架构: 否" << std::endl; - } - - // 打印温度 - if (bonusInfo->temperature.has_value()) { - std::cout << " 温度: " << *bonusInfo->temperature << "°C" << std::endl; - } else { - std::cout << " 温度: 不可用" << std::endl; - } - - return 0; -} -``` - -## 编译和运行 - -### 编译 - -```bash -cd /home/charliechen/project/QtProjects/CFDesktop -./scripts/build_helpers/linux_fast_develop_build.sh -``` - -### 运行 - -```bash -./out/build_develop/example/base/system/example_cpu_info -``` - -## 示例输出 - -在 x86_64 Linux 系统上的典型输出: - -``` -=== CPU 基础信息 === - 型号: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz - 架构: x86_64 - 制造商: GenuineIntel - -=== CPU 性能信息 === - 逻辑线程: 12 - 物理核心: 6 - 当前频率: 2600 MHz - 最大频率: 4500 MHz - 使用率: 15.5% - -=== CPU 扩展信息 === - 特性: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single ssbd ibrs ibpb stibp ibrs_enhanced tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_act_window hwp_epp hwp_pkg_req - - 缓存: 32KB 256KB 12288KB - 大小核架构: 否 - 温度: 不可用 -``` - -在 ARM Linux 系统上的典型输出: - -``` -=== CPU 基础信息 === - 型号: ARM Cortex-A76 - 架构: aarch64 - 制造商: ARM - -=== CPU 性能信息 === - 逻辑线程: 8 - 物理核心: 8 - 当前频率: 1800 MHz - 最大频率: 2400 MHz - 使用率: 8.2% - -=== CPU 扩展信息 === - 特性: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm ssbs sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint - - 缓存: 64KB 512KB 4096KB - 大小核架构: 否 - 温度: 45°C -``` - -## 错误处理 - -所有 API 都返回 `expected` 类型,需要检查是否有值: - -```cpp -auto result = cf::getCPUInfo(); - -if (!result.has_value()) { - // 处理错误 - switch (result.error()) { - case cf::CPUInfoErrorType::CPU_QUERY_NOERROR: - break; - case cf::CPUInfoErrorType::CPU_QUERY_GENERAL_FAILED: - std::cerr << "查询失败" << std::endl; - break; - } - return 1; -} - -// 使用结果 -auto info = result.value(); -``` - -## 最佳实践 - -1. **检查返回值**:始终检查 `has_value()` 或使用 `operator->` 直接访问 - -2. **避免存储视图**:`CPUInfoView` 和 `CPUBonusInfoView` 中的 `string_view` 仅在缓存有效时可用 - -3. **强制刷新**:需要更新信息时使用 `force_refresh = true` - -```cpp -// 首次查询 -auto info1 = cf::getCPUInfo(); - -// 稍后刷新查询 -auto info2 = cf::getCPUInfo(true); -``` - -## 相关文档 - -- [基础信息 API](../api/system/cpu/cfcpu.md) -- [性能信息 API](../api/system/cpu/cfcpu_profile.md) -- [扩展信息 API](../api/system/cpu/cfcpu_bonus.md) +--- +title: CPU 信息查询示例 +description: 本文档展示了如何使用 CFDesktop CPU 模块查询各种 CPU 信息。示例代码基于 。 +--- + +# CPU 信息查询示例 + +## 简介 + +本文档展示了如何使用 CFDesktop CPU 模块查询各种 CPU 信息。示例代码基于 `example/base/system/example_cpu_info.cpp`。 + +## 完整示例代码 + +```cpp +#include "system/cpu/cfcpu.h" +#include "system/cpu/cfcpu_bonus.h" +#include "system/cpu/cfcpu_profile.h" +#include + +int main() { + using namespace cf; + + // ===== 1. 查询基础信息 ===== + auto cpuInfo = getCPUInfo(); + if (!cpuInfo.has_value()) { + std::cerr << "无法获取 CPU 基础信息" << std::endl; + return 1; + } + + std::cout << "=== CPU 基础信息 ===" << std::endl; + std::cout << " 型号: " << std::string(cpuInfo->model) << std::endl; + std::cout << " 架构: " << std::string(cpuInfo->arch) << std::endl; + std::cout << " 制造商: " << std::string(cpuInfo->manufacturer) << std::endl; + + // ===== 2. 查询性能信息 ===== + auto profileInfo = getCPUProfileInfo(); + if (!profileInfo.has_value()) { + std::cerr << "无法获取 CPU 性能信息" << std::endl; + return 1; + } + + std::cout << "\n=== CPU 性能信息 ===" << std::endl; + std::cout << " 逻辑线程: " << profileInfo->logical_cnt << std::endl; + std::cout << " 物理核心: " << profileInfo->physical_cnt << std::endl; + std::cout << " 当前频率: " << profileInfo->current_frequecy << " MHz" << std::endl; + std::cout << " 最大频率: " << profileInfo->max_frequency << " MHz" << std::endl; + std::cout << " 使用率: " << profileInfo->cpu_usage_percentage << "%" << std::endl; + + // ===== 3. 查询扩展信息 ===== + auto bonusInfo = getCPUBonusInfo(); + if (!bonusInfo.has_value()) { + std::cerr << "无法获取 CPU 扩展信息" << std::endl; + return 1; + } + + std::cout << "\n=== CPU 扩展信息 ===" << std::endl; + + // 打印 CPU 特性 + std::cout << " 特性: "; + for (const auto& feature : bonusInfo->features) { + std::cout << std::string(feature) << " "; + } + std::cout << std::endl; + + // 打印缓存大小 + std::cout << " 缓存: "; + for (const auto& cache : bonusInfo->cache_size) { + std::cout << cache << "KB "; + } + std::cout << std::endl; + + // 打印大小核信息 + if (bonusInfo->has_big_little) { + std::cout << " 大小核架构: 是" << std::endl; + std::cout << " 大核: " << bonusInfo->big_core_count << std::endl; + std::cout << " 小核: " << bonusInfo->little_core_count << std::endl; + } else { + std::cout << " 大小核架构: 否" << std::endl; + } + + // 打印温度 + if (bonusInfo->temperature.has_value()) { + std::cout << " 温度: " << *bonusInfo->temperature << "°C" << std::endl; + } else { + std::cout << " 温度: 不可用" << std::endl; + } + + return 0; +} +```text + +## 编译和运行 + +### 编译 + +```bash +cd /home/charliechen/project/QtProjects/CFDesktop +./scripts/build_helpers/linux_fast_develop_build.sh +```text + +### 运行 + +```bash +./out/build_develop/example/base/system/example_cpu_info +```text + +## 示例输出 + +在 x86_64 Linux 系统上的典型输出: + +```yaml +=== CPU 基础信息 === + 型号: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz + 架构: x86_64 + 制造商: GenuineIntel + +=== CPU 性能信息 === + 逻辑线程: 12 + 物理核心: 6 + 当前频率: 2600 MHz + 最大频率: 4500 MHz + 使用率: 15.5% + +=== CPU 扩展信息 === + 特性: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single ssbd ibrs ibpb stibp ibrs_enhanced tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_act_window hwp_epp hwp_pkg_req + + 缓存: 32KB 256KB 12288KB + 大小核架构: 否 + 温度: 不可用 +```text + +在 ARM Linux 系统上的典型输出: + +```yaml +=== CPU 基础信息 === + 型号: ARM Cortex-A76 + 架构: aarch64 + 制造商: ARM + +=== CPU 性能信息 === + 逻辑线程: 8 + 物理核心: 8 + 当前频率: 1800 MHz + 最大频率: 2400 MHz + 使用率: 8.2% + +=== CPU 扩展信息 === + 特性: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm ssbs sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint + + 缓存: 64KB 512KB 4096KB + 大小核架构: 否 + 温度: 45°C +```text + +## 错误处理 + +所有 API 都返回 `expected` 类型,需要检查是否有值: + +```cpp +auto result = cf::getCPUInfo(); + +if (!result.has_value()) { + // 处理错误 + switch (result.error()) { + case cf::CPUInfoErrorType::CPU_QUERY_NOERROR: + break; + case cf::CPUInfoErrorType::CPU_QUERY_GENERAL_FAILED: + std::cerr << "查询失败" << std::endl; + break; + } + return 1; +} + +// 使用结果 +auto info = result.value(); +```cpp + +## 最佳实践 + +1. **检查返回值**:始终检查 `has_value()` 或使用 `operator->` 直接访问 + +2. **避免存储视图**:`CPUInfoView` 和 `CPUBonusInfoView` 中的 `string_view` 仅在缓存有效时可用 + +3. **强制刷新**:需要更新信息时使用 `force_refresh = true` + +```cpp +// 首次查询 +auto info1 = cf::getCPUInfo(); + +// 稍后刷新查询 +auto info2 = cf::getCPUInfo(true); +```text + +## 相关文档 + +- [基础信息 API](../api/system/cpu/cfcpu.md) +- [性能信息 API](../api/system/cpu/cfcpu_profile.md) +- [扩展信息 API](../api/system/cpu/cfcpu_bonus.md) diff --git a/document/HandBook/examples/index.md b/document/HandBook/examples/index.md index e0054c23f..72cefb93b 100644 --- a/document/HandBook/examples/index.md +++ b/document/HandBook/examples/index.md @@ -1,10 +1,11 @@ -# examples - -> Welcome to the examples section. +--- +title: 示例代码 +description: 本目录包含演示 CFDesktop API 用法的示例代码,覆盖硬件探测、UI 组件使用、配置管理以 +--- -## Overview +# 示例代码 -Documentation and resources for examples. +本目录包含演示 CFDesktop API 用法的示例代码,覆盖硬件探测、UI 组件使用、配置管理以及日志系统等典型场景,帮助开发者快速理解项目各模块的集成方式与调用约定。 --- diff --git a/document/HandBook/implementation/.pages b/document/HandBook/implementation/.pages deleted file mode 100644 index c1fef9526..000000000 --- a/document/HandBook/implementation/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: 平台实现 -nav: - - Linux: linux - - Windows: windows diff --git a/document/HandBook/implementation/index.md b/document/HandBook/implementation/index.md index d89831896..c48cca85a 100644 --- a/document/HandBook/implementation/index.md +++ b/document/HandBook/implementation/index.md @@ -1,10 +1,11 @@ -# implementation - -> Welcome to the implementation section. +--- +title: 平台实现 +description: 本目录包含 CFDesktop 在不同平台下的实现细节文档,主要涵盖 Linux(WSL X11)和 +--- -## Overview +# 平台实现 -Documentation and resources for implementation. +本目录包含 CFDesktop 在不同平台下的实现细节文档,主要涵盖 Linux(WSL X11)和 Windows 平台的窗口管理、显示后端(Display Server Backend)适配以及平台特定的系统调用封装。 --- diff --git a/document/HandBook/implementation/linux/.pages b/document/HandBook/implementation/linux/.pages deleted file mode 100644 index 36063c498..000000000 --- a/document/HandBook/implementation/linux/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: Linux 实现 -nav: - - CPU 实现: cpu_implementation.md - - 内存实现: memory diff --git a/document/HandBook/implementation/linux/cpu_implementation.md b/document/HandBook/implementation/linux/cpu_implementation.md index 7ec63611a..66a0e47bb 100644 --- a/document/HandBook/implementation/linux/cpu_implementation.md +++ b/document/HandBook/implementation/linux/cpu_implementation.md @@ -1,149 +1,154 @@ -# Linux 平台实现细节 - -Linux 下的 CPU 信息查询主要通过解析 `/proc` 和 `/sys` 伪文件系统实现。这套方案的优势是不需要额外的库依赖,缺点是文件格式比较繁琐——所以专门写了一套 `proc_parser` 工具来处理。 - -## 基础信息 - -CPU 型号、厂商这些静态信息从 `/proc/cpuinfo` 读取。这个文件的格式是固定的 `key: value` 结构,但不同架构下字段名不一样。x86 用 `vendor_id`,ARM 用 `CPU implementer`(是个十六进制 ID,需要转换)。 - -```cpp -cf::expected query_cpu_basic_info(cf::CPUInfoHost& hostInfo) { - std::ifstream cpuinfo("/proc/cpuinfo"); - std::string line; - - while (std::getline(cpuinfo, line)) { - std::string_view line_sv = line; - - // 解析 model name 字段 - const std::string_view model = cf::parse_cpuinfo_field(line_sv, "model name"); - if (!model.empty()) { - hostInfo.model = model; - } - - // ARM 架构需要特殊处理 - const std::string_view implementer = cf::parse_cpuinfo_field(line_sv, "CPU implementer"); - if (auto impl_val = cf::parse_hex_uint32(implementer)) { - hostInfo.manufest = cf::arm_implementer_to_vendor(*impl_val); - } - } - - // 架构信息用 uname() 获取更可靠 - struct utsname unameInfo; - if (uname(&unameInfo) == 0) { - hostInfo.arch = unameInfo.machine; - } - - return {}; -} -``` - -架构信息用 `uname()` 而不是读文件,是因为某些嵌入式板子的 `/proc/cpuinfo` 可能不包含完整的架构字段。 - -## 性能信息 - -核心数量和频率信息散落在不同地方:`/proc/cpuinfo` 有核心数,`/sys/devices/system/cpu/cpu0/cpufreq/` 里有频率。使用率需要读 `/proc/stat`,计算两次采样之间的时间差。 - -```cpp -// /proc/stat 第一行格式: -// cpu user nice system idle iowait ... -// 例如:cpu 2255 34 2290 22625563 6290 127 456 - -float calculate_cpu_usage() { - static uint64_t last_total = 0, last_idle = 0; - - uint64_t user, nice, system, idle; - FILE* stat = fopen("/proc/stat", "r"); - fscanf(stat, "cpu %lu %lu %lu %lu", &user, &nice, &system, &idle); - fclose(stat); - - uint64_t total = user + nice + system + idle; - uint64_t total_delta = total - last_total; - uint64_t idle_delta = idle - last_idle; - - last_total = total; - last_idle = idle; - - if (total_delta == 0) return 0.0f; - return 100.0f * (1.0f - static_cast(idle_delta) / total_delta); -} -``` - -⚠️ 首次调用会返回不准确的数据,因为需要上次采样的值作为基准。 - -## 扩展信息 - -CPU 特性标志在 `flags`(x86)或 `Features`(ARM)字段里,是个空格分隔的列表。缓存信息从 `/sys/devices/system/cpu/cpu0/cache/` 读取,每个缓存级别一个目录。 - -```cpp -// /sys/devices/system/cpu/cpu0/cache/ -// ├── index0/ -> L1 数据缓存 -// ├── index1/ -> L1 指令缓存 -// ├── index2/ -> L2 统一缓存 -// └── index3/ -> L3 统一缓存 - -// 读取缓存大小 -auto l1_size = cf::read_uint32_file("/sys/devices/system/cpu/cpu0/cache/index0/size"); -// 输出通常是 "32K",parse_cache_size() 会处理单位转换 -``` - -温度信息从 `/sys/class/thermal/thermal_zone*/temp` 读取,但不是所有设备都有温度传感器。返回值通常是 millidegree,需要除以 1000 转成摄氏度。 - -## big.LITTLE 检测 - -ARM 的大小核架构检测是通过比较不同 CPU 核心的最大频率来实现的。如果发现不同的频率值,就假定存在大小核,然后按频率分组统计核心数。 - -```cpp -bool detect_big_little(cf::CPUBonusInfoHost& host) { - std::vector max_frequencies; - - // 收集每个核心的最大频率 - for (int cpu = 0; cpu < logical_cores; ++cpu) { - std::string path = "/sys/devices/system/cpu/cpu" + std::to_string(cpu) + - "/cpufreq/cpuinfo_max_freq"; - auto freq = cf::read_uint32_file(path.c_str()); - if (freq.has_value()) { - max_frequencies.push_back(*freq); - } - } - - // 检查是否有不同的频率 - auto min_freq = *std::min_element(max_frequencies.begin(), max_frequencies.end()); - auto max_freq = *std::max_element(max_frequencies.begin(), max_frequencies.end()); - - if (min_freq != max_freq) { - host.has_big_little = true; - host.big_core_count = std::count(max_frequencies.begin(), max_frequencies.end(), max_freq); - host.little_core_count = std::count(max_frequencies.begin(), max_frequencies.end(), min_freq); - return true; - } - - return false; -} -``` - -这个方法不是百分之百可靠——有些同频 CPU 也可能被误判为大小核——但在我们支持的设备上效果还可以。 - -## 平台差异 - -ARM 和 x86 在 `cpuinfo` 格式上有很多不同。ARM 的 `CPU implementer` 是个十六进制 ID,需要映射到厂商名称;x86 直接用 `vendor_id` 字符串。特性标志的字段名也不一样,x86 用 `flags`,ARM 用 `Features`。 - -```cpp -std::string_view arm_implementer_to_vendor(uint32_t impl_val) { - switch (impl_val) { - case 0x41: return "ARM"; - case 0x42: return "Broadcom"; - case 0x43: return "Cavium"; - case 0x44: return "DEC"; - case 0x4E: return "NVIDIA"; - case 0x51: return "Qualcomm"; - case 0x69: return "Intel"; - default: return "Unknown"; - } -} -``` - -## 相关文档 - -- [Windows 平台实现](../windows/cpu_implementation.md) -- [proc_parser 工具](../../../base/linux/proc_parser/) -- [CPU 模块概述](../../../api/system/cpu/overview/) +--- +title: Linux 平台实现细节 +description: Linux 下的 CPU 信息查询主要通过解析 和 伪文件系统实现。这套方案的优势是不需要额外的 +--- + +# Linux 平台实现细节 + +Linux 下的 CPU 信息查询主要通过解析 `/proc` 和 `/sys` 伪文件系统实现。这套方案的优势是不需要额外的库依赖,缺点是文件格式比较繁琐——所以专门写了一套 `proc_parser` 工具来处理。 + +## 基础信息 + +CPU 型号、厂商这些静态信息从 `/proc/cpuinfo` 读取。这个文件的格式是固定的 `key: value` 结构,但不同架构下字段名不一样。x86 用 `vendor_id`,ARM 用 `CPU implementer`(是个十六进制 ID,需要转换)。 + +```cpp +cf::expected query_cpu_basic_info(cf::CPUInfoHost& hostInfo) { + std::ifstream cpuinfo("/proc/cpuinfo"); + std::string line; + + while (std::getline(cpuinfo, line)) { + std::string_view line_sv = line; + + // 解析 model name 字段 + const std::string_view model = cf::parse_cpuinfo_field(line_sv, "model name"); + if (!model.empty()) { + hostInfo.model = model; + } + + // ARM 架构需要特殊处理 + const std::string_view implementer = cf::parse_cpuinfo_field(line_sv, "CPU implementer"); + if (auto impl_val = cf::parse_hex_uint32(implementer)) { + hostInfo.manufest = cf::arm_implementer_to_vendor(*impl_val); + } + } + + // 架构信息用 uname() 获取更可靠 + struct utsname unameInfo; + if (uname(&unameInfo) == 0) { + hostInfo.arch = unameInfo.machine; + } + + return {}; +} +```text + +架构信息用 `uname()` 而不是读文件,是因为某些嵌入式板子的 `/proc/cpuinfo` 可能不包含完整的架构字段。 + +## 性能信息 + +核心数量和频率信息散落在不同地方:`/proc/cpuinfo` 有核心数,`/sys/devices/system/cpu/cpu0/cpufreq/` 里有频率。使用率需要读 `/proc/stat`,计算两次采样之间的时间差。 + +```cpp +// /proc/stat 第一行格式: +// cpu user nice system idle iowait ... +// 例如:cpu 2255 34 2290 22625563 6290 127 456 + +float calculate_cpu_usage() { + static uint64_t last_total = 0, last_idle = 0; + + uint64_t user, nice, system, idle; + FILE* stat = fopen("/proc/stat", "r"); + fscanf(stat, "cpu %lu %lu %lu %lu", &user, &nice, &system, &idle); + fclose(stat); + + uint64_t total = user + nice + system + idle; + uint64_t total_delta = total - last_total; + uint64_t idle_delta = idle - last_idle; + + last_total = total; + last_idle = idle; + + if (total_delta == 0) return 0.0f; + return 100.0f * (1.0f - static_cast(idle_delta) / total_delta); +} +```text + +⚠️ 首次调用会返回不准确的数据,因为需要上次采样的值作为基准。 + +## 扩展信息 + +CPU 特性标志在 `flags`(x86)或 `Features`(ARM)字段里,是个空格分隔的列表。缓存信息从 `/sys/devices/system/cpu/cpu0/cache/` 读取,每个缓存级别一个目录。 + +```cpp +// /sys/devices/system/cpu/cpu0/cache/ +// ├── index0/ -> L1 数据缓存 +// ├── index1/ -> L1 指令缓存 +// ├── index2/ -> L2 统一缓存 +// └── index3/ -> L3 统一缓存 + +// 读取缓存大小 +auto l1_size = cf::read_uint32_file("/sys/devices/system/cpu/cpu0/cache/index0/size"); +// 输出通常是 "32K",parse_cache_size() 会处理单位转换 +```text + +温度信息从 `/sys/class/thermal/thermal_zone*/temp` 读取,但不是所有设备都有温度传感器。返回值通常是 millidegree,需要除以 1000 转成摄氏度。 + +## big.LITTLE 检测 + +ARM 的大小核架构检测是通过比较不同 CPU 核心的最大频率来实现的。如果发现不同的频率值,就假定存在大小核,然后按频率分组统计核心数。 + +```cpp +bool detect_big_little(cf::CPUBonusInfoHost& host) { + std::vector max_frequencies; + + // 收集每个核心的最大频率 + for (int cpu = 0; cpu < logical_cores; ++cpu) { + std::string path = "/sys/devices/system/cpu/cpu" + std::to_string(cpu) + + "/cpufreq/cpuinfo_max_freq"; + auto freq = cf::read_uint32_file(path.c_str()); + if (freq.has_value()) { + max_frequencies.push_back(*freq); + } + } + + // 检查是否有不同的频率 + auto min_freq = *std::min_element(max_frequencies.begin(), max_frequencies.end()); + auto max_freq = *std::max_element(max_frequencies.begin(), max_frequencies.end()); + + if (min_freq != max_freq) { + host.has_big_little = true; + host.big_core_count = std::count(max_frequencies.begin(), max_frequencies.end(), max_freq); + host.little_core_count = std::count(max_frequencies.begin(), max_frequencies.end(), min_freq); + return true; + } + + return false; +} +```text + +这个方法不是百分之百可靠——有些同频 CPU 也可能被误判为大小核——但在我们支持的设备上效果还可以。 + +## 平台差异 + +ARM 和 x86 在 `cpuinfo` 格式上有很多不同。ARM 的 `CPU implementer` 是个十六进制 ID,需要映射到厂商名称;x86 直接用 `vendor_id` 字符串。特性标志的字段名也不一样,x86 用 `flags`,ARM 用 `Features`。 + +```cpp +std::string_view arm_implementer_to_vendor(uint32_t impl_val) { + switch (impl_val) { + case 0x41: return "ARM"; + case 0x42: return "Broadcom"; + case 0x43: return "Cavium"; + case 0x44: return "DEC"; + case 0x4E: return "NVIDIA"; + case 0x51: return "Qualcomm"; + case 0x69: return "Intel"; + default: return "Unknown"; + } +} +```text + +## 相关文档 + +- [Windows 平台实现](../windows/cpu_implementation.md) +- [proc_parser 工具](../../../base/linux/proc_parser/) +- [CPU 模块概述](../../../api/system/cpu/overview/) diff --git a/document/HandBook/implementation/linux/index.md b/document/HandBook/implementation/linux/index.md index 89c6764f0..e61803e02 100644 --- a/document/HandBook/implementation/linux/index.md +++ b/document/HandBook/implementation/linux/index.md @@ -1,10 +1,11 @@ -# linux - -> Welcome to the linux section. +--- +title: Linux 平台实现 +description: 本章节包含 CFDesktop 在 Linux 平台下的具体实现细节,涵盖硬件探测、系统信息采集以及 +--- -## Overview +# Linux 平台实现 -Documentation and resources for linux. +本章节包含 CFDesktop 在 Linux 平台下的具体实现细节,涵盖硬件探测、系统信息采集以及平台相关的底层工具函数。所有 Linux 特有功能均通过条件编译与跨平台接口隔离,确保上层模块的平台无关性。 --- diff --git a/document/HandBook/implementation/linux/memory/.pages b/document/HandBook/implementation/linux/memory/.pages deleted file mode 100644 index 6ef2dad8d..000000000 --- a/document/HandBook/implementation/linux/memory/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: Linux 内存实现 -nav: - - index.md - - 实现详情: memory_implementation.md diff --git a/document/HandBook/implementation/linux/memory/index.md b/document/HandBook/implementation/linux/memory/index.md index 6fde8055b..e0429301f 100644 --- a/document/HandBook/implementation/linux/memory/index.md +++ b/document/HandBook/implementation/linux/memory/index.md @@ -1,10 +1,11 @@ -# memory - -> Welcome to the memory section. +--- +title: Linux 内存检测 +description: 本章节详细描述 Linux 平台下的内存检测实现,包括物理内存总量、可用内存、虚拟内存以及进程级内存 +--- -## Overview +# Linux 内存检测 -Documentation and resources for memory. +本章节详细描述 Linux 平台下的内存检测实现,包括物理内存总量、可用内存、虚拟内存以及进程级内存使用情况的采集方式。实现基于 `/proc/meminfo` 等 Linux 特有的系统接口。 --- diff --git a/document/HandBook/implementation/linux/memory/memory_implementation.md b/document/HandBook/implementation/linux/memory/memory_implementation.md index 200473f90..58ca4c186 100644 --- a/document/HandBook/implementation/linux/memory/memory_implementation.md +++ b/document/HandBook/implementation/linux/memory/memory_implementation.md @@ -1,420 +1,425 @@ -# Linux 平台实现细节 - -Linux 下的内存信息查询主要通过解析 `/proc/meminfo` 和 `/proc/self/status` 实现。这套方案不需要额外的库依赖,但需要理解 Linux 的内存模型——特别是缓存内存这部分,和 Windows 的概念差异还挺大的。 - -## /proc/meminfo 解析 - -`/proc/meminfo` 是 Linux 内核导出的内存统计信息伪文件,格式是每行一个 `key: value kB` 的记录。解析它只需要逐行匹配字段名然后提取数值即可。 - -```bash -# /proc/meminfo 典型内容 -MemTotal: 16384000 kB -MemFree: 256000 kB -MemAvailable: 8192000 kB -Buffers: 128000 kB -Cached: 5120000 kB -SwapTotal: 8388608 kB -SwapFree: 8388608 kB -``` - -解析逻辑很简单:跳过字段名后的冒号和空格,用 `strtoul` 提取数字,然后乘以 1024 转换成字节。 - -```cpp -bool parseMemInfoLine(const char* line, const char* fieldName, uint64_t& outKb) { - size_t fieldNameLen = strlen(fieldName); - if (strncmp(line, fieldName, fieldNameLen) != 0) { - return false; - } - - // Skip to the number after the colon - const char* p = line + fieldNameLen; - while (*p == ':' || *p == ' ' || *p == '\t') { - p++; - } - - if (*p == '\0') { - return false; - } - - // Parse the number - char* end; - unsigned long value = strtoul(p, &end, 10); - if (end == p) { - return false; - } - - outKb = static_cast(value); - return true; -} -``` - -这个解析函数在所有基于 `/proc/meminfo` 的查询里都是共用的,避免了重复代码。只要找到需要的字段就返回,还能提前终止循环节省时间。 - -## 物理内存 - -物理内存查询读取 `MemTotal`、`MemAvailable` 和 `MemFree` 三个字段。`MemTotal` 是系统总物理内存,`MemFree` 是完全未使用的内存,`MemAvailable` 是内核认为可用于分配的总量(包含可回收的缓存)。 - -```cpp -void queryPhysicalMemory(PhysicalMemory& physical) { - FILE* fp = fopen("/proc/meminfo", "r"); - if (!fp) { - // Set to zero on failure - physical.total_bytes = 0; - physical.available_bytes = 0; - physical.free_bytes = 0; - return; - } - - uint64_t memTotal = 0, memAvailable = 0, memFree = 0; - - char line[256]; - while (fgets(line, sizeof(line), fp)) { - uint64_t value; - if (parseMemInfoLine(line, "MemTotal", value)) { - memTotal = value; - } else if (parseMemInfoLine(line, "MemAvailable", value)) { - memAvailable = value; - } else if (parseMemInfoLine(line, "MemFree", value)) { - memFree = value; - } - - // Break early if we have all values - if (memTotal > 0 && memAvailable > 0 && memFree > 0) { - break; - } - } - - fclose(fp); - - // Convert KB to bytes - physical.total_bytes = memTotal * 1024; - physical.available_bytes = memAvailable * 1024; - physical.free_bytes = memFree * 1024; -} -``` - -文件打开失败时所有字段归零,而不是抛异常——这在内存查询这种场景下是合理的,因为调用方通常更希望拿到"空数据"而不是崩溃。 - -## 缓存内存 - -Linux 的缓存内存是系统内存管理的重要组成部分,也是和 Windows 差异最大的地方。`CachedMemory` 结构包含四个字段:`Buffers`(块设备缓冲区)、`Cached`(页面缓存)、`Shmem`(共享内存/tmpfs)和 `Slab`(内核对象缓存)。 - -```cpp -void queryCachedMemory(CachedMemory& cached) { - FILE* fp = fopen("/proc/meminfo", "r"); - // ... - - uint64_t buffers = 0, cachedMem = 0, shmem = 0, slab = 0; - - char line[256]; - while (fgets(line, sizeof(line), fp)) { - uint64_t value; - if (parseMemInfoLine(line, "Buffers", value)) { - buffers = value; - } else if (parseMemInfoLine(line, "Cached", value)) { - cachedMem = value; - } else if (parseMemInfoLine(line, "Shmem", value)) { - shmem = value; - } else if (parseMemInfoLine(line, "Slab", value)) { - slab = value; - } - - if (buffers > 0 && cachedMem > 0 && shmem > 0 && slab > 0) { - break; - } - } - - // Convert KB to bytes - cached.buffers_bytes = buffers * 1024; - cached.cached_bytes = cachedMem * 1024; - cached.shared_bytes = shmem * 1024; - cached.slab_bytes = slab * 1024; -} -``` - -这些缓存内存都是可以被回收的,所以 `MemAvailable` 已经考虑了它们。如果你的程序只关心"还能分配多少内存",看 `MemAvailable` 就够了;如果需要分析内存占用细节(比如排查内存去哪了),这些字段会很有用。 - -注意 `Cached` 字段旁边还有一个 `SReclaimable`,它是 slab 可回收部分,和我们当前获取的 `Slab` 有重叠。目前实现里只取了主要的 `Cached` 值,简化了逻辑。 - -## 交换空间 - -交换空间查询读取 `SwapTotal` 和 `SwapFree`,逻辑和物理内存类似。 - -```cpp -void querySwapMemory(SwapMemory& swap) { - FILE* fp = fopen("/proc/meminfo", "r"); - // ... - - uint64_t swapTotal = 0, swapFree = 0; - - char line[256]; - while (fgets(line, sizeof(line), fp)) { - uint64_t value; - if (parseMemInfoLine(line, "SwapTotal", value)) { - swapTotal = value; - } else if (parseMemInfoLine(line, "SwapFree", value)) { - swapFree = value; - } - - if (swapTotal > 0 && swapFree > 0) { - break; - } - } - - swap.total_bytes = swapTotal * 1024; - swap.free_bytes = swapFree * 1024; -} -``` - -如果系统没有配置交换空间,`SwapTotal` 会是 0,这种情况在服务器上挺常见的。 - -## 进程内存 - -当前进程的内存信息从 `/proc/self/status` 读取,格式和 `/proc/meminfo` 类似。我们关注三个字段:`VmRSS`(驻留集大小,实际占用的物理内存)、`VmSize`(虚拟内存总大小)和 `VmPeak`(虚拟内存历史峰值)。 - -```cpp -void queryProcessMemory(ProcessMemory& process) { - FILE* fp = fopen("/proc/self/status", "r"); - // ... - - uint64_t vmRSS = 0, vmSize = 0, vmPeak = 0; - - char line[256]; - while (fgets(line, sizeof(line), fp)) { - uint64_t value; - if (parseStatusLine(line, "VmRSS", value)) { - vmRSS = value; - } else if (parseStatusLine(line, "VmSize", value)) { - vmSize = value; - } else if (parseStatusLine(line, "VmPeak", value)) { - vmPeak = value; - } - - if (vmRSS > 0 && vmSize > 0 && vmPeak > 0) { - break; - } - } - - // Convert KB to bytes - process.vm_rss_bytes = vmRSS * 1024; - process.vm_size_bytes = vmSize * 1024; - process.vm_peak_bytes = vmPeak * 1024; -} -``` - -`/proc/self` 是指向当前进程 `/proc/` 的符号链接,这样就不需要先获取自己的 PID 了。 - -`VmSize` 通常远大于 `VmRSS`,因为它包含了所有已分配的虚拟地址空间——包括尚未物理占用的部分(比如用 `mmap` 预留的、按需分配的)。关注进程实际内存占用时应该看 `VmRSS`。 - -## DIMM 信息 - -物理内存条的详细信息是最难获取的。完整的信息需要 `dmidecode` 命令,但它需要 root 权限。我们的实现会先尝试 `dmidecode`,失败后回退到 `/sys/class/dmi/id/`。 - -```cpp -void queryDimmInfo(std::vector& dimms) { - dimms.clear(); - - // Try dmidecode first (requires root) - if (!queryDimmViaDmidecode(dimms)) { - // Fall back to /sys/class/dmi/id/ - queryDimmViaSysFs(dimms); - } -} -``` - -### dmidecode 解析 - -`dmidecode -t memory` 输出的格式是文本块,每个 "Memory Device" 是一块内存。解析逻辑是状态机式的:扫描 "Memory Device" 标记开始新条目,然后解析缩进的字段,遇到空行或下一个设备时保存当前条目。 - -```cpp -bool parseDmidecode(const char* output, std::vector& dimms) { - const char* p = output; - DimmInfo currentDimm{}; - bool inDevice = false; - bool hasSize = false; - - while (*p != '\0') { - // Check for "Memory Device" marker - if (strncmp(p, "Memory Device", 13) == 0) { - // Save previous device if valid - if (inDevice && hasSize && currentDimm.capacity_bytes > 0) { - dimms.push_back(currentDimm); - } - - // Start new device - currentDimm = DimmInfo{}; - inDevice = true; - hasSize = false; - // ... - } - - if (inDevice) { - // Parse fields like "Size: 16384 MB" - auto extractField = [&p](const char* fieldName) -> char* { - // Implementation extracts value after "FieldName:" - // ... - }; - - std::unique_ptr field; - - if ((field.reset(extractField("Size")), field)) { - currentDimm.capacity_bytes = parseMemorySize(field.get()); - hasSize = true; - } else if ((field.reset(extractField("Type")), field)) { - currentDimm.type = parseMemoryType(field.get()); - } else if ((field.reset(extractField("Speed")), field)) { - currentDimm.frequency_mhz = parseFrequency(field.get()); - } - // ... 其他字段 - } - } - - // Don't forget last device - if (inDevice && hasSize && currentDimm.capacity_bytes > 0) { - dimms.push_back(currentDimm); - } - - return !dimms.empty(); -} -``` - -内存类型字符串的匹配是大小写不敏感的,支持 DDR2/3/4/5、LPDDR3/4/4X/5 以及 SDRAM。如果遇到未知类型会返回 `UNKNOWN`。 - -大小和频率的字符串格式比较统一——"16384 MB"、"3200 MT/s" 之类的——用 `sscanf` 配合单位判断就能解析。 - -```cpp -uint64_t parseMemorySize(const char* sizeStr) { - if (sizeStr == nullptr || strcmp(sizeStr, "No Module Installed") == 0) { - return 0; - } - - unsigned long value = 0; - char unit[16] = {0}; - - if (sscanf(sizeStr, "%lu %15s", &value, unit) == 2) { - if (strcasecmp(unit, "MB") == 0 || strcasecmp(unit, "M") == 0) { - return value * 1024 * 1024; - } else if (strcasecmp(unit, "GB") == 0 || strcasecmp(unit, "G") == 0) { - return value * 1024 * 1024 * 1024; - } - // ... - } - - return 0; -} -``` - -插槽编号从 `Locator` 字段提取,但这个字段格式不统一——可能是 "DIMM0"、"ChannelA-DIMM0"、"Slot 1" 之类的。我们用一个简单的启发式规则:找到最后一个数字,把它当作插槽号。这不是 100% 准确,但大多数情况下能工作。 - -### /sys 回退方案 - -`/sys/class/dmi/id/` 提供的是系统级别的信息,不是每条内存的详情。这里的回退实现只能拿到板卡厂商、序列号这种信息,无法获取单条内存的容量、类型、频率。 - -```cpp -bool queryDimmViaSysFs(std::vector& dimms) { - std::string manufacturer = readFile("/sys/class/dmi/id/board_vendor"); - std::string serial = readFile("/sys/class/dmi/id/board_serial"); - std::string product = readFile("/sys/class/dmi/id/board_name"); - - if (manufacturer.empty() && serial.empty() && product.empty()) { - return false; - } - - // Create a minimal DIMM entry - DimmInfo dimm{}; - dimm.manufacturer = manufacturer; - dimm.serial_number = serial; - dimm.part_number = product; - dimm.type = MemoryType::UNKNOWN; - dimm.capacity_bytes = 0; // Can't get individual DIMM size from /sys - dimm.slot = 0; - - dimms.push_back(dimm); - return true; -} -``` - -这个回退方案至少能保证不会返回空列表,但 `capacity_bytes` 会是 0,调用方需要处理这种情况。 - -## 实际文件示例 - -下面是一个典型的 `/proc/meminfo` 内容,可以看到我们解析的字段在其中: - -``` -MemTotal: 16384000 kB -MemFree: 256000 kB -MemAvailable: 8192000 kB -Buffers: 128000 kB -Cached: 5120000 kB -SwapCached: 0 kB -Active: 6144000 kB -Inactive: 3072000 kB -Active(anon): 2560000 kB -Inactive(anon): 512000 kB -Active(file): 3584000 kB -Inactive(file): 2560000 kB -Unevictable: 25600 kB -Mlocked: 0 kB -SwapTotal: 8388608 kB -SwapFree: 8388608 kB -Dirty: 64 kB -Writeback: 0 kB -AnonPages: 2560000 kB -Mapped: 256000 kB -Shmem: 128000 kB -Slab: 512000 kB -SReclaimable: 256000 kB -SUnreclaim: 256000 kB -KernelStack: 12800 kB -PageTables: 25600 kB -NFS_Unstable: 0 kB -Bounce: 0 kB -WritebackTmp: 0 kB -CommitLimit: 16588544 kB -Committed_AS: 6144000 kB -VmallocTotal: 34359738367 kB -VmallocUsed: 256000 kB -VmallocChunk: 34359478528 kB -HardwareCorrupted: 0 kB -AnonHugePages: 0 kB -ShmemHugePages: 0 kB -ShmemPmdMapped: 0 kB -HugePages_Total: 0 -HugePages_Free: 0 -HugePages_Rsvd: 0 -HugePages_Surp: 0 -DirectMap4k: 256000 kB -DirectMap2M: 5120000 kB -DirectMap1G: 10485760 kB -``` - -而 `/proc/self/status` 是这样: - -``` -Name: myprogram -State: S (sleeping) -Tgid: 12345 -Pid: 12345 -PPid: 10000 -TracerPid: 0 -Uid: 1000 1000 1000 1000 -Gid: 1000 1000 1000 1000 -VmSize: 123456 kB -VmRSS: 67890 kB -VmPeak: 234567 kB -... -``` - -## 注意事项 - -1. **权限问题**:`dmidecode` 需要权限,普通用户运行时 DIMM 信息会回退到 `/sys` 数据,`capacity_bytes` 会是 0。 - -2. **MemAvailable 兼容性**:`MemAvailable` 是较新的内核(3.14+)才有的字段。老内核上需要用 `MemFree + Buffers + Cached` 估算,但我们的实现目前直接读取这个字段——如果内核不支持,返回值会是 0。如果你的程序需要支持老内核,需要做兼容处理。 - -3. **文件打开失败**:所有实现在文件打开失败时会返回零值而不是报错,这是有意的设计。内存查询失败通常不是致命错误,返回"空数据"让调用方决定怎么处理更合适。 - -## 相关文档 - -- [Windows 平台实现](../../windows/memory/memory_implementation.md) -- [Memory 信息 API](../../../../api/system/memory/memory_info/) -- [proc_parser 工具](../../../../base/linux/proc_parser/) +--- +title: Linux 平台实现细节 +description: Linux 下的内存信息查询主要通过解析 和 实现。这套方案不需要额外的库依赖,但需要理解 Li +--- + +# Linux 平台实现细节 + +Linux 下的内存信息查询主要通过解析 `/proc/meminfo` 和 `/proc/self/status` 实现。这套方案不需要额外的库依赖,但需要理解 Linux 的内存模型——特别是缓存内存这部分,和 Windows 的概念差异还挺大的。 + +## /proc/meminfo 解析 + +`/proc/meminfo` 是 Linux 内核导出的内存统计信息伪文件,格式是每行一个 `key: value kB` 的记录。解析它只需要逐行匹配字段名然后提取数值即可。 + +```bash +# /proc/meminfo 典型内容 +MemTotal: 16384000 kB +MemFree: 256000 kB +MemAvailable: 8192000 kB +Buffers: 128000 kB +Cached: 5120000 kB +SwapTotal: 8388608 kB +SwapFree: 8388608 kB +```text + +解析逻辑很简单:跳过字段名后的冒号和空格,用 `strtoul` 提取数字,然后乘以 1024 转换成字节。 + +```cpp +bool parseMemInfoLine(const char* line, const char* fieldName, uint64_t& outKb) { + size_t fieldNameLen = strlen(fieldName); + if (strncmp(line, fieldName, fieldNameLen) != 0) { + return false; + } + + // Skip to the number after the colon + const char* p = line + fieldNameLen; + while (*p == ':' || *p == ' ' || *p == '\t') { + p++; + } + + if (*p == '\0') { + return false; + } + + // Parse the number + char* end; + unsigned long value = strtoul(p, &end, 10); + if (end == p) { + return false; + } + + outKb = static_cast(value); + return true; +} +```text + +这个解析函数在所有基于 `/proc/meminfo` 的查询里都是共用的,避免了重复代码。只要找到需要的字段就返回,还能提前终止循环节省时间。 + +## 物理内存 + +物理内存查询读取 `MemTotal`、`MemAvailable` 和 `MemFree` 三个字段。`MemTotal` 是系统总物理内存,`MemFree` 是完全未使用的内存,`MemAvailable` 是内核认为可用于分配的总量(包含可回收的缓存)。 + +```cpp +void queryPhysicalMemory(PhysicalMemory& physical) { + FILE* fp = fopen("/proc/meminfo", "r"); + if (!fp) { + // Set to zero on failure + physical.total_bytes = 0; + physical.available_bytes = 0; + physical.free_bytes = 0; + return; + } + + uint64_t memTotal = 0, memAvailable = 0, memFree = 0; + + char line[256]; + while (fgets(line, sizeof(line), fp)) { + uint64_t value; + if (parseMemInfoLine(line, "MemTotal", value)) { + memTotal = value; + } else if (parseMemInfoLine(line, "MemAvailable", value)) { + memAvailable = value; + } else if (parseMemInfoLine(line, "MemFree", value)) { + memFree = value; + } + + // Break early if we have all values + if (memTotal > 0 && memAvailable > 0 && memFree > 0) { + break; + } + } + + fclose(fp); + + // Convert KB to bytes + physical.total_bytes = memTotal * 1024; + physical.available_bytes = memAvailable * 1024; + physical.free_bytes = memFree * 1024; +} +```text + +文件打开失败时所有字段归零,而不是抛异常——这在内存查询这种场景下是合理的,因为调用方通常更希望拿到"空数据"而不是崩溃。 + +## 缓存内存 + +Linux 的缓存内存是系统内存管理的重要组成部分,也是和 Windows 差异最大的地方。`CachedMemory` 结构包含四个字段:`Buffers`(块设备缓冲区)、`Cached`(页面缓存)、`Shmem`(共享内存/tmpfs)和 `Slab`(内核对象缓存)。 + +```cpp +void queryCachedMemory(CachedMemory& cached) { + FILE* fp = fopen("/proc/meminfo", "r"); + // ... + + uint64_t buffers = 0, cachedMem = 0, shmem = 0, slab = 0; + + char line[256]; + while (fgets(line, sizeof(line), fp)) { + uint64_t value; + if (parseMemInfoLine(line, "Buffers", value)) { + buffers = value; + } else if (parseMemInfoLine(line, "Cached", value)) { + cachedMem = value; + } else if (parseMemInfoLine(line, "Shmem", value)) { + shmem = value; + } else if (parseMemInfoLine(line, "Slab", value)) { + slab = value; + } + + if (buffers > 0 && cachedMem > 0 && shmem > 0 && slab > 0) { + break; + } + } + + // Convert KB to bytes + cached.buffers_bytes = buffers * 1024; + cached.cached_bytes = cachedMem * 1024; + cached.shared_bytes = shmem * 1024; + cached.slab_bytes = slab * 1024; +} +```text + +这些缓存内存都是可以被回收的,所以 `MemAvailable` 已经考虑了它们。如果你的程序只关心"还能分配多少内存",看 `MemAvailable` 就够了;如果需要分析内存占用细节(比如排查内存去哪了),这些字段会很有用。 + +注意 `Cached` 字段旁边还有一个 `SReclaimable`,它是 slab 可回收部分,和我们当前获取的 `Slab` 有重叠。目前实现里只取了主要的 `Cached` 值,简化了逻辑。 + +## 交换空间 + +交换空间查询读取 `SwapTotal` 和 `SwapFree`,逻辑和物理内存类似。 + +```cpp +void querySwapMemory(SwapMemory& swap) { + FILE* fp = fopen("/proc/meminfo", "r"); + // ... + + uint64_t swapTotal = 0, swapFree = 0; + + char line[256]; + while (fgets(line, sizeof(line), fp)) { + uint64_t value; + if (parseMemInfoLine(line, "SwapTotal", value)) { + swapTotal = value; + } else if (parseMemInfoLine(line, "SwapFree", value)) { + swapFree = value; + } + + if (swapTotal > 0 && swapFree > 0) { + break; + } + } + + swap.total_bytes = swapTotal * 1024; + swap.free_bytes = swapFree * 1024; +} +```text + +如果系统没有配置交换空间,`SwapTotal` 会是 0,这种情况在服务器上挺常见的。 + +## 进程内存 + +当前进程的内存信息从 `/proc/self/status` 读取,格式和 `/proc/meminfo` 类似。我们关注三个字段:`VmRSS`(驻留集大小,实际占用的物理内存)、`VmSize`(虚拟内存总大小)和 `VmPeak`(虚拟内存历史峰值)。 + +```cpp +void queryProcessMemory(ProcessMemory& process) { + FILE* fp = fopen("/proc/self/status", "r"); + // ... + + uint64_t vmRSS = 0, vmSize = 0, vmPeak = 0; + + char line[256]; + while (fgets(line, sizeof(line), fp)) { + uint64_t value; + if (parseStatusLine(line, "VmRSS", value)) { + vmRSS = value; + } else if (parseStatusLine(line, "VmSize", value)) { + vmSize = value; + } else if (parseStatusLine(line, "VmPeak", value)) { + vmPeak = value; + } + + if (vmRSS > 0 && vmSize > 0 && vmPeak > 0) { + break; + } + } + + // Convert KB to bytes + process.vm_rss_bytes = vmRSS * 1024; + process.vm_size_bytes = vmSize * 1024; + process.vm_peak_bytes = vmPeak * 1024; +} +```text + +`/proc/self` 是指向当前进程 `/proc/` 的符号链接,这样就不需要先获取自己的 PID 了。 + +`VmSize` 通常远大于 `VmRSS`,因为它包含了所有已分配的虚拟地址空间——包括尚未物理占用的部分(比如用 `mmap` 预留的、按需分配的)。关注进程实际内存占用时应该看 `VmRSS`。 + +## DIMM 信息 + +物理内存条的详细信息是最难获取的。完整的信息需要 `dmidecode` 命令,但它需要 root 权限。我们的实现会先尝试 `dmidecode`,失败后回退到 `/sys/class/dmi/id/`。 + +```cpp +void queryDimmInfo(std::vector& dimms) { + dimms.clear(); + + // Try dmidecode first (requires root) + if (!queryDimmViaDmidecode(dimms)) { + // Fall back to /sys/class/dmi/id/ + queryDimmViaSysFs(dimms); + } +} +```text + +### dmidecode 解析 + +`dmidecode -t memory` 输出的格式是文本块,每个 "Memory Device" 是一块内存。解析逻辑是状态机式的:扫描 "Memory Device" 标记开始新条目,然后解析缩进的字段,遇到空行或下一个设备时保存当前条目。 + +```cpp +bool parseDmidecode(const char* output, std::vector& dimms) { + const char* p = output; + DimmInfo currentDimm{}; + bool inDevice = false; + bool hasSize = false; + + while (*p != '\0') { + // Check for "Memory Device" marker + if (strncmp(p, "Memory Device", 13) == 0) { + // Save previous device if valid + if (inDevice && hasSize && currentDimm.capacity_bytes > 0) { + dimms.push_back(currentDimm); + } + + // Start new device + currentDimm = DimmInfo{}; + inDevice = true; + hasSize = false; + // ... + } + + if (inDevice) { + // Parse fields like "Size: 16384 MB" + auto extractField = [&p](const char* fieldName) -> char* { + // Implementation extracts value after "FieldName:" + // ... + }; + + std::unique_ptr field; + + if ((field.reset(extractField("Size")), field)) { + currentDimm.capacity_bytes = parseMemorySize(field.get()); + hasSize = true; + } else if ((field.reset(extractField("Type")), field)) { + currentDimm.type = parseMemoryType(field.get()); + } else if ((field.reset(extractField("Speed")), field)) { + currentDimm.frequency_mhz = parseFrequency(field.get()); + } + // ... 其他字段 + } + } + + // Don't forget last device + if (inDevice && hasSize && currentDimm.capacity_bytes > 0) { + dimms.push_back(currentDimm); + } + + return !dimms.empty(); +} +```text + +内存类型字符串的匹配是大小写不敏感的,支持 DDR2/3/4/5、LPDDR3/4/4X/5 以及 SDRAM。如果遇到未知类型会返回 `UNKNOWN`。 + +大小和频率的字符串格式比较统一——"16384 MB"、"3200 MT/s" 之类的——用 `sscanf` 配合单位判断就能解析。 + +```cpp +uint64_t parseMemorySize(const char* sizeStr) { + if (sizeStr == nullptr || strcmp(sizeStr, "No Module Installed") == 0) { + return 0; + } + + unsigned long value = 0; + char unit[16] = {0}; + + if (sscanf(sizeStr, "%lu %15s", &value, unit) == 2) { + if (strcasecmp(unit, "MB") == 0 || strcasecmp(unit, "M") == 0) { + return value * 1024 * 1024; + } else if (strcasecmp(unit, "GB") == 0 || strcasecmp(unit, "G") == 0) { + return value * 1024 * 1024 * 1024; + } + // ... + } + + return 0; +} +```text + +插槽编号从 `Locator` 字段提取,但这个字段格式不统一——可能是 "DIMM0"、"ChannelA-DIMM0"、"Slot 1" 之类的。我们用一个简单的启发式规则:找到最后一个数字,把它当作插槽号。这不是 100% 准确,但大多数情况下能工作。 + +### /sys 回退方案 + +`/sys/class/dmi/id/` 提供的是系统级别的信息,不是每条内存的详情。这里的回退实现只能拿到板卡厂商、序列号这种信息,无法获取单条内存的容量、类型、频率。 + +```cpp +bool queryDimmViaSysFs(std::vector& dimms) { + std::string manufacturer = readFile("/sys/class/dmi/id/board_vendor"); + std::string serial = readFile("/sys/class/dmi/id/board_serial"); + std::string product = readFile("/sys/class/dmi/id/board_name"); + + if (manufacturer.empty() && serial.empty() && product.empty()) { + return false; + } + + // Create a minimal DIMM entry + DimmInfo dimm{}; + dimm.manufacturer = manufacturer; + dimm.serial_number = serial; + dimm.part_number = product; + dimm.type = MemoryType::UNKNOWN; + dimm.capacity_bytes = 0; // Can't get individual DIMM size from /sys + dimm.slot = 0; + + dimms.push_back(dimm); + return true; +} +```text + +这个回退方案至少能保证不会返回空列表,但 `capacity_bytes` 会是 0,调用方需要处理这种情况。 + +## 实际文件示例 + +下面是一个典型的 `/proc/meminfo` 内容,可以看到我们解析的字段在其中: + +```text +MemTotal: 16384000 kB +MemFree: 256000 kB +MemAvailable: 8192000 kB +Buffers: 128000 kB +Cached: 5120000 kB +SwapCached: 0 kB +Active: 6144000 kB +Inactive: 3072000 kB +Active(anon): 2560000 kB +Inactive(anon): 512000 kB +Active(file): 3584000 kB +Inactive(file): 2560000 kB +Unevictable: 25600 kB +Mlocked: 0 kB +SwapTotal: 8388608 kB +SwapFree: 8388608 kB +Dirty: 64 kB +Writeback: 0 kB +AnonPages: 2560000 kB +Mapped: 256000 kB +Shmem: 128000 kB +Slab: 512000 kB +SReclaimable: 256000 kB +SUnreclaim: 256000 kB +KernelStack: 12800 kB +PageTables: 25600 kB +NFS_Unstable: 0 kB +Bounce: 0 kB +WritebackTmp: 0 kB +CommitLimit: 16588544 kB +Committed_AS: 6144000 kB +VmallocTotal: 34359738367 kB +VmallocUsed: 256000 kB +VmallocChunk: 34359478528 kB +HardwareCorrupted: 0 kB +AnonHugePages: 0 kB +ShmemHugePages: 0 kB +ShmemPmdMapped: 0 kB +HugePages_Total: 0 +HugePages_Free: 0 +HugePages_Rsvd: 0 +HugePages_Surp: 0 +DirectMap4k: 256000 kB +DirectMap2M: 5120000 kB +DirectMap1G: 10485760 kB +```text + +而 `/proc/self/status` 是这样: + +```text +Name: myprogram +State: S (sleeping) +Tgid: 12345 +Pid: 12345 +PPid: 10000 +TracerPid: 0 +Uid: 1000 1000 1000 1000 +Gid: 1000 1000 1000 1000 +VmSize: 123456 kB +VmRSS: 67890 kB +VmPeak: 234567 kB +... +```text + +## 注意事项 + +1. **权限问题**:`dmidecode` 需要权限,普通用户运行时 DIMM 信息会回退到 `/sys` 数据,`capacity_bytes` 会是 0。 + +2. **MemAvailable 兼容性**:`MemAvailable` 是较新的内核(3.14+)才有的字段。老内核上需要用 `MemFree + Buffers + Cached` 估算,但我们的实现目前直接读取这个字段——如果内核不支持,返回值会是 0。如果你的程序需要支持老内核,需要做兼容处理。 + +3. **文件打开失败**:所有实现在文件打开失败时会返回零值而不是报错,这是有意的设计。内存查询失败通常不是致命错误,返回"空数据"让调用方决定怎么处理更合适。 + +## 相关文档 + +- [Windows 平台实现](../../windows/memory/memory_implementation.md) +- [Memory 信息 API](../../../../api/system/memory/memory_info/) +- [proc_parser 工具](../../../../base/linux/proc_parser/) diff --git a/document/HandBook/implementation/windows/.pages b/document/HandBook/implementation/windows/.pages deleted file mode 100644 index d11542e62..000000000 --- a/document/HandBook/implementation/windows/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: Windows 实现 -nav: - - CPU 实现: cpu_implementation.md - - 内存实现: memory diff --git a/document/HandBook/implementation/windows/cpu_implementation.md b/document/HandBook/implementation/windows/cpu_implementation.md index d6cdf337b..3bcfde769 100644 --- a/document/HandBook/implementation/windows/cpu_implementation.md +++ b/document/HandBook/implementation/windows/cpu_implementation.md @@ -1,181 +1,186 @@ -# Windows 平台实现细节 - -Windows 下的 CPU 信息主要通过 WMI(Windows Management Instrumentation)和 CPUID 指令获取。WMI 能拿到大部分基础信息,但查询开销较大且需要小心处理 COM 生命周期。CPU 指令则用于特性检测,这是跨平台的一致方案。 - -## COM 初始化 - -WMI 查询必须在 COM 环境中进行,我们用 `COMHelper` 封装了初始化和清理逻辑。选择 MTA(多线程公寓)而不是 STA,是因为 CPU 查询可能在不同线程执行。 - -```cpp -cf::expected query_cpu_basic_info(cf::CPUInfoHost& hostInfo) { - return cf::COMHelper::RunComInterfacesMTA( - [&hostInfo]() -> cf::expected { - // WMI 查询代码 - }); -} -``` - -这个封装确保 COM 正确初始化,线程退出时自动调用 `CoUninitialize()`,而且异常安全。 - -## 基础信息 - -CPU 型号、厂商、架构这些信息从 `Win32_Processor` 类查询。WQL 查询语法类似 SQL,但只能用于查询系统信息。 - -```cpp -cf::expected query_cpu_basic_info(cf::CPUInfoHost& hostInfo) { - IWbemLocator* pLoc = nullptr; - IWbemServices* pSvc = nullptr; - - // 1. 创建 WMI 定位器 - CoCreateInstance(CLSID_WbemLocator, nullptr, CLSCTX_INPROC_SERVER, - IID_IWbemLocator, (LPVOID*)&pLoc); - - // 2. 连接到 WMI 服务 - pLoc->ConnectServer(_bstr_t(L"ROOT\\CIMV2"), nullptr, nullptr, - nullptr, 0, nullptr, nullptr, &pSvc); - - // 3. 设置代理安全级别(必须,否则会访问被拒) - CoSetProxyBlanket(pSvc, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, - nullptr, RPC_C_AUTHN_LEVEL_CALL, - RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_NONE); - - // 4. 查询 CPU 信息 - auto modelName = queryWMIProperty(pSvc, L"Win32_Processor", L"Name"); - if (modelName) hostInfo.model = *modelName; - - auto manufacturer = queryWMIProperty(pSvc, L"Win32_Processor", L"Manufacturer"); - if (manufacturer) hostInfo.manufest = *manufacturer; - - // 清理 - pSvc->Release(); - pLoc->Release(); - - return {}; -} -``` - -⚠️ `CoSetProxyBlanket()` 调用是必须的,否则查询会返回 `E_ACCESSDENIED`。这个坑踩过一次。 - -## 架构值转换 - -WMI 返回的 `Architecture` 是个数字,需要映射到字符串。Windows 的架构值定义有点奇怪——9 是 x64,12 是 ARM64——但这些都是标准值,照着映射就行。 - -```cpp -std::string architectureToString(UINT16 archValue) { - switch (archValue) { - case 0: return "x86"; - case 1: return "MIPS"; - case 2: return "Alpha"; - case 3: return "PowerPC"; - case 5: return "ARM"; - case 6: return "ia64"; - case 9: return "x64"; - case 12: return "ARM64"; - default: return "Unknown"; - } -} -``` - -## 性能信息 - -核心数和最大频率可以从 `Win32_Processor` 直接拿到,但当前频率和 CPU 使用率需要其他方式。当前频率 WMI 也提供,但不太可靠;使用率则用 PDH(Performance Data Helper)API。 - -```cpp -float get_cpu_usage() { - PDH_HQUERY query; - PDH_HCOUNTER counter; - - PdhOpenQuery(nullptr, 0, &query); - PdhAddCounter(query, L"\\Processor(_Total)\\% Processor Time", 0, &counter); - - // 第一次调用初始化计数器 - PdhCollectQueryData(query); - Sleep(1000); // 必须等待,否则数据无效 - PdhCollectQueryData(query); - - PDH_FMT_COUNTERVALUE value; - PdhGetFormattedCounterValue(counter, PDH_FMT_DOUBLE, nullptr, &value); - - PdhCloseQuery(query); - return static_cast(value.doubleValue); -} -``` - -⚠️ 两次 `PdhCollectQueryData()` 之间必须有延迟,否则返回的数据是 0 或无效值。这是因为性能计数器是基于时间差计算的。 - -## 特性检测 - -CPU 特性通过 CPUID 指令检测,这是 x86 平台的标准方式。MSVC 用 `__cpuid()` intrinsic,GCC/Clang 用内联汇编。 - -```cpp -void cpuid(int info[4], int function_id) { - #ifdef _MSC_VER - __cpuid(info, function_id); - #else - __asm__ ( - "cpuid" - : "=a"(info[0]), "=b"(info[1]), "=c"(info[2]), "=d"(info[3]) - : "a"(function_id) - ); - #endif -} - -bool detect_feature(const char* feature_name) { - int info[4]; - cpuid(info, 1); // EAX=1 返回基本特性 - - if (strcmp(feature_name, "SSE") == 0) - return (info[3] & (1 << 25)) != 0; - if (strcmp(feature_name, "SSE2") == 0) - return (info[3] & (1 << 26)) != 0; - if (strcmp(feature_name, "AVX") == 0) - return (info[2] & (1 << 28)) != 0; - if (strcmp(feature_name, "AVX2") == 0) { - cpuid(info, 7); // EAX=7 返回扩展特性 - return (info[1] & (1 << 5)) != 0; - } - return false; -} -``` - -EAX=1 返回的是基本特性,EAX=7 返回的是扩展特性。不同特性位分布在不同的寄存器里,需要查 Intel 的手册确认。 - -## 温度信息 - -Windows 上温度信息比较麻烦。`MSAcpi_ThermalZoneTemperature` WMI 类理论上可以查,但大部分 PC 不支持。注册表 `HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\ThermalInfo` 也经常是空的。 - -```cpp -std::optional get_cpu_temperature() { - auto temp = queryWMIProperty(L"MSAcpi_ThermalZoneTemperature", L"CurrentTemperature"); - if (temp) { - // 返回值是摄氏度 * 10 - 273.15(开尔文转摄氏度) - return (std::stoull(*temp) / 10.0) - 273.15; - } - return std::nullopt; // 大多数情况不可用 -} -``` - -所以我们的实现里,温度信息在 Windows 上基本总是 `std::nullopt`。这不是 bug,是 Windows 硬件生态的限制。 - -## 资源管理 - -WMI 和 COM 的资源管理很繁琐,每个接口都要手动 `Release()`。用 `ScopeGuard` 可以确保即使中途出错也不会泄漏。 - -```cpp -IWbemLocator* pLoc = nullptr; -IWbemServices* pSvc = nullptr; -IWbemClassObject* pclsObj = nullptr; -VARIANT vtProp; - -cf::ScopeGuard locGuard([&pLoc]() { if (pLoc) pLoc->Release(); }); -cf::ScopeGuard svcGuard([&pSvc]() { if (pSvc) pSvc->Release(); }); -cf::ScopeGuard objGuard([&pclsObj]() { if (pclsObj) pclsObj->Release(); }); -cf::ScopeGuard varGuard([&vtProp]() { VariantClear(&vtProp); }); - -// ... 复杂的查询逻辑,无论哪里返回,资源都会被释放 -``` - -## 相关文档 - -- [Linux 平台实现](../linux/cpu_implementation.md) -- [CPU 模块概述](../../../api/system/cpu/overview/) +--- +title: Windows 平台实现细节 +description: Windows 下的 CPU 信息主要通过 WMI(Windows Management Instr +--- + +# Windows 平台实现细节 + +Windows 下的 CPU 信息主要通过 WMI(Windows Management Instrumentation)和 CPUID 指令获取。WMI 能拿到大部分基础信息,但查询开销较大且需要小心处理 COM 生命周期。CPU 指令则用于特性检测,这是跨平台的一致方案。 + +## COM 初始化 + +WMI 查询必须在 COM 环境中进行,我们用 `COMHelper` 封装了初始化和清理逻辑。选择 MTA(多线程公寓)而不是 STA,是因为 CPU 查询可能在不同线程执行。 + +```cpp +cf::expected query_cpu_basic_info(cf::CPUInfoHost& hostInfo) { + return cf::COMHelper::RunComInterfacesMTA( + [&hostInfo]() -> cf::expected { + // WMI 查询代码 + }); +} +```text + +这个封装确保 COM 正确初始化,线程退出时自动调用 `CoUninitialize()`,而且异常安全。 + +## 基础信息 + +CPU 型号、厂商、架构这些信息从 `Win32_Processor` 类查询。WQL 查询语法类似 SQL,但只能用于查询系统信息。 + +```cpp +cf::expected query_cpu_basic_info(cf::CPUInfoHost& hostInfo) { + IWbemLocator* pLoc = nullptr; + IWbemServices* pSvc = nullptr; + + // 1. 创建 WMI 定位器 + CoCreateInstance(CLSID_WbemLocator, nullptr, CLSCTX_INPROC_SERVER, + IID_IWbemLocator, (LPVOID*)&pLoc); + + // 2. 连接到 WMI 服务 + pLoc->ConnectServer(_bstr_t(L"ROOT\\CIMV2"), nullptr, nullptr, + nullptr, 0, nullptr, nullptr, &pSvc); + + // 3. 设置代理安全级别(必须,否则会访问被拒) + CoSetProxyBlanket(pSvc, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, + nullptr, RPC_C_AUTHN_LEVEL_CALL, + RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_NONE); + + // 4. 查询 CPU 信息 + auto modelName = queryWMIProperty(pSvc, L"Win32_Processor", L"Name"); + if (modelName) hostInfo.model = *modelName; + + auto manufacturer = queryWMIProperty(pSvc, L"Win32_Processor", L"Manufacturer"); + if (manufacturer) hostInfo.manufest = *manufacturer; + + // 清理 + pSvc->Release(); + pLoc->Release(); + + return {}; +} +```text + +⚠️ `CoSetProxyBlanket()` 调用是必须的,否则查询会返回 `E_ACCESSDENIED`。这个坑踩过一次。 + +## 架构值转换 + +WMI 返回的 `Architecture` 是个数字,需要映射到字符串。Windows 的架构值定义有点奇怪——9 是 x64,12 是 ARM64——但这些都是标准值,照着映射就行。 + +```cpp +std::string architectureToString(UINT16 archValue) { + switch (archValue) { + case 0: return "x86"; + case 1: return "MIPS"; + case 2: return "Alpha"; + case 3: return "PowerPC"; + case 5: return "ARM"; + case 6: return "ia64"; + case 9: return "x64"; + case 12: return "ARM64"; + default: return "Unknown"; + } +} +```text + +## 性能信息 + +核心数和最大频率可以从 `Win32_Processor` 直接拿到,但当前频率和 CPU 使用率需要其他方式。当前频率 WMI 也提供,但不太可靠;使用率则用 PDH(Performance Data Helper)API。 + +```cpp +float get_cpu_usage() { + PDH_HQUERY query; + PDH_HCOUNTER counter; + + PdhOpenQuery(nullptr, 0, &query); + PdhAddCounter(query, L"\\Processor(_Total)\\% Processor Time", 0, &counter); + + // 第一次调用初始化计数器 + PdhCollectQueryData(query); + Sleep(1000); // 必须等待,否则数据无效 + PdhCollectQueryData(query); + + PDH_FMT_COUNTERVALUE value; + PdhGetFormattedCounterValue(counter, PDH_FMT_DOUBLE, nullptr, &value); + + PdhCloseQuery(query); + return static_cast(value.doubleValue); +} +```text + +⚠️ 两次 `PdhCollectQueryData()` 之间必须有延迟,否则返回的数据是 0 或无效值。这是因为性能计数器是基于时间差计算的。 + +## 特性检测 + +CPU 特性通过 CPUID 指令检测,这是 x86 平台的标准方式。MSVC 用 `__cpuid()` intrinsic,GCC/Clang 用内联汇编。 + +```cpp +void cpuid(int info[4], int function_id) { + #ifdef _MSC_VER + __cpuid(info, function_id); + #else + __asm__ ( + "cpuid" + : "=a"(info[0]), "=b"(info[1]), "=c"(info[2]), "=d"(info[3]) + : "a"(function_id) + ); + #endif +} + +bool detect_feature(const char* feature_name) { + int info[4]; + cpuid(info, 1); // EAX=1 返回基本特性 + + if (strcmp(feature_name, "SSE") == 0) + return (info[3] & (1 << 25)) != 0; + if (strcmp(feature_name, "SSE2") == 0) + return (info[3] & (1 << 26)) != 0; + if (strcmp(feature_name, "AVX") == 0) + return (info[2] & (1 << 28)) != 0; + if (strcmp(feature_name, "AVX2") == 0) { + cpuid(info, 7); // EAX=7 返回扩展特性 + return (info[1] & (1 << 5)) != 0; + } + return false; +} +```text + +EAX=1 返回的是基本特性,EAX=7 返回的是扩展特性。不同特性位分布在不同的寄存器里,需要查 Intel 的手册确认。 + +## 温度信息 + +Windows 上温度信息比较麻烦。`MSAcpi_ThermalZoneTemperature` WMI 类理论上可以查,但大部分 PC 不支持。注册表 `HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\ThermalInfo` 也经常是空的。 + +```cpp +std::optional get_cpu_temperature() { + auto temp = queryWMIProperty(L"MSAcpi_ThermalZoneTemperature", L"CurrentTemperature"); + if (temp) { + // 返回值是摄氏度 * 10 - 273.15(开尔文转摄氏度) + return (std::stoull(*temp) / 10.0) - 273.15; + } + return std::nullopt; // 大多数情况不可用 +} +```text + +所以我们的实现里,温度信息在 Windows 上基本总是 `std::nullopt`。这不是 bug,是 Windows 硬件生态的限制。 + +## 资源管理 + +WMI 和 COM 的资源管理很繁琐,每个接口都要手动 `Release()`。用 `ScopeGuard` 可以确保即使中途出错也不会泄漏。 + +```cpp +IWbemLocator* pLoc = nullptr; +IWbemServices* pSvc = nullptr; +IWbemClassObject* pclsObj = nullptr; +VARIANT vtProp; + +cf::ScopeGuard locGuard([&pLoc]() { if (pLoc) pLoc->Release(); }); +cf::ScopeGuard svcGuard([&pSvc]() { if (pSvc) pSvc->Release(); }); +cf::ScopeGuard objGuard([&pclsObj]() { if (pclsObj) pclsObj->Release(); }); +cf::ScopeGuard varGuard([&vtProp]() { VariantClear(&vtProp); }); + +// ... 复杂的查询逻辑,无论哪里返回,资源都会被释放 +```text + +## 相关文档 + +- [Linux 平台实现](../linux/cpu_implementation.md) +- [CPU 模块概述](../../../api/system/cpu/overview/) diff --git a/document/HandBook/implementation/windows/index.md b/document/HandBook/implementation/windows/index.md index 1de415d6d..93e50bf62 100644 --- a/document/HandBook/implementation/windows/index.md +++ b/document/HandBook/implementation/windows/index.md @@ -1,10 +1,11 @@ -# windows - -> Welcome to the windows section. +--- +title: Windows 平台实现 +description: 本章节包含 CFDesktop 在 Windows 平台下的具体实现细节,涵盖通过 Windows +--- -## Overview +# Windows 平台实现 -Documentation and resources for windows. +本章节包含 CFDesktop 在 Windows 平台下的具体实现细节,涵盖通过 Windows API 进行硬件探测、内存检测以及系统工具函数。所有 Windows 特有功能均通过条件编译与跨平台接口隔离,确保上层模块的平台无关性。 --- diff --git a/document/HandBook/implementation/windows/memory/.pages b/document/HandBook/implementation/windows/memory/.pages deleted file mode 100644 index d9a488deb..000000000 --- a/document/HandBook/implementation/windows/memory/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: Windows 内存实现 -nav: - - index.md - - 实现详情: memory_implementation.md diff --git a/document/HandBook/implementation/windows/memory/index.md b/document/HandBook/implementation/windows/memory/index.md index 6fde8055b..be8bebdb4 100644 --- a/document/HandBook/implementation/windows/memory/index.md +++ b/document/HandBook/implementation/windows/memory/index.md @@ -1,10 +1,11 @@ -# memory - -> Welcome to the memory section. +--- +title: Windows 内存检测 +description: 本章节详细描述 Windows 平台下的内存检测实现,包括物理内存总量、可用内存、虚拟内存以及进程级 +--- -## Overview +# Windows 内存检测 -Documentation and resources for memory. +本章节详细描述 Windows 平台下的内存检测实现,包括物理内存总量、可用内存、虚拟内存以及进程级内存使用情况的采集方式。实现基于 `GlobalMemoryStatusEx` 等 Windows API 接口。 --- diff --git a/document/HandBook/implementation/windows/memory/memory_implementation.md b/document/HandBook/implementation/windows/memory/memory_implementation.md index b6ed52c1d..c668aadf4 100644 --- a/document/HandBook/implementation/windows/memory/memory_implementation.md +++ b/document/HandBook/implementation/windows/memory/memory_implementation.md @@ -1,286 +1,291 @@ -# Windows 平台内存模块实现 - -Windows 下的内存信息获取相对直接——大部分数据可以从单个 API 调用拿到。我们用 `GlobalMemoryStatusEx` 获取物理内存和交换空间,用 `GetProcessMemoryInfo` 获取进程内存,DIMM 信息则需要解析 SMBIOS 表。整体来说比 Linux 少踩坑,但也有一些平台特定的细节需要注意。 - -## 物理内存与交换空间 - -物理内存和交换空间都用 `GlobalMemoryStatusEx` 查询,这是 Windows 提供的一站式内存状态 API。只需要正确初始化结构体大小即可: - -```cpp -#include - -void queryPhysicalMemory(PhysicalMemory& physical) { - MEMORYSTATUSEX status; - status.dwLength = sizeof(status); // 必须设置,否则 API 会失败 - GlobalMemoryStatusEx(&status); - - physical.total_bytes = status.ullTotalPhys; - physical.available_bytes = status.ullAvailPhys; - physical.free_bytes = status.ullAvailPhys; // Windows: AvailPhys ~= Free -} -``` - -交换空间的获取方式类似: - -```cpp -void querySwapMemory(SwapMemory& swap) { - MEMORYSTATUSEX status; - status.dwLength = sizeof(status); - GlobalMemoryStatusEx(&status); - - swap.total_bytes = status.ullTotalPageFile; - swap.free_bytes = status.ullAvailPageFile; -} -``` - -⚠️ `dwLength` 字段必须设置为 `sizeof(MEMORYSTATUSEX)`,否则 `GlobalMemoryStatusEx` 会返回失败。这个设计很古老,但一直保留到现在。 - -Windows 上的 `ullAvailPhys` 和 Linux 的 `available` 概念接近,都是系统认为可用于分配的内存量。不过 Windows 没有类似 Linux 的页面缓存回收机制,所以 `AvailPhys` 基本等于真正空闲的物理内存,我们在实现里把 `free_bytes` 和 `available_bytes` 设成了相同值。 - -## 进程内存 - -进程内存使用 `GetProcessMemoryInfo` API,需要链接 `psapi.lib`。我们使用 `PROCESS_MEMORY_COUNTERS_EX` 结构来获取更详细的信息: - -```cpp -#include -#include - -#pragma comment(lib, "psapi.lib") - -void queryProcessMemory(ProcessMemory& process) { - PROCESS_MEMORY_COUNTERS_EX pmc; - pmc.cb = sizeof(pmc); - - if (GetProcessMemoryInfo(GetCurrentProcess(), - reinterpret_cast(&pmc), - sizeof(pmc))) { - process.vm_rss_bytes = pmc.WorkingSetSize; - process.vm_size_bytes = pmc.PagefileUsage; - process.vm_peak_bytes = pmc.PeakPagefileUsage; - } else { - // 查询失败时返回零值 - process.vm_rss_bytes = 0; - process.vm_size_bytes = 0; - process.vm_peak_bytes = 0; - } -} -``` - -字段映射关系: -| Windows 字段 | 我们的字段 | 含义 | -|-------------|-----------|------| -| `WorkingSetSize` | `vm_rss_bytes` | 驻留集大小,实际占用的物理内存 | -| `PagefileUsage` | `vm_size_bytes` | 页面文件使用量,类似虚拟内存大小 | -| `PeakPagefileUsage` | `vm_peak_bytes` | 页面文件使用的历史峰值 | - -⚠️ 需要注意的是 `WorkingSetSize` 包含了进程占用的所有物理内存,包括共享库和内存映射文件。如果只想看进程私有的内存,应该看 `PrivateUsage` 字段(需要 `PROCESS_MEMORY_COUNTERS_EX`)。 - -## DIMM 信息 - -内存条信息是整个模块里最复杂的部分。Windows 通过 SMBIOS(System Management BIOS)表暴露硬件信息,我们需要用 `GetSystemFirmwareTable` API 读取原始数据,然后手动解析。 - -### SMBIOS 数据获取 - -SMBIOS 签名是 `'RSMB'`(Raw SMBIOS),第一次调用获取缓冲区大小,第二次调用获取实际数据: - -```cpp -const DWORD signature = 'RSMB'; // 'RSMB' = Raw SMBIOS - -// 获取缓冲区大小 -DWORD bufferSize = GetSystemFirmwareTable(signature, 0, nullptr, 0); -if (bufferSize == 0) { - return; // SMBIOS 不可用 -} - -// 分配缓冲区并读取数据 -std::vector buffer(bufferSize); -DWORD result = GetSystemFirmwareTable(signature, 0, buffer.data(), bufferSize); -if (result != bufferSize) { - return; // 读取失败 -} - -const RawSMBIOSData* smbios = reinterpret_cast(buffer.data()); -parseSmbiosMemoryDevices(smbios->SMBIOSTableData, smbios->Length, dimms); -``` - -### SMBIOS 结构解析 - -SMBIOS 表由一系列结构组成,每个结构以类型号开头。内存设备是 Type 17,我们需要遍历整个表,找出所有 Type 17 的结构: - -```cpp -#pragma pack(push, 1) - -struct SMBIOSHeader { - uint8_t Type; - uint8_t Length; - uint16_t Handle; -}; - -struct MemoryDevice { - SMBIOSHeader Header; - uint16_t PhysMemArrayHandle; - uint16_t MemErrorInfoHandle; - uint16_t TotalWidth; - uint16_t DataWidth; - uint16_t Size; // 容量(MB),最高位表示扩展大小 - uint8_t FormFactor; - uint8_t DeviceSet; - uint8_t DeviceLocator; // 字符串索引 - uint8_t BankLocator; - uint8_t MemoryType; // 内存类型枚举值 - uint16_t TypeDetail; - uint16_t Speed; // 频率(MHz) - uint8_t Manufacturer; // 字符串索引 - uint8_t SerialNumber; // 字符串索引 - uint8_t AssetTag; - uint8_t PartNumber; // 字符串索引 - // SMBIOS 2.8+ 还有更多字段... -}; - -#pragma pack(pop) -``` - -SMBIOS 结构后面跟着一个字符串表,字符串索引从 1 开始。0 表示没有字符串。我们需要跳过格式化段(`Length` 字节),然后读取字符串直到双 null 字节: - -```cpp -const char* getSmbiosString(const uint8_t* structStart, uint8_t stringIndex, - const uint8_t* tableEnd) { - if (stringIndex == 0) - return ""; - - const uint8_t* fmtEnd = structStart + structStart[1]; // Length - const uint8_t* strStart = fmtEnd; - - // 跳到目标字符串(索引从 1 开始) - for (uint8_t i = 1; i < stringIndex; ++i) { - while (*strStart != 0 && strStart < tableEnd) { - ++strStart; - } - if (*strStart == 0) - ++strStart; // 跳过 null 终止符 - } - - if (strStart >= tableEnd) - return ""; - - return reinterpret_cast(strStart); -} -``` - -⚠️ SMBIOS 字符串索引是 1-based 的,这一点很容易忘。索引 0 表示没有字符串,不是第一个字符串。 - -### 内存类型映射 - -SMBIOS 定义的内存类型值很多,我们只映射常用的几种: - -```cpp -enum class SMBiosMemoryType : uint8_t { - Other = 0x01, - Unknown = 0x02, - // ... - DDR2 = 0x11, - DDR3 = 0x13, - DDR4 = 0x14, - LPDDR3 = 0x17, - LPDDR4 = 0x18, - LPDDR4_X = 0x19, - LPDDR5 = 0x1A, - DDR5 = 0x1B, - // ... -}; - -MemoryType smbiosToMemoryType(uint8_t smbType) { - switch (static_cast(smbType)) { - case SMBiosMemoryType::DDR2: return MemoryType::DDR2; - case SMBiosMemoryType::DDR3: return MemoryType::DDR3; - case SMBiosMemoryType::DDR4: return MemoryType::DDR4; - case SMBiosMemoryType::DDR5: return MemoryType::DDR5; - case SMBiosMemoryType::LPDDR3: return MemoryType::LPDDR3; - case SMBiosMemoryType::LPDDR4: return MemoryType::LPDDR4; - case SMBiosMemoryType::LPDDR4_X: return MemoryType::LPDDR4X; - case SMBiosMemoryType::LPDDR5: return MemoryType::LPDDR5; - case SMBiosMemoryType::SDRAM: return MemoryType::SDRAM; - default: return MemoryType::UNKNOWN; - } -} -``` - -### 容量解析 - -内存容量的解析有点坑。`Size` 字段如果是 0 表示插槽为空,如果最高位是 1(0x8000)表示使用扩展大小字段: - -```cpp -uint16_t sizeValue = memDev->Size; -if (sizeValue == 0 || (sizeValue & 0x8000) != 0) { - // 插槽为空或使用扩展大小 - if (sizeValue != 0 && hdr->Length >= 0x23) { - // 读取扩展大小(SMBIOS 3.2+) - uint32_t extSize = *reinterpret_cast(p + 0x1F); - if (extSize > 0) { - dimm.capacity_bytes = static_cast(extSize) * 1024 * 1024; - } - } -} else { - // 正常大小,单位是 MB - dimm.capacity_bytes = static_cast(sizeValue) * 1024 * 1024; -} -``` - -⚠️ 32GB 以上的内存条必须用扩展大小字段,因为 16 位的 `Size` 字段只能表示到 32767 MB(约 32GB)。 - -### 遍历 SMBIOS 表 - -完整的遍历逻辑如下: - -```cpp -void parseSmbiosMemoryDevices(const uint8_t* data, uint32_t length, - std::vector& dimms) { - const uint8_t* p = data; - const uint8_t* end = data + length; - - while (p + sizeof(SMBIOSHeader) <= end) { - const SMBIOSHeader* hdr = reinterpret_cast(p); - - // 结束标记:Type 127, Length 4 - if (hdr->Type == 127 && hdr->Length == 4) { - break; - } - - // 找到下一个结构(格式化段 + 字符串段) - const uint8_t* next = p + hdr->Length; - while (next + 1 < end && !(next[0] == 0 && next[1] == 0)) { - ++next; - } - next += 2; // 跳过双 null 终止符 - - // 处理 Type 17(Memory Device) - if (hdr->Type == 17 && hdr->Length >= 0x15) { - const MemoryDevice* memDev = reinterpret_cast(p); - - DimmInfo dimm{}; - dimm.type = smbiosToMemoryType(memDev->MemoryType); - - // 容量、频率、字符串等解析... - - dimms.push_back(dimm); - } - - p = next; - } -} -``` - -## 平台限制 - -1. **SMBIOS 可用性**:某些虚拟机或精简版 Windows 可能不提供 SMBIOS 数据,这种情况下 `dimms` 列表会为空。 - -2. **权限要求**:`GetSystemFirmwareTable` 通常不需要管理员权限,但某些 OEM 特定的 SMBIOS 扩展可能需要。 - -3. **32 位进程**:在 32 位进程上,`GetSystemFirmwareTable` 可能无法访问完整的 SMBIOS 表(取决于系统配置)。 - -4. **Slot/Channel 信息**:Windows SMBIOS 中的 `DeviceLocator` 和 `BankLocator` 字符串格式由 OEM 决定,没有统一标准。我们目前的实现只做简单计数,没有尝试解析字符串。如果需要准确的通道/插槽信息,需要针对不同 OEM 做字符串解析。 - -## 相关文档 - -- [Memory 信息 API](../../../../api/system/memory/memory_info/) -- [Linux 平台内存实现](../../linux/memory/memory_implementation.md) +--- +title: Windows 平台内存模块实现 +description: Windows 下的内存信息获取相对直接——大部分数据可以从单个 API 调用拿到。我们用 获取物 +--- + +# Windows 平台内存模块实现 + +Windows 下的内存信息获取相对直接——大部分数据可以从单个 API 调用拿到。我们用 `GlobalMemoryStatusEx` 获取物理内存和交换空间,用 `GetProcessMemoryInfo` 获取进程内存,DIMM 信息则需要解析 SMBIOS 表。整体来说比 Linux 少踩坑,但也有一些平台特定的细节需要注意。 + +## 物理内存与交换空间 + +物理内存和交换空间都用 `GlobalMemoryStatusEx` 查询,这是 Windows 提供的一站式内存状态 API。只需要正确初始化结构体大小即可: + +```cpp +#include + +void queryPhysicalMemory(PhysicalMemory& physical) { + MEMORYSTATUSEX status; + status.dwLength = sizeof(status); // 必须设置,否则 API 会失败 + GlobalMemoryStatusEx(&status); + + physical.total_bytes = status.ullTotalPhys; + physical.available_bytes = status.ullAvailPhys; + physical.free_bytes = status.ullAvailPhys; // Windows: AvailPhys ~= Free +} +```text + +交换空间的获取方式类似: + +```cpp +void querySwapMemory(SwapMemory& swap) { + MEMORYSTATUSEX status; + status.dwLength = sizeof(status); + GlobalMemoryStatusEx(&status); + + swap.total_bytes = status.ullTotalPageFile; + swap.free_bytes = status.ullAvailPageFile; +} +```text + +⚠️ `dwLength` 字段必须设置为 `sizeof(MEMORYSTATUSEX)`,否则 `GlobalMemoryStatusEx` 会返回失败。这个设计很古老,但一直保留到现在。 + +Windows 上的 `ullAvailPhys` 和 Linux 的 `available` 概念接近,都是系统认为可用于分配的内存量。不过 Windows 没有类似 Linux 的页面缓存回收机制,所以 `AvailPhys` 基本等于真正空闲的物理内存,我们在实现里把 `free_bytes` 和 `available_bytes` 设成了相同值。 + +## 进程内存 + +进程内存使用 `GetProcessMemoryInfo` API,需要链接 `psapi.lib`。我们使用 `PROCESS_MEMORY_COUNTERS_EX` 结构来获取更详细的信息: + +```cpp +#include +#include + +#pragma comment(lib, "psapi.lib") + +void queryProcessMemory(ProcessMemory& process) { + PROCESS_MEMORY_COUNTERS_EX pmc; + pmc.cb = sizeof(pmc); + + if (GetProcessMemoryInfo(GetCurrentProcess(), + reinterpret_cast(&pmc), + sizeof(pmc))) { + process.vm_rss_bytes = pmc.WorkingSetSize; + process.vm_size_bytes = pmc.PagefileUsage; + process.vm_peak_bytes = pmc.PeakPagefileUsage; + } else { + // 查询失败时返回零值 + process.vm_rss_bytes = 0; + process.vm_size_bytes = 0; + process.vm_peak_bytes = 0; + } +} +```bash + +字段映射关系: +| Windows 字段 | 我们的字段 | 含义 | +|-------------|-----------|------| +| `WorkingSetSize` | `vm_rss_bytes` | 驻留集大小,实际占用的物理内存 | +| `PagefileUsage` | `vm_size_bytes` | 页面文件使用量,类似虚拟内存大小 | +| `PeakPagefileUsage` | `vm_peak_bytes` | 页面文件使用的历史峰值 | + +⚠️ 需要注意的是 `WorkingSetSize` 包含了进程占用的所有物理内存,包括共享库和内存映射文件。如果只想看进程私有的内存,应该看 `PrivateUsage` 字段(需要 `PROCESS_MEMORY_COUNTERS_EX`)。 + +## DIMM 信息 + +内存条信息是整个模块里最复杂的部分。Windows 通过 SMBIOS(System Management BIOS)表暴露硬件信息,我们需要用 `GetSystemFirmwareTable` API 读取原始数据,然后手动解析。 + +### SMBIOS 数据获取 + +SMBIOS 签名是 `'RSMB'`(Raw SMBIOS),第一次调用获取缓冲区大小,第二次调用获取实际数据: + +```cpp +const DWORD signature = 'RSMB'; // 'RSMB' = Raw SMBIOS + +// 获取缓冲区大小 +DWORD bufferSize = GetSystemFirmwareTable(signature, 0, nullptr, 0); +if (bufferSize == 0) { + return; // SMBIOS 不可用 +} + +// 分配缓冲区并读取数据 +std::vector buffer(bufferSize); +DWORD result = GetSystemFirmwareTable(signature, 0, buffer.data(), bufferSize); +if (result != bufferSize) { + return; // 读取失败 +} + +const RawSMBIOSData* smbios = reinterpret_cast(buffer.data()); +parseSmbiosMemoryDevices(smbios->SMBIOSTableData, smbios->Length, dimms); +```text + +### SMBIOS 结构解析 + +SMBIOS 表由一系列结构组成,每个结构以类型号开头。内存设备是 Type 17,我们需要遍历整个表,找出所有 Type 17 的结构: + +```cpp +#pragma pack(push, 1) + +struct SMBIOSHeader { + uint8_t Type; + uint8_t Length; + uint16_t Handle; +}; + +struct MemoryDevice { + SMBIOSHeader Header; + uint16_t PhysMemArrayHandle; + uint16_t MemErrorInfoHandle; + uint16_t TotalWidth; + uint16_t DataWidth; + uint16_t Size; // 容量(MB),最高位表示扩展大小 + uint8_t FormFactor; + uint8_t DeviceSet; + uint8_t DeviceLocator; // 字符串索引 + uint8_t BankLocator; + uint8_t MemoryType; // 内存类型枚举值 + uint16_t TypeDetail; + uint16_t Speed; // 频率(MHz) + uint8_t Manufacturer; // 字符串索引 + uint8_t SerialNumber; // 字符串索引 + uint8_t AssetTag; + uint8_t PartNumber; // 字符串索引 + // SMBIOS 2.8+ 还有更多字段... +}; + +#pragma pack(pop) +```text + +SMBIOS 结构后面跟着一个字符串表,字符串索引从 1 开始。0 表示没有字符串。我们需要跳过格式化段(`Length` 字节),然后读取字符串直到双 null 字节: + +```cpp +const char* getSmbiosString(const uint8_t* structStart, uint8_t stringIndex, + const uint8_t* tableEnd) { + if (stringIndex == 0) + return ""; + + const uint8_t* fmtEnd = structStart + structStart[1]; // Length + const uint8_t* strStart = fmtEnd; + + // 跳到目标字符串(索引从 1 开始) + for (uint8_t i = 1; i < stringIndex; ++i) { + while (*strStart != 0 && strStart < tableEnd) { + ++strStart; + } + if (*strStart == 0) + ++strStart; // 跳过 null 终止符 + } + + if (strStart >= tableEnd) + return ""; + + return reinterpret_cast(strStart); +} +```text + +⚠️ SMBIOS 字符串索引是 1-based 的,这一点很容易忘。索引 0 表示没有字符串,不是第一个字符串。 + +### 内存类型映射 + +SMBIOS 定义的内存类型值很多,我们只映射常用的几种: + +```cpp +enum class SMBiosMemoryType : uint8_t { + Other = 0x01, + Unknown = 0x02, + // ... + DDR2 = 0x11, + DDR3 = 0x13, + DDR4 = 0x14, + LPDDR3 = 0x17, + LPDDR4 = 0x18, + LPDDR4_X = 0x19, + LPDDR5 = 0x1A, + DDR5 = 0x1B, + // ... +}; + +MemoryType smbiosToMemoryType(uint8_t smbType) { + switch (static_cast(smbType)) { + case SMBiosMemoryType::DDR2: return MemoryType::DDR2; + case SMBiosMemoryType::DDR3: return MemoryType::DDR3; + case SMBiosMemoryType::DDR4: return MemoryType::DDR4; + case SMBiosMemoryType::DDR5: return MemoryType::DDR5; + case SMBiosMemoryType::LPDDR3: return MemoryType::LPDDR3; + case SMBiosMemoryType::LPDDR4: return MemoryType::LPDDR4; + case SMBiosMemoryType::LPDDR4_X: return MemoryType::LPDDR4X; + case SMBiosMemoryType::LPDDR5: return MemoryType::LPDDR5; + case SMBiosMemoryType::SDRAM: return MemoryType::SDRAM; + default: return MemoryType::UNKNOWN; + } +} +```text + +### 容量解析 + +内存容量的解析有点坑。`Size` 字段如果是 0 表示插槽为空,如果最高位是 1(0x8000)表示使用扩展大小字段: + +```cpp +uint16_t sizeValue = memDev->Size; +if (sizeValue == 0 || (sizeValue & 0x8000) != 0) { + // 插槽为空或使用扩展大小 + if (sizeValue != 0 && hdr->Length >= 0x23) { + // 读取扩展大小(SMBIOS 3.2+) + uint32_t extSize = *reinterpret_cast(p + 0x1F); + if (extSize > 0) { + dimm.capacity_bytes = static_cast(extSize) * 1024 * 1024; + } + } +} else { + // 正常大小,单位是 MB + dimm.capacity_bytes = static_cast(sizeValue) * 1024 * 1024; +} +```text + +⚠️ 32GB 以上的内存条必须用扩展大小字段,因为 16 位的 `Size` 字段只能表示到 32767 MB(约 32GB)。 + +### 遍历 SMBIOS 表 + +完整的遍历逻辑如下: + +```cpp +void parseSmbiosMemoryDevices(const uint8_t* data, uint32_t length, + std::vector& dimms) { + const uint8_t* p = data; + const uint8_t* end = data + length; + + while (p + sizeof(SMBIOSHeader) <= end) { + const SMBIOSHeader* hdr = reinterpret_cast(p); + + // 结束标记:Type 127, Length 4 + if (hdr->Type == 127 && hdr->Length == 4) { + break; + } + + // 找到下一个结构(格式化段 + 字符串段) + const uint8_t* next = p + hdr->Length; + while (next + 1 < end && !(next[0] == 0 && next[1] == 0)) { + ++next; + } + next += 2; // 跳过双 null 终止符 + + // 处理 Type 17(Memory Device) + if (hdr->Type == 17 && hdr->Length >= 0x15) { + const MemoryDevice* memDev = reinterpret_cast(p); + + DimmInfo dimm{}; + dimm.type = smbiosToMemoryType(memDev->MemoryType); + + // 容量、频率、字符串等解析... + + dimms.push_back(dimm); + } + + p = next; + } +} +```text + +## 平台限制 + +1. **SMBIOS 可用性**:某些虚拟机或精简版 Windows 可能不提供 SMBIOS 数据,这种情况下 `dimms` 列表会为空。 + +2. **权限要求**:`GetSystemFirmwareTable` 通常不需要管理员权限,但某些 OEM 特定的 SMBIOS 扩展可能需要。 + +3. **32 位进程**:在 32 位进程上,`GetSystemFirmwareTable` 可能无法访问完整的 SMBIOS 表(取决于系统配置)。 + +4. **Slot/Channel 信息**:Windows SMBIOS 中的 `DeviceLocator` 和 `BankLocator` 字符串格式由 OEM 决定,没有统一标准。我们目前的实现只做简单计数,没有尝试解析字符串。如果需要准确的通道/插槽信息,需要针对不同 OEM 做字符串解析。 + +## 相关文档 + +- [Memory 信息 API](../../../../api/system/memory/memory_info/) +- [Linux 平台内存实现](../../linux/memory/memory_implementation.md) diff --git a/document/HandBook/index.md b/document/HandBook/index.md index de98ec53f..a073d9e49 100644 --- a/document/HandBook/index.md +++ b/document/HandBook/index.md @@ -1,3 +1,8 @@ +--- +title: 开发手册 +description: CFDesktop 开发手册涵盖所有模块的 API 文档、架构设计、使用指南和平台实现细节。 +--- + # 开发手册 CFDesktop 开发手册涵盖所有模块的 API 文档、架构设计、使用指南和平台实现细节。 diff --git a/document/HandBook/ui/.pages b/document/HandBook/ui/.pages deleted file mode 100644 index 5e4f83f26..000000000 --- a/document/HandBook/ui/.pages +++ /dev/null @@ -1,10 +0,0 @@ -title: UI 框架 -icon: material/palette -nav: - - architecture - - 基础类型: base - - 核心模块: core - - 组件系统: components - - Material 设计: material - - 应用层: application - - 如何开发 Widget: how-to-develop-widget.md diff --git a/document/HandBook/ui/application/.pages b/document/HandBook/ui/application/.pages deleted file mode 100644 index 82fbd1a25..000000000 --- a/document/HandBook/ui/application/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: 应用层 -nav: - - 应用: application.md - - Material 应用: material_application.md diff --git a/document/HandBook/ui/application/application.md b/document/HandBook/ui/application/application.md index cf03e3986..0db2bd812 100644 --- a/document/HandBook/ui/application/application.md +++ b/document/HandBook/ui/application/application.md @@ -1,109 +1,114 @@ -# Application - 应用基础类 - -`Application` 是 Qt 的 `QApplication` 替代品,在标准应用框架之上集成了主题管理和动画工厂。我们用它替换掉裸的 `QApplication`,这样在任何地方都能通过静态方法访问当前主题和动画资源,不需要到处传递单例指针。 - -## 初始化流程 - -`Application` 的构造函数只做最基础的设置,真正的初始化在 `init()` 方法里完成。这是有意为之的——派生类需要先注册自己的主题,然后才能调用基类的 `init()`,否则动画工厂创建时会找不到对应的主题。 - -```cpp -#include "ui/widget/application_support/application.h" - -using namespace cf::ui::widget::application_support; - -int main(int argc, char* argv[]) { - Application app(argc, argv); - - // 如果是派生类,在 init() 里注册主题 - app.setTheme("theme.material.light"); - - MainWindow w; - w.show(); - - return app.exec(); -} -``` - -注意:如果你直接用 `Application` 而不是它的派生类,需要手动注册主题并调用 `init()`,否则 `currentTheme()` 会抛异常。实际上更推荐用 `MaterialApplication`,下面会说。 - -## 主题系统集成 - -`Application` 内部持有一个指向 `ThemeManager` 单例的连接。当主题切换时,`ThemeManager` 会发出信号,`Application` 收到后会重建整个动画工厂——因为工厂内部持有对主题的引用,主题换了就必须重建。 - -```cpp -// 设置主题 -app.setTheme("theme.material.dark"); - -// 主题切换会触发这个信号 -connect(&app, &Application::themeChanged, [](const core::ICFTheme& newTheme) { - // 响应主题切换,更新 UI -}); -``` - -这里有个细节:动画工厂的重建会保留之前的启用状态。比如你禁用了动画,切换主题后动画仍然是禁用状态,不会突然恢复。 - -## 动画访问 - -通过 token 获取动画是最常见的用法: - -```cpp -// 从任何地方获取动画 -auto fadeIn = Application::animation("md.animation.fadeIn"); -if (fadeIn) { - fadeIn->setTargetWidget(myWidget); - fadeIn->start(); -} - -// 全局禁用动画(比如在批量操作时) -Application::setAnimationsEnabled(false); -``` - -返回的是 `WeakPtr`,因为动画工厂归 `Application` 所有,外部只持有弱引用。如果 `Application` 被销毁或者主题切换导致工厂重建,原来的 `WeakPtr` 就会失效——所以拿到指针后要尽快用,不要长期持有。 - -⚠️ `setAnimationsEnabled(false)` 只影响新创建的动画,已经正在运行的动画不会受影响。如果你需要立即停止所有动画,需要自己遍历并停止它们。 - -## 自定义动画工厂 - -如果你的应用不用 Material Design,或者想用自己的动画实现,可以注册一个自定义工厂: - -```cpp -// 在 main() 之前,或者在派生类的 init() 里 -Application::registerAnimationFactoryType("theme.fluent", - [](const core::ICFTheme& theme, QObject* parent) { - return std::make_unique(theme, parent); - }); - -// 现在切换到 "theme.fluent.*" 主题时会用你的工厂 -app.setTheme("theme.fluent.dark"); -``` - -匹配逻辑是按前缀最长匹配:`theme.fluent.dark` 会匹配 `theme.fluent`,而不是 `theme`。这样你可以同时注册多个工厂,每个负责一个主题家族。 - -## 派生类注意事项 - -如果你需要继承 `Application`,要记住 `init()` 的调用顺序: - -```cpp -class MyApplication : public Application { -protected: - void init() override { - // 1. 先注册动画工厂(如果需要) - registerAnimationFactoryType("theme.myapp", ...); - - // 2. 再注册主题 - themeManager()->insert_one("theme.myapp.light", ...); - themeManager()->setThemeTo("theme.myapp.light", false); - - // 3. 最后调用基类 init - Application::init(); - } -}; -``` - -如果顺序搞反了,基类的 `init()` 会尝试创建动画工厂,但此时主题还没注册,工厂创建会失败。 - -## 相关文档 - -- [MaterialApplication - Material 应用](./material_application.md) -- [ThemeManager - 主题管理](../core/theme_manager.md) -- [动画系统概述](../components/animation.md) +--- +title: "Application - 应用基础类" +description: 是 Qt 的 替代品,在标准应用框架之上集成了主题管理和动画工厂。我们用它替换掉裸的 ,这样在任何 +--- + +# Application - 应用基础类 + +`Application` 是 Qt 的 `QApplication` 替代品,在标准应用框架之上集成了主题管理和动画工厂。我们用它替换掉裸的 `QApplication`,这样在任何地方都能通过静态方法访问当前主题和动画资源,不需要到处传递单例指针。 + +## 初始化流程 + +`Application` 的构造函数只做最基础的设置,真正的初始化在 `init()` 方法里完成。这是有意为之的——派生类需要先注册自己的主题,然后才能调用基类的 `init()`,否则动画工厂创建时会找不到对应的主题。 + +```cpp +#include "ui/widget/application_support/application.h" + +using namespace cf::ui::widget::application_support; + +int main(int argc, char* argv[]) { + Application app(argc, argv); + + // 如果是派生类,在 init() 里注册主题 + app.setTheme("theme.material.light"); + + MainWindow w; + w.show(); + + return app.exec(); +} +```text + +注意:如果你直接用 `Application` 而不是它的派生类,需要手动注册主题并调用 `init()`,否则 `currentTheme()` 会抛异常。实际上更推荐用 `MaterialApplication`,下面会说。 + +## 主题系统集成 + +`Application` 内部持有一个指向 `ThemeManager` 单例的连接。当主题切换时,`ThemeManager` 会发出信号,`Application` 收到后会重建整个动画工厂——因为工厂内部持有对主题的引用,主题换了就必须重建。 + +```cpp +// 设置主题 +app.setTheme("theme.material.dark"); + +// 主题切换会触发这个信号 +connect(&app, &Application::themeChanged, [](const core::ICFTheme& newTheme) { + // 响应主题切换,更新 UI +}); +```text + +这里有个细节:动画工厂的重建会保留之前的启用状态。比如你禁用了动画,切换主题后动画仍然是禁用状态,不会突然恢复。 + +## 动画访问 + +通过 token 获取动画是最常见的用法: + +```cpp +// 从任何地方获取动画 +auto fadeIn = Application::animation("md.animation.fadeIn"); +if (fadeIn) { + fadeIn->setTargetWidget(myWidget); + fadeIn->start(); +} + +// 全局禁用动画(比如在批量操作时) +Application::setAnimationsEnabled(false); +```text + +返回的是 `WeakPtr`,因为动画工厂归 `Application` 所有,外部只持有弱引用。如果 `Application` 被销毁或者主题切换导致工厂重建,原来的 `WeakPtr` 就会失效——所以拿到指针后要尽快用,不要长期持有。 + +⚠️ `setAnimationsEnabled(false)` 只影响新创建的动画,已经正在运行的动画不会受影响。如果你需要立即停止所有动画,需要自己遍历并停止它们。 + +## 自定义动画工厂 + +如果你的应用不用 Material Design,或者想用自己的动画实现,可以注册一个自定义工厂: + +```cpp +// 在 main() 之前,或者在派生类的 init() 里 +Application::registerAnimationFactoryType("theme.fluent", + [](const core::ICFTheme& theme, QObject* parent) { + return std::make_unique(theme, parent); + }); + +// 现在切换到 "theme.fluent.*" 主题时会用你的工厂 +app.setTheme("theme.fluent.dark"); +```text + +匹配逻辑是按前缀最长匹配:`theme.fluent.dark` 会匹配 `theme.fluent`,而不是 `theme`。这样你可以同时注册多个工厂,每个负责一个主题家族。 + +## 派生类注意事项 + +如果你需要继承 `Application`,要记住 `init()` 的调用顺序: + +```cpp +class MyApplication : public Application { +protected: + void init() override { + // 1. 先注册动画工厂(如果需要) + registerAnimationFactoryType("theme.myapp", ...); + + // 2. 再注册主题 + themeManager()->insert_one("theme.myapp.light", ...); + themeManager()->setThemeTo("theme.myapp.light", false); + + // 3. 最后调用基类 init + Application::init(); + } +}; +```text + +如果顺序搞反了,基类的 `init()` 会尝试创建动画工厂,但此时主题还没注册,工厂创建会失败。 + +## 相关文档 + +- [MaterialApplication - Material 应用](./material_application.md) +- [ThemeManager - 主题管理](../core/theme_manager.md) +- [动画系统概述](../components/animation.md) diff --git a/document/HandBook/ui/application/index.md b/document/HandBook/ui/application/index.md index 84d93d4aa..037e9f607 100644 --- a/document/HandBook/ui/application/index.md +++ b/document/HandBook/ui/application/index.md @@ -1,10 +1,11 @@ -# application - -> Welcome to the application section. +--- +title: 应用框架 +description: 本章节介绍应用级别的 UI 工具与窗口管理功能,包括主窗口()接口、显示服务后端()以及跨平台窗口管 +--- -## Overview +# 应用框架 -Documentation and resources for application. +本章节介绍应用级别的 UI 工具与窗口管理功能,包括主窗口(`IWindow`)接口、显示服务后端(`IDisplayServerBackend`)以及跨平台窗口管理的适配层。应用框架为桌面环境的完整运行提供顶层支撑。 --- diff --git a/document/HandBook/ui/application/material_application.md b/document/HandBook/ui/application/material_application.md index 86bef788e..380888a6c 100644 --- a/document/HandBook/ui/application/material_application.md +++ b/document/HandBook/ui/application/material_application.md @@ -1,107 +1,112 @@ -# MaterialApplication - Material 应用 - -`MaterialApplication` 是 `Application` 的 Material Design 3 专用派生类,自动注册亮色和暗色主题,并把 Material 动画工厂绑定上去。如果你的应用用 Material Design 3,直接用它就行,不需要手动注册主题。 - -## 初始化 - -用法很简单,替换掉 `QApplication` 就行: - -```cpp -#include "ui/widget/material/application/material_application.h" - -using namespace cf::ui::widget::material; - -int main(int argc, char* argv[]) { - MaterialApplication app(argc, argv); - - // 默认已经是 Material 亮色主题 - MainWindow w; - w.show(); - - return app.exec(); -} -``` - -构造函数里会自动调用 `init()`,`init()` 按下面的顺序执行: - -1. 注册 Material 动画工厂(前缀 `theme.material`) -2. 注册 Material 亮色和暗色主题到 `ThemeManager` -3. 设置默认主题为 `theme.material.light` -4. 调用基类的 `init()` 创建动画工厂 - -## 主题切换 - -Material 主题已经预注册好了,直接切换就行: - -```cpp -// 切换到暗色主题 -app.setTheme("theme.material.dark"); - -// 或者用 token 字面量(推荐) -using namespace cf::ui::core::token::literals; -app.setTheme(MATERIAL_THEME_DARK); -``` - -主题切换会触发 `themeChanged` 信号,动画工厂会同步重建。 - -## 动画工厂绑定 - -Material 动画工厂是和主题绑定的——当主题切换到任何 `theme.material.*` 时,都会用 `CFMaterialAnimationFactory` 创建动画。工厂的前缀匹配保证了这点: - -```cpp -// 在 MaterialApplication::init() 里已经注册了 -Application::registerAnimationFactoryType("theme.material", - [](const core::ICFTheme& theme, QObject* parent) { - return std::make_unique(theme, nullptr, parent); - }); -``` - -所以你不需要自己管工厂创建,只要确保主题 token 以 `theme.material.` 开头就行。 - -## 默认主题 - -`MaterialApplication::DEFAULT_THEME` 定义为 `MATERIAL_THEME_LIGHT`(即 `"theme.material.light"`)。如果你想在构造后立即切换主题,可以直接用: - -```cpp -MaterialApplication app(argc, argv); - -// 如果想要暗色主题作为默认 -app.setTheme(MATERIAL_THEME_DARK); -``` - -或者自己派生一个类,在 `init()` 里改默认主题: - -```cpp -class DarkMaterialApplication : public MaterialApplication { -protected: - void init() override { - MaterialApplication::init(); // 先让基类注册完主题 - setTheme(MATERIAL_THEME_DARK); // 然后改默认 - } -}; -``` - -⚠️ 不要在 `MaterialApplication::init()` 里改默认主题再调用基类 `init()`——因为基类的 `init()` 会创建动画工厂,此时默认主题还是亮色的。要么在构造后改,要么在派生类的 `init()` 里先调基类 `init()` 再改。 - -## 和裸 Application 的区别 - -直接用 `Application` 需要自己注册主题和动画工厂: - -```cpp -// 用裸 Application 需要这些额外步骤 -Application app(argc, argv); - -Application::registerAnimationFactoryType("theme.material", ...); -themeManager()->insert_one("theme.material.light", ...); -themeManager()->setThemeTo("theme.material.light", false); - -app.init(); // 别忘了手动调用 init() -``` - -而 `MaterialApplication` 把这些都包圆了,构造完就能用。如果你用 Material Design 3,没理由不用它。 - -## 相关文档 - -- [Application - 应用基础类](./application.md) -- [Material 主题系统](../material/cfmaterial_theme.md) -- [Material 动画](../material/animation/index.md) +--- +title: "MaterialApplication - Material 应用" +description: 是 的 Material Design 3 专用派生类,自动注册亮色和暗色主题,并把 Materi +--- + +# MaterialApplication - Material 应用 + +`MaterialApplication` 是 `Application` 的 Material Design 3 专用派生类,自动注册亮色和暗色主题,并把 Material 动画工厂绑定上去。如果你的应用用 Material Design 3,直接用它就行,不需要手动注册主题。 + +## 初始化 + +用法很简单,替换掉 `QApplication` 就行: + +```cpp +#include "ui/widget/material/application/material_application.h" + +using namespace cf::ui::widget::material; + +int main(int argc, char* argv[]) { + MaterialApplication app(argc, argv); + + // 默认已经是 Material 亮色主题 + MainWindow w; + w.show(); + + return app.exec(); +} +```text + +构造函数里会自动调用 `init()`,`init()` 按下面的顺序执行: + +1. 注册 Material 动画工厂(前缀 `theme.material`) +2. 注册 Material 亮色和暗色主题到 `ThemeManager` +3. 设置默认主题为 `theme.material.light` +4. 调用基类的 `init()` 创建动画工厂 + +## 主题切换 + +Material 主题已经预注册好了,直接切换就行: + +```cpp +// 切换到暗色主题 +app.setTheme("theme.material.dark"); + +// 或者用 token 字面量(推荐) +using namespace cf::ui::core::token::literals; +app.setTheme(MATERIAL_THEME_DARK); +```text + +主题切换会触发 `themeChanged` 信号,动画工厂会同步重建。 + +## 动画工厂绑定 + +Material 动画工厂是和主题绑定的——当主题切换到任何 `theme.material.*` 时,都会用 `CFMaterialAnimationFactory` 创建动画。工厂的前缀匹配保证了这点: + +```cpp +// 在 MaterialApplication::init() 里已经注册了 +Application::registerAnimationFactoryType("theme.material", + [](const core::ICFTheme& theme, QObject* parent) { + return std::make_unique(theme, nullptr, parent); + }); +```text + +所以你不需要自己管工厂创建,只要确保主题 token 以 `theme.material.` 开头就行。 + +## 默认主题 + +`MaterialApplication::DEFAULT_THEME` 定义为 `MATERIAL_THEME_LIGHT`(即 `"theme.material.light"`)。如果你想在构造后立即切换主题,可以直接用: + +```cpp +MaterialApplication app(argc, argv); + +// 如果想要暗色主题作为默认 +app.setTheme(MATERIAL_THEME_DARK); +```text + +或者自己派生一个类,在 `init()` 里改默认主题: + +```cpp +class DarkMaterialApplication : public MaterialApplication { +protected: + void init() override { + MaterialApplication::init(); // 先让基类注册完主题 + setTheme(MATERIAL_THEME_DARK); // 然后改默认 + } +}; +```text + +⚠️ 不要在 `MaterialApplication::init()` 里改默认主题再调用基类 `init()`——因为基类的 `init()` 会创建动画工厂,此时默认主题还是亮色的。要么在构造后改,要么在派生类的 `init()` 里先调基类 `init()` 再改。 + +## 和裸 Application 的区别 + +直接用 `Application` 需要自己注册主题和动画工厂: + +```cpp +// 用裸 Application 需要这些额外步骤 +Application app(argc, argv); + +Application::registerAnimationFactoryType("theme.material", ...); +themeManager()->insert_one("theme.material.light", ...); +themeManager()->setThemeTo("theme.material.light", false); + +app.init(); // 别忘了手动调用 init() +```text + +而 `MaterialApplication` 把这些都包圆了,构造完就能用。如果你用 Material Design 3,没理由不用它。 + +## 相关文档 + +- [Application - 应用基础类](./application.md) +- [Material 主题系统](../material/cfmaterial_theme.md) +- [Material 动画](../material/animation/index.md) diff --git a/document/HandBook/ui/architecture/.pages b/document/HandBook/ui/architecture/.pages deleted file mode 100644 index df5e84e32..000000000 --- a/document/HandBook/ui/architecture/.pages +++ /dev/null @@ -1,8 +0,0 @@ -title: 架构总览 -icon: material/layers-triple -nav: - - 第一层 · 数学工具: layer-1-math-utility - - 第二层 · 主题引擎: layer-2-theme-engine - - 第三层 · 动画引擎: layer-3-animation-engine - - 第四层 · Material 行为: layer-4-material-behavior - - 第五层 · Widget 适配: layer-5-widget-adapter diff --git a/document/HandBook/ui/architecture/index.md b/document/HandBook/ui/architecture/index.md index 1443e800c..96a38990a 100644 --- a/document/HandBook/ui/architecture/index.md +++ b/document/HandBook/ui/architecture/index.md @@ -1,8 +1,13 @@ +--- +title: 架构总览 +description: CFDesktop UI 框架采用五层架构设计,遵循依赖倒置原则:上层依赖抽象接口,下层提供具体实现 +--- + # 架构总览 CFDesktop UI 框架采用五层架构设计,遵循依赖倒置原则:上层依赖抽象接口,下层提供具体实现。 -``` +```text ┌─────────────────────────────────────────┐ │ Layer 5 · Widget 适配层 │ Button, Checkbox, Slider... ├─────────────────────────────────────────┤ @@ -14,7 +19,7 @@ CFDesktop UI 框架采用五层架构设计,遵循依赖倒置原则:上层 ├─────────────────────────────────────────┤ │ Layer 1 · 数学工具层 │ HCT 色彩, 几何, 像素适配 └─────────────────────────────────────────┘ -``` +```text ## 各层详情 diff --git a/document/HandBook/ui/architecture/layer-1-math-utility/.pages b/document/HandBook/ui/architecture/layer-1-math-utility/.pages deleted file mode 100644 index 7fc560ec6..000000000 --- a/document/HandBook/ui/architecture/layer-1-math-utility/.pages +++ /dev/null @@ -1,5 +0,0 @@ -title: 第一层 · 数学工具 -nav: - - 为什么需要自己的数学层: 01-why-we-need-own-math-layer.md - - HCT 色彩系统: 02-color-system-hct.md - - 几何与设备像素: 03-geometry-and-device-pixel.md diff --git a/document/HandBook/ui/architecture/layer-1-math-utility/01-why-we-need-own-math-layer.md b/document/HandBook/ui/architecture/layer-1-math-utility/01-why-we-need-own-math-layer.md index a1092540a..d5ab3c999 100644 --- a/document/HandBook/ui/architecture/layer-1-math-utility/01-why-we-need-own-math-layer.md +++ b/document/HandBook/ui/architecture/layer-1-math-utility/01-why-we-need-own-math-layer.md @@ -1,89 +1,94 @@ -# 从零开始——为什么我们决定自己造轮子 - -说实话,当我说我们要自己写一套数学工具库的时候,队友的表情就像看到我决定徒手写 JSON 解析器一样——你说你有什么毛病?Qt 不是已经给你配好了 QColor、QEasingCurve,甚至还有 QPropertyAnimation,你这不是吃饱了撑的吗? - -这话其实一点没错。如果我们的目标只是"做一个能用的 UI",那直接用 Qt 的东西确实够了。但问题是,我们要做的不是"能用的 UI",而是 Material Design 3——那个 Google 把每一个像素、每一个动画曲线都规定得死死的 Design System。而这玩意儿和 Qt 原生提供的工具之间,存在着一条难以跨越的鸿沟。 - -## 问题一:RGB 空间的"不直觉" - -首先来说颜色。Qt 的 QColor 用的是 RGB/HSL,这本来没什么问题——直到你需要根据用户的壁纸自动生成一套主题。 - -假设用户选了一个品牌色,比如 `#6200EE`(Material 的经典紫色)。现在你需要生成这个颜色的"浅色变体"用于 hover 状态,"深色变体"用于 pressed 状态。用 RGB 的思路,你会怎么做?增加 RGB 值?那就麻烦了——RGB 空间里"变亮"和"变色"是绑定的,你把紫色变亮的时候,它的色相可能会偏移,最后得到一个不再是紫色的"亮紫色"。 - -Material Design 3 用的是 HCT 色彩空间(Hue-Chroma-Tone),这三个维度是正交的:你调整 Tone(亮度)的时候,Hue(色相)和 Chroma(色度)完全不受影响。这就意味着你可以放心地生成同一个颜色的 13 个亮度等级,而不用担心颜色"跑偏"。 - -Qt 的 QColor 不支持 HCT,所以我们只能自己实现。 - -## 问题二:QColor::lighter() 的灾难 - -有人会说,那用 QColor::lighter() 不就行了? - -这里真的要踩个坑。QColor::lighter() 在 HSL 空间里操作,它确实能让颜色"变亮",但问题是它调整的是 Lightness,而 Lightness 和人类感知的"亮度"并不完全一致。更糟糕的是,这个函数在处理某些颜色的时候会产生奇怪的色相漂移。 - -你可能会说,那我直接调整 RGB 的每个分量?比如都乘以 1.2?来试试看:`RGB(100, 50, 150)` 乘以 1.2 之后是 `RGB(120, 60, 180)`。看起来没问题,但问题是当某个分量超过 255 的时候,你需要钳位——这会导致颜色向白色漂移,而且漂移的方向和 RGB 三个分量的相对大小有关,完全不可预测。 - -所以我们决定实现一个 CFColor 类,它内部维护一个 QColor(因为 Qt 绘制 API 只接受 QColor),但同时缓存 HCT 值。当你需要调整亮度时,我们直接在 HCT 空间操作,然后再转回 RGB。这样既保证了颜色一致性,又能和 Qt 的 API 无缝对接。 - -## 问题三:缓动曲线的"不标准" - -再来说动画。Qt 的 QEasingCurve 提供了一堆预定义曲线:InQuad、OutCubic、InOutQuart……但 Material Design 3 用的是一套完全不同的命名和参数:Standard、Emphasized、Legacy。 - -当然你可以自己用 `QEasingCurve::addCubicBezier()` 手动添加曲线,但问题是 Material 的曲线参数是固定的,你每次都要去查文档。所以我们做了一个 Easing 命名空间,把 Material 的标准曲线预先定义好,你只需要 `Easing::fromEasingType(Type::Standard)` 就能拿到对应的 QEasingCurve。 - -## 问题四:零 Qt UI 依赖的执念 - -说到这里可能有人要问了:既然你最后还是要用 QColor、QEasingCurve,那为什么不直接在需要的地方调用 Qt 的 API,非要封装一层? - -这个问题触及到了我们设计的核心理念:ui/base 层应该是零 Qt UI 依赖的。 - -为什么?因为数学工具本身不应该依赖 UI 框架。插值、钳位、贝塞尔曲线——这些是纯粹的数学运算,它们在任何平台上、任何框架里都应该是一样的。如果我们把它们和 Qt 绑定在一起,将来想移植到其他框架,或者想单独测试这些数学函数,就会非常麻烦。 - -更重要的是,零依赖意味着我们可以把 ui/base 编译成一个独立的静态库,不需要链接 QtWidgets 或 QtGui。这对于单元测试、对于嵌入式环境的精简构建,都有很大的意义。 - -## 问题五:弹簧动画的缺失 - -Qt 的动画系统是基于时间的(QPropertyAnimation、QVariantAnimation),但 Material Design 3 大量使用弹簧动画——那种自然的、带一点弹性的运动效果。 - -Qt 没有提供弹簧动画的实现,所以我们只能自己写。springStep 函数使用半隐式欧拉积分法模拟弹簧物理,给定当前位置、速度、目标位置、劲度系数和阻尼系数,返回下一时刻的位置和速度。 - -这里有个坑点需要注意:stiffness 和 dt 的乘积不能太大,否则数值积分会不稳定,产生诡异的振荡。我们经过多次测试,发现 stiffness 在 100-500 之间、dt 在 1/60 秒时比较稳定。 - -## 我们的解决方案 - -最终,我们在 ui/base 层实现了以下模块: - -- `color.h` / `color_helper.h`:HCT 色彩空间支持、颜色混合、对比度计算 -- `math_helper.h`:lerp、clamp、remap、cubicBezier、springStep、lerpAngle -- `easing.h`:Material 标准缓动曲线和弹簧预设 -- `geometry_helper.h`:圆角矩形路径生成 -- `device_pixel.h`:dp/sp/px 单位转换 - -所有这些模块都只依赖 QtCore(有些甚至完全不依赖 Qt),可以独立编译、独立测试。 - -## 验证一下 - -写了这么多,不如直接跑一下看看效果。这里有个简单的例子:我们从一个品牌色生成整套 tonal palette,然后用它来构建一个 Material 主题。 - -```cpp -// 从品牌色生成 tonal palette -CFColor brandColor("#6200EE"); -QList palette = tonalPalette(brandColor); - -// palette[0] 是最接近品牌色的 tone 值 -// palette[1] 到 palette[13] 是从 Tone 0 到 Tone 100 的 13 个等级 -``` - -如果你打印出这些颜色的 RGB 值,你会发现它们的色相和色度基本保持一致,只有亮度在变化——这正是 HCT 空间的优势。 - -## 下一步 - -到这里,基础数学工具层就搭好了。但有了工具只是第一步,接下来我们需要把这些工具组织成一个完整的主题系统。Material Design 3 的主题不是简单的颜色集合,而是一套由 Token 驱动的、支持动态切换的、可扩展的架构。 - -接下来,我们进入 Layer 2:Theme Engine Layer。 - ---- - -**相关文档** - -- [HCT 色彩空间实战](./02-color-system-hct.md)——深入了解 HCT 的数学原理 -- [几何与设备无关](./03-geometry-and-device-pixel.md)——跨 DPI 适配的完整方案 +--- +title: 从零开始——为什么我们决定自己造轮子 +description: 说实话,当我说我们要自己写一套数学工具库的时候,队友的表情就像看到我决定徒手写 JSON 解析器一样 +--- + +# 从零开始——为什么我们决定自己造轮子 + +说实话,当我说我们要自己写一套数学工具库的时候,队友的表情就像看到我决定徒手写 JSON 解析器一样——你说你有什么毛病?Qt 不是已经给你配好了 QColor、QEasingCurve,甚至还有 QPropertyAnimation,你这不是吃饱了撑的吗? + +这话其实一点没错。如果我们的目标只是"做一个能用的 UI",那直接用 Qt 的东西确实够了。但问题是,我们要做的不是"能用的 UI",而是 Material Design 3——那个 Google 把每一个像素、每一个动画曲线都规定得死死的 Design System。而这玩意儿和 Qt 原生提供的工具之间,存在着一条难以跨越的鸿沟。 + +## 问题一:RGB 空间的"不直觉" + +首先来说颜色。Qt 的 QColor 用的是 RGB/HSL,这本来没什么问题——直到你需要根据用户的壁纸自动生成一套主题。 + +假设用户选了一个品牌色,比如 `#6200EE`(Material 的经典紫色)。现在你需要生成这个颜色的"浅色变体"用于 hover 状态,"深色变体"用于 pressed 状态。用 RGB 的思路,你会怎么做?增加 RGB 值?那就麻烦了——RGB 空间里"变亮"和"变色"是绑定的,你把紫色变亮的时候,它的色相可能会偏移,最后得到一个不再是紫色的"亮紫色"。 + +Material Design 3 用的是 HCT 色彩空间(Hue-Chroma-Tone),这三个维度是正交的:你调整 Tone(亮度)的时候,Hue(色相)和 Chroma(色度)完全不受影响。这就意味着你可以放心地生成同一个颜色的 13 个亮度等级,而不用担心颜色"跑偏"。 + +Qt 的 QColor 不支持 HCT,所以我们只能自己实现。 + +## 问题二:QColor::lighter() 的灾难 + +有人会说,那用 QColor::lighter() 不就行了? + +这里真的要踩个坑。QColor::lighter() 在 HSL 空间里操作,它确实能让颜色"变亮",但问题是它调整的是 Lightness,而 Lightness 和人类感知的"亮度"并不完全一致。更糟糕的是,这个函数在处理某些颜色的时候会产生奇怪的色相漂移。 + +你可能会说,那我直接调整 RGB 的每个分量?比如都乘以 1.2?来试试看:`RGB(100, 50, 150)` 乘以 1.2 之后是 `RGB(120, 60, 180)`。看起来没问题,但问题是当某个分量超过 255 的时候,你需要钳位——这会导致颜色向白色漂移,而且漂移的方向和 RGB 三个分量的相对大小有关,完全不可预测。 + +所以我们决定实现一个 CFColor 类,它内部维护一个 QColor(因为 Qt 绘制 API 只接受 QColor),但同时缓存 HCT 值。当你需要调整亮度时,我们直接在 HCT 空间操作,然后再转回 RGB。这样既保证了颜色一致性,又能和 Qt 的 API 无缝对接。 + +## 问题三:缓动曲线的"不标准" + +再来说动画。Qt 的 QEasingCurve 提供了一堆预定义曲线:InQuad、OutCubic、InOutQuart……但 Material Design 3 用的是一套完全不同的命名和参数:Standard、Emphasized、Legacy。 + +当然你可以自己用 `QEasingCurve::addCubicBezier()` 手动添加曲线,但问题是 Material 的曲线参数是固定的,你每次都要去查文档。所以我们做了一个 Easing 命名空间,把 Material 的标准曲线预先定义好,你只需要 `Easing::fromEasingType(Type::Standard)` 就能拿到对应的 QEasingCurve。 + +## 问题四:零 Qt UI 依赖的执念 + +说到这里可能有人要问了:既然你最后还是要用 QColor、QEasingCurve,那为什么不直接在需要的地方调用 Qt 的 API,非要封装一层? + +这个问题触及到了我们设计的核心理念:ui/base 层应该是零 Qt UI 依赖的。 + +为什么?因为数学工具本身不应该依赖 UI 框架。插值、钳位、贝塞尔曲线——这些是纯粹的数学运算,它们在任何平台上、任何框架里都应该是一样的。如果我们把它们和 Qt 绑定在一起,将来想移植到其他框架,或者想单独测试这些数学函数,就会非常麻烦。 + +更重要的是,零依赖意味着我们可以把 ui/base 编译成一个独立的静态库,不需要链接 QtWidgets 或 QtGui。这对于单元测试、对于嵌入式环境的精简构建,都有很大的意义。 + +## 问题五:弹簧动画的缺失 + +Qt 的动画系统是基于时间的(QPropertyAnimation、QVariantAnimation),但 Material Design 3 大量使用弹簧动画——那种自然的、带一点弹性的运动效果。 + +Qt 没有提供弹簧动画的实现,所以我们只能自己写。springStep 函数使用半隐式欧拉积分法模拟弹簧物理,给定当前位置、速度、目标位置、劲度系数和阻尼系数,返回下一时刻的位置和速度。 + +这里有个坑点需要注意:stiffness 和 dt 的乘积不能太大,否则数值积分会不稳定,产生诡异的振荡。我们经过多次测试,发现 stiffness 在 100-500 之间、dt 在 1/60 秒时比较稳定。 + +## 我们的解决方案 + +最终,我们在 ui/base 层实现了以下模块: + +- `color.h` / `color_helper.h`:HCT 色彩空间支持、颜色混合、对比度计算 +- `math_helper.h`:lerp、clamp、remap、cubicBezier、springStep、lerpAngle +- `easing.h`:Material 标准缓动曲线和弹簧预设 +- `geometry_helper.h`:圆角矩形路径生成 +- `device_pixel.h`:dp/sp/px 单位转换 + +所有这些模块都只依赖 QtCore(有些甚至完全不依赖 Qt),可以独立编译、独立测试。 + +## 验证一下 + +写了这么多,不如直接跑一下看看效果。这里有个简单的例子:我们从一个品牌色生成整套 tonal palette,然后用它来构建一个 Material 主题。 + +```cpp +// 从品牌色生成 tonal palette +CFColor brandColor("#6200EE"); +QList palette = tonalPalette(brandColor); + +// palette[0] 是最接近品牌色的 tone 值 +// palette[1] 到 palette[13] 是从 Tone 0 到 Tone 100 的 13 个等级 +```yaml + +如果你打印出这些颜色的 RGB 值,你会发现它们的色相和色度基本保持一致,只有亮度在变化——这正是 HCT 空间的优势。 + +## 下一步 + +到这里,基础数学工具层就搭好了。但有了工具只是第一步,接下来我们需要把这些工具组织成一个完整的主题系统。Material Design 3 的主题不是简单的颜色集合,而是一套由 Token 驱动的、支持动态切换的、可扩展的架构。 + +接下来,我们进入 Layer 2:Theme Engine Layer。 + +--- + +**相关文档** + +- [HCT 色彩空间实战](./02-color-system-hct.md)——深入了解 HCT 的数学原理 +- [几何与设备无关](./03-geometry-and-device-pixel.md)——跨 DPI 适配的完整方案 diff --git a/document/HandBook/ui/architecture/layer-1-math-utility/02-color-system-hct.md b/document/HandBook/ui/architecture/layer-1-math-utility/02-color-system-hct.md index 45bd5537c..28b6cc98f 100644 --- a/document/HandBook/ui/architecture/layer-1-math-utility/02-color-system-hct.md +++ b/document/HandBook/ui/architecture/layer-1-math-utility/02-color-system-hct.md @@ -1,184 +1,189 @@ -# HCT 色彩空间实战——Material 动态主题的数学基础 - -在上一篇文章里,我们聊了为什么需要自己实现一套数学工具。现在我们来深入其中一个核心问题:颜色。 - -假设你正在做一个支持"动态主题"的应用——用户可以选择自己的品牌色,然后应用自动生成一套完整的配色方案。这听起来很简单,但真的动手做的时候,你会发现这事儿比想象中麻烦得多。 - -## 问题:给定品牌色,生成整套主题 - -我们用一个具体的例子来说明问题。假设用户的品牌色是 `#6200EE`(Material Design 的经典紫色),现在需要生成以下颜色: - -- Primary:品牌色本身 -- OnPrimary:在 Primary 上显示的文本颜色 -- PrimaryContainer:使用 Primary 的容器背景 -- OnPrimaryContainer:在 PrimaryContainer 上显示的文本颜色 -- Secondary:辅助色 -- ...(还有 20+ 个角色) - -如果用 RGB 空间直接操作,你很快就会遇到问题。 - -## RGB 空间的"不直观" - -首先,我们想生成一个"比 Primary 浅一点"的颜色用于 hover 状态。在 RGB 空间里,怎么做? - -最直观的想法是:直接增加 RGB 三个分量。比如 `RGB(98, 0, 238)` 乘以 1.2 得到 `RGB(117, 0, 255)`。看起来没问题? - -但问题来了: - -1. 当某个分量超过 255 时,你需要钳位。这意味着三个分量的相对比例会发生变化,颜色会"跑偏"。 -2. 不同颜色的"变亮"效果不一致。深紫色增加 50 和深红色增加 50,视觉效果完全不同。 -3. 最要命的是:RGB 空间的"亮度"和人眼感知的亮度不一致。同样是增加 50,绿色看起来比蓝色亮得多。 - -这就是为什么我们需要一个更"符合人类感知"的色彩空间。 - -## HCT 色彩空间的三个维度 - -HCT 代表 Hue-Chroma-Tone,分别对应: - -- **Hue(色相)**:0-360 度,决定颜色是什么(红、橙、黄、绿...) -- **Chroma(色度)**:0-150,决定颜色的"鲜艳程度" -- **Tone(色调/亮度)**:0-100,决定颜色有多亮 - -这三个维度是(近似)正交的:调整 Tone 不会影响 Hue 和 Chroma,调整 Chroma 不会影响 Hue。这正是我们需要的——我们可以保持色相和色度不变,只调整亮度来生成一套配色。 - -## 从 RGB 到 HCT 的转换 - -现在问题来了:我们拿到的是 RGB 值(比如 `#6200EE`),怎么转换成 HCT? - -完整的 CAM16 色彩模型算法非常复杂,涉及到复杂的矩阵运算和迭代求解。我们这里用的是简化版本,通过 HSL 做中转。 - -```cpp -// RGB → HSL -HSL hsl = rgbToHsl(r, g, b); - -// HSL → HCT(近似) -outH = hsl.h; // 色相直接沿用 -outT = perceivedLightness * 100.0f; // 感知亮度 -outC = (hsl.s / toneFactor) * 100.0f; // 色度从饱和度推导 -``` - -这里有几个细节需要注意。首先是"感知亮度",它不是简单的 (R+G+B)/3,而是加权平均: - -```cpp -float perceivedLightness = 0.299f * r + 0.587f * g + 0.114f * b; -``` - -这个权重来源于人眼对不同颜色的敏感度——绿色看起来最亮,蓝色最暗。 - -然后是色度的计算。在极端亮度(非常亮或非常暗)的情况下,同样的饱和度产生的"鲜艳感"会变弱,所以我们需要一个 toneFactor 来补偿: - -```cpp -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; -``` - -## 从 HCT 到 RGB 的回转 - -生成配色的时候,我们是在 HCT 空间操作的(比如固定 Hue 和 Chroma,改变 Tone)。最终显示的时候,需要转回 RGB。 - -```cpp -// HCT → HSL(近似) -float t = tone / 100.0f; -float chromaFactor = math::clamp(chroma / 100.0f, 0.0f, 1.5f); - -// 饱和度在中色调时达到峰值 -float toneSaturationFactor = 1.0f - std::abs(t - 0.5f) * 2.0f; -toneSaturationFactor = toneSaturationFactor * toneSaturationFactor; - -outS = chromaFactor * (0.3f + 0.7f * toneSaturationFactor); -outL = t - chromaFactor * 0.05f; // 高色度会让颜色看起来更暗 -``` - -然后再用标准的 HSL → RGB 算法转回 RGB。 - -## tonalPalette 算法 - -有了 HCT,生成 tonal palette 就很简单了: - -```cpp -QList tonalPalette(CFColor keyColor) { - float hue = keyColor.hue(); // 保持色相 - float chroma = keyColor.chroma(); // 保持色度 - - // 13 个标准 Tone 值 - constexpr float TONAL_VALUES[] = { - 0.0f, 10.0f, 20.0f, 30.0f, 40.0f, - 50.0f, 60.0f, 70.0f, 80.0f, 90.0f, - 95.0f, 99.0f, 100.0f - }; - - QList palette; - for (int i = 0; i < 13; ++i) { - palette.append(CFColor(hue, chroma, TONAL_VALUES[i])); - } - return palette; -} -``` - -就这么简单。你固定 Hue 和 Chroma,只变 Tone,就能得到 13 个颜色,它们的"颜色感"是一致的,只有亮度不同。 - -## 相对亮度和对比度计算 - -有了颜色,我们还需要计算对比度——比如确保文本和背景之间的对比度达到 WCAG AA 标准(4.5:1)。 - -对比度计算需要先算出"相对亮度"(Relative Luminance),这里有个关键点:需要先做 gamma 校正。 - -```cpp -// sRGB → linear RGB(gamma 校正) -float toLinear(float c) { - if (c <= 0.04045f) { - return c / 12.92f; - } - return std::pow((c + 0.055f) / 1.055f, 2.4f); -} - -// 相对亮度 -float relativeLuminance() const { - 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; -} -``` - -为什么需要 gamma 校正?因为 sRGB 是非线性编码的,直接用 RGB 值计算亮度会得到错误的结果。gamma 校正后,RGB 值才与实际的物理光强成正比。 - -对比度计算很简单: - -```cpp -float contrastRatio(CFColor& a, CFColor& b) { - 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); -} -``` - -加 0.05 是 WCAG 标准的规定,用于避免极端情况的数值问题。 - -## 色域裁剪的问题 - -这里有个潜在的坑:某些 HCT 组合在 sRGB 色域里是无法表示的。比如非常高的 Chroma + 中等 Tone,可能会超出 sRGB 的范围。 - -我们在 HCT → RGB 转换时做了钳位处理,但钳位会导致颜色"失真"。一个更完善的方案是在检测到超出色域时,降低 Chroma 值直到颜色可表示。不过这个会增加计算复杂度,我们目前采用简化处理。 - -## 下一步 - -到这里,我们就有了完整的颜色系统:HCT 色彩空间支持、tonal palette 生成、对比度计算。但颜色只是主题系统的一部分,一个完整的主题还需要字体、圆角、动画规范等组件。 - -更重要的是,我们需要一个系统来管理这些组件,让它们能够动态切换、能够被控件方便地访问。 - -接下来,我们进入 Layer 2:Theme Engine Layer,看看如何把颜色、字体、形状等组件整合成一个完整的主题系统。 - ---- - -**相关文档** - -- [为什么我们需要自己的数学层](./01-why-we-need-own-math-layer.md)——基础层设计动机 -- [几何与设备无关](./03-geometry-and-device-pixel.md)——跨 DPI 适配的完整方案 -- [颜色方案实现](../layer-2-theme-engine/03-color-scheme.md)——主题系统中的颜色应用 +--- +title: HCT 色彩空间实战——Material 动态主题的数学基础 +description: 在上一篇文章里,我们聊了为什么需要自己实现一套数学工具。现在我们来深入其中一个核心问题:颜色。 +--- + +# HCT 色彩空间实战——Material 动态主题的数学基础 + +在上一篇文章里,我们聊了为什么需要自己实现一套数学工具。现在我们来深入其中一个核心问题:颜色。 + +假设你正在做一个支持"动态主题"的应用——用户可以选择自己的品牌色,然后应用自动生成一套完整的配色方案。这听起来很简单,但真的动手做的时候,你会发现这事儿比想象中麻烦得多。 + +## 问题:给定品牌色,生成整套主题 + +我们用一个具体的例子来说明问题。假设用户的品牌色是 `#6200EE`(Material Design 的经典紫色),现在需要生成以下颜色: + +- Primary:品牌色本身 +- OnPrimary:在 Primary 上显示的文本颜色 +- PrimaryContainer:使用 Primary 的容器背景 +- OnPrimaryContainer:在 PrimaryContainer 上显示的文本颜色 +- Secondary:辅助色 +- ...(还有 20+ 个角色) + +如果用 RGB 空间直接操作,你很快就会遇到问题。 + +## RGB 空间的"不直观" + +首先,我们想生成一个"比 Primary 浅一点"的颜色用于 hover 状态。在 RGB 空间里,怎么做? + +最直观的想法是:直接增加 RGB 三个分量。比如 `RGB(98, 0, 238)` 乘以 1.2 得到 `RGB(117, 0, 255)`。看起来没问题? + +但问题来了: + +1. 当某个分量超过 255 时,你需要钳位。这意味着三个分量的相对比例会发生变化,颜色会"跑偏"。 +2. 不同颜色的"变亮"效果不一致。深紫色增加 50 和深红色增加 50,视觉效果完全不同。 +3. 最要命的是:RGB 空间的"亮度"和人眼感知的亮度不一致。同样是增加 50,绿色看起来比蓝色亮得多。 + +这就是为什么我们需要一个更"符合人类感知"的色彩空间。 + +## HCT 色彩空间的三个维度 + +HCT 代表 Hue-Chroma-Tone,分别对应: + +- **Hue(色相)**:0-360 度,决定颜色是什么(红、橙、黄、绿...) +- **Chroma(色度)**:0-150,决定颜色的"鲜艳程度" +- **Tone(色调/亮度)**:0-100,决定颜色有多亮 + +这三个维度是(近似)正交的:调整 Tone 不会影响 Hue 和 Chroma,调整 Chroma 不会影响 Hue。这正是我们需要的——我们可以保持色相和色度不变,只调整亮度来生成一套配色。 + +## 从 RGB 到 HCT 的转换 + +现在问题来了:我们拿到的是 RGB 值(比如 `#6200EE`),怎么转换成 HCT? + +完整的 CAM16 色彩模型算法非常复杂,涉及到复杂的矩阵运算和迭代求解。我们这里用的是简化版本,通过 HSL 做中转。 + +```cpp +// RGB → HSL +HSL hsl = rgbToHsl(r, g, b); + +// HSL → HCT(近似) +outH = hsl.h; // 色相直接沿用 +outT = perceivedLightness * 100.0f; // 感知亮度 +outC = (hsl.s / toneFactor) * 100.0f; // 色度从饱和度推导 +```text + +这里有几个细节需要注意。首先是"感知亮度",它不是简单的 (R+G+B)/3,而是加权平均: + +```cpp +float perceivedLightness = 0.299f * r + 0.587f * g + 0.114f * b; +```text + +这个权重来源于人眼对不同颜色的敏感度——绿色看起来最亮,蓝色最暗。 + +然后是色度的计算。在极端亮度(非常亮或非常暗)的情况下,同样的饱和度产生的"鲜艳感"会变弱,所以我们需要一个 toneFactor 来补偿: + +```cpp +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; +```text + +## 从 HCT 到 RGB 的回转 + +生成配色的时候,我们是在 HCT 空间操作的(比如固定 Hue 和 Chroma,改变 Tone)。最终显示的时候,需要转回 RGB。 + +```cpp +// HCT → HSL(近似) +float t = tone / 100.0f; +float chromaFactor = math::clamp(chroma / 100.0f, 0.0f, 1.5f); + +// 饱和度在中色调时达到峰值 +float toneSaturationFactor = 1.0f - std::abs(t - 0.5f) * 2.0f; +toneSaturationFactor = toneSaturationFactor * toneSaturationFactor; + +outS = chromaFactor * (0.3f + 0.7f * toneSaturationFactor); +outL = t - chromaFactor * 0.05f; // 高色度会让颜色看起来更暗 +```text + +然后再用标准的 HSL → RGB 算法转回 RGB。 + +## tonalPalette 算法 + +有了 HCT,生成 tonal palette 就很简单了: + +```cpp +QList tonalPalette(CFColor keyColor) { + float hue = keyColor.hue(); // 保持色相 + float chroma = keyColor.chroma(); // 保持色度 + + // 13 个标准 Tone 值 + constexpr float TONAL_VALUES[] = { + 0.0f, 10.0f, 20.0f, 30.0f, 40.0f, + 50.0f, 60.0f, 70.0f, 80.0f, 90.0f, + 95.0f, 99.0f, 100.0f + }; + + QList palette; + for (int i = 0; i < 13; ++i) { + palette.append(CFColor(hue, chroma, TONAL_VALUES[i])); + } + return palette; +} +```text + +就这么简单。你固定 Hue 和 Chroma,只变 Tone,就能得到 13 个颜色,它们的"颜色感"是一致的,只有亮度不同。 + +## 相对亮度和对比度计算 + +有了颜色,我们还需要计算对比度——比如确保文本和背景之间的对比度达到 WCAG AA 标准(4.5:1)。 + +对比度计算需要先算出"相对亮度"(Relative Luminance),这里有个关键点:需要先做 gamma 校正。 + +```cpp +// sRGB → linear RGB(gamma 校正) +float toLinear(float c) { + if (c <= 0.04045f) { + return c / 12.92f; + } + return std::pow((c + 0.055f) / 1.055f, 2.4f); +} + +// 相对亮度 +float relativeLuminance() const { + 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; +} +```text + +为什么需要 gamma 校正?因为 sRGB 是非线性编码的,直接用 RGB 值计算亮度会得到错误的结果。gamma 校正后,RGB 值才与实际的物理光强成正比。 + +对比度计算很简单: + +```cpp +float contrastRatio(CFColor& a, CFColor& b) { + 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); +} +```yaml + +加 0.05 是 WCAG 标准的规定,用于避免极端情况的数值问题。 + +## 色域裁剪的问题 + +这里有个潜在的坑:某些 HCT 组合在 sRGB 色域里是无法表示的。比如非常高的 Chroma + 中等 Tone,可能会超出 sRGB 的范围。 + +我们在 HCT → RGB 转换时做了钳位处理,但钳位会导致颜色"失真"。一个更完善的方案是在检测到超出色域时,降低 Chroma 值直到颜色可表示。不过这个会增加计算复杂度,我们目前采用简化处理。 + +## 下一步 + +到这里,我们就有了完整的颜色系统:HCT 色彩空间支持、tonal palette 生成、对比度计算。但颜色只是主题系统的一部分,一个完整的主题还需要字体、圆角、动画规范等组件。 + +更重要的是,我们需要一个系统来管理这些组件,让它们能够动态切换、能够被控件方便地访问。 + +接下来,我们进入 Layer 2:Theme Engine Layer,看看如何把颜色、字体、形状等组件整合成一个完整的主题系统。 + +--- + +**相关文档** + +- [为什么我们需要自己的数学层](./01-why-we-need-own-math-layer.md)——基础层设计动机 +- [几何与设备无关](./03-geometry-and-device-pixel.md)——跨 DPI 适配的完整方案 +- [颜色方案实现](../layer-2-theme-engine/03-color-scheme.md)——主题系统中的颜色应用 diff --git a/document/HandBook/ui/architecture/layer-1-math-utility/03-geometry-and-device-pixel.md b/document/HandBook/ui/architecture/layer-1-math-utility/03-geometry-and-device-pixel.md index 0608b8253..3b9857ae2 100644 --- a/document/HandBook/ui/architecture/layer-1-math-utility/03-geometry-and-device-pixel.md +++ b/document/HandBook/ui/architecture/layer-1-math-utility/03-geometry-and-device-pixel.md @@ -1,174 +1,179 @@ -# 几何与设备无关——跨 DPI 适配的完整方案 - -如果你在 1080p 的屏幕上设计了一个 40 像素高的按钮,放到 4K 屏幕上会怎么样?答案是:按钮会变得非常小,几乎点不到。 - -这是桌面 UI 开发中最经典的问题之一:不同 DPI 的屏幕上,UI 元素的大小应该如何保持一致?Qt 提供了高 DPI 支持,但它的 API 设计...怎么说呢,有些历史包袱。我们在 Material Framework 里决定自己封装一套更简洁的方案。 - -## 问题背景:1080p 到 4K - -首先说一下基本概念。DPI(Dots Per Inch)是屏幕的像素密度,1080p 通常是 96 DPI,而 4K 可能是 192 DPI 甚至更高。devicePixelRatio 是操作系统报告的缩放因子,96 DPI 时是 1.0,192 DPI 时是 2.0。 - -如果你直接用"像素"作为单位,那么在 4K 屏幕上(假设 devicePixelRatio=2.0),一个 40 像素的按钮实际上只相当于 1080p 上的 20 像素——因为物理像素更小了。 - -## 设备无关单位 - -Material Design 使用 dp(device-independent pixel,设备无关像素)作为基本单位。1dp 在 160 DPI 的屏幕上等于 1 物理像素,在 320 DPI 的屏幕上等于 2 物理像素。 - -另外还有一个 sp(scalable pixel,可缩放像素),用于字体大小。sp 和 dp 类似,但会额外考虑用户设置的字体缩放偏好。 - -## CanvasUnitHelper 的实现 - -我们做了一个 CanvasUnitHelper 结构体,用来在 dp/sp/px 之间转换: - -```cpp -struct CanvasUnitHelper { - CanvasUnitHelper(const qreal devicePixelRatio); - qreal dpToPx(qreal dp) const; - qreal spToPx(qreal sp) const; - qreal pxToDp(qreal px) const; - qreal dpi() const; - -private: - qreal devicePixelRatio; -}; -``` - -转换逻辑很简单: - -```cpp -qreal dpToPx(qreal dp) const { - return dp * devicePixelRatio; -} - -qreal spToPx(qreal sp) const { - // sp 会考虑用户字体缩放偏好 - QFont font = QApplication::font(); - qreal fontScale = font.pointSizeF() / 10.0; // 假设默认 10pt - return sp * devicePixelRatio * fontScale; -} -``` - -这里有个坑:Windows 上获取 devicePixelRatio 的方式经历了多次变迁。早期版本用 `QScreen::devicePixelRatio()`,但这个值在 Windows 10 1709 之后的"缩放与布局"设置下可能不准确。现在推荐用 `QScreen::logicalDotsPerInch()` 除以 96 来计算。 - -## 响应式断点 - -Material Design 定义了一套响应式断点,根据窗口宽度来决定布局: - -```cpp -enum class BreakPoint { - Compact, // < 600dp - Medium, // 600dp - 839dp - Expanded // >= 840dp -}; -``` - -这个设计很有意思:Material 不是针对具体设备(手机/平板/桌面)分类,而是针对"可用宽度"分类。一个桌面窗口如果缩得很窄,也应该用 Compact 布局。 - -```cpp -BreakPoint breakPoint(qreal widthDp) { - if (widthDp < 600.0) { - return BreakPoint::Compact; - } else if (widthDp < 840.0) { - return BreakPoint::Medium; - } else { - return BreakPoint::Expanded; - } -} -``` - -## 圆角矩形工具 - -Qt 的 QPainterPath 确实支持圆角矩形,但 API 有点繁琐。你需要先创建一个 QPainterPath,然后调用 `addRoundedRect()`,而且这个函数的参数是 xRadius 和 yRadius,不太符合 Material 的"统一圆角"语义。 - -所以我们封装了一下: - -```cpp -// 使用 Material 预定义的圆角尺寸 -QPainterPath roundedRect(const QRectF& rect, ShapeScale scale); - -// 自定义统一圆角 -QPainterPath roundedRect(const QRectF& rect, float radius); - -// 每个角单独指定 -QPainterPath roundedRect(const QRectF& rect, float topLeft, float topRight, - float bottomLeft, float bottomRight); -``` - -ShapeScale 枚举对应 Material 的标准圆角尺寸: - -```cpp -enum class ShapeScale { - ShapeNone, // 0dp - ShapeExtraSmall, // 4dp - ShapeSmall, // 8dp - ShapeMedium, // 12dp - ShapeLarge, // 16dp - ShapeExtraLarge, // 28dp - ShapeFull // 50% of size -}; -``` - -这里有个需要注意的地方:ShapeFull 是"完全圆角",也就是变成一个胶囊或圆形。这种情况下圆角半径是矩形短边的一半。我们在实现时需要特殊处理: - -```cpp -if (scale == ShapeScale::ShapeFull) { - radius = std::min(rect.width(), rect.height()) / 2.0f; -} -``` - -## 为什么不直接用 QSS? - -Qt 支持 QSS(Qt Style Sheets),类似 CSS,可以在样式表里定义圆角、边距等。那为什么我们不直接用 QSS? - -有几个原因: - -1. **QSS 的功能有限**:它不支持复杂的绘制逻辑,比如涟漪效果、多层阴影。 -2. **动态切换困难**:QSS 的重新加载性能不好,而且无法在运行时精确控制某个属性。 -3. **Material 规范的复杂度**:Material 的状态层透明度、动画时长等都无法用 QSS 表达。 - -所以我们选择在 paintEvent 里完全接管绘制,而 QSS 只用于一些全局的、不常变化的属性(如果有的话)。 - -## 实际使用示例 - -在控件中,你会这样使用 CanvasUnitHelper: - -```cpp -void Button::paintEvent(QPaintEvent* event) { - CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Material 按钮高度固定 40dp - qreal buttonHeight = helper.dpToPx(40.0); - - // 水平内边距 24dp - qreal paddingH = helper.dpToPx(24.0); - - // 圆角半径 - qreal radius = helper.dpToPx(cornerRadius()); -} -``` - -这样无论在什么 DPI 的屏幕上,按钮的视觉大小都是一致的。 - -## 总结 - -到这里,ui/base 层的基础工具就介绍完了。我们有了: - -- HCT 色彩空间支持 -- 数学工具函数(lerp、clamp、贝塞尔、弹簧) -- Material 标准缓动曲线 -- 设备无关单位转换 -- 圆角矩形工具 - -这些工具是纯数学运算,不依赖任何 UI 框架的特定 API。它们可以被单独编译、单独测试,也可以在任何需要的地方复用。 - -但有了工具只是第一步,接下来我们需要把这些工具组织成一个完整的主题系统。Material Design 3 的主题不是简单的颜色集合,而是一套由 Token 驱动的、支持动态切换的、可扩展的架构。 - -接下来,我们进入 Layer 2:Theme Engine Layer。 - ---- - -**相关文档** - -- [为什么我们需要自己的数学层](./01-why-we-need-own-math-layer.md)——基础层设计动机 -- [HCT 色彩空间实战](./02-color-system-hct.md)——颜色系统的数学基础 -- [主题系统架构设计](../layer-2-theme-engine/01-theme-system-design.md)——下一层的入口 +--- +title: 几何与设备无关——跨 DPI 适配的完整方案 +description: 如果你在 1080p 的屏幕上设计了一个 40 像素高的按钮,放到 4K 屏幕上会怎么样?答案是:按 +--- + +# 几何与设备无关——跨 DPI 适配的完整方案 + +如果你在 1080p 的屏幕上设计了一个 40 像素高的按钮,放到 4K 屏幕上会怎么样?答案是:按钮会变得非常小,几乎点不到。 + +这是桌面 UI 开发中最经典的问题之一:不同 DPI 的屏幕上,UI 元素的大小应该如何保持一致?Qt 提供了高 DPI 支持,但它的 API 设计...怎么说呢,有些历史包袱。我们在 Material Framework 里决定自己封装一套更简洁的方案。 + +## 问题背景:1080p 到 4K + +首先说一下基本概念。DPI(Dots Per Inch)是屏幕的像素密度,1080p 通常是 96 DPI,而 4K 可能是 192 DPI 甚至更高。devicePixelRatio 是操作系统报告的缩放因子,96 DPI 时是 1.0,192 DPI 时是 2.0。 + +如果你直接用"像素"作为单位,那么在 4K 屏幕上(假设 devicePixelRatio=2.0),一个 40 像素的按钮实际上只相当于 1080p 上的 20 像素——因为物理像素更小了。 + +## 设备无关单位 + +Material Design 使用 dp(device-independent pixel,设备无关像素)作为基本单位。1dp 在 160 DPI 的屏幕上等于 1 物理像素,在 320 DPI 的屏幕上等于 2 物理像素。 + +另外还有一个 sp(scalable pixel,可缩放像素),用于字体大小。sp 和 dp 类似,但会额外考虑用户设置的字体缩放偏好。 + +## CanvasUnitHelper 的实现 + +我们做了一个 CanvasUnitHelper 结构体,用来在 dp/sp/px 之间转换: + +```cpp +struct CanvasUnitHelper { + CanvasUnitHelper(const qreal devicePixelRatio); + qreal dpToPx(qreal dp) const; + qreal spToPx(qreal sp) const; + qreal pxToDp(qreal px) const; + qreal dpi() const; + +private: + qreal devicePixelRatio; +}; +```text + +转换逻辑很简单: + +```cpp +qreal dpToPx(qreal dp) const { + return dp * devicePixelRatio; +} + +qreal spToPx(qreal sp) const { + // sp 会考虑用户字体缩放偏好 + QFont font = QApplication::font(); + qreal fontScale = font.pointSizeF() / 10.0; // 假设默认 10pt + return sp * devicePixelRatio * fontScale; +} +```text + +这里有个坑:Windows 上获取 devicePixelRatio 的方式经历了多次变迁。早期版本用 `QScreen::devicePixelRatio()`,但这个值在 Windows 10 1709 之后的"缩放与布局"设置下可能不准确。现在推荐用 `QScreen::logicalDotsPerInch()` 除以 96 来计算。 + +## 响应式断点 + +Material Design 定义了一套响应式断点,根据窗口宽度来决定布局: + +```cpp +enum class BreakPoint { + Compact, // < 600dp + Medium, // 600dp - 839dp + Expanded // >= 840dp +}; +```text + +这个设计很有意思:Material 不是针对具体设备(手机/平板/桌面)分类,而是针对"可用宽度"分类。一个桌面窗口如果缩得很窄,也应该用 Compact 布局。 + +```cpp +BreakPoint breakPoint(qreal widthDp) { + if (widthDp < 600.0) { + return BreakPoint::Compact; + } else if (widthDp < 840.0) { + return BreakPoint::Medium; + } else { + return BreakPoint::Expanded; + } +} +```text + +## 圆角矩形工具 + +Qt 的 QPainterPath 确实支持圆角矩形,但 API 有点繁琐。你需要先创建一个 QPainterPath,然后调用 `addRoundedRect()`,而且这个函数的参数是 xRadius 和 yRadius,不太符合 Material 的"统一圆角"语义。 + +所以我们封装了一下: + +```cpp +// 使用 Material 预定义的圆角尺寸 +QPainterPath roundedRect(const QRectF& rect, ShapeScale scale); + +// 自定义统一圆角 +QPainterPath roundedRect(const QRectF& rect, float radius); + +// 每个角单独指定 +QPainterPath roundedRect(const QRectF& rect, float topLeft, float topRight, + float bottomLeft, float bottomRight); +```text + +ShapeScale 枚举对应 Material 的标准圆角尺寸: + +```cpp +enum class ShapeScale { + ShapeNone, // 0dp + ShapeExtraSmall, // 4dp + ShapeSmall, // 8dp + ShapeMedium, // 12dp + ShapeLarge, // 16dp + ShapeExtraLarge, // 28dp + ShapeFull // 50% of size +}; +```text + +这里有个需要注意的地方:ShapeFull 是"完全圆角",也就是变成一个胶囊或圆形。这种情况下圆角半径是矩形短边的一半。我们在实现时需要特殊处理: + +```cpp +if (scale == ShapeScale::ShapeFull) { + radius = std::min(rect.width(), rect.height()) / 2.0f; +} +```text + +## 为什么不直接用 QSS? + +Qt 支持 QSS(Qt Style Sheets),类似 CSS,可以在样式表里定义圆角、边距等。那为什么我们不直接用 QSS? + +有几个原因: + +1. **QSS 的功能有限**:它不支持复杂的绘制逻辑,比如涟漪效果、多层阴影。 +2. **动态切换困难**:QSS 的重新加载性能不好,而且无法在运行时精确控制某个属性。 +3. **Material 规范的复杂度**:Material 的状态层透明度、动画时长等都无法用 QSS 表达。 + +所以我们选择在 paintEvent 里完全接管绘制,而 QSS 只用于一些全局的、不常变化的属性(如果有的话)。 + +## 实际使用示例 + +在控件中,你会这样使用 CanvasUnitHelper: + +```cpp +void Button::paintEvent(QPaintEvent* event) { + CanvasUnitHelper helper(qApp->devicePixelRatio()); + + // Material 按钮高度固定 40dp + qreal buttonHeight = helper.dpToPx(40.0); + + // 水平内边距 24dp + qreal paddingH = helper.dpToPx(24.0); + + // 圆角半径 + qreal radius = helper.dpToPx(cornerRadius()); +} +```yaml + +这样无论在什么 DPI 的屏幕上,按钮的视觉大小都是一致的。 + +## 总结 + +到这里,ui/base 层的基础工具就介绍完了。我们有了: + +- HCT 色彩空间支持 +- 数学工具函数(lerp、clamp、贝塞尔、弹簧) +- Material 标准缓动曲线 +- 设备无关单位转换 +- 圆角矩形工具 + +这些工具是纯数学运算,不依赖任何 UI 框架的特定 API。它们可以被单独编译、单独测试,也可以在任何需要的地方复用。 + +但有了工具只是第一步,接下来我们需要把这些工具组织成一个完整的主题系统。Material Design 3 的主题不是简单的颜色集合,而是一套由 Token 驱动的、支持动态切换的、可扩展的架构。 + +接下来,我们进入 Layer 2:Theme Engine Layer。 + +--- + +**相关文档** + +- [为什么我们需要自己的数学层](./01-why-we-need-own-math-layer.md)——基础层设计动机 +- [HCT 色彩空间实战](./02-color-system-hct.md)——颜色系统的数学基础 +- [主题系统架构设计](../layer-2-theme-engine/01-theme-system-design.md)——下一层的入口 diff --git a/document/HandBook/ui/architecture/layer-1-math-utility/index.md b/document/HandBook/ui/architecture/layer-1-math-utility/index.md index 1f31727af..3f299bb5a 100644 --- a/document/HandBook/ui/architecture/layer-1-math-utility/index.md +++ b/document/HandBook/ui/architecture/layer-1-math-utility/index.md @@ -1,10 +1,11 @@ -# layer-1-math-utility - -> Welcome to the layer-1-math-utility section. +--- +title: "Layer 1: 数学与工具层" +description: 本章节介绍 UI 渲染管线的第一层——数学与工具层,涵盖 颜色空间转换、 几何计算、 缓动函数以及 +--- -## Overview +# Layer 1: 数学与工具层 -Documentation and resources for layer-1-math-utility. +本章节介绍 UI 渲染管线的第一层——数学与工具层,涵盖 `CFColor` 颜色空间转换、`GeometryHelper` 几何计算、`Easing` 缓动函数以及 `DevicePixel` 设备像素适配等基础工具。该层为上层所有模块提供零依赖的纯计算支持。 --- diff --git a/document/HandBook/ui/architecture/layer-2-theme-engine/.pages b/document/HandBook/ui/architecture/layer-2-theme-engine/.pages deleted file mode 100644 index d39a88139..000000000 --- a/document/HandBook/ui/architecture/layer-2-theme-engine/.pages +++ /dev/null @@ -1,6 +0,0 @@ -title: 第二层 · 主题引擎 -nav: - - 主题系统设计: 01-theme-system-design.md - - Token 系统: 02-token-system.md - - 配色方案: 03-color-scheme.md - - 排版、形状与运动: 04-typography-shape-motion.md diff --git a/document/HandBook/ui/architecture/layer-2-theme-engine/01-theme-system-design.md b/document/HandBook/ui/architecture/layer-2-theme-engine/01-theme-system-design.md index c777063d6..1d6dbc0e6 100644 --- a/document/HandBook/ui/architecture/layer-2-theme-engine/01-theme-system-design.md +++ b/document/HandBook/ui/architecture/layer-2-theme-engine/01-theme-system-design.md @@ -1,177 +1,182 @@ -# 主题系统架构设计——从单例到工厂的完整方案 - -在 Layer 1 里,我们搭建了基础的数学工具层。有了颜色系统、缓动曲线、几何工具,现在的问题是:怎么把这些东西组织成一个完整的主题,让所有控件都能方便地访问? - -这篇文章聊聊主题系统的架构设计。 - -## 架构演进的故事 - -说实话,我们的主题系统经历了好几次"推倒重来"。 - -第一版方案非常简单粗暴:全局变量。一个全局的 `QColor g_primaryColor`,一个全局的 `QFont g_defaultFont`,控件直接访问这些变量。这方案的缺点显而易见:不支持多主题、不支持动态切换、全局污染、命名空间冲突…… - -第二版方案我们探索了 QSS(Qt Style Sheets)。QSS 确实能解决一些问题,但它对于 Material Design 这种复杂的设计规范来说力不从心。QSS 不支持状态层的半透明叠加,不支持涟漪动画,不支持运行时的精确属性控制。更重要的是,QSS 的重新加载性能很差,不适合动态主题切换。 - -最终我们确立了现在的方案:接口 + 工厂 + 单例管理器的组合。 - -## 核心架构 - -整个主题系统由三个核心部分组成: - -1. **ICFTheme 接口**:主题的抽象定义,包含颜色、字体、形状、动效四个组件 -2. **ThemeFactory 接口**:创建主题的工厂,支持从名称或 JSON 创建 -3. **ThemeManager 单例**:管理多个主题工厂,处理主题切换,广播变更信号 - -### ICFTheme:主题的抽象定义 - -ICFTheme 是一个纯接口(所有方法都是 virtual),它定义了主题应该包含什么: - -```cpp -struct ICFTheme { - ICFColorScheme& color_scheme() const; - IMotionSpec& motion_spec() const; - IRadiusScale& radius_scale() const; - IFontType& font_type() const; - -protected: - std::unique_ptr color_scheme_; - std::unique_ptr motion_spec_; - std::unique_ptr radius_scale_; - std::unique_ptr font_type_; -}; -``` - -设计成接口的原因是:我们可能有多种不同的主题实现(Material、Cupertino、Fluent),但它们都应该遵循同一个接口。控件只需要依赖 ICFTheme 接口,不需要知道具体是哪种主题。 - -这里有个细节:ICFTheme 的构造函数是 protected 的,只有 ThemeFactory(被声明为 friend)可以创建实例。这确保了主题只能通过工厂创建,避免用户直接构造导致的不一致。 - -### ThemeFactory:创建主题的工厂 - -ThemeFactory 也是一个接口,定义了创建主题的三种方式: - -```cpp -class ThemeFactory { -public: - virtual std::unique_ptr fromName(const char* name) = 0; - virtual std::unique_ptr fromJson(const QByteArray& json) = 0; - virtual QByteArray toJson(ICFTheme* raw_theme) = 0; -}; -``` - -`fromName()` 用于创建预定义的主题(比如 "light"、"dark"),`fromJson()` 用于从 Material Theme Builder 导出的 JSON 创建主题,`toJson()` 用于序列化。 - -工厂模式的好处是:可以在运行时注册新的主题类型,而不需要修改核心代码。比如你想添加一个"自定义主题"功能,只需要实现一个新的 ThemeFactory,然后注册到 ThemeManager。 - -### ThemeManager:单例管理器 - -ThemeManager 是整个主题系统的入口,它是一个单例: - -```cpp -class ThemeManager : public QObject { -public: - static ThemeManager& instance(); - - // 注册/移除主题工厂 - bool insert_one(const std::string& name, InstallerMaker make_one); - void remove_one(const std::string& name); - - // 获取主题 - const ICFTheme& theme(const std::string& name) const; - - // 切换主题 - void setThemeTo(const std::string& name, bool doBroadcast = true); - - // 控件订阅主题更新 - void install_widget(QWidget* w); - void remove_widget(QWidget* w); - -signals: - void themeChanged(const ICFTheme& new_theme); -}; -``` - -使用方式很直观: - -```cpp -// 注册主题 -ThemeManager::instance().insert_one("material.light", []() { - return std::make_unique(); -}); - -// 让控件订阅主题更新 -ThemeManager::instance().install_widget(myButton); - -// 切换主题 -ThemeManager::instance().setThemeTo("material.light"); -``` - -## 为什么用接口 + 工厂? - -你可能会问:为什么不直接让 ThemeManager 管理 ICFTheme 实例,非要中间加一个 ThemeFactory? - -原因有几个: - -1. **延迟创建**:主题可能包含大量数据(颜色表、字体缓存),如果不使用就创建会浪费内存。工厂模式下,主题只在第一次访问时创建。 -2. **支持动态注册**:你可以在运行时注册新的主题类型,而不需要修改 ThemeManager 的代码。 -3. **序列化支持**:fromJson/toJson 方法让主题可以被持久化和传输。 - -## 线程安全设计 - -ThemeManager 的单例实现使用了 C++11 的"魔术静态变量": - -```cpp -static ThemeManager& instance() { - static ThemeManager manager; - return manager; -} -``` - -C++11 保证局部静态变量的初始化是线程安全的,所以这个实现不需要额外的锁。 - -但 `install_widget/remove_widget` 和 `setThemeTo` 不是线程安全的。如果需要在多线程环境使用,需要外部同步。不过一般来说,主题操作都在主线程进行,这不是问题。 - -## 主题切换的广播机制 - -当主题切换时,ThemeManager 会发出 `themeChanged` 信号,所有订阅的控件都会收到通知: - -```cpp -// 在控件的构造函数中 -connect(&ThemeManager::instance(), &ThemeManager::themeChanged, - this, [this](const ICFTheme&) { update(); }); -``` - -这里有个设计细节:为什么用 `install_widget` 而不是让控件直接连接信号? - -原因是为了生命周期管理。如果控件直接连接信号,控件销毁时需要手动断开连接,否则会访问野指针。而 `install_widget` 会跟踪控件的生命周期,当控件销毁时自动从订阅列表中移除。 - -实际上我们并没有用 QObject::destroyed 信号,因为那会有循环引用的风险。我们选择在控件析构时手动调用 `remove_widget`,或者在 ThemeManager 里定期清理失效的指针。 - -## 生命周期管理 - -主题实例由 ThemeManager 持有(通过 `theme_cache_`),而主题内的各个组件(颜色方案、字体等)由主题持有。所有权链条是清晰的: - -``` -ThemeManager (owner) - └── unordered_map> theme_cache_ - └── ICFTheme (owner) - ├── unique_ptr color_scheme_ - ├── unique_ptr motion_spec_ - ├── unique_ptr radius_scale_ - └── unique_ptr font_type_ -``` - -控件只持有引用(通过 `themeChanged` 信号的参数),不拥有主题的所有权。这样当主题被销毁时,不会有 dangling pointer 的问题。 - -## 下一步 - -到这里,主题系统的骨架就搭好了。但我们还没有讨论具体的实现细节:颜色是怎么存储的?字体是怎么缓存的?Token 系统是怎么工作的? - -在下一篇文章里,我们会深入 Token 系统——那个让 Material Design 3 的颜色访问变得类型安全又高性能的魔法。 - ---- - -**相关文档** - -- [几何与设备无关](../layer-1-math-utility/03-geometry-and-device-pixel.md)——基础层的最后一篇 -- [Token 系统设计](./02-token-system.md)——字符串字面量的编译时魔法 -- [颜色方案实现](./03-color-scheme.md)——从种子颜色到完整主题 +--- +title: 主题系统架构设计——从单例到工厂的完整方案 +description: 在 Layer 1 里,我们搭建了基础的数学工具层。有了颜色系统、缓动曲线、几何工具,现在的问题是: +--- + +# 主题系统架构设计——从单例到工厂的完整方案 + +在 Layer 1 里,我们搭建了基础的数学工具层。有了颜色系统、缓动曲线、几何工具,现在的问题是:怎么把这些东西组织成一个完整的主题,让所有控件都能方便地访问? + +这篇文章聊聊主题系统的架构设计。 + +## 架构演进的故事 + +说实话,我们的主题系统经历了好几次"推倒重来"。 + +第一版方案非常简单粗暴:全局变量。一个全局的 `QColor g_primaryColor`,一个全局的 `QFont g_defaultFont`,控件直接访问这些变量。这方案的缺点显而易见:不支持多主题、不支持动态切换、全局污染、命名空间冲突…… + +第二版方案我们探索了 QSS(Qt Style Sheets)。QSS 确实能解决一些问题,但它对于 Material Design 这种复杂的设计规范来说力不从心。QSS 不支持状态层的半透明叠加,不支持涟漪动画,不支持运行时的精确属性控制。更重要的是,QSS 的重新加载性能很差,不适合动态主题切换。 + +最终我们确立了现在的方案:接口 + 工厂 + 单例管理器的组合。 + +## 核心架构 + +整个主题系统由三个核心部分组成: + +1. **ICFTheme 接口**:主题的抽象定义,包含颜色、字体、形状、动效四个组件 +2. **ThemeFactory 接口**:创建主题的工厂,支持从名称或 JSON 创建 +3. **ThemeManager 单例**:管理多个主题工厂,处理主题切换,广播变更信号 + +### ICFTheme:主题的抽象定义 + +ICFTheme 是一个纯接口(所有方法都是 virtual),它定义了主题应该包含什么: + +```cpp +struct ICFTheme { + ICFColorScheme& color_scheme() const; + IMotionSpec& motion_spec() const; + IRadiusScale& radius_scale() const; + IFontType& font_type() const; + +protected: + std::unique_ptr color_scheme_; + std::unique_ptr motion_spec_; + std::unique_ptr radius_scale_; + std::unique_ptr font_type_; +}; +```text + +设计成接口的原因是:我们可能有多种不同的主题实现(Material、Cupertino、Fluent),但它们都应该遵循同一个接口。控件只需要依赖 ICFTheme 接口,不需要知道具体是哪种主题。 + +这里有个细节:ICFTheme 的构造函数是 protected 的,只有 ThemeFactory(被声明为 friend)可以创建实例。这确保了主题只能通过工厂创建,避免用户直接构造导致的不一致。 + +### ThemeFactory:创建主题的工厂 + +ThemeFactory 也是一个接口,定义了创建主题的三种方式: + +```cpp +class ThemeFactory { +public: + virtual std::unique_ptr fromName(const char* name) = 0; + virtual std::unique_ptr fromJson(const QByteArray& json) = 0; + virtual QByteArray toJson(ICFTheme* raw_theme) = 0; +}; +```text + +`fromName()` 用于创建预定义的主题(比如 "light"、"dark"),`fromJson()` 用于从 Material Theme Builder 导出的 JSON 创建主题,`toJson()` 用于序列化。 + +工厂模式的好处是:可以在运行时注册新的主题类型,而不需要修改核心代码。比如你想添加一个"自定义主题"功能,只需要实现一个新的 ThemeFactory,然后注册到 ThemeManager。 + +### ThemeManager:单例管理器 + +ThemeManager 是整个主题系统的入口,它是一个单例: + +```cpp +class ThemeManager : public QObject { +public: + static ThemeManager& instance(); + + // 注册/移除主题工厂 + bool insert_one(const std::string& name, InstallerMaker make_one); + void remove_one(const std::string& name); + + // 获取主题 + const ICFTheme& theme(const std::string& name) const; + + // 切换主题 + void setThemeTo(const std::string& name, bool doBroadcast = true); + + // 控件订阅主题更新 + void install_widget(QWidget* w); + void remove_widget(QWidget* w); + +signals: + void themeChanged(const ICFTheme& new_theme); +}; +```text + +使用方式很直观: + +```cpp +// 注册主题 +ThemeManager::instance().insert_one("material.light", []() { + return std::make_unique(); +}); + +// 让控件订阅主题更新 +ThemeManager::instance().install_widget(myButton); + +// 切换主题 +ThemeManager::instance().setThemeTo("material.light"); +```text + +## 为什么用接口 + 工厂? + +你可能会问:为什么不直接让 ThemeManager 管理 ICFTheme 实例,非要中间加一个 ThemeFactory? + +原因有几个: + +1. **延迟创建**:主题可能包含大量数据(颜色表、字体缓存),如果不使用就创建会浪费内存。工厂模式下,主题只在第一次访问时创建。 +2. **支持动态注册**:你可以在运行时注册新的主题类型,而不需要修改 ThemeManager 的代码。 +3. **序列化支持**:fromJson/toJson 方法让主题可以被持久化和传输。 + +## 线程安全设计 + +ThemeManager 的单例实现使用了 C++11 的"魔术静态变量": + +```cpp +static ThemeManager& instance() { + static ThemeManager manager; + return manager; +} +```text + +C++11 保证局部静态变量的初始化是线程安全的,所以这个实现不需要额外的锁。 + +但 `install_widget/remove_widget` 和 `setThemeTo` 不是线程安全的。如果需要在多线程环境使用,需要外部同步。不过一般来说,主题操作都在主线程进行,这不是问题。 + +## 主题切换的广播机制 + +当主题切换时,ThemeManager 会发出 `themeChanged` 信号,所有订阅的控件都会收到通知: + +```cpp +// 在控件的构造函数中 +connect(&ThemeManager::instance(), &ThemeManager::themeChanged, + this, [this](const ICFTheme&) { update(); }); +```text + +这里有个设计细节:为什么用 `install_widget` 而不是让控件直接连接信号? + +原因是为了生命周期管理。如果控件直接连接信号,控件销毁时需要手动断开连接,否则会访问野指针。而 `install_widget` 会跟踪控件的生命周期,当控件销毁时自动从订阅列表中移除。 + +实际上我们并没有用 QObject::destroyed 信号,因为那会有循环引用的风险。我们选择在控件析构时手动调用 `remove_widget`,或者在 ThemeManager 里定期清理失效的指针。 + +## 生命周期管理 + +主题实例由 ThemeManager 持有(通过 `theme_cache_`),而主题内的各个组件(颜色方案、字体等)由主题持有。所有权链条是清晰的: + +```text +ThemeManager (owner) + └── unordered_map> theme_cache_ + └── ICFTheme (owner) + ├── unique_ptr color_scheme_ + ├── unique_ptr motion_spec_ + ├── unique_ptr radius_scale_ + └── unique_ptr font_type_ +```yaml + +控件只持有引用(通过 `themeChanged` 信号的参数),不拥有主题的所有权。这样当主题被销毁时,不会有 dangling pointer 的问题。 + +## 下一步 + +到这里,主题系统的骨架就搭好了。但我们还没有讨论具体的实现细节:颜色是怎么存储的?字体是怎么缓存的?Token 系统是怎么工作的? + +在下一篇文章里,我们会深入 Token 系统——那个让 Material Design 3 的颜色访问变得类型安全又高性能的魔法。 + +--- + +**相关文档** + +- [几何与设备无关](../layer-1-math-utility/03-geometry-and-device-pixel.md)——基础层的最后一篇 +- [Token 系统设计](./02-token-system.md)——字符串字面量的编译时魔法 +- [颜色方案实现](./03-color-scheme.md)——从种子颜色到完整主题 diff --git a/document/HandBook/ui/architecture/layer-2-theme-engine/02-token-system.md b/document/HandBook/ui/architecture/layer-2-theme-engine/02-token-system.md index ba022929f..f0116bff1 100644 --- a/document/HandBook/ui/architecture/layer-2-theme-engine/02-token-system.md +++ b/document/HandBook/ui/architecture/layer-2-theme-engine/02-token-system.md @@ -1,252 +1,257 @@ -# Token 系统设计——字符串字面量的编译时魔法 - -在上一篇文章里,我们讲了主题系统的整体架构:ICFTheme 接口定义了主题包含什么,ThemeFactory 负责创建主题,ThemeManager 管理多个主题并处理切换。 - -但有一个问题我们没深入聊:控件怎么访问主题里的数据?直接用字符串查询吗?比如 `theme->getColor("md.primary")`? - -这方案能用,但有几个问题:字符串拼接容易出错、编译期无法检查、运行时查找有性能开销。Material Design 3 有 26 个标准颜色角色,还有各种字体、圆角、动画参数,如果全靠字符串查询,代码里会到处是"魔法字符串"。 - -于是我们设计了 Token 系统。 - -## Token 系统的动机 - -Token 系统的核心思想是:把"字符串标识符"和"编译时类型安全"结合起来。 - -理想情况下,我们希望这样写: - -```cpp -// 定义一个 token -using PrimaryToken = StaticToken; - -// 注册 token -TokenRegistry::get().register_token(QColor("#6200EE")); - -// 使用 token -auto result = PrimaryToken::get(); -if (result) { - QColor color = *result; -} -``` - -这样有几个好处: - -1. **类型安全**:PrimaryToken 明确关联了 QColor 类型,编译器会检查 -2. **零运行时开销**:Hash 是编译时常量,查找用 `unordered_map` -3. **可重构**:如果 token 名字改了,编译器会报错(虽然要手动改 hash) - -## StaticToken:编译时类型安全 - -StaticToken 是一个模板类,它的两个模板参数分别是值类型 `T` 和名字的哈希值 `Hash`: - -```cpp -template class StaticToken { -public: - using value_type = T; - static constexpr uint64_t hash_value = Hash; - - // 删除所有构造函数——这只是一个类型封装,不能实例化 - StaticToken() = delete; - StaticToken(const StaticToken&) = delete; - - // 静态方法获取值 - static cf::expected get(); -}; -``` - -注意这里的设计技巧:StaticToken 的所有构造函数都被删除了。这意味着你不能创建 StaticToken 的实例——它只是一个"类型",用来承载编译时信息。 - -实际使用时,你用的是 `StaticToken::get()` 这个静态方法。 - -## constexpr 哈希 - -为了在编译时计算字符串的哈希,我们用了 FNV-1a 64 位哈希算法,它可以写成 `constexpr` 函数: - -```cpp -constexpr uint64_t fnv1a64(std::string_view str) { - uint64_t hash = 14695981039346656037ULL; - for (char c : str) { - hash ^= static_cast(c); - hash *= 1099511628211ULL; - } - return hash; -} - -// 用户定义字面量(可选) -constexpr uint64_t operator""_hash(const char* str, size_t len) { - return fnv1a64(std::string_view(str, len)); -} -``` - -这样你就可以在编译时计算字符串的哈希: - -```cpp -constexpr uint64_t PRIMARY_HASH = fnv1a64("md.primary"); // 编译时常量 -using PrimaryToken = StaticToken; -``` - -## TokenRegistry:运行时存储 - -StaticToken 只是一个"类型",真正的数据存在 TokenRegistry 里: - -```cpp -class TokenRegistry { -public: - static TokenRegistry& get(); - - // 注册静态 token - template - Result register_token(Args&&... args); - - // 获取静态 token - template - Result get(); - - // 动态 token(运行时字符串) - template - Result register_dynamic(std::string_view name, Args&&... args); - - template - Result get_dynamic(std::string_view name); - -private: - mutable std::shared_mutex registry_mutex_; - std::unordered_map slot_map_; -}; -``` - -TokenRegistry 是一个单例,内部用 `unordered_map` 存储数据,键是哈希值。 - -## 类型擦除与 std::any - -TokenSlot 使用 `std::any` 进行类型擦除: - -```cpp -struct TokenSlot { - std::unique_ptr data; // 类型擦除的值 - const std::type_info* type_info; // 用于类型检查 - std::string name; // 调试用 -}; -``` - -注册时,我们创建一个 `std::any` 存储 `T` 类型的值,同时保存 `typeid(T)` 用于后续的类型检查。 - -获取时,我们先检查 `type_info` 是否匹配,然后用 `std::any_cast` 提取值: - -```cpp -template -auto TokenRegistry::get() -> 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, "..."}); - } - - if (slot->type_info != &typeid(T)) { - return cf::unexpected(TokenError{TokenError::Kind::TypeMismatch, "..."}); - } - - return std::any_cast(slot->data.get()); -} -``` - -## 线程安全设计 - -TokenRegistry 使用 `std::shared_mutex` 来支持多读单写: - -```cpp -// 读操作(get):共享锁 -std::shared_lock lock(registry_mutex_); - -// 写操作(register_token):独占锁 -std::unique_lock lock(registry_mutex_); -``` - -这个设计适合"读多写少"的场景——token 注册通常发生在初始化阶段,之后大部分时候都是读取。 - -`shared_mutex` 的好处是:多个读操作可以并发进行,只有在写操作时才需要独占访问。 - -## constexpr 字面量定义 - -Material Design 3 的 26 个颜色 Token 被定义为 `constexpr` 字面量: - -```cpp -namespace cf::ui::core::token::literals { - -inline constexpr const char PRIMARY[] = "md.primary"; -inline constexpr const char ON_PRIMARY[] = "md.onPrimary"; -inline constexpr const char PRIMARY_CONTAINER[] = "md.primaryContainer"; -// ... 共 26 个 - -inline constexpr const char* const ALL_TOKENS[] = { - PRIMARY, ON_PRIMARY, PRIMARY_CONTAINER, ON_PRIMARY_CONTAINER, - // ... -}; -inline constexpr size_t TOKEN_COUNT = 26; - -} // namespace cf::ui::core::token::literals -``` - -这些 `constexpr` 字面量可以在编译时使用,而且避免了字符串字面量的重复。 - -## ICFColorScheme 的 Token 访问 - -ICFColorScheme 接口的 `queryExpectedColor` 方法就是用 Token 系统实现的: - -```cpp -struct ICFColorScheme { - virtual QColor& queryExpectedColor(const char* name) = 0; - - QColor queryColor(const char* name) const { - return const_cast(this)->queryExpectedColor(name); - } -}; -``` - -MaterialColorScheme 的实现内部会用 TokenRegistry 来查找颜色。虽然这里还是用了字符串参数,但内部实现可以复用 Token 系统,保持一致性。 - -## DynamicToken:运行时类型擦除 - -除了 StaticToken,我们还支持 DynamicToken——完全运行时的 token: - -```cpp -// 注册 -TokenRegistry::get().register_dynamic("custom.color", QColor("#FF0000")); - -// 使用 -auto result = TokenRegistry::get().get_dynamic("custom.color"); -if (result) { - QColor color = *result; -} -``` - -DynamicToken 用的是运行时字符串查找,没有编译时检查,但更加灵活。适合插件系统、用户自定义主题等场景。 - -## 三层 Token 结构 - -在完整的 Material Design 3 体系中,Token 分为三层: - -1. **Reference Token**:Material Design 原始规范的定义,如 `md.primary` -2. **System Token**:内部系统级 Token,如 `sys.color.primary` -3. **Component Token**:组件级 Token,如 `button.filled.container` - -这种分层让系统更加灵活:组件可以定义自己的 Token,而系统 Token 可以被多个组件复用。 - -## 总结 - -Token 系统是一个"编译时类型安全 + 运行时灵活查找"的混合方案。StaticToken 提供零开销的编译时检查,DynamicToken 提供运行时灵活性,两者共享同一个 TokenRegistry 存储。 - -有了 Token 系统,我们就可以规范地访问主题数据了。但 Token 只是"标识符",实际的值怎么来?如何从一个种子颜色生成完整的 Material 主题? - -接下来,我们进入颜色方案的具体实现。 - ---- - -**相关文档** - -- [主题系统架构设计](./01-theme-system-design.md)——主题系统的整体设计 -- [颜色方案实现](./03-color-scheme.md)——从种子颜色到完整主题 -- [HCT 色彩空间实战](../layer-1-math-utility/02-color-system-hct.md)——Layer 1 的颜色数学基础 +--- +title: Token 系统设计——字符串字面量的编译时魔法 +description: 在上一篇文章里,我们讲了主题系统的整体架构:ICFTheme 接口定义了主题包含什么,ThemeFa +--- + +# Token 系统设计——字符串字面量的编译时魔法 + +在上一篇文章里,我们讲了主题系统的整体架构:ICFTheme 接口定义了主题包含什么,ThemeFactory 负责创建主题,ThemeManager 管理多个主题并处理切换。 + +但有一个问题我们没深入聊:控件怎么访问主题里的数据?直接用字符串查询吗?比如 `theme->getColor("md.primary")`? + +这方案能用,但有几个问题:字符串拼接容易出错、编译期无法检查、运行时查找有性能开销。Material Design 3 有 26 个标准颜色角色,还有各种字体、圆角、动画参数,如果全靠字符串查询,代码里会到处是"魔法字符串"。 + +于是我们设计了 Token 系统。 + +## Token 系统的动机 + +Token 系统的核心思想是:把"字符串标识符"和"编译时类型安全"结合起来。 + +理想情况下,我们希望这样写: + +```cpp +// 定义一个 token +using PrimaryToken = StaticToken; + +// 注册 token +TokenRegistry::get().register_token(QColor("#6200EE")); + +// 使用 token +auto result = PrimaryToken::get(); +if (result) { + QColor color = *result; +} +```text + +这样有几个好处: + +1. **类型安全**:PrimaryToken 明确关联了 QColor 类型,编译器会检查 +2. **零运行时开销**:Hash 是编译时常量,查找用 `unordered_map` +3. **可重构**:如果 token 名字改了,编译器会报错(虽然要手动改 hash) + +## StaticToken:编译时类型安全 + +StaticToken 是一个模板类,它的两个模板参数分别是值类型 `T` 和名字的哈希值 `Hash`: + +```cpp +template class StaticToken { +public: + using value_type = T; + static constexpr uint64_t hash_value = Hash; + + // 删除所有构造函数——这只是一个类型封装,不能实例化 + StaticToken() = delete; + StaticToken(const StaticToken&) = delete; + + // 静态方法获取值 + static cf::expected get(); +}; +```text + +注意这里的设计技巧:StaticToken 的所有构造函数都被删除了。这意味着你不能创建 StaticToken 的实例——它只是一个"类型",用来承载编译时信息。 + +实际使用时,你用的是 `StaticToken::get()` 这个静态方法。 + +## constexpr 哈希 + +为了在编译时计算字符串的哈希,我们用了 FNV-1a 64 位哈希算法,它可以写成 `constexpr` 函数: + +```cpp +constexpr uint64_t fnv1a64(std::string_view str) { + uint64_t hash = 14695981039346656037ULL; + for (char c : str) { + hash ^= static_cast(c); + hash *= 1099511628211ULL; + } + return hash; +} + +// 用户定义字面量(可选) +constexpr uint64_t operator""_hash(const char* str, size_t len) { + return fnv1a64(std::string_view(str, len)); +} +```text + +这样你就可以在编译时计算字符串的哈希: + +```cpp +constexpr uint64_t PRIMARY_HASH = fnv1a64("md.primary"); // 编译时常量 +using PrimaryToken = StaticToken; +```text + +## TokenRegistry:运行时存储 + +StaticToken 只是一个"类型",真正的数据存在 TokenRegistry 里: + +```cpp +class TokenRegistry { +public: + static TokenRegistry& get(); + + // 注册静态 token + template + Result register_token(Args&&... args); + + // 获取静态 token + template + Result get(); + + // 动态 token(运行时字符串) + template + Result register_dynamic(std::string_view name, Args&&... args); + + template + Result get_dynamic(std::string_view name); + +private: + mutable std::shared_mutex registry_mutex_; + std::unordered_map slot_map_; +}; +```text + +TokenRegistry 是一个单例,内部用 `unordered_map` 存储数据,键是哈希值。 + +## 类型擦除与 std::any + +TokenSlot 使用 `std::any` 进行类型擦除: + +```cpp +struct TokenSlot { + std::unique_ptr data; // 类型擦除的值 + const std::type_info* type_info; // 用于类型检查 + std::string name; // 调试用 +}; +```text + +注册时,我们创建一个 `std::any` 存储 `T` 类型的值,同时保存 `typeid(T)` 用于后续的类型检查。 + +获取时,我们先检查 `type_info` 是否匹配,然后用 `std::any_cast` 提取值: + +```cpp +template +auto TokenRegistry::get() -> 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, "..."}); + } + + if (slot->type_info != &typeid(T)) { + return cf::unexpected(TokenError{TokenError::Kind::TypeMismatch, "..."}); + } + + return std::any_cast(slot->data.get()); +} +```text + +## 线程安全设计 + +TokenRegistry 使用 `std::shared_mutex` 来支持多读单写: + +```cpp +// 读操作(get):共享锁 +std::shared_lock lock(registry_mutex_); + +// 写操作(register_token):独占锁 +std::unique_lock lock(registry_mutex_); +```text + +这个设计适合"读多写少"的场景——token 注册通常发生在初始化阶段,之后大部分时候都是读取。 + +`shared_mutex` 的好处是:多个读操作可以并发进行,只有在写操作时才需要独占访问。 + +## constexpr 字面量定义 + +Material Design 3 的 26 个颜色 Token 被定义为 `constexpr` 字面量: + +```cpp +namespace cf::ui::core::token::literals { + +inline constexpr const char PRIMARY[] = "md.primary"; +inline constexpr const char ON_PRIMARY[] = "md.onPrimary"; +inline constexpr const char PRIMARY_CONTAINER[] = "md.primaryContainer"; +// ... 共 26 个 + +inline constexpr const char* const ALL_TOKENS[] = { + PRIMARY, ON_PRIMARY, PRIMARY_CONTAINER, ON_PRIMARY_CONTAINER, + // ... +}; +inline constexpr size_t TOKEN_COUNT = 26; + +} // namespace cf::ui::core::token::literals +```text + +这些 `constexpr` 字面量可以在编译时使用,而且避免了字符串字面量的重复。 + +## ICFColorScheme 的 Token 访问 + +ICFColorScheme 接口的 `queryExpectedColor` 方法就是用 Token 系统实现的: + +```cpp +struct ICFColorScheme { + virtual QColor& queryExpectedColor(const char* name) = 0; + + QColor queryColor(const char* name) const { + return const_cast(this)->queryExpectedColor(name); + } +}; +```text + +MaterialColorScheme 的实现内部会用 TokenRegistry 来查找颜色。虽然这里还是用了字符串参数,但内部实现可以复用 Token 系统,保持一致性。 + +## DynamicToken:运行时类型擦除 + +除了 StaticToken,我们还支持 DynamicToken——完全运行时的 token: + +```cpp +// 注册 +TokenRegistry::get().register_dynamic("custom.color", QColor("#FF0000")); + +// 使用 +auto result = TokenRegistry::get().get_dynamic("custom.color"); +if (result) { + QColor color = *result; +} +```yaml + +DynamicToken 用的是运行时字符串查找,没有编译时检查,但更加灵活。适合插件系统、用户自定义主题等场景。 + +## 三层 Token 结构 + +在完整的 Material Design 3 体系中,Token 分为三层: + +1. **Reference Token**:Material Design 原始规范的定义,如 `md.primary` +2. **System Token**:内部系统级 Token,如 `sys.color.primary` +3. **Component Token**:组件级 Token,如 `button.filled.container` + +这种分层让系统更加灵活:组件可以定义自己的 Token,而系统 Token 可以被多个组件复用。 + +## 总结 + +Token 系统是一个"编译时类型安全 + 运行时灵活查找"的混合方案。StaticToken 提供零开销的编译时检查,DynamicToken 提供运行时灵活性,两者共享同一个 TokenRegistry 存储。 + +有了 Token 系统,我们就可以规范地访问主题数据了。但 Token 只是"标识符",实际的值怎么来?如何从一个种子颜色生成完整的 Material 主题? + +接下来,我们进入颜色方案的具体实现。 + +--- + +**相关文档** + +- [主题系统架构设计](./01-theme-system-design.md)——主题系统的整体设计 +- [颜色方案实现](./03-color-scheme.md)——从种子颜色到完整主题 +- [HCT 色彩空间实战](../layer-1-math-utility/02-color-system-hct.md)——Layer 1 的颜色数学基础 diff --git a/document/HandBook/ui/architecture/layer-2-theme-engine/03-color-scheme.md b/document/HandBook/ui/architecture/layer-2-theme-engine/03-color-scheme.md index 1c5006dc9..2dc8c19e2 100644 --- a/document/HandBook/ui/architecture/layer-2-theme-engine/03-color-scheme.md +++ b/document/HandBook/ui/architecture/layer-2-theme-engine/03-color-scheme.md @@ -1,204 +1,209 @@ -# 颜色方案实现——从种子颜色到完整主题 - -在之前的文章里,我们聊了 HCT 色彩空间的数学原理,也讲了 Token 系统的设计。现在我们把两者结合起来:如何从一个"种子颜色"生成完整的 Material Design 3 主题。 - -## Material Design 3 的颜色角色 - -Material Design 3 定义了 26 个标准颜色角色,分为几组: - -- **Primary(4个)**:primary、onPrimary、primaryContainer、onPrimaryContainer -- **Secondary(4个)**:secondary、onSecondary、secondaryContainer、onSecondaryContainer -- **Tertiary(4个)**:tertiary、onTertiary、tertiaryContainer、onTertiaryContainer -- **Error(4个)**:error、onError、errorContainer、onErrorContainer -- **Surface(8个)**:background、onBackground、surface、onSurface、surfaceVariant、onSurfaceVariant、outline、outlineVariant -- **Utility(5个)**:shadow、scrim、inverseSurface、inverseOnSurface、inversePrimary - -这些角色不是随意定义的,它们之间有明确的语义关系。比如 `onX` 表示"在 X 颜色上显示的文本/图标颜色",`XContainer` 表示"使用 X 颜色的容器背景"。 - -## tonalPalette 算法 - -生成主题的核心是 tonalPalette 算法——从一个种子颜色生成 13 个亮度等级。 - -MaterialColorScheme 内部使用了 EmbeddedTokenRegistry 来存储颜色: - -```cpp -class MaterialColorScheme : public ICFColorScheme { -private: - EmbeddedTokenRegistry registry_; - mutable std::unordered_map color_cache_; -}; -``` - -注意这里有两个存储:`registry_` 存储原始的 CFColor(带 HCT 信息),`color_cache_` 存储转换为 QColor 的结果(用于查询缓存)。 - -生成 tonal palette 的过程: - -```cpp -QList tonalPalette(CFColor keyColor) { - float hue = keyColor.hue(); // 保持色相 - float chroma = keyColor.chroma(); // 保持色度 - - // 13 个标准 Tone 值 - constexpr float TONAL_VALUES[] = { - 0.0f, 10.0f, 20.0f, 30.0f, 40.0f, - 50.0f, 60.0f, 70.0f, 80.0f, 90.0f, - 95.0f, 99.0f, 100.0f - }; - - QList palette; - for (int i = 0; i < 13; ++i) { - palette.append(CFColor(hue, chroma, TONAL_VALUES[i])); - } - return palette; -} -``` - -这个算法我们在 Layer 1 里讲过,核心思想是固定 Hue 和 Chroma,只变 Tone。这样生成的 13 个颜色在视觉上是"同一个颜色的不同亮度版本"。 - -## Primary 组的生成 - -Primary 组的颜色来自主种子颜色: - -```cpp -// 生成 Primary tonal palette -QList primaryPalette = tonalPalette(seedColor); - -// 从 tonal palette 中选择特定的 Tone 值 -primary = primaryPalette[Tone 40]; // md.primary -onPrimary = primaryPalette[Tone 100]; // md.onPrimary(白色) -primaryContainer = primaryPalette[Tone 90]; // md.primaryContainer -onPrimaryContainer = primaryPalette[Tone 10]; // md.onPrimaryContainer -``` - -这里有个设计选择:为什么 Primary 选 Tone 40,而 PrimaryContainer 选 Tone 90? - -原因是 Primary 用作"主色",通常是中等亮度;而 PrimaryContainer 用作"主色容器",需要更亮一些,形成对比。具体的 Tone 值选择是 Material Design 3 规范的一部分,经过了大量视觉测试。 - -## Secondary 和 Tertiary 组 - -Secondary 和 Tertiary 组也用同样的算法,但种子颜色不同: - -```cpp -// Secondary:从主种子颜色衍生 -CFColor secondarySeed = deriveSecondary(seedColor); -QList secondaryPalette = tonalPalette(secondarySeed); - -// Tertiary:从主种子颜色衍生(不同的衍生规则) -CFColor tertiarySeed = deriveTertiary(seedColor); -QList tertiaryPalette = tonalPalette(tertiarySeed); -``` - -衍生算法会调整 HCT 值,让 Secondary 和 Tertiary 与 Primary 形成视觉和谐。比如 Secondary 可能旋转色相 30 度,Tertiary 可能旋转 60 度。 - -## Surface 组和 Light/Dark 差异 - -Surface 组(background、surface 等)的处理方式不同。在 Light 主题中,background 通常是接近白色的高亮度颜色;而在 Dark 主题中,background 是接近黑色的低亮度颜色。 - -```cpp -// Light 主题 -background = CFColor(hue, chroma, Tone 98); // 接近白色 -onBackground = CFColor(hue, chroma, Tone 10); // 深色文本 - -// Dark 主题 -background = CFColor(hue, chroma, Tone 10); // 接近黑色 -onBackground = CFColor(hue, chroma, Tone 90); // 浅色文本 -``` - -注意这里虽然用了相同的 `hue` 和 `chroma`,但 Tone 值差异很大。实际上,Surface 颜色通常会使用很低的 chroma(接近中性灰),以避免与内容颜色冲突。 - -## Error 组 - -Error 组使用固定的种子颜色(通常是红色),不随主题变化: - -```cpp -CFColor errorSeed("#B00020"); // Material 标准错误红 -QList errorPalette = tonalPalette(errorSeed); -``` - -## onX 颜色的对比度要求 - -Material Design 3 要求 `onX` 颜色与 X 颜色之间满足 WCAG AA 对比度标准(4.5:1)。这意味着: - -```cpp -float ratio = contrastRatio(primary, onPrimary); -// ratio >= 4.5 必须成立 -``` - -如果 tonalPalette 生成的颜色不满足对比度要求,需要调整。通常的做法是: - -1. 先用 tonalPalette 生成候选颜色 -2. 计算对比度 -3. 如果不满足,向黑色或白色方向调整 Tone 值 -4. 重新计算对比度,直到满足要求 - -## 查询接口 - -MaterialColorScheme 实现了 `queryExpectedColor` 方法: - -```cpp -QColor& MaterialColorScheme::queryExpectedColor(const char* name) { - // 先查缓存 - auto it = color_cache_.find(name); - if (it != color_cache_.end()) { - return it->second; - } - - // 从 registry 获取 CFColor - uint64_t hash = cf::hash::fnv1a64(name); - auto result = registry_.get_dynamic(name); - if (!result) { - // 返回默认颜色 - static QColor defaultColor(Qt::black); - return defaultColor; - } - - // 转换为 QColor 并缓存 - QColor color = (*result)->native_color(); - color_cache_[name] = color; - return color_cache_[name]; -} -``` - -注意这里返回的是引用,意味着调用者不应该修改返回的颜色(否则会影响缓存)。如果需要修改,应该用 `queryColor` 返回副本。 - -## EmbeddedTokenRegistry 的使用 - -MaterialColorScheme 使用 `EmbeddedTokenRegistry` 而不是全局的 `TokenRegistry`,原因是: - -1. **独立性**:每个颜色方案有自己独立的存储,不会互相干扰 -2. **可移动**:EmbeddedTokenRegistry 支持移动语义,整个颜色方案可以被高效地移动 -3. **生命周期管理**:颜色方案销毁时,EmbeddedTokenRegistry 也会自动销毁,无需手动清理 - -## 验证一下 - -创建一个完整的 Material 主题: - -```cpp -// 从品牌色生成 Light 主题 -CFColor brandColor("#6200EE"); -auto lightScheme = material::light(brandColor); - -// 查询颜色 -QColor primary = lightScheme.queryColor("md.primary"); -QColor onPrimary = lightScheme.queryColor("md.onPrimary"); - -// 验证对比度 -float ratio = contrastRatio(primary, onPrimary); -// ratio 应该 >= 4.5 -``` - -## 总结 - -MaterialColorScheme 完整实现了 Material Design 3 的颜色系统,从一个种子颜色生成 26 个标准角色。它使用 tonalPalette 算法生成颜色,用 EmbeddedTokenRegistry 存储颜色,用 color_cache_ 加速查询。 - -但颜色只是主题的一部分。一个完整的主题还需要字体系统、圆角规范、动画参数等组件。 - -接下来,我们聊聊字体、形状与动效。 - ---- - -**相关文档** - -- [Token 系统设计](./02-token-system.md)——字符串字面量的编译时魔法 -- [HCT 色彩空间实战](../layer-1-math-utility/02-color-system-hct.md)——Layer 1 的颜色数学基础 -- [字体、形状与动效](./04-typography-shape-motion.md)——主题的其余组件 +--- +title: 颜色方案实现——从种子颜色到完整主题 +description: 在之前的文章里,我们聊了 HCT 色彩空间的数学原理,也讲了 Token 系统的设计。现在我们把两者 +--- + +# 颜色方案实现——从种子颜色到完整主题 + +在之前的文章里,我们聊了 HCT 色彩空间的数学原理,也讲了 Token 系统的设计。现在我们把两者结合起来:如何从一个"种子颜色"生成完整的 Material Design 3 主题。 + +## Material Design 3 的颜色角色 + +Material Design 3 定义了 26 个标准颜色角色,分为几组: + +- **Primary(4个)**:primary、onPrimary、primaryContainer、onPrimaryContainer +- **Secondary(4个)**:secondary、onSecondary、secondaryContainer、onSecondaryContainer +- **Tertiary(4个)**:tertiary、onTertiary、tertiaryContainer、onTertiaryContainer +- **Error(4个)**:error、onError、errorContainer、onErrorContainer +- **Surface(8个)**:background、onBackground、surface、onSurface、surfaceVariant、onSurfaceVariant、outline、outlineVariant +- **Utility(5个)**:shadow、scrim、inverseSurface、inverseOnSurface、inversePrimary + +这些角色不是随意定义的,它们之间有明确的语义关系。比如 `onX` 表示"在 X 颜色上显示的文本/图标颜色",`XContainer` 表示"使用 X 颜色的容器背景"。 + +## tonalPalette 算法 + +生成主题的核心是 tonalPalette 算法——从一个种子颜色生成 13 个亮度等级。 + +MaterialColorScheme 内部使用了 EmbeddedTokenRegistry 来存储颜色: + +```cpp +class MaterialColorScheme : public ICFColorScheme { +private: + EmbeddedTokenRegistry registry_; + mutable std::unordered_map color_cache_; +}; +```text + +注意这里有两个存储:`registry_` 存储原始的 CFColor(带 HCT 信息),`color_cache_` 存储转换为 QColor 的结果(用于查询缓存)。 + +生成 tonal palette 的过程: + +```cpp +QList tonalPalette(CFColor keyColor) { + float hue = keyColor.hue(); // 保持色相 + float chroma = keyColor.chroma(); // 保持色度 + + // 13 个标准 Tone 值 + constexpr float TONAL_VALUES[] = { + 0.0f, 10.0f, 20.0f, 30.0f, 40.0f, + 50.0f, 60.0f, 70.0f, 80.0f, 90.0f, + 95.0f, 99.0f, 100.0f + }; + + QList palette; + for (int i = 0; i < 13; ++i) { + palette.append(CFColor(hue, chroma, TONAL_VALUES[i])); + } + return palette; +} +```text + +这个算法我们在 Layer 1 里讲过,核心思想是固定 Hue 和 Chroma,只变 Tone。这样生成的 13 个颜色在视觉上是"同一个颜色的不同亮度版本"。 + +## Primary 组的生成 + +Primary 组的颜色来自主种子颜色: + +```cpp +// 生成 Primary tonal palette +QList primaryPalette = tonalPalette(seedColor); + +// 从 tonal palette 中选择特定的 Tone 值 +primary = primaryPalette[Tone 40]; // md.primary +onPrimary = primaryPalette[Tone 100]; // md.onPrimary(白色) +primaryContainer = primaryPalette[Tone 90]; // md.primaryContainer +onPrimaryContainer = primaryPalette[Tone 10]; // md.onPrimaryContainer +```text + +这里有个设计选择:为什么 Primary 选 Tone 40,而 PrimaryContainer 选 Tone 90? + +原因是 Primary 用作"主色",通常是中等亮度;而 PrimaryContainer 用作"主色容器",需要更亮一些,形成对比。具体的 Tone 值选择是 Material Design 3 规范的一部分,经过了大量视觉测试。 + +## Secondary 和 Tertiary 组 + +Secondary 和 Tertiary 组也用同样的算法,但种子颜色不同: + +```cpp +// Secondary:从主种子颜色衍生 +CFColor secondarySeed = deriveSecondary(seedColor); +QList secondaryPalette = tonalPalette(secondarySeed); + +// Tertiary:从主种子颜色衍生(不同的衍生规则) +CFColor tertiarySeed = deriveTertiary(seedColor); +QList tertiaryPalette = tonalPalette(tertiarySeed); +```text + +衍生算法会调整 HCT 值,让 Secondary 和 Tertiary 与 Primary 形成视觉和谐。比如 Secondary 可能旋转色相 30 度,Tertiary 可能旋转 60 度。 + +## Surface 组和 Light/Dark 差异 + +Surface 组(background、surface 等)的处理方式不同。在 Light 主题中,background 通常是接近白色的高亮度颜色;而在 Dark 主题中,background 是接近黑色的低亮度颜色。 + +```cpp +// Light 主题 +background = CFColor(hue, chroma, Tone 98); // 接近白色 +onBackground = CFColor(hue, chroma, Tone 10); // 深色文本 + +// Dark 主题 +background = CFColor(hue, chroma, Tone 10); // 接近黑色 +onBackground = CFColor(hue, chroma, Tone 90); // 浅色文本 +```text + +注意这里虽然用了相同的 `hue` 和 `chroma`,但 Tone 值差异很大。实际上,Surface 颜色通常会使用很低的 chroma(接近中性灰),以避免与内容颜色冲突。 + +## Error 组 + +Error 组使用固定的种子颜色(通常是红色),不随主题变化: + +```cpp +CFColor errorSeed("#B00020"); // Material 标准错误红 +QList errorPalette = tonalPalette(errorSeed); +```text + +## onX 颜色的对比度要求 + +Material Design 3 要求 `onX` 颜色与 X 颜色之间满足 WCAG AA 对比度标准(4.5:1)。这意味着: + +```cpp +float ratio = contrastRatio(primary, onPrimary); +// ratio >= 4.5 必须成立 +```text + +如果 tonalPalette 生成的颜色不满足对比度要求,需要调整。通常的做法是: + +1. 先用 tonalPalette 生成候选颜色 +2. 计算对比度 +3. 如果不满足,向黑色或白色方向调整 Tone 值 +4. 重新计算对比度,直到满足要求 + +## 查询接口 + +MaterialColorScheme 实现了 `queryExpectedColor` 方法: + +```cpp +QColor& MaterialColorScheme::queryExpectedColor(const char* name) { + // 先查缓存 + auto it = color_cache_.find(name); + if (it != color_cache_.end()) { + return it->second; + } + + // 从 registry 获取 CFColor + uint64_t hash = cf::hash::fnv1a64(name); + auto result = registry_.get_dynamic(name); + if (!result) { + // 返回默认颜色 + static QColor defaultColor(Qt::black); + return defaultColor; + } + + // 转换为 QColor 并缓存 + QColor color = (*result)->native_color(); + color_cache_[name] = color; + return color_cache_[name]; +} +```text + +注意这里返回的是引用,意味着调用者不应该修改返回的颜色(否则会影响缓存)。如果需要修改,应该用 `queryColor` 返回副本。 + +## EmbeddedTokenRegistry 的使用 + +MaterialColorScheme 使用 `EmbeddedTokenRegistry` 而不是全局的 `TokenRegistry`,原因是: + +1. **独立性**:每个颜色方案有自己独立的存储,不会互相干扰 +2. **可移动**:EmbeddedTokenRegistry 支持移动语义,整个颜色方案可以被高效地移动 +3. **生命周期管理**:颜色方案销毁时,EmbeddedTokenRegistry 也会自动销毁,无需手动清理 + +## 验证一下 + +创建一个完整的 Material 主题: + +```cpp +// 从品牌色生成 Light 主题 +CFColor brandColor("#6200EE"); +auto lightScheme = material::light(brandColor); + +// 查询颜色 +QColor primary = lightScheme.queryColor("md.primary"); +QColor onPrimary = lightScheme.queryColor("md.onPrimary"); + +// 验证对比度 +float ratio = contrastRatio(primary, onPrimary); +// ratio 应该 >= 4.5 +```yaml + +## 总结 + +MaterialColorScheme 完整实现了 Material Design 3 的颜色系统,从一个种子颜色生成 26 个标准角色。它使用 tonalPalette 算法生成颜色,用 EmbeddedTokenRegistry 存储颜色,用 color_cache_ 加速查询。 + +但颜色只是主题的一部分。一个完整的主题还需要字体系统、圆角规范、动画参数等组件。 + +接下来,我们聊聊字体、形状与动效。 + +--- + +**相关文档** + +- [Token 系统设计](./02-token-system.md)——字符串字面量的编译时魔法 +- [HCT 色彩空间实战](../layer-1-math-utility/02-color-system-hct.md)——Layer 1 的颜色数学基础 +- [字体、形状与动效](./04-typography-shape-motion.md)——主题的其余组件 diff --git a/document/HandBook/ui/architecture/layer-2-theme-engine/04-typography-shape-motion.md b/document/HandBook/ui/architecture/layer-2-theme-engine/04-typography-shape-motion.md index 654fec64b..58e1db3c3 100644 --- a/document/HandBook/ui/architecture/layer-2-theme-engine/04-typography-shape-motion.md +++ b/document/HandBook/ui/architecture/layer-2-theme-engine/04-typography-shape-motion.md @@ -1,203 +1,208 @@ -# 字体、形状与动效——完整的设计规范系统 - -前面几篇文章我们聊了主题系统的架构、Token 的设计、颜色的实现。但 Material Design 3 不只是颜色,它是一套完整的设计规范系统,包括字体、形状、动效等维度。 - -这篇文章聊聊主题系统的其他组件。 - -## ICFTheme 的四个组件 - -回顾一下 ICFTheme 接口,它包含四个组件: - -```cpp -struct ICFTheme { - ICFColorScheme& color_scheme() const; - IMotionSpec& motion_spec() const; - IRadiusScale& radius_scale() const; - IFontType& font_type() const; - -protected: - std::unique_ptr color_scheme_; - std::unique_ptr motion_spec_; - std::unique_ptr radius_scale_; - std::unique_ptr font_type_; -}; -``` - -这四个组件相互独立,但通过主题聚合在一起。控件可以根据需要选择性地使用这些组件。 - -## Material Type Scale 字体分级体系 - -Material Design 3 定义了一套完整的字体分级体系,每个等级都有明确的用途: - -| 名称 | 用途 | 字重 | 大小(sp) | -|---|---|---|---| -| Display Large | 超大标题 | Regular 400 | 57 | -| Display Medium | 大标题 | Regular 400 | 45 | -| Display Small | 中标题 | Regular 400 | 36 | -| Headline Large | 一级标题 | Regular 400 | 32 | -| Headline Medium | 二级标题 | Regular 400 | 28 | -| Headline Small | 三级标题 | Regular 400 | 24 | -| Title Large | 大标题 | Medium 500 | 22 | -| Title Medium | 中标题 | Medium 500 | 16 | -| Title Small | 小标题 | Medium 500 | 14 | -| Body Large | 大正文 | Regular 400 | 16 | -| Body Medium | 中正文 | Regular 400 | 14 | -| Body Small | 小正文 | Regular 400 | 12 | -| Label Large | 大标签 | Medium 500 | 14 | -| Label Medium | 中标签 | Medium 500 | 12 | -| Label Small | 小标签 | Medium 500 | 11 | - -这套分级体系确保了整个应用的文字大小、字重保持一致。 - -## IFontType 接口 - -IFontType 是一个简单的接口,只有一个方法: - -```cpp -struct IFontType { - virtual QFont queryTargetFont(const char* name) = 0; -}; -``` - -使用方式: - -```cpp -// 获取 Body Large 字体 -QFont bodyLarge = theme->font_type().queryTargetFont("bodyLarge"); - -// 设置到控件 -label->setFont(bodyLarge); -``` - -MaterialTypography 的实现内部维护了一个 QFont 的映射表,用 Token 名称作为键。为了保证性能,QFont 对象被缓存起来,避免重复创建。 - -## IRadiusScale 圆角分级 - -Material Design 3 的圆角分为几个等级: - -| 名称 | 值(dp) | -|---|---:| -| None | 0 | -| ExtraSmall | 4 | -| Small | 8 | -| Medium | 12 | -| Large | 16 | -| ExtraLarge | 28 | -| Full | 50% (动态计算) | - -这个分级和我们在 Layer 1 提到的 ShapeScale 枚举是对应的。 - -IRadiusScale 接口也很简单: - -```cpp -struct IRadiusScale { - virtual float queryRadiusScale(const char* name) = 0; -}; -``` - -返回值是 dp 单位,控件需要根据 devicePixelRatio 转换成像素: - -```cpp -float radiusDp = theme->radius_scale().queryRadiusScale("cornerMedium"); -CanvasUnitHelper helper(qApp->devicePixelRatio()); -float radiusPx = helper.dpToPx(radiusDp); -``` - -注意这里有个设计细节:IRadiusScale 返回的是 dp,不是 px。原因是圆角应该在所有 DPI 下保持一致的"视觉大小",所以应该由控件根据当前的 devicePixelRatio 进行转换。 - -## IMotionSpec 动画规范 - -Material Design 3 的动画系统基于"时长 + 缓动"的二维配置。每个动画都有三个参数: - -- **Duration**:动画持续时间(毫秒) -- **Easing**:缓动曲线类型 -- **Delay**:延迟时间(毫秒,可选) - -IMotionSpec 接口提供了三个查询方法: - -```cpp -struct IMotionSpec { - virtual int queryDuration(const char* name) = 0; - virtual int queryEasing(const char* name) = 0; - virtual int queryDelay(const char* name) = 0; -}; -``` - -使用方式: - -```cpp -// 获取 "md.motion.shortEnter" 的动画参数 -int duration = theme->motion_spec().queryDuration("md.motion.shortEnter"); -int easing = theme->motion_spec().queryEasing("md.motion.shortEnter"); -int delay = theme->motion_spec().queryDelay("md.motion.shortEnter"); -``` - -Material Design 3 定义了几种标准动画: - -| 名称 | Duration | Easing | -|---|---:|---| -| shortEnter | 200ms | Emphasized | -| mediumEnter | 250ms | Emphasized | -| longEnter | 300ms | Emphasized | -| shortExit | 150ms | Standard | -| mediumExit | 200ms | Standard | -| longExit | 250ms | Standard | - -easing 返回的是一个整数,对应我们在 Layer 1 定义的 Easing::Type 枚举。这样做的原因是保持接口的跨语言兼容性——QFont、QColor 等类型在绑定到其他语言时可能有问题,而整数是通用的。 - -## 组合使用 - -控件在实际使用时,会同时访问多个组件: - -```cpp -void Button::paintEvent(QPaintEvent* event) { - // 获取颜色 - QColor containerColor = theme->color_scheme().queryColor("md.primaryContainer"); - QColor labelColor = theme->color_scheme().queryColor("md.onPrimaryContainer"); - - // 获取字体 - QFont labelFont = theme->font_type().queryTargetFont("labelLarge"); - - // 获取圆角 - float radiusDp = theme->radius_scale().queryRadiusScale("cornerFull"); - - // 获取动画参数(用于状态切换) - int duration = theme->motion_spec().queryDuration("md.motion.shortEnter"); - - // ... 使用这些值绘制 -} -``` - -## 扩展性 - -这套设计的一个好处是扩展性。如果你想添加一个新的主题类型,只需要: - -1. 实现 ICFColorScheme、IFontType、IRadiusScale、IMotionSpec 接口 -2. 实现一个 ThemeFactory 来创建主题 -3. 将工厂注册到 ThemeManager - -不需要修改任何控件代码,因为控件只依赖接口,不依赖具体实现。 - -## 总结 - -到这里,Layer 2(Theme Engine Layer)的内容就基本覆盖了。我们有了: - -- 主题系统的整体架构(ICFTheme + ThemeFactory + ThemeManager) -- Token 系统的设计(StaticToken + DynamicToken + TokenRegistry) -- 颜色方案的具体实现(MaterialColorScheme + tonalPalette) -- 字体、圆角、动画的接口定义 - -这套系统让控件能够以一致的方式访问主题数据,支持动态主题切换,并且有良好的扩展性。 - -但有了主题只是第一步,控件还需要行为层的支持——状态管理、涟漪效果、阴影绘制等。 - -接下来,我们进入 Layer 3:Animation Engine Layer,看看统一的动画系统是如何设计的。 - ---- - -**相关文档** - -- [主题系统架构设计](./01-theme-system-design.md)——主题系统的整体设计 -- [Token 系统设计](./02-token-system.md)——字符串字面量的编译时魔法 -- [颜色方案实现](./03-color-scheme.md)——从种子颜色到完整主题 -- [动画引擎架构](../layer-3-animation-engine/01-animation-architecture.md)——下一层的入口 +--- +title: 字体、形状与动效——完整的设计规范系统 +description: 前面几篇文章我们聊了主题系统的架构、Token 的设计、颜色的实现。但 Material Desig +--- + +# 字体、形状与动效——完整的设计规范系统 + +前面几篇文章我们聊了主题系统的架构、Token 的设计、颜色的实现。但 Material Design 3 不只是颜色,它是一套完整的设计规范系统,包括字体、形状、动效等维度。 + +这篇文章聊聊主题系统的其他组件。 + +## ICFTheme 的四个组件 + +回顾一下 ICFTheme 接口,它包含四个组件: + +```cpp +struct ICFTheme { + ICFColorScheme& color_scheme() const; + IMotionSpec& motion_spec() const; + IRadiusScale& radius_scale() const; + IFontType& font_type() const; + +protected: + std::unique_ptr color_scheme_; + std::unique_ptr motion_spec_; + std::unique_ptr radius_scale_; + std::unique_ptr font_type_; +}; +```bash + +这四个组件相互独立,但通过主题聚合在一起。控件可以根据需要选择性地使用这些组件。 + +## Material Type Scale 字体分级体系 + +Material Design 3 定义了一套完整的字体分级体系,每个等级都有明确的用途: + +| 名称 | 用途 | 字重 | 大小(sp) | +|---|---|---|---| +| Display Large | 超大标题 | Regular 400 | 57 | +| Display Medium | 大标题 | Regular 400 | 45 | +| Display Small | 中标题 | Regular 400 | 36 | +| Headline Large | 一级标题 | Regular 400 | 32 | +| Headline Medium | 二级标题 | Regular 400 | 28 | +| Headline Small | 三级标题 | Regular 400 | 24 | +| Title Large | 大标题 | Medium 500 | 22 | +| Title Medium | 中标题 | Medium 500 | 16 | +| Title Small | 小标题 | Medium 500 | 14 | +| Body Large | 大正文 | Regular 400 | 16 | +| Body Medium | 中正文 | Regular 400 | 14 | +| Body Small | 小正文 | Regular 400 | 12 | +| Label Large | 大标签 | Medium 500 | 14 | +| Label Medium | 中标签 | Medium 500 | 12 | +| Label Small | 小标签 | Medium 500 | 11 | + +这套分级体系确保了整个应用的文字大小、字重保持一致。 + +## IFontType 接口 + +IFontType 是一个简单的接口,只有一个方法: + +```cpp +struct IFontType { + virtual QFont queryTargetFont(const char* name) = 0; +}; +```text + +使用方式: + +```cpp +// 获取 Body Large 字体 +QFont bodyLarge = theme->font_type().queryTargetFont("bodyLarge"); + +// 设置到控件 +label->setFont(bodyLarge); +```bash + +MaterialTypography 的实现内部维护了一个 QFont 的映射表,用 Token 名称作为键。为了保证性能,QFont 对象被缓存起来,避免重复创建。 + +## IRadiusScale 圆角分级 + +Material Design 3 的圆角分为几个等级: + +| 名称 | 值(dp) | +|---|---:| +| None | 0 | +| ExtraSmall | 4 | +| Small | 8 | +| Medium | 12 | +| Large | 16 | +| ExtraLarge | 28 | +| Full | 50% (动态计算) | + +这个分级和我们在 Layer 1 提到的 ShapeScale 枚举是对应的。 + +IRadiusScale 接口也很简单: + +```cpp +struct IRadiusScale { + virtual float queryRadiusScale(const char* name) = 0; +}; +```text + +返回值是 dp 单位,控件需要根据 devicePixelRatio 转换成像素: + +```cpp +float radiusDp = theme->radius_scale().queryRadiusScale("cornerMedium"); +CanvasUnitHelper helper(qApp->devicePixelRatio()); +float radiusPx = helper.dpToPx(radiusDp); +```text + +注意这里有个设计细节:IRadiusScale 返回的是 dp,不是 px。原因是圆角应该在所有 DPI 下保持一致的"视觉大小",所以应该由控件根据当前的 devicePixelRatio 进行转换。 + +## IMotionSpec 动画规范 + +Material Design 3 的动画系统基于"时长 + 缓动"的二维配置。每个动画都有三个参数: + +- **Duration**:动画持续时间(毫秒) +- **Easing**:缓动曲线类型 +- **Delay**:延迟时间(毫秒,可选) + +IMotionSpec 接口提供了三个查询方法: + +```cpp +struct IMotionSpec { + virtual int queryDuration(const char* name) = 0; + virtual int queryEasing(const char* name) = 0; + virtual int queryDelay(const char* name) = 0; +}; +```text + +使用方式: + +```cpp +// 获取 "md.motion.shortEnter" 的动画参数 +int duration = theme->motion_spec().queryDuration("md.motion.shortEnter"); +int easing = theme->motion_spec().queryEasing("md.motion.shortEnter"); +int delay = theme->motion_spec().queryDelay("md.motion.shortEnter"); +```bash + +Material Design 3 定义了几种标准动画: + +| 名称 | Duration | Easing | +|---|---:|---| +| shortEnter | 200ms | Emphasized | +| mediumEnter | 250ms | Emphasized | +| longEnter | 300ms | Emphasized | +| shortExit | 150ms | Standard | +| mediumExit | 200ms | Standard | +| longExit | 250ms | Standard | + +easing 返回的是一个整数,对应我们在 Layer 1 定义的 Easing::Type 枚举。这样做的原因是保持接口的跨语言兼容性——QFont、QColor 等类型在绑定到其他语言时可能有问题,而整数是通用的。 + +## 组合使用 + +控件在实际使用时,会同时访问多个组件: + +```cpp +void Button::paintEvent(QPaintEvent* event) { + // 获取颜色 + QColor containerColor = theme->color_scheme().queryColor("md.primaryContainer"); + QColor labelColor = theme->color_scheme().queryColor("md.onPrimaryContainer"); + + // 获取字体 + QFont labelFont = theme->font_type().queryTargetFont("labelLarge"); + + // 获取圆角 + float radiusDp = theme->radius_scale().queryRadiusScale("cornerFull"); + + // 获取动画参数(用于状态切换) + int duration = theme->motion_spec().queryDuration("md.motion.shortEnter"); + + // ... 使用这些值绘制 +} +```yaml + +## 扩展性 + +这套设计的一个好处是扩展性。如果你想添加一个新的主题类型,只需要: + +1. 实现 ICFColorScheme、IFontType、IRadiusScale、IMotionSpec 接口 +2. 实现一个 ThemeFactory 来创建主题 +3. 将工厂注册到 ThemeManager + +不需要修改任何控件代码,因为控件只依赖接口,不依赖具体实现。 + +## 总结 + +到这里,Layer 2(Theme Engine Layer)的内容就基本覆盖了。我们有了: + +- 主题系统的整体架构(ICFTheme + ThemeFactory + ThemeManager) +- Token 系统的设计(StaticToken + DynamicToken + TokenRegistry) +- 颜色方案的具体实现(MaterialColorScheme + tonalPalette) +- 字体、圆角、动画的接口定义 + +这套系统让控件能够以一致的方式访问主题数据,支持动态主题切换,并且有良好的扩展性。 + +但有了主题只是第一步,控件还需要行为层的支持——状态管理、涟漪效果、阴影绘制等。 + +接下来,我们进入 Layer 3:Animation Engine Layer,看看统一的动画系统是如何设计的。 + +--- + +**相关文档** + +- [主题系统架构设计](./01-theme-system-design.md)——主题系统的整体设计 +- [Token 系统设计](./02-token-system.md)——字符串字面量的编译时魔法 +- [颜色方案实现](./03-color-scheme.md)——从种子颜色到完整主题 +- [动画引擎架构](../layer-3-animation-engine/01-animation-architecture.md)——下一层的入口 diff --git a/document/HandBook/ui/architecture/layer-2-theme-engine/index.md b/document/HandBook/ui/architecture/layer-2-theme-engine/index.md index 34f7ecf77..464145394 100644 --- a/document/HandBook/ui/architecture/layer-2-theme-engine/index.md +++ b/document/HandBook/ui/architecture/layer-2-theme-engine/index.md @@ -1,10 +1,11 @@ -# layer-2-theme-engine - -> Welcome to the layer-2-theme-engine section. +--- +title: "Layer 2: 主题引擎" +description: 本章节介绍 UI 渲染管线的第二层——主题引擎,涵盖 主题管理器、 Material 工厂以及 D +--- -## Overview +# Layer 2: 主题引擎 -Documentation and resources for layer-2-theme-engine. +本章节介绍 UI 渲染管线的第二层——主题引擎,涵盖 `ThemeManager` 主题管理器、`MaterialFactory` Material 工厂以及 Design Token 的加载与分发机制。主题引擎负责将设计规范转化为运行时可查询的 Token 配置,驱动全局主题切换。 --- diff --git a/document/HandBook/ui/architecture/layer-3-animation-engine/.pages b/document/HandBook/ui/architecture/layer-3-animation-engine/.pages deleted file mode 100644 index 544b7afa3..000000000 --- a/document/HandBook/ui/architecture/layer-3-animation-engine/.pages +++ /dev/null @@ -1,5 +0,0 @@ -title: 第三层 · 动画引擎 -nav: - - 动画架构: 01-animation-architecture.md - - Timing 与 Spring 动画: 02-timing-spring-animation.md - - 工厂与策略模式: 03-factory-and-strategy.md diff --git a/document/HandBook/ui/architecture/layer-3-animation-engine/01-animation-architecture.md b/document/HandBook/ui/architecture/layer-3-animation-engine/01-animation-architecture.md index c30aee776..2026e1ef2 100644 --- a/document/HandBook/ui/architecture/layer-3-animation-engine/01-animation-architecture.md +++ b/document/HandBook/ui/architecture/layer-3-animation-engine/01-animation-architecture.md @@ -1,171 +1,176 @@ -# 动画引擎架构——统一调度的生命线管理 - -在 Layer 2 里,我们聊了主题系统的各个组件。但有了颜色和字体还不够,Material Design 3 的灵魂在于动效——那些流畅的过渡、自然的弹性运动。 - -这篇文章聊聊动画引擎的架构设计。 - -## 为什么需要统一的动画引擎? - -Qt 已经提供了 QPropertyAnimation、QVariantAnimation 等动画类,为什么还要自己实现? - -有几个原因: - -1. **Material Design 3 的特殊需求**:MD3 定义了一套标准的动画时长和缓动曲线(shortEnter、mediumEnter 等),Qt 的默认配置不匹配。 -2. **生命周期管理问题**:如果每个控件都自己创建动画,很容易出现生命周期混乱——控件销毁了但动画还在运行,或者动画结束了但控件已经不存在了。 -3. **性能优化**:统一的动画引擎可以全局控制动画开关(比如用户禁用动画、嵌入式环境精简),避免零散的动画难以管理。 -4. **弹簧动画支持**:Qt 没有提供弹簧动画的原生支持,而 MD3 大量使用弹簧效果。 - -## ICFAbstractAnimation:动画基类 - -所有动画都继承自 `ICFAbstractAnimation`,它定义了动画的核心接口: - -```cpp -class ICFAbstractAnimation : public QObject { -public: - enum class State { Idle, Running, Paused, Finished }; - enum class Direction { Forward, Backward }; - - virtual void start(Direction dir = Direction::Forward) = 0; - virtual void pause() = 0; - virtual void stop() = 0; - virtual void reverse() = 0; - virtual bool tick(int dt) = 0; - virtual cf::WeakPtr GetWeakPtr() = 0; - -signals: - void started(); - void paused(); - void stopped(); - void finished(); - void progressChanged(float progress); - -protected: - float m_progress = 0.0f; - State m_state = State::Idle; -}; -``` - -这里有个关键设计:`GetWeakPtr()` 返回的是弱引用。原因我们后面会讲,但核心思想是:动画由工厂拥有所有权,用户只持有弱引用。 - -## 动画状态机 - -动画有四种状态: - -- **Idle**:动画尚未开始或已停止 -- **Running**:动画正在运行 -- **Paused**:动画已暂停 -- **Finished**:动画已完成 - -状态转换遵循以下规则: - -``` -Idle → Running (start) -Running → Paused (pause) -Paused → Running (start) -Running/Paused → Idle (stop) -Running → Finished (自然结束) -``` - -## Direction 方向控制 - -动画支持两种播放方向: - -- **Forward**:从 `from` 值到 `to` 值 -- **Backward**:从 `to` 值到 `from` 值 - -`reverse()` 方法会停止当前动画,然后以相反方向重新播放。这对于"鼠标进入淡入、鼠标离开淡出"的场景非常实用。 - -## tick() 虚函数 - -`tick(int dt)` 是动画的核心更新方法,每帧调用一次。`dt` 是距离上一帧的时间间隔(毫秒)。 - -每个具体的动画类需要实现自己的 `tick()` 逻辑: - -- TimingAnimation:根据 `m_elapsed` 和缓动曲线计算进度 -- SpringAnimation:使用弹簧物理公式计算位置和速度 - -`tick()` 返回 `true` 表示动画继续,`false` 表示动画已完成。 - -## WeakPtr 所有权模型 - -这是一个关键设计。动画的所有权结构是: - -``` -CFMaterialAnimationFactory (owner) - └── unordered_map> animations_ - -Controls - └── WeakPtr -``` - -工厂拥有动画的所有权(`unique_ptr`),控件只持有弱引用(`WeakPtr`)。这样设计的好处是: - -1. **生命周期安全**:控件销毁时,弱引用自动失效,不会访问野指针 -2. **统一管理**:工厂销毁时,所有动画自动销毁 -3. **共享访问**:多个控件可以共享同一个动画实例 - -获取动画的典型流程: - -```cpp -auto anim = factory->getAnimation("md.animation.fadeIn"); -if (anim) { // 检查 WeakPtr 是否有效 - connect(anim.get(), &ICFAbstractAnimation::progressChanged, - this, [this](float progress) { - update(); - }); - anim->start(); -} -``` - -## progressChanged 信号 - -每次 `tick()` 更新进度后,动画会发出 `progressChanged` 信号。控件可以连接这个信号来更新 UI。 - -注意这里用的是信号机制而不是 QProperty。原因是: - -1. **性能**:信号机制比 QProperty 更轻量 -2. **灵活性**:控件可以选择是否监听进度变化 -3. **兼容性**:不依赖 Qt 6 的新特性 - -## 全局动画开关 - -ICFAnimationManagerFactory 提供了全局开关: - -```cpp -factory->setEnabledAll(false); // 禁用所有动画 -factory->setEnabledAll(true); // 启用所有动画 -``` - -禁用时,`getAnimation()` 会返回无效的 WeakPtr,这样就不会创建新动画。已有的正在运行的动画不受影响,会自然完成。 - -这个功能对以下场景很有用: - -- **性能优化**:繁重任务期间禁用动画 -- **无障碍**:尊重系统的"减少动画"设置 -- **用户偏好**:提供一个"禁用动画"的选项 - -## 目标 FPS 设置 - -工厂可以设置目标 FPS: - -```cpp -factory->setTargetFps(60.0f); // 60 FPS -``` - -这会影响动画的定时器间隔。更高的 FPS 意味着更平滑的动画,但也意味着更多的 CPU 开销。 - -## 总结 - -动画引擎的核心设计是"统一调度 + WeakPtr 所有权"。工厂拥有动画,用户持有弱引用,这样既保证了生命周期安全,又提供了灵活的访问方式。 - -但抽象基类只是定义接口,具体的动画还需要实现。Material Design 3 用两种主要的动画范式:基于时间的动画和基于弹簧的动画。 - -接下来,我们深入这两种动画的具体实现。 - ---- - -**相关文档** - -- [字体、形状与动效](../layer-2-theme-engine/04-typography-shape-motion.md)——Layer 2 的动效接口 -- [时间与弹簧动画](./02-timing-spring-animation.md)——两种动画范式的实现 -- [工厂与策略](./03-factory-and-strategy.md)——动画创建的灵活组合 +--- +title: 动画引擎架构——统一调度的生命线管理 +description: 在 Layer 2 里,我们聊了主题系统的各个组件。但有了颜色和字体还不够,Material Des +--- + +# 动画引擎架构——统一调度的生命线管理 + +在 Layer 2 里,我们聊了主题系统的各个组件。但有了颜色和字体还不够,Material Design 3 的灵魂在于动效——那些流畅的过渡、自然的弹性运动。 + +这篇文章聊聊动画引擎的架构设计。 + +## 为什么需要统一的动画引擎? + +Qt 已经提供了 QPropertyAnimation、QVariantAnimation 等动画类,为什么还要自己实现? + +有几个原因: + +1. **Material Design 3 的特殊需求**:MD3 定义了一套标准的动画时长和缓动曲线(shortEnter、mediumEnter 等),Qt 的默认配置不匹配。 +2. **生命周期管理问题**:如果每个控件都自己创建动画,很容易出现生命周期混乱——控件销毁了但动画还在运行,或者动画结束了但控件已经不存在了。 +3. **性能优化**:统一的动画引擎可以全局控制动画开关(比如用户禁用动画、嵌入式环境精简),避免零散的动画难以管理。 +4. **弹簧动画支持**:Qt 没有提供弹簧动画的原生支持,而 MD3 大量使用弹簧效果。 + +## ICFAbstractAnimation:动画基类 + +所有动画都继承自 `ICFAbstractAnimation`,它定义了动画的核心接口: + +```cpp +class ICFAbstractAnimation : public QObject { +public: + enum class State { Idle, Running, Paused, Finished }; + enum class Direction { Forward, Backward }; + + virtual void start(Direction dir = Direction::Forward) = 0; + virtual void pause() = 0; + virtual void stop() = 0; + virtual void reverse() = 0; + virtual bool tick(int dt) = 0; + virtual cf::WeakPtr GetWeakPtr() = 0; + +signals: + void started(); + void paused(); + void stopped(); + void finished(); + void progressChanged(float progress); + +protected: + float m_progress = 0.0f; + State m_state = State::Idle; +}; +```text + +这里有个关键设计:`GetWeakPtr()` 返回的是弱引用。原因我们后面会讲,但核心思想是:动画由工厂拥有所有权,用户只持有弱引用。 + +## 动画状态机 + +动画有四种状态: + +- **Idle**:动画尚未开始或已停止 +- **Running**:动画正在运行 +- **Paused**:动画已暂停 +- **Finished**:动画已完成 + +状态转换遵循以下规则: + +```text +Idle → Running (start) +Running → Paused (pause) +Paused → Running (start) +Running/Paused → Idle (stop) +Running → Finished (自然结束) +```text + +## Direction 方向控制 + +动画支持两种播放方向: + +- **Forward**:从 `from` 值到 `to` 值 +- **Backward**:从 `to` 值到 `from` 值 + +`reverse()` 方法会停止当前动画,然后以相反方向重新播放。这对于"鼠标进入淡入、鼠标离开淡出"的场景非常实用。 + +## tick() 虚函数 + +`tick(int dt)` 是动画的核心更新方法,每帧调用一次。`dt` 是距离上一帧的时间间隔(毫秒)。 + +每个具体的动画类需要实现自己的 `tick()` 逻辑: + +- TimingAnimation:根据 `m_elapsed` 和缓动曲线计算进度 +- SpringAnimation:使用弹簧物理公式计算位置和速度 + +`tick()` 返回 `true` 表示动画继续,`false` 表示动画已完成。 + +## WeakPtr 所有权模型 + +这是一个关键设计。动画的所有权结构是: + +```text +CFMaterialAnimationFactory (owner) + └── unordered_map> animations_ + +Controls + └── WeakPtr +```text + +工厂拥有动画的所有权(`unique_ptr`),控件只持有弱引用(`WeakPtr`)。这样设计的好处是: + +1. **生命周期安全**:控件销毁时,弱引用自动失效,不会访问野指针 +2. **统一管理**:工厂销毁时,所有动画自动销毁 +3. **共享访问**:多个控件可以共享同一个动画实例 + +获取动画的典型流程: + +```cpp +auto anim = factory->getAnimation("md.animation.fadeIn"); +if (anim) { // 检查 WeakPtr 是否有效 + connect(anim.get(), &ICFAbstractAnimation::progressChanged, + this, [this](float progress) { + update(); + }); + anim->start(); +} +```text + +## progressChanged 信号 + +每次 `tick()` 更新进度后,动画会发出 `progressChanged` 信号。控件可以连接这个信号来更新 UI。 + +注意这里用的是信号机制而不是 QProperty。原因是: + +1. **性能**:信号机制比 QProperty 更轻量 +2. **灵活性**:控件可以选择是否监听进度变化 +3. **兼容性**:不依赖 Qt 6 的新特性 + +## 全局动画开关 + +ICFAnimationManagerFactory 提供了全局开关: + +```cpp +factory->setEnabledAll(false); // 禁用所有动画 +factory->setEnabledAll(true); // 启用所有动画 +```text + +禁用时,`getAnimation()` 会返回无效的 WeakPtr,这样就不会创建新动画。已有的正在运行的动画不受影响,会自然完成。 + +这个功能对以下场景很有用: + +- **性能优化**:繁重任务期间禁用动画 +- **无障碍**:尊重系统的"减少动画"设置 +- **用户偏好**:提供一个"禁用动画"的选项 + +## 目标 FPS 设置 + +工厂可以设置目标 FPS: + +```cpp +factory->setTargetFps(60.0f); // 60 FPS +```yaml + +这会影响动画的定时器间隔。更高的 FPS 意味着更平滑的动画,但也意味着更多的 CPU 开销。 + +## 总结 + +动画引擎的核心设计是"统一调度 + WeakPtr 所有权"。工厂拥有动画,用户持有弱引用,这样既保证了生命周期安全,又提供了灵活的访问方式。 + +但抽象基类只是定义接口,具体的动画还需要实现。Material Design 3 用两种主要的动画范式:基于时间的动画和基于弹簧的动画。 + +接下来,我们深入这两种动画的具体实现。 + +--- + +**相关文档** + +- [字体、形状与动效](../layer-2-theme-engine/04-typography-shape-motion.md)——Layer 2 的动效接口 +- [时间与弹簧动画](./02-timing-spring-animation.md)——两种动画范式的实现 +- [工厂与策略](./03-factory-and-strategy.md)——动画创建的灵活组合 diff --git a/document/HandBook/ui/architecture/layer-3-animation-engine/02-timing-spring-animation.md b/document/HandBook/ui/architecture/layer-3-animation-engine/02-timing-spring-animation.md index 807850ee2..c8c7379f5 100644 --- a/document/HandBook/ui/architecture/layer-3-animation-engine/02-timing-spring-animation.md +++ b/document/HandBook/ui/architecture/layer-3-animation-engine/02-timing-spring-animation.md @@ -1,230 +1,235 @@ -# 时间与弹簧——两种动画范式的完整实现 - -在上一篇文章里,我们讲了动画引擎的抽象架构。这篇文章聊聊两种具体的动画实现:基于时间的动画和基于弹簧的动画。 - -## ICFTimingAnimation:时间驱动的动画 - -ICFTimingAnimation 是基于时间的动画,它的核心思想是:给定一个时长和一条缓动曲线,在规定的时间内从 `from` 值过渡到 `to` 值。 - -```cpp -class ICFTimingAnimation : public ICFAbstractAnimation { -public: - explicit ICFTimingAnimation(IMotionSpec* spec, QObject* parent = nullptr); - - virtual void setRange(float from, float to) { - m_from = from; - m_to = to; - } - - virtual float currentValue() const = 0; - -protected: - IMotionSpec* motion_spec_ = nullptr; // 用于获取时长和缓动 - float m_from = 0.0f; - float m_to = 1.0f; - int m_elapsed = 0; // 已过时间(毫秒) -}; -``` - -注意 `motion_spec_` 是一个原始指针。这是一个设计决策:IMotionSpec 的生命周期必须比动画长。在 CFMaterialAnimationFactory 中,这个条件是满足的,因为工厂持有对主题的引用,而主题拥有 MotionSpec。 - -## tick() 实现逻辑 - -ICFTimingAnimation 的 `tick()` 实现大致如下: - -```cpp -bool ICFTimingAnimation::tick(int dt) { - m_elapsed += dt; - - // 从 MotionSpec 获取时长 - int duration = motion_spec_->queryDuration(m_motionToken); - float progress = std::min(m_elapsed / (float)duration, 1.0f); - - // 从 MotionSpec 获取缓动类型 - int easingType = motion_spec_->queryEasing(m_motionToken); - QEasingCurve curve = Easing::fromEasingType(static_cast(easingType)); - - // 应用缓动曲线 - float easedProgress = curve.valueForProgress(progress); - - // 计算当前值 - m_value = lerp(m_from, m_to, easedProgress); - - // 更新进度并发出信号 - m_progress = easedProgress; - emit progressChanged(m_progress); - - return m_elapsed < duration; // 返回 false 表示动画结束 -} -``` - -这里的关键是 `valueForProgress()`,它根据缓动曲线将 [0, 1] 的线性进度映射到非线性进度。 - -## 缓动曲线的应用 - -Material Design 3 定义了几种标准缓动: - -- **Emphasized**:快速启动,缓慢结束(强调进入) -- **Standard**:适中的加速减速 -- **Linear**:匀速运动 -- **Legacy**:Material Design 2 的缓动(兼容性考虑) - -在 Layer 1 我们实现了这些缓动的 QEasingCurve 封装。动画直接使用这些预定义的曲线,确保整个应用的动画风格一致。 - -## ICFSpringAnimation:弹簧驱动的动画 - -ICFSpringAnimation 使用弹簧物理来模拟自然的弹性运动。与时间驱动的动画不同,弹簧动画没有固定的时长——它会"自然地"收敛到目标值。 - -```cpp -class ICFSpringAnimation : public ICFAbstractAnimation { -public: - ICFSpringAnimation(const Easing::SpringPreset& easing, QObject* parent = nullptr); - - virtual void setTarget(float target) { m_target = target; } - virtual void setInitialVelocity(float velocity) { m_velocity = velocity; } - virtual float currentValue() const = 0; - - bool tick(int dt) override; // 使用 springStep - -protected: - Easing::SpringPreset easing_; // 包含 stiffness 和 damping - float m_position = 0.0f; - float m_velocity = 0.0f; - float m_target = 1.0f; -}; -``` - -## springStep 物理模拟 - -弹簧动画的核心是 `springStep` 函数,它使用半隐式欧拉积分法模拟弹簧物理: - -```cpp -std::pair springStep(float position, float velocity, float target, - float stiffness, float damping, float dt) { - // 计算弹簧力 - float force = (target - position) * stiffness; - - // 计算阻尼力 - float dampingForce = -velocity * damping; - - // 总加速度 - float acceleration = force + dampingForce; - - // 更新速度(半隐式欧拉) - float newVelocity = velocity + acceleration * dt; - - // 更新位置 - float newPosition = position + newVelocity * dt; - - return {newPosition, newVelocity}; -} -``` - -这个算法在 Layer 1 讲过,关键点是: - -1. **力与加速度成正比**:胡克定律 F = kx -2. **阻尼力与速度成正比**:防止永远振荡 -3. **半隐式欧拉**:先更新速度,再更新位置,比标准欧拉法更稳定 - -## 收敛判断 - -弹簧动画没有固定的时长,所以需要一个收敛判断: - -```cpp -bool ICFSpringAnimation::tick(int dt) { - // 转换 dt 到秒 - float dtSeconds = dt / 1000.0f; - - // 执行物理步进 - auto [newPos, newVel] = springStep( - m_position, m_velocity, m_target, - easing_.stiffness, easing_.damping, - dtSeconds - ); - - m_position = newPos; - m_velocity = newVel; - - // 计算当前进度(近似) - float displacement = m_target - m_from; - float currentDisp = m_position - m_from; - m_progress = (displacement != 0) ? currentDisp / displacement : 1.0f; - - emit progressChanged(m_progress); - - // 收敛判断:速度很小且接近目标 - bool isConverged = std::abs(m_velocity) < 0.01f && - std::abs(m_target - m_position) < 0.01f; - - return !isConverged; -} -``` - -收敛的条件是速度足够小且距离目标足够近。阈值 0.01 是经验值,可以根据需要调整。 - -## SpringPreset 弹簧预设 - -Material Design 3 定义了几种弹簧预设: - -```cpp -namespace Easing { - struct SpringPreset { - float stiffness; - float damping; - }; - - SpringPreset springGentle(); // 温和的弹性 - SpringPreset springBouncy(); // 明显的弹性 - SpringPreset springStiff(); // 僵硬的弹性 -} -``` - -不同的预设适用于不同的场景:按钮点击用 gentle,对话框进入用 bouncy,列表滚动用 stiff。 - -## 选择哪种动画? - -一个常见的问题是:什么时候用 TimingAnimation,什么时候用 SpringAnimation? - -粗略的原则是: - -- **UI 过渡**(淡入淡出、滑动):用 TimingAnimation -- **物理交互**(拖拽释放、弹性动画):用 SpringAnimation -- **标准场景**:优先用 TimingAnimation(更可控) -- **强调效果**:用 SpringAnimation(更生动) - -## 实际使用示例 - -控件中使用这两种动画的方式类似: - -```cpp -// TimingAnimation:淡入效果 -auto fadeAnim = factory->getAnimation("md.animation.fadeIn"); -if (fadeAnim) { - connect(fadeAnim.get(), &ICFAbstractAnimation::progressChanged, - this, [this](float progress) { - m_opacity = progress; - update(); - }); - fadeAnim->start(); -} - -// SpringAnimation:弹性缩放 -// 需要先注册自定义弹簧动画 -// 或者使用预设的弹簧动画 token -``` - -## 总结 - -TimingAnimation 和 SpringAnimation 是两种互补的动画范式。前者提供可预测的时间驱动动画,后者提供自然的物理驱动动画。它们共享同一个基类接口,可以无缝切换。 - -但有了具体的动画类型还不够,我们需要一个系统来创建和管理这些动画——这就是工厂和策略模式的作用。 - -接下来,我们聊聊动画工厂的设计。 - ---- - -**相关文档** - -- [动画引擎架构](./01-animation-architecture.md)——动画系统的整体设计 -- [工厂与策略](./03-factory-and-strategy.md)——动画创建的灵活组合 -- [弹簧物理的数学基础](../layer-1-math-utility/01-why-we-need-own-math-layer.md)——Layer 1 的数学工具 +--- +title: 时间与弹簧——两种动画范式的完整实现 +description: 在上一篇文章里,我们讲了动画引擎的抽象架构。这篇文章聊聊两种具体的动画实现:基于时间的动画和基于弹簧 +--- + +# 时间与弹簧——两种动画范式的完整实现 + +在上一篇文章里,我们讲了动画引擎的抽象架构。这篇文章聊聊两种具体的动画实现:基于时间的动画和基于弹簧的动画。 + +## ICFTimingAnimation:时间驱动的动画 + +ICFTimingAnimation 是基于时间的动画,它的核心思想是:给定一个时长和一条缓动曲线,在规定的时间内从 `from` 值过渡到 `to` 值。 + +```cpp +class ICFTimingAnimation : public ICFAbstractAnimation { +public: + explicit ICFTimingAnimation(IMotionSpec* spec, QObject* parent = nullptr); + + virtual void setRange(float from, float to) { + m_from = from; + m_to = to; + } + + virtual float currentValue() const = 0; + +protected: + IMotionSpec* motion_spec_ = nullptr; // 用于获取时长和缓动 + float m_from = 0.0f; + float m_to = 1.0f; + int m_elapsed = 0; // 已过时间(毫秒) +}; +```text + +注意 `motion_spec_` 是一个原始指针。这是一个设计决策:IMotionSpec 的生命周期必须比动画长。在 CFMaterialAnimationFactory 中,这个条件是满足的,因为工厂持有对主题的引用,而主题拥有 MotionSpec。 + +## tick() 实现逻辑 + +ICFTimingAnimation 的 `tick()` 实现大致如下: + +```cpp +bool ICFTimingAnimation::tick(int dt) { + m_elapsed += dt; + + // 从 MotionSpec 获取时长 + int duration = motion_spec_->queryDuration(m_motionToken); + float progress = std::min(m_elapsed / (float)duration, 1.0f); + + // 从 MotionSpec 获取缓动类型 + int easingType = motion_spec_->queryEasing(m_motionToken); + QEasingCurve curve = Easing::fromEasingType(static_cast(easingType)); + + // 应用缓动曲线 + float easedProgress = curve.valueForProgress(progress); + + // 计算当前值 + m_value = lerp(m_from, m_to, easedProgress); + + // 更新进度并发出信号 + m_progress = easedProgress; + emit progressChanged(m_progress); + + return m_elapsed < duration; // 返回 false 表示动画结束 +} +```text + +这里的关键是 `valueForProgress()`,它根据缓动曲线将 [0, 1] 的线性进度映射到非线性进度。 + +## 缓动曲线的应用 + +Material Design 3 定义了几种标准缓动: + +- **Emphasized**:快速启动,缓慢结束(强调进入) +- **Standard**:适中的加速减速 +- **Linear**:匀速运动 +- **Legacy**:Material Design 2 的缓动(兼容性考虑) + +在 Layer 1 我们实现了这些缓动的 QEasingCurve 封装。动画直接使用这些预定义的曲线,确保整个应用的动画风格一致。 + +## ICFSpringAnimation:弹簧驱动的动画 + +ICFSpringAnimation 使用弹簧物理来模拟自然的弹性运动。与时间驱动的动画不同,弹簧动画没有固定的时长——它会"自然地"收敛到目标值。 + +```cpp +class ICFSpringAnimation : public ICFAbstractAnimation { +public: + ICFSpringAnimation(const Easing::SpringPreset& easing, QObject* parent = nullptr); + + virtual void setTarget(float target) { m_target = target; } + virtual void setInitialVelocity(float velocity) { m_velocity = velocity; } + virtual float currentValue() const = 0; + + bool tick(int dt) override; // 使用 springStep + +protected: + Easing::SpringPreset easing_; // 包含 stiffness 和 damping + float m_position = 0.0f; + float m_velocity = 0.0f; + float m_target = 1.0f; +}; +```text + +## springStep 物理模拟 + +弹簧动画的核心是 `springStep` 函数,它使用半隐式欧拉积分法模拟弹簧物理: + +```cpp +std::pair springStep(float position, float velocity, float target, + float stiffness, float damping, float dt) { + // 计算弹簧力 + float force = (target - position) * stiffness; + + // 计算阻尼力 + float dampingForce = -velocity * damping; + + // 总加速度 + float acceleration = force + dampingForce; + + // 更新速度(半隐式欧拉) + float newVelocity = velocity + acceleration * dt; + + // 更新位置 + float newPosition = position + newVelocity * dt; + + return {newPosition, newVelocity}; +} +```text + +这个算法在 Layer 1 讲过,关键点是: + +1. **力与加速度成正比**:胡克定律 F = kx +2. **阻尼力与速度成正比**:防止永远振荡 +3. **半隐式欧拉**:先更新速度,再更新位置,比标准欧拉法更稳定 + +## 收敛判断 + +弹簧动画没有固定的时长,所以需要一个收敛判断: + +```cpp +bool ICFSpringAnimation::tick(int dt) { + // 转换 dt 到秒 + float dtSeconds = dt / 1000.0f; + + // 执行物理步进 + auto [newPos, newVel] = springStep( + m_position, m_velocity, m_target, + easing_.stiffness, easing_.damping, + dtSeconds + ); + + m_position = newPos; + m_velocity = newVel; + + // 计算当前进度(近似) + float displacement = m_target - m_from; + float currentDisp = m_position - m_from; + m_progress = (displacement != 0) ? currentDisp / displacement : 1.0f; + + emit progressChanged(m_progress); + + // 收敛判断:速度很小且接近目标 + bool isConverged = std::abs(m_velocity) < 0.01f && + std::abs(m_target - m_position) < 0.01f; + + return !isConverged; +} +```text + +收敛的条件是速度足够小且距离目标足够近。阈值 0.01 是经验值,可以根据需要调整。 + +## SpringPreset 弹簧预设 + +Material Design 3 定义了几种弹簧预设: + +```cpp +namespace Easing { + struct SpringPreset { + float stiffness; + float damping; + }; + + SpringPreset springGentle(); // 温和的弹性 + SpringPreset springBouncy(); // 明显的弹性 + SpringPreset springStiff(); // 僵硬的弹性 +} +```text + +不同的预设适用于不同的场景:按钮点击用 gentle,对话框进入用 bouncy,列表滚动用 stiff。 + +## 选择哪种动画? + +一个常见的问题是:什么时候用 TimingAnimation,什么时候用 SpringAnimation? + +粗略的原则是: + +- **UI 过渡**(淡入淡出、滑动):用 TimingAnimation +- **物理交互**(拖拽释放、弹性动画):用 SpringAnimation +- **标准场景**:优先用 TimingAnimation(更可控) +- **强调效果**:用 SpringAnimation(更生动) + +## 实际使用示例 + +控件中使用这两种动画的方式类似: + +```cpp +// TimingAnimation:淡入效果 +auto fadeAnim = factory->getAnimation("md.animation.fadeIn"); +if (fadeAnim) { + connect(fadeAnim.get(), &ICFAbstractAnimation::progressChanged, + this, [this](float progress) { + m_opacity = progress; + update(); + }); + fadeAnim->start(); +} + +// SpringAnimation:弹性缩放 +// 需要先注册自定义弹簧动画 +// 或者使用预设的弹簧动画 token +```yaml + +## 总结 + +TimingAnimation 和 SpringAnimation 是两种互补的动画范式。前者提供可预测的时间驱动动画,后者提供自然的物理驱动动画。它们共享同一个基类接口,可以无缝切换。 + +但有了具体的动画类型还不够,我们需要一个系统来创建和管理这些动画——这就是工厂和策略模式的作用。 + +接下来,我们聊聊动画工厂的设计。 + +--- + +**相关文档** + +- [动画引擎架构](./01-animation-architecture.md)——动画系统的整体设计 +- [工厂与策略](./03-factory-and-strategy.md)——动画创建的灵活组合 +- [弹簧物理的数学基础](../layer-1-math-utility/01-why-we-need-own-math-layer.md)——Layer 1 的数学工具 diff --git a/document/HandBook/ui/architecture/layer-3-animation-engine/03-factory-and-strategy.md b/document/HandBook/ui/architecture/layer-3-animation-engine/03-factory-and-strategy.md index f93723ce2..feaed5b1d 100644 --- a/document/HandBook/ui/architecture/layer-3-animation-engine/03-factory-and-strategy.md +++ b/document/HandBook/ui/architecture/layer-3-animation-engine/03-factory-and-strategy.md @@ -1,248 +1,253 @@ -# 工厂与策略——动画创建的灵活组合模式 - -在前面的文章里,我们讲了动画的抽象基类和两种具体实现。但控件怎么获取这些动画?直接 `new` 一个吗? - -不,那样会让控件和动画类型耦合,难以扩展和测试。我们用了工厂模式 + 策略模式的组合来解决这个问题。 - -## CFMaterialAnimationFactory 的职责 - -CFMaterialAnimationFactory 是 Material Design 3 动画的核心工厂,它负责: - -1. **Token 解析**:将 "md.animation.fadeIn" 这样的 token 转换为动画描述 -2. **动画创建**:根据描述创建具体的动画实例 -3. **生命周期管理**:拥有创建的动画,提供 WeakPtr 访问 -4. **全局开关**:支持启用/禁用所有动画 -5. **策略应用**:允许控件类型定制动画行为 - -```cpp -class CFMaterialAnimationFactory : public ICFAnimationManagerFactory { -public: - explicit CFMaterialAnimationFactory(const ICFTheme& theme, - std::unique_ptr strategy = nullptr, - QObject* parent = nullptr); - - cf::WeakPtr getAnimation(const char* animationToken) override; - - void setStrategy(std::unique_ptr strategy); - void setEnabledAll(bool enabled) override; - -private: - const ICFTheme& theme_; - std::unique_ptr strategy_; - bool globalEnabled_ = true; - std::unordered_map> animations_; -}; -``` - -## Token 到 AnimationDescriptor 的映射 - -Material Design 3 定义了标准的动画 Token,比如: - -| Token | 类型 | MotionSpec | -|---|---|---| -| md.animation.fadeIn | fade | shortEnter | -| md.animation.fadeOut | fade | shortExit | -| md.animation.slideUp | slide | mediumEnter | -| md.animation.slideDown | slide | mediumExit | -| md.animation.scaleUp | scale | shortEnter | -| md.animation.scaleDown | scale | shortExit | - -工厂内部维护了一个映射表,将 Token 转换为 AnimationDescriptor: - -```cpp -struct AnimationDescriptor { - QString type; // "fade", "slide", "scale" - QString motionToken; // "md.motion.shortEnter" 等 - QString targetProperty; // "opacity", "position", "scale" - float fromValue; - float toValue; -}; -``` - -## getAnimation() 的完整流程 - -当控件调用 `getAnimation("md.animation.fadeIn")` 时,工厂会执行以下步骤: - -1. 检查 `globalEnabled_`,如果为 false 返回无效 WeakPtr -2. 检查是否已有同名动画,如果有返回缓存的动画 -3. 解析 Token 到 AnimationDescriptor -4. 应用策略(如果设置了)调整描述符 -5. 根据类型创建具体的动画实例 -6. 存储动画(获得所有权) -7. 返回 WeakPtr 给调用者 - -```cpp -cf::WeakPtr CFMaterialAnimationFactory::getAnimation(const char* token) { - // 1. 全局开关检查 - if (!globalEnabled_) { - return cf::WeakPtr(); - } - - // 2. 查找缓存 - auto it = animations_.find(token); - if (it != animations_.end()) { - return it->second->GetWeakPtr(); - } - - // 3. 解析 Token - AnimationDescriptor desc = resolveToken(token); - if (desc.type.isEmpty()) { - return cf::WeakPtr(); - } - - // 4-5. 应用策略并创建动画 - desc = applyStrategy(desc, nullptr); - auto anim = createAnimation(desc, nullptr); - - if (!anim) { - return cf::WeakPtr(); - } - - // 6. 存储动画 - animations_[token] = std::move(anim); - - // 7. 返回 WeakPtr - return animations_[token]->GetWeakPtr(); -} -``` - -## 动画实例的创建 - -工厂根据 `AnimationDescriptor::type` 创建不同类型的动画: - -```cpp -std::unique_ptr CFMaterialAnimationFactory::createFadeAnimation( - const AnimationDescriptor& desc, QWidget* widget) { - - auto motionSpec = const_cast(&theme_.motion_spec()); - auto anim = std::make_unique(motionSpec, this); - - anim->setRange(desc.fromValue, desc.toValue); - anim->setMotionToken(desc.motionToken.toStdString()); - - return anim; -} - -std::unique_ptr CFMaterialAnimationFactory::createSlideAnimation(...) { - // 类似的逻辑,但创建的是 CFMaterialSlideAnimation -} - -std::unique_ptr CFMaterialAnimationFactory::createScaleAnimation(...) { - // 创建 CFMaterialScaleAnimation -} -``` - -## AnimationStrategy 策略模式 - -有时候,不同类型的控件需要不同的动画行为。比如按钮可能需要更快的动画,对话框可能需要更慢的动画。 - -策略模式允许在不修改工厂代码的情况下定制动画行为: - -```cpp -class AnimationStrategy { -public: - virtual ~AnimationStrategy() = default; - - virtual AnimationDescriptor adjust(const AnimationDescriptor& desc, - QWidget* widget) { - return desc; // 默认不做修改 - } - - virtual bool shouldEnable(QWidget* widget) { - return true; // 默认启用动画 - } -}; -``` - -控件类型可以实现自己的策略: - -```cpp -class ButtonAnimationStrategy : public AnimationStrategy { -public: - AnimationDescriptor adjust(const AnimationDescriptor& desc, QWidget* widget) override { - // 按钮的动画更快 - AnimationDescriptor adjusted = desc; - adjusted.fromValue = desc.fromValue * 0.8f; - adjusted.toValue = desc.toValue * 1.2f; - return adjusted; - } -}; - -class DialogAnimationStrategy : public AnimationStrategy { -public: - AnimationDescriptor adjust(const AnimationDescriptor& desc, QWidget* widget) override { - // 对话框的动画更慢、更平滑 - AnimationDescriptor adjusted = desc; - adjusted.motionToken = "md.motion.longEnter"; - return adjusted; - } -}; -``` - -使用策略: - -```cpp -// 创建工厂时设置策略 -auto buttonStrategy = std::make_unique(); -auto factory = std::make_unique(theme, std::move(buttonStrategy)); - -// 控件获取动画时会自动应用策略 -auto anim = factory->getAnimation("md.animation.fadeIn"); -``` - -## 策略的应用时机 - -策略在动画创建之前应用,这样可以在创建前调整参数。但策略只影响新创建的动画,已经创建的动画不受影响。 - -如果需要动态更换策略,可以再次调用 `setStrategy()`,之后创建的动画会使用新策略。 - -## 全局动画开关 - -工厂提供了全局开关功能: - -```cpp -factory->setEnabledAll(false); // 禁用所有动画 -``` - -禁用时,`getAnimation()` 会返回无效的 WeakPtr。已有的正在运行的动画不受影响,会自然完成。 - -这个功能对以下场景很有用: - -1. **性能优化**:繁重任务期间禁用动画 -2. **无障碍**:尊重系统的"减少动画"设置 -3. **用户偏好**:提供一个"禁用动画"的设置选项 -4. **嵌入式环境**:资源受限时禁用所有动画 - -## 单个动画的启用/禁用 - -工厂也支持单个动画的启用/禁用: - -```cpp -factory->setTargetEnabled("md.animation.fadeIn", false); -``` - -这样只有特定的动画被禁用,其他动画正常运行。 - -## 动画缓存 - -工厂会缓存已创建的动画。当多次调用 `getAnimation("md.animation.fadeIn")` 时,会返回同一个动画实例的 WeakPtr。 - -这意味着如果你需要多个独立的动画实例,需要用不同的 Token 名称或者使用 `createAnimation()` 直接创建。 - -## 总结 - -工厂模式 + 策略模式的组合让动画系统既统一又灵活。工厂负责创建和管理动画,策略负责定制行为。控件只需要通过 Token 获取动画,不需要关心具体的实现细节。 - -到这里,Layer 3(Animation Engine Layer)的内容就基本覆盖了。我们有了统一的动画调度系统,有了两种动画类型,有了工厂和策略模式。 - -但动画只是"效果",还需要有东西来触发这些动画——比如鼠标悬停、点击、焦点变化等交互行为。 - -接下来,我们进入 Layer 4:Material Behavior Layer,看看如何将 Qt 事件映射到 Material 视觉状态。 - ---- - -**相关文档** - -- [时间与弹簧动画](./02-timing-spring-animation.md)——两种动画范式的实现 -- [状态机设计](../layer-4-material-behavior/01-state-machine.md)——下一层的入口 -- [动画引擎架构](./01-animation-architecture.md)——动画系统的整体设计 +--- +title: 工厂与策略——动画创建的灵活组合模式 +description: 在前面的文章里,我们讲了动画的抽象基类和两种具体实现。但控件怎么获取这些动画?直接 一个吗? +--- + +# 工厂与策略——动画创建的灵活组合模式 + +在前面的文章里,我们讲了动画的抽象基类和两种具体实现。但控件怎么获取这些动画?直接 `new` 一个吗? + +不,那样会让控件和动画类型耦合,难以扩展和测试。我们用了工厂模式 + 策略模式的组合来解决这个问题。 + +## CFMaterialAnimationFactory 的职责 + +CFMaterialAnimationFactory 是 Material Design 3 动画的核心工厂,它负责: + +1. **Token 解析**:将 "md.animation.fadeIn" 这样的 token 转换为动画描述 +2. **动画创建**:根据描述创建具体的动画实例 +3. **生命周期管理**:拥有创建的动画,提供 WeakPtr 访问 +4. **全局开关**:支持启用/禁用所有动画 +5. **策略应用**:允许控件类型定制动画行为 + +```cpp +class CFMaterialAnimationFactory : public ICFAnimationManagerFactory { +public: + explicit CFMaterialAnimationFactory(const ICFTheme& theme, + std::unique_ptr strategy = nullptr, + QObject* parent = nullptr); + + cf::WeakPtr getAnimation(const char* animationToken) override; + + void setStrategy(std::unique_ptr strategy); + void setEnabledAll(bool enabled) override; + +private: + const ICFTheme& theme_; + std::unique_ptr strategy_; + bool globalEnabled_ = true; + std::unordered_map> animations_; +}; +```bash + +## Token 到 AnimationDescriptor 的映射 + +Material Design 3 定义了标准的动画 Token,比如: + +| Token | 类型 | MotionSpec | +|---|---|---| +| md.animation.fadeIn | fade | shortEnter | +| md.animation.fadeOut | fade | shortExit | +| md.animation.slideUp | slide | mediumEnter | +| md.animation.slideDown | slide | mediumExit | +| md.animation.scaleUp | scale | shortEnter | +| md.animation.scaleDown | scale | shortExit | + +工厂内部维护了一个映射表,将 Token 转换为 AnimationDescriptor: + +```cpp +struct AnimationDescriptor { + QString type; // "fade", "slide", "scale" + QString motionToken; // "md.motion.shortEnter" 等 + QString targetProperty; // "opacity", "position", "scale" + float fromValue; + float toValue; +}; +```text + +## getAnimation() 的完整流程 + +当控件调用 `getAnimation("md.animation.fadeIn")` 时,工厂会执行以下步骤: + +1. 检查 `globalEnabled_`,如果为 false 返回无效 WeakPtr +2. 检查是否已有同名动画,如果有返回缓存的动画 +3. 解析 Token 到 AnimationDescriptor +4. 应用策略(如果设置了)调整描述符 +5. 根据类型创建具体的动画实例 +6. 存储动画(获得所有权) +7. 返回 WeakPtr 给调用者 + +```cpp +cf::WeakPtr CFMaterialAnimationFactory::getAnimation(const char* token) { + // 1. 全局开关检查 + if (!globalEnabled_) { + return cf::WeakPtr(); + } + + // 2. 查找缓存 + auto it = animations_.find(token); + if (it != animations_.end()) { + return it->second->GetWeakPtr(); + } + + // 3. 解析 Token + AnimationDescriptor desc = resolveToken(token); + if (desc.type.isEmpty()) { + return cf::WeakPtr(); + } + + // 4-5. 应用策略并创建动画 + desc = applyStrategy(desc, nullptr); + auto anim = createAnimation(desc, nullptr); + + if (!anim) { + return cf::WeakPtr(); + } + + // 6. 存储动画 + animations_[token] = std::move(anim); + + // 7. 返回 WeakPtr + return animations_[token]->GetWeakPtr(); +} +```text + +## 动画实例的创建 + +工厂根据 `AnimationDescriptor::type` 创建不同类型的动画: + +```cpp +std::unique_ptr CFMaterialAnimationFactory::createFadeAnimation( + const AnimationDescriptor& desc, QWidget* widget) { + + auto motionSpec = const_cast(&theme_.motion_spec()); + auto anim = std::make_unique(motionSpec, this); + + anim->setRange(desc.fromValue, desc.toValue); + anim->setMotionToken(desc.motionToken.toStdString()); + + return anim; +} + +std::unique_ptr CFMaterialAnimationFactory::createSlideAnimation(...) { + // 类似的逻辑,但创建的是 CFMaterialSlideAnimation +} + +std::unique_ptr CFMaterialAnimationFactory::createScaleAnimation(...) { + // 创建 CFMaterialScaleAnimation +} +```text + +## AnimationStrategy 策略模式 + +有时候,不同类型的控件需要不同的动画行为。比如按钮可能需要更快的动画,对话框可能需要更慢的动画。 + +策略模式允许在不修改工厂代码的情况下定制动画行为: + +```cpp +class AnimationStrategy { +public: + virtual ~AnimationStrategy() = default; + + virtual AnimationDescriptor adjust(const AnimationDescriptor& desc, + QWidget* widget) { + return desc; // 默认不做修改 + } + + virtual bool shouldEnable(QWidget* widget) { + return true; // 默认启用动画 + } +}; +```text + +控件类型可以实现自己的策略: + +```cpp +class ButtonAnimationStrategy : public AnimationStrategy { +public: + AnimationDescriptor adjust(const AnimationDescriptor& desc, QWidget* widget) override { + // 按钮的动画更快 + AnimationDescriptor adjusted = desc; + adjusted.fromValue = desc.fromValue * 0.8f; + adjusted.toValue = desc.toValue * 1.2f; + return adjusted; + } +}; + +class DialogAnimationStrategy : public AnimationStrategy { +public: + AnimationDescriptor adjust(const AnimationDescriptor& desc, QWidget* widget) override { + // 对话框的动画更慢、更平滑 + AnimationDescriptor adjusted = desc; + adjusted.motionToken = "md.motion.longEnter"; + return adjusted; + } +}; +```text + +使用策略: + +```cpp +// 创建工厂时设置策略 +auto buttonStrategy = std::make_unique(); +auto factory = std::make_unique(theme, std::move(buttonStrategy)); + +// 控件获取动画时会自动应用策略 +auto anim = factory->getAnimation("md.animation.fadeIn"); +```text + +## 策略的应用时机 + +策略在动画创建之前应用,这样可以在创建前调整参数。但策略只影响新创建的动画,已经创建的动画不受影响。 + +如果需要动态更换策略,可以再次调用 `setStrategy()`,之后创建的动画会使用新策略。 + +## 全局动画开关 + +工厂提供了全局开关功能: + +```cpp +factory->setEnabledAll(false); // 禁用所有动画 +```text + +禁用时,`getAnimation()` 会返回无效的 WeakPtr。已有的正在运行的动画不受影响,会自然完成。 + +这个功能对以下场景很有用: + +1. **性能优化**:繁重任务期间禁用动画 +2. **无障碍**:尊重系统的"减少动画"设置 +3. **用户偏好**:提供一个"禁用动画"的设置选项 +4. **嵌入式环境**:资源受限时禁用所有动画 + +## 单个动画的启用/禁用 + +工厂也支持单个动画的启用/禁用: + +```cpp +factory->setTargetEnabled("md.animation.fadeIn", false); +```yaml + +这样只有特定的动画被禁用,其他动画正常运行。 + +## 动画缓存 + +工厂会缓存已创建的动画。当多次调用 `getAnimation("md.animation.fadeIn")` 时,会返回同一个动画实例的 WeakPtr。 + +这意味着如果你需要多个独立的动画实例,需要用不同的 Token 名称或者使用 `createAnimation()` 直接创建。 + +## 总结 + +工厂模式 + 策略模式的组合让动画系统既统一又灵活。工厂负责创建和管理动画,策略负责定制行为。控件只需要通过 Token 获取动画,不需要关心具体的实现细节。 + +到这里,Layer 3(Animation Engine Layer)的内容就基本覆盖了。我们有了统一的动画调度系统,有了两种动画类型,有了工厂和策略模式。 + +但动画只是"效果",还需要有东西来触发这些动画——比如鼠标悬停、点击、焦点变化等交互行为。 + +接下来,我们进入 Layer 4:Material Behavior Layer,看看如何将 Qt 事件映射到 Material 视觉状态。 + +--- + +**相关文档** + +- [时间与弹簧动画](./02-timing-spring-animation.md)——两种动画范式的实现 +- [状态机设计](../layer-4-material-behavior/01-state-machine.md)——下一层的入口 +- [动画引擎架构](./01-animation-architecture.md)——动画系统的整体设计 diff --git a/document/HandBook/ui/architecture/layer-3-animation-engine/index.md b/document/HandBook/ui/architecture/layer-3-animation-engine/index.md index 2ea20759b..97bec95c8 100644 --- a/document/HandBook/ui/architecture/layer-3-animation-engine/index.md +++ b/document/HandBook/ui/architecture/layer-3-animation-engine/index.md @@ -1,10 +1,11 @@ -# layer-3-animation-engine - -> Welcome to the layer-3-animation-engine section. +--- +title: "Layer 3: 动画引擎" +description: 本章节介绍 UI 渲染管线的第三层——动画引擎,涵盖 动画工厂和多种动画策略(Animation +--- -## Overview +# Layer 3: 动画引擎 -Documentation and resources for layer-3-animation-engine. +本章节介绍 UI 渲染管线的第三层——动画引擎,涵盖 `CFMaterialAnimationFactory` 动画工厂和多种动画策略(Animation Strategy)的实现。动画引擎通过策略模式提供可插拔的动效组合,支持弹性、衰减、关键帧等多种动画类型。 --- diff --git a/document/HandBook/ui/architecture/layer-4-material-behavior/.pages b/document/HandBook/ui/architecture/layer-4-material-behavior/.pages deleted file mode 100644 index f199575e9..000000000 --- a/document/HandBook/ui/architecture/layer-4-material-behavior/.pages +++ /dev/null @@ -1,5 +0,0 @@ -title: 第四层 · Material 行为 -nav: - - 状态机: 01-state-machine.md - - Ripple 与 Elevation: 02-ripple-and-elevation.md - - 焦点指示器: 03-focus-indicator.md diff --git a/document/HandBook/ui/architecture/layer-4-material-behavior/01-state-machine.md b/document/HandBook/ui/architecture/layer-4-material-behavior/01-state-machine.md index 8fe53d960..40dd49b6f 100644 --- a/document/HandBook/ui/architecture/layer-4-material-behavior/01-state-machine.md +++ b/document/HandBook/ui/architecture/layer-4-material-behavior/01-state-machine.md @@ -1,193 +1,198 @@ -# 状态机设计——Material 交互状态的核心管理器 - -在 Layer 3 里,我们讲了动画引擎的完整实现。但动画不会凭空触发——它们需要响应鼠标悬停、点击、焦点变化等交互事件。 - -这篇文章聊聊 Material 行为层的核心组件:StateMachine。 - -## 为什么需要独立的状态机? - -Qt 有 QStateMachine,为什么还要自己实现? - -有几个原因: - -1. **Qt QStateMachine 过于重量级**:它是一个通用的状态机框架,对于 Material 的简单状态管理来说太复杂了 -2. **Material 的特殊需求**:Material 定义了特定的状态透明度值(0.00、0.08、0.12 等),需要精确控制 -3. **状态叠加问题**:Material 的状态可以叠加(比如同时 Hovered 和 Checked),需要位运算处理 -4. **动画集成**:状态切换需要触发透明度动画,需要与我们的动画系统无缝集成 - -## Material 的交互状态 - -StateMachine 定义了 7 种交互状态: - -```cpp -enum class State { - StateNormal = 0x00, // 默认状态 - StateHovered = 0x01, // 鼠标悬停 - StatePressed = 0x02, // 鼠标按下 - StateFocused = 0x04, // 键盘焦点 - StateDisabled = 0x08, // 禁用状态 - StateChecked = 0x10, // 选中状态 - StateDragged = 0x20 // 拖拽状态 -}; -``` - -注意这里使用了位掩码设计,每个状态是一个 2 的幂次方。这样多个状态可以通过按位或运算组合。 - -## 状态优先级 - -Material Design 3 定义了状态的优先级顺序: - -``` -Disabled > Pressed > Dragged > Focused > Hovered > Normal -``` - -这意味着如果一个控件同时是 Disabled 和 Hovered, Disabled 状态会"赢",透明度由 Disabled 决定(通常是 0.00)。 - -## 状态透明度规范 - -每个状态对应一个特定的透明度值,用于 StateLayer 的叠加: - -| 状态 | 透明度 | 说明 | -|---|---:|---| -| Normal | 0.00 | 默认状态,无叠加 | -| Hovered | 0.08 | 鼠标悬停时的视觉反馈 | -| Pressed | 0.12 | 按下时的视觉反馈 | -| Focused | 0.12 | 焦点状态(同 Pressed) | -| Dragged | 0.16 | 拖拽时的视觉反馈 | -| Checked | 0.08 | 选中状态(同 Hovered) | -| Disabled | 0.00 | 禁用状态(无叠加) | - -这些透明度值是 Material Design 3 规范的一部分,不应该随意修改。 - -## 事件处理接口 - -StateMachine 提供了一系列事件处理方法: - -```cpp -void onHoverEnter(); -void onHoverLeave(); -void onPress(const QPoint& pos); -void onRelease(); -void onFocusIn(); -void onFocusOut(); -void onEnable(); -void onDisable(); -void onCheckedChanged(bool checked); -``` - -这些方法对应 Qt 的各种事件,控件在事件处理函数中调用它们: - -```cpp -void Button::enterEvent(QEnterEvent* event) { - QPushButton::enterEvent(event); // 先调用父类方法 - m_stateMachine->onHoverEnter(); -} - -void Button::mousePressEvent(QMouseEvent* event) { - QPushButton::mousePressEvent(event); - m_stateMachine->onPress(event->pos()); -} -``` - -注意这里的一个关键点:必须先调用父类方法。否则 Qt 的信号机制会被破坏,比如 `clicked()` 信号可能不会发出。 - -## 状态切换动画 - -当状态改变时,StateMachine 会触发透明度动画: - -```cpp -void StateMachine::animateOpacityTo(float from, float to) { - auto anim = m_animator->getAnimation("md.animation.fadeIn"); - if (anim) { - // 创建一个自定义动画,从 from 到 to - connect(anim.get(), &ICFAbstractAnimation::progressChanged, - this, [this, from, to](float progress) { - m_opacity = lerp(from, to, progress); - emit stateLayerOpacityChanged(m_opacity); - }); - anim->start(); - } -} -``` - -如果动画系统被禁用,会直接设置目标透明度: - -```cpp -m_opacity = to; -emit stateLayerOpacityChanged(m_opacity); -``` - -## 状态优先级的实现 - -`targetOpacityForState()` 方法实现了优先级逻辑: - -```cpp -float StateMachine::targetOpacityForState(States s) const { - // 按优先级从高到低检查 - if (s & StateDisabled) return 0.00f; - if (s & StatePressed) return 0.12f; - if (s & StateDragged) return 0.16f; - if (s & StateFocused) return 0.12f; - if (s & StateHovered) return 0.08f; - if (s & StateChecked) return 0.08f; - return 0.00f; // Normal -} -``` - -使用按位与运算(`&`)来检查状态是否被设置。这意味着多个状态可以同时存在,但只有一个决定透明度。 - -## Checked 状态的特殊处理 - -Checked 状态有些特殊——它不是由鼠标或键盘事件触发的,而是由控件的逻辑状态决定的。比如 CheckBox 的 `setChecked(bool)` 会调用 `onCheckedChanged()`。 - -```cpp -void CheckBox::setChecked(bool checked) { - QCheckBox::setChecked(checked); - m_stateMachine->onCheckedChanged(checked); -} -``` - -## Disabled 状态的特殊处理 - -Disabled 状态优先级最高。当控件被禁用时,所有其他交互都被忽略: - -```cpp -void StateMachine::onPress(const QPoint& pos) { - if (hasState(StateDisabled)) { - return; // 禁用状态下忽略按下事件 - } - // ... 正常处理 -} -``` - -## 信号通知 - -StateMachine 发出两个信号: - -```cpp -void stateChanged(States newState, States oldState); -void stateLayerOpacityChanged(float opacity); -``` - -控件可以连接这些信号来触发重绘: - -```cpp -connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, - this, [this](float) { update(); }); -``` - -## 总结 - -StateMachine 是 Material 行为层的核心,它管理控件的所有交互状态,并驱动状态层透明度的动画。它的设计简单而高效,使用位运算处理状态叠加,用优先级决定最终透明度。 - -但状态机只是管理状态,状态变化的视觉效果还需要其他组件来实现——比如涟漪效果、阴影绘制等。 - -接下来,我们聊聊涟漪与阴影的实现。 - ---- - -**相关文档** - -- [工厂与策略](../layer-3-animation-engine/03-factory-and-strategy.md)——动画工厂设计 -- [涟漪与阴影](./02-ripple-and-elevation.md)——视觉效果组件 -- [动画引擎架构](../layer-3-animation-engine/01-animation-architecture.md)——Layer 3 的整体设计 +--- +title: 状态机设计——Material 交互状态的核心管理器 +description: 在 Layer 3 里,我们讲了动画引擎的完整实现。但动画不会凭空触发——它们需要响应鼠标悬停、点击 +--- + +# 状态机设计——Material 交互状态的核心管理器 + +在 Layer 3 里,我们讲了动画引擎的完整实现。但动画不会凭空触发——它们需要响应鼠标悬停、点击、焦点变化等交互事件。 + +这篇文章聊聊 Material 行为层的核心组件:StateMachine。 + +## 为什么需要独立的状态机? + +Qt 有 QStateMachine,为什么还要自己实现? + +有几个原因: + +1. **Qt QStateMachine 过于重量级**:它是一个通用的状态机框架,对于 Material 的简单状态管理来说太复杂了 +2. **Material 的特殊需求**:Material 定义了特定的状态透明度值(0.00、0.08、0.12 等),需要精确控制 +3. **状态叠加问题**:Material 的状态可以叠加(比如同时 Hovered 和 Checked),需要位运算处理 +4. **动画集成**:状态切换需要触发透明度动画,需要与我们的动画系统无缝集成 + +## Material 的交互状态 + +StateMachine 定义了 7 种交互状态: + +```cpp +enum class State { + StateNormal = 0x00, // 默认状态 + StateHovered = 0x01, // 鼠标悬停 + StatePressed = 0x02, // 鼠标按下 + StateFocused = 0x04, // 键盘焦点 + StateDisabled = 0x08, // 禁用状态 + StateChecked = 0x10, // 选中状态 + StateDragged = 0x20 // 拖拽状态 +}; +```text + +注意这里使用了位掩码设计,每个状态是一个 2 的幂次方。这样多个状态可以通过按位或运算组合。 + +## 状态优先级 + +Material Design 3 定义了状态的优先级顺序: + +```text +Disabled > Pressed > Dragged > Focused > Hovered > Normal +```bash + +这意味着如果一个控件同时是 Disabled 和 Hovered, Disabled 状态会"赢",透明度由 Disabled 决定(通常是 0.00)。 + +## 状态透明度规范 + +每个状态对应一个特定的透明度值,用于 StateLayer 的叠加: + +| 状态 | 透明度 | 说明 | +|---|---:|---| +| Normal | 0.00 | 默认状态,无叠加 | +| Hovered | 0.08 | 鼠标悬停时的视觉反馈 | +| Pressed | 0.12 | 按下时的视觉反馈 | +| Focused | 0.12 | 焦点状态(同 Pressed) | +| Dragged | 0.16 | 拖拽时的视觉反馈 | +| Checked | 0.08 | 选中状态(同 Hovered) | +| Disabled | 0.00 | 禁用状态(无叠加) | + +这些透明度值是 Material Design 3 规范的一部分,不应该随意修改。 + +## 事件处理接口 + +StateMachine 提供了一系列事件处理方法: + +```cpp +void onHoverEnter(); +void onHoverLeave(); +void onPress(const QPoint& pos); +void onRelease(); +void onFocusIn(); +void onFocusOut(); +void onEnable(); +void onDisable(); +void onCheckedChanged(bool checked); +```text + +这些方法对应 Qt 的各种事件,控件在事件处理函数中调用它们: + +```cpp +void Button::enterEvent(QEnterEvent* event) { + QPushButton::enterEvent(event); // 先调用父类方法 + m_stateMachine->onHoverEnter(); +} + +void Button::mousePressEvent(QMouseEvent* event) { + QPushButton::mousePressEvent(event); + m_stateMachine->onPress(event->pos()); +} +```text + +注意这里的一个关键点:必须先调用父类方法。否则 Qt 的信号机制会被破坏,比如 `clicked()` 信号可能不会发出。 + +## 状态切换动画 + +当状态改变时,StateMachine 会触发透明度动画: + +```cpp +void StateMachine::animateOpacityTo(float from, float to) { + auto anim = m_animator->getAnimation("md.animation.fadeIn"); + if (anim) { + // 创建一个自定义动画,从 from 到 to + connect(anim.get(), &ICFAbstractAnimation::progressChanged, + this, [this, from, to](float progress) { + m_opacity = lerp(from, to, progress); + emit stateLayerOpacityChanged(m_opacity); + }); + anim->start(); + } +} +```text + +如果动画系统被禁用,会直接设置目标透明度: + +```cpp +m_opacity = to; +emit stateLayerOpacityChanged(m_opacity); +```text + +## 状态优先级的实现 + +`targetOpacityForState()` 方法实现了优先级逻辑: + +```cpp +float StateMachine::targetOpacityForState(States s) const { + // 按优先级从高到低检查 + if (s & StateDisabled) return 0.00f; + if (s & StatePressed) return 0.12f; + if (s & StateDragged) return 0.16f; + if (s & StateFocused) return 0.12f; + if (s & StateHovered) return 0.08f; + if (s & StateChecked) return 0.08f; + return 0.00f; // Normal +} +```text + +使用按位与运算(`&`)来检查状态是否被设置。这意味着多个状态可以同时存在,但只有一个决定透明度。 + +## Checked 状态的特殊处理 + +Checked 状态有些特殊——它不是由鼠标或键盘事件触发的,而是由控件的逻辑状态决定的。比如 CheckBox 的 `setChecked(bool)` 会调用 `onCheckedChanged()`。 + +```cpp +void CheckBox::setChecked(bool checked) { + QCheckBox::setChecked(checked); + m_stateMachine->onCheckedChanged(checked); +} +```text + +## Disabled 状态的特殊处理 + +Disabled 状态优先级最高。当控件被禁用时,所有其他交互都被忽略: + +```cpp +void StateMachine::onPress(const QPoint& pos) { + if (hasState(StateDisabled)) { + return; // 禁用状态下忽略按下事件 + } + // ... 正常处理 +} +```text + +## 信号通知 + +StateMachine 发出两个信号: + +```cpp +void stateChanged(States newState, States oldState); +void stateLayerOpacityChanged(float opacity); +```text + +控件可以连接这些信号来触发重绘: + +```cpp +connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, + this, [this](float) { update(); }); +```yaml + +## 总结 + +StateMachine 是 Material 行为层的核心,它管理控件的所有交互状态,并驱动状态层透明度的动画。它的设计简单而高效,使用位运算处理状态叠加,用优先级决定最终透明度。 + +但状态机只是管理状态,状态变化的视觉效果还需要其他组件来实现——比如涟漪效果、阴影绘制等。 + +接下来,我们聊聊涟漪与阴影的实现。 + +--- + +**相关文档** + +- [工厂与策略](../layer-3-animation-engine/03-factory-and-strategy.md)——动画工厂设计 +- [涟漪与阴影](./02-ripple-and-elevation.md)——视觉效果组件 +- [动画引擎架构](../layer-3-animation-engine/01-animation-architecture.md)——Layer 3 的整体设计 diff --git a/document/HandBook/ui/architecture/layer-4-material-behavior/02-ripple-and-elevation.md b/document/HandBook/ui/architecture/layer-4-material-behavior/02-ripple-and-elevation.md index 91321a556..e6efc4876 100644 --- a/document/HandBook/ui/architecture/layer-4-material-behavior/02-ripple-and-elevation.md +++ b/document/HandBook/ui/architecture/layer-4-material-behavior/02-ripple-and-elevation.md @@ -1,240 +1,245 @@ -# 涟漪与阴影——Material 视觉反馈的完整实现 - -在上一篇文章里,我们聊了 StateMachine 如何管理交互状态。但状态变化只是"逻辑"上的变化,用户还需要"视觉"上的反馈——涟漪效果和阴影变化。 - -这篇文章聊聊 Material 的两个核心视觉组件:RippleHelper 和 MdElevationController。 - -## RippleHelper:涟漪效果管理器 - -涟漪是 Material Design 最标志性的视觉元素之一。当用户点击一个控件时,一个圆形的涟漪从点击位置扩散开来,提供即时的视觉反馈。 - -### 涟漪的生命周期 - -涟漪有四个关键阶段: - -1. **创建(onPress)**:鼠标按下时,在点击位置创建一个新的涟漪 -2. **扩散**:涟漪半径从 0 扩散到最大半径 -3. **释放(onRelease)**:鼠标释放时,触发淡出动画 -4. **消失**:淡出动画完成后,涟漪被移除 - -### MdRipple 数据结构 - -每个涟漪由以下数据描述: - -```cpp -struct MdRipple { - QPointF center; // 涟漪中心 - float radius; // 当前半径 - float opacity; // 当前透明度 - bool releasing; // 是否处于释放阶段 - float maxRadius; // 最大半径 -}; -``` - -### 最大半径的计算 - -最大半径取决于渲染模式: - -- **Bounded 模式**:从点击位置到控件最远角落的距离 -- **Unbounded 模式**:足够大以覆盖整个屏幕(用于 FAB 等控件) - -```cpp -float RippleHelper::maxRadius(const QRectF& rect, const QPointF& center) const { - if (m_mode == Mode::Unbounded) { - return 1000.0f; // 足够大 - } - - // 计算到四个角落的距离,取最大值 - float d1 = std::hypot(center.x() - rect.left(), center.y() - rect.top()); - float d2 = std::hypot(center.x() - rect.right(), center.y() - rect.top()); - float d3 = std::hypot(center.x() - rect.left(), center.y() - rect.bottom()); - float d4 = std::hypot(center.x() - rect.right(), center.y() - rect.bottom()); - return std::max({d1, d2, d3, d4}); -} -``` - -### 多涟漪并存 - -快速多次点击会产生多个涟漪。RippleHelper 维护一个涟漪列表: - -```cpp -QList m_ripples; -``` - -每次 `paint()` 时,所有涟漪都被绘制。当涟漪的透明度降到 0 以下时,它被从列表中移除。 - -### 涟漪动画 - -涟漪的半径和透明度分别由两个动画控制: - -- 半径动画:从 0 扩散到 maxRadius(使用 md.animation.rippleExpand) -- 透明度动画:释放后从当前值淡出到 0(使用 md.animation.fadeOut) - -```cpp -void RippleHelper::onPress(const QPoint& pos, const QRectF& widgetRect) { - MdRipple ripple; - ripple.center = pos; - ripple.radius = 0.0f; - ripple.opacity = 1.0f; - ripple.releasing = false; - ripple.maxRadius = maxRadius(widgetRect, pos); - - m_ripples.append(ripple); - - // 启动半径扩散动画 - auto anim = m_animator->getAnimation("md.animation.rippleExpand"); - if (anim) { - connect(anim.get(), &ICFAbstractAnimation::progressChanged, - this, [this](float progress) { - // 更新涟漪半径 - for (auto& ripple : m_ripples) { - if (!ripple.releasing) { - ripple.radius = ripple.maxRadius * progress; - } - } - emit repaintNeeded(); - }); - anim->start(); - } -} -``` - -### 涟漪颜色 - -涟漪颜色通常是控件的状态颜色(onPrimary、onSurface 等),通过 `setColor()` 设置: - -```cpp -void RippleHelper::setColor(const CFColor& color) { - m_color = color; -} -``` - -绘制时,使用这个颜色加上当前的透明度值: - -```cpp -QColor rippleColor = m_color.native_color; -rippleColor.setAlphaF(m_ripples[i].opacity); -``` - -### 涟漪绘制 - -```cpp -void RippleHelper::paint(QPainter* painter, const QPainterPath& clipPath) { - painter->save(); - painter->setClipPath(clipPath); // Bounded 模式下裁剪 - - for (const auto& ripple : m_ripples) { - QColor rippleColor = m_color.native_color(); - rippleColor.setAlphaF(ripple.opacity); - - painter->setBrush(rippleColor); - painter->setPen(Qt::NoPen); - painter->drawEllipse(ripple.center, ripple.radius, ripple.radius); - } - - painter->restore(); -} -``` - -## MdElevationController:海拔阴影控制器 - -Material Design 3 用"海拔"(elevation)来表示控件的层级关系。海拔越高,阴影越明显。 - -### 6 级海拔系统 - -Material 定义了 6 个标准海拔级别: - -| Level | dp | Blur | Offset | Opacity | -|---:|---|---|---|---| -| 0 | 0dp | 0px | 0px | 0.00 | -| 1 | 1dp | 2px | 1px | 0.15 | -| 2 | 3dp | 4px | 2px | 0.20 | -| 3 | 6dp | 8px | 4px | 0.25 | -| 4 | 8dp | 12px | 6px | 0.30 | -| 5 | 12dp | 16px | 8px | 0.35 | - -### 阴影参数的计算 - -```cpp -MdElevationController::ShadowParams MdElevationController::paramsForLevel(float level) const { - // 根据级别返回对应的 blur、offset、opacity - // ... -} -``` - -### 阴影绘制 - -阴影使用多层叠加来实现更自然的效果: - -```cpp -void MdElevationController::paintShadow(QPainter* painter, const QPainterPath& shape) { - ShadowParams params = paramsForLevel(m_currentLevel); - - // 第一层阴影 - QColor shadowColor(0, 0, 0, params.opacity * 255); - QPainterPath shadow1 = shape.translated(params.offsetX, params.offsetY); - // ... 绘制带模糊的阴影 - - // 第二层阴影(可选,更明显的海拔效果) -} -``` - -实际实现中,Qt 的 QGraphicsDropShadowEffect 可以用来生成模糊阴影,但我们可能需要自定义绘制以获得更精确的控制。 - -### 按压效果 - -当控件被按下时,海拔会暂时"降低",产生"被压下去"的效果: - -```cpp -void MdElevationController::setPressed(bool pressed) { - m_isPressed = pressed; - if (pressed) { - // 暂时降低海拔 - } else { - // 恢复原始海拔 - } -} -``` - -这个效果通过调整阴影偏移和透明度来实现。 - -### pressOffset 计算 - -按压状态下的垂直偏移量用于实现"按下去"的视觉效果: - -```cpp -float MdElevationController::pressOffset() const { - // 基于当前海拔计算偏移量 - // 海拔越高,按压时的偏移量越大 - return m_currentLevel * 2.0f; // 简化的公式 -} -``` - -### Dark Theme 的叠色表示 - -在暗色主题中,海拔不是用阴影表示的,而是通过向 Surface 颜色叠加 Primary 色调来实现: - -```cpp -CFColor MdElevationController::tonalOverlay(CFColor surface, CFColor primary) const { - // 使用 elevationOverlay 函数计算叠色 - return elevationOverlay(surface, primary, static_cast(m_currentLevel)); -} -``` - -`elevationOverlay` 函数我们在 Layer 1 讲过,它根据海拔级别选择不同的叠加透明度。 - -## 总结 - -RippleHelper 和 MdElevationController 是 Material 视觉反馈的两个核心组件。涟漪提供即时的点击反馈,阴影表示控件的层级关系。 - -但除了涟漪和阴影,Material 还有一个重要的视觉元素——焦点指示器,它对键盘导航和无障碍访问至关重要。 - -接下来,我们聊聊焦点指示器的设计。 - ---- - -**相关文档** - -- [状态机设计](./01-state-machine.md)——交互状态的管理 -- [焦点指示器](./03-focus-indicator.md)——无障碍访问的视觉实现 -- [颜色系统实现](../layer-2-theme-engine/03-color-scheme.md)——Layer 2 的颜色方案 +--- +title: 涟漪与阴影——Material 视觉反馈的完整实现 +description: "在上一篇文章里,我们聊了 StateMachine 如何管理交互状态。但状态变化只是\"逻辑\"上的变化" +--- + +# 涟漪与阴影——Material 视觉反馈的完整实现 + +在上一篇文章里,我们聊了 StateMachine 如何管理交互状态。但状态变化只是"逻辑"上的变化,用户还需要"视觉"上的反馈——涟漪效果和阴影变化。 + +这篇文章聊聊 Material 的两个核心视觉组件:RippleHelper 和 MdElevationController。 + +## RippleHelper:涟漪效果管理器 + +涟漪是 Material Design 最标志性的视觉元素之一。当用户点击一个控件时,一个圆形的涟漪从点击位置扩散开来,提供即时的视觉反馈。 + +### 涟漪的生命周期 + +涟漪有四个关键阶段: + +1. **创建(onPress)**:鼠标按下时,在点击位置创建一个新的涟漪 +2. **扩散**:涟漪半径从 0 扩散到最大半径 +3. **释放(onRelease)**:鼠标释放时,触发淡出动画 +4. **消失**:淡出动画完成后,涟漪被移除 + +### MdRipple 数据结构 + +每个涟漪由以下数据描述: + +```cpp +struct MdRipple { + QPointF center; // 涟漪中心 + float radius; // 当前半径 + float opacity; // 当前透明度 + bool releasing; // 是否处于释放阶段 + float maxRadius; // 最大半径 +}; +```text + +### 最大半径的计算 + +最大半径取决于渲染模式: + +- **Bounded 模式**:从点击位置到控件最远角落的距离 +- **Unbounded 模式**:足够大以覆盖整个屏幕(用于 FAB 等控件) + +```cpp +float RippleHelper::maxRadius(const QRectF& rect, const QPointF& center) const { + if (m_mode == Mode::Unbounded) { + return 1000.0f; // 足够大 + } + + // 计算到四个角落的距离,取最大值 + float d1 = std::hypot(center.x() - rect.left(), center.y() - rect.top()); + float d2 = std::hypot(center.x() - rect.right(), center.y() - rect.top()); + float d3 = std::hypot(center.x() - rect.left(), center.y() - rect.bottom()); + float d4 = std::hypot(center.x() - rect.right(), center.y() - rect.bottom()); + return std::max({d1, d2, d3, d4}); +} +```text + +### 多涟漪并存 + +快速多次点击会产生多个涟漪。RippleHelper 维护一个涟漪列表: + +```cpp +QList m_ripples; +```text + +每次 `paint()` 时,所有涟漪都被绘制。当涟漪的透明度降到 0 以下时,它被从列表中移除。 + +### 涟漪动画 + +涟漪的半径和透明度分别由两个动画控制: + +- 半径动画:从 0 扩散到 maxRadius(使用 md.animation.rippleExpand) +- 透明度动画:释放后从当前值淡出到 0(使用 md.animation.fadeOut) + +```cpp +void RippleHelper::onPress(const QPoint& pos, const QRectF& widgetRect) { + MdRipple ripple; + ripple.center = pos; + ripple.radius = 0.0f; + ripple.opacity = 1.0f; + ripple.releasing = false; + ripple.maxRadius = maxRadius(widgetRect, pos); + + m_ripples.append(ripple); + + // 启动半径扩散动画 + auto anim = m_animator->getAnimation("md.animation.rippleExpand"); + if (anim) { + connect(anim.get(), &ICFAbstractAnimation::progressChanged, + this, [this](float progress) { + // 更新涟漪半径 + for (auto& ripple : m_ripples) { + if (!ripple.releasing) { + ripple.radius = ripple.maxRadius * progress; + } + } + emit repaintNeeded(); + }); + anim->start(); + } +} +```text + +### 涟漪颜色 + +涟漪颜色通常是控件的状态颜色(onPrimary、onSurface 等),通过 `setColor()` 设置: + +```cpp +void RippleHelper::setColor(const CFColor& color) { + m_color = color; +} +```text + +绘制时,使用这个颜色加上当前的透明度值: + +```cpp +QColor rippleColor = m_color.native_color; +rippleColor.setAlphaF(m_ripples[i].opacity); +```text + +### 涟漪绘制 + +```cpp +void RippleHelper::paint(QPainter* painter, const QPainterPath& clipPath) { + painter->save(); + painter->setClipPath(clipPath); // Bounded 模式下裁剪 + + for (const auto& ripple : m_ripples) { + QColor rippleColor = m_color.native_color(); + rippleColor.setAlphaF(ripple.opacity); + + painter->setBrush(rippleColor); + painter->setPen(Qt::NoPen); + painter->drawEllipse(ripple.center, ripple.radius, ripple.radius); + } + + painter->restore(); +} +```bash + +## MdElevationController:海拔阴影控制器 + +Material Design 3 用"海拔"(elevation)来表示控件的层级关系。海拔越高,阴影越明显。 + +### 6 级海拔系统 + +Material 定义了 6 个标准海拔级别: + +| Level | dp | Blur | Offset | Opacity | +|---:|---|---|---|---| +| 0 | 0dp | 0px | 0px | 0.00 | +| 1 | 1dp | 2px | 1px | 0.15 | +| 2 | 3dp | 4px | 2px | 0.20 | +| 3 | 6dp | 8px | 4px | 0.25 | +| 4 | 8dp | 12px | 6px | 0.30 | +| 5 | 12dp | 16px | 8px | 0.35 | + +### 阴影参数的计算 + +```cpp +MdElevationController::ShadowParams MdElevationController::paramsForLevel(float level) const { + // 根据级别返回对应的 blur、offset、opacity + // ... +} +```text + +### 阴影绘制 + +阴影使用多层叠加来实现更自然的效果: + +```cpp +void MdElevationController::paintShadow(QPainter* painter, const QPainterPath& shape) { + ShadowParams params = paramsForLevel(m_currentLevel); + + // 第一层阴影 + QColor shadowColor(0, 0, 0, params.opacity * 255); + QPainterPath shadow1 = shape.translated(params.offsetX, params.offsetY); + // ... 绘制带模糊的阴影 + + // 第二层阴影(可选,更明显的海拔效果) +} +```text + +实际实现中,Qt 的 QGraphicsDropShadowEffect 可以用来生成模糊阴影,但我们可能需要自定义绘制以获得更精确的控制。 + +### 按压效果 + +当控件被按下时,海拔会暂时"降低",产生"被压下去"的效果: + +```cpp +void MdElevationController::setPressed(bool pressed) { + m_isPressed = pressed; + if (pressed) { + // 暂时降低海拔 + } else { + // 恢复原始海拔 + } +} +```text + +这个效果通过调整阴影偏移和透明度来实现。 + +### pressOffset 计算 + +按压状态下的垂直偏移量用于实现"按下去"的视觉效果: + +```cpp +float MdElevationController::pressOffset() const { + // 基于当前海拔计算偏移量 + // 海拔越高,按压时的偏移量越大 + return m_currentLevel * 2.0f; // 简化的公式 +} +```text + +### Dark Theme 的叠色表示 + +在暗色主题中,海拔不是用阴影表示的,而是通过向 Surface 颜色叠加 Primary 色调来实现: + +```cpp +CFColor MdElevationController::tonalOverlay(CFColor surface, CFColor primary) const { + // 使用 elevationOverlay 函数计算叠色 + return elevationOverlay(surface, primary, static_cast(m_currentLevel)); +} +```yaml + +`elevationOverlay` 函数我们在 Layer 1 讲过,它根据海拔级别选择不同的叠加透明度。 + +## 总结 + +RippleHelper 和 MdElevationController 是 Material 视觉反馈的两个核心组件。涟漪提供即时的点击反馈,阴影表示控件的层级关系。 + +但除了涟漪和阴影,Material 还有一个重要的视觉元素——焦点指示器,它对键盘导航和无障碍访问至关重要。 + +接下来,我们聊聊焦点指示器的设计。 + +--- + +**相关文档** + +- [状态机设计](./01-state-machine.md)——交互状态的管理 +- [焦点指示器](./03-focus-indicator.md)——无障碍访问的视觉实现 +- [颜色系统实现](../layer-2-theme-engine/03-color-scheme.md)——Layer 2 的颜色方案 diff --git a/document/HandBook/ui/architecture/layer-4-material-behavior/03-focus-indicator.md b/document/HandBook/ui/architecture/layer-4-material-behavior/03-focus-indicator.md index 6870f2439..43353520a 100644 --- a/document/HandBook/ui/architecture/layer-4-material-behavior/03-focus-indicator.md +++ b/document/HandBook/ui/architecture/layer-4-material-behavior/03-focus-indicator.md @@ -1,169 +1,174 @@ -# 焦点指示器——无障碍访问的视觉实现 - -在前面的文章里,我们聊了状态机、涟漪效果和海拔阴影。这些组件主要响应鼠标交互。但对于键盘导航和无障碍访问来说,还需要一个重要的视觉元素——焦点指示器。 - -这篇文章聊聊 MdFocusIndicator 的设计。 - -## Material 焦点环规范 - -Material Design 3 对焦点指示器有明确的规范: - -- **宽度**:3dp -- **内边距**:3dp(距离控件边界) -- **动画**:淡入淡出效果 -- **颜色**:通常是 Primary 或 OnPrimary 颜色 - -焦点环只在控件获得键盘焦点时显示,为键盘用户提供清晰的视觉反馈。 - -## MdFocusIndicator 的设计 - -MdFocusIndicator 是一个轻量级组件,只负责绘制焦点环: - -```cpp -class MdFocusIndicator : public QObject { -public: - explicit MdFocusIndicator( - cf::WeakPtr factory, - QObject* parent = nullptr); - - void onFocusIn(); - void onFocusOut(); - - void paint(QPainter* painter, const QRectF& widgetRect, float cornerRadius); - -private: - float m_progress = 0.0f; // 0 = 隐藏,1 = 完全显示 - cf::WeakPtr m_animator; -}; -``` - -## 焦点进入/离开 - -当控件获得焦点时,焦点环淡入;失去焦点时,焦点环淡出: - -```cpp -void MdFocusIndicator::onFocusIn() { - auto anim = m_animator->getAnimation("md.animation.fadeIn"); - if (anim) { - connect(anim.get(), &ICFAbstractAnimation::progressChanged, - this, [this](float progress) { - m_progress = progress; - // 触发重绘 - }); - anim->start(); - } else { - m_progress = 1.0f; - } -} - -void MdFocusIndicator::onFocusOut() { - auto anim = m_animator->getAnimation("md.animation.fadeOut"); - if (anim) { - connect(anim.get(), &ICFAbstractAnimation::progressChanged, - this, [this](float progress) { - m_progress = 1.0f - progress; // 反向 - }); - anim->start(); - } else { - m_progress = 0.0f; - } -} -``` - -注意淡出动画使用的是 `1.0f - progress`,因为我们希望透明度从 1 变到 0。 - -## 焦点环的绘制 - -焦点环是一个圆角矩形,位于控件边界外侧 3dp 的位置: - -```cpp -void MdFocusIndicator::paint(QPainter* painter, const QRectF& widgetRect, - float cornerRadius) { - if (m_progress <= 0.001f) { - return; // 透明度为 0,跳过绘制 - } - - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float inset = helper.dpToPx(3.0f); - float width = helper.dpToPx(3.0f); - - // 计算焦点环矩形 - QRectF ringRect = widgetRect.adjusted(-inset, -inset, inset, inset); - - // 创建圆角矩形路径 - QPainterPath ringPath; - ringPath.addRoundedRect(ringRect, cornerRadius + inset, cornerRadius + inset); - - // 设置颜色和透明度 - QColor ringColor = /* 从主题获取 */; - ringColor.setAlphaF(m_progress * ringColor.alphaF()); - - // 绘制 - painter->save(); - QPen pen(ringColor, width); - pen.setCosmetic(true); // 不受变换影响 - painter->setPen(pen); - painter->setBrush(Qt::NoBrush); - painter->drawPath(ringPath); - painter->restore(); -} -``` - -## 与控件的集成 - -控件需要在焦点事件中调用 MdFocusIndicator: - -```cpp -void Button::focusInEvent(QFocusEvent* event) { - QPushButton::focusInEvent(event); - m_focusIndicator->onFocusIn(); -} - -void Button::focusOutEvent(QFocusEvent* event) { - QPushButton::focusOutEvent(event); - m_focusIndicator->onFocusOut(); -} - -void Button::paintEvent(QPaintEvent* event) { - // ... 其他绘制步骤 - - // 最后绘制焦点环 - m_focusIndicator->paint(&painter, rect(), cornerRadius()); -} -``` - -## 无障碍考虑 - -焦点指示器是无障碍访问的重要组成部分。对于使用键盘、屏幕阅读器或其它辅助技术的用户来说,焦点指示器提供了清晰的视觉反馈,帮助他们理解当前的交互位置。 - -Material Design 3 要求焦点指示器必须: - -1. **始终可见**:当控件有焦点时,焦点环必须清晰可见 -2. **高对比度**:焦点环颜色必须与背景有足够的对比度 -3. **动画平滑**:焦点切换时的过渡应该流畅自然 - -## 性能优化 - -焦点环的绘制相对简单,性能开销不大。但我们可以做一些优化: - -1. **透明度为 0 时跳过绘制**:避免无意义的绘制操作 -2. **使用 cosmetic pen**:`setCosmetic(true)` 让线宽不受缩放影响 -3. **路径缓存**:如果控件形状不变,可以缓存 QPainterPath - -## 总结 - -MdFocusIndicator 是 Material 行为层的最后一个核心组件。它提供了清晰的焦点反馈,支持平滑的淡入淡出动画,是无障碍访问的重要组成部分。 - -到这里,Layer 4(Material Behavior Layer)的内容就基本覆盖了。我们有了状态管理、涟漪效果、海拔阴影、焦点指示器四个核心组件。 - -但这些组件不能独立工作——它们需要被组合到具体的控件中,才能发挥作用。 - -接下来,我们进入 Layer 5:Widget Adapter Layer,看看如何将这些行为组件整合到具体控件中。 - ---- - -**相关文档** - -- [涟漪与阴影](./02-ripple-and-elevation.md)——Material 的其他视觉组件 -- [状态机设计](./01-state-machine.md)——交互状态的管理 -- [适配器模式](../layer-5-widget-adapter/01-adapter-pattern.md)——下一层的入口 +--- +title: 焦点指示器——无障碍访问的视觉实现 +description: 在前面的文章里,我们聊了状态机、涟漪效果和海拔阴影。这些组件主要响应鼠标交互。但对于键盘导航和无障碍 +--- + +# 焦点指示器——无障碍访问的视觉实现 + +在前面的文章里,我们聊了状态机、涟漪效果和海拔阴影。这些组件主要响应鼠标交互。但对于键盘导航和无障碍访问来说,还需要一个重要的视觉元素——焦点指示器。 + +这篇文章聊聊 MdFocusIndicator 的设计。 + +## Material 焦点环规范 + +Material Design 3 对焦点指示器有明确的规范: + +- **宽度**:3dp +- **内边距**:3dp(距离控件边界) +- **动画**:淡入淡出效果 +- **颜色**:通常是 Primary 或 OnPrimary 颜色 + +焦点环只在控件获得键盘焦点时显示,为键盘用户提供清晰的视觉反馈。 + +## MdFocusIndicator 的设计 + +MdFocusIndicator 是一个轻量级组件,只负责绘制焦点环: + +```cpp +class MdFocusIndicator : public QObject { +public: + explicit MdFocusIndicator( + cf::WeakPtr factory, + QObject* parent = nullptr); + + void onFocusIn(); + void onFocusOut(); + + void paint(QPainter* painter, const QRectF& widgetRect, float cornerRadius); + +private: + float m_progress = 0.0f; // 0 = 隐藏,1 = 完全显示 + cf::WeakPtr m_animator; +}; +```text + +## 焦点进入/离开 + +当控件获得焦点时,焦点环淡入;失去焦点时,焦点环淡出: + +```cpp +void MdFocusIndicator::onFocusIn() { + auto anim = m_animator->getAnimation("md.animation.fadeIn"); + if (anim) { + connect(anim.get(), &ICFAbstractAnimation::progressChanged, + this, [this](float progress) { + m_progress = progress; + // 触发重绘 + }); + anim->start(); + } else { + m_progress = 1.0f; + } +} + +void MdFocusIndicator::onFocusOut() { + auto anim = m_animator->getAnimation("md.animation.fadeOut"); + if (anim) { + connect(anim.get(), &ICFAbstractAnimation::progressChanged, + this, [this](float progress) { + m_progress = 1.0f - progress; // 反向 + }); + anim->start(); + } else { + m_progress = 0.0f; + } +} +```text + +注意淡出动画使用的是 `1.0f - progress`,因为我们希望透明度从 1 变到 0。 + +## 焦点环的绘制 + +焦点环是一个圆角矩形,位于控件边界外侧 3dp 的位置: + +```cpp +void MdFocusIndicator::paint(QPainter* painter, const QRectF& widgetRect, + float cornerRadius) { + if (m_progress <= 0.001f) { + return; // 透明度为 0,跳过绘制 + } + + CanvasUnitHelper helper(qApp->devicePixelRatio()); + float inset = helper.dpToPx(3.0f); + float width = helper.dpToPx(3.0f); + + // 计算焦点环矩形 + QRectF ringRect = widgetRect.adjusted(-inset, -inset, inset, inset); + + // 创建圆角矩形路径 + QPainterPath ringPath; + ringPath.addRoundedRect(ringRect, cornerRadius + inset, cornerRadius + inset); + + // 设置颜色和透明度 + QColor ringColor = /* 从主题获取 */; + ringColor.setAlphaF(m_progress * ringColor.alphaF()); + + // 绘制 + painter->save(); + QPen pen(ringColor, width); + pen.setCosmetic(true); // 不受变换影响 + painter->setPen(pen); + painter->setBrush(Qt::NoBrush); + painter->drawPath(ringPath); + painter->restore(); +} +```text + +## 与控件的集成 + +控件需要在焦点事件中调用 MdFocusIndicator: + +```cpp +void Button::focusInEvent(QFocusEvent* event) { + QPushButton::focusInEvent(event); + m_focusIndicator->onFocusIn(); +} + +void Button::focusOutEvent(QFocusEvent* event) { + QPushButton::focusOutEvent(event); + m_focusIndicator->onFocusOut(); +} + +void Button::paintEvent(QPaintEvent* event) { + // ... 其他绘制步骤 + + // 最后绘制焦点环 + m_focusIndicator->paint(&painter, rect(), cornerRadius()); +} +```yaml + +## 无障碍考虑 + +焦点指示器是无障碍访问的重要组成部分。对于使用键盘、屏幕阅读器或其它辅助技术的用户来说,焦点指示器提供了清晰的视觉反馈,帮助他们理解当前的交互位置。 + +Material Design 3 要求焦点指示器必须: + +1. **始终可见**:当控件有焦点时,焦点环必须清晰可见 +2. **高对比度**:焦点环颜色必须与背景有足够的对比度 +3. **动画平滑**:焦点切换时的过渡应该流畅自然 + +## 性能优化 + +焦点环的绘制相对简单,性能开销不大。但我们可以做一些优化: + +1. **透明度为 0 时跳过绘制**:避免无意义的绘制操作 +2. **使用 cosmetic pen**:`setCosmetic(true)` 让线宽不受缩放影响 +3. **路径缓存**:如果控件形状不变,可以缓存 QPainterPath + +## 总结 + +MdFocusIndicator 是 Material 行为层的最后一个核心组件。它提供了清晰的焦点反馈,支持平滑的淡入淡出动画,是无障碍访问的重要组成部分。 + +到这里,Layer 4(Material Behavior Layer)的内容就基本覆盖了。我们有了状态管理、涟漪效果、海拔阴影、焦点指示器四个核心组件。 + +但这些组件不能独立工作——它们需要被组合到具体的控件中,才能发挥作用。 + +接下来,我们进入 Layer 5:Widget Adapter Layer,看看如何将这些行为组件整合到具体控件中。 + +--- + +**相关文档** + +- [涟漪与阴影](./02-ripple-and-elevation.md)——Material 的其他视觉组件 +- [状态机设计](./01-state-machine.md)——交互状态的管理 +- [适配器模式](../layer-5-widget-adapter/01-adapter-pattern.md)——下一层的入口 diff --git a/document/HandBook/ui/architecture/layer-4-material-behavior/index.md b/document/HandBook/ui/architecture/layer-4-material-behavior/index.md index 0cff11ae4..c7496ec74 100644 --- a/document/HandBook/ui/architecture/layer-4-material-behavior/index.md +++ b/document/HandBook/ui/architecture/layer-4-material-behavior/index.md @@ -1,10 +1,11 @@ -# layer-4-material-behavior - -> Welcome to the layer-4-material-behavior section. +--- +title: "Layer 4: Material 行为" +description: 本章节介绍 UI 渲染管线的第四层——Material 行为层,涵盖 状态机、 涟漪效果辅助以及 +--- -## Overview +# Layer 4: Material 行为 -Documentation and resources for layer-4-material-behavior. +本章节介绍 UI 渲染管线的第四层——Material 行为层,涵盖 `StateMachine` 状态机、`RippleHelper` 涟漪效果辅助以及 `MdElevationController` 阴影高度控制器等核心行为模块。该层将 Material Design 3 的交互规范转化为可复用的行为组件。 --- diff --git a/document/HandBook/ui/architecture/layer-5-widget-adapter/.pages b/document/HandBook/ui/architecture/layer-5-widget-adapter/.pages deleted file mode 100644 index c5cf77342..000000000 --- a/document/HandBook/ui/architecture/layer-5-widget-adapter/.pages +++ /dev/null @@ -1,5 +0,0 @@ -title: 第五层 · Widget 适配 -nav: - - 适配器模式: 01-adapter-pattern.md - - Button 深入: 02-button-deep-dive.md - - 绘制管线: 03-painting-pipeline.md diff --git a/document/HandBook/ui/architecture/layer-5-widget-adapter/01-adapter-pattern.md b/document/HandBook/ui/architecture/layer-5-widget-adapter/01-adapter-pattern.md index a76456f9e..d20b49a7b 100644 --- a/document/HandBook/ui/architecture/layer-5-widget-adapter/01-adapter-pattern.md +++ b/document/HandBook/ui/architecture/layer-5-widget-adapter/01-adapter-pattern.md @@ -1,154 +1,159 @@ -# 适配器模式——Material 控件的"薄包装"设计 - -在前面的文章里,我们聊完了基础数学工具、主题引擎、动画系统、行为组件。现在所有的基础设施都就绪了,可以开始构建具体的控件了。 - -这篇文章聊聊控件适配层的设计理念。 - -## 继承 vs 组合的选择 - -一个经典的问题是:为什么必须继承 Qt 原生控件? - -有几个重要原因: - -1. **Qt 事件体系的要求**:Qt 的信号机制依赖于事件处理函数的正确调用顺序。如果不继承,就需要手动管理信号,复杂且容易出错 -2. **布局系统集成**:Qt 的布局管理器(QVBoxLayout、QGridLayout 等)直接与 QWidget 接口交互 -3. **QSS 兼容性**:虽然我们不使用 QSS 实现 Material 效果,但保持继承关系让 QSS 仍然可用于某些全局设置 -4. **第三方库兼容**:很多 Qt 库假设控件是 QWidget 的子类 - -所以我们选择继承 Qt 原生控件,但只做最小的修改——事件转发和 paintEvent 重写。 - -## "只做三件事"的设计原则 - -Material 控件适配器只做三件事: - -1. **事件转发**:在事件 override 中调用父类方法后,转发给行为组件 -2. **paintEvent 重写**:按顺序调用渲染层的 paint 方法 -3. **主题数据读取**:从 ThemeManager 读取 Token 决定颜色、形状、字体 - -任何不属于这三件事的逻辑都应该被拒绝——这保持了控件的"薄"特性,让行为组件保持独立。 - -## 事件转发模式 - -事件处理的标准模板是: - -```cpp -void Button::mousePressEvent(QMouseEvent* event) { - // 1. 先调用父类方法(必须!) - QPushButton::mousePressEvent(event); - - // 2. 转发给行为组件 - m_stateMachine->onPress(event->pos()); - m_ripple->onPress(event->pos(), rect()); -} -``` - -第一步调用父类方法是必须的,原因如下: - -- Qt 需要在事件处理中更新内部状态 -- `clicked()` 等信号在父类方法中发出 -- 跳过父类方法会导致信号不发出、状态不一致等问题 - -## 组件组合模式 - -控件通过组合行为组件来获得 Material 特性: - -```cpp -Button::Button(ButtonVariant variant, QWidget* parent) - : QPushButton(parent), variant_(variant) { - - // 获取动画工厂 - m_animationFactory = Application::animationFactory(); - - // 创建行为组件 - 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); - - // 连接信号 - connect(m_ripple, &RippleHelper::repaintNeeded, - this, QOverload<>::of(&Button::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, - this, [this](float) { update(); }); - - // 监听主题变化 - connect(&ThemeManager::instance(), &ThemeManager::themeChanged, - this, [this](const ICFTheme&) { update(); }); -} -``` - -注意所有行为组件的 parent 都是 `this`,这意味着它们会随控件一起销毁,无需手动管理生命周期。 - -## 主题数据读取 - -控件在绘制时从主题读取数据: - -```cpp -QColor Button::containerColor() const { - auto& theme = ThemeManager::instance().currentTheme(); - switch (variant_) { - case ButtonVariant::Filled: - return theme.color_scheme().queryColor("md.primaryContainer"); - case ButtonVariant::Tonal: - return theme.color_scheme().queryColor("md.secondaryContainer"); - // ... - } -} -``` - -这里使用 `currentTheme()` 获取当前活动的主题。如果主题切换,`themeChanged` 信号会触发 `update()`,控件会重新读取主题数据并重绘。 - -## 为什么不用 QSS? - -这是一个经常被问到的问题。Material Design 3 的视觉需求超出了 QSS 的能力: - -1. **多层叠加**:状态层、涟漪层、阴影层需要按特定顺序绘制,QSS 不支持 -2. **动态透明度**:状态层的透明度是动画的,QSS 无法表达 -3. **复杂动画**:涟漪扩散、弹性动画等需要精确控制,QSS 的 transition 过于简单 -4. **性能考虑**:QSS 的解析和应用有性能开销,直接绘制更高效 - -所以我们选择在 paintEvent 中完全接管绘制。 - -## 只读主题原则 - -控件只能读取主题数据,不能修改。这是一个硬性约束: - -```cpp -// 错误!不要这样做 -auto& theme = ThemeManager::instance().currentTheme(); -theme.color_scheme().setColor("md.primary", QColor("#FF0000")); -``` - -修改主题应该通过 ThemeManager 的 `setThemeTo()` 方法,或者在主题创建时指定颜色。这保证了主题的全局一致性。 - -## 适配器的最小化 - -理想情况下,控件适配器应该足够"薄",以至于你可以用一个表格来描述它的事件处理: - -| 事件 | 父类调用 | 行为组件调用 | -|---|---:|---| -| mousePressEvent | ✓ | StateMachine::onPress, RippleHelper::onPress | -| mouseReleaseEvent | ✓ | StateMachine::onRelease, RippleHelper::onRelease | -| enterEvent | ✓ | StateMachine::onHoverEnter | -| leaveEvent | ✓ | StateMachine::onHoverLeave | -| focusInEvent | ✓ | StateMachine::onFocusIn, FocusIndicator::onFocusIn | -| focusOutEvent | ✓ | StateMachine::onFocusOut, FocusIndicator::onFocusOut | -| paintEvent | ✗ | 完全重写,按顺序绘制各层 | - -这种模式的一致性让新控件的实现变得简单:只需要按照模板填充对应的行为组件调用即可。 - -## 总结 - -适配器模式的核心思想是"薄包装":继承 Qt 原生控件,只做事件转发和自定义绘制,通过组合行为组件获得 Material 特性。这种设计保持了代码的清晰和可维护性。 - -但适配器模式只是理论框架,具体的实现细节还需要深入探讨。Button 控件作为我们实现最完整的控件,是一个很好的研究案例。 - -接下来,我们深入 Button 控件的实现。 - ---- - -**相关文档** - -- [状态机设计](../layer-4-material-behavior/01-state-machine.md)——行为组件之一 -- [Button 控件深度解析](./02-button-deep-dive.md)——完整的实现案例 -- [主题系统架构设计](../layer-2-theme-engine/01-theme-system-design.md)——Layer 2 的整体设计 +--- +title: "适配器模式——Material 控件的\"薄包装\"设计" +description: 在前面的文章里,我们聊完了基础数学工具、主题引擎、动画系统、行为组件。现在所有的基础设施都就绪了,可 +--- + +# 适配器模式——Material 控件的"薄包装"设计 + +在前面的文章里,我们聊完了基础数学工具、主题引擎、动画系统、行为组件。现在所有的基础设施都就绪了,可以开始构建具体的控件了。 + +这篇文章聊聊控件适配层的设计理念。 + +## 继承 vs 组合的选择 + +一个经典的问题是:为什么必须继承 Qt 原生控件? + +有几个重要原因: + +1. **Qt 事件体系的要求**:Qt 的信号机制依赖于事件处理函数的正确调用顺序。如果不继承,就需要手动管理信号,复杂且容易出错 +2. **布局系统集成**:Qt 的布局管理器(QVBoxLayout、QGridLayout 等)直接与 QWidget 接口交互 +3. **QSS 兼容性**:虽然我们不使用 QSS 实现 Material 效果,但保持继承关系让 QSS 仍然可用于某些全局设置 +4. **第三方库兼容**:很多 Qt 库假设控件是 QWidget 的子类 + +所以我们选择继承 Qt 原生控件,但只做最小的修改——事件转发和 paintEvent 重写。 + +## "只做三件事"的设计原则 + +Material 控件适配器只做三件事: + +1. **事件转发**:在事件 override 中调用父类方法后,转发给行为组件 +2. **paintEvent 重写**:按顺序调用渲染层的 paint 方法 +3. **主题数据读取**:从 ThemeManager 读取 Token 决定颜色、形状、字体 + +任何不属于这三件事的逻辑都应该被拒绝——这保持了控件的"薄"特性,让行为组件保持独立。 + +## 事件转发模式 + +事件处理的标准模板是: + +```cpp +void Button::mousePressEvent(QMouseEvent* event) { + // 1. 先调用父类方法(必须!) + QPushButton::mousePressEvent(event); + + // 2. 转发给行为组件 + m_stateMachine->onPress(event->pos()); + m_ripple->onPress(event->pos(), rect()); +} +```text + +第一步调用父类方法是必须的,原因如下: + +- Qt 需要在事件处理中更新内部状态 +- `clicked()` 等信号在父类方法中发出 +- 跳过父类方法会导致信号不发出、状态不一致等问题 + +## 组件组合模式 + +控件通过组合行为组件来获得 Material 特性: + +```cpp +Button::Button(ButtonVariant variant, QWidget* parent) + : QPushButton(parent), variant_(variant) { + + // 获取动画工厂 + m_animationFactory = Application::animationFactory(); + + // 创建行为组件 + 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); + + // 连接信号 + connect(m_ripple, &RippleHelper::repaintNeeded, + this, QOverload<>::of(&Button::update)); + connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, + this, [this](float) { update(); }); + + // 监听主题变化 + connect(&ThemeManager::instance(), &ThemeManager::themeChanged, + this, [this](const ICFTheme&) { update(); }); +} +```text + +注意所有行为组件的 parent 都是 `this`,这意味着它们会随控件一起销毁,无需手动管理生命周期。 + +## 主题数据读取 + +控件在绘制时从主题读取数据: + +```cpp +QColor Button::containerColor() const { + auto& theme = ThemeManager::instance().currentTheme(); + switch (variant_) { + case ButtonVariant::Filled: + return theme.color_scheme().queryColor("md.primaryContainer"); + case ButtonVariant::Tonal: + return theme.color_scheme().queryColor("md.secondaryContainer"); + // ... + } +} +```text + +这里使用 `currentTheme()` 获取当前活动的主题。如果主题切换,`themeChanged` 信号会触发 `update()`,控件会重新读取主题数据并重绘。 + +## 为什么不用 QSS? + +这是一个经常被问到的问题。Material Design 3 的视觉需求超出了 QSS 的能力: + +1. **多层叠加**:状态层、涟漪层、阴影层需要按特定顺序绘制,QSS 不支持 +2. **动态透明度**:状态层的透明度是动画的,QSS 无法表达 +3. **复杂动画**:涟漪扩散、弹性动画等需要精确控制,QSS 的 transition 过于简单 +4. **性能考虑**:QSS 的解析和应用有性能开销,直接绘制更高效 + +所以我们选择在 paintEvent 中完全接管绘制。 + +## 只读主题原则 + +控件只能读取主题数据,不能修改。这是一个硬性约束: + +```cpp +// 错误!不要这样做 +auto& theme = ThemeManager::instance().currentTheme(); +theme.color_scheme().setColor("md.primary", QColor("#FF0000")); +```bash + +修改主题应该通过 ThemeManager 的 `setThemeTo()` 方法,或者在主题创建时指定颜色。这保证了主题的全局一致性。 + +## 适配器的最小化 + +理想情况下,控件适配器应该足够"薄",以至于你可以用一个表格来描述它的事件处理: + +| 事件 | 父类调用 | 行为组件调用 | +|---|---:|---| +| mousePressEvent | ✓ | StateMachine::onPress, RippleHelper::onPress | +| mouseReleaseEvent | ✓ | StateMachine::onRelease, RippleHelper::onRelease | +| enterEvent | ✓ | StateMachine::onHoverEnter | +| leaveEvent | ✓ | StateMachine::onHoverLeave | +| focusInEvent | ✓ | StateMachine::onFocusIn, FocusIndicator::onFocusIn | +| focusOutEvent | ✓ | StateMachine::onFocusOut, FocusIndicator::onFocusOut | +| paintEvent | ✗ | 完全重写,按顺序绘制各层 | + +这种模式的一致性让新控件的实现变得简单:只需要按照模板填充对应的行为组件调用即可。 + +## 总结 + +适配器模式的核心思想是"薄包装":继承 Qt 原生控件,只做事件转发和自定义绘制,通过组合行为组件获得 Material 特性。这种设计保持了代码的清晰和可维护性。 + +但适配器模式只是理论框架,具体的实现细节还需要深入探讨。Button 控件作为我们实现最完整的控件,是一个很好的研究案例。 + +接下来,我们深入 Button 控件的实现。 + +--- + +**相关文档** + +- [状态机设计](../layer-4-material-behavior/01-state-machine.md)——行为组件之一 +- [Button 控件深度解析](./02-button-deep-dive.md)——完整的实现案例 +- [主题系统架构设计](../layer-2-theme-engine/01-theme-system-design.md)——Layer 2 的整体设计 diff --git a/document/HandBook/ui/architecture/layer-5-widget-adapter/02-button-deep-dive.md b/document/HandBook/ui/architecture/layer-5-widget-adapter/02-button-deep-dive.md index 278c88354..08d6f7174 100644 --- a/document/HandBook/ui/architecture/layer-5-widget-adapter/02-button-deep-dive.md +++ b/document/HandBook/ui/architecture/layer-5-widget-adapter/02-button-deep-dive.md @@ -1,292 +1,297 @@ -# Button 控件深度解析——七步绘制流程的完整实现 - -在上一篇文章里,我们聊了控件适配层的"薄包装"设计理念。这篇文章以 Button 控件为例,完整展示这个理念如何付诸实践。 - -Button 是我们实现最完整的控件,可以作为所有其他控件的参考模板。 - -## 五种按钮变体 - -Material Design 3 定义了五种按钮变体,每种有不同的视觉用途: - -| 变体 | 用途 | 视觉特点 | -|---|---|---| -| Filled | 主要操作 | 填充 Primary 颜色 | -| Tonal | 次要操作 | 填充 SecondaryContainer 颜色 | -| Outlined | 次要操作 | 无填充,有边框 | -| Text | 最低优先级 | 无填充,无边框,仅文本 | -| Elevated | 悬浮元素 | 有阴影的海拔效果 | - -代码中用枚举表示: - -```cpp -enum class ButtonVariant { - Filled, - Tonal, - Outlined, - Text, - Elevated -}; -``` - -## 构造函数中的组件初始化 - -Button 的构造函数创建所有行为组件并连接信号: - -```cpp -Button::Button(ButtonVariant variant, QWidget* parent) - : QPushButton(parent), variant_(variant) { - - // 获取动画工厂 - m_animationFactory = Application::animationFactory(); - - // 创建行为组件 - 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); - - // 设置初始海拔 - if (variant_ == ButtonVariant::Elevated) { - m_elevation->setElevation(1); // Elevated 按钮有 1dp 海拔 - } - - // 连接信号 - connect(m_ripple, &RippleHelper::repaintNeeded, - this, QOverload<>::of(&Button::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, - this, [this](float) { update(); }); - - // 监听主题变化 - connect(&ThemeManager::instance(), &ThemeManager::themeChanged, - this, [this](const ICFTheme&) { update(); }); -} -``` - -## 事件处理的标准实现 - -每个事件处理函数都遵循相同的模式:先调用父类方法,再转发给行为组件。 - -```cpp -void Button::mousePressEvent(QMouseEvent* event) { - QPushButton::mousePressEvent(event); - m_stateMachine->onPress(event->pos()); - m_ripple->onPress(event->pos(), rect()); -} - -void Button::mouseReleaseEvent(QMouseEvent* event) { - QPushButton::mouseReleaseEvent(event); - m_stateMachine->onRelease(); - m_ripple->onRelease(); -} - -void Button::enterEvent(QEnterEvent* event) { - QPushButton::enterEvent(event); - m_stateMachine->onHoverEnter(); -} - -void Button::leaveEvent(QEvent* event) { - QPushButton::leaveEvent(event); - m_stateMachine->onHoverLeave(); -} - -void Button::focusInEvent(QFocusEvent* event) { - QPushButton::focusInEvent(event); - m_stateMachine->onFocusIn(); - m_focusIndicator->onFocusIn(); -} - -void Button::focusOutEvent(QFocusEvent* event) { - QPushButton::focusOutEvent(event); - m_stateMachine->onFocusOut(); - m_focusIndicator->onFocusOut(); -} -``` - -## 七步绘制流程 - -paintEvent 是整个控件的核心,它按七个固定步骤绘制所有视觉元素: - -```cpp -void Button::paintEvent(QPaintEvent* event) { - QPainter painter(this); - - // 准备工作 - painter.setRenderHint(QPainter::Antialiasing); - - // 计算按压偏移 - float pressOffset = m_pressEffectEnabled ? m_elevation->pressOffset() : 0.0f; - QRectF contentRect = rect().adjusted(-margin, -margin, margin, margin); - contentRect.translate(0, pressOffset); - - // 创建形状路径 - QPainterPath shape = geometry::roundedRect(contentRect, cornerRadius()); - - // 七步绘制管道 - drawShadow(painter, contentRect, shape); // 1. 阴影层 - drawBackground(painter, shape); // 2. 背景色 - drawStateLayer(painter, shape); // 3. 状态叠加层 - drawRipple(painter, shape); // 4. 涟漪效果 - if (variant_ == ButtonVariant::Outlined) { - drawOutline(painter, shape); // 5. 边框(仅Outlined) - } - drawContent(painter, contentRect); // 6. 内容(图标+文本) - drawFocusIndicator(painter, shape); // 7. 焦点环 -} -``` - -让我们逐步拆解这个流程。 - -### 第一步:绘制阴影 - -阴影位于最底层,需要在背景之前绘制。对于 Elevated 变体,使用 `MdElevationController` 的阴影绘制;对于其他变体,可以跳过或使用更简单的阴影。 - -```cpp -void Button::drawShadow(QPainter& p, const QRectF& contentRect, const QPainterPath& shape) { - if (variant_ == ButtonVariant::Elevated) { - m_elevation->paintShadow(&p, shape); - } - // 其他变体没有阴影(或只有轻微阴影) -} -``` - -### 第二步:绘制背景 - -背景色根据变体类型从主题获取: - -```cpp -void Button::drawBackground(QPainter& p, const QPainterPath& shape) { - QColor bgColor = containerColor(); - p.fillPath(shape, bgColor); -} - -QColor Button::containerColor() const { - auto& theme = ThemeManager::instance().currentTheme(); - switch (variant_) { - case ButtonVariant::Filled: - return theme.color_scheme().queryColor("md.primary"); - case ButtonVariant::Tonal: - return theme.color_scheme().queryColor("md.secondaryContainer"); - case ButtonVariant::Outlined: - case ButtonVariant::Text: - return Qt::transparent; // 无背景 - case ButtonVariant::Elevated: - return theme.color_scheme().queryColor("md.surfaceContainerLow"); - } - return Qt::black; // fallback -} -``` - -### 第三步:绘制状态层 - -状态层是一个半透明的叠加层,用于表示交互状态(Hover、Pressed 等): - -```cpp -void Button::drawStateLayer(QPainter& p, const QPainterPath& shape) { - float opacity = m_stateMachine->stateLayerOpacity(); - if (opacity > 0.001f) { - QColor stateColor = stateLayerColor(); - stateColor.setAlphaF(opacity); - p.fillPath(shape, stateColor); - } -} - -QColor Button::stateLayerColor() const { - auto& theme = ThemeManager::instance().currentTheme(); - switch (variant_) { - case ButtonVariant::Filled: - case ButtonVariant::Elevated: - return theme.color_scheme().queryColor("md.onPrimary"); - case ButtonVariant::Tonal: - return theme.color_scheme().queryColor("md.onSecondaryContainer"); - case ButtonVariant::Outlined: - return theme.color_scheme().queryColor("md.primary"); - case ButtonVariant::Text: - return theme.color_scheme().queryColor("md.primary"); - } - return Qt::white; -} -``` - -### 第四步:绘制涟漪 - -涟漪由 `RippleHelper` 绘制,它会处理多个涟漪的叠加和动画: - -```cpp -void Button::drawRipple(QPainter& p, const QPainterPath& shape) { - m_ripple->paint(&p, shape); -} -``` - -### 第五步:绘制边框 - -只有 Outlined 变体有边框: - -```cpp -void Button::drawOutline(QPainter& p, const QPainterPath& shape) { - if (variant_ != ButtonVariant::Outlined) { - return; - } - - QColor outlineColor = this->outlineColor(); - CanvasUnitHelper helper(qApp->devicePixelRatio()); - float borderWidth = helper.dpToPx(1.0f); - - QPen pen(outlineColor, borderWidth); - pen.setCosmetic(true); - p.setPen(pen); - p.setBrush(Qt::NoBrush); - p.drawPath(shape); -} -``` - -### 第六步:绘制内容 - -内容包括文本和可选的前置图标: - -```cpp -void Button::drawContent(QPainter& p, const QRectF& contentRect) { - // 设置字体 - p.setFont(labelFont()); - p.setPen(labelColor()); - - // 计算内容区域 - QRectF textRect = contentRect.adjusted(paddingH(), 0, -paddingH(), 0); - - // 如果有图标,绘制图标 - if (!leadingIcon_.isNull()) { - // ... 绘制图标 - textRect.adjust(iconSize + spacing, 0, 0, 0); // 调整文本区域 - } - - // 绘制文本 - p.drawText(textRect, Qt::AlignCenter, text()); -} -``` - -### 第七步:绘制焦点环 - -焦点环由 `MdFocusIndicator` 绘制,位于最顶层: - -```cpp -void Button::drawFocusIndicator(QPainter& p, const QPainterPath& shape) { - m_focusIndicator->paint(&p, rect(), cornerRadius()); -} -``` - -## 总结 - -Button 控件的实现展示了"薄包装"设计的精髓:继承 Qt 原生控件,通过组合行为组件获得 Material 特性,在 paintEvent 中按固定顺序绘制各层。 - -这个模式可以复用到其他控件:CheckBox、TextField、ComboBox 等都遵循相同的设计思路,只是组合的行为组件和绘制的元素有所不同。 - -但单个控件的实现只是第一步,我们还需要考虑整体绘制管道的性能优化。 - -接下来,我们聊聊绘制管道的性能考量。 - ---- - -**相关文档** - -- [适配器模式](./01-adapter-pattern.md)——控件适配层的设计理念 -- [绘制管道优化](./03-painting-pipeline.md)——性能优化技巧 -- [涟漪与阴影](../layer-4-material-behavior/02-ripple-and-elevation.md)——行为组件的使用 +--- +title: Button 控件深度解析——七步绘制流程的完整实现 +description: "在上一篇文章里,我们聊了控件适配层的\"薄包装\"设计理念。这篇文章以 Button 控件为例,完整展示" +--- + +# Button 控件深度解析——七步绘制流程的完整实现 + +在上一篇文章里,我们聊了控件适配层的"薄包装"设计理念。这篇文章以 Button 控件为例,完整展示这个理念如何付诸实践。 + +Button 是我们实现最完整的控件,可以作为所有其他控件的参考模板。 + +## 五种按钮变体 + +Material Design 3 定义了五种按钮变体,每种有不同的视觉用途: + +| 变体 | 用途 | 视觉特点 | +|---|---|---| +| Filled | 主要操作 | 填充 Primary 颜色 | +| Tonal | 次要操作 | 填充 SecondaryContainer 颜色 | +| Outlined | 次要操作 | 无填充,有边框 | +| Text | 最低优先级 | 无填充,无边框,仅文本 | +| Elevated | 悬浮元素 | 有阴影的海拔效果 | + +代码中用枚举表示: + +```cpp +enum class ButtonVariant { + Filled, + Tonal, + Outlined, + Text, + Elevated +}; +```text + +## 构造函数中的组件初始化 + +Button 的构造函数创建所有行为组件并连接信号: + +```cpp +Button::Button(ButtonVariant variant, QWidget* parent) + : QPushButton(parent), variant_(variant) { + + // 获取动画工厂 + m_animationFactory = Application::animationFactory(); + + // 创建行为组件 + 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); + + // 设置初始海拔 + if (variant_ == ButtonVariant::Elevated) { + m_elevation->setElevation(1); // Elevated 按钮有 1dp 海拔 + } + + // 连接信号 + connect(m_ripple, &RippleHelper::repaintNeeded, + this, QOverload<>::of(&Button::update)); + connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, + this, [this](float) { update(); }); + + // 监听主题变化 + connect(&ThemeManager::instance(), &ThemeManager::themeChanged, + this, [this](const ICFTheme&) { update(); }); +} +```text + +## 事件处理的标准实现 + +每个事件处理函数都遵循相同的模式:先调用父类方法,再转发给行为组件。 + +```cpp +void Button::mousePressEvent(QMouseEvent* event) { + QPushButton::mousePressEvent(event); + m_stateMachine->onPress(event->pos()); + m_ripple->onPress(event->pos(), rect()); +} + +void Button::mouseReleaseEvent(QMouseEvent* event) { + QPushButton::mouseReleaseEvent(event); + m_stateMachine->onRelease(); + m_ripple->onRelease(); +} + +void Button::enterEvent(QEnterEvent* event) { + QPushButton::enterEvent(event); + m_stateMachine->onHoverEnter(); +} + +void Button::leaveEvent(QEvent* event) { + QPushButton::leaveEvent(event); + m_stateMachine->onHoverLeave(); +} + +void Button::focusInEvent(QFocusEvent* event) { + QPushButton::focusInEvent(event); + m_stateMachine->onFocusIn(); + m_focusIndicator->onFocusIn(); +} + +void Button::focusOutEvent(QFocusEvent* event) { + QPushButton::focusOutEvent(event); + m_stateMachine->onFocusOut(); + m_focusIndicator->onFocusOut(); +} +```text + +## 七步绘制流程 + +paintEvent 是整个控件的核心,它按七个固定步骤绘制所有视觉元素: + +```cpp +void Button::paintEvent(QPaintEvent* event) { + QPainter painter(this); + + // 准备工作 + painter.setRenderHint(QPainter::Antialiasing); + + // 计算按压偏移 + float pressOffset = m_pressEffectEnabled ? m_elevation->pressOffset() : 0.0f; + QRectF contentRect = rect().adjusted(-margin, -margin, margin, margin); + contentRect.translate(0, pressOffset); + + // 创建形状路径 + QPainterPath shape = geometry::roundedRect(contentRect, cornerRadius()); + + // 七步绘制管道 + drawShadow(painter, contentRect, shape); // 1. 阴影层 + drawBackground(painter, shape); // 2. 背景色 + drawStateLayer(painter, shape); // 3. 状态叠加层 + drawRipple(painter, shape); // 4. 涟漪效果 + if (variant_ == ButtonVariant::Outlined) { + drawOutline(painter, shape); // 5. 边框(仅Outlined) + } + drawContent(painter, contentRect); // 6. 内容(图标+文本) + drawFocusIndicator(painter, shape); // 7. 焦点环 +} +```text + +让我们逐步拆解这个流程。 + +### 第一步:绘制阴影 + +阴影位于最底层,需要在背景之前绘制。对于 Elevated 变体,使用 `MdElevationController` 的阴影绘制;对于其他变体,可以跳过或使用更简单的阴影。 + +```cpp +void Button::drawShadow(QPainter& p, const QRectF& contentRect, const QPainterPath& shape) { + if (variant_ == ButtonVariant::Elevated) { + m_elevation->paintShadow(&p, shape); + } + // 其他变体没有阴影(或只有轻微阴影) +} +```text + +### 第二步:绘制背景 + +背景色根据变体类型从主题获取: + +```cpp +void Button::drawBackground(QPainter& p, const QPainterPath& shape) { + QColor bgColor = containerColor(); + p.fillPath(shape, bgColor); +} + +QColor Button::containerColor() const { + auto& theme = ThemeManager::instance().currentTheme(); + switch (variant_) { + case ButtonVariant::Filled: + return theme.color_scheme().queryColor("md.primary"); + case ButtonVariant::Tonal: + return theme.color_scheme().queryColor("md.secondaryContainer"); + case ButtonVariant::Outlined: + case ButtonVariant::Text: + return Qt::transparent; // 无背景 + case ButtonVariant::Elevated: + return theme.color_scheme().queryColor("md.surfaceContainerLow"); + } + return Qt::black; // fallback +} +```text + +### 第三步:绘制状态层 + +状态层是一个半透明的叠加层,用于表示交互状态(Hover、Pressed 等): + +```cpp +void Button::drawStateLayer(QPainter& p, const QPainterPath& shape) { + float opacity = m_stateMachine->stateLayerOpacity(); + if (opacity > 0.001f) { + QColor stateColor = stateLayerColor(); + stateColor.setAlphaF(opacity); + p.fillPath(shape, stateColor); + } +} + +QColor Button::stateLayerColor() const { + auto& theme = ThemeManager::instance().currentTheme(); + switch (variant_) { + case ButtonVariant::Filled: + case ButtonVariant::Elevated: + return theme.color_scheme().queryColor("md.onPrimary"); + case ButtonVariant::Tonal: + return theme.color_scheme().queryColor("md.onSecondaryContainer"); + case ButtonVariant::Outlined: + return theme.color_scheme().queryColor("md.primary"); + case ButtonVariant::Text: + return theme.color_scheme().queryColor("md.primary"); + } + return Qt::white; +} +```text + +### 第四步:绘制涟漪 + +涟漪由 `RippleHelper` 绘制,它会处理多个涟漪的叠加和动画: + +```cpp +void Button::drawRipple(QPainter& p, const QPainterPath& shape) { + m_ripple->paint(&p, shape); +} +```text + +### 第五步:绘制边框 + +只有 Outlined 变体有边框: + +```cpp +void Button::drawOutline(QPainter& p, const QPainterPath& shape) { + if (variant_ != ButtonVariant::Outlined) { + return; + } + + QColor outlineColor = this->outlineColor(); + CanvasUnitHelper helper(qApp->devicePixelRatio()); + float borderWidth = helper.dpToPx(1.0f); + + QPen pen(outlineColor, borderWidth); + pen.setCosmetic(true); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + p.drawPath(shape); +} +```text + +### 第六步:绘制内容 + +内容包括文本和可选的前置图标: + +```cpp +void Button::drawContent(QPainter& p, const QRectF& contentRect) { + // 设置字体 + p.setFont(labelFont()); + p.setPen(labelColor()); + + // 计算内容区域 + QRectF textRect = contentRect.adjusted(paddingH(), 0, -paddingH(), 0); + + // 如果有图标,绘制图标 + if (!leadingIcon_.isNull()) { + // ... 绘制图标 + textRect.adjust(iconSize + spacing, 0, 0, 0); // 调整文本区域 + } + + // 绘制文本 + p.drawText(textRect, Qt::AlignCenter, text()); +} +```text + +### 第七步:绘制焦点环 + +焦点环由 `MdFocusIndicator` 绘制,位于最顶层: + +```cpp +void Button::drawFocusIndicator(QPainter& p, const QPainterPath& shape) { + m_focusIndicator->paint(&p, rect(), cornerRadius()); +} +```yaml + +## 总结 + +Button 控件的实现展示了"薄包装"设计的精髓:继承 Qt 原生控件,通过组合行为组件获得 Material 特性,在 paintEvent 中按固定顺序绘制各层。 + +这个模式可以复用到其他控件:CheckBox、TextField、ComboBox 等都遵循相同的设计思路,只是组合的行为组件和绘制的元素有所不同。 + +但单个控件的实现只是第一步,我们还需要考虑整体绘制管道的性能优化。 + +接下来,我们聊聊绘制管道的性能考量。 + +--- + +**相关文档** + +- [适配器模式](./01-adapter-pattern.md)——控件适配层的设计理念 +- [绘制管道优化](./03-painting-pipeline.md)——性能优化技巧 +- [涟漪与阴影](../layer-4-material-behavior/02-ripple-and-elevation.md)——行为组件的使用 diff --git a/document/HandBook/ui/architecture/layer-5-widget-adapter/03-painting-pipeline.md b/document/HandBook/ui/architecture/layer-5-widget-adapter/03-painting-pipeline.md index 667864438..a42dba8f1 100644 --- a/document/HandBook/ui/architecture/layer-5-widget-adapter/03-painting-pipeline.md +++ b/document/HandBook/ui/architecture/layer-5-widget-adapter/03-painting-pipeline.md @@ -1,202 +1,207 @@ -# 绘制管道优化——从单一控件到批量渲染的性能考量 - -在上一篇文章里,我们深入分析了 Button 控件的七步绘制流程。每个步骤看起来都很简单,但当页面上有几十个控件时,性能问题就会显现。 - -这篇文章聊聊绘制管道的性能优化。 - -## QPainter 的初始化配置 - -在 paintEvent 开始时,我们通常需要配置 QPainter: - -```cpp -void Button::paintEvent(QPaintEvent* event) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); // 抗锯齿 - painter.setRenderHint(QPainter::SmoothPixmapTransform); // 平滑图像变换 - - // ... 绘制代码 -} -``` - -这两个设置会影响绘制质量: - -- `Antialiasing`:让边缘更平滑,但会有性能开销 -- `SmoothPixmapTransform`:让图像缩放更平滑,同样有性能开销 - -对于大多数控件,这两个设置是值得的。但在性能敏感的场景,可以考虑关闭。 - -## 避免不必要的重绘 - -重绘(`update()`)触发的 paintEvent 是昂贵的操作。我们应该只在真正需要时才触发重绘: - -```cpp -// 不要这样做 -void setChecked(bool checked) { - checked_ = checked; - update(); // 总是重绘 -} - -// 更好的做法 -void setChecked(bool checked) { - if (checked_ != checked) { - checked_ = checked; - update(); // 只在状态改变时重绘 - } -} -``` - -状态机已经做了这个优化——只有当透明度值真正改变时,才会发出 `stateLayerOpacityChanged` 信号。 - -## 状态缓存策略 - -很多值在绘制时需要从主题获取,但频繁查询主题有开销。我们可以缓存这些值: - -```cpp -class Button { -private: - void refreshThemeCache() { - auto& theme = ThemeManager::instance().currentTheme(); - cachedContainerColor_ = theme.color_scheme().queryColor("md.primary"); - cachedLabelColor_ = theme.color_scheme().queryColor("md.onPrimary"); - cachedCornerRadius_ = theme.radius_scale().queryRadiusScale("cornerFull"); - // ... - } - - QColor cachedContainerColor_; - QColor cachedLabelColor_; - float cachedCornerRadius_; -}; -``` - -在 `themeChanged` 信号处理中刷新缓存: - -```cpp -connect(&ThemeManager::instance(), &ThemeManager::themeChanged, - this, [this](const ICFTheme&) { - refreshThemeCache(); - update(); - }); -``` - -这样每次绘制时就不需要查询主题了。 - -## 裁剪区域的优化 - -绘制涟漪时,我们需要裁剪到控件边界: - -```cpp -void Button::drawRipple(QPainter& p, const QPainterPath& shape) { - QPainterPath clipPath = geometry::roundedRect(rect(), cornerRadius()); - m_ripple->paint(&p, clipPath); -} -``` - -Qt 的 `setClipPath` 操作比较昂贵,因为需要计算复杂的几何。如果控件形状简单(比如矩形),可以考虑用 `setClipRect` 替代: - -```cpp -painter.setClipRect(rect(), Qt::IntersectClip); // 更高效的矩形裁剪 -``` - -## 静态内容的预渲染 - -对于完全不变化的内容(比如某些背景图案),可以考虑预渲染为 QPixmap: - -```cpp -void Button::cacheBackground() { - QPixmap cache(size()); - cache.fill(Qt::transparent); - - QPainter painter(&cache); - // ... 绘制静态背景 - painter.end(); - - cachedBackground_ = cache; -} - -void Button::paintEvent(QPaintEvent* event) { - QPainter painter(this); - painter.drawPixmap(0, 0, cachedBackground_); // 直接绘制缓存的图像 - // ... 绘制动态内容 -} -``` - -但对于大多数控件来说,这个优化可能不值得——因为背景色会随主题切换而改变。 - -## 批量重绘的考虑 - -当主题切换时,所有订阅了主题的控件都会重绘。如果页面上有大量控件,这会导致卡顿。 - -一个优化策略是"批量重绘":主题切换时,先标记所有控件为"脏",然后在下一个事件循环中统一重绘: - -```cpp -// 主题切换时 -for (auto* widget : installed_widgets) { - widget->setAttribute(Qt::WA_WState_InPaintEvent, true); // 标记为脏 -} -QTimer::singleShot(0, this, []() { - for (auto* widget : installed_widgets) { - widget->setAttribute(Qt::WA_WState_InPaintEvent, false); - widget->update(); - } -}); -``` - -这样可以将多次重绘合并为一次。 - -## 嵌入式环境的特殊考虑 - -在嵌入式环境中,可能需要禁用某些视觉效果来提升性能: - -```cpp -enum class PerformanceProfile { - Desktop, // 全功能:60fps,阴影,Ripple,所有动画 - Embedded, // 精简:30fps,无阴影,无 Ripple,仅状态切换动画 - UltraLow // 极简:无动画,无阴影,仅颜色变化 -}; - -void Button::paintEvent(QPaintEvent* event) { - auto profile = Application::performanceProfile(); - - if (profile == PerformanceProfile::UltraLow) { - // 跳过所有动画和特效 - drawBackground(); - drawContent(); - return; - } - - if (profile == PerformanceProfile::Embedded) { - // 跳过阴影和涟漪 - drawBackground(); - drawStateLayer(); - drawContent(); - return; - } - - // Desktop 配置:绘制所有效果 - // ... 完整的七步流程 -} -``` - -## 总结 - -绘制管道优化的核心思想是"避免不必要的工作":避免不必要的重绘、缓存不变的值、使用更高效的 API。 - -但也要注意"过早优化是万恶之源"。在实际问题出现之前,保持代码的清晰和可维护性更重要。只有当性能测试显示确实存在瓶颈时,才应该考虑这些优化。 - -到这里,我们的"桌面主题架构设计系列"博客就全部完成了。我们从基础数学工具开始,一层一层地构建了完整的 Material Design 3 实现: - -- **Layer 1**:HCT 色彩空间、数学工具、几何和 DPI 适配 -- **Layer 2**:主题系统、Token 设计、颜色方案、字体形状动效 -- **Layer 3**:动画引擎、时间与弹簧动画、工厂与策略 -- **Layer 4**:状态机、涟漪与阴影、焦点指示器 -- **Layer 5**:适配器模式、Button 控件、绘制管道优化 - -希望这个系列能帮助你理解 Material Design 3 桌面主题的完整架构,以及我们为什么做出这样的设计决策。 - ---- - -**相关文档** - -- [Button 控件深度解析](./02-button-deep-dive.md)——控件实现的完整案例 -- [适配器模式](./01-adapter-pattern.md)——控件适配层的设计理念 -- [动画引擎架构](../layer-3-animation-engine/01-animation-architecture.md)——动画系统的性能考量 +--- +title: 绘制管道优化——从单一控件到批量渲染的性能考量 +description: 在上一篇文章里,我们深入分析了 Button 控件的七步绘制流程。每个步骤看起来都很简单,但当页面上 +--- + +# 绘制管道优化——从单一控件到批量渲染的性能考量 + +在上一篇文章里,我们深入分析了 Button 控件的七步绘制流程。每个步骤看起来都很简单,但当页面上有几十个控件时,性能问题就会显现。 + +这篇文章聊聊绘制管道的性能优化。 + +## QPainter 的初始化配置 + +在 paintEvent 开始时,我们通常需要配置 QPainter: + +```cpp +void Button::paintEvent(QPaintEvent* event) { + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); // 抗锯齿 + painter.setRenderHint(QPainter::SmoothPixmapTransform); // 平滑图像变换 + + // ... 绘制代码 +} +```text + +这两个设置会影响绘制质量: + +- `Antialiasing`:让边缘更平滑,但会有性能开销 +- `SmoothPixmapTransform`:让图像缩放更平滑,同样有性能开销 + +对于大多数控件,这两个设置是值得的。但在性能敏感的场景,可以考虑关闭。 + +## 避免不必要的重绘 + +重绘(`update()`)触发的 paintEvent 是昂贵的操作。我们应该只在真正需要时才触发重绘: + +```cpp +// 不要这样做 +void setChecked(bool checked) { + checked_ = checked; + update(); // 总是重绘 +} + +// 更好的做法 +void setChecked(bool checked) { + if (checked_ != checked) { + checked_ = checked; + update(); // 只在状态改变时重绘 + } +} +```text + +状态机已经做了这个优化——只有当透明度值真正改变时,才会发出 `stateLayerOpacityChanged` 信号。 + +## 状态缓存策略 + +很多值在绘制时需要从主题获取,但频繁查询主题有开销。我们可以缓存这些值: + +```cpp +class Button { +private: + void refreshThemeCache() { + auto& theme = ThemeManager::instance().currentTheme(); + cachedContainerColor_ = theme.color_scheme().queryColor("md.primary"); + cachedLabelColor_ = theme.color_scheme().queryColor("md.onPrimary"); + cachedCornerRadius_ = theme.radius_scale().queryRadiusScale("cornerFull"); + // ... + } + + QColor cachedContainerColor_; + QColor cachedLabelColor_; + float cachedCornerRadius_; +}; +```text + +在 `themeChanged` 信号处理中刷新缓存: + +```cpp +connect(&ThemeManager::instance(), &ThemeManager::themeChanged, + this, [this](const ICFTheme&) { + refreshThemeCache(); + update(); + }); +```text + +这样每次绘制时就不需要查询主题了。 + +## 裁剪区域的优化 + +绘制涟漪时,我们需要裁剪到控件边界: + +```cpp +void Button::drawRipple(QPainter& p, const QPainterPath& shape) { + QPainterPath clipPath = geometry::roundedRect(rect(), cornerRadius()); + m_ripple->paint(&p, clipPath); +} +```text + +Qt 的 `setClipPath` 操作比较昂贵,因为需要计算复杂的几何。如果控件形状简单(比如矩形),可以考虑用 `setClipRect` 替代: + +```cpp +painter.setClipRect(rect(), Qt::IntersectClip); // 更高效的矩形裁剪 +```text + +## 静态内容的预渲染 + +对于完全不变化的内容(比如某些背景图案),可以考虑预渲染为 QPixmap: + +```cpp +void Button::cacheBackground() { + QPixmap cache(size()); + cache.fill(Qt::transparent); + + QPainter painter(&cache); + // ... 绘制静态背景 + painter.end(); + + cachedBackground_ = cache; +} + +void Button::paintEvent(QPaintEvent* event) { + QPainter painter(this); + painter.drawPixmap(0, 0, cachedBackground_); // 直接绘制缓存的图像 + // ... 绘制动态内容 +} +```text + +但对于大多数控件来说,这个优化可能不值得——因为背景色会随主题切换而改变。 + +## 批量重绘的考虑 + +当主题切换时,所有订阅了主题的控件都会重绘。如果页面上有大量控件,这会导致卡顿。 + +一个优化策略是"批量重绘":主题切换时,先标记所有控件为"脏",然后在下一个事件循环中统一重绘: + +```cpp +// 主题切换时 +for (auto* widget : installed_widgets) { + widget->setAttribute(Qt::WA_WState_InPaintEvent, true); // 标记为脏 +} +QTimer::singleShot(0, this, []() { + for (auto* widget : installed_widgets) { + widget->setAttribute(Qt::WA_WState_InPaintEvent, false); + widget->update(); + } +}); +```text + +这样可以将多次重绘合并为一次。 + +## 嵌入式环境的特殊考虑 + +在嵌入式环境中,可能需要禁用某些视觉效果来提升性能: + +```cpp +enum class PerformanceProfile { + Desktop, // 全功能:60fps,阴影,Ripple,所有动画 + Embedded, // 精简:30fps,无阴影,无 Ripple,仅状态切换动画 + UltraLow // 极简:无动画,无阴影,仅颜色变化 +}; + +void Button::paintEvent(QPaintEvent* event) { + auto profile = Application::performanceProfile(); + + if (profile == PerformanceProfile::UltraLow) { + // 跳过所有动画和特效 + drawBackground(); + drawContent(); + return; + } + + if (profile == PerformanceProfile::Embedded) { + // 跳过阴影和涟漪 + drawBackground(); + drawStateLayer(); + drawContent(); + return; + } + + // Desktop 配置:绘制所有效果 + // ... 完整的七步流程 +} +```yaml + +## 总结 + +绘制管道优化的核心思想是"避免不必要的工作":避免不必要的重绘、缓存不变的值、使用更高效的 API。 + +但也要注意"过早优化是万恶之源"。在实际问题出现之前,保持代码的清晰和可维护性更重要。只有当性能测试显示确实存在瓶颈时,才应该考虑这些优化。 + +到这里,我们的"桌面主题架构设计系列"博客就全部完成了。我们从基础数学工具开始,一层一层地构建了完整的 Material Design 3 实现: + +- **Layer 1**:HCT 色彩空间、数学工具、几何和 DPI 适配 +- **Layer 2**:主题系统、Token 设计、颜色方案、字体形状动效 +- **Layer 3**:动画引擎、时间与弹簧动画、工厂与策略 +- **Layer 4**:状态机、涟漪与阴影、焦点指示器 +- **Layer 5**:适配器模式、Button 控件、绘制管道优化 + +希望这个系列能帮助你理解 Material Design 3 桌面主题的完整架构,以及我们为什么做出这样的设计决策。 + +--- + +**相关文档** + +- [Button 控件深度解析](./02-button-deep-dive.md)——控件实现的完整案例 +- [适配器模式](./01-adapter-pattern.md)——控件适配层的设计理念 +- [动画引擎架构](../layer-3-animation-engine/01-animation-architecture.md)——动画系统的性能考量 diff --git a/document/HandBook/ui/architecture/layer-5-widget-adapter/index.md b/document/HandBook/ui/architecture/layer-5-widget-adapter/index.md index f37ae04a3..f25d13313 100644 --- a/document/HandBook/ui/architecture/layer-5-widget-adapter/index.md +++ b/document/HandBook/ui/architecture/layer-5-widget-adapter/index.md @@ -1,10 +1,11 @@ -# layer-5-widget-adapter - -> Welcome to the layer-5-widget-adapter section. +--- +title: "Layer 5: 组件适配层" +description: 本章节介绍 UI 渲染管线的第五层——组件适配层,涵盖 19 个 Material Design 3 +--- -## Overview +# Layer 5: 组件适配层 -Documentation and resources for layer-5-widget-adapter. +本章节介绍 UI 渲染管线的第五层——组件适配层,涵盖 19 个 Material Design 3 标准组件的适配实现,包括 Button、TextField、Slider、Switch、Checkbox、Fab、NavigationBar 等。组件适配层将底层动画与行为整合为可直接使用的 Qt Widget。 --- diff --git a/document/HandBook/ui/base/.pages b/document/HandBook/ui/base/.pages deleted file mode 100644 index 9a69214ea..000000000 --- a/document/HandBook/ui/base/.pages +++ /dev/null @@ -1,8 +0,0 @@ -title: 基础类型 -nav: - - 颜色: color.md - - 颜色工具: color_helper.md - - 设备像素: device_pixel.md - - 缓动函数: easing.md - - 几何工具: geometry_helper.md - - 数学工具: math_helper.md diff --git a/document/HandBook/ui/base/color.md b/document/HandBook/ui/base/color.md index da493243e..334073d69 100644 --- a/document/HandBook/ui/base/color.md +++ b/document/HandBook/ui/base/color.md @@ -1,134 +1,139 @@ -# color - HCT 色彩空间 - -`CFColor` 是对 `QColor` 的扩展,增加了 HCT(Hue-Chroma-Tone)色彩空间支持。HCT 是 Material Design 3 采用的色彩空间,比 RGB 或 HSL 更符合人类对颜色的直觉感知——色相、鲜艳度、明度三个维度正交,调整其中一个不会影响其他两个。这在做动态主题系统时特别重要。 - -## 为什么需要 HCT - -RGB 空间的问题是"不直观"——你想把颜色变亮一点,结果色相也跟着变了;想把蓝色调得更鲜艳,结果明度也变了。HCT 解决了这个问题: - -```cpp -#include "ui/base/color.h" - -using namespace cf::ui::base; - -// RGB 方式:想要一个更亮的蓝色... -QColor rgbBlue(0, 0, 255); -QColor brighterBlue = rgbBlue.lighter(120); // 结果可能偏紫了 - -// HCT 方式:保持色相和色度,只调整色调 -CFColor hctBlue(240.0f, 80.0f, 50.0f); // 蓝色,中高饱和度,中等亮度 -CFColor brighterHCT = CFColor(hctBlue.hue(), hctBlue.chroma(), 70.0f); -// 色相 240° 不变,鲜艳度不变,只是更亮了 -``` - -这就是 Material Design 3 能从单个"种子颜色"生成整套主题的原因——在 HCT 空间里,你可以独立控制颜色的各个维度。 - -## 构造颜色 - -`CFColor` 可以从多种方式构造: - -```cpp -// 从 RGB 值 -CFColor red(255, 0, 0); - -// 从 QColor -QColor qColor("#FF0000"); -CFColor fromQColor(qColor); - -// 从十六进制字符串 -CFColor fromHex("#FF0000"); -CFColor fromHexWithAlpha("#80FF0000"); // 50% 透明的红色 - -// 从 HCT 值 -CFColor fromHCT(240.0f, 80.0f, 50.0f); // 蓝色,饱和度 80,明度 50 -``` - -HCT 值的范围: -- `hue`:0° 到 360°,绕色相环一圈 -- `chroma`:0 到 150,值越大越鲜艳(超过某个值可能无法表示) -- `tone`:0 到 100,0 是纯黑,100 是纯白 - -⚠️ 并非所有 (hue, chroma, tone) 组合都能映射到有效的 RGB。超出 sRGB 色域的组合会被裁剪,所以设置 chroma 时要保守一点,一般别超过 100。 - -## 获取 HCT 分量 - -构造后可以随时获取 HCT 值: - -```cpp -CFColor color("#6750A4"); // Material Purple - -float h = color.hue(); // 262.5°(紫色偏蓝) -float c = color.chroma(); // 约 52(中等鲜艳度) -float t = color.tone(); // 约 42(中等偏暗) - -// 基于这些值生成变体 -CFColor lighterVariant(color.hue(), color.chroma() * 0.8f, color.tone() + 20); -CFColor darkerVariant(color.hue(), color.chroma() * 1.2f, color.tone() - 20); -``` - -HCT 值在构造时计算并缓存,所以访问是 O(1) 的。别担心性能问题。 - -## 相对亮度 - -`relativeLuminance()` 计算 WCAG 标准的相对亮度,用于对比度计算: - -```cpp -CFColor text("#000000"); -CFColor bg("#FFFFFF"); - -float lumText = text.relativeLuminance(); // ~0.0(黑色) -float lumBg = bg.relativeLuminance(); // ~1.0(白色) - -// 对比度 = (Lmax + 0.05) / (Lmin + 0.05) -float ratio = (std::max(lumText, lumBg) + 0.05) / - (std::min(lumText, lumBg) + 0.05); -``` - -这个函数在 `color_helper.h` 的 `contrastRatio()` 里被使用,一般不需要直接调用。但如果你想自己实现一些可访问性相关的逻辑,可以用它。 - -## 与 QColor 互操作 - -`CFColor` 内部维护了一个 `QColor`,可以随时获取: - -```cpp -CFColor myColor("#FF0000"); - -// 获取原生 QColor 用于 Qt API -QColor qColor = myColor.native_color(); - -// 直接用在 QPainter 里 -QPainter painter(this); -painter.setBrush(myColor.native_color()); -painter.drawEllipse(center, 50, 50); -``` - -`CFColor` 不是 `QColor` 的子类,而是组合关系。这样设计是为了避免 `QColor` 的隐式转换带来的意外——`QColor(const char*)` 会把字符串当成颜色名,而我们希望 hex 字符串有明确的解析行为。 - -## 使用建议 - -做主题系统时,推荐的工作流: - -1. 选择一个"种子颜色"(通常是品牌色) -2. 转换到 HCT 空间 -3. 在 HCT 空间生成各种变体(调整 tone 生成深浅色,调整 chroma 生成去饱和变体) -4. 用 `color_helper.h` 的 `tonalPalette()` 自动完成这个步骤 - -```cpp -// 种子颜色 -CFColor seed("#6750A4"); - -// 生成完整色调板 -QList palette = tonalPalette(seed); - -// 使用预定义的色调 -CFColor primary = palette[6]; // 主要色 -CFColor onPrimary = palette[10]; // 主要色上的文字 -CFColor surface = palette[4]; // 表面色 -``` - -这样生成的主题保证色彩和谐,避免了手工调整 RGB 时容易出现的"脏色"问题。 - -## 相关文档 - -- [color_helper - 颜色助手](./color_helper.md) -- [math_helper - 数学工具](./math_helper.md) +--- +title: "color - HCT 色彩空间" +description: "是对 的扩展,增加了 HCT(Hue-Chroma-Tone)色彩空间支持。HCT 是 Mater" +--- + +# color - HCT 色彩空间 + +`CFColor` 是对 `QColor` 的扩展,增加了 HCT(Hue-Chroma-Tone)色彩空间支持。HCT 是 Material Design 3 采用的色彩空间,比 RGB 或 HSL 更符合人类对颜色的直觉感知——色相、鲜艳度、明度三个维度正交,调整其中一个不会影响其他两个。这在做动态主题系统时特别重要。 + +## 为什么需要 HCT + +RGB 空间的问题是"不直观"——你想把颜色变亮一点,结果色相也跟着变了;想把蓝色调得更鲜艳,结果明度也变了。HCT 解决了这个问题: + +```cpp +#include "ui/base/color.h" + +using namespace cf::ui::base; + +// RGB 方式:想要一个更亮的蓝色... +QColor rgbBlue(0, 0, 255); +QColor brighterBlue = rgbBlue.lighter(120); // 结果可能偏紫了 + +// HCT 方式:保持色相和色度,只调整色调 +CFColor hctBlue(240.0f, 80.0f, 50.0f); // 蓝色,中高饱和度,中等亮度 +CFColor brighterHCT = CFColor(hctBlue.hue(), hctBlue.chroma(), 70.0f); +// 色相 240° 不变,鲜艳度不变,只是更亮了 +```text + +这就是 Material Design 3 能从单个"种子颜色"生成整套主题的原因——在 HCT 空间里,你可以独立控制颜色的各个维度。 + +## 构造颜色 + +`CFColor` 可以从多种方式构造: + +```cpp +// 从 RGB 值 +CFColor red(255, 0, 0); + +// 从 QColor +QColor qColor("#FF0000"); +CFColor fromQColor(qColor); + +// 从十六进制字符串 +CFColor fromHex("#FF0000"); +CFColor fromHexWithAlpha("#80FF0000"); // 50% 透明的红色 + +// 从 HCT 值 +CFColor fromHCT(240.0f, 80.0f, 50.0f); // 蓝色,饱和度 80,明度 50 +```text + +HCT 值的范围: +- `hue`:0° 到 360°,绕色相环一圈 +- `chroma`:0 到 150,值越大越鲜艳(超过某个值可能无法表示) +- `tone`:0 到 100,0 是纯黑,100 是纯白 + +⚠️ 并非所有 (hue, chroma, tone) 组合都能映射到有效的 RGB。超出 sRGB 色域的组合会被裁剪,所以设置 chroma 时要保守一点,一般别超过 100。 + +## 获取 HCT 分量 + +构造后可以随时获取 HCT 值: + +```cpp +CFColor color("#6750A4"); // Material Purple + +float h = color.hue(); // 262.5°(紫色偏蓝) +float c = color.chroma(); // 约 52(中等鲜艳度) +float t = color.tone(); // 约 42(中等偏暗) + +// 基于这些值生成变体 +CFColor lighterVariant(color.hue(), color.chroma() * 0.8f, color.tone() + 20); +CFColor darkerVariant(color.hue(), color.chroma() * 1.2f, color.tone() - 20); +```text + +HCT 值在构造时计算并缓存,所以访问是 O(1) 的。别担心性能问题。 + +## 相对亮度 + +`relativeLuminance()` 计算 WCAG 标准的相对亮度,用于对比度计算: + +```cpp +CFColor text("#000000"); +CFColor bg("#FFFFFF"); + +float lumText = text.relativeLuminance(); // ~0.0(黑色) +float lumBg = bg.relativeLuminance(); // ~1.0(白色) + +// 对比度 = (Lmax + 0.05) / (Lmin + 0.05) +float ratio = (std::max(lumText, lumBg) + 0.05) / + (std::min(lumText, lumBg) + 0.05); +```text + +这个函数在 `color_helper.h` 的 `contrastRatio()` 里被使用,一般不需要直接调用。但如果你想自己实现一些可访问性相关的逻辑,可以用它。 + +## 与 QColor 互操作 + +`CFColor` 内部维护了一个 `QColor`,可以随时获取: + +```cpp +CFColor myColor("#FF0000"); + +// 获取原生 QColor 用于 Qt API +QColor qColor = myColor.native_color(); + +// 直接用在 QPainter 里 +QPainter painter(this); +painter.setBrush(myColor.native_color()); +painter.drawEllipse(center, 50, 50); +```text + +`CFColor` 不是 `QColor` 的子类,而是组合关系。这样设计是为了避免 `QColor` 的隐式转换带来的意外——`QColor(const char*)` 会把字符串当成颜色名,而我们希望 hex 字符串有明确的解析行为。 + +## 使用建议 + +做主题系统时,推荐的工作流: + +1. 选择一个"种子颜色"(通常是品牌色) +2. 转换到 HCT 空间 +3. 在 HCT 空间生成各种变体(调整 tone 生成深浅色,调整 chroma 生成去饱和变体) +4. 用 `color_helper.h` 的 `tonalPalette()` 自动完成这个步骤 + +```cpp +// 种子颜色 +CFColor seed("#6750A4"); + +// 生成完整色调板 +QList palette = tonalPalette(seed); + +// 使用预定义的色调 +CFColor primary = palette[6]; // 主要色 +CFColor onPrimary = palette[10]; // 主要色上的文字 +CFColor surface = palette[4]; // 表面色 +```text + +这样生成的主题保证色彩和谐,避免了手工调整 RGB 时容易出现的"脏色"问题。 + +## 相关文档 + +- [color_helper - 颜色助手](./color_helper.md) +- [math_helper - 数学工具](./math_helper.md) diff --git a/document/HandBook/ui/base/color_helper.md b/document/HandBook/ui/base/color_helper.md index 6a5f1601e..78a27e4e3 100644 --- a/document/HandBook/ui/base/color_helper.md +++ b/document/HandBook/ui/base/color_helper.md @@ -1,97 +1,102 @@ -# color_helper - 颜色助手 - -`color_helper` 提供了 Material Design 3 颜色系统常用的颜色操作函数,包括混合、高程叠加、对比度计算和色调板生成。这些本来应该是色彩理论的实现细节,但在做动态主题系统时反复出现,所以抽出来作为独立工具。 - -## 颜色混合 - -`blend()` 在两个颜色之间按指定比例混合: - -```cpp -#include "ui/base/color_helper.h" - -using namespace cf::ui::base; - -CFColor base("#1A1A1A"); -CFColor overlay("#FFFFFF"); - -// 50% 混合 -CFColor result = blend(base, overlay, 0.5f); - -// 半透明遮罩效果 -CFColor dimmed = blend(originalColor, CFColor(0, 0, 0), 0.3f); -``` - -混合是在 RGB 空间线性进行的,不是 alpha 混合。ratio = 0 返回 base,ratio = 1 返回 overlay,中间值按比例线性组合。 - -## 高程叠加 - -Material Design 用"高程"(elevation)来表示层级关系,`elevationOverlay()` 实现了这个效果: - -```cpp -CFColor surface("#FFFFFF"); -CFColor primary("#6750A4"); - -// 无高程 -CFColor flat = elevationOverlay(surface, primary, 0); - -// 高程 4dp 的卡片 -CFColor card = elevationOverlay(surface, primary, 4); - -// 高程 8dp 的对话框 -CFColor dialog = elevationOverlay(surface, primary, 8); -``` - -高程越大,surface 颜色会越向 primary 靠近,模拟阴影和光照效果。这是 Material Design 3 的标准做法,比单纯用 alpha 叠加黑色要精致一些。 - -⚠️ elevation 参数的单位是 dp,不是像素。需要根据屏幕密度转换,或者在高 DPI 环境下调整参数。 - -## 对比度计算 - -`contrastRatio()` 计算 WCAG 标准的对比度,用于判断文字和背景的可读性: - -```cpp -CFColor text("#000000"); -CFColor background("#FFFFFF"); - -float ratio = contrastRatio(text, background); // 21.0 (最大对比度) - -// WCAG AA 标准要求正文对比度 >= 4.5 -if (contrastRatio(foreground, background) < 4.5f) { - // 需要调整颜色 -} - -// 自动选择深色/浅色文字 -CFColor textColor = contrastRatio(darkText, bgColor) > contrastRatio(lightText, bgColor) - ? darkText : lightText; -``` - -对比度是基于相对亮度计算的,返回值范围是 1.0 到 21.0。1.0 表示两个颜色亮度相同,21.0 是黑白之间的最大对比度。 - -⚠️ 这个计算相对昂贵,因为涉及 RGB 到线性空间的转换。在热路径(比如渲染循环)里最好缓存结果,别每帧都算。 - -## 色调板生成 - -`tonalPalette()` 从一个关键色生成完整的色调板,返回 13 个色调从浅到深的颜色: - -```cpp -CFColor keyColor("#6750A4"); // Material Purple - -QList palette = tonalPalette(keyColor); - -// palette[0] 是最浅色调(tone ~95) -// palette[12] 是最深色调(tone ~10) - -// 用法示例 -CFColor surface = palette[4]; // 浅色表面 -CFColor surfaceVariant = palette[6]; // 变体表面 -CFColor onSurface = palette[10]; // 表面上的文字 -``` - -色调板是 Material Design 3 主题系统的核心——整个颜色系统都是从单个"种子颜色"衍生出来的。生成的色调遵循 Material 规范,确保视觉和谐度。 - -这个函数内部用的是 HCT 色彩空间,所以生成的颜色保持了原始色相,只是调整色度和色调。这比直接在 RGB 空间操作要自然得多。 - -## 相关文档 - -- [color - HCT 色彩空间](./color.md) -- [math_helper - 数学工具](./math_helper.md) +--- +title: "colorhelper - 颜色助手" +description: 提供了 Material Design 3 颜色系统常用的颜色操作函数,包括混合、高程叠加、对比度计 +--- + +# color_helper - 颜色助手 + +`color_helper` 提供了 Material Design 3 颜色系统常用的颜色操作函数,包括混合、高程叠加、对比度计算和色调板生成。这些本来应该是色彩理论的实现细节,但在做动态主题系统时反复出现,所以抽出来作为独立工具。 + +## 颜色混合 + +`blend()` 在两个颜色之间按指定比例混合: + +```cpp +#include "ui/base/color_helper.h" + +using namespace cf::ui::base; + +CFColor base("#1A1A1A"); +CFColor overlay("#FFFFFF"); + +// 50% 混合 +CFColor result = blend(base, overlay, 0.5f); + +// 半透明遮罩效果 +CFColor dimmed = blend(originalColor, CFColor(0, 0, 0), 0.3f); +```text + +混合是在 RGB 空间线性进行的,不是 alpha 混合。ratio = 0 返回 base,ratio = 1 返回 overlay,中间值按比例线性组合。 + +## 高程叠加 + +Material Design 用"高程"(elevation)来表示层级关系,`elevationOverlay()` 实现了这个效果: + +```cpp +CFColor surface("#FFFFFF"); +CFColor primary("#6750A4"); + +// 无高程 +CFColor flat = elevationOverlay(surface, primary, 0); + +// 高程 4dp 的卡片 +CFColor card = elevationOverlay(surface, primary, 4); + +// 高程 8dp 的对话框 +CFColor dialog = elevationOverlay(surface, primary, 8); +```text + +高程越大,surface 颜色会越向 primary 靠近,模拟阴影和光照效果。这是 Material Design 3 的标准做法,比单纯用 alpha 叠加黑色要精致一些。 + +⚠️ elevation 参数的单位是 dp,不是像素。需要根据屏幕密度转换,或者在高 DPI 环境下调整参数。 + +## 对比度计算 + +`contrastRatio()` 计算 WCAG 标准的对比度,用于判断文字和背景的可读性: + +```cpp +CFColor text("#000000"); +CFColor background("#FFFFFF"); + +float ratio = contrastRatio(text, background); // 21.0 (最大对比度) + +// WCAG AA 标准要求正文对比度 >= 4.5 +if (contrastRatio(foreground, background) < 4.5f) { + // 需要调整颜色 +} + +// 自动选择深色/浅色文字 +CFColor textColor = contrastRatio(darkText, bgColor) > contrastRatio(lightText, bgColor) + ? darkText : lightText; +```text + +对比度是基于相对亮度计算的,返回值范围是 1.0 到 21.0。1.0 表示两个颜色亮度相同,21.0 是黑白之间的最大对比度。 + +⚠️ 这个计算相对昂贵,因为涉及 RGB 到线性空间的转换。在热路径(比如渲染循环)里最好缓存结果,别每帧都算。 + +## 色调板生成 + +`tonalPalette()` 从一个关键色生成完整的色调板,返回 13 个色调从浅到深的颜色: + +```cpp +CFColor keyColor("#6750A4"); // Material Purple + +QList palette = tonalPalette(keyColor); + +// palette[0] 是最浅色调(tone ~95) +// palette[12] 是最深色调(tone ~10) + +// 用法示例 +CFColor surface = palette[4]; // 浅色表面 +CFColor surfaceVariant = palette[6]; // 变体表面 +CFColor onSurface = palette[10]; // 表面上的文字 +```text + +色调板是 Material Design 3 主题系统的核心——整个颜色系统都是从单个"种子颜色"衍生出来的。生成的色调遵循 Material 规范,确保视觉和谐度。 + +这个函数内部用的是 HCT 色彩空间,所以生成的颜色保持了原始色相,只是调整色度和色调。这比直接在 RGB 空间操作要自然得多。 + +## 相关文档 + +- [color - HCT 色彩空间](./color.md) +- [math_helper - 数学工具](./math_helper.md) diff --git a/document/HandBook/ui/base/device_pixel.md b/document/HandBook/ui/base/device_pixel.md index 02e358718..44e7e986b 100644 --- a/document/HandBook/ui/base/device_pixel.md +++ b/document/HandBook/ui/base/device_pixel.md @@ -1,90 +1,95 @@ -# device_pixel - 设备像素 (DPI 缩放) - -`device_pixel` 提供了设备无关像素(dp)、可缩放像素(sp)和物理像素之间的转换工具。在高 DPI 显示器普及的今天,这个组件是保证 UI 在不同屏幕上显示一致性的基础。我们自己实现而不是完全依赖 Qt 的缩放,是因为需要更精细的控制单位转换逻辑,特别是在响应式布局的断点判断上。 - -## 基本用法 - -`CanvasUnitHelper` 是核心工具类,构造时传入设备像素比(DPI),之后就可以进行各种单位转换: - -```cpp -#include "ui/base/device_pixel.h" - -using namespace cf::ui::base::device; - -// 创建一个 2x DPI 的 helper(常见于 Retina 屏幕) -CanvasUnitHelper helper(2.0); - -// 将设备无关像素转换为物理像素 -qreal buttonWidth = helper.dpToPx(88.0); // 88dp -> 176px - -// 将可缩放像素转换为物理像素(考虑字体缩放) -qreal fontSize = helper.spToPx(14.0); // 14sp -> 28px - -// 反向转换:物理像素转回设备无关像素 -qreal dpValue = helper.pxToDp(352.0); // 352px -> 176dp -``` - -设备像素比通常从 Qt 的 `QWindow` 或 `QScreen` 获取: - -```cpp -// 在 Qt 窗口中获取当前 DPI -qreal dpi = window()->devicePixelRatio(); -CanvasUnitHelper helper(dpi); -``` - -## 单位类型 - -`dp`(device-independent pixel)是设备无关像素,用于布局尺寸。在 160 DPI 的屏幕上,1dp 等于 1 物理像素。在 2x 屏幕上,1dp 等于 2 物理像素。 - -`sp`(scalable pixel)是可缩放像素,专门用于字体大小。它和 dp 类似,但会额外受到用户字体大小设置的影响。当前实现中 `spToPx()` 和 `dpToPx()` 行为一致,如果需要支持字体缩放,可以在未来扩展。 - -⚠️ 不要用物理像素硬编码尺寸。你写的 10px 宽的按钮在 4K 显示器上会细得看不见,用 10dp 就能保持一致。 - -## 响应式断点 - -`BreakPoint` 枚举定义了响应式布局的宽度断点,参考 Material Design 的分类标准: - -```cpp -// 判断当前窗口宽度属于哪个断点 -auto bp = helper.breakPoint(widthDp); - -if (bp == CanvasUnitHelper::BreakPoint::Compact) { - // 手机/窄屏布局:< 600dp - // 使用单列布局,隐藏次要元素 -} else if (bp == CanvasUnitHelper::BreakPoint::Medium) { - // 平板/折叠屏布局:600dp - 839dp - // 使用两列布局,显示侧边栏 -} else { - // 桌面布局:>= 840dp - // 使用多列布局,显示完整导航 -} -``` - -断点判断使用的是 dp 值,而不是物理像素。这样不管屏幕 DPI 如何,布局行为保持一致。 - -## DPI 缩放的注意事项 - -处理 DPI 缩放时,有几个容易踩的坑: - -第一,混用不同单位。比如 dp 值和 px 值直接相加,在高 DPI 下会得到错误结果。所有计算要么统一在 dp 空间,要么统一在 px 空间。 - -第二,缓存 DPI 值。如果用户在运行时移动窗口到不同 DPI 的显示器,或者修改系统缩放设置,缓存的 DPI 值会过时。需要在 DPI 变化时重新创建 `CanvasUnitHelper`。 - -第三,整型截断。`dpToPx()` 返回 `qreal`,但很多绘制 API 需要 `int`。截断前要四舍五入,否则会产生累积误差: - -```cpp -// 正确的做法 -int rounded = qRound(helper.dpToPx(16.0)); - -// 错误的做法:直接截断 -int truncated = static_cast(helper.dpToPx(16.0)); -``` - -## 性能考虑 - -`CanvasUnitHelper` 是一个轻量级的值类型,构造和拷贝都很便宜。如果在一个局部作用域内频繁转换,可以直接按值传递,不需要用指针或引用。但如果在全局或成员变量里存储,确保在 DPI 变化时更新。 - -## 相关文档 - -- [math_helper - 数学工具](./math_helper.md) -- [easing - 缓动曲线](./easing.md) +--- +title: "devicepixel - 设备像素 (DPI 缩放)" +description: 提供了设备无关像素(dp)、可缩放像素(sp)和物理像素之间的转换工具。在高 DPI 显示器普及的今 +--- + +# device_pixel - 设备像素 (DPI 缩放) + +`device_pixel` 提供了设备无关像素(dp)、可缩放像素(sp)和物理像素之间的转换工具。在高 DPI 显示器普及的今天,这个组件是保证 UI 在不同屏幕上显示一致性的基础。我们自己实现而不是完全依赖 Qt 的缩放,是因为需要更精细的控制单位转换逻辑,特别是在响应式布局的断点判断上。 + +## 基本用法 + +`CanvasUnitHelper` 是核心工具类,构造时传入设备像素比(DPI),之后就可以进行各种单位转换: + +```cpp +#include "ui/base/device_pixel.h" + +using namespace cf::ui::base::device; + +// 创建一个 2x DPI 的 helper(常见于 Retina 屏幕) +CanvasUnitHelper helper(2.0); + +// 将设备无关像素转换为物理像素 +qreal buttonWidth = helper.dpToPx(88.0); // 88dp -> 176px + +// 将可缩放像素转换为物理像素(考虑字体缩放) +qreal fontSize = helper.spToPx(14.0); // 14sp -> 28px + +// 反向转换:物理像素转回设备无关像素 +qreal dpValue = helper.pxToDp(352.0); // 352px -> 176dp +```text + +设备像素比通常从 Qt 的 `QWindow` 或 `QScreen` 获取: + +```cpp +// 在 Qt 窗口中获取当前 DPI +qreal dpi = window()->devicePixelRatio(); +CanvasUnitHelper helper(dpi); +```text + +## 单位类型 + +`dp`(device-independent pixel)是设备无关像素,用于布局尺寸。在 160 DPI 的屏幕上,1dp 等于 1 物理像素。在 2x 屏幕上,1dp 等于 2 物理像素。 + +`sp`(scalable pixel)是可缩放像素,专门用于字体大小。它和 dp 类似,但会额外受到用户字体大小设置的影响。当前实现中 `spToPx()` 和 `dpToPx()` 行为一致,如果需要支持字体缩放,可以在未来扩展。 + +⚠️ 不要用物理像素硬编码尺寸。你写的 10px 宽的按钮在 4K 显示器上会细得看不见,用 10dp 就能保持一致。 + +## 响应式断点 + +`BreakPoint` 枚举定义了响应式布局的宽度断点,参考 Material Design 的分类标准: + +```cpp +// 判断当前窗口宽度属于哪个断点 +auto bp = helper.breakPoint(widthDp); + +if (bp == CanvasUnitHelper::BreakPoint::Compact) { + // 手机/窄屏布局:< 600dp + // 使用单列布局,隐藏次要元素 +} else if (bp == CanvasUnitHelper::BreakPoint::Medium) { + // 平板/折叠屏布局:600dp - 839dp + // 使用两列布局,显示侧边栏 +} else { + // 桌面布局:>= 840dp + // 使用多列布局,显示完整导航 +} +```text + +断点判断使用的是 dp 值,而不是物理像素。这样不管屏幕 DPI 如何,布局行为保持一致。 + +## DPI 缩放的注意事项 + +处理 DPI 缩放时,有几个容易踩的坑: + +第一,混用不同单位。比如 dp 值和 px 值直接相加,在高 DPI 下会得到错误结果。所有计算要么统一在 dp 空间,要么统一在 px 空间。 + +第二,缓存 DPI 值。如果用户在运行时移动窗口到不同 DPI 的显示器,或者修改系统缩放设置,缓存的 DPI 值会过时。需要在 DPI 变化时重新创建 `CanvasUnitHelper`。 + +第三,整型截断。`dpToPx()` 返回 `qreal`,但很多绘制 API 需要 `int`。截断前要四舍五入,否则会产生累积误差: + +```cpp +// 正确的做法 +int rounded = qRound(helper.dpToPx(16.0)); + +// 错误的做法:直接截断 +int truncated = static_cast(helper.dpToPx(16.0)); +```text + +## 性能考虑 + +`CanvasUnitHelper` 是一个轻量级的值类型,构造和拷贝都很便宜。如果在一个局部作用域内频繁转换,可以直接按值传递,不需要用指针或引用。但如果在全局或成员变量里存储,确保在 DPI 变化时更新。 + +## 相关文档 + +- [math_helper - 数学工具](./math_helper.md) +- [easing - 缓动曲线](./easing.md) diff --git a/document/HandBook/ui/base/easing.md b/document/HandBook/ui/base/easing.md index e3ea51127..7e2feeaea 100644 --- a/document/HandBook/ui/base/easing.md +++ b/document/HandBook/ui/base/easing.md @@ -1,105 +1,110 @@ -# easing - 缓动曲线 - -`easing` 提供了 Material Design 规范的预设缓动曲线和弹簧物理参数。缓动曲线决定了动画的速度变化——是匀速、先慢后快、还是先快后慢——直接影响动画的"质感"。Material Design 对这个有详细规范,我们直接实现了它的标准曲线。 - -## 预设缓动类型 - -`Type` 枚举定义了 Material Design 的标准缓动类型: - -```cpp -#include "ui/base/easing.h" - -using namespace cf::ui::base::Easing; - -// 转换为 Qt 的 QEasingCurve -QEasingCurve curve = fromEasingType(Type::Standard); - -// 应用到 QVariantAnimation -QPropertyAnimation* anim = new QPropertyAnimation(this, "geometry"); -anim->setEasingCurve(fromEasingType(Type::Emphasized)); -anim->setDuration(300); -``` - -各种类型的区别: -- `Linear`:匀速,没有加速感 -- `Standard`:标准缓动,有轻微的加速和减速 -- `Emphasized`:强调型缓动,加减速更明显,适合重要的动画 -- `EmphasizedDecelerate` / `EmphasizedAccelerate`:单向的强调缓动 -- `StandardDecelerate` / `StandardAccelerate`:单向的标准缓动 - -选择的原则:**动画越重要,缓动越明显**。比如打开对话框用 `Emphasized`,淡出一个提示用 `Standard` 就够了。 - -## 自定义贝塞尔曲线 - -如果预设曲线不满足需求,`custom()` 函数可以创建任意三次贝塞尔曲线: - -```cpp -// 自定义曲线:控制点 (0.4, 0.0, 0.2, 1.0) -QEasingCurve myCurve = custom(0.4f, 0.0f, 0.2f, 1.0f); - -// 过冲效果:y 值超出 [0, 1] -QEasingCurve overshoot = custom(0.34f, 1.56f, 0.64f, 1.0f); - -// 弹性效果 -QEasingCurve bounce = custom(0.68f, -0.6f, 0.32f, 1.6f); -``` - -控制点参数的含义: -- `x1, y1` 是第一个控制点的坐标 -- `x2, y2` 是第二个控制点的坐标 -- 曲线起点固定在 (0, 0),终点固定在 (1, 1) -- x 坐标建议在 [0, 1] 范围内,y 坐标可以超出 - -⚠️ 如果 x1 或 x2 超出 [0, 1] 范围,曲线会出现"回退"——动画可能会先倒退再前进,或者先快进再退回。这是有意为之的效果,但要小心使用。 - -## 弹簧物理预设 - -除了贝塞尔曲线,我们还提供了弹簧物理参数。弹簧动画比贝塞尔曲线更自然,但参数调优更困难: - -```cpp -// 温和弹簧:几乎无震荡 -SpringPreset gentle = springGentle(); -// gentle.stiffness ≈ 200, gentle.damping ≈ 25 - -// 弹性弹簧:有明显回弹 -SpringPreset bouncy = springBouncy(); -// bouncy.stiffness ≈ 400, bouncy.damping ≈ 10 - -// 硬弹簧:快速到位 -SpringPreset stiff = springStiff(); -// stiff.stiffness ≈ 600, stiff.damping ≈ 30 - -// 配合 math_helper 的 springStep() 使用 -auto [pos, vel] = math::springStep(currentPos, velocity, targetPos, - bouncy.stiffness, bouncy.damping, dt); -``` - -弹簧的调优确实有点玄学——stiffness 太小会拖沓,太大容易震荡;damping 太小会一直抖,太大又没有弹性。我们提供的预设是在实际项目中调出来的经验值,基本够用了。 - -⚠️ 弹簧动画不适合用在有明确时间要求的场景(比如多个动画需要同步)。贝塞尔曲线的时间是确定的,弹簧的"到位时间"取决于目标距离。 - -## 实际使用建议 - -选哪种缓动,看场景: - -```cpp -// 入场动画:用 Emphasized,给用户明确的视觉反馈 -anim->setEasingCurve(fromEasingType(Type::Emphasized)); - -// 状态切换:用 Standard,不要过度吸引注意力 -anim->setEasingCurve(fromEasingType(Type::Standard)); - -// 背景淡入淡出:用 Linear,不抢戏 -anim->setEasingCurve(fromEasingType(Type::Linear)); - -// 按钮点击反馈:用弹簧,增加触感 -SpringPreset preset = springBouncy(); -// 在 update 循环中调用 springStep() -``` - -总的原则:**用户主动触发的操作用强缓动/弹簧,系统自动的状态变化用弱缓动**。这样既能传达操作的"手感",又不会让界面显得太花哨。 - -## 相关文档 - -- [math_helper - 数学工具](./math_helper.md) -- [geometry_helper - 几何助手](./geometry_helper.md) +--- +title: "easing - 缓动曲线" +description: 提供了 Material Design 规范的预设缓动曲线和弹簧物理参数。缓动曲线决定了动画的速度变 +--- + +# easing - 缓动曲线 + +`easing` 提供了 Material Design 规范的预设缓动曲线和弹簧物理参数。缓动曲线决定了动画的速度变化——是匀速、先慢后快、还是先快后慢——直接影响动画的"质感"。Material Design 对这个有详细规范,我们直接实现了它的标准曲线。 + +## 预设缓动类型 + +`Type` 枚举定义了 Material Design 的标准缓动类型: + +```cpp +#include "ui/base/easing.h" + +using namespace cf::ui::base::Easing; + +// 转换为 Qt 的 QEasingCurve +QEasingCurve curve = fromEasingType(Type::Standard); + +// 应用到 QVariantAnimation +QPropertyAnimation* anim = new QPropertyAnimation(this, "geometry"); +anim->setEasingCurve(fromEasingType(Type::Emphasized)); +anim->setDuration(300); +```text + +各种类型的区别: +- `Linear`:匀速,没有加速感 +- `Standard`:标准缓动,有轻微的加速和减速 +- `Emphasized`:强调型缓动,加减速更明显,适合重要的动画 +- `EmphasizedDecelerate` / `EmphasizedAccelerate`:单向的强调缓动 +- `StandardDecelerate` / `StandardAccelerate`:单向的标准缓动 + +选择的原则:**动画越重要,缓动越明显**。比如打开对话框用 `Emphasized`,淡出一个提示用 `Standard` 就够了。 + +## 自定义贝塞尔曲线 + +如果预设曲线不满足需求,`custom()` 函数可以创建任意三次贝塞尔曲线: + +```cpp +// 自定义曲线:控制点 (0.4, 0.0, 0.2, 1.0) +QEasingCurve myCurve = custom(0.4f, 0.0f, 0.2f, 1.0f); + +// 过冲效果:y 值超出 [0, 1] +QEasingCurve overshoot = custom(0.34f, 1.56f, 0.64f, 1.0f); + +// 弹性效果 +QEasingCurve bounce = custom(0.68f, -0.6f, 0.32f, 1.6f); +```text + +控制点参数的含义: +- `x1, y1` 是第一个控制点的坐标 +- `x2, y2` 是第二个控制点的坐标 +- 曲线起点固定在 (0, 0),终点固定在 (1, 1) +- x 坐标建议在 [0, 1] 范围内,y 坐标可以超出 + +⚠️ 如果 x1 或 x2 超出 [0, 1] 范围,曲线会出现"回退"——动画可能会先倒退再前进,或者先快进再退回。这是有意为之的效果,但要小心使用。 + +## 弹簧物理预设 + +除了贝塞尔曲线,我们还提供了弹簧物理参数。弹簧动画比贝塞尔曲线更自然,但参数调优更困难: + +```cpp +// 温和弹簧:几乎无震荡 +SpringPreset gentle = springGentle(); +// gentle.stiffness ≈ 200, gentle.damping ≈ 25 + +// 弹性弹簧:有明显回弹 +SpringPreset bouncy = springBouncy(); +// bouncy.stiffness ≈ 400, bouncy.damping ≈ 10 + +// 硬弹簧:快速到位 +SpringPreset stiff = springStiff(); +// stiff.stiffness ≈ 600, stiff.damping ≈ 30 + +// 配合 math_helper 的 springStep() 使用 +auto [pos, vel] = math::springStep(currentPos, velocity, targetPos, + bouncy.stiffness, bouncy.damping, dt); +```text + +弹簧的调优确实有点玄学——stiffness 太小会拖沓,太大容易震荡;damping 太小会一直抖,太大又没有弹性。我们提供的预设是在实际项目中调出来的经验值,基本够用了。 + +⚠️ 弹簧动画不适合用在有明确时间要求的场景(比如多个动画需要同步)。贝塞尔曲线的时间是确定的,弹簧的"到位时间"取决于目标距离。 + +## 实际使用建议 + +选哪种缓动,看场景: + +```cpp +// 入场动画:用 Emphasized,给用户明确的视觉反馈 +anim->setEasingCurve(fromEasingType(Type::Emphasized)); + +// 状态切换:用 Standard,不要过度吸引注意力 +anim->setEasingCurve(fromEasingType(Type::Standard)); + +// 背景淡入淡出:用 Linear,不抢戏 +anim->setEasingCurve(fromEasingType(Type::Linear)); + +// 按钮点击反馈:用弹簧,增加触感 +SpringPreset preset = springBouncy(); +// 在 update 循环中调用 springStep() +```text + +总的原则:**用户主动触发的操作用强缓动/弹簧,系统自动的状态变化用弱缓动**。这样既能传达操作的"手感",又不会让界面显得太花哨。 + +## 相关文档 + +- [math_helper - 数学工具](./math_helper.md) +- [geometry_helper - 几何助手](./geometry_helper.md) diff --git a/document/HandBook/ui/base/geometry_helper.md b/document/HandBook/ui/base/geometry_helper.md index ad7461e83..f13cf1268 100644 --- a/document/HandBook/ui/base/geometry_helper.md +++ b/document/HandBook/ui/base/geometry_helper.md @@ -1,95 +1,100 @@ -# geometry_helper - 几何助手 - -`geometry_helper` 提供了创建圆角矩形的工具函数,封装了 Material Design 的形状规范。Qt 原生的 `QPainterPath::addRoundedRect()` 只能统一设置圆角半径,而我们需要单独控制每个角,所以自己封装了一层。 - -## Material Design 形状规范 - -Material Design 定义了标准的圆角尺寸,我们用 `ShapeScale` 枚举表示: - -```cpp -#include "ui/base/geometry_helper.h" - -using namespace cf::ui::base::geometry; - -// 无圆角 -QPainterPath path1 = roundedRect(rect, ShapeScale::ShapeNone); - -// 小圆角 (8dp) - 适合按钮、卡片 -QPainterPath path2 = roundedRect(rect, ShapeScale::ShapeSmall); - -// 大圆角 (16dp) - 适合底部表单、对话框 -QPainterPath path3 = roundedRect(rect, ShapeScale::ShapeLarge); - -// 完全圆角 (50%) - 变成椭圆/胶囊形 -QPainterPath path4 = roundedRect(rect, ShapeScale::ShapeFull); -``` - -这些预设值来自 Material Design 3 的形状规范,确保整个应用的视觉语言一致。 - -## 自定义圆角半径 - -如果预设值不满足需求,可以直接指定像素半径: - -```cpp -QRectF buttonRect(0, 0, 120, 40); - -// 统一圆角 8px -QPainterPath buttonPath = roundedRect(buttonRect, 8.0f); - -// 大圆角,营造现代感 -QPainterPath cardPath = roundedRect(cardRect, 16.0f); -``` - -⚠️ 半径单位是像素,不是 dp。在高 DPI 屏幕上需要乘以设备像素比,否则看起来会太小。 - -## 独立控制四个角 - -某些场景需要单独控制每个角的圆角,比如顶部只有两个圆角的底部表单: - -```cpp -// 底部表单:顶部圆角,底部直角 -QPainterPath sheetPath = roundedRect(sheetRect, - 16.0f, // topLeft - 16.0f, // topRight - 0.0f, // bottomLeft - 0.0f); // bottomRight - -// 气泡对话框风格 -QPainterPath bubblePath = roundedRect(bubbleRect, - 4.0f, // topLeft - 16.0f, // topRight - 16.0f, // bottomLeft - 4.0f); // bottomRight -``` - -这种用法在做聊天气泡、底部抽屉之类的组件时特别常见。Qt 原生 API 需要手动构建 path,我们的封装简化了这个过程。 - -⚠️ 如果圆角半径超过矩形尺寸的一半,Qt 会自动裁剪。想做胶囊形状应该用 `ShapeFull`,而不是手动设置一个超大半径。 - -## 使用场景 - -圆角路径准备好之后,可以直接用于绘制: - -```cpp -void paintEvent(QPaintEvent* event) override { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - // 创建圆角路径 - QPainterPath path = roundedRect(rect(), ShapeScale::ShapeMedium); - - // 填充 - painter.fillPath(path, QColor("#FFFFFF")); - - // 描边 - painter.setPen(QPen(QColor("#E0E0E0"), 1.0)); - painter.drawPath(path); -} -``` - -记得启用 `Antialiasing`,否则圆角会有明显的锯齿。 - -## 相关文档 - -- [easing - 缓动曲线](./easing.md) -- [color - HCT 色彩空间](./color.md) +--- +title: "geometryhelper - 几何助手" +description: 提供了创建圆角矩形的工具函数,封装了 Material Design 的形状规范。Qt 原生的 只 +--- + +# geometry_helper - 几何助手 + +`geometry_helper` 提供了创建圆角矩形的工具函数,封装了 Material Design 的形状规范。Qt 原生的 `QPainterPath::addRoundedRect()` 只能统一设置圆角半径,而我们需要单独控制每个角,所以自己封装了一层。 + +## Material Design 形状规范 + +Material Design 定义了标准的圆角尺寸,我们用 `ShapeScale` 枚举表示: + +```cpp +#include "ui/base/geometry_helper.h" + +using namespace cf::ui::base::geometry; + +// 无圆角 +QPainterPath path1 = roundedRect(rect, ShapeScale::ShapeNone); + +// 小圆角 (8dp) - 适合按钮、卡片 +QPainterPath path2 = roundedRect(rect, ShapeScale::ShapeSmall); + +// 大圆角 (16dp) - 适合底部表单、对话框 +QPainterPath path3 = roundedRect(rect, ShapeScale::ShapeLarge); + +// 完全圆角 (50%) - 变成椭圆/胶囊形 +QPainterPath path4 = roundedRect(rect, ShapeScale::ShapeFull); +```text + +这些预设值来自 Material Design 3 的形状规范,确保整个应用的视觉语言一致。 + +## 自定义圆角半径 + +如果预设值不满足需求,可以直接指定像素半径: + +```cpp +QRectF buttonRect(0, 0, 120, 40); + +// 统一圆角 8px +QPainterPath buttonPath = roundedRect(buttonRect, 8.0f); + +// 大圆角,营造现代感 +QPainterPath cardPath = roundedRect(cardRect, 16.0f); +```text + +⚠️ 半径单位是像素,不是 dp。在高 DPI 屏幕上需要乘以设备像素比,否则看起来会太小。 + +## 独立控制四个角 + +某些场景需要单独控制每个角的圆角,比如顶部只有两个圆角的底部表单: + +```cpp +// 底部表单:顶部圆角,底部直角 +QPainterPath sheetPath = roundedRect(sheetRect, + 16.0f, // topLeft + 16.0f, // topRight + 0.0f, // bottomLeft + 0.0f); // bottomRight + +// 气泡对话框风格 +QPainterPath bubblePath = roundedRect(bubbleRect, + 4.0f, // topLeft + 16.0f, // topRight + 16.0f, // bottomLeft + 4.0f); // bottomRight +```text + +这种用法在做聊天气泡、底部抽屉之类的组件时特别常见。Qt 原生 API 需要手动构建 path,我们的封装简化了这个过程。 + +⚠️ 如果圆角半径超过矩形尺寸的一半,Qt 会自动裁剪。想做胶囊形状应该用 `ShapeFull`,而不是手动设置一个超大半径。 + +## 使用场景 + +圆角路径准备好之后,可以直接用于绘制: + +```cpp +void paintEvent(QPaintEvent* event) override { + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + // 创建圆角路径 + QPainterPath path = roundedRect(rect(), ShapeScale::ShapeMedium); + + // 填充 + painter.fillPath(path, QColor("#FFFFFF")); + + // 描边 + painter.setPen(QPen(QColor("#E0E0E0"), 1.0)); + painter.drawPath(path); +} +```text + +记得启用 `Antialiasing`,否则圆角会有明显的锯齿。 + +## 相关文档 + +- [easing - 缓动曲线](./easing.md) +- [color - HCT 色彩空间](./color.md) diff --git a/document/HandBook/ui/base/index.md b/document/HandBook/ui/base/index.md index 166eabd94..69f9cdd56 100644 --- a/document/HandBook/ui/base/index.md +++ b/document/HandBook/ui/base/index.md @@ -1,10 +1,11 @@ -# base - -> Welcome to the base section. +--- +title: UI 基础工具 +description: 本章节涵盖 UI 框架的基础工具层,包括 颜色管理、 几何计算辅助以及 缓动函数等核心工具类。这 +--- -## Overview +# UI 基础工具 -Documentation and resources for base. +本章节涵盖 UI 框架的基础工具层,包括 `CFColor` 颜色管理、`GeometryHelper` 几何计算辅助以及 `Easing` 缓动函数等核心工具类。这些工具为上层主题引擎和组件系统提供数学与渲染基础支持。 --- diff --git a/document/HandBook/ui/base/math_helper.md b/document/HandBook/ui/base/math_helper.md index a12060936..5e71348cf 100644 --- a/document/HandBook/ui/base/math_helper.md +++ b/document/HandBook/ui/base/math_helper.md @@ -1,99 +1,104 @@ -# math_helper - 数学工具 - -`math_helper` 提供了 UI 动画和过渡效果常用的数学工具函数,包括插值、映射、三次贝塞尔曲线和弹簧物理模拟。这些函数散落在很多地方都有实现,但我们抽出来统一管理,一是避免重复代码,二是方便后续统一调整算法参数。 - -## 线性插值 - -`lerp()` 是最基础的插值函数,在两个值之间按比例 t 进行线性过渡: - -```cpp -#include "ui/base/math_helper.h" - -using namespace cf::ui::math; - -// 基础用法:从 0 到 100,取中间值 -float value = lerp(0.0f, 100.0f, 0.5f); // 返回 50.0 - -// 动画中的淡入效果 -float opacity = lerp(0.0f, 1.0f, progress); // progress 从 0 到 1 - -// 颜色通道插值 -int red = static_cast(lerp(startColor.red(), endColor.red(), t)); -``` - -插值参数 t 不限制在 [0, 1] 范围内——超出时会产生"外推"效果,这在某些动画场景下是有用的,但要小心使用。 - -## 数值限制和映射 - -`clamp()` 用于将数值约束在指定范围内,`remap()` 则将一个区间的值映射到另一个区间: - -```cpp -// 限制范围 -float x = clamp(value, 0.0f, 1.0f); // 确保 x 在 [0, 1] 内 - -// 区间映射:将 [0, 100] 映射到 [-1, 1] -float normalized = remap(input, 0.0f, 100.0f, -1.0f, 1.0f); - -// 进度条的实际像素位置 -float pixel = remap(progress, 0.0f, 1.0f, 0.0f, trackWidth); -``` - -`remap()` 在处理滑块、进度条这类 UI 控件时特别方便——不用自己算比例和偏移了。 - -## 三次贝塞尔曲线 - -自定义缓动曲线时,`cubicBezier()` 是核心函数。给定两个控制点,计算曲线在 t 位置的 y 值: - -```cpp -// Material Design 的 emphasized 曲线 -// 控制点 (0.2, 0.0) 和 (0, 1.0) -float eased = cubicBezier(0.2f, 0.0f, 0.0f, 1.0f, t); - -// 自定义曲线 -float custom = cubicBezier(0.25f, 0.1f, 0.25f, 1.0f, progress); -``` - -这个函数的参数控制点 x 坐标建议限制在 [0, 1] 范围内,否则曲线会出现"回退"效果(一个 t 对应多个 x)。y 坐标可以超出这个范围,产生弹性过冲效果。 - -⚠️ 这是一个简化的实现,假设 x(t) ≈ t。如果需要精确的时间映射(比如控制点 x 超出 [0,1]),需要求解贝塞尔方程,那个计算量会大很多。对于 UI 动画,当前这个近似足够用了。 - -## 弹簧物理模拟 - -`springStep()` 实现了一个阻尼弹簧的单步物理模拟,用半隐式欧拉积分: - -```cpp -float pos = 0.0f; // 当前位置 -float vel = 0.0f; // 当前速度 -float target = 100.0f; // 目标位置 - -// 每帧调用(假设 60fps) -auto [newPos, newVel] = springStep(pos, vel, target, - 300.0f, // stiffness - 20.0f, // damping - 1.0f/60.0f); -pos = newPos; -vel = newVel; -``` - -stiffness 控制弹簧的硬度——值越大回弹越快。damping 控制阻尼——值越小震荡越明显。这两个参数的调优有点玄学,我们提供了一些预设值(参见 `easing.h`)。 - -⚠️ 弹簧模拟的稳定性依赖于 dt 足够小。如果 stiffness 很大但 dt 也很大,可能会数值爆炸。实时动画里一般用固定时间步,别用 frame time 差值。 - -## 角度插值 - -`lerpAngle()` 处理角度插值时的 0°/360° 边界问题,总是选择最短路径: - -```cpp -// 从 350° 转到 10°,会逆时针转 20°,而不是顺时针转 340° -float angle = lerpAngle(350.0f, 10.0f, 0.5f); // 返回 0° - -// 旋转动画 -float currentAngle = lerpAngle(startAngle, targetAngle, progress); -``` - -这个函数对旋转动画特别有用——不然你的 UI 元素可能会莫名其妙地绕一大圈。 - -## 相关文档 - -- [easing - 缓动曲线](./easing.md) -- [color - HCT 色彩空间](./color.md) +--- +title: "mathhelper - 数学工具" +description: 提供了 UI 动画和过渡效果常用的数学工具函数,包括插值、映射、三次贝塞尔曲线和弹簧物理模拟。这些函 +--- + +# math_helper - 数学工具 + +`math_helper` 提供了 UI 动画和过渡效果常用的数学工具函数,包括插值、映射、三次贝塞尔曲线和弹簧物理模拟。这些函数散落在很多地方都有实现,但我们抽出来统一管理,一是避免重复代码,二是方便后续统一调整算法参数。 + +## 线性插值 + +`lerp()` 是最基础的插值函数,在两个值之间按比例 t 进行线性过渡: + +```cpp +#include "ui/base/math_helper.h" + +using namespace cf::ui::math; + +// 基础用法:从 0 到 100,取中间值 +float value = lerp(0.0f, 100.0f, 0.5f); // 返回 50.0 + +// 动画中的淡入效果 +float opacity = lerp(0.0f, 1.0f, progress); // progress 从 0 到 1 + +// 颜色通道插值 +int red = static_cast(lerp(startColor.red(), endColor.red(), t)); +```text + +插值参数 t 不限制在 [0, 1] 范围内——超出时会产生"外推"效果,这在某些动画场景下是有用的,但要小心使用。 + +## 数值限制和映射 + +`clamp()` 用于将数值约束在指定范围内,`remap()` 则将一个区间的值映射到另一个区间: + +```cpp +// 限制范围 +float x = clamp(value, 0.0f, 1.0f); // 确保 x 在 [0, 1] 内 + +// 区间映射:将 [0, 100] 映射到 [-1, 1] +float normalized = remap(input, 0.0f, 100.0f, -1.0f, 1.0f); + +// 进度条的实际像素位置 +float pixel = remap(progress, 0.0f, 1.0f, 0.0f, trackWidth); +```text + +`remap()` 在处理滑块、进度条这类 UI 控件时特别方便——不用自己算比例和偏移了。 + +## 三次贝塞尔曲线 + +自定义缓动曲线时,`cubicBezier()` 是核心函数。给定两个控制点,计算曲线在 t 位置的 y 值: + +```cpp +// Material Design 的 emphasized 曲线 +// 控制点 (0.2, 0.0) 和 (0, 1.0) +float eased = cubicBezier(0.2f, 0.0f, 0.0f, 1.0f, t); + +// 自定义曲线 +float custom = cubicBezier(0.25f, 0.1f, 0.25f, 1.0f, progress); +```text + +这个函数的参数控制点 x 坐标建议限制在 [0, 1] 范围内,否则曲线会出现"回退"效果(一个 t 对应多个 x)。y 坐标可以超出这个范围,产生弹性过冲效果。 + +⚠️ 这是一个简化的实现,假设 x(t) ≈ t。如果需要精确的时间映射(比如控制点 x 超出 [0,1]),需要求解贝塞尔方程,那个计算量会大很多。对于 UI 动画,当前这个近似足够用了。 + +## 弹簧物理模拟 + +`springStep()` 实现了一个阻尼弹簧的单步物理模拟,用半隐式欧拉积分: + +```cpp +float pos = 0.0f; // 当前位置 +float vel = 0.0f; // 当前速度 +float target = 100.0f; // 目标位置 + +// 每帧调用(假设 60fps) +auto [newPos, newVel] = springStep(pos, vel, target, + 300.0f, // stiffness + 20.0f, // damping + 1.0f/60.0f); +pos = newPos; +vel = newVel; +```text + +stiffness 控制弹簧的硬度——值越大回弹越快。damping 控制阻尼——值越小震荡越明显。这两个参数的调优有点玄学,我们提供了一些预设值(参见 `easing.h`)。 + +⚠️ 弹簧模拟的稳定性依赖于 dt 足够小。如果 stiffness 很大但 dt 也很大,可能会数值爆炸。实时动画里一般用固定时间步,别用 frame time 差值。 + +## 角度插值 + +`lerpAngle()` 处理角度插值时的 0°/360° 边界问题,总是选择最短路径: + +```cpp +// 从 350° 转到 10°,会逆时针转 20°,而不是顺时针转 340° +float angle = lerpAngle(350.0f, 10.0f, 0.5f); // 返回 0° + +// 旋转动画 +float currentAngle = lerpAngle(startAngle, targetAngle, progress); +```text + +这个函数对旋转动画特别有用——不然你的 UI 元素可能会莫名其妙地绕一大圈。 + +## 相关文档 + +- [easing - 缓动曲线](./easing.md) +- [color - HCT 色彩空间](./color.md) diff --git a/document/HandBook/ui/components/.pages b/document/HandBook/ui/components/.pages deleted file mode 100644 index 6c1aa8953..000000000 --- a/document/HandBook/ui/components/.pages +++ /dev/null @@ -1,7 +0,0 @@ -title: 组件系统 -nav: - - 动画组件: animation.md - - 动画组: animation_group.md - - 动画工厂管理器: animation_factory_manager.md - - Spring 动画: spring_animation.md - - Timing 动画: timing_animation.md diff --git a/document/HandBook/ui/components/animation.md b/document/HandBook/ui/components/animation.md index 0ae1c5b61..0cdc28db5 100644 --- a/document/HandBook/ui/components/animation.md +++ b/document/HandBook/ui/components/animation.md @@ -1,88 +1,93 @@ -# ICFAbstractAnimation - 动画基类 - -`ICFAbstractAnimation` 是所有动画组件的抽象基类,定义了动画的生命周期、状态管理和信号接口。设计这个基类是为了统一时间动画和弹簧动画的行为模式——它们虽然内部插值逻辑不同,但启动、暂停、停止这些外部操作是一致的。 - -## 动画状态 - -动画在其生命周期内会经历四种状态: - -```cpp -enum class State { Idle, Running, Paused, Finished }; -``` - -- `Idle`:动画尚未启动或已停止,处于初始状态 -- `Running`:动画正在运行中 -- `Paused`:动画已暂停,可以继续运行 -- `Finished`:动画已完成 - -这些状态由内部管理,外部通过信号监听状态变化。 - -## 基本使用 - -动画的典型使用流程是创建、连接信号、启动: - -```cpp -#include "ui/components/animation.h" - -// 假设有一个具体实现 -auto* anim = new CFTimingAnimation(spec, this); - -// 连接信号监听动画生命周期 -connect(anim, &ICFAbstractAnimation::started, []() { - qDebug() << "Animation started"; -}); -connect(anim, &ICFAbstractAnimation::finished, []() { - qDebug() << "Animation finished"; -}); -connect(anim, &ICFAbstractAnimation::progressChanged, [](float progress) { - qDebug() << "Progress:" << progress; -}); - -// 启动动画 -anim->start(ICFAbstractAnimation::Direction::Forward); -``` - -## 方向控制 - -动画支持正向和反向播放,`reverse()` 方法会停止当前动画并以相反方向重新启动: - -```cpp -// 正向播放 -anim->start(ICFAbstractAnimation::Direction::Forward); - -// 反向播放 -anim->start(ICFAbstractAnimation::Direction::Backward); - -// 翻转当前方向 -anim->reverse(); // 停止并以相反方向重新开始 -``` - -⚠️ `reverse()` 会停止当前动画并重新启动,而不是简单地改变播放方向。如果需要无缝反向,需要自己管理逻辑。 - -## 生命周期控制 - -```cpp -// 暂停正在运行的动画 -anim->pause(); - -// 停止并重置到初始状态 -anim->stop(); -``` - -`stop()` 和 `pause()` 的区别在于:`pause()` 可以恢复,而 `stop()` 会将动画重置回初始状态。 - -## 弱引用获取 - -每个具体动画类都需要实现 `GetWeakPtr()` 方法,返回指向自己的弱引用。这是为了避免动画对象在异步回调中被意外持有导致内存泄漏: - -```cpp -cf::WeakPtr weak = anim->GetWeakPtr(); -``` - -⚠️ 动画类本身不是线程安全的。如果需要在多线程环境下使用,外部需要自行加锁。 - -## 相关文档 - -- [CFTimingAnimation - 时间动画](./timing_animation.md) -- [CFSpringAnimation - 弹簧动画](./spring_animation.md) -- [Easing - 缓动曲线](../base/easing.md) +--- +title: "ICFAbstractAnimation - 动画基类" +description: 是所有动画组件的抽象基类,定义了动画的生命周期、状态管理和信号接口。设计这个基类是为了统一时间动画和 +--- + +# ICFAbstractAnimation - 动画基类 + +`ICFAbstractAnimation` 是所有动画组件的抽象基类,定义了动画的生命周期、状态管理和信号接口。设计这个基类是为了统一时间动画和弹簧动画的行为模式——它们虽然内部插值逻辑不同,但启动、暂停、停止这些外部操作是一致的。 + +## 动画状态 + +动画在其生命周期内会经历四种状态: + +```cpp +enum class State { Idle, Running, Paused, Finished }; +```text + +- `Idle`:动画尚未启动或已停止,处于初始状态 +- `Running`:动画正在运行中 +- `Paused`:动画已暂停,可以继续运行 +- `Finished`:动画已完成 + +这些状态由内部管理,外部通过信号监听状态变化。 + +## 基本使用 + +动画的典型使用流程是创建、连接信号、启动: + +```cpp +#include "ui/components/animation.h" + +// 假设有一个具体实现 +auto* anim = new CFTimingAnimation(spec, this); + +// 连接信号监听动画生命周期 +connect(anim, &ICFAbstractAnimation::started, []() { + qDebug() << "Animation started"; +}); +connect(anim, &ICFAbstractAnimation::finished, []() { + qDebug() << "Animation finished"; +}); +connect(anim, &ICFAbstractAnimation::progressChanged, [](float progress) { + qDebug() << "Progress:" << progress; +}); + +// 启动动画 +anim->start(ICFAbstractAnimation::Direction::Forward); +```text + +## 方向控制 + +动画支持正向和反向播放,`reverse()` 方法会停止当前动画并以相反方向重新启动: + +```cpp +// 正向播放 +anim->start(ICFAbstractAnimation::Direction::Forward); + +// 反向播放 +anim->start(ICFAbstractAnimation::Direction::Backward); + +// 翻转当前方向 +anim->reverse(); // 停止并以相反方向重新开始 +```text + +⚠️ `reverse()` 会停止当前动画并重新启动,而不是简单地改变播放方向。如果需要无缝反向,需要自己管理逻辑。 + +## 生命周期控制 + +```cpp +// 暂停正在运行的动画 +anim->pause(); + +// 停止并重置到初始状态 +anim->stop(); +```text + +`stop()` 和 `pause()` 的区别在于:`pause()` 可以恢复,而 `stop()` 会将动画重置回初始状态。 + +## 弱引用获取 + +每个具体动画类都需要实现 `GetWeakPtr()` 方法,返回指向自己的弱引用。这是为了避免动画对象在异步回调中被意外持有导致内存泄漏: + +```cpp +cf::WeakPtr weak = anim->GetWeakPtr(); +```text + +⚠️ 动画类本身不是线程安全的。如果需要在多线程环境下使用,外部需要自行加锁。 + +## 相关文档 + +- [CFTimingAnimation - 时间动画](./timing_animation.md) +- [CFSpringAnimation - 弹簧动画](./spring_animation.md) +- [Easing - 缓动曲线](../base/easing.md) diff --git a/document/HandBook/ui/components/animation_factory_manager.md b/document/HandBook/ui/components/animation_factory_manager.md index 63c8398d1..d9f5a6f01 100644 --- a/document/HandBook/ui/components/animation_factory_manager.md +++ b/document/HandBook/ui/components/animation_factory_manager.md @@ -1,130 +1,135 @@ -# ICFAnimationManagerFactory - 动画工厂管理器 - -`ICFAnimationManagerFactory` 是动画的集中创建和管理接口,负责从字符串 token 生成动画实例并管理它们的生命周期。这个设计把动画的创建逻辑从业务代码里抽离出来,通过 token 来间接引用动画——这样可以在不修改调用代码的情况下,统一调整动画参数或者全局关闭动画。 - -## 工厂模式的核心思路 - -直接 new 动画对象的问题是调用方需要知道具体的动画类型、时长、缓动曲线等细节。工厂把这些都封装起来,只需要传入一个 token,就能拿到配置好的动画: - -```cpp -// 不用工厂:调用方需要知道所有细节 -auto* anim = new CFMaterialFadeAnimation( - motionSpec, // 从 theme 获取 - "opacity", 0.0f, 1.0f, - this -); - -// 用工厂:只需要一个 token -auto anim = factory->getAnimation("md.animation.fadeIn"); -if (anim) { - anim->start(); -} -``` - -## 动画注册 - -工厂支持两种注册方式——按类型名称注册,或者直接传入创建函数。 - -按类型注册比较简单,适合标准动画类型: - -```cpp -// 注册一个名为 "buttonPress" 的 fade 动画 -auto result = factory->registerOneAnimation("buttonPress", "fade"); - -if (result == ICFAnimationManagerFactory::RegisteredResult::OK) { - // 注册成功 -} else if (result == ICFAnimationManagerFactory::RegisteredResult::DUP_NAME) { - // 名称重复 -} -``` - -如果需要更精细的控制,可以用 `registerAnimationCreator` 传入一个 lambda: - -```cpp -factory->registerAnimationCreator("customFade", [](QObject* parent) { - auto* anim = new CFMaterialFadeAnimation(motionSpec, parent); - anim->setDuration(500); // 自定义时长 - return anim; -}); -``` - -⚠️ 传入的 lambda 会在每次 `getAnimation()` 被调用时执行,所以不要在这里捕获可能会失效的局部变量。 - -## Token 查询 - -`getAnimation()` 是工厂的核心方法,传入 token 字符串,返回动画的弱引用: - -```cpp -auto anim = factory->getAnimation("md.animation.fadeIn"); -if (anim) { - // 动画存在且可用 - anim->start(); -} -``` - -返回 `WeakPtr` 而不是原始指针,是因为工厂拥有动画的所有权。如果工厂被销毁,所有返回的 `WeakPtr` 会自动失效,不会变成悬空指针: - -```cpp -{ - CFMaterialAnimationFactory factory(*theme); - auto anim = factory.getAnimation("md.animation.fadeIn"); -} // factory 被销毁,anim 变成无效引用 - -// 使用 anim 前必须检查 -if (anim) { - // 这里不会执行,因为 anim 已经失效 -} -``` - -## 全局开关 - -工厂支持全局和单独的动画开关,这对性能优化和无障碍支持很有用: - -```cpp -// 全局关闭所有动画(比如在性能敏感场景) -factory->setEnabledAll(false); - -// 检查全局状态 -if (!factory->isAllEnabled()) { - return; // 动画被禁用 -} - -// 单独控制某个动画 -factory->setTargetEnabled("md.animation.fadeIn", false); -if (!factory->targetEnabled("md.animation.fadeIn")) { - // 这个动画被禁用 -} -``` - -⚠️ `setEnabledAll(false)` 只影响**新创建**的动画,已经正在运行的动画会继续直到完成。如果需要立即停止所有动画,需要自己维护一个引用列表并逐个调用 `stop()`。 - -## 目标帧率 - -`setTargetFps()` 控制动画更新的频率,影响所有由这个工厂创建的动画: - -```cpp -factory->setTargetFps(60.0f); // 60 FPS -factory->setTargetFps(30.0f); // 30 FPS(省电) -``` - -这个值会影响动画 tick 之间的时间间隔,但不会改变动画的总时长——时长是通过 `MotionSpec` 控制的。 - -## 注册结果处理 - -`registerOneAnimation()` 和 `registerAnimationCreator()` 都会返回一个 `RegisteredResult` 枚举: - -```cpp -enum class RegisteredResult { - OK, // 注册成功 - DUP_NAME, // 名称已存在 - UNSUPPORT_TYPE // 不支持的类型 -}; -``` - -遇到 `DUP_NAME` 时,如果要替换已存在的动画,需要先移除再重新注册。目前接口没有直接提供移除方法,这是设计上的取舍——我们假设动画注册是在初始化阶段一次性完成的。 - -## 相关文档 - -- [ICFAbstractAnimation - 动画基类](./animation.md) -- [CFMaterialAnimationFactory - Material 动画工厂](../material/animation/cfmaterial_animation_factory.md) -- [ICFTheme - 主题系统](../core/theme.md) +--- +title: "ICFAnimationManagerFactory - 动画工厂管理器" +description: 是动画的集中创建和管理接口,负责从字符串 token 生成动画实例并管理它们的生命周期。这个设计把动 +--- + +# ICFAnimationManagerFactory - 动画工厂管理器 + +`ICFAnimationManagerFactory` 是动画的集中创建和管理接口,负责从字符串 token 生成动画实例并管理它们的生命周期。这个设计把动画的创建逻辑从业务代码里抽离出来,通过 token 来间接引用动画——这样可以在不修改调用代码的情况下,统一调整动画参数或者全局关闭动画。 + +## 工厂模式的核心思路 + +直接 new 动画对象的问题是调用方需要知道具体的动画类型、时长、缓动曲线等细节。工厂把这些都封装起来,只需要传入一个 token,就能拿到配置好的动画: + +```cpp +// 不用工厂:调用方需要知道所有细节 +auto* anim = new CFMaterialFadeAnimation( + motionSpec, // 从 theme 获取 + "opacity", 0.0f, 1.0f, + this +); + +// 用工厂:只需要一个 token +auto anim = factory->getAnimation("md.animation.fadeIn"); +if (anim) { + anim->start(); +} +```text + +## 动画注册 + +工厂支持两种注册方式——按类型名称注册,或者直接传入创建函数。 + +按类型注册比较简单,适合标准动画类型: + +```cpp +// 注册一个名为 "buttonPress" 的 fade 动画 +auto result = factory->registerOneAnimation("buttonPress", "fade"); + +if (result == ICFAnimationManagerFactory::RegisteredResult::OK) { + // 注册成功 +} else if (result == ICFAnimationManagerFactory::RegisteredResult::DUP_NAME) { + // 名称重复 +} +```text + +如果需要更精细的控制,可以用 `registerAnimationCreator` 传入一个 lambda: + +```cpp +factory->registerAnimationCreator("customFade", [](QObject* parent) { + auto* anim = new CFMaterialFadeAnimation(motionSpec, parent); + anim->setDuration(500); // 自定义时长 + return anim; +}); +```text + +⚠️ 传入的 lambda 会在每次 `getAnimation()` 被调用时执行,所以不要在这里捕获可能会失效的局部变量。 + +## Token 查询 + +`getAnimation()` 是工厂的核心方法,传入 token 字符串,返回动画的弱引用: + +```cpp +auto anim = factory->getAnimation("md.animation.fadeIn"); +if (anim) { + // 动画存在且可用 + anim->start(); +} +```text + +返回 `WeakPtr` 而不是原始指针,是因为工厂拥有动画的所有权。如果工厂被销毁,所有返回的 `WeakPtr` 会自动失效,不会变成悬空指针: + +```cpp +{ + CFMaterialAnimationFactory factory(*theme); + auto anim = factory.getAnimation("md.animation.fadeIn"); +} // factory 被销毁,anim 变成无效引用 + +// 使用 anim 前必须检查 +if (anim) { + // 这里不会执行,因为 anim 已经失效 +} +```text + +## 全局开关 + +工厂支持全局和单独的动画开关,这对性能优化和无障碍支持很有用: + +```cpp +// 全局关闭所有动画(比如在性能敏感场景) +factory->setEnabledAll(false); + +// 检查全局状态 +if (!factory->isAllEnabled()) { + return; // 动画被禁用 +} + +// 单独控制某个动画 +factory->setTargetEnabled("md.animation.fadeIn", false); +if (!factory->targetEnabled("md.animation.fadeIn")) { + // 这个动画被禁用 +} +```text + +⚠️ `setEnabledAll(false)` 只影响**新创建**的动画,已经正在运行的动画会继续直到完成。如果需要立即停止所有动画,需要自己维护一个引用列表并逐个调用 `stop()`。 + +## 目标帧率 + +`setTargetFps()` 控制动画更新的频率,影响所有由这个工厂创建的动画: + +```cpp +factory->setTargetFps(60.0f); // 60 FPS +factory->setTargetFps(30.0f); // 30 FPS(省电) +```text + +这个值会影响动画 tick 之间的时间间隔,但不会改变动画的总时长——时长是通过 `MotionSpec` 控制的。 + +## 注册结果处理 + +`registerOneAnimation()` 和 `registerAnimationCreator()` 都会返回一个 `RegisteredResult` 枚举: + +```cpp +enum class RegisteredResult { + OK, // 注册成功 + DUP_NAME, // 名称已存在 + UNSUPPORT_TYPE // 不支持的类型 +}; +```text + +遇到 `DUP_NAME` 时,如果要替换已存在的动画,需要先移除再重新注册。目前接口没有直接提供移除方法,这是设计上的取舍——我们假设动画注册是在初始化阶段一次性完成的。 + +## 相关文档 + +- [ICFAbstractAnimation - 动画基类](./animation.md) +- [CFMaterialAnimationFactory - Material 动画工厂](../material/animation/cfmaterial_animation_factory.md) +- [ICFTheme - 主题系统](../core/theme.md) diff --git a/document/HandBook/ui/components/animation_group.md b/document/HandBook/ui/components/animation_group.md index ad56e0969..c6fc778f2 100644 --- a/document/HandBook/ui/components/animation_group.md +++ b/document/HandBook/ui/components/animation_group.md @@ -1,87 +1,92 @@ -# ICFAnimationGroup - 动画组 - -`ICFAnimationGroup` 是动画组合容器,用于将多个动画作为一个整体来控制。它支持两种执行模式——并行和顺序,能覆盖大多数需要协调多个动画的场景。设计这个组件是因为单个动画只能控制一个属性,而实际 UI 效果往往需要多个属性同步变化。 - -## 执行模式 - -动画组通过 `Mode` 枚举控制执行方式: - -```cpp -enum class Mode { Parallel, Sequential }; -``` - -`Parallel` 模式下,所有动画同时启动,适合处理多个属性的同步变化。`Sequential` 模式下,动画按添加顺序依次执行,前一个完成后才开始下一个。 - -## 基本使用 - -创建动画组并添加动画,然后像操作单个动画一样启动整个组: - -```cpp -#include "ui/components/animation_group.h" - -// 创建并行动画组 -auto* group = new ICFAnimationGroup(this); - -// 创建几个独立的动画 -auto* fadeAnim = new CFTimingAnimation(fadeSpec, this); -auto* scaleAnim = new CFTimingAnimation(scaleSpec, this); - -// 添加到组中 -group->addAnimation(fadeAnim->GetWeakPtr()); -group->addAnimation(scaleAnim->GetWeakPtr()); - -// 启动整个组 -group->start(ICFAbstractAnimation::Direction::Forward); -``` - -## 顺序执行 - -当需要动画依次出现时,使用 `Sequential` 模式。注意这个模式下动画是严格串行的,如果想做错位进入(stagger)效果,需要自己计算延迟: - -```cpp -// 顺序执行的动画组 -auto* sequentialGroup = new ICFAnimationGroup(this); -sequentialGroup->setMode(ICFAnimationGroup::Mode::Sequential); - -sequentialGroup->addAnimation(anim1->GetWeakPtr()); -sequentialGroup->addAnimation(anim2->GetWeakPtr()); -sequentialGroup->addAnimation(anim3->GetWeakPtr()); - -// anim1 完成后执行 anim2,然后 anim3 -sequentialGroup->start(); -``` - -## 弱引用管理 - -动画组持有的是动画的 `WeakPtr`,而不是直接持有原始指针。这个设计避免了循环引用导致的内存泄漏——如果动画被组持有,组又被某个对象持有,而那个对象又持有动画的引用,就会形成循环。 - -使用 `WeakPtr` 也意味着如果动画对象在外部被销毁,组里的引用会自动失效,不会变成悬空指针: - -```cpp -auto* anim = new CFTimingAnimation(spec, this); -group->addAnimation(anim->GetWeakPtr()); - -// 如果 anim 在外部被删除,组里的引用会自动失效 -delete anim; - -// 移除操作也是安全的 -group->removeAnimation(invalidWeakPtr); // 无操作 -``` - -## 生命周期 - -动画组本身也是一个 `ICFAbstractAnimation`,所以它继承了一切标准生命周期控制方法: - -```cpp -group->pause(); // 暂停组内所有动画 -group->stop(); // 停止并重置所有动画 -group->reverse(); // 翻转执行方向 -``` - -`stop()` 会停止组内所有动画并将它们重置到初始状态,而 `pause()` 只是暂停,可以继续恢复。 - -## 相关文档 - -- [ICFAbstractAnimation - 动画基类](./animation.md) -- [CFTimingAnimation - 时间动画](./timing_animation.md) -- [CFSpringAnimation - 弹簧动画](./spring_animation.md) +--- +title: "ICFAnimationGroup - 动画组" +description: 是动画组合容器,用于将多个动画作为一个整体来控制。它支持两种执行模式——并行和顺序,能覆盖大多数需要 +--- + +# ICFAnimationGroup - 动画组 + +`ICFAnimationGroup` 是动画组合容器,用于将多个动画作为一个整体来控制。它支持两种执行模式——并行和顺序,能覆盖大多数需要协调多个动画的场景。设计这个组件是因为单个动画只能控制一个属性,而实际 UI 效果往往需要多个属性同步变化。 + +## 执行模式 + +动画组通过 `Mode` 枚举控制执行方式: + +```cpp +enum class Mode { Parallel, Sequential }; +```text + +`Parallel` 模式下,所有动画同时启动,适合处理多个属性的同步变化。`Sequential` 模式下,动画按添加顺序依次执行,前一个完成后才开始下一个。 + +## 基本使用 + +创建动画组并添加动画,然后像操作单个动画一样启动整个组: + +```cpp +#include "ui/components/animation_group.h" + +// 创建并行动画组 +auto* group = new ICFAnimationGroup(this); + +// 创建几个独立的动画 +auto* fadeAnim = new CFTimingAnimation(fadeSpec, this); +auto* scaleAnim = new CFTimingAnimation(scaleSpec, this); + +// 添加到组中 +group->addAnimation(fadeAnim->GetWeakPtr()); +group->addAnimation(scaleAnim->GetWeakPtr()); + +// 启动整个组 +group->start(ICFAbstractAnimation::Direction::Forward); +```text + +## 顺序执行 + +当需要动画依次出现时,使用 `Sequential` 模式。注意这个模式下动画是严格串行的,如果想做错位进入(stagger)效果,需要自己计算延迟: + +```cpp +// 顺序执行的动画组 +auto* sequentialGroup = new ICFAnimationGroup(this); +sequentialGroup->setMode(ICFAnimationGroup::Mode::Sequential); + +sequentialGroup->addAnimation(anim1->GetWeakPtr()); +sequentialGroup->addAnimation(anim2->GetWeakPtr()); +sequentialGroup->addAnimation(anim3->GetWeakPtr()); + +// anim1 完成后执行 anim2,然后 anim3 +sequentialGroup->start(); +```text + +## 弱引用管理 + +动画组持有的是动画的 `WeakPtr`,而不是直接持有原始指针。这个设计避免了循环引用导致的内存泄漏——如果动画被组持有,组又被某个对象持有,而那个对象又持有动画的引用,就会形成循环。 + +使用 `WeakPtr` 也意味着如果动画对象在外部被销毁,组里的引用会自动失效,不会变成悬空指针: + +```cpp +auto* anim = new CFTimingAnimation(spec, this); +group->addAnimation(anim->GetWeakPtr()); + +// 如果 anim 在外部被删除,组里的引用会自动失效 +delete anim; + +// 移除操作也是安全的 +group->removeAnimation(invalidWeakPtr); // 无操作 +```text + +## 生命周期 + +动画组本身也是一个 `ICFAbstractAnimation`,所以它继承了一切标准生命周期控制方法: + +```cpp +group->pause(); // 暂停组内所有动画 +group->stop(); // 停止并重置所有动画 +group->reverse(); // 翻转执行方向 +```text + +`stop()` 会停止组内所有动画并将它们重置到初始状态,而 `pause()` 只是暂停,可以继续恢复。 + +## 相关文档 + +- [ICFAbstractAnimation - 动画基类](./animation.md) +- [CFTimingAnimation - 时间动画](./timing_animation.md) +- [CFSpringAnimation - 弹簧动画](./spring_animation.md) diff --git a/document/HandBook/ui/components/index.md b/document/HandBook/ui/components/index.md index 5e0fbe69b..2a42d57c8 100644 --- a/document/HandBook/ui/components/index.md +++ b/document/HandBook/ui/components/index.md @@ -1,10 +1,11 @@ -# components - -> Welcome to the components section. +--- +title: UI 组件 +description: 本章节包含 Material Design 3 UI 组件的使用文档,涵盖按钮(Button)、文本 +--- -## Overview +# UI 组件 -Documentation and resources for components. +本章节包含 Material Design 3 UI 组件的使用文档,涵盖按钮(Button)、文本输入框(TextField)、滑块(Slider)、开关(Switch)等组件的属性配置、信号接口和使用示例。所有组件均遵循 Material Design 3 设计规范。 --- diff --git a/document/HandBook/ui/components/spring_animation.md b/document/HandBook/ui/components/spring_animation.md index 9e27970ef..0d424d3c3 100644 --- a/document/HandBook/ui/components/spring_animation.md +++ b/document/HandBook/ui/components/spring_animation.md @@ -1,141 +1,146 @@ -# ICFSpringAnimation - 弹簧动画 - -`ICFSpringAnimation` 是基于物理弹簧模型的动画基类,用真实的弹簧动力学来驱动动画。与时间动画不同,弹簧动画没有固定的持续时间——它从当前值向目标值"弹"过去,根据刚度和阻尼参数决定运动轨迹。这种方式能产生更自然、更有"重量感"的交互反馈。 - -## 基本原理 - -弹簧动画模拟了一个连接在目标值上的物理弹簧: - -``` -加速度 = (目标值 - 当前值) * 刚度 - 速度 * 阻尼 -速度 += 加速度 * dt -位置 += 速度 * dt -``` - -系统会一直迭代,直到位置足够接近目标值且速度足够小。这意味着动画的实际时长取决于初始距离和物理参数,而不是预先设定的时间。 - -## 创建弹簧动画 - -创建时需要指定一个弹簧预设,包含刚度和阻尼参数: - -```cpp -#include "ui/components/spring_animation.h" -#include "ui/base/easing.h" - -// 使用预设的弹簧参数 -auto gentle = cf::ui::base::Easing::springGentle(); -auto* anim = new CFSpringAnimation(gentle, this); - -// 或者用高弹性预设 -auto bouncy = cf::ui::base::Easing::springBouncy(); -auto* anim2 = new CFSpringAnimation(bouncy, this); -``` - -框架提供了三种预设: -- `springGentle()`:柔和的弹簧,回弹较少,适合常规过渡 -- `springBouncy()`:高弹性,有明显的回弹,适合强调交互 -- `springStiff()`:高刚度低阻尼,快速到位但几乎无回弹 - -## 设置目标值 - -弹簧动画的核心是"追逐目标值",设置新目标会立即改变弹簧的行为: - -```cpp -// 设置目标为 1.0 -anim->setTarget(1.0f); - -// 运行过程中改变目标,弹簧会立即转向 -anim->setTarget(0.5f); // 会立即向新目标加速 -``` - -这种"追逐"特性使得弹簧动画非常适合交互式场景——比如拖拽释放后,元素会"弹"回原位,或者在拖拽过程中跟随手指。 - -## 初始速度 - -可以设置动画的初始速度,这在某些场景下很有用: - -```cpp -// 给一个向右的初速度 -anim->setInitialVelocity(100.0f); - -// 配合负目标值,可以做出"甩出去"的效果 -anim->setTarget(-1.0f); -anim->setInitialVelocity(500.0f); -``` - -如果没有设置初始速度,默认从静止开始。 - -## 获取当前值 - -具体实现类需要实现 `currentValue()` 方法,返回弹簧当前的物理位置: - -```cpp -float position = anim->currentValue(); -``` - -这个值会随着弹簧振荡而变化,直到收敛到目标值。 - -## 弹簧 vs 时间动画 - -弹簧动画的优势在于"跟随感"和"自然感": - -```cpp -// 时间动画:固定的缓动曲线,可预测但呆板 -tick(int dt) { - elapsed += dt; - float t = elapsed / duration; - float progress = easing(t); - value = from + progress * (to - from); - return elapsed < duration; -} - -// 弹簧动画:物理驱动,不可预测但生动 -tick(int dt) { - float acceleration = (target - position) * stiffness - velocity * damping; - velocity += acceleration * dt; - position += velocity * dt; - return !isSettled(); // 是否已稳定 -} -``` - -如果你在做一个按钮的点击反馈: -- 时间动画:按下时缩小,松开时放大,像在"播放一段视频" -- 弹簧动画:按下时被"压缩",松开时"弹开",像在按压真实的物体 - -后者的感觉要自然得多。 - -## 何时使用弹簧动画 - -适合弹簧动画的场景通常有这些特征: - -1. **有物理隐喻**:按钮、开关、卡片拖拽释放 -2. **需要跟随交互**:手势驱动的动画,需要能随时改变目标 -3. **需要强调感**:微交互、反馈动画,需要一点"弹性"来吸引注意 - -不适合的场景: -1. **需要精确控制时长**:编排好的多阶段动画序列 -2. **需要严格同步**:多个元素必须同时到达终点 -3. **需要线性过渡**:进度条、数据可视化 - -## 稳定条件 - -弹簧动画在以下条件时被认为"完成": - -```cpp -bool isSettled() { - return abs(position - target) < epsilon - && abs(velocity) < epsilon; -} -``` - -⚠️ 这意味着弹簧可能在理论上"永远"不结束(阻尼过低时会无限振荡)。实际使用中,框架会在若干帧后强制结束,避免空耗 CPU。 - -## 线程安全 - -弹簧动画的状态更新(`tick()`)必须在单线程上进行,通常是主线程。如果在其他线程修改 `target` 或 `velocity`,需要手动同步。 - -## 相关文档 - -- [ICFAbstractAnimation - 动画基类](./animation.md) -- [ICFTimingAnimation - 时间动画](./timing_animation.md) -- [Easing - 缓动曲线和弹簧预设](../base/easing.md) +--- +title: "ICFSpringAnimation - 弹簧动画" +description: 是基于物理弹簧模型的动画基类,用真实的弹簧动力学来驱动动画。与时间动画不同,弹簧动画没有固定的持续时 +--- + +# ICFSpringAnimation - 弹簧动画 + +`ICFSpringAnimation` 是基于物理弹簧模型的动画基类,用真实的弹簧动力学来驱动动画。与时间动画不同,弹簧动画没有固定的持续时间——它从当前值向目标值"弹"过去,根据刚度和阻尼参数决定运动轨迹。这种方式能产生更自然、更有"重量感"的交互反馈。 + +## 基本原理 + +弹簧动画模拟了一个连接在目标值上的物理弹簧: + +```text +加速度 = (目标值 - 当前值) * 刚度 - 速度 * 阻尼 +速度 += 加速度 * dt +位置 += 速度 * dt +```text + +系统会一直迭代,直到位置足够接近目标值且速度足够小。这意味着动画的实际时长取决于初始距离和物理参数,而不是预先设定的时间。 + +## 创建弹簧动画 + +创建时需要指定一个弹簧预设,包含刚度和阻尼参数: + +```cpp +#include "ui/components/spring_animation.h" +#include "ui/base/easing.h" + +// 使用预设的弹簧参数 +auto gentle = cf::ui::base::Easing::springGentle(); +auto* anim = new CFSpringAnimation(gentle, this); + +// 或者用高弹性预设 +auto bouncy = cf::ui::base::Easing::springBouncy(); +auto* anim2 = new CFSpringAnimation(bouncy, this); +```text + +框架提供了三种预设: +- `springGentle()`:柔和的弹簧,回弹较少,适合常规过渡 +- `springBouncy()`:高弹性,有明显的回弹,适合强调交互 +- `springStiff()`:高刚度低阻尼,快速到位但几乎无回弹 + +## 设置目标值 + +弹簧动画的核心是"追逐目标值",设置新目标会立即改变弹簧的行为: + +```cpp +// 设置目标为 1.0 +anim->setTarget(1.0f); + +// 运行过程中改变目标,弹簧会立即转向 +anim->setTarget(0.5f); // 会立即向新目标加速 +```text + +这种"追逐"特性使得弹簧动画非常适合交互式场景——比如拖拽释放后,元素会"弹"回原位,或者在拖拽过程中跟随手指。 + +## 初始速度 + +可以设置动画的初始速度,这在某些场景下很有用: + +```cpp +// 给一个向右的初速度 +anim->setInitialVelocity(100.0f); + +// 配合负目标值,可以做出"甩出去"的效果 +anim->setTarget(-1.0f); +anim->setInitialVelocity(500.0f); +```text + +如果没有设置初始速度,默认从静止开始。 + +## 获取当前值 + +具体实现类需要实现 `currentValue()` 方法,返回弹簧当前的物理位置: + +```cpp +float position = anim->currentValue(); +```text + +这个值会随着弹簧振荡而变化,直到收敛到目标值。 + +## 弹簧 vs 时间动画 + +弹簧动画的优势在于"跟随感"和"自然感": + +```cpp +// 时间动画:固定的缓动曲线,可预测但呆板 +tick(int dt) { + elapsed += dt; + float t = elapsed / duration; + float progress = easing(t); + value = from + progress * (to - from); + return elapsed < duration; +} + +// 弹簧动画:物理驱动,不可预测但生动 +tick(int dt) { + float acceleration = (target - position) * stiffness - velocity * damping; + velocity += acceleration * dt; + position += velocity * dt; + return !isSettled(); // 是否已稳定 +} +```text + +如果你在做一个按钮的点击反馈: +- 时间动画:按下时缩小,松开时放大,像在"播放一段视频" +- 弹簧动画:按下时被"压缩",松开时"弹开",像在按压真实的物体 + +后者的感觉要自然得多。 + +## 何时使用弹簧动画 + +适合弹簧动画的场景通常有这些特征: + +1. **有物理隐喻**:按钮、开关、卡片拖拽释放 +2. **需要跟随交互**:手势驱动的动画,需要能随时改变目标 +3. **需要强调感**:微交互、反馈动画,需要一点"弹性"来吸引注意 + +不适合的场景: +1. **需要精确控制时长**:编排好的多阶段动画序列 +2. **需要严格同步**:多个元素必须同时到达终点 +3. **需要线性过渡**:进度条、数据可视化 + +## 稳定条件 + +弹簧动画在以下条件时被认为"完成": + +```cpp +bool isSettled() { + return abs(position - target) < epsilon + && abs(velocity) < epsilon; +} +```text + +⚠️ 这意味着弹簧可能在理论上"永远"不结束(阻尼过低时会无限振荡)。实际使用中,框架会在若干帧后强制结束,避免空耗 CPU。 + +## 线程安全 + +弹簧动画的状态更新(`tick()`)必须在单线程上进行,通常是主线程。如果在其他线程修改 `target` 或 `velocity`,需要手动同步。 + +## 相关文档 + +- [ICFAbstractAnimation - 动画基类](./animation.md) +- [ICFTimingAnimation - 时间动画](./timing_animation.md) +- [Easing - 缓动曲线和弹簧预设](../base/easing.md) diff --git a/document/HandBook/ui/components/timing_animation.md b/document/HandBook/ui/components/timing_animation.md index d42b98642..048682522 100644 --- a/document/HandBook/ui/components/timing_animation.md +++ b/document/HandBook/ui/components/timing_animation.md @@ -1,99 +1,104 @@ -# ICFTimingAnimation - 时间动画 - -`ICFTimingAnimation` 是基于时间的动画基类,使用固定的持续时间和缓动曲线来完成插值。这是最常见、最直观的动画方式——我们指定一个起点、一个终点、一段时长,动画就按照缓动曲线从起点走到终点。相比弹簧动画,时间动画的行为更可预测,适合大多数 UI 过渡场景。 - -## 基本概念 - -时间动画的核心是"时间驱动"——内部维护一个已逝时间计数器,每帧累加 `dt`,当累计时间超过设定的持续时间时,动画结束。插值过程使用缓动函数将时间进度映射到值进度: - -``` -progress = easing(elapsed / duration) -value = from + progress * (to - from) -``` - -## 创建动画 - -创建时间动画需要提供一个 `IMotionSpec`,它定义了动画的持续时间和缓动类型: - -```cpp -#include "ui/components/timing_animation.h" -#include "ui/core/motion_spec.h" - -// 假设 theme->motionSpec() 返回有效的 IMotionSpec -auto* motionSpec = theme->motionSpec(); -auto* anim = new CFTimingAnimation(motionSpec, this); -``` - -⚠️ `IMotionSpec` 指针必须在动画的生命周期内保持有效。这个设计是经过权衡的——动画工厂创建动画时持有对主题的引用,而主题拥有 motion spec,所以生命周期是绑定的,不需要额外拷贝。 - -## 设置值范围 - -动画需要一个起点和终点: - -```cpp -// 从 0 到 1 -anim->setRange(0.0f, 1.0f); - -// 从透明到不透明(假设是透明度动画) -anim->setRange(0.0f, 255.0f); - -// 从位置 A 到位置 B -anim->setRange(startX, endX); -``` - -`setRange()` 可以在动画运行时调用,会即时改变当前帧的计算基准,但通常建议在 `start()` 前设置好。 - -## 获取当前值 - -具体实现类需要实现 `currentValue()` 方法,返回当前帧的插值结果: - -```cpp -float current = anim->currentValue(); -``` - -## 时间动画 vs 弹簧动画 - -选择时间动画还是弹簧动画,取决于场景的"可预测性"要求: - -| 场景 | 推荐动画 | 理由 | -|------|----------|------| -| 页面切换、模态框弹出 | 时间动画 | 需要固定时长,便于编排 | -| 按钮点击反馈 | 弹簧动画 | 需要有弹性、跟随手指的感觉 | -| 列表滚动、拖拽 | 时间动画 | 滚动距离和时长成比例 | -| 元素"弹"到位 | 弹簧动画 | 需要物理真实的减速回弹 | - -时间动画的好处是"可控"——你知道它确切会在什么时候结束,这对于编排多个连续动画非常重要。弹簧动画则更"自然",但结束时间取决于物理参数,难以精确预测。 - -## 典型使用模式 - -一个完整的淡入动画示例: - -```cpp -// 创建淡入动画 -auto* fadeIn = new CFOpacityAnimation(theme->motionSpec(), widget); -fadeIn->setRange(0.0f, 1.0f); - -// 设置持续时间和缓动(从 motion spec 查询) -int duration = theme->motionSpec()->queryDuration("md.motion.shortEnter"); -fadeIn->setDuration(duration); - -// 连接完成信号 -connect(fadeIn, &ICFAbstractAnimation::finished, []() { - qDebug() << "Fade in complete"; -}); - -// 启动 -fadeIn->start(); -``` - -## 线程安全 - -时间动画的 `tick()` 方法会被驱动器在主线程上调用,如果需要在其他线程创建或操作动画,必须手动同步。Qt 的信号槽机制可以简化跨线程调用,但动画状态变化仍需要在主线程发生。 - -⚠️ 不要在 `tick()` 内部执行耗时操作,会阻塞 UI 线程导致掉帧。 - -## 相关文档 - -- [ICFAbstractAnimation - 动画基类](./animation.md) -- [ICFSpringAnimation - 弹簧动画](./spring_animation.md) -- [IMotionSpec - 运动规范](../core/motion_spec.md) +--- +title: "ICFTimingAnimation - 时间动画" +description: 是基于时间的动画基类,使用固定的持续时间和缓动曲线来完成插值。这是最常见、最直观的动画方式——我们指 +--- + +# ICFTimingAnimation - 时间动画 + +`ICFTimingAnimation` 是基于时间的动画基类,使用固定的持续时间和缓动曲线来完成插值。这是最常见、最直观的动画方式——我们指定一个起点、一个终点、一段时长,动画就按照缓动曲线从起点走到终点。相比弹簧动画,时间动画的行为更可预测,适合大多数 UI 过渡场景。 + +## 基本概念 + +时间动画的核心是"时间驱动"——内部维护一个已逝时间计数器,每帧累加 `dt`,当累计时间超过设定的持续时间时,动画结束。插值过程使用缓动函数将时间进度映射到值进度: + +```text +progress = easing(elapsed / duration) +value = from + progress * (to - from) +```text + +## 创建动画 + +创建时间动画需要提供一个 `IMotionSpec`,它定义了动画的持续时间和缓动类型: + +```cpp +#include "ui/components/timing_animation.h" +#include "ui/core/motion_spec.h" + +// 假设 theme->motionSpec() 返回有效的 IMotionSpec +auto* motionSpec = theme->motionSpec(); +auto* anim = new CFTimingAnimation(motionSpec, this); +```text + +⚠️ `IMotionSpec` 指针必须在动画的生命周期内保持有效。这个设计是经过权衡的——动画工厂创建动画时持有对主题的引用,而主题拥有 motion spec,所以生命周期是绑定的,不需要额外拷贝。 + +## 设置值范围 + +动画需要一个起点和终点: + +```cpp +// 从 0 到 1 +anim->setRange(0.0f, 1.0f); + +// 从透明到不透明(假设是透明度动画) +anim->setRange(0.0f, 255.0f); + +// 从位置 A 到位置 B +anim->setRange(startX, endX); +```text + +`setRange()` 可以在动画运行时调用,会即时改变当前帧的计算基准,但通常建议在 `start()` 前设置好。 + +## 获取当前值 + +具体实现类需要实现 `currentValue()` 方法,返回当前帧的插值结果: + +```cpp +float current = anim->currentValue(); +```bash + +## 时间动画 vs 弹簧动画 + +选择时间动画还是弹簧动画,取决于场景的"可预测性"要求: + +| 场景 | 推荐动画 | 理由 | +|------|----------|------| +| 页面切换、模态框弹出 | 时间动画 | 需要固定时长,便于编排 | +| 按钮点击反馈 | 弹簧动画 | 需要有弹性、跟随手指的感觉 | +| 列表滚动、拖拽 | 时间动画 | 滚动距离和时长成比例 | +| 元素"弹"到位 | 弹簧动画 | 需要物理真实的减速回弹 | + +时间动画的好处是"可控"——你知道它确切会在什么时候结束,这对于编排多个连续动画非常重要。弹簧动画则更"自然",但结束时间取决于物理参数,难以精确预测。 + +## 典型使用模式 + +一个完整的淡入动画示例: + +```cpp +// 创建淡入动画 +auto* fadeIn = new CFOpacityAnimation(theme->motionSpec(), widget); +fadeIn->setRange(0.0f, 1.0f); + +// 设置持续时间和缓动(从 motion spec 查询) +int duration = theme->motionSpec()->queryDuration("md.motion.shortEnter"); +fadeIn->setDuration(duration); + +// 连接完成信号 +connect(fadeIn, &ICFAbstractAnimation::finished, []() { + qDebug() << "Fade in complete"; +}); + +// 启动 +fadeIn->start(); +```text + +## 线程安全 + +时间动画的 `tick()` 方法会被驱动器在主线程上调用,如果需要在其他线程创建或操作动画,必须手动同步。Qt 的信号槽机制可以简化跨线程调用,但动画状态变化仍需要在主线程发生。 + +⚠️ 不要在 `tick()` 内部执行耗时操作,会阻塞 UI 线程导致掉帧。 + +## 相关文档 + +- [ICFAbstractAnimation - 动画基类](./animation.md) +- [ICFSpringAnimation - 弹簧动画](./spring_animation.md) +- [IMotionSpec - 运动规范](../core/motion_spec.md) diff --git a/document/HandBook/ui/core/.pages b/document/HandBook/ui/core/.pages deleted file mode 100644 index 9ea8ae131..000000000 --- a/document/HandBook/ui/core/.pages +++ /dev/null @@ -1,10 +0,0 @@ -title: 核心模块 -nav: - - 主题: theme.md - - 配色方案: color_scheme.md - - 字体类型: font_type.md - - 运动规格: motion_spec.md - - 圆角缩放: radius_scale.md - - 主题工厂: theme_factory.md - - 主题管理器: theme_manager.md - - Token 系统: token diff --git a/document/HandBook/ui/core/color_scheme.md b/document/HandBook/ui/core/color_scheme.md index 39db6ba8e..8bdec3f2b 100644 --- a/document/HandBook/ui/core/color_scheme.md +++ b/document/HandBook/ui/core/color_scheme.md @@ -1,94 +1,99 @@ -# ICFColorScheme - 颜色方案接口 - -`ICFColorScheme` 定义了按名称查询颜色的抽象接口,是 Material Design 3 颜色系统的 C++ 映射层。选择用字符串键值对而不是枚举来访问颜色,是为了支持动态主题和运行时扩展——我们可以在不重新编译的情况下加载新的颜色方案。 - -## 基本用法 - -通过 `queryColor()` 查询颜色值: - -```cpp -#include "ui/core/color_scheme.h" - -ICFColorScheme& colors = theme.color_scheme(); - -// 查询 Material Design 颜色 token -QColor primary = colors.queryColor("md.primary"); -QColor onPrimary = colors.queryColor("md.onPrimary"); -QColor surface = colors.queryColor("md.surface"); -QColor error = colors.queryColor("md.error"); -``` - -`queryColor()` 返回的是 `QColor` 的副本,适合直接使用。如果需要避免拷贝开销(比如在循环中频繁访问),可以用 `queryExpectedColor()` 获取引用: - -```cpp -// 避免拷贝的高频访问场景 -const QColor& primary = colors.queryExpectedColor("md.primary"); -for (int i = 0; i < 1000; ++i) { - // 使用 primary,不会触发拷贝 - painter.setBrush(primary); -} -``` - -⚠️ `queryExpectedColor()` 返回的引用生命周期和 color scheme 对象绑定。如果 color scheme 被销毁,引用就会悬空——这个坑在异步代码里特别容易出现,务必注意。 - -## Token 命名约定 - -虽然接口本身不规定 token 的命名格式,但我们遵循 Material Design 3 的约定: - -```cpp -// 基础颜色角色 -"md.primary" // 主色 -"md.onPrimary" // 主色上的内容色 -"md.primaryContainer" // 主色容器 -"md.onPrimaryContainer" // 主色容器上的内容色 - -// 语义颜色 -"md.background" // 背景色 -"md.onBackground" // 背景上的内容色 -"md.surface" // 表面色 -"md.error" // 错误色 -"md.outline" // 边框/分割线色 -``` - -具体的 token 列表由实现类决定,可以在运行时动态扩展。如果查询不存在的 token,实现类的行为取决于具体实现——我们推荐返回一个明显的 fallback 颜色(比如品红色),方便调试。 - -## 引用 vs 拷贝 - -接口提供了两个查询方法,返回类型不同: - -```cpp -// 返回引用,可修改 -QColor& queryExpectedColor(const char* name); - -// 返回拷贝,只读便捷方法 -QColor queryColor(const char* name) const; -``` - -`queryExpectedColor()` 返回非 const 引用是有意为之的——这允许调用方动态修改颜色值,实现运行时主题调整。比如实现"跟随系统 accent color"的功能时,可以直接修改对应的颜色槽位: - -```cpp -// 动态更新主色 -void updateAccentColor(ICFColorScheme& colors, const QColor& newAccent) { - colors.queryExpectedColor("md.primary") = newAccent; - colors.queryExpectedColor("md.primaryContainer") = newAccent.lighter(120%); -} -``` - -⚠️ 直接修改颜色会影响所有使用这个 color scheme 的组件。如果只是局部需要特殊颜色,应该在查询后自行调整,而不是修改共享的 scheme。 - -## 实现要点 - -实现 `ICFColorScheme` 时需要考虑几个细节: - -1. **字符串比较开销**:每次查询都做字符串比较确实不快,但颜色查询通常不是性能热点。如果确实需要优化,可以用 `std::unordered_map` 而不是线性查找。 - -2. **线程安全**:如果实现类支持跨线程访问,需要在 `queryExpectedColor()` 内部加锁。考虑到锁的粒度很细(一次哈希查找),性能影响通常可接受。 - -3. **无效 token 处理**:不要返回 `QColor()` —— 默认构造的 QColor 是无效颜色(`isValid() == false`),在渲染时会表现为黑色,很难调试。推荐返回一个显眼的 fallback 颜色。 - -## 相关文档 - -- [ICFTheme - 主题接口](./theme.md) -- [IMotionSpec - 动画规格](./motion_spec.md) -- [IRadiusScale - 圆角规范](./radius_scale.md) -- [IFontType - 字体样式](./font_type.md) +--- +title: "ICFColorScheme - 颜色方案接口" +description: 定义了按名称查询颜色的抽象接口,是 Material Design 3 颜色系统的 C++ 映射层。 +--- + +# ICFColorScheme - 颜色方案接口 + +`ICFColorScheme` 定义了按名称查询颜色的抽象接口,是 Material Design 3 颜色系统的 C++ 映射层。选择用字符串键值对而不是枚举来访问颜色,是为了支持动态主题和运行时扩展——我们可以在不重新编译的情况下加载新的颜色方案。 + +## 基本用法 + +通过 `queryColor()` 查询颜色值: + +```cpp +#include "ui/core/color_scheme.h" + +ICFColorScheme& colors = theme.color_scheme(); + +// 查询 Material Design 颜色 token +QColor primary = colors.queryColor("md.primary"); +QColor onPrimary = colors.queryColor("md.onPrimary"); +QColor surface = colors.queryColor("md.surface"); +QColor error = colors.queryColor("md.error"); +```text + +`queryColor()` 返回的是 `QColor` 的副本,适合直接使用。如果需要避免拷贝开销(比如在循环中频繁访问),可以用 `queryExpectedColor()` 获取引用: + +```cpp +// 避免拷贝的高频访问场景 +const QColor& primary = colors.queryExpectedColor("md.primary"); +for (int i = 0; i < 1000; ++i) { + // 使用 primary,不会触发拷贝 + painter.setBrush(primary); +} +```text + +⚠️ `queryExpectedColor()` 返回的引用生命周期和 color scheme 对象绑定。如果 color scheme 被销毁,引用就会悬空——这个坑在异步代码里特别容易出现,务必注意。 + +## Token 命名约定 + +虽然接口本身不规定 token 的命名格式,但我们遵循 Material Design 3 的约定: + +```cpp +// 基础颜色角色 +"md.primary" // 主色 +"md.onPrimary" // 主色上的内容色 +"md.primaryContainer" // 主色容器 +"md.onPrimaryContainer" // 主色容器上的内容色 + +// 语义颜色 +"md.background" // 背景色 +"md.onBackground" // 背景上的内容色 +"md.surface" // 表面色 +"md.error" // 错误色 +"md.outline" // 边框/分割线色 +```text + +具体的 token 列表由实现类决定,可以在运行时动态扩展。如果查询不存在的 token,实现类的行为取决于具体实现——我们推荐返回一个明显的 fallback 颜色(比如品红色),方便调试。 + +## 引用 vs 拷贝 + +接口提供了两个查询方法,返回类型不同: + +```cpp +// 返回引用,可修改 +QColor& queryExpectedColor(const char* name); + +// 返回拷贝,只读便捷方法 +QColor queryColor(const char* name) const; +```text + +`queryExpectedColor()` 返回非 const 引用是有意为之的——这允许调用方动态修改颜色值,实现运行时主题调整。比如实现"跟随系统 accent color"的功能时,可以直接修改对应的颜色槽位: + +```cpp +// 动态更新主色 +void updateAccentColor(ICFColorScheme& colors, const QColor& newAccent) { + colors.queryExpectedColor("md.primary") = newAccent; + colors.queryExpectedColor("md.primaryContainer") = newAccent.lighter(120%); +} +```text + +⚠️ 直接修改颜色会影响所有使用这个 color scheme 的组件。如果只是局部需要特殊颜色,应该在查询后自行调整,而不是修改共享的 scheme。 + +## 实现要点 + +实现 `ICFColorScheme` 时需要考虑几个细节: + +1. **字符串比较开销**:每次查询都做字符串比较确实不快,但颜色查询通常不是性能热点。如果确实需要优化,可以用 `std::unordered_map` 而不是线性查找。 + +2. **线程安全**:如果实现类支持跨线程访问,需要在 `queryExpectedColor()` 内部加锁。考虑到锁的粒度很细(一次哈希查找),性能影响通常可接受。 + +3. **无效 token 处理**:不要返回 `QColor()` —— 默认构造的 QColor 是无效颜色(`isValid() == false`),在渲染时会表现为黑色,很难调试。推荐返回一个显眼的 fallback 颜色。 + +## 相关文档 + +- [ICFTheme - 主题接口](./theme.md) +- [IMotionSpec - 动画规格](./motion_spec.md) +- [IRadiusScale - 圆角规范](./radius_scale.md) +- [IFontType - 字体样式](./font_type.md) diff --git a/document/HandBook/ui/core/font_type.md b/document/HandBook/ui/core/font_type.md index 93f301118..ec578f236 100644 --- a/document/HandBook/ui/core/font_type.md +++ b/document/HandBook/ui/core/font_type.md @@ -1,113 +1,118 @@ -# IFontType - 字体样式接口 - -`IFontType` 定义了按名称查询字体样式的抽象接口,是 Material Design 3 排版系统的 C++ 映射层。选择用字符串键值而不是枚举来访问字体,是为了支持动态字体配置和运行时扩展——我们可以在不重新编译的情况下加载新的字体方案。 - -## 基本用法 - -通过 `queryTargetFont()` 查询字体样式: - -```cpp -#include "ui/core/font_type.h" - -IFontType& fonts = theme.font_type(); - -// 查询 Material Design 字体 token -QFont bodyLarge = fonts.queryTargetFont("bodyLarge"); -QFont headlineMedium = fonts.queryTargetFont("headlineMedium"); -QFont titleSmall = fonts.queryTargetFont("titleSmall"); -QFont labelLarge = fonts.queryTargetFont("labelLarge"); -``` - -`queryTargetFont()` 返回的是 `QFont` 的副本,适合直接使用。Qt 的 `QFont` 采用隐式共享(写时拷贝)机制,所以即使返回副本,实际拷贝开销也很小——只有在修改字体时才会真正复制数据。 - -## Token 命名约定 - -虽然接口本身不规定 token 的命名格式,但我们遵循 Material Design 3 的约定: - -```cpp -// 正文样式 -"bodyLarge" // 正文大号 -"bodyMedium" // 正文中号 -"bodySmall" // 正文小号 - -// 标题样式 -"headlineLarge" // 标题大号 -"headlineMedium" // 标题中号 -"headlineSmall" // 标题小号 - -"titleLarge" // 次级标题大号 -"titleMedium" // 次级标题中号 -"titleSmall" // 次级标题小号 - -// 标签样式 -"labelLarge" // 标签大号 -"labelMedium" // 标签中号 -"labelSmall" // 标签小号 -``` - -具体的 token 列表由实现类决定,可以在运行时动态扩展。如果查询不存在的 token,实现类的行为取决于具体实现——我们推荐返回一个默认的 fallback 字体,比如系统等宽字体,方便调试。 - -## 使用示例 - -在实际 UI 组件中使用字体: - -```cpp -// 在自定义 widget 中应用主题字体 -class MyWidget : public QWidget { -protected: - void paintEvent(QPaintEvent* event) override { - QPainter painter(this); - auto& fonts = getTheme().font_type(); - - // 使用主题字体绘制文本 - QFont titleFont = fonts.queryTargetFont("headlineMedium"); - painter.setFont(titleFont); - painter.drawText(rect(), "My Title"); - - // 使用正文字体 - QFont bodyFont = fonts.queryTargetFont("bodyMedium"); - painter.setFont(bodyFont); - painter.drawText(rect().adjusted(0, 30, 0, 0), "Body text"); - } -}; -``` - -⚠️ 不要在每次绘制时都查询字体——把字体对象缓存起来更好。`QFont` 的隐式共享机制让缓存几乎没有开销: - -```cpp -class MyWidget : public QWidget { -private: - QFont m_titleFont; // 缓存字体对象 - -public: - MyWidget(QWidget* parent = nullptr) : QWidget(parent) { - auto& fonts = getTheme().font_type(); - m_titleFont = fonts.queryTargetFont("headlineMedium"); - } -}; -``` - -## 实现要点 - -实现 `IFontType` 时需要考虑几个细节: - -1. **字符串比较开销**:每次查询都做字符串比较确实不快,但字体查询通常不是性能热点。如果确实需要优化,可以用 `std::unordered_map` 而不是线性查找。 - -2. **线程安全**:如果实现类支持跨线程访问,需要在 `queryTargetFont()` 内部加锁。考虑到 `QFont` 的拷贝开销很小,也可以在加锁后直接返回副本,避免返回引用带来的生命周期问题。 - -3. **无效 token 处理**:不要返回 `QFont()` —— 默认构造的 QFont 是应用程序默认字体,很难发现错误。推荐返回一个显眼的 fallback 字体,比如等宽字体或高亮样式,方便调试。 - -4. **字体家族回退**:如果配置的字体家族在系统上不存在,Qt 会自动回退到默认字体。实现类可以选择在初始化时验证字体可用性,或者依赖 Qt 的回退机制。 - -## 设计决策 - -`IFontType` 只提供了一个查询方法,而不是像 `ICFColorScheme` 那样提供引用和拷贝两种变体。这是因为 `QFont` 本身就是值类型的——隐式共享让拷贝开销可以忽略,而引用语义反而会带来生命周期管理的复杂度。 - -另一个设计点是,我们用 `const char*` 而不是 `QString` 作为参数类型。这是为了让接口更容易在 C 和其他语言中调用,同时避免 `QString` 带来的 Qt 依赖传播。如果实现类内部用 `QString` 存储,可以在方法入口处做一次转换。 - -## 相关文档 - -- [ICFTheme - 主题接口](./theme.md) -- [ICFColorScheme - 颜色方案](./color_scheme.md) -- [IMotionSpec - 动画规格](./motion_spec.md) -- [IRadiusScale - 圆角规范](./radius_scale.md) +--- +title: "IFontType - 字体样式接口" +description: 定义了按名称查询字体样式的抽象接口,是 Material Design 3 排版系统的 C++ 映射 +--- + +# IFontType - 字体样式接口 + +`IFontType` 定义了按名称查询字体样式的抽象接口,是 Material Design 3 排版系统的 C++ 映射层。选择用字符串键值而不是枚举来访问字体,是为了支持动态字体配置和运行时扩展——我们可以在不重新编译的情况下加载新的字体方案。 + +## 基本用法 + +通过 `queryTargetFont()` 查询字体样式: + +```cpp +#include "ui/core/font_type.h" + +IFontType& fonts = theme.font_type(); + +// 查询 Material Design 字体 token +QFont bodyLarge = fonts.queryTargetFont("bodyLarge"); +QFont headlineMedium = fonts.queryTargetFont("headlineMedium"); +QFont titleSmall = fonts.queryTargetFont("titleSmall"); +QFont labelLarge = fonts.queryTargetFont("labelLarge"); +```text + +`queryTargetFont()` 返回的是 `QFont` 的副本,适合直接使用。Qt 的 `QFont` 采用隐式共享(写时拷贝)机制,所以即使返回副本,实际拷贝开销也很小——只有在修改字体时才会真正复制数据。 + +## Token 命名约定 + +虽然接口本身不规定 token 的命名格式,但我们遵循 Material Design 3 的约定: + +```cpp +// 正文样式 +"bodyLarge" // 正文大号 +"bodyMedium" // 正文中号 +"bodySmall" // 正文小号 + +// 标题样式 +"headlineLarge" // 标题大号 +"headlineMedium" // 标题中号 +"headlineSmall" // 标题小号 + +"titleLarge" // 次级标题大号 +"titleMedium" // 次级标题中号 +"titleSmall" // 次级标题小号 + +// 标签样式 +"labelLarge" // 标签大号 +"labelMedium" // 标签中号 +"labelSmall" // 标签小号 +```text + +具体的 token 列表由实现类决定,可以在运行时动态扩展。如果查询不存在的 token,实现类的行为取决于具体实现——我们推荐返回一个默认的 fallback 字体,比如系统等宽字体,方便调试。 + +## 使用示例 + +在实际 UI 组件中使用字体: + +```cpp +// 在自定义 widget 中应用主题字体 +class MyWidget : public QWidget { +protected: + void paintEvent(QPaintEvent* event) override { + QPainter painter(this); + auto& fonts = getTheme().font_type(); + + // 使用主题字体绘制文本 + QFont titleFont = fonts.queryTargetFont("headlineMedium"); + painter.setFont(titleFont); + painter.drawText(rect(), "My Title"); + + // 使用正文字体 + QFont bodyFont = fonts.queryTargetFont("bodyMedium"); + painter.setFont(bodyFont); + painter.drawText(rect().adjusted(0, 30, 0, 0), "Body text"); + } +}; +```text + +⚠️ 不要在每次绘制时都查询字体——把字体对象缓存起来更好。`QFont` 的隐式共享机制让缓存几乎没有开销: + +```cpp +class MyWidget : public QWidget { +private: + QFont m_titleFont; // 缓存字体对象 + +public: + MyWidget(QWidget* parent = nullptr) : QWidget(parent) { + auto& fonts = getTheme().font_type(); + m_titleFont = fonts.queryTargetFont("headlineMedium"); + } +}; +```text + +## 实现要点 + +实现 `IFontType` 时需要考虑几个细节: + +1. **字符串比较开销**:每次查询都做字符串比较确实不快,但字体查询通常不是性能热点。如果确实需要优化,可以用 `std::unordered_map` 而不是线性查找。 + +2. **线程安全**:如果实现类支持跨线程访问,需要在 `queryTargetFont()` 内部加锁。考虑到 `QFont` 的拷贝开销很小,也可以在加锁后直接返回副本,避免返回引用带来的生命周期问题。 + +3. **无效 token 处理**:不要返回 `QFont()` —— 默认构造的 QFont 是应用程序默认字体,很难发现错误。推荐返回一个显眼的 fallback 字体,比如等宽字体或高亮样式,方便调试。 + +4. **字体家族回退**:如果配置的字体家族在系统上不存在,Qt 会自动回退到默认字体。实现类可以选择在初始化时验证字体可用性,或者依赖 Qt 的回退机制。 + +## 设计决策 + +`IFontType` 只提供了一个查询方法,而不是像 `ICFColorScheme` 那样提供引用和拷贝两种变体。这是因为 `QFont` 本身就是值类型的——隐式共享让拷贝开销可以忽略,而引用语义反而会带来生命周期管理的复杂度。 + +另一个设计点是,我们用 `const char*` 而不是 `QString` 作为参数类型。这是为了让接口更容易在 C 和其他语言中调用,同时避免 `QString` 带来的 Qt 依赖传播。如果实现类内部用 `QString` 存储,可以在方法入口处做一次转换。 + +## 相关文档 + +- [ICFTheme - 主题接口](./theme.md) +- [ICFColorScheme - 颜色方案](./color_scheme.md) +- [IMotionSpec - 动画规格](./motion_spec.md) +- [IRadiusScale - 圆角规范](./radius_scale.md) diff --git a/document/HandBook/ui/core/index.md b/document/HandBook/ui/core/index.md index b84875c5c..5b7fef150 100644 --- a/document/HandBook/ui/core/index.md +++ b/document/HandBook/ui/core/index.md @@ -1,10 +1,11 @@ -# core - -> Welcome to the core section. +--- +title: UI 核心引擎 +description: 本章节介绍 UI 框架的核心主题引擎,包括 主题管理器、 Material 工厂以及完整的 Des +--- -## Overview +# UI 核心引擎 -Documentation and resources for core. +本章节介绍 UI 框架的核心主题引擎,包括 `ThemeManager` 主题管理器、`MaterialFactory` Material 工厂以及完整的 Design Token 系统。核心引擎负责将 Material Design 3 的设计规范转化为可编程的 Token 配置,驱动整个 UI 渲染管线。 --- diff --git a/document/HandBook/ui/core/motion_spec.md b/document/HandBook/ui/core/motion_spec.md index 38e0e644e..4e9d65156 100644 --- a/document/HandBook/ui/core/motion_spec.md +++ b/document/HandBook/ui/core/motion_spec.md @@ -1,168 +1,173 @@ -# IMotionSpec - 动画规格接口 - -`IMotionSpec` 定义了按名称查询动画参数的抽象接口,是 Material Design 3 运动系统的 C++ 映射层。选择把持续时间、缓动类型和延迟分别查询而不是打包成一个结构体,是为了灵活性——有些场景只需要持续时间,有些场景需要完整的三参数组合。 - -## 基本用法 - -`IMotionSpec` 提供了三个查询方法,分别对应动画的三个核心参数: - -```cpp -#include "ui/core/motion_spec.h" - -IMotionSpec& motion = theme.motion_spec(); - -// 查询动画持续时间(毫秒) -int duration = motion.queryDuration("md.motion.shortEnter"); // 200ms -int standard = motion.queryDuration("md.motion.standard"); // 400ms - -// 查询缓动类型(枚举值) -int easing = motion.queryEasing("md.motion.standard"); // 线性/缓入缓出 - -// 查询动画延迟(毫秒) -int delay = motion.queryDelay("md.motion.shortEnter"); // 通常为 0 -``` - -这三个参数组合起来可以构造完整的动画配置: - -```cpp -// 使用 QPropertyAnimation -auto* animation = new QPropertyAnimation(button, "geometry"); -animation->setDuration(motion.queryDuration("md.motion.standard")); -animation->setEasingCurve(static_cast( - motion.queryEasing("md.motion.standard") -)); -animation->setStartValue(startRect); -animation->setEndValue(endRect); -animation->start(); -``` - -## Token 命名约定 - -虽然接口本身不规定 token 的命名格式,但我们遵循 Material Design 3 的约定: - -```cpp -// Material Design 3 运动规范 -"md.motion.standard" // 标准运动(400ms,缓入缓出) -"md.motion.shortEnter" // 短进入动画(200ms,缓出) -"md.motion.mediumEnter" // 中等进入动画(300ms,缓出) -"md.motion.longEnter" // 长进入动画(500ms,缓出) -"md.motion.shortExit" // 短退出动画(150ms,缓入) -"md.motion.mediumExit" // 中等退出动画(250ms,缓入) -"md.motion.longExit" // 长退出动画(350ms,缓入) -"md.motion.shortDuration" // 短持续时间(150ms) -"md.motion.mediumDuration" // 中等持续时间(250ms) -"md.motion.longDuration" // 长持续时间(350ms) -"md.motion.extraLongDuration" // 超长持续时间(500ms+) -``` - -缓动类型返回的是整数值,需要映射到具体的缓动曲线枚举。Qt 的 `QEasingCurve::Type` 是一个常见的映射目标: - -```cpp -// 缓动类型映射示例 -// 返回值 -> QEasingCurve::Type -// 0 -> Linear -// 1 -> InQuad -// 2 -> OutQuad -// 3 -> InOutQuad -// 32 -> InCubic -// 33 -> OutCubic -// 34 -> InOutCubic -// ... -``` - -具体的映射关系由实现类决定,可以在文档中说明。 - -## 使用示例 - -在实际 UI 组件中使用动画规格: - -```cpp -// 淡入动画 -void fadeInWidget(QWidget* widget, const char* motionToken) { - auto* effect = new QGraphicsOpacityEffect(widget); - widget->setGraphicsEffect(effect); - - auto* animation = new QPropertyAnimation(effect, "opacity"); - animation->setDuration(motion.queryDuration(motionToken)); - animation->setStartValue(0.0); - animation->setEndValue(1.0); - animation->setEasingCurve(static_cast( - motion.queryEasing(motionToken) - )); - animation->start(QAbstractAnimation::DeleteWhenStopped); -} - -// 使用 -fadeInWidget(myWidget, "md.motion.shortEnter"); // 快速淡入 -``` - -对于需要延迟的动画序列: - -```cpp -void staggeredShow(QList widgets) { - auto& motion = getTheme().motion_spec(); - int staggerDelay = motion.queryDelay("md.motion.staggerDelay"); // 50ms - - for (int i = 0; i < widgets.size(); ++i) { - auto* animation = new QPropertyAnimation(widgets[i], "opacity"); - animation->setDuration(motion.queryDuration("md.motion.shortEnter")); - animation->setStartValue(0.0); - animation->setEndValue(1.0); - animation->setStartDelay(i * staggerDelay); // 错开延迟 - animation->start(); - } -} -``` - -## 缓动曲线映射 - -缓动类型返回的整数值需要映射到具体框架的缓动曲线。这里是一个简单的映射辅助类示例: - -```cpp -class EasingCurveMapper { -public: - static QEasingCurve fromMotionValue(int value) { - switch (value) { - case 0: return QEasingCurve::Linear; - case 1: return QEasingCurve::InQuad; - case 2: return QEasingCurve::OutQuad; - case 3: return QEasingCurve::InOutQuad; - case 32: return QEasingCurve::InCubic; - case 33: return QEasingCurve::OutCubic; - case 34: return QEasingCurve::InOutCubic; - default: return QEasingCurve::InOutCubic; // fallback - } - } -}; - -// 使用 -auto curve = EasingCurveMapper::fromMotionValue( - motion.queryEasing("md.motion.standard") -); -animation->setEasingCurve(curve); -``` - -## 实现要点 - -实现 `IMotionSpec` 时需要考虑几个细节: - -1. **缓动类型的表示**:接口用 `int` 返回缓动类型,而不是定义自己的枚举。这是为了避免向调用方传播 C++ 类型定义——如果接口要被其他语言调用,整数比枚举更容易处理。实现类可以选择内部用枚举,导出时转换。 - -2. **默认延迟值**:大多数动画不需要延迟,所以 `queryDelay()` 的默认返回值应该是 0。只有在"交错动画"这类特殊场景下,才需要非零延迟。 - -3. **无效 token 处理**:不要抛异常或返回负值。对于持续时间,返回一个合理的默认值(比如 300ms);对于缓动类型,返回一个常用的缓动曲线;对于延迟,返回 0。 - -4. **线程安全**:动画规格查询通常发生在 UI 线程,但如果实现类支持跨线程访问,需要在各方法内部加锁。考虑到这些都是简单的哈希查找,锁的开销可以接受。 - -## 设计决策 - -`IMotionSpec` 把三个参数分开查询,而不是返回一个包含三个字段的结构体。这是为了使用场景的灵活性——很多动画只需要持续时间和缓动,延迟总是 0;而有些场景(比如交错动画)只需要延迟参数。如果打包成结构体,调用方总是要构造或接收一个他们不需要的字段。 - -另一个设计点是,我们用 `int` 表示时间而不是 `std::chrono::duration`。这是因为 Qt 的动画 API 用整数毫秒,用标准库类型反而需要转换。如果将来要支持更高精度的时间单位,可以在实现类内部用 `std::chrono`,导出时转换成毫秒。 - -## 相关文档 - -- [ICFTheme - 主题接口](./theme.md) -- [ICFColorScheme - 颜色方案](./color_scheme.md) -- [IRadiusScale - 圆角规范](./radius_scale.md) -- [IFontType - 字体样式](./font_type.md) +--- +title: "IMotionSpec - 动画规格接口" +description: 定义了按名称查询动画参数的抽象接口,是 Material Design 3 运动系统的 C++ 映射 +--- + +# IMotionSpec - 动画规格接口 + +`IMotionSpec` 定义了按名称查询动画参数的抽象接口,是 Material Design 3 运动系统的 C++ 映射层。选择把持续时间、缓动类型和延迟分别查询而不是打包成一个结构体,是为了灵活性——有些场景只需要持续时间,有些场景需要完整的三参数组合。 + +## 基本用法 + +`IMotionSpec` 提供了三个查询方法,分别对应动画的三个核心参数: + +```cpp +#include "ui/core/motion_spec.h" + +IMotionSpec& motion = theme.motion_spec(); + +// 查询动画持续时间(毫秒) +int duration = motion.queryDuration("md.motion.shortEnter"); // 200ms +int standard = motion.queryDuration("md.motion.standard"); // 400ms + +// 查询缓动类型(枚举值) +int easing = motion.queryEasing("md.motion.standard"); // 线性/缓入缓出 + +// 查询动画延迟(毫秒) +int delay = motion.queryDelay("md.motion.shortEnter"); // 通常为 0 +```text + +这三个参数组合起来可以构造完整的动画配置: + +```cpp +// 使用 QPropertyAnimation +auto* animation = new QPropertyAnimation(button, "geometry"); +animation->setDuration(motion.queryDuration("md.motion.standard")); +animation->setEasingCurve(static_cast( + motion.queryEasing("md.motion.standard") +)); +animation->setStartValue(startRect); +animation->setEndValue(endRect); +animation->start(); +```text + +## Token 命名约定 + +虽然接口本身不规定 token 的命名格式,但我们遵循 Material Design 3 的约定: + +```cpp +// Material Design 3 运动规范 +"md.motion.standard" // 标准运动(400ms,缓入缓出) +"md.motion.shortEnter" // 短进入动画(200ms,缓出) +"md.motion.mediumEnter" // 中等进入动画(300ms,缓出) +"md.motion.longEnter" // 长进入动画(500ms,缓出) +"md.motion.shortExit" // 短退出动画(150ms,缓入) +"md.motion.mediumExit" // 中等退出动画(250ms,缓入) +"md.motion.longExit" // 长退出动画(350ms,缓入) +"md.motion.shortDuration" // 短持续时间(150ms) +"md.motion.mediumDuration" // 中等持续时间(250ms) +"md.motion.longDuration" // 长持续时间(350ms) +"md.motion.extraLongDuration" // 超长持续时间(500ms+) +```text + +缓动类型返回的是整数值,需要映射到具体的缓动曲线枚举。Qt 的 `QEasingCurve::Type` 是一个常见的映射目标: + +```cpp +// 缓动类型映射示例 +// 返回值 -> QEasingCurve::Type +// 0 -> Linear +// 1 -> InQuad +// 2 -> OutQuad +// 3 -> InOutQuad +// 32 -> InCubic +// 33 -> OutCubic +// 34 -> InOutCubic +// ... +```text + +具体的映射关系由实现类决定,可以在文档中说明。 + +## 使用示例 + +在实际 UI 组件中使用动画规格: + +```cpp +// 淡入动画 +void fadeInWidget(QWidget* widget, const char* motionToken) { + auto* effect = new QGraphicsOpacityEffect(widget); + widget->setGraphicsEffect(effect); + + auto* animation = new QPropertyAnimation(effect, "opacity"); + animation->setDuration(motion.queryDuration(motionToken)); + animation->setStartValue(0.0); + animation->setEndValue(1.0); + animation->setEasingCurve(static_cast( + motion.queryEasing(motionToken) + )); + animation->start(QAbstractAnimation::DeleteWhenStopped); +} + +// 使用 +fadeInWidget(myWidget, "md.motion.shortEnter"); // 快速淡入 +```text + +对于需要延迟的动画序列: + +```cpp +void staggeredShow(QList widgets) { + auto& motion = getTheme().motion_spec(); + int staggerDelay = motion.queryDelay("md.motion.staggerDelay"); // 50ms + + for (int i = 0; i < widgets.size(); ++i) { + auto* animation = new QPropertyAnimation(widgets[i], "opacity"); + animation->setDuration(motion.queryDuration("md.motion.shortEnter")); + animation->setStartValue(0.0); + animation->setEndValue(1.0); + animation->setStartDelay(i * staggerDelay); // 错开延迟 + animation->start(); + } +} +```text + +## 缓动曲线映射 + +缓动类型返回的整数值需要映射到具体框架的缓动曲线。这里是一个简单的映射辅助类示例: + +```cpp +class EasingCurveMapper { +public: + static QEasingCurve fromMotionValue(int value) { + switch (value) { + case 0: return QEasingCurve::Linear; + case 1: return QEasingCurve::InQuad; + case 2: return QEasingCurve::OutQuad; + case 3: return QEasingCurve::InOutQuad; + case 32: return QEasingCurve::InCubic; + case 33: return QEasingCurve::OutCubic; + case 34: return QEasingCurve::InOutCubic; + default: return QEasingCurve::InOutCubic; // fallback + } + } +}; + +// 使用 +auto curve = EasingCurveMapper::fromMotionValue( + motion.queryEasing("md.motion.standard") +); +animation->setEasingCurve(curve); +```text + +## 实现要点 + +实现 `IMotionSpec` 时需要考虑几个细节: + +1. **缓动类型的表示**:接口用 `int` 返回缓动类型,而不是定义自己的枚举。这是为了避免向调用方传播 C++ 类型定义——如果接口要被其他语言调用,整数比枚举更容易处理。实现类可以选择内部用枚举,导出时转换。 + +2. **默认延迟值**:大多数动画不需要延迟,所以 `queryDelay()` 的默认返回值应该是 0。只有在"交错动画"这类特殊场景下,才需要非零延迟。 + +3. **无效 token 处理**:不要抛异常或返回负值。对于持续时间,返回一个合理的默认值(比如 300ms);对于缓动类型,返回一个常用的缓动曲线;对于延迟,返回 0。 + +4. **线程安全**:动画规格查询通常发生在 UI 线程,但如果实现类支持跨线程访问,需要在各方法内部加锁。考虑到这些都是简单的哈希查找,锁的开销可以接受。 + +## 设计决策 + +`IMotionSpec` 把三个参数分开查询,而不是返回一个包含三个字段的结构体。这是为了使用场景的灵活性——很多动画只需要持续时间和缓动,延迟总是 0;而有些场景(比如交错动画)只需要延迟参数。如果打包成结构体,调用方总是要构造或接收一个他们不需要的字段。 + +另一个设计点是,我们用 `int` 表示时间而不是 `std::chrono::duration`。这是因为 Qt 的动画 API 用整数毫秒,用标准库类型反而需要转换。如果将来要支持更高精度的时间单位,可以在实现类内部用 `std::chrono`,导出时转换成毫秒。 + +## 相关文档 + +- [ICFTheme - 主题接口](./theme.md) +- [ICFColorScheme - 颜色方案](./color_scheme.md) +- [IRadiusScale - 圆角规范](./radius_scale.md) +- [IFontType - 字体样式](./font_type.md) diff --git a/document/HandBook/ui/core/radius_scale.md b/document/HandBook/ui/core/radius_scale.md index 3daf3eb1b..d48292e73 100644 --- a/document/HandBook/ui/core/radius_scale.md +++ b/document/HandBook/ui/core/radius_scale.md @@ -1,136 +1,141 @@ -# IRadiusScale - 圆角规范接口 - -`IRadiusScale` 定义了按名称查询圆角值的抽象接口,是 Material Design 3 形状系统的 C++ 映射层。选择用字符串键值而不是枚举来访问圆角,是为了支持动态形状配置和运行时扩展——我们可以在不重新编译的情况下调整圆角方案。 - -## 基本用法 - -通过 `queryRadiusScale()` 查询圆角值: - -```cpp -#include "ui/core/radius_scale.h" - -IRadiusScale& radius = theme.radius_scale(); - -// 查询 Material Design 圆角 token -float small = radius.queryRadiusScale("cornerSmall"); -float medium = radius.queryRadiusScale("cornerMedium"); -float large = radius.queryRadiusScale("cornerLarge"); -float extraLarge = radius.queryRadiusScale("cornerExtraLarge"); -``` - -`queryRadiusScale()` 返回的是浮点数值,单位是密度无关像素(dp)。返回值可以直接用于 Qt 组件的圆角设置: - -```cpp -// 设置按钮圆角 -QPushButton* button = new QPushButton("Click Me"); -float cornerRadius = radius.queryRadiusScale("cornerMedium"); -button->setStyleSheet(QString("border-radius: %1px;").arg(cornerRadius)); -``` - -## Token 命名约定 - -虽然接口本身不规定 token 的命名格式,但我们遵循 Material Design 3 的约定: - -```cpp -// 基础圆角规范 -"cornerNone" // 无圆角(0dp) -"cornerExtraSmall" // 极小圆角(4dp) -"cornerSmall" // 小圆角(8dp) -"cornerMedium" // 中等圆角(12dp) -"cornerLarge" // 大圆角(16dp) -"cornerExtraLarge" // 极大圆角(28dp) -"cornerFull" // 完全圆角(形状变成胶囊或圆形) -``` - -具体的 token 列表由实现类决定,可以在运行时动态扩展。如果查询不存在的 token,接口规定返回 0——这是一个便于判断的 fallback 值,但可能隐藏配置错误。 - -⚠️ 返回 0 不总是最佳行为。如果某个圆角 token 配置缺失,你可能更希望看到一个明显的 fallback 值(比如 8dp),而不是静默地变成无圆角。实现类可以选择在查询失败时记录日志或返回默认值。 - -## 使用示例 - -在实际 UI 组件中使用圆角: - -```cpp -// 自定义圆角矩形按钮 -class RoundedButton : public QPushButton { -private: - float m_cornerRadius; - -public: - RoundedButton(const char* cornerToken, QWidget* parent = nullptr) - : QPushButton(parent) { - auto& radius = getTheme().radius_scale(); - m_cornerRadius = radius.queryRadiusScale(cornerToken); - } - -protected: - void paintEvent(QPaintEvent* event) override { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QPainterPath path; - path.addRoundedRect(rect(), m_cornerRadius, m_cornerRadius); - painter.fillPath(path, palette().button()); - } -}; -``` - -使用时: - -```cpp -// 创建不同圆角风格的按钮 -auto* smallBtn = new RoundedButton("cornerSmall"); // 轻微圆角 -auto* mediumBtn = new RoundedButton("cornerMedium"); // 标准圆角 -auto* largeBtn = new RoundedButton("cornerLarge"); // 大圆角 -auto* pillBtn = new RoundedButton("cornerFull"); // 胶囊形状 -``` - -## 与样式表集成 - -如果使用 Qt 样式表,圆角值可以直接嵌入字符串: - -```cpp -void applyCornerRadius(QWidget* widget, const char* cornerToken) { - auto& radius = getTheme().radius_scale(); - float r = radius.queryRadiusScale(cornerToken); - widget->setStyleSheet(QString( - "QWidget { border-radius: %1px; }" - ).arg(r)); -} -``` - -⚠️ 样式表中的圆角是按像素计算的,而 `queryRadiusScale()` 返回的是 dp 值。在高 DPI 屏幕上,需要手动乘以设备像素比率: - -```cpp -float dpToPx(float dp) { - return dp * QApplication::devicePixelRatio(); -} - -// 正确的高 DPI 处理 -float r = dpToPx(radius.queryRadiusScale("cornerMedium")); -``` - -## 实现要点 - -实现 `IRadiusScale` 时需要考虑几个细节: - -1. **返回 0 的语义**:接口规定未找到 token 时返回 0。这在语义上是合理的——"无圆角"是一个有效的形状选择——但也可能掩盖配置错误。实现类可以选择在查询失败时记录日志。 - -2. **单位处理**:接口返回的是 dp 值,调用方负责根据设备像素比率转换。这是有意为之——让接口保持平台无关,具体的缩放逻辑由调用方控制。 - -3. **cornerFull 的实现**:Material Design 3 的 `cornerFull` 表示"完全圆角",通常用于胶囊按钮或圆形头像。实现类可以选择返回一个足够大的值(比如 9999),让形状看起来是完全圆角,或者返回 `std::numeric_limits::max()`。 - -4. **线程安全**:如果实现类支持跨线程访问,需要在 `queryRadiusScale()` 内部加锁。考虑到圆角查询通常只发生在 UI 初始化阶段,线程安全开销可以接受。 - -## 设计决策 - -`IRadiusScale` 只返回 `float` 而不是 `int`,这是因为圆角值可能需要更精细的粒度——比如 Material Design 3 的某些圆角规范使用非整数值。虽然最终渲染时会四舍五入到像素,但在配置层面保持浮点精度可以避免累积误差。 - -另一个设计点是,我们没有提供批量查询接口。圆角 token 的数量通常很少(不超过 10 个),批量查询的收益不明显。如果确实需要优化,可以在实现类内部提供缓存层,而不是增加接口复杂度。 - -## 相关文档 - -- [ICFTheme - 主题接口](./theme.md) -- [ICFColorScheme - 颜色方案](./color_scheme.md) -- [IMotionSpec - 动画规格](./motion_spec.md) -- [IFontType - 字体样式](./font_type.md) +--- +title: "IRadiusScale - 圆角规范接口" +description: 定义了按名称查询圆角值的抽象接口,是 Material Design 3 形状系统的 C++ 映射层 +--- + +# IRadiusScale - 圆角规范接口 + +`IRadiusScale` 定义了按名称查询圆角值的抽象接口,是 Material Design 3 形状系统的 C++ 映射层。选择用字符串键值而不是枚举来访问圆角,是为了支持动态形状配置和运行时扩展——我们可以在不重新编译的情况下调整圆角方案。 + +## 基本用法 + +通过 `queryRadiusScale()` 查询圆角值: + +```cpp +#include "ui/core/radius_scale.h" + +IRadiusScale& radius = theme.radius_scale(); + +// 查询 Material Design 圆角 token +float small = radius.queryRadiusScale("cornerSmall"); +float medium = radius.queryRadiusScale("cornerMedium"); +float large = radius.queryRadiusScale("cornerLarge"); +float extraLarge = radius.queryRadiusScale("cornerExtraLarge"); +```text + +`queryRadiusScale()` 返回的是浮点数值,单位是密度无关像素(dp)。返回值可以直接用于 Qt 组件的圆角设置: + +```cpp +// 设置按钮圆角 +QPushButton* button = new QPushButton("Click Me"); +float cornerRadius = radius.queryRadiusScale("cornerMedium"); +button->setStyleSheet(QString("border-radius: %1px;").arg(cornerRadius)); +```text + +## Token 命名约定 + +虽然接口本身不规定 token 的命名格式,但我们遵循 Material Design 3 的约定: + +```cpp +// 基础圆角规范 +"cornerNone" // 无圆角(0dp) +"cornerExtraSmall" // 极小圆角(4dp) +"cornerSmall" // 小圆角(8dp) +"cornerMedium" // 中等圆角(12dp) +"cornerLarge" // 大圆角(16dp) +"cornerExtraLarge" // 极大圆角(28dp) +"cornerFull" // 完全圆角(形状变成胶囊或圆形) +```text + +具体的 token 列表由实现类决定,可以在运行时动态扩展。如果查询不存在的 token,接口规定返回 0——这是一个便于判断的 fallback 值,但可能隐藏配置错误。 + +⚠️ 返回 0 不总是最佳行为。如果某个圆角 token 配置缺失,你可能更希望看到一个明显的 fallback 值(比如 8dp),而不是静默地变成无圆角。实现类可以选择在查询失败时记录日志或返回默认值。 + +## 使用示例 + +在实际 UI 组件中使用圆角: + +```cpp +// 自定义圆角矩形按钮 +class RoundedButton : public QPushButton { +private: + float m_cornerRadius; + +public: + RoundedButton(const char* cornerToken, QWidget* parent = nullptr) + : QPushButton(parent) { + auto& radius = getTheme().radius_scale(); + m_cornerRadius = radius.queryRadiusScale(cornerToken); + } + +protected: + void paintEvent(QPaintEvent* event) override { + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + QPainterPath path; + path.addRoundedRect(rect(), m_cornerRadius, m_cornerRadius); + painter.fillPath(path, palette().button()); + } +}; +```text + +使用时: + +```cpp +// 创建不同圆角风格的按钮 +auto* smallBtn = new RoundedButton("cornerSmall"); // 轻微圆角 +auto* mediumBtn = new RoundedButton("cornerMedium"); // 标准圆角 +auto* largeBtn = new RoundedButton("cornerLarge"); // 大圆角 +auto* pillBtn = new RoundedButton("cornerFull"); // 胶囊形状 +```text + +## 与样式表集成 + +如果使用 Qt 样式表,圆角值可以直接嵌入字符串: + +```cpp +void applyCornerRadius(QWidget* widget, const char* cornerToken) { + auto& radius = getTheme().radius_scale(); + float r = radius.queryRadiusScale(cornerToken); + widget->setStyleSheet(QString( + "QWidget { border-radius: %1px; }" + ).arg(r)); +} +```text + +⚠️ 样式表中的圆角是按像素计算的,而 `queryRadiusScale()` 返回的是 dp 值。在高 DPI 屏幕上,需要手动乘以设备像素比率: + +```cpp +float dpToPx(float dp) { + return dp * QApplication::devicePixelRatio(); +} + +// 正确的高 DPI 处理 +float r = dpToPx(radius.queryRadiusScale("cornerMedium")); +```text + +## 实现要点 + +实现 `IRadiusScale` 时需要考虑几个细节: + +1. **返回 0 的语义**:接口规定未找到 token 时返回 0。这在语义上是合理的——"无圆角"是一个有效的形状选择——但也可能掩盖配置错误。实现类可以选择在查询失败时记录日志。 + +2. **单位处理**:接口返回的是 dp 值,调用方负责根据设备像素比率转换。这是有意为之——让接口保持平台无关,具体的缩放逻辑由调用方控制。 + +3. **cornerFull 的实现**:Material Design 3 的 `cornerFull` 表示"完全圆角",通常用于胶囊按钮或圆形头像。实现类可以选择返回一个足够大的值(比如 9999),让形状看起来是完全圆角,或者返回 `std::numeric_limits::max()`。 + +4. **线程安全**:如果实现类支持跨线程访问,需要在 `queryRadiusScale()` 内部加锁。考虑到圆角查询通常只发生在 UI 初始化阶段,线程安全开销可以接受。 + +## 设计决策 + +`IRadiusScale` 只返回 `float` 而不是 `int`,这是因为圆角值可能需要更精细的粒度——比如 Material Design 3 的某些圆角规范使用非整数值。虽然最终渲染时会四舍五入到像素,但在配置层面保持浮点精度可以避免累积误差。 + +另一个设计点是,我们没有提供批量查询接口。圆角 token 的数量通常很少(不超过 10 个),批量查询的收益不明显。如果确实需要优化,可以在实现类内部提供缓存层,而不是增加接口复杂度。 + +## 相关文档 + +- [ICFTheme - 主题接口](./theme.md) +- [ICFColorScheme - 颜色方案](./color_scheme.md) +- [IMotionSpec - 动画规格](./motion_spec.md) +- [IFontType - 字体样式](./font_type.md) diff --git a/document/HandBook/ui/core/theme.md b/document/HandBook/ui/core/theme.md index a2004e79c..4f1bf1ab9 100644 --- a/document/HandBook/ui/core/theme.md +++ b/document/HandBook/ui/core/theme.md @@ -1,82 +1,87 @@ -# ICFTheme - 主题接口 - -`ICFTheme` 是 UI 主题系统的顶层抽象,提供对颜色方案、动画规格、圆角规范和字体样式的统一访问入口。选择把这几个分散的设计维度聚合成一个接口,是因为在实际使用中它们总是作为一个整体被切换——我们很少需要"只换颜色但不换动画"的场景。 - -## 接口结构 - -`ICFTheme` 本身是一个抽象接口,包含四个子组件的访问器: - -```cpp -#include "ui/core/theme.h" - -// 获取主题的各个子组件 -const ICFTheme& theme = getTheme(); // 通过 ThemeFactory 获取 - -auto& colors = theme.color_scheme(); // ICFColorScheme& -auto& motion = theme.motion_spec(); // IMotionSpec& -auto& radius = theme.radius_scale(); // IRadiusScale& -auto& fonts = theme.font_type(); // IFontType& -``` - -所有访问器都返回引用,这是因为子组件的生命周期由 `ICFTheme` 的实现类管理,调用方不需要关心所有权问题。 - -## 获取主题实例 - -`ICFTheme` 的构造函数是 protected 的,不允许直接构造。这是有意为之——强制使用 `ThemeFactory` 来创建主题实例,保证所有主题对象都经过统一的创建流程: - -```cpp -#include "ui/core/theme_factory.h" - -// 通过工厂创建主题 -ThemeFactory* factory = getFactory(); // 获取具体工厂实现 -auto theme = factory->fromName("default"); // std::unique_ptr - -// 从 JSON 创建主题 -QByteArray json = loadThemeJson(); -auto custom_theme = factory->fromJson(json); - -// 序列化主题 -QByteArray serialized = factory->toJson(theme.get()); -``` - -使用工厂模式的好处是可以在运行时动态切换主题实现,比如从文件加载的 JSON 主题和硬编码的默认主题可以共存。 - -## 子组件使用 - -拿到主题对象后,通过它访问各个子组件: - -```cpp -// 获取颜色 -QColor primary = theme.color_scheme().queryColor("md.primary"); -QColor surface = theme.color_scheme().queryColor("md.surface"); - -// 获取动画参数 -int duration = theme.motion_spec().queryDuration("md.motion.shortEnter"); -int easing = theme.motion_spec().queryEasing("md.motion.standard"); - -// 获取圆角值 -float corner = theme.radius_scale().queryRadiusScale("cornerSmall"); - -// 获取字体 -QFont body_font = theme.font_type().queryTargetFont("bodyLarge"); -``` - -注意这里每个子组件都有自己的查询语法——这是为兼容 Material Design 3 的 token 命名设计的。具体的 token 名称由各个子组件的实现决定,`ICFTheme` 不做统一规定。 - -## 设计决策 - -把四个子组件聚合到 `ICFTheme` 里,而不是让使用者分别管理它们,主要是为了简化主题切换逻辑。如果没有这个聚合层,每次切换主题就需要同步替换四个不同的对象,很容易出现不一致的状态——比如颜色方案已经是 dark 模式了,但字体还是 light 模式的。 - -另一个值得注意的设计点是,子组件用 `unique_ptr` 持有而不是直接嵌入。这样做允许实现类在运行时选择具体的子组件实现,而不需要在编译期固定类型。对于支持插件式主题扩展的场景,这个灵活性很重要。 - -## 线程安全 - -`ICFTheme` 接口本身不提供线程安全保证。如果需要在多线程环境下访问同一个主题对象,调用方需要自己加锁。实际使用中,我们通常为每个线程维护独立的主题访问入口,或者通过 TLS 缓存主题查询结果,避免频繁的跨线程访问。 - -## 相关文档 - -- [ICFColorScheme - 颜色方案](./color_scheme.md) -- [IMotionSpec - 动画规格](./motion_spec.md) -- [IRadiusScale - 圆角规范](./radius_scale.md) -- [IFontType - 字体样式](./font_type.md) -- [ThemeFactory - 主题工厂](./theme_factory.md) +--- +title: "ICFTheme - 主题接口" +description: 是 UI 主题系统的顶层抽象,提供对颜色方案、动画规格、圆角规范和字体样式的统一访问入口。选择把这几 +--- + +# ICFTheme - 主题接口 + +`ICFTheme` 是 UI 主题系统的顶层抽象,提供对颜色方案、动画规格、圆角规范和字体样式的统一访问入口。选择把这几个分散的设计维度聚合成一个接口,是因为在实际使用中它们总是作为一个整体被切换——我们很少需要"只换颜色但不换动画"的场景。 + +## 接口结构 + +`ICFTheme` 本身是一个抽象接口,包含四个子组件的访问器: + +```cpp +#include "ui/core/theme.h" + +// 获取主题的各个子组件 +const ICFTheme& theme = getTheme(); // 通过 ThemeFactory 获取 + +auto& colors = theme.color_scheme(); // ICFColorScheme& +auto& motion = theme.motion_spec(); // IMotionSpec& +auto& radius = theme.radius_scale(); // IRadiusScale& +auto& fonts = theme.font_type(); // IFontType& +```text + +所有访问器都返回引用,这是因为子组件的生命周期由 `ICFTheme` 的实现类管理,调用方不需要关心所有权问题。 + +## 获取主题实例 + +`ICFTheme` 的构造函数是 protected 的,不允许直接构造。这是有意为之——强制使用 `ThemeFactory` 来创建主题实例,保证所有主题对象都经过统一的创建流程: + +```cpp +#include "ui/core/theme_factory.h" + +// 通过工厂创建主题 +ThemeFactory* factory = getFactory(); // 获取具体工厂实现 +auto theme = factory->fromName("default"); // std::unique_ptr + +// 从 JSON 创建主题 +QByteArray json = loadThemeJson(); +auto custom_theme = factory->fromJson(json); + +// 序列化主题 +QByteArray serialized = factory->toJson(theme.get()); +```text + +使用工厂模式的好处是可以在运行时动态切换主题实现,比如从文件加载的 JSON 主题和硬编码的默认主题可以共存。 + +## 子组件使用 + +拿到主题对象后,通过它访问各个子组件: + +```cpp +// 获取颜色 +QColor primary = theme.color_scheme().queryColor("md.primary"); +QColor surface = theme.color_scheme().queryColor("md.surface"); + +// 获取动画参数 +int duration = theme.motion_spec().queryDuration("md.motion.shortEnter"); +int easing = theme.motion_spec().queryEasing("md.motion.standard"); + +// 获取圆角值 +float corner = theme.radius_scale().queryRadiusScale("cornerSmall"); + +// 获取字体 +QFont body_font = theme.font_type().queryTargetFont("bodyLarge"); +```text + +注意这里每个子组件都有自己的查询语法——这是为兼容 Material Design 3 的 token 命名设计的。具体的 token 名称由各个子组件的实现决定,`ICFTheme` 不做统一规定。 + +## 设计决策 + +把四个子组件聚合到 `ICFTheme` 里,而不是让使用者分别管理它们,主要是为了简化主题切换逻辑。如果没有这个聚合层,每次切换主题就需要同步替换四个不同的对象,很容易出现不一致的状态——比如颜色方案已经是 dark 模式了,但字体还是 light 模式的。 + +另一个值得注意的设计点是,子组件用 `unique_ptr` 持有而不是直接嵌入。这样做允许实现类在运行时选择具体的子组件实现,而不需要在编译期固定类型。对于支持插件式主题扩展的场景,这个灵活性很重要。 + +## 线程安全 + +`ICFTheme` 接口本身不提供线程安全保证。如果需要在多线程环境下访问同一个主题对象,调用方需要自己加锁。实际使用中,我们通常为每个线程维护独立的主题访问入口,或者通过 TLS 缓存主题查询结果,避免频繁的跨线程访问。 + +## 相关文档 + +- [ICFColorScheme - 颜色方案](./color_scheme.md) +- [IMotionSpec - 动画规格](./motion_spec.md) +- [IRadiusScale - 圆角规范](./radius_scale.md) +- [IFontType - 字体样式](./font_type.md) +- [ThemeFactory - 主题工厂](./theme_factory.md) diff --git a/document/HandBook/ui/core/theme_factory.md b/document/HandBook/ui/core/theme_factory.md index 545ffb9d3..793280c07 100644 --- a/document/HandBook/ui/core/theme_factory.md +++ b/document/HandBook/ui/core/theme_factory.md @@ -1,258 +1,263 @@ -# ThemeFactory - 主题工厂 - -`ThemeFactory` 是创建主题实例的抽象接口,负责从名字或 JSON 数据构造 `ICFTheme` 对象,以及将现有主题序列化为 JSON。为什么需要工厂而不是直接 `new` 一个主题?因为主题的创建过程往往比较复杂——需要组合颜色方案、动画规格、圆角尺度等多个组件,工厂模式把这个创建逻辑封装起来,也让加载配置文件成为可能。 - -## 工厂接口 - -工厂是一个纯虚基类,你需要继承它并实现三个方法: - -```cpp -#include "ui/core/theme_factory.h" - -class MyThemeFactory : public cf::ui::core::ThemeFactory { -public: - // 从名字创建主题 - std::unique_ptr fromName(const char* name) override; - - // 从 JSON 创建主题 - std::unique_ptr fromJson(const QByteArray& json) override; - - // 将主题序列化为 JSON - QByteArray toJson(cf::ui::core::ICFTheme* raw_theme) override; -}; -``` - -注意返回类型是 `std::unique_ptr`,调用者获得主题的所有权。这个设计很自然——工厂负责生产,消费者负责销毁。 - -## 从名字创建 - -`fromName` 是最常见的创建方式,用于内置主题或预定义的主题变体: - -```cpp -std::unique_ptr MyThemeFactory::fromName(const char* name) { - auto theme = std::make_unique(); - - // 根据名字配置不同的组件 - if (strcmp(name, "default") == 0) { - theme->color_scheme_ = std::make_unique(); - theme->motion_spec_ = std::make_unique(); - theme->radius_scale_ = std::make_unique(); - theme->font_type_ = std::make_unique(); - } else if (strcmp(name, "compact") == 0) { - theme->color_scheme_ = std::make_unique(); - theme->motion_spec_ = std::make_unique(); // 更快的动画 - theme->radius_scale_ = std::make_unique(); // 更小的圆角 - theme->font_type_ = std::make_unique(); - } else { - return nullptr; // 未知主题返回空指针 - } - - return theme; -} -``` - -`name` 参数来自 `ThemeManager::insert_one` 时注册的名字,但工厂内部可以有自己的命名空间逻辑——你可以用同一个工厂支持多个主题变体,或者完全忽略名字只返回固定配置。 - -⚠️ 如果无法创建主题(比如名字不认识),返回 `nullptr` 即可,不要抛异常。`ThemeManager` 会检测空指针并优雅处理。 - -## 从 JSON 创建 - -`fromJson` 让主题可以从配置文件加载,这是实现用户自定义主题的基础: - -```cpp -std::unique_ptr MyThemeFactory::fromJson(const QByteArray& json) { - auto theme = std::make_unique(); - - // 使用 QJsonDocument 解析 - QJsonDocument doc = QJsonDocument::fromJson(json); - if (doc.isNull() || !doc.isObject()) { - return nullptr; - } - - QJsonObject root = doc.object(); - - // 解析颜色方案 - QJsonObject colors = root["colors"].toObject(); - theme->color_scheme_ = std::make_unique( - QColor(colors["background"].toString()), - QColor(colors["foreground"].toString()) - ); - - // 解析动画规格 - QJsonObject motion = root["motion"].toObject(); - theme->motion_spec_ = std::make_unique( - motion["duration"].toInt(200), - motion["easing"].toString("ease").toStdString() - ); - - // ... 解析其他组件 - - return theme; -} -``` - -JSON 格式完全由你定义,只要工厂能解析就行。我们建议遵循一定的约定,比如 `colors` 对象包含颜色、`motion` 对象包含动画参数,这样不同主题的配置文件可以保持一致性。 - -## 序列化到 JSON - -`toJson` 是 `fromJson` 的逆操作,用于导出当前主题配置: - -```cpp -QByteArray MyThemeFactory::toJson(cf::ui::core::ICFTheme* raw_theme) { - if (!raw_theme) { - return QByteArray(); - } - - QJsonObject root; - - // 序列化颜色方案 - const auto& colors = raw_theme->color_scheme(); - QJsonObject colorObj; - colorObj["background"] = colors.background().name(); - colorObj["foreground"] = colors.foreground().name(); - root["colors"] = colorObj; - - // 序列化动画规格 - const auto& motion = raw_theme->motion_spec(); - QJsonObject motionObj; - motionObj["duration"] = motion.default_duration(); - root["motion"] = motionObj; - - // ... 序列化其他组件 - - return QJsonDocument(root).toJson(QJsonDocument::Compact); -} -``` - -`raw_theme` 是裸指针而不是引用,这是为了兼容 Qt 的信号槽参数传递习惯。但在实际使用中,你传进来的应该总是一个有效的主题指针。 - -⚠️ 序列化时可能会丢失信息——如果你的 `ICFTheme` 实现包含一些无法简单表示的组件(比如引用了外部资源),`fromJson` 和 `toJson` 不必是完全可逆的。只要能保存"用户可配置"的那部分就够了。 - -## 注册到管理器 - -工厂本身不做任何事,需要注册到 `ThemeManager` 才能发挥作用: - -```cpp -// 注册内置主题 -cf::ui::core::ThemeManager::instance().insert_one("builtin", []() { - return std::make_unique(); -}); - -// 注册支持 JSON 的主题工厂 -cf::ui::core::ThemeManager::instance().insert_one("custom", []() { - return std::make_unique(); -}); -``` - -注意这里用的是 lambda 而不是直接传工厂实例。`ThemeManager` 会在需要时调用 lambda 来创建工厂,而不是在注册时就创建。这个延迟创建机制可以减少启动时的开销,特别是当你注册了很多插件主题但大部分都不会被使用时。 - -## 错误处理 - -工厂方法的错误处理建议遵循以下原则: - -```cpp -std::unique_ptr MyThemeFactory::fromJson(const QByteArray& json) { - // 1. 先检查基本有效性 - if (json.isEmpty()) { - qWarning() << "Empty JSON data"; - return nullptr; - } - - // 2. 解析失败返回 nullptr,不要抛异常 - QJsonDocument doc = QJsonDocument::fromJson(json); - if (doc.isNull()) { - qWarning() << "Invalid JSON format"; - return nullptr; - } - - // 3. 缺少必要字段可以回退到默认值 - QJsonObject root = doc.object(); - int duration = root.value("duration").toInt(200); // 默认 200ms - - // 4. 只在真正无法恢复时返回 nullptr - if (!root.contains("colors")) { - qWarning() << "Missing required 'colors' field"; - return nullptr; - } - - // ... 构造主题 -} -``` - -返回 `nullptr` 表示"无法创建",`ThemeManager` 会把这个情况传达给调用方。我们不建议在工厂方法里抛异常,因为 Qt 的信号槽机制和异常配合得不太好,而且这也让错误处理逻辑更清晰。 - -## 实现示例 - -这是一个完整的工厂实现,支持名字创建和 JSON 序列化: - -```cpp -class ModernThemeFactory : public cf::ui::core::ThemeFactory { -public: - std::unique_ptr fromName(const char* name) override { - auto theme = std::make_unique(); - - // 现代风格使用鲜艳的accent色和更快的动画 - theme->color_scheme_ = std::make_unique(); - theme->motion_spec_ = std::make_unique(); - theme->radius_scale_ = std::make_unique(); - theme->font_type_ = std::make_unique(); - - return theme; - } - - std::unique_ptr fromJson(const QByteArray& json) override { - QJsonDocument doc = QJsonDocument::fromJson(json); - if (doc.isNull() || !doc.isObject()) { - return nullptr; - } - - QJsonObject root = doc.object(); - auto theme = std::make_unique(); - - // 支持自定义 accent 颜色 - if (root.contains("accent_color")) { - QColor accent(root["accent_color"].toString()); - theme->color_scheme_ = std::make_unique(accent); - } else { - theme->color_scheme_ = std::make_unique(); - } - - // 支持自定义动画速度 - int duration = root.value("animation_duration").toInt(150); - theme->motion_spec_ = std::make_unique(duration); - - // 其他组件使用默认值 - theme->radius_scale_ = std::make_unique(); - theme->font_type_ = std::make_unique(); - - return theme; - } - - QByteArray toJson(cf::ui::core::ICFTheme* raw_theme) override { - if (!raw_theme) { - return QByteArray(); - } - - QJsonObject root; - - // 导出关键配置,供后续 fromJson 恢复 - const auto& colors = raw_theme->color_scheme(); - root["accent_color"] = colors.accent().name(); - - const auto& motion = raw_theme->motion_spec(); - root["animation_duration"] = motion.default_duration(); - - // 添加元数据 - root["theme_name"] = "modern"; - root["version"] = "1.0"; - - return QJsonDocument(root).toJson(); - } -}; -``` - -## 相关文档 - -- [ThemeManager - 主题管理器](./theme_manager.md) -- [ICFTheme - 主题接口](./theme.md) -- [UI 核心组件概述](./index.md) +--- +title: "ThemeFactory - 主题工厂" +description: 是创建主题实例的抽象接口,负责从名字或 JSON 数据构造 对象,以及将现有主题序列化为 JSON +--- + +# ThemeFactory - 主题工厂 + +`ThemeFactory` 是创建主题实例的抽象接口,负责从名字或 JSON 数据构造 `ICFTheme` 对象,以及将现有主题序列化为 JSON。为什么需要工厂而不是直接 `new` 一个主题?因为主题的创建过程往往比较复杂——需要组合颜色方案、动画规格、圆角尺度等多个组件,工厂模式把这个创建逻辑封装起来,也让加载配置文件成为可能。 + +## 工厂接口 + +工厂是一个纯虚基类,你需要继承它并实现三个方法: + +```cpp +#include "ui/core/theme_factory.h" + +class MyThemeFactory : public cf::ui::core::ThemeFactory { +public: + // 从名字创建主题 + std::unique_ptr fromName(const char* name) override; + + // 从 JSON 创建主题 + std::unique_ptr fromJson(const QByteArray& json) override; + + // 将主题序列化为 JSON + QByteArray toJson(cf::ui::core::ICFTheme* raw_theme) override; +}; +```text + +注意返回类型是 `std::unique_ptr`,调用者获得主题的所有权。这个设计很自然——工厂负责生产,消费者负责销毁。 + +## 从名字创建 + +`fromName` 是最常见的创建方式,用于内置主题或预定义的主题变体: + +```cpp +std::unique_ptr MyThemeFactory::fromName(const char* name) { + auto theme = std::make_unique(); + + // 根据名字配置不同的组件 + if (strcmp(name, "default") == 0) { + theme->color_scheme_ = std::make_unique(); + theme->motion_spec_ = std::make_unique(); + theme->radius_scale_ = std::make_unique(); + theme->font_type_ = std::make_unique(); + } else if (strcmp(name, "compact") == 0) { + theme->color_scheme_ = std::make_unique(); + theme->motion_spec_ = std::make_unique(); // 更快的动画 + theme->radius_scale_ = std::make_unique(); // 更小的圆角 + theme->font_type_ = std::make_unique(); + } else { + return nullptr; // 未知主题返回空指针 + } + + return theme; +} +```text + +`name` 参数来自 `ThemeManager::insert_one` 时注册的名字,但工厂内部可以有自己的命名空间逻辑——你可以用同一个工厂支持多个主题变体,或者完全忽略名字只返回固定配置。 + +⚠️ 如果无法创建主题(比如名字不认识),返回 `nullptr` 即可,不要抛异常。`ThemeManager` 会检测空指针并优雅处理。 + +## 从 JSON 创建 + +`fromJson` 让主题可以从配置文件加载,这是实现用户自定义主题的基础: + +```cpp +std::unique_ptr MyThemeFactory::fromJson(const QByteArray& json) { + auto theme = std::make_unique(); + + // 使用 QJsonDocument 解析 + QJsonDocument doc = QJsonDocument::fromJson(json); + if (doc.isNull() || !doc.isObject()) { + return nullptr; + } + + QJsonObject root = doc.object(); + + // 解析颜色方案 + QJsonObject colors = root["colors"].toObject(); + theme->color_scheme_ = std::make_unique( + QColor(colors["background"].toString()), + QColor(colors["foreground"].toString()) + ); + + // 解析动画规格 + QJsonObject motion = root["motion"].toObject(); + theme->motion_spec_ = std::make_unique( + motion["duration"].toInt(200), + motion["easing"].toString("ease").toStdString() + ); + + // ... 解析其他组件 + + return theme; +} +```text + +JSON 格式完全由你定义,只要工厂能解析就行。我们建议遵循一定的约定,比如 `colors` 对象包含颜色、`motion` 对象包含动画参数,这样不同主题的配置文件可以保持一致性。 + +## 序列化到 JSON + +`toJson` 是 `fromJson` 的逆操作,用于导出当前主题配置: + +```cpp +QByteArray MyThemeFactory::toJson(cf::ui::core::ICFTheme* raw_theme) { + if (!raw_theme) { + return QByteArray(); + } + + QJsonObject root; + + // 序列化颜色方案 + const auto& colors = raw_theme->color_scheme(); + QJsonObject colorObj; + colorObj["background"] = colors.background().name(); + colorObj["foreground"] = colors.foreground().name(); + root["colors"] = colorObj; + + // 序列化动画规格 + const auto& motion = raw_theme->motion_spec(); + QJsonObject motionObj; + motionObj["duration"] = motion.default_duration(); + root["motion"] = motionObj; + + // ... 序列化其他组件 + + return QJsonDocument(root).toJson(QJsonDocument::Compact); +} +```text + +`raw_theme` 是裸指针而不是引用,这是为了兼容 Qt 的信号槽参数传递习惯。但在实际使用中,你传进来的应该总是一个有效的主题指针。 + +⚠️ 序列化时可能会丢失信息——如果你的 `ICFTheme` 实现包含一些无法简单表示的组件(比如引用了外部资源),`fromJson` 和 `toJson` 不必是完全可逆的。只要能保存"用户可配置"的那部分就够了。 + +## 注册到管理器 + +工厂本身不做任何事,需要注册到 `ThemeManager` 才能发挥作用: + +```cpp +// 注册内置主题 +cf::ui::core::ThemeManager::instance().insert_one("builtin", []() { + return std::make_unique(); +}); + +// 注册支持 JSON 的主题工厂 +cf::ui::core::ThemeManager::instance().insert_one("custom", []() { + return std::make_unique(); +}); +```text + +注意这里用的是 lambda 而不是直接传工厂实例。`ThemeManager` 会在需要时调用 lambda 来创建工厂,而不是在注册时就创建。这个延迟创建机制可以减少启动时的开销,特别是当你注册了很多插件主题但大部分都不会被使用时。 + +## 错误处理 + +工厂方法的错误处理建议遵循以下原则: + +```cpp +std::unique_ptr MyThemeFactory::fromJson(const QByteArray& json) { + // 1. 先检查基本有效性 + if (json.isEmpty()) { + qWarning() << "Empty JSON data"; + return nullptr; + } + + // 2. 解析失败返回 nullptr,不要抛异常 + QJsonDocument doc = QJsonDocument::fromJson(json); + if (doc.isNull()) { + qWarning() << "Invalid JSON format"; + return nullptr; + } + + // 3. 缺少必要字段可以回退到默认值 + QJsonObject root = doc.object(); + int duration = root.value("duration").toInt(200); // 默认 200ms + + // 4. 只在真正无法恢复时返回 nullptr + if (!root.contains("colors")) { + qWarning() << "Missing required 'colors' field"; + return nullptr; + } + + // ... 构造主题 +} +```text + +返回 `nullptr` 表示"无法创建",`ThemeManager` 会把这个情况传达给调用方。我们不建议在工厂方法里抛异常,因为 Qt 的信号槽机制和异常配合得不太好,而且这也让错误处理逻辑更清晰。 + +## 实现示例 + +这是一个完整的工厂实现,支持名字创建和 JSON 序列化: + +```cpp +class ModernThemeFactory : public cf::ui::core::ThemeFactory { +public: + std::unique_ptr fromName(const char* name) override { + auto theme = std::make_unique(); + + // 现代风格使用鲜艳的accent色和更快的动画 + theme->color_scheme_ = std::make_unique(); + theme->motion_spec_ = std::make_unique(); + theme->radius_scale_ = std::make_unique(); + theme->font_type_ = std::make_unique(); + + return theme; + } + + std::unique_ptr fromJson(const QByteArray& json) override { + QJsonDocument doc = QJsonDocument::fromJson(json); + if (doc.isNull() || !doc.isObject()) { + return nullptr; + } + + QJsonObject root = doc.object(); + auto theme = std::make_unique(); + + // 支持自定义 accent 颜色 + if (root.contains("accent_color")) { + QColor accent(root["accent_color"].toString()); + theme->color_scheme_ = std::make_unique(accent); + } else { + theme->color_scheme_ = std::make_unique(); + } + + // 支持自定义动画速度 + int duration = root.value("animation_duration").toInt(150); + theme->motion_spec_ = std::make_unique(duration); + + // 其他组件使用默认值 + theme->radius_scale_ = std::make_unique(); + theme->font_type_ = std::make_unique(); + + return theme; + } + + QByteArray toJson(cf::ui::core::ICFTheme* raw_theme) override { + if (!raw_theme) { + return QByteArray(); + } + + QJsonObject root; + + // 导出关键配置,供后续 fromJson 恢复 + const auto& colors = raw_theme->color_scheme(); + root["accent_color"] = colors.accent().name(); + + const auto& motion = raw_theme->motion_spec(); + root["animation_duration"] = motion.default_duration(); + + // 添加元数据 + root["theme_name"] = "modern"; + root["version"] = "1.0"; + + return QJsonDocument(root).toJson(); + } +}; +```text + +## 相关文档 + +- [ThemeManager - 主题管理器](./theme_manager.md) +- [ICFTheme - 主题接口](./theme.md) +- [UI 核心组件概述](./index.md) diff --git a/document/HandBook/ui/core/theme_manager.md b/document/HandBook/ui/core/theme_manager.md index a2c7b5321..1e214886f 100644 --- a/document/HandBook/ui/core/theme_manager.md +++ b/document/HandBook/ui/core/theme_manager.md @@ -1,187 +1,192 @@ -# ThemeManager - 主题管理器 - -`ThemeManager` 是整个 UI 主题系统的入口,负责注册主题工厂、管理主题实例的生命周期,以及向已注册的 widget 分发主题变更事件。采用单例模式是因为整个应用程序只需要一个全局的主题管理点,多处实例化反而会造成状态同步的混乱。 - -## 获取单例 - -获取管理器实例的方式很直接,返回的是引用而不是指针——这样能避免空指针检查,同时也符合"全局唯一实例"的语义: - -```cpp -#include "ui/core/theme_manager.h" - -auto& manager = cf::ui::core::ThemeManager::instance(); -``` - -单例的初始化是线程安全的(C++11 魔法静态变量保证),不用担心多线程竞态。但我们只在 UI 线程里使用它,Qt 的信号机制本身就要求如此。 - -## 注册主题工厂 - -主题不是直接创建的,而是通过工厂模式。在使用前需要先注册工厂,名字是后续获取主题的唯一标识: - -```cpp -// 注册一个内置主题 -manager.insert_one("default", []() { - return std::make_unique(); -}); - -// 注册自定义主题 -manager.insert_one("dark_blue", []() { - return std::make_unique(); -}); -``` - -`insert_one` 返回 `bool`,如果名字已经存在会返回 `false`。这个设计挺实用,可以用来检测插件冲突——同一主题被两个插件注册时,至少有一个会失败。 - -工厂函数是延迟调用的,注册时只是把工厂创建器存起来,真正创建主题实例是在第一次访问时。这意味着你可以提前注册所有可用主题,而不用担心那些可能永远不会被用到的主题占用资源。 - -## 切换主题 - -切换主题只需要调用 `setThemeTo`,管理器会通知所有已注册的 widget: - -```cpp -// 切换到 dark 主题 -manager.setThemeTo("dark_blue"); - -// 切换时可以选择不广播(特殊场景下使用) -manager.setThemeTo("default", false); -``` - -默认情况下,切换主题会广播 `themeChanged` 信号到所有通过 `install_widget` 注册的 widget。`doBroadcast` 参数设为 `false` 会跳过广播,这主要用于批量操作时避免重复通知——比如你要连续切换三个主题,中间的两次就不需要广播。 - -⚠️ 传入的名字必须已经通过 `insert_one` 注册过,否则行为是未定义的。我们当时考虑过抛异常,但 Qt 的信号槽机制和异常配合得不太好,所以选择了静默失败。如果你需要检测错误,建议在调用前先确认主题已注册。 - -## Widget 注册与通知 - -widget 需要主动注册才能收到主题变更通知: - -```cpp -class MyWidget : public QWidget { -public: - MyWidget(QWidget* parent = nullptr) : QWidget(parent) { - // 注册到主题管理器 - cf::ui::core::ThemeManager::instance().install_widget(this); - - // 连接主题变更信号 - connect(&cf::ui::core::ThemeManager::instance(), - &cf::ui::core::ThemeManager::themeChanged, - this, &MyWidget::onThemeChanged); - } - - ~MyWidget() { - // 记得取消注册 - cf::ui::core::ThemeManager::instance().remove_widget(this); - } - -private slots: - void onThemeChanged(const cf::ui::core::ICFTheme& new_theme) { - // 应用新主题 - const auto& colors = new_theme.color_scheme(); - setStyleSheet(QString("background-color: %1;") - .arg(colors.background().name())); - } -}; -``` - -管理器持有的是 `QWidget` 的裸指针观察者,不负责生命周期。widget 析构时必须调用 `remove_widget`,否则管理器里会留下悬空指针。这个设计有点不安全,但 Qt 的对象树机制使得在 widget 析构时自动清理变得复杂,我们选择了简单粗暴的显式管理方式。 - -## 获取主题实例 - -有时候你不需要切换主题,只是想读取当前或某个主题的配置: - -```cpp -// 获取当前主题名称 -const std::string& current = manager.currentThemeName(); - -// 获取特定主题的引用(延迟创建) -const cf::ui::core::ICFTheme& theme = manager.theme("default"); - -// 读取颜色配置 -const auto& colors = theme.color_scheme(); -QColor bg = colors.background(); -``` - -`theme()` 方法返回的是常量引用,主题实例会被缓存起来,后续访问直接从缓存取。第一次访问时才会调用工厂创建,所以第一次可能会有轻微延迟。 - -## 移除主题 - -如果一个主题不再需要,可以动态移除: - -```cpp -manager.remove_one("old_theme"); -``` - -移除操作不会影响当前正在使用的主题——即使你移除的是当前主题,管理器里仍然保留着它的实例,直到切换到另一个主题。这个行为可能是 feature 也可能是 bug,取决于你从哪个角度看,但它确实避免了一些诡妙的崩溃。 - -## 线程安全 - -管理器的内部状态(工厂注册、widget 集合、主题缓存)没有加锁保护。这是有意为之——我们假设所有操作都在 UI 线程上进行,而 Qt 的信号槽机制本身就要求如此。 - -如果你确实需要在其他线程里访问主题数据,可以: -1. 在后台线程里只读取主题配置(ICFTheme 本身是线程安全的) -2. 通过 `QMetaObject::invokeMethod` 把 UI 操作转发到主线程 - -⚠️ 不要在多线程环境下同时调用 `insert_one`、`remove_one` 或 `setThemeTo`,一定会翻车。 - -## 完整示例 - -这是一个完整的使用流程,展示了从注册到切换的完整链条: - -```cpp -// 1. 定义工厂(实际项目里通常在单独文件中) -class LightThemeFactory : public cf::ui::core::ThemeFactory { -public: - std::unique_ptr fromName(const char* name) override { - auto theme = std::make_unique(); - theme->color_scheme_ = std::make_unique(); - theme->radius_scale_ = std::make_unique(); - // ... 其他组件 - return theme; - } - - std::unique_ptr fromJson(const QByteArray& json) override { - // 解析 JSON 并创建主题 - return nullptr; - } - - QByteArray toJson(cf::ui::core::ICFTheme* raw_theme) override { - // 序列化主题到 JSON - return QByteArray(); - } -}; - -// 2. 应用初始化时注册所有主题 -void App::initThemes() { - auto& manager = cf::ui::core::ThemeManager::instance(); - - manager.insert_one("light", []() { - return std::make_unique(); - }); - - manager.insert_one("dark", []() { - return std::make_unique(); - }); -} - -// 3. Widget 连接主题变更 -void MyWidget::setupThemeConnection() { - auto& manager = cf::ui::core::ThemeManager::instance(); - manager.install_widget(this); - - connect(&manager, &cf::ui::core::ThemeManager::themeChanged, - this, &MyWidget::applyTheme); - - // 立即应用当前主题 - applyTheme(manager.theme(manager.currentThemeName())); -} - -// 4. 用户切换主题时 -void SettingsDialog::onThemeSelected(const QString& name) { - cf::ui::core::ThemeManager::instance().setThemeTo(name.toStdString()); -} -``` - -## 相关文档 - -- [ThemeFactory - 主题工厂](./theme_factory.md) -- [ICFTheme - 主题接口](./theme.md) -- [UI 核心组件概述](./index.md) +--- +title: "ThemeManager - 主题管理器" +description: 是整个 UI 主题系统的入口,负责注册主题工厂、管理主题实例的生命周期,以及向已注册的 widget +--- + +# ThemeManager - 主题管理器 + +`ThemeManager` 是整个 UI 主题系统的入口,负责注册主题工厂、管理主题实例的生命周期,以及向已注册的 widget 分发主题变更事件。采用单例模式是因为整个应用程序只需要一个全局的主题管理点,多处实例化反而会造成状态同步的混乱。 + +## 获取单例 + +获取管理器实例的方式很直接,返回的是引用而不是指针——这样能避免空指针检查,同时也符合"全局唯一实例"的语义: + +```cpp +#include "ui/core/theme_manager.h" + +auto& manager = cf::ui::core::ThemeManager::instance(); +```text + +单例的初始化是线程安全的(C++11 魔法静态变量保证),不用担心多线程竞态。但我们只在 UI 线程里使用它,Qt 的信号机制本身就要求如此。 + +## 注册主题工厂 + +主题不是直接创建的,而是通过工厂模式。在使用前需要先注册工厂,名字是后续获取主题的唯一标识: + +```cpp +// 注册一个内置主题 +manager.insert_one("default", []() { + return std::make_unique(); +}); + +// 注册自定义主题 +manager.insert_one("dark_blue", []() { + return std::make_unique(); +}); +```text + +`insert_one` 返回 `bool`,如果名字已经存在会返回 `false`。这个设计挺实用,可以用来检测插件冲突——同一主题被两个插件注册时,至少有一个会失败。 + +工厂函数是延迟调用的,注册时只是把工厂创建器存起来,真正创建主题实例是在第一次访问时。这意味着你可以提前注册所有可用主题,而不用担心那些可能永远不会被用到的主题占用资源。 + +## 切换主题 + +切换主题只需要调用 `setThemeTo`,管理器会通知所有已注册的 widget: + +```cpp +// 切换到 dark 主题 +manager.setThemeTo("dark_blue"); + +// 切换时可以选择不广播(特殊场景下使用) +manager.setThemeTo("default", false); +```text + +默认情况下,切换主题会广播 `themeChanged` 信号到所有通过 `install_widget` 注册的 widget。`doBroadcast` 参数设为 `false` 会跳过广播,这主要用于批量操作时避免重复通知——比如你要连续切换三个主题,中间的两次就不需要广播。 + +⚠️ 传入的名字必须已经通过 `insert_one` 注册过,否则行为是未定义的。我们当时考虑过抛异常,但 Qt 的信号槽机制和异常配合得不太好,所以选择了静默失败。如果你需要检测错误,建议在调用前先确认主题已注册。 + +## Widget 注册与通知 + +widget 需要主动注册才能收到主题变更通知: + +```cpp +class MyWidget : public QWidget { +public: + MyWidget(QWidget* parent = nullptr) : QWidget(parent) { + // 注册到主题管理器 + cf::ui::core::ThemeManager::instance().install_widget(this); + + // 连接主题变更信号 + connect(&cf::ui::core::ThemeManager::instance(), + &cf::ui::core::ThemeManager::themeChanged, + this, &MyWidget::onThemeChanged); + } + + ~MyWidget() { + // 记得取消注册 + cf::ui::core::ThemeManager::instance().remove_widget(this); + } + +private slots: + void onThemeChanged(const cf::ui::core::ICFTheme& new_theme) { + // 应用新主题 + const auto& colors = new_theme.color_scheme(); + setStyleSheet(QString("background-color: %1;") + .arg(colors.background().name())); + } +}; +```text + +管理器持有的是 `QWidget` 的裸指针观察者,不负责生命周期。widget 析构时必须调用 `remove_widget`,否则管理器里会留下悬空指针。这个设计有点不安全,但 Qt 的对象树机制使得在 widget 析构时自动清理变得复杂,我们选择了简单粗暴的显式管理方式。 + +## 获取主题实例 + +有时候你不需要切换主题,只是想读取当前或某个主题的配置: + +```cpp +// 获取当前主题名称 +const std::string& current = manager.currentThemeName(); + +// 获取特定主题的引用(延迟创建) +const cf::ui::core::ICFTheme& theme = manager.theme("default"); + +// 读取颜色配置 +const auto& colors = theme.color_scheme(); +QColor bg = colors.background(); +```text + +`theme()` 方法返回的是常量引用,主题实例会被缓存起来,后续访问直接从缓存取。第一次访问时才会调用工厂创建,所以第一次可能会有轻微延迟。 + +## 移除主题 + +如果一个主题不再需要,可以动态移除: + +```cpp +manager.remove_one("old_theme"); +```text + +移除操作不会影响当前正在使用的主题——即使你移除的是当前主题,管理器里仍然保留着它的实例,直到切换到另一个主题。这个行为可能是 feature 也可能是 bug,取决于你从哪个角度看,但它确实避免了一些诡妙的崩溃。 + +## 线程安全 + +管理器的内部状态(工厂注册、widget 集合、主题缓存)没有加锁保护。这是有意为之——我们假设所有操作都在 UI 线程上进行,而 Qt 的信号槽机制本身就要求如此。 + +如果你确实需要在其他线程里访问主题数据,可以: +1. 在后台线程里只读取主题配置(ICFTheme 本身是线程安全的) +2. 通过 `QMetaObject::invokeMethod` 把 UI 操作转发到主线程 + +⚠️ 不要在多线程环境下同时调用 `insert_one`、`remove_one` 或 `setThemeTo`,一定会翻车。 + +## 完整示例 + +这是一个完整的使用流程,展示了从注册到切换的完整链条: + +```cpp +// 1. 定义工厂(实际项目里通常在单独文件中) +class LightThemeFactory : public cf::ui::core::ThemeFactory { +public: + std::unique_ptr fromName(const char* name) override { + auto theme = std::make_unique(); + theme->color_scheme_ = std::make_unique(); + theme->radius_scale_ = std::make_unique(); + // ... 其他组件 + return theme; + } + + std::unique_ptr fromJson(const QByteArray& json) override { + // 解析 JSON 并创建主题 + return nullptr; + } + + QByteArray toJson(cf::ui::core::ICFTheme* raw_theme) override { + // 序列化主题到 JSON + return QByteArray(); + } +}; + +// 2. 应用初始化时注册所有主题 +void App::initThemes() { + auto& manager = cf::ui::core::ThemeManager::instance(); + + manager.insert_one("light", []() { + return std::make_unique(); + }); + + manager.insert_one("dark", []() { + return std::make_unique(); + }); +} + +// 3. Widget 连接主题变更 +void MyWidget::setupThemeConnection() { + auto& manager = cf::ui::core::ThemeManager::instance(); + manager.install_widget(this); + + connect(&manager, &cf::ui::core::ThemeManager::themeChanged, + this, &MyWidget::applyTheme); + + // 立即应用当前主题 + applyTheme(manager.theme(manager.currentThemeName())); +} + +// 4. 用户切换主题时 +void SettingsDialog::onThemeSelected(const QString& name) { + cf::ui::core::ThemeManager::instance().setThemeTo(name.toStdString()); +} +```text + +## 相关文档 + +- [ThemeFactory - 主题工厂](./theme_factory.md) +- [ICFTheme - 主题接口](./theme.md) +- [UI 核心组件概述](./index.md) diff --git a/document/HandBook/ui/core/token/.pages b/document/HandBook/ui/core/token/.pages deleted file mode 100644 index c327575da..000000000 --- a/document/HandBook/ui/core/token/.pages +++ /dev/null @@ -1,6 +0,0 @@ -title: Token 系统 -nav: - - Material Scheme Token: material_scheme - - Motion Token: motion - - 圆角缩放 Token: radius_scale - - 排版 Token: typography diff --git a/document/HandBook/ui/core/token/index.md b/document/HandBook/ui/core/token/index.md index 2043d147f..d912ba992 100644 --- a/document/HandBook/ui/core/token/index.md +++ b/document/HandBook/ui/core/token/index.md @@ -1,10 +1,11 @@ -# token - -> Welcome to the token section. +--- +title: Design Token +description: 本章节介绍 Material Design 3 的 Design Token 体系,涵盖配色方案(M +--- -## Overview +# Design Token -Documentation and resources for token. +本章节介绍 Material Design 3 的 Design Token 体系,涵盖配色方案(Material Scheme)、排版(Typography)、圆角(Radius Scale)和动效(Motion)四大 Token 类别。Token 系统是 UI 框架实现主题切换与设计一致性的核心机制。 --- diff --git a/document/HandBook/ui/core/token/material_scheme/.pages b/document/HandBook/ui/core/token/material_scheme/.pages deleted file mode 100644 index 4abbe57f2..000000000 --- a/document/HandBook/ui/core/token/material_scheme/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: Material Scheme Token -nav: - - index.md - - Token 字面量: cfmaterial_token_literals.md diff --git a/document/HandBook/ui/core/token/material_scheme/cfmaterial_token_literals.md b/document/HandBook/ui/core/token/material_scheme/cfmaterial_token_literals.md index d6b55b6ad..11d76f5af 100644 --- a/document/HandBook/ui/core/token/material_scheme/cfmaterial_token_literals.md +++ b/document/HandBook/ui/core/token/material_scheme/cfmaterial_token_literals.md @@ -1,142 +1,147 @@ -# Material Token 字面量 - -`cfmaterial_token_literals.h` 定义了 Material Design 3 色彩系统的全部 Token 字面量。Material You 的核心思想是让 UI 从用户的壁纸中提取颜色来生成主题,这些 Token 就是这套动态色彩系统的基础锚点。 - -我们没有直接用字符串字面量,而是把这些常量抽出来统一管理,原因有两个:一是避免拼写错误("md.primary" 和 "md.pirmary" 编译器不会报错,但运行时就找不到了),二是方便 IDE 自动补全和重构。 - -## 色彩角色体系 - -Material Design 3 的色彩不是简单的调色板,而是基于"角色"(Role)的概念。每个颜色都有它特定的使用场景: - -```cpp -#include "ui/core/token/material_scheme/cfmaterial_token_literals.h" - -using namespace cf::ui::core::token::literals; - -// Primary 色系 - 主品牌色,用于最重要的组件 -const char* primary = PRIMARY; // "md.primary" -const char* onPrimary = ON_PRIMARY; // "md.onPrimary" -const char* primaryContainer = PRIMARY_CONTAINER; // "md.primaryContainer" -const char* onPrimaryContainer = ON_PRIMARY_CONTAINER; // "md.onPrimaryContainer" - -// Secondary 色系 - 次要强调色 -const char* secondary = SECONDARY; -const char* onSecondary = ON_SECONDARY; - -// Tertiary 色系 - 第三强调色,用于对比和平衡 -const char* tertiary = TERTIARY; -const char* onTertiary = ON_TERTIARY; - -// Error 色系 - 错误状态和危险操作 -const char* error = ERROR; -const char* onError = ON_ERROR; -``` - -注意那个 "On" 前缀——这不是"打开"的意思,而是"绘制在...之上"(On)。`ON_PRIMARY` 就是绘制在 Primary 颜色上的文字和图标的颜色,Material 的配色算法会自动计算对比度,保证可读性。 - -## Container 色系 - -Container 色是 Material You 新增的概念。它们是基色的"调色版本"——更浅或更深,用来承载对应颜色的内容: - -```cpp -// 场景:一个卡片容器 -// card_bg 使用 primaryContainer(浅调的主色) -// card_content 使用 primary(深调的主色) - -// 场景:一个按钮 -// button_bg 使用 primary -// button_text 使用 onPrimary(高对比度) - -// 场景:一个强调区域 -// area_bg 使用 tertiaryContainer -// area_icon 使用 onTertiaryContainer -``` - -这样设计的好处是,组件的颜色关系由语义决定,而不是由具体的颜色值决定。动态换肤时,整个应用的颜色关系依然保持一致。 - -## Surface 色系 - -Surface 色系用于各种"表面"——背景、卡片、菜单等: - -```cpp -// 基础背景 -const char* background = BACKGROUND; // 应用背景 -const char* onBackground = ON_BACKGROUND; // 背景上的文字 - -// 表面颜色 -const char* surface = SURFACE; // 卡片、表单等表面 -const char* onSurface = ON_SURFACE; // 表面上的文字 - -// 表面变体 -const char* surfaceVariant = SURFACE_VARIANT; // 微差分的表面色 -const char* onSurfaceVariant = ON_SURFACE_VARIANT; // 表面变体上的文字 - -// 边框颜色 -const char* outline = OUTLINE; // 边框和分割线 -const char* outlineVariant = OUTLINE_VARIANT; // 微差分的边框色 -``` - -`surfaceVariant` 和 `outlineVariant` 这两个名字确实有点拗口。它们的作用是在深色模式下提供"不那么突兀"的边框和背景,避免视觉噪音过多。 - -## 特殊场景色系 - -还有几个用于特殊场景的颜色: - -```cpp -// 阴影和遮罩 -const char* shadow = SHADOW; // 投影颜色 -const char* scrim = SCRIM; // 模态背景遮罩 - -// 反色场景(用于对话框、抽屉等浮层) -const char* inverseSurface = INVERSE_SURFACE; // 反转的表面色 -const char* inverseOnSurface = INVERSE_ON_SURFACE; // 反转表面上的文字 -const char* inversePrimary = INVERSE_PRIMARY; // 反转的主色 -``` - -`scrim` 是当弹出模态对话框时,后面那层半透明的黑色遮罩。深色模式下它可能是半透明的白色,取决于主题的动态生成逻辑。 - -## 在主题系统中使用 - -这些字面量最终会被主题系统解析成实际的颜色值: - -```cpp -// 伪代码:主题系统如何使用这些 Token -class MaterialTheme { - Color resolve(const char* token) const { - // 实际实现会从动态生成的颜色表中查找 - return colorTable.at(token); - } -}; - -// 使用示例 -MaterialTheme theme; -auto buttonBg = theme.resolve(PRIMARY); -auto buttonText = theme.resolve(ON_PRIMARY); -auto cardBg = theme.resolve(SURFACE); -auto cardBorder = theme.resolve(OUTLINE); -``` - -## 批量遍历 - -如果你需要遍历所有 Token(比如在主题编辑器中),提供了两个辅助: - -```cpp -// 获取所有 Token 的数组 -const char* const* allTokens = ALL_TOKENS; -size_t count = TOKEN_COUNT; // 26 个 - -// 遍历所有 Token -for (size_t i = 0; i < TOKEN_COUNT; ++i) { - printf("Token %zu: %s\n", i, ALL_TOKENS[i]); -} -``` - -## Token 命名规则 - -所有 Token 遵循 `md.` 前缀的命名规则。这个 `md` 是 Material Design 的缩写,用来和其他设计系统(比如未来可能引入的自定义设计 Token)做区分。如果你要扩展自己的 Token,建议用不同的前缀避免冲突。 - -## 相关文档 - -- [Typography Token 字面量](../typography/cfmaterial_typography_token_literals.md) -- [Radius Scale Token 字面量](../radius_scale/cfmaterial_radius_scale_literals.md) -- [Motion Token 字面量](../motion/cfmaterial_motion_token_literals.md) +--- +title: Material Token 字面量 +description: 定义了 Material Design 3 色彩系统的全部 Token 字面量。Material Y +--- + +# Material Token 字面量 + +`cfmaterial_token_literals.h` 定义了 Material Design 3 色彩系统的全部 Token 字面量。Material You 的核心思想是让 UI 从用户的壁纸中提取颜色来生成主题,这些 Token 就是这套动态色彩系统的基础锚点。 + +我们没有直接用字符串字面量,而是把这些常量抽出来统一管理,原因有两个:一是避免拼写错误("md.primary" 和 "md.pirmary" 编译器不会报错,但运行时就找不到了),二是方便 IDE 自动补全和重构。 + +## 色彩角色体系 + +Material Design 3 的色彩不是简单的调色板,而是基于"角色"(Role)的概念。每个颜色都有它特定的使用场景: + +```cpp +#include "ui/core/token/material_scheme/cfmaterial_token_literals.h" + +using namespace cf::ui::core::token::literals; + +// Primary 色系 - 主品牌色,用于最重要的组件 +const char* primary = PRIMARY; // "md.primary" +const char* onPrimary = ON_PRIMARY; // "md.onPrimary" +const char* primaryContainer = PRIMARY_CONTAINER; // "md.primaryContainer" +const char* onPrimaryContainer = ON_PRIMARY_CONTAINER; // "md.onPrimaryContainer" + +// Secondary 色系 - 次要强调色 +const char* secondary = SECONDARY; +const char* onSecondary = ON_SECONDARY; + +// Tertiary 色系 - 第三强调色,用于对比和平衡 +const char* tertiary = TERTIARY; +const char* onTertiary = ON_TERTIARY; + +// Error 色系 - 错误状态和危险操作 +const char* error = ERROR; +const char* onError = ON_ERROR; +```text + +注意那个 "On" 前缀——这不是"打开"的意思,而是"绘制在...之上"(On)。`ON_PRIMARY` 就是绘制在 Primary 颜色上的文字和图标的颜色,Material 的配色算法会自动计算对比度,保证可读性。 + +## Container 色系 + +Container 色是 Material You 新增的概念。它们是基色的"调色版本"——更浅或更深,用来承载对应颜色的内容: + +```cpp +// 场景:一个卡片容器 +// card_bg 使用 primaryContainer(浅调的主色) +// card_content 使用 primary(深调的主色) + +// 场景:一个按钮 +// button_bg 使用 primary +// button_text 使用 onPrimary(高对比度) + +// 场景:一个强调区域 +// area_bg 使用 tertiaryContainer +// area_icon 使用 onTertiaryContainer +```text + +这样设计的好处是,组件的颜色关系由语义决定,而不是由具体的颜色值决定。动态换肤时,整个应用的颜色关系依然保持一致。 + +## Surface 色系 + +Surface 色系用于各种"表面"——背景、卡片、菜单等: + +```cpp +// 基础背景 +const char* background = BACKGROUND; // 应用背景 +const char* onBackground = ON_BACKGROUND; // 背景上的文字 + +// 表面颜色 +const char* surface = SURFACE; // 卡片、表单等表面 +const char* onSurface = ON_SURFACE; // 表面上的文字 + +// 表面变体 +const char* surfaceVariant = SURFACE_VARIANT; // 微差分的表面色 +const char* onSurfaceVariant = ON_SURFACE_VARIANT; // 表面变体上的文字 + +// 边框颜色 +const char* outline = OUTLINE; // 边框和分割线 +const char* outlineVariant = OUTLINE_VARIANT; // 微差分的边框色 +```text + +`surfaceVariant` 和 `outlineVariant` 这两个名字确实有点拗口。它们的作用是在深色模式下提供"不那么突兀"的边框和背景,避免视觉噪音过多。 + +## 特殊场景色系 + +还有几个用于特殊场景的颜色: + +```cpp +// 阴影和遮罩 +const char* shadow = SHADOW; // 投影颜色 +const char* scrim = SCRIM; // 模态背景遮罩 + +// 反色场景(用于对话框、抽屉等浮层) +const char* inverseSurface = INVERSE_SURFACE; // 反转的表面色 +const char* inverseOnSurface = INVERSE_ON_SURFACE; // 反转表面上的文字 +const char* inversePrimary = INVERSE_PRIMARY; // 反转的主色 +```text + +`scrim` 是当弹出模态对话框时,后面那层半透明的黑色遮罩。深色模式下它可能是半透明的白色,取决于主题的动态生成逻辑。 + +## 在主题系统中使用 + +这些字面量最终会被主题系统解析成实际的颜色值: + +```cpp +// 伪代码:主题系统如何使用这些 Token +class MaterialTheme { + Color resolve(const char* token) const { + // 实际实现会从动态生成的颜色表中查找 + return colorTable.at(token); + } +}; + +// 使用示例 +MaterialTheme theme; +auto buttonBg = theme.resolve(PRIMARY); +auto buttonText = theme.resolve(ON_PRIMARY); +auto cardBg = theme.resolve(SURFACE); +auto cardBorder = theme.resolve(OUTLINE); +```text + +## 批量遍历 + +如果你需要遍历所有 Token(比如在主题编辑器中),提供了两个辅助: + +```cpp +// 获取所有 Token 的数组 +const char* const* allTokens = ALL_TOKENS; +size_t count = TOKEN_COUNT; // 26 个 + +// 遍历所有 Token +for (size_t i = 0; i < TOKEN_COUNT; ++i) { + printf("Token %zu: %s\n", i, ALL_TOKENS[i]); +} +```text + +## Token 命名规则 + +所有 Token 遵循 `md.` 前缀的命名规则。这个 `md` 是 Material Design 的缩写,用来和其他设计系统(比如未来可能引入的自定义设计 Token)做区分。如果你要扩展自己的 Token,建议用不同的前缀避免冲突。 + +## 相关文档 + +- [Typography Token 字面量](../typography/cfmaterial_typography_token_literals.md) +- [Radius Scale Token 字面量](../radius_scale/cfmaterial_radius_scale_literals.md) +- [Motion Token 字面量](../motion/cfmaterial_motion_token_literals.md) diff --git a/document/HandBook/ui/core/token/material_scheme/index.md b/document/HandBook/ui/core/token/material_scheme/index.md index 5d9a1c68a..d10bd4b9d 100644 --- a/document/HandBook/ui/core/token/material_scheme/index.md +++ b/document/HandBook/ui/core/token/material_scheme/index.md @@ -1,10 +1,11 @@ -# Material Scheme - -> Welcome to the Material Scheme section. +--- +title: Material 配色方案 +description: 本章节详细描述 Material Design 3 的配色方案 Token,包括主色(Primary +--- -## Overview +# Material 配色方案 -Documentation and resources for Material Scheme. +本章节详细描述 Material Design 3 的配色方案 Token,包括主色(Primary)、次色(Secondary)、第三色(Tertiary)、中性色(Neutral)等颜色角色的定义与配置。配色方案支持动态颜色生成,实现亮色与暗色主题的自动适配。 --- diff --git a/document/HandBook/ui/core/token/motion/.pages b/document/HandBook/ui/core/token/motion/.pages deleted file mode 100644 index ff2046117..000000000 --- a/document/HandBook/ui/core/token/motion/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: Motion Token -nav: - - index.md - - Motion Token 字面量: cfmaterial_motion_token_literals.md diff --git a/document/HandBook/ui/core/token/motion/cfmaterial_motion_token_literals.md b/document/HandBook/ui/core/token/motion/cfmaterial_motion_token_literals.md index f2de480e7..8d60a79f0 100644 --- a/document/HandBook/ui/core/token/motion/cfmaterial_motion_token_literals.md +++ b/document/HandBook/ui/core/token/motion/cfmaterial_motion_token_literals.md @@ -1,243 +1,248 @@ -# Motion Token 字面量 - -`cfmaterial_motion_token_literals.h` 定义了 Material Design 3 动效系统的全部 Token 字面量。Material 的动效不仅仅是"让东西动起来",而是一套精心设计的时间(Duration)和缓动(Easing)体系,用来传达 UI 的层级关系和交互反馈。 - -这套字面量让我们可以用语义化的方式引用动效参数,而不是到处散落 `animate(300ms, ease-out)` 这种魔法数字。统一管理动效参数的好处是,调整动效风格时只需改主题配置,整个应用的动画节奏会保持一致。 - -## 动效时长 Token - -Material 定义了 6 类标准动效时长,按元素尺寸和进出方向划分: - -```cpp -#include "ui/core/token/motion/cfmaterial_motion_token_literals.h" - -using namespace cf::ui::core::token::literals; - -// 小元素进入 - 200ms -const char* shortEnterDuration = MOTION_SHORT_ENTER_DURATION; -// "md.motion.shortEnter.duration" - -// 小元素退出 - 150ms -const char* shortExitDuration = MOTION_SHORT_EXIT_DURATION; -// "md.motion.shortExit.duration" - -// 中等元素进入 - 300ms -const char* mediumEnterDuration = MOTION_MEDIUM_ENTER_DURATION; - -// 中等元素退出 - 250ms -const char* mediumExitDuration = MOTION_MEDIUM_EXIT_DURATION; - -// 大元素进入 - 450ms -const char* longEnterDuration = MOTION_LONG_ENTER_DURATION; - -// 大元素退出 - 400ms -const char* longExitDuration = MOTION_LONG_EXIT_DURATION; - -// 状态变化 - 200ms -const char* stateChangeDuration = MOTION_STATE_CHANGE_DURATION; - -// 涟漪展开 - 400ms -const char* rippleExpandDuration = MOTION_RIPPLE_EXPAND_DURATION; - -// 涟漪消散 - 150ms -const char* rippleFadeDuration = MOTION_RIPPLE_FADE_DURATION; -``` - -注意退出时长通常比进入时长短,这是个刻意的设计——用户等待东西出现比等待东西消失更不耐烦。 - -## 动效缓动 Token - -缓动函数决定了动画随时间的变化速率,Material 定义了三类标准缓动: - -```cpp -// 小元素进入缓动 - EmphasizedDecelerate -const char* shortEnterEasing = MOTION_SHORT_ENTER_EASING; -// "md.motion.shortEnter.easing" - -// 小元素退出缓动 - EmphasizedAccelerate -const char* shortExitEasing = MOTION_SHORT_EXIT_EASING; - -// 中等元素进入缓动 - EmphasizedDecelerate -const char* mediumEnterEasing = MOTION_MEDIUM_ENTER_EASING; - -// 中等元素退出缓动 - EmphasizedAccelerate -const char* mediumExitEasing = MOTION_MEDIUM_EXIT_EASING; - -// 大元素进入/退出缓动 - Emphasized -const char* longEnterEasing = MOTION_LONG_ENTER_EASING; -const char* longExitEasing = MOTION_LONG_EXIT_EASING; - -// 状态变化缓动 - Standard -const char* stateChangeEasing = MOTION_STATE_CHANGE_EASING; - -// 涟漪展开缓动 - Standard -const char* rippleExpandEasing = MOTION_RIPPLE_EXPAND_EASING; - -// 涟漪消散缓动 - Linear -const char* rippleFadeEasing = MOTION_RIPPLE_FADE_EASING; -``` - -## 缓动函数说明 - -Material 的缓动函数名称有点抽象,这里解释一下: - -| 缓动名称 | 数学特性 | 适用场景 | 视觉效果 | -|---------|---------|---------|---------| -| EmphasizedDecelerate | 快进慢出 | 元素进入 | 快速入场后柔和减速 | -| EmphasizedAccelerate | 慢进快出 | 元素退出 | 缓慢启动后快速离开 | -| Emphasized | 先加速再减速(明显) | 大元素进入 | 起步和结束都柔和,中间加速明显 | -| Standard | 先加速再减速(轻微) | 状态变化 | 轻微的缓动,不干扰视线 | -| Linear | 匀速 | 涟漪消散 | 无缓动,适合淡出效果 | - -Emphasized 系列用于大尺寸元素,是因为大元素的移动更明显,需要更柔和的缓动来避免视觉冲击。Standard 用于状态变化(如按钮按下),不希望太夸张。 - -## 在主题系统中使用 - -动效 Token 的使用方式和其他 Token 一致,由主题系统解析成实际的时长和缓动: - -```cpp -// 伪代码:主题系统如何解析动效 Token -struct MotionSpec { - int durationMs; - EasingFunction easing; -}; - -class MaterialTheme { - MotionSpec resolveMotion(const char* durationToken, - const char* easingToken) const { - return { - durationTable.at(durationToken), - easingTable.at(easingToken) - }; - } -}; - -// 使用示例 -MaterialTheme theme; - -// 小元素进入动画 -auto fadeIn = theme.resolveMotion( - MOTION_SHORT_ENTER_DURATION, - MOTION_SHORT_ENTER_EASING -); -element.animate(fadeIn); - -// 对话框进入动画(大元素) -auto dialogEnter = theme.resolveMotion( - MOTION_LONG_ENTER_DURATION, - MOTION_LONG_ENTER_EASING -); -dialog.animate(dialogEnter); -``` - -## 场景选择指南 - -选择哪个动效参数组合,主要看元素尺寸和动效类型: - -```cpp -// 小元素(按钮、开关、标签) -// 进入: 200ms + EmphasizedDecelerate -// 退出: 150ms + EmphasizedAccelerate -auto smallEnter = resolveMotion( - MOTION_SHORT_ENTER_DURATION, - MOTION_SHORT_ENTER_EASING -); - -// 中等元素(卡片、列表项) -// 进入: 300ms + EmphasizedDecelerate -// 退出: 250ms + EmphasizedAccelerate -auto mediumEnter = resolveMotion( - MOTION_MEDIUM_ENTER_DURATION, - MOTION_MEDIUM_ENTER_EASING -); - -// 大元素(对话框、抽屉) -// 进入: 450ms + Emphasized -// 退出: 400ms + Emphasized -auto largeEnter = resolveMotion( - MOTION_LONG_ENTER_DURATION, - MOTION_LONG_ENTER_EASING -); - -// 状态变化(按钮按下、切换开关) -// 200ms + Standard -auto stateChange = resolveMotion( - MOTION_STATE_CHANGE_DURATION, - MOTION_STATE_CHANGE_EASING -); - -// 涟漪效果 -// 展开: 400ms + Standard -// 消散: 150ms + Linear -auto rippleExpand = resolveMotion( - MOTION_RIPPLE_EXPAND_DURATION, - MOTION_RIPPLE_EXPAND_EASING -); -``` - -⚠️ 一个常见的错误是用错缓动方向。进入动画应该用 Decelerate(减速),这样元素到达终点时会柔和地停下来;退出动画应该用 Accelerate(加速),元素快速离开不留痕迹。用反了会感觉很怪——进入时"哐"一下撞上终点,退出时拖泥带水。 - -## 批量遍历 - -提供了遍历所有动效 Token 的辅助: - -```cpp -// 遍历所有 Motion Token -for (size_t i = 0; i < MOTION_TOKEN_COUNT; ++i) { - printf("Motion Token %zu: %s\n", i, ALL_MOTION_TOKENS[i]); -} -``` - -## 自定义动效参数 - -如果你的设计需要非标准的动效参数,有两种处理方式: - -```cpp -// 方式 1:在主题配置中添加自定义 Token -// 保持代码语义化,但需要修改主题系统 - -// 方式 2:直接构造 MotionSpec(灵活但不统一) -// 对于特殊场景,直接传值更实际 -MotionSpec custom = {500, CustomEasing}; -element.animate(custom); -``` - -建议把常用的自定义动效也纳入 Token 管理,保持代码的一致性。 - -## 性能考虑 - -动效虽然是视觉体验的重要组成部分,但也要注意性能: - -```cpp -// 硬件加速是个好主意 -element.setLayerType(LAYER_TYPE_HARDWARE); -element.animate(spec); - -// 大量元素同时动画时,考虑简化动效 -// 比如用短时长代替长时长,或者取消缓动 -``` - -Material 的动效时长(最大 450ms)是经过平衡的——既足够传达视觉反馈,又不会让用户等太久。如果你的动效感觉"拖",可能不是时长问题,而是缓动曲线选择不当。 - -## 可访问性 - -对于动效敏感的用户,应该提供"减少动效"选项: - -```cpp -// 伪代码:尊重用户的动效偏好 -if (userPrefersReducedMotion()) { - // 跳过动画,或者使用极短时长 - element.setOpacity(1.0f); // 直接设置最终状态 -} else { - // 正常动画 - element.animate(spec); -} -``` - -Material 的动效设计已经考虑了可访问性,但提供降级选项仍然是好实践。 - -## 相关文档 - -- [Material Token 字面量](../material_scheme/cfmaterial_token_literals.md) -- [Typography Token 字面量](../typography/cfmaterial_typography_token_literals.md) -- [Radius Scale Token 字面量](../radius_scale/cfmaterial_radius_scale_literals.md) +--- +title: Motion Token 字面量 +description: 定义了 Material Design 3 动效系统的全部 Token 字面量。Material 的 +--- + +# Motion Token 字面量 + +`cfmaterial_motion_token_literals.h` 定义了 Material Design 3 动效系统的全部 Token 字面量。Material 的动效不仅仅是"让东西动起来",而是一套精心设计的时间(Duration)和缓动(Easing)体系,用来传达 UI 的层级关系和交互反馈。 + +这套字面量让我们可以用语义化的方式引用动效参数,而不是到处散落 `animate(300ms, ease-out)` 这种魔法数字。统一管理动效参数的好处是,调整动效风格时只需改主题配置,整个应用的动画节奏会保持一致。 + +## 动效时长 Token + +Material 定义了 6 类标准动效时长,按元素尺寸和进出方向划分: + +```cpp +#include "ui/core/token/motion/cfmaterial_motion_token_literals.h" + +using namespace cf::ui::core::token::literals; + +// 小元素进入 - 200ms +const char* shortEnterDuration = MOTION_SHORT_ENTER_DURATION; +// "md.motion.shortEnter.duration" + +// 小元素退出 - 150ms +const char* shortExitDuration = MOTION_SHORT_EXIT_DURATION; +// "md.motion.shortExit.duration" + +// 中等元素进入 - 300ms +const char* mediumEnterDuration = MOTION_MEDIUM_ENTER_DURATION; + +// 中等元素退出 - 250ms +const char* mediumExitDuration = MOTION_MEDIUM_EXIT_DURATION; + +// 大元素进入 - 450ms +const char* longEnterDuration = MOTION_LONG_ENTER_DURATION; + +// 大元素退出 - 400ms +const char* longExitDuration = MOTION_LONG_EXIT_DURATION; + +// 状态变化 - 200ms +const char* stateChangeDuration = MOTION_STATE_CHANGE_DURATION; + +// 涟漪展开 - 400ms +const char* rippleExpandDuration = MOTION_RIPPLE_EXPAND_DURATION; + +// 涟漪消散 - 150ms +const char* rippleFadeDuration = MOTION_RIPPLE_FADE_DURATION; +```text + +注意退出时长通常比进入时长短,这是个刻意的设计——用户等待东西出现比等待东西消失更不耐烦。 + +## 动效缓动 Token + +缓动函数决定了动画随时间的变化速率,Material 定义了三类标准缓动: + +```cpp +// 小元素进入缓动 - EmphasizedDecelerate +const char* shortEnterEasing = MOTION_SHORT_ENTER_EASING; +// "md.motion.shortEnter.easing" + +// 小元素退出缓动 - EmphasizedAccelerate +const char* shortExitEasing = MOTION_SHORT_EXIT_EASING; + +// 中等元素进入缓动 - EmphasizedDecelerate +const char* mediumEnterEasing = MOTION_MEDIUM_ENTER_EASING; + +// 中等元素退出缓动 - EmphasizedAccelerate +const char* mediumExitEasing = MOTION_MEDIUM_EXIT_EASING; + +// 大元素进入/退出缓动 - Emphasized +const char* longEnterEasing = MOTION_LONG_ENTER_EASING; +const char* longExitEasing = MOTION_LONG_EXIT_EASING; + +// 状态变化缓动 - Standard +const char* stateChangeEasing = MOTION_STATE_CHANGE_EASING; + +// 涟漪展开缓动 - Standard +const char* rippleExpandEasing = MOTION_RIPPLE_EXPAND_EASING; + +// 涟漪消散缓动 - Linear +const char* rippleFadeEasing = MOTION_RIPPLE_FADE_EASING; +```bash + +## 缓动函数说明 + +Material 的缓动函数名称有点抽象,这里解释一下: + +| 缓动名称 | 数学特性 | 适用场景 | 视觉效果 | +|---------|---------|---------|---------| +| EmphasizedDecelerate | 快进慢出 | 元素进入 | 快速入场后柔和减速 | +| EmphasizedAccelerate | 慢进快出 | 元素退出 | 缓慢启动后快速离开 | +| Emphasized | 先加速再减速(明显) | 大元素进入 | 起步和结束都柔和,中间加速明显 | +| Standard | 先加速再减速(轻微) | 状态变化 | 轻微的缓动,不干扰视线 | +| Linear | 匀速 | 涟漪消散 | 无缓动,适合淡出效果 | + +Emphasized 系列用于大尺寸元素,是因为大元素的移动更明显,需要更柔和的缓动来避免视觉冲击。Standard 用于状态变化(如按钮按下),不希望太夸张。 + +## 在主题系统中使用 + +动效 Token 的使用方式和其他 Token 一致,由主题系统解析成实际的时长和缓动: + +```cpp +// 伪代码:主题系统如何解析动效 Token +struct MotionSpec { + int durationMs; + EasingFunction easing; +}; + +class MaterialTheme { + MotionSpec resolveMotion(const char* durationToken, + const char* easingToken) const { + return { + durationTable.at(durationToken), + easingTable.at(easingToken) + }; + } +}; + +// 使用示例 +MaterialTheme theme; + +// 小元素进入动画 +auto fadeIn = theme.resolveMotion( + MOTION_SHORT_ENTER_DURATION, + MOTION_SHORT_ENTER_EASING +); +element.animate(fadeIn); + +// 对话框进入动画(大元素) +auto dialogEnter = theme.resolveMotion( + MOTION_LONG_ENTER_DURATION, + MOTION_LONG_ENTER_EASING +); +dialog.animate(dialogEnter); +```text + +## 场景选择指南 + +选择哪个动效参数组合,主要看元素尺寸和动效类型: + +```cpp +// 小元素(按钮、开关、标签) +// 进入: 200ms + EmphasizedDecelerate +// 退出: 150ms + EmphasizedAccelerate +auto smallEnter = resolveMotion( + MOTION_SHORT_ENTER_DURATION, + MOTION_SHORT_ENTER_EASING +); + +// 中等元素(卡片、列表项) +// 进入: 300ms + EmphasizedDecelerate +// 退出: 250ms + EmphasizedAccelerate +auto mediumEnter = resolveMotion( + MOTION_MEDIUM_ENTER_DURATION, + MOTION_MEDIUM_ENTER_EASING +); + +// 大元素(对话框、抽屉) +// 进入: 450ms + Emphasized +// 退出: 400ms + Emphasized +auto largeEnter = resolveMotion( + MOTION_LONG_ENTER_DURATION, + MOTION_LONG_ENTER_EASING +); + +// 状态变化(按钮按下、切换开关) +// 200ms + Standard +auto stateChange = resolveMotion( + MOTION_STATE_CHANGE_DURATION, + MOTION_STATE_CHANGE_EASING +); + +// 涟漪效果 +// 展开: 400ms + Standard +// 消散: 150ms + Linear +auto rippleExpand = resolveMotion( + MOTION_RIPPLE_EXPAND_DURATION, + MOTION_RIPPLE_EXPAND_EASING +); +```text + +⚠️ 一个常见的错误是用错缓动方向。进入动画应该用 Decelerate(减速),这样元素到达终点时会柔和地停下来;退出动画应该用 Accelerate(加速),元素快速离开不留痕迹。用反了会感觉很怪——进入时"哐"一下撞上终点,退出时拖泥带水。 + +## 批量遍历 + +提供了遍历所有动效 Token 的辅助: + +```cpp +// 遍历所有 Motion Token +for (size_t i = 0; i < MOTION_TOKEN_COUNT; ++i) { + printf("Motion Token %zu: %s\n", i, ALL_MOTION_TOKENS[i]); +} +```text + +## 自定义动效参数 + +如果你的设计需要非标准的动效参数,有两种处理方式: + +```cpp +// 方式 1:在主题配置中添加自定义 Token +// 保持代码语义化,但需要修改主题系统 + +// 方式 2:直接构造 MotionSpec(灵活但不统一) +// 对于特殊场景,直接传值更实际 +MotionSpec custom = {500, CustomEasing}; +element.animate(custom); +```text + +建议把常用的自定义动效也纳入 Token 管理,保持代码的一致性。 + +## 性能考虑 + +动效虽然是视觉体验的重要组成部分,但也要注意性能: + +```cpp +// 硬件加速是个好主意 +element.setLayerType(LAYER_TYPE_HARDWARE); +element.animate(spec); + +// 大量元素同时动画时,考虑简化动效 +// 比如用短时长代替长时长,或者取消缓动 +```text + +Material 的动效时长(最大 450ms)是经过平衡的——既足够传达视觉反馈,又不会让用户等太久。如果你的动效感觉"拖",可能不是时长问题,而是缓动曲线选择不当。 + +## 可访问性 + +对于动效敏感的用户,应该提供"减少动效"选项: + +```cpp +// 伪代码:尊重用户的动效偏好 +if (userPrefersReducedMotion()) { + // 跳过动画,或者使用极短时长 + element.setOpacity(1.0f); // 直接设置最终状态 +} else { + // 正常动画 + element.animate(spec); +} +```text + +Material 的动效设计已经考虑了可访问性,但提供降级选项仍然是好实践。 + +## 相关文档 + +- [Material Token 字面量](../material_scheme/cfmaterial_token_literals.md) +- [Typography Token 字面量](../typography/cfmaterial_typography_token_literals.md) +- [Radius Scale Token 字面量](../radius_scale/cfmaterial_radius_scale_literals.md) diff --git a/document/HandBook/ui/core/token/motion/index.md b/document/HandBook/ui/core/token/motion/index.md index ae4b313ba..8344bdcb7 100644 --- a/document/HandBook/ui/core/token/motion/index.md +++ b/document/HandBook/ui/core/token/motion/index.md @@ -1,10 +1,11 @@ -# Motion - -> Welcome to the Motion section. +--- +title: 动效系统 +description: 本章节详细描述 Material Design 3 的动效 Token,包括缓动曲线(Easing +--- -## Overview +# 动效系统 -Documentation and resources for Motion. +本章节详细描述 Material Design 3 的动效 Token,包括缓动曲线(Easing Curve)、持续时间(Duration)和运动模式(Motion Pattern)等动效属性的 Token 化配置。动效系统为组件的交互反馈提供统一的时间与节奏规范。 --- diff --git a/document/HandBook/ui/core/token/radius_scale/.pages b/document/HandBook/ui/core/token/radius_scale/.pages deleted file mode 100644 index c33f43689..000000000 --- a/document/HandBook/ui/core/token/radius_scale/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: 圆角缩放 Token -nav: - - index.md - - Token 字面量: cfmaterial_radius_scale_literals.md diff --git a/document/HandBook/ui/core/token/radius_scale/cfmaterial_radius_scale_literals.md b/document/HandBook/ui/core/token/radius_scale/cfmaterial_radius_scale_literals.md index 15983df81..ba853e24e 100644 --- a/document/HandBook/ui/core/token/radius_scale/cfmaterial_radius_scale_literals.md +++ b/document/HandBook/ui/core/token/radius_scale/cfmaterial_radius_scale_literals.md @@ -1,172 +1,177 @@ -# Radius Scale Token 字面量 - -`cfmaterial_radius_scale_literals.h` 定义了 Material Design 3 圆角系统的全部 Token 字面量。Material 的圆角不是随便选的数字,而是一套精心设计的 7 级标尺,从 0dp(完全直角)到 32dp(大圆角)。 - -这套字面量的存在是为了保证整个应用的圆角风格一致。与其到处硬编码 `borderRadius: 8`,不如用语义化的 Token,这样调整设计规范时只需改一处。 - -## 七级圆角标尺 - -Material Design 3 定义了 7 个标准圆角值: - -```cpp -#include "ui/core/token/radius_scale/cfmaterial_radius_scale_literals.h" - -using namespace cf::ui::core::token::literals; - -// 完全直角 - 0dp -const char* cornerNone = CORNER_NONE; // "md.shape.cornerNone" - -// 超小圆角 - 4dp -const char* cornerXS = CORNER_EXTRA_SMALL; // "md.shape.cornerExtraSmall" - -// 小圆角 - 8dp -const char* cornerSmall = CORNER_SMALL; // "md.shape.cornerSmall" - -// 中圆角 - 12dp -const char* cornerMedium = CORNER_MEDIUM; // "md.shape.cornerMedium" - -// 大圆角 - 16dp -const char* cornerLarge = CORNER_LARGE; // "md.shape.cornerLarge" - -// 超大圆角 - 28dp -const char* cornerXL = CORNER_EXTRA_LARGE; // "md.shape.cornerExtraLarge" - -// 超超大圆角 - 32dp -const char* cornerXXL = CORNER_EXTRA_EXTRA_LARGE; // "md.shape.cornerExtraExtraLarge" -``` - -注意这里用的是 XS/XL 这种缩写,而不是 ExtraSmall/ExtraLarge。常量名为了可读性用的完整拼写,但命名空间内提供别名是个不错的实践(不过当前实现里还没加,需要的话可以自己补)。 - -## 标准用途映射 - -每个圆角值都有 Material 推荐的标准用途: - -```cpp -// 0dp - 完全直角 -// 用于:需要强调边界、或者设计风格偏向硬朗的场景 -const char* buttonRadius = CORNER_NONE; // 某些设计系统用直角按钮 - -// 4dp - 超小圆角 -// 用于:芯片(Chip)、小卡片 -const char* chipRadius = CORNER_EXTRA_SMALL; - -// 8dp - 小圆角 -// 用于:文本框、复选框、小型按钮 -const char* textFieldRadius = CORNER_SMALL; -const char* checkboxRadius = CORNER_SMALL; -const char* smallButtonRadius = CORNER_SMALL; - -// 12dp - 中圆角 -// 用于:卡片(最常用的圆角尺寸) -const char* cardRadius = CORNER_MEDIUM; -const char* buttonRadius = CORNER_MEDIUM; // Material 标准按钮圆角 - -// 16dp - 大圆角 -// 用于:对话框、提示框 -const char* dialogRadius = CORNER_LARGE; -const char* alertDialogRadius = CORNER_LARGE; - -// 28dp - 超大圆角 -// 用于:悬浮操作按钮(FAB)、底部抽屉 -const char* fabRadius = CORNER_EXTRA_LARGE; -const char* modalRadius = CORNER_EXTRA_LARGE; - -// 32dp - 超超大圆角 -// 用于:导航抽屉、全屏模态 -const char* drawerRadius = CORNER_EXTRA_EXTRA_LARGE; -``` - -## 在主题系统中使用 - -圆角 Token 的使用方式和其他 Token 一致,由主题系统解析成实际的数值: - -```cpp -// 伪代码:主题系统如何解析圆角 Token -class MaterialTheme { - float resolveRadius(const char* token) const { - return radiusTable.at(token); // 返回 dp 值 - } -}; - -// 使用示例 -MaterialTheme theme; - -// 设置按钮圆角 -float buttonRadius = theme.resolveRadius(CORNER_MEDIUM); -button.setCornerRadius(buttonRadius); - -// 设置卡片圆角 -float cardRadius = theme.resolveRadius(CORNER_MEDIUM); -card.setCornerRadius(cardRadius); - -// 设置 FAB 圆角 -float fabRadius = theme.resolveRadius(CORNER_EXTRA_LARGE); -fab.setCornerRadius(fabRadius); -``` - -## 圆角选择指南 - -选择哪个圆角值有时候不太直观,这里有一些实用的判断依据: - -```cpp -// 小型组件(< 40dp 高度) -// 用 4dp 或 8dp,避免圆角占太多比例 -const char* smallComponentRadius = CORNER_SMALL; - -// 中型组件(40-80dp 高度) -// 用 12dp,平衡视觉和空间 -const char* mediumComponentRadius = CORNER_MEDIUM; - -// 大型组件(> 80dp 高度) -// 用 16dp 或更大,让圆角和组件尺寸成比例 -const char* largeComponentRadius = CORNER_LARGE; - -// 正圆形组件(如 FAB) -// 用 28dp 或 32dp,接近半圆 -const char* circularComponentRadius = CORNER_EXTRA_LARGE; -``` - -⚠️ 一个常见的错误是给小型组件用太大的圆角。比如一个高度只有 32dp 的按钮用 16dp 圆角,结果圆角占了一半高度,看起来会很奇怪。圆角值应该和组件尺寸成比例。 - -## 自定义圆角值 - -虽然 Material 定义了这 7 个标准值,但你的设计可能需要其他圆角尺寸。有两种处理方式: - -```cpp -// 方式 1:在主题配置中添加自定义 Token -// 这需要修改主题系统,但可以保持代码的语义化 - -// 方式 2:直接使用数值(不推荐,但现实) -// 对于特别的设计需求,有时候直接传值更实际 -button.setCornerRadius(20.0f); // 自定义值 -``` - -如果团队有统一的设计规范,建议把常用的自定义圆角也加到 Token 系统里,保持代码的语义化。 - -## 批量遍历 - -提供了遍历所有圆角 Token 的辅助: - -```cpp -// 遍历所有 Radius Token -for (size_t i = 0; i < RADIUS_TOKEN_COUNT; ++i) { - printf("Radius Token %zu: %s\n", i, ALL_RADIUS_TOKENS[i]); -} -``` - -## 响应式考虑 - -圆角的 dp 值在不同密度的屏幕上会自动缩放,这是 UI 框架的责任。如果你的框架不支持 dp/sp 单位,需要手动处理密度缩放: - -```cpp -// 伪代码:手动处理密度缩放 -float scale = getDeviceDensity(); -float radiusInPx = theme.resolveRadius(CORNER_MEDIUM) * scale; -view.setCornerRadiusPx(radiusInPx); -``` - -## 相关文档 - -- [Material Token 字面量](../material_scheme/cfmaterial_token_literals.md) -- [Typography Token 字面量](../typography/cfmaterial_typography_token_literals.md) -- [Motion Token 字面量](../motion/cfmaterial_motion_token_literals.md) +--- +title: Radius Scale Token 字面量 +description: 定义了 Material Design 3 圆角系统的全部 Token 字面量。Material 的 +--- + +# Radius Scale Token 字面量 + +`cfmaterial_radius_scale_literals.h` 定义了 Material Design 3 圆角系统的全部 Token 字面量。Material 的圆角不是随便选的数字,而是一套精心设计的 7 级标尺,从 0dp(完全直角)到 32dp(大圆角)。 + +这套字面量的存在是为了保证整个应用的圆角风格一致。与其到处硬编码 `borderRadius: 8`,不如用语义化的 Token,这样调整设计规范时只需改一处。 + +## 七级圆角标尺 + +Material Design 3 定义了 7 个标准圆角值: + +```cpp +#include "ui/core/token/radius_scale/cfmaterial_radius_scale_literals.h" + +using namespace cf::ui::core::token::literals; + +// 完全直角 - 0dp +const char* cornerNone = CORNER_NONE; // "md.shape.cornerNone" + +// 超小圆角 - 4dp +const char* cornerXS = CORNER_EXTRA_SMALL; // "md.shape.cornerExtraSmall" + +// 小圆角 - 8dp +const char* cornerSmall = CORNER_SMALL; // "md.shape.cornerSmall" + +// 中圆角 - 12dp +const char* cornerMedium = CORNER_MEDIUM; // "md.shape.cornerMedium" + +// 大圆角 - 16dp +const char* cornerLarge = CORNER_LARGE; // "md.shape.cornerLarge" + +// 超大圆角 - 28dp +const char* cornerXL = CORNER_EXTRA_LARGE; // "md.shape.cornerExtraLarge" + +// 超超大圆角 - 32dp +const char* cornerXXL = CORNER_EXTRA_EXTRA_LARGE; // "md.shape.cornerExtraExtraLarge" +```text + +注意这里用的是 XS/XL 这种缩写,而不是 ExtraSmall/ExtraLarge。常量名为了可读性用的完整拼写,但命名空间内提供别名是个不错的实践(不过当前实现里还没加,需要的话可以自己补)。 + +## 标准用途映射 + +每个圆角值都有 Material 推荐的标准用途: + +```cpp +// 0dp - 完全直角 +// 用于:需要强调边界、或者设计风格偏向硬朗的场景 +const char* buttonRadius = CORNER_NONE; // 某些设计系统用直角按钮 + +// 4dp - 超小圆角 +// 用于:芯片(Chip)、小卡片 +const char* chipRadius = CORNER_EXTRA_SMALL; + +// 8dp - 小圆角 +// 用于:文本框、复选框、小型按钮 +const char* textFieldRadius = CORNER_SMALL; +const char* checkboxRadius = CORNER_SMALL; +const char* smallButtonRadius = CORNER_SMALL; + +// 12dp - 中圆角 +// 用于:卡片(最常用的圆角尺寸) +const char* cardRadius = CORNER_MEDIUM; +const char* buttonRadius = CORNER_MEDIUM; // Material 标准按钮圆角 + +// 16dp - 大圆角 +// 用于:对话框、提示框 +const char* dialogRadius = CORNER_LARGE; +const char* alertDialogRadius = CORNER_LARGE; + +// 28dp - 超大圆角 +// 用于:悬浮操作按钮(FAB)、底部抽屉 +const char* fabRadius = CORNER_EXTRA_LARGE; +const char* modalRadius = CORNER_EXTRA_LARGE; + +// 32dp - 超超大圆角 +// 用于:导航抽屉、全屏模态 +const char* drawerRadius = CORNER_EXTRA_EXTRA_LARGE; +```text + +## 在主题系统中使用 + +圆角 Token 的使用方式和其他 Token 一致,由主题系统解析成实际的数值: + +```cpp +// 伪代码:主题系统如何解析圆角 Token +class MaterialTheme { + float resolveRadius(const char* token) const { + return radiusTable.at(token); // 返回 dp 值 + } +}; + +// 使用示例 +MaterialTheme theme; + +// 设置按钮圆角 +float buttonRadius = theme.resolveRadius(CORNER_MEDIUM); +button.setCornerRadius(buttonRadius); + +// 设置卡片圆角 +float cardRadius = theme.resolveRadius(CORNER_MEDIUM); +card.setCornerRadius(cardRadius); + +// 设置 FAB 圆角 +float fabRadius = theme.resolveRadius(CORNER_EXTRA_LARGE); +fab.setCornerRadius(fabRadius); +```text + +## 圆角选择指南 + +选择哪个圆角值有时候不太直观,这里有一些实用的判断依据: + +```cpp +// 小型组件(< 40dp 高度) +// 用 4dp 或 8dp,避免圆角占太多比例 +const char* smallComponentRadius = CORNER_SMALL; + +// 中型组件(40-80dp 高度) +// 用 12dp,平衡视觉和空间 +const char* mediumComponentRadius = CORNER_MEDIUM; + +// 大型组件(> 80dp 高度) +// 用 16dp 或更大,让圆角和组件尺寸成比例 +const char* largeComponentRadius = CORNER_LARGE; + +// 正圆形组件(如 FAB) +// 用 28dp 或 32dp,接近半圆 +const char* circularComponentRadius = CORNER_EXTRA_LARGE; +```text + +⚠️ 一个常见的错误是给小型组件用太大的圆角。比如一个高度只有 32dp 的按钮用 16dp 圆角,结果圆角占了一半高度,看起来会很奇怪。圆角值应该和组件尺寸成比例。 + +## 自定义圆角值 + +虽然 Material 定义了这 7 个标准值,但你的设计可能需要其他圆角尺寸。有两种处理方式: + +```cpp +// 方式 1:在主题配置中添加自定义 Token +// 这需要修改主题系统,但可以保持代码的语义化 + +// 方式 2:直接使用数值(不推荐,但现实) +// 对于特别的设计需求,有时候直接传值更实际 +button.setCornerRadius(20.0f); // 自定义值 +```text + +如果团队有统一的设计规范,建议把常用的自定义圆角也加到 Token 系统里,保持代码的语义化。 + +## 批量遍历 + +提供了遍历所有圆角 Token 的辅助: + +```cpp +// 遍历所有 Radius Token +for (size_t i = 0; i < RADIUS_TOKEN_COUNT; ++i) { + printf("Radius Token %zu: %s\n", i, ALL_RADIUS_TOKENS[i]); +} +```text + +## 响应式考虑 + +圆角的 dp 值在不同密度的屏幕上会自动缩放,这是 UI 框架的责任。如果你的框架不支持 dp/sp 单位,需要手动处理密度缩放: + +```cpp +// 伪代码:手动处理密度缩放 +float scale = getDeviceDensity(); +float radiusInPx = theme.resolveRadius(CORNER_MEDIUM) * scale; +view.setCornerRadiusPx(radiusInPx); +```text + +## 相关文档 + +- [Material Token 字面量](../material_scheme/cfmaterial_token_literals.md) +- [Typography Token 字面量](../typography/cfmaterial_typography_token_literals.md) +- [Motion Token 字面量](../motion/cfmaterial_motion_token_literals.md) diff --git a/document/HandBook/ui/core/token/radius_scale/index.md b/document/HandBook/ui/core/token/radius_scale/index.md index d96b23d79..83511171a 100644 --- a/document/HandBook/ui/core/token/radius_scale/index.md +++ b/document/HandBook/ui/core/token/radius_scale/index.md @@ -1,10 +1,11 @@ -# Radius Scale - -> Welcome to the Radius Scale section. +--- +title: 圆角系统 +description: 本章节详细描述 Material Design 3 的圆角 Token,定义了从 到 的多级圆角 +--- -## Overview +# 圆角系统 -Documentation and resources for Radius Scale. +本章节详细描述 Material Design 3 的圆角 Token,定义了从 `none` 到 `full` 的多级圆角尺度。圆角系统通过统一的 Token 配置确保各组件的圆角风格协调一致,支持主题级别的全局调整。 --- diff --git a/document/HandBook/ui/core/token/typography/.pages b/document/HandBook/ui/core/token/typography/.pages deleted file mode 100644 index bd665b0ad..000000000 --- a/document/HandBook/ui/core/token/typography/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: 排版 Token -nav: - - index.md - - Typography Token 字面量: cfmaterial_typography_token_literals.md diff --git a/document/HandBook/ui/core/token/typography/cfmaterial_typography_token_literals.md b/document/HandBook/ui/core/token/typography/cfmaterial_typography_token_literals.md index 4c72b8276..146fdf1bd 100644 --- a/document/HandBook/ui/core/token/typography/cfmaterial_typography_token_literals.md +++ b/document/HandBook/ui/core/token/typography/cfmaterial_typography_token_literals.md @@ -1,155 +1,160 @@ -# Typography Token 字面量 - -`cfmaterial_typography_token_literals.h` 定义了 Material Design 3 排版系统的全部 Token 字面量。Material 的字体规范不是简单的"字号/行高",而是一套完整的视觉层次系统,每个样式都有明确的用途场景。 - -这套字面量让我们可以用语义化的方式引用字体样式,而不是硬编码 "16sp" 这种魔法数字。更重要的是,当需要调整字体规范时,只需修改主题配置,不需要改动业务代码。 - -## 五大类型体系 - -Material Design 3 把文字样式分为五个类型:Display、Headline、Title、Body、Label。每个类型下有 Large/Medium/Small 三个变体,共 15 个样式: - -```cpp -#include "ui/core/token/typography/cfmaterial_typography_token_literals.h" - -using namespace cf::ui::core::token::literals; - -// Display - 英雄内容,用于非常大的展示性文字 -const char* displayLarge = TYPOGRAPHY_DISPLAY_LARGE; // 57sp, 落地页标题 -const char* displayMedium = TYPOGRAPHY_DISPLAY_MEDIUM; // 45sp -const char* displaySmall = TYPOGRAPHY_DISPLAY_SMALL; // 36sp - -// Headline - 应用栏重要文字 -const char* headlineLarge = TYPOGRAPHY_HEADLINE_LARGE; // 32sp, 页面主标题 -const char* headlineMedium = TYPOGRAPHY_HEADLINE_MEDIUM; // 28sp -const char* headlineSmall = TYPOGRAPHY_HEADLINE_SMALL; // 24sp, 次级标题 - -// Title - 分区标题 -const char* titleLarge = TYPOGRAPHY_TITLE_LARGE; // 22sp, 区块标题 -const char* titleMedium = TYPOGRAPHY_TITLE_MEDIUM; // 16sp, 卡片标题 -const char* titleSmall = TYPOGRAPHY_TITLE_SMALL; // 14sp, 小标题 - -// Body - 主要内容 -const char* bodyLarge = TYPOGRAPHY_BODY_LARGE; // 16sp, 正文 -const char* bodyMedium = TYPOGRAPHY_BODY_MEDIUM; // 14sp, 次要正文 -const char* bodySmall = TYPOGRAPHY_BODY_SMALL; // 12sp, 辅助文字 - -// Label - 次要信息,按钮和标签文字 -const char* labelLarge = TYPOGRAPHY_LABEL_LARGE; // 14sp, 按钮文字 -const char* labelMedium = TYPOGRAPHY_LABEL_MEDIUM; // 12sp, 标签 -const char* labelSmall = TYPOGRAPHY_LABEL_SMALL; // 11sp, 小标签 -``` - -## 字体属性说明 - -每个 Typography Token 不仅仅是字号,还包含字重和行高: - -| Token | 字号 | 字重 | 行高 | 典型用途 | -|-------|------|------|------|----------| -| Display Large | 57sp | 400 | 64sp | 落地页 Hero 标题 | -| Headline Large | 32sp | 400 | 40sp | 应用栏标题 | -| Title Large | 22sp | 500 | 28sp | 分区标题 | -| Body Large | 16sp | 400 | 24sp | 正文内容 | -| Label Large | 14sp | 500 | 20sp | 按钮文字 | - -注意到 Title 和 Label 系列的字重是 500(Medium),比其他系列的 400(Regular)更重。这是刻意的设计——标题和标签需要更强的视觉权重来引导注意力。 - -## 在主题系统中使用 - -排版 Token 的使用方式和颜色 Token 类似,由主题系统解析成实际的字体属性: - -```cpp -// 伪代码:主题系统如何解析排版 Token -struct TextStyle { - float fontSize; // sp - int fontWeight; // 100-900 - float lineHeight; // sp -}; - -class MaterialTheme { - TextStyle resolveTypography(const char* token) const { - return typographyTable.at(token); - } -}; - -// 使用示例 -MaterialTheme theme; - -// 设置按钮文字 -auto buttonStyle = theme.resolveTypography(TYPOGRAPHY_LABEL_LARGE); -button.setFontSize(buttonStyle.fontSize); -button.setFontWeight(buttonStyle.fontWeight); - -// 设置正文 -auto bodyStyle = theme.resolveTypography(TYPOGRAPHY_BODY_MEDIUM); -textLabel.setTextStyle(bodyStyle); -``` - -## 行高 Token - -除了排版 Token,还提供了独立的行高 Token。这看起来有点冗余——为什么行高不能包含在 Typography Token 里? - -原因是一些平台或渲染引擎可能需要单独设置行高属性。把它拆出来可以提供更灵活的配置: - -```cpp -// 行高 Token -const char* lineHeightBody = LINEHEIGHT_BODY_MEDIUM; // "md.lineHeight.bodyMedium" -const char* lineHeightTitle = LINEHEIGHT_TITLE_LARGE; // "md.lineHeight.titleLarge" - -// 使用场景:某些框架需要分别设置 -theme.setTypography(TYPOGRAPHY_BODY_MEDIUM); -theme.setLineHeight(LINEHEIGHT_BODY_MEDIUM); -``` - -不过在我们的推荐使用方式中,行高应该由 Typography Token 统一管理,单独使用行高 Token 是比较边缘的场景。 - -## 选择合适的样式 - -选择哪个样式有时会让人犹豫,这里有一些经验法则: - -```cpp -// 场景 1:页面主标题 -// 选 Headline Large,而不是 Display Large -// Display 太大了,除非是落地页那种非常突出的 Hero 区域 - -// 场景 2:卡片标题 -// 选 Title Medium(16sp),而不是 Headline Small -// Headline 是给应用栏那种页面级标题用的 - -// 场景 3:正文内容 -// 选 Body Medium(14sp),这是最常用的正文尺寸 -// 如果想要更突出的内容,用 Body Large(16sp) - -// 场景 4:按钮文字 -// 选 Label Large(14sp),这是专门为按钮设计的 -// 虽然尺寸和 Body Medium 一样,但字重不同(500 vs 400) - -// 场景 5:时间戳、元数据 -// 选 Label Small(11sp)或 Body Small(12sp) -// Label Small 字重更重,适合标签;Body Small 更轻,适合辅助信息 -``` - -## 批量遍历 - -和颜色 Token 一样,提供了遍历所有排版 Token 的辅助: - -```cpp -// 遍历所有 Typography Token -for (size_t i = 0; i < TYPOGRAPHY_TOKEN_COUNT; ++i) { - printf("Typography Token %zu: %s\n", i, ALL_TYPOGRAPHY_TOKENS[i]); -} - -// 遍历所有 LineHeight Token -for (size_t i = 0; i < LINEHEIGHT_TOKEN_COUNT; ++i) { - printf("LineHeight Token %zu: %s\n", i, ALL_LINEHEIGHT_TOKENS[i]); -} -``` - -## 可访问性考虑 - -Material Design 的字体规范考虑了可访问性。最小字号是 11sp(Label Small),这个尺寸在大多数设备上仍然可读。如果你的用户群体包含视力障碍用户,可以在主题配置中整体放大字号,同时保持样式之间的比例关系。 - -## 相关文档 - -- [Material Token 字面量](../material_scheme/cfmaterial_token_literals.md) -- [Radius Scale Token 字面量](../radius_scale/cfmaterial_radius_scale_literals.md) -- [Motion Token 字面量](../motion/cfmaterial_motion_token_literals.md) +--- +title: Typography Token 字面量 +description: 定义了 Material Design 3 排版系统的全部 Token 字面量。Material 的 +--- + +# Typography Token 字面量 + +`cfmaterial_typography_token_literals.h` 定义了 Material Design 3 排版系统的全部 Token 字面量。Material 的字体规范不是简单的"字号/行高",而是一套完整的视觉层次系统,每个样式都有明确的用途场景。 + +这套字面量让我们可以用语义化的方式引用字体样式,而不是硬编码 "16sp" 这种魔法数字。更重要的是,当需要调整字体规范时,只需修改主题配置,不需要改动业务代码。 + +## 五大类型体系 + +Material Design 3 把文字样式分为五个类型:Display、Headline、Title、Body、Label。每个类型下有 Large/Medium/Small 三个变体,共 15 个样式: + +```cpp +#include "ui/core/token/typography/cfmaterial_typography_token_literals.h" + +using namespace cf::ui::core::token::literals; + +// Display - 英雄内容,用于非常大的展示性文字 +const char* displayLarge = TYPOGRAPHY_DISPLAY_LARGE; // 57sp, 落地页标题 +const char* displayMedium = TYPOGRAPHY_DISPLAY_MEDIUM; // 45sp +const char* displaySmall = TYPOGRAPHY_DISPLAY_SMALL; // 36sp + +// Headline - 应用栏重要文字 +const char* headlineLarge = TYPOGRAPHY_HEADLINE_LARGE; // 32sp, 页面主标题 +const char* headlineMedium = TYPOGRAPHY_HEADLINE_MEDIUM; // 28sp +const char* headlineSmall = TYPOGRAPHY_HEADLINE_SMALL; // 24sp, 次级标题 + +// Title - 分区标题 +const char* titleLarge = TYPOGRAPHY_TITLE_LARGE; // 22sp, 区块标题 +const char* titleMedium = TYPOGRAPHY_TITLE_MEDIUM; // 16sp, 卡片标题 +const char* titleSmall = TYPOGRAPHY_TITLE_SMALL; // 14sp, 小标题 + +// Body - 主要内容 +const char* bodyLarge = TYPOGRAPHY_BODY_LARGE; // 16sp, 正文 +const char* bodyMedium = TYPOGRAPHY_BODY_MEDIUM; // 14sp, 次要正文 +const char* bodySmall = TYPOGRAPHY_BODY_SMALL; // 12sp, 辅助文字 + +// Label - 次要信息,按钮和标签文字 +const char* labelLarge = TYPOGRAPHY_LABEL_LARGE; // 14sp, 按钮文字 +const char* labelMedium = TYPOGRAPHY_LABEL_MEDIUM; // 12sp, 标签 +const char* labelSmall = TYPOGRAPHY_LABEL_SMALL; // 11sp, 小标签 +```bash + +## 字体属性说明 + +每个 Typography Token 不仅仅是字号,还包含字重和行高: + +| Token | 字号 | 字重 | 行高 | 典型用途 | +|-------|------|------|------|----------| +| Display Large | 57sp | 400 | 64sp | 落地页 Hero 标题 | +| Headline Large | 32sp | 400 | 40sp | 应用栏标题 | +| Title Large | 22sp | 500 | 28sp | 分区标题 | +| Body Large | 16sp | 400 | 24sp | 正文内容 | +| Label Large | 14sp | 500 | 20sp | 按钮文字 | + +注意到 Title 和 Label 系列的字重是 500(Medium),比其他系列的 400(Regular)更重。这是刻意的设计——标题和标签需要更强的视觉权重来引导注意力。 + +## 在主题系统中使用 + +排版 Token 的使用方式和颜色 Token 类似,由主题系统解析成实际的字体属性: + +```cpp +// 伪代码:主题系统如何解析排版 Token +struct TextStyle { + float fontSize; // sp + int fontWeight; // 100-900 + float lineHeight; // sp +}; + +class MaterialTheme { + TextStyle resolveTypography(const char* token) const { + return typographyTable.at(token); + } +}; + +// 使用示例 +MaterialTheme theme; + +// 设置按钮文字 +auto buttonStyle = theme.resolveTypography(TYPOGRAPHY_LABEL_LARGE); +button.setFontSize(buttonStyle.fontSize); +button.setFontWeight(buttonStyle.fontWeight); + +// 设置正文 +auto bodyStyle = theme.resolveTypography(TYPOGRAPHY_BODY_MEDIUM); +textLabel.setTextStyle(bodyStyle); +```text + +## 行高 Token + +除了排版 Token,还提供了独立的行高 Token。这看起来有点冗余——为什么行高不能包含在 Typography Token 里? + +原因是一些平台或渲染引擎可能需要单独设置行高属性。把它拆出来可以提供更灵活的配置: + +```cpp +// 行高 Token +const char* lineHeightBody = LINEHEIGHT_BODY_MEDIUM; // "md.lineHeight.bodyMedium" +const char* lineHeightTitle = LINEHEIGHT_TITLE_LARGE; // "md.lineHeight.titleLarge" + +// 使用场景:某些框架需要分别设置 +theme.setTypography(TYPOGRAPHY_BODY_MEDIUM); +theme.setLineHeight(LINEHEIGHT_BODY_MEDIUM); +```text + +不过在我们的推荐使用方式中,行高应该由 Typography Token 统一管理,单独使用行高 Token 是比较边缘的场景。 + +## 选择合适的样式 + +选择哪个样式有时会让人犹豫,这里有一些经验法则: + +```cpp +// 场景 1:页面主标题 +// 选 Headline Large,而不是 Display Large +// Display 太大了,除非是落地页那种非常突出的 Hero 区域 + +// 场景 2:卡片标题 +// 选 Title Medium(16sp),而不是 Headline Small +// Headline 是给应用栏那种页面级标题用的 + +// 场景 3:正文内容 +// 选 Body Medium(14sp),这是最常用的正文尺寸 +// 如果想要更突出的内容,用 Body Large(16sp) + +// 场景 4:按钮文字 +// 选 Label Large(14sp),这是专门为按钮设计的 +// 虽然尺寸和 Body Medium 一样,但字重不同(500 vs 400) + +// 场景 5:时间戳、元数据 +// 选 Label Small(11sp)或 Body Small(12sp) +// Label Small 字重更重,适合标签;Body Small 更轻,适合辅助信息 +```text + +## 批量遍历 + +和颜色 Token 一样,提供了遍历所有排版 Token 的辅助: + +```cpp +// 遍历所有 Typography Token +for (size_t i = 0; i < TYPOGRAPHY_TOKEN_COUNT; ++i) { + printf("Typography Token %zu: %s\n", i, ALL_TYPOGRAPHY_TOKENS[i]); +} + +// 遍历所有 LineHeight Token +for (size_t i = 0; i < LINEHEIGHT_TOKEN_COUNT; ++i) { + printf("LineHeight Token %zu: %s\n", i, ALL_LINEHEIGHT_TOKENS[i]); +} +```text + +## 可访问性考虑 + +Material Design 的字体规范考虑了可访问性。最小字号是 11sp(Label Small),这个尺寸在大多数设备上仍然可读。如果你的用户群体包含视力障碍用户,可以在主题配置中整体放大字号,同时保持样式之间的比例关系。 + +## 相关文档 + +- [Material Token 字面量](../material_scheme/cfmaterial_token_literals.md) +- [Radius Scale Token 字面量](../radius_scale/cfmaterial_radius_scale_literals.md) +- [Motion Token 字面量](../motion/cfmaterial_motion_token_literals.md) diff --git a/document/HandBook/ui/core/token/typography/index.md b/document/HandBook/ui/core/token/typography/index.md index 83c1b50ca..95fc50ce2 100644 --- a/document/HandBook/ui/core/token/typography/index.md +++ b/document/HandBook/ui/core/token/typography/index.md @@ -1,10 +1,11 @@ -# Typography - -> Welcome to the Typography section. +--- +title: 排版系统 +description: 本章节详细描述 Material Design 3 的排版 Token,包括字体族(Font Fam +--- -## Overview +# 排版系统 -Documentation and resources for Typography. +本章节详细描述 Material Design 3 的排版 Token,包括字体族(Font Family)、字重(Font Weight)、字号(Font Size)和行高(Line Height)等排版属性的 Token 化配置。排版系统确保跨组件的文本呈现一致性。 --- diff --git a/document/HandBook/ui/how-to-develop-widget.md b/document/HandBook/ui/how-to-develop-widget.md index e69de29bb..1f8d9a19d 100644 --- a/document/HandBook/ui/how-to-develop-widget.md +++ b/document/HandBook/ui/how-to-develop-widget.md @@ -0,0 +1,5 @@ +--- +title: how to develop widget +description: how to develop widget 的详细文档 +--- + diff --git a/document/HandBook/ui/index.md b/document/HandBook/ui/index.md index 4bd538fac..4ed4aa8fd 100644 --- a/document/HandBook/ui/index.md +++ b/document/HandBook/ui/index.md @@ -1,3 +1,8 @@ +--- +title: UI 框架 +description: CFDesktop 的 UI 框架是一套基于 Material Design 3 规范的完整实现,采 +--- + # UI 框架 CFDesktop 的 UI 框架是一套基于 Material Design 3 规范的完整实现,采用五层架构设计,每一层职责明确、可独立测试。 diff --git a/document/HandBook/ui/material/.pages b/document/HandBook/ui/material/.pages deleted file mode 100644 index 6fd687460..000000000 --- a/document/HandBook/ui/material/.pages +++ /dev/null @@ -1,13 +0,0 @@ -title: Material 设计 -icon: material/shape -nav: - - 动画: animation - - 基础: base - - Widget: widget - - 主题配置: cfmaterial_theme.md - - 配色方案: cfmaterial_scheme.md - - 字体类型: cfmaterial_fonttype.md - - 运动规格: cfmaterial_motion.md - - 圆角缩放: cfmaterial_radius_scale.md - - 工厂类: material_factory_class.md - - 工厂头文件: material_factory_hpp.md diff --git a/document/HandBook/ui/material/animation/.pages b/document/HandBook/ui/material/animation/.pages deleted file mode 100644 index d55591f21..000000000 --- a/document/HandBook/ui/material/animation/.pages +++ /dev/null @@ -1,7 +0,0 @@ -title: 动画 -nav: - - 动画工厂: cfmaterial_animation_factory.md - - 动画策略: cfmaterial_animation_strategy.md - - 淡入淡出: cfmaterial_fade_animation.md - - 缩放动画: cfmaterial_scale_animation.md - - 滑动动画: cfmaterial_slide_animation.md diff --git a/document/HandBook/ui/material/animation/cfmaterial_animation_factory.md b/document/HandBook/ui/material/animation/cfmaterial_animation_factory.md index e008ef5bb..f9ce4daf8 100644 --- a/document/HandBook/ui/material/animation/cfmaterial_animation_factory.md +++ b/document/HandBook/ui/material/animation/cfmaterial_animation_factory.md @@ -1,159 +1,164 @@ -# CFMaterialAnimationFactory - Material 动画工厂 - -`CFMaterialAnimationFactory` 是 Material Design 3 动画系统的核心入口,负责创建和管理符合 Material 规范的动画实例。我们设计这个工厂的初衷是让使用者不需要直接面对复杂的动画参数配置,而是通过一个语义化的 token(如 `"md.animation.fadeIn"`)就能获取到完整配置好的动画。 - -## Material Design 3 运动规范映射 - -工厂内部完整实现了 Material Design 3 的 Motion System 规范。每个动画都对应一个 motion token,这个 token 决定了动画的持续时间和缓动曲线: - -| Motion Token | 对应规范 | 典型用途 | -|--------------|----------|----------| -| `md.motion.shortEnter` | 200ms + EmphasizedDecelerate | 小元素淡入、缩放入场 | -| `md.motion.shortExit` | 150ms + EmphasizedAccelerate | 小元素淡出、缩放退场 | -| `md.motion.mediumEnter` | 300ms + EmphasizedDecelerate | 列表项滑入、旋转入场 | -| `md.motion.mediumExit` | 250ms + EmphasizedAccelerate | 列表项滑出、旋转退场 | - -这些映射关系在 `animation_token_mapping.h` 中定义,添加新动画类型时需要同步更新。 - -## 基本用法 - -工厂需要一个 Theme 实例来初始化,因为动画的时序参数(duration、easing)是从 MotionSpec 中查询的: - -```cpp -#include "ui/components/material/cfmaterial_animation_factory.h" -#include "ui/core/theme.h" - -using namespace cf::ui::components::material; -using namespace cf::ui::core; - -// 1. 准备主题(通常在应用初始化时完成) -MaterialFactory themeFactory; -auto theme = themeFactory.fromName("theme.material.light"); - -// 2. 创建动画工厂 -auto factory = std::make_unique(*theme); - -// 3. 通过 token 获取动画 -auto fadeIn = factory->getAnimation("md.animation.fadeIn"); -if (fadeIn) { - fadeIn->setTargetWidget(myWidget); - fadeIn->start(); -} -``` - -工厂拥有创建的动画实例,返回的是 `WeakPtr`。这个设计是为了避免悬空指针——如果工厂被销毁,所有返回的 WeakPtr 都会自动失效。 - -## 可用动画 Token - -当前支持的预定义动画 token 都在 `cf::ui::components::material::token_literals` 命名空间下: - -```cpp -using namespace cf::ui::components::material::token_literals; - -// 淡入淡出 -factory->getAnimation(ANIMATION_FADE_IN); // md.animation.fadeIn -factory->getAnimation(ANIMATION_FADE_OUT); // md.animation.fadeOut - -// 滑动 -factory->getAnimation(ANIMATION_SLIDE_UP); // md.animation.slideUp -factory->getAnimation(ANIMATION_SLIDE_DOWN); // md.animation.slideDown -factory->getAnimation(ANIMATION_SLIDE_LEFT); // md.animation.slideLeft -factory->getAnimation(ANIMATION_SLIDE_RIGHT); // md.animation.slideRight - -// 缩放 -factory->getAnimation(ANIMATION_SCALE_UP); // md.animation.scaleUp -factory->getAnimation(ANIMATION_SCALE_DOWN); // md.animation.scaleDown - -// 旋转 -factory->getAnimation(ANIMATION_ROTATE_IN); // md.animation.rotateIn -factory->getAnimation(ANIMATION_ROTATE_OUT); // md.animation.rotateOut -``` - -## 动画策略模式 - -工厂支持通过策略模式在不同组件类型间定制动画行为。策略允许你修改动画的参数而不需要改变工厂的接口: - -```cpp -// 自定义策略:按钮使用更短的动画时间 -class ButtonAnimationStrategy : public AnimationStrategy { -public: - AnimationDescriptor adjust(const AnimationDescriptor& desc, - QWidget* widget) override { - AnimationDescriptor adjusted = desc; - - // 按钮的滑动动画也用 shortEnter 而不是 mediumEnter - if (qobject_cast(widget)) { - if (strcmp(desc.motionToken, "md.motion.mediumEnter") == 0) { - adjusted.motionToken = "md.motion.shortEnter"; - } - } - return adjusted; - } -}; - -// 设置策略 -factory->setStrategy(std::make_unique()); -``` - -策略在动画创建之前被调用,所以你可以基于 widget 的状态、尺寸或者任何你关心的条件来调整动画参数。 - -## 自定义动画描述符 - -如果预定义的 token 不能满足需求,你可以用 `AnimationDescriptor` 手动构建动画配置: - -```cpp -// 创建一个自定义的淡入动画,使用 longEnter 时长 -AnimationDescriptor desc{ - "fade", // 动画类型 - "md.motion.longEnter", // motion token(需要在主题中定义) - "opacity", // 目标属性 - 0.0f, // 起始值 - 1.0f, // 结束值 - 100 // 延迟 100ms -}; - -auto customFade = factory->createAnimation(desc, myWidget); -if (customFade) { - customFade->start(); -} -``` - -⚠️ 注意:`createAnimation()` 总是创建新的动画实例,而 `getAnimation()` 会复用已存在的实例。如果你需要多个独立的动画实例(比如同时控制多个 widget),应该用 `createAnimation()`。 - -## 全局开关 - -工厂提供了全局启用/禁用动画的能力,这在性能敏感场景或无障碍需求下很有用: - -```cpp -// 沉重计算期间禁用动画提升性能 -factory->setEnabledAll(false); -// ... 做一些耗时操作 ... -factory->setEnabledAll(true); - -// 或者监听系统的"减少动画"设置 -if (QGuiApplication::styleHints()->showIsFullScreen() == false) { - factory->setEnabledAll(false); -} -``` - -`setEnabledAll()` 只影响新创建的动画,已经运行的动画不会被中断。 - -## 生命周期管理 - -工厂使用 `unique_ptr` 拥有所有创建的动画,返回给用户的是 `WeakPtr`。这个设计的含义是: - -```cpp -auto anim = factory->getAnimation("md.animation.fadeIn"); -// ... 工厂被销毁 ... -if (anim) { // 这里会失败,WeakPtr 已经失效 - anim->start(); -} -``` - -⚠️ 这个坑在异步代码里尤其容易出现——如果你把 WeakPtr 存起来延迟使用,一定要检查有效性。 - -## 相关文档 - -- [AnimationStrategy - 动画策略](./cfmaterial_animation_strategy.md) -- [动画 Token 映射](../../core/token/motion/cfmaterial_motion_token_literals.md) -- [Material Design 3 运动规范](https://m3.material.io/styles/motion) +--- +title: "CFMaterialAnimationFactory - Material 动画工厂" +description: 是 Material Design 3 动画系统的核心入口,负责创建和管理符合 Material 规 +--- + +# CFMaterialAnimationFactory - Material 动画工厂 + +`CFMaterialAnimationFactory` 是 Material Design 3 动画系统的核心入口,负责创建和管理符合 Material 规范的动画实例。我们设计这个工厂的初衷是让使用者不需要直接面对复杂的动画参数配置,而是通过一个语义化的 token(如 `"md.animation.fadeIn"`)就能获取到完整配置好的动画。 + +## Material Design 3 运动规范映射 + +工厂内部完整实现了 Material Design 3 的 Motion System 规范。每个动画都对应一个 motion token,这个 token 决定了动画的持续时间和缓动曲线: + +| Motion Token | 对应规范 | 典型用途 | +|--------------|----------|----------| +| `md.motion.shortEnter` | 200ms + EmphasizedDecelerate | 小元素淡入、缩放入场 | +| `md.motion.shortExit` | 150ms + EmphasizedAccelerate | 小元素淡出、缩放退场 | +| `md.motion.mediumEnter` | 300ms + EmphasizedDecelerate | 列表项滑入、旋转入场 | +| `md.motion.mediumExit` | 250ms + EmphasizedAccelerate | 列表项滑出、旋转退场 | + +这些映射关系在 `animation_token_mapping.h` 中定义,添加新动画类型时需要同步更新。 + +## 基本用法 + +工厂需要一个 Theme 实例来初始化,因为动画的时序参数(duration、easing)是从 MotionSpec 中查询的: + +```cpp +#include "ui/components/material/cfmaterial_animation_factory.h" +#include "ui/core/theme.h" + +using namespace cf::ui::components::material; +using namespace cf::ui::core; + +// 1. 准备主题(通常在应用初始化时完成) +MaterialFactory themeFactory; +auto theme = themeFactory.fromName("theme.material.light"); + +// 2. 创建动画工厂 +auto factory = std::make_unique(*theme); + +// 3. 通过 token 获取动画 +auto fadeIn = factory->getAnimation("md.animation.fadeIn"); +if (fadeIn) { + fadeIn->setTargetWidget(myWidget); + fadeIn->start(); +} +```text + +工厂拥有创建的动画实例,返回的是 `WeakPtr`。这个设计是为了避免悬空指针——如果工厂被销毁,所有返回的 WeakPtr 都会自动失效。 + +## 可用动画 Token + +当前支持的预定义动画 token 都在 `cf::ui::components::material::token_literals` 命名空间下: + +```cpp +using namespace cf::ui::components::material::token_literals; + +// 淡入淡出 +factory->getAnimation(ANIMATION_FADE_IN); // md.animation.fadeIn +factory->getAnimation(ANIMATION_FADE_OUT); // md.animation.fadeOut + +// 滑动 +factory->getAnimation(ANIMATION_SLIDE_UP); // md.animation.slideUp +factory->getAnimation(ANIMATION_SLIDE_DOWN); // md.animation.slideDown +factory->getAnimation(ANIMATION_SLIDE_LEFT); // md.animation.slideLeft +factory->getAnimation(ANIMATION_SLIDE_RIGHT); // md.animation.slideRight + +// 缩放 +factory->getAnimation(ANIMATION_SCALE_UP); // md.animation.scaleUp +factory->getAnimation(ANIMATION_SCALE_DOWN); // md.animation.scaleDown + +// 旋转 +factory->getAnimation(ANIMATION_ROTATE_IN); // md.animation.rotateIn +factory->getAnimation(ANIMATION_ROTATE_OUT); // md.animation.rotateOut +```text + +## 动画策略模式 + +工厂支持通过策略模式在不同组件类型间定制动画行为。策略允许你修改动画的参数而不需要改变工厂的接口: + +```cpp +// 自定义策略:按钮使用更短的动画时间 +class ButtonAnimationStrategy : public AnimationStrategy { +public: + AnimationDescriptor adjust(const AnimationDescriptor& desc, + QWidget* widget) override { + AnimationDescriptor adjusted = desc; + + // 按钮的滑动动画也用 shortEnter 而不是 mediumEnter + if (qobject_cast(widget)) { + if (strcmp(desc.motionToken, "md.motion.mediumEnter") == 0) { + adjusted.motionToken = "md.motion.shortEnter"; + } + } + return adjusted; + } +}; + +// 设置策略 +factory->setStrategy(std::make_unique()); +```text + +策略在动画创建之前被调用,所以你可以基于 widget 的状态、尺寸或者任何你关心的条件来调整动画参数。 + +## 自定义动画描述符 + +如果预定义的 token 不能满足需求,你可以用 `AnimationDescriptor` 手动构建动画配置: + +```cpp +// 创建一个自定义的淡入动画,使用 longEnter 时长 +AnimationDescriptor desc{ + "fade", // 动画类型 + "md.motion.longEnter", // motion token(需要在主题中定义) + "opacity", // 目标属性 + 0.0f, // 起始值 + 1.0f, // 结束值 + 100 // 延迟 100ms +}; + +auto customFade = factory->createAnimation(desc, myWidget); +if (customFade) { + customFade->start(); +} +```text + +⚠️ 注意:`createAnimation()` 总是创建新的动画实例,而 `getAnimation()` 会复用已存在的实例。如果你需要多个独立的动画实例(比如同时控制多个 widget),应该用 `createAnimation()`。 + +## 全局开关 + +工厂提供了全局启用/禁用动画的能力,这在性能敏感场景或无障碍需求下很有用: + +```cpp +// 沉重计算期间禁用动画提升性能 +factory->setEnabledAll(false); +// ... 做一些耗时操作 ... +factory->setEnabledAll(true); + +// 或者监听系统的"减少动画"设置 +if (QGuiApplication::styleHints()->showIsFullScreen() == false) { + factory->setEnabledAll(false); +} +```text + +`setEnabledAll()` 只影响新创建的动画,已经运行的动画不会被中断。 + +## 生命周期管理 + +工厂使用 `unique_ptr` 拥有所有创建的动画,返回给用户的是 `WeakPtr`。这个设计的含义是: + +```cpp +auto anim = factory->getAnimation("md.animation.fadeIn"); +// ... 工厂被销毁 ... +if (anim) { // 这里会失败,WeakPtr 已经失效 + anim->start(); +} +```text + +⚠️ 这个坑在异步代码里尤其容易出现——如果你把 WeakPtr 存起来延迟使用,一定要检查有效性。 + +## 相关文档 + +- [AnimationStrategy - 动画策略](./cfmaterial_animation_strategy.md) +- [动画 Token 映射](../../core/token/motion/cfmaterial_motion_token_literals.md) +- [Material Design 3 运动规范](https://m3.material.io/styles/motion) diff --git a/document/HandBook/ui/material/animation/cfmaterial_animation_strategy.md b/document/HandBook/ui/material/animation/cfmaterial_animation_strategy.md index a41755317..fa8354ee8 100644 --- a/document/HandBook/ui/material/animation/cfmaterial_animation_strategy.md +++ b/document/HandBook/ui/material/animation/cfmaterial_animation_strategy.md @@ -1,185 +1,190 @@ -# AnimationStrategy - Material 动画策略 - -`AnimationStrategy` 是 Material 动画系统的定制化接口,允许不同组件类型拥有自己的动画行为而不需要修改工厂代码。我们引入策略模式的背景是:在实际开发中发现,不同类型的 widget 对动画时长的敏感度差异很大——按钮需要快速响应,对话框可以更从容,而列表项则需要错落的节奏。 - -## 策略的作用时机 - -策略在动画创建流程的中间位置介入,时序如下: - -1. 用户通过 token 或 descriptor 请求动画 -2. 工厂解析 token 得到基础 `AnimationDescriptor` -3. **策略的 `adjust()` 被调用,可以修改 descriptor** -4. 工厂根据调整后的 descriptor 创建动画实例 -5. 返回动画的 WeakPtr 给用户 - -这个介入点让你有机会在动画真正创建之前改变它的任何参数:motion token、动画类型、属性、数值范围,甚至是延迟时间。 - -## 动画描述符 - -`AnimationDescriptor` 是策略的核心操作对象,它包含了创建一个动画所需的全部信息: - -```cpp -struct AnimationDescriptor { - const char* animationType; // "fade", "slide", "scale", "rotate" - const char* motionToken; // "md.motion.shortEnter", "md.motion.mediumEnter" 等 - const char* property; // "opacity", "positionX", "positionY", "scale" - float fromValue; // 起始值 - float toValue; // 结束值 - int delayMs = 0; // 延迟时间(毫秒) -}; -``` - -策略可以修改其中任何一个字段。比如你可以把一个 `slideUp` 改成 `fadeIn`,或者把 `mediumEnter` 时长替换为 `shortEnter`。 - -## 基础策略实现 - -最简单的策略是什么都不做,直接返回原始 descriptor: - -```cpp -#include "ui/components/material/cfmaterial_animation_strategy.h" - -using namespace cf::ui::components::material; - -class NoOpStrategy : public AnimationStrategy { -public: - AnimationDescriptor adjust(const AnimationDescriptor& desc, - QWidget* widget) override { - // 不做任何修改,原样返回 - return desc; - } -}; -``` - -这个实现其实就是 `DefaultAnimationStrategy` 的做法——当你不设置策略时,工厂默认使用的就是这个。 - -## 修改运动时长 - -最常见的定制需求是调整动画时长。Material Design 3 定义了四个标准时长等级,但不同组件可能需要不同的选择: - -```cpp -class FastButtonStrategy : public AnimationStrategy { -public: - AnimationDescriptor adjust(const AnimationDescriptor& desc, - QWidget* widget) override { - AnimationDescriptor adjusted = desc; - - // 按钮的动画全部使用 shortEnter/shortExit - if (qobject_cast(widget) || - qobject_cast(widget)) { - - if (strcmp(desc.motionToken, "md.motion.mediumEnter") == 0) { - adjusted.motionToken = "md.motion.shortEnter"; - } else if (strcmp(desc.motionToken, "md.motion.mediumExit") == 0) { - adjusted.motionToken = "md.motion.shortExit"; - } - } - return adjusted; - } -}; -``` - -Material Design 3 的时长标准是:shortEnter=200ms、mediumEnter=300ms、longEnter=400ms,对应的 exit 时长稍短一些。 - -## 条件禁用动画 - -策略还可以基于 widget 的状态决定是否启用动画,这对无障碍支持很有用: - -```cpp -class AccessibilityAwareStrategy : public AnimationStrategy { -public: - bool shouldEnable(QWidget* widget) const override { - // 检查系统的"减少动画"设置 - if (QGuiApplication::styleHints()->showIsFullScreen() == false) { - return false; - } - - // 或者基于 widget 特定条件 - if (widget && widget->property("suppressAnimation").toBool()) { - return false; - } - - return globalEnabled_; - } -}; -``` - -`shouldEnable()` 返回 false 时,工厂的 `getAnimation()` 和 `createAnimation()` 会返回无效的 WeakPtr。 - -## 组合多个条件 - -实际项目中,一个策略往往需要处理多种情况。我们建议用分步处理的方式保持代码清晰: - -```cpp -class SmartStrategy : public AnimationStrategy { -public: - AnimationDescriptor adjust(const AnimationDescriptor& desc, - QWidget* widget) override { - AnimationDescriptor adjusted = desc; - - // 步骤1:根据组件类型调整 - if (auto* btn = qobject_cast(widget)) { - adjusted = adjustForButton(adjusted, btn); - } else if (auto* combo = qobject_cast(widget)) { - adjusted = adjustForComboBox(adjusted, combo); - } - - // 步骤2:根据 widget 尺寸调整 - if (widget && widget->width() < 50) { - // 特别小的组件用更快的动画 - if (strcmp(adjusted.motionToken, "md.motion.mediumEnter") == 0) { - adjusted.motionToken = "md.motion.shortEnter"; - } - } - - return adjusted; - } - -private: - AnimationDescriptor adjustForButton(const AnimationDescriptor& desc, - QPushButton* btn) { - AnimationDescriptor result = desc; - // 按钮的滑动动画改为淡入(更轻量) - if (strcmp(desc.animationType, "slide") == 0) { - result.animationType = "fade"; - result.property = "opacity"; - result.fromValue = 0.0f; - result.toValue = 1.0f; - } - return result; - } - - AnimationDescriptor adjustForComboBox(const AnimationDescriptor& desc, - QComboBox* combo) { - // ComboBox 的定制逻辑... - return desc; - } -}; -``` - -## 全局启用状态 - -策略有一个 `globalEnabled_` 成员,可以通过 `setGlobalEnabled()` 修改: - -```cpp -strategy->setGlobalEnabled(false); // 禁用所有使用此策略的动画 -``` - -这个设置不会影响 `shouldEnable()` 的其他逻辑——你的实现仍然可以在 `globalEnabled_` 为 false 时返回 true。 - -⚠️ 不要在策略中存储 widget 指针。`adjust()` 和 `shouldEnable()` 接收的 widget 指针只在调用期间有效,存储它会导致悬空指针。如果需要持久化 widget 相关信息,应该用 QObject 的 property 机制或者其他方式关联。 - -## 性能考虑 - -策略的 `adjust()` 方法在每次创建动画时都会被调用,所以应该保持轻量。避免在策略中进行: - -- 复杂的计算或 I/O 操作 -- 动态内存分配 -- 耗时的查询操作 - -如果你需要基于一些复杂条件来做决策,建议预先计算好结果,用某种缓存机制存储起来,策略中只做简单的查找。 - -## 相关文档 - -- [CFMaterialAnimationFactory - 动画工厂](./cfmaterial_animation_factory.md) -- [AnimationDescriptor - 动画描述符](../../core/token/motion/cfmaterial_motion_token_literals.md) -- [Material Design 3 运动规范](https://m3.material.io/styles/motion) +--- +title: "AnimationStrategy - Material 动画策略" +description: 是 Material 动画系统的定制化接口,允许不同组件类型拥有自己的动画行为而不需要修改工厂代码。 +--- + +# AnimationStrategy - Material 动画策略 + +`AnimationStrategy` 是 Material 动画系统的定制化接口,允许不同组件类型拥有自己的动画行为而不需要修改工厂代码。我们引入策略模式的背景是:在实际开发中发现,不同类型的 widget 对动画时长的敏感度差异很大——按钮需要快速响应,对话框可以更从容,而列表项则需要错落的节奏。 + +## 策略的作用时机 + +策略在动画创建流程的中间位置介入,时序如下: + +1. 用户通过 token 或 descriptor 请求动画 +2. 工厂解析 token 得到基础 `AnimationDescriptor` +3. **策略的 `adjust()` 被调用,可以修改 descriptor** +4. 工厂根据调整后的 descriptor 创建动画实例 +5. 返回动画的 WeakPtr 给用户 + +这个介入点让你有机会在动画真正创建之前改变它的任何参数:motion token、动画类型、属性、数值范围,甚至是延迟时间。 + +## 动画描述符 + +`AnimationDescriptor` 是策略的核心操作对象,它包含了创建一个动画所需的全部信息: + +```cpp +struct AnimationDescriptor { + const char* animationType; // "fade", "slide", "scale", "rotate" + const char* motionToken; // "md.motion.shortEnter", "md.motion.mediumEnter" 等 + const char* property; // "opacity", "positionX", "positionY", "scale" + float fromValue; // 起始值 + float toValue; // 结束值 + int delayMs = 0; // 延迟时间(毫秒) +}; +```text + +策略可以修改其中任何一个字段。比如你可以把一个 `slideUp` 改成 `fadeIn`,或者把 `mediumEnter` 时长替换为 `shortEnter`。 + +## 基础策略实现 + +最简单的策略是什么都不做,直接返回原始 descriptor: + +```cpp +#include "ui/components/material/cfmaterial_animation_strategy.h" + +using namespace cf::ui::components::material; + +class NoOpStrategy : public AnimationStrategy { +public: + AnimationDescriptor adjust(const AnimationDescriptor& desc, + QWidget* widget) override { + // 不做任何修改,原样返回 + return desc; + } +}; +```text + +这个实现其实就是 `DefaultAnimationStrategy` 的做法——当你不设置策略时,工厂默认使用的就是这个。 + +## 修改运动时长 + +最常见的定制需求是调整动画时长。Material Design 3 定义了四个标准时长等级,但不同组件可能需要不同的选择: + +```cpp +class FastButtonStrategy : public AnimationStrategy { +public: + AnimationDescriptor adjust(const AnimationDescriptor& desc, + QWidget* widget) override { + AnimationDescriptor adjusted = desc; + + // 按钮的动画全部使用 shortEnter/shortExit + if (qobject_cast(widget) || + qobject_cast(widget)) { + + if (strcmp(desc.motionToken, "md.motion.mediumEnter") == 0) { + adjusted.motionToken = "md.motion.shortEnter"; + } else if (strcmp(desc.motionToken, "md.motion.mediumExit") == 0) { + adjusted.motionToken = "md.motion.shortExit"; + } + } + return adjusted; + } +}; +```text + +Material Design 3 的时长标准是:shortEnter=200ms、mediumEnter=300ms、longEnter=400ms,对应的 exit 时长稍短一些。 + +## 条件禁用动画 + +策略还可以基于 widget 的状态决定是否启用动画,这对无障碍支持很有用: + +```cpp +class AccessibilityAwareStrategy : public AnimationStrategy { +public: + bool shouldEnable(QWidget* widget) const override { + // 检查系统的"减少动画"设置 + if (QGuiApplication::styleHints()->showIsFullScreen() == false) { + return false; + } + + // 或者基于 widget 特定条件 + if (widget && widget->property("suppressAnimation").toBool()) { + return false; + } + + return globalEnabled_; + } +}; +```text + +`shouldEnable()` 返回 false 时,工厂的 `getAnimation()` 和 `createAnimation()` 会返回无效的 WeakPtr。 + +## 组合多个条件 + +实际项目中,一个策略往往需要处理多种情况。我们建议用分步处理的方式保持代码清晰: + +```cpp +class SmartStrategy : public AnimationStrategy { +public: + AnimationDescriptor adjust(const AnimationDescriptor& desc, + QWidget* widget) override { + AnimationDescriptor adjusted = desc; + + // 步骤1:根据组件类型调整 + if (auto* btn = qobject_cast(widget)) { + adjusted = adjustForButton(adjusted, btn); + } else if (auto* combo = qobject_cast(widget)) { + adjusted = adjustForComboBox(adjusted, combo); + } + + // 步骤2:根据 widget 尺寸调整 + if (widget && widget->width() < 50) { + // 特别小的组件用更快的动画 + if (strcmp(adjusted.motionToken, "md.motion.mediumEnter") == 0) { + adjusted.motionToken = "md.motion.shortEnter"; + } + } + + return adjusted; + } + +private: + AnimationDescriptor adjustForButton(const AnimationDescriptor& desc, + QPushButton* btn) { + AnimationDescriptor result = desc; + // 按钮的滑动动画改为淡入(更轻量) + if (strcmp(desc.animationType, "slide") == 0) { + result.animationType = "fade"; + result.property = "opacity"; + result.fromValue = 0.0f; + result.toValue = 1.0f; + } + return result; + } + + AnimationDescriptor adjustForComboBox(const AnimationDescriptor& desc, + QComboBox* combo) { + // ComboBox 的定制逻辑... + return desc; + } +}; +```text + +## 全局启用状态 + +策略有一个 `globalEnabled_` 成员,可以通过 `setGlobalEnabled()` 修改: + +```cpp +strategy->setGlobalEnabled(false); // 禁用所有使用此策略的动画 +```text + +这个设置不会影响 `shouldEnable()` 的其他逻辑——你的实现仍然可以在 `globalEnabled_` 为 false 时返回 true。 + +⚠️ 不要在策略中存储 widget 指针。`adjust()` 和 `shouldEnable()` 接收的 widget 指针只在调用期间有效,存储它会导致悬空指针。如果需要持久化 widget 相关信息,应该用 QObject 的 property 机制或者其他方式关联。 + +## 性能考虑 + +策略的 `adjust()` 方法在每次创建动画时都会被调用,所以应该保持轻量。避免在策略中进行: + +- 复杂的计算或 I/O 操作 +- 动态内存分配 +- 耗时的查询操作 + +如果你需要基于一些复杂条件来做决策,建议预先计算好结果,用某种缓存机制存储起来,策略中只做简单的查找。 + +## 相关文档 + +- [CFMaterialAnimationFactory - 动画工厂](./cfmaterial_animation_factory.md) +- [AnimationDescriptor - 动画描述符](../../core/token/motion/cfmaterial_motion_token_literals.md) +- [Material Design 3 运动规范](https://m3.material.io/styles/motion) diff --git a/document/HandBook/ui/material/animation/cfmaterial_fade_animation.md b/document/HandBook/ui/material/animation/cfmaterial_fade_animation.md index d0944a7e6..391e4fc89 100644 --- a/document/HandBook/ui/material/animation/cfmaterial_fade_animation.md +++ b/document/HandBook/ui/material/animation/cfmaterial_fade_animation.md @@ -1,166 +1,171 @@ -# CFMaterialFadeAnimation - Material 淡入淡出动画 - -`CFMaterialFadeAnimation` 是 Material Design 3 运动系统中的透明度动画实现,负责按照 Material 规范控制 widget 的不透明度变化。淡入淡出是 UI 中最基础的过渡效果,我们实现这个动画不仅是为了视觉美观,更重要的是在元素出现、消失或状态切换时给用户清晰的心理预期。 - -## 在 Material Design 3 中的定位 - -Material Design 3 的 Motion System 将淡入淡出归类为"强调性进入"和"强调性退出"的标准实现。与其他动画类型相比,淡入淡出不涉及位置或尺寸变化,是最"安静"的过渡方式,适合用于: - -- 模态对话框的出现和消失 -- 列表项的批量加载 -- 状态指示器的切换 -- 与其他动画组合使用时作为"透明度维度" - -## 基本用法 - -淡入淡出动画需要通过 `IMotionSpec` 获取时序参数,这个 spec 通常从主题的 MotionScheme 中获得: - -```cpp -#include "ui/components/material/cfmaterial_fade_animation.h" -#include "ui/core/theme.h" - -using namespace cf::ui::components::material; - -// 从主题获取 motion spec -auto& motionSpec = theme->motion_spec(); - -// 创建淡入动画 -auto fadeAnim = std::make_unique(&motionSpec, this); - -// 设置目标 widget -fadeAnim->setTargetWidget(myDialog); - -// 开始动画(Forward = 淡入,Backward = 淡出) -fadeAnim->start(Direction::Forward); -``` - -动画通过 `QGraphicsOpacityEffect` 作用于 widget,这意味着它适用于所有继承自 `QWidget` 的控件,包括那些本身不直接支持透明度属性的组件。 - -## 透明度范围 - -默认情况下,动画会在完全透明(0.0)和完全不透明(1.0)之间插值。如果你需要自定义范围(比如从半透明到完全不透明),可以通过继承或修改动画配置来实现: - -```cpp -// 动画内部使用 0.0 ~ 1.0 的标准范围 -// 如果需要部分淡入效果,需要在动画完成后手动设置最终状态 -connect(fadeAnim.get(), &ICFAbstractAnimation::finished, this, [widget]() { - // 设置为某个中间透明度值 - widget->setWindowOpacity(0.8); -}); -``` - -## 时序控制 - -动画的持续时间和缓动曲线由 MotionSpec 决定,工厂会根据动画类型自动选择合适的 motion token: - -| 场景 | 推荐时长 | 推荐缓动 | -|------|----------|----------| -| 小元素淡入(按钮、图标) | 200ms | EmphasizedDecelerate | -| 小元素淡出 | 150ms | EmphasizedAccelerate | -| 中等元素淡入(卡片、列表项) | 250ms | EmphasizedDecelerate | -| 大元素淡入(对话框、面板) | 300ms | EmphasizedDecelerate | - -## 与 QGraphicsOpacityEffect 的交互 - -动画会自动为 target widget 创建并管理 `QGraphicsOpacityEffect`。这个设计带来两个需要注意的点: - -第一,如果 widget 已经有 graphics effect,动画会尝试复用它而不是覆盖。这意味着你可以在同一个 effect 上组合多个属性动画: - -```cpp -// widget 已有一个模糊效果 -auto* blurEffect = new QGraphicsBlurEffect(widget); -widget->setGraphicsEffect(blurEffect); - -// 淡入动画会创建独立的 opacity effect -// 不会影响已有的模糊效果 -fadeAnim->setTargetWidget(widget); -``` - -第二,effect 的生命周期由动画管理。当动画销毁时,如果 effect 是它创建的,也会一并销毁。如果你需要在动画结束后保留最终的透明度状态,需要注意 effect 的所有权问题: - -```cpp -// 动画结束后 effect 会被清理,widget 会恢复到不透明状态 -// 如果需要保持半透明,考虑监听 finished 信号并手动设置样式 -``` - -## 生命周期控制 - -动画支持暂停、恢复、停止和反向操作: - -```cpp -fadeAnim->start(Direction::Forward); // 开始淡入 - -// ... 动画进行中 ... - -fadeAnim->pause(); // 暂停在当前状态 - -fadeAnim->reverse(); // 从当前状态反向播放(淡出) - -fadeAnim->stop(); // 停止并重置到初始状态 -``` - -`reverse()` 是一个很方便的操作,它会让动画从当前进度开始反向播放,而不是跳回起点。这在实现"鼠标悬停显示,移开隐藏"这类交互时特别好用。 - -## 性能考量 - -淡入淡出动画本身的开销很小,主要消耗在 `QGraphicsEffect` 的渲染上。如果你需要同时为大量 widget 应用淡入淡出(比如一个包含上百项的列表),考虑以下优化: - -1. 批量启动时使用不同的延迟,避免所有动画在同一帧更新 -2. 对于简单的透明度需求,考虑直接用 `QWidget::setWindowOpacity()` 或样式表 -3. 在低端设备上可以通过工厂的全局开关禁用动画 - -## 常见场景 - -### 模态对话框淡入 - -```cpp -void showModalDialog(QWidget* dialog) { - dialog->setWindowOpacity(0.0); - dialog->show(); - - auto fadeAnim = std::make_unique(&motionSpec); - fadeAnim->setTargetWidget(dialog); - fadeAnim->start(Direction::Forward); -} -``` - -### 加载状态切换 - -```cpp -void showLoadingIndicator() { - loadingLabel->show(); - auto fadeAnim = factory->getAnimation("md.animation.fadeIn"); - fadeAnim->setTargetWidget(loadingLabel); - fadeAnim->start(); -} - -void hideLoadingIndicator() { - auto fadeAnim = factory->getAnimation("md.animation.fadeOut"); - connect(fadeAnim.get(), &ICFAbstractAnimation::finished, loadingLabel, &QWidget::hide); - fadeAnim->setTargetWidget(loadingLabel); - fadeAnim->start(); -} -``` - -### 组合动画 - -淡入淡出常与其他动画组合使用,比如从下方滑入的同时淡入: - -```cpp -// 滑动动画和淡入动画同时启动 -auto slideAnim = factory->getAnimation("md.animation.slideUp"); -auto fadeAnim = factory->getAnimation("md.animation.fadeIn"); - -slideAnim->setTargetWidget(widget); -fadeAnim->setTargetWidget(widget); - -slideAnim->start(); -fadeAnim->start(); -``` - -## 相关文档 - -- [CFMaterialAnimationFactory - 动画工厂](./cfmaterial_animation_factory.md) -- [CFMaterialSlideAnimation - 滑动动画](./cfmaterial_slide_animation.md) -- [CFMaterialScaleAnimation - 缩放动画](./cfmaterial_scale_animation.md) -- [Material Design 3 运动规范](https://m3.material.io/styles/motion) +--- +title: "CFMaterialFadeAnimation - Material 淡入淡出动画" +description: 是 Material Design 3 运动系统中的透明度动画实现,负责按照 Material 规范 +--- + +# CFMaterialFadeAnimation - Material 淡入淡出动画 + +`CFMaterialFadeAnimation` 是 Material Design 3 运动系统中的透明度动画实现,负责按照 Material 规范控制 widget 的不透明度变化。淡入淡出是 UI 中最基础的过渡效果,我们实现这个动画不仅是为了视觉美观,更重要的是在元素出现、消失或状态切换时给用户清晰的心理预期。 + +## 在 Material Design 3 中的定位 + +Material Design 3 的 Motion System 将淡入淡出归类为"强调性进入"和"强调性退出"的标准实现。与其他动画类型相比,淡入淡出不涉及位置或尺寸变化,是最"安静"的过渡方式,适合用于: + +- 模态对话框的出现和消失 +- 列表项的批量加载 +- 状态指示器的切换 +- 与其他动画组合使用时作为"透明度维度" + +## 基本用法 + +淡入淡出动画需要通过 `IMotionSpec` 获取时序参数,这个 spec 通常从主题的 MotionScheme 中获得: + +```cpp +#include "ui/components/material/cfmaterial_fade_animation.h" +#include "ui/core/theme.h" + +using namespace cf::ui::components::material; + +// 从主题获取 motion spec +auto& motionSpec = theme->motion_spec(); + +// 创建淡入动画 +auto fadeAnim = std::make_unique(&motionSpec, this); + +// 设置目标 widget +fadeAnim->setTargetWidget(myDialog); + +// 开始动画(Forward = 淡入,Backward = 淡出) +fadeAnim->start(Direction::Forward); +```text + +动画通过 `QGraphicsOpacityEffect` 作用于 widget,这意味着它适用于所有继承自 `QWidget` 的控件,包括那些本身不直接支持透明度属性的组件。 + +## 透明度范围 + +默认情况下,动画会在完全透明(0.0)和完全不透明(1.0)之间插值。如果你需要自定义范围(比如从半透明到完全不透明),可以通过继承或修改动画配置来实现: + +```cpp +// 动画内部使用 0.0 ~ 1.0 的标准范围 +// 如果需要部分淡入效果,需要在动画完成后手动设置最终状态 +connect(fadeAnim.get(), &ICFAbstractAnimation::finished, this, [widget]() { + // 设置为某个中间透明度值 + widget->setWindowOpacity(0.8); +}); +```bash + +## 时序控制 + +动画的持续时间和缓动曲线由 MotionSpec 决定,工厂会根据动画类型自动选择合适的 motion token: + +| 场景 | 推荐时长 | 推荐缓动 | +|------|----------|----------| +| 小元素淡入(按钮、图标) | 200ms | EmphasizedDecelerate | +| 小元素淡出 | 150ms | EmphasizedAccelerate | +| 中等元素淡入(卡片、列表项) | 250ms | EmphasizedDecelerate | +| 大元素淡入(对话框、面板) | 300ms | EmphasizedDecelerate | + +## 与 QGraphicsOpacityEffect 的交互 + +动画会自动为 target widget 创建并管理 `QGraphicsOpacityEffect`。这个设计带来两个需要注意的点: + +第一,如果 widget 已经有 graphics effect,动画会尝试复用它而不是覆盖。这意味着你可以在同一个 effect 上组合多个属性动画: + +```cpp +// widget 已有一个模糊效果 +auto* blurEffect = new QGraphicsBlurEffect(widget); +widget->setGraphicsEffect(blurEffect); + +// 淡入动画会创建独立的 opacity effect +// 不会影响已有的模糊效果 +fadeAnim->setTargetWidget(widget); +```text + +第二,effect 的生命周期由动画管理。当动画销毁时,如果 effect 是它创建的,也会一并销毁。如果你需要在动画结束后保留最终的透明度状态,需要注意 effect 的所有权问题: + +```cpp +// 动画结束后 effect 会被清理,widget 会恢复到不透明状态 +// 如果需要保持半透明,考虑监听 finished 信号并手动设置样式 +```text + +## 生命周期控制 + +动画支持暂停、恢复、停止和反向操作: + +```cpp +fadeAnim->start(Direction::Forward); // 开始淡入 + +// ... 动画进行中 ... + +fadeAnim->pause(); // 暂停在当前状态 + +fadeAnim->reverse(); // 从当前状态反向播放(淡出) + +fadeAnim->stop(); // 停止并重置到初始状态 +```text + +`reverse()` 是一个很方便的操作,它会让动画从当前进度开始反向播放,而不是跳回起点。这在实现"鼠标悬停显示,移开隐藏"这类交互时特别好用。 + +## 性能考量 + +淡入淡出动画本身的开销很小,主要消耗在 `QGraphicsEffect` 的渲染上。如果你需要同时为大量 widget 应用淡入淡出(比如一个包含上百项的列表),考虑以下优化: + +1. 批量启动时使用不同的延迟,避免所有动画在同一帧更新 +2. 对于简单的透明度需求,考虑直接用 `QWidget::setWindowOpacity()` 或样式表 +3. 在低端设备上可以通过工厂的全局开关禁用动画 + +## 常见场景 + +### 模态对话框淡入 + +```cpp +void showModalDialog(QWidget* dialog) { + dialog->setWindowOpacity(0.0); + dialog->show(); + + auto fadeAnim = std::make_unique(&motionSpec); + fadeAnim->setTargetWidget(dialog); + fadeAnim->start(Direction::Forward); +} +```text + +### 加载状态切换 + +```cpp +void showLoadingIndicator() { + loadingLabel->show(); + auto fadeAnim = factory->getAnimation("md.animation.fadeIn"); + fadeAnim->setTargetWidget(loadingLabel); + fadeAnim->start(); +} + +void hideLoadingIndicator() { + auto fadeAnim = factory->getAnimation("md.animation.fadeOut"); + connect(fadeAnim.get(), &ICFAbstractAnimation::finished, loadingLabel, &QWidget::hide); + fadeAnim->setTargetWidget(loadingLabel); + fadeAnim->start(); +} +```text + +### 组合动画 + +淡入淡出常与其他动画组合使用,比如从下方滑入的同时淡入: + +```cpp +// 滑动动画和淡入动画同时启动 +auto slideAnim = factory->getAnimation("md.animation.slideUp"); +auto fadeAnim = factory->getAnimation("md.animation.fadeIn"); + +slideAnim->setTargetWidget(widget); +fadeAnim->setTargetWidget(widget); + +slideAnim->start(); +fadeAnim->start(); +```text + +## 相关文档 + +- [CFMaterialAnimationFactory - 动画工厂](./cfmaterial_animation_factory.md) +- [CFMaterialSlideAnimation - 滑动动画](./cfmaterial_slide_animation.md) +- [CFMaterialScaleAnimation - 缩放动画](./cfmaterial_scale_animation.md) +- [Material Design 3 运动规范](https://m3.material.io/styles/motion) diff --git a/document/HandBook/ui/material/animation/cfmaterial_scale_animation.md b/document/HandBook/ui/material/animation/cfmaterial_scale_animation.md index 91bfdc11d..19b019b5f 100644 --- a/document/HandBook/ui/material/animation/cfmaterial_scale_animation.md +++ b/document/HandBook/ui/material/animation/cfmaterial_scale_animation.md @@ -1,238 +1,243 @@ -# CFMaterialScaleAnimation - Material 缩放动画 - -`CFMaterialScaleAnimation` 是 Material Design 3 运动系统中的尺寸变换实现,控制 widget 以缩放方式出现或消失。与淡入淡出和滑动相比,缩放动画有更强的"强调"意味——它让元素感觉像是"从某个点生长出来",适合用于需要抓住用户注意力的重要交互。 - -## 在 Material Design 3 中的定位 - -Material Design 将缩放归类为"强调性变换",主要用于: - -| 场景 | 效果 | 语义 | -|------|------|------| -| 对话框、菜单弹出 | 从 0.8~0.9 放大到 1.0 | 聚焦,吸引用户注意 | -| 按钮按下效果 | 缩小到 0.95 | 触觉反馈 | -| FAB 展开 | 从小圆点放大成完整按钮 | 状态展开 | -| 图片查看 | 缩小/放大 | 空间导航 | - -缩放动画的核心是"中心锚点"——默认情况下,widget 会以其中心点为基准进行缩放,这符合人类对"生长"的直觉认知。 - -## 基本用法 - -缩放动画的构造比较简单,只需要 motion spec: - -```cpp -#include "ui/components/material/cfmaterial_scale_animation.h" - -using namespace cf::ui::components::material; - -// 创建一个缩放动画 -auto scaleAnim = std::make_unique(&motionSpec, this); - -// 设置目标 widget -scaleAnim->setTargetWidget(myDialog); - -// 开始动画(Forward = 放大,Backward = 缩小) -scaleAnim->start(Direction::Forward); -``` - -默认的缩放范围是 0.8 到 1.0(从 80% 放大到 100%),这是 Material 推荐的"弹出"效果参数。 - -## 中心锚点 - -缩放动画最关键的设计决策是锚点位置。我们默认从中心点缩放(`scaleFromCenter = true`),这样 widget 的中心位置保持不变,四周均匀缩放: - -```cpp -// 默认行为:从中心缩放 -auto scaleAnim = std::make_unique(&motionSpec); -scaleAnim->setScaleFromCenter(true); // 这是默认值 -scaleAnim->setTargetWidget(widget); -scaleAnim->start(); - -// widget 会保持中心位置不变,四周同时收缩/扩张 -``` - -如果需要从左上角或其他位置缩放,可以关闭中心缩放模式: - -```cpp -scaleAnim->setScaleFromCenter(false); -// 此时缩放会以 widget 几何原点(左上角)为锚点 -``` - -⚠️ 非中心缩放在某些布局下可能会导致 widget 位置发生明显偏移,使用时需要谨慎测试。 - -## 几何变换实现 - -我们的缩放是通过修改 widget 的 `setGeometry()` 实现的,而不是使用 `QTransform`。这个选择是经过权衡的: - -| 方案 | 优点 | 缺点 | -|------|------|------| -| setGeometry | 布局兼容性好,重绘正确 | 性能稍低,需要手动计算 | -| QTransform | 性能更好,硬件加速 | 可能导致布局错乱,边距问题 | - -使用 `setGeometry` 意味着动画过程中 widget 的实际几何属性在变化,布局系统和父容器会正确感知这个变化。如果你在动画期间需要查询 widget 的尺寸,得到的是实时缩放后的值: - -```cpp -scaleAnim->start(); -// ... 动画进行中 ... -qDebug() << widget->width(); // 当前缩放后的宽度 -qDebug() << widget->height(); // 当前缩放后的高度 -``` - -## 时序参数 - -缩放动画的时长选择取决于元素的"重要性"和"预期注意力": - -| 场景 | 推荐时长 | 推荐缓动 | -|------|----------|----------| -| 按钮/小图标 | 150ms | EmphasizedDecelerate | -| 对话框/卡片 | 200ms | EmphasizedDecelerate | -| 菜单/下拉列表 | 250ms | EmphasizedDecelerate | -| 全屏转场 | 300ms | EmphasizedDecelerate | - -缩放动画几乎总是用 `EmphasizedDecelerate` 缓动——快速启动、缓慢停止的效果能产生"有力但可控"的感觉。 - -## 常见使用场景 - -### 对话框弹出 - -```cpp -void showDialog(QWidget* dialog) { - // 初始状态:设置为缩小状态 - dialog->setGeometry( - dialog->x() + dialog->width() * 0.1, - dialog->y() + dialog->height() * 0.1, - dialog->width() * 0.8, - dialog->height() * 0.8 - ); - dialog->show(); - - // 缩放到正常大小 - auto scaleAnim = std::make_unique(&motionSpec); - scaleAnim->setTargetWidget(dialog); - scaleAnim->start(Direction::Forward); -} -``` - -### 按钮按下效果 - -```cpp -class PressButton : public QPushButton { -public: - void mousePressEvent(QMouseEvent* event) override { - // 按下时缩小 - auto scaleAnim = std::make_unique(&motionSpec); - scaleAnim->setTargetWidget(this); - scaleAnim->start(Direction::Backward); // 缩小 - QPushButton::mousePressEvent(event); - } - - void mouseReleaseEvent(QMouseEvent* event) override { - // 释放时恢复 - auto scaleAnim = std::make_unique(&motionSpec); - scaleAnim->setTargetWidget(this); - scaleAnim->start(Direction::Forward); // 放大 - QPushButton::mouseReleaseEvent(event); - } -}; -``` - -### 菜单展开 - -```cpp -void showMenu(QWidget* menu) { - // 菜单从按钮位置"生长"出来 - // 需要将菜单的缩放中心设置为靠近按钮的位置 - // 这可以通过初始几何属性调整实现 - - menu->show(); - - auto scaleAnim = std::make_unique(&motionSpec); - scaleAnim->setTargetWidget(menu); - scaleAnim->start(); -} -``` - -### 组合动画:弹出 + 淡入 - -缩放和淡入的组合是"出现"效果的经典组合: - -```cpp -void showCard(QWidget* card) { - card->show(); - - auto scaleAnim = factory->getAnimation("md.animation.scaleUp"); - auto fadeAnim = factory->getAnimation("md.animation.fadeIn"); - - scaleAnim->setTargetWidget(card); - fadeAnim->setTargetWidget(card); - - scaleAnim->start(); - fadeAnim->start(); -} -``` - -两个动画同步运行能产生"从无到有"的完整视觉体验——缩放负责空间维度,透明度负责视觉维度。 - -## 性能考量 - -缩放动画涉及 widget 几何属性的变化,可能触发父容器的重新布局: - -1. **避免在动画期间修改布局**:如果 widget 的父容器使用复杂布局,动画期间禁用自动更新可以提升性能 -2. **固定尺寸 widget 效果更好**:使用固定几何属性的 widget 动画更流畅 -3. **考虑淡入淡出替代**:对于简单场景,纯透明度动画性能更好且视觉效果足够 - -## 缩放值范围 - -虽然 Material 推荐的缩放范围是 0.8~1.0(弹出)或 1.0~0.8(收起),但你可以根据需要调整: - -```cpp -// 更明显的弹出效果:从 0.5 开始 -// 这需要修改动画配置或创建自定义动画 - -// 微妙的强调:从 0.95 开始 -// 适合小元素或需要低调的场景 -``` - -缩放到 1.0 以上(放大效果)在 Material Design 中比较少见,通常用于特殊交互如图片预览。这种场景下可能需要配合淡入淡出来避免突兀。 - -## 与布局管理器的交互 - -如果你的 widget 位于布局管理器中,缩放动画可能会与布局系统产生冲突: - -```cpp -// widget 在 QVBoxLayout 中 -auto layout = new QVBoxLayout(parent); -layout->addWidget(myWidget); - -// 缩放动画会改变 widget 的几何属性 -// 布局管理器可能会尝试"纠正"这个变化 -auto scaleAnim = std::make_unique(&motionSpec); -scaleAnim->setTargetWidget(myWidget); -scaleAnim->start(); // 可能导致布局抖动 -``` - -解决这个问题有几种方案: - -1. 动画期间临时移除布局约束 -2. 使用固定位置的 widget(不使用布局) -3. 在布局中插入占位 widget 来吸收尺寸变化 - -## 常见问题 - -**Q: 动画过程中 widget 闪烁** - -A: 可能是 `WA_OpaquePaintEvent` 或 `WA_NoSystemBackground` 属性设置不当。尝试设置 `widget->setAttribute(Qt::WA_OpaquePaintEvent)`。 - -**Q: 缩放后 widget 边缘模糊** - -A: 这是抗锯齿的正常现象。如果需要更清晰的边缘,可以启用 `AA_EnableHighDpiScaling` 或使用更高分辨率的资源。 - -**Q: 动画结束后布局被破坏** - -A: 动画会修改 widget 的几何属性。如果需要恢复布局,监听 `finished` 信号并在其中调用 `widget->setGeometry(originalGeometry)`。 - -## 相关文档 - -- [CFMaterialAnimationFactory - 动画工厂](./cfmaterial_animation_factory.md) -- [CFMaterialFadeAnimation - 淡入淡出动画](./cfmaterial_fade_animation.md) -- [CFMaterialSlideAnimation - 滑动动画](./cfmaterial_slide_animation.md) -- [Material Design 3 运动规范](https://m3.material.io/styles/motion) +--- +title: "CFMaterialScaleAnimation - Material 缩放动画" +description: 是 Material Design 3 运动系统中的尺寸变换实现,控制 widget 以缩放方式出现 +--- + +# CFMaterialScaleAnimation - Material 缩放动画 + +`CFMaterialScaleAnimation` 是 Material Design 3 运动系统中的尺寸变换实现,控制 widget 以缩放方式出现或消失。与淡入淡出和滑动相比,缩放动画有更强的"强调"意味——它让元素感觉像是"从某个点生长出来",适合用于需要抓住用户注意力的重要交互。 + +## 在 Material Design 3 中的定位 + +Material Design 将缩放归类为"强调性变换",主要用于: + +| 场景 | 效果 | 语义 | +|------|------|------| +| 对话框、菜单弹出 | 从 0.8~0.9 放大到 1.0 | 聚焦,吸引用户注意 | +| 按钮按下效果 | 缩小到 0.95 | 触觉反馈 | +| FAB 展开 | 从小圆点放大成完整按钮 | 状态展开 | +| 图片查看 | 缩小/放大 | 空间导航 | + +缩放动画的核心是"中心锚点"——默认情况下,widget 会以其中心点为基准进行缩放,这符合人类对"生长"的直觉认知。 + +## 基本用法 + +缩放动画的构造比较简单,只需要 motion spec: + +```cpp +#include "ui/components/material/cfmaterial_scale_animation.h" + +using namespace cf::ui::components::material; + +// 创建一个缩放动画 +auto scaleAnim = std::make_unique(&motionSpec, this); + +// 设置目标 widget +scaleAnim->setTargetWidget(myDialog); + +// 开始动画(Forward = 放大,Backward = 缩小) +scaleAnim->start(Direction::Forward); +```text + +默认的缩放范围是 0.8 到 1.0(从 80% 放大到 100%),这是 Material 推荐的"弹出"效果参数。 + +## 中心锚点 + +缩放动画最关键的设计决策是锚点位置。我们默认从中心点缩放(`scaleFromCenter = true`),这样 widget 的中心位置保持不变,四周均匀缩放: + +```cpp +// 默认行为:从中心缩放 +auto scaleAnim = std::make_unique(&motionSpec); +scaleAnim->setScaleFromCenter(true); // 这是默认值 +scaleAnim->setTargetWidget(widget); +scaleAnim->start(); + +// widget 会保持中心位置不变,四周同时收缩/扩张 +```text + +如果需要从左上角或其他位置缩放,可以关闭中心缩放模式: + +```cpp +scaleAnim->setScaleFromCenter(false); +// 此时缩放会以 widget 几何原点(左上角)为锚点 +```bash + +⚠️ 非中心缩放在某些布局下可能会导致 widget 位置发生明显偏移,使用时需要谨慎测试。 + +## 几何变换实现 + +我们的缩放是通过修改 widget 的 `setGeometry()` 实现的,而不是使用 `QTransform`。这个选择是经过权衡的: + +| 方案 | 优点 | 缺点 | +|------|------|------| +| setGeometry | 布局兼容性好,重绘正确 | 性能稍低,需要手动计算 | +| QTransform | 性能更好,硬件加速 | 可能导致布局错乱,边距问题 | + +使用 `setGeometry` 意味着动画过程中 widget 的实际几何属性在变化,布局系统和父容器会正确感知这个变化。如果你在动画期间需要查询 widget 的尺寸,得到的是实时缩放后的值: + +```cpp +scaleAnim->start(); +// ... 动画进行中 ... +qDebug() << widget->width(); // 当前缩放后的宽度 +qDebug() << widget->height(); // 当前缩放后的高度 +```bash + +## 时序参数 + +缩放动画的时长选择取决于元素的"重要性"和"预期注意力": + +| 场景 | 推荐时长 | 推荐缓动 | +|------|----------|----------| +| 按钮/小图标 | 150ms | EmphasizedDecelerate | +| 对话框/卡片 | 200ms | EmphasizedDecelerate | +| 菜单/下拉列表 | 250ms | EmphasizedDecelerate | +| 全屏转场 | 300ms | EmphasizedDecelerate | + +缩放动画几乎总是用 `EmphasizedDecelerate` 缓动——快速启动、缓慢停止的效果能产生"有力但可控"的感觉。 + +## 常见使用场景 + +### 对话框弹出 + +```cpp +void showDialog(QWidget* dialog) { + // 初始状态:设置为缩小状态 + dialog->setGeometry( + dialog->x() + dialog->width() * 0.1, + dialog->y() + dialog->height() * 0.1, + dialog->width() * 0.8, + dialog->height() * 0.8 + ); + dialog->show(); + + // 缩放到正常大小 + auto scaleAnim = std::make_unique(&motionSpec); + scaleAnim->setTargetWidget(dialog); + scaleAnim->start(Direction::Forward); +} +```text + +### 按钮按下效果 + +```cpp +class PressButton : public QPushButton { +public: + void mousePressEvent(QMouseEvent* event) override { + // 按下时缩小 + auto scaleAnim = std::make_unique(&motionSpec); + scaleAnim->setTargetWidget(this); + scaleAnim->start(Direction::Backward); // 缩小 + QPushButton::mousePressEvent(event); + } + + void mouseReleaseEvent(QMouseEvent* event) override { + // 释放时恢复 + auto scaleAnim = std::make_unique(&motionSpec); + scaleAnim->setTargetWidget(this); + scaleAnim->start(Direction::Forward); // 放大 + QPushButton::mouseReleaseEvent(event); + } +}; +```text + +### 菜单展开 + +```cpp +void showMenu(QWidget* menu) { + // 菜单从按钮位置"生长"出来 + // 需要将菜单的缩放中心设置为靠近按钮的位置 + // 这可以通过初始几何属性调整实现 + + menu->show(); + + auto scaleAnim = std::make_unique(&motionSpec); + scaleAnim->setTargetWidget(menu); + scaleAnim->start(); +} +```text + +### 组合动画:弹出 + 淡入 + +缩放和淡入的组合是"出现"效果的经典组合: + +```cpp +void showCard(QWidget* card) { + card->show(); + + auto scaleAnim = factory->getAnimation("md.animation.scaleUp"); + auto fadeAnim = factory->getAnimation("md.animation.fadeIn"); + + scaleAnim->setTargetWidget(card); + fadeAnim->setTargetWidget(card); + + scaleAnim->start(); + fadeAnim->start(); +} +```text + +两个动画同步运行能产生"从无到有"的完整视觉体验——缩放负责空间维度,透明度负责视觉维度。 + +## 性能考量 + +缩放动画涉及 widget 几何属性的变化,可能触发父容器的重新布局: + +1. **避免在动画期间修改布局**:如果 widget 的父容器使用复杂布局,动画期间禁用自动更新可以提升性能 +2. **固定尺寸 widget 效果更好**:使用固定几何属性的 widget 动画更流畅 +3. **考虑淡入淡出替代**:对于简单场景,纯透明度动画性能更好且视觉效果足够 + +## 缩放值范围 + +虽然 Material 推荐的缩放范围是 0.8~1.0(弹出)或 1.0~0.8(收起),但你可以根据需要调整: + +```cpp +// 更明显的弹出效果:从 0.5 开始 +// 这需要修改动画配置或创建自定义动画 + +// 微妙的强调:从 0.95 开始 +// 适合小元素或需要低调的场景 +```text + +缩放到 1.0 以上(放大效果)在 Material Design 中比较少见,通常用于特殊交互如图片预览。这种场景下可能需要配合淡入淡出来避免突兀。 + +## 与布局管理器的交互 + +如果你的 widget 位于布局管理器中,缩放动画可能会与布局系统产生冲突: + +```cpp +// widget 在 QVBoxLayout 中 +auto layout = new QVBoxLayout(parent); +layout->addWidget(myWidget); + +// 缩放动画会改变 widget 的几何属性 +// 布局管理器可能会尝试"纠正"这个变化 +auto scaleAnim = std::make_unique(&motionSpec); +scaleAnim->setTargetWidget(myWidget); +scaleAnim->start(); // 可能导致布局抖动 +```cpp + +解决这个问题有几种方案: + +1. 动画期间临时移除布局约束 +2. 使用固定位置的 widget(不使用布局) +3. 在布局中插入占位 widget 来吸收尺寸变化 + +## 常见问题 + +**Q: 动画过程中 widget 闪烁** + +A: 可能是 `WA_OpaquePaintEvent` 或 `WA_NoSystemBackground` 属性设置不当。尝试设置 `widget->setAttribute(Qt::WA_OpaquePaintEvent)`。 + +**Q: 缩放后 widget 边缘模糊** + +A: 这是抗锯齿的正常现象。如果需要更清晰的边缘,可以启用 `AA_EnableHighDpiScaling` 或使用更高分辨率的资源。 + +**Q: 动画结束后布局被破坏** + +A: 动画会修改 widget 的几何属性。如果需要恢复布局,监听 `finished` 信号并在其中调用 `widget->setGeometry(originalGeometry)`。 + +## 相关文档 + +- [CFMaterialAnimationFactory - 动画工厂](./cfmaterial_animation_factory.md) +- [CFMaterialFadeAnimation - 淡入淡出动画](./cfmaterial_fade_animation.md) +- [CFMaterialSlideAnimation - 滑动动画](./cfmaterial_slide_animation.md) +- [Material Design 3 运动规范](https://m3.material.io/styles/motion) diff --git a/document/HandBook/ui/material/animation/cfmaterial_slide_animation.md b/document/HandBook/ui/material/animation/cfmaterial_slide_animation.md index b6be1aebb..23c4ac393 100644 --- a/document/HandBook/ui/material/animation/cfmaterial_slide_animation.md +++ b/document/HandBook/ui/material/animation/cfmaterial_slide_animation.md @@ -1,233 +1,238 @@ -# CFMaterialSlideAnimation - Material 滑动动画 - -`CFMaterialSlideAnimation` 实现了 Material Design 3 规范中的位移动画,控制 widget 在屏幕空间中的位置变化。滑动动画是 Material Design 中最重要的过渡方式之一——它利用空间位置的变化来建立元素之间的视觉关联,让用户理解"这个东西从哪里来、到哪里去"。 - -## 在 Material Design 3 中的定位 - -在 Material 的运动语言里,滑动代表了"元素从边界进入或离开"。不同方向的滑动有不同的语义: - -| 方向 | 典型用途 | 语义 | -|------|----------|------| -| Up | 底部sheet、键盘升起、 Snackbar | 从下方边界进入 | -| Down | 下拉刷新、关闭底部sheet | 向下方边界退出 | -| Left | 侧边菜单收起、向右切换内容 | 向左边界退出/进入 | -| Right | 侧边菜单展开、向左切换内容 | 从右边界进入/退出 | - -这个语义映射不是绝对的——具体用哪个方向取决于你的 UI 布局和导航逻辑,但保持一致性很重要。 - -## 基本用法 - -滑动动画需要在构造时指定方向,因为方向决定了位移的正负: - -```cpp -#include "ui/components/material/cfmaterial_slide_animation.h" - -using namespace cf::ui::components::material; - -// 创建一个向上滑动的动画(widget 从下方移入) -auto slideAnim = std::make_unique( - &motionSpec, - SlideDirection::Up, // 滑动方向 - this -); - -// 设置滑动距离(像素) -slideAnim->setDistance(100.0f); - -// 设置目标并启动 -slideAnim->setTargetWidget(myCard); -slideAnim->start(Direction::Forward); -``` - -方向枚举的命名可能会让你困惑——`SlideDirection::Up` 表示 widget 向上移动,视觉上是"从下方滑入"。反过来想:枚举值描述的是 widget 移动的轨迹方向,而不是来源方向。 - -## 滑动距离 - -`setDistance()` 控制动画的位移量,单位是像素。默认值是 100px,但这个值在不同屏幕尺寸下可能不合适。我们建议根据实际使用场景动态计算: - -```cpp -// 列表项从右侧滑入,距离为父容器宽度的 30% -float offset = parentWidget->width() * 0.3f; -slideAnim->setDistance(offset); - -// 底部 sheet 从屏幕底部滑入,距离为父容器高度 -slideAnim->setDistance(parentWidget->height()); - -// Snackbar 从底部轻微上浮 -slideAnim->setDistance(50.0f); -``` - -⚠️ 距离值应该是正数。方向由 `SlideDirection` 控制,不需要用负距离来表示反向移动。 - -## 原始位置保存 - -动画会在启动时保存 widget 的原始位置,并在动画停止时恢复。这个设计的行为是: - -```cpp -// 假设 widget 当前在 (100, 100) -slideAnim->setTargetWidget(widget); -slideAnim->start(Direction::Forward); -// ... 动画运行中,widget 位置被修改 ... -slideAnim->stop(); // widget 回到 (100, 100) -``` - -但如果你在动画过程中手动移动了 widget,动画停止时恢复的位置仍然是启动时的原始位置,这可能导致视觉跳跃。如果需要在动画结束后将 widget 定位到新位置,应该在 `finished` 信号中处理: - -```cpp -connect(slideAnim.get(), &ICFAbstractAnimation::finished, this, [widget]() { - // 动画结束后,手动设置到目标位置 - widget->move(200, 100); -}); -``` - -## 方向详解 - -四个方向的位移计算逻辑如下: - -```cpp -// SlideDirection::Up: widget 向上移动(从下方进入) -// 位移应用到 y 轴,offset 为负值 -offset = QPoint(0, -distance) - -// SlideDirection::Down: widget 向下移动(从上方进入) -// 位移应用到 y 轴,offset 为正值 -offset = QPoint(0, distance) - -// SlideDirection::Left: widget 向左移动(从右侧进入) -// 位移应用到 x 轴,offset 为负值 -offset = QPoint(-distance, 0) - -// SlideDirection::Right: widget 向右移动(从左侧进入) -// 位移应用到 x 轴,offset 为正值 -offset = QPoint(distance, 0) -``` - -理解这个逻辑有助于你在调试时判断问题方向——如果动画朝反方向移动,大概率是方向枚举选错了。 - -## 时序参数 - -滑动动画通常使用比淡入淡出更长的时长,因为位移变化的视觉冲击力更大: - -| 场景 | 推荐时长 | 推荐缓动 | -|------|----------|----------| -| 小元素滑动(按钮、图标) | 200ms | EmphasizedDecelerate | -| 列表项滑入 | 250ms | EmphasizedDecelerate | -| 底部 Sheet / 侧边栏 | 300ms | EmphasizedDecelerate | -| 全屏页面切换 | 350ms | EmphasizedDecelerate | - -工厂会根据你选择的动画 token 自动应用这些参数,一般不需要手动干预。 - -## 常见使用场景 - -### 底部 Sheet 弹出 - -```cpp -void showBottomSheet(QWidget* sheet, QWidget* parent) { - // 初始位置:放在父容器底部之外 - int startY = parent->height(); - sheet->move(0, startY); - sheet->show(); - - // 向上滑动到最终位置 - auto slideAnim = std::make_unique( - &motionSpec, SlideDirection::Up); - slideAnim->setDistance(parent->height()); - slideAnim->setTargetWidget(sheet); - slideAnim->start(); -} -``` - -### 侧边菜单滑入 - -```cpp -void showSideMenu(QWidget* menu, QWidget* parent) { - // 从右侧滑入 - menu->setParent(parent); - menu->move(parent->width(), 0); - menu->show(); - - auto slideAnim = std::make_unique( - &motionSpec, SlideDirection::Left); - slideAnim->setDistance(parent->width()); - slideAnim->setTargetWidget(menu); - slideAnim->start(); -} -``` - -### 列表项交错动画 - -```cpp -void showListItems(const QList& items) { - for (int i = 0; i < items.size(); ++i) { - auto slideAnim = std::make_unique( - &motionSpec, SlideDirection::Up); - - // 设置交错延迟 - slideAnim->setDelay(i * 50); // 每项延迟 50ms - slideAnim->setDistance(30.0f); - slideAnim->setTargetWidget(items[i]); - slideAnim->start(); - } -} -``` - -### 下拉刷新 - -```cpp -void onPullDown(float offset) { - // offset 是下拉的距离,正值 - auto slideAnim = std::make_unique( - &motionSpec, SlideDirection::Up); - slideAnim->setDistance(offset); - - // 反向播放:回到原位 - slideAnim->start(Direction::Backward); -} -``` - -## 性能优化建议 - -滑动动画涉及 widget 几何属性的变化,相对淡入淡出有更高的渲染开销: - -1. **避免同时动画大量 widget**:如果一个列表有上百项同时滑入,考虑分批或使用交错延迟 -2. **硬件加速**:确保 widget 的 `WA_TranslucentBackground` 或 `WA_NoSystemBackground` 属性设置正确 -3. **简化布局**:动画期间避免复杂的布局计算,可以考虑用固定几何属性 - -## 组合使用 - -滑动动画常与其他动画组合以产生更丰富的效果: - -```cpp -// 从右下方滑入,同时淡入 -auto slideAnim = factory->getAnimation("md.animation.slideInRight"); -auto fadeAnim = factory->getAnimation("md.animation.fadeIn"); - -slideAnim->setTargetWidget(widget); -fadeAnim->setTargetWidget(widget); - -slideAnim->start(); -fadeAnim->start(); -``` - -这种组合在卡片、对话框等"重要元素"出现时特别有效。 - -## 常见问题 - -**Q: 动画结束后 widget 跳回原位置** - -A: 这是因为动画停止时会恢复原始位置。如果需要保持最终位置,监听 `finished` 信号并在其中手动设置 widget 位置。 - -**Q: 滑动方向与预期相反** - -A: 检查 `SlideDirection` 枚举值。枚举表示 widget 移动的方向,不是来源方向。`Up` = 向上移动 = 从下方进入。 - -**Q: 动画过程中 widget 闪烁** - -A: 可能是布局系统在干扰。尝试在动画期间暂停布局更新,或者使用固定位置而不是依赖布局管理器。 - -## 相关文档 - -- [CFMaterialAnimationFactory - 动画工厂](./cfmaterial_animation_factory.md) -- [CFMaterialFadeAnimation - 淡入淡出动画](./cfmaterial_fade_animation.md) -- [CFMaterialScaleAnimation - 缩放动画](./cfmaterial_scale_animation.md) -- [Material Design 3 运动规范](https://m3.material.io/styles/motion) +--- +title: "CFMaterialSlideAnimation - Material 滑动动画" +description: 实现了 Material Design 3 规范中的位移动画,控制 widget 在屏幕空间中的位置 +--- + +# CFMaterialSlideAnimation - Material 滑动动画 + +`CFMaterialSlideAnimation` 实现了 Material Design 3 规范中的位移动画,控制 widget 在屏幕空间中的位置变化。滑动动画是 Material Design 中最重要的过渡方式之一——它利用空间位置的变化来建立元素之间的视觉关联,让用户理解"这个东西从哪里来、到哪里去"。 + +## 在 Material Design 3 中的定位 + +在 Material 的运动语言里,滑动代表了"元素从边界进入或离开"。不同方向的滑动有不同的语义: + +| 方向 | 典型用途 | 语义 | +|------|----------|------| +| Up | 底部sheet、键盘升起、 Snackbar | 从下方边界进入 | +| Down | 下拉刷新、关闭底部sheet | 向下方边界退出 | +| Left | 侧边菜单收起、向右切换内容 | 向左边界退出/进入 | +| Right | 侧边菜单展开、向左切换内容 | 从右边界进入/退出 | + +这个语义映射不是绝对的——具体用哪个方向取决于你的 UI 布局和导航逻辑,但保持一致性很重要。 + +## 基本用法 + +滑动动画需要在构造时指定方向,因为方向决定了位移的正负: + +```cpp +#include "ui/components/material/cfmaterial_slide_animation.h" + +using namespace cf::ui::components::material; + +// 创建一个向上滑动的动画(widget 从下方移入) +auto slideAnim = std::make_unique( + &motionSpec, + SlideDirection::Up, // 滑动方向 + this +); + +// 设置滑动距离(像素) +slideAnim->setDistance(100.0f); + +// 设置目标并启动 +slideAnim->setTargetWidget(myCard); +slideAnim->start(Direction::Forward); +```text + +方向枚举的命名可能会让你困惑——`SlideDirection::Up` 表示 widget 向上移动,视觉上是"从下方滑入"。反过来想:枚举值描述的是 widget 移动的轨迹方向,而不是来源方向。 + +## 滑动距离 + +`setDistance()` 控制动画的位移量,单位是像素。默认值是 100px,但这个值在不同屏幕尺寸下可能不合适。我们建议根据实际使用场景动态计算: + +```cpp +// 列表项从右侧滑入,距离为父容器宽度的 30% +float offset = parentWidget->width() * 0.3f; +slideAnim->setDistance(offset); + +// 底部 sheet 从屏幕底部滑入,距离为父容器高度 +slideAnim->setDistance(parentWidget->height()); + +// Snackbar 从底部轻微上浮 +slideAnim->setDistance(50.0f); +```text + +⚠️ 距离值应该是正数。方向由 `SlideDirection` 控制,不需要用负距离来表示反向移动。 + +## 原始位置保存 + +动画会在启动时保存 widget 的原始位置,并在动画停止时恢复。这个设计的行为是: + +```cpp +// 假设 widget 当前在 (100, 100) +slideAnim->setTargetWidget(widget); +slideAnim->start(Direction::Forward); +// ... 动画运行中,widget 位置被修改 ... +slideAnim->stop(); // widget 回到 (100, 100) +```text + +但如果你在动画过程中手动移动了 widget,动画停止时恢复的位置仍然是启动时的原始位置,这可能导致视觉跳跃。如果需要在动画结束后将 widget 定位到新位置,应该在 `finished` 信号中处理: + +```cpp +connect(slideAnim.get(), &ICFAbstractAnimation::finished, this, [widget]() { + // 动画结束后,手动设置到目标位置 + widget->move(200, 100); +}); +```text + +## 方向详解 + +四个方向的位移计算逻辑如下: + +```cpp +// SlideDirection::Up: widget 向上移动(从下方进入) +// 位移应用到 y 轴,offset 为负值 +offset = QPoint(0, -distance) + +// SlideDirection::Down: widget 向下移动(从上方进入) +// 位移应用到 y 轴,offset 为正值 +offset = QPoint(0, distance) + +// SlideDirection::Left: widget 向左移动(从右侧进入) +// 位移应用到 x 轴,offset 为负值 +offset = QPoint(-distance, 0) + +// SlideDirection::Right: widget 向右移动(从左侧进入) +// 位移应用到 x 轴,offset 为正值 +offset = QPoint(distance, 0) +```bash + +理解这个逻辑有助于你在调试时判断问题方向——如果动画朝反方向移动,大概率是方向枚举选错了。 + +## 时序参数 + +滑动动画通常使用比淡入淡出更长的时长,因为位移变化的视觉冲击力更大: + +| 场景 | 推荐时长 | 推荐缓动 | +|------|----------|----------| +| 小元素滑动(按钮、图标) | 200ms | EmphasizedDecelerate | +| 列表项滑入 | 250ms | EmphasizedDecelerate | +| 底部 Sheet / 侧边栏 | 300ms | EmphasizedDecelerate | +| 全屏页面切换 | 350ms | EmphasizedDecelerate | + +工厂会根据你选择的动画 token 自动应用这些参数,一般不需要手动干预。 + +## 常见使用场景 + +### 底部 Sheet 弹出 + +```cpp +void showBottomSheet(QWidget* sheet, QWidget* parent) { + // 初始位置:放在父容器底部之外 + int startY = parent->height(); + sheet->move(0, startY); + sheet->show(); + + // 向上滑动到最终位置 + auto slideAnim = std::make_unique( + &motionSpec, SlideDirection::Up); + slideAnim->setDistance(parent->height()); + slideAnim->setTargetWidget(sheet); + slideAnim->start(); +} +```text + +### 侧边菜单滑入 + +```cpp +void showSideMenu(QWidget* menu, QWidget* parent) { + // 从右侧滑入 + menu->setParent(parent); + menu->move(parent->width(), 0); + menu->show(); + + auto slideAnim = std::make_unique( + &motionSpec, SlideDirection::Left); + slideAnim->setDistance(parent->width()); + slideAnim->setTargetWidget(menu); + slideAnim->start(); +} +```text + +### 列表项交错动画 + +```cpp +void showListItems(const QList& items) { + for (int i = 0; i < items.size(); ++i) { + auto slideAnim = std::make_unique( + &motionSpec, SlideDirection::Up); + + // 设置交错延迟 + slideAnim->setDelay(i * 50); // 每项延迟 50ms + slideAnim->setDistance(30.0f); + slideAnim->setTargetWidget(items[i]); + slideAnim->start(); + } +} +```text + +### 下拉刷新 + +```cpp +void onPullDown(float offset) { + // offset 是下拉的距离,正值 + auto slideAnim = std::make_unique( + &motionSpec, SlideDirection::Up); + slideAnim->setDistance(offset); + + // 反向播放:回到原位 + slideAnim->start(Direction::Backward); +} +```text + +## 性能优化建议 + +滑动动画涉及 widget 几何属性的变化,相对淡入淡出有更高的渲染开销: + +1. **避免同时动画大量 widget**:如果一个列表有上百项同时滑入,考虑分批或使用交错延迟 +2. **硬件加速**:确保 widget 的 `WA_TranslucentBackground` 或 `WA_NoSystemBackground` 属性设置正确 +3. **简化布局**:动画期间避免复杂的布局计算,可以考虑用固定几何属性 + +## 组合使用 + +滑动动画常与其他动画组合以产生更丰富的效果: + +```cpp +// 从右下方滑入,同时淡入 +auto slideAnim = factory->getAnimation("md.animation.slideInRight"); +auto fadeAnim = factory->getAnimation("md.animation.fadeIn"); + +slideAnim->setTargetWidget(widget); +fadeAnim->setTargetWidget(widget); + +slideAnim->start(); +fadeAnim->start(); +```yaml + +这种组合在卡片、对话框等"重要元素"出现时特别有效。 + +## 常见问题 + +**Q: 动画结束后 widget 跳回原位置** + +A: 这是因为动画停止时会恢复原始位置。如果需要保持最终位置,监听 `finished` 信号并在其中手动设置 widget 位置。 + +**Q: 滑动方向与预期相反** + +A: 检查 `SlideDirection` 枚举值。枚举表示 widget 移动的方向,不是来源方向。`Up` = 向上移动 = 从下方进入。 + +**Q: 动画过程中 widget 闪烁** + +A: 可能是布局系统在干扰。尝试在动画期间暂停布局更新,或者使用固定位置而不是依赖布局管理器。 + +## 相关文档 + +- [CFMaterialAnimationFactory - 动画工厂](./cfmaterial_animation_factory.md) +- [CFMaterialFadeAnimation - 淡入淡出动画](./cfmaterial_fade_animation.md) +- [CFMaterialScaleAnimation - 缩放动画](./cfmaterial_scale_animation.md) +- [Material Design 3 运动规范](https://m3.material.io/styles/motion) diff --git a/document/HandBook/ui/material/animation/index.md b/document/HandBook/ui/material/animation/index.md index 92ac768eb..24524b0c3 100644 --- a/document/HandBook/ui/material/animation/index.md +++ b/document/HandBook/ui/material/animation/index.md @@ -1,10 +1,11 @@ -# Animation - -> Welcome to the Animation section. +--- +title: Material 动画 +description: 本章节包含 Material Design 3 动画系统的文档,涵盖 动画工厂、动画策略(Anim +--- -## Overview +# Material 动画 -Documentation and resources for Animation. +本章节包含 Material Design 3 动画系统的文档,涵盖 `CFMaterialAnimationFactory` 动画工厂、动画策略(Animation Strategy)以及组件级过渡动画的实现机制。动画系统通过策略模式提供可扩展的动效组合能力。 --- diff --git a/document/HandBook/ui/material/base/.pages b/document/HandBook/ui/material/base/.pages deleted file mode 100644 index 04b85545b..000000000 --- a/document/HandBook/ui/material/base/.pages +++ /dev/null @@ -1,6 +0,0 @@ -title: 基础 -nav: - - Elevation 控制器: elevation_controller.md - - 焦点环: focus_ring.md - - 绘制层: painter_layer.md - - Ripple 助手: ripple_helper.md diff --git a/document/HandBook/ui/material/base/elevation_controller.md b/document/HandBook/ui/material/base/elevation_controller.md index 7e244a9ba..21d7aa545 100644 --- a/document/HandBook/ui/material/base/elevation_controller.md +++ b/document/HandBook/ui/material/base/elevation_controller.md @@ -1,212 +1,217 @@ -# MdElevationController - Material 阴影控制器 - -`MdElevationController` 是 Material Design 3 高程系统的核心实现,负责管理控件的海拔级别和对应的阴影渲染。在 Material Design 中,高程(Elevation)是表达 UI 层级关系的关键手段,通过阴影和色调变化来传达"哪个元素在上"。我们选择自己实现,是因为 Qt 的图形系统默认不支持 Material 的多光源阴影模型和动画过渡。 - -## Material Design 高程系统 - -Material Design 定义了 6 个标准海拔级别(0-5),每个级别对应不同的阴影参数: - -| 级别 | 高程值 | 模糊半径 | 偏移量 | 不透明度 | -|------|--------|----------|--------|----------| -| Level 0 | 0dp | 0px | 0px | 0% | -| Level 1 | 1dp | 2px | 1px | 15% | -| Level 2 | 3dp | 4px | 2px | 20% | -| Level 3 | 6dp | 8px | 4px | 25% | -| Level 4 | 8dp | 12px | 6px | 30% | -| Level 5 | 12dp | 16px | 8px | 35% | - -这些参数在 `paramsForLevel()` 中定义,阴影效果通过多层叠加实现近似模糊。 - -## 基本用法 - -`MdElevationController` 需要配合动画工厂使用,通常在控件构造函数中初始化: - -```cpp -#include "widget/material/base/elevation_controller.h" - -using namespace cf::ui::widget::material; - -class MyWidget : public QWidget { -public: - MyWidget(QWidget* parent = nullptr) : QWidget(parent) { - auto animationFactory = cf::WeakPtr::DynamicCast( - Application::animationFactory() - ); - - // 创建高程控制器 - m_elevation = new base::MdElevationController(animationFactory, this); - - // 设置海拔级别 - m_elevation->setElevation(2); // Level 2 - } - -private: - base::MdElevationController* m_elevation; -}; -``` - -## 设置高程级别 - -直接使用 `setElevation()` 设置高程,这会立即更新阴影效果: - -```cpp -// 静态设置 -m_elevation->setElevation(0); // 无阴影 -m_elevation->setElevation(3); // 中等阴影 -m_elevation->setElevation(5); // 最强阴影 -``` - -高程值会被 clamp 在 [0, 5] 范围内,超出范围的值会自动截断。 - -## 动画过渡 - -对于需要动态高程变化的场景(如按钮按压),使用 `animateTo()` 方法: - -```cpp -#include "core/material/cfmaterial_motion.h" - -// 按压时升高 -void MyWidget::mousePressEvent(QMouseEvent* event) { - QWidget::mousePressEvent(event); - m_elevation->setPressed(true); - - // 使用标准动画规格 - auto spec = core::MotionSpec::standard(); - m_elevation->animateTo(4, spec); -} - -// 释放时恢复 -void MyWidget::mouseReleaseEvent(QMouseEvent* event) { - QWidget::mouseReleaseEvent(event); - m_elevation->setPressed(false); - m_elevation->animateTo(2, core::MotionSpec::standard()); -} -``` - -`MotionSpec` 定义了动画的缓动曲线和时长,使用 Material 标准值可确保动画感觉一致。 - -## 绘制阴影 - -阴影应该在 `paintEvent` 中最先绘制,这样它会出现在控件背景下方: - -```cpp -void MyWidget::paintEvent(QPaintEvent* event) { - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - // Step 1: 先绘制阴影 - QPainterPath shape = widgetShape(); - m_elevation->paintShadow(&p, shape); - - // Step 2: 绘制背景(会覆盖阴影的主体部分,只露出边缘) - p.fillPath(shape, backgroundColor()); - - // 其他绘制... -} -``` - -⚠️ 阴影必须先绘制,否则会覆盖控件内容。绘制顺序错了视觉效果会很奇怪。 - -## 光源角度 - -Material Design 假设光源来自左上方(约 15 度),这形成向右下方的阴影。可以通过 `setLightSourceAngle()` 调整: - -```cpp -// 默认值(光源来自左上方) -m_elevation->setLightSourceAngle(15.0f); - -// 光源来自正上方 -m_elevation->setLightSourceAngle(0.0f); - -// 光源来自右侧 -m_elevation->setLightSourceAngle(-30.0f); -``` - -角度正值表示光源从左侧来,阴影向右投射;负值表示光源从右侧来。这个参数影响阴影的水平偏移量。 - -## 按压效果 - -按压效果包含两部分:阴影变化和控件位移。控制器提供了 `setPressed()` 和 `pressOffset()` 来支持这个效果: - -```cpp -void MyWidget::mousePressEvent(QMouseEvent* event) { - m_elevation->setPressed(true); - update(); -} - -void MyWidget::paintEvent(QPaintEvent* event) { - QPainter p(this); - - // 应用按压位移 - float offset = m_elevation->pressOffset(); - p.translate(0, offset); - - // 正常绘制... -} -``` - -按压时阴影会缩小并靠近控件(约 50%),同时控件向下移动,产生"按下"的视觉反馈。 - -## 暗色主题支持 - -在暗色主题中,阴影效果不明显,Material Design 使用色调叠加来表示高程: - -```cpp -CFColor MdElevationController::tonalOverlay(CFColor surface, CFColor primary) const { - // 返回混合后的表面颜色 -} -``` - -使用方式: - -```cpp -bool isDark = theme->isDarkTheme(); -CFColor surfaceColor = theme->getColor(MdColorRole::Surface); -CFColor primaryColor = theme->getColor(MdColorRole::Primary); - -if (isDark) { - // 暗色主题使用色调叠加 - backgroundColor = m_elevation->tonalOverlay(surfaceColor, primaryColor); -} else { - // 亮色主题使用阴影 - backgroundColor = surfaceColor; -} -``` - -色调叠加量与高程级别成正比,级别越高叠加越多。 - -## 高程层级选择 - -选择合适的高程级别取决于控件在界面中的层级关系: - -| 场景 | 推荐级别 | -|------|----------| -| 平面控件(卡片、表单) | 0-1 | -| 悬浮按钮(FAB) | 3 | -| 对话框 | 4 | -| 底部抽屉 | 5 | -| 菜单、下拉框 | 2-3 | - -保持相邻层级之间至少相差 1 级,确保视觉层次清晰。 - -## 性能考虑 - -阴影渲染涉及多层绘制和透明度混合,在低端设备上可能成为性能瓶颈。如果遇到性能问题: - -1. 降低高程级别(Level 1 和 2 的渲染开销较小) -2. 全局禁用动画(阴影计算仍然进行,但没有插值开销) -3. 在低端设备上禁用阴影(通过条件编译或运行时检测) - -## 常见问题 - -阴影看起来"不对劲"通常是因为: - -1. 绘制顺序错误——阴影必须在背景之前绘制 -2. 控件没有设置 `WA_TranslucentBackground` 属性(某些平台需要) -3. 设备像素比计算错误(高 DPI 屏幕上阴影会模糊) - -## 相关文档 - -- [StateMachine - Material 状态机](../widget/state_machine.md) -- [MdFocusIndicator - 焦点环](./focus_ring.md) -- [Material Design 3 高程规范](https://m3.material.io/styles/elevation) +--- +title: "MdElevationController - Material 阴影控制器" +description: 是 Material Design 3 高程系统的核心实现,负责管理控件的海拔级别和对应的阴影渲染。 +--- + +# MdElevationController - Material 阴影控制器 + +`MdElevationController` 是 Material Design 3 高程系统的核心实现,负责管理控件的海拔级别和对应的阴影渲染。在 Material Design 中,高程(Elevation)是表达 UI 层级关系的关键手段,通过阴影和色调变化来传达"哪个元素在上"。我们选择自己实现,是因为 Qt 的图形系统默认不支持 Material 的多光源阴影模型和动画过渡。 + +## Material Design 高程系统 + +Material Design 定义了 6 个标准海拔级别(0-5),每个级别对应不同的阴影参数: + +| 级别 | 高程值 | 模糊半径 | 偏移量 | 不透明度 | +|------|--------|----------|--------|----------| +| Level 0 | 0dp | 0px | 0px | 0% | +| Level 1 | 1dp | 2px | 1px | 15% | +| Level 2 | 3dp | 4px | 2px | 20% | +| Level 3 | 6dp | 8px | 4px | 25% | +| Level 4 | 8dp | 12px | 6px | 30% | +| Level 5 | 12dp | 16px | 8px | 35% | + +这些参数在 `paramsForLevel()` 中定义,阴影效果通过多层叠加实现近似模糊。 + +## 基本用法 + +`MdElevationController` 需要配合动画工厂使用,通常在控件构造函数中初始化: + +```cpp +#include "widget/material/base/elevation_controller.h" + +using namespace cf::ui::widget::material; + +class MyWidget : public QWidget { +public: + MyWidget(QWidget* parent = nullptr) : QWidget(parent) { + auto animationFactory = cf::WeakPtr::DynamicCast( + Application::animationFactory() + ); + + // 创建高程控制器 + m_elevation = new base::MdElevationController(animationFactory, this); + + // 设置海拔级别 + m_elevation->setElevation(2); // Level 2 + } + +private: + base::MdElevationController* m_elevation; +}; +```text + +## 设置高程级别 + +直接使用 `setElevation()` 设置高程,这会立即更新阴影效果: + +```cpp +// 静态设置 +m_elevation->setElevation(0); // 无阴影 +m_elevation->setElevation(3); // 中等阴影 +m_elevation->setElevation(5); // 最强阴影 +```text + +高程值会被 clamp 在 [0, 5] 范围内,超出范围的值会自动截断。 + +## 动画过渡 + +对于需要动态高程变化的场景(如按钮按压),使用 `animateTo()` 方法: + +```cpp +#include "core/material/cfmaterial_motion.h" + +// 按压时升高 +void MyWidget::mousePressEvent(QMouseEvent* event) { + QWidget::mousePressEvent(event); + m_elevation->setPressed(true); + + // 使用标准动画规格 + auto spec = core::MotionSpec::standard(); + m_elevation->animateTo(4, spec); +} + +// 释放时恢复 +void MyWidget::mouseReleaseEvent(QMouseEvent* event) { + QWidget::mouseReleaseEvent(event); + m_elevation->setPressed(false); + m_elevation->animateTo(2, core::MotionSpec::standard()); +} +```text + +`MotionSpec` 定义了动画的缓动曲线和时长,使用 Material 标准值可确保动画感觉一致。 + +## 绘制阴影 + +阴影应该在 `paintEvent` 中最先绘制,这样它会出现在控件背景下方: + +```cpp +void MyWidget::paintEvent(QPaintEvent* event) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + // Step 1: 先绘制阴影 + QPainterPath shape = widgetShape(); + m_elevation->paintShadow(&p, shape); + + // Step 2: 绘制背景(会覆盖阴影的主体部分,只露出边缘) + p.fillPath(shape, backgroundColor()); + + // 其他绘制... +} +```text + +⚠️ 阴影必须先绘制,否则会覆盖控件内容。绘制顺序错了视觉效果会很奇怪。 + +## 光源角度 + +Material Design 假设光源来自左上方(约 15 度),这形成向右下方的阴影。可以通过 `setLightSourceAngle()` 调整: + +```cpp +// 默认值(光源来自左上方) +m_elevation->setLightSourceAngle(15.0f); + +// 光源来自正上方 +m_elevation->setLightSourceAngle(0.0f); + +// 光源来自右侧 +m_elevation->setLightSourceAngle(-30.0f); +```text + +角度正值表示光源从左侧来,阴影向右投射;负值表示光源从右侧来。这个参数影响阴影的水平偏移量。 + +## 按压效果 + +按压效果包含两部分:阴影变化和控件位移。控制器提供了 `setPressed()` 和 `pressOffset()` 来支持这个效果: + +```cpp +void MyWidget::mousePressEvent(QMouseEvent* event) { + m_elevation->setPressed(true); + update(); +} + +void MyWidget::paintEvent(QPaintEvent* event) { + QPainter p(this); + + // 应用按压位移 + float offset = m_elevation->pressOffset(); + p.translate(0, offset); + + // 正常绘制... +} +```text + +按压时阴影会缩小并靠近控件(约 50%),同时控件向下移动,产生"按下"的视觉反馈。 + +## 暗色主题支持 + +在暗色主题中,阴影效果不明显,Material Design 使用色调叠加来表示高程: + +```cpp +CFColor MdElevationController::tonalOverlay(CFColor surface, CFColor primary) const { + // 返回混合后的表面颜色 +} +```text + +使用方式: + +```cpp +bool isDark = theme->isDarkTheme(); +CFColor surfaceColor = theme->getColor(MdColorRole::Surface); +CFColor primaryColor = theme->getColor(MdColorRole::Primary); + +if (isDark) { + // 暗色主题使用色调叠加 + backgroundColor = m_elevation->tonalOverlay(surfaceColor, primaryColor); +} else { + // 亮色主题使用阴影 + backgroundColor = surfaceColor; +} +```bash + +色调叠加量与高程级别成正比,级别越高叠加越多。 + +## 高程层级选择 + +选择合适的高程级别取决于控件在界面中的层级关系: + +| 场景 | 推荐级别 | +|------|----------| +| 平面控件(卡片、表单) | 0-1 | +| 悬浮按钮(FAB) | 3 | +| 对话框 | 4 | +| 底部抽屉 | 5 | +| 菜单、下拉框 | 2-3 | + +保持相邻层级之间至少相差 1 级,确保视觉层次清晰。 + +## 性能考虑 + +阴影渲染涉及多层绘制和透明度混合,在低端设备上可能成为性能瓶颈。如果遇到性能问题: + +1. 降低高程级别(Level 1 和 2 的渲染开销较小) +2. 全局禁用动画(阴影计算仍然进行,但没有插值开销) +3. 在低端设备上禁用阴影(通过条件编译或运行时检测) + +## 常见问题 + +阴影看起来"不对劲"通常是因为: + +1. 绘制顺序错误——阴影必须在背景之前绘制 +2. 控件没有设置 `WA_TranslucentBackground` 属性(某些平台需要) +3. 设备像素比计算错误(高 DPI 屏幕上阴影会模糊) + +## 相关文档 + +- [StateMachine - Material 状态机](../widget/state_machine.md) +- [MdFocusIndicator - 焦点环](./focus_ring.md) +- [Material Design 3 高程规范](https://m3.material.io/styles/elevation) diff --git a/document/HandBook/ui/material/base/focus_ring.md b/document/HandBook/ui/material/base/focus_ring.md index 5e7372316..39841afd7 100644 --- a/document/HandBook/ui/material/base/focus_ring.md +++ b/document/HandBook/ui/material/base/focus_ring.md @@ -1,151 +1,156 @@ -# MdFocusIndicator - Material 焦点环 - -`MdFocusIndicator` 是 Material Design 3 键盘焦点可视化的核心组件。它负责在控件获得键盘焦点时绘制聚焦环,并提供流畅的进出动画。我们单独实现这个组件,是因为 Qt 自带的焦点样式(`QWidget::setFocusPolicy` 配合样式表)无法实现 Material 规范的动画效果和精确尺寸控制。 - -## 可访问性要求 - -聚焦环不是装饰,是可访问性的核心组成部分。Material Design 要求所有可通过键盘交互的控件都必须有明显的焦点指示器,这对于视障用户和键盘导航用户至关重要。WCAG 2.1 AAA 级别要求焦点指示器与周围背景的对比度至少为 3:1,Material 的聚焦环设计满足了这一要求。 - -## Material Design 规范 - -Material Design 3 对聚焦环的尺寸有明确约定: - -- **环宽**: 3dp -- **内边距**: 3dp(距离控件边界) -- **动画时长**: 250ms(淡入淡出) -- **颜色**: 使用 `onSurface` 色彩角色 - -这些尺寸在代码中被硬编码为常量,改动会破坏与其他 Material 控件的视觉一致性。 - -## 基本用法 - -`MdFocusIndicator` 需要配合动画工厂使用,通常在控件构造函数中初始化: - -```cpp -#include "widget/material/base/focus_ring.h" - -using namespace cf::ui::widget::material; - -class MyWidget : public QWidget { -public: - MyWidget(QWidget* parent = nullptr) : QWidget(parent) { - // 获取全局动画工厂 - auto animationFactory = cf::WeakPtr::DynamicCast( - Application::animationFactory() - ); - - // 创建焦点指示器 - m_focusIndicator = new base::MdFocusIndicator(animationFactory, this); - } - -private: - base::MdFocusIndicator* m_focusIndicator; -}; -``` - -## 事件处理 - -焦点事件的转发非常直接,只需要在控件的事件处理中调用对应方法: - -```cpp -void MyWidget::focusInEvent(QFocusEvent* event) { - QWidget::focusInEvent(event); - m_focusIndicator->onFocusIn(); - update(); -} - -void MyWidget::focusOutEvent(QFocusEvent* event) { - QWidget::focusOutEvent(event); - m_focusIndicator->onFocusOut(); - update(); -} -``` - -⚠️ 记得在事件处理函数中先调用父类实现,否则 Qt 的焦点系统可能无法正常工作。 - -## 绘制聚焦环 - -聚焦环应该在绘制的最后阶段进行,确保它出现在所有内容的上层: - -```cpp -void MyWidget::paintEvent(QPaintEvent* event) { - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - // 先绘制背景、内容等... - drawBackground(p); - drawContent(p); - - // 最后绘制聚焦环(如果控件有焦点) - if (hasFocus()) { - CFColor indicatorColor = theme->getColor(MdColorRole::OnSurface); - m_focusIndicator->paint(&p, shape(), indicatorColor); - } -} -``` - -聚焦环的颜色通常使用 `onSurface` 角色获取,这样可以与控件内容保持一致的对比度。 - -## 形状处理 - -`paint()` 方法接受一个 `QPainterPath` 参数,这使得它能够适应各种控件形状: - -```cpp -// 圆角矩形按钮 -QPainterPath shape; -shape.addRoundedRect(rect(), cornerRadius, cornerRadius); -m_focusIndicator->paint(&p, shape, indicatorColor); - -// 圆形 FAB 按钮 -QPainterPath shape; -shape.addEllipse(rect()); -m_focusIndicator->paint(&p, shape, indicatorColor); - -// 自定义形状 -QPainterPath shape = customShape(); -m_focusIndicator->paint(&p, shape, indicatorColor); -``` - -环会自动沿着形状的边界向内偏移绘制,不需要手动计算偏移量。 - -## 动画性能 - -聚焦环使用工厂的 `md.animation.fadeIn` 和 `md.animation.fadeOut` 动画。如果全局动画被禁用,会直接设置透明度值而跳过动画: - -```cpp -// 在应用层面禁用动画 -auto factory = Application::animationFactory(); -if (factory) { - factory->setEnabledAll(false); -} -``` - -这对于低端设备或性能敏感的场景很有用。 - -## 键盘导航建议 - - Material Design 推荐的焦点策略是 `Qt::StrongFocus`,这样控件既可以通过点击获得焦点,也可以通过 Tab 键导航: - -```cpp -MyWidget::MyWidget(QWidget* parent) : QWidget(parent) { - setFocusPolicy(Qt::StrongFocus); - // ... -} -``` - -对于纯装饰性的控件,使用 `Qt::NoFocus` 避免干扰键盘导航流。 - -## 常见问题 - -如果聚焦环没有显示,检查以下几点: - -1. 控件是否真的有焦点(`hasFocus()` 返回 true) -2. 是否正确调用了 `onFocusIn()` -3. `paint()` 方法是否在 `paintEvent` 中被调用 -4. 传入的形状路径是否有效(非空) - -## 相关文档 - -- [StateMachine - Material 状态机](../widget/state_machine.md) -- [MdElevationController - 阴影控制器](./elevation_controller.md) -- [Material Design 3 焦点规范](https://m3.material.io/foundations/accessible-design/focus) +--- +title: "MdFocusIndicator - Material 焦点环" +description: 是 Material Design 3 键盘焦点可视化的核心组件。它负责在控件获得键盘焦点时绘制聚焦 +--- + +# MdFocusIndicator - Material 焦点环 + +`MdFocusIndicator` 是 Material Design 3 键盘焦点可视化的核心组件。它负责在控件获得键盘焦点时绘制聚焦环,并提供流畅的进出动画。我们单独实现这个组件,是因为 Qt 自带的焦点样式(`QWidget::setFocusPolicy` 配合样式表)无法实现 Material 规范的动画效果和精确尺寸控制。 + +## 可访问性要求 + +聚焦环不是装饰,是可访问性的核心组成部分。Material Design 要求所有可通过键盘交互的控件都必须有明显的焦点指示器,这对于视障用户和键盘导航用户至关重要。WCAG 2.1 AAA 级别要求焦点指示器与周围背景的对比度至少为 3:1,Material 的聚焦环设计满足了这一要求。 + +## Material Design 规范 + +Material Design 3 对聚焦环的尺寸有明确约定: + +- **环宽**: 3dp +- **内边距**: 3dp(距离控件边界) +- **动画时长**: 250ms(淡入淡出) +- **颜色**: 使用 `onSurface` 色彩角色 + +这些尺寸在代码中被硬编码为常量,改动会破坏与其他 Material 控件的视觉一致性。 + +## 基本用法 + +`MdFocusIndicator` 需要配合动画工厂使用,通常在控件构造函数中初始化: + +```cpp +#include "widget/material/base/focus_ring.h" + +using namespace cf::ui::widget::material; + +class MyWidget : public QWidget { +public: + MyWidget(QWidget* parent = nullptr) : QWidget(parent) { + // 获取全局动画工厂 + auto animationFactory = cf::WeakPtr::DynamicCast( + Application::animationFactory() + ); + + // 创建焦点指示器 + m_focusIndicator = new base::MdFocusIndicator(animationFactory, this); + } + +private: + base::MdFocusIndicator* m_focusIndicator; +}; +```text + +## 事件处理 + +焦点事件的转发非常直接,只需要在控件的事件处理中调用对应方法: + +```cpp +void MyWidget::focusInEvent(QFocusEvent* event) { + QWidget::focusInEvent(event); + m_focusIndicator->onFocusIn(); + update(); +} + +void MyWidget::focusOutEvent(QFocusEvent* event) { + QWidget::focusOutEvent(event); + m_focusIndicator->onFocusOut(); + update(); +} +```text + +⚠️ 记得在事件处理函数中先调用父类实现,否则 Qt 的焦点系统可能无法正常工作。 + +## 绘制聚焦环 + +聚焦环应该在绘制的最后阶段进行,确保它出现在所有内容的上层: + +```cpp +void MyWidget::paintEvent(QPaintEvent* event) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + // 先绘制背景、内容等... + drawBackground(p); + drawContent(p); + + // 最后绘制聚焦环(如果控件有焦点) + if (hasFocus()) { + CFColor indicatorColor = theme->getColor(MdColorRole::OnSurface); + m_focusIndicator->paint(&p, shape(), indicatorColor); + } +} +```text + +聚焦环的颜色通常使用 `onSurface` 角色获取,这样可以与控件内容保持一致的对比度。 + +## 形状处理 + +`paint()` 方法接受一个 `QPainterPath` 参数,这使得它能够适应各种控件形状: + +```cpp +// 圆角矩形按钮 +QPainterPath shape; +shape.addRoundedRect(rect(), cornerRadius, cornerRadius); +m_focusIndicator->paint(&p, shape, indicatorColor); + +// 圆形 FAB 按钮 +QPainterPath shape; +shape.addEllipse(rect()); +m_focusIndicator->paint(&p, shape, indicatorColor); + +// 自定义形状 +QPainterPath shape = customShape(); +m_focusIndicator->paint(&p, shape, indicatorColor); +```text + +环会自动沿着形状的边界向内偏移绘制,不需要手动计算偏移量。 + +## 动画性能 + +聚焦环使用工厂的 `md.animation.fadeIn` 和 `md.animation.fadeOut` 动画。如果全局动画被禁用,会直接设置透明度值而跳过动画: + +```cpp +// 在应用层面禁用动画 +auto factory = Application::animationFactory(); +if (factory) { + factory->setEnabledAll(false); +} +```text + +这对于低端设备或性能敏感的场景很有用。 + +## 键盘导航建议 + + Material Design 推荐的焦点策略是 `Qt::StrongFocus`,这样控件既可以通过点击获得焦点,也可以通过 Tab 键导航: + +```cpp +MyWidget::MyWidget(QWidget* parent) : QWidget(parent) { + setFocusPolicy(Qt::StrongFocus); + // ... +} +```text + +对于纯装饰性的控件,使用 `Qt::NoFocus` 避免干扰键盘导航流。 + +## 常见问题 + +如果聚焦环没有显示,检查以下几点: + +1. 控件是否真的有焦点(`hasFocus()` 返回 true) +2. 是否正确调用了 `onFocusIn()` +3. `paint()` 方法是否在 `paintEvent` 中被调用 +4. 传入的形状路径是否有效(非空) + +## 相关文档 + +- [StateMachine - Material 状态机](../widget/state_machine.md) +- [MdElevationController - 阴影控制器](./elevation_controller.md) +- [Material Design 3 焦点规范](https://m3.material.io/foundations/accessible-design/focus) diff --git a/document/HandBook/ui/material/base/index.md b/document/HandBook/ui/material/base/index.md index 513409880..404d1334e 100644 --- a/document/HandBook/ui/material/base/index.md +++ b/document/HandBook/ui/material/base/index.md @@ -1,10 +1,11 @@ -# Base - -> Welcome to the Base section. +--- +title: Material 基础 +description: 本章节包含 Material Design 3 基础渲染与行为机制的文档,涵盖状态层(State L +--- -## Overview +# Material 基础 -Documentation and resources for Base. +本章节包含 Material Design 3 基础渲染与行为机制的文档,涵盖状态层(State Layer)、涟漪效果(Ripple)的基础实现以及组件容器(Container)的绘制规范。这些基础模块为上层 Material 组件提供统一的视觉与交互基础。 --- diff --git a/document/HandBook/ui/material/base/painter_layer.md b/document/HandBook/ui/material/base/painter_layer.md index 9db3a9859..0e71865bd 100644 --- a/document/HandBook/ui/material/base/painter_layer.md +++ b/document/HandBook/ui/material/base/painter_layer.md @@ -1,216 +1,221 @@ -# PainterLayer - 绘图层管理器 - -`PainterLayer` 是 Material 分层绘制的基础组件。它的职责很简单:持有一个颜色和透明度值,在绘制时将两者叠加后填充到指定路径中。虽然看起来只是个颜色存储器,但单独抽出来的意义在于为状态层、遮罩层等提供统一接口,避免在每个控件里重复实现"带透明度的颜色填充"这个操作。 - -## 基本用法 - -`PainterLayer` 的使用分为三步:创建、设置属性、绘制: - -```cpp -#include "widget/material/base/painter_layer.h" - -using namespace cf::ui::widget::material; - -class MyWidget : public QWidget { -public: - MyWidget(QWidget* parent = nullptr) : QWidget(parent) { - // 创建绘制层 - m_stateLayer = new base::PainterLayer(this); - - // 设置颜色和透明度 - m_stateLayer->setColor(cf::ui::base::CFColor::fromRGB(0, 0, 0)); - m_stateLayer->setOpacity(0.08f); // 8% 透明度 - } - -private: - base::PainterLayer* m_stateLayer; -}; -``` - -## 绘制调用 - -在 `paintEvent` 中调用 `paint()` 方法,传入激活的 `QPainter` 和裁剪路径: - -```cpp -void MyWidget::paintEvent(QPaintEvent* event) { - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - // 先绘制背景 - p.fillPath(shape, backgroundColor()); - - // 绘制状态层 - m_stateLayer->paint(&p, shape); - - // 再绘制其他内容... -} -``` - -`paint()` 方法内部会处理透明度小于等于零的情况直接返回,所以不需要在外层判断。 - -## 透明度叠加 - -最终的绘制颜色是 `setColor()` 的颜色 alpha 值乘以 `setOpacity()` 的值: - -```cpp -// 内部实现(简化) -void PainterLayer::paint(QPainter* painter, const QPainterPath& clipPath) { - if (!painter || opacity_ <= 0.0f) return; - - QColor color = cached_color_.native_color(); - color.setAlphaF(color.alphaF() * opacity_); // 叠加透明度 - - painter->fillPath(clipPath, color); -} -``` - -这意味着如果颜色本身是半透明的(比如 alpha = 0.5),再设置 opacity = 0.5,最终 alpha 会是 0.25。这个设计允许你用一个基础颜色控制整体色调,用 opacity 控制当前状态下的强度。 - -## 状态层使用场景 - -`PainterLayer` 最常见的用途是实现 Material 的状态层(State Layer)——悬停、按下等交互时叠加的半透明层: - -```cpp -class MyWidget : public QWidget { - // ... 构造函数中初始化 ... - - void mousePressEvent(QMouseEvent* event) override { - // 按下时增加状态层透明度 - m_stateLayer->setOpacity(0.12f); - update(); - } - - void mouseReleaseEvent(QMouseEvent* event) override { - // 恢复默认透明度 - m_stateLayer->setOpacity(0.0f); - update(); - } - - void enterEvent(QEnterEvent* event) override { - // 悬停时轻微的视觉反馈 - m_stateLayer->setOpacity(0.08f); - update(); - } - - void leaveEvent(QEvent* event) override { - m_stateLayer->setOpacity(0.0f); - update(); - } -}; -``` - -实际项目中我们通常配合 `StateMachine` 来管理这些状态变化,而不是在每个事件里手动设置。 - -## 颜色选择 - -状态层的颜色通常使用文本颜色(label color),这样可以确保对比度: - -```cpp -void MyWidget::updateStateLayerColor() { - auto* theme = getTheme(); - auto* colors = static_cast(theme->color_scheme()); - - // 文本在表面色上的颜色 - QColor onSurface = colors->queryExpectedColor("md.onSurface"); - - m_stateLayer->setColor(cf::ui::base::CFColor(onSurface)); -} -``` - -⚠️ 不要用背景色做状态层颜色,那会改变控件的"色调"而不是"深浅"。Material 的状态层是通过叠加一层半透明的文本颜色来模拟"变深"或"变亮"的视觉效果。 - -## 性能特点 - -`PainterLayer` 本身不存储任何需要重绘的触发机制——它只是个被动的数据持有者。如果你想监听属性变化来触发重绘,需要自己处理: - -```cpp -// 手动触发重绘 -m_stateLayer->setOpacity(newOpacity); -update(); // 别忘了这个 -``` - -这和 `RippleHelper` 的设计不同——后者内部管理动画并主动发出 `repaintNeeded()` 信号,因为涟漪是"主动"的视觉效果,而状态层是"被动"的。 - -## 多层叠加 - -Material 控件可能需要多个绘制层,比如同时有状态层和遮罩层: - -```cpp -class MyWidget : public QWidget { -public: - MyWidget(QWidget* parent = nullptr) : QWidget(parent) { - // 状态层:交互反馈 - m_stateLayer = new base::PainterLayer(this); - m_stateLayer->setColor(labelColor); - m_stateLayer->setOpacity(0.0f); - - // 遮罩层:禁用时的半透明遮罩 - m_maskLayer = new base::PainterLayer(this); - m_maskLayer->setColor(cf::ui::base::CFColor::fromRGB(0, 0, 0)); - m_maskLayer->setOpacity(0.0f); - } - - void paintEvent(QPaintEvent* event) override { - QPainter p(this); - - // 背景层 - p.fillPath(shape, backgroundColor()); - - // 状态层 - m_stateLayer->paint(&p, shape); - - // 遮罩层(禁用时) - if (!isEnabled()) { - m_maskLayer->setOpacity(0.38f); - m_maskLayer->paint(&p, shape); - } - - // 内容层 - drawContent(p); - } - -private: - base::PainterLayer* m_stateLayer; - base::PainterLayer* m_maskLayer; -}; -``` - -绘制顺序很重要:背景 → 状态层 → 遮罩层 → 内容。改变顺序会破坏视觉层次。 - -## 内存管理 - -`PainterLayer` 继承自 `QObject`,支持 Qt 的父子对象内存管理: - -```cpp -// parent 为 this 时,会在控件销毁时自动删除 -m_layer = new base::PainterLayer(this); - -// 手动管理也是可以的 -m_layer = new base::PainterLayer(nullptr); -// ... 使用完毕后 -delete m_layer; -``` - -## 为什么不直接用 QColor - -这是个好问题。既然 `PainterLayer` 只是存储颜色和透明度,为什么不直接在控件里存两个成员变量? - -```cpp -// 看起来更简单的方式 -QColor m_stateColor; -float m_stateOpacity = 0.0f; -``` - -问题在于一致性——当有多个控件需要状态层、多个层需要管理时,每个控件都要自己实现"填充带透明度的颜色到路径"的逻辑,容易出错。抽出 `PainterLayer` 后: - -1. 统一的接口,`setColor()` + `setOpacity()` + `paint()` -2. 未来可以方便地添加渐变支持、混合模式等高级特性 -3. 便于测试,可以独立验证绘制逻辑 - -这是我们设计基础组件时的通用原则:简单可以,但一致更重要。 - -## 相关文档 - -- [RippleHelper - 涟漪效果助手](./ripple_helper.md) -- [StateMachine - Material 状态机](../widget/state_machine.md) -- [Button - Material 按钮](../widget/button.md) +--- +title: "PainterLayer - 绘图层管理器" +description: 是 Material 分层绘制的基础组件。它的职责很简单:持有一个颜色和透明度值,在绘制时将两者叠加 +--- + +# PainterLayer - 绘图层管理器 + +`PainterLayer` 是 Material 分层绘制的基础组件。它的职责很简单:持有一个颜色和透明度值,在绘制时将两者叠加后填充到指定路径中。虽然看起来只是个颜色存储器,但单独抽出来的意义在于为状态层、遮罩层等提供统一接口,避免在每个控件里重复实现"带透明度的颜色填充"这个操作。 + +## 基本用法 + +`PainterLayer` 的使用分为三步:创建、设置属性、绘制: + +```cpp +#include "widget/material/base/painter_layer.h" + +using namespace cf::ui::widget::material; + +class MyWidget : public QWidget { +public: + MyWidget(QWidget* parent = nullptr) : QWidget(parent) { + // 创建绘制层 + m_stateLayer = new base::PainterLayer(this); + + // 设置颜色和透明度 + m_stateLayer->setColor(cf::ui::base::CFColor::fromRGB(0, 0, 0)); + m_stateLayer->setOpacity(0.08f); // 8% 透明度 + } + +private: + base::PainterLayer* m_stateLayer; +}; +```text + +## 绘制调用 + +在 `paintEvent` 中调用 `paint()` 方法,传入激活的 `QPainter` 和裁剪路径: + +```cpp +void MyWidget::paintEvent(QPaintEvent* event) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + // 先绘制背景 + p.fillPath(shape, backgroundColor()); + + // 绘制状态层 + m_stateLayer->paint(&p, shape); + + // 再绘制其他内容... +} +```text + +`paint()` 方法内部会处理透明度小于等于零的情况直接返回,所以不需要在外层判断。 + +## 透明度叠加 + +最终的绘制颜色是 `setColor()` 的颜色 alpha 值乘以 `setOpacity()` 的值: + +```cpp +// 内部实现(简化) +void PainterLayer::paint(QPainter* painter, const QPainterPath& clipPath) { + if (!painter || opacity_ <= 0.0f) return; + + QColor color = cached_color_.native_color(); + color.setAlphaF(color.alphaF() * opacity_); // 叠加透明度 + + painter->fillPath(clipPath, color); +} +```text + +这意味着如果颜色本身是半透明的(比如 alpha = 0.5),再设置 opacity = 0.5,最终 alpha 会是 0.25。这个设计允许你用一个基础颜色控制整体色调,用 opacity 控制当前状态下的强度。 + +## 状态层使用场景 + +`PainterLayer` 最常见的用途是实现 Material 的状态层(State Layer)——悬停、按下等交互时叠加的半透明层: + +```cpp +class MyWidget : public QWidget { + // ... 构造函数中初始化 ... + + void mousePressEvent(QMouseEvent* event) override { + // 按下时增加状态层透明度 + m_stateLayer->setOpacity(0.12f); + update(); + } + + void mouseReleaseEvent(QMouseEvent* event) override { + // 恢复默认透明度 + m_stateLayer->setOpacity(0.0f); + update(); + } + + void enterEvent(QEnterEvent* event) override { + // 悬停时轻微的视觉反馈 + m_stateLayer->setOpacity(0.08f); + update(); + } + + void leaveEvent(QEvent* event) override { + m_stateLayer->setOpacity(0.0f); + update(); + } +}; +```text + +实际项目中我们通常配合 `StateMachine` 来管理这些状态变化,而不是在每个事件里手动设置。 + +## 颜色选择 + +状态层的颜色通常使用文本颜色(label color),这样可以确保对比度: + +```cpp +void MyWidget::updateStateLayerColor() { + auto* theme = getTheme(); + auto* colors = static_cast(theme->color_scheme()); + + // 文本在表面色上的颜色 + QColor onSurface = colors->queryExpectedColor("md.onSurface"); + + m_stateLayer->setColor(cf::ui::base::CFColor(onSurface)); +} +```text + +⚠️ 不要用背景色做状态层颜色,那会改变控件的"色调"而不是"深浅"。Material 的状态层是通过叠加一层半透明的文本颜色来模拟"变深"或"变亮"的视觉效果。 + +## 性能特点 + +`PainterLayer` 本身不存储任何需要重绘的触发机制——它只是个被动的数据持有者。如果你想监听属性变化来触发重绘,需要自己处理: + +```cpp +// 手动触发重绘 +m_stateLayer->setOpacity(newOpacity); +update(); // 别忘了这个 +```text + +这和 `RippleHelper` 的设计不同——后者内部管理动画并主动发出 `repaintNeeded()` 信号,因为涟漪是"主动"的视觉效果,而状态层是"被动"的。 + +## 多层叠加 + +Material 控件可能需要多个绘制层,比如同时有状态层和遮罩层: + +```cpp +class MyWidget : public QWidget { +public: + MyWidget(QWidget* parent = nullptr) : QWidget(parent) { + // 状态层:交互反馈 + m_stateLayer = new base::PainterLayer(this); + m_stateLayer->setColor(labelColor); + m_stateLayer->setOpacity(0.0f); + + // 遮罩层:禁用时的半透明遮罩 + m_maskLayer = new base::PainterLayer(this); + m_maskLayer->setColor(cf::ui::base::CFColor::fromRGB(0, 0, 0)); + m_maskLayer->setOpacity(0.0f); + } + + void paintEvent(QPaintEvent* event) override { + QPainter p(this); + + // 背景层 + p.fillPath(shape, backgroundColor()); + + // 状态层 + m_stateLayer->paint(&p, shape); + + // 遮罩层(禁用时) + if (!isEnabled()) { + m_maskLayer->setOpacity(0.38f); + m_maskLayer->paint(&p, shape); + } + + // 内容层 + drawContent(p); + } + +private: + base::PainterLayer* m_stateLayer; + base::PainterLayer* m_maskLayer; +}; +```text + +绘制顺序很重要:背景 → 状态层 → 遮罩层 → 内容。改变顺序会破坏视觉层次。 + +## 内存管理 + +`PainterLayer` 继承自 `QObject`,支持 Qt 的父子对象内存管理: + +```cpp +// parent 为 this 时,会在控件销毁时自动删除 +m_layer = new base::PainterLayer(this); + +// 手动管理也是可以的 +m_layer = new base::PainterLayer(nullptr); +// ... 使用完毕后 +delete m_layer; +```text + +## 为什么不直接用 QColor + +这是个好问题。既然 `PainterLayer` 只是存储颜色和透明度,为什么不直接在控件里存两个成员变量? + +```cpp +// 看起来更简单的方式 +QColor m_stateColor; +float m_stateOpacity = 0.0f; +```text + +问题在于一致性——当有多个控件需要状态层、多个层需要管理时,每个控件都要自己实现"填充带透明度的颜色到路径"的逻辑,容易出错。抽出 `PainterLayer` 后: + +1. 统一的接口,`setColor()` + `setOpacity()` + `paint()` +2. 未来可以方便地添加渐变支持、混合模式等高级特性 +3. 便于测试,可以独立验证绘制逻辑 + +这是我们设计基础组件时的通用原则:简单可以,但一致更重要。 + +## 相关文档 + +- [RippleHelper - 涟漪效果助手](./ripple_helper.md) +- [StateMachine - Material 状态机](../widget/state_machine.md) +- [Button - Material 按钮](../widget/button.md) diff --git a/document/HandBook/ui/material/base/ripple_helper.md b/document/HandBook/ui/material/base/ripple_helper.md index 089f236ef..eb5d1bd79 100644 --- a/document/HandBook/ui/material/base/ripple_helper.md +++ b/document/HandBook/ui/material/base/ripple_helper.md @@ -1,185 +1,190 @@ -# RippleHelper - Material 涟漪效果助手 - -`RippleHelper` 是 Material Design 水波纹效果的完整实现。当用户点击 Material 控件时,从点击位置扩散出的圆形涟漪能提供即时的触觉反馈。我们选择单独实现这个组件,是因为 Qt 的默认效果反馈太"僵硬",而 Material 的涟漪需要精确的扩散半径计算和淡入淡出时序控制。 - -## 效果原理 - -Material 涟漪效果的本质是一个从点击中心向外扩散的圆形渐变。按下时涟漪从零半径扩展到最大半径(覆盖整个控件),松开时透明度逐渐淡出直到消失。扩散和淡出两个动画同时进行,松开越早淡出越明显。 - -最大半径的计算方式是从点击中心到控件最远角点的距离,这样确保无论点击哪里,涟漪都能完全覆盖控件: - -```cpp -float maxRadius(const QRectF& rect, const QPointF& center) { - // 计算到四个角点的距离,取最大值 - float d1 = std::hypot(center.x() - rect.topLeft().x(), center.y() - rect.topLeft().y()); - float d2 = std::hypot(center.x() - rect.topRight().x(), center.y() - rect.topRight().y()); - float d3 = std::hypot(center.x() - rect.bottomLeft().x(), center.y() - rect.bottomLeft().y()); - float d4 = std::hypot(center.x() - rect.bottomRight().x(), center.y() - rect.bottomRight().y()); - return std::max({d1, d2, d3, d4}); -} -``` - -## 基本用法 - -在控件构造函数中初始化 `RippleHelper`,需要传入动画工厂: - -```cpp -#include "widget/material/base/ripple_helper.h" - -using namespace cf::ui::widget::material; - -class MyWidget : public QWidget { -public: - MyWidget(QWidget* parent = nullptr) : QWidget(parent) { - // 获取动画工厂 - auto animationFactory = cf::WeakPtr::DynamicCast( - Application::animationFactory() - ); - - // 创建涟漪助手 - m_rippleHelper = new base::RippleHelper(animationFactory, this); - - // 设置涟漪颜色(通常使用控件的主色调) - m_rippleHelper->setColor(cf::ui::base::CFColor::fromRGB(100, 100, 255)); - - // 监听重绘请求 - connect(m_rippleHelper, &base::RippleHelper::repaintNeeded, - this, QOverload<>::of(&MyWidget::update)); - } - -private: - base::RippleHelper* m_rippleHelper; -}; -``` - -## 事件处理 - -将鼠标事件转发给 `RippleHelper` 来驱动涟漪动画: - -```cpp -void MyWidget::mousePressEvent(QMouseEvent* event) { - QWidget::mousePressEvent(event); - m_rippleHelper->onPress(event->pos(), rect()); - update(); -} - -void MyWidget::mouseReleaseEvent(QMouseEvent* event) { - QWidget::mouseReleaseEvent(event); - m_rippleHelper->onRelease(); - update(); -} -``` - -`onCancel()` 用于取消未释放的涟漪,比如鼠标拖出控件范围时: - -```cpp -void MyWidget::leaveEvent(QEvent* event) { - QWidget::leaveEvent(event); - m_rippleHelper->onCancel(); // 清除所有涟漪 - update(); -} -``` - -⚠️ `onCancel()` 会立即清除所有涟漪,不做淡出动画。这是有意为之的设计——当用户明确"取消"交互时,不需要延迟反馈。 - -## 渲染模式 - -`RippleHelper` 支持两种渲染模式,区别在于是否裁剪涟漪到控件边界: - -```cpp -enum class Mode { - Bounded, // 涟漪被裁剪到控件形状内(默认) - Unbounded // 涟漪可以超出控件边界 -}; - -// 设置模式 -m_rippleHelper->setMode(base::RippleHelper::Mode::Bounded); -``` - -大多数控件应该使用 `Bounded` 模式,让涟漪限制在圆角矩形内。`Unbounded` 模式适用于特殊场景,比如浮动操作按钮(FAB)的涟漪可以扩散到圆形边界外。 - -## 绘制涟漪 - -在 `paintEvent` 中,涟漪应该绘制在状态层之上、内容之下: - -```cpp -void MyWidget::paintEvent(QPaintEvent* event) { - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - // 绘制背景 - p.fillPath(shape, backgroundColor()); - - // 绘制状态层(如果有的话) - // drawStateLayer(p, shape); - - // 绘制涟漪 - QPainterPath clipPath; - clipPath.addRoundedRect(rect(), cornerRadius, cornerRadius); - m_rippleHelper->paint(&p, clipPath); - - // 绘制内容 - // drawContent(p); -} -``` - -## 颜色设置 - -涟漪颜色通常使用控件内容颜色(label color)的半透明版本: - -```cpp -// 从主题获取颜色 -auto* theme = getTheme(); -auto* colors = static_cast(theme->color_scheme()); -QColor labelColor = colors->queryExpectedColor("md.onSurface"); - -// 设置涟漪颜色 -m_rippleHelper->setColor(cf::ui::base::CFColor(labelColor)); -``` - -在浅色主题上使用深色涟漪,深色主题上使用浅色涟漪,这是确保对比度的基本要求。 - -## 渲染细节 - -`RippleHelper` 内部使用径向渐变(`QRadialGradient`)绘制涟漪,中心实心、边缘柔和渐变到透明: - -```cpp -// 内部实现(简化) -QRadialGradient gradient(ripple.center, ripple.radius); -gradient.setColorAt(0.0f, color); // 中心完全实色 -gradient.setColorAt(0.7f, color); // 70% 半径处仍是实色 -gradient.setColorAt(1.0f, transparent); // 边缘渐变到透明 -``` - -这种处理避免了锯齿边缘,使涟漪看起来更自然。 - -## 性能考虑 - -涟漪效果依赖动画工厂,如果全局动画被禁用,`onPress()` 会直接返回,不创建任何涟漪: - -```cpp -// 禁用所有动画(包括涟漪) -auto factory = Application::animationFactory(); -if (factory) { - factory->setEnabledAll(false); -} -``` - -这对于低端设备或性能敏感的场景很有用。另外,`hasActiveRipple()` 可以用来判断是否需要重绘,避免无效的 `paintEvent` 调用。 - -## 动画名称 - -`RippleHelper` 使用动画工厂中注册的以下动画: - -| 名称 | 用途 | 范围 | -|------|------|------| -| `md.animation.rippleExpand` | 涟漪扩散 | 0.0 → 1.0 | -| `md.animation.rippleFade` | 涟漪淡出 | 1.0 → 0.0 | - -确保动画工厂中注册了这两个动画,否则涟漪不会显示。 - -## 相关文档 - -- [StateMachine - Material 状态机](../widget/state_machine.md) -- [PainterLayer - 绘图层管理器](./painter_layer.md) -- [CFMaterialAnimationFactory - 动画工厂](../animation/cfmaterial_animation_factory.md) +--- +title: "RippleHelper - Material 涟漪效果助手" +description: 是 Material Design 水波纹效果的完整实现。当用户点击 Material 控件时,从点 +--- + +# RippleHelper - Material 涟漪效果助手 + +`RippleHelper` 是 Material Design 水波纹效果的完整实现。当用户点击 Material 控件时,从点击位置扩散出的圆形涟漪能提供即时的触觉反馈。我们选择单独实现这个组件,是因为 Qt 的默认效果反馈太"僵硬",而 Material 的涟漪需要精确的扩散半径计算和淡入淡出时序控制。 + +## 效果原理 + +Material 涟漪效果的本质是一个从点击中心向外扩散的圆形渐变。按下时涟漪从零半径扩展到最大半径(覆盖整个控件),松开时透明度逐渐淡出直到消失。扩散和淡出两个动画同时进行,松开越早淡出越明显。 + +最大半径的计算方式是从点击中心到控件最远角点的距离,这样确保无论点击哪里,涟漪都能完全覆盖控件: + +```cpp +float maxRadius(const QRectF& rect, const QPointF& center) { + // 计算到四个角点的距离,取最大值 + float d1 = std::hypot(center.x() - rect.topLeft().x(), center.y() - rect.topLeft().y()); + float d2 = std::hypot(center.x() - rect.topRight().x(), center.y() - rect.topRight().y()); + float d3 = std::hypot(center.x() - rect.bottomLeft().x(), center.y() - rect.bottomLeft().y()); + float d4 = std::hypot(center.x() - rect.bottomRight().x(), center.y() - rect.bottomRight().y()); + return std::max({d1, d2, d3, d4}); +} +```text + +## 基本用法 + +在控件构造函数中初始化 `RippleHelper`,需要传入动画工厂: + +```cpp +#include "widget/material/base/ripple_helper.h" + +using namespace cf::ui::widget::material; + +class MyWidget : public QWidget { +public: + MyWidget(QWidget* parent = nullptr) : QWidget(parent) { + // 获取动画工厂 + auto animationFactory = cf::WeakPtr::DynamicCast( + Application::animationFactory() + ); + + // 创建涟漪助手 + m_rippleHelper = new base::RippleHelper(animationFactory, this); + + // 设置涟漪颜色(通常使用控件的主色调) + m_rippleHelper->setColor(cf::ui::base::CFColor::fromRGB(100, 100, 255)); + + // 监听重绘请求 + connect(m_rippleHelper, &base::RippleHelper::repaintNeeded, + this, QOverload<>::of(&MyWidget::update)); + } + +private: + base::RippleHelper* m_rippleHelper; +}; +```text + +## 事件处理 + +将鼠标事件转发给 `RippleHelper` 来驱动涟漪动画: + +```cpp +void MyWidget::mousePressEvent(QMouseEvent* event) { + QWidget::mousePressEvent(event); + m_rippleHelper->onPress(event->pos(), rect()); + update(); +} + +void MyWidget::mouseReleaseEvent(QMouseEvent* event) { + QWidget::mouseReleaseEvent(event); + m_rippleHelper->onRelease(); + update(); +} +```text + +`onCancel()` 用于取消未释放的涟漪,比如鼠标拖出控件范围时: + +```cpp +void MyWidget::leaveEvent(QEvent* event) { + QWidget::leaveEvent(event); + m_rippleHelper->onCancel(); // 清除所有涟漪 + update(); +} +```text + +⚠️ `onCancel()` 会立即清除所有涟漪,不做淡出动画。这是有意为之的设计——当用户明确"取消"交互时,不需要延迟反馈。 + +## 渲染模式 + +`RippleHelper` 支持两种渲染模式,区别在于是否裁剪涟漪到控件边界: + +```cpp +enum class Mode { + Bounded, // 涟漪被裁剪到控件形状内(默认) + Unbounded // 涟漪可以超出控件边界 +}; + +// 设置模式 +m_rippleHelper->setMode(base::RippleHelper::Mode::Bounded); +```text + +大多数控件应该使用 `Bounded` 模式,让涟漪限制在圆角矩形内。`Unbounded` 模式适用于特殊场景,比如浮动操作按钮(FAB)的涟漪可以扩散到圆形边界外。 + +## 绘制涟漪 + +在 `paintEvent` 中,涟漪应该绘制在状态层之上、内容之下: + +```cpp +void MyWidget::paintEvent(QPaintEvent* event) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + // 绘制背景 + p.fillPath(shape, backgroundColor()); + + // 绘制状态层(如果有的话) + // drawStateLayer(p, shape); + + // 绘制涟漪 + QPainterPath clipPath; + clipPath.addRoundedRect(rect(), cornerRadius, cornerRadius); + m_rippleHelper->paint(&p, clipPath); + + // 绘制内容 + // drawContent(p); +} +```text + +## 颜色设置 + +涟漪颜色通常使用控件内容颜色(label color)的半透明版本: + +```cpp +// 从主题获取颜色 +auto* theme = getTheme(); +auto* colors = static_cast(theme->color_scheme()); +QColor labelColor = colors->queryExpectedColor("md.onSurface"); + +// 设置涟漪颜色 +m_rippleHelper->setColor(cf::ui::base::CFColor(labelColor)); +```text + +在浅色主题上使用深色涟漪,深色主题上使用浅色涟漪,这是确保对比度的基本要求。 + +## 渲染细节 + +`RippleHelper` 内部使用径向渐变(`QRadialGradient`)绘制涟漪,中心实心、边缘柔和渐变到透明: + +```cpp +// 内部实现(简化) +QRadialGradient gradient(ripple.center, ripple.radius); +gradient.setColorAt(0.0f, color); // 中心完全实色 +gradient.setColorAt(0.7f, color); // 70% 半径处仍是实色 +gradient.setColorAt(1.0f, transparent); // 边缘渐变到透明 +```text + +这种处理避免了锯齿边缘,使涟漪看起来更自然。 + +## 性能考虑 + +涟漪效果依赖动画工厂,如果全局动画被禁用,`onPress()` 会直接返回,不创建任何涟漪: + +```cpp +// 禁用所有动画(包括涟漪) +auto factory = Application::animationFactory(); +if (factory) { + factory->setEnabledAll(false); +} +```bash + +这对于低端设备或性能敏感的场景很有用。另外,`hasActiveRipple()` 可以用来判断是否需要重绘,避免无效的 `paintEvent` 调用。 + +## 动画名称 + +`RippleHelper` 使用动画工厂中注册的以下动画: + +| 名称 | 用途 | 范围 | +|------|------|------| +| `md.animation.rippleExpand` | 涟漪扩散 | 0.0 → 1.0 | +| `md.animation.rippleFade` | 涟漪淡出 | 1.0 → 0.0 | + +确保动画工厂中注册了这两个动画,否则涟漪不会显示。 + +## 相关文档 + +- [StateMachine - Material 状态机](../widget/state_machine.md) +- [PainterLayer - 绘图层管理器](./painter_layer.md) +- [CFMaterialAnimationFactory - 动画工厂](../animation/cfmaterial_animation_factory.md) diff --git a/document/HandBook/ui/material/cfmaterial_fonttype.md b/document/HandBook/ui/material/cfmaterial_fonttype.md index 97094e7ac..366a67070 100644 --- a/document/HandBook/ui/material/cfmaterial_fonttype.md +++ b/document/HandBook/ui/material/cfmaterial_fonttype.md @@ -1,152 +1,157 @@ -# MaterialTypography - Material 字体系统 - -`MaterialTypography` 实现 Material Design 3 的 Type Scale 系统。Material 把字体规格分成 5 大类、15 个具体样式,每个样式都有精确的大小、字重和行高规定。我们用嵌入式 Token 注册表存储这些 QFont 对象,每个排版实例独立管理自己的字体配置。 - -## Type Scale 规格 - -Material Design 3 定义了完整的 Type Scale: - -| 类别 | 样式 | 大小 | 字重 | 行高 | 用途 | -|------|------|------|------|------|------| -| Display | displayLarge | 57sp | Regular | 64sp | 英雄内容 | -| Display | displayMedium | 45sp | Regular | 52sp | 英雄内容 | -| Display | displaySmall | 36sp | Regular | 44sp | 英雄内容 | -| Headline | headlineLarge | 32sp | Regular | 40sp | 应用栏重要文本 | -| Headline | headlineMedium | 28sp | Regular | 36sp | 应用栏重要文本 | -| Headline | headlineSmall | 24sp | Regular | 32sp | 应用栏重要文本 | -| Title | titleLarge | 22sp | Medium | 28sp | 章节标题 | -| Title | titleMedium | 16sp | Medium | 24sp | 章节标题 | -| Title | titleSmall | 14sp | Medium | 20sp | 章节标题 | -| Body | bodyLarge | 16sp | Regular | 24sp | 正文内容 | -| Body | bodyMedium | 14sp | Regular | 20sp | 正文内容 | -| Body | bodySmall | 12sp | Regular | 16sp | 正文内容 | -| Label | labelLarge | 14sp | Medium | 20sp | 次要信息 | -| Label | labelMedium | 12sp | Medium | 16sp | 次要信息 | -| Label | labelSmall | 11sp | Medium | 16sp | 次要信息 | - -## 基本用法 - -通过工厂函数创建默认排版系统: - -```cpp -#include "material_factory.hpp" - -// 创建默认排版 -auto typography = cf::ui::core::material::defaultTypography(); - -// 查询字体 -QFont titleFont = typography.queryTargetFont("md.typography.titleLarge"); -QFont bodyFont = typography.queryTargetFont("md.typography.bodyMedium"); -``` - -字体名称采用 `md.typography.` 前缀,后跟 Material 官方定义的样式名称。 - -## 字体组访问 - -可以通过类型安全的访问器获取字体组: - -```cpp -MaterialTypography typography = material::defaultTypography(); - -// 获取标题组 -TitleFonts title = typography.title(); -// 这些 Token 类型配合 Token Registry 使用 - -// 获取正文组 -BodyFonts body = typography.body(); -``` - -字体组结构体(`DisplayFonts`、`HeadlineFonts` 等)主要是为了类型安全和代码可读性。 - -## 行高查询 - -每个字体样式都有对应的行高规范: - -```cpp -MaterialTypography typography = material::defaultTypography(); - -float lineHeight = typography.getLineHeight("md.typography.bodyLarge"); -// 返回 24.0(单位 sp) -``` - -行高在多行文本布局时特别重要,确保行与行之间有合适的呼吸空间。 - -## 平台字体选择 - -我们根据平台自动选择合适的系统字体: - -| 平台 | 默认字体 | 中文回退 | -|------|----------|----------| -| Windows | Segoe UI | Microsoft YaHei UI | -| macOS | .SF NS Text | PingFang SC | -| Linux | Ubuntu | Noto Sans CJK SC | - -这样设计的好处是应用在各平台上看起来"原生",同时保持一致的排版比例。如果需要自定义字体,可以直接修改 registry: - -```cpp -MaterialTypography typography = material::defaultTypography(); - -QFont customFont("Roboto"); -customFont.setPointSize(16); -typography.registry().set("md.typography.bodyMedium", customFont); -``` - -## 缓存机制 - -字体查询有缓存层,避免重复解析字符串: - -```cpp -// 第一次查询会查找并缓存 -QFont font1 = typography.queryTargetFont("md.typography.titleLarge"); - -// 后续查询从缓存返回 -QFont font2 = typography.queryTargetFont("md.typography.titleLarge"); -``` - -缓存在 `MaterialTypography` 对象生命周期内有效。 - -## 在 Qt 中使用 - -获取的 `QFont` 可以直接用于 Qt 组件: - -```cpp -void MyLabel::updateFont() { - auto* theme = getTheme(); - auto* typography = static_cast(theme->font_type()); - - QFont titleFont = typography->queryTargetFont("md.typography.titleLarge"); - setFont(titleFont); -} -``` - -也可以直接设置到 `QPainter`: - -```cpp -void paintEvent(QPaintEvent*) { - QPainter painter(this); - - auto* typography = getMaterialTypography(); - QFont bodyFont = typography->queryTargetFont("md.typography.bodyMedium"); - painter.setFont(bodyFont); - - painter.drawText(rect(), "Hello Material"); -} -``` - -## 大小单位说明 - -Material 规范使用 `sp`(Scale-independent Pixels)作为字体大小单位。`sp` 和 `dp` 类似,但会根据用户设置的字体大小缩放。在 Qt 中,我们用 `pointSize` 来近似实现 `sp` 的行为: - -```cpp -// Material 规范:16sp -QFont font; -font.setPointSize(16); // Qt 中用 pointSize 近似 -``` - -这个近似在实际使用中效果足够好,因为 Qt 的字体系统已经有良好的 DPI 处理。 - -## 相关文档 - -- [MaterialTheme - 主题组合](./cfmaterial_theme.md) -- [MaterialColorScheme - 颜色方案](./cfmaterial_scheme.md) -- [MaterialRadiusScale - 圆角系统](./cfmaterial_radius_scale.md) +--- +title: "MaterialTypography - Material 字体系统" +description: 实现 Material Design 3 的 Type Scale 系统。Material 把字体规 +--- + +# MaterialTypography - Material 字体系统 + +`MaterialTypography` 实现 Material Design 3 的 Type Scale 系统。Material 把字体规格分成 5 大类、15 个具体样式,每个样式都有精确的大小、字重和行高规定。我们用嵌入式 Token 注册表存储这些 QFont 对象,每个排版实例独立管理自己的字体配置。 + +## Type Scale 规格 + +Material Design 3 定义了完整的 Type Scale: + +| 类别 | 样式 | 大小 | 字重 | 行高 | 用途 | +|------|------|------|------|------|------| +| Display | displayLarge | 57sp | Regular | 64sp | 英雄内容 | +| Display | displayMedium | 45sp | Regular | 52sp | 英雄内容 | +| Display | displaySmall | 36sp | Regular | 44sp | 英雄内容 | +| Headline | headlineLarge | 32sp | Regular | 40sp | 应用栏重要文本 | +| Headline | headlineMedium | 28sp | Regular | 36sp | 应用栏重要文本 | +| Headline | headlineSmall | 24sp | Regular | 32sp | 应用栏重要文本 | +| Title | titleLarge | 22sp | Medium | 28sp | 章节标题 | +| Title | titleMedium | 16sp | Medium | 24sp | 章节标题 | +| Title | titleSmall | 14sp | Medium | 20sp | 章节标题 | +| Body | bodyLarge | 16sp | Regular | 24sp | 正文内容 | +| Body | bodyMedium | 14sp | Regular | 20sp | 正文内容 | +| Body | bodySmall | 12sp | Regular | 16sp | 正文内容 | +| Label | labelLarge | 14sp | Medium | 20sp | 次要信息 | +| Label | labelMedium | 12sp | Medium | 16sp | 次要信息 | +| Label | labelSmall | 11sp | Medium | 16sp | 次要信息 | + +## 基本用法 + +通过工厂函数创建默认排版系统: + +```cpp +#include "material_factory.hpp" + +// 创建默认排版 +auto typography = cf::ui::core::material::defaultTypography(); + +// 查询字体 +QFont titleFont = typography.queryTargetFont("md.typography.titleLarge"); +QFont bodyFont = typography.queryTargetFont("md.typography.bodyMedium"); +```text + +字体名称采用 `md.typography.` 前缀,后跟 Material 官方定义的样式名称。 + +## 字体组访问 + +可以通过类型安全的访问器获取字体组: + +```cpp +MaterialTypography typography = material::defaultTypography(); + +// 获取标题组 +TitleFonts title = typography.title(); +// 这些 Token 类型配合 Token Registry 使用 + +// 获取正文组 +BodyFonts body = typography.body(); +```text + +字体组结构体(`DisplayFonts`、`HeadlineFonts` 等)主要是为了类型安全和代码可读性。 + +## 行高查询 + +每个字体样式都有对应的行高规范: + +```cpp +MaterialTypography typography = material::defaultTypography(); + +float lineHeight = typography.getLineHeight("md.typography.bodyLarge"); +// 返回 24.0(单位 sp) +```bash + +行高在多行文本布局时特别重要,确保行与行之间有合适的呼吸空间。 + +## 平台字体选择 + +我们根据平台自动选择合适的系统字体: + +| 平台 | 默认字体 | 中文回退 | +|------|----------|----------| +| Windows | Segoe UI | Microsoft YaHei UI | +| macOS | .SF NS Text | PingFang SC | +| Linux | Ubuntu | Noto Sans CJK SC | + +这样设计的好处是应用在各平台上看起来"原生",同时保持一致的排版比例。如果需要自定义字体,可以直接修改 registry: + +```cpp +MaterialTypography typography = material::defaultTypography(); + +QFont customFont("Roboto"); +customFont.setPointSize(16); +typography.registry().set("md.typography.bodyMedium", customFont); +```text + +## 缓存机制 + +字体查询有缓存层,避免重复解析字符串: + +```cpp +// 第一次查询会查找并缓存 +QFont font1 = typography.queryTargetFont("md.typography.titleLarge"); + +// 后续查询从缓存返回 +QFont font2 = typography.queryTargetFont("md.typography.titleLarge"); +```text + +缓存在 `MaterialTypography` 对象生命周期内有效。 + +## 在 Qt 中使用 + +获取的 `QFont` 可以直接用于 Qt 组件: + +```cpp +void MyLabel::updateFont() { + auto* theme = getTheme(); + auto* typography = static_cast(theme->font_type()); + + QFont titleFont = typography->queryTargetFont("md.typography.titleLarge"); + setFont(titleFont); +} +```text + +也可以直接设置到 `QPainter`: + +```cpp +void paintEvent(QPaintEvent*) { + QPainter painter(this); + + auto* typography = getMaterialTypography(); + QFont bodyFont = typography->queryTargetFont("md.typography.bodyMedium"); + painter.setFont(bodyFont); + + painter.drawText(rect(), "Hello Material"); +} +```text + +## 大小单位说明 + +Material 规范使用 `sp`(Scale-independent Pixels)作为字体大小单位。`sp` 和 `dp` 类似,但会根据用户设置的字体大小缩放。在 Qt 中,我们用 `pointSize` 来近似实现 `sp` 的行为: + +```cpp +// Material 规范:16sp +QFont font; +font.setPointSize(16); // Qt 中用 pointSize 近似 +```text + +这个近似在实际使用中效果足够好,因为 Qt 的字体系统已经有良好的 DPI 处理。 + +## 相关文档 + +- [MaterialTheme - 主题组合](./cfmaterial_theme.md) +- [MaterialColorScheme - 颜色方案](./cfmaterial_scheme.md) +- [MaterialRadiusScale - 圆角系统](./cfmaterial_radius_scale.md) diff --git a/document/HandBook/ui/material/cfmaterial_motion.md b/document/HandBook/ui/material/cfmaterial_motion.md index 63856a599..b0e32cc1f 100644 --- a/document/HandBook/ui/material/cfmaterial_motion.md +++ b/document/HandBook/ui/material/cfmaterial_motion.md @@ -1,196 +1,201 @@ -# MaterialMotionScheme - Material 动画系统 - -`MaterialMotionScheme` 实现 Material Design 3 的动画规范。Material 把动画分成 9 种预设,每种都有精确的时长和缓动曲线。这套系统的核心思想是"自然运动"——模仿真实物理世界的运动规律,让界面感觉更"真实"。 - -## 运动预设 - -Material Design 3 定义了 9 种运动预设: - -| 预设 | 时长 | 缓动曲线 | 用途 | -|------|------|----------|------| -| shortEnter | 200ms | EmphasizedDecelerate | 小元素进入屏幕 | -| shortExit | 150ms | EmphasizedAccelerate | 小元素退出屏幕 | -| mediumEnter | 300ms | EmphasizedDecelerate | 中等元素进入 | -| mediumExit | 250ms | EmphasizedAccelerate | 中等元素退出 | -| longEnter | 450ms | Emphasized | 大元素进入 | -| longExit | 400ms | Emphasized | 大元素退出 | -| stateChange | 200ms | Standard | 状态层动画 | -| rippleExpand | 400ms | Standard | 水波纹扩展 | -| rippleFade | 150ms | Linear | 水波纹淡出 | - -## 基本用法 - -通过工厂函数创建默认运动系统: - -```cpp -#include "material_factory.hpp" - -// 创建默认运动规范 -auto motion = cf::ui::core::material::motion(); - -// 查询时长 -int duration = motion.queryDuration("shortEnter"); // 200 - -// 查询缓动 -int easing = motion.queryEasing("shortEnter"); - -// 获取完整运动规格 -MotionSpec spec = motion.getMotionSpec("mediumEnter"); -// spec.durationMs = 300 -// spec.easing = Easing::Type::EmphasizedDecelerate -``` - -## 运动规格结构 - -`MotionSpec` 把时长、缓动和延迟打包在一起: - -```cpp -struct MotionSpec { - int durationMs; // 时长(毫秒) - cf::ui::base::Easing::Type easing; // 缓动类型 - int delayMs = 0; // 延迟(毫秒) -}; -``` - -这个结构可以直接用于动画设置: - -```cpp -void MyWidget::animateIn() { - auto* motion = getMotionScheme(); - MotionSpec spec = motion->getMotionSpec("mediumEnter"); - - QPropertyAnimation* anim = new QPropertyAnimation(this, "pos"); - anim->setDuration(spec.durationMs); - anim->setEasingCurve(spec.toEasingCurve()); - anim->setStartValue(offScreenPos()); - anim->setEndValue(targetPos()); - anim->start(QAbstractAnimation::DeleteWhenStopped); -} -``` - -## 静态预设函数 - -除了通过字符串查询,也可以直接用静态函数: - -```cpp -// 短进入动画 -MotionSpec spec = MotionPresets::shortEnter(); -// durationMs = 200, easing = EmphasizedDecelerate - -// 中退出动画 -MotionSpec spec = MotionPresets::mediumExit(); -// durationMs = 250, easing = EmphasizedAccelerate - -// 状态切换动画 -MotionSpec spec = MotionPresets::stateChange(); -// durationMs = 200, easing = Standard -``` - -静态函数在编译期就能确定值,没有字符串查找开销,性能更好。 - -## 获取所有预设 - -可以用 `presets()` 一次性获取所有预设: - -```cpp -MaterialMotionScheme motion = material::motion(); -auto all = motion.presets(); - -// 使用各个预设 -useSpec(all.shortEnter); -useSpec(all.mediumExit); -useSpec(all.rippleExpand); -``` - -这个设计方便在需要同时使用多个预设时减少代码重复。 - -## 缓动曲线说明 - -Material 使用自定义缓动曲线,不是标准的 Qt 曲线: - -| Material 曲线 | 描述 | -|---------------|------| -| Standard | 标准缓动,类似 EaseOutCubic | -| Emphasized | 强调缓动,快速开始平滑结束 | -| EmphasizedDecelerate | 强调减速,快速开始明显减速 | -| EmphasizedAccelerate | 强调加速,慢速开始快速结束 | -| Linear | 线性,匀速运动 | - -我们的 `cf::ui::base::Easing` 模块实现了这些曲线,`MotionSpec::toEasingCurve()` 会转换成 Qt 的 `QEasingCurve`。 - -## 元素大小对应关系 - -不同大小的元素应该用不同的运动时长: - -```cpp -// 根据元素大小选择预设 -MotionSpec spec; -if (size.width() < 200) { - spec = MotionPresets::shortEnter(); // 小元素 -} else if (size.width() < 500) { - spec = MotionPresets::mediumEnter(); // 中元素 -} else { - spec = MotionPresets::longEnter(); // 大元素 -} -``` - -这个对应关系能保证动画速度和元素大小匹配——大物体运动慢,小物体运动快,符合物理直觉。 - -## 进场和出场 - -进入动画通常比退出动画长,这是因为人眼对"出现"的过程更敏感: - -```cpp -// 进入:200ms(短) -animateIn(MotionPresets::shortEnter()); - -// 退出:150ms(短) -animateOut(MotionPresets::shortExit()); -``` - -同样的"短"级别,进入比退出慢 50ms。 - -## 水波纹效果 - -Material 的点击水波纹有专门的预设: - -```cpp -void RippleEffect::start() { - auto* motion = getMotionScheme(); - - // 扩展阶段 - m_expandSpec = motion->getMotionSpec("rippleExpand"); - // 400ms, Standard - - // 淡出阶段 - m_fadeSpec = motion->getMotionSpec("rippleFade"); - // 150ms, Linear -} -``` - -水波纹淡出用 Linear 是有原因的——淡出是透明度变化,用线性曲线更自然。 - -## 自定义运动规格 - -可以创建自定义 `MotionSpec`: - -```cpp -MotionSpec customSpec; -customSpec.durationMs = 500; -customSpec.easing = cf::ui::base::Easing::Type::Emphasized; -customSpec.delayMs = 100; -``` - -或者基于预设修改: - -```cpp -MotionSpec spec = MotionPresets::mediumEnter(); -spec.delayMs = 200; // 添加延迟 -spec.durationMs = 400; // 延长时长 -``` - -## 相关文档 - -- [MaterialTheme - 主题组合](./cfmaterial_theme.md) -- [MaterialColorScheme - 颜色方案](./cfmaterial_scheme.md) -- [base/easing.h - 缓动曲线](../base/easing.md) +--- +title: "MaterialMotionScheme - Material 动画系统" +description: 实现 Material Design 3 的动画规范。Material 把动画分成 9 种预设,每种 +--- + +# MaterialMotionScheme - Material 动画系统 + +`MaterialMotionScheme` 实现 Material Design 3 的动画规范。Material 把动画分成 9 种预设,每种都有精确的时长和缓动曲线。这套系统的核心思想是"自然运动"——模仿真实物理世界的运动规律,让界面感觉更"真实"。 + +## 运动预设 + +Material Design 3 定义了 9 种运动预设: + +| 预设 | 时长 | 缓动曲线 | 用途 | +|------|------|----------|------| +| shortEnter | 200ms | EmphasizedDecelerate | 小元素进入屏幕 | +| shortExit | 150ms | EmphasizedAccelerate | 小元素退出屏幕 | +| mediumEnter | 300ms | EmphasizedDecelerate | 中等元素进入 | +| mediumExit | 250ms | EmphasizedAccelerate | 中等元素退出 | +| longEnter | 450ms | Emphasized | 大元素进入 | +| longExit | 400ms | Emphasized | 大元素退出 | +| stateChange | 200ms | Standard | 状态层动画 | +| rippleExpand | 400ms | Standard | 水波纹扩展 | +| rippleFade | 150ms | Linear | 水波纹淡出 | + +## 基本用法 + +通过工厂函数创建默认运动系统: + +```cpp +#include "material_factory.hpp" + +// 创建默认运动规范 +auto motion = cf::ui::core::material::motion(); + +// 查询时长 +int duration = motion.queryDuration("shortEnter"); // 200 + +// 查询缓动 +int easing = motion.queryEasing("shortEnter"); + +// 获取完整运动规格 +MotionSpec spec = motion.getMotionSpec("mediumEnter"); +// spec.durationMs = 300 +// spec.easing = Easing::Type::EmphasizedDecelerate +```text + +## 运动规格结构 + +`MotionSpec` 把时长、缓动和延迟打包在一起: + +```cpp +struct MotionSpec { + int durationMs; // 时长(毫秒) + cf::ui::base::Easing::Type easing; // 缓动类型 + int delayMs = 0; // 延迟(毫秒) +}; +```text + +这个结构可以直接用于动画设置: + +```cpp +void MyWidget::animateIn() { + auto* motion = getMotionScheme(); + MotionSpec spec = motion->getMotionSpec("mediumEnter"); + + QPropertyAnimation* anim = new QPropertyAnimation(this, "pos"); + anim->setDuration(spec.durationMs); + anim->setEasingCurve(spec.toEasingCurve()); + anim->setStartValue(offScreenPos()); + anim->setEndValue(targetPos()); + anim->start(QAbstractAnimation::DeleteWhenStopped); +} +```text + +## 静态预设函数 + +除了通过字符串查询,也可以直接用静态函数: + +```cpp +// 短进入动画 +MotionSpec spec = MotionPresets::shortEnter(); +// durationMs = 200, easing = EmphasizedDecelerate + +// 中退出动画 +MotionSpec spec = MotionPresets::mediumExit(); +// durationMs = 250, easing = EmphasizedAccelerate + +// 状态切换动画 +MotionSpec spec = MotionPresets::stateChange(); +// durationMs = 200, easing = Standard +```text + +静态函数在编译期就能确定值,没有字符串查找开销,性能更好。 + +## 获取所有预设 + +可以用 `presets()` 一次性获取所有预设: + +```cpp +MaterialMotionScheme motion = material::motion(); +auto all = motion.presets(); + +// 使用各个预设 +useSpec(all.shortEnter); +useSpec(all.mediumExit); +useSpec(all.rippleExpand); +```bash + +这个设计方便在需要同时使用多个预设时减少代码重复。 + +## 缓动曲线说明 + +Material 使用自定义缓动曲线,不是标准的 Qt 曲线: + +| Material 曲线 | 描述 | +|---------------|------| +| Standard | 标准缓动,类似 EaseOutCubic | +| Emphasized | 强调缓动,快速开始平滑结束 | +| EmphasizedDecelerate | 强调减速,快速开始明显减速 | +| EmphasizedAccelerate | 强调加速,慢速开始快速结束 | +| Linear | 线性,匀速运动 | + +我们的 `cf::ui::base::Easing` 模块实现了这些曲线,`MotionSpec::toEasingCurve()` 会转换成 Qt 的 `QEasingCurve`。 + +## 元素大小对应关系 + +不同大小的元素应该用不同的运动时长: + +```cpp +// 根据元素大小选择预设 +MotionSpec spec; +if (size.width() < 200) { + spec = MotionPresets::shortEnter(); // 小元素 +} else if (size.width() < 500) { + spec = MotionPresets::mediumEnter(); // 中元素 +} else { + spec = MotionPresets::longEnter(); // 大元素 +} +```text + +这个对应关系能保证动画速度和元素大小匹配——大物体运动慢,小物体运动快,符合物理直觉。 + +## 进场和出场 + +进入动画通常比退出动画长,这是因为人眼对"出现"的过程更敏感: + +```cpp +// 进入:200ms(短) +animateIn(MotionPresets::shortEnter()); + +// 退出:150ms(短) +animateOut(MotionPresets::shortExit()); +```text + +同样的"短"级别,进入比退出慢 50ms。 + +## 水波纹效果 + +Material 的点击水波纹有专门的预设: + +```cpp +void RippleEffect::start() { + auto* motion = getMotionScheme(); + + // 扩展阶段 + m_expandSpec = motion->getMotionSpec("rippleExpand"); + // 400ms, Standard + + // 淡出阶段 + m_fadeSpec = motion->getMotionSpec("rippleFade"); + // 150ms, Linear +} +```text + +水波纹淡出用 Linear 是有原因的——淡出是透明度变化,用线性曲线更自然。 + +## 自定义运动规格 + +可以创建自定义 `MotionSpec`: + +```cpp +MotionSpec customSpec; +customSpec.durationMs = 500; +customSpec.easing = cf::ui::base::Easing::Type::Emphasized; +customSpec.delayMs = 100; +```text + +或者基于预设修改: + +```cpp +MotionSpec spec = MotionPresets::mediumEnter(); +spec.delayMs = 200; // 添加延迟 +spec.durationMs = 400; // 延长时长 +```text + +## 相关文档 + +- [MaterialTheme - 主题组合](./cfmaterial_theme.md) +- [MaterialColorScheme - 颜色方案](./cfmaterial_scheme.md) +- [base/easing.h - 缓动曲线](../base/easing.md) diff --git a/document/HandBook/ui/material/cfmaterial_radius_scale.md b/document/HandBook/ui/material/cfmaterial_radius_scale.md index 3819e3955..e108cc7da 100644 --- a/document/HandBook/ui/material/cfmaterial_radius_scale.md +++ b/document/HandBook/ui/material/cfmaterial_radius_scale.md @@ -1,152 +1,157 @@ -# MaterialRadiusScale - Material 圆角系统 - -`MaterialRadiusScale` 实现 Material Design 3 的圆角规范。Material 用 7 个标准圆角值覆盖几乎所有 UI 场景,从完全方形到超大圆角都有定义。我们把它们存在嵌入式 Token 注册表中,每个圆角规格实例独立管理。 - -## 圆角规格 - -Material Design 3 定义了 7 个标准圆角: - -| Token | 值 (dp) | 用途 | -|-------|---------|------| -| cornerNone | 0dp | 无圆角(矩形) | -| cornerExtraSmall | 4dp | Chip、小卡片 | -| cornerSmall | 8dp | 文本框、复选框 | -| cornerMedium | 12dp | 卡片 | -| cornerLarge | 16dp | 警告对话框 | -| cornerExtraLarge | 28dp | FAB、模态框 | -| cornerExtraExtraLarge | 32dp | 抽屉 | - -这个设计很有意思——只要记住 7 个数字,就能保证整个 UI 的形状一致性。不需要每个组件单独设计圆角。 - -## 基本用法 - -通过工厂函数创建默认圆角系统: - -```cpp -#include "material_factory.hpp" - -// 创建默认圆角规格 -auto radiusScale = cf::ui::core::material::defaultRadiusScale(); - -// 查询圆角 -float smallRadius = radiusScale.queryRadiusScale("md.shape.cornerSmall"); // 8.0f -float mediumRadius = radiusScale.queryRadiusScale("md.shape.cornerMedium"); // 12.0f -float largeRadius = radiusScale.queryRadiusScale("md.shape.cornerLarge"); // 16.0f -``` - -圆角名称采用 `md.shape.` 前缀,后跟 Material 官方定义的规格名称。 - -## 在 Qt 中使用 - -获取的圆角值可以直接用于 Qt 组件: - -```cpp -void MyCard::setupAppearance() { - auto* theme = getTheme(); - auto* radius = static_cast(theme->radius_scale()); - - float cardRadius = radius->queryRadiusScale("md.shape.cornerMedium"); - - // 应用到样式表 - QString style = QString("QWidget { border-radius: %1px; }").arg(cardRadius); - setStyleSheet(style); - - // 或者用于 QPainter - m_borderRadius = cardRadius; -} - -void MyCard::paintEvent(QPaintEvent*) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QRectF rect = this->rect().adjusted(1, 1, -1, -1); - painter.drawRoundedRect(rect, m_borderRadius, m_borderRadius); -} -``` - -## 组件对应关系 - -不同组件对应不同圆角规格,Material 有推荐值: - -```cpp -// 按钮用小圆角 -float buttonRadius = radiusScale.queryRadiusScale("md.shape.cornerSmall"); // 8dp - -// 卡片用中圆角 -float cardRadius = radiusScale.queryRadiusScale("md.shape.cornerMedium"); // 12dp - -// 对话框用大圆角 -float dialogRadius = radiusScale.queryRadiusScale("md.shape.cornerLarge"); // 16dp - -// FAB 用超大圆角 -float fabRadius = radiusScale.queryRadiusScale("md.shape.cornerExtraLarge"); // 28dp - -// 侧边栏用超大超大圆角 -float drawerRadius = radiusScale.queryRadiusScale("md.shape.cornerExtraExtraLarge"); // 32dp -``` - -## 自定义圆角 - -如果需要自定义圆角,可以直接修改 registry: - -```cpp -MaterialRadiusScale radiusScale = material::defaultRadiusScale(); - -// 添加自定义圆角 -radiusScale.registry().set("md.shape.cornerCustom", 20.0f); - -// 覆盖现有圆角 -radiusScale.registry().set("md.shape.cornerMedium", 16.0f); // 改成和 Large 一样 -``` - -这个设计让系统既支持标准 Material 规范,也允许根据品牌需求调整。 - -## 缓存机制 - -圆角查询有缓存层,避免重复解析字符串: - -```cpp -// 第一次查询会查找并缓存 -float r1 = radiusScale.queryRadiusScale("md.shape.cornerLarge"); - -// 后续查询从缓存返回 -float r2 = radiusScale.queryRadiusScale("md.shape.cornerLarge"); -``` - -缓存在 `MaterialRadiusScale` 对象生命周期内有效。 - -## 单位说明 - -圆角值以 `dp`(Density-independent Pixels)为单位。在我们的实现中直接返回浮点数,调用方负责根据 DPI 缩放: - -```cpp -// 获取 dp 值 -float radiusDp = radiusScale.queryRadiusScale("md.shape.cornerMedium"); - -// 转换为物理像素 -qreal scaleFactor = devicePixelRatioF(); -float radiusPx = radiusDp * scaleFactor; -``` - -大多数情况下直接用 dp 值传入 Qt 函数就可以,Qt 会自动处理 DPI 缩放。 - -## 高 DPI 注意事项 - -在高 DPI 屏幕上,圆角值可能需要特殊处理: - -```cpp -// 确保 QPainter 开启抗锯齿 -QPainter painter(this); -painter.setRenderHint(QPainter::Antialiasing); - -// 使用 float 精度的绘制函数 -painter.drawRoundedRect(rect, radius, radius); -``` - -小圆角值(如 4dp 的 ExtraSmall)在高 DPI 下可能会被平滑成几乎直线,这是正常行为。 - -## 相关文档 - -- [MaterialTheme - 主题组合](./cfmaterial_theme.md) -- [MaterialColorScheme - 颜色方案](./cfmaterial_scheme.md) -- [MaterialTypography - 字体系统](./cfmaterial_fonttype.md) +--- +title: "MaterialRadiusScale - Material 圆角系统" +description: 实现 Material Design 3 的圆角规范。Material 用 7 个标准圆角值覆盖几乎 +--- + +# MaterialRadiusScale - Material 圆角系统 + +`MaterialRadiusScale` 实现 Material Design 3 的圆角规范。Material 用 7 个标准圆角值覆盖几乎所有 UI 场景,从完全方形到超大圆角都有定义。我们把它们存在嵌入式 Token 注册表中,每个圆角规格实例独立管理。 + +## 圆角规格 + +Material Design 3 定义了 7 个标准圆角: + +| Token | 值 (dp) | 用途 | +|-------|---------|------| +| cornerNone | 0dp | 无圆角(矩形) | +| cornerExtraSmall | 4dp | Chip、小卡片 | +| cornerSmall | 8dp | 文本框、复选框 | +| cornerMedium | 12dp | 卡片 | +| cornerLarge | 16dp | 警告对话框 | +| cornerExtraLarge | 28dp | FAB、模态框 | +| cornerExtraExtraLarge | 32dp | 抽屉 | + +这个设计很有意思——只要记住 7 个数字,就能保证整个 UI 的形状一致性。不需要每个组件单独设计圆角。 + +## 基本用法 + +通过工厂函数创建默认圆角系统: + +```cpp +#include "material_factory.hpp" + +// 创建默认圆角规格 +auto radiusScale = cf::ui::core::material::defaultRadiusScale(); + +// 查询圆角 +float smallRadius = radiusScale.queryRadiusScale("md.shape.cornerSmall"); // 8.0f +float mediumRadius = radiusScale.queryRadiusScale("md.shape.cornerMedium"); // 12.0f +float largeRadius = radiusScale.queryRadiusScale("md.shape.cornerLarge"); // 16.0f +```text + +圆角名称采用 `md.shape.` 前缀,后跟 Material 官方定义的规格名称。 + +## 在 Qt 中使用 + +获取的圆角值可以直接用于 Qt 组件: + +```cpp +void MyCard::setupAppearance() { + auto* theme = getTheme(); + auto* radius = static_cast(theme->radius_scale()); + + float cardRadius = radius->queryRadiusScale("md.shape.cornerMedium"); + + // 应用到样式表 + QString style = QString("QWidget { border-radius: %1px; }").arg(cardRadius); + setStyleSheet(style); + + // 或者用于 QPainter + m_borderRadius = cardRadius; +} + +void MyCard::paintEvent(QPaintEvent*) { + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + QRectF rect = this->rect().adjusted(1, 1, -1, -1); + painter.drawRoundedRect(rect, m_borderRadius, m_borderRadius); +} +```text + +## 组件对应关系 + +不同组件对应不同圆角规格,Material 有推荐值: + +```cpp +// 按钮用小圆角 +float buttonRadius = radiusScale.queryRadiusScale("md.shape.cornerSmall"); // 8dp + +// 卡片用中圆角 +float cardRadius = radiusScale.queryRadiusScale("md.shape.cornerMedium"); // 12dp + +// 对话框用大圆角 +float dialogRadius = radiusScale.queryRadiusScale("md.shape.cornerLarge"); // 16dp + +// FAB 用超大圆角 +float fabRadius = radiusScale.queryRadiusScale("md.shape.cornerExtraLarge"); // 28dp + +// 侧边栏用超大超大圆角 +float drawerRadius = radiusScale.queryRadiusScale("md.shape.cornerExtraExtraLarge"); // 32dp +```text + +## 自定义圆角 + +如果需要自定义圆角,可以直接修改 registry: + +```cpp +MaterialRadiusScale radiusScale = material::defaultRadiusScale(); + +// 添加自定义圆角 +radiusScale.registry().set("md.shape.cornerCustom", 20.0f); + +// 覆盖现有圆角 +radiusScale.registry().set("md.shape.cornerMedium", 16.0f); // 改成和 Large 一样 +```text + +这个设计让系统既支持标准 Material 规范,也允许根据品牌需求调整。 + +## 缓存机制 + +圆角查询有缓存层,避免重复解析字符串: + +```cpp +// 第一次查询会查找并缓存 +float r1 = radiusScale.queryRadiusScale("md.shape.cornerLarge"); + +// 后续查询从缓存返回 +float r2 = radiusScale.queryRadiusScale("md.shape.cornerLarge"); +```text + +缓存在 `MaterialRadiusScale` 对象生命周期内有效。 + +## 单位说明 + +圆角值以 `dp`(Density-independent Pixels)为单位。在我们的实现中直接返回浮点数,调用方负责根据 DPI 缩放: + +```cpp +// 获取 dp 值 +float radiusDp = radiusScale.queryRadiusScale("md.shape.cornerMedium"); + +// 转换为物理像素 +qreal scaleFactor = devicePixelRatioF(); +float radiusPx = radiusDp * scaleFactor; +```text + +大多数情况下直接用 dp 值传入 Qt 函数就可以,Qt 会自动处理 DPI 缩放。 + +## 高 DPI 注意事项 + +在高 DPI 屏幕上,圆角值可能需要特殊处理: + +```cpp +// 确保 QPainter 开启抗锯齿 +QPainter painter(this); +painter.setRenderHint(QPainter::Antialiasing); + +// 使用 float 精度的绘制函数 +painter.drawRoundedRect(rect, radius, radius); +```text + +小圆角值(如 4dp 的 ExtraSmall)在高 DPI 下可能会被平滑成几乎直线,这是正常行为。 + +## 相关文档 + +- [MaterialTheme - 主题组合](./cfmaterial_theme.md) +- [MaterialColorScheme - 颜色方案](./cfmaterial_scheme.md) +- [MaterialTypography - 字体系统](./cfmaterial_fonttype.md) diff --git a/document/HandBook/ui/material/cfmaterial_scheme.md b/document/HandBook/ui/material/cfmaterial_scheme.md index 09a56e6a3..f9798cf32 100644 --- a/document/HandBook/ui/material/cfmaterial_scheme.md +++ b/document/HandBook/ui/material/cfmaterial_scheme.md @@ -1,180 +1,185 @@ -# MaterialColorScheme - Material Design 3 颜色方案 - -`MaterialColorScheme` 是我们对 Material Design 3 颜色系统的完整实现。Material You 的核心就是动态颜色系统——从一个种子颜色自动生成 26 个语义化颜色,确保整个 UI 在视觉上协调一致。我们把这个系统做成了嵌入式 Token 注册表的形式,每个颜色方案实例都独立管理自己的颜色值。 - -## Material Design 3 颜色系统 - -Material Design 3 的颜色系统是基于"色调调色板"(Tonal Palette)设计的,而不是传统 HSL/HSV。每个颜色角色都有对应的"On"颜色用于显示在其上方的文本。系统把颜色分成几个组: - -- **Primary**:主色,用于关键组件如按钮、活跃状态 -- **Secondary**:辅助色,用于次要组件和强调 -- **Tertiary**:第三色,用于平衡和表达品牌独特性 -- **Error**:错误色,用于错误状态和破坏性操作 -- **Surface**:表面色,包括背景、表面及其变体 -- **Utility**:工具色,支持阴影、遮罩和反色状态 - -## 基本用法 - -通过工厂函数创建颜色方案是最简单的方式: - -```cpp -#include "material_factory.hpp" - -// 创建默认浅色主题(Material 基准紫色) -auto lightScheme = cf::ui::core::material::light(); - -// 创建默认深色主题 -auto darkScheme = cf::ui::core::material::dark(); - -// 查询颜色 -QColor primary = lightScheme.queryExpectedColor("md.primary"); -QColor onPrimary = lightScheme.queryExpectedColor("md.onPrimary"); -QColor surface = lightScheme.queryExpectedColor("md.surface"); -``` - -颜色名称采用 `md.` 前缀,后跟 Material 官方定义的 token 名称。这样设计是为了和其他主题系统(比如我们未来可能实现的 Fluent)做区分。 - -## 颜色组访问 - -虽然可以用字符串查询,但类型安全的方式是通过颜色组访问器: - -```cpp -using namespace cf::ui::core; - -MaterialColorScheme scheme = material::light(); - -// 获取主色组——返回的是包含 Token 类型的结构体 -PrimaryColors primary = scheme.primary(); -// 这些 Token 类型可以配合我们的 Token Registry 使用 - -// 需要实际颜色值时还是通过查询 -QColor primaryColor = scheme.queryExpectedColor("md.primary"); -QColor containerColor = scheme.queryExpectedColor("md.primaryContainer"); -``` - -颜色组结构体(`PrimaryColors`、`SecondaryColors` 等)主要是为了类型安全和文档目的,实际颜色值还是从 registry 中查询。 - -## 从种子颜色生成 - -Material You 的特色是"动态颜色"——用户选一个喜欢的颜色,系统自动生成完整的配色: - -```cpp -#include "base/color.h" - -// 从任意颜色生成配色 -CFColor seedColor("#6750A4"); -auto scheme = cf::ui::core::material::fromKeyColor(seedColor); - -// 生成深色版本 -auto darkScheme = cf::ui::core::material::fromKeyColor(seedColor, true); -``` - -这个功能内部使用 HCT 色彩空间和 Material 的色调调色板算法。HCT(Hue-Chroma-Tone)是 Material 团队专门开发的色彩空间,比 HSL 更符合人眼对颜色的感知——这也是为什么 Material 的配色看起来特别和谐的原因。 - -## 从 JSON 导入 - -Material Theme Builder 是 Google 官方的配色在线工具,导出的 JSON 我们可以直接解析: - -```cpp -QByteArray json = R"({ - "schemes": { - "light": { - "primary": "#6750A4", - "onPrimary": "#FFFFFF", - "primaryContainer": "#EADDFF", - ... - } - } -})"; - -// 从 JSON 创建(指定使用 light 方案) -auto result = cf::ui::core::material::fromJson(json, false); - -// cf::expected 风格的错误处理 -if (result) { - auto scheme = std::move(*result); - // 使用 scheme -} else { - const auto& err = result.error(); - if (err.kind == MaterialSchemeError::Kind::InvalidJson) { - qDebug() << "JSON 解析失败:" << err.message.c_str(); - } -} -``` - -也支持直接传入颜色对象的方式(没有 `schemes` 包装的扁平结构)。 - -## 导出到 JSON - -可以把当前配色导出为 JSON 格式,方便保存或分享: - -```cpp -MaterialColorScheme scheme = material::light(); -QByteArray json = material::toJson(scheme); - -// 可以保存到文件或传输 -QFile file("my_theme.json"); -file.open(QIODevice::WriteOnly); -file.write(json); -``` - -## Token 注册表 - -每个 `MaterialColorScheme` 内部都有一个 `EmbeddedTokenRegistry`,用来存储所有颜色 Token: - -```cpp -MaterialColorScheme scheme = material::light(); - -// 直接访问底层注册表 -auto& registry = scheme.registry(); - -// 可以手动修改或添加 Token -registry.set("md.customColor", QColor("#FF5722")); -``` - -这个设计让系统既支持预定义的 Material 颜色,也允许扩展自定义颜色。 - -## 缓存机制 - -颜色查询有缓存层,避免重复解析字符串: - -```cpp -// 第一次查询会查找并缓存 -QColor color1 = scheme.queryColor("md.primary"); - -// 后续查询从缓存返回 -QColor color2 = scheme.queryColor("md.primary"); -``` - -缓存在 `MaterialColorScheme` 对象生命周期内有效。如果需要修改某个颜色后立即生效,直接修改 registry 即可——查询接口每次都会从 registry 读取最新值。 - -## 完整颜色列表 - -Material Design 3 定义了 26 个颜色角色,对应的查询名称如下: - -``` -主色组: - md.primary, md.onPrimary, md.primaryContainer, md.onPrimaryContainer - -辅助色组: - md.secondary, md.onSecondary, md.secondaryContainer, md.onSecondaryContainer - -第三色组: - md.tertiary, md.onTertiary, md.tertiaryContainer, md.onTertiaryContainer - -错误色组: - md.error, md.onError, md.errorContainer, md.onErrorContainer - -表面色组: - md.background, md.onBackground, md.surface, md.onSurface, - md.surfaceVariant, md.onSurfaceVariant, md.outline, md.outlineVariant - -工具色组: - md.shadow, md.scrim, md.inverseSurface, md.inverseOnSurface, md.inversePrimary -``` - -## 相关文档 - -- [MaterialTheme - 主题组合](./cfmaterial_theme.md) -- [MaterialFactory - 主题工厂](./material_factory_class.md) -- [MaterialTypography - 字体系统](./cfmaterial_fonttype.md) +--- +title: "MaterialColorScheme - Material Design 3 颜色方案" +description: 是我们对 Material Design 3 颜色系统的完整实现。Material You 的核心就 +--- + +# MaterialColorScheme - Material Design 3 颜色方案 + +`MaterialColorScheme` 是我们对 Material Design 3 颜色系统的完整实现。Material You 的核心就是动态颜色系统——从一个种子颜色自动生成 26 个语义化颜色,确保整个 UI 在视觉上协调一致。我们把这个系统做成了嵌入式 Token 注册表的形式,每个颜色方案实例都独立管理自己的颜色值。 + +## Material Design 3 颜色系统 + +Material Design 3 的颜色系统是基于"色调调色板"(Tonal Palette)设计的,而不是传统 HSL/HSV。每个颜色角色都有对应的"On"颜色用于显示在其上方的文本。系统把颜色分成几个组: + +- **Primary**:主色,用于关键组件如按钮、活跃状态 +- **Secondary**:辅助色,用于次要组件和强调 +- **Tertiary**:第三色,用于平衡和表达品牌独特性 +- **Error**:错误色,用于错误状态和破坏性操作 +- **Surface**:表面色,包括背景、表面及其变体 +- **Utility**:工具色,支持阴影、遮罩和反色状态 + +## 基本用法 + +通过工厂函数创建颜色方案是最简单的方式: + +```cpp +#include "material_factory.hpp" + +// 创建默认浅色主题(Material 基准紫色) +auto lightScheme = cf::ui::core::material::light(); + +// 创建默认深色主题 +auto darkScheme = cf::ui::core::material::dark(); + +// 查询颜色 +QColor primary = lightScheme.queryExpectedColor("md.primary"); +QColor onPrimary = lightScheme.queryExpectedColor("md.onPrimary"); +QColor surface = lightScheme.queryExpectedColor("md.surface"); +```text + +颜色名称采用 `md.` 前缀,后跟 Material 官方定义的 token 名称。这样设计是为了和其他主题系统(比如我们未来可能实现的 Fluent)做区分。 + +## 颜色组访问 + +虽然可以用字符串查询,但类型安全的方式是通过颜色组访问器: + +```cpp +using namespace cf::ui::core; + +MaterialColorScheme scheme = material::light(); + +// 获取主色组——返回的是包含 Token 类型的结构体 +PrimaryColors primary = scheme.primary(); +// 这些 Token 类型可以配合我们的 Token Registry 使用 + +// 需要实际颜色值时还是通过查询 +QColor primaryColor = scheme.queryExpectedColor("md.primary"); +QColor containerColor = scheme.queryExpectedColor("md.primaryContainer"); +```text + +颜色组结构体(`PrimaryColors`、`SecondaryColors` 等)主要是为了类型安全和文档目的,实际颜色值还是从 registry 中查询。 + +## 从种子颜色生成 + +Material You 的特色是"动态颜色"——用户选一个喜欢的颜色,系统自动生成完整的配色: + +```cpp +#include "base/color.h" + +// 从任意颜色生成配色 +CFColor seedColor("#6750A4"); +auto scheme = cf::ui::core::material::fromKeyColor(seedColor); + +// 生成深色版本 +auto darkScheme = cf::ui::core::material::fromKeyColor(seedColor, true); +```text + +这个功能内部使用 HCT 色彩空间和 Material 的色调调色板算法。HCT(Hue-Chroma-Tone)是 Material 团队专门开发的色彩空间,比 HSL 更符合人眼对颜色的感知——这也是为什么 Material 的配色看起来特别和谐的原因。 + +## 从 JSON 导入 + +Material Theme Builder 是 Google 官方的配色在线工具,导出的 JSON 我们可以直接解析: + +```cpp +QByteArray json = R"({ + "schemes": { + "light": { + "primary": "#6750A4", + "onPrimary": "#FFFFFF", + "primaryContainer": "#EADDFF", + ... + } + } +})"; + +// 从 JSON 创建(指定使用 light 方案) +auto result = cf::ui::core::material::fromJson(json, false); + +// cf::expected 风格的错误处理 +if (result) { + auto scheme = std::move(*result); + // 使用 scheme +} else { + const auto& err = result.error(); + if (err.kind == MaterialSchemeError::Kind::InvalidJson) { + qDebug() << "JSON 解析失败:" << err.message.c_str(); + } +} +```text + +也支持直接传入颜色对象的方式(没有 `schemes` 包装的扁平结构)。 + +## 导出到 JSON + +可以把当前配色导出为 JSON 格式,方便保存或分享: + +```cpp +MaterialColorScheme scheme = material::light(); +QByteArray json = material::toJson(scheme); + +// 可以保存到文件或传输 +QFile file("my_theme.json"); +file.open(QIODevice::WriteOnly); +file.write(json); +```text + +## Token 注册表 + +每个 `MaterialColorScheme` 内部都有一个 `EmbeddedTokenRegistry`,用来存储所有颜色 Token: + +```cpp +MaterialColorScheme scheme = material::light(); + +// 直接访问底层注册表 +auto& registry = scheme.registry(); + +// 可以手动修改或添加 Token +registry.set("md.customColor", QColor("#FF5722")); +```text + +这个设计让系统既支持预定义的 Material 颜色,也允许扩展自定义颜色。 + +## 缓存机制 + +颜色查询有缓存层,避免重复解析字符串: + +```cpp +// 第一次查询会查找并缓存 +QColor color1 = scheme.queryColor("md.primary"); + +// 后续查询从缓存返回 +QColor color2 = scheme.queryColor("md.primary"); +```text + +缓存在 `MaterialColorScheme` 对象生命周期内有效。如果需要修改某个颜色后立即生效,直接修改 registry 即可——查询接口每次都会从 registry 读取最新值。 + +## 完整颜色列表 + +Material Design 3 定义了 26 个颜色角色,对应的查询名称如下: + +```text +主色组: + md.primary, md.onPrimary, md.primaryContainer, md.onPrimaryContainer + +辅助色组: + md.secondary, md.onSecondary, md.secondaryContainer, md.onSecondaryContainer + +第三色组: + md.tertiary, md.onTertiary, md.tertiaryContainer, md.onTertiaryContainer + +错误色组: + md.error, md.onError, md.errorContainer, md.onErrorContainer + +表面色组: + md.background, md.onBackground, md.surface, md.onSurface, + md.surfaceVariant, md.onSurfaceVariant, md.outline, md.outlineVariant + +工具色组: + md.shadow, md.scrim, md.inverseSurface, md.inverseOnSurface, md.inversePrimary +```text + +## 相关文档 + +- [MaterialTheme - 主题组合](./cfmaterial_theme.md) +- [MaterialFactory - 主题工厂](./material_factory_class.md) +- [MaterialTypography - 字体系统](./cfmaterial_fonttype.md) diff --git a/document/HandBook/ui/material/cfmaterial_theme.md b/document/HandBook/ui/material/cfmaterial_theme.md index 2190f215b..7658bac35 100644 --- a/document/HandBook/ui/material/cfmaterial_theme.md +++ b/document/HandBook/ui/material/cfmaterial_theme.md @@ -1,127 +1,132 @@ -# MaterialTheme - Material Design 3 主题 - -`MaterialTheme` 是 Material Design 3 的完整主题实现。它把颜色方案、字体、圆角和动画规范组合成一个整体,代表一个"完整"的 Material 主题。这个类本身不对外暴露构造函数,只能通过 `MaterialFactory` 创建——这样设计是为了确保主题组件之间的协调一致性。 - -## 主题组成 - -一个 Material 主题由四个部分组成: - -- **MaterialColorScheme**:26 个语义化颜色 -- **MaterialTypography**:15 个字体样式(Type Scale) -- **MaterialRadiusScale**:7 个圆角规格 -- **MaterialMotionScheme**:动画时长和缓动曲线 - -`MaterialTheme` 实现了 `ICFTheme` 接口,所以可以和我们的主题系统无缝集成。 - -## 创建主题 - -主题必须通过 `MaterialFactory` 创建: - -```cpp -#include "material_factory_class.h" - -MaterialFactory factory; - -// 按名称创建预定义主题 -auto lightTheme = factory.fromName("theme.material.light"); -auto darkTheme = factory.fromName("theme.material.dark"); - -// 从 JSON 创建 -QByteArray json = loadThemeJson(); -auto customTheme = factory.fromJson(json); -``` - -支持的预定义主题名称: -- `theme.material.light`:默认浅色主题 -- `theme.material.dark`:默认深色主题 - -## 访问主题组件 - -拿到 `ICFTheme` 指针后,可以通过接口访问各个组件: - -```cpp -std::unique_ptr theme = factory.fromName("theme.material.light"); - -// 访问颜色方案 -auto* colorScheme = static_cast(theme->color_scheme()); -QColor primary = colorScheme->queryExpectedColor("md.primary"); - -// 访问字体 -auto* typography = static_cast(theme->font_type()); -QFont titleFont = typography->queryTargetFont("md.typography.titleLarge"); - -// 访问圆角 -auto* radiusScale = static_cast(theme->radius_scale()); -float cardRadius = radiusScale->queryRadiusScale("md.shape.cornerMedium"); - -// 访问动画 -auto* motion = static_cast(theme->motion_spec()); -int duration = motion->queryDuration("shortEnter"); -``` - -这里需要 `static_cast` 是因为 `ICFTheme` 接口返回的是基类指针。在实际使用中,既然我们明确知道是 Material 主题,这样转换是安全的。 - -## 主题应用 - -主题通常配合 UI 组件使用。典型的使用模式是在应用启动时设置主题: - -```cpp -void Application::setupTheme() { - MaterialFactory factory; - - // 根据系统设置选择主题 - bool isDark = isSystemDarkMode(); - const char* themeName = isDark ? "theme.material.dark" : "theme.material.light"; - - auto theme = factory.fromName(themeName); - setTheme(std::move(theme)); -} - -// 在组件中使用主题 -void MyWidget::paintEvent(QPaintEvent*) { - auto* theme = getTheme(); - auto* colors = static_cast(theme->color_scheme()); - - QColor backgroundColor = colors->queryExpectedColor("md.surface"); - // 使用背景色绘制... -} -``` - -## 组件协调性 - -`MaterialTheme` 的设计确保各组件之间是协调的。比如工厂创建浅色主题时,会同时创建浅色的颜色方案、和浅色匹配的字体对比度、适配的圆角大小等。这种协调性通过工厂内部的预设配置保证,而不是在运行时计算。 - -⚠️ 不要自己手动拼凑 `MaterialTheme` 的各个组件然后期望它们协调工作——组件间的协调性只有通过工厂创建的主题才能保证。 - -## 不可移动/不可复制 - -`MaterialTheme` 禁止了拷贝和移动操作。这是因为基类 `ICFTheme` 存储了 `unique_ptr` 成员,移动语义在继承体系下会带来一些复杂问题。既然主题应该通过工厂创建并由智能指针管理,禁用移动反而能防止一些误用: - -```cpp -// 编译错误:MaterialTheme 不可移动 -MaterialTheme theme2 = std::move(theme1); - -// 正确做法:使用工厂创建新实例 -auto theme2 = factory.fromName("theme.material.light"); -``` - -## Material Design 3 规范对应 - -我们的实现严格遵循 Material Design 3 规范: - -| 组件 | MD3 规范 | 我们的实现 | -|------|----------|-----------| -| 颜色 | Dynamic Color / Tonal Palette | `MaterialColorScheme` | -| 字体 | Type Scale (15 styles) | `MaterialTypography` | -| 形状 | Shape Scheme (7 corner radii) | `MaterialRadiusScale` | -| 动画 | Motion Duration & Easing | `MaterialMotionScheme` | - -每个组件的手册文档都会详细说明其对应的 MD3 规范部分。 - -## 相关文档 - -- [MaterialColorScheme - 颜色方案](./cfmaterial_scheme.md) -- [MaterialTypography - 字体系统](./cfmaterial_fonttype.md) -- [MaterialRadiusScale - 圆角系统](./cfmaterial_radius_scale.md) -- [MaterialMotionScheme - 动画系统](./cfmaterial_motion.md) -- [MaterialFactory - 主题工厂](./material_factory_class.md) +--- +title: "MaterialTheme - Material Design 3 主题" +description: 是 Material Design 3 的完整主题实现。它把颜色方案、字体、圆角和动画规范组合成一个 +--- + +# MaterialTheme - Material Design 3 主题 + +`MaterialTheme` 是 Material Design 3 的完整主题实现。它把颜色方案、字体、圆角和动画规范组合成一个整体,代表一个"完整"的 Material 主题。这个类本身不对外暴露构造函数,只能通过 `MaterialFactory` 创建——这样设计是为了确保主题组件之间的协调一致性。 + +## 主题组成 + +一个 Material 主题由四个部分组成: + +- **MaterialColorScheme**:26 个语义化颜色 +- **MaterialTypography**:15 个字体样式(Type Scale) +- **MaterialRadiusScale**:7 个圆角规格 +- **MaterialMotionScheme**:动画时长和缓动曲线 + +`MaterialTheme` 实现了 `ICFTheme` 接口,所以可以和我们的主题系统无缝集成。 + +## 创建主题 + +主题必须通过 `MaterialFactory` 创建: + +```cpp +#include "material_factory_class.h" + +MaterialFactory factory; + +// 按名称创建预定义主题 +auto lightTheme = factory.fromName("theme.material.light"); +auto darkTheme = factory.fromName("theme.material.dark"); + +// 从 JSON 创建 +QByteArray json = loadThemeJson(); +auto customTheme = factory.fromJson(json); +```text + +支持的预定义主题名称: +- `theme.material.light`:默认浅色主题 +- `theme.material.dark`:默认深色主题 + +## 访问主题组件 + +拿到 `ICFTheme` 指针后,可以通过接口访问各个组件: + +```cpp +std::unique_ptr theme = factory.fromName("theme.material.light"); + +// 访问颜色方案 +auto* colorScheme = static_cast(theme->color_scheme()); +QColor primary = colorScheme->queryExpectedColor("md.primary"); + +// 访问字体 +auto* typography = static_cast(theme->font_type()); +QFont titleFont = typography->queryTargetFont("md.typography.titleLarge"); + +// 访问圆角 +auto* radiusScale = static_cast(theme->radius_scale()); +float cardRadius = radiusScale->queryRadiusScale("md.shape.cornerMedium"); + +// 访问动画 +auto* motion = static_cast(theme->motion_spec()); +int duration = motion->queryDuration("shortEnter"); +```text + +这里需要 `static_cast` 是因为 `ICFTheme` 接口返回的是基类指针。在实际使用中,既然我们明确知道是 Material 主题,这样转换是安全的。 + +## 主题应用 + +主题通常配合 UI 组件使用。典型的使用模式是在应用启动时设置主题: + +```cpp +void Application::setupTheme() { + MaterialFactory factory; + + // 根据系统设置选择主题 + bool isDark = isSystemDarkMode(); + const char* themeName = isDark ? "theme.material.dark" : "theme.material.light"; + + auto theme = factory.fromName(themeName); + setTheme(std::move(theme)); +} + +// 在组件中使用主题 +void MyWidget::paintEvent(QPaintEvent*) { + auto* theme = getTheme(); + auto* colors = static_cast(theme->color_scheme()); + + QColor backgroundColor = colors->queryExpectedColor("md.surface"); + // 使用背景色绘制... +} +```text + +## 组件协调性 + +`MaterialTheme` 的设计确保各组件之间是协调的。比如工厂创建浅色主题时,会同时创建浅色的颜色方案、和浅色匹配的字体对比度、适配的圆角大小等。这种协调性通过工厂内部的预设配置保证,而不是在运行时计算。 + +⚠️ 不要自己手动拼凑 `MaterialTheme` 的各个组件然后期望它们协调工作——组件间的协调性只有通过工厂创建的主题才能保证。 + +## 不可移动/不可复制 + +`MaterialTheme` 禁止了拷贝和移动操作。这是因为基类 `ICFTheme` 存储了 `unique_ptr` 成员,移动语义在继承体系下会带来一些复杂问题。既然主题应该通过工厂创建并由智能指针管理,禁用移动反而能防止一些误用: + +```cpp +// 编译错误:MaterialTheme 不可移动 +MaterialTheme theme2 = std::move(theme1); + +// 正确做法:使用工厂创建新实例 +auto theme2 = factory.fromName("theme.material.light"); +```bash + +## Material Design 3 规范对应 + +我们的实现严格遵循 Material Design 3 规范: + +| 组件 | MD3 规范 | 我们的实现 | +|------|----------|-----------| +| 颜色 | Dynamic Color / Tonal Palette | `MaterialColorScheme` | +| 字体 | Type Scale (15 styles) | `MaterialTypography` | +| 形状 | Shape Scheme (7 corner radii) | `MaterialRadiusScale` | +| 动画 | Motion Duration & Easing | `MaterialMotionScheme` | + +每个组件的手册文档都会详细说明其对应的 MD3 规范部分。 + +## 相关文档 + +- [MaterialColorScheme - 颜色方案](./cfmaterial_scheme.md) +- [MaterialTypography - 字体系统](./cfmaterial_fonttype.md) +- [MaterialRadiusScale - 圆角系统](./cfmaterial_radius_scale.md) +- [MaterialMotionScheme - 动画系统](./cfmaterial_motion.md) +- [MaterialFactory - 主题工厂](./material_factory_class.md) diff --git a/document/HandBook/ui/material/index.md b/document/HandBook/ui/material/index.md index 040654bb2..843440ba4 100644 --- a/document/HandBook/ui/material/index.md +++ b/document/HandBook/ui/material/index.md @@ -1,3 +1,8 @@ +--- +title: Material Design +description: CFDesktop 对 Material Design 3 规范的完整实现,包括动画系统、基础行为层 +--- + # Material Design CFDesktop 对 Material Design 3 规范的完整实现,包括动画系统、基础行为层和 20+ Widget 控件。 diff --git a/document/HandBook/ui/material/material_factory_class.md b/document/HandBook/ui/material/material_factory_class.md index 9b9ae3b77..aa8110cd4 100644 --- a/document/HandBook/ui/material/material_factory_class.md +++ b/document/HandBook/ui/material/material_factory_class.md @@ -1,133 +1,138 @@ -# MaterialFactory - Material 主题工厂 - -`MaterialFactory` 是创建 Material Design 3 主题的入口。它实现了 `ThemeFactory` 接口,提供了按名称创建、从 JSON 创建和导出 JSON 的能力。这个类存在的意义是统一管理主题的创建方式,让调用方不需要知道 `MaterialTheme` 的构造细节。 - -## 按名称创建 - -最简单的使用方式是用预定义名称创建主题: - -```cpp -#include "material_factory_class.h" - -MaterialFactory factory; - -// 创建浅色主题 -auto lightTheme = factory.fromName("theme.material.light"); - -// 创建深色主题 -auto darkTheme = factory.fromName("theme.material.dark"); - -// 检查是否创建成功 -if (!lightTheme) { - qDebug() << "主题创建失败,可能是名称错误"; -} -``` - -如果传入无法识别的名称,`fromName` 会返回 `nullptr`。支持的名称列表: -- `theme.material.light`:Material 默认浅色主题 -- `theme.material.dark`:Material 默认深色主题 - -## 从 JSON 创建 - -支持从 Material Theme Builder 导出的 JSON 创建主题: - -```cpp -QByteArray json = R"({ - "colors": { - "primary": "#6750A4", - "onPrimary": "#FFFFFF", - "primaryContainer": "#EADDFF", - ... - }, - "typography": { - ... - }, - "shapes": { - ... - } -})"; - -auto theme = factory.fromJson(json); - -if (!theme) { - qDebug() << "JSON 解析失败"; -} -``` - -JSON 格式可以只包含颜色,也可以包含完整的主题配置。如果某个部分缺失,工厂会使用默认值填充。 - -⚠️ JSON 格式必须符合 Material Theme Builder 的导出规范。如果格式不正确,`fromJson` 会返回 `nullptr`。目前没有详细的错误信息返回,这是个待改进点。 - -## 导出到 JSON - -可以把已有主题导出为 JSON 格式: - -```cpp -auto theme = factory.fromName("theme.material.light"); -QByteArray json = factory.toJson(theme.get()); - -// 保存到文件 -QFile file("my_theme.json"); -file.open(QIODevice::WriteOnly); -file.write(json); -``` - -导出的 JSON 兼容 Material Theme Builder 的导入格式,可以在在线工具中编辑后再导回。 - -## 作为 ThemeFactory 使用 - -`MaterialFactory` 继承自 `ThemeFactory`,可以配合我们的主题管理系统使用: - -```cpp -std::unique_ptr factory = std::make_unique(); -setThemeFactory(std::move(factory)); - -// 后续可以通过通用接口创建主题 -auto theme = themeFactory()->fromName("theme.material.light"); -``` - -这样设计的好处是可以在运行时切换不同的主题系统(比如未来添加 Fluent Design 支持),而不需要修改业务代码。 - -## 错误处理 - -`MaterialFactory` 的错误处理比较简单——失败时返回空指针。这在某些场景下不够友好,因为调用方无法知道失败的具体原因: - -```cpp -// 当前的方式 -auto theme = factory.fromJson(json); -if (!theme) { - // 不知道是 JSON 格式错误、颜色值错误、还是其他问题 -} - -// 更理想的方式(待实现) -auto result = factory.fromJsonWithError(json); -if (!result) { - const auto& err = result.error(); - // 可以根据 err.kind 判断具体错误类型 -} -``` - -这是我们在后续版本中计划改进的地方。 - -## 线程安全 - -`MaterialFactory` 本身是无状态的,创建主题的过程不依赖共享状态。因此可以在多线程环境中安全使用: - -```cpp -// 多个线程可以同时调用 -MaterialFactory factory; - -// 线程 1 -auto t1 = factory.fromName("theme.material.light"); - -// 线程 2 -auto t2 = factory.fromName("theme.material.dark"); -``` - -创建出来的 `MaterialTheme` 对象也是独立不共享的,可以在线程间传递。 - -## 相关文档 - -- [MaterialTheme - 主题实现](./cfmaterial_theme.md) -- [MaterialColorScheme - 颜色方案](./cfmaterial_scheme.md) -- [material_factory.hpp - 工厂函数](./material_factory_hpp.md) +--- +title: "MaterialFactory - Material 主题工厂" +description: 是创建 Material Design 3 主题的入口。它实现了 接口,提供了按名称创建、从 JS +--- + +# MaterialFactory - Material 主题工厂 + +`MaterialFactory` 是创建 Material Design 3 主题的入口。它实现了 `ThemeFactory` 接口,提供了按名称创建、从 JSON 创建和导出 JSON 的能力。这个类存在的意义是统一管理主题的创建方式,让调用方不需要知道 `MaterialTheme` 的构造细节。 + +## 按名称创建 + +最简单的使用方式是用预定义名称创建主题: + +```cpp +#include "material_factory_class.h" + +MaterialFactory factory; + +// 创建浅色主题 +auto lightTheme = factory.fromName("theme.material.light"); + +// 创建深色主题 +auto darkTheme = factory.fromName("theme.material.dark"); + +// 检查是否创建成功 +if (!lightTheme) { + qDebug() << "主题创建失败,可能是名称错误"; +} +```text + +如果传入无法识别的名称,`fromName` 会返回 `nullptr`。支持的名称列表: +- `theme.material.light`:Material 默认浅色主题 +- `theme.material.dark`:Material 默认深色主题 + +## 从 JSON 创建 + +支持从 Material Theme Builder 导出的 JSON 创建主题: + +```cpp +QByteArray json = R"({ + "colors": { + "primary": "#6750A4", + "onPrimary": "#FFFFFF", + "primaryContainer": "#EADDFF", + ... + }, + "typography": { + ... + }, + "shapes": { + ... + } +})"; + +auto theme = factory.fromJson(json); + +if (!theme) { + qDebug() << "JSON 解析失败"; +} +```text + +JSON 格式可以只包含颜色,也可以包含完整的主题配置。如果某个部分缺失,工厂会使用默认值填充。 + +⚠️ JSON 格式必须符合 Material Theme Builder 的导出规范。如果格式不正确,`fromJson` 会返回 `nullptr`。目前没有详细的错误信息返回,这是个待改进点。 + +## 导出到 JSON + +可以把已有主题导出为 JSON 格式: + +```cpp +auto theme = factory.fromName("theme.material.light"); +QByteArray json = factory.toJson(theme.get()); + +// 保存到文件 +QFile file("my_theme.json"); +file.open(QIODevice::WriteOnly); +file.write(json); +```text + +导出的 JSON 兼容 Material Theme Builder 的导入格式,可以在在线工具中编辑后再导回。 + +## 作为 ThemeFactory 使用 + +`MaterialFactory` 继承自 `ThemeFactory`,可以配合我们的主题管理系统使用: + +```cpp +std::unique_ptr factory = std::make_unique(); +setThemeFactory(std::move(factory)); + +// 后续可以通过通用接口创建主题 +auto theme = themeFactory()->fromName("theme.material.light"); +```text + +这样设计的好处是可以在运行时切换不同的主题系统(比如未来添加 Fluent Design 支持),而不需要修改业务代码。 + +## 错误处理 + +`MaterialFactory` 的错误处理比较简单——失败时返回空指针。这在某些场景下不够友好,因为调用方无法知道失败的具体原因: + +```cpp +// 当前的方式 +auto theme = factory.fromJson(json); +if (!theme) { + // 不知道是 JSON 格式错误、颜色值错误、还是其他问题 +} + +// 更理想的方式(待实现) +auto result = factory.fromJsonWithError(json); +if (!result) { + const auto& err = result.error(); + // 可以根据 err.kind 判断具体错误类型 +} +```text + +这是我们在后续版本中计划改进的地方。 + +## 线程安全 + +`MaterialFactory` 本身是无状态的,创建主题的过程不依赖共享状态。因此可以在多线程环境中安全使用: + +```cpp +// 多个线程可以同时调用 +MaterialFactory factory; + +// 线程 1 +auto t1 = factory.fromName("theme.material.light"); + +// 线程 2 +auto t2 = factory.fromName("theme.material.dark"); +```text + +创建出来的 `MaterialTheme` 对象也是独立不共享的,可以在线程间传递。 + +## 相关文档 + +- [MaterialTheme - 主题实现](./cfmaterial_theme.md) +- [MaterialColorScheme - 颜色方案](./cfmaterial_scheme.md) +- [material_factory.hpp - 工厂函数](./material_factory_hpp.md) diff --git a/document/HandBook/ui/material/material_factory_hpp.md b/document/HandBook/ui/material/material_factory_hpp.md index 7396a1770..c5507f6bb 100644 --- a/document/HandBook/ui/material/material_factory_hpp.md +++ b/document/HandBook/ui/material/material_factory_hpp.md @@ -1,187 +1,192 @@ -# material_factory.hpp - Material 工厂函数 - -`material_factory.hpp` 提供了一套自由函数风格的工厂 API,用于创建 Material Design 3 的各个组件。相比 `MaterialFactory` 类,这套 API 更轻量,不需要实例化工厂对象——直接调用函数就行。 - -## 颜色方案工厂 - -创建颜色方案是最常用的功能: - -```cpp -#include "material_factory.hpp" - -// 默认浅色方案 -auto light = cf::ui::core::material::light(); - -// 默认深色方案 -auto dark = cf::ui::core::material::dark(); -``` - -这两个函数返回 `MaterialColorScheme` 对象(不是指针),可以直接使用或移动。 - -## 从种子颜色生成 - -Material You 的核心功能——动态颜色生成: - -```cpp -#include "base/color.h" - -// 从任意颜色生成 -CFColor seed("#6750A4"); -auto scheme = cf::ui::core::material::fromKeyColor(seed); - -// 生成深色版本 -auto darkScheme = cf::ui::core::material::fromKeyColor(seed, true); -``` - -种子颜色可以是用户选择的品牌色、墙纸的主色等等。生成的配色会自动计算 26 个颜色角色的值,确保视觉协调。 - -## 从 JSON 创建(带错误处理) - -`fromJson` 返回 `cf::expected` 类型,支持详细的错误处理: - -```cpp -QByteArray json = loadFromFile(); -auto result = cf::ui::core::material::fromJson(json); - -if (result) { - auto scheme = std::move(*result); - // 使用 scheme -} else { - const auto& err = result.error(); - switch (err.kind) { - case MaterialSchemeError::Kind::InvalidJson: - qDebug() << "JSON 格式错误:" << err.message.c_str(); - break; - case MaterialSchemeError::Kind::MissingColor: - qDebug() << "缺少必需颜色:" << err.message.c_str(); - break; - case MaterialSchemeError::Kind::InvalidColorFormat: - qDebug() << "颜色格式错误:" << err.message.c_str(); - break; - case MaterialSchemeError::Kind::GenerationFailed: - qDebug() << "颜色生成失败:" << err.message.c_str(); - break; - } -} -``` - -这个错误处理比 `MaterialFactory::fromJson` 的空指针友好得多。 - -## 导出到 JSON - -可以把配色导出为 JSON: - -```cpp -auto scheme = material::light(); -QByteArray json = material::toJson(scheme); - -// 保存到文件 -QFile file("theme.json"); -file.open(QIODevice::WriteOnly); -file.write(json); -``` - -导出的格式兼容 Material Theme Builder。 - -## 字体工厂 - -创建默认排版系统: - -```cpp -auto typography = cf::ui::core::material::defaultTypography(); - -QFont titleFont = typography.queryTargetFont("md.typography.titleLarge"); -QFont bodyFont = typography.queryTargetFont("md.typography.bodyMedium"); -``` - -默认字体会根据平台自动选择——Windows 用 Segoe UI,macOS 用 .SF NS Text,Linux 用 Ubuntu。 - -## 圆角工厂 - -创建默认圆角系统: - -```cpp -auto radius = cf::ui::core::material::defaultRadiusScale(); - -float small = radius.queryRadiusScale("md.shape.cornerSmall"); // 8.0f -float medium = radius.queryRadiusScale("md.shape.cornerMedium"); // 12.0f -float large = radius.queryRadiusScale("md.shape.cornerLarge"); // 16.0f -``` - -## 动画工厂 - -创建默认运动系统: - -```cpp -auto motion = cf::ui::core::material::motion(); - -int duration = motion.queryDuration("shortEnter"); // 200 -auto spec = motion.getMotionSpec("mediumExit"); -``` - -## 完整工作流 - -一个典型的使用流程是组合各个组件创建完整主题: - -```cpp -#include "material_factory.hpp" - -// 创建各个组件 -auto colors = material::fromKeyColor("#6750A4"); -auto typography = material::defaultTypography(); -auto radius = material::defaultRadiusScale(); -auto motion = material::motion(); - -// 使用组件... -QColor primary = colors.queryExpectedColor("md.primary"); -QFont titleFont = typography.queryTargetFont("md.typography.titleLarge"); -float cardRadius = radius.queryRadiusScale("md.shape.cornerMedium"); -auto enterSpec = motion.getMotionSpec("mediumEnter"); -``` - -如果需要完整的 `MaterialTheme` 对象,还是得用 `MaterialFactory` 类。 - -## 自定义主题示例 - -可以从种子颜色生成主题并应用: - -```cpp -void applyCustomTheme(const QColor& brandColor) { - using namespace cf::ui::core::material; - - // 从品牌色生成配色 - CFColor keyColor(brandColor); - auto scheme = fromKeyColor(keyColor, isDarkMode()); - - // 获取其他默认组件 - auto typography = defaultTypography(); - auto radius = defaultRadiusScale(); - auto motion = motion(); - - // 应用到应用... - updateColorScheme(&scheme); - updateTypography(&typography); - // ... -} -``` - -这种方式让应用可以轻松实现"品牌色换肤"功能。 - -## 与 MaterialFactory 的选择 - -两套 API 的区别: - -| 特性 | material_factory.hpp | MaterialFactory 类 | -|------|---------------------|-------------------| -| 使用方式 | 自由函数 | 类实例 | -| 错误处理 | cf::expected | 空指针 | -| 主题创建 | 不支持 | 支持 | -| 适用场景 | 独立组件创建 | 完整主题管理 | - -如果只需要创建颜色/字体/圆角等单个组件,用自由函数 API 更简洁。如果需要创建完整 `MaterialTheme` 对象,得用 `MaterialFactory` 类。 - -## 相关文档 - -- [MaterialFactory - 主题工厂类](./material_factory_class.md) -- [MaterialColorScheme - 颜色方案](./cfmaterial_scheme.md) -- [MaterialTheme - 主题实现](./cfmaterial_theme.md) +--- +title: "materialfactory.hpp - Material 工厂函数" +description: 提供了一套自由函数风格的工厂 API,用于创建 Material Design 3 的各个组件。相比 +--- + +# material_factory.hpp - Material 工厂函数 + +`material_factory.hpp` 提供了一套自由函数风格的工厂 API,用于创建 Material Design 3 的各个组件。相比 `MaterialFactory` 类,这套 API 更轻量,不需要实例化工厂对象——直接调用函数就行。 + +## 颜色方案工厂 + +创建颜色方案是最常用的功能: + +```cpp +#include "material_factory.hpp" + +// 默认浅色方案 +auto light = cf::ui::core::material::light(); + +// 默认深色方案 +auto dark = cf::ui::core::material::dark(); +```text + +这两个函数返回 `MaterialColorScheme` 对象(不是指针),可以直接使用或移动。 + +## 从种子颜色生成 + +Material You 的核心功能——动态颜色生成: + +```cpp +#include "base/color.h" + +// 从任意颜色生成 +CFColor seed("#6750A4"); +auto scheme = cf::ui::core::material::fromKeyColor(seed); + +// 生成深色版本 +auto darkScheme = cf::ui::core::material::fromKeyColor(seed, true); +```text + +种子颜色可以是用户选择的品牌色、墙纸的主色等等。生成的配色会自动计算 26 个颜色角色的值,确保视觉协调。 + +## 从 JSON 创建(带错误处理) + +`fromJson` 返回 `cf::expected` 类型,支持详细的错误处理: + +```cpp +QByteArray json = loadFromFile(); +auto result = cf::ui::core::material::fromJson(json); + +if (result) { + auto scheme = std::move(*result); + // 使用 scheme +} else { + const auto& err = result.error(); + switch (err.kind) { + case MaterialSchemeError::Kind::InvalidJson: + qDebug() << "JSON 格式错误:" << err.message.c_str(); + break; + case MaterialSchemeError::Kind::MissingColor: + qDebug() << "缺少必需颜色:" << err.message.c_str(); + break; + case MaterialSchemeError::Kind::InvalidColorFormat: + qDebug() << "颜色格式错误:" << err.message.c_str(); + break; + case MaterialSchemeError::Kind::GenerationFailed: + qDebug() << "颜色生成失败:" << err.message.c_str(); + break; + } +} +```text + +这个错误处理比 `MaterialFactory::fromJson` 的空指针友好得多。 + +## 导出到 JSON + +可以把配色导出为 JSON: + +```cpp +auto scheme = material::light(); +QByteArray json = material::toJson(scheme); + +// 保存到文件 +QFile file("theme.json"); +file.open(QIODevice::WriteOnly); +file.write(json); +```text + +导出的格式兼容 Material Theme Builder。 + +## 字体工厂 + +创建默认排版系统: + +```cpp +auto typography = cf::ui::core::material::defaultTypography(); + +QFont titleFont = typography.queryTargetFont("md.typography.titleLarge"); +QFont bodyFont = typography.queryTargetFont("md.typography.bodyMedium"); +```text + +默认字体会根据平台自动选择——Windows 用 Segoe UI,macOS 用 .SF NS Text,Linux 用 Ubuntu。 + +## 圆角工厂 + +创建默认圆角系统: + +```cpp +auto radius = cf::ui::core::material::defaultRadiusScale(); + +float small = radius.queryRadiusScale("md.shape.cornerSmall"); // 8.0f +float medium = radius.queryRadiusScale("md.shape.cornerMedium"); // 12.0f +float large = radius.queryRadiusScale("md.shape.cornerLarge"); // 16.0f +```text + +## 动画工厂 + +创建默认运动系统: + +```cpp +auto motion = cf::ui::core::material::motion(); + +int duration = motion.queryDuration("shortEnter"); // 200 +auto spec = motion.getMotionSpec("mediumExit"); +```text + +## 完整工作流 + +一个典型的使用流程是组合各个组件创建完整主题: + +```cpp +#include "material_factory.hpp" + +// 创建各个组件 +auto colors = material::fromKeyColor("#6750A4"); +auto typography = material::defaultTypography(); +auto radius = material::defaultRadiusScale(); +auto motion = material::motion(); + +// 使用组件... +QColor primary = colors.queryExpectedColor("md.primary"); +QFont titleFont = typography.queryTargetFont("md.typography.titleLarge"); +float cardRadius = radius.queryRadiusScale("md.shape.cornerMedium"); +auto enterSpec = motion.getMotionSpec("mediumEnter"); +```text + +如果需要完整的 `MaterialTheme` 对象,还是得用 `MaterialFactory` 类。 + +## 自定义主题示例 + +可以从种子颜色生成主题并应用: + +```cpp +void applyCustomTheme(const QColor& brandColor) { + using namespace cf::ui::core::material; + + // 从品牌色生成配色 + CFColor keyColor(brandColor); + auto scheme = fromKeyColor(keyColor, isDarkMode()); + + // 获取其他默认组件 + auto typography = defaultTypography(); + auto radius = defaultRadiusScale(); + auto motion = motion(); + + // 应用到应用... + updateColorScheme(&scheme); + updateTypography(&typography); + // ... +} +```bash + +这种方式让应用可以轻松实现"品牌色换肤"功能。 + +## 与 MaterialFactory 的选择 + +两套 API 的区别: + +| 特性 | material_factory.hpp | MaterialFactory 类 | +|------|---------------------|-------------------| +| 使用方式 | 自由函数 | 类实例 | +| 错误处理 | cf::expected | 空指针 | +| 主题创建 | 不支持 | 支持 | +| 适用场景 | 独立组件创建 | 完整主题管理 | + +如果只需要创建颜色/字体/圆角等单个组件,用自由函数 API 更简洁。如果需要创建完整 `MaterialTheme` 对象,得用 `MaterialFactory` 类。 + +## 相关文档 + +- [MaterialFactory - 主题工厂类](./material_factory_class.md) +- [MaterialColorScheme - 颜色方案](./cfmaterial_scheme.md) +- [MaterialTheme - 主题实现](./cfmaterial_theme.md) diff --git a/document/HandBook/ui/material/widget/.pages b/document/HandBook/ui/material/widget/.pages deleted file mode 100644 index 124d2d1ac..000000000 --- a/document/HandBook/ui/material/widget/.pages +++ /dev/null @@ -1,22 +0,0 @@ -title: Widget -nav: - - Button: button.md - - Checkbox: checkbox.md - - ComboBox: combobox.md - - DoubleSpinBox: doublespinbox.md - - GroupBox: groupbox.md - - Label: label.md - - ListView: listview.md - - ProgressBar: progressbar.md - - RadioButton: radiobutton.md - - ScrollView: scrollview.md - - Separator: separator.md - - Slider: slider.md - - SpinBox: spinbox.md - - 状态机: state_machine.md - - Switch: switch.md - - TableView: tableview.md - - TabView: tabview.md - - TextArea: textarea.md - - TextField: textfield.md - - TreeView: treeview.md diff --git a/document/HandBook/ui/material/widget/button.md b/document/HandBook/ui/material/widget/button.md index c26760aa1..0da48eb9e 100644 --- a/document/HandBook/ui/material/widget/button.md +++ b/document/HandBook/ui/material/widget/button.md @@ -1,200 +1,205 @@ -# Button - Material 按钮 - -`Button` 是 Material Design 3 按钮控件的完整实现,支持五种视觉变体、水波纹效果、状态层动画、阴影和焦点指示器。我们选择自己实现而不是继承 QPushButton 的样式,是因为 Material 的交互规范(特别是状态层动画和水波纹)无法通过 Qt 样式表很好地表达。 - -## 按钮变体 - -Material Design 3 定义了五种按钮变体,每种有不同的视觉层级: - -```cpp -enum class ButtonVariant { - Filled, // 填充按钮 - 最高的视觉强调 - Tonal, // 调和按钮 - 中等强调 - Outlined, // 描边按钮 - 较低强调 - Text, // 文本按钮 - 最低强调 - Elevated, // 浮起按钮 - 带阴影 -}; -``` - -选择哪种变体取决于按钮在界面中的层级关系。主操作用 Filled,次要操作用 Tonal 或 Outlined,低优先级操作用 Text 或 Elevated。 - -## 基本用法 - -创建按钮最简单的方式是指定文本和变体: - -```cpp -#include "widget/material/widget/button/button.h" - -using namespace cf::ui::widget::material; - -// 默认变体(Filled) -auto* button1 = new Button("Click me", this); - -// 指定变体 -auto* button2 = new Button("Secondary", ButtonVariant::Outlined, this); -auto* button3 = new Button("Low priority", ButtonVariant::Text, this); - -// 连接信号(与 QPushButton 兼容) -connect(button1, &Button::clicked, this, &MyClass::onButtonClick); -``` - -## 图标按钮 - -Material 按钮支持在文本前添加前导图标: - -```cpp -QIcon icon = QIcon::fromTheme("favorite"); -Button* button = new Button("Like", this); -button->setLeadingIcon(icon); - -// 或者使用 setIcon(别名) -button->setIcon(icon); -``` - -图标尺寸固定为 18dp,与文本间距 8dp,这是 Material 规范要求的。 - -## 交互状态 - -按钮遵循 Material Design 3 的交互状态规范,每种状态有不同的视觉反馈: - -| 状态 | 视觉效果 | 透明度 | -|------|----------|--------| -| Normal | 默认外观 | 0% | -| Hovered | 状态层叠加 | 8% | -| Pressed | 状态层叠加 + 轻微下沉 | 12% | -| Focused | 焦点环 + 状态层 | 12% | -| Disabled | 38% 透明度 | 0% | - -这些状态由内部的 `StateMachine` 管理,只需要在事件处理中正确转发即可。按钮已经处理好了所有事件,直接使用就行。 - -## 尺寸规范 - -按钮遵循 Material 的尺寸规范,内容区域高度固定为 40dp,水平内边距 24dp: - -```cpp -// 尺寸计算(已在 sizeHint 中实现) -CanvasUnitHelper helper(qApp->devicePixelRatio()); -float contentHeight = helper.dpToPx(40.0f); // 固定高度 -float hPadding = helper.dpToPx(24.0f); // 水平内边距 -float iconWidth = helper.dpToPx(18.0f); // 图标宽度 -float iconGap = helper.dpToPx(8.0f); // 图标与文本间距 -``` - -⚠️ 按钮的最小宽度不是固定的,而是由内容决定的。如果需要确保触摸目标大小(至少 48x48dp),需要在布局时留出足够的间距。 - -## 绘制流程 - -按钮的 `paintEvent` 实现了 Material 规范的七步绘制流程: - -```cpp -void Button::paintEvent(QPaintEvent* event) { - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - // Step 1: 绘制阴影(仅 Elevated 变体) - drawShadow(p, contentRect, shape); - - // Step 2: 绘制背景 - drawBackground(p, shape); - - // Step 3: 绘制状态层 - drawStateLayer(p, shape); - - // Step 4: 绘制水波纹 - drawRipple(p, shape); - - // Step 5: 绘制描边(仅 Outlined 变体) - drawOutline(p, shape); - - // Step 6: 绘制内容(图标 + 文本) - drawContent(p, contentRect); - - // Step 7: 绘制焦点指示器 - drawFocusIndicator(p, shape); -} -``` - -这个顺序很重要——状态层在背景之上、内容之下,水波纹在状态层之上,焦点环在最外层。改变顺序会导致视觉效果不符合 Material 规范。 - -## 阴影处理 - -Elevated 变体和按压效果需要阴影支持,由 `MdElevationController` 处理: - -```cpp -// 设置海拔级别(0-5) -button->setElevation(2); - -// 设置光源角度(默认 15 度,来自左上方) -button->setLightSourceAngle(15.0f); -``` - -海拔级别影响阴影的模糊半径和偏移量。按钮默认使用 level 2,按压时会临时增加,产生"下沉"的视觉效果。 - -## 按压效果 - -按压效果包括两部分:状态层透明度变化和海拔变化。可以通过属性禁用: - -```cpp -// 禁用按压效果(仅状态层动画保留) -button->setPressEffectEnabled(false); -``` - -禁用后,按钮的视觉反馈会减弱,但仍然有水波纹和状态层。这在某些自定义场景下有用。 - -## 颜色获取 - -按钮的颜色从当前主题中获取,每种变体使用不同的颜色角色: - -```cpp -// Filled: container = PRIMARY, label = ON_PRIMARY -// Tonal: container = SECONDARY_CONTAINER, label = ON_SECONDARY_CONTAINER -// Outlined/Text/Elevated: container = SURFACE, label = PRIMARY -``` - -如果主题不可用,会使用硬编码的 fallback 颜色。这在开发阶段很有用,但生产环境应该总是配置正确的主题。 - -## 圆角处理 - -按钮使用完全圆角(圆角半径等于高度的一半): - -```cpp -float cornerRadius = height() / 2.0f; -``` - -这在视觉上形成了胶囊形状,是 Material 3 的默认样式。如果需要方角按钮,需要子类化并重写 `cornerRadius()` 方法。 - -## 禁用状态 - -禁用状态下,按钮的背景和文本透明度降至 38%,这是 Material 规范要求的: - -```cpp -if (!isEnabled()) { - color.setAlphaF(0.38f); -} -``` - -禁用时状态层不显示,交互事件也不会触发状态变化。 - -## 布局建议 - -按钮在布局中的位置需要遵循 Material 的对齐规范: - -```cpp -// 对话框操作按钮通常右对齐 -auto* layout = new QHBoxLayout(dialog); -layout->addStretch(); -layout->addWidget(new Button("Cancel", ButtonVariant::Text, dialog)); -layout->addWidget(new Button("OK", ButtonVariant::Filled, dialog)); - -// 卡片操作按钮通常左对齐 -auto* cardLayout = new QHBoxLayout(card); -cardLayout->addWidget(new Button("Action", ButtonVariant::Outlined, card)); -cardLayout->addStretch(); -``` - -## 相关文档 - -- [StateMachine - Material 状态机](./state_machine.md) -- [RippleHelper - 水波纹效果](../base/ripple_helper.md) -- [MdElevationController - 阴影控制器](../base/elevation_controller.md) -- [Material Design 3 按钮规范](https://m3.material.io/components/buttons) +--- +title: "Button - Material 按钮" +description: 是 Material Design 3 按钮控件的完整实现,支持五种视觉变体、水波纹效果、状态层动画 +--- + +# Button - Material 按钮 + +`Button` 是 Material Design 3 按钮控件的完整实现,支持五种视觉变体、水波纹效果、状态层动画、阴影和焦点指示器。我们选择自己实现而不是继承 QPushButton 的样式,是因为 Material 的交互规范(特别是状态层动画和水波纹)无法通过 Qt 样式表很好地表达。 + +## 按钮变体 + +Material Design 3 定义了五种按钮变体,每种有不同的视觉层级: + +```cpp +enum class ButtonVariant { + Filled, // 填充按钮 - 最高的视觉强调 + Tonal, // 调和按钮 - 中等强调 + Outlined, // 描边按钮 - 较低强调 + Text, // 文本按钮 - 最低强调 + Elevated, // 浮起按钮 - 带阴影 +}; +```text + +选择哪种变体取决于按钮在界面中的层级关系。主操作用 Filled,次要操作用 Tonal 或 Outlined,低优先级操作用 Text 或 Elevated。 + +## 基本用法 + +创建按钮最简单的方式是指定文本和变体: + +```cpp +#include "widget/material/widget/button/button.h" + +using namespace cf::ui::widget::material; + +// 默认变体(Filled) +auto* button1 = new Button("Click me", this); + +// 指定变体 +auto* button2 = new Button("Secondary", ButtonVariant::Outlined, this); +auto* button3 = new Button("Low priority", ButtonVariant::Text, this); + +// 连接信号(与 QPushButton 兼容) +connect(button1, &Button::clicked, this, &MyClass::onButtonClick); +```text + +## 图标按钮 + +Material 按钮支持在文本前添加前导图标: + +```cpp +QIcon icon = QIcon::fromTheme("favorite"); +Button* button = new Button("Like", this); +button->setLeadingIcon(icon); + +// 或者使用 setIcon(别名) +button->setIcon(icon); +```bash + +图标尺寸固定为 18dp,与文本间距 8dp,这是 Material 规范要求的。 + +## 交互状态 + +按钮遵循 Material Design 3 的交互状态规范,每种状态有不同的视觉反馈: + +| 状态 | 视觉效果 | 透明度 | +|------|----------|--------| +| Normal | 默认外观 | 0% | +| Hovered | 状态层叠加 | 8% | +| Pressed | 状态层叠加 + 轻微下沉 | 12% | +| Focused | 焦点环 + 状态层 | 12% | +| Disabled | 38% 透明度 | 0% | + +这些状态由内部的 `StateMachine` 管理,只需要在事件处理中正确转发即可。按钮已经处理好了所有事件,直接使用就行。 + +## 尺寸规范 + +按钮遵循 Material 的尺寸规范,内容区域高度固定为 40dp,水平内边距 24dp: + +```cpp +// 尺寸计算(已在 sizeHint 中实现) +CanvasUnitHelper helper(qApp->devicePixelRatio()); +float contentHeight = helper.dpToPx(40.0f); // 固定高度 +float hPadding = helper.dpToPx(24.0f); // 水平内边距 +float iconWidth = helper.dpToPx(18.0f); // 图标宽度 +float iconGap = helper.dpToPx(8.0f); // 图标与文本间距 +```text + +⚠️ 按钮的最小宽度不是固定的,而是由内容决定的。如果需要确保触摸目标大小(至少 48x48dp),需要在布局时留出足够的间距。 + +## 绘制流程 + +按钮的 `paintEvent` 实现了 Material 规范的七步绘制流程: + +```cpp +void Button::paintEvent(QPaintEvent* event) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + // Step 1: 绘制阴影(仅 Elevated 变体) + drawShadow(p, contentRect, shape); + + // Step 2: 绘制背景 + drawBackground(p, shape); + + // Step 3: 绘制状态层 + drawStateLayer(p, shape); + + // Step 4: 绘制水波纹 + drawRipple(p, shape); + + // Step 5: 绘制描边(仅 Outlined 变体) + drawOutline(p, shape); + + // Step 6: 绘制内容(图标 + 文本) + drawContent(p, contentRect); + + // Step 7: 绘制焦点指示器 + drawFocusIndicator(p, shape); +} +```text + +这个顺序很重要——状态层在背景之上、内容之下,水波纹在状态层之上,焦点环在最外层。改变顺序会导致视觉效果不符合 Material 规范。 + +## 阴影处理 + +Elevated 变体和按压效果需要阴影支持,由 `MdElevationController` 处理: + +```cpp +// 设置海拔级别(0-5) +button->setElevation(2); + +// 设置光源角度(默认 15 度,来自左上方) +button->setLightSourceAngle(15.0f); +```text + +海拔级别影响阴影的模糊半径和偏移量。按钮默认使用 level 2,按压时会临时增加,产生"下沉"的视觉效果。 + +## 按压效果 + +按压效果包括两部分:状态层透明度变化和海拔变化。可以通过属性禁用: + +```cpp +// 禁用按压效果(仅状态层动画保留) +button->setPressEffectEnabled(false); +```text + +禁用后,按钮的视觉反馈会减弱,但仍然有水波纹和状态层。这在某些自定义场景下有用。 + +## 颜色获取 + +按钮的颜色从当前主题中获取,每种变体使用不同的颜色角色: + +```cpp +// Filled: container = PRIMARY, label = ON_PRIMARY +// Tonal: container = SECONDARY_CONTAINER, label = ON_SECONDARY_CONTAINER +// Outlined/Text/Elevated: container = SURFACE, label = PRIMARY +```text + +如果主题不可用,会使用硬编码的 fallback 颜色。这在开发阶段很有用,但生产环境应该总是配置正确的主题。 + +## 圆角处理 + +按钮使用完全圆角(圆角半径等于高度的一半): + +```cpp +float cornerRadius = height() / 2.0f; +```text + +这在视觉上形成了胶囊形状,是 Material 3 的默认样式。如果需要方角按钮,需要子类化并重写 `cornerRadius()` 方法。 + +## 禁用状态 + +禁用状态下,按钮的背景和文本透明度降至 38%,这是 Material 规范要求的: + +```cpp +if (!isEnabled()) { + color.setAlphaF(0.38f); +} +```text + +禁用时状态层不显示,交互事件也不会触发状态变化。 + +## 布局建议 + +按钮在布局中的位置需要遵循 Material 的对齐规范: + +```cpp +// 对话框操作按钮通常右对齐 +auto* layout = new QHBoxLayout(dialog); +layout->addStretch(); +layout->addWidget(new Button("Cancel", ButtonVariant::Text, dialog)); +layout->addWidget(new Button("OK", ButtonVariant::Filled, dialog)); + +// 卡片操作按钮通常左对齐 +auto* cardLayout = new QHBoxLayout(card); +cardLayout->addWidget(new Button("Action", ButtonVariant::Outlined, card)); +cardLayout->addStretch(); +```text + +## 相关文档 + +- [StateMachine - Material 状态机](./state_machine.md) +- [RippleHelper - 水波纹效果](../base/ripple_helper.md) +- [MdElevationController - 阴影控制器](../base/elevation_controller.md) +- [Material Design 3 按钮规范](https://m3.material.io/components/buttons) diff --git a/document/HandBook/ui/material/widget/checkbox.md b/document/HandBook/ui/material/widget/checkbox.md index 1fb02deba..a4d876890 100644 --- a/document/HandBook/ui/material/widget/checkbox.md +++ b/document/HandBook/ui/material/widget/checkbox.md @@ -1,3 +1,8 @@ +--- +title: "CheckBox - Material 复选框" +description: 是 Material Design 3 复选框控件的完整实现,支持选中、未选中和不确定(indete +--- + # CheckBox - Material 复选框 `CheckBox` 是 Material Design 3 复选框控件的完整实现,支持选中、未选中和不确定(indeterminate)三种状态。包含水波纹效果、状态层动画和焦点指示器,严格遵循 Material Design 3 规范。 @@ -11,7 +16,7 @@ class CheckBox : public QCheckBox { Q_OBJECT Q_PROPERTY(bool error READ hasError WRITE setError NOTIFY errorChanged) }; -``` +```text 头文件:`ui/widget/material/widget/checkbox/checkbox.h` @@ -36,7 +41,7 @@ cb2->setCheckState(Qt::PartiallyChecked); // 连接信号(与 QCheckBox 兼容) connect(cb2, &QCheckBox::stateChanged, this, &MyClass::onStateChanged); -``` +```text ## 错误状态 @@ -48,7 +53,7 @@ if (!agreedToTerms) { } else { checkBox->setError(false); } -``` +```bash 错误状态下,复选框边框使用 error 颜色,提供明确的视觉反馈。 diff --git a/document/HandBook/ui/material/widget/combobox.md b/document/HandBook/ui/material/widget/combobox.md index e7b014b6d..00803821e 100644 --- a/document/HandBook/ui/material/widget/combobox.md +++ b/document/HandBook/ui/material/widget/combobox.md @@ -1,3 +1,8 @@ +--- +title: "ComboBox - Material 下拉框" +description: 是 Material Design 3 下拉框控件的完整实现,支持填充(Filled)和描边(Out +--- + # ComboBox - Material 下拉框 `ComboBox` 是 Material Design 3 下拉框控件的完整实现,支持填充(Filled)和描边(Outlined)两种变体,带动画的下拉箭头和自定义列表样式。 @@ -10,7 +15,7 @@ namespace cf::ui::widget::material; class ComboBox : public QComboBox { Q_OBJECT }; -``` +```text 头文件:`ui/widget/material/widget/comboBox/combobox.h` @@ -21,7 +26,7 @@ enum class ComboBoxVariant { Filled, // 填充背景 + 边框 Outlined // 仅描边边框 }; -``` +```text ## 基本用法 @@ -45,7 +50,7 @@ outlined->setVariant(ComboBoxVariant::Outlined); // 连接信号(与 QComboBox 兼容) connect(combo, QOverload::of(&QComboBox::currentIndexChanged), this, &MyClass::onSelectionChanged); -``` +```text ## 下拉箭头动画 @@ -58,7 +63,7 @@ ComboBox 的下拉箭头有旋转动画: // 箭头旋转由 m_arrowRotation 控制 // showPopup() 触发箭头旋转到 180 度 // hidePopup() 触发箭头旋转回 0 度 -``` +```text ## 自定义弹出列表 @@ -74,7 +79,7 @@ ComboBox 的下拉箭头有旋转动画: // hidePopup() 内部: // 1. 箭头旋转回下方 // 2. 关闭弹出容器 -``` +```bash ## 交互状态 diff --git a/document/HandBook/ui/material/widget/doublespinbox.md b/document/HandBook/ui/material/widget/doublespinbox.md index a8976d55d..730163a8e 100644 --- a/document/HandBook/ui/material/widget/doublespinbox.md +++ b/document/HandBook/ui/material/widget/doublespinbox.md @@ -1,3 +1,8 @@ +--- +title: "DoubleSpinBox - Material 双精度数值调节框" +description: 是 Material Design 3 双精度数值调节框控件的完整实现,支持浮点数输入、增减按钮、描 +--- + # DoubleSpinBox - Material 双精度数值调节框 `DoubleSpinBox` 是 Material Design 3 双精度数值调节框控件的完整实现,支持浮点数输入、增减按钮、描边样式、焦点指示器和状态层效果。 @@ -10,7 +15,7 @@ namespace cf::ui::widget::material; class DoubleSpinBox : public QDoubleSpinBox { Q_OBJECT }; -``` +```text 头文件:`ui/widget/material/widget/doublespinbox/doublespinbox.h` @@ -35,7 +40,7 @@ spin->setSuffix(" mm"); // 连接信号(与 QDoubleSpinBox 兼容) connect(spin, QOverload::of(&QDoubleSpinBox::valueChanged), this, &MyClass::onValueChanged); -``` +```bash ## 与 SpinBox 的区别 @@ -56,7 +61,7 @@ connect(spin, QOverload::of(&QDoubleSpinBox::valueChanged), // 增加按钮(incrementButtonRect) // 减少按钮(decrementButtonRect) // 每个按钮有独立的 hover/pressed 状态追踪 -``` +```bash ## 交互状态 @@ -88,7 +93,7 @@ DoubleSpinBox 的 `paintEvent` 实现 7 步 Material Design 绘制流程: ```cpp // 将内部 lineEdit 限制在文本区域 // 避免输入框覆盖增减按钮区域 -``` +```bash ## 颜色系统 diff --git a/document/HandBook/ui/material/widget/groupbox.md b/document/HandBook/ui/material/widget/groupbox.md index 1e69d3cc1..879a51402 100644 --- a/document/HandBook/ui/material/widget/groupbox.md +++ b/document/HandBook/ui/material/widget/groupbox.md @@ -1,3 +1,8 @@ +--- +title: "GroupBox - Material 分组框" +description: 是 Material Design 3 分组框控件的完整实现,具有圆角、可选的海拔阴影和主题感知颜色 +--- + # GroupBox - Material 分组框 `GroupBox` 是 Material Design 3 分组框控件的完整实现,具有圆角、可选的海拔阴影和主题感知颜色。提供带有标题的容器,用于将相关控件分组显示。 @@ -13,7 +18,7 @@ class GroupBox : public QGroupBox { Q_PROPERTY(float cornerRadius READ cornerRadius WRITE setCornerRadius) Q_PROPERTY(bool hasBorder READ hasBorder WRITE setHasBorder) }; -``` +```text 头文件:`ui/widget/material/widget/groupbox/groupbox.h` @@ -34,7 +39,7 @@ layout->addWidget(new TextField(TextFieldVariant::Outlined, group)); // 创建不带标题的分组框 auto* group2 = new GroupBox(this); -``` +```text ## 海拔与阴影 @@ -46,7 +51,7 @@ group->setElevation(2); // 海拔级别越高,阴影越明显 group->setElevation(4); -``` +```text 海拔级别影响阴影的模糊半径和偏移量,遵循 Material Design 的标准级别定义。 @@ -61,7 +66,7 @@ group->setCornerRadius(12.0f); // 重置为默认值 group->setCornerRadius(0); -``` +```text ## 边框控制 @@ -69,7 +74,7 @@ group->setCornerRadius(0); // 启用/禁用边框(默认启用) group->setHasBorder(true); // 显示边框 group->setHasBorder(false); // 仅显示阴影(如果 elevation > 0) -``` +```bash 禁用边框时,分组框仅依靠阴影来区分层级,适合卡片式布局。 @@ -117,7 +122,7 @@ addressLayout->addWidget(new TextField("Street", TextFieldVariant::Outlined)); addressLayout->addWidget(new TextField("City", TextFieldVariant::Outlined)); mainLayout->addWidget(addressGroup); -``` +```text ## 相关文档 diff --git a/document/HandBook/ui/material/widget/index.md b/document/HandBook/ui/material/widget/index.md index 3b7d31d22..a39b478ba 100644 --- a/document/HandBook/ui/material/widget/index.md +++ b/document/HandBook/ui/material/widget/index.md @@ -1,10 +1,11 @@ -# Widget - -> Welcome to the Widget section. +--- +title: Material 组件 +description: 本章节包含 Material Design 3 组件的详细文档,涵盖 FilledButton、Ou +--- -## Overview +# Material 组件 -Documentation and resources for Widget. +本章节包含 Material Design 3 组件的详细文档,涵盖 FilledButton、OutlinedButton、TextField、Slider、Switch、Checkbox、Fab 等 19 个标准组件的实现细节、属性配置与使用指南。所有组件严格遵循 Material Design 3 规范。 --- diff --git a/document/HandBook/ui/material/widget/label.md b/document/HandBook/ui/material/widget/label.md index 3b2b92c0c..8e9fb5e42 100644 --- a/document/HandBook/ui/material/widget/label.md +++ b/document/HandBook/ui/material/widget/label.md @@ -1,3 +1,8 @@ +--- +title: "Label - Material 标签" +description: 是 Material Design 3 标签控件的完整实现,支持 15 种排版样式(Display、 +--- + # Label - Material 标签 `Label` 是 Material Design 3 标签控件的完整实现,支持 15 种排版样式(Display、Headline、Title、Body、Label)和 9 种颜色变体。完全集成主题系统,自动响应主题变化。 @@ -13,7 +18,7 @@ class Label : public QLabel { Q_PROPERTY(LabelColorVariant colorVariant READ colorVariant WRITE setColorVariant) Q_PROPERTY(bool autoHiding READ autoHiding WRITE setAutoHiding) }; -``` +```text 头文件:`ui/widget/material/widget/label/label.h` @@ -38,7 +43,7 @@ enum class TypographyStyle { // 标签样式(14sp, 12sp, 11sp)- 用于辅助信息 LabelLarge, LabelMedium, LabelSmall }; -``` +```text ## 颜色变体 @@ -57,7 +62,7 @@ enum class LabelColorVariant { InverseSurface, // 反转表面颜色 InverseOnSurface // 反转表面上的文本 }; -``` +```text ## 基本用法 @@ -77,7 +82,7 @@ title->setColorVariant(LabelColorVariant::Primary); // 创建展示文本 auto* hero = new Label("Welcome", TypographyStyle::DisplayLarge, this); -``` +```bash ## 排版样式选择指南 @@ -100,7 +105,7 @@ label->setAutoHiding(true); // 当 text 为空时,label->hide() 自动调用 // 当 text 非空时,label->show() 自动调用 -``` +```text 这在动态内容的场景下很有用,比如错误提示标签在没有错误时不占用布局空间。 @@ -114,7 +119,7 @@ Label 从当前主题中自动获取颜色和字体: // 字体根据 typographyStyle 从主题获取对应的排版令牌 // 查询通过 typographyTokenName() 转换样式为令牌名称 -``` +```bash 为了优化性能,Label 缓存了最近查询的颜色值(`cachedColor_`),避免重复的主题查询。 diff --git a/document/HandBook/ui/material/widget/listview.md b/document/HandBook/ui/material/widget/listview.md index 319a1312d..e9cca9800 100644 --- a/document/HandBook/ui/material/widget/listview.md +++ b/document/HandBook/ui/material/widget/listview.md @@ -1,3 +1,8 @@ +--- +title: "ListView - Material 列表视图" +description: 是 Material Design 3 列表视图控件的完整实现,支持单行、双行和三行列表项。包含水波 +--- + # ListView - Material 列表视图 `ListView` 是 Material Design 3 列表视图控件的完整实现,支持单行、双行和三行列表项。包含水波纹效果、状态层、分隔线和焦点指示器。 @@ -13,7 +18,7 @@ class ListView : public QListView { Q_PROPERTY(bool showSeparator READ showSeparator WRITE setShowSeparator) Q_PROPERTY(bool rippleEnabled READ rippleEnabled WRITE setRippleEnabled) }; -``` +```text 头文件:`ui/widget/material/widget/listview/listview.h` @@ -27,7 +32,7 @@ enum class ItemHeight { TwoLine = 72, // 72dp - 双行项目 ThreeLine = 88 // 88dp - 三行项目 }; -``` +```text ## 基本用法 @@ -54,7 +59,7 @@ list->setModel(model); // 连接信号 connect(list, &QListView::clicked, this, &MyClass::onItemClicked); -``` +```text ## 分隔线 @@ -64,7 +69,7 @@ list->setShowSeparator(true); // 禁用分隔线 list->setShowSeparator(false); -``` +```text 分隔线使用 `OutlineVariant` 颜色绘制,遵循 Material Design 的视觉规范。 @@ -73,7 +78,7 @@ list->setShowSeparator(false); ```cpp // 启用/禁用水波纹效果(默认启用) list->setRippleEnabled(true); -``` +```bash 水波纹在列表项被点击时从点击位置向外扩散,使用 `RippleHelper` 实现。 @@ -98,7 +103,7 @@ ListView 使用内部委托(`ListViewDelegate`)控制列表项的大小: ```cpp // 委托在 .cpp 中定义,自动设置 uniformItemSizes // 根据 ItemHeight 设置每项的高度 -``` +```bash ## 绘制流程 diff --git a/document/HandBook/ui/material/widget/progressbar.md b/document/HandBook/ui/material/widget/progressbar.md index 1b75f8238..deacdb1a3 100644 --- a/document/HandBook/ui/material/widget/progressbar.md +++ b/document/HandBook/ui/material/widget/progressbar.md @@ -1,3 +1,8 @@ +--- +title: "ProgressBar - Material 进度条" +description: 是 Material Design 3 进度条控件的完整实现,支持确定(Determinate)和不 +--- + # ProgressBar - Material 进度条 `ProgressBar` 是 Material Design 3 进度条控件的完整实现,支持确定(Determinate)和不确定(Indeterminate)两种模式。包含平滑动画、状态层和焦点指示器。 @@ -10,7 +15,7 @@ namespace cf::ui::widget::material; class ProgressBar : public QProgressBar { Q_OBJECT }; -``` +```text 头文件:`ui/widget/material/widget/progressbar/progressbar.h` @@ -32,7 +37,7 @@ loading->setRange(0, 0); // min=max 表示不确定模式 // 连接信号(与 QProgressBar 兼容) connect(progress, &QProgressBar::valueChanged, this, &MyClass::onProgressChanged); -``` +```bash ## 确定模式 vs 不确定模式 @@ -48,7 +53,7 @@ connect(progress, &QProgressBar::valueChanged, this, &MyClass::onProgressChanged ```cpp progress->setRange(0, 100); progress->setValue(75); // 填充 75% 的宽度 -``` +```text ### 不确定模式 @@ -56,7 +61,7 @@ progress->setValue(75); // 填充 75% 的宽度 ```cpp progress->setRange(0, 0); // 进入不确定模式 -``` +```bash 动画通过 `m_indeterminatePosition`(0.0 到 1.0)控制位置,以循环方式运行。 @@ -106,7 +111,7 @@ ProgressBar 的 `paintEvent` 实现以下绘制步骤: // m_indeterminatePosition 从 0.0 循环到 1.0 // 动画速度由 CFMaterialAnimationFactory 控制 // startIndeterminateAnimation() / stopIndeterminateAnimation() 管理生命周期 -``` +```bash ## 主要方法 diff --git a/document/HandBook/ui/material/widget/radiobutton.md b/document/HandBook/ui/material/widget/radiobutton.md index 038d8ef40..5b8d87122 100644 --- a/document/HandBook/ui/material/widget/radiobutton.md +++ b/document/HandBook/ui/material/widget/radiobutton.md @@ -1,3 +1,8 @@ +--- +title: "RadioButton - Material 单选按钮" +description: 是 Material Design 3 单选按钮控件的完整实现,具有圆形选择区域、内部圆缩放动画、水 +--- + # RadioButton - Material 单选按钮 `RadioButton` 是 Material Design 3 单选按钮控件的完整实现,具有圆形选择区域、内部圆缩放动画、水波纹效果和焦点指示器。通过 `QButtonGroup` 集成支持互斥选择。 @@ -12,7 +17,7 @@ class RadioButton : public QRadioButton { Q_PROPERTY(bool error READ hasError WRITE setError) Q_PROPERTY(bool pressEffectEnabled READ pressEffectEnabled WRITE setPressEffectEnabled) }; -``` +```text 头文件:`ui/widget/material/widget/radiobutton/radiobutton.h` @@ -39,7 +44,7 @@ option1->setChecked(true); // 连接信号 connect(group, &QButtonGroup::idClicked, this, &MyClass::onOptionSelected); -``` +```text ## 错误状态 @@ -51,7 +56,7 @@ if (!group->checkedButton()) { option2->setError(true); option3->setError(true); } -``` +```text 错误状态下,外环和内部圆使用 error 颜色。 @@ -62,7 +67,7 @@ if (!group->checkedButton()) { ```cpp // 禁用按压效果 radioButton->setPressEffectEnabled(false); -``` +```bash 禁用后,点击时不会触发水波纹动画,但状态层仍然正常工作。 @@ -85,7 +90,7 @@ radioButton->setPressEffectEnabled(false); // setChecked 会同步内部圆的缩放状态 radioButton->setChecked(true); // 内部圆从 0 缩放到目标尺寸 radioButton->setChecked(false); // 内部圆从目标尺寸缩放到 0 -``` +```bash ## 尺寸规范 diff --git a/document/HandBook/ui/material/widget/scrollview.md b/document/HandBook/ui/material/widget/scrollview.md index d03c2f853..be0ab69bd 100644 --- a/document/HandBook/ui/material/widget/scrollview.md +++ b/document/HandBook/ui/material/widget/scrollview.md @@ -1,3 +1,8 @@ +--- +title: "ScrollView - Material 滚动视图" +description: 是 Material Design 3 滚动区域控件的完整实现,具有自定义滚动条、淡入淡出效果和主题 +--- + # ScrollView - Material 滚动视图 `ScrollView` 是 Material Design 3 滚动区域控件的完整实现,具有自定义滚动条、淡入淡出效果和主题集成。支持水平和垂直滚动。 @@ -13,7 +18,7 @@ class ScrollView : public QScrollArea { Q_PROPERTY(int scrollbarFadeDelay READ scrollbarFadeDelay WRITE setScrollbarFadeDelay) Q_PROPERTY(bool scrollbarHoverExpansion READ scrollbarHoverExpansion WRITE setScrollbarHoverExpansion) }; -``` +```text 头文件:`ui/widget/material/widget/scrollview/scrollview.h` @@ -27,7 +32,7 @@ enum class ScrollbarState { Hovered, // 悬停 - 100% 透明度,16dp 宽度 Dragged // 拖拽 - 100% 透明度,16dp 宽度,状态层叠加 }; -``` +```text ## 基本用法 @@ -51,7 +56,7 @@ scroll->setScrollbarFadeDelay(500); // 500ms 后淡出 // 启用悬停扩展 scroll->setScrollbarHoverExpansion(true); -``` +```text ## 自定义滚动条 @@ -67,7 +72,7 @@ ScrollView 完全重写了默认滚动条渲染,使用自定义绘制: // ANIMATION_FRAME_MS = 16ms (~60fps) // ANIMATION_SPEED_WIDTH = 0.3f // ANIMATION_SPEED_OPACITY = 0.2f -``` +```text ## 淡入淡出效果 @@ -81,7 +86,7 @@ scroll->setScrollbarFadeDelay(500); // 滚动时自动显示滚动条 // 停止滚动后延迟隐藏 // 鼠标悬停时保持显示 -``` +```text ## 悬停扩展 @@ -92,7 +97,7 @@ scroll->setScrollbarHoverExpansion(true); // 悬停时: // 1. 宽度从 12dp 平滑过渡到 16dp // 2. 透明度从 40% 过渡到 100% -``` +```text ## 滑块拖动 @@ -108,7 +113,7 @@ ScrollView 支持直接拖动滚动条滑块: // isPointOverHorizontalThumb() // isPointOverVerticalTrack() // isPointOverHorizontalTrack() -``` +```text ## 滚动条覆盖层 @@ -118,7 +123,7 @@ ScrollView 使用内部 `ScrollbarOverlay` 小部件在视口上绘制滚动条 // ScrollbarOverlay 是内部实现,定义在 .cpp 中 // 通过 eventFilter 跟踪视口几何变化 // 保持滚动条与视口同步 -``` +```bash ## 绘制流程 diff --git a/document/HandBook/ui/material/widget/separator.md b/document/HandBook/ui/material/widget/separator.md index 4113e2f3b..910924cfb 100644 --- a/document/HandBook/ui/material/widget/separator.md +++ b/document/HandBook/ui/material/widget/separator.md @@ -1,3 +1,8 @@ +--- +title: "Separator - Material 分隔线" +description: 是 Material Design 3 分隔线(Divider)控件的完整实现,支持水平和垂直方向, +--- + # Separator - Material 分隔线 `Separator` 是 Material Design 3 分隔线(Divider)控件的完整实现,支持水平和垂直方向,以及全出血(Full-bleed)、内缩(Inset)和中内缩(Middle-inset)三种间距模式。 @@ -11,7 +16,7 @@ class Separator : public QFrame { Q_OBJECT Q_PROPERTY(SeparatorMode mode READ mode WRITE setMode) }; -``` +```text 头文件:`ui/widget/material/widget/separator/separator.h` @@ -25,7 +30,7 @@ enum class SeparatorMode { Inset, // 两侧各缩进 16dp MiddleInset // 仅在起始侧缩进 16dp }; -``` +```text ## 基本用法 @@ -47,7 +52,7 @@ inset->setMode(SeparatorMode::Inset); // 中内缩模式(起始侧 16dp 边距) auto* middleInset = new Separator(Qt::Horizontal, this); middleInset->setMode(SeparatorMode::MiddleInset); -``` +```text ## 方向控制 @@ -58,7 +63,7 @@ separator->setOrientation(Qt::Vertical); // 垂直分隔线 // 获取当前方向 Qt::Orientation orient = separator->orientation(); -``` +```bash ## 视觉规格 @@ -93,7 +98,7 @@ auto* hLayout = new QHBoxLayout(); hLayout->addWidget(sidebar); hLayout->addWidget(new Separator(Qt::Vertical)); hLayout->addWidget(content); -``` +```bash ## 绘制 diff --git a/document/HandBook/ui/material/widget/slider.md b/document/HandBook/ui/material/widget/slider.md index a93ac0e4c..17b1bcd73 100644 --- a/document/HandBook/ui/material/widget/slider.md +++ b/document/HandBook/ui/material/widget/slider.md @@ -1,3 +1,8 @@ +--- +title: "Slider - Material 滑块" +description: 是 Material Design 3 滑块控件的完整实现,支持水平/垂直方向、活动/非活动轨道、带 +--- + # Slider - Material 滑块 `Slider` 是 Material Design 3 滑块控件的完整实现,支持水平/垂直方向、活动/非活动轨道、带海拔的滑块、刻度标记和状态层。 @@ -10,7 +15,7 @@ namespace cf::ui::widget::material; class Slider : public QSlider { Q_OBJECT }; -``` +```text 头文件:`ui/widget/material/widget/slider/slider.h` @@ -32,7 +37,7 @@ vSlider->setRange(0, 100); // 连接信号(与 QSlider 兼容) connect(slider, &QSlider::valueChanged, this, &MyClass::onValueChanged); -``` +```text ## 方向 @@ -44,7 +49,7 @@ auto* horizontal = new Slider(Qt::Horizontal, this); // 垂直方向 auto* vertical = new Slider(Qt::Vertical, this); -``` +```text ## 轨道绘制 @@ -56,7 +61,7 @@ auto* vertical = new Slider(Qt::Vertical, this); ```cpp // 轨道高度遵循 Material Design 规范 // 滑块半径遵循 Material Design 规范 -``` +```text ## 滑块与海拔 @@ -73,7 +78,7 @@ Slider 支持刻度标记(Tick Marks)绘制: // 通过 QSlider 的标准属性控制刻度 slider->setTickPosition(QSlider::TicksBelow); slider->setTickInterval(10); -``` +```bash 刻度标记会沿着轨道均匀分布,使用 `inactiveTrackColor` 绘制。 diff --git a/document/HandBook/ui/material/widget/spinbox.md b/document/HandBook/ui/material/widget/spinbox.md index f5d9df56c..04c3a2b06 100644 --- a/document/HandBook/ui/material/widget/spinbox.md +++ b/document/HandBook/ui/material/widget/spinbox.md @@ -1,3 +1,8 @@ +--- +title: "SpinBox - Material 数值调节框" +description: 是 Material Design 3 数值调节框控件的完整实现,支持整数输入、增减按钮、描边样式、 +--- + # SpinBox - Material 数值调节框 `SpinBox` 是 Material Design 3 数值调节框控件的完整实现,支持整数输入、增减按钮、描边样式、焦点指示器和状态层效果。 @@ -10,7 +15,7 @@ namespace cf::ui::widget::material; class SpinBox : public QSpinBox { Q_OBJECT }; -``` +```text 头文件:`ui/widget/material/widget/spinbox/spinbox.h` @@ -34,7 +39,7 @@ spin->setSuffix(" px"); // 连接信号(与 QSpinBox 兼容) connect(spin, QOverload::of(&QSpinBox::valueChanged), this, &MyClass::onValueChanged); -``` +```text ## 增减按钮 @@ -44,7 +49,7 @@ SpinBox 在控件右侧提供增减按钮: // 增加按钮(incrementButtonRect) // 减少按钮(decrementButtonRect) // 鼠标悬停在按钮上时有独立的 hover 状态 -``` +```bash 每个按钮有独立的悬停和按压状态追踪: @@ -83,7 +88,7 @@ SpinBox 重写了 `resizeEvent` 以约束内部 LineEdit: ```cpp // resizeEvent() 将内部 lineEdit 限制在文本区域 // 避免输入框覆盖增减按钮区域 -``` +```bash ## 颜色系统 diff --git a/document/HandBook/ui/material/widget/state_machine.md b/document/HandBook/ui/material/widget/state_machine.md index b65a414ca..65567ed0c 100644 --- a/document/HandBook/ui/material/widget/state_machine.md +++ b/document/HandBook/ui/material/widget/state_machine.md @@ -1,189 +1,194 @@ -# StateMachine - Material 状态机 - -`StateMachine` 是 Material Design 3 交互状态的核心管理组件。它负责处理控件的各种交互状态(悬停、按下、焦点、禁用等),并按照 Material 规范驱动状态层的透明度动画。我们选择单独实现这个组件,是因为 Qt 的状态系统对 Material 的状态层动画支持不够友好,需要更精细的控制。 - -## 状态定义 - -Material Design 3 定义了以下几种交互状态,每个状态对应不同的状态层透明度: - -```cpp -enum class State { - StateNormal = 0x00, // 默认状态 - StateHovered = 0x01, // 鼠标悬停 - StatePressed = 0x02, // 鼠标按下 - StateFocused = 0x04, // 键盘焦点 - StateDisabled = 0x08, // 禁用状态 - StateChecked = 0x10, // 选中状态(如 ToggleButton) - StateDragged = 0x20, // 拖拽状态 -}; -``` - -这些状态可以组合存在(比如同时有焦点和悬停),状态机内部通过位运算处理优先级。 - -## 透明度规范 - -状态层的透明度直接影响交互反馈的"分量感"。Material Design 3 的规范如下: - -| 状态 | 透明度 | 说明 | -|------|--------|------| -| Normal | 0.00 | 无状态层 | -| Hovered | 0.08 | 轻微的视觉反馈 | -| Pressed | 0.12 | 明确的按下反馈 | -| Focused | 0.12 | 与按下相同 | -| Dragged | 0.16 | 最强的交互状态 | -| Disabled | 0.00 | 禁用时无状态层 | -| Checked | 0.08 | 与悬停相同 | - -当多个状态同时存在时,按以下优先级取最高值:`Disabled > Pressed > Dragged > Focused > Hovered > Normal`。这个顺序在 `targetOpacityForState()` 里实现,改动顺序会破坏交互的一致性。 - -## 基本用法 - -`StateMachine` 需要配合动画工厂使用,通常在控件的构造函数中初始化: - -```cpp -#include "widget/material/base/state_machine.h" - -using namespace cf::ui::widget::material; - -// 在控件构造函数中 -class MyWidget : public QWidget { -public: - MyWidget(QWidget* parent = nullptr) : QWidget(parent) { - // 获取全局动画工厂 - auto animationFactory = cf::WeakPtr::DynamicCast( - Application::animationFactory() - ); - - // 创建状态机 - m_stateMachine = new base::StateMachine(animationFactory, this); - - // 监听状态变化 - connect(m_stateMachine, &base::StateMachine::stateLayerOpacityChanged, - this, QOverload<>::of(&MyWidget::update)); - } - -private: - base::StateMachine* m_stateMachine; -}; -``` - -## 事件处理 - -状态机提供了一系列事件处理方法,需要将 Qt 的事件转发给它: - -```cpp -void MyWidget::enterEvent(QEnterEvent* event) { - QWidget::enterEvent(event); - m_stateMachine->onHoverEnter(); - update(); -} - -void MyWidget::leaveEvent(QEvent* event) { - QWidget::leaveEvent(event); - m_stateMachine->onHoverLeave(); - update(); -} - -void MyWidget::mousePressEvent(QMouseEvent* event) { - QWidget::mousePressEvent(event); - m_stateMachine->onPress(event->pos()); - update(); -} - -void MyWidget::mouseReleaseEvent(QMouseEvent* event) { - QWidget::mouseReleaseEvent(event); - m_stateMachine->onRelease(); - update(); -} - -void MyWidget::focusInEvent(QFocusEvent* event) { - QWidget::focusInEvent(event); - m_stateMachine->onFocusIn(); - update(); -} - -void MyWidget::focusOutEvent(QFocusEvent* event) { - QWidget::focusOutEvent(event); - m_stateMachine->onFocusOut(); - update(); -} -``` - -禁用状态的监听稍微特殊,因为它通过 `changeEvent` 触发: - -```cpp -void MyWidget::changeEvent(QEvent* event) { - QWidget::changeEvent(event); - if (event->type() == QEvent::EnabledChange) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - update(); - } -} -``` - -## 绘制状态层 - -状态层是一个半透明的叠加层,绘制在控件背景之上、内容之下。在 `paintEvent` 中使用状态机提供的透明度值: - -```cpp -void MyWidget::paintEvent(QPaintEvent* event) { - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - // 先绘制背景 - p.fillPath(shape, backgroundColor()); - - // 绘制状态层 - float opacity = m_stateMachine->stateLayerOpacity(); - if (opacity > 0.0f) { - CFColor stateColor = labelColor(); // 通常与文本颜色相同 - QColor layerColor = stateColor.native_color(); - layerColor.setAlphaF(layerColor.alphaF() * opacity); - p.fillPath(shape, layerColor); - } - - // 再绘制其他内容... -} -``` - -## 选中状态 - -对于有选中状态的控件(如 RadioButton、CheckBox、ToggleButton),还需要监听选中状态的变化: - -```cpp -void MyWidget::setChecked(bool checked) { - if (m_checked != checked) { - m_checked = checked; - m_stateMachine->onCheckedChanged(checked); - update(); - } -} -``` - -⚠️ 选中状态(Checked)只是一种"持久化"的悬停状态,它不应该阻止其他交互状态的叠加。 - -## 禁用状态保护 - -状态机内部已经对禁用状态做了保护——在禁用状态下,悬停、按下、焦点等交互事件不会生效。这确保了禁用控件的视觉一致性,不需要在控件层再做额外判断。 - -## 动画性能 - -状态机使用动画工厂创建淡入淡出动画,当全局动画被禁用时,会直接设置透明度值而跳过动画。这对于性能敏感的场景很有用: - -```cpp -// 在应用层面禁用动画 -auto factory = Application::animationFactory(); -if (factory) { - factory->setEnabledAll(false); -} -``` - -## 相关文档 - -- [Button - Material 按钮](./button.md) -- [RippleHelper - 水波纹效果](../base/ripple_helper.md) -- [MdElevationController - 阴影控制器](../base/elevation_controller.md) +--- +title: "StateMachine - Material 状态机" +description: 是 Material Design 3 交互状态的核心管理组件。它负责处理控件的各种交互状态(悬停、 +--- + +# StateMachine - Material 状态机 + +`StateMachine` 是 Material Design 3 交互状态的核心管理组件。它负责处理控件的各种交互状态(悬停、按下、焦点、禁用等),并按照 Material 规范驱动状态层的透明度动画。我们选择单独实现这个组件,是因为 Qt 的状态系统对 Material 的状态层动画支持不够友好,需要更精细的控制。 + +## 状态定义 + +Material Design 3 定义了以下几种交互状态,每个状态对应不同的状态层透明度: + +```cpp +enum class State { + StateNormal = 0x00, // 默认状态 + StateHovered = 0x01, // 鼠标悬停 + StatePressed = 0x02, // 鼠标按下 + StateFocused = 0x04, // 键盘焦点 + StateDisabled = 0x08, // 禁用状态 + StateChecked = 0x10, // 选中状态(如 ToggleButton) + StateDragged = 0x20, // 拖拽状态 +}; +```bash + +这些状态可以组合存在(比如同时有焦点和悬停),状态机内部通过位运算处理优先级。 + +## 透明度规范 + +状态层的透明度直接影响交互反馈的"分量感"。Material Design 3 的规范如下: + +| 状态 | 透明度 | 说明 | +|------|--------|------| +| Normal | 0.00 | 无状态层 | +| Hovered | 0.08 | 轻微的视觉反馈 | +| Pressed | 0.12 | 明确的按下反馈 | +| Focused | 0.12 | 与按下相同 | +| Dragged | 0.16 | 最强的交互状态 | +| Disabled | 0.00 | 禁用时无状态层 | +| Checked | 0.08 | 与悬停相同 | + +当多个状态同时存在时,按以下优先级取最高值:`Disabled > Pressed > Dragged > Focused > Hovered > Normal`。这个顺序在 `targetOpacityForState()` 里实现,改动顺序会破坏交互的一致性。 + +## 基本用法 + +`StateMachine` 需要配合动画工厂使用,通常在控件的构造函数中初始化: + +```cpp +#include "widget/material/base/state_machine.h" + +using namespace cf::ui::widget::material; + +// 在控件构造函数中 +class MyWidget : public QWidget { +public: + MyWidget(QWidget* parent = nullptr) : QWidget(parent) { + // 获取全局动画工厂 + auto animationFactory = cf::WeakPtr::DynamicCast( + Application::animationFactory() + ); + + // 创建状态机 + m_stateMachine = new base::StateMachine(animationFactory, this); + + // 监听状态变化 + connect(m_stateMachine, &base::StateMachine::stateLayerOpacityChanged, + this, QOverload<>::of(&MyWidget::update)); + } + +private: + base::StateMachine* m_stateMachine; +}; +```text + +## 事件处理 + +状态机提供了一系列事件处理方法,需要将 Qt 的事件转发给它: + +```cpp +void MyWidget::enterEvent(QEnterEvent* event) { + QWidget::enterEvent(event); + m_stateMachine->onHoverEnter(); + update(); +} + +void MyWidget::leaveEvent(QEvent* event) { + QWidget::leaveEvent(event); + m_stateMachine->onHoverLeave(); + update(); +} + +void MyWidget::mousePressEvent(QMouseEvent* event) { + QWidget::mousePressEvent(event); + m_stateMachine->onPress(event->pos()); + update(); +} + +void MyWidget::mouseReleaseEvent(QMouseEvent* event) { + QWidget::mouseReleaseEvent(event); + m_stateMachine->onRelease(); + update(); +} + +void MyWidget::focusInEvent(QFocusEvent* event) { + QWidget::focusInEvent(event); + m_stateMachine->onFocusIn(); + update(); +} + +void MyWidget::focusOutEvent(QFocusEvent* event) { + QWidget::focusOutEvent(event); + m_stateMachine->onFocusOut(); + update(); +} +```text + +禁用状态的监听稍微特殊,因为它通过 `changeEvent` 触发: + +```cpp +void MyWidget::changeEvent(QEvent* event) { + QWidget::changeEvent(event); + if (event->type() == QEvent::EnabledChange) { + if (isEnabled()) { + m_stateMachine->onEnable(); + } else { + m_stateMachine->onDisable(); + } + update(); + } +} +```text + +## 绘制状态层 + +状态层是一个半透明的叠加层,绘制在控件背景之上、内容之下。在 `paintEvent` 中使用状态机提供的透明度值: + +```cpp +void MyWidget::paintEvent(QPaintEvent* event) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + // 先绘制背景 + p.fillPath(shape, backgroundColor()); + + // 绘制状态层 + float opacity = m_stateMachine->stateLayerOpacity(); + if (opacity > 0.0f) { + CFColor stateColor = labelColor(); // 通常与文本颜色相同 + QColor layerColor = stateColor.native_color(); + layerColor.setAlphaF(layerColor.alphaF() * opacity); + p.fillPath(shape, layerColor); + } + + // 再绘制其他内容... +} +```text + +## 选中状态 + +对于有选中状态的控件(如 RadioButton、CheckBox、ToggleButton),还需要监听选中状态的变化: + +```cpp +void MyWidget::setChecked(bool checked) { + if (m_checked != checked) { + m_checked = checked; + m_stateMachine->onCheckedChanged(checked); + update(); + } +} +```text + +⚠️ 选中状态(Checked)只是一种"持久化"的悬停状态,它不应该阻止其他交互状态的叠加。 + +## 禁用状态保护 + +状态机内部已经对禁用状态做了保护——在禁用状态下,悬停、按下、焦点等交互事件不会生效。这确保了禁用控件的视觉一致性,不需要在控件层再做额外判断。 + +## 动画性能 + +状态机使用动画工厂创建淡入淡出动画,当全局动画被禁用时,会直接设置透明度值而跳过动画。这对于性能敏感的场景很有用: + +```cpp +// 在应用层面禁用动画 +auto factory = Application::animationFactory(); +if (factory) { + factory->setEnabledAll(false); +} +```text + +## 相关文档 + +- [Button - Material 按钮](./button.md) +- [RippleHelper - 水波纹效果](../base/ripple_helper.md) +- [MdElevationController - 阴影控制器](../base/elevation_controller.md) diff --git a/document/HandBook/ui/material/widget/switch.md b/document/HandBook/ui/material/widget/switch.md index ecbcea86b..71090fb5c 100644 --- a/document/HandBook/ui/material/widget/switch.md +++ b/document/HandBook/ui/material/widget/switch.md @@ -1,3 +1,8 @@ +--- +title: "Switch - Material 开关" +description: 是 Material Design 3 开关(Toggle)控件的完整实现,带有动画滑块位置、轨道颜 +--- + # Switch - Material 开关 `Switch` 是 Material Design 3 开关(Toggle)控件的完整实现,带有动画滑块位置、轨道颜色过渡和状态层。提供 CheckBox 的替代方案,适用于二进制开/关设置。 @@ -10,7 +15,7 @@ namespace cf::ui::widget::material; class Switch : public QCheckBox { Q_OBJECT }; -``` +```text 头文件:`ui/widget/material/widget/switch/switch.h` @@ -30,7 +35,7 @@ wifiSwitch->setChecked(true); // 连接信号(与 QCheckBox 兼容) connect(wifiSwitch, &QCheckBox::toggled, this, &MyClass::onWifiToggled); -``` +```bash ## 开关尺寸 @@ -53,7 +58,7 @@ Switch 遵循 Material Design 3 的尺寸规范: toggle->setChecked(true); // 滑块从左滑到右 toggle->setChecked(false); // 滑块从右滑到左 -``` +```bash 内部使用 `m_inNextCheckState` 标志防止 `setChecked()` 在状态切换时直接跳过动画。 diff --git a/document/HandBook/ui/material/widget/tableview.md b/document/HandBook/ui/material/widget/tableview.md index 790583404..9ce65a748 100644 --- a/document/HandBook/ui/material/widget/tableview.md +++ b/document/HandBook/ui/material/widget/tableview.md @@ -1,3 +1,8 @@ +--- +title: "TableView - Material 表格视图" +description: 是 Material Design 3 表格视图控件的完整实现,用于二维数据展示。具有自定义表头渲染 +--- + # TableView - Material 表格视图 `TableView` 是 Material Design 3 表格视图控件的完整实现,用于二维数据展示。具有自定义表头渲染、网格线、带水波纹的行选择、排序指示器、列调整大小反馈和交替行颜色。 @@ -15,7 +20,7 @@ class TableView : public QTableView { Q_PROPERTY(bool alternatingRowColors READ alternatingRowColors WRITE setAlternatingRowColors) Q_PROPERTY(bool rippleEnabled READ rippleEnabled WRITE setRippleEnabled) }; -``` +```text 头文件:`ui/widget/material/widget/tableview/tableview.h` @@ -26,7 +31,7 @@ enum class TableRowHeight { Compact, // 48dp - 紧凑模式,适合密集数据 Standard // 56dp - 标准模式(默认) }; -``` +```text ## 网格线样式 @@ -37,7 +42,7 @@ enum class TableGridStyle { Vertical, // 仅垂直线 Both // 水平和垂直线(默认) }; -``` +```text ## 基本用法 @@ -72,7 +77,7 @@ table->setModel(model); // 连接信号 connect(table, &QTableView::clicked, this, &MyClass::onCellClicked); -``` +```text ## 交替行颜色 @@ -82,7 +87,7 @@ table->setAlternatingRowColors(true); // 禁用交替行颜色 table->setAlternatingRowColors(false); -``` +```text 交替行颜色使用 `SurfaceVariant` 的 5% 透明度,提供轻微的视觉区分而不影响阅读。 @@ -94,7 +99,7 @@ table->setAlternatingRowColors(false); // 选中行有 PrimaryContainer 叠加层 // 水波纹效果从点击位置扩散 // 通过 m_hoveredRow 和 m_pressedRow 追踪状态 -``` +```text ## 表头 @@ -103,7 +108,7 @@ table->setAlternatingRowColors(false); ```cpp // 显示/隐藏表头 table->setShowHeader(true); -``` +```bash 表头支持排序指示器和列调整大小的视觉反馈。 diff --git a/document/HandBook/ui/material/widget/tabview.md b/document/HandBook/ui/material/widget/tabview.md index c7a8f3fcc..5e5f3fb44 100644 --- a/document/HandBook/ui/material/widget/tabview.md +++ b/document/HandBook/ui/material/widget/tabview.md @@ -1,3 +1,8 @@ +--- +title: "TabView - Material 标签页" +description: 是 Material Design 3 标签页控件的完整实现,支持标签文本、带滑动动画的选中指示器和 +--- + # TabView - Material 标签页 `TabView` 是 Material Design 3 标签页控件的完整实现,支持标签文本、带滑动动画的选中指示器和标签滚动。包含状态层、焦点指示器和 Material Design 3 样式。 @@ -13,7 +18,7 @@ class TabView : public QTabWidget { Q_PROPERTY(int tabMinWidth READ tabMinWidth WRITE setTabMinWidth) Q_PROPERTY(bool showIndicator READ showIndicator WRITE setShowIndicator) }; -``` +```text 头文件:`ui/widget/material/widget/tabview/tabview.h` @@ -37,7 +42,7 @@ tabs->setTabHeight(48); // 连接信号 connect(tabs, &QTabWidget::currentChanged, this, &MyClass::onTabChanged); -``` +```text ## 标签高度和宽度 @@ -47,7 +52,7 @@ tabs->setTabHeight(48); // 设置标签最小宽度(默认遵循 Material Design 3 规范) tabs->setTabMinWidth(90); -``` +```text ## 选中指示器 @@ -57,7 +62,7 @@ tabs->setShowIndicator(true); // 选中指示器是一个滑动条,在标签之间平滑过渡 // 使用 Primary 颜色绘制 -``` +```text 指示器在标签切换时通过滑动动画移动到新的选中标签位置。 @@ -70,7 +75,7 @@ tabs->setTabCloseable(1, true); // 第二个标签可关闭 // 连接关闭信号 connect(tabs, &TabView::tabCloseRequested, this, &MyClass::onTabClose); -``` +```text 可关闭的标签会显示关闭按钮。 @@ -81,7 +86,7 @@ TabView 使用内部 `MaterialTabBar` 实现标签栏: ```cpp // MaterialTabBar 是内部实现,不暴露给外部 // 负责标签的绘制、选中指示器动画和滚动 -``` +```bash ## 绘制流程 diff --git a/document/HandBook/ui/material/widget/textarea.md b/document/HandBook/ui/material/widget/textarea.md index 79944351d..e2fcdbb1d 100644 --- a/document/HandBook/ui/material/widget/textarea.md +++ b/document/HandBook/ui/material/widget/textarea.md @@ -1,3 +1,8 @@ +--- +title: "TextArea - Material 多行文本框" +description: 是 Material Design 3 多行文本输入框控件的完整实现,支持填充(Filled)和描边 +--- + # TextArea - Material 多行文本框 `TextArea` 是 Material Design 3 多行文本输入框控件的完整实现,支持填充(Filled)和描边(Outlined)两种变体。包含浮动标签、字符计数器、帮助/错误文本和自动调整高度功能。 @@ -19,7 +24,7 @@ class TextArea : public QTextEdit { Q_PROPERTY(int minLines READ minLines WRITE setMinLines) Q_PROPERTY(int maxLines READ maxLines WRITE setMaxLines) }; -``` +```text 头文件:`ui/widget/material/widget/textarea/textarea.h` @@ -30,7 +35,7 @@ enum class TextAreaVariant { Filled, // 填充变体 - 背景填充色,底部指示线 Outlined, // 描边变体 - 圆角边框,无背景填充 }; -``` +```text ## 基本用法 @@ -54,7 +59,7 @@ textarea->setShowCharacterCounter(true); // 连接信号 connect(textarea, &QTextEdit::textChanged, this, &MyClass::onContentChanged); -``` +```text ## 自动调整高度 @@ -69,7 +74,7 @@ textarea->setMaxLines(6); // 当内容增加时,文本框会从 minLines 增长到 maxLines // 超过 maxLines 后,内部会出现滚动条 -``` +```text `keyPressEvent` 会在达到 `maxLines` 限制时阻止 Enter 键产生新行。 @@ -82,7 +87,7 @@ textarea->setLabel("Comments"); // 浮动动画由 CFMaterialAnimationFactory 驱动 // floatingProgress 从 0.0(静止)到 1.0(浮动) -``` +```text ## 帮助文本与错误文本 @@ -92,7 +97,7 @@ textarea->setHelperText("Maximum 500 characters"); // 错误文本优先级高于帮助文本 textarea->setErrorText("Content exceeds maximum length"); textarea->setErrorText(""); // 清除错误 -``` +```bash ## 与 TextField 的区别 diff --git a/document/HandBook/ui/material/widget/textfield.md b/document/HandBook/ui/material/widget/textfield.md index 5af21bf03..9b56539fe 100644 --- a/document/HandBook/ui/material/widget/textfield.md +++ b/document/HandBook/ui/material/widget/textfield.md @@ -1,3 +1,8 @@ +--- +title: "TextField - Material 文本输入框" +description: 是 Material Design 3 文本输入框控件的完整实现,支持填充(Filled)和描边(O +--- + # TextField - Material 文本输入框 `TextField` 是 Material Design 3 文本输入框控件的完整实现,支持填充(Filled)和描边(Outlined)两种视觉变体。包含浮动标签、前缀/后缀图标、字符计数器、帮助/错误文本和密码模式。 @@ -19,7 +24,7 @@ class TextField : public QLineEdit { Q_PROPERTY(QIcon prefixIcon READ prefixIcon WRITE setPrefixIcon) Q_PROPERTY(QIcon suffixIcon READ suffixIcon WRITE setSuffixIcon) }; -``` +```text 头文件:`ui/widget/material/widget/textfield/textfield.h` @@ -32,7 +37,7 @@ enum class TextFieldVariant { Filled, // 填充变体 - 背景填充色,底部指示线 Outlined, // 描边变体 - 圆角边框,无背景填充 }; -``` +```text 选择哪种变体取决于整体设计风格。Filled 变体适合密集表单,Outlined 变体适合突出显示的场景。 @@ -58,7 +63,7 @@ search->setLabel("Search"); // 连接信号 connect(field1, &QLineEdit::textChanged, this, &MyClass::onTextChanged); -``` +```text ## 浮动标签 @@ -70,7 +75,7 @@ field->setLabel("Password"); // 标签行为: // - 空内容 + 无焦点:标签在输入区域内(占位符位置) // - 有内容或有焦点:标签浮动到输入框上方(小字体) -``` +```text 浮动动画通过 `m_floatingProgress`(0.0 到 1.0)控制,由 `CFMaterialAnimationFactory` 驱动平滑过渡。 @@ -85,7 +90,7 @@ field->setErrorText("Password is too short"); // 清除错误 field->setErrorText(""); // 恢复显示帮助文本 -``` +```text 错误状态下,输入框边框/指示线使用 error 颜色。 @@ -100,7 +105,7 @@ field->setSuffixIcon(QIcon::fromTheme("visibility_off")); // 密码模式(使用 QLineEdit 的内置功能) field->setEchoMode(QLineEdit::Password); -``` +```text ## 字符计数器 @@ -111,7 +116,7 @@ field->setShowCharacterCounter(true); // 显示格式:当前字符数 / 最大长度 // 例如:"42 / 100" -``` +```bash 字符计数器显示在帮助文本区域的右侧。 diff --git a/document/HandBook/ui/material/widget/treeview.md b/document/HandBook/ui/material/widget/treeview.md index da2c56c62..67579bfd6 100644 --- a/document/HandBook/ui/material/widget/treeview.md +++ b/document/HandBook/ui/material/widget/treeview.md @@ -1,3 +1,8 @@ +--- +title: "TreeView - Material 树视图" +description: 是 Material Design 3 树视图控件的完整实现,用于层级数据展示。具有展开/折叠动画、 +--- + # TreeView - Material 树视图 `TreeView` 是 Material Design 3 树视图控件的完整实现,用于层级数据展示。具有展开/折叠动画、树连接线、正确的层级缩进和 Material Design 3 颜色令牌。 @@ -14,7 +19,7 @@ class TreeView : public QTreeView { Q_PROPERTY(bool showTreeLines READ showTreeLines WRITE setShowTreeLines) Q_PROPERTY(bool rootIsDecorated READ rootIsDecorated WRITE setRootIsDecorated) }; -``` +```text 头文件:`ui/widget/material/widget/treeview/treeview.h` @@ -25,7 +30,7 @@ enum class TreeItemHeight { Compact, // 48dp - 紧凑模式 Standard // 56dp - 标准模式(默认) }; -``` +```text ## 缩进样式 @@ -34,7 +39,7 @@ enum class TreeIndentStyle { Material, // 56dp 每级 + 引导线 Classic // 传统嵌套缩进 }; -``` +```text ## 基本用法 @@ -64,7 +69,7 @@ tree->setModel(model); // 连接信号 connect(tree, &QTreeView::clicked, this, &MyClass::onItemClicked); -``` +```text ## 树连接线 @@ -74,7 +79,7 @@ tree->setShowTreeLines(true); // 连接线在父节点和子节点之间绘制 // 使用 OutlineVariant 颜色 -``` +```text ## 展开/折叠 @@ -83,7 +88,7 @@ TreeView 使用 `TreeViewItemDelegate` 处理展开/折叠图标的渲染: ```cpp // drawBranches() 被重写为空实现 // 所有展开/折叠图标由委托渲染,避免与默认 Qt 渲染冲突 -``` +```text ## 根节点装饰 @@ -93,7 +98,7 @@ tree->setRootIsDecorated(true); // 隐藏根节点装饰 tree->setRootIsDecorated(false); -``` +```bash ## 交互状态 diff --git a/document/ci/.pages b/document/ci/.pages deleted file mode 100644 index a219f3227..000000000 --- a/document/ci/.pages +++ /dev/null @@ -1,6 +0,0 @@ -title: CI/CD -icon: material/pipe -nav: - - Docker 环境: docker-environment.md - - 工具链配置: toolchain-setup.md - - 构建入口: ci-build-entry.md diff --git a/document/ci/README.md b/document/ci/README.md index 02736134f..72cc40a0c 100644 --- a/document/ci/README.md +++ b/document/ci/README.md @@ -1,174 +1,179 @@ -# CFDesktop CI/CD 架构文档 - -## 概述 - -CFDesktop 采用多架构 Docker 容器构建方案,确保代码在不同平台上都能正确编译和运行。 - -### 核心特性 - -- **利用现有机制**: 通过 `check_toolchain.cmake` 实现工具链选择 -- **多架构支持**: 使用 Docker `--platform` 参数支持 AMD64 和 ARM64 原生编译 -- **本地验证**: 通过 Docker 在本地进行多架构构建测试 -- **独立配置**: CI 构建与本地开发构建使用独立的配置和目录 - -## 系统架构 - -``` -开发者本地 Docker 容器构建 - │ │ - │ 修改代码 │ - │ │ │ - │ 本地测试 │ - │ │ │ - ├──────┴───────────────────────────────>│ - │ │ │ - │ │ ┌────┴────┐ - │ │ │ 选择架构 │ - │ │ └────┬────┘ - │ │ │ - │ │ ┌─────┴─────┐ - │ │ │ 启动容器 │ - │ │ └─────┬─────┘ - │ │ │ - │ │ ┌───────────┼───────────┐ - │ │ │ │ │ - │ │ ┌────▼───┐ ┌───▼────┐ ┌──▼─────┐ - │ │ │ AMD64 │ │ ARM64 │ │ ARM32 │ - │ │ │ Docker │ │ Docker │ │ Docker │ - │ │ │ + Test │ │ + Test │ │ + Test │ - │ │ └────────┘ └────────┘ └────────┘ - │ │ │ │ │ - │ │ └───────────┼───────────┘ - │ │ │ - │ │ 构建成功? - │ │ │ - │ │ ┌─────▼─────┐ - │ │ │ 验证产物 │ - │ │ └───────────┘ - │ │ - │ 继续开发 ✓ - │ - └──> 代码就绪 -``` - -## 设计理念 - -### 1. 工具链选择机制 - -项目使用 [`check_toolchain.cmake`](../../cmake/check_toolchain.cmake) 实现工具链自动选择: - -``` -配置: toolchain=linux/ci-x86_64 - ↓ -CMake: -DUSE_TOOLCHAIN=linux/ci-x86_64 - ↓ -自动解析: cmake/cmake_toolchain/linux/ci-x86_64-toolchain.cmake - ↓ -设置: CMAKE_TOOLCHAIN_FILE -``` - -### 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 -``` - -### 直接运行构建 - -```bash -# 在 Linux 环境中直接运行 -bash scripts/build_helpers/ci_build_entry.sh ci -``` - -### 查看完整文档 - -```bash -# 查看完整的流水线设计文档 -cat PIPELINE.md -``` - ---- - -*文档版本: v2.0 | 最后更新: 2026-03-07* +--- +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/ci-build-entry.md b/document/ci/ci-build-entry.md index 6616730f6..f9120adbb 100644 --- a/document/ci/ci-build-entry.md +++ b/document/ci/ci-build-entry.md @@ -1,3 +1,8 @@ +--- +title: CI 构建入口设置指南 +description: "状态: ✅ 已完成,完成日期: 2026-03-07" +--- + # CI 构建入口设置指南 **状态**: ✅ 已完成 @@ -16,13 +21,13 @@ ## 文件结构 -``` +```text scripts/build_helpers/ ├── ci_build_entry.sh # CI 构建入口脚本 ├── build_ci_config.ini # AMD64 CI 配置 ├── build_ci_aarch64_config.ini # ARM64 CI 配置 └── build_ci_armhf_config.ini # ARM32 CI 配置 -``` +```text ## CI 构建配置 @@ -44,7 +49,7 @@ build_dir=out/build_ci # CI 专用构建目录 [options] jobs=16 # 并行编译任务数 -``` +```text ### build_ci_aarch64_config.ini (ARM64) @@ -64,7 +69,7 @@ build_dir=out/build_ci_aarch64 # ARM64 专用构建目录 [options] jobs=16 -``` +```text ### build_ci_armhf_config.ini (ARM32) @@ -84,7 +89,7 @@ build_dir=out/build_ci_armhf # ARM32 专用构建目录 [options] jobs=8 -``` +```text ## CI 构建入口脚本 @@ -106,11 +111,11 @@ bash scripts/build_helpers/ci_build_entry.sh ci # 仅运行测试 bash scripts/build_helpers/ci_build_entry.sh test -``` +```text ### 架构检测流程 -``` +```text ci_build_entry.sh │ ├── 检测架构 (uname -m) @@ -126,7 +131,7 @@ ci_build_entry.sh │ └── 执行构建 └──> linux_develop_build.sh ci -c -``` +```bash ## 日志系统 @@ -141,7 +146,7 @@ ci_build_entry.sh **示例输出**: -``` +```text [2026-03-07 10:30:00] [INFO] ======================================== [2026-03-07 10:30:00] [INFO] CI Build Entry Point [2026-03-07 10:30:00] [INFO] Mode: ci @@ -152,7 +157,7 @@ ci_build_entry.sh ... [2026-03-07 10:35:00] [SUCCESS] CI build completed successfully! [2026-03-07 10:35:00] [INFO] ======================================== -``` +```text ## 集成方式 @@ -172,14 +177,14 @@ docker run --rm --platform linux/arm64 \ -v $(pwd):/project \ cfdesktop-build:arm64 \ bash scripts/build_helpers/ci_build_entry.sh ci -``` +```text ### 直接运行 ```bash # 在 Linux 环境中直接运行 bash scripts/build_helpers/ci_build_entry.sh ci -``` +```text ## 错误处理 @@ -194,7 +199,7 @@ else log "CI build failed with exit code: $exit_code" "ERROR" exit $exit_code fi -``` +```text ## 验证方法 @@ -206,7 +211,7 @@ bash scripts/build_helpers/docker_start.sh --verify # ARM64 bash scripts/build_helpers/docker_start.sh --arch arm64 --verify -``` +```text ### 2. 验证配置 @@ -219,7 +224,7 @@ cat scripts/build_helpers/build_ci_config.ini # generator=Unix Makefiles # toolchain=linux/ci-x86_64 # build_type=Release -``` +```text ### 3. 验证构建目录 @@ -229,7 +234,7 @@ ls -la out/build_ci/ # 与开发构建分离 ls -la out/build_develop/ -``` +```text ## 技术细节 @@ -252,14 +257,14 @@ case "$ARCH" in exit 1 ;; esac -``` +```text ### 脚本位置计算 ```bash SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -``` +```bash 这确保脚本可以从任何位置正确调用,并找到相关文件。 @@ -292,7 +297,7 @@ docker run --rm --platform linux/amd64 ... # ARM64 docker run --rm --platform linux/arm64 ... -``` +```bash ### Q: 构建产物在哪里? diff --git a/document/ci/docker-environment.md b/document/ci/docker-environment.md index d202af65c..dc21eb218 100644 --- a/document/ci/docker-environment.md +++ b/document/ci/docker-environment.md @@ -1,3 +1,8 @@ +--- +title: Docker 构建环境设置指南 +description: "状态: ✅ 已完成,完成日期: 2026-03-07" +--- + # Docker 构建环境设置指南 **状态**: ✅ 已完成 @@ -16,7 +21,7 @@ ## 文件结构 -``` +```text scripts/ ├── docker/ │ ├── Dockerfile.build # 多架构 Docker 镜像定义 @@ -25,7 +30,7 @@ scripts/ └── build_helpers/ ├── docker_start.sh # Linux 启动脚本 └── docker_start.ps1 # Windows 启动脚本 -``` +```text ## Docker 镜像 @@ -48,7 +53,7 @@ ARG QT_VERSION=6.8.1 # Qt 版本 ARG QT_ARCH=linux_gcc_64 # Qt 架构 (gcc_64/gcc_arm64) ARG QT_MIRROR= # Qt 镜像源 ARG INSTALL_DEPS=1 # 是否安装依赖 -``` +```text **构建镜像**: ```bash @@ -62,7 +67,7 @@ docker build --platform linux/arm64 \ -f scripts/docker/Dockerfile.build \ --build-arg QT_ARCH=linux_gcc_arm64 \ -t cfdesktop-build:arm64 . -``` +```text ## Docker Compose 配置 @@ -74,7 +79,7 @@ services: build-amd64: # AMD64 构建环境 build-arm64: # ARM64 构建环境 verify: # 快速验证服务 -``` +```text **使用方式**: ```bash @@ -89,7 +94,7 @@ docker-compose -f scripts/docker/docker-compose.yml run build-arm64 # 运行验证 docker-compose -f scripts/docker/docker-compose.yml run verify -``` +```text ## 启动脚本 @@ -112,7 +117,7 @@ docker-compose -f scripts/docker/docker-compose.yml run verify --verify # 运行 CI 构建验证 --no-log # 禁用文件日志 --help # 显示帮助信息 -``` +```text **使用示例**: ```bash @@ -127,7 +132,7 @@ bash scripts/build_helpers/docker_start.sh --verify # ARM64 构建 bash scripts/build_helpers/docker_start.sh --arch arm64 -``` +```bash ### Windows: docker_start.ps1 @@ -172,7 +177,7 @@ MOUNT_PATH="$(pwd | sed 's|^\([A-Za-z]\):|/\1|' | sed 's|\\|/|g')" # Linux / macOS MOUNT_PATH="$PROJECT_ROOT" -``` +```text ## 验证方法 @@ -189,7 +194,7 @@ docker build --platform linux/arm64 \ -f scripts/docker/Dockerfile.build \ --build-arg QT_ARCH=linux_gcc_arm64 \ -t cfdesktop-build:arm64 . -``` +```text ### 2. 验证环境 @@ -203,7 +208,7 @@ docker run --rm --platform linux/amd64 \ which cmake # /usr/bin/cmake which qmake6 # /opt/Qt/6.8.1/gcc_64/bin/qmake6 gcc --version # Ubuntu GCC -``` +```text ### 3. 运行构建 @@ -216,7 +221,7 @@ docker run --rm --platform linux/amd64 \ -v $(pwd):/project \ cfdesktop-build \ bash scripts/build_helpers/ci_build_entry.sh ci -``` +```text ## 日志系统 @@ -231,10 +236,10 @@ docker run --rm --platform linux/amd64 \ ### Docker 未安装或未运行 脚本会自动检测并提示: -``` +```text 错误: Docker 未安装或未运行 请安装 Docker Desktop: https://www.docker.com/products/docker-desktop -``` +```text ### ARM64 在 x86_64 主机上无法运行 @@ -242,7 +247,7 @@ docker run --rm --platform linux/amd64 \ ```bash # 安装 QEMU docker run --privileged --rm tonistiigi/binfmt --install all -``` +```yaml ### Windows 路径问题 diff --git a/document/ci/index.md b/document/ci/index.md index 51a440369..8834f9d2e 100644 --- a/document/ci/index.md +++ b/document/ci/index.md @@ -1,11 +1,23 @@ -# CI +--- +title: CI/CD +description: CFDesktop 当前使用 GitHub Actions 分层验证。 +--- -> Welcome to the CI section. +# CI/CD -## Overview +CFDesktop 当前使用 GitHub Actions 分层验证。 -Documentation and resources for CI. +## 工作流 ---- +| Workflow | 触发 | 作用 | +|----------|------|------| +| `docs-check.yml` | PR 到 `develop` 且带 `build-doc` 标签 | 运行 VitePress 文档构建 | +| `cpp-check.yml` | PR 到 `main` | 运行 Linux C++ 构建、CTest 和文档构建 | +| `deploy.yml` | push 到 `main` | 构建并发布 VitePress 到 GitHub Pages | + +## 分支策略 -*Last updated: 2026-03-20* +- `feature/* -> develop`: 默认轻量,不强制全量构建。 +- 文档相关 PR: 打 `build-doc` 标签触发文档构建。 +- `develop -> main`: 必须通过 C++ build/test 和 docs build。 +- `main`: 合入后发布文档站。 diff --git a/document/ci/toolchain-setup.md b/document/ci/toolchain-setup.md index 9f3023d74..84acfb647 100644 --- a/document/ci/toolchain-setup.md +++ b/document/ci/toolchain-setup.md @@ -1,3 +1,8 @@ +--- +title: CI 工具链设置指南 +description: 创建专门面向 CI 构建的精简工具链文件,利用现有的 机制,实现 Docker 容器内的原生编译。 +--- + # CI 工具链设置指南 ## 目标 @@ -23,7 +28,7 @@ ## 目录结构 -``` +```text cmake/ └── cmake_toolchain/ ├── windows/ # 已存在 @@ -32,7 +37,7 @@ cmake/ └── linux/ # 扩展 ├── ci-x86_64-toolchain.cmake # AMD64 CI 工具链 └── ci-aarch64-toolchain.cmake # ARM64 CI 工具链 -``` +```text ## 工具链文件说明 @@ -45,7 +50,7 @@ cmake/ **使用方式**: ```bash cmake -DUSE_TOOLCHAIN=linux/ci-x86_64 -S . -B build -``` +```text ### ci-aarch64-toolchain.cmake @@ -56,7 +61,7 @@ cmake -DUSE_TOOLCHAIN=linux/ci-x86_64 -S . -B build **使用方式**: ```bash cmake -DUSE_TOOLCHAIN=linux/ci-aarch64 -S . -B build -``` +```text ## 工具链文件内容 @@ -103,7 +108,7 @@ if(CCACHE_PROGRAM) set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}" CACHE FILEPATH "C compiler launcher") set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}" CACHE FILEPATH "CXX compiler launcher") endif() -``` +```text ## ccache 支持 @@ -117,13 +122,13 @@ ccache 是一个编译缓存工具,可以显著加速重复构建: ```bash # 在 Dockerfile 中配置 ccache --max-size=5G --set-config=compiler_check=%compiler%2S -s -``` +```text ## 多架构支持 采用分离的架构特定工具链方案: -``` +```text ┌─────────────────────────────────────────────────────────┐ │ Docker 容器 │ ├──────────────────────────┬──────────────────────────────┤ @@ -133,7 +138,7 @@ ccache --max-size=5G --set-config=compiler_check=%compiler%2S -s │ Qt路径: gcc_64 │ Qt路径: gcc_arm64 │ │ 编译器: x86_64-gcc │ 编译器: aarch64-gcc │ └──────────────────────────┴──────────────────────────────┘ -``` +```text CI 构建脚本会自动检测容器架构并选择对应的工具链。 @@ -153,7 +158,7 @@ file out/build_ci/bin/* # AMD64 预期输出: # ELF 64-bit LSB executable, x86-64, ... -``` +```text ### Docker 容器验证 @@ -165,7 +170,7 @@ bash scripts/build_helpers/docker_start.sh --arch amd64 --mode build # 测试 ARM64 bash scripts/build_helpers/docker_start.sh --arch arm64 --mode build -``` +```bash ## 预期结果 @@ -193,7 +198,7 @@ ls cmake/cmake_toolchain/linux/ci-aarch64-toolchain.cmake # 确认使用正确的简写格式 cmake -DUSE_TOOLCHAIN=linux/ci-x86_64 -S . -B build cmake -DUSE_TOOLCHAIN=linux/ci-aarch64 -S . -B build -``` +```text ### Q: Qt6 not found @@ -206,7 +211,7 @@ find /opt/Qt -name "Qt6Config.cmake" 2>/dev/null # 在容器内安装 Qt6(使用 aqtinstall) python3 -m aqt install-qt --outputdir /opt/Qt 6.8.1 linux desktop gcc_64 -``` +```text ### Q: ccache 未启用 @@ -216,13 +221,13 @@ python3 -m aqt install-qt --outputdir /opt/Qt 6.8.1 linux desktop gcc_64 ```bash # 在 Dockerfile 中添加 RUN apt-get install -y ccache -``` +```text ## 与现有系统集成 ### 工具链选择流程 -``` +```text INI 配置: toolchain=linux/ci-x86_64 (或 ci-aarch64) ↓ 构建脚本: ci_build_entry.sh 自动检测架构 @@ -234,7 +239,7 @@ check_toolchain.cmake 解析 查找对应的工具链文件 ↓ 设置: CMAKE_TOOLCHAIN_FILE -``` +```text ### 配置文件示例 @@ -244,7 +249,7 @@ check_toolchain.cmake 解析 generator=Ninja toolchain=linux/ci-x86_64 build_type=Release -``` +```text ```ini # scripts/build_helpers/build_ci_aarch64_config.ini (ARM64) @@ -252,7 +257,7 @@ build_type=Release generator=Ninja toolchain=linux/ci-aarch64 build_type=Release -``` +```yaml ## 后续步骤 diff --git a/document/design_stage/.pages b/document/design_stage/.pages deleted file mode 100644 index 83006c959..000000000 --- a/document/design_stage/.pages +++ /dev/null @@ -1,11 +0,0 @@ -title: 设计阶段 -icon: material/drawing -nav: - - Phase 0 · 工程骨架: 00_phase0_project_skeleton.md - - Phase 1 · 硬件探针: 01_phase1_hardware_probe.md - - Phase 2 · 基础库: 02_phase2_base_library.md - - Phase 3 · 输入层: 03_phase3_input_layer.md - - Phase 4 · 模拟器: 04_phase6_simulator.md - - Phase 5 · 测试: 05_phase8_testing.md - - 多显示后端架构: multi_display_backend_architecture.md - - 系统架构总览: system_architecture_overview.md diff --git a/document/design_stage/00_phase0_project_skeleton.md b/document/design_stage/00_phase0_project_skeleton.md index d8ead8a4b..2b730dfe5 100644 --- a/document/design_stage/00_phase0_project_skeleton.md +++ b/document/design_stage/00_phase0_project_skeleton.md @@ -1,3 +1,8 @@ +--- +title: "Phase 0: 工程骨架搭建详细设计文档" +description: "- \"xaver.clang-tidy\"" +--- + # Phase 0: 工程骨架搭建详细设计文档 ## 文档信息 @@ -63,7 +68,7 @@ add_subdirectory(tests) # ==================== 打包配置 ==================== include(CPack) -``` +```text ### 3.2 Base 库 CMakeLists.txt @@ -122,7 +127,7 @@ install(TARGETS CFFesktopBase install(FILES ${BASE_PUBLIC_HEADERS} DESTINATION include/CFDesktop/Base ) -``` +```text ### 3.3 SDK 库 CMakeLists.txt @@ -145,7 +150,7 @@ install(FILES cmake/CFDesktopSDKConfigVersion.cmake DESTINATION lib/cmake/CFDesktopSDK ) -``` +```text ### 3.4 Shell 主程序 CMakeLists.txt @@ -166,7 +171,7 @@ target_link_libraries(cfdesktop-shell PRIVATE install(TARGETS cfdesktop-shell RUNTIME DESTINATION bin ) -``` +```text ### 3.5 交叉编译工具链配置 @@ -187,7 +192,7 @@ 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) 工具链 @@ -206,7 +211,7 @@ 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 @@ -260,7 +265,7 @@ set(Qt6_DIR /opt/qt6-arm64/lib/cmake/Qt6) } ] } -``` +```yaml --- @@ -341,7 +346,7 @@ jobs: - name: Format Check run: | ./tools/format.sh --check -``` +```text ### 4.2 部署流程 (.github/workflows/deploy.yml) @@ -377,7 +382,7 @@ jobs: systemctl stop cfdesktop tar -xzf cfdesktop.tar.gz -C /opt/ systemctl start cfdesktop -``` +```yaml --- @@ -400,7 +405,7 @@ jobs: "C_Cpp.clang_format_style": "file", "clang-format.style.location": ".clang-format" } -``` +```text ### 5.2 推荐扩展 @@ -437,7 +442,7 @@ IncludeCategories: Priority: 3 - Regex: '^".*"' Priority: 4 -``` +```bash --- diff --git a/document/design_stage/01_phase1_hardware_probe.md b/document/design_stage/01_phase1_hardware_probe.md index 6af91373c..f22d15ea3 100644 --- a/document/design_stage/01_phase1_hardware_probe.md +++ b/document/design_stage/01_phase1_hardware_probe.md @@ -1,3 +1,8 @@ +--- +title: "Phase 1: 硬件探针与能力分级详细设计文档" +description: "Phase 1: 硬件探针与能力分级详细设计文档 的详细文档" +--- + # Phase 1: 硬件探针与能力分级详细设计文档 ## 文档信息 @@ -29,7 +34,7 @@ ### 2.1 整体架构图 -``` +```text ┌─────────────────────────────────────────────────────────┐ │ Application Layer │ │ (使用 HWTier 查询) │ @@ -54,11 +59,11 @@ │ Platform Abstraction (平台抽象) │ │ /proc/cpuinfo /sys/class/ /dev/dri/ evdev │ └─────────────────────────────────────────────────────────┘ -``` +```text ### 2.2 文件结构 -``` +```text src/base/ ├── include/CFDesktop/Base/HardwareProbe/ │ ├── HWTier.h # 档位枚举定义 @@ -79,7 +84,7 @@ src/base/ └── platform/ # 平台特定实现 ├── LinuxDetector.cpp └── WindowsDetector.cpp -``` +```yaml --- @@ -147,7 +152,7 @@ QString tierToString(HWTier tier); HWTier tierFromString(const QString& str); } // namespace CFDesktop::Base -``` +```text ### 3.2 HardwareInfo 结构体 @@ -226,7 +231,7 @@ struct HardwareInfo { }; } // namespace CFDesktop::Base -``` +```text ### 3.3 CapabilityPolicy 配置结构 @@ -282,7 +287,7 @@ struct MemoryPolicy { }; } // namespace CFDesktop::Base -``` +```yaml --- @@ -389,7 +394,7 @@ private: }; } // namespace CFDesktop::Base -``` +```text ### 4.2 CapabilityPolicy 策略引擎 @@ -481,7 +486,7 @@ private: }; } // namespace CFDesktop::Base -``` +```text ### 4.3 DeviceConfig 配置文件 @@ -542,7 +547,7 @@ public: }; } // namespace CFDesktop::Base -``` +```yaml --- @@ -622,7 +627,7 @@ QString CPUDetector::detectArchitecture(const QString& cpuInfoLine) { QString implementer = /* 从 cpuinfo 提取 */; return implementerMap.value(implementer, "Unknown"); } -``` +```text #### 5.1.2 Windows 平台 @@ -638,7 +643,7 @@ CPUInfo CPUDetector::detectCPUWindows() { return info; } -``` +```text ### 5.2 GPU 检测 @@ -688,7 +693,7 @@ GPUInfo GPUDetector::detectGPU() { return info; } -``` +```text ### 5.3 内存检测 @@ -717,7 +722,7 @@ MemoryInfo MemoryDetector::detectMemory() { return info; } -``` +```text ### 5.4 网络检测 @@ -757,7 +762,7 @@ QList NetworkDetector::detectNetwork() { return interfaces; } -``` +```text ### 5.5 档位计算逻辑 @@ -794,7 +799,7 @@ void HardwareProbe::calculateTier(HardwareInfo& info) { info.tier = calculatedTier; } -``` +```yaml --- @@ -838,7 +843,7 @@ VideoDecoder=auto [Logging] # 硬件检测日志级别 LogLevel=Info -``` +```text ### 6.2 自定义检测脚本示例 @@ -853,7 +858,7 @@ if [ -e /sys/bus/i2c/devices/0-0050 ]; then else echo '{"tier": "low"}' fi -``` +```yaml --- @@ -861,7 +866,7 @@ fi ### 7.1 Mock 数据目录结构 -``` +```text tests/mock/ └── proc/ ├── cpuinfo_imx6ull # IMX6ULL CPU 信息 @@ -873,7 +878,7 @@ tests/mock/ └── devices/ # 模拟设备文件 └── dri/ └── card0 # 模拟 GPU 设备 -``` +```text ### 7.2 测试用例清单 @@ -921,7 +926,7 @@ private slots: void testMalformedConfig(); void testTierOverride(); }; -``` +```text ### 7.3 示例测试用例 @@ -943,7 +948,7 @@ void TestHardwareProbe::testDetectCPU_IMX6ULL() { // 4. 验证档位 QCOMPARE(info.tier, HWTier::Low); } -``` +```bash --- diff --git a/document/design_stage/02_phase2_base_library.md b/document/design_stage/02_phase2_base_library.md index 9c83f49c0..f1a4b38d4 100644 --- a/document/design_stage/02_phase2_base_library.md +++ b/document/design_stage/02_phase2_base_library.md @@ -1,3 +1,8 @@ +--- +title: "Phase 2: Base 库核心详细设计文档" +description: "Phase 2: Base 库核心详细设计文档 的详细文档" +--- + # Phase 2: Base 库核心详细设计文档 ## 文档信息 @@ -30,7 +35,7 @@ ### 2.1 整体依赖关系 -``` +```text ┌──────────────────────────────────────────────────────────┐ │ Application Layer │ │ (Shell / Third-party Apps) │ @@ -48,11 +53,11 @@ │ Qt6 Framework │ │ Core / Gui / Widgets / Multimedia / Network │ └──────────────────────────────────────────────────────────┘ -``` +```text ### 2.2 Base 库目录结构 -``` +```text src/base/ ├── include/CFDesktop/Base/ │ ├── ThemeEngine/ @@ -111,7 +116,7 @@ src/base/ ├── FileSink.cpp ├── ConsoleSink.cpp └── NetworkSink.cpp -``` +```yaml --- @@ -119,7 +124,7 @@ src/base/ ### 3.1 主题包结构 -``` +```text assets/themes/ └── default/ ├── theme.json # 主题元数据 @@ -135,7 +140,7 @@ assets/themes/ ├── actions/ # 动作图标 ├── status/ # 状态图标 └── devices/ # 设备图标 -``` +```text ### 3.2 主题元数据格式 @@ -158,7 +163,7 @@ assets/themes/ "animationPolicy": "full" } } -``` +```text ### 3.3 颜色变量定义 @@ -186,7 +191,7 @@ assets/themes/ "divider": "#E0E0E0", "shadow": "rgba(0, 0, 0, 0.12)" } -``` +```text ### 3.4 QSS 模板语法 @@ -218,7 +223,7 @@ QPushButton:disabled { background-color: @divider; color: @text.disabled; } -``` +```text ### 3.5 ThemeEngine 类接口 @@ -342,7 +347,7 @@ private: }; } // namespace CFDesktop::Base -``` +```bash ### 3.6 主题降级策略 @@ -513,7 +518,7 @@ private: }; } // namespace CFDesktop::Base -``` +```text ### 4.2 预定义动画类型 @@ -553,7 +558,7 @@ enum class EasingType { QEasingCurve easingCurve(EasingType type); } // namespace CFDesktop::Base -``` +```yaml --- @@ -679,7 +684,7 @@ 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 计算公式 @@ -696,7 +701,7 @@ int DPIManager::sp(int value) const { qreal fontScale = QApplication::font().pointSizeF() / 10.0; return qRound(value * m_dpi / BASE_DPI * m_devicePixelRatio * fontScale); } -``` +```text ### 5.3 常用尺寸定义 @@ -724,7 +729,7 @@ namespace CFDesktop::Base::Sizes { inline int iconSizeMedium() { return dp(24); } inline int iconSizeLarge() { return dp(48); } } -``` +```yaml --- @@ -860,7 +865,7 @@ void setConfig(const QString& key, const T& value) { } } // namespace CFDesktop::Base -``` +```text ### 6.2 配置文件格式 @@ -887,7 +892,7 @@ UpdateCheck=true Level=Info MaxFileSizeMB=10 MaxFiles=5 -``` +```text **用户配置**: `~/.config/CFDesktop/config.conf` @@ -903,7 +908,7 @@ StatusBarPosition=top [User] Name=User AutoLogin=false -``` +```yaml --- @@ -1030,7 +1035,7 @@ private: #define LOG_FATAL(tag, msg) Logger::instance()->fatal(tag, msg) } // namespace CFDesktop::Base -``` +```text ### 7.2 LogMessage 结构 @@ -1054,7 +1059,7 @@ struct LogMessage { }; } // namespace CFDesktop::Base -``` +```text ### 7.3 LogSink 实现 @@ -1090,7 +1095,7 @@ private: }; } // namespace CFDesktop::Base -``` +```text #### ConsoleSink @@ -1118,7 +1123,7 @@ private: }; } // namespace CFDesktop::Base -``` +```text #### NetworkSink @@ -1145,7 +1150,7 @@ private: }; } // namespace CFDesktop::Base -``` +```text ### 7.4 日志配置 @@ -1171,7 +1176,7 @@ MaxFiles=5 Enabled=false Host=192.168.1.100 Port=514 -``` +```yaml --- diff --git a/document/design_stage/03_phase3_input_layer.md b/document/design_stage/03_phase3_input_layer.md index 875b85b14..2c52ea5b0 100644 --- a/document/design_stage/03_phase3_input_layer.md +++ b/document/design_stage/03_phase3_input_layer.md @@ -1,3 +1,8 @@ +--- +title: "Phase 3: 输入抽象层详细设计文档" +description: "完成 Phase 3 后,进入 Phase 4: Shell UI 主体。" +--- + # Phase 3: 输入抽象层详细设计文档 ## 文档信息 @@ -30,7 +35,7 @@ ### 2.1 整体架构图 -``` +```text ┌─────────────────────────────────────────────────────────────┐ │ Application Layer │ │ (Widgets receive events) │ @@ -54,11 +59,11 @@ │ Kernel / Driver Layer │ │ evdev, GPIO, I2C, SPI input drivers │ └─────────────────────────────────────────────────────────────┘ -``` +```text ### 2.2 文件结构 -``` +```text src/base/ ├── include/CFDesktop/Base/Input/ │ ├── InputManager.h # 输入管理器主类 @@ -83,7 +88,7 @@ src/base/ │ └── RotaryEncoder.cpp # 旋转编码器 └── simulator/ # 模拟器支持 └── SimulatedInput.cpp -``` +```yaml --- @@ -216,7 +221,7 @@ public: }; } // namespace CFDesktop::Base -``` +```yaml --- @@ -339,7 +344,7 @@ private: }; } // namespace CFDesktop::Base -``` +```yaml --- @@ -446,11 +451,11 @@ private: }; } // namespace CFDesktop::Base -``` +```text ### 5.2 触摸事件流程图 -``` +```text QTouchEvent │ ├─> TouchPress @@ -473,7 +478,7 @@ QTouchEvent ├─> 如果是第二次单击:双击 ├─> 如果定时器触发:长按 └─> 清理触摸点 -``` +```yaml --- @@ -581,7 +586,7 @@ private: }; } // namespace CFDesktop::Base -``` +```text ### 6.2 预定义按键映射 @@ -633,7 +638,7 @@ struct StandardKeyMapping { }; } // namespace CFDesktop::Base -``` +```yaml --- @@ -740,7 +745,7 @@ private: }; } // namespace CFDesktop::Base -``` +```yaml --- @@ -900,7 +905,7 @@ private: }; } // namespace CFDesktop::Base -``` +```yaml --- @@ -1034,7 +1039,7 @@ private: }; } // namespace CFDesktop::Base -``` +```text ### 9.2 焦点导航算法 @@ -1096,7 +1101,7 @@ bool FocusNavigator::isInDirection( return true; } } -``` +```yaml --- @@ -1184,7 +1189,7 @@ private: }; } // namespace CFDesktop::Base -``` +```text ### 10.2 GPIO 按键 @@ -1258,7 +1263,7 @@ private: }; } // namespace CFDesktop::Base -``` +```text ### 10.3 旋转编码器 @@ -1329,7 +1334,7 @@ private: }; } // namespace CFDesktop::Base -``` +```yaml --- @@ -1367,7 +1372,7 @@ struct SimulatedInputConfig { }; } // namespace CFDesktop::Base -``` +```yaml --- diff --git a/document/design_stage/04_phase6_simulator.md b/document/design_stage/04_phase6_simulator.md index 4a7bd005b..d14c87bfe 100644 --- a/document/design_stage/04_phase6_simulator.md +++ b/document/design_stage/04_phase6_simulator.md @@ -1,3 +1,8 @@ +--- +title: "Phase 6: 多平台模拟器详细设计文档" +description: "完成 Phase 6 后,进入 Phase 4: Shell UI 主体 或 Phase 5: SD" +--- + # Phase 6: 多平台模拟器详细设计文档 ## 文档信息 @@ -30,7 +35,7 @@ ### 2.1 整体架构图 -``` +```text ┌───────────────────────────────────────────────────────────────┐ │ Simulator Window │ │ ┌─────────────────────────────────────────────────────────┐ │ @@ -63,11 +68,11 @@ │ Shared Base & Shell │ │ (与真实设备完全相同的代码) │ └───────────────────────────────────────────────────────────────┘ -``` +```text ### 2.2 文件结构 -``` +```text src/simulator/ ├── include/CFDesktop/Simulator/ │ ├── SimulatorWindow.h # 模拟器主窗口 @@ -108,7 +113,7 @@ assets/simulator/ │ └── generic-1080p.json └── effects/ └── touch-ripple.png # 触摸涟漪效果 -``` +```yaml --- @@ -233,11 +238,11 @@ private: }; } // namespace CFDesktop::Simulator -``` +```text ### 3.2 窗口布局 -``` +```text ┌─────────────────────────────────────────────────────────────┐ │ Simulator Window │ ├─────────────────────────────────────────────────────────────┤ @@ -263,7 +268,7 @@ private: │ │ [Device▼] [800×480▼] [Low▼] [Touch:ON] [Screenshot] │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ -``` +```yaml --- @@ -346,7 +351,7 @@ struct DeviceProfile { }; } // namespace CFDesktop::Simulator -``` +```text ### 4.2 设备配置 JSON 示例 @@ -387,7 +392,7 @@ struct DeviceProfile { "description": "4.3 inch WVGA panel with IMX6ULL", "version": "1.0" } -``` +```bash ### 4.3 预设设备配置列表 @@ -466,7 +471,7 @@ private: }; } // namespace CFDesktop::Simulator -``` +```text ### 5.2 矢量绘制示例 @@ -504,7 +509,7 @@ void DeviceFrame::drawFrameVector(QPainter& painter) { drawHardwareButtons(painter); } } -``` +```yaml --- @@ -594,7 +599,7 @@ private: }; } // namespace CFDesktop::Simulator -``` +```yaml --- @@ -698,7 +703,7 @@ private: }; } // namespace CFDesktop::Simulator -``` +```text ### 7.2 硬件档位选择器 (HWTierSelector) @@ -738,7 +743,7 @@ private: }; } // namespace CFDesktop::Simulator -``` +```yaml --- @@ -786,7 +791,7 @@ private: }; } // namespace CFDesktop::Simulator -``` +```text ### 8.2 硬件 Mock @@ -832,7 +837,7 @@ private: }; } // namespace CFDesktop::Simulator -``` +```text ### 8.3 输入模拟器 @@ -883,7 +888,7 @@ private: }; } // namespace CFDesktop::Simulator -``` +```yaml --- @@ -996,7 +1001,7 @@ int main(int argc, char* argv[]) { return app.exec(); } -``` +```text ### 11.2 切换设备配置 @@ -1006,7 +1011,7 @@ simulator.loadDeviceProfile("/path/to/imx6ull-7.0.json"); // 或者通过控制面板选择 simulator.controlPanel()->setCurrentProfile("IMX6ULL 7.0 inch"); -``` +```yaml --- diff --git a/document/design_stage/05_phase8_testing.md b/document/design_stage/05_phase8_testing.md index 21f816648..ec488100f 100644 --- a/document/design_stage/05_phase8_testing.md +++ b/document/design_stage/05_phase8_testing.md @@ -1,3 +1,8 @@ +--- +title: "Phase 8: 测试体系详细设计文档" +description: "Phase 8: 测试体系详细设计文档 的详细文档" +--- + # Phase 8: 测试体系详细设计文档 ## 文档信息 @@ -30,7 +35,7 @@ ### 2.1 测试金字塔 -``` +```text /\ / \ / E2E \ @@ -51,11 +56,11 @@ / 20% 集成 \ / 10% UI \ /____________________________________\ -``` +```text ### 2.2 测试目录结构 -``` +```text tests/ ├── unit/ # 单元测试 │ ├── base/ @@ -110,7 +115,7 @@ tests/ │ └── test_theme.h │ └── CMakeLists.txt -``` +```yaml --- @@ -132,7 +137,7 @@ add_subdirectory(unit) add_subdirectory(integration) add_subdirectory(ui) add_subdirectory(performance) -``` +```text ### 3.2 基础测试模板 @@ -191,7 +196,7 @@ void TestHardwareProbe::testDetectCPU_IMX6ULL() { QTEST_MAIN(TestHardwareProbe) #include "test_hardware_probe.moc" -``` +```text ### 3.3 Mock 系统设计 @@ -232,7 +237,7 @@ public: }; } // namespace CFDesktop::Testing -``` +```bash ### 3.4 关键单元测试用例清单 @@ -325,7 +330,7 @@ void TestBaseIntegration::testFullBaseStack() { // 完整的 Base 库集成测试 // 模拟真实启动流程 } -``` +```text ### 4.2 测试应用框架 @@ -371,7 +376,7 @@ private: }; } // namespace CFDesktop::Testing -``` +```yaml --- @@ -431,7 +436,7 @@ void TestLauncherUI::testSwipeGesture() { QTest::qWait(500); QCOMPARE(m_launcher->currentPage(), 1); } -``` +```text ### 5.2 可访问性测试 @@ -456,7 +461,7 @@ void TestAccessibility::testScreenReader() { QCOMPARE(iface->role(), QAccessible::Role::PushButton); QVERIFY(!iface->text(QAccessible::Name).isEmpty()); } -``` +```yaml --- @@ -520,7 +525,7 @@ void BenchmarkThemeLoad::testVariableLookupPerformance() { qDebug() << "Average variable lookup time:" << avgTime * 1000 << "μs"; } -``` +```text ### 6.2 内存使用测试 @@ -569,7 +574,7 @@ qint64 TestMemoryUsage::getCurrentMemoryUsage() { } return -1; } -``` +```bash ### 6.3 性能基准 @@ -697,7 +702,7 @@ jobs: with: name: performance-results path: build/testing/performance/ -``` +```bash ### 7.2 代码覆盖率要求 @@ -720,7 +725,7 @@ jobs: **文件**: `tests/mock/proc/cpuinfo_imx6ull` -``` +```text processor : 0 model name : Freescale i.MX6 UltraLite 528 MHz BogoMIPS : 264.00 @@ -734,11 +739,11 @@ 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 @@ -749,7 +754,7 @@ Active: 131072 kB Inactive: 104857 kB SwapTotal: 0 kB SwapFree: 0 kB -``` +```text ### 8.2 测试夹具 @@ -790,7 +795,7 @@ inline Theme createDefaultTestTheme() { } } // namespace CFDesktop::Testing -``` +```bash --- @@ -884,7 +889,7 @@ void TestExample::cleanup() { // 每个测试用例后:清理资源 delete m_testObject; } -``` +```bash --- diff --git a/document/design_stage/README.md b/document/design_stage/README.md index bf94fc85a..5850f0e41 100644 --- a/document/design_stage/README.md +++ b/document/design_stage/README.md @@ -1,3 +1,8 @@ +--- +title: CFDesktop 前期阶段设计文档总览 +description: 本目录包含 CFDesktop 项目前期阶段的详细设计文档。 +--- + # CFDesktop 前期阶段设计文档总览 ## 文档目录 diff --git a/document/design_stage/index.md b/document/design_stage/index.md index e04552d54..81bf25b77 100644 --- a/document/design_stage/index.md +++ b/document/design_stage/index.md @@ -1,10 +1,11 @@ -# Design Stage - -> Welcome to the Design Stage section. +--- +title: 设计阶段文档 +description: 本目录包含 CFDesktop 项目各阶段的设计文档,覆盖 Phase 0 至 Phase 8 的完 +--- -## Overview +# 设计阶段文档 -Documentation and resources for Design Stage. +本目录包含 CFDesktop 项目各阶段的设计文档,覆盖 Phase 0 至 Phase 8 的完整规划。内容包括系统整体架构设计、多显示后端方案、硬件探测层(Hardware Probe)设计、基础库(Base Library)架构以及输入抽象层(Input Layer)设计等核心模块的技术方案。 --- diff --git a/document/design_stage/multi_display_backend_architecture.md b/document/design_stage/multi_display_backend_architecture.md index 26c3f5d94..54974034f 100644 --- a/document/design_stage/multi_display_backend_architecture.md +++ b/document/design_stage/multi_display_backend_architecture.md @@ -1,3 +1,8 @@ +--- +title: CFDesktop 多显示后端架构设计 +description: "最后更新: 2026-03-29,在 中增加:" +--- + # CFDesktop 多显示后端架构设计 > **状态**: 设计中 @@ -24,7 +29,7 @@ CFDesktop 需要在以下场景中运行: ## 二、系统角色模型 -``` +```text ┌─────────────────────────────────────────────────────┐ │ CFDesktop Shell │ │ (ShellLayer, PanelManager, WindowManager, IWindow) │ @@ -45,7 +50,7 @@ CFDesktop 需要在以下场景中运行: ├───────────────────────────────────────────────────────┤ │ 硬件层 (GPU / DRM / FB / Win32) │ └───────────────────────────────────────────────────────┘ -``` +```bash ### DisplayServerRole 枚举 @@ -80,7 +85,7 @@ signals: void externalWindowAppeared(WeakPtr window); void externalWindowDisappeared(WeakPtr window); }; -``` +```text **职责**: - 决定 CFDesktop 的运行角色(客户端 / 合成器 / 直接渲染) @@ -101,7 +106,7 @@ public: virtual QSize screenSize() const = 0; virtual void* nativeHandle() const = 0; // EGLDisplay, HWND 等 }; -``` +```text **职责**: - 抽象底层渲染硬件初始化 @@ -121,7 +126,7 @@ struct BackendCapabilities { bool supportsScreenshot = true; int maxTextureSize = 4096; }; -``` +```yaml --- @@ -132,7 +137,7 @@ struct BackendCapabilities { 在 `IWindowBackend` 中增加: ```cpp virtual BackendCapabilities capabilities() const = 0; -``` +```text 允许 WindowManager 和 Shell 在运行时查询后端能力,做出适配决策。 @@ -154,7 +159,7 @@ enum class DisplayServerMode { }; DisplayServerMode DetectDisplayServerMode(); -``` +```text `DetectDisplayServerMode()` 检测逻辑: 1. 检查环境变量 `CFDESKTOP_DISPLAY_SERVER` (强制覆盖) @@ -173,7 +178,7 @@ public: virtual void setStrategy(std::unique_ptr strategy) = 0; virtual QRect geometry() const = 0; }; -``` +```text `ShellLayer` 同时继承 `IShellLayer` 和 `QWidget`: ```cpp @@ -182,7 +187,7 @@ class ShellLayer : public QWidget, public IShellLayer { }; // Wayland 合成器可以实现 IShellLayer 而不继承 QWidget -``` +```bash --- @@ -261,13 +266,13 @@ if(CFDESKTOP_ENABLE_X11_WM) find_package(PkgConfig) pkg_check_modules(XCB OPTIONAL xcb xcb-composite xcb-ewmh) endif() -``` +```yaml --- ## 八、运行时后端选择流程 -``` +```text CFDesktop 启动 │ ▼ @@ -292,4 +297,4 @@ DetectDisplayServerMode() │ └── DirectRender 模式 (linuxfb) │ └── 默认: Client 模式 -``` +```text diff --git a/document/design_stage/system_architecture_overview.md b/document/design_stage/system_architecture_overview.md index 2e394eb4d..fe8e880f8 100644 --- a/document/design_stage/system_architecture_overview.md +++ b/document/design_stage/system_architecture_overview.md @@ -1,3 +1,8 @@ +--- +title: CFDesktop 系统架构总览 +description: "版本: 0.13.1,最后更新: 2026-03-30" +--- + # CFDesktop 系统架构总览 > **状态**: 已实现 @@ -33,7 +38,7 @@ ### 2.1 三层模块架构 -``` +```text ┌─────────────────────────────────────────────────────────┐ │ desktop 模块 │ │ 桌面环境实现 — Shell, Panel, 后端 │ @@ -46,7 +51,7 @@ ├─────────────────────────────────────────────────────────┤ │ Qt 6 / OS API │ └─────────────────────────────────────────────────────────┘ -``` +```bash **依赖规则**:`desktop` → `ui` → `base`,严格单向依赖。 @@ -79,7 +84,7 @@ ### 2.3 最终构建产物 -``` +```text CFDesktop.exe / CFDesktop ← 薄壳 main.cpp └── CFDesktop_shared.dll/.so ← 统一共享库(聚合所有静态库) ├── CFDesktopMain ← 初始化与启动 @@ -101,7 +106,7 @@ CFDesktop.exe / CFDesktop ← 薄壳 main.cpp ├── cffilesystem ├── cfconfig └── cfasciiart -``` +```yaml --- @@ -109,7 +114,7 @@ CFDesktop.exe / CFDesktop ← 薄壳 main.cpp ### 3.1 接口层次关系 -``` +```text ┌─────────────────────────────────────────────────────┐ │ IDisplayServerBackend │ │ (顶层抽象:角色、能力、生命周期、事件循环) │ @@ -124,7 +129,7 @@ CFDesktop.exe / CFDesktop ← 薄壳 main.cpp │ │ └──────────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────┘ -``` +```text ### 3.2 IDisplayServerBackend @@ -146,7 +151,7 @@ struct DisplayServerCapabilities { bool supportsWaylandProtocol; bool supportsX11Protocol; }; -``` +```bash **核心方法**: @@ -203,7 +208,7 @@ struct DisplayServerCapabilities { ### 3.5 信号流转 -``` +```text 外部窗口出现(OS 层事件) │ ▼ @@ -222,7 +227,7 @@ CFDesktopEntity::run_init() ← 日志记录 + Shell 分发 │ ▼ WindowManager / ShellLayer ← UI 响应 -``` +```yaml --- @@ -234,7 +239,7 @@ WindowManager / ShellLayer ← UI 响应 **Factory 模式** — 创建平台特定对象: -``` +```text ┌─────────────────────────────────────────────────┐ │ RegisteredFactory │ │ creator_: std::function │ @@ -249,23 +254,23 @@ WindowManager / ShellLayer ← UI 响应 │ 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(后端工厂) @@ -273,11 +278,11 @@ platform_helper.h ← 通用接口 display_backend_helper.h ← 显示后端专用接口 native_display() → 转发到 native_display_impl() native_display_impl() ← 每个平台实现此函数 -``` +```text **调用链**: -``` +```text CFDesktopEntity 构造 │ ├─ platform::native_display() @@ -286,7 +291,7 @@ CFDesktopEntity 构造 │ └─ WSL: new WSLDisplayServerBackend │ └─ DisplayServerBackendFactory::register_creator(creator, release) -``` +```bash ### 4.3 WSL 检测 @@ -320,7 +325,7 @@ CFDesktopEntity 构造 ### 5.2 Windows 后端数据流 -``` +```text ┌─────────────────────────────────────────────────────┐ │ WindowsDisplayServerBackend │ │ initialize(): │ @@ -349,11 +354,11 @@ CFDesktopEntity 构造 │ requestClose() → PostMessage(WM_CLOSE) │ │ raise() → SetForegroundWindow + BringWindowToTop│ └─────────────────────────────────────────────────────┘ -``` +```text ### 5.3 WSL/X11 后端数据流 -``` +```text ┌─────────────────────────────────────────────────────┐ │ WSLDisplayServerBackend │ │ initialize(): │ @@ -396,7 +401,7 @@ CFDesktopEntity 构造 │ → fallback: xcb_kill_client │ │ raise() → xcb_configure_window(ABOVE) │ └─────────────────────────────────────────────────────┘ -``` +```bash ### 5.4 未来后端规划 @@ -413,7 +418,7 @@ CFDesktopEntity 构造 UI 模块采用五层架构,从底层数学工具到顶层控件,逐层构建 Material Design 3 设计系统: -``` +```text ┌─────────────────────────────────────────────────┐ │ Layer 5: Widget 适配层 (cf_ui_widget) │ │ Button, TextField, Switch, TabView... │ @@ -437,7 +442,7 @@ UI 模块采用五层架构,从底层数学工具到顶层控件,逐层构 │ Color, Math, Geometry, Easing, DevicePixel │ │ (基础数学:HCT 色彩、easing 曲线、DPI) │ └─────────────────────────────────────────────────┘ -``` +```yaml ### 各层详情 @@ -476,7 +481,7 @@ UI 模块采用五层架构,从底层数学工具到顶层控件,逐层构 ### 7.1 完整启动流程 -``` +```cpp main(argc, argv) │ ├─ QApplication cf_desktop_app(argc, argv) ← Qt 应用创建 @@ -520,13 +525,13 @@ main(argc, argv) │ ├─ CFDesktopEntity::release() ← 清理(QApplication 存活时) └─ Logger::flush_sync() ← 刷日志 -``` +```text ### 7.2 CFDesktopEntity 生命周期 `CFDesktopEntity` 是桌面环境的中央单例,管理整个生命周期: -``` +```text 构造 ──────────────────────────────────────── 析构 │ │ ├─ CFDesktop* (Widget) ├─ stopTracking() @@ -536,7 +541,7 @@ main(argc, argv) │ └─ IWindow[] (窗口列表) │ └─ run_init() 初始化全部组件 -``` +```yaml **关键约束**:`CFDesktopEntity::release()` 必须在 `QApplication` 存活时调用,因为其内部持有 `QWidget` 实例。 @@ -561,7 +566,7 @@ add_subdirectory(desktop) # 3. 桌面实现(依赖 base + ui) # 辅助模块 add_subdirectory(example) # 示例应用 add_subdirectory(test) # 测试套件 -``` +```bash ### 8.2 条件编译 @@ -582,7 +587,7 @@ target_link_libraries(CFDesktop_shared PRIVATE ${CFDESKTOP_STATIC_LIBS} "-Wl,--no-whole-archive" ) -``` +```cmake 最终可执行文件 `CFDesktop` 仅包含 `main.cpp`,链接 `CFDesktop_shared`。 diff --git a/document/desktop/.pages b/document/desktop/.pages deleted file mode 100644 index 57f2ba82f..000000000 --- a/document/desktop/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: Desktop 模块 -icon: material/monitor -nav: - - 基础组件: base diff --git a/document/desktop/base/.pages b/document/desktop/base/.pages deleted file mode 100644 index 1e3e430e8..000000000 --- a/document/desktop/base/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: 基础组件 -nav: - - 配置管理器: config_manager - - 日志系统: logger diff --git a/document/desktop/base/config_manager/.pages b/document/desktop/base/config_manager/.pages deleted file mode 100644 index ab656f156..000000000 --- a/document/desktop/base/config_manager/.pages +++ /dev/null @@ -1,9 +0,0 @@ -title: 配置管理器 -nav: - - index.md - - API 类型: 02-api-types.md - - 查询 API: 03-api-query.md - - 写入 API: 04-api-write.md - - 监听 API: 05-api-watch.md - - 持久化 API: 06-api-persist.md - - 单例 API: 07-api-singleton.md diff --git a/document/desktop/base/config_manager/02-api-types.md b/document/desktop/base/config_manager/02-api-types.md deleted file mode 100644 index ede1e7d9a..000000000 --- a/document/desktop/base/config_manager/02-api-types.md +++ /dev/null @@ -1,363 +0,0 @@ -# ConfigStore API - 核心类型 - -本文档介绍 ConfigStore API 中使用的核心数据类型。 - -## 目录 - -- [Layer 枚举](#layer-枚举) -- [NotifyPolicy 枚举](#notifypolicy-枚举) -- [KeyView 结构体](#keyview-结构体) -- [Key 结构体](#key-结构体) -- [Watcher 回调类型](#watcher-回调类型) -- [辅助类型](#辅助类型) - ---- - -## Layer 枚举 - -配置存储层,定义配置的优先级和持久化策略。 - -### 定义 - -```cpp -namespace cf::config { -enum class Layer { System, User, App, Temp }; -} -``` - -### 层级说明 - -| 层级 | 优先级 | 持久化 | 说明 | -|------|--------|--------|------| -| `Temp` | 最高 | 否 | 临时层,仅内存存储,用于测试或临时配置变更 | -| `App` | 高 | 是 | 应用层,应用级别的配置设置 | -| `User` | 中 | 是 | 用户层,用户个人配置偏好 | -| `System` | 低 | 是 | 系统层,系统默认配置和回退值 | - -### 查询优先级 - -``` -查询顺序: Temp -> App -> User -> System - | | | | - v v v v - [最高] [高] [中] [低/默认] -``` - -### 使用示例 - -```cpp -using namespace cf::config; - -// 查询时按优先级自动查找 -std::string theme = ConfigStore::instance().query( - KeyView{.group = "app.theme", .key = "name"}, - "default" -); - -// 从特定层查询 -auto user_theme = ConfigStore::instance().query( - KeyView{.group = "app.theme", .key = "name"}, - Layer::User -); -if (user_theme.has_value()) { - // 使用 user_theme.value() -} - -// 写入指定层 -ConfigStore::instance().set( - KeyView{.group = "app.theme", .key = "name"}, - std::string("dark"), - Layer::App -); -``` - ---- - -## NotifyPolicy 枚举 - -通知策略,控制配置变更时是否立即触发监听回调。 - -### 定义 - -```cpp -namespace cf::config { -enum class NotifyPolicy { Manual, Immediate }; -} -``` - -### 策略说明 - -| 策略 | 行为 | 使用场景 | -|------|------|----------| -| `Manual` | 手动通知,变更不触发回调,需调用 `notify()` | 批量修改配置,避免多次触发 | -| `Immediate` | 立即通知,每次变更立即触发回调 | 需要即时响应配置变化 | - -### 使用示例 - -```cpp -using namespace cf::config; - -// 手动通知模式 - 批量修改 -ConfigStore::instance().set( - KeyView{.group = "app", .key = "theme"}, - std::string("dark"), - Layer::App, - NotifyPolicy::Manual -); - -ConfigStore::instance().set( - KeyView{.group = "app", .key = "language"}, - std::string("zh-CN"), - Layer::App, - NotifyPolicy::Manual -); - -// 统一触发通知 -ConfigStore::instance().notify(); - -// 立即通知模式 - 即时响应 -ConfigStore::instance().set( - KeyView{.group = "app", .key = "volume"}, - 80, - Layer::App, - NotifyPolicy::Immediate // 立即触发 Watcher 回调 -); -``` - ---- - -## KeyView 结构体 - -配置键的轻量级视图,用于运行时配置访问。 - -### 定义 - -```cpp -namespace cf::config { -struct KeyView { - std::string_view group; - std::string_view key; -}; -} -``` - -### 字段说明 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `group` | `std::string_view` | 配置分组,如 `"app.theme"` | -| `key` | `std::string_view` | 配置键名,如 `"name"` | - -### 组合规则 - -``` -KeyView{group = "app.theme", key = "name"} - ↓ - 完整键: "app.theme.name" -``` - -### 使用示例 - -```cpp -using namespace cf::config; - -// 查询配置 -KeyView theme_key{.group = "app.theme", .key = "name"}; -std::string theme = ConfigStore::instance().query( - theme_key, - "default" -); - -// 修改配置 -ConfigStore::instance().set( - KeyView{.group = "app.window", .key = "width"}, - 1920, - Layer::App -); - -// 检查键是否存在 -bool has = ConfigStore::instance().has_key( - KeyView{.group = "app", .key = "version"} -); -``` - ---- - -## Key 结构体 - -配置键的完整定义,用于键注册和持久化描述。 - -### 定义 - -```cpp -namespace cf::config { -struct Key { - std::string full_key; // 完整键路径 "app.theme.name" - std::string full_description; // 配置项描述,用于生成帮助文档 -}; -} -``` - -### 字段说明 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `full_key` | `std::string` | 完整键路径,由 `group.key` 组合而成 | -| `full_description` | `std::string` | 配置项的详细描述 | - -### 与 KeyView 的关系 - -``` -KeyView KeyHelper Key - ↓ ↓ ↓ -[group="a.b"] 转换器 [full_key="a.b.c"] -[key="c"] [description="..."] -``` - -### 使用示例 - -```cpp -using namespace cf::config; - -// 注册配置键 -Key theme_key{ - .full_key = "app.theme.name", - .full_description = "Application theme name (default, light, dark)" -}; - -ConfigStore::instance().register_key( - theme_key, - std::string("default"), - Layer::App -); - -// Watcher 回调中接收完整 Key -WatcherHandle handle = ConfigStore::instance().watch( - "app.theme.*", - [](const Key& k, const std::any* old_value, const std::any* new_value, Layer from_layer) { - // k.full_key 和 k.full_description 可用 - } -); -``` - ---- - -## Watcher 回调类型 - -配置变更监听器回调函数类型。 - -### 定义 - -```cpp -namespace cf::config { -using Watcher = std::function; -} -``` - -### 参数说明 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `k` | `const Key&` | 发生变更的配置键 | -| `old_value` | `const std::any*` | 旧值指针,新增键时为 `nullptr` | -| `new_value` | `const std::any*` | 新值指针,删除键时为 `nullptr` | -| `from_layer` | `Layer` | 变更发生的层级 | - -### 注意事项 - -- `old_value` 和 `new_value` 仅在回调执行期间有效 -- 回调中避免调用 `ConfigStore::set()` 以防止死锁 -- 使用 `std::any_cast` 转换值类型 - -### 使用示例 - -```cpp -using namespace cf::config; - -WatcherHandle handle = ConfigStore::instance().watch( - "app.theme.*", - [](const Key& k, const std::any* old_value, const std::any* new_value, Layer from_layer) { - if (new_value) { - std::string new_theme = std::any_cast(*new_value); - std::cout << "Theme changed to: " << new_theme << std::endl; - } - - if (old_value && new_value) { - std::string old = std::any_cast(*old_value); - std::string neu = std::any_cast(*new_value); - std::cout << "Updated: " << old << " -> " << neu << std::endl; - } - }, - NotifyPolicy::Immediate -); -``` - ---- - -## 辅助类型 - -### RegisterResult - -键注册结果。 - -```cpp -enum class RegisterResult { - KeyAlreadyIn = 0, // 键已存在 - KeyRegisteredSuccess = 1 // 注册成功 -}; -``` - -### UnRegisterResult - -键注销结果。 - -```cpp -enum class UnRegisterResult { - KeyUnexisted = 0, // 键不存在 - KeyUnRegisteredSuccess = 1 // 注销成功 -}; -``` - -### NotifyResult - -通知操作结果。 - -```cpp -enum class NotifyResult { - NotifyFailed = 0, // 通知失败(内部错误) - NothingWorthNotify = 1, // 无待通知的变更 - NotifySuccess = 2 // 通知成功 -}; -``` - -### WatcherHandle - -监听器句柄,用于取消监听。 - -```cpp -using WatcherHandle = std::size_t; -``` - -### SyncMethod - -同步方法。 - -```cpp -enum class SyncMethod { Sync, Async }; -``` - -| 值 | 说明 | -|------|------| -| `Sync` | 同步写入,阻塞直到完成 | -| `Async` | 异步写入,立即返回 | - ---- - -## 下一章 - -- [03-api-query.md](./03-api-query.md) - 查询操作 API diff --git a/document/desktop/base/config_manager/03-api-query.md b/document/desktop/base/config_manager/03-api-query.md deleted file mode 100644 index 36e146975..000000000 --- a/document/desktop/base/config_manager/03-api-query.md +++ /dev/null @@ -1,318 +0,0 @@ -# ConfigStore API - 查询操作 - -本文档介绍 ConfigStore 的配置查询 API。 - -## 目录 - -- [query() - 按优先级查询](#query---按优先级查询) -- [query() - 查询指定层](#query---查询指定层) -- [has_key() - 检查键存在性](#has_key---检查键存在性) - ---- - -## query() - 按优先级查询 - -从所有层中按优先级查询配置值。 - -### 函数签名 - -```cpp -template -[[nodiscard]] std::optional query(const KeyView key); - -template -[[nodiscard]] Value query(const KeyView key, const Value& default_value); -``` - -### 参数说明 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `key` | `KeyView` | 配置键视图 | -| `default_value` | `const Value&` | 默认值(仅重载2) | - -### 返回值 - -| 重载 | 返回类型 | 说明 | -|------|----------|------| -| 1 | `std::optional` | 找到返回值,未找到返回 `std::nullopt` | -| 2 | `Value` | 找到返回值,未找到返回 `default_value` | - -### 查询顺序 - -``` -查询流程: - KeyView: "app.theme.name" - ↓ - ┌─────────────────────────────┐ - │ 查找顺序: Temp → App → User │ - │ │ - │ [Temp] 命中? ──Yes──→ 返回 │ - │ │ No │ - │ v │ - │ [App] 命中? ──Yes──→ 返回 │ - │ │ No │ - │ v │ - │ [User] 命中? ──Yes──→ 返回 │ - │ │ No │ - │ v │ - │ [System] 命中? ──Yes──→ 返回│ - │ │ No │ - │ v │ - │ 返回 nullopt/default │ - └─────────────────────────────┘ -``` - -### 使用示例 - -```cpp -using namespace cf::config; - -// 方式1: 使用 optional 检查是否存在 -std::optional theme = ConfigStore::instance().query( - KeyView{.group = "app.theme", .key = "name"} -); - -if (theme.has_value()) { - std::cout << "Theme: " << theme.value() << std::endl; -} else { - std::cout << "Theme not configured" << std::endl; -} - -// 方式2: 使用默认值 -std::string theme = ConfigStore::instance().query( - KeyView{.group = "app.theme", .key = "name"}, - "default" // 未找到时返回此值 -); - -// 查询数值类型 -int width = ConfigStore::instance().query( - KeyView{.group = "app.window", .key = "width"}, - 1280 -); - -// 查询布尔类型 -bool fullscreen = ConfigStore::instance().query( - KeyView{.group = "app.window", .key = "fullscreen"}, - false -); -``` - ---- - -## query() - 查询指定层 - -从指定层直接查询配置值,不使用优先级合并。 - -### 函数签名 - -```cpp -template -[[nodiscard]] std::optional query(const KeyView key, Layer layer); - -template -[[nodiscard]] Value query(const KeyView key, Layer layer, const Value& default_value); -``` - -### 参数说明 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `key` | `KeyView` | 配置键视图 | -| `layer` | `Layer` | 目标层级 | -| `default_value` | `const Value&` | 默认值(仅重载2) | - -### 返回值 - -| 重载 | 返回类型 | 说明 | -|------|----------|------| -| 1 | `std::optional` | 找到返回值,未找到返回 `std::nullopt` | -| 2 | `Value` | 找到返回值,未找到返回 `default_value` | - -### 查询逻辑 - -``` -指定层查询: - KeyView: "app.theme.name", Layer: User - ↓ - ┌─────────────────────────────┐ - │ 仅在 User 层查找 │ - │ │ - │ [Temp] ── 跳过 │ - │ [App] ── 跳过 │ - │ [User] ── 查找 ──→ 命中/未命中│ - │ [System] ── 跳过 │ - └─────────────────────────────┘ -``` - -### 使用示例 - -```cpp -using namespace cf::config; - -// 查询用户层配置 -std::optional user_theme = ConfigStore::instance().query( - KeyView{.group = "app.theme", .key = "name"}, - Layer::User -); - -// 查询系统默认配置 -std::string system_lang = ConfigStore::instance().query( - KeyView{.group = "app", .key = "language"}, - Layer::System, - "en-US" -); - -// 检查临时层配置(测试场景) -std::optional temp_value = ConfigStore::instance().query( - KeyView{.group = "test", .key = "timeout"}, - Layer::Temp -); - -// 对比不同层的值 -std::string app_value = ConfigStore::instance().query( - KeyView{.group = "app.setting", .key = "mode"}, - Layer::App, - "app-default" -); - -std::string user_value = ConfigStore::instance().query( - KeyView{.group = "app.setting", .key = "mode"}, - Layer::User, - "user-default" -); - -std::cout << "App: " << app_value << ", User: " << user_value << std::endl; -``` - ---- - -## has_key() - 检查键存在性 - -检查配置键是否存在。 - -### 函数签名 - -```cpp -[[nodiscard]] bool has_key(const KeyView key); - -[[nodiscard]] bool has_key(const KeyView key, Layer layer); -``` - -### 参数说明 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `key` | `KeyView` | 配置键视图 | -| `layer` | `Layer` | 目标层级(仅重载2) | - -### 返回值 - -| 类型 | 说明 | -|------|------| -| `bool` | 键存在返回 `true`,否则返回 `false` | - -### 查询逻辑 - -``` -has_key(key): - 按优先级搜索所有层: Temp → App → User → System - 任一层命中即返回 true - -has_key(key, layer): - 仅在指定层查找 -``` - -### 使用示例 - -```cpp -using namespace cf::config; - -// 检查键是否存在(任意层) -bool has_theme = ConfigStore::instance().has_key( - KeyView{.group = "app.theme", .key = "name"} -); - -if (has_theme) { - // 安全地获取值 - std::string theme = ConfigStore::instance().query( - KeyView{.group = "app.theme", .key = "name"}, - "default" - ); -} - -// 检查特定层的键 -bool user_has_theme = ConfigStore::instance().has_key( - KeyView{.group = "app.theme", .key = "name"}, - Layer::User -); - -if (user_has_theme) { - std::cout << "User has customized theme" << std::endl; -} else { - std::cout << "Using default theme" << std::endl; -} - -// 条件配置初始化 -if (!ConfigStore::instance().has_key(KeyView{.group = "app", .key = "initialized"})) { - // 首次运行,初始化默认配置 - ConfigStore::instance().set( - KeyView{.group = "app", .key = "initialized"}, - true, - Layer::App - ); - setup_default_config(); -} -``` - ---- - -## 类型转换 - -`query()` 支持自动类型转换: - -| 存储类型 | 查询类型 | 转换 | -|----------|----------|------| -| `std::string` | `int` | 字符串转数字 | -| `std::string` | `double` | 字符串转浮点 | -| `std::string` | `bool` | 字符串转布尔 | -| `int` | `std::string` | 数字转字符串 | -| `double` | `std::string` | 浮点转字符串 | -| `bool` | `std::string` | 布尔转 "true"/"false" | -| `QVariant` | 基本类型 | QVariant 转换 | - -### 转换示例 - -```cpp -using namespace cf::config; - -// 存储为字符串,读取为整数 -ConfigStore::instance().set( - KeyView{.group = "app", .key = "width"}, - std::string("1920"), - Layer::App -); - -int width = ConfigStore::instance().query( - KeyView{.group = "app", .key = "width"}, - 1280 -); // width = 1920 - -// 存储为整数,读取为字符串 -ConfigStore::instance().set( - KeyView{.group = "app", .key = "count"}, - 42, - Layer::App -); - -std::string count = ConfigStore::instance().query( - KeyView{.group = "app", .key = "count"}, - "0" -); // count = "42" -``` - ---- - -## 下一章 - -- [04-api-write.md](./04-api-write.md) - 写入操作 API diff --git a/document/desktop/base/config_manager/04-api-write.md b/document/desktop/base/config_manager/04-api-write.md deleted file mode 100644 index b1d3a2537..000000000 --- a/document/desktop/base/config_manager/04-api-write.md +++ /dev/null @@ -1,429 +0,0 @@ -# ConfigStore API - 写入操作 - -本文档介绍 ConfigStore 的配置写入 API。 - -## 目录 - -- [set() - 设置配置值](#set---设置配置值) -- [register_key() - 注册配置键](#register_key---注册配置键) -- [unregister_key() - 注销配置键](#unregister_key---注销配置键) -- [clear() - 清空所有配置](#clear---清空所有配置) -- [clear_layer() - 清空指定层](#clear_layer---清空指定层) - ---- - -## set() - 设置配置值 - -设置配置键的值。 - -### 函数签名 - -```cpp -template -[[nodiscard]] bool set( - const KeyView key, - const Value& v, - Layer layer = Layer::App, - NotifyPolicy notify_policy = NotifyPolicy::Immediate -); -``` - -### 参数说明 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `key` | `KeyView` | 配置键视图 | -| `v` | `const Value&` | 要设置的值 | -| `layer` | `Layer` | 目标层级,默认 `Layer::App` | -| `notify_policy` | `NotifyPolicy` | 通知策略,默认 `Immediate` | - -### 返回值 - -| 类型 | 说明 | -|------|------| -| `bool` | 成功返回 `true`,失败返回 `false` | - -### 写入流程 - -``` -set() 执行流程: - KeyView + Value - ↓ - ┌─────────────────────────────┐ - │ 1. KeyView 转换为 Key │ - │ 2. 值转换为 std::any │ - │ 3. 写入指定 Layer │ - │ 4. 根据 NotifyPolicy 处理 │ - │ │ - │ Manual: 标记待通知 │ - │ Immediate: 立即触发 Watcher │ - └─────────────────────────────┘ - ↓ - 返回 bool -``` - -### 使用示例 - -```cpp -using namespace cf::config; - -// 基本使用 - 设置字符串 -bool success = ConfigStore::instance().set( - KeyView{.group = "app.theme", .key = "name"}, - std::string("dark"), - Layer::App -); - -// 设置数值 -ConfigStore::instance().set( - KeyView{.group = "app.window", .key = "width"}, - 1920, - Layer::App -); - -// 设置布尔值 -ConfigStore::instance().set( - KeyView{.group = "app.window", .key = "fullscreen"}, - true, - Layer::User -); - -// 写入不同层级 -ConfigStore::instance().set( - KeyView{.group = "test", .key = "timeout"}, - 5000, - Layer::Temp // 临时层,不持久化 -); - -// 使用手动通知策略 - 批量修改 -ConfigStore::instance().set( - KeyView{.group = "app", .key = "theme"}, - std::string("dark"), - Layer::App, - NotifyPolicy::Manual -); - -ConfigStore::instance().set( - KeyView{.group = "app", .key = "language"}, - std::string("zh-CN"), - Layer::App, - NotifyPolicy::Manual -); - -// 统一触发通知 -ConfigStore::instance().notify(); -``` - ---- - -## register_key() - 注册配置键 - -显式注册配置键,用于配置项的声明和文档生成。 - -### 函数签名 - -```cpp -template -[[nodiscard]] RegisterResult register_key( - const Key& key, - const Value& init_value, - Layer layer = Layer::App, - NotifyPolicy notify_policy = NotifyPolicy::Immediate -); -``` - -### 参数说明 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `key` | `const Key&` | 完整键定义(含描述) | -| `init_value` | `const Value&` | 初始值 | -| `layer` | `Layer` | 目标层级,默认 `Layer::App` | -| `notify_policy` | `NotifyPolicy` | 通知策略,默认 `Immediate` | - -### 返回值 - -| 值 | 说明 | -|------|------| -| `RegisterResult::KeyAlreadyIn` | 键已存在,注册失败 | -| `RegisterResult::KeyRegisteredSuccess` | 注册成功 | - -### 注册与 set 的区别 - -| 操作 | 键定义 | 描述 | 典型用途 | -|------|--------|------|----------| -| `register_key()` | 需要 `Key` (含描述) | 显式声明,带初始值 | 配置项初始化、文档生成 | -| `set()` | 仅需 `KeyView` | 直接设置值 | 运行时配置修改 | - -### 使用示例 - -```cpp -using namespace cf::config; - -// 注册主题配置 -Key theme_key{ - .full_key = "app.theme.name", - .full_description = "Application theme name (default, light, dark)" -}; - -auto result = ConfigStore::instance().register_key( - theme_key, - std::string("default"), - Layer::App -); - -if (result == RegisterResult::KeyRegisteredSuccess) { - std::cout << "Theme key registered" << std::endl; -} else { - std::cout << "Theme key already exists" << std::endl; -} - -// 注册窗口配置 -Key window_width{ - .full_key = "app.window.width", - .full_description = "Main window width in pixels" -}; - -ConfigStore::instance().register_key( - window_width, - 1280, - Layer::App, - NotifyPolicy::Manual -); - -// 注册系统默认配置 -Key system_lang{ - .full_key = "app.language", - .full_description = "System default language code" -}; - -ConfigStore::instance().register_key( - system_lang, - std::string("en-US"), - Layer::System -); - -// 注册到用户层(用户偏好) -Key user_font_size{ - .full_key = "app.ui.font_size", - .full_description = "User preferred font size" -}; - -ConfigStore::instance().register_key( - user_font_size, - 14, - Layer::User -); -``` - ---- - -## unregister_key() - 注销配置键 - -注销已注册的配置键。 - -### 函数签名 - -```cpp -[[nodiscard]] UnRegisterResult unregister_key( - const Key& key, - Layer layer = Layer::App, - NotifyPolicy notify_policy = NotifyPolicy::Immediate -); -``` - -### 参数说明 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `key` | `const Key&` | 要注销的键 | -| `layer` | `Layer` | 目标层级,默认 `Layer::App` | -| `notify_policy` | `NotifyPolicy` | 通知策略,默认 `Immediate` | - -### 返回值 - -| 值 | 说明 | -|------|------| -| `UnRegisterResult::KeyUnexisted` | 键不存在,注销失败 | -| `UnRegisterResult::KeyUnRegisteredSuccess` | 注销成功 | - -### 使用示例 - -```cpp -using namespace cf::config; - -// 注销配置键 -Key old_key{ - .full_key = "app.deprecated_feature", - .full_description = "Deprecated feature flag" -}; - -auto result = ConfigStore::instance().unregister_key( - old_key, - Layer::App -); - -if (result == UnRegisterResult::KeyUnRegisteredSuccess) { - std::cout << "Key unregistered" << std::endl; -} else { - std::cout << "Key not found" << std::endl; -} - -// 从特定层注销 -auto user_result = ConfigStore::instance().unregister_key( - Key{.full_key = "app.temp.setting", .full_description = ""}, - Layer::User, - NotifyPolicy::Manual -); -``` - ---- - -## clear() - 清空所有配置 - -清空所有层的所有配置。 - -### 函数签名 - -```cpp -void clear(); -``` - -### 警告 - -> 此操作不可逆,请谨慎使用。 - -### 使用示例 - -```cpp -using namespace cf::config; - -// 重置所有配置(测试场景) -ConfigStore::instance().clear(); - -// 重新初始化默认配置 -initialize_default_config(); -``` - ---- - -## clear_layer() - 清空指定层 - -清空指定层的所有配置。 - -### 函数签名 - -```cpp -void clear_layer(Layer layer); -``` - -### 参数说明 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `layer` | `Layer` | 要清空的层级 | - -### 使用示例 - -```cpp -using namespace cf::config; - -// 清空临时层(测试清理) -ConfigStore::instance().clear_layer(Layer::Temp); - -// 重置用户配置 -ConfigStore::instance().clear_layer(Layer::User); -// 用户登出时清除个人设置 - -// 清空应用层配置 -ConfigStore::instance().clear_layer(Layer::App); -// 应用重置为默认状态 -``` - -### 清空范围对比 - -``` -clear() 影响范围: - ┌───────────────────────────┐ - │ [Temp] ✓ 清空 │ - │ [App] ✓ 清空 │ - │ [User] ✓ 清空 │ - │ [System] ✓ 清空 │ - └───────────────────────────┘ - -clear_layer(Layer::App) 影响范围: - ┌───────────────────────────┐ - │ [Temp] × 保留 │ - │ [App] ✓ 清空 │ - │ [User] × 保留 │ - │ [System] × 保留 │ - └───────────────────────────┘ -``` - ---- - -## 写入操作与通知 - -### 通知策略对比 - -| 策略 | 行为 | 使用场景 | -|------|------|----------| -| `NotifyPolicy::Manual` | 变更不触发回调 | 批量修改、避免频繁触发 | -| `NotifyPolicy::Immediate` | 立即触发回调 | 需要即时响应 | - -### 批量写入模式 - -```cpp -using namespace cf::config; - -// 模式1: 使用 Manual 策略批量修改 -ConfigStore::instance().set( - KeyView{.group = "app", .key = "theme"}, - std::string("dark"), - Layer::App, - NotifyPolicy::Manual -); -ConfigStore::instance().set( - KeyView{.group = "app", .key = "language"}, - std::string("zh-CN"), - Layer::App, - NotifyPolicy::Manual -); -ConfigStore::instance().set( - KeyView{.group = "app.ui", .key = "font_size"}, - 14, - Layer::App, - NotifyPolicy::Manual -); -ConfigStore::instance().notify(); // 统一触发通知 - -// 模式2: 使用 Temp 层暂存,完成后移动到 App -ConfigStore::instance().set( - KeyView{.group = "app", .key = "theme"}, - std::string("dark"), - Layer::Temp -); -ConfigStore::instance().set( - KeyView{.group = "app", .key = "language"}, - std::string("zh-CN"), - Layer::Temp -); -// ... 验证配置 ... -ConfigStore::instance().set( - KeyView{.group = "app", .key = "theme"}, - std::string("dark"), - Layer::App -); -ConfigStore::instance().set( - KeyView{.group = "app", .key = "language"}, - std::string("zh-CN"), - Layer::App -); -ConfigStore::instance().clear_layer(Layer::Temp); -``` - ---- - -## 下一章 - -- [05-api-watch.md](./05-api-watch.md) - 监听操作 API diff --git a/document/desktop/base/config_manager/05-api-watch.md b/document/desktop/base/config_manager/05-api-watch.md deleted file mode 100644 index 3aa409e02..000000000 --- a/document/desktop/base/config_manager/05-api-watch.md +++ /dev/null @@ -1,426 +0,0 @@ -# ConfigStore API - 监听操作 - -本文档介绍 ConfigStore 的配置变更监听 API。 - -## 目录 - -- [watch() - 注册监听器](#watch---注册监听器) -- [unwatch() - 取消监听](#unwatch---取消监听) -- [notify() - 手动触发通知](#notify---手动触发通知) -- [pending_changes() - 待通知变更计数](#pending_changes---待通知变更计数) - ---- - -## watch() - 注册监听器 - -监听配置键的变更,当键值变化时触发回调。 - -### 函数签名 - -```cpp -WatcherHandle watch( - const std::string& key_pattern, - Watcher callback, - NotifyPolicy policy = NotifyPolicy::Immediate -); -``` - -### 参数说明 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `key_pattern` | `const std::string&` | 键模式,支持通配符 | -| `callback` | `Watcher` | 回调函数 | -| `policy` | `NotifyPolicy` | 通知策略,默认 `Immediate` | - -### 返回值 - -| 类型 | 说明 | -|------|------| -| `WatcherHandle` | 监听器句柄,用于后续取消监听 | - -### 键模式匹配 - -| 模式 | 匹配示例 | 说明 | -|------|----------|------| -| `"app.theme.name"` | 仅 `app.theme.name` | 精确匹配 | -| `"app.theme.*"` | `app.theme.name`, `app.theme.mode` | 单层通配符 | -| `"app.*.name"` | `app.theme.name`, `app.ui.name` | 单层通配符 | -| `"app.**"` | `app` 下所有键 | 递归通配符(如支持) | - -### Watcher 回调签名 - -```cpp -using Watcher = std::function; -``` - -### 回调参数含义 - -``` -变更场景 old_value new_value 说明 -────────────────────────────────────────────────────── -新增配置 nullptr 有效指针 新键创建 -修改配置 有效指针 有效指针 值更新 -删除配置 有效指针 nullptr 键移除 -``` - -### 使用示例 - -```cpp -using namespace cf::config; - -// 示例1: 监听单个键 -WatcherHandle h1 = ConfigStore::instance().watch( - "app.theme.name", - [](const Key& k, const std::any* old_val, const std::any* new_val, Layer layer) { - if (new_val) { - std::string theme = std::any_cast(*new_val); - std::cout << "Theme changed to: " << theme << std::endl; - apply_theme(theme); - } - } -); - -// 示例2: 监听一组键(通配符) -WatcherHandle h2 = ConfigStore::instance().watch( - "app.theme.*", - [](const Key& k, const std::any* old_val, const std::any* new_val, Layer layer) { - std::cout << "Theme config changed: " << k.full_key << std::endl; - reload_theme_config(); - } -); - -// 示例3: 处理新增/修改/删除 -WatcherHandle h3 = ConfigStore::instance().watch( - "app.window.*", - [](const Key& k, const std::any* old_val, const std::any* new_val, Layer layer) { - if (!old_val && new_val) { - std::cout << "New window setting: " << k.full_key << std::endl; - } else if (old_val && new_val) { - std::cout << "Window setting updated: " << k.full_key << std::endl; - } else if (old_val && !new_val) { - std::cout << "Window setting removed: " << k.full_key << std::endl; - } - } -); - -// 示例4: 使用 Manual 通知策略 -WatcherHandle h4 = ConfigStore::instance().watch( - "app.batch.*", - [](const Key& k, const std::any* old_val, const std::any* new_val, Layer layer) { - // 仅在 notify() 调用时触发 - std::cout << "Batch update completed for: " << k.full_key << std::endl; - }, - NotifyPolicy::Manual -); -``` - -### 注意事项 - -- 回调中避免调用 `ConfigStore::set()` 以防止死锁 -- `old_value` 和 `new_value` 仅在回调执行期间有效 -- 不要存储这些指针的引用 - ---- - -## unwatch() - 取消监听 - -取消已注册的配置变更监听器。 - -### 函数签名 - -```cpp -void unwatch(WatcherHandle handle); -``` - -### 参数说明 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `handle` | `WatcherHandle` | `watch()` 返回的句柄 | - -### 使用示例 - -```cpp -using namespace cf::config; - -// 注册监听 -WatcherHandle handle = ConfigStore::instance().watch( - "app.theme.*", - [](const Key& k, const std::any* old_val, const std::any* new_val, Layer layer) { - // 处理变更 - } -); - -// 不再需要监听时取消 -ConfigStore::instance().unwatch(handle); - -// RAII 封装示例 -class ThemeWatcher { - public: - ThemeWatcher() { - handle_ = ConfigStore::instance().watch("app.theme.*", callback); - } - ~ThemeWatcher() { - ConfigStore::instance().unwatch(handle_); - } - private: - WatcherHandle handle_; - Watcher callback = [](const Key& k, const std::any* old, const std::any* neu, Layer) { - // ... - }; -}; -``` - ---- - -## notify() - 手动触发通知 - -手动触发所有 `NotifyPolicy::Manual` 的监听器回调。 - -### 函数签名 - -```cpp -[[nodiscard]] NotifyResult notify(); -``` - -### 返回值 - -| 值 | 说明 | -|------|------| -| `NotifyResult::NotifyFailed` | 通知失败(内部错误) | -| `NotifyResult::NothingWorthNotify` | 无待通知的变更 | -| `NotifyResult::NotifySuccess` | 通知成功 | - -### 通知流程 - -``` -notify() 执行流程: - ┌─────────────────────────────┐ - │ 收集所有 Manual Watcher │ - │ 检查是否有待通知变更 │ - │ │ - │ 无变更 → NothingWorthNotify │ - │ 有变更 → 触发回调 │ - │ 成功 → NotifySuccess │ - │ 失败 → NotifyFailed │ - └─────────────────────────────┘ -``` - -### 使用示例 - -```cpp -using namespace cf::config; - -// 注册 Manual 模式监听器 -WatcherHandle handle = ConfigStore::instance().watch( - "app.settings.*", - [](const Key& k, const std::any* old_val, const std::any* new_val, Layer layer) { - std::cout << "Settings updated: " << k.full_key << std::endl; - refresh_settings_ui(); - }, - NotifyPolicy::Manual -); - -// 批量修改配置 -ConfigStore::instance().set( - KeyView{.group = "app.settings", .key = "theme"}, - "dark", Layer::App, NotifyPolicy::Manual -); -ConfigStore::instance().set( - KeyView{.group = "app.settings", .key = "lang"}, - "zh-CN", Layer::App, NotifyPolicy::Manual -); -ConfigStore::instance().set( - KeyView{.group = "app.settings", .key = "font_size"}, - 14, Layer::App, NotifyPolicy::Manual -); - -// 统一触发通知 -auto result = ConfigStore::instance().notify(); - -switch (result) { - case NotifyResult::NotifySuccess: - std::cout << "All watchers notified" << std::endl; - break; - case NotifyResult::NothingWorthNotify: - std::cout << "No changes to notify" << std::endl; - break; - case NotifyResult::NotifyFailed: - std::cerr << "Notification failed" << std::endl; - break; -} -``` - -### 与 Manual 策略配合使用 - -```cpp -using namespace cf::config; - -// 模式1: 批量更新后统一通知 -void update_multiple_settings(const Settings& new_settings) { - ConfigStore::instance().set( - KeyView{.group = "app.settings", .key = "theme"}, - new_settings.theme, Layer::App, NotifyPolicy::Manual - ); - ConfigStore::instance().set( - KeyView{.group = "app.settings", .key = "lang"}, - new_settings.lang, Layer::App, NotifyPolicy::Manual - ); - ConfigStore::instance().set( - KeyView{.group = "app.settings", .key = "font"}, - new_settings.font, Layer::App, NotifyPolicy::Manual - ); - ConfigStore::instance().notify(); // 触发一次回调 -} - -// 模式2: 延迟通知 -void schedule_later_update() { - ConfigStore::instance().set( - KeyView{.group = "app", .key = "pending"}, - true, Layer::App, NotifyPolicy::Manual - ); - // ... 其他操作 ... -} - -void commit_updates() { - ConfigStore::instance().notify(); // 提交变更 -} -``` - ---- - -## pending_changes() - 待通知变更计数 - -获取当前待通知的变更数量(用于诊断)。 - -### 函数签名 - -```cpp -[[nodiscard]] std::size_t pending_changes() const; -``` - -### 返回值 - -| 类型 | 说明 | -|------|------| -| `std::size_t` | 待通知的变更数量 | - -### 使用示例 - -```cpp -using namespace cf::config; - -// 检查是否有待处理变更 -std::size_t count = ConfigStore::instance().pending_changes(); - -if (count > 0) { - std::cout << "There are " << count << " pending changes" << std::endl; - ConfigStore::instance().notify(); -} - -// 调试输出 -void debug_config_state() { - std::cout << "Pending changes: " << ConfigStore::instance().pending_changes() << std::endl; -} - -// 确保所有变更已通知 -void ensure_notified() { - while (ConfigStore::instance().pending_changes() > 0) { - ConfigStore::instance().notify(); - } -} -``` - ---- - -## 监听器使用模式 - -### 模式1: 实时响应(Immediate) - -```cpp -// 配置变更立即生效 -ConfigStore::instance().watch( - "app.volume", - [](const Key& k, const std::any* old, const std::any* neu, Layer) { - if (neu) { - int volume = std::any_cast(*neu); - set_system_volume(volume); - } - }, - NotifyPolicy::Immediate -); - -// 设置音量时立即生效 -ConfigStore::instance().set( - KeyView{.group = "app", .key = "volume"}, - 80, - Layer::App, - NotifyPolicy::Immediate // 立即触发回调 -); -``` - -### 模式2: 批量处理(Manual) - -```cpp -// 多项配置统一更新 -ConfigStore::instance().watch( - "app.ui.*", - [](const Key& k, const std::any* old, const std::any* neu, Layer) { - // 仅在 notify() 时调用一次 - refresh_ui(); - }, - NotifyPolicy::Manual -); - -// 批量修改 -ConfigStore::instance().set( - KeyView{.group = "app.ui", .key = "theme"}, - "dark", Layer::App, NotifyPolicy::Manual -); -ConfigStore::instance().set( - KeyView{.group = "app.ui", .key = "font"}, - "Arial", Layer::App, NotifyPolicy::Manual -); -ConfigStore::instance().set( - KeyView{.group = "app.ui", .key = "size"}, - 14, Layer::App, NotifyPolicy::Manual -); -ConfigStore::instance().notify(); // 统一触发 -``` - -### 模式3: 条件监听 - -```cpp -// 根据条件启用/禁用监听 -class ConditionalWatcher { - public: - void enable() { - if (!handle_) { - handle_ = ConfigStore::instance().watch("app.*", callback_); - } - } - void disable() { - if (handle_) { - ConfigStore::instance().unwatch(*handle_); - handle_ = std::nullopt; - } - } - private: - std::optional handle_; - Watcher callback_ = [](const Key&, const std::any*, const std::any*, Layer) { - // 处理变更 - }; -}; -``` - ---- - -## 下一章 - -- [06-api-persist.md](./06-api-persist.md) - 持久化操作 API diff --git a/document/desktop/base/config_manager/06-api-persist.md b/document/desktop/base/config_manager/06-api-persist.md deleted file mode 100644 index a5b8f519a..000000000 --- a/document/desktop/base/config_manager/06-api-persist.md +++ /dev/null @@ -1,322 +0,0 @@ -# ConfigStore API - 持久化操作 - -本文档介绍 ConfigStore 的配置持久化 API。 - -## 目录 - -- [sync() - 同步到磁盘](#sync---同步到磁盘) -- [reload() - 从磁盘重载](#reload---从磁盘重载) - ---- - -## sync() - 同步到磁盘 - -将配置变更写入磁盘持久化存储。 - -### 函数签名 - -```cpp -void sync(const SyncMethod m = SyncMethod::Async); -``` - -### 参数说明 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `m` | `SyncMethod` | 同步方式,默认 `Async` | - -### SyncMethod 枚举 - -```cpp -enum class SyncMethod { Sync, Async }; -``` - -| 值 | 说明 | 阻塞 | -|------|------|------| -| `Sync` | 同步写入,等待完成 | 是 | -| `Async` | 异步写入,立即返回 | 否 | - -### 同步行为 - -``` -sync() 执行流程: - ┌─────────────────────────────┐ - │ 检查各层是否有变更 │ - │ 仅写入有变更的层 │ - │ │ - │ [Temp] ── 跳过(不持久化) │ - │ [App] ── 写入 app.conf │ - │ [User] ── 写入 user.conf │ - │ [System] ── 写入 sys.conf │ - └─────────────────────────────┘ - ↓ - Sync: 阻塞直到写入完成 - Async: 立即返回,后台写入 -``` - -### 使用示例 - -```cpp -using namespace cf::config; - -// 异步同步(默认) -ConfigStore::instance().set( - KeyView{.group = "app", .key = "theme"}, - std::string("dark"), - Layer::App -); -ConfigStore::instance().sync(); // 默认 Async,立即返回 - -// 同步同步(确保写入完成) -std::string sensitive_value = "critical_data_value"; -ConfigStore::instance().set( - KeyView{.group = "app", .key = "critical_data"}, - sensitive_value, - Layer::User -); -ConfigStore::instance().sync(SyncMethod::Sync); // 等待写入完成 -std::cout << "Data safely stored" << std::endl; - -// 关闭前同步 -void on_app_shutdown() { - ConfigStore::instance().sync(SyncMethod::Sync); // 确保数据保存 -} - -// 定期自动同步 -void periodic_sync() { - ConfigStore::instance().sync(SyncMethod::Async); -} -``` - -### Sync vs Async - -| 场景 | 推荐方式 | 原因 | -|------|----------|------| -| 普通配置变更 | `Async` | 不阻塞 UI,性能更好 | -| 关键数据保存 | `Sync` | 确保写入成功 | -| 应用退出 | `Sync` | 防止数据丢失 | -| 批量修改后 | `Async` 或 `Sync` | 根据重要性选择 | - ---- - -## reload() - 从磁盘重载 - -从磁盘重新加载所有配置文件,清除内存缓存。 - -### 函数签名 - -```cpp -void reload(); -``` - -### 重载行为 - -``` -reload() 执行流程: - ┌─────────────────────────────┐ - │ 1. 清除内存缓存 │ - │ 2. 丢弃 Temp 层所有数据 │ - │ 3. 重新读取各层配置文件 │ - │ │ - │ [Temp] ── 清空 │ - │ [App] ── 重新加载 │ - │ [User] ── 重新加载 │ - │ [System] ── 重新加载 │ - └─────────────────────────────┘ -``` - -### 影响范围 - -| 层级 | 重载行为 | -|------|----------| -| `Temp` | **完全清空**,所有临时配置丢失 | -| `App` | 从文件重新加载 | -| `User` | 从文件重新加载 | -| `System` | 从文件重新加载 | - -### 使用示例 - -```cpp -using namespace cf::config; - -// 基本使用 - 重载所有配置 -ConfigStore::instance().reload(); - -// 场景1: 检测到外部配置文件修改 -void on_config_file_changed(const std::string& path) { - std::cout << "Config file modified: " << path << std::endl; - ConfigStore::instance().reload(); - apply_config_to_ui(); -} - -// 场景2: 用户切换 -void on_user_switch(User new_user) { - // 切换用户配置文件路径 - update_config_path_for_user(new_user); - // 重载配置 - ConfigStore::instance().reload(); -} - -// 场景3: 恢复默认设置 -void restore_defaults() { - // 删除用户配置文件 - delete_user_config_file(); - // 重载(将使用系统默认) - ConfigStore::instance().reload(); -} - -// 场景4: 测试后清理 -void cleanup_test_config() { - // 清除测试期间设置的临时配置 - ConfigStore::instance().reload(); -} -``` - -### 注意事项 - -- `reload()` 会**丢弃** `Temp` 层的所有数据 -- 内存中未 `sync()` 的变更将丢失 -- 重载后已注册的 `Watcher` 仍然有效 - ---- - -## 持久化存储路径 - -配置文件由 `IConfigStorePathProvider` 提供,默认路径如下: - -``` -默认存储路径: - System: /etc/cfdesktop/config/system.conf - User: ~/.config/cfdesktop/user.conf - App: ~/.config/cfdesktop/app.conf - Temp: (不持久化,仅内存) -``` - -### 自定义路径 - -```cpp -// 使用自定义路径提供者初始化 -auto custom_provider = std::make_shared("/custom/path"); -ConfigStore::instance().initialize(custom_provider); -``` - ---- - -## 持久化操作模式 - -### 模式1: 自动持久化 - -```cpp -// 每次修改后异步同步 -void set_config_and_sync(const KeyView& key, const auto& value) { - ConfigStore::instance().set(key, value, Layer::App); - ConfigStore::instance().sync(SyncMethod::Async); -} -``` - -### 模式2: 延迟持久化 - -```cpp -// 收集变更,定期同步 -std::chrono::steady_clock::time_point last_sync; - -void set_config_lazy(const KeyView& key, const auto& value) { - ConfigStore::instance().set(key, value, Layer::App); - - auto now = std::chrono::steady_clock::now(); - if (now - last_sync > std::chrono::seconds(30)) { - ConfigStore::instance().sync(SyncMethod::Async); - last_sync = now; - } -} -``` - -### 模式3: 关键数据立即持久化 - -```cpp -// 关键配置使用同步写入 -void save_critical_config(const KeyView& key, const auto& value) { - ConfigStore::instance().set(key, value, Layer::User); - ConfigStore::instance().sync(SyncMethod::Sync); // 确保写入 -} -``` - -### 模式4: 事务式保存 - -```cpp -// 假设 ConfigChange 是用户定义的包含 KeyView 和值的类型 -// struct ConfigChange { -// KeyView key; -// std::any value; -// }; - -// 批量修改后统一同步 -void update_config_batch(const std::vector& changes) { - // 1. 备份当前配置 - backup_current_config(); - - // 2. 批量修改 - for (const auto& change : changes) { - ConfigStore::instance().set( - change.key, - change.value, - Layer::App, - NotifyPolicy::Manual - ); - } - - // 3. 同步保存 - ConfigStore::instance().sync(SyncMethod::Sync); - - // 4. 触发通知 - ConfigStore::instance().notify(); -} -``` - ---- - -## 完整生命周期示例 - -```cpp -using namespace cf::config; - -// 应用启动 -void app_startup() { - // 1. 初始化(自动加载配置) - ConfigStore::instance().initialize(custom_path_provider); - - // 2. 注册监听器 - ConfigStore::instance().watch("app.*", config_change_handler); - - // 3. 应用配置 - apply_config_to_ui(); -} - -// 应用运行 -void app_runtime() { - // 修改配置 - ConfigStore::instance().set( - KeyView{.group = "app", .key = "theme"}, - std::string("dark"), - Layer::App - ); - - // 异步同步 - ConfigStore::instance().sync(SyncMethod::Async); -} - -// 应用关闭 -void app_shutdown() { - // 1. 同步保存(确保数据不丢失) - ConfigStore::instance().sync(SyncMethod::Sync); - - // 2. 取消监听 - // (RAII 自动处理或手动 unwatch) -} -``` - ---- - -## 下一章 - -- [07-api-singleton.md](./07-api-singleton.md) - 单例访问 API diff --git a/document/desktop/base/config_manager/07-api-singleton.md b/document/desktop/base/config_manager/07-api-singleton.md deleted file mode 100644 index ef8e277b5..000000000 --- a/document/desktop/base/config_manager/07-api-singleton.md +++ /dev/null @@ -1,482 +0,0 @@ -# ConfigStore API - 单例访问 - -本文档介绍 ConfigStore 的单例访问和初始化 API。 - -## 目录 - -- [instance() - 获取单例](#instance---获取单例) -- [initialize() - 初始化](#initialize---初始化) -- [使用模式](#使用模式) - ---- - -## instance() - 获取单例 - -获取 ConfigStore 的全局唯一实例。 - -### 函数签名 - -```cpp -static ConfigStore& instance(); -``` - -### 返回值 - -| 类型 | 说明 | -|------|------| -| `ConfigStore&` | ConfigStore 的引用 | - -### 单例模式 - -``` -ConfigStore 单例模式: - ┌─────────────────────────────┐ - │ ConfigStore │ - │ (单例实例) │ - │ │ - │ instance() ──→ 引用 │ - └─────────────────────────────┘ - ▲ - │ - 所有访问点通过 instance() -``` - -### 使用示例 - -```cpp -using namespace cf::config; - -// 基本访问 -ConfigStore& store = ConfigStore::instance(); - -// 链式调用 -std::string theme = ConfigStore::instance().query( - KeyView{.group = "app.theme", .key = "name"}, - "default" -); - -// 直接调用方法 -ConfigStore::instance().set( - KeyView{.group = "app", .key = "volume"}, - 80, - Layer::App -); - -// 同步 -ConfigStore::instance().sync(SyncMethod::Async); -``` - ---- - -## initialize() - 初始化 - -使用自定义路径提供者初始化 ConfigStore。 - -### 函数签名 - -```cpp -void initialize(std::shared_ptr path_provider); -``` - -### 参数说明 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `path_provider` | `std::shared_ptr` | 路径提供者 | - -### 初始化行为 - -``` -initialize() 执行流程: - ┌─────────────────────────────┐ - │ 首次调用: │ - │ 1. 保存路径提供者 │ - │ 2. 创建存储层级 │ - │ 3. 从磁盘加载配置文件 │ - │ │ - │ 后续调用: │ - │ ── 忽略(不产生效果) │ - └─────────────────────────────┘ -``` - -### 重要特性 - -- **仅首次生效**:第一次调用后的所有调用会被忽略 -- **线程安全**:多线程环境下安全 -- **可选**:不调用时使用默认路径 - -### 使用示例 - -```cpp -using namespace cf::config; - -// 场景1: 使用默认路径 -// 无需调用 initialize(),直接使用 instance() -auto& store = ConfigStore::instance(); - -// 场景2: 自定义路径(测试环境) -class TestPathProvider : public IConfigStorePathProvider { - public: - std::filesystem::path system_config() const override { - return "/tmp/test_config/system.conf"; - } - std::filesystem::path user_config() const override { - return "/tmp/test_config/user.conf"; - } - std::filesystem::path app_config() const override { - return "/tmp/test_config/app.conf"; - } -}; - -void setup_test_environment() { - auto test_provider = std::make_shared(); - ConfigStore::instance().initialize(test_provider); -} - -// 场景3: 便携式应用路径 -class PortablePathProvider : public IConfigStorePathProvider { - public: - PortablePathProvider(const std::filesystem::path& base) : base_(base) {} - - std::filesystem::path system_config() const override { - return base_ / "default.conf"; - } - std::filesystem::path user_config() const override { - return base_ / "user.conf"; - } - std::filesystem::path app_config() const override { - return base_ / "app.conf"; - } - private: - std::filesystem::path base_; -}; - -void setup_portable_mode(const std::filesystem::path& app_dir) { - auto portable_provider = std::make_shared(app_dir); - ConfigStore::instance().initialize(portable_provider); -} -``` - ---- - -## IConfigStorePathProvider 接口 - -路径提供者接口,用于自定义配置文件存储位置。 - -### 接口定义 - -```cpp -class IConfigStorePathProvider { - public: - virtual ~IConfigStorePathProvider() = default; - - // 系统默认配置路径 - virtual std::filesystem::path system_config() const = 0; - - // 用户配置路径 - virtual std::filesystem::path user_config() const = 0; - - // 应用配置路径 - virtual std::filesystem::path app_config() const = 0; -}; -``` - -### 实现示例 - -```cpp -// Linux 标准路径实现 -class LinuxPathProvider : public IConfigStorePathProvider { - public: - std::filesystem::path system_config() const override { - return "/etc/cfdesktop/config/system.conf"; - } - - std::filesystem::path user_config() const override { - if (const char* home = std::getenv("HOME")) { - return std::filesystem::path(home) / ".config/cfdesktop/user.conf"; - } - return ".config/cfdesktop/user.conf"; - } - - std::filesystem::path app_config() const override { - if (const char* home = std::getenv("HOME")) { - return std::filesystem::path(home) / ".config/cfdesktop/app.conf"; - } - return ".config/cfdesktop/app.conf"; - } -}; - -// Windows 实现略... -// macOS 实现略... -``` - ---- - -## 使用模式 - -### 模式1: 默认初始化 - -```cpp -using namespace cf::config; - -// 应用启动 -int main() { - // 直接使用,自动初始化为默认路径 - ConfigStore& store = ConfigStore::instance(); - - // 正常使用 - std::string theme = store.query( - KeyView{.group = "app.theme", .key = "name"}, - "default" - ); - - return run_app(); -} -``` - -### 模式2: 提前初始化 - -```cpp -using namespace cf::config; - -// 应用启动时显式初始化 -int main(int argc, char* argv[]) { - // 解析命令行参数 - std::string config_dir = parse_config_dir(argc, argv); - - // 创建路径提供者 - auto provider = std::make_shared(config_dir); - - // 初始化 - ConfigStore::instance().initialize(provider); - - // 后续使用 - return run_app(); -} -``` - -### 模式3: 测试环境初始化 - -```cpp -using namespace cf::config; - -class ConfigTest : public ::testing::Test { - protected: - void SetUp() override { - // 每个测试使用独立的临时目录 - test_dir_ = create_temp_dir(); - auto test_provider = std::make_shared(test_dir_); - - // 重置并初始化(注意:仅在首次有效) - ConfigStore::instance().initialize(test_provider); - } - - void TearDown() override { - // 清理临时目录 - cleanup_temp_dir(test_dir_); - } - - std::string test_dir_; -}; - -TEST_F(ConfigTest, BasicOperations) { - // 使用干净的配置存储进行测试 - ConfigStore::instance().set( - KeyView{.group = "test", .key = "value"}, - 42, - Layer::App - ); - - int result = ConfigStore::instance().query( - KeyView{.group = "test", .key = "value"}, - 0 - ); - - EXPECT_EQ(result, 42); -} -``` - -### 模式4: 延迟初始化 - -```cpp -using namespace cf::config; - -class ConfigManager { - public: - static ConfigManager& get() { - static ConfigManager instance; - return instance; - } - - void init(const std::string& config_path) { - auto provider = std::make_shared(config_path); - ConfigStore::instance().initialize(provider); - initialized_ = true; - } - - ConfigStore& store() { - if (!initialized_) { - throw std::runtime_error("ConfigManager not initialized"); - } - return ConfigStore::instance(); - } - private: - ConfigManager() = default; - bool initialized_ = false; -}; - -// 使用 -int main() { - ConfigManager::get().init("/custom/path"); - auto& store = ConfigManager::get().store(); - // ... -} -``` - ---- - -## 初始化时序 - -``` -推荐时序: - ┌─────────────────────────────────────────────────────┐ - │ │ - │ 1. main() 启动 │ - │ ↓ │ - │ 2. 解析命令行/确定配置路径 │ - │ ↓ │ - │ 3. ConfigStore::instance().initialize(provider) │ - │ ↓ │ - │ 4. ConfigStore::instance().query/set/... │ - │ │ - └─────────────────────────────────────────────────────┘ - -错误时序: - ┌─────────────────────────────────────────────────────┐ - │ │ - │ 1. ConfigStore::instance().query() ──→ 使用默认路径│ - │ ↓ │ - │ 2. ConfigStore::instance().initialize(provider) │ - │ ↓ │ - │ 3. ❌ 初始化被忽略,配置仍在默认路径 │ - │ │ - └─────────────────────────────────────────────────────┘ -``` - ---- - -## 完整应用示例 - -```cpp -using namespace cf::config; - -class Application { - public: - int run(int argc, char* argv[]) { - // 1. 解析参数 - if (!parse_args(argc, argv)) { - return 1; - } - - // 2. 初始化配置 - init_config(); - - // 3. 注册监听器 - setup_watchers(); - - // 4. 加载 UI - create_ui(); - - // 5. 运行事件循环 - return event_loop_.exec(); - } - - private: - void init_config() { - std::shared_ptr provider; - - if (portable_mode_) { - // 便携模式:使用程序目录 - provider = std::make_shared( - get_app_dir() - ); - } else { - // 标准模式:使用系统路径 - provider = std::make_shared(); - } - - ConfigStore::instance().initialize(provider); - - // 确保基础配置存在 - ensure_default_config(); - } - - void setup_watchers() { - ConfigStore::instance().watch( - "app.theme.*", - [this](const Key& k, const std::any* old, const std::any* neu, Layer) { - this->on_theme_changed(k, old, neu); - } - ); - } - - void ensure_default_config() { - // 检查是否首次运行 - if (!ConfigStore::instance().has_key( - KeyView{.group = "app", .key = "initialized"})) { - setup_default_config(); - ConfigStore::instance().set( - KeyView{.group = "app", .key = "initialized"}, - true, - Layer::App - ); - ConfigStore::instance().sync(SyncMethod::Sync); - } - } - - bool portable_mode_ = false; - EventLoop event_loop_; -}; - -int main(int argc, char* argv[]) { - Application app; - return app.run(argc, argv); -} -``` - ---- - -## API 参考 - -### instance() - -```cpp -static ConfigStore& instance(); -``` - -| 项目 | 说明 | -|------|------| -| 功能 | 获取 ConfigStore 单例引用 | -| 返回值 | ConfigStore& | -| 线程安全 | 是 | -| 首次调用 | 自动使用默认路径初始化 | - -### initialize() - -```cpp -void initialize(std::shared_ptr path_provider); -``` - -| 项目 | 说明 | -|------|------| -| 功能 | 设置自定义配置文件路径 | -| 参数 | path_provider - 路径提供者 | -| 首次调用 | 设置路径并加载配置 | -| 后续调用 | 忽略,不产生效果 | -| 线程安全 | 是 | - ---- - -## 下一篇 - -- [返回 Config Manager](./) diff --git a/document/desktop/base/config_manager/README.md b/document/desktop/base/config_manager/README.md deleted file mode 100644 index e6d81415a..000000000 --- a/document/desktop/base/config_manager/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# ConfigStore API 参考手册 - -完整的 ConfigStore API 参考。 - -## 目录 - -### 类型定义 -| 文档 | 描述 | -|------|------| -| [核心类型](./02-api-types.md) | Layer、NotifyPolicy、KeyView、Key、Watcher 等类型定义 | - -### API 分类 -| 文档 | 描述 | -|------|------| -| [单例访问](./07-api-singleton.md) | instance()、initialize() | -| [查询操作](./03-api-query.md) | query()、has_key() | -| [写入操作](./04-api-write.md) | set()、register_key()、unregister_key()、clear() | -| [监听操作](./05-api-watch.md) | watch()、unwatch()、notify() | -| [持久化操作](./06-api-persist.md) | sync()、reload() | - -## 相关文档 - -- [使用手册](../../HandBook/desktop/base/config_manager/) - 快速入门和最佳实践 diff --git a/document/desktop/base/config_manager/index.md b/document/desktop/base/config_manager/index.md deleted file mode 100644 index f9752bbf6..000000000 --- a/document/desktop/base/config_manager/index.md +++ /dev/null @@ -1,11 +0,0 @@ -# Config Manager - -> Welcome to the Config Manager section. - -## Overview - -Documentation and resources for Config Manager. - ---- - -*Last updated: 2026-03-20* diff --git a/document/desktop/base/index.md b/document/desktop/base/index.md deleted file mode 100644 index 513409880..000000000 --- a/document/desktop/base/index.md +++ /dev/null @@ -1,11 +0,0 @@ -# Base - -> Welcome to the Base section. - -## Overview - -Documentation and resources for Base. - ---- - -*Last updated: 2026-03-20* diff --git a/document/desktop/base/logger/.pages b/document/desktop/base/logger/.pages deleted file mode 100644 index 418bbe708..000000000 --- a/document/desktop/base/logger/.pages +++ /dev/null @@ -1,6 +0,0 @@ -title: 日志系统 -nav: - - index.md - - 架构: architecture.md - - 概览: overview.md - - API 参考: api_reference.md diff --git a/document/desktop/base/logger/api_reference.md b/document/desktop/base/logger/api_reference.md deleted file mode 100644 index b9c794acc..000000000 --- a/document/desktop/base/logger/api_reference.md +++ /dev/null @@ -1,617 +0,0 @@ -# CFLogger API 参考 - -本文档提供 CFLogger 的完整 API 参考。 - -## 目录 - -- [简单 API](#简单-api-cflogh) -- [高级 API](#高级-api-cflogcfloghpp) -- [数据类型](#数据类型) -- [Formatter API](#formatter-api) -- [Sink API](#sink-api) -- [FormatterFactory API](#formatterfactory-api) - -## 简单 API (cflog.h) - -简单 API 提供了一组便捷函数,适合快速上手使用。 - -### 头文件 - -```cpp -#include "cflog/cflog.h" -``` - -### 函数 - -#### trace() - -```cpp -void trace(std::string_view msg, - std::string_view tag = "CFLog", - std::source_location loc = std::source_location::current()); -``` - -记录 TRACE 级别的日志。 - -**参数**: -- `msg`: 日志消息 -- `tag`: 日志标签(默认 "CFLog") -- `loc`: 源代码位置(自动捕获) - -#### debug() - -```cpp -void debug(std::string_view msg, - std::string_view tag = "CFLog", - std::source_location loc = std::source_location::current()); -``` - -记录 DEBUG 级别的日志。 - -**参数**: -- `msg`: 日志消息 -- `tag`: 日志标签(默认 "CFLog") -- `loc`: 源代码位置(自动捕获) - -#### info() - -```cpp -void info(std::string_view msg, - std::string_view tag = "CFLog", - std::source_location loc = std::source_location::current()); -``` - -记录 INFO 级别的日志。 - -**参数**: -- `msg`: 日志消息 -- `tag`: 日志标签(默认 "CFLog") -- `loc`: 源代码位置(自动捕获) - -#### warning() - -```cpp -void warning(std::string_view msg, - std::string_view tag = "CFLog", - std::source_location loc = std::source_location::current()); -``` - -记录 WARNING 级别的日志。 - -**参数**: -- `msg`: 日志消息 -- `tag`: 日志标签(默认 "CFLog") -- `loc`: 源代码位置(自动捕获) - -#### error() - -```cpp -void error(std::string_view msg, - std::string_view tag = "CFLog", - std::source_location loc = std::source_location::current()); -``` - -记录 ERROR 级别的日志。 - -**参数**: -- `msg`: 日志消息 -- `tag`: 日志标签(默认 "CFLog") -- `loc`: 源代码位置(自动捕获) - -#### set_level() - -```cpp -void set_level(level lvl); -``` - -设置全局最低日志级别。 - -**参数**: -- `lvl`: 最低日志级别,低于此级别的日志将被过滤 - -#### flush() - -```cpp -void flush(); -``` - -异步刷新日志缓冲区,立即返回。 - ---- - -## 高级 API (cflog/cflog.hpp) - -高级 API 提供对 Logger 的完整控制。 - -### 头文件 - -```cpp -#include "cflog/cflog.hpp" -``` - -### Logger 类 - -#### 获取单例 - -```cpp -static Logger& instance(); -``` - -获取 Logger 单例实例。 - -**返回值**:Logger 单例的引用 - -#### log() - -```cpp -bool log(level log_level, - std::string_view msg, - std::string_view tag, - std::source_location loc); -``` - -记录一条日志消息。 - -**参数**: -- `log_level`: 日志级别 -- `msg`: 日志消息 -- `tag`: 日志标签 -- `loc`: 源代码位置 - -**返回值**:如果日志被接受返回 `true`,被过滤返回 `false` - -#### setMininumLevel() - -```cpp -void setMininumLevel(const level lvl); -``` - -设置最低日志级别。 - -**参数**: -- `lvl`: 最低日志级别 - -#### add_sink() - -```cpp -void add_sink(std::shared_ptr sink); -``` - -添加一个输出目标。 - -**参数**: -- `sink`: 要添加的 sink - -#### remove_sink() - -```cpp -void remove_sink(ISink* sink); -``` - -移除一个输出目标。 - -**参数**: -- `sink`: 要移除的 sink 指针 - -#### clear_sinks() - -```cpp -void clear_sinks(); -``` - -清除所有输出目标。 - -#### flush() - -```cpp -void flush(); -``` - -异步刷新,立即返回。 - -#### flush_sync() - -```cpp -void flush_sync(); -``` - -同步刷新,等待所有日志写入完成。 - ---- - -## 数据类型 - -### level 枚举 - -```cpp -enum class level { - TRACE, // 最详细的输出 - DEBUG, // 调试信息 - INFO, // 一般信息 - WARNING, // 警告信息 - ERROR // 错误信息 -}; -``` - -**默认级别**:`kDEFAULT_LEVEL = level::DEBUG` - -### to_string() - -```cpp -constexpr std::string_view to_string(level lvl) noexcept; -``` - -将日志级别转换为字符串。 - -### as() - -```cpp -template -constexpr T as(level lvl) noexcept; -``` - -将日志级别转换为整数。 - -**示例**: -```cpp -int value = as(level::INFO); // value == 2 -``` - -### LogRecord 结构体 - -```cpp -struct LogRecord { - level lvl; // 日志级别 - std::string tag; // 标签 - std::string msg; // 消息 - cflog_timestamp_t timestamp; // 时间戳 - std::thread::id tid; // 线程 ID - std::source_location loc; // 源代码位置 -}; -``` - -### OpenMode 枚举 - -```cpp -enum class OpenMode { - Append, // 追加模式 - Truncate // 覆盖模式 -}; -``` - -用于 FileSink 的文件打开模式。 - ---- - -## Formatter API - -### IFormatter 接口 - -```cpp -class IFormatter { -public: - virtual ~IFormatter() = default; - - virtual std::string format_me(const LogRecord& r) = 0; - virtual bool configurable() const; - virtual bool set_config(std::shared_ptr config); - virtual std::shared_ptr get_config() const; -}; -``` - -### FormatterFlag 枚举 - -```cpp -enum FormatterFlag : uint32_t { - NONE = 0, - TIMESTAMP = 1 << 0, // 时间戳 - LEVEL = 1 << 1, // 日志级别 - TAG = 1 << 2, // 标签 - THREAD_ID = 1 << 3, // 线程 ID - SOURCE_LOCATION = 1 << 4, // 源代码位置 - MESSAGE = 1 << 5, // 消息 - COLOR = 1 << 6, // ANSI 颜色 - - // 预设组合 - DEFAULT = TIMESTAMP | LEVEL | TAG | SOURCE_LOCATION | MESSAGE, - MINIMAL = LEVEL | MESSAGE, - VERBOSE = TIMESTAMP | LEVEL | TAG | THREAD_ID | SOURCE_LOCATION | MESSAGE, -}; -``` - -### FormatterConfig 类 - -```cpp -class FormatterConfig { -public: - explicit FormatterConfig( - FormatterFlag flags = FormatterFlag::DEFAULT, - std::string timestamp_format = "%H:%M:%S" - ); - - // 标志操作 - FormatterFlag get_flags() const noexcept; - void set_flags(FormatterFlag flags) noexcept; - void enable(FormatterFlag flag) noexcept; - void disable(FormatterFlag flag) noexcept; - bool is_enabled(FormatterFlag flag) const noexcept; - - // 时间戳格式 - void set_timestamp_format(std::string fmt); - const std::string& get_timestamp_format() const noexcept; -}; -``` - -**时间戳格式**:使用 `strftime` 格式字符串,默认 `"%H:%M:%S"` - -### AsciiColorFormatter 类 - -```cpp -class AsciiColorFormatter : public IFormatter { -public: - explicit AsciiColorFormatter(FormatterFlag flags = FormatterFlag::DEFAULT); - - std::string format_me(const LogRecord& r) override; - bool configurable() const override; - bool set_config(std::shared_ptr config) override; - std::shared_ptr get_config() const override; -}; -``` - -**颜色映射**: -- TRACE: 青色 `\033[96m` -- DEBUG: 蓝色 `\033[94m` -- INFO: 绿色 `\033[92m` -- WARNING: 黄色 `\033[93m` -- ERROR: 红色 `\033[91m` - -### FileFormatter 类 - -```cpp -class FileFormatter : public IFormatter { -public: - explicit FileFormatter(FormatterFlag flags = FormatterFlag::DEFAULT); - - std::string format_me(const LogRecord& r) override; - bool configurable() const override; - bool set_config(std::shared_ptr config) override; - std::shared_ptr get_config() const override; -}; -``` - -与 `AsciiColorFormatter` 相同,但忽略 `COLOR` 标志。 - -### DefaultFormatter 类 - -```cpp -class DefaultFormatter : public IFormatter { -public: - DefaultFormatter() = default; - - std::string format_me(const LogRecord& r) override; - bool configurable() const override; -}; -``` - -最简单的格式化器,只输出消息内容。不可配置。 - ---- - -## Sink API - -### ISink 接口 - -```cpp -class ISink { -public: - virtual ~ISink() = default; - - virtual bool write(const LogRecord& record) = 0; - virtual bool flush() = 0; - - virtual bool setFormat(std::shared_ptr formatter); - -protected: - virtual bool formatable() const; - virtual bool actFormat(); - std::shared_ptr formatter_; -}; -``` - -### ConsoleSink 类 - -```cpp -class ConsoleSink : public ISink { -public: - bool write(const LogRecord& record) override; - bool flush() override; -}; -``` - -将日志输出到控制台(stdout)。 - -### FileSink 类 - -```cpp -class FileSink : public ISink { -public: - explicit FileSink(const std::string& filepath, - OpenMode mode = OpenMode::Append); - ~FileSink() override; - - bool write(const LogRecord& record) override; - bool flush() override; - -private: - std::ofstream file_; - std::string filepath_; - OpenMode mode_; -}; -``` - -将日志输出到文件。 - -**构造函数参数**: -- `filepath`: 文件路径 -- `mode`: 打开模式(默认 `OpenMode::Append`) - -**注意**:不可拷贝,删除了拷贝构造和赋值运算符。 - ---- - -## FormatterFactory API - -```cpp -class FormatterFactory { -public: - using IFormatterPtr = std::shared_ptr; - using IFormatterTag = std::string; - using IFormatterTagView = std::string_view; - using MakeFormatter = std::function; - - // 注册格式化器创建函数 - void register_formatter(IFormatterTagView tag, MakeFormatter creator); - - // 注册单例格式化器 - void register_formatter(IFormatterTagView tag, IFormatterPtr instance); - - // 注销格式化器 - bool unregister_formatter(IFormatterTagView tag); - - // 创建新实例(不缓存) - IFormatterPtr create(IFormatterTagView formatter_tag) const; - - // 获取或创建(带缓存) - IFormatterPtr get_or_create(IFormatterTagView formatter_tag); - - // 清除缓存 - void clear_cache(); -}; -``` - -**注意**:`FormatterFactory` 不可拷贝或移动(包含 `std::mutex`)。 - ---- - -## 使用示例 - -### 简单 API 使用 - -```cpp -#include "cflog/cflog.h" - -int main() { - using namespace cf::log; - - // 设置级别 - set_level(level::TRACE); - - // 记录日志 - trace("详细跟踪信息"); - debug("调试信息"); - info("程序正常运行"); - warning("发现潜在问题"); - error("发生错误"); - - // 刷新 - flush(); - - return 0; -} -``` - -### 高级 API 使用 - -```cpp -#include "cflog/cflog.hpp" -#include "cflog/cflog_format_factory.h" -#include "cflog/formatter/console_formatter.h" -#include "cflog/sinks/console_sink.h" -#include "cflog/sinks/file_sink.h" - -void init_logger() { - using namespace cf::log; - - // 创建格式化器工厂 - FormatterFactory factory; - - // 注册格式化器 - factory.register_formatter("console", []() { - return std::make_shared(); - }); - factory.register_formatter("file", []() { - return std::make_shared(); - }); - - // 创建并配置控制台 sink - auto console_sink = std::make_shared(); - console_sink->setFormat(factory.create("console")); - Logger::instance().add_sink(console_sink); - - // 创建并配置文件 sink - auto file_sink = std::make_shared("app.log"); - file_sink->setFormat(factory.create("file")); - Logger::instance().add_sink(file_sink); - - // 设置最低级别 - Logger::instance().setMininumLevel(level::INFO); -} -``` - -### 自定义 Formatter - -```cpp -#include "cflog/cflog_format.h" -#include "cflog/cflog_record.h" - -class MyCustomFormatter : public IFormatter { -public: - std::string format_me(const LogRecord& r) override { - return "[MY LOG] " + r.msg; - } - - bool configurable() const override { return false; } -}; - -// 使用 -auto formatter = std::make_shared(); -sink->setFormat(formatter); -``` - -### 自定义 Sink - -```cpp -#include "cflog/cflog_sink.h" -#include - -class MyCustomSink : public ISink { -public: - bool write(const LogRecord& record) override { - if (formatable() && formatter_) { - auto formatted = formatter_->format_me(record); - std::cout << "[CUSTOM] " << formatted << std::endl; - return true; - } - return false; - } - - bool flush() override { - std::cout << std::flush; - return true; - } -}; - -// 使用 -auto sink = std::make_shared(); -sink->setFormat(std::make_shared()); -Logger::instance().add_sink(sink); -``` - ---- - -## 相关文档 - -- [概述](./overview.md) - 系统概述 -- [架构详解](./architecture.md) - 详细架构说明 -- [HandBook/快速入门](../../../HandBook/desktop/base/logger/quick_start.md) - 5 分钟入门 -- [HandBook/高级用法](../../../HandBook/desktop/base/logger/advanced_usage.md) - 高级特性详解 diff --git a/document/desktop/base/logger/architecture.md b/document/desktop/base/logger/architecture.md deleted file mode 100644 index 1f0c9aa00..000000000 --- a/document/desktop/base/logger/architecture.md +++ /dev/null @@ -1,505 +0,0 @@ -# CFLogger 架构详解 - -本文档详细阐述 CFLogger 的内部架构、各组件的交互方式以及关键设计决策。 - -## 整体架构 - -CFLogger 采用异步日志架构,将日志记录的提交与处理分离: - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 应用程序线程 │ -│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ -│ │Thread 1 │ │Thread 2 │ │Thread 3 │ │Thread N │ │ Main │ │ -│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ -│ │ │ │ │ │ │ -│ └────────────┴────────────┴────────────┴────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌───────────────┐ │ -│ │ Logger API │ │ -│ │ (Singleton) │ │ -│ └───────┬───────┘ │ -└───────────────────────────┼───────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 异步队列层 (AsyncPostQueue) │ -│ ┌──────────────────────────┐ ┌──────────────────────────┐ │ -│ │ 正常队列 (MPSC) │ │ 错误队列 (Mutex) │ │ -│ │ 容量: 65,536 条 │ │ 容量: 无限制 │ │ -│ │ 策略: 满时丢弃 │ │ 策略: 永不丢弃 │ │ -│ └──────────────────────────┘ └──────────────────────────┘ │ -└────────────────────────────────────┬────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 工作线程 (单线程) │ -│ ┌────────────────────────────────────────────────────────────────────┐ │ -│ │ worker_loop(): │ │ -│ │ 1. 从队列取出 LogRecord │ │ -│ │ 2. 遍历所有 sinks │ │ -│ │ 3. 对每个 sink: format → write │ │ -│ │ 4. 处理 flush 请求 │ │ -│ └────────────────────────────────────────────────────────────────────┘ │ -└────────────────────────────────────┬────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Sinks 层 │ -│ │ -│ ┌──────────────────┐ ┌──────────────────┐ ┌────────────┐ │ -│ │ ConsoleSink │ │ FileSink │ │ CustomSink │ │ -│ │ (控制台输出) │ │ (文件输出) │ │ (自定义) │ │ -│ └────────┬─────────┘ └────────┬─────────┘ └─────┬──────┘ │ -│ │ │ │ │ -│ └────────────┬───────────────┴─────────────────────────┘ │ -│ ▼ │ -│ ┌──────────────────────────────────────────────────────────────────────┐ │ -│ │ Formatter 层 │ │ -│ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ │ -│ │ │ AsciiColorFormat │ │ FileFormatter │ │ CustomFormatter │ │ │ -│ │ │ (带颜色的控制台) │ │ (纯文本文件) │ │ (自定义) │ │ │ -│ │ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ │ -│ └──────────────────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -## 组件详解 - -### 1. Logger 类(门面) - -Logger 类是整个系统的入口点,继承自 `SimpleSingleton`: - -```cpp -class Logger : public SimpleSingleton { - 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(); - -private: - std::atomic minimal_level{kDEFAULT_LEVEL}; - std::shared_ptr logger_impl; // PIMPL -}; -``` - -**职责**: -- 提供简洁的公共 API -- 执行日志级别过滤(原子操作,无锁) -- 委托给 LoggerImpl 处理 - -### 2. LoggerImpl 类(实现) - -使用 PIMPL 模式隐藏实现细节: - -```cpp -class LoggerImpl { - bool log(LogRecord record); - void flush(); - void flush_sync(); - - void add_sink(std::shared_ptr sink); - void remove_sink(ISink* sink); - void clear_sinks(); - -private: - AsyncPostQueue async_queue_; -}; -``` - -**职责**: -- 拥有 AsyncPostQueue 实例 -- 将日志记录提交到队列 -- 管理 sink 生命周期 - -### 3. AsyncPostQueue(异步队列) - -核心组件,管理日志的异步处理: - -```cpp -class AsyncPostQueue { - // 队列容量(必须是 2 的幂) - static constexpr size_t kMaxNormalQueueSize = 65536; // 2^16 - - void submit(LogRecord record); - void flush(); // 异步刷新,立即返回 - void flush_sync(); // 同步刷新,等待完成 - - void add_sink(std::shared_ptr sink); - void remove_sink(ISink* sink); - void clear_sinks(); - - size_t get_normal_queue_overflow() const; - -private: - void worker_loop(); - void dispatch_one(const LogRecord& record); -}; -``` - -#### 队列架构 - -``` - submit(LogRecord) - │ - ▼ - ┌─────────────────┐ - │ level == ERROR? │ - └────────┬────────┘ - │ - ┌──────────────┴──────────────┐ - │ YES │ NO - ▼ ▼ - ┌──────────────┐ ┌──────────────────┐ - │ errorQueue_ │ │ normalQueue_ │ - │ (无锁 deque) │ │ (MPSC Queue) │ - │ 无限制 │ │ 65,536 限制 │ - │ 永不丢弃 │ │ 满时丢弃 │ - └──────┬───────┘ └────────┬─────────┘ - │ │ - └────────────┬───────────────┘ - │ - ▼ - ┌─────────────────┐ - │ worker_loop │ - │ (工作线程) │ - └────────┬────────┘ - │ - ▼ - ┌─────────────────┐ - │ dispatch_one │ - │ 遍历所有 sinks │ - └─────────────────┘ -``` - -#### 队列选择策略 - -| 条件 | 选择的队列 | 原因 | -|------|-----------|------| -| `level == ERROR` | errorQueue_ | 错误日志必须保留 | -| `level != ERROR` | normalQueue_ | 普通日志可丢弃 | - -#### 队列容量管理 - -**正常队列** (`normalQueue_`): -- 类型:`MpscQueue` -- 容量:65,536 条消息 -- 策略:满时丢弃新消息,`normalQueueOverflow_` 计数器递增 - -**错误队列** (`errorQueue_`): -- 类型:`std::deque` -- 容量:无限制 -- 策略:永不丢弃 - -### 4. 工作线程 (Worker Thread) - -单线程工作循环,负责处理队列中的消息: - -```cpp -void AsyncPostQueue::worker_loop() { - while (running_) { - // 1. 优先处理错误队列 - { - std::lock_guard lock(errorMu_); - if (!errorQueue_.empty()) { - auto record = std::move(errorQueue_.front()); - errorQueue_.pop_front(); - dispatch_one(record); - continue; - } - } - - // 2. 处理正常队列 - LogRecord record; - if (normalQueue_.try_pop(record)) { - dispatch_one(record); - continue; - } - - // 3. 队列为空,等待唤醒 - if (flush_requested_.load()) { - flush_completed_.store(true); - flush_completed_cv_.notify_one(); - flush_requested_.store(false); - } - - std::unique_lock lock(wakeMu_); - cv_.wait(lock); - } -} -``` - -### 5. 日志记录 (LogRecord) - -日志消息的数据结构: - -```cpp -struct LogRecord { - level log_level; // 日志级别 - std::string tag; // 标签(如 "CFLog") - std::string message; // 日志消息 - std::chrono::system_clock::time_point timestamp; // 时间戳 - std::thread::id thread_id; // 线程 ID - std::source_location source_loc; // 源代码位置 -}; -``` - -### 6. Formatter 架构 - -#### Formatter 接口 - -```cpp -class IFormatter { -public: - virtual ~IFormatter() = default; - virtual std::string format(const LogRecord& record) = 0; - virtual void set_config(std::shared_ptr config) = 0; -}; -``` - -#### FormatterConfig(配置系统) - -```cpp -class FormatterConfig { -public: - void enable(FormatterFlag flag); - void disable(FormatterFlag flag); - bool is_enabled(FormatterFlag flag) const; - void set_timestamp_format(const std::string& fmt); -private: - std::atomic flags_; // 原子操作,线程安全 - std::string timestamp_format_; -}; -``` - -#### FormatterFlag(输出组件) - -```cpp -enum FormatterFlag : uint32_t { - TIMESTAMP = 1 << 0, // 时间戳 - LEVEL = 1 << 1, // 日志级别 - TAG = 1 << 2, // 标签 - THREAD_ID = 1 << 3, // 线程 ID - SOURCE_LOCATION = 1 << 4, // 源代码位置 - MESSAGE = 1 << 5, // 消息内容 - COLOR = 1 << 6, // ANSI 颜色 - - // 预设组合 - MINIMAL = MESSAGE, - DEFAULT = TIMESTAMP | LEVEL | MESSAGE, - VERBOSE = TIMESTAMP | LEVEL | TAG | THREAD_ID | SOURCE_LOCATION | MESSAGE -}; -``` - -#### 可用的 Formatter - -| Formatter | 用途 | COLOR 标志处理 | -|-----------|------|---------------| -| DefaultFormatter | 最简单的输出 | 忽略 | -| AsciiColorFormatter | 控制台彩色输出 | 应用 ANSI 转义码 | -| FileFormatter | 文件纯文本输出 | 忽略 | - -### 7. Sink 架构 - -#### Sink 接口 - -```cpp -class ISink { -public: - virtual ~ISink() = default; - virtual void write(const std::string& formatted) = 0; - virtual void flush() = 0; - virtual void setFormat(std::shared_ptr formatter); -}; -``` - -#### 可用的 Sink - -| Sink | 输出目标 | 特点 | -|------|---------|------| -| ConsoleSink | stdout/stderr | 线程安全,适合开发 | -| FileSink | 文件 | 支持追加/截断模式 | - -#### FileSink 打开模式 - -```cpp -enum class OpenMode { - Truncate, // 截断模式(覆盖现有文件) - Append // 追加模式(添加到文件末尾) -}; -``` - -## 数据流 - -### 日志记录的完整流程 - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ 1. 应用调用 │ -│ info("Hello", "MyTag", source_location::current()); │ -└─────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────┐ -│ 2. Logger::log() │ -│ • 检查 minimal_level(原子操作) │ -│ • 创建 LogRecord │ -│ • 委托给 LoggerImpl │ -└─────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────┐ -│ 3. LoggerImpl::log() │ -│ • 调用 AsyncPostQueue::submit() │ -└─────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────┐ -│ 4. AsyncPostQueue::submit() │ -│ • 如果是 ERROR:推入 errorQueue_(互斥锁保护) │ -│ • 否则:尝试推入 normalQueue_(无锁操作) │ -│ - 成功:继续 │ -│ - 失败:递增 overflow 计数器,丢弃 │ -│ • 通知工作线程 │ -└─────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────┐ -│ 5. Worker Thread::worker_loop() │ -│ • 从队列取出 LogRecord(优先错误队列) │ -│ • 调用 dispatch_one() │ -└─────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────┐ -│ 6. AsyncPostQueue::dispatch_one() │ -│ • 遍历所有 sinks(互斥锁保护) │ -│ • 对每个 sink: │ -│ 1. 调用 formatter->format(record) │ -│ 2. 调用 sink->write(formatted_string) │ -└─────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────┐ -│ 7. Sink::write() │ -│ • ConsoleSink: 写入 stdout/stderr │ -│ • FileSink: 写入文件 │ -│ • CustomSink: 自定义行为 │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -## 线程安全保证 - -### 原子操作 - -| 组件 | 操作 | 同步机制 | -|------|------|----------| -| Logger | minimal_level 读写 | `std::atomic` | -| FormatterConfig | flags 读写 | `std::atomic` | -| AsyncPostQueue | running_/flush_requested_ | `std::atomic` | -| AsyncPostQueue | normalQueueOverflow_ | `std::atomic` | - -### 互斥锁保护 - -| 组件 | 保护的数据 | 锁类型 | -|------|-----------|--------| -| AsyncPostQueue | errorQueue_ | `std::mutex errorMu_` | -| AsyncPostQueue | sinks_ | `std::mutex sinksMu_` | -| AsyncPostQueue | wakeMu_ / cv_ | `std::mutex wakeMu_` | -| FileSink | 文件写入 | 内部互斥锁 | - -### 无锁数据结构 - -| 组件 | 类型 | 特点 | -|------|------|------| -| normalQueue_ | MpscQueue | 无锁 MPSC 队列 | - -## Flush 机制 - -### 异步 Flush (flush()) - -立即返回,不等待刷新完成: - -```cpp -void Logger::flush() { - // 请求刷新 - flush_requested_.store(true); - // 唤醒工作线程 - cv_.notify_one(); -} -``` - -### 同步 Flush (flush_sync()) - -等待刷新完成后返回: - -```cpp -void Logger::flush_sync() { - // 请求刷新 - flush_requested_.store(true); - flush_completed_.store(false); - cv_.notify_one(); - - // 等待完成 - std::unique_lock lock(flush_completed_mu_); - flush_completed_cv_.wait(lock, [] { - return flush_completed_.load(); - }); -} -``` - -## 队列满时的行为 - -### 正常日志队列满 - -``` -应用程序提交日志 - │ - ▼ -normalQueue_.try_push(record) - │ - ┌────┴────┐ - │ │ - 成功 失败 - │ │ - │ ▼ - │ normalQueueOverflow_++ - │ │ - │ ▼ - │ [丢弃日志] - │ - ▼ -继续执行 -``` - -### 错误日志永不丢弃 - -``` -ERROR 日志提交 - │ - ▼ -推入 errorQueue_ - │ - ▼ - [等待处理] -``` - -## 性能优化点 - -1. **无锁队列**:正常日志使用无锁 MPSC 队列,减少同步开销 -2. **原子操作**:级别过滤使用原子变量,无需加锁 -3. **移动语义**:LogRecord 使用 std::move,避免字符串拷贝 -4. **延迟格式化**:在工作线程中格式化,不阻塞调用者 -5. **缓存友好**:队列大小为 2 的幂,优化取模运算 - -## 相关文档 - -- [概述](./overview.md) - 系统概述和快速开始 -- [API 参考](./api_reference.md) - 完整 API 文档 -- [HandBook/性能详解](../../../HandBook/desktop/base/logger/performance.md) - 性能数据和优化 diff --git a/document/desktop/base/logger/index.md b/document/desktop/base/logger/index.md deleted file mode 100644 index 8b465c29f..000000000 --- a/document/desktop/base/logger/index.md +++ /dev/null @@ -1,11 +0,0 @@ -# Logger - -> Welcome to the Logger section. - -## Overview - -Documentation and resources for Logger. - ---- - -*Last updated: 2026-03-20* diff --git a/document/desktop/base/logger/overview.md b/document/desktop/base/logger/overview.md deleted file mode 100644 index f82400474..000000000 --- a/document/desktop/base/logger/overview.md +++ /dev/null @@ -1,265 +0,0 @@ -# CFLogger 概述 - -CFLogger 是 CFDesktop 框架的异步日志系统,专为桌面应用程序设计。它提供高性能、线程安全的日志记录功能,支持多种输出目标(控制台、文件)和灵活的格式化选项。 - -## 为什么选择 CFLogger? - -### 核心特性 - -- **异步处理**:日志消息在单独的工作线程中处理,不会阻塞主线程 -- **高性能**:使用无锁 MPSC 队列,支持高并发写入场景 -- **线程安全**:所有操作都是线程安全的,可在多线程环境中安全使用 -- **灵活扩展**:支持自定义格式化器(Formatter)和输出目标(Sink) -- **优雅降级**:队列满时丢弃普通日志,但保证错误日志不丢失 -- **零依赖**:不依赖 Qt 或其他第三方库,可独立使用 - -## 适用场景 - -CFLogger 适合以下场景: - -- 需要在多线程环境中记录日志的应用程序 -- 对性能敏感的桌面应用 -- 需要同时输出到多个目标(控制台 + 文件)的场景 -- 需要自定义日志格式的项目 -- 嵌入式环境或禁用异常的环境 - -## 系统架构 - -CFLogger 采用分层架构设计: - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 应用层代码 │ -│ trace() / debug() / info() / warning() / error() │ -└────────────────────────┬────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Logger Facade │ -│ (Logger::instance()) │ -└────────────────────────┬────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ AsyncPostQueue │ -│ (无锁 MPSC 队列 + 错误队列) │ -└────────────────────────┬────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 工作线程 │ -│ (异步处理日志消息) │ -└────────────────────────┬────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Sinks 层 │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ ConsoleSink │ │ FileSink │ │ CustomSink │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Formatters 层 │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ AsciiColor │ │ FileFormatter│ │ Custom │ │ -│ │ Formatter │ │ │ │ Formatter │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -## 设计模式 - -CFLogger 使用了多种设计模式: - -### 1. 单例模式 (Singleton) - -Logger 类使用单例模式,确保全局只有一个日志实例: - -```cpp -auto& logger = Logger::instance(); -``` - -### 2. PIMPL 模式 - -实现细节隐藏在 `LoggerImpl` 中,减少编译依赖: - -```cpp -// Logger 类只声明接口 -class Logger { -private: - class Impl; - std::unique_ptr impl_; -}; -``` - -### 3. 策略模式 (Strategy) - -Formatter 和 Sink 都是可插拔的策略组件: - -```cpp -// 可以在运行时切换不同的策略 -sink->setFormat(factory.create("console")); -sink->setFormat(factory.create("file")); -``` - -### 4. 工厂模式 (Factory) - -FormatterFactory 负责创建和管理 formatter 实例: - -```cpp -FormatterFactory factory; -factory.register_formatter("console", []() { - return std::make_shared(); -}); -``` - -### 5. 观察者模式 (Observer) - -多个 Sink 可以"观察"同一个日志消息并各自处理: - -```cpp -Logger::instance().add_sink(console_sink); -Logger::instance().add_sink(file_sink); -// 两个 sink 都会收到相同的日志消息 -``` - -## 日志级别 - -CFLogger 支持五个日志级别(按严重程度递增): - -| 级别 | 说明 | 颜色 | 典型用途 | -|------|------|------|----------| -| TRACE | 最详细的输出 | 青色 | 追踪函数调用流程 | -| DEBUG | 调试信息 | 蓝色 | 开发调试使用 | -| INFO | 一般信息 | 绿色 | 正常运行信息 | -| WARNING | 警告信息 | 黄色 | 潜在问题提示 | -| ERROR | 错误信息 | 红色 | 错误和异常 | - -默认级别是 **DEBUG**,低于当前级别的日志消息会被过滤掉。 - -## 核心组件 - -### Logger / LoggerImpl - -主日志类,提供日志记录的公共 API。使用 PIMPL 模式隐藏实现细节。 - -**关键文件**: -- `cflog_impl.h` - -### AsyncPostQueue - -异步队列,使用无锁 MPSC(多生产者单消费者)队列实现: - -- 正常日志队列容量:65536 条 -- 错误日志队列:无限制 -- 队列满时:丢弃普通日志,保留错误日志 - -**关键文件**: -- `async_queue.h` - -### Formatter(格式化器) - -将日志记录转换为可读文本: - -- `DefaultFormatter`:最简单的格式化器,只输出消息本身 -- `AsciiColorFormatter`:带 ANSI 颜色的控制台格式化器 -- `FileFormatter`:用于文件的纯文本格式化器 - -### Sink(输出目标) - -日志输出的目标位置: - -- `ConsoleSink`:输出到控制台(stdout/stderr) -- `FileSink`:输出到文件(支持追加和截断模式) -- `ISink`:自定义 sink 的接口 - -## 快速示例 - -### 简单使用 - -```cpp -#include "cflog/cflog.h" - -int main() { - using namespace cf::log; - - // 设置日志级别 - set_level(level::TRACE); - - // 记录不同级别的日志 - trace("程序启动"); - info("初始化完成"); - warning("配置文件未找到,使用默认值"); - - flush(); // 确保所有日志都写入 - return 0; -} -``` - -### 高级使用 - -```cpp -#include "cflog/cflog.hpp" - -void init_logger() { - using namespace cf::log; - - // 创建格式化器工厂 - FormatterFactory factory; - factory.register_formatter("console", []() { - return std::make_shared(); - }); - factory.register_formatter("file", []() { - return std::make_shared(); - }); - - // 配置控制台输出 - auto console_sink = std::make_shared(); - console_sink->setFormat(factory.create("console")); - Logger::instance().add_sink(console_sink); - - // 配置文件输出 - auto file_sink = std::make_shared("app.log"); - file_sink->setFormat(factory.create("file")); - Logger::instance().add_sink(file_sink); - - // 设置最低日志级别 - Logger::instance().setMininumLevel(level::INFO); -} -``` - -## 性能特性 - -根据基准测试结果: - -| 场景 | 性能 | -|------|------| -| 单线程基准 | ≥10,000 条日志/秒 | -| 多线程(16 线程) | 保持基准性能 | -| 目标性能 | 10,000-50,000 条日志/秒 | - -详细性能数据请参阅 `HandBook/performance.md`。 - -## 线程安全保证 - -- ✅ 所有公共 API 都是线程安全的 -- ✅ 多线程同时写入不会造成数据竞争 -- ✅ 动态添加/移除 sink 是线程安全的 -- ✅ 运行时修改日志级别是线程安全的 - -## 错误处理 - -CFLogger 采用"不抛异常"的设计理念: - -- 队列满时静默丢弃普通日志,计数器递增 -- 错误级别的日志永远不会被丢弃 -- 文件写入失败不会影响其他 sink 的正常工作 - -## 相关文档 - -- [架构详解](./architecture.md) - 详细的架构设计和组件交互 -- [API 参考](./api_reference.md) - 完整的 API 文档 -- `HandBook/快速入门` - 5 分钟入门指南 -- `HandBook/最佳实践` - 使用建议 diff --git a/document/desktop/index.md b/document/desktop/index.md deleted file mode 100644 index 116c640ae..000000000 --- a/document/desktop/index.md +++ /dev/null @@ -1,11 +0,0 @@ -# Desktop - -> Welcome to the Desktop section. - -## Overview - -Documentation and resources for Desktop. - ---- - -*Last updated: 2026-03-20* diff --git a/document/development/.pages b/document/development/.pages deleted file mode 100644 index 96713e5ad..000000000 --- a/document/development/.pages +++ /dev/null @@ -1,10 +0,0 @@ -title: 开发指南 -icon: material/developer-board -nav: - - 环境准备: 01_prerequisites.md - - 快速开始: 02_quick_start.md - - 构建系统: 03_build_system.md - - 开发工具: 04_development_tools.md - - Docker 构建: 05_docker_build.md - - Git Hooks: 06_git_hooks.md - - 故障排除: 07_troubleshooting.md diff --git a/document/development/01_prerequisites.md b/document/development/01_prerequisites.md index 4483ef5a9..b2337fe4d 100644 --- a/document/development/01_prerequisites.md +++ b/document/development/01_prerequisites.md @@ -1,651 +1,656 @@ -# 01. 前置要求 - -本文档详细介绍 CFDesktop 开发环境的所有前置要求,包括硬件要求、操作系统支持和必需软件的安装指南。 - ---- - -## 目录 - -- [硬件要求](#硬件要求) -- [操作系统支持](#操作系统支持) -- [必需软件安装](#必需软件安装) - - [Docker Desktop](#docker-desktop) - - [Git](#git) - - [VSCode](#vscode) - - [Qt6](#qt6) -- [可选软件](#可选软件) -- [验证安装](#验证安装) - ---- - -## 硬件要求 - -### 最低配置 - -| 组件 | 要求 | 说明 | -|:---|:---|:---| -| **CPU** | 4 核心 | 支持 x86_64 或 ARM64 架构 | -| **RAM** | 8GB | 较大型项目可能需要更多内存 | -| **硬盘** | 20GB 可用空间 | 包含 Qt6、源码和构建产物 | - -### 推荐配置 - -| 组件 | 要求 | 说明 | -|:---|:---|:---| -| **CPU** | 8 核心以上 | 更快的编译速度 | -| **RAM** | 16GB 或更多 | 同时运行 IDE、Docker 等工具 | -| **硬盘** | 50GB+ SSD | 更快的 I/O 性能 | - -### 开发场景资源估算 - -| 开发场景 | CPU | RAM | 硬盘 | -|:---|:---:|:---:|:---:| -| **轻量开发** (仅修改代码) | 2 核心 | 4GB | 10GB | -| **常规开发** (本地编译) | 4 核心 | 8GB | 20GB | -| **完整开发** (Docker 多架构) | 8 核心以上 | 16GB+ | 50GB+ | - ---- - -## 操作系统支持 - -### Windows - -| 版本 | 支持状态 | 工具链 | 备注 | -|:---|:---:|:---|:---| -| **Windows 10** | 支持 | MinGW, LLVM | 需 64 位版本 | -| **Windows 11** | 支持 | MinGW, LLVM | 完全支持 | - -### Linux - -| 发行版 | 支持状态 | 工具链 | 备注 | -|:---|:---:|:---|:---| -| **Ubuntu 22.04 LTS** | 完全支持 | GCC, Clang | 主力开发平台 | -| **Ubuntu 24.04 LTS** | 完全支持 | GCC, Clang | 推荐 | -| **Debian 12+** | 支持 | GCC, Clang | 需手动安装部分依赖 | -| **其他发行版** | 部分支持 | GCC, Clang | 需手动适配 | - -### 其他平台 - -| 平台 | 支持状态 | 备注 | -|:---|:---:|:---| -| **macOS** | 社区支持 | 需自行适配构建脚本 | -| **WSL** | 实验性 | 可能遇到 GUI 相关问题 | - ---- - -## 必需软件安装 - -### Docker Desktop - -Docker 用于多架构构建验证,是 CI/CD 流程的重要组成部分。 - -#### Windows 安装 - -1. **下载安装包** - - 访问 Docker 官方下载页面: - ``` - https://www.docker.com/products/docker-desktop/ - ``` - - 或直接下载 Windows 版本: - - [Docker Desktop for Windows](https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe) - -2. **运行安装程序** - - 双击下载的 `.exe` 文件,按照安装向导完成安装。 - -3. **启动 Docker Desktop** - - 安装完成后,从开始菜单启动 Docker Desktop。首次启动可能需要重启计算机。 - -4. **验证安装** - - ```powershell - docker --version - docker compose version - ``` - -#### Linux 安装 - -在 Ubuntu 22.04+ 上安装 Docker CE 和 Docker Compose: - -```bash -# 1. 更新软件包列表 -sudo apt update - -# 2. 安装必要的依赖 -sudo apt install -y ca-certificates curl gnupg lsb-release - -# 3. 添加 Docker 官方 GPG 密钥 -sudo mkdir -p /etc/apt/keyrings -curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg - -# 4. 添加 Docker 仓库 -echo \ - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ - $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - -# 5. 安装 Docker CE -sudo apt update -sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin - -# 6. 将当前用户添加到 docker 组(避免使用 sudo) -sudo usermod -aG docker $USER - -# 7. 重新登录以使组权限生效 - -# 8. 验证安装 -docker --version -docker compose version -``` - -#### 配置建议 - -**Windows 配置**: -- 在 Docker Desktop 设置中,分配至少 4GB 内存给 Docker -- 启用 WSL 2 集成以获得更好性能 - -**Linux 配置**: -- 配置 Docker 日志大小限制以避免磁盘占满 -- 考虑配置镜像加速器(中国大陆用户) - ---- - -### Git - -Git 是版本控制工具,用于获取和管理 CFDesktop 源代码。 - -#### Windows 安装 - -1. **下载安装包** - - 访问 Git 官方网站: - ``` - https://git-scm.com/download/win - ``` - -2. **运行安装程序** - - 双击下载的安装程序,建议配置: - - 默认编辑器:选择您喜欢的编辑器(如 VSCode) - - PATH 环境:选择 "Git from the command line and also from 3rd-party software" - - 行尾转换:默认即可 - -3. **验证安装** - - ```powershell - git --version - ``` - -#### Linux 安装 - -```bash -# Ubuntu/Debian -sudo apt update -sudo apt install -y git - -# 验证安装 -git --version -``` - -#### 配置建议 - -安装完成后,建议配置您的 Git 信息: - -```bash -git config --global user.name "Your Name" -git config --global user.email "your.email@example.com" - -# Windows 下配置行尾处理 -git config --global core.autocrlf true - -# Linux/Mac 下配置行尾处理 -git config --global core.autocrlf input -``` - ---- - -### VSCode - -Visual Studio Code 是推荐的代码编辑器,提供丰富的 C++ 开发插件支持。 - -#### Windows 安装 - -1. **下载安装包** - - 访问 VSCode 官方网站: - ``` - https://code.visualstudio.com/download - ``` - - 下载 `VSCodeUserSetup-x64.exe`(或 ARM64 版本) - -2. **运行安装程序** - - 双击安装程序,建议勾选: - - 通过 Code 打开添加到资源管理器上下文菜单 - - 将 Code 注册为受支持的文件类型的编辑器 - - 添加到 PATH(可从命令行访问) - -3. **安装推荐插件** - - 启动 VSCode 后,安装以下插件: - - **C/C++** (ms-vscode.cpptools) - - **C/C++ Extension Pack** (ms-vscode.cpptools-extension-pack) - - **Clangd** (llvm-vs-code-extensions.vscode-clangd) - - **CMake Tools** (ms-vscode.cmake-tools) - - **Chinese (Simplified)** (MS-CEINTL.vscode-language-pack-zh-hans) - 可选 - -#### Linux 安装 - -```bash -# Ubuntu/Debian - 下载 .deb 包 -wget https://go.microsoft.com/fwlink/?LinkID=760868 -sudo dpkg -i .deb -sudo apt install -f # 修复依赖 - -# 或使用 snap -sudo snap install --classic code - -# 验证安装 -code --version -``` - -#### 配置建议 - -CFDesktop 项目会自动生成 `.vscode` 配置文件,包括: -- `launch.json` - 调试配置 -- `tasks.json` - 构建任务 -- `.clangd` - Clangd 语言服务器配置 - -确保安装了 Clangd 插件以获得最佳开发体验。 - ---- - -### Qt6 - -Qt6 是 CFDesktop 的核心 UI 框架,需要安装 6.8.3 或更高版本。 - -#### 版本要求 - -| 组件 | 最低版本 | 推荐版本 | -|:---|:---:|:---:| -| **Qt Base** | 6.8.3 | 6.8.3 | -| **Qt Modules** | 6.8.3 | 6.8.3 | - -必需的 Qt 模块: -- `Qt6Core` -- `Qt6Gui` -- `Qt6Widgets` - -#### 安装方式 - -Qt6 有两种安装方式: - -1. **官方在线安装器** (适合初学者) -2. **aqtinstall** (适合开发者和 CI) - -##### 方式一:官方在线安装器 - -**Windows 安装**: - -1. 下载 Qt 在线安装器: - ``` - https://www.qt.io/download-qt-installer - ``` - -2. 运行安装器并注册 Qt 账号(免费) - -3. 在安装器中选择: - - **Qt 6.8.3** (或最新版本) - - **MinGW** 或 **LLVM** 编译器 - - **Qt Creator** (可选) - -4. 安装目录建议: - - Windows: `C:\Qt\6.8.3\` - - Linux: `~/Qt/6.8.3/` - -##### 方式二:aqtinstall (推荐) - -aqtinstall 是一个命令行 Qt 安装工具,更适合开发环境。 - -**安装 aqtinstall**: - -```bash -# 使用 pip 安装 -pip install aqtinstall - -# 或使用 pipx(推荐) -pipx install aqtinstall -``` - -**Windows 安装 Qt6**: - -```powershell -# 安装 Qt 6.8.3 (MinGW 编译器) -aqt install-qt windows desktop 6.8.3 mingw-win64-everywhere -O C:\Qt - -# 或安装 LLVM 编译器版本 -aqt install-qt windows desktop 6.8.3 llvm-mingw-win64-everywhere -O C:\Qt -``` - -**Linux 安装 Qt6**: - -```bash -# 安装 Qt 6.8.3 (GCC 编译器) -aqt install-qt linux desktop 6.8.3 gcc_64 -O ~/Qt - -# 安装到系统目录(需要 sudo) -sudo aqt install-qt linux desktop 6.8.3 gcc_64 -O /opt/Qt -``` - -**ARM64 架构安装**: - -```bash -# Linux ARM64 -aqt install-qt linux desktop 6.8.3 linux_gcc_64 -O ~/Qt -``` - -#### 验证安装 - -```bash -# 检查 qmake 版本 -# Windows -C:\Qt\6.8.3\mingw_64\bin\qmake.exe --version - -# Linux -~/Qt/6.8.3/gcc_64/bin/qmake --version -``` - -#### 环境变量配置 - -**Windows**: - -在系统环境变量中添加: - -``` -QTDIR=C:\Qt\6.8.3\mingw_64 -QTDIR_BIN=C:\Qt\6.8.3\mingw_64\bin -CMAKE_PREFIX_PATH=C:\Qt\6.8.3\mingw_64 -``` - -并将 `%QTDIR_BIN%` 添加到 PATH。 - -**Linux**: - -在 `~/.bashrc` 或 `~/.zshrc` 中添加: - -```bash -export QTDIR=~/Qt/6.8.3/gcc_64 -export QTDIR_BIN=$QTDIR/bin -export PATH=$QTDIR_BIN:$PATH -export CMAKE_PREFIX_PATH=$QTDIR -``` - -然后执行 `source ~/.bashrc` 使配置生效。 - ---- - -## 可选软件 - -### CMake - -CMake 用于构建项目,Docker 镜像中已包含。如需本地安装: - -**Windows**: -```powershell -winget install Kitware.CMake -``` - -**Linux**: -```bash -sudo apt install -y cmake -``` - -### ccache - -ccache 可以加速重复编译: - -**Windows**: -```powershell -winget install ccache -``` - -**Linux**: -```bash -sudo apt install -y ccache -``` - -配置 ccache(可选): - -```bash -# 配置缓存目录 -ccache -M 10G # 设置缓存大小为 10GB -``` - -### Ninja - -Ninja 是一个更快的构建工具: - -**Windows**: -```powershell -winget install ninja -``` - -**Linux**: -```bash -sudo apt install -y ninja-build -``` - ---- - -## 验证安装 - -在继续下一步之前,请验证所有必需软件已正确安装。 - -### Windows 验证脚本 - -在 PowerShell 中运行: - -```powershell -# 创建验证脚本 -@' -Write-Host "=== CFDesktop 开发环境验证 ===" -ForegroundColor Cyan -Write-Host "" - -# Git -Write-Host "检查 Git..." -NoNewline -if (Get-Command git -ErrorAction SilentlyContinue) { - Write-Host " OK ($(git --version))" -ForegroundColor Green -} else { - Write-Host " 未安装" -ForegroundColor Red -} - -# Docker -Write-Host "检查 Docker..." -NoNewline -if (Get-Command docker -ErrorAction SilentlyContinue) { - Write-Host " OK ($(docker --version))" -ForegroundColor Green -} else { - Write-Host " 未安装" -ForegroundColor Red -} - -# VSCode -Write-Host "检查 VSCode..." -NoNewline -if (Get-Command code -ErrorAction SilentlyContinue) { - Write-Host " OK" -ForegroundColor Green -} else { - Write-Host " 未安装" -ForegroundColor Yellow -} - -# CMake -Write-Host "检查 CMake..." -NoNewline -if (Get-Command cmake -ErrorAction SilentlyContinue) { - Write-Host " OK ($(cmake --version | Select-Object -First 1))" -ForegroundColor Green -} else { - Write-Host " 未安装 (Docker 镜像中包含)" -ForegroundColor Yellow -} - -# Python -Write-Host "检查 Python..." -NoNewline -if (Get-Command python -ErrorAction SilentlyContinue) { - Write-Host " OK ($(python --version))" -ForegroundColor Green -} else { - Write-Host " 未安装" -ForegroundColor Yellow -} - -# Qt6 (需要手动检查) -Write-Host "" -Write-Host "请手动验证 Qt6 安装:" -ForegroundColor Yellow -Write-Host " 运行: `\bin\qmake.exe --version`" -Write-Host " 应显示: Qt version 6.8.3" -Write-Host "" -'@ | Out-File -FilePath verify_env.ps1 -Encoding UTF8 - -# 运行验证 -.\verify_env.ps1 -``` - -### Linux 验证脚本 - -```bash -#!/bin/bash -echo "=== CFDesktop 开发环境验证 ===" -echo "" - -# Git -echo -n "检查 Git... " -if command -v git &> /dev/null; then - echo -e "\033[32mOK ($(git --version))\033[0m" -else - echo -e "\033[31m未安装\033[0m" -fi - -# Docker -echo -n "检查 Docker... " -if command -v docker &> /dev/null; then - echo -e "\033[32mOK ($(docker --version))\033[0m" -else - echo -e "\033[31m未安装\033[0m" -fi - -# VSCode -echo -n "检查 VSCode... " -if command -v code &> /dev/null; then - echo -e "\033[32mOK\033[0m" -else - echo -e "\033[33m未安装\033[0m" -fi - -# CMake -echo -n "检查 CMake... " -if command -v cmake &> /dev/null; then - echo -e "\033[32mOK ($(cmake --version | head -n1))\033[0m" -else - echo -e "\033[33m未安装 (Docker 镜像中包含)\033[0m" -fi - -# Python -echo -n "检查 Python... " -if command -v python3 &> /dev/null; then - echo -e "\033[32mOK ($(python3 --version))\033[0m" -else - echo -e "\033[33m未安装\033[0m" -fi - -# Qt6 -echo "" -echo -e "\033[33m请手动验证 Qt6 安装:\033[0m" -echo " 运行: \`/bin/qmake --version\`" -echo " 应显示: Qt version 6.8.3" -echo "" -``` - ---- - -## 下一步 - -完成前置要求安装和验证后,请继续阅读: - -- **[02. 快速开始](02_quick_start.md)** - 快速上手开发 -- **[03. 构建系统](03_build_system.md)** - 了解 CMake 构建系统和编译命令 - ---- - -## 常见问题 - -### Q1: 为什么 Docker 安装后无法启动? - -**A (Windows)**: 确保启用了虚拟化和 Hyper-V/WSL 2 功能。 - -```powershell -# 检查虚拟化状态 -systeminfo | find "Virtualization" -``` - -**A (Linux)**: 确保 Docker 服务已启动。 - -```bash -sudo systemctl start docker -sudo systemctl enable docker -``` - -### Q2: aqtinstall 下载速度太慢怎么办? - -**A**: 可以配置国内镜像源: - -```bash -# 使用清华大学镜像 -aqt install-qt windows desktop 6.8.3 mingw_64 -O C:\Qt -b https://mirrors.tuna.tsinghua.edu.cn/qt -``` - -### Q3: Windows 下安装 Qt 时提示缺少运行库? - -**A**: 安装 Microsoft Visual C++ Redistributable: - -``` -https://aka.ms/vs/17/release/vc_redist.x64.exe -``` - -### Q4: 可以使用系统包管理器安装 Qt 吗? - -**A (Linux)**: 可以,但版本可能不是最新的。 - -```bash -# Ubuntu/Debian -sudo apt install qt6-base-dev qt6-tools-dev -``` - -建议使用 aqtinstall 安装特定版本的 Qt6。 - ---- - -## 附录 - -### 下载链接汇总 - -| 软件 | Windows | Linux | -|:---|:---|:---| -| **Docker Desktop** | [下载](https://www.docker.com/products/docker-desktop/) | [手动安装](https://docs.docker.com/engine/install/) | -| **Git** | [下载](https://git-scm.com/download/win) | `apt install git` | -| **VSCode** | [下载](https://code.visualstudio.com/download) | `snap install code` | -| **Qt 在线安装器** | [下载](https://www.qt.io/download-qt-installer) | [下载](https://www.qt.io/download-qt-installer) | -| **aqtinstall** | `pip install aqtinstall` | `pip install aqtinstall` | -| **CMake** | [下载](https://cmake.org/download/) | `apt install cmake` | - -### 参考文档 - -- [Qt6 官方文档](https://doc.qt.io/qt-6/) -- [aqtinstall 文档](https://aqtinstall.readthedocs.io/) -- [Docker 官方文档](https://docs.docker.com/) -- [CMake 官方文档](https://cmake.org/documentation/) - ---- - -
- - [← 返回](./) | [快速开始 →](02_quick_start.md) - - **版本**: 0.13.1 | **最后更新**: 2026-03-30 - -
+--- +title: 01. 前置要求 +description: 本文档详细介绍 CFDesktop 开发环境的所有前置要求,包括硬件要求、操作系统支持和必需软件的安 +--- + +# 01. 前置要求 + +本文档详细介绍 CFDesktop 开发环境的所有前置要求,包括硬件要求、操作系统支持和必需软件的安装指南。 + +--- + +## 目录 + +- [硬件要求](#硬件要求) +- [操作系统支持](#操作系统支持) +- [必需软件安装](#必需软件安装) + - [Docker Desktop](#docker-desktop) + - [Git](#git) + - [VSCode](#vscode) + - [Qt6](#qt6) +- [可选软件](#可选软件) +- [验证安装](#验证安装) + +--- + +## 硬件要求 + +### 最低配置 + +| 组件 | 要求 | 说明 | +|:---|:---|:---| +| **CPU** | 4 核心 | 支持 x86_64 或 ARM64 架构 | +| **RAM** | 8GB | 较大型项目可能需要更多内存 | +| **硬盘** | 20GB 可用空间 | 包含 Qt6、源码和构建产物 | + +### 推荐配置 + +| 组件 | 要求 | 说明 | +|:---|:---|:---| +| **CPU** | 8 核心以上 | 更快的编译速度 | +| **RAM** | 16GB 或更多 | 同时运行 IDE、Docker 等工具 | +| **硬盘** | 50GB+ SSD | 更快的 I/O 性能 | + +### 开发场景资源估算 + +| 开发场景 | CPU | RAM | 硬盘 | +|:---|:---:|:---:|:---:| +| **轻量开发** (仅修改代码) | 2 核心 | 4GB | 10GB | +| **常规开发** (本地编译) | 4 核心 | 8GB | 20GB | +| **完整开发** (Docker 多架构) | 8 核心以上 | 16GB+ | 50GB+ | + +--- + +## 操作系统支持 + +### Windows + +| 版本 | 支持状态 | 工具链 | 备注 | +|:---|:---:|:---|:---| +| **Windows 10** | 支持 | MinGW, LLVM | 需 64 位版本 | +| **Windows 11** | 支持 | MinGW, LLVM | 完全支持 | + +### Linux + +| 发行版 | 支持状态 | 工具链 | 备注 | +|:---|:---:|:---|:---| +| **Ubuntu 22.04 LTS** | 完全支持 | GCC, Clang | 主力开发平台 | +| **Ubuntu 24.04 LTS** | 完全支持 | GCC, Clang | 推荐 | +| **Debian 12+** | 支持 | GCC, Clang | 需手动安装部分依赖 | +| **其他发行版** | 部分支持 | GCC, Clang | 需手动适配 | + +### 其他平台 + +| 平台 | 支持状态 | 备注 | +|:---|:---:|:---| +| **macOS** | 社区支持 | 需自行适配构建脚本 | +| **WSL** | 实验性 | 可能遇到 GUI 相关问题 | + +--- + +## 必需软件安装 + +### Docker Desktop + +Docker 用于多架构构建验证,是 CI/CD 流程的重要组成部分。 + +#### Windows 安装 + +1. **下载安装包** + + 访问 Docker 官方下载页面: + ``` + https://www.docker.com/products/docker-desktop/ + ``` + + 或直接下载 Windows 版本: + - [Docker Desktop for Windows](https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe) + +2. **运行安装程序** + + 双击下载的 `.exe` 文件,按照安装向导完成安装。 + +3. **启动 Docker Desktop** + + 安装完成后,从开始菜单启动 Docker Desktop。首次启动可能需要重启计算机。 + +4. **验证安装** + + ```powershell + docker --version + docker compose version + ``` + +#### Linux 安装 + +在 Ubuntu 22.04+ 上安装 Docker CE 和 Docker Compose: + +```bash +# 1. 更新软件包列表 +sudo apt update + +# 2. 安装必要的依赖 +sudo apt install -y ca-certificates curl gnupg lsb-release + +# 3. 添加 Docker 官方 GPG 密钥 +sudo mkdir -p /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg + +# 4. 添加 Docker 仓库 +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + +# 5. 安装 Docker CE +sudo apt update +sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + +# 6. 将当前用户添加到 docker 组(避免使用 sudo) +sudo usermod -aG docker $USER + +# 7. 重新登录以使组权限生效 + +# 8. 验证安装 +docker --version +docker compose version +```yaml + +#### 配置建议 + +**Windows 配置**: +- 在 Docker Desktop 设置中,分配至少 4GB 内存给 Docker +- 启用 WSL 2 集成以获得更好性能 + +**Linux 配置**: +- 配置 Docker 日志大小限制以避免磁盘占满 +- 考虑配置镜像加速器(中国大陆用户) + +--- + +### Git + +Git 是版本控制工具,用于获取和管理 CFDesktop 源代码。 + +#### Windows 安装 + +1. **下载安装包** + + 访问 Git 官方网站: + ``` + https://git-scm.com/download/win + ``` + +2. **运行安装程序** + + 双击下载的安装程序,建议配置: + - 默认编辑器:选择您喜欢的编辑器(如 VSCode) + - PATH 环境:选择 "Git from the command line and also from 3rd-party software" + - 行尾转换:默认即可 + +3. **验证安装** + + ```powershell + git --version + ``` + +#### Linux 安装 + +```bash +# Ubuntu/Debian +sudo apt update +sudo apt install -y git + +# 验证安装 +git --version +```text + +#### 配置建议 + +安装完成后,建议配置您的 Git 信息: + +```bash +git config --global user.name "Your Name" +git config --global user.email "your.email@example.com" + +# Windows 下配置行尾处理 +git config --global core.autocrlf true + +# Linux/Mac 下配置行尾处理 +git config --global core.autocrlf input +```yaml + +--- + +### VSCode + +Visual Studio Code 是推荐的代码编辑器,提供丰富的 C++ 开发插件支持。 + +#### Windows 安装 + +1. **下载安装包** + + 访问 VSCode 官方网站: + ``` + https://code.visualstudio.com/download + ``` + + 下载 `VSCodeUserSetup-x64.exe`(或 ARM64 版本) + +2. **运行安装程序** + + 双击安装程序,建议勾选: + - 通过 Code 打开添加到资源管理器上下文菜单 + - 将 Code 注册为受支持的文件类型的编辑器 + - 添加到 PATH(可从命令行访问) + +3. **安装推荐插件** + + 启动 VSCode 后,安装以下插件: + - **C/C++** (ms-vscode.cpptools) + - **C/C++ Extension Pack** (ms-vscode.cpptools-extension-pack) + - **Clangd** (llvm-vs-code-extensions.vscode-clangd) + - **CMake Tools** (ms-vscode.cmake-tools) + - **Chinese (Simplified)** (MS-CEINTL.vscode-language-pack-zh-hans) - 可选 + +#### Linux 安装 + +```bash +# Ubuntu/Debian - 下载 .deb 包 +wget https://go.microsoft.com/fwlink/?LinkID=760868 +sudo dpkg -i .deb +sudo apt install -f # 修复依赖 + +# 或使用 snap +sudo snap install --classic code + +# 验证安装 +code --version +```bash + +#### 配置建议 + +CFDesktop 项目会自动生成 `.vscode` 配置文件,包括: +- `launch.json` - 调试配置 +- `tasks.json` - 构建任务 +- `.clangd` - Clangd 语言服务器配置 + +确保安装了 Clangd 插件以获得最佳开发体验。 + +--- + +### Qt6 + +Qt6 是 CFDesktop 的核心 UI 框架,需要安装 6.8.3 或更高版本。 + +#### 版本要求 + +| 组件 | 最低版本 | 推荐版本 | +|:---|:---:|:---:| +| **Qt Base** | 6.8.3 | 6.8.3 | +| **Qt Modules** | 6.8.3 | 6.8.3 | + +必需的 Qt 模块: +- `Qt6Core` +- `Qt6Gui` +- `Qt6Widgets` + +#### 安装方式 + +Qt6 有两种安装方式: + +1. **官方在线安装器** (适合初学者) +2. **aqtinstall** (适合开发者和 CI) + +##### 方式一:官方在线安装器 + +**Windows 安装**: + +1. 下载 Qt 在线安装器: + ``` + https://www.qt.io/download-qt-installer + ``` + +2. 运行安装器并注册 Qt 账号(免费) + +3. 在安装器中选择: + - **Qt 6.8.3** (或最新版本) + - **MinGW** 或 **LLVM** 编译器 + - **Qt Creator** (可选) + +4. 安装目录建议: + - Windows: `C:\Qt\6.8.3\` + - Linux: `~/Qt/6.8.3/` + +##### 方式二:aqtinstall (推荐) + +aqtinstall 是一个命令行 Qt 安装工具,更适合开发环境。 + +**安装 aqtinstall**: + +```bash +# 使用 pip 安装 +pip install aqtinstall + +# 或使用 pipx(推荐) +pipx install aqtinstall +```text + +**Windows 安装 Qt6**: + +```powershell +# 安装 Qt 6.8.3 (MinGW 编译器) +aqt install-qt windows desktop 6.8.3 mingw-win64-everywhere -O C:\Qt + +# 或安装 LLVM 编译器版本 +aqt install-qt windows desktop 6.8.3 llvm-mingw-win64-everywhere -O C:\Qt +```text + +**Linux 安装 Qt6**: + +```bash +# 安装 Qt 6.8.3 (GCC 编译器) +aqt install-qt linux desktop 6.8.3 gcc_64 -O ~/Qt + +# 安装到系统目录(需要 sudo) +sudo aqt install-qt linux desktop 6.8.3 gcc_64 -O /opt/Qt +```text + +**ARM64 架构安装**: + +```bash +# Linux ARM64 +aqt install-qt linux desktop 6.8.3 linux_gcc_64 -O ~/Qt +```text + +#### 验证安装 + +```bash +# 检查 qmake 版本 +# Windows +C:\Qt\6.8.3\mingw_64\bin\qmake.exe --version + +# Linux +~/Qt/6.8.3/gcc_64/bin/qmake --version +```text + +#### 环境变量配置 + +**Windows**: + +在系统环境变量中添加: + +```text +QTDIR=C:\Qt\6.8.3\mingw_64 +QTDIR_BIN=C:\Qt\6.8.3\mingw_64\bin +CMAKE_PREFIX_PATH=C:\Qt\6.8.3\mingw_64 +```text + +并将 `%QTDIR_BIN%` 添加到 PATH。 + +**Linux**: + +在 `~/.bashrc` 或 `~/.zshrc` 中添加: + +```bash +export QTDIR=~/Qt/6.8.3/gcc_64 +export QTDIR_BIN=$QTDIR/bin +export PATH=$QTDIR_BIN:$PATH +export CMAKE_PREFIX_PATH=$QTDIR +```yaml + +然后执行 `source ~/.bashrc` 使配置生效。 + +--- + +## 可选软件 + +### CMake + +CMake 用于构建项目,Docker 镜像中已包含。如需本地安装: + +**Windows**: +```powershell +winget install Kitware.CMake +```text + +**Linux**: +```bash +sudo apt install -y cmake +```text + +### ccache + +ccache 可以加速重复编译: + +**Windows**: +```powershell +winget install ccache +```text + +**Linux**: +```bash +sudo apt install -y ccache +```text + +配置 ccache(可选): + +```bash +# 配置缓存目录 +ccache -M 10G # 设置缓存大小为 10GB +```text + +### Ninja + +Ninja 是一个更快的构建工具: + +**Windows**: +```powershell +winget install ninja +```text + +**Linux**: +```bash +sudo apt install -y ninja-build +```yaml + +--- + +## 验证安装 + +在继续下一步之前,请验证所有必需软件已正确安装。 + +### Windows 验证脚本 + +在 PowerShell 中运行: + +```powershell +# 创建验证脚本 +@' +Write-Host "=== CFDesktop 开发环境验证 ===" -ForegroundColor Cyan +Write-Host "" + +# Git +Write-Host "检查 Git..." -NoNewline +if (Get-Command git -ErrorAction SilentlyContinue) { + Write-Host " OK ($(git --version))" -ForegroundColor Green +} else { + Write-Host " 未安装" -ForegroundColor Red +} + +# Docker +Write-Host "检查 Docker..." -NoNewline +if (Get-Command docker -ErrorAction SilentlyContinue) { + Write-Host " OK ($(docker --version))" -ForegroundColor Green +} else { + Write-Host " 未安装" -ForegroundColor Red +} + +# VSCode +Write-Host "检查 VSCode..." -NoNewline +if (Get-Command code -ErrorAction SilentlyContinue) { + Write-Host " OK" -ForegroundColor Green +} else { + Write-Host " 未安装" -ForegroundColor Yellow +} + +# CMake +Write-Host "检查 CMake..." -NoNewline +if (Get-Command cmake -ErrorAction SilentlyContinue) { + Write-Host " OK ($(cmake --version | Select-Object -First 1))" -ForegroundColor Green +} else { + Write-Host " 未安装 (Docker 镜像中包含)" -ForegroundColor Yellow +} + +# Python +Write-Host "检查 Python..." -NoNewline +if (Get-Command python -ErrorAction SilentlyContinue) { + Write-Host " OK ($(python --version))" -ForegroundColor Green +} else { + Write-Host " 未安装" -ForegroundColor Yellow +} + +# Qt6 (需要手动检查) +Write-Host "" +Write-Host "请手动验证 Qt6 安装:" -ForegroundColor Yellow +Write-Host " 运行: `\bin\qmake.exe --version`" +Write-Host " 应显示: Qt version 6.8.3" +Write-Host "" +'@ | Out-File -FilePath verify_env.ps1 -Encoding UTF8 + +# 运行验证 +.\verify_env.ps1 +```text + +### Linux 验证脚本 + +```bash +#!/bin/bash +echo "=== CFDesktop 开发环境验证 ===" +echo "" + +# Git +echo -n "检查 Git... " +if command -v git &> /dev/null; then + echo -e "\033[32mOK ($(git --version))\033[0m" +else + echo -e "\033[31m未安装\033[0m" +fi + +# Docker +echo -n "检查 Docker... " +if command -v docker &> /dev/null; then + echo -e "\033[32mOK ($(docker --version))\033[0m" +else + echo -e "\033[31m未安装\033[0m" +fi + +# VSCode +echo -n "检查 VSCode... " +if command -v code &> /dev/null; then + echo -e "\033[32mOK\033[0m" +else + echo -e "\033[33m未安装\033[0m" +fi + +# CMake +echo -n "检查 CMake... " +if command -v cmake &> /dev/null; then + echo -e "\033[32mOK ($(cmake --version | head -n1))\033[0m" +else + echo -e "\033[33m未安装 (Docker 镜像中包含)\033[0m" +fi + +# Python +echo -n "检查 Python... " +if command -v python3 &> /dev/null; then + echo -e "\033[32mOK ($(python3 --version))\033[0m" +else + echo -e "\033[33m未安装\033[0m" +fi + +# Qt6 +echo "" +echo -e "\033[33m请手动验证 Qt6 安装:\033[0m" +echo " 运行: \`/bin/qmake --version\`" +echo " 应显示: Qt version 6.8.3" +echo "" +```yaml + +--- + +## 下一步 + +完成前置要求安装和验证后,请继续阅读: + +- **[02. 快速开始](02_quick_start.md)** - 快速上手开发 +- **[03. 构建系统](03_build_system.md)** - 了解 CMake 构建系统和编译命令 + +--- + +## 常见问题 + +### Q1: 为什么 Docker 安装后无法启动? + +**A (Windows)**: 确保启用了虚拟化和 Hyper-V/WSL 2 功能。 + +```powershell +# 检查虚拟化状态 +systeminfo | find "Virtualization" +```text + +**A (Linux)**: 确保 Docker 服务已启动。 + +```bash +sudo systemctl start docker +sudo systemctl enable docker +```text + +### Q2: aqtinstall 下载速度太慢怎么办? + +**A**: 可以配置国内镜像源: + +```bash +# 使用清华大学镜像 +aqt install-qt windows desktop 6.8.3 mingw_64 -O C:\Qt -b https://mirrors.tuna.tsinghua.edu.cn/qt +```text + +### Q3: Windows 下安装 Qt 时提示缺少运行库? + +**A**: 安装 Microsoft Visual C++ Redistributable: + +```text +https://aka.ms/vs/17/release/vc_redist.x64.exe +```text + +### Q4: 可以使用系统包管理器安装 Qt 吗? + +**A (Linux)**: 可以,但版本可能不是最新的。 + +```bash +# Ubuntu/Debian +sudo apt install qt6-base-dev qt6-tools-dev +```bash + +建议使用 aqtinstall 安装特定版本的 Qt6。 + +--- + +## 附录 + +### 下载链接汇总 + +| 软件 | Windows | Linux | +|:---|:---|:---| +| **Docker Desktop** | [下载](https://www.docker.com/products/docker-desktop/) | [手动安装](https://docs.docker.com/engine/install/) | +| **Git** | [下载](https://git-scm.com/download/win) | `apt install git` | +| **VSCode** | [下载](https://code.visualstudio.com/download) | `snap install code` | +| **Qt 在线安装器** | [下载](https://www.qt.io/download-qt-installer) | [下载](https://www.qt.io/download-qt-installer) | +| **aqtinstall** | `pip install aqtinstall` | `pip install aqtinstall` | +| **CMake** | [下载](https://cmake.org/download/) | `apt install cmake` | + +### 参考文档 + +- [Qt6 官方文档](https://doc.qt.io/qt-6/) +- [aqtinstall 文档](https://aqtinstall.readthedocs.io/) +- [Docker 官方文档](https://docs.docker.com/) +- [CMake 官方文档](https://cmake.org/documentation/) + +--- + +
+ + [← 返回](./) | [快速开始 →](02_quick_start.md) + + **版本**: 0.13.1 | **最后更新**: 2026-03-30 + +
diff --git a/document/development/02_quick_start.md b/document/development/02_quick_start.md index 246d5b86b..234902354 100644 --- a/document/development/02_quick_start.md +++ b/document/development/02_quick_start.md @@ -1,354 +1,359 @@ -# Quick Start Guide - -> Get up and running with CFDesktop in 30 minutes - -## Table of Contents - -- [Prerequisites](#prerequisites) -- [5-Step Quick Start](#5-step-quick-start) -- [Windows Users](#windows-users) -- [Verification](#verification) -- [Next Steps](#next-steps) -- [Troubleshooting](#troubleshooting) - ---- - -## Prerequisites - -Before starting, ensure you have the following installed: - -| Requirement | Minimum Version | Recommended | How to Check | -|:------------|----------------:|:------------|:-------------| -| **Git** | 2.30+ | Latest | `git --version` | -| **CMake** | 3.16 | 3.20+ | `cmake --version` | -| **Qt6** | 6.8.3 | 6.8+ | Check Qt Installation | -| **Compiler** | LLVM/Clang or GCC | Latest | `clang --version` or `gcc --version` | -| **Docker** (optional) | 20.10+ | Latest | `docker --version` | - -### Platform-Specific Requirements - -**Windows:** -- Git Bash or WSL2 for running build scripts -- Qt6 installed with MinGW or LLVM-MinGW toolchain -- PowerShell 5.1+ for Windows-specific scripts - -**Linux:** -- GCC 12+ or Clang 16+ -- Qt6 development packages -- Ninja build system (optional but recommended) - ---- - -## 5-Step Quick Start - -### Step 1: Clone the Repository - -```bash -git clone https://github.com/Awesome-Embedded-Learning-Studio/CFDesktop.git -cd CFDesktop -``` - -### Step 2: Install VSCode Extensions - -Install the following VSCode extensions for optimal development experience: - -| Extension | Purpose | Required | -|:----------|:--------|:--------:| -| [Clangd](https://marketplace.visualstudio.com/items?itemName=llvm-vs-code-extensions.vscode-clangd) | C++ language server | Yes | -| [CMake](https://marketplace.visualstudio.com/items?itemName=twxs.cmake) | CMake syntax highlighting | Recommended | -| [CMake Tools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cmake-tools) | CMake integration | Recommended | -| [Qt for Python](https://marketplace.visualstudio.com/items?itemName=seanwu.vscode-qt-for-python) | Qt support | Optional | - -**Quick Install (Command Palette):** - -1. Press `Ctrl+Shift+P` (Windows/Linux) or `Cmd+Shift+P` (macOS) -2. Type "Extensions: Install Extensions" -3. Search for each extension name above - -### Step 3: Docker First Build (Recommended) - -The fastest way to get started is using Docker, which provides a pre-configured build environment: - -```bash -# Fast build (reuses existing image if available) -bash scripts/build_helpers/docker_start.sh --fast-build --build-project-fast -``` - -**What this does:** -1. Builds (or reuses) the Docker image with all dependencies -2. Configures the project with CMake -3. Builds all modules (base, ui, examples) -4. Places outputs in `out/build_develop/` - -**Docker Options:** - -| Option | Description | -|:-------|:------------| -| `--fast-build` | Skip image cleanup, reuse existing image | -| `--build-project` | Full clean build | -| `--build-project-fast` | Fast incremental build | -| `--run-project-test` | Build and run tests | -| `--arch arm64` | Build for ARM64 architecture | -| `--verify` | Run CI-style verification build | - -### Step 4: Run Example Programs - -After successful build, run the example programs: - -**Windows:** -```powershell -# Material Design Gallery -.\out\build_develop\examples\gui\material_gallery.exe - -# Button Widget Example -.\out\build_develop\examples\ui\button.exe - -# CPU Information Demo -.\out\build_develop\examples\base\cpu_info.exe -``` - -**Linux:** -```bash -# Material Design Gallery -./out/build_develop/examples/gui/material_gallery - -# Button Widget Example -./out/build_develop/examples/ui/button - -# CPU Information Demo -./out/build_develop/examples/base/cpu_info -``` - -**Available Examples:** - -| Category | Examples | Description | -|:---------|:---------|:------------| -| **base/** | cpu_info, memory_info | System information demos | -| **ui/** | button, label, textfield, checkbox, radiobutton, textarea, groupbox | Material Design widgets | -| **gui/** | material_gallery, theme | Complete UI demos | - -### Step 5: Run Tests - -Verify your build by running the test suite: - -**Windows:** -```powershell -.\scripts\build_helpers\windows_run_tests.ps1 -``` - -**Linux:** -```bash -bash scripts/build_helpers/linux_run_tests.sh -``` - -**Docker:** -```bash -bash scripts/build_helpers/docker_start.sh --run-project-test -``` - ---- - -## Windows Users - -### Using Git Bash - -CFDesktop build scripts are designed for Unix-style shells. On Windows, use **Git Bash**: - -1. Install [Git for Windows](https://git-scm.com/download/win) -2. Open Git Bash from the Start Menu -3. Navigate to your project directory: - ```bash - cd /d/ProjectHome/CFDesktop - ``` - -### Using WSL2 (Alternative) - -For better performance, use Windows Subsystem for Linux: - -1. Install WSL2: - ```powershell - wsl --install - ``` - -2. Access your Windows files from WSL: - ```bash - cd /mnt/d/ProjectHome/CFDesktop - ``` - -### Path Format Conversion - -When running scripts on Windows, be aware of path formats: - -| Type | Format | Example | -|:-----|:-------|:--------| -| **Windows** | `D:\Path\To\File` | `D:/ProjectHome/CFDesktop` | -| **Git Bash** | `/d/Path/To/File` | `/d/ProjectHome/CFDesktop` | -| **Docker Mount** | `/d/Path/To/File` | `/d/ProjectHome/CFDesktop` | -| **PowerShell** | `D:\Path\To\File` | `D:\ProjectHome\CFDesktop` | - -The build scripts automatically handle path conversion for Docker mounts. - -### Windows Build Scripts - -For native Windows builds (without Docker), use PowerShell scripts: - -```powershell -# Configure and build (fast) -.\scripts\build_helpers\windows_fast_develop_build.ps1 - -# Configure and build (full clean) -.\scripts\build_helpers\windows_develop_build.ps1 - -# Configure only -.\scripts\build_helpers\windows_configure.ps1 - -# Run tests -.\scripts\build_helpers\windows_run_tests.ps1 -``` - ---- - -## Verification - -### Check Docker Installation - -```bash -# Verify Docker is installed -docker --version - -# Verify Docker daemon is running -docker info -``` - -Expected output: -``` -Docker version 20.10.x -... -Server Version: 20.10.x -``` - -### Check Build Success - -A successful build will create the following directory structure: - -``` -out/build_develop/ -├── bin/ -│ ├── cfbase.dll # Base library (Windows) -│ ├── cfui.dll # UI library (Windows) -│ └── ... -├── lib/ -│ ├── libcfbase.a # Static libraries (Linux) -│ └── ... -├── examples/ -│ ├── base/ # Base examples -│ ├── ui/ # UI widget examples -│ └── gui/ # GUI examples -├── runtimes/ # Qt runtime DLLs (Windows) -└── test/ # Test executables -``` - -### Check Build Logs - -Build logs are saved to `scripts/docker/logger/` when using Docker: - -```bash -# List recent build logs -ls -lt scripts/docker/logger/ - -# View the latest log -cat scripts/docker/logger/ci_build_*.log | tail -50 -``` - ---- - -## Next Steps - -After completing the quick start: - -1. **Read the Build System Documentation**: [`03_build_system.md`](03_build_system.md) -2. **Explore the Examples**: Browse `example/` directory for sample code -3. **Review Project Structure**: Check the [Project Skeleton Design](../design_stage/00_phase0_project_skeleton.md) -4. **Set Up Your Development Environment**: See [VSCode Configuration](../design_stage/00_phase0_project_skeleton.md#五开发环境配置) - ---- - -## Troubleshooting - -### Docker Build Failures - -| Issue | Solution | -|:------|:----------| -| Docker daemon not running | Start Docker Desktop | -| Permission denied | Use `sudo` on Linux or run terminal as Administrator on Windows | -| Port conflicts | Ensure no other containers are using required ports | -| Out of memory | Increase Docker memory limit in settings | - -### Build Errors - -| Issue | Solution | -|:------|----------| -| Qt not found | Set `Qt6_DIR` environment variable or use Docker | -| Compiler not found | Install LLVM/Clang or GCC, or use Docker build | -| CMake version too old | Upgrade CMake to 3.16+ | -| Missing dependencies | Use Docker build which includes all dependencies | - -### Windows-Specific Issues - -| Issue | Solution | -|:------|----------| -| Path too long | Enable long path support in Windows or move project closer to drive root | -| PowerShell execution policy | Run `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser` | -| Git Bash path issues | Use `/d/` format for D: drive | - -### Getting Help - -If you encounter issues not covered here: - -1. Check the [Design Documentation](../../design_stage/) for detailed information -2. Review the [Build System Documentation](03_build_system.md) -3. Open an issue on [GitHub](https://github.com/Awesome-Embedded-Learning-Studio/CFDesktop/issues) - ---- - -## Quick Reference - -### Essential Commands - -```bash -# Clone and setup -git clone https://github.com/Awesome-Embedded-Learning-Studio/CFDesktop.git -cd CFDesktop - -# Docker build (fastest) -bash scripts/build_helpers/docker_start.sh --fast-build --build-project-fast - -# Run tests -bash scripts/build_helpers/docker_start.sh --run-project-test - -# Interactive shell in Docker -bash scripts/build_helpers/docker_start.sh - -# Build specific architecture -bash scripts/build_helpers/docker_start.sh --arch arm64 --verify -``` - -### Configuration Files - -| File | Purpose | -|:-----|:---------| -| `build_develop_config.ini` | Development build configuration | -| `build_deploy_config.ini` | Deployment build configuration | -| `build_ci_config.ini` | CI build configuration | - -### Output Locations - -| Build Type | Output Directory | -|:-----------|:-----------------| -| Development | `out/build_develop/` | -| Deployment | `out/build_deploy/` | -| CI | `out/build_ci/` | - ---- - -**Last Updated**: 2026-03-07 +--- +title: 快速开始指南 +description: 30 分钟内搭建 CFDesktop 开发环境 +--- + +# 快速开始指南 + +> 30 分钟内搭建 CFDesktop 开发环境 + +## 目录 + +- [前置条件](#前置条件) +- [五步快速开始](#五步快速开始) +- [Windows 用户](#windows-用户) +- [验证](#验证) +- [后续步骤](#后续步骤) +- [常见问题排查](#常见问题排查) + +--- + +## 前置条件 + +在开始之前,请确保已安装以下工具: + +| 需求 | 最低版本 | 推荐版本 | 检查命令 | +|:-----|---------:|:---------|:---------| +| **Git** | 2.30+ | 最新版 | `git --version` | +| **CMake** | 3.16 | 3.20+ | `cmake --version` | +| **Qt6** | 6.8.3 | 6.8+ | 检查 Qt 安装目录 | +| **编译器** | LLVM/Clang 或 GCC | 最新版 | `clang --version` 或 `gcc --version` | +| **Docker**(可选) | 20.10+ | 最新版 | `docker --version` | + +### 平台特定要求 + +**Windows:** +- 需要使用 Git Bash 或 WSL2 来运行构建脚本 +- 需要安装 Qt6,并配置 MinGW 或 LLVM-MinGW 工具链 +- Windows 专用脚本需要 PowerShell 5.1+ + +**Linux:** +- GCC 12+ 或 Clang 16+ +- Qt6 开发包 +- Ninja 构建系统(可选,但推荐安装) + +--- + +## 五步快速开始 + +### 第 1 步:克隆仓库 + +```bash +git clone https://github.com/Awesome-Embedded-Learning-Studio/CFDesktop.git +cd CFDesktop +```bash + +### 第 2 步:安装 VSCode 扩展 + +安装以下 VSCode 扩展以获得最佳开发体验: + +| 扩展 | 用途 | 是否必需 | +|:-----|:-----|:--------:| +| [Clangd](https://marketplace.visualstudio.com/items?itemName=llvm-vs-code-extensions.vscode-clangd) | C++ 语言服务器 | 是 | +| [CMake](https://marketplace.visualstudio.com/items?itemName=twxs.cmake) | CMake 语法高亮 | 推荐 | +| [CMake Tools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cmake-tools) | CMake 集成 | 推荐 | +| [Qt for Python](https://marketplace.visualstudio.com/items?itemName=seanwu.vscode-qt-for-python) | Qt 支持 | 可选 | + +**快速安装(命令面板):** + +1. 按 `Ctrl+Shift+P`(Windows/Linux)或 `Cmd+Shift+P`(macOS) +2. 输入 "Extensions: Install Extensions" +3. 搜索上方的每个扩展名称 + +### 第 3 步:Docker 首次构建(推荐) + +最快的上手方式是使用 Docker,它提供了预配置的构建环境: + +```bash +# Fast build (reuses existing image if available) +bash scripts/build_helpers/docker_start.sh --fast-build --build-project-fast +```bash + +**此命令会执行以下操作:** +1. 构建(或复用)包含所有依赖的 Docker 镜像 +2. 使用 CMake 配置项目 +3. 构建所有模块(base、ui、examples) +4. 将输出放置在 `out/build_develop/` 目录中 + +**Docker 选项:** + +| 选项 | 说明 | +|:-----|:-----| +| `--fast-build` | 跳过镜像清理,复用已有镜像 | +| `--build-project` | 完整的干净构建 | +| `--build-project-fast` | 快速增量构建 | +| `--run-project-test` | 构建并运行测试 | +| `--arch arm64` | 为 ARM64 架构构建 | +| `--verify` | 运行 CI 风格的验证构建 | + +### 第 4 步:运行示例程序 + +构建成功后,运行示例程序: + +**Windows:** +```powershell +# Material Design Gallery +.\out\build_develop\examples\gui\material_gallery.exe + +# Button Widget Example +.\out\build_develop\examples\ui\button.exe + +# CPU Information Demo +.\out\build_develop\examples\base\cpu_info.exe +```text + +**Linux:** +```bash +# Material Design Gallery +./out/build_develop/examples/gui/material_gallery + +# Button Widget Example +./out/build_develop/examples/ui/button + +# CPU Information Demo +./out/build_develop/examples/base/cpu_info +```bash + +**可用示例:** + +| 分类 | 示例 | 说明 | +|:-----|:-----|:-----| +| **base/** | cpu_info, memory_info | 系统信息演示 | +| **ui/** | button, label, textfield, checkbox, radiobutton, textarea, groupbox | Material Design 控件 | +| **gui/** | material_gallery, theme | 完整 UI 演示 | + +### 第 5 步:运行测试 + +通过运行测试套件验证构建: + +**Windows:** +```powershell +.\scripts\build_helpers\windows_run_tests.ps1 +```text + +**Linux:** +```bash +bash scripts/build_helpers/linux_run_tests.sh +```text + +**Docker:** +```bash +bash scripts/build_helpers/docker_start.sh --run-project-test +```bash + +--- + +## Windows 用户 + +### 使用 Git Bash + +CFDesktop 的构建脚本为 Unix 风格的 shell 设计。在 Windows 上,请使用 **Git Bash**: + +1. 安装 [Git for Windows](https://git-scm.com/download/win) +2. 从开始菜单打开 Git Bash +3. 导航到项目目录: + ```bash + cd /d/ProjectHome/CFDesktop + ``` + +### 使用 WSL2(替代方案) + +为了获得更好的性能,可以使用 Windows Subsystem for Linux: + +1. 安装 WSL2: + ```powershell + wsl --install + ``` + +2. 从 WSL 中访问 Windows 文件: + ```bash + cd /mnt/d/ProjectHome/CFDesktop + ``` + +### 路径格式转换 + +在 Windows 上运行脚本时,请注意路径格式的差异: + +| 类型 | 格式 | 示例 | +|:-----|:-----|:-----| +| **Windows** | `D:\Path\To\File` | `D:/ProjectHome/CFDesktop` | +| **Git Bash** | `/d/Path/To/File` | `/d/ProjectHome/CFDesktop` | +| **Docker 挂载** | `/d/Path/To/File` | `/d/ProjectHome/CFDesktop` | +| **PowerShell** | `D:\Path\To\File` | `D:\ProjectHome\CFDesktop` | + +构建脚本会自动处理 Docker 挂载的路径转换。 + +### Windows 构建脚本 + +对于原生 Windows 构建(不使用 Docker),请使用 PowerShell 脚本: + +```powershell +# Configure and build (fast) +.\scripts\build_helpers\windows_fast_develop_build.ps1 + +# Configure and build (full clean) +.\scripts\build_helpers\windows_develop_build.ps1 + +# Configure only +.\scripts\build_helpers\windows_configure.ps1 + +# Run tests +.\scripts\build_helpers\windows_run_tests.ps1 +```yaml + +--- + +## 验证 + +### 检查 Docker 安装 + +```bash +# Verify Docker is installed +docker --version + +# Verify Docker daemon is running +docker info +```text + +预期输出: +```text +Docker version 20.10.x +... +Server Version: 20.10.x +```text + +### 检查构建是否成功 + +构建成功后会创建以下目录结构: + +```text +out/build_develop/ +├── bin/ +│ ├── cfbase.dll # Base library (Windows) +│ ├── cfui.dll # UI library (Windows) +│ └── ... +├── lib/ +│ ├── libcfbase.a # Static libraries (Linux) +│ └── ... +├── examples/ +│ ├── base/ # Base examples +│ ├── ui/ # UI widget examples +│ └── gui/ # GUI examples +├── runtimes/ # Qt runtime DLLs (Windows) +└── test/ # Test executables +```text + +### 检查构建日志 + +使用 Docker 时,构建日志保存在 `scripts/docker/logger/` 中: + +```bash +# List recent build logs +ls -lt scripts/docker/logger/ + +# View the latest log +cat scripts/docker/logger/ci_build_*.log | tail -50 +```bash + +--- + +## 后续步骤 + +完成快速开始后: + +1. **阅读构建系统文档**:[`03_build_system.md`](03_build_system.md) +2. **探索示例程序**:浏览 `example/` 目录中的示例代码 +3. **了解项目结构**:查看[项目骨架设计](../design_stage/00_phase0_project_skeleton.md) +4. **配置开发环境**:参考 [VSCode 配置](../design_stage/00_phase0_project_skeleton.md#五开发环境配置) + +--- + +## 常见问题排查 + +### Docker 构建失败 + +| 问题 | 解决方案 | +|:-----|:---------| +| Docker 守护进程未运行 | 启动 Docker Desktop | +| 权限不足 | 在 Linux 上使用 `sudo`,或在 Windows 上以管理员身份运行终端 | +| 端口冲突 | 确保没有其他容器占用所需端口 | +| 内存不足 | 在设置中增加 Docker 内存限制 | + +### 构建错误 + +| 问题 | 解决方案 | +|:-----|---------| +| 未找到 Qt | 设置 `Qt6_DIR` 环境变量,或使用 Docker 构建 | +| 未找到编译器 | 安装 LLVM/Clang 或 GCC,或使用 Docker 构建 | +| CMake 版本过低 | 将 CMake 升级到 3.16+ | +| 缺少依赖 | 使用 Docker 构建,其中包含所有依赖 | + +### Windows 特定问题 + +| 问题 | 解决方案 | +|:-----|---------| +| 路径过长 | 在 Windows 中启用长路径支持,或将项目移到更靠近驱动器根目录的位置 | +| PowerShell 执行策略限制 | 运行 `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser` | +| Git Bash 路径问题 | 使用 `/d/` 格式表示 D: 盘 | + +### 获取帮助 + +如果遇到本文未涵盖的问题: + +1. 查看[设计文档](../../design_stage/)获取详细信息 +2. 参考[构建系统文档](03_build_system.md) +3. 在 [GitHub](https://github.com/Awesome-Embedded-Learning-Studio/CFDesktop/issues) 上提交 issue + +--- + +## 快速参考 + +### 常用命令 + +```bash +# Clone and setup +git clone https://github.com/Awesome-Embedded-Learning-Studio/CFDesktop.git +cd CFDesktop + +# Docker build (fastest) +bash scripts/build_helpers/docker_start.sh --fast-build --build-project-fast + +# Run tests +bash scripts/build_helpers/docker_start.sh --run-project-test + +# Interactive shell in Docker +bash scripts/build_helpers/docker_start.sh + +# Build specific architecture +bash scripts/build_helpers/docker_start.sh --arch arm64 --verify +```bash + +### 配置文件 + +| 文件 | 用途 | +|:-----|:-----| +| `build_develop_config.ini` | 开发构建配置 | +| `build_deploy_config.ini` | 部署构建配置 | +| `build_ci_config.ini` | CI 构建配置 | + +### 输出目录 + +| 构建类型 | 输出目录 | +|:---------|:---------| +| 开发 | `out/build_develop/` | +| 部署 | `out/build_deploy/` | +| CI | `out/build_ci/` | + +--- + +**Last Updated**: 2026-03-07 diff --git a/document/development/03_build_system.md b/document/development/03_build_system.md index 53069a06e..5af5bffdd 100644 --- a/document/development/03_build_system.md +++ b/document/development/03_build_system.md @@ -1,681 +1,686 @@ -# Build System Documentation - -> Comprehensive guide to the CFDesktop CMake build system - -## Table of Contents - -- [Overview](#overview) -- [CMake Architecture](#cmake-architecture) -- [Project Modules](#project-modules) -- [Build Types](#build-types) -- [Toolchain Configuration](#toolchain-configuration) -- [Build Scripts](#build-scripts) -- [Output Directories](#output-directories) -- [Common Build Options](#common-build-options) -- [Advanced Usage](#advanced-usage) - ---- - -## Overview - -CFDesktop uses a modular CMake-based build system designed for cross-platform development and embedded deployment. The build system supports: - -- **Multiple platforms**: Windows (MinGW/LLVM), Linux (GCC/Clang) -- **Multiple architectures**: x86_64, ARM64, ARMhf -- **Build types**: Debug, Release, RelWithDebInfo -- **Containerized builds**: Docker with multi-architecture support - -### Build System Diagram - -``` -CFDesktop Build System -├── Configuration Files (.ini) -│ ├── build_develop_config.ini (Debug builds) -│ ├── build_deploy_config.ini (Release builds) -│ └── build_ci_config.ini (CI builds) -│ -├── Build Scripts -│ ├── windows_*.ps1 (PowerShell scripts) -│ ├── linux_*.sh (Bash scripts) -│ └── docker_start.sh (Docker wrapper) -│ -├── CMake Modules -│ ├── check_toolchain.cmake (Toolchain selection) -│ ├── OutputDirectoryConfig.cmake (Output directory management) -│ └── generate_develop_helpers.cmake (IDE config generation) -│ -└── Toolchains - ├── windows/llvm-toolchain.cmake - ├── windows/gcc-toolchain.cmake - ├── linux/ci-x86_64-toolchain.cmake - └── linux/ci-aarch64-toolchain.cmake -``` - ---- - -## CMake Architecture - -### Root CMakeLists.txt - -The root `CMakeLists.txt` is the entry point for the build system: - -```cmake -cmake_minimum_required(VERSION 3.16) -project(CFDesktop VERSION 0.9.0 LANGUAGES CXX) - -# Toolchain configuration (supports shorthand: -DUSE_TOOLCHAIN=windows/llvm) -include(cmake/check_toolchain.cmake) - -# Build type validation -set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "RelWithDebInfo") - -# Compiler flags per build type -set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g" CACHE STRING "" FORCE) -set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG" CACHE STRING "" FORCE) -set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g -DNDEBUG" CACHE STRING "" FORCE) - -# Output directories -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") - -# Qt6 dependency -find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) - -# Subdirectories -add_subdirectory(base) -add_subdirectory(ui) -add_subdirectory(example) -add_subdirectory(test) -``` - -### CMake Module Structure - -``` -cmake/ -├── build_log_helper.cmake # Logging utilities -├── check_toolchain.cmake # Toolchain selection -├── custom_target_helper.cmake # Custom target helpers -├── OutputDirectoryConfig.cmake # Output directory management -├── ExampleLauncher.cmake # Windows launcher generation -├── QtDeployUtils.cmake # Qt deployment utilities -└── generate_develop_helpers.cmake # IDE configuration generation -``` - ---- - -## Project Modules - -### Module Overview - -| Module | Description | Output | Dependencies | -|:-------|:------------|:-------|:-------------| -| **base/** | Base utilities and platform abstractions | `cfbase.dll` / `libcfbase.so` | Qt6::Core | -| **ui/** | UI framework and components | `cfui.dll` / `libcfui.so` | base, Qt6::Core, Qt6::Gui | -| **example/** | Example programs | `examples/{category}/` | base, ui | -| **test/** | Unit tests | `test/` | base, ui, GoogleTest | - -### Base Module - -The base module provides fundamental utilities: - -``` -base/ -├── include/ # Header-only utilities -│ ├── cfbase/ -│ │ ├── expected.hpp # std::expected-like type -│ │ ├── scope_guard.hpp # RAII resource management -│ │ └── weak_ptr.hpp # Weak pointer utilities -│ └── CFDesktop/ -│ └── Base/ -│ └── system/ -│ ├── cpu/ # CPU detection -│ └── memory/ # Memory detection -└── system/ - ├── cpu/CMakeLists.txt # CPU module - └── memory/CMakeLists.txt # Memory module -``` - -**Unified Base Library:** -All base components are linked into a single shared library (`cfbase.dll` on Windows, `libcfbase.so` on Linux). - -```cmake -# base/CMakeLists.txt -add_library(cfbase SHARED) -target_sources(cfbase PRIVATE - $ - $ -) -target_link_libraries(cfbase PUBLIC Qt6::Core) -``` - -### UI Module - -The UI module provides the Material Design framework: - -``` -ui/ -├── base/ # Math utilities -│ ├── math_helper.hpp -│ ├── color_helper.hpp -│ ├── geometry_helper.hpp -│ └── easing.hpp -├── core/ # Theme engine -│ ├── theme/ -│ ├── color_scheme/ -│ ├── motion_spec/ -│ └── typography/ -├── components/ # Animation system -│ ├── animation/ -│ ├── timing_animation/ -│ └── spring_animation/ -└── widget/ # Widget adapters - └── material/ - ├── button/ - ├── label/ - ├── textfield/ - └── ... -``` - -**Unified UI Library:** -All UI components are linked into a single shared library (`cfui.dll` on Windows, `libcfui.so` on Linux). - -```cmake -# ui/CMakeLists.txt -add_library(cfui SHARED) -target_link_libraries(cfui PUBLIC - cf_ui_core - cf_ui_base - cf_ui_components - cf_ui_widget - CFDesktop::base - Qt6::Core - Qt6::Gui -) -``` - ---- - -## Build Types - -### Available Build Types - -| Build Type | Optimization | Debug Info | Use Case | -|:-----------|:------------|:----------:|:---------| -| **Debug** | `-O0` | Full (`-g`) | Development and debugging | -| **Release** | `-O3` | None | Production deployment | -| **RelWithDebInfo** | `-O2` | Full (`-g`) | Profiling and testing | - -### Build Type Selection - -Build types are configured via `.ini` files: - -```ini -[cmake] -build_type=Debug # or Release, RelWithDebInfo -``` - -**Configuration Files:** - -| File | Build Type | Use Case | -|:-----|:-----------|:---------| -| `build_develop_config.ini` | Debug | Daily development | -| `build_deploy_config.ini` | Release | Production builds | -| `build_ci_config.ini` | Release | CI/CD pipeline | - ---- - -## Toolchain Configuration - -### Toolchain Shorthand - -CFDesktop supports a shorthand notation for toolchain selection: - -```bash -cmake -DUSE_TOOLCHAIN=windows/llvm -S . -B build -cmake -DUSE_TOOLCHAIN=windows/gcc -S . -B build -cmake -DUSE_TOOLCHAIN=linux/ci-x86_64 -S . -B build -``` - -### Available Toolchains - -| Platform | Toolchain | Shorthand | Compiler | -|:---------|:----------|:----------|:---------| -| **Windows** | LLVM-MinGW | `windows/llvm` | clang/LLVM | -| **Windows** | MinGW-GCC | `windows/gcc` | gcc/MinGW | -| **Linux** | CI x86_64 | `linux/ci-x86_64` | gcc (Docker) | -| **Linux** | CI ARM64 | `linux/ci-aarch64` | aarch64 gcc (Docker) | - -### Toolchain File Structure - -Toolchain files are located in `cmake/cmake_toolchain/{platform}/`: - -``` -cmake/cmake_toolchain/ -├── windows/ -│ ├── llvm-toolchain.cmake -│ └── gcc-toolchain.cmake -└── linux/ - ├── ci-x86_64-toolchain.cmake - └── ci-aarch64-toolchain.cmake -``` - -### Windows LLVM-MinGW Toolchain - -```cmake -# cmake/cmake_toolchain/windows/llvm-toolchain.cmake -set(CMAKE_PREFIX_PATH "D:/QT/Qt6.6.0/6.8.3/llvm-mingw_64") -set(CMAKE_C_COMPILER "D:/QT/Qt6.6.0/Tools/llvm-mingw1706_64/bin/gcc.exe") -set(CMAKE_CXX_COMPILER "D:/QT/Qt6.6.0/Tools/llvm-mingw1706_64/bin/g++.exe") -``` - -### Linux CI Toolchain - -```cmake -# cmake/cmake_toolchain/linux/ci-x86_64-toolchain.cmake -set(CMAKE_SYSTEM_NAME Linux) -set(QT6_BASE_DIR "/opt/Qt/6.8.1/gcc_64") -set(Qt6_DIR "${QT6_BASE_DIR}/lib/cmake/Qt6") -set(CMAKE_PREFIX_PATH "${QT6_BASE_DIR}") -``` - ---- - -## Build Scripts - -### Script Overview - -Build scripts are organized by platform and purpose: - -``` -scripts/build_helpers/ -├── windows_*.ps1 # Windows PowerShell scripts -│ ├── windows_configure.ps1 -│ ├── windows_fast_develop_build.ps1 -│ ├── windows_develop_build.ps1 -│ └── windows_run_tests.ps1 -├── linux_*.sh # Linux Bash scripts -│ ├── linux_configure.sh -│ ├── linux_fast_develop_build.sh -│ ├── linux_develop_build.sh -│ └── linux_run_tests.sh -└── docker_start.sh # Docker wrapper script -``` - -### Windows Build Scripts - -#### Configure Script - -```powershell -# Configure only (no build) -.\scripts\build_helpers\windows_configure.ps1 [-Config ] -``` - -**What it does:** -1. Loads configuration from `.ini` file -2. Validates build type -3. Runs CMake configuration -4. Generates build files - -#### Fast Build Script - -```powershell -# Fast incremental build -.\scripts\build_helpers\windows_fast_develop_build.ps1 -``` - -**What it does:** -1. Calls configure script -2. Builds with CMake (no clean) -3. Uses parallel jobs - -#### Full Build Script - -```powershell -# Full clean build -.\scripts\build_helpers\windows_develop_build.ps1 -``` - -**What it does:** -1. Cleans build directory -2. Calls fast build script -3. Runs tests - -### Linux Build Scripts - -#### Configure Script - -```bash -# Configure only -bash scripts/build_helpers/linux_configure.sh [develop|deploy|ci] [-c ] -``` - -#### Fast Build Script - -```bash -# Fast incremental build -bash scripts/build_helpers/linux_fast_develop_build.sh [develop|deploy|ci] -``` - -#### Full Build Script - -```bash -# Full clean build -bash scripts/build_helpers/linux_develop_build.sh [develop|deploy|ci] -``` - -### Docker Build Script - -```bash -# Interactive shell -bash scripts/build_helpers/docker_start.sh - -# Fast build -bash scripts/build_helpers/docker_start.sh --fast-build --build-project-fast - -# Full build -bash scripts/build_helpers/docker_start.sh --build-project - -# Run tests -bash scripts/build_helpers/docker_start.sh --run-project-test - -# CI verification -bash scripts/build_helpers/docker_start.sh --verify - -# ARM64 build -bash scripts/build_helpers/docker_start.sh --arch arm64 --verify -``` - -**Docker Options:** - -| Option | Description | -|:-------|:------------| -| `--arch amd64|arm64` | Target architecture | -| `--fast-build` | Reuse existing image | -| `--verify` | Run CI verification | -| `--build-project` | Full clean build | -| `--build-project-fast` | Fast incremental build | -| `--run-project-test` | Run tests | -| `--stay-on-error` | Keep container on error | -| `--no-log` | Disable file logging | -| `--no-deps` | Skip dependency installation | - ---- - -## Output Directories - -### Output Directory Structure - -``` -out/build_{config}/ -├── bin/ # Executables and shared libraries -│ ├── cfbase.dll # Base library (Windows) -│ ├── cfui.dll # UI library (Windows) -│ └── ... -├── lib/ # Static libraries -│ ├── libcfbase.a -│ └── ... -├── examples/ # Example programs -│ ├── base/ # Base examples -│ │ ├── cpu_info -│ │ └── memory_info -│ ├── ui/ # UI widget examples -│ │ ├── button -│ │ ├── label -│ │ └── ... -│ └── gui/ # GUI examples -│ ├── material_gallery -│ └── theme -├── plugins/ # Qt plugins -├── resources/ # Resource files -├── runtimes/ # Qt runtime DLLs (Windows) -└── test/ # Test executables - ├── base_test - ├── ui_test - └── ... -``` - -### Output Directory Configuration - -Output directories are configured in `cmake/OutputDirectoryConfig.cmake`: - -```cmake -# Global output directories -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") - -# Example-specific outputs -function(cf_set_example_output_dir TARGET_NAME CATEGORY) - set_target_properties(${TARGET_NAME} PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/examples/${CATEGORY}" - ) -endfunction() -``` - ---- - -## Common Build Options - -### Configuration File Options - -Configuration files use INI format: - -```ini -[cmake] -# CMake generator -generator=MinGW Makefiles # or "Unix Makefiles", "Ninja" - -# Toolchain selection -toolchain=windows/llvm # or windows/gcc, linux/ci-x86_64 - -# Build type -build_type=Debug # or Release, RelWithDebInfo - -[paths] -# Source directory (relative to project root) -source=. - -# Build output directory (relative to project root) -build_dir=out/build_develop - -[options] -# Parallel jobs for compilation -jobs=16 -``` - -### CMake Options - -| Option | Description | Default | -|:-------|:------------|:--------| -| `CMAKE_BUILD_TYPE` | Build type (Debug/Release/RelWithDebInfo) | Required | -| `CMAKE_PREFIX_PATH` | Qt installation path | From toolchain | -| `USE_TOOLCHAIN` | Toolchain shorthand | Required | -| `CMAKE_EXPORT_COMPILE_COMMANDS` | Generate compile_commands.json | ON | -| `BUILD_TESTING` | Build tests | ON | - ---- - -## Advanced Usage - -### Custom Toolchain Configuration - -To use a custom toolchain: - -1. Create a toolchain file in `cmake/cmake_toolchain/{platform}/` -2. Use the shorthand notation: - -```bash -cmake -DUSE_TOOLCHAIN=windows/mytoolchain -S . -B build -``` - -Or use the full path: - -```bash -cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/toolchain.cmake -S . -B build -``` - -### Incremental Builds - -For faster development, use incremental builds: - -**Linux:** -```bash -bash scripts/build_helpers/linux_fast_develop_build.sh -``` - -**Windows:** -```powershell -.\scripts\build_helpers\windows_fast_develop_build.ps1 -``` - -### Parallel Builds - -Control the number of parallel jobs via the configuration file: - -```ini -[options] -jobs=8 # Use 8 parallel jobs -``` - -Or via CMake: - -```bash -cmake --build build --parallel 8 -``` - -### Building Specific Targets - -To build specific targets: - -```bash -cmake --build build --target cfbase -cmake --build build --target cfui -cmake --build build --target material_gallery -``` - -### Clean Builds - -For a completely clean build: - -**Linux:** -```bash -bash scripts/build_helpers/linux_develop_build.sh -``` - -**Windows:** -```powershell -.\scripts\build_helpers\windows_develop_build.ps1 -``` - -Or manually: - -```bash -rm -rf out/build_develop -cmake -DUSE_TOOLCHAIN=windows/llvm -DCMAKE_BUILD_TYPE=Debug -S . -B out/build_develop -cmake --build out/build_develop -``` - ---- - -## Cross-Platform Build Matrix - -### Supported Platforms - -| Platform | Architecture | Toolchain | Status | -|:---------|:------------:|:----------|:------:| -| Windows 10+ | x86_64 | LLVM-MinGW | Supported | -| Windows 10+ | x86_64 | MinGW-GCC | Supported | -| Linux | x86_64 | GCC | Supported | -| Linux | x86_64 | Clang | Supported | -| Linux (Docker) | x86_64 | GCC | Supported | -| Linux (Docker) | ARM64 | aarch64 gcc | Supported | - -### Build Command Reference - -| Platform | Command | -|:---------|:---------| -| **Windows (LLVM)** | `.\scripts\build_helpers\windows_fast_develop_build.ps1` | -| **Windows (GCC)** | Edit `build_develop_config.ini`: `toolchain=windows/gcc` | -| **Linux (Native)** | `bash scripts/build_helpers/linux_fast_develop_build.sh` | -| **Linux (Docker)** | `bash scripts/build_helpers/docker_start.sh --build-project-fast` | -| **ARM64 (Docker)** | `bash scripts/build_helpers/docker_start.sh --arch arm64 --build-project-fast` | - ---- - -## IDE Integration - -### VSCode Configuration - -The build system automatically generates VSCode configuration files: - -- `.vscode/launch.json` - Debug configurations -- `.clangd` - Clangd language server configuration -- `compile_commands.json` - Compilation database - -These are generated by `cmake/generate_develop_helpers.cmake` during CMake configuration. - -### QtCreator - -QtCreator can open the project directly: - -1. File -> Open File or Project -2. Select `CMakeLists.txt` -3. Configure the build directory -4. Choose the toolchain -5. Click "Run CMake" - ---- - -## Troubleshooting - -### Common Issues - -| Issue | Solution | -|:------|:----------| -| CMake not found | Install CMake 3.16+ or add to PATH | -| Qt not found | Set `Qt6_DIR` or use correct toolchain | -| Compiler not found | Install compiler or use Docker build | -| Permission denied | Use `sudo` on Linux or run as admin on Windows | -| Out of memory during build | Reduce parallel jobs in `.ini` file | - -### Debug Mode Builds - -For debugging, ensure you're using the Debug build type: - -```ini -[cmake] -build_type=Debug -``` - -This will: -- Disable optimizations (`-O0`) -- Include full debug symbols (`-g`) -- Enable assertions - -### Release Builds - -For production: - -```ini -[cmake] -build_type=Release -``` - -This will: -- Enable maximum optimizations (`-O3`) -- Disable debug info -- Define `NDEBUG` - ---- - -## Related Documentation - -- [Quick Start Guide](02_quick_start.md) - Get started in 30 minutes -- [Project Skeleton Design](../design_stage/00_phase0_project_skeleton.md) - Detailed project architecture -- [Base Library Design](../design_stage/02_phase2_base_library.md) - Base module documentation -- [UI Framework Design](../todo/base/99_ui_material_framework.md) - UI module documentation - ---- - -**Last Updated**: 2026-03-07 +--- +title: 构建系统文档 +description: CFDesktop CMake 构建系统完整指南 +--- + +# 构建系统文档 + +> CFDesktop CMake 构建系统完整指南 + +## 目录 + +- [概述](#概述) +- [CMake 架构](#cmake-架构) +- [项目模块](#项目模块) +- [构建类型](#构建类型) +- [工具链配置](#工具链配置) +- [构建脚本](#构建脚本) +- [输出目录](#输出目录) +- [常用构建选项](#常用构建选项) +- [高级用法](#高级用法) + +--- + +## 概述 + +CFDesktop 使用基于 CMake 的模块化构建系统,专为跨平台开发和嵌入式部署而设计。该构建系统支持: + +- **多平台**:Windows(MinGW/LLVM)、Linux(GCC/Clang) +- **多架构**:x86_64、ARM64、ARMhf +- **构建类型**:Debug、Release、RelWithDebInfo +- **容器化构建**:Docker 多架构支持 + +### 构建系统示意图 + +```text +CFDesktop Build System +├── Configuration Files (.ini) +│ ├── build_develop_config.ini (Debug builds) +│ ├── build_deploy_config.ini (Release builds) +│ └── build_ci_config.ini (CI builds) +│ +├── Build Scripts +│ ├── windows_*.ps1 (PowerShell scripts) +│ ├── linux_*.sh (Bash scripts) +│ └── docker_start.sh (Docker wrapper) +│ +├── CMake Modules +│ ├── check_toolchain.cmake (Toolchain selection) +│ ├── OutputDirectoryConfig.cmake (Output directory management) +│ └── generate_develop_helpers.cmake (IDE config generation) +│ +└── Toolchains + ├── windows/llvm-toolchain.cmake + ├── windows/gcc-toolchain.cmake + ├── linux/ci-x86_64-toolchain.cmake + └── linux/ci-aarch64-toolchain.cmake +```yaml + +--- + +## CMake 架构 + +### 根 CMakeLists.txt + +根 `CMakeLists.txt` 是构建系统的入口点: + +```cmake +cmake_minimum_required(VERSION 3.16) +project(CFDesktop VERSION 0.9.0 LANGUAGES CXX) + +# Toolchain configuration (supports shorthand: -DUSE_TOOLCHAIN=windows/llvm) +include(cmake/check_toolchain.cmake) + +# Build type validation +set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "RelWithDebInfo") + +# Compiler flags per build type +set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g" CACHE STRING "" FORCE) +set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG" CACHE STRING "" FORCE) +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g -DNDEBUG" CACHE STRING "" FORCE) + +# Output directories +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") + +# Qt6 dependency +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) + +# Subdirectories +add_subdirectory(base) +add_subdirectory(ui) +add_subdirectory(example) +add_subdirectory(test) +```text + +### CMake 模块结构 + +```text +cmake/ +├── build_log_helper.cmake # Logging utilities +├── check_toolchain.cmake # Toolchain selection +├── custom_target_helper.cmake # Custom target helpers +├── OutputDirectoryConfig.cmake # Output directory management +├── ExampleLauncher.cmake # Windows launcher generation +├── QtDeployUtils.cmake # Qt deployment utilities +└── generate_develop_helpers.cmake # IDE configuration generation +```bash + +--- + +## 项目模块 + +### 模块概览 + +| 模块 | 描述 | 输出 | 依赖 | +|:-----|:-----|:-----|:-----| +| **base/** | 基础工具和平台抽象 | `cfbase.dll` / `libcfbase.so` | Qt6::Core | +| **ui/** | UI 框架和组件 | `cfui.dll` / `libcfui.so` | base、Qt6::Core、Qt6::Gui | +| **example/** | 示例程序 | `examples/{category}/` | base、ui | +| **test/** | 单元测试 | `test/` | base、ui、GoogleTest | + +### Base 模块 + +Base 模块提供基础工具: + +```text +base/ +├── include/ # Header-only utilities +│ ├── cfbase/ +│ │ ├── expected.hpp # std::expected-like type +│ │ ├── scope_guard.hpp # RAII resource management +│ │ └── weak_ptr.hpp # Weak pointer utilities +│ └── CFDesktop/ +│ └── Base/ +│ └── system/ +│ ├── cpu/ # CPU detection +│ └── memory/ # Memory detection +└── system/ + ├── cpu/CMakeLists.txt # CPU module + └── memory/CMakeLists.txt # Memory module +```text + +**统一 Base 库:** +所有 base 组件链接为一个单独的共享库(Windows 上为 `cfbase.dll`,Linux 上为 `libcfbase.so`)。 + +```cmake +# base/CMakeLists.txt +add_library(cfbase SHARED) +target_sources(cfbase PRIVATE + $ + $ +) +target_link_libraries(cfbase PUBLIC Qt6::Core) +```text + +### UI 模块 + +UI 模块提供 Material Design 框架: + +```text +ui/ +├── base/ # Math utilities +│ ├── math_helper.hpp +│ ├── color_helper.hpp +│ ├── geometry_helper.hpp +│ └── easing.hpp +├── core/ # Theme engine +│ ├── theme/ +│ ├── color_scheme/ +│ ├── motion_spec/ +│ └── typography/ +├── components/ # Animation system +│ ├── animation/ +│ ├── timing_animation/ +│ └── spring_animation/ +└── widget/ # Widget adapters + └── material/ + ├── button/ + ├── label/ + ├── textfield/ + └── ... +```text + +**统一 UI 库:** +所有 UI 组件链接为一个单独的共享库(Windows 上为 `cfui.dll`,Linux 上为 `libcfui.so`)。 + +```cmake +# ui/CMakeLists.txt +add_library(cfui SHARED) +target_link_libraries(cfui PUBLIC + cf_ui_core + cf_ui_base + cf_ui_components + cf_ui_widget + CFDesktop::base + Qt6::Core + Qt6::Gui +) +```bash + +--- + +## 构建类型 + +### 可用的构建类型 + +| 构建类型 | 优化级别 | 调试信息 | 使用场景 | +|:---------|:---------|:--------:|:---------| +| **Debug** | `-O0` | 完整(`-g`) | 开发与调试 | +| **Release** | `-O3` | 无 | 生产部署 | +| **RelWithDebInfo** | `-O2` | 完整(`-g`) | 性能分析和测试 | + +### 构建类型选择 + +构建类型通过 `.ini` 文件进行配置: + +```ini +[cmake] +build_type=Debug # or Release, RelWithDebInfo +```bash + +**配置文件:** + +| 文件 | 构建类型 | 使用场景 | +|:-----|:---------|:---------| +| `build_develop_config.ini` | Debug | 日常开发 | +| `build_deploy_config.ini` | Release | 生产构建 | +| `build_ci_config.ini` | Release | CI/CD 流水线 | + +--- + +## 工具链配置 + +### 工具链简写 + +CFDesktop 支持工具链选择的简写表示法: + +```bash +cmake -DUSE_TOOLCHAIN=windows/llvm -S . -B build +cmake -DUSE_TOOLCHAIN=windows/gcc -S . -B build +cmake -DUSE_TOOLCHAIN=linux/ci-x86_64 -S . -B build +```bash + +### 可用工具链 + +| 平台 | 工具链 | 简写 | 编译器 | +|:-----|:-------|:-----|:-------| +| **Windows** | LLVM-MinGW | `windows/llvm` | clang/LLVM | +| **Windows** | MinGW-GCC | `windows/gcc` | gcc/MinGW | +| **Linux** | CI x86_64 | `linux/ci-x86_64` | gcc (Docker) | +| **Linux** | CI ARM64 | `linux/ci-aarch64` | aarch64 gcc (Docker) | + +### 工具链文件结构 + +工具链文件位于 `cmake/cmake_toolchain/{platform}/`: + +```text +cmake/cmake_toolchain/ +├── windows/ +│ ├── llvm-toolchain.cmake +│ └── gcc-toolchain.cmake +└── linux/ + ├── ci-x86_64-toolchain.cmake + └── ci-aarch64-toolchain.cmake +```text + +### Windows LLVM-MinGW 工具链 + +```cmake +# cmake/cmake_toolchain/windows/llvm-toolchain.cmake +set(CMAKE_PREFIX_PATH "D:/QT/Qt6.6.0/6.8.3/llvm-mingw_64") +set(CMAKE_C_COMPILER "D:/QT/Qt6.6.0/Tools/llvm-mingw1706_64/bin/gcc.exe") +set(CMAKE_CXX_COMPILER "D:/QT/Qt6.6.0/Tools/llvm-mingw1706_64/bin/g++.exe") +```text + +### Linux CI 工具链 + +```cmake +# cmake/cmake_toolchain/linux/ci-x86_64-toolchain.cmake +set(CMAKE_SYSTEM_NAME Linux) +set(QT6_BASE_DIR "/opt/Qt/6.8.1/gcc_64") +set(Qt6_DIR "${QT6_BASE_DIR}/lib/cmake/Qt6") +set(CMAKE_PREFIX_PATH "${QT6_BASE_DIR}") +```yaml + +--- + +## 构建脚本 + +### 脚本概览 + +构建脚本按平台和用途组织: + +```text +scripts/build_helpers/ +├── windows_*.ps1 # Windows PowerShell scripts +│ ├── windows_configure.ps1 +│ ├── windows_fast_develop_build.ps1 +│ ├── windows_develop_build.ps1 +│ └── windows_run_tests.ps1 +├── linux_*.sh # Linux Bash scripts +│ ├── linux_configure.sh +│ ├── linux_fast_develop_build.sh +│ ├── linux_develop_build.sh +│ └── linux_run_tests.sh +└── docker_start.sh # Docker wrapper script +```text + +### Windows 构建脚本 + +#### 配置脚本 + +```powershell +# Configure only (no build) +.\scripts\build_helpers\windows_configure.ps1 [-Config ] +```text + +**该脚本执行以下操作:** +1. 从 `.ini` 文件加载配置 +2. 验证构建类型 +3. 运行 CMake 配置 +4. 生成构建文件 + +#### 快速构建脚本 + +```powershell +# Fast incremental build +.\scripts\build_helpers\windows_fast_develop_build.ps1 +```text + +**该脚本执行以下操作:** +1. 调用配置脚本 +2. 使用 CMake 构建(不清理) +3. 使用并行任务 + +#### 完整构建脚本 + +```powershell +# Full clean build +.\scripts\build_helpers\windows_develop_build.ps1 +```text + +**该脚本执行以下操作:** +1. 清理构建目录 +2. 调用快速构建脚本 +3. 运行测试 + +### Linux 构建脚本 + +#### 配置脚本 + +```bash +# Configure only +bash scripts/build_helpers/linux_configure.sh [develop|deploy|ci] [-c ] +```text + +#### 快速构建脚本 + +```bash +# Fast incremental build +bash scripts/build_helpers/linux_fast_develop_build.sh [develop|deploy|ci] +```text + +#### 完整构建脚本 + +```bash +# Full clean build +bash scripts/build_helpers/linux_develop_build.sh [develop|deploy|ci] +```text + +### Docker 构建脚本 + +```bash +# Interactive shell +bash scripts/build_helpers/docker_start.sh + +# Fast build +bash scripts/build_helpers/docker_start.sh --fast-build --build-project-fast + +# Full build +bash scripts/build_helpers/docker_start.sh --build-project + +# Run tests +bash scripts/build_helpers/docker_start.sh --run-project-test + +# CI verification +bash scripts/build_helpers/docker_start.sh --verify + +# ARM64 build +bash scripts/build_helpers/docker_start.sh --arch arm64 --verify +```bash + +**Docker 选项:** + +| 选项 | 描述 | +|:-----|:-----| +| `--arch amd64\|arm64` | 目标架构 | +| `--fast-build` | 复用已有镜像 | +| `--verify` | 运行 CI 验证 | +| `--build-project` | 完整清理构建 | +| `--build-project-fast` | 快速增量构建 | +| `--run-project-test` | 运行测试 | +| `--stay-on-error` | 出错时保留容器 | +| `--no-log` | 禁用文件日志 | +| `--no-deps` | 跳过依赖安装 | + +--- + +## 输出目录 + +### 输出目录结构 + +```text +out/build_{config}/ +├── bin/ # Executables and shared libraries +│ ├── cfbase.dll # Base library (Windows) +│ ├── cfui.dll # UI library (Windows) +│ └── ... +├── lib/ # Static libraries +│ ├── libcfbase.a +│ └── ... +├── examples/ # Example programs +│ ├── base/ # Base examples +│ │ ├── cpu_info +│ │ └── memory_info +│ ├── ui/ # UI widget examples +│ │ ├── button +│ │ ├── label +│ │ └── ... +│ └── gui/ # GUI examples +│ ├── material_gallery +│ └── theme +├── plugins/ # Qt plugins +├── resources/ # Resource files +├── runtimes/ # Qt runtime DLLs (Windows) +└── test/ # Test executables + ├── base_test + ├── ui_test + └── ... +```text + +### 输出目录配置 + +输出目录在 `cmake/OutputDirectoryConfig.cmake` 中配置: + +```cmake +# Global output directories +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") + +# Example-specific outputs +function(cf_set_example_output_dir TARGET_NAME CATEGORY) + set_target_properties(${TARGET_NAME} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/examples/${CATEGORY}" + ) +endfunction() +```yaml + +--- + +## 常用构建选项 + +### 配置文件选项 + +配置文件使用 INI 格式: + +```ini +[cmake] +# CMake generator +generator=MinGW Makefiles # or "Unix Makefiles", "Ninja" + +# Toolchain selection +toolchain=windows/llvm # or windows/gcc, linux/ci-x86_64 + +# Build type +build_type=Debug # or Release, RelWithDebInfo + +[paths] +# Source directory (relative to project root) +source=. + +# Build output directory (relative to project root) +build_dir=out/build_develop + +[options] +# Parallel jobs for compilation +jobs=16 +```bash + +### CMake 选项 + +| 选项 | 描述 | 默认值 | +|:-----|:-----|:-------| +| `CMAKE_BUILD_TYPE` | 构建类型(Debug/Release/RelWithDebInfo) | 必填 | +| `CMAKE_PREFIX_PATH` | Qt 安装路径 | 由工具链提供 | +| `USE_TOOLCHAIN` | 工具链简写 | 必填 | +| `CMAKE_EXPORT_COMPILE_COMMANDS` | 生成 compile_commands.json | ON | +| `BUILD_TESTING` | 构建测试 | ON | + +--- + +## 高级用法 + +### 自定义工具链配置 + +要使用自定义工具链: + +1. 在 `cmake/cmake_toolchain/{platform}/` 中创建工具链文件 +2. 使用简写表示法: + +```bash +cmake -DUSE_TOOLCHAIN=windows/mytoolchain -S . -B build +```text + +或使用完整路径: + +```bash +cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/toolchain.cmake -S . -B build +```text + +### 增量构建 + +为了加速开发,可使用增量构建: + +**Linux:** +```bash +bash scripts/build_helpers/linux_fast_develop_build.sh +```text + +**Windows:** +```powershell +.\scripts\build_helpers\windows_fast_develop_build.ps1 +```text + +### 并行构建 + +通过配置文件控制并行任务数: + +```ini +[options] +jobs=8 # Use 8 parallel jobs +```text + +或通过 CMake: + +```bash +cmake --build build --parallel 8 +```text + +### 构建特定目标 + +要构建特定目标: + +```bash +cmake --build build --target cfbase +cmake --build build --target cfui +cmake --build build --target material_gallery +```text + +### 清理构建 + +要执行完全清理构建: + +**Linux:** +```bash +bash scripts/build_helpers/linux_develop_build.sh +```text + +**Windows:** +```powershell +.\scripts\build_helpers\windows_develop_build.ps1 +```text + +或手动执行: + +```bash +rm -rf out/build_develop +cmake -DUSE_TOOLCHAIN=windows/llvm -DCMAKE_BUILD_TYPE=Debug -S . -B out/build_develop +cmake --build out/build_develop +```cpp + +--- + +## 跨平台构建矩阵 + +### 支持的平台 + +| 平台 | 架构 | 工具链 | 状态 | +|:-----|:----:|:-------|:----:| +| Windows 10+ | x86_64 | LLVM-MinGW | 已支持 | +| Windows 10+ | x86_64 | MinGW-GCC | 已支持 | +| Linux | x86_64 | GCC | 已支持 | +| Linux | x86_64 | Clang | 已支持 | +| Linux (Docker) | x86_64 | GCC | 已支持 | +| Linux (Docker) | ARM64 | aarch64 gcc | 已支持 | + +### 构建命令参考 + +| 平台 | 命令 | +|:-----|:-----| +| **Windows (LLVM)** | `.\scripts\build_helpers\windows_fast_develop_build.ps1` | +| **Windows (GCC)** | 编辑 `build_develop_config.ini`:`toolchain=windows/gcc` | +| **Linux(原生)** | `bash scripts/build_helpers/linux_fast_develop_build.sh` | +| **Linux (Docker)** | `bash scripts/build_helpers/docker_start.sh --build-project-fast` | +| **ARM64 (Docker)** | `bash scripts/build_helpers/docker_start.sh --arch arm64 --build-project-fast` | + +--- + +## IDE 集成 + +### VSCode 配置 + +构建系统会自动生成 VSCode 配置文件: + +- `.vscode/launch.json` - 调试配置 +- `.clangd` - Clangd 语言服务器配置 +- `compile_commands.json` - 编译数据库 + +这些文件由 `cmake/generate_develop_helpers.cmake` 在 CMake 配置阶段生成。 + +### QtCreator + +QtCreator 可以直接打开项目: + +1. 文件 -> 打开文件或项目 +2. 选择 `CMakeLists.txt` +3. 配置构建目录 +4. 选择工具链 +5. 点击"运行 CMake" + +--- + +## 常见问题排查 + +### 常见问题 + +| 问题 | 解决方案 | +|:-----|:---------| +| 找不到 CMake | 安装 CMake 3.16+ 或将其添加到 PATH | +| 找不到 Qt | 设置 `Qt6_DIR` 或使用正确的工具链 | +| 找不到编译器 | 安装编译器或使用 Docker 构建 | +| 权限被拒绝 | 在 Linux 上使用 `sudo`,或在 Windows 上以管理员身份运行 | +| 构建时内存不足 | 在 `.ini` 文件中减少并行任务数 | + +### Debug 模式构建 + +用于调试时,请确保使用 Debug 构建类型: + +```ini +[cmake] +build_type=Debug +```text + +这将: +- 禁用优化(`-O0`) +- 包含完整的调试符号(`-g`) +- 启用断言 + +### Release 构建 + +用于生产部署: + +```ini +[cmake] +build_type=Release +```yaml + +这将: +- 启用最大优化(`-O3`) +- 禁用调试信息 +- 定义 `NDEBUG` + +--- + +## 相关文档 + +- [快速入门指南](02_quick_start.md) - 30 分钟上手 +- [项目骨架设计](../design_stage/00_phase0_project_skeleton.md) - 详细的项目架构 +- [Base 库设计](../design_stage/02_phase2_base_library.md) - Base 模块文档 +- [UI 框架设计](../todo/base/99_ui_material_framework.md) - UI 模块文档 + +--- + +**Last Updated**: 2026-03-07 diff --git a/document/development/04_development_tools.md b/document/development/04_development_tools.md index c084030a9..33e41589e 100644 --- a/document/development/04_development_tools.md +++ b/document/development/04_development_tools.md @@ -1,273 +1,278 @@ -# 开发工具配置 - -本文档介绍 CFDesktop 项目推荐的开发工具配置,包括 VSCode 设置、clangd 配置、代码格式化和调试配置。 - -## 目录 - -- [VSCode 配置](#vscode-配置) -- [clangd 配置](#clangd-配置) -- [推荐 VSCode 扩展](#推荐-vscode-扩展) -- [代码格式化](#代码格式化) -- [调试配置](#调试配置) - ---- - -## VSCode 配置 - -### .vscode/settings.json 说明 - -项目根目录的 `.vscode/settings.json` 包含以下关键配置: - -```json -{ - "clangd.path": "D:/QT/Qt6.6.0/Tools/llvm-mingw1706_64/bin/clangd.exe", - "clangd.arguments": [ - "--compile-commands-dir=D:/ProjectHome/CFDesktop/out/build_develop", - "--query-driver=D:/QT/Qt6.6.0/Tools/*/bin/g++.exe", - "--background-index", - "--clang-tidy", - "--header-insertion=iwyu", - "--log=error", - "--header-insertion-decorators", - "--ranking-model=decision_forest" - ] -} -``` - -**参数说明:** - -| 参数 | 说明 | -|------|------| -| `clangd.path` | clangd 可执行文件路径(使用 Qt 自带的 LLVM) | -| `--compile-commands-dir` | compile_commands.json 所在目录(构建输出目录) | -| `--query-driver` | 允许 clangd 索引的编译器路径 | -| `--background-index` | 启用后台索引,加快全局搜索速度 | -| `--clang-tidy` | 启用静态代码分析 | -| `--header-insertion=iwyu` | 智能头文件插入(IWYU:Include What You Use) | -| `--ranking-model` | 代码补全排序算法 | - -### 生成 compile_commands.json - -`compile_commands.json` 由 CMake 自动生成,位于构建输出目录: - -```bash -# Windows (Develop 配置) -cmake -B out/build_develop -DCMAKE_BUILD_TYPE=Develop -# 生成 out/build_develop/compile_commands.json -``` - ---- - -## clangd 配置 - -### 代码补全 - -clangd 提供以下代码补全功能: - -- **函数/类名补全**:输入函数名前几个字符自动补全 -- **参数提示**:调用函数时显示参数列表 -- **成员访问**:使用 `.` 或 `->` 时自动显示成员列表 -- **头文件补全**:`#include` 时自动搜索可用头文件 - -### 代码导航 - -| 快捷键 | 功能 | -|--------|------| -| `F12` | 跳转到定义 | -| `Shift+F12` | 查找所有引用 | -| `Ctrl+T` | 符号搜索 | -| `Alt+←/→` | 前进/后退 | - -### 常见问题 - -**Q: clangd 无法索引 Qt 头文件?** - -A: 检查 `--query-driver` 参数是否包含 Qt 的编译器路径,或检查 compile_commands.json 中的编译器路径。 - -**Q: 代码补全很慢?** - -A: 确保 `--background-index` 已启用,首次索引可能需要几分钟。 - ---- - -## 推荐 VSCode 扩展 - -项目 `.vscode/extensions.json` 定义了推荐的扩展列表: - -| 扩展 ID | 名称 | 用途 | -|---------|------|------| -| `llvm-vs-code-extensions.vscode-clangd` | clangd | C++ 代码补全、导航、诊断 | -| `ms-vscode.cmake-tools` | CMake Tools | CMake 项目管理、构建、调试 | -| `twxs.cmake` | CMake Syntax | CMake 语法高亮 | -| `qt-labs.qt-all` | Qt ALL | Qt 资源文件预览、.ui 编辑 | - -### 安装推荐扩展 - -1. 打开 VSCode -2. 按 `Ctrl+Shift+X` 打开扩展面板 -3. 搜索 "Recommended" -4. 点击 "Workspace Recommended" 安装所有推荐扩展 - -或通过命令行安装: - -```bash -code --install-extension llvm-vs-code-extensions.vscode-clangd -code --install-extension ms-vscode.cmake-tools -code --install-extension twxs.cmake -code --install-extension qt-labs.qt-all -``` - ---- - -## 代码格式化 - -### .clang-format 配置 - -项目使用 LLVM 风格的代码格式化规则,配置文件为 `.clang-format`: - -```yaml -# 基础风格: LLVM -BasedOnStyle: LLVM - -# 缩进设置 -IndentWidth: 4 -UseTab: Never -ColumnLimit: 100 - -# 大括号风格: 附加式 { 在语句末尾 -BreakBeforeBraces: Attach - -# 指针/引用星号位置: 左侧 (int* a) -PointerAlignment: Left -DerivePointerAlignment: false - -# 其他设置 -Language: Cpp -Standard: c++17 -SortIncludes: true -``` - -### 格式化规则摘要 - -| 规则 | 设置 | -|------|------| -| 基础风格 | LLVM | -| 缩进 | 4 空格 | -| 列宽 | 100 字符 | -| 大括号 | 附加式 (`if (x) {` | -| 指针星号 | 左侧 (`int* a`) | -| C++ 标准 | C++17 | -| 头文件排序 | 启用 | - -### 手动格式化命令 - -**格式化整个文件:** - -```bash -# 格式化单个文件 -clang-format -i path/to/file.cpp - -# 格式化整个项目 -find . -name "*.cpp" -o -name "*.h" | xargs clang-format -i -``` - -**VSCode 快捷键:** - -- `Shift+Alt+F`:格式化当前文件 -- 右键 -> "Format Document":格式化当前文件 - -### 预提交检查 - -在提交前检查代码格式: - -```bash -# 检查格式是否正确(不修改文件) -clang-format --dry-run --Werror path/to/file.cpp -``` - ---- - -## 调试配置 - -### GDB 配置 - -项目使用 GDB 作为调试器,`.vscode/launch.json` 配置示例: - -```json -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Debug Target", - "type": "cppdbg", - "request": "launch", - "program": "${workspaceFolder}/out/build_develop/bin/textarea_example.exe", - "args": [], - "stopAtEntry": false, - "cwd": "${workspaceFolder}", - "environment": [], - "externalConsole": false, - "MIMode": "gdb", - "miDebuggerPath": "D:/QT/Qt6.6.0/Tools/mingw1310_64/bin/gdb.exe", - "setupCommands": [ - { - "description": "Enable pretty-printing", - "text": "-enable-pretty-printing", - "ignoreFailures": true - } - ] - } - ] -} -``` - -### 断点设置 - -| 操作 | 快捷键 | -|------|--------| -| 设置/取消断点 | `F9` | -| 条件断点 | 右键断点 -> "Edit Breakpoint" -> 输入条件 | -| 日志断点 | 右键断点 -> "Add Logpoint" -> 输入日志消息 | - -### 变量查看 - -| 面板 | 功能 | -|------|------| -| VARIABLES | 查看当前作用域的变量值 | -| WATCH | 添加表达式监视 | -| CALL STACK | 查看调用栈 | - -### 调试快捷键 - -| 快捷键 | 功能 | -|--------|------| -| `F5` | 开始调试 | -| `Shift+F5` | 停止调试 | -| `F10` | 单步跳过 | -| `F11` | 单步进入 | -| `Shift+F11` | 单步跳出 | - -### Linux 调试 - -在 Docker 容器中调试: - -```bash -# 进入容器 -bash scripts/build_helpers/docker_start.sh - -# 构建调试版本 -cmake -B out/build_develop -DCMAKE_BUILD_TYPE=Debug -cmake --build out/build_develop - -# 使用 gdb 调试 -gdb out/build_develop/bin/your_app -``` - ---- - -## 相关文档 - -- [快速开始](./02_quick_start.md) -- [构建系统](./03_build_system.md) -- [Docker 构建](./05_docker_build.md) -- [项目索引](../index.md) +--- +title: 开发工具配置 +description: 本文档介绍 CFDesktop 项目推荐的开发工具配置,包括 VSCode 设置、clangd 配置 +--- + +# 开发工具配置 + +本文档介绍 CFDesktop 项目推荐的开发工具配置,包括 VSCode 设置、clangd 配置、代码格式化和调试配置。 + +## 目录 + +- [VSCode 配置](#vscode-配置) +- [clangd 配置](#clangd-配置) +- [推荐 VSCode 扩展](#推荐-vscode-扩展) +- [代码格式化](#代码格式化) +- [调试配置](#调试配置) + +--- + +## VSCode 配置 + +### .vscode/settings.json 说明 + +项目根目录的 `.vscode/settings.json` 包含以下关键配置: + +```json +{ + "clangd.path": "D:/QT/Qt6.6.0/Tools/llvm-mingw1706_64/bin/clangd.exe", + "clangd.arguments": [ + "--compile-commands-dir=D:/ProjectHome/CFDesktop/out/build_develop", + "--query-driver=D:/QT/Qt6.6.0/Tools/*/bin/g++.exe", + "--background-index", + "--clang-tidy", + "--header-insertion=iwyu", + "--log=error", + "--header-insertion-decorators", + "--ranking-model=decision_forest" + ] +} +```bash + +**参数说明:** + +| 参数 | 说明 | +|------|------| +| `clangd.path` | clangd 可执行文件路径(使用 Qt 自带的 LLVM) | +| `--compile-commands-dir` | compile_commands.json 所在目录(构建输出目录) | +| `--query-driver` | 允许 clangd 索引的编译器路径 | +| `--background-index` | 启用后台索引,加快全局搜索速度 | +| `--clang-tidy` | 启用静态代码分析 | +| `--header-insertion=iwyu` | 智能头文件插入(IWYU:Include What You Use) | +| `--ranking-model` | 代码补全排序算法 | + +### 生成 compile_commands.json + +`compile_commands.json` 由 CMake 自动生成,位于构建输出目录: + +```bash +# Windows (Develop 配置) +cmake -B out/build_develop -DCMAKE_BUILD_TYPE=Develop +# 生成 out/build_develop/compile_commands.json +```cpp + +--- + +## clangd 配置 + +### 代码补全 + +clangd 提供以下代码补全功能: + +- **函数/类名补全**:输入函数名前几个字符自动补全 +- **参数提示**:调用函数时显示参数列表 +- **成员访问**:使用 `.` 或 `->` 时自动显示成员列表 +- **头文件补全**:`#include` 时自动搜索可用头文件 + +### 代码导航 + +| 快捷键 | 功能 | +|--------|------| +| `F12` | 跳转到定义 | +| `Shift+F12` | 查找所有引用 | +| `Ctrl+T` | 符号搜索 | +| `Alt+←/→` | 前进/后退 | + +### 常见问题 + +**Q: clangd 无法索引 Qt 头文件?** + +A: 检查 `--query-driver` 参数是否包含 Qt 的编译器路径,或检查 compile_commands.json 中的编译器路径。 + +**Q: 代码补全很慢?** + +A: 确保 `--background-index` 已启用,首次索引可能需要几分钟。 + +--- + +## 推荐 VSCode 扩展 + +项目 `.vscode/extensions.json` 定义了推荐的扩展列表: + +| 扩展 ID | 名称 | 用途 | +|---------|------|------| +| `llvm-vs-code-extensions.vscode-clangd` | clangd | C++ 代码补全、导航、诊断 | +| `ms-vscode.cmake-tools` | CMake Tools | CMake 项目管理、构建、调试 | +| `twxs.cmake` | CMake Syntax | CMake 语法高亮 | +| `qt-labs.qt-all` | Qt ALL | Qt 资源文件预览、.ui 编辑 | + +### 安装推荐扩展 + +1. 打开 VSCode +2. 按 `Ctrl+Shift+X` 打开扩展面板 +3. 搜索 "Recommended" +4. 点击 "Workspace Recommended" 安装所有推荐扩展 + +或通过命令行安装: + +```bash +code --install-extension llvm-vs-code-extensions.vscode-clangd +code --install-extension ms-vscode.cmake-tools +code --install-extension twxs.cmake +code --install-extension qt-labs.qt-all +```yaml + +--- + +## 代码格式化 + +### .clang-format 配置 + +项目使用 LLVM 风格的代码格式化规则,配置文件为 `.clang-format`: + +```yaml +# 基础风格: LLVM +BasedOnStyle: LLVM + +# 缩进设置 +IndentWidth: 4 +UseTab: Never +ColumnLimit: 100 + +# 大括号风格: 附加式 { 在语句末尾 +BreakBeforeBraces: Attach + +# 指针/引用星号位置: 左侧 (int* a) +PointerAlignment: Left +DerivePointerAlignment: false + +# 其他设置 +Language: Cpp +Standard: c++17 +SortIncludes: true +```cmake + +### 格式化规则摘要 + +| 规则 | 设置 | +|------|------| +| 基础风格 | LLVM | +| 缩进 | 4 空格 | +| 列宽 | 100 字符 | +| 大括号 | 附加式 (`if (x) {` | +| 指针星号 | 左侧 (`int* a`) | +| C++ 标准 | C++23 | +| 头文件排序 | 启用 | + +### 手动格式化命令 + +**格式化整个文件:** + +```bash +# 格式化单个文件 +clang-format -i path/to/file.cpp + +# 格式化整个项目 +find . -name "*.cpp" -o -name "*.h" | xargs clang-format -i +```cpp + +**VSCode 快捷键:** + +- `Shift+Alt+F`:格式化当前文件 +- 右键 -> "Format Document":格式化当前文件 + +### 预提交检查 + +在提交前检查代码格式: + +```bash +# 检查格式是否正确(不修改文件) +clang-format --dry-run --Werror path/to/file.cpp +```yaml + +--- + +## 调试配置 + +### GDB 配置 + +项目使用 GDB 作为调试器,`.vscode/launch.json` 配置示例: + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Target", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/out/build_develop/bin/textarea_example.exe", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "miDebuggerPath": "D:/QT/Qt6.6.0/Tools/mingw1310_64/bin/gdb.exe", + "setupCommands": [ + { + "description": "Enable pretty-printing", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + } + ] +} +```cpp + +### 断点设置 + +| 操作 | 快捷键 | +|------|--------| +| 设置/取消断点 | `F9` | +| 条件断点 | 右键断点 -> "Edit Breakpoint" -> 输入条件 | +| 日志断点 | 右键断点 -> "Add Logpoint" -> 输入日志消息 | + +### 变量查看 + +| 面板 | 功能 | +|------|------| +| VARIABLES | 查看当前作用域的变量值 | +| WATCH | 添加表达式监视 | +| CALL STACK | 查看调用栈 | + +### 调试快捷键 + +| 快捷键 | 功能 | +|--------|------| +| `F5` | 开始调试 | +| `Shift+F5` | 停止调试 | +| `F10` | 单步跳过 | +| `F11` | 单步进入 | +| `Shift+F11` | 单步跳出 | + +### Linux 调试 + +在 Docker 容器中调试: + +```bash +# 进入容器 +bash scripts/build_helpers/docker_start.sh + +# 构建调试版本 +cmake -B out/build_develop -DCMAKE_BUILD_TYPE=Debug +cmake --build out/build_develop + +# 使用 gdb 调试 +gdb out/build_develop/bin/your_app +```yaml + +--- + +## 相关文档 + +- [快速开始](./02_quick_start.md) +- [构建系统](./03_build_system.md) +- [Docker 构建](./05_docker_build.md) +- [项目索引](../index.md) diff --git a/document/development/05_docker_build.md b/document/development/05_docker_build.md index ca751a7a2..276d3989a 100644 --- a/document/development/05_docker_build.md +++ b/document/development/05_docker_build.md @@ -1,383 +1,388 @@ -# Docker 构建指南 - -本文档介绍如何使用 Docker 构建多架构的 CFDesktop 项目,包括镜像说明、命令速查、多架构构建和调试配置。 - -## 目录 - -- [Docker 镜像说明](#docker-镜像说明) -- [docker_start.sh 命令速查](#docker_startsh-命令速查) -- [多架构构建](#多架构构建) -- [构建配置文件](#构建配置文件) -- [日志和调试](#日志和调试) -- [常见问题](#常见问题) - ---- - -## Docker 镜像说明 - -### 基础镜像 - -| 属性 | 值 | -|------|-----| -| 基础镜像 | Ubuntu 24.04 | -| 镜像名称 | `cfdesktop-build` | -| Dockerfile | `scripts/docker/Dockerfile.build` | - -### Qt 版本 - -| 属性 | 值 | -|------|-----| -| Qt 版本 | 6.8.1 | -| 安装方式 | aqtinstall (预编译二进制) | -| 镜像源 | https://mirrors.aliyun.com/qt (国内) | - -### 支持架构 - -| 架构 | 平台标识 | 目标平台 | 应用场景 | -|------|----------|----------|----------| -| amd64 | linux/amd64 | x86_64 | 本地开发、CI 构建 | -| arm64 | linux/arm64 | ARM64 | RK3568、RK3588 | -| armhf | linux/arm/v7 | ARM32 | IMX6ULL (交叉编译) | - ---- - -## docker_start.sh 命令速查 - -### 命令选项表 - -| 选项 | 说明 | 默认值 | -|------|------|--------| -| `--arch amd64\|arm64` | 目标架构 | amd64 | -| `--fast-build` | 跳过镜像清理,复用现有镜像 | false | -| `--verify` | 运行 CI 构建验证 | false | -| `--build-project` | 构建镜像 + 完整清理构建 | false | -| `--build-project-fast` | 构建镜像 + 快速构建(增量) | false | -| `--run-project-test` | 构建镜像 + 运行测试 | false | -| `--stay-on-error` | CI 失败时保持容器开启用于调试 | false | -| `--no-log` | 禁用文件日志记录 | false | -| `--no-deps` | 跳过镜像中的依赖安装 | false | -| `--help` | 显示帮助信息 | - | - -### 命令示例 - -#### 交互式 Shell - -```bash -# 默认进入交互式 shell(amd64 架构) -bash scripts/build_helpers/docker_start.sh - -# 指定架构 -bash scripts/build_helpers/docker_start.sh --arch arm64 -``` - -#### CI 验证模式 - -```bash -# 运行 CI 构建验证 -bash scripts/build_helpers/docker_start.sh --verify - -# 失败时保持容器开启(用于调试) -bash scripts/build_helpers/docker_start.sh --verify --stay-on-error -``` - -#### 完整构建 - -```bash -# 构建镜像 + 完整清理构建 -bash scripts/build_helpers/docker_start.sh --build-project - -# 指定架构 -bash scripts/build_helpers/docker_start.sh --build-project --arch arm64 -``` - -#### 快速构建 - -```bash -# 构建镜像 + 快速构建(增量,不清理) -bash scripts/build_helpers/docker_start.sh --build-project-fast - -# 快速构建 ARM64 -bash scripts/build_helpers/docker_start.sh --build-project-fast --arch arm64 -``` - -#### 运行测试 - -```bash -# 构建镜像 + 运行测试 -bash scripts/build_helpers/docker_start.sh --run-project-test - -# 指定架构 -bash scripts/build_helpers/docker_start.sh --run-project-test --arch arm64 -``` - -#### 快速模式 - -```bash -# 复用现有镜像(跳过镜像重建) -bash scripts/build_helpers/docker_start.sh --fast-build -``` - -#### 组合选项 - -```bash -# CI 验证 + 失败保持 + 禁用日志 -bash scripts/build_helpers/docker_start.sh --verify --stay-on-error --no-log - -# 快速构建 + ARM64 -bash scripts/build_helpers/docker_start.sh --build-project-fast --arch arm64 --fast-build -``` - ---- - -## 多架构构建 - -### 架构对比 - -| 架构 | 构建模式 | 目标设备 | 构建配置文件 | 输出目录 | -|------|----------|----------|--------------|----------| -| amd64 | 原生 | x86_64 PC | build_ci_config.ini | out/build_ci | -| arm64 | 原生/QEMU | RK3568/RK3588 | build_ci_aarch64_config.ini | out/build_ci_aarch64 | -| armhf | 交叉编译 | IMX6ULL | build_ci_armhf_config.ini | out/build_ci_armhf | - -### AMD64 构建(本地开发) - -```bash -# 默认架构,无需指定 -bash scripts/build_helpers/docker_start.sh --verify -``` - -### ARM64 构建 - -```bash -# ARM64 原生容器(需要 QEMU 支持) -bash scripts/build_helpers/docker_start.sh --arch arm64 --verify - -# 完整构建 -bash scripts/build_helpers/docker_start.sh --arch arm64 --build-project -``` - -**ARM64 应用场景:** -- Rockchip RK3568 (四核 ARM Cortex-A55) -- Rockchip RK3588 (八核 ARM Cortex-A76/A55) - -### ARMHF 构建(交叉编译) - -```bash -# ARM32 交叉编译(在 amd64 容器中) -bash scripts/build_helpers/docker_start.sh --arch armhf --verify -``` - -**ARMHF 应用场景:** -- NXP i.MX 6ULL (ARM Cortex-A7) - -### QEMU 支持 - -ARM64 构建依赖 QEMU 模拟器,确保系统已安装: - -```bash -# 检查 QEMU 是否安装 -docker run --rm --platform linux/arm64 ubuntu:24.04 uname -m -# 应输出: aarch64 - -# 如果失败,安装 binfmt 支持 -docker run --privileged --rm tonistiigi/binfmt --install all -``` - ---- - -## 构建配置文件 - -### build_ci_config.ini (AMD64) - -```ini -[cmake] -generator=Unix Makefiles -toolchain=linux/ci-x86_64 -build_type=Release - -[paths] -source=. -build_dir=out/build_ci - -[options] -jobs=16 -``` - -### build_ci_aarch64_config.ini (ARM64) - -```ini -[cmake] -generator=Unix Makefiles -toolchain=linux/ci-aarch64 -build_type=Release - -[paths] -source=. -build_dir=out/build_ci_aarch64 - -[options] -jobs=16 -``` - -### build_ci_armhf_config.ini (ARMHF) - -```ini -[cmake] -generator=Unix Makefiles -toolchain=linux/ci-armhf -build_type=Release - -[paths] -source=. -build_dir=out/build_ci_armhf - -[options] -jobs=16 -``` - -### 配置文件说明 - -| 节 | 选项 | 说明 | -|-----|------|------| -| [cmake] | generator | CMake 生成器 | -| [cmake] | toolchain | 工具链文件路径 | -| [cmake] | build_type | 构建类型 | -| [paths] | source | 源码目录(相对项目根目录) | -| [paths] | build_dir | 构建输出目录 | -| [options] | jobs | 并行编译任务数 | - ---- - -## 日志和调试 - -### 日志目录 - -构建日志保存在 `scripts/docker/logger/` 目录: - -```bash -# 日志文件命名格式 -ci_build_YYYYMMDD_HHMMSS.log -``` - -### 日志内容 - -日志文件包含以下信息: - -``` -============================================================================== -CFDesktop Docker Build Log -============================================================================== -Start Time: 2026-03-07 14:30:00 CST -Architecture: amd64 -Platform: linux/amd64 -Fast Build: false -Verify Mode: true -============================================================================== -``` - -### --stay-on-error 调试 - -当 CI 构建失败时,使用 `--stay-on-error` 保持容器开启: - -```bash -bash scripts/build_helpers/docker_start.sh --verify --stay-on-error -``` - -**效果:** -- 构建失败后不会退出容器 -- 可以手动检查构建输出 -- 可以手动运行调试命令 - -**使用场景:** -```bash -# 失败后可以在容器中执行 -cd /project -ls -la out/build_ci/ -cat build.log -``` - -### 禁用日志 - -```bash -# 不生成日志文件(输出到终端) -bash scripts/build_helpers/docker_start.sh --verify --no-log -``` - ---- - -## 常见问题 - -### Q: Docker 镜像构建失败? - -**A:** 检查以下几点: - -1. 确认 Docker 服务运行中: - ```bash - docker info - ``` - -2. 检查网络连接(镜像拉取): - ```bash - docker pull ubuntu:24.04 - ``` - -3. 清理旧镜像重试: - ```bash - docker rmi cfdesktop-build - ``` - -### Q: ARM64 构建很慢? - -**A:** ARM64 在 x86_64 主机上通过 QEMU 模拟,速度较慢是正常的。优化建议: - -1. 使用 `--fast-build` 复用镜像 -2. 使用 `--build-project-fast` 增量构建 -3. 减少并行任务数 - -### Q: 如何在容器中调试? - -**A:** 使用交互式模式: - -```bash -# 进入容器 -bash scripts/build_helpers/docker_start.sh - -# 手动构建 -cd /project -cmake -B out/build_ci -DCMAKE_BUILD_TYPE=Release -cmake --build out/build_ci - -# 运行程序 -./out/build_ci/bin/your_app -``` - -### Q: 如何修改 Qt 版本? - -**A:** 编辑 `scripts/docker/Dockerfile.build`: - -```dockerfile -ARG QT_VERSION=6.8.1 # 修改为需要的版本 -``` - -然后重新构建镜像。 - -### Q: 容器中如何访问外网? - -**A:** Docker 容器默认使用宿主机网络配置,可以直接访问外网。如果遇到问题: - -```bash -# 检查容器网络 -docker run --rm ubuntu:24.04 ping -c 3 8.8.8.8 - -# 检查 DNS -docker run --rm ubuntu:24.4 cat /etc/resolv.conf -``` - ---- - -## 相关文档 - -- [开发工具配置](./04_development_tools.md) -- [构建系统](./03_build_system.md) -- [快速开始](./02_quick_start.md) -- [项目索引](../index.md) +--- +title: Docker 构建指南 +description: 本文档介绍如何使用 Docker 构建多架构的 CFDesktop 项目,包括镜像说明、命令速查、多 +--- + +# Docker 构建指南 + +本文档介绍如何使用 Docker 构建多架构的 CFDesktop 项目,包括镜像说明、命令速查、多架构构建和调试配置。 + +## 目录 + +- [Docker 镜像说明](#docker-镜像说明) +- [docker_start.sh 命令速查](#docker_startsh-命令速查) +- [多架构构建](#多架构构建) +- [构建配置文件](#构建配置文件) +- [日志和调试](#日志和调试) +- [常见问题](#常见问题) + +--- + +## Docker 镜像说明 + +### 基础镜像 + +| 属性 | 值 | +|------|-----| +| 基础镜像 | Ubuntu 24.04 | +| 镜像名称 | `cfdesktop-build` | +| Dockerfile | `scripts/docker/Dockerfile.build` | + +### Qt 版本 + +| 属性 | 值 | +|------|-----| +| Qt 版本 | 6.8.1 | +| 安装方式 | aqtinstall (预编译二进制) | +| 镜像源 | https://mirrors.aliyun.com/qt (国内) | + +### 支持架构 + +| 架构 | 平台标识 | 目标平台 | 应用场景 | +|------|----------|----------|----------| +| amd64 | linux/amd64 | x86_64 | 本地开发、CI 构建 | +| arm64 | linux/arm64 | ARM64 | RK3568、RK3588 | +| armhf | linux/arm/v7 | ARM32 | IMX6ULL (交叉编译) | + +--- + +## docker_start.sh 命令速查 + +### 命令选项表 + +| 选项 | 说明 | 默认值 | +|------|------|--------| +| `--arch amd64\|arm64` | 目标架构 | amd64 | +| `--fast-build` | 跳过镜像清理,复用现有镜像 | false | +| `--verify` | 运行 CI 构建验证 | false | +| `--build-project` | 构建镜像 + 完整清理构建 | false | +| `--build-project-fast` | 构建镜像 + 快速构建(增量) | false | +| `--run-project-test` | 构建镜像 + 运行测试 | false | +| `--stay-on-error` | CI 失败时保持容器开启用于调试 | false | +| `--no-log` | 禁用文件日志记录 | false | +| `--no-deps` | 跳过镜像中的依赖安装 | false | +| `--help` | 显示帮助信息 | - | + +### 命令示例 + +#### 交互式 Shell + +```bash +# 默认进入交互式 shell(amd64 架构) +bash scripts/build_helpers/docker_start.sh + +# 指定架构 +bash scripts/build_helpers/docker_start.sh --arch arm64 +```text + +#### CI 验证模式 + +```bash +# 运行 CI 构建验证 +bash scripts/build_helpers/docker_start.sh --verify + +# 失败时保持容器开启(用于调试) +bash scripts/build_helpers/docker_start.sh --verify --stay-on-error +```text + +#### 完整构建 + +```bash +# 构建镜像 + 完整清理构建 +bash scripts/build_helpers/docker_start.sh --build-project + +# 指定架构 +bash scripts/build_helpers/docker_start.sh --build-project --arch arm64 +```text + +#### 快速构建 + +```bash +# 构建镜像 + 快速构建(增量,不清理) +bash scripts/build_helpers/docker_start.sh --build-project-fast + +# 快速构建 ARM64 +bash scripts/build_helpers/docker_start.sh --build-project-fast --arch arm64 +```text + +#### 运行测试 + +```bash +# 构建镜像 + 运行测试 +bash scripts/build_helpers/docker_start.sh --run-project-test + +# 指定架构 +bash scripts/build_helpers/docker_start.sh --run-project-test --arch arm64 +```text + +#### 快速模式 + +```bash +# 复用现有镜像(跳过镜像重建) +bash scripts/build_helpers/docker_start.sh --fast-build +```text + +#### 组合选项 + +```bash +# CI 验证 + 失败保持 + 禁用日志 +bash scripts/build_helpers/docker_start.sh --verify --stay-on-error --no-log + +# 快速构建 + ARM64 +bash scripts/build_helpers/docker_start.sh --build-project-fast --arch arm64 --fast-build +```bash + +--- + +## 多架构构建 + +### 架构对比 + +| 架构 | 构建模式 | 目标设备 | 构建配置文件 | 输出目录 | +|------|----------|----------|--------------|----------| +| amd64 | 原生 | x86_64 PC | build_ci_config.ini | out/build_ci | +| arm64 | 原生/QEMU | RK3568/RK3588 | build_ci_aarch64_config.ini | out/build_ci_aarch64 | +| armhf | 交叉编译 | IMX6ULL | build_ci_armhf_config.ini | out/build_ci_armhf | + +### AMD64 构建(本地开发) + +```bash +# 默认架构,无需指定 +bash scripts/build_helpers/docker_start.sh --verify +```text + +### ARM64 构建 + +```bash +# ARM64 原生容器(需要 QEMU 支持) +bash scripts/build_helpers/docker_start.sh --arch arm64 --verify + +# 完整构建 +bash scripts/build_helpers/docker_start.sh --arch arm64 --build-project +```text + +**ARM64 应用场景:** +- Rockchip RK3568 (四核 ARM Cortex-A55) +- Rockchip RK3588 (八核 ARM Cortex-A76/A55) + +### ARMHF 构建(交叉编译) + +```bash +# ARM32 交叉编译(在 amd64 容器中) +bash scripts/build_helpers/docker_start.sh --arch armhf --verify +```text + +**ARMHF 应用场景:** +- NXP i.MX 6ULL (ARM Cortex-A7) + +### QEMU 支持 + +ARM64 构建依赖 QEMU 模拟器,确保系统已安装: + +```bash +# 检查 QEMU 是否安装 +docker run --rm --platform linux/arm64 ubuntu:24.04 uname -m +# 应输出: aarch64 + +# 如果失败,安装 binfmt 支持 +docker run --privileged --rm tonistiigi/binfmt --install all +```yaml + +--- + +## 构建配置文件 + +### build_ci_config.ini (AMD64) + +```ini +[cmake] +generator=Unix Makefiles +toolchain=linux/ci-x86_64 +build_type=Release + +[paths] +source=. +build_dir=out/build_ci + +[options] +jobs=16 +```text + +### build_ci_aarch64_config.ini (ARM64) + +```ini +[cmake] +generator=Unix Makefiles +toolchain=linux/ci-aarch64 +build_type=Release + +[paths] +source=. +build_dir=out/build_ci_aarch64 + +[options] +jobs=16 +```text + +### build_ci_armhf_config.ini (ARMHF) + +```ini +[cmake] +generator=Unix Makefiles +toolchain=linux/ci-armhf +build_type=Release + +[paths] +source=. +build_dir=out/build_ci_armhf + +[options] +jobs=16 +```bash + +### 配置文件说明 + +| 节 | 选项 | 说明 | +|-----|------|------| +| [cmake] | generator | CMake 生成器 | +| [cmake] | toolchain | 工具链文件路径 | +| [cmake] | build_type | 构建类型 | +| [paths] | source | 源码目录(相对项目根目录) | +| [paths] | build_dir | 构建输出目录 | +| [options] | jobs | 并行编译任务数 | + +--- + +## 日志和调试 + +### 日志目录 + +构建日志保存在 `scripts/docker/logger/` 目录: + +```bash +# 日志文件命名格式 +ci_build_YYYYMMDD_HHMMSS.log +```text + +### 日志内容 + +日志文件包含以下信息: + +```text +============================================================================== +CFDesktop Docker Build Log +============================================================================== +Start Time: 2026-03-07 14:30:00 CST +Architecture: amd64 +Platform: linux/amd64 +Fast Build: false +Verify Mode: true +============================================================================== +```text + +### --stay-on-error 调试 + +当 CI 构建失败时,使用 `--stay-on-error` 保持容器开启: + +```bash +bash scripts/build_helpers/docker_start.sh --verify --stay-on-error +```text + +**效果:** +- 构建失败后不会退出容器 +- 可以手动检查构建输出 +- 可以手动运行调试命令 + +**使用场景:** +```bash +# 失败后可以在容器中执行 +cd /project +ls -la out/build_ci/ +cat build.log +```text + +### 禁用日志 + +```bash +# 不生成日志文件(输出到终端) +bash scripts/build_helpers/docker_start.sh --verify --no-log +```yaml + +--- + +## 常见问题 + +### Q: Docker 镜像构建失败? + +**A:** 检查以下几点: + +1. 确认 Docker 服务运行中: + ```bash + docker info + ``` + +2. 检查网络连接(镜像拉取): + ```bash + docker pull ubuntu:24.04 + ``` + +3. 清理旧镜像重试: + ```bash + docker rmi cfdesktop-build + ``` + +### Q: ARM64 构建很慢? + +**A:** ARM64 在 x86_64 主机上通过 QEMU 模拟,速度较慢是正常的。优化建议: + +1. 使用 `--fast-build` 复用镜像 +2. 使用 `--build-project-fast` 增量构建 +3. 减少并行任务数 + +### Q: 如何在容器中调试? + +**A:** 使用交互式模式: + +```bash +# 进入容器 +bash scripts/build_helpers/docker_start.sh + +# 手动构建 +cd /project +cmake -B out/build_ci -DCMAKE_BUILD_TYPE=Release +cmake --build out/build_ci + +# 运行程序 +./out/build_ci/bin/your_app +```text + +### Q: 如何修改 Qt 版本? + +**A:** 编辑 `scripts/docker/Dockerfile.build`: + +```dockerfile +ARG QT_VERSION=6.8.1 # 修改为需要的版本 +```text + +然后重新构建镜像。 + +### Q: 容器中如何访问外网? + +**A:** Docker 容器默认使用宿主机网络配置,可以直接访问外网。如果遇到问题: + +```bash +# 检查容器网络 +docker run --rm ubuntu:24.04 ping -c 3 8.8.8.8 + +# 检查 DNS +docker run --rm ubuntu:24.4 cat /etc/resolv.conf +```yaml + +--- + +## 相关文档 + +- [开发工具配置](./04_development_tools.md) +- [构建系统](./03_build_system.md) +- [快速开始](./02_quick_start.md) +- [项目索引](../index.md) diff --git a/document/development/06_git_hooks.md b/document/development/06_git_hooks.md index 758993c5c..699592f6d 100644 --- a/document/development/06_git_hooks.md +++ b/document/development/06_git_hooks.md @@ -1,332 +1,337 @@ -# Git Hooks 使用说明 - -本文档介绍 CFDesktop 项目中 Git Hooks 的安装、配置和使用方法。 - -## 目录 - -- [概述](#概述) -- [Pre-Commit Hook](#pre-commit-hook) -- [Pre-Push Hook](#pre-push-hook) -- [版本工具](#版本工具) -- [安装与卸载](#安装与卸载) -- [常见问题](#常见问题) - -## 概述 - -CFDesktop 使用 Git Hooks 来确保代码质量和构建验证。项目包含两个主要 Hook: - -| Hook | 触发时机 | 用途 | -|------|---------|------| -| pre-commit | `git commit` 前 | 代码质量检查和自动格式化 | -| pre-push | `git push` 前 | Docker 构建验证和版本检查 | - -## Pre-Commit Hook - -### 功能 - -Pre-commit Hook 在每次提交前自动执行以下检查: - -1. **空白字符检查** - - 检测并阻止带有尾随空格(trailing whitespace)的提交 - - 支持空格和 Tab 字符检测 - -2. **C++ 代码自动格式化** - - 使用 `clang-format` 自动格式化暂存的 C/C++ 文件 - - 格式化后自动更新暂存区 - -### 跨平台支持 - -Hook 会自动检测运行平台并使用相应的脚本: - -| 平台 | 使用的脚本 | -|------|-----------| -| Windows (Git Bash/MSYS) | `scripts/develop/remove_trailing_space.ps1` | -| Linux/macOS | `scripts/develop/remove_trailing_space.sh` | - -### 手动修复空白字符问题 - -如果 pre-commit 检查失败,可以手动运行修复脚本: - -```bash -# Linux/macOS -bash scripts/develop/remove_trailing_space.sh --staged - -# Windows (PowerShell) -pwsh scripts/develop/remove_trailing_space.ps1 -Staged -``` - -### 绕过 Pre-Commit 检查 - -紧急情况下可以使用 `--no-verify` 跳过检查(不推荐): - -```bash -git commit --no-verify -m "紧急修复" -``` - -### 检查模式 - -remove_trailing_space 脚本支持多种模式: - -```bash -# 检查所有文件(不修改) -bash scripts/develop/remove_trailing_space.sh --check - -# 仅检查暂存文件 -bash scripts/develop/remove_trailing_space.sh --staged-check - -# 修复暂存文件 -bash scripts/develop/remove_trailing_space.sh --staged - -# 预览将要修改的内容 -bash scripts/develop/remove_trailing_space.sh --dry-run -``` - -## Pre-Push Hook - -### 功能 - -Pre-push Hook 在推送前执行 Docker 构建验证,确保代码可以成功构建。 - -### 分支验证策略 - -不同的分支有不同的验证策略: - -#### main 分支 - -- **验证级别**: X64 FastBuild + Tests -- **说明**: 快速验证,适用于日常开发推送 - -#### release 分支 - -根据版本号自动检测验证级别: - -| 版本变化 | 验证级别 | 说明 | -|---------|---------|------| -| Major (x.0.0 -> y.0.0) | X64 + ARM64 完整构建 + 测试 | 完整多架构验证 | -| Minor (x.y.0 -> x.z.0) | X64 完整构建 + 测试 | 标准验证 | -| Patch (x.y.z -> x.y.w) | X64 FastBuild + 测试 | 快速验证 | - -#### 其他分支 - -- 跳过验证,直接允许推送 - -### 版本号检查 - -推送到 main 或 release 分支时,会检查 `CMakeLists.txt` 中的版本号是否已更新: - -```cmake -project(CFDesktop VERSION X.Y.Z LANGUAGES CXX) -``` - -如果版本号未变更,推送将被阻止,提示信息如下: - -``` -======================================== -版本号未变更,推送被阻止 -======================================== - -当前版本: 0.13.1 -远程版本: 0.13.1 - -请按以下步骤更新版本号: - - 1. 更新 CMakeLists.txt 版本号: - project(CFDesktop VERSION X.Y.Z LANGUAGES CXX) - - 2. 更新 README.md 中的版本徽章 (如果存在) - -版本号规则: - - Patch: 0.13.1 -> 0.13.2 (bug 修复、小改动) - - Minor: 0.13.1 -> 0.14.0 (新功能) - - Major: 0.13.1 -> 1.0.0 (破坏性变更) -``` - -### 首次推送 - -如果是首次推送(远程没有版本标签),版本号检查会被跳过。 - -### 绕过 Pre-Push 检查 - -紧急情况下可以使用 `--no-verify` 跳过检查(不推荐): - -```bash -git push --no-verify -``` - -### Docker 环境要求 - -Pre-push Hook 需要以下环境: - -- Docker 已安装并运行 -- Docker daemon 可用 - -如果 Docker 不可用,Hook 会显示安装提示并退出。 - -## 版本工具 - -`scripts/release/hooks/version_utils.sh` 提供版本号解析和验证级别检测功能。 - -### 可用函数 - -| 函数 | 说明 | -|------|------| -| `get_major_version ` | 获取主版本号 (X.y.z) | -| `get_minor_version ` | 获取次版本号 (x.Y.z) | -| `get_patch_version ` | 获取补丁版本号 (x.y.Z) | -| `determine_verify_level ` | 根据版本号确定验证级别 | -| `get_verify_level_description ` | 获取验证级别的描述 | -| `get_local_version` | 获取当前分支的版本标签 | -| `get_remote_version` | 获取远程 main 分支的版本标签 | -| `get_cmake_version ` | 获取 CMakeLists.txt 中定义的版本号 | -| `print_version_info ` | 打印版本信息(调试用) | - -### 使用示例 - -```bash -# 加载版本工具 -source scripts/release/hooks/version_utils.sh - -# 获取版本号组件 -VERSION="1.2.3" -MAJOR=$(get_major_version "$VERSION") # 输出: 1 -MINOR=$(get_minor_version "$VERSION") # 输出: 2 -PATCH=$(get_patch_version "$VERSION") # 输出: 3 - -# 确定验证级别 -LEVEL=$(determine_verify_level "1.2.3" "1.1.0") # 输出: minor -``` - -## 安装与卸载 - -### 安装 Hooks - -使用提供的安装脚本: - -```bash -bash scripts/release/hooks/install_hooks.sh -``` - -安装过程: - -1. 检查是否在 Git 仓库中 -2. 显示安装信息(源目录、目标目录) -3. 备份现有的自定义 Hook(如果有) -4. 复制 Hook 文件到 `.git/hooks/` 目录 -5. 设置执行权限 - -### 验证安装 - -安装完成后,可以验证 Hook 是否正确安装: - -```bash -ls -la .git/hooks/pre-commit .git/hooks/pre-push -``` - -### 卸载 Hooks - -直接删除 Hook 文件: - -```bash -rm .git/hooks/pre-commit .git/hooks/pre-push -``` - -### 备份机制 - -安装脚本会自动备份现有的自定义 Hook: - -- 如果现有 Hook 包含 "CFDesktop Git Hooks" 标记,直接覆盖 -- 如果是用户自定义的 Hook,备份为 `.backup.YYYYMMDDHHMMSS` - -## 常见问题 - -### Pre-Commit 失败 - -**问题**: 提交时提示空白字符检查失败 - -**解决方案**: - -```bash -# 自动修复暂存文件 -bash scripts/develop/remove_trailing_space.sh --staged - -# 或强制提交(不推荐) -git commit --no-verify -m "message" -``` - -### Pre-Push 验证失败 - -**问题**: Docker 构建验证失败 - -**解决方案**: - -1. 查看构建日志,找到失败原因 -2. 本地修复问题后重新提交 -3. 或使用 `--no-verify` 强制推送(不推荐) - -### 版本号检查失败 - -**问题**: 推送时提示版本号未变更 - -**解决方案**: - -1. 更新 `CMakeLists.txt` 中的版本号 -2. 提交版本号变更 -3. 重新推送 - -```bash -# 更新版本号示例 -vim CMakeLists.txt # 修改 VERSION x.y.z -git add CMakeLists.txt -git commit -m "chore: bump version to x.y.z" -git push -``` - -### Docker 未运行 - -**问题**: Pre-push Hook 提示 Docker daemon 未运行 - -**解决方案**: - -1. 启动 Docker Desktop (Windows/macOS) -2. 或启动 Docker 服务 (Linux) - -```bash -# Linux -sudo systemctl start docker -``` - -### clang-format 未安装 - -**问题**: C++ 代码无法自动格式化 - -**解决方案**: - -```bash -# Ubuntu/Debian -sudo apt install clang-format - -# macOS -brew install clang-format - -# Windows (使用 Chocolatey) -choco install clang-format -``` - -### ARM64 构建很慢 - -**问题**: release 分支 Major 版本验证时 ARM64 构建非常慢 - -**原因**: ARM64 使用 QEMU 仿真,速度较慢 - -**解决方案**: - -1. 耐心等待(首次构建可能需要 30+ 分钟) -2. 使用 `--fast-build` 缓存镜像 -3. 在原生 ARM64 环境中构建 - -## 相关文档 - -- [环境准备](./01_prerequisites.md) -- [构建系统](./03_build_system.md) -- [常见问题排查](./07_troubleshooting.md) -- [Docker 构建](./05_docker_build.md) +--- +title: Git Hooks 使用说明 +description: 本文档介绍 CFDesktop 项目中 Git Hooks 的安装、配置和使用方法。 +--- + +# Git Hooks 使用说明 + +本文档介绍 CFDesktop 项目中 Git Hooks 的安装、配置和使用方法。 + +## 目录 + +- [概述](#概述) +- [Pre-Commit Hook](#pre-commit-hook) +- [Pre-Push Hook](#pre-push-hook) +- [版本工具](#版本工具) +- [安装与卸载](#安装与卸载) +- [常见问题](#常见问题) + +## 概述 + +CFDesktop 使用 Git Hooks 来确保代码质量和构建验证。项目包含两个主要 Hook: + +| Hook | 触发时机 | 用途 | +|------|---------|------| +| pre-commit | `git commit` 前 | 代码质量检查和自动格式化 | +| pre-push | `git push` 前 | Docker 构建验证和版本检查 | + +## Pre-Commit Hook + +### 功能 + +Pre-commit Hook 在每次提交前自动执行以下检查: + +1. **空白字符检查** + - 检测并阻止带有尾随空格(trailing whitespace)的提交 + - 支持空格和 Tab 字符检测 + +2. **C++ 代码自动格式化** + - 使用 `clang-format` 自动格式化暂存的 C/C++ 文件 + - 格式化后自动更新暂存区 + +### 跨平台支持 + +Hook 会自动检测运行平台并使用相应的脚本: + +| 平台 | 使用的脚本 | +|------|-----------| +| Windows (Git Bash/MSYS) | `scripts/develop/remove_trailing_space.ps1` | +| Linux/macOS | `scripts/develop/remove_trailing_space.sh` | + +### 手动修复空白字符问题 + +如果 pre-commit 检查失败,可以手动运行修复脚本: + +```bash +# Linux/macOS +bash scripts/develop/remove_trailing_space.sh --staged + +# Windows (PowerShell) +pwsh scripts/develop/remove_trailing_space.ps1 -Staged +```text + +### 绕过 Pre-Commit 检查 + +紧急情况下可以使用 `--no-verify` 跳过检查(不推荐): + +```bash +git commit --no-verify -m "紧急修复" +```text + +### 检查模式 + +remove_trailing_space 脚本支持多种模式: + +```bash +# 检查所有文件(不修改) +bash scripts/develop/remove_trailing_space.sh --check + +# 仅检查暂存文件 +bash scripts/develop/remove_trailing_space.sh --staged-check + +# 修复暂存文件 +bash scripts/develop/remove_trailing_space.sh --staged + +# 预览将要修改的内容 +bash scripts/develop/remove_trailing_space.sh --dry-run +```cpp + +## Pre-Push Hook + +### 功能 + +Pre-push Hook 在推送前执行 Docker 构建验证,确保代码可以成功构建。 + +### 分支验证策略 + +不同的分支有不同的验证策略: + +#### main 分支 + +- **验证级别**: X64 FastBuild + Tests +- **说明**: 快速验证,适用于日常开发推送 + +#### release 分支 + +根据版本号自动检测验证级别: + +| 版本变化 | 验证级别 | 说明 | +|---------|---------|------| +| Major (x.0.0 -> y.0.0) | X64 + ARM64 完整构建 + 测试 | 完整多架构验证 | +| Minor (x.y.0 -> x.z.0) | X64 完整构建 + 测试 | 标准验证 | +| Patch (x.y.z -> x.y.w) | X64 FastBuild + 测试 | 快速验证 | + +#### 其他分支 + +- 跳过验证,直接允许推送 + +### 版本号检查 + +推送到 main 或 release 分支时,会检查 `CMakeLists.txt` 中的版本号是否已更新: + +```cmake +project(CFDesktop VERSION X.Y.Z LANGUAGES CXX) +```text + +如果版本号未变更,推送将被阻止,提示信息如下: + +```cpp +======================================== +版本号未变更,推送被阻止 +======================================== + +当前版本: 0.13.1 +远程版本: 0.13.1 + +请按以下步骤更新版本号: + + 1. 更新 CMakeLists.txt 版本号: + project(CFDesktop VERSION X.Y.Z LANGUAGES CXX) + + 2. 更新 README.md 中的版本徽章 (如果存在) + +版本号规则: + - Patch: 0.13.1 -> 0.13.2 (bug 修复、小改动) + - Minor: 0.13.1 -> 0.14.0 (新功能) + - Major: 0.13.1 -> 1.0.0 (破坏性变更) +```text + +### 首次推送 + +如果是首次推送(远程没有版本标签),版本号检查会被跳过。 + +### 绕过 Pre-Push 检查 + +紧急情况下可以使用 `--no-verify` 跳过检查(不推荐): + +```bash +git push --no-verify +```bash + +### Docker 环境要求 + +Pre-push Hook 需要以下环境: + +- Docker 已安装并运行 +- Docker daemon 可用 + +如果 Docker 不可用,Hook 会显示安装提示并退出。 + +## 版本工具 + +`scripts/release/hooks/version_utils.sh` 提供版本号解析和验证级别检测功能。 + +### 可用函数 + +| 函数 | 说明 | +|------|------| +| `get_major_version ` | 获取主版本号 (X.y.z) | +| `get_minor_version ` | 获取次版本号 (x.Y.z) | +| `get_patch_version ` | 获取补丁版本号 (x.y.Z) | +| `determine_verify_level ` | 根据版本号确定验证级别 | +| `get_verify_level_description ` | 获取验证级别的描述 | +| `get_local_version` | 获取当前分支的版本标签 | +| `get_remote_version` | 获取远程 main 分支的版本标签 | +| `get_cmake_version ` | 获取 CMakeLists.txt 中定义的版本号 | +| `print_version_info ` | 打印版本信息(调试用) | + +### 使用示例 + +```bash +# 加载版本工具 +source scripts/release/hooks/version_utils.sh + +# 获取版本号组件 +VERSION="1.2.3" +MAJOR=$(get_major_version "$VERSION") # 输出: 1 +MINOR=$(get_minor_version "$VERSION") # 输出: 2 +PATCH=$(get_patch_version "$VERSION") # 输出: 3 + +# 确定验证级别 +LEVEL=$(determine_verify_level "1.2.3" "1.1.0") # 输出: minor +```text + +## 安装与卸载 + +### 安装 Hooks + +使用提供的安装脚本: + +```bash +bash scripts/release/hooks/install_hooks.sh +```text + +安装过程: + +1. 检查是否在 Git 仓库中 +2. 显示安装信息(源目录、目标目录) +3. 备份现有的自定义 Hook(如果有) +4. 复制 Hook 文件到 `.git/hooks/` 目录 +5. 设置执行权限 + +### 验证安装 + +安装完成后,可以验证 Hook 是否正确安装: + +```bash +ls -la .git/hooks/pre-commit .git/hooks/pre-push +```text + +### 卸载 Hooks + +直接删除 Hook 文件: + +```bash +rm .git/hooks/pre-commit .git/hooks/pre-push +```text + +### 备份机制 + +安装脚本会自动备份现有的自定义 Hook: + +- 如果现有 Hook 包含 "CFDesktop Git Hooks" 标记,直接覆盖 +- 如果是用户自定义的 Hook,备份为 `.backup.YYYYMMDDHHMMSS` + +## 常见问题 + +### Pre-Commit 失败 + +**问题**: 提交时提示空白字符检查失败 + +**解决方案**: + +```bash +# 自动修复暂存文件 +bash scripts/develop/remove_trailing_space.sh --staged + +# 或强制提交(不推荐) +git commit --no-verify -m "message" +```text + +### Pre-Push 验证失败 + +**问题**: Docker 构建验证失败 + +**解决方案**: + +1. 查看构建日志,找到失败原因 +2. 本地修复问题后重新提交 +3. 或使用 `--no-verify` 强制推送(不推荐) + +### 版本号检查失败 + +**问题**: 推送时提示版本号未变更 + +**解决方案**: + +1. 更新 `CMakeLists.txt` 中的版本号 +2. 提交版本号变更 +3. 重新推送 + +```bash +# 更新版本号示例 +vim CMakeLists.txt # 修改 VERSION x.y.z +git add CMakeLists.txt +git commit -m "chore: bump version to x.y.z" +git push +```text + +### Docker 未运行 + +**问题**: Pre-push Hook 提示 Docker daemon 未运行 + +**解决方案**: + +1. 启动 Docker Desktop (Windows/macOS) +2. 或启动 Docker 服务 (Linux) + +```bash +# Linux +sudo systemctl start docker +```text + +### clang-format 未安装 + +**问题**: C++ 代码无法自动格式化 + +**解决方案**: + +```bash +# Ubuntu/Debian +sudo apt install clang-format + +# macOS +brew install clang-format + +# Windows (使用 Chocolatey) +choco install clang-format +```text + +### ARM64 构建很慢 + +**问题**: release 分支 Major 版本验证时 ARM64 构建非常慢 + +**原因**: ARM64 使用 QEMU 仿真,速度较慢 + +**解决方案**: + +1. 耐心等待(首次构建可能需要 30+ 分钟) +2. 使用 `--fast-build` 缓存镜像 +3. 在原生 ARM64 环境中构建 + +## 相关文档 + +- [环境准备](./01_prerequisites.md) +- [构建系统](./03_build_system.md) +- [常见问题排查](./07_troubleshooting.md) +- [Docker 构建](./05_docker_build.md) diff --git a/document/development/07_troubleshooting.md b/document/development/07_troubleshooting.md index a30c81344..009ee6185 100644 --- a/document/development/07_troubleshooting.md +++ b/document/development/07_troubleshooting.md @@ -1,618 +1,623 @@ -# 常见问题排查 - -本文档提供 CFDesktop 项目开发和构建过程中常见问题的解决方案。 - -## 目录 - -- [Docker 相关问题](#docker-相关问题) -- [构建问题](#构建问题) -- [Git Hooks 问题](#git-hooks-问题) -- [性能优化](#性能优化) -- [获取更多帮助](#获取更多帮助) - -## Docker 相关问题 - -### Docker 未启动 - -**症状**: - -``` -error: Docker daemon is not running! -``` - -**解决方案**: - -```bash -# Linux -sudo systemctl start docker -sudo systemctl status docker - -# macOS/Windows -# 启动 Docker Desktop 应用程序 -``` - -**验证 Docker 运行状态**: - -```bash -docker info -docker run --rm hello-world -``` - ---- - -### Docker 镜像构建失败 - -**症状**: 构建镜像时出现错误 - -**常见原因和解决方案**: - -#### 1. 网络问题 - -```bash -# 使用国内镜像加速 -# 编辑 /etc/docker/daemon.json (Linux) 或 Docker Desktop 设置 -{ - "registry-mirrors": [ - "https://docker.mirrors.ustc.edu.cn", - "https://hub-mirror.c.163.com" - ] -} - -# 重启 Docker -sudo systemctl restart docker -``` - -#### 2. 基础镜像拉取失败 - -```bash -# 手动拉取基础镜像 -docker pull ubuntu:24.04 - -# 或使用镜像加速 -docker pull registry.cn-hangzhou.aliyuncs.com/library/ubuntu:24.04 -``` - -#### 3. 磁盘空间不足 - -```bash -# 清理未使用的镜像和容器 -docker system prune -a - -# 查看 Docker 占用空间 -docker system df -``` - -#### 4. 构建缓存问题 - -```bash -# 使用 --no-cache 重新构建 -docker build --no-cache -f scripts/docker/Dockerfile.build -t cfdesktop-build . -``` - ---- - -### 容器网络问题 - -**症状**: 容器内无法访问外部网络 - -**解决方案**: - -```bash -# 检查 Docker 网络 -docker network ls -docker network inspect bridge - -# 重启 Docker 网络服务 -sudo systemctl restart docker - -# 使用 host 网络模式(Linux) -docker run --rm --network host -v $(pwd):/project cfdesktop-build -``` - ---- - -### ARM64 QEMU 仿真慢 - -**症状**: ARM64 构建非常缓慢(30+ 分钟) - -**原因**: x86_64 主机使用 QEMU 仿真 ARM64 - -**解决方案**: - -1. **使用快速构建模式**: - -```bash -# 缓存镜像,避免重复构建 -bash scripts/build_helpers/docker_start.sh --fast-build --verify -``` - -2. **跳过 ARM64 验证**(如果不是必需): - -```bash -# 修改 pre-push hook 或使用 --no-verify -git push --no-verify -``` - -3. **在原生 ARM64 环境构建**: - - - 使用 ARM64 服务器/实例 - - 使用 Apple Silicon Mac - ---- - -### Windows 路径挂载问题 - -**症状**: 容器内找不到挂载的目录 - -**解决方案**: - -```bash -# 确保路径使用正确格式 -# Docker 需要 POSIX 路径 - -# Git Bash 自动转换 -bash scripts/build_helpers/docker_start.sh - -# PowerShell 需要手动转换 -$env:MSYS_NO_PATHCONV=1 -docker run --rm -v "D:/ProjectHome/CFDesktop:/project" cfdesktop-build -``` - ---- - -### 容器内权限问题 - -**症状**: 容器内无法写入文件 - -**解决方案**: - -```bash -# Linux: 使用当前用户运行 -docker run --rm -u $(id -u):$(id -g) -v $(pwd):/project cfdesktop-build - -# 或修改文件权限 -sudo chown -R $USER:$USER out/ -``` - -## 构建问题 - -### 编译错误 - -**症状**: C++ 编译时出现错误 - -**常见类型和解决方案**: - -#### 1. 头文件未找到 - -``` -fatal error: some_header.h: No such file or directory -``` - -**解决方案**: - -```bash -# 检查 CMake_INCLUDE_PATH -# 检查依赖是否正确安装 - -# Docker 内重新安装依赖 -docker run --rm -v $(pwd):/project cfdesktop-build bash -c " - apt-get update && \ - bash /project/scripts/dependency/install_build_dependencies.sh -" -``` - -#### 2. Qt 版本不匹配 - -``` -error: 'Qt6::xxx' not found -``` - -**解决方案**: - -```bash -# 检查 Qt 版本 -qmake --version -qmlplugindump --version - -# CMakeLists.txt 中指定正确版本 -find_package(Qt6 6.8.1 REQUIRED COMPONENTS Core Gui Widgets) -``` - -#### 3. C++ 标准问题 - -``` -error: 'xxx' is not a member of 'std' -``` - -**解决方案**: - -```cmake -# CMakeLists.txt -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -``` - ---- - -### 链接错误 - -**症状**: 编译成功但链接失败 - -**常见错误和解决方案**: - -#### 1. 未定义引用 - -``` -undefined reference to 'xxx' -``` - -**解决方案**: - -```cmake -# 检查链接库是否正确添加 -target_link_libraries(your_target - PRIVATE - Qt6::Core - Qt6::Widgets - # 添加缺失的库 -) -``` - -#### 2. 库文件未找到 - -``` -cannot find -lxxx -``` - -**解决方案**: - -```bash -# 检查库文件路径 -ldconfig -p | grep xxx - -# 添加库路径 -link_directories(/path/to/lib) -``` - ---- - -### CMake 配置错误 - -**症状**: CMake 配置阶段失败 - -**常见问题**: - -#### 1. 构建类型未指定 - -``` -CMAKE_BUILD_TYPE is not set -``` - -**解决方案**: - -```bash -# 使用正确的配置脚本 -bash scripts/build_helpers/linux_develop_build.sh -c build_develop_config.ini -``` - -#### 2. 工具链问题 - -``` -CMAKE_CXX_COMPILER not set -``` - -**解决方案**: - -```bash -# 指定工具链 -cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/toolchain.cmake -S . -B out/build - -# 或使用预设工具链 -cmake -DUSE_TOOLCHAIN=linux/llvm -S . -B out/build -``` - ---- - -### Qt 相关问题 - -#### Qt 未找到 - -``` -Could not find Qt6 -``` - -**解决方案**: - -```bash -# 设置 Qt 路径 -export Qt6_DIR=/path/to/Qt/6.8.1/gcc_64/lib/cmake/Qt6 -cmake -DQt6_DIR=/path/to/Qt/6.8.1/gcc_64/lib/cmake/Qt6 -S . -B out/build -``` - -#### Qt 模块缺失 - -``` -Qt6 component 'Widgets' not found -``` - -**解决方案**: - -```bash -# Docker 内重新安装 Qt -docker run --rm -v $(pwd):/project cfdesktop-build bash -c " - python3 -m aqt install-qt --outputdir /opt linux desktop 6.8.1 gcc_64 -m qtbase qttools -" -``` - ---- - -### 测试失败 - -**症状**: 构建成功但测试失败 - -**调试步骤**: - -```bash -# 详细测试输出 -bash scripts/build_helpers/linux_run_tests.sh ci -c build_ci_config.ini --verbose - -# 运行特定测试 -ctest -R test_name --verbose - -# 输出到文件 -ctest --output-on-failure > test_results.txt -``` - -## Git Hooks 问题 - -### Pre-Commit 失败 - -**症状**: 提交时被 Hook 阻止 - -#### 1. 空白字符检查失败 - -``` -error: 空白字符检查失败 -``` - -**解决方案**: - -```bash -# 自动修复 -bash scripts/develop/remove_trailing_space.sh --staged - -# 或强制提交(不推荐) -git commit --no-verify -m "message" -``` - -#### 2. clang-format 失败 - -``` -error: clang-format failed -``` - -**解决方案**: - -```bash -# 安装 clang-format -sudo apt install clang-format - -# 或跳过格式化(删除 Hook 中的格式化部分) -``` - ---- - -### Pre-Push 验证失败 - -**症状**: 推送时被 Hook 阻止 - -#### 1. 版本号检查失败 - -``` -error: 版本号未变更,推送被阻止 -``` - -**解决方案**: - -```bash -# 更新版本号 -vim CMakeLists.txt # 修改 VERSION x.y.z - -# 提交版本变更 -git add CMakeLists.txt -git commit -m "chore: bump version to x.y.z" - -# 重新推送 -git push -``` - -#### 2. Docker 构建失败 - -``` -error: Docker 构建验证失败 -``` - -**解决方案**: - -```bash -# 本地手动验证 -bash scripts/build_helpers/docker_start.sh --verify - -# 查看详细日志 -# 日志保存在 scripts/docker/logger/ci_build_*.log - -# 或强制推送(不推荐) -git push --no-verify -``` - ---- - -### Hook 未执行 - -**症状**: Hook 脚本没有运行 - -**解决方案**: - -```bash -# 检查 Hook 是否安装 -ls -la .git/hooks/pre-commit .git/hooks/pre-push - -# 重新安装 -bash scripts/release/hooks/install_hooks.sh - -# 检查执行权限 -chmod +x .git/hooks/pre-commit .git/hooks/pre-push -``` - ---- - -### Git Bash 路径问题 - -**症状**: Windows Git Bash 中 Hook 路径错误 - -**解决方案**: - -```bash -# 使用 MSYS_NO_PATHCONV=1 -export MSYS_NO_PATHCONV=1 - -# 或修改 Hook 脚本,使用项目根目录的绝对路径 -``` - -## 性能优化 - -### 使用快速构建缓存 - -```bash -# 复用现有 Docker 镜像 -bash scripts/build_helpers/docker_start.sh --fast-build - -# 结合验证使用 -bash scripts/build_helpers/docker_start.sh --fast-build --verify -``` - ---- - -### 配置 ccache - -```bash -# 在 Docker 容器内配置 ccache -docker run --rm -v $(pwd):/project -v ~/.ccache:/root/.ccache cfdesktop-build - -# 或使用环境变量 -export CCACHE_DIR=~/.ccache -export CCACHE_MAXSIZE=10G -``` - ---- - -### 并行编译调整 - -```bash -# 限制并行任务数(减少内存压力) -cmake -S . -B out/build -DCMAKE_BUILD_PARALLEL_LEVEL=4 - -# 或使用 make -make -j4 -``` - ---- - -### 本地构建(跳过 Docker) - -对于频繁的本地开发,可以使用宿主机环境: - -```bash -# 设置 Qt 路径 -export Qt6_DIR=/path/to/Qt/6.8.1/gcc_64/lib/cmake/Qt6 - -# 配置和构建 -cmake -S . -B out/build -DCMAKE_BUILD_TYPE=Debug -cmake --build out/build --parallel -``` - ---- - -### 增量构建 - -```bash -# 使用快速构建脚本(不清理) -bash scripts/build_helpers/linux_fast_develop_build.sh -c build_develop_config.ini - -# 只构建变更的目标 -cmake --build out/build --target -``` - -## 获取更多帮助 - -### 查看日志 - -```bash -# Docker 构建日志 -ls scripts/docker/logger/ci_build_*.log - -# CMake 配置日志 -cat out/build/CMakeFiles/CMakeOutput.log -cat out/build/CMakeFiles/CMakeError.log -``` - ---- - -### 调试模式 - -```bash -# Docker 交互式调试 -bash scripts/build_helpers/docker_start.sh -# 在容器内手动运行命令 - -# CMake 详细输出 -cmake -S . -B out/build --trace-expand - -# Make 详细输出 -make VERBOSE=1 -``` - ---- - -### 项目资源 - -- **GitHub Issues**: [提交问题](https://github.com/your-org/CFDesktop/issues) -- **文档首页**: [文档索引](../index.md) -- **开发指南**: [环境准备](./01_prerequisites.md) -- **构建系统**: [构建系统详解](./03_build_system.md) - ---- - -### 联系方式 - -- **项目维护者**: 通过 GitHub Issues 联系 -- **讨论区**: GitHub Discussions - ---- - -### 报告问题模板 - -提交问题时,请包含以下信息: - -```markdown -## 环境信息 -- 操作系统: [Ubuntu 24.04 / Windows 11 / macOS 14] -- Docker 版本: [Docker version 24.0.7] -- Qt 版本: [6.8.1] -- CMake 版本: [3.28.0] - -## 问题描述 -[简要描述问题] - -## 复现步骤 -1. [步骤 1] -2. [步骤 2] - -## 错误信息 -``` -[粘贴错误输出] -``` - -## 已尝试的解决方案 -[列出已尝试的方法] -``` +--- +title: 常见问题排查 +description: 本文档提供 CFDesktop 项目开发和构建过程中常见问题的解决方案。 +--- + +# 常见问题排查 + +本文档提供 CFDesktop 项目开发和构建过程中常见问题的解决方案。 + +## 目录 + +- [Docker 相关问题](#docker-相关问题) +- [构建问题](#构建问题) +- [Git Hooks 问题](#git-hooks-问题) +- [性能优化](#性能优化) +- [获取更多帮助](#获取更多帮助) + +## Docker 相关问题 + +### Docker 未启动 + +**症状**: + +```text +error: Docker daemon is not running! +```text + +**解决方案**: + +```bash +# Linux +sudo systemctl start docker +sudo systemctl status docker + +# macOS/Windows +# 启动 Docker Desktop 应用程序 +```text + +**验证 Docker 运行状态**: + +```bash +docker info +docker run --rm hello-world +```yaml + +--- + +### Docker 镜像构建失败 + +**症状**: 构建镜像时出现错误 + +**常见原因和解决方案**: + +#### 1. 网络问题 + +```bash +# 使用国内镜像加速 +# 编辑 /etc/docker/daemon.json (Linux) 或 Docker Desktop 设置 +{ + "registry-mirrors": [ + "https://docker.mirrors.ustc.edu.cn", + "https://hub-mirror.c.163.com" + ] +} + +# 重启 Docker +sudo systemctl restart docker +```text + +#### 2. 基础镜像拉取失败 + +```bash +# 手动拉取基础镜像 +docker pull ubuntu:24.04 + +# 或使用镜像加速 +docker pull registry.cn-hangzhou.aliyuncs.com/library/ubuntu:24.04 +```text + +#### 3. 磁盘空间不足 + +```bash +# 清理未使用的镜像和容器 +docker system prune -a + +# 查看 Docker 占用空间 +docker system df +```text + +#### 4. 构建缓存问题 + +```bash +# 使用 --no-cache 重新构建 +docker build --no-cache -f scripts/docker/Dockerfile.build -t cfdesktop-build . +```yaml + +--- + +### 容器网络问题 + +**症状**: 容器内无法访问外部网络 + +**解决方案**: + +```bash +# 检查 Docker 网络 +docker network ls +docker network inspect bridge + +# 重启 Docker 网络服务 +sudo systemctl restart docker + +# 使用 host 网络模式(Linux) +docker run --rm --network host -v $(pwd):/project cfdesktop-build +```yaml + +--- + +### ARM64 QEMU 仿真慢 + +**症状**: ARM64 构建非常缓慢(30+ 分钟) + +**原因**: x86_64 主机使用 QEMU 仿真 ARM64 + +**解决方案**: + +1. **使用快速构建模式**: + +```bash +# 缓存镜像,避免重复构建 +bash scripts/build_helpers/docker_start.sh --fast-build --verify +```text + +2. **跳过 ARM64 验证**(如果不是必需): + +```bash +# 修改 pre-push hook 或使用 --no-verify +git push --no-verify +```yaml + +3. **在原生 ARM64 环境构建**: + + - 使用 ARM64 服务器/实例 + - 使用 Apple Silicon Mac + +--- + +### Windows 路径挂载问题 + +**症状**: 容器内找不到挂载的目录 + +**解决方案**: + +```bash +# 确保路径使用正确格式 +# Docker 需要 POSIX 路径 + +# Git Bash 自动转换 +bash scripts/build_helpers/docker_start.sh + +# PowerShell 需要手动转换 +$env:MSYS_NO_PATHCONV=1 +docker run --rm -v "D:/ProjectHome/CFDesktop:/project" cfdesktop-build +```yaml + +--- + +### 容器内权限问题 + +**症状**: 容器内无法写入文件 + +**解决方案**: + +```bash +# Linux: 使用当前用户运行 +docker run --rm -u $(id -u):$(id -g) -v $(pwd):/project cfdesktop-build + +# 或修改文件权限 +sudo chown -R $USER:$USER out/ +```text + +## 构建问题 + +### 编译错误 + +**症状**: C++ 编译时出现错误 + +**常见类型和解决方案**: + +#### 1. 头文件未找到 + +```text +fatal error: some_header.h: No such file or directory +```text + +**解决方案**: + +```bash +# 检查 CMake_INCLUDE_PATH +# 检查依赖是否正确安装 + +# Docker 内重新安装依赖 +docker run --rm -v $(pwd):/project cfdesktop-build bash -c " + apt-get update && \ + bash /project/scripts/dependency/install_build_dependencies.sh +" +```text + +#### 2. Qt 版本不匹配 + +```text +error: 'Qt6::xxx' not found +```text + +**解决方案**: + +```bash +# 检查 Qt 版本 +qmake --version +qmlplugindump --version + +# CMakeLists.txt 中指定正确版本 +find_package(Qt6 6.8.1 REQUIRED COMPONENTS Core Gui Widgets) +```text + +#### 3. C++ 标准问题 + +```text +error: 'xxx' is not a member of 'std' +```text + +**解决方案**: + +```cmake +# CMakeLists.txt +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +```yaml + +--- + +### 链接错误 + +**症状**: 编译成功但链接失败 + +**常见错误和解决方案**: + +#### 1. 未定义引用 + +```text +undefined reference to 'xxx' +```text + +**解决方案**: + +```cmake +# 检查链接库是否正确添加 +target_link_libraries(your_target + PRIVATE + Qt6::Core + Qt6::Widgets + # 添加缺失的库 +) +```text + +#### 2. 库文件未找到 + +```bash +cannot find -lxxx +```text + +**解决方案**: + +```bash +# 检查库文件路径 +ldconfig -p | grep xxx + +# 添加库路径 +link_directories(/path/to/lib) +```yaml + +--- + +### CMake 配置错误 + +**症状**: CMake 配置阶段失败 + +**常见问题**: + +#### 1. 构建类型未指定 + +```text +CMAKE_BUILD_TYPE is not set +```text + +**解决方案**: + +```bash +# 使用正确的配置脚本 +bash scripts/build_helpers/linux_develop_build.sh -c build_develop_config.ini +```text + +#### 2. 工具链问题 + +```text +CMAKE_CXX_COMPILER not set +```text + +**解决方案**: + +```bash +# 指定工具链 +cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/toolchain.cmake -S . -B out/build + +# 或使用预设工具链 +cmake -DUSE_TOOLCHAIN=linux/llvm -S . -B out/build +```yaml + +--- + +### Qt 相关问题 + +#### Qt 未找到 + +```bash +Could not find Qt6 +```text + +**解决方案**: + +```bash +# 设置 Qt 路径 +export Qt6_DIR=/path/to/Qt/6.8.1/gcc_64/lib/cmake/Qt6 +cmake -DQt6_DIR=/path/to/Qt/6.8.1/gcc_64/lib/cmake/Qt6 -S . -B out/build +```text + +#### Qt 模块缺失 + +```text +Qt6 component 'Widgets' not found +```text + +**解决方案**: + +```bash +# Docker 内重新安装 Qt +docker run --rm -v $(pwd):/project cfdesktop-build bash -c " + python3 -m aqt install-qt --outputdir /opt linux desktop 6.8.1 gcc_64 -m qtbase qttools +" +```yaml + +--- + +### 测试失败 + +**症状**: 构建成功但测试失败 + +**调试步骤**: + +```bash +# 详细测试输出 +bash scripts/build_helpers/linux_run_tests.sh ci -c build_ci_config.ini --verbose + +# 运行特定测试 +ctest -R test_name --verbose + +# 输出到文件 +ctest --output-on-failure > test_results.txt +```text + +## Git Hooks 问题 + +### Pre-Commit 失败 + +**症状**: 提交时被 Hook 阻止 + +#### 1. 空白字符检查失败 + +```text +error: 空白字符检查失败 +```text + +**解决方案**: + +```bash +# 自动修复 +bash scripts/develop/remove_trailing_space.sh --staged + +# 或强制提交(不推荐) +git commit --no-verify -m "message" +```text + +#### 2. clang-format 失败 + +```text +error: clang-format failed +```text + +**解决方案**: + +```bash +# 安装 clang-format +sudo apt install clang-format + +# 或跳过格式化(删除 Hook 中的格式化部分) +```yaml + +--- + +### Pre-Push 验证失败 + +**症状**: 推送时被 Hook 阻止 + +#### 1. 版本号检查失败 + +```text +error: 版本号未变更,推送被阻止 +```text + +**解决方案**: + +```bash +# 更新版本号 +vim CMakeLists.txt # 修改 VERSION x.y.z + +# 提交版本变更 +git add CMakeLists.txt +git commit -m "chore: bump version to x.y.z" + +# 重新推送 +git push +```text + +#### 2. Docker 构建失败 + +```text +error: Docker 构建验证失败 +```text + +**解决方案**: + +```bash +# 本地手动验证 +bash scripts/build_helpers/docker_start.sh --verify + +# 查看详细日志 +# 日志保存在 scripts/docker/logger/ci_build_*.log + +# 或强制推送(不推荐) +git push --no-verify +```yaml + +--- + +### Hook 未执行 + +**症状**: Hook 脚本没有运行 + +**解决方案**: + +```bash +# 检查 Hook 是否安装 +ls -la .git/hooks/pre-commit .git/hooks/pre-push + +# 重新安装 +bash scripts/release/hooks/install_hooks.sh + +# 检查执行权限 +chmod +x .git/hooks/pre-commit .git/hooks/pre-push +```yaml + +--- + +### Git Bash 路径问题 + +**症状**: Windows Git Bash 中 Hook 路径错误 + +**解决方案**: + +```bash +# 使用 MSYS_NO_PATHCONV=1 +export MSYS_NO_PATHCONV=1 + +# 或修改 Hook 脚本,使用项目根目录的绝对路径 +```text + +## 性能优化 + +### 使用快速构建缓存 + +```bash +# 复用现有 Docker 镜像 +bash scripts/build_helpers/docker_start.sh --fast-build + +# 结合验证使用 +bash scripts/build_helpers/docker_start.sh --fast-build --verify +```yaml + +--- + +### 配置 ccache + +```bash +# 在 Docker 容器内配置 ccache +docker run --rm -v $(pwd):/project -v ~/.ccache:/root/.ccache cfdesktop-build + +# 或使用环境变量 +export CCACHE_DIR=~/.ccache +export CCACHE_MAXSIZE=10G +```yaml + +--- + +### 并行编译调整 + +```bash +# 限制并行任务数(减少内存压力) +cmake -S . -B out/build -DCMAKE_BUILD_PARALLEL_LEVEL=4 + +# 或使用 make +make -j4 +```yaml + +--- + +### 本地构建(跳过 Docker) + +对于频繁的本地开发,可以使用宿主机环境: + +```bash +# 设置 Qt 路径 +export Qt6_DIR=/path/to/Qt/6.8.1/gcc_64/lib/cmake/Qt6 + +# 配置和构建 +cmake -S . -B out/build -DCMAKE_BUILD_TYPE=Debug +cmake --build out/build --parallel +```yaml + +--- + +### 增量构建 + +```bash +# 使用快速构建脚本(不清理) +bash scripts/build_helpers/linux_fast_develop_build.sh -c build_develop_config.ini + +# 只构建变更的目标 +cmake --build out/build --target +```text + +## 获取更多帮助 + +### 查看日志 + +```bash +# Docker 构建日志 +ls scripts/docker/logger/ci_build_*.log + +# CMake 配置日志 +cat out/build/CMakeFiles/CMakeOutput.log +cat out/build/CMakeFiles/CMakeError.log +```yaml + +--- + +### 调试模式 + +```bash +# Docker 交互式调试 +bash scripts/build_helpers/docker_start.sh +# 在容器内手动运行命令 + +# CMake 详细输出 +cmake -S . -B out/build --trace-expand + +# Make 详细输出 +make VERBOSE=1 +```yaml + +--- + +### 项目资源 + +- **GitHub Issues**: [提交问题](https://github.com/your-org/CFDesktop/issues) +- **文档首页**: [文档索引](../index.md) +- **开发指南**: [环境准备](./01_prerequisites.md) +- **构建系统**: [构建系统详解](./03_build_system.md) + +--- + +### 联系方式 + +- **项目维护者**: 通过 GitHub Issues 联系 +- **讨论区**: GitHub Discussions + +--- + +### 报告问题模板 + +提交问题时,请包含以下信息: + +```markdown +## 环境信息 +- 操作系统: [Ubuntu 24.04 / Windows 11 / macOS 14] +- Docker 版本: [Docker version 24.0.7] +- Qt 版本: [6.8.1] +- CMake 版本: [3.28.0] + +## 问题描述 +[简要描述问题] + +## 复现步骤 +1. [步骤 1] +2. [步骤 2] + +## 错误信息 +```text +[粘贴错误输出] +```text + +## 已尝试的解决方案 +[列出已尝试的方法] +```text diff --git a/document/development/README.md b/document/development/README.md index 431ceb457..f894365ae 100644 --- a/document/development/README.md +++ b/document/development/README.md @@ -1,168 +1,173 @@ -# 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 -``` - -### 快速开始 - -```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 -``` - ---- - -## 下一步 - -请按照文档顺序阅读: - -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** - 为嵌入式设备打造的现代化桌面框架 - -
+--- +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/ai-assistant-guide.md b/document/development/ai-assistant-guide.md new file mode 100644 index 000000000..1eb6933ff --- /dev/null +++ b/document/development/ai-assistant-guide.md @@ -0,0 +1,38 @@ +--- +title: AI 辅助开发指南 +description: 本页是通用 AI 协作入口,适用于 Claude Code、Codex 或其他代码助手。私人配置文件 +--- + +# AI 辅助开发指南 + +本页是通用 AI 协作入口,适用于 Claude Code、Codex 或其他代码助手。私人配置文件如 `CLAUDE.md`、`MEMORY.md`、`.claude/`、`.codex/` 不属于仓库公共基建。 + +## 优先读取 + +1. `README.md` +2. `document/status/current.md` +3. `document/development/index.md` +4. `document/design_stage/system_architecture_overview.md` +5. `document/todo/desktop/milestone_00_overview.md` + +## 检索习惯 + +- 优先使用 `rg` 和 `rg --files`。 +- 先读最近的 `CMakeLists.txt`,再判断 target 和依赖方向。 +- 先确认文件是否已有测试,再新增或修改测试。 +- 遇到历史状态报告时,先和 `document/status/current.md` 对照。 + +## 项目约束 + +- `base -> ui -> desktop` 是严格单向依赖。 +- `base/` 不应包含 UI 或 desktop 概念。 +- `ui/` 不应包含 desktop 概念。 +- 新公共 API 应补 Doxygen 注释。 +- 文档站使用 VitePress,不再使用 MkDocs。 + +## 安全边界 + +- 不提交私人 AI 配置。 +- 不修改 `out/`、`node_modules/`、VitePress dist/cache 等生成物。 +- 不把历史 TODO 当成当前事实源。 +- 大规模删除或重排文档前,先确认它是否仍被导航引用。 diff --git a/document/development/index.md b/document/development/index.md index c0456efe2..dce85f3f7 100644 --- a/document/development/index.md +++ b/document/development/index.md @@ -1,11 +1,36 @@ -# Development +--- +title: 开发指南 +description: 本节面向重新启动开发时的日常入口。,1. 当前项目状态 +--- -> Welcome to the Development section. +# 开发指南 -## Overview +本节面向重新启动开发时的日常入口。 -Documentation and resources for Development. +## 推荐阅读顺序 ---- +1. [当前项目状态](../status/current.md) +2. [前置环境](01_prerequisites.md) +3. [快速开始](02_quick_start.md) +4. [构建系统](03_build_system.md) +5. [开发工具](04_development_tools.md) +6. [Git Hooks](06_git_hooks.md) +7. [AI 辅助开发指南](ai-assistant-guide.md) + +## 常用命令 + +```bash +# Linux 快速构建 +bash scripts/build_helpers/linux_fast_develop_build.sh + +# Linux 完整构建 + 测试 +bash scripts/build_helpers/linux_develop_build.sh + +# 仅运行已有 CTest +QT_QPA_PLATFORM=offscreen ctest --test-dir out/build_develop/test --output-on-failure -*Last updated: 2026-03-20* +# 文档开发 +pnpm install +pnpm dev +pnpm build +```text diff --git a/document/index.md b/document/index.md index 7929842e6..e600b76a0 100644 --- a/document/index.md +++ b/document/index.md @@ -1,71 +1,35 @@ -# CFDesktop - -**跨平台桌面环境框架** -- 基于 Qt 的 Material Design 3 实现 - --- - -CFDesktop 是一套面向嵌入式和桌面场景的 UI 框架,包含完整的硬件探针层、基础工具库、五层 Material Design 3 架构,以及桌面 Shell 基础设施。 - -## 快速导航 - -
- -- :material-book-open-page-variant:{ .lg .middle } **开发手册** - - --- - - API 参考、组件文档、架构详解、平台实现指南 - - [:octicons-arrow-right-24: 浏览手册](HandBook/index.md) - -- :material-developer-board:{ .lg .middle } **开发指南** - - --- - - 环境搭建、构建系统、开发工具、Docker、Git Hooks - - [:octicons-arrow-right-24: 开始开发](development/01_prerequisites.md) - -- :material-palette:{ .lg .middle } **UI 框架** - - --- - - 五层架构设计:数学工具 → 主题引擎 → 动画引擎 → Material 行为 → Widget 适配 - - [:octicons-arrow-right-24: 了解架构](HandBook/ui/architecture/index.md) - -- :material-api:{ .lg .middle } **API 参考** - - --- - - Doxygen 自动生成的 C++ API 文档 - - [:octicons-arrow-right-24: 查看 API](HandBook/api/index.md) - -- :material-clipboard-check:{ .lg .middle } **项目进度** - - --- - - TODO 看板、里程碑追踪、已完成状态 - - [:octicons-arrow-right-24: 查看进度](todo/index.md) - -- :material-notebook:{ .lg .middle } **技术笔记** - - --- - - 设计决策、架构分析、模式实战 - - [:octicons-arrow-right-24: 阅读笔记](notes/index.md) - -
- -## 项目概况 - -| 项目 | 说明 | -|------|------| -| 语言 | C++17 / CMake | -| UI 框架 | Qt 6 + Material Design 3 | -| 目标平台 | Linux (X11/Wayland)、Windows、Embedded | -| 构建系统 | CMake + CI/CD (GitHub Actions) | -| 文档 | MkDocs Material + Doxygen | +layout: home + +hero: + name: "CFDesktop" + text: "嵌入式桌面框架" + tagline: 基于 Qt 6 / C++23 / Material Design 3 的跨平台桌面 Shell 实验场 + image: + src: /Awesome-Embedded.png + alt: CFDesktop Logo + actions: + - theme: brand + text: 开始开发 + link: /development/ + - theme: alt + text: 当前状态 + link: /status/current + - theme: alt + text: 桌面路线图 + link: /todo/desktop/ + +features: + - title: 三层架构 + details: base、ui、desktop 单向依赖,分别承载基础库、Material UI 框架和桌面 Shell。 + link: /design_stage/system_architecture_overview + - title: Material Design 3 + details: 已有主题、Token、动画、行为层和 P0/P1 控件基础,适合作为 Shell UI 底座。 + link: /HandBook/ui/ + - title: 可见桌面闭环 + details: 下一阶段优先推进状态栏、任务栏、应用启动器和窗口管理联动。 + link: /todo/desktop/milestone_00_overview + - title: VitePress 文档站 + details: 文档系统已迁移到 VitePress,使用 npm 脚本和 GitHub Actions 构建发布。 + link: /scripts/document/ +--- diff --git a/document/notes/.pages b/document/notes/.pages deleted file mode 100644 index b903c32c0..000000000 --- a/document/notes/.pages +++ /dev/null @@ -1,8 +0,0 @@ -title: 技术笔记 -icon: material/notebook -nav: - - 01 · 从 bool 到 QFlags: 01-Desktop-Behavior-Modeling-From-Bool-To-QFlags.md - - 02 · Qt 窗口行为解析: 02-Qt-Window-Behavior-Analysis.md - - 03 · 策略模式实战: 03-Desktop-Strategy-Pattern-Design.md - - 04 · 行为系统架构: 04-Desktop-Behavior-System-Architecture.md - - 05 · Logger 单例链接架构: 05-Logger-Singleton-Link-Architecture.md 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 d70d905de..a1443c493 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,3 +1,8 @@ +--- +title: 桌面行为建模:从 bool 到 QFlags +description: 1. 问题背景:为什么 struct bool 不够用 +--- + # 桌面行为建模:从 bool 到 QFlags ## 目录 @@ -30,7 +35,7 @@ struct DesktopBehavior { bool transparent; // 是否透明 bool clickThrough; // 是否点击穿透 }; -``` +```text ### 方案缺陷分析 @@ -40,7 +45,7 @@ struct DesktopBehavior { ```cpp sizeof(DesktopBehavior) // 通常为 8 × bool = 8 字节(甚至更多由于对齐) -``` +```text 虽然 8 字节看起来不大,但当我们需要存储大量行为配置时,这种内存开销会变得显著。更重要的是,bool 类型的操作通常不是原子的,在多线程环境下需要额外的同步机制。 @@ -53,7 +58,7 @@ DesktopBehavior b2 = {false, true, false, false, false, false, false, false}; // 如何得到"全屏且无边框"的行为? // 需要逐字段手动合并,繁琐且容易出错 -``` +```text #### 3. 扩展性差 @@ -70,7 +75,7 @@ struct DesktopBehavior { // ... 原有字段 bool newFeature; // 新增字段 }; -``` +```text #### 4. 不适合策略系统 @@ -96,13 +101,13 @@ DesktopBehavior combine(const DesktopBehavior& a, const DesktopBehavior& b) { // ... 需要处理每个字段 }; } -``` +```text ### 更好的方案:Bitmask 模型 计算机科学中,处理多个独立布尔值的经典方法是使用位掩码(Bitmask): -``` +```text 位 0: Fullscreen 位 1: Frameless 位 2: StayOnTop @@ -110,7 +115,7 @@ DesktopBehavior combine(const DesktopBehavior& a, const DesktopBehavior& b) { 位 4: AllowResize 位 5: AvoidSystemUI ... -``` +```yaml 这种方式的优势: @@ -147,7 +152,7 @@ enum DesktopBehaviorFlag { TransparentFlag = 1 << 6, // 二进制: 01000000 ClickThroughFlag = 1 << 7, // 二进制: 10000000 }; -``` +```text ### 标志位组合 @@ -159,7 +164,7 @@ unsigned int behaviors = FullscreenFlag | FramelessFlag; // 添加更多特性 behaviors |= StayOnTopFlag; // 结果: 00000111 (全屏 + 无边框 + 置顶) -``` +```text ### 标志位测试 @@ -167,7 +172,7 @@ behaviors |= StayOnTopFlag; // ✅ 使用位运算测试特性 bool isFullscreen = (behaviors & FullscreenFlag) != 0; bool isFrameless = (behaviors & FramelessFlag) != 0; -``` +```text ### 标志位移除 @@ -175,7 +180,7 @@ bool isFrameless = (behaviors & FramelessFlag) != 0; // ✅ 使用位运算移除特性 behaviors &= ~FullscreenFlag; // 移除全屏特性 // 结果: 00000110 -``` +```yaml --- @@ -202,7 +207,7 @@ unsigned int behaviors = FullscreenFlag | 123; // 编译通过,但语义错 // ✅ QFlags 实现 - 有类型检查 QFlags behaviors = FullscreenFlag | FramelessFlag; // behaviors = FullscreenFlag | 123; // 编译错误 -``` +```text #### 2. 运算符友好 @@ -228,7 +233,7 @@ auto inverted = ~b1; b1 |= b2; b1 &= b2; b1 ^= b2; -``` +```text #### 3. QVariant / MetaObject 兼容 @@ -237,7 +242,7 @@ b1 ^= b2; ```cpp QVariant var = QVariant::fromValue(fullscreen | frameless); auto flags = var.value>(); -``` +```text ### QFlags 声明宏 @@ -258,21 +263,21 @@ public: 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() 方法 @@ -290,7 +295,7 @@ if (behaviors.testFlag(FullscreenFlag)) { if ((behaviors & FullscreenFlag) != 0) { // ... } -``` +```text ### Q_ENUM 与 Q_FLAGS 集成 @@ -309,7 +314,7 @@ public: Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) Q_FLAGS(DesktopBehaviors) }; -``` +```text 这样做的优势: @@ -364,7 +369,7 @@ int main() { return 0; } -``` +```yaml --- @@ -403,7 +408,7 @@ Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) Q_DECLARE_OPERATORS_FOR_FLAGS(DesktopBehaviors) } // namespace desktop -``` +```text ### 常用预定义组合 @@ -420,7 +425,7 @@ constexpr auto WidgetBehavior = DesktopBehaviorFlag::Tool | DesktopBehav constexpr auto SplashBehavior = DesktopBehaviorFlag::Splash | DesktopBehaviorFlag::Frameless | DesktopBehaviorFlag::StayOnTop; } // namespace desktop -``` +```text ### 行为查询接口 @@ -508,7 +513,7 @@ private: }; } // namespace desktop -``` +```text ### 行为修改接口 @@ -597,7 +602,7 @@ private: }; } // namespace desktop -``` +```text ### 与 QWidget 集成 @@ -687,7 +692,7 @@ public: }; } // namespace desktop -``` +```yaml --- @@ -709,7 +714,7 @@ enum class StateFlag { IsFullscreen = 1 << 0, // 当前是否全屏 IsResizing = 1 << 1, // 当前是否正在调整大小 }; -``` +```text ### 2. 不返回单个枚举值 @@ -725,7 +730,7 @@ DesktopBehaviorFlag getCurrentBehavior() { DesktopBehaviors getCurrentBehaviors() { return DesktopBehaviorFlag::Fullscreen | DesktopBehaviorFlag::Frameless; } -``` +```text ### 3. 保持 ABI 可扩展 @@ -748,7 +753,7 @@ enum class DesktopBehaviorFlag { StayOnTop = 3, // 添加新标志容易冲突 }; -``` +```text ### 4. 使用 enum class 提高类型安全 @@ -766,7 +771,7 @@ enum DesktopBehaviorFlag { Fullscreen = 1 << 0, // ... }; -``` +```text `enum class` 的优势: @@ -808,7 +813,7 @@ public: behaviors.testFlag(DesktopBehaviorFlag::Popup); } }; -``` +```text ### 6. 使用 constexpr 编译时计算 @@ -821,7 +826,7 @@ constexpr auto FullscreenBehavior = DesktopBehaviorFlag::Fullscreen; constexpr auto FramelessBehavior = DesktopBehaviorFlag::Frameless; constexpr auto AlwaysOnTopBehavior = DesktopBehaviorFlag::StayOnTop | DesktopBehaviorFlag::Frameless; -``` +```text ### 7. 文档化标志位含义 @@ -865,7 +870,7 @@ enum class DesktopBehaviorFlag { // ... 更多标志位 }; -``` +```yaml --- @@ -880,7 +885,7 @@ Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) DesktopBehaviors b = DesktopBehaviorFlag::Fullscreen | DesktopBehaviorFlag::Frameless; // 编译错误:没有匹配的 operator| -``` +```text **解决方案**:总是成对使用这两个宏 @@ -888,7 +893,7 @@ DesktopBehaviors b = DesktopBehaviorFlag::Fullscreen | DesktopBehaviorFlag::Fram // ✅ 正确 Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) Q_DECLARE_OPERATORS_FOR_FLAGS(DesktopBehaviors) -``` +```text 参考:[QFlags tutorial - Qt Wiki](https://wiki.qt.io/QFlags_tutorial) @@ -902,14 +907,14 @@ enum class DesktopBehaviorFlag { DesktopBehaviorFlag flag = DesktopBehaviorFlag::Fullscreen; int value = flag; // 编译错误 -``` +```text **解决方案**:显式转换 ```cpp // ✅ 正确 int value = static_cast(flag); -``` +```text ### 陷阱 3:位运算符优先级 @@ -923,7 +928,7 @@ if (b & DesktopBehaviorFlag::Fullscreen == DesktopBehaviorFlag::None) { // 实际解析为:if (b & (DesktopBehaviorFlag::Fullscreen == DesktopBehaviorFlag::None)) // 即:if (b & false) // 即:if (b & 0) -``` +```text **解决方案**:使用括号 @@ -932,7 +937,7 @@ if (b & DesktopBehaviorFlag::Fullscreen == DesktopBehaviorFlag::None) { if ((b & DesktopBehaviorFlag::Fullscreen) == DesktopBehaviorFlag::None) { // 或者使用 testFlag() if (!b.testFlag(DesktopBehaviorFlag::Fullscreen)) { -``` +```text ### 陷阱 4:移除标志位时忘记取反 @@ -945,14 +950,14 @@ b &= DesktopBehaviorFlag::Fullscreen; // 正确的做法是: b &= ~DesktopBehaviorFlag::Fullscreen; // 结果:只剩下 Frameless -``` +```text ### 陷阱 5:标志位冲突 ```cpp // ❌ 错误:StayOnTop 和 StayOnBottom 不应该同时存在 DesktopBehaviors b = DesktopBehaviorFlag::StayOnTop | DesktopBehaviorFlag::StayOnBottom; -``` +```text **解决方案**:在应用时进行冲突检测 @@ -976,7 +981,7 @@ public: return behaviors; } }; -``` +```text ### 陷阱 6:跨平台兼容性 @@ -985,7 +990,7 @@ public: ```cpp // ⚠️ 警告:StayOnTop 在 Wayland 上可能不工作 behaviors |= DesktopBehaviorFlag::StayOnTop; -``` +```text **解决方案**:添加平台检测 @@ -1013,7 +1018,7 @@ public: #endif } }; -``` +```bash 参考:[Wayland and Qt - Qt 6.11 文档](https://doc.qt.io/qt-6/wayland-and-qt.html) @@ -1043,7 +1048,7 @@ Flags f = Flag::A | Flag::B; std::bitset<2> f; f[0] = true; // Flag A f[1] = true; // Flag B -``` +```text **QFlags 优势**: - 类型安全(编译时检查) @@ -1066,7 +1071,7 @@ if (f.testFlag(Flag::A)) { } // uint32_t uint32_t f = 0x03; if (f & 0x01) { } -``` +```yaml **QFlags 优势**: - 类型安全 diff --git a/document/notes/02-Qt-Window-Behavior-Analysis.md b/document/notes/02-Qt-Window-Behavior-Analysis.md index 5a801ffbc..6099dfed8 100644 --- a/document/notes/02-Qt-Window-Behavior-Analysis.md +++ b/document/notes/02-Qt-Window-Behavior-Analysis.md @@ -1,3 +1,8 @@ +--- +title: Qt 窗口行为解析:QWidget 到 DesktopBehaviors +description: 1. 引言:为什么需要从 Qt 状态反推行为 +--- + # Qt 窗口行为解析:QWidget 到 DesktopBehaviors ## 目录 @@ -38,7 +43,7 @@ Qt 的窗口标志(Window Flags)由两部分组成: ```cpp Qt::WindowFlags flags = Qt::Window | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint; -``` +```cpp ### 反推行为的挑战 @@ -76,7 +81,7 @@ Qt::WindowFlags flags = Qt::Window | Qt::FramelessWindowHint | Qt::WindowStaysOn ```cpp // 直接查询 API bool isFullscreen = widget->isFullScreen(); -``` +```text **注意事项**: - `isFullScreen()` 返回窗口当前是否处于全屏状态 @@ -95,7 +100,7 @@ bool isFullscreen = widget->isFullScreen(); // 通过窗口标志查询 Qt::WindowFlags flags = widget->windowFlags(); bool isFrameless = (flags & Qt::FramelessWindowHint) != 0; -``` +```text **相关标志**: - `Qt::FramelessWindowHint`:无边框窗口 @@ -117,7 +122,7 @@ bool isFrameless = (flags & Qt::FramelessWindowHint) != 0; ```cpp bool isStayOnTop = (widget->windowFlags() & Qt::WindowStaysOnTopHint) != 0; -``` +```text **相关标志**: - `Qt::WindowStaysOnTopHint`:保持在其他窗口之上 @@ -133,7 +138,7 @@ bool isStayOnTop = (widget->windowFlags() & Qt::WindowStaysOnTopHint) != 0; ```cpp bool isStayOnBottom = (widget->windowFlags() & Qt::WindowStaysOnBottomHint) != 0; -``` +```text **平台差异**: - **Windows**:完全支持 @@ -149,7 +154,7 @@ QSize minSize = widget->minimumSize(); QSize maxSize = widget->maximumSize(); bool isResizable = (minSize.isEmpty() || minSize.width() == 0) && (maxSize.isEmpty() || maxSize.width() == QWIDGETSIZE_MAX); -``` +```text **更精确的判断**: @@ -171,7 +176,7 @@ bool isResizable(QWidget* widget) { return widthResizable && heightResizable; } -``` +```text **相关 API**: - `setFixedSize()`:设置固定大小 @@ -212,7 +217,7 @@ bool hasAvoidSystemUI(QWidget* widget) { return false; } -``` +```yaml --- @@ -387,7 +392,7 @@ QString behaviorDescription(const DesktopBehaviors& behaviors) { } } // namespace cf::desktop::platform_strategy -``` +```bash --- @@ -432,7 +437,7 @@ QString behaviorDescription(const DesktopBehaviors& behaviors) { // Windows 特有的无边框处理 // 需要处理 WM_NCHITTEST 消息实现窗口拖动 #endif -``` +```bash **参考**:[MSDN Window Styles](https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles) @@ -471,7 +476,7 @@ QString behaviorDescription(const DesktopBehaviors& behaviors) { } } #endif -``` +```text #### Wayland 平台 @@ -507,7 +512,7 @@ QString behaviorDescription(const DesktopBehaviors& behaviors) { } } #endif -``` +```yaml **Wayland 协议扩展**: 某些 compositor 提供扩展协议: @@ -606,7 +611,7 @@ bool inferAvoidSystemUI(QWidget* widget) { return false; } -``` +```text ### AllowResize 推断详解 @@ -645,7 +650,7 @@ bool inferAllowResize(QWidget* widget) { // 默认允许调整 return true; } -``` +```text ### 状态变化监听 @@ -682,7 +687,7 @@ private: QWidget* m_widget; DesktopBehaviors m_behaviors = DesktopBehaviorFlag::None; }; -``` +```yaml --- @@ -717,7 +722,7 @@ std::unique_ptr createPlatformStrategy() { #endif return std::make_unique(); } -``` +```text ### 平台特定实现 @@ -740,7 +745,7 @@ public: return true; } }; -``` +```text #### Wayland 策略 @@ -766,7 +771,7 @@ public: return true; } }; -``` +```yaml --- @@ -890,7 +895,7 @@ private: }; } // namespace cf::desktop::platform_strategy -``` +```text ### 使用示例 @@ -915,7 +920,7 @@ void logWidgetBehaviors(QWidget* widget) { qDebug() << "Unsupported on this platform:" << result.unsupported; } } -``` +```text ### 最佳实践总结 @@ -944,7 +949,7 @@ void debugWindowInfo(QWidget* widget) { qDebug() << "Size policy:" << widget->sizePolicy(); qDebug() << "========================"; } -``` +```yaml --- diff --git a/document/notes/03-Desktop-Strategy-Pattern-Design.md b/document/notes/03-Desktop-Strategy-Pattern-Design.md index 87cab696e..8fae82169 100644 --- a/document/notes/03-Desktop-Strategy-Pattern-Design.md +++ b/document/notes/03-Desktop-Strategy-Pattern-Design.md @@ -1,3 +1,8 @@ +--- +title: 桌面策略系统设计(Strategy Pattern 实战) +description: 1. 为什么使用 Strategy 模式 +--- + # 桌面策略系统设计(Strategy Pattern 实战) ## 目录 @@ -76,7 +81,7 @@ void applyWindowFlags(QWidget* widget) { void applyWindowFlags(QWidget* widget, IDesktopDisplaySizeStrategy* strategy) { strategy->action(widget); // 平台特定实现被封装 } -``` +```text ### 1.2 行为解耦与单一职责 @@ -99,7 +104,7 @@ class CFDesktop : public QWidget { // 1. 实现特定平台的窗口行为 // 2. 提供行为查询接口 // 3. 管理平台相关的状态 -``` +```text 这种分离带来的好处: @@ -131,7 +136,7 @@ PlatformFactoryAPI native() noexcept; // PlatformFactoryAPI* remote() noexcept; } // namespace -``` +```text 这支持以下场景: @@ -178,7 +183,7 @@ TEST(DesktopTest, ApplyStrategy) { 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) @@ -203,7 +208,7 @@ TEST(DesktopTest, ApplyStrategy) { ### 2.2 UML 结构图 -``` +```text ┌─────────────────────────────────────────────────────────────────────┐ │ Context (CFDesktop) │ │ │ @@ -231,7 +236,7 @@ TEST(DesktopTest, ApplyStrategy) { │+ action() │ │+ action() │ │+ action() │ │+ query() │ │+ query() │ │+ query() │ └─────────────────┘ └─────────────────┘ └─────────────────┘ -``` +```text ### 2.3 C++ 实现要点 @@ -245,7 +250,7 @@ public: virtual ~IDesktopDisplaySizeStrategy() = default; // ... }; -``` +```text #### 智能指针支持 @@ -264,7 +269,7 @@ public: std::shared_ptr factorize_shared(const IDesktopPropertyStrategy::StrategyType t); }; -``` +```text #### WeakPtr 集成 @@ -280,7 +285,7 @@ public: private: WeakPtrFactory weak_factory_ptr_; }; -``` +```yaml 参考资料: - [Strategy Design Pattern - GeeksforGeeks](https://www.geeksforgeeks.org/system-design/strategy-pattern-set-1/) @@ -348,7 +353,7 @@ protected: }; } // namespace cf::desktop::platform_strategy -``` +```text ### 3.2 策略类型枚举 @@ -370,7 +375,7 @@ inline const char* strategyTypeToString(IDesktopPropertyStrategy::StrategyType t return "Unknown"; } } -``` +```text ### 3.3 ABI 友好设计 @@ -392,7 +397,7 @@ public: virtual const char* name() const noexcept = 0; virtual StrategyType type() const noexcept { return type_; } }; -``` +```text #### 2. 使用 Pimpl 模式隐藏实现 @@ -410,7 +415,7 @@ private: class IDesktopPropertyStrategy::Impl { // 实现细节可以随意修改而不影响 ABI }; -``` +```text #### 3. 自定义删除器 @@ -428,7 +433,7 @@ public: }}; } }; -``` +```text ### 3.4 显示策略接口 @@ -509,7 +514,7 @@ private: }; } // namespace cf::desktop::platform_strategy -``` +```bash 参考资料: - [Qt 6.10.2 QFlags 官方文档](https://doc.qt.io/qt-6/qflags.html) @@ -555,7 +560,7 @@ Action 方法遵循以下设计原则: * @warning 不应在持有锁的状态下调用此方法(可能触发 Qt 事件) */ virtual bool action(QWidget* widget_data); -``` +```text #### 2. 返回值表示操作结果 @@ -573,7 +578,7 @@ virtual bool action(QWidget* widget_data) { throw std::invalid_argument("widget is null"); // 不推荐 } } -``` +```text #### 3. 幂等性 @@ -594,7 +599,7 @@ public: return true; } }; -``` +```text ### 4.3 Query 方法设计 @@ -617,7 +622,7 @@ Query 方法遵循以下设计原则: * @note 可以多次调用而不影响系统状态 */ virtual DesktopBehaviors query() const; -``` +```text #### 2. 返回完整信息 @@ -631,7 +636,7 @@ if (behaviors.testFlag(DesktopBehaviorFlag::Fullscreen)) { // ❌ 错误:提供多个查询方法 bool isFullscreen() const; // 应该使用 query().testFlag() bool isFrameless() const; // 应该使用 query().testFlag() -``` +```text #### 3. const 正确性 @@ -643,7 +648,7 @@ virtual DesktopBehaviors query() const; void monitorDesktop(const IDesktopDisplaySizeStrategy& strategy) { DesktopBehaviors current = strategy.query(); // OK } -``` +```text ### 4.4 分离的好处 @@ -674,7 +679,7 @@ private: mutable DesktopBehaviors cached_behaviors_; mutable bool cache_valid_ = false; }; -``` +```text #### 2. 并发访问 @@ -690,7 +695,7 @@ DesktopBehaviors state2 = strategy.query(); // 线程 3:修改状态(需要同步) std::lock_guard lock(mtx); strategy.action(widget); -``` +```text #### 3. 前置条件检查 @@ -703,7 +708,7 @@ if (!current.testFlag(DesktopBehaviorFlag::Fullscreen)) { // 只有在非全屏状态下才执行全屏操作 strategy.action(widget); } -``` +```yaml 参考资料: - [CQRS - Martin Fowler](https://martinfowler.com/bliki/CQRS.html) @@ -778,7 +783,7 @@ public: }; } // namespace -``` +```text ### 5.2 FramelessStrategy @@ -840,7 +845,7 @@ public: }; } // namespace -``` +```text ### 5.3 WSL 平台策略 @@ -941,7 +946,7 @@ private: }; } // namespace cf::desktop::platform_strategy::wsl -``` +```text ### 5.4 组合行为策略 @@ -990,7 +995,7 @@ public: DesktopBehaviorFlag::AvoidSystemUI; } }; -``` +```yaml 参考资料: - [QWidget::setWindowFlags - Qt Documentation](https://doc.qt.io/qt-6/qwidget.html#windowFlags-prop) @@ -1039,7 +1044,7 @@ public: */ virtual void clearStrategies() = 0; }; -``` +```text ### 6.2 CompositeStrategy @@ -1140,7 +1145,7 @@ private: }; } // namespace -``` +```text ### 6.3 策略链模式 @@ -1229,7 +1234,7 @@ public: private: std::vector> chain_; }; -``` +```text ### 6.4 条件策略 @@ -1290,7 +1295,7 @@ auto createConditionalStrategy() { std::make_shared() // false 分支 ); } -``` +```yaml 参考资料: - [Composite Pattern - Refactoring.Guru](https://refactoring.guru/design-patterns/composite) @@ -1346,7 +1351,7 @@ inline const std::vector kConflictRules = { }; } // namespace BehaviorConflictRules -``` +```text ### 7.2 冲突检测接口 @@ -1428,7 +1433,7 @@ public: return detect(combined); } }; -``` +```text ### 7.3 冲突解决策略 @@ -1546,7 +1551,7 @@ private: return behaviors; } }; -``` +```text ### 7.4 集成到策略应用 @@ -1596,7 +1601,7 @@ public: return strategy->action(widget); } }; -``` +```yaml 参考资料: - [Conflict-Free Replicated Data Types (CRDTs) - Wikipedia](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) @@ -1673,7 +1678,7 @@ private: }; } // namespace -``` +```text ### 8.2 平台特定工厂 @@ -1722,7 +1727,7 @@ private: }; } // namespace -``` +```text ### 8.3 插件化工厂 @@ -1811,7 +1816,7 @@ public: */ virtual QStringList supportedStrategies() const = 0; }; -``` +```yaml 参考资料: - [How to Create Qt Plugins - Qt Documentation](https://doc.qt.io/qt-6/plugins-howto.html) @@ -1843,7 +1848,7 @@ class WindowStrategy : public IDesktopDisplaySizeStrategy { // ... 更多行为 } }; -``` +```text #### 2. 开闭原则 @@ -1863,7 +1868,7 @@ class FullscreenStrategy : public IDesktopDisplaySizeStrategy { addNewFeature(widget); // 违反开闭原则 } }; -``` +```text #### 3. 依赖倒置 @@ -1881,7 +1886,7 @@ class CFDesktop { private: FullscreenStrategy* display_strategy_; // 具体实现 }; -``` +```text ### 9.2 命名约定 @@ -1904,7 +1909,7 @@ class StrategyFactory; // 组合策略:Composite + 功能 class CompositeStrategy; class StrategyChain; -``` +```text ### 9.3 错误处理 @@ -1962,7 +1967,7 @@ public: private: StrategyResult last_result_; }; -``` +```text ### 9.4 性能考虑 @@ -2000,7 +2005,7 @@ private: mutable DesktopBehaviors cached_behaviors_; mutable bool cache_valid_; }; -``` +```text ### 9.5 线程安全 @@ -2027,7 +2032,7 @@ private: std::shared_ptr impl_; mutable std::mutex mutex_; }; -``` +```text ### 9.6 测试策略 @@ -2071,7 +2076,7 @@ TEST(FullscreenStrategyTest, ApplyFullscreen) { DesktopBehaviorFlag::Fullscreen | DesktopBehaviorFlag::Frameless )); } -``` +```yaml --- diff --git a/document/notes/04-Desktop-Behavior-System-Architecture.md b/document/notes/04-Desktop-Behavior-System-Architecture.md index b23c954ee..c802dbeea 100644 --- a/document/notes/04-Desktop-Behavior-System-Architecture.md +++ b/document/notes/04-Desktop-Behavior-System-Architecture.md @@ -1,3 +1,8 @@ +--- +title: 桌面行为系统设计:从策略到Window Manager抽象 +description: 桌面行为系统设计:从策略到Window Manager抽象 的详细文档 +--- + # 桌面行为系统设计:从策略到Window Manager抽象 ## 目录 @@ -35,7 +40,7 @@ namespace desktop::architecture { // 4. 接口隔离:客户端不应该依赖它不需要的接口 } // namespace desktop::architecture -``` +```yaml --- @@ -43,7 +48,7 @@ namespace desktop::architecture { ### 整体分层视图 -``` +```text ┌─────────────────────────────────────────────────────────────────┐ │ Application Layer │ │ (用户代码 / 业务逻辑层) │ @@ -95,7 +100,7 @@ namespace desktop::architecture { │ Window Manager / OS │ │ (窗口管理器 / 操作系统层) │ └─────────────────────────────────────────────────────────────────┘ -``` +```text ### 各层职责详解 @@ -122,7 +127,7 @@ private: }; } // namespace desktop::app -``` +```text **特点**: - 完全不依赖 Qt 具体类 @@ -181,7 +186,7 @@ public: }; } // namespace desktop::abstraction -``` +```text #### 3. Strategy Layer(策略层) @@ -234,7 +239,7 @@ private: }; } // namespace desktop::strategy -``` +```text #### 4. Qt Integration Layer(Qt 集成层) @@ -379,7 +384,7 @@ private: }; } // namespace desktop::qtintegration -``` +```text #### 5. Platform Abstraction Layer(平台抽象层) @@ -482,7 +487,7 @@ public: }; } // namespace desktop::platform -``` +```text ### 层间通信协议 @@ -542,7 +547,7 @@ private: }; } // namespace desktop::communication -``` +```yaml --- @@ -550,7 +555,7 @@ private: ### 完整流程图 -``` +```text ┌─────────────────────────────────────────────────────────────────┐ │ 行为变更请求 │ │ (User / Plugin / System) │ @@ -611,7 +616,7 @@ private: │ 行为变更完成 │ │ (通知观察者 / 触发事件) │ └─────────────────────────────────────────────────────────────────┘ -``` +```text ### 流程代码实现 @@ -750,7 +755,7 @@ private: }; } // namespace desktop::flow -``` +```text ### 流程监控与调试 @@ -817,7 +822,7 @@ private: }; } // namespace desktop::flow -``` +```yaml --- @@ -1044,7 +1049,7 @@ struct ResolutionResult { }; } // namespace desktop::conflict -``` +```text ### 优先级系统 @@ -1099,7 +1104,7 @@ public: }; } // namespace desktop::priority -``` +```yaml --- @@ -1148,7 +1153,7 @@ public: Q_DECLARE_INTERFACE(desktop::plugin::IDesktopBehaviorPlugin, desktop::plugin::DesktopBehaviorPlugin_iid) -``` +```text ### 插件管理器 @@ -1216,7 +1221,7 @@ PluginManager& PluginManager::instance() { } } // namespace desktop::plugin -``` +```text ### 插件实现示例 @@ -1298,7 +1303,7 @@ public: }; } // namespace desktop::plugin::example -``` +```text ### 插件元数据文件 @@ -1317,7 +1322,7 @@ public: "RequiredFeatures": [] } } -``` +```text ### 动态加载策略 @@ -1365,7 +1370,7 @@ public: }; } // namespace desktop::plugin -``` +```yaml --- @@ -1452,7 +1457,7 @@ enum class WindowLevel { }; } // namespace desktop::future -``` +```text ### 平台特定扩展 @@ -1510,7 +1515,7 @@ struct WaylandSpecificBehaviors { #endif } // namespace desktop::platform -``` +```yaml --- @@ -1613,7 +1618,7 @@ private: }; } // namespace desktop::performance -``` +```text ### 测试策略 @@ -1688,7 +1693,7 @@ private: }; } // namespace desktop::testing -``` +```yaml --- diff --git a/document/notes/05-Logger-Singleton-Link-Architecture.md b/document/notes/05-Logger-Singleton-Link-Architecture.md index a8b357f8c..4bb6c2ee0 100644 --- a/document/notes/05-Logger-Singleton-Link-Architecture.md +++ b/document/notes/05-Logger-Singleton-Link-Architecture.md @@ -1,3 +1,8 @@ +--- +title: Logger 单实例链接架构 +description: 本文档记录 CFDesktop 项目中 (日志系统)的单实例保证方案,包括 CMake 链接策略、 +--- + # Logger 单实例链接架构 本文档记录 CFDesktop 项目中 `cflogger`(日志系统)的单实例保证方案,包括 CMake 链接策略、`INTERFACE` 头文件库的引入、以及 DLL 导出/导入机制的设计决策。 @@ -10,13 +15,13 @@ CFDesktop 采用"多静态库 → 单一共享库"架构: -``` +```text 多个 STATIC library (cflogger, cfbase, CFDesktopMain, CFDesktopUi, ...) ↓ --whole-archive CFDesktop_shared (SHARED / DLL) ↓ CFDesktop.exe -``` +```bash 所有静态库通过 `--whole-archive` 合并进一个 DLL,EXE 只链接这个 DLL。 @@ -64,7 +69,7 @@ target_include_directories(cflogger_headers INTERFACE $ ) target_link_libraries(cflogger_headers INTERFACE cfbase) -``` +```cmake ### 2.3 为什么需要 `cflogger_headers` 而不是直接用 `cflogger` @@ -82,7 +87,7 @@ target_link_libraries(cflogger_headers INTERFACE cfbase) ### 3.1 完整依赖图 -``` +```text cflogger (STATIC) ──────────────────────────────┐ ├─ cflog.cpp, cflog_impl.cpp │ ├─ console_sink.cpp, file_sink.cpp │ @@ -103,7 +108,7 @@ cflogger_headers (INTERFACE) ├───────── CFDesktopMain (STATIC) ──PRIVATE──→ cflogger ───┘ └─ 直接使用了 FileSink, ConsoleSink 等内部类 这些类没有 CFLOG_API 标记,不能通过 DLL 导入 -``` +```bash ### 3.2 各模块的角色 @@ -138,7 +143,7 @@ CFDesktopMain (STATIC) ──PRIVATE──→ cflogger ───┘ #else #define CFLOG_API __attribute__((visibility("default"))) // Linux:可见 #endif -``` +```bash ### 4.2 标记规则 @@ -158,10 +163,10 @@ CFDesktopMain (STATIC) ──PRIVATE──→ cflogger ───┘ 如果 `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` 中解析这些符号。 @@ -173,12 +178,12 @@ ld.lld: error: undefined symbol: vtable for cf::log::ConsoleSink 编译 `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`。 @@ -188,13 +193,13 @@ LLD 警告的含义是:**某 `.obj` 文件通过 `__declspec(dllimport)` 引 具体链路: -``` +```text cflog_export.h 中的宏展开逻辑: CFLOG_BUILDING → __declspec(dllexport) ← 仅 cflogger 自身编译时 CFLOG_STATIC_BUILD → (空) ← 测试/示例用 默认 → __declspec(dllimport) ← 其他所有消费者 -``` +```text DLL 内的静态库(如 `CFDesktopMain`)编译时: - 没有定义 `CFLOG_BUILDING`(那是 cflogger 自己的) @@ -216,7 +221,7 @@ 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。 @@ -247,7 +252,7 @@ target_compile_definitions(cf_desktop_ui_widget_init_session PRIVATE CFLOG_STATI ### 6.1 在 DLL 架构下 -``` +```text cflogger.cpp 编译一次 → libcflogger.a (静态库) ↓ --whole-archive 合并 @@ -257,7 +262,7 @@ cflogger.cpp 编译一次 → libcflogger.a (静态库) CFDesktop.exe 通过 dllimport 访问 Logger::instance() CFDesktopMain.a 通过 PRIVATE 链接直接解析内部类符号 其他 .a 通过 cflogger_headers 只拿到头文件,不链接 .a -``` +```text `--whole-archive` 确保即使没有直接引用的符号也被保留在 DLL 中,避免链接器丢弃"未使用"的 Logger 实现。 @@ -267,7 +272,7 @@ CFDesktopMain.a 通过 PRIVATE 链接直接解析内部类符号 ```cpp printf("Logger: %p\n", &Logger::instance()); -``` +```yaml 如果在 DLL 中调用和 EXE 中调用得到的地址相同,说明单实例保证生效。 @@ -281,7 +286,7 @@ printf("Logger: %p\n", &Logger::instance()); # 可选方案(未采用) add_library(cflog_obj OBJECT cflog.cpp ...) target_sources(CFDesktopMain PRIVATE $) -``` +```text OBJECT library 要求所有使用 cflogger 的模块都从同一个 OBJECT target 获取 `.o` 文件,这在当前多模块架构中引入更多管理复杂度。当前的 `STATIC + --whole-archive` 方案已经足够保证单实例。 @@ -293,7 +298,7 @@ inline Logger& instance() { static Logger x; // 每个 TU 可能独立实例化 return x; } -``` +```bash `inline` 函数中的 `static` 局部变量在 C++17 前的行为是 UB(多个 TU 可能各自实例化),即使在 C++17 后也有跨动态库的复杂性。始终让 `.cpp` 文件只编译一次是最安全的做法。 @@ -341,7 +346,7 @@ inline Logger& instance() { add_library(cfevent STATIC ...) add_library(cfevent_headers INTERFACE ...) target_include_directories(cfevent_headers INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) -``` +```yaml **原则:`.cpp` 只编译一次,`.h` 可以到处传。** diff --git a/document/notes/README.md b/document/notes/README.md index 8d31afe14..0f1ba2f75 100644 --- a/document/notes/README.md +++ b/document/notes/README.md @@ -1,3 +1,8 @@ +--- +title: CFDesktop 桌面行为系统设计文档 +description: 一套完整的跨平台桌面应用窗口行为管理架构设计方案 +--- + # CFDesktop 桌面行为系统设计文档 > 一套完整的跨平台桌面应用窗口行为管理架构设计方案 @@ -31,17 +36,17 @@ ```bash cd /home/charliechen/CFDesktop/document/notes -``` +```text ### 阅读顺序 -``` +```text 新手开发者: 01 → 02 → 03 → 04 有经验开发者: 直接阅读 04,需要时查阅其他文档 -``` +```text ### 代码示例 @@ -65,11 +70,11 @@ DesktopBehaviors behaviors = DesktopBehaviorFlag::Fullscreen if (behaviors.testFlag(DesktopBehaviorFlag::Fullscreen)) { // 处理全屏逻辑 } -``` +```text ## 架构概览 -``` +```text ┌─────────────────────────────────────────────────────────────┐ │ Application Layer │ │ (用户代码 / 业务逻辑) │ @@ -94,7 +99,7 @@ if (behaviors.testFlag(DesktopBehaviorFlag::Fullscreen)) { │ Platform Abstraction Layer │ │ (Windows / macOS / X11 / Wayland / Embedded) │ └─────────────────────────────────────────────────────────────┘ -``` +```bash ## 核心概念 @@ -135,7 +140,7 @@ if (behaviors.testFlag(DesktopBehaviorFlag::Fullscreen)) { ## 项目结构 -``` +```text desktop/ ├── ui/ │ ├── CFDesktop.cpp # 主窗口实现 @@ -155,7 +160,7 @@ document/notes/ # 本文档目录 ├── 02-Qt-Window-Behavior-Analysis.md ├── 03-Desktop-Strategy-Pattern-Design.md └── 04-Desktop-Behavior-System-Architecture.md -``` +```yaml ## 贡献指南 diff --git a/document/notes/index.md b/document/notes/index.md index 3ed4cc868..970577ff3 100644 --- a/document/notes/index.md +++ b/document/notes/index.md @@ -1,3 +1,8 @@ +--- +title: 桌面行为系统设计文档 +description: 本文档系列详细介绍了桌面应用程序中窗口行为建模、Qt 集成、策略模式应用和系统架构设计的完整方案。 +--- + # 桌面行为系统设计文档 本文档系列详细介绍了桌面应用程序中窗口行为建模、Qt 集成、策略模式应用和系统架构设计的完整方案。 @@ -84,11 +89,11 @@ enum class DesktopBehaviorFlag { }; Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) -``` +```text ### 架构分层 -``` +```text Application Layer (用户代码) ↓ Behavior Abstraction (DesktopBehaviors) @@ -98,7 +103,7 @@ Strategy Layer (IDesktopBehaviorStrategy) Qt Integration (WindowFlags) ↓ Platform Abstraction (Windows/X11/Wayland) -``` +```bash --- diff --git a/document/optimize/.pages b/document/optimize/.pages deleted file mode 100644 index bcaa7dad3..000000000 --- a/document/optimize/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: 优化 -icon: material/speedometer -nav: - - 预发布代码格式: pre-release-code-format.md diff --git a/document/optimize/index.md b/document/optimize/index.md index 964663e95..a8fba9441 100644 --- a/document/optimize/index.md +++ b/document/optimize/index.md @@ -1,10 +1,11 @@ -# Optimize - -> Welcome to the Optimize section. +--- +title: 优化指南 +description: 本目录提供 CFDesktop 项目的代码优化指南与最佳实践,重点关注 C++23 零开销抽象、编译 +--- -## Overview +# 优化指南 -Documentation and resources for Optimize. +本目录提供 CFDesktop 项目的代码优化指南与最佳实践,重点关注 C++23 零开销抽象、编译期计算、内存布局优化以及 Qt 与标准库的高效使用方式,帮助开发者在保持代码可读性的同时获得最佳运行时性能。 --- diff --git a/document/optimize/pre-release-code-format.md b/document/optimize/pre-release-code-format.md index b8e80eb90..05ca22a07 100644 --- a/document/optimize/pre-release-code-format.md +++ b/document/optimize/pre-release-code-format.md @@ -1,225 +1,230 @@ -# CFDesktop 性能优化和架构优化计划 - -## Context - -CFDesktop 是一个基于 Qt6 和 C++23 的桌面应用程序框架,实现了完整的 Material Design 3 UI 组件库。项目采用模块化架构设计,代码质量较高,但仍存在一些性能和架构层面的优化空间。 - -本计划旨在在 pre-release 阶段对项目进行系统的性能和架构优化,提升代码质量、运行效率和可维护性。 - ---- - -## 当前架构概览 - -``` -CFDesktop/ -├── base/ # 基础库模块 -│ ├── include/ # 头文件(expected、WeakPtr、哈希等) -│ └── system/ # 系统信息抽象层(CPU/Memory) -├── ui/ # UI框架模块 -│ ├── base/ # 基础工具(颜色、几何、缓动) -│ ├── core/ # 核心引擎(主题、动效、令牌) -│ ├── components/ # 通用组件(动画、状态机) -│ └── widget/ # Material控件(按钮、文本框等) -├── example/ # 示例程序 -├── test/ # GoogleTest单元测试 -└── scripts/ # 构建和工具脚本 -``` - -**技术栈:** Qt6 + C++23 + CMake + GoogleTest - ---- - -## 优化计划 - -### 一、性能优化 - -#### 1.1 系统信息查询优化 - -**问题文件:** -- `base/system/memory/private/linux_impl/cached_memory.cpp` -- `base/system/cpu/private/linux_impl/cpu_profile.cpp` -- `base/system/memory/private/win_impl/process_memory.cpp` - -**问题分析:** -- Linux平台频繁读取 `/proc/meminfo`、`/proc/stat` 等系统文件 -- CPU使用率计算需要阻塞100ms进行两次采样 -- 缓存机制存在但缺乏过期策略 - -**优化方案:** -1. 实现智能缓存策略(TTL过期 + LRU淘汰) -2. 将CPU使用率计算移到后台线程 -3. 使用 `QTimer` 替代 `std::this_thread::sleep_for` -4. 添加查询频率限制机制 - -**预期收益:** 减少系统调用次数,降低CPU占用 - ---- - -#### 1.2 动画工厂缓存管理优化 - -**问题文件:** -- `ui/components/material/cfmaterial_animation_factory.cpp` -- `ui/widget/material/base/state_machine.cpp` -- `ui/widget/material/base/elevation_controller.cpp` - -**问题分析:** -- `createAnimation()` 使用 `targetWidget` 指针地址作为缓存 key -- widget 销毁后缓存条目不会被清理,导致内存泄漏 -- 传 `nullptr` 时 key = "token_0",所有调用共享同一实例(如 state_machine 和 elevation_controller) - -**代码位置:** `cfmaterial_animation_factory.cpp:109-152` - -**优化方案(已实现):** -1. 添加 `owner` 参数,优先使用 owner 作为缓存 key -2. 监听 `QObject::destroyed` 信号,widget/owner 销毁时自动清理缓存 -3. 调用处传入 `this` 作为 owner,每个组件有自己的动画实例 - -**预期收益:** 防止内存泄漏,按调用者隔离动画实例 - -**实现文件:** -- `ui/components/material/cfmaterial_animation_factory.h` - 添加 owner 参数 -- `ui/components/material/cfmaterial_animation_factory.cpp` - 实现自动清理 -- `ui/widget/material/base/state_machine.cpp` - 传入 this 作为 owner -- `ui/widget/material/base/elevation_controller.cpp` - 传入 this 作为 owner - ---- - -#### 1.3 内存管理优化 - -**问题文件:** -- `ui/widget/material/base/state_machine.cpp` -- `base/include/base/weak_ptr/weak_ptr.h` - -**问题分析:** -- 动画信号槽连接可能在对象销毁时未正确断开 -- 定时器可能未在对象销毁时清除 -- WeakPtr 生命周期管理需要加强 - -**优化方案:** -1. 在析构函数中确保所有动画正确清理 -2. 使用 RAII 模式管理定时器和信号连接 -3. 添加内存使用统计和分析工具 -4. 考虑实现对象池减少频繁创建销毁 - -**预期收益:** 减少内存泄漏风险,稳定内存占用 - ---- - -### 二、架构优化 - -#### 2.1 模块依赖优化 - -**当前状态:** 依赖关系清晰 (`example → ui → base`) - -**优化方案:** -1. 进一步减少头文件依赖,使用前向声明 -2. 将 Pimpl 惯用法应用到更多组件 -3. 评估 `ui/widget` 和 `ui/components` 的边界是否清晰 - -**预期收益:** 减少编译时间,提高模块独立性 - ---- - -#### 2.2 错误处理标准化 - -**当前状态:** 使用 `cf::expected` 进行错误处理 - -**优化方案:** -1. 建立统一的错误码体系 -2. 为 `expected` 添加更多实用方法 -3. 考虑添加错误上下文追踪功能 - -**预期收益:** 提高错误处理一致性和调试效率 - ---- - -#### 2.3 测试覆盖率提升 - -**当前状态:** 有 GoogleTest 框架,但测试覆盖不足 - -**优化方案:** -1. 添加代码覆盖率统计工具(如 gcov/lcov) -2. 为核心组件补充边界条件测试 -3. 添加性能基准测试 -4. 集成静态分析工具(clang-tidy) -5. 补充 widgets 和 components 的交互测试 -6. 添加跨平台兼容性测试用例 - -**预期收益:** 提高代码质量和可靠性,确保跨平台兼容 - ---- - -### 三、构建和发布优化 - -#### 3.1 构建配置优化 - -**问题分析:** Debug 构建使用 `-O0`,开发体验较差 - -**优化方案:** -1. 添加 `DebugOptimized` 构建类型(`-Og` 优化级别) -2. 优化链接器参数 - ---- - -#### 3.2 CI/CD 增强 - -**优化方案:** -1. 添加性能回归检测 -2. 集成静态分析和代码质量检查 -3. 自动化发布流程 - ---- - -## 优先级排序 - -| 优先级 | 优化项 | 预期工作量 | 影响范围 | 状态 | -|--------|--------|-----------|----------|------| -| P0 | 系统信息查询缓存优化 | 中 | 全局性能 | 待完成 | -| P0 | 动画工厂缓存泄漏修复 | 中 | 内存稳定性 | ✅ 已完成 | -| P0 | 测试覆盖率提升 | 高 | 可靠性 | 待完成 | -| P2 | 错误处理标准化 | 低 | 代码质量 | 待完成 | -| P3 | 构建配置优化 | 低 | 开发体验 | 待完成 | - ---- - -## 关键文件清单 - -### 需要修改的文件 - -**性能优化:** -- `base/system/memory/private/linux_impl/cached_memory.cpp` - 缓存策略 -- `base/system/cpu/private/linux_impl/cpu_profile.cpp` - CPU查询异步化 -- `ui/components/material/cfmaterial_animation_factory.cpp` - 动画缓存清理机制 - -**架构优化:** -- `base/include/base/expected/expected.hpp` - 错误处理增强 -- `ui/core/theme_manager.cpp` - 单例模式优化 -- 各组件头文件 - 前向声明优化 - -**Scripts 重构:** -- `scripts/` 下所有脚本文件 - 提取公共函数库 - -### 需要新增的文件 - -- `base/include/base/cache/lru_cache.h` - LRU缓存实现 -- `ui/base/paint_optimizer.h` - 绘制优化器 -- `test/performance/` - 性能基准测试目录 - ---- - -## 验证计划 - -1. **编译验证:** 确保所有构建类型编译通过 -2. **单元测试:** 运行完整测试套件,确保无回归 -3. **性能测试:** 运行基准测试,对比优化前后数据 -4. **内存检测:** 使用 Valgrind/ASan 检测内存问题 -5. **UI测试:** 手动验证动画流畅度和响应速度 - ---- - -## 实施建议 - -1. 分阶段实施,优先完成 P0 级别优化 -2. 每项优化后进行测试验证 -3. 保持向后兼容性,避免破坏现有API -4. 及时更新相关文档和示例 +--- +title: CFDesktop 性能优化和架构优化计划 +description: CFDesktop 是一个基于 Qt6 和 C++23 的桌面应用程序框架,实现了完整的 Mater +--- + +# CFDesktop 性能优化和架构优化计划 + +## Context + +CFDesktop 是一个基于 Qt6 和 C++23 的桌面应用程序框架,实现了完整的 Material Design 3 UI 组件库。项目采用模块化架构设计,代码质量较高,但仍存在一些性能和架构层面的优化空间。 + +本计划旨在在 pre-release 阶段对项目进行系统的性能和架构优化,提升代码质量、运行效率和可维护性。 + +--- + +## 当前架构概览 + +```text +CFDesktop/ +├── base/ # 基础库模块 +│ ├── include/ # 头文件(expected、WeakPtr、哈希等) +│ └── system/ # 系统信息抽象层(CPU/Memory) +├── ui/ # UI框架模块 +│ ├── base/ # 基础工具(颜色、几何、缓动) +│ ├── core/ # 核心引擎(主题、动效、令牌) +│ ├── components/ # 通用组件(动画、状态机) +│ └── widget/ # Material控件(按钮、文本框等) +├── example/ # 示例程序 +├── test/ # GoogleTest单元测试 +└── scripts/ # 构建和工具脚本 +```bash + +**技术栈:** Qt6 + C++23 + CMake + GoogleTest + +--- + +## 优化计划 + +### 一、性能优化 + +#### 1.1 系统信息查询优化 + +**问题文件:** +- `base/system/memory/private/linux_impl/cached_memory.cpp` +- `base/system/cpu/private/linux_impl/cpu_profile.cpp` +- `base/system/memory/private/win_impl/process_memory.cpp` + +**问题分析:** +- Linux平台频繁读取 `/proc/meminfo`、`/proc/stat` 等系统文件 +- CPU使用率计算需要阻塞100ms进行两次采样 +- 缓存机制存在但缺乏过期策略 + +**优化方案:** +1. 实现智能缓存策略(TTL过期 + LRU淘汰) +2. 将CPU使用率计算移到后台线程 +3. 使用 `QTimer` 替代 `std::this_thread::sleep_for` +4. 添加查询频率限制机制 + +**预期收益:** 减少系统调用次数,降低CPU占用 + +--- + +#### 1.2 动画工厂缓存管理优化 + +**问题文件:** +- `ui/components/material/cfmaterial_animation_factory.cpp` +- `ui/widget/material/base/state_machine.cpp` +- `ui/widget/material/base/elevation_controller.cpp` + +**问题分析:** +- `createAnimation()` 使用 `targetWidget` 指针地址作为缓存 key +- widget 销毁后缓存条目不会被清理,导致内存泄漏 +- 传 `nullptr` 时 key = "token_0",所有调用共享同一实例(如 state_machine 和 elevation_controller) + +**代码位置:** `cfmaterial_animation_factory.cpp:109-152` + +**优化方案(已实现):** +1. 添加 `owner` 参数,优先使用 owner 作为缓存 key +2. 监听 `QObject::destroyed` 信号,widget/owner 销毁时自动清理缓存 +3. 调用处传入 `this` 作为 owner,每个组件有自己的动画实例 + +**预期收益:** 防止内存泄漏,按调用者隔离动画实例 + +**实现文件:** +- `ui/components/material/cfmaterial_animation_factory.h` - 添加 owner 参数 +- `ui/components/material/cfmaterial_animation_factory.cpp` - 实现自动清理 +- `ui/widget/material/base/state_machine.cpp` - 传入 this 作为 owner +- `ui/widget/material/base/elevation_controller.cpp` - 传入 this 作为 owner + +--- + +#### 1.3 内存管理优化 + +**问题文件:** +- `ui/widget/material/base/state_machine.cpp` +- `base/include/base/weak_ptr/weak_ptr.h` + +**问题分析:** +- 动画信号槽连接可能在对象销毁时未正确断开 +- 定时器可能未在对象销毁时清除 +- WeakPtr 生命周期管理需要加强 + +**优化方案:** +1. 在析构函数中确保所有动画正确清理 +2. 使用 RAII 模式管理定时器和信号连接 +3. 添加内存使用统计和分析工具 +4. 考虑实现对象池减少频繁创建销毁 + +**预期收益:** 减少内存泄漏风险,稳定内存占用 + +--- + +### 二、架构优化 + +#### 2.1 模块依赖优化 + +**当前状态:** 依赖关系清晰 (`example → ui → base`) + +**优化方案:** +1. 进一步减少头文件依赖,使用前向声明 +2. 将 Pimpl 惯用法应用到更多组件 +3. 评估 `ui/widget` 和 `ui/components` 的边界是否清晰 + +**预期收益:** 减少编译时间,提高模块独立性 + +--- + +#### 2.2 错误处理标准化 + +**当前状态:** 使用 `cf::expected` 进行错误处理 + +**优化方案:** +1. 建立统一的错误码体系 +2. 为 `expected` 添加更多实用方法 +3. 考虑添加错误上下文追踪功能 + +**预期收益:** 提高错误处理一致性和调试效率 + +--- + +#### 2.3 测试覆盖率提升 + +**当前状态:** 有 GoogleTest 框架,但测试覆盖不足 + +**优化方案:** +1. 添加代码覆盖率统计工具(如 gcov/lcov) +2. 为核心组件补充边界条件测试 +3. 添加性能基准测试 +4. 集成静态分析工具(clang-tidy) +5. 补充 widgets 和 components 的交互测试 +6. 添加跨平台兼容性测试用例 + +**预期收益:** 提高代码质量和可靠性,确保跨平台兼容 + +--- + +### 三、构建和发布优化 + +#### 3.1 构建配置优化 + +**问题分析:** Debug 构建使用 `-O0`,开发体验较差 + +**优化方案:** +1. 添加 `DebugOptimized` 构建类型(`-Og` 优化级别) +2. 优化链接器参数 + +--- + +#### 3.2 CI/CD 增强 + +**优化方案:** +1. 添加性能回归检测 +2. 集成静态分析和代码质量检查 +3. 自动化发布流程 + +--- + +## 优先级排序 + +| 优先级 | 优化项 | 预期工作量 | 影响范围 | 状态 | +|--------|--------|-----------|----------|------| +| P0 | 系统信息查询缓存优化 | 中 | 全局性能 | 待完成 | +| P0 | 动画工厂缓存泄漏修复 | 中 | 内存稳定性 | ✅ 已完成 | +| P0 | 测试覆盖率提升 | 高 | 可靠性 | 待完成 | +| P2 | 错误处理标准化 | 低 | 代码质量 | 待完成 | +| P3 | 构建配置优化 | 低 | 开发体验 | 待完成 | + +--- + +## 关键文件清单 + +### 需要修改的文件 + +**性能优化:** +- `base/system/memory/private/linux_impl/cached_memory.cpp` - 缓存策略 +- `base/system/cpu/private/linux_impl/cpu_profile.cpp` - CPU查询异步化 +- `ui/components/material/cfmaterial_animation_factory.cpp` - 动画缓存清理机制 + +**架构优化:** +- `base/include/base/expected/expected.hpp` - 错误处理增强 +- `ui/core/theme_manager.cpp` - 单例模式优化 +- 各组件头文件 - 前向声明优化 + +**Scripts 重构:** +- `scripts/` 下所有脚本文件 - 提取公共函数库 + +### 需要新增的文件 + +- `base/include/base/cache/lru_cache.h` - LRU缓存实现 +- `ui/base/paint_optimizer.h` - 绘制优化器 +- `test/performance/` - 性能基准测试目录 + +--- + +## 验证计划 + +1. **编译验证:** 确保所有构建类型编译通过 +2. **单元测试:** 运行完整测试套件,确保无回归 +3. **性能测试:** 运行基准测试,对比优化前后数据 +4. **内存检测:** 使用 Valgrind/ASan 检测内存问题 +5. **UI测试:** 手动验证动画流畅度和响应速度 + +--- + +## 实施建议 + +1. 分阶段实施,优先完成 P0 级别优化 +2. 每项优化后进行测试验证 +3. 保持向后兼容性,避免破坏现有API +4. 及时更新相关文档和示例 diff --git a/document/release_rule/.pages b/document/release_rule/.pages deleted file mode 100644 index ef5c8f25d..000000000 --- a/document/release_rule/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: 发布规范 -icon: material/tag -nav: - - Git Hooks 指南: git_hooks_guide.md diff --git a/document/release_rule/git_hooks_guide.md b/document/release_rule/git_hooks_guide.md index e86269e4b..75909dee8 100644 --- a/document/release_rule/git_hooks_guide.md +++ b/document/release_rule/git_hooks_guide.md @@ -1,339 +1,344 @@ -# Git Hooks 使用指南 - -本项目配置了 Git hooks,在本地进行代码质量检查和构建验证,确保远程 main 分支始终保持可构建状态。 - -## 目录 - -- [快速开始](#快速开始) -- [安装钩子](#安装钩子) -- [钩子说明](#钩子说明) -- [分支策略](#分支策略) -- [验证级别](#验证级别) -- [常见问题](#常见问题) -- [卸载钩子](#卸载钩子) - ---- - -## 快速开始 - -### 安装 - -**Linux/macOS:** -```bash -bash scripts/release/hooks/install_hooks.sh -``` - -**Windows (PowerShell):** -```powershell -.\scripts\release\hooks\install_hooks.ps1 -``` - -### 验证安装 - -```bash -# Linux/macOS -ls -la .git/hooks/pre-commit .git/hooks/pre-push - -# Windows -dir .git\hooks\pre-commit.* -``` - ---- - -## 安装钩子 - -### 前置要求 - -| 组件 | 要求 | 说明 | -|------|------|------| -| Git | >= 2.0 | 用于执行钩子脚本 | -| Docker | 最新版 | 用于 pre-push 构建验证 | -| clang-format | 可选 | 用于 pre-commit 格式检查 | - -### 安装步骤 - -1. **运行安装脚本** - - 安装脚本会自动: - - 检查是否在 Git 仓库中 - - 备份现有的自定义钩子(如果有) - - 复制钩子到 `.git/hooks/` 目录 - - 设置执行权限 - -2. **确认安装成功** - - ```bash - cat .git/hooks/pre-commit | head -n 5 - ``` - - 应该看到钩子脚本的开头部分。 - ---- - -## 钩子说明 - -### pre-commit - -**触发时机**: 每次 `git commit` 时 - -**适用分支**: 所有分支 - -**检查内容**: -1. 空白字符检查(尾随空格、混合缩进等) -2. C++ 代码格式检查(需要 clang-format) - -**失败行为**: 阻止提交 - -**绕过方法**: -```bash -git commit --no-verify -m "message" -``` - ---- - -### pre-push - -**触发时机**: 每次 `git push` 时 - -**适用分支**: 仅 `main` 和 `release/*` 分支 - -**检查内容**: Docker 容器内完整构建 + 测试 - -**失败行为**: 阻止推送,远程仓库保持干净 - -**绕过方法**: -```bash -git push --no-verify -``` - ---- - -## 分支策略 - -| 分支类型 | 命名规则 | pre-commit | pre-push | 推送到远程 | -|----------|----------|-----------|----------|-----------| -| **main** | - | ✅ | ✅ (FastBuild) | ✅ | -| **release** | `release/x.y` | ✅ | ✅ (自动检测) | ✅ | -| **feat** | 任意 | ✅ | ❌ | ❌ | - -**工作流程**: - -``` -1. 创建 feat 分支开发 - $ git checkout -b feat/my-feature - # ... 编码 ... - $ git commit -m "feat: xxx" # 触发 pre-commit - -2. 合并到 main - $ git checkout main - $ git merge feat/my-feature - $ git push # 触发 pre-push 验证 - -3. 验证失败时回退 - $ git reset --hard origin/main -``` - ---- - -## 验证级别 - -### main 分支 - -默认验证级别:**X64 FastBuild + Tests** - -```bash -# 等价于执行 -docker_start.sh --verify --fast-build --arch amd64 -``` - -### release 分支 - -根据版本号变化自动检测验证级别: - -| 版本变化 | 验证级别 | 执行命令 | -|----------|----------|----------| -| **Major** (x.y.z → x+1.0.0) | X64 + ARM64 完整构建 | `docker_start.sh --verify --arch amd64` + `--arch arm64` | -| **Minor** (x.y.z → x.y+1.0) | X64 完整构建 | `docker_start.sh --verify --arch amd64` | -| **Patch** (x.y.z → x.y.z+1) | X64 FastBuild + Tests | `docker_start.sh --verify --fast-build --arch amd64` | - -**示例**: - -``` -远程 main: 1.2.3 - -推送 release/1.2.4 (标签 1.2.4) - → Patch 变化 → X64 FastBuild + Tests - -推送 release/1.3.0 (标签 1.3.0) - → Minor 变化 → X64 完整构建 - -推送 release/2.0.0 (标签 2.0.0) - → Major 变化 → X64 + ARM64 完整构建 -``` - ---- - -## 常见问题 - -### Q: pre-push 验证太慢,可以跳过吗? - -**A**: 可以使用 `--no-verify` 跳过,但不推荐: - -```bash -git push --no-verify -``` - -更好的做法是确保本地代码通过测试后再推送。 - ---- - -### Q: Docker daemon 未运行怎么办? - -**A**: 启动 Docker Desktop 或 Docker 服务: - -```bash -# Windows/macOS -# 启动 Docker Desktop 应用程序 - -# Linux -sudo systemctl start docker -sudo systemctl enable docker -``` - ---- - -### Q: clang-format 未安装会怎样? - -**A**: pre-commit 会跳过格式检查并显示警告: - -``` -⚠ clang-format 未安装,跳过格式检查 -``` - -不影响提交,但建议安装: - -```bash -# Ubuntu/Debian -sudo apt install clang-format - -# macOS -brew install clang-format - -# Windows -# 通过 LLVM 或包管理器安装 -``` - ---- - -### Q: 如何验证钩子是否正常工作? - -**A**: 测试 pre-commit: - -```bash -# 制造格式错误 -echo "int x;" >> src/some_file.cpp -git add src/some_file.cpp -git commit -m "test" -# 应该被阻止 -``` - -测试 pre-push: - -```bash -git checkout main -# 合并一些更改 -git push -# 应该触发 Docker 验证 -``` - ---- - -### Q: 验证失败后如何回退? - -**A**: 使用以下命令回退本地 main 分支: - -```bash -git fetch origin -git reset --hard origin/main -``` - -**注意**: 这会丢弃本地所有未推送的更改,请确保已备份重要内容。 - ---- - -### Q: release 分支的版本号从哪里获取? - -**A**: 钩子通过以下方式获取版本号: - -1. **本地版本**: 使用 `git describe --tags --abbrev=0` 获取最近的标签 -2. **远程版本**: 使用 `git fetch` 获取远程 main 分支的最近标签 - -**确保在推送 release 分支前已创建版本标签**: - -```bash -git tag 1.2.4 -git push origin release/1.2 -``` - ---- - -### Q: ARM64 构建需要很长时间? - -**A**: 是的,在 x86_64 主机上,ARM64 构建使用 QEMU 仿真,速度较慢。 - -**优化建议**: -- 仅在 Major 版本发布时运行 ARM64 验证 -- 考虑使用 ARM64 runner(GitHub Actions 自托管) - ---- - -## 卸载钩子 - -### 完全卸载 - -```bash -# Linux/macOS -rm .git/hooks/pre-commit .git/hooks/pre-push - -# Windows -Remove-Item .git\hooks\pre-commit, .git\hooks\pre-push -``` - -### 重新安装 - -直接运行安装脚本即可,会自动覆盖现有钩子: - -```bash -# Linux/macOS -bash scripts/release/hooks/install_hooks.sh - -# Windows -.\scripts\release\hooks\install_hooks.ps1 -``` - ---- - -## 文件结构 - -``` -scripts/release/hooks/ -├── install_hooks.sh # Linux/macOS 安装脚本 -├── install_hooks.ps1 # Windows 安装脚本 -├── version_utils.sh # 版本检测辅助函数 -├── pre-commit.sample # pre-commit 钩子模板 -└── pre-push.sample # pre-push 钩子模板 - -document/release_rule/ -└── git_hooks_guide.md # 本文档 -``` - -安装后: -``` -.git/hooks/ -├── pre-commit # 从 pre-commit.sample 复制 -└── pre-push # 从 pre-push.sample 复制 -``` - ---- - -*文档版本: v1.0 | 更新日期: 2026-03-07* +--- +title: Git Hooks 使用指南 +description: 本项目配置了 Git hooks,在本地进行代码质量检查和构建验证,确保远程 main 分支始终保持 +--- + +# Git Hooks 使用指南 + +本项目配置了 Git hooks,在本地进行代码质量检查和构建验证,确保远程 main 分支始终保持可构建状态。 + +## 目录 + +- [快速开始](#快速开始) +- [安装钩子](#安装钩子) +- [钩子说明](#钩子说明) +- [分支策略](#分支策略) +- [验证级别](#验证级别) +- [常见问题](#常见问题) +- [卸载钩子](#卸载钩子) + +--- + +## 快速开始 + +### 安装 + +**Linux/macOS:** +```bash +bash scripts/release/hooks/install_hooks.sh +```text + +**Windows (PowerShell):** +```powershell +.\scripts\release\hooks\install_hooks.ps1 +```text + +### 验证安装 + +```bash +# Linux/macOS +ls -la .git/hooks/pre-commit .git/hooks/pre-push + +# Windows +dir .git\hooks\pre-commit.* +```bash + +--- + +## 安装钩子 + +### 前置要求 + +| 组件 | 要求 | 说明 | +|------|------|------| +| Git | >= 2.0 | 用于执行钩子脚本 | +| Docker | 最新版 | 用于 pre-push 构建验证 | +| clang-format | 可选 | 用于 pre-commit 格式检查 | + +### 安装步骤 + +1. **运行安装脚本** + + 安装脚本会自动: + - 检查是否在 Git 仓库中 + - 备份现有的自定义钩子(如果有) + - 复制钩子到 `.git/hooks/` 目录 + - 设置执行权限 + +2. **确认安装成功** + + ```bash + cat .git/hooks/pre-commit | head -n 5 + ``` + + 应该看到钩子脚本的开头部分。 + +--- + +## 钩子说明 + +### pre-commit + +**触发时机**: 每次 `git commit` 时 + +**适用分支**: 所有分支 + +**检查内容**: +1. 空白字符检查(尾随空格、混合缩进等) +2. C++ 代码格式检查(需要 clang-format) + +**失败行为**: 阻止提交 + +**绕过方法**: +```bash +git commit --no-verify -m "message" +```bash + +--- + +### pre-push + +**触发时机**: 每次 `git push` 时 + +**适用分支**: 仅 `main` 和 `release/*` 分支 + +**检查内容**: Docker 容器内完整构建 + 测试 + +**失败行为**: 阻止推送,远程仓库保持干净 + +**绕过方法**: +```bash +git push --no-verify +```bash + +--- + +## 分支策略 + +| 分支类型 | 命名规则 | pre-commit | pre-push | 推送到远程 | +|----------|----------|-----------|----------|-----------| +| **main** | - | ✅ | ✅ (FastBuild) | ✅ | +| **release** | `release/x.y` | ✅ | ✅ (自动检测) | ✅ | +| **feat** | 任意 | ✅ | ❌ | ❌ | + +**工作流程**: + +```bash +1. 创建 feat 分支开发 + $ git checkout -b feat/my-feature + # ... 编码 ... + $ git commit -m "feat: xxx" # 触发 pre-commit + +2. 合并到 main + $ git checkout main + $ git merge feat/my-feature + $ git push # 触发 pre-push 验证 + +3. 验证失败时回退 + $ git reset --hard origin/main +```yaml + +--- + +## 验证级别 + +### main 分支 + +默认验证级别:**X64 FastBuild + Tests** + +```bash +# 等价于执行 +docker_start.sh --verify --fast-build --arch amd64 +```bash + +### release 分支 + +根据版本号变化自动检测验证级别: + +| 版本变化 | 验证级别 | 执行命令 | +|----------|----------|----------| +| **Major** (x.y.z → x+1.0.0) | X64 + ARM64 完整构建 | `docker_start.sh --verify --arch amd64` + `--arch arm64` | +| **Minor** (x.y.z → x.y+1.0) | X64 完整构建 | `docker_start.sh --verify --arch amd64` | +| **Patch** (x.y.z → x.y.z+1) | X64 FastBuild + Tests | `docker_start.sh --verify --fast-build --arch amd64` | + +**示例**: + +```text +远程 main: 1.2.3 + +推送 release/1.2.4 (标签 1.2.4) + → Patch 变化 → X64 FastBuild + Tests + +推送 release/1.3.0 (标签 1.3.0) + → Minor 变化 → X64 完整构建 + +推送 release/2.0.0 (标签 2.0.0) + → Major 变化 → X64 + ARM64 完整构建 +```yaml + +--- + +## 常见问题 + +### Q: pre-push 验证太慢,可以跳过吗? + +**A**: 可以使用 `--no-verify` 跳过,但不推荐: + +```bash +git push --no-verify +```yaml + +更好的做法是确保本地代码通过测试后再推送。 + +--- + +### Q: Docker daemon 未运行怎么办? + +**A**: 启动 Docker Desktop 或 Docker 服务: + +```bash +# Windows/macOS +# 启动 Docker Desktop 应用程序 + +# Linux +sudo systemctl start docker +sudo systemctl enable docker +```yaml + +--- + +### Q: clang-format 未安装会怎样? + +**A**: pre-commit 会跳过格式检查并显示警告: + +```text +⚠ clang-format 未安装,跳过格式检查 +```text + +不影响提交,但建议安装: + +```bash +# Ubuntu/Debian +sudo apt install clang-format + +# macOS +brew install clang-format + +# Windows +# 通过 LLVM 或包管理器安装 +```yaml + +--- + +### Q: 如何验证钩子是否正常工作? + +**A**: 测试 pre-commit: + +```bash +# 制造格式错误 +echo "int x;" >> src/some_file.cpp +git add src/some_file.cpp +git commit -m "test" +# 应该被阻止 +```text + +测试 pre-push: + +```bash +git checkout main +# 合并一些更改 +git push +# 应该触发 Docker 验证 +```yaml + +--- + +### Q: 验证失败后如何回退? + +**A**: 使用以下命令回退本地 main 分支: + +```bash +git fetch origin +git reset --hard origin/main +```yaml + +**注意**: 这会丢弃本地所有未推送的更改,请确保已备份重要内容。 + +--- + +### Q: release 分支的版本号从哪里获取? + +**A**: 钩子通过以下方式获取版本号: + +1. **本地版本**: 使用 `git describe --tags --abbrev=0` 获取最近的标签 +2. **远程版本**: 使用 `git fetch` 获取远程 main 分支的最近标签 + +**确保在推送 release 分支前已创建版本标签**: + +```bash +git tag 1.2.4 +git push origin release/1.2 +```yaml + +--- + +### Q: ARM64 构建需要很长时间? + +**A**: 是的,在 x86_64 主机上,ARM64 构建使用 QEMU 仿真,速度较慢。 + +**优化建议**: +- 仅在 Major 版本发布时运行 ARM64 验证 +- 考虑使用 ARM64 runner(GitHub Actions 自托管) + +--- + +## 卸载钩子 + +### 完全卸载 + +```bash +# Linux/macOS +rm .git/hooks/pre-commit .git/hooks/pre-push + +# Windows +Remove-Item .git\hooks\pre-commit, .git\hooks\pre-push +```text + +### 重新安装 + +直接运行安装脚本即可,会自动覆盖现有钩子: + +```bash +# Linux/macOS +bash scripts/release/hooks/install_hooks.sh + +# Windows +.\scripts\release\hooks\install_hooks.ps1 +```yaml + +--- + +## 文件结构 + +```text +scripts/release/hooks/ +├── install_hooks.sh # Linux/macOS 安装脚本 +├── install_hooks.ps1 # Windows 安装脚本 +├── version_utils.sh # 版本检测辅助函数 +├── pre-commit.sample # pre-commit 钩子模板 +└── pre-push.sample # pre-push 钩子模板 + +document/release_rule/ +└── git_hooks_guide.md # 本文档 +```text + +安装后: +```text +.git/hooks/ +├── pre-commit # 从 pre-commit.sample 复制 +└── pre-push # 从 pre-push.sample 复制 +```yaml + +--- + +*文档版本: v1.0 | 更新日期: 2026-03-07* diff --git a/document/release_rule/index.md b/document/release_rule/index.md index 6937b5882..b8fbffb8a 100644 --- a/document/release_rule/index.md +++ b/document/release_rule/index.md @@ -1,10 +1,11 @@ -# Release Rule - -> Welcome to the Release Rule section. +--- +title: 发布规范 +description: 本目录定义了 CFDesktop 项目的发布流程与版本管理规则,包括语义化版本号(SemVer)策略 +--- -## Overview +# 发布规范 -Documentation and resources for Release Rule. +本目录定义了 CFDesktop 项目的发布流程与版本管理规则,包括语义化版本号(SemVer)策略、分支管理规范、变更日志(Changelog)格式要求以及正式发布的检查清单。 --- diff --git a/document/scripts/.pages b/document/scripts/.pages deleted file mode 100644 index 710fdeb48..000000000 --- a/document/scripts/.pages +++ /dev/null @@ -1,11 +0,0 @@ -title: 脚本文档 -icon: material/console -nav: - - 构建辅助: build_helpers - - 依赖安装: dependency - - 开发脚本: develop - - Docker: docker - - 文档工具: document - - Doxygen: doxygen - - 脚本库: lib - - 发布: release diff --git a/document/scripts/README.md b/document/scripts/README.md index c2ce6b323..14e5e25ea 100644 --- a/document/scripts/README.md +++ b/document/scripts/README.md @@ -1,3 +1,8 @@ +--- +title: Scripts文档 +description: "文档编写日期: 2026-03-20,本目录包含CFDesktop项目所有脚本的完整文档。" +--- + # Scripts文档 > 文档编写日期: 2026-03-20 @@ -6,7 +11,7 @@ ## 目录结构 -``` +```text scripts/ ├── build_helpers/ # 构建辅助脚本 (Linux/Windows) ├── dependency/ # 依赖安装 @@ -19,7 +24,7 @@ scripts/ ├── release/ # 发布相关 │ └── hooks/ # Git钩子 └── run_helpers/ # 运行辅助 -``` +```bash ## 快速导航 diff --git a/document/scripts/build_helpers/.pages b/document/scripts/build_helpers/.pages deleted file mode 100644 index 25d6d880d..000000000 --- a/document/scripts/build_helpers/.pages +++ /dev/null @@ -1,18 +0,0 @@ -title: 构建辅助 -nav: - - CI 构建入口: ci_build_entry.sh.md - - 配置文件: config_files.md - - Docker 启动: docker_start.md - - Docker 启动脚本: docker_start.sh.md - - Linux 配置: linux_configure.sh.md - - Linux 部署构建: linux_deploy_build.sh.md - - Linux 开发构建: linux_develop_build.sh.md - - Linux 快速部署: linux_fast_deploy_build.sh.md - - Linux 快速开发: linux_fast_develop_build.sh.md - - Linux 测试运行: linux_run_tests.sh.md - - Windows 配置: windows_configure.md - - Windows 部署构建: windows_deploy_build.md - - Windows 开发构建: windows_develop_build.md - - Windows 快速部署: windows_fast_deploy_build.md - - Windows 快速开发: windows_fast_develop_build.md - - Windows 测试运行: windows_run_tests.md diff --git a/document/scripts/build_helpers/README.md b/document/scripts/build_helpers/README.md index e2ba8651c..f9c24a925 100644 --- a/document/scripts/build_helpers/README.md +++ b/document/scripts/build_helpers/README.md @@ -1,3 +1,8 @@ +--- +title: 构建辅助脚本 (Build Helpers) +description: "文档编写日期: 2026-03-20,本目录包含CFDesktop项目的构建辅助脚本。" +--- + # 构建辅助脚本 (Build Helpers) > 文档编写日期: 2026-03-20 @@ -46,7 +51,7 @@ # 快速开发构建(增量编译) ./scripts/build_helpers/linux_fast_develop_build.sh -``` +```text ### 部署构建 @@ -56,19 +61,19 @@ # 快速部署构建(增量编译) ./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构建 @@ -87,7 +92,7 @@ # 运行测试 ./scripts/build_helpers/docker_start.sh --run-project-test -``` +```bash ## 架构支持 diff --git a/document/scripts/build_helpers/ci_build_entry.sh.md b/document/scripts/build_helpers/ci_build_entry.sh.md index 7f6db9250..ef72b3fe5 100644 --- a/document/scripts/build_helpers/ci_build_entry.sh.md +++ b/document/scripts/build_helpers/ci_build_entry.sh.md @@ -1,3 +1,8 @@ +--- +title: cibuildentry.sh +description: "文档编写日期: 2026-03-20,是CI(持续集成)环境下的构建入口脚本,主要用于Docker容" +--- + # ci_build_entry.sh > 文档编写日期: 2026-03-20 @@ -8,7 +13,7 @@ ```bash ./scripts/build_helpers/ci_build_entry.sh [ci|test] -``` +```bash ## Scripts详解 @@ -36,7 +41,7 @@ ```bash ./scripts/build_helpers/ci_build_entry.sh ci -``` +```text 该模式会执行: 1. 调用 `linux_develop_build.sh` 进行配置和构建 @@ -49,7 +54,7 @@ ```bash ./scripts/build_helpers/ci_build_entry.sh test -``` +```text 该模式会调用 `linux_run_tests.sh` 运行已有构建的测试。 @@ -81,16 +86,16 @@ docker run --rm cfdesktop-build bash scripts/build_helpers/ci_build_entry.sh ci # 仅运行测试 docker run --rm cfdesktop-build bash scripts/build_helpers/ci_build_entry.sh test -``` +```text ### 错误处理 当遇到未知架构时,脚本会输出错误信息并退出: -``` +```text ERROR: Unknown architecture: <架构名> Supported: x86_64, aarch64, armv7l -``` +```text ### 注意事项 diff --git a/document/scripts/build_helpers/config_files.md b/document/scripts/build_helpers/config_files.md index 588e962a4..82a3ac49b 100644 --- a/document/scripts/build_helpers/config_files.md +++ b/document/scripts/build_helpers/config_files.md @@ -1,3 +1,8 @@ +--- +title: 构建配置文件说明 +description: "文档编写日期: 2026-03-20,配置文件位于 目录,用于控制 CMake 构建参数。根据不同" +--- + # 构建配置文件说明 > 文档编写日期: 2026-03-20 @@ -99,7 +104,7 @@ build_dir=out/build_develop [options] jobs=12 -``` +```text **特点**:使用 Debug 模式,适合日常开发调试。 @@ -117,7 +122,7 @@ build_dir=out/build_deploy [options] jobs=16 -``` +```text **特点**:使用 Release 模式,最高优化级别,适合生产部署。 @@ -135,7 +140,7 @@ build_dir=out/build_ci [options] jobs=16 -``` +```text **特点**:用于 Docker CI 环境,x86_64 平台标准化构建。 @@ -153,7 +158,7 @@ build_dir=out/build_ci_aarch64 [options] jobs=16 -``` +```text **特点**:在 x86_64 主机上交叉编译 ARM64 程序,使用 `aarch64-linux-gnu-gcc`。 @@ -171,6 +176,6 @@ build_dir=out/build_ci_armhf [options] jobs=16 -``` +```text **特点**:在 x86_64 主机上交叉编译 ARM32 HF 程序,使用 `arm-linux-gnueabihf-gcc`,目标平台包括 IMX6ULL (i.MX 6UltraLite) 等 ARM Cortex-A7 设备。 diff --git a/document/scripts/build_helpers/docker_start.md b/document/scripts/build_helpers/docker_start.md index 6be92499e..e348ff80c 100644 --- a/document/scripts/build_helpers/docker_start.md +++ b/document/scripts/build_helpers/docker_start.md @@ -1,3 +1,8 @@ +--- +title: dockerstart.ps1 +description: "文档编写日期: 2026-03-20,本脚本用于在Docker容器中构建CFDesktop项目,提供" +--- + # docker_start.ps1 > 文档编写日期: 2026-03-20 @@ -7,7 +12,7 @@ ### 基本语法 ```powershell .\scripts\build_helpers\docker_start.ps1 [OPTIONS] -``` +```bash ### 参数说明 | 参数 | 类型 | 默认值 | 说明 | @@ -51,7 +56,7 @@ # 禁用日志记录 .\scripts\build_helpers\docker_start.ps1 -NoLog -``` +```text ## Scripts详解 @@ -76,14 +81,14 @@ 启动容器并进入交互式bash shell: ```powershell .\docker_start.ps1 -``` +```text 项目根目录挂载到容器内的 `/project`。 #### 2. CI验证模式 运行完整的CI构建: ```powershell .\docker_start.ps1 -Verify -``` +```text 执行 `scripts/build_helpers/ci_build_entry.sh ci` #### 3. 构建项目模式 @@ -100,7 +105,7 @@ 运行项目测试: ```powershell .\docker_start.ps1 -RunProjectTest -``` +```bash ### 架构支持 @@ -146,7 +151,7 @@ 使用 `-StayOnError` 参数,CI构建失败时容器不会退出,可进入交互模式调试: ```powershell .\docker_start.ps1 -Verify -StayOnError -``` +```text ### 快速构建模式 使用 `-FastBuild` 参数复用已有镜像: diff --git a/document/scripts/build_helpers/docker_start.sh.md b/document/scripts/build_helpers/docker_start.sh.md index b3d1b1c42..780fdd2f8 100644 --- a/document/scripts/build_helpers/docker_start.sh.md +++ b/document/scripts/build_helpers/docker_start.sh.md @@ -1,3 +1,8 @@ +--- +title: dockerstart.sh +description: "文档编写日期: 2026-03-20,是CFDesktop项目的Docker构建环境包装脚本,提供C" +--- + # docker_start.sh > 文档编写日期: 2026-03-20 @@ -8,7 +13,7 @@ ```bash bash scripts/build_helpers/docker_start.sh [options] -``` +```bash ### 参数说明 @@ -50,7 +55,7 @@ bash scripts/build_helpers/docker_start.sh [options] ```bash bash scripts/build_helpers/docker_start.sh -``` +```text 启动一个交互式bash shell,可以手动执行命令。 @@ -58,7 +63,7 @@ bash scripts/build_helpers/docker_start.sh ```bash bash scripts/build_helpers/docker_start.sh --verify -``` +```text 在容器中运行完整的CI构建流程。 @@ -70,13 +75,13 @@ bash scripts/build_helpers/docker_start.sh --build-project # 快速构建 bash scripts/build_helpers/docker_start.sh --build-project-fast -``` +```text #### 测试模式 ```bash bash scripts/build_helpers/docker_start.sh --run-project-test -``` +```text ### 使用示例 @@ -101,11 +106,11 @@ bash scripts/build_helpers/docker_start.sh --no-log # 跳过依赖安装(手动设置) bash scripts/build_helpers/docker_start.sh --no-deps -``` +```text ### 输出示例 -``` +```text ══════════════════════════════════════════════════════════════════════════ CFDesktop Docker Build Environment @@ -132,7 +137,7 @@ CFDesktop Docker Build Environment [●] RUN - Starting interactive shell → Type 'exit' to leave the container -``` +```bash ### Docker镜像 @@ -156,9 +161,9 @@ CFDesktop Docker Build Environment 日志默认保存在 `scripts/docker/logger/` 目录: -``` +```text scripts/docker/logger/ci_build_YYYYMMDD_HHMMSS.log -``` +```text 日志包含: - 开始时间 @@ -172,14 +177,14 @@ scripts/docker/logger/ci_build_YYYYMMDD_HHMMSS.log ```bash bash scripts/build_helpers/docker_start.sh --verify --stay-on-error -``` +```text 失败后会显示: -``` +```text === Build failed, staying in container for debugging === Type "exit" to leave the container -``` +```bash ### 路径处理 @@ -220,11 +225,11 @@ Type "exit" to leave the container # .github/workflows/ci.yml 示例 - name: Build in Docker run: bash scripts/build_helpers/docker_start.sh --verify -``` +```text 或调用测试入口: ```yaml - name: Run Tests run: bash scripts/build_helpers/docker_start.sh --run-project-test -``` +```text diff --git a/document/scripts/build_helpers/index.md b/document/scripts/build_helpers/index.md index 3ee5ba3e9..8195b0368 100644 --- a/document/scripts/build_helpers/index.md +++ b/document/scripts/build_helpers/index.md @@ -1,10 +1,11 @@ -# Build Helpers - -> Welcome to the Build Helpers section. +--- +title: 构建辅助脚本 +description: 本目录包含平台相关的构建辅助脚本,提供 Linux(Bash)和 Windows(PowerShel +--- -## Overview +# 构建辅助脚本 -Documentation and resources for Build Helpers. +本目录包含平台相关的构建辅助脚本,提供 Linux(Bash)和 Windows(PowerShell)双平台的 CMake 配置、快速开发构建、完整构建以及测试运行等标准化构建流程。同时提供 Docker 环境下的构建脚本以确保跨平台一致性。 --- diff --git a/document/scripts/build_helpers/linux_configure.sh.md b/document/scripts/build_helpers/linux_configure.sh.md index 42f7b38a5..c453b50cc 100644 --- a/document/scripts/build_helpers/linux_configure.sh.md +++ b/document/scripts/build_helpers/linux_configure.sh.md @@ -1,3 +1,8 @@ +--- +title: linuxconfigure.sh +description: "文档编写日期: 2026-03-20,是专门用于执行CMake配置的脚本,不执行构建。该脚本读取配置" +--- + # linux_configure.sh > 文档编写日期: 2026-03-20 @@ -8,7 +13,7 @@ ```bash ./scripts/build_helpers/linux_configure.sh [develop|deploy|ci] [-c|--config ] -``` +```bash ### 参数说明 @@ -65,7 +70,7 @@ # 使用自定义配置文件 ./scripts/build_helpers/linux_configure.sh deploy -c my_config.ini -``` +```text ### 执行流程 @@ -78,7 +83,7 @@ ### 输出示例 -``` +```bash ======================================== Starting Linux CMake Configuration Configuration: develop @@ -106,7 +111,7 @@ Running CMake configuration... CMake configuration completed successfully! To build the project, run: cmake --build build_develop ======================================== -``` +```bash ### 错误处理 @@ -126,7 +131,7 @@ cmake --build build_develop # 或使用构建脚本 ./scripts/build_helpers/linux_fast_develop_build.sh -``` +```text ### 注意事项 diff --git a/document/scripts/build_helpers/linux_deploy_build.sh.md b/document/scripts/build_helpers/linux_deploy_build.sh.md index c7ea16360..b2ba19031 100644 --- a/document/scripts/build_helpers/linux_deploy_build.sh.md +++ b/document/scripts/build_helpers/linux_deploy_build.sh.md @@ -1,3 +1,8 @@ +--- +title: linuxdeploybuild.sh +description: "文档编写日期: 2026-03-20,是完整的部署构建脚本,执行清理 + 配置 + 构建 + 测试的" +--- + # linux_deploy_build.sh > 文档编写日期: 2026-03-20 @@ -8,7 +13,7 @@ ```bash ./scripts/build_helpers/linux_deploy_build.sh [develop|deploy|ci] [-c|--config ] -``` +```bash ### 参数说明 @@ -47,7 +52,7 @@ ```bash Step 1: Cleaning build directory -``` +```text 使用 `lib_build.sh` 中的 `clean_build_dir` 函数清理构建目录。 @@ -55,7 +60,7 @@ Step 1: Cleaning build directory ```bash Step 2: Calling fast build script -``` +```text 调用 `linux_fast_deploy_build.sh` 执行实际的配置和构建。 @@ -63,7 +68,7 @@ Step 2: Calling fast build script ```bash Step 3: Running tests -``` +```text 调用 `linux_run_tests.sh` 运行所有测试。测试失败不会导致脚本退出(仅警告)。 @@ -81,11 +86,11 @@ Step 3: Running tests # 使用自定义配置文件 ./scripts/build_helpers/linux_deploy_build.sh deploy -c my_deploy_config.ini -``` +```text ### 输出示例 -``` +```text ======================================== Starting Linux Build Process (Full Clean + Build) ======================================== @@ -115,7 +120,7 @@ Executing: linux_run_tests.sh deploy ======================================== All tests passed successfully! ======================================== -``` +```text ### 部署配置示例 @@ -133,7 +138,7 @@ build_dir = build_deploy [options] jobs = 4 -``` +```bash ### 使用场景 diff --git a/document/scripts/build_helpers/linux_develop_build.sh.md b/document/scripts/build_helpers/linux_develop_build.sh.md index accc52cd2..be46d6871 100644 --- a/document/scripts/build_helpers/linux_develop_build.sh.md +++ b/document/scripts/build_helpers/linux_develop_build.sh.md @@ -1,3 +1,8 @@ +--- +title: linuxdevelopbuild.sh +description: "文档编写日期: 2026-03-20,是完整的开发构建脚本,执行清理 + 配置 + 构建 + 测试的" +--- + # linux_develop_build.sh > 文档编写日期: 2026-03-20 @@ -8,7 +13,7 @@ ```bash ./scripts/build_helpers/linux_develop_build.sh [develop|deploy|ci] [-c|--config ] -``` +```bash ### 参数说明 @@ -37,7 +42,7 @@ ```bash Step 1: Cleaning build directory -``` +```text 使用 `lib_build.sh` 中的 `clean_build_dir` 函数清理构建目录。这确保每次都是干净的构建。 @@ -45,7 +50,7 @@ Step 1: Cleaning build directory ```bash Step 2: Calling fast build script -``` +```text 调用 `linux_fast_develop_build.sh` 执行实际的配置和构建。 @@ -53,7 +58,7 @@ Step 2: Calling fast build script ```bash Step 3: Running tests -``` +```text 调用 `linux_run_tests.sh` 运行所有测试。测试失败不会导致脚本退出(仅警告)。 @@ -71,11 +76,11 @@ Step 3: Running tests # 使用自定义配置文件 ./scripts/build_helpers/linux_develop_build.sh develop -c my_config.ini -``` +```text ### 输出示例 -``` +```text ======================================== Starting Linux Build Process (Full Clean + Build) ======================================== @@ -105,7 +110,7 @@ Executing: linux_run_tests.sh develop ======================================== All tests passed successfully! ======================================== -``` +```bash ### 与快速构建的对比 diff --git a/document/scripts/build_helpers/linux_fast_deploy_build.sh.md b/document/scripts/build_helpers/linux_fast_deploy_build.sh.md index c3b32275e..61e533295 100644 --- a/document/scripts/build_helpers/linux_fast_deploy_build.sh.md +++ b/document/scripts/build_helpers/linux_fast_deploy_build.sh.md @@ -1,3 +1,8 @@ +--- +title: linuxfastdeploybuild.sh +description: "文档编写日期: 2026-03-20,是快速部署构建脚本,执行配置 + 构建流程,不清理构建目录,支" +--- + # linux_fast_deploy_build.sh > 文档编写日期: 2026-03-20 @@ -8,7 +13,7 @@ ```bash ./scripts/build_helpers/linux_fast_deploy_build.sh [develop|deploy|ci] [-c|--config ] -``` +```bash ### 参数说明 @@ -47,7 +52,7 @@ ```bash Step 1: Configuring with CMake -``` +```text 调用 `linux_configure.sh` 执行CMake配置。 @@ -55,7 +60,7 @@ Step 1: Configuring with CMake ```bash Step 2: Building project -``` +```text 使用CMake构建项目。如果配置了并行任务数,会使用 `--parallel` 参数加速编译。 @@ -73,11 +78,11 @@ Step 2: Building project # 使用自定义配置文件 ./scripts/build_helpers/linux_fast_deploy_build.sh deploy -c my_config.ini -``` +```text ### 输出示例 -``` +```bash ======================================== Starting Linux FAST Build Process (Deploy) ======================================== @@ -93,7 +98,7 @@ Step 2: Building project ======================================== Command: cmake --build build_deploy --parallel 4 ... -``` +```text ### 配置参数 @@ -102,7 +107,7 @@ Command: cmake --build build_deploy --parallel 4 ```ini [options] jobs=4 -``` +```text 如果未设置,则不使用并行参数。 @@ -140,4 +145,4 @@ jobs=4 # 需要测试时 ./scripts/build_helpers/linux_run_tests.sh deploy -``` +```text diff --git a/document/scripts/build_helpers/linux_fast_develop_build.sh.md b/document/scripts/build_helpers/linux_fast_develop_build.sh.md index 346f52fcf..77d16aa07 100644 --- a/document/scripts/build_helpers/linux_fast_develop_build.sh.md +++ b/document/scripts/build_helpers/linux_fast_develop_build.sh.md @@ -1,3 +1,8 @@ +--- +title: linuxfastdevelopbuild.sh +description: "文档编写日期: 2026-03-20,是快速开发构建脚本,执行配置 + 构建流程,不清理构建目录,支" +--- + # linux_fast_develop_build.sh > 文档编写日期: 2026-03-20 @@ -8,7 +13,7 @@ ```bash ./scripts/build_helpers/linux_fast_develop_build.sh [develop|deploy|ci] [-c|--config ] -``` +```bash ### 参数说明 @@ -37,7 +42,7 @@ ```bash Step 1: Configuring with CMake -``` +```text 调用 `linux_configure.sh` 执行CMake配置。如果构建目录已存在且有有效配置,此步骤会很快完成。 @@ -45,7 +50,7 @@ Step 1: Configuring with CMake ```bash Step 2: Building project -``` +```text 使用CMake构建项目。如果配置了并行任务数,会使用 `--parallel` 参数加速编译。 @@ -63,11 +68,11 @@ Step 2: Building project # 使用自定义配置文件 ./scripts/build_helpers/linux_fast_develop_build.sh develop -c my_config.ini -``` +```text ### 输出示例 -``` +```bash ======================================== Starting Linux FAST Build Process ======================================== @@ -83,7 +88,7 @@ Step 2: Building project ======================================== Command: cmake --build build_develop --parallel 4 ... -``` +```text ### 配置参数 @@ -92,7 +97,7 @@ Command: cmake --build build_develop --parallel 4 ```ini [options] jobs=4 -``` +```bash 如果未设置,则不使用并行参数。 @@ -140,4 +145,4 @@ jobs=4 # 需要测试时 ./scripts/build_helpers/linux_run_tests.sh -``` +```text diff --git a/document/scripts/build_helpers/linux_run_tests.sh.md b/document/scripts/build_helpers/linux_run_tests.sh.md index a12dc92ba..6cf6714d7 100644 --- a/document/scripts/build_helpers/linux_run_tests.sh.md +++ b/document/scripts/build_helpers/linux_run_tests.sh.md @@ -1,3 +1,8 @@ +--- +title: linuxruntests.sh +description: "文档编写日期: 2026-03-20,是测试运行脚本,使用CTest执行CMake测试套件。" +--- + # linux_run_tests.sh > 文档编写日期: 2026-03-20 @@ -8,7 +13,7 @@ ```bash ./scripts/build_helpers/linux_run_tests.sh [develop|deploy|ci] [-c|--config ] -``` +```bash ### 参数说明 @@ -43,9 +48,9 @@ 脚本会在配置的构建目录下的 `test` 子目录中查找测试: -``` +```text /test/ -``` +```text 例如,如果 `build_dir = build_develop`,则测试目录为 `build_develop/test/`。 @@ -63,13 +68,13 @@ # 使用自定义配置文件 ./scripts/build_helpers/linux_run_tests.sh develop -c my_config.ini -``` +```text ### 输出示例 #### 测试成功 -``` +```text ======================================== Running Tests (Config: develop) ======================================== @@ -94,11 +99,11 @@ Total Test time (real) = 0.25 sec ======================================== All tests passed successfully! ======================================== -``` +```text #### 测试失败 -``` +```text ======================================== Running Tests (Config: develop) ======================================== @@ -109,7 +114,7 @@ Running Tests (Config: develop) ======================================== Some tests failed with exit code: 8 ======================================== -``` +```bash ### 错误处理 @@ -161,4 +166,4 @@ Some tests failed with exit code: 8 # 或仅运行测试(假设已构建) ./scripts/build_helpers/linux_run_tests.sh ci -``` +```text diff --git a/document/scripts/build_helpers/windows_configure.md b/document/scripts/build_helpers/windows_configure.md index 44712ea14..564a292f9 100644 --- a/document/scripts/build_helpers/windows_configure.md +++ b/document/scripts/build_helpers/windows_configure.md @@ -1,3 +1,8 @@ +--- +title: windowsconfigure.ps1 +description: "文档编写日期: 2026-03-20,本脚本仅用于配置项目,使用CMake生成构建系统文件,不执行编" +--- + # windows_configure.ps1 > 文档编写日期: 2026-03-20 @@ -7,7 +12,7 @@ ### 基本语法 ```powershell .\scripts\build_helpers\windows_configure.ps1 [-Config ] -``` +```bash ### 参数说明 | 参数 | 类型 | 默认值 | 说明 | @@ -21,7 +26,7 @@ # 使用部署配置 .\scripts\build_helpers\windows_configure.ps1 -Config deploy -``` +```text ## Scripts详解 @@ -53,7 +58,7 @@ build_type = 构建类型 (Debug/Release/RelWithDebInfo) [paths] source = 源代码目录 (相对于项目根目录) build_dir = 构建输出目录 (相对于项目根目录) -``` +```yaml ### 执行流程 1. 加载配置文件 @@ -81,8 +86,8 @@ build_dir = 构建输出目录 (相对于项目根目录) 配置成功后,可使用以下命令进行编译: ```powershell cmake --build -``` +```yaml 或使用快速构建脚本: ```powershell .\scripts\build_helpers\windows_fast_develop_build.ps1 -``` +```text diff --git a/document/scripts/build_helpers/windows_deploy_build.md b/document/scripts/build_helpers/windows_deploy_build.md index b18f30801..8c6929840 100644 --- a/document/scripts/build_helpers/windows_deploy_build.md +++ b/document/scripts/build_helpers/windows_deploy_build.md @@ -1,3 +1,8 @@ +--- +title: windowsdeploybuild.ps1 +description: "文档编写日期: 2026-03-20,本脚本执行完整的部署构建流程,包含以下步骤:" +--- + # windows_deploy_build.ps1 > 文档编写日期: 2026-03-20 @@ -7,13 +12,13 @@ ### 基本语法 ```powershell .\scripts\build_helpers\windows_deploy_build.ps1 -``` +```text ### 使用示例 ```powershell # 执行完整的部署构建 (清理 + 配置 + 编译 + 测试) .\scripts\build_helpers\windows_deploy_build.ps1 -``` +```bash ## Scripts详解 diff --git a/document/scripts/build_helpers/windows_develop_build.md b/document/scripts/build_helpers/windows_develop_build.md index c1f0c5569..1ad720f6a 100644 --- a/document/scripts/build_helpers/windows_develop_build.md +++ b/document/scripts/build_helpers/windows_develop_build.md @@ -1,3 +1,8 @@ +--- +title: windowsdevelopbuild.ps1 +description: "文档编写日期: 2026-03-20,本脚本执行完整的开发构建流程,包含以下步骤:" +--- + # windows_develop_build.ps1 > 文档编写日期: 2026-03-20 @@ -7,13 +12,13 @@ ### 基本语法 ```powershell .\scripts\build_helpers\windows_develop_build.ps1 -``` +```text ### 使用示例 ```powershell # 执行完整的开发构建 (清理 + 配置 + 编译 + 测试) .\scripts\build_helpers\windows_develop_build.ps1 -``` +```bash ## Scripts详解 diff --git a/document/scripts/build_helpers/windows_fast_deploy_build.md b/document/scripts/build_helpers/windows_fast_deploy_build.md index ac063deda..fa255fa92 100644 --- a/document/scripts/build_helpers/windows_fast_deploy_build.md +++ b/document/scripts/build_helpers/windows_fast_deploy_build.md @@ -1,3 +1,8 @@ +--- +title: windowsfastdeploybuild.ps1 +description: "文档编写日期: 2026-03-20,本脚本执行快速部署构建,包含:" +--- + # windows_fast_deploy_build.ps1 > 文档编写日期: 2026-03-20 @@ -7,13 +12,13 @@ ### 基本语法 ```powershell .\scripts\build_helpers\windows_fast_deploy_build.ps1 -``` +```text ### 使用示例 ```powershell # 执行快速部署构建 (配置 + 编译) .\scripts\build_helpers\windows_fast_deploy_build.ps1 -``` +```text ## Scripts详解 @@ -45,7 +50,7 @@ 从配置文件读取构建目录和并行任务数,然后执行编译: ```bash cmake --build [--parallel ] -``` +```bash ### 构建计时 脚本使用`Start-BuildTimer`和`Stop-BuildTimer`记录编译耗时。 @@ -78,7 +83,7 @@ cmake --build [--parallel ] ```ini [options] jobs = 8 # 并行编译任务数,留空则使用CMake默认值 -``` +```text ### 注意事项 - 本脚本不运行测试,如需测试请使用 `windows_run_tests.ps1 -Config deploy` diff --git a/document/scripts/build_helpers/windows_fast_develop_build.md b/document/scripts/build_helpers/windows_fast_develop_build.md index a5634db06..f448ee15a 100644 --- a/document/scripts/build_helpers/windows_fast_develop_build.md +++ b/document/scripts/build_helpers/windows_fast_develop_build.md @@ -1,3 +1,8 @@ +--- +title: windowsfastdevelopbuild.ps1 +description: "文档编写日期: 2026-03-20,本脚本执行快速开发构建,包含:" +--- + # windows_fast_develop_build.ps1 > 文档编写日期: 2026-03-20 @@ -7,13 +12,13 @@ ### 基本语法 ```powershell .\scripts\build_helpers\windows_fast_develop_build.ps1 -``` +```text ### 使用示例 ```powershell # 执行快速开发构建 (配置 + 编译) .\scripts\build_helpers\windows_fast_develop_build.ps1 -``` +```text ## Scripts详解 @@ -45,7 +50,7 @@ 从配置文件读取构建目录和并行任务数,然后执行编译: ```bash cmake --build [--parallel ] -``` +```bash ### 构建计时 脚本使用`Start-BuildTimer`和`Stop-BuildTimer`记录编译耗时。 @@ -71,7 +76,7 @@ cmake --build [--parallel ] ```ini [options] jobs = 8 # 并行编译任务数,留空则使用CMake默认值 -``` +```text ### 注意事项 - 本脚本不运行测试,如需测试请使用 `windows_run_tests.ps1` diff --git a/document/scripts/build_helpers/windows_run_tests.md b/document/scripts/build_helpers/windows_run_tests.md index 3a43bfabe..71c83a987 100644 --- a/document/scripts/build_helpers/windows_run_tests.md +++ b/document/scripts/build_helpers/windows_run_tests.md @@ -1,3 +1,8 @@ +--- +title: windowsruntests.ps1 +description: "文档编写日期: 2026-03-20,本脚本使用CTest运行项目的测试套件。它会:" +--- + # windows_run_tests.ps1 > 文档编写日期: 2026-03-20 @@ -7,7 +12,7 @@ ### 基本语法 ```powershell .\scripts\build_helpers\windows_run_tests.ps1 [-Config ] -``` +```bash ### 参数说明 | 参数 | 类型 | 默认值 | 说明 | @@ -21,7 +26,7 @@ # 使用部署配置运行测试 .\scripts\build_helpers\windows_run_tests.ps1 -Config deploy -``` +```text ## Scripts详解 @@ -60,7 +65,7 @@ #### 4. 执行测试 ```bash ctest --test-dir --output-on-failure -``` +```text ### CTest参数说明 - `--test-dir`: 指定测试目录 diff --git a/document/scripts/dependency/.pages b/document/scripts/dependency/.pages deleted file mode 100644 index 5b2106f3b..000000000 --- a/document/scripts/dependency/.pages +++ /dev/null @@ -1,3 +0,0 @@ -title: 依赖安装 -nav: - - 构建依赖: install_build_dependencies.sh.md diff --git a/document/scripts/dependency/index.md b/document/scripts/dependency/index.md index 0acd268f8..53e880093 100644 --- a/document/scripts/dependency/index.md +++ b/document/scripts/dependency/index.md @@ -1,10 +1,11 @@ -# Dependency - -> Welcome to the Dependency section. +--- +title: 依赖管理脚本 +description: 本目录包含第三方依赖的安装与管理脚本,用于自动检测系统环境并安装 CFDesktop 所需的构建工具 +--- -## Overview +# 依赖管理脚本 -Documentation and resources for Dependency. +本目录包含第三方依赖的安装与管理脚本,用于自动检测系统环境并安装 CFDesktop 所需的构建工具链、Qt SDK、CMake 及其他运行时依赖,支持 Linux 和 Windows 双平台。 --- diff --git a/document/scripts/dependency/install_build_dependencies.sh.md b/document/scripts/dependency/install_build_dependencies.sh.md index 9a4d37fc2..d43df44f3 100644 --- a/document/scripts/dependency/install_build_dependencies.sh.md +++ b/document/scripts/dependency/install_build_dependencies.sh.md @@ -1,3 +1,8 @@ +--- +title: installbuilddependencies.sh +description: "文档编写日期: 2026-03-20,本脚本用于安装CFDesktop项目的构建依赖,主要包含:" +--- + # install_build_dependencies.sh > 文档编写日期: 2026-03-20 @@ -7,7 +12,7 @@ ### 基本语法 ```bash ./scripts/dependency/install_build_dependencies.sh -``` +```bash ### 环境变量 | 环境变量 | 默认值 | 说明 | @@ -32,7 +37,7 @@ QT_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/qt ./scripts/dependency/install_b # 指定APT镜像 QT_MIRROR_APT=https://mirrors.ustc.edu.cn/ubuntu ./scripts/dependency/install_build_dependencies.sh -``` +```bash ## Scripts详解 (Detailed Explanation) @@ -102,10 +107,10 @@ QT_MIRROR_APT=https://mirrors.ustc.edu.cn/ubuntu ./scripts/dependency/install_bu export Qt6_DIR=/opt/Qt/6.8.1//lib/cmake/Qt6 export PATH=$Qt6_DIR/bin:$PATH export LD_LIBRARY_PATH=$Qt6_DIR/lib:$LD_LIBRARY_PATH -``` +```text ### 安装目录结构 -``` +```text /opt/Qt/ └── 6.8.1/ ├── gcc_64/ # x86_64架构 @@ -117,7 +122,7 @@ export LD_LIBRARY_PATH=$Qt6_DIR/lib:$LD_LIBRARY_PATH ├── bin/ ├── lib/ └── ... -``` +```text ### 注意事项 - 需要 **root 权限** 执行 diff --git a/document/scripts/develop/.pages b/document/scripts/develop/.pages deleted file mode 100644 index f40ba5cf6..000000000 --- a/document/scripts/develop/.pages +++ /dev/null @@ -1,6 +0,0 @@ -title: 开发脚本 -nav: - - C++ 格式化 (bash): format_cpp.sh.md - - C++ 格式化 (PowerShell): format_cpp.ps1.md - - 尾部空格清理 (bash): remove_trailing_space.sh.md - - 尾部空格清理 (PowerShell): remove_trailing_space.ps1.md diff --git a/document/scripts/develop/format_cpp.ps1.md b/document/scripts/develop/format_cpp.ps1.md index 53d4ad1e8..f7dd14089 100644 --- a/document/scripts/develop/format_cpp.ps1.md +++ b/document/scripts/develop/format_cpp.ps1.md @@ -1,3 +1,8 @@ +--- +title: formatcpp.ps1 +description: "文档编写日期: 2026-03-20,使用 自动格式化项目中的 C++ 源代码文件,确保代码风格一" +--- + # format_cpp.ps1 > 文档编写日期: 2026-03-20 @@ -8,7 +13,7 @@ ```powershell .\scripts\develop\format_cpp.ps1 [OPTIONS] -``` +```bash ### 参数说明 @@ -28,7 +33,7 @@ # 预览格式化效果 .\scripts\develop\format_cpp.ps1 -DryRun -``` +```bash ## Scripts详解 (Detailed Explanation) @@ -69,7 +74,7 @@ # 或使用 WSL/Linux 子系统 sudo apt install clang-format -``` +```bash ### 工作模式 @@ -86,7 +91,7 @@ sudo apt install clang-format ```powershell Import-Module LibCommon.psm1 # 提供日志函数 (Write-LogInfo, Write-LogSuccess 等) Import-Module LibPaths.psm1 # 提供路径函数 (Get-ProjectRoot) -``` +```bash ### 输出日志 diff --git a/document/scripts/develop/format_cpp.sh.md b/document/scripts/develop/format_cpp.sh.md index 10a8e9ac5..be65bc2f7 100644 --- a/document/scripts/develop/format_cpp.sh.md +++ b/document/scripts/develop/format_cpp.sh.md @@ -1,3 +1,8 @@ +--- +title: formatcpp.sh +description: "文档编写日期: 2026-03-20,使用 自动格式化项目中的 C++ 源代码文件,确保代码风格一" +--- + # format_cpp.sh > 文档编写日期: 2026-03-20 @@ -8,7 +13,7 @@ ```bash ./scripts/develop/format_cpp.sh [OPTIONS] -``` +```bash ### 参数说明 @@ -29,7 +34,7 @@ # 预览格式化效果 ./scripts/develop/format_cpp.sh --dry-run -``` +```bash ## Scripts详解 (Detailed Explanation) @@ -72,7 +77,7 @@ sudo pacman -S clang # macOS brew install clang-format -``` +```bash ### 工作模式 diff --git a/document/scripts/develop/index.md b/document/scripts/develop/index.md index 3a310d9c9..e34f72a5d 100644 --- a/document/scripts/develop/index.md +++ b/document/scripts/develop/index.md @@ -1,10 +1,11 @@ -# develop - -> Welcome to the develop section. +--- +title: 开发辅助脚本 +description: 本目录包含日常开发工作流辅助脚本,提供代码格式化、依赖快速更新、开发环境初始化等便捷工具,帮助开发者 +--- -## Overview +# 开发辅助脚本 -Documentation and resources for develop. +本目录包含日常开发工作流辅助脚本,提供代码格式化、依赖快速更新、开发环境初始化等便捷工具,帮助开发者快速进入开发状态并保持代码风格一致。 --- diff --git a/document/scripts/develop/remove_trailing_space.ps1.md b/document/scripts/develop/remove_trailing_space.ps1.md index da79a548b..3ac229680 100644 --- a/document/scripts/develop/remove_trailing_space.ps1.md +++ b/document/scripts/develop/remove_trailing_space.ps1.md @@ -1,3 +1,8 @@ +--- +title: removetrailingspace.ps1 +description: "文档编写日期: 2026-03-20,Windows PowerShell版本的行尾空白删除工具,与" +--- + # remove_trailing_space.ps1 > 文档编写日期: 2026-03-20 @@ -7,7 +12,7 @@ ### 基本语法 ```powershell .\scripts\develop\remove_trailing_space.ps1 [OPTIONS] -``` +```bash ### 参数说明 | 参数 | 说明 | @@ -31,7 +36,7 @@ # 检查暂存文件(pre-commit钩子) .\scripts\develop\remove_trailing_space.ps1 -StagedCheck -``` +```powershell ## Scripts详解 (Detailed Explanation) @@ -66,7 +71,7 @@ Windows PowerShell版本的行尾空白删除工具,与bash版本功能对等 - try-catch 自动跳过二进制文件 ### 输出格式 -``` +```yaml === Remove Trailing Whitespace === Project: C:\path\to\project Mode: Staged files only @@ -78,7 +83,7 @@ src/main.cpp: === Summary === Processed: 150 files Fixed: 2 files -``` +```bash ### 退出码 | 退出码 | 说明 | diff --git a/document/scripts/develop/remove_trailing_space.sh.md b/document/scripts/develop/remove_trailing_space.sh.md index 32a6c448e..f2fbfca9b 100644 --- a/document/scripts/develop/remove_trailing_space.sh.md +++ b/document/scripts/develop/remove_trailing_space.sh.md @@ -1,3 +1,8 @@ +--- +title: removetrailingspace.sh +description: "文档编写日期: 2026-03-20,删除项目中所有文本文件的行尾空格和制表符,保持代码库整洁。" +--- + # remove_trailing_space.sh > 文档编写日期: 2026-03-20 @@ -7,7 +12,7 @@ ### 基本语法 ```bash ./scripts/develop/remove_trailing_space.sh [OPTIONS] -``` +```bash ### 参数说明 | 参数 | 说明 | @@ -31,7 +36,7 @@ # 检查模式(CI/CD场景) ./scripts/develop/remove_trailing_space.sh --check -``` +```bash ## Scripts详解 (Detailed Explanation) @@ -61,7 +66,7 @@ - 每个文件最多显示5行有问题内容(dry-run/check模式) ### 输出格式 -``` +```yaml === Remove Trailing Whitespace === Project: /path/to/project Mode: Staged files only @@ -73,7 +78,7 @@ src/main.cpp: === Summary === Processed: 150 files Fixed: 2 files -``` +```bash ### 退出码 | 退出码 | 说明 | diff --git a/document/scripts/docker/.dockerignore.md b/document/scripts/docker/.dockerignore.md index 954e25c65..464b493ba 100644 --- a/document/scripts/docker/.dockerignore.md +++ b/document/scripts/docker/.dockerignore.md @@ -1,3 +1,8 @@ +--- +title: .dockerignore +description: "文档编写日期: 2026-03-20,用于优化Docker构建上下文,排除不必要的文件,减少构建上下" +--- + # .dockerignore > 文档编写日期: 2026-03-20 diff --git a/document/scripts/docker/.pages b/document/scripts/docker/.pages deleted file mode 100644 index 81ae330d7..000000000 --- a/document/scripts/docker/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: Docker -nav: - - Docker Compose: docker-compose.yml.md - - Dockerfile: Dockerfile.build.md diff --git a/document/scripts/docker/Dockerfile.build.md b/document/scripts/docker/Dockerfile.build.md index b52214440..d31e734e6 100644 --- a/document/scripts/docker/Dockerfile.build.md +++ b/document/scripts/docker/Dockerfile.build.md @@ -1,3 +1,8 @@ +--- +title: Dockerfile.build +description: "文档编写日期: 2026-03-20,- - 避免交互式提示" +--- + # Dockerfile.build > 文档编写日期: 2026-03-20 @@ -16,7 +21,7 @@ docker buildx build --platform linux/amd64,linux/arm64 \ # ARM64构建 docker build --build-arg QT_ARCH=linux_gcc_arm64 --platform linux/arm64 \ -f scripts/docker/Dockerfile.build -t cfdesktop-build:arm64 . -``` +```text ### 运行容器 ```bash @@ -25,7 +30,7 @@ docker run --rm --platform linux/amd64 -v $(pwd):/project cfdesktop-build # ARM64平台 docker run --rm --platform linux/arm64 -v $(pwd):/project cfdesktop-build -``` +```bash ## Scripts详解 diff --git a/document/scripts/docker/docker-compose.yml.md b/document/scripts/docker/docker-compose.yml.md index af4ad5b52..bcd4da38d 100644 --- a/document/scripts/docker/docker-compose.yml.md +++ b/document/scripts/docker/docker-compose.yml.md @@ -1,3 +1,8 @@ +--- +title: "docker-compose.yml" +description: "文档编写日期: 2026-03-20,所有服务挂载项目根目录到容器的" +--- + # docker-compose.yml > 文档编写日期: 2026-03-20 @@ -7,7 +12,7 @@ ### 基本语法 ```bash docker-compose -f scripts/docker/docker-compose.yml [COMMAND] -``` +```text ### 常用命令 ```bash @@ -22,7 +27,7 @@ docker-compose -f scripts/docker/docker-compose.yml run build-arm64 # 运行验证服务 docker-compose -f scripts/docker/docker-compose.yml run verify -``` +```bash ## Scripts详解 @@ -57,4 +62,4 @@ docker-compose -f scripts/docker/docker-compose.yml run verify verify服务目前使用`/bin/bash`作为默认命令。在完成Phase 3后,可取消注释以下命令以使用`ci_build_entry.sh`: ```bash command: bash scripts/build_helpers/ci_build_entry.sh ci -``` +```text diff --git a/document/scripts/docker/index.md b/document/scripts/docker/index.md index 35c48d294..113a202f6 100644 --- a/document/scripts/docker/index.md +++ b/document/scripts/docker/index.md @@ -1,10 +1,11 @@ -# docker - -> Welcome to the docker section. +--- +title: Docker 脚本 +description: 本目录包含基于 Docker 的构建与部署脚本,用于在容器化环境中执行标准化的编译、测试和打包流程, +--- -## Overview +# Docker 脚本 -Documentation and resources for docker. +本目录包含基于 Docker 的构建与部署脚本,用于在容器化环境中执行标准化的编译、测试和打包流程,确保不同开发机器上的构建结果一致性。 --- diff --git a/document/scripts/document/.pages b/document/scripts/document/.pages deleted file mode 100644 index b2011fb2a..000000000 --- a/document/scripts/document/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: 文档工具 -nav: - - MkDocs 开发 (bash): mkdocs_dev.sh.md - - MkDocs 开发 (PowerShell): mkdocs_dev.ps1.md diff --git a/document/scripts/document/index.md b/document/scripts/document/index.md index 7074d7d6f..e7d00106d 100644 --- a/document/scripts/document/index.md +++ b/document/scripts/document/index.md @@ -1,89 +1,31 @@ -# document - -> 文档编写日期: 2026-04-06 - -## 概述 - -本目录包含 CFDesktop 项目 MkDocs 文档系统的开发环境管理脚本。这些脚本用于自动化 Python 虚拟环境的创建、依赖安装、MkDocs 开发服务器的启停,以及 API 文档(Doxygen + Doxybook2)的生成管线。 - -## 目录结构 +--- +title: 文档脚本 +description: CFDesktop 文档系统已从 MkDocs 迁移到 VitePress。 +--- -``` -scripts/document/ -├── mkdocs_dev.sh # Linux/macOS Bash 脚本(主入口) -├── mkdocs_dev.ps1 # Windows PowerShell 脚本(对等实现) -└── pyproject.toml # Python 依赖声明文件 -``` +# 文档脚本 -## 快速开始 +CFDesktop 文档系统已从 MkDocs 迁移到 VitePress。 -### Linux / macOS +## 当前入口 ```bash -# 首次安装环境并启动开发服务器 -./scripts/document/mkdocs_dev.sh serve - -# 自定义端口启动 -./scripts/document/mkdocs_dev.sh serve -p 3000 -``` - -### Windows - -```powershell -# 首次安装环境并启动开发服务器 -.\scripts\document\mkdocs_dev.ps1 serve - -# 自定义端口启动 -.\scripts\document\mkdocs_dev.ps1 serve -Port 3000 -``` - -## 脚本一览 - -| 脚本 | 平台 | 功能 | -|------|------|------| -| `mkdocs_dev.sh` | Linux/macOS | Bash 版本,提供全功能 MkDocs 开发工作流 | -| `mkdocs_dev.ps1` | Windows | PowerShell 版本,与 Bash 版本功能完全对等 | -| `pyproject.toml` | 通用 | Python 依赖声明,包含 mkdocs 及所有插件 | - -## 工作原理 - -脚本执行以下自动化流程: - -``` -检查 Python >= 3.10 - ↓ -检测 .venv/ 是否存在 - ├── 不存在 → python3 -m venv .venv - └── 已存在 → 跳过创建 - ↓ -激活虚拟环境 - ↓ -检查依赖是否需要更新(通过 hash 比对 pyproject.toml) - ├── 需要更新 → pip install - └── 无变更 → 跳过安装 - ↓ -执行用户指定的子命令(serve / build / api 等) -``` - -## 相关配置文件 - -| 文件 | 位置 | 说明 | -|------|------|------| -| `mkdocs.yml` | 项目根目录 | MkDocs 主配置文件 | -| `Doxyfile` | 项目根目录 | Doxygen 配置文件(`api` 子命令使用) | -| `doxybook.json` | 项目根目录 | Doxybook2 配置文件(`api` 子命令使用) | +pnpm install +pnpm dev +pnpm build +pnpm preview +```bash -## 子命令对比 +## 配置文件 -| 子命令 | 功能 | 是否需要虚拟环境 | 说明 | -|--------|------|-----------------|------| -| `serve` | 启动开发服务器 | 自动创建 | 默认命令,支持热重载 | -| `build` | 构建静态站点 | 自动创建 | 输出到 `out/docs/site/` | -| `api` | 生成 API 文档 | 自动创建 | 需要 doxygen + doxybook2 | -| `install` | 仅安装依赖 | 自动创建 | 用于环境初始化 | -| `clean` | 清理构建产物 | 不需要 | 保留 `.venv` | -| `reset` | 重建虚拟环境 | 删除后重建 | 完全重置开发环境 | +| 文件 | 说明 | +|------|------| +| `package.json` | pnpm 脚本和 VitePress 依赖 | +| `pnpm-lock.yaml` | 文档站依赖锁文件 | +| `project.config.ts` | 项目文档配置,参考 imx-forge 的组织方式 | +| `site/.vitepress/config.mts` | VitePress 主配置 | +| `.github/workflows/deploy.yml` | GitHub Pages 发布 | ---- +## API 文档 -*Last updated: 2026-04-06* +Doxygen 相关配置暂时保留,但自动生成的 `document/api/**` 不进入主站导航。后续二期再决定是否以 Doxygen HTML 或修复后的 Markdown 形式发布 API 参考。 diff --git a/document/scripts/document/mkdocs_dev.ps1.md b/document/scripts/document/mkdocs_dev.ps1.md deleted file mode 100644 index 701522903..000000000 --- a/document/scripts/document/mkdocs_dev.ps1.md +++ /dev/null @@ -1,290 +0,0 @@ -# mkdocs_dev.ps1 - -> 文档编写日期: 2026-04-06 - -## 使用办法 (Usage) - -### 基本语法 - -```powershell -.\scripts\document\mkdocs_dev.ps1 [OPTIONS] -``` - -### 命令说明 - -| 命令 | 说明 | -|------|------| -| `serve` | 启动 MkDocs 开发服务器(默认命令) | -| `build` | 构建静态站点到 `out\docs\site\` | -| `api` | 运行 Doxygen + Doxybook2 API 文档管线 | -| `install` | 仅创建/更新虚拟环境和安装依赖 | -| `clean` | 清理构建产物(`out\docs\`、`__pycache__`、Doxygen XML) | -| `reset` | 删除并重建 `.venv` 虚拟环境 | -| `help` | 显示帮助信息 | - -### 参数说明 - -| 参数 | 类型 | 说明 | -|------|------|------| -| `-Command` | string | 要执行的命令(默认: `serve`) | -| `-Port` | int | 开发服务器端口(默认: `8000`) | -| `-Bind` | string | 开发服务器绑定地址(默认: `127.0.0.1`) | -| `-VerboseFlag` | switch | 启用详细输出模式 | - -### 使用示例 - -```powershell -# 启动开发服务器(默认 8000 端口) -.\scripts\document\mkdocs_dev.ps1 serve - -# 自定义端口和绑定地址启动 -.\scripts\document\mkdocs_dev.ps1 serve -Port 3000 -Bind "0.0.0.0" - -# 构建静态站点 -.\scripts\document\mkdocs_dev.ps1 build - -# 生成 API 文档(需要 doxygen + doxybook2) -.\scripts\document\mkdocs_dev.ps1 api - -# 仅安装依赖(不启动服务器) -.\scripts\document\mkdocs_dev.ps1 install - -# 清理构建产物 -.\scripts\document\mkdocs_dev.ps1 clean - -# 完全重置虚拟环境 -.\scripts\document\mkdocs_dev.ps1 reset - -# 使用详细模式调试 -.\scripts\document\mkdocs_dev.ps1 install -VerboseFlag - -# 查看帮助 -.\scripts\document\mkdocs_dev.ps1 help -``` - -## Scripts详解 (Detailed Explanation) - -### 脚本用途 - -`mkdocs_dev.ps1` 是 `mkdocs_dev.sh` 的 Windows PowerShell 等效实现,提供完全一致的 MkDocs 文档开发环境管理功能。两者功能、命令和参数完全对等,确保跨平台开发体验一致。 - -### 与 Bash 版本的差异 - -| 方面 | `mkdocs_dev.sh` | `mkdocs_dev.ps1` | -|------|------------------|-------------------| -| 平台 | Linux / macOS | Windows | -| 虚拟环境激活 | `source .venv/bin/activate` | `. .venv\Scripts\Activate.ps1` | -| 参数风格 | `-p`, `--port` | `-Port` | -| 详细模式 | `-v`, `--verbose` | `-VerboseFlag` (switch) | -| 脚本库 | `lib_common.sh` | `LibCommon.psm1` | -| 路径分隔符 | `/` | `\` | -| 依赖 hash | `md5sum` | `Get-FileHash -Algorithm MD5` | - -### 脚本常量 - -| 常量 | 值 | 说明 | -|------|-----|------| -| `$VenvDir` | `.venv` | 虚拟环境目录名(位于项目根目录) | -| `$DepsMarker` | `.deps_installed` | 依赖安装标记文件(位于 `.venv\` 内) | -| `$DocsOutputDir` | `out\docs\site` | 静态站点输出路径 | -| `$DoxygenXmlDir` | `xml` | Doxygen XML 输出目录 | -| `$DoxygenApiDir` | `document\api` | API 文档 Markdown 输出目录 | -| `$ProjectRoot` | 自动推导 | 项目根目录绝对路径 | -| `$ScriptDir` | `$PSScriptRoot` | 脚本所在目录 | - -### 环境要求 - -| 依赖项 | 版本要求 | 用途 | -|--------|----------|------| -| **Python** | >= 3.10 | 运行 MkDocs 及相关工具 | -| **PowerShell** | >= 5.1 | 脚本运行环境 | -| **pip** | 随 Python 安装 | 包管理 | -| **doxygen** | 任意稳定版 | API 文档生成(仅 `api` 命令需要) | -| **doxybook2** | >= 1.5.0 | Doxygen XML → Markdown 转换(仅 `api` 命令需要) | - -### Python 安装指引 - -```powershell -# 方法 1: 官方安装器 -# 从 https://www.python.org/downloads/ 下载安装 -# 安装时勾选 "Add Python to PATH" - -# 方法 2: winget -winget install Python.Python.3.12 - -# 方法 3: Chocolatey -choco install python - -# 方法 4: Scoop -scoop install python -``` - -### 模块导入 - -脚本从 `scripts/lib/powershell/` 目录导入以下模块: - -```powershell -Import-Module LibCommon.psm1 # 提供日志函数 (Write-LogInfo, Write-LogSuccess 等) -``` - -### 依赖安装策略 - -与 Bash 版本一致的智能安装策略: - -``` -检查 .venv\.deps_installed 标记文件 - ↓ -标记文件存在? - ├── 存在 → 读取标记中存储的 pyproject.toml 的 MD5 hash - │ 与当前 pyproject.toml 的 MD5 hash 比对(Get-FileHash) - │ ├── 一致 → 跳过安装 - │ └── 不一致 → 执行 pip install 并更新标记 - └── 不存在 → 执行 pip install 并写入标记 -``` - -### 各命令详细说明 - -#### `serve` — 开发服务器 - -```powershell -.\scripts\document\mkdocs_dev.ps1 serve -Port 3000 -Bind "0.0.0.0" -``` - -**行为:** - -1. 调用 `Test-Python` 检查 Python 环境 -2. 调用 `New-Venv` 确保虚拟环境存在 -3. 调用 `Enable-Venv` 激活虚拟环境 -4. 调用 `Install-DocDeps` 确保依赖已安装 -5. 使用 `Push-Location` 切换到项目根目录 -6. 执行 `mkdocs serve --dev-addr=ADDR:PORT` -7. 使用 `try/finally` 确保退出时恢复工作目录 - -#### `build` — 构建静态站点 - -```powershell -.\scripts\document\mkdocs_dev.ps1 build -``` - -**行为:** - -1. 确保环境就绪(同 `serve`) -2. 执行 `mkdocs build --clean -d out\docs\site` -3. 输出构建完成信息 - -#### `api` — API 文档管线 - -```powershell -.\scripts\document\mkdocs_dev.ps1 api -``` - -**管线流程(与 Bash 版本一致):** - -``` -C++ 头文件 (*.h, *.hpp) → doxygen Doxyfile → xml\ → doxybook2 → document\api\ -``` - -**前置条件:** - -```powershell -# 安装 Doxygen -# 从 https://www.doxygen.nl/download.html 下载 Windows 安装包 -# 或使用 Chocolatey: choco install doxygen - -# 安装 Doxybook2 -# 从 https://github.com/matusnovak/doxybook2/releases 下载 -``` - -#### `install` — 安装依赖 - -```powershell -.\scripts\document\mkdocs_dev.ps1 install -.\scripts\document\mkdocs_dev.ps1 install -VerboseFlag # 详细输出 -``` - -#### `clean` — 清理构建产物 - -```powershell -.\scripts\document\mkdocs_dev.ps1 clean -``` - -**清理范围:** - -| 清理目标 | 路径 | 说明 | -|----------|------|------| -| MkDocs 输出 | `out\docs\site\` | 静态站点构建产物 | -| Doxygen XML | `xml\` | Doxygen 生成的 XML 中间文件 | -| Python 缓存 | `__pycache__\` | Python 字节码缓存(递归清理) | - -#### `reset` — 重置虚拟环境 - -```powershell -.\scripts\document\mkdocs_dev.ps1 reset -``` - -完全删除 `.venv\` 并重新创建,安装所有依赖。 - -### 核心函数 - -| 函数名 | 说明 | -|--------|------| -| `Test-Python` | 检查 Python 是否安装(支持 `python` 和 `python3` 两种命令名) | -| `New-Venv` | 创建 Python 虚拟环境(如果不存在) | -| `Enable-Venv` | 激活虚拟环境(`.venv\Scripts\Activate.ps1`) | -| `Install-DocDeps` | 安装/更新依赖(带 hash 比对) | -| `Initialize-DocEnv` | 组合调用上述四个函数,确保环境就绪 | -| `Invoke-Serve` | 执行 `serve` 命令 | -| `Invoke-Build` | 执行 `build` 命令 | -| `Invoke-Api` | 执行 `api` 命令 | -| `Invoke-Install` | 执行 `install` 命令 | -| `Invoke-Clean` | 执行 `clean` 命令 | -| `Invoke-Reset` | 执行 `reset` 命令 | - -### 返回码 - -| 返回码 | 说明 | -|--------|------| -| 0 | 成功 | -| 1 | Python 未安装 / 虚拟环境创建失败 / 外部工具缺失 | - -### 彩色日志输出 - -使用 `LibCommon.psm1` 模块提供的统一日志函数: - -``` -[2026-04-06 12:00:00] [INFO] 创建 Python 虚拟环境: C:\CFDesktop\.venv (青色) -[2026-04-06 12:00:00] [SUCCESS] 虚拟环境创建成功 (绿色) -[2026-04-06 12:00:00] [WARNING] 依赖版本不匹配 (黄色) -[2026-04-06 12:00:00] [ERROR] Python 3 未安装 (红色) -``` - -### 注意事项 - -1. **执行策略** — Windows 可能需要设置 PowerShell 执行策略:`Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser` -2. **Python PATH** — 确保 Python 已添加到系统 PATH(安装时勾选 "Add Python to PATH") -3. **`$ErrorActionPreference = "Stop"`** — 启用严格错误处理,任何命令失败都会立即终止 -4. **`Push-Location` / `Pop-Location`** — 使用 `try/finally` 模式确保工作目录正确恢复 -5. **Python 命令兼容** — 同时检测 `python` 和 `python3` 命令 -6. **与 Bash 版本完全对等** — 命令、参数、行为一致,确保跨平台体验统一 -7. **虚拟环境位置** — `.venv\` 创建在项目根目录,已添加到 `.gitignore` -8. **幂等性** — 多次运行不会重复安装依赖 - -### 完整输出示例 - -``` -PS C:\CFDesktop> .\scripts\document\mkdocs_dev.ps1 serve - -[2026-04-06 12:00:00] [INFO] 创建 Python 虚拟环境: C:\CFDesktop\.venv -[2026-04-06 12:00:03] [SUCCESS] 虚拟环境创建成功 -[2026-04-06 12:00:03] [INFO] 安装文档开发依赖... -[2026-04-06 12:00:20] [SUCCESS] 依赖安装完成 - -=== MkDocs 开发服务器 === -地址: http://127.0.0.1:8000 -文档: C:\CFDesktop\document - -INFO - Building documentation... -INFO - Documentation built in 2.35 seconds -INFO - [12:00:20] Serving on http://127.0.0.1:8000 -INFO - [12:00:20] Watching paths for changes -``` diff --git a/document/scripts/document/mkdocs_dev.sh.md b/document/scripts/document/mkdocs_dev.sh.md deleted file mode 100644 index b0d4e5601..000000000 --- a/document/scripts/document/mkdocs_dev.sh.md +++ /dev/null @@ -1,345 +0,0 @@ -# mkdocs_dev.sh - -> 文档编写日期: 2026-04-06 - -## 使用办法 (Usage) - -### 基本语法 - -```bash -./scripts/document/mkdocs_dev.sh [OPTIONS] -``` - -### 命令说明 - -| 命令 | 说明 | -|------|------| -| `serve` | 启动 MkDocs 开发服务器(默认命令) | -| `build` | 构建静态站点到 `out/docs/site/` | -| `api` | 运行 Doxygen + Doxybook2 API 文档管线 | -| `install` | 仅创建/更新虚拟环境和安装依赖 | -| `clean` | 清理构建产物(`out/docs/`、`__pycache__`、Doxygen XML) | -| `reset` | 删除并重建 `.venv` 虚拟环境 | -| `help` | 显示帮助信息 | - -### 参数说明 - -| 参数 | 说明 | -|------|------| -| `-p`, `--port PORT` | 开发服务器端口(默认: `8000`) | -| `-b`, `--bind ADDR` | 开发服务器绑定地址(默认: `127.0.0.1`) | -| `-v`, `--verbose` | 启用详细输出模式 | -| `-h`, `--help` | 显示帮助信息 | - -### 使用示例 - -```bash -# 启动开发服务器(默认 8000 端口) -./scripts/document/mkdocs_dev.sh serve - -# 自定义端口和绑定地址启动 -./scripts/document/mkdocs_dev.sh serve --port 3000 --bind 0.0.0.0 - -# 构建静态站点 -./scripts/document/mkdocs_dev.sh build - -# 生成 API 文档(需要 doxygen + doxybook2) -./scripts/document/mkdocs_dev.sh api - -# 仅安装依赖(不启动服务器) -./scripts/document/mkdocs_dev.sh install - -# 清理构建产物 -./scripts/document/mkdocs_dev.sh clean - -# 完全重置虚拟环境 -./scripts/document/mkdocs_dev.sh reset - -# 使用详细模式调试 -./scripts/document/mkdocs_dev.sh install --verbose - -# 查看帮助 -./scripts/document/mkdocs_dev.sh help -``` - -## Scripts详解 (Detailed Explanation) - -### 脚本用途 - -`mkdocs_dev.sh` 是 CFDesktop 项目的 MkDocs 文档开发环境管理脚本(Linux/macOS 版本)。它自动化了以下工作流: - -1. **Python 虚拟环境管理** — 检测、创建、激活 `.venv` -2. **依赖安装** — 基于 `pyproject.toml` 安装 MkDocs 及插件 -3. **开发服务器** — 一键启动 `mkdocs serve` 实时预览 -4. **静态站点构建** — 输出到 `out/docs/site/` -5. **API 文档生成** — Doxygen + Doxybook2 完整管线 -6. **环境维护** — 清理、重置等操作 - -### 脚本常量 - -| 常量 | 值 | 说明 | -|------|-----|------| -| `MIN_PYTHON_MAJOR` | `3` | 最低 Python 主版本号 | -| `MIN_PYTHON_MINOR` | `10` | 最低 Python 次版本号 | -| `VENV_DIR` | `.venv` | 虚拟环境目录名(位于项目根目录) | -| `DEPS_MARKER` | `.deps_installed` | 依赖安装标记文件(位于 `.venv/` 内) | -| `DOCS_OUTPUT_DIR` | `out/docs/site` | 静态站点输出路径 | -| `DOXYGEN_XML_DIR` | `xml` | Doxygen XML 输出目录 | -| `DOXYGEN_API_DIR` | `document/api` | API 文档 Markdown 输出目录 | - -### 环境要求 - -| 依赖项 | 版本要求 | 用途 | -|--------|----------|------| -| **Python 3** | >= 3.10 | 运行 MkDocs 及相关工具 | -| **python3-venv** | 与 Python 版本一致 | 创建虚拟环境 | -| **pip** | 随 Python 安装 | 包管理 | -| **doxygen** | 任意稳定版 | API 文档生成(仅 `api` 命令需要) | -| **doxybook2** | >= 1.5.0 | Doxygen XML → Markdown 转换(仅 `api` 命令需要) | - -### Python 安装指引 - -```bash -# Ubuntu / Debian -sudo apt install python3 python3-venv python3-pip - -# Fedora / RHEL -sudo dnf install python3 python3-pip - -# Arch Linux -sudo pacman -S python python-pip - -# macOS -brew install python@3.12 -``` - -### 依赖安装策略 - -脚本使用基于 hash 比对的智能安装策略: - -``` -检查 .venv/.deps_installed 标记文件 - ↓ -标记文件存在? - ├── 存在 → 读取标记中存储的 pyproject.toml 的 MD5 hash - │ 与当前 pyproject.toml 的 MD5 hash 比对 - │ ├── 一致 → 跳过安装(节省时间) - │ └── 不一致 → 执行 pip install 并更新标记 - └── 不存在 → 执行 pip install 并写入标记 -``` - -**优点:** - -- 首次运行自动安装所有依赖 -- 后续启动秒级完成(跳过安装) -- 修改 `pyproject.toml` 后自动触发依赖更新 -- `reset` 和 `install` 命令强制重装 - -### 脚本库依赖 - -脚本复用项目现有的 Bash 脚本库: - -| 库文件 | 路径 | 使用的函数 | -|--------|------|-----------| -| `lib_common.sh` | `scripts/lib/bash/lib_common.sh` | `log_info`, `log_success`, `log_warn`, `log_error`, `log_cyan` | - -### 各命令详细说明 - -#### `serve` — 开发服务器 - -启动 MkDocs 内置的开发服务器,支持文件修改后自动热重载。 - -```bash -./scripts/document/mkdocs_dev.sh serve -p 3000 -b 0.0.0.0 -# 访问 http://0.0.0.0:3000 预览文档 -``` - -**行为:** - -1. 检查 Python 版本 -2. 确保 `.venv` 存在并激活 -3. 检查并安装依赖 -4. 执行 `mkdocs serve --dev-addr=ADDR:PORT` - -**输出示例:** - -``` -=== MkDocs 开发服务器 === -地址: http://127.0.0.1:8000 -文档: /home/user/CFDesktop/document - -INFO - Building documentation... -INFO - Cleaning site directory -INFO - Documentation built in 2.35 seconds -INFO - [12:00:00] Watching paths for changes -``` - -#### `build` — 构建静态站点 - -构建完整的静态网站,输出到 `out/docs/site/` 目录。 - -```bash -./scripts/document/mkdocs_dev.sh build -# 输出到 out/docs/site/ -``` - -**行为:** - -1. 确保环境就绪 -2. 执行 `mkdocs build --clean -d out/docs/site` -3. `--clean` 参数确保每次构建都是全新输出 - -**构建产物结构:** - -``` -out/docs/site/ -├── index.html # 首页 -├── assets/ # 静态资源(CSS/JS/字体) -├── search/ # 搜索索引 -├── HandBook/ # 手册页面 -├── development/ # 开发文档页面 -├── design_stage/ # 设计文档页面 -└── ... # 其他文档页面 -``` - -#### `api` — API 文档管线 - -运行完整的 Doxygen → Doxybook2 → Markdown 管线,从 C++ 头文件生成 API 参考文档。 - -```bash -./scripts/document/mkdocs_dev.sh api -``` - -**管线流程:** - -``` -C++ 头文件 (*.h, *.hpp) - ↓ doxygen Doxyfile -XML 格式输出 → xml/ - ↓ 清理旧文档 -删除 document/api/ - ↓ doxybook2 -Markdown 格式 → document/api/ - ↓ 完成 -可通过 mkdocs serve 预览 -``` - -**前置条件:** - -```bash -# 安装 Doxygen -sudo apt install doxygen - -# 安装 Doxybook2(从 GitHub Releases 下载) -wget https://github.com/matusnovak/doxybook2/releases/download/v1.5.0/doxybook2-linux-amd64-v1.5.0.zip -unzip doxybook2-linux-amd64-v1.5.0.zip -sudo mv bin/doxybook2 /usr/local/bin/ -``` - -**使用的配置文件:** - -| 文件 | 用途 | -|------|------| -| `Doxyfile` | Doxygen 配置(输入源码目录、输出格式等) | -| `doxybook.json` | Doxybook2 配置(输出目录结构、Markdown 格式等) | - -#### `install` — 安装依赖 - -仅创建/更新虚拟环境,不启动任何服务。 - -```bash -./scripts/document/mkdocs_dev.sh install -./scripts/document/mkdocs_dev.sh install --verbose # 显示详细安装过程 -``` - -**适用场景:** - -- CI/CD 环境初始化 -- 首次克隆项目后设置文档开发环境 -- 依赖更新后刷新环境 - -#### `clean` — 清理构建产物 - -清理所有文档构建产物,但保留虚拟环境(`.venv`)。 - -```bash -./scripts/document/mkdocs_dev.sh clean -``` - -**清理范围:** - -| 清理目标 | 路径 | 说明 | -|----------|------|------| -| MkDocs 输出 | `out/docs/site/` | 静态站点构建产物 | -| Doxygen XML | `xml/` | Doxygen 生成的 XML 中间文件 | -| Python 缓存 | `__pycache__/` | Python 字节码缓存(递归清理) | - -**注意:** `.venv/` 虚拟环境不会被删除,如需完全重置请使用 `reset` 命令。 - -#### `reset` — 重置虚拟环境 - -完全删除并重建 Python 虚拟环境。 - -```bash -./scripts/document/mkdocs_dev.sh reset -``` - -**行为:** - -1. 删除 `.venv/` 目录 -2. 重新创建虚拟环境 -3. 安装所有依赖 - -**适用场景:** - -- Python 依赖出现兼容性问题 -- 虚拟环境损坏 -- 需要确保干净的环境状态 - -### 返回码 - -| 返回码 | 说明 | -|--------|------| -| 0 | 成功 | -| 1 | Python 未安装 / 版本过低 / 虚拟环境创建失败 / 外部工具缺失 | - -### 彩色日志输出 - -脚本使用 `lib_common.sh` 提供的统一日志函数,所有输出均带时间戳和颜色: - -``` -[2026-04-06 12:00:00] [INFO] 创建 Python 虚拟环境: /path/to/.venv (青色) -[2026-04-06 12:00:00] [SUCCESS] 虚拟环境创建成功 (绿色) -[2026-04-06 12:00:00] [WARNING] 依赖版本不匹配 (黄色) -[2026-04-06 12:00:00] [ERROR] Python 3 未安装 (红色) -``` - -### 注意事项 - -1. **Python 版本** — 要求 Python >= 3.10,脚本启动时会自动检测 -2. **虚拟环境位置** — `.venv/` 创建在项目根目录,已添加到 `.gitignore` -3. **幂等性** — 多次运行 `serve` 或 `install` 不会重复安装依赖 -4. **依赖变更检测** — 修改 `pyproject.toml` 后自动触发依赖更新 -5. **`api` 命令** — 需要额外安装 `doxygen` 和 `doxybook2`,脚本会检查并给出安装提示 -6. **工作目录** — 脚本通过 `SCRIPT_DIR` 自动定位项目根目录,可在任意目录执行 -7. **`set -eo pipefail`** — 启用严格错误处理,任何命令失败都会立即终止 - -### 完整输出示例 - -``` -$ ./scripts/document/mkdocs_dev.sh serve - -[2026-04-06 12:00:00] [INFO] 创建 Python 虚拟环境: /home/user/CFDesktop/.venv -[2026-04-06 12:00:03] [SUCCESS] 虚拟环境创建成功 -[2026-04-06 12:00:03] [INFO] 安装文档开发依赖... -[2026-04-06 12:00:20] [SUCCESS] 依赖安装完成 - -=== MkDocs 开发服务器 === -地址: http://127.0.0.1:8000 -文档: /home/user/CFDesktop/document - -INFO - Building documentation... -INFO - Documentation built in 2.35 seconds -INFO - [12:00:20] Serving on http://127.0.0.1:8000 -INFO - [12:00:20] Watching paths for changes -``` diff --git a/document/scripts/document/pyproject.toml.md b/document/scripts/document/pyproject.toml.md deleted file mode 100644 index 2fe295385..000000000 --- a/document/scripts/document/pyproject.toml.md +++ /dev/null @@ -1,134 +0,0 @@ -# pyproject.toml - -> 文档编写日期: 2026-04-06 - -## 使用办法 (Usage) - -### 文件位置 - -``` -scripts/document/pyproject.toml -``` - -### 安装依赖 - -```bash -# 方式 1: 通过开发脚本(推荐) -./scripts/document/mkdocs_dev.sh install - -# 方式 2: 手动安装(需要先激活 .venv) -source .venv/bin/activate -pip install -e scripts/document/ -``` - -```powershell -# Windows PowerShell -.\scripts\document\mkdocs_dev.ps1 install - -# 或手动安装 -. .venv\Scripts\Activate.ps1 -pip install -e scripts\document\ -``` - -## 详解 (Detailed Explanation) - -### 文件用途 - -`pyproject.toml` 是 Python 项目的现代配置文件标准(PEP 517/518),用于声明 CFDesktop 文档开发环境所需的 Python 依赖。`mkdocs_dev.sh` 和 `mkdocs_dev.ps1` 脚本通过 `pip install -e` 命令读取此文件来安装依赖。 - -### 完整内容 - -```toml -[project] -name = "cfdesktop-docs" -version = "0.1.0" -description = "CFDesktop documentation development environment" -requires-python = ">=3.10" -dependencies = [ - "mkdocs>=1.5.0", - "mkdocs-material>=9.5.0", - "mkdocs-awesome-pages-plugin>=2.9.0", - "mkdocs-git-revision-date-localized-plugin>=1.2.0", -] -``` - -### 字段说明 - -| 字段 | 值 | 说明 | -|------|-----|------| -| `name` | `cfdesktop-docs` | 项目名称,用于 pip 包标识 | -| `version` | `0.1.0` | 版本号 | -| `description` | `CFDesktop documentation ...` | 项目描述 | -| `requires-python` | `>=3.10` | 最低 Python 版本要求 | -| `dependencies` | (列表) | 运行时依赖列表 | - -### 依赖包详解 - -| 包名 | 最低版本 | 用途 | 项目中使用位置 | -|------|----------|------|---------------| -| `mkdocs` | >= 1.5.0 | MkDocs 静态站点生成器本体 | `mkdocs serve`, `mkdocs build` | -| `mkdocs-material` | >= 9.5.0 | Material Design 主题,提供现代化 UI | `mkdocs.yml` → `theme: material` | -| `mkdocs-awesome-pages-plugin` | >= 2.9.0 | 灵活的页面组织插件 | `mkdocs.yml` → `plugins: awesome-pages` | -| `mkdocs-git-revision-date-localized-plugin` | >= 1.2.0 | 自动显示文章创建和更新时间 | `mkdocs.yml` → `plugins: git-revision-date-localized` | - -### 与 CI 环境的关系 - -CI 环境 (`.github/workflows/deploy.yml`) 直接使用 `pip install` 安装相同的包: - -```yaml -# CI 中的安装方式 -- name: 安装依赖 - run: | - pip install mkdocs-material - pip install mkdocs-awesome-pages-plugin - pip install mkdocs-git-revision-date-localized-plugin -``` - -本地开发环境通过 `pyproject.toml` 统一管理版本约束,确保 CI 和本地依赖一致。 - -### 版本约束说明 - -使用 `>=` 约束而非固定版本(`==`),原因: - -1. **兼容性** — 允许在不修改配置的情况下获取 bugfix 更新 -2. **安全性** — 允许获取安全补丁 -3. **CI 稳定性** — CI 环境总是安装最新兼容版本 - -如需固定版本,可创建 `requirements.lock` 文件(当前未使用)。 - -### 修改此文件 - -当需要添加或升级依赖时: - -1. 编辑 `scripts/document/pyproject.toml` 中的 `dependencies` 列表 -2. 运行以下命令使变更生效: - -```bash -# 方式 1: 通过脚本自动检测变更 -./scripts/document/mkdocs_dev.sh install - -# 方式 2: 强制重装 -./scripts/document/mkdocs_dev.sh reset -``` - -**自动检测机制:** 开发脚本会比较 `pyproject.toml` 的 MD5 hash 与 `.venv/.deps_installed` 中存储的 hash,如不一致则自动触发依赖更新。 - -### 与其他配置文件的关系 - -``` -pyproject.toml 声明 Python 依赖 - ↓ -mkdocs_dev.sh / mkdocs_dev.ps1 读取 pyproject.toml 安装依赖 - ↓ -mkdocs.yml 使用已安装的包配置站点 - ↓ -document/ 文档源文件 -``` - -### 注意事项 - -1. **版本约束** — 修改版本约束时注意与 CI 环境的兼容性 -2. **依赖数量** — 当前仅包含 4 个直接依赖,MkDocs Material 会自动拉取其子依赖 -3. **Python 版本** — `requires-python = ">=3.10"` 确保使用现代 Python 特性 -4. **文件格式** — 遵循 TOML 规范,编辑时注意语法正确性 -5. **可编辑安装** — 脚本使用 `pip install -e` 安装,修改 `pyproject.toml` 后可通过 hash 比对自动检测 diff --git a/document/scripts/doxygen/.pages b/document/scripts/doxygen/.pages deleted file mode 100644 index 1575e5098..000000000 --- a/document/scripts/doxygen/.pages +++ /dev/null @@ -1,3 +0,0 @@ -title: Doxygen -nav: - - Lint 工具: lint.py.md diff --git a/document/scripts/doxygen/index.md b/document/scripts/doxygen/index.md index 45ac5450e..751edeb1a 100644 --- a/document/scripts/doxygen/index.md +++ b/document/scripts/doxygen/index.md @@ -1,10 +1,11 @@ -# doxygen - -> Welcome to the doxygen section. +--- +title: Doxygen 脚本 +description: 本目录包含基于 Doxygen 的文档生成脚本与注释风格检查工具,自动从源码中提取 API 文档并验 +--- -## Overview +# Doxygen 脚本 -Documentation and resources for doxygen. +本目录包含基于 Doxygen 的文档生成脚本与注释风格检查工具,自动从源码中提取 API 文档并验证注释是否符合项目 Doxygen 规范(`DOXYGEN_REQUEST.md`),确保文档与代码保持同步。 --- diff --git a/document/scripts/doxygen/lint.py.md b/document/scripts/doxygen/lint.py.md index 88b95f01d..6ebe5070d 100644 --- a/document/scripts/doxygen/lint.py.md +++ b/document/scripts/doxygen/lint.py.md @@ -1,3 +1,8 @@ +--- +title: lint.py +description: "文档编写日期: 2026-03-20,- 递归扫描指定目录(默认为项目根目录)" +--- + # lint.py > 文档编写日期: 2026-03-20 @@ -7,12 +12,12 @@ ### 基本语法 ```bash python3 scripts/doxygen/lint.py -``` +```text ### 指定目录检查 ```bash python3 scripts/doxygen/lint.py /path/to/directory -``` +```text ### 工作模式 - 递归扫描指定目录(默认为项目根目录) @@ -60,17 +65,17 @@ python3 scripts/doxygen/lint.py /path/to/directory ### 输出 #### 成功输出 -``` +```text All Doxygen checks passed -``` +```text - 返回码: `0` - 删除已存在的 `FAILED_DOXYGEN.md` 文件 #### 失败输出 -``` +```text FAILED: N violations See: /path/to/FAILED_DOXYGEN.md -``` +```bash - 返回码: `1` - 生成 `FAILED_DOXYGEN.md` 详细报告,包含: - 违规总数 @@ -95,7 +100,7 @@ See: /path/to/FAILED_DOXYGEN.md ```bash # .git/hooks/pre-commit python3 scripts/doxygen/lint.py || exit 1 -``` +```text #### CMake集成 ```cmake @@ -104,7 +109,7 @@ add_custom_target(doxygen_lint WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMENT "Checking Doxygen compliance..." ) -``` +```text ### 注意事项 - 仅检查 `.h` 和 `.hpp` 头文件 diff --git a/document/scripts/index.md b/document/scripts/index.md index 939c8769d..68facdca5 100644 --- a/document/scripts/index.md +++ b/document/scripts/index.md @@ -1,10 +1,11 @@ -# Scripts - -> Welcome to the Scripts section. +--- +title: 脚本工具 +description: 本目录包含 CFDesktop 项目全部脚本工具的文档,涵盖构建辅助脚本(Linux Bash / +--- -## Overview +# 脚本工具 -Documentation and resources for Scripts. +本目录包含 CFDesktop 项目全部脚本工具的文档,涵盖构建辅助脚本(Linux Bash / Windows PowerShell)、Docker 构建与部署脚本、第三方依赖管理脚本、开发工作流辅助工具、Doxygen 文档生成脚本以及正式发布流程脚本。 --- diff --git a/document/scripts/lib/.pages b/document/scripts/lib/.pages deleted file mode 100644 index bc8d4a04a..000000000 --- a/document/scripts/lib/.pages +++ /dev/null @@ -1,4 +0,0 @@ -title: 脚本库 -nav: - - Bash 库: bash - - PowerShell 库: powershell diff --git a/document/scripts/lib/bash/.pages b/document/scripts/lib/bash/.pages deleted file mode 100644 index f6d3c90f5..000000000 --- a/document/scripts/lib/bash/.pages +++ /dev/null @@ -1,8 +0,0 @@ -title: Bash 库 -nav: - - 参数库: lib_args.sh.md - - 构建库: lib_build.sh.md - - 通用库: lib_common.sh.md - - 配置库: lib_config.sh.md - - Git 库: lib_git.sh.md - - 路径库: lib_paths.sh.md diff --git a/document/scripts/lib/bash/README.md b/document/scripts/lib/bash/README.md index 17d8e6e2c..e7051884a 100644 --- a/document/scripts/lib/bash/README.md +++ b/document/scripts/lib/bash/README.md @@ -1,3 +1,8 @@ +--- +title: Bash库文档 +description: "文档编写日期: 2026-03-20,本目录包含CFDesktop构建系统使用的Bash库文件。" +--- + # Bash库文档 > 文档编写日期: 2026-03-20 @@ -28,15 +33,15 @@ 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(用于日志输出) @@ -59,7 +64,7 @@ log_warn "警告消息" log_error "错误消息" log_separator log_progress 5 10 "处理中" -``` +```text ### 配置解析 (lib_config.sh) @@ -69,7 +74,7 @@ echo "$config_cmake_generator" value=$(get_ini_value config.ini "cmake" "generator") has_ini_value config.ini "cmake" "generator" && echo "存在" -``` +```text ### 构建操作 (lib_build.sh) @@ -77,4 +82,4 @@ has_ini_value config.ini "cmake" "generator" && echo "存在" 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 4c27d38b1..22d953dd0 100644 --- a/document/scripts/lib/bash/index.md +++ b/document/scripts/lib/bash/index.md @@ -1,10 +1,11 @@ -# bash - -> Welcome to the bash section. +--- +title: Bash 脚本库 +description: 本目录包含 Bash 共享工具函数,为 Linux 平台的构建、测试与发布脚本提供通用功能支持,包括 +--- -## Overview +# Bash 脚本库 -Documentation and resources for bash. +本目录包含 Bash 共享工具函数,为 Linux 平台的构建、测试与发布脚本提供通用功能支持,包括彩色日志输出、路径工具、环境变量检测与 CMake 封装调用等。 --- diff --git a/document/scripts/lib/bash/lib_args.sh.md b/document/scripts/lib/bash/lib_args.sh.md index cca9d800f..0942cd3a1 100644 --- a/document/scripts/lib/bash/lib_args.sh.md +++ b/document/scripts/lib/bash/lib_args.sh.md @@ -1,3 +1,8 @@ +--- +title: libargs.sh +description: "文档编写日期: 2026-03-20,提供命令行参数解析的辅助函数,包括:" +--- + # lib_args.sh > 文档编写日期: 2026-03-20 @@ -7,7 +12,7 @@ ### 加载方式 ```bash source scripts/lib/bash/lib_args.sh -``` +```text ### 基本用法示例 ```bash @@ -31,7 +36,7 @@ while [[ $# -gt 0 ]]; do ;; esac done -``` +```bash ## Scripts详解 @@ -64,7 +69,7 @@ done ```bash parse_config_mode "develop" # 输出: develop, 返回: 0 parse_config_mode "invalid" # 无输出, 返回: 1 -``` +```text #### is_valid_config_mode 检查给定字符串是否为有效的配置模式。 @@ -72,7 +77,7 @@ parse_config_mode "invalid" # 无输出, 返回: 1 if is_valid_config_mode "$1"; then CONFIG="$1" fi -``` +```text #### parse_config_file 解析配置文件参数,仅对 `-c` 或 `--config` 有效。 @@ -80,7 +85,7 @@ fi parse_config_file "-c" "config.ini" # 输出: config.ini, 返回: 0 parse_config_file "--config" "a.conf" # 输出: a.conf, 返回: 0 parse_config_file "--verbose" "on" # 无输出, 返回: 1 -``` +```text #### show_standard_usage 显示一行标准用法信息。 @@ -90,16 +95,16 @@ show_standard_usage show_standard_usage "my_build.sh" # 输出: Usage: my_build.sh [develop|deploy|ci] [-c|--config ] -``` +```text #### show_detailed_usage 显示格式化的详细帮助信息。 ```bash show_detailed_usage "build.sh" "这是一个构建工具脚本" -``` +```text 输出示例: -``` +```yaml ======================================== build.sh ======================================== @@ -118,7 +123,7 @@ Options: Examples: build.sh develop build.sh deploy -c custom_config.ini -``` +```text #### validate_arg_count 验证参数数量是否满足最小要求。 @@ -127,7 +132,7 @@ if ! validate_arg_count "$#" 2; then echo "错误: 参数不足" exit 1 fi -``` +```text #### is_help_arg 检查参数是否为帮助请求。 @@ -136,7 +141,7 @@ if is_help_arg "$1"; then show_detailed_usage "$(basename "$0")" exit 0 fi -``` +```text 支持的格式:`-h`, `--help`, `help` @@ -147,7 +152,7 @@ show_unknown_arg_error "--invalid" "build.sh" # 输出到 stderr: # ERROR: Unknown argument '--invalid' # Run 'build.sh --help' for usage information. -``` +```text #### show_missing_value_error 显示参数值缺失的错误信息。 @@ -155,7 +160,7 @@ show_unknown_arg_error "--invalid" "build.sh" show_missing_value_error "--config" # 输出到 stderr: # ERROR: Missing value for argument '--config' -``` +```text ### 依赖关系 - Bash 内置命令 diff --git a/document/scripts/lib/bash/lib_build.sh.md b/document/scripts/lib/bash/lib_build.sh.md index 2a4741890..890d59ba6 100644 --- a/document/scripts/lib/bash/lib_build.sh.md +++ b/document/scripts/lib/bash/lib_build.sh.md @@ -1,3 +1,8 @@ +--- +title: libbuild.sh +description: "文档编写日期: 2026-03-20,注意: 此模块依赖 ,会自动加载。" +--- + # lib_build.sh > 文档编写日期: 2026-03-20 @@ -7,7 +12,7 @@ ### 加载方式 ```bash source scripts/lib/bash/lib_build.sh -``` +```bash **注意:** 此模块依赖 `lib_common.sh`,会自动加载。 @@ -52,7 +57,7 @@ source scripts/lib/bash/lib_build.sh **示例:** ```bash clean_build_dir "$BUILD_DIR" -``` +```text ### ensure_build_dir(build_path) @@ -64,7 +69,7 @@ clean_build_dir "$BUILD_DIR" **示例:** ```bash ensure_build_dir "$BUILD_DIR" -``` +```text ### run_cmake_configure(generator, build_type, source_dir, build_dir, [extra_args]) @@ -85,7 +90,7 @@ ensure_build_dir "$BUILD_DIR" ```bash run_cmake_configure "Ninja" "Release" "$SOURCE_DIR" "$BUILD_DIR" run_cmake_configure "Unix Makefiles" "Debug" "$SOURCE_DIR" "$BUILD_DIR" "-DENABLE_TESTS=ON" -``` +```text ### run_cmake_build(build_dir, [target], [jobs]) @@ -104,7 +109,7 @@ run_cmake_configure "Unix Makefiles" "Debug" "$SOURCE_DIR" "$BUILD_DIR" "-DENABL ```bash run_cmake_build "$BUILD_DIR" run_cmake_build "$BUILD_DIR" "mytarget" 4 -``` +```text ### has_cmake_cache(build_dir) @@ -122,7 +127,7 @@ run_cmake_build "$BUILD_DIR" "mytarget" 4 if has_cmake_cache "$BUILD_DIR"; then log_info "CMake 已配置,跳过配置步骤" fi -``` +```text ### get_cmake_cache_var(build_dir, var_name) @@ -138,7 +143,7 @@ fi **示例:** ```bash generator=$(get_cmake_cache_var "$BUILD_DIR" "CMAKE_GENERATOR") -``` +```text ### get_parallel_job_count() @@ -151,7 +156,7 @@ generator=$(get_cmake_cache_var "$BUILD_DIR" "CMAKE_GENERATOR") ```bash jobs=$(get_parallel_job_count) run_cmake_build "$BUILD_DIR" "--all" "$jobs" -``` +```text ### build_timer_start() @@ -160,7 +165,7 @@ run_cmake_build "$BUILD_DIR" "--all" "$jobs" **示例:** ```bash build_timer_start -``` +```text ### build_timer_end() @@ -172,7 +177,7 @@ build_timer_start # ... 执行构建 ... build_timer_end # 输出: Build time: 2m 15s -``` +```yaml --- @@ -203,4 +208,4 @@ run_cmake_build "$BUILD_DIR" "--all" "$jobs" # 构建计时结束 build_timer_end -``` +```text diff --git a/document/scripts/lib/bash/lib_common.sh.md b/document/scripts/lib/bash/lib_common.sh.md index 75f9fde61..c3836628f 100644 --- a/document/scripts/lib/bash/lib_common.sh.md +++ b/document/scripts/lib/bash/lib_common.sh.md @@ -1,3 +1,8 @@ +--- +title: libcommon.sh +description: libcommon.sh 的详细文档 +--- + # lib_common.sh > 文档编写日期: 2026-03-20 @@ -7,7 +12,7 @@ ### 加载方式 ```bash source scripts/lib/bash/lib_common.sh -``` +```bash ## Scripts详解 @@ -54,7 +59,7 @@ log "构建开始" "INFO" log "操作成功" "SUCCESS" log "警告信息" "WARNING" log "发生错误" "ERROR" -``` +```text ### log_info(message) @@ -66,7 +71,7 @@ log "发生错误" "ERROR" **示例:** ```bash log_info "正在处理文件..." -``` +```text ### log_success(message) @@ -78,7 +83,7 @@ log_info "正在处理文件..." **示例:** ```bash log_success "构建完成!" -``` +```text ### log_warn(message) @@ -90,7 +95,7 @@ log_success "构建完成!" **示例:** ```bash log_warn "配置文件不存在,使用默认值" -``` +```text ### log_error(message) @@ -102,7 +107,7 @@ log_warn "配置文件不存在,使用默认值" **示例:** ```bash log_error "构建失败,请检查日志" -``` +```text ### log_cyan(message) @@ -114,7 +119,7 @@ log_error "构建失败,请检查日志" **示例:** ```bash log_cyan "这是重要提示" -``` +```text ### log_separator(char, width) @@ -128,7 +133,7 @@ log_cyan "这是重要提示" ```bash log_separator # 输出 40 个 = log_separator "-" 60 # 输出 60 个 - -``` +```text ### log_debug(message) @@ -140,7 +145,7 @@ log_separator "-" 60 # 输出 60 个 - **示例:** ```bash DEBUG=true log_debug "调试信息" -``` +```text ### log_progress(current, total, message) @@ -155,7 +160,7 @@ DEBUG=true log_debug "调试信息" ```bash log_progress 5 10 "处理文件中" # 输出: [5/10] (50%) 处理文件中 -``` +```yaml --- @@ -178,4 +183,4 @@ done log_success "所有文件处理完成!" log_separator -``` +```text diff --git a/document/scripts/lib/bash/lib_config.sh.md b/document/scripts/lib/bash/lib_config.sh.md index 58ef1c961..152a1c5c5 100644 --- a/document/scripts/lib/bash/lib_config.sh.md +++ b/document/scripts/lib/bash/lib_config.sh.md @@ -1,3 +1,8 @@ +--- +title: libconfig.sh +description: libconfig.sh 的详细文档 +--- + # lib_config.sh > 文档编写日期: 2026-03-20 @@ -7,7 +12,7 @@ ### 加载方式 ```bash source scripts/lib/bash/lib_config.sh -``` +```bash ## Scripts详解 @@ -58,14 +63,14 @@ build_type = Release [paths] build_dir = build output_dir = out -``` +```text 使用方式: ```bash eval "$(get_ini_config config.ini)" echo "$config_cmake_generator" # 输出: Ninja echo "$config_paths_build_dir" # 输出: build -``` +```text ### get_ini_value(filepath, section, key) @@ -83,7 +88,7 @@ echo "$config_paths_build_dir" # 输出: build ```bash value=$(get_ini_value "config.ini" "cmake" "generator") echo "$value" # 输出: Ninja -``` +```text ### has_ini_value(filepath, section, key) @@ -103,7 +108,7 @@ echo "$value" # 输出: Ninja if has_ini_value "config.ini" "cmake" "generator"; then log_info "Generator 已配置" fi -``` +```bash ### get_default_config_file(mode) @@ -126,7 +131,7 @@ fi ```bash config_file=$(get_default_config_file "develop") eval "$(get_ini_config "$config_file")" -``` +```yaml --- @@ -156,7 +161,7 @@ if has_ini_value "$CONFIG_FILE" "cmake" "generator"; then else echo "使用默认生成器" fi -``` +```text ### 条件配置示例 @@ -177,7 +182,7 @@ BUILD_TYPE="$config_cmake_build_type" log_info "配置模式: $MODE" log_info "构建目录: $BUILD_DIR" log_info "构建类型: $BUILD_TYPE" -``` +```text ### INI 配置文件示例 @@ -198,4 +203,4 @@ install_dir = /usr/local parallel = true jobs = 8 verbose = false -``` +```text diff --git a/document/scripts/lib/bash/lib_git.sh.md b/document/scripts/lib/bash/lib_git.sh.md index ec9b02a32..ab1253b1b 100644 --- a/document/scripts/lib/bash/lib_git.sh.md +++ b/document/scripts/lib/bash/lib_git.sh.md @@ -1,3 +1,8 @@ +--- +title: libgit.sh +description: "文档编写日期: 2026-03-20,提供 Git 相关的辅助函数,主要包括:" +--- + # lib_git.sh > 文档编写日期: 2026-03-20 @@ -7,7 +12,7 @@ ### 加载方式 ```bash source scripts/lib/bash/lib_git.sh -``` +```bash ## Scripts详解 @@ -53,7 +58,7 @@ echo $? # 输出: 1 (第一个版本大于第二个版本) compare_versions "1.0.0" "1.0.0" echo $? # 输出: 0 (版本相等) -``` +```text #### determine_verify_level 根据本地版本和远程版本的差异,确定需要进行的验证级别。 @@ -61,7 +66,7 @@ echo $? # 输出: 0 (版本相等) determine_verify_level "1.2.3" "1.3.0" # 输出: minor determine_verify_level "1.2.3" "2.0.0" # 输出: major determine_verify_level "1.2.3" "1.2.4" # 输出: patch -``` +```text 验证级别含义: - **major**: X64 + ARM64 完整构建 + 测试 @@ -72,7 +77,7 @@ determine_verify_level "1.2.3" "1.2.4" # 输出: patch 从项目根目录的 CMakeLists.txt 中提取版本号。 ```bash get_cmake_version "/path/to/project" # 输出: 1.2.3 -``` +```text ### 依赖关系 - Git 命令行工具(git) diff --git a/document/scripts/lib/bash/lib_paths.sh.md b/document/scripts/lib/bash/lib_paths.sh.md index 4e081941f..67d93bb83 100644 --- a/document/scripts/lib/bash/lib_paths.sh.md +++ b/document/scripts/lib/bash/lib_paths.sh.md @@ -1,3 +1,8 @@ +--- +title: libpaths.sh +description: "文档编写日期: 2026-03-20,加载后可使用以下环境变量或调用对应的 getter 函数:" +--- + # lib_paths.sh > 文档编写日期: 2026-03-20 @@ -9,7 +14,7 @@ # 推荐方式:在脚本中先设置 SCRIPT_DIR,然后加载 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../lib/bash/lib_paths.sh" -``` +```bash 加载后可使用以下环境变量或调用对应的 getter 函数: - `$PROJECT_ROOT` 或 `get_project_root()` - 项目根目录 @@ -46,7 +51,7 @@ source "$SCRIPT_DIR/../lib/bash/lib_paths.sh" ### 目录结构假设 -``` +```text PROJECT_ROOT/ ├── scripts/ │ ├── lib/ @@ -55,7 +60,7 @@ PROJECT_ROOT/ │ └── build_helpers/ │ └── your_script.sh └── ... -``` +```text ### 核心函数详解 @@ -65,13 +70,13 @@ PROJECT_ROOT/ if path_exists "/some/path"; then echo "路径存在" fi -``` +```text #### ensure_dir 确保目录存在,如果不存在则创建(包括父目录)。 ```bash ensure_dir "$PROJECT_ROOT/build/output" -``` +```text ### 依赖关系 - Bash 内置命令 diff --git a/document/scripts/lib/index.md b/document/scripts/lib/index.md index b56eac42e..9d941a0a5 100644 --- a/document/scripts/lib/index.md +++ b/document/scripts/lib/index.md @@ -1,10 +1,11 @@ -# lib - -> Welcome to the lib section. +--- +title: 脚本库 +description: 本目录包含构建与发布脚本共用的工具函数库,按语言分为 Bash 和 PowerShell 两个子目录 +--- -## Overview +# 脚本库 -Documentation and resources for lib. +本目录包含构建与发布脚本共用的工具函数库,按语言分为 Bash 和 PowerShell 两个子目录。这些共享函数涵盖路径处理、日志输出、环境检测等通用逻辑,避免各脚本间的代码重复。 --- diff --git a/document/scripts/lib/powershell/.pages b/document/scripts/lib/powershell/.pages deleted file mode 100644 index 19cd633b9..000000000 --- a/document/scripts/lib/powershell/.pages +++ /dev/null @@ -1,8 +0,0 @@ -title: PowerShell 库 -nav: - - LibArgs: LibArgs.psm1.md - - LibBuild: LibBuild.psm1.md - - LibCommon: LibCommon.psm1.md - - LibConfig: LibConfig.psm1.md - - LibGit: LibGit.psm1.md - - LibPaths: LibPaths.psm1.md diff --git a/document/scripts/lib/powershell/LibArgs.psm1.md b/document/scripts/lib/powershell/LibArgs.psm1.md index 7b9dca8c8..bd51ebf9a 100644 --- a/document/scripts/lib/powershell/LibArgs.psm1.md +++ b/document/scripts/lib/powershell/LibArgs.psm1.md @@ -1,3 +1,8 @@ +--- +title: LibArgs.psm1 +description: "文档编写日期: 2026-03-20,LibArgs.psm1 提供命令行参数解析和用户帮助信息显示" +--- + # LibArgs.psm1 > 文档编写日期: 2026-03-20 @@ -7,7 +12,7 @@ ### 加载方式 ```powershell Import-Module scripts/lib/powershell/LibArgs.psm1 -``` +```bash ## Scripts详解 @@ -31,7 +36,7 @@ LibArgs.psm1 提供命令行参数解析和用户帮助信息显示功能。该 #### Parse-ConfigMode ```powershell Parse-ConfigMode [-Mode] -``` +```text 解析并验证配置模式参数。 **有效模式:** @@ -46,12 +51,12 @@ Parse-ConfigMode [-Mode] ```powershell $mode = Parse-ConfigMode "develop" # 返回 "develop" $mode = Parse-ConfigMode "invalid" # 返回 $null -``` +```text #### Show-DetailedUsage ```powershell Show-DetailedUsage [[-ScriptName] ] [[-Description] ] -``` +```text 显示格式化的详细帮助信息,包括: - 脚本名称(带边框) - 脚本描述(如果提供) @@ -64,7 +69,7 @@ Show-DetailedUsage [[-ScriptName] ] [[-Description] ] - `Description`: 脚本描述(可选) **输出示例:** -``` +```yaml ======================================== build.ps1 ======================================== @@ -83,12 +88,12 @@ Options: Examples: .\build.ps1 develop .\build.ps1 deploy -c custom_config.ini -``` +```text #### Test-HelpArg ```powershell Test-HelpArg [-Arg] -``` +```text 检查参数是否为帮助请求。 **识别的帮助参数:** @@ -101,15 +106,15 @@ if (Test-HelpArg $args[0]) { Show-DetailedUsage exit } -``` +```text ### 标准命令行接口 此模块定义的命令行接口规范: -``` +```text script.ps1 [develop|deploy|ci] [-c|--config ] [-h|--help] -``` +```cmake **参数说明:** | 位置参数 | 说明 | diff --git a/document/scripts/lib/powershell/LibBuild.psm1.md b/document/scripts/lib/powershell/LibBuild.psm1.md index 76b67f6cf..e2becf750 100644 --- a/document/scripts/lib/powershell/LibBuild.psm1.md +++ b/document/scripts/lib/powershell/LibBuild.psm1.md @@ -1,3 +1,8 @@ +--- +title: LibBuild.psm1 +description: "文档编写日期: 2026-03-20,描述: 确保构建目录存在,不存在则创建" +--- + # LibBuild.psm1 > 文档编写日期: 2026-03-20 @@ -10,14 +15,14 @@ # LibBuild 依赖 LibCommon,需先加载 Import-Module scripts/lib/powershell/LibCommon.psm1 Import-Module scripts/lib/powershell/LibBuild.psm1 -``` +```text 或者: ```powershell . "$PSScriptRoot\LibCommon.psm1" . "$PSScriptRoot\LibBuild.psm1" -``` +```bash ## Scripts详解 @@ -56,7 +61,7 @@ Import-Module scripts/lib/powershell/LibBuild.psm1 **示例**: ```powershell Clean-BuildDir "C:\Build\Output" -``` +```yaml --- @@ -72,7 +77,7 @@ Clean-BuildDir "C:\Build\Output" **示例**: ```powershell Ensure-BuildDir "C:\Build\Output" -``` +```yaml --- @@ -92,12 +97,12 @@ Ensure-BuildDir "C:\Build\Output" **示例**: ```powershell Invoke-CMakeConfigure -Generator "Ninja" -BuildType "Release" -SourceDir "." -BuildDir "build" -``` +```text 带额外参数: ```powershell Invoke-CMakeConfigure -Generator "Ninja" -BuildType "Release" -SourceDir "." -BuildDir "build" -ExtraArgs @("-DCMAKE_EXPORT_COMPILE_COMMANDS=ON") -``` +```yaml --- @@ -119,7 +124,7 @@ Invoke-CMakeBuild -BuildDir "build" # 构建特定目标,指定并行数 Invoke-CMakeBuild -BuildDir "build" -Target "myapp" -Parallel 4 -``` +```yaml --- @@ -137,7 +142,7 @@ Invoke-CMakeBuild -BuildDir "build" -Target "myapp" -Parallel 4 if (Test-CmakeCache "build") { Write-LogInfo "CMake cache exists" } -``` +```yaml --- @@ -155,7 +160,7 @@ if (Test-CmakeCache "build") { ```powershell $generator = Get-CmakeCacheVar -BuildDir "build" -VarName "CMAKE_GENERATOR" Write-LogInfo "Generator: $generator" -``` +```yaml --- @@ -171,7 +176,7 @@ Write-LogInfo "Generator: $generator" ```powershell $jobs = Get-ParallelJobCount Write-LogInfo "Using $jobs parallel jobs" -``` +```yaml --- @@ -188,7 +193,7 @@ Write-LogInfo "Using $jobs parallel jobs" Start-BuildTimer # ... 执行构建 ... Stop-BuildTimer -``` +```yaml --- @@ -206,7 +211,7 @@ Start-BuildTimer # ... 执行构建 ... Stop-BuildTimer # 输出示例: [2026-03-20 10:30:45] [INFO] Build time: 2m 15s -``` +```yaml --- diff --git a/document/scripts/lib/powershell/LibCommon.psm1.md b/document/scripts/lib/powershell/LibCommon.psm1.md index 65691e847..f824b1fa2 100644 --- a/document/scripts/lib/powershell/LibCommon.psm1.md +++ b/document/scripts/lib/powershell/LibCommon.psm1.md @@ -1,3 +1,8 @@ +--- +title: LibCommon.psm1 +description: "文档编写日期: 2026-03-20,描述: 写入 INFO 级别日志(青色显示)" +--- + # LibCommon.psm1 > 文档编写日期: 2026-03-20 @@ -8,13 +13,13 @@ ```powershell Import-Module scripts/lib/powershell/LibCommon.psm1 -``` +```text 或者: ```powershell . "$PSScriptRoot\LibCommon.psm1" -``` +```bash ## Scripts详解 @@ -45,7 +50,7 @@ Import-Module scripts/lib/powershell/LibCommon.psm1 **示例**: ```powershell Write-Log -Message "Build completed" -Level "SUCCESS" -``` +```yaml --- @@ -62,7 +67,7 @@ Write-Log -Message "Build completed" -Level "SUCCESS" ```powershell Write-LogInfo "Starting build process" Write-LogInfo "Processing" "file" "1.txt" -``` +```yaml --- @@ -78,7 +83,7 @@ Write-LogInfo "Processing" "file" "1.txt" **示例**: ```powershell Write-LogSuccess "Build completed successfully" -``` +```yaml --- @@ -94,7 +99,7 @@ Write-LogSuccess "Build completed successfully" **示例**: ```powershell Write-LogWarning "Configuration file not found, using defaults" -``` +```yaml --- @@ -110,7 +115,7 @@ Write-LogWarning "Configuration file not found, using defaults" **示例**: ```powershell Write-LogError "Build failed with exit code: 1" -``` +```yaml --- @@ -128,7 +133,7 @@ Write-LogError "Build failed with exit code: 1" ```powershell Write-LogSeparator Write-LogSeparator -Char "-" -Width 60 -``` +```yaml --- @@ -147,7 +152,7 @@ Write-LogSeparator -Char "-" -Width 60 ```powershell Write-LogProgress -Current 5 -Total 10 -Message "Processing files" # 输出: [5/10] (50%) Processing files -``` +```bash --- diff --git a/document/scripts/lib/powershell/LibConfig.psm1.md b/document/scripts/lib/powershell/LibConfig.psm1.md index ac60c1f1f..5eec12d04 100644 --- a/document/scripts/lib/powershell/LibConfig.psm1.md +++ b/document/scripts/lib/powershell/LibConfig.psm1.md @@ -1,3 +1,8 @@ +--- +title: LibConfig.psm1 +description: "文档编写日期: 2026-03-20,描述: 获取特定的配置值" +--- + # LibConfig.psm1 > 文档编写日期: 2026-03-20 @@ -8,13 +13,13 @@ ```powershell Import-Module scripts/lib/powershell/LibConfig.psm1 -``` +```text 或者: ```powershell . "$PSScriptRoot\LibConfig.psm1" -``` +```bash ## Scripts详解 @@ -56,7 +61,7 @@ Import-Module scripts/lib/powershell/LibConfig.psm1 $config = Get-IniConfig -FilePath "build_config.ini" $buildType = $config["cmake"]["build_type"] Write-LogInfo "Build type: $buildType" -``` +```yaml --- @@ -77,7 +82,7 @@ $generator = Get-IniValue -FilePath "config.ini" -Section "cmake" -Key "generato if (-not [string]::IsNullOrEmpty($generator)) { Write-LogInfo "Generator: $generator" } -``` +```yaml --- @@ -99,7 +104,7 @@ if (Test-IniValue -FilePath "config.ini" -Section "cmake" -Key "generator") { } else { Write-LogWarning "Generator is not defined" } -``` +```bash --- @@ -130,7 +135,7 @@ $configFile = Get-DefaultConfigFile -Mode "deploy" # CI 模式配置,指定脚本目录 $configFile = Get-DefaultConfigFile -Mode "ci" -ScriptDir "C:\Scripts" -``` +```yaml --- @@ -157,4 +162,4 @@ $installDir = Get-IniValue -FilePath $configFile -Section "paths" -Key "install_ if (Test-IniValue -FilePath $configFile -Section "cmake" -Key "generator") { Write-LogSuccess "CMake generator is configured" } -``` +```text diff --git a/document/scripts/lib/powershell/LibGit.psm1.md b/document/scripts/lib/powershell/LibGit.psm1.md index 5d9d94fb1..e735233d6 100644 --- a/document/scripts/lib/powershell/LibGit.psm1.md +++ b/document/scripts/lib/powershell/LibGit.psm1.md @@ -1,3 +1,8 @@ +--- +title: LibGit.psm1 +description: "文档编写日期: 2026-03-20,LibGit.psm1 是一个 Git 辅助函数库,提供版本号" +--- + # LibGit.psm1 > 文档编写日期: 2026-03-20 @@ -7,7 +12,7 @@ ### 加载方式 ```powershell Import-Module scripts/lib/powershell/LibGit.psm1 -``` +```bash ## Scripts详解 @@ -39,7 +44,7 @@ LibGit.psm1 是一个 Git 辅助函数库,提供版本号解析、Git 仓库 #### Determine-VerifyLevel ```powershell Determine-VerifyLevel [-LocalVersion] [-RemoteVersion] -``` +```text 根据本地版本和远程版本的差异,确定需要执行的验证级别: - **major**: 主版本号不同(1.x.x vs 2.x.x),需要 X64 + ARM64 完整构建 + 测试 @@ -49,7 +54,7 @@ Determine-VerifyLevel [-LocalVersion] [-RemoteVersion] #### Compare-Versions ```powershell Compare-Versions -Version1 -Version2 -``` +```text 比较两个语义化版本号的大小: - 返回 `-1`: Version1 < Version2 @@ -62,7 +67,7 @@ Compare-Versions -Version1 -Version2 ```powershell Get-LocalVersion Get-RemoteVersion -``` +```text - `Get-LocalVersion`: 获取当前分支最近的 Git 标签,若无标签返回 `"0.0.0"` - `Get-RemoteVersion`: 自动执行 `git fetch` 获取最新远程信息,然后返回远程 main 分支的最新标签 diff --git a/document/scripts/lib/powershell/LibPaths.psm1.md b/document/scripts/lib/powershell/LibPaths.psm1.md index 93a78429e..90c048e44 100644 --- a/document/scripts/lib/powershell/LibPaths.psm1.md +++ b/document/scripts/lib/powershell/LibPaths.psm1.md @@ -1,3 +1,8 @@ +--- +title: LibPaths.psm1 +description: "文档编写日期: 2026-03-20,LibPaths.psm1 提供路径解析和目录管理功能。该模块" +--- + # LibPaths.psm1 > 文档编写日期: 2026-03-20 @@ -7,7 +12,7 @@ ### 加载方式 ```powershell Import-Module scripts/lib/powershell/LibPaths.psm1 -``` +```bash ## Scripts详解 @@ -31,7 +36,7 @@ LibPaths.psm1 提供路径解析和目录管理功能。该模块专门设计用 #### Get-ScriptDir ```powershell Get-ScriptDir -``` +```text 获取调用此模块的脚本所在目录的绝对路径。这是模块的核心函数,使用多种回退机制确保在各种执行场景下都能正确获取路径: **检测机制(按优先级):** @@ -51,22 +56,22 @@ Get-ScriptDir #### Get-ProjectRoot ```powershell Get-ProjectRoot -``` +```text 获取项目根目录。假设脚本位于 `scripts/` 的子目录中,函数会向上追溯两级目录。 **目录结构假设:** -``` +```text ProjectRoot/ scripts/ lib/ LibPaths.psm1 some-script.ps1 <-- 从这里调用 -``` +```text #### ConvertTo-AbsolutePath ```powershell ConvertTo-AbsolutePath [-Path] [[-BasePath] ] -``` +```text 将相对路径转换为绝对路径。 **参数:** @@ -81,7 +86,7 @@ ConvertTo-AbsolutePath [-Path] [[-BasePath] ] #### New-Directory ```powershell New-Directory [-Path] -``` +```text 确保指定目录存在。如果目录不存在则创建,已存在则不做任何操作。 ### 导出的变量 diff --git a/document/scripts/lib/powershell/README.md b/document/scripts/lib/powershell/README.md index 1f11224b5..ea546aba5 100644 --- a/document/scripts/lib/powershell/README.md +++ b/document/scripts/lib/powershell/README.md @@ -1,3 +1,8 @@ +--- +title: PowerShell库文档 +description: "文档编写日期: 2026-03-20,本目录包含 CFDesktop 构建系统使用的 PowerSh" +--- + # PowerShell库文档 > 文档编写日期: 2026-03-20 @@ -22,7 +27,7 @@ ```powershell Import-Module scripts/lib/powershell/LibCommon.psm1 Import-Module scripts/lib/powershell/LibConfig.psm1 -``` +```text ### 脚本内加载(点号加载) @@ -30,18 +35,18 @@ Import-Module scripts/lib/powershell/LibConfig.psm1 . "$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。 @@ -62,7 +67,7 @@ Write-LogWarning "Warning message" Write-LogError "Error message" Write-LogSeparator Write-LogProgress -Current 5 -Total 10 -Message "Processing" -``` +```text ### 构建操作 (LibBuild) @@ -78,7 +83,7 @@ Invoke-CMakeConfigure -Generator "Ninja" -BuildType "Release" -SourceDir "." -Bu # CMake 构建 Invoke-CMakeBuild -BuildDir "build" -Parallel (Get-ParallelJobCount) -``` +```text ### 配置读取 (LibConfig) @@ -94,4 +99,4 @@ $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 ffeb77675..ab5a37c13 100644 --- a/document/scripts/lib/powershell/index.md +++ b/document/scripts/lib/powershell/index.md @@ -1,10 +1,11 @@ -# powershell - -> Welcome to the powershell section. +--- +title: PowerShell 脚本库 +description: 本目录包含 PowerShell 共享工具函数,为 Windows 平台的构建、测试与发布脚本提供通 +--- -## Overview +# PowerShell 脚本库 -Documentation and resources for powershell. +本目录包含 PowerShell 共享工具函数,为 Windows 平台的构建、测试与发布脚本提供通用功能支持,包括日志输出、路径处理、环境检测与 CMake 封装调用等,与 Bash 脚本库保持功能对齐。 --- diff --git a/document/scripts/release/.pages b/document/scripts/release/.pages deleted file mode 100644 index 36c2535a2..000000000 --- a/document/scripts/release/.pages +++ /dev/null @@ -1,3 +0,0 @@ -title: 发布脚本 -nav: - - Git Hooks: hooks diff --git a/document/scripts/release/hooks/.pages b/document/scripts/release/hooks/.pages deleted file mode 100644 index c9432111c..000000000 --- a/document/scripts/release/hooks/.pages +++ /dev/null @@ -1,7 +0,0 @@ -title: Git Hooks -nav: - - 安装 Hooks (bash): install_hooks.sh.md - - 安装 Hooks (PowerShell): install_hooks.ps1.md - - pre-commit 示例: pre-commit.sample.md - - pre-push 示例: pre-push.sample.md - - 版本工具: version_utils.sh.md diff --git a/document/scripts/release/hooks/README.md b/document/scripts/release/hooks/README.md index aca843b89..7c84ebbfe 100644 --- a/document/scripts/release/hooks/README.md +++ b/document/scripts/release/hooks/README.md @@ -1,3 +1,8 @@ +--- +title: Git Hooks +description: "文档编写日期: 2026-03-20,CFDesktop项目的Git hooks配置目录。" +--- + # Git Hooks > 文档编写日期: 2026-03-20 @@ -12,7 +17,7 @@ bash scripts/release/hooks/install_hooks.sh # Windows PowerShell .\scripts\release\hooks\install_hooks.ps1 -``` +```bash ## 文件说明 diff --git a/document/scripts/release/hooks/index.md b/document/scripts/release/hooks/index.md index 50b2271be..d59853b91 100644 --- a/document/scripts/release/hooks/index.md +++ b/document/scripts/release/hooks/index.md @@ -1,10 +1,11 @@ -# hooks - -> Welcome to the hooks section. +--- +title: Git Hooks +description: 本目录包含发布流程中使用的 Git Hooks 脚本,用于在 等关键操作前后自动执行代码检查、文档 +--- -## Overview +# Git Hooks -Documentation and resources for hooks. +本目录包含发布流程中使用的 Git Hooks 脚本,用于在 `git push` 等关键操作前后自动执行代码检查、文档生成和版本一致性验证,防止不符合规范的代码进入远程仓库。 --- diff --git a/document/scripts/release/hooks/install_hooks.ps1.md b/document/scripts/release/hooks/install_hooks.ps1.md index a92d0b58a..180a25e75 100644 --- a/document/scripts/release/hooks/install_hooks.ps1.md +++ b/document/scripts/release/hooks/install_hooks.ps1.md @@ -1,3 +1,8 @@ +--- +title: installhooks.ps1 +description: "文档编写日期: 2026-03-20,Windows PowerShell版本的Git钩子安装脚本," +--- + # install_hooks.ps1 > 文档编写日期: 2026-03-20 @@ -8,7 +13,7 @@ ```powershell # Windows PowerShell .\scripts\release\hooks\install_hooks.ps1 -``` +```text ## Scripts详解 @@ -16,9 +21,9 @@ Windows PowerShell版本的Git钩子安装脚本,自动安装Git钩子到.git/hooks/目录。 ### 依赖模块 -``` +```text scripts\lib\powershell\LibPaths.psm1 -``` +```bash 提供路径解析功能模块。 ### 安装的钩子 @@ -58,12 +63,12 @@ scripts\lib\powershell\LibPaths.psm1 ### 卸载方法 ```powershell Remove-Item .git\hooks\pre-commit, .git\hooks\pre-push -``` +```text ### 验证安装 ```powershell dir .git\hooks\pre-* -``` +```text ### 相关文件 - `/home/charliechen/CFDesktop/scripts/release/hooks/install_hooks.ps1` diff --git a/document/scripts/release/hooks/install_hooks.sh.md b/document/scripts/release/hooks/install_hooks.sh.md index c722d2c71..a90f0d4cc 100644 --- a/document/scripts/release/hooks/install_hooks.sh.md +++ b/document/scripts/release/hooks/install_hooks.sh.md @@ -1,3 +1,8 @@ +--- +title: installhooks.sh / installhooks.ps1 +description: "文档编写日期: 2026-03-20,自动安装Git钩子到.git/hooks/目录。" +--- + # install_hooks.sh / install_hooks.ps1 > 文档编写日期: 2026-03-20 @@ -11,7 +16,7 @@ bash scripts/release/hooks/install_hooks.sh # Windows PowerShell .\scripts\release\hooks\install_hooks.ps1 -``` +```bash ## Scripts详解 @@ -49,7 +54,7 @@ rm .git/hooks/pre-commit .git/hooks/pre-push # Windows PowerShell Remove-Item .git\hooks\pre-commit, .git\hooks\pre-push -``` +```text ### 验证安装 ```bash @@ -58,7 +63,7 @@ ls -la .git/hooks/pre-commit .git/hooks/pre-push # Windows PowerShell dir .git\hooks\pre-* -``` +```text ### 相关文件 - `/home/charliechen/CFDesktop/scripts/release/hooks/install_hooks.sh` diff --git a/document/scripts/release/hooks/pre-commit.sample.md b/document/scripts/release/hooks/pre-commit.sample.md index 5d9ed4942..92f143733 100644 --- a/document/scripts/release/hooks/pre-commit.sample.md +++ b/document/scripts/release/hooks/pre-commit.sample.md @@ -1,3 +1,8 @@ +--- +title: "pre-commit.sample" +description: "文档编写日期: 2026-03-20,运行 installhooks.sh/installhooks" +--- + # pre-commit.sample > 文档编写日期: 2026-03-20 @@ -10,7 +15,7 @@ ### 绕过方法 ```bash git commit --no-verify -m "message" -``` +```text ## Scripts详解 diff --git a/document/scripts/release/hooks/pre-push.sample.md b/document/scripts/release/hooks/pre-push.sample.md index 12c935ad0..a936d6da8 100644 --- a/document/scripts/release/hooks/pre-push.sample.md +++ b/document/scripts/release/hooks/pre-push.sample.md @@ -1,3 +1,8 @@ +--- +title: "pre-push.sample" +description: "文档编写日期: 2026-03-20,在推送前验证Docker构建,仅对main和release分支" +--- + # pre-push.sample > 文档编写日期: 2026-03-20 @@ -7,7 +12,7 @@ ### 绕过方法 ```bash git push --no-verify -``` +```bash ## Scripts详解 diff --git a/document/scripts/release/hooks/version_utils.sh.md b/document/scripts/release/hooks/version_utils.sh.md index e07134702..ebf069539 100644 --- a/document/scripts/release/hooks/version_utils.sh.md +++ b/document/scripts/release/hooks/version_utils.sh.md @@ -1,3 +1,8 @@ +--- +title: versionutils.sh +description: "文档编写日期: 2026-03-20,为Git钩子提供版本号解析、验证级别检测和Git版本信息获取功" +--- + # version_utils.sh > 文档编写日期: 2026-03-20 @@ -7,7 +12,7 @@ ### 加载方式 ```bash source scripts/release/hooks/version_utils.sh -``` +```cpp ## Scripts详解 @@ -57,7 +62,7 @@ source scripts/release/hooks/version_utils.sh #### determine_verify_level ```bash determine_verify_level -``` +```text - **参数1**: 本地版本号(如 `1.2.3`) - **参数2**: 远程版本号(如 `1.1.0`) - **返回**: `major`, `minor`, 或 `patch` @@ -66,7 +71,7 @@ determine_verify_level #### get_cmake_version ```bash get_cmake_version -``` +```text - **参数1**: 项目根目录路径 - **返回**: CMakeLists.txt中的版本号(提取 `VERSION x.y.z` 格式) - **失败**: 返回空字符串 @@ -74,7 +79,7 @@ get_cmake_version #### get_remote_cmake_version ```bash get_remote_cmake_version [remote_branch] -``` +```text - **参数1**: 远程分支名(默认: `origin/main`) - **返回**: 远程CMakeLists.txt中的版本号 - **失败**: 返回空字符串 @@ -108,7 +113,7 @@ echo "$(get_verify_level_description "$LEVEL")" # 获取CMake版本 CMAKE_VER=$(get_cmake_version "/path/to/project") echo "$CMAKE_VER" # 输出: x.y.z -``` +```text ### 相关文件 - `/home/charliechen/CFDesktop/scripts/release/hooks/version_utils.sh` diff --git a/document/scripts/release/index.md b/document/scripts/release/index.md index b287e880d..fc4f18eaa 100644 --- a/document/scripts/release/index.md +++ b/document/scripts/release/index.md @@ -1,10 +1,11 @@ -# release - -> Welcome to the release section. +--- +title: 发布脚本 +description: 本目录包含正式发布流程所使用的脚本,负责版本号更新、变更日志生成、二进制包打包以及 Git Tag +--- -## Overview +# 发布脚本 -Documentation and resources for release. +本目录包含正式发布流程所使用的脚本,负责版本号更新、变更日志生成、二进制包打包以及 Git Tag 创建等发布步骤的自动化执行。 --- diff --git a/document/status/current.md b/document/status/current.md new file mode 100644 index 000000000..d204d3fff --- /dev/null +++ b/document/status/current.md @@ -0,0 +1,43 @@ +--- +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 new file mode 100644 index 000000000..b5259660b --- /dev/null +++ b/document/status/index.md @@ -0,0 +1,10 @@ +--- +title: 项目状态 +description: 本节记录 CFDesktop 的当前事实状态。后续开发和 AI 辅助分析应优先读取这里,而不是旧的历 +--- + +# 项目状态 + +本节记录 CFDesktop 的当前事实状态。后续开发和 AI 辅助分析应优先读取这里,而不是旧的历史状态报告。 + +- [当前状态](current.md) diff --git a/document/stylesheets/extra.css b/document/stylesheets/extra.css deleted file mode 100644 index e42011c96..000000000 --- a/document/stylesheets/extra.css +++ /dev/null @@ -1,362 +0,0 @@ -/* ========================================================================== - Tutorial_AwesomeModernCPP - Custom Stylesheet - Improve Chinese typography, code readability, and visual hierarchy - ========================================================================== */ - -/* ---------- Global ---------- */ - -/* Smooth scrolling for the entire page */ -html { - scroll-behavior: smooth; -} - -/* Custom text selection color */ -::selection { - background: rgba(81, 107, 232, 0.25); - color: inherit; -} - -[data-md-color-scheme="slate"] ::selection { - background: rgba(99, 126, 255, 0.3); -} - -/* ---------- Chinese Typography ---------- */ - -.md-typeset { - line-height: 1.85; - font-size: 0.82rem; -} - -.md-typeset h1 { - line-height: 1.4; -} - -.md-typeset h2 { - line-height: 1.45; -} - -.md-typeset h3 { - line-height: 1.5; -} - -.md-typeset h4 { - line-height: 1.55; -} - -.md-typeset p { - margin-bottom: 1.2em; -} - -/* ---------- Header Gradient ---------- */ - -/* Subtle indigo-to-blue gradient for a modern look */ -.md-header { - background: linear-gradient(135deg, #3f51b5 0%, #5c6bc0 50%, #42a5f5 100%); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); -} - -[data-md-color-scheme="slate"] .md-header { - background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); -} - -/* Header tabs bar matches gradient */ -.md-tabs { - background: linear-gradient(135deg, #3949ab 0%, #5c6bc0 50%, #42a5f5 100%); -} - -[data-md-color-scheme="slate"] .md-tabs { - background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); -} - -/* ---------- Sidebar ---------- */ - -/* Sidebar items: more spacing and smooth hover transitions */ -.md-nav__item { - padding: 0.15em 0; -} - -.md-nav__link { - transition: all 0.2s ease; - border-radius: 4px; - margin: 1px 4px; - padding: 4px 8px !important; -} - -.md-nav__link:hover { - background-color: rgba(81, 107, 232, 0.08); -} - -[data-md-color-scheme="slate"] .md-nav__link:hover { - background-color: rgba(99, 126, 255, 0.12); -} - -/* Active sidebar item highlight */ -.md-nav__link--active { - font-weight: 600; -} - -.md-nav__section-title { - font-size: 0.72rem; - letter-spacing: 0.03em; -} - -/* ---------- Content Width ---------- */ - -.md-grid { - max-width: 80rem; -} - -/* ---------- Code Blocks ---------- */ - -/* Rounded corners for code blocks */ -.md-typeset pre>code { - max-height: 32rem; - font-size: 0.75rem; - line-height: 1.6; - border-radius: 8px; -} - -/* Code block container with subtle shadow */ -.md-typeset pre { - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); - margin: 1em 0; -} - -[data-md-color-scheme="slate"] .md-typeset pre { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); -} - -/* Inline code with better visual treatment */ -.md-typeset code { - font-size: 0.78em; - padding: 0.15em 0.35em; - border-radius: 4px; - transition: background-color 0.2s ease; -} - -/* Code copy button styling */ -.md-clipboard { - transition: all 0.2s ease; - border-radius: 4px; -} - -.md-clipboard:hover { - color: var(--md-accent-fg-color); -} - -/* ---------- Tables ---------- */ - -.md-typeset table:not([class]) { - font-size: 0.78rem; - line-height: 1.6; - display: table; - width: 100%; - border-radius: 8px; - overflow: hidden; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05); -} - -[data-md-color-scheme="slate"] .md-typeset table:not([class]) { - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15); -} - -.md-typeset table:not([class]) th { - font-weight: 600; - padding: 0.75em 1em; -} - -.md-typeset table:not([class]) td { - padding: 0.65em 1em; -} - -.md-typeset table:not([class]) tr:nth-child(even) { - background-color: rgba(0, 0, 0, 0.02); -} - -[data-md-color-scheme="slate"] .md-typeset table:not([class]) tr:nth-child(even) { - background-color: rgba(255, 255, 255, 0.03); -} - -/* ---------- Admonitions ---------- */ - -/* Admonitions with rounded corners and subtle shadow */ -.md-typeset .admonition, -.md-typeset details { - font-size: 0.8rem; - line-height: 1.75; - padding: 0.8em 1.2em; - border-radius: 8px; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); - transition: box-shadow 0.2s ease; -} - -.md-typeset .admonition:hover, -.md-typeset details:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); -} - -[data-md-color-scheme="slate"] .md-typeset .admonition, -[data-md-color-scheme="slate"] .md-typeset details { - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); -} - -.md-typeset .admonition-title, -.md-typeset summary { - font-weight: 600; - font-size: 0.85rem; -} - -/* ---------- Links ---------- */ - -.md-typeset a { - text-decoration: underline; - text-decoration-style: dotted; - text-underline-offset: 3px; - transition: color 0.2s ease, text-decoration-style 0.2s ease; -} - -.md-typeset a:hover { - text-decoration-style: solid; -} - -/* ---------- Blockquotes ---------- */ - -.md-typeset blockquote { - border-left: 4px solid var(--md-primary-fg-color); - padding: 0.5em 1em; - margin-left: 0; - font-style: normal; - opacity: 0.9; - border-radius: 0 4px 4px 0; - background: rgba(81, 107, 232, 0.04); -} - -[data-md-color-scheme="slate"] .md-typeset blockquote { - background: rgba(99, 126, 255, 0.06); -} - -/* ---------- Back-to-top Button ---------- */ - -.md-top { - border-radius: 50% !important; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -.md-top:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); -} - -/* ---------- Search Overlay ---------- */ - -.md-search__overlay { - transition: opacity 0.2s ease; -} - -.md-search__form { - border-radius: 8px; - transition: box-shadow 0.2s ease; -} - -.md-search__form:focus-within { - box-shadow: 0 0 0 2px var(--md-accent-fg-color); -} - -/* ---------- Custom Scrollbar ---------- */ - -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(0, 0, 0, 0.15); - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(0, 0, 0, 0.25); -} - -[data-md-color-scheme="slate"] ::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.15); -} - -[data-md-color-scheme="slate"] ::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.25); -} - -/* ---------- Footer ---------- */ - -.md-footer { - border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -[data-md-color-scheme="slate"] .md-footer { - border-top: 1px solid rgba(255, 255, 255, 0.05); -} - -.md-footer__link { - transition: background-color 0.2s ease; -} - -/* ---------- Dark Mode Adjustments ---------- */ - -[data-md-color-scheme="slate"] .md-typeset code { - background-color: rgba(255, 255, 255, 0.07); -} - -/* Dark mode: slightly brighter code block background for contrast */ -[data-md-color-scheme="slate"] .md-typeset pre>code { - background-color: rgba(0, 0, 0, 0.3); -} - -/* ---------- Tabbed Content ---------- */ - -/* Smooth tab transitions */ -.md-typeset .tabbed-labels { - box-shadow: 0 1px 0 var(--md-primary-fg-color--light); -} - -.md-typeset .tabbed-labels>label { - transition: color 0.2s ease; -} - -/* ---------- Images ---------- */ - -/* Rounded corners and subtle shadow for images */ -.md-typeset img:not([class]) { - border-radius: 6px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); - transition: box-shadow 0.2s ease; -} - -.md-typeset img:not([class]):hover { - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); -} - -[data-md-color-scheme="slate"] .md-typeset img:not([class]) { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); -} - -/* ---------- Print Styles ---------- */ - -@media print { - .md-typeset { - font-size: 11pt; - line-height: 1.7; - } - - .md-sidebar, - .md-header, - .md-footer, - .md-top { - display: none !important; - } -} \ No newline at end of file diff --git a/document/todo/.pages b/document/todo/.pages deleted file mode 100644 index 6105a2b9f..000000000 --- a/document/todo/.pages +++ /dev/null @@ -1,6 +0,0 @@ -title: TODO -icon: material/clipboard-check -nav: - - 基础模块: base - - Desktop 模块: desktop - - 已完成: done diff --git a/document/todo/README.md b/document/todo/README.md index 3cd557e88..a2b50237f 100644 --- a/document/todo/README.md +++ b/document/todo/README.md @@ -1,75 +1,80 @@ -# 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* +--- +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/base/.pages b/document/todo/base/.pages deleted file mode 100644 index d369ad6f5..000000000 --- a/document/todo/base/.pages +++ /dev/null @@ -1,6 +0,0 @@ -title: 基础模块 TODO -nav: - - 输入层: 02_input_layer.md - - 模拟器: 03_simulator.md - - 测试: 04_testing.md - - UI Material 框架: 99_ui_material_framework.md diff --git a/document/todo/base/02_input_layer.md b/document/todo/base/02_input_layer.md index ca538a7f0..aad1d07b4 100644 --- a/document/todo/base/02_input_layer.md +++ b/document/todo/base/02_input_layer.md @@ -1,212 +1,217 @@ -# Phase 3: 输入抽象层 TODO - -> **状态**: ⬜ 待开始 -> **预计周期**: 1~2 周 -> **依赖阶段**: Phase 0, Phase 1, Phase 2 -> **目标交付物**: 统一输入分发、触摸处理、按键处理、手势识别、焦点导航 - ---- - -## 一、阶段目标 - -### 核心目标 -屏蔽底层输入差异,统一触摸、物理按键、旋钮等输入事件,支持焦点导航模式。 - -### 具体交付物 -- [ ] `InputManager` 统一分发层 -- [ ] `TouchInputHandler` 触摸处理器 -- [ ] `KeyInputHandler` 按键处理器 -- [ ] `RotaryInputHandler` 旋钮处理器 -- [ ] `FocusNavigator` 焦点导航器 -- [ ] 单元测试 - ---- - -## 二、Week 1: 核心处理器 - -### Day 1-2: InputManager 基础 -- [ ] 创建 InputManager 类 - - [ ] 单例模式 - - [ ] 设备注册/注销 - - [ ] 事件分发机制 -- [ ] 实现设备注册/注销 - - [ ] `registerDevice()` - - [ ] `unregisterDevice()` - - [ ] 设备列表管理 -- [ ] 实现事件分发机制 - - [ ] `dispatchEvent()` - - [ ] 事件过滤器 -- [ ] 添加事件过滤器支持 - - [ ] `addEventFilter()` - - [ ] `removeEventFilter()` - -### Day 3: TouchInputHandler -- [ ] 创建 TouchInputHandler 类 - - [ ] 触摸点跟踪 - - [ ] 手势检测基础 -- [ ] 实现触摸点跟踪 - - [ ] `TouchPoint` 结构 - - [ ] 多点触摸支持 - - [ ] 压力检测 -- [ ] 实现单击/双击检测 - - [ ] 点击阈值判定 - - [ ] 时间窗口判定 -- [ ] 实现长按检测 - - [ ] 定时器实现 - - [ ] 移动距离判定 - -### Day 4: KeyInputHandler -- [ ] 创建 KeyInputHandler 类 - - [ ] 按键配置支持 - - [ ] 状态跟踪 -- [ ] 实现按键状态跟踪 - - [ ] 按键列表 - - [ ] 时间戳记录 -- [ ] 实现长按检测 - - [ ] 阈值配置 - - [ ] 信号触发 -- [ ] 实现连击检测 - - [ ] 计数器 - - [ ] 时间窗口 - -### Day 5: RotaryInputHandler -- [ ] 创建 RotaryInputHandler 类 - - [ ] 旋钮配置 - - [ ] 速度计算 -- [ ] 实现旋转解码 - - [ ] AB 相位解码 - - [ ] 方向判定 -- [ ] 实现速度计算 - - [ ] 滑动平均 - - [ ] 加速因子 -- [ ] 实现加速功能 - - [ ] 速度映射 - - [ ] 可配置系数 - ---- - -## 三、Week 2: 手势与导航 - -### Day 1-2: GestureRecognizer -- [ ] 创建 GestureRecognizer 类 - - [ ] 手势状态机 - - [ ] 配置支持 -- [ ] 实现滑动手势 - - [ ] 方向判定 - - [ ] 距离阈值 - - [ ] 超时检测 -- [ ] 实现捏合手势 - - [ ] 双指检测 - - [ ] 缩放计算 -- [ ] 实现旋转手势 - - [ ] 角度计算 - - [ ] 方向判定 - -### Day 3: FocusNavigator -- [ ] 创建 FocusNavigator 类 - - [ ] 单例模式 - - [ ] 焦点策略 -- [ ] 实现方向导航算法 - - [ ] 四方向查找 - - [ ] 距离计算 - - [ ] 方向判定 -- [ ] 实现焦点链自定义 - - [ ] `addFocusChain()` - - [ ] 自定义跳转 -- [ ] 实现边界策略 - - [ ] 循环策略 - - [ ] 停止策略 - -### Day 4: 原生设备 -- [ ] 实现 EvdevDevice - - [ ] `/dev/input/eventX` 读取 - - [ ] 事件解析 - - [ ] 设备识别 -- [ ] 实现 GPIOButton - - [ ] sysfs 接口 - - [ ] 边沿检测 - - [ ] 防抖处理 -- [ ] 实现 RotaryEncoder - - [ ] GPIO 输入 - - [ ] 状态解码 - - [ ] 位置计算 - -### Day 5: 测试与集成 -- [ ] 编写单元测试 - - [ ] 触摸处理测试 - - [ ] 按键处理测试 - - [ ] 手势识别测试 - - [ ] 焦点导航测试 -- [ ] 集成测试 - - [ ] 多设备协同 - - [ ] 事件流测试 -- [ ] 性能测试 - - [ ] 事件延迟测试 - - [ ] CPU 占用测试 - ---- - -## 四、验收标准 - -### 功能验收 -- [ ] 触摸输入正常响应 -- [ ] 手势识别准确率 > 95% -- [ ] 焦点导航无死循环 -- [ ] 原生设备正常读取 - -### 性能验收 -- [ ] 事件延迟 < 16ms -- [ ] CPU 占用 < 5% - -### 兼容性验收 -- [ ] 模拟器和真机行为一致 - ---- - -## 五、文件清单 - -### 核心接口 -- [ ] `include/CFDesktop/Base/Input/InputManager.h` -- [ ] `include/CFDesktop/Base/Input/InputEvent.h` -- [ ] `include/CFDesktop/Base/Input/InputDevice.h` - -### 处理器 -- [ ] `include/CFDesktop/Base/Input/TouchInputHandler.h` -- [ ] `include/CFDesktop/Base/Input/KeyInputHandler.h` -- [ ] `include/CFDesktop/Base/Input/RotaryInputHandler.h` -- [ ] `include/CFDesktop/Base/Input/GestureRecognizer.h` -- [ ] `include/CFDesktop/Base/Input/FocusNavigator.h` - -### 实现 -- [ ] `src/base/input/InputManager.cpp` -- [ ] `src/base/input/TouchInputHandler.cpp` -- [ ] `src/base/input/KeyInputHandler.cpp` -- [ ] `src/base/input/RotaryInputHandler.cpp` -- [ ] `src/base/input/GestureRecognizer.cpp` -- [ ] `src/base/input/FocusNavigator.cpp` - -### 原生设备 -- [ ] `src/base/input/native/EvdevDevice.cpp` -- [ ] `src/base/input/native/GPIOButton.cpp` -- [ ] `src/base/input/native/RotaryEncoder.cpp` -- [ ] `src/base/input/simulator/SimulatedInput.cpp` - -### 测试 -- [ ] `tests/unit/base/input/test_input_manager.cpp` -- [ ] `tests/unit/base/input/test_touch_handler.cpp` -- [ ] `tests/unit/base/input/test_key_handler.cpp` -- [ ] `tests/unit/base/input/test_rotary_handler.cpp` -- [ ] `tests/unit/base/input/test_gesture_recognizer.cpp` -- [ ] `tests/unit/base/input/test_focus_navigator.cpp` - ---- - -## 六、相关文档 - -- 设计文档: [../../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) - ---- - -*最后更新: 2026-03-05* +--- +title: "Phase 3: 输入抽象层 TODO" +description: "预计周期: 1~2 周,依赖阶段: Phase 0, Phase 1, Phase 2" +--- + +# Phase 3: 输入抽象层 TODO + +> **状态**: ⬜ 待开始 +> **预计周期**: 1~2 周 +> **依赖阶段**: Phase 0, Phase 1, Phase 2 +> **目标交付物**: 统一输入分发、触摸处理、按键处理、手势识别、焦点导航 + +--- + +## 一、阶段目标 + +### 核心目标 +屏蔽底层输入差异,统一触摸、物理按键、旋钮等输入事件,支持焦点导航模式。 + +### 具体交付物 +- [ ] `InputManager` 统一分发层 +- [ ] `TouchInputHandler` 触摸处理器 +- [ ] `KeyInputHandler` 按键处理器 +- [ ] `RotaryInputHandler` 旋钮处理器 +- [ ] `FocusNavigator` 焦点导航器 +- [ ] 单元测试 + +--- + +## 二、Week 1: 核心处理器 + +### Day 1-2: InputManager 基础 +- [ ] 创建 InputManager 类 + - [ ] 单例模式 + - [ ] 设备注册/注销 + - [ ] 事件分发机制 +- [ ] 实现设备注册/注销 + - [ ] `registerDevice()` + - [ ] `unregisterDevice()` + - [ ] 设备列表管理 +- [ ] 实现事件分发机制 + - [ ] `dispatchEvent()` + - [ ] 事件过滤器 +- [ ] 添加事件过滤器支持 + - [ ] `addEventFilter()` + - [ ] `removeEventFilter()` + +### Day 3: TouchInputHandler +- [ ] 创建 TouchInputHandler 类 + - [ ] 触摸点跟踪 + - [ ] 手势检测基础 +- [ ] 实现触摸点跟踪 + - [ ] `TouchPoint` 结构 + - [ ] 多点触摸支持 + - [ ] 压力检测 +- [ ] 实现单击/双击检测 + - [ ] 点击阈值判定 + - [ ] 时间窗口判定 +- [ ] 实现长按检测 + - [ ] 定时器实现 + - [ ] 移动距离判定 + +### Day 4: KeyInputHandler +- [ ] 创建 KeyInputHandler 类 + - [ ] 按键配置支持 + - [ ] 状态跟踪 +- [ ] 实现按键状态跟踪 + - [ ] 按键列表 + - [ ] 时间戳记录 +- [ ] 实现长按检测 + - [ ] 阈值配置 + - [ ] 信号触发 +- [ ] 实现连击检测 + - [ ] 计数器 + - [ ] 时间窗口 + +### Day 5: RotaryInputHandler +- [ ] 创建 RotaryInputHandler 类 + - [ ] 旋钮配置 + - [ ] 速度计算 +- [ ] 实现旋转解码 + - [ ] AB 相位解码 + - [ ] 方向判定 +- [ ] 实现速度计算 + - [ ] 滑动平均 + - [ ] 加速因子 +- [ ] 实现加速功能 + - [ ] 速度映射 + - [ ] 可配置系数 + +--- + +## 三、Week 2: 手势与导航 + +### Day 1-2: GestureRecognizer +- [ ] 创建 GestureRecognizer 类 + - [ ] 手势状态机 + - [ ] 配置支持 +- [ ] 实现滑动手势 + - [ ] 方向判定 + - [ ] 距离阈值 + - [ ] 超时检测 +- [ ] 实现捏合手势 + - [ ] 双指检测 + - [ ] 缩放计算 +- [ ] 实现旋转手势 + - [ ] 角度计算 + - [ ] 方向判定 + +### Day 3: FocusNavigator +- [ ] 创建 FocusNavigator 类 + - [ ] 单例模式 + - [ ] 焦点策略 +- [ ] 实现方向导航算法 + - [ ] 四方向查找 + - [ ] 距离计算 + - [ ] 方向判定 +- [ ] 实现焦点链自定义 + - [ ] `addFocusChain()` + - [ ] 自定义跳转 +- [ ] 实现边界策略 + - [ ] 循环策略 + - [ ] 停止策略 + +### Day 4: 原生设备 +- [ ] 实现 EvdevDevice + - [ ] `/dev/input/eventX` 读取 + - [ ] 事件解析 + - [ ] 设备识别 +- [ ] 实现 GPIOButton + - [ ] sysfs 接口 + - [ ] 边沿检测 + - [ ] 防抖处理 +- [ ] 实现 RotaryEncoder + - [ ] GPIO 输入 + - [ ] 状态解码 + - [ ] 位置计算 + +### Day 5: 测试与集成 +- [ ] 编写单元测试 + - [ ] 触摸处理测试 + - [ ] 按键处理测试 + - [ ] 手势识别测试 + - [ ] 焦点导航测试 +- [ ] 集成测试 + - [ ] 多设备协同 + - [ ] 事件流测试 +- [ ] 性能测试 + - [ ] 事件延迟测试 + - [ ] CPU 占用测试 + +--- + +## 四、验收标准 + +### 功能验收 +- [ ] 触摸输入正常响应 +- [ ] 手势识别准确率 > 95% +- [ ] 焦点导航无死循环 +- [ ] 原生设备正常读取 + +### 性能验收 +- [ ] 事件延迟 < 16ms +- [ ] CPU 占用 < 5% + +### 兼容性验收 +- [ ] 模拟器和真机行为一致 + +--- + +## 五、文件清单 + +### 核心接口 +- [ ] `include/CFDesktop/Base/Input/InputManager.h` +- [ ] `include/CFDesktop/Base/Input/InputEvent.h` +- [ ] `include/CFDesktop/Base/Input/InputDevice.h` + +### 处理器 +- [ ] `include/CFDesktop/Base/Input/TouchInputHandler.h` +- [ ] `include/CFDesktop/Base/Input/KeyInputHandler.h` +- [ ] `include/CFDesktop/Base/Input/RotaryInputHandler.h` +- [ ] `include/CFDesktop/Base/Input/GestureRecognizer.h` +- [ ] `include/CFDesktop/Base/Input/FocusNavigator.h` + +### 实现 +- [ ] `src/base/input/InputManager.cpp` +- [ ] `src/base/input/TouchInputHandler.cpp` +- [ ] `src/base/input/KeyInputHandler.cpp` +- [ ] `src/base/input/RotaryInputHandler.cpp` +- [ ] `src/base/input/GestureRecognizer.cpp` +- [ ] `src/base/input/FocusNavigator.cpp` + +### 原生设备 +- [ ] `src/base/input/native/EvdevDevice.cpp` +- [ ] `src/base/input/native/GPIOButton.cpp` +- [ ] `src/base/input/native/RotaryEncoder.cpp` +- [ ] `src/base/input/simulator/SimulatedInput.cpp` + +### 测试 +- [ ] `tests/unit/base/input/test_input_manager.cpp` +- [ ] `tests/unit/base/input/test_touch_handler.cpp` +- [ ] `tests/unit/base/input/test_key_handler.cpp` +- [ ] `tests/unit/base/input/test_rotary_handler.cpp` +- [ ] `tests/unit/base/input/test_gesture_recognizer.cpp` +- [ ] `tests/unit/base/input/test_focus_navigator.cpp` + +--- + +## 六、相关文档 + +- 设计文档: [../../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) + +--- + +*最后更新: 2026-03-05* diff --git a/document/todo/base/03_simulator.md b/document/todo/base/03_simulator.md index f5e8112f4..77fa73ac5 100644 --- a/document/todo/base/03_simulator.md +++ b/document/todo/base/03_simulator.md @@ -1,266 +1,271 @@ -# Phase 6: 多平台模拟器 TODO - -> **状态**: ⬜ 待开始 -> **预计周期**: 2~3 周 -> **依赖阶段**: Phase 0, Phase 2, Phase 3 -> **目标交付物**: 模拟器窗口、设备外壳、触摸可视化、档位选择器 - ---- - -## 一、阶段目标 - -### 核心目标 -在 Windows/Ubuntu 上通过 Qt Creator 还原真实嵌入式设备的 UI 效果,实现"所见即所得"的开发体验。 - -### 具体交付物 -- [ ] `SimulatorWindow` 模拟器主窗口 -- [ ] `DeviceFrame` 设备外壳渲染 -- [ ] `TouchVisualizer` 触摸可视化 -- [ ] `HWTierSelector` 硬件档位选择器 -- [ ] `ResolutionPreset` 分辨率预设管理 -- [ ] 独立可执行文件 `cfdesktop-sim` - ---- - -## 二、Week 1: 基础框架 - -### Day 1-2: 主窗口与设备外壳 -- [ ] 创建 SimulatorWindow 类 - - [ ] QMainWindow 继承 - - [ ] 布局管理 - - [ ] 菜单栏 -- [ ] 实现 DeviceFrame 基础绘制 - - [ ] QWidget 实现 - - [ ] 屏幕容器 - - [ ] 绘制事件 -- [ ] 实现矢量外壳绘制 - - [ ] 圆角矩形 - - [ ] 阴影效果 - - [ ] 硬件按键 -- [ ] 支持图片外壳 - - [ ] 图片加载 - - [ ] 缩放适配 - - [ ] 透明度支持 - -### Day 3: 设备配置 -- [ ] 定义 DeviceProfile 结构 - - [ ] 屏幕参数 - - [ ] 外壳参数 - - [ ] 硬件能力 -- [ ] 实现 JSON 序列化 - - [ ] `fromJson()` - - [ ] `toJson()` -- [ ] 创建预设配置文件 - - [ ] `imx6ull-4.3.json` - - [ ] `imx6ull-7.0.json` - - [ ] `rk3568-7.0.json` - - [ ] `rk3568-10.1.json` - - [ ] `rk3588-10.1.json` - - [ ] `generic-1080p.json` -- [ ] 实现配置加载 - - [ ] 目录扫描 - - [ ] 默认配置 - -### Day 4: 控制面板 -- [ ] 创建 ControlPanel 类 - - [ ] QWidget 布局 - - [ ] 控件排列 -- [ ] 实现设备选择器 - - [ ] QComboBox - - [ ] 配置列表 -- [ ] 实现分辨率选择器 - - [ ] 预设分辨率 - - [ ] 自定义分辨率 -- [ ] 实现档位选择器 - - [ ] HWTierSelector - - [ ] 单选按钮组 - -### Day 5: 集成测试 -- [ ] 集成各模块 - - [ ] 信号连接 - - [ ] 状态同步 -- [ ] 基本功能测试 - - [ ] 配置加载 - - [ ] 窗口显示 - - [ ] 控件响应 - ---- - -## 三、Week 2: 注入与模拟 - -### Day 1-2: DPI 注入 -- [ ] 实现 DPI 注入器 - - [ ] DPIInjector 类 - - [ ] 注入接口 -- [ ] 修改 DPIManager 支持注入 - - [ ] 添加注入检查 - - [ ] 优先级处理 -- [ ] 测试不同分辨率 - - [ ] 480×272 - - [ ] 800×480 - - [ ] 1024×600 - - [ ] 1920×1200 - -### Day 3: 硬件 Mock -- [ ] 实现硬件信息 Mock - - [ ] HardwareMock 类 - - [ ] 数据注入 -- [ ] 修改 HardwareProbe 支持 Mock - - [ ] 测试模式检测 - - [ ] Mock 数据优先 -- [ ] 测试档位切换 - - [ ] Low → Mid → High - - [ ] 策略验证 - -### Day 4: 输入模拟 -- [ ] 实现输入模拟器 - - [ ] InputSimulator 类 - - [ ] 事件过滤器 -- [ ] 鼠标转触摸 - - [ ] QTouchEvent 转换 - - [ ] 多点模拟 -- [ ] 键盘转按键 - - [ ] Qt Key 映射 - - [ ] 动作触发 -- [ ] 滚轮转旋钮 - - [ ] QWheelEvent 转换 - - [ ] 步数计算 - -### Day 5: 触摸可视化 -- [ ] 实现 TouchVisualizer - - [ ] QWidget 覆盖层 - - [ ] 绘制事件 -- [ ] 涟漪动画 - - [ ] QPropertyAnimation - - [ ] 透明度渐变 -- [ ] 多点触摸支持 - - [ ] 触摸点列表 - - [ ] 独立动画 - ---- - -## 四、Week 3: 完善与优化 - -### Day 1-2: UI 完善 -- [ ] 完善设备外壳 - - [ ] 更多预设图片 - - [ ] SVG 矢量支持 -- [ ] 添加更多预设 - - [ ] 手机类型 - - [ ] 平板类型 - - [ ] 自定义尺寸 -- [ ] 美化控制面板 - - [ ] 图标支持 - - [ ] 工具提示 - -### Day 3: 高级功能 -- [ ] 截图功能 - - [ ] 整窗口截图 - - [ ] 仅内容区域 - - [ ] 保存对话框 -- [ ] 录屏功能 - - ] GIF 录制 - - [ ] 帧率配置 -- [ ] 性能监控 - - [ ] FPS 显示 - - [ ] 内存显示 - - [ ] CPU 占用 - -### Day 4: 文档 -- [ ] 使用说明 - - [ ] 快速开始 - - [ ] 功能介绍 - - [ ] 常见问题 -- [ ] API 文档 - - [ ] 类接口 - - [ ] 使用示例 -- [ ] 示例代码 - - [ ] 加载 Shell UI - - [ ] 自定义配置 - -### Day 5: 测试 -- [ ] 跨平台测试 - - [ ] Windows 10/11 - - [ ] Ubuntu 20.04+ - - [ ] macOS 12+ -- [ ] 性能测试 - - [ ] 启动时间 - - [ ] 帧率稳定性 -- [ ] 修复 Bug - - [ ] 已知问题 - - [ ] 边界情况 - ---- - -## 五、验收标准 - -### 功能验收 -- [ ] 能正确显示各种分辨率 -- [ ] 档位切换生效 -- [ ] 触摸模拟正常 -- [ ] 截图功能正常 - -### 性能验收 -- [ ] 启动时间 < 2 秒 -- [ ] 帧率稳定 60fps - -### 兼容性验收 -- [ ] Windows 10/11 正常运行 -- [ ] Ubuntu 20.04+ 正常运行 -- [ ] macOS 12+ 正常运行 - ---- - -## 六、文件清单 - -### 核心类 -- [ ] `include/CFDesktop/Simulator/SimulatorWindow.h` -- [ ] `include/CFDesktop/Simulator/DeviceFrame.h` -- [ ] `include/CFDesktop/Simulator/DeviceProfile.h` -- [ ] `include/CFDesktop/Simulator/TouchVisualizer.h` -- [ ] `include/CFDesktop/Simulator/ControlPanel.h` -- [ ] `include/CFDesktop/Simulator/HWTierSelector.h` -- [ ] `include/CFDesktop/Simulator/ResolutionPreset.h` -- [ ] `include/CFDesktop/Simulator/SimulatorInjection.h` - -### 实现 -- [ ] `src/simulator/SimulatorWindow.cpp` -- [ ] `src/simulator/DeviceFrame.cpp` -- [ ] `src/simulator/TouchVisualizer.cpp` -- [ ] `src/simulator/ControlPanel.cpp` -- [ ] `src/simulator/HWTierSelector.cpp` -- [ ] `src/simulator/ResolutionPreset.cpp` - -### 注入 -- [ ] `src/simulator/injection/DPIInjector.cpp` -- [ ] `src/simulator/injection/HardwareMock.cpp` -- [ ] `src/simulator/injection/InputSimulator.cpp` - -### 资源 -- [ ] `assets/simulator/devices/phone-generic.png` -- [ ] `assets/simulator/devices/tablet-10inch.png` -- [ ] `assets/simulator/devices/panel-7inch.png` -- [ ] `assets/simulator/devices/panel-10inch.png` -- [ ] `assets/simulator/profiles/imx6ull-4.3.json` -- [ ] `assets/simulator/profiles/imx6ull-7.0.json` -- [ ] `assets/simulator/profiles/rk3568-7.0.json` -- [ ] `assets/simulator/profiles/rk3568-10.1.json` -- [ ] `assets/simulator/profiles/rk3588-10.1.json` -- [ ] `assets/simulator/profiles/generic-1080p.json` -- [ ] `assets/simulator/effects/touch-ripple.png` - -### 测试 -- [ ] `tests/unit/simulator/test_device_profile.cpp` -- [ ] `tests/unit/simulator/test_device_frame.cpp` -- [ ] `tests/unit/simulator/test_touch_visualizer.cpp` - ---- - -## 七、相关文档 - -- 设计文档: [../../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) - ---- - -*最后更新: 2026-03-05* +--- +title: "Phase 6: 多平台模拟器 TODO" +description: "预计周期: 2~3 周,依赖阶段: Phase 0, Phase 2, Phase 3" +--- + +# Phase 6: 多平台模拟器 TODO + +> **状态**: ⬜ 待开始 +> **预计周期**: 2~3 周 +> **依赖阶段**: Phase 0, Phase 2, Phase 3 +> **目标交付物**: 模拟器窗口、设备外壳、触摸可视化、档位选择器 + +--- + +## 一、阶段目标 + +### 核心目标 +在 Windows/Ubuntu 上通过 Qt Creator 还原真实嵌入式设备的 UI 效果,实现"所见即所得"的开发体验。 + +### 具体交付物 +- [ ] `SimulatorWindow` 模拟器主窗口 +- [ ] `DeviceFrame` 设备外壳渲染 +- [ ] `TouchVisualizer` 触摸可视化 +- [ ] `HWTierSelector` 硬件档位选择器 +- [ ] `ResolutionPreset` 分辨率预设管理 +- [ ] 独立可执行文件 `cfdesktop-sim` + +--- + +## 二、Week 1: 基础框架 + +### Day 1-2: 主窗口与设备外壳 +- [ ] 创建 SimulatorWindow 类 + - [ ] QMainWindow 继承 + - [ ] 布局管理 + - [ ] 菜单栏 +- [ ] 实现 DeviceFrame 基础绘制 + - [ ] QWidget 实现 + - [ ] 屏幕容器 + - [ ] 绘制事件 +- [ ] 实现矢量外壳绘制 + - [ ] 圆角矩形 + - [ ] 阴影效果 + - [ ] 硬件按键 +- [ ] 支持图片外壳 + - [ ] 图片加载 + - [ ] 缩放适配 + - [ ] 透明度支持 + +### Day 3: 设备配置 +- [ ] 定义 DeviceProfile 结构 + - [ ] 屏幕参数 + - [ ] 外壳参数 + - [ ] 硬件能力 +- [ ] 实现 JSON 序列化 + - [ ] `fromJson()` + - [ ] `toJson()` +- [ ] 创建预设配置文件 + - [ ] `imx6ull-4.3.json` + - [ ] `imx6ull-7.0.json` + - [ ] `rk3568-7.0.json` + - [ ] `rk3568-10.1.json` + - [ ] `rk3588-10.1.json` + - [ ] `generic-1080p.json` +- [ ] 实现配置加载 + - [ ] 目录扫描 + - [ ] 默认配置 + +### Day 4: 控制面板 +- [ ] 创建 ControlPanel 类 + - [ ] QWidget 布局 + - [ ] 控件排列 +- [ ] 实现设备选择器 + - [ ] QComboBox + - [ ] 配置列表 +- [ ] 实现分辨率选择器 + - [ ] 预设分辨率 + - [ ] 自定义分辨率 +- [ ] 实现档位选择器 + - [ ] HWTierSelector + - [ ] 单选按钮组 + +### Day 5: 集成测试 +- [ ] 集成各模块 + - [ ] 信号连接 + - [ ] 状态同步 +- [ ] 基本功能测试 + - [ ] 配置加载 + - [ ] 窗口显示 + - [ ] 控件响应 + +--- + +## 三、Week 2: 注入与模拟 + +### Day 1-2: DPI 注入 +- [ ] 实现 DPI 注入器 + - [ ] DPIInjector 类 + - [ ] 注入接口 +- [ ] 修改 DPIManager 支持注入 + - [ ] 添加注入检查 + - [ ] 优先级处理 +- [ ] 测试不同分辨率 + - [ ] 480×272 + - [ ] 800×480 + - [ ] 1024×600 + - [ ] 1920×1200 + +### Day 3: 硬件 Mock +- [ ] 实现硬件信息 Mock + - [ ] HardwareMock 类 + - [ ] 数据注入 +- [ ] 修改 HardwareProbe 支持 Mock + - [ ] 测试模式检测 + - [ ] Mock 数据优先 +- [ ] 测试档位切换 + - [ ] Low → Mid → High + - [ ] 策略验证 + +### Day 4: 输入模拟 +- [ ] 实现输入模拟器 + - [ ] InputSimulator 类 + - [ ] 事件过滤器 +- [ ] 鼠标转触摸 + - [ ] QTouchEvent 转换 + - [ ] 多点模拟 +- [ ] 键盘转按键 + - [ ] Qt Key 映射 + - [ ] 动作触发 +- [ ] 滚轮转旋钮 + - [ ] QWheelEvent 转换 + - [ ] 步数计算 + +### Day 5: 触摸可视化 +- [ ] 实现 TouchVisualizer + - [ ] QWidget 覆盖层 + - [ ] 绘制事件 +- [ ] 涟漪动画 + - [ ] QPropertyAnimation + - [ ] 透明度渐变 +- [ ] 多点触摸支持 + - [ ] 触摸点列表 + - [ ] 独立动画 + +--- + +## 四、Week 3: 完善与优化 + +### Day 1-2: UI 完善 +- [ ] 完善设备外壳 + - [ ] 更多预设图片 + - [ ] SVG 矢量支持 +- [ ] 添加更多预设 + - [ ] 手机类型 + - [ ] 平板类型 + - [ ] 自定义尺寸 +- [ ] 美化控制面板 + - [ ] 图标支持 + - [ ] 工具提示 + +### Day 3: 高级功能 +- [ ] 截图功能 + - [ ] 整窗口截图 + - [ ] 仅内容区域 + - [ ] 保存对话框 +- [ ] 录屏功能 + - ] GIF 录制 + - [ ] 帧率配置 +- [ ] 性能监控 + - [ ] FPS 显示 + - [ ] 内存显示 + - [ ] CPU 占用 + +### Day 4: 文档 +- [ ] 使用说明 + - [ ] 快速开始 + - [ ] 功能介绍 + - [ ] 常见问题 +- [ ] API 文档 + - [ ] 类接口 + - [ ] 使用示例 +- [ ] 示例代码 + - [ ] 加载 Shell UI + - [ ] 自定义配置 + +### Day 5: 测试 +- [ ] 跨平台测试 + - [ ] Windows 10/11 + - [ ] Ubuntu 20.04+ + - [ ] macOS 12+ +- [ ] 性能测试 + - [ ] 启动时间 + - [ ] 帧率稳定性 +- [ ] 修复 Bug + - [ ] 已知问题 + - [ ] 边界情况 + +--- + +## 五、验收标准 + +### 功能验收 +- [ ] 能正确显示各种分辨率 +- [ ] 档位切换生效 +- [ ] 触摸模拟正常 +- [ ] 截图功能正常 + +### 性能验收 +- [ ] 启动时间 < 2 秒 +- [ ] 帧率稳定 60fps + +### 兼容性验收 +- [ ] Windows 10/11 正常运行 +- [ ] Ubuntu 20.04+ 正常运行 +- [ ] macOS 12+ 正常运行 + +--- + +## 六、文件清单 + +### 核心类 +- [ ] `include/CFDesktop/Simulator/SimulatorWindow.h` +- [ ] `include/CFDesktop/Simulator/DeviceFrame.h` +- [ ] `include/CFDesktop/Simulator/DeviceProfile.h` +- [ ] `include/CFDesktop/Simulator/TouchVisualizer.h` +- [ ] `include/CFDesktop/Simulator/ControlPanel.h` +- [ ] `include/CFDesktop/Simulator/HWTierSelector.h` +- [ ] `include/CFDesktop/Simulator/ResolutionPreset.h` +- [ ] `include/CFDesktop/Simulator/SimulatorInjection.h` + +### 实现 +- [ ] `src/simulator/SimulatorWindow.cpp` +- [ ] `src/simulator/DeviceFrame.cpp` +- [ ] `src/simulator/TouchVisualizer.cpp` +- [ ] `src/simulator/ControlPanel.cpp` +- [ ] `src/simulator/HWTierSelector.cpp` +- [ ] `src/simulator/ResolutionPreset.cpp` + +### 注入 +- [ ] `src/simulator/injection/DPIInjector.cpp` +- [ ] `src/simulator/injection/HardwareMock.cpp` +- [ ] `src/simulator/injection/InputSimulator.cpp` + +### 资源 +- [ ] `assets/simulator/devices/phone-generic.png` +- [ ] `assets/simulator/devices/tablet-10inch.png` +- [ ] `assets/simulator/devices/panel-7inch.png` +- [ ] `assets/simulator/devices/panel-10inch.png` +- [ ] `assets/simulator/profiles/imx6ull-4.3.json` +- [ ] `assets/simulator/profiles/imx6ull-7.0.json` +- [ ] `assets/simulator/profiles/rk3568-7.0.json` +- [ ] `assets/simulator/profiles/rk3568-10.1.json` +- [ ] `assets/simulator/profiles/rk3588-10.1.json` +- [ ] `assets/simulator/profiles/generic-1080p.json` +- [ ] `assets/simulator/effects/touch-ripple.png` + +### 测试 +- [ ] `tests/unit/simulator/test_device_profile.cpp` +- [ ] `tests/unit/simulator/test_device_frame.cpp` +- [ ] `tests/unit/simulator/test_touch_visualizer.cpp` + +--- + +## 七、相关文档 + +- 设计文档: [../../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) + +--- + +*最后更新: 2026-03-05* diff --git a/document/todo/base/04_testing.md b/document/todo/base/04_testing.md index d2fe2c7b8..4f4fcd6ea 100644 --- a/document/todo/base/04_testing.md +++ b/document/todo/base/04_testing.md @@ -1,3 +1,8 @@ +--- +title: "Phase 8: 测试体系 TODO" +description: "预计周期: 贯穿全程,依赖阶段: 所有阶段" +--- + # Phase 8: 测试体系 TODO > **状态**: 🚧 进行中 @@ -26,7 +31,7 @@ ## 二、测试金字塔 -``` +```text /\ / \ / E2E \ @@ -47,7 +52,7 @@ / 20% 集成 \ / 10% UI \ /____________________________________\ -``` +```yaml --- diff --git a/document/todo/base/99_ui_material_framework.md b/document/todo/base/99_ui_material_framework.md index c00124c63..4cb50321c 100644 --- a/document/todo/base/99_ui_material_framework.md +++ b/document/todo/base/99_ui_material_framework.md @@ -1,3 +1,8 @@ +--- +title: UI Material Framework TODO +description: "状态: ⬜ 高级控件待实现,预计周期: 持续迭代" +--- + # UI Material Framework TODO > **状态**: ⬜ 高级控件待实现 @@ -18,14 +23,14 @@ ## 一、架构概览 ### 分层架构 -``` +```text Layer 6: Qt Native Widgets (QPushButton, QLineEdit, ...) Layer 5: Material Widget Adapter (Button, TextField, ...) Layer 4: Material Behavior Layer (StateMachine, Ripple, ...) Layer 3: Animation Engine Layer (TimingAnimation, SpringAnimation, ...) Layer 2: Theme Engine Layer (ThemeManager, ICFColorScheme, ...) Layer 1: Core Math & Utility Layer (math_helper, color, geometry, ...) -``` +```cpp ### 核心约束(RULE-01 至 RULE-09) - [ ] RULE-01: 所有 Material 控件必须继承 Qt 原生控件 diff --git a/document/todo/base/index.md b/document/todo/base/index.md index 166eabd94..6dd9379fb 100644 --- a/document/todo/base/index.md +++ b/document/todo/base/index.md @@ -1,10 +1,11 @@ -# base - -> Welcome to the base section. +--- +title: 基础库任务 +description: 本目录记录 基础库模块的待办事项与开发里程碑,涵盖硬件探测(CPU、GPU、Memory、Netw +--- -## Overview +# 基础库任务 -Documentation and resources for base. +本目录记录 `base/` 基础库模块的待办事项与开发里程碑,涵盖硬件探测(CPU、GPU、Memory、Network)、策略链(Policy Chain)、控制台设备抽象以及头文件工具库等组件的规划与进度追踪。 --- diff --git a/document/todo/desktop/.pages b/document/todo/desktop/.pages deleted file mode 100644 index 4b946e033..000000000 --- a/document/todo/desktop/.pages +++ /dev/null @@ -1,19 +0,0 @@ -title: Desktop TODO -nav: - - 总览: summary.md - - 里程碑总览: milestone_00_overview.md - - 里程碑 01 · 骨架: milestone_01_desktop_skeleton.md - - 里程碑 02 · 状态栏: milestone_02_status_bar.md - - 里程碑 03 · 任务栏: milestone_03_taskbar.md - - 里程碑 04 · 启动器: milestone_04_app_launcher.md - - 里程碑 05 · 窗口管理: milestone_05_window_management.md - - 里程碑 06 · 控制中心: milestone_06_widget_control_center.md - - 基础设施: 06_infrastructure.md - - 渲染后端: 07_render_backend.md - - P1 控件: 08_p1_controls.md - - 窗口管理器: 09_window_manager.md - - Shell 导航: 10_shell_navigation.md - - 通知控件: 11_notification_control.md - - 主题/锁屏: 12_theme_settings_lockscreen.md - - 小部件应用: 13_widget_apps.md - - 崩溃处理: CRASH_HANDLE_STEP.md diff --git a/document/todo/desktop/06_infrastructure.md b/document/todo/desktop/06_infrastructure.md index 6307a2d4e..43267cd39 100644 --- a/document/todo/desktop/06_infrastructure.md +++ b/document/todo/desktop/06_infrastructure.md @@ -1,3 +1,8 @@ +--- +title: "Phase 6: 基础设施补全 TODO" +description: "状态: 🚧 部分完成 (~50%),预计周期: 4~5 周" +--- + # Phase 6: 基础设施补全 TODO > **状态**: 🚧 部分完成 (~50%) diff --git a/document/todo/desktop/07_render_backend.md b/document/todo/desktop/07_render_backend.md index 6f4e368df..eb57e52d4 100644 --- a/document/todo/desktop/07_render_backend.md +++ b/document/todo/desktop/07_render_backend.md @@ -1,3 +1,8 @@ +--- +title: "Phase 7: 渲染后端抽象 TODO" +description: "状态: 🚧 部分完成 (接口已实现,具体后端待开发)" +--- + # Phase 7: 渲染后端抽象 TODO > **状态**: 🚧 部分完成 (接口已实现,具体后端待开发) diff --git a/document/todo/desktop/08_p1_controls.md b/document/todo/desktop/08_p1_controls.md index f8aa6d52f..5114276c8 100644 --- a/document/todo/desktop/08_p1_controls.md +++ b/document/todo/desktop/08_p1_controls.md @@ -1,3 +1,8 @@ +--- +title: "Phase 8: P1 控件补全 TODO" +description: "完成日期: 2026-03-18,详细完成状态: done/13widgetappsstatus.m" +--- + # Phase 8: P1 控件补全 TODO > **状态**: ✅ 已完成 diff --git a/document/todo/desktop/09_window_manager.md b/document/todo/desktop/09_window_manager.md index 595a600fa..ab029fe13 100644 --- a/document/todo/desktop/09_window_manager.md +++ b/document/todo/desktop/09_window_manager.md @@ -1,3 +1,8 @@ +--- +title: "Phase 9: 窗口管理器 TODO" +description: "状态: 🚧 部分完成,依赖阶段: Phase 6, Phase 7" +--- + # Phase 9: 窗口管理器 TODO > **状态**: 🚧 部分完成 diff --git a/document/todo/desktop/10_shell_navigation.md b/document/todo/desktop/10_shell_navigation.md index 880757846..0c6e9d737 100644 --- a/document/todo/desktop/10_shell_navigation.md +++ b/document/todo/desktop/10_shell_navigation.md @@ -1,3 +1,8 @@ +--- +title: "Phase 10: Shell 导航系统 TODO" +description: "依赖阶段: Phase 6, Phase 9" +--- + # Phase 10: Shell 导航系统 TODO > **状态**: ⬜ 待开始 diff --git a/document/todo/desktop/11_notification_control.md b/document/todo/desktop/11_notification_control.md index 2b860d808..958b1f425 100644 --- a/document/todo/desktop/11_notification_control.md +++ b/document/todo/desktop/11_notification_control.md @@ -1,3 +1,8 @@ +--- +title: "Phase 11: 通知与控制中心 TODO" +description: "预计周期: 2~3 周,依赖阶段: Phase 6, Phase 10" +--- + # Phase 11: 通知与控制中心 TODO > **状态**: ⬜ 待开始 diff --git a/document/todo/desktop/12_theme_settings_lockscreen.md b/document/todo/desktop/12_theme_settings_lockscreen.md index 5370a45ad..b2defc66e 100644 --- a/document/todo/desktop/12_theme_settings_lockscreen.md +++ b/document/todo/desktop/12_theme_settings_lockscreen.md @@ -1,3 +1,8 @@ +--- +title: "Phase 12: 主题、设置与锁屏 TODO" +description: "预计周期: 4~5 周,依赖阶段: Phase 6, Phase 10, Phase 11" +--- + # Phase 12: 主题、设置与锁屏 TODO > **状态**: ⬜ 待开始 diff --git a/document/todo/desktop/13_widget_apps.md b/document/todo/desktop/13_widget_apps.md index 8a9f5e3d7..9aa65e093 100644 --- a/document/todo/desktop/13_widget_apps.md +++ b/document/todo/desktop/13_widget_apps.md @@ -1,3 +1,8 @@ +--- +title: "Phase 13: Widget 与桌面应用 TODO" +description: "预计周期: 4~5 周,依赖阶段: Phase 9, Phase 12" +--- + # Phase 13: Widget 与桌面应用 TODO > **状态**: ⬜ 待开始 diff --git a/document/todo/desktop/CRASH_HANDLE_STEP.md b/document/todo/desktop/CRASH_HANDLE_STEP.md index 25c5a763b..395fb8b2f 100644 --- a/document/todo/desktop/CRASH_HANDLE_STEP.md +++ b/document/todo/desktop/CRASH_HANDLE_STEP.md @@ -1,3 +1,8 @@ +--- +title: "CrashHandler 崩溃处理与自动重启 - 基建就绪度评估" +description: "评估日期:2026-03-30,整体基建就绪度约 60%,核心崩溃捕获可直接开始,完整功能(Cras" +--- + # CrashHandler 崩溃处理与自动重启 - 基建就绪度评估 > 评估日期:2026-03-30 diff --git a/document/todo/desktop/index.md b/document/todo/desktop/index.md index b293efb5c..e904ac574 100644 --- a/document/todo/desktop/index.md +++ b/document/todo/desktop/index.md @@ -1,3 +1,8 @@ +--- +title: desktop +description: 桌面本体 (Desktop Shell) 开发规划 +--- + # desktop > 桌面本体 (Desktop Shell) 开发规划 diff --git a/document/todo/desktop/milestone_00_overview.md b/document/todo/desktop/milestone_00_overview.md index 9decb91ba..9ac74fa0d 100644 --- a/document/todo/desktop/milestone_00_overview.md +++ b/document/todo/desktop/milestone_00_overview.md @@ -1,3 +1,8 @@ +--- +title: "里程碑总览: 从空白桌面到可见可用" +description: "创建日期: 2026-03-31,目标: 将当前空白桌面推进到用户可见、可交互的状态" +--- + # 里程碑总览: 从空白桌面到可见可用 > **创建日期**: 2026-03-31 @@ -20,7 +25,7 @@ ## 依赖关系 -``` +```text MS1: 桌面骨架 (壁纸+布局) ✅ 已完成 │ ├──→ MS2: 状态栏 (时间+图标) @@ -32,7 +37,7 @@ MS1: 桌面骨架 (壁纸+布局) ✅ 已完成 │ │ └──→ MS5: 窗口管理可见 │ │ │ └──→ MS6: 小组件+控制中心 (可与 MS3/4/5 并行) -``` +```bash **关键路径 (最快通路)**: MS1 → MS2 → MS3 → MS4 → MS5 diff --git a/document/todo/desktop/milestone_02_status_bar.md b/document/todo/desktop/milestone_02_status_bar.md index f64d96536..6c34bdb1c 100644 --- a/document/todo/desktop/milestone_02_status_bar.md +++ b/document/todo/desktop/milestone_02_status_bar.md @@ -1,3 +1,8 @@ +--- +title: "Milestone 2: 状态栏" +description: "预计周期: 3-5 天,前置依赖: Milestone 1: 桌面骨架可见" +--- + # Milestone 2: 状态栏 > **状态**: ⬜ 待开始 diff --git a/document/todo/desktop/milestone_03_taskbar.md b/document/todo/desktop/milestone_03_taskbar.md index 38d2e0990..48d17d294 100644 --- a/document/todo/desktop/milestone_03_taskbar.md +++ b/document/todo/desktop/milestone_03_taskbar.md @@ -1,3 +1,8 @@ +--- +title: "Milestone 3: 任务栏/导航栏" +description: "预计周期: 5-7 天,前置依赖: Milestone 2: 状态栏" +--- + # Milestone 3: 任务栏/导航栏 > **状态**: ⬜ 待开始 diff --git a/document/todo/desktop/milestone_04_app_launcher.md b/document/todo/desktop/milestone_04_app_launcher.md index d5f42ba40..e83e73485 100644 --- a/document/todo/desktop/milestone_04_app_launcher.md +++ b/document/todo/desktop/milestone_04_app_launcher.md @@ -1,3 +1,8 @@ +--- +title: "Milestone 4: 应用启动器" +description: "预计周期: 5-7 天,前置依赖: Milestone 3: 任务栏" +--- + # Milestone 4: 应用启动器 > **状态**: ⬜ 待开始 diff --git a/document/todo/desktop/milestone_05_window_management.md b/document/todo/desktop/milestone_05_window_management.md index b9b30ddd4..7800d6f40 100644 --- a/document/todo/desktop/milestone_05_window_management.md +++ b/document/todo/desktop/milestone_05_window_management.md @@ -1,3 +1,8 @@ +--- +title: "Milestone 5: 窗口管理可见" +description: "预计周期: 7-10 天,前置依赖: Milestone 3: 任务栏 (任务栏联动)" +--- + # Milestone 5: 窗口管理可见 > **状态**: ⬜ 待开始 diff --git a/document/todo/desktop/milestone_06_widget_control_center.md b/document/todo/desktop/milestone_06_widget_control_center.md index 9efb2759a..9325659fa 100644 --- a/document/todo/desktop/milestone_06_widget_control_center.md +++ b/document/todo/desktop/milestone_06_widget_control_center.md @@ -1,3 +1,8 @@ +--- +title: "Milestone 6: 小组件 + 控制中心" +description: "预计周期: 7-10 天,前置依赖: Milestone 2: 状态栏 (下拉触发点)" +--- + # Milestone 6: 小组件 + 控制中心 > **状态**: ⬜ 待开始 diff --git a/document/todo/desktop/summary.md b/document/todo/desktop/summary.md index e1e74e3d7..ab92f6519 100644 --- a/document/todo/desktop/summary.md +++ b/document/todo/desktop/summary.md @@ -1,694 +1,699 @@ -# CFDesktop 桌面本体规划文档 - ---- - -## 一、项目背景与现状 - -### 1.1 项目定性 - -CFDesktop 是一个基于 **Qt 6.8.3+ / C++23** 开发的**嵌入式桌面 UI 框架**,遵循 Material Design 3 规范。当前版本(0.13.1)本质上是一个**UI 组件库 + 底层架构**,尚不构成完整的桌面环境。 - -本文档的目标是在现有基础上,规划**桌面本体(Desktop Shell)**的完整建设路径。 - -### 1.2 当前已完成的基础(可复用) - -| 模块 | 完成度 | 可复用价值 | -|------|--------|-----------| -| 工程骨架(CMake/CI/CD) | 100% | ✅ 直接复用 | -| Material Design 3 Token 系统 | 100% | ✅ 主题系统基础 | -| ThemeEngine 主题管理 | 100% | ✅ 桌面主题切换核心 | -| AnimationManager 动画引擎 | 100% | ✅ 桌面过渡动效基础 | -| DPI 管理 | 100% | ✅ 多分辨率屏幕适配 | -| 19个 P0+P1 控件 | 100% | ✅ 桌面 Shell UI 基础 | -| Google Test 测试框架 | 100% | ✅ 持续集成基础 | -| CPU/内存/GPU/网络检测器 | 90% | ✅ 硬件探针基本完成 | -| ConfigStore 配置中心 | 100% | ✅ 四层存储,INI持久化 | -| Logger 日志系统 | 100% | ✅ 异步日志,多Sink | -| Windows 显示后端 | 100% | ✅ Win32 DWM + HWND | -| WSL X11 显示后端 | 100% | ✅ XCB + XWayland | -| 显示服务器抽象 (IDisplayServerBackend) | 100% | ✅ 三模式抽象 | -| 窗口管理基础 (WindowManager/PanelManager) | 80% | ✅ 基础注册/查询/弱引用 | - -### 1.3 当前关键缺口(桌面本体的前置依赖) - -| 缺口模块 | 当前进度 | 对桌面本体的影响 | -|----------|--------|----------------| -| 输入抽象层(InputManager) | 0% | 🔴 所有交互的根基,必须最先完成 | -| HWTier 硬件分级系统 | 0% | 🔴 性能自适应的核心判据 | -| CrashHandler 崩溃处理 | 0% | 🟡 稳定性保障 | -| IPC 进程间通信 | 0% | 🟡 多进程架构依赖 | -| P2 高级控件(27个) | 0% | 🟡 文件管理器等应用依赖 | - ---- - -## 二、需求定义 - -### 2.1 目标用户与场景 - -**主要面向**: -- 开源社区与个人开发者 - -**典型使用场景**: -- 嵌入式 Linux 设备上运行一个完整的桌面环境 -- 开发者在 Windows 上开发调试,部署到 Linux 嵌入式设备 -- 设备厂商基于 CFDesktop 定制自己品牌的桌面风格 - -### 2.2 目标硬件规格 - -| 档位 | 硬件规格 | 行为策略 | -|------|---------|---------| -| Low Tier | 无独立 GPU,≤512MB RAM,ARM Cortex-A5/A7 | 关闭动效、简化阴影、降低合成层数 | -| Mid Tier | 弱 GPU,1-2GB RAM,ARM Cortex-A53/A55 | 基础动效开启,限制并发动画数量 | -| High Tier | 独立 GPU,≥4GB RAM,ARM Cortex-A72/A76+ | 全量动效、模糊/透明效果全开 | - -> **HWTier 系统是整个性能自适应的核心判据**,必须在桌面本体启动前完成探测并注入全局配置。 - -### 2.3 屏幕与输入规格 - -- **屏幕尺寸**:5.5 ~ 10.1 英寸矩形触摸屏 -- **分辨率**:变化范围较大,依赖现有 DPI 管理模块动态适配 -- **输入方式**:触摸(单点/多点手势)+ 鼠标键盘(开发调试/PC风格主题) -- **操作系统**:Linux 主力部署;Windows 作为开发调试等价环境 - -### 2.4 渲染后端策略 - -采用**多后端抽象层**,运行时按环境选择: - -``` -渲染后端抽象层(RenderBackend Interface) - ├── EGLFS / LinuxFB → 嵌入式 Linux 直驱(主要部署环境) - ├── Wayland Client → 跑在现有 Wayland 合成器之上 - ├── X11 → 旧版 Linux 桌面兼容 - └── Windows (Win32) → 开发调试等价环境 -``` - -> 不自研 Wayland Compositor,优先保证嵌入式 EGLFS 直驱路径的稳定性。 - -### 2.5 应用模型 - -- **多应用自由切换**(类手机/PC 体验)为主,同时支持 Kiosk 全屏模式配置 -- **进程隔离**:每个 App 独立进程,桌面 Shell 作为宿主进程(Compositor Process) -- **系统服务**:核心服务单进程(通知/窗口管理/输入路由),可选服务(媒体控制/设置服务)独立进程,服务以**插件式**形式挂载 -- **IPC 机制**:优先使用 Qt 自带机制(QLocalSocket / Shared Memory),如复杂度不可控再迁移 D-Bus - -### 2.6 商业模式 - -**MIT 开源协议**,无商业授权限制。 - ---- - -## 三、桌面本体功能定义 - -### 3.1 核心子系统总览 - -``` -CFDesktop Shell -├── 主题风格系统(Theme Style System) -│ ├── iOS 风格主题包 -│ └── Windows 11 风格主题包 -├── 窗口管理器(Window Manager) -├── 任务栏 / 导航系统(Shell Navigation) -├── 应用启动器(App Launcher) -├── 通知系统(Notification System) -├── 快捷控制中心(Control Center) -├── 系统设置(System Settings App) -├── 锁屏模块(Lock Screen) -├── 桌面壁纸与 Widget 系统(Desktop Layer) -├── 文件管理器(File Manager App) -├── 媒体控制服务(Media Control Service) -└── 硬件性能自适应引擎(HWTier Adaptive Engine) -``` - -### 3.2 双主题风格系统(核心特色) - -CFDesktop 的核心差异化特性:**支持整体主题包切换 + 组件级微调**。 - -#### iOS 风格主题包 - -| 特性 | 描述 | -|------|------| -| 导航范式 | 底部手势条(类 iOS Home Indicator)+ 底部 Tab Bar | -| 启动器 | 网格图标桌面(类 iOS 主屏),支持图标拖拽排列 | -| 窗口过渡 | 全屏滑入/滑出(类 iOS App 打开/关闭动效) | -| 任务切换 | 上滑调出卡片式任务切换器(类 iOS App Switcher) | -| 控制中心 | 右上角下滑展开圆角磁贴面板 | -| 通知 | 顶部下滑展开通知列表 | -| 字体/圆角 | 大圆角卡片,SF Pro 风格字体层级 | -| 动效风格 | 弹簧动效(Spring Animation),响应灵敏 | - -#### Windows 11 风格主题包 - -| 特性 | 描述 | -|------|------| -| 导航范式 | 底部居中任务栏(类 Win11 Centered Taskbar) | -| 启动器 | 点击任务栏图标展开开始菜单(类 Win11 Start Menu) | -| 窗口过渡 | 缩放淡入淡出(类 Win11 窗口动效) | -| 任务切换 | 任务栏悬停预览缩略图 + Task View 全览 | -| 控制中心 | 右下角系统托盘区点击展开快捷面板(类 Win11 Quick Settings) | -| 通知 | 右侧滑出通知中心面板(类 Win11 Action Center) | -| 字体/圆角 | 中圆角,Segoe UI 风格字体层级 | -| 动效风格 | 时间曲线动效(Ease In/Out),流畅稳重 | - -#### 主题切换架构 - -``` -ThemeStyleManager - ├── loadThemePack(ThemePack::iOS / ThemePack::Windows) - ├── 注入 ThemeEngine(颜色 Token) - ├── 注入 NavigationPolicy(导航范式) - ├── 注入 AnimationPolicy(动效策略) - ├── 注入 LayoutPolicy(圆角/间距/字体) - └── 触发 Shell 重新布局 -``` - -> 主题包切换是**运行时热切换**,无需重启桌面进程。 - -### 3.3 窗口管理器 - -- **浮动窗口模式**(可拖拽/缩放,Windows 风格主题默认) -- **全屏平铺模式**(iOS 风格主题默认) -- **分屏模式**:左右/上下固定分割 -- **窗口层级管理**:Z-order 管理,支持 Always On Top -- **窗口动效**:开启/关闭/最小化/恢复均有主题对应动效 -- **多显示器**:基础支持(后期扩展) - -### 3.4 Shell 导航系统 - -导航组件根据当前主题包**动态切换形态**,不硬编码: - -``` -NavigationPolicy Interface - ├── iOS Policy → BottomGestureBar + BottomTabBar - └── Windows Policy → CenteredTaskbar + SystemTray -``` - -公共元素(跨主题): -- 顶部状态栏(时间、网络、电量、通知角标) -- 应用标题区(返回按钮/窗口控制按钮随主题变化) - -### 3.5 通知系统 - -对标 iOS 通知中心 + Windows Action Center,要求**完善**: - -``` -NotificationService(独立进程) - ├── 通知接收 API(App 调用) - ├── 通知持久化存储(ConfigStore 依赖) - ├── 分组/折叠(按 App 归组) - ├── 优先级分级(Critical / Normal / Silent) - ├── 横幅弹窗(Banner,自动消失) - ├── 通知中心面板(下拉/侧滑展开,可清除) - ├── 角标计数(状态栏图标角标) - └── 勿扰模式(Do Not Disturb) -``` - -### 3.6 快捷控制中心 - -对标 iOS Control Center + Windows Quick Settings: - -- 亮度滑条 -- 音量滑条 -- WiFi / 蓝牙 快捷开关 -- 截图 -- 勿扰模式切换 -- 主题风格快速切换入口 -- 自定义快捷瓦片(开发者可注册) - -### 3.7 系统设置 App - -对标 KDE System Settings + Windows 设置,内置完整设置 App: - -| 分类 | 设置项 | -|------|--------| -| 显示 | 分辨率、亮度、夜间模式、缩放比例 | -| 声音 | 音量、输出设备、提示音 | -| 网络 | WiFi 列表、IP 配置、代理 | -| 蓝牙 | 设备配对管理 | -| 桌面 | 主题包切换、壁纸、动效开关 | -| 输入 | 触摸灵敏度、鼠标速度、键盘布局 | -| 应用 | 已安装应用列表、权限管理、默认应用 | -| 语言 | 语言/地区/时区(框架预留,后期实现) | -| 辅助 | 字体大小缩放、高对比度(后期实现) | -| 系统 | 关于本机、硬件信息(HWTier 展示)、日志查看 | - -### 3.8 锁屏模块 - -- **PIN / 密码 / 图案**三种解锁方式 -- 锁屏壁纸(独立于桌面壁纸,或继承) -- 锁屏通知展示(仅 Normal 及以上级别) -- 息屏超时自动锁屏(可配置时长) -- 锁屏时媒体控制卡片(正在播放时展示) - -### 3.9 桌面壁纸 + Widget 系统 - -**壁纸**: -- 静态壁纸(Low Tier 及以上) -- 动态壁纸(High Tier,视频/Lottie 动画,Mid/Low 自动降级为静态) -- 主题联动:主题包切换时自动推荐配套壁纸 - -**桌面 Widget(小组件)**: -- 内置 Widget:时钟、日历、天气、系统资源监控 -- Widget 框架:开发者可注册自定义 Widget(提供 Widget API) -- Widget 编辑模式(长按进入,支持拖拽/缩放) -- iOS 风格:Widget 与图标共存于网格桌面 -- Windows 风格:Widget 区域独立(类 Win11 Widgets Panel) - -### 3.10 文件管理器 App - -对标 Windows 资源管理器,内置完整文件管理器: - -- 目录树导航(左侧面板) -- 文件列表视图(图标/列表/详情切换) -- 基础文件操作(复制/剪切/粘贴/删除/重命名) -- 文件搜索 -- 压缩包浏览(后期) -- 文件选择器对话框(供应用调用的系统级 API) - -### 3.11 媒体控制服务 - -- 系统级媒体会话管理(跨 App 统一控制) -- 状态栏/锁屏媒体卡片(播放/暂停/上下曲) -- 快捷控制中心媒体区块 -- 音量控制与输出设备切换 -- 蓝牙音频设备快速切换(依赖蓝牙模块) - ---- - -## 四、架构分层设计 - -``` -┌─────────────────────────────────────────────────────────┐ -│ Layer 7: Apps Layer │ -│ 文件管理器 / 系统设置 / 内置 Widget App │ -├─────────────────────────────────────────────────────────┤ -│ Layer 6: Shell Layer │ -│ 任务栏 / 启动器 / 通知中心 / 控制中心 / 锁屏 / 桌面层 │ -├─────────────────────────────────────────────────────────┤ -│ Layer 5: Theme Style System │ -│ iOS主题包 / Windows主题包 / ThemeStyleManager │ -├─────────────────────────────────────────────────────────┤ -│ Layer 4: Window Manager │ -│ 浮动/全屏/分屏 / Z-order / 窗口动效 │ -├─────────────────────────────────────────────────────────┤ -│ Layer 3: System Services │ -│ NotificationService / MediaService / InputRouter │ -├─────────────────────────────────────────────────────────┤ -│ Layer 2: Base Infrastructure │ -│ InputManager / ConfigStore / Logger / IPC │ -├─────────────────────────────────────────────────────────┤ -│ Layer 1: Hardware Abstraction │ -│ HardwareProbe / HWTier / RenderBackend │ -├─────────────────────────────────────────────────────────┤ -│ Layer 0: Already Completed │ -│ ThemeEngine / AnimationManager / DPI / P0 Controls │ -└─────────────────────────────────────────────────────────┘ -``` - -**设计原则**: -- 每层只依赖下层,严禁跨层调用 -- Layer 5(主题风格系统)横切 Layer 4-6,通过 Policy 注入而非直接调用 -- 系统服务通过 IPC 与 Shell 通信,Shell 不直接持有服务实现 - ---- - -## 五、开发路线图 - -> **节奏定义**:不赶时间,架构分层优先,质量优先,每个 Phase 完成后需通过完整测试才可进入下一 Phase。 - ---- - -### Phase A:基础设施补全(前置必做) -> 对应现有 Phase 1/2 的未完成部分,是桌面本体的地基 - -**A1 — 硬件探针完善** -- GPU 检测器(DRM 设备枚举、OpenGL 上下文探测) -- HWTier 枚举定义(Low / Mid / High) -- HardwareProbe 主类(整合 CPU + 内存 + GPU 探测) -- CapabilityPolicy 策略引擎(各档位默认配置集) -- 单元测试覆盖 - -**A2 — ConfigStore 配置中心** -- 三层存储模型:系统默认 → 用户配置 → 运行时覆写 -- 变更监听机制(Observer 模式) -- 持久化序列化(JSON / QSettings 后端) -- 线程安全访问 -- 单元测试覆盖 - -**A3 — Logger 日志系统** -- 多 Sink 支持(文件 / 终端 / 系统日志) -- 日志级别(Debug / Info / Warning / Error / Fatal) -- 日志轮转(大小/时间) -- 结构化日志(JSON 格式可选) -- 单元测试覆盖 - -**A4 — 输入抽象层** -- InputManager 统一分发层 -- TouchInputHandler(单点 + 多点手势识别) -- KeyInputHandler(键盘/快捷键) -- MouseInputHandler(鼠标 + 滚轮) -- GestureRecognizer(Tap / LongPress / Swipe / Pinch / Pan) -- FocusNavigator(焦点导航系统) -- 单元测试 + UI 自动化测试框架搭建 - -**A5 — IPC 基础层** -- QLocalSocket 封装(进程间消息信道) -- 消息序列化协议定义 -- 服务注册/发现机制(ServiceLocator) -- 基础测试覆盖 - -**里程碑验收标准**: -- HWTier 可在目标设备上正确探测并输出档位 -- ConfigStore 可持久化读写并触发变更通知 -- 触摸/鼠标/键盘输入均可通过 InputManager 统一路由 -- 两个进程可通过 IPC 层互相发送消息 - ---- - -### Phase B:渲染后端抽象层 -> 解决"同一代码跑在 EGLFS / Wayland / Windows"的问题 - -**B1 — RenderBackend 接口定义** -- RenderBackend 抽象接口(初始化/交换缓冲/截图/VSync) -- 后端注册机制(运行时按环境选择) - -**B2 — EGLFS 后端** -- Qt EGLFS 封装 -- HWTier 联动:High Tier 开启 OpenGL ES 合成,Low Tier 降级 LinuxFB - -**B3 — Windows 等价后端** -- Qt Windows 后端封装 -- 开发调试辅助工具:屏幕尺寸模拟(5.5" / 8" / 10.1" 三档快速切换) - -**B4 — Wayland/X11 后端** -- 跑在现有 Wayland/X11 合成器之上的支持(次优先) - -**里程碑验收标准**: -- 同一 Shell 代码在 Linux EGLFS 和 Windows 上均可启动 -- 屏幕尺寸模拟器可在 Windows 上精确还原嵌入式屏幕效果 - ---- - -### Phase C:P1 控件补全 ✅ 已完成 (2026-03-18) -> Shell UI 所需的基础控件,与 Phase A/B 并行推进 - -**已实现控件**(12个): - -| 优先级 | 控件 | 桌面依赖场景 | 状态 | -|--------|------|------------|------| -| P1-1 | Slider | 亮度/音量控制 | ✅ | -| P1-2 | Switch | 设置开关项 | ✅ | -| P1-3 | ProgressBar | 下载/加载状态 | ✅ | -| P1-4 | TabView | 设置分类导航 | ✅ | -| P1-5 | ComboBox | 设置下拉选项 | ✅ | -| P1-6 | ListView | 列表显示 | ✅ | -| P1-7 | TableView | 表格显示 | ✅ | -| P1-8 | TreeView | 树形显示 | ✅ | -| P1-9 | ScrollView | 滚动视图 | ✅ | -| P1-10 | Separator | 分隔线 | ✅ | -| P1-11 | SpinBox | 整数输入 | ✅ | -| P1-12 | DoubleSpinBox | 浮点输入 | ✅ | - -**待实现 P2 控件**(文件管理器等依赖): -- ToolBar / ToolButton -- MenuBar / ContextMenu -- StatusBar -- Dialog / Card / Snackbar -- Tooltip / Popover -- ... - ---- - -### Phase D:窗口管理器 -> Shell 核心,建立在 Phase A/B 完成之后 - -**D1 — 窗口模型** -- WindowInfo(标题/图标/PID/状态/几何/层级) -- 窗口状态机(Normal / Maximized / Minimized / Fullscreen / Closing) -- Z-order 管理(层级排序) - -**D2 — 布局策略** -- FullscreenPolicy(iOS 风格默认) -- FloatingPolicy(Windows 风格默认,支持拖拽/缩放) -- SplitPolicy(左右/上下分屏) -- 策略运行时切换(随主题包切换自动切换默认策略) - -**D3 — 窗口动效** -- 开启动效(按主题包:iOS 弹入 / Win 缩放淡入) -- 关闭动效 -- 最小化/恢复动效 -- HWTier 联动:Low Tier 关闭窗口动效 - -**D4 — 任务切换** -- iOS 风格:上滑卡片式 App Switcher(缩略图预览) -- Windows 风格:Task View 全览 + 任务栏悬停预览 - -**里程碑验收标准**: -- 可在桌面上打开/关闭/切换至少 3 个并发应用窗口 -- iOS/Windows 两种布局策略均可正常运作 -- 窗口动效在 Mid/High Tier 流畅,Low Tier 自动关闭 - ---- - -### Phase E:Shell 导航 + 任务栏 -> 桌面的"骨架",用户每次使用都会接触 - -**E1 — 状态栏(Status Bar)** -- 时间/日期显示 -- 网络状态图标 -- 电量图标(如有硬件支持) -- 通知角标区 -- 主题联动(iOS:居中时间;Windows:左侧时间 + 右侧系统托盘) - -**E2 — iOS 风格导航** -- 底部手势条(Home Indicator) -- 手势识别:上滑回主屏 / 上滑停留进 App Switcher / 左右滑切换应用 -- 底部 Tab Bar(可配置应用快捷入口) - -**E3 — Windows 风格导航** -- 居中任务栏(固定应用图标 + 运行中应用) -- 系统托盘区(时间/通知图标/媒体控制入口) -- 开始按钮(触发 App Launcher) - -**E4 — 应用启动器** -- iOS 风格:网格图标桌面(图标拖拽/文件夹/多页滑动) -- Windows 风格:开始菜单(最近使用 + 固定应用 + 全部应用列表) -- 全局搜索(应用/文件/设置 统一搜索入口) - -**里程碑验收标准**: -- 两种主题的导航体系均可独立运作 -- 主题包切换后导航形态热切换无需重启 -- 应用启动器可浏览和启动所有已安装应用 - ---- - -### Phase F:通知系统 + 控制中心 -> 完善程度对标 iOS / Windows 11 - -**F1 — NotificationService(独立进程)** -- 通知接收 API(Qt IPC) -- 通知持久化(ConfigStore 依赖) -- 优先级分级(Critical / Normal / Silent) -- 勿扰模式 - -**F2 — 横幅弹窗(Banner)** -- 顶部滑入展示,3秒自动消失(可配置) -- 可展开操作按钮(最多2个快捷操作) -- HWTier:Low Tier 仅显示文字,关闭展开动效 - -**F3 — 通知中心面板** -- iOS 风格:顶部下拉展开 -- Windows 风格:右侧滑出面板 -- 分组折叠(按 App) -- 全部清除 / 单条清除 - -**F4 — 快捷控制中心** -- 亮度/音量滑条 -- WiFi / 蓝牙 / 勿扰 / 截图 快捷开关 -- 媒体控制卡片(当有音乐播放时展示) -- 主题切换入口 -- 自定义瓦片注册 API - -**里程碑验收标准**: -- 任意进程可通过 API 发送通知并在桌面正常展示 -- 通知中心可展示、分组、清除通知 -- 控制中心亮度/音量调节生效 - ---- - -### Phase G:主题风格系统完整实现 -> 双主题包的完整实现与热切换 - -**G1 — ThemeStyleManager 核心** -- ThemePack 数据结构定义(颜色/圆角/间距/动效/布局策略 完整集) -- 主题包加载/卸载机制 -- 热切换协调器(通知所有订阅方) -- 主题包文件格式定义(JSON + 资源文件) - -**G2 — iOS 主题包** -- 完整颜色 Token 集(对标 iOS 17 Human Interface Guidelines) -- 布局策略注入(NavigationPolicy / WindowPolicy) -- 动效策略注入(Spring Animation 参数集) -- 图标风格(圆角矩形图标,带阴影) -- 壁纸默认集(3-5 张高质量默认壁纸) - -**G3 — Windows 11 主题包** -- 完整颜色 Token 集(对标 Windows 11 Fluent Design) -- 布局策略注入 -- 动效策略注入(Ease 曲线参数集) -- Mica/Acrylic 效果(High Tier 开启,模糊半透明) -- 壁纸默认集(3-5 张) - -**G4 — 组件级微调 API** -- 每个 P0/P1 控件暴露 StyleOverride 接口 -- 用户可在主题包基础上微调单个控件样式 -- 微调配置持久化(ConfigStore) - -**里程碑验收标准**: -- iOS / Windows 两套主题包均完整可用 -- 运行时热切换主题无崩溃、无视觉撕裂 -- 动效在各 HWTier 按策略正确降级 - ---- - -### Phase H:系统设置 App - -对标 KDE System Settings + Windows 设置(参见 3.7 节功能定义) - -**开发要点**: -- 作为独立进程启动,通过 IPC 调用各系统服务 -- 设置项变更实时生效(不需要重启) -- 设置界面本身也遵循当前主题风格 -- 搜索功能:可在所有设置项中模糊搜索 - ---- - -### Phase I:锁屏模块 - -参见 3.8 节功能定义 - -**开发要点**: -- 锁屏运行在最高 Z-order 层,不可被其他窗口覆盖 -- 解锁验证逻辑与凭证存储安全隔离 -- 锁屏时输入路由只处理解锁相关输入 -- 息屏策略与 HWTier 联动(Low Tier 更激进的息屏超时) - ---- - -### Phase J:桌面 Widget + 壁纸系统 - -参见 3.9 节功能定义 - -**开发要点**: -- Widget 沙箱机制(Widget 崩溃不影响 Shell) -- Widget API 设计需兼顾未来第三方开发者接入 -- 动态壁纸严格限制 High Tier 才启用,防止低端设备性能问题 - ---- - -### Phase K:文件管理器 App - -参见 3.10 节功能定义 - -**开发要点**: -- 依赖 P1 控件:ToolBar / ToolButton / StatusBar / ContextMenu -- 文件选择器对话框作为**系统级 API** 独立封装,所有 App 均可调用 -- 支持主题联动(iOS/Windows 风格自动切换界面布局) - ---- - -### Phase L:媒体控制服务 - -参见 3.11 节功能定义 - -**开发要点**: -- MediaService 作为独立插件进程,可选挂载 -- 与 NotificationService 协作(媒体通知类型) -- 锁屏媒体卡片与锁屏模块联动 - ---- - -### Phase M:P2 高级控件 + SDK 初稿 - -> 此阶段桌面本体已基本完整,开始向外暴露开发者接口 - -- P2 控件按需实现(Dialog / Card / Snackbar / Tooltip 等,按使用频率排序) -- C++ SDK 头文件整理与文档编写 -- QML 接口层(低门槛开发者接口,后期扩展) -- 开发者文档网站更新(MkDocs) - ---- - -## 六、依赖关系图 - -``` -Phase A(基础设施) - ├──→ Phase B(渲染后端) - ├──→ Phase C(P1 控件)[可并行] - └──→ Phase D(窗口管理器) - └──→ Phase E(Shell 导航) - └──→ Phase F(通知 + 控制中心) - └──→ Phase G(主题系统完整实现) - ├──→ Phase H(系统设置) - ├──→ Phase I(锁屏) - ├──→ Phase J(Widget + 壁纸) - ├──→ Phase K(文件管理器) - └──→ Phase L(媒体服务) - └──→ Phase M(SDK + P2 控件) -``` - -**可并行开发的模块**: -- Phase C(P1 控件)可与 Phase A/B 全程并行 -- Phase H / I / J / K / L 在 Phase G 完成后可并行推进 -- 文档编写、测试补全可贯穿全程 - ---- - -## 七、测试策略 - -### 7.1 测试分层 - -| 层次 | 内容 | 工具 | -|------|------|------| -| 单元测试 | 每个类/模块的独立测试 | Google Test / GMock | -| 集成测试 | 跨模块协作测试(如输入→窗口→通知) | Google Test | -| UI 自动化测试 | 鼠标/触摸/键盘模拟操作流程 | Qt Test / 自研 | -| 性能基准测试 | 帧率/内存/CPU 占用基线 | 自研 Benchmark 框架 | -| HWTier 回归测试 | 模拟三档硬件环境验证降级策略 | 硬件 Mock + Docker | - -### 7.2 性能指标目标 - -| 指标 | Low Tier 目标 | Mid Tier 目标 | High Tier 目标 | -|------|--------------|--------------|---------------| -| Shell 启动时间 | < 3s | < 1.5s | < 0.8s | -| 应用启动时间 | < 2s | < 1s | < 0.5s | -| 桌面帧率 | 30 FPS(静态) | 45 FPS | 60 FPS | -| 通知弹窗延迟 | < 200ms | < 100ms | < 50ms | -| 主题切换时间 | < 500ms | < 300ms | < 200ms | -| Shell 内存占用 | < 80MB | < 150MB | < 300MB | - ---- - -## 八、关键设计决策记录 - -| 决策 | 选择 | 理由 | -|------|------|------| -| IPC 机制 | Qt IPC(QLocalSocket)优先,必要时迁移 D-Bus | 减少依赖,嵌入式环境 D-Bus 不一定可用 | -| 渲染后端 | 多后端抽象,不自研 Wayland Compositor | 降低复杂度,嵌入式 EGLFS 直驱更稳定 | -| 主题切换 | 运行时热切换,Policy 注入而非硬编码 | 用户体验好,架构扩展性强 | -| 应用模型 | 进程隔离,Shell 作为宿主 | 稳定性更好,一个 App 崩溃不影响 Shell | -| 动效策略 | HWTier 联动,可配置不可强制关闭 | 低端设备保证性能,高端设备保证体验 | -| SDK 时机 | Phase M(桌面本体基本完整后)再开放 | 先把自己的桌面做好,API 稳定后再开放 | -| 商业协议 | MIT 开源 | 最大化社区采用,无授权障碍 | - ---- - -## 九、附录:参考项目 - -| 参考对象 | 学习要点 | -|---------|---------| -| iOS / iPadOS | 手势体系、弹簧动效、控制中心交互、通知分组 | -| Windows 11 | Fluent Design、任务栏居中、快捷设置面板、Action Center | -| KDE Plasma | 高度可定制架构、系统设置结构、Wayland 集成方式 | -| Android | 通知优先级体系、快捷设置瓦片自定义、Widget 沙箱机制 | - ---- +--- +title: CFDesktop 桌面本体规划文档 +description: CFDesktop 桌面本体规划文档 的详细文档 +--- + +# CFDesktop 桌面本体规划文档 + +--- + +## 一、项目背景与现状 + +### 1.1 项目定性 + +CFDesktop 是一个基于 **Qt 6.8.3+ / C++23** 开发的**嵌入式桌面 UI 框架**,遵循 Material Design 3 规范。当前版本(0.13.1)本质上是一个**UI 组件库 + 底层架构**,尚不构成完整的桌面环境。 + +本文档的目标是在现有基础上,规划**桌面本体(Desktop Shell)**的完整建设路径。 + +### 1.2 当前已完成的基础(可复用) + +| 模块 | 完成度 | 可复用价值 | +|------|--------|-----------| +| 工程骨架(CMake/CI/CD) | 100% | ✅ 直接复用 | +| Material Design 3 Token 系统 | 100% | ✅ 主题系统基础 | +| ThemeEngine 主题管理 | 100% | ✅ 桌面主题切换核心 | +| AnimationManager 动画引擎 | 100% | ✅ 桌面过渡动效基础 | +| DPI 管理 | 100% | ✅ 多分辨率屏幕适配 | +| 19个 P0+P1 控件 | 100% | ✅ 桌面 Shell UI 基础 | +| Google Test 测试框架 | 100% | ✅ 持续集成基础 | +| CPU/内存/GPU/网络检测器 | 90% | ✅ 硬件探针基本完成 | +| ConfigStore 配置中心 | 100% | ✅ 四层存储,INI持久化 | +| Logger 日志系统 | 100% | ✅ 异步日志,多Sink | +| Windows 显示后端 | 100% | ✅ Win32 DWM + HWND | +| WSL X11 显示后端 | 100% | ✅ XCB + XWayland | +| 显示服务器抽象 (IDisplayServerBackend) | 100% | ✅ 三模式抽象 | +| 窗口管理基础 (WindowManager/PanelManager) | 80% | ✅ 基础注册/查询/弱引用 | + +### 1.3 当前关键缺口(桌面本体的前置依赖) + +| 缺口模块 | 当前进度 | 对桌面本体的影响 | +|----------|--------|----------------| +| 输入抽象层(InputManager) | 0% | 🔴 所有交互的根基,必须最先完成 | +| HWTier 硬件分级系统 | 0% | 🔴 性能自适应的核心判据 | +| CrashHandler 崩溃处理 | 0% | 🟡 稳定性保障 | +| IPC 进程间通信 | 0% | 🟡 多进程架构依赖 | +| P2 高级控件(27个) | 0% | 🟡 文件管理器等应用依赖 | + +--- + +## 二、需求定义 + +### 2.1 目标用户与场景 + +**主要面向**: +- 开源社区与个人开发者 + +**典型使用场景**: +- 嵌入式 Linux 设备上运行一个完整的桌面环境 +- 开发者在 Windows 上开发调试,部署到 Linux 嵌入式设备 +- 设备厂商基于 CFDesktop 定制自己品牌的桌面风格 + +### 2.2 目标硬件规格 + +| 档位 | 硬件规格 | 行为策略 | +|------|---------|---------| +| Low Tier | 无独立 GPU,≤512MB RAM,ARM Cortex-A5/A7 | 关闭动效、简化阴影、降低合成层数 | +| Mid Tier | 弱 GPU,1-2GB RAM,ARM Cortex-A53/A55 | 基础动效开启,限制并发动画数量 | +| High Tier | 独立 GPU,≥4GB RAM,ARM Cortex-A72/A76+ | 全量动效、模糊/透明效果全开 | + +> **HWTier 系统是整个性能自适应的核心判据**,必须在桌面本体启动前完成探测并注入全局配置。 + +### 2.3 屏幕与输入规格 + +- **屏幕尺寸**:5.5 ~ 10.1 英寸矩形触摸屏 +- **分辨率**:变化范围较大,依赖现有 DPI 管理模块动态适配 +- **输入方式**:触摸(单点/多点手势)+ 鼠标键盘(开发调试/PC风格主题) +- **操作系统**:Linux 主力部署;Windows 作为开发调试等价环境 + +### 2.4 渲染后端策略 + +采用**多后端抽象层**,运行时按环境选择: + +```text +渲染后端抽象层(RenderBackend Interface) + ├── EGLFS / LinuxFB → 嵌入式 Linux 直驱(主要部署环境) + ├── Wayland Client → 跑在现有 Wayland 合成器之上 + ├── X11 → 旧版 Linux 桌面兼容 + └── Windows (Win32) → 开发调试等价环境 +```yaml + +> 不自研 Wayland Compositor,优先保证嵌入式 EGLFS 直驱路径的稳定性。 + +### 2.5 应用模型 + +- **多应用自由切换**(类手机/PC 体验)为主,同时支持 Kiosk 全屏模式配置 +- **进程隔离**:每个 App 独立进程,桌面 Shell 作为宿主进程(Compositor Process) +- **系统服务**:核心服务单进程(通知/窗口管理/输入路由),可选服务(媒体控制/设置服务)独立进程,服务以**插件式**形式挂载 +- **IPC 机制**:优先使用 Qt 自带机制(QLocalSocket / Shared Memory),如复杂度不可控再迁移 D-Bus + +### 2.6 商业模式 + +**MIT 开源协议**,无商业授权限制。 + +--- + +## 三、桌面本体功能定义 + +### 3.1 核心子系统总览 + +```text +CFDesktop Shell +├── 主题风格系统(Theme Style System) +│ ├── iOS 风格主题包 +│ └── Windows 11 风格主题包 +├── 窗口管理器(Window Manager) +├── 任务栏 / 导航系统(Shell Navigation) +├── 应用启动器(App Launcher) +├── 通知系统(Notification System) +├── 快捷控制中心(Control Center) +├── 系统设置(System Settings App) +├── 锁屏模块(Lock Screen) +├── 桌面壁纸与 Widget 系统(Desktop Layer) +├── 文件管理器(File Manager App) +├── 媒体控制服务(Media Control Service) +└── 硬件性能自适应引擎(HWTier Adaptive Engine) +```bash + +### 3.2 双主题风格系统(核心特色) + +CFDesktop 的核心差异化特性:**支持整体主题包切换 + 组件级微调**。 + +#### iOS 风格主题包 + +| 特性 | 描述 | +|------|------| +| 导航范式 | 底部手势条(类 iOS Home Indicator)+ 底部 Tab Bar | +| 启动器 | 网格图标桌面(类 iOS 主屏),支持图标拖拽排列 | +| 窗口过渡 | 全屏滑入/滑出(类 iOS App 打开/关闭动效) | +| 任务切换 | 上滑调出卡片式任务切换器(类 iOS App Switcher) | +| 控制中心 | 右上角下滑展开圆角磁贴面板 | +| 通知 | 顶部下滑展开通知列表 | +| 字体/圆角 | 大圆角卡片,SF Pro 风格字体层级 | +| 动效风格 | 弹簧动效(Spring Animation),响应灵敏 | + +#### Windows 11 风格主题包 + +| 特性 | 描述 | +|------|------| +| 导航范式 | 底部居中任务栏(类 Win11 Centered Taskbar) | +| 启动器 | 点击任务栏图标展开开始菜单(类 Win11 Start Menu) | +| 窗口过渡 | 缩放淡入淡出(类 Win11 窗口动效) | +| 任务切换 | 任务栏悬停预览缩略图 + Task View 全览 | +| 控制中心 | 右下角系统托盘区点击展开快捷面板(类 Win11 Quick Settings) | +| 通知 | 右侧滑出通知中心面板(类 Win11 Action Center) | +| 字体/圆角 | 中圆角,Segoe UI 风格字体层级 | +| 动效风格 | 时间曲线动效(Ease In/Out),流畅稳重 | + +#### 主题切换架构 + +```text +ThemeStyleManager + ├── loadThemePack(ThemePack::iOS / ThemePack::Windows) + ├── 注入 ThemeEngine(颜色 Token) + ├── 注入 NavigationPolicy(导航范式) + ├── 注入 AnimationPolicy(动效策略) + ├── 注入 LayoutPolicy(圆角/间距/字体) + └── 触发 Shell 重新布局 +```text + +> 主题包切换是**运行时热切换**,无需重启桌面进程。 + +### 3.3 窗口管理器 + +- **浮动窗口模式**(可拖拽/缩放,Windows 风格主题默认) +- **全屏平铺模式**(iOS 风格主题默认) +- **分屏模式**:左右/上下固定分割 +- **窗口层级管理**:Z-order 管理,支持 Always On Top +- **窗口动效**:开启/关闭/最小化/恢复均有主题对应动效 +- **多显示器**:基础支持(后期扩展) + +### 3.4 Shell 导航系统 + +导航组件根据当前主题包**动态切换形态**,不硬编码: + +```text +NavigationPolicy Interface + ├── iOS Policy → BottomGestureBar + BottomTabBar + └── Windows Policy → CenteredTaskbar + SystemTray +```text + +公共元素(跨主题): +- 顶部状态栏(时间、网络、电量、通知角标) +- 应用标题区(返回按钮/窗口控制按钮随主题变化) + +### 3.5 通知系统 + +对标 iOS 通知中心 + Windows Action Center,要求**完善**: + +```text +NotificationService(独立进程) + ├── 通知接收 API(App 调用) + ├── 通知持久化存储(ConfigStore 依赖) + ├── 分组/折叠(按 App 归组) + ├── 优先级分级(Critical / Normal / Silent) + ├── 横幅弹窗(Banner,自动消失) + ├── 通知中心面板(下拉/侧滑展开,可清除) + ├── 角标计数(状态栏图标角标) + └── 勿扰模式(Do Not Disturb) +```bash + +### 3.6 快捷控制中心 + +对标 iOS Control Center + Windows Quick Settings: + +- 亮度滑条 +- 音量滑条 +- WiFi / 蓝牙 快捷开关 +- 截图 +- 勿扰模式切换 +- 主题风格快速切换入口 +- 自定义快捷瓦片(开发者可注册) + +### 3.7 系统设置 App + +对标 KDE System Settings + Windows 设置,内置完整设置 App: + +| 分类 | 设置项 | +|------|--------| +| 显示 | 分辨率、亮度、夜间模式、缩放比例 | +| 声音 | 音量、输出设备、提示音 | +| 网络 | WiFi 列表、IP 配置、代理 | +| 蓝牙 | 设备配对管理 | +| 桌面 | 主题包切换、壁纸、动效开关 | +| 输入 | 触摸灵敏度、鼠标速度、键盘布局 | +| 应用 | 已安装应用列表、权限管理、默认应用 | +| 语言 | 语言/地区/时区(框架预留,后期实现) | +| 辅助 | 字体大小缩放、高对比度(后期实现) | +| 系统 | 关于本机、硬件信息(HWTier 展示)、日志查看 | + +### 3.8 锁屏模块 + +- **PIN / 密码 / 图案**三种解锁方式 +- 锁屏壁纸(独立于桌面壁纸,或继承) +- 锁屏通知展示(仅 Normal 及以上级别) +- 息屏超时自动锁屏(可配置时长) +- 锁屏时媒体控制卡片(正在播放时展示) + +### 3.9 桌面壁纸 + Widget 系统 + +**壁纸**: +- 静态壁纸(Low Tier 及以上) +- 动态壁纸(High Tier,视频/Lottie 动画,Mid/Low 自动降级为静态) +- 主题联动:主题包切换时自动推荐配套壁纸 + +**桌面 Widget(小组件)**: +- 内置 Widget:时钟、日历、天气、系统资源监控 +- Widget 框架:开发者可注册自定义 Widget(提供 Widget API) +- Widget 编辑模式(长按进入,支持拖拽/缩放) +- iOS 风格:Widget 与图标共存于网格桌面 +- Windows 风格:Widget 区域独立(类 Win11 Widgets Panel) + +### 3.10 文件管理器 App + +对标 Windows 资源管理器,内置完整文件管理器: + +- 目录树导航(左侧面板) +- 文件列表视图(图标/列表/详情切换) +- 基础文件操作(复制/剪切/粘贴/删除/重命名) +- 文件搜索 +- 压缩包浏览(后期) +- 文件选择器对话框(供应用调用的系统级 API) + +### 3.11 媒体控制服务 + +- 系统级媒体会话管理(跨 App 统一控制) +- 状态栏/锁屏媒体卡片(播放/暂停/上下曲) +- 快捷控制中心媒体区块 +- 音量控制与输出设备切换 +- 蓝牙音频设备快速切换(依赖蓝牙模块) + +--- + +## 四、架构分层设计 + +```text +┌─────────────────────────────────────────────────────────┐ +│ Layer 7: Apps Layer │ +│ 文件管理器 / 系统设置 / 内置 Widget App │ +├─────────────────────────────────────────────────────────┤ +│ Layer 6: Shell Layer │ +│ 任务栏 / 启动器 / 通知中心 / 控制中心 / 锁屏 / 桌面层 │ +├─────────────────────────────────────────────────────────┤ +│ Layer 5: Theme Style System │ +│ iOS主题包 / Windows主题包 / ThemeStyleManager │ +├─────────────────────────────────────────────────────────┤ +│ Layer 4: Window Manager │ +│ 浮动/全屏/分屏 / Z-order / 窗口动效 │ +├─────────────────────────────────────────────────────────┤ +│ Layer 3: System Services │ +│ NotificationService / MediaService / InputRouter │ +├─────────────────────────────────────────────────────────┤ +│ Layer 2: Base Infrastructure │ +│ InputManager / ConfigStore / Logger / IPC │ +├─────────────────────────────────────────────────────────┤ +│ Layer 1: Hardware Abstraction │ +│ HardwareProbe / HWTier / RenderBackend │ +├─────────────────────────────────────────────────────────┤ +│ Layer 0: Already Completed │ +│ ThemeEngine / AnimationManager / DPI / P0 Controls │ +└─────────────────────────────────────────────────────────┘ +```bash + +**设计原则**: +- 每层只依赖下层,严禁跨层调用 +- Layer 5(主题风格系统)横切 Layer 4-6,通过 Policy 注入而非直接调用 +- 系统服务通过 IPC 与 Shell 通信,Shell 不直接持有服务实现 + +--- + +## 五、开发路线图 + +> **节奏定义**:不赶时间,架构分层优先,质量优先,每个 Phase 完成后需通过完整测试才可进入下一 Phase。 + +--- + +### Phase A:基础设施补全(前置必做) +> 对应现有 Phase 1/2 的未完成部分,是桌面本体的地基 + +**A1 — 硬件探针完善** +- GPU 检测器(DRM 设备枚举、OpenGL 上下文探测) +- HWTier 枚举定义(Low / Mid / High) +- HardwareProbe 主类(整合 CPU + 内存 + GPU 探测) +- CapabilityPolicy 策略引擎(各档位默认配置集) +- 单元测试覆盖 + +**A2 — ConfigStore 配置中心** +- 三层存储模型:系统默认 → 用户配置 → 运行时覆写 +- 变更监听机制(Observer 模式) +- 持久化序列化(JSON / QSettings 后端) +- 线程安全访问 +- 单元测试覆盖 + +**A3 — Logger 日志系统** +- 多 Sink 支持(文件 / 终端 / 系统日志) +- 日志级别(Debug / Info / Warning / Error / Fatal) +- 日志轮转(大小/时间) +- 结构化日志(JSON 格式可选) +- 单元测试覆盖 + +**A4 — 输入抽象层** +- InputManager 统一分发层 +- TouchInputHandler(单点 + 多点手势识别) +- KeyInputHandler(键盘/快捷键) +- MouseInputHandler(鼠标 + 滚轮) +- GestureRecognizer(Tap / LongPress / Swipe / Pinch / Pan) +- FocusNavigator(焦点导航系统) +- 单元测试 + UI 自动化测试框架搭建 + +**A5 — IPC 基础层** +- QLocalSocket 封装(进程间消息信道) +- 消息序列化协议定义 +- 服务注册/发现机制(ServiceLocator) +- 基础测试覆盖 + +**里程碑验收标准**: +- HWTier 可在目标设备上正确探测并输出档位 +- ConfigStore 可持久化读写并触发变更通知 +- 触摸/鼠标/键盘输入均可通过 InputManager 统一路由 +- 两个进程可通过 IPC 层互相发送消息 + +--- + +### Phase B:渲染后端抽象层 +> 解决"同一代码跑在 EGLFS / Wayland / Windows"的问题 + +**B1 — RenderBackend 接口定义** +- RenderBackend 抽象接口(初始化/交换缓冲/截图/VSync) +- 后端注册机制(运行时按环境选择) + +**B2 — EGLFS 后端** +- Qt EGLFS 封装 +- HWTier 联动:High Tier 开启 OpenGL ES 合成,Low Tier 降级 LinuxFB + +**B3 — Windows 等价后端** +- Qt Windows 后端封装 +- 开发调试辅助工具:屏幕尺寸模拟(5.5" / 8" / 10.1" 三档快速切换) + +**B4 — Wayland/X11 后端** +- 跑在现有 Wayland/X11 合成器之上的支持(次优先) + +**里程碑验收标准**: +- 同一 Shell 代码在 Linux EGLFS 和 Windows 上均可启动 +- 屏幕尺寸模拟器可在 Windows 上精确还原嵌入式屏幕效果 + +--- + +### Phase C:P1 控件补全 ✅ 已完成 (2026-03-18) +> Shell UI 所需的基础控件,与 Phase A/B 并行推进 + +**已实现控件**(12个): + +| 优先级 | 控件 | 桌面依赖场景 | 状态 | +|--------|------|------------|------| +| P1-1 | Slider | 亮度/音量控制 | ✅ | +| P1-2 | Switch | 设置开关项 | ✅ | +| P1-3 | ProgressBar | 下载/加载状态 | ✅ | +| P1-4 | TabView | 设置分类导航 | ✅ | +| P1-5 | ComboBox | 设置下拉选项 | ✅ | +| P1-6 | ListView | 列表显示 | ✅ | +| P1-7 | TableView | 表格显示 | ✅ | +| P1-8 | TreeView | 树形显示 | ✅ | +| P1-9 | ScrollView | 滚动视图 | ✅ | +| P1-10 | Separator | 分隔线 | ✅ | +| P1-11 | SpinBox | 整数输入 | ✅ | +| P1-12 | DoubleSpinBox | 浮点输入 | ✅ | + +**待实现 P2 控件**(文件管理器等依赖): +- ToolBar / ToolButton +- MenuBar / ContextMenu +- StatusBar +- Dialog / Card / Snackbar +- Tooltip / Popover +- ... + +--- + +### Phase D:窗口管理器 +> Shell 核心,建立在 Phase A/B 完成之后 + +**D1 — 窗口模型** +- WindowInfo(标题/图标/PID/状态/几何/层级) +- 窗口状态机(Normal / Maximized / Minimized / Fullscreen / Closing) +- Z-order 管理(层级排序) + +**D2 — 布局策略** +- FullscreenPolicy(iOS 风格默认) +- FloatingPolicy(Windows 风格默认,支持拖拽/缩放) +- SplitPolicy(左右/上下分屏) +- 策略运行时切换(随主题包切换自动切换默认策略) + +**D3 — 窗口动效** +- 开启动效(按主题包:iOS 弹入 / Win 缩放淡入) +- 关闭动效 +- 最小化/恢复动效 +- HWTier 联动:Low Tier 关闭窗口动效 + +**D4 — 任务切换** +- iOS 风格:上滑卡片式 App Switcher(缩略图预览) +- Windows 风格:Task View 全览 + 任务栏悬停预览 + +**里程碑验收标准**: +- 可在桌面上打开/关闭/切换至少 3 个并发应用窗口 +- iOS/Windows 两种布局策略均可正常运作 +- 窗口动效在 Mid/High Tier 流畅,Low Tier 自动关闭 + +--- + +### Phase E:Shell 导航 + 任务栏 +> 桌面的"骨架",用户每次使用都会接触 + +**E1 — 状态栏(Status Bar)** +- 时间/日期显示 +- 网络状态图标 +- 电量图标(如有硬件支持) +- 通知角标区 +- 主题联动(iOS:居中时间;Windows:左侧时间 + 右侧系统托盘) + +**E2 — iOS 风格导航** +- 底部手势条(Home Indicator) +- 手势识别:上滑回主屏 / 上滑停留进 App Switcher / 左右滑切换应用 +- 底部 Tab Bar(可配置应用快捷入口) + +**E3 — Windows 风格导航** +- 居中任务栏(固定应用图标 + 运行中应用) +- 系统托盘区(时间/通知图标/媒体控制入口) +- 开始按钮(触发 App Launcher) + +**E4 — 应用启动器** +- iOS 风格:网格图标桌面(图标拖拽/文件夹/多页滑动) +- Windows 风格:开始菜单(最近使用 + 固定应用 + 全部应用列表) +- 全局搜索(应用/文件/设置 统一搜索入口) + +**里程碑验收标准**: +- 两种主题的导航体系均可独立运作 +- 主题包切换后导航形态热切换无需重启 +- 应用启动器可浏览和启动所有已安装应用 + +--- + +### Phase F:通知系统 + 控制中心 +> 完善程度对标 iOS / Windows 11 + +**F1 — NotificationService(独立进程)** +- 通知接收 API(Qt IPC) +- 通知持久化(ConfigStore 依赖) +- 优先级分级(Critical / Normal / Silent) +- 勿扰模式 + +**F2 — 横幅弹窗(Banner)** +- 顶部滑入展示,3秒自动消失(可配置) +- 可展开操作按钮(最多2个快捷操作) +- HWTier:Low Tier 仅显示文字,关闭展开动效 + +**F3 — 通知中心面板** +- iOS 风格:顶部下拉展开 +- Windows 风格:右侧滑出面板 +- 分组折叠(按 App) +- 全部清除 / 单条清除 + +**F4 — 快捷控制中心** +- 亮度/音量滑条 +- WiFi / 蓝牙 / 勿扰 / 截图 快捷开关 +- 媒体控制卡片(当有音乐播放时展示) +- 主题切换入口 +- 自定义瓦片注册 API + +**里程碑验收标准**: +- 任意进程可通过 API 发送通知并在桌面正常展示 +- 通知中心可展示、分组、清除通知 +- 控制中心亮度/音量调节生效 + +--- + +### Phase G:主题风格系统完整实现 +> 双主题包的完整实现与热切换 + +**G1 — ThemeStyleManager 核心** +- ThemePack 数据结构定义(颜色/圆角/间距/动效/布局策略 完整集) +- 主题包加载/卸载机制 +- 热切换协调器(通知所有订阅方) +- 主题包文件格式定义(JSON + 资源文件) + +**G2 — iOS 主题包** +- 完整颜色 Token 集(对标 iOS 17 Human Interface Guidelines) +- 布局策略注入(NavigationPolicy / WindowPolicy) +- 动效策略注入(Spring Animation 参数集) +- 图标风格(圆角矩形图标,带阴影) +- 壁纸默认集(3-5 张高质量默认壁纸) + +**G3 — Windows 11 主题包** +- 完整颜色 Token 集(对标 Windows 11 Fluent Design) +- 布局策略注入 +- 动效策略注入(Ease 曲线参数集) +- Mica/Acrylic 效果(High Tier 开启,模糊半透明) +- 壁纸默认集(3-5 张) + +**G4 — 组件级微调 API** +- 每个 P0/P1 控件暴露 StyleOverride 接口 +- 用户可在主题包基础上微调单个控件样式 +- 微调配置持久化(ConfigStore) + +**里程碑验收标准**: +- iOS / Windows 两套主题包均完整可用 +- 运行时热切换主题无崩溃、无视觉撕裂 +- 动效在各 HWTier 按策略正确降级 + +--- + +### Phase H:系统设置 App + +对标 KDE System Settings + Windows 设置(参见 3.7 节功能定义) + +**开发要点**: +- 作为独立进程启动,通过 IPC 调用各系统服务 +- 设置项变更实时生效(不需要重启) +- 设置界面本身也遵循当前主题风格 +- 搜索功能:可在所有设置项中模糊搜索 + +--- + +### Phase I:锁屏模块 + +参见 3.8 节功能定义 + +**开发要点**: +- 锁屏运行在最高 Z-order 层,不可被其他窗口覆盖 +- 解锁验证逻辑与凭证存储安全隔离 +- 锁屏时输入路由只处理解锁相关输入 +- 息屏策略与 HWTier 联动(Low Tier 更激进的息屏超时) + +--- + +### Phase J:桌面 Widget + 壁纸系统 + +参见 3.9 节功能定义 + +**开发要点**: +- Widget 沙箱机制(Widget 崩溃不影响 Shell) +- Widget API 设计需兼顾未来第三方开发者接入 +- 动态壁纸严格限制 High Tier 才启用,防止低端设备性能问题 + +--- + +### Phase K:文件管理器 App + +参见 3.10 节功能定义 + +**开发要点**: +- 依赖 P1 控件:ToolBar / ToolButton / StatusBar / ContextMenu +- 文件选择器对话框作为**系统级 API** 独立封装,所有 App 均可调用 +- 支持主题联动(iOS/Windows 风格自动切换界面布局) + +--- + +### Phase L:媒体控制服务 + +参见 3.11 节功能定义 + +**开发要点**: +- MediaService 作为独立插件进程,可选挂载 +- 与 NotificationService 协作(媒体通知类型) +- 锁屏媒体卡片与锁屏模块联动 + +--- + +### Phase M:P2 高级控件 + SDK 初稿 + +> 此阶段桌面本体已基本完整,开始向外暴露开发者接口 + +- P2 控件按需实现(Dialog / Card / Snackbar / Tooltip 等,按使用频率排序) +- C++ SDK 头文件整理与文档编写 +- QML 接口层(低门槛开发者接口,后期扩展) +- 开发者文档网站更新(MkDocs) + +--- + +## 六、依赖关系图 + +```text +Phase A(基础设施) + ├──→ Phase B(渲染后端) + ├──→ Phase C(P1 控件)[可并行] + └──→ Phase D(窗口管理器) + └──→ Phase E(Shell 导航) + └──→ Phase F(通知 + 控制中心) + └──→ Phase G(主题系统完整实现) + ├──→ Phase H(系统设置) + ├──→ Phase I(锁屏) + ├──→ Phase J(Widget + 壁纸) + ├──→ Phase K(文件管理器) + └──→ Phase L(媒体服务) + └──→ Phase M(SDK + P2 控件) +```bash + +**可并行开发的模块**: +- Phase C(P1 控件)可与 Phase A/B 全程并行 +- Phase H / I / J / K / L 在 Phase G 完成后可并行推进 +- 文档编写、测试补全可贯穿全程 + +--- + +## 七、测试策略 + +### 7.1 测试分层 + +| 层次 | 内容 | 工具 | +|------|------|------| +| 单元测试 | 每个类/模块的独立测试 | Google Test / GMock | +| 集成测试 | 跨模块协作测试(如输入→窗口→通知) | Google Test | +| UI 自动化测试 | 鼠标/触摸/键盘模拟操作流程 | Qt Test / 自研 | +| 性能基准测试 | 帧率/内存/CPU 占用基线 | 自研 Benchmark 框架 | +| HWTier 回归测试 | 模拟三档硬件环境验证降级策略 | 硬件 Mock + Docker | + +### 7.2 性能指标目标 + +| 指标 | Low Tier 目标 | Mid Tier 目标 | High Tier 目标 | +|------|--------------|--------------|---------------| +| Shell 启动时间 | < 3s | < 1.5s | < 0.8s | +| 应用启动时间 | < 2s | < 1s | < 0.5s | +| 桌面帧率 | 30 FPS(静态) | 45 FPS | 60 FPS | +| 通知弹窗延迟 | < 200ms | < 100ms | < 50ms | +| 主题切换时间 | < 500ms | < 300ms | < 200ms | +| Shell 内存占用 | < 80MB | < 150MB | < 300MB | + +--- + +## 八、关键设计决策记录 + +| 决策 | 选择 | 理由 | +|------|------|------| +| IPC 机制 | Qt IPC(QLocalSocket)优先,必要时迁移 D-Bus | 减少依赖,嵌入式环境 D-Bus 不一定可用 | +| 渲染后端 | 多后端抽象,不自研 Wayland Compositor | 降低复杂度,嵌入式 EGLFS 直驱更稳定 | +| 主题切换 | 运行时热切换,Policy 注入而非硬编码 | 用户体验好,架构扩展性强 | +| 应用模型 | 进程隔离,Shell 作为宿主 | 稳定性更好,一个 App 崩溃不影响 Shell | +| 动效策略 | HWTier 联动,可配置不可强制关闭 | 低端设备保证性能,高端设备保证体验 | +| SDK 时机 | Phase M(桌面本体基本完整后)再开放 | 先把自己的桌面做好,API 稳定后再开放 | +| 商业协议 | MIT 开源 | 最大化社区采用,无授权障碍 | + +--- + +## 九、附录:参考项目 + +| 参考对象 | 学习要点 | +|---------|---------| +| iOS / iPadOS | 手势体系、弹簧动效、控制中心交互、通知分组 | +| Windows 11 | Fluent Design、任务栏居中、快捷设置面板、Action Center | +| KDE Plasma | 高度可定制架构、系统设置结构、Wayland 集成方式 | +| Android | 通知优先级体系、快捷设置瓦片自定义、Widget 沙箱机制 | + +--- diff --git a/document/todo/done/.pages b/document/todo/done/.pages deleted file mode 100644 index 99470ee69..000000000 --- a/document/todo/done/.pages +++ /dev/null @@ -1,13 +0,0 @@ -title: 已完成 -nav: - - 项目状态报告: PROJECT_STATUS_REPORT.md - - 工程骨架: 00_project_skeleton_status.md - - 硬件探针: 01_hardware_probe_status.md - - 基础库: 02_base_library_status.md - - 输入层: 03_input_layer_status.md - - 模拟器: 04_simulator_status.md - - 测试: 05_testing_status.md - - 基础设施: 06_infrastructure_status.md - - 小部件应用: 13_widget_apps_status.md - - 显示后端: 14_display_backend_status.md - - UI 框架: 99_ui_material_framework_status.md diff --git a/document/todo/done/00_project_skeleton_status.md b/document/todo/done/00_project_skeleton_status.md index 0c29381d3..ff55a7eb8 100644 --- a/document/todo/done/00_project_skeleton_status.md +++ b/document/todo/done/00_project_skeleton_status.md @@ -1,3 +1,8 @@ +--- +title: "Phase 0: 工程骨架搭建 - 状态文档" +description: "模块ID: Phase 0,总体进度: 100%" +--- + # Phase 0: 工程骨架搭建 - 状态文档 > **模块ID**: Phase 0 @@ -66,7 +71,7 @@ add_subdirectory(base) # 基础库 add_subdirectory(ui) # UI 框架 add_subdirectory(example) # 示例程序 add_subdirectory(test) # 测试代码 -``` +```text **依赖文件**: - `cmake/build_log_helper.cmake` - 构建日志辅助 @@ -100,7 +105,7 @@ PointerAlignment: Left BreakBeforeBraces: Attach Standard: c++17 SortIncludes: true -``` +```text ### 3.3 开发工具集成 (70%) @@ -209,7 +214,7 @@ SortIncludes: true ### 3.5 目录结构 **已建立的目录结构**: -``` +```text CFDesktop/ ├── base/ # 基础库模块 │ ├── system/ # 系统检测 (CPU, 内存) @@ -223,7 +228,7 @@ CFDesktop/ ├── cmake/ # CMake 模块 ├── .github/ # GitHub 配置 └── .vscode/ # VSCode 配置 -``` +```bash --- @@ -324,19 +329,19 @@ CFDesktop/ ### 5.2 src/base/sdk/shell 三层结构 - 已调整 **原计划**: -``` +```text src/ ├── base/ # 基础库 ├── sdk/ # SDK 层 └── shell/ # Shell UI -``` +```text **实际采用**: -``` +```text base/ # 基础库 ui/ # UI 框架 example/ # 示例程序 -``` +```yaml **原因**: 更简洁的模块划分 @@ -368,7 +373,7 @@ example/ # 示例程序 ### 已实现文件 -``` +```text CFDesktop/ ├── CMakeLists.txt # 主 CMake 配置 ├── .clang-format # 代码格式化配置 @@ -402,11 +407,11 @@ CFDesktop/ ├── generate_develop_helpers.cmake # 开发辅助生成 ├── ExampleLauncher.cmake # 启动脚本生成 └── QtDeployUtils.cmake # Qt 部署工具 -``` +```text ### 待创建文件 -``` +```text CFDesktop/ ├── .clang-tidy # 静态分析配置 (不考虑) ├── .github/ @@ -416,7 +421,7 @@ CFDesktop/ └── toolchains/ ├── arm-linux-gnueabihf.cmake # ARMv7 工具链 (推迟) └── aarch64-linux-gnu.cmake # ARM64 工具链 (推迟) -``` +```bash --- diff --git a/document/todo/done/01_hardware_probe_status.md b/document/todo/done/01_hardware_probe_status.md index 4e666d861..bebe0e676 100644 --- a/document/todo/done/01_hardware_probe_status.md +++ b/document/todo/done/01_hardware_probe_status.md @@ -1,3 +1,8 @@ +--- +title: "Phase 1: 硬件探针与能力分级 - 状态文档" +description: "模块ID: Phase 1,状态: 🚧 部分完成" +--- + # Phase 1: 硬件探针与能力分级 - 状态文档 > **模块ID**: Phase 1 @@ -178,7 +183,7 @@ if (mem_info) { qDebug() << "Total:" << mem_info.value().totalBytes; qDebug() << "Available:" << mem_info.value().availableBytes; } -``` +```yaml --- @@ -209,7 +214,7 @@ QString tierToString(HWTier tier); HWTier tierFromString(const QString& str); } // namespace CFDesktop::Base -``` +```yaml --- @@ -260,7 +265,7 @@ private: }; } // namespace CFDesktop::Base -``` +```yaml --- @@ -325,7 +330,7 @@ signals: }; } // namespace CFDesktop::Base -``` +```yaml --- @@ -356,7 +361,7 @@ VideoDecoder=auto [Logging] LogLevel=Info -``` +```yaml --- @@ -407,7 +412,7 @@ private slots: void testMalformedConfig(); void testTierOverride(); }; -``` +```text ### 6.2 评分算法 @@ -443,7 +448,7 @@ void HardwareProbe::calculateTier(HardwareInfo& info) { info.tier = calculatedTier; } -``` +```yaml --- @@ -451,7 +456,7 @@ void HardwareProbe::calculateTier(HardwareInfo& info) { ### 已实现文件 -``` +```text base/ ├── system/ │ ├── cpu/ @@ -469,11 +474,11 @@ base/ └── include/ ├── system/cpu/ # CPU 公共头文件 └── system/memory/ # 内存公共头文件 -``` +```text ### 待创建文件 -``` +```text base/ ├── hardware/ │ ├── HWTier.h # 档位枚举 @@ -487,18 +492,18 @@ base/ │ └── platform/ │ ├── LinuxDetector.cpp # Linux 平台实现 │ └── WindowsDetector.cpp # Windows 平台实现 -``` +```text ### 测试文件 -``` +```text tests/ ├── hardware/ │ ├── test_hardware_probe.cpp # 主测试 │ ├── test_capability_policy.cpp # 策略测试 │ └── mock/ │ └── proc/ # Mock 数据 -``` +```yaml --- diff --git a/document/todo/done/02_base_library_status.md b/document/todo/done/02_base_library_status.md index 1f4cb35b3..887ebe886 100644 --- a/document/todo/done/02_base_library_status.md +++ b/document/todo/done/02_base_library_status.md @@ -1,3 +1,8 @@ +--- +title: "Phase 2: 基础库核心 - 状态文档" +description: "模块ID: Phase 2,总体进度: 100%" +--- + # Phase 2: 基础库核心 - 状态文档 > **模块ID**: Phase 2 @@ -83,7 +88,7 @@ signals: void themeChanged(const ICFTheme& new_theme); }; } -``` +```bash #### 测试 @@ -148,7 +153,7 @@ class ICFAnimationManagerFactory : public QObject { enum class State { Idle, Running, Paused, Finished }; enum class Direction { Forward, Backward }; } -``` +```bash #### 示例 @@ -189,7 +194,7 @@ struct CanvasUnitHelper { BreakPoint breakPoint(qreal widthDp); }; } -``` +```bash #### 测试 @@ -275,7 +280,7 @@ public: }; } -``` +```bash #### 测试 @@ -351,7 +356,7 @@ public: }; } -``` +```yaml #### 测试 @@ -373,7 +378,7 @@ public: auto& manager = cf::ui::core::ThemeManager::instance(); manager.setThemeTo("theme.material.light"); -``` +```text ### 动画使用 @@ -385,7 +390,7 @@ auto anim = factory->getAnimation("md.animation.fadeIn"); if (anim) { anim->start(); } -``` +```text ### DPI 转换 @@ -394,7 +399,7 @@ if (anim) { cf::ui::base::device::CanvasUnitHelper helper(2.0); qreal pixels = helper.dpToPx(16.0); // 16dp -> 32px -``` +```text ### 配置读写 @@ -422,7 +427,7 @@ ConfigStore::instance().watch("app.theme.*", // 同步到磁盘 ConfigStore::instance().sync(SyncMethod::Async); -``` +```text ### 日志记录 @@ -445,7 +450,7 @@ logger.log(level::Info, "Message", "Tag", std::source_location::current()); // 刷新 flush(); -``` +```bash --- @@ -492,7 +497,7 @@ flush(); #### ui/ 目录 (主题、动画、DPI) -``` +```text ui/ ├── core/ │ ├── theme_manager.h # 主题管理器 (核心) @@ -515,11 +520,11 @@ ui/ ├── easing.h # 缓动曲线 ├── geometry_helper.h # 几何工具 └── math_helper.h # 数学工具 -``` +```text #### desktop/base/config_manager/ (配置中心) ✅ -``` +```text desktop/base/config_manager/ ├── include/ │ ├── cfconfig.hpp # 主配置存储接口 @@ -535,11 +540,11 @@ desktop/base/config_manager/ └── impl/ ├── config_impl.h # 内部实现 └── config_impl.cpp -``` +```text #### desktop/base/logger/ (日志系统) ✅ -``` +```text desktop/base/logger/ ├── include/ │ ├── cflog.h # 便捷日志函数 @@ -558,16 +563,16 @@ desktop/base/logger/ └── impl/ ├── cflog_impl.h # 内部实现 └── cflog_impl.cpp -``` +```text ### 待创建文件 (可选增强) -``` +```text ui/core/ ├── theme_loader.h # 主题加载器 (待创建) ├── qss_processor.h # QSS处理器 (待创建) └── variable_resolver.h # 变量解析器 (待创建) -``` +```yaml --- diff --git a/document/todo/done/03_input_layer_status.md b/document/todo/done/03_input_layer_status.md index e9a301fdf..a15ac02c7 100644 --- a/document/todo/done/03_input_layer_status.md +++ b/document/todo/done/03_input_layer_status.md @@ -1,3 +1,8 @@ +--- +title: "Phase 3: 输入抽象层 - 状态文档" +description: "模块ID: Phase 3,状态: **模块ID**: Phase 3 @@ -116,7 +121,7 @@ signals: }; } // namespace cf::base::input -``` +```text ### 3.2 TouchInputHandler - 触摸处理器 (0%) @@ -172,7 +177,7 @@ private: }; } // namespace cf::base::input -``` +```text ### 3.3 KeyInputHandler - 按键处理器 (0%) @@ -222,7 +227,7 @@ private: }; } // namespace cf::base::input -``` +```text ### 3.4 RotaryInputHandler - 旋钮处理器 (0%) @@ -268,7 +273,7 @@ private: }; } // namespace cf::base::input -``` +```text ### 3.5 GestureRecognizer - 手势识别器 (0%) @@ -317,7 +322,7 @@ private: }; } // namespace cf::base::input -``` +```text ### 3.6 FocusNavigator - 焦点导航器 (0%) @@ -366,7 +371,7 @@ private: }; } // namespace cf::base::input -``` +```bash --- @@ -425,7 +430,7 @@ private: ### 待创建文件 -``` +```text include/CFDesktop/Base/Input/ ├── InputManager.h # 统一分发层 ├── InputEvent.h # 输入事件定义 @@ -457,7 +462,7 @@ tests/unit/base/input/ ├── test_rotary_handler.cpp ├── test_gesture_recognizer.cpp └── test_focus_navigator.cpp -``` +```yaml --- diff --git a/document/todo/done/04_simulator_status.md b/document/todo/done/04_simulator_status.md index 68c715a3d..285664dbc 100644 --- a/document/todo/done/04_simulator_status.md +++ b/document/todo/done/04_simulator_status.md @@ -1,3 +1,8 @@ +--- +title: "Phase 4: 多平台模拟器 - 状态文档" +description: "模块ID: Phase 4,状态: **模块ID**: Phase 4 diff --git a/document/todo/done/05_testing_status.md b/document/todo/done/05_testing_status.md index 63cae7ecc..43138b1b1 100644 --- a/document/todo/done/05_testing_status.md +++ b/document/todo/done/05_testing_status.md @@ -1,3 +1,8 @@ +--- +title: "Phase 5: 测试系统 - 状态文档" +description: "模块ID: Phase 5,状态: 🚧 基础框架完成,UI控件测试缺失" +--- + # Phase 5: 测试系统 - 状态文档 > **模块ID**: Phase 5 @@ -55,7 +60,7 @@ ### 3.2 测试目录结构 -``` +```text test/ ├── CMakeLists.txt # 测试构建配置 ├── base/ # 基础工具库测试 @@ -92,7 +97,7 @@ test/ ├── boot_test_gui.cpp # GUI 启动测试 ├── CMakeLists.txt └── README.md # 启动测试文档 -``` +```bash --- @@ -223,7 +228,7 @@ test/ ## 五、测试金字塔 -``` +```text /\ / \ / E2E\ (端到端测试 - 少量) @@ -234,7 +239,7 @@ test/ /------------\ / 单元测试 \ (单元测试 - 大量,已完成 ~60%) /________________\ -``` +```yaml --- @@ -246,13 +251,13 @@ test/ # 从项目根目录 cmake -B out/build_test -DBUILD_TESTING=ON cmake --build out/build_test -``` +```text ### 运行所有测试 ```bash ./out/build_test/bin/cf_desktop_tests -``` +```text ### 运行特定测试 @@ -262,19 +267,19 @@ cmake --build out/build_test # 运行特定测试用例 ./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 --- diff --git a/document/todo/done/06_infrastructure_status.md b/document/todo/done/06_infrastructure_status.md index 86b0b4b97..bd1022bc9 100644 --- a/document/todo/done/06_infrastructure_status.md +++ b/document/todo/done/06_infrastructure_status.md @@ -1,3 +1,8 @@ +--- +title: "Phase A: 基础设施部分完成状态" +description: "状态: ✅ 部分完成,总体进度: ~50%" +--- + # Phase A: 基础设施部分完成状态 > **状态**: ✅ 部分完成 diff --git a/document/todo/done/13_widget_apps_status.md b/document/todo/done/13_widget_apps_status.md index 2c8588ab5..ab974e48e 100644 --- a/document/todo/done/13_widget_apps_status.md +++ b/document/todo/done/13_widget_apps_status.md @@ -1,3 +1,8 @@ +--- +title: "Phase G: Widget 应用与示例程序状态" +description: "完成日期: 2026-03-21,总体进度: 100%" +--- + # Phase G: Widget 应用与示例程序状态 > **状态**: ✅ 已完成 diff --git a/document/todo/done/14_display_backend_status.md b/document/todo/done/14_display_backend_status.md index b49fe1017..c3251f27e 100644 --- a/document/todo/done/14_display_backend_status.md +++ b/document/todo/done/14_display_backend_status.md @@ -1,3 +1,8 @@ +--- +title: "Phase H: 显示后端与窗口管理系统状态" +description: "状态: ✅ 核心完成,完成日期: 2026-03-30" +--- + # Phase H: 显示后端与窗口管理系统状态 > **状态**: ✅ 核心完成 diff --git a/document/todo/done/99_ui_material_framework_status.md b/document/todo/done/99_ui_material_framework_status.md index 685652d0e..853489c9c 100644 --- a/document/todo/done/99_ui_material_framework_status.md +++ b/document/todo/done/99_ui_material_framework_status.md @@ -1,3 +1,8 @@ +--- +title: "Phase 6: UI Material 框架 - 状态文档" +description: "模块ID: Phase 6,状态: 🚧 P1 控件完成,核心架构缺失" +--- + # Phase 6: UI Material 框架 - 状态文档 > **模块ID**: Phase 6 @@ -11,7 +16,7 @@ ### 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%) @@ -19,7 +24,7 @@ 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) @@ -37,12 +42,12 @@ Layer 0: Layout System (0%) - **新增:布局系统完全缺失** ### 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 --- @@ -86,7 +91,7 @@ ui/base -> QtCore only (no QtWidgets/QtGui) #### Token 系统架构 -``` +```bash Reference Token (MD3 spec) | v @@ -94,7 +99,7 @@ System Token (cfmaterial scheme) | v Component Token (widget-specific) -``` +```text #### 已完成文件 @@ -138,12 +143,12 @@ Component Token (widget-specific) #### 动画状态 -``` +```cpp Idle -> Running -> (Paused | Finished) | v Stopped -``` +```bash #### 支持的 Token 类型 - `md.animation.fadeIn` @@ -318,7 +323,7 @@ enum class PerformanceProfile { Embedded, // 降级: 30fps, 无阴影, 无涟漪, 仅状态动画 UltraLow // 最小: 无动画, 无阴影, 仅颜色 }; -``` +```bash #### 降级策略 @@ -359,7 +364,7 @@ private: MdElevationController* m_elevation; MdFocusIndicator* m_focusIndicator; }; -``` +```text ### 5.2 事件处理模式 @@ -369,7 +374,7 @@ void Widget::enterEvent(QEnterEvent* event) { m_stateMachine->onHoverEnter(); update(); } -``` +```text ### 5.3 标准绘制模式 @@ -395,7 +400,7 @@ void Widget::paintEvent(QPaintEvent* event) { m_focusIndicator->paint(&painter, rect()); } } -``` +```bash --- @@ -446,7 +451,7 @@ void Widget::paintEvent(QPaintEvent* event) { ### 已实现文件 **基础层 (Layer 1):** -``` +```text d:\ProjectHome\CFDesktop\ui\base\ ├── math_helper.h/cpp ├── color.h/cpp @@ -454,10 +459,10 @@ d:\ProjectHome\CFDesktop\ui\base\ ├── 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 @@ -474,10 +479,10 @@ d:\ProjectHome\CFDesktop\ui\core\ ├── radius_scale/ ├── typography/ └── theme_name/ -``` +```text **动画层 (Layer 3):** -``` +```text d:\ProjectHome\CFDesktop\ui\components\ ├── animation.h/cpp ├── timing_animation.h/cpp @@ -490,20 +495,20 @@ d:\ProjectHome\CFDesktop\ui\components\ ├── 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 ✅ @@ -524,12 +529,12 @@ d:\ProjectHome\CFDesktop\ui\widget\material\base\ ├── 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/ # 时间选择器 @@ -558,13 +563,13 @@ ui/widget/material/widget/ ├── bottomsheet/ # 底部面板 ├── alertdialog/ # 警告对话框 └── menu/ # 菜单 -``` +```text **Layer 6 - 性能配置:** -``` +```text d:\ProjectHome\CFDesktop\ui\core\ └── performance_profile.h/cpp -``` +```bash --- diff --git a/document/todo/done/PROJECT_STATUS_REPORT.md b/document/todo/done/PROJECT_STATUS_REPORT.md index ef4816a7f..0c7ed5771 100644 --- a/document/todo/done/PROJECT_STATUS_REPORT.md +++ b/document/todo/done/PROJECT_STATUS_REPORT.md @@ -1,3 +1,8 @@ +--- +title: CFDesktop 项目状态报告 +description: "报告日期: 2026-03-30,报告版本: v4.0" +--- + # CFDesktop 项目状态报告 > **报告日期**: 2026-03-30 @@ -459,7 +464,7 @@ CFDesktop 是一个基于 Qt6 的嵌入式桌面框架项目,采用 Material D ## 四、关键文件路径 -``` +```text CFDesktop/ ├── CMakeLists.txt # 主构建配置 (v0.13.1, C++23) ├── base/ # 基础库模块 @@ -490,7 +495,7 @@ CFDesktop/ ├── test/ # 测试代码 (55%覆盖率, 24个测试) ├── example/ # 示例程序 (70%覆盖, 80个示例) └── document/ # 文档 (80%覆盖, 271篇) -``` +```yaml --- diff --git a/document/todo/done/index.md b/document/todo/done/index.md index ab611a703..bbb0e7e1a 100644 --- a/document/todo/done/index.md +++ b/document/todo/done/index.md @@ -1,3 +1,8 @@ +--- +title: done +description: 已完成模块的状态文档索引,本目录记录 CFDesktop 各模块的完成状态和实现细节。 +--- + # done > 已完成模块的状态文档索引 diff --git a/document/todo/done/milestone_01_desktop_skeleton.md b/document/todo/done/milestone_01_desktop_skeleton.md index 77c4b187e..d3c7c41ea 100644 --- a/document/todo/done/milestone_01_desktop_skeleton.md +++ b/document/todo/done/milestone_01_desktop_skeleton.md @@ -1,3 +1,8 @@ +--- +title: "Milestone 1: 桌面骨架可见" +description: "状态: ✅ 已完成 (2026-04-09)" +--- + # Milestone 1: 桌面骨架可见 > **状态**: ✅ 已完成 (2026-04-09) diff --git a/document/todo/index.md b/document/todo/index.md index 658016fd7..4617709b3 100644 --- a/document/todo/index.md +++ b/document/todo/index.md @@ -1,3 +1,8 @@ +--- +title: CFDesktop 项目 TODO 索引 +description: "- 综合报告: PROJECTSTATUSREPORT.md" +--- + # CFDesktop 项目 TODO 索引 ## 快速导航 diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 5489edcf1..000000000 --- a/mkdocs.yml +++ /dev/null @@ -1,207 +0,0 @@ -site_name: DocHome For CFDesktop # 网站标题,显示在浏览器标签和页面顶部 -site_url: https://awesome-embedded-learning-studio.github.io/CFDesktop/ -site_description: CFDesktop的文档 # 网站描述,用于SEO优化 -site_author: CharlieChen # 作者名称,可以改成你的真实姓名或网名 - -# 版权信息,显示在页面底部 -copyright: Copyright © 2026 CharlieChen - 保留所有权利 - -# 文档源文件夹 -docs_dir: "document" - - -# ==================== 主题配置 ==================== -# Material 是一个现代化的 MkDocs 主题,提供了丰富的功能和美观的界面 - -theme: - name: material # 使用 Material 主题 - language: zh # 界面语言设置为简体中文 - - # 自定义网站图标和Logo - logo: Awesome-Embedded.png # 网站Logo - favicon: Awesome-Embedded.ico # 浏览器标签图标 - - # 调色板配置 - 支持亮色/暗色模式切换 - palette: - # 亮色模式 - - media: "(prefers-color-scheme: light)" - scheme: default # 使用默认亮色主题 - primary: indigo # 主色调:靛蓝色(导航栏等) - accent: indigo # 强调色(链接、按钮等) - toggle: - icon: material/brightness-7 # 切换图标 - name: 切换至暗色模式 - - # 暗色模式 - - media: "(prefers-color-scheme: dark)" - scheme: slate # 使用暗色主题 - primary: black # 主色调:黑色 - accent: indigo # 强调色保持一致 - toggle: - icon: material/brightness-4 # 切换图标 - name: 切换至亮色模式 - - # 字体配置 - font: - text: Noto Sans SC # 中文优先正文字体 - code: JetBrains Mono # 等宽字体,C++符号更清晰 - - # 功能特性开关 - features: - # ---------- 导航功能 ---------- - - navigation.instant # 即时加载,页面切换更流畅(类似SPA) - - navigation.instant.prefetch # 预加载链接,提升访问速度 - - navigation.instant.progress # 显示加载进度条 - - navigation.tracking # 地址栏自动更新为当前标题的锚点 - - navigation.tabs # 顶部显示主要章节标签(适合多章节博客) - - navigation.tabs.sticky # 滚动时标签栏保持固定 - - navigation.sections # 侧边栏显示章节分组 - - navigation.prune # 裁剪不可见的导航项,提升大站点性能 - - navigation.path # 显示当前页面的完整路径 - - navigation.indexes # 支持章节索引页 - - navigation.top # 显示"返回顶部"按钮 - - navigation.footer # 页面底部显示上一页/下一页导航 - - # ---------- 目录功能 ---------- - - toc.follow # 目录自动跟随滚动 - # toc.integrate 已移除: 右侧独立 TOC 面板提升阅读体验 - - # ---------- 搜索功能 ---------- - - search.suggest # 搜索时显示建议 - - search.highlight # 高亮显示搜索结果 - - search.share # 允许分享搜索结果链接 - - # ---------- 内容功能 ---------- - - content.code.copy # 代码块添加复制按钮 - - content.code.select # 代码块可以选择 - - content.code.annotate # 代码块支持注释 - - content.tabs.link # 内容标签页可以链接 - - content.tooltips # 鼠标悬停显示提示信息 - - content.action.edit # 显示"编辑此页"按钮 - - content.action.view # 显示"查看源代码"按钮 - - -# ==================== Markdown 扩展 ==================== -# 这些扩展增强了 Markdown 的功能,让你能写出更丰富的内容 - -markdown_extensions: - # ---------- 基础扩展 ---------- - - abbr # 支持缩写定义 - - attr_list # 允许为元素添加HTML属性 - - def_list # 支持定义列表 - - footnotes # 支持脚注 - - md_in_html # 允许在HTML中使用Markdown - - tables # 表格支持(标准Markdown已支持,这里确保启用) - - # 目录扩展 - - toc: - permalink: true # 标题旁显示永久链接符号(#) - permalink_title: 链接到此章节 # 永久链接的提示文字 - slugify: !!python/object/apply:pymdownx.slugs.slugify - kwds: - case: lower # URL中的标题转为小写 - - # 警告框扩展 - 可以创建提示、警告、危险等样式的信息框 - - admonition # 基础警告框支持 - - # ---------- PyMdown 扩展(强大的Markdown增强) ---------- - - # 细节折叠块 - 可折叠的内容区域 - - pymdownx.details - - # 代码高亮 - - pymdownx.highlight: - anchor_linenums: true # 代码行号可以被链接 - line_spans: __span # 每行代码单独包装 - pygments_lang_class: true # 添加语言类名 - linenums: true # 显示行号 - linenums_style: pymdownx-inline # 行号样式 - - # 行内代码高亮 - - pymdownx.inlinehilite - - # 代码块和其他内容的围栏支持 - - pymdownx.superfences: - custom_fences: - # 支持 Mermaid 图表 - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format - - # 内容标签页 - 可以创建多个标签切换的内容区域 - - pymdownx.tabbed: - alternate_style: true # 使用替代样式 - combine_header_slug: true # 合并标签头的slug - slugify: !!python/object/apply:pymdownx.slugs.slugify - kwds: - case: lower - - # Emoji 支持 - 可以使用 :smile: 这样的表情符号 - - pymdownx.emoji: - emoji_index: !!python/name:material.extensions.emoji.twemoji - emoji_generator: !!python/name:material.extensions.emoji.to_svg - - # 其他实用扩展 - - pymdownx.caret # 支持上标 ^text^ - - pymdownx.mark # 支持高亮标记 ==text== - - pymdownx.tilde # 支持删除线 ~~text~~ 和下标 ~text~ - - pymdownx.keys # 支持键盘按键显示 ++ctrl+alt+del++ - - pymdownx.smartsymbols # 智能符号替换 - - pymdownx.snippets # 支持包含其他文件的代码片段 - - pymdownx.critic # 支持批注和修订标记 - - pymdownx.betterem # 改进的强调语法 - - -# ==================== 插件配置 ==================== -# 插件为网站添加额外功能 - -plugins: - # 搜索插件 - 提供全站搜索功能 - - search: - separator: '[\s\u200b\-_,:!=\[\]()"/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])' # 中文分词支持 - lang: - - zh # 中文搜索 - - en # 英文搜索 - pipeline: - - stemmer - - stopWordFilter - - trimmer - - # Awesome Pages 插件 - 灵活的页面组织方式 - - awesome-pages - - # Git 修订日期插件 - 自动显示文章的创建和更新时间 - - git-revision-date-localized: - enable_creation_date: true # 显示创建日期 - fallback_to_build_date: true # 如果Git历史不可用,使用构建日期 - type: datetime # 日期格式: datetime(日期+时间) / date(仅日期) / iso_date / iso_datetime - timezone: Asia/Shanghai # 时区设置 - locale: zh # 本地化语言 - - -# ==================== 额外配置 ==================== - -# 社交媒体链接 - 显示在页面右上角 -extra: - # 社交媒体图标 - social: - - icon: fontawesome/brands/github # GitHub图标 - link: https://github.com/Awesome-Embedded-Learning-Studio # 你的GitHub主页 - name: GitHub - - icon: fontawesome/solid/paper-plane # 邮件图标 - link: mailto:725610365@qq.com - name: 发送邮件 - - -# ==================== 额外的CSS和JavaScript ==================== -# 可以添加自定义样式和脚本 - -extra_css: - - stylesheets/extra.css - - -# ==================== 注意事项 ==================== -# 本地预览命令: -# mkdocs serve # 启动本地服务器,访问 http://127.0.0.1:8000 -# -# ==================== 配置结束 ==================== diff --git a/package.json b/package.json new file mode 100644 index 000000000..df2e55314 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "cfdesktop-docs", + "version": "0.18.0", + "private": true, + "type": "module", + "packageManager": "pnpm@10.33.3", + "scripts": { + "dev": "vitepress dev site", + "build": "vitepress build site", + "preview": "vitepress preview site" + }, + "devDependencies": { + "vitepress": "1.6.4" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 000000000..003e6dd47 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1620 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + vitepress: + specifier: 1.6.4 + version: 1.6.4(@algolia/client-search@5.52.1)(postcss@8.5.15)(search-insights@2.17.3) + +packages: + + '@algolia/abtesting@1.18.1': + resolution: {integrity: sha512-aehCadlWOGvrT91KUIZpC0MbB8KBW9yUuvTJFd2xesR7le/IsT4nJUnjCCZ4ZqZCeTcPHPV5mo//fZ5oxcSVYw==} + engines: {node: '>= 14.0.0'} + + '@algolia/autocomplete-core@1.17.7': + resolution: {integrity: sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==} + + '@algolia/autocomplete-plugin-algolia-insights@1.17.7': + resolution: {integrity: sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==} + peerDependencies: + search-insights: '>= 1 < 3' + + '@algolia/autocomplete-preset-algolia@1.17.7': + resolution: {integrity: sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/autocomplete-shared@1.17.7': + resolution: {integrity: sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/client-abtesting@5.52.1': + resolution: {integrity: sha512-HmXOGBOAOJPounpBzBpuY0zDYeiCpxgHnQmuA7JO6ScukcBdGp3/XM9zJk5pJx/xNGD68mbPGXWpDxGtl6BwDQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-analytics@5.52.1': + resolution: {integrity: sha512-5oo4+I8iixie9vXhCyNFCzeIr8pqA3FQ//VsLHTDvZAV4ttYOPGvYHGQq5NSalrLx5Jc3dRro/5uDOlnUMcBJg==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-common@5.52.1': + resolution: {integrity: sha512-qCDoZfx5MpX7XQzvQ3bC4tSEMkQWQMaF/ABtLuoze03Y/flR563CCSws02qIJ23oX7lxl92LsilZjINVyTdtLw==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-insights@5.52.1': + resolution: {integrity: sha512-hnGs0/lsFJ2PWDxNBz7pxreXo/Xz7gxYRcfePBUjsH26ad0kU/sgnVZd9LwWBpsQv65z2jlb5dkyaB9WE9M9FQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-personalization@5.52.1': + resolution: {integrity: sha512-2VxxNc/uBysyKvGeBdSM5n9eIDKH8kWD7wd9/yqbJAiVwU4Yv6tU1LSJusHKrXV/aCu1KW7t9Gug9QyeEmtn/Q==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-query-suggestions@5.52.1': + resolution: {integrity: sha512-O6mPtsw3xEfNOe6gWFpYLeAZAIljNa4Hgna3bq15PwyN7nbjTY0wXJFRbzs/0YVf75Br+SbOQUmjKxXYjDiSiQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-search@5.52.1': + resolution: {integrity: sha512-gA8oJOV1LnQQkDf91iebNnFInHuW0gRPEgLSOQ7EfipCEjYTHm5swm1DlH9H5RaRw4RrHuzHBegnlzc0MAstcg==} + engines: {node: '>= 14.0.0'} + + '@algolia/ingestion@1.52.1': + resolution: {integrity: sha512-U9zZfc5xIu9wRxZkt+HceJUAD4VKHKbAyLSloJdEyMRmphXeibfrY9cxqIXBcmPeZzGhn3Imb35Dq8l19PkJhw==} + engines: {node: '>= 14.0.0'} + + '@algolia/monitoring@1.52.1': + resolution: {integrity: sha512-a3SGNceHmkQfq77iG8Ka+w1pvwfZa/0lzEIgse30fL0kD+yKnd/dg0dQvSfFPAEt2f21DMcGkDSSeJlO3KdQjQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/recommend@5.52.1': + resolution: {integrity: sha512-z98QEguCFDpxb4S/PyrUK1igqF8tPsdbqOUUO6ON91vJ58w+Gwa6ncrI0oNXSFcrkxA5EqPKPQ2A1PBCn08TYQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-browser-xhr@5.52.1': + resolution: {integrity: sha512-CI7+/0I11QeZM59Uc8whd2or0kqzFVjpaPn9Qpwll/krHcBAxk24WkAQ6WX+IwDVMfpont4YGbKwAmCre3vE8Q==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-fetch@5.52.1': + resolution: {integrity: sha512-S6bDuw9byfOvm3T71cgdoZgrgnZq6hpdMLkx52Louh57nUAmvGQESz2aojOynQHjbTiV55smvAFbgn0qT4tJrg==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-node-http@5.52.1': + resolution: {integrity: sha512-tqZXM+54rWo4mk5jL5Z/flE11nPmNEdXwFBM5py9DkOmbjeCNemfVd45FyM97XdzfZ0dl9uOJC6PYn1FpkeyQg==} + engines: {node: '>= 14.0.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@docsearch/css@3.8.2': + resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} + + '@docsearch/js@3.8.2': + resolution: {integrity: sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==} + + '@docsearch/react@3.8.2': + resolution: {integrity: sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==} + peerDependencies: + '@types/react': '>= 16.8.0 < 19.0.0' + react: '>= 16.8.0 < 19.0.0' + react-dom: '>= 16.8.0 < 19.0.0' + search-insights: '>= 1 < 3' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + search-insights: + optional: true + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@iconify-json/simple-icons@1.2.83': + resolution: {integrity: sha512-6Pp9V++XisT9RKH7FB4RLPqUDzcmLtSma0ovOEIoEWGrXtHwBFsH7oN1z8vvCVCb95fb87QgR46/zRLyN9Y3kg==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + cpu: [x64] + os: [win32] + + '@shikijs/core@2.5.0': + resolution: {integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==} + + '@shikijs/engine-javascript@2.5.0': + resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==} + + '@shikijs/engine-oniguruma@2.5.0': + resolution: {integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==} + + '@shikijs/langs@2.5.0': + resolution: {integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==} + + '@shikijs/themes@2.5.0': + resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==} + + '@shikijs/transformers@2.5.0': + resolution: {integrity: sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==} + + '@shikijs/types@2.5.0': + resolution: {integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@ungap/structured-clone@1.3.1': + resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.34': + resolution: {integrity: sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==} + + '@vue/compiler-dom@3.5.34': + resolution: {integrity: sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==} + + '@vue/compiler-sfc@3.5.34': + resolution: {integrity: sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==} + + '@vue/compiler-ssr@3.5.34': + resolution: {integrity: sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==} + + '@vue/devtools-api@7.7.9': + resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + + '@vue/devtools-kit@7.7.9': + resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + + '@vue/devtools-shared@7.7.9': + resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + + '@vue/reactivity@3.5.34': + resolution: {integrity: sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==} + + '@vue/runtime-core@3.5.34': + resolution: {integrity: sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==} + + '@vue/runtime-dom@3.5.34': + resolution: {integrity: sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==} + + '@vue/server-renderer@3.5.34': + resolution: {integrity: sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==} + peerDependencies: + vue: 3.5.34 + + '@vue/shared@3.5.34': + resolution: {integrity: sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==} + + '@vueuse/core@12.8.2': + resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} + + '@vueuse/integrations@12.8.2': + resolution: {integrity: sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + + '@vueuse/metadata@12.8.2': + resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} + + '@vueuse/shared@12.8.2': + resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} + + algoliasearch@5.52.1: + resolution: {integrity: sha512-fHA8+kXTbjagw3jkLiaS7KKrH8qe2DyOsiUhGlN4cdT77PEsfqXZl7ewDk1hsg+pJnPlnE50XtLxjR91iJOpmg==} + engines: {node: '>= 14.0.0'} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + focus-trap@7.8.0: + resolution: {integrity: sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mark.js@8.11.1: + resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + minisearch@7.2.0: + resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + oniguruma-to-es@3.1.1: + resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + preact@10.29.2: + resolution: {integrity: sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + search-insights@2.17.3: + resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} + + shiki@2.5.0: + resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitepress@1.6.4: + resolution: {integrity: sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==} + hasBin: true + peerDependencies: + markdown-it-mathjax3: ^4 + postcss: ^8 + peerDependenciesMeta: + markdown-it-mathjax3: + optional: true + postcss: + optional: true + + vue@3.5.34: + resolution: {integrity: sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@algolia/abtesting@1.18.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/autocomplete-core@1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-plugin-algolia-insights': 1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1)(search-insights@2.17.3) + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1) + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + - search-insights + + '@algolia/autocomplete-plugin-algolia-insights@1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1) + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + + '@algolia/autocomplete-preset-algolia@1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1)': + dependencies: + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1) + '@algolia/client-search': 5.52.1 + algoliasearch: 5.52.1 + + '@algolia/autocomplete-shared@1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1)': + dependencies: + '@algolia/client-search': 5.52.1 + algoliasearch: 5.52.1 + + '@algolia/client-abtesting@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/client-analytics@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/client-common@5.52.1': {} + + '@algolia/client-insights@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/client-personalization@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/client-query-suggestions@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/client-search@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/ingestion@1.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/monitoring@1.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/recommend@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/requester-browser-xhr@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + + '@algolia/requester-fetch@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + + '@algolia/requester-node-http@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@docsearch/css@3.8.2': {} + + '@docsearch/js@3.8.2(@algolia/client-search@5.52.1)(search-insights@2.17.3)': + dependencies: + '@docsearch/react': 3.8.2(@algolia/client-search@5.52.1)(search-insights@2.17.3) + preact: 10.29.2 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/react' + - react + - react-dom + - search-insights + + '@docsearch/react@3.8.2(@algolia/client-search@5.52.1)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1)(search-insights@2.17.3) + '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1) + '@docsearch/css': 3.8.2 + algoliasearch: 5.52.1 + optionalDependencies: + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@iconify-json/simple-icons@1.2.83': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@rollup/rollup-android-arm-eabi@4.60.4': + optional: true + + '@rollup/rollup-android-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-x64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.4': + optional: true + + '@shikijs/core@2.5.0': + dependencies: + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 3.1.1 + + '@shikijs/engine-oniguruma@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + + '@shikijs/themes@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + + '@shikijs/transformers@2.5.0': + dependencies: + '@shikijs/core': 2.5.0 + '@shikijs/types': 2.5.0 + + '@shikijs/types@2.5.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + + '@types/unist@3.0.3': {} + + '@types/web-bluetooth@0.0.21': {} + + '@ungap/structured-clone@1.3.1': {} + + '@vitejs/plugin-vue@5.2.4(vite@5.4.21)(vue@3.5.34)': + dependencies: + vite: 5.4.21 + vue: 3.5.34 + + '@vue/compiler-core@3.5.34': + dependencies: + '@babel/parser': 7.29.3 + '@vue/shared': 3.5.34 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.34': + dependencies: + '@vue/compiler-core': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/compiler-sfc@3.5.34': + dependencies: + '@babel/parser': 7.29.3 + '@vue/compiler-core': 3.5.34 + '@vue/compiler-dom': 3.5.34 + '@vue/compiler-ssr': 3.5.34 + '@vue/shared': 3.5.34 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.15 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.34': + dependencies: + '@vue/compiler-dom': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/devtools-api@7.7.9': + dependencies: + '@vue/devtools-kit': 7.7.9 + + '@vue/devtools-kit@7.7.9': + dependencies: + '@vue/devtools-shared': 7.7.9 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-shared@7.7.9': + dependencies: + rfdc: 1.4.1 + + '@vue/reactivity@3.5.34': + dependencies: + '@vue/shared': 3.5.34 + + '@vue/runtime-core@3.5.34': + dependencies: + '@vue/reactivity': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/runtime-dom@3.5.34': + dependencies: + '@vue/reactivity': 3.5.34 + '@vue/runtime-core': 3.5.34 + '@vue/shared': 3.5.34 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.34(vue@3.5.34)': + dependencies: + '@vue/compiler-ssr': 3.5.34 + '@vue/shared': 3.5.34 + vue: 3.5.34 + + '@vue/shared@3.5.34': {} + + '@vueuse/core@12.8.2': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 12.8.2 + '@vueuse/shared': 12.8.2 + vue: 3.5.34 + transitivePeerDependencies: + - typescript + + '@vueuse/integrations@12.8.2(focus-trap@7.8.0)': + dependencies: + '@vueuse/core': 12.8.2 + '@vueuse/shared': 12.8.2 + vue: 3.5.34 + optionalDependencies: + focus-trap: 7.8.0 + transitivePeerDependencies: + - typescript + + '@vueuse/metadata@12.8.2': {} + + '@vueuse/shared@12.8.2': + dependencies: + vue: 3.5.34 + transitivePeerDependencies: + - typescript + + algoliasearch@5.52.1: + dependencies: + '@algolia/abtesting': 1.18.1 + '@algolia/client-abtesting': 5.52.1 + '@algolia/client-analytics': 5.52.1 + '@algolia/client-common': 5.52.1 + '@algolia/client-insights': 5.52.1 + '@algolia/client-personalization': 5.52.1 + '@algolia/client-query-suggestions': 5.52.1 + '@algolia/client-search': 5.52.1 + '@algolia/ingestion': 1.52.1 + '@algolia/monitoring': 1.52.1 + '@algolia/recommend': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + birpc@2.9.0: {} + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + comma-separated-tokens@2.0.3: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + csstype@3.2.3: {} + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + emoji-regex-xs@1.0.0: {} + + entities@7.0.1: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + estree-walker@2.0.2: {} + + focus-trap@7.8.0: + dependencies: + tabbable: 6.4.0 + + fsevents@2.3.3: + optional: true + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hookable@5.5.3: {} + + html-void-elements@3.0.0: {} + + is-what@5.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mark.js@8.11.1: {} + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.1 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + minisearch@7.2.0: {} + + mitt@3.0.1: {} + + nanoid@3.3.12: {} + + oniguruma-to-es@3.1.1: + dependencies: + emoji-regex-xs: 1.0.0 + regex: 6.1.0 + regex-recursion: 6.0.2 + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + preact@10.29.2: {} + + property-information@7.1.0: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + rfdc@1.4.1: {} + + rollup@4.60.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 + fsevents: 2.3.3 + + search-insights@2.17.3: {} + + shiki@2.5.0: + dependencies: + '@shikijs/core': 2.5.0 + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/langs': 2.5.0 + '@shikijs/themes': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + speakingurl@14.0.1: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + tabbable@6.4.0: {} + + trim-lines@3.0.1: {} + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@5.4.21: + dependencies: + esbuild: 0.21.5 + postcss: 8.5.15 + rollup: 4.60.4 + optionalDependencies: + fsevents: 2.3.3 + + vitepress@1.6.4(@algolia/client-search@5.52.1)(postcss@8.5.15)(search-insights@2.17.3): + dependencies: + '@docsearch/css': 3.8.2 + '@docsearch/js': 3.8.2(@algolia/client-search@5.52.1)(search-insights@2.17.3) + '@iconify-json/simple-icons': 1.2.83 + '@shikijs/core': 2.5.0 + '@shikijs/transformers': 2.5.0 + '@shikijs/types': 2.5.0 + '@types/markdown-it': 14.1.2 + '@vitejs/plugin-vue': 5.2.4(vite@5.4.21)(vue@3.5.34) + '@vue/devtools-api': 7.7.9 + '@vue/shared': 3.5.34 + '@vueuse/core': 12.8.2 + '@vueuse/integrations': 12.8.2(focus-trap@7.8.0) + focus-trap: 7.8.0 + mark.js: 8.11.1 + minisearch: 7.2.0 + shiki: 2.5.0 + vite: 5.4.21 + vue: 3.5.34 + optionalDependencies: + postcss: 8.5.15 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/node' + - '@types/react' + - async-validator + - axios + - change-case + - drauu + - fuse.js + - idb-keyval + - jwt-decode + - less + - lightningcss + - nprogress + - qrcode + - react + - react-dom + - sass + - sass-embedded + - search-insights + - sortablejs + - stylus + - sugarss + - terser + - typescript + - universal-cookie + + vue@3.5.34: + dependencies: + '@vue/compiler-dom': 3.5.34 + '@vue/compiler-sfc': 3.5.34 + '@vue/runtime-dom': 3.5.34 + '@vue/server-renderer': 3.5.34(vue@3.5.34) + '@vue/shared': 3.5.34 + + zwitch@2.0.4: {} diff --git a/project.config.ts b/project.config.ts new file mode 100644 index 000000000..edde6f41d --- /dev/null +++ b/project.config.ts @@ -0,0 +1,62 @@ +export interface SidebarVolume { + name: string; + srcDir: string; + urlPrefix: string; +} + +export interface ProjectConfig { + name: string; + title: string; + description: string; + base: string; + documentsDir: string; + github: { + owner: string; + repo: string; + branch: string; + documentsPath: string; + }; + nav: Array<{ text: string; link: string }>; + sidebar: { + volumes: SidebarVolume[]; + }; +} + +const projectConfig: ProjectConfig = { + name: "CFDesktop", + title: "CFDesktop 文档", + description: "面向嵌入式设备的 Qt / Material Design 3 桌面框架", + base: "/CFDesktop/", + documentsDir: "document", + github: { + owner: "Awesome-Embedded-Learning-Studio", + repo: "CFDesktop", + branch: "develop", + documentsPath: "document" + }, + nav: [ + { text: "首页", link: "/" }, + { text: "状态", link: "/status/current" }, + { text: "开发", link: "/development/" }, + { text: "架构", link: "/design_stage/system_architecture_overview" }, + { text: "桌面路线图", link: "/todo/desktop/" }, + { text: "手册", link: "/HandBook/" }, + { text: "GitHub", link: "https://github.com/Awesome-Embedded-Learning-Studio/CFDesktop" } + ], + sidebar: { + volumes: [ + { name: "development", srcDir: "development", urlPrefix: "/development" }, + { name: "status", srcDir: "status", urlPrefix: "/status" }, + { name: "ci", srcDir: "ci", urlPrefix: "/ci" }, + { name: "architecture", srcDir: "design_stage", urlPrefix: "/design_stage" }, + { name: "desktop", srcDir: "desktop", urlPrefix: "/desktop" }, + { name: "handbook", srcDir: "HandBook", urlPrefix: "/HandBook" }, + { name: "todo", srcDir: "todo", urlPrefix: "/todo" }, + { name: "scripts", srcDir: "scripts", urlPrefix: "/scripts" }, + { name: "notes", srcDir: "notes", urlPrefix: "/notes" }, + { name: "release", srcDir: "release_rule", urlPrefix: "/release_rule" } + ] + } +}; + +export default projectConfig; diff --git a/scripts/document/cfdesktop_docs.egg-info/PKG-INFO b/scripts/document/cfdesktop_docs.egg-info/PKG-INFO deleted file mode 100644 index 739d6c78b..000000000 --- a/scripts/document/cfdesktop_docs.egg-info/PKG-INFO +++ /dev/null @@ -1,9 +0,0 @@ -Metadata-Version: 2.4 -Name: cfdesktop-docs -Version: 0.1.0 -Summary: CFDesktop documentation development environment -Requires-Python: >=3.10 -Requires-Dist: mkdocs>=1.5.0 -Requires-Dist: mkdocs-material>=9.5.0 -Requires-Dist: mkdocs-awesome-pages-plugin>=2.9.0 -Requires-Dist: mkdocs-git-revision-date-localized-plugin>=1.2.0 diff --git a/scripts/document/cfdesktop_docs.egg-info/SOURCES.txt b/scripts/document/cfdesktop_docs.egg-info/SOURCES.txt deleted file mode 100644 index 5801884e9..000000000 --- a/scripts/document/cfdesktop_docs.egg-info/SOURCES.txt +++ /dev/null @@ -1,6 +0,0 @@ -pyproject.toml -cfdesktop_docs.egg-info/PKG-INFO -cfdesktop_docs.egg-info/SOURCES.txt -cfdesktop_docs.egg-info/dependency_links.txt -cfdesktop_docs.egg-info/requires.txt -cfdesktop_docs.egg-info/top_level.txt \ No newline at end of file diff --git a/scripts/document/cfdesktop_docs.egg-info/dependency_links.txt b/scripts/document/cfdesktop_docs.egg-info/dependency_links.txt deleted file mode 100644 index 8b1378917..000000000 --- a/scripts/document/cfdesktop_docs.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/scripts/document/cfdesktop_docs.egg-info/requires.txt b/scripts/document/cfdesktop_docs.egg-info/requires.txt deleted file mode 100644 index e06f2f64c..000000000 --- a/scripts/document/cfdesktop_docs.egg-info/requires.txt +++ /dev/null @@ -1,4 +0,0 @@ -mkdocs>=1.5.0 -mkdocs-material>=9.5.0 -mkdocs-awesome-pages-plugin>=2.9.0 -mkdocs-git-revision-date-localized-plugin>=1.2.0 diff --git a/scripts/document/cfdesktop_docs.egg-info/top_level.txt b/scripts/document/cfdesktop_docs.egg-info/top_level.txt deleted file mode 100644 index 8b1378917..000000000 --- a/scripts/document/cfdesktop_docs.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/scripts/document/check_404_links.py b/scripts/document/check_404_links.py deleted file mode 100644 index 7782d8077..000000000 --- a/scripts/document/check_404_links.py +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/env python3 -""" -MkDocs 404 Link Scanner -======================== -Scans the MkDocs development server for broken internal links. - -Usage: - .venv/bin/python scripts/document/check_404_links.py # HandBook only - .venv/bin/python scripts/document/check_404_links.py --all # All pages - .venv/bin/python scripts/document/check_404_links.py --api-only # API pages only - -Strategy: - Phase 1 - Collect all internal links from every page (concurrent) - Phase 2 - Check each *unique* resolved URL exactly once (no duplicates) - Phase 3 - Map broken URLs back to their source pages -""" - -import re -import sys -from collections import defaultdict -from concurrent.futures import ThreadPoolExecutor, as_completed -from html.parser import HTMLParser -from urllib.parse import urljoin - -import requests - -BASE_URL = "http://127.0.0.1:8000/CFDesktop/" -MAX_WORKERS = 8 - - -class LinkExtractor(HTMLParser): - def __init__(self): - super().__init__() - self.links = [] - - def handle_starttag(self, tag, attrs): - if tag == "a": - for attr, value in attrs: - if attr == "href" and value: - self.links.append(value) - - -def is_internal(href): - if not href: - return False - if href.startswith(("#", "javascript:", "mailto:", "tel:")): - return False - if href.startswith("http") and BASE_URL.rstrip("/") not in href: - return False - if re.search(r"\.(css|js|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot)(\?|$)", href): - return False - return True - - -def fetch(url, timeout=10): - try: - resp = requests.get(url, timeout=timeout) - return resp.status_code, resp.text - except requests.RequestException: - return None, "" - - -def collect_links(page_url): - """Fetch a page and return (page_url, [resolved_links]).""" - status, html = fetch(page_url) - if status != 200: - return page_url, [] - parser = LinkExtractor() - try: - parser.feed(html) - except Exception: - return page_url, [] - - resolved = set() - for href in parser.links: - if not is_internal(href): - continue - resolved.add(urljoin(page_url, href).split("#")[0]) - return page_url, resolved - - -def main(): - scan_all = "--all" in sys.argv - scan_api = "--api-only" in sys.argv - - print("MkDocs 404 Link Scanner") - print(f"Target: {BASE_URL}") - if scan_all: - print("Mode: all pages (including API)") - elif scan_api: - print("Mode: API pages only") - else: - print("Mode: HandBook pages only (use --all for full scan)") - print("=" * 60) - - # --- Phase 1: Collect all links --- - print("\n[Phase 1] Collecting links from all pages...") - sitemap_url = urljoin(BASE_URL, "sitemap.xml") - status, sitemap_xml = fetch(sitemap_url) - if status != 200: - print(f"ERROR: Cannot fetch sitemap (HTTP {status})") - print("Make sure MkDocs dev server is running: mkdocs serve") - sys.exit(1) - - page_urls = re.findall(r"(.*?)", sitemap_xml) - page_urls = [u for u in page_urls if BASE_URL.rstrip("/") in u and "sitemaps.org" not in u] - - if not scan_all and not scan_api: - page_urls = [u for u in page_urls if "/api/" not in u] - elif scan_api: - page_urls = [u for u in page_urls if "/api/" in u] - - print(f" Pages to scan: {len(page_urls)}") - - # url_to_sources: resolved_url -> set of source pages - url_to_sources = defaultdict(set) - # raw_href_map: resolved_url -> set of raw href strings - raw_href_map = defaultdict(set) - - with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool: - futures = {pool.submit(collect_links, url): url for url in page_urls} - done = 0 - for future in as_completed(futures): - done += 1 - if done % 20 == 0 or done == len(page_urls): - print(f"\r Collecting: [{done * 100 // len(page_urls):3d}%] {done}/{len(page_urls)}", end="", flush=True) - page_url, links = future.result() - for link in links: - url_to_sources[link].add(page_url) - - print(f"\n Found {len(url_to_sources)} unique URLs to check") - - # --- Phase 2: Check each unique URL exactly once --- - print(f"\n[Phase 2] Checking {len(url_to_sources)} unique URLs...") - broken_targets = {} # url -> status_code - - unique_urls = list(url_to_sources.keys()) - with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool: - futures = {pool.submit(fetch, url): url for url in unique_urls} - done = 0 - for future in as_completed(futures): - done += 1 - if done % 50 == 0 or done == len(unique_urls): - print(f"\r Checking: [{done * 100 // len(unique_urls):3d}%] {done}/{len(unique_urls)}", end="", flush=True) - url = futures[future] - link_status, _ = future.result() - if link_status == 404: - broken_targets[url] = link_status - - print() - - # --- Phase 3: Report --- - print(f"\n[Phase 3] Results") - print("=" * 60) - - if not broken_targets: - print("\n All internal links are valid. No 404s found.") - sys.exit(0) - - print(f"\n Found {len(broken_targets)} broken target(s):\n") - - for i, target in enumerate(sorted(broken_targets), 1): - sources = sorted(url_to_sources[target]) - print(f" {i}. 404 --> {target}") - print(f" Referenced from {len(sources)} page(s):") - for src in sources[:5]: - print(f" - {src}") - if len(sources) > 5: - print(f" ... and {len(sources) - 5} more") - print() - - print("=" * 60) - print(f" Broken targets: {len(broken_targets)}") - print(f" Total references: {sum(len(url_to_sources[t]) for t in broken_targets)}") - - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/scripts/document/fix_doxybook_links.py b/scripts/document/fix_doxybook_links.py index f825ba879..ed4b7fc83 100644 --- a/scripts/document/fix_doxybook_links.py +++ b/scripts/document/fix_doxybook_links.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 """ -Fix doxybook-generated API doc links for MkDocs. +Fix doxybook-generated API doc links for Markdown documentation sites. Doxybook generates links like `Classes/xxx.md` assuming they're relative to -the api/ root, but MkDocs resolves them relative to the file's actual location. -This script adds the correct `../` prefixes. +the api/ root, but Markdown site generators resolve them relative to the +file's actual location. This script adds the correct `../` prefixes. Usage: python scripts/document/fix_doxybook_links.py -Run this AFTER doxybook generates the docs, BEFORE mkdocs build/serve. +Run this after doxybook generates the docs and before publishing API pages. """ import os import re diff --git a/scripts/document/mkdocs_dev.ps1 b/scripts/document/mkdocs_dev.ps1 deleted file mode 100644 index 4350c909d..000000000 --- a/scripts/document/mkdocs_dev.ps1 +++ /dev/null @@ -1,328 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - MkDocs documentation development environment manager - -.DESCRIPTION - Manages Python virtual environment and MkDocs documentation development workflow. - Supports serving, building, API documentation generation, and environment management. - -.PARAMETER Command - The command to execute: - - serve: Start MkDocs dev server (default) - - build: Build static site to out/docs/site/ - - api: Run Doxygen + Doxybook2 API documentation pipeline - - install: Create/update virtual environment and install dependencies - - clean: Clean build artifacts (out/docs/, __pycache__, Doxygen XML) - - reset: Delete and recreate .venv - - help: Show this help message - -.PARAMETER Port - Dev server port (default: 8000) - -.PARAMETER Bind - Dev server bind address (default: 127.0.0.1) - -.PARAMETER Verbose - Enable verbose output - -.EXAMPLE - .\scripts\document\mkdocs_dev.ps1 serve - Start MkDocs dev server on default port - -.EXAMPLE - .\scripts\document\mkdocs_dev.ps1 serve -Port 3000 - Start MkDocs dev server on port 3000 - -.EXAMPLE - .\scripts\document\mkdocs_dev.ps1 build - Build static site to out/docs/site/ - -.EXAMPLE - .\scripts\document\mkdocs_dev.ps1 api - Generate API documentation from C++ source - -.EXAMPLE - .\scripts\document\mkdocs_dev.ps1 install - Create/update virtual environment - -.EXAMPLE - .\scripts\document\mkdocs_dev.ps1 reset - Delete and recreate .venv -#> - -param( - [Parameter(Position=0)] - [ValidateSet("serve", "build", "api", "install", "clean", "reset", "help")] - [string]$Command = "serve", - - [Parameter()] - [int]$Port = 8000, - - [Parameter()] - [string]$Bind = "127.0.0.1", - - [Parameter()] - [switch]$VerboseFlag -) - -# Import library modules -$LibDir = Join-Path (Split-Path -Parent $PSScriptRoot) "lib\powershell" -Import-Module (Join-Path $LibDir "LibCommon.psm1") -Force - -# Constants -$VenvDir = ".venv" -$DepsMarker = ".deps_installed" -$DocsOutputDir = "out\docs\site" -$DoxygenXmlDir = "xml" -$DoxygenApiDir = "document\api" -$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path -$ScriptDir = $PSScriptRoot - -# ============================================================================= -# Python environment -# ============================================================================= - -function Test-Python { - $python = Get-Command "python" -ErrorAction SilentlyContinue - if ($null -eq $python) { - $python = Get-Command "python3" -ErrorAction SilentlyContinue - } - if ($null -eq $python) { - Write-LogError "Python 3 未安装,请先安装 Python >= 3.10" - Write-Host " Windows: https://www.python.org/downloads/" -ForegroundColor Gray - return $false - } - - $version = & $python.Source --version 2>&1 - if ($VerboseFlag) { Write-LogInfo "[VERBOSE] Found: $version" } - return $true -} - -function New-Venv { - $venvPath = Join-Path $ProjectRoot $VenvDir - - if (Test-Path $venvPath) { - if ($VerboseFlag) { Write-LogInfo "[VERBOSE] 虚拟环境已存在: $venvPath" } - return - } - - Write-LogInfo "创建 Python 虚拟环境: $venvPath" - $python = Get-Command "python" -ErrorAction SilentlyContinue - if ($null -eq $python) { - $python = Get-Command "python3" -ErrorAction SilentlyContinue - } - & $python.Source -m venv $venvPath - - if (-not (Test-Path $venvPath)) { - Write-LogError "虚拟环境创建失败" - exit 1 - } - - Write-LogSuccess "虚拟环境创建成功" -} - -function Enable-Venv { - $activateScript = Join-Path $ProjectRoot "$VenvDir\Scripts\Activate.ps1" - - if (-not (Test-Path $activateScript)) { - Write-LogError "虚拟环境激活脚本不存在: $activateScript" - Write-LogInfo "请先运行: .\scripts\document\mkdocs_dev.ps1 install" - exit 1 - } - - . $activateScript - if ($VerboseFlag) { Write-LogInfo "[VERBOSE] 虚拟环境已激活" } -} - -function Install-DocDeps { - $marker = Join-Path $ProjectRoot "$VenvDir\$DepsMarker" - $pyproject = Join-Path $ScriptDir "pyproject.toml" - - # Check if deps are already installed and pyproject hasn't changed - if (Test-Path $marker) { - $markerHash = Get-Content $marker -ErrorAction SilentlyContinue - $pyprojectHash = (Get-FileHash $pyproject -Algorithm MD5).Hash - - if ($markerHash -eq $pyprojectHash) { - if ($VerboseFlag) { Write-LogInfo "[VERBOSE] 依赖已安装且未变更,跳过安装" } - return - } - } - - Write-LogInfo "安装文档开发依赖..." - pip install --upgrade pip --quiet - pip install -e $ScriptDir --quiet - - # Write marker with pyproject hash for change detection - (Get-FileHash $pyproject -Algorithm MD5).Hash | Set-Content $marker - Write-LogSuccess "依赖安装完成" -} - -function Initialize-DocEnv { - if (-not (Test-Python)) { exit 1 } - New-Venv - Enable-Venv - Install-DocDeps -} - -# ============================================================================= -# Command implementations -# ============================================================================= - -function Invoke-Serve { - Initialize-DocEnv - - Write-Host "" - Write-Host "=== MkDocs 开发服务器 ===" -ForegroundColor Cyan - Write-LogInfo "地址: http://${Bind}:${Port}" - Write-LogInfo "文档: $ProjectRoot\document" - Write-Host "" - - Push-Location $ProjectRoot - try { - mkdocs serve --dev-addr="${Bind}:${Port}" - } finally { - Pop-Location - } -} - -function Invoke-Build { - Initialize-DocEnv - - $outputPath = Join-Path $ProjectRoot $DocsOutputDir - - Write-Host "" - Write-Host "=== MkDocs 构建 ===" -ForegroundColor Cyan - Write-LogInfo "输出: $outputPath" - Write-Host "" - - Push-Location $ProjectRoot - try { - mkdocs build --clean -d $DocsOutputDir - } finally { - Pop-Location - } - - Write-LogSuccess "构建完成: $outputPath" -} - -function Invoke-Api { - # Check external tools - $doxygen = Get-Command "doxygen" -ErrorAction SilentlyContinue - if ($null -eq $doxygen) { - Write-LogError "Doxygen 未安装" - Write-Host " Windows: https://www.doxygen.nl/download.html" -ForegroundColor Gray - exit 1 - } - - $doxybook2 = Get-Command "doxybook2" -ErrorAction SilentlyContinue - if ($null -eq $doxybook2) { - Write-LogError "Doxybook2 未安装" - Write-Host " 请从 https://github.com/matusnovak/doxybook2/releases 下载" -ForegroundColor Gray - exit 1 - } - - Write-Host "" - Write-Host "=== API 文档管线 ===" -ForegroundColor Cyan - Write-Host "" - - Push-Location $ProjectRoot - try { - # Step 1: Run Doxygen - Write-LogInfo "[1/3] 运行 Doxygen..." - & doxygen Doxyfile - - # Step 2: Clean old API docs - Write-LogInfo "[2/3] 清理旧 API 文档..." - if (Test-Path $DoxygenApiDir) { - Remove-Item -Recurse -Force $DoxygenApiDir - } - New-Item -ItemType Directory -Path $DoxygenApiDir -Force | Out-Null - - # Step 3: Run Doxybook2 - Write-LogInfo "[3/3] 运行 Doxybook2..." - & doxybook2 --input ".\$DoxygenXmlDir" ` - --output ".\$DoxygenApiDir" ` - --config doxybook.json - - Write-LogSuccess "API 文档已生成到 $DoxygenApiDir" - } finally { - Pop-Location - } -} - -function Invoke-Install { - Initialize-DocEnv - Write-LogSuccess "环境准备就绪" -} - -function Invoke-Clean { - Write-Host "" - Write-Host "=== 清理构建产物 ===" -ForegroundColor Cyan - - $cleaned = 0 - - # Clean MkDocs output - $docsPath = Join-Path $ProjectRoot $DocsOutputDir - if (Test-Path $docsPath) { - Remove-Item -Recurse -Force $docsPath - Write-LogInfo "已清理: $DocsOutputDir" - $cleaned++ - } - - # Clean Doxygen XML - $xmlPath = Join-Path $ProjectRoot $DoxygenXmlDir - if (Test-Path $xmlPath) { - Remove-Item -Recurse -Force $xmlPath - Write-LogInfo "已清理: $DoxygenXmlDir" - $cleaned++ - } - - # Clean __pycache__ - $pycacheDirs = Get-ChildItem -Path $ProjectRoot -Directory -Recurse -Filter "__pycache__" -ErrorAction SilentlyContinue - if ($pycacheDirs.Count -gt 0) { - $pycacheDirs | Remove-Item -Recurse -Force - Write-LogInfo "已清理: __pycache__ ($($pycacheDirs.Count) 个)" - $cleaned++ - } - - if ($cleaned -eq 0) { - Write-LogInfo "没有需要清理的内容" - } else { - Write-LogSuccess "清理完成 ($cleaned 项)" - } -} - -function Invoke-Reset { - $venvPath = Join-Path $ProjectRoot $VenvDir - - Write-Host "" - Write-Host "=== 重置虚拟环境 ===" -ForegroundColor Cyan - - if (Test-Path $venvPath) { - Write-LogInfo "删除: $venvPath" - Remove-Item -Recurse -Force $venvPath - Write-LogSuccess "已删除旧环境" - } - - # Recreate - Initialize-DocEnv - Write-LogSuccess "虚拟环境已重建" -} - -# ============================================================================= -# Execute command -# ============================================================================= - -$ErrorActionPreference = "Stop" - -switch ($Command) { - "serve" { Invoke-Serve } - "build" { Invoke-Build } - "api" { Invoke-Api } - "install" { Invoke-Install } - "clean" { Invoke-Clean } - "reset" { Invoke-Reset } - "help" { Get-Help $MyInvocation.MyCommand.Path -Detailed } -} diff --git a/scripts/document/mkdocs_dev.sh b/scripts/document/mkdocs_dev.sh deleted file mode 100755 index 71e84bdef..000000000 --- a/scripts/document/mkdocs_dev.sh +++ /dev/null @@ -1,332 +0,0 @@ -#!/usr/bin/env bash -## -## @file mkdocs_dev.sh -## @brief MkDocs documentation development environment manager -## @date 2026-04-06 -## -## Usage: -## ./scripts/document/mkdocs_dev.sh [OPTIONS] -## -## Commands: -## serve Start MkDocs dev server (default) -## build Build static site to out/docs/site/ -## api Run Doxygen + Doxybook2 API documentation pipeline -## install Create/update virtual environment and install dependencies -## clean Clean build artifacts (out/docs/, __pycache__, Doxygen XML) -## reset Delete and recreate .venv -## help Show this help message -## -## Options: -## -p, --port PORT Dev server port (default: 8000) -## -b, --bind ADDR Dev server bind address (default: 127.0.0.1) -## -v, --verbose Enable verbose output -## -h, --help Show this help message -## - -set -eo pipefail - -# === Constants === -readonly MIN_PYTHON_MAJOR=3 -readonly MIN_PYTHON_MINOR=10 -readonly VENV_DIR=".venv" -readonly DEPS_MARKER=".deps_installed" -readonly DOCS_OUTPUT_DIR="out/docs/site" -readonly DOXYGEN_XML_DIR="xml" -readonly DOXYGEN_API_DIR="document/api" -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" - -# === Source libraries === -source "$SCRIPT_DIR/../lib/bash/lib_common.sh" - -# === Global variables === -DEV_PORT=8000 -DEV_ADDR="127.0.0.1" -VERBOSE=false - -# ============================================================================= -# Utility functions -# ============================================================================= - -verbose_log() { - if [[ "$VERBOSE" == "true" ]]; then - log_info "[VERBOSE] $*" - fi -} - -# ============================================================================= -# Python environment -# ============================================================================= - -check_python() { - if ! command -v python3 &>/dev/null; then - log_error "Python 3 未安装,请先安装 Python >= ${MIN_PYTHON_MAJOR}.${MIN_PYTHON_MINOR}" - log_info " Ubuntu/Debian: sudo apt install python3 python3-venv" - log_info " macOS: brew install python@3.12" - exit 1 - fi - - local major minor - major=$(python3 -c 'import sys; print(sys.version_info.major)') - minor=$(python3 -c 'import sys; print(sys.version_info.minor)') - - if [[ "$major" -lt "$MIN_PYTHON_MAJOR" ]] || \ - [[ "$major" -eq "$MIN_PYTHON_MAJOR" && "$minor" -lt "$MIN_PYTHON_MINOR" ]]; then - log_error "Python 版本过低: ${major}.${minor},需要 >= ${MIN_PYTHON_MAJOR}.${MIN_PYTHON_MINOR}" - exit 1 - fi - - local full_version - full_version=$(python3 --version 2>&1) - verbose_log "Found: $full_version" -} - -create_venv() { - local venv_path="$PROJECT_ROOT/$VENV_DIR" - - if [[ -d "$venv_path" ]]; then - verbose_log "虚拟环境已存在: $venv_path" - return 0 - fi - - log_info "创建 Python 虚拟环境: $venv_path" - python3 -m venv "$venv_path" - - if [[ ! -d "$venv_path" ]]; then - log_error "虚拟环境创建失败" - exit 1 - fi - - log_success "虚拟环境创建成功" -} - -activate_venv() { - local activate_script="$PROJECT_ROOT/$VENV_DIR/bin/activate" - - if [[ ! -f "$activate_script" ]]; then - log_error "虚拟环境激活脚本不存在: $activate_script" - log_info "请先运行: $0 install" - exit 1 - fi - - source "$activate_script" - verbose_log "虚拟环境已激活" -} - -install_deps() { - local marker="$PROJECT_ROOT/$VENV_DIR/$DEPS_MARKER" - local pyproject="$SCRIPT_DIR/pyproject.toml" - - # Check if deps are already installed and pyproject hasn't changed - if [[ -f "$marker" ]]; then - local marker_hash pyproject_hash - marker_hash=$(cat "$marker" 2>/dev/null) - pyproject_hash=$(md5sum "$pyproject" 2>/dev/null | cut -d' ' -f1) - - if [[ "$marker_hash" == "$pyproject_hash" ]]; then - verbose_log "依赖已安装且未变更,跳过安装" - return 0 - fi - fi - - log_info "安装文档开发依赖..." - pip install --upgrade pip --quiet - pip install -e "$SCRIPT_DIR" --quiet - - # Write marker with pyproject hash for change detection - md5sum "$pyproject" 2>/dev/null | cut -d' ' -f1 > "$marker" - log_success "依赖安装完成" -} - -ensure_venv() { - check_python - create_venv - activate_venv - install_deps -} - -# ============================================================================= -# Command implementations -# ============================================================================= - -cmd_serve() { - ensure_venv - - log_cyan "=== MkDocs 开发服务器 ===" - log_info "地址: http://${DEV_ADDR}:${DEV_PORT}" - log_info "文档: $PROJECT_ROOT/document" - echo "" - - cd "$PROJECT_ROOT" - mkdocs serve --dev-addr="${DEV_ADDR}:${DEV_PORT}" -} - -cmd_build() { - ensure_venv - - local output_path="$PROJECT_ROOT/$DOCS_OUTPUT_DIR" - - log_cyan "=== MkDocs 构建 ===" - log_info "输出: $output_path" - echo "" - - cd "$PROJECT_ROOT" - mkdocs build --clean -d "$DOCS_OUTPUT_DIR" - - log_success "构建完成: $output_path" -} - -cmd_api() { - # Check external tools - if ! command -v doxygen &>/dev/null; then - log_error "Doxygen 未安装" - log_info " Ubuntu/Debian: sudo apt install doxygen" - log_info " macOS: brew install doxygen" - exit 1 - fi - - if ! command -v doxybook2 &>/dev/null; then - log_error "Doxybook2 未安装" - log_info " 请从 https://github.com/matusnovak/doxybook2/releases 下载" - exit 1 - fi - - log_cyan "=== API 文档管线 ===" - echo "" - - cd "$PROJECT_ROOT" - - # Step 1: Run Doxygen - log_info "[1/3] 运行 Doxygen..." - doxygen Doxyfile - - # Step 2: Clean old API docs - log_info "[2/3] 清理旧 API 文档..." - rm -rf "$DOXYGEN_API_DIR" - mkdir -p "$DOXYGEN_API_DIR" - - # Step 3: Run Doxybook2 - log_info "[3/3] 运行 Doxybook2..." - doxybook2 --input "./$DOXYGEN_XML_DIR" \ - --output "./$DOXYGEN_API_DIR" \ - --config doxybook.json - - log_success "API 文档已生成到 $DOXYGEN_API_DIR" -} - -cmd_install() { - ensure_venv - log_success "环境准备就绪" -} - -cmd_clean() { - log_cyan "=== 清理构建产物 ===" - - local cleaned=0 - - # Clean MkDocs output - if [[ -d "$PROJECT_ROOT/$DOCS_OUTPUT_DIR" ]]; then - rm -rf "$PROJECT_ROOT/$DOCS_OUTPUT_DIR" - log_info "已清理: $DOCS_OUTPUT_DIR" - cleaned=$((cleaned + 1)) - fi - - # Clean Doxygen XML - if [[ -d "$PROJECT_ROOT/$DOXYGEN_XML_DIR" ]]; then - rm -rf "$PROJECT_ROOT/$DOXYGEN_XML_DIR" - log_info "已清理: $DOXYGEN_XML_DIR" - cleaned=$((cleaned + 1)) - fi - - # Clean __pycache__ - local pycache_count - pycache_count=$(find "$PROJECT_ROOT" -type d -name "__pycache__" 2>/dev/null | wc -l) - if [[ "$pycache_count" -gt 0 ]]; then - find "$PROJECT_ROOT" -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true - log_info "已清理: __pycache__ (${pycache_count} 个)" - cleaned=$((cleaned + 1)) - fi - - if [[ "$cleaned" -eq 0 ]]; then - log_info "没有需要清理的内容" - else - log_success "清理完成 (${cleaned} 项)" - fi -} - -cmd_reset() { - local venv_path="$PROJECT_ROOT/$VENV_DIR" - - log_cyan "=== 重置虚拟环境 ===" - - if [[ -d "$venv_path" ]]; then - log_info "删除: $venv_path" - rm -rf "$venv_path" - log_success "已删除旧环境" - fi - - # Recreate - ensure_venv - log_success "虚拟环境已重建" -} - -# ============================================================================= -# Help & Argument parsing -# ============================================================================= - -show_help() { - grep '^##' "$0" | sed 's/^## \?//g' | sed '/^$/d' - exit 0 -} - -parse_args() { - local command="" - - while [[ $# -gt 0 ]]; do - case "$1" in - serve|build|api|install|clean|reset|help) - command="$1" - shift - ;; - -p|--port) - DEV_PORT="$2" - shift 2 - ;; - -b|--bind) - DEV_ADDR="$2" - shift 2 - ;; - -v|--verbose) - VERBOSE=true - shift - ;; - -h|--help) - show_help - ;; - *) - log_error "未知参数: $1" - echo "" - show_help - ;; - esac - done - - # Default command - command="${command:-serve}" - - case "$command" in - serve) cmd_serve ;; - build) cmd_build ;; - api) cmd_api ;; - install) cmd_install ;; - clean) cmd_clean ;; - reset) cmd_reset ;; - help) show_help ;; - esac -} - -# ============================================================================= -# Entry point -# ============================================================================= - -parse_args "$@" diff --git a/scripts/document/pyproject.toml b/scripts/document/pyproject.toml deleted file mode 100644 index 5de6b7cea..000000000 --- a/scripts/document/pyproject.toml +++ /dev/null @@ -1,11 +0,0 @@ -[project] -name = "cfdesktop-docs" -version = "0.1.0" -description = "CFDesktop documentation development environment" -requires-python = ">=3.10" -dependencies = [ - "mkdocs>=1.5.0", - "mkdocs-material>=9.5.0", - "mkdocs-awesome-pages-plugin>=2.9.0", - "mkdocs-git-revision-date-localized-plugin>=1.2.0", -] diff --git a/scripts/release/hooks/install_hooks.sh b/scripts/release/hooks/install_hooks.sh index 672a414d1..b79cda9d7 100755 --- a/scripts/release/hooks/install_hooks.sh +++ b/scripts/release/hooks/install_hooks.sh @@ -154,7 +154,7 @@ log_success "========================================" echo "" echo "已安装的钩子:" echo " - pre-commit: 代码格式检查(所有分支)" -echo " - pre-push: Docker 构建验证(仅 main 和 release 分支)" +echo " - pre-push: 版本号检查(仅 main 和 release 分支)" echo "" echo "验证安装:" echo " ls -la .git/hooks/pre-commit .git/hooks/pre-push" diff --git a/scripts/release/hooks/pre-push.sample b/scripts/release/hooks/pre-push.sample index f1232fa8e..1cfe14dfc 100644 --- a/scripts/release/hooks/pre-push.sample +++ b/scripts/release/hooks/pre-push.sample @@ -1,25 +1,22 @@ #!/bin/bash # ============================================================================= -# Git Pre-Push Hook - 本地 Docker 构建验证 +# Git Pre-Push Hook - Version Check # ============================================================================= # -# 安装方法: bash scripts/release/hooks/install_hooks.sh +# Install: bash scripts/release/hooks/install_hooks.sh # -# 行为: -# - main 分支: X64 FastBuild + Tests -# - release/* 分支: 根据版本号自动检测验证级别 -# * Major: X64 + ARM64 完整构建 + 测试 -# * Minor: X64 完整构建 + 测试 -# * Patch: X64 FastBuild + 测试 -# - 其他分支: 跳过验证 +# Behavior: +# - main/release branches: verify CMakeLists.txt version has changed +# - other branches: skip # -# 绕过方法: git push --no-verify +# Build & test validation is handled by remote CI (GitHub Actions). +# Bypass: git push --no-verify # ============================================================================= set -e # ============================================================================= -# 颜色定义 +# Colors # ============================================================================= RED='\033[0;31m' GREEN='\033[0;32m' @@ -29,7 +26,7 @@ CYAN='\033[0;36m' NC='\033[0m' # ============================================================================= -# 日志函数 +# Logging # ============================================================================= log_info() { echo -e "${BLUE}>>>${NC} $1" @@ -39,44 +36,34 @@ log_success() { echo -e "${GREEN}✓${NC} $1" } -log_warning() { - echo -e "${YELLOW}⚠${NC} $1" -} - log_error() { echo -e "${RED}✗${NC} $1" } -log_step() { - echo -e "${CYAN}==>${NC} $1" -} - # ============================================================================= -# 获取项目根目录 +# Project root # ============================================================================= SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" cd "$PROJECT_ROOT" # ============================================================================= -# 加载版本工具函数 +# Load version utilities # ============================================================================= VERSION_UTILS="$PROJECT_ROOT/scripts/release/hooks/version_utils.sh" if [[ -f "$VERSION_UTILS" ]]; then source "$VERSION_UTILS" else - log_error "版本工具脚本不存在: $VERSION_UTILS" + log_error "Version utilities not found: $VERSION_UTILS" exit 1 fi # ============================================================================= -# 获取当前分支 +# Detect current branch # ============================================================================= CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "") -# 如果在 detached HEAD 状态,尝试从推送参数获取分支 if [[ -z "$CURRENT_BRANCH" ]]; then - # 从 stdin 读取推送信息 while read local_ref local_sha remote_ref remote_sha; do CURRENT_BRANCH=$(echo "$local_ref" | sed 's|refs/heads/||') break @@ -84,338 +71,82 @@ if [[ -z "$CURRENT_BRANCH" ]]; then fi # ============================================================================= -# 分支判断 +# Branch predicates # ============================================================================= -# 检查是否是 main 分支 is_main_branch() { [[ "$CURRENT_BRANCH" == "main" ]] } -# 检查是否是 release 分支 is_release_branch() { [[ "$CURRENT_BRANCH" == release/* ]] } -# 检查是否需要验证 should_verify() { is_main_branch || is_release_branch } # ============================================================================= -# 版本号检查 (仅 main 和 release 分支) +# Version check (main and release branches only) # ============================================================================= check_version_changed() { if ! should_verify; then return 0 fi - # 获取本地 CMakeLists.txt 中的版本号 LOCAL_CMAKE_VERSION=$(get_cmake_version "$PROJECT_ROOT") - - # 优先检查远程 CMakeLists.txt 版本号(适用于 main 分支无标签场景) REMOTE_CMAKE_VERSION=$(get_remote_cmake_version "origin/main") - # 如果远程没有版本号(首次推送),允许通过 if [[ -z "$REMOTE_CMAKE_VERSION" ]]; then - log_info "首次推送到远程,跳过版本号检查" + log_info "First push to remote, skipping version check" return 0 fi - # 比较本地和远程的 CMakeLists.txt 版本号 if [[ "$LOCAL_CMAKE_VERSION" == "$REMOTE_CMAKE_VERSION" ]]; then echo "" log_error "========================================" - log_error "版本号未变更,推送被阻止" + log_error "Version unchanged, push blocked" log_error "========================================" echo "" - echo -e "本地版本: ${YELLOW}$LOCAL_CMAKE_VERSION${NC}" - echo -e "远程版本: ${YELLOW}$REMOTE_CMAKE_VERSION${NC}" - echo "" - echo "推送到 main/release 分支需要先更新版本号。" + echo -e "Local: ${YELLOW}$LOCAL_CMAKE_VERSION${NC}" + echo -e "Remote: ${YELLOW}$REMOTE_CMAKE_VERSION${NC}" echo "" - echo "请按以下步骤更新版本号:" + echo "Pushing to main/release requires a version bump in CMakeLists.txt." echo "" - echo " 1. 更新 CMakeLists.txt 版本号:" + echo " 1. Update CMakeLists.txt:" echo -e " ${CYAN}project(CFDesktop VERSION X.Y.Z LANGUAGES CXX)${NC}" echo "" - echo " 2. 更新 README.md 中的版本徽章 (如果存在)" + echo "Version rules:" + echo " - Patch: 0.13.1 -> 0.13.2 (bug fixes)" + echo " - Minor: 0.13.1 -> 0.14.0 (new features)" + echo " - Major: 0.13.1 -> 1.0.0 (breaking changes)" echo "" - echo "版本号规则:" - echo " - Patch: 0.13.1 → 0.13.2 (bug 修复、小改动)" - echo " - Minor: 0.13.1 → 0.14.0 (新功能)" - echo " - Major: 0.13.1 → 1.0.0 (破坏性变更)" - echo "" - echo -e "${YELLOW}或使用 --no-verify 强制推送(不推荐)${NC}" + echo -e "${YELLOW}Bypass with --no-verify (not recommended)${NC}" echo "" return 1 fi - log_success "版本号检查通过: $LOCAL_CMAKE_VERSION (远程: $REMOTE_CMAKE_VERSION)" - echo "" + log_success "Version check passed: $LOCAL_CMAKE_VERSION (remote: $REMOTE_CMAKE_VERSION)" return 0 } -# 检查版本号是否变更 +# ============================================================================= +# Run version check +# ============================================================================= if ! check_version_changed; then exit 1 fi # ============================================================================= -# 如果不需要验证,直接退出 +# Non-protected branches: skip # ============================================================================= if ! should_verify; then echo "" - log_info "Pre-Push Hook: 分支 '$CURRENT_BRANCH' 跳过 Docker 验证" - echo "" - echo "Docker 验证仅在以下分支启用:" - echo " - main: X64 FastBuild + Tests" - echo " - release/*: 根据版本号自动检测验证级别" + log_info "Pre-Push: branch '$CURRENT_BRANCH' — version check skipped" echo "" exit 0 fi -# ============================================================================= -# 需要验证,开始检查 -# ============================================================================= -echo "" -log_info "========================================" -log_info "Pre-Push Hook: Docker 构建验证" -log_info "========================================" echo "" -log_info "当前分支: ${YELLOW}$CURRENT_BRANCH${NC}" +log_info "Pre-Push: branch '$CURRENT_BRANCH' — version check passed" +log_info "Build & test validation will run in remote CI" echo "" - -# ============================================================================= -# 检测 Docker 是否可用 -# ============================================================================= -if ! command -v docker >/dev/null 2>&1; then - log_error "Docker 未安装或未在 PATH 中" - echo "" - echo "请安装 Docker:" - echo " Windows/macOS: https://www.docker.com/products/docker-desktop" - echo " Linux: sudo apt install docker.io" - echo "" - echo -e "${YELLOW}或使用 --no-verify 跳过此检查: git push --no-verify${NC}" - exit 1 -fi - -# 检测 Docker daemon 是否运行 -if ! docker info &> /dev/null; then - log_error "Docker daemon 未运行" - echo "" - echo "请启动 Docker Desktop 或 Docker 服务" - echo "" - echo -e "${YELLOW}或使用 --no-verify 跳过此检查: git push --no-verify${NC}" - exit 1 -fi -log_success "Docker 检测通过" -echo "" - -# ============================================================================= -# 确定验证参数 -# ============================================================================= -DOCKER_START_SCRIPT="$PROJECT_ROOT/scripts/build_helpers/docker_start.sh" - -# 检查脚本是否存在 -if [[ ! -f "$DOCKER_START_SCRIPT" ]]; then - log_error "Docker 启动脚本不存在: $DOCKER_START_SCRIPT" - exit 1 -fi - -# 构建参数数组 -declare -a DOCKER_ARGS - -if is_main_branch; then - # ============================================================================= - # main 分支: X64 FastBuild + Tests - # ============================================================================= - log_step "检测到 main 分支" - log_info "验证级别: X64 FastBuild + Tests" - echo "" - - # 先执行增量构建 - log_step "开始增量构建..." - DOCKER_ARGS=( - "--build-project-fast" - "--arch" - "amd64" - ) - - if ! bash "$DOCKER_START_SCRIPT" "${DOCKER_ARGS[@]}"; then - log_error "增量构建验证失败" - exit 1 - fi - - log_success "增量构建验证通过" - echo "" - - # 再执行测试 - log_step "开始运行测试..." - DOCKER_ARGS=( - "--run-project-test" - "--arch" - "amd64" - ) - - if ! bash "$DOCKER_START_SCRIPT" "${DOCKER_ARGS[@]}"; then - log_error "测试验证失败" - exit 1 - fi - - log_success "测试验证通过" - echo "" - log_success "========================================" - log_success "所有验证通过,可以推送" - log_success "========================================" - exit 0 - -elif is_release_branch; then - # ============================================================================= - # release 分支: 根据版本号自动检测验证级别 - # ============================================================================= - log_step "检测到 release 分支" - echo "" - - # 获取版本号 - LOCAL_VERSION=$(get_local_version) - REMOTE_VERSION=$(get_remote_version) - - # 确定验证级别 - VERIFY_LEVEL=$(determine_verify_level "$LOCAL_VERSION" "$REMOTE_VERSION") - - # 打印版本信息 - print_version_info "$LOCAL_VERSION" "$REMOTE_VERSION" "$VERIFY_LEVEL" - echo "" - - case "$VERIFY_LEVEL" in - major) - # Major: X64 + ARM64 完整构建 - log_warning "Major 版本变更,将执行 X64 + ARM64 完整构建" - log_warning "这可能需要较长时间(ARM64 使用 QEMU 仿真)" - echo "" - - # 先构建 X64 - log_step "开始 X64 验证..." - DOCKER_ARGS=( - "--verify" - "--arch" - "amd64" - ) - - if ! bash "$DOCKER_START_SCRIPT" "${DOCKER_ARGS[@]}"; then - log_error "X64 构建验证失败" - exit 1 - fi - - log_success "X64 验证通过" - echo "" - - # 再构建 ARM64 - log_step "开始 ARM64 验证..." - DOCKER_ARGS=( - "--verify" - "--arch" - "arm64" - ) - - if ! bash "$DOCKER_START_SCRIPT" "${DOCKER_ARGS[@]}"; then - log_error "ARM64 构建验证失败" - exit 1 - fi - - log_success "ARM64 验证通过" - echo "" - log_success "========================================" - log_success "所有验证通过,可以推送" - log_success "========================================" - exit 0 - ;; - - minor) - # Minor: X64 完整构建 - log_info "验证级别: X64 完整构建" - echo "" - - DOCKER_ARGS=( - "--verify" - "--arch" - "amd64" - ) - ;; - - patch) - # Patch: X64 FastBuild + Tests - log_info "验证级别: X64 FastBuild + Tests" - echo "" - - # 先执行增量构建 - log_step "开始增量构建..." - DOCKER_ARGS=( - "--build-project-fast" - "--arch" - "amd64" - ) - - if ! bash "$DOCKER_START_SCRIPT" "${DOCKER_ARGS[@]}"; then - log_error "增量构建验证失败" - exit 1 - fi - - log_success "增量构建验证通过" - echo "" - - # 再执行测试 - log_step "开始运行测试..." - DOCKER_ARGS=( - "--run-project-test" - "--arch" - "amd64" - ) - - if ! bash "$DOCKER_START_SCRIPT" "${DOCKER_ARGS[@]}"; then - log_error "测试验证失败" - exit 1 - fi - - log_success "测试验证通过" - echo "" - log_success "========================================" - log_success "所有验证通过,可以推送" - log_success "========================================" - exit 0 - ;; - - *) - log_error "未知的验证级别: $VERIFY_LEVEL" - exit 1 - ;; - esac -fi - -# ============================================================================= -# 执行验证 -# ============================================================================= -log_step "开始 Docker 验证..." -echo "" - -if bash "$DOCKER_START_SCRIPT" "${DOCKER_ARGS[@]}"; then - echo "" - log_success "========================================" - log_success "验证通过,可以推送" - log_success "========================================" - echo "" - exit 0 -else - exit_code=$? - echo "" - log_error "========================================" - log_error "验证失败,推送被阻止" - log_error "========================================" - echo "" - echo "回退方法:" - echo " git reset --hard origin/$CURRENT_BRANCH" - echo "" - echo -e "${YELLOW}或使用 --no-verify 强制推送(不推荐)${NC}" - echo "" - exit $exit_code -fi diff --git a/site/.vitepress/config.mts b/site/.vitepress/config.mts new file mode 100644 index 000000000..74e2d70d4 --- /dev/null +++ b/site/.vitepress/config.mts @@ -0,0 +1,153 @@ +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { defineConfig, type DefaultTheme } from "vitepress"; +import projectConfig from "../../project.config"; + +type SidebarItem = DefaultTheme.SidebarItem; + +const docsRoot = fileURLToPath(new URL(`../../${projectConfig.documentsDir}`, import.meta.url)); +const githubUrl = `https://github.com/${projectConfig.github.owner}/${projectConfig.github.repo}`; +const editPattern = `${githubUrl}/edit/${projectConfig.github.branch}/${projectConfig.github.documentsPath}/:path`; + +function extractTitle(filePath: string): string | null { + try { + const content = readFileSync(filePath, "utf-8"); + const frontmatterTitle = content.match(/^---[\s\S]*?^title:\s*["']?(.+?)["']?\s*$/m); + if (frontmatterTitle) return frontmatterTitle[1]; + const h1 = content.match(/^#\s+(.+)$/m); + if (h1) return h1[1].replace(/\{.*?\}/g, "").trim(); + } catch { + return null; + } + return null; +} + +function humanize(name: string): string { + return name + .replace(/^\d+[-_]?/, "") + .replace(/[-_]/g, " ") + .replace(/\b\w/g, (c) => c.toUpperCase()); +} + +function sortEntries(a: string, b: string): number { + const aIndex = a.match(/^(\d+)/)?.[1]; + const bIndex = b.match(/^(\d+)/)?.[1]; + if (aIndex && bIndex) return Number(aIndex) - Number(bIndex); + if (aIndex) return -1; + if (bIndex) return 1; + if (a === "index.md") return -1; + if (b === "index.md") return 1; + return a.localeCompare(b, "en"); +} + +function shouldSkip(name: string): boolean { + return ( + name.startsWith(".") || + name === "api" || + name === "Awesome-Embedded.png" || + name === "Awesome-Embedded.ico" + ); +} + +function scanDir(dir: string, urlPrefix: string, depth = 0): SidebarItem[] { + if (depth > 5) return []; + + let entries: string[]; + try { + entries = readdirSync(dir).filter((entry) => !shouldSkip(entry)); + } catch { + return []; + } + + entries.sort(sortEntries); + const items: SidebarItem[] = []; + + for (const name of entries) { + const fullPath = join(dir, name); + const stat = statSync(fullPath); + + if (stat.isDirectory()) { + const indexPath = join(fullPath, "index.md"); + const children = scanDir(fullPath, `${urlPrefix}/${name}`, depth + 1); + const title = extractTitle(indexPath) || humanize(name); + + if (children.length > 0) { + items.push({ + text: title, + link: existsSync(indexPath) ? `${urlPrefix}/${name}/` : undefined, + collapsed: depth > 0, + items: children + }); + } else if (existsSync(indexPath)) { + items.push({ text: title, link: `${urlPrefix}/${name}/` }); + } + continue; + } + + if (name.endsWith(".md") && name !== "index.md" && name !== "README.md") { + const title = extractTitle(fullPath) || humanize(name.replace(/\.md$/, "")); + items.push({ text: title, link: `${urlPrefix}/${name.replace(/\.md$/, "")}` }); + } + } + + return items; +} + +function volumeSidebar(srcDir: string, urlPrefix: string): SidebarItem[] { + const dir = join(docsRoot, srcDir); + const indexPath = join(dir, "index.md"); + const title = extractTitle(indexPath) || humanize(srcDir); + return [{ text: title, link: `${urlPrefix}/` }, ...scanDir(dir, urlPrefix)]; +} + +function buildSidebar(): DefaultTheme.Sidebar { + const sidebar: DefaultTheme.Sidebar = {}; + for (const volume of projectConfig.sidebar.volumes) { + sidebar[`${volume.urlPrefix}/`] = volumeSidebar(volume.srcDir, volume.urlPrefix); + } + return sidebar; +} + +export default defineConfig({ + srcDir: `../${projectConfig.documentsDir}`, + title: projectConfig.title, + description: projectConfig.description, + lang: "zh-CN", + base: projectConfig.base, + cleanUrls: true, + lastUpdated: true, + ignoreDeadLinks: true, + srcExclude: ["api/**"], + head: [["link", { rel: "icon", href: `${projectConfig.base}Awesome-Embedded.ico` }]], + markdown: { + html: false, + lineNumbers: true, + theme: { + light: "github-light", + dark: "github-dark" + } + }, + themeConfig: { + nav: projectConfig.nav, + sidebar: buildSidebar(), + search: { + provider: "local" + }, + editLink: { + pattern: editPattern, + text: "在 GitHub 上编辑此页" + }, + footer: { + message: "Built with VitePress", + copyright: "Copyright © 2026 CharlieChen" + }, + socialLinks: [{ icon: "github", link: githubUrl }] + }, + vite: { + publicDir: fileURLToPath(new URL("./public", import.meta.url)), + build: { + chunkSizeWarningLimit: 5000 + } + } +}); diff --git a/site/.vitepress/public/Awesome-Embedded.ico b/site/.vitepress/public/Awesome-Embedded.ico new file mode 100644 index 000000000..36b7cd8ad Binary files /dev/null and b/site/.vitepress/public/Awesome-Embedded.ico differ diff --git a/site/.vitepress/public/Awesome-Embedded.png b/site/.vitepress/public/Awesome-Embedded.png new file mode 100644 index 000000000..8fb053d60 Binary files /dev/null and b/site/.vitepress/public/Awesome-Embedded.png differ diff --git a/test/config_manager/mock_path_provider.h b/test/config_manager/mock_path_provider.h index 846d5477c..26df52517 100644 --- a/test/config_manager/mock_path_provider.h +++ b/test/config_manager/mock_path_provider.h @@ -66,6 +66,30 @@ class MockPathProvider : public IConfigStorePathProvider { return true; } + QString domain_path(int layer_index, const QString& domain_name) const override { + if (domain_name == "default") { + switch (layer_index) { + case 0: + return system_path(); + case 1: + return user_dir() + "/" + user_filename(); + case 2: + return app_dir() + "/" + app_filename(); + } + return QString(); + } + QString filename = domain_name + "_test.ini"; + switch (layer_index) { + case 0: + return temp_dir_ + "/" + filename; + case 1: + return temp_dir_ + "/user/" + filename; + case 2: + return temp_dir_ + "/app/" + filename; + } + return QString(); + } + /** * @brief Get the temp directory for cleanup. */ @@ -107,6 +131,8 @@ class LayerControlMockProvider : public MockPathProvider { return !(disabled_layers_ & (1 << layer_index)); } + using MockPathProvider::domain_path; + private: int disabled_layers_; }; @@ -140,6 +166,30 @@ class JsonMockPathProvider : public IConfigStorePathProvider { return true; } + QString domain_path(int layer_index, const QString& domain_name) const override { + if (domain_name == "default") { + switch (layer_index) { + case 0: + return system_path(); + case 1: + return user_dir() + "/" + user_filename(); + case 2: + return app_dir() + "/" + app_filename(); + } + return QString(); + } + QString filename = domain_name + "_test.json"; + switch (layer_index) { + case 0: + return temp_dir_ + "/" + filename; + case 1: + return temp_dir_ + "/user/" + filename; + case 2: + return temp_dir_ + "/app/" + filename; + } + return QString(); + } + const QString& temp_dir() const { return temp_dir_; } private: