diff --git a/Cargo.lock b/Cargo.lock index afbe68d..a3d08f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1669,7 +1669,7 @@ dependencies = [ "bytemuck", "byteorder", "color_quant", - "num-rational", + "num-rational 0.4.1", "num-traits", "png", ] @@ -1803,6 +1803,7 @@ dependencies = [ "env_logger", "getrandom", "log", + "percentage", "rand", "rayon", "reqwest", @@ -2066,6 +2067,41 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational 0.2.4", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -2076,6 +2112,29 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.1" @@ -2301,6 +2360,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "percentage" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd23b938276f14057220b707937bcb42fa76dda7560e57a2da30cb52d557937" +dependencies = [ + "num", +] + [[package]] name = "pin-project-lite" version = "0.2.9" diff --git a/Cargo.toml b/Cargo.toml index c2ba4d5..ca10ba1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ client = ["dep:reqwest", "dep:url"] server = ["dep:actix-cors", "dep:actix-web"] [dependencies] +percentage = "0.1.0" egui = "0.22.0" egui_extras = "0.22.0" eframe = { version = "0.22.0", default-features = false, features = [ diff --git a/src/app.rs b/src/app.rs index c24d166..999f700 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,6 +8,7 @@ use egui::{ Align2, Color32, NumExt, Pos2, Rect, RichText, ScrollArea, Slider, Stroke, TextStyle, Vec2, }; use egui_extras::{Column, TableBuilder}; +use percentage::{Percentage, PercentageInteger}; use serde::{Deserialize, Serialize}; use crate::data::{ @@ -158,9 +159,16 @@ struct Window { config: Config, } +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] +enum IntervalOrigin { + Zoom, + Pan, +} + #[derive(Debug, Clone, Default, Deserialize, Serialize)] -struct ZoomState { +struct IntervalState { levels: Vec, + origins: Vec, index: usize, } @@ -243,9 +251,9 @@ struct Context { show_controls: bool, #[serde(skip)] - zoom_state: ZoomState, + view_interval_history: IntervalState, #[serde(skip)] - interval_state: IntervalSelectState, + interval_select_state: IntervalSelectState, } #[derive(Default, Deserialize, Serialize)] @@ -1449,62 +1457,65 @@ impl Window { let start_res = ui .horizontal(|ui| { ui.label("Start:"); - ui.text_edit_singleline(&mut cx.interval_state.start_buffer) + ui.text_edit_singleline(&mut cx.interval_select_state.start_buffer) }) .inner; - if let Some(error) = cx.interval_state.start_error { + if let Some(error) = cx.interval_select_state.start_error { ui.label(RichText::new(error.to_string()).color(Color32::RED)); } let stop_res = ui .horizontal(|ui| { ui.label("Stop:"); - ui.text_edit_singleline(&mut cx.interval_state.stop_buffer) + ui.text_edit_singleline(&mut cx.interval_select_state.stop_buffer) }) .inner; - if let Some(error) = cx.interval_state.stop_error { + if let Some(error) = cx.interval_select_state.stop_error { ui.label(RichText::new(error.to_string()).color(Color32::RED)); } if start_res.lost_focus() - && cx.interval_state.start_buffer != cx.view_interval.start.to_string() + && cx.interval_select_state.start_buffer != cx.view_interval.start.to_string() { - match Timestamp::parse(&cx.interval_state.start_buffer) { + match Timestamp::parse(&cx.interval_select_state.start_buffer) { Ok(start) => { // validate timestamp if start > cx.view_interval.stop { - cx.interval_state.start_error = Some(IntervalSelectError::StartAfterStop); + cx.interval_select_state.start_error = + Some(IntervalSelectError::StartAfterStop); return; } if start > cx.total_interval.stop { - cx.interval_state.start_error = Some(IntervalSelectError::StartAfterEnd); + cx.interval_select_state.start_error = + Some(IntervalSelectError::StartAfterEnd); return; } let target = Interval::new(start, cx.view_interval.stop); ProfApp::zoom(cx, target); } Err(e) => { - cx.interval_state.start_error = Some(e.into()); + cx.interval_select_state.start_error = Some(e.into()); } } } if stop_res.lost_focus() - && cx.interval_state.stop_buffer != cx.view_interval.stop.to_string() + && cx.interval_select_state.stop_buffer != cx.view_interval.stop.to_string() { - match Timestamp::parse(&cx.interval_state.stop_buffer) { + match Timestamp::parse(&cx.interval_select_state.stop_buffer) { Ok(stop) => { // validate timestamp if stop < cx.view_interval.start { - cx.interval_state.stop_error = Some(IntervalSelectError::StopBeforeStart); + cx.interval_select_state.stop_error = + Some(IntervalSelectError::StopBeforeStart); return; } let target = Interval::new(cx.view_interval.start, stop); ProfApp::zoom(cx, target); } Err(e) => { - cx.interval_state.stop_error = Some(e.into()); + cx.interval_select_state.stop_error = Some(e.into()); } } } @@ -1669,6 +1680,11 @@ impl Window { } } +enum PanDirection { + Left, + Right, +} + impl ProfApp { /// Called once before the first frame. pub fn new( @@ -1711,43 +1727,76 @@ impl ProfApp { result } + fn update_interval_select_state(cx: &mut Context) { + cx.interval_select_state.start_buffer = cx.view_interval.start.to_string(); + cx.interval_select_state.stop_buffer = cx.view_interval.stop.to_string(); + cx.interval_select_state.start_error = None; + cx.interval_select_state.stop_error = None; + } + + fn update_view_interval(cx: &mut Context, interval: Interval, origin: IntervalOrigin) { + cx.view_interval = interval; + + let history = &mut cx.view_interval_history; + let index = history.index; + + // Only keep at most one Pan origin in a row + if !history.levels.is_empty() + && history.origins[index] == IntervalOrigin::Pan + && origin == IntervalOrigin::Pan + { + history.levels.truncate(index); + history.origins.truncate(index); + } + + history.levels.truncate(index + 1); + history.levels.push(interval); + history.origins.truncate(index + 1); + history.origins.push(origin); + history.index = history.levels.len() - 1; + } + + fn pan(cx: &mut Context, percent: PercentageInteger, dir: PanDirection) { + if percent.value() == 0 { + return; + } + + let duration = percent.apply_to(cx.view_interval.duration_ns()); + let sign = match dir { + PanDirection::Left => -1, + PanDirection::Right => 1, + }; + let interval = cx.view_interval.translate(duration * sign); + + ProfApp::update_view_interval(cx, interval, IntervalOrigin::Pan); + ProfApp::update_interval_select_state(cx); + } + fn zoom(cx: &mut Context, interval: Interval) { if cx.view_interval == interval { return; } - cx.view_interval = interval; - cx.zoom_state.levels.truncate(cx.zoom_state.index + 1); - cx.zoom_state.levels.push(cx.view_interval); - cx.zoom_state.index = cx.zoom_state.levels.len() - 1; - cx.interval_state.start_buffer = cx.view_interval.start.to_string(); - cx.interval_state.stop_buffer = cx.view_interval.stop.to_string(); - cx.interval_state.start_error = None; - cx.interval_state.stop_error = None; + ProfApp::update_view_interval(cx, interval, IntervalOrigin::Zoom); + ProfApp::update_interval_select_state(cx); } - fn undo_zoom(cx: &mut Context) { - if cx.zoom_state.index == 0 { + fn undo_pan_zoom(cx: &mut Context) { + if cx.view_interval_history.index == 0 { return; } - cx.zoom_state.index -= 1; - cx.view_interval = cx.zoom_state.levels[cx.zoom_state.index]; - cx.interval_state.start_buffer = cx.view_interval.start.to_string(); - cx.interval_state.stop_buffer = cx.view_interval.stop.to_string(); - cx.interval_state.start_error = None; - cx.interval_state.stop_error = None; + cx.view_interval_history.index -= 1; + cx.view_interval = cx.view_interval_history.levels[cx.view_interval_history.index]; + ProfApp::update_interval_select_state(cx); } - fn redo_zoom(cx: &mut Context) { - if cx.zoom_state.index + 1 >= cx.zoom_state.levels.len() { + fn redo_pan_zoom(cx: &mut Context) { + if cx.view_interval_history.index + 1 >= cx.view_interval_history.levels.len() { return; } - cx.zoom_state.index += 1; - cx.view_interval = cx.zoom_state.levels[cx.zoom_state.index]; - cx.interval_state.start_buffer = cx.view_interval.start.to_string(); - cx.interval_state.stop_buffer = cx.view_interval.stop.to_string(); - cx.interval_state.start_error = None; - cx.interval_state.stop_error = None; + cx.view_interval_history.index += 1; + cx.view_interval = cx.view_interval_history.levels[cx.view_interval_history.index]; + ProfApp::update_interval_select_state(cx); } fn zoom_in(cx: &mut Context) { @@ -1783,6 +1832,7 @@ impl ProfApp { UndoZoom, RedoZoom, ResetZoom, + Pan(PercentageInteger, PanDirection), ExpandVertical, ShrinkVertical, ResetVertical, @@ -1814,8 +1864,20 @@ impl ProfApp { } else { Actions::NoAction } + } else if i.modifiers.shift { + if i.key_pressed(egui::Key::ArrowLeft) { + Actions::Pan(Percentage::from(1), PanDirection::Left) + } else if i.key_pressed(egui::Key::ArrowRight) { + Actions::Pan(Percentage::from(1), PanDirection::Right) + } else { + Actions::NoAction + } } else if i.key_pressed(egui::Key::H) { Actions::ToggleControls + } else if i.key_pressed(egui::Key::ArrowLeft) { + Actions::Pan(Percentage::from(5), PanDirection::Left) + } else if i.key_pressed(egui::Key::ArrowRight) { + Actions::Pan(Percentage::from(5), PanDirection::Right) } else { Actions::NoAction } @@ -1823,9 +1885,10 @@ impl ProfApp { match action { Actions::ZoomIn => ProfApp::zoom_in(cx), Actions::ZoomOut => ProfApp::zoom_out(cx), - Actions::UndoZoom => ProfApp::undo_zoom(cx), - Actions::RedoZoom => ProfApp::redo_zoom(cx), + Actions::UndoZoom => ProfApp::undo_pan_zoom(cx), + Actions::RedoZoom => ProfApp::redo_pan_zoom(cx), Actions::ResetZoom => ProfApp::zoom(cx, cx.total_interval), + Actions::Pan(percent, dir) => ProfApp::pan(cx, percent, dir), Actions::ExpandVertical => ProfApp::multiply_scale_factor(cx, 2.0), Actions::ShrinkVertical => ProfApp::multiply_scale_factor(cx, 0.5), Actions::ResetVertical => ProfApp::reset_scale_factor(cx), @@ -1966,11 +2029,13 @@ impl ProfApp { }); }; show_row("Zoom to Interval", "Click and Drag"); + show_row("Pan 5%", "Left/Right Arrow"); + show_row("Pan 1%", "Shift + Left/Right Arrow"); show_row("Zoom In", "Ctrl + Plus/Equals"); show_row("Zoom Out", "Ctrl + Minus"); - show_row("Undo Zoom", "Ctrl + Left Arrow"); - show_row("Redo Zoom", "Ctrl + Right Arrow"); - show_row("Reset Zoom", "Ctrl + 0"); + show_row("Undo Pan/Zoom", "Ctrl + Left Arrow"); + show_row("Redo Pan/Zoom", "Ctrl + Right Arrow"); + show_row("Reset Pan/Zoom", "Ctrl + 0"); show_row("Expand Vertical Spacing", "Ctrl + Alt + Plus/Equals"); show_row("Shrink Vertical Spacing", "Ctrl + Alt + Minus"); show_row("Reset Vertical Spacing", "Ctrl + Alt + 0"); diff --git a/src/timestamp.rs b/src/timestamp.rs index 0d470e8..8b4b09c 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -164,148 +164,187 @@ impl Interval { stop: Timestamp(self.stop.0 + duration_ns), } } + // Translate interval by duration_ns on both sides. + pub fn translate(self, duration_ns: i64) -> Self { + Self { + start: Timestamp(self.start.0 + duration_ns), + stop: Timestamp(self.stop.0 + duration_ns), + } + } } #[cfg(test)] mod tests { use super::*; - #[test] - fn test_s() { - assert_eq!(Timestamp::parse("123.4 s"), Ok(Timestamp(123_400_000_000))); - } + mod timestamp { + use super::*; - #[test] - fn test_ms() { - assert_eq!(Timestamp::parse("234.5 ms"), Ok(Timestamp(234_500_000))); - } + #[test] + fn test_s() { + assert_eq!(Timestamp::parse("123.4 s"), Ok(Timestamp(123_400_000_000))); + } - #[test] - fn test_us() { - assert_eq!(Timestamp::parse("345.6 us"), Ok(Timestamp(345_600))); - } + #[test] + fn test_ms() { + assert_eq!(Timestamp::parse("234.5 ms"), Ok(Timestamp(234_500_000))); + } - #[test] - fn test_ns() { - assert_eq!(Timestamp::parse("567.0 ns"), Ok(Timestamp(567))); - } + #[test] + fn test_us() { + assert_eq!(Timestamp::parse("345.6 us"), Ok(Timestamp(345_600))); + } - #[test] - fn test_s_upper() { - assert_eq!(Timestamp::parse("123.4 S"), Ok(Timestamp(123_400_000_000))); - } + #[test] + fn test_ns() { + assert_eq!(Timestamp::parse("567.0 ns"), Ok(Timestamp(567))); + } - #[test] - fn test_ms_upper() { - assert_eq!(Timestamp::parse("234.5 MS"), Ok(Timestamp(234_500_000))); - } + #[test] + fn test_s_upper() { + assert_eq!(Timestamp::parse("123.4 S"), Ok(Timestamp(123_400_000_000))); + } - #[test] - fn test_us_upper() { - assert_eq!(Timestamp::parse("345.6 US"), Ok(Timestamp(345_600))); - } + #[test] + fn test_ms_upper() { + assert_eq!(Timestamp::parse("234.5 MS"), Ok(Timestamp(234_500_000))); + } - #[test] - fn test_ns_upper() { - assert_eq!(Timestamp::parse("567.0 NS"), Ok(Timestamp(567))); - } + #[test] + fn test_us_upper() { + assert_eq!(Timestamp::parse("345.6 US"), Ok(Timestamp(345_600))); + } - #[test] - fn test_s_nospace() { - assert_eq!(Timestamp::parse("123.4s"), Ok(Timestamp(123_400_000_000))); - } + #[test] + fn test_ns_upper() { + assert_eq!(Timestamp::parse("567.0 NS"), Ok(Timestamp(567))); + } - #[test] - fn test_ms_nospace() { - assert_eq!(Timestamp::parse("234.5ms"), Ok(Timestamp(234_500_000))); - } + #[test] + fn test_s_nospace() { + assert_eq!(Timestamp::parse("123.4s"), Ok(Timestamp(123_400_000_000))); + } - #[test] - fn test_us_nospace() { - assert_eq!(Timestamp::parse("345.6us"), Ok(Timestamp(345_600))); - } + #[test] + fn test_ms_nospace() { + assert_eq!(Timestamp::parse("234.5ms"), Ok(Timestamp(234_500_000))); + } - #[test] - fn test_ns_nospace() { - assert_eq!(Timestamp::parse("567.0ns"), Ok(Timestamp(567))); - } + #[test] + fn test_us_nospace() { + assert_eq!(Timestamp::parse("345.6us"), Ok(Timestamp(345_600))); + } - #[test] - fn test_s_spaces() { - assert_eq!( - Timestamp::parse(" 123.4 s "), - Ok(Timestamp(123_400_000_000)) - ); - } + #[test] + fn test_ns_nospace() { + assert_eq!(Timestamp::parse("567.0ns"), Ok(Timestamp(567))); + } - #[test] - fn test_ms_spaces() { - assert_eq!( - Timestamp::parse(" 234.5 ms "), - Ok(Timestamp(234_500_000)) - ); - } + #[test] + fn test_s_spaces() { + assert_eq!( + Timestamp::parse(" 123.4 s "), + Ok(Timestamp(123_400_000_000)) + ); + } - #[test] - fn test_us_spaces() { - assert_eq!(Timestamp::parse(" 345.6 us "), Ok(Timestamp(345_600))); - } + #[test] + fn test_ms_spaces() { + assert_eq!( + Timestamp::parse(" 234.5 ms "), + Ok(Timestamp(234_500_000)) + ); + } - #[test] - fn test_ns_spaces() { - assert_eq!(Timestamp::parse(" 567.0 ns "), Ok(Timestamp(567))); - } + #[test] + fn test_us_spaces() { + assert_eq!(Timestamp::parse(" 345.6 us "), Ok(Timestamp(345_600))); + } - #[test] - fn test_no_unit() { - assert_eq!(Timestamp::parse("500.0"), Err(TimestampParseError::NoUnit)); - } + #[test] + fn test_ns_spaces() { + assert_eq!(Timestamp::parse(" 567.0 ns "), Ok(Timestamp(567))); + } - #[test] - fn test_no_value() { - assert_eq!( - Timestamp::parse("ms"), - Err(TimestampParseError::InvalidValue) - ); - } + #[test] + fn test_no_unit() { + assert_eq!(Timestamp::parse("500.0"), Err(TimestampParseError::NoUnit)); + } - #[test] - fn test_invalid_unit() { - assert_eq!( - Timestamp::parse("500.0 foo"), - Err(TimestampParseError::InvalidUnit) - ); - } + #[test] + fn test_no_value() { + assert_eq!( + Timestamp::parse("ms"), + Err(TimestampParseError::InvalidValue) + ); + } - #[test] - fn test_invalid_value() { - assert_eq!( - Timestamp::parse("foo ms"), - Err(TimestampParseError::InvalidValue) - ); - } + #[test] + fn test_invalid_unit() { + assert_eq!( + Timestamp::parse("500.0 foo"), + Err(TimestampParseError::InvalidUnit) + ); + } - #[test] - fn test_invalid_value2() { - assert_eq!( - Timestamp::parse("500.0.0 ms"), - Err(TimestampParseError::InvalidValue) - ); - } + #[test] + fn test_invalid_value() { + assert_eq!( + Timestamp::parse("foo ms"), + Err(TimestampParseError::InvalidValue) + ); + } + + #[test] + fn test_invalid_value2() { + assert_eq!( + Timestamp::parse("500.0.0 ms"), + Err(TimestampParseError::InvalidValue) + ); + } - #[test] - fn test_invalid_value3() { - assert_eq!( - Timestamp::parse("500.0.0"), - Err(TimestampParseError::NoUnit) - ); + #[test] + fn test_invalid_value3() { + assert_eq!( + Timestamp::parse("500.0.0"), + Err(TimestampParseError::NoUnit) + ); + } + + #[test] + fn test_extra() { + assert_eq!( + Timestamp::parse("500.0 ms asdfadf"), + Err(TimestampParseError::InvalidUnit) + ); + } } - #[test] - fn test_extra() { - assert_eq!( - Timestamp::parse("500.0 ms asdfadf"), - Err(TimestampParseError::InvalidUnit) - ); + mod interval { + use super::*; + + #[test] + fn test_translate_positive() { + let start = Timestamp::parse("234.5 ms").unwrap(); + let end = Timestamp::parse("235.5 ms").unwrap(); + let expected_start = Timestamp(start.0 + 250); + let expected_end = Timestamp(end.0 + 250); + assert_eq!( + Interval::new(start, end).translate(250), + Interval::new(expected_start, expected_end) + ) + } + + #[test] + fn test_translate_negative() { + let start = Timestamp::parse("234.5 ms").unwrap(); + let end = Timestamp::parse("235.5 ms").unwrap(); + let expected_start = Timestamp(start.0 - 250); + let expected_end = Timestamp(end.0 - 250); + assert_eq!( + Interval::new(start, end).translate(-250), + Interval::new(expected_start, expected_end) + ) + } } }