Skip to main content

oximedia_edit/
marker.rs

1//! Timeline markers and regions.
2//!
3//! Markers are used to annotate specific points on the timeline, while regions
4//! define ranges of interest.
5
6use std::collections::BTreeMap;
7
8/// Unique identifier for markers.
9pub type MarkerId = u64;
10
11/// A marker on the timeline.
12#[derive(Clone, Debug)]
13pub struct Marker {
14    /// Unique marker identifier.
15    pub id: MarkerId,
16    /// Timeline position.
17    pub position: i64,
18    /// Marker type.
19    pub marker_type: MarkerType,
20    /// Marker name.
21    pub name: String,
22    /// Marker color (for UI).
23    pub color: Option<[u8; 3]>,
24    /// User notes.
25    pub notes: Option<String>,
26}
27
28impl Marker {
29    /// Create a new marker.
30    #[must_use]
31    pub fn new(id: MarkerId, position: i64, name: String) -> Self {
32        Self {
33            id,
34            position,
35            marker_type: MarkerType::Standard,
36            name,
37            color: None,
38            notes: None,
39        }
40    }
41
42    /// Create a chapter marker.
43    #[must_use]
44    pub fn chapter(id: MarkerId, position: i64, name: String) -> Self {
45        Self {
46            id,
47            position,
48            marker_type: MarkerType::Chapter,
49            name,
50            color: Some([0, 100, 200]),
51            notes: None,
52        }
53    }
54
55    /// Create a comment marker.
56    #[must_use]
57    pub fn comment(id: MarkerId, position: i64, name: String, comment: String) -> Self {
58        Self {
59            id,
60            position,
61            marker_type: MarkerType::Comment,
62            name,
63            color: Some([255, 200, 0]),
64            notes: Some(comment),
65        }
66    }
67}
68
69/// Type of marker.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub enum MarkerType {
72    /// Standard marker.
73    Standard,
74    /// Chapter marker (for video chapters).
75    Chapter,
76    /// Comment/note marker.
77    Comment,
78    /// In point marker.
79    In,
80    /// Out point marker.
81    Out,
82    /// Cue point marker.
83    Cue,
84    /// Beat marker (for music).
85    Beat,
86}
87
88/// A region on the timeline (time range with metadata).
89#[derive(Clone, Debug)]
90pub struct Region {
91    /// Unique region identifier.
92    pub id: u64,
93    /// Region start position.
94    pub start: i64,
95    /// Region end position.
96    pub end: i64,
97    /// Region name.
98    pub name: String,
99    /// Region color (for UI).
100    pub color: Option<[u8; 3]>,
101    /// User notes.
102    pub notes: Option<String>,
103    /// Region is locked.
104    pub locked: bool,
105}
106
107impl Region {
108    /// Create a new region.
109    #[must_use]
110    pub fn new(id: u64, start: i64, end: i64, name: String) -> Self {
111        Self {
112            id,
113            start,
114            end,
115            name,
116            color: None,
117            notes: None,
118            locked: false,
119        }
120    }
121
122    /// Get region duration.
123    #[must_use]
124    pub fn duration(&self) -> i64 {
125        self.end - self.start
126    }
127
128    /// Check if region contains a position.
129    #[must_use]
130    pub fn contains(&self, position: i64) -> bool {
131        position >= self.start && position < self.end
132    }
133
134    /// Check if region overlaps with another region.
135    #[must_use]
136    pub fn overlaps(&self, other: &Region) -> bool {
137        !(self.end <= other.start || self.start >= other.end)
138    }
139
140    /// Check if region overlaps with a time range.
141    #[must_use]
142    pub fn overlaps_range(&self, start: i64, end: i64) -> bool {
143        !(self.end <= start || self.start >= end)
144    }
145}
146
147/// Manager for timeline markers.
148#[derive(Debug, Default)]
149pub struct MarkerManager {
150    /// All markers indexed by position.
151    markers: BTreeMap<i64, Vec<Marker>>,
152    /// Next marker ID.
153    next_id: MarkerId,
154}
155
156impl MarkerManager {
157    /// Create a new marker manager.
158    #[must_use]
159    pub fn new() -> Self {
160        Self {
161            markers: BTreeMap::new(),
162            next_id: 1,
163        }
164    }
165
166    /// Add a marker.
167    pub fn add(&mut self, mut marker: Marker) -> MarkerId {
168        marker.id = self.next_id;
169        self.next_id += 1;
170
171        self.markers
172            .entry(marker.position)
173            .or_default()
174            .push(marker.clone());
175
176        marker.id
177    }
178
179    /// Add a marker at a specific position.
180    pub fn add_at(&mut self, position: i64, name: String) -> MarkerId {
181        let marker = Marker::new(self.next_id, position, name);
182        self.add(marker)
183    }
184
185    /// Remove a marker by ID.
186    pub fn remove(&mut self, id: MarkerId) -> Option<Marker> {
187        for markers in self.markers.values_mut() {
188            if let Some(pos) = markers.iter().position(|m| m.id == id) {
189                return Some(markers.remove(pos));
190            }
191        }
192        None
193    }
194
195    /// Get a marker by ID.
196    #[must_use]
197    pub fn get(&self, id: MarkerId) -> Option<&Marker> {
198        self.markers
199            .values()
200            .flat_map(|v| v.iter())
201            .find(|m| m.id == id)
202    }
203
204    /// Get markers at a specific position.
205    #[must_use]
206    pub fn get_at(&self, position: i64) -> Vec<&Marker> {
207        self.markers
208            .get(&position)
209            .map(|v| v.iter().collect())
210            .unwrap_or_default()
211    }
212
213    /// Get all markers in a time range.
214    #[must_use]
215    pub fn get_in_range(&self, start: i64, end: i64) -> Vec<&Marker> {
216        self.markers
217            .range(start..end)
218            .flat_map(|(_, markers)| markers.iter())
219            .collect()
220    }
221
222    /// Get all markers.
223    #[must_use]
224    pub fn all(&self) -> Vec<&Marker> {
225        self.markers.values().flat_map(|v| v.iter()).collect()
226    }
227
228    /// Get markers by type.
229    #[must_use]
230    pub fn get_by_type(&self, marker_type: MarkerType) -> Vec<&Marker> {
231        self.all()
232            .into_iter()
233            .filter(|m| m.marker_type == marker_type)
234            .collect()
235    }
236
237    /// Get chapter markers sorted by position.
238    #[must_use]
239    pub fn get_chapters(&self) -> Vec<&Marker> {
240        self.get_by_type(MarkerType::Chapter)
241    }
242
243    /// Find nearest marker to a position.
244    #[must_use]
245    pub fn find_nearest(&self, position: i64) -> Option<&Marker> {
246        let mut nearest: Option<&Marker> = None;
247        let mut min_distance = i64::MAX;
248
249        for marker in self.all() {
250            let distance = (marker.position - position).abs();
251            if distance < min_distance {
252                min_distance = distance;
253                nearest = Some(marker);
254            }
255        }
256
257        nearest
258    }
259
260    /// Find next marker after position.
261    #[must_use]
262    pub fn find_next(&self, position: i64) -> Option<&Marker> {
263        self.markers
264            .range(position + 1..)
265            .flat_map(|(_, markers)| markers.iter())
266            .next()
267    }
268
269    /// Find previous marker before position.
270    #[must_use]
271    pub fn find_previous(&self, position: i64) -> Option<&Marker> {
272        self.markers
273            .range(..position)
274            .rev()
275            .flat_map(|(_, markers)| markers.iter())
276            .next()
277    }
278
279    /// Clear all markers.
280    pub fn clear(&mut self) {
281        self.markers.clear();
282    }
283
284    /// Get total marker count.
285    #[must_use]
286    pub fn len(&self) -> usize {
287        self.markers.values().map(Vec::len).sum()
288    }
289
290    /// Check if there are no markers.
291    #[must_use]
292    pub fn is_empty(&self) -> bool {
293        self.markers.is_empty()
294    }
295}
296
297/// Manager for timeline regions.
298#[derive(Debug, Default)]
299pub struct RegionManager {
300    /// All regions.
301    regions: Vec<Region>,
302    /// Next region ID.
303    next_id: u64,
304}
305
306impl RegionManager {
307    /// Create a new region manager.
308    #[must_use]
309    pub fn new() -> Self {
310        Self {
311            regions: Vec::new(),
312            next_id: 1,
313        }
314    }
315
316    /// Add a region.
317    pub fn add(&mut self, mut region: Region) -> u64 {
318        region.id = self.next_id;
319        self.next_id += 1;
320        let id = region.id;
321        self.regions.push(region);
322        id
323    }
324
325    /// Add a region with start and end positions.
326    pub fn add_range(&mut self, start: i64, end: i64, name: String) -> u64 {
327        let region = Region::new(self.next_id, start, end, name);
328        self.add(region)
329    }
330
331    /// Remove a region by ID.
332    pub fn remove(&mut self, id: u64) -> Option<Region> {
333        if let Some(pos) = self.regions.iter().position(|r| r.id == id) {
334            Some(self.regions.remove(pos))
335        } else {
336            None
337        }
338    }
339
340    /// Get a region by ID.
341    #[must_use]
342    pub fn get(&self, id: u64) -> Option<&Region> {
343        self.regions.iter().find(|r| r.id == id)
344    }
345
346    /// Get mutable region by ID.
347    pub fn get_mut(&mut self, id: u64) -> Option<&mut Region> {
348        self.regions.iter_mut().find(|r| r.id == id)
349    }
350
351    /// Get all regions.
352    #[must_use]
353    pub fn all(&self) -> Vec<&Region> {
354        self.regions.iter().collect()
355    }
356
357    /// Get regions containing a position.
358    #[must_use]
359    pub fn get_at(&self, position: i64) -> Vec<&Region> {
360        self.regions
361            .iter()
362            .filter(|r| r.contains(position))
363            .collect()
364    }
365
366    /// Get regions overlapping a time range.
367    #[must_use]
368    pub fn get_in_range(&self, start: i64, end: i64) -> Vec<&Region> {
369        self.regions
370            .iter()
371            .filter(|r| r.overlaps_range(start, end))
372            .collect()
373    }
374
375    /// Clear all regions.
376    pub fn clear(&mut self) {
377        self.regions.clear();
378    }
379
380    /// Get total region count.
381    #[must_use]
382    pub fn len(&self) -> usize {
383        self.regions.len()
384    }
385
386    /// Check if there are no regions.
387    #[must_use]
388    pub fn is_empty(&self) -> bool {
389        self.regions.is_empty()
390    }
391}
392
393/// In/Out points for timeline editing.
394#[derive(Clone, Copy, Debug, Default)]
395pub struct InOutPoints {
396    /// In point (start of selection).
397    pub in_point: Option<i64>,
398    /// Out point (end of selection).
399    pub out_point: Option<i64>,
400}
401
402impl InOutPoints {
403    /// Create new in/out points.
404    #[must_use]
405    pub fn new() -> Self {
406        Self {
407            in_point: None,
408            out_point: None,
409        }
410    }
411
412    /// Set in point.
413    pub fn set_in(&mut self, position: i64) {
414        self.in_point = Some(position);
415    }
416
417    /// Set out point.
418    pub fn set_out(&mut self, position: i64) {
419        self.out_point = Some(position);
420    }
421
422    /// Clear in point.
423    pub fn clear_in(&mut self) {
424        self.in_point = None;
425    }
426
427    /// Clear out point.
428    pub fn clear_out(&mut self) {
429        self.out_point = None;
430    }
431
432    /// Clear both points.
433    pub fn clear(&mut self) {
434        self.in_point = None;
435        self.out_point = None;
436    }
437
438    /// Check if both points are set.
439    #[must_use]
440    pub fn is_complete(&self) -> bool {
441        self.in_point.is_some() && self.out_point.is_some()
442    }
443
444    /// Get duration if both points are set.
445    #[must_use]
446    pub fn duration(&self) -> Option<i64> {
447        match (self.in_point, self.out_point) {
448            (Some(i), Some(o)) if o > i => Some(o - i),
449            _ => None,
450        }
451    }
452
453    /// Get the range as a tuple.
454    #[must_use]
455    pub fn range(&self) -> Option<(i64, i64)> {
456        match (self.in_point, self.out_point) {
457            (Some(i), Some(o)) if o > i => Some((i, o)),
458            _ => None,
459        }
460    }
461
462    /// Check if a position is within the in/out range.
463    #[must_use]
464    pub fn contains(&self, position: i64) -> bool {
465        match (self.in_point, self.out_point) {
466            (Some(i), Some(o)) => position >= i && position < o,
467            _ => false,
468        }
469    }
470}