1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
use crate::{
action::Action,
app_context::AppContext,
app_error::AppError,
component_name::ComponentName,
components::{
component_traits::Component, core_window::CoreWindow, status_bar::StatusBar,
title_bar::TitleBar, SMALL_AREA_HEIGHT, SMALL_AREA_WIDTH,
},
event::Event,
};
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use std::{collections::HashMap, sync::Arc};
use tokio::sync::mpsc::UnboundedSender;
/// `Tui` is a struct that represents the main user interface for the
/// application. It is responsible for managing the layout and rendering of all
/// the components. It also handles the distribution of events and actions to
/// the appropriate components.
pub struct Tui {
/// The application configuration.
app_context: Arc<AppContext>,
/// An optional unbounded sender that can send actions to be processed.
action_tx: Option<UnboundedSender<Action>>,
/// A hashmap of components that make up the user interface.
components: HashMap<ComponentName, Box<dyn Component>>,
}
/// Implement the `Tui` struct.
impl Tui {
/// Create a new instance of the `Tui` struct.
///
/// # Arguments
/// * `app_context` - An Arc wrapped AppContext struct.
///
/// # Returns
/// * `Self` - The new instance of the `Tui` struct.
pub fn new(app_context: Arc<AppContext>) -> Self {
let components_iter: Vec<(ComponentName, Box<dyn Component>)> = vec![
(
ComponentName::TitleBar,
TitleBar::new(Arc::clone(&app_context))
.with_name("Tgt")
.new_boxed(),
),
(
ComponentName::CoreWindow,
CoreWindow::new(Arc::clone(&app_context))
.with_name("Core Window")
.new_boxed(),
),
(
ComponentName::StatusBar,
StatusBar::new(Arc::clone(&app_context))
.with_name("Status Bar")
.new_boxed(),
),
];
let action_tx = None;
let components: HashMap<ComponentName, Box<dyn Component>> =
components_iter.into_iter().collect();
Tui {
action_tx,
components,
app_context,
}
}
/// Register an action handler that can send actions for processing if
/// necessary.
///
/// # Arguments
/// * `tx` - An unbounded sender that can send actions.
///
/// # Returns
///
/// * `Result<()>` - An Ok result or an error.
pub fn register_action_handler(
&mut self,
tx: UnboundedSender<Action>,
) -> Result<(), AppError<Action>> {
self.action_tx = Some(tx.clone());
self.components
.iter_mut()
.try_for_each(|(_, component)| component.register_action_handler(tx.clone()))?;
Ok(())
}
/// Handle incoming events and produce actions if necessary.
///
/// # Arguments
/// * `event` - An optional event to be processed.
///
/// # Returns
///
/// * `Result<Option<Action>>` - An action to be processed or none.
pub fn handle_events(
&mut self,
event: Option<Event>,
) -> Result<Option<Action>, AppError<Action>> {
self.components
.get_mut(&ComponentName::CoreWindow)
.unwrap()
.handle_events(event.clone())
}
/// Update the state of the component based on a received action.
///
/// # Arguments
///
/// * `action` - An action that may modify the state of the component.
pub fn update(&mut self, action: Action) {
// We can not send the action only to the `CoreWindow` component because
// the `StatusBar` component needs to know the area to render the size.
self.components
.iter_mut()
.for_each(|(_, component)| component.update(action.clone()));
}
/// Render the user interface to the screen.
///
/// # Arguments
/// * `frame` - A mutable reference to the frame to be rendered.
/// * `area` - A rectangular area to render the user interface within.
///
/// # Returns
/// * `Result<()>` - An Ok result or an error.
pub fn draw(&mut self, frame: &mut ratatui::Frame<'_>, area: Rect) -> Result<(), AppError<()>> {
self.components
.get_mut(&ComponentName::StatusBar)
.unwrap()
.update(Action::UpdateArea(area));
let core_window: &mut dyn std::any::Any =
self.components.get_mut(&ComponentName::CoreWindow).unwrap();
if let Some(core_window) = core_window.downcast_mut::<CoreWindow>() {
core_window.with_small_area(area.width < SMALL_AREA_WIDTH);
}
let main_layout = Layout::new(
Direction::Vertical,
[
Constraint::Length(if self.app_context.app_config().show_title_bar {
if area.height > SMALL_AREA_HEIGHT + 5 {
3
} else {
0
}
} else {
0
}),
Constraint::Min(SMALL_AREA_HEIGHT),
Constraint::Length(if self.app_context.app_config().show_status_bar {
if area.height > SMALL_AREA_HEIGHT + 5 {
3
} else {
0
}
} else {
0
}),
],
)
.split(area);
self.components
.get_mut(&ComponentName::TitleBar)
.unwrap_or_else(|| {
tracing::error!("Failed to get component: {}", ComponentName::TitleBar);
panic!("Failed to get component: {}", ComponentName::TitleBar)
})
.draw(frame, main_layout[0])?;
self.components
.get_mut(&ComponentName::CoreWindow)
.unwrap_or_else(|| {
tracing::error!("Failed to get component: {}", ComponentName::CoreWindow);
panic!("Failed to get component: {}", ComponentName::CoreWindow)
})
.draw(frame, main_layout[1])?;
self.components
.get_mut(&ComponentName::StatusBar)
.unwrap_or_else(|| {
tracing::error!("Failed to get component: {}", ComponentName::StatusBar);
panic!("Failed to get component: {}", ComponentName::StatusBar)
})
.draw(frame, main_layout[2])?;
Ok(())
}
}