rust_kanban/ui/rendering/
utils.rs

1use ratatui::{
2    layout::{Constraint, Direction, Layout, Rect},
3    style::Style,
4    widgets::ListState,
5};
6
7use crate::{
8    app::{
9        state::{AppStatus, Focus},
10        App,
11    },
12    ui::text_box::TextBox,
13    util::num_digits,
14};
15
16/// Checks for popup to return inactive style if not returns the style passed
17pub fn check_if_active_and_get_style(
18    is_active: bool,
19    inactive_style: Style,
20    style: Style,
21) -> Style {
22    if !is_active {
23        inactive_style
24    } else {
25        style
26    }
27}
28
29pub fn check_for_card_drag_and_get_style(
30    card_drag_mode: bool,
31    is_active: bool,
32    inactive_style: Style,
33    style: Style,
34) -> Style {
35    if card_drag_mode {
36        inactive_style
37    } else {
38        check_if_active_and_get_style(is_active, inactive_style, style)
39    }
40}
41
42pub fn check_if_mouse_is_in_area(mouse_coordinates: &(u16, u16), rect_to_check: &Rect) -> bool {
43    let (x, y) = mouse_coordinates;
44    let (x1, y1, x2, y2) = (
45        rect_to_check.x,
46        rect_to_check.y,
47        rect_to_check.x + rect_to_check.width,
48        rect_to_check.y + rect_to_check.height,
49    );
50    if x >= &x1 && x <= &x2 && y >= &y1 && y <= &y2 {
51        return true;
52    }
53    false
54}
55
56pub fn centered_rect_with_percentage(percent_width: u16, percent_height: u16, r: Rect) -> Rect {
57    let popup_layout = Layout::default()
58        .direction(Direction::Vertical)
59        .constraints(
60            [
61                Constraint::Percentage((100 - percent_height) / 2),
62                Constraint::Percentage(percent_height),
63                Constraint::Percentage((100 - percent_height) / 2),
64            ]
65            .as_ref(),
66        )
67        .split(r);
68
69    Layout::default()
70        .direction(Direction::Horizontal)
71        .constraints(
72            [
73                Constraint::Percentage((100 - percent_width) / 2),
74                Constraint::Percentage(percent_width),
75                Constraint::Percentage((100 - percent_width) / 2),
76            ]
77            .as_ref(),
78        )
79        .split(popup_layout[1])[1]
80}
81
82pub fn centered_rect_with_length(width: u16, height: u16, r: Rect) -> Rect {
83    let popup_layout = Layout::default()
84        .direction(Direction::Vertical)
85        .constraints(
86            [
87                Constraint::Length((r.height - height) / 2),
88                Constraint::Length(height),
89                Constraint::Length((r.height - height) / 2),
90            ]
91            .as_ref(),
92        )
93        .split(r);
94
95    Layout::default()
96        .direction(Direction::Horizontal)
97        .constraints(
98            [
99                Constraint::Length((r.width - width) / 2),
100                Constraint::Length(width),
101                Constraint::Length((r.width - width) / 2),
102            ]
103            .as_ref(),
104        )
105        .split(popup_layout[1])[1]
106}
107
108pub fn top_left_rect(width: u16, height: u16, r: Rect) -> Rect {
109    let popup_layout = Layout::default()
110        .direction(Direction::Vertical)
111        .constraints(
112            [
113                Constraint::Length(height),
114                Constraint::Length((r.height - height) / 2),
115                Constraint::Length((r.height - height) / 2),
116            ]
117            .as_ref(),
118        )
119        .split(r);
120
121    Layout::default()
122        .direction(Direction::Horizontal)
123        .constraints(
124            [
125                Constraint::Length(width),
126                Constraint::Length((r.width - width) / 2),
127                Constraint::Length((r.width - width) / 2),
128            ]
129            .as_ref(),
130        )
131        .split(popup_layout[0])[0]
132}
133
134/// Returns the style for the field based on the current focus and mouse position and sets the focus if the mouse is in the field area
135pub fn get_mouse_focusable_field_style(
136    app: &mut App,
137    focus: Focus,
138    chunk: &Rect,
139    is_active: bool,
140    auto_user_input_mode: bool,
141) -> Style {
142    if !is_active {
143        app.current_theme.inactive_text_style
144    } else if check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, chunk) {
145        if app.state.mouse_focus != Some(focus) {
146            app.state.app_status = AppStatus::Initialized;
147        } else if auto_user_input_mode {
148            app.state.app_status = AppStatus::UserInput;
149        } else {
150            app.state.app_status = AppStatus::Initialized;
151        }
152        app.state.mouse_focus = Some(focus);
153        app.state.set_focus(focus);
154        app.current_theme.mouse_focus_style
155    } else if app.state.focus == focus {
156        app.current_theme.keyboard_focus_style
157    } else {
158        app.current_theme.general_style
159    }
160}
161
162// TODO: maybe merge with get_mouse_focusable_field_style
163pub fn get_button_style(
164    app: &mut App,
165    focus: Focus,
166    chunk_for_mouse_check: Option<&Rect>,
167    is_active: bool,
168    default_to_error_style: bool,
169) -> Style {
170    if !is_active {
171        app.current_theme.inactive_text_style
172    } else if let Some(chunk) = chunk_for_mouse_check {
173        if check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, chunk) {
174            app.state.mouse_focus = Some(focus);
175            app.state.set_focus(focus);
176            app.current_theme.mouse_focus_style
177        } else if app.state.focus == focus {
178            app.current_theme.keyboard_focus_style
179        } else {
180            app.current_theme.general_style
181        }
182    } else if app.state.focus == focus {
183        if default_to_error_style {
184            app.current_theme.error_text_style
185        } else {
186            app.current_theme.keyboard_focus_style
187        }
188    } else {
189        app.current_theme.general_style
190    }
191}
192
193pub fn get_scrollable_widget_row_bounds(
194    all_rows_len: usize,
195    selected_index: usize,
196    offset: usize,
197    max_height: usize,
198) -> (usize, usize) {
199    let offset = offset.min(all_rows_len.saturating_sub(1));
200    let mut start = offset;
201    let mut end = offset;
202    let mut height = 0;
203    for _ in (0..all_rows_len)
204        .collect::<std::vec::Vec<usize>>()
205        .iter()
206        .skip(offset)
207    {
208        if height + 1 > max_height {
209            break;
210        }
211        height += 1;
212        end += 1;
213    }
214
215    while selected_index >= end {
216        height = height.saturating_add(1);
217        end += 1;
218        while height > max_height {
219            height = height.saturating_sub(1);
220            start += 1;
221        }
222    }
223    while selected_index < start {
224        start -= 1;
225        height = height.saturating_add(1);
226        while height > max_height {
227            end -= 1;
228            height = height.saturating_sub(1);
229        }
230    }
231    (start, end.saturating_sub(1))
232}
233
234pub fn calculate_viewport_corrected_cursor_position(
235    text_box: &TextBox,
236    show_line_numbers: &bool,
237    chunk: &Rect,
238) -> (u16, u16) {
239    let (y_pos, _) = text_box.cursor();
240    let x_pos = text_box.get_non_ascii_aware_cursor_x_pos();
241    let text_box_viewport = text_box.viewport.position();
242    let adjusted_x_cursor: u16 = if x_pos as u16 > text_box_viewport.3 {
243        x_pos as u16 - text_box_viewport.3
244    } else {
245        x_pos as u16
246    };
247    let x_pos = if *show_line_numbers && !text_box.single_line_mode {
248        let mut line_number_padding = 3;
249        let num_lines = text_box.get_num_lines();
250        let num_digits_in_max_line_number = num_digits(num_lines) as u16;
251        line_number_padding += num_digits_in_max_line_number;
252        chunk.left()
253            + 1
254            + adjusted_x_cursor.saturating_sub(text_box_viewport.1)
255            + line_number_padding
256    } else {
257        chunk.left() + 1 + adjusted_x_cursor.saturating_sub(text_box_viewport.1)
258    };
259    let adjusted_y_cursor = if y_pos as u16 > text_box_viewport.2 {
260        y_pos as u16 - text_box_viewport.2
261    } else {
262        y_pos as u16
263    };
264    let y_pos = chunk.top() + 1 + adjusted_y_cursor - text_box_viewport.0;
265    (x_pos, y_pos)
266}
267
268// TODO: maybe merge with get_mouse_focusable_field_style
269// TODO: see if the name can be shortened
270pub fn get_mouse_focusable_field_style_with_vertical_list_selection<T>(
271    app: &mut App<'_>,
272    main_menu_items: &[T],
273    render_area: Rect,
274    is_active: bool,
275) -> Style {
276    let mouse_coordinates = app.state.current_mouse_coordinates;
277
278    if !is_active {
279        app.current_theme.inactive_text_style
280    } else if check_if_mouse_is_in_area(&mouse_coordinates, &render_area) {
281        app.state.mouse_focus = Some(Focus::MainMenu);
282        app.state.set_focus(Focus::MainMenu);
283        calculate_mouse_list_select_index(
284            mouse_coordinates.1,
285            main_menu_items,
286            render_area,
287            &mut app.state.app_list_states.main_menu,
288        );
289        app.current_theme.mouse_focus_style
290    } else if matches!(app.state.focus, Focus::MainMenu) {
291        app.current_theme.keyboard_focus_style
292    } else {
293        app.current_theme.general_style
294    }
295}
296
297pub fn calculate_mouse_list_select_index<T>(
298    mouse_y: u16,
299    list_to_check_against: &[T],
300    render_area: Rect,
301    list_state: &mut ListState,
302) {
303    let top_of_list = render_area.top() + 1;
304    let mut bottom_of_list = top_of_list + list_to_check_against.len() as u16;
305    if bottom_of_list > render_area.bottom() {
306        bottom_of_list = render_area.bottom();
307    }
308    if mouse_y >= top_of_list && mouse_y <= bottom_of_list {
309        list_state.select(Some((mouse_y - top_of_list) as usize));
310    }
311}