1339 lines
51 KiB
Rust
1339 lines
51 KiB
Rust
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)]
|
|
pub enum AppEvent {
|
|
RuntimeFailed { message: String },
|
|
DocumentUpdated,
|
|
ShutdownComplete { error: Option<String> },
|
|
ConsoleFailed { message: String, source: String },
|
|
}
|
|
|
|
pub struct TesseraApp {
|
|
resource: u64,
|
|
control: crate::api::RuntimeControl,
|
|
exit_requested: bool,
|
|
native_session: u64,
|
|
startup_page_requested: bool,
|
|
native_error: Option<String>,
|
|
children: std::collections::BTreeMap<u64, TesseraApp>,
|
|
proxy: EventLoopProxy<AppEvent>,
|
|
window: Option<Arc<Window>>,
|
|
context: Option<Context<Arc<Window>>>,
|
|
surface: Option<Surface<Arc<Window>, Arc<Window>>>,
|
|
namespace: Arc<RwLock<NamespaceState>>,
|
|
developer_tools: DeveloperToolsState,
|
|
layout: SceneLayout,
|
|
pointer: Point,
|
|
sidebar: crate::sidebar::State,
|
|
sidebar_keyboard: bool,
|
|
panning: bool,
|
|
pan_viewport: Option<u64>,
|
|
orbiting: bool,
|
|
space_down: bool,
|
|
modifiers: ModifiersState,
|
|
activity: Activity,
|
|
event_client: crate::events::Client,
|
|
guest_pressed: Option<crate::events::Input>,
|
|
guest_focus: Option<crate::events::Input>,
|
|
guest_key: Option<(NamedKey, crate::events::Input)>,
|
|
guest_keyboard: bool,
|
|
}
|
|
|
|
impl TesseraApp {
|
|
pub fn new(
|
|
namespace: Arc<RwLock<NamespaceState>>,
|
|
activity: Activity,
|
|
proxy: EventLoopProxy<AppEvent>,
|
|
control: crate::api::RuntimeControl,
|
|
) -> anyhow::Result<Self> {
|
|
let event_client = crate::events::Client::new(proxy.clone(), namespace.clone())?;
|
|
Ok(Self::with_event_client(
|
|
namespace,
|
|
activity,
|
|
proxy,
|
|
control,
|
|
event_client,
|
|
))
|
|
}
|
|
|
|
fn with_event_client(
|
|
namespace: Arc<RwLock<NamespaceState>>,
|
|
activity: Activity,
|
|
proxy: EventLoopProxy<AppEvent>,
|
|
control: crate::api::RuntimeControl,
|
|
event_client: crate::events::Client,
|
|
) -> Self {
|
|
Self {
|
|
resource: crate::window::MAIN_WINDOW,
|
|
control,
|
|
exit_requested: false,
|
|
native_session: 0,
|
|
startup_page_requested: false,
|
|
native_error: None,
|
|
children: std::collections::BTreeMap::new(),
|
|
proxy: proxy.clone(),
|
|
window: None,
|
|
context: None,
|
|
surface: None,
|
|
namespace,
|
|
developer_tools: DeveloperToolsState::new(proxy),
|
|
layout: SceneLayout::default(),
|
|
pointer: Point::default(),
|
|
sidebar: crate::sidebar::State::default(),
|
|
sidebar_keyboard: false,
|
|
panning: false,
|
|
pan_viewport: None,
|
|
orbiting: false,
|
|
space_down: false,
|
|
modifiers: ModifiersState::default(),
|
|
activity,
|
|
event_client,
|
|
guest_pressed: None,
|
|
guest_focus: None,
|
|
guest_key: None,
|
|
guest_keyboard: true,
|
|
}
|
|
}
|
|
|
|
fn reconcile_windows(&mut self, event_loop: &ActiveEventLoop) {
|
|
if self.resource != crate::window::MAIN_WINDOW {
|
|
return;
|
|
}
|
|
let Ok(namespace) = self.namespace.read() else {
|
|
return;
|
|
};
|
|
let session = namespace.runtime_session;
|
|
let revision = namespace.revision;
|
|
let submission = namespace.submission;
|
|
let documents = namespace.document.windows.clone();
|
|
drop(namespace);
|
|
if session != self.native_session {
|
|
// Console reset preserves the old projection with input disabled.
|
|
// A real replacement changes frame sessions and drops old OS views.
|
|
let replaces = documents
|
|
.values()
|
|
.any(|document| document.frame.session == session && session != 0);
|
|
if replaces || (session != 0 && documents.is_empty()) {
|
|
self.children.clear();
|
|
}
|
|
self.native_session = session;
|
|
}
|
|
self.children.retain(|id, child| {
|
|
documents.get(id).is_some_and(|document| {
|
|
document.frame.session == 0 || child.native_session == document.frame.session
|
|
})
|
|
});
|
|
for (id, document) in documents {
|
|
if let Some(child) = self.children.get_mut(&id) {
|
|
if let Ok(mut namespace) = child.namespace.write() {
|
|
namespace.document = document;
|
|
namespace.runtime_session = session;
|
|
namespace.revision = revision;
|
|
namespace.submission = submission;
|
|
}
|
|
child.document_updated(event_loop);
|
|
} else {
|
|
let namespace = Arc::new(RwLock::new(NamespaceState {
|
|
document,
|
|
runtime_session: session,
|
|
revision,
|
|
submission,
|
|
..NamespaceState::default()
|
|
}));
|
|
let mut child = Self::with_event_client(
|
|
namespace,
|
|
self.activity.clone(),
|
|
self.proxy.clone(),
|
|
self.control.clone(),
|
|
self.event_client.clone(),
|
|
);
|
|
child.resource = id;
|
|
child.native_session = session;
|
|
child.resumed(event_loop);
|
|
if let Some(error) = &child.native_error {
|
|
self.developer_tools.input_error = error.clone();
|
|
self.developer_tools.open = true;
|
|
}
|
|
child.document_updated(event_loop);
|
|
self.children.insert(id, child);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn apply_window_commands(&mut self) {
|
|
let Ok(mut namespace) = self.namespace.write() else {
|
|
return;
|
|
};
|
|
let session = namespace.runtime_session;
|
|
let commands = std::mem::take(&mut namespace.window_commands);
|
|
drop(namespace);
|
|
for envelope in commands {
|
|
if envelope.session == 0 || envelope.session != session {
|
|
continue;
|
|
}
|
|
let id = envelope.command.window;
|
|
if matches!(envelope.command.action, crate::window::Action::Close) {
|
|
if id == crate::window::MAIN_WINDOW {
|
|
self.request_exit();
|
|
} else {
|
|
self.children.remove(&id);
|
|
}
|
|
continue;
|
|
}
|
|
let target = if id == self.resource {
|
|
self.window.as_ref()
|
|
} else {
|
|
self.children
|
|
.get(&id)
|
|
.and_then(|child| child.window.as_ref())
|
|
};
|
|
let Some(window) = target else {
|
|
continue;
|
|
};
|
|
match envelope.command.action {
|
|
crate::window::Action::Resize { width, height } => {
|
|
let _ = window.request_inner_size(LogicalSize::new(width, height));
|
|
}
|
|
crate::window::Action::SetTitle(title) => window.set_title(&title),
|
|
crate::window::Action::MoveTo { x, y } => {
|
|
if window.outer_position().is_ok() {
|
|
window.set_outer_position(PhysicalPosition::new(x, y));
|
|
} else {
|
|
let _ = self.proxy.send_event(AppEvent::RuntimeFailed {
|
|
message: format!(
|
|
"Window {id}: desktop placement is unsupported on this platform"
|
|
),
|
|
});
|
|
}
|
|
}
|
|
crate::window::Action::Focus => window.focus_window(),
|
|
crate::window::Action::Close => {}
|
|
}
|
|
}
|
|
self.publish_viewport();
|
|
}
|
|
|
|
fn publish_viewport(&self) {
|
|
let Some(window) = &self.window else {
|
|
return;
|
|
};
|
|
let size = window.inner_size().to_logical::<f32>(window.scale_factor());
|
|
if size.width <= 0.0
|
|
|| size.height <= 0.0
|
|
|| !size.width.is_finite()
|
|
|| !size.height.is_finite()
|
|
{
|
|
return;
|
|
}
|
|
if let Ok(mut namespace) = self.namespace.write() {
|
|
let size = crate::layout::Size {
|
|
width: (size.width - self.sidebar.left())
|
|
.max(0.0)
|
|
.min(crate::ui_bridge::MAX_DIMENSION as f32),
|
|
height: size.height.min(crate::ui_bridge::MAX_DIMENSION as f32),
|
|
};
|
|
namespace.viewport = Some(size);
|
|
namespace.window_viewports.insert(self.resource, size);
|
|
if self.resource == crate::window::MAIN_WINDOW {
|
|
let session = namespace.runtime_session;
|
|
namespace.window_viewports.retain(|id, _| {
|
|
*id == crate::window::MAIN_WINDOW
|
|
|| self
|
|
.children
|
|
.get(id)
|
|
.is_some_and(|child| session != 0 && child.native_session == session)
|
|
});
|
|
for (id, child) in &self.children {
|
|
if child.native_session == session
|
|
&& session != 0
|
|
&& let Ok(local) = child.namespace.read()
|
|
&& local.document.frame.session == session
|
|
&& let Some(size) = local.viewport
|
|
{
|
|
namespace.window_viewports.insert(*id, size);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn request_redraw(&self) {
|
|
if let Some(window) = &self.window {
|
|
window.request_redraw();
|
|
}
|
|
}
|
|
|
|
fn open_developer_tool(&mut self, name: &str) {
|
|
let input = self.namespace.read().ok().map(|namespace| {
|
|
let frame = &namespace.document.frame;
|
|
crate::events::Input {
|
|
window: self.resource,
|
|
session: frame.session,
|
|
layout_version: frame.layout_version,
|
|
mount: 0,
|
|
kind: format!("developer-open:{name}"),
|
|
point: None,
|
|
delta: None,
|
|
press_point: None,
|
|
tab_drop: None,
|
|
screen_position: None,
|
|
}
|
|
});
|
|
if let Some(input) = input {
|
|
if input.session == 0 {
|
|
self.developer_tools.initialize_tools(name);
|
|
} else {
|
|
self.send_guest(input);
|
|
}
|
|
}
|
|
self.guest_keyboard = false;
|
|
self.request_redraw();
|
|
}
|
|
|
|
fn document_updated(&mut self, event_loop: &ActiveEventLoop) {
|
|
self.reconcile_windows(event_loop);
|
|
self.apply_window_commands();
|
|
if let Ok(namespace) = self.namespace.read() {
|
|
self.developer_tools.document_updated(&namespace.document);
|
|
let frame = &namespace.document.frame;
|
|
self.guest_keyboard = !frame
|
|
.tabs
|
|
.iter()
|
|
.any(|tab| tab.active && tab.developer_tool.is_some());
|
|
self.guest_focus =
|
|
frame
|
|
.focus
|
|
.filter(|_| frame.fault.is_none())
|
|
.map(|mount| crate::events::Input {
|
|
window: self.resource,
|
|
session: frame.session,
|
|
layout_version: frame.layout_version,
|
|
mount,
|
|
kind: "focus".into(),
|
|
point: None,
|
|
delta: None,
|
|
press_point: None,
|
|
tab_drop: None,
|
|
screen_position: None,
|
|
});
|
|
}
|
|
self.request_redraw();
|
|
}
|
|
|
|
fn handle_keyboard(&mut self, event: &winit::event::KeyEvent) {
|
|
let command = self.modifiers.super_key() || self.modifiers.control_key();
|
|
if command
|
|
&& event.state == ElementState::Pressed
|
|
&& !event.repeat
|
|
&& matches!(&event.logical_key, Key::Character(key) if key.eq_ignore_ascii_case("b"))
|
|
{
|
|
self.sidebar_action(crate::sidebar::Action::Toggle);
|
|
return;
|
|
}
|
|
if event.logical_key == Key::Named(NamedKey::Escape) {
|
|
self.panning = false;
|
|
self.space_down = false;
|
|
self.guest_pressed = None;
|
|
}
|
|
if event.logical_key == Key::Named(NamedKey::Space)
|
|
&& self.guest_focus.is_none()
|
|
&& self
|
|
.namespace
|
|
.read()
|
|
.is_ok_and(|n| n.document.frame.surface.mode == crate::page::Mode::Canvas)
|
|
{
|
|
self.space_down = event.state == ElementState::Pressed;
|
|
if !self.space_down {
|
|
self.panning = false;
|
|
}
|
|
return;
|
|
}
|
|
if self.sidebar_keyboard {
|
|
if event.logical_key == Key::Named(NamedKey::Tab)
|
|
&& event.state == ElementState::Pressed
|
|
{
|
|
self.sidebar
|
|
.interaction
|
|
.focus_next(&self.sidebar.targets, self.modifiers.shift_key());
|
|
self.request_redraw();
|
|
return;
|
|
}
|
|
if matches!(
|
|
event.logical_key,
|
|
Key::Named(NamedKey::Enter | NamedKey::Space)
|
|
) {
|
|
if event.state == ElementState::Pressed {
|
|
self.sidebar.interaction.key_press(&self.sidebar.targets);
|
|
} else if let Some(action) =
|
|
self.sidebar.interaction.key_release(&self.sidebar.targets)
|
|
{
|
|
self.sidebar_action(action);
|
|
}
|
|
self.request_redraw();
|
|
return;
|
|
}
|
|
}
|
|
if event.state == ElementState::Pressed
|
|
&& !event.repeat
|
|
&& (self.modifiers.super_key() || self.modifiers.control_key())
|
|
&& matches!(&event.logical_key, Key::Character(key) if key.eq_ignore_ascii_case("t"))
|
|
{
|
|
self.open_developer_tool("New tab");
|
|
return;
|
|
}
|
|
|
|
if event.state == ElementState::Pressed && !event.repeat {
|
|
let close = (self.modifiers.super_key() || self.modifiers.control_key())
|
|
&& matches!(&event.logical_key, Key::Character(key) if key.eq_ignore_ascii_case("w"));
|
|
let cycle = self.modifiers.control_key()
|
|
&& matches!(event.logical_key, Key::Named(NamedKey::Tab));
|
|
if (close || cycle) && self.send_tab_shortcut(close) {
|
|
return;
|
|
}
|
|
}
|
|
if self.guest_keyboard && self.handle_guest_key(event) {
|
|
self.request_redraw();
|
|
return;
|
|
}
|
|
if self.developer_tools.keyboard_control(
|
|
event,
|
|
self.modifiers.shift_key(),
|
|
&self.layout.developer_tools,
|
|
&self.activity,
|
|
) {
|
|
self.request_redraw();
|
|
return;
|
|
}
|
|
if event.state != ElementState::Pressed {
|
|
return;
|
|
}
|
|
let is_f12 = matches!(event.logical_key, Key::Named(NamedKey::F12));
|
|
let is_i =
|
|
matches!(&event.logical_key, Key::Character(value) if value.eq_ignore_ascii_case("i"));
|
|
let mac_shortcut = self.modifiers.super_key()
|
|
&& (self.modifiers.alt_key() || self.modifiers.shift_key())
|
|
&& is_i;
|
|
let other_shortcut = self.modifiers.control_key() && self.modifiers.shift_key() && is_i;
|
|
if is_f12 || mac_shortcut || other_shortcut {
|
|
if !event.repeat {
|
|
self.open_developer_tool("Console");
|
|
}
|
|
return;
|
|
}
|
|
if !self.developer_tools.open || self.developer_tools.focus.is_none() {
|
|
return;
|
|
}
|
|
let command = self.modifiers.super_key() || self.modifiers.control_key();
|
|
if command && let Key::Character(value) = &event.logical_key {
|
|
match value.to_lowercase().as_str() {
|
|
"a" => self.developer_tools.selection = true,
|
|
"v" => match crate::clipboard::read() {
|
|
Ok(value) => self.developer_tools.insert(&value),
|
|
Err(error) => self.developer_tools.input_error = format!("Clipboard: {error}"),
|
|
},
|
|
"c" | "x" => {
|
|
let selected = self.developer_tools.selected_text();
|
|
if !selected.is_empty() {
|
|
match crate::clipboard::write(&selected) {
|
|
Ok(()) if value.eq_ignore_ascii_case("x") => {
|
|
self.developer_tools.insert("")
|
|
}
|
|
Ok(()) => {}
|
|
Err(error) => {
|
|
self.developer_tools.input_error = format!("Clipboard: {error}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
} else if let Key::Named(key) = &event.logical_key {
|
|
if !self.developer_tools.preedit.is_empty() {
|
|
return;
|
|
}
|
|
self.developer_tools
|
|
.key(key, self.modifiers.shift_key(), command);
|
|
} else if !command
|
|
&& self.developer_tools.preedit.is_empty()
|
|
&& let Some(text) = &event.text
|
|
{
|
|
let text: String = text.chars().filter(|c| !c.is_control()).collect();
|
|
self.developer_tools.insert(&text);
|
|
}
|
|
self.request_redraw();
|
|
}
|
|
|
|
fn guest_covered(&self, point: Point) -> bool {
|
|
if self.sidebar.covers(point) {
|
|
return true;
|
|
}
|
|
let panel = self.layout.developer_tools.panel;
|
|
self.developer_tools.open
|
|
&& crate::geometry::Rect::new(
|
|
panel.x - 6.0,
|
|
panel.y - 6.0,
|
|
panel.width + 12.0,
|
|
panel.height + 12.0,
|
|
)
|
|
.contains(point)
|
|
}
|
|
|
|
fn guest_point(&self) -> Option<Point> {
|
|
if self.guest_covered(self.pointer) {
|
|
return None;
|
|
}
|
|
let current = self.namespace.read().ok()?;
|
|
let point = Point::new(self.pointer.x - self.sidebar.left(), self.pointer.y);
|
|
if point.x < 0.0
|
|
|| point.y < 0.0
|
|
|| current
|
|
.viewport
|
|
.is_some_and(|size| point.x >= size.width || point.y >= size.height)
|
|
{
|
|
return None;
|
|
}
|
|
Some(current.document.frame.surface.to_world(point))
|
|
}
|
|
|
|
fn guest_hit(&self) -> Option<crate::events::Input> {
|
|
let point = self.guest_point()?;
|
|
let current = self.namespace.read().ok()?;
|
|
let frame = ¤t.document.frame;
|
|
if frame.session == 0 {
|
|
return None;
|
|
}
|
|
let mount = frame.hit_test(point)?;
|
|
Some(crate::events::Input {
|
|
window: self.resource,
|
|
session: frame.session,
|
|
layout_version: frame.layout_version,
|
|
mount: mount.id,
|
|
kind: "click".into(),
|
|
point: Some([point.x, point.y]),
|
|
press_point: Some([point.x, point.y]),
|
|
delta: None,
|
|
tab_drop: None,
|
|
screen_position: None,
|
|
})
|
|
}
|
|
|
|
fn send_tab_shortcut(&mut self, close: bool) -> bool {
|
|
let input = self.namespace.read().ok().and_then(|namespace| {
|
|
let frame = &namespace.document.frame;
|
|
if frame.session == 0 || frame.fault.is_some() || frame.tabs.is_empty() {
|
|
return None;
|
|
}
|
|
let active = frame.tabs.iter().position(|tab| tab.active).unwrap_or(0);
|
|
let index = if close {
|
|
active
|
|
} else if self.modifiers.shift_key() {
|
|
(active + frame.tabs.len() - 1) % frame.tabs.len()
|
|
} else {
|
|
(active + 1) % frame.tabs.len()
|
|
};
|
|
Some(crate::events::Input {
|
|
window: self.resource,
|
|
session: frame.session,
|
|
layout_version: frame.layout_version,
|
|
mount: frame.tabs[index].id,
|
|
kind: if close { "tab-close" } else { "tab-activate" }.into(),
|
|
point: None,
|
|
delta: None,
|
|
press_point: None,
|
|
tab_drop: None,
|
|
screen_position: None,
|
|
})
|
|
});
|
|
if let Some(input) = input {
|
|
self.guest_pressed = None;
|
|
self.guest_key = None;
|
|
self.send_guest(input);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn sidebar_action(&mut self, action: crate::sidebar::Action) {
|
|
use crate::sidebar::Action;
|
|
match action {
|
|
Action::Toggle => {
|
|
self.sidebar.open = !self.sidebar.open;
|
|
self.sidebar.interaction.cancel();
|
|
self.sidebar_keyboard = self.sidebar.open;
|
|
self.guest_pressed = None;
|
|
self.publish_viewport();
|
|
}
|
|
Action::NewPage => self.open_developer_tool("New tab"),
|
|
Action::Tool(name) => self.open_developer_tool(name),
|
|
Action::Activate(id) | Action::Close(id) => {
|
|
let input = self
|
|
.namespace
|
|
.read()
|
|
.ok()
|
|
.map(|current| crate::events::Input {
|
|
window: self.resource,
|
|
session: current.document.frame.session,
|
|
layout_version: current.document.frame.layout_version,
|
|
mount: id,
|
|
kind: if matches!(action, Action::Close(_)) {
|
|
"tab-close"
|
|
} else {
|
|
"tab-activate"
|
|
}
|
|
.into(),
|
|
point: None,
|
|
press_point: None,
|
|
delta: None,
|
|
tab_drop: None,
|
|
screen_position: None,
|
|
});
|
|
if let Some(input) = input {
|
|
self.send_guest(input);
|
|
}
|
|
}
|
|
}
|
|
self.request_redraw();
|
|
}
|
|
|
|
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);
|
|
let viewport = if self.panning {
|
|
self.pan_viewport.and_then(|id| {
|
|
frame.mounts.iter().find(|mount| {
|
|
mount.id == id
|
|
&& (mount.canvas_viewport
|
|
|| mount.scene3d_viewport && mount.camera_controls)
|
|
})
|
|
})
|
|
} else {
|
|
frame.camera_at(world)
|
|
};
|
|
if self.panning && self.pan_viewport.is_some() && viewport.is_none() {
|
|
return None;
|
|
}
|
|
if let Some(viewport) = viewport {
|
|
return Some(crate::events::Input {
|
|
window: self.resource,
|
|
session: frame.session,
|
|
layout_version: frame.layout_version,
|
|
mount: viewport.id,
|
|
kind: if viewport.scene3d_viewport {
|
|
if kind == "page-zoom" {
|
|
"scene-zoom"
|
|
} else if self.orbiting {
|
|
"scene-orbit"
|
|
} else {
|
|
"scene-pan"
|
|
}
|
|
} else if kind == "page-zoom" {
|
|
"viewport-zoom"
|
|
} else {
|
|
"viewport-pan"
|
|
}
|
|
.into(),
|
|
point: Some([world.x, world.y]),
|
|
press_point: None,
|
|
delta: Some([
|
|
delta[0]
|
|
/ if kind == "page-pan" {
|
|
frame.surface.zoom
|
|
} else {
|
|
1.0
|
|
},
|
|
delta[1] / frame.surface.zoom,
|
|
]),
|
|
tab_drop: None,
|
|
screen_position: None,
|
|
});
|
|
}
|
|
if frame.surface.mode == crate::page::Mode::Window
|
|
|| kind == "page-zoom" && frame.surface.mode != crate::page::Mode::Canvas
|
|
{
|
|
return None;
|
|
}
|
|
Some(crate::events::Input {
|
|
window: self.resource,
|
|
session: frame.session,
|
|
layout_version: frame.layout_version,
|
|
mount: 0,
|
|
kind: kind.into(),
|
|
point: (kind == "page-zoom").then_some([point.x, point.y]),
|
|
press_point: None,
|
|
delta: Some(delta),
|
|
tab_drop: None,
|
|
screen_position: None,
|
|
})
|
|
});
|
|
if let Some(input) = input {
|
|
self.guest_pressed = None;
|
|
self.send_guest(input);
|
|
}
|
|
}
|
|
|
|
fn send_guest(&mut self, event: crate::events::Input) {
|
|
if !self.event_client.send(event) {
|
|
self.developer_tools.input_error = "Native event queue is unavailable or full".into();
|
|
self.developer_tools.open = true;
|
|
}
|
|
}
|
|
|
|
fn handle_guest_key(&mut self, event: &winit::event::KeyEvent) -> bool {
|
|
let Key::Named(key) = &event.logical_key else {
|
|
return false;
|
|
};
|
|
if *key == NamedKey::Tab {
|
|
if event.state == ElementState::Pressed && !event.repeat {
|
|
let next = self.namespace.read().ok().and_then(|namespace| {
|
|
let frame = &namespace.document.frame;
|
|
if frame.session == 0 || frame.fault.is_some() {
|
|
return None;
|
|
}
|
|
let targets: Vec<_> = frame
|
|
.mounts
|
|
.iter()
|
|
.filter(|mount| mount.interactive)
|
|
.collect();
|
|
if targets.is_empty() {
|
|
return None;
|
|
}
|
|
let current = self
|
|
.guest_focus
|
|
.as_ref()
|
|
.and_then(|focus| targets.iter().position(|mount| mount.id == focus.mount));
|
|
let index = if self.modifiers.shift_key() {
|
|
current
|
|
.map(|i| (i + targets.len() - 1) % targets.len())
|
|
.unwrap_or(targets.len() - 1)
|
|
} else {
|
|
current.map(|i| (i + 1) % targets.len()).unwrap_or(0)
|
|
};
|
|
Some(crate::events::Input {
|
|
window: self.resource,
|
|
session: frame.session,
|
|
layout_version: frame.layout_version,
|
|
mount: targets[index].id,
|
|
kind: "focus".into(),
|
|
point: None,
|
|
delta: None,
|
|
press_point: None,
|
|
tab_drop: None,
|
|
screen_position: None,
|
|
})
|
|
});
|
|
if let Some(next) = next {
|
|
self.guest_key = None;
|
|
self.guest_focus = Some(next.clone());
|
|
self.send_guest(next);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
return self.guest_focus.is_some();
|
|
}
|
|
if matches!(key, NamedKey::Enter | NamedKey::Space) && self.guest_focus.is_some() {
|
|
if event.state == ElementState::Pressed {
|
|
if !event.repeat {
|
|
self.guest_key = self.guest_focus.clone().map(|focus| (*key, focus));
|
|
}
|
|
} else if let Some((pressed, mut input)) = self.guest_key.take()
|
|
&& pressed == *key
|
|
{
|
|
input.kind = "click".into();
|
|
self.send_guest(input);
|
|
}
|
|
return true;
|
|
}
|
|
false
|
|
}
|
|
|
|
fn create_native(&mut self, event_loop: &ActiveEventLoop) -> anyhow::Result<()> {
|
|
if self.window.is_some() {
|
|
return Ok(());
|
|
}
|
|
|
|
let attributes = WindowAttributes::default()
|
|
.with_title(if self.resource == crate::window::MAIN_WINDOW {
|
|
"Tessera".into()
|
|
} else {
|
|
format!("Endor Window {}", self.resource)
|
|
})
|
|
.with_inner_size(if self.resource == crate::window::MAIN_WINDOW {
|
|
LogicalSize::new(1_280.0, 800.0)
|
|
} else {
|
|
LogicalSize::new(960.0, 640.0)
|
|
})
|
|
.with_min_inner_size(LogicalSize::new(720.0, 480.0))
|
|
.with_max_inner_size(LogicalSize::new(
|
|
crate::window::MAX_WINDOW_DIMENSION,
|
|
crate::window::MAX_WINDOW_DIMENSION,
|
|
));
|
|
#[cfg(target_os = "macos")]
|
|
let attributes = {
|
|
use winit::platform::macos::WindowAttributesExtMacOS;
|
|
attributes
|
|
.with_title_hidden(true)
|
|
.with_titlebar_transparent(true)
|
|
.with_fullsize_content_view(true)
|
|
};
|
|
let window = Arc::new(event_loop.create_window(attributes)?);
|
|
#[cfg(target_os = "macos")]
|
|
crate::native_chrome::align_controls(&window);
|
|
let context = Context::new(window.clone()).map_err(|error| anyhow::anyhow!("{error}"))?;
|
|
let surface =
|
|
Surface::new(&context, window.clone()).map_err(|error| anyhow::anyhow!("{error}"))?;
|
|
|
|
window.set_ime_allowed(true);
|
|
self.window = Some(window.clone());
|
|
self.context = Some(context);
|
|
self.surface = Some(surface);
|
|
self.apply_window_commands();
|
|
self.publish_viewport();
|
|
window.request_redraw();
|
|
self.native_error = None;
|
|
Ok(())
|
|
}
|
|
|
|
fn request_exit(&mut self) {
|
|
if self.exit_requested {
|
|
return;
|
|
}
|
|
self.guest_pressed = None;
|
|
self.guest_key = None;
|
|
if self.control.request_shutdown() {
|
|
self.exit_requested = true;
|
|
} else {
|
|
self.native_error = Some("VM worker unavailable during shutdown".into());
|
|
let _ = self.proxy.send_event(AppEvent::ShutdownComplete {
|
|
error: self.native_error.clone(),
|
|
});
|
|
}
|
|
}
|
|
|
|
pub fn exit_result(&self) -> anyhow::Result<()> {
|
|
if let Some(error) = &self.native_error {
|
|
anyhow::bail!("{error}");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn redraw(&mut self) {
|
|
#[cfg(target_os = "macos")]
|
|
if let Some(window) = self.window.as_ref() {
|
|
crate::native_chrome::align_controls(window);
|
|
}
|
|
match self.try_redraw() {
|
|
Ok(()) => {
|
|
if self.window.is_some() && self.surface.is_some() {
|
|
self.native_error = None;
|
|
}
|
|
}
|
|
Err(error) => {
|
|
let message = format!("Window {} redraw failed: {error:#}", self.resource);
|
|
if self.native_error.as_ref() != Some(&message) {
|
|
self.native_error = Some(message.clone());
|
|
let _ = self.proxy.send_event(AppEvent::RuntimeFailed { message });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn try_redraw(&mut self) -> anyhow::Result<()> {
|
|
let guest_cursor = self.guest_hit().is_some()
|
|
|| self
|
|
.sidebar
|
|
.interaction
|
|
.hit(&self.sidebar.targets)
|
|
.is_some();
|
|
let (Some(window), Some(surface)) = (self.window.as_ref(), self.surface.as_mut()) else {
|
|
return Ok(());
|
|
};
|
|
let size = window.inner_size();
|
|
let (Some(width), Some(height)) =
|
|
(NonZeroU32::new(size.width), NonZeroU32::new(size.height))
|
|
else {
|
|
return Ok(());
|
|
};
|
|
|
|
let Ok(namespace) = self.namespace.read() else {
|
|
return Ok(());
|
|
};
|
|
let scene = scene::build_scene(
|
|
size.width,
|
|
size.height,
|
|
window.scale_factor() as f32,
|
|
&namespace.document,
|
|
&mut self.developer_tools,
|
|
&self.activity,
|
|
&mut self.sidebar,
|
|
);
|
|
drop(namespace);
|
|
self.layout = scene.layout;
|
|
window.set_cursor(if guest_cursor {
|
|
winit::window::CursorIcon::Pointer
|
|
} else {
|
|
self.developer_tools
|
|
.cursor_kind(&self.layout.developer_tools)
|
|
});
|
|
if self.developer_tools.repaint {
|
|
self.developer_tools.repaint = false;
|
|
window.request_redraw();
|
|
}
|
|
let ime = self
|
|
.developer_tools
|
|
.ime_position(&self.layout.developer_tools);
|
|
window.set_ime_cursor_area(
|
|
LogicalPosition::new(ime.x as f64, ime.y as f64),
|
|
LogicalSize::new(1.0, 19.0),
|
|
);
|
|
|
|
surface
|
|
.resize(width, height)
|
|
.map_err(|error| anyhow::anyhow!("{error}"))?;
|
|
let mut buffer = surface
|
|
.buffer_mut()
|
|
.map_err(|error| anyhow::anyhow!("{error}"))?;
|
|
SoftwareRenderer::render(&scene.display_list, &mut buffer, size.width, size.height)
|
|
.map_err(anyhow::Error::msg)?;
|
|
buffer
|
|
.present()
|
|
.map_err(|error| anyhow::anyhow!("{error}"))?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl ApplicationHandler<AppEvent> for TesseraApp {
|
|
fn exiting(&mut self, _: &ActiveEventLoop) {
|
|
// OS termination can bypass CloseRequested. The loop is already ending;
|
|
// use a bounded fallback rather than silently dropping every callback.
|
|
if !self.control.request_shutdown() {
|
|
self.native_error = Some("VM worker unavailable during shutdown".into());
|
|
return;
|
|
}
|
|
match self.control.wait_shutdown(Duration::from_secs(5)) {
|
|
Some(outcome) => {
|
|
if let Some(error) = outcome.error {
|
|
self.native_error = Some(error);
|
|
}
|
|
}
|
|
None => self.native_error = Some("VM shutdown did not finish within 5 seconds".into()),
|
|
}
|
|
}
|
|
|
|
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {}
|
|
|
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
|
if self.resource == crate::window::MAIN_WINDOW
|
|
&& !self.startup_page_requested
|
|
&& self
|
|
.namespace
|
|
.read()
|
|
.is_ok_and(|n| n.runtime_session == 0 && n.document.frame.tabs.is_empty())
|
|
{
|
|
self.startup_page_requested = true;
|
|
self.developer_tools.initialize_tools("New tab");
|
|
}
|
|
|
|
if let Err(error) = self.create_native(event_loop) {
|
|
self.native_error = Some(format!(
|
|
"Window {} initialization failed: {error:#}",
|
|
self.resource
|
|
));
|
|
if self.resource == crate::window::MAIN_WINDOW {
|
|
event_loop.exit();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: AppEvent) {
|
|
if self.control.is_exiting() && !matches!(&event, AppEvent::ShutdownComplete { .. }) {
|
|
return;
|
|
}
|
|
match event {
|
|
AppEvent::ShutdownComplete { error } => {
|
|
if let Some(error) = error {
|
|
self.native_error = Some(error);
|
|
}
|
|
event_loop.exit();
|
|
}
|
|
AppEvent::RuntimeFailed { message } => {
|
|
self.developer_tools.input_error = message;
|
|
self.developer_tools.open = true;
|
|
self.guest_pressed = None;
|
|
self.guest_key = None;
|
|
self.request_redraw();
|
|
}
|
|
AppEvent::DocumentUpdated => self.document_updated(event_loop),
|
|
AppEvent::ConsoleFailed { message, source } => {
|
|
self.developer_tools.input_error = format!("Request failed: {message}");
|
|
if self.developer_tools.input.is_empty() {
|
|
self.developer_tools.input = source;
|
|
self.developer_tools.cursor = self.developer_tools.input.len();
|
|
self.developer_tools.reveal_cursor();
|
|
}
|
|
self.request_redraw();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn window_event(
|
|
&mut self,
|
|
_event_loop: &ActiveEventLoop,
|
|
window_id: WindowId,
|
|
event: WindowEvent,
|
|
) {
|
|
if self.control.is_exiting() {
|
|
return;
|
|
}
|
|
let Some(window) = self.window.as_ref().cloned() else {
|
|
return;
|
|
};
|
|
if window.id() != window_id {
|
|
if let Some(child) = self.children.values_mut().find(|child| {
|
|
child
|
|
.window
|
|
.as_ref()
|
|
.is_some_and(|window| window.id() == window_id)
|
|
}) {
|
|
child.window_event(_event_loop, window_id, event);
|
|
self.publish_viewport();
|
|
}
|
|
return;
|
|
}
|
|
|
|
match event {
|
|
WindowEvent::CloseRequested => {
|
|
if self.resource == crate::window::MAIN_WINDOW {
|
|
self.request_exit();
|
|
} else {
|
|
let input = self
|
|
.namespace
|
|
.read()
|
|
.ok()
|
|
.map(|namespace| crate::events::Input {
|
|
window: self.resource,
|
|
session: namespace.document.frame.session,
|
|
layout_version: namespace.document.frame.layout_version,
|
|
mount: 0,
|
|
kind: "window-close".into(),
|
|
point: None,
|
|
delta: None,
|
|
press_point: None,
|
|
tab_drop: None,
|
|
screen_position: None,
|
|
});
|
|
if let Some(input) = input {
|
|
self.send_guest(input);
|
|
}
|
|
}
|
|
}
|
|
WindowEvent::Resized(_) | WindowEvent::ScaleFactorChanged { .. } => {
|
|
self.publish_viewport();
|
|
window.request_redraw();
|
|
}
|
|
WindowEvent::CursorMoved {
|
|
position: PhysicalPosition { x, y },
|
|
..
|
|
} => {
|
|
let scale = window.scale_factor() as f32;
|
|
let next = Point::new(x as f32 / scale, y as f32 / scale);
|
|
let delta = Point::new(next.x - self.pointer.x, next.y - self.pointer.y);
|
|
self.pointer = next;
|
|
self.sidebar
|
|
.interaction
|
|
.pointer_moved(next, &self.sidebar.targets);
|
|
if !self.panning
|
|
&& let Some(press) = &self.guest_pressed
|
|
&& let Some(point) = self.guest_point()
|
|
&& let Some(start) = press.point
|
|
{
|
|
let is_3d = self.namespace.read().is_ok_and(|current| {
|
|
current.document.frame.mounts.iter().any(|mount| {
|
|
mount.id == press.mount
|
|
&& mount.scene3d_viewport
|
|
&& mount.camera_controls
|
|
})
|
|
});
|
|
if is_3d && (point.x - start[0]).hypot(point.y - start[1]) > 3. {
|
|
self.orbiting = true;
|
|
self.panning = self.begin_pan();
|
|
self.guest_pressed = None;
|
|
}
|
|
}
|
|
if self.panning {
|
|
self.page_view("page-pan", [-delta.x, -delta.y]);
|
|
window.request_redraw();
|
|
return;
|
|
}
|
|
self.developer_tools.move_pointer(
|
|
next,
|
|
delta,
|
|
&self.layout.developer_tools,
|
|
window.inner_size().width as f32 / scale,
|
|
);
|
|
window.set_cursor(
|
|
if self.guest_hit().is_some()
|
|
|| self
|
|
.sidebar
|
|
.interaction
|
|
.hit(&self.sidebar.targets)
|
|
.is_some()
|
|
{
|
|
winit::window::CursorIcon::Pointer
|
|
} else {
|
|
self.developer_tools
|
|
.cursor_kind(&self.layout.developer_tools)
|
|
},
|
|
);
|
|
window.request_redraw();
|
|
}
|
|
WindowEvent::CursorLeft { .. } => {
|
|
self.guest_pressed = None;
|
|
self.panning = false;
|
|
self.sidebar.interaction.pointer_left();
|
|
self.developer_tools.pointer_left();
|
|
window.set_cursor(winit::window::CursorIcon::Default);
|
|
window.request_redraw();
|
|
}
|
|
WindowEvent::MouseInput {
|
|
state,
|
|
button: MouseButton::Middle,
|
|
..
|
|
} => {
|
|
self.orbiting = false;
|
|
self.panning = state == ElementState::Pressed && self.begin_pan();
|
|
self.guest_pressed = None;
|
|
window.request_redraw();
|
|
}
|
|
WindowEvent::MouseInput {
|
|
state,
|
|
button: MouseButton::Left,
|
|
..
|
|
} => {
|
|
if state == ElementState::Pressed {
|
|
if self.sidebar.covers(self.pointer) {
|
|
self.sidebar_keyboard = true;
|
|
self.sidebar.interaction.press(&self.sidebar.targets);
|
|
self.guest_pressed = None;
|
|
if self.sidebar.open
|
|
&& self.pointer.y < crate::sidebar::HEADER
|
|
&& self.pointer.x >= crate::sidebar::CONTROLS
|
|
&& self
|
|
.sidebar
|
|
.interaction
|
|
.hit(&self.sidebar.targets)
|
|
.is_none()
|
|
{
|
|
#[cfg(target_os = "macos")]
|
|
if crate::native_chrome::titlebar_double_click(&window) {
|
|
return;
|
|
}
|
|
let _ = window.drag_window();
|
|
}
|
|
} else if self.space_down && self.guest_point().is_some() {
|
|
self.orbiting = false;
|
|
self.panning = self.begin_pan();
|
|
self.guest_pressed = None;
|
|
} else {
|
|
self.sidebar_keyboard = false;
|
|
self.sidebar.interaction.blur();
|
|
self.guest_keyboard = !self.guest_covered(self.pointer);
|
|
if self.guest_keyboard {
|
|
self.developer_tools.blur();
|
|
}
|
|
self.guest_pressed = self.guest_hit();
|
|
self.developer_tools.press(
|
|
&self.layout.developer_tools,
|
|
self.pointer,
|
|
&self.activity,
|
|
);
|
|
}
|
|
} else {
|
|
if self.panning {
|
|
self.panning = false;
|
|
self.guest_pressed = None;
|
|
window.request_redraw();
|
|
return;
|
|
}
|
|
if let Some(action) = self.sidebar.interaction.release(&self.sidebar.targets) {
|
|
self.sidebar_action(action);
|
|
} else if let Some(pressed) = self.guest_pressed.take()
|
|
&& let Some(mut released) = self.guest_hit()
|
|
&& released.session == pressed.session
|
|
&& released.layout_version == pressed.layout_version
|
|
{
|
|
released.press_point = pressed.point;
|
|
self.send_guest(released);
|
|
}
|
|
self.developer_tools.release(
|
|
&self.layout.developer_tools,
|
|
self.pointer,
|
|
&self.activity,
|
|
);
|
|
}
|
|
window.request_redraw();
|
|
}
|
|
WindowEvent::PinchGesture { delta, .. } => {
|
|
self.page_view("page-zoom", [(delta as f32).exp(), 0.0]);
|
|
window.request_redraw();
|
|
}
|
|
WindowEvent::MouseWheel { delta, .. } => {
|
|
let (mut dx, mut dy) = match delta {
|
|
MouseScrollDelta::LineDelta(x, y) => (x * 38.0, y * 38.0),
|
|
MouseScrollDelta::PixelDelta(p) => (
|
|
p.x as f32 / window.scale_factor() as f32,
|
|
p.y as f32 / window.scale_factor() as f32,
|
|
),
|
|
};
|
|
if self.modifiers.shift_key() && dx.abs() < 0.1 {
|
|
dx = dy;
|
|
dy = 0.0;
|
|
}
|
|
if self.sidebar.covers(self.pointer) {
|
|
if self.sidebar.open && self.pointer.y >= 333.0 {
|
|
self.sidebar.scroll -= dy;
|
|
}
|
|
window.request_redraw();
|
|
return;
|
|
}
|
|
let point = self.guest_point();
|
|
let canvas = self.namespace.read().is_ok_and(|n| {
|
|
n.document.frame.surface.mode == crate::page::Mode::Canvas
|
|
|| point.is_some_and(|point| n.document.frame.camera_at(point).is_some())
|
|
});
|
|
if canvas && (self.modifiers.super_key() || self.modifiers.control_key()) {
|
|
self.page_view("page-zoom", [(dy * 0.005).exp().clamp(0.1, 10.0), 0.0]);
|
|
window.request_redraw();
|
|
return;
|
|
}
|
|
let scene3d = self.namespace.read().is_ok_and(|current| {
|
|
point
|
|
.and_then(|point| current.document.frame.camera_at(point))
|
|
.is_some_and(|mount| mount.scene3d_viewport)
|
|
});
|
|
let point = self.guest_point();
|
|
let input = point.and_then(|point| {
|
|
self.namespace.read().ok().and_then(|namespace| {
|
|
let frame = &namespace.document.frame;
|
|
if frame.session == 0 || frame.fault.is_some() {
|
|
return None;
|
|
}
|
|
let delta = [-dx / frame.surface.zoom, -dy / frame.surface.zoom];
|
|
let mount = crate::scroll::wheel_target(frame, point, delta)?;
|
|
Some(crate::events::Input {
|
|
window: self.resource,
|
|
session: frame.session,
|
|
layout_version: frame.layout_version,
|
|
mount,
|
|
kind: "scroll".into(),
|
|
point: Some([point.x, point.y]),
|
|
press_point: None,
|
|
tab_drop: None,
|
|
screen_position: None,
|
|
delta: Some(delta),
|
|
})
|
|
})
|
|
});
|
|
if let Some(input) = input {
|
|
self.send_guest(input);
|
|
} else if scene3d {
|
|
self.page_view("page-zoom", [(dy * 0.005).exp().clamp(0.1, 10.), 0.]);
|
|
} else {
|
|
let horizontal = self.namespace.read().is_ok_and(|n| {
|
|
n.document.frame.surface.mode == crate::page::Mode::Horizontal
|
|
});
|
|
self.page_view(
|
|
"page-pan",
|
|
if horizontal && dx.abs() < 0.1 {
|
|
[-dy, 0.0]
|
|
} else {
|
|
[-dx, -dy]
|
|
},
|
|
);
|
|
}
|
|
self.developer_tools
|
|
.wheel(&self.layout.developer_tools, self.pointer, dx, dy);
|
|
window.request_redraw();
|
|
}
|
|
WindowEvent::Ime(Ime::Preedit(value, _)) => {
|
|
self.developer_tools.preedit = value;
|
|
window.request_redraw();
|
|
}
|
|
WindowEvent::Ime(Ime::Commit(value)) => {
|
|
self.developer_tools.preedit.clear();
|
|
if self.developer_tools.open {
|
|
self.developer_tools.insert(&value);
|
|
}
|
|
window.request_redraw();
|
|
}
|
|
WindowEvent::Focused(false) => {
|
|
self.guest_pressed = None;
|
|
self.guest_key = None;
|
|
self.developer_tools.blur();
|
|
self.sidebar.interaction.blur();
|
|
self.panning = false;
|
|
self.space_down = false;
|
|
self.modifiers = ModifiersState::default();
|
|
window.request_redraw();
|
|
}
|
|
WindowEvent::ModifiersChanged(modifiers) => self.modifiers = modifiers.state(),
|
|
WindowEvent::KeyboardInput { event, .. } => self.handle_keyboard(&event),
|
|
WindowEvent::RedrawRequested => self.redraw(),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|