//! Event selection and dirty tracking for incremental rendering use crate::utils::RenderError; use ass_core::parser::{ast::EventType, Event, Script, Section}; #[cfg(not(feature = "std"))] use alloc::{collections::BTreeSet, vec::Vec}; #[cfg(feature = "std")] use std::collections::HashSet; #[derive(Debug, Clone)] struct SortedEvent { index: usize, start_cs: u32, end_cs: u32, } /// Tracks active events for incremental rendering. #[derive(Debug, Clone)] pub struct EventSelector { #[cfg(feature = "std")] previous_active: HashSet, #[cfg(not(feature = "std"))] previous_active: BTreeSet, last_timestamp: Option, render_comments: bool, /// Pre-sorted event index, built lazily. Sorted by start_cs for binary search. sorted_events: Vec, /// Event count when `sorted_events` was last built; triggers rebuild on mismatch. sorted_event_count: usize, } /// Result of event selection with dirty tracking #[derive(Debug)] pub struct ActiveEvents<'a> { pub events: Vec<&'a Event<'a>>, pub newly_active: Vec, pub newly_inactive: Vec, pub is_dirty: bool, } impl EventSelector { pub fn new() -> Self { Self { #[cfg(feature = "std")] previous_active: HashSet::new(), #[cfg(not(feature = "std"))] previous_active: BTreeSet::new(), last_timestamp: None, render_comments: false, // Comments are disabled events — not rendered by libass sorted_events: Vec::new(), sorted_event_count: 0, } } /// Set whether to render comment events. /// /// Resets the sorted event index so it is rebuilt on the next /// `select_active` call — the index filters by event type, so changing /// this flag must invalidate any previously built index. pub fn set_render_comments(&mut self, render: bool) { self.render_comments = render; self.sorted_event_count = 0; } fn build_sorted_index(&mut self, events: &[Event<'_>]) { self.sorted_events.clear(); self.sorted_events.reserve(events.len()); for (idx, event) in events.iter().enumerate() { let should_include = match event.event_type { EventType::Dialogue => true, EventType::Comment => self.render_comments, _ => false, }; if should_include { self.sorted_events.push(SortedEvent { index: idx, start_cs: event.start_time_cs().unwrap_or(0), end_cs: event.end_time_cs().unwrap_or(0), }); } } // Sort by start_cs for binary search self.sorted_events.sort_by_key(|e| e.start_cs); self.sorted_event_count = events.len(); } /// Select active events and track changes for incremental rendering. /// /// Uses a pre-sorted index with binary search (partition_point) for /// O(log n + k) typical case, where k is the number of active events. pub fn select_active<'a>( &mut self, script: &'a Script<'a>, time_cs: u32, ) -> Result, RenderError> { let mut active_events = Vec::new(); #[cfg(feature = "std")] let mut current_active = HashSet::new(); #[cfg(not(feature = "std"))] let mut current_active = BTreeSet::new(); // Find all active events using binary search on sorted index if let Some(events_section) = script.sections().iter().find_map(|section| { if let Section::Events(events) = section { Some(events) } else { None } }) { // Build or rebuild sorted index if needed if self.sorted_event_count != events_section.len() { self.build_sorted_index(events_section); } log::trace!( "EventSelector: Checking {} events at time {} (sorted index: {} entries)", events_section.len(), time_cs, self.sorted_events.len(), ); // Binary search: find the partition point where start_cs > time_cs. // All events with start_cs <= time_cs are in sorted_events[..partition]. let partition = self.sorted_events.partition_point(|e| e.start_cs <= time_cs); for se in &self.sorted_events[..partition] { if se.end_cs >= time_cs { let event = &events_section[se.index]; // Debug output for fade events if event.text.contains("\\fad") { log::trace!( "EventSelector: Fade event - start={}cs, end={}cs, time_cs={}cs, active=true", se.start_cs, se.end_cs, time_cs ); } active_events.push(event); current_active.insert(se.index); } } } // Track changes for incremental rendering let newly_active: Vec = current_active .iter() .filter(|idx| !self.previous_active.contains(idx)) .cloned() .collect(); let newly_inactive: Vec = self .previous_active .iter() .filter(|idx| !current_active.contains(idx)) .cloned() .collect(); // Check if re-render is needed let is_dirty = !newly_active.is_empty() || !newly_inactive.is_empty() || self.has_animated_events(&active_events, time_cs) || self .last_timestamp .is_none_or(|last| (time_cs as i32 - last as i32).abs() > 100); // Update state self.previous_active = current_active; self.last_timestamp = Some(time_cs); Ok(ActiveEvents { events: active_events, newly_active, newly_inactive, is_dirty, }) } /// Check if any events have active animations fn has_animated_events(&self, events: &[&Event], time_cs: u32) -> bool { for event in events { let text = event.text; // Check for animation tags if text.contains(r"\t(") || text.contains(r"\move(") || text.contains(r"\fade(") || text.contains(r"\fad(") { return true; } // Check for karaoke if text.contains(r"\k") || text.contains(r"\K") { if let Ok(start) = event.start_time_cs() { if time_cs > start { return true; } } } } false } pub fn reset(&mut self) { self.previous_active.clear(); self.last_timestamp = None; self.sorted_events.clear(); self.sorted_event_count = 0; } } impl Default for EventSelector { fn default() -> Self { Self::new() } }