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

1373 lines
54 KiB
Rust

//! 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 {
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>,
window: Option<Arc<Window>>,
context: Option<Context<Arc<Window>>>,
surface: Option<Surface<Arc<Window>, Arc<Window>>>,
namespace: Arc<RwLock<NamespaceState>>,
developer_tools: DeveloperToolsState,
layout: SceneLayout,
pointer: Point,
sidebar: crate::sidebar::State,
sidebar_keyboard: bool,
panning: bool,
pan_viewport: Option<u64>,
orbiting: bool,
space_down: bool,
modifiers: ModifiersState,
activity: Activity,
event_client: crate::events::Client,
guest_pressed: Option<crate::events::Input>,
guest_focus: Option<crate::events::Input>,
guest_key: Option<(NamedKey, crate::events::Input)>,
guest_keyboard: bool,
}
impl TesseraApp {
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
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())?;
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,
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;
let documents = namespace.document.windows.clone();
drop(namespace);
if session != self.native_session {
// Console reset preserves the old projection with input disabled.
// A real replacement changes frame sessions and drops old OS views.
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;
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;
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;
}
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}")
}
}
}
}
_ => {}
}
} 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;
}
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,
})
}
/// 将标签快捷键转换为当前窗口/帧中的明确页面动作。
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,
});
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);
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;
}
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,
});
}
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;
}
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));
}
} 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)
};
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;
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(());
};
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),
);
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 termination can bypass CloseRequested. The loop is already ending;
// use a bounded fallback rather than silently dropping every callback.
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;
};
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 {
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;
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
})
});
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.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.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 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
{
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, .. } => {
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.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(),
_ => {}
}
}
}