Compare commits

..

10 commits

Author SHA1 Message Date
725720f1cb tweaked deco colors 2026-03-31 11:39:44 -05:00
En-En
a879e5e089
fix: cast ExitStatus to correct type, remove Boxes (#399)
* fix: cast ExitStatus to correct type, remove Boxes

Absolutely no clue why Past Me thought `usize` was the correct type
to convert `ExitStatus` from. Rust docs make it really clear it is a
`c_int` (i32) on Unix.

Past Me also decided both leaking memory with `Box::into_raw` then
unsoundly calling `Box::from_raw` on a pointer to a stack-allocated
array was smart. Dunno why. Loser did not even leave a safety comment
smh.

* fix: use `mem::size_of` to get size of `i32`
2026-03-15 20:51:05 -04:00
Shawn Wallace
309d8e2a29 Bump version to 0.8.1
Missed from the last release. Closes #384
2026-03-14 10:37:21 -04:00
awsms
10f985b84c fix: avoid panic when no outputs are present
Replace `unwrap()` on the output scale query with conditional handling.
This prevents a panic when the last output is removed and the query
temporarily returns no results + add test to cover removing all outputs.
2026-03-02 00:34:03 -05:00
En-En
ecde06ed3a fix: take ownership of _NET_WM_CM_S0
EWMH section 8.1 requires that compositing window managers take
ownership of `_NET_WM_CM_Sn` for each screen `n` they manage (in the
case of `satellite`, this is just screen 0).

The Avalonia UI toolkit needs this atom to be owned in order to
provide transparent window support. Resolves #376.
2026-03-02 00:27:44 -05:00
En-En
33c344fee5 fix: align global output offset with screen edges
Previously, global output offset only moved outputs if they were in
negative coordinate space. Doing so guaranteed an output on the top-most
and left-most border of the screen. This commit offsets outputs to achieve
the same goal on outputs in positive coordinate space, which matches the
output placement of `xrandr`, a placement assumption made by Krita's
tablet tool logic.

The new definition of global output offset can be thought of as drawing
the smallest possible rectangle with contains every monitor, and moving
that bounding box to (0, 0).
2026-02-22 14:04:02 -05:00
磁铁开发部(MDD)
536bd32efc
fix: Round up the width under HIDPI (#374) 2026-02-15 17:52:49 -05:00
GoranKovac
86f5bd5d86
Motif popup fix (#370)
Add resize check also since that is not something that popup should
have.
Closes #365
2026-02-08 15:41:11 -05:00
En-En
e6dd3c05c0 fix: panics on output removal
Despawning the output when its global was removed proved to be
overzealous, as the Dispatch of `zxdg_output_v1` still needed it for
cleanup. Instead, only the `OutputScaleFactor` element is removed, and
every surface entity with an `OnOutput` referencing the removed output,
so scaling events sent to surfaces on non-existent outputs did not panic
or use the provided scale as if an output still existed for it.
2026-02-08 11:36:38 -05:00
En-En
0947c4685f
feat: handle global removals, recalc output scale (#367)
All `GlobalRemove` events sent from the server are now handled by
recording them in a new clientside `vec` and passing the identifier
returned by `create_global` (now stored by a map in the state) to
`disable_global`. `handle_globals` (the top-level function) and
`handle_new_globals` (the `InnerServerState` member function) have
swapped names to better represent their new purposes.

This enables action to be taken when globals are removed. In this case,
the desired action is to forward output removal, so that the scaling
calculation does not account for disconnected monitors in its logic.

Resolves #351.
2026-02-03 20:19:49 -05:00
13 changed files with 645 additions and 333 deletions

View file

@ -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
View file

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

View file

@ -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."

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;
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;

View file

@ -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 {
match event {
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::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();
});
}
}

View file

@ -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);

View file

@ -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,14 +1258,17 @@ 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;
};
if !global_offset_updated {
output.geometry(
x - global_output_offset.x.value,
y - global_output_offset.y.value,
@ -1267,6 +1279,7 @@ impl OutputEvent {
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,6 +1363,7 @@ 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)
@ -1357,11 +1373,14 @@ impl OutputEvent {
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 {

View file

@ -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_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
);
@ -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,14 +696,12 @@ 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();
for (_, output_scale) in outputs {
@ -710,6 +720,7 @@ impl<C: XConnection> ServerState<C> {
debug!("Using new scale {scale}");
self.new_scale = Some(scale);
}
}
{
if let Some(FocusData {
@ -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 {

View file

@ -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));
}
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
);
wl_output::Event::Done => {
if let Some(geo) = geo {
assert_eq!(geo, pos);
return;
}
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);
}
_ => {}
}
}
if geo.is_none() {
panic!("Did not receive any geometry events");
} else {
panic!("Did not receive a done event");
}
}
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();
#[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!(
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();

View file

@ -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,

View file

@ -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);

View file

@ -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);
}
}