1use crate::clip::{Clip, ClipId, Clipboard};
7use crate::error::{EditError, EditResult};
8use crate::timeline::Timeline;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum EditMode {
13 Normal,
15 Ripple,
17 Roll,
19 Slip,
21 Slide,
23}
24
25pub struct TimelineEditor {
27 clipboard: Clipboard,
29 history: EditHistory,
31}
32
33impl TimelineEditor {
34 #[must_use]
36 pub fn new() -> Self {
37 Self {
38 clipboard: Clipboard::new(),
39 history: EditHistory::new(),
40 }
41 }
42
43 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 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 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 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 let clipboard_clips = self.clipboard.clips.clone();
100 for mut clip in clipboard_clips {
101 clip.timeline_start += offset;
102
103 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 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 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 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 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 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 let new_id = timeline.next_clip_id;
164 timeline.next_clip_id += 1;
165
166 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 timeline.add_clip(track_index, second_half)?;
175
176 Ok(new_id)
177 }
178
179 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 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 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 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 let track_count = timeline.tracks.len();
242
243 timeline.remove_clip(clip_id)?;
245
246 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 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 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 #[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 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 if clip_a.timeline_end() != clip_b.timeline_start {
324 return Err(EditError::InvalidEdit("Clips are not adjacent".to_string()));
325 }
326
327 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 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 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 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 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 if delta < 0 {
396 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 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 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 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 #[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 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 #[must_use]
463 pub fn clipboard(&self) -> &Clipboard {
464 &self.clipboard
465 }
466
467 #[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#[derive(Debug)]
482pub struct EditHistory {
483 history: Vec<EditAction>,
485 current: usize,
487 max_size: usize,
489}
490
491impl EditHistory {
492 #[must_use]
494 pub fn new() -> Self {
495 Self {
496 history: Vec::new(),
497 current: 0,
498 max_size: 100,
499 }
500 }
501
502 pub fn push(&mut self, action: EditAction) {
504 self.history.truncate(self.current);
506
507 self.history.push(action);
509 self.current += 1;
510
511 if self.history.len() > self.max_size {
513 self.history.remove(0);
514 self.current -= 1;
515 }
516 }
517
518 #[must_use]
520 pub fn can_undo(&self) -> bool {
521 self.current > 0
522 }
523
524 #[must_use]
526 pub fn can_redo(&self) -> bool {
527 self.current < self.history.len()
528 }
529
530 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 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 pub fn clear(&mut self) {
553 self.history.clear();
554 self.current = 0;
555 }
556
557 #[must_use]
559 pub fn len(&self) -> usize {
560 self.history.len()
561 }
562
563 #[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#[derive(Clone, Debug)]
578pub enum EditAction {
579 AddClip {
581 track: usize,
583 clip: Clip,
585 },
586 RemoveClip {
588 track: usize,
590 clip_id: ClipId,
592 },
593 MoveClip {
595 clip_id: ClipId,
597 old_start: i64,
599 new_start: i64,
601 },
602 TrimClip {
604 clip_id: ClipId,
606 old_in: i64,
608 old_out: i64,
610 new_in: i64,
612 new_out: i64,
614 },
615 SplitClip {
617 original_id: ClipId,
619 new_id: ClipId,
621 position: i64,
623 },
624}
625
626#[derive(Clone, Debug)]
628pub struct SnapSettings {
629 pub enabled: bool,
631 pub snap_to_playhead: bool,
633 pub snap_to_clips: bool,
635 pub snap_to_markers: bool,
637 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 #[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 #[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}