rust_kanban/ui/rendering/
utils.rs1use 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
16pub 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
134pub 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
162pub 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
268pub 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}