Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/input/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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());
}
Expand Down
58 changes: 58 additions & 0 deletions src/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions src/shell/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ pub struct Shell {
pub active_hint: bool,
overview_mode: OverviewMode,
swap_indicator: Option<SwapIndicator>,
window_switcher_binding: Option<shortcuts::Binding>,
window_switcher_index: Option<usize>,
resize_mode: ResizeMode,
resize_state: Option<(
KeyboardFocusTarget,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<shortcuts::Binding>) {
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<usize> {
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<Trigger>,
Expand Down