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