Add configurable mouse and wheel bindings, window move and resize, tab drops, cross-output dragging, and device settings. Adjust display scaling in 0.05 increments. Preserve opposite-edge positioning during scrolling resizes and show rows for edge drops into tabbed columns.
251 lines
7.8 KiB
Rust
251 lines
7.8 KiB
Rust
#[cfg(test)]
|
|
use crate::config::BindingAction;
|
|
#[cfg(test)]
|
|
use ferese_layout::Direction;
|
|
#[cfg(test)]
|
|
use smithay::input::keyboard::keysyms;
|
|
|
|
use super::{
|
|
Binding, BindingIdentity, BindingMatch, BindingModifiers, HashMap, Keycode,
|
|
binding_identity_for_runtime,
|
|
};
|
|
|
|
/// Immutable bindings and their lookup index are replaced together on reload.
|
|
/// The ordered slice remains available for gestures and the shortcut guide.
|
|
#[derive(Clone)]
|
|
pub(crate) struct BindingSet {
|
|
ordered: Vec<Binding>,
|
|
index: HashMap<BindingIdentity, usize>,
|
|
}
|
|
|
|
impl From<Vec<Binding>> for BindingSet {
|
|
fn from(ordered: Vec<Binding>) -> Self {
|
|
let mut index = HashMap::with_capacity(ordered.len());
|
|
|
|
for (position, binding) in ordered.iter().enumerate() {
|
|
let identity = binding_identity_for_runtime(binding);
|
|
|
|
if matches!(
|
|
identity.match_mode,
|
|
BindingMatch::Keysym | BindingMatch::Physical | BindingMatch::Pointer
|
|
) {
|
|
index.entry(identity).or_insert(position);
|
|
}
|
|
}
|
|
|
|
Self { ordered, index }
|
|
}
|
|
}
|
|
|
|
impl std::ops::Deref for BindingSet {
|
|
type Target = [Binding];
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.ordered
|
|
}
|
|
}
|
|
|
|
impl BindingSet {
|
|
pub(crate) fn binding_for_pointer(
|
|
&self,
|
|
trigger: super::PointerTrigger,
|
|
modifiers: smithay::input::keyboard::ModifiersState,
|
|
) -> Option<&Binding> {
|
|
let modifiers = BindingModifiers {
|
|
logo: modifiers.logo,
|
|
ctrl: modifiers.ctrl,
|
|
alt: modifiers.alt,
|
|
shift: modifiers.shift,
|
|
};
|
|
self.index
|
|
.get(&BindingIdentity {
|
|
modifiers,
|
|
match_mode: BindingMatch::Pointer,
|
|
key: trigger.key(),
|
|
})
|
|
.map(|index| &self.ordered[*index])
|
|
}
|
|
|
|
pub(crate) fn binding_for_key(
|
|
&self,
|
|
keycode: Keycode,
|
|
symbols: &[u32],
|
|
logo: bool,
|
|
ctrl: bool,
|
|
alt: bool,
|
|
shift: bool,
|
|
) -> Option<&Binding> {
|
|
let modifiers = BindingModifiers {
|
|
logo,
|
|
ctrl,
|
|
alt,
|
|
shift,
|
|
};
|
|
let physical = self.index.get(&BindingIdentity {
|
|
modifiers,
|
|
match_mode: BindingMatch::Physical,
|
|
key: keycode.raw(),
|
|
});
|
|
let symbolic = symbols.iter().filter_map(|&key| {
|
|
self.index.get(&BindingIdentity {
|
|
modifiers,
|
|
match_mode: BindingMatch::Keysym,
|
|
key,
|
|
})
|
|
});
|
|
// Preserve list precedence across physical keys and all raw symbols;
|
|
// neither hash iteration order nor symbol order determines the winner.
|
|
physical
|
|
.into_iter()
|
|
.chain(symbolic)
|
|
.min()
|
|
.map(|&index| &self.ordered[index])
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn action_for_key(
|
|
&self,
|
|
keycode: Keycode,
|
|
symbols: &[u32],
|
|
logo: bool,
|
|
ctrl: bool,
|
|
alt: bool,
|
|
shift: bool,
|
|
) -> Option<&BindingAction> {
|
|
self.binding_for_key(keycode, symbols, logo, ctrl, alt, shift)
|
|
.map(|binding| &binding.action)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::config::Config;
|
|
use crate::config::bindings::BindingTrigger;
|
|
use std::sync::Arc;
|
|
|
|
fn binding_for(trigger: BindingTrigger, action: BindingAction) -> Binding {
|
|
Binding {
|
|
modifiers: BindingModifiers::default(),
|
|
trigger,
|
|
action,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn keyboard_index_preserves_order_across_symbols_and_physical_keys() {
|
|
let code = Keycode::new(38);
|
|
let bindings = vec![
|
|
binding_for(BindingTrigger::Keysym(keysyms::KEY_b), BindingAction::Close),
|
|
binding_for(
|
|
BindingTrigger::Physical(code),
|
|
BindingAction::ToggleFloating,
|
|
),
|
|
binding_for(BindingTrigger::Keysym(keysyms::KEY_a), BindingAction::Exit),
|
|
binding_for(BindingTrigger::Keysym(keysyms::KEY_b), BindingAction::None),
|
|
];
|
|
|
|
for ordered in [bindings.clone(), bindings.into_iter().rev().collect()] {
|
|
let index = BindingSet::from(ordered);
|
|
|
|
for symbols in [
|
|
vec![keysyms::KEY_a, keysyms::KEY_b],
|
|
vec![keysyms::KEY_b, keysyms::KEY_a],
|
|
vec![],
|
|
] {
|
|
let expected = index
|
|
.iter()
|
|
.find(|binding| binding.matches(code, &symbols, false, false, false, false));
|
|
assert_eq!(
|
|
index.action_for_key(code, &symbols, false, false, false, false),
|
|
expected.map(|binding| &binding.action)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn indexed_lookup_matches_linear_lookup_for_all_modifier_combinations() {
|
|
let config = Config::default();
|
|
let mut bindings = config.bindings(&config.input_settings().unwrap()).unwrap();
|
|
bindings.extend([
|
|
binding_for(
|
|
BindingTrigger::Physical(Keycode::new(38)),
|
|
BindingAction::None,
|
|
),
|
|
binding_for(BindingTrigger::Keysym(keysyms::KEY_a), BindingAction::Close),
|
|
binding_for(
|
|
BindingTrigger::Swipe {
|
|
fingers: 3,
|
|
direction: crate::gestures::SwipeDirection::Left,
|
|
},
|
|
BindingAction::Focus(Direction::Left),
|
|
),
|
|
]);
|
|
let index = BindingSet::from(bindings);
|
|
|
|
for bits in 0..16 {
|
|
let (logo, ctrl, alt, shift) =
|
|
(bits & 1 != 0, bits & 2 != 0, bits & 4 != 0, bits & 8 != 0);
|
|
|
|
for code in 8..100 {
|
|
for symbols in [
|
|
vec![],
|
|
vec![keysyms::KEY_Return],
|
|
vec![keysyms::KEY_b, keysyms::KEY_a],
|
|
vec![keysyms::KEY_a, keysyms::KEY_Tab],
|
|
vec![keysyms::KEY_NoSymbol],
|
|
] {
|
|
let code = Keycode::new(code);
|
|
let expected = index
|
|
.iter()
|
|
.find(|binding| binding.matches(code, &symbols, logo, ctrl, alt, shift));
|
|
assert_eq!(
|
|
index.action_for_key(code, &symbols, logo, ctrl, alt, shift),
|
|
expected.map(|binding| &binding.action)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn replacing_bindings_discards_old_index_entries() {
|
|
let code = Keycode::new(38);
|
|
let mut bindings = BindingSet::from(vec![binding_for(
|
|
BindingTrigger::Physical(code),
|
|
BindingAction::Close,
|
|
)]);
|
|
assert_eq!(
|
|
bindings.action_for_key(code, &[], false, false, false, false),
|
|
Some(&BindingAction::Close)
|
|
);
|
|
bindings = BindingSet::from(vec![binding_for(
|
|
BindingTrigger::Keysym(keysyms::KEY_b),
|
|
BindingAction::None,
|
|
)]);
|
|
assert_eq!(
|
|
bindings.action_for_key(code, &[], false, false, false, false),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
bindings.action_for_key(code, &[keysyms::KEY_b], false, false, false, false),
|
|
Some(&BindingAction::None)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cloning_spawn_actions_shares_the_command_storage() {
|
|
let action =
|
|
BindingAction::Spawn(vec!["foot".into(), "--app-id".into(), "work".into()].into());
|
|
let cloned = action.clone();
|
|
let (BindingAction::Spawn(original), BindingAction::Spawn(cloned)) = (&action, cloned)
|
|
else {
|
|
unreachable!();
|
|
};
|
|
assert!(Arc::ptr_eq(original, &cloned));
|
|
assert_eq!(original[0], "foot");
|
|
assert_eq!(original.len(), 3);
|
|
}
|
|
}
|