Set primary output when window focused

Closes #80
This commit is contained in:
Shawn Wallace 2024-12-18 01:49:55 -05:00
parent c45c2ed990
commit 03a53b6ad7
9 changed files with 338 additions and 59 deletions

View file

@ -25,7 +25,7 @@ wayland-client.workspace = true
wayland-protocols = { workspace = true, features = ["client", "server", "staging", "unstable"] } wayland-protocols = { workspace = true, features = ["client", "server", "staging", "unstable"] }
wayland-scanner.workspace = true wayland-scanner.workspace = true
wayland-server.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" } wl_drm = { path = "wl_drm" }
libc = "0.2.153" libc = "0.2.153"
log = "0.4.21" log = "0.4.21"

View file

@ -4,14 +4,13 @@ mod server;
pub mod xstate; pub mod xstate;
use crate::server::{PendingSurfaceState, ServerState}; use crate::server::{PendingSurfaceState, ServerState};
use crate::xstate::XState; use crate::xstate::{RealConnection, XState};
use log::{error, info}; use log::{error, info};
use rustix::event::{poll, PollFd, PollFlags}; use rustix::event::{poll, PollFd, PollFlags};
use std::io::{BufRead, BufReader, Read, Write}; use std::io::{BufRead, BufReader, Read, Write};
use std::os::fd::{AsFd, AsRawFd, BorrowedFd}; use std::os::fd::{AsFd, AsRawFd, BorrowedFd};
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::sync::Arc;
use wayland_server::{Display, ListeningSocket}; use wayland_server::{Display, ListeningSocket};
use xcb::x; use xcb::x;
@ -22,7 +21,12 @@ pub trait XConnection: Sized + 'static {
fn root_window(&self) -> x::Window; fn root_window(&self) -> x::Window;
fn set_window_dims(&mut self, window: x::Window, dims: PendingSurfaceState); 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 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<String>,
data: Self::ExtraData,
);
fn close_window(&mut self, window: x::Window, data: Self::ExtraData); fn close_window(&mut self, window: x::Window, data: Self::ExtraData);
fn raise_to_top(&mut self, window: x::Window); fn raise_to_top(&mut self, window: x::Window);
} }
@ -36,7 +40,7 @@ pub trait MimeTypeData {
fn data(&self) -> &[u8]; fn data(&self) -> &[u8];
} }
type RealServerState = ServerState<Arc<xcb::Connection>>; type RealServerState = ServerState<RealConnection>;
pub trait RunData { pub trait RunData {
fn display(&self) -> Option<&str>; fn display(&self) -> Option<&str>;
@ -156,8 +160,7 @@ pub fn main(data: impl RunData) -> Option<()> {
display.insert(0, ':'); display.insert(0, ':');
info!("Connected to Xwayland on {display}"); info!("Connected to Xwayland on {display}");
data.xwayland_ready(display); data.xwayland_ready(display);
server_state.set_x_connection(xstate.connection.clone()); xstate.server_state_setup(&mut server_state);
server_state.atoms = Some(xstate.atoms.clone());
#[cfg(feature = "systemd")] #[cfg(feature = "systemd")]
{ {

View file

@ -241,6 +241,7 @@ impl<C: XConnection>
role: None, role: None,
xwl: None, xwl: None,
window: None, window: None,
output_key: None,
} }
.into() .into()
}); });

View file

@ -91,8 +91,25 @@ impl HandleEvent for SurfaceData {
} }
impl SurfaceData { impl SurfaceData {
fn get_output_name(&self, state: &ServerState<impl XConnection>) -> Option<String> {
let output_name = self
.output_key
.and_then(|key| state.objects.get(key))
.map(|obj| <_ as AsRef<Output>>::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<C: XConnection>( fn surface_event<C: XConnection>(
&self, &mut self,
event: client::wl_surface::Event, event: client::wl_surface::Event,
state: &mut ServerState<C>, state: &mut ServerState<C>,
) { ) {
@ -106,10 +123,14 @@ impl SurfaceData {
}; };
let output: &mut Output = object.as_mut(); 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 if let Some(win_data) = self
.window .window
.as_ref() .as_ref()
.map(|win| state.windows.get_mut(&win).unwrap()) .map(|win| windows.get_mut(&win).unwrap())
{ {
let (x, y) = match output.position { let (x, y) = match output.position {
OutputPosition::Xdg { x, y } => (x, y), OutputPosition::Xdg { x, y } => (x, y),
@ -120,10 +141,16 @@ impl SurfaceData {
WindowOutputOffset { x, y }, WindowOutputOffset { x, y },
state.connection.as_mut().unwrap(), 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 } => { Event::Leave { output } => {
let key: ObjectKey = output.data().copied().unwrap(); let key: ObjectKey = output.data().copied().unwrap();
@ -132,6 +159,9 @@ impl SurfaceData {
}; };
let output: &mut Output = object.as_mut(); let output: &mut Output = object.as_mut();
self.server.leave(&output.server); self.server.leave(&output.server);
if self.output_key == Some(key) {
self.output_key = None;
}
} }
Event::PreferredBufferScale { factor } => self.server.preferred_buffer_scale(factor), Event::PreferredBufferScale { factor } => self.server.preferred_buffer_scale(factor),
other => warn!("unhandled surface request: {other:?}"), other => warn!("unhandled surface request: {other:?}"),
@ -522,7 +552,11 @@ impl HandleEvent for Keyboard {
.map(|o| <_ as AsRef<SurfaceData>>::as_ref(o)) .map(|o| <_ as AsRef<SurfaceData>>::as_ref(o))
{ {
state.last_kb_serial = Some(serial); 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); self.server.enter(serial, &data.server, keys);
} }
} }
@ -536,7 +570,7 @@ impl HandleEvent for Keyboard {
.get(key) .get(key)
.map(|o| <_ as AsRef<SurfaceData>>::as_ref(o)) .map(|o| <_ as AsRef<SurfaceData>>::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(); state.to_focus.take();
} else { } else {
state.unfocus = true; state.unfocus = true;
@ -637,6 +671,7 @@ pub struct Output {
pub xdg: Option<XdgOutput>, pub xdg: Option<XdgOutput>,
windows: HashSet<x::Window>, windows: HashSet<x::Window>,
position: OutputPosition, position: OutputPosition,
name: String,
} }
impl Output { impl Output {
@ -647,6 +682,7 @@ impl Output {
xdg: None, xdg: None,
windows: HashSet::new(), windows: HashSet::new(),
position: OutputPosition::Wl { x: 0, y: 0 }, position: OutputPosition::Wl { x: 0, y: 0 },
name: "<unknown>".to_string(),
} }
} }
} }
@ -723,7 +759,12 @@ impl Output {
simple_event_shunt! { simple_event_shunt! {
self.server, event: client::wl_output::Event => [ self.server, event: client::wl_output::Event => [
Name { name }, Name {
|name| {
self.name = name.clone();
name
}
},
Description { description }, Description { description },
Mode { Mode {
|flags| convert_wenum(flags), |flags| convert_wenum(flags),

View file

@ -179,6 +179,7 @@ pub struct SurfaceData {
role: Option<SurfaceRole>, role: Option<SurfaceRole>,
xwl: Option<XwaylandSurfaceV1>, xwl: Option<XwaylandSurfaceV1>,
window: Option<x::Window>, window: Option<x::Window>,
output_key: Option<ObjectKey>,
} }
impl SurfaceData { impl SurfaceData {
@ -468,6 +469,12 @@ fn handle_globals<'a, C: XConnection>(
new_key_type! { new_key_type! {
pub struct ObjectKey; pub struct ObjectKey;
} }
struct FocusData {
window: x::Window,
output_name: Option<String>,
}
pub struct ServerState<C: XConnection> { pub struct ServerState<C: XConnection> {
pub atoms: Option<Atoms>, pub atoms: Option<Atoms>,
dh: DisplayHandle, dh: DisplayHandle,
@ -478,11 +485,11 @@ pub struct ServerState<C: XConnection> {
qh: ClientQueueHandle, qh: ClientQueueHandle,
client: Option<Client>, client: Option<Client>,
to_focus: Option<x::Window>, to_focus: Option<FocusData>,
unfocus: bool, unfocus: bool,
last_focused_toplevel: Option<x::Window>, last_focused_toplevel: Option<x::Window>,
last_hovered: Option<x::Window>, last_hovered: Option<x::Window>,
connection: Option<C>, pub connection: Option<C>,
xdg_wm_base: XdgWmBase, xdg_wm_base: XdgWmBase,
clipboard_data: Option<ClipboardData<C::MimeTypeData>>, clipboard_data: Option<ClipboardData<C::MimeTypeData>>,
@ -846,16 +853,20 @@ impl<C: XConnection> ServerState<C> {
} }
{ {
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 data = C::ExtraData::create(self);
let conn = self.connection.as_mut().unwrap(); let conn = self.connection.as_mut().unwrap();
debug!("focusing window {win:?}"); debug!("focusing window {window:?}");
conn.focus_window(win, data); conn.focus_window(window, output_name, data);
self.last_focused_toplevel = Some(win); self.last_focused_toplevel = Some(window);
} else if self.unfocus { } else if self.unfocus {
let data = C::ExtraData::create(self); let data = C::ExtraData::create(self);
let conn = self.connection.as_mut().unwrap(); 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; self.unfocus = false;
} }

View file

@ -221,7 +221,7 @@ impl super::XConnection for FakeXConnection {
} }
#[track_caller] #[track_caller]
fn focus_window(&mut self, window: Window, _: ()) { fn focus_window(&mut self, window: Window, _output_name: Option<String>, _: ()) {
assert!( assert!(
self.windows.contains_key(&window), self.windows.contains_key(&window),
"Unknown window: {window:?}" "Unknown window: {window:?}"

View file

@ -1,12 +1,13 @@
mod selection; mod selection;
use selection::{SelectionData, SelectionTarget}; use selection::{SelectionData, SelectionTarget};
use crate::server::WindowAttributes; use crate::{server::WindowAttributes, XConnection};
use bitflags::bitflags; use bitflags::bitflags;
use log::{debug, trace, warn}; use log::{debug, trace, warn};
use std::collections::HashMap;
use std::ffi::CString; use std::ffi::CString;
use std::os::fd::{AsRawFd, BorrowedFd}; use std::os::fd::{AsRawFd, BorrowedFd};
use std::sync::Arc; use std::rc::Rc;
use xcb::{x, Xid, XidNew}; use xcb::{x, Xid, XidNew};
use xcb_util_cursor::{Cursor, CursorContext}; use xcb_util_cursor::{Cursor, CursorContext};
@ -103,8 +104,8 @@ impl WmName {
} }
pub struct XState { pub struct XState {
pub connection: Arc<xcb::Connection>, connection: Rc<xcb::Connection>,
pub atoms: Atoms, atoms: Atoms,
root: x::Window, root: x::Window,
wm_window: x::Window, wm_window: x::Window,
selection_data: SelectionData, selection_data: SelectionData,
@ -112,7 +113,15 @@ pub struct XState {
impl XState { impl XState {
pub fn new(fd: BorrowedFd) -> Self { 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 setup = connection.get_setup();
let screen = setup.roots().next().unwrap(); let screen = setup.roots().next().unwrap();
let root = screen.root(); let root = screen.root();
@ -139,6 +148,14 @@ impl XState {
}) })
.unwrap(); .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 // Setup default cursor theme
let ctx = CursorContext::new(&connection, screen).unwrap(); let ctx = CursorContext::new(&connection, screen).unwrap();
@ -164,6 +181,13 @@ impl XState {
r 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<P: x::PropEl>(&self, property: x::Atom, r#type: x::Atom, data: &[P]) { fn set_root_property<P: x::PropEl>(&self, property: x::Atom, r#type: x::Atom, data: &[P]) {
self.connection self.connection
.send_and_check_request(&x::ChangeProperty { .send_and_check_request(&x::ChangeProperty {
@ -309,9 +333,10 @@ impl XState {
let active_win: &[x::Window] = active_win.value(); let active_win: &[x::Window] = active_win.value();
if active_win[0] == e.window() { if active_win[0] == e.window() {
<_ as super::XConnection>::focus_window( // The connection on the server state stores state.
&mut self.connection, server_state.connection.as_mut().unwrap().focus_window(
x::Window::none(), x::Window::none(),
None,
self.atoms.clone(), self.atoms.clone(),
); );
} }
@ -401,6 +426,15 @@ impl XState {
t => warn!("unrecognized message: {t:?}"), t => warn!("unrecognized message: {t:?}"),
}, },
xcb::Event::X(x::Event::MappingNotify(_)) => {} 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 => { other => {
warn!("unhandled event: {other:?}"); warn!("unhandled event: {other:?}");
} }
@ -774,17 +808,73 @@ impl TryFrom<u32> for SetState {
} }
} }
impl super::XConnection for Arc<xcb::Connection> { pub struct RealConnection {
connection: Rc<xcb::Connection>,
outputs: HashMap<String, xcb::randr::Output>,
primary_output: xcb::randr::Output,
}
impl RealConnection {
fn new(connection: Rc<xcb::Connection>) -> 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 ExtraData = Atoms;
type MimeTypeData = SelectionTarget; type MimeTypeData = SelectionTarget;
fn root_window(&self) -> x::Window { 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) { fn set_window_dims(&mut self, window: x::Window, dims: crate::server::PendingSurfaceState) {
trace!("set window dimensions {window:?} {dims:?}"); 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, window,
value_list: &[ value_list: &[
x::ConfigWindow::X(dims.x), x::ConfigWindow::X(dims.x),
@ -801,38 +891,74 @@ impl super::XConnection for Arc<xcb::Connection> {
} else { } else {
&[] &[]
}; };
self.send_and_check_request(&x::ChangeProperty::<x::Atom> { self.connection
mode: x::PropMode::Replace, .send_and_check_request(&x::ChangeProperty::<x::Atom> {
window, mode: x::PropMode::Replace,
property: atoms.net_wm_state, window,
r#type: x::ATOM_ATOM, property: atoms.net_wm_state,
data, r#type: x::ATOM_ATOM,
}) data,
.unwrap(); })
.unwrap();
} }
fn focus_window(&mut self, window: x::Window, atoms: Self::ExtraData) { fn focus_window(
if let Err(e) = self.send_and_check_request(&x::SetInputFocus { &mut self,
window: x::Window,
output_name: Option<String>,
atoms: Self::ExtraData,
) {
trace!("{window:?} {output_name:?}");
if let Err(e) = self.connection.send_and_check_request(&x::SetInputFocus {
focus: window, focus: window,
revert_to: x::InputFocus::None, revert_to: x::InputFocus::None,
time: x::CURRENT_TIME, time: x::CURRENT_TIME,
}) { }) {
log::debug!("SetInputFocus failed ({:?}: {:?})", window, e); debug!("SetInputFocus failed ({:?}: {:?})", window, e);
return; 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, mode: x::PropMode::Replace,
window: self.root_window(), window: self.root_window(),
property: atoms.active_win, property: atoms.active_win,
r#type: x::ATOM_WINDOW, r#type: x::ATOM_WINDOW,
data: &[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) { 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, window,
delete: false, delete: false,
property: atoms.wm_protocols, property: atoms.wm_protocols,
@ -840,7 +966,7 @@ impl super::XConnection for Arc<xcb::Connection> {
long_offset: 0, long_offset: 0,
long_length: 10, 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::<x::Atom>().contains(&atoms.wm_delete_window) { if reply.value::<x::Atom>().contains(&atoms.wm_delete_window) {
let data = [atoms.wm_delete_window.resource_id(), 0, 0, 0, 0]; let data = [atoms.wm_delete_window.resource_id(), 0, 0, 0, 0];
@ -850,28 +976,28 @@ impl super::XConnection for Arc<xcb::Connection> {
x::ClientMessageData::Data32(data), 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), destination: x::SendEventDest::Window(window),
propagate: false, propagate: false,
event_mask: x::EventMask::empty(), event_mask: x::EventMask::empty(),
event, event,
})); }));
} else { } 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() resource: window.resource_id()
})) }))
} }
} }
fn raise_to_top(&mut self, window: x::Window) { 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, window,
value_list: &[x::ConfigWindow::StackMode(x::StackMode::Above)], value_list: &[x::ConfigWindow::StackMode(x::StackMode::Above)],
})); }));
} }
} }
impl super::FromServerState<Arc<xcb::Connection>> for Atoms { impl super::FromServerState<RealConnection> for Atoms {
fn create(state: &super::RealServerState) -> Self { fn create(state: &super::RealServerState) -> Self {
state.atoms.as_ref().unwrap().clone() state.atoms.as_ref().unwrap().clone()
} }

View file

@ -82,7 +82,7 @@ impl Drop for Fixture {
} }
impl Fixture { impl Fixture {
fn new() -> Self { fn new_preset(pre_connect: impl FnOnce(&mut testwl::Server)) -> Self {
static INIT: Once = Once::new(); static INIT: Once = Once::new();
INIT.call_once(|| { INIT.call_once(|| {
env_logger::builder() env_logger::builder()
@ -94,6 +94,7 @@ impl Fixture {
let (a, b) = UnixStream::pair().unwrap(); let (a, b) = UnixStream::pair().unwrap();
let mut testwl = testwl::Server::new(false); let mut testwl = testwl::Server::new(false);
pre_connect(&mut testwl);
testwl.connect(a); testwl.connect(a);
let our_data = TestData::new(b); let our_data = TestData::new(b);
let data = our_data.clone(); let data = our_data.clone();
@ -147,6 +148,9 @@ impl Fixture {
display, display,
} }
} }
fn new() -> Self {
Self::new_preset(|_| {})
}
#[track_caller] #[track_caller]
fn wait_and_dispatch(&mut self) { fn wait_and_dispatch(&mut self) {
@ -193,6 +197,22 @@ impl Fixture {
assert_eq!(geometry.height(), 100); 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 /// Triggers a Wayland side toplevel Close event and processes the corresponding
/// X11 side WM_DELETE_WINDOW client message /// X11 side WM_DELETE_WINDOW client message
fn wm_delete_window( fn wm_delete_window(
@ -229,6 +249,12 @@ impl Fixture {
other => panic!("wrong data type: {other:?}"), 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! { xcb::atoms_struct! {
@ -927,9 +953,7 @@ fn different_output_position() {
.expect("No surface created!"); .expect("No surface created!");
f.configure_and_verify_new_toplevel(&mut connection, window, surface); f.configure_and_verify_new_toplevel(&mut connection, window, surface);
f.testwl.new_output(0, 0); let output = f.create_output(0, 0);
f.wait_and_dispatch();
let output = f.testwl.last_created_output();
f.testwl.move_surface_to_output(surface, &output); f.testwl.move_surface_to_output(surface, &output);
f.testwl.move_pointer_to(surface, 10.0, 10.0); f.testwl.move_pointer_to(surface, 10.0, 10.0);
f.wait_and_dispatch(); f.wait_and_dispatch();
@ -938,9 +962,7 @@ fn different_output_position() {
assert_eq!(reply.win_x(), 10); assert_eq!(reply.win_x(), 10);
assert_eq!(reply.win_y(), 10); assert_eq!(reply.win_y(), 10);
f.testwl.new_output(100, 0); let output = f.create_output(100, 0);
f.wait_and_dispatch();
let output = f.testwl.last_created_output();
//f.testwl.move_xdg_output(&output, 100, 0); //f.testwl.move_xdg_output(&output, 100, 0);
f.testwl.move_surface_to_output(surface, &output); f.testwl.move_surface_to_output(surface, &output);
f.testwl.move_pointer_to(surface, 150.0, 12.0); f.testwl.move_pointer_to(surface, 150.0, 12.0);
@ -1111,3 +1133,67 @@ fn close_window() {
// Connection should no longer work (KillClient) // Connection should no longer work (KillClient)
assert!(connection.poll_for_event().is_err()); 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<_>>(), 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);
}

View file

@ -167,6 +167,7 @@ struct DataSourceData {
} }
struct Output { struct Output {
name: String,
wl: WlOutput, wl: WlOutput,
xdg: Option<ZxdgOutputV1>, xdg: Option<ZxdgOutputV1>,
} }
@ -565,6 +566,13 @@ impl Server {
self.display.flush_clients().unwrap(); self.display.flush_clients().unwrap();
} }
pub fn get_output(&mut self, name: &str) -> Option<WlOutput> {
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) { pub fn move_output(&mut self, output: &WlOutput, x: i32, y: i32) {
output.geometry( output.geometry(
x, x,
@ -786,11 +794,14 @@ impl GlobalDispatch<WlOutput, (i32, i32)> for State {
"fake monitor".to_string(), "fake monitor".to_string(),
wl_output::Transform::Normal, 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.mode(wl_output::Mode::Current, 1000, 1000, 0);
output.done(); output.done();
state.outputs.insert( state.outputs.insert(
output.clone(), output.clone(),
Output { Output {
name,
wl: output.clone(), wl: output.clone(),
xdg: None, xdg: None,
}, },