1use crate::engine::tracking::TrackJob;
12use crate::media::Backend;
13use crate::model::{Id, Project};
14use crate::theme::Palette;
15use crate::ui::tools::{glyph_text_button, Glyph};
16use eframe::egui::{self, Button, DragValue};
17
18pub struct TrackState {
19 pub clip: Option<Id>,
21 pub cx: f32,
23 pub cy: f32,
24 pub hw: f32,
25 pub hh: f32,
26 pub search: f32,
28 pub refresh: u32,
30 pub name: String,
31 pub status: String,
32 job: Option<TrackJob>,
33 points: Vec<(f32, f32, f32)>,
34}
35
36impl Default for TrackState {
37 fn default() -> Self {
38 Self {
39 clip: None,
40 cx: 0.0,
41 cy: 0.0,
42 hw: 32.0,
43 hh: 32.0,
44 search: 24.0,
45 refresh: 10,
46 name: String::new(),
47 status: String::new(),
48 job: None,
49 points: Vec::new(),
50 }
51 }
52}
53
54impl TrackState {
55 pub fn box_rect(&self) -> (f32, f32, f32, f32) {
57 (self.cx, self.cy, self.hw, self.hh)
58 }
59}
60
61fn target(project: &Project, state: &TrackState, selection: &[Id]) -> Option<Id> {
63 state
64 .clip
65 .filter(|&id| project.clip(id).is_some())
66 .or_else(|| selection.iter().copied().find(|&id| project.clip(id).is_some_and(|c| c.is_visual())))
67}
68
69pub fn show(
70 ui: &mut egui::Ui,
71 state: &mut TrackState,
72 project: &mut Project,
73 selection: &[Id],
74 backend: Backend,
75 _palette: &Palette,
76 undo: &mut dyn FnMut(&Project),
77) -> bool {
78 let mut changed = false;
79 if state.job.as_mut().is_some_and(|j| j.poll()) {
81 ui.ctx().request_repaint();
82 } else if let Some(j) = state.job.take() {
83 state.status = format!("tracked {} points", j.points.len());
84 state.points = j.points;
85 }
86 let running = state.job.is_some();
87 let target = target(project, state, selection);
88
89 ui.strong("Tracking");
90 ui.horizontal(|ui| {
91 ui.label("Clip");
92 let name = target.and_then(|id| project.clip(id)).map(|c| c.name.clone()).unwrap_or_else(|| "—".into());
93 ui.monospace(name);
94 if ui.add_enabled(!running, Button::new("Use selected")).clicked() {
95 state.clip = selection.first().copied();
96 }
97 });
98 egui::Grid::new("track_params").num_columns(2).show(ui, |ui| {
99 let rows: [(&str, f32, f32); 5] = [
100 ("X", -10000.0, 10000.0),
101 ("Y", -10000.0, 10000.0),
102 ("Width", 4.0, 2000.0),
103 ("Height", 4.0, 2000.0),
104 ("Search radius", 1.0, 512.0),
105 ];
106 for (label, lo, hi) in rows {
107 ui.label(label);
108 let (v, half) = match label {
110 "X" => (&mut state.cx, false),
111 "Y" => (&mut state.cy, false),
112 "Width" => (&mut state.hw, true),
113 "Height" => (&mut state.hh, true),
114 _ => (&mut state.search, false),
115 };
116 let mut shown = if half { *v * 2.0 } else { *v };
117 if ui.add_enabled(!running, DragValue::new(&mut shown).range(lo..=hi).speed(1.0).suffix(" px")).changed() {
118 *v = if half { shown / 2.0 } else { shown };
119 }
120 ui.end_row();
121 }
122 ui.label("Refresh every");
123 ui.add_enabled(!running, DragValue::new(&mut state.refresh).range(0..=240).suffix(" frames"))
124 .on_hover_text("Re-grab the template that often (0 = never, best for a rigid feature)");
125 ui.end_row();
126 });
127
128 let mut go = None;
129 ui.horizontal(|ui| {
130 ui.add_enabled_ui(!running && target.is_some(), |ui| {
131 if glyph_text_button(ui, Glyph::Target, "Track forward").clicked() {
132 go = Some(false);
133 }
134 if glyph_text_button(ui, Glyph::Target, "Track backward").clicked() {
135 go = Some(true);
136 }
137 });
138 if running && ui.button("Cancel").clicked() {
139 state.job = None; state.status = "cancelled".into();
141 }
142 });
143 if let (Some(backward), Some(id)) = (go, target) {
144 state.points.clear();
145 state.status.clear();
146 match TrackJob::start(project, id, state.box_rect(), state.search, state.refresh, backward, backend) {
147 Ok(j) => state.job = Some(j),
148 Err(e) => state.status = e,
149 }
150 }
151 if let Some(j) = &state.job {
152 ui.add(egui::ProgressBar::new(j.progress).show_percentage());
153 }
154
155 let have = state.points.len() >= 2;
156 ui.separator();
157 ui.horizontal(|ui| {
158 ui.label("Name");
159 ui.add(egui::TextEdit::singleline(&mut state.name).desired_width(110.0).hint_text("Path"));
160 if ui.add_enabled(have, Button::new("Save as path")).clicked() {
161 undo(project);
162 project.add_path(state.name.clone(), state.points.clone());
163 state.status = format!("saved {} points to the project paths", state.points.len());
164 changed = true;
165 }
166 if ui.add_enabled(have && target.is_some(), Button::new("Apply to clip")).clicked() {
167 undo(project);
168 if let Some(id) = target {
169 changed = project.apply_path(id, &state.points);
170 state.status =
171 if changed { "keyframed the clip's position".into() } else { "the track is too short".to_string() };
172 }
173 }
174 });
175 if !state.status.is_empty() {
176 ui.weak(&state.status);
177 }
178 changed
179}