tui/components/
component.rs1use crossterm::event::{KeyEvent, KeyEventKind, MouseEvent};
2
3use crate::rendering::frame::Frame;
4use crate::rendering::render_context::{Size, ViewContext};
5
6pub enum Event {
8 Key(KeyEvent),
9 Paste(String),
10 Mouse(MouseEvent),
11 Tick,
12 Resize(Size),
13}
14
15impl TryFrom<crossterm::event::Event> for Event {
16 type Error = ();
17
18 fn try_from(event: crossterm::event::Event) -> Result<Self, ()> {
19 match event {
20 crossterm::event::Event::Key(key)
21 if matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat) =>
22 {
23 Ok(Event::Key(key))
24 }
25 crossterm::event::Event::Paste(text) => Ok(Event::Paste(text)),
26 crossterm::event::Event::Mouse(mouse) => Ok(Event::Mouse(mouse)),
27 crossterm::event::Event::Resize(cols, rows) => Ok(Event::Resize((cols, rows).into())),
28 _ => Err(()),
29 }
30 }
31}
32
33pub trait Component {
35 type Message;
37
38 fn on_event(&mut self, event: &Event) -> impl Future<Output = Option<Vec<Self::Message>>>;
44
45 fn render(&mut self, ctx: &ViewContext) -> Frame;
47}
48
49pub fn merge<M>(a: Option<Vec<M>>, b: Option<Vec<M>>) -> Option<Vec<M>> {
52 match (a, b) {
53 (None, other) | (other, None) => other,
54 (Some(mut a), Some(b)) => {
55 a.extend(b);
56 Some(a)
57 }
58 }
59}
60
61pub enum PickerMessage<T> {
63 Close,
64 CloseAndPopChar,
65 CloseWithChar(char),
66 Confirm(T),
67 CharTyped(char),
68 PopChar,
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use crossterm::event::{KeyCode, KeyEventState, KeyModifiers};
75
76 #[test]
77 fn try_from_key_press_succeeds() {
78 let crossterm_event = crossterm::event::Event::Key(KeyEvent {
79 code: KeyCode::Char('a'),
80 modifiers: KeyModifiers::NONE,
81 kind: KeyEventKind::Press,
82 state: KeyEventState::NONE,
83 });
84 let event = Event::try_from(crossterm_event);
85 assert!(matches!(event, Ok(Event::Key(_))));
86 }
87
88 #[test]
89 fn try_from_key_release_fails() {
90 let crossterm_event = crossterm::event::Event::Key(KeyEvent {
91 code: KeyCode::Char('a'),
92 modifiers: KeyModifiers::NONE,
93 kind: KeyEventKind::Release,
94 state: KeyEventState::NONE,
95 });
96 assert!(Event::try_from(crossterm_event).is_err());
97 }
98
99 #[test]
100 fn try_from_paste_succeeds() {
101 let crossterm_event = crossterm::event::Event::Paste("hello".to_string());
102 let event = Event::try_from(crossterm_event);
103 assert!(matches!(event, Ok(Event::Paste(text)) if text == "hello"));
104 }
105
106 #[test]
107 fn try_from_resize_succeeds() {
108 let crossterm_event = crossterm::event::Event::Resize(80, 24);
109 let event = Event::try_from(crossterm_event);
110 assert!(matches!(event, Ok(Event::Resize(size)) if size.width == 80 && size.height == 24));
111 }
112
113 #[test]
114 fn try_from_focus_gained_fails() {
115 let crossterm_event = crossterm::event::Event::FocusGained;
116 assert!(Event::try_from(crossterm_event).is_err());
117 }
118}