feat(focus): 完成阶段 2 通用焦点系统
This commit is contained in:
@@ -191,6 +191,11 @@ public:
|
||||
void setTooltipText(const std::string& s) { tipTextClick = s; tipUserOverride = true; }
|
||||
// 设置 TOGGLE 模式开/关两种状态下的 Tooltip 文本。
|
||||
void setTooltipTextsForToggle(const std::string& onText, const std::string& offText);
|
||||
protected:
|
||||
bool isFocusEnabled() const override
|
||||
{
|
||||
return mode != StellarX::ButtonMode::DISABLED;
|
||||
}
|
||||
private:
|
||||
// 初始化按钮内部状态、颜色和 Tooltip 默认样式
|
||||
void initButton(const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape, COLORREF ct, COLORREF cf, COLORREF ch);
|
||||
|
||||
@@ -118,6 +118,12 @@ public:
|
||||
// 托管收口阶段执行 Canvas 的真正重绘
|
||||
void commitManagedRepaint() override;
|
||||
protected:
|
||||
bool queryFocusTargetSnapshot(const StellarX::FocusTargetId& target,
|
||||
StellarX::FocusTargetSnapshot& snapshot) const override;
|
||||
void collectFocusTargetSnapshots(
|
||||
std::vector<StellarX::FocusTargetSnapshot>& snapshots) const override;
|
||||
bool applyFocusState(const StellarX::FocusTargetId& target, bool focused,
|
||||
StellarX::FocusReason reason) override;
|
||||
// 进入 Canvas 子树 mutation transaction;会向父容器/Window 传播事务深度。
|
||||
void beginMutationScope() override;
|
||||
// 离开 Canvas 子树 mutation transaction;最外层结束时提交子控件队列。
|
||||
|
||||
@@ -36,10 +36,17 @@
|
||||
|
||||
class Window;
|
||||
|
||||
namespace StellarX
|
||||
{
|
||||
class FocusManager;
|
||||
}
|
||||
|
||||
class Control
|
||||
{
|
||||
friend class Window;
|
||||
friend class Canvas;
|
||||
friend class Table;
|
||||
friend class TabControl;
|
||||
protected:
|
||||
inline static std::atomic<std::uint64_t> nextHandleValue{ 1 };
|
||||
// 句柄随控件构造生成,容器只通过句柄排队删除,避免遍历期间直接持有失效指针。
|
||||
@@ -52,6 +59,9 @@ protected:
|
||||
bool dirty = true; // 是否重绘
|
||||
bool show = true; // 是否显示
|
||||
bool eventVisualChanged = false; // 最近一次 handleEvent 是否真的引发了视觉变化(用于上层判断是否需要登记重绘)
|
||||
StellarX::FocusPolicy focusPolicy = StellarX::FocusPolicy::None; // 逻辑焦点入口能力
|
||||
std::int32_t tabIndex = 0; // 同一作用域内的显式 Tab 顺序;相同时保持树顺序
|
||||
bool logicalFocused = false; // 仅由 FocusManager 提交,控件不得自行切换
|
||||
|
||||
/* == 布局模式 == */
|
||||
StellarX::LayoutMode layoutMode = StellarX::LayoutMode::Fixed; // 布局模式
|
||||
@@ -228,6 +238,15 @@ public:
|
||||
std::string getId() const { return id; }
|
||||
// 返回稳定句柄而非对象地址;调用方不得据此推断控件当前仍在容器中。
|
||||
StellarX::ControlHandle getHandle() const noexcept { return handle; }
|
||||
// 申请逻辑焦点。回调重入时请求会进入 FocusManager 当前回合的单槽 pending。
|
||||
StellarX::FocusChangeResult requestFocus(
|
||||
StellarX::FocusReason reason = StellarX::FocusReason::Programmatic);
|
||||
// 设置焦点入口策略;修改后若当前焦点不再有效,会立即进入焦点回退。
|
||||
void setFocusPolicy(StellarX::FocusPolicy policy);
|
||||
StellarX::FocusPolicy getFocusPolicy() const noexcept { return focusPolicy; }
|
||||
void setTabIndex(std::int32_t value);
|
||||
std::int32_t getTabIndex() const noexcept { return tabIndex; }
|
||||
bool hasLogicalFocus() const noexcept { return logicalFocused; }
|
||||
// 检查控件当前是否处于待重绘状态。
|
||||
bool isDirty() { return dirty; }
|
||||
//获取控件最近一次事件处理是否引发了视觉变化
|
||||
@@ -280,6 +299,19 @@ public:
|
||||
// - 仅用于少数需要把当前结果固化为新设计基线的场景
|
||||
void commitCurrentGeometryAsDesignRect();
|
||||
protected:
|
||||
// 焦点参与协议:默认节点只解析 {本控件句柄, 0};复合控件递归转发给真实子 Control。
|
||||
virtual bool queryFocusTargetSnapshot(const StellarX::FocusTargetId& target,
|
||||
StellarX::FocusTargetSnapshot& snapshot) const;
|
||||
virtual void collectFocusTargetSnapshots(
|
||||
std::vector<StellarX::FocusTargetSnapshot>& snapshots) const;
|
||||
virtual bool applyFocusState(const StellarX::FocusTargetId& target, bool focused,
|
||||
StellarX::FocusReason reason);
|
||||
// 各控件动态提供 enabled 语义;Button 会根据 DISABLED 模式覆盖。
|
||||
virtual bool isFocusEnabled() const { return true; }
|
||||
// 派生控件可响应焦点变化;回调允许安全地请求焦点或排队修改控件树。
|
||||
virtual void onLogicalFocusChanged(bool, StellarX::FocusReason) {}
|
||||
bool isEffectivelyVisible() const;
|
||||
void notifyFocusSubtreeStateChanged();
|
||||
// 容器和窗口在事件、绘制、tick 期间进入事务;具体容器在最外层结束时提交队列。
|
||||
MutationScope enterMutationScope() { return MutationScope(this); }
|
||||
// 进入子树结构变更事务。默认向父容器或宿主 Window 传播,使整棵树共享同一提交边界。
|
||||
|
||||
@@ -64,6 +64,111 @@ namespace StellarX
|
||||
bool operator!=(const ControlHandle& other) const noexcept { return value != other.value; }
|
||||
};
|
||||
|
||||
/**
|
||||
* @结构体名称: FocusTargetId
|
||||
* @功能描述: 表示一个逻辑焦点参与节点的稳定身份
|
||||
*
|
||||
* @详细说明:
|
||||
* owner 指向拥有该焦点节点的 Control。当前阶段所有焦点节点都是真实 Control,
|
||||
* 因此 subNode 固定为 0;非 0 值预留给未来没有独立 Control 对象的轻量节点。
|
||||
* ControlHandle 在进程内单调分配且不复用,因此本结构不额外复制 generation。
|
||||
*/
|
||||
struct FocusTargetId
|
||||
{
|
||||
ControlHandle owner{};
|
||||
std::uint32_t subNode = 0;
|
||||
|
||||
explicit operator bool() const noexcept { return static_cast<bool>(owner); }
|
||||
bool operator==(const FocusTargetId& other) const noexcept
|
||||
{
|
||||
return owner == other.owner && subNode == other.subNode;
|
||||
}
|
||||
bool operator!=(const FocusTargetId& other) const noexcept { return !(*this == other); }
|
||||
};
|
||||
|
||||
// 标识一个 Window 内的逻辑焦点作用域。0 表示无效,根作用域由 FocusManager 创建。
|
||||
struct FocusScopeId
|
||||
{
|
||||
std::uint64_t value = 0;
|
||||
|
||||
explicit operator bool() const noexcept { return value != 0; }
|
||||
bool operator==(const FocusScopeId& other) const noexcept { return value == other.value; }
|
||||
bool operator!=(const FocusScopeId& other) const noexcept { return value != other.value; }
|
||||
};
|
||||
|
||||
// 控件声明自己允许通过哪些入口获得逻辑焦点。
|
||||
enum class FocusPolicy : std::uint8_t
|
||||
{
|
||||
None = 0,
|
||||
Programmatic = 1u << 0,
|
||||
Pointer = 1u << 1,
|
||||
Tab = 1u << 2
|
||||
};
|
||||
|
||||
constexpr FocusPolicy operator|(FocusPolicy left, FocusPolicy right) noexcept
|
||||
{
|
||||
return static_cast<FocusPolicy>(static_cast<std::uint8_t>(left) |
|
||||
static_cast<std::uint8_t>(right));
|
||||
}
|
||||
|
||||
constexpr FocusPolicy operator&(FocusPolicy left, FocusPolicy right) noexcept
|
||||
{
|
||||
return static_cast<FocusPolicy>(static_cast<std::uint8_t>(left) &
|
||||
static_cast<std::uint8_t>(right));
|
||||
}
|
||||
|
||||
constexpr bool hasFocusPolicy(FocusPolicy value, FocusPolicy flag) noexcept
|
||||
{
|
||||
return (value & flag) != FocusPolicy::None;
|
||||
}
|
||||
|
||||
enum class FocusReason : std::uint8_t
|
||||
{
|
||||
Programmatic,
|
||||
Pointer,
|
||||
TabTraversal,
|
||||
ScopeActivation,
|
||||
ScopeRestore,
|
||||
InvalidationFallback
|
||||
};
|
||||
|
||||
// FocusManager 的可断言结果;ReentrancyLimit 是独立错误,不依赖日志判断。
|
||||
enum class FocusError : std::uint8_t
|
||||
{
|
||||
None,
|
||||
InvalidTarget,
|
||||
NotFocusable,
|
||||
NotVisible,
|
||||
Disabled,
|
||||
OutsideActiveScope,
|
||||
InvalidScope,
|
||||
ReentrancyLimit
|
||||
};
|
||||
|
||||
struct FocusChangeResult
|
||||
{
|
||||
bool changed = false;
|
||||
FocusError error = FocusError::None;
|
||||
FocusTargetId focused{};
|
||||
};
|
||||
|
||||
/**
|
||||
* 焦点参与协议的瞬时快照。
|
||||
* 所有布尔值只对采集时刻有效;任何用户回调结束后都必须重新解析并拉取新快照。
|
||||
*/
|
||||
struct FocusTargetSnapshot
|
||||
{
|
||||
FocusTargetId id{};
|
||||
FocusTargetId logicalParent{};
|
||||
FocusScopeId scope{};
|
||||
FocusPolicy policy = FocusPolicy::None;
|
||||
bool visible = false;
|
||||
bool enabled = false;
|
||||
bool focusable = false;
|
||||
bool tabStop = false;
|
||||
std::int32_t tabIndex = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @枚举类名称: FillStyle
|
||||
* @功能描述: 用来定义控件填充图案的枚举类
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*******************************************************************************
|
||||
* FocusManager.h - per-Window logical focus manager.
|
||||
******************************************************************************/
|
||||
#pragma once
|
||||
|
||||
#include "CoreTypes.h"
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
class Window;
|
||||
|
||||
namespace StellarX
|
||||
{
|
||||
class FocusManager
|
||||
{
|
||||
struct FocusRequest
|
||||
{
|
||||
FocusTargetId target{};
|
||||
FocusReason reason = FocusReason::Programmatic;
|
||||
};
|
||||
|
||||
struct ScopeEntry
|
||||
{
|
||||
FocusScopeId id{};
|
||||
FocusTargetId root{};
|
||||
FocusTargetId initialFocus{};
|
||||
FocusTargetId restoreFocus{};
|
||||
};
|
||||
|
||||
::Window* window = nullptr;
|
||||
FocusTargetId activeTarget{};
|
||||
// Stable identity of the target whose focus callback is currently running.
|
||||
// pushScope() uses it because activeTarget is intentionally empty during FocusLost.
|
||||
FocusTargetId callbackFocusContext{};
|
||||
bool transitionActive = false;
|
||||
bool pendingPresent = false;
|
||||
FocusRequest pending{};
|
||||
FocusError lastError = FocusError::None;
|
||||
std::vector<ScopeEntry> scopes;
|
||||
std::vector<ControlHandle> invalidatedSubtrees;
|
||||
std::uint64_t nextScopeValue = 2;
|
||||
|
||||
static constexpr std::size_t MaxReentrantRounds = 64;
|
||||
|
||||
bool querySnapshot(FocusTargetId target, FocusTargetSnapshot& snapshot) const;
|
||||
bool isUnderInvalidatedSubtree(FocusTargetId target) const;
|
||||
bool isWithinSubtree(FocusTargetId target, FocusTargetId root) const;
|
||||
bool isWithinActiveScope(FocusTargetId target) const;
|
||||
FocusError validateTarget(FocusTargetId target, FocusReason reason,
|
||||
FocusTargetSnapshot* snapshot = nullptr) const;
|
||||
FocusError validateCurrentTarget(FocusTargetId target,
|
||||
FocusTargetSnapshot* snapshot = nullptr) const;
|
||||
FocusPolicy requiredPolicy(FocusReason reason) const noexcept;
|
||||
FocusChangeResult performTransition(FocusTargetId target, FocusReason reason);
|
||||
FocusTargetId findInvalidationFallback(FocusTargetId invalidTarget) const;
|
||||
std::vector<FocusTargetSnapshot> collectOrderedTargets(bool tabStopsOnly) const;
|
||||
void queueReentrantRequest(FocusTargetId target, FocusReason reason) noexcept;
|
||||
void normalizeInvalidScopes();
|
||||
|
||||
public:
|
||||
explicit FocusManager(::Window& owner);
|
||||
~FocusManager() = default;
|
||||
|
||||
FocusManager(const FocusManager&) = delete;
|
||||
FocusManager& operator=(const FocusManager&) = delete;
|
||||
|
||||
// Normal calls switch immediately. Reentrant calls replace the per-round pending slot.
|
||||
FocusChangeResult requestFocus(FocusTargetId target,
|
||||
FocusReason reason = FocusReason::Programmatic);
|
||||
FocusChangeResult clearFocus(FocusReason reason = FocusReason::Programmatic)
|
||||
{
|
||||
return requestFocus({}, reason);
|
||||
}
|
||||
|
||||
FocusTargetId focusedTarget() const noexcept { return activeTarget; }
|
||||
bool hasFocus(FocusTargetId target) const noexcept
|
||||
{
|
||||
return static_cast<bool>(target) && activeTarget == target;
|
||||
}
|
||||
FocusError getLastError() const noexcept { return lastError; }
|
||||
FocusScopeId rootScope() const noexcept;
|
||||
FocusScopeId activeScope() const noexcept;
|
||||
|
||||
// Valid Tab stops in the active scope, ordered by tabIndex and stable tree order.
|
||||
std::vector<FocusTargetSnapshot> collectTabStops() const;
|
||||
|
||||
// Stage 2 provides scope mechanics; real Dialog integration remains in stage 4.
|
||||
FocusScopeId pushScope(FocusTargetId scopeRoot, FocusTargetId initialFocus = {});
|
||||
bool popScope(FocusScopeId scope);
|
||||
|
||||
// Called when removal is queued, before physical destruction at mutation flush.
|
||||
void notifySubtreePendingRemoval(ControlHandle rootHandle);
|
||||
void notifySubtreesPendingRemoval(const std::vector<ControlHandle>& rootHandles);
|
||||
// Revalidates active focus after visibility, enabled-state, or policy changes.
|
||||
void notifySubtreeStateChanged(ControlHandle rootHandle);
|
||||
// Clears invalidation markers after the outer mutation flush.
|
||||
void onMutationCommitted();
|
||||
};
|
||||
}
|
||||
@@ -30,6 +30,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreTypes.h"
|
||||
#include "FocusManager.h"
|
||||
#include "InputEvents.h"
|
||||
#include "SxLog.h"
|
||||
#include "Control.h"
|
||||
|
||||
@@ -111,6 +111,12 @@ public:
|
||||
bool canCommitManagedPartialRepaint() const override; // 判断当前 TabControl 是否可安全做局部提交
|
||||
void commitManagedRepaint() override; // 托管收口阶段执行 TabControl 的真正重绘
|
||||
protected:
|
||||
bool queryFocusTargetSnapshot(const StellarX::FocusTargetId& target,
|
||||
StellarX::FocusTargetSnapshot& snapshot) const override;
|
||||
void collectFocusTargetSnapshots(
|
||||
std::vector<StellarX::FocusTargetSnapshot>& snapshots) const override;
|
||||
bool applyFocusState(const StellarX::FocusTargetId& target, bool focused,
|
||||
StellarX::FocusReason reason) override;
|
||||
// 将回调期间加入的页签和页内控件在外层事务结束时一次性提交。
|
||||
void flushPendingMutations() override;
|
||||
// 外层统一解算后,TabControl 需要同步刷新其内部页签栏和页面区。
|
||||
|
||||
@@ -186,4 +186,11 @@ public:
|
||||
int getTableWidth() const;
|
||||
// 获取表格当前运行态高度。
|
||||
int getTableHeight() const;
|
||||
protected:
|
||||
bool queryFocusTargetSnapshot(const StellarX::FocusTargetId& target,
|
||||
StellarX::FocusTargetSnapshot& snapshot) const override;
|
||||
void collectFocusTargetSnapshots(
|
||||
std::vector<StellarX::FocusTargetSnapshot>& snapshots) const override;
|
||||
bool applyFocusState(const StellarX::FocusTargetId& target, bool focused,
|
||||
StellarX::FocusReason reason) override;
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Control.h"
|
||||
#include "FocusManager.h"
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
@@ -32,9 +33,15 @@ namespace StellarX::Internal
|
||||
NativeInputRuntimeSnapshot inspectWindowNativeInputForTesting(const ::Window& window) noexcept;
|
||||
}
|
||||
|
||||
namespace StellarX
|
||||
{
|
||||
class FocusManager;
|
||||
}
|
||||
|
||||
class Window
|
||||
{
|
||||
friend class Dialog;
|
||||
friend class StellarX::FocusManager;
|
||||
friend StellarX::Internal::NativeInputRuntimeSnapshot
|
||||
StellarX::Internal::inspectWindowNativeInputForTesting(const Window& window) noexcept;
|
||||
|
||||
@@ -78,6 +85,7 @@ class Window
|
||||
// —— 控件/对话框 ——(容器内的普通控件与非模态对话框)
|
||||
std::vector<std::unique_ptr<Control>> controls; // 普通顶层控件;绘制顺序也决定层级顺序
|
||||
std::vector<std::unique_ptr<Control>> dialogs; // 非模态对话框;始终位于普通控件之上
|
||||
std::unique_ptr<StellarX::FocusManager> focusManager; // Window 内逻辑焦点唯一所有者
|
||||
// 顶层控件的结构变更与事件分发解耦,防止回调直接改动正在遍历的 controls。
|
||||
// add/remove/clear 在事件、绘制和回调期间只入队;最外层 mutation scope 结束后再提交。
|
||||
enum class ControlMutationKind { Add, Remove, Clear };
|
||||
@@ -141,6 +149,9 @@ public:
|
||||
const Control* findControl(StellarX::ControlHandle handle) const;
|
||||
// 返回当前已提交控件的句柄快照;调用方不得据此推断控件之后仍然存在。
|
||||
std::vector<StellarX::ControlHandle> getControlHandles() const;
|
||||
// 获取当前 Window 的通用逻辑焦点管理器。
|
||||
StellarX::FocusManager& getFocusManager();
|
||||
const StellarX::FocusManager& getFocusManager() const;
|
||||
// 添加非模态对话框;Window 接管生命周期并在事件安全点清理关闭对象。
|
||||
void addDialog(std::unique_ptr<Control> dialogs);
|
||||
//非模态对话框去重
|
||||
@@ -184,6 +195,13 @@ public:
|
||||
// 请求下一安全点执行整场景重绘;用于 Dialog 关闭、全局覆盖关系变化等无法安全局部修复的场景。
|
||||
void requestFullSceneRedraw();
|
||||
private:
|
||||
// FocusManager 专用的递归焦点参与协议;不复用只查直接子级的 findControl()。
|
||||
bool queryFocusTargetSnapshot(const StellarX::FocusTargetId& target,
|
||||
StellarX::FocusTargetSnapshot& snapshot) const;
|
||||
void collectFocusTargetSnapshots(
|
||||
std::vector<StellarX::FocusTargetSnapshot>& snapshots) const;
|
||||
bool applyFocusState(const StellarX::FocusTargetId& target, bool focused,
|
||||
StellarX::FocusReason reason);
|
||||
// 提交 Window 顶层控件队列;只允许在最外层事务安全点调用。
|
||||
void flushPendingControlMutations();
|
||||
// 自顶向下提交 Window、Canvas、TabControl 等容器中累积的 pending mutation。
|
||||
|
||||
Reference in New Issue
Block a user