Compare commits
10 commits
75c9f5e775
...
725720f1cb
| Author | SHA1 | Date | |
|---|---|---|---|
| 725720f1cb | |||
|
|
a879e5e089 | ||
|
|
309d8e2a29 | ||
|
|
10f985b84c | ||
|
|
ecde06ed3a | ||
|
|
33c344fee5 | ||
|
|
536bd32efc | ||
|
|
86f5bd5d86 | ||
|
|
e6dd3c05c0 | ||
|
|
0947c4685f |
13 changed files with 645 additions and 333 deletions
|
|
@ -34,13 +34,16 @@ 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
|
||||
- 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
|
||||
- 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
|
||||
|
||||
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
2
Cargo.lock
generated
|
|
@ -1241,7 +1241,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
|||
|
||||
[[package]]
|
||||
name = "xwayland-satellite"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"ab_glyph",
|
||||
"anyhow",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ edition = "2024"
|
|||
|
||||
[package]
|
||||
name = "xwayland-satellite"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
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."
|
||||
|
|
|
|||
19
src/lib.rs
19
src/lib.rs
|
|
@ -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;
|
||||
use std::os::unix::{net::UnixStream, process::ExitStatusExt};
|
||||
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 = Box::new(xwayland.wait().unwrap());
|
||||
let status = Box::into_raw(status) as usize;
|
||||
finish_tx.write_all(&status.to_ne_bytes()).unwrap();
|
||||
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 mut ready_fds = [
|
||||
|
|
@ -131,11 +131,10 @@ pub fn main(mut data: impl RunData) -> Option<()> {
|
|||
PollFd::new(&finish_rx, PollFlags::IN),
|
||||
];
|
||||
|
||||
fn xwayland_exit_code(rx: &mut UnixStream) -> Box<ExitStatus> {
|
||||
let mut data = [0; (usize::BITS / 8) as usize];
|
||||
fn xwayland_exit_code(rx: &mut UnixStream) -> ExitStatus {
|
||||
let mut data = [0; std::mem::size_of::<i32>()];
|
||||
rx.read_exact(&mut data).unwrap();
|
||||
let data = usize::from_ne_bytes(data);
|
||||
unsafe { Box::from_raw(data as *mut _) }
|
||||
ExitStatus::from_raw(i32::from_ne_bytes(data))
|
||||
}
|
||||
|
||||
let connection = match poll(&mut ready_fds, None) {
|
||||
|
|
@ -179,7 +178,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;
|
||||
|
|
@ -246,7 +245,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;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use super::decoration::DecorationMarker;
|
||||
|
||||
use super::ObjectEvent;
|
||||
use super::{GlobalName, ObjectEvent};
|
||||
use hecs::{Entity, World};
|
||||
use smithay_client_toolkit::{
|
||||
activation::{ActivationHandler, RequestData, RequestDataExt},
|
||||
|
|
@ -115,6 +115,7 @@ 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>,
|
||||
|
|
@ -128,6 +129,7 @@ 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(),
|
||||
|
|
@ -204,18 +206,23 @@ impl Dispatch<WlRegistry, GlobalListContents> for MyWorld {
|
|||
_: &wayland_client::Connection,
|
||||
_: &wayland_client::QueueHandle<Self>,
|
||||
) {
|
||||
if let Event::<WlRegistry>::Global {
|
||||
name,
|
||||
interface,
|
||||
version,
|
||||
} = event
|
||||
{
|
||||
state.new_globals.push(Global {
|
||||
match event {
|
||||
Event::<WlRegistry>::Global {
|
||||
name,
|
||||
interface,
|
||||
version,
|
||||
});
|
||||
};
|
||||
} => {
|
||||
state.new_globals.push(Global {
|
||||
name,
|
||||
interface,
|
||||
version,
|
||||
});
|
||||
}
|
||||
Event::<WlRegistry>::GlobalRemove { name } => {
|
||||
state.removed_globals.push(GlobalName(name));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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::WHITE);
|
||||
bar.fill(Color::from_rgba(0.2, 0.2, 0.2, 1.0).unwrap());
|
||||
|
||||
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.0, 0.0, 0.8).unwrap());
|
||||
x.fill(Color::from_rgba(1.0, 0.2, 0.2, 0.8).unwrap());
|
||||
} else {
|
||||
x.fill(Color::WHITE);
|
||||
x.fill(Color::from_rgba(0.2, 0.2, 0.2, 1.0).unwrap());
|
||||
}
|
||||
let size = x.width() as f32;
|
||||
let margin = 8.4 * scale;
|
||||
|
|
@ -363,9 +363,11 @@ 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,
|
||||
&Default::default(),
|
||||
&paint,
|
||||
&Stroke {
|
||||
width: scale + 0.5,
|
||||
..Default::default()
|
||||
|
|
@ -400,7 +402,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(0, 0, 0, (coverage * 255.0) as u8).premultiply();
|
||||
ColorU8::from_rgba(255, 255, 255, (coverage * 255.0) as u8).premultiply();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1479,6 +1479,7 @@ 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);
|
||||
|
|
|
|||
|
|
@ -212,7 +212,9 @@ impl SurfaceEvents {
|
|||
|
||||
let mut query = data.query::<(&x::Window, &mut WindowData)>();
|
||||
if let Some((window, win_data)) = query.get() {
|
||||
let dimensions = output_data.get::<&OutputDimensions>().unwrap();
|
||||
let Some(dimensions) = output_data.get::<&OutputDimensions>() else {
|
||||
return;
|
||||
};
|
||||
win_data.update_output_offset(
|
||||
*window,
|
||||
WindowOutputOffset {
|
||||
|
|
@ -472,8 +474,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) as i32;
|
||||
let height = (dims.height as f64 / scale_factor.0) as i32;
|
||||
let width = (dims.width as f64 / scale_factor.0).ceil() as i32;
|
||||
let height = (dims.height as f64 / scale_factor.0).ceil() as i32;
|
||||
if width > 0 && height > 0 {
|
||||
viewport.set_destination(width, height);
|
||||
}
|
||||
|
|
@ -983,7 +985,7 @@ impl Event for client::wl_touch::Event {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub(super) struct OnOutput(pub Entity);
|
||||
struct OutputName(String);
|
||||
fn get_output_name(output: Option<&OnOutput>, world: &World) -> Option<String> {
|
||||
|
|
@ -1077,7 +1079,9 @@ fn update_output_offset(
|
|||
let connection = &mut state.connection;
|
||||
let state = &mut state.inner;
|
||||
{
|
||||
let mut dimensions = state.world.get::<&mut OutputDimensions>(output).unwrap();
|
||||
let Ok(mut dimensions) = state.world.get::<&mut OutputDimensions>(output) else {
|
||||
return;
|
||||
};
|
||||
if matches!(source, OutputDimensionsSource::Wl { .. })
|
||||
&& matches!(dimensions.source, OutputDimensionsSource::Xdg)
|
||||
{
|
||||
|
|
@ -1093,7 +1097,8 @@ fn update_output_offset(
|
|||
};
|
||||
state.global_offset_updated = true;
|
||||
} else if dim.owner == Some(output) && value > dim.value {
|
||||
*dim = Default::default();
|
||||
// Another output's position could be less than the new value, so recalculate
|
||||
dim.owner = None;
|
||||
state.global_offset_updated = true;
|
||||
}
|
||||
};
|
||||
|
|
@ -1124,7 +1129,9 @@ fn update_window_output_offsets(
|
|||
world: &World,
|
||||
connection: &mut impl XConnection,
|
||||
) {
|
||||
let dimensions = world.get::<&OutputDimensions>(output).unwrap();
|
||||
let Ok(dimensions) = world.get::<&OutputDimensions>(output) else {
|
||||
return;
|
||||
};
|
||||
let mut query = world.query::<(&x::Window, &mut WindowData, &OnOutput)>();
|
||||
|
||||
for (_, (window, data, _)) in query
|
||||
|
|
@ -1150,7 +1157,9 @@ pub(super) fn update_global_output_offset(
|
|||
) {
|
||||
let entity = world.entity(output).unwrap();
|
||||
let mut query = entity.query::<(&OutputDimensions, &WlOutput)>();
|
||||
let (dimensions, server) = query.get().unwrap();
|
||||
let Some((dimensions, server)) = query.get() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let x = dimensions.x - global_output_offset.x.value;
|
||||
let y = dimensions.y - global_output_offset.y.value;
|
||||
|
|
@ -1249,24 +1258,28 @@ impl OutputEvent {
|
|||
state,
|
||||
);
|
||||
let global_output_offset = state.global_output_offset;
|
||||
let global_offset_updated = state.global_offset_updated;
|
||||
|
||||
let (output, dimensions, xdg) = state
|
||||
.world
|
||||
.query_one_mut::<(&WlOutput, &mut OutputDimensions, Option<&XdgOutputServer>)>(
|
||||
target,
|
||||
)
|
||||
.unwrap();
|
||||
let Ok((output, dimensions, xdg)) = state.world.query_one_mut::<(
|
||||
&WlOutput,
|
||||
&mut OutputDimensions,
|
||||
Option<&XdgOutputServer>,
|
||||
)>(target) else {
|
||||
return;
|
||||
};
|
||||
|
||||
output.geometry(
|
||||
x - global_output_offset.x.value,
|
||||
y - global_output_offset.y.value,
|
||||
physical_width,
|
||||
physical_height,
|
||||
convert_wenum(subpixel),
|
||||
make,
|
||||
model,
|
||||
convert_wenum(transform),
|
||||
);
|
||||
if !global_offset_updated {
|
||||
output.geometry(
|
||||
x - global_output_offset.x.value,
|
||||
y - global_output_offset.y.value,
|
||||
physical_width,
|
||||
physical_height,
|
||||
convert_wenum(subpixel),
|
||||
make,
|
||||
model,
|
||||
convert_wenum(transform),
|
||||
);
|
||||
}
|
||||
dimensions.rotated_90 = transform.into_result().is_ok_and(|t| {
|
||||
matches!(
|
||||
t,
|
||||
|
|
@ -1290,10 +1303,12 @@ impl OutputEvent {
|
|||
height,
|
||||
refresh,
|
||||
} => {
|
||||
let (output, dimensions) = state
|
||||
let Ok((output, dimensions)) = state
|
||||
.world
|
||||
.query_one_mut::<(&WlOutput, &mut OutputDimensions)>(target)
|
||||
.unwrap();
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if flags
|
||||
.into_result()
|
||||
|
|
@ -1348,20 +1363,24 @@ impl OutputEvent {
|
|||
match event {
|
||||
Event::LogicalPosition { x, y } => {
|
||||
update_output_offset(target, OutputDimensionsSource::Xdg, x, y, state);
|
||||
state
|
||||
.world
|
||||
.get::<&XdgOutputServer>(target)
|
||||
.unwrap()
|
||||
.logical_position(
|
||||
x - state.global_output_offset.x.value,
|
||||
y - state.global_output_offset.y.value,
|
||||
);
|
||||
if !state.global_offset_updated {
|
||||
state
|
||||
.world
|
||||
.get::<&XdgOutputServer>(target)
|
||||
.unwrap()
|
||||
.logical_position(
|
||||
x - state.global_output_offset.x.value,
|
||||
y - state.global_output_offset.y.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
Event::LogicalSize { .. } => {
|
||||
let (xdg, dimensions) = state
|
||||
let Ok((xdg, dimensions)) = state
|
||||
.world
|
||||
.query_one_mut::<(&XdgOutputServer, &OutputDimensions)>(target)
|
||||
.unwrap();
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if dimensions.rotated_90 {
|
||||
xdg.logical_size(dimensions.height, dimensions.width);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ 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,
|
||||
|
|
@ -142,7 +143,7 @@ impl WindowData {
|
|||
offset: WindowOutputOffset,
|
||||
connection: &mut C,
|
||||
) {
|
||||
log::trace!("offset: {offset:?}");
|
||||
log::trace!(target: "output_offset", "offset: {offset:?}");
|
||||
if offset == self.output_offset {
|
||||
return;
|
||||
}
|
||||
|
|
@ -161,7 +162,7 @@ impl WindowData {
|
|||
height: self.attrs.dims.height as _,
|
||||
},
|
||||
) {
|
||||
debug!("set {:?} offset to {:?}", window, self.output_offset);
|
||||
debug!(target: "output_offset", "set {:?} offset to {:?}", window, self.output_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -342,7 +343,8 @@ enum ObjectEvent {
|
|||
}
|
||||
}
|
||||
|
||||
fn handle_globals<'a, S: X11Selection + 'static>(
|
||||
fn handle_new_globals<'a, S: X11Selection + 'static>(
|
||||
globals_map: &mut HashMap<GlobalName, (Global, GlobalId)>,
|
||||
dh: &DisplayHandle,
|
||||
globals: impl IntoIterator<Item = &'a Global>,
|
||||
) {
|
||||
|
|
@ -353,7 +355,8 @@ fn handle_globals<'a, S: X11Selection + 'static>(
|
|||
$(
|
||||
ref x if x == <$global>::interface().name => {
|
||||
let version = u32::min(global.version, <$global>::interface().version);
|
||||
dh.create_global::<InnerServerState<S>, $global, Global>(version, global.clone());
|
||||
let global_id = dh.create_global::<InnerServerState<S>, $global, Global>(version, global.clone());
|
||||
globals_map.insert(GlobalName(global.name), (global.clone(), global_id));
|
||||
}
|
||||
)+
|
||||
_ => {}
|
||||
|
|
@ -377,6 +380,9 @@ fn handle_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>,
|
||||
|
|
@ -446,6 +452,7 @@ 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,
|
||||
|
|
@ -532,9 +539,10 @@ 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_globals::<S>(&dh, globals));
|
||||
.with_list(|globals| handle_new_globals::<S>(&mut globals_map, &dh, globals));
|
||||
|
||||
let world = MyWorld::new(global_list);
|
||||
let client = dh.insert_client(client, std::sync::Arc::new(())).unwrap();
|
||||
|
|
@ -545,6 +553,7 @@ impl<S: X11Selection> ServerState<NoConnection<S>> {
|
|||
client,
|
||||
queue,
|
||||
qh,
|
||||
globals_map,
|
||||
dh,
|
||||
to_focus: None,
|
||||
unfocus: false,
|
||||
|
|
@ -613,7 +622,7 @@ impl<C: XConnection> ServerState<C> {
|
|||
}
|
||||
|
||||
pub fn handle_clientside_events(&mut self) {
|
||||
self.handle_new_globals();
|
||||
self.handle_globals();
|
||||
|
||||
for (target, event) in self.world.read_events() {
|
||||
if !self.world.contains(target) {
|
||||
|
|
@ -635,14 +644,15 @@ impl<C: XConnection> ServerState<C> {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
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 {
|
||||
if self.global_output_offset.x.owner.is_none()
|
||||
|| self.global_output_offset.y.owner.is_none()
|
||||
{
|
||||
self.calc_global_output_offset();
|
||||
}
|
||||
|
||||
debug!(
|
||||
target: "output_offset",
|
||||
"updated global output offset: {}x{}",
|
||||
self.global_output_offset.x.value, self.global_output_offset.y.value
|
||||
);
|
||||
|
|
@ -659,8 +669,10 @@ impl<C: XConnection> ServerState<C> {
|
|||
}
|
||||
|
||||
if !self.updated_outputs.is_empty() {
|
||||
for output in self.updated_outputs.iter() {
|
||||
let output_scale = self.world.get::<&OutputScaleFactor>(*output).unwrap();
|
||||
for output in std::mem::take(&mut self.updated_outputs).iter() {
|
||||
let Ok(output_scale) = self.world.get::<&OutputScaleFactor>(*output) else {
|
||||
continue;
|
||||
};
|
||||
if matches!(*output_scale, OutputScaleFactor::Output(..)) {
|
||||
let mut surface_query = self
|
||||
.world
|
||||
|
|
@ -684,31 +696,30 @@ 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();
|
||||
let (_, output_scale) = outputs.next().unwrap();
|
||||
if let Some((_, output_scale)) = outputs.next() {
|
||||
scale = output_scale.get();
|
||||
|
||||
scale = output_scale.get();
|
||||
|
||||
for (_, output_scale) in outputs {
|
||||
if output_scale.get() != scale {
|
||||
mixed_scale = true;
|
||||
scale = scale.min(output_scale.get());
|
||||
for (_, output_scale) in outputs {
|
||||
if output_scale.get() != scale {
|
||||
mixed_scale = true;
|
||||
scale = scale.min(output_scale.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mixed_scale {
|
||||
warn!(
|
||||
"Mixed output scales detected, choosing to give apps the smallest detected scale ({scale}x)"
|
||||
);
|
||||
}
|
||||
if mixed_scale {
|
||||
warn!(
|
||||
"Mixed output scales detected, choosing to give apps the smallest detected scale ({scale}x)"
|
||||
);
|
||||
}
|
||||
|
||||
debug!("Using new scale {scale}");
|
||||
self.new_scale = Some(scale);
|
||||
debug!("Using new scale {scale}");
|
||||
self.new_scale = Some(scale);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
|
|
@ -780,9 +791,55 @@ impl<S: X11Selection + 'static> InnerServerState<S> {
|
|||
self.queue.as_fd()
|
||||
}
|
||||
|
||||
fn handle_new_globals(&mut self) {
|
||||
fn handle_globals(&mut self) {
|
||||
let globals = std::mem::take(&mut self.world.new_globals);
|
||||
handle_globals::<S>(&self.dh, globals.iter());
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_window(
|
||||
|
|
@ -1277,6 +1334,8 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -530,7 +530,19 @@ impl<C: XConnection> TestFixture<C> {
|
|||
);
|
||||
self.run();
|
||||
self.run();
|
||||
(output, self.testwl.last_created_output())
|
||||
(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:?}");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -852,13 +864,6 @@ 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 {
|
||||
|
|
@ -1608,86 +1613,257 @@ fn override_redirect_choose_hover_window() {
|
|||
assert_eq!(&popup_data.popup().parent, win1_xdg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_offset() {
|
||||
let (mut f, comp) = TestFixture::new_with_compositor();
|
||||
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 (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();
|
||||
#[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;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
if geo.is_none() {
|
||||
panic!("Did not receive any geometry events");
|
||||
} else {
|
||||
panic!("Did not receive a done event");
|
||||
}
|
||||
f.testwl.configure_toplevel(t_id, 100, 100, vec![]);
|
||||
f.testwl.focus_toplevel(t_id);
|
||||
f.run();
|
||||
}
|
||||
|
||||
{
|
||||
let data = &f.connection().windows[&window];
|
||||
assert_eq!(data.dims.x, 500);
|
||||
assert_eq!(data.dims.y, 100);
|
||||
#[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;
|
||||
}
|
||||
}
|
||||
|
||||
let popup = Window::new(2);
|
||||
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();
|
||||
assert!(done, "Did not get zxdg_output_v1 logical_position");
|
||||
let events = std::mem::take(&mut *out.data.events.lock().unwrap());
|
||||
assert_eq!(
|
||||
data.popup().positioner_state.offset,
|
||||
testwl::Vec2 { x: 10, y: 10 }
|
||||
);
|
||||
|
||||
f.satellite.unmap_window(popup);
|
||||
p_surface.obj.destroy();
|
||||
f.run();
|
||||
|
||||
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 }
|
||||
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_change() {
|
||||
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() {
|
||||
let (mut f, comp) = TestFixture::new_with_compositor();
|
||||
|
||||
f.new_output(0, 0);
|
||||
let (_, output) = f.new_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);
|
||||
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 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,
|
||||
};
|
||||
f.testwl.move_surface_to_output(p_id, &output);
|
||||
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 }
|
||||
);
|
||||
f.assert_window_dimensions(popup, p_id, popup_dims);
|
||||
|
||||
f.testwl.move_output(&output, 600, 200);
|
||||
f.run();
|
||||
f.run();
|
||||
|
||||
toplevel_pos.x = 600;
|
||||
toplevel_pos.y = 200;
|
||||
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 = 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() {
|
||||
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);
|
||||
let (_, id) = f.create_toplevel(&comp, window);
|
||||
|
|
@ -1701,13 +1877,8 @@ fn output_offset_change() {
|
|||
};
|
||||
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);
|
||||
f.create_xdg_output(&man, output_obj.obj.clone());
|
||||
// testwl inits xdg output position to 0, and it should take priority over wl_output position
|
||||
test_position(&f, 0, 0);
|
||||
|
||||
|
|
@ -1721,6 +1892,82 @@ fn output_offset_change() {
|
|||
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();
|
||||
|
|
@ -2025,140 +2272,6 @@ 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();
|
||||
|
|
@ -2288,8 +2401,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, 66);
|
||||
assert_eq!(viewport.height, 66);
|
||||
assert_eq!(viewport.width, 67);
|
||||
assert_eq!(viewport.height, 67);
|
||||
}
|
||||
|
||||
let popup = Window::new(2);
|
||||
|
|
@ -2725,6 +2838,63 @@ 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();
|
||||
|
|
|
|||
|
|
@ -333,6 +333,14 @@ 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) {
|
||||
|
|
@ -723,9 +731,12 @@ 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.contains(motif::Functions::Minimize)
|
||||
|| f.contains(motif::Functions::Maximize)
|
||||
|| f.contains(motif::Functions::All)
|
||||
f.intersects(
|
||||
motif::Functions::Minimize
|
||||
| motif::Functions::Maximize
|
||||
| motif::Functions::Resize
|
||||
| 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.
|
||||
|
|
@ -1031,6 +1042,7 @@ 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,
|
||||
|
|
|
|||
|
|
@ -102,12 +102,11 @@ 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(&return_ptr.to_ne_bytes())
|
||||
.write_all(&0_i32.to_ne_bytes())
|
||||
.is_err_and(|e| e.kind() != std::io::ErrorKind::BrokenPipe)
|
||||
{
|
||||
panic!("could not message the main thread to terminate");
|
||||
|
|
@ -319,7 +318,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.last_created_output()
|
||||
self.testwl.finalize_output()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2174,6 +2173,27 @@ 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]
|
||||
|
|
@ -2233,7 +2253,7 @@ fn xsettings_fractional_scale() {
|
|||
let mut connection = Connection::new(&f.display);
|
||||
f.testwl.enable_xdg_output_manager();
|
||||
|
||||
let output = f.testwl.last_created_output();
|
||||
let output = f.testwl.finalize_output();
|
||||
|
||||
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
|
||||
let surface = f.map_as_toplevel(&mut connection, window);
|
||||
|
|
|
|||
|
|
@ -233,6 +233,7 @@ struct DataSourceData {
|
|||
struct Output {
|
||||
name: String,
|
||||
xdg: Option<ZxdgOutputV1>,
|
||||
global_id: Option<GlobalId>,
|
||||
}
|
||||
|
||||
struct KeyboardState {
|
||||
|
|
@ -266,6 +267,8 @@ 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>,
|
||||
|
|
@ -296,6 +299,8 @@ 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,
|
||||
|
|
@ -572,13 +577,15 @@ 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 last_created_output(&self) -> WlOutput {
|
||||
self.state
|
||||
.last_output
|
||||
.as_ref()
|
||||
.expect("No outputs created!")
|
||||
.clone()
|
||||
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 get_object<T: Resource + 'static>(
|
||||
|
|
@ -845,7 +852,8 @@ impl Server {
|
|||
}
|
||||
|
||||
pub fn new_output(&mut self, x: i32, y: i32) {
|
||||
self.dh.create_global::<State, WlOutput, _>(4, (x, y));
|
||||
self.state.last_output_global =
|
||||
Some(self.dh.create_global::<State, WlOutput, _>(4, (x, y)));
|
||||
self.display.flush_clients().unwrap();
|
||||
}
|
||||
|
||||
|
|
@ -877,6 +885,12 @@ 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, ());
|
||||
|
|
@ -1126,13 +1140,19 @@ impl GlobalDispatch<WlOutput, (i32, i32)> for State {
|
|||
"fake monitor".to_string(),
|
||||
wl_output::Transform::Normal,
|
||||
);
|
||||
let name = format!("WL-{}", state.outputs.len() + 1);
|
||||
state.output_counter += 1;
|
||||
let name = format!("WL-{}", state.output_counter);
|
||||
output.name(name.clone());
|
||||
output.mode(wl_output::Mode::Current, 1000, 1000, 0);
|
||||
output.done();
|
||||
state
|
||||
.outputs
|
||||
.insert(output.clone(), Output { name, xdg: None });
|
||||
state.outputs.insert(
|
||||
output.clone(),
|
||||
Output {
|
||||
name,
|
||||
xdg: None,
|
||||
global_id: None,
|
||||
},
|
||||
);
|
||||
state.last_output = Some(output);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue