aboutsummaryrefslogtreecommitdiff
path: root/src/gui/question.rs
blob: 5f060e333fc3dcd80a2b55d237cc7a9781d13a28 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
use crate::{
    gui::util,
    model::{card::Card, difficulty, difficulty::Difficulty},
    util::event::{Event, Events},
    util::serialization,
};
use anyhow::Result;
use termion::event::Key;
use tui::{
    backend::Backend,
    layout::{Alignment, Constraint, Direction, Layout},
    style::{Color, Modifier, Style},
    text::{Span, Spans, Text},
    widgets::{Block, Borders, Paragraph, Wrap},
    Terminal,
};

struct State {
    pub input: String,
    pub answer: Answer,
}

enum Answer {
    Write,
    Difficulty { difficulty: Difficulty },
}

pub fn ask<B: Backend>(
    terminal: &mut Terminal<B>,
    events: &Events,
    title: &str,
    card: &Card,
) -> Result<Difficulty> {
    let mut state = State {
        input: String::new(),
        answer: Answer::Write,
    };

    loop {
        terminal.draw(|f| {
            let chunks = Layout::default()
                .direction(Direction::Vertical)
                .margin(2)
                .constraints(
                    [
                        Constraint::Length(1),
                        Constraint::Percentage(30),
                        Constraint::Length(5),
                        Constraint::Percentage(30),
                        Constraint::Length(5),
                    ]
                    .as_ref(),
                )
                .split(f.size());

            let d1 = util::title(title);
            f.render_widget(d1, chunks[0]);

            let question = Paragraph::new(util::center_vertically(chunks[1], &card.question))
                .style(match state.answer {
                    Answer::Write => {
                        if state.input.is_empty() {
                            Style::default().fg(Color::Yellow)
                        } else {
                            Style::default()
                        }
                    }
                    _ => Style::default(),
                })
                .alignment(Alignment::Center);
            f.render_widget(question, chunks[1]);

            let answer = Paragraph::new(util::center_vertically(chunks[2], &state.input))
                .style(match state.answer {
                    Answer::Write => Style::default(),
                    Answer::Difficulty { difficulty: _ } => {
                        if is_correct(&state.input, &card.responses) {
                            Style::default().fg(Color::Green)
                        } else {
                            Style::default().fg(Color::Red)
                        }
                    }
                })
                .alignment(Alignment::Center)
                .block(Block::default().borders(Borders::ALL).title("Réponse"))
                .wrap(Wrap { trim: true });
            f.render_widget(answer, chunks[2]);

            if let Answer::Difficulty {
                difficulty: selected,
            } = state.answer
            {
                if !is_correct(&state.input, &card.responses) || card.responses.len() > 1 {
                    let paragraph = Paragraph::new(util::center_vertically(
                        chunks[3],
                        &serialization::words_to_line(&card.responses),
                    ))
                    .alignment(Alignment::Center);
                    f.render_widget(paragraph, chunks[3]);
                };

                let difficulties = card.state.difficulties();
                let l = difficulties.len();
                let sep = Span::styled(" • ", Style::default());
                let tabs = difficulties
                    .iter()
                    .enumerate()
                    .map(|(i, d)| {
                        let style = if *d == selected {
                            Style::default()
                                .fg(Color::Yellow)
                                .add_modifier(Modifier::UNDERLINED)
                        } else {
                            Style::default().add_modifier(Modifier::DIM)
                        };
                        let d = Span::styled(difficulty::label(*d), style);
                        if i < l - 1 {
                            [d, sep.clone()].to_vec()
                        } else {
                            [d].to_vec()
                        }
                    })
                    .collect::<Vec<Vec<Span>>>()
                    .concat();
                let p = Paragraph::new(Text::from(Spans::from(tabs))).alignment(Alignment::Center);
                f.render_widget(p, chunks[4]);
            }
        })?;

        if let Event::Input(key) = events.next()? {
            match state.answer {
                Answer::Write => match key {
                    Key::Char('\n') => {
                        let difficulty = if is_correct(&state.input, &card.responses) {
                            Difficulty::Good
                        } else {
                            Difficulty::Again
                        };
                        state.answer = Answer::Difficulty { difficulty }
                    }
                    Key::Char(c) => {
                        state.input.push(c);
                    }
                    Key::Backspace => {
                        state.input.pop();
                    }
                    _ => {}
                },
                Answer::Difficulty {
                    difficulty: selected,
                } => match key {
                    Key::Left => {
                        for d in relative_element(&card.state.difficulties(), &selected, -1).iter()
                        {
                            state.answer = Answer::Difficulty { difficulty: *d }
                        }
                    }
                    Key::Right => {
                        for d in relative_element(&card.state.difficulties(), &selected, 1).iter() {
                            state.answer = Answer::Difficulty { difficulty: *d }
                        }
                    }
                    Key::Char('\n') => return Ok(selected),
                    _ => {}
                },
            }
        }
    }
}

fn is_correct(input: &str, responses: &[String]) -> bool {
    responses
        .iter()
        .map(|r| r.split('(').collect::<Vec<&str>>()[0].trim())
        .any(|x| x == input)
}

fn relative_element<T: Clone + PartialEq>(xs: &[T], x: &T, ri: i32) -> Option<T> {
    let i = xs.iter().position(|t| t == x)? as i32 + ri;
    if i >= 0 && i < xs.len() as i32 {
        Some(xs[i as usize].clone())
    } else {
        None
    }
}