use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
use super::{DropdownLayout, DropdownState, FocusState};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DropdownEvent {
Opened,
Closed,
SelectionChanged(usize),
Cancelled,
Hovered,
Left,
}
impl DropdownState {
pub fn handle_mouse(
&mut self,
event: MouseEvent,
layout: &DropdownLayout,
) -> Option<DropdownEvent> {
if !self.is_enabled() {
return None;
}
match event.kind {
MouseEventKind::Down(MouseButton::Left) => {
if self.open {
if let Some(index) = layout.option_at(event.column, event.row) {
self.select(index);
return Some(DropdownEvent::SelectionChanged(index));
}
if layout.is_button(event.column, event.row) {
self.toggle_open();
return Some(DropdownEvent::Closed);
}
self.cancel();
return Some(DropdownEvent::Cancelled);
} else {
if layout.is_button(event.column, event.row) {
self.toggle_open();
return Some(DropdownEvent::Opened);
}
}
None
}
MouseEventKind::Moved => {
let inside = layout.is_button(event.column, event.row)
|| layout.option_at(event.column, event.row).is_some();
if inside {
if self.focus != FocusState::Focused && self.focus != FocusState::Hovered {
self.focus = FocusState::Hovered;
}
Some(DropdownEvent::Hovered)
} else if self.focus == FocusState::Hovered && !self.open {
self.focus = FocusState::Normal;
Some(DropdownEvent::Left)
} else {
None
}
}
MouseEventKind::ScrollUp => {
if self.open {
self.scroll_by(-3);
Some(DropdownEvent::SelectionChanged(self.selected))
} else {
None
}
}
MouseEventKind::ScrollDown => {
if self.open {
self.scroll_by(3);
Some(DropdownEvent::SelectionChanged(self.selected))
} else {
None
}
}
_ => None,
}
}
pub fn handle_key(&mut self, key: KeyEvent) -> Option<DropdownEvent> {
if !self.is_enabled() {
return None;
}
if self.focus != FocusState::Focused && !self.open {
return None;
}
match key.code {
KeyCode::Enter | KeyCode::Char(' ') => {
if self.open {
self.confirm();
Some(DropdownEvent::Closed)
} else {
self.toggle_open();
Some(DropdownEvent::Opened)
}
}
KeyCode::Esc => {
if self.open {
self.cancel();
Some(DropdownEvent::Cancelled)
} else {
None
}
}
KeyCode::Up | KeyCode::Char('k') => {
self.select_prev();
Some(DropdownEvent::SelectionChanged(self.selected))
}
KeyCode::Down | KeyCode::Char('j') => {
self.select_next();
Some(DropdownEvent::SelectionChanged(self.selected))
}
KeyCode::Home => {
if !self.options.is_empty() {
self.selected = 0;
self.ensure_visible();
Some(DropdownEvent::SelectionChanged(0))
} else {
None
}
}
KeyCode::End => {
if !self.options.is_empty() {
self.selected = self.options.len() - 1;
self.ensure_visible();
Some(DropdownEvent::SelectionChanged(self.selected))
} else {
None
}
}
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyModifiers;
use ratatui::layout::Rect;
fn make_layout(open: bool) -> DropdownLayout {
let mut layout = DropdownLayout {
button_area: Rect::new(10, 0, 15, 1),
option_areas: Vec::new(),
full_area: Rect::new(0, 0, 25, 1),
scroll_offset: 0,
};
if open {
layout.option_areas = vec![
Rect::new(10, 1, 15, 1),
Rect::new(10, 2, 15, 1),
Rect::new(10, 3, 15, 1),
];
}
layout
}
fn mouse_down(x: u16, y: u16) -> MouseEvent {
MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: x,
row: y,
modifiers: KeyModifiers::empty(),
}
}
#[test]
fn test_click_opens() {
let mut state = DropdownState::new(
vec!["A".to_string(), "B".to_string(), "C".to_string()],
"Test",
);
let layout = make_layout(false);
let result = state.handle_mouse(mouse_down(12, 0), &layout);
assert_eq!(result, Some(DropdownEvent::Opened));
assert!(state.open);
}
#[test]
fn test_click_option_selects() {
let mut state = DropdownState::new(
vec!["A".to_string(), "B".to_string(), "C".to_string()],
"Test",
);
state.open = true;
let layout = make_layout(true);
let result = state.handle_mouse(mouse_down(12, 2), &layout);
assert_eq!(result, Some(DropdownEvent::SelectionChanged(1)));
assert_eq!(state.selected, 1);
assert!(!state.open);
}
#[test]
fn test_click_outside_cancels() {
let mut state = DropdownState::new(
vec!["A".to_string(), "B".to_string(), "C".to_string()],
"Test",
)
.with_selected(1);
state.toggle_open();
state.select_next();
assert_eq!(state.selected, 2);
let layout = make_layout(true);
let result = state.handle_mouse(mouse_down(0, 5), &layout);
assert_eq!(result, Some(DropdownEvent::Cancelled));
assert!(!state.open);
assert_eq!(state.selected, 1); }
#[test]
fn test_keyboard_navigation() {
let mut state = DropdownState::new(
vec!["A".to_string(), "B".to_string(), "C".to_string()],
"Test",
)
.with_focus(FocusState::Focused);
let down = KeyEvent::new(KeyCode::Down, KeyModifiers::empty());
let result = state.handle_key(down);
assert_eq!(result, Some(DropdownEvent::SelectionChanged(1)));
let up = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
let result = state.handle_key(up);
assert_eq!(result, Some(DropdownEvent::SelectionChanged(0)));
}
#[test]
fn test_enter_toggles() {
let mut state = DropdownState::new(vec!["A".to_string(), "B".to_string()], "Test")
.with_focus(FocusState::Focused);
let enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
let result = state.handle_key(enter);
assert_eq!(result, Some(DropdownEvent::Opened));
assert!(state.open);
let result = state.handle_key(enter);
assert_eq!(result, Some(DropdownEvent::Closed));
assert!(!state.open);
}
#[test]
fn test_escape_cancels() {
let mut state = DropdownState::new(
vec!["A".to_string(), "B".to_string(), "C".to_string()],
"Test",
)
.with_focus(FocusState::Focused);
state.toggle_open();
state.select_next();
assert_eq!(state.selected, 1);
let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
let result = state.handle_key(esc);
assert_eq!(result, Some(DropdownEvent::Cancelled));
assert!(!state.open);
assert_eq!(state.selected, 0); }
}