diff --git a/crates/egui/src/widgets/plot/items/bar.rs b/crates/egui/src/widgets/plot/items/bar.rs index 6d27d1285..74602199d 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 985df8eeb..a865a9db2 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 def04a458..f8532aa2a 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}; @@ -72,6 +72,7 @@ pub(super) trait PlotItem { &self, elem: ClosestElem, shapes: &mut Vec, + cursors: &mut Vec, plot: &PlotConfig<'_>, label_formatter: &LabelFormatter, ) { @@ -96,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, + ); } } @@ -1392,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); } } @@ -1534,20 +1544,21 @@ 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); } } // ---------------------------------------------------------------------------- // 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 +1566,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 +1581,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( [ @@ -1582,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 @@ -1589,37 +1609,23 @@ 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), - }; - shapes.push(line); - }; - for pos in elem.arguments_with_ruler() { - push_argument_ruler(pos, shapes); + 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, 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), - }; - shapes.push(line); - }; - for pos in elem.values_with_ruler() { - push_value_ruler(pos, shapes); + cursors.push(match orientation { + Orientation::Horizontal => Cursor::Vertical { x: pos.x }, + Orientation::Vertical => Cursor::Horizontal { y: pos.y }, + }); } } @@ -1656,14 +1662,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)); + cursors.push(Cursor::Vertical { x: value.x }); } if plot.show_y { - shapes.push(horizontal_line(pointer, plot.transform, line_color)); + 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 d9c08afbb..fd5a121fc 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -1,6 +1,10 @@ //! Simple plotting library. -use std::{cell::Cell, ops::RangeInclusive, rc::Rc}; +use std::{ + cell::{Cell, RefCell}, + ops::RangeInclusive, + rc::Rc, +}; use crate::*; use epaint::color::Hsva; @@ -17,6 +21,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; @@ -114,6 +120,74 @@ impl PlotMemory { // ---------------------------------------------------------------------------- +/// Indicates a vertical or horizontal cursor line in plot coordinates. +#[derive(Copy, Clone, PartialEq)] +enum Cursor { + Horizontal { y: f64 }, + Vertical { x: f64 }, +} + +/// Contains the cursors drawn for a plot widget in a single frame. +#[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. 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>>, +} + +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. @@ -199,6 +273,7 @@ pub struct Plot { allow_boxed_zoom: bool, boxed_zoom_pointer_button: PointerButton, linked_axes: Option, + linked_cursors: Option, min_size: Vec2, width: Option, @@ -233,6 +308,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, @@ -509,6 +585,13 @@ impl Plot { self } + /// 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); + 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)) @@ -544,6 +627,7 @@ impl Plot { show_background, show_axes, linked_axes, + linked_cursors, grid_spacers, } = self; @@ -660,6 +744,31 @@ impl Plot { // --- Bound computation --- let mut bounds = *last_screen_transform.bounds(); + // 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. 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. 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()) + .collect() + } else { + Vec::new() + }; + // Transfer the bounds from a link group. if let Some(axes) = linked_axes.as_ref() { if let Some(linked_bounds) = axes.get() { @@ -818,8 +927,11 @@ impl Plot { show_axes, transform: transform.clone(), grid_spacers, + 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, }; - 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); @@ -832,6 +944,14 @@ 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() { group.set(*transform.bounds()); } @@ -1118,10 +1238,13 @@ struct PreparedPlot { show_axes: [bool; 2], transform: ScreenTransform, grid_spacers: [GridSpacer; 2], + draw_cursor_x: bool, + draw_cursor_y: bool, + draw_cursors: Vec, } impl PreparedPlot { - fn ui(self, ui: &mut Ui, response: &Response) { + fn ui(self, ui: &mut Ui, response: &Response) -> Vec { let mut shapes = Vec::new(); for d in 0..2 { @@ -1138,9 +1261,42 @@ 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 { + Vec::new() + }; + + // Draw cursors + let line_color = rulers_color(ui); + + let mut draw_cursor = |cursors: &Vec, always| { + 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, y)), + &self.transform, + line_color, + )); + } + } + Cursor::Vertical { x } => { + if self.draw_cursor_x || always { + shapes.push(vertical_line( + transform.position_from_point(&PlotPoint::new(x, 0.0)), + &self.transform, + line_color, + )); + } + } + } + } + }; + + draw_cursor(&self.draw_cursors, false); + draw_cursor(&cursors, true); let painter = ui.painter().with_clip_rect(*transform.frame()); painter.extend(shapes); @@ -1160,6 +1316,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 +1411,7 @@ impl PreparedPlot { } } - fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec) { + fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec) -> Vec { let Self { transform, show_x, @@ -1264,7 +1422,7 @@ impl PreparedPlot { } = self; if !show_x && !show_y { - return; + return Vec::new(); } let interact_radius_sq: f32 = (16.0f32).powi(2); @@ -1280,6 +1438,8 @@ impl PreparedPlot { .min_by_key(|(_, elem)| elem.dist_sq.ord()) .filter(|(_, elem)| elem.dist_sq <= interact_radius_sq); + let mut cursors = Vec::new(); + let plot = items::PlotConfig { ui, transform, @@ -1288,11 +1448,21 @@ impl PreparedPlot { }; 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 } } diff --git a/crates/egui_demo_lib/src/demo/plot_demo.rs b/crates/egui_demo_lib/src/demo/plot_demo.rs index 8d6b36674..99c2bd402 100644 --- a/crates/egui_demo_lib/src/demo/plot_demo.rs +++ b/crates/egui_demo_lib/src/demo/plot_demo.rs @@ -459,16 +459,24 @@ struct LinkedAxisDemo { 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_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, } } } @@ -514,18 +522,27 @@ impl LinkedAxisDemo { }); 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") @@ -533,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 }