diff --git a/src/input/actions.rs b/src/input/actions.rs index 4cecbe28a..cb26ae6e5 100644 --- a/src/input/actions.rs +++ b/src/input/actions.rs @@ -73,6 +73,7 @@ impl State { &self.common.config, self.common.event_loop_handle.clone(), ); + shell.set_window_switcher_binding(None); } let pointer = seat.get_pointer().unwrap(); let keyboard = seat.get_keyboard().unwrap(); @@ -1002,6 +1003,34 @@ impl State { // Gets the configured command for a given system action. Action::System(system) => { + // Track the triggering binding for the window switcher so + // modifier-release can dismiss it even for custom shortcuts. + if matches!( + system, + shortcuts::action::System::WindowSwitcher + | shortcuts::action::System::WindowSwitcherPrevious + ) { + let mods = &pattern.modifiers; + // For custom modifiers (not Alt/Super), cosmic-launcher does + // not know when to confirm the selection, so cosmic-comp + // tracks the cycle index itself and focuses the window + // directly on modifier release. + // Native Alt/Super bindings are handled entirely by + // cosmic-launcher, so we do not track state for them. + if !mods.alt && !mods.logo { + let forward = + matches!(system, shortcuts::action::System::WindowSwitcher); + let output = seat.active_output(); + let mut shell = self.common.shell.write(); + let len = shell + .active_space(&output) + .map(|ws| ws.focus_stack.get(seat).iter().count()) + .unwrap_or(1) + .max(1); + shell.advance_window_switcher(forward, len); + shell.set_window_switcher_binding(Some(pattern.clone())); + } + } if let Some(command) = self.common.config.system_actions.get(&system) { self.spawn_command(command.clone()); } diff --git a/src/input/mod.rs b/src/input/mod.rs index c57eeca4a..c65357b0f 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1654,6 +1654,52 @@ impl State { } } + // Dismiss window switcher when its trigger modifier is released and the + // window-switcher app does not natively watch for that modifier (i.e., + // the binding uses neither Alt nor Super, which cosmic-launcher already + // intercepts on its own). + // + // Only check modifier release — not key release. For Alt+Tab the user + // holds Alt and presses Tab to cycle; releasing Tab should NOT close + // the switcher. The same applies to custom bindings like Shift+F: the + // user holds Shift and can press F multiple times to cycle through + // windows; only releasing Shift should confirm and close. + // + // For custom-modifier bindings, cosmic-launcher does not know which + // modifier to watch, so it cannot activate the selected window itself. + // Instead, cosmic-comp tracks a cycle index and focuses the window + // directly when the modifier is released. Capture the pending target + // now (before the binding/index is cleared below). + let (dismiss_window_switcher, pending_switcher_focus) = + if let Some(binding) = shell.window_switcher_binding().cloned() { + let mods = &binding.modifiers; + // cosmic-launcher already handles Alt and Super release internally; + // only intervene for other modifiers (Ctrl, Shift, …). + let launcher_handles_natively = mods.alt || mods.logo; + let dismiss = !launcher_handles_natively + && event.state() == KeyState::Released + && ((mods.ctrl && !modifiers.ctrl) || (mods.shift && !modifiers.shift)); + + let pending = if dismiss { + shell.window_switcher_index().and_then(|idx| { + let output = seat.active_output(); + shell + .active_space(&output) + .and_then(|ws| ws.focus_stack.get(seat).iter().nth(idx).cloned()) + .map(KeyboardFocusTarget::from) + }) + } else { + None + }; + + (dismiss, pending) + } else { + (false, None) + }; + if dismiss_window_switcher { + shell.set_window_switcher_binding(None); + } + // Leave or update resize mode, if modifiers changed or initial key was released if let Some(action_pattern) = shell.resize_mode().0.active_binding() { if action_pattern.key.is_some() @@ -1759,6 +1805,18 @@ impl State { std::mem::drop(shell); + // Dismiss window switcher for custom modifier bindings (non-Alt/Super). + // Focus the window that was selected (tracked by the cycle index) so + // that cosmic-launcher does not need to handle the custom modifier. + // If the tracked window closed while the switcher was open, skip the + // focus call rather than clearing all focus via set_focus(None). + if dismiss_window_switcher { + if let Some(ref focus) = pending_switcher_focus { + Shell::set_focus(self, Some(focus), seat, Some(serial), false); + } + return FilterResult::Intercept(None); + } + // cancel grabs if is_grabbed && handle.modified_sym() == Keysym::Escape diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 6ba300d13..920f1f636 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -276,6 +276,8 @@ pub struct Shell { pub active_hint: bool, overview_mode: OverviewMode, swap_indicator: Option, + window_switcher_binding: Option, + window_switcher_index: Option, resize_mode: ResizeMode, resize_state: Option<( KeyboardFocusTarget, @@ -1592,6 +1594,8 @@ impl Shell { active_hint: config.cosmic_conf.active_hint, overview_mode: OverviewMode::None, swap_indicator: None, + window_switcher_binding: None, + window_switcher_index: None, resize_mode: ResizeMode::None, resize_state: None, resize_indicator: None, @@ -2182,6 +2186,40 @@ impl Shell { clients } + pub fn window_switcher_binding(&self) -> Option<&shortcuts::Binding> { + self.window_switcher_binding.as_ref() + } + + pub fn set_window_switcher_binding(&mut self, binding: Option) { + if binding.is_none() { + self.window_switcher_index = None; + } + // When setting a new (Some) binding the index is intentionally kept: + // the caller already called advance_window_switcher() just before this, + // so resetting here would undo that advance. + self.window_switcher_binding = binding; + } + + pub fn window_switcher_index(&self) -> Option { + self.window_switcher_index + } + + /// Advance the window-switcher cycle index. + /// + /// `len` is the number of windows available. The index wraps around. + /// The very first call (when `window_switcher_index` is `None`) starts at + /// position 1 for forward (the window after the current one) or `len-1` + /// for backward — matching standard Alt+Tab behaviour. + pub fn advance_window_switcher(&mut self, forward: bool, len: usize) { + let len = len.max(1); + self.window_switcher_index = Some(match self.window_switcher_index { + None if forward => 1 % len, + None => len.saturating_sub(1), + Some(i) if forward => (i + 1) % len, + Some(i) => i.checked_sub(1).unwrap_or(len - 1), + }); + } + pub fn set_overview_mode( &mut self, enabled: Option,