diff --git a/electron/src/main.ts b/electron/src/main.ts index 01a3e36..74c166b 100644 --- a/electron/src/main.ts +++ b/electron/src/main.ts @@ -1156,6 +1156,29 @@ function connectToCompositor(): Promise { continue; } + if (magic === MAGIC.ENV_UPDATE) { + // magic(4) + json_len(4) + json(N) = 8 + N bytes + if (compositorRxBuffer.length < 8) break; + const jsonLen = compositorRxBuffer.readUInt32LE(4); + if (compositorRxBuffer.length < 8 + jsonLen) break; + const jsonStr = compositorRxBuffer.slice(8, 8 + jsonLen).toString('utf8'); + compositorRxBuffer = compositorRxBuffer.slice(8 + jsonLen); + + try { + const vars = JSON.parse(jsonStr) as Record; + for (const [key, value] of Object.entries(vars)) { + process.env[key] = value; + debugLog(`[Wo] env update: ${key}=${value}`); + } + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('wo:env-update', vars); + } + } catch (err) { + debugLog('[Wo] ENV_UPDATE parse error:', err); + } + continue; + } + // Unknown message magic; resync by advancing one byte. debugLog('[Wo] UNKNOWN magic 0x' + magic.toString(16).padStart(8, '0') + ' bufLen=' + compositorRxBuffer.length + ' — stream may be corrupted'); compositorRxBuffer = compositorRxBuffer.slice(1); diff --git a/electron/src/preload.ts b/electron/src/preload.ts index 737404c..780150c 100644 --- a/electron/src/preload.ts +++ b/electron/src/preload.ts @@ -91,6 +91,14 @@ const compositorAPI: CompositorAPI = { return () => ipcRenderer.removeListener('wo:pointer-lock-request', handler); }, + // Environment variable updates from compositor (e.g. DISPLAY after XWayland ready) + onEnvUpdate: (callback: (vars: Record) => void) => { + const handler = (_event: Electron.IpcRendererEvent, vars: Record) => + callback(vars); + ipcRenderer.on('wo:env-update', handler); + return () => ipcRenderer.removeListener('wo:env-update', handler); + }, + // Generic portal request event for client-provided popup/approval handlers onPortalRequest: (callback: (data: { requestId: string; diff --git a/electron/src/protocol.ts b/electron/src/protocol.ts index 53555e6..0adf67c 100644 --- a/electron/src/protocol.ts +++ b/electron/src/protocol.ts @@ -30,6 +30,7 @@ export const MAGIC = { FORWARD_KEYBOARD: 0x574F4B45, // "WOKE" — forwarded keyboard from web UI canvas FORWARD_RELATIVE_POINTER: 0x574F5245, // "WORE" — forwarded relative pointer (dx/dy) POINTER_LOCK_REQUEST: 0x574F504C, // "WOPL" — server-to-client pointer lock request + ENV_UPDATE: 0x574F4555, // "WOEU" — environment variable update broadcast } as const; /** @@ -90,6 +91,11 @@ export interface PointerLockRequestEvent { lock: boolean; } +export interface EnvUpdateEvent { + type: 'envUpdate'; + vars: Record; +} + export type InputEvent = | MouseMoveEvent | MouseButtonEvent @@ -97,7 +103,8 @@ export type InputEvent = | ScrollEvent | FocusChangeEvent | WindowMetadataEvent - | PointerLockRequestEvent; + | PointerLockRequestEvent + | EnvUpdateEvent; /** * Compositor Actions from Client → Compositor diff --git a/electron/types/types.d.ts b/electron/types/types.d.ts index ba7bc1b..89059c6 100644 --- a/electron/types/types.d.ts +++ b/electron/types/types.d.ts @@ -278,6 +278,12 @@ export interface Compositor { */ onPointerLockRequest(callback: (data: { window: string; lock: boolean }) => void): Unsubscribe; + /** + * Subscribe to environment variable updates from the compositor + * (e.g. DISPLAY becoming available after XWayland starts). + */ + onEnvUpdate(callback: (vars: Record) => void): Unsubscribe; + /** * Subscribe to generic portal requests so clients can provide custom UI. */ diff --git a/src/electron.rs b/src/electron.rs index 720c9fe..93bfc06 100644 --- a/src/electron.rs +++ b/src/electron.rs @@ -48,6 +48,7 @@ pub const MAGIC_FORWARD_RELATIVE_POINTER: u32 = 0x574F5245; // "WORE" pub const MAGIC_FORWARD_POINTER_BUTTON: u32 = 0x574F5042; // "WOPB" pub const MAGIC_FORWARD_POINTER_SCROLL: u32 = 0x574F5053; // "WOPS" pub const MAGIC_POINTER_LOCK_REQUEST: u32 = 0x574F504C; // "WOPL" — server-to-client pointer lock request +pub const MAGIC_ENV_UPDATE: u32 = 0x574F4555; // "WOEU" — environment variable update broadcast #[repr(C, packed)] @@ -143,6 +144,8 @@ pub enum ElectronInputEvent { FocusChange { window_name: String, focused: bool }, WindowMetadata { metadata: String }, // JSON payload PointerLockRequest { window_name: String, lock: bool }, + /// Environment variable update (e.g. DISPLAY after XWayland is ready). + EnvUpdate { vars: String }, } /// A bidirectional connection to an Electron client. @@ -435,6 +438,14 @@ impl ElectronClientConnection { buf[12..].copy_from_slice(name_bytes); self.write_bytes(&buf)?; }, + ElectronInputEvent::EnvUpdate { vars } => { + let var_bytes = vars.as_bytes(); + let mut buf = vec![0u8; 8 + var_bytes.len()]; + buf[0..4].copy_from_slice(&MAGIC_ENV_UPDATE.to_le_bytes()); + buf[4..8].copy_from_slice(&(var_bytes.len() as u32).to_le_bytes()); + buf[8..].copy_from_slice(var_bytes); + self.write_bytes(&buf)?; + }, } Ok(()) } @@ -502,6 +513,14 @@ impl ElectronClientConnection { buf[12..].copy_from_slice(name_bytes); self.write_bytes_nonblocking(&buf) } + ElectronInputEvent::EnvUpdate { vars } => { + let var_bytes = vars.as_bytes(); + let mut buf = vec![0u8; 8 + var_bytes.len()]; + buf[0..4].copy_from_slice(&MAGIC_ENV_UPDATE.to_le_bytes()); + buf[4..8].copy_from_slice(&(var_bytes.len() as u32).to_le_bytes()); + buf[8..].copy_from_slice(var_bytes); + self.write_bytes_nonblocking(&buf) + } } } } @@ -667,6 +686,24 @@ impl ElectronIpc { Ok(()) } + /// Broadcast environment variable updates to all connected Electron clients. + /// `vars` is a JSON object like `{"DISPLAY":":0"}`. + pub fn broadcast_env_update(&self, vars: &str) -> Result<()> { + let snapshot: Vec<_> = { + let clients = self.clients.lock().unwrap(); + clients.values().cloned().collect() + }; + let event = ElectronInputEvent::EnvUpdate { + vars: vars.to_string(), + }; + for client in &snapshot { + if let Err(e) = client.try_send_input_event(&event) { + warn!("env update broadcast to '{}' failed: {e:#}", client.name); + } + } + Ok(()) + } + /// Send syscall response to a window (non-blocking). /// If the socket buffer is full, the response is dropped and a warning is /// logged. This is preferable to blocking the compositor event loop — the diff --git a/src/handlers/input.rs b/src/handlers/input.rs index cc69e85..bc3c135 100644 --- a/src/handlers/input.rs +++ b/src/handlers/input.rs @@ -228,33 +228,7 @@ impl WoState { return; } - // In nested mode, pointer events are routed through Electron/comraw. - if self.electron_ipc.is_some() { - let under_window = - self.window_under((self.pointer_location.x, self.pointer_location.y)); - - if self.pointer_window_focus != under_window { - self.pointer_window_focus = under_window.clone(); - } - - if let Some(ref win_name) = under_window { - if let Some(ref ipc) = self.electron_ipc { - if let Some(local) = self.local_window_coords( - win_name, - (self.pointer_location.x, self.pointer_location.y), - ) { - if let Some(client) = ipc.clients.lock().unwrap().get(win_name).cloned() { - // Non-blocking send. - let _ = client.try_send_input_event(&ElectronInputEvent::MouseMove { - x: local.0, - y: local.1, - }); - } - } - } - } - return; - } + self.dispatch_pointer_to_targets(event.time_msec()); } fn on_pointer_motion_abs(&mut self, event: B::PointerMotionAbsoluteEvent) { @@ -329,33 +303,90 @@ impl WoState { return; } - // In nested mode, pointer events are routed through Electron/comraw. - if self.electron_ipc.is_some() { - let under_window = - self.window_under((self.pointer_location.x, self.pointer_location.y)); + self.dispatch_pointer_to_targets(event.time_msec()); + } - if self.pointer_window_focus != under_window { - self.pointer_window_focus = under_window.clone(); - } + /// Route the current pointer location to Electron windows (IPC) and/or + /// native Wayland/X11 windows (Smithay pointer.motion). Called from both + /// relative and absolute pointer motion handlers. + fn dispatch_pointer_to_targets(&mut self, time: u32) { + use smithay::{ + desktop::WindowSurfaceType, + input::pointer::MotionEvent, + utils::{Logical, Point}, + }; - if let Some(ref win_name) = under_window { - if let Some(ref ipc) = self.electron_ipc { - if let Some(local) = self.local_window_coords( - win_name, - (self.pointer_location.x, self.pointer_location.y), - ) { - let client = ipc.clients.lock().unwrap().get(win_name).cloned(); - if let Some(client) = client { - let _ = client.try_send_input_event(&ElectronInputEvent::MouseMove { - x: local.0, - y: local.1, - }); - } + let pos = (self.pointer_location.x, self.pointer_location.y); + + // Route to Electron-managed window if cursor is over one + let under_electron = self.window_under(pos); + if self.pointer_window_focus != under_electron { + self.pointer_window_focus = under_electron.clone(); + } + if let Some(ref win_name) = under_electron { + if let Some(ref ipc) = self.electron_ipc { + if let Some(local) = self.local_window_coords(win_name, pos) { + if let Some(client) = ipc.clients.lock().unwrap().get(win_name).cloned() { + let _ = client.try_send_input_event(&ElectronInputEvent::MouseMove { + x: local.0, + y: local.1, + }); } } } + } + + let pointer = match self.seat.get_pointer() { + Some(p) => p, + None => return, + }; + let serial = SERIAL_COUNTER.next_serial(); + + // When the cursor is over an Electron-managed window, the compositor + // must NOT touch Smithay's pointer state at all. Electron/comraw owns + // the cursor: it renders the cursor sprite, tracks position inside its + // canvas, and — when the cursor is over an embedded native Wayland/X11 + // window — sends a ForwardedPointer IPC back to the compositor. That + // forwarded path (handle_forwarded_pointer_event) is the ONLY code that + // should call pointer.motion() for native windows. + // + // Previously this block called pointer.motion(None) to "clear" native + // focus, but Smithay interprets that as a real motion event and sends + // wl_pointer.leave to any focused client. When Electron's forwarded + // event arrives a moment later and calls pointer.motion(Some(focus)), + // Smithay sends wl_pointer.enter — so the client sees + // leave→enter→leave→enter every frame, causing cursor rubberbanding. + if under_electron.is_some() && self.electron_ipc.is_some() { return; } + + // Not over an Electron window — dispatch directly to native windows. + let pos_logical: Point = self.pointer_location; + let under_native = self.space.element_under(pos_logical); + let focus = under_native.and_then(|(window, _loc)| { + let win_loc = self.space.element_location(&window)?; + let local_x = self.pointer_location.x - win_loc.x as f64; + let local_y = self.pointer_location.y - win_loc.y as f64; + window + .surface_under(Point::::from((local_x, local_y)), WindowSurfaceType::ALL) + .map(|(s, offset)| { + let surface_global: Point = ( + win_loc.x as f64 + offset.x as f64, + win_loc.y as f64 + offset.y as f64, + ).into(); + (s, surface_global) + }) + }); + pointer.motion( + self, + focus, + &MotionEvent { + location: self.pointer_location, + serial, + time, + }, + ); + pointer.frame(self); } fn on_pointer_button(&mut self, event: B::PointerButtonEvent) { @@ -488,10 +519,13 @@ impl WoState { Some(w) => w, None => return, }; - let win_loc = self.space.element_location(&window).unwrap_or_default(); - let global_x = win_loc.x as f64 + x; - let global_y = win_loc.y as f64 + y; - self.pointer_location = (global_x, global_y).into(); + + // Do NOT overwrite self.pointer_location here. The hardware input + // handler (on_pointer_motion / on_pointer_motion_abs) already set it + // to the authoritative screen position. Recomputing it from + // element_location + geo offsets uses a different coordinate system + // and causes the position to ping-pong between the hardware value + // and the recomputed value, producing visible rubberbanding. let pointer = match self.seat.get_pointer() { Some(p) => p, diff --git a/src/main.rs b/src/main.rs index e105a86..d1ac52d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -648,16 +648,22 @@ fn render_frame(data: &mut EventLoopData) { ); } - for element in wayland_elements { - let src = element.src(); - let dst = element.geometry(scale); - if let Err(e) = element.draw(&mut frame, src, dst, &[data.output_rect], &[]) { - if is_egl_context_or_surface_lost(&e.to_string()) { - error!("EGL context lost during space rendering"); - context_lost = true; - return false; + // Wayland window surfaces are exported to Electron clients via + // DMABUF/SHM passthrough (below), not rendered directly to the DRM + // framebuffer. Only render them directly when there is no Electron IPC + // (i.e. pure compositor mode without a shell). + if data.state.electron_ipc.is_none() { + for element in wayland_elements { + let src = element.src(); + let dst = element.geometry(scale); + if let Err(e) = element.draw(&mut frame, src, dst, &[data.output_rect], &[]) { + if is_egl_context_or_surface_lost(&e.to_string()) { + error!("EGL context lost during space rendering"); + context_lost = true; + return false; + } + trace!("Element rendering failed: {e}"); } - trace!("Element rendering failed: {e}"); } } @@ -702,9 +708,9 @@ fn render_frame(data: &mut EventLoopData) { } let dirty_surfaces: Vec<_> = data.state.dirty_surfaces.drain().collect(); - for surface in dirty_surfaces { + for surface in &dirty_surfaces { smithay::desktop::utils::send_frames_surface_tree( - &surface, + surface, &data.state.backend.output, presented_at, Some(data.frame_time), @@ -712,6 +718,101 @@ fn render_frame(data: &mut EventLoopData) { ); } + // Export dirty Wayland surfaces to Electron clients via DMABUF/SHM + if data.state.electron_ipc.is_some() { + use smithay::backend::renderer::utils::RendererSurfaceStateUserData; + use smithay::reexports::wayland_server::Resource; + use smithay::wayland::compositor::with_states; + use smithay::wayland::shm::with_buffer_contents; + use tracing::debug; + + let mut exported_pixels: Vec<(String, u32, u32, u32, Vec)> = Vec::new(); + + for surface in &dirty_surfaces { + let surface_id = surface.id(); + let (window_name, window_geo) = if let Some(n) = + data.state.wayland_window_names.get(&surface_id).cloned() + { + let sid = surface_id.clone(); + let geo = data.state.space.elements().find_map(|w| { + let wl_id = w.toplevel() + .map(|t| t.wl_surface().id()) + .or_else(|| w.x11_surface().and_then(|x| x.wl_surface().map(|s| s.id()))); + if wl_id == Some(sid.clone()) { + data.state.space.element_geometry(w) + } else { + None + } + }); + (n, geo) + } else { + let sid = surface_id.clone(); + let x11_result = data.state.space.elements().find_map(|w| { + let x11 = w.x11_surface()?; + let wl = x11.wl_surface()?; + if wl.id() == sid.clone() { + let name = data.state.x11_window_names.get(&x11.window_id()).cloned()?; + let geo = data.state.space.element_geometry(w); + Some((name, geo)) + } else { + None + } + }); + match x11_result { + Some((n, geo)) => (n, geo), + None => continue, + } + }; + + let Some(_geo) = window_geo else { continue }; + + // Try SHM buffer first (software-rendered clients) + let pixels_opt: Option<(u32, u32, u32, Vec)> = + with_states(surface, |states| { + let rss_data = states.data_map.get::()?; + let rss = rss_data.lock().ok()?; + let buffer = rss.buffer()?; + + let mut result: Option<(u32, u32, u32, Vec)> = None; + let _ = with_buffer_contents(buffer, |ptr, len, spec| { + let pixel_bytes = + (spec.stride.max(0) * spec.height.max(0)) as usize; + if pixel_bytes > 0 && pixel_bytes <= len { + let slice = + unsafe { std::slice::from_raw_parts(ptr, pixel_bytes) }; + result = Some(( + spec.width as u32, + spec.height as u32, + spec.stride as u32, + slice.to_vec(), + )); + } + }); + result + }); + + if let Some((bw, bh, stride, pixels)) = pixels_opt { + exported_pixels.push((window_name, bw, bh, stride, pixels)); + } else if let Some(ref ipc) = data.state.electron_ipc { + // Try DMABUF (hardware-rendered clients / games) + let dmabuf_opt = with_states(surface, |states| { + let rss_data = states.data_map.get::()?; + let rss = rss_data.lock().ok()?; + let buffer = rss.buffer()?; + smithay::wayland::dmabuf::get_dmabuf(buffer).ok().cloned() + }); + + if let Some(dmabuf) = dmabuf_opt { + if let Err(e) = ipc.broadcast_dmabuf_frame(&window_name, &dmabuf) { + debug!("DRM dmabuf broadcast failed for {}: {}", window_name, e); + } + } + } + } + + data.state.pending_shm_exports = exported_pixels; + } + // Send frame callbacks for the cursor surface so the client keeps // animating it (e.g. spinning/loading cursors). if let smithay::input::pointer::CursorImageStatus::Surface(ref cursor_surface) = data.state.cursor_status { @@ -766,6 +867,51 @@ fn render_frame(data: &mut EventLoopData) { data.state.damaged_windows.clear(); data.state.dirty_surfaces.clear(); + // Write exported SHM pixel buffers to memfd and broadcast to Electron + { + use std::os::unix::io::AsRawFd; + use nix::sys::memfd::MFdFlags; + use tracing::debug; + + let exports = std::mem::take(&mut data.state.pending_shm_exports); + for (window_name, w, h, stride, pixels) in exports { + let memfd_fd = data.state + .window_shm_buffers + .entry(window_name.clone()) + .or_insert_with(|| { + nix::sys::memfd::memfd_create( + std::ffi::CStr::from_bytes_with_nul(b"wayland_window\0").unwrap(), + MFdFlags::MFD_CLOEXEC, + ) + .expect("creating memfd") + }); + + let size = pixels.len() as i64; + let stat = nix::sys::stat::fstat(&*memfd_fd).unwrap(); + if stat.st_size != size { + nix::unistd::ftruncate(&*memfd_fd, size).expect("truncating memfd"); + } + + nix::unistd::lseek(&*memfd_fd, 0, nix::unistd::Whence::SeekSet) + .expect("seeking memfd"); + nix::unistd::write(&*memfd_fd, &pixels).expect("writing to memfd"); + + if let Some(ref ipc) = data.state.electron_ipc { + let pid = std::process::id(); + if let Err(e) = ipc.broadcast_shm_buffer( + &window_name, + w, + h, + stride, + pid, + memfd_fd.as_raw_fd() as i32, + ) { + debug!("DRM shm buffer broadcast failed for {}: {}", window_name, e); + } + } + } + } + // Get or create the DRM framebuffer for this slot. // Caching in the slot's userdata avoids a create/destroy cycle every // frame and — critically — keeps the GbmFramebuffer alive through @@ -1288,7 +1434,7 @@ fn run_drm(mut config: Config) -> Result<(), anyhow::Error> { std::iter::empty::<(String, String)>(), true, std::process::Stdio::null(), - std::process::Stdio::null(), + std::process::Stdio::inherit(), |_| {}, ) { Ok((xwayland, xw_client)) => { @@ -1306,6 +1452,14 @@ fn run_drm(mut config: Config) -> Result<(), anyhow::Error> { } => { info!("XWayland ready on DISPLAY :{display_number}"); std::env::set_var("DISPLAY", format!(":{display_number}")); + crate::state::propagate_environment(&["DISPLAY", "WAYLAND_DISPLAY"]); + // Broadcast DISPLAY to already-running Electron clients + if let Some(ref ipc) = data.state.electron_ipc { + let vars = format!("{{\"DISPLAY\":\":{display_number}\"}}"); + if let Err(e) = ipc.broadcast_env_update(&vars) { + warn!("Failed to broadcast DISPLAY to Electron: {e:#}"); + } + } if let Some(client) = xw_client_cb.borrow_mut().take() { match X11Wm::start_wm(wm_handle.clone(), x11_socket, client) { Ok(wm) => { @@ -1333,6 +1487,8 @@ fn run_drm(mut config: Config) -> Result<(), anyhow::Error> { } std::env::set_var("WAYLAND_DISPLAY", &config.compositor.socket_name); + // Propagate now; DISPLAY will be re-propagated once XWayland is ready. + crate::state::propagate_environment(&["WAYLAND_DISPLAY"]); let app_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("electron"); let electron_render_node = render_node_for_card(&gpu_path); @@ -2057,11 +2213,22 @@ fn run_drm(mut config: Config) -> Result<(), anyhow::Error> { // Execute any due autostart tasks (scheduled at startup) if !data.autostart_tasks.is_empty() { let now = std::time::Instant::now(); + let display_ready = std::env::var("DISPLAY").is_ok(); let mut pending: Vec<(crate::config::AutostartConfig, std::time::Instant)> = Vec::new(); for (task, when) in data.autostart_tasks.drain(..) { if when <= now { let cmd = task.command.clone(); + + // Defer non-builtin commands until DISPLAY is set + // (XWayland ready). X11 apps like Steam need it. + if !display_ready + && !cmd.starts_with("wo://") + { + pending.push((task, now + std::time::Duration::from_millis(200))); + continue; + } + // Handle simple wo://window/ built-in action if let Some(name) = cmd.strip_prefix("wo://window/") { if let Some(win_cfg) = diff --git a/src/nested.rs b/src/nested.rs index 06c69a0..50c9df2 100644 --- a/src/nested.rs +++ b/src/nested.rs @@ -179,7 +179,7 @@ pub fn run_nested(config: Config) -> Result<()> { std::iter::empty::<(String, String)>(), true, // open abstract socket for X11 compatibility std::process::Stdio::null(), - std::process::Stdio::null(), + std::process::Stdio::inherit(), |_| {}, // no extra user data ) { Ok((xwayland, xw_client)) => { @@ -196,6 +196,14 @@ pub fn run_nested(config: Config) -> Result<()> { } => { info!("XWayland ready on DISPLAY :{display_number}"); std::env::set_var("DISPLAY", format!(":{display_number}")); + crate::state::propagate_environment(&["DISPLAY", "WAYLAND_DISPLAY"]); + // Broadcast DISPLAY to already-running Electron clients + if let Some(ref ipc) = state.electron_ipc { + let vars = format!("{{\"DISPLAY\":\":{display_number}\"}}"); + if let Err(e) = ipc.broadcast_env_update(&vars) { + warn!("Failed to broadcast DISPLAY to Electron: {e:#}"); + } + } if let Some(client) = xw_client_cb.borrow_mut().take() { match X11Wm::start_wm(wm_handle.clone(), x11_socket, client) { Ok(wm) => { @@ -224,6 +232,7 @@ pub fn run_nested(config: Config) -> Result<()> { std::env::set_var("WAYLAND_DISPLAY", &config.compositor.socket_name); info!("Set WAYLAND_DISPLAY={}", config.compositor.socket_name); + crate::state::propagate_environment(&["WAYLAND_DISPLAY"]); let app_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("electron"); // Nested mode has no DRM card; use the first available render node. @@ -1351,12 +1360,10 @@ fn process_electron_message( let surface_x = px; let surface_y = py; - let global: Point = ( - loc.x as f64 - geo.loc.x.max(0) as f64 + surface_x, - loc.y as f64 - geo.loc.y.max(0) as f64 + surface_y, - ) - .into(); - state.pointer_location = global; + // Do NOT overwrite state.pointer_location. + // The hardware input handler already set it + // to the authoritative position; recomputing + // from element_location + geo causes ping-pong. let under = window.surface_under( (surface_x, surface_y), smithay::desktop::WindowSurfaceType::ALL, @@ -1376,7 +1383,7 @@ fn process_electron_message( state, focus, &smithay::input::pointer::MotionEvent { - location: global, + location: state.pointer_location, serial, time, }, @@ -1415,12 +1422,8 @@ fn process_electron_message( let surface_x = px; let surface_y = py; - let global: Point = ( - loc.x as f64 - geo.loc.x.max(0) as f64 + surface_x, - loc.y as f64 - geo.loc.y.max(0) as f64 + surface_y, - ) - .into(); - state.pointer_location = global; + // Do NOT overwrite state.pointer_location + // (same rationale as pointer_motion above). if pressed { let surface = window .toplevel() diff --git a/src/state.rs b/src/state.rs index 6d59b1f..97070a4 100644 --- a/src/state.rs +++ b/src/state.rs @@ -650,3 +650,55 @@ impl KeyboardShortcutsInhibitHandler for WoState { } smithay::delegate_keyboard_shortcuts_inhibit!(WoState); + +/// Propagate environment variables to the DBus session and systemd user +/// manager so that applications launched via desktop entries, D-Bus +/// activation, or Steam inherit DISPLAY and WAYLAND_DISPLAY. +pub fn propagate_environment(vars: &[&str]) { + use tracing::{info, warn}; + + let var_args: Vec = vars + .iter() + .filter_map(|&name| { + std::env::var(name).ok().map(|val| format!("{name}={val}")) + }) + .collect(); + + if var_args.is_empty() { + return; + } + + let names: Vec<&str> = vars + .iter() + .filter(|&&name| std::env::var(name).is_ok()) + .copied() + .collect(); + + // dbus-update-activation-environment sets variables for D-Bus-activated services. + let mut dbus_cmd = std::process::Command::new("dbus-update-activation-environment"); + dbus_cmd.arg("--systemd"); + for arg in &var_args { + dbus_cmd.arg(arg); + } + match dbus_cmd.stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null()).spawn() { + Ok(mut child) => { + let _ = child.wait(); + info!("Propagated {:?} to DBus activation environment", names); + } + Err(e) => warn!("dbus-update-activation-environment failed: {e}"), + } + + // systemctl --user import-environment sets variables for systemd user services. + let mut systemctl_cmd = std::process::Command::new("systemctl"); + systemctl_cmd.arg("--user").arg("import-environment"); + for name in &names { + systemctl_cmd.arg(name); + } + match systemctl_cmd.stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null()).spawn() { + Ok(mut child) => { + let _ = child.wait(); + info!("Propagated {:?} to systemd user environment", names); + } + Err(e) => warn!("systemctl --user import-environment failed: {e}"), + } +}