Skip to main content

oximedia_edit/
selection.rs

1//! Selection management for timeline editing.
2//!
3//! Provides range-based and clip-based selection with multiple selection modes.
4
5#![allow(dead_code)]
6
7/// A time range on the timeline.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct SelectionRange {
10    /// Inclusive start of the range (in timebase units).
11    pub start: u64,
12    /// Exclusive end of the range (in timebase units).
13    pub end: u64,
14}
15
16impl SelectionRange {
17    /// Creates a new `SelectionRange`.
18    ///
19    /// # Panics
20    ///
21    /// Panics in debug mode if `end < start`.
22    #[must_use]
23    pub fn new(start: u64, end: u64) -> Self {
24        debug_assert!(end >= start, "SelectionRange: end must be >= start");
25        Self { start, end }
26    }
27
28    /// Returns the duration of this range.
29    #[must_use]
30    pub fn duration(&self) -> u64 {
31        self.end.saturating_sub(self.start)
32    }
33
34    /// Returns `true` if this range overlaps with `other`.
35    #[must_use]
36    pub fn overlaps(&self, other: &Self) -> bool {
37        self.start < other.end && other.start < self.end
38    }
39
40    /// Returns `true` if this range is adjacent to or overlapping `other`
41    /// (i.e., they can be merged into one).
42    #[must_use]
43    pub fn can_merge(&self, other: &Self) -> bool {
44        self.start <= other.end && other.start <= self.end
45    }
46
47    /// Merges two ranges into their union.
48    #[must_use]
49    pub fn merge(&self, other: &Self) -> Self {
50        Self {
51            start: self.start.min(other.start),
52            end: self.end.max(other.end),
53        }
54    }
55
56    /// Returns `true` if `t` falls within `[start, end)`.
57    #[must_use]
58    pub fn contains_time(&self, t: u64) -> bool {
59        t >= self.start && t < self.end
60    }
61}
62
63/// Determines how a new selection interacts with the existing selection.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum SelectionMode {
66    /// Replace the entire current selection with the new one.
67    Replace,
68    /// Add the new range/item to the existing selection (union).
69    Add,
70    /// Remove the new range/item from the existing selection (difference).
71    Subtract,
72    /// Toggle the new range/item (add if not selected, remove if selected).
73    Toggle,
74}
75
76/// A lightweight reference to a clip on the timeline, used for range-based
77/// selection queries.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct TimelineClipRef {
80    /// Unique clip identifier.
81    pub id: u64,
82    /// Timeline start position (inclusive).
83    pub start: u64,
84    /// Timeline end position (exclusive).
85    pub end: u64,
86}
87
88impl TimelineClipRef {
89    /// Creates a new `TimelineClipRef`.
90    #[must_use]
91    pub const fn new(id: u64, start: u64, end: u64) -> Self {
92        Self { id, start, end }
93    }
94
95    /// Returns `true` if this clip overlaps with `range`.
96    #[must_use]
97    pub fn overlaps_range(&self, range: &SelectionRange) -> bool {
98        self.start < range.end && range.start < self.end
99    }
100}
101
102/// Manages the current selection state for timeline editing.
103///
104/// A `Selection` tracks three orthogonal selection types:
105/// * **Ranges** – time spans on the timeline ruler.
106/// * **Clips** – individual clip IDs.
107/// * **Tracks** – track indices.
108#[derive(Debug, Clone, Default)]
109pub struct Selection {
110    /// Time ranges currently selected (kept non-overlapping and sorted).
111    ranges: Vec<SelectionRange>,
112    /// IDs of selected clips.
113    selected_clips: Vec<u64>,
114    /// Indices of selected tracks.
115    selected_tracks: Vec<u32>,
116}
117
118impl Selection {
119    /// Creates a new, empty `Selection`.
120    #[must_use]
121    pub fn new() -> Self {
122        Self::default()
123    }
124
125    // ------------------------------------------------------------------
126    // Range operations
127    // ------------------------------------------------------------------
128
129    /// Applies `range` to the time-range selection using `mode`.
130    pub fn select_range(&mut self, range: SelectionRange, mode: SelectionMode) {
131        match mode {
132            SelectionMode::Replace => {
133                self.ranges.clear();
134                if range.duration() > 0 {
135                    self.ranges.push(range);
136                }
137            }
138            SelectionMode::Add => {
139                if range.duration() > 0 {
140                    self.ranges.push(range);
141                    self.merge_overlapping_ranges();
142                }
143            }
144            SelectionMode::Subtract => {
145                self.subtract_range(range);
146            }
147            SelectionMode::Toggle => {
148                if self.ranges.iter().any(|r| r.overlaps(&range)) {
149                    self.subtract_range(range);
150                } else {
151                    self.ranges.push(range);
152                    self.merge_overlapping_ranges();
153                }
154            }
155        }
156    }
157
158    /// Subtracts a range from the current selection.
159    fn subtract_range(&mut self, sub: SelectionRange) {
160        let mut result = Vec::new();
161        for r in self.ranges.drain(..) {
162            if r.end <= sub.start || r.start >= sub.end {
163                // No overlap – keep as-is.
164                result.push(r);
165            } else {
166                // Left fragment.
167                if r.start < sub.start {
168                    result.push(SelectionRange::new(r.start, sub.start));
169                }
170                // Right fragment.
171                if r.end > sub.end {
172                    result.push(SelectionRange::new(sub.end, r.end));
173                }
174            }
175        }
176        self.ranges = result;
177    }
178
179    /// Merges all overlapping or adjacent ranges in `self.ranges`.
180    pub fn merge_overlapping_ranges(&mut self) {
181        if self.ranges.len() < 2 {
182            return;
183        }
184        self.ranges.sort_by_key(|r| r.start);
185        let mut merged: Vec<SelectionRange> = Vec::new();
186        for r in self.ranges.drain(..) {
187            if let Some(last) = merged.last_mut() {
188                if last.can_merge(&r) {
189                    *last = last.merge(&r);
190                    continue;
191                }
192            }
193            merged.push(r);
194        }
195        self.ranges = merged;
196    }
197
198    /// Returns the total duration covered by all selected ranges.
199    #[must_use]
200    pub fn selected_duration(&self) -> u64 {
201        self.ranges.iter().map(SelectionRange::duration).sum()
202    }
203
204    /// Returns a slice of the currently selected ranges.
205    #[must_use]
206    pub fn ranges(&self) -> &[SelectionRange] {
207        &self.ranges
208    }
209
210    // ------------------------------------------------------------------
211    // Clip operations
212    // ------------------------------------------------------------------
213
214    /// Applies `id` to the clip selection using `mode`.
215    pub fn select_clip(&mut self, id: u64, mode: SelectionMode) {
216        match mode {
217            SelectionMode::Replace => {
218                self.selected_clips.clear();
219                self.selected_clips.push(id);
220            }
221            SelectionMode::Add => {
222                if !self.selected_clips.contains(&id) {
223                    self.selected_clips.push(id);
224                }
225            }
226            SelectionMode::Subtract => {
227                self.selected_clips.retain(|&c| c != id);
228            }
229            SelectionMode::Toggle => {
230                if self.selected_clips.contains(&id) {
231                    self.selected_clips.retain(|&c| c != id);
232                } else {
233                    self.selected_clips.push(id);
234                }
235            }
236        }
237    }
238
239    /// Returns `true` if the clip with `id` is currently selected.
240    #[must_use]
241    pub fn is_clip_selected(&self, id: u64) -> bool {
242        self.selected_clips.contains(&id)
243    }
244
245    /// Returns a slice of all currently selected clip IDs.
246    #[must_use]
247    pub fn selected_clips(&self) -> &[u64] {
248        &self.selected_clips
249    }
250
251    /// Selects all clips from `clips` whose time span overlaps `range`.
252    pub fn select_all_in_range(&mut self, clips: &[TimelineClipRef], range: SelectionRange) {
253        for clip in clips {
254            if clip.overlaps_range(&range) && !self.selected_clips.contains(&clip.id) {
255                self.selected_clips.push(clip.id);
256            }
257        }
258    }
259
260    // ------------------------------------------------------------------
261    // Track operations
262    // ------------------------------------------------------------------
263
264    /// Selects the track with the given index using `mode`.
265    pub fn select_track(&mut self, track: u32, mode: SelectionMode) {
266        match mode {
267            SelectionMode::Replace => {
268                self.selected_tracks.clear();
269                self.selected_tracks.push(track);
270            }
271            SelectionMode::Add => {
272                if !self.selected_tracks.contains(&track) {
273                    self.selected_tracks.push(track);
274                }
275            }
276            SelectionMode::Subtract => {
277                self.selected_tracks.retain(|&t| t != track);
278            }
279            SelectionMode::Toggle => {
280                if self.selected_tracks.contains(&track) {
281                    self.selected_tracks.retain(|&t| t != track);
282                } else {
283                    self.selected_tracks.push(track);
284                }
285            }
286        }
287    }
288
289    /// Returns `true` if the track with `index` is currently selected.
290    #[must_use]
291    pub fn is_track_selected(&self, index: u32) -> bool {
292        self.selected_tracks.contains(&index)
293    }
294
295    /// Returns a slice of all currently selected track indices.
296    #[must_use]
297    pub fn selected_tracks(&self) -> &[u32] {
298        &self.selected_tracks
299    }
300
301    // ------------------------------------------------------------------
302    // General
303    // ------------------------------------------------------------------
304
305    /// Clears all selection state (ranges, clips, and tracks).
306    pub fn clear(&mut self) {
307        self.ranges.clear();
308        self.selected_clips.clear();
309        self.selected_tracks.clear();
310    }
311
312    /// Returns `true` if nothing at all is selected.
313    #[must_use]
314    pub fn is_empty(&self) -> bool {
315        self.ranges.is_empty() && self.selected_clips.is_empty() && self.selected_tracks.is_empty()
316    }
317}
318
319/// A reference to a clip on a specific track, used in multi-track selection.
320#[derive(Debug, Clone, Copy, PartialEq, Eq)]
321pub struct SelectionItem {
322    /// Clip identifier.
323    pub clip_id: u64,
324    /// Track identifier.
325    pub track_id: u32,
326}
327
328impl SelectionItem {
329    /// Create a new selection item.
330    #[must_use]
331    pub fn new(clip_id: u64, track_id: u32) -> Self {
332        Self { clip_id, track_id }
333    }
334
335    /// Returns `true` when both items reside on the same track.
336    #[must_use]
337    pub fn same_track(&self, other: &SelectionItem) -> bool {
338        self.track_id == other.track_id
339    }
340}
341
342/// A multi-track clip selection backed by a flat list of [`SelectionItem`]s.
343#[derive(Debug, Clone, Default)]
344pub struct EditSelection {
345    /// Items currently selected.
346    pub items: Vec<SelectionItem>,
347}
348
349impl EditSelection {
350    /// Create an empty selection.
351    #[must_use]
352    pub fn new() -> Self {
353        Self::default()
354    }
355
356    /// Add an item; silently ignored if a clip with the same `clip_id` is
357    /// already present.
358    pub fn add(&mut self, item: SelectionItem) {
359        if !self.contains(item.clip_id) {
360            self.items.push(item);
361        }
362    }
363
364    /// Remove the item whose `clip_id` matches.
365    pub fn remove(&mut self, clip_id: u64) {
366        self.items.retain(|i| i.clip_id != clip_id);
367    }
368
369    /// Returns `true` if a clip with the given ID is selected.
370    #[must_use]
371    pub fn contains(&self, clip_id: u64) -> bool {
372        self.items.iter().any(|i| i.clip_id == clip_id)
373    }
374
375    /// Clear all selected items.
376    pub fn clear(&mut self) {
377        self.items.clear();
378    }
379
380    /// Number of selected items.
381    #[must_use]
382    pub fn count(&self) -> usize {
383        self.items.len()
384    }
385
386    /// Returns the unique track IDs present in the selection (order unspecified).
387    #[must_use]
388    pub fn tracks(&self) -> Vec<u32> {
389        let mut seen: Vec<u32> = Vec::new();
390        for item in &self.items {
391            if !seen.contains(&item.track_id) {
392                seen.push(item.track_id);
393            }
394        }
395        seen
396    }
397}
398
399/// Manages groups of clips that are linked together so that selecting one
400/// automatically includes its linked peers.
401#[derive(Debug, Clone, Default)]
402pub struct LinkedSelection {
403    /// Each inner `Vec<u64>` is a group of linked clip IDs.
404    pub groups: Vec<Vec<u64>>,
405}
406
407impl LinkedSelection {
408    /// Create an empty linked-selection manager.
409    #[must_use]
410    pub fn new() -> Self {
411        Self::default()
412    }
413
414    /// Register a new group of linked clips.
415    pub fn add_linked_group(&mut self, ids: Vec<u64>) {
416        if !ids.is_empty() {
417            self.groups.push(ids);
418        }
419    }
420
421    /// Return all clip IDs that are linked to `clip_id` (not including
422    /// `clip_id` itself).  Returns an empty `Vec` when the clip has no links.
423    #[must_use]
424    pub fn linked_clips(&self, clip_id: u64) -> Vec<u64> {
425        for group in &self.groups {
426            if group.contains(&clip_id) {
427                return group.iter().copied().filter(|&id| id != clip_id).collect();
428            }
429        }
430        Vec::new()
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    // ------------------------------------------------------------------
439    // SelectionRange tests
440    // ------------------------------------------------------------------
441
442    #[test]
443    fn test_range_duration() {
444        let r = SelectionRange::new(10, 30);
445        assert_eq!(r.duration(), 20);
446    }
447
448    #[test]
449    fn test_range_overlaps() {
450        let a = SelectionRange::new(0, 10);
451        let b = SelectionRange::new(5, 15);
452        let c = SelectionRange::new(10, 20);
453        assert!(a.overlaps(&b));
454        assert!(!a.overlaps(&c)); // touching but not overlapping
455    }
456
457    #[test]
458    fn test_range_merge() {
459        let a = SelectionRange::new(0, 10);
460        let b = SelectionRange::new(8, 20);
461        let m = a.merge(&b);
462        assert_eq!(m.start, 0);
463        assert_eq!(m.end, 20);
464    }
465
466    #[test]
467    fn test_range_contains_time() {
468        let r = SelectionRange::new(10, 20);
469        assert!(r.contains_time(10));
470        assert!(r.contains_time(15));
471        assert!(!r.contains_time(20)); // exclusive end
472    }
473
474    // ------------------------------------------------------------------
475    // Selection – range mode tests
476    // ------------------------------------------------------------------
477
478    #[test]
479    fn test_select_range_replace() {
480        let mut s = Selection::new();
481        s.select_range(SelectionRange::new(0, 100), SelectionMode::Add);
482        s.select_range(SelectionRange::new(200, 300), SelectionMode::Replace);
483        assert_eq!(s.ranges().len(), 1);
484        assert_eq!(s.ranges()[0], SelectionRange::new(200, 300));
485    }
486
487    #[test]
488    fn test_select_range_add_merges() {
489        let mut s = Selection::new();
490        s.select_range(SelectionRange::new(0, 50), SelectionMode::Add);
491        s.select_range(SelectionRange::new(40, 100), SelectionMode::Add);
492        assert_eq!(s.ranges().len(), 1);
493        assert_eq!(s.ranges()[0].end, 100);
494    }
495
496    #[test]
497    fn test_select_range_subtract() {
498        let mut s = Selection::new();
499        s.select_range(SelectionRange::new(0, 100), SelectionMode::Add);
500        s.select_range(SelectionRange::new(40, 60), SelectionMode::Subtract);
501        assert_eq!(s.ranges().len(), 2);
502        assert_eq!(s.ranges()[0], SelectionRange::new(0, 40));
503        assert_eq!(s.ranges()[1], SelectionRange::new(60, 100));
504    }
505
506    #[test]
507    fn test_selected_duration() {
508        let mut s = Selection::new();
509        s.select_range(SelectionRange::new(0, 50), SelectionMode::Add);
510        s.select_range(SelectionRange::new(100, 150), SelectionMode::Add);
511        assert_eq!(s.selected_duration(), 100);
512    }
513
514    // ------------------------------------------------------------------
515    // Selection – clip mode tests
516    // ------------------------------------------------------------------
517
518    #[test]
519    fn test_select_clip_replace() {
520        let mut s = Selection::new();
521        s.select_clip(1, SelectionMode::Add);
522        s.select_clip(2, SelectionMode::Add);
523        s.select_clip(3, SelectionMode::Replace);
524        assert_eq!(s.selected_clips().len(), 1);
525        assert!(s.is_clip_selected(3));
526    }
527
528    #[test]
529    fn test_select_clip_add_no_duplicates() {
530        let mut s = Selection::new();
531        s.select_clip(5, SelectionMode::Add);
532        s.select_clip(5, SelectionMode::Add);
533        assert_eq!(s.selected_clips().len(), 1);
534    }
535
536    #[test]
537    fn test_select_clip_toggle() {
538        let mut s = Selection::new();
539        s.select_clip(7, SelectionMode::Toggle);
540        assert!(s.is_clip_selected(7));
541        s.select_clip(7, SelectionMode::Toggle);
542        assert!(!s.is_clip_selected(7));
543    }
544
545    #[test]
546    fn test_select_all_in_range() {
547        let clips = vec![
548            TimelineClipRef::new(1, 0, 50),
549            TimelineClipRef::new(2, 50, 100),
550            TimelineClipRef::new(3, 200, 300),
551        ];
552        let mut s = Selection::new();
553        s.select_all_in_range(&clips, SelectionRange::new(0, 100));
554        assert!(s.is_clip_selected(1));
555        assert!(s.is_clip_selected(2));
556        assert!(!s.is_clip_selected(3));
557    }
558
559    // ------------------------------------------------------------------
560    // Selection – general tests
561    // ------------------------------------------------------------------
562
563    #[test]
564    fn test_clear() {
565        let mut s = Selection::new();
566        s.select_clip(1, SelectionMode::Add);
567        s.select_range(SelectionRange::new(0, 100), SelectionMode::Add);
568        s.select_track(0, SelectionMode::Add);
569        s.clear();
570        assert!(s.is_empty());
571    }
572
573    #[test]
574    fn test_merge_overlapping_three_ranges() {
575        let mut s = Selection::new();
576        s.select_range(SelectionRange::new(0, 30), SelectionMode::Add);
577        s.select_range(SelectionRange::new(20, 60), SelectionMode::Add);
578        s.select_range(SelectionRange::new(55, 100), SelectionMode::Add);
579        assert_eq!(s.ranges().len(), 1);
580        assert_eq!(s.ranges()[0], SelectionRange::new(0, 100));
581    }
582
583    #[test]
584    fn test_is_empty_initially() {
585        let s = Selection::new();
586        assert!(s.is_empty());
587    }
588
589    #[test]
590    fn test_track_selection() {
591        let mut s = Selection::new();
592        s.select_track(0, SelectionMode::Add);
593        s.select_track(1, SelectionMode::Add);
594        assert!(s.is_track_selected(0));
595        assert!(s.is_track_selected(1));
596        s.select_track(0, SelectionMode::Subtract);
597        assert!(!s.is_track_selected(0));
598    }
599
600    // ------------------------------------------------------------------
601    // SelectionItem tests
602    // ------------------------------------------------------------------
603
604    #[test]
605    fn test_selection_item_same_track_true() {
606        let a = SelectionItem::new(1, 0);
607        let b = SelectionItem::new(2, 0);
608        assert!(a.same_track(&b));
609    }
610
611    #[test]
612    fn test_selection_item_same_track_false() {
613        let a = SelectionItem::new(1, 0);
614        let b = SelectionItem::new(2, 1);
615        assert!(!a.same_track(&b));
616    }
617
618    // ------------------------------------------------------------------
619    // EditSelection tests
620    // ------------------------------------------------------------------
621
622    #[test]
623    fn test_edit_selection_add_and_contains() {
624        let mut sel = EditSelection::new();
625        sel.add(SelectionItem::new(10, 0));
626        assert!(sel.contains(10));
627        assert!(!sel.contains(99));
628    }
629
630    #[test]
631    fn test_edit_selection_add_no_duplicate() {
632        let mut sel = EditSelection::new();
633        sel.add(SelectionItem::new(5, 1));
634        sel.add(SelectionItem::new(5, 1));
635        assert_eq!(sel.count(), 1);
636    }
637
638    #[test]
639    fn test_edit_selection_remove() {
640        let mut sel = EditSelection::new();
641        sel.add(SelectionItem::new(3, 0));
642        sel.remove(3);
643        assert!(!sel.contains(3));
644        assert_eq!(sel.count(), 0);
645    }
646
647    #[test]
648    fn test_edit_selection_clear() {
649        let mut sel = EditSelection::new();
650        sel.add(SelectionItem::new(1, 0));
651        sel.add(SelectionItem::new(2, 1));
652        sel.clear();
653        assert_eq!(sel.count(), 0);
654    }
655
656    #[test]
657    fn test_edit_selection_tracks_unique() {
658        let mut sel = EditSelection::new();
659        sel.add(SelectionItem::new(1, 0));
660        sel.add(SelectionItem::new(2, 0));
661        sel.add(SelectionItem::new(3, 1));
662        let tracks = sel.tracks();
663        assert_eq!(tracks.len(), 2);
664        assert!(tracks.contains(&0));
665        assert!(tracks.contains(&1));
666    }
667
668    #[test]
669    fn test_edit_selection_count() {
670        let mut sel = EditSelection::new();
671        assert_eq!(sel.count(), 0);
672        sel.add(SelectionItem::new(7, 2));
673        assert_eq!(sel.count(), 1);
674    }
675
676    // ------------------------------------------------------------------
677    // LinkedSelection tests
678    // ------------------------------------------------------------------
679
680    #[test]
681    fn test_linked_selection_no_links() {
682        let ls = LinkedSelection::new();
683        assert!(ls.linked_clips(42).is_empty());
684    }
685
686    #[test]
687    fn test_linked_selection_add_group() {
688        let mut ls = LinkedSelection::new();
689        ls.add_linked_group(vec![1, 2, 3]);
690        let linked = ls.linked_clips(1);
691        assert!(linked.contains(&2));
692        assert!(linked.contains(&3));
693        assert!(!linked.contains(&1)); // self excluded
694    }
695
696    #[test]
697    fn test_linked_selection_multiple_groups() {
698        let mut ls = LinkedSelection::new();
699        ls.add_linked_group(vec![10, 11]);
700        ls.add_linked_group(vec![20, 21, 22]);
701        // clip 10 should only know about 11
702        assert_eq!(ls.linked_clips(10), vec![11]);
703        // clip 22 should know about 20, 21
704        let linked = ls.linked_clips(22);
705        assert!(linked.contains(&20));
706        assert!(linked.contains(&21));
707    }
708
709    #[test]
710    fn test_linked_selection_empty_group_ignored() {
711        let mut ls = LinkedSelection::new();
712        ls.add_linked_group(vec![]);
713        assert_eq!(ls.groups.len(), 0);
714    }
715
716    #[test]
717    fn test_linked_selection_clip_not_in_any_group() {
718        let mut ls = LinkedSelection::new();
719        ls.add_linked_group(vec![1, 2]);
720        assert!(ls.linked_clips(99).is_empty());
721    }
722}