diff --git a/desktop/ui/components/launcher/app_launcher.cpp b/desktop/ui/components/launcher/app_launcher.cpp index 2b6404b6c..1c3f5c415 100644 --- a/desktop/ui/components/launcher/app_launcher.cpp +++ b/desktop/ui/components/launcher/app_launcher.cpp @@ -5,12 +5,14 @@ * Renders a frameless rounded Material surface holding a grid of LauncherTile * entries. popup() sizes and centers it above the taskbar; tile clicks forward * appLaunched() and dismiss the popup; ESC and outside clicks (Qt::Popup) also - * dismiss it. All rendering is QPainter-native. + * dismiss it. The popup fades and slides in on open and reverses on close, + * driven by the QuarkWidgets MD3 engine: motion-token-bound fade + slide + * animations whose duration and easing resolve from the active theme. * * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) * @date 2026-06-26 * @version 0.1 - * @since 0.20 + * @since 0.20 * @ingroup components */ @@ -18,8 +20,11 @@ #include "launcher_tile.h" +#include "components/material/cfmaterial_fade_animation.h" +#include "components/material/cfmaterial_slide_animation.h" #include "core/theme_manager.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" +#include "ui/widget/material/widget/textfield/textfield.h" #include #include @@ -48,6 +53,7 @@ constexpr int kMinWidth = 480; ///< Minimum popup width (px). constexpr int kMaxWidth = 720; ///< Maximum popup width (px). constexpr int kMinHeight = 360; ///< Minimum popup height (px). constexpr int kMaxHeight = 540; ///< Maximum popup height (px). +constexpr int kEnterSlidePx = 24; ///< Enter/exit vertical slide distance (px). } // namespace AppLauncher::AppLauncher(QWidget* parent) : QWidget(parent) { @@ -64,6 +70,11 @@ AppLauncher::AppLauncher(QWidget* parent) : QWidget(parent) { setAutoFillBackground(false); setupUi(); applyTheme(); + // setupAnimations creates the MD3 fade + slide animations; the fade pair + // installs the QGraphicsOpacityEffect on this widget via setTargetWidget(), + // so no manual effect ownership is needed here. + setupAnimations(); + // Stay hidden until popup() is called. As a child of the desktop (not a // top-level Qt::Popup), Qt would otherwise auto-show this widget when the // desktop becomes visible -- rendering the tiles at the default (0,0) @@ -101,12 +112,28 @@ void AppLauncher::popup(const QRect& available) { if (grid_ != nullptr) { grid_->activate(); } + + rest_pos_ = pos(); + // Start the enter pair BEFORE show() so the initial frame (transparent + + // offset downward) is applied to a still-hidden widget -- the popup never + // flashes fully opaque for one frame. The slide captures pos() (== rest_pos_) + // as its anchor; the fade seeds opacity 0 via its opacity effect. + enter_slide_->start(); + enter_fade_->start(); show(); raise(); } void AppLauncher::hideLauncher() { - hide(); + if (!isVisible()) { + hide(); + return; + } + // Start the exit pair. In the normal flow the enter pair already finished + // (popup was fully open), so there is no overlap on the shared opacity + // effect. exit_fade_::finished hides the widget once the fade-out completes. + exit_slide_->start(); + exit_fade_->start(); } bool AppLauncher::isShowing() const noexcept { @@ -138,7 +165,8 @@ void AppLauncher::setupUi() { outer->setContentsMargins(kMargin, kMargin, kMargin, kMargin); outer->setSpacing(kGridSpacing); - search_edit_ = new QLineEdit(this); + using qw::widget::material::TextField; + search_edit_ = new TextField(TextField::TextFieldVariant::Outlined, this); search_edit_->setPlaceholderText(QStringLiteral("Search apps...")); outer->addWidget(search_edit_); @@ -149,7 +177,9 @@ void AppLauncher::setupUi() { outer->addWidget(grid_container); // Live filter: any change to the search box rebuilds the grid with only - // tiles whose display_name contains the (case-insensitive) query. + // tiles whose display_name contains the (case-insensitive) query. TextField + // inherits QLineEdit::textChanged (its own textChanged is a protected + // helper, not a signal), so connect against the base signal explicitly. connect(search_edit_, &QLineEdit::textChanged, this, [this](const QString&) { rebuildGrid(); }); } @@ -195,4 +225,47 @@ void AppLauncher::rebuildGrid() { } } +void AppLauncher::setupAnimations() { + // Resolve the theme's motion spec once; each animation queries it for MD3 + // duration + easing via its bound motion token at start(). + qw::core::IMotionSpec* spec = nullptr; + try { + auto& tm = qw::core::ThemeManager::instance(); + spec = &tm.theme(tm.currentThemeName()).motion_spec(); + } catch (...) { + // No theme registered yet; animations fall back to default timing. + } + + using qw::components::material::CFMaterialFadeAnimation; + using qw::components::material::CFMaterialSlideAnimation; + using qw::components::material::SlideDirection; + + // Enter: fade in (0->1, shortEnter) + slide up (-px->0, mediumEnter). + // enter_fade_ installs the shared QGraphicsOpacityEffect; exit_fade_ reuses + // it (the fade animation detects the existing effect on the widget). + enter_fade_ = new CFMaterialFadeAnimation(spec, this); + enter_fade_->setRange(0.0f, 1.0f); + enter_fade_->setMotionToken("shortEnter"); + enter_fade_->setTargetWidget(this); + + enter_slide_ = new CFMaterialSlideAnimation(spec, SlideDirection::Up, this); + enter_slide_->setRange(static_cast(-kEnterSlidePx), 0.0f); + enter_slide_->setMotionToken("mediumEnter"); + enter_slide_->setTargetWidget(this); + + // Exit: fade out (1->0, shortExit) + slide down (0->+px, mediumExit). + // exit_fade_::finished hides the popup once the fade-out completes. + exit_fade_ = new CFMaterialFadeAnimation(spec, this); + exit_fade_->setRange(1.0f, 0.0f); + exit_fade_->setMotionToken("shortExit"); + exit_fade_->setTargetWidget(this); + connect(exit_fade_, &qw::components::ICFAbstractAnimation::finished, this, + [this]() { hide(); }); + + exit_slide_ = new CFMaterialSlideAnimation(spec, SlideDirection::Down, this); + exit_slide_->setRange(0.0f, static_cast(kEnterSlidePx)); + exit_slide_->setMotionToken("mediumExit"); + exit_slide_->setTargetWidget(this); +} + } // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/launcher/app_launcher.h b/desktop/ui/components/launcher/app_launcher.h index dc0b0dd85..a7b8bcdd7 100644 --- a/desktop/ui/components/launcher/app_launcher.h +++ b/desktop/ui/components/launcher/app_launcher.h @@ -28,10 +28,17 @@ class QGridLayout; class QKeyEvent; -class QLineEdit; class QPaintEvent; class QRect; +namespace qw::components::material { +class CFMaterialFadeAnimation; +class CFMaterialSlideAnimation; +} // namespace qw::components::material +namespace qw::widget::material { +class TextField; +} + namespace cf::desktop::desktop_component { class LauncherTile; @@ -175,17 +182,31 @@ class AppLauncher final : public QWidget { void keyPressEvent(QKeyEvent* event) override; private: - /// @brief Creates the grid layout. + /// @brief Creates the grid + search box layout. void setupUi(); /// @brief Resolves theme colors, then repaints. void applyTheme(); /// @brief Rebuilds the tile grid from apps_. void rebuildGrid(); - - QLineEdit* search_edit_{nullptr}; ///< Filter box (ownership: this widget). - QGridLayout* grid_{nullptr}; ///< Tile grid (ownership: grid container). - QList tiles_; ///< Current tiles. Ownership: Qt parented. - QList apps_; ///< Backing application list. + /// @brief Creates the four MD3 enter/exit animations (fade + slide each), + /// bound to motion tokens so duration + easing resolve from the theme. + void setupAnimations(); + + qw::widget::material::TextField* search_edit_{ + nullptr}; ///< Filter box (ownership: this widget). + QGridLayout* grid_{nullptr}; ///< Tile grid (ownership: grid container). + QList tiles_; ///< Current tiles. Ownership: Qt parented. + QList apps_; ///< Backing application list. + + qw::components::material::CFMaterialFadeAnimation* enter_fade_{ + nullptr}; ///< Enter fade in (0->1, shortEnter). + qw::components::material::CFMaterialSlideAnimation* enter_slide_{ + nullptr}; ///< Enter slide up (-px->0, mediumEnter). + qw::components::material::CFMaterialFadeAnimation* exit_fade_{ + nullptr}; ///< Exit fade out (1->0, shortExit); finished -> hide. + qw::components::material::CFMaterialSlideAnimation* exit_slide_{ + nullptr}; ///< Exit slide down (0->+px, mediumExit). + QPoint rest_pos_; ///< Final anchored position (set in popup()). QColor surface_color_; ///< Popup background fill (surface). QColor outline_color_; ///< Reserved for future border (outline variant). diff --git a/document/todo/desktop/milestone_04_app_launcher.md b/document/todo/desktop/milestone_04_app_launcher.md index 45180800d..d79cc3c25 100644 --- a/document/todo/desktop/milestone_04_app_launcher.md +++ b/document/todo/desktop/milestone_04_app_launcher.md @@ -5,7 +5,7 @@ description: "预计周期: 5-7 天,前置依赖: Milestone 3: 任务栏" # Milestone 4: 应用启动器 -> **状态**: 🚧 闭环跑通·网格待做(`app_launcher` / `launcher_tile` / `app_launch_service` 已落地,点 Files/Browser 可真打开外部进程;开始菜单弹窗网格 + 搜索框待做) +> **状态**: ✅ 闭环跑通(网格 + 搜索 + .desktop + 双态 + 入场/退场动画[QuarkWidgets MD3 引擎] + MD3 TextField)。仅余:引擎 Backward/stop 语义待修(见 §七) > **预计周期**: 5-7 天 > **前置依赖**: [Milestone 3: 任务栏](milestone_03_taskbar.md) > **目标**: 点击任务栏"开始"按钮或桌面区域,弹出一个应用网格,可启动外部程序 @@ -271,10 +271,14 @@ desktop 编译期引用 `apps/calculator/calculator_panel.cpp` 源文件 + `cfde - **Noter 移植**([apps/noter/](../../../apps/noter/)):CCIMXNoter → CFDesktop 第二个独立 App,QuarkWidgets MD3 Button 重写工具栏(Open/Save/Bold/Italic + 字号 QSlider),QTextEdit 编辑,manifest `launch_kind:auto` - 单测:DesktopEntryIndex 7 例(解析/NoDisplay/非 Application/Exec 清理/basename 回退),全过 -### 待做 +### 已落地(动画) -- 入场/退场动画(Day 5 原计划:Slide+Fade 组合,复用 `ui/components/animation/`) -- 搜索框换 QuarkWidgets MD3 TextField(现用 QLineEdit 占位) +- 入场/退场动画改用 QuarkWidgets MD3 引擎(`CFMaterialFadeAnimation`/`CFMaterialSlideAnimation`),motion token 绑定(`shortEnter`/`mediumEnter` 进,`shortExit`/`mediumExit` 出),duration + easing 从 theme 的 `IMotionSpec` 解析。4 个 Forward-only 动画(enter/exit × fade/slide),见 [app_launcher.cpp](../../../desktop/ui/components/launcher/app_launcher.cpp) `setupAnimations`。 + +### 引擎遗留(后续) + +- 引擎 `tick()` 不认 `Backward` 方向 + `stop()` reset 回 `m_from` → launcher 用 Forward-only + 反转 range 绕过。「开着立刻关」(~250ms 窗)可能轻微抖动;修引擎 `tick` 方向 + `stop` 语义可彻底干净。 +- 引擎修复 5 文件在 QuarkWidgets 子模块(分支 `feat/finish-md3-animation-engine`,待 push):`calculateEasedProgress` 接 MotionSpec、factory 死代码 → `setMotionToken`、mapping 双前缀、`timing_animation.h` 错误的 `qw::components::core` forward decl。 --- diff --git a/third_party/QuarkWidgets b/third_party/QuarkWidgets index 73a3d1d8a..77b23a8c3 160000 --- a/third_party/QuarkWidgets +++ b/third_party/QuarkWidgets @@ -1 +1 @@ -Subproject commit 73a3d1d8a6f65ae76a08009b524280b563888337 +Subproject commit 77b23a8c3c9d33e12bc57ddb592dd94fee99d575