1use std::ops::RangeInclusive;
2
3use derive_more::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
4use num::{BigInt, BigRational, FromPrimitive, ToPrimitive};
5use serde::{Deserialize, Serialize};
6
7use crate::wave_data::TimeRange;
8
9#[derive(
10 Debug,
11 Clone,
12 Copy,
13 Serialize,
14 Deserialize,
15 Add,
16 Sub,
17 Mul,
18 Neg,
19 AddAssign,
20 SubAssign,
21 PartialOrd,
22 PartialEq,
23)]
24pub struct Relative(pub f64);
25
26impl Relative {
27 #[must_use]
28 pub fn absolute(&self, range: &TimeRange) -> Absolute {
29 Absolute(
30 range.start.to_f64().unwrap()
31 + self.0
32 * range
33 .length()
34 .to_f64()
35 .expect("Failed to convert timestamp to f64"),
36 )
37 }
38
39 #[must_use]
40 pub fn inner(&self) -> f64 {
41 self.0
42 }
43
44 #[must_use]
45 pub fn min(&self, other: &Relative) -> Self {
46 Self(self.0.min(other.0))
47 }
48
49 #[must_use]
50 pub fn max(&self, other: &Relative) -> Self {
51 Self(self.0.max(other.0))
52 }
53}
54
55impl std::ops::Div for Relative {
56 type Output = Relative;
57
58 fn div(self, rhs: Self) -> Self::Output {
59 Self(self.0 / rhs.0)
60 }
61}
62
63#[derive(
64 Debug, Clone, Copy, Serialize, Deserialize, Add, Sub, Mul, Neg, Div, PartialOrd, PartialEq,
65)]
66pub struct Absolute(pub f64);
67
68impl Absolute {
69 #[must_use]
70 pub fn relative(&self, range: &TimeRange) -> Relative {
71 Relative(
72 (self.0 - range.start.to_f64().unwrap())
73 / range
74 .length()
75 .to_f64()
76 .expect("Failed to convert timestamp to f64"),
77 )
78 }
79
80 #[must_use]
81 pub fn inner(&self) -> f64 {
82 self.0
83 }
84}
85
86impl std::ops::Div for Absolute {
87 type Output = Absolute;
88
89 fn div(self, rhs: Self) -> Self::Output {
90 Self(self.0 / rhs.0)
91 }
92}
93
94impl From<&BigInt> for Absolute {
95 fn from(value: &BigInt) -> Self {
96 Self(value.to_f64().expect("Failed to convert timestamp to f64"))
97 }
98}
99
100fn default_edge_space() -> f64 {
101 0.2
102}
103
104fn default_min_width() -> Absolute {
105 Absolute(0.5)
106}
107
108#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
109pub struct Viewport {
110 pub curr_left: Relative,
111 pub curr_right: Relative,
112
113 target_left: Relative,
114 target_right: Relative,
115
116 move_start_left: Relative,
117 move_start_right: Relative,
118
119 move_duration: Option<f32>,
121 pub move_strategy: ViewportStrategy,
122 #[serde(skip, default = "default_edge_space")]
123 edge_space: f64,
124
125 #[serde(skip, default = "default_min_width")]
126 min_width: Absolute,
127}
128
129impl Default for Viewport {
130 fn default() -> Self {
131 Self {
132 curr_left: Relative(0.0),
133 curr_right: Relative(1.0),
134 target_left: Relative(0.0),
135 target_right: Relative(1.0),
136 move_start_left: Relative(0.0),
137 move_start_right: Relative(1.0),
138 move_duration: None,
139 move_strategy: ViewportStrategy::Instant,
140 edge_space: default_edge_space(),
141 min_width: default_min_width(),
142 }
143 }
144}
145
146impl Viewport {
147 #[must_use]
148 pub fn new() -> Self {
149 Self::default()
150 }
151 #[must_use]
152 pub fn left_edge_time(self, range: &TimeRange) -> BigInt {
153 BigInt::from(self.curr_left.absolute(range).0 as i64)
154 }
155 #[must_use]
156 pub fn right_edge_time(self, range: &TimeRange) -> BigInt {
157 BigInt::from(self.curr_right.absolute(range).0 as i64)
158 }
159
160 #[must_use]
162 pub fn as_absolute_time(&self, x: f64, view_width: f32, range: &TimeRange) -> Absolute {
163 let time_spacing = self.width_absolute(range) / f64::from(view_width);
164 self.curr_left.absolute(range) + time_spacing * x
165 }
166
167 #[must_use]
168 pub fn as_time_bigint(&self, x: f32, view_width: f32, range: &TimeRange) -> BigInt {
169 let Viewport {
170 curr_left: left,
171 curr_right: right,
172 ..
173 } = &self;
174
175 let big_right = BigRational::from_f64(right.absolute(range).0)
176 .unwrap_or_else(|| BigRational::from_u8(1).unwrap());
177 let big_left = BigRational::from_f64(left.absolute(range).0)
178 .unwrap_or_else(|| BigRational::from_u8(1).unwrap());
179 let big_width =
180 BigRational::from_f32(view_width).unwrap_or_else(|| BigRational::from_u8(1).unwrap());
181 let big_x = BigRational::from_f32(x).unwrap_or_else(|| BigRational::from_u8(1).unwrap());
182
183 let time = big_left.clone() + (big_right - big_left) / big_width * big_x;
184 time.round().to_integer()
185 }
186
187 #[must_use]
190 pub fn pixel_from_time(&self, time: &BigInt, view_width: f32, range: &TimeRange) -> f32 {
191 let distance_from_left = Absolute(time.to_f64().unwrap()) - self.curr_left.absolute(range);
192 let width = self.width_absolute(range);
193
194 (((distance_from_left / width).0) * f64::from(view_width)) as f32
195 }
196
197 #[must_use]
199 pub fn pixel_from_absolute_time(
200 &self,
201 time: Absolute,
202 view_width: f32,
203 range: &TimeRange,
204 ) -> f32 {
205 let distance_from_left = time - self.curr_left.absolute(range);
206 let width = self.width_absolute(range);
207
208 (((distance_from_left / width).0) * f64::from(view_width)) as f32
209 }
210
211 #[must_use]
217 pub fn clip_to(&self, old_range: &TimeRange, new_max_timestamp: &BigInt) -> Viewport {
218 let left_timestamp = self.curr_left.absolute(old_range);
219 let right_timestamp = self.curr_right.absolute(old_range);
220 let absolute_width = right_timestamp - left_timestamp;
221
222 let new_absolute_width = new_max_timestamp
223 .to_f64()
224 .expect("Failed to convert timestamp to f64")
225 * (2.0 * self.edge_space);
226 let (left, right) = if absolute_width.0 > new_absolute_width {
227 (Relative(-self.edge_space), Relative(1.0 + self.edge_space))
229 } else {
230 let new_num_ts_f64 = new_max_timestamp
232 .to_f64()
233 .expect("Failed to convert timestamp to f64");
234 let unmoved_left = Relative(left_timestamp.0 / new_num_ts_f64);
235 let unmoved_right = Relative((left_timestamp + absolute_width).0 / new_num_ts_f64);
236 if unmoved_right <= Relative(1.0 + self.edge_space) {
237 (unmoved_left, unmoved_right)
239 } else {
240 let relative_width = absolute_width.0 / new_num_ts_f64;
244 (
245 Relative(1.0 + self.edge_space - relative_width),
246 Relative(1.0 + self.edge_space),
247 )
248 }
249 };
250
251 Viewport {
252 curr_left: left,
253 curr_right: right,
254 target_left: left,
255 target_right: right,
256 move_start_left: left,
257 move_start_right: right,
258 move_duration: None,
259 move_strategy: self.move_strategy,
260 edge_space: self.edge_space,
261 min_width: self.min_width,
262 }
263 }
264
265 #[inline]
266 fn width(&self) -> Relative {
267 self.curr_right - self.curr_left
268 }
269
270 #[inline]
271 pub(crate) fn width_absolute(&self, range: &TimeRange) -> Absolute {
272 self.curr_right.absolute(range) - self.curr_left.absolute(range)
273 }
274
275 pub fn go_to_time(&mut self, center: &BigInt, range: &TimeRange) {
276 let center_point: Absolute = center.into();
277 let half_width = self.half_width_absolute(range);
278
279 let target_left = (center_point - half_width).relative(range);
280 let target_right = (center_point + half_width).relative(range);
281 self.set_viewport_to_clipped(target_left, target_right, range);
282 }
283
284 pub fn zoom_to_fit(&mut self) {
285 self.set_target_left(Relative(0.0));
286 self.set_target_right(Relative(1.0));
287 }
288
289 pub fn go_to_start(&mut self) {
290 let old_width = self.width();
291 self.set_target_left(Relative(0.0));
292 self.set_target_right(old_width);
293 }
294
295 pub fn go_to_end(&mut self) {
296 self.set_target_left(Relative(1.0) - self.width());
297 self.set_target_right(Relative(1.0));
298 }
299
300 pub(crate) fn zoom_to_time(&mut self, center: &BigInt, delta: f64, range: &TimeRange) {
301 let center = Absolute::from(center);
302 let half_width =
303 (self.curr_right.absolute(range) - self.curr_left.absolute(range)) * delta * 0.5;
304
305 self.set_viewport_to_clipped(
306 (center - half_width).relative(range),
307 (center + half_width).relative(range),
308 range,
309 );
310 }
311
312 pub(crate) fn handle_canvas_zoom(
313 &mut self,
314 mouse_ptr_timestamp: Option<BigInt>,
315 delta: f64,
316 range: &TimeRange,
317 ) {
318 let Viewport {
320 curr_left: left,
321 curr_right: right,
322 ..
323 } = &self;
324
325 let (target_left, target_right) = if let Some(mouse_location) =
326 mouse_ptr_timestamp.map(|t| Absolute::from(&t).relative(range))
327 {
328 (
329 (*left - mouse_location) / Relative(delta) + mouse_location,
330 (*right - mouse_location) / Relative(delta) + mouse_location,
331 )
332 } else {
333 let mid_point = self.midpoint();
334 let offset = self.half_width() * delta;
335
336 (mid_point - offset, mid_point + offset)
337 };
338
339 self.set_viewport_to_clipped(target_left, target_right, range);
340 }
341
342 pub fn handle_canvas_scroll(&mut self, deltay: f64) {
343 let scroll_step = -self.width() / Relative(50. * 20.);
346 let scaled_deltay = scroll_step * deltay;
347 self.set_viewport_to_clipped_no_width_check(
348 self.curr_left + scaled_deltay,
349 self.curr_right + scaled_deltay,
350 );
351 }
352
353 fn set_viewport_to_clipped(
354 &mut self,
355 target_left: Relative,
356 target_right: Relative,
357 range: &TimeRange,
358 ) {
359 let rel_min_width = self.min_width.relative(range);
360
361 if (target_right - target_left) <= rel_min_width + Relative(f64::EPSILON) {
362 let center = (target_left + target_right) * 0.5;
363 self.set_viewport_to_clipped_no_width_check(
364 center - rel_min_width,
365 center + rel_min_width,
366 );
367 } else {
368 self.set_viewport_to_clipped_no_width_check(target_left, target_right);
369 }
370 }
371
372 fn set_viewport_to_clipped_no_width_check(
373 &mut self,
374 target_left: Relative,
375 target_right: Relative,
376 ) {
377 let width = target_right - target_left;
378
379 let abs_min = Relative(-self.edge_space);
380 let abs_max = Relative(1.0 + self.edge_space);
381
382 let max_right = Relative(1.0) + width * self.edge_space;
383 let min_left = -width * self.edge_space;
384 if width > (abs_max - abs_min) {
385 self.set_target_left(abs_min);
386 self.set_target_right(abs_max);
387 } else if target_left < min_left {
388 self.set_target_left(min_left);
389 self.set_target_right(min_left + width);
390 } else if target_right > max_right {
391 self.set_target_left(max_right - width);
392 self.set_target_right(max_right);
393 } else {
394 self.set_target_left(target_left);
395 self.set_target_right(target_right);
396 }
397 }
398
399 #[inline]
400 fn midpoint(&self) -> Relative {
401 (self.curr_right + self.curr_left) * 0.5
402 }
403
404 #[inline]
405 fn half_width(&self) -> Relative {
406 self.width() * 0.5
407 }
408
409 #[inline]
410 fn half_width_absolute(&self, range: &TimeRange) -> Absolute {
411 (self.width() * 0.5).absolute(range)
412 }
413
414 pub(crate) fn zoom_to_range(&mut self, left: &BigInt, right: &BigInt, range: &TimeRange) {
415 self.set_viewport_to_clipped(
416 Absolute::from(left).relative(range),
417 Absolute::from(right).relative(range),
418 range,
419 );
420 }
421
422 pub(crate) fn go_to_cursor_if_not_in_view(
423 &mut self,
424 cursor: &BigInt,
425 range: &TimeRange,
426 ) -> bool {
427 let fcursor = cursor.into();
428 if fcursor <= self.curr_left.absolute(range) || fcursor >= self.curr_right.absolute(range) {
429 self.go_to_time_f64(fcursor, range);
430 true
431 } else {
432 false
433 }
434 }
435
436 pub(crate) fn go_to_time_f64(&mut self, center: Absolute, range: &TimeRange) {
437 let half_width = (self.curr_right.absolute(range) - self.curr_left.absolute(range)) / 2.;
438
439 self.set_viewport_to_clipped(
440 (center - half_width).relative(range),
441 (center + half_width).relative(range),
442 range,
443 );
444 }
445
446 fn set_target_left(&mut self, target_left: Relative) {
447 if let ViewportStrategy::Instant = self.move_strategy {
448 self.curr_left = target_left;
449 } else {
450 self.target_left = target_left;
451 self.move_start_left = self.curr_left;
452 self.move_duration = Some(0.);
453 }
454 }
455 fn set_target_right(&mut self, target_right: Relative) {
456 if let ViewportStrategy::Instant = self.move_strategy {
457 self.curr_right = target_right;
458 } else {
459 self.target_right = target_right;
460 self.move_start_right = self.curr_right;
461 self.move_duration = Some(0.);
462 }
463 }
464
465 pub fn move_viewport(&mut self, frame_time: f32) {
466 match &self.move_strategy {
467 ViewportStrategy::Instant => {
468 self.curr_left = self.target_left;
469 self.curr_right = self.target_right;
470 self.move_duration = None;
471 }
472 ViewportStrategy::EaseInOut { duration } => {
473 if let Some(move_duration) = &mut self.move_duration {
474 if *move_duration + frame_time >= *duration {
475 self.move_duration = None;
476 self.curr_left = self.target_left;
477 self.curr_right = self.target_right;
478 } else {
479 *move_duration += frame_time;
480
481 self.curr_left = Relative(ease_in_out_size(
482 self.move_start_left.0..=self.target_left.0,
483 f64::from(*move_duration) / f64::from(*duration),
484 ));
485 self.curr_right = Relative(ease_in_out_size(
486 self.move_start_right.0..=self.target_right.0,
487 f64::from(*move_duration) / f64::from(*duration),
488 ));
489 }
490 }
491 }
492 }
493 }
494
495 #[must_use]
496 pub fn is_moving(&self) -> bool {
497 self.move_duration.is_some()
498 }
499}
500
501#[must_use]
502fn ease_in_out_size(r: RangeInclusive<f64>, t: f64) -> f64 {
503 r.start() + ((r.end() - r.start()) * -((std::f64::consts::PI * t).cos() - 1.) * 0.5)
504}
505
506#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
507pub enum ViewportStrategy {
508 Instant,
509 EaseInOut { duration: f32 },
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515 use num::BigInt;
516
517 fn bi(n: i64) -> BigInt {
518 BigInt::from(n)
519 }
520
521 #[test]
522 fn ease_in_out_endpoints_and_mid() {
523 let r = 0.0..=10.0;
524 let s0 = ease_in_out_size(r.clone(), 0.0);
525 let s05 = ease_in_out_size(r.clone(), 0.5);
526 let s1 = ease_in_out_size(r.clone(), 1.0);
527 assert!((s0 - 0.0).abs() < 1e-12);
528 assert!((s05 - 5.0).abs() < 1e-12);
529 assert!((s1 - 10.0).abs() < 1e-12);
530 }
531
532 #[test]
533 fn relative_absolute_roundtrip() {
534 let range = TimeRange {
536 start: bi(0),
537 end: bi(1000),
538 };
539 let r = Relative(0.25);
540 let abs = r.absolute(&range);
541 assert!((abs.0 - 250.0).abs() < 1e-9);
543 let back = abs.relative(&range);
544 assert!((back.0 - r.0).abs() < 1e-9);
545
546 let range = TimeRange {
547 start: bi(500),
548 end: bi(1000),
549 };
550 let r = Relative(0.25);
551 let abs = r.absolute(&range);
552 assert!((abs.0 - 625.0).abs() < 1e-9);
554 let back = abs.relative(&range);
555 assert!((back.0 - r.0).abs() < 1e-9);
556 }
557
558 #[test]
559 fn pixel_from_time_consistency() {
560 let vp = Viewport::default();
561 let n = bi(1000);
562 let view_w = 1000.0_f32;
563
564 let time_offset = bi(0);
565 let time_abs = Absolute(250.0);
566 let range = TimeRange {
567 start: time_offset,
568 end: n.clone(),
569 };
570 let x1 = vp.pixel_from_absolute_time(time_abs, view_w, &range);
571 let x2 = vp.pixel_from_time(&bi(250), view_w, &range);
572 assert!((x1 - 250.0).abs() < 1e-6);
573 assert!((x2 - 250.0).abs() < 1e-6);
574
575 let range = TimeRange {
576 start: bi(500),
577 end: n,
578 };
579 let time_abs = Absolute(750.0); let x1 = vp.pixel_from_absolute_time(time_abs, view_w, &range);
581 let x2 = vp.pixel_from_time(&bi(750), view_w, &range);
582 assert!((x1 - 500.0).abs() < 1e-6);
583 assert!((x2 - 500.0).abs() < 1e-6);
584 }
585
586 #[test]
587 fn set_viewport_min_width_enforced() {
588 let mut vp = Viewport::default();
589 let range = TimeRange {
590 start: bi(0),
591 end: bi(1000),
592 };
593 let center = Relative(0.5);
595 vp.set_viewport_to_clipped(center, center, &range);
596 let rel_min = vp.min_width.relative(&range).0;
598 let width = (vp.curr_right - vp.curr_left).0;
599 assert!(
600 width + f64::EPSILON >= rel_min,
601 "width {width} < min {rel_min}"
602 );
603 }
604
605 #[test]
606 fn go_to_start_and_end_preserve_width() {
607 let mut vp = Viewport::default();
608 let w0 = (vp.curr_right - vp.curr_left).0;
609 vp.go_to_end();
610 let w1 = (vp.curr_right - vp.curr_left).0;
611 assert!((w0 - w1).abs() < 1e-12);
612 assert!((vp.curr_right.0 - 1.0).abs() < 1e-12);
613 vp.go_to_start();
614 let w2 = (vp.curr_right - vp.curr_left).0;
615 assert!((w0 - w2).abs() < 1e-12);
616 assert!((vp.curr_left.0 - 0.0).abs() < 1e-12);
617 }
618
619 #[test]
620 fn move_viewport_ease_in_out_reaches_target() {
621 let mut vp = Viewport {
622 move_strategy: ViewportStrategy::EaseInOut { duration: 0.3 },
623 ..Default::default()
624 };
625 let range = TimeRange {
626 start: bi(0),
627 end: bi(1000),
628 };
629 vp.set_viewport_to_clipped(Relative(0.1), Relative(0.3), &range);
631 let mut t = 0.0;
632 while vp.is_moving() && t < 1.0 {
634 vp.move_viewport(0.05);
635 t += 0.05;
636 }
637 assert!(!vp.is_moving());
638 assert!((vp.curr_left.0 - 0.1).abs() < 1e-6);
639 assert!((vp.curr_right.0 - 0.3).abs() < 1e-6);
640 }
641
642 #[test]
643 fn clip_to_does_not_invert_viewport() {
644 let mut vp = Viewport::default();
648 vp.curr_left = Relative(0.9027133537478365);
649 vp.curr_right = Relative(0.9455180041784441);
650 vp.target_left = vp.curr_left;
651 vp.target_right = vp.curr_right;
652
653 let old_max_timestamp = bi(122055);
654 let new_max_timestamp = bi(131445);
655 let old_range = TimeRange {
656 start: bi(0),
657 end: old_max_timestamp,
658 };
659
660 let clipped = vp.clip_to(&old_range, &new_max_timestamp);
661
662 assert!(
663 clipped.curr_left.0 < clipped.curr_right.0,
664 "Viewport inverted after clip_to: left={} >= right={}",
665 clipped.curr_left.0,
666 clipped.curr_right.0
667 );
668 }
669
670 #[test]
671 fn clip_to_preserves_valid_viewport_on_file_growth() {
672 let mut vp = Viewport::default();
674 vp.curr_left = Relative(0.8);
675 vp.curr_right = Relative(0.9);
676 vp.target_left = vp.curr_left;
677 vp.target_right = vp.curr_right;
678
679 let old_max_timestamp = bi(1000);
680 let new_max_timestamp = bi(2000); let old_range = TimeRange {
682 start: bi(0),
683 end: old_max_timestamp,
684 };
685
686 let clipped = vp.clip_to(&old_range, &new_max_timestamp);
687
688 assert!(
690 clipped.curr_left.0 < clipped.curr_right.0,
691 "Viewport inverted: left={} >= right={}",
692 clipped.curr_left.0,
693 clipped.curr_right.0
694 );
695
696 let old_width = 0.1; let expected_relative_width = old_width * 1000.0 / 2000.0; let actual_width = clipped.curr_right.0 - clipped.curr_left.0;
700 assert!(
701 (actual_width - expected_relative_width).abs() < 1e-9,
702 "Width not preserved: expected={}, actual={}",
703 expected_relative_width,
704 actual_width
705 );
706 }
707
708 #[test]
709 fn clip_to_handles_file_shrink_with_viewport_overshoot() {
710 let mut vp = Viewport::default();
713 vp.curr_left = Relative(0.95);
715 vp.curr_right = Relative(1.0);
716 vp.target_left = vp.curr_left;
717 vp.target_right = vp.curr_right;
718
719 let old_max_timestamp = bi(1000);
720 let new_max_timestamp = bi(500); let old_range = TimeRange {
722 start: bi(0),
723 end: old_max_timestamp,
724 };
725
726 let clipped = vp.clip_to(&old_range, &new_max_timestamp);
727
728 assert!(
730 clipped.curr_left.0 < clipped.curr_right.0,
731 "Viewport inverted: left={} >= right={}",
732 clipped.curr_left.0,
733 clipped.curr_right.0
734 );
735
736 assert!(
738 clipped.curr_left.0 >= -vp.edge_space,
739 "Left edge out of bounds: {}",
740 clipped.curr_left.0
741 );
742
743 let expected_relative_width = 50.0 / 500.0; let actual_width = clipped.curr_right.0 - clipped.curr_left.0;
746 assert!(
747 (actual_width - expected_relative_width).abs() < 1e-9,
748 "Width not preserved: expected={}, actual={}",
749 expected_relative_width,
750 actual_width
751 );
752 }
753}