kr580 2.0.0

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

use super::StatusKind;
use super::messages::Message;
use super::state::DesktopApp;
use crate::i18n::Key;
use crate::persistence::SubprogramSerializer;
use crate::runtime::parse::parse_hex_u16;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SubprogramDialogMode {
    Open,
    Save,
}

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

impl SubprogramDialogFocus {
    fn next(self, mode: SubprogramDialogMode) -> Self {
        match (mode, self) {
            (SubprogramDialogMode::Open, Self::Start) => Self::Cancel,
            (SubprogramDialogMode::Open, Self::Cancel) => Self::Confirm,
            (SubprogramDialogMode::Open, Self::Confirm) => Self::Start,
            (SubprogramDialogMode::Save, Self::Start) => Self::End,
            (SubprogramDialogMode::Save, Self::End) => Self::Cancel,
            (SubprogramDialogMode::Save, Self::Cancel) => Self::Confirm,
            (SubprogramDialogMode::Save, Self::Confirm) => Self::Start,
            (SubprogramDialogMode::Open, Self::End) => Self::Cancel,
        }
    }

    fn previous(self, mode: SubprogramDialogMode) -> Self {
        match (mode, self) {
            (SubprogramDialogMode::Open, Self::Start) => Self::Confirm,
            (SubprogramDialogMode::Open, Self::Cancel) => Self::Start,
            (SubprogramDialogMode::Open, Self::Confirm) => Self::Cancel,
            (SubprogramDialogMode::Save, Self::Start) => Self::Confirm,
            (SubprogramDialogMode::Save, Self::End) => Self::Start,
            (SubprogramDialogMode::Save, Self::Cancel) => Self::End,
            (SubprogramDialogMode::Save, Self::Confirm) => Self::Cancel,
            (SubprogramDialogMode::Open, Self::End) => Self::Start,
        }
    }
}

#[derive(Clone, Debug)]
pub(crate) struct SubprogramDialog {
    pub(crate) mode: SubprogramDialogMode,
    pub(crate) path: PathBuf,
    pub(crate) start_input: String,
    pub(crate) end_input: String,
    pub(crate) focus: SubprogramDialogFocus,
    pub(crate) keyboard_focus_visible: bool,
    pub(crate) error: Option<String>,
}

impl DesktopApp {
    pub(crate) fn open_subprogram_dialog(&mut self, path: PathBuf, mode: SubprogramDialogMode) {
        let start = parse_hex_u16(&self.memory_address_input)
            .or_else(|| self.selected_memory_address())
            .unwrap_or(0);
        self.close_top_menu();
        self.hide_opcode_dropdown();
        self.subprogram_dialog = Some(SubprogramDialog {
            mode,
            path,
            start_input: format!("{start:04X}"),
            end_input: "FFFF".to_owned(),
            focus: SubprogramDialogFocus::Start,
            keyboard_focus_visible: false,
            error: None,
        });
    }

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

        match message {
            Message::Tick | Message::CursorMoved(_) | Message::ModifiersChanged(_) => None,
            Message::SubprogramStartChanged(value) => {
                if let Some(dialog) = self.subprogram_dialog.as_mut() {
                    dialog.start_input = value.clone();
                    dialog.error = None;
                }
                Some(Task::none())
            }
            Message::SubprogramEndChanged(value) => {
                if let Some(dialog) = self.subprogram_dialog.as_mut() {
                    dialog.end_input = value.clone();
                    dialog.error = None;
                }
                Some(Task::none())
            }
            Message::ConfirmSubprogram => {
                self.confirm_subprogram();
                Some(Task::none())
            }
            Message::CancelSubprogram | Message::EscPressed => {
                self.subprogram_dialog = None;
                Some(Task::none())
            }
            Message::FocusCycle { backward } => {
                self.cycle_subprogram_focus(*backward);
                Some(Task::none())
            }
            Message::EnterPressed => {
                if self
                    .subprogram_dialog
                    .as_ref()
                    .is_some_and(|dialog| dialog.focus == SubprogramDialogFocus::Confirm)
                {
                    self.confirm_subprogram();
                }
                Some(Task::none())
            }
            Message::MousePressed | Message::MousePressedIgnored => {
                if let Some(dialog) = self.subprogram_dialog.as_mut() {
                    dialog.keyboard_focus_visible = false;
                }
                Some(Task::none())
            }
            _ => Some(Task::none()),
        }
    }

    fn cycle_subprogram_focus(&mut self, backward: bool) {
        if let Some(dialog) = self.subprogram_dialog.as_mut() {
            dialog.keyboard_focus_visible = true;
            dialog.focus = if backward {
                dialog.focus.previous(dialog.mode)
            } else {
                dialog.focus.next(dialog.mode)
            };
        }
    }

    fn confirm_subprogram(&mut self) {
        let Some(dialog) = self.subprogram_dialog.take() else {
            return;
        };
        let start = match parse_hex_u16(&dialog.start_input) {
            Some(start) => start,
            None => return self.restore_subprogram_error(dialog, Key::StatusInvalidAddressHex),
        };
        let end = match dialog.mode {
            SubprogramDialogMode::Open => match SubprogramSerializer::file_end(&dialog.path, start)
            {
                Ok(end) => end,
                Err(error) => {
                    return self.restore_subprogram_error_text(
                        dialog,
                        crate::runtime::humanize_error::humanize(&error.to_string(), self.lang),
                    );
                }
            },
            SubprogramDialogMode::Save => match parse_hex_u16(&dialog.end_input) {
                Some(end) if start <= end => end,
                Some(_) => {
                    return self.restore_subprogram_error(dialog, Key::SubprogramRangeInvalid);
                }
                None => return self.restore_subprogram_error(dialog, Key::StatusInvalidAddressHex),
            },
        };

        self.clear_error_notice();
        let path = dialog.path.clone();
        let display = path.display().to_string();
        let command = match dialog.mode {
            SubprogramDialogMode::Open => crate::backend::AppCommand::LoadSubprogram {
                path: path.clone(),
                start,
            },
            SubprogramDialogMode::Save => crate::backend::AppCommand::SaveSubprogram {
                path: path.clone(),
                start,
                end,
            },
        };
        self.dispatch_sync(command);
        if let Some(error) = self.error_notice.take() {
            self.error_notice_dismiss_at = None;
            return self.restore_subprogram_error_text(dialog, error);
        }

        self.current_snapshot_path = Some(path);
        self.current_subprogram_range = Some((start, end));
        self.undo_stack.clear();
        self.mark_saved();
        self.set_memory_address(start);
        self.set_status(match dialog.mode {
            SubprogramDialogMode::Open => StatusKind::Opened { display },
            SubprogramDialogMode::Save => StatusKind::SavedTo { display },
        });
    }

    fn restore_subprogram_error(&mut self, dialog: SubprogramDialog, key: Key) {
        self.restore_subprogram_error_text(dialog, self.lang.t(key).to_owned());
    }

    fn restore_subprogram_error_text(&mut self, mut dialog: SubprogramDialog, error: String) {
        dialog.error = Some(error);
        self.subprogram_dialog = Some(dialog);
    }
}

#[cfg(test)]
mod tests {
    use super::{SubprogramDialogFocus, SubprogramDialogMode};

    #[test]
    fn open_dialog_skips_end_address() {
        assert_eq!(
            SubprogramDialogFocus::Start.next(SubprogramDialogMode::Open),
            SubprogramDialogFocus::Cancel
        );
        assert_eq!(
            SubprogramDialogFocus::Cancel.previous(SubprogramDialogMode::Open),
            SubprogramDialogFocus::Start
        );
    }

    #[test]
    fn save_dialog_cycles_through_range_and_actions() {
        assert_eq!(
            SubprogramDialogFocus::Start.next(SubprogramDialogMode::Save),
            SubprogramDialogFocus::End
        );
        assert_eq!(
            SubprogramDialogFocus::Confirm.next(SubprogramDialogMode::Save),
            SubprogramDialogFocus::Start
        );
    }
}