1#![allow(dead_code)]
8
9use std::collections::HashMap;
10use std::fmt;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum MarkerCategory {
19 General,
21 Comment,
23 Chapter,
25 SyncPoint,
27 Cue,
29 Error,
31 Review,
33 Todo,
35}
36
37impl fmt::Display for MarkerCategory {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 match self {
40 Self::General => write!(f, "general"),
41 Self::Comment => write!(f, "comment"),
42 Self::Chapter => write!(f, "chapter"),
43 Self::SyncPoint => write!(f, "sync"),
44 Self::Cue => write!(f, "cue"),
45 Self::Error => write!(f, "error"),
46 Self::Review => write!(f, "review"),
47 Self::Todo => write!(f, "todo"),
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
58pub struct EditMarker {
59 pub id: u64,
61 pub position: u64,
63 pub end_position: Option<u64>,
65 pub category: MarkerCategory,
67 pub label: String,
69 pub notes: String,
71 pub color: u32,
73 pub locked: bool,
75}
76
77impl EditMarker {
78 pub fn new(id: u64, position: u64, label: impl Into<String>) -> Self {
80 Self {
81 id,
82 position,
83 end_position: None,
84 category: MarkerCategory::General,
85 label: label.into(),
86 notes: String::new(),
87 color: 0xFFFF00FF, locked: false,
89 }
90 }
91
92 pub fn range(id: u64, start: u64, end: u64, label: impl Into<String>) -> Self {
94 Self {
95 id,
96 position: start,
97 end_position: Some(end),
98 category: MarkerCategory::General,
99 label: label.into(),
100 notes: String::new(),
101 color: 0x00FF00FF,
102 locked: false,
103 }
104 }
105
106 #[must_use]
108 pub fn with_category(mut self, cat: MarkerCategory) -> Self {
109 self.category = cat;
110 self
111 }
112
113 pub fn with_notes(mut self, notes: impl Into<String>) -> Self {
115 self.notes = notes.into();
116 self
117 }
118
119 #[must_use]
121 pub fn with_color(mut self, rgba: u32) -> Self {
122 self.color = rgba;
123 self
124 }
125
126 #[must_use]
128 pub fn duration(&self) -> u64 {
129 self.end_position
130 .map_or(0, |end| end.saturating_sub(self.position))
131 }
132
133 #[must_use]
135 pub fn is_range(&self) -> bool {
136 self.end_position.is_some()
137 }
138
139 #[must_use]
142 pub fn contains_position(&self, pos: u64) -> bool {
143 match self.end_position {
144 Some(end) => pos >= self.position && pos < end,
145 None => pos == self.position,
146 }
147 }
148
149 pub fn nudge(&mut self, offset: i64) {
151 if self.locked {
152 return;
153 }
154 let new_pos = (self.position as i64).saturating_add(offset).max(0) as u64;
155 if let Some(ref mut end) = self.end_position {
156 let delta = new_pos as i64 - self.position as i64;
157 *end = (*end as i64).saturating_add(delta).max(0) as u64;
158 }
159 self.position = new_pos;
160 }
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub struct SnapResult {
170 pub marker_id: u64,
172 pub position: u64,
174 pub distance: u64,
176}
177
178#[must_use]
181pub fn snap_to_nearest(markers: &[EditMarker], pos: u64, threshold: u64) -> Option<SnapResult> {
182 let mut best: Option<SnapResult> = None;
183 for m in markers {
184 let dist = pos.abs_diff(m.position);
185 if dist <= threshold && best.as_ref().map_or(true, |b| dist < b.distance) {
186 best = Some(SnapResult {
187 marker_id: m.id,
188 position: m.position,
189 distance: dist,
190 });
191 }
192 }
193 best
194}
195
196#[derive(Debug, Clone)]
202pub struct MarkerEditor {
203 markers: HashMap<u64, EditMarker>,
205 next_id: u64,
207}
208
209impl MarkerEditor {
210 #[must_use]
212 pub fn new() -> Self {
213 Self {
214 markers: HashMap::new(),
215 next_id: 1,
216 }
217 }
218
219 pub fn add_point(&mut self, position: u64, label: impl Into<String>) -> u64 {
221 let id = self.next_id;
222 self.next_id += 1;
223 self.markers
224 .insert(id, EditMarker::new(id, position, label));
225 id
226 }
227
228 pub fn add_range(&mut self, start: u64, end: u64, label: impl Into<String>) -> u64 {
230 let id = self.next_id;
231 self.next_id += 1;
232 self.markers
233 .insert(id, EditMarker::range(id, start, end, label));
234 id
235 }
236
237 pub fn remove(&mut self, id: u64) -> Option<EditMarker> {
239 self.markers.remove(&id)
240 }
241
242 #[must_use]
244 pub fn get(&self, id: u64) -> Option<&EditMarker> {
245 self.markers.get(&id)
246 }
247
248 pub fn get_mut(&mut self, id: u64) -> Option<&mut EditMarker> {
250 self.markers.get_mut(&id)
251 }
252
253 #[must_use]
255 pub fn sorted(&self) -> Vec<&EditMarker> {
256 let mut v: Vec<&EditMarker> = self.markers.values().collect();
257 v.sort_by_key(|m| m.position);
258 v
259 }
260
261 #[must_use]
263 pub fn filter_by_category(&self, cat: MarkerCategory) -> Vec<&EditMarker> {
264 self.markers
265 .values()
266 .filter(|m| m.category == cat)
267 .collect()
268 }
269
270 pub fn nudge_all(&mut self, offset: i64) {
272 for marker in self.markers.values_mut() {
273 marker.nudge(offset);
274 }
275 }
276
277 pub fn delete_by_category(&mut self, cat: MarkerCategory) -> usize {
279 let to_remove: Vec<u64> = self
280 .markers
281 .values()
282 .filter(|m| m.category == cat)
283 .map(|m| m.id)
284 .collect();
285 let count = to_remove.len();
286 for id in to_remove {
287 self.markers.remove(&id);
288 }
289 count
290 }
291
292 #[must_use]
294 pub fn count(&self) -> usize {
295 self.markers.len()
296 }
297
298 #[must_use]
300 pub fn is_empty(&self) -> bool {
301 self.markers.is_empty()
302 }
303
304 pub fn clear(&mut self) {
306 self.markers.clear();
307 }
308
309 #[must_use]
311 pub fn markers_at(&self, pos: u64) -> Vec<&EditMarker> {
312 self.markers
313 .values()
314 .filter(|m| m.contains_position(pos))
315 .collect()
316 }
317}
318
319impl Default for MarkerEditor {
320 fn default() -> Self {
321 Self::new()
322 }
323}
324
325#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test_marker_category_display() {
335 assert_eq!(MarkerCategory::Chapter.to_string(), "chapter");
336 assert_eq!(MarkerCategory::Todo.to_string(), "todo");
337 }
338
339 #[test]
340 fn test_edit_marker_point() {
341 let m = EditMarker::new(1, 1000, "Take 1");
342 assert_eq!(m.position, 1000);
343 assert!(!m.is_range());
344 assert_eq!(m.duration(), 0);
345 }
346
347 #[test]
348 fn test_edit_marker_range() {
349 let m = EditMarker::range(1, 100, 500, "Scene");
350 assert!(m.is_range());
351 assert_eq!(m.duration(), 400);
352 }
353
354 #[test]
355 fn test_edit_marker_contains_position_point() {
356 let m = EditMarker::new(1, 50, "x");
357 assert!(m.contains_position(50));
358 assert!(!m.contains_position(51));
359 }
360
361 #[test]
362 fn test_edit_marker_contains_position_range() {
363 let m = EditMarker::range(1, 100, 200, "r");
364 assert!(m.contains_position(100));
365 assert!(m.contains_position(199));
366 assert!(!m.contains_position(200));
367 assert!(!m.contains_position(99));
368 }
369
370 #[test]
371 fn test_edit_marker_nudge() {
372 let mut m = EditMarker::new(1, 100, "n");
373 m.nudge(50);
374 assert_eq!(m.position, 150);
375 }
376
377 #[test]
378 fn test_edit_marker_nudge_negative_clamps() {
379 let mut m = EditMarker::new(1, 10, "n");
380 m.nudge(-100);
381 assert_eq!(m.position, 0);
382 }
383
384 #[test]
385 fn test_edit_marker_nudge_locked() {
386 let mut m = EditMarker::new(1, 100, "locked");
387 m.locked = true;
388 m.nudge(50);
389 assert_eq!(m.position, 100);
390 }
391
392 #[test]
393 fn test_edit_marker_nudge_range() {
394 let mut m = EditMarker::range(1, 100, 200, "r");
395 m.nudge(50);
396 assert_eq!(m.position, 150);
397 assert_eq!(m.end_position, Some(250));
398 }
399
400 #[test]
401 fn test_snap_to_nearest_found() {
402 let markers = vec![EditMarker::new(1, 100, "a"), EditMarker::new(2, 200, "b")];
403 let result = snap_to_nearest(&markers, 105, 10);
404 assert!(result.is_some());
405 assert_eq!(result.expect("test expectation failed").marker_id, 1);
406 assert_eq!(result.expect("test expectation failed").distance, 5);
407 }
408
409 #[test]
410 fn test_snap_to_nearest_not_found() {
411 let markers = vec![EditMarker::new(1, 100, "a")];
412 let result = snap_to_nearest(&markers, 200, 10);
413 assert!(result.is_none());
414 }
415
416 #[test]
417 fn test_marker_editor_add_and_get() {
418 let mut ed = MarkerEditor::new();
419 let id = ed.add_point(500, "Point");
420 assert_eq!(ed.count(), 1);
421 assert!(ed.get(id).is_some());
422 assert_eq!(ed.get(id).expect("get should succeed").label, "Point");
423 }
424
425 #[test]
426 fn test_marker_editor_remove() {
427 let mut ed = MarkerEditor::new();
428 let id = ed.add_point(100, "X");
429 assert!(ed.remove(id).is_some());
430 assert!(ed.is_empty());
431 }
432
433 #[test]
434 fn test_marker_editor_sorted() {
435 let mut ed = MarkerEditor::new();
436 ed.add_point(300, "c");
437 ed.add_point(100, "a");
438 ed.add_point(200, "b");
439 let sorted = ed.sorted();
440 assert_eq!(sorted[0].position, 100);
441 assert_eq!(sorted[1].position, 200);
442 assert_eq!(sorted[2].position, 300);
443 }
444
445 #[test]
446 fn test_marker_editor_filter_by_category() {
447 let mut ed = MarkerEditor::new();
448 let id1 = ed.add_point(100, "ch1");
449 ed.get_mut(id1).expect("get_mut should succeed").category = MarkerCategory::Chapter;
450 let _id2 = ed.add_point(200, "gen");
451 let chapters = ed.filter_by_category(MarkerCategory::Chapter);
452 assert_eq!(chapters.len(), 1);
453 }
454
455 #[test]
456 fn test_marker_editor_nudge_all() {
457 let mut ed = MarkerEditor::new();
458 ed.add_point(100, "a");
459 ed.add_point(200, "b");
460 ed.nudge_all(50);
461 let sorted = ed.sorted();
462 assert_eq!(sorted[0].position, 150);
463 assert_eq!(sorted[1].position, 250);
464 }
465
466 #[test]
467 fn test_marker_editor_delete_by_category() {
468 let mut ed = MarkerEditor::new();
469 let id = ed.add_point(100, "err");
470 ed.get_mut(id).expect("get_mut should succeed").category = MarkerCategory::Error;
471 ed.add_point(200, "gen");
472 let removed = ed.delete_by_category(MarkerCategory::Error);
473 assert_eq!(removed, 1);
474 assert_eq!(ed.count(), 1);
475 }
476
477 #[test]
478 fn test_marker_editor_markers_at() {
479 let mut ed = MarkerEditor::new();
480 ed.add_range(100, 300, "range");
481 ed.add_point(200, "point");
482 let at_200 = ed.markers_at(200);
483 assert_eq!(at_200.len(), 2);
484 }
485
486 #[test]
487 fn test_marker_editor_clear() {
488 let mut ed = MarkerEditor::new();
489 ed.add_point(10, "x");
490 ed.clear();
491 assert!(ed.is_empty());
492 }
493
494 #[test]
495 fn test_marker_editor_default() {
496 let ed = MarkerEditor::default();
497 assert!(ed.is_empty());
498 }
499
500 #[test]
501 fn test_marker_builders() {
502 let m = EditMarker::new(1, 0, "t")
503 .with_category(MarkerCategory::Cue)
504 .with_notes("hello")
505 .with_color(0xFF0000FF);
506 assert_eq!(m.category, MarkerCategory::Cue);
507 assert_eq!(m.notes, "hello");
508 assert_eq!(m.color, 0xFF0000FF);
509 }
510}