annatomic 0.2.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 crate::{
    AnnatomicApp,
    app::{MainView, OPEN_LABEL},
};
use anyhow::Result;
use egui::{Button, Id, Key, Modifiers, RichText, ScrollArea, TextEdit, Ui, Widget};
use egui_notify::Toast;

#[cfg(test)]
mod tests;

pub(crate) fn show(ui: &mut Ui, app: &mut AnnatomicApp) -> Result<()> {
    let mut corpora: Vec<_> = app.project.corpus_locations.keys().cloned().collect();
    corpora.sort();

    ui.heading("Corpus Selection");

    ui.columns_const(|[c1, c2]| {
        if let Err(e) = corpus_selection(c1, app, &corpora) {
            app.notifier.report_error(e);
        }
        if c2.link(RichText::new("Import corpus").heading()).clicked() {
            app.main_view = MainView::Import;
        }
        let export_link = egui::Link::new(RichText::new("Export corpus").heading());
        if c2
            .add_enabled(app.project.selected_corpus.is_some(), export_link)
            .clicked()
        {
            app.main_view = MainView::Export;
        }
        create_new_corpus(c2, app);
    });

    Ok(())
}

fn corpus_selection(ui: &mut Ui, app: &mut AnnatomicApp, corpora: &[String]) -> Result<()> {
    if corpora.is_empty() {
        ui.label("No corpus available.");
    } else {
        ui.group(|ui| {
            ui.vertical(|ui| {
                ScrollArea::vertical().show(ui, |ui| {
                    for (idx, corpus_name) in corpora.iter().enumerate() {
                        ui.horizontal(|ui| {
                            let is_selected = app.project.selected_corpus.as_ref().is_some_and(
                                |selected_corpus| selected_corpus.name == *corpus_name,
                            );

                            let label = ui.selectable_label(is_selected, corpus_name);
                            // Request focus on a label when no other widget is currently focused.
                            // Use the already selected label or as a fallback the first one.
                            if ui.ctx().memory(|m| m.focused().is_none())
                                && (is_selected
                                    || (app.project.selected_corpus.is_none() && idx == 0))
                            {
                                label.request_focus();
                            }
                            let delete_button = Button::new(egui_phosphor::regular::TRASH)
                                .ui(ui)
                                .on_hover_text(format!("Delete corpus {corpus_name}"));

                            if label.clicked() && !ui.ctx().input(|i| i.modifiers.command) {
                                let result = app.apply_pending_updates();
                                if app.notifier.ok_or_report(result).is_some() {
                                    if is_selected {
                                        // Unselect the current corpus
                                        app.select_corpus(None);
                                    } else {
                                        // Select this corpus
                                        app.select_corpus(Some(corpus_name.clone()));
                                    }
                                }
                            }

                            if delete_button.clicked() {
                                let result = app.apply_pending_updates();
                                if app.notifier.ok_or_report(result).is_some() {
                                    app.project.scheduled_for_deletion = Some(corpus_name.clone());
                                }
                            }
                            if is_selected
                                && (ui.link(OPEN_LABEL.as_str()).clicked()
                                    || label.ctx.input_mut(|i| {
                                        i.consume_key(Modifiers::COMMAND, Key::Enter)
                                    }))
                            {
                                app.change_view(MainView::CorpusStructure);
                            }
                        });
                    }
                });
            });
        });
    }

    Ok(())
}

fn create_new_corpus(ui: &mut Ui, app: &mut AnnatomicApp) {
    ui.vertical(|ui| {
        ui.heading("Create new");
        let edit_id = Id::from("new-corpus-name");
        let text_edit = TextEdit::singleline(&mut app.new_corpus_name)
            .hint_text("Corpus name")
            .id(edit_id)
            .desired_width(ui.available_width())
            .ui(ui);

        if ui.button("Add").clicked()
            || (!app.new_corpus_name.is_empty()
                && text_edit
                    .ctx
                    .input_mut(|i| i.consume_key(Modifiers::NONE, Key::Enter)))
        {
            let result = app.apply_pending_updates();
            if app.notifier.ok_or_report(result).is_some() && app.new_corpus_name.is_empty() {
                app.notifier
                    .add_toast(Toast::warning("Empty corpus name not allowed"));
            } else if let Err(e) = app.project.new_empty_corpus(&app.new_corpus_name) {
                app.notifier.report_error(e);
            } else {
                app.notifier.add_toast(Toast::info(format!(
                    "Corpus \"{}\" added",
                    &app.new_corpus_name
                )));
                app.select_corpus(Some(app.new_corpus_name.clone()));
                app.new_corpus_name = String::new();
                ui.memory_mut(|mem| mem.surrender_focus(edit_id));
            }
        }
    });
}