docs: clarify shell input window and host widget lifecycles
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
//! 窗口线程不执行 Endor 业务回调;共享状态通过锁读取,实际 VM 更新由 runtime worker 串行完成。
|
||||
//! 侧栏、页面与开发工具有各自命中区域。拖动捕获必须保留最初的视口目标,不能随指针经过其他组件而跳转。
|
||||
//!
|
||||
//! main 调用 new 后交 winit 事件循环的 run_app 驱动:resumed 建立原生资源,window_event 处理 OS 输入,user_event 接收 worker/工具通知。
|
||||
//! DocumentUpdated 先协调子窗口和一次性窗口命令,再更新焦点令牌;RedrawRequested 才读取完整文档生成显示列表并 present。
|
||||
//! 主窗口 NamespaceState 是 worker 的共享发布入口,子窗口本地状态只是该窗文档投影;所有窗口的 event_client 仍通向同一个权威队列。
|
||||
//! OS WindowId、运行时窗口 resource、VM session、layout_version 和 Mount ID 各有用途,不能只凭屏幕位置或某个数字相同认定同一目标。
|
||||
use std::{
|
||||
num::NonZeroU32,
|
||||
sync::{Arc, RwLock},
|
||||
@@ -207,6 +211,7 @@ impl TesseraApp {
|
||||
submission,
|
||||
..NamespaceState::default()
|
||||
}));
|
||||
// 新建的是宿主窗口状态,共享活动日志/控制/输入客户端;不会在此为副窗口建立另一套解释器。
|
||||
let mut child = Self::with_event_client(
|
||||
namespace,
|
||||
self.activity.clone(),
|
||||
@@ -264,11 +269,13 @@ impl TesseraApp {
|
||||
.and_then(|child| child.window.as_ref())
|
||||
};
|
||||
let Some(window) = target else {
|
||||
// 该批已一次性消费,没有 OS 句柄的目标不会留到以后按同编号重试,避免影响后来创建的资源。
|
||||
continue;
|
||||
};
|
||||
// 尺寸使用逻辑单位,桌面位置使用物理屏幕坐标;这些 OS 操作都在原生事件线程执行。
|
||||
match envelope.command.action {
|
||||
crate::window::Action::Resize { width, height } => {
|
||||
// 向窗口管理器发逻辑尺寸请求,返回值不当作最终 viewport;批结束后重新读取真实窗口大小。
|
||||
let _ = window.request_inner_size(LogicalSize::new(width, height));
|
||||
}
|
||||
crate::window::Action::SetTitle(title) => window.set_title(&title),
|
||||
@@ -389,6 +396,7 @@ impl TesseraApp {
|
||||
// 先使原生资源对应新文档,再执行其窗口命令;随后重建焦点令牌,避免沿用旧布局身份。
|
||||
self.reconcile_windows(event_loop);
|
||||
self.apply_window_commands();
|
||||
// 重新读取的是资源/命令同步后的完整本窗投影,焦点和工具标记来自这份发布 Frame,而非 GUI 自行猜测。
|
||||
if let Ok(namespace) = self.namespace.read() {
|
||||
self.developer_tools.document_updated(&namespace.document);
|
||||
let frame = &namespace.document.frame;
|
||||
@@ -448,6 +456,7 @@ impl TesseraApp {
|
||||
{
|
||||
self.space_down = event.state == ElementState::Pressed;
|
||||
if !self.space_down {
|
||||
// Space 释放结束正在进行的整页/局部平移,不能因漏掉鼠标释放而一直把后续移动当作拖动。
|
||||
self.panning = false;
|
||||
}
|
||||
return;
|
||||
@@ -469,6 +478,7 @@ impl TesseraApp {
|
||||
Key::Named(NamedKey::Enter | NamedKey::Space)
|
||||
) {
|
||||
if event.state == ElementState::Pressed {
|
||||
// 侧栏只保存按下捕获,真正动作等下面对应释放确认;不在按下和抬起各激活一次。
|
||||
self.sidebar.interaction.key_press(&self.sidebar.targets);
|
||||
} else if let Some(action) =
|
||||
self.sidebar.interaction.key_release(&self.sidebar.targets)
|
||||
@@ -479,6 +489,7 @@ impl TesseraApp {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 新页快捷键在侧栏键盘路由之后处理;只首次按下发一个工具请求,系统重复不创建多页。
|
||||
if event.state == ElementState::Pressed
|
||||
&& !event.repeat
|
||||
&& (self.modifiers.super_key() || self.modifiers.control_key())
|
||||
@@ -494,6 +505,7 @@ impl TesseraApp {
|
||||
&& matches!(&event.logical_key, Key::Character(key) if key.eq_ignore_ascii_case("w"));
|
||||
let cycle = self.modifiers.control_key()
|
||||
&& matches!(event.logical_key, Key::Named(NamedKey::Tab));
|
||||
// 只有确有关闭/切页动作且能构造当前页事件才消费按键,否则继续页面与工具的普通键盘处理。
|
||||
if (close || cycle) && self.send_tab_shortcut(close) {
|
||||
return;
|
||||
}
|
||||
@@ -528,6 +540,7 @@ impl TesseraApp {
|
||||
let other_shortcut = self.modifiers.control_key() && self.modifiers.shift_key() && is_i;
|
||||
if is_f12 || mac_shortcut || other_shortcut {
|
||||
if !event.repeat {
|
||||
// 快捷键只请求打开 Console;实际工具 Page 由所属 VM 创建/激活,窗口线程不直接分配托管对象。
|
||||
self.open_developer_tool("Console");
|
||||
}
|
||||
return;
|
||||
@@ -627,6 +640,7 @@ impl TesseraApp {
|
||||
let current = self.namespace.read().ok()?;
|
||||
let frame = ¤t.document.frame;
|
||||
if frame.session == 0 {
|
||||
// reset/失活后仍可显示旧静态文档,但没有活 VM 身份时不能从它构造可执行点击令牌。
|
||||
return None;
|
||||
}
|
||||
// 命中的是当前发布帧,事件同时携带 session/layout_version;worker 可拒绝排队期间已过期的输入。
|
||||
@@ -651,6 +665,7 @@ impl TesseraApp {
|
||||
/// 左键捕获和悬停移动共用,从粗命中出现向祖先寻找 pointer 订阅,不把所有交互目标都当作连续指针接收者。
|
||||
fn pointer_target(&self) -> Option<crate::events::Input> {
|
||||
let mut input = self.guest_hit()?;
|
||||
// 这里只沿本窗最新出现树找订阅祖先,保留命中令牌的窗口/会话/版本;执行端仍需再次核验。
|
||||
let current = self.namespace.read().ok()?;
|
||||
let frame = ¤t.document.frame;
|
||||
let mut mount = frame.mounts.iter().find(|m| m.id == input.mount)?;
|
||||
@@ -679,6 +694,7 @@ impl TesseraApp {
|
||||
self.emit_pointer(input, "pointercancel");
|
||||
}
|
||||
if let Some(input) = self.pointer_hover.take() {
|
||||
// 悬停与按下捕获是两份状态,取消捕获后仍须单独告知原悬停目标离开;take 保证重复清理不重发。
|
||||
self.emit_pointer(input, "pointerleave");
|
||||
}
|
||||
}
|
||||
@@ -710,6 +726,7 @@ impl TesseraApp {
|
||||
let input = self.namespace.read().ok().and_then(|namespace| {
|
||||
let frame = &namespace.document.frame;
|
||||
if frame.session == 0 || frame.fault.is_some() || frame.tabs.is_empty() {
|
||||
// 没有活会话、帧已故障或标签列表为空时都不能生成页动作,尤其不能对空列表取模。
|
||||
return None;
|
||||
}
|
||||
// 无显式 active 时从第一项开始;tabs 已检查非空,后续取模和前向环绕不会除零或下溢。
|
||||
@@ -750,6 +767,7 @@ impl TesseraApp {
|
||||
// 侧栏鼠标、键盘以及命令+B 调用,UI 侧只调整宿主开合,业务页面操作仍发带版本事件。
|
||||
fn sidebar_action(&mut self, action: crate::sidebar::Action) {
|
||||
use crate::sidebar::Action;
|
||||
// Toggle 属于宿主布局状态;页面激活/关闭与工具入口则转换成 worker 请求,两类动作不能混成直接 VM 调用。
|
||||
match action {
|
||||
Action::Toggle => {
|
||||
self.sidebar.open = !self.sidebar.open;
|
||||
@@ -801,6 +819,7 @@ impl TesseraApp {
|
||||
let Some(point) = self.guest_point() else {
|
||||
return false;
|
||||
};
|
||||
// 只借已发布帧选择局部相机,锁外不保留 Mount 引用;pan_viewport 保存的是纯数值出现身份。
|
||||
let Ok(current) = self.namespace.read() else {
|
||||
return false;
|
||||
};
|
||||
@@ -817,6 +836,7 @@ impl TesseraApp {
|
||||
if !self.panning && self.guest_covered(self.pointer) {
|
||||
return;
|
||||
}
|
||||
// 此处先得到页面视图坐标,下面闭包才用当前 Surface.to_world 变成页面世界点;不把侧栏偏移带入相机空间。
|
||||
let point = Point::new(self.pointer.x - self.sidebar.left(), self.pointer.y);
|
||||
let input = self.namespace.read().ok().and_then(|current| {
|
||||
let frame = ¤t.document.frame;
|
||||
@@ -859,6 +879,7 @@ impl TesseraApp {
|
||||
layout_version: frame.layout_version,
|
||||
mount: viewport.id,
|
||||
kind: if viewport.scene3d_viewport {
|
||||
// 同一入口按目标能力选择 scene-zoom/orbit/pan,二维 viewport 不使用三维轨道语义。
|
||||
if kind == "page-zoom" {
|
||||
"scene-zoom"
|
||||
} else if self.orbiting {
|
||||
@@ -933,9 +954,11 @@ impl TesseraApp {
|
||||
// 首次 Tab 按下选下一焦点出现,抬起或系统重复只根据已有 guest_focus 决定是否消费。
|
||||
if *key == NamedKey::Tab {
|
||||
if event.state == ElementState::Pressed && !event.repeat {
|
||||
// 每次焦点移动读取本窗发布候选,不在上一次保存的几何里按索引盲目选择下一项。
|
||||
let next = self.namespace.read().ok().and_then(|namespace| {
|
||||
let frame = &namespace.document.frame;
|
||||
if frame.session == 0 || frame.fault.is_some() {
|
||||
// 静态保留画面或故障域不能响应焦点移动,返回 None 让调用方按未消费处理。
|
||||
return None;
|
||||
}
|
||||
// 按发布帧中的挂载顺序遍历可交互目标;Shift+Tab 逆向环绕,尚无焦点时从首/末项进入。
|
||||
@@ -954,6 +977,7 @@ impl TesseraApp {
|
||||
.as_ref()
|
||||
.and_then(|focus| targets.iter().position(|mount| mount.id == focus.mount));
|
||||
let index = if self.modifiers.shift_key() {
|
||||
// 逆序首次进入取末项,已有焦点则向前环绕;正序分支首次进入取首项。
|
||||
current
|
||||
.map(|i| (i + targets.len() - 1) % targets.len())
|
||||
.unwrap_or(targets.len() - 1)
|
||||
@@ -988,6 +1012,7 @@ impl TesseraApp {
|
||||
if matches!(key, NamedKey::Enter | NamedKey::Space) && self.guest_focus.is_some() {
|
||||
if event.state == ElementState::Pressed {
|
||||
if !event.repeat {
|
||||
// 保存首次按下时的键名与版本令牌,系统连发不能覆盖捕获后在释放时制造额外点击。
|
||||
self.guest_key = self.guest_focus.clone().map(|focus| (*key, focus));
|
||||
}
|
||||
// 释放会取走捕获,保证不会重复激活;仅与原来按下的 Enter/Space 相同才转换成 click。
|
||||
@@ -1040,6 +1065,7 @@ impl TesseraApp {
|
||||
let window = Arc::new(event_loop.create_window(attributes)?);
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::native_chrome::align_controls(&window);
|
||||
// Context 与 Surface 都持 Window Arc;这两个局部资源创建成功后才写入 self,失败不会留下半套可重绘字段。
|
||||
let context = Context::new(window.clone()).map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||
let surface =
|
||||
Surface::new(&context, window.clone()).map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||
@@ -1096,10 +1122,12 @@ impl TesseraApp {
|
||||
match self.try_redraw() {
|
||||
Ok(()) => {
|
||||
if self.window.is_some() && self.surface.is_some() {
|
||||
// try_redraw 无原生资源时也可返回 Ok;这里只在资源齐全时清上一次错误,不据此重复发送文档通知。
|
||||
self.native_error = None;
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
// 保留窗口资源编号定位错误,文本相同的连续失败不反复投递 RuntimeFailed 刷屏。
|
||||
let message = format!("Window {} redraw failed: {error:#}", self.resource);
|
||||
if self.native_error.as_ref() != Some(&message) {
|
||||
self.native_error = Some(message.clone());
|
||||
@@ -1132,6 +1160,7 @@ impl TesseraApp {
|
||||
};
|
||||
|
||||
let Ok(namespace) = self.namespace.read() else {
|
||||
// 取不到可靠发布文档时跳过本次重绘,不用局部旧字段拼一份不一致场景,也不把锁故障转换成 VM 调用。
|
||||
return Ok(());
|
||||
};
|
||||
// 读锁保护一次完整文档取样与场景生成;生成后释放锁,像素写入和 present 不再占用 namespace。
|
||||
@@ -1198,6 +1227,7 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
match self.control.wait_shutdown(Duration::from_secs(5)) {
|
||||
Some(outcome) => {
|
||||
if let Some(error) = outcome.error {
|
||||
// 完成通知只证明 worker 已结束清理,仍须保留其错误供 main 的 exit_result 报告。
|
||||
self.native_error = Some(error);
|
||||
}
|
||||
}
|
||||
@@ -1245,6 +1275,7 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
match event {
|
||||
AppEvent::ShutdownComplete { error } => {
|
||||
if let Some(error) = error {
|
||||
// 与 exiting 的最终等待复用同一错误结果;接收完成事件后才结束 winit 循环,不在此重新清理 VM。
|
||||
self.native_error = Some(error);
|
||||
}
|
||||
event_loop.exit();
|
||||
@@ -1307,6 +1338,7 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
if self.resource == crate::window::MAIN_WINDOW {
|
||||
self.request_exit();
|
||||
} else {
|
||||
// 副窗口关闭携带当前本地发布投影的身份,worker 会对照主 Namespace 的权威资源再决定源关闭或故障退役。
|
||||
let input = self
|
||||
.namespace
|
||||
.read()
|
||||
@@ -1324,6 +1356,7 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
screen_position: None,
|
||||
});
|
||||
if let Some(input) = input {
|
||||
// 只是尝试入队,不立即删除 OS 窗口;实际资源消失由后续文档协调或 Close 命令反映。
|
||||
self.send_guest(input);
|
||||
}
|
||||
}
|
||||
@@ -1362,6 +1395,7 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
// 3D 相机拖动超过世界坐标下的 3 单位阈值后转为 orbit,并清除 click 捕获,避免释放时误激活。
|
||||
if is_3d && (point.x - start[0]).hypot(point.y - start[1]) > 3. {
|
||||
self.orbiting = true;
|
||||
// 超阈值后才开始相机手势,begin_pan 按当前指针位置选相机;普通点击捕获随后丢弃。
|
||||
self.panning = self.begin_pan();
|
||||
self.guest_pressed = None;
|
||||
}
|
||||
@@ -1451,6 +1485,7 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
{
|
||||
#[cfg(target_os = "macos")]
|
||||
if crate::native_chrome::titlebar_double_click(&window) {
|
||||
// macOS 原生双击标题栏已消费本次动作,不再叠加一次系统窗口拖动。
|
||||
return;
|
||||
}
|
||||
let _ = window.drag_window();
|
||||
@@ -1537,6 +1572,7 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
|| point.is_some_and(|point| n.document.frame.camera_at(point).is_some())
|
||||
});
|
||||
if canvas && (self.modifiers.super_key() || self.modifiers.control_key()) {
|
||||
// 命令+滚轮优先转换为缩放倍率,钳到事件协议范围;返回后不会再把同一次滚轮当内容滚动。
|
||||
self.page_view("page-zoom", [(dy * 0.005).exp().clamp(0.1, 10.0), 0.0]);
|
||||
window.request_redraw();
|
||||
return;
|
||||
@@ -1547,11 +1583,13 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
.and_then(|point| current.document.frame.camera_at(point))
|
||||
.is_some_and(|mount| mount.scene3d_viewport)
|
||||
});
|
||||
// 此处重新取得页面点用于滚动目标,后面 Input 带该点与反缩放后的增量,由 worker 再做几何/能力复核。
|
||||
let point = self.guest_point();
|
||||
let input = point.and_then(|point| {
|
||||
self.namespace.read().ok().and_then(|namespace| {
|
||||
let frame = &namespace.document.frame;
|
||||
if frame.session == 0 || frame.fault.is_some() {
|
||||
// 无活会话或故障帧只保留显示,不能据其旧 ScrollBlock 几何继续产生可执行滚动。
|
||||
return None;
|
||||
}
|
||||
// 滚动方向取反并撤销整页 zoom,wheel_target 只选至少一轴能移动的最近祖先容器。
|
||||
@@ -1584,6 +1622,7 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
self.page_view(
|
||||
"page-pan",
|
||||
if horizontal && dx.abs() < 0.1 {
|
||||
// 仅几乎没有横向输入时把纵向量映到页面横轴,已有明确横向量则走下面保留双轴的分支。
|
||||
[-dy, 0.0]
|
||||
} else {
|
||||
[-dx, -dy]
|
||||
@@ -1603,6 +1642,7 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
WindowEvent::Ime(Ime::Commit(value)) => {
|
||||
self.developer_tools.preedit.clear();
|
||||
if self.developer_tools.open {
|
||||
// 已确认的组合文本进入工具当前焦点编辑器,既不作为原生 KeyEvent 重放,也不自动提交到 Console 执行。
|
||||
self.developer_tools.insert(&value);
|
||||
}
|
||||
window.request_redraw();
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
//! 仅在 macOS 编译;Objective-C 交互局限在原生窗口层,不参与 Endor 布局与事件分发。
|
||||
//!
|
||||
//! Keep AppKit's real window buttons aligned with the custom tab strip.
|
||||
//! app::create_native/redraw 负责在事件循环线程调用对齐,app::window_event 的标题空白区按下路径调用双击处理。
|
||||
//! 两个入口都只临时借用 winit 持有的 NSView/NSWindow;本模块不保存原生指针,也不向 VM 发送伪造标题栏事件。
|
||||
use objc2_app_kit::{NSApplication, NSView, NSWindowButton, NSWindowStyleMask};
|
||||
use objc2_foundation::{MainThreadMarker, NSPoint, NSString, NSUserDefaults};
|
||||
use winit::{
|
||||
@@ -19,9 +21,11 @@ pub fn titlebar_double_click(window: &Window) -> bool {
|
||||
return false;
|
||||
};
|
||||
let Ok(handle) = window.window_handle() else {
|
||||
// 借用句柄失败表示当前无法可靠访问原生视图,不把它当成一次已处理的双击吞掉。
|
||||
return false;
|
||||
};
|
||||
let RawWindowHandle::AppKit(handle) = handle.as_raw() else {
|
||||
// 只允许 AppKit 句柄进入下面 NSView 转型,其他平台句柄不具有这些 Objective-C 对象前提。
|
||||
return false;
|
||||
};
|
||||
// SAFETY: called on the event-loop thread during a left-button press, with
|
||||
@@ -74,12 +78,15 @@ pub fn align_controls(window: &Window) {
|
||||
// SAFETY: winit owns this NSView for the lifetime of the borrowed Window.
|
||||
// All AppKit access is on the event-loop thread; no native pointers escape.
|
||||
unsafe {
|
||||
// 安全前提由调用方保证:事件循环主线程、仍活着的 Window 借用,以及 winit 提供的有效 NSView;这里只短期访问。
|
||||
let view = &*handle.ns_view.as_ptr().cast::<NSView>();
|
||||
let Some(native) = view.window() else {
|
||||
// 原生视图尚未关联窗口时无需移动控制按钮,不创建替代 NSWindow 来补齐。
|
||||
return;
|
||||
};
|
||||
// Fullscreen traffic lights belong to AppKit's separate floating toolbar.
|
||||
if native.styleMask().contains(NSWindowStyleMask::FullScreen) {
|
||||
// 全屏按钮由 AppKit 独立工具栏管理,内容视图坐标不适合作为它们的位置来源。
|
||||
return;
|
||||
}
|
||||
// 读取内容视图边界,用与侧栏相同的 HEADER 高度确定标题行中心。
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//! 用统一主题绘制宿主管理控件,并同步注册交互目标。
|
||||
//! 每个控件的图形、角色、启用状态和 action 一起登记,绘制几何与命中几何保持一致。
|
||||
//!
|
||||
//! sidebar 和 developer_tools 的 Painter 为每帧创建 Ui,消费其显示列表与 targets,再由外层 Interaction 处理输入。
|
||||
//! 这里的 Button 是宿主绘制描述,不是 Endor 的托管 Button;register 只登记 Action,不调用业务回调或立即执行动作。
|
||||
//! 主题与 Interaction 是本轮输入快照;控件重绘不会自行改变焦点、按下捕获或滚动偏移,跨帧状态由外层事件处理维护。
|
||||
use super::{
|
||||
Axis, Interaction, Role, Target, VisualState, WidgetId,
|
||||
theme::{self, Theme},
|
||||
@@ -34,16 +37,19 @@ impl<'a> Button<'a> {
|
||||
}
|
||||
/// 按 builder 风格设置选中状态,返回描述供链式调用。
|
||||
pub fn selected(mut self, value: bool) -> Self {
|
||||
// 侧栏/工具构建链传入本轮选择结果,只改这个短期描述;Ui::button 稍后合成视觉状态,不在这里切换页面。
|
||||
self.selected = value;
|
||||
self
|
||||
}
|
||||
/// 设置启用状态,影响外观和目标的可激活资格。
|
||||
pub fn enabled(mut self, value: bool) -> Self {
|
||||
// 调用方按业务资格选择是否启用,后面登记到 Target.enabled;不删除动作身份,便于跨帧 reconcile 清理旧捕获。
|
||||
self.enabled = value;
|
||||
self
|
||||
}
|
||||
/// 设置控件语义角色,影响焦点资格、光标及 Tab 底线等外观。
|
||||
pub fn role(mut self, value: Role) -> Self {
|
||||
// Painter 可为同一按钮原语选择 Tab/Toggle 等角色,后续登记与绘制共用这一描述,不另建一份命中控件。
|
||||
self.role = value;
|
||||
self
|
||||
}
|
||||
@@ -210,6 +216,7 @@ impl<A: Hash> Ui<A> {
|
||||
/// 登记文本输入角色并绘制背景和焦点/悬停描边,不负责实际文本与光标。
|
||||
// Network Filter 和 Settings 自定义颜色框使用,仅负责框的目标/外观,实际文字与编辑缓冲由工具状态维护。
|
||||
pub fn text_field(&mut self, rect: Rect, action: A, focused: bool) {
|
||||
// Action 的稳定哈希同时关联本帧文本框目标和视觉状态;focused 还允许外层真实编辑焦点提供强调外观。
|
||||
let s = self.register(
|
||||
WidgetId::new(&action),
|
||||
rect,
|
||||
@@ -250,6 +257,7 @@ impl<A: Hash> Ui<A> {
|
||||
// 行背景可绘制完整行,但 hit 可裁到可见部分;不要把裁剪区外的行注册成可点击区域。
|
||||
// Painter 为请求、AST 等有动作的显示行调用,paint 可是完整行,hit 只允许可见片段。
|
||||
pub fn row(&mut self, paint: Rect, hit: Rect, action: A, selected: bool) {
|
||||
// 目标登记使用裁剪后的 hit,背景使用 paint;源行可以部分可见,不能用完整 paint 扩大鼠标可点击区域。
|
||||
let s = self.register(
|
||||
WidgetId::new(&action),
|
||||
hit,
|
||||
@@ -263,6 +271,7 @@ impl<A: Hash> Ui<A> {
|
||||
/// 登记色块目标,绘制按压变暗、外圈及选中勾号。
|
||||
// Settings 调色板调用,颜色来自候选色,selected 来自当前强调色比较;点击只登记主题 Action。
|
||||
pub fn swatch(&mut self, rect: Rect, action: A, color: Color, selected: bool) {
|
||||
// 色块 Action 由 Settings 消费,控件只登记/画色;selected 是传入比较结果,不在绘制时改当前主题。
|
||||
let s = self.register(
|
||||
WidgetId::new(&action),
|
||||
rect,
|
||||
@@ -334,6 +343,7 @@ impl<A: Hash> Ui<A> {
|
||||
/// 登记滚动手柄目标并绘制其状态颜色,偏移和拖动倍率由外部维护。
|
||||
// Painter 在内容裁剪弹出后调用,只有手柄几何和轴进入 Ui,内容偏移及倍率仍由外层状态管理。
|
||||
pub fn scrollbar(&mut self, rect: Rect, id: WidgetId, axis: Axis) {
|
||||
// 无点击 Action,外层用 Role::Scrollbar 和显式 WidgetId 选择拖动状态;Ui 不拥有内容长度或偏移计算。
|
||||
let mut s = self.register(id, rect, Role::Scrollbar(axis), None, false, true);
|
||||
// 拖动捕获时即使指针移到手柄外仍保持强调色,不能套用普通按钮拖出后取消按下外观的规则。
|
||||
s.pressed |= self.interaction.captured(id);
|
||||
|
||||
Reference in New Issue
Block a user