From 61844a6fc55e46749d0ce2e23ca793c32ba69920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20K=C3=A5re=20Alsaker?= Date: Thu, 8 Sep 2022 04:55:14 +0200 Subject: [PATCH 1/4] Make double click reset to the initial view --- crates/egui/src/widgets/plot/mod.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index 9e82d2565e5..6dc99bf7f7b 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -73,7 +73,7 @@ impl Default for CoordinatesFormatter { const MIN_LINE_SPACING_IN_POINTS: f64 = 6.0; // TODO(emilk): large enough for a wide label #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[derive(Clone)] +#[derive(Copy, Clone)] struct AutoBounds { x: bool, y: bool, @@ -658,11 +658,13 @@ impl Plot { // Allocate the space. let (rect, response) = ui.allocate_exact_size(size, Sense::drag()); + let initial_auto_bounds = (!min_auto_bounds.is_valid()).into(); + // Load or initialize the memory. let plot_id = ui.make_persistent_id(id_source); ui.ctx().check_for_id_clash(plot_id, rect, "Plot"); let mut memory = PlotMemory::load(ui.ctx(), plot_id).unwrap_or_else(|| PlotMemory { - auto_bounds: (!min_auto_bounds.is_valid()).into(), + auto_bounds: initial_auto_bounds, hovered_entry: None, hidden_items: Default::default(), min_auto_bounds, @@ -678,7 +680,7 @@ impl Plot { // If the min bounds changed, recalculate everything. if min_auto_bounds != memory.min_auto_bounds { memory = PlotMemory { - auto_bounds: (!min_auto_bounds.is_valid()).into(), + auto_bounds: initial_auto_bounds, hovered_entry: None, min_auto_bounds, ..memory @@ -785,9 +787,10 @@ impl Plot { } }; - // Allow double clicking to reset to automatic bounds. + // Allow double clicking to reset to the initial bounds. if response.double_clicked_by(PointerButton::Primary) { - auto_bounds = true.into(); + bounds = min_auto_bounds; + auto_bounds = initial_auto_bounds; } if !bounds.is_valid() { From 24a57cac98012a1d074fa9bc3ccbad304d8c1925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20K=C3=A5re=20Alsaker?= Date: Thu, 8 Sep 2022 06:26:58 +0200 Subject: [PATCH 2/4] Allow forcing auto bounds --- crates/egui/src/widgets/plot/mod.rs | 108 ++++++++++------------ crates/egui/src/widgets/plot/transform.rs | 23 ++++- 2 files changed, 68 insertions(+), 63 deletions(-) diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index 6dc99bf7f7b..5e791921478 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -74,24 +74,14 @@ const MIN_LINE_SPACING_IN_POINTS: f64 = 6.0; // TODO(emilk): large enough for a #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[derive(Copy, Clone)] -struct AutoBounds { +struct AxisBools { x: bool, y: bool, } -impl AutoBounds { - fn from_bool(val: bool) -> Self { - AutoBounds { x: val, y: val } - } - - fn any(&self) -> bool { - self.x || self.y - } -} - -impl From for AutoBounds { +impl From for AxisBools { fn from(val: bool) -> Self { - AutoBounds::from_bool(val) + AxisBools { x: val, y: val } } } @@ -99,10 +89,9 @@ impl From for AutoBounds { #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[derive(Clone)] struct PlotMemory { - auto_bounds: AutoBounds, + modified: AxisBools, hovered_entry: Option, hidden_items: ahash::HashSet, - min_auto_bounds: PlotBounds, last_screen_transform: ScreenTransform, /// Allows to remember the first click position when performing a boxed zoom last_click_pos_for_zoom: Option, @@ -268,6 +257,7 @@ pub struct Plot { allow_zoom: bool, allow_drag: bool, allow_scroll: bool, + auto_bounds: AxisBools, min_auto_bounds: PlotBounds, margin_fraction: Vec2, allow_boxed_zoom: bool, @@ -303,6 +293,7 @@ impl Plot { allow_zoom: true, allow_drag: true, allow_scroll: true, + auto_bounds: false.into(), min_auto_bounds: PlotBounds::NOTHING, margin_fraction: Vec2::splat(0.05), allow_boxed_zoom: true, @@ -402,7 +393,7 @@ impl Plot { self } - /// Set the side margin as a fraction of the plot size. + /// Set the side margin as a fraction of the plot size. Only used for auto bounds. /// /// For instance, a value of `0.1` will add 10% space on both sides. pub fn set_margin_fraction(mut self, margin_fraction: Vec2) -> Self { @@ -556,6 +547,18 @@ impl Plot { self } + /// Expand bounds to fit all items across the x axis, including values given by `include_x`. + pub fn auto_bounds_x(mut self) -> Self { + self.auto_bounds.x = true; + self + } + + /// Expand bounds to fit all items across the y axis, including values given by `include_y`. + pub fn auto_bounds_y(mut self) -> Self { + self.auto_bounds.y = true; + self + } + /// Show a legend including all named items. pub fn legend(mut self, legend: Legend) -> Self { self.legend_config = Some(legend); @@ -611,6 +614,7 @@ impl Plot { allow_drag, allow_boxed_zoom, boxed_zoom_pointer_button: boxed_zoom_pointer, + auto_bounds, min_auto_bounds, margin_fraction, width, @@ -658,16 +662,13 @@ impl Plot { // Allocate the space. let (rect, response) = ui.allocate_exact_size(size, Sense::drag()); - let initial_auto_bounds = (!min_auto_bounds.is_valid()).into(); - // Load or initialize the memory. let plot_id = ui.make_persistent_id(id_source); ui.ctx().check_for_id_clash(plot_id, rect, "Plot"); - let mut memory = PlotMemory::load(ui.ctx(), plot_id).unwrap_or_else(|| PlotMemory { - auto_bounds: initial_auto_bounds, + let memory = PlotMemory::load(ui.ctx(), plot_id).unwrap_or_else(|| PlotMemory { + modified: false.into(), hovered_entry: None, hidden_items: Default::default(), - min_auto_bounds, last_screen_transform: ScreenTransform::new( rect, min_auto_bounds, @@ -677,24 +678,12 @@ impl Plot { last_click_pos_for_zoom: None, }); - // If the min bounds changed, recalculate everything. - if min_auto_bounds != memory.min_auto_bounds { - memory = PlotMemory { - auto_bounds: initial_auto_bounds, - hovered_entry: None, - min_auto_bounds, - ..memory - }; - memory.clone().store(ui.ctx(), plot_id); - } - let PlotMemory { - mut auto_bounds, + mut modified, mut hovered_entry, mut hidden_items, last_screen_transform, mut last_click_pos_for_zoom, - .. } = memory; // Call the plot build function. @@ -776,53 +765,51 @@ impl Plot { if let Some(linked_bounds) = axes.get() { if axes.link_x { bounds.set_x(&linked_bounds); - // Turn off auto bounds to keep it from overriding what we just set. - auto_bounds.x = false; + // Mark the axis as modified to prevent it from being changed. + modified.x = true; } if axes.link_y { bounds.set_y(&linked_bounds); - // Turn off auto bounds to keep it from overriding what we just set. - auto_bounds.y = false; + // Mark the axis as modified to prevent it from being changed. + modified.y = true; } } }; // Allow double clicking to reset to the initial bounds. if response.double_clicked_by(PointerButton::Primary) { - bounds = min_auto_bounds; - auto_bounds = initial_auto_bounds; + modified = false.into(); } - if !bounds.is_valid() { - auto_bounds = true.into(); + // Reset bounds to initial bounds if we haven't been modified. + if !modified.x { + bounds.set_x(&min_auto_bounds); + } + if !modified.y { + bounds.set_y(&min_auto_bounds); } - // Set bounds automatically based on content. - if auto_bounds.any() { - if auto_bounds.x { - bounds.set_x(&min_auto_bounds); - } - - if auto_bounds.y { - bounds.set_y(&min_auto_bounds); - } + let auto_x = !modified.x && (!min_auto_bounds.is_valid_x() || auto_bounds.x); + let auto_y = !modified.y && (!min_auto_bounds.is_valid_y() || auto_bounds.y); + // Set bounds automatically based on content. + if auto_x || auto_y { for item in &items { let item_bounds = item.bounds(); - if auto_bounds.x { + if auto_x { bounds.merge_x(&item_bounds); } - if auto_bounds.y { + if auto_y { bounds.merge_y(&item_bounds); } } - if auto_bounds.x { + if auto_x { bounds.add_relative_margin_x(margin_fraction); } - if auto_bounds.y { + if auto_y { bounds.add_relative_margin_y(margin_fraction); } } @@ -843,7 +830,7 @@ impl Plot { if allow_drag && response.dragged_by(PointerButton::Primary) { response = response.on_hover_cursor(CursorIcon::Grabbing); transform.translate_bounds(-response.drag_delta()); - auto_bounds = false.into(); + modified = true.into(); } // Zooming @@ -890,7 +877,7 @@ impl Plot { }; if new_bounds.is_valid() { transform.set_bounds(new_bounds); - auto_bounds = false.into(); + modified = true.into(); } // reset the boxed zoom state last_click_pos_for_zoom = None; @@ -907,14 +894,14 @@ impl Plot { }; if zoom_factor != Vec2::splat(1.0) { transform.zoom(zoom_factor, hover_pos); - auto_bounds = false.into(); + modified = true.into(); } } if allow_scroll { let scroll_delta = ui.input().scroll_delta; if scroll_delta != Vec2::ZERO { transform.translate_bounds(-scroll_delta); - auto_bounds = false.into(); + modified = true.into(); } } } @@ -964,10 +951,9 @@ impl Plot { } let memory = PlotMemory { - auto_bounds, + modified, hovered_entry, hidden_items, - min_auto_bounds, last_screen_transform: transform, last_click_pos_for_zoom, }; diff --git a/crates/egui/src/widgets/plot/transform.rs b/crates/egui/src/widgets/plot/transform.rs index 584d0a0a82f..6abfe2616e3 100644 --- a/crates/egui/src/widgets/plot/transform.rs +++ b/crates/egui/src/widgets/plot/transform.rs @@ -40,10 +40,26 @@ impl PlotBounds { && self.max[1].is_finite() } + pub fn is_finite_x(&self) -> bool { + self.min[0].is_finite() && self.max[0].is_finite() + } + + pub fn is_finite_y(&self) -> bool { + self.min[1].is_finite() && self.max[1].is_finite() + } + pub fn is_valid(&self) -> bool { self.is_finite() && self.width() > 0.0 && self.height() > 0.0 } + pub fn is_valid_x(&self) -> bool { + self.is_finite_x() && self.width() > 0.0 + } + + pub fn is_valid_y(&self) -> bool { + self.is_finite_y() && self.height() > 0.0 + } + pub fn width(&self) -> f64 { self.max[0] - self.min[0] } @@ -181,8 +197,11 @@ pub(crate) struct ScreenTransform { impl ScreenTransform { pub fn new(frame: Rect, mut bounds: PlotBounds, x_centered: bool, y_centered: bool) -> Self { // Make sure they are not empty. - if !bounds.is_valid() { - bounds = PlotBounds::new_symmetrical(1.0); + if !bounds.is_valid_x() { + bounds.set_x(&PlotBounds::new_symmetrical(1.0)); + } + if !bounds.is_valid_y() { + bounds.set_y(&PlotBounds::new_symmetrical(1.0)); } // Scale axes so that the origin is in the center. From cf35b0265f4bf91b4e142f0e03657ecad407d7c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20K=C3=A5re=20Alsaker?= Date: Thu, 8 Sep 2022 06:57:35 +0200 Subject: [PATCH 3/4] Allow plots to be reset --- crates/egui/src/widgets/plot/mod.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index 5e791921478..184de69e3b2 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -271,6 +271,8 @@ pub struct Plot { data_aspect: Option, view_aspect: Option, + reset: bool, + show_x: bool, show_y: bool, label_formatter: LabelFormatter, @@ -307,6 +309,8 @@ impl Plot { data_aspect: None, view_aspect: None, + reset: false, + show_x: true, show_y: true, label_formatter: None, @@ -595,6 +599,12 @@ impl Plot { self } + /// Resets the plot. + pub fn reset(mut self) -> Self { + self.reset = true; + self + } + /// Interact with and add items to the plot and finally draw it. pub fn show(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> InnerResponse { self.show_dyn(ui, Box::new(build_fn)) @@ -628,6 +638,7 @@ impl Plot { coordinates_formatter, axis_formatters, legend_config, + reset, show_background, show_axes, linked_axes, @@ -665,7 +676,16 @@ impl Plot { // Load or initialize the memory. let plot_id = ui.make_persistent_id(id_source); ui.ctx().check_for_id_clash(plot_id, rect, "Plot"); - let memory = PlotMemory::load(ui.ctx(), plot_id).unwrap_or_else(|| PlotMemory { + let memory = if reset { + if let Some(axes) = linked_axes.as_ref() { + axes.bounds.set(None); + }; + + None + } else { + PlotMemory::load(ui.ctx(), plot_id) + } + .unwrap_or_else(|| PlotMemory { modified: false.into(), hovered_entry: None, hidden_items: Default::default(), From ad21c969d176d066c86fd363e0e8afc27cd372d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20K=C3=A5re=20Alsaker?= Date: Tue, 27 Sep 2022 00:21:23 +0200 Subject: [PATCH 4/4] Rename `modified` to `bounds_modified` --- crates/egui/src/widgets/plot/mod.rs | 32 +++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index 184de69e3b2..14d1fa0f133 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -89,7 +89,9 @@ impl From for AxisBools { #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[derive(Clone)] struct PlotMemory { - modified: AxisBools, + /// Indicates if the user has modified the bounds, for example by moving or zooming, + /// or if the bounds should be calculated based by included point or auto bounds. + bounds_modified: AxisBools, hovered_entry: Option, hidden_items: ahash::HashSet, last_screen_transform: ScreenTransform, @@ -686,7 +688,7 @@ impl Plot { PlotMemory::load(ui.ctx(), plot_id) } .unwrap_or_else(|| PlotMemory { - modified: false.into(), + bounds_modified: false.into(), hovered_entry: None, hidden_items: Default::default(), last_screen_transform: ScreenTransform::new( @@ -699,7 +701,7 @@ impl Plot { }); let PlotMemory { - mut modified, + mut bounds_modified, mut hovered_entry, mut hidden_items, last_screen_transform, @@ -786,31 +788,31 @@ impl Plot { if axes.link_x { bounds.set_x(&linked_bounds); // Mark the axis as modified to prevent it from being changed. - modified.x = true; + bounds_modified.x = true; } if axes.link_y { bounds.set_y(&linked_bounds); // Mark the axis as modified to prevent it from being changed. - modified.y = true; + bounds_modified.y = true; } } }; // Allow double clicking to reset to the initial bounds. if response.double_clicked_by(PointerButton::Primary) { - modified = false.into(); + bounds_modified = false.into(); } // Reset bounds to initial bounds if we haven't been modified. - if !modified.x { + if !bounds_modified.x { bounds.set_x(&min_auto_bounds); } - if !modified.y { + if !bounds_modified.y { bounds.set_y(&min_auto_bounds); } - let auto_x = !modified.x && (!min_auto_bounds.is_valid_x() || auto_bounds.x); - let auto_y = !modified.y && (!min_auto_bounds.is_valid_y() || auto_bounds.y); + let auto_x = !bounds_modified.x && (!min_auto_bounds.is_valid_x() || auto_bounds.x); + let auto_y = !bounds_modified.y && (!min_auto_bounds.is_valid_y() || auto_bounds.y); // Set bounds automatically based on content. if auto_x || auto_y { @@ -850,7 +852,7 @@ impl Plot { if allow_drag && response.dragged_by(PointerButton::Primary) { response = response.on_hover_cursor(CursorIcon::Grabbing); transform.translate_bounds(-response.drag_delta()); - modified = true.into(); + bounds_modified = true.into(); } // Zooming @@ -897,7 +899,7 @@ impl Plot { }; if new_bounds.is_valid() { transform.set_bounds(new_bounds); - modified = true.into(); + bounds_modified = true.into(); } // reset the boxed zoom state last_click_pos_for_zoom = None; @@ -914,14 +916,14 @@ impl Plot { }; if zoom_factor != Vec2::splat(1.0) { transform.zoom(zoom_factor, hover_pos); - modified = true.into(); + bounds_modified = true.into(); } } if allow_scroll { let scroll_delta = ui.input().scroll_delta; if scroll_delta != Vec2::ZERO { transform.translate_bounds(-scroll_delta); - modified = true.into(); + bounds_modified = true.into(); } } } @@ -971,7 +973,7 @@ impl Plot { } let memory = PlotMemory { - modified, + bounds_modified, hovered_entry, hidden_items, last_screen_transform: transform,