Files
endorphin-tessera-gui/src/shell/app.rs
T

1476 lines
62 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! winit 原生应用生命周期和窗口事件入口。这里创建窗口、发布逻辑视口、转发输入,并把不可变文档画到原生表面。
//! 窗口线程不执行 Endor 业务回调;共享状态通过锁读取,实际 VM 更新由 runtime worker 串行完成。
//! 侧栏、页面与开发工具有各自命中区域。拖动捕获必须保留最初的视口目标,不能随指针经过其他组件而跳转。
//!
use std::{
num::NonZeroU32,
sync::{Arc, RwLock},
time::Duration,
};
use softbuffer::{Context, Surface};
use winit::{
application::ApplicationHandler,
dpi::{LogicalPosition, LogicalSize, PhysicalPosition},
event::{ElementState, Ime, MouseButton, MouseScrollDelta, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy},
keyboard::{Key, ModifiersState, NamedKey},
window::{Window, WindowAttributes, WindowId},
};
use crate::{
activity::Activity,
developer_tools::Workspace as DeveloperToolsState,
document::NamespaceState,
geometry::Point,
scene::{self, SceneLayout},
software_renderer::SoftwareRenderer,
};
#[derive(Clone, Debug)]
/// worker 到原生事件循环的通知,携带更新/错误/退出状态而不是可执行 VM 引用。
pub enum AppEvent {
RuntimeFailed { message: String },
DocumentUpdated,
ShutdownComplete { error: Option<String> },
ConsoleFailed { message: String, source: String },
}
/// 一个原生窗口的宿主状态;主窗口还协调子窗口资源,共用同一运行时与输入通道。
pub struct TesseraApp {
// 运行时窗口资源编号与 winit WindowId 分属不同命名空间,输入携带前者,OS 事件匹配后者。
resource: u64,
control: crate::api::RuntimeControl,
exit_requested: bool,
// 记录当前原生窗口所对应的运行时世代,避免重启后继续显示已失效的子窗口。
native_session: u64,
startup_page_requested: bool,
native_error: Option<String>,
// 只有主窗口维护此表;子窗口有独立文档/绘制状态,但复用运行时控制与输入队列。
children: std::collections::BTreeMap<u64, TesseraApp>,
proxy: EventLoopProxy<AppEvent>,
// 窗口、softbuffer 上下文与表面延迟创建;Arc 让表面持有有效的原生窗口句柄。
window: Option<Arc<Window>>,
context: Option<Context<Arc<Window>>>,
surface: Option<Surface<Arc<Window>, Arc<Window>>>,
namespace: Arc<RwLock<NamespaceState>>,
developer_tools: DeveloperToolsState,
// 最近一次绘制生成的宿主控件几何,用于下一次输入命中、光标和 IME 定位。
layout: SceneLayout,
pointer: Point,
sidebar: crate::sidebar::State,
sidebar_keyboard: bool,
panning: bool,
// 拖动开始时捕获视口,None 也可能表示拖动整张 Canvas,而非手势未开始。
pan_viewport: Option<u64>,
orbiting: bool,
space_down: bool,
modifiers: ModifiersState,
activity: Activity,
event_client: crate::events::Client,
// 保存按下时的版本与位置;释放时需确认仍处在同一会话与布局,防止旧画面误点击。
guest_pressed: Option<crate::events::Input>,
pointer_hover: Option<crate::events::Input>,
pointer_capture: Option<crate::events::Input>,
guest_focus: Option<crate::events::Input>,
// Enter/Space 的按下捕获;只在对应按键释放时发出一次 click,忽略系统连发。
guest_key: Option<(NamedKey, crate::events::Input)>,
guest_keyboard: bool,
}
impl TesseraApp {
/// 先建立连接权威 namespace 的有界事件客户端,再初始化主窗口状态。
/// 客户端创建失败向调用方返回错误;原生窗口和像素表面延迟到 resumed 创建。
pub fn new(
namespace: Arc<RwLock<NamespaceState>>,
activity: Activity,
proxy: EventLoopProxy<AppEvent>,
control: crate::api::RuntimeControl,
) -> anyhow::Result<Self> {
let event_client =
crate::events::Client::new(proxy.clone(), namespace.clone(), control.clone())?;
Ok(Self::with_event_client(
namespace,
activity,
proxy,
control,
event_client,
))
}
/// 建立窗口宿主状态并复用权威输入客户端,避免子窗口使用滞后的独立事件队列。
fn with_event_client(
namespace: Arc<RwLock<NamespaceState>>,
activity: Activity,
proxy: EventLoopProxy<AppEvent>,
control: crate::api::RuntimeControl,
event_client: crate::events::Client,
) -> Self {
Self {
resource: crate::window::MAIN_WINDOW,
control,
exit_requested: false,
native_session: 0,
startup_page_requested: false,
native_error: None,
children: std::collections::BTreeMap::new(),
proxy: proxy.clone(),
window: None,
context: None,
surface: None,
namespace,
developer_tools: DeveloperToolsState::new(proxy),
layout: SceneLayout::default(),
pointer: Point::default(),
sidebar: crate::sidebar::State::default(),
sidebar_keyboard: false,
panning: false,
pan_viewport: None,
orbiting: false,
space_down: false,
modifiers: ModifiersState::default(),
activity,
event_client,
guest_pressed: None,
pointer_hover: None,
pointer_capture: None,
guest_focus: None,
guest_key: None,
guest_keyboard: true,
}
}
/// 将已发布窗口文档与原生资源对齐;session 更换时丢弃旧域的子窗口。
fn reconcile_windows(&mut self, event_loop: &ActiveEventLoop) {
if self.resource != crate::window::MAIN_WINDOW {
return;
}
let Ok(namespace) = self.namespace.read() else {
return;
};
let session = namespace.runtime_session;
let revision = namespace.revision;
let submission = namespace.submission;
// 先复制已发布的子窗口投影并释放读锁,再创建/销毁 OS 资源;原生调用不会占着发布锁。
let documents = namespace.document.windows.clone();
drop(namespace);
if session != self.native_session {
// 控制台 reset 可以保留旧投影但禁用输入;真正替换运行时后,
// 新文档带新 session,此时销毁旧原生视图,防止跨世代复用。
let replaces = documents
.values()
.any(|document| document.frame.session == session && session != 0);
if replaces || (session != 0 && documents.is_empty()) {
self.children.clear();
}
self.native_session = session;
}
self.children.retain(|id, child| {
documents.get(id).is_some_and(|document| {
document.frame.session == 0 || child.native_session == document.frame.session
})
});
for (id, document) in documents {
if let Some(child) = self.children.get_mut(&id) {
if let Ok(mut namespace) = child.namespace.write() {
namespace.document = document;
namespace.runtime_session = session;
namespace.revision = revision;
namespace.submission = submission;
}
child.document_updated(event_loop);
} else {
let namespace = Arc::new(RwLock::new(NamespaceState {
document,
runtime_session: session,
revision,
submission,
..NamespaceState::default()
}));
let mut child = Self::with_event_client(
namespace,
self.activity.clone(),
self.proxy.clone(),
self.control.clone(),
self.event_client.clone(),
);
child.resource = id;
child.native_session = session;
child.resumed(event_loop);
if let Some(error) = &child.native_error {
self.developer_tools.input_error = error.clone();
self.developer_tools.open = true;
}
child.document_updated(event_loop);
self.children.insert(id, child);
}
}
}
/// 只应用当前运行时身份的窗口命令;执行前从共享队列取走,避免重复执行。
fn apply_window_commands(&mut self) {
let Ok(mut namespace) = self.namespace.write() else {
return;
};
let session = namespace.runtime_session;
// 取走整批命令即表示消费;随后释放写锁,执行窗口 API 时不阻塞 worker 的文档发布。
let commands = std::mem::take(&mut namespace.window_commands);
drop(namespace);
for envelope in commands {
if envelope.session == 0 || envelope.session != session {
continue;
}
let id = envelope.command.window;
if matches!(envelope.command.action, crate::window::Action::Close) {
if id == crate::window::MAIN_WINDOW {
self.request_exit();
} else {
self.children.remove(&id);
}
continue;
}
let target = if id == self.resource {
self.window.as_ref()
} else {
self.children
.get(&id)
.and_then(|child| child.window.as_ref())
};
let Some(window) = target else {
continue;
};
match envelope.command.action {
crate::window::Action::Resize { width, height } => {
let _ = window.request_inner_size(LogicalSize::new(width, height));
}
crate::window::Action::SetTitle(title) => window.set_title(&title),
crate::window::Action::MoveTo { x, y } => {
if window.outer_position().is_ok() {
window.set_outer_position(PhysicalPosition::new(x, y));
} else {
let _ = self.proxy.send_event(AppEvent::RuntimeFailed {
message: format!(
"Window {id}: desktop placement is unsupported on this platform"
),
});
}
}
crate::window::Action::Focus => window.focus_window(),
crate::window::Action::Close => {}
}
}
self.publish_viewport();
}
/// 将真实原生窗口尺寸换算成扣除侧栏的逻辑页面尺寸,并发布给 worker。
fn publish_viewport(&self) {
let Some(window) = &self.window else {
return;
};
let size = window.inner_size().to_logical::<f32>(window.scale_factor());
if size.width <= 0.0
|| size.height <= 0.0
|| !size.width.is_finite()
|| !size.height.is_finite()
{
return;
}
if let Ok(mut namespace) = self.namespace.write() {
let size = crate::layout::Size {
width: (size.width - self.sidebar.left())
.max(0.0)
.min(crate::ui_bridge::MAX_DIMENSION as f32),
height: size.height.min(crate::ui_bridge::MAX_DIMENSION as f32),
};
namespace.viewport = Some(size);
namespace.window_viewports.insert(self.resource, size);
if self.resource == crate::window::MAIN_WINDOW {
let session = namespace.runtime_session;
// 主窗口维护 worker 可见的视口总表:先移除旧世代/已关闭子窗口,再复制当前有效子窗口的逻辑尺寸。
namespace.window_viewports.retain(|id, _| {
*id == crate::window::MAIN_WINDOW
|| self
.children
.get(id)
.is_some_and(|child| session != 0 && child.native_session == session)
});
for (id, child) in &self.children {
if child.native_session == session
&& session != 0
&& let Ok(local) = child.namespace.read()
&& local.document.frame.session == session
&& let Some(size) = local.viewport
{
namespace.window_viewports.insert(*id, size);
}
}
}
}
}
/// 仅请求原生事件循环安排重绘,不在调用处立即递归绘制。
fn request_redraw(&self) {
if let Some(window) = &self.window {
window.request_redraw();
}
}
/// 通过托管开发工具入口打开指定工具页面,保留对应窗口上下文。
fn open_developer_tool(&mut self, name: &str) {
let input = self.namespace.read().ok().map(|namespace| {
let frame = &namespace.document.frame;
crate::events::Input {
window: self.resource,
session: frame.session,
layout_version: frame.layout_version,
mount: 0,
kind: format!("developer-open:{name}"),
point: None,
delta: None,
press_point: None,
tab_drop: None,
screen_position: None,
}
});
if let Some(input) = input {
if input.session == 0 {
self.developer_tools.initialize_tools(name);
} else {
self.send_guest(input);
}
}
self.guest_keyboard = false;
self.request_redraw();
}
/// 接收完整文档更新,协调子窗口和原生命令,再更新输入焦点并请求重绘。
fn document_updated(&mut self, event_loop: &ActiveEventLoop) {
// 先使原生资源对应新文档,再执行其窗口命令;随后重建焦点令牌,避免沿用旧布局身份。
self.reconcile_windows(event_loop);
self.apply_window_commands();
if let Ok(namespace) = self.namespace.read() {
self.developer_tools.document_updated(&namespace.document);
let frame = &namespace.document.frame;
self.guest_keyboard = !frame
.tabs
.iter()
.any(|tab| tab.active && tab.developer_tool.is_some());
self.guest_focus =
frame
.focus
.filter(|_| frame.fault.is_none())
.map(|mount| crate::events::Input {
window: self.resource,
session: frame.session,
layout_version: frame.layout_version,
mount,
kind: "focus".into(),
point: None,
delta: None,
press_point: None,
tab_drop: None,
screen_position: None,
});
}
self.request_redraw();
}
/// 先处理宿主管理输入、选择与快捷键,再按焦点决定是否交给页面。
fn handle_keyboard(&mut self, event: &winit::event::KeyEvent) {
// 宿主快捷键优先级最高;后续依次交给侧栏、页面交互、开发工具按钮及文本编辑。
let command = self.modifiers.super_key() || self.modifiers.control_key();
if command
&& event.state == ElementState::Pressed
&& !event.repeat
&& matches!(&event.logical_key, Key::Character(key) if key.eq_ignore_ascii_case("b"))
{
self.sidebar_action(crate::sidebar::Action::Toggle);
return;
}
if event.logical_key == Key::Named(NamedKey::Escape) {
self.panning = false;
self.space_down = false;
self.guest_pressed = None;
}
if event.logical_key == Key::Named(NamedKey::Space)
&& self.guest_focus.is_none()
&& self
.namespace
.read()
.is_ok_and(|n| n.document.frame.surface.mode == crate::page::Mode::Canvas)
{
self.space_down = event.state == ElementState::Pressed;
if !self.space_down {
self.panning = false;
}
return;
}
// 侧栏获得键盘路由后,Tab 在宿主目标间移动,Enter/Space 采用按下捕获、释放触发。
if self.sidebar_keyboard {
if event.logical_key == Key::Named(NamedKey::Tab)
&& event.state == ElementState::Pressed
{
self.sidebar
.interaction
.focus_next(&self.sidebar.targets, self.modifiers.shift_key());
self.request_redraw();
return;
}
if matches!(
event.logical_key,
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)
{
self.sidebar_action(action);
}
self.request_redraw();
return;
}
}
if event.state == ElementState::Pressed
&& !event.repeat
&& (self.modifiers.super_key() || self.modifiers.control_key())
&& matches!(&event.logical_key, Key::Character(key) if key.eq_ignore_ascii_case("t"))
{
self.open_developer_tool("New tab");
return;
}
if event.state == ElementState::Pressed && !event.repeat {
let close = (self.modifiers.super_key() || self.modifiers.control_key())
&& 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;
}
}
// 页面交互成功消费按键才提前返回;未处理的按键还可交给开发工具,保持明确的路由顺序。
if self.guest_keyboard && self.handle_guest_key(event) {
self.request_redraw();
return;
}
if self.developer_tools.keyboard_control(
event,
self.modifiers.shift_key(),
&self.layout.developer_tools,
&self.activity,
) {
self.request_redraw();
return;
}
if event.state != ElementState::Pressed {
return;
}
let is_f12 = matches!(event.logical_key, Key::Named(NamedKey::F12));
let is_i =
matches!(&event.logical_key, Key::Character(value) if value.eq_ignore_ascii_case("i"));
let mac_shortcut = self.modifiers.super_key()
&& (self.modifiers.alt_key() || self.modifiers.shift_key())
&& is_i;
let other_shortcut = self.modifiers.control_key() && self.modifiers.shift_key() && is_i;
if is_f12 || mac_shortcut || other_shortcut {
if !event.repeat {
self.open_developer_tool("Console");
}
return;
}
// 只有展开且有编辑焦点的工具接受文本;剪切先成功写系统剪贴板,再删除选中内容。
if !self.developer_tools.open || self.developer_tools.focus.is_none() {
return;
}
let command = self.modifiers.super_key() || self.modifiers.control_key();
if command && let Key::Character(value) = &event.logical_key {
match value.to_lowercase().as_str() {
"a" => self.developer_tools.selection = true,
"v" => match crate::clipboard::read() {
Ok(value) => self.developer_tools.insert(&value),
Err(error) => self.developer_tools.input_error = format!("Clipboard: {error}"),
},
"c" | "x" => {
let selected = self.developer_tools.selected_text();
if !selected.is_empty() {
match crate::clipboard::write(&selected) {
Ok(()) if value.eq_ignore_ascii_case("x") => {
self.developer_tools.insert("")
}
Ok(()) => {}
Err(error) => {
self.developer_tools.input_error = format!("Clipboard: {error}")
}
}
}
}
_ => {}
}
// 输入法预编辑期间不再把原始按键转换为编辑动作;最终文本通过 Ime::Commit 插入。
} else if let Key::Named(key) = &event.logical_key {
if !self.developer_tools.preedit.is_empty() {
return;
}
self.developer_tools
.key(key, self.modifiers.shift_key(), command);
} else if !command
&& self.developer_tools.preedit.is_empty()
&& let Some(text) = &event.text
{
let text: String = text.chars().filter(|c| !c.is_control()).collect();
self.developer_tools.insert(&text);
}
self.request_redraw();
}
/// 判断指针是否被侧栏或管理面板覆盖,阻止一次点击同时落入页面。
fn guest_covered(&self, point: Point) -> bool {
if self.sidebar.covers(point) {
return true;
}
let panel = self.layout.developer_tools.panel;
self.developer_tools.open
&& crate::geometry::Rect::new(
panel.x - 6.0,
panel.y - 6.0,
panel.width + 12.0,
panel.height + 12.0,
)
.contains(point)
}
/// 排除宿主管理区域后,把原生指针转换到页面使用的坐标系。
fn guest_point(&self) -> Option<Point> {
if self.guest_covered(self.pointer) {
return None;
}
let current = self.namespace.read().ok()?;
let point = Point::new(self.pointer.x - self.sidebar.left(), self.pointer.y);
if point.x < 0.0
|| point.y < 0.0
|| current
.viewport
.is_some_and(|size| point.x >= size.width || point.y >= size.height)
{
return None;
}
Some(current.document.frame.surface.to_world(point))
}
/// 从页面坐标与当前帧命中结果构造带版本的点击输入。
fn guest_hit(&self) -> Option<crate::events::Input> {
let point = self.guest_point()?;
let current = self.namespace.read().ok()?;
let frame = &current.document.frame;
if frame.session == 0 {
return None;
}
// 命中的是当前发布帧,事件同时携带 session/layout_version;worker 可拒绝排队期间已过期的输入。
let mount = frame.hit_test(point)?;
Some(crate::events::Input {
window: self.resource,
session: frame.session,
layout_version: frame.layout_version,
mount: mount.id,
kind: "click".into(),
point: Some([point.x, point.y]),
press_point: Some([point.x, point.y]),
delta: None,
tab_drop: None,
screen_position: None,
})
}
/// Pointer subscriptions explicitly opt a Mount into continuous input.
/// Capture retains window/session/Mount; the worker validates it again.
fn pointer_target(&self) -> Option<crate::events::Input> {
let mut input = self.guest_hit()?;
let current = self.namespace.read().ok()?;
let frame = &current.document.frame;
let mut mount = frame.mounts.iter().find(|m| m.id == input.mount)?;
while !mount.pointer_events {
mount = frame.mounts.iter().find(|m| Some(m.id) == mount.parent)?;
}
input.mount = mount.id;
input.press_point = None;
Some(input)
}
fn emit_pointer(&self, mut input: crate::events::Input, kind: &str) {
input.kind = kind.into();
input.press_point = None;
if let Some(p) = self.guest_point() {
input.point = Some([p.x, p.y]);
}
self.event_client.send(input);
}
fn cancel_pointer(&mut self) {
if let Some(input) = self.pointer_capture.take() {
self.emit_pointer(input, "pointercancel");
}
if let Some(input) = self.pointer_hover.take() {
self.emit_pointer(input, "pointerleave");
}
}
fn move_pointer(&mut self) {
if let Some(input) = self.pointer_capture.clone() {
self.emit_pointer(input, "pointermove");
return;
}
let next = self.pointer_target();
if self.pointer_hover.as_ref().map(|i| (i.session, i.mount))
!= next.as_ref().map(|i| (i.session, i.mount))
&& let Some(old) = self.pointer_hover.take()
{
self.emit_pointer(old, "pointerleave");
}
if let Some(input) = next.clone() {
self.emit_pointer(input, "pointermove");
}
self.pointer_hover = next;
}
/// 将标签快捷键转换为当前窗口/帧中的明确页面动作。
fn send_tab_shortcut(&mut self, close: bool) -> bool {
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;
}
let active = frame.tabs.iter().position(|tab| tab.active).unwrap_or(0);
let index = if close {
active
} else if self.modifiers.shift_key() {
(active + frame.tabs.len() - 1) % frame.tabs.len()
} else {
(active + 1) % frame.tabs.len()
};
Some(crate::events::Input {
window: self.resource,
session: frame.session,
layout_version: frame.layout_version,
mount: frame.tabs[index].id,
kind: if close { "tab-close" } else { "tab-activate" }.into(),
point: None,
delta: None,
press_point: None,
tab_drop: None,
screen_position: None,
})
});
if let Some(input) = input {
self.guest_pressed = None;
self.guest_key = None;
self.send_guest(input);
true
} else {
false
}
}
/// 处理侧栏展开、工具入口和页面管理,尺寸变化还需重新发布可用视口。
fn sidebar_action(&mut self, action: crate::sidebar::Action) {
use crate::sidebar::Action;
match action {
Action::Toggle => {
self.sidebar.open = !self.sidebar.open;
self.sidebar.interaction.cancel();
self.sidebar_keyboard = self.sidebar.open;
self.guest_pressed = None;
self.publish_viewport();
}
Action::NewPage => self.open_developer_tool("New tab"),
Action::Tool(name) => self.open_developer_tool(name),
Action::Activate(id) | Action::Close(id) => {
let input = self
.namespace
.read()
.ok()
.map(|current| crate::events::Input {
window: self.resource,
session: current.document.frame.session,
layout_version: current.document.frame.layout_version,
mount: id,
kind: if matches!(action, Action::Close(_)) {
"tab-close"
} else {
"tab-activate"
}
.into(),
point: None,
press_point: None,
delta: None,
tab_drop: None,
screen_position: None,
});
// 滚轮优先交给可滚动挂载;未命中则尝试 3D 缩放或整页平移,最后更新开发工具自身滚动。
if let Some(input) = input {
self.send_guest(input);
}
}
}
self.request_redraw();
}
/// 在手势开始时捕获具体相机视口 ID,后续拖动保持同一目标。
fn begin_pan(&mut self) -> bool {
self.pan_viewport = None;
let Some(point) = self.guest_point() else {
return false;
};
let Ok(current) = self.namespace.read() else {
return false;
};
let frame = &current.document.frame;
self.pan_viewport = frame.camera_at(point).map(|mount| mount.id);
self.pan_viewport.is_some() || frame.surface.mode != crate::page::Mode::Window
}
/// 把窗口位移/缩放转换为捕获视口的纯数据相机事件,处理侧栏和页面视角的尺度差异。
fn page_view(&mut self, kind: &str, delta: [f32; 2]) {
if !self.panning && self.guest_covered(self.pointer) {
return;
}
let point = Point::new(self.pointer.x - self.sidebar.left(), self.pointer.y);
let input = self.namespace.read().ok().and_then(|current| {
let frame = &current.document.frame;
if frame.session == 0
|| frame.fault.is_some()
|| !self.panning
&& (point.x < 0.0
|| point.y < 0.0
|| current
.viewport
.is_some_and(|size| point.x >= size.width || point.y >= size.height))
{
return None;
}
let world = frame.surface.to_world(point);
// 拖动沿用捕获 ID,滚轮/缩放则按当前点命中;被捕获的视口若已消失,取消此次输入而不切换目标。
let viewport = if self.panning {
self.pan_viewport.and_then(|id| {
frame.mounts.iter().find(|mount| {
mount.id == id
&& (mount.canvas_viewport
|| mount.scene3d_viewport && mount.camera_controls)
})
})
} else {
frame.camera_at(world)
};
if self.panning && self.pan_viewport.is_some() && viewport.is_none() {
return None;
}
// 局部 2D/3D 视口使用不同动作名。平移除去页面 zoom 以还原世界位移,缩放倍率本身不按 zoom 缩放。
if let Some(viewport) = viewport {
return Some(crate::events::Input {
window: self.resource,
session: frame.session,
layout_version: frame.layout_version,
mount: viewport.id,
kind: if viewport.scene3d_viewport {
if kind == "page-zoom" {
"scene-zoom"
} else if self.orbiting {
"scene-orbit"
} else {
"scene-pan"
}
} else if kind == "page-zoom" {
"viewport-zoom"
} else {
"viewport-pan"
}
.into(),
point: Some([world.x, world.y]),
press_point: None,
delta: Some([
delta[0]
/ if kind == "page-pan" {
frame.surface.zoom
} else {
1.0
},
delta[1] / frame.surface.zoom,
]),
tab_drop: None,
screen_position: None,
});
}
// 没有局部相机时才考虑整页变换:普通 Window 不平移,只有 Canvas 接受整页缩放。
if frame.surface.mode == crate::page::Mode::Window
|| kind == "page-zoom" && frame.surface.mode != crate::page::Mode::Canvas
{
return None;
}
Some(crate::events::Input {
window: self.resource,
session: frame.session,
layout_version: frame.layout_version,
mount: 0,
kind: kind.into(),
point: (kind == "page-zoom").then_some([point.x, point.y]),
press_point: None,
delta: Some(delta),
tab_drop: None,
screen_position: None,
})
});
if let Some(input) = input {
self.guest_pressed = None;
self.send_guest(input);
}
}
/// 把已经构造的页面输入交给有界客户端,保持 GUI 线程不直接调用 VM。
fn send_guest(&mut self, event: crate::events::Input) {
if !self.event_client.send(event) {
self.developer_tools.input_error = "Native event queue is unavailable or full".into();
self.developer_tools.open = true;
}
}
/// 把页面拥有焦点时的键盘动作转换为托管事件,保留按下/释放配对。
fn handle_guest_key(&mut self, event: &winit::event::KeyEvent) -> bool {
let Key::Named(key) = &event.logical_key else {
return false;
};
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() {
return None;
}
// 按发布帧中的挂载顺序遍历可交互目标;Shift+Tab 逆向环绕,尚无焦点时从首/末项进入。
let targets: Vec<_> = frame
.mounts
.iter()
.filter(|mount| mount.interactive)
.collect();
if targets.is_empty() {
return None;
}
let current = self
.guest_focus
.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)
} else {
current.map(|i| (i + 1) % targets.len()).unwrap_or(0)
};
Some(crate::events::Input {
window: self.resource,
session: frame.session,
layout_version: frame.layout_version,
mount: targets[index].id,
kind: "focus".into(),
point: None,
delta: None,
press_point: None,
tab_drop: None,
screen_position: None,
})
});
if let Some(next) = next {
self.guest_key = None;
self.guest_focus = Some(next.clone());
self.send_guest(next);
return true;
}
return false;
}
return self.guest_focus.is_some();
}
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。
} else if let Some((pressed, mut input)) = self.guest_key.take()
&& pressed == *key
{
input.kind = "click".into();
self.send_guest(input);
}
return true;
}
false
}
/// 按平台窗口属性创建原生窗口、软件表面和输入法支持,再公布实际逻辑视口。
fn create_native(&mut self, event_loop: &ActiveEventLoop) -> anyhow::Result<()> {
if self.window.is_some() {
return Ok(());
}
let attributes = WindowAttributes::default()
.with_title(if self.resource == crate::window::MAIN_WINDOW {
"Tessera".into()
} else {
format!("Endor Window {}", self.resource)
})
.with_inner_size(if self.resource == crate::window::MAIN_WINDOW {
LogicalSize::new(1_280.0, 800.0)
} else {
LogicalSize::new(960.0, 640.0)
})
.with_min_inner_size(LogicalSize::new(720.0, 480.0))
.with_max_inner_size(LogicalSize::new(
crate::window::MAX_WINDOW_DIMENSION,
crate::window::MAX_WINDOW_DIMENSION,
));
#[cfg(target_os = "macos")]
let attributes = {
use winit::platform::macos::WindowAttributesExtMacOS;
attributes
.with_title_hidden(true)
.with_titlebar_transparent(true)
.with_fullsize_content_view(true)
};
// 先创建平台窗口,再建立软件绘图上下文/表面;任何一步失败都向外返回,由 resumed 记录并处理。
let window = Arc::new(event_loop.create_window(attributes)?);
#[cfg(target_os = "macos")]
crate::native_chrome::align_controls(&window);
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}"))?;
window.set_ime_allowed(true);
self.window = Some(window.clone());
self.context = Some(context);
self.surface = Some(surface);
self.apply_window_commands();
self.publish_viewport();
window.request_redraw();
self.native_error = None;
Ok(())
}
/// 停止新的交互并请求运行时清理;原生事件循环等待完成通知后退出。
fn request_exit(&mut self) {
if self.exit_requested {
return;
}
self.guest_pressed = None;
self.guest_key = None;
// 正常关闭先请求 VM 清理,等待 ShutdownComplete 再退出 OS 循环;发送失败时自行投递结束通知并保留错误。
if self.control.request_shutdown() {
self.exit_requested = true;
} else {
self.native_error = Some("VM worker unavailable during shutdown".into());
let _ = self.proxy.send_event(AppEvent::ShutdownComplete {
error: self.native_error.clone(),
});
}
}
/// 把原生应用记录的最终错误反馈给 main 的退出结果。
pub fn exit_result(&self) -> anyhow::Result<()> {
if let Some(error) = &self.native_error {
anyhow::bail!("{error}");
}
Ok(())
}
/// 包装一次实际绘制,统一记录故障并避免对同一错误反复发送通知。
fn redraw(&mut self) {
#[cfg(target_os = "macos")]
if let Some(window) = self.window.as_ref() {
crate::native_chrome::align_controls(window);
}
match self.try_redraw() {
Ok(()) => {
if self.window.is_some() && self.surface.is_some() {
self.native_error = None;
}
}
Err(error) => {
let message = format!("Window {} redraw failed: {error:#}", self.resource);
if self.native_error.as_ref() != Some(&message) {
self.native_error = Some(message.clone());
let _ = self.proxy.send_event(AppEvent::RuntimeFailed { message });
}
}
}
}
/// 读取完整发布文档,生成整窗显示列表,写入原生像素表面并呈现;错误交由外层记录。
fn try_redraw(&mut self) -> anyhow::Result<()> {
let guest_cursor = self.guest_hit().is_some()
|| self
.sidebar
.interaction
.hit(&self.sidebar.targets)
.is_some();
let (Some(window), Some(surface)) = (self.window.as_ref(), self.surface.as_mut()) else {
return Ok(());
};
let size = window.inner_size();
let (Some(width), Some(height)) =
(NonZeroU32::new(size.width), NonZeroU32::new(size.height))
else {
return Ok(());
};
let Ok(namespace) = self.namespace.read() else {
return Ok(());
};
// 读锁保护一次完整文档取样与场景生成;生成后释放锁,像素写入和 present 不再占用 namespace。
let scene = scene::build_scene(
size.width,
size.height,
window.scale_factor() as f32,
&namespace.document,
&mut self.developer_tools,
&self.activity,
&mut self.sidebar,
);
drop(namespace);
self.layout = scene.layout;
window.set_cursor(if guest_cursor {
winit::window::CursorIcon::Pointer
} else {
self.developer_tools
.cursor_kind(&self.layout.developer_tools)
});
if self.developer_tools.repaint {
self.developer_tools.repaint = false;
window.request_redraw();
}
let ime = self
.developer_tools
.ime_position(&self.layout.developer_tools);
window.set_ime_cursor_area(
LogicalPosition::new(ime.x as f64, ime.y as f64),
LogicalSize::new(1.0, 19.0),
);
// 根据真实物理尺寸调整像素表面;逻辑坐标到物理像素的转换已由场景构造中的 scale_factor 负责。
surface
.resize(width, height)
.map_err(|error| anyhow::anyhow!("{error}"))?;
let mut buffer = surface
.buffer_mut()
.map_err(|error| anyhow::anyhow!("{error}"))?;
SoftwareRenderer::render(&scene.display_list, &mut buffer, size.width, size.height)
.map_err(anyhow::Error::msg)?;
buffer
.present()
.map_err(|error| anyhow::anyhow!("{error}"))?;
Ok(())
}
}
impl ApplicationHandler<AppEvent> for TesseraApp {
/// 处理 OS 直接结束事件循环的收尾,尝试在明确时限内等待运行时清理。
fn exiting(&mut self, _: &ActiveEventLoop) {
// OS 终止可能跳过 CloseRequested;此时循环已在退出,无法再等事件通知。
// 最多阻塞 5 秒等待运行时完成清理,超时保留退出错误而不无限等待。
if !self.control.request_shutdown() {
self.native_error = Some("VM worker unavailable during shutdown".into());
return;
}
match self.control.wait_shutdown(Duration::from_secs(5)) {
Some(outcome) => {
if let Some(error) = outcome.error {
self.native_error = Some(error);
}
}
None => self.native_error = Some("VM shutdown did not finish within 5 seconds".into()),
}
}
/// 当前不在进入等待前轮询工作;文档通知和原生事件负责唤醒循环,避免空闲忙转。
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {}
/// 应用获得原生活跃上下文时建立窗口,并仅在需要时请求初始页面。
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.resource == crate::window::MAIN_WINDOW
&& !self.startup_page_requested
&& self
.namespace
.read()
.is_ok_and(|n| n.runtime_session == 0 && n.document.frame.tabs.is_empty())
{
self.startup_page_requested = true;
self.developer_tools.initialize_tools("New tab");
}
if let Err(error) = self.create_native(event_loop) {
self.native_error = Some(format!(
"Window {} initialization failed: {error:#}",
self.resource
));
if self.resource == crate::window::MAIN_WINDOW {
event_loop.exit();
}
}
}
/// 处理 worker 发回的文档更新、故障和退出完成通知,不在 GUI 线程执行源回调。
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: AppEvent) {
if self.control.is_exiting() && !matches!(&event, AppEvent::ShutdownComplete { .. }) {
return;
}
match event {
AppEvent::ShutdownComplete { error } => {
if let Some(error) = error {
self.native_error = Some(error);
}
event_loop.exit();
}
AppEvent::RuntimeFailed { message } => {
self.developer_tools.input_error = message;
self.developer_tools.open = true;
self.guest_pressed = None;
self.guest_key = None;
self.request_redraw();
}
AppEvent::DocumentUpdated => self.document_updated(event_loop),
AppEvent::ConsoleFailed { message, source } => {
self.developer_tools.input_error = format!("Request failed: {message}");
if self.developer_tools.input.is_empty() {
self.developer_tools.input = source;
self.developer_tools.cursor = self.developer_tools.input.len();
self.developer_tools.reveal_cursor();
}
self.request_redraw();
}
}
}
/// 处理原生尺寸、输入、焦点和关闭事件;先判断宿主控件,再转发页面事件。
fn window_event(
&mut self,
_event_loop: &ActiveEventLoop,
window_id: WindowId,
event: WindowEvent,
) {
if self.control.is_exiting() {
return;
}
let Some(window) = self.window.as_ref().cloned() else {
return;
};
// 主处理器先按 OS WindowId 转发子窗口事件;转发后汇总视口,供同一运行时布局所有窗口。
if window.id() != window_id {
if let Some(child) = self.children.values_mut().find(|child| {
child
.window
.as_ref()
.is_some_and(|window| window.id() == window_id)
}) {
child.window_event(_event_loop, window_id, event);
self.publish_viewport();
}
return;
}
match event {
// 主窗口关闭触发应用退出;子窗口关闭先发 window-close,由源程序生命周期逻辑决定资源清理。
WindowEvent::CloseRequested => {
if self.resource == crate::window::MAIN_WINDOW {
self.request_exit();
} else {
let input = self
.namespace
.read()
.ok()
.map(|namespace| crate::events::Input {
window: self.resource,
session: namespace.document.frame.session,
layout_version: namespace.document.frame.layout_version,
mount: 0,
kind: "window-close".into(),
point: None,
delta: None,
press_point: None,
tab_drop: None,
screen_position: None,
});
if let Some(input) = input {
self.send_guest(input);
}
}
}
WindowEvent::Resized(_) | WindowEvent::ScaleFactorChanged { .. } => {
self.publish_viewport();
window.request_redraw();
}
WindowEvent::CursorMoved {
position: PhysicalPosition { x, y },
..
} => {
let scale = window.scale_factor() as f32;
// OS 指针是物理像素,先除 DPI 得逻辑坐标;后续侧栏命中和页面世界坐标转换都从该坐标出发。
let next = Point::new(x as f32 / scale, y as f32 / scale);
let delta = Point::new(next.x - self.pointer.x, next.y - self.pointer.y);
self.pointer = next;
self.sidebar
.interaction
.pointer_moved(next, &self.sidebar.targets);
if !self.panning
&& let Some(press) = &self.guest_pressed
&& let Some(point) = self.guest_point()
&& let Some(start) = press.point
{
let is_3d = self.namespace.read().is_ok_and(|current| {
current.document.frame.mounts.iter().any(|mount| {
mount.id == press.mount
&& mount.scene3d_viewport
&& mount.camera_controls
})
});
// 3D 相机拖动超过世界坐标下的 3 单位阈值后转为 orbit,并清除 click 捕获,避免释放时误激活。
if is_3d && (point.x - start[0]).hypot(point.y - start[1]) > 3. {
self.orbiting = true;
self.panning = self.begin_pan();
self.guest_pressed = None;
}
}
if self.panning {
self.page_view("page-pan", [-delta.x, -delta.y]);
window.request_redraw();
return;
}
self.move_pointer();
self.developer_tools.move_pointer(
next,
delta,
&self.layout.developer_tools,
window.inner_size().width as f32 / scale,
);
window.set_cursor(
if self.guest_hit().is_some()
|| self
.sidebar
.interaction
.hit(&self.sidebar.targets)
.is_some()
{
winit::window::CursorIcon::Pointer
} else {
self.developer_tools
.cursor_kind(&self.layout.developer_tools)
},
);
window.request_redraw();
}
WindowEvent::CursorLeft { .. } => {
self.cancel_pointer();
self.guest_pressed = None;
self.panning = false;
self.sidebar.interaction.pointer_left();
self.developer_tools.pointer_left();
window.set_cursor(winit::window::CursorIcon::Default);
window.request_redraw();
}
WindowEvent::MouseInput {
state,
button: MouseButton::Middle,
..
} => {
self.orbiting = false;
self.panning = state == ElementState::Pressed && self.begin_pan();
self.guest_pressed = None;
window.request_redraw();
}
WindowEvent::MouseInput {
state,
button: MouseButton::Left,
..
} => {
if state == ElementState::Pressed {
if let Some(input) = self.pointer_target() {
self.pointer_capture = Some(input.clone());
self.emit_pointer(input, "pointerdown");
return;
}
} else if let Some(input) = self.pointer_capture.take() {
self.emit_pointer(input, "pointerup");
return;
}
if state == ElementState::Pressed {
if self.sidebar.covers(self.pointer) {
self.sidebar_keyboard = true;
self.sidebar.interaction.press(&self.sidebar.targets);
self.guest_pressed = None;
if self.sidebar.open
&& self.pointer.y < crate::sidebar::HEADER
&& self.pointer.x >= crate::sidebar::CONTROLS
&& self
.sidebar
.interaction
.hit(&self.sidebar.targets)
.is_none()
{
#[cfg(target_os = "macos")]
if crate::native_chrome::titlebar_double_click(&window) {
return;
}
let _ = window.drag_window();
}
} else if self.space_down && self.guest_point().is_some() {
self.orbiting = false;
self.panning = self.begin_pan();
self.guest_pressed = None;
} else {
self.sidebar_keyboard = false;
self.sidebar.interaction.blur();
self.guest_keyboard = !self.guest_covered(self.pointer);
if self.guest_keyboard {
self.developer_tools.blur();
}
self.guest_pressed = self.guest_hit();
self.developer_tools.press(
&self.layout.developer_tools,
self.pointer,
&self.activity,
);
}
} else {
// 拖动结束仅结束手势,不把同一次释放再当成点击;普通点击需保持按下与释放的帧身份一致。
if self.panning {
self.panning = false;
self.guest_pressed = None;
window.request_redraw();
return;
}
if let Some(action) = self.sidebar.interaction.release(&self.sidebar.targets) {
self.sidebar_action(action);
} else if let Some(pressed) = self.guest_pressed.take()
&& let Some(mut released) = self.guest_hit()
&& released.session == pressed.session
&& released.layout_version == pressed.layout_version
{
// 保留起点用于运行时判断点击/拖动语义;目标来自释放位置,具体可接受性仍由 worker 校验。
released.press_point = pressed.point;
self.send_guest(released);
}
self.developer_tools.release(
&self.layout.developer_tools,
self.pointer,
&self.activity,
);
}
window.request_redraw();
}
WindowEvent::PinchGesture { delta, .. } => {
self.page_view("page-zoom", [(delta as f32).exp(), 0.0]);
window.request_redraw();
}
WindowEvent::MouseWheel { delta, .. } => {
// 行滚动按每行 38 逻辑像素换算,像素滚动除去 DPI;Shift 在几乎没有横向分量时将纵向量转横向。
let (mut dx, mut dy) = match delta {
MouseScrollDelta::LineDelta(x, y) => (x * 38.0, y * 38.0),
MouseScrollDelta::PixelDelta(p) => (
p.x as f32 / window.scale_factor() as f32,
p.y as f32 / window.scale_factor() as f32,
),
};
if self.modifiers.shift_key() && dx.abs() < 0.1 {
dx = dy;
dy = 0.0;
}
if self.sidebar.covers(self.pointer) {
if self.sidebar.open && self.pointer.y >= 333.0 {
self.sidebar.scroll -= dy;
}
window.request_redraw();
return;
}
let point = self.guest_point();
let canvas = self.namespace.read().is_ok_and(|n| {
n.document.frame.surface.mode == crate::page::Mode::Canvas
|| 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;
}
let scene3d = self.namespace.read().is_ok_and(|current| {
point
.and_then(|point| current.document.frame.camera_at(point))
.is_some_and(|mount| mount.scene3d_viewport)
});
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() {
return None;
}
let delta = [-dx / frame.surface.zoom, -dy / frame.surface.zoom];
let mount = crate::scroll::wheel_target(frame, point, delta)?;
Some(crate::events::Input {
window: self.resource,
session: frame.session,
layout_version: frame.layout_version,
mount,
kind: "scroll".into(),
point: Some([point.x, point.y]),
press_point: None,
tab_drop: None,
screen_position: None,
delta: Some(delta),
})
})
});
if let Some(input) = input {
self.send_guest(input);
} else if scene3d {
self.page_view("page-zoom", [(dy * 0.005).exp().clamp(0.1, 10.), 0.]);
} else {
let horizontal = self.namespace.read().is_ok_and(|n| {
n.document.frame.surface.mode == crate::page::Mode::Horizontal
});
self.page_view(
"page-pan",
if horizontal && dx.abs() < 0.1 {
[-dy, 0.0]
} else {
[-dx, -dy]
},
);
}
self.developer_tools
.wheel(&self.layout.developer_tools, self.pointer, dx, dy);
window.request_redraw();
}
WindowEvent::Ime(Ime::Preedit(value, _)) => {
self.developer_tools.preedit = value;
window.request_redraw();
}
WindowEvent::Ime(Ime::Commit(value)) => {
self.developer_tools.preedit.clear();
if self.developer_tools.open {
self.developer_tools.insert(&value);
}
window.request_redraw();
}
// 失焦可能导致收不到对应释放事件,因此清空按键/鼠标捕获、平移状态及修饰键,避免恢复焦点后卡住。
WindowEvent::Focused(false) => {
self.cancel_pointer();
self.guest_pressed = None;
self.guest_key = None;
self.developer_tools.blur();
self.sidebar.interaction.blur();
self.panning = false;
self.space_down = false;
self.modifiers = ModifiersState::default();
window.request_redraw();
}
WindowEvent::ModifiersChanged(modifiers) => self.modifiers = modifiers.state(),
WindowEvent::KeyboardInput { event, .. } => self.handle_keyboard(&event),
WindowEvent::RedrawRequested => self.redraw(),
_ => {}
}
}
}