mirror of
https://github.com/marcoallegretti/WEFT_OS.git
synced 2026-03-26 17:03:09 +00:00
feat(appd): per-app process isolation via weft-app-shell
Add weft-app-shell binary: takes <app_id> <session_id> args, connects to zweft_shell_manager_v1 as an application window, resolves the app UI URL, and runs a single Servo WebView in an isolated process. Prints READY to stdout after the window is initialised so weft-appd can track the session lifecycle. weft-appd runtime.rs: after weft-runtime emits READY, spawn weft-app-shell (WEFT_APP_SHELL_BIN env var) alongside it. The app shell is killed when the session ends via abort or natural runtime exit. weft-servo-shell: remove in-process app WebView management. The shell now manages the system UI WebView only; all app rendering happens in dedicated weft-app-shell processes.
This commit is contained in:
parent
3ee2f283d8
commit
a401510b88
11 changed files with 945 additions and 187 deletions
|
|
@ -1,5 +1,6 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"crates/weft-app-shell",
|
||||
"crates/weft-appd",
|
||||
"crates/weft-build-meta",
|
||||
"crates/weft-compositor",
|
||||
|
|
|
|||
25
crates/weft-app-shell/Cargo.toml
Normal file
25
crates/weft-app-shell/Cargo.toml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "weft-app-shell"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "weft-app-shell"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
# Enable actual Servo rendering. Requires manually adding the deps listed in
|
||||
# weft-servo-shell/SERVO_PIN.md to this file before building; they are not
|
||||
# included here to avoid pulling the Servo monorepo (~1 GB) into every
|
||||
# `cargo check` cycle.
|
||||
servo-embed = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
wayland-client = "0.31"
|
||||
wayland-backend = "0.3"
|
||||
wayland-scanner = "0.31"
|
||||
bitflags = "2"
|
||||
380
crates/weft-app-shell/src/embedder.rs
Normal file
380
crates/weft-app-shell/src/embedder.rs
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
#![cfg(feature = "servo-embed")]
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use servo::{
|
||||
EventLoopWaker, InputEvent, MouseButton as ServoMouseButton, MouseButtonAction,
|
||||
MouseButtonEvent, MouseMoveEvent, ServoBuilder, ServoDelegate, ServoUrl, UserContentManager,
|
||||
UserScript, WebViewBuilder, WebViewDelegate,
|
||||
};
|
||||
use winit::{
|
||||
application::ApplicationHandler,
|
||||
event::{ElementState, MouseButton, WindowEvent},
|
||||
event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
|
||||
keyboard::ModifiersState,
|
||||
window::{Window, WindowAttributes, WindowId},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct WeftEventLoopWaker {
|
||||
proxy: Arc<Mutex<EventLoopProxy<ServoWake>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ServoWake;
|
||||
|
||||
impl EventLoopWaker for WeftEventLoopWaker {
|
||||
fn clone_box(&self) -> Box<dyn EventLoopWaker> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn wake(&self) {
|
||||
let _ = self
|
||||
.proxy
|
||||
.lock()
|
||||
.unwrap_or_else(|p| p.into_inner())
|
||||
.send_event(ServoWake);
|
||||
}
|
||||
}
|
||||
|
||||
struct WeftServoDelegate;
|
||||
|
||||
impl ServoDelegate for WeftServoDelegate {
|
||||
fn notify_error(&self, error: servo::ServoError) {
|
||||
tracing::error!(?error, "Servo error");
|
||||
}
|
||||
}
|
||||
|
||||
struct WeftWebViewDelegate {
|
||||
redraw_requested: Arc<std::sync::atomic::AtomicBool>,
|
||||
}
|
||||
|
||||
impl WebViewDelegate for WeftWebViewDelegate {
|
||||
fn notify_new_frame_ready(&self, _webview: servo::WebView) {
|
||||
self.redraw_requested
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
enum RenderingCtx {
|
||||
Software(Rc<servo::SoftwareRenderingContext>),
|
||||
Egl(Rc<servo::WindowRenderingContext>),
|
||||
}
|
||||
|
||||
impl RenderingCtx {
|
||||
fn as_dyn(&self) -> Rc<dyn servo::RenderingContext> {
|
||||
match self {
|
||||
Self::Software(rc) => Rc::clone(rc) as Rc<dyn servo::RenderingContext>,
|
||||
Self::Egl(rc) => Rc::clone(rc) as Rc<dyn servo::RenderingContext>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct App {
|
||||
url: ServoUrl,
|
||||
session_id: u64,
|
||||
ws_port: u16,
|
||||
window: Option<Arc<Window>>,
|
||||
servo: Option<servo::Servo>,
|
||||
webview: Option<servo::WebView>,
|
||||
rendering_context: Option<RenderingCtx>,
|
||||
redraw_requested: Arc<std::sync::atomic::AtomicBool>,
|
||||
waker: WeftEventLoopWaker,
|
||||
shutting_down: bool,
|
||||
ready_signalled: bool,
|
||||
modifiers: ModifiersState,
|
||||
cursor_pos: servo::euclid::default::Point2D<f32>,
|
||||
shell_client: Option<crate::shell_client::ShellClient>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new(
|
||||
url: ServoUrl,
|
||||
session_id: u64,
|
||||
ws_port: u16,
|
||||
waker: WeftEventLoopWaker,
|
||||
shell_client: Option<crate::shell_client::ShellClient>,
|
||||
) -> Self {
|
||||
Self {
|
||||
url,
|
||||
session_id,
|
||||
ws_port,
|
||||
window: None,
|
||||
servo: None,
|
||||
webview: None,
|
||||
rendering_context: None,
|
||||
redraw_requested: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
waker,
|
||||
shutting_down: false,
|
||||
ready_signalled: false,
|
||||
modifiers: ModifiersState::default(),
|
||||
cursor_pos: servo::euclid::default::Point2D::zero(),
|
||||
shell_client,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_frame(window: &Arc<Window>, ctx: &RenderingCtx) {
|
||||
match ctx {
|
||||
RenderingCtx::Software(rc) => Self::blit_software(window, rc),
|
||||
RenderingCtx::Egl(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn blit_software(window: &Arc<Window>, rendering_context: &servo::SoftwareRenderingContext) {
|
||||
let size = window.inner_size();
|
||||
let Some(pixels) = rendering_context.read_pixels() else {
|
||||
return;
|
||||
};
|
||||
let ctx = softbuffer::Context::new(Arc::clone(window)).expect("softbuffer context");
|
||||
let mut surface =
|
||||
softbuffer::Surface::new(&ctx, Arc::clone(window)).expect("softbuffer surface");
|
||||
let _ = surface.resize(
|
||||
std::num::NonZeroU32::new(size.width).unwrap_or(std::num::NonZeroU32::new(1).unwrap()),
|
||||
std::num::NonZeroU32::new(size.height).unwrap_or(std::num::NonZeroU32::new(1).unwrap()),
|
||||
);
|
||||
let Ok(mut buf) = surface.buffer_mut() else {
|
||||
return;
|
||||
};
|
||||
for (dst, src) in buf.iter_mut().zip(pixels.chunks(4)) {
|
||||
*dst = u32::from_be_bytes([0, src[0], src[1], src[2]]);
|
||||
}
|
||||
let _ = buf.present();
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler<ServoWake> for App {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.window.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let title = format!("WEFT App — {}", self.url.host_str().unwrap_or("app"));
|
||||
let attrs = WindowAttributes::default().with_title(title);
|
||||
let window = Arc::new(
|
||||
event_loop
|
||||
.create_window(attrs)
|
||||
.expect("failed to create app window"),
|
||||
);
|
||||
let size = window.inner_size();
|
||||
self.window = Some(Arc::clone(&window));
|
||||
|
||||
let servo = ServoBuilder::default()
|
||||
.event_loop_waker(Box::new(self.waker.clone()))
|
||||
.build();
|
||||
|
||||
servo.set_delegate(Rc::new(WeftServoDelegate));
|
||||
|
||||
let rendering_context = build_rendering_ctx(event_loop, &window, size);
|
||||
|
||||
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 = self.session_id,
|
||||
);
|
||||
ucm.add_script(Rc::new(UserScript::new(bridge_js, None)));
|
||||
|
||||
let webview = WebViewBuilder::new(&servo, rendering_context.as_dyn())
|
||||
.delegate(Rc::new(WeftWebViewDelegate {
|
||||
redraw_requested: Arc::clone(&self.redraw_requested),
|
||||
}))
|
||||
.user_content_manager(ucm)
|
||||
.url(self.url.clone())
|
||||
.build();
|
||||
|
||||
self.servo = Some(servo);
|
||||
self.webview = Some(webview);
|
||||
self.rendering_context = Some(rendering_context);
|
||||
|
||||
if !self.ready_signalled {
|
||||
self.ready_signalled = true;
|
||||
println!("READY");
|
||||
use std::io::Write;
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
}
|
||||
|
||||
fn user_event(&mut self, _event_loop: &ActiveEventLoop, _event: ServoWake) {
|
||||
if let Some(servo) = &self.servo {
|
||||
servo.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.shutting_down {
|
||||
event_loop.exit();
|
||||
return;
|
||||
}
|
||||
if let Some(sc) = &mut self.shell_client {
|
||||
match sc.dispatch_pending() {
|
||||
Ok(false) => {
|
||||
self.shutting_down = true;
|
||||
if let Some(servo) = &self.servo {
|
||||
servo.start_shutting_down();
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::warn!("shell client dispatch error: {e}"),
|
||||
Ok(true) => {}
|
||||
}
|
||||
}
|
||||
if let Some(servo) = &self.servo {
|
||||
servo.spin_event_loop();
|
||||
}
|
||||
if self
|
||||
.redraw_requested
|
||||
.swap(false, std::sync::atomic::Ordering::Relaxed)
|
||||
{
|
||||
if let Some(w) = &self.window {
|
||||
w.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
_window_id: WindowId,
|
||||
event: WindowEvent,
|
||||
) {
|
||||
match event {
|
||||
WindowEvent::RedrawRequested => {
|
||||
if let (Some(window), Some(servo)) = (&self.window, &self.servo) {
|
||||
if let Some(rc) = &self.rendering_context {
|
||||
Self::render_frame(window, rc);
|
||||
}
|
||||
servo.spin_event_loop();
|
||||
}
|
||||
}
|
||||
WindowEvent::Resized(new_size) => {
|
||||
let sz = servo::euclid::Size2D::new(new_size.width, new_size.height);
|
||||
if let Some(wv) = &self.webview {
|
||||
wv.resize(sz);
|
||||
}
|
||||
}
|
||||
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 {
|
||||
servo.start_shutting_down();
|
||||
}
|
||||
event_loop.exit();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_rendering_ctx(
|
||||
event_loop: &ActiveEventLoop,
|
||||
window: &Arc<Window>,
|
||||
size: winit::dpi::PhysicalSize<u32>,
|
||||
) -> RenderingCtx {
|
||||
if std::env::var_os("WEFT_EGL_RENDERING").is_some() {
|
||||
let display_handle = event_loop.display_handle();
|
||||
let window_handle = window.window_handle();
|
||||
if let (Ok(dh), Ok(wh)) = (display_handle, window_handle) {
|
||||
match servo::WindowRenderingContext::new(dh, wh, size) {
|
||||
Ok(rc) => {
|
||||
tracing::info!("using EGL rendering context");
|
||||
return RenderingCtx::Egl(Rc::new(rc));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("EGL rendering context failed ({e}), falling back to software");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RenderingCtx::Software(Rc::new(
|
||||
servo::SoftwareRenderingContext::new(servo::euclid::Size2D::new(size.width, size.height))
|
||||
.expect("SoftwareRenderingContext"),
|
||||
))
|
||||
}
|
||||
|
||||
fn resolve_weft_app_url(app_id: &str) -> Option<ServoUrl> {
|
||||
let url_str = format!("weft-app://{app_id}/index.html");
|
||||
let raw = ServoUrl::parse(&url_str).ok()?;
|
||||
let rel = raw.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<PathBuf> {
|
||||
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(
|
||||
app_id: &str,
|
||||
session_id: u64,
|
||||
ws_port: u16,
|
||||
shell_client: Option<crate::shell_client::ShellClient>,
|
||||
) -> anyhow::Result<()> {
|
||||
let url = resolve_weft_app_url(app_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("no ui/index.html found for app {app_id}"))?;
|
||||
|
||||
let event_loop = EventLoop::<ServoWake>::with_user_event()
|
||||
.build()
|
||||
.map_err(|e| anyhow::anyhow!("event loop: {e}"))?;
|
||||
|
||||
let waker = WeftEventLoopWaker {
|
||||
proxy: Arc::new(Mutex::new(event_loop.create_proxy())),
|
||||
};
|
||||
|
||||
let mut app = App::new(url, session_id, ws_port, waker, shell_client);
|
||||
event_loop
|
||||
.run_app(&mut app)
|
||||
.map_err(|e| anyhow::anyhow!("event loop run: {e}"))
|
||||
}
|
||||
204
crates/weft-app-shell/src/keyutils.rs
Normal file
204
crates/weft-app-shell/src/keyutils.rs
Normal file
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
80
crates/weft-app-shell/src/main.rs
Normal file
80
crates/weft-app-shell/src/main.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
mod protocols;
|
||||
mod shell_client;
|
||||
|
||||
#[cfg(feature = "servo-embed")]
|
||||
mod embedder;
|
||||
#[cfg(feature = "servo-embed")]
|
||||
mod keyutils;
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
)
|
||||
.init();
|
||||
|
||||
let mut args = std::env::args().skip(1);
|
||||
let app_id = args
|
||||
.next()
|
||||
.context("usage: weft-app-shell <app_id> <session_id>")?;
|
||||
let session_id: u64 = args
|
||||
.next()
|
||||
.context("usage: weft-app-shell <app_id> <session_id>")?
|
||||
.parse()
|
||||
.context("session_id must be a number")?;
|
||||
|
||||
let ws_port = appd_ws_port();
|
||||
|
||||
let shell = match shell_client::ShellClient::connect_as_app(&app_id, session_id) {
|
||||
Ok(c) => {
|
||||
tracing::info!("app shell window registered with compositor");
|
||||
Some(c)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "shell protocol unavailable; continuing without compositor registration");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
embed_app(&app_id, session_id, ws_port, shell)
|
||||
}
|
||||
|
||||
fn appd_ws_port() -> u16 {
|
||||
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
|
||||
let port_file = std::path::PathBuf::from(runtime_dir).join("weft/appd.wsport");
|
||||
if let Ok(s) = std::fs::read_to_string(port_file) {
|
||||
if let Ok(n) = s.trim().parse() {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(s) = std::env::var("WEFT_APPD_WS_PORT") {
|
||||
if let Ok(n) = s.parse() {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
7410
|
||||
}
|
||||
|
||||
fn embed_app(
|
||||
app_id: &str,
|
||||
session_id: u64,
|
||||
ws_port: u16,
|
||||
shell_client: Option<shell_client::ShellClient>,
|
||||
) -> anyhow::Result<()> {
|
||||
#[cfg(feature = "servo-embed")]
|
||||
return embedder::run(app_id, session_id, ws_port, shell_client);
|
||||
|
||||
#[cfg(not(feature = "servo-embed"))]
|
||||
{
|
||||
let _ = (app_id, session_id, ws_port, shell_client);
|
||||
println!("READY");
|
||||
use std::io::Write;
|
||||
let _ = std::io::stdout().flush();
|
||||
std::thread::park();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
20
crates/weft-app-shell/src/protocols/mod.rs
Normal file
20
crates/weft-app-shell/src/protocols/mod.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#[allow(dead_code, non_camel_case_types, unused_unsafe, unused_variables)]
|
||||
#[allow(non_upper_case_globals, non_snake_case, unused_imports)]
|
||||
#[allow(missing_docs, clippy::all)]
|
||||
pub mod client {
|
||||
use wayland_client;
|
||||
use wayland_client::protocol::*;
|
||||
|
||||
pub mod __interfaces {
|
||||
use wayland_client::protocol::__interfaces::*;
|
||||
wayland_scanner::generate_interfaces!("../../protocol/weft-shell-unstable-v1.xml");
|
||||
}
|
||||
use self::__interfaces::*;
|
||||
|
||||
wayland_scanner::generate_client_code!("../../protocol/weft-shell-unstable-v1.xml");
|
||||
}
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use client::zweft_shell_manager_v1::ZweftShellManagerV1;
|
||||
#[allow(unused_imports)]
|
||||
pub use client::zweft_shell_window_v1::ZweftShellWindowV1;
|
||||
197
crates/weft-app-shell/src/shell_client.rs
Normal file
197
crates/weft-app-shell/src/shell_client.rs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
use anyhow::Context;
|
||||
use wayland_client::{
|
||||
Connection, Dispatch, EventQueue, QueueHandle,
|
||||
protocol::{wl_registry, wl_surface::WlSurface},
|
||||
};
|
||||
|
||||
use crate::protocols::{
|
||||
ZweftShellManagerV1, ZweftShellWindowV1,
|
||||
client::{zweft_shell_manager_v1, zweft_shell_window_v1},
|
||||
};
|
||||
|
||||
pub struct ShellWindowState {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
pub state_flags: u32,
|
||||
pub focused: bool,
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
struct AppData {
|
||||
manager: Option<ZweftShellManagerV1>,
|
||||
window: Option<ZweftShellWindowV1>,
|
||||
window_state: ShellWindowState,
|
||||
}
|
||||
|
||||
impl AppData {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
manager: None,
|
||||
window: None,
|
||||
window_state: ShellWindowState {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
state_flags: 0,
|
||||
focused: false,
|
||||
closed: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, ()> for AppData {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
registry: &wl_registry::WlRegistry,
|
||||
event: wl_registry::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
if let wl_registry::Event::Global {
|
||||
name,
|
||||
interface,
|
||||
version,
|
||||
} = event
|
||||
&& interface == "zweft_shell_manager_v1"
|
||||
{
|
||||
let mgr = registry.bind::<ZweftShellManagerV1, _, _>(name, version.min(1), qh, ());
|
||||
state.manager = Some(mgr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZweftShellManagerV1, ()> for AppData {
|
||||
fn event(
|
||||
_: &mut Self,
|
||||
_: &ZweftShellManagerV1,
|
||||
_event: zweft_shell_manager_v1::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZweftShellWindowV1, ()> for AppData {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
window: &ZweftShellWindowV1,
|
||||
event: zweft_shell_window_v1::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
zweft_shell_window_v1::Event::Configure {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
state: flags,
|
||||
} => {
|
||||
let ws = &mut state.window_state;
|
||||
ws.x = x;
|
||||
ws.y = y;
|
||||
ws.width = width;
|
||||
ws.height = height;
|
||||
ws.state_flags = flags;
|
||||
tracing::debug!(x, y, width, height, flags, "app shell window configure");
|
||||
}
|
||||
zweft_shell_window_v1::Event::FocusChanged { focused } => {
|
||||
state.window_state.focused = focused != 0;
|
||||
tracing::debug!(focused, "app shell window focus changed");
|
||||
}
|
||||
zweft_shell_window_v1::Event::WindowClosed => {
|
||||
tracing::info!("app shell window closed by compositor");
|
||||
state.window_state.closed = true;
|
||||
window.destroy();
|
||||
}
|
||||
zweft_shell_window_v1::Event::PresentationFeedback {
|
||||
tv_sec,
|
||||
tv_nsec,
|
||||
refresh,
|
||||
} => {
|
||||
tracing::trace!(tv_sec, tv_nsec, refresh, "app shell presentation feedback");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct ShellClient {
|
||||
event_queue: EventQueue<AppData>,
|
||||
data: AppData,
|
||||
}
|
||||
|
||||
impl ShellClient {
|
||||
pub fn connect_as_app(app_id: &str, session_id: u64) -> anyhow::Result<Self> {
|
||||
let conn =
|
||||
Connection::connect_to_env().context("failed to connect to Wayland compositor")?;
|
||||
|
||||
let mut event_queue = conn.new_event_queue::<AppData>();
|
||||
let qh = event_queue.handle();
|
||||
|
||||
conn.display().get_registry(&qh, ());
|
||||
|
||||
let mut data = AppData::new();
|
||||
event_queue
|
||||
.roundtrip(&mut data)
|
||||
.context("Wayland globals roundtrip")?;
|
||||
|
||||
anyhow::ensure!(
|
||||
data.manager.is_some(),
|
||||
"zweft_shell_manager_v1 not advertised; WEFT compositor must be running"
|
||||
);
|
||||
|
||||
let manager = data.manager.as_ref().unwrap();
|
||||
let title = format!("{app_id}/{session_id}");
|
||||
let window = manager.create_window(
|
||||
app_id.to_string(),
|
||||
title,
|
||||
"application".to_string(),
|
||||
None::<&WlSurface>,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
&qh,
|
||||
(),
|
||||
);
|
||||
data.window = Some(window);
|
||||
|
||||
event_queue
|
||||
.roundtrip(&mut data)
|
||||
.context("Wayland create_window roundtrip")?;
|
||||
|
||||
tracing::info!(
|
||||
app_id,
|
||||
session_id,
|
||||
x = data.window_state.x,
|
||||
y = data.window_state.y,
|
||||
width = data.window_state.width,
|
||||
height = data.window_state.height,
|
||||
"app shell window registered with compositor"
|
||||
);
|
||||
|
||||
Ok(Self { event_queue, data })
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn dispatch_pending(&mut self) -> anyhow::Result<bool> {
|
||||
self.event_queue
|
||||
.dispatch_pending(&mut self.data)
|
||||
.context("Wayland dispatch")?;
|
||||
self.event_queue.flush().context("Wayland flush")?;
|
||||
Ok(!self.data.window_state.closed)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn window_state(&self) -> &ShellWindowState {
|
||||
&self.data.window_state
|
||||
}
|
||||
}
|
||||
|
|
@ -95,6 +95,36 @@ async fn kill_portal(portal: Option<(PathBuf, tokio::process::Child)>) {
|
|||
}
|
||||
}
|
||||
|
||||
async fn spawn_app_shell(
|
||||
session_id: u64,
|
||||
app_id: &str,
|
||||
) -> Option<tokio::process::Child> {
|
||||
let bin = std::env::var("WEFT_APP_SHELL_BIN").ok()?;
|
||||
let mut cmd = tokio::process::Command::new(&bin);
|
||||
cmd.arg(app_id)
|
||||
.arg(session_id.to_string())
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null());
|
||||
match cmd.spawn() {
|
||||
Ok(child) => {
|
||||
tracing::info!(session_id, %app_id, bin = %bin, "app shell spawned");
|
||||
Some(child)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(session_id, %app_id, error = %e, "failed to spawn app shell");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn kill_app_shell(child: Option<tokio::process::Child>) {
|
||||
if let Some(mut c) = child {
|
||||
let _ = c.kill().await;
|
||||
let _ = c.wait().await;
|
||||
}
|
||||
}
|
||||
|
||||
fn portal_socket_path(session_id: u64) -> Option<PathBuf> {
|
||||
let runtime_dir = std::env::var("XDG_RUNTIME_DIR").ok()?;
|
||||
let dir = PathBuf::from(runtime_dir).join("weft");
|
||||
|
|
@ -226,6 +256,7 @@ pub(crate) async fn supervise(
|
|||
_ = &mut abort_rx => None,
|
||||
};
|
||||
|
||||
let mut app_shell: Option<tokio::process::Child> = None;
|
||||
match ready_result {
|
||||
Some(Ok(Ok(remaining_stdout))) => {
|
||||
registry
|
||||
|
|
@ -238,6 +269,7 @@ pub(crate) async fn supervise(
|
|||
});
|
||||
tracing::info!(session_id, %app_id, "app ready");
|
||||
tokio::spawn(drain_stdout(remaining_stdout, session_id));
|
||||
app_shell = spawn_app_shell(session_id, app_id).await;
|
||||
}
|
||||
Some(Ok(Err(e))) => {
|
||||
tracing::warn!(session_id, %app_id, error = %e, "stdout read error before READY");
|
||||
|
|
@ -281,6 +313,8 @@ pub(crate) async fn supervise(
|
|||
}
|
||||
}
|
||||
|
||||
kill_app_shell(app_shell).await;
|
||||
|
||||
if let Some(tx) = &compositor_tx {
|
||||
let _ = tx
|
||||
.send(AppdToCompositor::AppSurfaceDestroyed { session_id })
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
#![cfg(feature = "servo-embed")]
|
||||
|
||||
use std::sync::mpsc;
|
||||
|
||||
pub enum AppdCmd {
|
||||
Launch { session_id: u64, app_id: String },
|
||||
Stop { session_id: u64 },
|
||||
SyncSessions { active: Vec<(u64, String)> },
|
||||
}
|
||||
|
||||
pub fn spawn_appd_listener(
|
||||
ws_port: u16,
|
||||
tx: mpsc::SyncSender<AppdCmd>,
|
||||
wake: Box<dyn Fn() + Send>,
|
||||
) {
|
||||
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<AppdCmd>, wake: Box<dyn Fn() + Send>) {
|
||||
let url = format!("ws://127.0.0.1:{ws_port}");
|
||||
let mut backoff = std::time::Duration::from_millis(500);
|
||||
const MAX_BACKOFF: std::time::Duration = std::time::Duration::from_secs(16);
|
||||
|
||||
loop {
|
||||
match tungstenite::connect(&url) {
|
||||
Err(e) => {
|
||||
tracing::debug!("appd WebSocket connect failed: {e}; retry in {backoff:?}");
|
||||
std::thread::sleep(backoff);
|
||||
backoff = (backoff * 2).min(MAX_BACKOFF);
|
||||
continue;
|
||||
}
|
||||
Ok((mut ws, _)) => {
|
||||
backoff = std::time::Duration::from_millis(500);
|
||||
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(e) => {
|
||||
tracing::debug!("appd WebSocket read error: {e}; reconnecting");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::thread::sleep(backoff);
|
||||
backoff = (backoff * 2).min(MAX_BACKOFF);
|
||||
}
|
||||
}
|
||||
|
||||
fn process_message(text: &str, tx: &mpsc::SyncSender<AppdCmd>, wake: &dyn Fn()) {
|
||||
let Ok(v) = serde_json::from_str::<serde_json::Value>(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;
|
||||
};
|
||||
let active: Vec<(u64, String)> = sessions
|
||||
.iter()
|
||||
.filter_map(|s| {
|
||||
let sid = s["session_id"].as_u64()?;
|
||||
let aid = s["app_id"].as_str()?.to_string();
|
||||
Some((sid, aid))
|
||||
})
|
||||
.collect();
|
||||
if tx.try_send(AppdCmd::SyncSessions { active }).is_ok() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
#![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::{
|
||||
|
|
@ -91,9 +89,6 @@ struct App {
|
|||
servo: Option<servo::Servo>,
|
||||
webview: Option<servo::WebView>,
|
||||
rendering_context: Option<RenderingCtx>,
|
||||
app_webviews: HashMap<u64, servo::WebView>,
|
||||
active_session: Option<u64>,
|
||||
app_rx: mpsc::Receiver<crate::appd_ws::AppdCmd>,
|
||||
redraw_requested: Arc<std::sync::atomic::AtomicBool>,
|
||||
waker: WeftEventLoopWaker,
|
||||
shutting_down: bool,
|
||||
|
|
@ -107,7 +102,6 @@ impl App {
|
|||
url: ServoUrl,
|
||||
waker: WeftEventLoopWaker,
|
||||
ws_port: u16,
|
||||
app_rx: mpsc::Receiver<crate::appd_ws::AppdCmd>,
|
||||
shell_client: Option<crate::shell_client::ShellClient>,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
|
@ -117,9 +111,6 @@ impl App {
|
|||
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,
|
||||
|
|
@ -129,43 +120,6 @@ 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) {
|
||||
if self.app_webviews.contains_key(&session_id) {
|
||||
return;
|
||||
}
|
||||
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.as_dyn())
|
||||
.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 render_frame(window: &Arc<Window>, ctx: &RenderingCtx) {
|
||||
match ctx {
|
||||
RenderingCtx::Software(rc) => Self::blit_software(window, rc),
|
||||
|
|
@ -261,32 +215,6 @@ impl ApplicationHandler<ServoWake> for App {
|
|||
Ok(true) => {}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
crate::appd_ws::AppdCmd::SyncSessions { active } => {
|
||||
let active_ids: std::collections::HashSet<u64> =
|
||||
active.iter().map(|(sid, _)| *sid).collect();
|
||||
self.app_webviews.retain(|sid, _| active_ids.contains(sid));
|
||||
if let Some(cur) = self.active_session {
|
||||
if !active_ids.contains(&cur) {
|
||||
self.active_session = None;
|
||||
}
|
||||
}
|
||||
for (session_id, app_id) in active {
|
||||
self.create_app_webview(session_id, &app_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(servo) = &self.servo {
|
||||
servo.spin_event_loop();
|
||||
}
|
||||
|
|
@ -320,15 +248,12 @@ impl ApplicationHandler<ServoWake> for App {
|
|||
if let Some(wv) = &self.webview {
|
||||
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.active_webview() {
|
||||
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));
|
||||
}
|
||||
|
|
@ -336,7 +261,7 @@ impl ApplicationHandler<ServoWake> 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.active_webview() {
|
||||
if let Some(wv) = &self.webview {
|
||||
let _ = wv.notify_input_event(InputEvent::MouseMove(MouseMoveEvent::new(pt)));
|
||||
}
|
||||
}
|
||||
|
|
@ -351,7 +276,7 @@ impl ApplicationHandler<ServoWake> for App {
|
|||
ElementState::Pressed => MouseButtonAction::Click,
|
||||
ElementState::Released => MouseButtonAction::Up,
|
||||
};
|
||||
if let Some(wv) = self.active_webview() {
|
||||
if let Some(wv) = &self.webview {
|
||||
let _ = wv.notify_input_event(InputEvent::MouseButton(MouseButtonEvent::new(
|
||||
action,
|
||||
btn,
|
||||
|
|
@ -451,11 +376,7 @@ pub fn run(
|
|||
proxy: Arc::new(Mutex::new(event_loop.create_proxy())),
|
||||
};
|
||||
|
||||
let (app_tx, app_rx) = mpsc::sync_channel::<crate::appd_ws::AppdCmd>(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, shell_client);
|
||||
let mut app = App::new(url, waker, ws_port, shell_client);
|
||||
event_loop
|
||||
.run_app(&mut app)
|
||||
.map_err(|e| anyhow::anyhow!("event loop run: {e}"))
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ use std::path::PathBuf;
|
|||
|
||||
use anyhow::Context;
|
||||
|
||||
#[cfg(feature = "servo-embed")]
|
||||
mod appd_ws;
|
||||
#[cfg(feature = "servo-embed")]
|
||||
mod embedder;
|
||||
#[cfg(feature = "servo-embed")]
|
||||
|
|
|
|||
Loading…
Reference in a new issue