Cry
This commit is contained in:
parent
d28e50fcf9
commit
83cdaffd05
9 changed files with 415 additions and 78 deletions
|
|
@ -1156,6 +1156,29 @@ function connectToCompositor(): Promise<void> {
|
||||||
continue;
|
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<string, string>;
|
||||||
|
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.
|
// 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');
|
debugLog('[Wo] UNKNOWN magic 0x' + magic.toString(16).padStart(8, '0') + ' bufLen=' + compositorRxBuffer.length + ' — stream may be corrupted');
|
||||||
compositorRxBuffer = compositorRxBuffer.slice(1);
|
compositorRxBuffer = compositorRxBuffer.slice(1);
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,14 @@ const compositorAPI: CompositorAPI = {
|
||||||
return () => ipcRenderer.removeListener('wo:pointer-lock-request', handler);
|
return () => ipcRenderer.removeListener('wo:pointer-lock-request', handler);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Environment variable updates from compositor (e.g. DISPLAY after XWayland ready)
|
||||||
|
onEnvUpdate: (callback: (vars: Record<string, string>) => void) => {
|
||||||
|
const handler = (_event: Electron.IpcRendererEvent, vars: Record<string, string>) =>
|
||||||
|
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
|
// Generic portal request event for client-provided popup/approval handlers
|
||||||
onPortalRequest: (callback: (data: {
|
onPortalRequest: (callback: (data: {
|
||||||
requestId: string;
|
requestId: string;
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export const MAGIC = {
|
||||||
FORWARD_KEYBOARD: 0x574F4B45, // "WOKE" — forwarded keyboard from web UI canvas
|
FORWARD_KEYBOARD: 0x574F4B45, // "WOKE" — forwarded keyboard from web UI canvas
|
||||||
FORWARD_RELATIVE_POINTER: 0x574F5245, // "WORE" — forwarded relative pointer (dx/dy)
|
FORWARD_RELATIVE_POINTER: 0x574F5245, // "WORE" — forwarded relative pointer (dx/dy)
|
||||||
POINTER_LOCK_REQUEST: 0x574F504C, // "WOPL" — server-to-client pointer lock request
|
POINTER_LOCK_REQUEST: 0x574F504C, // "WOPL" — server-to-client pointer lock request
|
||||||
|
ENV_UPDATE: 0x574F4555, // "WOEU" — environment variable update broadcast
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -90,6 +91,11 @@ export interface PointerLockRequestEvent {
|
||||||
lock: boolean;
|
lock: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EnvUpdateEvent {
|
||||||
|
type: 'envUpdate';
|
||||||
|
vars: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
export type InputEvent =
|
export type InputEvent =
|
||||||
| MouseMoveEvent
|
| MouseMoveEvent
|
||||||
| MouseButtonEvent
|
| MouseButtonEvent
|
||||||
|
|
@ -97,7 +103,8 @@ export type InputEvent =
|
||||||
| ScrollEvent
|
| ScrollEvent
|
||||||
| FocusChangeEvent
|
| FocusChangeEvent
|
||||||
| WindowMetadataEvent
|
| WindowMetadataEvent
|
||||||
| PointerLockRequestEvent;
|
| PointerLockRequestEvent
|
||||||
|
| EnvUpdateEvent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compositor Actions from Client → Compositor
|
* Compositor Actions from Client → Compositor
|
||||||
|
|
|
||||||
6
electron/types/types.d.ts
vendored
6
electron/types/types.d.ts
vendored
|
|
@ -278,6 +278,12 @@ export interface Compositor {
|
||||||
*/
|
*/
|
||||||
onPointerLockRequest(callback: (data: { window: string; lock: boolean }) => void): Unsubscribe;
|
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<string, string>) => void): Unsubscribe;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe to generic portal requests so clients can provide custom UI.
|
* Subscribe to generic portal requests so clients can provide custom UI.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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_BUTTON: u32 = 0x574F5042; // "WOPB"
|
||||||
pub const MAGIC_FORWARD_POINTER_SCROLL: u32 = 0x574F5053; // "WOPS"
|
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_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)]
|
#[repr(C, packed)]
|
||||||
|
|
@ -143,6 +144,8 @@ pub enum ElectronInputEvent {
|
||||||
FocusChange { window_name: String, focused: bool },
|
FocusChange { window_name: String, focused: bool },
|
||||||
WindowMetadata { metadata: String }, // JSON payload
|
WindowMetadata { metadata: String }, // JSON payload
|
||||||
PointerLockRequest { window_name: String, lock: bool },
|
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.
|
/// A bidirectional connection to an Electron client.
|
||||||
|
|
@ -435,6 +438,14 @@ impl ElectronClientConnection {
|
||||||
buf[12..].copy_from_slice(name_bytes);
|
buf[12..].copy_from_slice(name_bytes);
|
||||||
self.write_bytes(&buf)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -502,6 +513,14 @@ impl ElectronClientConnection {
|
||||||
buf[12..].copy_from_slice(name_bytes);
|
buf[12..].copy_from_slice(name_bytes);
|
||||||
self.write_bytes_nonblocking(&buf)
|
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(())
|
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).
|
/// Send syscall response to a window (non-blocking).
|
||||||
/// If the socket buffer is full, the response is dropped and a warning is
|
/// 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
|
/// logged. This is preferable to blocking the compositor event loop — the
|
||||||
|
|
|
||||||
|
|
@ -228,33 +228,7 @@ impl WoState {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In nested mode, pointer events are routed through Electron/comraw.
|
self.dispatch_pointer_to_targets(event.time_msec());
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_pointer_motion_abs<B: InputBackend>(&mut self, event: B::PointerMotionAbsoluteEvent) {
|
fn on_pointer_motion_abs<B: InputBackend>(&mut self, event: B::PointerMotionAbsoluteEvent) {
|
||||||
|
|
@ -329,23 +303,30 @@ impl WoState {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In nested mode, pointer events are routed through Electron/comraw.
|
self.dispatch_pointer_to_targets(event.time_msec());
|
||||||
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 {
|
/// 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},
|
||||||
|
};
|
||||||
|
|
||||||
|
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(ref ipc) = self.electron_ipc {
|
||||||
if let Some(local) = self.local_window_coords(
|
if let Some(local) = self.local_window_coords(win_name, pos) {
|
||||||
win_name,
|
if let Some(client) = ipc.clients.lock().unwrap().get(win_name).cloned() {
|
||||||
(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 {
|
let _ = client.try_send_input_event(&ElectronInputEvent::MouseMove {
|
||||||
x: local.0,
|
x: local.0,
|
||||||
y: local.1,
|
y: local.1,
|
||||||
|
|
@ -354,8 +335,58 @@ impl WoState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Not over an Electron window — dispatch directly to native windows.
|
||||||
|
let pos_logical: Point<f64, Logical> = 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::<f64, Logical>::from((local_x, local_y)), WindowSurfaceType::ALL)
|
||||||
|
.map(|(s, offset)| {
|
||||||
|
let surface_global: Point<f64, Logical> = (
|
||||||
|
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<B: InputBackend>(&mut self, event: B::PointerButtonEvent) {
|
fn on_pointer_button<B: InputBackend>(&mut self, event: B::PointerButtonEvent) {
|
||||||
|
|
@ -488,10 +519,13 @@ impl WoState {
|
||||||
Some(w) => w,
|
Some(w) => w,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
let win_loc = self.space.element_location(&window).unwrap_or_default();
|
|
||||||
let global_x = win_loc.x as f64 + x;
|
// Do NOT overwrite self.pointer_location here. The hardware input
|
||||||
let global_y = win_loc.y as f64 + y;
|
// handler (on_pointer_motion / on_pointer_motion_abs) already set it
|
||||||
self.pointer_location = (global_x, global_y).into();
|
// 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() {
|
let pointer = match self.seat.get_pointer() {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
|
|
|
||||||
173
src/main.rs
173
src/main.rs
|
|
@ -648,6 +648,11 @@ fn render_frame(data: &mut EventLoopData) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
for element in wayland_elements {
|
||||||
let src = element.src();
|
let src = element.src();
|
||||||
let dst = element.geometry(scale);
|
let dst = element.geometry(scale);
|
||||||
|
|
@ -660,6 +665,7 @@ fn render_frame(data: &mut EventLoopData) {
|
||||||
trace!("Element rendering failed: {e}");
|
trace!("Element rendering failed: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Render cursor on top of everything
|
// Render cursor on top of everything
|
||||||
if !cursor_elements.is_empty() {
|
if !cursor_elements.is_empty() {
|
||||||
|
|
@ -702,9 +708,9 @@ fn render_frame(data: &mut EventLoopData) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let dirty_surfaces: Vec<_> = data.state.dirty_surfaces.drain().collect();
|
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(
|
smithay::desktop::utils::send_frames_surface_tree(
|
||||||
&surface,
|
surface,
|
||||||
&data.state.backend.output,
|
&data.state.backend.output,
|
||||||
presented_at,
|
presented_at,
|
||||||
Some(data.frame_time),
|
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<u8>)> = 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<u8>)> =
|
||||||
|
with_states(surface, |states| {
|
||||||
|
let rss_data = states.data_map.get::<RendererSurfaceStateUserData>()?;
|
||||||
|
let rss = rss_data.lock().ok()?;
|
||||||
|
let buffer = rss.buffer()?;
|
||||||
|
|
||||||
|
let mut result: Option<(u32, u32, u32, Vec<u8>)> = 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::<RendererSurfaceStateUserData>()?;
|
||||||
|
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
|
// Send frame callbacks for the cursor surface so the client keeps
|
||||||
// animating it (e.g. spinning/loading cursors).
|
// animating it (e.g. spinning/loading cursors).
|
||||||
if let smithay::input::pointer::CursorImageStatus::Surface(ref cursor_surface) = data.state.cursor_status {
|
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.damaged_windows.clear();
|
||||||
data.state.dirty_surfaces.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.
|
// Get or create the DRM framebuffer for this slot.
|
||||||
// Caching in the slot's userdata avoids a create/destroy cycle every
|
// Caching in the slot's userdata avoids a create/destroy cycle every
|
||||||
// frame and — critically — keeps the GbmFramebuffer alive through
|
// 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)>(),
|
std::iter::empty::<(String, String)>(),
|
||||||
true,
|
true,
|
||||||
std::process::Stdio::null(),
|
std::process::Stdio::null(),
|
||||||
std::process::Stdio::null(),
|
std::process::Stdio::inherit(),
|
||||||
|_| {},
|
|_| {},
|
||||||
) {
|
) {
|
||||||
Ok((xwayland, xw_client)) => {
|
Ok((xwayland, xw_client)) => {
|
||||||
|
|
@ -1306,6 +1452,14 @@ fn run_drm(mut config: Config) -> Result<(), anyhow::Error> {
|
||||||
} => {
|
} => {
|
||||||
info!("XWayland ready on DISPLAY :{display_number}");
|
info!("XWayland ready on DISPLAY :{display_number}");
|
||||||
std::env::set_var("DISPLAY", format!(":{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() {
|
if let Some(client) = xw_client_cb.borrow_mut().take() {
|
||||||
match X11Wm::start_wm(wm_handle.clone(), x11_socket, client) {
|
match X11Wm::start_wm(wm_handle.clone(), x11_socket, client) {
|
||||||
Ok(wm) => {
|
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);
|
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 app_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("electron");
|
||||||
let electron_render_node = render_node_for_card(&gpu_path);
|
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)
|
// Execute any due autostart tasks (scheduled at startup)
|
||||||
if !data.autostart_tasks.is_empty() {
|
if !data.autostart_tasks.is_empty() {
|
||||||
let now = std::time::Instant::now();
|
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)> =
|
let mut pending: Vec<(crate::config::AutostartConfig, std::time::Instant)> =
|
||||||
Vec::new();
|
Vec::new();
|
||||||
for (task, when) in data.autostart_tasks.drain(..) {
|
for (task, when) in data.autostart_tasks.drain(..) {
|
||||||
if when <= now {
|
if when <= now {
|
||||||
let cmd = task.command.clone();
|
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/<name> built-in action
|
// Handle simple wo://window/<name> built-in action
|
||||||
if let Some(name) = cmd.strip_prefix("wo://window/") {
|
if let Some(name) = cmd.strip_prefix("wo://window/") {
|
||||||
if let Some(win_cfg) =
|
if let Some(win_cfg) =
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
std::iter::empty::<(String, String)>(),
|
std::iter::empty::<(String, String)>(),
|
||||||
true, // open abstract socket for X11 compatibility
|
true, // open abstract socket for X11 compatibility
|
||||||
std::process::Stdio::null(),
|
std::process::Stdio::null(),
|
||||||
std::process::Stdio::null(),
|
std::process::Stdio::inherit(),
|
||||||
|_| {}, // no extra user data
|
|_| {}, // no extra user data
|
||||||
) {
|
) {
|
||||||
Ok((xwayland, xw_client)) => {
|
Ok((xwayland, xw_client)) => {
|
||||||
|
|
@ -196,6 +196,14 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
} => {
|
} => {
|
||||||
info!("XWayland ready on DISPLAY :{display_number}");
|
info!("XWayland ready on DISPLAY :{display_number}");
|
||||||
std::env::set_var("DISPLAY", format!(":{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() {
|
if let Some(client) = xw_client_cb.borrow_mut().take() {
|
||||||
match X11Wm::start_wm(wm_handle.clone(), x11_socket, client) {
|
match X11Wm::start_wm(wm_handle.clone(), x11_socket, client) {
|
||||||
Ok(wm) => {
|
Ok(wm) => {
|
||||||
|
|
@ -224,6 +232,7 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
|
|
||||||
std::env::set_var("WAYLAND_DISPLAY", &config.compositor.socket_name);
|
std::env::set_var("WAYLAND_DISPLAY", &config.compositor.socket_name);
|
||||||
info!("Set 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");
|
let app_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("electron");
|
||||||
// Nested mode has no DRM card; use the first available render node.
|
// 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_x = px;
|
||||||
let surface_y = py;
|
let surface_y = py;
|
||||||
|
|
||||||
let global: Point<f64, Logical> = (
|
// Do NOT overwrite state.pointer_location.
|
||||||
loc.x as f64 - geo.loc.x.max(0) as f64 + surface_x,
|
// The hardware input handler already set it
|
||||||
loc.y as f64 - geo.loc.y.max(0) as f64 + surface_y,
|
// to the authoritative position; recomputing
|
||||||
)
|
// from element_location + geo causes ping-pong.
|
||||||
.into();
|
|
||||||
state.pointer_location = global;
|
|
||||||
let under = window.surface_under(
|
let under = window.surface_under(
|
||||||
(surface_x, surface_y),
|
(surface_x, surface_y),
|
||||||
smithay::desktop::WindowSurfaceType::ALL,
|
smithay::desktop::WindowSurfaceType::ALL,
|
||||||
|
|
@ -1376,7 +1383,7 @@ fn process_electron_message(
|
||||||
state,
|
state,
|
||||||
focus,
|
focus,
|
||||||
&smithay::input::pointer::MotionEvent {
|
&smithay::input::pointer::MotionEvent {
|
||||||
location: global,
|
location: state.pointer_location,
|
||||||
serial,
|
serial,
|
||||||
time,
|
time,
|
||||||
},
|
},
|
||||||
|
|
@ -1415,12 +1422,8 @@ fn process_electron_message(
|
||||||
let surface_x = px;
|
let surface_x = px;
|
||||||
let surface_y = py;
|
let surface_y = py;
|
||||||
|
|
||||||
let global: Point<f64, Logical> = (
|
// Do NOT overwrite state.pointer_location
|
||||||
loc.x as f64 - geo.loc.x.max(0) as f64 + surface_x,
|
// (same rationale as pointer_motion above).
|
||||||
loc.y as f64 - geo.loc.y.max(0) as f64 + surface_y,
|
|
||||||
)
|
|
||||||
.into();
|
|
||||||
state.pointer_location = global;
|
|
||||||
if pressed {
|
if pressed {
|
||||||
let surface = window
|
let surface = window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
|
|
|
||||||
52
src/state.rs
52
src/state.rs
|
|
@ -650,3 +650,55 @@ impl KeyboardShortcutsInhibitHandler for WoState {
|
||||||
}
|
}
|
||||||
|
|
||||||
smithay::delegate_keyboard_shortcuts_inhibit!(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<String> = 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}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue