rust_kanban/ui/rendering/popup/
view_card.rs

1use crate::{
2    app::{
3        kanban::{CardPriority, CardStatus},
4        state::{AppStatus, Focus},
5        App, DateTimeFormat,
6    },
7    constants::FIELD_NOT_SET,
8    ui::{
9        rendering::{
10            common::{render_blank_styled_canvas, render_close_button},
11            popup::ViewCard,
12            utils::{
13                calculate_viewport_corrected_cursor_position, centered_rect_with_percentage,
14                check_if_active_and_get_style, check_if_mouse_is_in_area, get_button_style,
15            },
16        },
17        widgets::SelfViewportCorrection,
18        PopUp, Renderable,
19    },
20    util::{date_format_converter, date_format_finder},
21};
22use chrono::{Local, NaiveDate, NaiveDateTime};
23use ratatui::{
24    layout::{Alignment, Constraint, Direction, Layout},
25    text::{Line, Span},
26    widgets::{Block, BorderType, Borders, List, ListItem, Paragraph},
27    Frame,
28};
29
30impl Renderable for ViewCard {
31    fn render(rect: &mut Frame, app: &mut App, is_active: bool) {
32        let popup_area = centered_rect_with_percentage(90, 90, rect.area());
33        // This is done early as board and card are not guaranteed to be selected
34        render_blank_styled_canvas(rect, &app.current_theme, popup_area, is_active);
35        let error_style = check_if_active_and_get_style(
36            is_active,
37            app.current_theme.inactive_text_style,
38            app.current_theme.error_text_style,
39        );
40        let general_style = check_if_active_and_get_style(
41            is_active,
42            app.current_theme.inactive_text_style,
43            app.current_theme.general_style,
44        );
45        let keyboard_focus_style = check_if_active_and_get_style(
46            is_active,
47            app.current_theme.inactive_text_style,
48            app.current_theme.keyboard_focus_style,
49        );
50        let list_select_style = check_if_active_and_get_style(
51            is_active,
52            app.current_theme.inactive_text_style,
53            app.current_theme.list_select_style,
54        );
55        let card_tags_style = get_button_style(app, Focus::CardTags, None, is_active, false);
56        let card_comments_style =
57            get_button_style(app, Focus::CardComments, None, is_active, false);
58        let save_changes_style = get_button_style(app, Focus::SubmitButton, None, is_active, false);
59        let name_style = get_button_style(app, Focus::CardName, None, is_active, false);
60        let description_style =
61            get_button_style(app, Focus::CardDescription, None, is_active, false);
62        let card_due_default_style =
63            get_button_style(app, Focus::CardDueDate, None, is_active, false);
64        if app.state.current_board_id.is_none() || app.state.current_card_id.is_none() {
65            let no_board_or_card_selected = Paragraph::new("No board or card selected.")
66                .block(
67                    Block::default()
68                        .title("Card Info")
69                        .borders(Borders::ALL)
70                        .border_type(BorderType::Rounded)
71                        .style(error_style),
72                )
73                .alignment(Alignment::Center);
74            rect.render_widget(no_board_or_card_selected, popup_area);
75            return;
76        }
77
78        let board = app
79            .boards
80            .get_board_with_id(app.state.current_board_id.unwrap());
81        if board.is_none() {
82            let could_not_find_board = Paragraph::new("Could not find board to view card.")
83                .block(
84                    Block::default()
85                        .title("Card Info")
86                        .borders(Borders::ALL)
87                        .border_type(BorderType::Rounded)
88                        .style(error_style),
89                )
90                .alignment(Alignment::Center)
91                .wrap(ratatui::widgets::Wrap { trim: true });
92            rect.render_widget(could_not_find_board, popup_area);
93            return;
94        }
95
96        let board = board.unwrap();
97        let card = board
98            .cards
99            .get_card_with_id(app.state.current_card_id.unwrap());
100        if card.is_none() {
101            let could_not_find_card = Paragraph::new("Could not find card to view.")
102                .block(
103                    Block::default()
104                        .title("Card Info")
105                        .borders(Borders::ALL)
106                        .border_type(BorderType::Rounded)
107                        .style(error_style),
108                )
109                .alignment(Alignment::Center)
110                .wrap(ratatui::widgets::Wrap { trim: true });
111            rect.render_widget(could_not_find_card, popup_area);
112            return;
113        }
114
115        let card_being_edited = app.state.get_card_being_edited();
116        let card = if let Some(card_being_edited) = card_being_edited {
117            card_being_edited.1
118        } else {
119            card.unwrap().to_owned()
120        };
121        if app.widgets.date_time_picker.selected_date_time.is_none()
122            && !card.due_date.is_empty()
123            && card.due_date != FIELD_NOT_SET
124        {
125            if let Ok(current_format) = date_format_finder(card.due_date.trim()) {
126                app.widgets.date_time_picker.selected_date_time =
127                    match NaiveDateTime::parse_from_str(
128                        card.due_date.trim(),
129                        current_format.to_parser_string(),
130                    ) {
131                        Ok(date_time) => Some(date_time),
132                        Err(_) => None,
133                    };
134            }
135        }
136        let board_name = board.name.clone();
137        let card_name = card.name.clone();
138
139        // Prepare Main Block Widget
140        let main_block_widget = {
141            Block::default()
142                .title(format!("{} >> Board({})", card_name, board_name))
143                .borders(Borders::ALL)
144                .border_type(BorderType::Rounded)
145                .border_style(general_style)
146        };
147
148        // Prepare Name Block Widget
149        let name_paragraph_block = Block::default()
150            .title("Name")
151            .borders(Borders::ALL)
152            .border_type(BorderType::Rounded)
153            .border_style(name_style);
154
155        app.state
156            .text_buffers
157            .card_name
158            .set_block(name_paragraph_block);
159
160        // Process Card Description
161        let description_length = app.state.text_buffers.card_description.get_num_lines();
162        let description_block = Block::default()
163            .title(format!("Description ({} line(s))", description_length))
164            .borders(Borders::ALL)
165            .border_type(BorderType::Rounded)
166            .border_style(description_style);
167
168        if app.config.show_line_numbers {
169            app.state
170                .text_buffers
171                .card_description
172                .set_line_number_style(general_style)
173        } else {
174            app.state.text_buffers.card_description.remove_line_number()
175        }
176        app.state
177            .text_buffers
178            .card_description
179            .set_block(description_block);
180
181        // Process Card Extra Info
182        let (card_extra_info_widget, card_extra_info_items_len, card_due_date_width) = {
183            let card_date_created = if date_format_finder(&card.date_created).is_ok() {
184                if let Ok(parsed_date) =
185                    date_format_converter(&card.date_created, app.config.date_time_format)
186                {
187                    Span::styled(format!("Created: {}", parsed_date), general_style)
188                } else {
189                    match NaiveDateTime::parse_from_str(
190                        &card.date_created,
191                        app.config.date_time_format.to_parser_string(),
192                    ) {
193                        Ok(parsed_date) => Span::styled(
194                            format!(
195                                "Created: {}",
196                                parsed_date.format(app.config.date_time_format.to_parser_string())
197                            ),
198                            general_style,
199                        ),
200                        Err(_) => {
201                            Span::styled(format!("Created: {}", card.date_created), general_style)
202                        }
203                    }
204                }
205            } else {
206                Span::styled(format!("Created: {}", card.date_created), general_style)
207            };
208            let card_date_modified = if date_format_finder(&card.date_modified).is_ok() {
209                if let Ok(parsed_date) =
210                    date_format_converter(&card.date_modified, app.config.date_time_format)
211                {
212                    Span::styled(format!("Modified: {}", parsed_date), general_style)
213                } else {
214                    Span::styled(format!("Modified: {}", card.date_modified), general_style)
215                }
216            } else {
217                match NaiveDateTime::parse_from_str(
218                    &card.date_modified,
219                    app.config.date_time_format.to_parser_string(),
220                ) {
221                    Ok(parsed_date) => Span::styled(
222                        format!(
223                            "Modified: {}",
224                            parsed_date.format(app.config.date_time_format.to_parser_string())
225                        ),
226                        general_style,
227                    ),
228                    Err(_) => {
229                        Span::styled(format!("Modified: {}", card.date_modified), general_style)
230                    }
231                }
232            };
233            let card_date_completed = if date_format_finder(&card.date_completed).is_ok() {
234                if let Ok(parsed_date) =
235                    date_format_converter(&card.date_completed, app.config.date_time_format)
236                {
237                    Span::styled(format!("Completed: {}", parsed_date), general_style)
238                } else {
239                    Span::styled(format!("Completed: {}", card.date_completed), general_style)
240                }
241            } else {
242                match NaiveDateTime::parse_from_str(
243                    &card.date_completed,
244                    app.config.date_time_format.to_parser_string(),
245                ) {
246                    Ok(parsed_date) => Span::styled(
247                        format!(
248                            "Completed: {}",
249                            parsed_date.format(app.config.date_time_format.to_parser_string())
250                        ),
251                        general_style,
252                    ),
253                    Err(_) => {
254                        Span::styled(format!("Completed: {}", card.date_completed), general_style)
255                    }
256                }
257            };
258            let card_priority = format!("Priority: {}", card.priority);
259            let card_status = format!("Status: {}", card.card_status);
260            let parsed_due_date = if date_format_finder(&card.due_date).is_ok() {
261                date_format_converter(&card.due_date, app.config.date_time_format)
262            } else {
263                Ok(FIELD_NOT_SET.to_string())
264            };
265            let card_due_date_styled = if let Ok(parsed_due_date) = parsed_due_date {
266                if app.state.focus == Focus::CardDueDate {
267                    Span::styled(format!("Due: {}", parsed_due_date), list_select_style)
268                } else if parsed_due_date == FIELD_NOT_SET || parsed_due_date.is_empty() {
269                    Span::styled(format!("Due: {}", parsed_due_date), card_due_default_style)
270                } else {
271                    let formatted_date_format = date_format_finder(&parsed_due_date).unwrap();
272                    let days_left = match formatted_date_format {
273                        DateTimeFormat::DayMonthYear
274                        | DateTimeFormat::MonthDayYear
275                        | DateTimeFormat::YearMonthDay => {
276                            let today = Local::now().date_naive();
277                            let string_to_naive_date_format = NaiveDate::parse_from_str(
278                                &parsed_due_date,
279                                app.config.date_time_format.to_parser_string(),
280                            )
281                            .unwrap();
282                            string_to_naive_date_format
283                                .signed_duration_since(today)
284                                .num_days()
285                        }
286                        DateTimeFormat::DayMonthYearTime
287                        | DateTimeFormat::MonthDayYearTime
288                        | DateTimeFormat::YearMonthDayTime {} => {
289                            let today = Local::now().naive_local();
290                            let string_to_naive_date_format = NaiveDateTime::parse_from_str(
291                                &parsed_due_date,
292                                app.config.date_time_format.to_parser_string(),
293                            )
294                            .unwrap();
295                            string_to_naive_date_format
296                                .signed_duration_since(today)
297                                .num_days()
298                        }
299                    };
300                    if !is_active {
301                        Span::styled(
302                            format!("Due: {}", parsed_due_date),
303                            app.current_theme.inactive_text_style,
304                        )
305                    } else if days_left <= app.config.warning_delta.into() && days_left >= 0 {
306                        Span::styled(
307                            format!("Due: {}", parsed_due_date),
308                            app.current_theme.card_due_warning_style,
309                        )
310                    } else if days_left < 0 {
311                        Span::styled(
312                            format!("Due: {}", parsed_due_date),
313                            app.current_theme.card_due_overdue_style,
314                        )
315                    } else {
316                        Span::styled(format!("Due: {}", parsed_due_date), card_due_default_style)
317                    }
318                }
319            } else if app.state.focus == Focus::CardDueDate {
320                Span::styled(format!("Due: {}", FIELD_NOT_SET), list_select_style)
321            } else {
322                Span::styled(format!("Due: {}", FIELD_NOT_SET), general_style)
323            };
324            let card_priority_styled = if !is_active {
325                Span::styled(card_priority, app.current_theme.inactive_text_style)
326            } else if app.state.focus == Focus::CardPriority {
327                Span::styled(card_priority, app.current_theme.list_select_style)
328            } else if card.priority == CardPriority::High {
329                Span::styled(card_priority, app.current_theme.card_priority_high_style)
330            } else if card.priority == CardPriority::Medium {
331                Span::styled(card_priority, app.current_theme.card_priority_medium_style)
332            } else if card.priority == CardPriority::Low {
333                Span::styled(card_priority, app.current_theme.card_priority_low_style)
334            } else {
335                Span::styled(card_priority, app.current_theme.general_style)
336            };
337            let card_status_styled = if !is_active {
338                Span::styled(card_status, app.current_theme.inactive_text_style)
339            } else if app.state.focus == Focus::CardStatus {
340                Span::styled(card_status, app.current_theme.list_select_style)
341            } else if card.card_status == CardStatus::Complete {
342                Span::styled(card_status, app.current_theme.card_status_completed_style)
343            } else if card.card_status == CardStatus::Active {
344                Span::styled(card_status, app.current_theme.card_status_active_style)
345            } else if card.card_status == CardStatus::Stale {
346                Span::styled(card_status, app.current_theme.card_status_stale_style)
347            } else {
348                Span::styled(card_status, app.current_theme.general_style)
349            };
350            let card_extra_info_items = vec![
351                ListItem::new(vec![Line::from(card_date_created)]),
352                ListItem::new(vec![Line::from(card_date_modified)]),
353                ListItem::new(vec![Line::from(card_due_date_styled.clone())]),
354                ListItem::new(vec![Line::from(card_date_completed)]),
355                ListItem::new(vec![Line::from(card_priority_styled)]),
356                ListItem::new(vec![Line::from(card_status_styled)]),
357            ];
358            let card_extra_info_items_len = card_extra_info_items.len();
359            let card_extra_info = List::new(card_extra_info_items).block(
360                Block::default()
361                    .title("Card Info")
362                    .borders(Borders::ALL)
363                    .border_type(BorderType::Rounded)
364                    .border_style(general_style),
365            );
366            (
367                card_extra_info,
368                card_extra_info_items_len,
369                card_due_date_styled.width(),
370            )
371        };
372
373        // TODO: Refactor card tag and comments processing
374        // Process Card Tags
375        let card_tag_lines = {
376            let card_tags = if app.state.focus == Focus::CardTags {
377                let mut tags = vec![];
378                if app
379                    .state
380                    .app_list_states
381                    .card_view_tag_list
382                    .selected()
383                    .is_none()
384                {
385                    for (index, tag) in card.tags.iter().enumerate() {
386                        tags.push(Span::styled(
387                            format!("{}) {} ", index + 1, tag),
388                            general_style,
389                        ));
390                    }
391                } else {
392                    let selected_tag = app
393                        .state
394                        .app_list_states
395                        .card_view_tag_list
396                        .selected()
397                        .unwrap();
398                    for (index, tag) in card.tags.iter().enumerate() {
399                        if index == selected_tag {
400                            tags.push(Span::styled(
401                                format!("{}) {} ", index + 1, tag),
402                                keyboard_focus_style,
403                            ));
404                        } else {
405                            tags.push(Span::styled(
406                                format!("{}) {} ", index + 1, tag),
407                                general_style,
408                            ));
409                        }
410                    }
411                }
412                tags
413            } else {
414                let mut tags = vec![];
415                for (index, tag) in card.tags.iter().enumerate() {
416                    tags.push(Span::styled(
417                        format!("{}) {} ", index + 1, tag),
418                        general_style,
419                    ));
420                }
421                tags
422            };
423            let mut card_tag_lines = vec![];
424            let mut card_tags_per_line = vec![];
425            let mut collector = String::new();
426            let mut collector_start = 0;
427            let mut collector_end = 0;
428            for (i, tag) in card.tags.iter().enumerate() {
429                let tag_string = format!("{}) {} ", i + 1, tag);
430                if (collector.len() + tag_string.len()) < (popup_area.width - 2) as usize {
431                    collector.push_str(&tag_string);
432                    collector_end = i + 1;
433                } else {
434                    card_tag_lines.push(Line::from(
435                        card_tags[collector_start..collector_end].to_vec(),
436                    ));
437                    card_tags_per_line.push(collector_end - collector_start);
438                    collector = String::new();
439                    collector.push_str(&tag_string);
440                    collector_start = i;
441                    collector_end = i + 1;
442                }
443            }
444            if !collector.is_empty() {
445                card_tag_lines.push(Line::from(
446                    card_tags[collector_start..collector_end].to_vec(),
447                ));
448            }
449            card_tag_lines
450        };
451
452        // Process Card Comments
453        let card_comment_lines = {
454            let card_comments = if app.state.focus == Focus::CardComments {
455                let mut comments = vec![];
456                if app
457                    .state
458                    .app_list_states
459                    .card_view_comment_list
460                    .selected()
461                    .is_none()
462                {
463                    for (index, comment) in card.comments.iter().enumerate() {
464                        comments.push(Span::styled(
465                            format!("{}) {} ", index + 1, comment),
466                            general_style,
467                        ));
468                    }
469                } else {
470                    let selected_comment = app
471                        .state
472                        .app_list_states
473                        .card_view_comment_list
474                        .selected()
475                        .unwrap();
476                    for (index, comment) in card.comments.iter().enumerate() {
477                        if index == selected_comment {
478                            comments.push(Span::styled(
479                                format!("{}) {} ", index + 1, comment),
480                                keyboard_focus_style,
481                            ));
482                        } else {
483                            comments.push(Span::styled(
484                                format!("{}) {} ", index + 1, comment),
485                                general_style,
486                            ));
487                        }
488                    }
489                }
490                comments
491            } else {
492                let mut comments = vec![];
493                for (index, comment) in card.comments.iter().enumerate() {
494                    comments.push(Span::styled(
495                        format!("{}) {} ", index + 1, comment),
496                        general_style,
497                    ));
498                }
499                comments
500            };
501            let mut card_comment_lines = vec![];
502            let mut collector = String::new();
503            let mut collector_start = 0;
504            let mut collector_end = 0;
505            for (i, comment) in card.comments.iter().enumerate() {
506                let comment_string = format!("{}) {} ", i + 1, comment);
507                if (collector.len() + comment_string.len()) < (popup_area.width - 2) as usize {
508                    collector.push_str(&comment_string);
509                    collector_end = i + 1;
510                } else {
511                    card_comment_lines.push(Line::from(
512                        card_comments[collector_start..collector_end].to_vec(),
513                    ));
514                    collector = String::new();
515                    collector.push_str(&comment_string);
516                    collector_start = i;
517                    collector_end = i + 1;
518                }
519            }
520            if !collector.is_empty() {
521                card_comment_lines.push(Line::from(
522                    card_comments[collector_start..collector_end].to_vec(),
523                ));
524            }
525            card_comment_lines
526        };
527
528        // Determine chunk sizes
529        let card_chunks = {
530            let min_box_height: u16 = 2;
531            let border_height: u16 = 2;
532            let max_height: u16 = popup_area.height - border_height;
533            let submit_button_height: u16 = 3;
534            let card_name_box_height: u16 = 3;
535            let card_extra_info_height: u16 = 8;
536            let mut available_height: u16 = if app.state.card_being_edited.is_some() {
537                max_height - card_name_box_height - card_extra_info_height - submit_button_height
538            } else {
539                max_height - card_name_box_height - card_extra_info_height
540            };
541
542            let raw_card_description_height =
543                app.state.text_buffers.card_description.get_num_lines() as u16;
544
545            let raw_tags_height = card_tag_lines.len() as u16;
546            let raw_comments_height = card_comment_lines.len() as u16;
547
548            let mut card_description_height = if app.state.focus == Focus::CardDescription {
549                if available_height
550                    .saturating_sub(raw_tags_height + border_height)
551                    .saturating_sub(raw_comments_height + border_height)
552                    > 0
553                {
554                    let calc = available_height
555                        - raw_tags_height
556                        - raw_comments_height
557                        - (border_height * 2);
558                    if calc < (raw_card_description_height + border_height) {
559                        let diff = (raw_card_description_height + border_height) - calc;
560                        if diff < min_box_height {
561                            raw_card_description_height + border_height
562                        } else {
563                            calc
564                        }
565                    } else {
566                        calc
567                    }
568                } else if (raw_card_description_height + border_height) <= available_height {
569                    raw_card_description_height + border_height
570                } else {
571                    available_height
572                }
573            } else if ((raw_card_description_height + border_height) <= available_height)
574                && app.state.focus != Focus::CardTags
575                && app.state.focus != Focus::CardComments
576            {
577                raw_card_description_height.saturating_sub(border_height)
578            } else {
579                min_box_height
580            };
581
582            available_height = available_height.saturating_sub(card_description_height);
583
584            let card_tags_height = if available_height > 0 {
585                if app.state.focus == Focus::CardTags {
586                    raw_tags_height + border_height
587                } else {
588                    min_box_height
589                }
590            } else {
591                min_box_height
592            };
593
594            available_height = available_height.saturating_sub(card_tags_height);
595
596            let card_comments_height = if available_height > 0 {
597                if app.state.focus == Focus::CardComments {
598                    raw_comments_height + border_height
599                } else {
600                    min_box_height
601                }
602            } else {
603                min_box_height
604            };
605
606            available_height = available_height.saturating_sub(card_comments_height);
607
608            if available_height > 0 {
609                card_description_height += available_height;
610            }
611
612            if app.state.card_being_edited.is_some() {
613                Layout::default()
614                    .direction(Direction::Vertical)
615                    .constraints([
616                        Constraint::Length(card_name_box_height),
617                        Constraint::Length(card_description_height),
618                        Constraint::Length(card_extra_info_height),
619                        Constraint::Length(card_tags_height),
620                        Constraint::Length(card_comments_height),
621                        Constraint::Length(submit_button_height),
622                    ])
623                    .margin(1)
624                    .split(popup_area)
625            } else {
626                Layout::default()
627                    .direction(Direction::Vertical)
628                    .constraints([
629                        Constraint::Length(card_name_box_height),
630                        Constraint::Length(card_description_height),
631                        Constraint::Length(card_extra_info_height),
632                        Constraint::Length(card_tags_height),
633                        Constraint::Length(card_comments_height),
634                    ])
635                    .margin(1)
636                    .split(popup_area)
637            }
638        };
639
640        if app.state.z_stack.last() == Some(&PopUp::DateTimePicker) {
641            if app.widgets.date_time_picker.get_anchor().is_none() {
642                app.widgets.date_time_picker.set_anchor(Some((
643                    card_chunks[2].x + card_due_date_width as u16 + 2,
644                    card_chunks[2].y + 3,
645                ))); // offsets to make sure date is visible
646            }
647            app.widgets.date_time_picker.current_viewport = Some(rect.area());
648        }
649
650        if is_active
651            && check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, &card_chunks[2])
652        {
653            let top_of_list = card_chunks[2].y + 1;
654            let mut bottom_of_list = card_chunks[2].y + card_extra_info_items_len as u16;
655            if bottom_of_list > card_chunks[2].bottom() {
656                bottom_of_list = card_chunks[2].bottom();
657            }
658            let mouse_y = app.state.current_mouse_coordinates.1;
659            if mouse_y >= top_of_list && mouse_y <= bottom_of_list {
660                match mouse_y - top_of_list {
661                    2 => {
662                        app.state.set_focus(Focus::CardDueDate);
663                        app.state.mouse_focus = Some(Focus::CardDueDate);
664                        app.state
665                            .app_list_states
666                            .card_view_comment_list
667                            .select(None);
668                        app.state.app_list_states.card_view_tag_list.select(None);
669                    }
670                    4 => {
671                        app.state.set_focus(Focus::CardPriority);
672                        app.state.mouse_focus = Some(Focus::CardPriority);
673                        app.state
674                            .app_list_states
675                            .card_view_comment_list
676                            .select(None);
677                        app.state.app_list_states.card_view_tag_list.select(None);
678                    }
679                    5 => {
680                        app.state.set_focus(Focus::CardStatus);
681                        app.state.mouse_focus = Some(Focus::CardStatus);
682                        app.state
683                            .app_list_states
684                            .card_view_comment_list
685                            .select(None);
686                        app.state.app_list_states.card_view_tag_list.select(None);
687                    }
688                    _ => {
689                        app.state.set_focus(Focus::NoFocus);
690                        app.state.mouse_focus = None;
691                    }
692                }
693                app.state
694                    .app_list_states
695                    .card_view_list
696                    .select(Some((mouse_y - top_of_list) as usize));
697            } else {
698                app.state.app_list_states.card_view_list.select(None);
699            }
700        };
701        if is_active
702            && check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, &card_chunks[0])
703        {
704            app.state.set_focus(Focus::CardName);
705            app.state.mouse_focus = Some(Focus::CardName);
706            app.state
707                .app_list_states
708                .card_view_comment_list
709                .select(None);
710            app.state.app_list_states.card_view_tag_list.select(None);
711        }
712        if is_active
713            && check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, &card_chunks[1])
714        {
715            app.state.set_focus(Focus::CardDescription);
716            app.state.mouse_focus = Some(Focus::CardDescription);
717            app.state
718                .app_list_states
719                .card_view_comment_list
720                .select(None);
721            app.state.app_list_states.card_view_tag_list.select(None);
722        }
723
724        let card_tags_widget = Paragraph::new(card_tag_lines.clone())
725            .block(
726                Block::default()
727                    .title(format!("Tags ({})", card.tags.len()))
728                    .border_type(BorderType::Rounded)
729                    .borders(Borders::ALL)
730                    .border_style(card_tags_style),
731            )
732            .alignment(Alignment::Left);
733
734        let card_comments_widget = Paragraph::new(card_comment_lines.clone())
735            .block(
736                Block::default()
737                    .title(format!("Comments ({})", card.comments.len()))
738                    .border_type(BorderType::Rounded)
739                    .borders(Borders::ALL)
740                    .border_style(card_comments_style),
741            )
742            .alignment(Alignment::Left);
743
744        if is_active
745            && check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, &card_chunks[3])
746        {
747            app.state.set_focus(Focus::CardTags);
748            app.state.mouse_focus = Some(Focus::CardTags);
749            app.state
750                .app_list_states
751                .card_view_comment_list
752                .select(None);
753        }
754
755        if is_active
756            && check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, &card_chunks[4])
757        {
758            app.state.set_focus(Focus::CardComments);
759            app.state.mouse_focus = Some(Focus::CardComments);
760            app.state.app_list_states.card_view_tag_list.select(None);
761        }
762
763        if app.state.app_status == AppStatus::UserInput {
764            match app.state.focus {
765                Focus::CardName => {
766                    let (x_pos, y_pos) = calculate_viewport_corrected_cursor_position(
767                        &app.state.text_buffers.card_name,
768                        &app.config.show_line_numbers,
769                        &card_chunks[0],
770                    );
771                    rect.set_cursor_position((x_pos, y_pos));
772                }
773                Focus::CardDescription => {
774                    let (x_pos, y_pos) = calculate_viewport_corrected_cursor_position(
775                        &app.state.text_buffers.card_description,
776                        &app.config.show_line_numbers,
777                        &card_chunks[1],
778                    );
779                    rect.set_cursor_position((x_pos, y_pos));
780                }
781                Focus::CardTags => {
782                    if app
783                        .state
784                        .app_list_states
785                        .card_view_tag_list
786                        .selected()
787                        .is_some()
788                        && !app.state.text_buffers.card_tags.is_empty()
789                    {
790                        let selected_index = app
791                            .state
792                            .app_list_states
793                            .card_view_tag_list
794                            .selected()
795                            .unwrap();
796                        let mut counter = 0;
797                        let mut y_index = 0;
798                        let mut length_before_selected_tag = 0;
799                        let mut prv_spans_length = 0;
800                        let tag_offset = 3;
801                        for line in card_tag_lines.iter() {
802                            for _ in line.spans.iter() {
803                                if counter == selected_index {
804                                    break;
805                                } else {
806                                    let element = line.spans.get(counter - prv_spans_length);
807                                    if let Some(element) = element {
808                                        length_before_selected_tag += element.content.len();
809                                    }
810                                    counter += 1;
811                                }
812                            }
813                            if counter == selected_index {
814                                break;
815                            }
816                            y_index += 1;
817                            prv_spans_length += line.spans.iter().len();
818                            length_before_selected_tag = 0;
819                        }
820                        let digits_in_counter = (counter + 1).to_string().len();
821                        if let Some(text_box) = app.state.text_buffers.card_tags.get(selected_index)
822                        {
823                            let text_box_cursor = text_box.cursor();
824                            let x_pos = card_chunks[3].left()
825                                + length_before_selected_tag as u16
826                                + text_box_cursor.1 as u16
827                                + tag_offset
828                                + digits_in_counter as u16;
829                            let y_pos = card_chunks[3].top() + y_index as u16 + 1;
830
831                            if app.state.focus == Focus::CardTags {
832                                app.widgets.tag_picker.set_anchor(Some((
833                                    x_pos - (text_box_cursor.1 as u16)
834                                        + (text_box.get_joined_lines().len() as u16),
835                                    y_pos,
836                                )));
837                            }
838
839                            // TODO: Card tags and comments cursor is incorrect as the view does not change when the comment or tag is longer than the screen
840                            rect.set_cursor_position((x_pos, y_pos));
841                        }
842                    }
843                }
844                Focus::CardComments => {
845                    if app
846                        .state
847                        .app_list_states
848                        .card_view_comment_list
849                        .selected()
850                        .is_some()
851                        && !app.state.text_buffers.card_comments.is_empty()
852                    {
853                        let selected_index = app
854                            .state
855                            .app_list_states
856                            .card_view_comment_list
857                            .selected()
858                            .unwrap();
859                        let mut counter = 0;
860                        let mut y_index = 0;
861                        let mut length_before_selected_comment = 0;
862                        let mut prv_spans_length = 0;
863                        let comment_offset = 3;
864                        for line in card_comment_lines.iter() {
865                            for _ in line.spans.iter() {
866                                if counter == selected_index {
867                                    break;
868                                } else {
869                                    let element = line.spans.get(counter - prv_spans_length);
870                                    if let Some(element) = element {
871                                        length_before_selected_comment += element.content.len();
872                                    }
873                                    counter += 1;
874                                }
875                            }
876                            if counter == selected_index {
877                                break;
878                            }
879                            y_index += 1;
880                            prv_spans_length += line.spans.iter().len();
881                            length_before_selected_comment = 0;
882                        }
883                        let digits_in_counter = (counter + 1).to_string().len();
884                        if let Some(text_box) =
885                            app.state.text_buffers.card_comments.get(selected_index)
886                        {
887                            let text_box_cursor = text_box.cursor();
888                            let x_pos = card_chunks[4].left()
889                                + length_before_selected_comment as u16
890                                + text_box_cursor.1 as u16
891                                + comment_offset
892                                + digits_in_counter as u16;
893                            let y_pos = card_chunks[4].top() + y_index as u16 + 1;
894                            rect.set_cursor_position((x_pos, y_pos));
895                        }
896                    }
897                }
898                _ => {}
899            }
900        }
901
902        // Render everything
903        rect.render_widget(main_block_widget, popup_area);
904        rect.render_widget(app.state.text_buffers.card_name.widget(), card_chunks[0]);
905        rect.render_widget(
906            app.state.text_buffers.card_description.widget(),
907            card_chunks[1],
908        );
909        rect.render_widget(card_extra_info_widget, card_chunks[2]);
910        rect.render_widget(card_tags_widget, card_chunks[3]);
911        rect.render_widget(card_comments_widget, card_chunks[4]);
912
913        // Render Submit button if card is being edited
914        if app.state.card_being_edited.is_some() {
915            if is_active
916                && check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, &card_chunks[5])
917            {
918                app.state.set_focus(Focus::SubmitButton);
919                app.state.mouse_focus = Some(Focus::SubmitButton);
920                app.state
921                    .app_list_states
922                    .card_view_comment_list
923                    .select(None);
924                app.state.app_list_states.card_view_tag_list.select(None);
925            }
926            let save_changes_button = Paragraph::new("Save Changes")
927                .block(
928                    Block::default()
929                        .title("Save Changes")
930                        .borders(Borders::ALL)
931                        .border_type(BorderType::Rounded)
932                        .border_style(save_changes_style),
933                )
934                .alignment(Alignment::Center);
935            rect.render_widget(save_changes_button, card_chunks[5]);
936        }
937
938        if app.config.enable_mouse_support {
939            render_close_button(rect, app, is_active);
940        }
941    }
942}