From 5f6eb1a3e875fbee4e18599811a6630985dcf6ad Mon Sep 17 00:00:00 2001
From: Victoria Brekenfeld <victoria@system76.com>
Date: Tue, 7 Nov 2023 18:46:25 +0100
Subject: [PATCH] xdg-activation: Initial implementation

---
 src/shell/focus/mod.rs                 |  12 +-
 src/shell/layout/floating/mod.rs       |   2 +-
 src/shell/mod.rs                       | 184 +++++++++++++++++++++----
 src/shell/workspace.rs                 |  10 +-
 src/wayland/handlers/mod.rs            |   1 +
 src/wayland/handlers/xdg_activation.rs | 147 ++++++++++++++++++++
 src/wayland/handlers/xdg_shell/mod.rs  |   2 +-
 src/xwayland.rs                        |  69 ++++++++--
 8 files changed, 380 insertions(+), 47 deletions(-)
 create mode 100644 src/wayland/handlers/xdg_activation.rs

diff --git a/src/shell/focus/mod.rs b/src/shell/focus/mod.rs
index ba389d411..542d8d04a 100644
--- a/src/shell/focus/mod.rs
+++ b/src/shell/focus/mod.rs
@@ -91,11 +91,10 @@ impl ActiveFocus {
 }
 
 impl Shell {
-    pub fn set_focus<'a>(
+    pub fn append_focus_stack(
         state: &mut State,
         target: Option<&KeyboardFocusTarget>,
         active_seat: &Seat<State>,
-        serial: Option<Serial>,
     ) {
         // update FocusStack and notify layouts about new focus (if any window)
         let element = match target {
@@ -125,6 +124,15 @@ impl Shell {
                 }
             }
         }
+    }
+
+    pub fn set_focus(
+        state: &mut State,
+        target: Option<&KeyboardFocusTarget>,
+        active_seat: &Seat<State>,
+        serial: Option<Serial>,
+    ) {
+        Self::append_focus_stack(state, target, active_seat);
 
         // update keyboard focus
         if let Some(keyboard) = active_seat.get_keyboard() {
diff --git a/src/shell/layout/floating/mod.rs b/src/shell/layout/floating/mod.rs
index 75d847d7a..0676ae2a3 100644
--- a/src/shell/layout/floating/mod.rs
+++ b/src/shell/layout/floating/mod.rs
@@ -35,7 +35,7 @@ pub use self::grabs::*;
 
 #[derive(Debug, Default)]
 pub struct FloatingLayout {
-    pub(in crate::shell) space: Space<CosmicMapped>,
+    pub(crate) space: Space<CosmicMapped>,
 }
 
 impl FloatingLayout {
diff --git a/src/shell/mod.rs b/src/shell/mod.rs
index 6a5ba211b..d315adbb1 100644
--- a/src/shell/mod.rs
+++ b/src/shell/mod.rs
@@ -30,6 +30,7 @@ use smithay::{
             },
             xdg::XdgShellState,
         },
+        xdg_activation::XdgActivationState,
     },
     xwayland::X11Surface,
 };
@@ -38,12 +39,15 @@ use crate::{
     config::{Config, KeyModifiers, KeyPattern},
     state::client_should_see_privileged_protocols,
     utils::prelude::*,
-    wayland::protocols::{
-        toplevel_info::ToplevelInfoState,
-        toplevel_management::{ManagementCapabilities, ToplevelManagementState},
-        workspace::{
-            WorkspaceCapabilities, WorkspaceGroupHandle, WorkspaceHandle, WorkspaceState,
-            WorkspaceUpdateGuard,
+    wayland::{
+        handlers::xdg_activation::ActivationContext,
+        protocols::{
+            toplevel_info::ToplevelInfoState,
+            toplevel_management::{ManagementCapabilities, ToplevelManagementState},
+            workspace::{
+                WorkspaceCapabilities, WorkspaceGroupHandle, WorkspaceHandle, WorkspaceState,
+                WorkspaceUpdateGuard,
+            },
         },
     },
 };
@@ -145,6 +149,22 @@ pub enum MaximizeMode {
     OnTop,
 }
 
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum ActivationKey {
+    Wayland(WlSurface),
+    X11(u32),
+}
+
+impl From<&CosmicSurface> for ActivationKey {
+    fn from(value: &CosmicSurface) -> Self {
+        match value {
+            CosmicSurface::Wayland(w) => ActivationKey::Wayland(w.toplevel().wl_surface().clone()),
+            CosmicSurface::X11(s) => ActivationKey::X11(s.window_id()),
+            _ => unreachable!(),
+        }
+    }
+}
+
 #[derive(Debug)]
 pub struct Shell {
     pub workspaces: Workspaces,
@@ -153,6 +173,7 @@ pub struct Shell {
     pub maximize_mode: MaximizeMode,
     pub pending_windows: Vec<(CosmicSurface, Seat<State>, Option<Output>)>,
     pub pending_layers: Vec<(LayerSurface, Output, Seat<State>)>,
+    pub pending_activations: HashMap<ActivationKey, ActivationContext>,
     pub override_redirect_windows: Vec<X11Surface>,
 
     // wayland_state
@@ -160,6 +181,7 @@ pub struct Shell {
     pub toplevel_info_state: ToplevelInfoState<State, CosmicSurface>,
     pub toplevel_management_state: ToplevelManagementState,
     pub xdg_shell_state: XdgShellState,
+    pub xdg_activation_state: XdgActivationState,
     pub workspace_state: WorkspaceState<State>,
 
     theme: cosmic::Theme,
@@ -279,6 +301,8 @@ impl WorkspaceSet {
         if self.active != idx {
             let old_active = self.active;
             state.remove_workspace_state(&self.workspaces[old_active].handle, WState::Active);
+            state.remove_workspace_state(&self.workspaces[old_active].handle, WState::Urgent);
+            state.remove_workspace_state(&self.workspaces[idx].handle, WState::Urgent);
             state.add_workspace_state(&self.workspaces[idx].handle, WState::Active);
             self.previously_active = Some((old_active, Instant::now()));
             self.active = idx;
@@ -299,13 +323,13 @@ impl WorkspaceSet {
         self.output = new_output.clone();
     }
 
-    fn refresh<'a>(&mut self) {
+    fn refresh<'a>(&mut self, xdg_activation_state: &XdgActivationState) {
         if let Some((_, start)) = self.previously_active {
             if Instant::now().duration_since(start).as_millis() >= ANIMATION_DURATION.as_millis() {
                 self.previously_active = None;
             }
         } else {
-            self.workspaces[self.active].refresh();
+            self.workspaces[self.active].refresh(xdg_activation_state);
         }
     }
 
@@ -332,7 +356,7 @@ impl WorkspaceSet {
         if self
             .workspaces
             .last()
-            .map(|last| last.windows().next().is_some())
+            .map(|last| !last.pending_tokens.is_empty() || last.windows().next().is_some())
             .unwrap_or(true)
         {
             self.add_empty_workspace(state);
@@ -342,7 +366,8 @@ impl WorkspaceSet {
         let len = self.workspaces.len();
         let mut keep = vec![true; len];
         for (i, workspace) in self.workspaces.iter().enumerate() {
-            let has_windows = workspace.windows().next().is_some();
+            let has_windows =
+                !workspace.pending_tokens.is_empty() || workspace.windows().next().is_some();
 
             if !has_windows && i != self.active && i != len - 1 {
                 state.remove_workspace(workspace.handle);
@@ -370,6 +395,7 @@ impl WorkspaceSet {
         amount: usize,
         state: &mut WorkspaceUpdateGuard<State>,
         toplevel_info: &mut ToplevelInfoState<State, CosmicSurface>,
+        xdg_activation_state: &XdgActivationState,
     ) {
         if amount < self.workspaces.len() {
             // merge last ones
@@ -401,7 +427,7 @@ impl WorkspaceSet {
                 state.remove_workspace(workspace.handle);
             }
 
-            last_space.refresh();
+            last_space.refresh(xdg_activation_state);
         } else if amount > self.workspaces.len() {
             // add empty ones
             while amount > self.workspaces.len() {
@@ -508,6 +534,7 @@ impl Workspaces {
         seats: impl Iterator<Item = Seat<State>>,
         workspace_state: &mut WorkspaceUpdateGuard<'_, State>,
         toplevel_info_state: &mut ToplevelInfoState<State, CosmicSurface>,
+        xdg_activation_state: &XdgActivationState,
     ) {
         if !self.sets.contains_key(output) {
             return;
@@ -557,7 +584,7 @@ impl Workspaces {
 
                     // update mapping
                     workspace.set_output(&new_output, toplevel_info_state);
-                    workspace.refresh();
+                    workspace.refresh(xdg_activation_state);
 
                     // TODO: merge if mode = static
                     new_set.workspaces.push(workspace);
@@ -576,7 +603,7 @@ impl Workspaces {
                 self.backup_set = Some(set);
             }
 
-            self.refresh(workspace_state, toplevel_info_state)
+            self.refresh(workspace_state, toplevel_info_state, xdg_activation_state)
         }
     }
 
@@ -585,6 +612,7 @@ impl Workspaces {
         config: &Config,
         workspace_state: &mut WorkspaceUpdateGuard<'_, State>,
         toplevel_info_state: &mut ToplevelInfoState<State, CosmicSurface>,
+        xdg_activation_state: &XdgActivationState,
     ) {
         let old_mode = self.mode;
 
@@ -650,13 +678,14 @@ impl Workspaces {
             _ => {}
         };
 
-        self.refresh(workspace_state, toplevel_info_state)
+        self.refresh(workspace_state, toplevel_info_state, xdg_activation_state)
     }
 
     pub fn refresh(
         &mut self,
         workspace_state: &mut WorkspaceUpdateGuard<'_, State>,
         toplevel_info_state: &mut ToplevelInfoState<State, CosmicSurface>,
+        xdg_activation_state: &XdgActivationState,
     ) {
         match self.mode {
             WorkspaceMode::Global => {
@@ -696,10 +725,10 @@ impl Workspaces {
                         let mut active = self.sets[0].active;
                         let mut keep = vec![true; len];
                         for i in 0..len {
-                            let has_windows = self
-                                .sets
-                                .values()
-                                .any(|s| s.workspaces[i].windows().next().is_some());
+                            let has_windows = self.sets.values().any(|s| {
+                                !s.workspaces[i].pending_tokens.is_empty()
+                                    || s.workspaces[i].windows().next().is_some()
+                            });
 
                             if !has_windows && i != active && i != len - 1 {
                                 for workspace in self.sets.values().map(|s| &s.workspaces[i]) {
@@ -733,7 +762,12 @@ impl Workspaces {
                     }
                     WorkspaceAmount::Static(amount) => {
                         for set in self.sets.values_mut() {
-                            set.ensure_static(amount as usize, workspace_state, toplevel_info_state)
+                            set.ensure_static(
+                                amount as usize,
+                                workspace_state,
+                                toplevel_info_state,
+                                xdg_activation_state,
+                            )
                         }
                     }
                 }
@@ -746,14 +780,19 @@ impl Workspaces {
                 }
                 WorkspaceAmount::Static(amount) => {
                     for set in self.sets.values_mut() {
-                        set.ensure_static(amount as usize, workspace_state, toplevel_info_state)
+                        set.ensure_static(
+                            amount as usize,
+                            workspace_state,
+                            toplevel_info_state,
+                            xdg_activation_state,
+                        )
                     }
                 }
             },
         }
 
         for set in self.sets.values_mut() {
-            set.refresh()
+            set.refresh(xdg_activation_state)
         }
     }
 
@@ -826,7 +865,7 @@ impl Workspaces {
         }
     }
 
-    pub fn set_theme(&mut self, theme: cosmic::Theme) {
+    pub fn set_theme(&mut self, theme: cosmic::Theme, xdg_activation_state: &XdgActivationState) {
         for (_, s) in &mut self.sets {
             s.theme = theme.clone();
             for mut w in &mut s.workspaces {
@@ -837,7 +876,7 @@ impl Workspaces {
                     m.force_redraw();
                 });
 
-                w.refresh();
+                w.refresh(xdg_activation_state);
                 w.dirty.store(true, Ordering::Relaxed);
                 w.recalculate();
             }
@@ -854,6 +893,7 @@ impl Shell {
             client_should_see_privileged_protocols,
         );
         let xdg_shell_state = XdgShellState::new::<State>(dh);
+        let xdg_activation_state = XdgActivationState::new::<State>(dh);
         let toplevel_info_state =
             ToplevelInfoState::new(dh, client_should_see_privileged_protocols);
         let toplevel_management_state = ToplevelManagementState::new::<State, _>(
@@ -874,12 +914,14 @@ impl Shell {
 
             pending_windows: Vec::new(),
             pending_layers: Vec::new(),
+            pending_activations: HashMap::new(),
             override_redirect_windows: Vec::new(),
 
             layer_shell_state,
             toplevel_info_state,
             toplevel_management_state,
             xdg_shell_state,
+            xdg_activation_state,
             workspace_state,
 
             theme,
@@ -905,6 +947,7 @@ impl Shell {
             seats,
             &mut self.workspace_state.update(),
             &mut self.toplevel_info_state,
+            &self.xdg_activation_state,
         );
         self.refresh(); // cleans up excess of workspaces and empty workspaces
     }
@@ -912,8 +955,12 @@ impl Shell {
     pub fn update_config(&mut self, config: &Config) {
         let mut workspace_state = self.workspace_state.update();
         let toplevel_info_state = &mut self.toplevel_info_state;
-        self.workspaces
-            .update_config(config, &mut workspace_state, toplevel_info_state);
+        self.workspaces.update_config(
+            config,
+            &mut workspace_state,
+            toplevel_info_state,
+            &self.xdg_activation_state,
+        );
     }
 
     pub fn activate(
@@ -958,6 +1005,12 @@ impl Shell {
         self.workspaces.active_mut(output)
     }
 
+    pub fn refresh_active_space(&mut self, output: &Output) {
+        self.workspaces
+            .active_mut(output)
+            .refresh(&self.xdg_activation_state)
+    }
+
     pub fn visible_outputs_for_surface<'a>(
         &'a self,
         surface: &'a WlSurface,
@@ -1190,9 +1243,13 @@ impl Shell {
 
         self.popups.cleanup();
 
+        self.xdg_activation_state.retain_tokens(|_, data| {
+            Instant::now().duration_since(data.timestamp) < Duration::from_secs(5)
+        });
         self.workspaces.refresh(
             &mut self.workspace_state.update(),
             &mut self.toplevel_info_state,
+            &self.xdg_activation_state,
         );
 
         for output in self.outputs() {
@@ -1258,10 +1315,42 @@ impl Shell {
             .unwrap();
         let (window, seat, output) = state.common.shell.pending_windows.remove(pos);
 
+        let pending_activation = state
+            .common
+            .shell
+            .pending_activations
+            .remove(&(&window).into());
+        let workspace_handle = match pending_activation {
+            Some(ActivationContext::Workspace(handle)) => Some(handle),
+            _ => None,
+        };
+
         let should_be_fullscreen = output.is_some();
-        let output = output.unwrap_or_else(|| seat.active_output());
+        let mut output = output.unwrap_or_else(|| seat.active_output());
+
+        // this is beyond stupid, just to make the borrow checker happy
+        let workspace = if let Some(handle) = workspace_handle.filter(|handle| {
+            state
+                .common
+                .shell
+                .workspaces
+                .spaces()
+                .any(|space| &space.handle == handle)
+        }) {
+            state
+                .common
+                .shell
+                .workspaces
+                .spaces_mut()
+                .find(|space| space.handle == handle)
+                .unwrap()
+        } else {
+            state.common.shell.workspaces.active_mut(&output)
+        };
+        if output != workspace.output {
+            output = workspace.output.clone();
+        }
 
-        let workspace = state.common.shell.workspaces.active_mut(&output);
         if let Some((mapped, layer, previous_workspace)) = workspace.remove_fullscreen() {
             let old_handle = workspace.handle.clone();
             let new_workspace_handle = state
@@ -1280,7 +1369,26 @@ impl Shell {
             );
         };
 
-        let workspace = state.common.shell.workspaces.active_mut(&output);
+        let active_handle = state.common.shell.workspaces.active(&output).1.handle;
+        let workspace = if let Some(handle) = workspace_handle.filter(|handle| {
+            state
+                .common
+                .shell
+                .workspaces
+                .spaces()
+                .any(|space| &space.handle == handle)
+        }) {
+            state
+                .common
+                .shell
+                .workspaces
+                .spaces_mut()
+                .find(|space| space.handle == handle)
+                .unwrap()
+        } else {
+            state.common.shell.workspaces.active_mut(&output)
+        };
+
         state.common.shell.toplevel_info_state.new_toplevel(&window);
         state
             .common
@@ -1302,6 +1410,9 @@ impl Shell {
         {
             mapped.set_debug(state.common.egui.active);
         }
+
+        let workspace_empty = workspace.mapped().next().is_none();
+
         if layout::should_be_floating(&window) || !workspace.tiling_enabled {
             workspace.floating_layer.map(mapped.clone(), None);
         } else {
@@ -1324,7 +1435,14 @@ impl Shell {
             workspace.fullscreen_request(&mapped.active_window(), None);
         }
 
-        Shell::set_focus(state, Some(&KeyboardFocusTarget::from(mapped)), &seat, None);
+        if workspace.output == seat.active_output() && active_handle == workspace.handle {
+            // TODO: enforce focus stealing prevention by also checking the same rules as for the else case.
+            Shell::set_focus(state, Some(&KeyboardFocusTarget::from(mapped)), &seat, None);
+        } else if workspace_empty || workspace_handle.is_some() || should_be_fullscreen {
+            let handle = workspace.handle;
+            Shell::append_focus_stack(state, Some(&KeyboardFocusTarget::from(mapped)), &seat);
+            state.common.shell.set_urgent(&handle);
+        }
 
         let active_space = state.common.shell.active_space(&output);
         for mapped in active_space.mapped() {
@@ -1666,7 +1784,13 @@ impl Shell {
     pub(crate) fn set_theme(&mut self, theme: cosmic::Theme) {
         self.theme = theme.clone();
         self.refresh();
-        self.workspaces.set_theme(theme.clone());
+        self.workspaces
+            .set_theme(theme.clone(), &self.xdg_activation_state);
+    }
+
+    pub fn set_urgent(&mut self, workspace: &WorkspaceHandle) {
+        let mut workspace_guard = self.workspace_state.update();
+        workspace_guard.add_workspace_state(workspace, WState::Urgent);
     }
 }
 
diff --git a/src/shell/workspace.rs b/src/shell/workspace.rs
index 3f1de3790..916d274f1 100644
--- a/src/shell/workspace.rs
+++ b/src/shell/workspace.rs
@@ -43,11 +43,12 @@ use smithay::{
     wayland::{
         compositor::{add_blocker, Blocker, BlockerState},
         seat::WaylandFocus,
+        xdg_activation::{XdgActivationState, XdgActivationToken},
     },
     xwayland::X11Surface,
 };
 use std::{
-    collections::{HashMap, VecDeque},
+    collections::{HashMap, HashSet, VecDeque},
     sync::{
         atomic::{AtomicBool, Ordering},
         Arc,
@@ -87,6 +88,7 @@ pub struct Workspace {
     pub pending_buffers: Vec<(ScreencopySession, BufferParams)>,
     pub screencopy_sessions: Vec<DropableSession>,
     pub output_stack: VecDeque<String>,
+    pub pending_tokens: HashSet<XdgActivationToken>,
     pub(super) backdrop_id: Id,
     pub dirty: AtomicBool,
 }
@@ -227,12 +229,13 @@ impl Workspace {
             pending_buffers: Vec::new(),
             screencopy_sessions: Vec::new(),
             output_stack: VecDeque::new(),
+            pending_tokens: HashSet::new(),
             backdrop_id: Id::new(),
             dirty: AtomicBool::new(false),
         }
     }
 
-    pub fn refresh(&mut self) {
+    pub fn refresh(&mut self, xdg_activation_state: &XdgActivationState) {
         #[cfg(feature = "debug")]
         puffin::profile_function!();
 
@@ -243,6 +246,9 @@ impl Workspace {
 
         self.floating_layer.refresh();
         self.tiling_layer.refresh();
+
+        self.pending_tokens
+            .retain(|token| xdg_activation_state.data_for_token(token).is_some());
     }
 
     pub fn refresh_focus_stack(&mut self) {
diff --git a/src/wayland/handlers/mod.rs b/src/wayland/handlers/mod.rs
index 14aa5308b..1ce631f86 100644
--- a/src/wayland/handlers/mod.rs
+++ b/src/wayland/handlers/mod.rs
@@ -27,5 +27,6 @@ pub mod toplevel_management;
 pub mod viewporter;
 pub mod wl_drm;
 pub mod workspace;
+pub mod xdg_activation;
 pub mod xdg_shell;
 pub mod xwayland_keyboard_grab;
diff --git a/src/wayland/handlers/xdg_activation.rs b/src/wayland/handlers/xdg_activation.rs
new file mode 100644
index 000000000..642ec3df5
--- /dev/null
+++ b/src/wayland/handlers/xdg_activation.rs
@@ -0,0 +1,147 @@
+use smithay::{
+    delegate_xdg_activation,
+    input::Seat,
+    reexports::wayland_server::protocol::wl_surface::WlSurface,
+    wayland::xdg_activation::{
+        XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData,
+    },
+};
+use tracing::debug;
+
+use crate::{shell::ActivationKey, state::ClientState, utils::prelude::*};
+use crate::{state::State, wayland::protocols::workspace::WorkspaceHandle};
+
+#[derive(Debug, Clone, Copy)]
+pub enum ActivationContext {
+    UrgentOnly,
+    Workspace(WorkspaceHandle),
+}
+
+impl XdgActivationHandler for State {
+    fn activation_state(&mut self) -> &mut XdgActivationState {
+        &mut self.common.shell.xdg_activation_state
+    }
+
+    fn token_created(&mut self, token: XdgActivationToken, data: XdgActivationTokenData) -> bool {
+        // Privileged clients always get valid tokens
+        if self
+            .common
+            .display_handle
+            .backend_handle()
+            .get_client_data(data.client_id)
+            .ok()
+            .and_then(|data| {
+                data.downcast_ref::<ClientState>()
+                    .map(|data| data.privileged)
+            })
+            .unwrap_or(false)
+        {
+            if let Some(seat) = data.serial.and_then(|(_, seat)| Seat::from_resource(&seat)) {
+                let output = seat.active_output();
+                let workspace = self.common.shell.active_space_mut(&output);
+                workspace.pending_tokens.insert(token.clone());
+                let handle = workspace.handle;
+                data.user_data
+                    .insert_if_missing(move || ActivationContext::Workspace(handle));
+                debug!(?token, "created workspace token for privileged client");
+            } else {
+                data.user_data
+                    .insert_if_missing(|| ActivationContext::UrgentOnly);
+                debug!(
+                    ?token,
+                    "created urgent-only token for privileged client without seat"
+                );
+            }
+
+            return true;
+        };
+
+        // Tokens without validation aren't allowed to steal focus
+        let Some((serial, seat)) = data.serial else {
+            data.user_data.insert_if_missing(|| ActivationContext::UrgentOnly);
+            debug!(?token, "created urgent-only token for missing seat/serial");
+            return true
+        };
+        let Some(seat) = Seat::from_resource(&seat) else {
+            data.user_data.insert_if_missing(|| ActivationContext::UrgentOnly);
+            debug!(?token, "created urgent-only token for unknown seat");
+            return true
+        };
+
+        // At this point we don't bother with urgent-only tokens.
+        // If the client provides a bad serial, it should be fixed.
+
+        let keyboard = seat.get_keyboard().unwrap();
+        let valid = keyboard
+            .last_enter()
+            .map(|last_enter| serial.is_no_older_than(&last_enter))
+            .unwrap_or(false);
+
+        if valid {
+            let output = seat.active_output();
+            let workspace = self.common.shell.active_space_mut(&output);
+            workspace.pending_tokens.insert(token.clone());
+            let handle = workspace.handle;
+            data.user_data
+                .insert_if_missing(move || ActivationContext::Workspace(handle));
+
+            debug!(?token, "created workspace token");
+        } else {
+            debug!(?token, "created urgent-only token for invalid serial");
+        }
+
+        valid
+    }
+
+    fn request_activation(
+        &mut self,
+        _token: XdgActivationToken,
+        token_data: XdgActivationTokenData,
+        surface: WlSurface,
+    ) {
+        if let Some(context) = token_data.user_data.get::<ActivationContext>() {
+            if let Some(element) = self.common.shell.element_for_wl_surface(&surface).cloned() {
+                match context {
+                    ActivationContext::UrgentOnly => {
+                        if let Some((workspace, _output)) =
+                            self.common.shell.workspace_for_surface(&surface)
+                        {
+                            self.common.shell.set_urgent(&workspace);
+                        }
+                    }
+                    ActivationContext::Workspace(workspace) => {
+                        let seat = self.common.last_active_seat().clone();
+                        let current_output = seat.active_output();
+                        let current_workspace = self.common.shell.active_space_mut(&current_output);
+
+                        if current_workspace
+                            .floating_layer
+                            .mapped()
+                            .any(|m| m == &element)
+                        {
+                            current_workspace
+                                .floating_layer
+                                .space
+                                .raise_element(&element, true);
+                        }
+
+                        if workspace == &current_workspace.handle {
+                            let target = element.into();
+                            Shell::set_focus(self, Some(&target), &seat, None);
+                        } else {
+                            Shell::append_focus_stack(self, Some(&target), &seat);
+                            self.common.shell.set_urgent(workspace);
+                        }
+                    }
+                }
+            } else {
+                self.common
+                    .shell
+                    .pending_activations
+                    .insert(ActivationKey::Wayland(surface), context.clone());
+            }
+        }
+    }
+}
+
+delegate_xdg_activation!(State);
diff --git a/src/wayland/handlers/xdg_shell/mod.rs b/src/wayland/handlers/xdg_shell/mod.rs
index bc8b23906..ca15441c1 100644
--- a/src/wayland/handlers/xdg_shell/mod.rs
+++ b/src/wayland/handlers/xdg_shell/mod.rs
@@ -312,7 +312,7 @@ impl XdgShellHandler for State {
             .visible_outputs_for_surface(surface.wl_surface())
             .collect::<Vec<_>>();
         for output in outputs.iter() {
-            self.common.shell.active_space_mut(output).refresh();
+            self.common.shell.refresh_active_space(output);
         }
 
         // animations might be unblocked now
diff --git a/src/xwayland.rs b/src/xwayland.rs
index a344178a1..600632e5c 100644
--- a/src/xwayland.rs
+++ b/src/xwayland.rs
@@ -5,23 +5,29 @@ use crate::{
     shell::{focus::target::KeyboardFocusTarget, CosmicSurface, Shell},
     state::State,
     utils::prelude::*,
-    wayland::{handlers::screencopy::PendingScreencopyBuffers, protocols::screencopy::SessionType},
+    wayland::{
+        handlers::{screencopy::PendingScreencopyBuffers, xdg_activation::ActivationContext},
+        protocols::screencopy::SessionType,
+    },
 };
 use smithay::{
     backend::drm::DrmNode,
     desktop::space::SpaceElement,
     reexports::x11rb::protocol::xproto::Window as X11Window,
     utils::{Logical, Point, Rectangle, Size},
-    wayland::selection::{
-        data_device::{
-            clear_data_device_selection, current_data_device_selection_userdata,
-            request_data_device_client_selection, set_data_device_selection,
-        },
-        primary_selection::{
-            clear_primary_selection, current_primary_selection_userdata,
-            request_primary_client_selection, set_primary_selection,
+    wayland::{
+        selection::{
+            data_device::{
+                clear_data_device_selection, current_data_device_selection_userdata,
+                request_data_device_client_selection, set_data_device_selection,
+            },
+            primary_selection::{
+                clear_primary_selection, current_primary_selection_userdata,
+                request_primary_client_selection, set_primary_selection,
+            },
+            SelectionTarget,
         },
-        SelectionTarget,
+        xdg_activation::XdgActivationToken,
     },
     xwayland::{
         xwm::{Reorder, XwmId},
@@ -145,12 +151,29 @@ impl XwmHandler for State {
             warn!(?window, ?err, "Failed to send Xwayland Mapped-Event",);
         }
 
+        let startup_id = window.startup_id();
         let surface = CosmicSurface::X11(window.clone());
         if self.common.shell.element_for_surface(&surface).is_some() {
             return;
         }
 
         let seat = self.common.last_active_seat().clone();
+        if let Some(context) = startup_id
+            .map(XdgActivationToken::from)
+            .and_then(|token| {
+                self.common
+                    .shell
+                    .xdg_activation_state
+                    .data_for_token(&token)
+            })
+            .and_then(|data| data.user_data.get::<ActivationContext>())
+        {
+            self.common.shell.pending_activations.insert(
+                crate::shell::ActivationKey::X11(window.window_id()),
+                context.clone(),
+            );
+        }
+
         self.common
             .shell
             .pending_windows
@@ -172,6 +195,30 @@ impl XwmHandler for State {
             })
             .cloned()
         {
+            if !self
+                .common
+                .shell
+                .pending_activations
+                .contains_key(&crate::shell::ActivationKey::X11(surface.window_id()))
+            {
+                if let Some(startup_id) = match &window {
+                    CosmicSurface::X11(x11) => x11.startup_id(),
+                    _ => None,
+                } {
+                    if let Some(context) = self
+                        .common
+                        .shell
+                        .xdg_activation_state
+                        .data_for_token(&XdgActivationToken::from(startup_id))
+                        .and_then(|data| data.user_data.get::<ActivationContext>())
+                    {
+                        self.common.shell.pending_activations.insert(
+                            crate::shell::ActivationKey::X11(surface.window_id()),
+                            context.clone(),
+                        );
+                    }
+                }
+            }
             Shell::map_window(self, &window);
         }
     }
@@ -224,7 +271,7 @@ impl XwmHandler for State {
             self.common.shell.outputs().cloned().collect::<Vec<_>>()
         };
         for output in outputs.iter() {
-            self.common.shell.active_space_mut(output).refresh();
+            self.common.shell.refresh_active_space(output);
         }
 
         // screencopy