Files
endorphin-tessera-gui/src/app.rs
T
codex-bot e3e46068eb
Verify Tessera GUI / verify (push) Failing after 7m35s
Support shared-VM native windows and Page detachment
2026-10-21 18:11:58 +08:00

818 lines
31 KiB
Rust

use std::{
num::NonZeroU32,
sync::{Arc, RwLock},
};
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::DeveloperToolsState,
document::NamespaceState,
geometry::Point,
scene::{self, SceneLayout},
software_renderer::SoftwareRenderer,
};
#[derive(Clone, Debug)]
pub enum AppEvent {
RuntimeFailed { message: String },
DocumentUpdated,
ConsoleFailed { message: String, source: String },
}
pub struct TesseraApp {
resource: u64,
native_session: u64,
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,
modifiers: ModifiersState,
activity: Activity,
event_client: crate::events::Client,
guest_pressed: Option<crate::events::Input>,
tab_pressed: Option<crate::events::Input>,
tab_drag_origin: Option<Point>,
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>,
) -> anyhow::Result<Self> {
let event_client = crate::events::Client::new(proxy.clone())?;
Ok(Self {
resource: crate::window::MAIN_WINDOW,
native_session: 0,
children: std::collections::BTreeMap::new(),
proxy: proxy.clone(),
window: None,
context: None,
surface: None,
namespace,
developer_tools: {
let mut state = DeveloperToolsState::default();
state.proxy = Some(proxy);
state
},
layout: SceneLayout::default(),
pointer: Point::default(),
modifiers: ModifiersState::default(),
activity,
event_client,
guest_pressed: None,
tab_pressed: None,
tab_drag_origin: None,
guest_focus: None,
guest_key: None,
guest_keyboard: true,
})
}
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()
}));
match Self::new(namespace, self.activity.clone(), self.proxy.clone()) {
Ok(mut child) => {
child.resource = id;
child.native_session = session;
child.resumed(event_loop);
child.document_updated(event_loop);
self.children.insert(id, child);
}
Err(error) => {
self.developer_tools.input_error = format!("Window {id}: {error}");
self.developer_tools.open = true;
}
}
}
}
}
fn apply_window_commands(&mut self, event_loop: &ActiveEventLoop) {
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 {
event_loop.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::Focus => window.focus_window(),
crate::window::Action::Close => {}
}
}
self.publish_viewport();
}
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.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 {
namespace.window_viewports.retain(|id, _| {
*id == crate::window::MAIN_WINDOW || self.children.contains_key(id)
});
for (id, child) in &self.children {
if let Ok(local) = child.namespace.read()
&& 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 toggle_developer_tools(&mut self) {
if let Ok(namespace) = self.namespace.read() {
self.developer_tools.toggle(&namespace.document);
}
self.guest_keyboard = !self.developer_tools.open;
self.request_redraw();
}
fn document_updated(&mut self, event_loop: &ActiveEventLoop) {
self.reconcile_windows(event_loop);
self.apply_window_commands(event_loop);
if let Ok(namespace) = self.namespace.read() {
self.developer_tools.document_updated(&namespace.document);
let frame = &namespace.document.frame;
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,
press_point: None,
});
}
self.request_redraw();
}
fn handle_keyboard(&mut self, event: &winit::event::KeyEvent) {
if self.guest_keyboard && 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() && 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.toggle_developer_tools();
}
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 {
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_hit(&self) -> Option<crate::events::Input> {
if self.guest_covered(self.pointer) {
return None;
}
let current = self.namespace.read().ok()?;
if current.viewport.is_some_and(|size| {
self.pointer.x < 0.0
|| self.pointer.y < 0.0
|| self.pointer.x >= size.width
|| self.pointer.y >= size.height
}) {
return None;
}
let frame = &current.document.frame;
if frame.session == 0 {
return None;
}
let offset = crate::tabs::height(&frame.tabs);
if self.pointer.y < offset {
return None;
}
let point = Point::new(self.pointer.x, self.pointer.y - offset);
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]),
})
}
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,
press_point: None,
})
});
if let Some(input) = input {
self.guest_pressed = None;
self.guest_key = None;
self.tab_pressed = None;
self.tab_drag_origin = None;
self.send_guest(input);
true
} else {
false
}
}
fn tab_hit(&self) -> Option<crate::events::Input> {
if self.guest_covered(self.pointer) {
return None;
}
let window = self.window.as_ref()?;
let namespace = self.namespace.read().ok()?;
let frame = &namespace.document.frame;
if frame.session == 0 || frame.fault.is_some() {
return None;
}
let width = window.inner_size().width as f32 / window.scale_factor() as f32;
let (id, close) = crate::tabs::hit(&frame.tabs, width, self.pointer)?;
Some(crate::events::Input {
window: self.resource,
session: frame.session,
layout_version: frame.layout_version,
mount: id,
kind: if close { "tab-close" } else { "tab-activate" }.into(),
point: None,
press_point: None,
})
}
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,
press_point: 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 redraw(&mut self) {
let guest_cursor = self.guest_hit().is_some() || self.tab_hit().is_some();
let (Some(window), Some(surface)) = (self.window.as_ref(), self.surface.as_mut()) else {
return;
};
let size = window.inner_size();
let (Some(width), Some(height)) =
(NonZeroU32::new(size.width), NonZeroU32::new(size.height))
else {
return;
};
let Ok(namespace) = self.namespace.read() else {
return;
};
let scene = scene::build_scene(
size.width,
size.height,
window.scale_factor() as f32,
&namespace.document,
&mut self.developer_tools,
&self.activity,
);
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)
.expect("resize software surface");
let mut buffer = surface.buffer_mut().expect("acquire software buffer");
SoftwareRenderer::render(&scene.display_list, &mut buffer, size.width, size.height);
buffer.present().expect("present software-rendered frame");
}
}
impl ApplicationHandler<AppEvent> for TesseraApp {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.window.is_some() {
return;
}
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,
));
let window = Arc::new(
event_loop
.create_window(attributes)
.expect("create Tessera window"),
);
let context = Context::new(window.clone()).expect("create software-rendering context");
let surface =
Surface::new(&context, window.clone()).expect("create software-rendering surface");
window.set_ime_allowed(true);
self.window = Some(window.clone());
self.context = Some(context);
self.surface = Some(surface);
self.apply_window_commands(event_loop);
self.publish_viewport();
window.request_redraw();
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: AppEvent) {
match event {
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,
) {
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 {
event_loop.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,
press_point: 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.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.tab_hit().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;
if self.tab_drag_origin.is_none() {
self.tab_pressed = None;
}
self.developer_tools.pointer_left();
window.set_cursor(winit::window::CursorIcon::Default);
window.request_redraw();
}
WindowEvent::MouseInput {
state,
button: MouseButton::Left,
..
} => {
if state == ElementState::Pressed {
self.guest_keyboard = !self.guest_covered(self.pointer);
self.tab_pressed = self.tab_hit();
self.tab_drag_origin = self
.tab_pressed
.as_ref()
.filter(|input| input.kind == "tab-activate")
.map(|_| self.pointer);
self.guest_pressed = self.guest_hit();
if self.guest_keyboard {
self.developer_tools.blur();
}
self.developer_tools.press(
&self.layout.developer_tools,
self.pointer,
&self.activity,
);
} else {
if let Some(mut pressed) = self.tab_pressed.take() {
let width = window.inner_size().width as f32 / window.scale_factor() as f32;
let dragged = self.tab_drag_origin.take().is_some_and(|origin| {
(self.pointer.x - origin.x).hypot(self.pointer.y - origin.y) > 12.0
&& (self.pointer.y < 0.0
|| self.pointer.y >= crate::tabs::HEIGHT + 12.0
|| self.pointer.x < 0.0
|| self.pointer.x >= width)
});
if dragged {
pressed.kind = "tab-detach".into();
self.send_guest(pressed);
} else if let Some(released) = self.tab_hit()
&& released.session == pressed.session
&& released.layout_version == pressed.layout_version
&& released.mount == pressed.mount
&& released.kind == pressed.kind
{
self.send_guest(released);
}
}
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::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;
}
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.modifiers = ModifiersState::default();
window.request_redraw();
}
WindowEvent::ModifiersChanged(modifiers) => self.modifiers = modifiers.state(),
WindowEvent::KeyboardInput { event, .. } => self.handle_keyboard(&event),
WindowEvent::RedrawRequested => self.redraw(),
_ => {}
}
}
}