//! 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 }, 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, // 只有主窗口维护此表;子窗口有独立文档/绘制状态,但复用运行时控制与输入队列。 children: std::collections::BTreeMap, proxy: EventLoopProxy, // 窗口、softbuffer 上下文与表面延迟创建;Arc 让表面持有有效的原生窗口句柄。 window: Option>, context: Option>>, surface: Option, Arc>>, namespace: Arc>, developer_tools: DeveloperToolsState, // 最近一次绘制生成的宿主控件几何,用于下一次输入命中、光标和 IME 定位。 layout: SceneLayout, pointer: Point, sidebar: crate::sidebar::State, sidebar_keyboard: bool, panning: bool, // 拖动开始时捕获视口,None 也可能表示拖动整张 Canvas,而非手势未开始。 pan_viewport: Option, orbiting: bool, space_down: bool, modifiers: ModifiersState, activity: Activity, event_client: crate::events::Client, // 保存按下时的版本与位置;释放时需确认仍处在同一会话与布局,防止旧画面误点击。 guest_pressed: Option, pointer_hover: Option, pointer_capture: Option, guest_focus: Option, // Enter/Space 的按下捕获;只在对应按键释放时发出一次 click,忽略系统连发。 guest_key: Option<(NamedKey, crate::events::Input)>, guest_keyboard: bool, } impl TesseraApp { /// 先建立连接权威 namespace 的有界事件客户端,再初始化主窗口状态。 /// 客户端创建失败向调用方返回错误;原生窗口和像素表面延迟到 resumed 创建。 pub fn new( namespace: Arc>, activity: Activity, proxy: EventLoopProxy, control: crate::api::RuntimeControl, ) -> anyhow::Result { 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>, activity: Activity, proxy: EventLoopProxy, 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::(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 { 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 { let point = self.guest_point()?; let current = self.namespace.read().ok()?; let frame = ¤t.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 { 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)?; 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 = ¤t.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 = ¤t.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 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(), _ => {} } } }