From bb72340a13e1adceddf29ae9129efb1d24d7e9d4 Mon Sep 17 00:00:00 2001 From: benoitlx Date: Tue, 17 Feb 2026 11:14:59 +0100 Subject: [PATCH 1/8] wip: reworking ui --- kanash-components/src/app.rs | 11 +++----- kanash-components/src/home.rs | 47 +++++++++++++++++++++-------------- kanash-components/src/lib.rs | 4 +-- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/kanash-components/src/app.rs b/kanash-components/src/app.rs index 1484dc6..b0970a4 100644 --- a/kanash-components/src/app.rs +++ b/kanash-components/src/app.rs @@ -48,23 +48,18 @@ 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)); + 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/home.rs b/kanash-components/src/home.rs index 92cf9c0..7f79bc6 100644 --- a/kanash-components/src/home.rs +++ b/kanash-components/src/home.rs @@ -1,9 +1,5 @@ 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 SELECTED_STYLE: Style = Style::new() .bg(ColorPalette::SELECTION) @@ -26,7 +22,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 +34,8 @@ pub enum HomeMessage { Down, RainFx, + + Help, } impl Components for HomeModel { @@ -52,14 +50,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 { @@ -75,6 +73,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('x') => Some(Message::Home(HomeMessage::RainFx)), + KeyCode::Char('h') => Some(Message::Home(HomeMessage::Help)), _ => None, } } @@ -88,6 +87,9 @@ impl Components for HomeModel { HomeMessage::Up => { self.state.select_previous(); } + HomeMessage::Help => { + self.show_help_popup = !self.show_help_popup; + } _ => {} } } @@ -96,26 +98,19 @@ 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([ + let [_, vert_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 [_, main_area, _] = + Layout::horizontal([Constraint::Min(0), Constraint::Max(23), Constraint::Min(0)]) + .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(), - ) .border_type(BorderType::Rounded) .padding(Padding::vertical(1)) .borders(Borders::ALL); @@ -133,5 +128,21 @@ 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 { + let block = Block::new().title("Help").borders(Borders::ALL); + let p = Paragraph::new( + "h - show this popup\njk - navigate up and down\nx - disable rain fx", + ) + .block(block); + + let vertical = Layout::vertical([Constraint::Percentage(30)]).flex(Flex::Center); + let horizontal = Layout::horizontal([Constraint::Percentage(50)]).flex(Flex::Center); + let [area] = vertical.areas(frame.area()); + let [area] = horizontal.areas(area); + + frame.render_widget(Clear, area); + frame.render_widget(p, area); + } } } diff --git a/kanash-components/src/lib.rs b/kanash-components/src/lib.rs index 3ce3aee..e262e1b 100644 --- a/kanash-components/src/lib.rs +++ b/kanash-components/src/lib.rs @@ -6,7 +6,7 @@ pub mod kana; #[cfg(not(target_arch = "wasm32"))] pub use ratatui::{ crossterm::event::{self, Event, KeyCode}, - layout::{Constraint, Layout}, + layout::{Constraint, Flex, Layout, Rect}, style::Stylize, style::{palette::tailwind::SLATE, Color, Modifier, Style}, text::Line, @@ -20,7 +20,7 @@ pub type PlatformKeyEvent = ratatui::crossterm::event::KeyEvent; #[cfg(target_arch = "wasm32")] pub use ratzilla::ratatui::{ - layout::{Constraint, Layout}, + layout::{Constraint, Flex, Layout}, style::Stylize, style::{palette::tailwind::SLATE, Color, Modifier, Style}, text::Line, From 5ccb4289e80c52d52e40d51691305911de4815af Mon Sep 17 00:00:00 2001 From: benoitlx Date: Tue, 17 Feb 2026 12:19:49 +0100 Subject: [PATCH 2/8] feat: help popup --- kanash-components/src/home.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/kanash-components/src/home.rs b/kanash-components/src/home.rs index 7f79bc6..ab08f34 100644 --- a/kanash-components/src/home.rs +++ b/kanash-components/src/home.rs @@ -1,6 +1,8 @@ use super::*; const TITLE: &str = " KANA SH "; +const KEY_HELPER: &str = " help - "; +const HELP_STRING: &str = "h? - toggle help popup\njk - navigate up and down\nx - disable rain fx\nEnter - enter the selected mode\nq Escape - quit"; const SELECTED_STYLE: Style = Style::new() .bg(ColorPalette::SELECTION) .add_modifier(Modifier::BOLD); @@ -72,8 +74,8 @@ 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)), - KeyCode::Char('h') => Some(Message::Home(HomeMessage::Help)), _ => None, } } @@ -111,6 +113,7 @@ impl Components for HomeModel { let block = Block::new() .title(Line::from(TITLE).fg(ColorPalette::TITLE).centered()) + .title_bottom(Line::from(KEY_HELPER).fg(ColorPalette::KEY_HINT).centered()) .border_type(BorderType::Rounded) .padding(Padding::vertical(1)) .borders(Borders::ALL); @@ -130,11 +133,13 @@ impl Components for HomeModel { frame.render_stateful_widget(list, main_area, &mut self.state); if self.show_help_popup { - let block = Block::new().title("Help").borders(Borders::ALL); - let p = Paragraph::new( - "h - show this popup\njk - navigate up and down\nx - disable rain fx", - ) - .block(block); + let block = Block::new() + .title("Help") + .border_type(BorderType::Rounded) + .borders(Borders::ALL); + let p = Paragraph::new(HELP_STRING) + .fg(ColorPalette::KEY_HINT) + .block(block); let vertical = Layout::vertical([Constraint::Percentage(30)]).flex(Flex::Center); let horizontal = Layout::horizontal([Constraint::Percentage(50)]).flex(Flex::Center); From 06c32dce3a1b664b2b4e1825ae4bf688e75bb552 Mon Sep 17 00:00:00 2001 From: benoitlx Date: Wed, 18 Feb 2026 15:54:43 +0100 Subject: [PATCH 3/8] refactor: help popup for home menu --- kanash-components/src/helper/mod.rs | 37 +++++++++++++++++++++++++ kanash-components/src/home.rs | 43 ++++++++++++----------------- kanash-components/src/lib.rs | 16 +++++++---- 3 files changed, 64 insertions(+), 32 deletions(-) diff --git a/kanash-components/src/helper/mod.rs b/kanash-components/src/helper/mod.rs index 2035d94..55db74c 100644 --- a/kanash-components/src/helper/mod.rs +++ b/kanash-components/src/helper/mod.rs @@ -1,2 +1,39 @@ pub mod ja; pub mod rain; + +use super::{ + Block, BorderType, Borders, Clear, Color, ColorPalette, Constraint, Flex, Frame, Layout, Line, + Paragraph, Span, Stylize, Text, Wrap, +}; + +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| { + 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 ab08f34..f2bc601 100644 --- a/kanash-components/src/home.rs +++ b/kanash-components/src/home.rs @@ -1,8 +1,16 @@ +use crate::helper::help_popup; + use super::*; const TITLE: &str = " KANA SH "; -const KEY_HELPER: &str = " help - "; -const HELP_STRING: &str = "h? - toggle help popup\njk - navigate up and down\nx - disable rain fx\nEnter - enter the selected mode\nq Escape - quit"; +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); @@ -100,16 +108,13 @@ 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 [_, vert_area, _] = Layout::vertical([ - Constraint::Min(0), - Constraint::Length(n_page + 4), - Constraint::Min(0), - ]) - .areas(frame.area()); + let [vert_area] = Layout::vertical([Constraint::Length(n_page + 4)]) + .flex(Flex::Center) + .areas(frame.area()); - let [_, main_area, _] = - Layout::horizontal([Constraint::Min(0), Constraint::Max(23), Constraint::Min(0)]) - .areas(vert_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()) @@ -133,21 +138,7 @@ impl Components for HomeModel { frame.render_stateful_widget(list, main_area, &mut self.state); if self.show_help_popup { - let block = Block::new() - .title("Help") - .border_type(BorderType::Rounded) - .borders(Borders::ALL); - let p = Paragraph::new(HELP_STRING) - .fg(ColorPalette::KEY_HINT) - .block(block); - - let vertical = Layout::vertical([Constraint::Percentage(30)]).flex(Flex::Center); - let horizontal = Layout::horizontal([Constraint::Percentage(50)]).flex(Flex::Center); - let [area] = vertical.areas(frame.area()); - let [area] = horizontal.areas(area); - - frame.render_widget(Clear, area); - frame.render_widget(p, area); + help_popup(HELP_STRINGS, 10, 30, frame); } } } diff --git a/kanash-components/src/lib.rs b/kanash-components/src/lib.rs index e262e1b..4264684 100644 --- a/kanash-components/src/lib.rs +++ b/kanash-components/src/lib.rs @@ -9,9 +9,11 @@ pub use ratatui::{ layout::{Constraint, Flex, Layout, Rect}, style::Stylize, style::{palette::tailwind::SLATE, Color, Modifier, Style}, - text::Line, - widgets::{Block, Paragraph}, - widgets::{BorderType, Borders, Clear, HighlightSpacing, List, ListItem, ListState, Padding}, + text::{Line, Span, Text}, + widgets::{ + Block, BorderType, Borders, Clear, HighlightSpacing, List, ListItem, ListState, Padding, + Paragraph, Wrap, + }, Frame, }; @@ -23,9 +25,11 @@ pub use ratzilla::ratatui::{ layout::{Constraint, Flex, Layout}, style::Stylize, style::{palette::tailwind::SLATE, Color, Modifier, Style}, - text::Line, - widgets::{Block, Paragraph}, - widgets::{BorderType, Borders, Clear, HighlightSpacing, List, ListItem, ListState, Padding}, + text::{Line, Span, Text}, + widgets::{ + Block, BorderType, Borders, Clear, HighlightSpacing, List, ListItem, ListState, Padding, + Paragraph, Wrap, + }, Frame, }; From 39a917ab5c89151d1b78b3bc6e82791b3ed56ee8 Mon Sep 17 00:00:00 2001 From: benoitlx Date: Fri, 20 Feb 2026 10:00:32 +0100 Subject: [PATCH 4/8] feat: help popup for kana page --- kanash-components/src/helper/mod.rs | 10 +++++++ kanash-components/src/home.rs | 2 -- kanash-components/src/kana.rs | 45 +++++++++++++++++++---------- kanash-components/src/lib.rs | 1 + 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/kanash-components/src/helper/mod.rs b/kanash-components/src/helper/mod.rs index 55db74c..66b32fc 100644 --- a/kanash-components/src/helper/mod.rs +++ b/kanash-components/src/helper/mod.rs @@ -30,9 +30,19 @@ 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 f2bc601..44d6223 100644 --- a/kanash-components/src/home.rs +++ b/kanash-components/src/home.rs @@ -1,5 +1,3 @@ -use crate::helper::help_popup; - use super::*; const TITLE: &str = " KANA SH "; diff --git a/kanash-components/src/kana.rs b/kanash-components/src/kana.rs index 021ea75..40e72a5 100644 --- a/kanash-components/src/kana.rs +++ b/kanash-components/src/kana.rs @@ -1,5 +1,3 @@ -// use crate::components::helper::image; -use crate::helper::ja::*; use wana_kana::ConvertJapanese; use super::*; @@ -7,7 +5,14 @@ use super::*; const TITLE: &str = " Hiragana "; 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 { @@ -16,6 +21,7 @@ pub struct KanaModel { input: String, current_kana: String, display_answer: bool, + show_help_popup: bool, pub mode: Mode, } @@ -30,8 +36,11 @@ pub enum KanaMessage { /// Delete roma DeleteRoma, - /// Pass, + /// Pass Pass, + + /// Help + Help, } impl Components for KanaModel { @@ -43,6 +52,7 @@ impl Components for KanaModel { input: String::new(), current_kana: random_kana(), display_answer: false, + show_help_popup: false, mode: Mode::Hira, } } @@ -52,6 +62,7 @@ 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))), _ => None, @@ -94,6 +105,10 @@ impl Components for KanaModel { self.input.pop(); None } + KanaMessage::Help => { + self.show_help_popup = !self.show_help_popup; + None + } }; } None @@ -106,19 +121,13 @@ impl Components for KanaModel { 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 [v_area] = Layout::vertical([Constraint::Length(7)]) + .flex(Flex::Center) + .areas(frame.area()); - let [_, main_area, _] = Layout::horizontal([ - Constraint::Min(0), - Constraint::Length(43), - Constraint::Min(0), - ]) - .areas(v_area); + let [main_area] = Layout::horizontal([Constraint::Length(43)]) + .flex(Flex::Center) + .areas(v_area); let left_title = Line::from(vec![ LEFT_TITLE.fg(ColorPalette::SUBTITLE).into(), @@ -162,5 +171,9 @@ impl KanaModel { frame.render_widget(Clear, main_area); frame.render_widget(p, main_area); + + if self.show_help_popup { + help_popup(HELP_STRINGS, 10, 30, frame); + } } } diff --git a/kanash-components/src/lib.rs b/kanash-components/src/lib.rs index 4264684..4e879c5 100644 --- a/kanash-components/src/lib.rs +++ b/kanash-components/src/lib.rs @@ -41,6 +41,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; From b5c2c5170cb882a3a894c61870a3d9e496bfed4c Mon Sep 17 00:00:00 2001 From: benoitlx Date: Fri, 20 Feb 2026 11:34:12 +0100 Subject: [PATCH 5/8] wip: stats with scrollbar --- kanash-components/src/app.rs | 8 ++++ kanash-components/src/helper/ja.rs | 8 ++-- kanash-components/src/helper/mod.rs | 5 +-- kanash-components/src/kana.rs | 59 ++++++++++++++++++++++++----- kanash-components/src/lib.rs | 2 +- 5 files changed, 63 insertions(+), 19 deletions(-) diff --git a/kanash-components/src/app.rs b/kanash-components/src/app.rs index b0970a4..5685402 100644 --- a/kanash-components/src/app.rs +++ b/kanash-components/src/app.rs @@ -53,6 +53,14 @@ impl Components for App { let mut new_kana = KanaModel::new(); new_kana.mode = mode; + 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); diff --git a/kanash-components/src/helper/ja.rs b/kanash-components/src/helper/ja.rs index ffde2f9..53bc858 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, diff --git a/kanash-components/src/helper/mod.rs b/kanash-components/src/helper/mod.rs index 66b32fc..c2da6f2 100644 --- a/kanash-components/src/helper/mod.rs +++ b/kanash-components/src/helper/mod.rs @@ -1,10 +1,7 @@ pub mod ja; pub mod rain; -use super::{ - Block, BorderType, Borders, Clear, Color, ColorPalette, Constraint, Flex, Frame, Layout, Line, - Paragraph, Span, Stylize, Text, Wrap, -}; +use super::*; pub fn help_popup(help_strings: [&str; 5], max_height: u16, max_width: u16, frame: &mut Frame) { let block = Block::new() diff --git a/kanash-components/src/kana.rs b/kanash-components/src/kana.rs index 40e72a5..e086f75 100644 --- a/kanash-components/src/kana.rs +++ b/kanash-components/src/kana.rs @@ -23,6 +23,7 @@ pub struct KanaModel { display_answer: bool, show_help_popup: bool, pub mode: Mode, + pub scroll_state: ScrollbarState, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -41,6 +42,9 @@ pub enum KanaMessage { /// Help Help, + + ScrollUp, + ScrollDown, } impl Components for KanaModel { @@ -54,6 +58,7 @@ impl Components for KanaModel { display_answer: false, show_help_popup: false, mode: Mode::Hira, + scroll_state: ScrollbarState::new(HIRAGANA_NUMBER), } } @@ -65,13 +70,15 @@ impl Components for KanaModel { 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); @@ -84,7 +91,6 @@ impl Components for KanaModel { self.shown += 1; return Some(Message::Kana(KanaMessage::Pass)); } - None } KanaMessage::Pass => { self.input = String::new(); @@ -93,21 +99,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; - None + } + KanaMessage::ScrollUp => { + self.scroll_state.prev(); + } + KanaMessage::ScrollDown => { + self.scroll_state.next(); } }; } @@ -125,9 +132,10 @@ impl KanaModel { .flex(Flex::Center) .areas(frame.area()); - let [main_area] = Layout::horizontal([Constraint::Length(43)]) - .flex(Flex::Center) - .areas(v_area); + let [main_area, stats_area] = + Layout::horizontal([Constraint::Length(43), Constraint::Length(10)]) + .flex(Flex::Center) + .areas(v_area); let left_title = Line::from(vec![ LEFT_TITLE.fg(ColorPalette::SUBTITLE).into(), @@ -157,6 +165,11 @@ impl KanaModel { .padding(Padding::vertical(1)) .borders(Borders::ALL); + let stats_block = Block::new() + .title("Stats") + .border_type(BorderType::Rounded) + .borders(Borders::ALL); + let text = vec![ Line::from(self.current_kana.clone()), if self.display_answer { @@ -169,9 +182,35 @@ impl KanaModel { let p = Paragraph::new(text).block(block).centered(); + let stat_text = match self.mode { + Mode::Hira => { + Text::from_iter(WANTED_HIRAGANA.map(|u| String::from_utf16(&[u]).expect("error"))) + } + Mode::Kata => { + Text::from_iter(WANTED_KATAKANA.map(|u| String::from_utf16(&[u]).expect("error"))) + } + Mode::Both => Text::from_iter( + WANTED_KATAKANA + .iter() + .chain(WANTED_HIRAGANA.iter()) + .map(|u| String::from_utf16(&[*u]).expect("error")), + ), + }; + + let stats = Paragraph::new(stat_text) + .block(stats_block) + .scroll((self.scroll_state.get_position() as u16, 0)); + frame.render_widget(Clear, main_area); frame.render_widget(p, main_area); + frame.render_widget(stats, stats_area); + frame.render_stateful_widget( + Scrollbar::new(ScrollbarOrientation::VerticalRight), + stats_area, + &mut self.scroll_state, + ); + if self.show_help_popup { help_popup(HELP_STRINGS, 10, 30, frame); } diff --git a/kanash-components/src/lib.rs b/kanash-components/src/lib.rs index 4e879c5..37d802c 100644 --- a/kanash-components/src/lib.rs +++ b/kanash-components/src/lib.rs @@ -12,7 +12,7 @@ pub use ratatui::{ text::{Line, Span, Text}, widgets::{ Block, BorderType, Borders, Clear, HighlightSpacing, List, ListItem, ListState, Padding, - Paragraph, Wrap, + Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap, }, Frame, }; From bf12481f16ca58aba3f476b964037980eafcf0e8 Mon Sep 17 00:00:00 2001 From: benoitlx Date: Fri, 20 Feb 2026 14:27:15 +0100 Subject: [PATCH 6/8] feat: per kana stats --- kanash-components/src/helper/ja.rs | 18 ++++++++--- kanash-components/src/kana.rs | 51 +++++++++++++++++++++--------- kanash-components/src/lib.rs | 12 +++---- 3 files changed, 54 insertions(+), 27 deletions(-) diff --git a/kanash-components/src/helper/ja.rs b/kanash-components/src/helper/ja.rs index 53bc858..e925dfa 100644 --- a/kanash-components/src/helper/ja.rs +++ b/kanash-components/src/helper/ja.rs @@ -18,15 +18,23 @@ pub 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/kana.rs b/kanash-components/src/kana.rs index e086f75..1fbdeca 100644 --- a/kanash-components/src/kana.rs +++ b/kanash-components/src/kana.rs @@ -3,6 +3,7 @@ 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 = " ? "; @@ -19,11 +20,13 @@ 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 + KATAKANA_NUMBER], + bad_cnts: [usize; HIRAGANA_NUMBER + KATAKANA_NUMBER], } #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -59,6 +62,8 @@ impl Components for KanaModel { show_help_popup: false, mode: Mode::Hira, scroll_state: ScrollbarState::new(HIRAGANA_NUMBER), + good_cnts: [0; HIRAGANA_NUMBER + KATAKANA_NUMBER], + bad_cnts: [0; HIRAGANA_NUMBER + KATAKANA_NUMBER], } } @@ -82,10 +87,12 @@ impl Components for KanaModel { KanaMessage::TypingRoma(c) => { self.input.push(c); - if self.input == self.current_kana.to_romaji() { + if self.input == self.current_kana.0.to_romaji() { 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; @@ -166,14 +173,14 @@ impl KanaModel { .borders(Borders::ALL); let stats_block = Block::new() - .title("Stats") + .title(Line::from(STATS_TITLE).fg(ColorPalette::TITLE).centered()) .border_type(BorderType::Rounded) .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() }, @@ -182,18 +189,29 @@ impl KanaModel { let p = Paragraph::new(text).block(block).centered(); + let stats_closure = |(i, u): (usize, &u16)| { + format!( + "{} {}/{}", + String::from_utf16(&[*u]).expect("error"), + self.good_cnts[i], + self.bad_cnts[i] + ) + }; + let stat_text = match self.mode { - Mode::Hira => { - Text::from_iter(WANTED_HIRAGANA.map(|u| String::from_utf16(&[u]).expect("error"))) - } - Mode::Kata => { - Text::from_iter(WANTED_KATAKANA.map(|u| String::from_utf16(&[u]).expect("error"))) - } - Mode::Both => Text::from_iter( + Mode::Hira => Text::from_iter(WANTED_HIRAGANA.iter().enumerate().map(stats_closure)), + Mode::Kata => Text::from_iter( WANTED_KATAKANA .iter() - .chain(WANTED_HIRAGANA.iter()) - .map(|u| String::from_utf16(&[*u]).expect("error")), + .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), ), }; @@ -204,9 +222,12 @@ impl KanaModel { frame.render_widget(Clear, main_area); frame.render_widget(p, main_area); + frame.render_widget(Clear, stats_area); frame.render_widget(stats, stats_area); frame.render_stateful_widget( - Scrollbar::new(ScrollbarOrientation::VerticalRight), + Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(Some("╮")) + .end_symbol(Some("╯")), stats_area, &mut self.scroll_state, ); diff --git a/kanash-components/src/lib.rs b/kanash-components/src/lib.rs index 37d802c..f028a96 100644 --- a/kanash-components/src/lib.rs +++ b/kanash-components/src/lib.rs @@ -7,12 +7,11 @@ pub mod kana; pub use ratatui::{ crossterm::event::{self, Event, KeyCode}, layout::{Constraint, Flex, Layout, Rect}, - style::Stylize, - style::{palette::tailwind::SLATE, Color, Modifier, Style}, + 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, + Bar, BarChart, Block, BorderType, Borders, Clear, HighlightSpacing, List, ListItem, + ListState, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap, }, Frame, }; @@ -23,12 +22,11 @@ pub type PlatformKeyEvent = ratatui::crossterm::event::KeyEvent; #[cfg(target_arch = "wasm32")] pub use ratzilla::ratatui::{ layout::{Constraint, Flex, Layout}, - style::Stylize, - style::{palette::tailwind::SLATE, Color, Modifier, Style}, + style::{palette::tailwind::SLATE, Color, Modifier, Style, Stylize}, text::{Line, Span, Text}, widgets::{ Block, BorderType, Borders, Clear, HighlightSpacing, List, ListItem, ListState, Padding, - Paragraph, Wrap, + Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap, }, Frame, }; From 55227f861376e28ecd79e8a82c3ffe29ce0a7838 Mon Sep 17 00:00:00 2001 From: benoitlx Date: Mon, 23 Feb 2026 14:22:23 +0100 Subject: [PATCH 7/8] refactor: kana view --- kanash-components/src/kana.rs | 47 +++++++++++++++++++---------------- kanash-components/src/lib.rs | 2 +- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/kanash-components/src/kana.rs b/kanash-components/src/kana.rs index 1fbdeca..c2dbf9b 100644 --- a/kanash-components/src/kana.rs +++ b/kanash-components/src/kana.rs @@ -129,21 +129,26 @@ impl Components for KanaModel { } fn view(&mut self, frame: &mut Frame, _elapsed: Duration) { - self.learning_zone(frame); - } -} - -impl KanaModel { - fn learning_zone(&mut self, frame: &mut Frame) { let [v_area] = Layout::vertical([Constraint::Length(7)]) .flex(Flex::Center) .areas(frame.area()); - let [main_area, stats_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, area: Rect) { let left_title = Line::from(vec![ LEFT_TITLE.fg(ColorPalette::SUBTITLE).into(), self.shown.to_string().yellow(), @@ -172,11 +177,6 @@ impl KanaModel { .padding(Padding::vertical(1)) .borders(Borders::ALL); - let stats_block = Block::new() - .title(Line::from(STATS_TITLE).fg(ColorPalette::TITLE).centered()) - .border_type(BorderType::Rounded) - .borders(Borders::ALL); - let text = vec![ Line::from(self.current_kana.0.clone()), if self.display_answer { @@ -189,6 +189,16 @@ impl KanaModel { let p = Paragraph::new(text).block(block).centered(); + 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!( "{} {}/{}", @@ -219,21 +229,14 @@ impl KanaModel { .block(stats_block) .scroll((self.scroll_state.get_position() as u16, 0)); - frame.render_widget(Clear, main_area); - frame.render_widget(p, main_area); - - frame.render_widget(Clear, stats_area); - frame.render_widget(stats, stats_area); + frame.render_widget(Clear, area); + frame.render_widget(stats, area); frame.render_stateful_widget( Scrollbar::new(ScrollbarOrientation::VerticalRight) .begin_symbol(Some("╮")) .end_symbol(Some("╯")), - stats_area, + area, &mut self.scroll_state, ); - - if self.show_help_popup { - help_popup(HELP_STRINGS, 10, 30, frame); - } } } diff --git a/kanash-components/src/lib.rs b/kanash-components/src/lib.rs index f028a96..14b39d7 100644 --- a/kanash-components/src/lib.rs +++ b/kanash-components/src/lib.rs @@ -21,7 +21,7 @@ pub type PlatformKeyEvent = ratatui::crossterm::event::KeyEvent; #[cfg(target_arch = "wasm32")] pub use ratzilla::ratatui::{ - layout::{Constraint, Flex, Layout}, + layout::{Constraint, Flex, Layout, Rect}, style::{palette::tailwind::SLATE, Color, Modifier, Style, Stylize}, text::{Line, Span, Text}, widgets::{ From 333425944a1858805cc49a14a7e60e8530ddbe84 Mon Sep 17 00:00:00 2001 From: benoitlx Date: Fri, 27 Feb 2026 15:00:45 +0100 Subject: [PATCH 8/8] wip: trying to fix ordering bug may be reverted --- kanash-components/src/kana.rs | 64 +++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/kanash-components/src/kana.rs b/kanash-components/src/kana.rs index c2dbf9b..e1fa76c 100644 --- a/kanash-components/src/kana.rs +++ b/kanash-components/src/kana.rs @@ -25,8 +25,9 @@ pub struct KanaModel { show_help_popup: bool, pub mode: Mode, pub scroll_state: ScrollbarState, - good_cnts: [usize; HIRAGANA_NUMBER + KATAKANA_NUMBER], - bad_cnts: [usize; HIRAGANA_NUMBER + KATAKANA_NUMBER], + good_cnts: [usize; HIRAGANA_NUMBER], + bad_cnts: [usize; HIRAGANA_NUMBER], + kana_permutation: Vec, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -62,8 +63,9 @@ impl Components for KanaModel { show_help_popup: false, mode: Mode::Hira, scroll_state: ScrollbarState::new(HIRAGANA_NUMBER), - good_cnts: [0; HIRAGANA_NUMBER + KATAKANA_NUMBER], - bad_cnts: [0; HIRAGANA_NUMBER + KATAKANA_NUMBER], + good_cnts: [0; HIRAGANA_NUMBER], + bad_cnts: [0; HIRAGANA_NUMBER], + kana_permutation: (0..(HIRAGANA_NUMBER)).collect(), } } @@ -88,6 +90,7 @@ impl Components for KanaModel { self.input.push(c); 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; @@ -199,31 +202,42 @@ impl KanaModel { .border_type(BorderType::Rounded) .borders(Borders::ALL); - let stats_closure = |(i, u): (usize, &u16)| { + // 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!( - "{} {}/{}", - String::from_utf16(&[*u]).expect("error"), - self.good_cnts[i], - self.bad_cnts[i] + "{i}{} {} {}", + String::from_utf16(&[WANTED_HIRAGANA[*i]]).expect("err"), + self.good_cnts[*i], + self.bad_cnts[*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 stat_text = Text::from_iter(self.kana_permutation.iter().map(stats_closure)); let stats = Paragraph::new(stat_text) .block(stats_block)