typeman 0.2.2

Typing speed test with practice mode in GUI, TUI and CLI
Documentation
use macroquad::prelude::*;
use miniquad::CursorIcon;
use miniquad::window::set_mouse_cursor;
use std::collections::VecDeque;
use std::thread;
use std::time::{Duration, Instant};

use crate::color_scheme::ColorScheme;
use crate::practice::{TYPING_LEVELS, check_if_completed};
use crate::ui::gui::config;
use crate::utils;

pub fn display_practice_menu(
    font: Option<Font>,
    scroll_offset: &mut f32,
    emoji_font: Font,
    selected_level: &mut Option<usize>,
    practice_menu: &mut bool,
    time_mode: &mut bool,
    pressed_vec: &mut Vec<char>,
    is_correct: &mut VecDeque<i32>,
    pos1: &mut usize,
    timer: &mut Duration,
    start_time: &mut Instant,
    game_started: &mut bool,
    game_over: &mut bool,
    speed_per_second: &mut Vec<f64>,
    last_recorded_time: &mut Instant,
    words_done: &mut usize,
    errors_per_second: &mut Vec<f64>,
    saved_results: &mut bool,
    error_positions: &mut Vec<bool>,
    color_scheme: &ColorScheme,
) -> Option<usize> {
    let font_size = if screen_width() > 3000.0 {
        20
    } else if screen_width() > 1900.0 {
        18
    } else {
        15
    };
    let tick_offset = if screen_width() > 1900.0 {
        (screen_width() - measure_text(TYPING_LEVELS[20].0, font.as_ref(), font_size, 1.0).width)
            / 2.0
            - 50.0
    } else {
        60.0
    };
    let mouse_pos = mouse_position();

    let (_, y_scroll) = mouse_wheel();
    *scroll_offset -= y_scroll * 60.0;

    let total_height = TYPING_LEVELS.len() as f32 * 60.0;
    let visible_height = screen_height() - 100.0;
    let max_scroll = f32::max(
        (TYPING_LEVELS.len() + 5) as f32 * (20.0 + font_size as f32) - screen_height(),
        0.0,
    );

    *scroll_offset = scroll_offset.clamp(0.0, max_scroll);

    if *scroll_offset < 20.0 {
        draw_text_ex(
            "Select Typing Level",
            tick_offset + 20.0,
            screen_height() / 10.0,
            TextParams {
                font: font.as_ref(),
                font_size: if screen_height() > 2000.0 && screen_width() > 1900.0 {
                    40
                } else if screen_height() > 1000.0 && screen_width() > 800.0 {
                    30
                } else {
                    25
                },
                color: color_scheme.main_color(),
                ..Default::default()
            },
        );
    }

    let start_index = 0;
    let end_index = TYPING_LEVELS.len();

    let mut any_hovered = false;
    let mut y: f32 = screen_height() / 10.0 + 2.0 * font_size as f32;
    for (i, (level_name, _)) in TYPING_LEVELS
        .iter()
        .enumerate()
        .take(end_index)
        .skip(start_index)
    {
        let mut text = format!("{}. {}", i + 1, level_name);
        if i + 1 < 10 {
            text = format!("{}.  {}", i + 1, level_name);
        }
        let text_size = measure_text(&text, font.as_ref(), font_size, 1.0);
        let button_rect = Rect::new(
            tick_offset + 40.0,
            y - *scroll_offset,
            text_size.width + 2.0 * font_size as f32,
            font_size as f32 + 20.0,
        );

        if button_rect.contains(vec2(mouse_pos.0, mouse_pos.1)) {
            any_hovered = true;
            break;
        }
        y += 20.0 + font_size as f32;
    }
    let mut y: f32 = screen_height() / 10.0 + 2.0 * font_size as f32;

    for (i, _) in TYPING_LEVELS
        .iter()
        .enumerate()
        .take(end_index)
        .skip(start_index)
    {
        let (level_name, _) = &TYPING_LEVELS[i];
        let mut text = format!("{}. {}", i + 1, level_name);
        if i + 1 < 10 {
            text = format!("{}.  {}", i + 1, level_name);
        }

        let text_size = measure_text(&text, font.as_ref(), font_size, 1.0);

        if y - *scroll_offset > screen_height() + 60.0 || y - *scroll_offset < 0.0 {
            y += 20.0 + font_size as f32;
            continue;
        }

        let button_rect = Rect::new(
            tick_offset + 40.0,
            y - *scroll_offset,
            text_size.width + 2.0 * font_size as f32,
            20.0 + font_size as f32,
        );

        let results_path = format!("practice_results/level_{}.txt", i + 1);
        let show_tick = check_if_completed(results_path.as_str());

        let is_hovered = if any_hovered {
            button_rect.contains(vec2(mouse_pos.0, mouse_pos.1))
        } else if let Some(selected) = selected_level {
            *selected == i
        } else {
            false
        };
        if is_hovered {
            *selected_level = Some(i);
        }

        set_mouse_cursor(if any_hovered {
            CursorIcon::Pointer
        } else {
            CursorIcon::Default
        });

        let is_clicked = is_hovered && any_hovered && is_mouse_button_pressed(MouseButton::Left);

        if is_clicked {
            *selected_level = Some(i);
            if let Some(level) = *selected_level {
                return Some(level);
            }
        }

        let text_color = if is_hovered {
            color_scheme.main_color()
        } else {
            color_scheme.text_color()
        };

        if show_tick {
            draw_text_ex(
                "✓",
                tick_offset,
                y + 1.8 * font_size as f32 - *scroll_offset,
                TextParams {
                    font: Some(&emoji_font),
                    font_size: 50,
                    color: Color::from_rgba(0, 255, 0, 255),
                    ..Default::default()
                },
            );
        }

        draw_text_ex(
            &text,
            60.0 + tick_offset,
            y + 1.2 * font_size as f32 - *scroll_offset,
            TextParams {
                font: font.as_ref(),
                font_size,
                color: text_color,
                ..Default::default()
            },
        );
        y += 20.0 + font_size as f32;
    }

    thread_local! {
        static DOWN_KEY_HELD_START: std::cell::RefCell<Option<Instant>> = const { std::cell::RefCell::new(None) };
        static LAST_DOWN_SCROLL: std::cell::RefCell<Option<Instant>> = const { std::cell::RefCell::new(None) };
        static UP_KEY_HELD_START: std::cell::RefCell<Option<Instant>> = const { std::cell::RefCell::new(None) };
        static LAST_UP_SCROLL: std::cell::RefCell<Option<Instant>> = const { std::cell::RefCell::new(None) };
    }

    let down_pressed = is_key_down(KeyCode::Down);
    let up_pressed = is_key_down(KeyCode::Up);

    DOWN_KEY_HELD_START.with(|down_start| {
        LAST_DOWN_SCROLL.with(|last_down| {
            UP_KEY_HELD_START.with(|up_start| {
                LAST_UP_SCROLL.with(|last_up| {
                    let mut down_key_held_start = *down_start.borrow();
                    let mut last_down_scroll = *last_down.borrow();
                    let mut up_key_held_start = *up_start.borrow();
                    let mut last_up_scroll = *last_up.borrow();

                    if down_pressed && !any_hovered {
                        let now = Instant::now();
                        if down_key_held_start.is_none() {
                            down_key_held_start = Some(now);
                            last_down_scroll = Some(now);
                            *scroll_offset =
                                (*scroll_offset + 20.0 + font_size as f32).min(max_scroll);
                            *selected_level = if let Some(level) = *selected_level {
                                Some((level + 1).min(TYPING_LEVELS.len() - 1))
                            } else {
                                Some(0)
                            };
                        } else if let (Some(start), Some(last)) =
                            (down_key_held_start, last_down_scroll)
                        {
                            let held_duration = now.duration_since(start).as_millis();
                            let interval = if held_duration < 500 {
                                200
                            } else if held_duration < 1500 {
                                80
                            } else {
                                30
                            };
                            if now.duration_since(last).as_millis() >= interval {
                                *scroll_offset =
                                    (*scroll_offset + 20.0 + font_size as f32).min(max_scroll);
                                *selected_level = if let Some(level) = *selected_level {
                                    Some((level + 1).min(TYPING_LEVELS.len() - 1))
                                } else {
                                    Some(0)
                                };
                                last_down_scroll = Some(now);
                            }
                        }
                    } else {
                        down_key_held_start = None;
                        last_down_scroll = None;
                    }

                    if up_pressed && !any_hovered {
                        let now = Instant::now();
                        if up_key_held_start.is_none() {
                            up_key_held_start = Some(now);
                            last_up_scroll = Some(now);
                            *scroll_offset = (*scroll_offset - 20.0 - font_size as f32).max(0.0);
                            *selected_level = if let Some(level) = *selected_level {
                                Some((level as isize - 1).max(0) as usize)
                            } else {
                                Some(TYPING_LEVELS.len() - 1)
                            };
                        } else if let (Some(start), Some(last)) =
                            (up_key_held_start, last_up_scroll)
                        {
                            let held_duration = now.duration_since(start).as_millis();
                            let interval = if held_duration < 500 {
                                200
                            } else if held_duration < 1500 {
                                80
                            } else {
                                30
                            };
                            if now.duration_since(last).as_millis() >= interval {
                                *scroll_offset =
                                    (*scroll_offset - 20.0 - font_size as f32).max(0.0);
                                *selected_level = if let Some(level) = *selected_level {
                                    Some((level as isize - 1).max(0) as usize)
                                } else {
                                    Some(TYPING_LEVELS.len() - 1)
                                };
                                last_up_scroll = Some(now);
                            }
                        }
                    } else {
                        up_key_held_start = None;
                        last_up_scroll = None;
                    }

                    *down_start.borrow_mut() = down_key_held_start;
                    *last_down.borrow_mut() = last_down_scroll;
                    *up_start.borrow_mut() = up_key_held_start;
                    *last_up.borrow_mut() = last_up_scroll;
                });
            });
        });
    });

    if is_key_pressed(KeyCode::Enter) && !is_key_down(KeyCode::Tab) {
        if let Some(level) = *selected_level {
            return Some(level);
        }
    }
    if is_key_pressed(KeyCode::Q) {
        if *practice_menu {
            *practice_menu = false;
            *time_mode = true;
            config::reset_game_state(
                pressed_vec,
                is_correct,
                pos1,
                timer,
                start_time,
                game_started,
                game_over,
                speed_per_second,
                last_recorded_time,
                words_done,
                errors_per_second,
                saved_results,
                error_positions,
            );
            thread::sleep(Duration::from_millis(200));
            pressed_vec.clear();
            return None;
        }
    } else {
        let _pressed = get_char_pressed();
    }

    if max_scroll > 0.0 {
        let scroll_area_height = screen_height() - 100.0;
        let thumb_height = f32::min(
            scroll_area_height * (visible_height / total_height),
            screen_height() - 200.0,
        );
        let thumb_position = if max_scroll > 0.0 {
            100.0 + (*scroll_offset / max_scroll) * (scroll_area_height - thumb_height)
        } else {
            100.0
        };

        utils::draw_rounded_rect(
            screen_width() - 20.0,
            thumb_position,
            10.0,
            thumb_height,
            5.0,
            Color::from_rgba(180, 180, 180, 220),
        );
    }

    None
}