Compare commits

..

No commits in common. "725720f1cb438ff916f4ec2ae9684e59b327d3a2" and "75c9f5e77573b36b8d341c28c0d32e9c9a0a2309" have entirely different histories.

13 changed files with 317 additions and 629 deletions

View file

@ -34,16 +34,13 @@ The code for the X11 portion of satellite lives in `src/xstate`. Satellite must
as any other standard X11 window manager. This includes:
- Setting SubstructureRedirect and SubstructureNotify on the root window, to get notifications for when new windows are being created
- Following (most of) the [ICCCM](https://www.x.org/releases/X11R7.6/doc/xorg-docs/specs/ICCCM/icccm.html) and [EWMH](https://specifications.freedesktop.org/wm-spec/latest/) specs
- Follwing (most of) the [ICCCM](https://www.x.org/releases/X11R7.6/doc/xorg-docs/specs/ICCCM/icccm.html) and [EWMH](https://specifications.freedesktop.org/wm-spec/latest/) specs
In addition, satellite must do some other things that a normal X11 window manager wouldn't - but a compositor integrating
Xwayland would - such as synchronize X11 and Wayland selections. This is explained further in the Wayland server section.
The way that satellite manages windows from the X11 point of view is as follows:
- All monitors maintain their relative positions to one another. Their absolute position is such that
the top-most monitor's top edge is on the X-axis and the left-most monitor's left edge is on the Y-axis.
- All monitors are on non-negative coordinates with no gaps between the screen and any monitor, matching what `xrandr` does.
- All toplevels on a monitor are positioned at 0x0 on that monitor. So if you have one monitor at 0x0,
all the windows are located at 0x0. If you have a monitor at 300x600, all the windows on that monitor are at 300x600.
- This offset is needed because all monitors rest in the same coordinate plane in X11, so missing this offset would

2
Cargo.lock generated
View file

@ -1241,7 +1241,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
[[package]]
name = "xwayland-satellite"
version = "0.8.1"
version = "0.8.0"
dependencies = [
"ab_glyph",
"anyhow",

View file

@ -17,7 +17,7 @@ edition = "2024"
[package]
name = "xwayland-satellite"
version = "0.8.1"
version = "0.8.0"
authors = ["Shawn Wallace"]
license = "MPL-2.0"
description = "xwayland-satellite grants rootless Xwayland integration to any Wayland compositor implementing xdg_wm_base and viewporter. This is particularly useful for compositors that (understandably) do not want to go through implementing support for rootless Xwayland themselves."

View file

@ -9,7 +9,7 @@ use server::selection::{Clipboard, Primary};
use smithay_client_toolkit::data_device_manager::WritePipe;
use std::io::{BufRead, BufReader, Read, Write};
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
use std::os::unix::{net::UnixStream, process::ExitStatusExt};
use std::os::unix::net::UnixStream;
use std::process::{Command, ExitStatus, Stdio};
use wayland_server::{Display, ListeningSocket};
use xcb::x;
@ -121,9 +121,9 @@ pub fn main(mut data: impl RunData) -> Option<()> {
let line = line.unwrap();
info!(target: "xwayland_process", "{line}");
}
let status = xwayland.wait().unwrap().into_raw();
// On a successful integration test, the rx will be dropped, so keep logs/GDB clean
let _ = finish_tx.write_all(&status.to_ne_bytes());
let status = Box::new(xwayland.wait().unwrap());
let status = Box::into_raw(status) as usize;
finish_tx.write_all(&status.to_ne_bytes()).unwrap();
});
let mut ready_fds = [
@ -131,10 +131,11 @@ pub fn main(mut data: impl RunData) -> Option<()> {
PollFd::new(&finish_rx, PollFlags::IN),
];
fn xwayland_exit_code(rx: &mut UnixStream) -> ExitStatus {
let mut data = [0; std::mem::size_of::<i32>()];
fn xwayland_exit_code(rx: &mut UnixStream) -> Box<ExitStatus> {
let mut data = [0; (usize::BITS / 8) as usize];
rx.read_exact(&mut data).unwrap();
ExitStatus::from_raw(i32::from_ne_bytes(data))
let data = usize::from_ne_bytes(data);
unsafe { Box::from_raw(data as *mut _) }
}
let connection = match poll(&mut ready_fds, None) {
@ -178,7 +179,7 @@ pub fn main(mut data: impl RunData) -> Option<()> {
Ok(_) => {
if !fds[3].revents().is_empty() {
let status = xwayland_exit_code(&mut quit_rx);
if status != ExitStatus::default() {
if *status != ExitStatus::default() {
error!("Xwayland exited early with {status}");
}
return None;
@ -245,7 +246,7 @@ pub fn main(mut data: impl RunData) -> Option<()> {
Ok(_) => {
if !fds[3].revents().is_empty() {
let status = xwayland_exit_code(&mut quit_rx);
if status != ExitStatus::default() {
if *status != ExitStatus::default() {
error!("Xwayland exited early with {status}");
}
return None;

View file

@ -1,6 +1,6 @@
use super::decoration::DecorationMarker;
use super::{GlobalName, ObjectEvent};
use super::ObjectEvent;
use hecs::{Entity, World};
use smithay_client_toolkit::{
activation::{ActivationHandler, RequestData, RequestDataExt},
@ -115,7 +115,6 @@ pub(super) struct MyWorld {
pub world: World,
pub global_list: GlobalList,
pub new_globals: Vec<Global>,
pub removed_globals: Vec<GlobalName>,
events: Vec<(Entity, ObjectEvent)>,
queued_events: Vec<mpsc::Receiver<(Entity, ObjectEvent)>>,
pub clipboard: SelectionEvents<SelectionOffer>,
@ -129,7 +128,6 @@ impl MyWorld {
world: World::new(),
global_list,
new_globals: Vec::new(),
removed_globals: Vec::new(),
events: Vec::new(),
queued_events: Vec::new(),
clipboard: Default::default(),
@ -206,23 +204,18 @@ impl Dispatch<WlRegistry, GlobalListContents> for MyWorld {
_: &wayland_client::Connection,
_: &wayland_client::QueueHandle<Self>,
) {
match event {
Event::<WlRegistry>::Global {
if let Event::<WlRegistry>::Global {
name,
interface,
version,
} => {
} = event
{
state.new_globals.push(Global {
name,
interface,
version,
});
}
Event::<WlRegistry>::GlobalRemove { name } => {
state.removed_globals.push(GlobalName(name));
}
_ => {}
}
};
}
}

View file

@ -173,7 +173,7 @@ impl DecorationsDataSatellite {
// Draw the bar and its components
let mut bar = Pixmap::new(drawn_width as u32, drawn_height as u32).unwrap();
bar.fill(Color::from_rgba(0.2, 0.2, 0.2, 1.0).unwrap());
bar.fill(Color::WHITE);
if let Some(title) = title {
bar.draw_pixmap(
@ -350,9 +350,9 @@ fn draw_pixmap_to_buffer(pixmap: &Pixmap, buffer: &mut [u8]) {
fn x_pixmap(bar_height: u32, scale: f32, hovered: bool) -> Pixmap {
let mut x = Pixmap::new(bar_height, bar_height).unwrap();
if hovered {
x.fill(Color::from_rgba(1.0, 0.2, 0.2, 0.8).unwrap());
x.fill(Color::from_rgba(1.0, 0.0, 0.0, 0.8).unwrap());
} else {
x.fill(Color::from_rgba(0.2, 0.2, 0.2, 1.0).unwrap());
x.fill(Color::WHITE);
}
let size = x.width() as f32;
let margin = 8.4 * scale;
@ -363,11 +363,9 @@ fn x_pixmap(bar_height: u32, scale: f32, hovered: bool) -> Pixmap {
line.move_to(size - margin, margin);
line.line_to(margin, size - margin);
let line = line.finish().unwrap();
let mut paint = Paint::default();
paint.set_color(Color::WHITE);
x.stroke_path(
&line,
&paint,
&Default::default(),
&Stroke {
width: scale + 0.5,
..Default::default()
@ -402,7 +400,7 @@ fn title_pixmap(title: &str, max_width: u32, height: u32, scale: f32) -> Option<
((bounds.min.x as u32 + x) + (bounds.min.y as u32 + y) * width) as usize;
data[pixel_idx] =
ColorU8::from_rgba(255, 255, 255, (coverage * 255.0) as u8).premultiply();
ColorU8::from_rgba(0, 0, 0, (coverage * 255.0) as u8).premultiply();
});
}
}

View file

@ -1479,7 +1479,6 @@ impl<S: X11Selection> GlobalDispatch<WlOutput, Global> for InnerServerState<S> {
client,
event::OutputScaleFactor::Output(1),
event::OutputDimensions::default(),
GlobalName(data.name),
),
);
state.updated_outputs.push(entity);

View file

@ -212,9 +212,7 @@ impl SurfaceEvents {
let mut query = data.query::<(&x::Window, &mut WindowData)>();
if let Some((window, win_data)) = query.get() {
let Some(dimensions) = output_data.get::<&OutputDimensions>() else {
return;
};
let dimensions = output_data.get::<&OutputDimensions>().unwrap();
win_data.update_output_offset(
*window,
WindowOutputOffset {
@ -474,8 +472,8 @@ pub(super) fn update_surface_viewport(
let dims = &window_data.attrs.dims;
let size_hints = &window_data.attrs.size_hints;
let width = (dims.width as f64 / scale_factor.0).ceil() as i32;
let height = (dims.height as f64 / scale_factor.0).ceil() as i32;
let width = (dims.width as f64 / scale_factor.0) as i32;
let height = (dims.height as f64 / scale_factor.0) as i32;
if width > 0 && height > 0 {
viewport.set_destination(width, height);
}
@ -985,7 +983,7 @@ impl Event for client::wl_touch::Event {
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
#[derive(Copy, Clone)]
pub(super) struct OnOutput(pub Entity);
struct OutputName(String);
fn get_output_name(output: Option<&OnOutput>, world: &World) -> Option<String> {
@ -1079,9 +1077,7 @@ fn update_output_offset(
let connection = &mut state.connection;
let state = &mut state.inner;
{
let Ok(mut dimensions) = state.world.get::<&mut OutputDimensions>(output) else {
return;
};
let mut dimensions = state.world.get::<&mut OutputDimensions>(output).unwrap();
if matches!(source, OutputDimensionsSource::Wl { .. })
&& matches!(dimensions.source, OutputDimensionsSource::Xdg)
{
@ -1097,8 +1093,7 @@ fn update_output_offset(
};
state.global_offset_updated = true;
} else if dim.owner == Some(output) && value > dim.value {
// Another output's position could be less than the new value, so recalculate
dim.owner = None;
*dim = Default::default();
state.global_offset_updated = true;
}
};
@ -1129,9 +1124,7 @@ fn update_window_output_offsets(
world: &World,
connection: &mut impl XConnection,
) {
let Ok(dimensions) = world.get::<&OutputDimensions>(output) else {
return;
};
let dimensions = world.get::<&OutputDimensions>(output).unwrap();
let mut query = world.query::<(&x::Window, &mut WindowData, &OnOutput)>();
for (_, (window, data, _)) in query
@ -1157,9 +1150,7 @@ pub(super) fn update_global_output_offset(
) {
let entity = world.entity(output).unwrap();
let mut query = entity.query::<(&OutputDimensions, &WlOutput)>();
let Some((dimensions, server)) = query.get() else {
return;
};
let (dimensions, server) = query.get().unwrap();
let x = dimensions.x - global_output_offset.x.value;
let y = dimensions.y - global_output_offset.y.value;
@ -1258,17 +1249,14 @@ impl OutputEvent {
state,
);
let global_output_offset = state.global_output_offset;
let global_offset_updated = state.global_offset_updated;
let Ok((output, dimensions, xdg)) = state.world.query_one_mut::<(
&WlOutput,
&mut OutputDimensions,
Option<&XdgOutputServer>,
)>(target) else {
return;
};
let (output, dimensions, xdg) = state
.world
.query_one_mut::<(&WlOutput, &mut OutputDimensions, Option<&XdgOutputServer>)>(
target,
)
.unwrap();
if !global_offset_updated {
output.geometry(
x - global_output_offset.x.value,
y - global_output_offset.y.value,
@ -1279,7 +1267,6 @@ impl OutputEvent {
model,
convert_wenum(transform),
);
}
dimensions.rotated_90 = transform.into_result().is_ok_and(|t| {
matches!(
t,
@ -1303,12 +1290,10 @@ impl OutputEvent {
height,
refresh,
} => {
let Ok((output, dimensions)) = state
let (output, dimensions) = state
.world
.query_one_mut::<(&WlOutput, &mut OutputDimensions)>(target)
else {
return;
};
.unwrap();
if flags
.into_result()
@ -1363,7 +1348,6 @@ impl OutputEvent {
match event {
Event::LogicalPosition { x, y } => {
update_output_offset(target, OutputDimensionsSource::Xdg, x, y, state);
if !state.global_offset_updated {
state
.world
.get::<&XdgOutputServer>(target)
@ -1373,14 +1357,11 @@ impl OutputEvent {
y - state.global_output_offset.y.value,
);
}
}
Event::LogicalSize { .. } => {
let Ok((xdg, dimensions)) = state
let (xdg, dimensions) = state
.world
.query_one_mut::<(&XdgOutputServer, &OutputDimensions)>(target)
else {
return;
};
.unwrap();
if dimensions.rotated_90 {
xdg.logical_size(dimensions.height, dimensions.width);
} else {

View file

@ -65,7 +65,6 @@ use wayland_protocols::{
use wayland_server::protocol::wl_seat::WlSeat;
use wayland_server::{
Client, DisplayHandle, Resource, WEnum,
backend::GlobalId,
protocol::{
wl_callback::WlCallback, wl_compositor::WlCompositor, wl_output::WlOutput, wl_shm::WlShm,
wl_surface::WlSurface,
@ -143,7 +142,7 @@ impl WindowData {
offset: WindowOutputOffset,
connection: &mut C,
) {
log::trace!(target: "output_offset", "offset: {offset:?}");
log::trace!("offset: {offset:?}");
if offset == self.output_offset {
return;
}
@ -162,7 +161,7 @@ impl WindowData {
height: self.attrs.dims.height as _,
},
) {
debug!(target: "output_offset", "set {:?} offset to {:?}", window, self.output_offset);
debug!("set {:?} offset to {:?}", window, self.output_offset);
}
}
}
@ -343,8 +342,7 @@ enum ObjectEvent {
}
}
fn handle_new_globals<'a, S: X11Selection + 'static>(
globals_map: &mut HashMap<GlobalName, (Global, GlobalId)>,
fn handle_globals<'a, S: X11Selection + 'static>(
dh: &DisplayHandle,
globals: impl IntoIterator<Item = &'a Global>,
) {
@ -355,8 +353,7 @@ fn handle_new_globals<'a, S: X11Selection + 'static>(
$(
ref x if x == <$global>::interface().name => {
let version = u32::min(global.version, <$global>::interface().version);
let global_id = dh.create_global::<InnerServerState<S>, $global, Global>(version, global.clone());
globals_map.insert(GlobalName(global.name), (global.clone(), global_id));
dh.create_global::<InnerServerState<S>, $global, Global>(version, global.clone());
}
)+
_ => {}
@ -380,9 +377,6 @@ fn handle_new_globals<'a, S: X11Selection + 'static>(
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub(super) struct GlobalName(pub u32);
struct FocusData {
window: x::Window,
output_name: Option<String>,
@ -452,7 +446,6 @@ pub struct InnerServerState<S: X11Selection> {
world: MyWorld,
queue: EventQueue<MyWorld>,
qh: QueueHandle<MyWorld>,
globals_map: HashMap<GlobalName, (Global, GlobalId)>,
client: Client,
to_focus: Option<FocusData>,
unfocus: bool,
@ -539,10 +532,9 @@ impl<S: X11Selection> ServerState<NoConnection<S>> {
dh.create_global::<InnerServerState<S>, XwaylandShellV1, _>(1, ());
let mut globals_map = HashMap::new();
global_list
.contents()
.with_list(|globals| handle_new_globals::<S>(&mut globals_map, &dh, globals));
.with_list(|globals| handle_globals::<S>(&dh, globals));
let world = MyWorld::new(global_list);
let client = dh.insert_client(client, std::sync::Arc::new(())).unwrap();
@ -553,7 +545,6 @@ impl<S: X11Selection> ServerState<NoConnection<S>> {
client,
queue,
qh,
globals_map,
dh,
to_focus: None,
unfocus: false,
@ -622,7 +613,7 @@ impl<C: XConnection> ServerState<C> {
}
pub fn handle_clientside_events(&mut self) {
self.handle_globals();
self.handle_new_globals();
for (target, event) in self.world.read_events() {
if !self.world.contains(target) {
@ -644,15 +635,14 @@ impl<C: XConnection> ServerState<C> {
.unwrap();
}
if self.global_offset_updated {
if self.global_output_offset.x.owner.is_none()
|| self.global_output_offset.y.owner.is_none()
{
self.calc_global_output_offset();
self.global_offset_updated = true;
}
if self.global_offset_updated {
debug!(
target: "output_offset",
"updated global output offset: {}x{}",
self.global_output_offset.x.value, self.global_output_offset.y.value
);
@ -669,10 +659,8 @@ impl<C: XConnection> ServerState<C> {
}
if !self.updated_outputs.is_empty() {
for output in std::mem::take(&mut self.updated_outputs).iter() {
let Ok(output_scale) = self.world.get::<&OutputScaleFactor>(*output) else {
continue;
};
for output in self.updated_outputs.iter() {
let output_scale = self.world.get::<&OutputScaleFactor>(*output).unwrap();
if matches!(*output_scale, OutputScaleFactor::Output(..)) {
let mut surface_query = self
.world
@ -696,12 +684,14 @@ impl<C: XConnection> ServerState<C> {
}
}
}
self.updated_outputs.clear();
let mut mixed_scale = false;
let mut scale;
let mut outputs = self.world.query_mut::<&OutputScaleFactor>().into_iter();
if let Some((_, output_scale)) = outputs.next() {
let (_, output_scale) = outputs.next().unwrap();
scale = output_scale.get();
for (_, output_scale) in outputs {
@ -720,7 +710,6 @@ impl<C: XConnection> ServerState<C> {
debug!("Using new scale {scale}");
self.new_scale = Some(scale);
}
}
{
if let Some(FocusData {
@ -791,55 +780,9 @@ impl<S: X11Selection + 'static> InnerServerState<S> {
self.queue.as_fd()
}
fn handle_globals(&mut self) {
fn handle_new_globals(&mut self) {
let globals = std::mem::take(&mut self.world.new_globals);
handle_new_globals::<S>(&mut self.globals_map, &self.dh, &globals);
let globals = std::mem::take(&mut self.world.removed_globals);
for global in globals {
let (global_struct, global_id) = self.globals_map.remove(&global).unwrap();
self.dh.disable_global::<InnerServerState<S>>(global_id);
if global_struct.interface == <WlOutput>::interface().name {
self.remove_output(global);
}
}
}
fn remove_output(&mut self, global: GlobalName) {
let query = self
.world
.query_mut::<(&WlOutput, &GlobalName)>()
.into_iter()
.map(|(e, (_, name))| (e, *name))
.collect::<Vec<_>>();
for (entity, name) in query.iter() {
if *name == global {
self.updated_outputs.push(*entity);
self.world
.remove::<(OutputScaleFactor, OutputDimensions)>(*entity)
.unwrap();
let query = self
.world
.query_mut::<&OnOutput>()
.into_iter()
.map(|(e, on_out)| (e, *on_out))
.collect::<Vec<_>>();
for (e, on_out) in query.iter() {
if *on_out == OnOutput(*entity) {
self.world.remove_one::<OnOutput>(*e).unwrap();
}
}
if self.global_output_offset.x.owner == Some(*entity) {
self.global_offset_updated = true;
self.global_output_offset.x.owner = None;
}
if self.global_output_offset.y.owner == Some(*entity) {
self.global_offset_updated = true;
self.global_output_offset.y.owner = None;
}
break;
}
}
handle_globals::<S>(&self.dh, globals.iter());
}
pub fn new_window(
@ -1334,8 +1277,6 @@ impl<S: X11Selection + 'static> InnerServerState<S> {
}
fn calc_global_output_offset(&mut self) {
self.global_output_offset.x.value = i32::MAX;
self.global_output_offset.y.value = i32::MAX;
for (entity, dimensions) in self.world.query_mut::<&OutputDimensions>() {
if dimensions.x < self.global_output_offset.x.value {
self.global_output_offset.x = GlobalOutputOffsetDimension {

View file

@ -530,19 +530,7 @@ impl<C: XConnection> TestFixture<C> {
);
self.run();
self.run();
(output, self.testwl.finalize_output())
}
fn remove_output(&mut self, output_s: wayland_server::protocol::wl_output::WlOutput) {
self.testwl.remove_output(output_s);
self.run();
self.run();
let mut events = std::mem::take(&mut *self.registry.data.events.lock().unwrap());
assert_eq!(events.len(), 1);
let event = events.pop().unwrap();
let Ev::<WlRegistry>::GlobalRemove { .. } = event else {
panic!("Unexpected event: {event:?}");
};
(output, self.testwl.last_created_output())
}
}
@ -864,6 +852,13 @@ impl TestFixture<FakeXConnection> {
let data = self.testwl.get_surface_data(surface_id).unwrap();
match data.role {
Some(SurfaceRole::Popup(_)) => {
assert_eq!(
data.popup().positioner_state.offset,
testwl::Vec2 {
x: dims.x as _,
y: dims.y as _
}
);
assert_eq!(
data.popup().positioner_state.size,
Some(testwl::Vec2 {
@ -1613,209 +1608,56 @@ fn override_redirect_choose_hover_window() {
assert_eq!(&popup_data.popup().parent, win1_xdg);
}
#[track_caller]
fn check_output_position_event(output: &TestObject<WlOutput>, pos: (i32, i32)) {
let mut geo = None;
let events = std::mem::take(&mut *output.data.events.lock().unwrap());
log::debug!("events: {events:?}");
for event in events {
match event {
wl_output::Event::Geometry { x, y, .. } => {
geo = Some((x, y));
}
wl_output::Event::Done => {
if let Some(geo) = geo {
assert_eq!(geo, pos);
return;
}
}
_ => {}
}
}
if geo.is_none() {
panic!("Did not receive any geometry events");
} else {
panic!("Did not receive a done event");
}
}
#[track_caller]
fn check_output_position_event_xdg(
xdg_out: &TestObject<ZxdgOutputV1>,
out: &TestObject<WlOutput>,
pos: (i32, i32),
goo_updated: bool,
) {
let mut done = false;
let events = std::mem::take(&mut *xdg_out.data.events.lock().unwrap())
.into_iter()
.rev();
for event in events {
if let zxdg_output_v1::Event::LogicalPosition { x, y } = event {
assert_eq!(pos, (x, y));
done = true;
break;
}
}
assert!(done, "Did not get zxdg_output_v1 logical_position");
let events = std::mem::take(&mut *out.data.events.lock().unwrap());
assert_eq!(
events
.into_iter()
.filter(|e| matches!(*e, wl_output::Event::Done))
.count(),
goo_updated as usize,
"Did not get expected wl_output done event"
);
}
#[test]
fn output_offset_one_output() {
// If there is only one output, that output is always positioned at 0x0
let (mut f, _) = TestFixture::new_with_compositor();
let (output_obj, output) = f.new_output(0, 0);
f.run();
f.run();
check_output_position_event(&output_obj, (0, 0));
f.testwl.move_output(&output, 500, 100);
f.run();
f.run();
check_output_position_event(&output_obj, (0, 0));
f.testwl.move_output(&output, -500, -100);
f.run();
f.run();
check_output_position_event(&output_obj, (0, 0));
}
#[test]
fn output_offset_multi_output() {
// With multiple outputs, the top-most output is on the X-axis, the left-most output is on the
// Y-axis, and they always maintain relative positioning.
let (mut f, _) = TestFixture::new_with_compositor();
let (output_obj_1, output_1) = f.new_output(1000, 0);
f.run();
check_output_position_event(&output_obj_1, (0, 0));
let (output_obj_2, _) = f.new_output(0, 1000);
f.run();
check_output_position_event(&output_obj_1, (1000, 0));
check_output_position_event(&output_obj_2, (0, 1000));
f.testwl.move_output(&output_1, 1000, 2000);
f.run();
f.run();
check_output_position_event(&output_obj_1, (1000, 1000));
check_output_position_event(&output_obj_2, (0, 0));
// Global output offset does not change
f.testwl.move_output(&output_1, 1000, 1000);
f.run();
f.run();
check_output_position_event(&output_obj_1, (1000, 0));
assert!(&output_obj_2.data.events.lock().unwrap().is_empty());
}
#[test]
fn output_offset_multi_output_xdg() {
let (mut f, _) = TestFixture::new_with_compositor();
let man = f.enable_xdg_output();
let (output_obj_1, output_1) = f.new_output(0, 0);
f.run();
std::mem::take(&mut *output_obj_1.data.events.lock().unwrap());
let output_xdg_1 = f.create_xdg_output(&man, output_obj_1.obj.clone());
f.testwl.move_xdg_output(&output_1, 1000, 0);
f.run();
f.run();
check_output_position_event_xdg(&output_xdg_1, &output_obj_1, (0, 0), true);
let (output_obj_2, output_2) = f.new_output(1000, 1000);
f.run();
std::mem::take(&mut *output_obj_2.data.events.lock().unwrap());
let output_xdg_2 = f.create_xdg_output(&man, output_obj_2.obj.clone());
f.testwl.move_xdg_output(&output_2, 0, 1000);
f.run();
f.run();
check_output_position_event_xdg(&output_xdg_1, &output_obj_1, (1000, 0), true);
check_output_position_event_xdg(&output_xdg_2, &output_obj_2, (0, 1000), true);
f.testwl.move_xdg_output(&output_1, 1000, 2000);
f.run();
f.run();
check_output_position_event_xdg(&output_xdg_1, &output_obj_1, (1000, 1000), true);
check_output_position_event_xdg(&output_xdg_2, &output_obj_2, (0, 0), true);
f.testwl.move_xdg_output(&output_1, 1000, 1000);
f.run();
f.run();
check_output_position_event_xdg(&output_xdg_1, &output_obj_1, (1000, 0), false);
assert!(output_xdg_2.data.events.lock().unwrap().is_empty());
assert!(output_obj_2.data.events.lock().unwrap().is_empty());
}
#[test]
fn output_offset_remove_output() {
let (mut f, _) = TestFixture::new_with_compositor();
let (output_ext_c, output_ext) = f.new_output(0, 0);
let (output_main_c, _) = f.new_output(1000, 500);
f.run();
check_output_position_event(&output_ext_c, (0, 0));
check_output_position_event(&output_main_c, (1000, 500));
f.remove_output(output_ext);
f.run();
f.run();
check_output_position_event(&output_main_c, (0, 0));
}
#[test]
fn remove_all_outputs() {
let (mut f, _) = TestFixture::new_with_compositor();
let (_, output) = f.new_output(0, 0);
f.run();
f.remove_output(output);
f.run();
}
#[test]
fn output_offset_surface_positioning() {
fn output_offset() {
let (mut f, comp) = TestFixture::new_with_compositor();
f.new_output(0, 0);
let (_, output) = f.new_output(500, 100);
let (output_obj, output) = f.new_output(0, 0);
let man = f.enable_xdg_output();
f.create_xdg_output(&man, output_obj.obj);
f.testwl.move_xdg_output(&output, 500, 100);
f.run();
let window = Window::new(1);
let (_, toplevel_id) = f.create_toplevel(&comp, window);
f.testwl.move_surface_to_output(toplevel_id, &output);
{
let (surface, surface_id) = f.create_toplevel(&comp, window);
f.testwl.move_surface_to_output(surface_id, &output);
f.run();
let data = &f.connection().windows[&window];
assert_eq!(data.dims.x, 500);
assert_eq!(data.dims.y, 100);
f.satellite.unmap_window(window);
surface.obj.destroy();
f.run();
}
let (t_buffer, t_surface) = comp.create_surface();
f.map_window(&comp, window, &t_surface.obj, &t_buffer);
f.run();
let t_id = f.testwl.last_created_surface_id().unwrap();
f.testwl.move_surface_to_output(t_id, &output);
f.run();
{
let data = f.testwl.get_surface_data(t_id).unwrap();
assert!(
matches!(data.role, Some(testwl::SurfaceRole::Toplevel(_))),
"surface role: {:?}",
data.role
);
}
f.testwl.configure_toplevel(t_id, 100, 100, vec![]);
f.testwl.focus_toplevel(t_id);
f.run();
let mut toplevel_pos = WindowDims {
x: 500,
y: 100,
width: 100,
height: 100,
};
f.assert_window_dimensions(window, toplevel_id, toplevel_pos);
{
let data = &f.connection().windows[&window];
assert_eq!(data.dims.x, 500);
assert_eq!(data.dims.y, 100);
}
let popup = Window::new(2);
let (_, p_id) = f.create_popup(
&comp,
PopupBuilder::new(popup, window, toplevel_id).x(510).y(110),
);
let mut popup_dims = WindowDims {
x: 510,
y: 110,
width: 50,
height: 50,
};
let (p_surface, p_id) =
f.create_popup(&comp, PopupBuilder::new(popup, window, t_id).x(510).y(110));
f.testwl.move_surface_to_output(p_id, &output);
f.run();
let data = f.testwl.get_surface_data(p_id).unwrap();
@ -1823,46 +1665,28 @@ fn output_offset_surface_positioning() {
data.popup().positioner_state.offset,
testwl::Vec2 { x: 10, y: 10 }
);
f.assert_window_dimensions(popup, p_id, popup_dims);
f.testwl.move_output(&output, 600, 200);
f.run();
f.satellite.unmap_window(popup);
p_surface.obj.destroy();
f.run();
toplevel_pos.x = 600;
toplevel_pos.y = 200;
f.assert_window_dimensions(window, toplevel_id, toplevel_pos);
let (buffer, surface) = comp.create_surface();
f.map_window(&comp, popup, &surface.obj, &buffer);
f.run();
let p_id = f.testwl.last_created_surface_id().unwrap();
f.testwl.move_surface_to_output(p_id, &output);
f.testwl.configure_popup(p_id);
f.run();
let data = f.testwl.get_surface_data(p_id).unwrap();
assert_eq!(
data.popup().positioner_state.offset,
testwl::Vec2 { x: 10, y: 10 }
);
popup_dims.x = 610;
popup_dims.y = 210;
f.assert_window_dimensions(popup, p_id, popup_dims);
f.testwl.move_output(&output, -100, -200);
f.run();
f.run();
toplevel_pos.x = 0;
toplevel_pos.y = 0;
f.assert_window_dimensions(window, toplevel_id, toplevel_pos);
let data = f.testwl.get_surface_data(p_id).unwrap();
assert_eq!(
data.popup().positioner_state.offset,
testwl::Vec2 { x: 10, y: 10 }
);
popup_dims.x = 10;
popup_dims.y = 10;
f.assert_window_dimensions(popup, p_id, popup_dims);
}
#[test]
fn output_offset_xdg_override() {
fn output_offset_change() {
let (mut f, comp) = TestFixture::new_with_compositor();
f.new_output(0, 0);
f.run();
let (output_obj, output) = f.new_output(500, 100);
let window = Window::new(1);
@ -1877,8 +1701,13 @@ fn output_offset_xdg_override() {
};
test_position(&f, 500, 100);
f.testwl.move_output(&output, 600, 200);
f.run();
f.run();
test_position(&f, 600, 200);
let man = f.enable_xdg_output();
f.create_xdg_output(&man, output_obj.obj.clone());
f.create_xdg_output(&man, output_obj.obj);
// testwl inits xdg output position to 0, and it should take priority over wl_output position
test_position(&f, 0, 0);
@ -1892,82 +1721,6 @@ fn output_offset_xdg_override() {
test_position(&f, 1000, 22);
}
#[test]
fn output_offset_negative_position() {
let mut f = TestFixture::new();
std::mem::take(&mut *f.registry.data.events.lock().unwrap());
let (output, _) = f.new_output(-500, -500);
f.run();
f.run();
check_output_position_event(&output, (0, 0));
let (output2, _) = f.new_output(0, 0);
f.run();
f.run();
check_output_position_event(&output2, (500, 500));
assert!(output.data.events.lock().unwrap().is_empty());
let (output3, _) = f.new_output(500, 500);
f.run();
f.run();
check_output_position_event(&output3, (1000, 1000));
assert!(output.data.events.lock().unwrap().is_empty());
assert!(output2.data.events.lock().unwrap().is_empty());
}
#[test]
fn output_offset_negative_position_update() {
let mut f = TestFixture::new();
std::mem::take(&mut *f.registry.data.events.lock().unwrap());
let (output, _) = f.new_output(-500, -500);
f.run();
f.run();
check_output_position_event(&output, (0, 0));
let (output2, _) = f.new_output(0, -1000);
f.run();
f.run();
check_output_position_event(&output, (0, 500));
check_output_position_event(&output2, (500, 0));
let (output3, o3) = f.new_output(-1000, 0);
f.run();
f.run();
check_output_position_event(&output, (500, 500));
check_output_position_event(&output2, (1000, 0));
check_output_position_event(&output3, (0, 1000));
f.testwl.move_output(&o3, 0, 0);
f.run();
f.run();
check_output_position_event(&output, (0, 500));
check_output_position_event(&output2, (500, 0));
check_output_position_event(&output3, (500, 1000));
}
#[test]
fn output_offset_negative_position_update_xdg() {
let mut f = TestFixture::new();
std::mem::take(&mut *f.registry.data.events.lock().unwrap());
let xdg = f.enable_xdg_output();
let (output, _) = f.new_output(-500, -500);
f.run();
f.run();
check_output_position_event(&output, (0, 0));
let (output2, output_s) = f.new_output(0, 0);
f.run();
std::mem::take(&mut *output2.data.events.lock().unwrap());
let xdg_output = f.create_xdg_output(&xdg, output2.obj.clone());
f.testwl.move_xdg_output(&output_s, 0, -1000);
f.run();
f.run();
check_output_position_event(&output, (0, 500));
check_output_position_event_xdg(&xdg_output, &output2, (500, 0), true);
}
#[test]
fn reconfigure_popup() {
let (mut f, comp) = TestFixture::new_with_compositor();
@ -2272,6 +2025,140 @@ fn fullscreen_heuristic() {
check_fullscreen(3, true);
}
#[track_caller]
fn check_output_position_event(output: &TestObject<WlOutput>, x: i32, y: i32) {
let events = std::mem::take(&mut *output.data.events.lock().unwrap());
assert!(!events.is_empty());
let mut done = false;
let mut geo = false;
for event in events {
match event {
wl_output::Event::Geometry {
x: geo_x, y: geo_y, ..
} => {
assert_eq!(geo_x, x);
assert_eq!(geo_y, y);
geo = true;
}
wl_output::Event::Done => {
done = true;
}
_ => {}
}
}
assert!(geo, "Didn't get geometry event");
assert!(done, "Didn't get done event");
}
#[test]
fn negative_output_position() {
let mut f = TestFixture::new();
std::mem::take(&mut *f.registry.data.events.lock().unwrap());
let (output, _) = f.new_output(-500, -500);
f.run();
f.run();
check_output_position_event(&output, 0, 0);
let (output2, _) = f.new_output(0, 0);
f.run();
f.run();
check_output_position_event(&output2, 500, 500);
assert!(output.data.events.lock().unwrap().is_empty());
let (output3, _) = f.new_output(500, 500);
f.run();
f.run();
check_output_position_event(&output3, 1000, 1000);
assert!(output.data.events.lock().unwrap().is_empty());
assert!(output2.data.events.lock().unwrap().is_empty());
}
#[test]
fn negative_output_position_update_offset() {
let mut f = TestFixture::new();
std::mem::take(&mut *f.registry.data.events.lock().unwrap());
let (output, _) = f.new_output(-500, -500);
f.run();
f.run();
check_output_position_event(&output, 0, 0);
let (output2, _) = f.new_output(0, -1000);
f.run();
f.run();
check_output_position_event(&output, 0, 500);
check_output_position_event(&output2, 500, 0);
let (output3, _) = f.new_output(-1000, 0);
f.run();
f.run();
check_output_position_event(&output, 500, 500);
check_output_position_event(&output2, 1000, 0);
check_output_position_event(&output3, 0, 1000);
}
#[test]
fn negative_output_xdg_position_update_offset() {
let mut f = TestFixture::new();
std::mem::take(&mut *f.registry.data.events.lock().unwrap());
let xdg = f.enable_xdg_output();
let (output, _) = f.new_output(-500, -500);
f.run();
f.run();
check_output_position_event(&output, 0, 0);
let (output2, output_s) = f.new_output(0, 0);
let xdg_output = f.create_xdg_output(&xdg, output2.obj);
f.testwl.move_xdg_output(&output_s, 0, -1000);
f.run();
f.run();
check_output_position_event(&output, 0, 500);
let mut found = false;
let mut first = false;
for event in std::mem::take(&mut *xdg_output.data.events.lock().unwrap()) {
if let zxdg_output_v1::Event::LogicalPosition { x, y } = event {
// Testwl sends a logical position event when the output is first created
// We are interested in the second one generated by satellite
if !first {
first = true;
continue;
}
assert_eq!(x, 500);
assert_eq!(y, 0);
found = true;
break;
}
}
assert!(found, "Did not get xdg output logical position");
found = false;
for event in std::mem::take(&mut *output2.data.events.lock().unwrap()) {
if let wl_output::Event::Done = event {
found = true;
break;
}
}
assert!(found, "Did not get done event");
}
#[test]
fn negative_output_position_remove_offset() {
let mut f = TestFixture::new();
std::mem::take(&mut *f.registry.data.events.lock().unwrap());
let (c_output, s_output) = f.new_output(-500, -500);
f.run();
f.run();
check_output_position_event(&c_output, 0, 0);
f.testwl.move_output(&s_output, 500, 500);
f.run();
f.run();
check_output_position_event(&c_output, 500, 500);
}
#[test]
fn scaled_output_popup() {
let (mut f, comp) = TestFixture::new_with_compositor();
@ -2401,8 +2288,8 @@ fn fractional_scale_small_popup() {
{
let data = f.testwl.get_surface_data(toplevel_id).unwrap();
let viewport = data.viewport.as_ref().expect("Missing viewport");
assert_eq!(viewport.width, 67);
assert_eq!(viewport.height, 67);
assert_eq!(viewport.width, 66);
assert_eq!(viewport.height, 66);
}
let popup = Window::new(2);
@ -2838,63 +2725,6 @@ fn scaled_pointer_lock_position_hint() {
);
}
#[test]
fn disconnected_output_rescaling() {
let mut f = TestFixture::new_pre_connect(|testwl| {
testwl.enable_fractional_scale();
});
let comp = f.compositor();
let (_, output_main) = f.new_output(0, 0);
let (_, output_ext) = f.new_output(1000, 0);
let window = Window::new(1);
let (_, id) = f.create_toplevel(&comp, window);
let surface_data = f.testwl.get_surface_data(id).expect("No surface data");
let fractional = surface_data
.fractional
.as_ref()
.expect("No fractional scale for surface");
fractional.preferred_scale(240); // 2.0 scale
f.testwl.move_surface_to_output(id, &output_main);
f.run();
let surface_data = f.testwl.get_surface_data(id).expect("No surface data");
let fractional = surface_data
.fractional
.as_ref()
.expect("No fractional scale for surface");
fractional.preferred_scale(180); // 1.5 scale
f.testwl.move_surface_to_output(id, &output_ext);
f.run();
// Multiple monitors with different scaling will select the lowest scale across monitors
assert_eq!(f.satellite.inner.new_scale, Some(1.5));
f.remove_output(output_ext);
let surface_data = f.testwl.get_surface_data(id).expect("No surface data");
let fractional = surface_data
.fractional
.as_ref()
.expect("No fractional scale for surface");
fractional.preferred_scale(120); // 1.0 scale
f.run();
f.run();
// An fractional scale change done while the surface is on a removed output is ignored
assert_eq!(f.satellite.inner.new_scale, Some(1.5));
f.testwl.move_surface_to_output(id, &output_main);
let surface_data = f.testwl.get_surface_data(id).expect("No surface data");
let fractional = surface_data
.fractional
.as_ref()
.expect("No fractional scale for surface");
fractional.preferred_scale(240); // 2.0 scale
f.run();
f.run();
// After the output is disconnected, only the 2x scale output remains, so use that scale
assert_eq!(f.satellite.inner.new_scale, Some(2.0));
}
#[test]
fn client_side_decorations() {
let (mut f, compositor) = TestFixture::new_with_compositor();

View file

@ -333,14 +333,6 @@ impl XState {
time: x::CURRENT_TIME,
})
.unwrap();
self.connection
.send_and_check_request(&x::SetSelectionOwner {
owner: self.wm_window,
selection: self.atoms.net_wm_cm_s0,
time: x::CURRENT_TIME,
})
.unwrap();
}
pub fn handle_events(&mut self, server_state: &mut super::RealServerState) {
@ -731,12 +723,9 @@ impl XState {
wmhint_popup = motif_popup
&& wm_hints.is_some_and(|h| !h.acquire_input_via_wm)
&& !hints.functions.as_ref().is_some_and(|f| {
f.intersects(
motif::Functions::Minimize
| motif::Functions::Maximize
| motif::Functions::Resize
| motif::Functions::All,
)
f.contains(motif::Functions::Minimize)
|| f.contains(motif::Functions::Maximize)
|| f.contains(motif::Functions::All)
});
// If the motif hints indicate the user shouldn't be able to do anything
// to the window at all, it stands to reason it's probably a popup.
@ -1042,7 +1031,6 @@ xcb::atoms_struct! {
wm_transient_for => b"WM_TRANSIENT_FOR" only_if_exists = false,
wm_state => b"WM_STATE" only_if_exists = false,
wm_s0 => b"WM_S0" only_if_exists = false,
net_wm_cm_s0 => b"_NET_WM_CM_S0" only_if_exists = false,
wm_check => b"_NET_SUPPORTING_WM_CHECK" only_if_exists = false,
net_wm_name => b"_NET_WM_NAME" only_if_exists = false,
wm_pid => b"_NET_WM_PID" only_if_exists = false,

View file

@ -102,11 +102,12 @@ impl Drop for Fixture {
let thread = unsafe { ManuallyDrop::take(&mut self.thread) };
// Sending anything to the quit receiver to stop the main loop. Then we guarantee a main
// thread does not use file descriptors which outlive the Fixture's BorrowedFd
let return_ptr = Box::into_raw(Box::new(0_usize)) as usize;
// If the receiver end of the pipe closed, the main thread dropped it, which means that
// thread already terminated
if self
.quit_tx
.write_all(&0_i32.to_ne_bytes())
.write_all(&return_ptr.to_ne_bytes())
.is_err_and(|e| e.kind() != std::io::ErrorKind::BrokenPipe)
{
panic!("could not message the main thread to terminate");
@ -318,7 +319,7 @@ impl Fixture {
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.finalize_output()
self.testwl.last_created_output()
}
}
@ -2173,27 +2174,6 @@ fn popup_heuristics() {
&[0x1_u32, 0, 0, 0, 0, 0, 0, 0, 0],
);
f.map_as_toplevel(&mut connection, battle_net);
let wallpaper_engine = connection.new_window(connection.root, 10, 10, 50, 50, false);
connection.set_property(
wallpaper_engine,
x::ATOM_ATOM,
connection.atoms.win_type,
&[connection.atoms.win_type_normal],
);
connection.set_property(
wallpaper_engine,
connection.atoms.motif_wm_hints,
connection.atoms.motif_wm_hints,
&[0x3_u32, 0x6, 0x0, 0x0, 0x0],
);
connection.set_property(
wallpaper_engine,
connection.atoms.wm_hints,
connection.atoms.wm_hints,
&[0x1_u32, 0, 0, 0, 0, 0, 0, 0, 0],
);
f.map_as_toplevel(&mut connection, wallpaper_engine);
}
#[test]
@ -2253,7 +2233,7 @@ fn xsettings_fractional_scale() {
let mut connection = Connection::new(&f.display);
f.testwl.enable_xdg_output_manager();
let output = f.testwl.finalize_output();
let output = f.testwl.last_created_output();
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
let surface = f.map_as_toplevel(&mut connection, window);

View file

@ -233,7 +233,6 @@ struct DataSourceData {
struct Output {
name: String,
xdg: Option<ZxdgOutputV1>,
global_id: Option<GlobalId>,
}
struct KeyboardState {
@ -267,8 +266,6 @@ struct State {
last_surface_id: Option<SurfaceId>,
created_surfaces: Vec<SurfaceId>,
last_output: Option<WlOutput>,
last_output_global: Option<GlobalId>,
output_counter: u32,
callbacks: Vec<WlCallback>,
seat: Option<WlSeat>,
pointer: Option<PointerState>,
@ -299,8 +296,6 @@ impl Default for State {
begin: Instant::now(),
last_surface_id: None,
last_output: None,
last_output_global: None,
output_counter: 0,
callbacks: Vec::new(),
seat: None,
pointer: None,
@ -577,15 +572,13 @@ impl Server {
&self.state.created_surfaces
}
/// Finish the initialization of an output created by `new_output`.
/// This function must be called after the globals have been dispatched in order to use the
/// output on the server side created by `new_output` (this function's return value).
#[track_caller]
pub fn finalize_output(&mut self) -> WlOutput {
let output_s = self.state.last_output.take().expect("No new outputs");
let output_data = self.state.outputs.get_mut(&output_s).unwrap();
output_data.global_id = self.state.last_output_global.take();
output_s
pub fn last_created_output(&self) -> WlOutput {
self.state
.last_output
.as_ref()
.expect("No outputs created!")
.clone()
}
pub fn get_object<T: Resource + 'static>(
@ -852,8 +845,7 @@ impl Server {
}
pub fn new_output(&mut self, x: i32, y: i32) {
self.state.last_output_global =
Some(self.dh.create_global::<State, WlOutput, _>(4, (x, y)));
self.dh.create_global::<State, WlOutput, _>(4, (x, y));
self.display.flush_clients().unwrap();
}
@ -885,12 +877,6 @@ impl Server {
self.display.flush_clients().unwrap();
}
pub fn remove_output(&mut self, output: WlOutput) {
let output = self.state.outputs.remove(&output).unwrap();
self.dh.remove_global::<State>(output.global_id.unwrap());
self.display.flush_clients().unwrap();
}
pub fn enable_xdg_output_manager(&mut self) {
self.dh
.create_global::<State, ZxdgOutputManagerV1, _>(3, ());
@ -1140,19 +1126,13 @@ impl GlobalDispatch<WlOutput, (i32, i32)> for State {
"fake monitor".to_string(),
wl_output::Transform::Normal,
);
state.output_counter += 1;
let name = format!("WL-{}", state.output_counter);
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,
xdg: None,
global_id: None,
},
);
state
.outputs
.insert(output.clone(), Output { name, xdg: None });
state.last_output = Some(output);
}
}