feat(input): 完成阶段3键盘路由与焦点重绘收口
This commit is contained in:
@@ -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);
|
||||
// 判断鼠标是否在圆形按钮内
|
||||
|
||||
@@ -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;最外层结束时提交子控件队列。
|
||||
|
||||
@@ -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; }
|
||||
// 派生控件可响应焦点变化;回调允许安全地请求焦点或排队修改控件树。
|
||||
|
||||
@@ -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 = {});
|
||||
|
||||
@@ -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{};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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 需要同步刷新其内部页签栏和页面区。
|
||||
|
||||
@@ -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,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。
|
||||
|
||||
Reference in New Issue
Block a user