From 249c505dc5ad7760cbd7508f97e6905d62dd877b Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Wed, 1 Jul 2026 14:55:46 +0800 Subject: [PATCH 1/3] feat(launcher): add enter/exit animation + MD3 TextField search box MS4 closeout (interim animation approach): - AppLauncher popup now fades (QGraphicsOpacityEffect) and slides up (QPropertyAnimation on pos) on open via a shared enter animation (250ms OutCubic), and reverses on close (150ms InCubic -> hide). Re-entry cancels any in-flight exit and vice-versa; the enter initial frame is seeded before show() so the popup never flashes opaque. - Search box: QLineEdit -> QuarkWidgets TextField (Outlined variant). Drop-in: TextField extends QLineEdit, so the live-filter connect is unchanged (connected against the base &QLineEdit::textChanged explicitly, since TextField hides that name behind a protected helper). The animation uses QPropertyAnimation rather than the QuarkWidgets MD3 engine (CFMaterialFade/SlideAnimation): the engine's MotionSpec integration is unfinished today (calculateEasedProgress is linear, durations hardcoded), so the eased QPropertyAnimation version is higher quality. A TODO in setupAnimations marks the eventual switch once the engine is completed. milestone_04 doc: header status + the section-7 TODO updated to reflect this interim and the engine-switch prerequisite. Verified: build green; app_discoverer_test 9/9, desktop_entry_index_test 7/7, builtin_panel_registry_test 5/5 pass; setupAnimations/applyAnimProgress + TextField symbols present in libCFDesktop_shared.so. --- .../ui/components/launcher/app_launcher.cpp | 103 +++++++++++++++--- desktop/ui/components/launcher/app_launcher.h | 31 ++++-- .../todo/desktop/milestone_04_app_launcher.md | 8 +- 3 files changed, 117 insertions(+), 25 deletions(-) diff --git a/desktop/ui/components/launcher/app_launcher.cpp b/desktop/ui/components/launcher/app_launcher.cpp index 2b6404b6c..41479a2ad 100644 --- a/desktop/ui/components/launcher/app_launcher.cpp +++ b/desktop/ui/components/launcher/app_launcher.cpp @@ -5,12 +5,13 @@ * 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. + * All rendering is QPainter-native. * * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) * @date 2026-06-26 * @version 0.1 - * @since 0.20 + * @since 0.20 * @ingroup components */ @@ -20,7 +21,10 @@ #include "core/theme_manager.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" +#include "ui/widget/material/widget/textfield/textfield.h" +#include +#include #include #include #include @@ -30,6 +34,7 @@ #include #include #include +#include #include @@ -38,16 +43,19 @@ namespace cf::desktop::desktop_component { using namespace qw::core::token::literals; namespace { -constexpr int kMaxColumns = 5; ///< Maximum tiles per grid row. -constexpr int kGridSpacing = 12; ///< Gap between tiles (px). -constexpr int kMargin = 24; ///< Popup inner margin (px). -constexpr qreal kCornerRadius = 16; ///< Popup corner radius (px). -constexpr qreal kWidthRatio = 0.5; ///< Popup width as a fraction of available. -constexpr qreal kHeightRatio = 0.55; ///< Popup height as a fraction of available. -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 kMaxColumns = 5; ///< Maximum tiles per grid row. +constexpr int kGridSpacing = 12; ///< Gap between tiles (px). +constexpr int kMargin = 24; ///< Popup inner margin (px). +constexpr qreal kCornerRadius = 16; ///< Popup corner radius (px). +constexpr qreal kWidthRatio = 0.5; ///< Popup width as a fraction of available. +constexpr qreal kHeightRatio = 0.55; ///< Popup height as a fraction of available. +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 kEnterDurationMs = 250; ///< Enter animation duration (ms). +constexpr int kExitDurationMs = 150; ///< Exit animation duration (ms). +constexpr int kEnterSlidePx = 24; ///< Enter/exit vertical slide distance (px). } // namespace AppLauncher::AppLauncher(QWidget* parent) : QWidget(parent) { @@ -64,6 +72,13 @@ AppLauncher::AppLauncher(QWidget* parent) : QWidget(parent) { setAutoFillBackground(false); setupUi(); applyTheme(); + + // Whole-popup fade effect; drives the enter/exit fade alongside the slide. + opacity_effect_ = new QGraphicsOpacityEffect(this); + setGraphicsEffect(opacity_effect_); + opacity_effect_->setOpacity(1.0); + 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 +116,31 @@ void AppLauncher::popup(const QRect& available) { if (grid_ != nullptr) { grid_->activate(); } + + rest_pos_ = pos(); + // Seed the enter animation's initial frame (offset down + transparent) + // before show() so the popup never flashes fully opaque for one frame. + applyAnimProgress(0.0); show(); raise(); + // Cancel any in-flight exit before entering (e.g. re-open while closing). + exit_anim_->stop(); + enter_anim_->stop(); + enter_anim_->start(); } void AppLauncher::hideLauncher() { - hide(); + if (exit_anim_ == nullptr || !isVisible()) { + hide(); + return; + } + // Animate out from the current opacity so a half-entered popup also exits + // smoothly rather than snapping. + enter_anim_->stop(); + exit_anim_->stop(); + exit_anim_->setStartValue(opacity_effect_ != nullptr ? opacity_effect_->opacity() : qreal(1.0)); + exit_anim_->setEndValue(qreal(0.0)); + exit_anim_->start(); } bool AppLauncher::isShowing() const noexcept { @@ -138,7 +172,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 +184,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 +232,40 @@ void AppLauncher::rebuildGrid() { } } +void AppLauncher::setupAnimations() { + // TODO: eventually switch to the QuarkWidgets MD3 engine + // (CFMaterialFadeAnimation + CFMaterialSlideAnimation). Deferred because the + // engine's MotionSpec integration is unfinished today (linear easing via + // calculateEasedProgress, hardcoded durations) -- this OutCubic/InCubic + // variant is higher quality until the engine is completed. + enter_anim_ = new QVariantAnimation(this); + enter_anim_->setDuration(kEnterDurationMs); + enter_anim_->setEasingCurve(QEasingCurve::OutCubic); + enter_anim_->setStartValue(qreal(0.0)); + enter_anim_->setEndValue(qreal(1.0)); + connect(enter_anim_, &QVariantAnimation::valueChanged, this, + [this](const QVariant& v) { applyAnimProgress(v.toReal()); }); + + exit_anim_ = new QVariantAnimation(this); + exit_anim_->setDuration(kExitDurationMs); + exit_anim_->setEasingCurve(QEasingCurve::InCubic); + connect(exit_anim_, &QVariantAnimation::valueChanged, this, + [this](const QVariant& v) { applyAnimProgress(v.toReal()); }); + // On exit completion: hide and reset to the resting state so the next + // popup() opens from a clean slate. + connect(exit_anim_, &QVariantAnimation::finished, this, [this]() { + hide(); + applyAnimProgress(1.0); + }); +} + +void AppLauncher::applyAnimProgress(qreal t) { + if (opacity_effect_ != nullptr) { + opacity_effect_->setOpacity(t); + } + // Slide vertically: t=0 -> offset down by kEnterSlidePx; t=1 -> at rest_pos_. + const int dy = static_cast((1.0 - t) * kEnterSlidePx); + move(rest_pos_ + QPoint(0, dy)); +} + } // 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..73fd75621 100644 --- a/desktop/ui/components/launcher/app_launcher.h +++ b/desktop/ui/components/launcher/app_launcher.h @@ -26,11 +26,16 @@ #include #include +class QGraphicsOpacityEffect; class QGridLayout; class QKeyEvent; -class QLineEdit; class QPaintEvent; class QRect; +class QVariantAnimation; + +namespace qw::widget::material { +class TextField; +} namespace cf::desktop::desktop_component { @@ -175,17 +180,29 @@ 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 opacity effect and the enter/exit animations. + void setupAnimations(); + /// @brief Applies an animation progress value to the popup: t=0 is fully + /// hidden and offset downward, t=1 is at rest (opaque, anchored). + /// @param[in] t Progress in [0.0, 1.0]. + void applyAnimProgress(qreal t); + + 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. + + QGraphicsOpacityEffect* opacity_effect_{nullptr}; ///< Whole-popup fade (owned by this). + QVariantAnimation* enter_anim_{nullptr}; ///< Enter: 0.0 -> 1.0 (fade in + slide up). + QVariantAnimation* exit_anim_{nullptr}; ///< Exit: 1.0 -> 0.0 (fade out + slide down). + 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..9bc11a714 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 + 双态 + 入场/退场动画 + MD3 TextField 已落地)。仅余:动画切到 QuarkWidgets MD3 引擎(待引擎 MotionSpec 接入完成,见 §七 待做) > **预计周期**: 5-7 天 > **前置依赖**: [Milestone 3: 任务栏](milestone_03_taskbar.md) > **目标**: 点击任务栏"开始"按钮或桌面区域,弹出一个应用网格,可启动外部程序 @@ -273,8 +273,10 @@ desktop 编译期引用 `apps/calculator/calculator_panel.cpp` 源文件 + `cfde ### 待做 -- 入场/退场动画(Day 5 原计划:Slide+Fade 组合,复用 `ui/components/animation/`) -- 搜索框换 QuarkWidgets MD3 TextField(现用 QLineEdit 占位) +- 入场/退场动画切到 QuarkWidgets MD3 引擎(`CFMaterialFadeAnimation`/`CFMaterialSlideAnimation`)。 + 当前 interim 用 `QPropertyAnimation`(OutCubic/InCubic + `QGraphicsOpacityEffect`,见 [app_launcher.cpp](../../../desktop/ui/components/launcher/app_launcher.cpp) `setupAnimations`)。 + 引擎自身的 `calculateEasedProgress` 目前是**线性** + 时长**硬编码**(MotionSpec 未接,`.cpp` 里 `// TODO: Integrate with MaterialMotionScheme`), + 需先补完 QuarkWidgets 引擎再切,否则动画质量降级。 --- From 60c1d4b8072ced549d45b2d340d2a096f3b5fbce Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Wed, 1 Jul 2026 15:35:35 +0800 Subject: [PATCH 2/3] refactor(launcher): drive popup animation via the QuarkWidgets MD3 engine Replace the interim QPropertyAnimation fade/slide with the now-finished QuarkWidgets MD3 engine: four motion-token-bound animations (enter/exit x fade/slide) whose duration + easing resolve from the active theme's IMotionSpec at start() -- shortEnter/mediumEnter for enter, shortExit/ mediumExit for exit. The enter pair starts before show() so the popup never flashes opaque; exit_fade_::finished hides the widget once the fade-out completes. Bumps the QuarkWidgets submodule to the engine-finishing commit, which: makes calculateEasedProgress non-linear, applies timing in the factory via setMotionToken, and fixes the token-mapping double-prefix plus the bogus qw::components::core forward decl that blocked including the engine from desktop code. See the submodule commit for the full engine fix. Engine caveats (pre-existing, do not block the normal open/close flow): tick() ignores the Backward direction and stop() resets to m_from, so the launcher uses Forward-only enter/exit pairs with reversed ranges and accepts a possible minor glitch if the popup is dismissed during the ~250ms enter. milestone_04 doc: animation section updated -- engine switch done; the remaining caveat is the engine Backward/stop semantics (future work). Verified: build green; app_discoverer_test 9/9, desktop_entry_index_test 7/7, builtin_panel_registry_test 5/5 pass; CFMaterialFade/SlideAnimation symbols present in libCFDesktop_shared.so. --- .../ui/components/launcher/app_launcher.cpp | 140 +++++++++--------- desktop/ui/components/launcher/app_launcher.h | 26 ++-- .../todo/desktop/milestone_04_app_launcher.md | 14 +- third_party/QuarkWidgets | 2 +- 4 files changed, 94 insertions(+), 88 deletions(-) diff --git a/desktop/ui/components/launcher/app_launcher.cpp b/desktop/ui/components/launcher/app_launcher.cpp index 41479a2ad..1c3f5c415 100644 --- a/desktop/ui/components/launcher/app_launcher.cpp +++ b/desktop/ui/components/launcher/app_launcher.cpp @@ -5,8 +5,9 @@ * 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. The popup fades and slides in on open and reverses on close. - * 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 @@ -19,12 +20,12 @@ #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 #include #include #include @@ -34,7 +35,6 @@ #include #include #include -#include #include @@ -43,19 +43,17 @@ namespace cf::desktop::desktop_component { using namespace qw::core::token::literals; namespace { -constexpr int kMaxColumns = 5; ///< Maximum tiles per grid row. -constexpr int kGridSpacing = 12; ///< Gap between tiles (px). -constexpr int kMargin = 24; ///< Popup inner margin (px). -constexpr qreal kCornerRadius = 16; ///< Popup corner radius (px). -constexpr qreal kWidthRatio = 0.5; ///< Popup width as a fraction of available. -constexpr qreal kHeightRatio = 0.55; ///< Popup height as a fraction of available. -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 kEnterDurationMs = 250; ///< Enter animation duration (ms). -constexpr int kExitDurationMs = 150; ///< Exit animation duration (ms). -constexpr int kEnterSlidePx = 24; ///< Enter/exit vertical slide distance (px). +constexpr int kMaxColumns = 5; ///< Maximum tiles per grid row. +constexpr int kGridSpacing = 12; ///< Gap between tiles (px). +constexpr int kMargin = 24; ///< Popup inner margin (px). +constexpr qreal kCornerRadius = 16; ///< Popup corner radius (px). +constexpr qreal kWidthRatio = 0.5; ///< Popup width as a fraction of available. +constexpr qreal kHeightRatio = 0.55; ///< Popup height as a fraction of available. +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) { @@ -72,11 +70,9 @@ AppLauncher::AppLauncher(QWidget* parent) : QWidget(parent) { setAutoFillBackground(false); setupUi(); applyTheme(); - - // Whole-popup fade effect; drives the enter/exit fade alongside the slide. - opacity_effect_ = new QGraphicsOpacityEffect(this); - setGraphicsEffect(opacity_effect_); - opacity_effect_->setOpacity(1.0); + // 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 @@ -118,29 +114,26 @@ void AppLauncher::popup(const QRect& available) { } rest_pos_ = pos(); - // Seed the enter animation's initial frame (offset down + transparent) - // before show() so the popup never flashes fully opaque for one frame. - applyAnimProgress(0.0); + // 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(); - // Cancel any in-flight exit before entering (e.g. re-open while closing). - exit_anim_->stop(); - enter_anim_->stop(); - enter_anim_->start(); } void AppLauncher::hideLauncher() { - if (exit_anim_ == nullptr || !isVisible()) { + if (!isVisible()) { hide(); return; } - // Animate out from the current opacity so a half-entered popup also exits - // smoothly rather than snapping. - enter_anim_->stop(); - exit_anim_->stop(); - exit_anim_->setStartValue(opacity_effect_ != nullptr ? opacity_effect_->opacity() : qreal(1.0)); - exit_anim_->setEndValue(qreal(0.0)); - exit_anim_->start(); + // 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 { @@ -233,39 +226,46 @@ void AppLauncher::rebuildGrid() { } void AppLauncher::setupAnimations() { - // TODO: eventually switch to the QuarkWidgets MD3 engine - // (CFMaterialFadeAnimation + CFMaterialSlideAnimation). Deferred because the - // engine's MotionSpec integration is unfinished today (linear easing via - // calculateEasedProgress, hardcoded durations) -- this OutCubic/InCubic - // variant is higher quality until the engine is completed. - enter_anim_ = new QVariantAnimation(this); - enter_anim_->setDuration(kEnterDurationMs); - enter_anim_->setEasingCurve(QEasingCurve::OutCubic); - enter_anim_->setStartValue(qreal(0.0)); - enter_anim_->setEndValue(qreal(1.0)); - connect(enter_anim_, &QVariantAnimation::valueChanged, this, - [this](const QVariant& v) { applyAnimProgress(v.toReal()); }); - - exit_anim_ = new QVariantAnimation(this); - exit_anim_->setDuration(kExitDurationMs); - exit_anim_->setEasingCurve(QEasingCurve::InCubic); - connect(exit_anim_, &QVariantAnimation::valueChanged, this, - [this](const QVariant& v) { applyAnimProgress(v.toReal()); }); - // On exit completion: hide and reset to the resting state so the next - // popup() opens from a clean slate. - connect(exit_anim_, &QVariantAnimation::finished, this, [this]() { - hide(); - applyAnimProgress(1.0); - }); -} - -void AppLauncher::applyAnimProgress(qreal t) { - if (opacity_effect_ != nullptr) { - opacity_effect_->setOpacity(t); + // 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. } - // Slide vertically: t=0 -> offset down by kEnterSlidePx; t=1 -> at rest_pos_. - const int dy = static_cast((1.0 - t) * kEnterSlidePx); - move(rest_pos_ + QPoint(0, dy)); + + 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 73fd75621..a7b8bcdd7 100644 --- a/desktop/ui/components/launcher/app_launcher.h +++ b/desktop/ui/components/launcher/app_launcher.h @@ -26,13 +26,15 @@ #include #include -class QGraphicsOpacityEffect; class QGridLayout; class QKeyEvent; class QPaintEvent; class QRect; -class QVariantAnimation; +namespace qw::components::material { +class CFMaterialFadeAnimation; +class CFMaterialSlideAnimation; +} // namespace qw::components::material namespace qw::widget::material { class TextField; } @@ -186,12 +188,9 @@ class AppLauncher final : public QWidget { void applyTheme(); /// @brief Rebuilds the tile grid from apps_. void rebuildGrid(); - /// @brief Creates the opacity effect and the enter/exit animations. + /// @brief Creates the four MD3 enter/exit animations (fade + slide each), + /// bound to motion tokens so duration + easing resolve from the theme. void setupAnimations(); - /// @brief Applies an animation progress value to the popup: t=0 is fully - /// hidden and offset downward, t=1 is at rest (opaque, anchored). - /// @param[in] t Progress in [0.0, 1.0]. - void applyAnimProgress(qreal t); qw::widget::material::TextField* search_edit_{ nullptr}; ///< Filter box (ownership: this widget). @@ -199,10 +198,15 @@ class AppLauncher final : public QWidget { QList tiles_; ///< Current tiles. Ownership: Qt parented. QList apps_; ///< Backing application list. - QGraphicsOpacityEffect* opacity_effect_{nullptr}; ///< Whole-popup fade (owned by this). - QVariantAnimation* enter_anim_{nullptr}; ///< Enter: 0.0 -> 1.0 (fade in + slide up). - QVariantAnimation* exit_anim_{nullptr}; ///< Exit: 1.0 -> 0.0 (fade out + slide down). - QPoint rest_pos_; ///< Final anchored position (set in popup()). + 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 9bc11a714..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: 应用启动器 -> **状态**: ✅ 闭环跑通(网格 + 搜索 + .desktop + 双态 + 入场/退场动画 + MD3 TextField 已落地)。仅余:动画切到 QuarkWidgets MD3 引擎(待引擎 MotionSpec 接入完成,见 §七 待做) +> **状态**: ✅ 闭环跑通(网格 + 搜索 + .desktop + 双态 + 入场/退场动画[QuarkWidgets MD3 引擎] + MD3 TextField)。仅余:引擎 Backward/stop 语义待修(见 §七) > **预计周期**: 5-7 天 > **前置依赖**: [Milestone 3: 任务栏](milestone_03_taskbar.md) > **目标**: 点击任务栏"开始"按钮或桌面区域,弹出一个应用网格,可启动外部程序 @@ -271,12 +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 回退),全过 -### 待做 +### 已落地(动画) -- 入场/退场动画切到 QuarkWidgets MD3 引擎(`CFMaterialFadeAnimation`/`CFMaterialSlideAnimation`)。 - 当前 interim 用 `QPropertyAnimation`(OutCubic/InCubic + `QGraphicsOpacityEffect`,见 [app_launcher.cpp](../../../desktop/ui/components/launcher/app_launcher.cpp) `setupAnimations`)。 - 引擎自身的 `calculateEasedProgress` 目前是**线性** + 时长**硬编码**(MotionSpec 未接,`.cpp` 里 `// TODO: Integrate with MaterialMotionScheme`), - 需先补完 QuarkWidgets 引擎再切,否则动画质量降级。 +- 入场/退场动画改用 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..99413b830 160000 --- a/third_party/QuarkWidgets +++ b/third_party/QuarkWidgets @@ -1 +1 @@ -Subproject commit 73a3d1d8a6f65ae76a08009b524280b563888337 +Subproject commit 99413b83059c81acb9bcfa73fce738402a3b749f From b5a2abb22ce8c26091720e6e74855ba10ffd0a8b Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Fri, 3 Jul 2026 13:24:28 +0800 Subject: [PATCH 3/3] third_party update --- third_party/QuarkWidgets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/third_party/QuarkWidgets b/third_party/QuarkWidgets index 99413b830..77b23a8c3 160000 --- a/third_party/QuarkWidgets +++ b/third_party/QuarkWidgets @@ -1 +1 @@ -Subproject commit 99413b83059c81acb9bcfa73fce738402a3b749f +Subproject commit 77b23a8c3c9d33e12bc57ddb592dd94fee99d575