ratatui_kit/render/
tree.rs1use futures::{
2 FutureExt,
3 future::{Either, select},
4};
5use std::io::{self};
6
7use crate::{
8 ElementKey,
9 component::{ComponentHelperExt, InstantiatedComponent},
10 context::{ContextStack, SystemContext},
11 element::ElementRepr,
12 props::AnyProps,
13 terminal::{CrossTerminal, Terminal, TerminalImpl, UpdaterTerminal},
14};
15
16use super::ComponentDrawer;
17
18struct RestoreGuard;
19
20impl Drop for RestoreGuard {
21 fn drop(&mut self) {
22 ratatui::restore();
23 }
24}
25
26fn should_quit_on_ctrl_c(system_context: &SystemContext, event: &crossterm::event::Event) -> bool {
27 system_context.auto_quit_on_ctrl_c() && CrossTerminal::received_ctrl_c(event.clone())
28}
29
30#[doc(hidden)]
31pub struct Tree<'a> {
32 root_component: InstantiatedComponent,
33 props: AnyProps<'a>,
34 system_context: SystemContext,
35}
36
37impl<'a> Tree<'a> {
38 pub(crate) fn new(mut props: AnyProps<'a>, helper: Box<dyn ComponentHelperExt>) -> Self {
39 Tree {
40 root_component: InstantiatedComponent::new(
41 ElementKey::user("_root_tree_"),
42 props.borrow(),
43 helper,
44 ),
45 props,
46 system_context: SystemContext::new(),
47 }
48 }
49
50 pub(crate) fn update_once(&mut self, terminal: &mut dyn UpdaterTerminal) {
53 self.system_context.input.begin_frame();
56 let mut component_context_stack = ContextStack::root(&mut self.system_context);
57 self.root_component
58 .update(terminal, &mut component_context_stack, self.props.borrow());
59 }
60
61 pub(crate) fn draw_root(&mut self, drawer: &mut ComponentDrawer) {
63 self.root_component.draw(drawer);
64 }
65
66 fn render(&mut self, terminal: &mut Terminal) -> io::Result<()> {
67 self.update_once(terminal);
68
69 terminal.draw(|frame| {
70 let area = frame.area();
71 let mut drawer = ComponentDrawer::new(frame, area);
72 self.draw_root(&mut drawer);
73 })?;
74
75 Ok(())
76 }
77
78 async fn render_loop(&mut self, terminal: &mut Terminal) -> io::Result<()> {
79 loop {
80 self.render(terminal)?;
81 if self.system_context.should_exit() {
82 break;
83 }
84 match select(
85 self.root_component.wait().boxed_local(),
86 terminal.next_event().boxed_local(),
87 )
88 .await
89 {
90 Either::Left(((), _)) => continue,
92 Either::Right((Some(event), _)) => {
94 if should_quit_on_ctrl_c(&self.system_context, &event) {
95 break;
96 }
97 self.system_context.input.dispatch(event);
98 continue;
101 }
102 Either::Right((None, _)) => break,
104 }
105 }
106 Ok(())
107 }
108}
109
110pub(crate) async fn render_loop<E: ElementRepr>(
111 mut element: E,
112 mut terminal: Terminal,
113) -> io::Result<()> {
114 let helper = element.helper();
115 let mut tree = Tree::new(element.props_mut(), helper);
116 let _restore_guard = RestoreGuard;
117
118 tree.render_loop(&mut terminal).await
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
125
126 fn ctrl_c_event() -> Event {
127 Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL))
128 }
129
130 #[test]
131 fn ctrl_c_quits_by_default() {
132 assert!(should_quit_on_ctrl_c(
133 &SystemContext::new(),
134 &ctrl_c_event()
135 ));
136 }
137
138 #[test]
139 fn ctrl_c_is_dispatched_when_auto_quit_is_disabled() {
140 let mut system_context = SystemContext::new();
141 system_context.set_auto_quit_on_ctrl_c(false);
142
143 assert!(!should_quit_on_ctrl_c(&system_context, &ctrl_c_event()));
144 }
145
146 #[test]
147 fn regular_key_does_not_quit() {
148 let event = Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));
149
150 assert!(!should_quit_on_ctrl_c(&SystemContext::new(), &event));
151 }
152}