Commit cce3ef7e0162
tarball15 files changed+622-50
@@ -32,6 +32,7 @@ smithay = { workspace = true, features = [
3232
"backend_libinput",3333
"backend_udev",3434
"backend_session_libseat",35
+ "xwayland",3536
] }3637
smithay-drm-extras = { git = "https://github.com/Smithay/smithay", rev = "023b78b1c5a05ca512767cacc07efca55fae8be4", default-features = false }3738
xcursor = "0.3"@@ -5,6 +5,7 @@
55
use crate::state::State;66
use monolith_layout::{OutputId, TagId};77
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;8
+use smithay::wayland::seat::WaylandFocus;89
use smithay::wayland::xdg_activation::{910
XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData,1011
};@@ -27,7 +28,10 @@ impl XdgActivationHandler for State {
2728
2829
fn token_created(&mut self, _token: XdgActivationToken, data: XdgActivationTokenData) -> bool {2930
let keyboard = self.seat.get_keyboard();30
- let focused = keyboard.as_ref().and_then(|k| k.current_focus());31
+ let focused = keyboard32
+ .as_ref()33
+ .and_then(|k| k.current_focus())34
+ .and_then(|f| f.wl_surface().map(|s| s.into_owned()));3135
let may_focus = data.surface.is_some() && data.surface == focused;3236
let (output, tag) = self.launch_context();3337
data.user_data.insert_if_missing(|| Context {@@ -220,6 +220,7 @@ impl State {
220220
&self.config.decoration,221221
&mut self.borders,222222
Some(output),223
+ self.xwayland.as_ref().map_or(&[], |x| &x.unmanaged),223224
)),224225
}225226
elements@@ -62,11 +62,14 @@ impl State {
6262
match cmd {6363
Command::Spawn { argv } => self.spawn(&argv)?,6464
Command::Close => {65
- if let Some(t) = focused66
- .and_then(|w| self.windows.get(&w))67
- .and_then(|w| w.toplevel())68
- {69
- t.send_close();65
+ if let Some(w) = focused.and_then(|w| self.windows.get(&w)) {66
+ match (w.toplevel(), w.x11_surface()) {67
+ (Some(t), _) => t.send_close(),68
+ (_, Some(x)) => {69
+ let _ = x.close();70
+ }71
+ _ => {}72
+ }7073
}7174
}7275
Command::Quit => self.signal.stop(),@@ -177,6 +180,9 @@ impl State {
177180
cmd.env("XDG_CURRENT_DESKTOP", "monolith")178181
.env("XDG_SESSION_TYPE", "wayland");179182
}183
+ if let Some(x) = &self.xwayland {184
+ cmd.env("DISPLAY", format!(":{}", x.display));185
+ }180186
let token = self.launch_token();181187
cmd.env("XDG_ACTIVATION_TOKEN", &token)182188
.env("DESKTOP_STARTUP_ID", &token);@@ -592,6 +592,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
592592
pointer_pos,593593
lock,594594
captures,595
+ xwayland,595596
..596597
} = state597598
else {@@ -652,6 +653,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
652653
&config.decoration,653654
borders,654655
Some(&surface.output),656
+ xwayland.as_ref().map_or(&[], |x| &x.unmanaged),655657
)),656658
}657659
let scene_ns = scene_began.elapsed().as_nanos() as u64;@@ -0,0 +1,92 @@
1
+//! Keyboard focus targets. X11 windows need their own target, since2
+//! Xwayland only routes keys to the window the WM gave X input focus.3
+4
+use crate::state::State;5
+use smithay::backend::input::KeyState;6
+use smithay::input::Seat;7
+use smithay::input::keyboard::{KeyboardTarget, KeysymHandle, ModifiersState};8
+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;9
+use smithay::utils::{IsAlive, Serial};10
+use smithay::wayland::seat::WaylandFocus;11
+use smithay::xwayland::X11Surface;12
+use std::borrow::Cow;13
+14
+#[derive(Debug, Clone, PartialEq)]15
+pub enum Focus {16
+ Surface(WlSurface),17
+ X11(Box<X11Surface>),18
+}19
+20
+impl From<WlSurface> for Focus {21
+ fn from(s: WlSurface) -> Self {22
+ Focus::Surface(s)23
+ }24
+}25
+26
+impl IsAlive for Focus {27
+ fn alive(&self) -> bool {28
+ match self {29
+ Focus::Surface(s) => s.alive(),30
+ Focus::X11(x) => x.alive(),31
+ }32
+ }33
+}34
+35
+impl WaylandFocus for Focus {36
+ fn wl_surface(&self) -> Option<Cow<'_, WlSurface>> {37
+ match self {38
+ Focus::Surface(s) => Some(Cow::Borrowed(s)),39
+ Focus::X11(x) => x.wl_surface().map(Cow::Owned),40
+ }41
+ }42
+}43
+44
+impl KeyboardTarget<State> for Focus {45
+ fn enter(46
+ &self,47
+ seat: &Seat<State>,48
+ data: &mut State,49
+ keys: Vec<KeysymHandle<'_>>,50
+ serial: Serial,51
+ ) {52
+ match self {53
+ Focus::Surface(s) => KeyboardTarget::enter(s, seat, data, keys, serial),54
+ Focus::X11(x) => KeyboardTarget::enter(x.as_ref(), seat, data, keys, serial),55
+ }56
+ }57
+58
+ fn leave(&self, seat: &Seat<State>, data: &mut State, serial: Serial) {59
+ match self {60
+ Focus::Surface(s) => KeyboardTarget::leave(s, seat, data, serial),61
+ Focus::X11(x) => KeyboardTarget::leave(x.as_ref(), seat, data, serial),62
+ }63
+ }64
+65
+ fn key(66
+ &self,67
+ seat: &Seat<State>,68
+ data: &mut State,69
+ key: KeysymHandle<'_>,70
+ state: KeyState,71
+ serial: Serial,72
+ time: smithay::backend::input::InputTime,73
+ ) {74
+ match self {75
+ Focus::Surface(s) => KeyboardTarget::key(s, seat, data, key, state, serial, time),76
+ Focus::X11(x) => KeyboardTarget::key(x.as_ref(), seat, data, key, state, serial, time),77
+ }78
+ }79
+80
+ fn modifiers(81
+ &self,82
+ seat: &Seat<State>,83
+ data: &mut State,84
+ modifiers: ModifiersState,85
+ serial: Serial,86
+ ) {87
+ match self {88
+ Focus::Surface(s) => KeyboardTarget::modifiers(s, seat, data, modifiers, serial),89
+ Focus::X11(x) => KeyboardTarget::modifiers(x.as_ref(), seat, data, modifiers, serial),90
+ }91
+ }92
+}@@ -25,6 +25,7 @@ use smithay::wayland::dmabuf::{
2525
DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier, get_dmabuf,2626
};2727
use smithay::wayland::output::OutputHandler;28
+use smithay::wayland::seat::WaylandFocus;2829
use smithay::wayland::selection::SelectionHandler;2930
use smithay::wayland::selection::primary_selection::{3031
PrimarySelectionHandler, PrimarySelectionState, set_primary_focus,@@ -51,6 +52,9 @@ impl CompositorHandler for State {
5152
}5253
5354
fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {55
+ if let Some(x) = client.get_data::<smithay::xwayland::XWaylandClientData>() {56
+ return &x.compositor_state;57
+ }5458
&client5559
.get_data::<ClientState>()5660
.expect("every client has ClientState")@@ -73,7 +77,7 @@ impl CompositorHandler for State {
7377
.windows7478
.values()7579
.chain(&self.unmapped)76
- .find(|w| w.toplevel().is_some_and(|t| t.wl_surface() == &root))80
+ .find(|w| crate::state::root(w).as_ref() == Some(&root))7781
{7882
window.on_commit();7983
}@@ -85,7 +89,7 @@ impl CompositorHandler for State {
8589
{8690
let window = self.unmapped.remove(i);8791
let toplevel = window.toplevel().cloned();88
- self.map_window(window);92
+ self.map_window(window, None);8993
if let Some(t) = toplevel {9094
t.send_configure();9195
}@@ -174,7 +178,7 @@ impl XdgShellHandler for State {
174178
}175179
176180
impl SeatHandler for State {177
- type KeyboardFocus = WlSurface;181
+ type KeyboardFocus = crate::focus::Focus;178182
type PointerFocus = WlSurface;179183
type TouchFocus = WlSurface;180184
@@ -187,8 +191,10 @@ impl SeatHandler for State {
187191
self.needs_redraw = true;188192
}189193
190
- fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&WlSurface>) {191
- let client = focused.and_then(|s| self.display.get_client(s.id()).ok());194
+ fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&crate::focus::Focus>) {195
+ let client = focused196
+ .and_then(|f| f.wl_surface())197
+ .and_then(|s| self.display.get_client(s.id()).ok());192198
set_data_device_focus(&self.display, seat, client.clone());193199
set_primary_focus(&self.display, seat, client);194200
}@@ -43,7 +43,7 @@ impl State {
4343
}4444
self.space4545
.elements()46
- .any(|w| w.toplevel().is_some_and(|t| t.wl_surface() == &root))46
+ .any(|w| crate::state::root(w).as_ref() == Some(&root))4747
|| self.layer_maps_contain(&root)4848
});4949
if shown != self.inhibited {@@ -153,8 +153,8 @@ fn focus_under_pointer(state: &mut State) {
153153
let id = state154154
.space155155
.element_under(pos)156
- .and_then(|(w, _)| w.toplevel())157
- .and_then(|t| state.window_id(t.wl_surface()));156
+ .and_then(|(w, _)| crate::state::root(w))157
+ .and_then(|s| state.window_id(&s));158158
if let Some(id) = id159159
&& state.layout.focused() != Some(id)160160
{@@ -172,12 +172,7 @@ fn snapshot(state: &State) -> monolith_ipc::State {
172172
let mut windows: Vec<monolith_ipc::Window> = l173173
.windows()174174
.map(|(id, w)| {175
- let (app_id, title) = state176
- .windows177
- .get(&id)178
- .and_then(|w| w.toplevel())179
- .map(identity)180
- .unwrap_or_default();175
+ let (app_id, title) = state.windows.get(&id).map(identity).unwrap_or_default();181176
monolith_ipc::Window {182177
id: slotmap::Key::data(&id).as_ffi(),183178
app_id,@@ -4,6 +4,7 @@ mod capture;
44
mod commands;55
mod cursor;66
mod drm;7
+mod focus;78
mod handlers;89
mod idle;910
mod input;@@ -14,6 +15,7 @@ mod lock;
1415
mod nested;1516
mod scene;1617
mod state;18
+mod xwayland;1719
1820
#[cfg(feature = "spans")]1921
mod slow_spans;@@ -140,6 +142,12 @@ fn export_session(state: &State) {
140142
))141143
.arg("XDG_CURRENT_DESKTOP=monolith")142144
.arg("XDG_SESSION_TYPE=wayland")145
+ .args(146
+ state147
+ .xwayland148
+ .as_ref()149
+ .map(|x| format!("DISPLAY=:{}", x.display)),150
+ )143151
.stdin(std::process::Stdio::null())144152
.spawn();145153
match result {@@ -172,6 +180,7 @@ fn run(
172180
None => nested::init(&mut event_loop, &mut state)?,173181
}174182
ipc::init(&event_loop, &mut state)?;183
+ xwayland::init(&mut state);175184
tracing::info!(socket = ?state.socket_name, device = state.renderer.device_name(), "monolith running");176185
177186
if gpu.is_some() {@@ -181,6 +181,7 @@ pub fn render(state: &mut State) {
181181
output_id,182182
lock,183183
captures,184
+ xwayland,184185
..185186
} = state;186187
let Some(host) = host.as_mut() else { return };@@ -236,6 +237,7 @@ pub fn render(state: &mut State) {
236237
&config.decoration,237238
borders,238239
Some(&host.output),240
+ xwayland.as_ref().map_or(&[], |x| &x.unmanaged),239241
)240242
});241243
match host@@ -8,6 +8,7 @@ use monolith_render_vk::{Effect, VulkanFrame, VulkanRenderer, VulkanTexture};
88
use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;99
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};1010
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;11
+use smithay::backend::renderer::element::surface::render_elements_from_surface_tree;1112
use smithay::backend::renderer::element::texture::TextureRenderElement;1213
use smithay::backend::renderer::element::utils::{CropRenderElement, RescaleRenderElement};1314
use smithay::backend::renderer::element::{AsRenderElements, Element, Id, Kind, RenderElement};@@ -19,6 +20,7 @@ use smithay::output::Output;
1920
use smithay::utils::user_data::UserDataMap;2021
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};2122
use smithay::wayland::shell::wlr_layer::Layer;23
+use smithay::xwayland::X11Surface;2224
use std::collections::HashMap;2325
2426
smithay::backend::renderer::element::render_elements! {@@ -50,6 +52,7 @@ pub fn build(
5052
deco: &Decoration,5153
borders: &mut Borders,5254
output: Option<&Output>,55
+ unmanaged: &[X11Surface],5356
) -> Vec<Draw> {5457
let Some(area) = layout.output(o).map(|out| out.area) else {5558
return Vec::new();@@ -90,6 +93,18 @@ pub fn build(
9093
borders.0.retain(|id, _| windows.contains_key(id));9194
let focused = layout.focused();9295
let mut out = Vec::new();96
+ for x in unmanaged.iter().rev() {97
+ let Some(surface) = x.wl_surface() else {98
+ continue;99
+ };100
+ let loc = x.geometry().loc;101
+ let at = Point::<i32, Physical>::from((loc.x - area.x as i32, loc.y - area.y as i32));102
+ out.extend(103
+ render_elements_from_surface_tree(renderer, &surface, at, 1.0, 1.0, Kind::Unspecified)104
+ .into_iter()105
+ .map(Draw::Surface),106
+ );107
+ }93108
if let Some(output) = output {94109
crate::layers::elements(&mut out, renderer, output, Layer::Overlay);95110
if !fullscreen_at_rest {@@ -185,7 +200,7 @@ fn window_elements(
185200
.filter_map(|e| CropRenderElement::from_element(e, 1.0, crop))186201
.map(Draw::Cropped),187202
);188
- let Some(root) = window.toplevel().map(|t| t.wl_surface().clone()) else {203
+ let Some(root) = crate::state::root(window) else {189204
return;190205
};191206
let context = renderer.context_id();@@ -302,7 +317,7 @@ fn effect_elements(
302317
(at, size): ((i32, i32), (i32, i32)),303318
effect: Effect,304319
) {305
- let Some(root) = window.toplevel().map(|t| t.wl_surface().clone()) else {320
+ let Some(root) = crate::state::root(window) else {306321
return;307322
};308323
let root_id = Id::from_wayland_resource(&root);@@ -1,5 +1,6 @@
11
use crate::anim::{Animator, Ghost};22
use crate::commands::Binds;3
+use crate::focus::Focus;34
use crate::nested::Host;45
use monolith_config::Config;56
use monolith_layout::{Layout, OutputId, Rect, WindowId};@@ -9,6 +10,7 @@ use monolith_render_vk::VulkanTexture;
910
use smithay::backend::renderer::ImportDma;1011
use smithay::backend::renderer::Renderer;1112
use smithay::backend::renderer::utils::{import_surface_tree, with_renderer_surface_state};13
+use smithay::desktop::utils::under_from_surface_tree;1214
use smithay::desktop::{PopupManager, Space, Window, WindowSurfaceType};1315
use smithay::input::keyboard::{Keycode, XkbConfig};1416
use smithay::input::pointer::CursorImageStatus;@@ -21,7 +23,7 @@ use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::Sta
2123
use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};2224
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;2325
use smithay::reexports::wayland_server::{Display, DisplayHandle, Resource};24
-use smithay::utils::{Clock, Logical, Monotonic, Point, SERIAL_COUNTER, Size};26
+use smithay::utils::{Clock, Logical, Monotonic, Point, Rectangle, SERIAL_COUNTER, Size};2527
use smithay::wayland::compositor::{CompositorClientState, CompositorState, with_states};2628
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufState};2729
use smithay::wayland::idle_inhibit::IdleInhibitManagerState;@@ -29,6 +31,7 @@ use smithay::wayland::idle_notify::IdleNotifierState;
2931
use smithay::wayland::image_capture_source::{ImageCaptureSourceState, OutputCaptureSourceState};3032
use smithay::wayland::image_copy_capture::ImageCopyCaptureState;3133
use smithay::wayland::output::OutputManagerState;34
+use smithay::wayland::seat::WaylandFocus;3235
use smithay::wayland::selection::data_device::DataDeviceState;3336
use smithay::wayland::selection::ext_data_control::DataControlState as ExtDataControlState;3437
use smithay::wayland::selection::primary_selection::PrimarySelectionState;@@ -40,6 +43,7 @@ use smithay::wayland::shell::xdg::{XdgShellState, XdgToplevelSurfaceData};
4043
use smithay::wayland::shm::ShmState;4144
use smithay::wayland::socket::ListeningSocketSource;4245
use smithay::wayland::xdg_activation::XdgActivationState;46
+use smithay::wayland::xwayland_shell::XWaylandShellState;4347
use std::collections::{HashMap, HashSet};4448
use std::ffi::OsString;4549
use std::path::PathBuf;@@ -90,6 +94,8 @@ pub struct State {
9094
pub placed_by_rule: HashSet<WindowId>,9195
/// A layer surface given the keyboard by a click, like a launcher.9296
pub layer_focus: Option<WlSurface>,97
+ pub xwayland: Option<crate::xwayland::Xwayland>,98
+ pub xwayland_shell_state: XWaylandShellState,9399
pub captures: crate::capture::Captures,94100
/// The render node clients get dmabufs for, known on bare metal.95101
pub render_node: Option<smithay::backend::drm::DrmNode>,@@ -205,6 +211,8 @@ impl State {
205211
awaiting: HashSet::new(),206212
placed_by_rule: HashSet::new(),207213
layer_focus: None,214
+ xwayland: None,215
+ xwayland_shell_state: XWaylandShellState::new::<State>(&dh),208216
captures: Default::default(),209217
render_node: None,210218
lock: Default::default(),@@ -277,14 +285,18 @@ impl State {
277285
278286
/// Adds a toplevel to the layout on its first commit, where its app_id279287
/// and title are known and the initial configure can carry its size.280
- pub fn map_window(&mut self, window: Window) {281
- let (app_id, title) = window.toplevel().map(identity).unwrap_or_default();288
+ pub fn map_window(&mut self, window: Window, float: Option<Rectangle<i32, Logical>>) {289
+ let (app_id, title) = identity(&window);282290
let rule = self.config.placement(&app_id, &title);283
- let launch = window284
- .toplevel()285
- .and_then(|t| t.wl_surface().client())286
- .and_then(|c| c.get_credentials(&self.display).ok())287
- .and_then(|c| self.launches.find(c.pid as u32));291
+ let pid = match window.x11_surface() {292
+ Some(x) => x.pid(),293
+ None => window294
+ .toplevel()295
+ .and_then(|t| t.wl_surface().client())296
+ .and_then(|c| c.get_credentials(&self.display).ok())297
+ .map(|c| c.pid as u32),298
+ };299
+ let launch = pid.and_then(|pid| self.launches.find(pid));288300
let output = rule289301
.output290302
.as_deref()@@ -299,7 +311,15 @@ impl State {
299311
Some(t) => Some(self.layout.tag(t)),300312
None => launch.map(|l| l.1),301313
};302
- let id = self.layout.add_window(output, tag, None);314
+ let float = float.map(|r| {315
+ Rect::new(316
+ f64::from(r.loc.x),317
+ f64::from(r.loc.y),318
+ f64::from(r.size.w),319
+ f64::from(r.size.h),320
+ )321
+ });322
+ let id = self.layout.add_window(output, tag, float);303323
if rule.tag.is_some() || rule.output.is_some() {304324
self.placed_by_rule.insert(id);305325
}@@ -353,19 +373,22 @@ impl State {
353373
}354374
355375
pub fn unmap_window(&mut self, surface: &WlSurface) {356
- self.unmapped357
- .retain(|w| w.toplevel().is_some_and(|t| t.wl_surface() != surface));376
+ self.unmapped.retain(|w| root(w).as_ref() != Some(surface));358377
if let Some(id) = self.window_id(surface) {359
- if let Some(window) = self.windows.remove(&id) {360
- self.space.unmap_elem(&window);361
- self.leave_ghost(id, &window);362
- }363
- self.layout.remove_window(id);364
- self.placed_by_rule.remove(&id);365
- self.apply_layout();378
+ self.remove_window(id);366379
}367380
}368381
382
+ pub fn remove_window(&mut self, id: WindowId) {383
+ if let Some(window) = self.windows.remove(&id) {384
+ self.space.unmap_elem(&window);385
+ self.leave_ghost(id, &window);386
+ }387
+ self.layout.remove_window(id);388
+ self.placed_by_rule.remove(&id);389
+ self.apply_layout();390
+ }391
+369392
/// Keeps a visible tiled window's last frame shrinking to its seed while370393
/// its neighbours close over it.371394
fn leave_ghost(&mut self, id: WindowId, window: &Window) {@@ -388,7 +411,7 @@ impl State {
388411
else {389412
return;390413
};391
- let Some(surface) = window.toplevel().map(|t| t.wl_surface().clone()) else {414
+ let Some(surface) = root(window) else {392415
return;393416
};394417
let _ = import_surface_tree(&mut self.renderer, &surface);@@ -416,7 +439,7 @@ impl State {
416439
pub fn window_id(&self, surface: &WlSurface) -> Option<WindowId> {417440
self.windows418441
.iter()419
- .find(|(_, w)| w.toplevel().is_some_and(|t| t.wl_surface() == surface))442
+ .find(|(_, w)| root(w).as_ref() == Some(surface))420443
.map(|(id, _)| *id)421444
}422445
@@ -541,14 +564,15 @@ impl State {
541564
t.send_pending_configure();542565
}543566
}544
- let target = if self.lock.active() {545
- self.lock_keyboard_focus()567
+ let target: Option<Focus> = if self.lock.active() {568
+ self.lock_keyboard_focus().map(Focus::from)546569
} else {547
- self.layer_keyboard_focus().or_else(|| {548
- focused549
- .and_then(|id| self.windows.get(&id))550
- .and_then(|w| w.toplevel())551
- .map(|t| t.wl_surface().clone())570
+ self.layer_keyboard_focus().map(Focus::from).or_else(|| {571
+ let w = focused.and_then(|id| self.windows.get(&id))?;572
+ match w.x11_surface() {573
+ Some(x) => Some(Focus::X11(Box::new(x.clone()))),574
+ None => w.toplevel().map(|t| Focus::from(t.wl_surface().clone())),575
+ }552576
})553577
};554578
if let Some(keyboard) = self.seat.get_keyboard()@@ -611,6 +635,21 @@ impl State {
611635
if self.lock.active() {612636
return self.lock_surface_under(pos);613637
}638
+ let unmanaged = self.xwayland.iter().flat_map(|x| x.unmanaged.iter().rev());639
+ for x in unmanaged {640
+ let geo = x.geometry();641
+ if let Some(s) = x.wl_surface()642
+ && geo.to_f64().contains(pos)643
+ && let Some((s, p)) = under_from_surface_tree(644
+ &s,645
+ pos - geo.loc.to_f64(),646
+ (0, 0),647
+ WindowSurfaceType::ALL,648
+ )649
+ {650
+ return Some((s, (p + geo.loc).to_f64()));651
+ }652
+ }614653
let fullscreen = self.layout.outputs().any(|(o, out)| {615654
let a = out.area;616655
(a.x..a.right()).contains(&pos.x)@@ -653,6 +692,12 @@ fn configure(window: &Window, rect: Rect, kind: Kind, maximized: bool) -> bool {
653692
if w < 1 || h < 1 {654693
return false;655694
}695
+ let (x0, y0) = (x as i32, y as i32);696
+ if let Some(x) = window.x11_surface() {697
+ let _ = x.configure(Rectangle::new((x0, y0).into(), (w, h).into()));698
+ let _ = x.set_fullscreen(kind == Kind::Fullscreen);699
+ let _ = x.set_maximized(maximized && kind == Kind::Tiled);700
+ }656701
if let Some(t) = window.toplevel() {657702
t.with_pending_state(|s| {658703
s.size = Some((w, h).into());@@ -691,7 +736,19 @@ pub fn now_ns() -> u64 {
691736
Duration::from(Clock::<Monotonic>::new().now()).as_nanos() as u64692737
}693738
694
-pub fn identity(t: &smithay::wayland::shell::xdg::ToplevelSurface) -> (String, String) {739
+/// A window's surface, for xdg toplevels and X11 windows alike.740
+pub fn root(window: &Window) -> Option<WlSurface> {741
+ window.wl_surface().map(|s| s.into_owned())742
+}743
+744
+/// A window's app id and title. X11 windows give their class.745
+pub fn identity(window: &Window) -> (String, String) {746
+ if let Some(x) = window.x11_surface() {747
+ return (x.class(), x.title());748
+ }749
+ let Some(t) = window.toplevel() else {750
+ return Default::default();751
+ };695752
with_states(t.wl_surface(), |s| {696753
let data = s697754
.data_map@@ -0,0 +1,382 @@
1
+//! Lazy Xwayland. monolith owns an X display socket from startup and only2
+//! starts Xwayland when a client connects, handing it the socket so the3
+//! connection that woke it is served. Xwayland exits once its last client4
+//! leaves and the socket is watched again.5
+6
+use crate::state::State;7
+use smithay::desktop::Window;8
+use smithay::reexports::calloop::generic::Generic;9
+use smithay::reexports::calloop::{Interest, Mode, PostAction, RegistrationToken};10
+use smithay::reexports::rustix::io::{FdFlags, fcntl_getfd, fcntl_setfd};11
+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;12
+use smithay::utils::{Logical, Rectangle};13
+use smithay::wayland::xwayland_shell::{XWaylandShellHandler, XWaylandShellState};14
+use smithay::xwayland::xwm::{Reorder, ResizeEdge, WmWindowType, XwmId};15
+use smithay::xwayland::{X11Surface, X11Wm, XWayland, XWaylandEvent, XwmHandler};16
+use std::os::fd::AsFd;17
+use std::os::linux::net::SocketAddrExt;18
+use std::os::unix::net::{SocketAddr, UnixListener};19
+use std::process::Stdio;20
+21
+pub struct Xwayland {22
+ pub display: u32,23
+ listeners: Vec<UnixListener>,24
+ watch: Vec<(usize, RegistrationToken)>,25
+ running: bool,26
+ pub wm: Option<X11Wm>,27
+ /// Override-redirect windows such as menus and tooltips, bottom to top.28
+ pub unmanaged: Vec<X11Surface>,29
+}30
+31
+impl Drop for Xwayland {32
+ fn drop(&mut self) {33
+ let _ = std::fs::remove_file(socket_path(self.display));34
+ let _ = std::fs::remove_file(lock_path(self.display));35
+ }36
+}37
+38
+fn socket_path(n: u32) -> String {39
+ format!("/tmp/.X11-unix/X{n}")40
+}41
+42
+fn lock_path(n: u32) -> String {43
+ format!("/tmp/.X{n}-lock")44
+}45
+46
+/// Claims the first free X display, taking over stale locks left by dead47
+/// servers.48
+fn claim() -> Option<(u32, Vec<UnixListener>)> {49
+ let _ = std::fs::create_dir_all("/tmp/.X11-unix");50
+ for n in 0..64 {51
+ let lock = lock_path(n);52
+ let created = std::fs::OpenOptions::new()53
+ .write(true)54
+ .create_new(true)55
+ .open(&lock);56
+ let file = match created {57
+ Ok(f) => f,58
+ Err(_) => {59
+ let owner = std::fs::read_to_string(&lock)60
+ .ok()61
+ .and_then(|s| s.trim().parse::<u32>().ok());62
+ let stale = owner63
+ .is_some_and(|pid| !std::path::Path::new(&format!("/proc/{pid}")).exists());64
+ if !stale || std::fs::remove_file(&lock).is_err() {65
+ continue;66
+ }67
+ match std::fs::OpenOptions::new()68
+ .write(true)69
+ .create_new(true)70
+ .open(&lock)71
+ {72
+ Ok(f) => f,73
+ Err(_) => continue,74
+ }75
+ }76
+ };77
+ use std::io::Write;78
+ let _ = writeln!(&file, "{:>10}", std::process::id());79
+ let path = socket_path(n);80
+ let _ = std::fs::remove_file(&path);81
+ let Ok(fs) = UnixListener::bind(&path) else {82
+ let _ = std::fs::remove_file(&lock);83
+ continue;84
+ };85
+ let mut listeners = vec![fs];86
+ if let Ok(addr) = SocketAddr::from_abstract_name(path.as_bytes())87
+ && let Ok(abs) = UnixListener::bind_addr(&addr)88
+ {89
+ listeners.push(abs);90
+ }91
+ return Some((n, listeners));92
+ }93
+ None94
+}95
+96
+pub fn init(state: &mut State) {97
+ let Some((number, listeners)) = claim() else {98
+ tracing::warn!("no free X display, X11 clients will not run");99
+ return;100
+ };101
+ let mut watch = Vec::new();102
+ for (i, l) in listeners.iter().enumerate() {103
+ let Ok(l) = l.try_clone() else { continue };104
+ let token = state.handle.insert_source(105
+ Generic::new(l, Interest::READ, Mode::Level),106
+ move |_, _, state| {107
+ start(state, i);108
+ Ok(PostAction::Disable)109
+ },110
+ );111
+ if let Ok(t) = token {112
+ watch.push((i, t));113
+ }114
+ }115
+ tracing::info!("X11 clients will start Xwayland on :{number}");116
+ state.xwayland = Some(Xwayland {117
+ display: number,118
+ listeners,119
+ watch,120
+ running: false,121
+ wm: None,122
+ unmanaged: Vec::new(),123
+ });124
+}125
+126
+/// Waits for the next X client to knock.127
+fn rearm(state: &mut State) {128
+ let Some(x) = state.xwayland.as_mut() else {129
+ return;130
+ };131
+ x.running = false;132
+ for (_, t) in &x.watch {133
+ let _ = state.handle.enable(t);134
+ }135
+}136
+137
+fn set_inherit(listeners: &[UnixListener], inherit: bool) {138
+ for l in listeners {139
+ let flags = fcntl_getfd(l.as_fd()).unwrap_or(FdFlags::CLOEXEC);140
+ let flags = if inherit {141
+ flags - FdFlags::CLOEXEC142
+ } else {143
+ flags | FdFlags::CLOEXEC144
+ };145
+ let _ = fcntl_setfd(l.as_fd(), flags);146
+ }147
+}148
+149
+fn start(state: &mut State, fired: usize) {150
+ let Some(x) = state.xwayland.as_mut() else {151
+ return;152
+ };153
+ if x.running {154
+ return;155
+ }156
+ x.running = true;157
+ for (i, t) in &x.watch {158
+ if *i != fired {159
+ state.handle.disable(t).ok();160
+ }161
+ }162
+ let mut args = Vec::new();163
+ for l in &x.listeners {164
+ args.push("-listenfd".to_string());165
+ args.push(std::os::fd::AsRawFd::as_raw_fd(l).to_string());166
+ }167
+ // smithay only passes on the sockets it creates itself, so ours are168
+ // made inheritable just for this one spawn.169
+ set_inherit(&x.listeners, true);170
+ let spawned = XWayland::spawn(171
+ &state.display,172
+ None,173
+ std::iter::empty::<(String, String)>(),174
+ args,175
+ false,176
+ Stdio::null(),177
+ Stdio::null(),178
+ |_| {},179
+ );180
+ set_inherit(&x.listeners, false);181
+ let (xwayland, client) = match spawned {182
+ Ok(s) => s,183
+ Err(e) => {184
+ tracing::error!("cannot start Xwayland, X11 clients will not run: {e}");185
+ return;186
+ }187
+ };188
+ tracing::info!("starting Xwayland");189
+ let inserted = state190
+ .handle191
+ .insert_source(xwayland, move |event, _, state| match event {192
+ XWaylandEvent::Ready { x11_socket, .. } => {193
+ let dh = state.display.clone();194
+ match X11Wm::start_wm(state.handle.clone(), &dh, x11_socket, client.clone()) {195
+ Ok(wm) => {196
+ tracing::info!("Xwayland ready");197
+ if let Some(x) = state.xwayland.as_mut() {198
+ x.wm = Some(wm);199
+ }200
+ }201
+ Err(e) => tracing::error!("cannot manage Xwayland windows: {e}"),202
+ }203
+ }204
+ XWaylandEvent::Error => {205
+ tracing::warn!("Xwayland failed to start");206
+ rearm(state);207
+ }208
+ });209
+ if let Err(e) = inserted {210
+ tracing::error!("cannot watch Xwayland: {e}");211
+ }212
+}213
+214
+impl XWaylandShellHandler for State {215
+ fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState {216
+ &mut self.xwayland_shell_state217
+ }218
+219
+ fn surface_associated(&mut self, _xwm: XwmId, surface: WlSurface, _window: X11Surface) {220
+ self.on_buffer(&surface);221
+ self.needs_redraw = true;222
+ }223
+}224
+225
+impl XwmHandler for State {226
+ fn xwm_state(&mut self, _xwm: XwmId) -> &mut X11Wm {227
+ self.xwayland228
+ .as_mut()229
+ .and_then(|x| x.wm.as_mut())230
+ .expect("X11 events only arrive while the WM runs")231
+ }232
+233
+ fn new_window(&mut self, _xwm: XwmId, _window: X11Surface) {}234
+235
+ fn new_override_redirect_window(&mut self, _xwm: XwmId, _window: X11Surface) {}236
+237
+ fn map_window_request(&mut self, _xwm: XwmId, window: X11Surface) {238
+ if let Err(e) = window.set_mapped(true) {239
+ tracing::warn!("cannot map X11 window: {e}");240
+ return;241
+ }242
+ let float = floats(&window).then(|| window.geometry());243
+ self.map_window(Window::new_x11_window(window), float);244
+ }245
+246
+ fn mapped_override_redirect_window(&mut self, _xwm: XwmId, window: X11Surface) {247
+ if let Some(x) = self.xwayland.as_mut() {248
+ x.unmanaged.retain(|w| w != &window);249
+ x.unmanaged.push(window);250
+ }251
+ self.needs_redraw = true;252
+ }253
+254
+ fn unmapped_window(&mut self, _xwm: XwmId, window: X11Surface) {255
+ self.forget_x11(&window);256
+ if !window.is_override_redirect() {257
+ let _ = window.set_mapped(false);258
+ }259
+ }260
+261
+ fn destroyed_window(&mut self, _xwm: XwmId, window: X11Surface) {262
+ self.forget_x11(&window);263
+ }264
+265
+ fn configure_request(266
+ &mut self,267
+ _xwm: XwmId,268
+ window: X11Surface,269
+ x: Option<i32>,270
+ y: Option<i32>,271
+ w: Option<u32>,272
+ h: Option<u32>,273
+ _reorder: Option<Reorder>,274
+ ) {275
+ if self.x11_window_id(&window).is_some() {276
+ let _ = window.configure(None);277
+ return;278
+ }279
+ let mut geo = window.geometry();280
+ geo.loc.x = x.unwrap_or(geo.loc.x);281
+ geo.loc.y = y.unwrap_or(geo.loc.y);282
+ geo.size.w = w.map_or(geo.size.w, |w| w as i32);283
+ geo.size.h = h.map_or(geo.size.h, |h| h as i32);284
+ let _ = window.configure(geo);285
+ }286
+287
+ fn configure_notify(288
+ &mut self,289
+ _xwm: XwmId,290
+ window: X11Surface,291
+ _geometry: Rectangle<i32, Logical>,292
+ _above: Option<smithay::xwayland::xwm::X11Window>,293
+ ) {294
+ if window.is_override_redirect() {295
+ self.needs_redraw = true;296
+ }297
+ }298
+299
+ fn resize_request(300
+ &mut self,301
+ _xwm: XwmId,302
+ _window: X11Surface,303
+ _button: u32,304
+ _edge: ResizeEdge,305
+ ) {306
+ }307
+308
+ fn move_request(&mut self, _xwm: XwmId, _window: X11Surface, _button: u32) {}309
+310
+ fn fullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) {311
+ self.x11_fullscreen(&window, true);312
+ }313
+314
+ fn unfullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) {315
+ self.x11_fullscreen(&window, false);316
+ }317
+318
+ fn disconnected(&mut self, _xwm: XwmId) {319
+ tracing::info!("Xwayland exited");320
+ let gone: Vec<_> = self321
+ .windows322
+ .iter()323
+ .filter(|(_, w)| w.x11_surface().is_some())324
+ .map(|(id, _)| *id)325
+ .collect();326
+ for id in gone {327
+ self.remove_window(id);328
+ }329
+ if let Some(x) = self.xwayland.as_mut() {330
+ x.wm = None;331
+ x.unmanaged.clear();332
+ }333
+ rearm(self);334
+ }335
+}336
+337
+/// Dialogs, splash screens and fixed-size windows float at their own size.338
+fn floats(window: &X11Surface) -> bool {339
+ let kind = matches!(340
+ window.window_type(),341
+ Some(342
+ WmWindowType::Dialog343
+ | WmWindowType::Splash344
+ | WmWindowType::Utility345
+ | WmWindowType::Toolbar346
+ | WmWindowType::Menu347
+ )348
+ );349
+ let fixed = matches!(350
+ (window.min_size(), window.max_size()),351
+ (Some(min), Some(max)) if min == max && min.w > 0352
+ );353
+ kind || fixed || window.is_transient_for().is_some()354
+}355
+356
+impl State {357
+ pub fn x11_window_id(&self, window: &X11Surface) -> Option<monolith_layout::WindowId> {358
+ self.windows359
+ .iter()360
+ .find(|(_, w)| w.x11_surface() == Some(window))361
+ .map(|(id, _)| *id)362
+ }363
+364
+ fn forget_x11(&mut self, window: &X11Surface) {365
+ if let Some(x) = self.xwayland.as_mut() {366
+ x.unmanaged.retain(|w| w != window);367
+ }368
+ if let Some(id) = self.x11_window_id(window) {369
+ self.remove_window(id);370
+ }371
+ self.needs_redraw = true;372
+ }373
+374
+ fn x11_fullscreen(&mut self, window: &X11Surface, on: bool) {375
+ if let Some(id) = self.x11_window_id(window)376
+ && self.layout.window(id).is_some_and(|w| w.fullscreen != on)377
+ {378
+ self.layout.toggle_fullscreen(id);379
+ self.apply_layout();380
+ }381
+ }382
+}