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 egui::{
    Color32, Context, Id, InnerResponse, Key, Modifiers, RichText, TextEdit, Theme, Widget,
};
use serde::{Deserialize, Serialize};

pub struct SearchWidget {
    widget_id: Id,
}

#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
pub enum SearchMode {
    #[default]
    TokenIndex,
    NodeName,
}

#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct SearchWidgetState {
    mode: SearchMode,
    search_text: String,
    current_error: Option<String>,
}

#[derive(Default, Debug)]
pub enum SearchWidgetOutput {
    #[default]
    None,
    TokenIndex(usize),
    NodeName(String),
    CloseRequested,
}

impl SearchWidget {
    pub fn new<I: Into<Id>>(widget_id: I) -> Self {
        Self {
            widget_id: widget_id.into(),
        }
    }

    pub fn load_state(&self, ctx: &Context) -> SearchWidgetState {
        ctx.data_mut(|d| d.get_persisted(self.widget_id))
            .unwrap_or_default()
    }

    pub fn store_state(&self, ctx: &Context, state: SearchWidgetState) {
        ctx.data_mut(|d| d.insert_persisted(self.widget_id, state));
    }

    pub fn show(self, ui: &mut egui::Ui) -> InnerResponse<SearchWidgetOutput> {
        let mut state = self.load_state(ui.ctx());

        ui.horizontal(|ui| {
            ui.label("Search for:");

            ui.radio_value(&mut state.mode, SearchMode::TokenIndex, "Token Nr.");
            ui.radio_value(&mut state.mode, SearchMode::NodeName, "Node Name");
        });
        ui.separator();

        let mut text_edit = TextEdit::singleline(&mut state.search_text);
        if state.current_error.is_some() {
            if ui.ctx().theme() == Theme::Light {
                text_edit = text_edit.background_color(Color32::LIGHT_RED);
            } else {
                text_edit = text_edit.background_color(Color32::DARK_RED);
            }
        }
        let mut output = SearchWidgetOutput::None;
        let text_edit_response = text_edit.show(ui).response;
        if text_edit_response.changed() {
            if state.search_text.trim().is_empty() {
                output = SearchWidgetOutput::None;
                state.current_error = None;
            } else {
                match state.mode {
                    SearchMode::TokenIndex => {
                        // Parse as number
                        if let Ok(parsed) = state.search_text.parse::<usize>() {
                            output = SearchWidgetOutput::TokenIndex(parsed);
                            state.current_error = None;
                        } else {
                            state.current_error =
                                Some(format!("\"{}\" is not a number", &state.search_text));
                        }
                    }
                    SearchMode::NodeName => {
                        output = SearchWidgetOutput::NodeName(state.search_text.clone());
                        state.current_error = None;
                    }
                }
            }
        } else if text_edit_response.lost_focus()
            && ui
                .ctx()
                .input_mut(|input| input.consume_key(Modifiers::NONE, Key::Escape))
        {
            output = SearchWidgetOutput::CloseRequested;
        }

        if let Some(err) = &state.current_error {
            let color = if ui.ctx().theme() == Theme::Light {
                Color32::DARK_RED
            } else {
                Color32::LIGHT_RED
            };
            ui.label(RichText::new(err).color(color));
        }

        let response = ui.response();

        self.store_state(ui.ctx(), state);
        InnerResponse::new(output, response)
    }
}

impl Widget for SearchWidget {
    fn ui(self, ui: &mut egui::Ui) -> egui::Response {
        self.show(ui).response
    }
}