kr580 1.0.0

Desktop KR580VM80 / Intel 8080 emulator.
Documentation
mod controls;
mod groups;
mod local_icons;
mod styles;
mod target;

use iced::widget::{Space, button, column, container, mouse_area, opaque, row, stack};
use iced::{Element, Length, alignment};

use groups::{MemoryGroupState, flags_group, memory_group, register_group};
use styles::{footer_button_style, modal_backdrop_style, modal_dialog_style, tab_button_style};

use super::theme::{TOKYO_TEXT, ui_text};
use super::widgets::modal_footer_button;
use crate::app::{
    ExportFlagSelection, ExportMemoryColumns, ExportRegisterSelection, ExportTab, Message,
};
use crate::i18n::{Key, Lang};

const DIALOG_WIDTH: f32 = 660.0;
const GROUP_HEIGHT: f32 = 258.0;
const FLAGS_GROUP_HEIGHT: f32 = 72.0;
const TAB_HEIGHT: f32 = 34.0;

pub(super) struct ExportModalViewState<'a> {
    pub(super) tab: ExportTab,
    pub(super) target_input: &'a str,
    pub(super) target_options: &'a [String],
    pub(super) target_dropdown_open: bool,
    pub(super) target_highlight: Option<usize>,
    pub(super) memory_start: &'a str,
    pub(super) memory_end: &'a str,
    pub(super) columns: ExportMemoryColumns,
    pub(super) registers: ExportRegisterSelection,
    pub(super) flags: ExportFlagSelection,
    pub(super) lang: Lang,
}

pub(super) fn export_modal_overlay<'a>(state: ExportModalViewState<'a>) -> Element<'a, Message> {
    let ExportModalViewState {
        tab,
        target_input,
        target_options,
        target_dropdown_open,
        target_highlight,
        memory_start,
        memory_end,
        columns,
        registers,
        flags,
        lang,
    } = state;

    let backdrop = mouse_area(
        container(Space::new())
            .width(Length::Fill)
            .height(Length::Fill)
            .style(modal_backdrop_style),
    )
    .on_press(Message::CancelExport);

    let body = container(
        column![
            tabs(tab, lang),
            row![
                memory_group(MemoryGroupState {
                    tab,
                    target_input,
                    target_options,
                    target_dropdown_open,
                    target_highlight,
                    memory_start,
                    memory_end,
                    columns,
                    lang,
                }),
                register_group(registers, lang),
            ]
            .spacing(12)
            .height(Length::Fixed(GROUP_HEIGHT)),
            container(flags_group(flags, lang))
                .height(Length::Fixed(FLAGS_GROUP_HEIGHT))
                .width(Length::Fill),
            footer(lang),
        ]
        .spacing(12)
        .width(Length::Fixed(DIALOG_WIDTH)),
    )
    .padding([18, 20])
    .style(modal_dialog_style);

    let centred = column![
        Space::new().height(Length::Fill),
        row![
            Space::new().width(Length::Fill),
            opaque(body),
            Space::new().width(Length::Fill),
        ]
        .width(Length::Fill),
        Space::new().height(Length::Fill),
    ]
    .width(Length::Fill)
    .height(Length::Fill);

    stack![opaque(backdrop), centred]
        .width(Length::Fill)
        .height(Length::Fill)
        .into()
}

fn tabs(tab: ExportTab, lang: Lang) -> Element<'static, Message> {
    row![
        tab_button(
            lang.t(Key::ExportFormatXlsx),
            ExportTab::Xlsx,
            tab == ExportTab::Xlsx,
        ),
        tab_button(
            lang.t(Key::ExportFormatText),
            ExportTab::Text,
            tab == ExportTab::Text,
        ),
    ]
    .spacing(4)
    .width(Length::Fill)
    .into()
}

fn tab_button(label: &'static str, target: ExportTab, active: bool) -> Element<'static, Message> {
    button(
        container(ui_text(label, 15, TOKYO_TEXT))
            .width(Length::Fill)
            .height(Length::Fill)
            .align_x(alignment::Horizontal::Center)
            .align_y(alignment::Vertical::Center),
    )
    .on_press(Message::ExportTabSelected(target))
    .padding(0)
    .width(Length::FillPortion(1))
    .height(Length::Fixed(TAB_HEIGHT))
    .style(move |_theme, status| tab_button_style(status, active))
    .into()
}

fn footer(lang: Lang) -> Element<'static, Message> {
    row![
        Space::new().width(Length::Fill),
        modal_footer_button(
            lang.t(Key::DiscardCancel),
            Message::CancelExport,
            footer_button_style,
        ),
        modal_footer_button(
            lang.t(Key::FileExport),
            Message::ConfirmExport,
            footer_button_style,
        ),
    ]
    .spacing(12)
    .width(Length::Fill)
    .into()
}

#[cfg(test)]
mod tests {
    use super::super::theme::{TOKYO_BORDER, TOKYO_SURFACE};
    use super::styles::{checkbox_style, flag_checkbox_style, tab_button_style};
    use iced::Background;
    use iced::widget::button;

    #[test]
    fn active_tab_uses_fill_without_accent_border() {
        let style = tab_button_style(button::Status::Active, true);

        assert_eq!(style.background, Some(Background::Color(TOKYO_SURFACE)));
        assert_eq!(style.border.color, TOKYO_BORDER);
    }

    #[test]
    fn checked_box_has_background_fill() {
        let style = checkbox_style(true);

        assert!(matches!(style.background, Some(Background::Color(_))));
    }

    #[test]
    fn unchecked_box_keeps_empty_background() {
        let style = checkbox_style(false);

        assert_eq!(style.background, None);
    }

    #[test]
    fn flag_checkbox_uses_rounder_border_than_regular_checkbox() {
        let regular = checkbox_style(false);
        let flag = flag_checkbox_style(false);

        assert_ne!(flag.border.radius, regular.border.radius);
    }
}