From e6cd38e340c0f659ff1201d6bab9fbc09365f03b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20K=C3=A5re=20Alsaker?= Date: Tue, 7 Jun 2022 22:30:08 +0200 Subject: [PATCH 1/9] Allow cursors to be linked --- crates/egui/src/widgets/plot/items/mod.rs | 23 ++++-- crates/egui/src/widgets/plot/mod.rs | 86 ++++++++++++++++++++--- 2 files changed, 97 insertions(+), 12 deletions(-) diff --git a/crates/egui/src/widgets/plot/items/mod.rs b/crates/egui/src/widgets/plot/items/mod.rs index def04a458627..9cc89a969a50 100644 --- a/crates/egui/src/widgets/plot/items/mod.rs +++ b/crates/egui/src/widgets/plot/items/mod.rs @@ -1,6 +1,6 @@ //! Contains items that can be added to a plot. -use std::ops::RangeInclusive; +use std::{cell::RefCell, ops::RangeInclusive}; use epaint::util::FloatOrd; use epaint::Mesh; @@ -28,6 +28,7 @@ pub(super) struct PlotConfig<'a> { pub transform: &'a ScreenTransform, pub show_x: bool, pub show_y: bool, + pub cursors: &'a RefCell>, } /// Trait shared by things that can be drawn in the plot. @@ -1547,7 +1548,7 @@ impl PlotItem for BoxPlot { // ---------------------------------------------------------------------------- // Helper functions -fn rulers_color(ui: &Ui) -> Color32 { +pub(crate) fn rulers_color(ui: &Ui) -> Color32 { if ui.visuals().dark_mode { Color32::from_gray(100).additive() } else { @@ -1555,7 +1556,11 @@ fn rulers_color(ui: &Ui) -> Color32 { } } -fn vertical_line(pointer: Pos2, transform: &ScreenTransform, line_color: Color32) -> Shape { +pub(crate) fn vertical_line( + pointer: Pos2, + transform: &ScreenTransform, + line_color: Color32, +) -> Shape { let frame = transform.frame(); Shape::line_segment( [ @@ -1566,7 +1571,11 @@ fn vertical_line(pointer: Pos2, transform: &ScreenTransform, line_color: Color32 ) } -fn horizontal_line(pointer: Pos2, transform: &ScreenTransform, line_color: Color32) -> Shape { +pub(crate) fn horizontal_line( + pointer: Pos2, + transform: &ScreenTransform, + line_color: Color32, +) -> Shape { let frame = transform.frame(); Shape::line_segment( [ @@ -1661,9 +1670,15 @@ pub(super) fn rulers_at_value( let line_color = rulers_color(plot.ui); if plot.show_x { shapes.push(vertical_line(pointer, plot.transform, line_color)); + plot.cursors + .borrow_mut() + .push((Orientation::Vertical, value.x)); } if plot.show_y { shapes.push(horizontal_line(pointer, plot.transform, line_color)); + plot.cursors + .borrow_mut() + .push((Orientation::Horizontal, value.y)); } let mut prefix = String::new(); diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index d9c08afbb5d5..1709772e5299 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -1,6 +1,11 @@ //! Simple plotting library. -use std::{cell::Cell, ops::RangeInclusive, rc::Rc}; +use std::{ + cell::{Cell, RefCell}, + collections::HashMap, + ops::RangeInclusive, + rc::Rc, +}; use crate::*; use epaint::color::Hsva; @@ -17,6 +22,8 @@ pub use items::{ pub use legend::{Corner, Legend}; pub use transform::PlotBounds; +use self::items::{horizontal_line, rulers_color, vertical_line}; + mod items; mod legend; mod transform; @@ -121,7 +128,9 @@ impl PlotMemory { pub struct LinkedAxisGroup { pub(crate) link_x: bool, pub(crate) link_y: bool, + pub(crate) link_cursor: bool, pub(crate) bounds: Rc>>, + pub(crate) cursors: Rc>>>, } impl LinkedAxisGroup { @@ -129,7 +138,9 @@ impl LinkedAxisGroup { Self { link_x, link_y, + link_cursor: false, bounds: Rc::new(Cell::new(None)), + cursors: Rc::new(RefCell::new(HashMap::new())), } } @@ -160,6 +171,11 @@ impl LinkedAxisGroup { self.link_y = link; } + /// Change whether the cursor is linked for this group. By default it is unlinked. + pub fn set_link_cursor(&mut self, link: bool) { + self.link_cursor = link; + } + fn get(&self) -> Option { self.bounds.get() } @@ -660,8 +676,13 @@ impl Plot { // --- Bound computation --- let mut bounds = *last_screen_transform.bounds(); + let mut cursors = HashMap::new(); + // Transfer the bounds from a link group. if let Some(axes) = linked_axes.as_ref() { + if axes.link_cursor { + cursors = axes.cursors.borrow().clone(); + } if let Some(linked_bounds) = axes.get() { if axes.link_x { bounds.set_x(&linked_bounds); @@ -818,8 +839,11 @@ impl Plot { show_axes, transform: transform.clone(), grid_spacers, + draw_cursor_x: linked_axes.as_ref().map_or(false, |axes| axes.link_x), + draw_cursor_y: linked_axes.as_ref().map_or(false, |axes| axes.link_y), + draw_cursors: cursors, }; - prepared.ui(ui, &response); + let cursors = prepared.ui(ui, &response); if let Some(boxed_zoom_rect) = boxed_zoom_rect { ui.painter().with_clip_rect(rect).add(boxed_zoom_rect.0); @@ -833,6 +857,12 @@ impl Plot { } if let Some(group) = linked_axes.as_ref() { + let cursors_map = &mut *group.cursors.borrow_mut(); + if response.hovered() { + cursors_map.insert(id_source, cursors); + } else { + cursors_map.remove(&id_source); + } group.set(*transform.bounds()); } @@ -1118,10 +1148,13 @@ struct PreparedPlot { show_axes: [bool; 2], transform: ScreenTransform, grid_spacers: [GridSpacer; 2], + draw_cursor_x: bool, + draw_cursor_y: bool, + draw_cursors: HashMap>, } impl PreparedPlot { - fn ui(self, ui: &mut Ui, response: &Response) { + fn ui(self, ui: &mut Ui, response: &Response) -> Vec<(Orientation, f64)> { let mut shapes = Vec::new(); for d in 0..2 { @@ -1138,9 +1171,40 @@ impl PreparedPlot { item.shapes(&mut plot_ui, transform, &mut shapes); } - if let Some(pointer) = response.hover_pos() { - self.hover(ui, pointer, &mut shapes); - } + let cursors = if let Some(pointer) = response.hover_pos() { + self.hover(ui, pointer, &mut shapes) + } else { + // Draw cursors from other plots + let line_color = rulers_color(ui); + for &(orientation, value) in self + .draw_cursors + .values() + .flat_map(|cursors| cursors.iter()) + { + match orientation { + Orientation::Horizontal => { + if self.draw_cursor_y { + shapes.push(horizontal_line( + transform.position_from_point(&PlotPoint::new(0.0, value)), + &self.transform, + line_color, + )); + } + } + Orientation::Vertical => { + if self.draw_cursor_x { + shapes.push(vertical_line( + transform.position_from_point(&PlotPoint::new(value, 0.0)), + &self.transform, + line_color, + )); + } + } + } + } + + Vec::new() + }; let painter = ui.painter().with_clip_rect(*transform.frame()); painter.extend(shapes); @@ -1160,6 +1224,8 @@ impl PreparedPlot { painter.text(position, anchor, text, font_id, ui.visuals().text_color()); } } + + cursors } fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec) { @@ -1253,7 +1319,7 @@ impl PreparedPlot { } } - fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec) { + fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec) -> Vec<(Orientation, f64)> { let Self { transform, show_x, @@ -1264,7 +1330,7 @@ impl PreparedPlot { } = self; if !show_x && !show_y { - return; + return Vec::new(); } let interact_radius_sq: f32 = (16.0f32).powi(2); @@ -1280,11 +1346,13 @@ impl PreparedPlot { .min_by_key(|(_, elem)| elem.dist_sq.ord()) .filter(|(_, elem)| elem.dist_sq <= interact_radius_sq); + let cursors = RefCell::new(Vec::new()); let plot = items::PlotConfig { ui, transform, show_x: *show_x, show_y: *show_y, + cursors: &cursors, }; if let Some((item, elem)) = closest { @@ -1293,6 +1361,8 @@ impl PreparedPlot { let value = transform.value_from_position(pointer); items::rulers_at_value(pointer, value, "", &plot, shapes, label_formatter); } + + cursors.into_inner() } } From 375ab6c87a808c6e0e7aa6a9eb49af6bf5a43f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20K=C3=A5re=20Alsaker?= Date: Fri, 29 Jul 2022 17:14:11 +0200 Subject: [PATCH 2/9] Link cursors in demo --- crates/egui_demo_lib/src/demo/plot_demo.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/egui_demo_lib/src/demo/plot_demo.rs b/crates/egui_demo_lib/src/demo/plot_demo.rs index 8d6b36674787..2eea6103c3be 100644 --- a/crates/egui_demo_lib/src/demo/plot_demo.rs +++ b/crates/egui_demo_lib/src/demo/plot_demo.rs @@ -456,6 +456,7 @@ impl CustomAxisDemo { #[derive(PartialEq)] struct LinkedAxisDemo { + link_cursor: bool, link_x: bool, link_y: bool, group: plot::LinkedAxisGroup, @@ -466,6 +467,7 @@ impl Default for LinkedAxisDemo { let link_x = true; let link_y = false; Self { + link_cursor: true, link_x, link_y, group: plot::LinkedAxisGroup::new(link_x, link_y), @@ -508,10 +510,12 @@ impl LinkedAxisDemo { fn ui(&mut self, ui: &mut Ui) -> Response { ui.horizontal(|ui| { + ui.checkbox(&mut &mut self.link_cursor, "Link cursor"); ui.label("Linked axes:"); ui.checkbox(&mut self.link_x, "X"); ui.checkbox(&mut self.link_y, "Y"); }); + self.group.set_link_cursor(self.link_cursor); self.group.set_link_x(self.link_x); self.group.set_link_y(self.link_y); ui.horizontal(|ui| { From 29906e48b765eae131b9d71da6edb2e3abdbde20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20K=C3=A5re=20Alsaker?= Date: Fri, 29 Jul 2022 18:09:19 +0200 Subject: [PATCH 3/9] Refactor cursor memory to deal with removal of plots --- crates/egui/src/widgets/plot/items/mod.rs | 18 ++++---- crates/egui/src/widgets/plot/mod.rs | 56 +++++++++++++---------- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/crates/egui/src/widgets/plot/items/mod.rs b/crates/egui/src/widgets/plot/items/mod.rs index 9cc89a969a50..bfe111f8c37a 100644 --- a/crates/egui/src/widgets/plot/items/mod.rs +++ b/crates/egui/src/widgets/plot/items/mod.rs @@ -7,7 +7,7 @@ use epaint::Mesh; use crate::*; -use super::{LabelFormatter, PlotBounds, ScreenTransform}; +use super::{Cursor, LabelFormatter, PlotBounds, ScreenTransform}; use rect_elem::*; use values::{ClosestElem, PlotGeometry}; @@ -28,7 +28,7 @@ pub(super) struct PlotConfig<'a> { pub transform: &'a ScreenTransform, pub show_x: bool, pub show_y: bool, - pub cursors: &'a RefCell>, + pub cursors: &'a RefCell>, } /// Trait shared by things that can be drawn in the plot. @@ -1670,15 +1670,17 @@ pub(super) fn rulers_at_value( let line_color = rulers_color(plot.ui); if plot.show_x { shapes.push(vertical_line(pointer, plot.transform, line_color)); - plot.cursors - .borrow_mut() - .push((Orientation::Vertical, value.x)); + plot.cursors.borrow_mut().push(Cursor { + orientation: Orientation::Vertical, + point: value.x, + }); } if plot.show_y { shapes.push(horizontal_line(pointer, plot.transform, line_color)); - plot.cursors - .borrow_mut() - .push((Orientation::Horizontal, value.y)); + plot.cursors.borrow_mut().push(Cursor { + orientation: Orientation::Horizontal, + point: value.y, + }); } let mut prefix = String::new(); diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index 1709772e5299..e1b600c62835 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -2,7 +2,6 @@ use std::{ cell::{Cell, RefCell}, - collections::HashMap, ops::RangeInclusive, rc::Rc, }; @@ -121,6 +120,12 @@ impl PlotMemory { // ---------------------------------------------------------------------------- +#[derive(Copy, Clone, PartialEq)] +struct Cursor { + orientation: Orientation, + point: f64, +} + /// Defines how multiple plots share the same range for one or both of their axes. Can be added while building /// a plot with [`Plot::link_axis`]. Contains an internal state, meaning that this object should be stored by /// the user between frames. @@ -130,7 +135,7 @@ pub struct LinkedAxisGroup { pub(crate) link_y: bool, pub(crate) link_cursor: bool, pub(crate) bounds: Rc>>, - pub(crate) cursors: Rc>>>, + cursors: Rc)>>>, } impl LinkedAxisGroup { @@ -140,7 +145,7 @@ impl LinkedAxisGroup { link_y, link_cursor: false, bounds: Rc::new(Cell::new(None)), - cursors: Rc::new(RefCell::new(HashMap::new())), + cursors: Rc::new(RefCell::new(Vec::new())), } } @@ -676,12 +681,24 @@ impl Plot { // --- Bound computation --- let mut bounds = *last_screen_transform.bounds(); - let mut cursors = HashMap::new(); + let mut draw_cursors: Vec = Vec::new(); // Transfer the bounds from a link group. if let Some(axes) = linked_axes.as_ref() { if axes.link_cursor { - cursors = axes.cursors.borrow().clone(); + let mut cursors = axes.cursors.borrow_mut(); + + // Look for our previous entry + let index = cursors + .iter() + .enumerate() + .find(|(_, e)| e.0 == plot_id) + .map(|(i, _)| i); + + // Remove our previous entry and all older entries as these are no longer displayed. + index.map(|index| cursors.drain(0..=index)); + + draw_cursors = cursors.iter().flat_map(|e| e.1.iter().copied()).collect(); } if let Some(linked_bounds) = axes.get() { if axes.link_x { @@ -841,9 +858,9 @@ impl Plot { grid_spacers, draw_cursor_x: linked_axes.as_ref().map_or(false, |axes| axes.link_x), draw_cursor_y: linked_axes.as_ref().map_or(false, |axes| axes.link_y), - draw_cursors: cursors, + draw_cursors, }; - let cursors = prepared.ui(ui, &response); + let plot_cursors = prepared.ui(ui, &response); if let Some(boxed_zoom_rect) = boxed_zoom_rect { ui.painter().with_clip_rect(rect).add(boxed_zoom_rect.0); @@ -857,11 +874,8 @@ impl Plot { } if let Some(group) = linked_axes.as_ref() { - let cursors_map = &mut *group.cursors.borrow_mut(); - if response.hovered() { - cursors_map.insert(id_source, cursors); - } else { - cursors_map.remove(&id_source); + if group.link_cursor { + group.cursors.borrow_mut().push((plot_id, plot_cursors)); } group.set(*transform.bounds()); } @@ -1150,11 +1164,11 @@ struct PreparedPlot { grid_spacers: [GridSpacer; 2], draw_cursor_x: bool, draw_cursor_y: bool, - draw_cursors: HashMap>, + draw_cursors: Vec, } impl PreparedPlot { - fn ui(self, ui: &mut Ui, response: &Response) -> Vec<(Orientation, f64)> { + fn ui(self, ui: &mut Ui, response: &Response) -> Vec { let mut shapes = Vec::new(); for d in 0..2 { @@ -1176,16 +1190,12 @@ impl PreparedPlot { } else { // Draw cursors from other plots let line_color = rulers_color(ui); - for &(orientation, value) in self - .draw_cursors - .values() - .flat_map(|cursors| cursors.iter()) - { - match orientation { + for cursor in &self.draw_cursors { + match cursor.orientation { Orientation::Horizontal => { if self.draw_cursor_y { shapes.push(horizontal_line( - transform.position_from_point(&PlotPoint::new(0.0, value)), + transform.position_from_point(&PlotPoint::new(0.0, cursor.point)), &self.transform, line_color, )); @@ -1194,7 +1204,7 @@ impl PreparedPlot { Orientation::Vertical => { if self.draw_cursor_x { shapes.push(vertical_line( - transform.position_from_point(&PlotPoint::new(value, 0.0)), + transform.position_from_point(&PlotPoint::new(cursor.point, 0.0)), &self.transform, line_color, )); @@ -1319,7 +1329,7 @@ impl PreparedPlot { } } - fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec) -> Vec<(Orientation, f64)> { + fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec) -> Vec { let Self { transform, show_x, From 64b4bc7f227f6707d6b73f20b8834b902d9e5fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20K=C3=A5re=20Alsaker?= Date: Fri, 29 Jul 2022 19:13:50 +0200 Subject: [PATCH 4/9] Refactor PlotItem::on_hover to produce a list of cursors --- crates/egui/src/widgets/plot/items/bar.rs | 5 +- .../egui/src/widgets/plot/items/box_elem.rs | 5 +- crates/egui/src/widgets/plot/items/mod.rs | 63 +++++++++---------- crates/egui/src/widgets/plot/mod.rs | 54 ++++++++++++---- 4 files changed, 79 insertions(+), 48 deletions(-) diff --git a/crates/egui/src/widgets/plot/items/bar.rs b/crates/egui/src/widgets/plot/items/bar.rs index 6d27d1285e43..74602199d804 100644 --- a/crates/egui/src/widgets/plot/items/bar.rs +++ b/crates/egui/src/widgets/plot/items/bar.rs @@ -2,7 +2,7 @@ use crate::emath::NumExt; use crate::epaint::{Color32, RectShape, Rounding, Shape, Stroke}; use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement}; -use crate::plot::{BarChart, PlotPoint, ScreenTransform}; +use crate::plot::{BarChart, Cursor, PlotPoint, ScreenTransform}; /// One bar in a [`BarChart`]. Potentially floating, allowing stacked bar charts. /// Width can be changed to allow variable-width histograms. @@ -142,13 +142,14 @@ impl Bar { parent: &BarChart, plot: &PlotConfig<'_>, shapes: &mut Vec, + cursors: &mut Vec, ) { let text: Option = parent .element_formatter .as_ref() .map(|fmt| fmt(self, parent)); - add_rulers_and_text(self, plot, text, shapes); + add_rulers_and_text(self, plot, text, shapes, cursors); } } diff --git a/crates/egui/src/widgets/plot/items/box_elem.rs b/crates/egui/src/widgets/plot/items/box_elem.rs index 985df8eeb29c..a865a9db2456 100644 --- a/crates/egui/src/widgets/plot/items/box_elem.rs +++ b/crates/egui/src/widgets/plot/items/box_elem.rs @@ -2,7 +2,7 @@ use crate::emath::NumExt; use crate::epaint::{Color32, RectShape, Rounding, Shape, Stroke}; use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement}; -use crate::plot::{BoxPlot, PlotPoint, ScreenTransform}; +use crate::plot::{BoxPlot, Cursor, PlotPoint, ScreenTransform}; /// Contains the values of a single box in a box plot. #[derive(Clone, Debug, PartialEq)] @@ -221,13 +221,14 @@ impl BoxElem { parent: &BoxPlot, plot: &PlotConfig<'_>, shapes: &mut Vec, + cursors: &mut Vec, ) { let text: Option = parent .element_formatter .as_ref() .map(|fmt| fmt(self, parent)); - add_rulers_and_text(self, plot, text, shapes); + add_rulers_and_text(self, plot, text, shapes, cursors); } } diff --git a/crates/egui/src/widgets/plot/items/mod.rs b/crates/egui/src/widgets/plot/items/mod.rs index bfe111f8c37a..68d8e0ed3c54 100644 --- a/crates/egui/src/widgets/plot/items/mod.rs +++ b/crates/egui/src/widgets/plot/items/mod.rs @@ -1,6 +1,6 @@ //! Contains items that can be added to a plot. -use std::{cell::RefCell, ops::RangeInclusive}; +use std::ops::RangeInclusive; use epaint::util::FloatOrd; use epaint::Mesh; @@ -28,7 +28,6 @@ pub(super) struct PlotConfig<'a> { pub transform: &'a ScreenTransform, pub show_x: bool, pub show_y: bool, - pub cursors: &'a RefCell>, } /// Trait shared by things that can be drawn in the plot. @@ -73,6 +72,7 @@ pub(super) trait PlotItem { &self, elem: ClosestElem, shapes: &mut Vec, + cursors: &mut Vec, plot: &PlotConfig<'_>, label_formatter: &LabelFormatter, ) { @@ -97,7 +97,15 @@ pub(super) trait PlotItem { let pointer = plot.transform.position_from_point(&value); shapes.push(Shape::circle_filled(pointer, 3.0, line_color)); - rulers_at_value(pointer, value, self.name(), plot, shapes, label_formatter); + rulers_at_value( + pointer, + value, + self.name(), + plot, + shapes, + cursors, + label_formatter, + ); } } @@ -1393,13 +1401,14 @@ impl PlotItem for BarChart { &self, elem: ClosestElem, shapes: &mut Vec, + cursors: &mut Vec, plot: &PlotConfig<'_>, _: &LabelFormatter, ) { let bar = &self.bars[elem.index]; bar.add_shapes(plot.transform, true, shapes); - bar.add_rulers_and_text(self, plot, shapes); + bar.add_rulers_and_text(self, plot, shapes, cursors); } } @@ -1535,13 +1544,14 @@ impl PlotItem for BoxPlot { &self, elem: ClosestElem, shapes: &mut Vec, + cursors: &mut Vec, plot: &PlotConfig<'_>, _: &LabelFormatter, ) { let box_plot = &self.boxes[elem.index]; box_plot.add_shapes(plot.transform, true, shapes); - box_plot.add_rulers_and_text(self, plot, shapes); + box_plot.add_rulers_and_text(self, plot, shapes, cursors); } } @@ -1591,6 +1601,7 @@ fn add_rulers_and_text( plot: &PlotConfig<'_>, text: Option, shapes: &mut Vec, + cursors: &mut Vec, ) { let orientation = elem.orientation(); let show_argument = plot.show_x && orientation == Orientation::Vertical @@ -1598,37 +1609,33 @@ fn add_rulers_and_text( let show_values = plot.show_y && orientation == Orientation::Vertical || plot.show_x && orientation == Orientation::Horizontal; - let line_color = rulers_color(plot.ui); - // Rulers for argument (usually vertical) if show_argument { - let push_argument_ruler = |argument: PlotPoint, shapes: &mut Vec| { - let position = plot.transform.position_from_point(&argument); - let line = match orientation { - Orientation::Horizontal => horizontal_line(position, plot.transform, line_color), - Orientation::Vertical => vertical_line(position, plot.transform, line_color), + let push_argument_ruler = |argument: PlotPoint, cursors: &mut Vec| { + let cursor = match orientation { + Orientation::Horizontal => Cursor::horizontal(argument.y), + Orientation::Vertical => Cursor::vertical(argument.x), }; - shapes.push(line); + cursors.push(cursor); }; for pos in elem.arguments_with_ruler() { - push_argument_ruler(pos, shapes); + push_argument_ruler(pos, cursors); } } // Rulers for values (usually horizontal) if show_values { - let push_value_ruler = |value: PlotPoint, shapes: &mut Vec| { - let position = plot.transform.position_from_point(&value); - let line = match orientation { - Orientation::Horizontal => vertical_line(position, plot.transform, line_color), - Orientation::Vertical => horizontal_line(position, plot.transform, line_color), + let push_value_ruler = |value: PlotPoint, cursors: &mut Vec| { + let cursor = match orientation { + Orientation::Horizontal => Cursor::vertical(value.x), + Orientation::Vertical => Cursor::horizontal(value.y), }; - shapes.push(line); + cursors.push(cursor); }; for pos in elem.values_with_ruler() { - push_value_ruler(pos, shapes); + push_value_ruler(pos, cursors); } } @@ -1665,22 +1672,14 @@ pub(super) fn rulers_at_value( name: &str, plot: &PlotConfig<'_>, shapes: &mut Vec, + cursors: &mut Vec, label_formatter: &LabelFormatter, ) { - let line_color = rulers_color(plot.ui); if plot.show_x { - shapes.push(vertical_line(pointer, plot.transform, line_color)); - plot.cursors.borrow_mut().push(Cursor { - orientation: Orientation::Vertical, - point: value.x, - }); + cursors.push(Cursor::vertical(value.x)); } if plot.show_y { - shapes.push(horizontal_line(pointer, plot.transform, line_color)); - plot.cursors.borrow_mut().push(Cursor { - orientation: Orientation::Horizontal, - point: value.y, - }); + cursors.push(Cursor::horizontal(value.y)); } let mut prefix = String::new(); diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index e1b600c62835..13be4b1c0041 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -126,6 +126,22 @@ struct Cursor { point: f64, } +impl Cursor { + fn vertical(point: f64) -> Self { + Self { + orientation: Orientation::Vertical, + point, + } + } + + fn horizontal(point: f64) -> Self { + Self { + orientation: Orientation::Horizontal, + point, + } + } +} + /// Defines how multiple plots share the same range for one or both of their axes. Can be added while building /// a plot with [`Plot::link_axis`]. Contains an internal state, meaning that this object should be stored by /// the user between frames. @@ -1188,12 +1204,17 @@ impl PreparedPlot { let cursors = if let Some(pointer) = response.hover_pos() { self.hover(ui, pointer, &mut shapes) } else { - // Draw cursors from other plots - let line_color = rulers_color(ui); - for cursor in &self.draw_cursors { + Vec::new() + }; + + // Draw cursors + let line_color = rulers_color(ui); + + let mut draw_cursor = |cursors: &Vec, always| { + for cursor in cursors { match cursor.orientation { Orientation::Horizontal => { - if self.draw_cursor_y { + if self.draw_cursor_y || always { shapes.push(horizontal_line( transform.position_from_point(&PlotPoint::new(0.0, cursor.point)), &self.transform, @@ -1202,7 +1223,7 @@ impl PreparedPlot { } } Orientation::Vertical => { - if self.draw_cursor_x { + if self.draw_cursor_x || always { shapes.push(vertical_line( transform.position_from_point(&PlotPoint::new(cursor.point, 0.0)), &self.transform, @@ -1212,10 +1233,11 @@ impl PreparedPlot { } } } - - Vec::new() }; + draw_cursor(&self.draw_cursors, false); + draw_cursor(&cursors, true); + let painter = ui.painter().with_clip_rect(*transform.frame()); painter.extend(shapes); @@ -1356,23 +1378,31 @@ impl PreparedPlot { .min_by_key(|(_, elem)| elem.dist_sq.ord()) .filter(|(_, elem)| elem.dist_sq <= interact_radius_sq); - let cursors = RefCell::new(Vec::new()); + let mut cursors = Vec::new(); + let plot = items::PlotConfig { ui, transform, show_x: *show_x, show_y: *show_y, - cursors: &cursors, }; if let Some((item, elem)) = closest { - item.on_hover(elem, shapes, &plot, label_formatter); + item.on_hover(elem, shapes, &mut cursors, &plot, label_formatter); } else { let value = transform.value_from_position(pointer); - items::rulers_at_value(pointer, value, "", &plot, shapes, label_formatter); + items::rulers_at_value( + pointer, + value, + "", + &plot, + shapes, + &mut cursors, + label_formatter, + ); } - cursors.into_inner() + cursors } } From d00918bc6b465e05a596b78f3d1d46c3598a6496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20K=C3=A5re=20Alsaker?= Date: Sat, 6 Aug 2022 23:16:13 +0200 Subject: [PATCH 5/9] Use a separate `LinkedCursorsGroup` type to link cursors. --- crates/egui/src/widgets/plot/mod.rs | 129 ++++++++++++++++----- crates/egui_demo_lib/src/demo/plot_demo.rs | 22 +++- 2 files changed, 117 insertions(+), 34 deletions(-) diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index 13be4b1c0041..6f203f917d03 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -142,6 +142,63 @@ impl Cursor { } } +/// Contains the cursors drawn for a specific frame of a plot. +#[derive(PartialEq)] +struct PlotFrameCursors { + id: Id, + cursors: Vec, +} + +/// Defines how multiple plots share the same cursor for one or both of their axes. Can be added while building +/// a plot with [`Plot::link_cursor`]. Contains an internal state, meaning that this object should be stored by +/// the user between frames. +#[derive(Clone, PartialEq)] +pub struct LinkedCursorsGroup { + link_x: bool, + link_y: bool, + // We store the cursors drawn for each linked plot. + frames: Rc>>, +} + +impl LinkedCursorsGroup { + pub fn new(link_x: bool, link_y: bool) -> Self { + Self { + link_x, + link_y, + frames: Rc::new(RefCell::new(Vec::new())), + } + } + + /// Only link the cursor for the x-axis. + pub fn x() -> Self { + Self::new(true, false) + } + + /// Only link the cursor for the y-axis. + pub fn y() -> Self { + Self::new(false, true) + } + + /// Link the cursors for both axes. + pub fn both() -> Self { + Self::new(true, true) + } + + /// Change whether the cursor for the x-axis is linked for this group. Using this after plots in this group have been + /// drawn in this frame already may lead to unexpected results. + pub fn set_link_x(&mut self, link: bool) { + self.link_x = link; + } + + /// Change whether the cursor for the y-axis is linked for this group. Using this after plots in this group have been + /// drawn in this frame already may lead to unexpected results. + pub fn set_link_y(&mut self, link: bool) { + self.link_y = link; + } +} + +// ---------------------------------------------------------------------------- + /// Defines how multiple plots share the same range for one or both of their axes. Can be added while building /// a plot with [`Plot::link_axis`]. Contains an internal state, meaning that this object should be stored by /// the user between frames. @@ -149,9 +206,7 @@ impl Cursor { pub struct LinkedAxisGroup { pub(crate) link_x: bool, pub(crate) link_y: bool, - pub(crate) link_cursor: bool, pub(crate) bounds: Rc>>, - cursors: Rc)>>>, } impl LinkedAxisGroup { @@ -159,9 +214,7 @@ impl LinkedAxisGroup { Self { link_x, link_y, - link_cursor: false, bounds: Rc::new(Cell::new(None)), - cursors: Rc::new(RefCell::new(Vec::new())), } } @@ -192,11 +245,6 @@ impl LinkedAxisGroup { self.link_y = link; } - /// Change whether the cursor is linked for this group. By default it is unlinked. - pub fn set_link_cursor(&mut self, link: bool) { - self.link_cursor = link; - } - fn get(&self) -> Option { self.bounds.get() } @@ -236,6 +284,7 @@ pub struct Plot { allow_boxed_zoom: bool, boxed_zoom_pointer_button: PointerButton, linked_axes: Option, + linked_cursors: Option, min_size: Vec2, width: Option, @@ -270,6 +319,7 @@ impl Plot { allow_boxed_zoom: true, boxed_zoom_pointer_button: PointerButton::Secondary, linked_axes: None, + linked_cursors: None, min_size: Vec2::splat(64.0), width: None, @@ -546,6 +596,13 @@ impl Plot { self } + /// Add a [`LinkedCursorGroup`] so that this plot will share the bounds with other plots that have this + /// group assigned. A plot cannot belong to more than one group. + pub fn link_cursor(mut self, group: LinkedCursorsGroup) -> Self { + self.linked_cursors = Some(group); + 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)) @@ -581,6 +638,7 @@ impl Plot { show_background, show_axes, linked_axes, + linked_cursors, grid_spacers, } = self; @@ -697,25 +755,31 @@ impl Plot { // --- Bound computation --- let mut bounds = *last_screen_transform.bounds(); - let mut draw_cursors: Vec = Vec::new(); + // Find the cursors from other plots we need to draw + let draw_cursors: Vec = if let Some(group) = linked_cursors.as_ref() { + let mut frames = group.frames.borrow_mut(); + + // Look for our previous frame + let index = frames + .iter() + .enumerate() + .find(|(_, frame)| frame.id == plot_id) + .map(|(i, _)| i); + + // Remove our previous frame and all older frames as these are no longer displayed. + index.map(|index| frames.drain(0..=index)); + + // Gather all cursors of the remaining frames + frames + .iter() + .flat_map(|frame| frame.cursors.iter().copied()) + .collect() + } else { + Vec::new() + }; // Transfer the bounds from a link group. if let Some(axes) = linked_axes.as_ref() { - if axes.link_cursor { - let mut cursors = axes.cursors.borrow_mut(); - - // Look for our previous entry - let index = cursors - .iter() - .enumerate() - .find(|(_, e)| e.0 == plot_id) - .map(|(i, _)| i); - - // Remove our previous entry and all older entries as these are no longer displayed. - index.map(|index| cursors.drain(0..=index)); - - draw_cursors = cursors.iter().flat_map(|e| e.1.iter().copied()).collect(); - } if let Some(linked_bounds) = axes.get() { if axes.link_x { bounds.set_x(&linked_bounds); @@ -872,8 +936,8 @@ impl Plot { show_axes, transform: transform.clone(), grid_spacers, - draw_cursor_x: linked_axes.as_ref().map_or(false, |axes| axes.link_x), - draw_cursor_y: linked_axes.as_ref().map_or(false, |axes| axes.link_y), + draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.link_x), + draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.link_y), draw_cursors, }; let plot_cursors = prepared.ui(ui, &response); @@ -889,10 +953,15 @@ impl Plot { hovered_entry = legend.hovered_entry_name(); } + if let Some(group) = linked_cursors.as_ref() { + // Push the frame we just drew to the list of frames + group.frames.borrow_mut().push(PlotFrameCursors { + id: plot_id, + cursors: plot_cursors, + }); + } + if let Some(group) = linked_axes.as_ref() { - if group.link_cursor { - group.cursors.borrow_mut().push((plot_id, plot_cursors)); - } group.set(*transform.bounds()); } diff --git a/crates/egui_demo_lib/src/demo/plot_demo.rs b/crates/egui_demo_lib/src/demo/plot_demo.rs index 2eea6103c3be..99c2bd40260c 100644 --- a/crates/egui_demo_lib/src/demo/plot_demo.rs +++ b/crates/egui_demo_lib/src/demo/plot_demo.rs @@ -456,21 +456,27 @@ impl CustomAxisDemo { #[derive(PartialEq)] struct LinkedAxisDemo { - link_cursor: bool, link_x: bool, link_y: bool, group: plot::LinkedAxisGroup, + cursor_group: plot::LinkedCursorsGroup, + link_cursor_x: bool, + link_cursor_y: bool, } impl Default for LinkedAxisDemo { fn default() -> Self { let link_x = true; let link_y = false; + let link_cursor_x = true; + let link_cursor_y = false; Self { - link_cursor: true, link_x, link_y, group: plot::LinkedAxisGroup::new(link_x, link_y), + cursor_group: plot::LinkedCursorsGroup::new(link_cursor_x, link_cursor_y), + link_cursor_x, + link_cursor_y, } } } @@ -510,26 +516,33 @@ impl LinkedAxisDemo { fn ui(&mut self, ui: &mut Ui) -> Response { ui.horizontal(|ui| { - ui.checkbox(&mut &mut self.link_cursor, "Link cursor"); ui.label("Linked axes:"); ui.checkbox(&mut self.link_x, "X"); ui.checkbox(&mut self.link_y, "Y"); }); - self.group.set_link_cursor(self.link_cursor); self.group.set_link_x(self.link_x); self.group.set_link_y(self.link_y); + ui.horizontal(|ui| { + ui.label("Linked cursors:"); + ui.checkbox(&mut self.link_cursor_x, "X"); + ui.checkbox(&mut self.link_cursor_y, "Y"); + }); + self.cursor_group.set_link_x(self.link_cursor_x); + self.cursor_group.set_link_y(self.link_cursor_y); ui.horizontal(|ui| { Plot::new("linked_axis_1") .data_aspect(1.0) .width(250.0) .height(250.0) .link_axis(self.group.clone()) + .link_cursor(self.cursor_group.clone()) .show(ui, LinkedAxisDemo::configure_plot); Plot::new("linked_axis_2") .data_aspect(2.0) .width(150.0) .height(250.0) .link_axis(self.group.clone()) + .link_cursor(self.cursor_group.clone()) .show(ui, LinkedAxisDemo::configure_plot); }); Plot::new("linked_axis_3") @@ -537,6 +550,7 @@ impl LinkedAxisDemo { .width(250.0) .height(150.0) .link_axis(self.group.clone()) + .link_cursor(self.cursor_group.clone()) .show(ui, LinkedAxisDemo::configure_plot) .response } From e019d9cf56d30c675456426b032c308348708e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20K=C3=A5re=20Alsaker?= Date: Sat, 6 Aug 2022 23:38:23 +0200 Subject: [PATCH 6/9] Refactor `Cursor`. --- crates/egui/src/widgets/plot/items/mod.rs | 12 ++++---- crates/egui/src/widgets/plot/mod.rs | 35 +++++++---------------- 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/crates/egui/src/widgets/plot/items/mod.rs b/crates/egui/src/widgets/plot/items/mod.rs index 68d8e0ed3c54..6b4233a99a8c 100644 --- a/crates/egui/src/widgets/plot/items/mod.rs +++ b/crates/egui/src/widgets/plot/items/mod.rs @@ -1613,8 +1613,8 @@ fn add_rulers_and_text( if show_argument { let push_argument_ruler = |argument: PlotPoint, cursors: &mut Vec| { let cursor = match orientation { - Orientation::Horizontal => Cursor::horizontal(argument.y), - Orientation::Vertical => Cursor::vertical(argument.x), + Orientation::Horizontal => Cursor::Horizontal { y: argument.y }, + Orientation::Vertical => Cursor::Vertical { x: argument.x }, }; cursors.push(cursor); }; @@ -1628,8 +1628,8 @@ fn add_rulers_and_text( if show_values { let push_value_ruler = |value: PlotPoint, cursors: &mut Vec| { let cursor = match orientation { - Orientation::Horizontal => Cursor::vertical(value.x), - Orientation::Vertical => Cursor::horizontal(value.y), + Orientation::Horizontal => Cursor::Vertical { x: value.x }, + Orientation::Vertical => Cursor::Horizontal { y: value.y }, }; cursors.push(cursor); }; @@ -1676,10 +1676,10 @@ pub(super) fn rulers_at_value( label_formatter: &LabelFormatter, ) { if plot.show_x { - cursors.push(Cursor::vertical(value.x)); + cursors.push(Cursor::Vertical { x: value.x }); } if plot.show_y { - cursors.push(Cursor::horizontal(value.y)); + cursors.push(Cursor::Horizontal { y: value.y }); } let mut prefix = String::new(); diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index 6f203f917d03..0d5a810b633e 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -120,26 +120,11 @@ impl PlotMemory { // ---------------------------------------------------------------------------- +/// Indicates a vertical or horizontal cursor line in plot coordinates. #[derive(Copy, Clone, PartialEq)] -struct Cursor { - orientation: Orientation, - point: f64, -} - -impl Cursor { - fn vertical(point: f64) -> Self { - Self { - orientation: Orientation::Vertical, - point, - } - } - - fn horizontal(point: f64) -> Self { - Self { - orientation: Orientation::Horizontal, - point, - } - } +enum Cursor { + Horizontal { y: f64 }, + Vertical { x: f64 }, } /// Contains the cursors drawn for a specific frame of a plot. @@ -1280,21 +1265,21 @@ impl PreparedPlot { let line_color = rulers_color(ui); let mut draw_cursor = |cursors: &Vec, always| { - for cursor in cursors { - match cursor.orientation { - Orientation::Horizontal => { + for &cursor in cursors { + match cursor { + Cursor::Horizontal { y } => { if self.draw_cursor_y || always { shapes.push(horizontal_line( - transform.position_from_point(&PlotPoint::new(0.0, cursor.point)), + transform.position_from_point(&PlotPoint::new(0.0, y)), &self.transform, line_color, )); } } - Orientation::Vertical => { + Cursor::Vertical { x } => { if self.draw_cursor_x || always { shapes.push(vertical_line( - transform.position_from_point(&PlotPoint::new(cursor.point, 0.0)), + transform.position_from_point(&PlotPoint::new(x, 0.0)), &self.transform, line_color, )); From e82f4943c5aa6061dd9bb5be8ff0d7c2ec5031e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20K=C3=A5re=20Alsaker?= Date: Sat, 6 Aug 2022 23:44:30 +0200 Subject: [PATCH 7/9] Inline `push_argument_ruler` and `push_value_ruler`. --- crates/egui/src/widgets/plot/items/mod.rs | 26 +++++++---------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/crates/egui/src/widgets/plot/items/mod.rs b/crates/egui/src/widgets/plot/items/mod.rs index 6b4233a99a8c..f8532aa2a9c6 100644 --- a/crates/egui/src/widgets/plot/items/mod.rs +++ b/crates/egui/src/widgets/plot/items/mod.rs @@ -1611,31 +1611,21 @@ fn add_rulers_and_text( // Rulers for argument (usually vertical) if show_argument { - let push_argument_ruler = |argument: PlotPoint, cursors: &mut Vec| { - let cursor = match orientation { - Orientation::Horizontal => Cursor::Horizontal { y: argument.y }, - Orientation::Vertical => Cursor::Vertical { x: argument.x }, - }; - cursors.push(cursor); - }; - for pos in elem.arguments_with_ruler() { - push_argument_ruler(pos, cursors); + cursors.push(match orientation { + Orientation::Horizontal => Cursor::Horizontal { y: pos.y }, + Orientation::Vertical => Cursor::Vertical { x: pos.x }, + }); } } // Rulers for values (usually horizontal) if show_values { - let push_value_ruler = |value: PlotPoint, cursors: &mut Vec| { - let cursor = match orientation { - Orientation::Horizontal => Cursor::Vertical { x: value.x }, - Orientation::Vertical => Cursor::Horizontal { y: value.y }, - }; - cursors.push(cursor); - }; - for pos in elem.values_with_ruler() { - push_value_ruler(pos, cursors); + cursors.push(match orientation { + Orientation::Horizontal => Cursor::Vertical { x: pos.x }, + Orientation::Vertical => Cursor::Horizontal { y: pos.y }, + }); } } From 5c3b22e70741c41e138c4bfffc22bdf28502a367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20K=C3=A5re=20Alsaker?= Date: Thu, 25 Aug 2022 11:58:18 +0200 Subject: [PATCH 8/9] Update documentation --- crates/egui/src/widgets/plot/mod.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index 0d5a810b633e..1c819927c54d 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -127,7 +127,7 @@ enum Cursor { Vertical { x: f64 }, } -/// Contains the cursors drawn for a specific frame of a plot. +/// Contains the cursors drawn for a plot widget in a single frame. #[derive(PartialEq)] struct PlotFrameCursors { id: Id, @@ -141,7 +141,11 @@ struct PlotFrameCursors { pub struct LinkedCursorsGroup { link_x: bool, link_y: bool, - // We store the cursors drawn for each linked plot. + // We store the cursors drawn for each linked plot. Each time a plot in the group is drawn, the + // cursors due to hovering it drew are appended to `frames`, so lower indices are older. + // When a plot is redrawn all entries older than its previous entry are removed. This avoids + // unbounded growth and also ensures entries for plots which are not longer part of the group + // gets removed. frames: Rc>>, } @@ -751,10 +755,12 @@ impl Plot { .find(|(_, frame)| frame.id == plot_id) .map(|(i, _)| i); - // Remove our previous frame and all older frames as these are no longer displayed. + // Remove our previous frame and all older frames as these are no longer displayed. This avoids + // unbounded growth, as we add an entry each time we draw a plot. index.map(|index| frames.drain(0..=index)); - // Gather all cursors of the remaining frames + // Gather all cursors of the remaining frames. This will be all the cursors of the + // other plots in the group. We want to draw these in the current plot too. frames .iter() .flat_map(|frame| frame.cursors.iter().copied()) From 1bd048c704bee3df28485fa7bd8bf0d2760e7126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20K=C3=A5re=20Alsaker?= Date: Thu, 25 Aug 2022 12:15:45 +0200 Subject: [PATCH 9/9] Update doc reference --- crates/egui/src/widgets/plot/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index 1c819927c54d..fd5a121fc95d 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -585,7 +585,7 @@ impl Plot { self } - /// Add a [`LinkedCursorGroup`] so that this plot will share the bounds with other plots that have this + /// Add a [`LinkedCursorsGroup`] so that this plot will share the bounds with other plots that have this /// group assigned. A plot cannot belong to more than one group. pub fn link_cursor(mut self, group: LinkedCursorsGroup) -> Self { self.linked_cursors = Some(group);