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

1546 lines
59 KiB
Rust

use std::{
num::NonZeroU32,
sync::{Arc, RwLock},
time::{Duration, Instant},
};
use softbuffer::{Context, Surface};
use winit::{
application::ApplicationHandler,
dpi::{LogicalPosition, LogicalSize, PhysicalPosition},
event::{ElementState, Ime, MouseButton, MouseScrollDelta, WindowEvent},
event_loop::{ActiveEventLoop, ControlFlow, 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)]
pub enum AppEvent {
RuntimeFailed { message: String },
DocumentUpdated,
ShutdownComplete { error: Option<String> },
ConsoleFailed { message: String, source: String },
}
struct TabScrollHover {
window: u64,
session: u64,
point: Point,
direction: i8,
next: Instant,
}
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,
pointer_inside: bool,
new_tab_pressed: bool,
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>,
tab_drag_grab: Option<Point>,
tab_drop_index: Option<usize>,
tab_first: Option<usize>,
tab_motion: crate::tabs::Motion,
tab_visual: crate::tabs::Visual,
tab_scroll_hover: Option<TabScrollHover>,
window_order: Vec<u64>,
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())?;
Ok(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(),
pointer_inside: false,
new_tab_pressed: false,
modifiers: ModifiersState::default(),
activity,
event_client,
guest_pressed: None,
tab_pressed: None,
tab_drag_origin: None,
tab_drag_grab: None,
tab_drop_index: None,
tab_first: None,
tab_motion: crate::tabs::Motion::default(),
tab_visual: crate::tabs::Visual::default(),
tab_scroll_hover: None,
window_order: vec![crate::window::MAIN_WINDOW],
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(),
self.control.clone(),
) {
Ok(mut child) => {
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);
}
Err(error) => {
self.developer_tools.input_error = format!("Window {id}: {error}");
self.developer_tools.open = true;
}
}
}
}
}
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();
}
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 {
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());
if self.tab_pressed.as_ref().is_some_and(|pressed| {
frame.session != pressed.session
|| frame.fault.is_some()
|| !frame.tabs.iter().any(|tab| tab.id == pressed.mount)
}) {
self.tab_pressed = None;
self.tab_drag_origin = None;
self.tab_drag_grab = None;
self.tab_drop_index = None;
self.tab_first = None;
self.tab_scroll_hover = None;
}
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) {
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 {
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 - crate::tabs::RAIL, 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]),
delta: None,
press_point: Some([point.x, point.y]),
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.tab_pressed = None;
self.tab_drag_origin = None;
self.tab_drag_grab = None;
self.send_guest(input);
true
} else {
false
}
}
fn new_tab_hit(&self) -> bool {
let Some(window) = &self.window else {
return false;
};
self.namespace.read().is_ok_and(|n| {
(if self.tab_visual.plus.width > 0.0 {
self.tab_visual.plus
} else {
crate::tabs::plus_rect(
self.tab_first,
&n.document.frame.tabs,
window.inner_size().width as f32 / window.scale_factor() as f32,
)
})
.contains(self.pointer)
})
}
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) = if self.tab_visual.rects.is_empty() {
crate::tabs::hit(self.tab_first, &frame.tabs, width, self.pointer)
} else {
self.tab_visual.hit(self.pointer).or_else(|| {
crate::tabs::navigation_hit(self.tab_first, &frame.tabs, width, self.pointer)
})
}?;
if !frame.tabs.iter().any(|tab| tab.id == id) {
return None;
}
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,
delta: None,
press_point: None,
tab_drop: None,
screen_position: 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,
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 apps(&self) -> impl Iterator<Item = &Self> {
std::iter::once(self).chain(self.children.values())
}
fn drag_anchor(&self, input: &crate::events::Input) -> Option<Point> {
let window = self.window.as_ref()?;
let current = self.namespace.read().ok()?;
if let Some((_, rect)) = self
.tab_visual
.rects
.iter()
.find(|(id, _)| *id == input.mount)
{
return Some(Point::new(self.pointer.x - rect.x, self.pointer.y));
}
crate::tabs::drag_anchor(
self.tab_first,
&current.document.frame.tabs,
window.inner_size().width as f32 / window.scale_factor() as f32,
input.mount,
self.pointer,
)
}
fn clear_drag_feedback(&mut self, target: Option<(u64, usize)>) {
let index = target
.filter(|(window, _)| *window == self.resource)
.map(|(_, index)| index);
if self.tab_drop_index != index {
self.tab_drop_index = index;
self.request_redraw();
}
for child in self.children.values_mut() {
child.clear_drag_feedback(target);
}
}
fn clear_tab_scroll(&mut self) {
self.tab_scroll_hover = None;
if self.tab_first.take().is_some() {
self.request_redraw();
}
for child in self.children.values_mut() {
child.clear_tab_scroll();
}
}
fn tick_tab_scroll(&mut self, event_loop: &ActiveEventLoop) {
let Some(mut hover) = self.tab_scroll_hover.take() else {
return;
};
let dragging = self.apps().any(|app| {
app.tab_drag_origin.is_some()
&& app.tab_pressed.as_ref().is_some_and(|input| {
input.session == hover.session
&& app.namespace.read().ok().is_some_and(|current| {
let frame = &current.document.frame;
frame.session == input.session
&& frame.fault.is_none()
&& frame.tabs.iter().any(|tab| tab.id == input.mount)
})
})
});
if !dragging {
self.clear_tab_scroll();
return;
}
let now = Instant::now();
if now < hover.next {
event_loop.set_control_flow(ControlFlow::WaitUntil(hover.next));
self.tab_scroll_hover = Some(hover);
return;
}
let target = if self.resource == hover.window {
Some(&mut *self)
} else {
self.children.get_mut(&hover.window)
};
let feedback = target.and_then(|target| {
let window = target.window.as_ref()?;
let width = window.inner_size().width as f32 / window.scale_factor() as f32;
let current = target.namespace.read().ok()?;
let frame = &current.document.frame;
if frame.session != hover.session
|| frame.fault.is_some()
|| target.guest_covered(hover.point)
{
return None;
}
let direction =
crate::tabs::edge_direction(&frame.tabs, width, target.tab_first, hover.point);
if direction != hover.direction {
return None;
}
let first = crate::tabs::scroll_start(&frame.tabs, width, target.tab_first, direction)?;
let index = crate::tabs::insertion_index(Some(first), &frame.tabs, width, hover.point)?;
drop(current);
target.tab_first = Some(first);
target.request_redraw();
Some((hover.window, index))
});
if let Some(feedback) = feedback {
self.clear_drag_feedback(Some(feedback));
hover.next = now + Duration::from_millis(120);
event_loop.set_control_flow(ControlFlow::WaitUntil(hover.next));
self.tab_scroll_hover = Some(hover);
}
}
fn native_tab_drag(&mut self, window_id: WindowId, event: &WindowEvent) {
if self.resource != crate::window::MAIN_WINDOW {
return;
}
let Some(receiver) = self.apps().find(|app| {
app.window
.as_ref()
.is_some_and(|window| window.id() == window_id)
}) else {
return;
};
let receiver_id = receiver.resource;
if matches!(event, WindowEvent::CursorLeft { .. }) {
if self
.tab_scroll_hover
.as_ref()
.is_some_and(|hover| hover.window == receiver_id)
{
self.tab_scroll_hover = None;
self.clear_drag_feedback(None);
}
return;
}
if matches!(
event,
WindowEvent::Focused(true)
| WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Left,
..
}
) {
self.window_order.retain(|id| *id != receiver_id);
self.window_order.push(receiver_id);
}
if matches!(
event,
WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Left,
..
}
) || matches!(event,WindowEvent::KeyboardInput { event,.. } if event.state==ElementState::Pressed && event.logical_key==Key::Named(NamedKey::Escape))
{
self.tab_pressed = None;
self.tab_drag_origin = None;
self.tab_drag_grab = None;
for child in self.children.values_mut() {
child.tab_pressed = None;
child.tab_drag_origin = None;
child.tab_drag_grab = None;
}
self.clear_drag_feedback(None);
self.clear_tab_scroll();
return;
}
let release = matches!(
event,
WindowEvent::MouseInput {
state: ElementState::Released,
button: MouseButton::Left,
..
}
);
if !release && !matches!(event, WindowEvent::CursorMoved { .. }) {
return;
}
let Some(source) = self
.apps()
.find(|app| app.tab_pressed.is_some() && app.tab_drag_origin.is_some())
else {
return;
};
let source_id = source.resource;
let source_window = source.window.as_ref().expect("dragging native window");
let source_scale = source_window.scale_factor();
let origin = source.tab_drag_origin.expect("drag origin");
let receiver = self
.apps()
.find(|app| app.resource == receiver_id)
.expect("event window");
let receiver_window = receiver.window.as_ref().expect("event native window");
let point = if let WindowEvent::CursorMoved { position, .. } = event {
Point::new(
(position.x / receiver_window.scale_factor()) as f32,
(position.y / receiver_window.scale_factor()) as f32,
)
} else {
receiver.pointer
};
let screen = receiver_window.inner_position().ok().map(|position| {
[
position.x as f64 + point.x as f64 * receiver_window.scale_factor(),
position.y as f64 + point.y as f64 * receiver_window.scale_factor(),
]
});
let distance = if let (Some(screen), Ok(start)) = (screen, source_window.inner_position()) {
((screen[0] - start.x as f64 - origin.x as f64 * source_scale)
.hypot(screen[1] - start.y as f64 - origin.y as f64 * source_scale)
/ source_scale) as f32
} else if source_id == receiver_id {
(point.x - origin.x).hypot(point.y - origin.y)
} else {
0.0
};
if distance <= 12.0 {
if release {
self.clear_drag_feedback(None);
}
return;
}
let mut input = source.tab_pressed.clone().expect("pressed tab");
let valid = source.namespace.read().ok().is_some_and(|current| {
let frame = &current.document.frame;
if frame.session != input.session
|| frame.fault.is_some()
|| !frame.tabs.iter().any(|tab| tab.id == input.mount)
{
return false;
}
// Opaque tab identities survive a redraw; use release-time geometry
// while preserving the pressed Page and runtime identity.
input.layout_version = frame.layout_version;
true
});
if !valid {
self.tab_pressed = None;
self.tab_drag_origin = None;
self.tab_drag_grab = None;
for child in self.children.values_mut() {
child.tab_pressed = None;
child.tab_drag_origin = None;
child.tab_drag_grab = None;
}
self.clear_drag_feedback(None);
self.clear_tab_scroll();
return;
}
let mut apps: Vec<_> = self.apps().collect();
apps.sort_by_key(|app| {
self.window_order
.iter()
.position(|id| *id == app.resource)
.unwrap_or(0)
});
let front = apps.into_iter().rev().find_map(|app| {
let window = app.window.as_ref()?;
if window.is_visible() == Some(false) || window.is_minimized() == Some(true) {
return None;
}
let relative = if let Some(screen) = screen {
let start = window.inner_position().ok()?;
Point::new(
((screen[0] - start.x as f64) / window.scale_factor()) as f32,
((screen[1] - start.y as f64) / window.scale_factor()) as f32,
)
} else if app.resource == source_id && receiver_id == source_id {
point
} else {
return None;
};
let size = window.inner_size().to_logical::<f32>(window.scale_factor());
let inside = relative.x >= 0.0
&& relative.y >= 0.0
&& relative.x < size.width
&& relative.y < size.height;
let chrome = screen
.zip(window.outer_position().ok())
.is_some_and(|(screen, outer)| {
let size = window.outer_size();
screen[0] >= outer.x as f64
&& screen[1] >= outer.y as f64
&& screen[0] < outer.x as f64 + size.width as f64
&& screen[1] < outer.y as f64 + size.height as f64
});
(inside || chrome).then_some((app, relative, size, inside))
});
// An obscuring window (including its chrome/developer panel) prevents
// dropping into a lower window that happens to share these coordinates.
let mut scroll = None;
let target = front.and_then(|(app, relative, size, inside)| {
if !inside || app.guest_covered(relative) {
return None;
}
let current = app.namespace.read().ok()?;
let frame = &current.document.frame;
if frame.session != input.session || frame.fault.is_some() {
return None;
}
let index = if frame.tabs.is_empty() {
0
} else {
crate::tabs::insertion_index(app.tab_first, &frame.tabs, size.width, relative)?
};
let direction =
crate::tabs::edge_direction(&frame.tabs, size.width, app.tab_first, relative);
if direction != 0 {
scroll = Some((app.resource, frame.session, relative, direction));
}
Some(crate::events::TabDrop {
window: app.resource,
layout_version: frame.layout_version,
index,
})
});
let position = screen.and_then(|point| {
let anchor = source.tab_drag_grab?;
let inner = source_window.inner_position().ok()?;
let outer = source_window.outer_position().ok()?;
let x = point[0] - anchor.x as f64 * source_scale - (inner.x - outer.x) as f64;
let y = point[1] - anchor.y as f64 * source_scale - (inner.y - outer.y) as f64;
[x, y]
.iter()
.all(|value| value.is_finite() && value.abs() <= 1_000_000.0)
.then_some([x.round() as i32, y.round() as i32])
});
self.tab_scroll_hover = scroll.map(|(window, session, point, direction)| {
let next = self
.tab_scroll_hover
.as_ref()
.filter(|old| {
old.window == window && old.session == session && old.direction == direction
})
.map(|old| old.next)
.unwrap_or_else(|| Instant::now() + Duration::from_millis(350));
TabScrollHover {
window,
session,
point,
direction,
next,
}
});
self.clear_drag_feedback(target.as_ref().map(|target| (target.window, target.index)));
if !release {
return;
}
input.kind = if target.is_some() {
"tab-drop"
} else {
"tab-detach"
}
.into();
input.tab_drop = target;
input.screen_position = if input.tab_drop.is_none() {
position
} else {
None
};
let source = if self.resource == source_id {
&mut *self
} else {
self.children.get_mut(&source_id).expect("drag source")
};
source.tab_pressed = None;
source.tab_drag_origin = None;
source.tab_drag_grab = None;
source.guest_pressed = None;
source.send_guest(input);
self.clear_drag_feedback(None);
self.clear_tab_scroll();
}
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)?);
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.clear_tab_scroll();
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(),
});
}
}
pub fn exit_result(&self) -> anyhow::Result<()> {
if let Some(error) = &self.native_error {
anyhow::bail!("{error}");
}
Ok(())
}
fn redraw(&mut self) {
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 pointer = self.pointer_inside.then_some(self.pointer);
let hover = pointer.and_then(|_| self.tab_hit().map(|input| input.mount));
let guest_cursor = self.guest_hit().is_some() || hover.is_some() || self.new_tab_hit();
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(());
};
self.tab_visual = self.tab_motion.sample(
&namespace.document.frame.tabs,
self.tab_first,
size.width as f32 / window.scale_factor() as f32,
);
let scene = scene::build_scene(
size.width,
size.height,
window.scale_factor() as f32,
&namespace.document,
&mut self.developer_tools,
&self.activity,
crate::tabs::Chrome {
first: self.tab_first,
insertion: self.tab_drop_index,
pointer,
visual: self.tab_visual.clone(),
},
);
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);
buffer
.present()
.map_err(|error| anyhow::anyhow!("{error}"))?;
Ok(())
}
}
impl ApplicationHandler<AppEvent> for TesseraApp {
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) {
event_loop.set_control_flow(ControlFlow::Wait);
self.tick_tab_scroll(event_loop);
let now = Instant::now();
let mut next = match event_loop.control_flow() {
ControlFlow::WaitUntil(deadline) => Some(deadline),
_ => None,
};
for app in std::iter::once(&*self).chain(self.children.values()) {
if let Some(deadline) = app.tab_motion.next_frame {
if now >= deadline {
app.request_redraw();
}
let deadline = deadline.max(now + Duration::from_millis(1));
next = Some(next.map_or(deadline, |previous| previous.min(deadline)));
}
}
if let Some(deadline) = next {
event_loop.set_control_flow(ControlFlow::WaitUntil(deadline));
}
}
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();
}
}
}
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;
}
self.native_tab_drag(window_id, &event);
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_inside = true;
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() || self.new_tab_hit()
{
winit::window::CursorIcon::Pointer
} else {
self.developer_tools
.cursor_kind(&self.layout.developer_tools)
},
);
window.request_redraw();
}
WindowEvent::CursorLeft { .. } => {
self.pointer_inside = false;
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 {
if self.new_tab_hit() {
self.new_tab_pressed = true;
window.request_redraw();
return;
}
if self.pointer.y < crate::tabs::HEIGHT
&& self.pointer.x >= crate::tabs::LEADING
&& self.tab_hit().is_none()
{
let _ = window.drag_window();
}
if self.pointer.y >= crate::tabs::height(&[]) {
self.guest_keyboard = !self.guest_covered(self.pointer);
if self.guest_keyboard {
self.developer_tools.blur();
}
}
self.tab_pressed = self.tab_hit();
self.tab_drag_grab = self
.tab_pressed
.as_ref()
.filter(|input| input.kind == "tab-activate")
.and_then(|input| self.drag_anchor(input));
self.tab_drag_origin = self.tab_drag_grab.map(|_| self.pointer);
self.guest_pressed = self.guest_hit();
self.developer_tools.press(
&self.layout.developer_tools,
self.pointer,
&self.activity,
);
} else {
if self.new_tab_pressed {
self.new_tab_pressed = false;
if self.new_tab_hit() {
self.open_developer_tool("New tab");
}
window.request_redraw();
return;
}
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;
}
if !self.guest_covered(self.pointer) {
let input = self.namespace.read().ok().and_then(|namespace| {
let frame = &namespace.document.frame;
let offset = crate::tabs::height(&frame.tabs);
if frame.session == 0
|| frame.fault.is_some()
|| self.pointer.y < offset
|| namespace.viewport.is_some_and(|s| {
self.pointer.x < 0.0
|| self.pointer.x >= s.width
|| self.pointer.y >= s.height
})
{
return None;
}
let point =
Point::new(self.pointer.x - crate::tabs::RAIL, self.pointer.y - offset);
let delta = [-dx, -dy];
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);
}
}
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(),
_ => {}
}
}
}