mod input;
mod render;
use super::FocusState;
use ratatui::layout::Rect;
use ratatui::style::Color;
use serde_json::Value;
pub use input::KeybindingListEvent;
pub use render::{format_key_combo, render_keybinding_list};
#[derive(Debug, Clone)]
pub struct KeybindingListState {
pub bindings: Vec<Value>,
pub focused_index: Option<usize>,
pub label: String,
pub focus: FocusState,
pub item_schema: Option<Box<crate::view::settings::schema::SettingSchema>>,
pub display_field: Option<String>,
}
impl KeybindingListState {
pub fn new(label: impl Into<String>) -> Self {
Self {
bindings: Vec::new(),
focused_index: None,
label: label.into(),
focus: FocusState::Normal,
item_schema: None,
display_field: None,
}
}
pub fn with_bindings(mut self, value: &Value) -> Self {
if let Some(arr) = value.as_array() {
self.bindings = arr.clone();
}
self
}
pub fn with_focus(mut self, focus: FocusState) -> Self {
self.focus = focus;
self
}
pub fn with_item_schema(
mut self,
schema: crate::view::settings::schema::SettingSchema,
) -> Self {
self.item_schema = Some(Box::new(schema));
self
}
pub fn with_display_field(mut self, field: String) -> Self {
self.display_field = Some(field);
self
}
pub fn is_enabled(&self) -> bool {
self.focus != FocusState::Disabled
}
pub fn to_value(&self) -> Value {
Value::Array(self.bindings.clone())
}
pub fn focused_binding(&self) -> Option<&Value> {
self.focused_index.and_then(|idx| self.bindings.get(idx))
}
pub fn focus_next(&mut self) {
match self.focused_index {
None => {
if !self.bindings.is_empty() {
self.focused_index = Some(0);
}
}
Some(idx) if idx + 1 < self.bindings.len() => {
self.focused_index = Some(idx + 1);
}
Some(_) => {
self.focused_index = None;
}
}
}
pub fn focus_prev(&mut self) {
match self.focused_index {
None => {
if !self.bindings.is_empty() {
self.focused_index = Some(self.bindings.len() - 1);
}
}
Some(0) => {
}
Some(idx) => {
self.focused_index = Some(idx - 1);
}
}
}
pub fn remove_focused(&mut self) {
if let Some(idx) = self.focused_index {
if idx < self.bindings.len() {
self.bindings.remove(idx);
if self.bindings.is_empty() {
self.focused_index = None;
} else if idx >= self.bindings.len() {
self.focused_index = Some(self.bindings.len() - 1);
}
}
}
}
pub fn remove_binding(&mut self, index: usize) {
if index < self.bindings.len() {
self.bindings.remove(index);
if let Some(focused) = self.focused_index {
if focused >= self.bindings.len() {
self.focused_index = if self.bindings.is_empty() {
None
} else {
Some(self.bindings.len() - 1)
};
}
}
}
}
pub fn add_binding(&mut self, binding: Value) {
self.bindings.push(binding);
}
pub fn update_binding(&mut self, index: usize, binding: Value) {
if index < self.bindings.len() {
self.bindings[index] = binding;
}
}
pub fn focus_entry(&mut self, index: usize) {
if index < self.bindings.len() {
self.focused_index = Some(index);
}
}
pub fn focus_add_row(&mut self) {
self.focused_index = None;
}
}
#[derive(Debug, Clone, Copy)]
pub struct KeybindingListColors {
pub label_fg: Color,
pub key_fg: Color,
pub action_fg: Color,
pub focused_bg: Color,
pub focused_fg: Color,
pub delete_fg: Color,
pub add_fg: Color,
}
impl Default for KeybindingListColors {
fn default() -> Self {
Self {
label_fg: Color::White,
key_fg: Color::Yellow,
action_fg: Color::Cyan,
focused_bg: Color::DarkGray,
focused_fg: Color::White,
delete_fg: Color::Red,
add_fg: Color::Green,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct KeybindingListLayout {
pub entry_rects: Vec<Rect>,
pub delete_rects: Vec<Rect>,
pub add_rect: Option<Rect>,
}
impl KeybindingListLayout {
pub fn hit_test(&self, x: u16, y: u16) -> Option<KeybindingListHit> {
for (idx, rect) in self.delete_rects.iter().enumerate() {
if y == rect.y && x >= rect.x && x < rect.x + rect.width {
return Some(KeybindingListHit::DeleteButton(idx));
}
}
for (idx, rect) in self.entry_rects.iter().enumerate() {
if y == rect.y && x >= rect.x && x < rect.x + rect.width {
return Some(KeybindingListHit::Entry(idx));
}
}
if let Some(ref add_rect) = self.add_rect {
if y == add_rect.y && x >= add_rect.x && x < add_rect.x + add_rect.width {
return Some(KeybindingListHit::AddRow);
}
}
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeybindingListHit {
Entry(usize),
DeleteButton(usize),
AddRow,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_keybinding_list_state_new() {
let state = KeybindingListState::new("Keybindings");
assert_eq!(state.label, "Keybindings");
assert!(state.bindings.is_empty());
assert!(state.focused_index.is_none());
}
#[test]
fn test_keybinding_list_navigation() {
let mut state = KeybindingListState::new("Test");
state.add_binding(serde_json::json!({"key": "a", "action": "test1"}));
state.add_binding(serde_json::json!({"key": "b", "action": "test2"}));
assert!(state.focused_index.is_none());
state.focus_next();
assert_eq!(state.focused_index, Some(0));
state.focus_next();
assert_eq!(state.focused_index, Some(1));
state.focus_next();
assert!(state.focused_index.is_none());
state.focus_prev();
assert_eq!(state.focused_index, Some(1));
state.focus_prev();
assert_eq!(state.focused_index, Some(0));
state.focus_prev();
assert_eq!(state.focused_index, Some(0));
}
#[test]
fn test_keybinding_list_remove() {
let mut state = KeybindingListState::new("Test");
state.add_binding(serde_json::json!({"key": "a", "action": "test1"}));
state.add_binding(serde_json::json!({"key": "b", "action": "test2"}));
state.focus_entry(0);
state.remove_focused();
assert_eq!(state.bindings.len(), 1);
assert_eq!(state.focused_index, Some(0));
}
#[test]
fn test_keybinding_list_hit_test() {
let layout = KeybindingListLayout {
entry_rects: vec![Rect::new(2, 1, 40, 1), Rect::new(2, 2, 40, 1)],
delete_rects: vec![Rect::new(38, 1, 3, 1), Rect::new(38, 2, 3, 1)],
add_rect: Some(Rect::new(2, 3, 40, 1)),
};
assert_eq!(
layout.hit_test(38, 1),
Some(KeybindingListHit::DeleteButton(0))
);
assert_eq!(layout.hit_test(10, 1), Some(KeybindingListHit::Entry(0)));
assert_eq!(layout.hit_test(10, 2), Some(KeybindingListHit::Entry(1)));
assert_eq!(layout.hit_test(10, 3), Some(KeybindingListHit::AddRow));
assert_eq!(layout.hit_test(0, 0), None);
}
}