1use edit::EditProject;
13use leptos::prelude::*;
14
15pub const SPEED_PRESETS: [f64; 5] = [0.5, 1.0, 1.5, 2.0, 4.0];
18
19#[must_use]
22pub fn speed_label(timescale: f64) -> String {
23 format!("{timescale}×")
24}
25
26#[must_use]
28pub fn is_active_speed(current: f64, preset: f64) -> bool {
29 (current - preset).abs() < 1e-6
30}
31
32#[must_use]
34pub fn selected_segment_speed(project: &EditProject, index: usize) -> Option<f64> {
35 project.segments.get(index).map(|s| s.timescale)
36}
37
38type ProjectSignal = Option<RwSignal<Option<EditProject>>>;
39type HistoryStore = Option<StoredValue<Option<edit::History>>>;
40
41fn speed_panel(
43 index: usize,
44 current: f64,
45 project: ProjectSignal,
46 history: HistoryStore,
47) -> AnyView {
48 let presets = SPEED_PRESETS
49 .into_iter()
50 .map(|preset| {
51 let mut class = String::from("clip-speed-preset");
52 if is_active_speed(current, preset) {
53 class.push_str(" clip-speed-preset--active");
54 }
55 view! {
56 <button
57 class=class
58 on:click=move |_| {
59 if let (Some(p), Some(h)) = (project, history) {
60 crate::editor_edits::set_speed(p, h, index, preset);
61 }
62 }
63 >
64 {speed_label(preset)}
65 </button>
66 }
67 })
68 .collect_view();
69 view! {
70 <div class="clip-inspector-section">
71 <h3 class="clip-inspector-title">"Speed"</h3>
72 <p class="clip-inspector-current">
73 {format!("Clip {} · {}", index + 1, speed_label(current))}
74 </p>
75 <div class="clip-speed-presets">{presets}</div>
76 </div>
77 }
78 .into_any()
79}
80
81#[component]
84pub fn ClipInspector() -> impl IntoView {
85 let project = use_context::<RwSignal<Option<EditProject>>>();
86 let history = use_context::<StoredValue<Option<edit::History>>>();
87 let selection = use_context::<RwSignal<Option<usize>>>().unwrap_or_else(|| RwSignal::new(None));
88 view! {
89 <div class="clip-inspector">
90 {move || match selection.get() {
91 None => view! {
92 <p class="clip-inspector-empty">"Select a clip to edit its speed."</p>
93 }
94 .into_any(),
95 Some(index) => {
96 let current = project
97 .and_then(|s| s.get().as_ref().and_then(|p| selected_segment_speed(p, index)))
98 .unwrap_or(1.0);
99 speed_panel(index, current, project, history)
100 }
101 }}
102 </div>
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use edit::{ClipRef, EditProject, TimelineSegment};
110 use std::path::PathBuf;
111
112 fn project() -> EditProject {
113 EditProject::from_recording(ClipRef::new(
114 PathBuf::from("/tmp/rec.mp4"),
115 1920,
116 1080,
117 30,
118 900,
119 ))
120 }
121
122 #[test]
123 fn speed_labels_drop_trailing_zero() {
124 assert_eq!(speed_label(1.0), "1×");
125 assert_eq!(speed_label(2.0), "2×");
126 assert_eq!(speed_label(0.5), "0.5×");
127 assert_eq!(speed_label(1.5), "1.5×");
128 }
129
130 #[test]
131 fn active_speed_uses_tolerance() {
132 assert!(is_active_speed(1.0, 1.0));
133 assert!(is_active_speed(2.0 + 1e-9, 2.0));
134 assert!(!is_active_speed(1.0, 2.0));
135 }
136
137 #[test]
138 fn selected_speed_reads_timescale() {
139 let mut p = project();
140 p.segments = vec![
141 TimelineSegment::new(0, 300),
142 TimelineSegment::with_speed(300, 900, 2.0),
143 ];
144 assert!((selected_segment_speed(&p, 0).unwrap() - 1.0).abs() < 1e-9);
145 assert!((selected_segment_speed(&p, 1).unwrap() - 2.0).abs() < 1e-9);
146 assert_eq!(selected_segment_speed(&p, 2), None);
147 }
148}