full macros navigation

This commit is contained in:
2026-02-10 12:12:16 +03:00
parent 4868156ca7
commit fe75d7de23
4 changed files with 482 additions and 255 deletions
+208
View File
@@ -0,0 +1,208 @@
use rand::RngExt;
#[derive(Clone, Debug)]
pub struct KanaSet {
name: String,
pub(crate) chars_type: KanaType,
pub(crate) dictionary: Vec<Vec<(String, String)>>,
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<Self> for KanaSet {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
+46 -255
View File
@@ -1,11 +1,12 @@
mod lang; mod lang;
mod quiz;
mod selector;
use crate::Page::{Back, Quiz, Selector}; use crate::quiz::*;
use iced::Font; use crate::selector::*;
use iced::widget::{Column, Row, button, checkbox, column, container, row, text, text_input}; use crate::Page::{Quiz, Selector};
use iced::{Element, Fill, alignment}; use iced::widget::text;
use rand::RngExt; use iced::Element;
use crate::lang::*;
fn main() -> iced::Result { fn main() -> iced::Result {
iced::application("A kana learn app", ScreenState::update, ScreenState::view).run() iced::application("A kana learn app", ScreenState::update, ScreenState::view).run()
@@ -43,266 +44,56 @@ impl Default for ScreenState {
impl ScreenState { impl ScreenState {
pub fn update(&mut self, message: RootMessage) { pub fn update(&mut self, message: RootMessage) {
match message { state_update!(message, self.stack, Selector, Quiz);
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);
}
}
}
}
} }
pub fn view(&self) -> Element<'_, RootMessage> { pub fn view(&self) -> Element<'_, RootMessage> {
match &self.stack.last().unwrap() { view_navigation!(self.stack, Quiz, Selector)
Quiz(q) => q.view().map(RootMessage::Quiz), }
Selector(s) => s.view().map(RootMessage::Selector), }
#[macro_export]
macro_rules! view_navigation {
($stack:expr, $($e:ident), *) => {
match &$stack.last().unwrap() {
$(
$e(s) => s.view().map(RootMessage::$e),
)*
_ => text!("").into(), _ => text!("").into(),
} }
} }
} }
#[derive(Debug, Clone)]
pub enum QuizMessage { #[macro_export]
ContentChanged(String), macro_rules! state_update {
Back, ($message:expr, $stack:expr, $($e:ident), *) => {
match $message {
$(
RootMessage::$e(msg) => {
if let $e(s) = $stack.last_mut().unwrap() {
message_navigation!(msg, $stack, s)
}
}
)*
}
}
} }
#[macro_export]
macro_rules! message_navigation {
#[derive(Default, Debug, Clone)] ($msg:expr, $stack:expr, $state:expr) => {
pub struct Score { if let Some(new_page) = $state.navigate(&$msg) {
total: i32, if let Page::Back = new_page {
correct: i32, $stack.pop();
fail: i32, return;
}
$stack.push(new_page);
} else {
$state.update($msg);
}
};
} }
trait NavigatedPage<T> { trait NavigatedPage<T> {
fn navigate(&self, message: &T) -> Option<Page>; fn navigate(&self, message: &T) -> Option<Page>;
} }
#[derive(Clone, Debug)]
struct QuizState {
kana: String,
current_romanji: String,
correct_roman: String,
set: KanaSet,
score: Score,
is_help: bool,
}
impl NavigatedPage<QuizMessage> for QuizState {
fn navigate(&self, message: &QuizMessage) -> Option<Page> {
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<SelectorMessage> for SelectorState {
fn navigate(&self, message: &SelectorMessage) -> Option<Page> {
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()
}
}
+135
View File
@@ -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<QuizMessage> for QuizState {
fn navigate(&self, message: &QuizMessage) -> Option<Page> {
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,
}
+93
View File
@@ -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<SelectorMessage> for SelectorState {
fn navigate(&self, message: &SelectorMessage) -> Option<Page> {
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()
}
}