summary refs log tree commit diff
path: root/src/cue_view.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/cue_view.rs')
-rw-r--r--src/cue_view.rs179
1 files changed, 179 insertions, 0 deletions
diff --git a/src/cue_view.rs b/src/cue_view.rs
new file mode 100644
index 0000000..c031720
--- /dev/null
+++ b/src/cue_view.rs
@@ -0,0 +1,179 @@
+use std::ops::Range;
+use std::str::FromStr;
+
+use gtk::gdk;
+use gtk::glib;
+use gtk::{pango, prelude::*};
+use relm4::prelude::*;
+use relm4::{ComponentParts, SimpleComponent};
+use unicode_segmentation::UnicodeSegmentation;
+
+use crate::util::OptionTracker;
+
+pub struct CueView {
+    text: OptionTracker<String>,
+    // byte ranges for the words in `text`
+    word_ranges: Vec<Range<usize>>,
+}
+
+#[derive(Debug)]
+pub enum CueViewMsg {
+    // messages from the app
+    SetText(Option<String>),
+    // messages from UI
+    MouseMotion,
+}
+
+#[derive(Debug)]
+pub enum CueViewOutput {
+    MouseEnter,
+    MouseLeave,
+}
+
+#[relm4::component(pub)]
+impl SimpleComponent for CueView {
+    type Init = ();
+    type Input = CueViewMsg;
+    type Output = CueViewOutput;
+
+    view! {
+        #[root]
+        #[name(label)]
+        gtk::Label {
+            add_controller: event_controller.clone(),
+            set_use_markup: true,
+            set_visible: false,
+            set_justify: gtk::Justification::Center,
+            add_css_class: "cue-view",
+        },
+
+        #[name(event_controller)]
+        gtk::EventControllerMotion {
+            connect_enter[sender] => move |_, _, _| { sender.output(CueViewOutput::MouseEnter).unwrap() },
+            connect_motion[sender] => move |_, _, _| { sender.input(CueViewMsg::MouseMotion) },
+            connect_leave[sender] => move |_| { sender.output(CueViewOutput::MouseLeave).unwrap() },
+        },
+
+        #[name(popover)]
+        gtk::Popover {
+            set_parent: &root,
+            set_position: gtk::PositionType::Top,
+            set_autohide: false,
+
+            #[name(popover_label)]
+            gtk::Label { }
+        }
+    }
+
+    fn init(
+        _init: Self::Init,
+        root: Self::Root,
+        sender: relm4::ComponentSender<Self>,
+    ) -> relm4::ComponentParts<Self> {
+        let model = Self {
+            text: OptionTracker::new(None),
+            word_ranges: Vec::new(),
+        };
+
+        let widgets = view_output!();
+
+        ComponentParts { model, widgets }
+    }
+
+    fn update(&mut self, message: Self::Input, _sender: relm4::ComponentSender<Self>) {
+        match message {
+            CueViewMsg::SetText(text) => {
+                self.text.set(text);
+
+                if let Some(text) = self.text.get() {
+                    self.word_ranges = UnicodeSegmentation::unicode_word_indices(text.as_str())
+                        .map(|(offset, slice)| Range {
+                            start: offset,
+                            end: offset + slice.len(),
+                        })
+                        .collect();
+                } else {
+                    self.word_ranges = Vec::new();
+                }
+            }
+            CueViewMsg::MouseMotion => {
+                // only used to update popover in view
+            }
+        }
+    }
+
+    fn post_view() {
+        if self.text.is_dirty() {
+            if let Some(text) = self.text.get() {
+                let mut markup = String::new();
+
+                let mut it = self.word_ranges.iter().enumerate().peekable();
+                if let Some((_, first_word_range)) = it.peek() {
+                    markup.push_str(
+                        glib::markup_escape_text(&text[..first_word_range.start]).as_str(),
+                    );
+                }
+                while let Some((word_ix, word_range)) = it.next() {
+                    markup.push_str(&format!(
+                        "<a href=\"{}\">{}</a>",
+                        word_ix,
+                        glib::markup_escape_text(&text[word_range.clone()])
+                    ));
+                    let next_gap_range = if let Some((_, next_word_range)) = it.peek() {
+                        word_range.end..next_word_range.start
+                    } else {
+                        word_range.end..text.len()
+                    };
+                    markup.push_str(glib::markup_escape_text(&text[next_gap_range]).as_str());
+                }
+
+                widgets.label.set_markup(markup.as_str());
+                widgets.label.set_visible(true);
+            } else {
+                widgets.label.set_visible(false);
+            }
+        }
+
+        if let Some(word_ix_str) = widgets.label.current_uri() {
+            let range = self
+                .word_ranges
+                .get(usize::from_str(word_ix_str.as_str()).unwrap())
+                .unwrap();
+            widgets
+                .popover_label
+                .set_text(&self.text.get().as_ref().unwrap()[range.clone()]);
+            widgets
+                .popover
+                .set_pointing_to(Some(&Self::get_rect_of_byte_range(&widgets.label, &range)));
+            widgets.popover.popup();
+        } else {
+            widgets.popover.popdown();
+        }
+    }
+
+    fn shutdown(&mut self, widgets: &mut Self::Widgets, _output: relm4::Sender<Self::Output>) {
+        widgets.popover.unparent();
+    }
+}
+
+impl CueView {
+    fn get_rect_of_byte_range(label: &gtk::Label, range: &Range<usize>) -> gdk::Rectangle {
+        let layout = label.layout();
+        let (offset_x, offset_y) = label.layout_offsets();
+
+        let start_pos = layout.index_to_pos(range.start as i32);
+        let end_pos = layout.index_to_pos(range.end as i32);
+        let (x, width) = if start_pos.x() <= end_pos.x() {
+            (start_pos.x(), end_pos.x() - start_pos.x())
+        } else {
+            (end_pos.x(), start_pos.x() - end_pos.x())
+        };
+
+        gdk::Rectangle::new(
+            x / pango::SCALE + offset_x,
+            start_pos.y() / pango::SCALE + offset_y,
+            width / pango::SCALE,
+            start_pos.height() / pango::SCALE,
+        )
+    }
+}