diff --git a/CHANGELOG.md b/CHANGELOG.md index 41a29c52..41b0bf9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG ## Unreleased * Add `char_limit` to `TextEdit` singleline mode to limit the amount of characters +* ⚠️ BREAKING: `Plot::link_axis` and `Plot::link_cursor` now take the name of the group ([#2410](https://github.com/emilk/egui/pull/2410)). + ## 0.21.0 - 2023-02-08 - Deadlock fix and style customizability * ⚠️ BREAKING: `egui::Context` now use closures for locking ([#2625](https://github.com/emilk/egui/pull/2625)): diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index 63873f86..8797d61d 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -1,10 +1,7 @@ //! Simple plotting library. -use std::{ - cell::{Cell, RefCell}, - ops::RangeInclusive, - rc::Rc, -}; +use ahash::HashMap; +use std::ops::RangeInclusive; use crate::*; use epaint::util::FloatOrd; @@ -35,9 +32,11 @@ type AxisFormatter = Option>; type GridSpacerFn = dyn Fn(GridInput) -> Vec; type GridSpacer = Box; +type CoordinatesFormatterFn = dyn Fn(&PlotPoint, &PlotBounds) -> String; + /// Specifies the coordinates formatting when passed to [`Plot::coordinates_formatter`]. pub struct CoordinatesFormatter { - function: Box String>, + function: Box, } impl CoordinatesFormatter { @@ -126,120 +125,23 @@ enum Cursor { } /// Contains the cursors drawn for a plot widget in a single frame. -#[derive(PartialEq)] +#[derive(PartialEq, Clone)] 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>>, +#[derive(Default, Clone)] +struct CursorLinkGroups(HashMap>); + +#[derive(Clone)] +struct LinkedBounds { + bounds: PlotBounds, + bounds_modified: AxisBools, } -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. -#[derive(Clone, PartialEq)] -pub struct LinkedAxisGroup { - pub(crate) link_x: bool, - pub(crate) link_y: bool, - pub(crate) bounds: Rc>>, -} - -impl LinkedAxisGroup { - pub fn new(link_x: bool, link_y: bool) -> Self { - Self { - link_x, - link_y, - bounds: Rc::new(Cell::new(None)), - } - } - - /// Only link the x-axis. - pub fn x() -> Self { - Self::new(true, false) - } - - /// Only link the y-axis. - pub fn y() -> Self { - Self::new(false, true) - } - - /// Link both axes. Note that this still respects the aspect ratio of the individual plots. - pub fn both() -> Self { - Self::new(true, true) - } - - /// Change whether 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 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; - } - - fn get(&self) -> Option { - self.bounds.get() - } - - fn set(&self, bounds: PlotBounds) { - self.bounds.set(Some(bounds)); - } -} +#[derive(Default, Clone)] +struct BoundsLinkGroups(HashMap); // ---------------------------------------------------------------------------- @@ -272,8 +174,8 @@ pub struct Plot { min_auto_bounds: PlotBounds, margin_fraction: Vec2, boxed_zoom_pointer_button: PointerButton, - linked_axes: Option, - linked_cursors: Option, + linked_axes: Option<(Id, AxisBools)>, + linked_cursors: Option<(Id, AxisBools)>, min_size: Vec2, width: Option, @@ -617,17 +519,29 @@ impl Plot { self } - /// Add a [`LinkedAxisGroup`] 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_axis(mut self, group: LinkedAxisGroup) -> Self { - self.linked_axes = Some(group); + /// Add this plot to an axis link group so that this plot will share the bounds with other plots in the + /// same group. A plot cannot belong to more than one axis group. + pub fn link_axis(mut self, group_id: impl Into, link_x: bool, link_y: bool) -> Self { + self.linked_axes = Some(( + group_id.into(), + AxisBools { + x: link_x, + y: link_y, + }, + )); 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); + /// Add this plot to a cursor link group so that this plot will share the cursor position with other plots + /// in the same group. A plot cannot belong to more than one cursor group. + pub fn link_cursor(mut self, group_id: impl Into, link_x: bool, link_y: bool) -> Self { + self.linked_cursors = Some(( + group_id.into(), + AxisBools { + x: link_x, + y: link_y, + }, + )); self } @@ -720,10 +634,13 @@ impl Plot { let plot_id = ui.make_persistent_id(id_source); ui.ctx().check_for_id_clash(plot_id, rect, "Plot"); let memory = if reset { - if let Some(axes) = linked_axes.as_ref() { - axes.bounds.set(None); + if let Some((name, _)) = linked_axes.as_ref() { + ui.memory_mut(|memory| { + let link_groups: &mut BoundsLinkGroups = + memory.data.get_temp_mut_or_default(Id::null()); + link_groups.0.remove(name); + }); }; - None } else { PlotMemory::load(ui.ctx(), plot_id) @@ -742,7 +659,7 @@ impl Plot { }); let PlotMemory { - bounds_modified, + mut bounds_modified, mut hovered_entry, mut hidden_items, last_screen_transform, @@ -754,8 +671,8 @@ impl Plot { items: Vec::new(), next_auto_color_idx: 0, last_screen_transform, - bounds_modified, response, + bounds_modifications: Vec::new(), ctx: ui.ctx().clone(), }; let inner = build_fn(&mut plot_ui); @@ -763,7 +680,7 @@ impl Plot { mut items, mut response, last_screen_transform, - mut bounds_modified, + bounds_modifications, .. } = plot_ui; @@ -801,52 +718,71 @@ impl Plot { 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(); + let draw_cursors: Vec = if let Some((id, _)) = linked_cursors.as_ref() { + ui.memory_mut(|memory| { + let frames: &mut CursorLinkGroups = memory.data.get_temp_mut_or_default(Id::null()); + let cursors = frames.0.entry(*id).or_default(); - // Look for our previous frame - let index = frames - .iter() - .enumerate() - .find(|(_, frame)| frame.id == plot_id) - .map(|(i, _)| i); + // Look for our previous frame + let index = cursors + .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)); + // 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| cursors.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() + // 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. + cursors + .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() { - if axes.link_x { - bounds.set_x(&linked_bounds); - // Mark the axis as modified to prevent it from being changed. - bounds_modified.x = true; - } - if axes.link_y { - bounds.set_y(&linked_bounds); - // Mark the axis as modified to prevent it from being changed. - bounds_modified.y = true; - } - } + if let Some((id, axes)) = linked_axes.as_ref() { + ui.memory_mut(|memory| { + let link_groups: &mut BoundsLinkGroups = + memory.data.get_temp_mut_or_default(Id::null()); + if let Some(linked_bounds) = link_groups.0.get(id) { + if axes.x { + bounds.set_x(&linked_bounds.bounds); + bounds_modified.x = linked_bounds.bounds_modified.x; + } + if axes.y { + bounds.set_y(&linked_bounds.bounds); + bounds_modified.y = linked_bounds.bounds_modified.y; + } + }; + }); }; - // Allow double clicking to reset to the initial bounds? - if allow_double_click_reset && response.double_clicked_by(PointerButton::Primary) { + // Allow double clicking to reset to the initial bounds. + if allow_double_click_reset && response.double_clicked() { bounds_modified = false.into(); } - // Reset bounds to initial bounds if we haven't been modified. + // Apply bounds modifications. + for modification in bounds_modifications { + match modification { + BoundsModification::Set(new_bounds) => { + bounds = new_bounds; + bounds_modified = true.into(); + } + BoundsModification::Translate(delta) => { + bounds.translate(delta); + bounds_modified = true.into(); + } + } + } + + // Reset bounds to initial bounds if they haven't been modified. if !bounds_modified.x { bounds.set_x(&min_auto_bounds); } @@ -861,7 +797,6 @@ impl Plot { if auto_x || auto_y { for item in &items { let item_bounds = item.bounds(); - if auto_x { bounds.merge_x(&item_bounds); } @@ -883,8 +818,8 @@ impl Plot { // Enforce aspect ratio if let Some(data_aspect) = data_aspect { - if let Some(linked_axes) = &linked_axes { - let change_x = linked_axes.link_y && !linked_axes.link_x; + if let Some((_, linked_axes)) = &linked_axes { + let change_x = linked_axes.y && !linked_axes.x; transform.set_aspect_by_changing_axis(data_aspect as f64, change_x); } else if auto_bounds.any() { transform.set_aspect_by_expanding(data_aspect as f64); @@ -952,7 +887,8 @@ impl Plot { } } - if let Some(hover_pos) = response.hover_pos() { + let hover_pos = response.hover_pos(); + if let Some(hover_pos) = hover_pos { if allow_zoom { let zoom_factor = if data_aspect.is_some() { Vec2::splat(ui.input(|i| i.zoom_delta())) @@ -987,8 +923,8 @@ impl Plot { axis_formatters, show_axes, transform: transform.clone(), - 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_cursor_x: linked_cursors.as_ref().map_or(false, |(_, group)| group.x), + draw_cursor_y: linked_cursors.as_ref().map_or(false, |(_, group)| group.y), draw_cursors, grid_spacers, sharp_grid_lines, @@ -1007,16 +943,31 @@ impl Plot { hovered_entry = legend.hovered_entry_name(); } - if let Some(group) = linked_cursors.as_ref() { + if let Some((id, _)) = 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, + ui.memory_mut(|memory| { + let frames: &mut CursorLinkGroups = memory.data.get_temp_mut_or_default(Id::null()); + let cursors = frames.0.entry(*id).or_default(); + cursors.push(PlotFrameCursors { + id: plot_id, + cursors: plot_cursors, + }); }); } - if let Some(group) = linked_axes.as_ref() { - group.set(*transform.bounds()); + if let Some((id, _)) = linked_axes.as_ref() { + // Save the linked bounds. + ui.memory_mut(|memory| { + let link_groups: &mut BoundsLinkGroups = + memory.data.get_temp_mut_or_default(Id::null()); + link_groups.0.insert( + *id, + LinkedBounds { + bounds: *transform.bounds(), + bounds_modified, + }, + ); + }); } let memory = PlotMemory { @@ -1038,14 +989,21 @@ impl Plot { } } +/// User-requested modifications to the plot bounds. We collect them in the plot build function to later apply +/// them at the right time, as other modifications need to happen first. +enum BoundsModification { + Set(PlotBounds), + Translate(Vec2), +} + /// Provides methods to interact with a plot while building it. It is the single argument of the closure /// provided to [`Plot::show`]. See [`Plot`] for an example of how to use it. pub struct PlotUi { items: Vec>, next_auto_color_idx: usize, last_screen_transform: ScreenTransform, - bounds_modified: AxisBools, response: Response, + bounds_modifications: Vec, ctx: Context, } @@ -1071,14 +1029,14 @@ impl PlotUi { /// Set the plot bounds. Can be useful for implementing alternative plot navigation methods. pub fn set_plot_bounds(&mut self, plot_bounds: PlotBounds) { - self.last_screen_transform.set_bounds(plot_bounds); - self.bounds_modified = true.into(); + self.bounds_modifications + .push(BoundsModification::Set(plot_bounds)); } /// Move the plot bounds. Can be useful for implementing alternative plot navigation methods. pub fn translate_bounds(&mut self, delta_pos: Vec2) { - self.last_screen_transform.translate_bounds(delta_pos); - self.bounds_modified = true.into(); + self.bounds_modifications + .push(BoundsModification::Translate(delta_pos)); } /// Returns `true` if the plot area is currently hovered. @@ -1355,7 +1313,8 @@ impl PreparedPlot { item.shapes(&mut plot_ui, transform, &mut shapes); } - let cursors = if let Some(pointer) = response.hover_pos() { + let hover_pos = response.hover_pos(); + let cursors = if let Some(pointer) = hover_pos { self.hover(ui, pointer, &mut shapes) } else { Vec::new() @@ -1396,7 +1355,8 @@ impl PreparedPlot { painter.extend(shapes); if let Some((corner, formatter)) = self.coordinates_formatter.as_ref() { - if let Some(pointer) = response.hover_pos() { + let hover_pos = response.hover_pos(); + if let Some(pointer) = hover_pos { let font_id = TextStyle::Monospace.resolve(ui.style()); let coordinate = transform.value_from_position(pointer); let text = formatter.format(&coordinate, 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 d7dd40f3..0865bf7a 100644 --- a/crates/egui_demo_lib/src/demo/plot_demo.rs +++ b/crates/egui_demo_lib/src/demo/plot_demo.rs @@ -576,8 +576,6 @@ impl CustomAxisDemo { struct LinkedAxisDemo { link_x: bool, link_y: bool, - group: plot::LinkedAxisGroup, - cursor_group: plot::LinkedCursorsGroup, link_cursor_x: bool, link_cursor_y: bool, } @@ -591,8 +589,6 @@ impl Default for LinkedAxisDemo { 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, } @@ -638,37 +634,35 @@ impl LinkedAxisDemo { ui.checkbox(&mut self.link_x, "X"); ui.checkbox(&mut self.link_y, "Y"); }); - 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); + + let link_group_id = ui.id().with("linked_demo"); 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()) + .link_axis(link_group_id, self.link_x, self.link_y) + .link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y) .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()) + .link_axis(link_group_id, self.link_x, self.link_y) + .link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y) .show(ui, LinkedAxisDemo::configure_plot); }); Plot::new("linked_axis_3") .data_aspect(0.5) .width(250.0) .height(150.0) - .link_axis(self.group.clone()) - .link_cursor(self.cursor_group.clone()) + .link_axis(link_group_id, self.link_x, self.link_y) + .link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y) .show(ui, LinkedAxisDemo::configure_plot) .response }