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, CornerRadius, Frame, Id, InnerResponse, Key, Label, Layout, Modifiers,
    RichText, Sense, Stroke, TextEdit, UiBuilder, Vec2, Widget, WidgetInfo, WidgetWithState,
};
use graphannis::graph::{AnnoKey, Annotation};
use graphannis_core::{graph::ANNIS_NS, util::join_qname};
use itertools::Itertools;
use serde::{Deserialize, Serialize};

use crate::app::{data::Span, widgets::create_multiple_anno_key_labels};

use super::token::SelectionType;

#[derive(Debug)]
pub struct SpanWidget<'a> {
    span: &'a Span,
    selected: SelectionType,
}

#[derive(Default, Debug)]
pub struct SpanWidgetOutput {
    pub selected: bool,
    pub changed_annotations: Vec<Annotation>,
}

#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct SpanWidgetState {
    pub edited_value: Option<(AnnoKey, String)>,
}

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

impl<'a> WidgetWithState for SpanWidget<'a> {
    type State = SpanWidgetState;
}

impl<'s> SpanWidget<'s> {
    pub fn new(span: &'s Span, selected: SelectionType) -> Self {
        SpanWidget { span, selected }
    }

    fn create_widget_id(&self) -> Id {
        Id::from(self.span.node_name.clone())
    }

    pub fn show(self, ui: &mut egui::Ui) -> InnerResponse<SpanWidgetOutput> {
        let widget_id = self.create_widget_id();
        let mut state = SpanWidgetState::load(ui.ctx(), widget_id).unwrap_or_default();
        let mut output = SpanWidgetOutput::default();

        let (text_color, bg_color) = if self.selected == SelectionType::None {
            (Color32::WHITE, Color32::DARK_GRAY)
        } else {
            (Color32::BLACK, ui.style().visuals.selection.bg_fill)
        };
        let widget_width = ui.max_rect().width();

        let frame_widget = Frame::group(ui.style())
            .stroke(Stroke::NONE)
            .inner_margin(3)
            .fill(bg_color)
            .corner_radius(CornerRadius::ZERO);
        let frame_margin = frame_widget.total_margin();
        let mut frame_response = frame_widget
            .show(ui, |ui| {
                let min_label_height = 20.0;
                let desired_size = Vec2::new(
                    widget_width - frame_margin.left - frame_margin.right,
                    min_label_height,
                );

                ui.vertical(|ui| {
                    for (current_key, current_value) in &self.span.labels {
                        if current_key.ns != ANNIS_NS {
                            if let Some((key, value)) = &mut state.edited_value {
                                let value_edit = TextEdit::singleline(value)
                                    .clip_text(true)
                                    .desired_width(widget_width)
                                    .show(ui);
                                if value_edit.response.lost_focus() {
                                    if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Enter))
                                    {
                                        output.changed_annotations.push(Annotation {
                                            key: key.clone(),
                                            val: value.as_str().into(),
                                        });
                                    }
                                    state.edited_value = None;
                                } else if !value_edit.response.has_focus() {
                                    value_edit.response.request_focus();
                                    if !ui.clip_rect().contains_rect(value_edit.response.rect) {
                                        value_edit.response.scroll_to_me(Some(egui::Align::Min));
                                    }
                                }
                            } else {
                                // Allocate UI space with the given width so the frame has exactly the desired with.
                                let (mut space_for_label, _) =
                                    ui.allocate_exact_size(desired_size, Sense::empty());

                                // If parts of the space are not inside the
                                // visible area, add an offset to the label
                                let visible_range = ui.clip_rect().x_range();
                                let offset = visible_range.min - space_for_label.left();
                                // Long labels are truncated to "...", so make
                                // sure we have space available to show these.
                                if offset > 0.0 && (offset + 15.0) < space_for_label.width() {
                                    space_for_label.set_left(space_for_label.left() + offset);
                                }

                                // Create the actual label
                                let lbl =
                                    Label::new(RichText::new(current_value).color(text_color))
                                        .halign(egui::Align::Min)
                                        .wrap_mode(egui::TextWrapMode::Truncate);

                                // Directly put the label text at the allocated position
                                let label_response = ui
                                    .scope_builder(
                                        UiBuilder::new()
                                            .max_rect(space_for_label)
                                            .layout(Layout::left_to_right(egui::Align::Center)),
                                        |ui| ui.add(lbl),
                                    )
                                    .inner;

                                if label_response.hovered() {
                                    ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
                                }

                                if self.selected == SelectionType::None {
                                    if label_response.clicked() {
                                        output.selected = true;
                                    }
                                } else if label_response.clicked()
                                    || ui
                                        .ctx()
                                        .input_mut(|i| i.consume_key(Modifiers::NONE, Key::Enter))
                                {
                                    state.edited_value =
                                        Some((current_key.clone(), current_value.clone()));
                                }
                            }
                        }
                    }
                });
            })
            .response;
        // React on interactions on the frame itself, since the labels are not
        // taking the full width.
        if self.selected == SelectionType::None {
            // Allow to select the span by clicking anywhere in its range
            frame_response = frame_response.interact(Sense::click());
        }

        frame_response = frame_response.on_hover_ui(|ui| {
            let keys = self.span.labels.keys().cloned().collect_vec();
            create_multiple_anno_key_labels(&keys, &self.span.node_name, ui);
        });
        if frame_response.hovered() {
            ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
        }
        if self.selected == SelectionType::None && frame_response.clicked() {
            output.selected = true;
        }

        SpanWidgetState::store(state, ui.ctx(), widget_id);

        frame_response.widget_info(move || {
            let annos = self
                .span
                .labels
                .iter()
                .map(|(key, value)| format!("{}={}", join_qname(&key.ns, &key.name), value))
                .join(", ");
            let widget_label = format!("Span annotation {annos} ({})", self.span.node_name);
            WidgetInfo::labeled(egui::WidgetType::Other, true, widget_label)
        });

        InnerResponse {
            inner: output,
            response: frame_response,
        }
    }

    /// Sets the state so that the first label is edited.
    pub fn request_edit_label_value(&mut self, ui: &egui::Ui) {
        let widget_id = self.create_widget_id();
        let mut state = SpanWidgetState::load(ui.ctx(), widget_id).unwrap_or_default();
        if state.edited_value.is_none()
            && let Some((key, value)) = self.span.labels.first_key_value()
        {
            state.edited_value = Some((key.clone(), value.clone()));
        };
        SpanWidgetState::store(state, ui.ctx(), widget_id);
    }
}

impl SpanWidgetState {
    pub fn load(ctx: &Context, id: Id) -> Option<Self> {
        ctx.data_mut(|d| d.get_persisted(id))
    }

    pub fn store(self, ctx: &Context, id: Id) {
        ctx.data_mut(|d| d.insert_persisted(id, self));
    }
}