kr580 1.0.0

Desktop KR580VM80 / Intel 8080 emulator.
Documentation
use super::messages::RegisterInlineTarget;
use iced::keyboard;
use k580_core::RegisterName;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RegisterMove {
    Up,
    Down,
    Left,
    Right,
}

pub(crate) fn ctrl_arrow_move(
    key: &keyboard::Key,
    modifiers: keyboard::Modifiers,
) -> Option<RegisterMove> {
    if !modifiers.command() {
        return None;
    }

    match key {
        keyboard::Key::Named(keyboard::key::Named::ArrowUp) => Some(RegisterMove::Up),
        keyboard::Key::Named(keyboard::key::Named::ArrowDown) => Some(RegisterMove::Down),
        keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => Some(RegisterMove::Left),
        keyboard::Key::Named(keyboard::key::Named::ArrowRight) => Some(RegisterMove::Right),
        _ => None,
    }
}

impl RegisterInlineTarget {
    pub(crate) fn adjacent(self, backward: bool) -> Option<Self> {
        let order = match self {
            Self::Schematic(_) => &[RegisterName::A, RegisterName::B, RegisterName::C][..],
            Self::Mux(_) => &[
                RegisterName::B,
                RegisterName::C,
                RegisterName::D,
                RegisterName::E,
                RegisterName::H,
                RegisterName::L,
            ][..],
        };

        let register = self.register();
        let index = order.iter().position(|candidate| *candidate == register)?;
        let next = if backward {
            index.checked_sub(1)?
        } else {
            index.checked_add(1).filter(|next| *next < order.len())?
        };

        Some(match self {
            Self::Schematic(_) => Self::Schematic(order[next]),
            Self::Mux(_) => Self::Mux(order[next]),
        })
    }

    pub(crate) fn navigate(self, direction: RegisterMove) -> Option<Self> {
        match self {
            Self::Schematic(register) => {
                navigate_schematic(register, direction).map(Self::Schematic)
            }
            Self::Mux(register) => navigate_mux(register, direction).map(Self::Mux),
        }
    }
}

fn navigate_schematic(register: RegisterName, direction: RegisterMove) -> Option<RegisterName> {
    let index = [RegisterName::A, RegisterName::B, RegisterName::C]
        .iter()
        .position(|candidate| *candidate == register)?;

    let next = match direction {
        RegisterMove::Left => index.checked_sub(1)?,
        RegisterMove::Right => index.checked_add(1).filter(|next| *next < 3)?,
        RegisterMove::Up | RegisterMove::Down => return None,
    };

    Some([RegisterName::A, RegisterName::B, RegisterName::C][next])
}

fn navigate_mux(register: RegisterName, direction: RegisterMove) -> Option<RegisterName> {
    let (row, column) = match register {
        RegisterName::B => (0_usize, 0_usize),
        RegisterName::C => (0, 1),
        RegisterName::D => (1, 0),
        RegisterName::E => (1, 1),
        RegisterName::H => (2, 0),
        RegisterName::L => (2, 1),
        RegisterName::A => return None,
    };

    let (next_row, next_column) = match direction {
        RegisterMove::Up => (row.checked_sub(1)?, column),
        RegisterMove::Down => (row.checked_add(1).filter(|next| *next < 3)?, column),
        RegisterMove::Left => (row, column.checked_sub(1)?),
        RegisterMove::Right => (row, column.checked_add(1).filter(|next| *next < 2)?),
    };

    Some(match (next_row, next_column) {
        (0, 0) => RegisterName::B,
        (0, 1) => RegisterName::C,
        (1, 0) => RegisterName::D,
        (1, 1) => RegisterName::E,
        (2, 0) => RegisterName::H,
        (2, 1) => RegisterName::L,
        _ => return None,
    })
}

#[cfg(test)]
mod tests {
    use super::RegisterInlineTarget;
    use crate::app::DesktopApp;
    use k580_core::RegisterName;

    #[test]
    fn schematic_inline_order_walks_buffers_only() {
        use RegisterInlineTarget::Schematic;

        assert_eq!(
            Schematic(RegisterName::A).adjacent(false),
            Some(Schematic(RegisterName::B))
        );
        assert_eq!(
            Schematic(RegisterName::B).adjacent(false),
            Some(Schematic(RegisterName::C))
        );
        assert_eq!(Schematic(RegisterName::C).adjacent(false), None);
        assert_eq!(Schematic(RegisterName::A).adjacent(true), None);
        assert_eq!(
            Schematic(RegisterName::C).adjacent(true),
            Some(Schematic(RegisterName::B))
        );
    }

    #[test]
    fn mux_inline_order_walks_visible_register_grid() {
        use RegisterInlineTarget::Mux;

        assert_eq!(
            Mux(RegisterName::B).adjacent(false),
            Some(Mux(RegisterName::C))
        );
        assert_eq!(
            Mux(RegisterName::C).adjacent(false),
            Some(Mux(RegisterName::D))
        );
        assert_eq!(
            Mux(RegisterName::D).adjacent(false),
            Some(Mux(RegisterName::E))
        );
        assert_eq!(
            Mux(RegisterName::E).adjacent(false),
            Some(Mux(RegisterName::H))
        );
        assert_eq!(
            Mux(RegisterName::H).adjacent(false),
            Some(Mux(RegisterName::L))
        );
        assert_eq!(Mux(RegisterName::L).adjacent(false), None);
        assert_eq!(Mux(RegisterName::B).adjacent(true), None);
        assert_eq!(
            Mux(RegisterName::L).adjacent(true),
            Some(Mux(RegisterName::H))
        );
    }

    #[test]
    fn schematic_arrow_navigation_is_horizontal_only() {
        use super::RegisterMove::{Down, Left, Right, Up};
        use RegisterInlineTarget::Schematic;

        assert_eq!(
            Schematic(RegisterName::A).navigate(Right),
            Some(Schematic(RegisterName::B))
        );
        assert_eq!(
            Schematic(RegisterName::B).navigate(Right),
            Some(Schematic(RegisterName::C))
        );
        assert_eq!(Schematic(RegisterName::C).navigate(Right), None);
        assert_eq!(Schematic(RegisterName::A).navigate(Left), None);
        assert_eq!(
            Schematic(RegisterName::C).navigate(Left),
            Some(Schematic(RegisterName::B))
        );
        assert_eq!(Schematic(RegisterName::B).navigate(Up), None);
        assert_eq!(Schematic(RegisterName::B).navigate(Down), None);
    }

    #[test]
    fn mux_arrow_navigation_respects_columns_and_rows() {
        use super::RegisterMove::{Down, Left, Right, Up};
        use RegisterInlineTarget::Mux;

        assert_eq!(
            Mux(RegisterName::B).navigate(Right),
            Some(Mux(RegisterName::C))
        );
        assert_eq!(Mux(RegisterName::C).navigate(Right), None);
        assert_eq!(
            Mux(RegisterName::C).navigate(Left),
            Some(Mux(RegisterName::B))
        );
        assert_eq!(Mux(RegisterName::B).navigate(Left), None);

        assert_eq!(
            Mux(RegisterName::B).navigate(Down),
            Some(Mux(RegisterName::D))
        );
        assert_eq!(
            Mux(RegisterName::D).navigate(Down),
            Some(Mux(RegisterName::H))
        );
        assert_eq!(Mux(RegisterName::H).navigate(Down), None);
        assert_eq!(
            Mux(RegisterName::L).navigate(Up),
            Some(Mux(RegisterName::E))
        );
        assert_eq!(
            Mux(RegisterName::E).navigate(Up),
            Some(Mux(RegisterName::C))
        );
        assert_eq!(Mux(RegisterName::C).navigate(Up), None);
    }

    #[test]
    fn selecting_memory_clears_register_keyboard_context() {
        let (mut app, _task) = DesktopApp::with_initial_path(None);
        app.select_register_target(RegisterInlineTarget::Mux(RegisterName::B));

        app.select_memory(0x0010);

        assert_eq!(app.active_register_target, None);
        assert_eq!(app.inline_register_target, None);
    }

    #[test]
    fn right_register_editor_value_is_shared_with_register_readouts() {
        let (mut app, _task) = DesktopApp::with_initial_path(None);
        app.select_register(RegisterName::B);
        app.change_register_value("7F".to_owned());

        assert_eq!(app.display_register_value(RegisterName::B), "7F");
        assert_eq!(app.display_register_value(RegisterName::C), "00");
    }

    #[test]
    fn inline_register_value_is_shared_with_matching_readouts() {
        use RegisterInlineTarget::Mux;

        let (mut app, _task) = DesktopApp::with_initial_path(None);
        app.enter_inline_register(Mux(RegisterName::C));
        app.change_inline_register_value(Mux(RegisterName::C), "12".to_owned());

        assert_eq!(app.display_register_value(RegisterName::C), "12");
        assert_eq!(app.display_register_value(RegisterName::B), "00");
    }

    #[test]
    fn ctrl_arrow_navigation_keeps_register_inline_editor_active() {
        use super::RegisterMove::Right;
        use RegisterInlineTarget::Schematic;

        let (mut app, _task) = DesktopApp::with_initial_path(None);
        app.enter_inline_register(Schematic(RegisterName::A));
        app.focused_input = Some(crate::app::REGISTER_INLINE_INPUT_ID);

        let _task = app.navigate_inline_register_target(Right);

        assert_eq!(app.active_register_target, Some(Schematic(RegisterName::B)));
        assert_eq!(app.inline_register_target, Some(Schematic(RegisterName::B)));
        assert_eq!(
            app.focused_input,
            Some(crate::app::REGISTER_INLINE_INPUT_ID)
        );
    }

    #[test]
    fn ctrl_arrow_keys_map_to_register_moves() {
        let modifiers = iced::keyboard::Modifiers::CTRL;

        assert_eq!(
            super::ctrl_arrow_move(
                &iced::keyboard::Key::Named(iced::keyboard::key::Named::ArrowLeft),
                modifiers
            ),
            Some(super::RegisterMove::Left)
        );
        assert_eq!(
            super::ctrl_arrow_move(
                &iced::keyboard::Key::Named(iced::keyboard::key::Named::ArrowRight),
                modifiers
            ),
            Some(super::RegisterMove::Right)
        );
        assert_eq!(
            super::ctrl_arrow_move(&iced::keyboard::Key::Character("a".into()), modifiers),
            None
        );
        assert_eq!(
            super::ctrl_arrow_move(
                &iced::keyboard::Key::Named(iced::keyboard::key::Named::ArrowLeft),
                iced::keyboard::Modifiers::default()
            ),
            None
        );
    }

    #[test]
    fn focus_reconcile_outside_inline_register_cancels_edit() {
        use RegisterInlineTarget::Schematic;

        let (mut app, _task) = DesktopApp::with_initial_path(None);
        app.enter_inline_register(Schematic(RegisterName::A));
        app.focused_input = Some(crate::app::REGISTER_INLINE_INPUT_ID);
        let _task = app.handle_focus_reconciled(0, None);
        assert_eq!(app.inline_register_target, Some(Schematic(RegisterName::A)));

        let _task = app.handle_focus_reconciled(0, None);

        assert_eq!(app.inline_register_target, None);
        assert_eq!(app.focused_input, None);
        assert_eq!(app.active_register_target, Some(Schematic(RegisterName::A)));
    }

    #[test]
    fn focus_reconcile_on_inline_register_input_keeps_edit() {
        use RegisterInlineTarget::Mux;

        let (mut app, _task) = DesktopApp::with_initial_path(None);
        app.enter_inline_register(Mux(RegisterName::B));
        app.focused_input = Some(crate::app::REGISTER_INLINE_INPUT_ID);
        let _task = app.handle_focus_reconciled(0, None);

        let hit = iced::widget::Id::new(crate::app::REGISTER_INLINE_INPUT_ID);
        let _task = app.handle_focus_reconciled(0, Some(hit));

        assert_eq!(app.inline_register_target, Some(Mux(RegisterName::B)));
        assert_eq!(
            app.focused_input,
            Some(crate::app::REGISTER_INLINE_INPUT_ID)
        );
    }

    #[test]
    fn focus_reconcile_keeps_inline_open_on_entry_frame() {
        use RegisterInlineTarget::Mux;

        let (mut app, _task) = DesktopApp::with_initial_path(None);
        app.enter_inline_register(Mux(RegisterName::C));
        assert!(app.inline_register_just_entered);

        let _task = app.handle_focus_reconciled(0, None);

        assert_eq!(app.inline_register_target, Some(Mux(RegisterName::C)));
        assert!(!app.inline_register_just_entered);
    }
}