diff --git a/src/lang.rs b/src/lang.rs index e69de29..0eb1354 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -0,0 +1,208 @@ +use rand::RngExt; + +#[derive(Clone, Debug)] +pub struct KanaSet { + name: String, + pub(crate) chars_type: KanaType, + pub(crate) dictionary: Vec>, + pub(crate) include_map: [bool; 10], +} + +#[derive(Clone, Debug)] +pub enum KanaType { + Hiragana, + Katakana, +} + +impl KanaSet { + pub fn hiragana() -> Self { + Self { + name: "Хирагана".to_string(), + chars_type: KanaType::Hiragana, + dictionary: vec![ + vec![ + (String::from("ぁ"), String::from("a")), + (String::from("ぃ"), String::from("i")), + (String::from("ぅ"), String::from("u")), + (String::from("ぇ"), String::from("e")), + (String::from("ぉ"), String::from("o")), + ], + vec![ + (String::from("さ"), String::from("sa")), + (String::from("し"), String::from("shi")), + (String::from("す"), String::from("su")), + (String::from("せ"), String::from("se")), + (String::from("そ"), String::from("so")), + ], + vec![ + (String::from("か"), String::from("ka")), + (String::from("き"), String::from("ki")), + (String::from("く"), String::from("ku")), + (String::from("け"), String::from("ke")), + (String::from("こ"), String::from("ko")), + ], + // Ряд «та» (たちつてと) + vec![ + (String::from("た"), String::from("ta")), + (String::from("ち"), String::from("chi")), + (String::from("つ"), String::from("tsu")), + (String::from("て"), String::from("te")), + (String::from("と"), String::from("to")), + ], + // Ряд «на» (なにぬねの) + vec![ + (String::from("な"), String::from("na")), + (String::from("に"), String::from("ni")), + (String::from("ぬ"), String::from("nu")), + (String::from("ね"), String::from("ne")), + (String::from("の"), String::from("no")), + ], + // Ряд «ха» (はひふへほ) + vec![ + (String::from("は"), String::from("ha")), + (String::from("ひ"), String::from("hi")), + (String::from("ふ"), String::from("fu")), + (String::from("へ"), String::from("he")), + (String::from("ほ"), String::from("ho")), + ], + // Ряд «ма» (まみむめも) + vec![ + (String::from("ま"), String::from("ma")), + (String::from("み"), String::from("mi")), + (String::from("む"), String::from("mu")), + (String::from("め"), String::from("me")), + (String::from("も"), String::from("mo")), + ], + // Ряд «я» (やゆよ) — только 3 символа + vec![ + (String::from("や"), String::from("ya")), + (String::from("ゆ"), String::from("yu")), + (String::from("よ"), String::from("yo")), + ], + // Ряд «ра» (らりるれろ) + vec![ + (String::from("ら"), String::from("ra")), + (String::from("り"), String::from("ri")), + (String::from("る"), String::from("ru")), + (String::from("れ"), String::from("re")), + (String::from("ろ"), String::from("ro")), + ], + vec![ + (String::from("わ"), String::from("wa")), + (String::from("を"), String::from("wo")), + (String::from("ん"), String::from("n")), + ], + ], + include_map: [true; 10], + } + } + + pub fn katakana() -> Self { + Self { + name: "Катакана".to_string(), + chars_type: KanaType::Katakana, + dictionary: vec![ + vec![ + (String::from("ァ"), String::from("a")), + (String::from("ィ"), String::from("i")), + (String::from("ゥ"), String::from("u")), + (String::from("ェ"), String::from("e")), + (String::from("ォ"), String::from("o")), + ], + vec![ + (String::from("サ"), String::from("sa")), + (String::from("シ"), String::from("shi")), + (String::from("ス"), String::from("su")), + (String::from("セ"), String::from("se")), + (String::from("ソ"), String::from("so")), + ], + vec![ + (String::from("カ"), String::from("ka")), + (String::from("キ"), String::from("ki")), + (String::from("ク"), String::from("ku")), + (String::from("ケ"), String::from("ke")), + (String::from("コ"), String::from("ko")), + ], + // Ряд «та» (タチツテト) + vec![ + (String::from("タ"), String::from("ta")), + (String::from("チ"), String::from("chi")), + (String::from("ツ"), String::from("tsu")), + (String::from("テ"), String::from("te")), + (String::from("ト"), String::from("to")), + ], + // Ряд «на» (ナニヌネノ) + vec![ + (String::from("ナ"), String::from("na")), + (String::from("ニ"), String::from("ni")), + (String::from("ヌ"), String::from("nu")), + (String::from("ネ"), String::from("ne")), + (String::from("ノ"), String::from("no")), + ], + // Ряд «ха» (ハヒフヘホ) + vec![ + (String::from("ハ"), String::from("ha")), + (String::from("ヒ"), String::from("hi")), + (String::from("フ"), String::from("fu")), + (String::from("ヘ"), String::from("he")), + (String::from("ホ"), String::from("ho")), + ], + // Ряд «ма» (マミムメモ) + vec![ + (String::from("マ"), String::from("ma")), + (String::from("ミ"), String::from("mi")), + (String::from("ム"), String::from("mu")), + (String::from("メ"), String::from("me")), + (String::from("モ"), String::from("mo")), + ], + // Ряд «я» (ヤユヨ) — только 3 символа + vec![ + (String::from("ヤ"), String::from("ya")), + (String::from("ユ"), String::from("yu")), + (String::from("ヨ"), String::from("yo")), + ], + // Ряд «ра» (ラリルレロ) + vec![ + (String::from("ラ"), String::from("ra")), + (String::from("リ"), String::from("ri")), + (String::from("ル"), String::from("ru")), + (String::from("レ"), String::from("re")), + (String::from("ロ"), String::from("ro")), + ], + vec![ + (String::from("ワ"), String::from("wa")), + (String::from("ヲ"), String::from("wo")), + (String::from("ン"), String::from("n")), + ], + ], + include_map: [true; 10], + } + } + + pub fn next(&mut self) -> (String, String) { + let mut current_set: Vec<(String, String)> = Vec::new(); + + for i in 0..10 { + if self.include_map[i] { + self.dictionary[i].iter().for_each(|v| { + current_set.push(v.clone()); + }); + } + } + + let mut rand = rand::rng(); + let index: u32 = rand.random(); + current_set[index as usize % current_set.len()].clone() + } +} + +impl Default for KanaSet { + fn default() -> Self { + KanaSet::hiragana() + } +} +impl PartialEq for KanaSet { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} diff --git a/src/main.rs b/src/main.rs index fbe484a..5b1ddfd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,12 @@ mod lang; +mod quiz; +mod selector; -use crate::Page::{Back, Quiz, Selector}; -use iced::Font; -use iced::widget::{Column, Row, button, checkbox, column, container, row, text, text_input}; -use iced::{Element, Fill, alignment}; -use rand::RngExt; -use crate::lang::*; +use crate::quiz::*; +use crate::selector::*; +use crate::Page::{Quiz, Selector}; +use iced::widget::text; +use iced::Element; fn main() -> iced::Result { iced::application("A kana learn app", ScreenState::update, ScreenState::view).run() @@ -43,266 +44,56 @@ impl Default for ScreenState { impl ScreenState { pub fn update(&mut self, message: RootMessage) { - match message { - RootMessage::Selector(selector) => { - if let Selector(s) = self.stack.last_mut().unwrap() { - if let Some(new_page) = s.navigate(&selector) { - if let Page::Back = new_page { - self.stack.pop(); - return; - } - self.stack.push(new_page); - } else { - s.update(selector); - } - } - } - - RootMessage::Quiz(quiz) => { - if let Page::Quiz(q) = self.stack.last_mut().unwrap() { - if let Some(new_page) = q.navigate(&quiz) { - if let Page::Back = new_page { - self.stack.pop(); - return; - } - self.stack.push(new_page); - } else { - q.update(quiz); - } - } - } - } + state_update!(message, self.stack, Selector, Quiz); } pub fn view(&self) -> Element<'_, RootMessage> { - match &self.stack.last().unwrap() { - Quiz(q) => q.view().map(RootMessage::Quiz), - Selector(s) => s.view().map(RootMessage::Selector), + view_navigation!(self.stack, Quiz, Selector) + } +} + +#[macro_export] +macro_rules! view_navigation { + ($stack:expr, $($e:ident), *) => { + match &$stack.last().unwrap() { + $( + $e(s) => s.view().map(RootMessage::$e), + )* _ => text!("").into(), } } } -#[derive(Debug, Clone)] -pub enum QuizMessage { - ContentChanged(String), - Back, + +#[macro_export] +macro_rules! state_update { + ($message:expr, $stack:expr, $($e:ident), *) => { + match $message { + $( + RootMessage::$e(msg) => { + if let $e(s) = $stack.last_mut().unwrap() { + message_navigation!(msg, $stack, s) + } + } + )* + } + } } - - -#[derive(Default, Debug, Clone)] -pub struct Score { - total: i32, - correct: i32, - fail: i32, +#[macro_export] +macro_rules! message_navigation { + ($msg:expr, $stack:expr, $state:expr) => { + if let Some(new_page) = $state.navigate(&$msg) { + if let Page::Back = new_page { + $stack.pop(); + return; + } + $stack.push(new_page); + } else { + $state.update($msg); + } + }; } trait NavigatedPage { fn navigate(&self, message: &T) -> Option; } - -#[derive(Clone, Debug)] -struct QuizState { - kana: String, - current_romanji: String, - correct_roman: String, - set: KanaSet, - score: Score, - is_help: bool, -} - -impl NavigatedPage for QuizState { - fn navigate(&self, message: &QuizMessage) -> Option { - if let QuizMessage::Back = message { - Some(Back) - } else { - None - } - } -} - -impl QuizState { - fn new() -> QuizState { - QuizState { - kana: "ぁ".to_string(), - correct_roman: "a".to_string(), - is_help: false, - current_romanji: "".to_string(), - set: KanaSet::hiragana(), - score: Score { - total: 0, - correct: 0, - fail: 0, - }, - } - } -} - -impl Default for QuizState { - fn default() -> Self { - QuizState::new() - } -} - -impl QuizState { - pub fn update(&mut self, message: QuizMessage) { - match message { - QuizMessage::ContentChanged(content) => { - if content.contains("`") { - self.is_help = true; - self.score.fail += 1; - - return; - } - self.current_romanji = content; - println!("Content: {}", self.current_romanji); - if self.correct_roman == self.current_romanji { - if self.is_help == false { - self.score.correct += 1; - } - - self.is_help = false; - self.score.total += 1; - self.update_showed() - } - } - QuizMessage::Back => todo!(), - } - } - - fn update_showed(&mut self) { - self.current_romanji = String::new(); - let pair = self.set.next(); - self.kana = pair.0; - self.correct_roman = pair.1; - } - - pub fn view(&self) -> Element<'_, QuizMessage> { - container( - column![ - row![ - text!("{}", self.kana.to_uppercase()) - .size(48) - .font(Font::with_name("AppleGothic")), - text!( - "{}", - if self.is_help { - self.correct_roman.clone() - } else { - String::new() - } - ), - ] - .align_y(alignment::Vertical::Center) - .spacing(20), - text_input("Романдзи", &self.current_romanji) - .size(28) - .width(150) - .on_input(QuizMessage::ContentChanged), - row![ - text!("{}", self.score.total.to_string()).size(25), - text!("{}", self.score.correct.to_string()) - .size(25) - .color(iced::Color::from_rgb8(60, 170, 60)), - text!("{}", self.score.fail.to_string()) - .color(iced::Color::from_rgb8(255, 79, 0)) - .size(25), - ] - .spacing(10), - button("Закончить").on_press(QuizMessage::Back), - ] - .spacing(10) - .align_x(alignment::Horizontal::Center), - ) - .center_x(Fill) - .into() - } -} - -pub struct SelectorState { - pub set: KanaSet, -} - -impl Default for SelectorState { - fn default() -> Self { - Self { - set: KanaSet::hiragana(), - } - } -} - -#[derive(Debug, Clone)] -pub enum SelectorMessage { - Change, - Goto, - Check(usize, bool), -} - -impl NavigatedPage for SelectorState { - fn navigate(&self, message: &SelectorMessage) -> Option { - if let SelectorMessage::Goto = message { - let mut quiz = QuizState::new(); - quiz.set = self.set.clone(); - return Some(Quiz(quiz)); - } - None - } -} - -impl SelectorState { - pub fn update(&mut self, message: SelectorMessage) { - match message { - SelectorMessage::Change => match self.set.chars_type { - KanaType::Katakana => self.set = KanaSet::hiragana(), - KanaType::Hiragana => self.set = KanaSet::katakana(), - }, - SelectorMessage::Goto => {} - SelectorMessage::Check(i, b) => self.set.include_map[i] = b, - } - } - - pub fn view(&self) -> Element<'_, SelectorMessage> { - container( - column![ - button("Переключить").on_press(SelectorMessage::Change), - self.rows_selector(), - button("К тесту").on_press(SelectorMessage::Goto), - ] - .spacing(10), - ) - .padding(20) - .into() - } - - fn rows_selector(&self) -> Element<'_, SelectorMessage> { - let mut row = Row::new(); - - for i in 0..self.set.dictionary.len() { - let setup_checked = move |b: bool| -> SelectorMessage { SelectorMessage::Check(i, b) }; - - let mut chars_column: Column<'_, _> = Column::new(); - chars_column = - chars_column.push(checkbox("", self.set.include_map[i]).on_toggle(setup_checked)); - - for v in &self.set.dictionary[i] { - chars_column = chars_column.push( - container( - text!("{}", v.0.clone().to_uppercase()) - .size(36) - .font(Font::with_name("AppleGothic")), - ) - .padding(20) - .style(container::rounded_box), - ); - } - - row = row.push( - chars_column - .spacing(10) - .align_x(alignment::Horizontal::Center), - ); - } - - row.spacing(10).into() - } -} diff --git a/src/quiz.rs b/src/quiz.rs new file mode 100644 index 0000000..bcb5d0a --- /dev/null +++ b/src/quiz.rs @@ -0,0 +1,135 @@ +use crate::lang::KanaSet; +use crate::Page::Back; +use crate::{NavigatedPage, Page}; +use iced::widget::*; +use iced::{alignment, Element, Fill, Font}; + +#[derive(Clone, Debug)] +pub struct QuizState { + kana: String, + current_roman: String, + correct_roman: String, + pub(crate) set: KanaSet, + score: Score, + is_help: bool, +} + +impl NavigatedPage for QuizState { + fn navigate(&self, message: &QuizMessage) -> Option { + if let QuizMessage::Back = message { + Some(Back) + } else { + None + } + } +} + +impl QuizState { + pub(crate) fn new() -> QuizState { + QuizState { + kana: "ぁ".to_string(), + correct_roman: "a".to_string(), + is_help: false, + current_roman: "".to_string(), + set: KanaSet::hiragana(), + score: Score { + total: 0, + correct: 0, + fail: 0, + }, + } + } +} + +impl Default for QuizState { + fn default() -> Self { + QuizState::new() + } +} + +impl QuizState { + pub fn update(&mut self, message: QuizMessage) { + match message { + QuizMessage::ContentChanged(content) => { + if content.contains("`") { + self.is_help = true; + self.score.fail += 1; + + return; + } + self.current_roman = content; + println!("Content: {}", self.current_roman); + if self.correct_roman == self.current_roman { + if self.is_help == false { + self.score.correct += 1; + } + + self.is_help = false; + self.score.total += 1; + self.update_showed() + } + } + QuizMessage::Back => todo!(), + } + } + + fn update_showed(&mut self) { + self.current_roman = String::new(); + let pair = self.set.next(); + self.kana = pair.0; + self.correct_roman = pair.1; + } + + pub fn view(&self) -> Element<'_, QuizMessage> { + container( + iced::widget::column![ + row![ + text!("{}", self.kana.to_uppercase()) + .size(48) + .font(Font::with_name("AppleGothic")), + text!( + "{}", + if self.is_help { + self.correct_roman.clone() + } else { + String::new() + } + ), + ] + .align_y(alignment::Vertical::Center) + .spacing(20), + text_input("Романдзи", &self.current_roman) + .size(28) + .width(150) + .on_input(QuizMessage::ContentChanged), + row![ + text!("{}", self.score.total.to_string()).size(25), + text!("{}", self.score.correct.to_string()) + .size(25) + .color(iced::Color::from_rgb8(60, 170, 60)), + text!("{}", self.score.fail.to_string()) + .color(iced::Color::from_rgb8(255, 79, 0)) + .size(25), + ] + .spacing(10), + button("Закончить").on_press(QuizMessage::Back), + ] + .spacing(10) + .align_x(alignment::Horizontal::Center), + ) + .center_x(Fill) + .into() + } +} +#[derive(Debug, Clone)] +pub enum QuizMessage { + ContentChanged(String), + Back, +} + +#[derive(Default, Debug, Clone)] +pub struct Score { + total: i32, + correct: i32, + fail: i32, +} diff --git a/src/selector.rs b/src/selector.rs new file mode 100644 index 0000000..93cf9ac --- /dev/null +++ b/src/selector.rs @@ -0,0 +1,93 @@ +use crate::lang::{KanaSet, KanaType}; +use crate::Page::Quiz; +use crate::{NavigatedPage, Page, QuizState}; +use iced::widget::{button, checkbox, container, text, Column, Row}; +use iced::{alignment, Element, Font}; + +pub struct SelectorState { + pub set: KanaSet, +} + +impl Default for SelectorState { + fn default() -> Self { + Self { + set: KanaSet::hiragana(), + } + } +} + +#[derive(Debug, Clone)] +pub enum SelectorMessage { + Change, + Goto, + Check(usize, bool), +} + +impl NavigatedPage for SelectorState { + fn navigate(&self, message: &SelectorMessage) -> Option { + if let SelectorMessage::Goto = message { + let mut quiz = QuizState::new(); + quiz.set = self.set.clone(); + return Some(Quiz(quiz)); + } + None + } +} + +impl SelectorState { + pub fn update(&mut self, message: SelectorMessage) { + match message { + SelectorMessage::Change => match self.set.chars_type { + KanaType::Katakana => self.set = KanaSet::hiragana(), + KanaType::Hiragana => self.set = KanaSet::katakana(), + }, + SelectorMessage::Goto => {} + SelectorMessage::Check(i, b) => self.set.include_map[i] = b, + } + } + + pub fn view(&self) -> Element<'_, SelectorMessage> { + container( + iced::widget::column![ + button("Переключить").on_press(SelectorMessage::Change), + self.rows_selector(), + button("К тесту").on_press(SelectorMessage::Goto), + ] + .spacing(10), + ) + .padding(20) + .into() + } + + fn rows_selector(&self) -> Element<'_, SelectorMessage> { + let mut row = Row::new(); + + for i in 0..self.set.dictionary.len() { + let setup_checked = move |b: bool| -> SelectorMessage { SelectorMessage::Check(i, b) }; + + let mut chars_column: Column<'_, _> = Column::new(); + chars_column = + chars_column.push(checkbox("", self.set.include_map[i]).on_toggle(setup_checked)); + + for v in &self.set.dictionary[i] { + chars_column = chars_column.push( + container( + text!("{}", v.0.clone().to_uppercase()) + .size(36) + .font(Font::with_name("AppleGothic")), + ) + .padding(20) + .style(container::rounded_box), + ); + } + + row = row.push( + chars_column + .spacing(10) + .align_x(alignment::Horizontal::Center), + ); + } + + row.spacing(10).into() + } +}