kr580 1.0.0

Desktop KR580VM80 / Intel 8080 emulator.
Documentation
use std::collections::BTreeSet;
use std::sync::Arc;
use std::time::{Duration, Instant};

use iced::widget::text_editor;

use super::markdown::{HelpMarkdownDocument, parse_help_markdown};
use super::search::{HelpSearchIndex, HelpSearchRequest, HelpSearchResponse};
use super::{HelpDialog, HelpNode};
use crate::i18n::Lang;

const HELP_SEARCH_DEBOUNCE: Duration = Duration::from_millis(140);

#[derive(Clone)]
pub(super) struct PendingHelpSearch {
    generation: u64,
    due_at: Instant,
}

impl HelpDialog {
    pub(crate) fn new(lang: Lang) -> Self {
        let article = article_document(HelpNode::TopicAbout, lang);
        Self {
            selected: HelpNode::TopicAbout,
            expanded: BTreeSet::new(),
            search: String::new(),
            article_content: text_editor::Content::with_text(&article.text),
            article_highlights: article.highlights,
            article_content_node: HelpNode::TopicAbout,
            article_content_lang: lang,
            search_index: Arc::new(HelpSearchIndex::new(lang)),
            search_generation: 0,
            pending_search: None,
            search_results_query: String::new(),
            search_matches: BTreeSet::new(),
            search_results: Vec::new(),
        }
    }

    pub(crate) fn select_node(&mut self, node: HelpNode, lang: Lang) {
        self.selected = node;
        self.reveal_node(node);
        self.sync_article_content(node, lang);
    }

    pub(crate) fn toggle_expanded(&mut self, node: HelpNode, lang: Lang) {
        if !node.is_category() {
            self.select_node(node, lang);
            return;
        }
        if self.expanded.contains(&node) {
            self.expanded.remove(&node);
        } else {
            self.expanded.insert(node);
        }
    }

    pub(crate) fn expand_all(&mut self) {
        let mut all = BTreeSet::new();
        for node in HelpNode::ROOTS {
            collect_categories(node, &mut all);
        }
        self.expanded = all;
    }

    pub(crate) fn collapse_all(&mut self) {
        self.expanded.clear();
    }

    pub(crate) fn all_expanded(&self) -> bool {
        let mut all_cats = BTreeSet::new();
        for node in HelpNode::ROOTS {
            collect_categories(node, &mut all_cats);
        }
        self.expanded == all_cats
    }

    pub(crate) fn update_search_input(&mut self, query: String, lang: Lang) {
        self.search = query;
        self.search_generation = self.search_generation.wrapping_add(1);

        if self.search.trim().is_empty() {
            self.clear_search_results();
            return;
        }

        if self.search_index.lang() != lang {
            self.search_index = Arc::new(HelpSearchIndex::new(lang));
        }
        self.pending_search = Some(PendingHelpSearch {
            generation: self.search_generation,
            due_at: Instant::now() + HELP_SEARCH_DEBOUNCE,
        });
    }

    pub(crate) fn results_query(&self) -> &str {
        &self.search_results_query
    }

    pub(crate) fn search_results(&self) -> &[super::HelpSearchResult] {
        &self.search_results
    }

    pub(crate) fn node_matches_search(&self, node: HelpNode, _lang: Lang) -> bool {
        if self.results_query().is_empty() {
            return true;
        }
        self.search_matches.contains(&node)
    }

    pub(crate) fn take_due_search_request(
        &mut self,
        lang: Lang,
        now: Instant,
    ) -> Option<HelpSearchRequest> {
        let pending = self.pending_search.as_ref()?;
        if now < pending.due_at {
            return None;
        }

        let generation = pending.generation;
        let query = self.search.trim().to_owned();
        self.pending_search = None;

        if query.is_empty() {
            self.clear_search_results();
            return None;
        }
        if self.search_index.lang() != lang {
            self.search_index = Arc::new(HelpSearchIndex::new(lang));
        }
        Some(HelpSearchRequest::new(
            generation,
            lang,
            query,
            self.search_index.clone(),
        ))
    }

    pub(crate) fn apply_search_response(&mut self, response: HelpSearchResponse, lang: Lang) {
        if response.lang() != lang
            || response.generation() != self.search_generation
            || response.query() != self.search.trim()
        {
            return;
        }

        self.search_results_query = response.query().to_owned();
        self.search_matches.clear();
        self.search_results = response.into_results();
        let nodes = self
            .search_results
            .iter()
            .map(|result| result.node())
            .collect::<Vec<_>>();
        for node in nodes {
            self.collect_search_path(node);
        }

        if let Some(first) = self.search_results.first().map(|result| result.node()) {
            self.select_node(first, lang);
        }
    }

    fn reveal_node(&mut self, node: HelpNode) {
        for root in HelpNode::ROOTS {
            let mut path = Vec::new();
            if collect_path(root, node, &mut path) {
                for ancestor in path {
                    if ancestor != node && ancestor.is_category() {
                        self.expanded.insert(ancestor);
                    }
                }
                return;
            }
        }
    }

    fn sync_article_content(&mut self, node: HelpNode, lang: Lang) {
        if self.article_content_node == node && self.article_content_lang == lang {
            return;
        }
        let article = article_document(node, lang);
        self.article_content = text_editor::Content::with_text(&article.text);
        self.article_highlights = article.highlights;
        self.article_content_node = node;
        self.article_content_lang = lang;
    }

    pub(crate) fn perform_text_action(&mut self, action: text_editor::Action) {
        match action {
            text_editor::Action::Click(point) => {
                self.article_content
                    .perform(text_editor::Action::Click(point));
                self.suppress_article_caret();
            }
            text_editor::Action::Drag(point) => {
                self.article_content
                    .perform(text_editor::Action::Drag(point));
                self.suppress_empty_article_caret();
            }
            text_editor::Action::Edit(_) | text_editor::Action::Move(_) => {}
            action => self.article_content.perform(action),
        }
    }

    fn suppress_empty_article_caret(&mut self) {
        match self.article_content.selection() {
            Some(selection) if !selection.is_empty() => {}
            _ => self.suppress_article_caret(),
        }
    }

    fn suppress_article_caret(&mut self) {
        let cursor = self.article_content.cursor();
        self.article_content.move_to(text_editor::Cursor {
            position: cursor.position,
            selection: Some(cursor.position),
        });
    }

    fn clear_search_results(&mut self) {
        self.pending_search = None;
        self.search_results_query.clear();
        self.search_matches.clear();
        self.search_results.clear();
    }

    fn collect_search_path(&mut self, node: HelpNode) {
        for root in HelpNode::ROOTS {
            let mut path = Vec::new();
            if collect_path(root, node, &mut path) {
                self.search_matches.extend(path);
                return;
            }
        }
    }
}

fn collect_categories(node: HelpNode, expanded: &mut BTreeSet<HelpNode>) {
    if node.is_category() {
        expanded.insert(node);
        for child in node.children() {
            collect_categories(*child, expanded);
        }
    }
}

fn collect_path(current: HelpNode, target: HelpNode, path: &mut Vec<HelpNode>) -> bool {
    path.push(current);
    if current == target {
        return true;
    }
    for child in current.children() {
        if collect_path(*child, target, path) {
            return true;
        }
    }
    path.pop();
    false
}

fn article_document(node: HelpNode, lang: Lang) -> HelpMarkdownDocument {
    parse_help_markdown(lang.t(node.content_key()))
}