kr580 1.0.0

Desktop KR580VM80 / Intel 8080 emulator.
Documentation
//! Monitor (port 00h) inspection window.

mod canvas;
mod hex_popup;
mod sections;
mod styles;

use std::time::Duration;

use crate::backend::MonitorState;
use iced::widget::{Space, button, column, container, mouse_area, opaque, row, stack, svg};
use iced::{Element, Length};

use crate::app::{HexStreamFilter, Message, ToolWindowKind};
use crate::i18n::{Key, Lang};
use crate::view::icons;
use crate::view::theme::{TOKYO_BLUE, TOKYO_TEXT};
use crate::view::tooltips::{hover_tooltip, shortcut_hint};

use hex_popup::hex_popup_overlay;
use sections::{pixel_layer_section, text_layer_section, unified_screen_section};
use styles::{
    ICON_BUTTON_SIZE, ICON_GLYPH_SIZE, MODAL_MARGIN, backdrop_style, dialog_style,
    icon_button_style,
};

pub(in crate::view) fn monitor_window_overlay<'a>(
    state: &'a MonitorState,
    split: bool,
    hex_popup: bool,
    hex_filter: HexStreamFilter,
    hex_reveal: bool,
    lang: Lang,
) -> Element<'a, Message> {
    let backdrop = mouse_area(
        container(Space::new())
            .width(Length::Fill)
            .height(Length::Fill)
            .style(backdrop_style),
    )
    .on_press(Message::CloseMonitor);

    let body = monitor_content(state, split, false, false, hex_popup, lang);

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

    let inset_dialog = column![
        Space::new().height(Length::Fixed(MODAL_MARGIN)),
        row![
            Space::new().width(Length::Fixed(MODAL_MARGIN)),
            opaque(dialog),
            Space::new().width(Length::Fixed(MODAL_MARGIN)),
        ]
        .height(Length::Fill),
        Space::new().height(Length::Fixed(MODAL_MARGIN)),
    ]
    .width(Length::Fill)
    .height(Length::Fill);

    let monitor_layer: Element<'_, Message> = stack![opaque(backdrop), inset_dialog]
        .width(Length::Fill)
        .height(Length::Fill)
        .into();

    if hex_popup {
        stack![
            monitor_layer,
            hex_popup_overlay(state, hex_filter, hex_reveal, lang)
        ]
        .width(Length::Fill)
        .height(Length::Fill)
        .into()
    } else {
        monitor_layer
    }
}

pub(in crate::view) fn monitor_window<'a>(
    state: &'a MonitorState,
    split: bool,
    hex_popup: bool,
    hex_filter: HexStreamFilter,
    hex_reveal: bool,
    always_on_top: bool,
    lang: Lang,
) -> Element<'a, Message> {
    let body = container(monitor_content(
        state,
        split,
        true,
        always_on_top,
        hex_popup,
        lang,
    ))
    .padding(16)
    .style(dialog_style)
    .width(Length::Fill)
    .height(Length::Fill);
    if hex_popup {
        stack![body, hex_popup_overlay(state, hex_filter, hex_reveal, lang)]
            .width(Length::Fill)
            .height(Length::Fill)
            .into()
    } else {
        body.into()
    }
}

fn monitor_content<'a>(
    state: &'a MonitorState,
    split: bool,
    detached: bool,
    always_on_top: bool,
    hex_popup: bool,
    lang: Lang,
) -> Element<'a, Message> {
    let layer_section: Element<'_, Message> = if split {
        column![
            container(pixel_layer_section(state, lang))
                .width(Length::Fill)
                .height(Length::FillPortion(3)),
            Space::new().height(Length::Fixed(12.0)),
            container(text_layer_section(state, lang))
                .width(Length::Fill)
                .height(Length::FillPortion(1)),
        ]
        .height(Length::Fill)
        .into()
    } else {
        unified_screen_section(state, lang)
    };
    column![
        monitor_header(split, detached, always_on_top, hex_popup, lang),
        Space::new().height(Length::Fixed(12.0)),
        layer_section,
    ]
    .width(Length::Fill)
    .height(Length::Fill)
    .into()
}

fn monitor_header<'a>(
    split: bool,
    detached: bool,
    always_on_top: bool,
    hex_popup: bool,
    lang: Lang,
) -> Element<'a, Message> {
    let toggle_tooltip = if split {
        Key::MonitorViewUnified
    } else {
        Key::MonitorViewSplit
    };
    let toggle_icon = if split {
        icons::square_merge_vertical()
    } else {
        icons::square_split_vertical()
    };

    let title = container(Space::new())
        .width(Length::Fill)
        .height(Length::Fixed(ICON_BUTTON_SIZE));
    let drag_handle: Element<'_, Message> = if detached {
        mouse_area(title)
            .on_press(Message::ToolWindowDragStart(ToolWindowKind::Monitor))
            .into()
    } else {
        title.into()
    };
    let window_toggle = if detached {
        icon_button(
            icons::panel_attach(),
            Message::AttachToolWindow(ToolWindowKind::Monitor),
            lang.t(Key::MonitorAttach),
            None,
            false,
        )
    } else {
        icon_button(
            icons::panel_detach(),
            Message::DetachToolWindow(ToolWindowKind::Monitor),
            lang.t(Key::MonitorDetach),
            None,
            false,
        )
    };
    let pin_toggle: Element<'_, Message> = if detached {
        row![
            icon_button(
                icons::pin(),
                Message::ToggleToolWindowAlwaysOnTop(ToolWindowKind::Monitor),
                lang.t(if always_on_top {
                    Key::MonitorUnpin
                } else {
                    Key::MonitorPin
                }),
                None,
                always_on_top,
            ),
            Space::new().width(Length::Fixed(6.0)),
        ]
        .into()
    } else {
        Space::new().width(Length::Shrink).into()
    };

    row![
        drag_handle,
        window_toggle,
        Space::new().width(Length::Fixed(6.0)),
        pin_toggle,
        icon_button(
            toggle_icon,
            Message::ToggleMonitorSplit,
            lang.t(toggle_tooltip),
            None,
            false,
        ),
        Space::new().width(Length::Fixed(6.0)),
        icon_button(
            icons::binary(),
            Message::ToggleMonitorHexPopup,
            lang.t(Key::MonitorHexBuffer),
            None,
            hex_popup,
        ),
        Space::new().width(Length::Fixed(6.0)),
        icon_button(
            icons::brush_cleaning(),
            Message::ClearMonitorBuffer,
            lang.t(Key::MonitorClearBuffer),
            None,
            false,
        ),
        Space::new().width(Length::Fixed(6.0)),
        icon_button(
            icons::image(),
            Message::SaveMonitorImage,
            lang.t(Key::MonitorSaveImage),
            None,
            false,
        ),
        Space::new().width(Length::Fixed(6.0)),
        icon_button(
            icons::window_close(),
            Message::CloseMonitor,
            lang.t(Key::MonitorClose),
            shortcut_hint(&Message::CloseMonitor),
            false,
        ),
    ]
    .align_y(iced::alignment::Vertical::Center)
    .spacing(0)
    .into()
}

fn icon_button(
    handle: svg::Handle,
    on_press: Message,
    hint: &'static str,
    shortcut: Option<&'static str>,
    active: bool,
) -> Element<'static, Message> {
    let glyph_color = if active { TOKYO_BLUE } else { TOKYO_TEXT };
    let glyph = svg(handle)
        .width(Length::Fixed(ICON_GLYPH_SIZE))
        .height(Length::Fixed(ICON_GLYPH_SIZE))
        .style(move |_theme, _status| svg::Style {
            color: Some(glyph_color),
        });

    let face = button(
        container(glyph)
            .padding(0)
            .width(Length::Fill)
            .height(Length::Fill)
            .align_x(iced::alignment::Horizontal::Center)
            .align_y(iced::alignment::Vertical::Center),
    )
    .on_press(on_press)
    .padding(0)
    .width(Length::Fixed(ICON_BUTTON_SIZE))
    .height(Length::Fixed(ICON_BUTTON_SIZE))
    .style(move |_theme, status| icon_button_style(status, active));

    hover_tooltip(
        face.into(),
        hint,
        shortcut,
        iced::widget::tooltip::Position::Bottom,
        Duration::from_millis(450),
    )
}