Skip to main content

oximedia_edit/
edit.rs

1//! Edit operations for the timeline.
2//!
3//! This module provides various editing operations including cut, copy, paste,
4//! split, trim, and advanced edits like ripple, roll, slip, and slide.
5
6use crate::clip::{Clip, ClipId, Clipboard};
7use crate::error::{EditError, EditResult};
8use crate::timeline::Timeline;
9
10/// Edit mode for different types of editing operations.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum EditMode {
13    /// Normal edit - no ripple.
14    Normal,
15    /// Ripple edit - all clips after the edit point move.
16    Ripple,
17    /// Roll edit - adjust the boundary between two clips.
18    Roll,
19    /// Slip edit - adjust in/out points without changing position.
20    Slip,
21    /// Slide edit - move clip without changing duration.
22    Slide,
23}
24
25/// Timeline editing operations.
26pub struct TimelineEditor {
27    /// Clipboard for cut/copy/paste.
28    clipboard: Clipboard,
29    /// Edit history for undo/redo.
30    history: EditHistory,
31}
32
33impl TimelineEditor {
34    /// Create a new timeline editor.
35    #[must_use]
36    pub fn new() -> Self {
37        Self {
38            clipboard: Clipboard::new(),
39            history: EditHistory::new(),
40        }
41    }
42
43    /// Cut selected clips to clipboard.
44    pub fn cut(&mut self, timeline: &mut Timeline) -> EditResult<()> {
45        let selected_ids: Vec<ClipId> = timeline.selection.clips.clone();
46        if selected_ids.is_empty() {
47            return Ok(());
48        }
49
50        let mut clips = Vec::new();
51        for clip_id in &selected_ids {
52            let clip = timeline.remove_clip(*clip_id)?;
53            clips.push(clip);
54        }
55
56        self.clipboard.cut(clips);
57        timeline.selection.clear();
58
59        Ok(())
60    }
61
62    /// Copy selected clips to clipboard.
63    pub fn copy(&mut self, timeline: &Timeline) -> EditResult<()> {
64        let selected_ids: Vec<ClipId> = timeline.selection.clips.clone();
65        if selected_ids.is_empty() {
66            return Ok(());
67        }
68
69        let mut clips = Vec::new();
70        for clip_id in &selected_ids {
71            if let Some(clip) = timeline.get_clip(*clip_id) {
72                clips.push(clip.clone());
73            }
74        }
75
76        self.clipboard.copy(clips);
77
78        Ok(())
79    }
80
81    /// Paste clips from clipboard at playhead position.
82    pub fn paste(&mut self, timeline: &mut Timeline) -> EditResult<Vec<ClipId>> {
83        if self.clipboard.is_empty() {
84            return Ok(Vec::new());
85        }
86
87        let paste_position = timeline.playhead;
88        let mut new_clip_ids = Vec::new();
89
90        // Calculate offset based on first clip in clipboard
91        let time_range = self.clipboard.time_range();
92        let offset = if let Some((min_start, _)) = time_range {
93            paste_position - min_start
94        } else {
95            0
96        };
97
98        // Group clips by track and find appropriate tracks
99        let clipboard_clips = self.clipboard.clips.clone();
100        for mut clip in clipboard_clips {
101            clip.timeline_start += offset;
102
103            // Find or create appropriate track
104            let track_index = timeline
105                .tracks
106                .iter()
107                .position(|t| t.track_type.matches_clip(clip.clip_type))
108                .ok_or_else(|| {
109                    EditError::InvalidEdit("No suitable track for clip type".to_string())
110                })?;
111
112            let clip_id = timeline.add_clip(track_index, clip)?;
113            new_clip_ids.push(clip_id);
114        }
115
116        // Select pasted clips
117        timeline.selection.clear();
118        for clip_id in &new_clip_ids {
119            timeline.selection.add(*clip_id);
120        }
121
122        Ok(new_clip_ids)
123    }
124
125    /// Split clip at playhead position.
126    pub fn split_at_playhead(&mut self, timeline: &mut Timeline) -> EditResult<Vec<ClipId>> {
127        let position = timeline.playhead;
128        let mut new_clip_ids = Vec::new();
129
130        // Find all clips at playhead position
131        let clips_at_playhead: Vec<(usize, ClipId)> = timeline
132            .tracks
133            .iter()
134            .enumerate()
135            .filter_map(|(track_idx, track)| {
136                track.get_clip_at(position).map(|clip| (track_idx, clip.id))
137            })
138            .collect();
139
140        // Split each clip
141        for (_track_idx, clip_id) in clips_at_playhead {
142            let new_clip_id = self.split_clip(timeline, clip_id, position)?;
143            new_clip_ids.push(new_clip_id);
144        }
145
146        Ok(new_clip_ids)
147    }
148
149    /// Split a specific clip at a position.
150    pub fn split_clip(
151        &mut self,
152        timeline: &mut Timeline,
153        clip_id: ClipId,
154        position: i64,
155    ) -> EditResult<ClipId> {
156        let (track_index, _) = timeline
157            .clip_map
158            .get(&clip_id)
159            .copied()
160            .ok_or(EditError::ClipNotFound(clip_id))?;
161
162        // Get next clip ID and increment before mutable borrowing
163        let new_id = timeline.next_clip_id;
164        timeline.next_clip_id += 1;
165
166        // Get the clip and split it
167        let clip = timeline
168            .get_clip_mut(clip_id)
169            .ok_or(EditError::ClipNotFound(clip_id))?;
170
171        let second_half = clip.split_at(position, new_id)?;
172
173        // Add the second half to the timeline
174        timeline.add_clip(track_index, second_half)?;
175
176        Ok(new_id)
177    }
178
179    /// Delete selected clips.
180    pub fn delete_selection(&mut self, timeline: &mut Timeline) -> EditResult<()> {
181        let selected_ids: Vec<ClipId> = timeline.selection.clips.clone();
182        for clip_id in selected_ids {
183            timeline.remove_clip(clip_id)?;
184        }
185        timeline.selection.clear();
186        Ok(())
187    }
188
189    /// Trim clip in point.
190    pub fn trim_in(
191        &mut self,
192        timeline: &mut Timeline,
193        clip_id: ClipId,
194        delta: i64,
195    ) -> EditResult<()> {
196        let clip = timeline
197            .get_clip_mut(clip_id)
198            .ok_or(EditError::ClipNotFound(clip_id))?;
199        clip.trim_in(delta)?;
200        Ok(())
201    }
202
203    /// Trim clip out point.
204    pub fn trim_out(
205        &mut self,
206        timeline: &mut Timeline,
207        clip_id: ClipId,
208        delta: i64,
209    ) -> EditResult<()> {
210        let clip = timeline
211            .get_clip_mut(clip_id)
212            .ok_or(EditError::ClipNotFound(clip_id))?;
213        clip.trim_out(delta)?;
214        Ok(())
215    }
216
217    /// Ripple delete - delete clips and move following clips backward.
218    pub fn ripple_delete(
219        &mut self,
220        timeline: &mut Timeline,
221        track_index: usize,
222        clip_id: ClipId,
223    ) -> EditResult<()> {
224        let track = timeline
225            .get_track(track_index)
226            .ok_or(EditError::InvalidTrackIndex(
227                track_index,
228                timeline.tracks.len(),
229            ))?;
230
231        let clip = track
232            .clips
233            .iter()
234            .find(|c| c.id == clip_id)
235            .ok_or(EditError::ClipNotFound(clip_id))?;
236
237        let clip_start = clip.timeline_start;
238        let clip_duration = clip.timeline_duration;
239
240        // Get the track count before getting a mutable reference
241        let track_count = timeline.tracks.len();
242
243        // Remove the clip
244        timeline.remove_clip(clip_id)?;
245
246        // Move all following clips on this track backward
247        let track = timeline
248            .get_track_mut(track_index)
249            .ok_or(EditError::InvalidTrackIndex(track_index, track_count))?;
250
251        for clip in &mut track.clips {
252            if clip.timeline_start >= clip_start {
253                clip.timeline_start -= clip_duration;
254            }
255        }
256
257        timeline.rebuild_clip_map();
258        Ok(())
259    }
260
261    /// Ripple trim - trim a clip and move following clips.
262    pub fn ripple_trim(
263        &mut self,
264        timeline: &mut Timeline,
265        clip_id: ClipId,
266        delta: i64,
267        trim_end: bool,
268    ) -> EditResult<()> {
269        let (track_index, _) = timeline
270            .clip_map
271            .get(&clip_id)
272            .copied()
273            .ok_or(EditError::ClipNotFound(clip_id))?;
274
275        let clip = timeline
276            .get_clip_mut(clip_id)
277            .ok_or(EditError::ClipNotFound(clip_id))?;
278
279        let clip_end = clip.timeline_end();
280
281        if trim_end {
282            clip.trim_out(delta)?;
283        } else {
284            clip.trim_in(delta)?;
285        }
286
287        // Move following clips
288        if trim_end {
289            let track_count = timeline.tracks.len();
290            let track = timeline
291                .get_track_mut(track_index)
292                .ok_or(EditError::InvalidTrackIndex(track_index, track_count))?;
293
294            for clip in &mut track.clips {
295                if clip.timeline_start >= clip_end {
296                    clip.timeline_start += delta;
297                }
298            }
299        }
300
301        timeline.rebuild_clip_map();
302        Ok(())
303    }
304
305    /// Roll edit - adjust the boundary between two adjacent clips.
306    #[allow(clippy::similar_names)]
307    pub fn roll_edit(
308        &mut self,
309        timeline: &mut Timeline,
310        clip_a_id: ClipId,
311        clip_b_id: ClipId,
312        delta: i64,
313    ) -> EditResult<()> {
314        // Get both clips
315        let clip_a = timeline
316            .get_clip(clip_a_id)
317            .ok_or(EditError::ClipNotFound(clip_a_id))?;
318        let clip_b = timeline
319            .get_clip(clip_b_id)
320            .ok_or(EditError::ClipNotFound(clip_b_id))?;
321
322        // Verify clips are adjacent
323        if clip_a.timeline_end() != clip_b.timeline_start {
324            return Err(EditError::InvalidEdit("Clips are not adjacent".to_string()));
325        }
326
327        // Trim first clip out point
328        let clip_a = timeline
329            .get_clip_mut(clip_a_id)
330            .ok_or(EditError::ClipNotFound(clip_a_id))?;
331        clip_a.trim_out(delta)?;
332
333        // Trim second clip in point
334        let clip_b = timeline
335            .get_clip_mut(clip_b_id)
336            .ok_or(EditError::ClipNotFound(clip_b_id))?;
337        clip_b.trim_in(-delta)?;
338
339        Ok(())
340    }
341
342    /// Slip edit - adjust in/out points without changing timeline position.
343    pub fn slip_edit(
344        &mut self,
345        timeline: &mut Timeline,
346        clip_id: ClipId,
347        delta: i64,
348    ) -> EditResult<()> {
349        let clip = timeline
350            .get_clip_mut(clip_id)
351            .ok_or(EditError::ClipNotFound(clip_id))?;
352
353        let new_in = clip.source_in + delta;
354        let new_out = clip.source_out + delta;
355
356        // Validate new range
357        if new_in < 0 || new_out > clip.max_source_duration() {
358            return Err(EditError::InvalidEdit(
359                "Slip would exceed source bounds".to_string(),
360            ));
361        }
362
363        clip.source_in = new_in;
364        clip.source_out = new_out;
365
366        Ok(())
367    }
368
369    /// Slide edit - move clip without changing duration, adjusting adjacent clips.
370    pub fn slide_edit(
371        &mut self,
372        timeline: &mut Timeline,
373        track_index: usize,
374        clip_id: ClipId,
375        delta: i64,
376    ) -> EditResult<()> {
377        let track = timeline
378            .get_track(track_index)
379            .ok_or(EditError::InvalidTrackIndex(
380                track_index,
381                timeline.tracks.len(),
382            ))?;
383
384        let clip_index = track
385            .clips
386            .iter()
387            .position(|c| c.id == clip_id)
388            .ok_or(EditError::ClipNotFound(clip_id))?;
389
390        let clip = &track.clips[clip_index];
391        let new_start = clip.timeline_start + delta;
392        let new_end = clip.timeline_end() + delta;
393
394        // Check if we can slide (need adjacent clips with enough room)
395        if delta < 0 {
396            // Sliding left
397            if clip_index > 0 {
398                let prev_clip = &track.clips[clip_index - 1];
399                if prev_clip.timeline_end() > new_start {
400                    return Err(EditError::InvalidEdit(
401                        "Cannot slide: not enough room".to_string(),
402                    ));
403                }
404            }
405        } else if delta > 0 {
406            // Sliding right
407            if clip_index < track.clips.len() - 1 {
408                let next_clip = &track.clips[clip_index + 1];
409                if next_clip.timeline_start < new_end {
410                    return Err(EditError::InvalidEdit(
411                        "Cannot slide: not enough room".to_string(),
412                    ));
413                }
414            }
415        }
416
417        // Perform the slide
418        let clip = timeline
419            .get_clip_mut(clip_id)
420            .ok_or(EditError::ClipNotFound(clip_id))?;
421        clip.timeline_start = new_start;
422
423        Ok(())
424    }
425
426    /// Set clip speed.
427    pub fn set_speed(
428        &mut self,
429        timeline: &mut Timeline,
430        clip_id: ClipId,
431        speed: f64,
432    ) -> EditResult<()> {
433        if speed <= 0.0 {
434            return Err(EditError::InvalidEdit("Speed must be positive".to_string()));
435        }
436
437        let clip = timeline
438            .get_clip_mut(clip_id)
439            .ok_or(EditError::ClipNotFound(clip_id))?;
440
441        clip.speed = speed;
442
443        // Adjust timeline duration based on speed
444        #[allow(clippy::cast_possible_truncation)]
445        #[allow(clippy::cast_precision_loss)]
446        let new_duration = (clip.source_duration() as f64 / speed) as i64;
447        clip.timeline_duration = new_duration;
448
449        Ok(())
450    }
451
452    /// Reverse clip playback.
453    pub fn reverse_clip(&mut self, timeline: &mut Timeline, clip_id: ClipId) -> EditResult<()> {
454        let clip = timeline
455            .get_clip_mut(clip_id)
456            .ok_or(EditError::ClipNotFound(clip_id))?;
457        clip.reverse = !clip.reverse;
458        Ok(())
459    }
460
461    /// Get clipboard.
462    #[must_use]
463    pub fn clipboard(&self) -> &Clipboard {
464        &self.clipboard
465    }
466
467    /// Get edit history.
468    #[must_use]
469    pub fn history(&self) -> &EditHistory {
470        &self.history
471    }
472}
473
474impl Default for TimelineEditor {
475    fn default() -> Self {
476        Self::new()
477    }
478}
479
480/// Edit history for undo/redo operations.
481#[derive(Debug)]
482pub struct EditHistory {
483    /// History stack.
484    history: Vec<EditAction>,
485    /// Current position in history.
486    current: usize,
487    /// Maximum history size.
488    max_size: usize,
489}
490
491impl EditHistory {
492    /// Create a new edit history.
493    #[must_use]
494    pub fn new() -> Self {
495        Self {
496            history: Vec::new(),
497            current: 0,
498            max_size: 100,
499        }
500    }
501
502    /// Add an action to the history.
503    pub fn push(&mut self, action: EditAction) {
504        // Remove any actions after current position
505        self.history.truncate(self.current);
506
507        // Add new action
508        self.history.push(action);
509        self.current += 1;
510
511        // Limit history size
512        if self.history.len() > self.max_size {
513            self.history.remove(0);
514            self.current -= 1;
515        }
516    }
517
518    /// Check if undo is available.
519    #[must_use]
520    pub fn can_undo(&self) -> bool {
521        self.current > 0
522    }
523
524    /// Check if redo is available.
525    #[must_use]
526    pub fn can_redo(&self) -> bool {
527        self.current < self.history.len()
528    }
529
530    /// Get the action to undo.
531    pub fn undo(&mut self) -> Option<&EditAction> {
532        if self.can_undo() {
533            self.current -= 1;
534            Some(&self.history[self.current])
535        } else {
536            None
537        }
538    }
539
540    /// Get the action to redo.
541    pub fn redo(&mut self) -> Option<&EditAction> {
542        if self.can_redo() {
543            let action = &self.history[self.current];
544            self.current += 1;
545            Some(action)
546        } else {
547            None
548        }
549    }
550
551    /// Clear history.
552    pub fn clear(&mut self) {
553        self.history.clear();
554        self.current = 0;
555    }
556
557    /// Get history size.
558    #[must_use]
559    pub fn len(&self) -> usize {
560        self.history.len()
561    }
562
563    /// Check if history is empty.
564    #[must_use]
565    pub fn is_empty(&self) -> bool {
566        self.history.is_empty()
567    }
568}
569
570impl Default for EditHistory {
571    fn default() -> Self {
572        Self::new()
573    }
574}
575
576/// An edit action that can be undone/redone.
577#[derive(Clone, Debug)]
578pub enum EditAction {
579    /// Add clip.
580    AddClip {
581        /// Track index.
582        track: usize,
583        /// Clip data.
584        clip: Clip,
585    },
586    /// Remove clip.
587    RemoveClip {
588        /// Track index.
589        track: usize,
590        /// Clip ID.
591        clip_id: ClipId,
592    },
593    /// Move clip.
594    MoveClip {
595        /// Clip ID.
596        clip_id: ClipId,
597        /// Old position.
598        old_start: i64,
599        /// New position.
600        new_start: i64,
601    },
602    /// Trim clip.
603    TrimClip {
604        /// Clip ID.
605        clip_id: ClipId,
606        /// Old in/out points.
607        old_in: i64,
608        /// Old out point.
609        old_out: i64,
610        /// New in point.
611        new_in: i64,
612        /// New out point.
613        new_out: i64,
614    },
615    /// Split clip.
616    SplitClip {
617        /// Original clip ID.
618        original_id: ClipId,
619        /// New clip ID.
620        new_id: ClipId,
621        /// Split position.
622        position: i64,
623    },
624}
625
626/// Snap settings for timeline editing.
627#[derive(Clone, Debug)]
628pub struct SnapSettings {
629    /// Enable snapping.
630    pub enabled: bool,
631    /// Snap to playhead.
632    pub snap_to_playhead: bool,
633    /// Snap to clip edges.
634    pub snap_to_clips: bool,
635    /// Snap to markers.
636    pub snap_to_markers: bool,
637    /// Snap threshold (in timebase units).
638    pub threshold: i64,
639}
640
641impl Default for SnapSettings {
642    fn default() -> Self {
643        Self {
644            enabled: true,
645            snap_to_playhead: true,
646            snap_to_clips: true,
647            snap_to_markers: true,
648            threshold: 5,
649        }
650    }
651}
652
653impl SnapSettings {
654    /// Check if a position should snap to a target.
655    #[must_use]
656    pub fn should_snap(&self, position: i64, target: i64) -> bool {
657        if !self.enabled {
658            return false;
659        }
660        (position - target).abs() <= self.threshold
661    }
662
663    /// Get snap position if within threshold.
664    #[must_use]
665    pub fn snap_position(&self, position: i64, targets: &[i64]) -> i64 {
666        if !self.enabled {
667            return position;
668        }
669
670        for &target in targets {
671            if self.should_snap(position, target) {
672                return target;
673            }
674        }
675
676        position
677    }
678}