Fix stuck HUD on fast modifier release

This commit is contained in:
2026-07-18 11:47:15 -07:00
parent 17ac4a4b3b
commit ab47c01cf7
+90
View File
@@ -40,6 +40,7 @@ enum DaemonMessage {
} }
struct Session { struct Session {
id: u64,
groups: Vec<AppGroup>, groups: Vec<AppGroup>,
selected: usize, selected: usize,
original: usize, original: usize,
@@ -49,6 +50,7 @@ struct Session {
struct RuntimeState { struct RuntimeState {
history: FocusHistory, history: FocusHistory,
session: Option<Session>, session: Option<Session>,
next_session_id: u64,
window_cycle: Option<WindowCycleSession>, window_cycle: Option<WindowCycleSession>,
ignore_focus_until: Option<Instant>, ignore_focus_until: Option<Instant>,
} }
@@ -107,6 +109,7 @@ impl Switcher {
state: RefCell::new(RuntimeState { state: RefCell::new(RuntimeState {
history, history,
session: None, session: None,
next_session_id: 0,
window_cycle: None, window_cycle: None,
ignore_focus_until: None, ignore_focus_until: None,
}), }),
@@ -149,7 +152,57 @@ impl Switcher {
} }
}); });
let switcher = Rc::clone(self);
controller.connect_modifiers(move |_controller, modifiers| {
if !switch_modifier_is_pressed(modifiers) {
switcher.finish(true);
}
Propagation::Proceed
});
self.window.add_controller(controller); self.window.add_controller(controller);
let focus_controller = gtk::EventControllerLegacy::new();
let switcher = Rc::clone(self);
focus_controller.connect_event(move |_controller, event| {
let Some(focus_event) = event.downcast_ref::<gdk::FocusEvent>() else {
return Propagation::Proceed;
};
if !focus_event.is_in() {
return Propagation::Proceed;
}
let session_id = switcher
.state
.borrow()
.session
.as_ref()
.map(|session| session.id);
let Some(session_id) = session_id else {
return Propagation::Proceed;
};
let switcher = Rc::clone(&switcher);
// The Wayland keyboard-enter and modifier updates can arrive in
// adjacent dispatches. Let GDK apply both before checking whether
// Super was released before the exclusive grab took effect.
glib::timeout_add_local_once(Duration::from_millis(10), move || {
let is_current_session = switcher
.state
.borrow()
.session
.as_ref()
.is_some_and(|session| session.id == session_id);
if is_current_session
&& current_keyboard_modifiers()
.is_some_and(|modifiers| !switch_modifier_is_pressed(modifiers))
{
switcher.finish(true);
}
});
Propagation::Proceed
});
self.window.add_controller(focus_controller);
} }
fn handle_message(&self, message: DaemonMessage) { fn handle_message(&self, message: DaemonMessage) {
@@ -247,7 +300,10 @@ impl Switcher {
(false, Direction::Next) => (original + 1) % groups.len(), (false, Direction::Next) => (original + 1) % groups.len(),
(false, Direction::Previous) => (original + groups.len() - 1) % groups.len(), (false, Direction::Previous) => (original + groups.len() - 1) % groups.len(),
}; };
let session_id = state.next_session_id;
state.next_session_id = state.next_session_id.wrapping_add(1);
state.session = Some(Session { state.session = Some(Session {
id: session_id,
groups, groups,
selected, selected,
original, original,
@@ -503,6 +559,17 @@ impl Switcher {
} }
} }
fn switch_modifier_is_pressed(modifiers: gdk::ModifierType) -> bool {
modifiers.intersects(gdk::ModifierType::SUPER_MASK | gdk::ModifierType::META_MASK)
}
fn current_keyboard_modifiers() -> Option<gdk::ModifierType> {
gdk::Display::default()?
.default_seat()?
.keyboard()
.map(|keyboard| keyboard.modifier_state())
}
fn install_css() { fn install_css() {
let provider = gtk::CssProvider::new(); let provider = gtk::CssProvider::new();
provider.load_from_data( provider.load_from_data(
@@ -657,3 +724,26 @@ fn main() -> Result<()> {
Some(command) => anyhow::bail!("unknown command: {command}"), Some(command) => anyhow::bail!("unknown command: {command}"),
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn switch_modifier_accepts_super_and_meta_mappings() {
assert!(switch_modifier_is_pressed(gdk::ModifierType::SUPER_MASK));
assert!(switch_modifier_is_pressed(gdk::ModifierType::META_MASK));
assert!(switch_modifier_is_pressed(
gdk::ModifierType::SHIFT_MASK | gdk::ModifierType::SUPER_MASK
));
}
#[test]
fn switch_modifier_rejects_unrelated_modifiers() {
assert!(!switch_modifier_is_pressed(gdk::ModifierType::empty()));
assert!(!switch_modifier_is_pressed(gdk::ModifierType::SHIFT_MASK));
assert!(!switch_modifier_is_pressed(
gdk::ModifierType::CONTROL_MASK | gdk::ModifierType::ALT_MASK
));
}
}