use gtk::{ListBox, pango::WrapMode, prelude::*}; use relm4::prelude::*; use crate::subtitle_extractor::{StreamIndex, SubtitleCue, TRACKS}; #[derive(Debug)] pub enum SubtitleCueOutput { SeekTo(gst::ClockTime), } #[relm4::factory(pub)] impl FactoryComponent for SubtitleCue { type Init = Self; type Input = (); type Output = SubtitleCueOutput; type CommandOutput = (); type ParentWidget = gtk::ListBox; view! { gtk::Button { inline_css: "padding: 5px; border-radius: 0;", connect_clicked: { let start = self.start; move |_| { sender.output(SubtitleCueOutput::SeekTo(start)).unwrap() } }, gtk::Label { set_label: &self.text, set_wrap: true, set_wrap_mode: WrapMode::Word, set_xalign: 0.0, add_css_class: "body", } } } fn init_model(init: Self::Init, _index: &Self::Index, _sender: FactorySender) -> Self { init } } pub struct Transcript { active_stream_index: Option, active_cues: FactoryVecDeque, pending_scroll: Option, } #[derive(Debug)] pub enum TranscriptMsg { NewCue(StreamIndex, SubtitleCue), SelectTrack(Option), ScrollToCue(usize), } #[derive(Debug)] pub enum TranscriptOutput { SeekTo(gst::ClockTime), } pub struct TranscriptWidgets { viewport: gtk::Viewport, } impl SimpleComponent for Transcript { type Init = (); type Input = TranscriptMsg; type Output = TranscriptOutput; type Widgets = TranscriptWidgets; type Root = gtk::ScrolledWindow; fn init( _init: Self::Init, root: Self::Root, sender: ComponentSender, ) -> ComponentParts { let listbox = ListBox::builder() .selection_mode(gtk::SelectionMode::None) .build(); let active_cues = FactoryVecDeque::builder() .launch(listbox) .forward(sender.output_sender(), |output| match output { SubtitleCueOutput::SeekTo(pos) => TranscriptOutput::SeekTo(pos), }); let model = Self { active_stream_index: None, active_cues, pending_scroll: None, }; let widgets = TranscriptWidgets { viewport: gtk::Viewport::builder().build(), }; widgets.viewport.set_child(Some(model.active_cues.widget())); root.set_child(Some(&widgets.viewport)); ComponentParts { model, widgets } } fn init_root() -> Self::Root { gtk::ScrolledWindow::new() } fn update(&mut self, msg: Self::Input, _sender: ComponentSender) { self.pending_scroll = None; match msg { TranscriptMsg::NewCue(stream_index, cue) => { if self.active_stream_index == Some(stream_index) { self.active_cues.guard().push_back(cue); } } TranscriptMsg::SelectTrack(stream_index) => { self.active_stream_index = stream_index; // Clear current widgets and populate with selected track's cues self.active_cues.guard().clear(); if let Some(stream_ix) = stream_index { let tracks = TRACKS.read(); if let Some(track) = tracks.get(&stream_ix) { for cue in &track.cues { self.active_cues.guard().push_back(cue.clone()); } } } } TranscriptMsg::ScrollToCue(ix) => { self.pending_scroll = Some(ix); } } } fn update_view(&self, widgets: &mut Self::Widgets, _sender: ComponentSender) { if let Some(ix) = self.pending_scroll { if let Some(row) = self.active_cues.widget().row_at_index(ix as i32) { widgets.viewport.scroll_to(&row, None); } } } }