quest_tui/
widget.rs

1//! Custom widgets
2
3use tui::{
4    layout::{Constraint, Direction, Layout, Rect},
5    style::Modifier,
6    text::{Span, Spans, Text},
7    widgets::{Block, BorderType, Borders, List, ListItem, Paragraph},
8};
9
10use crate::{configs::keycode_to_string, App, InputMode, Quest};
11
12/// Split terminal view
13pub fn main_chunks(area: Rect) -> Vec<Rect> {
14    let chunks = Layout::default()
15        .direction(Direction::Vertical)
16        .margin(2)
17        .constraints(
18            [
19                Constraint::Min(1),
20                Constraint::Length(3),
21                Constraint::Length(1),
22            ]
23            .as_ref(),
24        )
25        .split(area);
26
27    chunks
28}
29
30/// Shows a list of quests
31pub fn quest_list(app: &App) -> List {
32    // Map quests to ListItem widget
33    let quests: Vec<ListItem> = app
34        .quests
35        .iter()
36        .enumerate()
37        .map(|q| indexed_quest_item(app, q))
38        .collect();
39
40    List::new(quests).style(app.default_style()).block(
41        Block::default()
42            .title("Quests")
43            .borders(Borders::ALL)
44            .border_type(BorderType::Rounded)
45            .style(app.default_style()),
46    )
47}
48
49/// Check if a quest is selected then renders it properly
50fn indexed_quest_item<'a>(app: &'a App, (index, quest): (usize, &Quest)) -> ListItem<'a> {
51    if let Some(selected_index) = app.selected_quest {
52        quest_item(
53            quest.title.clone(),
54            quest.completed,
55            selected_index == index,
56            app,
57        )
58    } else {
59        quest_item(quest.title.clone(), quest.completed, false, app)
60    }
61}
62
63/// Widget to show a single quest
64fn quest_item(title: String, completed: bool, selected: bool, app: &App) -> ListItem {
65    let style = if selected {
66        app.selection_style()
67    } else {
68        app.default_style()
69    };
70
71    let quest = if completed {
72        ListItem::new(Spans::from(vec![
73            Span::styled("✔  ", app.check_sign_style(selected)),
74            Span::styled(title, app.checked_quest_style(selected)),
75        ]))
76    } else {
77        ListItem::new(Spans::from(vec![
78            Span::styled("   ", style),
79            Span::styled(title, style),
80        ]))
81    };
82
83    quest.style(style)
84}
85
86/// Input field to make a new quest
87pub fn quest_input(app: &App) -> Paragraph {
88    let style = match app.input_mode {
89        InputMode::Normal => app.default_style(),
90        InputMode::Adding => app.default_style().fg(app.configs.colors.selection_bg),
91    };
92
93    let input = Paragraph::new(app.input.as_ref()).style(style).block(
94        Block::default()
95            .borders(Borders::ALL)
96            .title("New Quest")
97            .border_type(BorderType::Rounded)
98            .style(style),
99    );
100
101    input
102}
103
104/// Help text
105pub fn navigation_hint(app: &App) -> Paragraph {
106    let keybindings = &app.configs.keybindings;
107
108    let (msg, style) = match app.input_mode {
109        InputMode::Normal => (
110            vec![
111                Span::styled(
112                    keycode_to_string(keybindings.exit_app),
113                    app.default_style().add_modifier(Modifier::BOLD),
114                ),
115                Span::styled(" exit | ", app.default_style()),
116                Span::styled(
117                    keycode_to_string(keybindings.new_quest),
118                    app.default_style().add_modifier(Modifier::BOLD),
119                ),
120                Span::styled(" new quest | ", app.default_style()),
121                Span::styled(
122                    keycode_to_string(keybindings.check_and_uncheck_quest),
123                    app.default_style().add_modifier(Modifier::BOLD),
124                ),
125                Span::styled(" check/uncheck quest | ", app.default_style()),
126                Span::styled(
127                    format!(
128                        "{}/{}",
129                        keycode_to_string(keybindings.list_up),
130                        keycode_to_string(keybindings.list_down)
131                    ),
132                    app.default_style().add_modifier(Modifier::BOLD),
133                ),
134                Span::styled(" navigate list | ", app.default_style()),
135                Span::styled(
136                    keycode_to_string(keybindings.delete_quest),
137                    app.default_style().add_modifier(Modifier::BOLD),
138                ),
139                Span::styled(" delete quest", app.default_style()),
140            ],
141            app.default_style().add_modifier(Modifier::RAPID_BLINK),
142        ),
143        InputMode::Adding => (
144            vec![
145                Span::styled(
146                    keycode_to_string(keybindings.exit_adding),
147                    app.default_style().add_modifier(Modifier::BOLD),
148                ),
149                Span::styled(" stop adding | ", app.default_style()),
150                Span::styled(
151                    keycode_to_string(keybindings.save_quest),
152                    app.default_style().add_modifier(Modifier::BOLD),
153                ),
154                Span::styled(" save quest", app.default_style()),
155            ],
156            app.default_style(),
157        ),
158    };
159
160    let mut help_text = Text::from(Spans::from(msg));
161    help_text.patch_style(style);
162    Paragraph::new(help_text).style(app.default_style())
163}