1use crate::{
2 app::{state::Focus, App},
3 ui::rendering::view::{BodyHelp, TitleBody, Zen},
4};
5use ratatui::{
6 style::{Color, Modifier},
7 Frame,
8};
9use rendering::{
10 popup::{
11 widgets::{CommandPalette, DateTimePicker, TagPicker},
12 CardPrioritySelector, CardStatusSelector, ChangeDateFormat, ChangeTheme, ChangeView,
13 ConfirmDiscardCardChanges, CustomHexColorPrompt, EditGeneralConfig, EditSpecificKeybinding,
14 EditThemeStyle, FilterByTag, SaveThemePrompt, SelectDefaultView, ViewCard,
15 },
16 view::{
17 BodyHelpLog, BodyLog, ConfigMenu, CreateTheme, EditKeybindings, HelpMenu, LoadASave,
18 LoadCloudSave, LogView, Login, MainMenuView, NewBoardForm, NewCardForm, ResetPassword,
19 Signup, TitleBodyHelp, TitleBodyHelpLog, TitleBodyLog,
20 },
21};
22use serde::{Deserialize, Serialize};
23use std::fmt::{self, Formatter};
24use strum::{Display, EnumIter, EnumString};
25
26pub mod inbuilt_themes;
27pub mod rendering;
28pub mod text_box;
29pub mod theme;
30pub mod ui_helper;
31pub mod ui_main;
32pub mod widgets;
33
34#[derive(Debug, Clone, Serialize, Deserialize, EnumIter, Display, Copy)]
35pub enum TextColorOptions {
36 Black,
37 Blue,
38 Cyan,
39 DarkGray,
40 Gray,
41 Green,
42 LightBlue,
43 LightCyan,
44 LightGreen,
45 LightMagenta,
46 LightRed,
47 LightYellow,
48 Magenta,
49 None,
50 #[strum(to_string = "HEX #{0:02x}{1:02x}{2:02x}")]
51 HEX(u8, u8, u8),
52 Red,
53 White,
54 Yellow,
55}
56
57impl From<Color> for TextColorOptions {
58 fn from(color: Color) -> Self {
59 match color {
60 Color::Black => TextColorOptions::Black,
61 Color::Blue => TextColorOptions::Blue,
62 Color::Cyan => TextColorOptions::Cyan,
63 Color::DarkGray => TextColorOptions::DarkGray,
64 Color::Gray => TextColorOptions::Gray,
65 Color::Green => TextColorOptions::Green,
66 Color::LightBlue => TextColorOptions::LightBlue,
67 Color::LightCyan => TextColorOptions::LightCyan,
68 Color::LightGreen => TextColorOptions::LightGreen,
69 Color::LightMagenta => TextColorOptions::LightMagenta,
70 Color::LightRed => TextColorOptions::LightRed,
71 Color::LightYellow => TextColorOptions::LightYellow,
72 Color::Magenta => TextColorOptions::Magenta,
73 Color::Red => TextColorOptions::Red,
74 Color::Reset => TextColorOptions::None,
75 Color::Rgb(r, g, b) => TextColorOptions::HEX(r, g, b),
76 Color::White => TextColorOptions::White,
77 Color::Yellow => TextColorOptions::Yellow,
78 _ => TextColorOptions::None,
79 }
80 }
81}
82
83impl From<TextColorOptions> for Color {
84 fn from(color: TextColorOptions) -> Self {
85 match color {
86 TextColorOptions::Black => Color::Black,
87 TextColorOptions::Blue => Color::Blue,
88 TextColorOptions::Cyan => Color::Cyan,
89 TextColorOptions::DarkGray => Color::DarkGray,
90 TextColorOptions::Gray => Color::Gray,
91 TextColorOptions::Green => Color::Green,
92 TextColorOptions::LightBlue => Color::LightBlue,
93 TextColorOptions::LightCyan => Color::LightCyan,
94 TextColorOptions::LightGreen => Color::LightGreen,
95 TextColorOptions::LightMagenta => Color::LightMagenta,
96 TextColorOptions::LightRed => Color::LightRed,
97 TextColorOptions::LightYellow => Color::LightYellow,
98 TextColorOptions::Magenta => Color::Magenta,
99 TextColorOptions::None => Color::Reset,
100 TextColorOptions::Red => Color::Red,
101 TextColorOptions::HEX(r, g, b) => Color::Rgb(r, g, b),
102 TextColorOptions::White => Color::White,
103 TextColorOptions::Yellow => Color::Yellow,
104 }
105 }
106}
107
108impl TextColorOptions {
109 pub fn to_rgb(&self) -> (u8, u8, u8) {
110 match self {
111 TextColorOptions::Black => (0, 0, 0),
112 TextColorOptions::Blue => (0, 0, 128),
113 TextColorOptions::Cyan => (0, 128, 128),
114 TextColorOptions::DarkGray => (128, 128, 128),
115 TextColorOptions::Gray => (192, 192, 192),
116 TextColorOptions::Green => (0, 128, 0),
117 TextColorOptions::LightBlue => (0, 0, 255),
118 TextColorOptions::LightCyan => (0, 255, 255),
119 TextColorOptions::LightGreen => (255, 255, 0),
120 TextColorOptions::LightMagenta => (255, 0, 255),
121 TextColorOptions::LightRed => (255, 0, 0),
122 TextColorOptions::LightYellow => (0, 255, 0),
123 TextColorOptions::Magenta => (128, 0, 128),
124 TextColorOptions::None => (0, 0, 0),
125 TextColorOptions::Red => (128, 0, 0),
126 TextColorOptions::HEX(r, g, b) => (*r, *g, *b),
127 TextColorOptions::White => (255, 255, 255),
128 TextColorOptions::Yellow => (128, 128, 0),
129 }
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, Display, EnumIter)]
134pub enum TextModifierOptions {
135 Bold,
136 CrossedOut,
137 Dim,
138 Hidden,
139 Italic,
140 None,
141 RapidBlink,
142 Reversed,
143 SlowBlink,
144 Underlined,
145}
146
147impl From<TextModifierOptions> for Modifier {
148 fn from(modifier: TextModifierOptions) -> Self {
149 match modifier {
150 TextModifierOptions::Bold => Modifier::BOLD,
151 TextModifierOptions::CrossedOut => Modifier::CROSSED_OUT,
152 TextModifierOptions::Dim => Modifier::DIM,
153 TextModifierOptions::Hidden => Modifier::HIDDEN,
154 TextModifierOptions::Italic => Modifier::ITALIC,
155 TextModifierOptions::None => Modifier::empty(),
156 TextModifierOptions::RapidBlink => Modifier::RAPID_BLINK,
157 TextModifierOptions::Reversed => Modifier::REVERSED,
158 TextModifierOptions::SlowBlink => Modifier::SLOW_BLINK,
159 TextModifierOptions::Underlined => Modifier::UNDERLINED,
160 }
161 }
162}
163
164pub trait Renderable {
165 fn render(rect: &mut Frame, app: &mut App, is_active: bool);
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Copy, Default, EnumString)]
169pub enum View {
170 BodyHelp,
171 BodyHelpLog,
172 BodyLog,
173 ConfigMenu,
174 CreateTheme,
175 EditKeybindings,
176 HelpMenu,
177 LoadCloudSave,
178 LoadLocalSave,
179 Login,
180 LogsOnly,
181 MainMenu,
182 NewBoard,
183 NewCard,
184 ResetPassword,
185 SignUp,
186 TitleBody,
187 TitleBodyHelp,
188 TitleBodyHelpLog,
189 TitleBodyLog,
190 #[default]
191 Zen,
192}
193
194impl View {
195 pub fn from_string(s: &str) -> Option<View> {
196 match s {
197 "Body and Help" => Some(View::BodyHelp),
198 "Body, Help and Log" => Some(View::BodyHelpLog),
199 "Body and Log" => Some(View::BodyLog),
200 "Config" => Some(View::ConfigMenu),
201 "Create Theme" => Some(View::CreateTheme),
202 "Edit Keybindings" => Some(View::EditKeybindings),
203 "Help Menu" => Some(View::HelpMenu),
204 "Load a Save (Cloud)" => Some(View::LoadCloudSave),
205 "Load a Save (Local)" => Some(View::LoadLocalSave),
206 "Login" => Some(View::Login),
207 "Logs Only" => Some(View::LogsOnly),
208 "Main Menu" => Some(View::MainMenu),
209 "New Board" => Some(View::NewBoard),
210 "New Card" => Some(View::NewCard),
211 "Reset Password" => Some(View::ResetPassword),
212 "Sign Up" => Some(View::SignUp),
213 "Title and Body" => Some(View::TitleBody),
214 "Title, Body and Help" => Some(View::TitleBodyHelp),
215 "Title, Body, Help and Log" => Some(View::TitleBodyHelpLog),
216 "Title, Body and Log" => Some(View::TitleBodyLog),
217 "Zen" => Some(View::Zen),
218 _ => None,
219 }
220 }
221
222 pub fn from_number(n: u8) -> View {
223 match n {
224 1 => View::Zen,
225 2 => View::TitleBody,
226 3 => View::BodyHelp,
227 4 => View::BodyLog,
228 5 => View::TitleBodyHelp,
229 6 => View::TitleBodyLog,
230 7 => View::BodyHelpLog,
231 8 => View::TitleBodyHelpLog,
232 9 => View::LogsOnly,
233 _ => {
234 log::error!("Invalid View: {}", n);
235 View::TitleBody
236 }
237 }
238 }
239
240 pub fn get_available_targets(&self) -> Vec<Focus> {
241 match self {
242 View::BodyHelp => vec![Focus::Body, Focus::Help],
243 View::BodyHelpLog => vec![Focus::Body, Focus::Help, Focus::Log],
244 View::BodyLog => vec![Focus::Body, Focus::Log],
245 View::ConfigMenu => vec![Focus::ConfigTable, Focus::SubmitButton, Focus::ExtraFocus],
246 View::CreateTheme => vec![Focus::ThemeEditor, Focus::SubmitButton, Focus::ExtraFocus],
247 View::EditKeybindings => vec![Focus::EditKeybindingsTable, Focus::SubmitButton],
248 View::HelpMenu => vec![Focus::Help, Focus::Log],
249 View::LoadCloudSave => vec![Focus::Body],
250 View::LoadLocalSave => vec![Focus::Body],
251 View::Login => vec![
252 Focus::Title,
253 Focus::EmailIDField,
254 Focus::PasswordField,
255 Focus::ExtraFocus,
256 Focus::SubmitButton,
257 ],
258 View::LogsOnly => vec![Focus::Log],
259 View::MainMenu => vec![Focus::MainMenu, Focus::Help, Focus::Log],
260 View::NewBoard => vec![
261 Focus::NewBoardName,
262 Focus::NewBoardDescription,
263 Focus::SubmitButton,
264 ],
265 View::NewCard => vec![
266 Focus::CardName,
267 Focus::CardDescription,
268 Focus::CardDueDate,
269 Focus::SubmitButton,
270 ],
271 View::ResetPassword => vec![
272 Focus::Title,
273 Focus::EmailIDField,
274 Focus::SendResetPasswordLinkButton,
275 Focus::ResetPasswordLinkField,
276 Focus::PasswordField,
277 Focus::ConfirmPasswordField,
278 Focus::ExtraFocus,
279 Focus::SubmitButton,
280 ],
281 View::SignUp => vec![
282 Focus::Title,
283 Focus::EmailIDField,
284 Focus::PasswordField,
285 Focus::ConfirmPasswordField,
286 Focus::ExtraFocus,
287 Focus::SubmitButton,
288 ],
289 View::TitleBody => vec![Focus::Title, Focus::Body],
290 View::TitleBodyHelp => vec![Focus::Title, Focus::Body, Focus::Help],
291 View::TitleBodyHelpLog => vec![Focus::Title, Focus::Body, Focus::Help, Focus::Log],
292 View::TitleBodyLog => vec![Focus::Title, Focus::Body, Focus::Log],
293 View::Zen => vec![Focus::Body],
294 }
295 }
296
297 pub fn all_views_as_string() -> Vec<String> {
298 View::views_with_kanban_board()
299 .iter()
300 .map(|x| x.to_string())
301 .collect()
302 }
303
304 pub fn views_with_kanban_board() -> Vec<View> {
305 vec![
306 View::Zen,
307 View::TitleBody,
308 View::BodyHelp,
309 View::BodyLog,
310 View::TitleBodyHelp,
311 View::TitleBodyLog,
312 View::BodyHelpLog,
313 View::TitleBodyHelpLog,
314 ]
315 }
316
317 pub fn render(self, rect: &mut Frame, app: &mut App, is_active: bool) {
318 let skip_setting_focus = if let Some(popup) = app.state.z_stack.last() {
319 !popup.requires_previous_element_disabled()
320 && !popup.requires_previous_element_control()
321 } else {
322 false
323 };
324 if is_active && !skip_setting_focus {
325 let current_focus = app.state.focus;
326 if !self.get_available_targets().contains(¤t_focus)
327 && !self.get_available_targets().is_empty()
328 {
329 app.state.set_focus(self.get_available_targets()[0]);
330 }
331 }
332 match self {
333 View::Zen => {
334 Zen::render(rect, app, is_active);
335 }
336 View::TitleBody => {
337 TitleBody::render(rect, app, is_active);
338 }
339 View::BodyHelp => {
340 BodyHelp::render(rect, app, is_active);
341 }
342 View::BodyLog => {
343 BodyLog::render(rect, app, is_active);
344 }
345 View::TitleBodyHelp => {
346 TitleBodyHelp::render(rect, app, is_active);
347 }
348 View::TitleBodyLog => {
349 TitleBodyLog::render(rect, app, is_active);
350 }
351 View::BodyHelpLog => {
352 BodyHelpLog::render(rect, app, is_active);
353 }
354 View::TitleBodyHelpLog => {
355 TitleBodyHelpLog::render(rect, app, is_active);
356 }
357 View::ConfigMenu => {
358 ConfigMenu::render(rect, app, is_active);
359 }
360 View::EditKeybindings => {
361 EditKeybindings::render(rect, app, is_active);
362 }
363 View::MainMenu => {
364 MainMenuView::render(rect, app, is_active);
365 }
366 View::HelpMenu => {
367 HelpMenu::render(rect, app, is_active);
368 }
369 View::LogsOnly => {
370 LogView::render(rect, app, is_active);
371 }
372 View::NewBoard => {
373 NewBoardForm::render(rect, app, is_active);
374 }
375 View::NewCard => NewCardForm::render(rect, app, is_active),
376 View::LoadLocalSave => {
377 LoadASave::render(rect, app, is_active);
378 }
379 View::CreateTheme => CreateTheme::render(rect, app, is_active),
380 View::Login => Login::render(rect, app, is_active),
381 View::SignUp => Signup::render(rect, app, is_active),
382 View::ResetPassword => ResetPassword::render(rect, app, is_active),
383 View::LoadCloudSave => LoadCloudSave::render(rect, app, is_active),
384 }
385 }
386}
387
388impl fmt::Display for View {
389 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
390 match self {
391 View::BodyHelp => write!(f, "Body and Help"),
392 View::BodyHelpLog => write!(f, "Body, Help and Log"),
393 View::BodyLog => write!(f, "Body and Log"),
394 View::ConfigMenu => write!(f, "Config"),
395 View::CreateTheme => write!(f, "Create Theme"),
396 View::EditKeybindings => write!(f, "Edit Keybindings"),
397 View::HelpMenu => write!(f, "Help Menu"),
398 View::LoadCloudSave => write!(f, "Load a Save (Cloud)"),
399 View::LoadLocalSave => write!(f, "Load a Save (Local)"),
400 View::Login => write!(f, "Login"),
401 View::LogsOnly => write!(f, "Logs Only"),
402 View::MainMenu => write!(f, "Main Menu"),
403 View::NewBoard => write!(f, "New Board"),
404 View::NewCard => write!(f, "New Card"),
405 View::ResetPassword => write!(f, "Reset Password"),
406 View::SignUp => write!(f, "Sign Up"),
407 View::TitleBody => write!(f, "Title and Body"),
408 View::TitleBodyHelp => write!(f, "Title, Body and Help"),
409 View::TitleBodyHelpLog => write!(f, "Title, Body, Help and Log"),
410 View::TitleBodyLog => write!(f, "Title, Body and Log"),
411 View::Zen => write!(f, "Zen"),
412 }
413 }
414}
415
416#[derive(Clone, PartialEq, Debug, Copy)]
417pub enum PopUp {
418 ViewCard,
419 CommandPalette,
420 EditSpecificKeyBinding,
421 ChangeView,
422 CardStatusSelector,
423 EditGeneralConfig,
424 SelectDefaultView,
425 ChangeDateFormatPopup,
426 ChangeTheme,
427 EditThemeStyle,
428 SaveThemePrompt,
429 CustomHexColorPromptFG,
430 CustomHexColorPromptBG,
431 ConfirmDiscardCardChanges,
432 CardPrioritySelector,
433 FilterByTag,
434 DateTimePicker,
435 TagPicker,
436}
437
438impl fmt::Display for PopUp {
439 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
440 match *self {
441 PopUp::ViewCard => write!(f, "Card View"),
442 PopUp::CommandPalette => write!(f, "Command Palette"),
443 PopUp::EditSpecificKeyBinding => write!(f, "Edit Specific Key Binding"),
444 PopUp::ChangeView => write!(f, "Change View"),
445 PopUp::CardStatusSelector => write!(f, "Change Card Status"),
446 PopUp::EditGeneralConfig => write!(f, "Edit General Config"),
447 PopUp::SelectDefaultView => write!(f, "Select Default View"),
448 PopUp::ChangeDateFormatPopup => write!(f, "Change Date Format"),
449 PopUp::ChangeTheme => write!(f, "Change Theme"),
450 PopUp::EditThemeStyle => write!(f, "Edit Theme Style"),
451 PopUp::SaveThemePrompt => write!(f, "Save Theme Prompt"),
452 PopUp::CustomHexColorPromptFG => write!(f, "Custom Hex Color Prompt FG"),
453 PopUp::CustomHexColorPromptBG => write!(f, "Custom Hex Color Prompt BG"),
454 PopUp::ConfirmDiscardCardChanges => write!(f, "Confirm Discard Card Changes"),
455 PopUp::CardPrioritySelector => write!(f, "Change Card Priority"),
456 PopUp::FilterByTag => write!(f, "Filter By Tag"),
457 PopUp::DateTimePicker => write!(f, "Date Time Picker"),
458 PopUp::TagPicker => write!(f, "Tag Picker"),
459 }
460 }
461}
462
463impl PopUp {
464 pub fn get_available_targets(&self) -> Vec<Focus> {
465 match self {
466 PopUp::ViewCard => vec![
467 Focus::CardName,
468 Focus::CardDescription,
469 Focus::CardDueDate,
470 Focus::CardPriority,
471 Focus::CardStatus,
472 Focus::CardTags,
473 Focus::CardComments,
474 Focus::SubmitButton,
475 ],
476 PopUp::CommandPalette => vec![
477 Focus::CommandPaletteCommand,
478 Focus::CommandPaletteCard,
479 Focus::CommandPaletteBoard,
480 ],
481 PopUp::EditSpecificKeyBinding => vec![],
482 PopUp::ChangeView => vec![],
483 PopUp::CardStatusSelector => vec![],
484 PopUp::EditGeneralConfig => vec![],
485 PopUp::SelectDefaultView => vec![],
486 PopUp::ChangeDateFormatPopup => vec![],
487 PopUp::ChangeTheme => vec![],
488 PopUp::EditThemeStyle => vec![
489 Focus::StyleEditorFG,
490 Focus::StyleEditorBG,
491 Focus::StyleEditorModifier,
492 Focus::SubmitButton,
493 ],
494 PopUp::SaveThemePrompt => vec![Focus::SubmitButton, Focus::ExtraFocus],
495 PopUp::CustomHexColorPromptFG => vec![Focus::TextInput, Focus::SubmitButton],
496 PopUp::CustomHexColorPromptBG => vec![Focus::TextInput, Focus::SubmitButton],
497 PopUp::ConfirmDiscardCardChanges => vec![Focus::SubmitButton, Focus::ExtraFocus],
498 PopUp::CardPrioritySelector => vec![],
499 PopUp::FilterByTag => vec![Focus::FilterByTagPopup, Focus::SubmitButton],
500 PopUp::DateTimePicker => vec![
501 Focus::DTPCalender,
502 Focus::DTPMonth,
503 Focus::DTPYear,
504 Focus::DTPToggleTimePicker,
505 Focus::DTPHour,
506 Focus::DTPMinute,
507 Focus::DTPSecond,
508 ],
509 PopUp::TagPicker => vec![Focus::CardTags],
510 }
511 }
512
513 pub fn requires_previous_element_disabled(self) -> bool {
514 !(matches!(self, PopUp::TagPicker) || matches!(self, PopUp::DateTimePicker))
515 }
516
517 pub fn requires_previous_element_control(self) -> bool {
518 matches!(self, PopUp::TagPicker)
519 }
520
521 pub fn render(self, rect: &mut Frame, app: &mut App, is_active: bool) {
522 let skip_setting_focus = if let Some(popup) = app.state.z_stack.last() {
523 if popup.requires_previous_element_disabled() {
524 false
525 } else {
526 !popup.requires_previous_element_control()
527 }
528 } else {
529 true
530 };
531 if is_active && !skip_setting_focus {
532 let current_focus = app.state.focus;
533 if !self.get_available_targets().contains(¤t_focus)
534 && !self.get_available_targets().is_empty()
535 {
536 app.state.set_focus(self.get_available_targets()[0]);
537 }
538 }
539 match self {
540 PopUp::ViewCard => {
541 ViewCard::render(rect, app, is_active);
542 }
543 PopUp::CardStatusSelector => {
544 CardStatusSelector::render(rect, app, is_active);
545 }
546 PopUp::ChangeView => {
547 ChangeView::render(rect, app, is_active);
548 }
549 PopUp::CommandPalette => {
550 CommandPalette::render(rect, app, is_active);
551 }
552 PopUp::EditGeneralConfig => {
553 EditGeneralConfig::render(rect, app, is_active);
554 }
555 PopUp::EditSpecificKeyBinding => {
556 EditSpecificKeybinding::render(rect, app, is_active);
557 }
558 PopUp::SelectDefaultView => {
559 SelectDefaultView::render(rect, app, is_active);
560 }
561 PopUp::ChangeTheme => {
562 ChangeTheme::render(rect, app, is_active);
563 }
564 PopUp::EditThemeStyle => {
565 EditThemeStyle::render(rect, app, is_active);
566 }
567 PopUp::SaveThemePrompt => {
568 SaveThemePrompt::render(rect, app, is_active);
569 }
570 PopUp::CustomHexColorPromptFG | PopUp::CustomHexColorPromptBG => {
571 CustomHexColorPrompt::render(rect, app, is_active);
572 }
573 PopUp::ConfirmDiscardCardChanges => {
574 ConfirmDiscardCardChanges::render(rect, app, is_active);
575 }
576 PopUp::CardPrioritySelector => {
577 CardPrioritySelector::render(rect, app, is_active);
578 }
579 PopUp::FilterByTag => {
580 FilterByTag::render(rect, app, is_active);
581 }
582 PopUp::ChangeDateFormatPopup => {
583 ChangeDateFormat::render(rect, app, is_active);
584 }
585 PopUp::DateTimePicker => {
586 DateTimePicker::render(rect, app, is_active);
587 }
588 PopUp::TagPicker => {
589 TagPicker::render(rect, app, is_active);
590 }
591 }
592 }
593}