diff --git a/Cargo.toml b/Cargo.toml index 523bd19..3617c76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ wayland-client.workspace = true wayland-protocols = { workspace = true, features = ["client", "server", "staging", "unstable"] } wayland-scanner.workspace = true wayland-server.workspace = true -xcb = { version = "1.3.0", features = ["composite"] } +xcb = { version = "1.3.0", features = ["composite", "randr"] } wl_drm = { path = "wl_drm" } libc = "0.2.153" log = "0.4.21" diff --git a/src/lib.rs b/src/lib.rs index 9a0b957..93abab3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,14 +4,13 @@ mod server; pub mod xstate; use crate::server::{PendingSurfaceState, ServerState}; -use crate::xstate::XState; +use crate::xstate::{RealConnection, XState}; use log::{error, info}; use rustix::event::{poll, PollFd, PollFlags}; use std::io::{BufRead, BufReader, Read, Write}; use std::os::fd::{AsFd, AsRawFd, BorrowedFd}; use std::os::unix::net::UnixStream; use std::process::{Command, Stdio}; -use std::sync::Arc; use wayland_server::{Display, ListeningSocket}; use xcb::x; @@ -22,7 +21,12 @@ pub trait XConnection: Sized + 'static { fn root_window(&self) -> x::Window; fn set_window_dims(&mut self, window: x::Window, dims: PendingSurfaceState); fn set_fullscreen(&mut self, window: x::Window, fullscreen: bool, data: Self::ExtraData); - fn focus_window(&mut self, window: x::Window, data: Self::ExtraData); + fn focus_window( + &mut self, + window: x::Window, + output_name: Option, + data: Self::ExtraData, + ); fn close_window(&mut self, window: x::Window, data: Self::ExtraData); fn raise_to_top(&mut self, window: x::Window); } @@ -36,7 +40,7 @@ pub trait MimeTypeData { fn data(&self) -> &[u8]; } -type RealServerState = ServerState>; +type RealServerState = ServerState; pub trait RunData { fn display(&self) -> Option<&str>; @@ -156,8 +160,7 @@ pub fn main(data: impl RunData) -> Option<()> { display.insert(0, ':'); info!("Connected to Xwayland on {display}"); data.xwayland_ready(display); - server_state.set_x_connection(xstate.connection.clone()); - server_state.atoms = Some(xstate.atoms.clone()); + xstate.server_state_setup(&mut server_state); #[cfg(feature = "systemd")] { diff --git a/src/server/dispatch.rs b/src/server/dispatch.rs index 59febaf..4a21ec6 100644 --- a/src/server/dispatch.rs +++ b/src/server/dispatch.rs @@ -241,6 +241,7 @@ impl role: None, xwl: None, window: None, + output_key: None, } .into() }); diff --git a/src/server/event.rs b/src/server/event.rs index 367b82d..e76f272 100644 --- a/src/server/event.rs +++ b/src/server/event.rs @@ -91,8 +91,25 @@ impl HandleEvent for SurfaceData { } impl SurfaceData { + fn get_output_name(&self, state: &ServerState) -> Option { + let output_name = self + .output_key + .and_then(|key| state.objects.get(key)) + .map(|obj| <_ as AsRef>::as_ref(obj).name.clone()); + + if output_name.is_none() { + warn!( + "{} has no output name ({:?})", + self.server.id(), + self.output_key + ); + } + + output_name + } + fn surface_event( - &self, + &mut self, event: client::wl_surface::Event, state: &mut ServerState, ) { @@ -106,10 +123,14 @@ impl SurfaceData { }; let output: &mut Output = object.as_mut(); + self.server.enter(&output.server); + self.output_key = Some(key); + debug!("{} entered {}", self.server.id(), output.server.id()); + let windows = &mut state.windows; if let Some(win_data) = self .window .as_ref() - .map(|win| state.windows.get_mut(&win).unwrap()) + .map(|win| windows.get_mut(&win).unwrap()) { let (x, y) = match output.position { OutputPosition::Xdg { x, y } => (x, y), @@ -120,10 +141,16 @@ impl SurfaceData { WindowOutputOffset { x, y }, state.connection.as_mut().unwrap(), ); - output.windows.insert(win_data.window); + let window = win_data.window; + output.windows.insert(window); + if self.window.is_some() && state.last_focused_toplevel == self.window { + let data = C::ExtraData::create(state); + let output = self.get_output_name(state); + let conn = state.connection.as_mut().unwrap(); + debug!("focused window changed outputs - resetting primary output"); + conn.focus_window(window, output, data); + } } - self.server.enter(&output.server); - debug!("{} entered {}", self.server.id(), output.server.id()); } Event::Leave { output } => { let key: ObjectKey = output.data().copied().unwrap(); @@ -132,6 +159,9 @@ impl SurfaceData { }; let output: &mut Output = object.as_mut(); self.server.leave(&output.server); + if self.output_key == Some(key) { + self.output_key = None; + } } Event::PreferredBufferScale { factor } => self.server.preferred_buffer_scale(factor), other => warn!("unhandled surface request: {other:?}"), @@ -522,7 +552,11 @@ impl HandleEvent for Keyboard { .map(|o| <_ as AsRef>::as_ref(o)) { state.last_kb_serial = Some(serial); - state.to_focus = Some(data.window.unwrap()); + let output_name = data.get_output_name(state); + state.to_focus = Some(FocusData { + window: data.window.unwrap(), + output_name, + }); self.server.enter(serial, &data.server, keys); } } @@ -536,7 +570,7 @@ impl HandleEvent for Keyboard { .get(key) .map(|o| <_ as AsRef>::as_ref(o)) { - if state.to_focus == Some(data.window.unwrap()) { + if state.to_focus.as_ref().map(|d| d.window) == Some(data.window.unwrap()) { state.to_focus.take(); } else { state.unfocus = true; @@ -637,6 +671,7 @@ pub struct Output { pub xdg: Option, windows: HashSet, position: OutputPosition, + name: String, } impl Output { @@ -647,6 +682,7 @@ impl Output { xdg: None, windows: HashSet::new(), position: OutputPosition::Wl { x: 0, y: 0 }, + name: "".to_string(), } } } @@ -723,7 +759,12 @@ impl Output { simple_event_shunt! { self.server, event: client::wl_output::Event => [ - Name { name }, + Name { + |name| { + self.name = name.clone(); + name + } + }, Description { description }, Mode { |flags| convert_wenum(flags), diff --git a/src/server/mod.rs b/src/server/mod.rs index e71a6a2..af3698a 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -179,6 +179,7 @@ pub struct SurfaceData { role: Option, xwl: Option, window: Option, + output_key: Option, } impl SurfaceData { @@ -468,6 +469,12 @@ fn handle_globals<'a, C: XConnection>( new_key_type! { pub struct ObjectKey; } + +struct FocusData { + window: x::Window, + output_name: Option, +} + pub struct ServerState { pub atoms: Option, dh: DisplayHandle, @@ -478,11 +485,11 @@ pub struct ServerState { qh: ClientQueueHandle, client: Option, - to_focus: Option, + to_focus: Option, unfocus: bool, last_focused_toplevel: Option, last_hovered: Option, - connection: Option, + pub connection: Option, xdg_wm_base: XdgWmBase, clipboard_data: Option>, @@ -846,16 +853,20 @@ impl ServerState { } { - if let Some(win) = self.to_focus.take() { + if let Some(FocusData { + window, + output_name, + }) = self.to_focus.take() + { let data = C::ExtraData::create(self); let conn = self.connection.as_mut().unwrap(); - debug!("focusing window {win:?}"); - conn.focus_window(win, data); - self.last_focused_toplevel = Some(win); + debug!("focusing window {window:?}"); + conn.focus_window(window, output_name, data); + self.last_focused_toplevel = Some(window); } else if self.unfocus { let data = C::ExtraData::create(self); let conn = self.connection.as_mut().unwrap(); - conn.focus_window(x::WINDOW_NONE, data); + conn.focus_window(x::WINDOW_NONE, None, data); } self.unfocus = false; } diff --git a/src/server/tests.rs b/src/server/tests.rs index 2c34362..f8d02a7 100644 --- a/src/server/tests.rs +++ b/src/server/tests.rs @@ -221,7 +221,7 @@ impl super::XConnection for FakeXConnection { } #[track_caller] - fn focus_window(&mut self, window: Window, _: ()) { + fn focus_window(&mut self, window: Window, _output_name: Option, _: ()) { assert!( self.windows.contains_key(&window), "Unknown window: {window:?}" diff --git a/src/xstate/mod.rs b/src/xstate/mod.rs index a8d93cd..d1b5cb4 100644 --- a/src/xstate/mod.rs +++ b/src/xstate/mod.rs @@ -1,12 +1,13 @@ mod selection; use selection::{SelectionData, SelectionTarget}; -use crate::server::WindowAttributes; +use crate::{server::WindowAttributes, XConnection}; use bitflags::bitflags; use log::{debug, trace, warn}; +use std::collections::HashMap; use std::ffi::CString; use std::os::fd::{AsRawFd, BorrowedFd}; -use std::sync::Arc; +use std::rc::Rc; use xcb::{x, Xid, XidNew}; use xcb_util_cursor::{Cursor, CursorContext}; @@ -103,8 +104,8 @@ impl WmName { } pub struct XState { - pub connection: Arc, - pub atoms: Atoms, + connection: Rc, + atoms: Atoms, root: x::Window, wm_window: x::Window, selection_data: SelectionData, @@ -112,7 +113,15 @@ pub struct XState { impl XState { pub fn new(fd: BorrowedFd) -> Self { - let connection = Arc::new(xcb::Connection::connect_to_fd(fd.as_raw_fd(), None).unwrap()); + let connection = Rc::new( + xcb::Connection::connect_to_fd_with_extensions( + fd.as_raw_fd(), + None, + &[xcb::Extension::Composite, xcb::Extension::RandR], + &[], + ) + .unwrap(), + ); let setup = connection.get_setup(); let screen = setup.roots().next().unwrap(); let root = screen.root(); @@ -139,6 +148,14 @@ impl XState { }) .unwrap(); + // Track RandR output changes + connection + .send_and_check_request(&xcb::randr::SelectInput { + window: root, + enable: xcb::randr::NotifyMask::RESOURCE_CHANGE, + }) + .unwrap(); + { // Setup default cursor theme let ctx = CursorContext::new(&connection, screen).unwrap(); @@ -164,6 +181,13 @@ impl XState { r } + pub fn server_state_setup(&self, server_state: &mut super::RealServerState) { + let mut c = RealConnection::new(self.connection.clone()); + c.update_outputs(self.root); + server_state.set_x_connection(c); + server_state.atoms = Some(self.atoms.clone()); + } + fn set_root_property(&self, property: x::Atom, r#type: x::Atom, data: &[P]) { self.connection .send_and_check_request(&x::ChangeProperty { @@ -309,9 +333,10 @@ impl XState { let active_win: &[x::Window] = active_win.value(); if active_win[0] == e.window() { - <_ as super::XConnection>::focus_window( - &mut self.connection, + // The connection on the server state stores state. + server_state.connection.as_mut().unwrap().focus_window( x::Window::none(), + None, self.atoms.clone(), ); } @@ -401,6 +426,15 @@ impl XState { t => warn!("unrecognized message: {t:?}"), }, xcb::Event::X(x::Event::MappingNotify(_)) => {} + xcb::Event::RandR(xcb::randr::Event::Notify(e)) + if matches!(e.u(), xcb::randr::NotifyData::Rc(_)) => + { + server_state + .connection + .as_mut() + .unwrap() + .update_outputs(self.root); + } other => { warn!("unhandled event: {other:?}"); } @@ -774,17 +808,73 @@ impl TryFrom for SetState { } } -impl super::XConnection for Arc { +pub struct RealConnection { + connection: Rc, + outputs: HashMap, + primary_output: xcb::randr::Output, +} + +impl RealConnection { + fn new(connection: Rc) -> Self { + Self { + connection, + outputs: Default::default(), + primary_output: Xid::none(), + } + } + + fn update_outputs(&mut self, root: x::Window) { + self.outputs.clear(); + let reply = self + .connection + .wait_for_reply( + self.connection + .send_request(&xcb::randr::GetScreenResources { window: root }), + ) + .expect("Couldn't grab screen resources"); + + for output in reply.outputs().iter().copied() { + let reply = self + .connection + .wait_for_reply(self.connection.send_request(&xcb::randr::GetOutputInfo { + output, + config_timestamp: reply.config_timestamp(), + })) + .expect("Couldn't get output info"); + + let name = std::str::from_utf8(reply.name()) + .unwrap_or_else(|_| panic!("couldn't parse output name: {:?}", reply.name())); + + self.outputs.insert(name.to_string(), output); + } + + self.primary_output = self + .connection + .wait_for_reply( + self.connection + .send_request(&xcb::randr::GetOutputPrimary { window: root }), + ) + .expect("Couldn't get primary output") + .output(); + + debug!( + "new outputs: {:?} | primary: {:?}", + self.outputs, self.primary_output + ); + } +} + +impl XConnection for RealConnection { type ExtraData = Atoms; type MimeTypeData = SelectionTarget; fn root_window(&self) -> x::Window { - self.get_setup().roots().next().unwrap().root() + self.connection.get_setup().roots().next().unwrap().root() } fn set_window_dims(&mut self, window: x::Window, dims: crate::server::PendingSurfaceState) { trace!("set window dimensions {window:?} {dims:?}"); - unwrap_or_skip_bad_window!(self.send_and_check_request(&x::ConfigureWindow { + unwrap_or_skip_bad_window!(self.connection.send_and_check_request(&x::ConfigureWindow { window, value_list: &[ x::ConfigWindow::X(dims.x), @@ -801,38 +891,74 @@ impl super::XConnection for Arc { } else { &[] }; - self.send_and_check_request(&x::ChangeProperty:: { - mode: x::PropMode::Replace, - window, - property: atoms.net_wm_state, - r#type: x::ATOM_ATOM, - data, - }) - .unwrap(); + self.connection + .send_and_check_request(&x::ChangeProperty:: { + mode: x::PropMode::Replace, + window, + property: atoms.net_wm_state, + r#type: x::ATOM_ATOM, + data, + }) + .unwrap(); } - fn focus_window(&mut self, window: x::Window, atoms: Self::ExtraData) { - if let Err(e) = self.send_and_check_request(&x::SetInputFocus { + fn focus_window( + &mut self, + window: x::Window, + output_name: Option, + atoms: Self::ExtraData, + ) { + trace!("{window:?} {output_name:?}"); + if let Err(e) = self.connection.send_and_check_request(&x::SetInputFocus { focus: window, revert_to: x::InputFocus::None, time: x::CURRENT_TIME, }) { - log::debug!("SetInputFocus failed ({:?}: {:?})", window, e); + debug!("SetInputFocus failed ({:?}: {:?})", window, e); return; } - if let Err(e) = self.send_and_check_request(&x::ChangeProperty { + if let Err(e) = self.connection.send_and_check_request(&x::ChangeProperty { mode: x::PropMode::Replace, window: self.root_window(), property: atoms.active_win, r#type: x::ATOM_WINDOW, data: &[window], }) { - log::debug!("ChangeProperty failed ({:?}: {:?})", window, e); + debug!("ChangeProperty failed ({:?}: {:?})", window, e); + } + + if let Some(name) = output_name { + let Some(output) = self.outputs.get(&name).copied() else { + warn!("Couldn't find output {name}, primary output will be wrong"); + return; + }; + if output == self.primary_output { + debug!("primary output is already {name}"); + return; + } + + if let Err(e) = self + .connection + .send_and_check_request(&xcb::randr::SetOutputPrimary { window, output }) + { + warn!("Couldn't set output {name} as primary: {e:?}"); + } else { + debug!("set {name} as primary output"); + self.primary_output = output; + } + } else { + let _ = self + .connection + .send_and_check_request(&xcb::randr::SetOutputPrimary { + window, + output: Xid::none(), + }); + self.primary_output = Xid::none(); } } fn close_window(&mut self, window: x::Window, atoms: Self::ExtraData) { - let cookie = self.send_request(&x::GetProperty { + let cookie = self.connection.send_request(&x::GetProperty { window, delete: false, property: atoms.wm_protocols, @@ -840,7 +966,7 @@ impl super::XConnection for Arc { long_offset: 0, long_length: 10, }); - let reply = unwrap_or_skip_bad_window!(self.wait_for_reply(cookie)); + let reply = unwrap_or_skip_bad_window!(self.connection.wait_for_reply(cookie)); if reply.value::().contains(&atoms.wm_delete_window) { let data = [atoms.wm_delete_window.resource_id(), 0, 0, 0, 0]; @@ -850,28 +976,28 @@ impl super::XConnection for Arc { x::ClientMessageData::Data32(data), ); - unwrap_or_skip_bad_window!(self.send_and_check_request(&x::SendEvent { + unwrap_or_skip_bad_window!(self.connection.send_and_check_request(&x::SendEvent { destination: x::SendEventDest::Window(window), propagate: false, event_mask: x::EventMask::empty(), event, })); } else { - unwrap_or_skip_bad_window!(self.send_and_check_request(&x::KillClient { + unwrap_or_skip_bad_window!(self.connection.send_and_check_request(&x::KillClient { resource: window.resource_id() })) } } fn raise_to_top(&mut self, window: x::Window) { - unwrap_or_skip_bad_window!(self.send_and_check_request(&x::ConfigureWindow { + unwrap_or_skip_bad_window!(self.connection.send_and_check_request(&x::ConfigureWindow { window, value_list: &[x::ConfigWindow::StackMode(x::StackMode::Above)], })); } } -impl super::FromServerState> for Atoms { +impl super::FromServerState for Atoms { fn create(state: &super::RealServerState) -> Self { state.atoms.as_ref().unwrap().clone() } diff --git a/tests/integration.rs b/tests/integration.rs index da3fcdf..aa20a95 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -82,7 +82,7 @@ impl Drop for Fixture { } impl Fixture { - fn new() -> Self { + fn new_preset(pre_connect: impl FnOnce(&mut testwl::Server)) -> Self { static INIT: Once = Once::new(); INIT.call_once(|| { env_logger::builder() @@ -94,6 +94,7 @@ impl Fixture { let (a, b) = UnixStream::pair().unwrap(); let mut testwl = testwl::Server::new(false); + pre_connect(&mut testwl); testwl.connect(a); let our_data = TestData::new(b); let data = our_data.clone(); @@ -147,6 +148,9 @@ impl Fixture { display, } } + fn new() -> Self { + Self::new_preset(|_| {}) + } #[track_caller] fn wait_and_dispatch(&mut self) { @@ -193,6 +197,22 @@ impl Fixture { assert_eq!(geometry.height(), 100); } + #[track_caller] + fn map_as_toplevel( + &mut self, + connection: &mut Connection, + window: x::Window, + ) -> testwl::SurfaceId { + connection.map_window(window); + self.wait_and_dispatch(); + let surface = self + .testwl + .last_created_surface_id() + .expect("No surface created"); + self.configure_and_verify_new_toplevel(connection, window, surface); + surface + } + /// Triggers a Wayland side toplevel Close event and processes the corresponding /// X11 side WM_DELETE_WINDOW client message fn wm_delete_window( @@ -229,6 +249,12 @@ impl Fixture { other => panic!("wrong data type: {other:?}"), } } + + fn create_output(&mut self, x: i32, y: i32) -> wayland_server::protocol::wl_output::WlOutput { + self.testwl.new_output(x, y); + self.wait_and_dispatch(); + self.testwl.last_created_output() + } } xcb::atoms_struct! { @@ -927,9 +953,7 @@ fn different_output_position() { .expect("No surface created!"); f.configure_and_verify_new_toplevel(&mut connection, window, surface); - f.testwl.new_output(0, 0); - f.wait_and_dispatch(); - let output = f.testwl.last_created_output(); + let output = f.create_output(0, 0); f.testwl.move_surface_to_output(surface, &output); f.testwl.move_pointer_to(surface, 10.0, 10.0); f.wait_and_dispatch(); @@ -938,9 +962,7 @@ fn different_output_position() { assert_eq!(reply.win_x(), 10); assert_eq!(reply.win_y(), 10); - f.testwl.new_output(100, 0); - f.wait_and_dispatch(); - let output = f.testwl.last_created_output(); + let output = f.create_output(100, 0); //f.testwl.move_xdg_output(&output, 100, 0); f.testwl.move_surface_to_output(surface, &output); f.testwl.move_pointer_to(surface, 150.0, 12.0); @@ -1111,3 +1133,67 @@ fn close_window() { // Connection should no longer work (KillClient) assert!(connection.poll_for_event().is_err()); } + +// TODO: figure out if the sleeps in this test can be dealt with... +#[test] +fn primary_output() { + let mut f = Fixture::new_preset(|testwl| { + testwl.new_output(0, 0); // WL-1 + testwl.new_output(500, 500); // WL-2 + }); + let mut conn = Connection::new(&f.display); + + let reply = conn.get_reply(&xcb::randr::GetScreenResources { window: conn.root }); + let config_timestamp = reply.config_timestamp(); + let mut it = reply.outputs().into_iter().copied().map(|output| { + let reply = conn.get_reply(&xcb::randr::GetOutputInfo { + output, + config_timestamp, + }); + let name = std::str::from_utf8(reply.name()).unwrap(); + ( + f.testwl + .get_output(name) + .unwrap_or_else(|| panic!("Couldn't find output {name}")), + output, + ) + }); + let (wl_output1, output1) = it.next().expect("Couldn't find first output"); + let (wl_output2, output2) = it.next().expect("Couldn't find second output"); + assert_eq!(it.collect::>(), vec![]); + + let window1 = conn.new_window(conn.root, 0, 0, 20, 20, false); + let surface1 = f.map_as_toplevel(&mut conn, window1); + f.testwl.move_surface_to_output(surface1, &wl_output1); + let window2 = conn.new_window(conn.root, 0, 0, 20, 20, false); + std::thread::sleep(std::time::Duration::from_millis(10)); + let surface2 = f.map_as_toplevel(&mut conn, window2); + f.testwl.move_surface_to_output(surface2, &wl_output2); + assert_ne!(surface1, surface2); + f.wait_and_dispatch(); + + f.testwl.focus_toplevel(surface1); + std::thread::sleep(std::time::Duration::from_millis(10)); + let reply = conn.get_reply(&xcb::randr::GetOutputPrimary { window: conn.root }); + assert_eq!(reply.output(), output1); + + f.testwl.focus_toplevel(surface2); + std::thread::sleep(std::time::Duration::from_millis(10)); + let reply = conn.get_reply(&xcb::randr::GetOutputPrimary { window: conn.root }); + assert_eq!(reply.output(), output2); + + let wl_output3 = f.create_output(24, 46); + f.testwl.move_surface_to_output(surface2, &wl_output3); + std::thread::sleep(std::time::Duration::from_millis(10)); + + let reply = conn.get_reply(&xcb::randr::GetScreenResources { window: conn.root }); + assert_eq!(reply.outputs().len(), 3); + let output3 = reply + .outputs() + .iter() + .copied() + .find(|o| ![output1, output2].contains(o)) + .unwrap(); + let reply = conn.get_reply(&xcb::randr::GetOutputPrimary { window: conn.root }); + assert_eq!(reply.output(), output3); +} diff --git a/testwl/src/lib.rs b/testwl/src/lib.rs index abe1b65..9422707 100644 --- a/testwl/src/lib.rs +++ b/testwl/src/lib.rs @@ -167,6 +167,7 @@ struct DataSourceData { } struct Output { + name: String, wl: WlOutput, xdg: Option, } @@ -565,6 +566,13 @@ impl Server { self.display.flush_clients().unwrap(); } + pub fn get_output(&mut self, name: &str) -> Option { + self.state + .outputs + .iter() + .find_map(|(output, data)| (data.name == name).then_some(output.clone())) + } + pub fn move_output(&mut self, output: &WlOutput, x: i32, y: i32) { output.geometry( x, @@ -786,11 +794,14 @@ impl GlobalDispatch for State { "fake monitor".to_string(), wl_output::Transform::Normal, ); + let name = format!("WL-{}", state.outputs.len() + 1); + output.name(name.clone()); output.mode(wl_output::Mode::Current, 1000, 1000, 0); output.done(); state.outputs.insert( output.clone(), Output { + name, wl: output.clone(), xdg: None, },