rust_kanban/ui/rendering/popup/
edit_theme_style.rs

1use crate::{
2    app::{
3        state::{Focus, KeyBindingEnum},
4        App,
5    },
6    constants::LIST_SELECTED_SYMBOL,
7    ui::{
8        rendering::{
9            common::{render_blank_styled_canvas, render_close_button},
10            popup::EditThemeStyle,
11            utils::{
12                centered_rect_with_percentage, check_if_active_and_get_style,
13                check_if_mouse_is_in_area, get_mouse_focusable_field_style,
14            },
15        },
16        theme::{Theme, ThemeEnum},
17        Renderable, TextColorOptions, TextModifierOptions,
18    },
19    util::parse_hex_to_rgb,
20};
21use log::debug;
22use ratatui::{
23    layout::{Alignment, Constraint, Direction, Layout},
24    style::{Color, Style},
25    text::{Line, Span},
26    widgets::{Block, BorderType, Borders, List, ListItem, Paragraph},
27    Frame,
28};
29use strum::IntoEnumIterator;
30
31impl Renderable for EditThemeStyle {
32    fn render(rect: &mut Frame, app: &mut App, is_active: bool) {
33        let popup_area = centered_rect_with_percentage(90, 80, rect.area());
34        let main_chunks = Layout::default()
35            .direction(Direction::Vertical)
36            .constraints(
37                [
38                    Constraint::Fill(1),
39                    Constraint::Length(4),
40                    Constraint::Length(3),
41                ]
42                .as_ref(),
43            )
44            .margin(1)
45            .split(popup_area);
46        let chunks = Layout::default()
47            .direction(Direction::Horizontal)
48            .constraints(
49                [
50                    Constraint::Fill(1),
51                    Constraint::Fill(1),
52                    Constraint::Fill(1),
53                ]
54                .as_ref(),
55            )
56            .split(main_chunks[0]);
57
58        fn set_foreground_color(app: &App, style: &mut Style) {
59            if let Some(fg_selected) = app.state.app_list_states.edit_specific_style[0].selected() {
60                if let Some(fg_color) = TextColorOptions::iter().nth(fg_selected).map(Color::from) {
61                    if let Color::Rgb(_, _, _) = fg_color {
62                        let user_input = app
63                            .state
64                            .text_buffers
65                            .theme_editor_fg_hex
66                            .get_joined_lines();
67                        let parsed_hex = parse_hex_to_rgb(&user_input);
68                        if let Some((r, g, b)) = parsed_hex {
69                            style.fg = Some(Color::Rgb(r, g, b));
70                            return;
71                        }
72                    }
73                    style.fg = Some(fg_color);
74                }
75            }
76        }
77
78        fn set_background_color(app: &mut App, style: &mut Style) {
79            if let Some(bg_selected) = app.state.app_list_states.edit_specific_style[1].selected() {
80                if let Some(bg_color) = TextColorOptions::iter().nth(bg_selected).map(Color::from) {
81                    if let Color::Rgb(_, _, _) = bg_color {
82                        let user_input = app
83                            .state
84                            .text_buffers
85                            .theme_editor_bg_hex
86                            .get_joined_lines();
87                        let parsed_hex = parse_hex_to_rgb(&user_input);
88                        if let Some((r, g, b)) = parsed_hex {
89                            style.bg = Some(Color::Rgb(r, g, b));
90                            return;
91                        }
92                    }
93                    style.bg = Some(bg_color);
94                }
95            }
96        }
97
98        fn set_text_modifier(app: &mut App, style: &mut Style) {
99            if let Some(modifier) = app.state.app_list_states.edit_specific_style[2].selected() {
100                if let Some(modifier) = TextModifierOptions::iter()
101                    .nth(modifier)
102                    .map(ratatui::style::Modifier::from)
103                {
104                    Theme::add_modifier_to_style(style, modifier);
105                }
106            }
107        }
108
109        fn create_list_item_from_color<'a>(
110            color: TextColorOptions,
111            style: Style,
112            app: &mut App,
113            is_active: bool,
114        ) -> ListItem<'a> {
115            let text_style = check_if_active_and_get_style(
116                is_active,
117                app.current_theme.inactive_text_style,
118                style,
119            );
120            let general_style = check_if_active_and_get_style(
121                is_active,
122                app.current_theme.inactive_text_style,
123                app.current_theme.general_style,
124            );
125            ListItem::new(vec![Line::from(vec![
126                Span::styled("Sample Text", text_style),
127                Span::styled(format!(" - {}", color), general_style),
128            ])])
129        }
130
131        fn handle_custom_hex_input<'a>(
132            hex_value: String,
133            mut style: Style,
134            app: &mut App,
135            is_active: bool,
136        ) -> Option<ListItem<'a>> {
137            if let Some((red_channel, green_channel, blue_channel)) = parse_hex_to_rgb(&hex_value) {
138                let color = TextColorOptions::HEX(red_channel, green_channel, blue_channel);
139                style.fg = Some(Color::from(color));
140                Some(create_list_item_from_color(color, style, app, is_active))
141            } else {
142                None
143            }
144        }
145
146        let general_style = check_if_active_and_get_style(
147            is_active,
148            app.current_theme.inactive_text_style,
149            app.current_theme.general_style,
150        );
151        let help_key_style = check_if_active_and_get_style(
152            is_active,
153            app.current_theme.inactive_text_style,
154            app.current_theme.help_key_style,
155        );
156        let help_text_style = check_if_active_and_get_style(
157            is_active,
158            app.current_theme.inactive_text_style,
159            app.current_theme.help_text_style,
160        );
161        // TODO: Generalize this;
162        // Exception to not using get_button_style as we have to manage other state
163        let fg_list_border_style = if !is_active {
164            app.current_theme.inactive_text_style
165        } else if check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, &chunks[0]) {
166            if app.state.app_list_states.edit_specific_style[0]
167                .selected()
168                .is_none()
169            {
170                app.state.app_list_states.edit_specific_style[0].select(Some(0));
171            }
172            app.state.mouse_focus = Some(Focus::StyleEditorFG);
173            app.state.set_focus(Focus::StyleEditorFG);
174            app.current_theme.mouse_focus_style
175        } else if app.state.focus == Focus::StyleEditorFG {
176            app.current_theme.keyboard_focus_style
177        } else {
178            app.current_theme.general_style
179        };
180        // Exception to not using get_button_style as we have to manage other state
181        let bg_list_border_style = if !is_active {
182            app.current_theme.inactive_text_style
183        } else if check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, &chunks[1]) {
184            if app.state.app_list_states.edit_specific_style[1]
185                .selected()
186                .is_none()
187            {
188                app.state.app_list_states.edit_specific_style[1].select(Some(0));
189            }
190            app.state.mouse_focus = Some(Focus::StyleEditorBG);
191            app.state.set_focus(Focus::StyleEditorBG);
192            app.current_theme.mouse_focus_style
193        } else if app.state.focus == Focus::StyleEditorBG {
194            app.current_theme.keyboard_focus_style
195        } else {
196            app.current_theme.general_style
197        };
198        // Exception to not using get_button_style as we have to manage other state
199        let modifiers_list_border_style = if !is_active {
200            app.current_theme.inactive_text_style
201        } else if check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, &chunks[2]) {
202            if app.state.app_list_states.edit_specific_style[2]
203                .selected()
204                .is_none()
205            {
206                app.state.app_list_states.edit_specific_style[2].select(Some(0));
207            }
208            app.state.mouse_focus = Some(Focus::StyleEditorModifier);
209            app.state.set_focus(Focus::StyleEditorModifier);
210            app.current_theme.mouse_focus_style
211        } else if app.state.focus == Focus::StyleEditorModifier {
212            app.current_theme.keyboard_focus_style
213        } else {
214            app.current_theme.general_style
215        };
216        let submit_button_style = get_mouse_focusable_field_style(
217            app,
218            Focus::SubmitButton,
219            &main_chunks[1],
220            is_active,
221            false,
222        );
223        let fg_list_items: Vec<ListItem> = TextColorOptions::iter()
224            .map(|color| {
225                let mut fg_style = Style::default();
226                let current_user_input = app
227                    .state
228                    .text_buffers
229                    .theme_editor_fg_hex
230                    .get_joined_lines();
231
232                set_background_color(app, &mut fg_style);
233                set_text_modifier(app, &mut fg_style);
234
235                if let TextColorOptions::HEX(_, _, _) = color {
236                    if current_user_input.is_empty() {
237                        fg_style.fg = Some(Color::Rgb(0, 0, 0));
238                        return create_list_item_from_color(
239                            TextColorOptions::HEX(0, 0, 0),
240                            fg_style,
241                            app,
242                            is_active,
243                        );
244                    } else if let Some(list_item) =
245                        handle_custom_hex_input(current_user_input, fg_style, app, is_active)
246                    {
247                        return list_item;
248                    }
249                }
250                fg_style.fg = Some(Color::from(color));
251                create_list_item_from_color(color, fg_style, app, is_active)
252            })
253            .collect();
254
255        let bg_list_items: Vec<ListItem> = TextColorOptions::iter()
256            .map(|color| {
257                let mut bg_style = Style::default();
258                let current_user_input = app
259                    .state
260                    .text_buffers
261                    .theme_editor_bg_hex
262                    .get_joined_lines();
263
264                set_foreground_color(app, &mut bg_style);
265                set_text_modifier(app, &mut bg_style);
266
267                if let TextColorOptions::HEX(_, _, _) = color {
268                    if current_user_input.is_empty() {
269                        bg_style.bg = Some(Color::Rgb(0, 0, 0));
270                        return create_list_item_from_color(
271                            TextColorOptions::HEX(0, 0, 0),
272                            bg_style,
273                            app,
274                            is_active,
275                        );
276                    } else if let Some(list_item) =
277                        handle_custom_hex_input(current_user_input, bg_style, app, is_active)
278                    {
279                        return list_item;
280                    }
281                }
282                bg_style.bg = Some(Color::from(color));
283                create_list_item_from_color(color, bg_style, app, is_active)
284            })
285            .collect();
286
287        let modifier_list_items: Vec<ListItem> = TextModifierOptions::iter()
288            .map(|modifier| {
289                let mut modifier_style = general_style;
290                if is_active {
291                    set_foreground_color(app, &mut modifier_style);
292                    set_background_color(app, &mut modifier_style);
293                }
294
295                Theme::add_modifier_to_style(
296                    &mut modifier_style,
297                    ratatui::style::Modifier::from(modifier.clone()),
298                );
299                ListItem::new(vec![Line::from(vec![
300                    Span::styled("Sample Text", modifier_style),
301                    Span::styled(format!(" - {}", modifier), general_style),
302                ])])
303            })
304            .collect();
305
306        let fg_list = if is_active {
307            List::new(fg_list_items)
308                .block(
309                    Block::default()
310                        .borders(Borders::ALL)
311                        .border_type(BorderType::Rounded)
312                        .title("Foreground")
313                        .border_style(fg_list_border_style),
314                )
315                .highlight_symbol(LIST_SELECTED_SYMBOL)
316        } else {
317            List::new(fg_list_items).block(
318                Block::default()
319                    .borders(Borders::ALL)
320                    .border_type(BorderType::Rounded)
321                    .title("Foreground")
322                    .border_style(fg_list_border_style),
323            )
324        };
325
326        let bg_list = if is_active {
327            List::new(bg_list_items)
328                .block(
329                    Block::default()
330                        .borders(Borders::ALL)
331                        .border_type(BorderType::Rounded)
332                        .title("Background")
333                        .border_style(bg_list_border_style),
334                )
335                .highlight_symbol(LIST_SELECTED_SYMBOL)
336        } else {
337            List::new(bg_list_items).block(
338                Block::default()
339                    .borders(Borders::ALL)
340                    .border_type(BorderType::Rounded)
341                    .title("Background")
342                    .border_style(bg_list_border_style),
343            )
344        };
345
346        let modifier_list = if is_active {
347            List::new(modifier_list_items)
348                .block(
349                    Block::default()
350                        .borders(Borders::ALL)
351                        .border_type(BorderType::Rounded)
352                        .title("Modifiers")
353                        .border_style(modifiers_list_border_style),
354                )
355                .highlight_symbol(LIST_SELECTED_SYMBOL)
356        } else {
357            List::new(modifier_list_items).block(
358                Block::default()
359                    .borders(Borders::ALL)
360                    .border_type(BorderType::Rounded)
361                    .title("Modifiers")
362                    .border_style(modifiers_list_border_style),
363            )
364        };
365
366        let theme_style_being_edited =
367            if let Some(index) = app.state.app_table_states.theme_editor.selected() {
368                if let Some(theme_enum) = ThemeEnum::iter().nth(index) {
369                    theme_enum.to_string()
370                } else {
371                    debug!("Index is out of bounds for theme_style_being_edited");
372                    "None".to_string()
373                }
374            } else {
375                "None".to_string()
376            };
377        let border_block = Block::default()
378            .title(format!("Editing Style: {}", theme_style_being_edited))
379            .borders(Borders::ALL)
380            .border_type(BorderType::Rounded)
381            .border_style(general_style);
382
383        let submit_button = Paragraph::new("Confirm Changes")
384            .style(submit_button_style)
385            .block(
386                Block::default()
387                    .borders(Borders::ALL)
388                    .border_type(BorderType::Rounded)
389                    .border_style(submit_button_style),
390            )
391            .alignment(Alignment::Center);
392
393        let up_key = app
394            .get_first_keybinding(KeyBindingEnum::Up)
395            .unwrap_or("".to_string());
396        let down_key = app
397            .get_first_keybinding(KeyBindingEnum::Down)
398            .unwrap_or("".to_string());
399        let next_focus_key = app
400            .get_first_keybinding(KeyBindingEnum::NextFocus)
401            .unwrap_or("".to_string());
402        let prv_focus_key = app
403            .get_first_keybinding(KeyBindingEnum::PrvFocus)
404            .unwrap_or("".to_string());
405        let accept_key = app
406            .get_first_keybinding(KeyBindingEnum::Accept)
407            .unwrap_or("".to_string());
408
409        let help_spans = vec![
410            Span::styled("Use ", help_text_style),
411            Span::styled(up_key, help_key_style),
412            Span::styled(" and ", help_text_style),
413            Span::styled(down_key, help_key_style),
414            Span::styled(" or scroll with the mouse", help_text_style),
415            Span::styled(" to select a Color/Modifier, Press ", help_text_style),
416            Span::styled(accept_key, help_key_style),
417            Span::styled(" or ", help_text_style),
418            Span::styled("<Mouse Left Click>", help_key_style),
419            Span::styled(" to edit (for custom RBG), Press ", help_text_style),
420            Span::styled(next_focus_key, help_key_style),
421            Span::styled(" or ", help_text_style),
422            Span::styled(prv_focus_key, help_key_style),
423            Span::styled(" to change focus.", help_text_style),
424        ];
425
426        let help_text = Paragraph::new(Line::from(help_spans))
427            .block(
428                Block::default()
429                    .borders(Borders::ALL)
430                    .border_type(BorderType::Rounded)
431                    .border_style(help_text_style),
432            )
433            .alignment(Alignment::Center)
434            .wrap(ratatui::widgets::Wrap { trim: true });
435
436        render_blank_styled_canvas(rect, &app.current_theme, popup_area, is_active);
437        rect.render_stateful_widget(
438            fg_list,
439            chunks[0],
440            &mut app.state.app_list_states.edit_specific_style[0],
441        );
442        rect.render_stateful_widget(
443            bg_list,
444            chunks[1],
445            &mut app.state.app_list_states.edit_specific_style[1],
446        );
447        rect.render_stateful_widget(
448            modifier_list,
449            chunks[2],
450            &mut app.state.app_list_states.edit_specific_style[2],
451        );
452        rect.render_widget(help_text, main_chunks[1]);
453        rect.render_widget(submit_button, main_chunks[2]);
454        rect.render_widget(border_block, popup_area);
455        if app.config.enable_mouse_support {
456            render_close_button(rect, app, is_active)
457        }
458    }
459}