Support XDG Activation

Test XDG Activation
This commit is contained in:
bbb651 2025-03-03 00:18:27 +02:00 committed by Shawn Wallace
parent b34b08f004
commit 180efb0ba9
10 changed files with 413 additions and 27 deletions

View file

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

View file

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

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

View file

@ -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 } => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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!(),
}
}
}