rust_kanban/ui/
mod.rs

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(&current_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(&current_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}