Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 78 additions & 5 deletions desktop/ui/components/launcher/app_launcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,26 @@
* 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
*/

#include "app_launcher.h"

#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 <QGridLayout>
#include <QGuiApplication>
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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_);

Expand All @@ -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(); });
}

Expand Down Expand Up @@ -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<float>(-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<float>(kEnterSlidePx));
exit_slide_->setMotionToken("mediumExit");
exit_slide_->setTargetWidget(this);
}

} // namespace cf::desktop::desktop_component
35 changes: 28 additions & 7 deletions desktop/ui/components/launcher/app_launcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<LauncherTile*> tiles_; ///< Current tiles. Ownership: Qt parented.
QList<AppEntry> 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<LauncherTile*> tiles_; ///< Current tiles. Ownership: Qt parented.
QList<AppEntry> 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).
Expand Down
12 changes: 8 additions & 4 deletions document/todo/desktop/milestone_04_app_launcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
> **目标**: 点击任务栏"开始"按钮或桌面区域,弹出一个应用网格,可启动外部程序
Expand Down Expand Up @@ -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。

---

Expand Down
Loading