rust_kanban/ui/rendering/view/
config_menu.rs

1use crate::{
2    app::{
3        state::{Focus, KeyBindingEnum},
4        App,
5    },
6    constants::{SCROLLBAR_BEGIN_SYMBOL, SCROLLBAR_END_SYMBOL, SCROLLBAR_TRACK_SYMBOL},
7    ui::{
8        rendering::{
9            common::{draw_title, render_close_button, render_logs},
10            utils::{
11                check_if_active_and_get_style, get_button_style, get_mouse_focusable_field_style,
12                get_scrollable_widget_row_bounds,
13            },
14            view::ConfigMenu,
15        },
16        Renderable,
17    },
18};
19use ratatui::{
20    layout::{Alignment, Constraint, Direction, Layout, Margin},
21    style::Style,
22    text::{Line, Span},
23    widgets::{
24        Block, BorderType, Borders, Cell, Paragraph, Row, Scrollbar, ScrollbarOrientation,
25        ScrollbarState, Table,
26    },
27    Frame,
28};
29
30impl Renderable for ConfigMenu {
31    fn render(rect: &mut Frame, app: &mut App, is_active: bool) {
32        let chunks = Layout::default()
33            .direction(Direction::Vertical)
34            .constraints(
35                [
36                    Constraint::Length(3),
37                    Constraint::Fill(1),
38                    Constraint::Length(3),
39                    Constraint::Length(5),
40                    Constraint::Length(5),
41                ]
42                .as_ref(),
43            )
44            .split(rect.area());
45
46        let reset_btn_chunks = Layout::default()
47            .direction(Direction::Horizontal)
48            .constraints([Constraint::Fill(1), Constraint::Fill(1)].as_ref())
49            .split(chunks[2]);
50
51        let reset_both_style = get_button_style(
52            app,
53            Focus::SubmitButton,
54            Some(&reset_btn_chunks[0]),
55            is_active,
56            true,
57        );
58        let reset_config_style = get_button_style(
59            app,
60            Focus::ExtraFocus,
61            Some(&reset_btn_chunks[1]),
62            is_active,
63            true,
64        );
65        let scrollbar_style = check_if_active_and_get_style(
66            is_active,
67            app.current_theme.inactive_text_style,
68            app.current_theme.progress_bar_style,
69        );
70        let config_text_style = check_if_active_and_get_style(
71            is_active,
72            app.current_theme.inactive_text_style,
73            app.current_theme.general_style,
74        );
75        let default_style =
76            get_mouse_focusable_field_style(app, Focus::ConfigTable, &chunks[1], is_active, false);
77
78        let config_table =
79            draw_config_table_selector(app, config_text_style, default_style, is_active);
80
81        let all_rows = app.config.to_view_list();
82        let total_rows = all_rows.len();
83        let current_index = app
84            .state
85            .app_table_states
86            .config
87            .selected()
88            .unwrap_or(0)
89            .min(total_rows - 1);
90
91        // mouse selection, TODO: make this a helper function
92        if is_active {
93            let available_height = (chunks[1].height - 2) as usize;
94            let (row_start_index, _) = get_scrollable_widget_row_bounds(
95                all_rows.len(),
96                current_index,
97                app.state.app_table_states.config.offset(),
98                available_height,
99            );
100            let current_mouse_y_position = app.state.current_mouse_coordinates.1;
101            let hovered_index = if current_mouse_y_position > chunks[1].y
102                && current_mouse_y_position < (chunks[1].y + chunks[1].height - 1)
103            {
104                Some(
105                    ((current_mouse_y_position - chunks[1].y - 1) + row_start_index as u16)
106                        as usize,
107                )
108            } else {
109                None
110            };
111            if hovered_index.is_some()
112                && (app.state.previous_mouse_coordinates != app.state.current_mouse_coordinates)
113            {
114                app.state.app_table_states.config.select(hovered_index);
115            }
116        }
117
118        let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
119            .begin_symbol(SCROLLBAR_BEGIN_SYMBOL)
120            .style(scrollbar_style)
121            .end_symbol(SCROLLBAR_END_SYMBOL)
122            .track_symbol(SCROLLBAR_TRACK_SYMBOL)
123            .track_style(app.current_theme.inactive_text_style);
124
125        let mut scrollbar_state = ScrollbarState::new(total_rows).position(current_index);
126        let scrollbar_area = chunks[1].inner(Margin {
127            horizontal: 0,
128            vertical: 1,
129        });
130
131        let reset_both_button = Paragraph::new("Reset Config and KeyBindings to Default")
132            .block(
133                Block::default()
134                    .title("Reset")
135                    .borders(Borders::ALL)
136                    .border_type(BorderType::Rounded),
137            )
138            .style(reset_both_style)
139            .alignment(Alignment::Center);
140
141        let reset_config_button = Paragraph::new("Reset Only Config to Default")
142            .block(
143                Block::default()
144                    .title("Reset")
145                    .borders(Borders::ALL)
146                    .border_type(BorderType::Rounded),
147            )
148            .style(reset_config_style)
149            .alignment(Alignment::Center);
150
151        let config_help = draw_config_help(app, is_active);
152
153        rect.render_widget(draw_title(app, chunks[0], is_active), chunks[0]);
154        rect.render_stateful_widget(
155            config_table,
156            chunks[1],
157            &mut app.state.app_table_states.config,
158        );
159        rect.render_stateful_widget(scrollbar, scrollbar_area, &mut scrollbar_state);
160        rect.render_widget(reset_both_button, reset_btn_chunks[0]);
161        rect.render_widget(reset_config_button, reset_btn_chunks[1]);
162        rect.render_widget(config_help, chunks[3]);
163        render_logs(app, true, chunks[4], rect, is_active);
164        if app.config.enable_mouse_support {
165            render_close_button(rect, app, is_active)
166        }
167    }
168}
169
170fn draw_config_table_selector(
171    app: &mut App,
172    config_text_style: Style,
173    default_style: Style,
174    is_active: bool,
175) -> Table<'static> {
176    let config_list = app.config.to_view_list();
177    let rows = config_list.iter().map(|item| {
178        let height = item
179            .iter()
180            .map(|content| content.chars().filter(|c| *c == '\n').count())
181            .max()
182            .unwrap_or(0)
183            + 1;
184        let cells = item.iter().map(|c| Cell::from(c.to_string()));
185        Row::new(cells).height(height as u16)
186    });
187
188    let highlight_style = check_if_active_and_get_style(
189        is_active,
190        app.current_theme.inactive_text_style,
191        app.current_theme.list_select_style,
192    );
193
194    Table::new(
195        rows,
196        [Constraint::Percentage(40), Constraint::Percentage(60)],
197    )
198    .block(
199        Block::default()
200            .title("Config Editor")
201            .borders(Borders::ALL)
202            .style(config_text_style)
203            .border_style(default_style)
204            .border_type(BorderType::Rounded),
205    )
206    .row_highlight_style(highlight_style)
207    .highlight_symbol(">> ")
208}
209
210fn draw_config_help<'a>(app: &mut App, is_active: bool) -> Paragraph<'a> {
211    let help_box_style = get_button_style(app, Focus::ConfigHelp, None, is_active, false);
212    let help_key_style = check_if_active_and_get_style(
213        is_active,
214        app.current_theme.inactive_text_style,
215        app.current_theme.help_key_style,
216    );
217    let help_text_style = check_if_active_and_get_style(
218        is_active,
219        app.current_theme.inactive_text_style,
220        app.current_theme.help_text_style,
221    );
222
223    let up_key = app
224        .get_first_keybinding(KeyBindingEnum::Up)
225        .unwrap_or("".to_string());
226    let down_key = app
227        .get_first_keybinding(KeyBindingEnum::Down)
228        .unwrap_or("".to_string());
229    let next_focus_key = app
230        .get_first_keybinding(KeyBindingEnum::NextFocus)
231        .unwrap_or("".to_string());
232    let prv_focus_key = app
233        .get_first_keybinding(KeyBindingEnum::PrvFocus)
234        .unwrap_or("".to_string());
235    let accept_key = app
236        .get_first_keybinding(KeyBindingEnum::Accept)
237        .unwrap_or("".to_string());
238    let cancel_key = app
239        .get_first_keybinding(KeyBindingEnum::GoToPreviousViewOrCancel)
240        .unwrap_or("".to_string());
241
242    let help_spans = Line::from(vec![
243        Span::styled("Use ", help_text_style),
244        Span::styled(up_key, help_key_style),
245        Span::styled(" and ", help_text_style),
246        Span::styled(down_key, help_key_style),
247        Span::styled(" or scroll with the mouse", help_text_style),
248        Span::styled(" to navigate. To edit a value press ", help_text_style),
249        Span::styled(accept_key.clone(), help_key_style),
250        Span::styled(" or ", help_text_style),
251        Span::styled("<Mouse Left Click>", help_key_style),
252        Span::styled(". Press ", help_text_style),
253        Span::styled(cancel_key, help_key_style),
254        Span::styled(
255            " to cancel. To Reset Keybindings or config to Default, press ",
256            help_text_style,
257        ),
258        Span::styled(next_focus_key, help_key_style),
259        Span::styled(" or ", help_text_style),
260        Span::styled(prv_focus_key, help_key_style),
261        Span::styled(
262            " to highlight respective Reset Button then press ",
263            help_text_style,
264        ),
265        Span::styled(accept_key, help_key_style),
266        Span::styled(" to reset", help_text_style),
267    ]);
268
269    Paragraph::new(help_spans)
270        .alignment(Alignment::Left)
271        .block(
272            Block::default()
273                .title("Help")
274                .borders(Borders::ALL)
275                .style(help_box_style)
276                .border_type(BorderType::Rounded),
277        )
278        .alignment(Alignment::Center)
279        .wrap(ratatui::widgets::Wrap { trim: true })
280}