annatomic 0.4.0

The Annatomic annotation editor is intended to be used for the [RIDGES corpus](https://www.linguistik.hu-berlin.de/en/institut-en/professuren-en/korpuslinguistik/research/ridges-projekt). It is based on [graphANNIS](https://github.com/korpling/graphANNIS) and thus is internal data model is in principle suitable for a wide range of annotation concepts. "
Documentation
use std::{backtrace::BacktraceStatus, collections::VecDeque, sync::Arc};

use anyhow::Error;
use egui::{Color32, Context, mutex::RwLock};
use egui_notify::{Toast, Toasts};
use log::error;

#[derive(Default, Clone)]
pub(crate) struct Notifier {
    toasts: Arc<RwLock<Toasts>>,
    error_queue: Arc<RwLock<VecDeque<Error>>>,
}

impl Notifier {
    pub(crate) fn report_error(&self, e: Error) {
        let error_msg = if e.chain().len() > 1 {
            format!("{e}: {}", e.root_cause())
        } else {
            format!("{e}")
        };
        let backtrace = e.backtrace();
        if backtrace.status() == BacktraceStatus::Captured {
            error!("{error_msg}\n{backtrace}");
        } else {
            error!("{error_msg}");
        }

        let mut error_queue = self.error_queue.write();
        error_queue.push_back(e);
    }

    /// Converts from `anyhow::Result<T>` to [`Option<T>`] and reports any error.
    pub(crate) fn ok_or_report<T>(&self, result: anyhow::Result<T>) -> Option<T> {
        match result {
            Ok(v) => Some(v),
            Err(err) => {
                self.report_error(err);
                None
            }
        }
    }

    pub(crate) fn unwrap_or_default<T>(&self, result: anyhow::Result<T>) -> T
    where
        T: Default,
    {
        match result {
            Ok(o) => o,
            Err(e) => {
                self.report_error(e);
                T::default()
            }
        }
    }

    pub(crate) fn add_toast(&self, toast: Toast) {
        let mut messages = self.toasts.write();
        messages.add(toast);
    }
    pub(super) fn show(&self, ctx: &Context) {
        let mut messages = self.toasts.write();
        let mut error_queue = self.error_queue.write();
        while let Some(e) = error_queue.pop_front() {
            let error_msg = if e.chain().len() > 1 {
                format!("{e}: {}", e.root_cause())
            } else {
                format!("{e}")
            };
            // Add a custom toast with the correct phosphor icon for the exclamation mark (U+EE44)

            let mut toast = Toast::custom(
                error_msg,
                egui_notify::ToastLevel::Custom(
                    egui_phosphor::regular::EXCLAMATION_MARK.to_string(),
                    Color32::from_rgb(200, 90, 90),
                ),
            );
            toast.closable(true);
            toast.show_progress_bar(false);
            toast.duration(None);
            messages.add(toast);
        }
        messages.show(ctx);
    }

    #[cfg(test)]
    pub(crate) fn is_empty(&self) -> bool {
        let messages = self.toasts.read();
        messages.is_empty()
    }
}

#[cfg(test)]
mod tests {
    use crate::app::tests::{create_app_with_corpus, create_test_harness};

    #[test]
    fn show_error_message() {
        let app_state = create_app_with_corpus(
            "single_sentence",
            &include_bytes!("../../tests/data/single_sentence.graphml")[..],
        );
        let notifier = app_state.notifier.clone();
        let (mut harness, _app_state) = create_test_harness(app_state);
        harness.run();

        // Trigger showing an error
        notifier.report_error(anyhow::anyhow!("Test error"));
        harness.run();

        harness.snapshot("show_error_message");
    }
}