kr580 2.0.0

Desktop KR580VM80 / Intel 8080 emulator.
Documentation
use iced::Task;
use std::path::PathBuf;

use super::{DesktopApp, Message};

#[derive(Clone, Debug)]
pub(crate) enum PendingAction {
    OpenSnapshot,
    OpenDroppedFile(PathBuf),
    NewFile,
    Import,
    CloseWindow,
    DeleteHdd,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum DiscardModalButton {
    Cancel,
    Confirm,
}

impl DiscardModalButton {
    fn next(self) -> Self {
        match self {
            Self::Cancel => Self::Confirm,
            Self::Confirm => Self::Cancel,
        }
    }

    fn previous(self) -> Self {
        match self {
            Self::Cancel => Self::Confirm,
            Self::Confirm => Self::Cancel,
        }
    }
}

impl DesktopApp {
    pub(crate) fn open_discard_modal(&mut self, action: PendingAction) {
        self.pending_action = Some(action);
        self.discard_modal_focus = DiscardModalButton::Cancel;
        self.discard_modal_keyboard_focus_visible = false;
        self.close_top_menu();
        self.hide_opcode_dropdown();
    }

    pub(crate) fn route_discard_modal_message(
        &mut self,
        message: &Message,
    ) -> Option<Task<Message>> {
        self.pending_action.as_ref()?;

        match message {
            Message::Tick
            | Message::CursorMoved(_)
            | Message::ModifiersChanged(_)
            | Message::CloseAbout
            | Message::OpenUrl(_) => None,
            Message::ConfirmDiscard => Some(self.confirm_discard()),
            Message::CancelDiscard | Message::EscPressed => {
                self.cancel_discard();
                Some(Task::none())
            }
            Message::FocusCycle { backward } => {
                self.cycle_discard_modal_focus(*backward);
                Some(Task::none())
            }
            Message::EnterPressed => Some(self.submit_discard_modal_focus()),
            Message::MousePressed | Message::MousePressedIgnored => {
                self.discard_modal_keyboard_focus_visible = false;
                Some(Task::none())
            }
            _ => Some(Task::none()),
        }
    }

    pub(crate) fn cycle_discard_modal_focus(&mut self, backward: bool) {
        self.discard_modal_keyboard_focus_visible = true;
        self.discard_modal_focus = if backward {
            self.discard_modal_focus.previous()
        } else {
            self.discard_modal_focus.next()
        };
    }

    pub(crate) fn submit_discard_modal_focus(&mut self) -> Task<Message> {
        match self.discard_modal_focus {
            DiscardModalButton::Cancel => {
                self.cancel_discard();
                Task::none()
            }
            DiscardModalButton::Confirm => self.confirm_discard(),
        }
    }

    pub(crate) fn confirm_discard(&mut self) -> Task<Message> {
        let Some(action) = self.pending_action.take() else {
            return Task::none();
        };
        self.discard_modal_focus = DiscardModalButton::Cancel;
        self.discard_modal_keyboard_focus_visible = false;
        match action {
            PendingAction::OpenSnapshot => {
                self.open_program();
                Task::none()
            }
            PendingAction::OpenDroppedFile(path) => {
                self.load_program_from_path(path);
                Task::none()
            }
            PendingAction::NewFile => {
                self.run_new_file();
                Task::none()
            }
            PendingAction::Import => {
                self.open_import_modal();
                Task::none()
            }
            PendingAction::CloseWindow => Task::done(Message::WindowClose),
            PendingAction::DeleteHdd => {
                self.delete_hdd_file();
                Task::none()
            }
        }
    }

    pub(crate) fn cancel_discard(&mut self) {
        self.pending_action = None;
        self.discard_modal_focus = DiscardModalButton::Cancel;
        self.discard_modal_keyboard_focus_visible = false;
    }

    pub(crate) fn close_titlebar_popup_before_drag(&mut self) -> bool {
        if self.opcode_dropdown_address.is_some() {
            self.hide_opcode_dropdown();
            return true;
        }
        if self.open_menu.is_some() || self.top_menu_focus.is_some() {
            self.close_top_menu();
            return true;
        }
        false
    }
}

#[cfg(test)]
mod tests {
    use super::super::{DesktopApp, MenuId, Message, PendingAction};
    use super::DiscardModalButton;

    #[test]
    fn discard_modal_blocks_memory_navigation_keys() {
        let (mut app, _task) = DesktopApp::with_initial_path(None);
        app.memory_address_input = "000A".to_owned();
        app.open_discard_modal(PendingAction::OpenSnapshot);

        let _task = app.update(Message::ArrowKey(1));

        assert_eq!(app.memory_address_input, "000A");
        assert!(matches!(
            app.pending_action,
            Some(PendingAction::OpenSnapshot)
        ));
    }

    #[test]
    fn discard_modal_blocks_run_shortcut_messages() {
        let (mut app, _task) = DesktopApp::with_initial_path(None);
        app.snapshot.cpu.memory.write(0, 0x13);
        app.open_discard_modal(PendingAction::OpenSnapshot);

        let _task = app.update(Message::ToggleRun);

        assert!(!app.running);
        assert!(matches!(
            app.pending_action,
            Some(PendingAction::OpenSnapshot)
        ));
    }

    #[test]
    fn enter_chooses_cancel_by_default_in_discard_modal() {
        let (mut app, _task) = DesktopApp::with_initial_path(None);
        app.dirty = true;
        app.open_discard_modal(PendingAction::OpenSnapshot);
        assert!(!app.discard_modal_keyboard_focus_visible);

        let _task = app.update(Message::EnterPressed);

        assert!(app.pending_action.is_none());
        assert!(app.dirty);
    }

    #[test]
    fn tab_cycles_discard_modal_buttons_in_a_ring() {
        let (mut app, _task) = DesktopApp::with_initial_path(None);
        app.open_discard_modal(PendingAction::OpenSnapshot);

        let _task = app.update(Message::FocusCycle { backward: false });
        assert_eq!(app.discard_modal_focus, DiscardModalButton::Confirm);
        assert!(app.discard_modal_keyboard_focus_visible);

        let _task = app.update(Message::FocusCycle { backward: false });
        assert_eq!(app.discard_modal_focus, DiscardModalButton::Cancel);

        let _task = app.update(Message::FocusCycle { backward: true });
        assert_eq!(app.discard_modal_focus, DiscardModalButton::Confirm);
    }

    #[test]
    fn enter_confirms_when_confirm_button_is_focused() {
        let (mut app, _task) = DesktopApp::with_initial_path(None);
        app.dirty = true;
        app.open_discard_modal(PendingAction::NewFile);

        let _task = app.update(Message::FocusCycle { backward: false });
        let _task = app.update(Message::EnterPressed);

        assert!(app.pending_action.is_none());
        assert!(!app.dirty);
    }

    #[test]
    fn cancelled_import_after_discard_confirmation_keeps_dirty_gate() {
        let (mut app, _task) = DesktopApp::with_initial_path(None);
        app.dirty = true;
        app.open_discard_modal(PendingAction::Import);

        let _task = app.update(Message::ConfirmDiscard);

        assert!(app.dirty);
        assert!(app.import_modal_open);

        let _task = app.update(Message::CancelImport);
        let _task = app.update(Message::Import);

        assert!(matches!(app.pending_action, Some(PendingAction::Import)));
    }

    #[test]
    fn esc_closes_open_top_menu() {
        let (mut app, _task) = DesktopApp::with_initial_path(None);
        app.open_menu = Some(MenuId::File);

        let _task = app.update(Message::EscPressed);

        assert_eq!(app.open_menu, None);
    }

    #[test]
    fn titlebar_empty_press_closes_opcode_dropdown_before_dragging() {
        let (mut app, _task) = DesktopApp::with_initial_path(None);
        app.opcode_dropdown_address = Some(0x0010);
        app.opcode_search_input = "mov".to_owned();

        let _task = app.update(Message::WindowDragStart);

        assert_eq!(app.opcode_dropdown_address, None);
        assert!(app.opcode_search_input.is_empty());
    }

    #[test]
    fn titlebar_empty_press_closes_top_menu_before_dragging() {
        let (mut app, _task) = DesktopApp::with_initial_path(None);
        app.open_menu = Some(MenuId::Mp);

        let _task = app.update(Message::WindowDragStart);

        assert_eq!(app.open_menu, None);
    }
}