rust_kanban/ui/rendering/popup/widgets/
date_time_picker.rs

1use crate::{
2    app::{state::Focus, App},
3    constants::MOUSE_OUT_OF_BOUNDS_COORDINATES,
4    ui::{
5        rendering::{
6            common::render_blank_styled_canvas,
7            popup::widgets::DateTimePicker,
8            utils::{check_if_active_and_get_style, check_if_mouse_is_in_area, get_button_style},
9        },
10        widgets::SelfViewportCorrection,
11        Renderable,
12    },
13};
14use ratatui::{
15    layout::{Alignment, Constraint, Direction, Layout, Rect},
16    widgets::{Block, BorderType, Borders, Paragraph},
17    Frame,
18};
19
20impl Renderable for DateTimePicker {
21    fn render(rect: &mut Frame, app: &mut App, is_active: bool) {
22        let anchor = app
23            .widgets
24            .date_time_picker
25            .viewport_corrected_anchor
26            .unwrap_or_default();
27        let (current_month, current_year) = app
28            .widgets
29            .date_time_picker
30            .calculate_styled_lines_of_dates(is_active, &app.current_theme);
31        let render_area = Rect {
32            x: anchor.0,
33            y: anchor.1,
34            width: app.widgets.date_time_picker.widget_width,
35            height: app.widgets.date_time_picker.widget_height,
36        };
37
38        app.widgets
39            .date_time_picker
40            .set_current_viewport(Some(rect.area()));
41
42        // 3 is for the " - ", additional 4 is to compensate for the borders that show when focus is on month or year
43        let title_length = (current_month.len() + 3 + current_year.len() + 4) as u16;
44        let padding = (render_area
45            .width
46            .min(app.widgets.date_time_picker.date_target_width)
47            - 3
48            - 2)
49        .saturating_sub(title_length); // 3 is for the Time section expand button, 2 is for margin
50        let month_length = current_month.len() as u16 + (padding / 2) + 2;
51        let year_length = current_year.len() as u16 + (padding / 2) + 2;
52
53        let (date_picker_render_area, time_picker_render_area) =
54            if app.widgets.date_time_picker.widget_width
55                > app.widgets.date_time_picker.date_target_width
56            {
57                let chunks = Layout::default()
58                    .direction(Direction::Horizontal)
59                    .constraints(
60                        [
61                            Constraint::Length(app.widgets.date_time_picker.date_target_width),
62                            Constraint::Fill(1),
63                        ]
64                        .as_ref(),
65                    )
66                    .split(render_area);
67                // add margin to the time picker
68                let time_picker_render_area = Layout::default()
69                    .direction(Direction::Vertical)
70                    .constraints([Constraint::Length(1), Constraint::Fill(1)].as_ref())
71                    .margin(1)
72                    .split(chunks[1]);
73                (chunks[0], Some(time_picker_render_area[1]))
74            } else {
75                (render_area, None)
76            };
77
78        let chunks = Layout::default()
79            .direction(Direction::Vertical)
80            .constraints(
81                [
82                    Constraint::Length(1),
83                    Constraint::Length(1),
84                    Constraint::Fill(1),
85                ]
86                .as_ref(),
87            )
88            .margin(1)
89            .split(date_picker_render_area);
90        let header_chunks = Layout::default()
91            .direction(Direction::Horizontal)
92            .constraints(
93                [
94                    Constraint::Length(month_length),
95                    Constraint::Length(1),
96                    Constraint::Length(year_length),
97                    Constraint::Fill(1),
98                    Constraint::Length(1),
99                ]
100                .as_ref(),
101            )
102            .split(chunks[0]);
103
104        let time_picker_toggle_style = get_button_style(
105            app,
106            Focus::DTPToggleTimePicker,
107            Some(&header_chunks[3]),
108            is_active,
109            false,
110        );
111        let month_style = get_button_style(
112            app,
113            Focus::DTPMonth,
114            Some(&header_chunks[0]),
115            is_active,
116            false,
117        );
118        let year_style = get_button_style(
119            app,
120            Focus::DTPYear,
121            Some(&header_chunks[2]),
122            is_active,
123            false,
124        );
125        let general_style = check_if_active_and_get_style(
126            is_active,
127            app.current_theme.inactive_text_style,
128            app.current_theme.general_style,
129        );
130
131        let month_block = if app.state.focus == Focus::DTPMonth {
132            Block::default()
133                .borders(Borders::LEFT | Borders::RIGHT)
134                .border_type(BorderType::Rounded)
135                .border_style(month_style)
136        } else {
137            Block::default()
138        };
139
140        let year_block = if app.state.focus == Focus::DTPYear {
141            Block::default()
142                .borders(Borders::LEFT | Borders::RIGHT)
143                .border_type(BorderType::Rounded)
144                .border_style(year_style)
145        } else {
146            Block::default()
147        };
148
149        let border_block = if app.widgets.date_time_picker.time_picker_active {
150            Block::default()
151                .borders(Borders::ALL)
152                .border_type(BorderType::Rounded)
153                .border_style(general_style)
154                .title("Date Time Picker")
155        } else {
156            Block::default()
157                .borders(Borders::ALL)
158                .border_type(BorderType::Rounded)
159                .border_style(general_style)
160                .title("Date Picker")
161        };
162
163        let time_picker_toggle_button = if app.widgets.date_time_picker.time_picker_active {
164            "<"
165        } else {
166            ">"
167        };
168
169        let main_paragraph =
170            Paragraph::new(app.widgets.date_time_picker.styled_date_lines.0.clone())
171                .block(Block::default())
172                .wrap(ratatui::widgets::Wrap { trim: true })
173                .alignment(Alignment::Center);
174        let month_paragraph = Paragraph::new(current_month)
175            .style(month_style)
176            .block(month_block)
177            .alignment(Alignment::Center);
178        let separator_paragraph = Paragraph::new("-")
179            .style(general_style)
180            .block(Block::default())
181            .alignment(Alignment::Center);
182        let year_paragraph = Paragraph::new(current_year)
183            .style(year_style)
184            .block(year_block)
185            .alignment(Alignment::Center);
186        let toggle_time_panel_paragraph = Paragraph::new(time_picker_toggle_button)
187            .style(time_picker_toggle_style)
188            .block(Block::default())
189            .alignment(Alignment::Right);
190
191        if !check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, &render_area)
192            && (app.state.current_mouse_coordinates != MOUSE_OUT_OF_BOUNDS_COORDINATES)
193        {
194            app.state.set_focus(Focus::NoFocus);
195        }
196
197        if check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, &chunks[2]) {
198            app.state.set_focus(Focus::DTPCalender);
199            let maybe_date_to_select = if let Some((calculated_pos, _, _)) =
200                &app.widgets.date_time_picker.calculated_mouse_coords
201            {
202                calculated_pos.iter().find_map(|(rect, date)| {
203                    if check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, rect) {
204                        Some(*date)
205                    } else {
206                        None
207                    }
208                })
209            } else {
210                None
211            };
212
213            if let Some(date_to_select) = maybe_date_to_select {
214                app.widgets
215                    .date_time_picker
216                    .select_date_in_current_month(date_to_select);
217            }
218        }
219
220        render_blank_styled_canvas(rect, &app.current_theme, render_area, is_active);
221        rect.render_widget(border_block, render_area);
222        rect.render_widget(month_paragraph, header_chunks[0]);
223        rect.render_widget(separator_paragraph, header_chunks[1]);
224        rect.render_widget(year_paragraph, header_chunks[2]);
225        rect.render_widget(toggle_time_panel_paragraph, header_chunks[3]);
226        rect.render_widget(main_paragraph, chunks[2]);
227
228        if app.widgets.date_time_picker.time_picker_active && time_picker_render_area.is_some() {
229            let render_area = time_picker_render_area.unwrap();
230            // only used for mouse detection, it looks like it would be incorrect but it is not
231            let time_picker_chunks = Layout::default()
232                .direction(Direction::Horizontal)
233                .constraints(
234                    [
235                        Constraint::Length(2),
236                        Constraint::Length(3),
237                        Constraint::Length(2),
238                    ]
239                    .as_ref(),
240                )
241                .margin(1)
242                .split(render_area);
243            if check_if_mouse_is_in_area(
244                &app.state.current_mouse_coordinates,
245                &time_picker_chunks[0],
246            ) {
247                app.state.set_focus(Focus::DTPHour);
248            }
249            if check_if_mouse_is_in_area(
250                &app.state.current_mouse_coordinates,
251                &time_picker_chunks[1],
252            ) {
253                app.state.set_focus(Focus::DTPMinute);
254            }
255            if check_if_mouse_is_in_area(
256                &app.state.current_mouse_coordinates,
257                &time_picker_chunks[2],
258            ) {
259                app.state.set_focus(Focus::DTPSecond);
260            }
261            let time_picker_lines = app.widgets.date_time_picker.get_styled_lines_of_time(
262                is_active,
263                &app.current_theme,
264                &app.state.focus,
265            );
266            let time_picker_paragraph = Paragraph::new(time_picker_lines)
267                .block(Block::default())
268                .wrap(ratatui::widgets::Wrap { trim: true });
269            rect.render_widget(time_picker_paragraph, render_area);
270        }
271    }
272}