kr580 1.0.0

Desktop KR580VM80 / Intel 8080 emulator.
Documentation
pub(in crate::view) mod chrome;
mod floppy;
mod hdd;
mod text;

pub(in crate::view) use floppy::{floppy_window, floppy_window_overlay};
pub(in crate::view) use hdd::{hdd_window, hdd_window_overlay};

use crate::backend::{DeviceStatus, StorageState};
use iced::widget::{Space, column, container, mouse_area, opaque, row, scrollable, stack};
use iced::{Element, Length, Padding, alignment};

use self::chrome::{device_backdrop_style, device_buffer_style};
use super::styles::{panel_style as dialog_style, scrollable_style};
use super::theme::{MONO_FONT, TOKYO_MUTED, TOKYO_TEXT, ui_text};
use crate::app::Message;
use crate::i18n::{Key, Lang};
use text::storage_buffer_text;

const WINDOW_WIDTH: f32 = 760.0;
const WINDOW_HEIGHT: f32 = 340.0;

#[derive(Clone, Copy)]
pub(super) struct StorageKeys {
    pub(super) content: Key,
    pub(super) image_content: Key,
    pub(super) status: Key,
    pub(super) path: Key,
    pub(super) path_missing: Key,
    pub(super) image_path_missing: Key,
    pub(super) bytes_queued: Key,
    pub(super) debug_enabled: Key,
}

pub(super) const FLOPPY_KEYS: StorageKeys = StorageKeys {
    content: Key::FloppyContent,
    image_content: Key::FloppyImageContent,
    status: Key::FloppyStatus,
    path: Key::FloppyPath,
    path_missing: Key::FloppyPathMissing,
    image_path_missing: Key::FloppyImagePathMissing,
    bytes_queued: Key::FloppyBytesQueued,
    debug_enabled: Key::FloppyDebugEnabled,
};

pub(super) const HDD_KEYS: StorageKeys = StorageKeys {
    content: Key::HddContent,
    image_content: Key::HddImageContent,
    status: Key::HddStatus,
    path: Key::HddPath,
    path_missing: Key::HddPathMissing,
    image_path_missing: Key::HddImagePathMissing,
    bytes_queued: Key::HddBytesQueued,
    debug_enabled: Key::HddDebugEnabled,
};

#[allow(clippy::too_many_arguments)]
pub(super) fn storage_window_overlay<'a>(
    state: &'a StorageState,
    show_image_contents: bool,
    image_contents: &'a [u8],
    image_error: Option<&'a str>,
    lang: Lang,
    close_msg: Message,
    header_fn: impl FnOnce(&'a StorageState, bool, bool, bool, Lang) -> Element<'a, Message>,
    keys: StorageKeys,
) -> Element<'a, Message> {
    let backdrop: Element<'_, Message> = mouse_area(
        container(Space::new())
            .width(Length::Fill)
            .height(Length::Fill)
            .style(device_backdrop_style),
    )
    .on_press(close_msg)
    .into();

    let body = storage_content(
        state,
        show_image_contents,
        image_contents,
        image_error,
        false,
        false,
        lang,
        header_fn,
        keys,
    );

    let dialog = container(body)
        .padding(16)
        .style(dialog_style)
        .width(Length::Fixed(WINDOW_WIDTH))
        .height(Length::Fixed(WINDOW_HEIGHT));

    let centered = column![
        Space::new().height(Length::FillPortion(1)),
        row![
            Space::new().width(Length::FillPortion(1)),
            opaque(dialog),
            Space::new().width(Length::FillPortion(1)),
        ]
        .align_y(alignment::Vertical::Center),
        Space::new().height(Length::FillPortion(1)),
    ]
    .width(Length::Fill)
    .height(Length::Fill);

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

#[allow(clippy::too_many_arguments)]
pub(super) fn storage_window<'a>(
    state: &'a StorageState,
    show_image_contents: bool,
    image_contents: &'a [u8],
    image_error: Option<&'a str>,
    always_on_top: bool,
    lang: Lang,
    header_fn: impl FnOnce(&'a StorageState, bool, bool, bool, Lang) -> Element<'a, Message>,
    keys: StorageKeys,
) -> Element<'a, Message> {
    container(storage_content(
        state,
        show_image_contents,
        image_contents,
        image_error,
        true,
        always_on_top,
        lang,
        header_fn,
        keys,
    ))
    .padding(16)
    .style(dialog_style)
    .width(Length::Fill)
    .height(Length::Fill)
    .into()
}

#[allow(clippy::too_many_arguments)]
fn storage_content<'a>(
    state: &'a StorageState,
    show_image_contents: bool,
    image_contents: &'a [u8],
    image_error: Option<&'a str>,
    detached: bool,
    always_on_top: bool,
    lang: Lang,
    header_fn: impl FnOnce(&'a StorageState, bool, bool, bool, Lang) -> Element<'a, Message>,
    keys: StorageKeys,
) -> Element<'a, Message> {
    column![
        header_fn(state, show_image_contents, detached, always_on_top, lang),
        Space::new().height(Length::Fixed(12.0)),
        dialog_body(
            state,
            show_image_contents,
            image_contents,
            image_error,
            lang,
            keys
        ),
    ]
    .width(Length::Fill)
    .height(Length::Fill)
    .into()
}

fn dialog_body<'a>(
    state: &'a StorageState,
    show_image_contents: bool,
    image_contents: &'a [u8],
    image_error: Option<&'a str>,
    lang: Lang,
    keys: StorageKeys,
) -> Element<'a, Message> {
    let (bytes, title_key, error) = if show_image_contents {
        (image_contents, keys.image_content, image_error)
    } else {
        (state.visible_buffer.as_slice(), keys.content, None)
    };
    let image_path_missing = show_image_contents && state.path.is_none();
    let text = error
        .filter(|_| !image_path_missing)
        .map(str::to_owned)
        .unwrap_or_else(|| storage_buffer_text(bytes));
    let empty = text.is_empty();
    let label = if image_path_missing {
        Some(lang.t(keys.image_path_missing))
    } else {
        empty.then(|| lang.t(title_key))
    };
    let buffer = scrollable(
        container(
            iced::widget::text(text)
                .font(MONO_FONT)
                .size(15)
                .color(TOKYO_TEXT)
                .wrapping(iced::widget::text::Wrapping::Glyph),
        )
        .padding(buffer_padding(empty))
        .width(Length::Fill)
        .height(Length::Shrink),
    )
    .width(Length::Fill)
    .height(Length::Fill)
    .style(|theme, status| scrollable_style(true, theme, status));

    let buffer_frame = container(buffer)
        .width(Length::Fill)
        .height(Length::Fill)
        .style(device_buffer_style)
        .clip(true);

    column![
        framed_buffer(buffer_frame.into(), label),
        storage_footer(state, lang, keys),
    ]
    .spacing(12)
    .width(Length::Fill)
    .height(Length::Fill)
    .into()
}

fn framed_buffer<'a>(buffer: Element<'a, Message>, title: Option<&'a str>) -> Element<'a, Message> {
    let Some(title) = title else {
        return buffer;
    };

    let label = container(ui_text(title, 13, TOKYO_MUTED))
        .padding(Padding {
            top: 8.0,
            right: 12.0,
            bottom: 0.0,
            left: 12.0,
        })
        .width(Length::Fill);

    stack![buffer, label]
        .width(Length::Fill)
        .height(Length::Fill)
        .into()
}

fn storage_footer<'a>(
    state: &'a StorageState,
    lang: Lang,
    keys: StorageKeys,
) -> Element<'a, Message> {
    let path = if state.debug_buffer {
        lang.t(keys.debug_enabled).to_owned()
    } else {
        state
            .path
            .as_ref()
            .map(|path| path.display().to_string())
            .unwrap_or_else(|| lang.t(keys.path_missing).to_owned())
    };
    let status = status_label(&state.status, lang);
    let meta = format!(
        "{}: {status}   {}: {path}   {}: {}",
        lang.t(keys.status),
        lang.t(keys.path),
        lang.t(keys.bytes_queued),
        state.bytes_queued,
    );

    row![
        container(
            iced::widget::text(meta)
                .font(MONO_FONT)
                .size(12)
                .color(TOKYO_TEXT)
                .wrapping(iced::widget::text::Wrapping::None),
        )
        .width(Length::Fill)
        .clip(true),
    ]
    .align_y(alignment::Vertical::Center)
    .into()
}

pub(in crate::view) fn status_label(status: &DeviceStatus, lang: Lang) -> String {
    match status {
        DeviceStatus::Ready => lang.t(Key::DeviceStatusReady).to_owned(),
        DeviceStatus::NotReady => lang.t(Key::DeviceStatusNotReady).to_owned(),
        DeviceStatus::Busy => lang.t(Key::DeviceStatusBusy).to_owned(),
        DeviceStatus::NoData => lang.t(Key::DeviceStatusNoData).to_owned(),
        DeviceStatus::Connected => lang.t(Key::DeviceStatusConnected).to_owned(),
        DeviceStatus::Listening => lang.t(Key::DeviceStatusListening).to_owned(),
        DeviceStatus::Disconnected => lang.t(Key::DeviceStatusDisconnected).to_owned(),
        DeviceStatus::Error(error) => error.clone(),
    }
}

fn buffer_padding(empty: bool) -> Padding {
    Padding {
        top: if empty { 34.0 } else { 12.0 },
        right: 12.0,
        bottom: 12.0,
        left: 12.0,
    }
}