From 229e22f98df574cc644d80db1ac80ff7c076215e Mon Sep 17 00:00:00 2001 From: Shark Date: Sat, 28 Dec 2024 21:15:15 +0100 Subject: [PATCH 1/3] add WebEventLoop --- Cargo.lock | 11 ++ crates/gosub_interface/src/config.rs | 4 + crates/gosub_web_platform/Cargo.toml | 15 ++ crates/gosub_web_platform/src/callback.rs | 49 +++++++ .../gosub_web_platform/src/event_listeners.rs | 131 ++++++++++++++++++ crates/gosub_web_platform/src/lib.rs | 121 ++++++++++++++++ crates/gosub_web_platform/src/poll_guard.rs | 32 +++++ crates/gosub_web_platform/src/timers.rs | 83 +++++++++++ 8 files changed, 446 insertions(+) create mode 100644 crates/gosub_web_platform/Cargo.toml create mode 100644 crates/gosub_web_platform/src/callback.rs create mode 100644 crates/gosub_web_platform/src/event_listeners.rs create mode 100644 crates/gosub_web_platform/src/lib.rs create mode 100644 crates/gosub_web_platform/src/poll_guard.rs create mode 100644 crates/gosub_web_platform/src/timers.rs diff --git a/Cargo.lock b/Cargo.lock index 7e1d3226..9fa20ebb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2016,6 +2016,17 @@ dependencies = [ "vello_svg", ] +[[package]] +name = "gosub_web_platform" +version = "0.1.0" +dependencies = [ + "gosub_interface", + "gosub_shared", + "pin-project", + "slotmap", + "tokio", +] + [[package]] name = "gosub_webexecutor" version = "0.1.1" diff --git a/crates/gosub_interface/src/config.rs b/crates/gosub_interface/src/config.rs index 6e6e8b2b..d9425d82 100644 --- a/crates/gosub_interface/src/config.rs +++ b/crates/gosub_interface/src/config.rs @@ -30,3 +30,7 @@ pub trait ModuleConfiguration: pub trait HasDrawComponents: HasRenderTree + HasRenderBackend {} impl HasDrawComponents for C {} + +pub trait HasWebComponents: HasChrome + 'static {} + +impl HasWebComponents for C {} diff --git a/crates/gosub_web_platform/Cargo.toml b/crates/gosub_web_platform/Cargo.toml new file mode 100644 index 00000000..e08afec8 --- /dev/null +++ b/crates/gosub_web_platform/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "gosub_web_platform" +version = "0.1.0" +edition = "2021" +authors = ["Gosub Community "] +license = "MIT" +description = "Gosub Web Platform implementation" + +[dependencies] +gosub_shared = { version = "0.1.1", registry = "gosub", path = "../gosub_shared" } +gosub_interface = { version = "0.1.2", registry = "gosub", path = "../gosub_interface", features = [] } +slotmap = "1.0.7" +tokio = { version = "1.42.0", features = ["sync", "rt", "macros"] } +pin-project = "1.1.7" +log = "0.4.22" diff --git a/crates/gosub_web_platform/src/callback.rs b/crates/gosub_web_platform/src/callback.rs new file mode 100644 index 00000000..e4b803f1 --- /dev/null +++ b/crates/gosub_web_platform/src/callback.rs @@ -0,0 +1,49 @@ +use crate::poll_guard::{PollCallback, PollGuard}; +use std::future::Future; +use tokio::task; + +struct TestPollGuard; + +impl PollCallback for TestPollGuard { + fn poll(&mut self) { + //TODO: execute the js microtasks + } +} + +pub trait FutureExecutor { + fn execute + 'static>(&mut self, future: T); +} + +pub struct Callback { + #[allow(clippy::type_complexity)] + spawner: Box, +} + +impl Callback { + pub fn new(spawner: impl FnMut(&mut T, D) + 'static) -> Self { + Self { + spawner: Box::new(spawner), + } + } + + pub fn execute(&mut self, executor: &mut T, data: D) { + (self.spawner)(executor, data); + } +} + +impl Callback { + pub fn exec(&mut self, executor: &mut T) { + self.execute(executor, ()); + } +} + +#[derive(Debug, Default)] +pub struct TokioExecutor; + +impl FutureExecutor for TokioExecutor { + fn execute + 'static>(&mut self, future: T) { + let guard = PollGuard::new(future, TestPollGuard); + + task::spawn_local(guard); + } +} diff --git a/crates/gosub_web_platform/src/event_listeners.rs b/crates/gosub_web_platform/src/event_listeners.rs new file mode 100644 index 00000000..0b52329f --- /dev/null +++ b/crates/gosub_web_platform/src/event_listeners.rs @@ -0,0 +1,131 @@ +use crate::callback::{Callback, FutureExecutor}; +use gosub_interface::input::{InputEvent, MouseButton}; +use gosub_shared::geo::Point; +use log::info; +use std::fmt::Debug; + +pub enum Listeners { + MouseDown(Callback), + MouseUp(Callback), + MouseMove(Callback), + MouseScroll(Callback), + KeyboardUp(Callback), + KeyboardDown(Callback), +} + +#[derive(Debug, Clone, Copy)] +pub struct MouseButtonEvent { + pub button: MouseButton, +} + +#[derive(Debug, Clone, Copy)] +pub struct KeyboardEvent { + pub key: char, +} + +#[derive(Debug, Clone, Copy)] +pub struct MouseMoveEvent { + pub pos: Point, +} + +#[derive(Debug, Clone, Copy)] +pub struct MouseScrollEvent { + pub delta: Point, +} + +pub struct EventListener { + listeners: Vec>, +} + +impl EventListener { + pub fn handle_event(&mut self, event: D, e: &mut E) { + for listener in self.listeners.iter_mut() { + listener.execute(e, event.clone()); + } + } +} + +impl Debug for EventListener { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EventListener") + .field("listeners", &self.listeners.len()) + .finish() + } +} + +impl Default for EventListener { + fn default() -> Self { + Self { listeners: Vec::new() } + } +} + +pub struct EventListeners { + mouse_up: EventListener, + mouse_down: EventListener, + mouse_move: EventListener, + mouse_scroll: EventListener, + keyboard_up: EventListener, + keyboard_down: EventListener, +} + +impl EventListeners { + pub(crate) fn add_listener(&mut self, listener: Listeners) { + match listener { + Listeners::MouseDown(callback) => self.mouse_down.listeners.push(callback), + Listeners::MouseUp(callback) => self.mouse_up.listeners.push(callback), + Listeners::MouseMove(callback) => self.mouse_move.listeners.push(callback), + Listeners::MouseScroll(callback) => self.mouse_scroll.listeners.push(callback), + Listeners::KeyboardUp(callback) => self.keyboard_up.listeners.push(callback), + Listeners::KeyboardDown(callback) => self.keyboard_down.listeners.push(callback), + } + } + + pub(crate) fn handle_input_event(&mut self, event: InputEvent, e: &mut E) { + match event { + InputEvent::MouseDown(button) => { + self.mouse_down.handle_event(MouseButtonEvent { button }, e); + } + InputEvent::MouseUp(button) => { + self.mouse_up.handle_event(MouseButtonEvent { button }, e); + } + InputEvent::MouseMove(pos) => { + self.mouse_move.handle_event(MouseMoveEvent { pos }, e); + } + InputEvent::MouseScroll(delta) => { + self.mouse_scroll.handle_event(MouseScrollEvent { delta }, e); + } + InputEvent::KeyboardDown(key) => { + self.keyboard_down.handle_event(KeyboardEvent { key }, e); + } + InputEvent::KeyboardUp(key) => { + self.keyboard_up.handle_event(KeyboardEvent { key }, e); + } + } + } +} + +impl Default for EventListeners { + fn default() -> Self { + Self { + mouse_up: EventListener::default(), + mouse_down: EventListener::default(), + mouse_move: EventListener::default(), + mouse_scroll: EventListener::default(), + keyboard_up: EventListener::default(), + keyboard_down: EventListener::default(), + } + } +} + +impl Debug for EventListeners { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EventListeners") + .field("mouse_down", &self.mouse_down) + .field("mouse_up", &self.mouse_up) + .field("mouse_move", &self.mouse_move) + .field("mouse_scroll", &self.mouse_scroll) + .field("keyboard_down", &self.keyboard_down) + .field("keyboard_up", &self.keyboard_up) + .finish() + } +} diff --git a/crates/gosub_web_platform/src/lib.rs b/crates/gosub_web_platform/src/lib.rs new file mode 100644 index 00000000..0dc7e5b7 --- /dev/null +++ b/crates/gosub_web_platform/src/lib.rs @@ -0,0 +1,121 @@ +extern crate core; + +use crate::callback::{FutureExecutor, TokioExecutor}; +use crate::event_listeners::{EventListeners, Listeners}; +use crate::timers::WebTimers; +use gosub_interface::config::HasWebComponents; +use gosub_interface::input::InputEvent; +use gosub_interface::instance::Handles; +use std::thread; +use tokio::runtime::{Handle, Runtime}; +use tokio::sync::mpsc::{Receiver, Sender}; +use tokio::task::LocalSet; + +mod callback; +mod event_listeners; +pub mod poll_guard; +#[allow(dead_code)] +mod timers; + +/// The web event loop, this will be the main event loop for a JS or Lua runtime, it is directly tied to an instance's EventLoop +#[allow(unused)] +pub struct WebEventLoop { + listeners: EventListeners, + rt: Handle, + handles: Handles, + rx: Receiver, + irx: Receiver>, + itx: Sender>, + timers: WebTimers, +} + +/// Handle to the event loop, this can be used to spawn tasks or send messages to the event loop +pub struct WebEventLoopHandle { + pub rt: Handle, + pub tx: Sender, +} + +pub enum WebEventLoopMessage { + InputEvent(InputEvent), + Close, +} + +pub enum LocalEventLoopMessage { + AddListener(Listeners), +} + +impl WebEventLoop { + /// Create a new WebEventLoop on a new thead, returning the handle to the event loop + pub fn new_on_thread(handles: Handles) -> WebEventLoopHandle { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + let handle = rt.handle().clone(); + + let (tx, rx) = tokio::sync::mpsc::channel(100); + + thread::spawn(|| { + let (itx, irx) = tokio::sync::mpsc::channel(100); + let mut el = WebEventLoop { + listeners: EventListeners::default(), + handles, + rt: rt.handle().clone(), + irx, + itx, + rx, + timers: WebTimers::new(), + }; + + el.run(rt, TokioExecutor); + }); + + WebEventLoopHandle { rt: handle, tx } + } +} + +impl WebEventLoop { + pub fn run(&mut self, rt: Runtime, mut e: E) { + let set = LocalSet::new(); + + set.block_on(&rt, async { + loop { + tokio::select! { + val = self.rx.recv() => { + let Some(msg) = val else { + break; + }; + self.handle_message(msg, &mut e); + } + + val = self.irx.recv() => { + let Some(msg) = val else { + break; + }; + self.handle_local_message(msg); + } + } + } + }); + } + + fn handle_message(&mut self, msg: WebEventLoopMessage, exec: &mut E) { + match msg { + WebEventLoopMessage::InputEvent(e) => { + self.listeners.handle_input_event(e, exec); + } + WebEventLoopMessage::Close => { + self.rx.close(); + } + } + } + + fn handle_local_message(&mut self, msg: LocalEventLoopMessage) { + match msg { + LocalEventLoopMessage::AddListener(listener) => { + self.listeners.add_listener(listener); + } + } + } +} diff --git a/crates/gosub_web_platform/src/poll_guard.rs b/crates/gosub_web_platform/src/poll_guard.rs new file mode 100644 index 00000000..589b2a2b --- /dev/null +++ b/crates/gosub_web_platform/src/poll_guard.rs @@ -0,0 +1,32 @@ +use core::task; +use pin_project::pin_project; +use std::future::Future; +use std::pin::Pin; + +pub trait PollCallback { + fn poll(&mut self); +} + +#[pin_project] +pub struct PollGuard, C: PollCallback> { + cb: C, + #[pin] + fut: T, +} + +impl, C: PollCallback> Future for PollGuard { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll<()> { + let this = self.project(); + this.cb.poll(); + + this.fut.poll(cx) + } +} + +impl, C: PollCallback> PollGuard { + pub fn new(fut: F, cb: C) -> Self { + Self { cb, fut } + } +} diff --git a/crates/gosub_web_platform/src/timers.rs b/crates/gosub_web_platform/src/timers.rs new file mode 100644 index 00000000..995f7947 --- /dev/null +++ b/crates/gosub_web_platform/src/timers.rs @@ -0,0 +1,83 @@ +use crate::callback::{Callback, TokioExecutor}; +use slotmap::{DefaultKey, SlotMap}; +use std::cell::RefCell; +use std::rc::Rc; +use std::time::Duration; +use tokio::task; +use tokio::task::JoinHandle; + +#[derive(Debug)] +pub struct WebTimers { + inner: Rc>, +} + +#[derive(Debug)] +pub struct WebTimersInner { + timers: SlotMap, +} + +impl WebTimers { + pub fn new() -> Self { + Self { + inner: Rc::new(RefCell::new(WebTimersInner { timers: SlotMap::new() })), + } + } + + pub fn add_timer(&mut self, timer: Timer) -> TimerId { + TimerId(self.inner.borrow_mut().timers.insert(timer)) + } + + /// Removes and cancels the timer with the given id. + pub fn remove(&mut self, id: TimerId) { + if let Some(timer) = self.inner.borrow_mut().timers.remove(id.0) { + timer.handle.abort(); + } + } + + pub fn set_timeout(&mut self, duration: Duration, mut callback: Callback) { + let inner = self.inner.clone(); + + self.inner.borrow_mut().timers.insert_with_key(move |key| { + let handle = task::spawn_local(async move { + tokio::time::sleep(duration).await; + + callback.exec(&mut TokioExecutor); + + inner.borrow_mut().timers.remove(key); + }); + + Timer { handle } + }); + } + + pub fn set_interval(&mut self, duration: Duration, mut callback: Callback) { + let handle = task::spawn_local(async move { + let mut interval = tokio::time::interval(duration); + + interval.tick().await; // First tick is immediate + + loop { + interval.tick().await; + callback.exec(&mut TokioExecutor); + } + }); + + let timer = Timer { handle }; + + self.inner.borrow_mut().timers.insert(timer); + } + + pub fn remove_all(&mut self) { + for (_, timer) in self.inner.borrow_mut().timers.drain() { + timer.handle.abort(); + } + } +} + +#[derive(Debug)] +pub struct Timer { + handle: JoinHandle<()>, +} + +#[derive(Debug)] +pub struct TimerId(DefaultKey); From 7f4f80680bcbc14e4ee68867497f1049dc97ad2a Mon Sep 17 00:00:00 2001 From: Shark Date: Sun, 29 Dec 2024 18:49:17 +0100 Subject: [PATCH 2/3] use event loop in EngineInstance and send events to it --- Cargo.lock | 14 ++++++++++++++ crates/gosub_instance/Cargo.toml | 5 +++-- crates/gosub_instance/src/lib.rs | 27 ++++++++++++++++++--------- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9fa20ebb..77f3b11d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1857,6 +1857,7 @@ dependencies = [ "gosub_interface", "gosub_net", "gosub_shared", + "gosub_web_platform", "log", "tokio", "url", @@ -2022,6 +2023,7 @@ version = "0.1.0" dependencies = [ "gosub_interface", "gosub_shared", + "log", "pin-project", "slotmap", "tokio", @@ -4778,9 +4780,21 @@ dependencies = [ "mio", "pin-project-lite", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", +] + [[package]] name = "toml" version = "0.8.19" diff --git a/crates/gosub_instance/Cargo.toml b/crates/gosub_instance/Cargo.toml index 8ad5e98f..62cd8e61 100644 --- a/crates/gosub_instance/Cargo.toml +++ b/crates/gosub_instance/Cargo.toml @@ -4,8 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] -gosub_shared = { path = "../gosub_shared" } -gosub_interface = { path = "../gosub_interface" } +gosub_shared = { path = "../gosub_shared", registry = "gosub" } +gosub_interface = { path = "../gosub_interface", registry = "gosub" } +gosub_web_platform = { path = "../gosub_web_platform", registry = "gosub" } gosub_net = { path = "../gosub_net" } tokio = { version = "1.43.0", features = ["sync", "rt"] } url = "2.5.4" diff --git a/crates/gosub_instance/src/lib.rs b/crates/gosub_instance/src/lib.rs index 74fea883..f7a100f4 100644 --- a/crates/gosub_instance/src/lib.rs +++ b/crates/gosub_instance/src/lib.rs @@ -9,6 +9,7 @@ use gosub_interface::render_backend::{ImageBuffer, NodeDesc}; use gosub_net::http::fetcher::Fetcher; use gosub_shared::geo::SizeU32; use gosub_shared::types::Result; +use gosub_web_platform::{WebEventLoop, WebEventLoopHandle, WebEventLoopMessage}; use log::warn; use std::sync::mpsc::Sender as SyncSender; use std::sync::Arc; @@ -23,6 +24,7 @@ pub struct EngineInstance { pub title: String, pub url: Url, pub data: C::TreeDrawer, + web: WebEventLoopHandle, rx: Receiver, irx: Receiver>, el: El, @@ -61,8 +63,11 @@ impl EngineInstance { let (itx, irx) = tokio::sync::mpsc::channel(128); + let web = WebEventLoop::new_on_thread(handles.clone()); + Ok(EngineInstance { title: "Gosub".to_string(), + web, url, data, rx, @@ -184,18 +189,22 @@ impl EngineInstance { } } } - InstanceMessage::Input(event) => match event { - InputEvent::MouseScroll(delta) => { - self.data.scroll(delta); - self.redraw(); - } - InputEvent::MouseMove(point) => { - if self.data.mouse_move(point.x, point.y) { + InstanceMessage::Input(event) => { + match event { + InputEvent::MouseScroll(delta) => { + self.data.scroll(delta); self.redraw(); } + InputEvent::MouseMove(point) => { + if self.data.mouse_move(point.x, point.y) { + self.redraw(); + } + } + _ => {} } - _ => {} //TODO: send all events to the WebEventLoop - }, + + self.web.tx.send(WebEventLoopMessage::InputEvent(event)).await?; + } } Ok(()) From e3a6b70cd7516a84be4aad62d21d024ed0158ee0 Mon Sep 17 00:00:00 2001 From: Shark Date: Sun, 29 Dec 2024 18:58:33 +0100 Subject: [PATCH 3/3] pass more events to the `EngineInstance` (vello_renderer) --- .../gosub_web_platform/src/event_listeners.rs | 1 + examples/vello-renderer/event_loop.rs | 192 +++++++++++------- 2 files changed, 114 insertions(+), 79 deletions(-) diff --git a/crates/gosub_web_platform/src/event_listeners.rs b/crates/gosub_web_platform/src/event_listeners.rs index 0b52329f..95480d74 100644 --- a/crates/gosub_web_platform/src/event_listeners.rs +++ b/crates/gosub_web_platform/src/event_listeners.rs @@ -39,6 +39,7 @@ pub struct EventListener { impl EventListener { pub fn handle_event(&mut self, event: D, e: &mut E) { + info!("Handling event: {:?}", event); for listener in self.listeners.iter_mut() { listener.execute(e, event.clone()); } diff --git a/examples/vello-renderer/event_loop.rs b/examples/vello-renderer/event_loop.rs index d3aafd94..1ad43ca2 100644 --- a/examples/vello-renderer/event_loop.rs +++ b/examples/vello-renderer/event_loop.rs @@ -1,9 +1,10 @@ use crate::window::{Window, WindowState}; use gosub_instance::{DebugEvent, InstanceMessage}; use gosub_interface::config::ModuleConfiguration; -use gosub_interface::input::InputEvent; +use gosub_interface::input::{InputEvent, MouseButton}; use gosub_interface::render_backend::{Point, RenderBackend, SizeU32, FP}; use gosub_shared::types::Result; +use log::info; use winit::event::{ElementState, MouseScrollDelta, WindowEvent}; use winit::event_loop::ActiveEventLoop; use winit::keyboard::{KeyCode, ModifiersState, PhysicalKey}; @@ -72,108 +73,141 @@ impl Window<'_, C> { } WindowEvent::KeyboardInput { event, .. } => { - if event.repeat || event.state == ElementState::Released { + let Some(tab) = self.tabs.get_current_tab() else { return Ok(()); - } + }; - let Some(tab) = self.tabs.get_current_tab() else { + let winit::keyboard::Key::Character(code) = event.logical_key else { return Ok(()); }; - if let PhysicalKey::Code(code) = event.physical_key { - match code { - KeyCode::KeyD => { - tab.tx.blocking_send(InstanceMessage::Debug(DebugEvent::Toggle))?; - self.window.request_redraw(); - } - KeyCode::KeyC => { - tab.tx.blocking_send(InstanceMessage::Debug(DebugEvent::ClearBuffers))?; - self.window.request_redraw(); - } - KeyCode::F5 => { - tab.tx.blocking_send(InstanceMessage::Reload)?; - } - KeyCode::ArrowRight => { - if self.mods.state().contains(ModifiersState::CONTROL) { - self.tabs.next_tab(); + let Some(code) = code.to_string().chars().next() else { + return Ok(()); + }; + + let input = match event.state { + ElementState::Pressed => InputEvent::KeyboardDown, + ElementState::Released => InputEvent::KeyboardUp, + }(code); + + tab.tx.blocking_send(InstanceMessage::Input(input))?; + + if !event.repeat && event.state != ElementState::Released { + if let PhysicalKey::Code(code) = event.physical_key { + match code { + KeyCode::KeyD => { + tab.tx.blocking_send(InstanceMessage::Debug(DebugEvent::Toggle))?; self.window.request_redraw(); } - } - KeyCode::ArrowLeft => { - if self.mods.state().contains(ModifiersState::CONTROL) { - self.tabs.previous_tab(); + KeyCode::KeyC => { + tab.tx.blocking_send(InstanceMessage::Debug(DebugEvent::ClearBuffers))?; self.window.request_redraw(); } - } - KeyCode::Digit0 => { - if self.mods.state().contains(ModifiersState::CONTROL) { - self.tabs.activate_idx(0); - self.window.request_redraw(); + KeyCode::F5 => { + tab.tx.blocking_send(InstanceMessage::Reload)?; } - } - KeyCode::Digit1 => { - if self.mods.state().contains(ModifiersState::CONTROL) { - self.tabs.activate_idx(1); - self.window.request_redraw(); + KeyCode::ArrowRight => { + if self.mods.state().contains(ModifiersState::CONTROL) { + self.tabs.next_tab(); + self.window.request_redraw(); + } } - } - KeyCode::Digit2 => { - if self.mods.state().contains(ModifiersState::CONTROL) { - self.tabs.activate_idx(2); - self.window.request_redraw(); + KeyCode::ArrowLeft => { + if self.mods.state().contains(ModifiersState::CONTROL) { + self.tabs.previous_tab(); + self.window.request_redraw(); + } } - } - KeyCode::Digit3 => { - if self.mods.state().contains(ModifiersState::CONTROL) { - self.tabs.activate_idx(3); - self.window.request_redraw(); + KeyCode::Digit0 => { + if self.mods.state().contains(ModifiersState::CONTROL) { + self.tabs.activate_idx(0); + self.window.request_redraw(); + } } - } - KeyCode::Digit4 => { - if self.mods.state().contains(ModifiersState::CONTROL) { - self.tabs.activate_idx(4); - self.window.request_redraw(); + KeyCode::Digit1 => { + if self.mods.state().contains(ModifiersState::CONTROL) { + self.tabs.activate_idx(1); + self.window.request_redraw(); + } } - } - KeyCode::Digit5 => { - if self.mods.state().contains(ModifiersState::CONTROL) { - self.tabs.activate_idx(5); - self.window.request_redraw(); + KeyCode::Digit2 => { + if self.mods.state().contains(ModifiersState::CONTROL) { + self.tabs.activate_idx(2); + self.window.request_redraw(); + } } - } - KeyCode::Digit6 => { - if self.mods.state().contains(ModifiersState::CONTROL) { - self.tabs.activate_idx(6); - self.window.request_redraw(); + KeyCode::Digit3 => { + if self.mods.state().contains(ModifiersState::CONTROL) { + self.tabs.activate_idx(3); + self.window.request_redraw(); + } } - } - KeyCode::Digit7 => { - if self.mods.state().contains(ModifiersState::CONTROL) { - self.tabs.activate_idx(7); - self.window.request_redraw(); + KeyCode::Digit4 => { + if self.mods.state().contains(ModifiersState::CONTROL) { + self.tabs.activate_idx(4); + self.window.request_redraw(); + } } - } - KeyCode::Digit8 => { - if self.mods.state().contains(ModifiersState::CONTROL) { - self.tabs.activate_idx(8); - self.window.request_redraw(); + KeyCode::Digit5 => { + if self.mods.state().contains(ModifiersState::CONTROL) { + self.tabs.activate_idx(5); + self.window.request_redraw(); + } } - } - KeyCode::Digit9 => { - if self.mods.state().contains(ModifiersState::CONTROL) { - self.tabs.activate_idx(9); - self.window.request_redraw(); + KeyCode::Digit6 => { + if self.mods.state().contains(ModifiersState::CONTROL) { + self.tabs.activate_idx(6); + self.window.request_redraw(); + } + } + KeyCode::Digit7 => { + if self.mods.state().contains(ModifiersState::CONTROL) { + self.tabs.activate_idx(7); + self.window.request_redraw(); + } + } + KeyCode::Digit8 => { + if self.mods.state().contains(ModifiersState::CONTROL) { + self.tabs.activate_idx(8); + self.window.request_redraw(); + } + } + KeyCode::Digit9 => { + if self.mods.state().contains(ModifiersState::CONTROL) { + self.tabs.activate_idx(9); + self.window.request_redraw(); + } } - } - // KeyCode::F6 => self.el.open_tab(Url::parse("https://news.ycombinator.com")?), - // KeyCode::F7 => self.el.open_tab(Url::parse("https://archlinux.org")?), - // KeyCode::F8 => self.el.open_tab(Url::parse("file://resources/test.html")?), - _ => {} + // KeyCode::F6 => self.el.open_tab(Url::parse("https://news.ycombinator.com")?), + // KeyCode::F7 => self.el.open_tab(Url::parse("https://archlinux.org")?), + // KeyCode::F8 => self.el.open_tab(Url::parse("file://resources/test.html")?), + _ => {} + } } } } + WindowEvent::MouseInput { state, button, .. } => { + let Some(tab) = self.tabs.get_current_tab() else { + return Ok(()); + }; + + let button = match button { + winit::event::MouseButton::Left => MouseButton::Left, + winit::event::MouseButton::Right => MouseButton::Right, + winit::event::MouseButton::Middle => MouseButton::Middle, + _ => return Ok(()), + }; + + let event = match state { + ElementState::Pressed => InputEvent::MouseDown, + ElementState::Released => InputEvent::MouseUp, + }(button); + + tab.tx.blocking_send(InstanceMessage::Input(event))?; + } + WindowEvent::ModifiersChanged(mods) => { self.mods = mods; }