diff --git a/kanash-components/src/app.rs b/kanash-components/src/app.rs index 1484dc6..5685402 100644 --- a/kanash-components/src/app.rs +++ b/kanash-components/src/app.rs @@ -48,23 +48,26 @@ impl Components for App { return None; } - if msg == Message::Home(HomeMessage::Up) || msg == Message::Home(HomeMessage::Down) - { - let response = h.update(msg.clone()); - return response; - } - // transform self en App::Kana(new_kana(selected)) if msg == Message::Home(Enter) if let Message::Home(HomeMessage::Enter(mode)) = msg { let mut new_kana = KanaModel::new(); new_kana.mode = mode; - new_kana.update(Message::Kana(KanaMessage::Pass)); + match mode { + Mode::Hira => new_kana.scroll_state = ScrollbarState::new(HIRAGANA_NUMBER), + Mode::Kata => new_kana.scroll_state = ScrollbarState::new(KATAKANA_NUMBER), + Mode::Both => { + new_kana.scroll_state = + ScrollbarState::new(HIRAGANA_NUMBER + KATAKANA_NUMBER) + } + } + let response = new_kana.update(Message::Kana(KanaMessage::Pass)); self.page = AppPage::Kana(new_kana); + return response; } - None + h.update(msg.clone()) } AppPage::Kana(ref mut k) => { let response = k.update(msg.clone()); diff --git a/kanash-components/src/helper/ja.rs b/kanash-components/src/helper/ja.rs index ffde2f9..e925dfa 100644 --- a/kanash-components/src/helper/ja.rs +++ b/kanash-components/src/helper/ja.rs @@ -1,5 +1,5 @@ -const HIRAGANA_NUMBER: usize = 71; -const WANTED_HIRAGANA: [u16; HIRAGANA_NUMBER] = [ +pub const HIRAGANA_NUMBER: usize = 71; +pub const WANTED_HIRAGANA: [u16; HIRAGANA_NUMBER] = [ 12354, 12356, 12358, 12360, 12362, 12363, 12364, 12365, 12366, 12367, 12368, 12369, 12370, 12371, 12372, 12373, 12374, 12375, 12376, 12377, 12378, 12379, 12380, 12381, 12382, 12383, 12384, 12385, 12386, 12388, 12389, 12390, 12391, 12392, 12393, 12394, 12395, 12396, 12397, @@ -7,8 +7,8 @@ const WANTED_HIRAGANA: [u16; HIRAGANA_NUMBER] = [ 12411, 12412, 12413, 12414, 12415, 12416, 12417, 12418, 12420, 12422, 12424, 12425, 12426, 12427, 12428, 12429, 12431, 12434, 12435, ]; -const KATAKANA_NUMBER: usize = 81; -const WANTED_KATAKANA: [u16; KATAKANA_NUMBER] = [ +pub const KATAKANA_NUMBER: usize = 81; +pub const WANTED_KATAKANA: [u16; KATAKANA_NUMBER] = [ 12449, 12450, 12451, 12452, 12453, 12454, 12455, 12456, 12457, 12458, 12459, 12460, 12461, 12462, 12463, 12464, 12465, 12466, 12467, 12468, 12469, 12470, 12471, 12472, 12473, 12474, 12475, 12476, 12477, 12478, 12479, 12480, 12481, 12482, 12484, 12485, 12486, 12487, 12488, @@ -18,15 +18,23 @@ const WANTED_KATAKANA: [u16; KATAKANA_NUMBER] = [ 12530, 12531, 12532, ]; -pub fn random_hiragana() -> String { - String::from_utf16(&[WANTED_HIRAGANA[rand::random_range(0..71)]]).expect("error hiragana") +pub fn random_hiragana() -> (String, usize) { + let index = rand::random_range(0..HIRAGANA_NUMBER); + ( + String::from_utf16(&[WANTED_HIRAGANA[index]]).expect("error hiragana"), + index, + ) } -pub fn random_katakana() -> String { - String::from_utf16(&[WANTED_KATAKANA[rand::random_range(0..81)]]).expect("error katakana") +pub fn random_katakana() -> (String, usize) { + let index = rand::random_range(0..KATAKANA_NUMBER); + ( + String::from_utf16(&[WANTED_KATAKANA[index]]).expect("error katakana"), + index + HIRAGANA_NUMBER, + ) } -pub fn random_kana() -> String { +pub fn random_kana() -> (String, usize) { if rand::random() { random_hiragana() } else { diff --git a/kanash-components/src/helper/mod.rs b/kanash-components/src/helper/mod.rs index 2035d94..c2da6f2 100644 --- a/kanash-components/src/helper/mod.rs +++ b/kanash-components/src/helper/mod.rs @@ -1,2 +1,46 @@ pub mod ja; pub mod rain; + +use super::*; + +pub fn help_popup(help_strings: [&str; 5], max_height: u16, max_width: u16, frame: &mut Frame) { + let block = Block::new() + .title(Line::from(" Help ").fg(ColorPalette::TITLE).centered()) + .border_type(BorderType::Rounded) + .fg(ColorPalette::KEY_HINT) + .borders(Borders::ALL); + + let p = Paragraph::new(parse_strings_to_text(help_strings)) + .wrap(Wrap { trim: true }) + .fg(Color::White); + + let vertical = Layout::vertical([Constraint::Max(max_height)]).flex(Flex::Center); + let horizontal = Layout::horizontal([Constraint::Max(max_width)]).flex(Flex::Center); + let [area] = vertical.areas(frame.area()); + let [area] = horizontal.areas(area); + + frame.render_widget(Clear, area); + frame.render_widget(p.block(block), area); +} + +fn parse_strings_to_text(strings: [&str; 5]) -> Text<'_> { + strings + .into_iter() + .map(|s| { + if s.is_empty() { + return Line::default(); + } + + if !s.contains(" - ") { + return Line::from(s).centered(); + } + + let mut sub_strings = s.split(" - "); + + let key = Span::from(sub_strings.next().unwrap_or("")).bg(ColorPalette::SELECTION); + let function = Span::from(sub_strings.next().unwrap_or("")); + + Line::from(vec![key, " - ".into(), function]).centered() + }) + .collect() +} diff --git a/kanash-components/src/home.rs b/kanash-components/src/home.rs index 92cf9c0..44d6223 100644 --- a/kanash-components/src/home.rs +++ b/kanash-components/src/home.rs @@ -1,10 +1,14 @@ use super::*; -const KEY_HELPER_CYCLE: &str = - " Quit | Move | Select | Rain | Cycle Background "; -const KEY_HELPER_NONE: &str = - " Quit | Move | Select | Rain | No Background "; const TITLE: &str = " KANA SH "; +const KEY_HELPER: &str = " h - ? "; +const HELP_STRINGS: [&str; 5] = [ + "h? - toggle this popup", + "jk - navigate up and down", + "x - disable rain fx", + "Enter - enter selected mode", + "esc q - quit", +]; const SELECTED_STYLE: Style = Style::new() .bg(ColorPalette::SELECTION) .add_modifier(Modifier::BOLD); @@ -26,7 +30,7 @@ pub enum BackgroundMode { pub struct HomeModel { page_list: Vec, state: ListState, - pub key_helper_state: BackgroundMode, + show_help_popup: bool, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -38,6 +42,8 @@ pub enum HomeMessage { Down, RainFx, + + Help, } impl Components for HomeModel { @@ -52,14 +58,14 @@ impl Components for HomeModel { "Learn Both".into(), ], state: init_state, - key_helper_state: BackgroundMode::Cycle, + show_help_popup: false, } } /// Handle Event (Mostly convert key event to message) fn handle_event(&self, event: &PlatformKeyEvent) -> Option { match event.code { - KeyCode::Esc => Some(Message::Back), + KeyCode::Esc | KeyCode::Char('q') => Some(Message::Back), KeyCode::Enter => { if let Some(i) = self.state.selected() { match i { @@ -74,6 +80,7 @@ impl Components for HomeModel { } KeyCode::Char('j') | KeyCode::Down => Some(Message::Home(HomeMessage::Down)), KeyCode::Char('k') | KeyCode::Up => Some(Message::Home(HomeMessage::Up)), + KeyCode::Char('h') | KeyCode::Char('?') => Some(Message::Home(HomeMessage::Help)), KeyCode::Char('x') => Some(Message::Home(HomeMessage::RainFx)), _ => None, } @@ -88,6 +95,9 @@ impl Components for HomeModel { HomeMessage::Up => { self.state.select_previous(); } + HomeMessage::Help => { + self.show_help_popup = !self.show_help_popup; + } _ => {} } } @@ -96,26 +106,17 @@ impl Components for HomeModel { fn view(&mut self, frame: &mut Frame, _elapsed: Duration) { let n_page: u16 = self.page_list.len().try_into().unwrap(); - let [_, main_area, _] = Layout::vertical([ - Constraint::Min(0), - Constraint::Length(n_page + 4), - Constraint::Min(0), - ]) - .areas(frame.area()); - - let key_helper = match self.key_helper_state { - BackgroundMode::Cycle => KEY_HELPER_CYCLE, - BackgroundMode::Disable => KEY_HELPER_NONE, - }; + let [vert_area] = Layout::vertical([Constraint::Length(n_page + 4)]) + .flex(Flex::Center) + .areas(frame.area()); + + let [main_area] = Layout::horizontal([Constraint::Max(23)]) + .flex(Flex::Center) + .areas(vert_area); let block = Block::new() .title(Line::from(TITLE).fg(ColorPalette::TITLE).centered()) - .title_bottom( - Line::from(key_helper) - .fg(ColorPalette::KEY_HINT) - .bold() - .centered(), - ) + .title_bottom(Line::from(KEY_HELPER).fg(ColorPalette::KEY_HINT).centered()) .border_type(BorderType::Rounded) .padding(Padding::vertical(1)) .borders(Borders::ALL); @@ -133,5 +134,9 @@ impl Components for HomeModel { frame.render_widget(Clear, main_area); frame.render_stateful_widget(list, main_area, &mut self.state); + + if self.show_help_popup { + help_popup(HELP_STRINGS, 10, 30, frame); + } } } diff --git a/kanash-components/src/kana.rs b/kanash-components/src/kana.rs index 021ea75..e1fa76c 100644 --- a/kanash-components/src/kana.rs +++ b/kanash-components/src/kana.rs @@ -1,22 +1,33 @@ -// use crate::components::helper::image; -use crate::helper::ja::*; use wana_kana::ConvertJapanese; use super::*; const TITLE: &str = " Hiragana "; +const STATS_TITLE: &str = " Stats "; const LEFT_TITLE: &str = " Shown: "; const RIGHT_TITLE: &str = " Correct: "; -const KEY_HELPER: &str = " Main Menu | Show answer "; +const KEY_HELPER: &str = " ? "; +const HELP_STRINGS: [&str; 5] = [ + "Type the corresponding romaji. Good answers are logged automaticaly.", + "", + "? - toggle this popup", + "esc - go back to main menu", + "space - reveal answer", +]; #[derive(Debug, PartialEq, Eq)] pub struct KanaModel { shown: u32, correct: u32, input: String, - current_kana: String, + current_kana: (String, usize), display_answer: bool, + show_help_popup: bool, pub mode: Mode, + pub scroll_state: ScrollbarState, + good_cnts: [usize; HIRAGANA_NUMBER], + bad_cnts: [usize; HIRAGANA_NUMBER], + kana_permutation: Vec, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -30,8 +41,14 @@ pub enum KanaMessage { /// Delete roma DeleteRoma, - /// Pass, + /// Pass Pass, + + /// Help + Help, + + ScrollUp, + ScrollDown, } impl Components for KanaModel { @@ -43,7 +60,12 @@ impl Components for KanaModel { input: String::new(), current_kana: random_kana(), display_answer: false, + show_help_popup: false, mode: Mode::Hira, + scroll_state: ScrollbarState::new(HIRAGANA_NUMBER), + good_cnts: [0; HIRAGANA_NUMBER], + bad_cnts: [0; HIRAGANA_NUMBER], + kana_permutation: (0..(HIRAGANA_NUMBER)).collect(), } } @@ -52,28 +74,33 @@ impl Components for KanaModel { match key_event.code { KeyCode::Esc => Some(Message::Back), KeyCode::Backspace => Some(Message::Kana(KanaMessage::DeleteRoma)), + KeyCode::Char('?') => Some(Message::Kana(KanaMessage::Help)), KeyCode::Char(' ') => Some(Message::Kana(KanaMessage::Answer)), KeyCode::Char(c) => Some(Message::Kana(KanaMessage::TypingRoma(c))), + KeyCode::Up => Some(Message::Kana(KanaMessage::ScrollUp)), + KeyCode::Down => Some(Message::Kana(KanaMessage::ScrollDown)), _ => None, } } fn update(&mut self, msg: Message) -> Option { if let Message::Kana(kana_msg) = msg { - return match kana_msg { + match kana_msg { KanaMessage::TypingRoma(c) => { self.input.push(c); - if self.input == self.current_kana.to_romaji() { + if self.input == self.current_kana.0.to_romaji() { + self.kana_permutation[..=self.current_kana.1].rotate_right(1); if self.display_answer { self.display_answer = false; + self.bad_cnts[self.current_kana.1] += 1; } else { + self.good_cnts[self.current_kana.1] += 1; self.correct += 1; } self.shown += 1; return Some(Message::Kana(KanaMessage::Pass)); } - None } KanaMessage::Pass => { self.input = String::new(); @@ -82,17 +109,22 @@ impl Components for KanaModel { Mode::Kata => self.current_kana = random_katakana(), Mode::Both => self.current_kana = random_kana(), } - - None } KanaMessage::Answer => { self.display_answer = true; self.input = String::new(); - None } KanaMessage::DeleteRoma => { self.input.pop(); - None + } + KanaMessage::Help => { + self.show_help_popup = !self.show_help_popup; + } + KanaMessage::ScrollUp => { + self.scroll_state.prev(); + } + KanaMessage::ScrollDown => { + self.scroll_state.next(); } }; } @@ -100,26 +132,26 @@ impl Components for KanaModel { } fn view(&mut self, frame: &mut Frame, _elapsed: Duration) { - self.learning_zone(frame); + let [v_area] = Layout::vertical([Constraint::Length(7)]) + .flex(Flex::Center) + .areas(frame.area()); + + let [learning_area, stats_area] = + Layout::horizontal([Constraint::Length(43), Constraint::Length(10)]) + .flex(Flex::Center) + .areas(v_area); + + self.learning_zone(frame, learning_area); + self.stats_zone(frame, stats_area); + + if self.show_help_popup { + help_popup(HELP_STRINGS, 10, 30, frame); + } } } impl KanaModel { - fn learning_zone(&mut self, frame: &mut Frame) { - let [_, v_area, _] = Layout::vertical([ - Constraint::Min(0), - Constraint::Length(7), - Constraint::Min(0), - ]) - .areas(frame.area()); - - let [_, main_area, _] = Layout::horizontal([ - Constraint::Min(0), - Constraint::Length(43), - Constraint::Min(0), - ]) - .areas(v_area); - + fn learning_zone(&mut self, frame: &mut Frame, area: Rect) { let left_title = Line::from(vec![ LEFT_TITLE.fg(ColorPalette::SUBTITLE).into(), self.shown.to_string().yellow(), @@ -149,9 +181,9 @@ impl KanaModel { .borders(Borders::ALL); let text = vec![ - Line::from(self.current_kana.clone()), + Line::from(self.current_kana.0.clone()), if self.display_answer { - Line::from(self.current_kana.to_romaji()).fg(ColorPalette::ERROR) + Line::from(self.current_kana.0.to_romaji()).fg(ColorPalette::ERROR) } else { Line::default() }, @@ -160,7 +192,65 @@ impl KanaModel { let p = Paragraph::new(text).block(block).centered(); - frame.render_widget(Clear, main_area); - frame.render_widget(p, main_area); + frame.render_widget(Clear, area); + frame.render_widget(p, area); + } + + fn stats_zone(&mut self, frame: &mut Frame, area: Rect) { + let stats_block = Block::new() + .title(Line::from(STATS_TITLE).fg(ColorPalette::TITLE).centered()) + .border_type(BorderType::Rounded) + .borders(Borders::ALL); + + // let stats_closure = |(i, u): (usize, &u16)| { + // format!( + // "{} {}/{}", + // String::from_utf16(&[WANTED_HIRAGANA[self.kana_permutation[i]]]).expect("error"), + // self.good_cnts[self.kana_permutation[i]], + // self.bad_cnts[self.kana_permutation[i]] + // ) + // }; + + // let stat_text = match self.mode { + // Mode::Hira => Text::from_iter(WANTED_HIRAGANA.iter().enumerate().map(stats_closure)), + // Mode::Kata => Text::from_iter( + // WANTED_KATAKANA + // .iter() + // .enumerate() + // .map(|(i, u)| stats_closure((i + HIRAGANA_NUMBER, u))), + // ), + // Mode::Both => Text::from_iter( + // WANTED_HIRAGANA + // .iter() + // .chain(WANTED_KATAKANA.iter()) + // .enumerate() + // .map(stats_closure), + // ), + // }; + + let stats_closure = |i: &usize| { + format!( + "{i}{} {} {}", + String::from_utf16(&[WANTED_HIRAGANA[*i]]).expect("err"), + self.good_cnts[*i], + self.bad_cnts[*i], + ) + }; + + let stat_text = Text::from_iter(self.kana_permutation.iter().map(stats_closure)); + + let stats = Paragraph::new(stat_text) + .block(stats_block) + .scroll((self.scroll_state.get_position() as u16, 0)); + + frame.render_widget(Clear, area); + frame.render_widget(stats, area); + frame.render_stateful_widget( + Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(Some("╮")) + .end_symbol(Some("╯")), + area, + &mut self.scroll_state, + ); } } diff --git a/kanash-components/src/lib.rs b/kanash-components/src/lib.rs index 3ce3aee..14b39d7 100644 --- a/kanash-components/src/lib.rs +++ b/kanash-components/src/lib.rs @@ -6,12 +6,13 @@ pub mod kana; #[cfg(not(target_arch = "wasm32"))] pub use ratatui::{ crossterm::event::{self, Event, KeyCode}, - layout::{Constraint, Layout}, - style::Stylize, - style::{palette::tailwind::SLATE, Color, Modifier, Style}, - text::Line, - widgets::{Block, Paragraph}, - widgets::{BorderType, Borders, Clear, HighlightSpacing, List, ListItem, ListState, Padding}, + layout::{Constraint, Flex, Layout, Rect}, + style::{palette::tailwind::SLATE, Color, Modifier, Style, Stylize}, + text::{Line, Span, Text}, + widgets::{ + Bar, BarChart, Block, BorderType, Borders, Clear, HighlightSpacing, List, ListItem, + ListState, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap, + }, Frame, }; @@ -20,12 +21,13 @@ pub type PlatformKeyEvent = ratatui::crossterm::event::KeyEvent; #[cfg(target_arch = "wasm32")] pub use ratzilla::ratatui::{ - layout::{Constraint, Layout}, - style::Stylize, - style::{palette::tailwind::SLATE, Color, Modifier, Style}, - text::Line, - widgets::{Block, Paragraph}, - widgets::{BorderType, Borders, Clear, HighlightSpacing, List, ListItem, ListState, Padding}, + layout::{Constraint, Flex, Layout, Rect}, + style::{palette::tailwind::SLATE, Color, Modifier, Style, Stylize}, + text::{Line, Span, Text}, + widgets::{ + Block, BorderType, Borders, Clear, HighlightSpacing, List, ListItem, ListState, Padding, + Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap, + }, Frame, }; @@ -37,6 +39,7 @@ pub use ratzilla::{event::KeyCode, web_sys::console}; use std::time::Duration; +use helper::{help_popup, ja::*}; use home::{HomeMessage, Mode}; use kana::KanaMessage;