diff options
| author | cashmere <cashmere@autistici.org> | 2026-07-28 19:41:16 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-07-28 19:41:16 +0200 |
| commit | f5a82152394fde4d8f740f664ab7737b82861618 (patch) | |
| tree | 5a51b181abead1ce03d2dbfc12b77918d3be0e1b /rust/crates/commands/src/timeline | |
| parent | e837e7504379e100ff3b56282577ec67bb88bec9 (diff) | |
| download | opencut-f5a82152394fde4d8f740f664ab7737b82861618.tar.gz | |
feat: GPUI desktop port foundation
- scene crate: full project/scene/element model, serde round-trip
proven against the web SerializedProject shape
- transfer crate: .ocp zip container, cross-compat with web fflate
archives proven in both directions
- storage crate: filesystem project store replacing IndexedDB,
.ocp import/export
- media crate: ffmpeg-next 8 decoder (frames, audio, thumbnails,
waveform summaries)
- playback crate: wall-clock anchored, frame-rounded playback clock
- gpu crate: native texture readback; compositor renders headless
offscreen on Vulkan (spike test)
- flake devshell: rust toolchain, GPUI native deps, ffmpeg dev,
bindgen clang args; GPUI hello-world builds
- port plan in notes/gpui-port.md
Diffstat (limited to 'rust/crates/commands/src/timeline')
13 files changed, 829 insertions, 0 deletions
diff --git a/rust/crates/commands/src/timeline/element/delete_elements.rs b/rust/crates/commands/src/timeline/element/delete_elements.rs new file mode 100644 index 00000000..bdcb5b6d --- /dev/null +++ b/rust/crates/commands/src/timeline/element/delete_elements.rs @@ -0,0 +1,64 @@ +use scene::{ElementRef, Project, SceneTracks, Track}; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; + +fn remove_track_elements(track: &mut Track, elements: &[ElementRef]) { + if !elements.iter().any(|target| target.track_id == track.id()) { + return; + } + track.elements_mut().retain(|element| { + !elements + .iter() + .any(|target| target.track_id == track.id() && target.element_id == element.base().id) + }); +} + +fn remove_elements_from_tracks(tracks: &mut SceneTracks, elements: &[ElementRef]) { + for track in &mut tracks.overlay { + remove_track_elements(track, elements); + } + remove_track_elements(&mut tracks.main, elements); + for track in &mut tracks.audio { + remove_track_elements(track, elements); + } +} + +pub struct DeleteElementsCommand { + elements: Vec<ElementRef>, + saved_state: Option<SceneTracks>, +} + +impl DeleteElementsCommand { + pub fn new(elements: Vec<ElementRef>) -> Self { + Self { + elements, + saved_state: None, + } + } +} + +impl Command for DeleteElementsCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + self.saved_state = Some(scene.tracks.clone()); + remove_elements_from_tracks(&mut scene.tracks, &self.elements); + + Ok(Some(CommandResult::element_selection(Vec::new()))) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_state else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.tracks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/timeline/element/mod.rs b/rust/crates/commands/src/timeline/element/mod.rs new file mode 100644 index 00000000..2ba806e1 --- /dev/null +++ b/rust/crates/commands/src/timeline/element/mod.rs @@ -0,0 +1,3 @@ +pub mod delete_elements; + +pub use delete_elements::DeleteElementsCommand; diff --git a/rust/crates/commands/src/timeline/element/move_elements.rs b/rust/crates/commands/src/timeline/element/move_elements.rs new file mode 100644 index 00000000..fc6425d1 --- /dev/null +++ b/rust/crates/commands/src/timeline/element/move_elements.rs @@ -0,0 +1,145 @@ +use scene::{Element, Project, SceneTracks, Track}; +use time::MediaTime; +use timeline::TrackType; +use timeline::placement::build_empty_track; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; +use crate::tracks::{all_tracks_mut, find_track}; + +#[derive(Clone, Debug, PartialEq)] +pub struct PlannedTrackCreation { + pub id: String, + pub track_type: TrackType, + pub index: usize, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct PlannedElementMove { + pub source_track_id: String, + pub target_track_id: String, + pub element_id: String, + pub new_start_time: MediaTime, +} + +fn insert_track_at_display_index( + tracks: &mut SceneTracks, + track: Track, + insert_index: usize, +) { + if matches!(track, Track::Audio { .. }) { + let audio_insert_index = insert_index + .saturating_sub(tracks.overlay.len() + 1) + .min(tracks.audio.len()); + tracks.audio.insert(audio_insert_index, track); + return; + } + + let overlay_insert_index = insert_index.min(tracks.overlay.len()); + tracks.overlay.insert(overlay_insert_index, track); +} + +fn validate_compatibility(element: &Element, track: &Track) -> Result<(), CommandError> { + timeline::placement::validate_element_track_compatibility(element, track) + .map_err(CommandError::IncompatiblePlacement) +} + +pub struct MoveElementCommand { + moves: Vec<PlannedElementMove>, + create_tracks: Vec<PlannedTrackCreation>, + saved_state: Option<SceneTracks>, +} + +impl MoveElementCommand { + pub fn new(moves: Vec<PlannedElementMove>, create_tracks: Vec<PlannedTrackCreation>) -> Self { + Self { + moves, + create_tracks, + saved_state: None, + } + } +} + +impl Command for MoveElementCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + let saved_state = scene.tracks.clone(); + self.saved_state = Some(saved_state.clone()); + + let mut create_tracks = self.create_tracks.clone(); + create_tracks.sort_by_key(|creation| creation.index); + for creation in &create_tracks { + let track = build_empty_track(&creation.id, creation.track_type, None); + insert_track_at_display_index(&mut scene.tracks, track, creation.index); + } + + let mut moved_elements = Vec::new(); + for planned_move in &self.moves { + let source_track = find_track(&saved_state, &planned_move.source_track_id); + let source_element = source_track.and_then(|track| { + track + .elements() + .iter() + .find(|element| element.base().id == planned_move.element_id) + }); + let Some(source_element) = source_element else { + return Err(CommandError::SourceNotFound); + }; + + let Some(target_track) = find_track(&scene.tracks, &planned_move.target_track_id) + else { + return Err(CommandError::TargetTrackNotFound); + }; + + validate_compatibility(source_element, target_track)?; + + let mut moved = source_element.clone(); + moved.base_mut().start_time = planned_move.new_start_time; + moved_elements.push((planned_move.target_track_id.clone(), moved)); + } + + let moved_ids: Vec<&str> = self + .moves + .iter() + .map(|planned_move| planned_move.element_id.as_str()) + .collect(); + + for track in all_tracks_mut(&mut scene.tracks) { + let track_id = track.id().to_string(); + let mut elements = std::mem::take(track.elements_mut()); + elements.retain(|element| !moved_ids.contains(&element.base().id.as_str())); + elements.extend( + moved_elements + .iter() + .filter(|(target_track_id, _)| *target_track_id == track_id) + .map(|(_, element)| element.clone()), + ); + *track.elements_mut() = elements; + } + + Ok(Some(CommandResult::element_selection( + self.moves + .iter() + .map(|planned_move| scene::ElementRef { + track_id: planned_move.target_track_id.clone(), + element_id: planned_move.element_id.clone(), + }) + .collect(), + ))) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_state else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.tracks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/timeline/element/shift_split_remainder.rs b/rust/crates/commands/src/timeline/element/shift_split_remainder.rs new file mode 100644 index 00000000..be2f78e4 --- /dev/null +++ b/rust/crates/commands/src/timeline/element/shift_split_remainder.rs @@ -0,0 +1,58 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use scene::{ElementRef, Project}; +use time::MediaTime; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::timeline::element::move_elements::{MoveElementCommand, PlannedElementMove}; + +pub struct ShiftSplitRemainderCommand { + right_side: Rc<RefCell<Vec<ElementRef>>>, + new_start_time: MediaTime, + inner: Option<MoveElementCommand>, +} + +impl ShiftSplitRemainderCommand { + pub fn new(right_side: Rc<RefCell<Vec<ElementRef>>>, new_start_time: MediaTime) -> Self { + Self { + right_side, + new_start_time, + inner: None, + } + } +} + +impl Command for ShiftSplitRemainderCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let right_side = self.right_side.borrow().clone(); + if right_side.is_empty() { + return Ok(None); + } + + let mut inner = MoveElementCommand::new( + right_side + .iter() + .map(|element| PlannedElementMove { + source_track_id: element.track_id.clone(), + target_track_id: element.track_id.clone(), + element_id: element.element_id.clone(), + new_start_time: self.new_start_time, + }) + .collect(), + Vec::new(), + ); + let result = inner.execute(project)?; + self.inner = Some(inner); + Ok(result) + } + + fn undo(&mut self, project: &mut Project) { + if let Some(inner) = &mut self.inner { + inner.undo(project); + } + } +} diff --git a/rust/crates/commands/src/timeline/element/split_elements.rs b/rust/crates/commands/src/timeline/element/split_elements.rs new file mode 100644 index 00000000..0da90792 --- /dev/null +++ b/rust/crates/commands/src/timeline/element/split_elements.rs @@ -0,0 +1,225 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use scene::{AudioElement, Element, ElementRef, Project, RetimeConfig, SceneTracks}; +use time::MediaTime; +use uuid::Uuid; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; +use crate::tracks::all_tracks_mut; + +const MIN_RETIME_RATE: f64 = 0.01; +const MAX_RETIME_RATE: f64 = 5.0; +const DEFAULT_RETIME_RATE: f64 = 1.0; + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum RetainSide { + #[default] + Both, + Left, + Right, +} + +fn clamp_retime_rate(rate: f64) -> f64 { + if !rate.is_finite() || rate <= 0.0 { + return DEFAULT_RETIME_RATE; + } + rate.clamp(MIN_RETIME_RATE, MAX_RETIME_RATE) +} + +fn source_span_at_clip_time(clip_time: MediaTime, retime: Option<RetimeConfig>) -> f64 { + let rate = clamp_retime_rate(retime.map(|config| config.rate).unwrap_or(1.0)); + (clip_time.as_ticks() as f64 * rate).max(0.0) +} + +fn round_to_ticks(time: f64) -> MediaTime { + MediaTime::from_ticks(time.round() as i64) +} + +fn retime_of(element: &Element) -> Option<RetimeConfig> { + match element { + Element::Video(video) => video.retime, + Element::Audio(AudioElement::Upload(upload)) => upload.retime, + Element::Audio(AudioElement::Library(library)) => library.retime, + _ => None, + } +} + +pub struct SplitElementsCommand { + elements: Vec<ElementRef>, + split_time: MediaTime, + retain_side: RetainSide, + saved_state: Option<SceneTracks>, + right_side: Rc<RefCell<Vec<ElementRef>>>, +} + +impl SplitElementsCommand { + pub fn new(elements: Vec<ElementRef>, split_time: MediaTime, retain_side: RetainSide) -> Self { + Self { + elements, + split_time, + retain_side, + saved_state: None, + right_side: Rc::new(RefCell::new(Vec::new())), + } + } + + pub fn right_side_handle(&self) -> Rc<RefCell<Vec<ElementRef>>> { + Rc::clone(&self.right_side) + } + + pub fn right_side_elements(&self) -> Vec<ElementRef> { + self.right_side.borrow().clone() + } + + fn split_element( + &self, + track_id: &str, + element: Element, + right_side: &mut Vec<ElementRef>, + ) -> Vec<Element> { + let start = element.base().start_time; + let end = start + element.base().duration; + + if self.split_time <= start || self.split_time >= end { + return vec![element]; + } + + let relative_time = self.split_time - start; + let left_visible_duration = relative_time; + let right_visible_duration = element.base().duration - relative_time; + let retime = retime_of(&element); + + let left_source_span = round_to_ticks(source_span_at_clip_time( + left_visible_duration, + retime, + )); + let total_source_span = + round_to_ticks(source_span_at_clip_time(element.base().duration, retime)); + let right_source_span = total_source_span - left_source_span; + + let (left_animations, right_animations) = animation::split_animations_at_time_with_options( + element.base().animations.as_ref(), + relative_time, + true, + ); + + let left_trim_end = element.base().trim_end + right_source_span; + let right_trim_start = element.base().trim_start + left_source_span; + + match self.retain_side { + RetainSide::Left => { + let mut left = element; + { + let base = left.base_mut(); + base.duration = left_visible_duration; + base.trim_end = left_trim_end; + base.name = format!("{} (left)", base.name); + base.animations = left_animations; + } + vec![left] + } + RetainSide::Right => { + let mut right = element; + let new_id = Uuid::new_v4().to_string(); + right_side.push(ElementRef { + track_id: track_id.to_string(), + element_id: new_id.clone(), + }); + { + let base = right.base_mut(); + base.id = new_id; + base.start_time = self.split_time; + base.duration = right_visible_duration; + base.trim_start = right_trim_start; + base.name = format!("{} (right)", base.name); + base.animations = right_animations; + } + vec![right] + } + RetainSide::Both => { + let mut left = element; + let mut right = left.clone(); + let right_id = Uuid::new_v4().to_string(); + right_side.push(ElementRef { + track_id: track_id.to_string(), + element_id: right_id.clone(), + }); + { + let base = left.base_mut(); + base.duration = left_visible_duration; + base.trim_end = left_trim_end; + base.name = format!("{} (left)", base.name); + base.animations = left_animations; + } + { + let base = right.base_mut(); + base.id = right_id; + base.start_time = self.split_time; + base.duration = right_visible_duration; + base.trim_start = right_trim_start; + base.name = format!("{} (right)", base.name); + base.animations = right_animations; + } + vec![left, right] + } + } + } +} + +impl Command for SplitElementsCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + self.saved_state = Some(scene.tracks.clone()); + self.right_side.borrow_mut().clear(); + + for track in all_tracks_mut(&mut scene.tracks) { + let targets: Vec<&str> = self + .elements + .iter() + .filter(|target| target.track_id == track.id()) + .map(|target| target.element_id.as_str()) + .collect(); + if targets.is_empty() { + continue; + } + + let track_id = track.id().to_string(); + let previous = std::mem::take(track.elements_mut()); + let mut next = Vec::with_capacity(previous.len()); + { + let mut right_side = self.right_side.borrow_mut(); + for element in previous { + if !targets.contains(&element.base().id.as_str()) { + next.push(element); + continue; + } + next.extend(self.split_element(&track_id, element, &mut right_side)); + } + } + *track.elements_mut() = next; + } + + let right_side = self.right_side.borrow(); + if right_side.is_empty() { + return Ok(None); + } + Ok(Some(CommandResult::element_selection(right_side.clone()))) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_state else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.tracks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/timeline/element/update_elements.rs b/rust/crates/commands/src/timeline/element/update_elements.rs new file mode 100644 index 00000000..f5931bb7 --- /dev/null +++ b/rust/crates/commands/src/timeline/element/update_elements.rs @@ -0,0 +1,72 @@ +use scene::{ElementRef, Project, SceneTracks}; +use timeline::update_pipeline::{ElementPatch, ElementUpdateContext, apply_element_update}; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; +use crate::tracks::find_track_mut; + +#[derive(Clone, Debug, PartialEq)] +pub struct ElementUpdate { + pub track_id: String, + pub element_id: String, + pub patch: ElementPatch, +} + +pub struct UpdateElementsCommand { + updates: Vec<ElementUpdate>, + saved_state: Option<SceneTracks>, +} + +impl UpdateElementsCommand { + pub fn new(updates: Vec<ElementUpdate>) -> Self { + Self { + updates, + saved_state: None, + } + } +} + +impl Command for UpdateElementsCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + self.saved_state = Some(scene.tracks.clone()); + + for update in &self.updates { + let context_tracks = scene.tracks.clone(); + let context = ElementUpdateContext { + tracks: &context_tracks, + track_id: &update.track_id, + }; + let Some(track) = find_track_mut(&mut scene.tracks, &update.track_id) else { + continue; + }; + let Some(element) = track + .elements_mut() + .iter_mut() + .find(|element| element.base().id == update.element_id) + else { + continue; + }; + + let current = element.clone(); + *element = apply_element_update(current, update.patch.clone(), &context); + } + + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_state else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.tracks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/timeline/mod.rs b/rust/crates/commands/src/timeline/mod.rs new file mode 100644 index 00000000..532885ce --- /dev/null +++ b/rust/crates/commands/src/timeline/mod.rs @@ -0,0 +1,5 @@ +pub mod element; +pub mod track; +pub mod tracks_snapshot; + +pub use tracks_snapshot::TracksSnapshotCommand; diff --git a/rust/crates/commands/src/timeline/track/add_track.rs b/rust/crates/commands/src/timeline/track/add_track.rs new file mode 100644 index 00000000..d6e9bbae --- /dev/null +++ b/rust/crates/commands/src/timeline/track/add_track.rs @@ -0,0 +1,69 @@ +use scene::{Project, SceneTracks}; +use timeline::placement::{build_empty_track, get_default_insert_index_for_track}; +use timeline::TrackType; +use uuid::Uuid; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; + +pub struct AddTrackCommand { + track_type: TrackType, + index: Option<usize>, + track_id: String, + saved_state: Option<SceneTracks>, +} + +impl AddTrackCommand { + pub fn new(track_type: TrackType, index: Option<usize>) -> Self { + Self { + track_type, + index, + track_id: Uuid::new_v4().to_string(), + saved_state: None, + } + } + + pub fn track_id(&self) -> &str { + &self.track_id + } +} + +impl Command for AddTrackCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + self.saved_state = Some(scene.tracks.clone()); + + let insert_index = self.index.unwrap_or_else(|| { + get_default_insert_index_for_track(&scene.tracks, self.track_type) + }); + + let new_track = build_empty_track(&self.track_id, self.track_type, None); + if self.track_type == TrackType::Audio { + let audio_insert_index = insert_index + .saturating_sub(scene.tracks.overlay.len() + 1); + scene + .tracks + .audio + .insert(audio_insert_index.min(scene.tracks.audio.len()), new_track); + } else { + let overlay_insert_index = insert_index.min(scene.tracks.overlay.len()); + scene.tracks.overlay.insert(overlay_insert_index, new_track); + } + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_state else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.tracks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/timeline/track/mod.rs b/rust/crates/commands/src/timeline/track/mod.rs new file mode 100644 index 00000000..c133a12d --- /dev/null +++ b/rust/crates/commands/src/timeline/track/mod.rs @@ -0,0 +1,9 @@ +pub mod add_track; +pub mod remove_track; +pub mod toggle_track_mute; +pub mod toggle_track_visibility; + +pub use add_track::AddTrackCommand; +pub use remove_track::RemoveTrackCommand; +pub use toggle_track_mute::ToggleTrackMuteCommand; +pub use toggle_track_visibility::ToggleTrackVisibilityCommand; diff --git a/rust/crates/commands/src/timeline/track/remove_track.rs b/rust/crates/commands/src/timeline/track/remove_track.rs new file mode 100644 index 00000000..5a79b4ae --- /dev/null +++ b/rust/crates/commands/src/timeline/track/remove_track.rs @@ -0,0 +1,43 @@ +use scene::{Project, SceneTracks}; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; + +pub struct RemoveTrackCommand { + track_id: String, + saved_state: Option<SceneTracks>, +} + +impl RemoveTrackCommand { + pub fn new(track_id: impl Into<String>) -> Self { + Self { + track_id: track_id.into(), + saved_state: None, + } + } +} + +impl Command for RemoveTrackCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + self.saved_state = Some(scene.tracks.clone()); + scene.tracks.overlay.retain(|track| track.id() != self.track_id); + scene.tracks.audio.retain(|track| track.id() != self.track_id); + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_state else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.tracks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/timeline/track/toggle_track_mute.rs b/rust/crates/commands/src/timeline/track/toggle_track_mute.rs new file mode 100644 index 00000000..afaaf3f3 --- /dev/null +++ b/rust/crates/commands/src/timeline/track/toggle_track_mute.rs @@ -0,0 +1,49 @@ +use scene::{Project, SceneTracks, Track}; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; +use crate::tracks::find_track_mut; + +pub struct ToggleTrackMuteCommand { + track_id: String, + saved_state: Option<SceneTracks>, +} + +impl ToggleTrackMuteCommand { + pub fn new(track_id: impl Into<String>) -> Self { + Self { + track_id: track_id.into(), + saved_state: None, + } + } +} + +impl Command for ToggleTrackMuteCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + self.saved_state = Some(scene.tracks.clone()); + + match find_track_mut(&mut scene.tracks, &self.track_id) { + Some(Track::Video { muted, .. }) | Some(Track::Audio { muted, .. }) => { + *muted = !*muted; + } + _ => {} + } + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_state else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.tracks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/timeline/track/toggle_track_visibility.rs b/rust/crates/commands/src/timeline/track/toggle_track_visibility.rs new file mode 100644 index 00000000..79000a73 --- /dev/null +++ b/rust/crates/commands/src/timeline/track/toggle_track_visibility.rs @@ -0,0 +1,54 @@ +use scene::{Project, SceneTracks, Track}; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; +use crate::tracks::find_track_mut; + +pub struct ToggleTrackVisibilityCommand { + track_id: String, + saved_state: Option<SceneTracks>, +} + +impl ToggleTrackVisibilityCommand { + pub fn new(track_id: impl Into<String>) -> Self { + Self { + track_id: track_id.into(), + saved_state: None, + } + } +} + +impl Command for ToggleTrackVisibilityCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + let Some(scene) = active_scene_mut(project) else { + return Ok(None); + }; + + self.saved_state = Some(scene.tracks.clone()); + + match find_track_mut(&mut scene.tracks, &self.track_id) { + Some( + Track::Video { hidden, .. } + | Track::Text { hidden, .. } + | Track::Graphic { hidden, .. } + | Track::Effect { hidden, .. }, + ) => { + *hidden = !*hidden; + } + _ => {} + } + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + let Some(saved) = &self.saved_state else { + return; + }; + if let Some(scene) = active_scene_mut(project) { + scene.tracks = saved.clone(); + } + } +} diff --git a/rust/crates/commands/src/timeline/tracks_snapshot.rs b/rust/crates/commands/src/timeline/tracks_snapshot.rs new file mode 100644 index 00000000..5108e123 --- /dev/null +++ b/rust/crates/commands/src/timeline/tracks_snapshot.rs @@ -0,0 +1,33 @@ +use scene::{Project, SceneTracks}; + +use crate::command::{Command, CommandError, CommandResult}; +use crate::project_ext::active_scene_mut; + +pub struct TracksSnapshotCommand { + before: SceneTracks, + after: SceneTracks, +} + +impl TracksSnapshotCommand { + pub fn new(before: SceneTracks, after: SceneTracks) -> Self { + Self { before, after } + } +} + +impl Command for TracksSnapshotCommand { + fn execute( + &mut self, + project: &mut Project, + ) -> Result<Option<CommandResult>, CommandError> { + if let Some(scene) = active_scene_mut(project) { + scene.tracks = self.after.clone(); + } + Ok(None) + } + + fn undo(&mut self, project: &mut Project) { + if let Some(scene) = active_scene_mut(project) { + scene.tracks = self.before.clone(); + } + } +} |
