cyberpunk_mod_manager/ui/
ui.rs1use std::path::PathBuf;
2use tui::{
3 layout::{Rect, Layout, Direction, Constraint, Alignment},
4 backend::Backend,
5 Frame,
6 text::{Spans, Span, Text},
7 widgets::{Paragraph, Block, Borders, Wrap, ListItem, List, ListState, Clear}
8};
9use tui_logger::TuiLoggerWidget;
10
11use crate::{
12 constants::{MIN_TERM_WIDTH, MIN_TERM_HEIGHT, ERROR_TEXT_STYLE,
13 APP_TITLE, FOCUS_STYLE, LOG_ERROR_STYLE,
14 LOG_DEBUG_STYLE, LOG_WARN_STYLE, LOG_TRACE_STYLE,
15 LOG_INFO_STYLE, MOD_FOLDER_INPUT_EMPTY_ERROR, NOT_A_DIRECTORY_ERROR,
16 CYBERPUNK_FOLDER_INPUT_EMPTY_ERROR, NOT_A_VALID_CYBERPUNK_FOLDER_ERROR,
17 CYBERPUNK_STYLE_YELLOW, CYBERPUNK_STYLE_PINK, CYBERPUNK_STYLE_CYAN,
18 CYBERPUNK_STYLE_YELLOW_DARK, CYBERPUNK_STYLE_PINK_DARK, CYBERPUNK_STYLE_CYAN_DARK
19 },
20 App, app::{state::{Focus, AppStatus}, utils::ModOptions},
21};
22
23pub fn check_size(rect: &Rect) -> String {
25 let mut msg = String::new();
26 if rect.width < MIN_TERM_WIDTH {
27 msg.push_str(&format!("For optimal viewing experience, Terminal width should be >= {}, (current {})",MIN_TERM_WIDTH, rect.width));
28 }
29 else if rect.height < MIN_TERM_HEIGHT {
30 msg.push_str(&format!("For optimal viewing experience, Terminal height should be >= {}, (current {})",MIN_TERM_HEIGHT, rect.height));
31 }
32 else {
33 msg.push_str("Size OK");
34 }
35 msg
36}
37
38pub fn draw_size_error<B>(rect: &mut Frame<B>, size: &Rect, msg: String)
40where
41 B: Backend,
42{
43 let chunks = Layout::default()
44 .direction(Direction::Vertical)
45 .constraints([Constraint::Length(3), Constraint::Min(10)].as_ref())
46 .split(*size);
47
48 let title = draw_title(false);
49 rect.render_widget(title, chunks[0]);
50
51 let mut text = vec![Spans::from(Span::styled(&msg, ERROR_TEXT_STYLE))];
52 text.append(&mut vec![Spans::from(Span::raw("Resize the window to continue, or press 'q' to quit."))]);
53 let body = Paragraph::new(text)
54 .block(Block::default().borders(Borders::ALL))
55 .alignment(Alignment::Center);
56 rect.render_widget(body, chunks[1]);
57}
58
59pub fn draw_title<'a>(dark_mode: bool) -> Paragraph<'a> {
61
62 let title_style = if dark_mode {
63 CYBERPUNK_STYLE_YELLOW_DARK
64 } else {
65 CYBERPUNK_STYLE_YELLOW
66 };
67
68 Paragraph::new(APP_TITLE)
69 .alignment(Alignment::Center)
70 .block(
71 Block::default()
72 .borders(Borders::ALL)
73 .style(title_style)
74 )
75}
76
77pub fn draw_select_folder<B: Backend>(f: &mut Frame<B>, app: &App) {
78
79 let submit_style = if app.state.focus == Focus::Submit {
80 FOCUS_STYLE
81 } else {
82 CYBERPUNK_STYLE_CYAN
83 };
84
85 let chunks = Layout::default()
86 .direction(Direction::Vertical)
87 .constraints([
88 Constraint::Percentage(10),
89 Constraint::Percentage(40),
90 Constraint::Percentage(40),
91 Constraint::Percentage(10)
92 ].as_ref())
93 .split(f.size());
94
95 let title = Paragraph::new(Text::styled("Select Folder, Press <i> to edit, <Tab> to change focus and <Enter> to submit", CYBERPUNK_STYLE_CYAN))
96 .block(Block::default().borders(Borders::ALL))
97 .style(CYBERPUNK_STYLE_CYAN)
98 .wrap(Wrap { trim: true });
99
100 let mod_folder_text = app.state.select_folder_form[0].clone();
101 let mod_folder_input_style = if app.state.focus == Focus::ModFolderInput {
102 if app.state.status == AppStatus::UserInput {
103 CYBERPUNK_STYLE_PINK
104 } else {
105 FOCUS_STYLE
106 }
107 } else if mod_folder_text.contains(MOD_FOLDER_INPUT_EMPTY_ERROR) || mod_folder_text.contains(NOT_A_DIRECTORY_ERROR){
108 ERROR_TEXT_STYLE
109 } else {
110 CYBERPUNK_STYLE_CYAN
111 };
112 let mod_folder = Paragraph::new(Text::raw(mod_folder_text))
113 .block(Block::default().borders(Borders::ALL).title("Mods Folder"))
114 .style(mod_folder_input_style)
115 .wrap(Wrap { trim: true });
116
117 let cyberpunk_folder_text = app.state.select_folder_form[1].clone();
118 let cyberpunk_folder_input_style = if app.state.focus == Focus::CyberpunkFolderInput {
119 if app.state.status == AppStatus::UserInput {
120 CYBERPUNK_STYLE_PINK
121 } else {
122 FOCUS_STYLE
123 }
124 } else if cyberpunk_folder_text.contains(CYBERPUNK_FOLDER_INPUT_EMPTY_ERROR)
125 || cyberpunk_folder_text.contains(NOT_A_DIRECTORY_ERROR)
126 || cyberpunk_folder_text.contains(NOT_A_VALID_CYBERPUNK_FOLDER_ERROR)
127 {
128 ERROR_TEXT_STYLE
129 } else {
130 CYBERPUNK_STYLE_CYAN
131 };
132 let cyberpunk_folder = Paragraph::new(Text::raw(cyberpunk_folder_text))
133 .block(Block::default().borders(Borders::ALL).title("Cyberpunk Folder"))
134 .style(cyberpunk_folder_input_style)
135 .wrap(Wrap { trim: true });
136
137 let submit_button = Paragraph::new("Submit")
138 .block(Block::default().borders(Borders::ALL))
139 .style(submit_style)
140 .wrap(Wrap { trim: true });
141
142 if app.state.status == AppStatus::UserInput && app.state.focus == Focus::ModFolderInput {
144 f.set_cursor(
145 chunks[1].x + app.state.cursor_position.unwrap_or_else(||0) as u16 + 1,
146 chunks[1].y + 1,
147 );
148 } else if app.state.status == AppStatus::UserInput && app.state.focus == Focus::CyberpunkFolderInput {
149 f.set_cursor(
150 chunks[2].x + app.state.cursor_position.unwrap_or_else(||0) as u16 + 1,
151 chunks[2].y + 1,
152 );
153 }
154
155 f.render_widget(title, chunks[0]);
156 f.render_widget(mod_folder, chunks[1]);
157 f.render_widget(cyberpunk_folder, chunks[2]);
158 f.render_widget(submit_button, chunks[3]);
159}
160
161pub fn draw_explore<B: Backend>(f: &mut Frame<B>, app: &App, file_list_state: &mut ListState) {
162 let main_chunks = Layout::default()
164 .direction(Direction::Vertical)
165 .constraints([
166 Constraint::Percentage(10),
167 Constraint::Percentage(70),
168 Constraint::Percentage(10),
169 Constraint::Percentage(10)
170 ].as_ref())
171 .split(f.size());
172
173 let chunks = Layout::default()
174 .direction(Direction::Horizontal)
175 .constraints([
176 Constraint::Percentage(70),
177 Constraint::Percentage(30)
178 ].as_ref())
179 .split(main_chunks[1]);
180
181 let title_widget = draw_title(app.mod_popup.is_some());
182
183 let current_folder = app.mod_folder.clone().unwrap_or_else(|| PathBuf::new());
184 let current_folder_string = if current_folder.is_dir() {
186 current_folder.to_string_lossy().to_string()
187 } else {
188 "No folder selected".to_string()
189 };
190 let current_folder_widget_style = if app.mod_popup.is_some() {
191 CYBERPUNK_STYLE_PINK_DARK
192 } else {
193 CYBERPUNK_STYLE_PINK
194 };
195 let current_folder_widget = Paragraph::new(Text::raw(current_folder_string))
196 .block(Block::default().borders(Borders::ALL).title("Mod Folder"))
197 .style(current_folder_widget_style)
198 .wrap(Wrap { trim: true });
199
200 let cyberpunk_folder = app.cyberpunk_folder.clone().unwrap_or_else(|| PathBuf::new());
201 let cyberpunk_folder_string = if cyberpunk_folder.is_dir() {
203 cyberpunk_folder.to_string_lossy().to_string()
204 } else {
205 "No folder selected".to_string()
206 };
207 let cyberpunk_folder_widget_style = if app.mod_popup.is_some() {
208 CYBERPUNK_STYLE_YELLOW_DARK
209 } else {
210 CYBERPUNK_STYLE_YELLOW
211 };
212 let cyberpunk_folder_widget = Paragraph::new(Text::raw(cyberpunk_folder_string))
213 .block(Block::default().borders(Borders::ALL).title("Cyberpunk Folder"))
214 .style(cyberpunk_folder_widget_style)
215 .wrap(Wrap { trim: true });
216
217 let items: Vec<ListItem> = app
219 .state.file_list
220 .items
221 .iter()
222 .map(|(name, _size)| {
223 ListItem::new(Text::from(name.clone()))
224 })
225 .collect();
226
227 let item_list_style = if app.mod_popup.is_some() {
228 CYBERPUNK_STYLE_CYAN_DARK
229 } else {
230 CYBERPUNK_STYLE_CYAN
231 };
232
233 let items_list = List::new(items)
235 .block(Block::default().borders(Borders::ALL).title("Available files"))
236 .highlight_style(current_folder_widget_style)
237 .highlight_symbol(">> ")
238 .style(item_list_style);
239
240 let log_widget = TuiLoggerWidget::default()
241 .style_error(LOG_ERROR_STYLE)
242 .style_debug(LOG_DEBUG_STYLE)
243 .style_warn(LOG_WARN_STYLE)
244 .style_trace(LOG_TRACE_STYLE)
245 .style_info(LOG_INFO_STYLE)
246 .block(
247 Block::default()
248 .title("Logs")
249 .border_style(item_list_style)
250 .borders(Borders::ALL),
251 )
252 .output_timestamp(None)
253 .output_target(false)
254 .output_level(None);
255
256 f.render_widget(title_widget, main_chunks[0]);
257 f.render_stateful_widget(items_list, chunks[0], file_list_state);
258 f.render_widget(log_widget, chunks[1]);
259 f.render_widget(current_folder_widget, main_chunks[2]);
260 f.render_widget(cyberpunk_folder_widget, main_chunks[3]);
261}
262
263pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
265 let popup_layout = Layout::default()
266 .direction(Direction::Vertical)
267 .constraints(
268 [
269 Constraint::Percentage((100 - percent_y) / 2),
270 Constraint::Percentage(percent_y),
271 Constraint::Percentage((100 - percent_y) / 2),
272 ]
273 .as_ref(),
274 )
275 .split(r);
276
277 Layout::default()
278 .direction(Direction::Horizontal)
279 .constraints(
280 [
281 Constraint::Percentage((100 - percent_x) / 2),
282 Constraint::Percentage(percent_x),
283 Constraint::Percentage((100 - percent_x) / 2),
284 ]
285 .as_ref(),
286 )
287 .split(popup_layout[1])[1]
288 }
289
290pub fn draw_mod_popup<B: Backend>(f: &mut Frame<B>, app: &App, mod_options_state: &mut ListState) {
291 let clear_area = centered_rect(90, 90, f.size());
292 let popup_area = centered_rect(80, 80, f.size());
293 f.render_widget(Clear, clear_area);
295 f.render_widget(Block::default()
296 .borders(Borders::ALL)
297 .border_style(CYBERPUNK_STYLE_YELLOW)
298 .title("What do you want to do?"), clear_area);
299
300 let chunks = Layout::default()
301 .direction(Direction::Vertical)
302 .constraints(
303 [
304 Constraint::Length(3),
305 Constraint::Percentage(75),
306 Constraint::Length(3),
307 ]
308 .as_ref(),
309 )
310 .split(popup_area);
311
312 let mod_name = app.mod_popup.as_ref().unwrap().get_mod_name();
313 let mod_name_widget = Paragraph::new(Text::raw(mod_name))
314 .block(Block::default().borders(Borders::ALL).title("Mod Name"))
315 .style(CYBERPUNK_STYLE_YELLOW)
316 .wrap(Wrap { trim: true });
317
318 let items: Vec<ListItem> = ModOptions::get_all_options()
319 .iter()
320 .map(|mod_option| {
321 ListItem::new(Text::from(mod_option.to_string()))
322 })
323 .collect();
324 let items_list = List::new(items)
325 .block(Block::default().borders(Borders::ALL).title("Available files"))
326 .highlight_style(CYBERPUNK_STYLE_PINK)
327 .highlight_symbol(">> ")
328 .style(CYBERPUNK_STYLE_CYAN);
329
330 let mod_install_status_bool = app.mod_popup.as_ref().unwrap().get_mod_install_status();
331 let mod_install_status = if mod_install_status_bool.is_none() {
332 "Checking...".to_string()
333 } else {
334 if mod_install_status_bool.unwrap() {
335 "Installed".to_string()
336 } else {
337 "Not Installed".to_string()
338 }
339 };
340 let mod_install_status_widget = Paragraph::new(Text::raw(mod_install_status))
341 .block(Block::default().borders(Borders::ALL).title("Mod Install Status"))
342 .style(CYBERPUNK_STYLE_YELLOW)
343 .wrap(Wrap { trim: true });
344
345 f.render_widget(mod_name_widget, chunks[0]);
346 f.render_stateful_widget(items_list, chunks[1], mod_options_state);
347 f.render_widget(mod_install_status_widget, chunks[2]);
348}