Files
Lu Fan(SonnyCalcr) e9825a521f feat(server): 优化设置窗口滚动手感并加入启动画面 (#1045)
* feat(ui): 滚轮平滑滚动贴近浏览器手感

- 每次滚轮的平滑滚动改为 cubic ease-out,接续当前速度,不再有指数逼近的长尾
- 滚动距离跟随系统「一次滚动的行数」,「一次滚动一屏」时按整页滚动
- 合成交换链的帧时间对齐到 DWM 的刷新网格,消除绘制开始时刻抖动带来的位移抖动

* fix(server): 设置页滚动到 Niya Demo 皮肤时不再卡顿

Niya Demo 的 background.png 实际是 1280x720 的 WebP,预览第一次滚入视野时在界面线程同步解码,首次加载 WebP 解码器要 100-200ms。皮肤预览的静态图改为打开页面时就在后台线程解码,界面线程只创建位图。

设置页与词库表格的滚轮步长同时调到 100(与 Chrome 一致),平滑时长 260ms。

* feat(server): 设置窗口加入启动画面与开关

打开设置时先在窗口中央显示齿轮图标(Win11 用 Segoe Fluent Icons U+E713,Win10 回退 Segoe MDL2 Assets),等首个 configSnapshot 填好控件后再淡入主界面,避免输入方案先显示全拼再跳到双拼。齿轮至少停留 200ms,淡出 140ms。

「关于」页新增「启动动画」开关(appearance.settings_launch_animation,默认开启),关闭后只短暂显示纯色背景。新键已写入出厂模板和开发配置,并补了默认值与读写往返测试。
2026-10-18 00:27:50 +08:00

129 lines
5.0 KiB
C++

#pragma once
#include "DeviceResources.h"
#include "Scene.h"
#include <memory>
#include <string>
namespace msimeui
{
enum class WindowInitialPlacement
{
Center,
BottomCenter,
};
class Window
{
public:
Window(std::wstring className, std::wstring title, int width, int height);
~Window();
bool Create();
bool AdoptExistingHwnd(HWND hwnd);
LRESULT DispatchImportedMessage(UINT message, WPARAM wParam, LPARAM lParam);
void SetStealFocusOnClick(bool enabled);
int Run(int nCmdShow);
HWND GetHandle() const;
HINSTANCE GetInstance() const;
Scene *GetScene() const;
DeviceResources &GetDeviceResources();
/// DPI override, mirrored into the window's own DeviceResources. Only for
/// cases where the system window DPI diverges from the content's real
/// scale (RDP client-scaling sync); see DeviceResources::SetDpiOverride.
/// GetDpi() and every pixels<->DIPs conversion (hit testing, drag region,
/// layout) honor it while set.
void SetDpiOverride(FLOAT dpi);
float GetDpi() const;
PointF ClientPixelsToDips(const POINT &point) const;
SizeF ClientPixelsToDips(const SIZE &size) const;
RECT DipsToClientPixels(const RectF &rect) const;
void Invalidate();
void Invalidate(const RectF &rect);
void InvalidateMeasure();
void InvalidateMeasure(Visual *source);
void InvalidateArrange();
void InvalidateArrange(Visual *source);
void Relayout();
void FocusVisual(Visual *visual);
void SetWindowStyle(DWORD style, DWORD extendedStyle = 0);
void SetDragRegionHeight(float height);
void SetRoundedCorners(bool enabled);
void SetInitialPlacement(WindowInitialPlacement placement, int bottomMargin = 12);
// Hosts that own the message loop (an adopted HWND in a larger application) keep
// running after this window goes away.
void SetQuitOnDestroy(bool quit);
// Replaces ThemeManager's windowBackground as the color each frame is cleared to.
void SetBackgroundColor(const D2D1_COLOR_F &color);
void SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE mode);
// Presents through a DirectComposition swap chain instead of an HWND render target.
// Needed by windows whose frame is drawn by the app over DWM's extended frame, where
// an HWND target's output is not composited; it also avoids the redirection copy.
// `opaque` ignores alpha, which an opaque window needs for ClearType text.
void SetCompositionRendering(bool enabled, bool opaque = false);
// Caret blinking for TextBox. Create() starts it; an adopted HWND has to ask.
void StartCaretTimer();
// Schedules another frame after the current one. Animated visuals call this from
// Render while they are still moving, so frames keep coming at the display rate
// and stop on their own when everything has settled.
void RequestAnimationFrame();
// Timestamp of the frame being rendered (NowMs() outside of painting): when it will
// reach the screen, as far as the compositor tells. Every animation sampled in one
// frame sees the same time.
double GetFrameTime() const;
// Re-evaluates which visual is under the cursor, e.g. after content scrolled or
// was replaced without the mouse moving.
void RefreshHover();
void SetScene(std::unique_ptr<Scene> scene);
private:
// Repaints when the next frame of an animated image is due.
static constexpr UINT_PTR kImageAnimationTimerId = 0x4D49;
static LRESULT CALLBACK WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
LRESULT HandleMessage(UINT message, WPARAM wParam, LPARAM lParam);
void OnPaint();
double NextFrameTimeMs() const;
void OnSize();
bool OnMouseWheel(WPARAM wParam, LPARAM lParam);
LRESULT OnLeftButtonDown(WPARAM wParam, LPARAM lParam);
void UpdateHover(const POINT &point);
void SetFocusedVisual(Visual *visual);
bool UpdateCursorForClientPoint(const POINT &point);
std::wstring className_;
std::wstring title_;
int width_ = 0;
int height_ = 0;
HINSTANCE instance_ = nullptr;
HWND hwnd_ = nullptr;
FLOAT dpiOverride_ = 0.0f;
DeviceResources deviceResources_;
std::unique_ptr<Scene> scene_;
Visual *focusedVisual_ = nullptr;
Visual *hoveredVisual_ = nullptr;
Visual *capturedVisual_ = nullptr;
bool mouseLeaveTracking_ = false;
DWORD windowStyle_ = WS_OVERLAPPEDWINDOW;
DWORD extendedWindowStyle_ = 0;
float dragRegionHeight_ = 0.0f;
bool roundedCorners_ = false;
WindowInitialPlacement initialPlacement_ = WindowInitialPlacement::Center;
int initialBottomMargin_ = 12;
bool stealFocusOnClick_ = true;
bool adoptedHwnd_ = false;
bool quitOnDestroy_ = true;
bool hasBackgroundColor_ = false;
D2D1_COLOR_F backgroundColor_ = {};
D2D1_TEXT_ANTIALIAS_MODE textAntialiasMode_ = D2D1_TEXT_ANTIALIAS_MODE_DEFAULT;
bool painting_ = false;
bool composition_ = false;
bool animationRequested_ = false;
double frameTimeMs_ = 0.0;
};
} // namespace msimeui