use crate::{
languages::spoken,
ui::tui::{self, screens, Evt, Screen},
Error, Status,
};
use crossterm::event::{self, KeyCode};
use ratatui::{
buffer::Buffer,
layout::{Alignment, Constraint, Flex, Layout, Rect},
style::{Color, Modifier, Style},
symbols::border::Set,
text::{Line, Span},
widgets::{
block::Position, Block, Borders, Clear, List, ListState, Padding, StatefulWidget, Widget,
},
};
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc::Sender;
use tracing::debug;
const TOP_DIALOG_BORDER: Set = Set {
top_left: "┌",
top_right: "┐",
bottom_left: "│",
bottom_right: "│",
vertical_left: "│",
vertical_right: "│",
horizontal_top: "─",
horizontal_bottom: " ",
};
const STATUS_BORDER: Set = Set {
top_left: " ",
top_right: " ",
bottom_left: "└",
bottom_right: "┘",
vertical_left: " ",
vertical_right: " ",
horizontal_top: " ",
horizontal_bottom: "─",
};
#[derive(Clone, Debug, Default)]
pub struct Spoken<'a> {
spoken_languages: Vec<spoken::Code>,
spoken_language: Option<spoken::Code>,
allow_any: bool,
event: Option<Evt>,
lines: u16,
area: Rect,
centered: Rect,
list: List<'a>,
list_state: ListState,
}
impl Spoken<'_> {
async fn init(
&mut self,
spoken_languages: &[spoken::Code],
spoken_language: Option<spoken::Code>,
allow_any: bool,
event: Option<Evt>,
) -> Result<(), Error> {
self.spoken_languages = spoken_languages.to_vec();
self.spoken_language = spoken_language;
self.allow_any = allow_any;
self.event = event;
self.lines = self.selection_lines(spoken_languages) + 4;
self.area = Rect::default();
self.centered = Rect::default();
let title = Line::from(vec![
Span::styled("─", Style::default().fg(Color::DarkGray)),
Span::styled(
"/ Select a Spoken Language /",
Style::default().fg(Color::White),
),
]);
self.list = List::new(self.language_names())
.block(
Block::default()
.title(title)
.title_style(Style::default().fg(Color::White))
.padding(Padding::uniform(1))
.style(Style::default().fg(Color::DarkGray))
.borders(Borders::LEFT | Borders::RIGHT | Borders::TOP)
.border_set(TOP_DIALOG_BORDER),
)
.highlight_style(
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD),
)
.style(Style::default().fg(Color::White))
.highlight_symbol("> ");
self.list_state
.select(self.selection_from_language(self.spoken_language));
Ok(())
}
fn selection_lines<T, S: AsRef<[T]>>(&self, s: S) -> u16 {
if self.allow_any {
s.as_ref().len() as u16 + 1
} else {
s.as_ref().len() as u16
}
}
fn lang_to_selection(&self, index: usize) -> usize {
if self.allow_any {
index + 1 } else {
index
}
}
fn selection_to_lang(&self, index: usize) -> usize {
if self.allow_any {
index.saturating_sub(1)
} else {
index
}
}
fn language_names(&self) -> Vec<String> {
let mut names = if self.allow_any {
vec!["Any".to_string()]
} else {
vec![]
};
names.extend(
self.spoken_languages
.iter()
.map(|code| code.get_name_in_english().to_string()),
);
names
}
fn language_from_selection(&self, index: usize) -> Option<spoken::Code> {
if index == 0 && self.allow_any {
None
} else {
self.spoken_languages
.get(self.selection_to_lang(index))
.cloned()
}
}
fn selection_from_language(&self, lang: Option<spoken::Code>) -> Option<usize> {
match lang {
Some(code) => match self.spoken_languages.iter().position(|&c| c == code) {
Some(index) => Some(self.lang_to_selection(index)),
None => Some(0),
},
None => Some(0),
}
}
fn recalculate_rect(&mut self, area: Rect) {
if self.area != area {
let [_, hc, _] = Layout::horizontal([
Constraint::Fill(1),
Constraint::Max(44),
Constraint::Fill(1),
])
.areas(area);
[_, self.centered, _] = Layout::vertical([
Constraint::Fill(1),
Constraint::Length(self.lines),
Constraint::Fill(1),
])
.areas(hc);
self.area = area;
}
}
fn render_list(&mut self, area: Rect, buf: &mut Buffer) {
Widget::render(Clear, area, buf);
StatefulWidget::render(&self.list, area, buf, &mut self.list_state);
}
fn render_status(&mut self, area: Rect, buf: &mut Buffer) {
let line = Line::from(vec![
Span::styled("─", Style::default().fg(Color::DarkGray)),
Span::styled(
"/ j,k scroll / ↵ select /",
Style::default().fg(Color::White),
),
]);
let block = Block::default()
.title(line)
.title_style(Style::default().fg(Color::White))
.title_position(Position::Bottom)
.title_alignment(Alignment::Left)
.style(Style::default().fg(Color::DarkGray))
.borders(Borders::LEFT | Borders::BOTTOM | Borders::RIGHT)
.border_set(STATUS_BORDER)
.padding(Padding::horizontal(1));
Widget::render(block, area, buf);
}
pub async fn handle_ui_event(
&mut self,
event: tui::Event,
to_ui: Sender<screens::Event>,
_status: Arc<Mutex<Status>>,
) -> Result<(), Error> {
match event {
tui::Event::ChangeSpokenLanguage(all_languages, spoken, allow_any, next) => {
let mut spoken_languages = all_languages.keys().cloned().collect::<Vec<_>>();
spoken_languages.sort();
debug!("Changing spoken language");
self.init(&spoken_languages, spoken, allow_any, next)
.await?;
to_ui
.send((None, tui::Event::Show(screens::Screens::Spoken)).into())
.await?;
}
_ => {
debug!("Ignoring UI event: {:?}", event);
}
}
Ok(())
}
pub async fn handle_input_event(
&mut self,
event: event::Event,
to_ui: Sender<screens::Event>,
_status: Arc<Mutex<Status>>,
) -> Result<(), Error> {
if let event::Event::Key(key) = event {
match key.code {
KeyCode::PageUp => self.list_state.select_first(),
KeyCode::PageDown => self.list_state.select_last(),
KeyCode::Char('b') | KeyCode::Esc => {
debug!("Back to previous screen");
to_ui
.send((Some(screens::Screens::Workshops), tui::Event::LoadWorkshops).into())
.await?;
}
KeyCode::Char('j') | KeyCode::Down => self.list_state.select_next(),
KeyCode::Char('k') | KeyCode::Up => self.list_state.select_previous(),
KeyCode::Enter => {
let event = self.event.take();
if let Some(selected) = self.list_state.selected() {
let spoken_language = self.language_from_selection(selected);
let set_spoken_language = (
None,
tui::Event::SetSpokenLanguage(
spoken_language,
None, event,
),
);
to_ui.send(set_spoken_language.into()).await?;
}
}
_ => {}
}
}
Ok(())
}
}
#[async_trait::async_trait]
impl Screen for Spoken<'_> {
async fn handle_event(
&mut self,
event: screens::Event,
to_ui: Sender<screens::Event>,
status: Arc<Mutex<Status>>,
) -> Result<(), Error> {
match event {
screens::Event::Input(input_event) => {
self.handle_input_event(input_event, to_ui, status).await
}
screens::Event::Ui(_, ui_event) => self.handle_ui_event(ui_event, to_ui, status).await,
}
}
fn render_screen(&mut self, area: Rect, buf: &mut Buffer) -> Result<(), Error> {
self.recalculate_rect(area);
Widget::render(Clear, self.centered, buf);
let [list_area, status_area] =
Layout::vertical([Constraint::Percentage(100), Constraint::Min(1)])
.flex(Flex::End)
.areas(self.centered);
self.render_list(list_area, buf);
self.render_status(status_area, buf);
Ok(())
}
}