diff --git a/Cargo.lock b/Cargo.lock index 0537c02..dfee955 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4072,8 +4072,10 @@ version = "0.1.0" dependencies = [ "anyhow", "bitflags 2.11.0", + "serde_json", "tracing", "tracing-subscriber", + "tungstenite", "wayland-backend", "wayland-client", "wayland-scanner", diff --git a/crates/weft-servo-shell/Cargo.toml b/crates/weft-servo-shell/Cargo.toml index d1f952d..ee981ee 100644 --- a/crates/weft-servo-shell/Cargo.toml +++ b/crates/weft-servo-shell/Cargo.toml @@ -22,3 +22,5 @@ wayland-client = "0.31" wayland-backend = "0.3" wayland-scanner = "0.31" bitflags = "2" +serde_json = "1" +tungstenite = "0.24" diff --git a/crates/weft-servo-shell/src/appd_ws.rs b/crates/weft-servo-shell/src/appd_ws.rs new file mode 100644 index 0000000..d5831d7 --- /dev/null +++ b/crates/weft-servo-shell/src/appd_ws.rs @@ -0,0 +1,88 @@ +#![cfg(feature = "servo-embed")] + +use std::sync::mpsc; + +pub enum AppdCmd { + Launch { session_id: u64, app_id: String }, + Stop { session_id: u64 }, +} + +pub fn spawn_appd_listener( + ws_port: u16, + tx: mpsc::SyncSender, + wake: Box, +) { + std::thread::Builder::new() + .name("appd-ws".into()) + .spawn(move || run_listener(ws_port, tx, wake)) + .ok(); +} + +fn run_listener(ws_port: u16, tx: mpsc::SyncSender, wake: Box) { + let url = format!("ws://127.0.0.1:{ws_port}"); + let (mut ws, _) = match tungstenite::connect(url) { + Ok(p) => p, + Err(e) => { + tracing::warn!("appd WebSocket connect failed: {e}"); + return; + } + }; + + let _ = ws.send(tungstenite::Message::Text( + r#"{"type":"QUERY_RUNNING"}"#.into(), + )); + + loop { + match ws.read() { + Ok(tungstenite::Message::Text(text)) => { + process_message(&text, &tx, &*wake); + } + Ok(_) => {} + Err(_) => break, + } + } +} + +fn process_message(text: &str, tx: &mpsc::SyncSender, wake: &dyn Fn()) { + let Ok(v) = serde_json::from_str::(text) else { + return; + }; + + match v["type"].as_str() { + Some("LAUNCH_ACK") => { + let Some(session_id) = v["session_id"].as_u64() else { + return; + }; + let Some(app_id) = v["app_id"].as_str().map(str::to_string) else { + return; + }; + if tx.try_send(AppdCmd::Launch { session_id, app_id }).is_ok() { + wake(); + } + } + Some("RUNNING_APPS") => { + let Some(sessions) = v["sessions"].as_array() else { + return; + }; + for s in sessions { + let Some(session_id) = s["session_id"].as_u64() else { + continue; + }; + let Some(app_id) = s["app_id"].as_str().map(str::to_string) else { + continue; + }; + let _ = tx.try_send(AppdCmd::Launch { session_id, app_id }); + } + wake(); + } + Some("APP_STATE") if v["state"].as_str() == Some("stopped") => { + let Some(session_id) = v["session_id"].as_u64() else { + return; + }; + if tx.try_send(AppdCmd::Stop { session_id }).is_ok() { + wake(); + } + } + _ => {} + } +} diff --git a/crates/weft-servo-shell/src/embedder.rs b/crates/weft-servo-shell/src/embedder.rs index 281324d..f471951 100644 --- a/crates/weft-servo-shell/src/embedder.rs +++ b/crates/weft-servo-shell/src/embedder.rs @@ -1,7 +1,9 @@ #![cfg(feature = "servo-embed")] +use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::rc::Rc; +use std::sync::mpsc; use std::sync::{Arc, Mutex}; use servo::{ @@ -72,6 +74,10 @@ struct App { window: Option>, servo: Option, webview: Option, + rendering_context: Option>, + app_webviews: HashMap, + active_session: Option, + app_rx: mpsc::Receiver, redraw_requested: Arc, waker: WeftEventLoopWaker, shutting_down: bool, @@ -80,13 +86,22 @@ struct App { } impl App { - fn new(url: ServoUrl, waker: WeftEventLoopWaker, ws_port: u16) -> Self { + fn new( + url: ServoUrl, + waker: WeftEventLoopWaker, + ws_port: u16, + app_rx: mpsc::Receiver, + ) -> Self { Self { url, ws_port, window: None, servo: None, webview: None, + rendering_context: None, + app_webviews: HashMap::new(), + active_session: None, + app_rx, redraw_requested: Arc::new(std::sync::atomic::AtomicBool::new(false)), waker, shutting_down: false, @@ -95,6 +110,40 @@ impl App { } } + fn active_webview(&self) -> Option<&servo::WebView> { + self.active_session + .and_then(|sid| self.app_webviews.get(&sid)) + .or(self.webview.as_ref()) + } + + fn create_app_webview(&mut self, session_id: u64, app_id: &str) { + let (Some(servo), Some(rc)) = (&self.servo, &self.rendering_context) else { + return; + }; + let url_str = format!("weft-app://{app_id}/index.html"); + let raw = match ServoUrl::parse(&url_str) { + Ok(u) => u, + Err(_) => return, + }; + let url = resolve_weft_app_url(&raw).unwrap_or(raw); + let ucm = Rc::new(UserContentManager::new(servo)); + let bridge_js = format!( + r#"(function(){{var ws=new WebSocket('ws://127.0.0.1:{p}');var sid={sid};var q=[];var r=false;ws.onopen=function(){{r=true;q.forEach(function(m){{ws.send(JSON.stringify(m))}});q.length=0}};window.weftSessionId=sid;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, + sid = session_id, + ); + ucm.add_script(Rc::new(UserScript::new(bridge_js, None))); + let wv = WebViewBuilder::new(servo, Rc::clone(rc)) + .delegate(Rc::new(WeftWebViewDelegate { + redraw_requested: Arc::clone(&self.redraw_requested), + })) + .user_content_manager(ucm) + .url(url) + .build(); + self.app_webviews.insert(session_id, wv); + self.active_session = Some(session_id); + } + fn blit_to_window(window: &Arc, rendering_context: &servo::SoftwareRenderingContext) { let size = window.inner_size(); let Some(pixels) = rendering_context.read_pixels() else { @@ -163,6 +212,7 @@ impl ApplicationHandler for App { self.servo = Some(servo); self.webview = Some(webview); + self.rendering_context = Some(rendering_context); } fn user_event(&mut self, _event_loop: &ActiveEventLoop, _event: ServoWake) { @@ -176,6 +226,19 @@ impl ApplicationHandler for App { event_loop.exit(); return; } + while let Ok(cmd) = self.app_rx.try_recv() { + match cmd { + crate::appd_ws::AppdCmd::Launch { session_id, app_id } => { + self.create_app_webview(session_id, &app_id); + } + crate::appd_ws::AppdCmd::Stop { session_id } => { + self.app_webviews.remove(&session_id); + if self.active_session == Some(session_id) { + self.active_session = None; + } + } + } + } if let Some(servo) = &self.servo { servo.spin_event_loop(); } @@ -198,7 +261,7 @@ impl ApplicationHandler for App { match event { WindowEvent::RedrawRequested => { if let (Some(window), Some(servo)) = (&self.window, &self.servo) { - if let Some(wv) = &self.webview { + if let Some(wv) = self.active_webview() { let rc = wv.rendering_context(); Self::blit_to_window(window, rc); } @@ -206,15 +269,19 @@ impl ApplicationHandler for App { } } WindowEvent::Resized(new_size) => { + let sz = servo::euclid::Size2D::new(new_size.width, new_size.height); if let Some(wv) = &self.webview { - wv.resize(servo::euclid::Size2D::new(new_size.width, new_size.height)); + wv.resize(sz); + } + for wv in self.app_webviews.values() { + wv.resize(sz); } } WindowEvent::ModifiersChanged(mods) => { self.modifiers = mods.state(); } WindowEvent::KeyboardInput { event, .. } => { - if let Some(wv) = &self.webview { + if let Some(wv) = self.active_webview() { let ev = super::keyutils::keyboard_event_from_winit(&event, self.modifiers); let _ = wv.notify_input_event(InputEvent::Keyboard(ev)); } @@ -222,7 +289,7 @@ impl ApplicationHandler for App { 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 { + if let Some(wv) = self.active_webview() { let _ = wv.notify_input_event(InputEvent::MouseMove(MouseMoveEvent::new(pt))); } } @@ -237,7 +304,7 @@ impl ApplicationHandler for App { ElementState::Pressed => MouseButtonAction::Click, ElementState::Released => MouseButtonAction::Up, }; - if let Some(wv) = &self.webview { + if let Some(wv) = self.active_webview() { let _ = wv.notify_input_event(InputEvent::MouseButton(MouseButtonEvent::new( action, btn, @@ -305,7 +372,11 @@ 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, ws_port); + let (app_tx, app_rx) = mpsc::sync_channel::(64); + let waker_for_thread = waker.clone(); + crate::appd_ws::spawn_appd_listener(ws_port, app_tx, Box::new(move || waker_for_thread.wake())); + + let mut app = App::new(url, waker, ws_port, app_rx); event_loop .run_app(&mut app) .map_err(|e| anyhow::anyhow!("event loop run: {e}")) diff --git a/crates/weft-servo-shell/src/main.rs b/crates/weft-servo-shell/src/main.rs index aa675c3..bab4280 100644 --- a/crates/weft-servo-shell/src/main.rs +++ b/crates/weft-servo-shell/src/main.rs @@ -2,6 +2,8 @@ use std::path::PathBuf; use anyhow::Context; +#[cfg(feature = "servo-embed")] +mod appd_ws; #[cfg(feature = "servo-embed")] mod embedder; #[cfg(feature = "servo-embed")]