From f5a82152394fde4d8f740f664ab7737b82861618 Mon Sep 17 00:00:00 2001 From: cashmere Date: Tue, 28 Jul 2026 19:41:16 +0200 Subject: 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 --- rust/crates/timeline/src/placement/resolve.rs | 223 ++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 rust/crates/timeline/src/placement/resolve.rs (limited to 'rust/crates/timeline/src/placement/resolve.rs') diff --git a/rust/crates/timeline/src/placement/resolve.rs b/rust/crates/timeline/src/placement/resolve.rs new file mode 100644 index 00000000..e95bc93c --- /dev/null +++ b/rust/crates/timeline/src/placement/resolve.rs @@ -0,0 +1,223 @@ +use scene::{SceneTracks, Track}; +use time::MediaTime; + +use super::compatibility::get_track_type_for_element_type; +use super::insert_index::{ + get_default_insert_index_for_track, get_highest_insert_index_for_track, + resolve_preferred_new_track_placement, +}; +use super::main_track::enforce_main_track_start; +use super::overlap::can_place_time_spans_on_track; +use super::{ + InsertDirection, NewTrackPosition, PlacementResult, PlacementStrategy, PlacementSubject, + PlacementTimeSpan, VerticalDragDirection, ordered_tracks, +}; +use crate::types::TrackType; + +pub struct ResolveTrackPlacementParams { + pub tracks: SceneTracks, + pub subject: PlacementSubject, + pub time_spans: Vec, + pub strategy: PlacementStrategy, +} + +fn build_existing_track_result( + track: &Track, + track_index: usize, + tracks: &SceneTracks, + time_spans: &[PlacementTimeSpan], +) -> PlacementResult { + let first_span = time_spans.first(); + let requested_start_time = first_span + .map(|span| span.start_time) + .unwrap_or(MediaTime::ZERO); + let adjusted_start_time = enforce_main_track_start( + tracks, + track.id(), + requested_start_time, + first_span.and_then(|span| span.exclude_element_id.as_deref()), + ); + + PlacementResult::ExistingTrack { + track_id: track.id().to_string(), + track_index, + track_type: TrackType::of_track(track), + adjusted_start_time: (adjusted_start_time != requested_start_time) + .then_some(adjusted_start_time), + } +} + +fn build_new_track_result( + track_type: TrackType, + insert_index: usize, + insert_position: Option, +) -> PlacementResult { + PlacementResult::NewTrack { + track_type, + insert_index, + insert_position, + } +} + +fn find_first_available_track_index( + tracks: &[&Track], + track_type: TrackType, + time_spans: &[PlacementTimeSpan], +) -> Option { + tracks.iter().position(|track| { + TrackType::of_track(track) == track_type + && can_place_time_spans_on_track(track.elements(), time_spans) + }) +} + +fn resolve_always_new_track( + tracks: &SceneTracks, + track_type: TrackType, + position: NewTrackPosition, +) -> PlacementResult { + let insert_index = match position { + NewTrackPosition::Highest => get_highest_insert_index_for_track(tracks, track_type), + NewTrackPosition::Default => get_default_insert_index_for_track(tracks, track_type), + }; + + build_new_track_result(track_type, insert_index, None) +} + +fn get_insert_direction( + hover_direction: InsertDirection, + vertical_drag_direction: Option, +) -> InsertDirection { + match vertical_drag_direction { + Some(VerticalDragDirection::Up) => InsertDirection::Above, + Some(VerticalDragDirection::Down) => InsertDirection::Below, + None => hover_direction, + } +} + +pub fn resolve_track_placement(params: &ResolveTrackPlacementParams) -> Option { + let tracks = ¶ms.tracks; + let ordered = ordered_tracks(tracks); + let track_type = match params.subject { + PlacementSubject::TrackType(track_type) => track_type, + PlacementSubject::ElementType(element_type) => { + get_track_type_for_element_type(element_type) + } + }; + let time_spans = ¶ms.time_spans; + + match ¶ms.strategy { + PlacementStrategy::Explicit { track_id } => { + let track_index = ordered.iter().position(|track| track.id() == track_id)?; + let track = ordered[track_index]; + if TrackType::of_track(track) != track_type { + return None; + } + + Some(build_existing_track_result( + track, + track_index, + tracks, + time_spans, + )) + } + PlacementStrategy::FirstAvailable => { + if let Some(existing_index) = + find_first_available_track_index(&ordered, track_type, time_spans) + { + return Some(build_existing_track_result( + ordered[existing_index], + existing_index, + tracks, + time_spans, + )); + } + + Some(resolve_always_new_track( + tracks, + track_type, + NewTrackPosition::Highest, + )) + } + PlacementStrategy::PreferIndex { + track_index, + hover_direction, + vertical_drag_direction, + create_new_track_only, + } => { + let preferred_track = ordered.get(*track_index); + let is_preferred_compatible = preferred_track + .is_some_and(|track| TrackType::of_track(track) == track_type); + let can_use_existing = !create_new_track_only + && is_preferred_compatible + && preferred_track.is_some_and(|track| { + can_place_time_spans_on_track(track.elements(), time_spans) + }); + if can_use_existing { + return Some(build_existing_track_result( + preferred_track?, + *track_index, + tracks, + time_spans, + )); + } + + let direction = get_insert_direction( + *hover_direction, + if is_preferred_compatible { + None + } else { + *vertical_drag_direction + }, + ); + let placement = resolve_preferred_new_track_placement( + tracks, + track_type, + *track_index, + direction, + ); + Some(build_new_track_result( + track_type, + placement.insert_index, + placement.insert_position, + )) + } + PlacementStrategy::AboveSource { source_track_index } => { + let above_track = source_track_index + .checked_sub(1) + .and_then(|above_index| ordered.get(above_index)); + if let Some(above_track) = above_track + && TrackType::of_track(above_track) == track_type + && can_place_time_spans_on_track(above_track.elements(), time_spans) + { + return Some(build_existing_track_result( + above_track, + source_track_index - 1, + tracks, + time_spans, + )); + } + + if let Some(first_available_index) = + find_first_available_track_index(&ordered, track_type, time_spans) + { + return Some(build_existing_track_result( + ordered[first_available_index], + first_available_index, + tracks, + time_spans, + )); + } + + Some(build_new_track_result( + track_type, + get_highest_insert_index_for_track(tracks, track_type), + None, + )) + } + PlacementStrategy::AlwaysNew { position } => Some(resolve_always_new_track( + tracks, + track_type, + *position, + )), + } +} -- cgit v1.3.1