feat(input): 完成阶段 1 输入采集与规范化

This commit is contained in:
Codex
2026-10-07 20:42:50 +08:00
parent 18caf91633
commit c57ddaf74f
12 changed files with 1803 additions and 6 deletions
+2
View File
@@ -185,6 +185,8 @@ public:
// 绘制入口。实现应只在 dirty/show 条件满足时写屏,并在完成后清理 dirty。
virtual void draw() = 0;
// 事件入口。返回 true 表示事件已消费;若事件造成视觉变化,应设置 eventVisualChanged 并请求重绘。
// 阶段 1 兼容过渡:EX_MOUSE 永久保留在此入口;EX_KEY 暂时仍按旧路径下发以保持行为,
// 计划在阶段 3 KeyboardRouter 一次性换轨后停止下发。派生控件不要再新增对 EX_KEY 的依赖。
virtual bool handleEvent(const ExMessage& msg) = 0;
// 轻量清理接口:仅用于“没有机会收到本次 WM_MOUSEMOVE”的兄弟分支,
// 清掉 hover / tooltip / 临时按下态等鼠标瞬时状态。
+124
View File
@@ -0,0 +1,124 @@
/*******************************************************************************
* 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.
******************************************************************************/
#pragma once
#include <windows.h>
#include <cstdint>
#include <string>
#include <variant>
namespace StellarX
{
enum class KeyAction : std::uint8_t
{
Down,
Up
};
// Snapshot modifiers in WndProc; delayed consumers must not reread global state.
struct KeyModifiers
{
bool leftControl = false;
bool rightControl = false;
bool leftShift = false;
bool rightShift = false;
bool leftAlt = false;
bool rightAlt = false;
bool control() const noexcept { return leftControl || rightControl; }
bool shift() const noexcept { return leftShift || rightShift; }
bool alt() const noexcept { return leftAlt || rightAlt; }
};
struct KeyEvent
{
KeyAction action = KeyAction::Down;
UINT virtualKey = 0; // Raw WndProc VK; VK_PROCESSKEY is never rewritten.
UINT physicalVirtualKey = 0; // Left/right-normalized modifier VK.
UINT scanCode = 0;
std::uint16_t repeatCount = 0;
bool extended = false;
bool wasDown = false;
bool altContext = false; // Raw lParam bit 29; modifiers.alt() is the semantic snapshot.
bool systemKey = false;
// Snapshot of the standard IME lifecycle axis at capture time.
// It never overrides the per-key ownership expressed by VK_PROCESSKEY.
bool imeComposingAtCapture = false;
KeyModifiers modifiers{};
bool isAutoRepeat() const noexcept
{
return action == KeyAction::Down && wasDown;
}
bool isImeOwnedAtCapture() const noexcept
{
// Per-key ownership and IME lifecycle are intentionally independent axes.
// A normal key captured during a standard composition remains a normal key;
// routing may suppress it because of the lifecycle snapshot, but must not
// rewrite its ownership.
return virtualKey == VK_PROCESSKEY;
}
};
enum class TextSource : std::uint8_t
{
ImeResult,
UnicodeChar,
UnicodeScalar,
AnsiCharFallback,
Clipboard
};
struct TextInputEvent
{
std::wstring text;
TextSource source = TextSource::UnicodeChar;
std::uint64_t compositionId = 0; // Zero for non-IME input.
};
enum class CompositionStage : std::uint8_t
{
Start,
Update,
Commit,
Cancel,
End
};
struct CompositionEvent
{
CompositionStage stage = CompositionStage::Start;
std::uint64_t compositionId = 0;
std::wstring compositionText;
std::uint32_t caretOffset = 0;
};
struct WindowActivationEvent
{
bool keyboardFocusActive = false;
HWND otherWindow = nullptr;
};
using NativeInputPayload = std::variant<
KeyEvent,
TextInputEvent,
CompositionEvent,
WindowActivationEvent>;
struct NativeInputEvent
{
std::uint64_t sequence = 0;
DWORD messageTime = 0;
HWND hwnd = nullptr;
NativeInputPayload payload{};
};
}
+1
View File
@@ -30,6 +30,7 @@
#pragma once
#include "CoreTypes.h"
#include "InputEvents.h"
#include "SxLog.h"
#include "Control.h"
#include"Canvas.h"
+19
View File
@@ -17,14 +17,26 @@
#include "Control.h"
#include <atomic>
#include <cstdint>
#include <string>
#include <vector>
#include <memory>
#include <windows.h>
class Window;
namespace StellarX::Internal
{
class NativeInputContext;
struct NativeInputRuntimeSnapshot;
NativeInputRuntimeSnapshot inspectWindowNativeInputForTesting(const ::Window& window) noexcept;
}
class Window
{
friend class Dialog;
friend StellarX::Internal::NativeInputRuntimeSnapshot
StellarX::Internal::inspectWindowNativeInputForTesting(const Window& window) noexcept;
// —— 尺寸状态 ——(绘制尺寸与待应用尺寸分离;收口时一次性更新)
int width; // 当前有效宽(已应用到画布/控件的客户区宽)
@@ -46,7 +58,14 @@ class Window
// EasyX 的 WndProc 与宿主事件循环分属不同线程;只有 WndProc 完成 WM_NCDESTROY
// 转发后才发布 false,避免宿主提前析构仍在执行 WndProcThunk 的 Window 对象。
std::atomic<bool> nativeWindowAlive{ false };
// 阶段 1 只建立采集、规范化与跨线程队列;宿主安全点只观测排空,不做业务路由。
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;
bool hasLiveNativeWindow() const noexcept;
bool drainNativeInputAtSafePoint();
static LRESULT CALLBACK WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l); // 静态过程分发到 this
// —— 绘制相关 ——(是否使用合成双缓冲、窗口标题、背景等)