Theme

alex/beta-minecraft-rust

Minecraft Beta 1.7.3 Rewrite

BRANCH main
main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
Download file

root / crates / client / src / menu / mod.rs

mod layout;
mod text_field;

use std::time::{Duration, Instant};

use chrono::Datelike;
use common::JavaRandom;
use common::item::Item;
use common::stats;
use server::save::catalog::Entry;
use winit::keyboard::KeyCode;

use crate::assets::packs::Pack;
use crate::settings::{Binding, Options, Setting, key_code};
pub use layout::contains;
pub use text_field::TextField;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Page {
    Title,
    Worlds,
    Create,
    Rename,
    Delete,
    Options,
    Video,
    Controls,
    Multiplayer,
    Packs,
    Pause,
    Achievements,
    Statistics,
    Loading,
    Saving,
    Error,
    Chat,
    /// `GuiSleepMP`: chat while lying in bed on a server.
    Sleep,
    /// `GuiEditSign`.
    Sign,
}

/// The sign being written by `GuiEditSign`.
pub struct SignEditor {
    pub sign: crate::interaction::PlacedSign,
    pub lines: [String; 4],
    pub line: usize,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Action {
    Open(Page),
    Back,
    Quit,
    Play,
    Create,
    Rename,
    Delete,
    Connect,
    Resume,
    Leave,
    Setting(Setting),
    Bind(Binding),
    Pack(usize),
    OpenPackFolder,
    SendChat,
    FinishSign,
    LeaveBed,
    StatsTab(u8),
    StatsSort(usize),
}

pub struct Menu {
    pub page: Option<Page>,
    pub worlds: Vec<Entry>,
    pub selected: Option<usize>,
    pub packs: Vec<Pack>,
    pub fields: [TextField; 2],
    pub folder: String,
    pub splash: String,
    splashes: Vec<String>,
    splash_rand: JavaRandom,
    pub message: String,
    pub progress: f32,
    pub statistics: crate::stats::Shared,
    pub stats_tab: u8,
    pub stats_sort: Option<(usize, bool)>,
    pub achievement_scroll: [f32; 2],
    achievement_drag: Option<[f32; 2]>,
    pub loading_title: &'static str,
    pub error_title: String,
    pub scroll: f32,
    pub binding: Option<Binding>,
    pub remote: bool,
    pub in_world: bool,
    pub opened: Instant,
    pub keyboard_focus: Option<usize>,
    pub sign: Option<SignEditor>,
    pub options_parent: Page,
    pub error_parent: Page,
    /// Whether the GPU supports descriptor heaps, so the Video Settings can switch to them.
    pub descriptor_heaps: bool,
    /// Whether the GPU and window system support frame pacing.
    pub frame_pacing: bool,
    /// The present modes the Video Settings cycle through.
    pub present_modes: Vec<crate::gpu::PresentMode>,
    slider: Option<(Setting, [f32; 4])>,
    drag: Option<(f32, f32)>,
    last_click: Option<(usize, Instant)>,
}

impl Menu {
    /// A fresh `GuiMainMenu`: a random splash, unless a holiday has its own.
    pub fn choose_splash(&mut self) {
        let now = chrono::Local::now();
        self.splash = match (now.month(), now.day()) {
            (11, 9) => "Happy birthday, ez!".into(),
            (6, 1) => "Happy birthday, Notch!".into(),
            (12, 24) => "Merry X-mas!".into(),
            (1, 1) => "Happy new year!".into(),
            _ if self.splashes.is_empty() => "missingno".into(),
            _ => {
                let index = self.splash_rand.next_int(self.splashes.len() as i32);
                self.splashes[index as usize].clone()
            }
        };
    }

    pub fn new(splashes: &str, seed: i64) -> Self {
        let splashes = splashes.lines().map(str::trim).filter(|s| !s.is_empty());
        let mut fields = [TextField::new("", 32), TextField::new("", 128)];
        for field in &mut fields {
            field.focused = false;
        }
        let mut menu = Self {
            page: Some(Page::Title),
            worlds: Vec::new(),
            selected: None,
            packs: Vec::new(),
            fields,
            folder: String::new(),
            splash: String::new(),
            splashes: splashes.map(str::to_owned).collect(),
            splash_rand: JavaRandom::new(seed),
            message: String::new(),
            progress: 0.0,
            statistics: Default::default(),
            stats_tab: 0,
            stats_sort: None,
            achievement_scroll: [-82.0, -70.0],
            achievement_drag: None,
            loading_title: "Generating level",
            error_title: String::new(),
            scroll: 0.0,
            binding: None,
            remote: false,
            in_world: false,
            opened: Instant::now(),
            keyboard_focus: None,
            sign: None,
            options_parent: Page::Title,
            error_parent: Page::Title,
            descriptor_heaps: false,
            frame_pacing: false,
            present_modes: Vec::new(),
            slider: None,
            drag: None,
            last_click: None,
        };
        menu.choose_splash();
        menu
    }

    pub fn open(&mut self, page: Page, options: &Options) {
        self.page = Some(page);
        self.scroll = 0.0;
        self.achievement_drag = None;
        self.slider = None;
        self.drag = None;
        self.last_click = None;
        self.binding = None;
        self.keyboard_focus = None;
        self.opened = Instant::now();
        self.fields
            .iter_mut()
            .for_each(|field| field.focused = false);
        match page {
            Page::Options if self.in_world => self.options_parent = Page::Pause,
            Page::Options => self.options_parent = Page::Title,
            Page::Statistics => (self.stats_tab, self.stats_sort) = (0, None),
            Page::Achievements => self.achievement_scroll = [-82.0, -70.0],
            Page::Create => {
                self.fields = [
                    TextField::new("New World", 32),
                    TextField::new("", usize::MAX),
                ]
            }
            Page::Rename => {
                if let Some(world) = self.selected_world() {
                    self.fields[0] = TextField::new(&world.name, 32);
                }
            }
            Page::Multiplayer => self.fields[0] = TextField::new(&options.last_server, 128),
            Page::Chat | Page::Sleep => self.fields[0] = TextField::chat(100),
            _ => {}
        }
        self.fields[1].focused = false;
    }

    pub fn selected_world(&self) -> Option<&Entry> {
        self.selected.and_then(|index| self.worlds.get(index))
    }

    pub fn fail(&mut self, title: &str, message: impl ToString, parent: Page, options: &Options) {
        self.open(Page::Error, options);
        self.error_title = title.into();
        self.message = message.to_string();
        self.error_parent = parent;
    }

    pub fn back(&self) -> Action {
        match self.page {
            Some(Page::Statistics) => Action::Open(Page::Pause),
            Some(Page::Achievements) => Action::Resume,
            Some(Page::Options) => Action::Open(self.options_parent),
            Some(Page::Video | Page::Controls) => Action::Open(Page::Options),
            Some(Page::Create | Page::Rename | Page::Delete) => Action::Open(Page::Worlds),
            Some(Page::Pause | Page::Chat) => Action::Resume,
            Some(Page::Sleep) => Action::LeaveBed,
            Some(Page::Loading) => Action::Leave,
            Some(Page::Error) => Action::Open(self.error_parent),
            _ => Action::Open(Page::Title),
        }
    }

    pub fn list_len(&self) -> usize {
        match self.page {
            Some(Page::Worlds) => self.worlds.len(),
            Some(Page::Packs) => self.packs.len(),
            Some(Page::Statistics) if self.stats_tab == 0 => stats::GENERAL.len(),
            Some(Page::Statistics) => self.stat_items().len(),
            _ => 0,
        }
    }

    pub fn list_bottom(&self, size: [f32; 2]) -> f32 {
        size[1]
            - if self.page == Some(Page::Packs) {
                51.0
            } else {
                64.0
            }
    }

    /// The row height of the list, the height of its header and the distance a wheel step scrolls.
    fn rows(&self) -> (f32, f32, f32) {
        match (self.page, self.stats_tab) {
            (Some(Page::Statistics), 0) => (10.0, 0.0, 5.0),
            (Some(Page::Statistics), _) => (20.0, 20.0, 10.0),
            _ => (36.0, 0.0, 18.0),
        }
    }

    pub fn max_scroll(&self, size: [f32; 2]) -> f32 {
        let (row, header, _) = self.rows();
        (self.list_len() as f32 * row + header - (self.list_bottom(size) - 36.0)).max(0.0)
    }

    /// The height of the scroll bar's thumb in the world and pack lists.
    pub fn scroll_thumb(&self, size: [f32; 2]) -> f32 {
        let height = self.list_bottom(size) - 32.0;
        (height * height / (self.list_len() as f32 * 36.0)).clamp(32.0, height - 8.0)
    }

    pub fn scroll(&mut self, delta: f32, size: [f32; 2]) {
        let amount = self.rows().2;
        self.scroll = (self.scroll - delta * amount).clamp(0.0, self.max_scroll(size));
    }

    pub fn mouse_move(&mut self, cursor: [f32; 2], size: [f32; 2], options: &mut Options) -> bool {
        self.keyboard_focus = None;
        if let Some(previous) = &mut self.achievement_drag {
            self.achievement_scroll[0] =
                (self.achievement_scroll[0] - (cursor[0] - previous[0])).clamp(-136.0, 114.0);
            self.achievement_scroll[1] =
                (self.achievement_scroll[1] - (cursor[1] - previous[1])).clamp(-232.0, 66.0);
            *previous = cursor;
        }
        if let Some((setting, rect)) = self.slider {
            setting.set_slider(options, (cursor[0] - rect[0] - 4.0) / (rect[2] - 8.0));
            return true;
        }
        if let Some((y, multiplier)) = &mut self.drag {
            self.scroll -= (cursor[1] - *y) * *multiplier;
            *y = cursor[1];
            self.scroll = self.scroll.clamp(0.0, self.max_scroll(size));
        }
        false
    }

    pub fn mouse_up(&mut self) -> bool {
        self.drag = None;
        self.achievement_drag = None;
        self.slider.take().is_some()
    }

    pub fn click(
        &mut self,
        cursor: [f32; 2],
        size: [f32; 2],
        options: &mut Options,
    ) -> Option<Action> {
        if self.binding.is_some() {
            return None;
        }
        let map = [
            (size[0] - 256.0) / 2.0 + 8.0,
            (size[1] - 202.0) / 2.0 + 17.0,
            224.0,
            155.0,
        ];
        if self.page == Some(Page::Achievements) && contains(map, cursor) {
            self.achievement_drag = Some(cursor);
        }
        let in_list = (32.0..self.list_bottom(size)).contains(&cursor[1]);
        if self.page == Some(Page::Statistics) && in_list {
            let top = 36.0 - self.scroll;
            if self.stats_tab != 0
                && (top..56.0 - self.scroll).contains(&cursor[1])
                && let Some(column) = (0..3).find(|&column| {
                    contains(
                        [size[0] / 2.0 - 31.0 + column as f32 * 50.0, top, 36.0, 20.0],
                        cursor,
                    )
                })
            {
                return Some(Action::StatsSort(column));
            }
            self.drag = Some((cursor[1], 1.0));
        }
        for button in self.buttons(size, options) {
            if button.enabled && contains(button.rect, cursor) {
                if let Action::Setting(setting) = button.action
                    && setting.slider(options).is_some()
                {
                    self.slider = Some((setting, button.rect));
                    self.mouse_move(cursor, size, options);
                }
                return Some(button.action);
            }
        }
        let fields = self.field_rects(size);
        for (field, rect) in self.fields.iter_mut().zip(fields) {
            field.focused = rect.is_some_and(|rect| contains(rect, cursor));
        }
        if matches!(self.page, Some(Page::Worlds | Page::Packs)) && in_list {
            let max = self.max_scroll(size);
            let multiplier = if (size[0] / 2.0 + 124.0..size[0] / 2.0 + 130.0).contains(&cursor[0])
                && max > 0.0
            {
                -max / (self.list_bottom(size) - 32.0 - self.scroll_thumb(size))
            } else {
                1.0
            };
            self.drag = Some((cursor[1], multiplier));
            let y = cursor[1] - 36.0 + self.scroll;
            let index = (y / 36.0) as usize;
            if (size[0] / 2.0 - 110.0..size[0] / 2.0 + 110.0).contains(&cursor[0])
                && y >= 0.0
                && index < self.list_len()
            {
                if self.page == Some(Page::Packs) {
                    return Some(Action::Pack(index));
                }
                let double = self.last_click.is_some_and(|(old, time)| {
                    old == index && time.elapsed() < Duration::from_millis(250)
                });
                self.selected = Some(index);
                self.last_click = Some((index, Instant::now()));
                if double {
                    return Some(Action::Play);
                }
            }
        }
        None
    }

    pub fn key(
        &mut self,
        code: KeyCode,
        text: Option<&str>,
        control: bool,
        shift: bool,
        size: [f32; 2],
        options: &mut Options,
    ) -> Option<Action> {
        if self.page == Some(Page::Achievements) && options.matches(Binding::Inventory, code) {
            return Some(Action::Resume);
        }
        // `GuiEditSign.keyTyped`, which leaves out escaping the screen
        if self.page == Some(Page::Sign)
            && let Some(editor) = &mut self.sign
        {
            let line = &mut editor.lines[editor.line];
            match code {
                KeyCode::ArrowUp => editor.line = (editor.line + 3) & 3,
                KeyCode::ArrowDown | KeyCode::Enter | KeyCode::NumpadEnter => {
                    editor.line = (editor.line + 1) & 3
                }
                KeyCode::Backspace => {
                    line.pop();
                }
                _ => {
                    for ch in text.unwrap_or_default().chars() {
                        if common::text::ALLOWED_CHARACTERS.contains(ch)
                            && line.chars().count() < 15
                        {
                            line.push(ch);
                        }
                    }
                }
            }
            return None;
        }
        if let Some(binding) = self.binding {
            if let Some(key) = key_code(code) {
                options.keys[binding as usize] = key;
                self.binding = None;
            }
            return None;
        }
        if code == KeyCode::Escape {
            // `GuiScreen.keyTyped` closes these settings screens and resumes the game
            if self.in_world
                && matches!(
                    self.page,
                    Some(
                        Page::Options
                            | Page::Video
                            | Page::Controls
                            | Page::Statistics
                            | Page::Achievements
                    )
                )
            {
                return Some(Action::Resume);
            }
            return (!matches!(self.page, Some(Page::Title | Page::Saving)))
                .then_some(Action::Back);
        }
        if code == KeyCode::Tab {
            let fields = self.field_rects(size);
            if fields[0].is_some() {
                if fields[1].is_some() {
                    let focused = self.fields[0].focused;
                    self.fields[0].focused = !focused;
                    self.fields[1].focused = focused;
                } else {
                    self.fields[0].focused = true;
                }
            } else {
                let buttons = self.buttons(size, options);
                let indices: Vec<_> = (0..buttons.len()).filter(|&i| buttons[i].enabled).collect();
                if !indices.is_empty() {
                    let len = indices.len();
                    let old = self
                        .keyboard_focus
                        .and_then(|focus| indices.iter().position(|index| *index == focus));
                    let next = match old {
                        None if shift => len - 1,
                        None => 0,
                        Some(old) if shift => (old + len - 1) % len,
                        Some(old) => (old + 1) % len,
                    };
                    self.keyboard_focus = Some(indices[next]);
                }
            }
            return None;
        }
        if matches!(code, KeyCode::Enter | KeyCode::NumpadEnter) {
            let buttons = self.buttons(size, options);
            let index = self.keyboard_focus.unwrap_or(0);
            return buttons
                .get(index)
                .filter(|button| button.enabled)
                .map(|button| button.action);
        }
        if self.page == Some(Page::Worlds)
            && matches!(code, KeyCode::ArrowUp | KeyCode::ArrowDown)
            && !self.worlds.is_empty()
        {
            self.selected = Some(match self.selected {
                None => 0,
                Some(index) if code == KeyCode::ArrowUp => index.saturating_sub(1),
                Some(index) => (index + 1).min(self.worlds.len() - 1),
            });
            let y = self.selected.unwrap() as f32 * 36.0;
            self.scroll = self
                .scroll
                .min(y)
                .max(y + 36.0 - (self.list_bottom(size) - 36.0))
                .clamp(0.0, self.max_scroll(size));
        }
        for field in &mut self.fields {
            if field.focused {
                field.key(code, text, control, shift);
            }
        }
        None
    }
}

impl Menu {
    /// The statistic bases of the columns of the blocks (1) or items (2) tab.
    pub fn stat_columns(tab: u8) -> [i32; 3] {
        if tab == 1 {
            [stats::CRAFT, stats::USE, stats::MINE]
        } else {
            [stats::BREAK, stats::CRAFT, stats::USE]
        }
    }

    pub fn stat_items(&self) -> Vec<Item> {
        self.stat_items_for(self.stats_tab)
    }

    pub fn stat_items_for(&self, tab: u8) -> Vec<Item> {
        let stats = self.statistics.lock().unwrap();
        let columns = Self::stat_columns(tab);
        let mut items: Vec<_> = (1..=2257)
            .map(Item)
            .filter(|&item| {
                let id = i32::from(item.0);
                item.properties().is_some()
                    && if tab == 1 {
                        item.0 < 256 && id == stats::item_id(item)
                    } else {
                        item.0 >= 256
                    }
                    && columns.iter().any(|base| stats.get(base + id) > 0)
            })
            .collect();
        if let Some((column, descending)) = self.stats_sort {
            let base = columns[column];
            // `SorterStatsItem`: unavailable statistics stay last in either direction
            let stat = |item: Item| {
                let id = base + stats::item_id(item);
                stats.guids.contains_key(&id).then(|| stats.get(id))
            };
            items.sort_unstable_by(|a, b| {
                let order = match (stat(*a), stat(*b)) {
                    (Some(a), Some(b)) if descending => b.cmp(&a),
                    (Some(a), Some(b)) => a.cmp(&b),
                    (a, b) => a.is_none().cmp(&b.is_none()),
                };
                order.then_with(|| a.0.cmp(&b.0))
            });
        }
        items
    }
}