kr580 2.0.0

Desktop KR580VM80 / Intel 8080 emulator.
Documentation
use iced::Task;
use iced::keyboard;

use super::messages::{MenuId, Message, TopMenuFocus, TopMenuIndicator};
use super::state::DesktopApp;

const TOP_MENUS: [MenuId; 5] = [
    MenuId::File,
    MenuId::Mp,
    MenuId::View,
    MenuId::Settings,
    MenuId::Help,
];
const FILE_MENU_ACTIONS: &[Message] = &[
    Message::NewFile,
    Message::OpenSnapshot,
    Message::SaveSnapshot,
    Message::SaveSnapshotAs,
    Message::Import,
    Message::Export,
];
const MP_MENU_ACTIONS: &[Message] = &[
    Message::ToggleRun,
    Message::StepInstruction,
    Message::StepTact,
    Message::ResetRam,
    Message::ResetCpu,
    Message::ClearHalt,
];
const VIEW_MENU_ACTIONS: &[Message] = &[
    Message::OpenMonitor,
    Message::OpenFloppy,
    Message::OpenHdd,
    Message::OpenNetwork,
    Message::OpenPrinter,
    Message::ToggleStackView,
];
const HELP_MENU_ACTIONS: &[Message] = &[Message::OpenHelp, Message::OpenAbout];

pub(super) fn navigation_key_message(
    key: &keyboard::Key,
    modifiers: keyboard::Modifiers,
) -> Option<Message> {
    match key {
        keyboard::Key::Named(keyboard::key::Named::Tab) => Some(Message::FocusCycle {
            backward: modifiers.shift(),
        }),
        keyboard::Key::Named(keyboard::key::Named::ArrowUp) => Some(Message::ArrowKey(1)),
        keyboard::Key::Named(keyboard::key::Named::ArrowDown) => Some(Message::ArrowKey(-1)),
        keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
            Some(Message::HorizontalArrowKey(-1))
        }
        keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
            Some(Message::HorizontalArrowKey(1))
        }
        keyboard::Key::Named(keyboard::key::Named::Enter) => Some(Message::EnterPressed),
        _ => None,
    }
}

impl DesktopApp {
    pub(crate) fn route_open_menu_message(&mut self, message: &Message) -> Option<Task<Message>> {
        if self.open_menu.is_none() && self.top_menu_focus.is_none() {
            return None;
        }
        match message {
            Message::ArrowKey(direction) => {
                self.move_top_menu_item(*direction);
                Some(Task::none())
            }
            Message::HorizontalArrowKey(direction) => {
                self.move_top_menu_category(*direction);
                Some(Task::none())
            }
            Message::FocusCycle { backward } => {
                self.cycle_top_menu_focus(*backward);
                Some(Task::none())
            }
            Message::EnterPressed => Some(self.activate_top_menu_focus()),
            Message::MousePressed | Message::MousePressedIgnored => {
                if self.open_menu.is_some() {
                    self.top_menu_indicator = TopMenuIndicator::Hidden;
                } else {
                    self.close_top_menu();
                }
                None
            }
            _ => None,
        }
    }

    pub(crate) fn toggle_top_menu(&mut self, menu: MenuId) {
        if self.open_menu == Some(menu) {
            self.close_top_menu();
        } else {
            self.set_top_menu_focus(TopMenuFocus::Category(menu), TopMenuIndicator::Hidden);
        }
    }

    pub(crate) fn hover_top_menu(&mut self, menu: MenuId) {
        if self.open_menu.is_some() && self.open_menu != Some(menu) && menu != MenuId::Settings {
            self.set_top_menu_focus(TopMenuFocus::Category(menu), TopMenuIndicator::Hidden);
        }
    }

    pub(crate) fn close_top_menu(&mut self) {
        self.open_menu = None;
        self.top_menu_focus = None;
        self.top_menu_indicator = TopMenuIndicator::Hidden;
    }

    fn move_top_menu_item(&mut self, direction: i32) {
        let Some(menu) = self.open_menu else {
            return;
        };
        let count = top_menu_item_count(menu, self.snapshot.cpu.halted);
        let current = match self.top_menu_focus {
            Some(TopMenuFocus::Item {
                menu: focused_menu,
                index,
            }) if focused_menu == menu && index < count => Some(index),
            _ => None,
        };
        let index = if direction > 0 {
            current.map_or(count - 1, |index| (index + count - 1) % count)
        } else {
            current.map_or(0, |index| (index + 1) % count)
        };
        self.set_top_menu_focus(
            TopMenuFocus::Item { menu, index },
            TopMenuIndicator::ArrowFill,
        );
    }

    fn move_top_menu_category(&mut self, direction: i32) {
        let Some(menu) = self.top_menu_focus.map(|focus| match focus {
            TopMenuFocus::Category(menu) | TopMenuFocus::Item { menu, .. } => menu,
        }) else {
            return;
        };
        let menu = adjacent_dropdown_menu(menu, direction < 0);
        self.set_top_menu_focus(TopMenuFocus::Category(menu), TopMenuIndicator::Hidden);
    }

    fn cycle_top_menu_focus(&mut self, backward: bool) {
        let current = match self.top_menu_focus {
            Some(TopMenuFocus::Category(menu)) => TopMenuFocus::Category(menu),
            Some(TopMenuFocus::Item { menu, index })
                if index < top_menu_item_count(menu, self.snapshot.cpu.halted) =>
            {
                TopMenuFocus::Item { menu, index }
            }
            _ => return,
        };
        let next = match current {
            TopMenuFocus::Category(menu) if backward => {
                let menu = adjacent_top_menu(menu, true);
                match top_menu_item_count(menu, self.snapshot.cpu.halted) {
                    0 => TopMenuFocus::Category(menu),
                    count => TopMenuFocus::Item {
                        menu,
                        index: count - 1,
                    },
                }
            }
            TopMenuFocus::Category(menu) => {
                if top_menu_item_count(menu, self.snapshot.cpu.halted) == 0 {
                    TopMenuFocus::Category(adjacent_top_menu(menu, false))
                } else {
                    TopMenuFocus::Item { menu, index: 0 }
                }
            }
            TopMenuFocus::Item { menu, index } if backward && index == 0 => {
                TopMenuFocus::Category(menu)
            }
            TopMenuFocus::Item { menu, index } if backward => TopMenuFocus::Item {
                menu,
                index: index - 1,
            },
            TopMenuFocus::Item { menu, index }
                if index + 1 < top_menu_item_count(menu, self.snapshot.cpu.halted) =>
            {
                TopMenuFocus::Item {
                    menu,
                    index: index + 1,
                }
            }
            TopMenuFocus::Item { menu, .. } => {
                TopMenuFocus::Category(adjacent_top_menu(menu, false))
            }
        };
        self.set_top_menu_focus(next, TopMenuIndicator::TabRing);
    }

    fn set_top_menu_focus(&mut self, focus: TopMenuFocus, indicator: TopMenuIndicator) {
        let menu = match focus {
            TopMenuFocus::Category(menu) | TopMenuFocus::Item { menu, .. } => menu,
        };
        self.open_menu = (menu != MenuId::Settings).then_some(menu);
        self.top_menu_focus = Some(focus);
        self.top_menu_indicator = indicator;
    }

    fn activate_top_menu_focus(&mut self) -> Task<Message> {
        let (menu, index) = match self.top_menu_focus {
            Some(TopMenuFocus::Category(MenuId::Settings)) => {
                self.close_top_menu();
                return Task::done(Message::OpenSettings);
            }
            Some(TopMenuFocus::Item { menu, index }) => (menu, index),
            _ => return Task::none(),
        };
        if index >= top_menu_item_count(menu, self.snapshot.cpu.halted) {
            return Task::none();
        }
        let action = top_menu_action(menu, index).expect("focused menu item must have an action");
        self.close_top_menu();
        Task::done(action)
    }
}

fn adjacent_top_menu(menu: MenuId, backward: bool) -> MenuId {
    let index = TOP_MENUS
        .iter()
        .position(|candidate| *candidate == menu)
        .unwrap_or(0);
    let next = if backward {
        (index + TOP_MENUS.len() - 1) % TOP_MENUS.len()
    } else {
        (index + 1) % TOP_MENUS.len()
    };
    TOP_MENUS[next]
}

fn adjacent_dropdown_menu(menu: MenuId, backward: bool) -> MenuId {
    let menu = adjacent_top_menu(menu, backward);
    if menu == MenuId::Settings {
        adjacent_top_menu(menu, backward)
    } else {
        menu
    }
}

fn top_menu_item_count(menu: MenuId, halted: bool) -> usize {
    let count = top_menu_actions(menu).len();
    if menu == MenuId::Mp && !halted {
        count - 1
    } else {
        count
    }
}

fn top_menu_actions(menu: MenuId) -> &'static [Message] {
    match menu {
        MenuId::File => FILE_MENU_ACTIONS,
        MenuId::Mp => MP_MENU_ACTIONS,
        MenuId::View => VIEW_MENU_ACTIONS,
        MenuId::Settings => &[],
        MenuId::Help => HELP_MENU_ACTIONS,
    }
}

pub(crate) fn top_menu_action(menu: MenuId, index: usize) -> Option<Message> {
    top_menu_actions(menu).get(index).cloned()
}

#[cfg(test)]
#[path = "menu_keyboard/tests.rs"]
mod tests;