kr580 1.0.0

Desktop KR580VM80 / Intel 8080 emulator.
Documentation
use std::fmt::Write;

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

use super::icons;
use super::storage::chrome::{
    device_backdrop_style, device_buffer_style, icon_button, window_controls,
};
use super::storage::status_label;
use super::styles::{panel_style, scrollable_style};
use super::theme::{MONO_FONT, TOKYO_MUTED, TOKYO_TEXT, ui_text};
use crate::app::{Message, ToolWindowKind};
use crate::i18n::{Key, Lang, PrinterKey};

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

pub(in crate::view) fn printer_window_overlay(
    state: &PrinterState,
    text_view: bool,
    lang: Lang,
) -> Element<'_, Message> {
    let backdrop: Element<'_, Message> = mouse_area(
        container(Space::new())
            .width(Length::Fill)
            .height(Length::Fill)
            .style(device_backdrop_style),
    )
    .on_press(Message::ClosePrinter)
    .into();
    let dialog = container(printer_content(state, text_view, false, false, lang))
        .padding(16)
        .style(panel_style)
        .width(Length::Fixed(WINDOW_WIDTH))
        .height(Length::Fixed(WINDOW_HEIGHT));

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

pub(in crate::view) fn printer_window(
    state: &PrinterState,
    text_view: bool,
    always_on_top: bool,
    lang: Lang,
) -> Element<'_, Message> {
    container(printer_content(state, text_view, true, always_on_top, lang))
        .padding(16)
        .style(panel_style)
        .width(Length::Fill)
        .height(Length::Fill)
        .into()
}

fn printer_content(
    state: &PrinterState,
    text_view: bool,
    detached: bool,
    always_on_top: bool,
    lang: Lang,
) -> Element<'_, Message> {
    let body = column![buffer_panel(state, text_view, lang), footer(state, lang)]
        .spacing(12)
        .width(Length::Fill)
        .height(Length::Fill);
    column![
        header(state, text_view, detached, always_on_top, lang),
        Space::new().height(Length::Fixed(12.0)),
        body,
    ]
    .width(Length::Fill)
    .height(Length::Fill)
    .into()
}

fn header(
    state: &PrinterState,
    text_view: bool,
    detached: bool,
    always_on_top: bool,
    lang: Lang,
) -> Element<'static, Message> {
    let busy = state.status == DeviceStatus::Busy;
    let (print_enabled, clear_enabled) = printer_actions_enabled(state);
    row![
        window_controls(ToolWindowKind::Printer, detached, always_on_top, lang),
        icon_button(
            icons::type_icon(),
            Some(Message::TogglePrinterBufferView),
            lang.t(Key::Printer(if text_view {
                PrinterKey::ShowBytes
            } else {
                PrinterKey::ShowText
            })),
            text_view,
            None,
        ),
        Space::new().width(Length::Fixed(6.0)),
        icon_button(
            icons::device_printer(),
            print_enabled.then_some(Message::PrintPrinterPdf),
            lang.t(Key::Printer(PrinterKey::PrintPdf)),
            busy,
            None,
        ),
        Space::new().width(Length::Fixed(6.0)),
        icon_button(
            icons::brush_cleaning(),
            clear_enabled.then_some(Message::ClearPrinterBuffer),
            lang.t(Key::Printer(PrinterKey::ClearBuffer)),
            false,
            None,
        ),
        Space::new().width(Length::Fixed(6.0)),
        icon_button(
            icons::window_close(),
            Some(Message::ClosePrinter),
            lang.t(Key::MonitorClose),
            false,
            None,
        ),
    ]
    .align_y(alignment::Vertical::Center)
    .into()
}

fn printer_actions_enabled(state: &PrinterState) -> (bool, bool) {
    (state.status != DeviceStatus::Busy, true)
}

fn buffer_panel<'a>(state: &'a PrinterState, text_view: bool, lang: Lang) -> Element<'a, Message> {
    let content = if text_view {
        format_printer_text(&state.spool)
    } else {
        format_printer_buffer(&state.spool)
    };
    let empty = content.is_empty();
    let scroll = scrollable(
        container(
            iced::widget::text(content)
                .font(MONO_FONT)
                .size(11)
                .color(TOKYO_TEXT)
                .wrapping(iced::widget::text::Wrapping::None),
        )
        .padding(if empty { [34, 12] } else { [12, 12] })
        .width(Length::Fill),
    )
    .width(Length::Fill)
    .height(Length::Fill)
    .style(|theme, status| scrollable_style(true, theme, status));
    let frame = container(scroll)
        .width(Length::Fill)
        .height(Length::Fill)
        .style(device_buffer_style)
        .clip(true);
    if empty {
        let label = container(ui_text(
            lang.t(Key::Printer(PrinterKey::BufferContents)),
            13,
            TOKYO_MUTED,
        ))
        .padding(Padding {
            top: 8.0,
            right: 12.0,
            bottom: 0.0,
            left: 12.0,
        })
        .width(Length::Fill);
        stack![frame, label]
            .width(Length::Fill)
            .height(Length::Fill)
            .into()
    } else {
        frame.into()
    }
}

fn footer<'a>(state: &'a PrinterState, lang: Lang) -> Element<'a, Message> {
    let target = state
        .target_path
        .as_ref()
        .map(|path| path.display().to_string())
        .unwrap_or_else(|| {
            lang.t(Key::Printer(PrinterKey::PdfTargetMissing))
                .to_owned()
        });
    let meta = format!(
        "{}: {}   {}: {}   {}: {target}",
        lang.t(Key::Printer(PrinterKey::Status)),
        status_label(&state.status, lang),
        lang.t(Key::Printer(PrinterKey::BytesBuffered)),
        state.bytes_buffered,
        lang.t(Key::Printer(PrinterKey::PdfTarget)),
    );
    iced::widget::text(meta)
        .font(MONO_FONT)
        .size(12)
        .color(TOKYO_TEXT)
        .wrapping(iced::widget::text::Wrapping::None)
        .into()
}

fn centered<'a>(content: Element<'a, Message>) -> Element<'a, Message> {
    column![
        Space::new().height(Length::Fill),
        row![
            Space::new().width(Length::Fill),
            content,
            Space::new().width(Length::Fill)
        ],
        Space::new().height(Length::Fill),
    ]
    .width(Length::Fill)
    .height(Length::Fill)
    .into()
}

fn format_printer_buffer(bytes: &[u8]) -> String {
    let mut output = String::new();
    for (line, chunk) in bytes.chunks(16).enumerate() {
        if line != 0 {
            output.push('\n');
        }
        let _ = write!(output, "{:04X}:", line * 16);
        for byte in chunk {
            let _ = write!(output, " {byte:02X}");
        }
    }
    output
}

fn format_printer_text(bytes: &[u8]) -> String {
    decode_oem_text(bytes).replace('\t', "    ")
}

#[cfg(test)]
mod tests {
    use super::{format_printer_buffer, format_printer_text, printer_actions_enabled};
    use crate::backend::{DeviceStatus, PrinterState};
    use std::path::PathBuf;

    #[test]
    fn printer_buffer_uses_hex_offsets_and_sixteen_bytes_per_line() {
        let bytes = (0..18).collect::<Vec<_>>();

        assert_eq!(
            format_printer_buffer(&bytes),
            "0000: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F\n0010: 10 11"
        );
    }

    #[test]
    fn printer_text_view_decodes_cp866_and_normalizes_controls() {
        assert_eq!(
            format_printer_text(&[0x8F, 0xE0, b'!', b'\r', b'\n', b'\t', 0x01]),
            "Пр!\n    ·"
        );
    }

    #[test]
    fn printer_actions_are_available_for_empty_ready_spool() {
        assert_eq!(
            printer_actions_enabled(&printer_state(DeviceStatus::Ready)),
            (true, true)
        );
    }

    #[test]
    fn printer_clear_remains_available_while_printing() {
        assert_eq!(
            printer_actions_enabled(&printer_state(DeviceStatus::Busy)),
            (false, true)
        );
    }

    fn printer_state(status: DeviceStatus) -> PrinterState {
        PrinterState {
            spool: Vec::new(),
            target_path: None::<PathBuf>,
            status,
            bytes_buffered: 0,
            last_error: None,
        }
    }
}