rush_sync_server/ui/
widget.rs1use crate::core::prelude::*;
3use crate::input::state::InputStateBackup;
4use ratatui::widgets::Paragraph;
5
6pub trait Widget {
8 fn render(&self) -> Paragraph<'_>;
9 fn handle_input(&mut self, key: KeyEvent) -> Option<String>;
10}
11
12pub trait CursorWidget: Widget {
14 fn render_with_cursor(&self) -> (Paragraph<'_>, Option<(u16, u16)>);
15}
16
17pub trait StatefulWidget<T = InputStateBackup> {
19 fn export_state(&self) -> T;
20 fn import_state(&mut self, state: T);
21}
22
23pub trait AnimatedWidget {
25 fn tick(&mut self);
26}
27
28pub trait InputWidget: Widget + CursorWidget + StatefulWidget + AnimatedWidget {}
30
31impl<T> InputWidget for T where T: Widget + CursorWidget + StatefulWidget + AnimatedWidget {}
33
34pub mod utils {
36 use super::*;
37 use ratatui::{
38 style::Style,
39 widgets::{Block, Borders},
40 };
41
42 pub fn simple_text(content: &str, style: Style) -> Paragraph<'_> {
43 Paragraph::new(content.to_string())
44 .style(style)
45 .block(Block::default().borders(Borders::NONE))
46 }
47
48 pub fn has_cursor<T: Widget>(_: &T) -> bool {
49 std::any::type_name::<T>().contains("CursorWidget")
50 }
51}
52
53#[cfg(test)]
55mod examples {
56 use super::*;
57
58 #[derive(Debug)]
59 pub struct SimpleWidget(String);
60
61 impl Widget for SimpleWidget {
62 fn render(&self) -> Paragraph<'_> {
63 utils::simple_text(&self.0, ratatui::style::Style::default())
64 }
65
66 fn handle_input(&mut self, _: KeyEvent) -> Option<String> {
67 None
68 }
69 }
70
71 #[derive(Debug)]
72 pub struct FullInputWidget {
73 content: String,
74 cursor_pos: usize,
75 visible: bool,
76 }
77
78 impl Widget for FullInputWidget {
79 fn render(&self) -> Paragraph<'_> {
80 self.render_with_cursor().0
81 }
82
83 fn handle_input(&mut self, _: KeyEvent) -> Option<String> {
84 Some("handled".to_string())
85 }
86 }
87
88 impl CursorWidget for FullInputWidget {
89 fn render_with_cursor(&self) -> (Paragraph<'_>, Option<(u16, u16)>) {
90 let para = utils::simple_text(&self.content, ratatui::style::Style::default());
91 let cursor = if self.visible {
92 Some((self.cursor_pos as u16, 0))
93 } else {
94 None
95 };
96 (para, cursor)
97 }
98 }
99
100 impl StatefulWidget for FullInputWidget {
101 fn export_state(&self) -> InputStateBackup {
102 InputStateBackup {
103 content: self.content.clone(),
104 history: vec![],
105 cursor_pos: self.cursor_pos,
106 }
107 }
108
109 fn import_state(&mut self, state: InputStateBackup) {
110 self.content = state.content;
111 self.cursor_pos = state.cursor_pos;
112 }
113 }
114
115 impl AnimatedWidget for FullInputWidget {
116 fn tick(&mut self) {
117 self.visible = !self.visible;
118 }
119 }
120
121 #[test]
122 fn test_widget_system() {
123 let mut simple = SimpleWidget("test".to_string());
124 let _para = simple.render();
125 assert_eq!(simple.handle_input(KeyEvent::from(KeyCode::Enter)), None);
126
127 let mut full = FullInputWidget {
128 content: "input".to_string(),
129 cursor_pos: 5,
130 visible: true,
131 };
132
133 let _para = full.render();
135 let (_para, cursor) = full.render_with_cursor();
136 assert_eq!(cursor, Some((5, 0)));
137
138 let state = full.export_state();
139 full.content = "changed".to_string();
140 full.import_state(state);
141 assert_eq!(full.content, "input");
142
143 let old_visible = full.visible;
144 full.tick();
145 assert_ne!(full.visible, old_visible);
146 }
147}
148
149pub mod compat {
151 pub use super::{
152 AnimatedWidget as Tickable, CursorWidget as RenderWithCursor,
153 InputWidget as InputWidgetFull, StatefulWidget as Stateful, Widget,
154 };
155}