570 lines
21 KiB
Rust
570 lines
21 KiB
Rust
use std::{
|
|
num::NonZeroU32,
|
|
sync::{Arc, RwLock},
|
|
};
|
|
|
|
use softbuffer::{Context, Surface};
|
|
use winit::{
|
|
application::ApplicationHandler,
|
|
dpi::{LogicalPosition, LogicalSize, PhysicalPosition},
|
|
event::{ElementState, Ime, MouseButton, MouseScrollDelta, WindowEvent},
|
|
event_loop::{ActiveEventLoop, EventLoopProxy},
|
|
keyboard::{Key, ModifiersState, NamedKey},
|
|
window::{Window, WindowAttributes, WindowId},
|
|
};
|
|
|
|
use crate::{
|
|
activity::Activity,
|
|
developer_tools::DeveloperToolsState,
|
|
document::NamespaceState,
|
|
geometry::Point,
|
|
scene::{self, SceneLayout},
|
|
software_renderer::SoftwareRenderer,
|
|
};
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum AppEvent {
|
|
RuntimeFailed { message: String },
|
|
DocumentUpdated,
|
|
ConsoleFailed { message: String, source: String },
|
|
}
|
|
|
|
pub struct TesseraApp {
|
|
window: Option<Arc<Window>>,
|
|
context: Option<Context<Arc<Window>>>,
|
|
surface: Option<Surface<Arc<Window>, Arc<Window>>>,
|
|
namespace: Arc<RwLock<NamespaceState>>,
|
|
developer_tools: DeveloperToolsState,
|
|
layout: SceneLayout,
|
|
pointer: Point,
|
|
modifiers: ModifiersState,
|
|
activity: Activity,
|
|
event_client: crate::events::Client,
|
|
guest_pressed: Option<crate::events::Input>,
|
|
guest_focus: Option<crate::events::Input>,
|
|
guest_key: Option<(NamedKey, crate::events::Input)>,
|
|
guest_keyboard: bool,
|
|
}
|
|
|
|
impl TesseraApp {
|
|
pub fn new(
|
|
namespace: Arc<RwLock<NamespaceState>>,
|
|
activity: Activity,
|
|
proxy: EventLoopProxy<AppEvent>,
|
|
) -> anyhow::Result<Self> {
|
|
let event_client = crate::events::Client::new(proxy.clone())?;
|
|
Ok(Self {
|
|
window: None,
|
|
context: None,
|
|
surface: None,
|
|
namespace,
|
|
developer_tools: {
|
|
let mut state = DeveloperToolsState::default();
|
|
state.proxy = Some(proxy);
|
|
state
|
|
},
|
|
layout: SceneLayout::default(),
|
|
pointer: Point::default(),
|
|
modifiers: ModifiersState::default(),
|
|
activity,
|
|
event_client,
|
|
guest_pressed: None,
|
|
guest_focus: None,
|
|
guest_key: None,
|
|
guest_keyboard: true,
|
|
})
|
|
}
|
|
|
|
fn apply_window_commands(&self) {
|
|
let Some(window) = &self.window else {
|
|
return;
|
|
};
|
|
let Ok(mut namespace) = self.namespace.write() else {
|
|
return;
|
|
};
|
|
let commands = std::mem::take(&mut namespace.window_commands);
|
|
for envelope in commands {
|
|
if envelope.session == 0
|
|
|| envelope.session != namespace.runtime_session
|
|
|| envelope.command.window != crate::window::MAIN_WINDOW
|
|
{
|
|
continue;
|
|
}
|
|
match envelope.command.action {
|
|
crate::window::Action::Resize { width, height } => {
|
|
let _ = window.request_inner_size(LogicalSize::new(width, height));
|
|
}
|
|
crate::window::Action::SetTitle(title) => window.set_title(&title),
|
|
crate::window::Action::Focus => window.focus_window(),
|
|
}
|
|
}
|
|
drop(namespace);
|
|
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() {
|
|
namespace.viewport = Some(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),
|
|
});
|
|
}
|
|
}
|
|
|
|
fn request_redraw(&self) {
|
|
if let Some(window) = &self.window {
|
|
window.request_redraw();
|
|
}
|
|
}
|
|
|
|
fn toggle_developer_tools(&mut self) {
|
|
if let Ok(namespace) = self.namespace.read() {
|
|
self.developer_tools.toggle(&namespace.document);
|
|
}
|
|
self.guest_keyboard = !self.developer_tools.open;
|
|
self.request_redraw();
|
|
}
|
|
|
|
fn document_updated(&mut self) {
|
|
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_focus =
|
|
frame
|
|
.focus
|
|
.filter(|_| frame.fault.is_none())
|
|
.map(|mount| crate::events::Input {
|
|
session: frame.session,
|
|
layout_version: frame.layout_version,
|
|
mount,
|
|
kind: "focus".into(),
|
|
point: None,
|
|
press_point: None,
|
|
});
|
|
}
|
|
self.request_redraw();
|
|
}
|
|
|
|
fn handle_keyboard(&mut self, event: &winit::event::KeyEvent) {
|
|
if self.guest_keyboard && self.handle_guest_key(event) {
|
|
self.request_redraw();
|
|
return;
|
|
}
|
|
if self.developer_tools.keyboard_control(
|
|
event,
|
|
self.modifiers.shift_key(),
|
|
&self.layout.developer_tools,
|
|
&self.activity,
|
|
) {
|
|
self.request_redraw();
|
|
return;
|
|
}
|
|
if event.state != ElementState::Pressed {
|
|
return;
|
|
}
|
|
let is_f12 = matches!(event.logical_key, Key::Named(NamedKey::F12));
|
|
let is_i =
|
|
matches!(&event.logical_key, Key::Character(value) if value.eq_ignore_ascii_case("i"));
|
|
let mac_shortcut = self.modifiers.super_key() && self.modifiers.alt_key() && is_i;
|
|
let other_shortcut = self.modifiers.control_key() && self.modifiers.shift_key() && is_i;
|
|
if is_f12 || mac_shortcut || other_shortcut {
|
|
if !event.repeat {
|
|
self.toggle_developer_tools();
|
|
}
|
|
return;
|
|
}
|
|
if !self.developer_tools.open || self.developer_tools.focus.is_none() {
|
|
return;
|
|
}
|
|
let command = self.modifiers.super_key() || self.modifiers.control_key();
|
|
if command && let Key::Character(value) = &event.logical_key {
|
|
match value.to_lowercase().as_str() {
|
|
"a" => self.developer_tools.selection = true,
|
|
"v" => match crate::clipboard::read() {
|
|
Ok(value) => self.developer_tools.insert(&value),
|
|
Err(error) => self.developer_tools.input_error = format!("Clipboard: {error}"),
|
|
},
|
|
"c" | "x" => {
|
|
let selected = self.developer_tools.selected_text();
|
|
if !selected.is_empty() {
|
|
match crate::clipboard::write(&selected) {
|
|
Ok(()) if value.eq_ignore_ascii_case("x") => {
|
|
self.developer_tools.insert("")
|
|
}
|
|
Ok(()) => {}
|
|
Err(error) => {
|
|
self.developer_tools.input_error = format!("Clipboard: {error}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
} else if let Key::Named(key) = &event.logical_key {
|
|
if !self.developer_tools.preedit.is_empty() {
|
|
return;
|
|
}
|
|
self.developer_tools
|
|
.key(key, self.modifiers.shift_key(), command);
|
|
} else if !command
|
|
&& self.developer_tools.preedit.is_empty()
|
|
&& let Some(text) = &event.text
|
|
{
|
|
let text: String = text.chars().filter(|c| !c.is_control()).collect();
|
|
self.developer_tools.insert(&text);
|
|
}
|
|
self.request_redraw();
|
|
}
|
|
|
|
fn guest_covered(&self, point: Point) -> bool {
|
|
let panel = self.layout.developer_tools.panel;
|
|
self.developer_tools.open
|
|
&& crate::geometry::Rect::new(
|
|
panel.x - 6.0,
|
|
panel.y - 6.0,
|
|
panel.width + 12.0,
|
|
panel.height + 12.0,
|
|
)
|
|
.contains(point)
|
|
}
|
|
|
|
fn guest_hit(&self) -> Option<crate::events::Input> {
|
|
if self.guest_covered(self.pointer) {
|
|
return None;
|
|
}
|
|
let current = self.namespace.read().ok()?;
|
|
if current.viewport.is_some_and(|size| {
|
|
self.pointer.x < 0.0
|
|
|| self.pointer.y < 0.0
|
|
|| self.pointer.x >= size.width
|
|
|| self.pointer.y >= size.height
|
|
}) {
|
|
return None;
|
|
}
|
|
let frame = ¤t.document.frame;
|
|
if frame.session == 0 {
|
|
return None;
|
|
}
|
|
let mount = frame.hit_test(self.pointer)?;
|
|
Some(crate::events::Input {
|
|
session: frame.session,
|
|
layout_version: frame.layout_version,
|
|
mount: mount.id,
|
|
kind: "click".into(),
|
|
point: Some([self.pointer.x, self.pointer.y]),
|
|
press_point: Some([self.pointer.x, self.pointer.y]),
|
|
})
|
|
}
|
|
|
|
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 {
|
|
session: frame.session,
|
|
layout_version: frame.layout_version,
|
|
mount: targets[index].id,
|
|
kind: "focus".into(),
|
|
point: None,
|
|
press_point: None,
|
|
})
|
|
});
|
|
if let Some(next) = next {
|
|
self.guest_key = None;
|
|
self.guest_focus = Some(next.clone());
|
|
self.send_guest(next);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
return self.guest_focus.is_some();
|
|
}
|
|
if matches!(key, NamedKey::Enter | NamedKey::Space) && self.guest_focus.is_some() {
|
|
if event.state == ElementState::Pressed {
|
|
if !event.repeat {
|
|
self.guest_key = self.guest_focus.clone().map(|focus| (*key, focus));
|
|
}
|
|
} else if let Some((pressed, mut input)) = self.guest_key.take()
|
|
&& pressed == *key
|
|
{
|
|
input.kind = "click".into();
|
|
self.send_guest(input);
|
|
}
|
|
return true;
|
|
}
|
|
false
|
|
}
|
|
|
|
fn redraw(&mut self) {
|
|
let guest_cursor = self.guest_hit().is_some();
|
|
let (Some(window), Some(surface)) = (self.window.as_ref(), self.surface.as_mut()) else {
|
|
return;
|
|
};
|
|
let size = window.inner_size();
|
|
let (Some(width), Some(height)) =
|
|
(NonZeroU32::new(size.width), NonZeroU32::new(size.height))
|
|
else {
|
|
return;
|
|
};
|
|
|
|
let Ok(namespace) = self.namespace.read() else {
|
|
return;
|
|
};
|
|
let scene = scene::build_scene(
|
|
size.width,
|
|
size.height,
|
|
window.scale_factor() as f32,
|
|
&namespace.document,
|
|
&mut self.developer_tools,
|
|
&self.activity,
|
|
);
|
|
drop(namespace);
|
|
self.layout = scene.layout;
|
|
window.set_cursor(if guest_cursor {
|
|
winit::window::CursorIcon::Pointer
|
|
} else {
|
|
self.developer_tools
|
|
.cursor_kind(&self.layout.developer_tools)
|
|
});
|
|
if self.developer_tools.repaint {
|
|
self.developer_tools.repaint = false;
|
|
window.request_redraw();
|
|
}
|
|
let ime = self
|
|
.developer_tools
|
|
.ime_position(&self.layout.developer_tools);
|
|
window.set_ime_cursor_area(
|
|
LogicalPosition::new(ime.x as f64, ime.y as f64),
|
|
LogicalSize::new(1.0, 19.0),
|
|
);
|
|
|
|
surface
|
|
.resize(width, height)
|
|
.expect("resize software surface");
|
|
let mut buffer = surface.buffer_mut().expect("acquire software buffer");
|
|
SoftwareRenderer::render(&scene.display_list, &mut buffer, size.width, size.height);
|
|
buffer.present().expect("present software-rendered frame");
|
|
}
|
|
}
|
|
|
|
impl ApplicationHandler<AppEvent> for TesseraApp {
|
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
|
if self.window.is_some() {
|
|
return;
|
|
}
|
|
|
|
let attributes = WindowAttributes::default()
|
|
.with_title("Tessera")
|
|
.with_inner_size(LogicalSize::new(1_280.0, 800.0))
|
|
.with_min_inner_size(LogicalSize::new(720.0, 480.0))
|
|
.with_max_inner_size(LogicalSize::new(
|
|
crate::window::MAX_WINDOW_DIMENSION,
|
|
crate::window::MAX_WINDOW_DIMENSION,
|
|
));
|
|
let window = Arc::new(
|
|
event_loop
|
|
.create_window(attributes)
|
|
.expect("create Tessera window"),
|
|
);
|
|
let context = Context::new(window.clone()).expect("create software-rendering context");
|
|
let surface =
|
|
Surface::new(&context, window.clone()).expect("create software-rendering surface");
|
|
|
|
window.set_ime_allowed(true);
|
|
self.window = Some(window.clone());
|
|
self.context = Some(context);
|
|
self.surface = Some(surface);
|
|
self.apply_window_commands();
|
|
self.publish_viewport();
|
|
window.request_redraw();
|
|
}
|
|
|
|
fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: AppEvent) {
|
|
match event {
|
|
AppEvent::RuntimeFailed { message } => {
|
|
self.developer_tools.input_error = message;
|
|
self.developer_tools.open = true;
|
|
self.guest_pressed = None;
|
|
self.guest_key = None;
|
|
self.request_redraw();
|
|
}
|
|
AppEvent::DocumentUpdated => self.document_updated(),
|
|
AppEvent::ConsoleFailed { message, source } => {
|
|
self.developer_tools.input_error = format!("Request failed: {message}");
|
|
if self.developer_tools.input.is_empty() {
|
|
self.developer_tools.input = source;
|
|
self.developer_tools.cursor = self.developer_tools.input.len();
|
|
self.developer_tools.reveal_cursor();
|
|
}
|
|
self.request_redraw();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn window_event(
|
|
&mut self,
|
|
event_loop: &ActiveEventLoop,
|
|
window_id: WindowId,
|
|
event: WindowEvent,
|
|
) {
|
|
let Some(window) = self.window.as_ref().cloned() else {
|
|
return;
|
|
};
|
|
if window.id() != window_id {
|
|
return;
|
|
}
|
|
|
|
match event {
|
|
WindowEvent::CloseRequested => event_loop.exit(),
|
|
WindowEvent::Resized(_) | WindowEvent::ScaleFactorChanged { .. } => {
|
|
self.publish_viewport();
|
|
window.request_redraw();
|
|
}
|
|
WindowEvent::CursorMoved {
|
|
position: PhysicalPosition { x, y },
|
|
..
|
|
} => {
|
|
let scale = window.scale_factor() as f32;
|
|
let next = Point::new(x as f32 / scale, y as f32 / scale);
|
|
let delta = Point::new(next.x - self.pointer.x, next.y - self.pointer.y);
|
|
self.pointer = next;
|
|
self.developer_tools.move_pointer(
|
|
next,
|
|
delta,
|
|
&self.layout.developer_tools,
|
|
window.inner_size().width as f32 / scale,
|
|
);
|
|
window.set_cursor(if self.guest_hit().is_some() {
|
|
winit::window::CursorIcon::Pointer
|
|
} else {
|
|
self.developer_tools
|
|
.cursor_kind(&self.layout.developer_tools)
|
|
});
|
|
window.request_redraw();
|
|
}
|
|
WindowEvent::CursorLeft { .. } => {
|
|
self.guest_pressed = None;
|
|
self.developer_tools.pointer_left();
|
|
window.set_cursor(winit::window::CursorIcon::Default);
|
|
window.request_redraw();
|
|
}
|
|
WindowEvent::MouseInput {
|
|
state,
|
|
button: MouseButton::Left,
|
|
..
|
|
} => {
|
|
if state == ElementState::Pressed {
|
|
self.guest_keyboard = !self.guest_covered(self.pointer);
|
|
self.guest_pressed = self.guest_hit();
|
|
if self.guest_keyboard {
|
|
self.developer_tools.blur();
|
|
}
|
|
self.developer_tools.press(
|
|
&self.layout.developer_tools,
|
|
self.pointer,
|
|
&self.activity,
|
|
);
|
|
} else {
|
|
if let Some(pressed) = self.guest_pressed.take()
|
|
&& let Some(mut released) = self.guest_hit()
|
|
&& released.session == pressed.session
|
|
&& released.layout_version == pressed.layout_version
|
|
{
|
|
released.press_point = pressed.point;
|
|
self.send_guest(released);
|
|
}
|
|
self.developer_tools.release(
|
|
&self.layout.developer_tools,
|
|
self.pointer,
|
|
&self.activity,
|
|
);
|
|
}
|
|
window.request_redraw();
|
|
}
|
|
WindowEvent::MouseWheel { delta, .. } => {
|
|
let (mut dx, mut dy) = match delta {
|
|
MouseScrollDelta::LineDelta(x, y) => (x * 38.0, y * 38.0),
|
|
MouseScrollDelta::PixelDelta(p) => (
|
|
p.x as f32 / window.scale_factor() as f32,
|
|
p.y as f32 / window.scale_factor() as f32,
|
|
),
|
|
};
|
|
if self.modifiers.shift_key() && dx.abs() < 0.1 {
|
|
dx = dy;
|
|
dy = 0.0;
|
|
}
|
|
self.developer_tools
|
|
.wheel(&self.layout.developer_tools, self.pointer, dx, dy);
|
|
window.request_redraw();
|
|
}
|
|
WindowEvent::Ime(Ime::Preedit(value, _)) => {
|
|
self.developer_tools.preedit = value;
|
|
window.request_redraw();
|
|
}
|
|
WindowEvent::Ime(Ime::Commit(value)) => {
|
|
self.developer_tools.preedit.clear();
|
|
if self.developer_tools.open {
|
|
self.developer_tools.insert(&value);
|
|
}
|
|
window.request_redraw();
|
|
}
|
|
WindowEvent::Focused(false) => {
|
|
self.guest_pressed = None;
|
|
self.guest_key = None;
|
|
self.developer_tools.blur();
|
|
self.modifiers = ModifiersState::default();
|
|
window.request_redraw();
|
|
}
|
|
WindowEvent::ModifiersChanged(modifiers) => self.modifiers = modifiers.state(),
|
|
WindowEvent::KeyboardInput { event, .. } => self.handle_keyboard(&event),
|
|
WindowEvent::RedrawRequested => self.redraw(),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|