From e61cfffd8049d553e7acad397902f0edf5370ce1 Mon Sep 17 00:00:00 2001 From: 0xb-s <145866191+0xb-s@users.noreply.github.com> Date: Sat, 15 Nov 2025 08:19:46 -0800 Subject: [PATCH 01/14] Update lib.rs --- egui_plot/src/lib.rs | 313 +++++++++++++++++++++++++++++-------------- 1 file changed, 213 insertions(+), 100 deletions(-) diff --git a/egui_plot/src/lib.rs b/egui_plot/src/lib.rs index 05eaeca3..f3a94d4d 100644 --- a/egui_plot/src/lib.rs +++ b/egui_plot/src/lib.rs @@ -14,6 +14,7 @@ mod collect_events; mod items; mod legend; mod memory; +mod navigation; mod plot_ui; mod segmented_axis; mod span; @@ -24,6 +25,7 @@ mod action; pub use crate::action::PlotEvent; pub use crate::action::{ActionExecutor, ActionQueue}; pub use crate::action::{BoundsChangeCause, InputInfo, PinSnapshot}; +pub use navigation::{AxisToggle, BoxZoomConfig, NavigationConfig, ResetBehavior, ZoomConfig}; pub use crate::segmented_axis::SegmentedAxis; pub use crate::{ @@ -41,9 +43,9 @@ pub use crate::{ }; use ahash::HashMap; use egui::{ - Align2, Color32, CursorIcon, Id, Layout, NumExt as _, PointerButton, Pos2, Rangef, Rect, - Response, Sense, Shape, Stroke, TextStyle, Ui, Vec2, Vec2b, WidgetText, epaint, remap_clamp, - vec2, + Align2, Color32, CursorIcon, Id, Layout, Modifiers, NumExt as _, PointerButton, Pos2, Rangef, + Rect, Response, Sense, Shape, Stroke, TextStyle, Ui, Vec2, Vec2b, WidgetText, epaint, + remap_clamp, vec2, }; pub use span::{HSpan, VSpan}; pub use span_utils::interval_to_screen_x; @@ -55,7 +57,7 @@ use emath::Float as _; use axis::AxisWidget; use items::{horizontal_line, rulers_color, vertical_line}; use legend::LegendWidget; -use egui::pos2; + type LabelFormatterFn<'a> = dyn Fn(&str, &PlotPoint) -> String + 'a; pub type LabelFormatter<'a> = Option>>; @@ -213,6 +215,8 @@ pub struct Plot<'a> { sense: Sense, segmented_x_axis: Option, + + navigation: Option, } impl<'a> Plot<'a> { @@ -263,8 +267,15 @@ impl<'a> Plot<'a> { sense: egui::Sense::click_and_drag(), segmented_x_axis: None, + navigation: None, } } + + /// custom navigation configuration. + pub fn navigation(mut self, config: NavigationConfig) -> Self { + self.navigation = Some(config); + self + } pub fn segmented_x_axis(mut self, segmented: Option) -> Self { self.segmented_x_axis = segmented; self @@ -845,12 +856,32 @@ impl<'a> Plot<'a> { grid_spacers, sense, segmented_x_axis, + navigation, } = self; + let mut nav = if let Some(cfg) = navigation { + cfg + } else { + NavigationConfig::from_legacy_flags( + allow_drag, + allow_zoom, + allow_scroll, + allow_axis_zoom_drag, + allow_double_click_reset, + allow_boxed_zoom, + boxed_zoom_pointer_button, + ) + }; + // Disable interaction if ui is disabled. - let allow_zoom = allow_zoom.and(ui.is_enabled()); - let allow_drag = allow_drag.and(ui.is_enabled()); - let allow_scroll = allow_scroll.and(ui.is_enabled()); + let ui_enabled = ui.is_enabled(); + if !ui_enabled { + nav.drag.enabled = false; + nav.scroll.enabled = false; + nav.zoom.enabled = false; + nav.double_click_reset = false; + nav.box_zoom.enabled = false; + } // Determine position of widget. let pos = ui.available_rect_before_wrap().min; @@ -910,7 +941,7 @@ impl<'a> Plot<'a> { .iter() .map(|widget| { let axis_resp = ui.allocate_rect(widget.rect, Sense::drag()); - if allow_axis_zoom_drag.x { + if nav.axis_zoom_drag.x { axis_resp.on_hover_cursor(CursorIcon::ResizeHorizontal) } else { axis_resp @@ -922,7 +953,7 @@ impl<'a> Plot<'a> { .iter() .map(|widget| { let axis_resp = ui.allocate_rect(widget.rect, Sense::drag()); - if allow_axis_zoom_drag.y { + if nav.axis_zoom_drag.y { axis_resp.on_hover_cursor(CursorIcon::ResizeVertical) } else { axis_resp @@ -951,9 +982,14 @@ impl<'a> Plot<'a> { last_click_pos_for_zoom: None, x_axis_thickness: Default::default(), y_axis_thickness: Default::default(), + original_bounds: None, }); + // Remember the very first bounds shown to the user (for ResetBehavior::OriginalBounds) + if mem.original_bounds.is_none() { + mem.original_bounds = Some(*mem.transform.bounds()); + } - let last_plot_transform = mem.transform; + let last_plot_transform = mem.transform.clone(); // Call the plot build function. let mut plot_ui = PlotUi { ctx: ui.ctx().clone(), @@ -963,6 +999,7 @@ impl<'a> Plot<'a> { last_auto_bounds: mem.auto_bounds, response: response.clone(), called_once: false, + navigation: nav, }; let inner = build_fn(&mut plot_ui); @@ -1031,6 +1068,7 @@ impl<'a> Plot<'a> { let frames: &mut CursorLinkGroups = data.get_temp_mut_or_default(Id::NULL); let cursors = frames.0.entry(*id).or_default(); // Look for our previous frame + if let Some(index) = cursors .iter() .enumerate() @@ -1068,17 +1106,37 @@ impl<'a> Plot<'a> { }); } - // Double-click reset - if allow_double_click_reset && response.double_clicked() { - mem.auto_bounds = true.into(); - events.push(PlotEvent::ResetApplied { - input: InputInfo { - pointer: ui.input(|i| i.pointer.hover_pos()), - button: Some(PointerButton::Primary), - modifiers: ui.input(|i| i.modifiers), - }, - }); - last_user_cause = Some(BoundsChangeCause::Reset); + // Double-click reset (configurable rn) + if nav.double_click_reset && response.double_clicked() { + match nav.reset_behavior { + ResetBehavior::AutoFit => { + mem.auto_bounds = true.into(); + events.push(PlotEvent::ResetApplied { + input: InputInfo { + pointer: ui.input(|i| i.pointer.hover_pos()), + button: Some(PointerButton::Primary), + modifiers: ui.input(|i| i.modifiers), + }, + }); + last_user_cause = Some(BoundsChangeCause::Reset); + } + ResetBehavior::OriginalBounds => { + if let Some(orig) = mem.original_bounds { + bounds = orig; + + mem.auto_bounds = false.into(); + + events.push(PlotEvent::ResetApplied { + input: InputInfo { + pointer: ui.input(|i| i.pointer.hover_pos()), + button: Some(PointerButton::Primary), + modifiers: ui.input(|i| i.modifiers), + }, + }); + last_user_cause = Some(BoundsChangeCause::Reset); + } + } + } } if mem.auto_bounds.x { @@ -1139,7 +1197,10 @@ impl<'a> Plot<'a> { } // Pan - if allow_drag.any() && response.dragged_by(PointerButton::Primary) { + if nav.drag.enabled + && (nav.drag.axis.x || nav.drag.axis.y) + && response.dragged_by(PointerButton::Primary) + { response = response.on_hover_cursor(CursorIcon::Grabbing); if response.drag_started() { @@ -1153,10 +1214,10 @@ impl<'a> Plot<'a> { } let mut delta = -response.drag_delta(); - if !allow_drag.x { + if !nav.drag.axis.x { delta.x = 0.0; } - if !allow_drag.y { + if !nav.drag.axis.y { delta.y = 0.0; } @@ -1173,13 +1234,13 @@ impl<'a> Plot<'a> { if mem.transform.segment_xaxis().is_some() { mem.transform.translate_segment_offset(-delta.x); - mem.transform.translate_bounds((0.0, delta.y as f64)); } else { mem.transform .translate_bounds((delta.x as f64, delta.y as f64)); } - mem.auto_bounds = mem.auto_bounds.and(!allow_drag); + + mem.auto_bounds = mem.auto_bounds.and(!nav.drag.axis); last_user_cause = Some(BoundsChangeCause::Pan); if response.drag_stopped() { @@ -1195,7 +1256,7 @@ impl<'a> Plot<'a> { // Axis zoom drag for d in 0..2 { - if allow_axis_zoom_drag[d] { + if nav.axis_zoom_drag[d] { if let Some(axis_resp) = (if d == 0 { &x_axis_responses } else { @@ -1267,27 +1328,35 @@ impl<'a> Plot<'a> { // Boxed zoom let mut boxed_zoom_rect = None; - if allow_boxed_zoom { - // Save last click to allow boxed zooming - - if response.drag_started() && response.dragged_by(boxed_zoom_pointer_button) { - // it would be best for egui that input has a memory of the last click pos because it's a common pattern + if nav.box_zoom.enabled { + let modifiers_ok = |cur: Modifiers, req: Modifiers| -> bool { + (!req.alt || cur.alt) + && (!req.ctrl || cur.ctrl) + && (!req.shift || cur.shift) + && (!req.command || cur.command) + && (!req.mac_cmd || cur.mac_cmd) + }; + if response.drag_started() + && response.dragged_by(nav.box_zoom.button) + && modifiers_ok(ui.input(|i| i.modifiers), nav.box_zoom.required_mods) + { mem.last_click_pos_for_zoom = response.hover_pos(); events.push(PlotEvent::BoxZoomStarted { screen_start: mem.last_click_pos_for_zoom.unwrap_or(plot_rect.center()), input: InputInfo { pointer: mem.last_click_pos_for_zoom, - button: Some(boxed_zoom_pointer_button), + button: Some(nav.box_zoom.button), modifiers: ui.input(|i| i.modifiers), }, }); } + let (start, end) = (mem.last_click_pos_for_zoom, response.hover_pos()); if let (Some(s), Some(e)) = (start, end) { - // while dragging prepare a Shape and draw it later on top of the plot - - if response.dragged_by(boxed_zoom_pointer_button) { + if response.dragged_by(nav.box_zoom.button) + && modifiers_ok(ui.input(|i| i.modifiers), nav.box_zoom.required_mods) + { response = response.on_hover_cursor(CursorIcon::ZoomIn); let rect = epaint::Rect::from_two_pos(s, e); boxed_zoom_rect = Some(( @@ -1296,16 +1365,16 @@ impl<'a> Plot<'a> { 0.0, epaint::Stroke::new(4., Color32::DARK_BLUE), egui::StrokeKind::Middle, - ), // Outer stroke + ), epaint::RectShape::stroke( rect, 0.0, epaint::Stroke::new(2., Color32::WHITE), egui::StrokeKind::Middle, - ), // Inner stroke + ), )); } - // when the click is release perform the zoom + if response.drag_stopped() { let s_val = mem.transform.value_from_position(s); let e_val = mem.transform.value_from_position(e); @@ -1323,13 +1392,12 @@ impl<'a> Plot<'a> { new_y, input: InputInfo { pointer: response.hover_pos(), - button: Some(boxed_zoom_pointer_button), + button: Some(nav.box_zoom.button), modifiers: ui.input(|i| i.modifiers), }, }); last_user_cause = Some(BoundsChangeCause::BoxZoom); } - // reset the boxed zoom state mem.last_click_pos_for_zoom = None; } } @@ -1342,25 +1410,38 @@ impl<'a> Plot<'a> { response.contains_pointer(), ui.input(|i| i.pointer.hover_pos()), ) { - if allow_zoom.any() { - let mut zoom_factor = if data_aspect.is_some() { + // Zoom + if nav.zoom.enabled && (nav.zoom.axis.x || nav.zoom.axis.y) { + let mut z = if data_aspect.is_some() { Vec2::splat(ui.input(|i| i.zoom_delta())) } else { ui.input(|i| i.zoom_delta_2d()) }; - if !allow_zoom.x { - zoom_factor.x = 1.0; + + if !nav.zoom.axis.x { + z.x = 1.0; } - if !allow_zoom.y { - zoom_factor.y = 1.0; + if !nav.zoom.axis.y { + z.y = 1.0; + } + + if nav.zoom.wheel_factor_exp != 1.0 { + z.x = z.x.powf(nav.zoom.wheel_factor_exp); + z.y = z.y.powf(nav.zoom.wheel_factor_exp); } - if zoom_factor != Vec2::splat(1.0) { - mem.transform.zoom(zoom_factor, hover_pos); + + if z != Vec2::splat(1.0) { + let center = if nav.zoom.zoom_to_mouse { + hover_pos + } else { + plot_rect.center() + }; + mem.transform.zoom(z, center); events.push(PlotEvent::ZoomDelta { - factor_x: zoom_factor.x, - factor_y: zoom_factor.y, - center_plot_x: mem.transform.value_from_position(hover_pos).x, - center_plot_y: mem.transform.value_from_position(hover_pos).y, + factor_x: z.x, + factor_y: z.y, + center_plot_x: mem.transform.value_from_position(center).x, + center_plot_y: mem.transform.value_from_position(center).y, input: InputInfo { pointer: Some(hover_pos), button: None, @@ -1368,22 +1449,35 @@ impl<'a> Plot<'a> { }, }); last_user_cause = Some(BoundsChangeCause::Zoom); - mem.auto_bounds = mem.auto_bounds.and(!allow_zoom); + + let ab = Vec2b::new( + if nav.zoom.axis.x { + false + } else { + mem.auto_bounds.x + }, + if nav.zoom.axis.y { + false + } else { + mem.auto_bounds.y + }, + ); + mem.auto_bounds = ab; } } - if allow_scroll.any() { + // Scroll pan + if nav.scroll.enabled && (nav.scroll.axis.x || nav.scroll.axis.y) { let mut scroll = ui.input(|i| i.smooth_scroll_delta); - if !allow_scroll.x { + if !nav.scroll.axis.x { scroll.x = 0.0; } - if !allow_scroll.y { + if !nav.scroll.axis.y { scroll.y = 0.0; } if scroll != Vec2::ZERO { if mem.transform.segment_xaxis().is_some() { mem.transform.translate_segment_offset(-scroll.x); - mem.transform.translate_bounds((0.0, -scroll.y as f64)); } else { mem.transform @@ -1393,6 +1487,7 @@ impl<'a> Plot<'a> { } } } + // --- transform initialized // Add legend widgets to plot @@ -1454,18 +1549,6 @@ impl<'a> Plot<'a> { let (plot_cursors, mut hovered_plot_item) = prepared.ui(ui, &response); - if let Some(gaps) = mem.transform.segment_x_gap_screen_ranges() { - let frame = mem.transform.frame(); - let gap_color = ui.visuals().extreme_bg_color; - for (left, right) in gaps { - let gap_rect = - Rect::from_min_max(pos2(left, frame.top()), pos2(right, frame.bottom())); - ui.painter() - .with_clip_rect(*frame) - .add(egui::Shape::rect_filled(gap_rect, 0.0, gap_color)); - } - } - // Click/Context menu -> events if response.clicked() { events.push(PlotEvent::Activate { @@ -1525,22 +1608,6 @@ impl<'a> Plot<'a> { ); }); } - - let transform = mem.transform.clone(); - mem.store(ui.ctx(), plot_id); - - response = if show_x || show_y { - response.on_hover_cursor(CursorIcon::Crosshair) - } else { - response - }; - ui.advance_cursor_after_rect(complete_rect); - - if let Some(screen) = response.hover_pos() { - let pos = transform.value_from_position(screen); - events.push(PlotEvent::Hover { pos }); - } - if response.has_focus() || response.contains_pointer() { let pressed = |k: egui::Key| ui.ctx().input(|i| i.key_pressed(k)); let released = |k: egui::Key| ui.ctx().input(|i| i.key_released(k)); @@ -1569,27 +1636,59 @@ impl<'a> Plot<'a> { } } - if ui.ctx().input(|i| i.key_pressed(egui::Key::P)) { - if let Some(ptr) = ui.ctx().input(|i| i.pointer.latest_pos()) { - let plot = transform.value_from_position(ptr); - events.push(PlotEvent::PinAdded { - snapshot: crate::action::PinSnapshot { - plot_x: plot.x, - rows: Vec::new(), - }, - }); + // Fit-to-view shortcut + if let Some(k) = nav.fit_to_view_key { + if ui.ctx().input(|i| i.key_pressed(k)) { + mem.auto_bounds = true.into(); + last_user_cause = Some(BoundsChangeCause::AutoFit); } } - if ui.ctx().input(|i| i.key_pressed(egui::Key::U)) { - events.push(PlotEvent::PinRemoved { index: 0 }); + + //Restore original bounds shortcut + if let Some(k) = nav.restore_original_key { + if ui.ctx().input(|i| i.key_pressed(k)) { + if let Some(orig) = mem.original_bounds { + mem.transform.set_bounds(orig); + mem.auto_bounds = false.into(); + last_user_cause = Some(BoundsChangeCause::Reset); + } + } } - if ui.ctx().input(|i| i.key_pressed(egui::Key::Delete)) { - events.push(PlotEvent::PinsCleared); + + // Pinning shortcuts + if nav.pinning_enabled { + if let Some(k) = nav.pin_add_key { + if ui.ctx().input(|i| i.key_pressed(k)) { + if let Some(ptr) = ui.ctx().input(|i| i.pointer.latest_pos()) { + let plot = mem.transform.value_from_position(ptr); + events.push(PlotEvent::PinAdded { + snapshot: crate::action::PinSnapshot { + plot_x: plot.x, + rows: Vec::new(), + }, + }); + } + } + } + + // Remove latest / first pin + if let Some(k) = nav.pin_remove_key { + if ui.ctx().input(|i| i.key_pressed(k)) { + events.push(PlotEvent::PinRemoved { index: 0 }); + } + } + + // Clear all pins + if let Some(k) = nav.pins_clear_key { + if ui.ctx().input(|i| i.key_pressed(k)) { + events.push(PlotEvent::PinsCleared); + } + } } } let old_bounds = *last_plot_transform.bounds(); - let new_bounds = *transform.bounds(); + let new_bounds = *mem.transform.bounds(); if old_bounds != new_bounds { events.push(PlotEvent::BoundsChanged { old: old_bounds, @@ -1597,6 +1696,20 @@ impl<'a> Plot<'a> { cause: last_user_cause.unwrap_or(BoundsChangeCause::Programmatic), }); } + let transform = mem.transform.clone(); + mem.store(ui.ctx(), plot_id); + + response = if show_x || show_y { + response.on_hover_cursor(CursorIcon::Crosshair) + } else { + response + }; + ui.advance_cursor_after_rect(complete_rect); + + if let Some(screen) = response.hover_pos() { + let pos = transform.value_from_position(screen); + events.push(PlotEvent::Hover { pos }); + } PlotResponse { inner, From 6eea7b0b6b4f15c56946477753971ad04c4d90c0 Mon Sep 17 00:00:00 2001 From: 0xb-s <145866191+0xb-s@users.noreply.github.com> Date: Sat, 15 Nov 2025 08:20:53 -0800 Subject: [PATCH 02/14] Update memory.rs --- egui_plot/src/memory.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/egui_plot/src/memory.rs b/egui_plot/src/memory.rs index d292690d..da3d32ec 100644 --- a/egui_plot/src/memory.rs +++ b/egui_plot/src/memory.rs @@ -32,6 +32,9 @@ pub struct PlotMemory { /// in order to fit the labels, if necessary. pub(crate) x_axis_thickness: BTreeMap, pub(crate) y_axis_thickness: BTreeMap, + + /// first bounds that has been shown. + pub original_bounds: Option, } impl PlotMemory { From 6b6eb9a1acd7ea3daf15c4399047deb0f1307a28 Mon Sep 17 00:00:00 2001 From: 0xb-s <145866191+0xb-s@users.noreply.github.com> Date: Sat, 15 Nov 2025 08:21:47 -0800 Subject: [PATCH 03/14] Create navigation.rs --- egui_plot/src/navigation.rs | 234 ++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 egui_plot/src/navigation.rs diff --git a/egui_plot/src/navigation.rs b/egui_plot/src/navigation.rs new file mode 100644 index 00000000..bec527f8 --- /dev/null +++ b/egui_plot/src/navigation.rs @@ -0,0 +1,234 @@ +//! Navigation module. + +use egui::{Key, Modifiers, PointerButton, Vec2b}; + +/// A reset operation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ResetBehavior { + /// Reset by auto-fitting bounds to visible content. + AutoFit, + /// Restore the original bounds from the first frame the plot was shown. + OriginalBounds, +} + +/// Per-axis enable flags. +#[derive(Clone, Copy, Debug)] +pub struct AxisToggle { + /// Master flag. If `false`, the feature is disabled even if individual axes are `true`. + pub enabled: bool, + /// Which axes are affected (`x` and/or `y`). + pub axis: Vec2b, +} + +impl AxisToggle { + #[inline] + pub const fn new(enabled: bool, axis: Vec2b) -> Self { + Self { enabled, axis } + } +} + +/// Zoom configuration. +#[derive(Clone, Copy, Debug)] +pub struct ZoomConfig { + /// Master enable. + pub enabled: bool, + /// Axes to zoom (`x` and/or `y`). + pub axis: Vec2b, + /// If `true`, zoom centers at the mouse position; otherwise at plot center. + pub zoom_to_mouse: bool, + /// Exponent applied to `egui` zoom delta (1.0 = unchanged). + /// Values >1.0 make zoom more aggressive; <1.0 make it gentler. + pub wheel_factor_exp: f32, +} + +impl ZoomConfig { + #[inline] + pub const fn new(enabled: bool, axis: Vec2b) -> Self { + Self { + enabled, + axis, + zoom_to_mouse: true, + wheel_factor_exp: 1.0, + } + } + + #[inline] + pub fn zoom_to_mouse(mut self, v: bool) -> Self { + self.zoom_to_mouse = v; + self + } + + #[inline] + pub fn wheel_factor_exp(mut self, exp: f32) -> Self { + self.wheel_factor_exp = exp; + self + } +} + +/// Box (rubber-band) zoom settings. +#[derive(Clone, Copy, Debug)] +pub struct BoxZoomConfig { + /// Enable boxed zoom. + pub enabled: bool, + /// Which pointer button starts the box. + pub button: PointerButton, + /// Which modifiers must be down. Any `true` field here must be pressed at runtime. + pub required_mods: Modifiers, +} + +impl BoxZoomConfig { + #[inline] + pub const fn new(enabled: bool, button: PointerButton, required_mods: Modifiers) -> Self { + Self { + enabled, + button, + required_mods, + } + } +} + +/// All navigation & shortcut controls in one place. +#[derive(Clone, Copy, Debug)] +pub struct NavigationConfig { + /// Dragging (per axis). + pub drag: AxisToggle, + /// Scrolling/panning with mouse wheel/touchpad (per axis). + pub scroll: AxisToggle, + /// Axis-zoom-drag (drag on axis strips). + pub axis_zoom_drag: Vec2b, + /// Wheel/pinch zoom. + pub zoom: ZoomConfig, + /// Box zoom. + pub box_zoom: BoxZoomConfig, + /// What double-click reset does. + pub reset_behavior: ResetBehavior, + /// Allow double-click reset. + pub double_click_reset: bool, + /// Enable pinning (P/U/Delete by default). + pub pinning_enabled: bool, + /// Shortcut: fit to view (e.g., `Key::F`). `None` disables shortcut. + pub fit_to_view_key: Option, + /// Shortcut: restore original bounds (e.g., `Key::R`). `None` disables shortcut. + pub restore_original_key: Option, + /// Pin shortcuts. + pub pin_add_key: Option, + pub pin_remove_key: Option, + pub pins_clear_key: Option, +} + +impl Default for NavigationConfig { + fn default() -> Self { + Self { + drag: AxisToggle::new(true, Vec2b::new(true, true)), + scroll: AxisToggle::new(true, Vec2b::new(true, true)), + axis_zoom_drag: Vec2b::new(false, false), + zoom: ZoomConfig::new(true, Vec2b::new(true, true)) + .zoom_to_mouse(true) + .wheel_factor_exp(1.0), + box_zoom: BoxZoomConfig::new(false, PointerButton::Secondary, Modifiers::NONE), + reset_behavior: ResetBehavior::AutoFit, + double_click_reset: true, + pinning_enabled: true, + fit_to_view_key: Some(Key::F), + restore_original_key: Some(Key::R), + pin_add_key: Some(Key::P), + pin_remove_key: Some(Key::U), + pins_clear_key: Some(Key::Delete), + } + } +} +impl NavigationConfig { + #[allow(clippy::fn_params_excessive_bools)] + /// Helper used to migrate legacy per-field flags into a `NavigationConfig`. + pub fn from_legacy_flags( + allow_drag: Vec2b, + allow_zoom: Vec2b, + allow_scroll: Vec2b, + allow_axis_zoom_drag: Vec2b, + allow_double_click_reset: bool, + allow_boxed_zoom: bool, + boxed_zoom_button: PointerButton, + ) -> Self { + Self { + drag: AxisToggle::new(allow_drag.any(), allow_drag), + scroll: AxisToggle::new(allow_scroll.any(), allow_scroll), + axis_zoom_drag: allow_axis_zoom_drag, + zoom: ZoomConfig::new(allow_zoom.any(), allow_zoom) + .zoom_to_mouse(true) + .wheel_factor_exp(1.0), + box_zoom: BoxZoomConfig::new(allow_boxed_zoom, boxed_zoom_button, Modifiers::NONE), + reset_behavior: ResetBehavior::AutoFit, + double_click_reset: allow_double_click_reset, + ..Default::default() + } + } + + /// Builders for convenience. + #[inline] + pub fn drag(mut self, axis: Vec2b, enabled: bool) -> Self { + self.drag = AxisToggle::new(enabled, axis); + self + } + + #[inline] + pub fn scroll(mut self, axis: Vec2b, enabled: bool) -> Self { + self.scroll = AxisToggle::new(enabled, axis); + self + } + + #[inline] + pub fn axis_zoom_drag(mut self, axis: Vec2b) -> Self { + self.axis_zoom_drag = axis; + self + } + + #[inline] + pub fn zoom(mut self, cfg: ZoomConfig) -> Self { + self.zoom = cfg; + self + } + + #[inline] + pub fn box_zoom(mut self, cfg: BoxZoomConfig) -> Self { + self.box_zoom = cfg; + self + } + + #[inline] + pub fn reset_behavior(mut self, behavior: ResetBehavior) -> Self { + self.reset_behavior = behavior; + self + } + + #[inline] + pub fn double_click_reset(mut self, on: bool) -> Self { + self.double_click_reset = on; + self + } + + #[inline] + pub fn pinning(mut self, on: bool) -> Self { + self.pinning_enabled = on; + self + } + + #[inline] + pub fn shortcuts_fit_restore(mut self, fit: Option, restore: Option) -> Self { + self.fit_to_view_key = fit; + self.restore_original_key = restore; + self + } + + #[inline] + pub fn shortcuts_pin( + mut self, + add: Option, + remove: Option, + clear: Option, + ) -> Self { + self.pin_add_key = add; + self.pin_remove_key = remove; + self.pins_clear_key = clear; + self + } +} From f0ee5deed12e214360b0e1f538ae99f998b16ebc Mon Sep 17 00:00:00 2001 From: 0xb-s <145866191+0xb-s@users.noreply.github.com> Date: Sat, 15 Nov 2025 08:22:01 -0800 Subject: [PATCH 04/14] Update plot_ui.rs --- egui_plot/src/plot_ui.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/egui_plot/src/plot_ui.rs b/egui_plot/src/plot_ui.rs index 07e13101..abc0b397 100644 --- a/egui_plot/src/plot_ui.rs +++ b/egui_plot/src/plot_ui.rs @@ -2,7 +2,9 @@ use std::ops::RangeInclusive; use egui::{Color32, Pos2, Response, Vec2, Vec2b, epaint::Hsva}; -use crate::{PlotBounds, PlotItem, PlotPoint, PlotTransform, action::ActionQueue}; +use crate::{ + NavigationConfig, PlotBounds, PlotItem, PlotPoint, PlotTransform, action::ActionQueue, +}; #[allow(unused_imports)] // for links in docstrings use crate::Plot; @@ -17,9 +19,14 @@ pub struct PlotUi<'a> { pub(crate) last_auto_bounds: Vec2b, pub(crate) response: Response, pub(crate) called_once: bool, + pub(crate) navigation: NavigationConfig, } impl<'a> PlotUi<'a> { + #[inline] + pub fn navigation_config(&self) -> &NavigationConfig { + &self.navigation + } #[inline] pub fn set_segmented_x_axis(&mut self, segment: Option) { self.last_plot_transform.set_segment_xaxis(segment); From 72b6aea2167b9409d017c01b371f8b9ef8aca54d Mon Sep 17 00:00:00 2001 From: 0xb-s <145866191+0xb-s@users.noreply.github.com> Date: Sat, 15 Nov 2025 08:22:40 -0800 Subject: [PATCH 05/14] Update tooltip.rs --- egui_plot/src/items/tooltip.rs | 46 +++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/egui_plot/src/items/tooltip.rs b/egui_plot/src/items/tooltip.rs index 19348f2e..094c8355 100644 --- a/egui_plot/src/items/tooltip.rs +++ b/egui_plot/src/items/tooltip.rs @@ -40,8 +40,7 @@ //! - Series highlighting currently matches by **series name**. Prefer unique names. use egui::{ - self, Align2, Area, Color32, Frame, Grid, Id, Key, Order, Pos2, Rect, RichText, Stroke, - TextStyle, + self, Align2, Area, Color32, Frame, Grid, Id, Order, Pos2, Rect, RichText, Stroke, TextStyle, }; use crate::{PlotPoint, PlotUi, items::PlotGeometry}; @@ -193,6 +192,7 @@ impl PlotUi<'_> { let transform = self.transform().clone(); let frame = transform.frame(); + let nav = *self.navigation_config(); // Draw existing pins (rails + markers) on a foreground layer: let mut pins = load_pins(&ctx, self.response.id); draw_pins_overlay( @@ -391,13 +391,17 @@ impl PlotUi<'_> { } if hits.is_empty() { - if self.response.hovered() { + if self.response.hovered() && nav.pinning_enabled { ctx.input(|i| { - if i.key_pressed(Key::U) { - pins.pop(); + if let Some(k) = nav.pin_remove_key { + if i.key_pressed(k) { + pins.pop(); + } } - if i.key_pressed(Key::Delete) { - pins.clear(); + if let Some(k) = nav.pins_clear_key { + if i.key_pressed(k) { + pins.clear(); + } } }); save_pins(&ctx, self.response.id, pins); @@ -422,20 +426,26 @@ impl PlotUi<'_> { } } - if self.response.hovered() { + if self.response.hovered() && nav.pinning_enabled { ctx.input(|i| { - if i.key_pressed(Key::P) { - let pointer_plot = transform.value_from_position(pointer_screen); - pins.push(PinnedPoints { - hits: hits.clone(), - plot_x: pointer_plot.x, - }); + if let Some(k) = nav.pin_add_key { + if i.key_pressed(k) { + let pointer_plot = transform.value_from_position(pointer_screen); + pins.push(PinnedPoints { + hits: hits.clone(), + plot_x: pointer_plot.x, + }); + } } - if i.key_pressed(Key::U) { - pins.pop(); + if let Some(k) = nav.pin_remove_key { + if i.key_pressed(k) { + pins.pop(); + } } - if i.key_pressed(Key::Delete) { - pins.clear(); + if let Some(k) = nav.pins_clear_key { + if i.key_pressed(k) { + pins.clear(); + } } }); save_pins(&ctx, self.response.id, pins.clone()); From 154e1c406c8133bfb712a629030edb05336f24dc Mon Sep 17 00:00:00 2001 From: 0xb-s <145866191+0xb-s@users.noreply.github.com> Date: Sat, 15 Nov 2025 08:23:18 -0800 Subject: [PATCH 06/14] Create Cargo.toml --- examples/navigation/Cargo.toml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 examples/navigation/Cargo.toml diff --git a/examples/navigation/Cargo.toml b/examples/navigation/Cargo.toml new file mode 100644 index 00000000..780f0182 --- /dev/null +++ b/examples/navigation/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "navigation" +edition.workspace = true +license.workspace = true +rust-version.workspace = true +version.workspace = true + +[dependencies] +eframe = { workspace = true, features = ["default"] } +egui_plot.workspace = true +[lints] +workspace = true From ef2867140b595287a84f830454a8ddc4417ff6e2 Mon Sep 17 00:00:00 2001 From: 0xb-s <145866191+0xb-s@users.noreply.github.com> Date: Sat, 15 Nov 2025 08:24:17 -0800 Subject: [PATCH 07/14] Create main.rs --- examples/navigation/src/main.rs | 80 +++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 examples/navigation/src/main.rs diff --git a/examples/navigation/src/main.rs b/examples/navigation/src/main.rs new file mode 100644 index 00000000..bfea199b --- /dev/null +++ b/examples/navigation/src/main.rs @@ -0,0 +1,80 @@ +#![allow(rustdoc::missing_crate_level_docs)] + +use eframe::{App, Frame, egui}; +use egui::{Color32, Context, Key, Modifiers, PointerButton, Vec2b}; +use egui_plot::{ + Line, Plot, TooltipOptions, {BoxZoomConfig, NavigationConfig, ResetBehavior, ZoomConfig}, +}; + +fn main() -> eframe::Result<()> { + eframe::run_native( + "Line::new_xy + custom navigation", + eframe::NativeOptions::default(), + Box::new(|_| Ok(Box::new(Demo::new()))), + ) +} + +struct Demo { + xs: Vec, + f1: Vec, + f2: Vec, +} + +impl Demo { + fn new() -> Self { + let n = 500; + let xs: Vec = (0..n).map(|i| i as f64 * 0.02).collect(); + let f1: Vec = xs.iter().map(|&t| t.sin()).collect(); + let f2: Vec = xs + .iter() + .map(|&t| (t * 0.6 + 0.8).sin() * 0.8 + 0.2) + .collect(); + + Self { xs, f1, f2 } + } +} + +impl App for Demo { + fn update(&mut self, ctx: &Context, _frame: &mut Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("Line::new_xy + completely custom navigation"); + + let nav = NavigationConfig::default() + .drag(Vec2b::new(true, false), true) + .scroll(Vec2b::new(true, false), true) + .axis_zoom_drag(Vec2b::new(true, false)) + .zoom( + ZoomConfig::new(true, Vec2b::new(true, true)) + .zoom_to_mouse(true) + .wheel_factor_exp(1.15), + ) + .box_zoom(BoxZoomConfig::new( + true, + PointerButton::Secondary, + Modifiers { + shift: true, + ..Modifiers::NONE + }, + )) + .reset_behavior(ResetBehavior::OriginalBounds) + .double_click_reset(true) + .shortcuts_fit_restore(Some(Key::F), Some(Key::R)) + .shortcuts_pin(Some(Key::D), Some(Key::U), Some(Key::Delete)); + + Plot::new("demo_plot").navigation(nav).show(ui, |plot_ui| { + plot_ui.line( + Line::new_xy("f1", &self.xs, &self.f1) + .color(Color32::from_rgb(200, 100, 100)) + .width(2.0), + ); + plot_ui.line( + Line::new_xy("f2", &self.xs, &self.f2) + .color(Color32::from_rgb(100, 160, 240)) + .width(2.0), + ); + + plot_ui.show_tooltip_with_options(&TooltipOptions::default()); + }); + }); + } +} From acdbe675b8beb176fcc7642c74737e34c2f54efd Mon Sep 17 00:00:00 2001 From: 0xb-s <145866191+0xb-s@users.noreply.github.com> Date: Sun, 16 Nov 2025 04:47:08 -0800 Subject: [PATCH 08/14] Update lib.rs --- egui_plot/src/lib.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/egui_plot/src/lib.rs b/egui_plot/src/lib.rs index f3a94d4d..89576564 100644 --- a/egui_plot/src/lib.rs +++ b/egui_plot/src/lib.rs @@ -984,10 +984,6 @@ impl<'a> Plot<'a> { y_axis_thickness: Default::default(), original_bounds: None, }); - // Remember the very first bounds shown to the user (for ResetBehavior::OriginalBounds) - if mem.original_bounds.is_none() { - mem.original_bounds = Some(*mem.transform.bounds()); - } let last_plot_transform = mem.transform.clone(); // Call the plot build function. @@ -1689,6 +1685,9 @@ impl<'a> Plot<'a> { let old_bounds = *last_plot_transform.bounds(); let new_bounds = *mem.transform.bounds(); + if mem.original_bounds.is_none() { + mem.original_bounds = Some(new_bounds); + } if old_bounds != new_bounds { events.push(PlotEvent::BoundsChanged { old: old_bounds, From e0b0f29d84ca3c9ca73e910f3a8c4292e7f7a0cc Mon Sep 17 00:00:00 2001 From: 0xb-s <145866191+0xb-s@users.noreply.github.com> Date: Sun, 16 Nov 2025 05:43:01 -0800 Subject: [PATCH 09/14] Update links.yml --- .github/workflows/links.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml index 89778615..72e1a4cb 100644 --- a/.github/workflows/links.yml +++ b/.github/workflows/links.yml @@ -30,4 +30,4 @@ jobs: fail: true # When given a directory, lychee checks only markdown, html and text files, everything else we have to glob in manually. args: | - --base . --cache --max-cache-age 1d . "**/*.rs" "**/*.toml" "**/*.hpp" "**/*.cpp" "**/CMakeLists.txt" "**/*.py" "**/*.yml" + --cache --max-cache-age 1d . "**/*.rs" "**/*.toml" "**/*.hpp" "**/*.cpp" "**/CMakeLists.txt" "**/*.py" "**/*.yml" From 08e648564599295c1e6ffeddab1c0e3e03402075 Mon Sep 17 00:00:00 2001 From: 0xb-s <145866191+0xb-s@users.noreply.github.com> Date: Sun, 16 Nov 2025 05:44:30 -0800 Subject: [PATCH 10/14] Update bound.rs --- egui_plot/src/bound.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/egui_plot/src/bound.rs b/egui_plot/src/bound.rs index 1e135d77..cc9fb5c4 100644 --- a/egui_plot/src/bound.rs +++ b/egui_plot/src/bound.rs @@ -15,7 +15,6 @@ impl Interval { let start_inf = self.start.is_infinite(); let end_inf = self.end.is_infinite(); - if start_inf && end_inf { // (-∞, +∞) if self.start.is_sign_negative() && self.end.is_sign_positive() { @@ -26,12 +25,10 @@ impl Interval { return 0.0; } - if start_inf || end_inf { return f64::INFINITY; } - (self.end - self.start).max(0.0) } From d68d242461f692fda8ae04221063950b710fdc25 Mon Sep 17 00:00:00 2001 From: 0xb-s <145866191+0xb-s@users.noreply.github.com> Date: Sun, 16 Nov 2025 05:44:52 -0800 Subject: [PATCH 11/14] Update transform.rs --- egui_plot/src/transform.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui_plot/src/transform.rs b/egui_plot/src/transform.rs index 353d200d..9a8641aa 100644 --- a/egui_plot/src/transform.rs +++ b/egui_plot/src/transform.rs @@ -660,7 +660,7 @@ impl PlotTransform { let total_len: f64 = bx .segments .iter() - .map(|seg| seg.len().max(f64::EPSILON)) + .map(|seg| seg.len().max(f64::EPSILON)) .sum(); let total_gap_px: f32 = if n >= 2 { From 5fe688bf4c202add587c38b8be3217112c14b8bc Mon Sep 17 00:00:00 2001 From: 0xb-s <145866191+0xb-s@users.noreply.github.com> Date: Thu, 20 Nov 2025 00:19:30 -0800 Subject: [PATCH 12/14] Update navigation.rs --- egui_plot/src/navigation.rs | 113 +++++++++++++++++++++++++++--------- 1 file changed, 86 insertions(+), 27 deletions(-) diff --git a/egui_plot/src/navigation.rs b/egui_plot/src/navigation.rs index bec527f8..0629d6ef 100644 --- a/egui_plot/src/navigation.rs +++ b/egui_plot/src/navigation.rs @@ -5,8 +5,6 @@ use egui::{Key, Modifiers, PointerButton, Vec2b}; /// A reset operation. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ResetBehavior { - /// Reset by auto-fitting bounds to visible content. - AutoFit, /// Restore the original bounds from the first frame the plot was shown. OriginalBounds, } @@ -108,8 +106,7 @@ pub struct NavigationConfig { pub pinning_enabled: bool, /// Shortcut: fit to view (e.g., `Key::F`). `None` disables shortcut. pub fit_to_view_key: Option, - /// Shortcut: restore original bounds (e.g., `Key::R`). `None` disables shortcut. - pub restore_original_key: Option, + /// Pin shortcuts. pub pin_add_key: Option, pub pin_remove_key: Option, @@ -126,11 +123,11 @@ impl Default for NavigationConfig { .zoom_to_mouse(true) .wheel_factor_exp(1.0), box_zoom: BoxZoomConfig::new(false, PointerButton::Secondary, Modifiers::NONE), - reset_behavior: ResetBehavior::AutoFit, + reset_behavior: ResetBehavior::OriginalBounds, double_click_reset: true, pinning_enabled: true, fit_to_view_key: Some(Key::F), - restore_original_key: Some(Key::R), + pin_add_key: Some(Key::P), pin_remove_key: Some(Key::U), pins_clear_key: Some(Key::Delete), @@ -139,7 +136,7 @@ impl Default for NavigationConfig { } impl NavigationConfig { #[allow(clippy::fn_params_excessive_bools)] - /// Helper used to migrate legacy per-field flags into a `NavigationConfig`. + /// Build a `NavigationConfig`. pub fn from_legacy_flags( allow_drag: Vec2b, allow_zoom: Vec2b, @@ -157,68 +154,130 @@ impl NavigationConfig { .zoom_to_mouse(true) .wheel_factor_exp(1.0), box_zoom: BoxZoomConfig::new(allow_boxed_zoom, boxed_zoom_button, Modifiers::NONE), - reset_behavior: ResetBehavior::AutoFit, - double_click_reset: allow_double_click_reset, - ..Default::default() + + ..Self::default().reset_controls( + ResetBehavior::OriginalBounds, + allow_double_click_reset, + Some(Key::R), + ) } } - /// Builders for convenience. + /// Configure drag behavior for the given axes. + /// + /// The `axes` parameter uses `(x, y)` ordering: + /// - `Some(Vec2b::new(true, true))` → drag on both X and Y + /// - `Some(Vec2b::new(true, false))` → drag on X only + /// - `Some(Vec2b::new(false, true))` → drag on Y only + /// - `None` → dragging completely disabled #[inline] - pub fn drag(mut self, axis: Vec2b, enabled: bool) -> Self { - self.drag = AxisToggle::new(enabled, axis); + pub fn drag(mut self, axes: Option) -> Self { + match axes { + Some(axis) => { + self.drag = AxisToggle::new(true, axis); + } + None => { + self.drag = AxisToggle::new(false, Vec2b::new(false, false)); + } + } self } + /// Configure scrolling/panning with the mouse wheel or touchpad. + /// + /// Same `(x, y)` ordering as `drag`: + /// - `Some(Vec2b::new(true, false))` → scroll horizontally only + /// - `None` → disable scroll-based navigation #[inline] - pub fn scroll(mut self, axis: Vec2b, enabled: bool) -> Self { - self.scroll = AxisToggle::new(enabled, axis); + pub fn scroll(mut self, axes: Option) -> Self { + match axes { + Some(axis) => { + self.scroll = AxisToggle::new(true, axis); + } + None => { + self.scroll = AxisToggle::new(false, Vec2b::new(false, false)); + } + } self } + /// Configure zoom-drag on the axis strips. + /// + /// `axis` selects which axes can be zoomed by dragging on their axis strips. #[inline] - pub fn axis_zoom_drag(mut self, axis: Vec2b) -> Self { + pub fn axis_zoom(mut self, axis: Vec2b) -> Self { self.axis_zoom_drag = axis; self } + /// Set the full zoom configuration. #[inline] - pub fn zoom(mut self, cfg: ZoomConfig) -> Self { + pub fn scroll_zoom(mut self, cfg: ZoomConfig) -> Self { self.zoom = cfg; self } + /// Set the box-zoom configuration. #[inline] pub fn box_zoom(mut self, cfg: BoxZoomConfig) -> Self { self.box_zoom = cfg; self } + /// Configure all reset-related controls in a single place. + /// + /// `behavior` defines how reset behaves, `double_click` toggles double-click + /// reset, and `fit_key` / `restore_key` configure keyboard shortcuts. #[inline] - pub fn reset_behavior(mut self, behavior: ResetBehavior) -> Self { + pub fn reset_controls( + mut self, + behavior: ResetBehavior, + double_click: bool, + fit_key: Option, + ) -> Self { self.reset_behavior = behavior; + self.double_click_reset = double_click; + self.fit_to_view_key = fit_key; + self } + /// Set the reset behavior + /// + /// This keeps other reset-related fields (double click, shortcuts) unchanged. #[inline] - pub fn double_click_reset(mut self, on: bool) -> Self { - self.double_click_reset = on; - self + pub fn reset_behavior(self, behavior: ResetBehavior) -> Self { + self.reset_controls(behavior, self.double_click_reset, self.fit_to_view_key) } + /// Enable or disable double-click reset. + /// + /// This keeps the reset behavior and shortcuts unchanged. #[inline] - pub fn pinning(mut self, on: bool) -> Self { - self.pinning_enabled = on; - self + pub fn double_click_reset(self, on: bool) -> Self { + self.reset_controls(self.reset_behavior, on, self.fit_to_view_key) + } + + /// Configure keyboard shortcuts for "fit to view" and "restore original". + /// + /// Pass `None` to disable a shortcut. + #[inline] + pub fn shortcuts_fit_restore(self, fit: Option) -> Self { + self.reset_controls(self.reset_behavior, self.double_click_reset, fit) } + /// Enable or disable pinning (tooltip pin add/remove/clear). + /// + /// This affects keyboard shortcuts for pins and any pin-related UI. #[inline] - pub fn shortcuts_fit_restore(mut self, fit: Option, restore: Option) -> Self { - self.fit_to_view_key = fit; - self.restore_original_key = restore; + pub fn pinning(mut self, on: bool) -> Self { + self.pinning_enabled = on; self } + /// Configure keyboard shortcuts for pin management. + /// + /// `add`, `remove`, and `clear` control pin creation and deletion. #[inline] pub fn shortcuts_pin( mut self, From f72d6cc242bb417caf5df81cce30744df355a51e Mon Sep 17 00:00:00 2001 From: 0xb-s <145866191+0xb-s@users.noreply.github.com> Date: Thu, 20 Nov 2025 00:22:22 -0800 Subject: [PATCH 13/14] Update main.rs --- examples/navigation/src/main.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/navigation/src/main.rs b/examples/navigation/src/main.rs index bfea199b..881b69d2 100644 --- a/examples/navigation/src/main.rs +++ b/examples/navigation/src/main.rs @@ -40,10 +40,10 @@ impl App for Demo { ui.heading("Line::new_xy + completely custom navigation"); let nav = NavigationConfig::default() - .drag(Vec2b::new(true, false), true) - .scroll(Vec2b::new(true, false), true) - .axis_zoom_drag(Vec2b::new(true, false)) - .zoom( + .drag(Some(Vec2b::new(true, false))) + .scroll(Some(Vec2b::new(true, false))) + .axis_zoom(Vec2b::new(true, false)) + .scroll_zoom( ZoomConfig::new(true, Vec2b::new(true, true)) .zoom_to_mouse(true) .wheel_factor_exp(1.15), @@ -58,7 +58,7 @@ impl App for Demo { )) .reset_behavior(ResetBehavior::OriginalBounds) .double_click_reset(true) - .shortcuts_fit_restore(Some(Key::F), Some(Key::R)) + .shortcuts_fit_restore(Some(Key::R)) .shortcuts_pin(Some(Key::D), Some(Key::U), Some(Key::Delete)); Plot::new("demo_plot").navigation(nav).show(ui, |plot_ui| { From 657ddd920ae9e6e6b281513b1b7cfc747bb7ef0e Mon Sep 17 00:00:00 2001 From: 0xb-s <145866191+0xb-s@users.noreply.github.com> Date: Thu, 20 Nov 2025 00:26:24 -0800 Subject: [PATCH 14/14] Update lib.rs --- egui_plot/src/lib.rs | 51 +++++++++++--------------------------------- 1 file changed, 13 insertions(+), 38 deletions(-) diff --git a/egui_plot/src/lib.rs b/egui_plot/src/lib.rs index 89576564..6d657370 100644 --- a/egui_plot/src/lib.rs +++ b/egui_plot/src/lib.rs @@ -1102,36 +1102,22 @@ impl<'a> Plot<'a> { }); } - // Double-click reset (configurable rn) + // Double-click reset to original bounds (if configured). if nav.double_click_reset && response.double_clicked() { - match nav.reset_behavior { - ResetBehavior::AutoFit => { - mem.auto_bounds = true.into(); - events.push(PlotEvent::ResetApplied { - input: InputInfo { - pointer: ui.input(|i| i.pointer.hover_pos()), - button: Some(PointerButton::Primary), - modifiers: ui.input(|i| i.modifiers), - }, - }); - last_user_cause = Some(BoundsChangeCause::Reset); - } - ResetBehavior::OriginalBounds => { - if let Some(orig) = mem.original_bounds { - bounds = orig; + if let Some(orig) = mem.original_bounds { + bounds = orig; - mem.auto_bounds = false.into(); + // Once the user explicitly resets, stop auto-bounds. + mem.auto_bounds = false.into(); - events.push(PlotEvent::ResetApplied { - input: InputInfo { - pointer: ui.input(|i| i.pointer.hover_pos()), - button: Some(PointerButton::Primary), - modifiers: ui.input(|i| i.modifiers), - }, - }); - last_user_cause = Some(BoundsChangeCause::Reset); - } - } + events.push(PlotEvent::ResetApplied { + input: InputInfo { + pointer: ui.input(|i| i.pointer.hover_pos()), + button: Some(PointerButton::Primary), + modifiers: ui.input(|i| i.modifiers), + }, + }); + last_user_cause = Some(BoundsChangeCause::Reset); } } @@ -1640,17 +1626,6 @@ impl<'a> Plot<'a> { } } - //Restore original bounds shortcut - if let Some(k) = nav.restore_original_key { - if ui.ctx().input(|i| i.key_pressed(k)) { - if let Some(orig) = mem.original_bounds { - mem.transform.set_bounds(orig); - mem.auto_bounds = false.into(); - last_user_cause = Some(BoundsChangeCause::Reset); - } - } - } - // Pinning shortcuts if nav.pinning_enabled { if let Some(k) = nav.pin_add_key {