Add named keyboard binding modes (#32)
Add named keyboard shortcut maps with an Escape exit and a panel indicator. Keep press/release ownership across transitions, validate remapped Escape keys, and cancel stale repeats and focus previews. Give the indicator ownership of the panel while a mode is active. Close popovers, collapse Morph, and keep Live Activities data current without resizing the hidden panel. Document configuration and transition behavior. Validation: 1,519 workspace Rust tests passed; 70 Python tests passed. Workspace build, formatting, Clippy, and Settings configuration validation passed. Opt-in native desktop checks were not rerun because Weston is absent. Related: #5.
This commit is contained in:
1 parent
6b0d903ad0
commit
7d58eeb65d
28 files changed
+1258
-35
No files matched your search
@@ -0,0 +1,244 @@
|
||||
use super::*;
|
||||
use smithay::backend::input::UnusedEvent;
|
||||
use smithay::backend::winit::WinitVirtualDevice;
|
||||
use smithay::input::keyboard::Keycode;
|
||||
use smithay::reexports::{calloop::EventLoop, wayland_server::Display};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestInput;
|
||||
|
||||
impl InputBackend for TestInput {
|
||||
type Device = WinitVirtualDevice;
|
||||
type PointerButtonEvent = UnusedEvent;
|
||||
type KeyboardKeyEvent = TestKey;
|
||||
type PointerAxisEvent = UnusedEvent;
|
||||
type PointerMotionEvent = UnusedEvent;
|
||||
type PointerMotionAbsoluteEvent = UnusedEvent;
|
||||
type GestureSwipeBeginEvent = UnusedEvent;
|
||||
type GestureSwipeUpdateEvent = UnusedEvent;
|
||||
type GestureSwipeEndEvent = UnusedEvent;
|
||||
type GesturePinchBeginEvent = UnusedEvent;
|
||||
type GesturePinchUpdateEvent = UnusedEvent;
|
||||
type GesturePinchEndEvent = UnusedEvent;
|
||||
type GestureHoldBeginEvent = UnusedEvent;
|
||||
type GestureHoldEndEvent = UnusedEvent;
|
||||
type TouchDownEvent = UnusedEvent;
|
||||
type TouchUpEvent = UnusedEvent;
|
||||
type TouchMotionEvent = UnusedEvent;
|
||||
type TouchCancelEvent = UnusedEvent;
|
||||
type TouchFrameEvent = UnusedEvent;
|
||||
type TabletToolAxisEvent = UnusedEvent;
|
||||
type TabletToolProximityEvent = UnusedEvent;
|
||||
type TabletToolTipEvent = UnusedEvent;
|
||||
type TabletToolButtonEvent = UnusedEvent;
|
||||
type SwitchToggleEvent = UnusedEvent;
|
||||
type SpecialEvent = UnusedEvent;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestKey(Keycode, KeyState);
|
||||
impl Event<TestInput> for TestKey {
|
||||
fn time(&self) -> u64 {
|
||||
1_000
|
||||
}
|
||||
fn device(&self) -> WinitVirtualDevice {
|
||||
WinitVirtualDevice
|
||||
}
|
||||
}
|
||||
impl KeyboardKeyEvent<TestInput> for TestKey {
|
||||
fn key_code(&self) -> Keycode {
|
||||
self.0
|
||||
}
|
||||
fn state(&self) -> KeyState {
|
||||
self.1
|
||||
}
|
||||
fn count(&self) -> u32 {
|
||||
1
|
||||
}
|
||||
}
|
||||
pub(crate) fn key(state: &mut Ferese, code: u32, pressed: bool) {
|
||||
state.process_input_event::<TestInput>(InputEvent::Keyboard {
|
||||
event: TestKey(
|
||||
Keycode::new(code),
|
||||
if pressed { KeyState::Pressed } else { KeyState::Released },
|
||||
),
|
||||
});
|
||||
}
|
||||
fn tap(state: &mut Ferese, code: u32) {
|
||||
key(state, code, true);
|
||||
key(state, code, false);
|
||||
}
|
||||
const SOURCE: &str = r#"
|
||||
binding "Super+r" binding-mode resize
|
||||
binding-mode resize { binding h resize left; binding l binding-mode launch; }
|
||||
binding-mode launch { binding h binding-mode default; }
|
||||
"#;
|
||||
fn state(events: &mut EventLoop<'static, Ferese>) -> Ferese {
|
||||
let (_, mut config, _) = crate::config::prepare(SOURCE, std::path::Path::new(".")).unwrap();
|
||||
config.wallpaper.path = None;
|
||||
let mut state = Ferese::new(events, Display::new().unwrap(), config).unwrap();
|
||||
state.config_source = Some(SOURCE.into());
|
||||
let mut sections = ferese_config::Document::parse(SOURCE).unwrap().value().clone();
|
||||
sections.as_object_mut().unwrap().remove("theme");
|
||||
state.config_sections = Some(sections);
|
||||
state
|
||||
}
|
||||
#[test]
|
||||
fn keyboard_modes_pair_releases_and_handle_held_modifiers_and_keys() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"input::binding_mode_tests::keyboard_modes_pair_releases_and_handle_held_modifiers_and_keys",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let mut events = EventLoop::try_new().unwrap();
|
||||
let mut state = state(&mut events);
|
||||
// Enter with Super held. A new unmodified mode shortcut must not match
|
||||
// until the modifier is released; Escape remains usable throughout.
|
||||
key(&mut state, 133, true);
|
||||
key(&mut state, 27, true);
|
||||
assert_eq!(state.binding_modes.active(), Some("resize"));
|
||||
assert!(state.intercepted_keys.contains(&Keycode::new(27)));
|
||||
tap(&mut state, 46); // Super+l does not switch to launch.
|
||||
assert_eq!(state.binding_modes.active(), Some("resize"));
|
||||
tap(&mut state, 9); // Super+Escape always leaves.
|
||||
assert_eq!(state.binding_modes.active(), None);
|
||||
key(&mut state, 27, false);
|
||||
key(&mut state, 133, false);
|
||||
assert!(state.intercepted_keys.is_empty());
|
||||
|
||||
// An application-owned h stays application-owned after entering a mode.
|
||||
key(&mut state, 43, true);
|
||||
state.set_binding_mode("launch");
|
||||
key(&mut state, 43, true);
|
||||
assert_eq!(state.binding_modes.active(), Some("launch"));
|
||||
assert!(!state.intercepted_keys.contains(&Keycode::new(43)));
|
||||
key(&mut state, 43, false);
|
||||
assert_eq!(state.binding_modes.active(), Some("launch"));
|
||||
tap(&mut state, 43);
|
||||
assert_eq!(state.binding_modes.active(), None);
|
||||
|
||||
// A consumed resize key retains release ownership across an explicit exit.
|
||||
state.set_binding_mode("resize");
|
||||
key(&mut state, 43, true);
|
||||
assert!(state.intercepted_keys.contains(&Keycode::new(43)));
|
||||
tap(&mut state, 9);
|
||||
assert!(state.intercepted_keys.contains(&Keycode::new(43)));
|
||||
key(&mut state, 43, false);
|
||||
assert!(state.intercepted_keys.is_empty());
|
||||
assert!(state.binding_mode_pressed.is_empty());
|
||||
key(&mut state, 9, true);
|
||||
state.set_binding_mode("launch");
|
||||
key(&mut state, 9, false);
|
||||
assert_eq!(
|
||||
state.binding_modes.active(),
|
||||
Some("launch"),
|
||||
"an application-owned Escape release cannot exit a new mode"
|
||||
);
|
||||
tap(&mut state, 9);
|
||||
assert_eq!(state.binding_modes.active(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reload_transitions_cancel_modes_without_losing_key_ownership() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"input::binding_mode_tests::reload_transitions_cancel_modes_without_losing_key_ownership",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let mut events = EventLoop::try_new().unwrap();
|
||||
let mut state = state(&mut events);
|
||||
state.set_binding_mode("resize");
|
||||
key(&mut state, 43, true);
|
||||
assert!(state.reload_config_source("binding-mode default {}".into()).is_err());
|
||||
assert_eq!(state.binding_modes.active(), Some("resize"));
|
||||
assert!(state.intercepted_keys.contains(&Keycode::new(43)));
|
||||
state
|
||||
.reload_config_source(format!("{SOURCE}\nanimations {{ speed 0.75; }}"))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
state.binding_modes.active(),
|
||||
Some("resize"),
|
||||
"unrelated reload preserves mode"
|
||||
);
|
||||
state
|
||||
.reload_config_source(format!("{SOURCE}\ninput {{ repeat-rate 30; }}"))
|
||||
.unwrap();
|
||||
assert_eq!(state.binding_modes.active(), None, "input reload exits mode");
|
||||
state.set_binding_mode("resize");
|
||||
state
|
||||
.reload_config_source("binding-mode launch { binding t spawn terminal; }".into())
|
||||
.unwrap();
|
||||
assert_eq!(state.binding_modes.active(), None);
|
||||
assert!(!state.binding_modes.set("resize"));
|
||||
key(&mut state, 43, false);
|
||||
assert!(state.intercepted_keys.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_exits_with_every_modifier_combination() {
|
||||
if !crate::startup_tests::private_runtime("input::binding_mode_tests::escape_exits_with_every_modifier_combination")
|
||||
{
|
||||
return;
|
||||
}
|
||||
let mut events = EventLoop::try_new().unwrap();
|
||||
let mut state = state(&mut events);
|
||||
let modifiers = [133, 37, 64, 50]; // Super, Ctrl, Alt, Shift.
|
||||
for bits in 0..16 {
|
||||
state.set_binding_mode("resize");
|
||||
for (bit, code) in modifiers.iter().enumerate() {
|
||||
if bits & (1 << bit) != 0 {
|
||||
key(&mut state, *code, true);
|
||||
}
|
||||
}
|
||||
tap(&mut state, 9);
|
||||
assert_eq!(state.binding_modes.active(), None, "modifiers={bits:04b}");
|
||||
for (bit, code) in modifiers.iter().enumerate() {
|
||||
if bits & (1 << bit) != 0 {
|
||||
key(&mut state, *code, false);
|
||||
}
|
||||
}
|
||||
assert!(state.intercepted_keys.is_empty());
|
||||
assert!(state.binding_mode_pressed.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_capture_exits_modes_and_prevents_reentry() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"input::binding_mode_tests::input_capture_exits_modes_and_prevents_reentry",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
use crate::input_capture::{Barrier, Zone};
|
||||
let mut events = EventLoop::try_new().unwrap();
|
||||
let mut state = state(&mut events);
|
||||
state.set_binding_mode("resize");
|
||||
state.input_capture.update_zones(vec![Zone {
|
||||
width: 800,
|
||||
height: 600,
|
||||
x: 0,
|
||||
y: 0,
|
||||
}]);
|
||||
let id = state.input_capture.register(1, 3).unwrap();
|
||||
let zone_set = state.input_capture.zones(id).unwrap()["zone_set"].as_u64().unwrap() as u32;
|
||||
state
|
||||
.input_capture
|
||||
.set_barriers(
|
||||
id,
|
||||
zone_set,
|
||||
vec![Barrier {
|
||||
id: 1,
|
||||
position: [800, 0, 800, 599],
|
||||
}],
|
||||
)
|
||||
.unwrap();
|
||||
state.input_capture.enable(id).unwrap();
|
||||
assert!(state.input_capture.motion((799., 50.), (810., 50.), true));
|
||||
crate::after_dispatch_with_redraw(&mut state, |_, _| {});
|
||||
assert_eq!(state.binding_modes.active(), None);
|
||||
state.set_binding_mode("resize");
|
||||
assert_eq!(state.binding_modes.active(), None);
|
||||
state.input_capture.disable(id).unwrap();
|
||||
crate::after_dispatch_with_redraw(&mut state, |_, _| {});
|
||||
assert_eq!(state.binding_modes.active(), None);
|
||||
}
|
||||
Reference in new issue
Block a user