1546 lines
59 KiB
Rust
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 = ¤t.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,
|
|
¤t.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 = ¤t.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 = ¤t.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 = ¤t.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 = ¤t.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(),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|