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 enum Response { Aborted, Answered { difficulty: Difficulty }, } pub fn ask( terminal: &mut Terminal, events: &Events, title: &str, card: &Card, ) -> Result { 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.trim().is_empty() { Style::default().fg(Color::Yellow) } else { Style::default() } } _ => Style::default(), }) .alignment(Alignment::Center); f.render_widget(question, chunks[1]); let formatted_input = match state.answer { Answer::Write => format!("{}█", state.input), _ => format!("{} ", state.input), }; let answer = Paragraph::new(util::center_vertically(chunks[2], &formatted_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) { 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::>>() .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(); } Key::Ctrl('u') => { state.input.clear(); } Key::Ctrl('w') => { let mut words = state.input.split_whitespace().collect::>(); if words.len() > 0 { words.truncate(words.len() - 1); state.input = format!( "{}{}", words.join(" "), if words.len() > 0 { " " } else { "" } ); } } Key::Ctrl('c') => { return Ok(Response::Aborted); } _ => {} }, 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(Response::Answered { difficulty: selected, }) } Key::Ctrl('c') => { return Ok(Response::Aborted); } _ => {} }, } } } } fn is_correct(input: &str, responses: &[String]) -> bool { // Remove whitespaces let input = input.split_whitespace().map(|word| word.trim()).collect::>().join(" "); responses .iter() .map(|r| r.split('(').collect::>()[0].trim()) .any(|x| x == input) } fn relative_element(xs: &[T], x: &T, ri: i32) -> Option { 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 } }