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::subtitles::state::CueAddress; use crate::translation::TRANSLATIONS; use crate::util::Tracker; pub struct ActiveCueViewState { addr: CueAddress, text: String, // byte ranges for the words in `text` word_ranges: Vec>, } pub struct CueView { state: Tracker>, } #[derive(Debug)] pub enum CueViewMsg { // messages from the app SetCue(Option), // 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_sensitive: 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, ) -> relm4::ComponentParts { let model = Self { state: Tracker::new(None), }; let widgets = view_output!(); ComponentParts { model, widgets } } fn update(&mut self, message: Self::Input, _sender: relm4::ComponentSender) { self.state.reset(); match message { CueViewMsg::SetCue(addr) => { if let Some(addr) = addr { let text = addr.resolve_text(); let word_ranges = UnicodeSegmentation::unicode_word_indices(text.as_str()) .map(|(offset, slice)| Range { start: offset, end: offset + slice.len(), }) .collect(); self.state.set(Some(ActiveCueViewState { addr, text, word_ranges, })) } else { self.state.set(None); } } CueViewMsg::MouseMotion => { // only used to update popover in view } } } fn post_view() { if self.state.is_dirty() { if let Some(ActiveCueViewState { addr: _, text, word_ranges, }) = self.state.get() { let mut markup = String::new(); let mut it = 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!( "{}", 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); widgets.label.set_sensitive(true); } else { // insensitive = invisible by css widgets.label.set_sensitive(false); } } if let ( Some(ActiveCueViewState { addr: CueAddress(stream_ix, cue_ix), text: _, word_ranges, }), Some(word_ix_str), ) = (self.state.get(), widgets.label.current_uri()) { let word_ix = usize::from_str(word_ix_str.as_str()).unwrap(); { // TODO get translation widgets.popover_label.set_text(word_ix_str.as_str()); } let range = word_ranges.get(word_ix).unwrap(); 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) { widgets.popover.unparent(); } } impl CueView { fn get_rect_of_byte_range(label: >k::Label, range: &Range) -> 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, ) } }