diff --git a/crates/weft-file-portal/src/main.rs b/crates/weft-file-portal/src/main.rs index 524757f..23a87c8 100644 --- a/crates/weft-file-portal/src/main.rs +++ b/crates/weft-file-portal/src/main.rs @@ -62,12 +62,12 @@ fn parse_allowed(args: &[String]) -> Vec { let mut allowed = Vec::new(); let mut i = 0; while i < args.len() { - if args[i] == "--allow" { - if let Some(p) = args.get(i + 1) { - allowed.push(PathBuf::from(p)); - i += 2; - continue; - } + if args[i] == "--allow" + && let Some(p) = args.get(i + 1) + { + allowed.push(PathBuf::from(p)); + i += 2; + continue; } i += 1; } diff --git a/crates/weft-servo-shell/src/embedder.rs b/crates/weft-servo-shell/src/embedder.rs index f4bfbe3..281324d 100644 --- a/crates/weft-servo-shell/src/embedder.rs +++ b/crates/weft-servo-shell/src/embedder.rs @@ -1,16 +1,19 @@ #![cfg(feature = "servo-embed")] -use std::path::Path; +use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::{Arc, Mutex}; use servo::{ - EventLoopWaker, ServoBuilder, ServoDelegate, ServoUrl, WebViewBuilder, WebViewDelegate, + EventLoopWaker, InputEvent, MouseButton as ServoMouseButton, MouseButtonAction, + MouseButtonEvent, MouseMoveEvent, ServoBuilder, ServoDelegate, ServoUrl, UserContentManager, + UserScript, WebViewBuilder, WebViewDelegate, }; use winit::{ application::ApplicationHandler, - event::WindowEvent, + event::{ElementState, MouseButton, WindowEvent}, event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy}, + keyboard::ModifiersState, window::{Window, WindowAttributes, WindowId}, }; @@ -65,24 +68,30 @@ impl WebViewDelegate for WeftWebViewDelegate { struct App { url: ServoUrl, + ws_port: u16, window: Option>, servo: Option, webview: Option, redraw_requested: Arc, waker: WeftEventLoopWaker, shutting_down: bool, + modifiers: ModifiersState, + cursor_pos: servo::euclid::default::Point2D, } impl App { - fn new(url: ServoUrl, waker: WeftEventLoopWaker) -> Self { + fn new(url: ServoUrl, waker: WeftEventLoopWaker, ws_port: u16) -> Self { Self { url, + ws_port, window: None, servo: None, webview: None, redraw_requested: Arc::new(std::sync::atomic::AtomicBool::new(false)), waker, shutting_down: false, + modifiers: ModifiersState::default(), + cursor_pos: servo::euclid::default::Point2D::zero(), } } @@ -137,10 +146,18 @@ impl ApplicationHandler for App { .expect("SoftwareRenderingContext"), ); + let user_content_manager = Rc::new(UserContentManager::new(&servo)); + let bridge_js = format!( + r#"(function(){{var ws=new WebSocket('ws://127.0.0.1:{p}');var q=[];var r=false;ws.onopen=function(){{r=true;q.forEach(function(m){{ws.send(JSON.stringify(m))}});q.length=0}};window.weftIpc={{send:function(m){{if(r)ws.send(JSON.stringify(m));else q.push(m)}},onmessage:null}};ws.onmessage=function(e){{if(window.weftIpc.onmessage)window.weftIpc.onmessage(JSON.parse(e.data))}}}})()"#, + p = self.ws_port + ); + user_content_manager.add_script(Rc::new(UserScript::new(bridge_js, None))); + let webview = WebViewBuilder::new(&servo, Rc::clone(&rendering_context)) .delegate(Rc::new(WeftWebViewDelegate { redraw_requested: Arc::clone(&self.redraw_requested), })) + .user_content_manager(Rc::clone(&user_content_manager)) .url(self.url.clone()) .build(); @@ -193,6 +210,41 @@ impl ApplicationHandler for App { wv.resize(servo::euclid::Size2D::new(new_size.width, new_size.height)); } } + WindowEvent::ModifiersChanged(mods) => { + self.modifiers = mods.state(); + } + WindowEvent::KeyboardInput { event, .. } => { + if let Some(wv) = &self.webview { + let ev = super::keyutils::keyboard_event_from_winit(&event, self.modifiers); + let _ = wv.notify_input_event(InputEvent::Keyboard(ev)); + } + } + WindowEvent::CursorMoved { position, .. } => { + let pt = servo::euclid::default::Point2D::new(position.x as f32, position.y as f32); + self.cursor_pos = pt; + if let Some(wv) = &self.webview { + let _ = wv.notify_input_event(InputEvent::MouseMove(MouseMoveEvent::new(pt))); + } + } + WindowEvent::MouseInput { state, button, .. } => { + let btn = match button { + MouseButton::Left => ServoMouseButton::Left, + MouseButton::Right => ServoMouseButton::Right, + MouseButton::Middle => ServoMouseButton::Middle, + _ => return, + }; + let action = match state { + ElementState::Pressed => MouseButtonAction::Click, + ElementState::Released => MouseButtonAction::Up, + }; + if let Some(wv) = &self.webview { + let _ = wv.notify_input_event(InputEvent::MouseButton(MouseButtonEvent::new( + action, + btn, + self.cursor_pos.cast_unit(), + ))); + } + } WindowEvent::CloseRequested => { self.shutting_down = true; if let Some(servo) = &self.servo { @@ -207,10 +259,43 @@ impl ApplicationHandler for App { // ── Public entry point ──────────────────────────────────────────────────────── -pub fn run(html_path: &Path, _ws_port: u16) -> anyhow::Result<()> { +fn resolve_weft_app_url(url: &ServoUrl) -> Option { + if url.scheme() != "weft-app" { + return None; + } + let app_id = url.host_str()?; + let rel = url.path().trim_start_matches('/'); + let file_path = app_store_roots() + .into_iter() + .map(|r| r.join(app_id).join("ui").join(rel)) + .find(|p| p.exists())?; + let s = format!("file://{}", file_path.display()); + ServoUrl::parse(&s).ok() +} + +fn app_store_roots() -> Vec { + if let Ok(v) = std::env::var("WEFT_APP_STORE") { + return vec![PathBuf::from(v)]; + } + let mut roots = Vec::new(); + if let Ok(home) = std::env::var("HOME") { + roots.push( + PathBuf::from(home) + .join(".local") + .join("share") + .join("weft") + .join("apps"), + ); + } + roots.push(PathBuf::from("/usr/share/weft/apps")); + roots +} + +pub fn run(html_path: &Path, ws_port: u16) -> anyhow::Result<()> { let url_str = format!("file://{}", html_path.display()); - let url = + let raw_url = ServoUrl::parse(&url_str).map_err(|e| anyhow::anyhow!("invalid URL {url_str}: {e}"))?; + let url = resolve_weft_app_url(&raw_url).unwrap_or(raw_url); let event_loop = EventLoop::::with_user_event() .build() @@ -220,7 +305,7 @@ pub fn run(html_path: &Path, _ws_port: u16) -> anyhow::Result<()> { proxy: Arc::new(Mutex::new(event_loop.create_proxy())), }; - let mut app = App::new(url, waker); + let mut app = App::new(url, waker, ws_port); event_loop .run_app(&mut app) .map_err(|e| anyhow::anyhow!("event loop run: {e}")) diff --git a/crates/weft-servo-shell/src/keyutils.rs b/crates/weft-servo-shell/src/keyutils.rs new file mode 100644 index 0000000..c4fd87b --- /dev/null +++ b/crates/weft-servo-shell/src/keyutils.rs @@ -0,0 +1,204 @@ +#![cfg(feature = "servo-embed")] + +use servo::{Code, Key, KeyState, KeyboardEvent, Location, Modifiers}; +use winit::event::ElementState; +use winit::keyboard::{Key as WinitKey, KeyLocation, ModifiersState, NamedKey, PhysicalKey}; + +pub fn keyboard_event_from_winit( + event: &winit::event::KeyEvent, + modifiers: ModifiersState, +) -> KeyboardEvent { + let state = match event.state { + ElementState::Pressed => KeyState::Down, + ElementState::Released => KeyState::Up, + }; + + let key = match &event.logical_key { + WinitKey::Named(n) => Key::Named(named_key(*n)), + WinitKey::Character(c) => Key::Character(c.to_string().into()), + WinitKey::Unidentified(_) => Key::Unidentified, + WinitKey::Dead(c) => Key::Dead(*c), + }; + + let code = match event.physical_key { + PhysicalKey::Code(c) => winit_code_to_code(c), + PhysicalKey::Unidentified(_) => Code::Unidentified, + }; + + let location = match event.location { + KeyLocation::Standard => Location::Standard, + KeyLocation::Left => Location::Left, + KeyLocation::Right => Location::Right, + KeyLocation::Numpad => Location::Numpad, + }; + + let mut mods = Modifiers::empty(); + if modifiers.shift_key() { + mods |= Modifiers::SHIFT; + } + if modifiers.control_key() { + mods |= Modifiers::CONTROL; + } + if modifiers.alt_key() { + mods |= Modifiers::ALT; + } + if modifiers.super_key() { + mods |= Modifiers::META; + } + + KeyboardEvent { + state, + key, + code, + location, + modifiers: mods, + repeat: event.repeat, + is_composing: false, + } +} + +fn named_key(n: NamedKey) -> servo::NamedKey { + use servo::NamedKey as S; + match n { + NamedKey::Alt => S::Alt, + NamedKey::AltGraph => S::AltGraph, + NamedKey::CapsLock => S::CapsLock, + NamedKey::Control => S::Control, + NamedKey::Fn => S::Fn, + NamedKey::FnLock => S::FnLock, + NamedKey::Meta => S::Meta, + NamedKey::NumLock => S::NumLock, + NamedKey::ScrollLock => S::ScrollLock, + NamedKey::Shift => S::Shift, + NamedKey::Symbol => S::Symbol, + NamedKey::SymbolLock => S::SymbolLock, + NamedKey::Enter => S::Enter, + NamedKey::Tab => S::Tab, + NamedKey::Space => S::Space, + NamedKey::ArrowDown => S::ArrowDown, + NamedKey::ArrowLeft => S::ArrowLeft, + NamedKey::ArrowRight => S::ArrowRight, + NamedKey::ArrowUp => S::ArrowUp, + NamedKey::End => S::End, + NamedKey::Home => S::Home, + NamedKey::PageDown => S::PageDown, + NamedKey::PageUp => S::PageUp, + NamedKey::Backspace => S::Backspace, + NamedKey::Clear => S::Clear, + NamedKey::Copy => S::Copy, + NamedKey::CrSel => S::CrSel, + NamedKey::Cut => S::Cut, + NamedKey::Delete => S::Delete, + NamedKey::EraseEof => S::EraseEof, + NamedKey::ExSel => S::ExSel, + NamedKey::Insert => S::Insert, + NamedKey::Paste => S::Paste, + NamedKey::Redo => S::Redo, + NamedKey::Undo => S::Undo, + NamedKey::Escape => S::Escape, + NamedKey::F1 => S::F1, + NamedKey::F2 => S::F2, + NamedKey::F3 => S::F3, + NamedKey::F4 => S::F4, + NamedKey::F5 => S::F5, + NamedKey::F6 => S::F6, + NamedKey::F7 => S::F7, + NamedKey::F8 => S::F8, + NamedKey::F9 => S::F9, + NamedKey::F10 => S::F10, + NamedKey::F11 => S::F11, + NamedKey::F12 => S::F12, + _ => S::Unidentified, + } +} + +fn winit_code_to_code(c: winit::keyboard::KeyCode) -> Code { + use winit::keyboard::KeyCode as W; + match c { + W::Backquote => Code::Backquote, + W::Backslash => Code::Backslash, + W::BracketLeft => Code::BracketLeft, + W::BracketRight => Code::BracketRight, + W::Comma => Code::Comma, + W::Digit0 => Code::Digit0, + W::Digit1 => Code::Digit1, + W::Digit2 => Code::Digit2, + W::Digit3 => Code::Digit3, + W::Digit4 => Code::Digit4, + W::Digit5 => Code::Digit5, + W::Digit6 => Code::Digit6, + W::Digit7 => Code::Digit7, + W::Digit8 => Code::Digit8, + W::Digit9 => Code::Digit9, + W::Equal => Code::Equal, + W::KeyA => Code::KeyA, + W::KeyB => Code::KeyB, + W::KeyC => Code::KeyC, + W::KeyD => Code::KeyD, + W::KeyE => Code::KeyE, + W::KeyF => Code::KeyF, + W::KeyG => Code::KeyG, + W::KeyH => Code::KeyH, + W::KeyI => Code::KeyI, + W::KeyJ => Code::KeyJ, + W::KeyK => Code::KeyK, + W::KeyL => Code::KeyL, + W::KeyM => Code::KeyM, + W::KeyN => Code::KeyN, + W::KeyO => Code::KeyO, + W::KeyP => Code::KeyP, + W::KeyQ => Code::KeyQ, + W::KeyR => Code::KeyR, + W::KeyS => Code::KeyS, + W::KeyT => Code::KeyT, + W::KeyU => Code::KeyU, + W::KeyV => Code::KeyV, + W::KeyW => Code::KeyW, + W::KeyX => Code::KeyX, + W::KeyY => Code::KeyY, + W::KeyZ => Code::KeyZ, + W::Minus => Code::Minus, + W::Period => Code::Period, + W::Quote => Code::Quote, + W::Semicolon => Code::Semicolon, + W::Slash => Code::Slash, + W::Backspace => Code::Backspace, + W::CapsLock => Code::CapsLock, + W::Enter => Code::Enter, + W::Space => Code::Space, + W::Tab => Code::Tab, + W::Delete => Code::Delete, + W::End => Code::End, + W::Home => Code::Home, + W::Insert => Code::Insert, + W::PageDown => Code::PageDown, + W::PageUp => Code::PageUp, + W::ArrowDown => Code::ArrowDown, + W::ArrowLeft => Code::ArrowLeft, + W::ArrowRight => Code::ArrowRight, + W::ArrowUp => Code::ArrowUp, + W::NumLock => Code::NumLock, + W::Escape => Code::Escape, + W::F1 => Code::F1, + W::F2 => Code::F2, + W::F3 => Code::F3, + W::F4 => Code::F4, + W::F5 => Code::F5, + W::F6 => Code::F6, + W::F7 => Code::F7, + W::F8 => Code::F8, + W::F9 => Code::F9, + W::F10 => Code::F10, + W::F11 => Code::F11, + W::F12 => Code::F12, + W::ShiftLeft => Code::ShiftLeft, + W::ShiftRight => Code::ShiftRight, + W::ControlLeft => Code::ControlLeft, + W::ControlRight => Code::ControlRight, + W::AltLeft => Code::AltLeft, + W::AltRight => Code::AltRight, + W::SuperLeft => Code::MetaLeft, + W::SuperRight => Code::MetaRight, + _ => Code::Unidentified, + } +} diff --git a/crates/weft-servo-shell/src/main.rs b/crates/weft-servo-shell/src/main.rs index 836b83a..aa675c3 100644 --- a/crates/weft-servo-shell/src/main.rs +++ b/crates/weft-servo-shell/src/main.rs @@ -4,6 +4,8 @@ use anyhow::Context; #[cfg(feature = "servo-embed")] mod embedder; +#[cfg(feature = "servo-embed")] +mod keyutils; mod protocols; mod shell_client;