Support XDG Activation
Test XDG Activation
This commit is contained in:
parent
b34b08f004
commit
180efb0ba9
10 changed files with 413 additions and 27 deletions
|
|
@ -28,7 +28,7 @@ wayland-client.workspace = true
|
||||||
wayland-protocols = { workspace = true, features = ["client", "server", "staging", "unstable"] }
|
wayland-protocols = { workspace = true, features = ["client", "server", "staging", "unstable"] }
|
||||||
wayland-scanner.workspace = true
|
wayland-scanner.workspace = true
|
||||||
wayland-server.workspace = true
|
wayland-server.workspace = true
|
||||||
xcb = { version = "1.3.0", features = ["composite", "randr"] }
|
xcb = { version = "1.3.0", features = ["composite", "randr", "res"] }
|
||||||
wl_drm = { path = "wl_drm" }
|
wl_drm = { path = "wl_drm" }
|
||||||
libc = "0.2.153"
|
libc = "0.2.153"
|
||||||
log = "0.4.21"
|
log = "0.4.21"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
mod data_device;
|
mod data_device;
|
||||||
|
pub mod xdg_activation;
|
||||||
|
|
||||||
use crate::server::{ObjectEvent, ObjectKey};
|
use crate::server::{ObjectEvent, ObjectKey};
|
||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
|
|
@ -46,6 +47,7 @@ use wayland_protocols::{
|
||||||
viewporter::client::{wp_viewport::WpViewport, wp_viewporter::WpViewporter},
|
viewporter::client::{wp_viewport::WpViewport, wp_viewporter::WpViewporter},
|
||||||
},
|
},
|
||||||
xdg::{
|
xdg::{
|
||||||
|
activation::v1::client::xdg_activation_v1::XdgActivationV1,
|
||||||
shell::client::{
|
shell::client::{
|
||||||
xdg_popup::XdgPopup, xdg_positioner::XdgPositioner, xdg_surface::XdgSurface,
|
xdg_popup::XdgPopup, xdg_positioner::XdgPositioner, xdg_surface::XdgSurface,
|
||||||
xdg_toplevel::XdgToplevel, xdg_wm_base::XdgWmBase,
|
xdg_toplevel::XdgToplevel, xdg_wm_base::XdgWmBase,
|
||||||
|
|
@ -69,6 +71,7 @@ pub struct Globals {
|
||||||
smithay_client_toolkit::data_device_manager::WritePipe,
|
smithay_client_toolkit::data_device_manager::WritePipe,
|
||||||
)>,
|
)>,
|
||||||
pub cancelled: bool,
|
pub cancelled: bool,
|
||||||
|
pub pending_activations: Vec<(xcb::x::Window, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ClientQueueHandle = QueueHandle<Globals>;
|
pub type ClientQueueHandle = QueueHandle<Globals>;
|
||||||
|
|
@ -136,6 +139,7 @@ delegate_noop!(Globals: WpViewport);
|
||||||
delegate_noop!(Globals: ZxdgOutputManagerV1);
|
delegate_noop!(Globals: ZxdgOutputManagerV1);
|
||||||
delegate_noop!(Globals: ZwpPointerConstraintsV1);
|
delegate_noop!(Globals: ZwpPointerConstraintsV1);
|
||||||
delegate_noop!(Globals: ZwpTabletManagerV2);
|
delegate_noop!(Globals: ZwpTabletManagerV2);
|
||||||
|
delegate_noop!(Globals: XdgActivationV1);
|
||||||
|
|
||||||
impl Dispatch<WlRegistry, GlobalListContents> for Globals {
|
impl Dispatch<WlRegistry, GlobalListContents> for Globals {
|
||||||
fn event(
|
fn event(
|
||||||
|
|
|
||||||
42
src/clientside/xdg_activation.rs
Normal file
42
src/clientside/xdg_activation.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
use smithay_client_toolkit::{
|
||||||
|
activation::{ActivationHandler, RequestData, RequestDataExt},
|
||||||
|
delegate_activation,
|
||||||
|
};
|
||||||
|
use xcb::x;
|
||||||
|
|
||||||
|
use crate::clientside::Globals;
|
||||||
|
|
||||||
|
delegate_activation!(Globals, ActivationData);
|
||||||
|
|
||||||
|
pub struct ActivationData {
|
||||||
|
window: x::Window,
|
||||||
|
data: RequestData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActivationData {
|
||||||
|
pub fn new(window: x::Window, data: RequestData) -> Self {
|
||||||
|
Self { window, data }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestDataExt for ActivationData {
|
||||||
|
fn app_id(&self) -> Option<&str> {
|
||||||
|
self.data.app_id()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn seat_and_serial(&self) -> Option<(&wayland_client::protocol::wl_seat::WlSeat, u32)> {
|
||||||
|
self.data.seat_and_serial()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn surface(&self) -> Option<&wayland_client::protocol::wl_surface::WlSurface> {
|
||||||
|
self.data.surface()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActivationHandler for Globals {
|
||||||
|
type RequestData = ActivationData;
|
||||||
|
|
||||||
|
fn new_token(&mut self, token: String, data: &Self::RequestData) {
|
||||||
|
self.pending_activations.push((data.window, token));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -445,10 +445,18 @@ impl<C: XConnection> Dispatch<WlSeat, ObjectKey> for ServerState<C> {
|
||||||
state
|
state
|
||||||
.objects
|
.objects
|
||||||
.insert_from_other_objects([*key], |[seat_obj], key| {
|
.insert_from_other_objects([*key], |[seat_obj], key| {
|
||||||
let Seat { client, .. }: &Seat = seat_obj.try_into().unwrap();
|
let Seat {
|
||||||
let client = client.get_keyboard(&state.qh, key);
|
client: client_seat,
|
||||||
|
..
|
||||||
|
}: &Seat = seat_obj.try_into().unwrap();
|
||||||
|
let client = client_seat.get_keyboard(&state.qh, key);
|
||||||
let server = data_init.init(id, key);
|
let server = data_init.init(id, key);
|
||||||
Keyboard { client, server }.into()
|
Keyboard {
|
||||||
|
client,
|
||||||
|
server,
|
||||||
|
seat: client_seat.clone(),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Request::<WlSeat>::GetTouch { id } => {
|
Request::<WlSeat>::GetTouch { id } => {
|
||||||
|
|
|
||||||
|
|
@ -540,7 +540,12 @@ impl HandleEvent for Pointer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Keyboard = GenericObject<WlKeyboard, client::wl_keyboard::WlKeyboard>;
|
pub struct Keyboard {
|
||||||
|
pub server: WlKeyboard,
|
||||||
|
pub client: client::wl_keyboard::WlKeyboard,
|
||||||
|
pub seat: client::wl_seat::WlSeat,
|
||||||
|
}
|
||||||
|
|
||||||
impl HandleEvent for Keyboard {
|
impl HandleEvent for Keyboard {
|
||||||
type Event = client::wl_keyboard::Event;
|
type Event = client::wl_keyboard::Event;
|
||||||
|
|
||||||
|
|
@ -557,7 +562,14 @@ impl HandleEvent for Keyboard {
|
||||||
.get(key)
|
.get(key)
|
||||||
.map(<_ as AsRef<SurfaceData>>::as_ref)
|
.map(<_ as AsRef<SurfaceData>>::as_ref)
|
||||||
}) {
|
}) {
|
||||||
state.last_kb_serial = Some(serial);
|
state.last_kb_serial = Some((
|
||||||
|
state
|
||||||
|
.last_kb_serial
|
||||||
|
.take()
|
||||||
|
.and_then(|(seat, _)| (seat == self.seat).then_some(seat))
|
||||||
|
.unwrap_or_else(|| self.seat.clone()),
|
||||||
|
serial,
|
||||||
|
));
|
||||||
let output_name = data.get_output_name(state);
|
let output_name = data.get_output_name(state);
|
||||||
state.to_focus = Some(FocusData {
|
state.to_focus = Some(FocusData {
|
||||||
window: data.window.unwrap(),
|
window: data.window.unwrap(),
|
||||||
|
|
@ -584,6 +596,22 @@ impl HandleEvent for Keyboard {
|
||||||
self.server.leave(serial, &data.server);
|
self.server.leave(serial, &data.server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
client::wl_keyboard::Event::Key {
|
||||||
|
serial,
|
||||||
|
time,
|
||||||
|
key,
|
||||||
|
state: key_state,
|
||||||
|
} => {
|
||||||
|
state.last_kb_serial = Some((
|
||||||
|
state
|
||||||
|
.last_kb_serial
|
||||||
|
.take()
|
||||||
|
.and_then(|(seat, _)| (seat == self.seat).then_some(seat))
|
||||||
|
.unwrap_or_else(|| self.seat.clone()),
|
||||||
|
serial,
|
||||||
|
));
|
||||||
|
self.server.key(serial, time, key, convert_wenum(key_state));
|
||||||
|
}
|
||||||
_ => simple_event_shunt! {
|
_ => simple_event_shunt! {
|
||||||
self.server, event: client::wl_keyboard::Event => [
|
self.server, event: client::wl_keyboard::Event => [
|
||||||
Keymap {
|
Keymap {
|
||||||
|
|
@ -591,12 +619,6 @@ impl HandleEvent for Keyboard {
|
||||||
|fd| fd.as_fd(),
|
|fd| fd.as_fd(),
|
||||||
size
|
size
|
||||||
},
|
},
|
||||||
Key {
|
|
||||||
serial,
|
|
||||||
time,
|
|
||||||
key,
|
|
||||||
|state| convert_wenum(state)
|
|
||||||
},
|
|
||||||
Modifiers {
|
Modifiers {
|
||||||
serial,
|
serial,
|
||||||
mods_depressed,
|
mods_depressed,
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,12 @@ use crate::{X11Selection, XConnection};
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use rustix::event::{poll, PollFd, PollFlags};
|
use rustix::event::{poll, PollFd, PollFlags};
|
||||||
use slotmap::{new_key_type, HopSlotMap, SparseSecondaryMap};
|
use slotmap::{new_key_type, HopSlotMap, SparseSecondaryMap};
|
||||||
|
use smithay_client_toolkit::activation::ActivationState;
|
||||||
use smithay_client_toolkit::data_device_manager::{
|
use smithay_client_toolkit::data_device_manager::{
|
||||||
data_device::DataDevice, data_offer::SelectionOffer, data_source::CopyPasteSource,
|
data_device::DataDevice, data_offer::SelectionOffer, data_source::CopyPasteSource,
|
||||||
DataDeviceManagerState,
|
DataDeviceManagerState,
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::os::fd::{AsFd, BorrowedFd};
|
use std::os::fd::{AsFd, BorrowedFd};
|
||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
|
|
@ -43,10 +44,11 @@ use wayland_protocols::{
|
||||||
xwayland_shell_v1::XwaylandShellV1, xwayland_surface_v1::XwaylandSurfaceV1,
|
xwayland_shell_v1::XwaylandShellV1, xwayland_surface_v1::XwaylandSurfaceV1,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use wayland_server::protocol::wl_seat::WlSeat;
|
||||||
use wayland_server::{
|
use wayland_server::{
|
||||||
protocol::{
|
protocol::{
|
||||||
wl_callback::WlCallback, wl_compositor::WlCompositor, wl_output::WlOutput, wl_seat::WlSeat,
|
wl_callback::WlCallback, wl_compositor::WlCompositor, wl_output::WlOutput, wl_shm::WlShm,
|
||||||
wl_shm::WlShm, wl_surface::WlSurface,
|
wl_surface::WlSurface,
|
||||||
},
|
},
|
||||||
Client, DisplayHandle, Resource, WEnum,
|
Client, DisplayHandle, Resource, WEnum,
|
||||||
};
|
};
|
||||||
|
|
@ -102,6 +104,7 @@ struct WindowData {
|
||||||
attrs: WindowAttributes,
|
attrs: WindowAttributes,
|
||||||
output_offset: WindowOutputOffset,
|
output_offset: WindowOutputOffset,
|
||||||
output_key: Option<ObjectKey>,
|
output_key: Option<ObjectKey>,
|
||||||
|
activation_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WindowData {
|
impl WindowData {
|
||||||
|
|
@ -110,6 +113,7 @@ impl WindowData {
|
||||||
override_redirect: bool,
|
override_redirect: bool,
|
||||||
dims: WindowDims,
|
dims: WindowDims,
|
||||||
parent: Option<x::Window>,
|
parent: Option<x::Window>,
|
||||||
|
activation_token: Option<String>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
window,
|
window,
|
||||||
|
|
@ -124,6 +128,7 @@ impl WindowData {
|
||||||
},
|
},
|
||||||
output_offset: WindowOutputOffset::default(),
|
output_offset: WindowOutputOffset::default(),
|
||||||
output_key: None,
|
output_key: None,
|
||||||
|
activation_token,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -488,6 +493,7 @@ pub struct ServerState<C: XConnection> {
|
||||||
associated_windows: SparseSecondaryMap<ObjectKey, x::Window>,
|
associated_windows: SparseSecondaryMap<ObjectKey, x::Window>,
|
||||||
output_keys: SparseSecondaryMap<ObjectKey, ()>,
|
output_keys: SparseSecondaryMap<ObjectKey, ()>,
|
||||||
windows: HashMap<x::Window, WindowData>,
|
windows: HashMap<x::Window, WindowData>,
|
||||||
|
pids: HashSet<u32>,
|
||||||
|
|
||||||
qh: ClientQueueHandle,
|
qh: ClientQueueHandle,
|
||||||
client: Option<Client>,
|
client: Option<Client>,
|
||||||
|
|
@ -499,7 +505,8 @@ pub struct ServerState<C: XConnection> {
|
||||||
|
|
||||||
xdg_wm_base: XdgWmBase,
|
xdg_wm_base: XdgWmBase,
|
||||||
clipboard_data: Option<ClipboardData<C::X11Selection>>,
|
clipboard_data: Option<ClipboardData<C::X11Selection>>,
|
||||||
last_kb_serial: Option<u32>,
|
last_kb_serial: Option<(client::wl_seat::WlSeat, u32)>,
|
||||||
|
activation_state: Option<ActivationState>,
|
||||||
global_output_offset: GlobalOutputOffset,
|
global_output_offset: GlobalOutputOffset,
|
||||||
global_offset_updated: bool,
|
global_offset_updated: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -529,6 +536,12 @@ impl<C: XConnection> ServerState<C> {
|
||||||
source: None::<CopyPasteData<C::X11Selection>>,
|
source: None::<CopyPasteData<C::X11Selection>>,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let activation_state = ActivationState::bind(&clientside.global_list, &qh)
|
||||||
|
.inspect_err(|e| {
|
||||||
|
warn!("Could not bind xdg activation ({e:?}). Windows might not recive focus depending on compositor focus stealing policy.")
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
dh.create_global::<Self, XwaylandShellV1, _>(1, ());
|
dh.create_global::<Self, XwaylandShellV1, _>(1, ());
|
||||||
clientside
|
clientside
|
||||||
.global_list
|
.global_list
|
||||||
|
|
@ -537,6 +550,7 @@ impl<C: XConnection> ServerState<C> {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
windows: HashMap::new(),
|
windows: HashMap::new(),
|
||||||
|
pids: HashSet::new(),
|
||||||
clientside,
|
clientside,
|
||||||
client: None,
|
client: None,
|
||||||
qh,
|
qh,
|
||||||
|
|
@ -552,6 +566,7 @@ impl<C: XConnection> ServerState<C> {
|
||||||
xdg_wm_base,
|
xdg_wm_base,
|
||||||
clipboard_data,
|
clipboard_data,
|
||||||
last_kb_serial: None,
|
last_kb_serial: None,
|
||||||
|
activation_state,
|
||||||
global_output_offset: GlobalOutputOffset {
|
global_output_offset: GlobalOutputOffset {
|
||||||
x: GlobalOutputOffsetDimension {
|
x: GlobalOutputOffsetDimension {
|
||||||
owner: None,
|
owner: None,
|
||||||
|
|
@ -593,10 +608,23 @@ impl<C: XConnection> ServerState<C> {
|
||||||
override_redirect: bool,
|
override_redirect: bool,
|
||||||
dims: WindowDims,
|
dims: WindowDims,
|
||||||
parent: Option<x::Window>,
|
parent: Option<x::Window>,
|
||||||
|
pid: Option<u32>,
|
||||||
) {
|
) {
|
||||||
|
let activation_token = pid
|
||||||
|
.filter(|pid| self.pids.insert(*pid))
|
||||||
|
.and_then(|pid| std::fs::read(format!("/proc/{pid}/environ")).ok())
|
||||||
|
.and_then(|environ| {
|
||||||
|
environ
|
||||||
|
.split(|byte| *byte == 0)
|
||||||
|
.find_map(|line| line.strip_prefix(b"XDG_ACTIVATION_TOKEN="))
|
||||||
|
.and_then(|token| String::from_utf8(token.to_vec()).ok())
|
||||||
|
});
|
||||||
|
if activation_token.is_none() {
|
||||||
|
self.activate_window(window);
|
||||||
|
}
|
||||||
self.windows.insert(
|
self.windows.insert(
|
||||||
window,
|
window,
|
||||||
WindowData::new(window, override_redirect, dims, parent),
|
WindowData::new(window, override_redirect, dims, parent, activation_token),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -823,6 +851,41 @@ impl<C: XConnection> ServerState<C> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn activate_window(&mut self, window: x::Window) {
|
||||||
|
let Some(activation_state) = self.activation_state.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(last_focused_toplevel) = self.last_focused_toplevel else {
|
||||||
|
warn!("No last focused toplevel, cannot focus window {window:?}");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(win) = self.windows.get(&last_focused_toplevel) else {
|
||||||
|
warn!("Unknown last focused toplevel, cannot focus window {window:?}");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(key) = win.surface_key else {
|
||||||
|
warn!("Last focused toplevel has no surface, cannot focus window {window:?}");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(object) = self.objects.get_mut(key) else {
|
||||||
|
warn!("Last focused toplevel has stale reference, cannot focus window {window:?}");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let surface: &mut SurfaceData = object.as_mut();
|
||||||
|
activation_state.request_token_with_data(
|
||||||
|
&self.qh,
|
||||||
|
xdg_activation::ActivationData::new(
|
||||||
|
window,
|
||||||
|
smithay_client_toolkit::activation::RequestData {
|
||||||
|
app_id: win.attrs.class.clone(),
|
||||||
|
seat_and_serial: self.last_kb_serial.clone(),
|
||||||
|
surface: Some(surface.client.clone()),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn destroy_window(&mut self, window: x::Window) {
|
pub fn destroy_window(&mut self, window: x::Window) {
|
||||||
let _ = self.windows.remove(&window);
|
let _ = self.windows.remove(&window);
|
||||||
}
|
}
|
||||||
|
|
@ -839,7 +902,12 @@ impl<C: XConnection> ServerState<C> {
|
||||||
let CopyPasteData::X11 { inner, .. } = d.source.insert(data) else {
|
let CopyPasteData::X11 { inner, .. } = d.source.insert(data) else {
|
||||||
unreachable!();
|
unreachable!();
|
||||||
};
|
};
|
||||||
if let Some(serial) = self.last_kb_serial.as_ref().copied() {
|
if let Some(serial) = self
|
||||||
|
.last_kb_serial
|
||||||
|
.as_ref()
|
||||||
|
.map(|(_seat, serial)| serial)
|
||||||
|
.copied()
|
||||||
|
{
|
||||||
inner.set_selection(d.device.as_ref().unwrap(), serial);
|
inner.set_selection(d.device.as_ref().unwrap(), serial);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -920,6 +988,7 @@ impl<C: XConnection> ServerState<C> {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.handle_clipboard_events();
|
self.handle_clipboard_events();
|
||||||
|
self.handle_activations();
|
||||||
self.clientside
|
self.clientside
|
||||||
.queue
|
.queue
|
||||||
.flush()
|
.flush()
|
||||||
|
|
@ -967,6 +1036,24 @@ impl<C: XConnection> ServerState<C> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_activations(&mut self) {
|
||||||
|
let Some(activation_state) = self.activation_state.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let globals = &mut self.clientside.globals;
|
||||||
|
|
||||||
|
globals.pending_activations.retain(|(window, token)| {
|
||||||
|
if let Some(window) = self.windows.get(window) {
|
||||||
|
if let Some(key) = window.surface_key {
|
||||||
|
let surface: &SurfaceData = self.objects[key].as_ref();
|
||||||
|
activation_state.activate::<Self>(&surface.client, token.clone());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn calc_global_output_offset(&mut self) {
|
fn calc_global_output_offset(&mut self) {
|
||||||
for (key, _) in &self.output_keys {
|
for (key, _) in &self.output_keys {
|
||||||
let Some(object) = &self.objects.get(key) else {
|
let Some(object) = &self.objects.get(key) else {
|
||||||
|
|
@ -1134,6 +1221,14 @@ impl<C: XConnection> ServerState<C> {
|
||||||
toplevel.set_fullscreen(None);
|
toplevel.set_fullscreen(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let surface: &SurfaceData = self.objects[surface_key].as_ref();
|
||||||
|
if let (Some(activation_state), Some(token)) = (
|
||||||
|
self.activation_state.as_ref(),
|
||||||
|
window.activation_token.clone(),
|
||||||
|
) {
|
||||||
|
activation_state.activate::<Self>(&surface.client, token);
|
||||||
|
}
|
||||||
|
|
||||||
ToplevelData {
|
ToplevelData {
|
||||||
xdg: XdgSurfaceData {
|
xdg: XdgSurfaceData {
|
||||||
surface: xdg,
|
surface: xdg,
|
||||||
|
|
|
||||||
|
|
@ -515,7 +515,7 @@ impl TestFixture {
|
||||||
let dims = data.dims;
|
let dims = data.dims;
|
||||||
self.register_window(window, data);
|
self.register_window(window, data);
|
||||||
self.satellite
|
self.satellite
|
||||||
.new_window(window, override_redirect, dims, parent);
|
.new_window(window, override_redirect, dims, parent, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_window(
|
fn map_window(
|
||||||
|
|
@ -1118,6 +1118,7 @@ fn window_group_properties() {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
f.satellite
|
f.satellite
|
||||||
.set_win_title(prop_win, WmName::WmName("window".into()));
|
.set_win_title(prop_win, WmName::WmName("window".into()));
|
||||||
|
|
@ -1137,7 +1138,7 @@ fn window_group_properties() {
|
||||||
let (_, surface) = comp.create_surface();
|
let (_, surface) = comp.create_surface();
|
||||||
let dims = data.dims;
|
let dims = data.dims;
|
||||||
f.register_window(win, data);
|
f.register_window(win, data);
|
||||||
f.satellite.new_window(win, false, dims, None);
|
f.satellite.new_window(win, false, dims, None, None);
|
||||||
f.satellite.set_win_hints(
|
f.satellite.set_win_hints(
|
||||||
win,
|
win,
|
||||||
super::WmHints {
|
super::WmHints {
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ impl XState {
|
||||||
xcb::Extension::Composite,
|
xcb::Extension::Composite,
|
||||||
xcb::Extension::RandR,
|
xcb::Extension::RandR,
|
||||||
xcb::Extension::XFixes,
|
xcb::Extension::XFixes,
|
||||||
|
xcb::Extension::Res,
|
||||||
],
|
],
|
||||||
&[],
|
&[],
|
||||||
)
|
)
|
||||||
|
|
@ -301,7 +302,13 @@ impl XState {
|
||||||
} else {
|
} else {
|
||||||
Some(parent)
|
Some(parent)
|
||||||
};
|
};
|
||||||
server_state.new_window(e.window(), e.override_redirect(), (&e).into(), parent);
|
server_state.new_window(
|
||||||
|
e.window(),
|
||||||
|
e.override_redirect(),
|
||||||
|
(&e).into(),
|
||||||
|
parent,
|
||||||
|
self.get_pid(e.window()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
xcb::Event::X(x::Event::ReparentNotify(e)) => {
|
xcb::Event::X(x::Event::ReparentNotify(e)) => {
|
||||||
debug!("reparent event: {e:?}");
|
debug!("reparent event: {e:?}");
|
||||||
|
|
@ -313,6 +320,7 @@ impl XState {
|
||||||
attrs.override_redirect,
|
attrs.override_redirect,
|
||||||
attrs.dims,
|
attrs.dims,
|
||||||
None,
|
None,
|
||||||
|
self.get_pid(e.window()),
|
||||||
);
|
);
|
||||||
self.handle_window_attributes(server_state, e.window(), attrs);
|
self.handle_window_attributes(server_state, e.window(), attrs);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -447,6 +455,9 @@ impl XState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
x if x == self.atoms.active_win => {
|
||||||
|
server_state.activate_window(e.window());
|
||||||
|
}
|
||||||
t => warn!("unrecognized message: {t:?}"),
|
t => warn!("unrecognized message: {t:?}"),
|
||||||
},
|
},
|
||||||
xcb::Event::X(x::Event::MappingNotify(_)) => {}
|
xcb::Event::X(x::Event::MappingNotify(_)) => {}
|
||||||
|
|
@ -649,6 +660,24 @@ impl XState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_pid(&self, window: x::Window) -> Option<u32> {
|
||||||
|
let Some(pid) = self
|
||||||
|
.connection
|
||||||
|
.wait_for_reply(self.connection.send_request(&xcb::res::QueryClientIds {
|
||||||
|
specs: &[xcb::res::ClientIdSpec {
|
||||||
|
client: window.resource_id(),
|
||||||
|
mask: xcb::res::ClientIdMask::LOCAL_CLIENT_PID,
|
||||||
|
}],
|
||||||
|
}))
|
||||||
|
.ok()
|
||||||
|
.and_then(|reply| Some(*reply.ids().next()?.value().first()?))
|
||||||
|
else {
|
||||||
|
warn!("Failed to get pid of window: {window:?}");
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(pid)
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_property_change(
|
fn handle_property_change(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: x::PropertyNotifyEvent,
|
event: x::PropertyNotifyEvent,
|
||||||
|
|
|
||||||
|
|
@ -546,6 +546,17 @@ impl Connection {
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn send_client_message(&self, request: &x::ClientMessageEvent) {
|
||||||
|
self.send_and_check_request(&x::SendEvent {
|
||||||
|
propagate: false,
|
||||||
|
destination: x::SendEventDest::Window(self.root),
|
||||||
|
event_mask: x::EventMask::SUBSTRUCTURE_NOTIFY | x::EventMask::SUBSTRUCTURE_REDIRECT,
|
||||||
|
event: request,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -781,6 +792,28 @@ fn input_focus() {
|
||||||
f.wm_delete_window(&mut connection, win1, surface1);
|
f.wm_delete_window(&mut connection, win1, surface1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn activation_x11_to_x11() {
|
||||||
|
let mut f = Fixture::new();
|
||||||
|
let mut connection = Connection::new(&f.display);
|
||||||
|
|
||||||
|
let window1 = connection.new_window(connection.root, 0, 0, 20, 20, false);
|
||||||
|
let surface1 = f.map_as_toplevel(&mut connection, window1);
|
||||||
|
let window2 = connection.new_window(connection.root, 0, 0, 20, 20, false);
|
||||||
|
let surface2 = f.map_as_toplevel(&mut connection, window2);
|
||||||
|
|
||||||
|
f.testwl.focus_toplevel(surface2);
|
||||||
|
std::thread::sleep(Duration::from_millis(1));
|
||||||
|
connection.send_client_message(&x::ClientMessageEvent::new(
|
||||||
|
window1,
|
||||||
|
connection.atoms.net_active_window,
|
||||||
|
x::ClientMessageData::Data32([2, x::CURRENT_TIME, 0, 0, 0]),
|
||||||
|
));
|
||||||
|
f.wait_and_dispatch();
|
||||||
|
|
||||||
|
assert_eq!(f.testwl.get_focused(), Some(surface1));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quick_delete() {
|
fn quick_delete() {
|
||||||
let mut f = Fixture::new();
|
let mut f = Fixture::new();
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,10 @@ use wayland_protocols::{
|
||||||
viewporter::server::wp_viewporter::WpViewporter,
|
viewporter::server::wp_viewporter::WpViewporter,
|
||||||
},
|
},
|
||||||
xdg::{
|
xdg::{
|
||||||
|
activation::v1::server::{
|
||||||
|
xdg_activation_token_v1::{self, XdgActivationTokenV1},
|
||||||
|
xdg_activation_v1::{self, XdgActivationV1},
|
||||||
|
},
|
||||||
shell::server::{
|
shell::server::{
|
||||||
xdg_popup::{self, XdgPopup},
|
xdg_popup::{self, XdgPopup},
|
||||||
xdg_positioner::{self, XdgPositioner},
|
xdg_positioner::{self, XdgPositioner},
|
||||||
|
|
@ -176,6 +180,14 @@ struct KeyboardState {
|
||||||
current_focus: Option<SurfaceId>,
|
current_focus: Option<SurfaceId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct ActivationTokenData {
|
||||||
|
serial: Option<(u32, WlSeat)>,
|
||||||
|
app_id: Option<String>,
|
||||||
|
surface: Option<WlSurface>,
|
||||||
|
constructed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
surfaces: HashMap<SurfaceId, SurfaceData>,
|
surfaces: HashMap<SurfaceId, SurfaceData>,
|
||||||
outputs: HashMap<WlOutput, Output>,
|
outputs: HashMap<WlOutput, Output>,
|
||||||
|
|
@ -185,12 +197,16 @@ struct State {
|
||||||
last_surface_id: Option<SurfaceId>,
|
last_surface_id: Option<SurfaceId>,
|
||||||
last_output: Option<WlOutput>,
|
last_output: Option<WlOutput>,
|
||||||
callbacks: Vec<WlCallback>,
|
callbacks: Vec<WlCallback>,
|
||||||
|
seat: Option<WlSeat>,
|
||||||
pointer: Option<WlPointer>,
|
pointer: Option<WlPointer>,
|
||||||
keyboard: Option<KeyboardState>,
|
keyboard: Option<KeyboardState>,
|
||||||
configure_serial: u32,
|
configure_serial: u32,
|
||||||
selection: Option<WlDataSource>,
|
selection: Option<WlDataSource>,
|
||||||
data_device_man: Option<WlDataDeviceManager>,
|
data_device_man: Option<WlDataDeviceManager>,
|
||||||
data_device: Option<WlDataDevice>,
|
data_device: Option<WlDataDevice>,
|
||||||
|
xdg_activation: Option<XdgActivationV1>,
|
||||||
|
valid_tokens: HashSet<String>,
|
||||||
|
token_counter: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for State {
|
impl Default for State {
|
||||||
|
|
@ -204,12 +220,16 @@ impl Default for State {
|
||||||
last_surface_id: None,
|
last_surface_id: None,
|
||||||
last_output: None,
|
last_output: None,
|
||||||
callbacks: Vec::new(),
|
callbacks: Vec::new(),
|
||||||
|
seat: None,
|
||||||
pointer: None,
|
pointer: None,
|
||||||
keyboard: None,
|
keyboard: None,
|
||||||
configure_serial: 0,
|
configure_serial: 0,
|
||||||
selection: None,
|
selection: None,
|
||||||
data_device_man: None,
|
data_device_man: None,
|
||||||
data_device: None,
|
data_device: None,
|
||||||
|
xdg_activation: None,
|
||||||
|
valid_tokens: HashSet::new(),
|
||||||
|
token_counter: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -246,11 +266,9 @@ impl State {
|
||||||
keyboard.leave(self.configure_serial, &self.surfaces[id].surface);
|
keyboard.leave(self.configure_serial, &self.surfaces[id].surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
keyboard.enter(
|
let surface = self.surfaces.get_mut(&surface_id).unwrap();
|
||||||
self.configure_serial,
|
keyboard.enter(self.configure_serial, &surface.surface, Vec::default());
|
||||||
&self.surfaces[&surface_id].surface,
|
surface.last_enter_serial = Some(self.configure_serial);
|
||||||
Vec::default(),
|
|
||||||
);
|
|
||||||
|
|
||||||
*current_focus = Some(surface_id);
|
*current_focus = Some(surface_id);
|
||||||
}
|
}
|
||||||
|
|
@ -267,6 +285,10 @@ impl State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_focused(&self) -> Option<SurfaceId> {
|
||||||
|
self.keyboard.as_ref()?.current_focus
|
||||||
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn configure_popup(&mut self, surface_id: SurfaceId) {
|
fn configure_popup(&mut self, surface_id: SurfaceId) {
|
||||||
let surface = self.surfaces.get_mut(&surface_id).unwrap();
|
let surface = self.surfaces.get_mut(&surface_id).unwrap();
|
||||||
|
|
@ -360,6 +382,7 @@ impl Server {
|
||||||
dh.create_global::<State, WlSeat, _>(5, ());
|
dh.create_global::<State, WlSeat, _>(5, ());
|
||||||
dh.create_global::<State, WlDataDeviceManager, _>(3, ());
|
dh.create_global::<State, WlDataDeviceManager, _>(3, ());
|
||||||
dh.create_global::<State, ZwpTabletManagerV2, _>(1, ());
|
dh.create_global::<State, ZwpTabletManagerV2, _>(1, ());
|
||||||
|
dh.create_global::<State, XdgActivationV1, _>(1, ());
|
||||||
global_noop!(ZwpLinuxDmabufV1);
|
global_noop!(ZwpLinuxDmabufV1);
|
||||||
global_noop!(ZwpRelativePointerManagerV1);
|
global_noop!(ZwpRelativePointerManagerV1);
|
||||||
global_noop!(WpViewporter);
|
global_noop!(WpViewporter);
|
||||||
|
|
@ -495,6 +518,11 @@ impl Server {
|
||||||
self.display.flush_clients().unwrap();
|
self.display.flush_clients().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn get_focused(&self) -> Option<SurfaceId> {
|
||||||
|
self.state.get_focused()
|
||||||
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn configure_popup(&mut self, surface_id: SurfaceId) {
|
pub fn configure_popup(&mut self, surface_id: SurfaceId) {
|
||||||
self.state.configure_popup(surface_id);
|
self.state.configure_popup(surface_id);
|
||||||
|
|
@ -971,7 +999,7 @@ impl Dispatch<WlDataDeviceManager, ()> for State {
|
||||||
|
|
||||||
impl GlobalDispatch<WlSeat, ()> for State {
|
impl GlobalDispatch<WlSeat, ()> for State {
|
||||||
fn bind(
|
fn bind(
|
||||||
_: &mut Self,
|
state: &mut Self,
|
||||||
_: &DisplayHandle,
|
_: &DisplayHandle,
|
||||||
_: &Client,
|
_: &Client,
|
||||||
resource: wayland_server::New<WlSeat>,
|
resource: wayland_server::New<WlSeat>,
|
||||||
|
|
@ -980,6 +1008,7 @@ impl GlobalDispatch<WlSeat, ()> for State {
|
||||||
) {
|
) {
|
||||||
let seat = data_init.init(resource, ());
|
let seat = data_init.init(resource, ());
|
||||||
seat.capabilities(wl_seat::Capability::Pointer | wl_seat::Capability::Keyboard);
|
seat.capabilities(wl_seat::Capability::Pointer | wl_seat::Capability::Keyboard);
|
||||||
|
state.seat = Some(seat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1524,3 +1553,126 @@ impl Dispatch<WlCallback, ()> for State {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl GlobalDispatch<XdgActivationV1, ()> for State {
|
||||||
|
fn bind(
|
||||||
|
state: &mut Self,
|
||||||
|
_: &DisplayHandle,
|
||||||
|
_: &Client,
|
||||||
|
resource: wayland_server::New<XdgActivationV1>,
|
||||||
|
_: &(),
|
||||||
|
data_init: &mut wayland_server::DataInit<'_, Self>,
|
||||||
|
) {
|
||||||
|
state.xdg_activation = Some(data_init.init(resource, ()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<XdgActivationV1, ()> for State {
|
||||||
|
fn request(
|
||||||
|
state: &mut Self,
|
||||||
|
_: &Client,
|
||||||
|
_: &XdgActivationV1,
|
||||||
|
request: <XdgActivationV1 as Resource>::Request,
|
||||||
|
_: &(),
|
||||||
|
_: &DisplayHandle,
|
||||||
|
data_init: &mut wayland_server::DataInit<'_, Self>,
|
||||||
|
) {
|
||||||
|
match request {
|
||||||
|
xdg_activation_v1::Request::Destroy => {}
|
||||||
|
xdg_activation_v1::Request::GetActivationToken { id } => {
|
||||||
|
data_init.init(id, Mutex::new(ActivationTokenData::default()));
|
||||||
|
}
|
||||||
|
xdg_activation_v1::Request::Activate { token, surface } => {
|
||||||
|
if state.valid_tokens.remove(&token) {
|
||||||
|
let surface_id = SurfaceId(surface.id().protocol_id());
|
||||||
|
state.focus_toplevel(surface_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<XdgActivationTokenV1, Mutex<ActivationTokenData>> for State {
|
||||||
|
fn request(
|
||||||
|
state: &mut Self,
|
||||||
|
_: &Client,
|
||||||
|
token: &XdgActivationTokenV1,
|
||||||
|
request: <XdgActivationTokenV1 as Resource>::Request,
|
||||||
|
data: &Mutex<ActivationTokenData>,
|
||||||
|
_: &DisplayHandle,
|
||||||
|
_: &mut wayland_server::DataInit<'_, Self>,
|
||||||
|
) {
|
||||||
|
let mut data = data.lock().unwrap();
|
||||||
|
match request {
|
||||||
|
xdg_activation_token_v1::Request::SetSerial { serial, seat } => {
|
||||||
|
if data.constructed {
|
||||||
|
token.post_error(
|
||||||
|
xdg_activation_token_v1::Error::AlreadyUsed,
|
||||||
|
"The activation token has already been constructed",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.serial = Some((serial, seat));
|
||||||
|
}
|
||||||
|
xdg_activation_token_v1::Request::SetAppId { app_id } => {
|
||||||
|
if data.constructed {
|
||||||
|
token.post_error(
|
||||||
|
xdg_activation_token_v1::Error::AlreadyUsed,
|
||||||
|
"The activation token has already been constructed",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.app_id = Some(app_id);
|
||||||
|
}
|
||||||
|
xdg_activation_token_v1::Request::SetSurface { surface } => {
|
||||||
|
if data.constructed {
|
||||||
|
token.post_error(
|
||||||
|
xdg_activation_token_v1::Error::AlreadyUsed,
|
||||||
|
"The activation token has already been constructed",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.surface = Some(surface);
|
||||||
|
}
|
||||||
|
xdg_activation_token_v1::Request::Commit => {
|
||||||
|
if data.constructed {
|
||||||
|
token.post_error(
|
||||||
|
xdg_activation_token_v1::Error::AlreadyUsed,
|
||||||
|
"The activation token has already been constructed",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.constructed = true;
|
||||||
|
|
||||||
|
// Require a valid serial, otherwise ignore the activation.
|
||||||
|
// This matches niri's behavior: https://github.com/YaLTeR/niri/blob/5e549e13238a853f8860e29621ab6b31ee1b9ee4/src/handlers/mod.rs#L712-L723
|
||||||
|
let valid = if let (Some((serial, seat)), Some(surface_data)) = (
|
||||||
|
data.serial.take(),
|
||||||
|
data.surface.take().and_then(|surface| {
|
||||||
|
state.surfaces.get(&SurfaceId(surface.id().protocol_id()))
|
||||||
|
}),
|
||||||
|
) {
|
||||||
|
state.seat == Some(seat)
|
||||||
|
&& surface_data
|
||||||
|
.last_enter_serial
|
||||||
|
.is_some_and(|last_enter| serial >= last_enter)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
let activation_token = state.token_counter.to_string();
|
||||||
|
state.token_counter += 1;
|
||||||
|
if valid {
|
||||||
|
state.valid_tokens.insert(activation_token.clone());
|
||||||
|
}
|
||||||
|
token.done(activation_token);
|
||||||
|
}
|
||||||
|
xdg_activation_token_v1::Request::Destroy => {}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue