kr580 1.0.0

Desktop KR580VM80 / Intel 8080 emulator.
Documentation
use crate::backend::{MonitorState, TextCell};
use iced::widget::canvas::Cache;
use iced::widget::{Canvas, container, stack, text::Wrapping};
use iced::{Element, Length, Padding};

use crate::app::Message;
use crate::i18n::{Key, Lang};
use crate::view::theme::{TOKYO_MUTED, TOKYO_TEXT, mono_text, ui_text};

use super::canvas::{PixelCanvas, UnifiedCanvas};
use super::styles::{framebuffer_padding, framebuffer_style};

pub(super) fn unified_screen_section<'a>(
    state: &'a MonitorState,
    lang: Lang,
) -> Element<'a, Message> {
    let empty = state.pixels.is_empty() && state.text_cells.iter().all(|c| c.ch == 0);

    let canvas: Element<'a, Message> = container(
        Canvas::new(UnifiedCanvas {
            state,
            cache: Cache::new(),
        })
        .width(Length::Fill)
        .height(Length::Fill),
    )
    .padding(framebuffer_padding(empty))
    .width(Length::Fill)
    .height(Length::Fill)
    .style(framebuffer_style)
    .into();

    framed_layer(canvas, empty.then(|| lang.t(Key::MonitorUnifiedScreen)))
}

pub(super) fn pixel_layer_section<'a>(state: &'a MonitorState, lang: Lang) -> Element<'a, Message> {
    let empty = state.pixels.is_empty();

    let canvas: Element<'a, Message> = container(
        Canvas::new(PixelCanvas {
            state,
            cache: Cache::new(),
        })
        .width(Length::Fill)
        .height(Length::Fill),
    )
    .padding(framebuffer_padding(empty))
    .width(Length::Fill)
    .height(Length::Fill)
    .style(framebuffer_style)
    .into();

    framed_layer(canvas, empty.then(|| lang.t(Key::MonitorPixelLayer)))
}

pub(super) fn text_layer_section<'a>(state: &MonitorState, lang: Lang) -> Element<'a, Message> {
    let (text, empty) = text_layer_text(&state.text_cells);

    let body: Element<'_, Message> = container(
        mono_text(text, 13, TOKYO_TEXT)
            .width(Length::Fill)
            .wrapping(Wrapping::Glyph),
    )
    .padding(framebuffer_padding(empty))
    .width(Length::Fill)
    .height(Length::Fill)
    .style(framebuffer_style)
    .into();

    framed_layer(body, empty.then(|| lang.t(Key::MonitorTextLayer)))
}

fn text_layer_text(cells: &[TextCell]) -> (String, bool) {
    let visible_len = cells
        .iter()
        .rposition(|cell| cell.ch != 0)
        .map_or(0, |index| index + 1);
    let mut text = String::with_capacity(visible_len);
    let mut empty = true;

    for cell in &cells[..visible_len] {
        let glyph = if cell.ch.is_ascii_graphic() || cell.ch == b' ' {
            cell.ch as char
        } else if cell.ch == 0 {
            ' '
        } else {
            'ยท'
        };
        if cell.ch != 0 && cell.ch != b' ' {
            empty = false;
        }
        text.push(glyph);
    }

    (text, empty)
}

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

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

    stack![canvas, labels]
        .width(Length::Fill)
        .height(Length::Fill)
        .into()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn text_only_layer_does_not_insert_framebuffer_row_breaks() {
        let cells: Vec<TextCell> = (b'a'..=b'z')
            .cycle()
            .take(104)
            .map(|ch| TextCell { ch, color: 0x40 })
            .collect();

        let (text, empty) = text_layer_text(&cells);

        assert!(!empty);
        assert_eq!(text.len(), 104);
        assert!(!text.contains('\n'));
    }
}