use crate::input::InputRegistry;
use crate::input::KeyChord;
#[cfg(feature = "canvas")]
use crate::canvas::CanvasAction;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BindingSource {
Builtin,
Config,
CanvasBuiltin,
Runtime,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BindingLayer {
Keymap,
Canvas,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BindingInfo<A> {
pub layer: BindingLayer,
pub mode: String,
pub sequence: Vec<KeyChord>,
pub action: A,
pub source: BindingSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CanvasRoutingPrecedence {
KeymapFirst,
CanvasFirst,
StickyOwner,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BindingConflict<A> {
SameModeDuplicate {
mode: String,
sequence: Vec<KeyChord>,
first: A,
second: A,
},
ActiveModeShadow {
sequence: Vec<KeyChord>,
first_mode: String,
first: A,
shadowed_mode: String,
shadowed: A,
},
#[cfg(feature = "canvas")]
CanvasOverlap {
mode: String,
sequence: Vec<KeyChord>,
keymap_action: A,
canvas_action: CanvasAction,
routing: CanvasRoutingPrecedence,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BindingCatalog<A> {
pub bindings: Vec<BindingInfo<A>>,
}
impl<A> Default for BindingCatalog<A> {
fn default() -> Self {
Self {
bindings: Vec::new(),
}
}
}
impl<A: Clone> BindingCatalog<A> {
pub fn from_registry(registry: &InputRegistry<A>, source: BindingSource) -> Self {
let mut bindings = Vec::new();
for map in registry.maps.values() {
for (sequence, action) in &map.bindings {
bindings.push(BindingInfo {
layer: BindingLayer::Keymap,
mode: map.id.clone(),
sequence: sequence.clone(),
action: action.clone(),
source,
});
}
}
Self { bindings }
}
pub fn push(&mut self, info: BindingInfo<A>) {
self.bindings.push(info);
}
pub fn extend(&mut self, other: BindingCatalog<A>) {
self.bindings.extend(other.bindings);
}
}
impl<A> BindingCatalog<A> {
pub fn new() -> Self {
Self::default()
}
pub fn bindings_for_mode(&self, mode: &str) -> Vec<&BindingInfo<A>> {
self.bindings
.iter()
.filter(|info| info.mode == mode)
.collect()
}
pub fn bindings_for_sequence(&self, mode: &str, sequence: &[KeyChord]) -> Vec<&BindingInfo<A>> {
self.bindings
.iter()
.filter(|info| info.mode == mode && info.sequence == sequence)
.collect()
}
}
impl<A: PartialEq> BindingCatalog<A> {
pub fn bindings_for_action(&self, action: &A) -> Vec<&BindingInfo<A>> {
self.bindings
.iter()
.filter(|info| &info.action == action)
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BindableActionInfo<A> {
pub action: A,
pub name: &'static str,
pub description: &'static str,
pub modes: &'static [&'static str],
}
const NAV_FOCUS_MODES: &[&str] = &["general"];
const NAV_GLOBAL_MODES: &[&str] = &["global"];
impl<A> BindableActionInfo<A> {
pub fn new(action: A, name: &'static str, description: &'static str) -> Self {
Self {
action,
name,
description,
modes: NAV_GLOBAL_MODES,
}
}
pub fn modes(mut self, modes: &'static [&'static str]) -> Self {
self.modes = modes;
self
}
}
pub fn navigation_bindable_actions<A>() -> Vec<BindableActionInfo<A>>
where
A: From<crate::keybindings::NavigationAction>,
{
crate::keybindings::NavigationAction::infos()
.iter()
.map(|info| BindableActionInfo {
action: A::from(info.action),
name: info.name,
description: info.description,
modes: match info.category {
"Focus" => NAV_FOCUS_MODES,
_ => NAV_GLOBAL_MODES,
},
})
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BindingAnalysis<A> {
pub bindings: Vec<BindingInfo<A>>,
pub conflicts: Vec<BindingConflict<A>>,
}
pub fn analyze_keymap_bindings<A>(
catalog: &BindingCatalog<A>,
active_modes: &[impl AsRef<str>],
) -> BindingAnalysis<A>
where
A: Clone + PartialEq,
{
let mut conflicts = Vec::new();
let mut seen: Vec<(&str, &[KeyChord], &A)> = Vec::new();
for info in &catalog.bindings {
if info.layer != BindingLayer::Keymap {
continue;
}
if let Some((_, _, existing)) = seen
.iter()
.find(|(mode, seq, _)| *mode == info.mode.as_str() && *seq == info.sequence.as_slice())
{
if **existing != info.action {
conflicts.push(BindingConflict::SameModeDuplicate {
mode: info.mode.clone(),
sequence: info.sequence.clone(),
first: (*existing).clone(),
second: info.action.clone(),
});
}
} else {
seen.push((info.mode.as_str(), info.sequence.as_slice(), &info.action));
}
}
let active: Vec<&str> = active_modes.iter().map(|m| m.as_ref()).collect();
let mut handled: Vec<&[KeyChord]> = Vec::new();
for info in &catalog.bindings {
if info.layer != BindingLayer::Keymap || !active.contains(&info.mode.as_str()) {
continue;
}
if handled.iter().any(|seq| *seq == info.sequence.as_slice()) {
continue;
}
handled.push(info.sequence.as_slice());
let mut hits: Vec<&BindingInfo<A>> = active
.iter()
.filter_map(|mode| {
catalog.bindings.iter().find(|other| {
other.layer == BindingLayer::Keymap
&& other.mode == *mode
&& other.sequence == info.sequence
})
})
.collect();
hits.dedup_by(|a, b| a.mode == b.mode);
if hits.len() < 2 {
continue;
}
let winner = hits[0];
for shadowed in &hits[1..] {
conflicts.push(BindingConflict::ActiveModeShadow {
sequence: info.sequence.clone(),
first_mode: winner.mode.clone(),
first: winner.action.clone(),
shadowed_mode: shadowed.mode.clone(),
shadowed: shadowed.action.clone(),
});
}
}
BindingAnalysis {
bindings: catalog.bindings.clone(),
conflicts,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::input::{InputRegistry, KeyMap, try_parse_binding};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TestAction {
A,
B,
}
fn seq(binding: &str) -> Vec<KeyChord> {
try_parse_binding(binding).unwrap()
}
fn registry() -> InputRegistry<TestAction> {
let mut reg = InputRegistry::empty();
let mut general = KeyMap::new("general");
general.bind(seq("ctrl+a"), TestAction::A);
reg.add_map(general);
let mut global = KeyMap::new("global");
global.bind(seq("ctrl+a"), TestAction::B);
reg.add_map(global);
reg
}
#[test]
fn catalog_records_layer_and_source() {
let catalog = BindingCatalog::from_registry(®istry(), BindingSource::Config);
assert_eq!(catalog.bindings.len(), 2);
assert!(
catalog
.bindings
.iter()
.all(|b| b.layer == BindingLayer::Keymap && b.source == BindingSource::Config)
);
assert_eq!(catalog.bindings_for_mode("general").len(), 1);
assert_eq!(catalog.bindings_for_action(&TestAction::A).len(), 1);
assert_eq!(
catalog
.bindings_for_sequence("global", &seq("ctrl+a"))
.len(),
1
);
}
#[test]
fn active_mode_shadow_ordered_by_priority() {
let catalog = BindingCatalog::from_registry(®istry(), BindingSource::Config);
let analysis = analyze_keymap_bindings(&catalog, &["general", "global"]);
let shadows: Vec<_> = analysis
.conflicts
.iter()
.filter(|c| matches!(c, BindingConflict::ActiveModeShadow { .. }))
.collect();
assert_eq!(shadows.len(), 1);
match shadows[0] {
BindingConflict::ActiveModeShadow {
first_mode,
shadowed_mode,
..
} => {
assert_eq!(first_mode, "general");
assert_eq!(shadowed_mode, "global");
}
_ => unreachable!(),
}
}
#[test]
fn same_action_in_two_modes_is_not_a_duplicate() {
let mut reg = InputRegistry::empty();
let mut general = KeyMap::new("general");
general.bind(seq("ctrl+a"), TestAction::A);
reg.add_map(general);
let mut global = KeyMap::new("global");
global.bind(seq("ctrl+a"), TestAction::A);
reg.add_map(global);
let catalog = BindingCatalog::from_registry(®, BindingSource::Builtin);
let analysis = analyze_keymap_bindings(&catalog, &["general", "global"]);
assert!(
!analysis
.conflicts
.iter()
.any(|c| matches!(c, BindingConflict::SameModeDuplicate { .. }))
);
}
#[test]
fn same_mode_duplicate_detected_when_merged() {
let mut catalog = BindingCatalog::new();
catalog.push(BindingInfo {
layer: BindingLayer::Keymap,
mode: "nor".to_string(),
sequence: seq("d"),
action: TestAction::A,
source: BindingSource::Builtin,
});
catalog.push(BindingInfo {
layer: BindingLayer::Keymap,
mode: "nor".to_string(),
sequence: seq("d"),
action: TestAction::B,
source: BindingSource::Config,
});
let analysis = analyze_keymap_bindings(&catalog, &["nor"]);
assert!(
analysis
.conflicts
.iter()
.any(|c| matches!(c, BindingConflict::SameModeDuplicate { .. }))
);
}
}