kr580 1.0.0

Desktop KR580VM80 / Intel 8080 emulator.
Documentation
use iced::widget::{button, column, container, row, scrollable, svg, text, text_input};
use iced::{Color, Element, Length, alignment};

use super::styles::{help_search_input_style, hidden_scrollbar_style, sidebar_chip_style};
use crate::app::{HelpDialog, HelpNode, Message};
use crate::i18n::Lang;
use crate::view::icons;
use crate::view::theme::{TOKYO_MUTED, TOKYO_SURFACE, TOKYO_TEXT, ui_text};

const CHEVRON_SIZE: f32 = 14.0;
const SEARCH_ICON_SIZE: f32 = 13.0;
const TOGGLE_ICON_SIZE: f32 = 15.0;

pub(super) fn help_sidebar<'a>(dialog: &'a HelpDialog, lang: Lang) -> Element<'a, Message> {
    let search_icon = svg(icons::search())
        .width(Length::Fixed(SEARCH_ICON_SIZE))
        .height(Length::Fixed(SEARCH_ICON_SIZE))
        .style(|_theme, _status| svg::Style {
            color: Some(TOKYO_MUTED),
        });

    let search_field = text_input(
        lang.t(crate::i18n::Key::HelpSearchPlaceholder),
        &dialog.search,
    )
    .on_input(Message::HelpSearchChanged)
    .padding(0)
    .size(14)
    .style(help_search_input_style);

    let all_expanded = dialog.all_expanded();
    let toggle_icon = if all_expanded {
        icons::collapse_all()
    } else {
        icons::expand_all()
    };
    let toggle_btn = button(
        svg(toggle_icon)
            .width(Length::Fixed(TOGGLE_ICON_SIZE))
            .height(Length::Fixed(TOGGLE_ICON_SIZE))
            .style(|_theme, _status| svg::Style {
                color: Some(TOKYO_MUTED),
            }),
    )
    .on_press(Message::HelpToggleExpandAll)
    .padding(0)
    .style(|_theme, status| {
        let bg = match status {
            button::Status::Hovered | button::Status::Pressed => TOKYO_SURFACE,
            _ => Color::TRANSPARENT,
        };
        button::Style {
            background: Some(iced::Background::Color(bg)),
            border: iced::Border {
                radius: 4.0.into(),
                width: 0.0,
                color: Color::TRANSPARENT,
            },
            ..button::Style::default()
        }
    });

    let search_row = row![search_icon, search_field.width(Length::Fill), toggle_btn]
        .spacing(6)
        .align_y(alignment::Vertical::Center);

    let mut items: Vec<Element<'a, Message>> = Vec::new();
    let searching = !dialog.results_query().is_empty();
    build_tree(&mut items, &HelpNode::ROOTS, dialog, lang, 0, searching);

    let list = column(items).spacing(2).width(Length::Fill);

    column![
        container(search_row).padding(iced::Padding::new(10.0).right(8.0).bottom(6.0).left(12.0)),
        scrollable(
            container(list).padding(iced::Padding::new(4.0).right(8.0).bottom(12.0).left(8.0)),
        )
        .width(Length::Fill)
        .height(Length::Fill)
        .style(hidden_scrollbar_style),
    ]
    .width(Length::Fill)
    .height(Length::Fill)
    .into()
}

fn build_tree<'a>(
    items: &mut Vec<Element<'a, Message>>,
    nodes: &[HelpNode],
    dialog: &HelpDialog,
    lang: Lang,
    depth: u8,
    searching: bool,
) {
    for &node in nodes {
        let is_category = node.is_category();
        let expanded = dialog.expanded.contains(&node);
        let selected = !is_category && dialog.selected == node;
        let label_key = node.label_key();
        let label = lang.t(label_key);
        let indent = (depth as u16) * 14;

        if searching && !dialog.node_matches_search(node, lang) {
            continue;
        }

        let arrow: Element<'a, Message> = if is_category {
            let icon = if expanded {
                icons::chevron_down()
            } else {
                icons::chevron_right()
            };
            svg(icon)
                .width(Length::Fixed(CHEVRON_SIZE))
                .height(Length::Fixed(CHEVRON_SIZE))
                .style(|_theme, _status| svg::Style {
                    color: Some(TOKYO_MUTED),
                })
                .into()
        } else {
            text("")
                .size(10)
                .color_maybe(Some(Color::TRANSPARENT))
                .width(Length::Fixed(CHEVRON_SIZE))
                .into()
        };

        let row_content = row![
            text("").width(Length::Fixed(indent as f32)),
            arrow,
            ui_text(label, 14, TOKYO_TEXT),
        ]
        .spacing(2)
        .align_y(alignment::Vertical::Center);

        let message = if is_category {
            Message::HelpNodeToggled(node)
        } else {
            Message::HelpNodeSelected(node)
        };

        items.push(
            button(container(row_content).padding([4, 6]).width(Length::Fill))
                .on_press(message)
                .padding(0)
                .width(Length::Fill)
                .style(move |_theme, status| sidebar_chip_style(status, selected, false))
                .into(),
        );

        if is_category && expanded {
            build_tree(items, node.children(), dialog, lang, depth + 1, searching);
        }
    }
}