1use std::collections::HashMap;
34use std::os::fd::OwnedFd;
35use std::sync::Arc;
36use std::time::{Duration, Instant};
37
38use smithay::desktop::Window;
39use smithay::input::Seat;
40use smithay::reexports::calloop::LoopHandle;
41use smithay::utils::{Logical, Point, Rectangle};
42use smithay::wayland::compositor::CompositorHandler;
43use smithay::wayland::selection::SelectionTarget;
44use smithay::wayland::selection::data_device::{
45 clear_data_device_selection, current_data_device_selection_userdata,
46 request_data_device_client_selection, set_data_device_selection,
47};
48use smithay::wayland::selection::primary_selection::{
49 clear_primary_selection, current_primary_selection_userdata, request_primary_client_selection,
50 set_primary_selection,
51};
52use smithay::wayland::xwayland_shell::{XWaylandShellHandler, XWaylandShellState};
53use smithay::xwayland::xwm::{
54 Reorder, ResizeEdge as X11ResizeEdge, WmWindowProperty, WmWindowType, XwmId,
55};
56use smithay::xwayland::{X11Surface, X11Wm, XWayland, XWaylandEvent, XwmHandler};
57use x11rb::connection::Connection;
58use x11rb::protocol::xproto::{self, AtomEnum, ConnectionExt, PropMode};
59use x11rb::rust_connection::RustConnection;
60use x11rb::wrapper::ConnectionExt as _;
61
62use crate::wayland::frontend::{AppState, FocusTarget, NEXT_WINDOW_ID, WindowMeta};
63use crate::wayland::wm;
64use crate::wm::{
65 Decoration, Rect, Request, ResizeEdge, Size, SizeHints, States, WindowId, WindowInfo,
66 WindowKind, WindowType,
67};
68
69const RESTART_DELAY: Duration = Duration::from_secs(2);
71const MESA_EGL_VENDOR: &str = "/usr/share/glvnd/egl_vendor.d/50_mesa.json";
73
74pub struct XwmState {
76 pub wm: Option<X11Wm>,
77 pub display: Option<u32>,
78 pub notify: Vec<std::sync::mpsc::Sender<u32>>,
80 pub windows: HashMap<WindowId, Window>,
82 pub restart_at: Option<Instant>,
84 extras: Option<Extras>,
86 pub shell_state: XWaylandShellState,
87}
88
89impl XwmState {
90 pub fn new(shell_state: XWaylandShellState) -> XwmState {
91 XwmState {
92 wm: None,
93 display: None,
94 notify: Vec::new(),
95 windows: HashMap::new(),
96 restart_at: None,
97 extras: None,
98 shell_state,
99 }
100 }
101}
102
103x11rb::atom_manager! {
104 pub Atoms: AtomsCookie {
105 _NET_WORKAREA,
106 _NET_ACTIVE_WINDOW,
107 _NET_CLIENT_LIST_STACKING,
108 _NET_FRAME_EXTENTS,
109 _NET_SUPPORTED,
110 _NET_SUPPORTING_WM_CHECK,
111 _NET_WM_NAME,
112 UTF8_STRING,
113 _NET_WM_STATE_HIDDEN,
114 _NET_WM_ACTION_MOVE,
115 _NET_WM_ACTION_RESIZE,
116 _NET_WM_ACTION_MINIMIZE,
117 _NET_WM_ACTION_MAXIMIZE_HORZ,
118 _NET_WM_ACTION_MAXIMIZE_VERT,
119 _NET_WM_ACTION_FULLSCREEN,
120 _NET_WM_ACTION_CLOSE,
121 _NET_WM_ALLOWED_ACTIONS,
122 }
123}
124
125struct Extras {
127 conn: RustConnection,
128 root: xproto::Window,
129 atoms: Atoms,
130}
131
132impl Extras {
133 fn connect(display: u32) -> Option<Extras> {
134 let (conn, screen) = RustConnection::connect(Some(&format!(":{display}"))).ok()?;
135 let root = conn.setup().roots.get(screen)?.root;
136 let atoms = Atoms::new(&conn).ok()?.reply().ok()?;
137 Some(Extras { conn, root, atoms })
138 }
139
140 fn set_cardinals(&self, window: xproto::Window, property: xproto::Atom, values: &[u32]) {
141 let _ = self.conn.change_property32(
142 PropMode::REPLACE,
143 window,
144 property,
145 AtomEnum::CARDINAL,
146 values,
147 );
148 }
149
150 fn set_windows(&self, window: xproto::Window, property: xproto::Atom, values: &[u32]) {
151 let _ = self.conn.change_property32(
152 PropMode::REPLACE,
153 window,
154 property,
155 AtomEnum::WINDOW,
156 values,
157 );
158 }
159
160 fn set_atoms(&self, window: xproto::Window, property: xproto::Atom, values: &[u32]) {
161 let _ = self.conn.change_property32(
162 PropMode::REPLACE,
163 window,
164 property,
165 AtomEnum::ATOM,
166 values,
167 );
168 }
169
170 fn flush(&self) {
171 let _ = self.conn.flush();
172 }
173}
174
175pub const XWAYLAND_VAR: &str = "PIXELFLUX_XWAYLAND";
178
179fn xwayland_path(
184 named: Option<std::ffi::OsString>,
185 path: Option<std::ffi::OsString>,
186) -> Option<String> {
187 let named = std::path::PathBuf::from(named?);
188 let dir = named.parent()?.to_string_lossy().into_owned();
189 if !named.is_file() || dir.is_empty() {
190 return None;
191 }
192 let rest = path
193 .map(|p| p.to_string_lossy().into_owned())
194 .unwrap_or_default();
195 Some(if rest.is_empty() {
196 dir
197 } else {
198 format!("{dir}:{rest}")
199 })
200}
201
202fn no_render_node_launch() -> (Vec<(&'static str, String)>, Vec<&'static str>) {
206 let mut env = Vec::new();
207 if let Some(path) = xwayland_path(std::env::var_os(XWAYLAND_VAR), std::env::var_os("PATH")) {
208 env.push(("PATH", path));
209 }
210 let has_render_node = std::fs::read_dir("/dev/dri").is_ok_and(|dir| {
211 dir.flatten()
212 .any(|entry| entry.file_name().to_string_lossy().starts_with("renderD"))
213 });
214 if has_render_node {
215 return (env, Vec::new());
216 }
217 println!(
218 "[Wayland] No DRM render node: Xwayland starts on Mesa's EGL with shared-memory buffers."
219 );
220 env.push(("__EGL_EXTERNAL_PLATFORM_CONFIG_FILENAMES", String::new()));
221 if std::path::Path::new(MESA_EGL_VENDOR).exists() {
222 env.push((
223 "__EGL_VENDOR_LIBRARY_FILENAMES",
224 MESA_EGL_VENDOR.to_string(),
225 ));
226 }
227 (env, vec!["-shm"])
228}
229
230pub fn start(
233 state: &mut AppState,
234 handle: LoopHandle<'static, AppState>,
235 notify: Option<std::sync::mpsc::Sender<u32>>,
236) {
237 if let Some(notify) = notify {
238 state.xwm.notify.push(notify);
239 }
240 if state.xwm.wm.is_some() || state.xwm.restart_at.is_some() {
241 if let Some(display) = state.xwm.display {
242 for n in &state.xwm.notify {
243 let _ = n.send(display);
244 }
245 state.xwm.notify.clear();
246 }
247 return;
248 }
249 ensure_x11_socket_dir();
250 let (env, args) = no_render_node_launch();
251 let (xwayland, client) = match XWayland::spawn(
252 &state.dh,
253 None,
254 env,
255 args,
256 true,
257 std::process::Stdio::null(),
258 std::process::Stdio::null(),
259 |_| (),
260 ) {
261 Ok(spawned) => spawned,
262 Err(e) => {
263 eprintln!("[Wayland] Xwayland did not start: {e}; X11 clients have no server.");
264 state.xwm.restart_at = Some(Instant::now() + RESTART_DELAY);
265 return;
266 }
267 };
268 let dh = state.dh.clone();
269 let loop_handle = handle.clone();
270 let inserted = handle.insert_source(xwayland, move |event, _, state| match event {
271 XWaylandEvent::Ready {
272 x11_socket,
273 display_number,
274 } => {
275 state.client_compositor_state(&client).set_client_scale(1.0);
276 match X11Wm::start_wm(loop_handle.clone(), &dh, x11_socket, client.clone()) {
277 Ok(mut wm) => {
278 let image = state.cursor_helper.get_image(1, Duration::ZERO);
279 let _ = wm.set_cursor(
280 &image.pixels_rgba,
281 (image.width as u16, image.height as u16).into(),
282 (image.xhot as u16, image.yhot as u16).into(),
283 );
284 state.xwm.wm = Some(wm);
285 state.xwm.display = Some(display_number);
286 state.xwm.extras = Extras::connect(display_number);
287 if state.xwm.extras.is_none() {
288 eprintln!("[Wayland] Xwayland :{display_number}: no second connection; _NET_WORKAREA and _NET_ACTIVE_WINDOW stay unset.");
289 }
290 publish_supported(state);
291 publish_name(state);
292 publish_workarea(state);
293 println!("[Wayland] Xwayland on display :{display_number}; X11 clients are managed with the Wayland ones.");
294 for n in state.xwm.notify.drain(..) {
295 let _ = n.send(display_number);
296 }
297 wm::x11_display(state, display_number);
298 }
299 Err(e) => {
300 eprintln!("[Wayland] Xwayland window manager did not start: {e}");
301 state.xwm.restart_at = Some(Instant::now() + RESTART_DELAY);
302 }
303 }
304 }
305 XWaylandEvent::Error => {
306 eprintln!("[Wayland] Xwayland exited during startup; starting it again in {RESTART_DELAY:?}.");
307 state.xwm.wm = None;
308 state.xwm.display = None;
309 state.xwm.extras = None;
310 state.xwm.restart_at = Some(Instant::now() + RESTART_DELAY);
311 }
312 });
313 if let Err(e) = inserted {
314 eprintln!("[Wayland] Xwayland source not registered: {e}");
315 }
316}
317
318fn ensure_x11_socket_dir() {
322 use std::os::unix::fs::PermissionsExt;
323 let dir = std::path::Path::new("/tmp/.X11-unix");
324 if dir.is_dir() {
325 return;
326 }
327 if let Err(e) = std::fs::create_dir_all(dir) {
328 eprintln!("[Wayland] {}: not created: {e}", dir.display());
329 return;
330 }
331 let _ = std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o1777));
332}
333
334pub fn maintain(state: &mut AppState, handle: &LoopHandle<'static, AppState>, now: Instant) {
336 if state.xwm.wm.is_none() && state.xwm.restart_at.is_some_and(|at| at <= now) {
337 state.xwm.restart_at = None;
338 start(state, handle.clone(), None);
339 }
340}
341
342fn window_of(state: &AppState, surface: &X11Surface) -> Option<(WindowId, Window)> {
344 state
345 .xwm
346 .windows
347 .iter()
348 .find(|(_, w)| w.x11_surface() == Some(surface))
349 .map(|(id, w)| (*id, w.clone()))
350}
351
352fn window_type_of(surface: &X11Surface) -> WindowType {
353 match surface.window_type() {
354 Some(WmWindowType::Dialog) => WindowType::Dialog,
355 Some(WmWindowType::Utility) => WindowType::Utility,
356 Some(WmWindowType::Toolbar) => WindowType::Toolbar,
357 Some(WmWindowType::Menu) => WindowType::Menu,
358 Some(WmWindowType::Splash) => WindowType::Splash,
359 Some(WmWindowType::Dock) => WindowType::Dock,
360 Some(WmWindowType::Desktop) => WindowType::Desktop,
361 Some(WmWindowType::Notification) => WindowType::Notification,
362 Some(WmWindowType::DropdownMenu) => WindowType::DropdownMenu,
363 Some(WmWindowType::PopupMenu) => WindowType::PopupMenu,
364 Some(WmWindowType::Tooltip) => WindowType::Tooltip,
365 Some(WmWindowType::Combo) => WindowType::Combo,
366 Some(WmWindowType::Dnd) => WindowType::Dnd,
367 Some(WmWindowType::Normal) | None => {
368 if surface.is_transient_for().is_some() {
369 WindowType::Dialog
370 } else {
371 WindowType::Normal
372 }
373 }
374 }
375}
376
377pub fn window_info(
379 state: &AppState,
380 id: WindowId,
381 surface: &X11Surface,
382 output: u32,
383 _decoration: Decoration,
384) -> WindowInfo {
385 let parent = surface.is_transient_for().and_then(|parent| {
386 state
387 .xwm
388 .windows
389 .iter()
390 .find(|(_, w)| w.x11_surface().is_some_and(|s| s.window_id() == parent))
391 .map(|(id, _)| *id)
392 });
393 let hints = surface.size_hints();
394 let geometry = surface.geometry();
395 let decoration = if surface.is_decorated() {
396 Decoration::None
397 } else {
398 Decoration::Server
399 };
400 let accepts_focus = !matches!(
401 surface.input_model(),
402 smithay::xwayland::xwm::WmInputModel::None
403 );
404 WindowInfo {
405 id,
406 kind: Some(WindowKind::X11 {
407 override_redirect: surface.is_override_redirect(),
408 }),
409 window_type: window_type_of(surface),
410 title: surface.title(),
411 app_id: surface.class(),
412 instance: surface.instance(),
413 parent,
414 modal: surface.is_modal(),
415 hints: SizeHints {
416 min: surface.min_size().map(|s| Size { w: s.w, h: s.h }),
417 max: surface.max_size().map(|s| Size { w: s.w, h: s.h }),
418 increment: hints
419 .as_ref()
420 .and_then(|h| h.size_increment)
421 .map(|(w, h)| Size { w, h }),
422 base: surface.base_size().map(|s| Size { w: s.w, h: s.h }),
423 },
424 requested: Some(Rect::new(
425 geometry.loc.x,
426 geometry.loc.y,
427 geometry.size.w,
428 geometry.size.h,
429 )),
430 positioned: hints
431 .as_ref()
432 .is_some_and(|h| h.position.is_some() && (geometry.loc.x != 0 || geometry.loc.y != 0))
433 || !matches!(
434 window_type_of(surface),
435 WindowType::Normal | WindowType::Dialog | WindowType::Utility | WindowType::Toolbar
436 ),
437 decoration,
438 output,
439 pid: surface.pid(),
440 wants_fullscreen: surface.is_fullscreen(),
441 wants_maximized: surface.is_maximized(),
442 wants_minimized: false,
443 skip_taskbar: surface.is_skip_taskbar(),
444 accepts_focus,
445 }
446}
447
448pub fn configure(surface: &X11Surface, rect: Rect, states: States) {
450 if rect.w > 0 && rect.h > 0 {
451 let _ = surface.configure(Some(Rectangle::<i32, Logical>::new(
452 (rect.x, rect.y).into(),
453 (rect.w, rect.h).into(),
454 )));
455 }
456 let _ = surface.set_maximized(states.maximized);
457 let _ = surface.set_fullscreen(states.fullscreen);
458 let _ = surface.set_activated(states.activated);
459 let _ = surface.set_hidden(states.minimized || states.hidden);
460}
461
462fn publish_supported(state: &mut AppState) {
464 let Some(extras) = state.xwm.extras.as_ref() else {
465 return;
466 };
467 let a = &extras.atoms;
468 let supported = [
469 a._NET_WORKAREA,
470 a._NET_ACTIVE_WINDOW,
471 a._NET_CLIENT_LIST_STACKING,
472 a._NET_FRAME_EXTENTS,
473 a._NET_WM_STATE_HIDDEN,
474 a._NET_WM_ALLOWED_ACTIONS,
475 ];
476 let existing: Vec<u32> = extras
477 .conn
478 .get_property(
479 false,
480 extras.root,
481 a._NET_SUPPORTED,
482 AtomEnum::ATOM,
483 0,
484 1024,
485 )
486 .ok()
487 .and_then(|c| c.reply().ok())
488 .and_then(|r| r.value32().map(|v| v.collect()))
489 .unwrap_or_default();
490 let mut all = existing;
491 for atom in supported {
492 if !all.contains(&atom) {
493 all.push(atom);
494 }
495 }
496 extras.set_atoms(extras.root, a._NET_SUPPORTED, &all);
497 extras.flush();
498}
499
500const NET_WM_NAME: &str = "Openbox";
506
507fn publish_name(state: &mut AppState) {
509 let Some(extras) = state.xwm.extras.as_ref() else {
510 return;
511 };
512 let a = &extras.atoms;
513 let check = extras
514 .conn
515 .get_property(
516 false,
517 extras.root,
518 a._NET_SUPPORTING_WM_CHECK,
519 AtomEnum::WINDOW,
520 0,
521 1,
522 )
523 .ok()
524 .and_then(|c| c.reply().ok())
525 .and_then(|r| r.value32().and_then(|mut v| v.next()));
526 let Some(check) = check else {
527 return;
528 };
529 let _ = extras.conn.change_property8(
530 PropMode::REPLACE,
531 check,
532 a._NET_WM_NAME,
533 a.UTF8_STRING,
534 NET_WM_NAME.as_bytes(),
535 );
536 extras.flush();
537}
538
539pub fn publish_workarea(state: &mut AppState) {
541 let Some(extras) = state.xwm.extras.as_ref() else {
542 return;
543 };
544 let workarea = state
545 .wm
546 .as_ref()
547 .and_then(|wm| {
548 wm.outputs
549 .iter()
550 .find(|o| o.primary)
551 .or(wm.outputs.first())
552 .map(|o| o.workarea)
553 })
554 .or_else(|| {
555 state
556 .output_nodes
557 .first()
558 .and_then(|n| n.logical_geometry())
559 .map(|g| Rect::new(g.loc.x, g.loc.y, g.size.w, g.size.h))
560 });
561 if let Some(w) = workarea {
562 extras.set_cardinals(
563 extras.root,
564 extras.atoms._NET_WORKAREA,
565 &[
566 w.x.max(0) as u32,
567 w.y.max(0) as u32,
568 w.w.max(0) as u32,
569 w.h.max(0) as u32,
570 ],
571 );
572 extras.flush();
573 }
574}
575
576pub fn publish_stacking(state: &mut AppState) {
578 let Some(extras) = state.xwm.extras.as_ref() else {
579 return;
580 };
581 let active = state
582 .keyboard_focus
583 .and_then(|id| state.xwm.windows.get(&id))
584 .and_then(|w| w.x11_surface().map(|s| s.window_id()))
585 .unwrap_or(0);
586 extras.set_windows(extras.root, extras.atoms._NET_ACTIVE_WINDOW, &[active]);
587 let stacking: Vec<u32> = state
588 .space
589 .elements()
590 .filter_map(|w| w.x11_surface().map(|s| s.window_id()))
591 .collect();
592 extras.set_windows(
593 extras.root,
594 extras.atoms._NET_CLIENT_LIST_STACKING,
595 &stacking,
596 );
597 extras.flush();
598}
599
600pub fn publish_frame(state: &mut AppState, id: WindowId) {
602 let Some(extras) = state.xwm.extras.as_ref() else {
603 return;
604 };
605 let Some(window) = state.xwm.windows.get(&id) else {
606 return;
607 };
608 let Some(surface) = window.x11_surface() else {
609 return;
610 };
611 let e = wm::frame_extents(state, id);
612 extras.set_cardinals(
613 surface.window_id(),
614 extras.atoms._NET_FRAME_EXTENTS,
615 &[e.left as u32, e.right as u32, e.top as u32, e.bottom as u32],
616 );
617 let a = &extras.atoms;
618 extras.set_atoms(
619 surface.window_id(),
620 a._NET_WM_ALLOWED_ACTIONS,
621 &[
622 a._NET_WM_ACTION_MOVE,
623 a._NET_WM_ACTION_RESIZE,
624 a._NET_WM_ACTION_MINIMIZE,
625 a._NET_WM_ACTION_MAXIMIZE_HORZ,
626 a._NET_WM_ACTION_MAXIMIZE_VERT,
627 a._NET_WM_ACTION_FULLSCREEN,
628 a._NET_WM_ACTION_CLOSE,
629 ],
630 );
631 extras.flush();
632}
633
634impl smithay::input::dnd::DndGrabHandler for AppState {
637 fn dropped(
638 &mut self,
639 _target: Option<smithay::input::dnd::DndTarget<'_, Self>>,
640 _validated: bool,
641 _seat: Seat<Self>,
642 _location: Point<f64, Logical>,
643 ) {
644 self.dnd_icon = None;
645 }
646
647 fn cancelled(&mut self, _seat: Seat<Self>, _location: Point<f64, Logical>) {
648 self.dnd_icon = None;
649 }
650}
651
652impl XWaylandShellHandler for AppState {
653 fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState {
654 &mut self.xwm.shell_state
655 }
656}
657
658impl XwmHandler for AppState {
659 fn xwm_state(&mut self, _xwm: XwmId) -> &mut X11Wm {
660 self.xwm
661 .wm
662 .as_mut()
663 .expect("the X11 window manager that called")
664 }
665
666 fn new_window(&mut self, _xwm: XwmId, _window: X11Surface) {}
667
668 fn new_override_redirect_window(&mut self, _xwm: XwmId, _window: X11Surface) {}
669
670 fn map_window_request(&mut self, _xwm: XwmId, surface: X11Surface) {
671 let output = self.pointer_display();
672 let window = Window::new_x11_window(surface.clone());
673 let id = NEXT_WINDOW_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
674 window
675 .user_data()
676 .insert_if_missing_threadsafe(|| WindowMeta {
677 id,
678 output: std::sync::atomic::AtomicU32::new(output),
679 placed: std::sync::atomic::AtomicBool::new(true),
680 parked: std::sync::atomic::AtomicBool::new(false),
681 });
682 self.xwm.windows.insert(id, window.clone());
683 let _ = surface.set_mapped(true);
684 if wm::active(self) {
685 wm::window_added(self, &window);
686 }
687 let rect = wm::rect_of(self, id)
688 .filter(|r| r.w > 0 && r.h > 0)
689 .unwrap_or_else(|| {
690 let g = surface.geometry();
691 Rect::new(g.loc.x, g.loc.y, g.size.w.max(1), g.size.h.max(1))
692 });
693 let _ = surface.configure(Some(Rectangle::<i32, Logical>::new(
694 (rect.x, rect.y).into(),
695 (rect.w, rect.h).into(),
696 )));
697 self.space
698 .map_element(window.clone(), (rect.x, rect.y), true);
699 wm::window_mapped(
700 self,
701 &window,
702 Size {
703 w: rect.w,
704 h: rect.h,
705 },
706 );
707 crate::wayland::foreign_toplevel::add(self, &window);
708 publish_frame(self, id);
709 publish_stacking(self);
710 if !wm::active(self)
711 && let Some(keyboard) = self.seat.get_keyboard()
712 {
713 keyboard.set_focus(
714 self,
715 Some(FocusTarget::Window(window)),
716 crate::wayland::frontend::next_serial(),
717 );
718 }
719 self.refocus_pointer();
720 }
721
722 fn map_window_notify(&mut self, _xwm: XwmId, _window: X11Surface) {}
723
724 fn mapped_override_redirect_window(&mut self, _xwm: XwmId, surface: X11Surface) {
725 let location = surface.geometry().loc;
726 let window = Window::new_x11_window(surface);
727 window
728 .user_data()
729 .insert_if_missing_threadsafe(|| WindowMeta {
730 id: NEXT_WINDOW_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
731 output: std::sync::atomic::AtomicU32::new(self.pointer_display()),
732 placed: std::sync::atomic::AtomicBool::new(true),
733 parked: std::sync::atomic::AtomicBool::new(false),
734 });
735 self.space.map_element(window, location, true);
736 self.refocus_pointer();
737 for node in &mut self.output_nodes {
738 if let Some(cap) = node.capture.as_mut() {
739 cap.change_pending = true;
740 }
741 }
742 }
743
744 fn unmapped_window(&mut self, _xwm: XwmId, surface: X11Surface) {
745 let found = self
746 .space
747 .elements()
748 .find(|w| w.x11_surface() == Some(&surface))
749 .cloned();
750 if let Some(window) = found {
751 wm::snapshot(self, &window);
752 self.space.unmap_elem(&window);
753 }
754 if let Some((id, window)) = window_of(self, &surface) {
755 wm::window_unmapped(self, &window);
756 crate::wayland::foreign_toplevel::remove(self, id);
757 if let Some(wm) = self.wm.as_mut() {
758 wm.hidden.remove(&id);
759 }
760 }
761 if !surface.is_override_redirect() {
762 let _ = surface.set_mapped(false);
763 }
764 self.refocus_pointer();
765 publish_stacking(self);
766 }
767
768 fn destroyed_window(&mut self, _xwm: XwmId, surface: X11Surface) {
769 if let Some((id, _)) = window_of(self, &surface) {
770 self.xwm.windows.remove(&id);
771 wm::window_removed(self, id);
772 crate::wayland::foreign_toplevel::remove(self, id);
773 }
774 }
775
776 fn configure_request(
777 &mut self,
778 _xwm: XwmId,
779 surface: X11Surface,
780 x: Option<i32>,
781 y: Option<i32>,
782 w: Option<u32>,
783 h: Option<u32>,
784 reorder: Option<Reorder>,
785 ) {
786 let mut geo = surface.geometry();
787 if let Some(x) = x {
788 geo.loc.x = x;
789 }
790 if let Some(y) = y {
791 geo.loc.y = y;
792 }
793 if let Some(w) = w {
794 geo.size.w = w as i32;
795 }
796 if let Some(h) = h {
797 geo.size.h = h as i32;
798 }
799 match window_of(self, &surface) {
800 Some((id, _)) if wm::active(self) => {
801 wm::request(
802 self,
803 id,
804 Request::Geometry(Rect::new(geo.loc.x, geo.loc.y, geo.size.w, geo.size.h)),
805 );
806 match reorder {
807 Some(Reorder::Top) => {
808 wm::request(self, id, Request::Raise);
809 }
810 Some(Reorder::Bottom) => {
811 wm::request(self, id, Request::Lower);
812 }
813 _ => {}
814 }
815 }
816 _ => {
817 let _ = surface.configure(Some(geo));
818 }
819 }
820 }
821
822 fn configure_notify(
823 &mut self,
824 _xwm: XwmId,
825 surface: X11Surface,
826 geometry: Rectangle<i32, Logical>,
827 _above: Option<u32>,
828 ) {
829 if !surface.is_override_redirect() {
830 return;
831 }
832 let found = self
833 .space
834 .elements()
835 .find(|w| w.x11_surface() == Some(&surface))
836 .cloned();
837 if let Some(window) = found {
838 self.space.map_element(window, geometry.loc, false);
839 for node in &mut self.output_nodes {
840 if let Some(cap) = node.capture.as_mut() {
841 cap.change_pending = true;
842 }
843 }
844 }
845 }
846
847 fn property_notify(&mut self, _xwm: XwmId, surface: X11Surface, property: WmWindowProperty) {
848 if matches!(
849 property,
850 WmWindowProperty::Title
851 | WmWindowProperty::Class
852 | WmWindowProperty::TransientFor
853 | WmWindowProperty::WindowType
854 | WmWindowProperty::MotifHints
855 | WmWindowProperty::NormalHints
856 | WmWindowProperty::Hints
857 ) && let Some((id, window)) = window_of(self, &surface)
858 {
859 wm::window_changed(self, &window);
860 crate::wayland::foreign_toplevel::update(self, id);
861 }
862 }
863
864 fn maximize_request(&mut self, _xwm: XwmId, surface: X11Surface) {
865 if let Some((id, _)) = window_of(self, &surface) {
866 wm::request(self, id, Request::Maximize);
867 }
868 }
869
870 fn unmaximize_request(&mut self, _xwm: XwmId, surface: X11Surface) {
871 if let Some((id, _)) = window_of(self, &surface) {
872 wm::request(self, id, Request::Unmaximize);
873 }
874 }
875
876 fn fullscreen_request(&mut self, _xwm: XwmId, surface: X11Surface) {
877 if let Some((id, _)) = window_of(self, &surface) {
878 wm::request(self, id, Request::Fullscreen(None));
879 }
880 }
881
882 fn unfullscreen_request(&mut self, _xwm: XwmId, surface: X11Surface) {
883 if let Some((id, _)) = window_of(self, &surface) {
884 wm::request(self, id, Request::Unfullscreen);
885 }
886 }
887
888 fn minimize_request(&mut self, _xwm: XwmId, surface: X11Surface) {
889 if let Some((id, _)) = window_of(self, &surface) {
890 wm::request(self, id, Request::Minimize);
891 }
892 }
893
894 fn unminimize_request(&mut self, _xwm: XwmId, surface: X11Surface) {
895 if let Some((id, _)) = window_of(self, &surface) {
896 wm::request(self, id, Request::Unminimize);
897 }
898 }
899
900 fn above_request(&mut self, _xwm: XwmId, surface: X11Surface) {
901 if let Some((id, _)) = window_of(self, &surface) {
902 wm::request(self, id, Request::Above(true));
903 }
904 }
905
906 fn unabove_request(&mut self, _xwm: XwmId, surface: X11Surface) {
907 if let Some((id, _)) = window_of(self, &surface) {
908 wm::request(self, id, Request::Above(false));
909 }
910 }
911
912 fn below_request(&mut self, _xwm: XwmId, surface: X11Surface) {
913 if let Some((id, _)) = window_of(self, &surface) {
914 wm::request(self, id, Request::Below(true));
915 }
916 }
917
918 fn unbelow_request(&mut self, _xwm: XwmId, surface: X11Surface) {
919 if let Some((id, _)) = window_of(self, &surface) {
920 wm::request(self, id, Request::Below(false));
921 }
922 }
923
924 fn shade_request(&mut self, _xwm: XwmId, surface: X11Surface) {
925 if let Some((id, _)) = window_of(self, &surface) {
926 wm::request(self, id, Request::Shade(true));
927 }
928 }
929
930 fn unshade_request(&mut self, _xwm: XwmId, surface: X11Surface) {
931 if let Some((id, _)) = window_of(self, &surface) {
932 wm::request(self, id, Request::Shade(false));
933 }
934 }
935
936 fn demands_attention_request(&mut self, _xwm: XwmId, surface: X11Surface) {
937 if let Some((id, _)) = window_of(self, &surface) {
938 wm::request(self, id, Request::Attention(true));
939 }
940 }
941
942 fn undemands_attention_request(&mut self, _xwm: XwmId, surface: X11Surface) {
943 if let Some((id, _)) = window_of(self, &surface) {
944 wm::request(self, id, Request::Attention(false));
945 }
946 }
947
948 fn resize_request(
949 &mut self,
950 _xwm: XwmId,
951 surface: X11Surface,
952 _button: u32,
953 edges: X11ResizeEdge,
954 ) {
955 let Some((id, _)) = window_of(self, &surface) else {
956 return;
957 };
958 let edge = match edges {
959 X11ResizeEdge::Top => ResizeEdge::Top,
960 X11ResizeEdge::Bottom => ResizeEdge::Bottom,
961 X11ResizeEdge::Left => ResizeEdge::Left,
962 X11ResizeEdge::Right => ResizeEdge::Right,
963 X11ResizeEdge::TopLeft => ResizeEdge::TopLeft,
964 X11ResizeEdge::TopRight => ResizeEdge::TopRight,
965 X11ResizeEdge::BottomLeft => ResizeEdge::BottomLeft,
966 X11ResizeEdge::BottomRight => ResizeEdge::BottomRight,
967 };
968 wm::request(self, id, Request::Resize(edge));
969 }
970
971 fn move_request(&mut self, _xwm: XwmId, surface: X11Surface, _button: u32) {
972 if let Some((id, _)) = window_of(self, &surface) {
973 wm::request(self, id, Request::Move);
974 }
975 }
976
977 fn active_window_request(
978 &mut self,
979 _xwm: XwmId,
980 surface: X11Surface,
981 _timestamp: u32,
982 _current: Option<X11Surface>,
983 ) {
984 if let Some((id, _)) = window_of(self, &surface) {
985 wm::request(self, id, Request::Activate);
986 }
987 }
988
989 fn allow_selection_access(&mut self, xwm: XwmId, _selection: SelectionTarget) -> bool {
990 self.seat
991 .get_keyboard()
992 .and_then(|k| k.current_focus())
993 .is_some_and(|f| matches!(f, FocusTarget::Window(w) if w.x11_surface().and_then(|s| s.xwm_id()) == Some(xwm)))
994 }
995
996 fn send_selection(
997 &mut self,
998 _xwm: XwmId,
999 selection: SelectionTarget,
1000 mime_type: String,
1001 fd: OwnedFd,
1002 ) {
1003 match selection {
1004 SelectionTarget::Clipboard => {
1005 let _ = request_data_device_client_selection(&self.seat, mime_type, fd);
1006 }
1007 SelectionTarget::Primary => {
1008 let _ = request_primary_client_selection(&self.seat, mime_type, fd);
1009 }
1010 }
1011 }
1012
1013 fn new_selection(&mut self, _xwm: XwmId, selection: SelectionTarget, mime_types: Vec<String>) {
1014 let dh = self.dh.clone();
1015 match selection {
1016 SelectionTarget::Clipboard => {
1017 let flavors = crate::wayland::frontend::clipboard_flavors(&mime_types);
1018 set_data_device_selection(&dh, &self.seat, mime_types, Arc::new(Vec::new()));
1019 self.current_selection_mimes.clone_from(&flavors);
1020 if self.clipboard_callback.is_some() {
1021 self.pending_clipboard_read = Some(flavors);
1022 }
1023 }
1024 SelectionTarget::Primary => {
1025 set_primary_selection(&dh, &self.seat, mime_types, Arc::new(Vec::new()));
1026 }
1027 }
1028 }
1029
1030 fn cleared_selection(&mut self, _xwm: XwmId, selection: SelectionTarget) {
1031 let dh = self.dh.clone();
1032 match selection {
1033 SelectionTarget::Clipboard => {
1034 if current_data_device_selection_userdata(&self.seat).is_some() {
1035 clear_data_device_selection(&dh, &self.seat);
1036 }
1037 }
1038 SelectionTarget::Primary => {
1039 if current_primary_selection_userdata(&self.seat).is_some() {
1040 clear_primary_selection(&dh, &self.seat);
1041 }
1042 }
1043 }
1044 }
1045
1046 fn show_desktop_request(&mut self, _xwm: XwmId) {
1047 wm::show_desktop(self, true);
1048 }
1049
1050 fn unshow_desktop_request(&mut self, _xwm: XwmId) {
1051 wm::show_desktop(self, false);
1052 }
1053
1054 fn disconnected(&mut self, _xwm: XwmId) {
1055 eprintln!("[Wayland] Xwayland disconnected; starting it again in {RESTART_DELAY:?}.");
1056 let ids: Vec<WindowId> = self.xwm.windows.keys().copied().collect();
1057 for id in ids {
1058 if let Some(window) = self.xwm.windows.remove(&id) {
1059 self.space.unmap_elem(&window);
1060 }
1061 wm::window_removed(self, id);
1062 crate::wayland::foreign_toplevel::remove(self, id);
1063 }
1064 self.xwm.wm = None;
1065 self.xwm.display = None;
1066 self.xwm.extras = None;
1067 self.xwm.restart_at = Some(Instant::now() + RESTART_DELAY);
1068 }
1069}
1070
1071#[cfg(test)]
1072mod tests {
1073 use super::*;
1074
1075 #[test]
1076 fn a_named_xwayland_is_found_first_and_a_missing_one_is_not_named() {
1077 let dir = std::env::temp_dir().join(format!("pixelflux-xwayland-{}", std::process::id()));
1078 std::fs::create_dir_all(&dir).unwrap();
1079 let bin = dir.join("Xwayland");
1080 std::fs::write(&bin, "").unwrap();
1081 assert_eq!(
1082 xwayland_path(Some(bin.clone().into()), Some("/usr/bin:/bin".into())),
1083 Some(format!("{}:/usr/bin:/bin", dir.display()))
1084 );
1085 assert_eq!(
1086 xwayland_path(Some(bin.into()), None),
1087 Some(dir.display().to_string())
1088 );
1089 assert_eq!(
1090 xwayland_path(Some(dir.join("missing").into()), Some("/usr/bin".into())),
1091 None
1092 );
1093 assert_eq!(xwayland_path(None, Some("/usr/bin".into())), None);
1094 std::fs::remove_dir_all(&dir).unwrap();
1095 }
1096}