feat(input): 完成阶段3键盘路由与焦点重绘收口

This commit is contained in:
Codex
2026-10-10 00:05:23 +08:00
parent 69c26a35a2
commit 29df994f09
30 changed files with 2227 additions and 80 deletions
+15 -1
View File
@@ -32,6 +32,9 @@ class Button : public Control
std::string text; // 按钮上的文字
bool click; // 是否被点击
bool hover; // 是否被悬停
// 独立于 click:NORMAL 的 click 是鼠标瞬时态,TOGGLE 的 click 是持久态;
// 键盘 Space 的按下/释放生命周期不能复用它们。
bool keyboardArmed = false;
std::string cutText; // 切割后的文本
bool needCutText = true; // 是否需要切割文本
@@ -104,6 +107,8 @@ class Button : public Control
void draw() override;
// 处理按钮点击、悬停、切换状态和 Tooltip 触发。
bool handleEvent(const ExMessage& msg) override;
// 阶段 3 Routed 生产路径:Space 采用按下/释放语义,Enter 首次按下即激活。
StellarX::KeyRouteResult handleKeyEvent(StellarX::RoutedKeyEvent& event) override;
// 由窗口时钟推进 Tooltip 到期状态,即使没有新的鼠标消息也能触发显示。
void tick(ULONGLONG nowMs) override;
// 清理按钮的鼠标瞬时状态;用于父容器在 WM_MOUSEMOVE 短路后补做 hover/tooltip 收口。
@@ -121,7 +126,9 @@ class Button : public Control
// 设置 TOGGLE 模式下切换到关闭状态时的回调
void setOnToggleOffListener(std::function<void()> callback);
// 设置按钮模式(NORMAL / TOGGLE / DISABLED)
void setbuttonMode(StellarX::ButtonMode mode);
void setbuttonMode(StellarX::ButtonMode newMode);
// 隐藏会同步取消尚未释放的键盘按压态。
void setIsVisible(bool visible) override;
// 设置圆角矩形的圆角宽度
void setROUND_RECTANGLEwidth(int width);
// 设置圆角矩形的圆角高度
@@ -196,7 +203,14 @@ protected:
{
return mode != StellarX::ButtonMode::DISABLED;
}
void onLogicalFocusChanged(bool focused, StellarX::FocusReason reason) override;
void onKeyboardRoutingSuspended() override;
private:
// 返回状态是否发生变化;requestVisual=false 用于焦点/可见性/模式路径,
// 这些调用方已经负责整体失效或重绘。
bool setKeyboardArmed(bool armed, bool requestVisual);
// 执行一次 Button 命令。调用用户回调后不再访问成员。
void activateFromKeyboard();
// 初始化按钮内部状态、颜色和 Tooltip 默认样式
void initButton(const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape, COLORREF ct, COLORREF cf, COLORREF ch);
// 判断鼠标是否在圆形按钮内
+4
View File
@@ -124,6 +124,10 @@ protected:
std::vector<StellarX::FocusTargetSnapshot>& snapshots) const override;
bool applyFocusState(const StellarX::FocusTargetId& target, bool focused,
StellarX::FocusReason reason) override;
bool dispatchKeyEventToTarget(const StellarX::FocusTargetId& target,
StellarX::RoutedKeyEvent& event, StellarX::KeyRouteResult& result) override;
bool dispatchKeyboardSuspensionToTarget(
const StellarX::FocusTargetId& target) override;
// 进入 Canvas 子树 mutation transaction;会向父容器/Window 传播事务深度。
void beginMutationScope() override;
// 离开 Canvas 子树 mutation transaction;最外层结束时提交子控件队列。
+15 -2
View File
@@ -33,6 +33,7 @@
#include <functional>
#include <atomic>
#include "CoreTypes.h"
#include "KeyboardRouting.h"
class Window;
@@ -195,9 +196,12 @@ public:
// 绘制入口。实现应只在 dirty/show 条件满足时写屏,并在完成后清理 dirty。
virtual void draw() = 0;
// 事件入口。返回 true 表示事件已消费;若事件造成视觉变化,应设置 eventVisualChanged 并请求重绘。
// 阶段 1 兼容过渡:EX_MOUSE 永久保留在此入口;EX_KEY 暂时仍按旧路径下发以保持行为,
// 计划在阶段 3 KeyboardRouter 一次性换轨后停止下发。派生控件不要再新增对 EX_KEY 的依赖。
// EX_MOUSE 永久保留在此入口;阶段 3 起生产事件循环不再向这里下发 EX_KEY。
// 派生控件的键盘逻辑应实现 handleKeyEvent(),旧 EX_KEY 覆盖不会再被调用。
virtual bool handleEvent(const ExMessage& msg) = 0;
// 阶段 3 定向键盘入口。默认空实现保证现有派生控件无需立即修改;
// KeyboardRouter 在句柄解析与 mutation 边界内逐节点调用。
virtual StellarX::KeyRouteResult handleKeyEvent(StellarX::RoutedKeyEvent&) { return {}; }
// 轻量清理接口:仅用于“没有机会收到本次 WM_MOUSEMOVE”的兄弟分支,
// 清掉 hover / tooltip / 临时按下态等鼠标瞬时状态。
// 该接口不是事件分发入口,不参与新的命中判断,也不允许回写设计基线。
@@ -306,6 +310,15 @@ protected:
std::vector<StellarX::FocusTargetSnapshot>& snapshots) const;
virtual bool applyFocusState(const StellarX::FocusTargetId& target, bool focused,
StellarX::FocusReason reason);
// 键盘路由按稳定身份逐节点重新解析;返回 true 仅表示本次找到并调用了目标。
// 实现不得把解析得到的 Control* 暴露到当前调用之外,回调返回后也不得再次访问目标。
virtual bool dispatchKeyEventToTarget(const StellarX::FocusTargetId& target,
StellarX::RoutedKeyEvent& event, StellarX::KeyRouteResult& result);
// 失活或模态隔离时按稳定身份取消键盘瞬时交互;Button 将在切片 5
// 覆盖 onKeyboardRoutingSuspended() 清理独立 keyboard armed 状态。
virtual bool dispatchKeyboardSuspensionToTarget(
const StellarX::FocusTargetId& target);
virtual void onKeyboardRoutingSuspended() {}
// 各控件动态提供 enabled 语义;Button 会根据 DISABLED 模式覆盖。
virtual bool isFocusEnabled() const { return true; }
// 派生控件可响应焦点变化;回调允许安全地请求焦点或排队修改控件树。
+9
View File
@@ -12,8 +12,14 @@ class Window;
namespace StellarX
{
namespace Internal
{
class KeyboardRouter;
}
class FocusManager
{
friend class Internal::KeyboardRouter;
struct FocusRequest
{
FocusTargetId target{};
@@ -84,6 +90,9 @@ namespace StellarX
// Valid Tab stops in the active scope, ordered by tabIndex and stable tree order.
std::vector<FocusTargetSnapshot> collectTabStops() const;
// Rebuilds the current scope's Tab order for every call, then moves with
// wrap-around. No traversal snapshot is retained across key events.
FocusChangeResult advanceTabFocus(bool reverse);
// Stage 2 provides scope mechanics; real Dialog integration remains in stage 4.
FocusScopeId pushScope(FocusTargetId scopeRoot, FocusTargetId initialFocus = {});
+14 -2
View File
@@ -1,7 +1,8 @@
/*******************************************************************************
* StellarX native keyboard, text, IME, and activation event model.
* Stage 1 only captures, normalizes, queues, and observes these events.
* Focus, routing, and control behavior are intentionally out of scope.
* Stage 1 introduced capture, normalization, queuing, and observation.
* After the Stage 3 atomic cutover, normalized KeyEvent routing is the
* production business path; EasyX EX_KEY remains a drain-only observation.
******************************************************************************/
#pragma once
@@ -108,6 +109,16 @@ namespace StellarX
HWND otherWindow = nullptr;
};
// Atomic producer snapshot copied into every native input event. Generation
// changes whenever keyboard focus active state changes, so a delayed event
// cannot cross a deactivate/reactivate boundary merely because the window is
// active again when the host thread eventually drains it.
struct NativeInputActivationSnapshot
{
bool keyboardFocusActive = false;
std::uint64_t generation = 0;
};
using NativeInputPayload = std::variant<
KeyEvent,
TextInputEvent,
@@ -119,6 +130,7 @@ namespace StellarX
std::uint64_t sequence = 0;
DWORD messageTime = 0;
HWND hwnd = nullptr;
NativeInputActivationSnapshot activationAtCapture{};
NativeInputPayload payload{};
};
}
+34
View File
@@ -0,0 +1,34 @@
/*******************************************************************************
* StellarX routed keyboard event contract.
* Stage 3 defines the public control seam and internal bubbling contract.
* Production routing uses this contract after the slice-6 atomic cutover.
******************************************************************************/
#pragma once
#include "InputEvents.h"
#include "CoreTypes.h"
namespace StellarX
{
// A normalized key plus the stable identities used while it bubbles through
// the focus path. The router updates currentTarget before each control call;
// controls must not retain either identity as a raw pointer.
struct RoutedKeyEvent
{
std::uint64_t sequence = 0;
DWORD messageTime = 0;
HWND hwnd = nullptr;
KeyEvent key{};
FocusTargetId originalTarget{};
FocusTargetId currentTarget{};
};
// Stopping the control bubble and suppressing a later framework default are
// independent decisions. Neither result can change the completed WndProc
// return value.
struct KeyRouteResult
{
bool stopBubble = false;
bool preventFrameworkDefault = false;
};
}
+3 -1
View File
@@ -22,7 +22,8 @@
* 只需包含此文件即可使用框架所有功能。
* 示例: #include "StellarX.h"
* @包含模块:
* CoreTypes.h / SxLog.h / Control.h / Canvas.h / Window.h
* CoreTypes.h / InputEvents.h / KeyboardRouting.h / SxLog.h
* Control.h / Canvas.h / Window.h
* Button.h / Label.h / TextBox.h / Table.h
* Dialog.h / MessageBox.h / TabControl.h
******************************************************************************/
@@ -32,6 +33,7 @@
#include "CoreTypes.h"
#include "FocusManager.h"
#include "InputEvents.h"
#include "KeyboardRouting.h"
#include "SxLog.h"
#include "Control.h"
#include"Canvas.h"
+4
View File
@@ -117,6 +117,10 @@ protected:
std::vector<StellarX::FocusTargetSnapshot>& snapshots) const override;
bool applyFocusState(const StellarX::FocusTargetId& target, bool focused,
StellarX::FocusReason reason) override;
bool dispatchKeyEventToTarget(const StellarX::FocusTargetId& target,
StellarX::RoutedKeyEvent& event, StellarX::KeyRouteResult& result) override;
bool dispatchKeyboardSuspensionToTarget(
const StellarX::FocusTargetId& target) override;
// 将回调期间加入的页签和页内控件在外层事务结束时一次性提交。
void flushPendingMutations() override;
// 外层统一解算后,TabControl 需要同步刷新其内部页签栏和页面区。
+4
View File
@@ -193,4 +193,8 @@ protected:
std::vector<StellarX::FocusTargetSnapshot>& snapshots) const override;
bool applyFocusState(const StellarX::FocusTargetId& target, bool focused,
StellarX::FocusReason reason) override;
bool dispatchKeyEventToTarget(const StellarX::FocusTargetId& target,
StellarX::RoutedKeyEvent& event, StellarX::KeyRouteResult& result) override;
bool dispatchKeyboardSuspensionToTarget(
const StellarX::FocusTargetId& target) override;
};
+29 -2
View File
@@ -29,8 +29,15 @@ class Window;
namespace StellarX::Internal
{
class NativeInputContext;
class KeyboardRouter;
struct NativeInputRuntimeSnapshot;
enum class KeyboardPipelineMode : std::uint8_t;
NativeInputRuntimeSnapshot inspectWindowNativeInputForTesting(const ::Window& window) noexcept;
bool configureKeyboardPipelineForTesting(
::Window& window, KeyboardPipelineMode mode) noexcept;
KeyboardPipelineMode keyboardPipelineMode(
const ::Window& window) noexcept;
void lockKeyboardPipeline(::Window& window) noexcept;
}
namespace StellarX
@@ -44,6 +51,12 @@ class Window
friend class StellarX::FocusManager;
friend StellarX::Internal::NativeInputRuntimeSnapshot
StellarX::Internal::inspectWindowNativeInputForTesting(const Window& window) noexcept;
friend bool StellarX::Internal::configureKeyboardPipelineForTesting(
Window& window, StellarX::Internal::KeyboardPipelineMode mode) noexcept;
friend StellarX::Internal::KeyboardPipelineMode
StellarX::Internal::keyboardPipelineMode(const Window& window) noexcept;
friend void StellarX::Internal::lockKeyboardPipeline(Window& window) noexcept;
friend class StellarX::Internal::KeyboardRouter;
// —— 尺寸状态 ——(绘制尺寸与待应用尺寸分离;收口时一次性更新)
int width; // 当前有效宽(已应用到画布/控件的客户区宽)
@@ -65,14 +78,22 @@ class Window
// EasyX 的 WndProc 与宿主事件循环分属不同线程;只有 WndProc 完成 WM_NCDESTROY
// 转发后才发布 false,避免宿主提前析构仍在执行 WndProcThunk 的 Window 对象。
std::atomic<bool> nativeWindowAlive{ false };
// 阶段 1 只建立采集、规范化与跨线程队列;宿主安全点只观测排空,不做业务路由。
// Routed 生产模式在宿主安全点逐条交给 KeyboardRouter;Legacy 仅供切换基线测试。
std::unique_ptr<StellarX::Internal::NativeInputContext> nativeInputContext;
bool nativeInputFailureReported = false;
std::uint64_t nativeInputRejectedTextReported = 0;
std::uint64_t nativeInputImeQueryFailuresReported = 0;
std::uint64_t nativeInputDiscardedFragmentsReported = 0;
// Stage 3 slice 6 production default. WndProc also reads this for the
// synchronous WM_GETDLGCODE answer, so the cross-thread snapshot is atomic.
std::atomic<bool> routedKeyboardPipeline{ true };
bool keyboardPipelineLocked = false;
// Host-thread diagnostics: EX_KEY must continue draining after cutover while
// legacy business delivery remains exactly zero.
std::uint64_t easyXKeyboardMessagesDrained = 0;
std::uint64_t legacyEasyXKeyboardBusinessDeliveries = 0;
bool hasLiveNativeWindow() const noexcept;
bool drainNativeInputAtSafePoint();
bool drainNativeInputAtSafePoint(bool modalBusinessSuppressed = false);
static LRESULT CALLBACK WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l); // 静态过程分发到 this
// —— 绘制相关 ——(是否使用合成双缓冲、窗口标题、背景等)
@@ -202,6 +223,12 @@ private:
std::vector<StellarX::FocusTargetSnapshot>& snapshots) const;
bool applyFocusState(const StellarX::FocusTargetId& target, bool focused,
StellarX::FocusReason reason);
// KeyboardRouter 专用:在当前调用栈内按稳定身份重新解析并立即投递,
// 不把借用的 Control* 传出本函数。
bool dispatchKeyEventToTarget(const StellarX::FocusTargetId& target,
StellarX::RoutedKeyEvent& event, StellarX::KeyRouteResult& result);
bool dispatchKeyboardSuspensionToTarget(
const StellarX::FocusTargetId& target);
// 提交 Window 顶层控件队列;只允许在最外层事务安全点调用。
void flushPendingControlMutations();
// 自顶向下提交 Window、Canvas、TabControl 等容器中累积的 pending mutation。