From 527f4bfdf69f1ae2eb204258f872cfc2da20433f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 30 Jan 2024 12:45:27 +0100 Subject: [PATCH] Expand plot axes thickness to fit their labels (#3921) Expand the plot axis thickness as the contained plot axis labels get wider. This fixes a problem where the plot labels would otherwise get clipped. ![plot-axis-expansion](https://github.com/emilk/egui/assets/1148717/4500a26e-4a11-401d-9e8e-2d98d02ef3b7) --- crates/egui/src/painter.rs | 13 +- crates/egui_plot/src/axis.rs | 204 +++++++++++-------- crates/egui_plot/src/lib.rs | 353 ++++++++++++++++----------------- crates/egui_plot/src/memory.rs | 9 + crates/emath/src/rot2.rs | 13 ++ 5 files changed, 328 insertions(+), 264 deletions(-) diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index c0eb8e12f..e318fbcd6 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -6,7 +6,7 @@ use crate::{ Color32, Context, FontId, }; use epaint::{ - text::{Fonts, Galley}, + text::{Fonts, Galley, LayoutJob}, CircleShape, ClippedShape, RectShape, Rounding, Shape, Stroke, }; @@ -436,9 +436,18 @@ impl Painter { self.fonts(|f| f.layout(text, font_id, color, f32::INFINITY)) } + /// Lay out this text layut job in a galley. + /// + /// Paint the results with [`Self::galley`]. + #[inline] + #[must_use] + pub fn layout_job(&self, layout_job: LayoutJob) -> Arc { + self.fonts(|f| f.layout_job(layout_job)) + } + /// Paint text that has already been laid out in a [`Galley`]. /// - /// You can create the [`Galley`] with [`Self::layout`]. + /// You can create the [`Galley`] with [`Self::layout`] or [`Self::layout_job`]. /// /// Any uncolored parts of the [`Galley`] (using [`Color32::PLACEHOLDER`]) will be replaced with the given color. /// diff --git a/crates/egui_plot/src/axis.rs b/crates/egui_plot/src/axis.rs index d81debed3..df182631c 100644 --- a/crates/egui_plot/src/axis.rs +++ b/crates/egui_plot/src/axis.rs @@ -1,9 +1,9 @@ use std::{fmt::Debug, ops::RangeInclusive, sync::Arc}; use egui::{ - emath::{remap_clamp, round_to_decimals}, + emath::{remap_clamp, round_to_decimals, Rot2}, epaint::TextShape, - Pos2, Rangef, Rect, Response, Sense, Shape, TextStyle, Ui, WidgetText, + Pos2, Rangef, Rect, Response, Sense, TextStyle, Ui, Vec2, WidgetText, }; use super::{transform::PlotTransform, GridMark}; @@ -64,6 +64,16 @@ impl From for Placement { } } +impl From for HPlacement { + #[inline] + fn from(placement: Placement) -> Self { + match placement { + Placement::LeftBottom => Self::Left, + Placement::RightTop => Self::Right, + } + } +} + impl From for Placement { #[inline] fn from(placement: VPlacement) -> Self { @@ -74,6 +84,16 @@ impl From for Placement { } } +impl From for VPlacement { + #[inline] + fn from(placement: Placement) -> Self { + match placement { + Placement::LeftBottom => Self::Bottom, + Placement::RightTop => Self::Top, + } + } +} + /// Axis configuration. /// /// Used to configure axis label and ticks. @@ -211,16 +231,18 @@ impl AxisHints { #[derive(Clone)] pub(super) struct AxisWidget { - pub(super) range: RangeInclusive, - pub(super) hints: AxisHints, - pub(super) rect: Rect, - pub(super) transform: Option, - pub(super) steps: Arc>, + pub range: RangeInclusive, + pub hints: AxisHints, + + /// The region where we draw the axis labels. + pub rect: Rect, + pub transform: Option, + pub steps: Arc>, } impl AxisWidget { /// if `rect` as width or height == 0, is will be automatically calculated from ticks and text. - pub(super) fn new(hints: AxisHints, rect: Rect) -> Self { + pub fn new(hints: AxisHints, rect: Rect) -> Self { Self { range: (0.0..=0.0), hints, @@ -230,70 +252,76 @@ impl AxisWidget { } } - pub fn ui(self, ui: &mut Ui, axis: Axis) -> Response { + /// Returns the actual thickness of the axis. + pub fn ui(self, ui: &mut Ui, axis: Axis) -> (Response, f32) { let response = ui.allocate_rect(self.rect, Sense::hover()); if !ui.is_rect_visible(response.rect) { - return response; + return (response, 0.0); } let visuals = ui.style().visuals.clone(); - let text = self.hints.label; - let galley = text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Body); - let text_color = visuals - .override_text_color - .unwrap_or_else(|| ui.visuals().text_color()); - let angle: f32 = match axis { - Axis::X => 0.0, - Axis::Y => -std::f32::consts::TAU * 0.25, - }; - // select text_pos and angle depending on placement and orientation of widget - let text_pos = match self.hints.placement { - Placement::LeftBottom => match axis { - Axis::X => { - let pos = response.rect.center_bottom(); - Pos2 { - x: pos.x - galley.size().x / 2.0, - y: pos.y - galley.size().y * 1.25, + + { + let text = self.hints.label; + let galley = text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Body); + let text_color = visuals + .override_text_color + .unwrap_or_else(|| ui.visuals().text_color()); + let angle: f32 = match axis { + Axis::X => 0.0, + Axis::Y => -std::f32::consts::TAU * 0.25, + }; + // select text_pos and angle depending on placement and orientation of widget + let text_pos = match self.hints.placement { + Placement::LeftBottom => match axis { + Axis::X => { + let pos = response.rect.center_bottom(); + Pos2 { + x: pos.x - galley.size().x / 2.0, + y: pos.y - galley.size().y * 1.25, + } } - } - Axis::Y => { - let pos = response.rect.left_center(); - Pos2 { - x: pos.x, - y: pos.y + galley.size().x / 2.0, + Axis::Y => { + let pos = response.rect.left_center(); + Pos2 { + x: pos.x, + y: pos.y + galley.size().x / 2.0, + } } - } - }, - Placement::RightTop => match axis { - Axis::X => { - let pos = response.rect.center_top(); - Pos2 { - x: pos.x - galley.size().x / 2.0, - y: pos.y + galley.size().y * 0.25, + }, + Placement::RightTop => match axis { + Axis::X => { + let pos = response.rect.center_top(); + Pos2 { + x: pos.x - galley.size().x / 2.0, + y: pos.y + galley.size().y * 0.25, + } } - } - Axis::Y => { - let pos = response.rect.right_center(); - Pos2 { - x: pos.x - galley.size().y * 1.5, - y: pos.y + galley.size().x / 2.0, + Axis::Y => { + let pos = response.rect.right_center(); + Pos2 { + x: pos.x - galley.size().y * 1.5, + y: pos.y + galley.size().x / 2.0, + } } - } - }, - }; + }, + }; - ui.painter() - .add(TextShape::new(text_pos, galley, text_color).with_angle(angle)); + ui.painter() + .add(TextShape::new(text_pos, galley, text_color).with_angle(angle)); + } - // --- add ticks --- let font_id = TextStyle::Body.resolve(ui.style()); let Some(transform) = self.transform else { - return response; + return (response, 0.0); }; let label_spacing = self.hints.label_spacing; + let mut thickness: f32 = 0.0; + + // Add tick labels: for step in self.steps.iter() { let text = (self.hints.formatter)(*step, self.hints.digits, &self.range); if !text.is_empty() { @@ -314,41 +342,61 @@ impl AxisWidget { .layout_no_wrap(text, font_id.clone(), text_color); if spacing_in_points < galley.size()[axis as usize] { - continue; // the galley won't fit + continue; // the galley won't fit (likely too wide on the X axis). } - let text_pos = match axis { + match axis { Axis::X => { - let y = match self.hints.placement { - Placement::LeftBottom => self.rect.min.y, - Placement::RightTop => self.rect.max.y - galley.size().y, - }; + thickness = thickness.max(galley.size().y); + let projected_point = super::PlotPoint::new(step.value, 0.0); - Pos2 { - x: transform.position_from_point(&projected_point).x - - galley.size().x / 2.0, - y, - } + let center_x = transform.position_from_point(&projected_point).x; + let y = match VPlacement::from(self.hints.placement) { + VPlacement::Bottom => self.rect.min.y, + VPlacement::Top => self.rect.max.y - galley.size().y, + }; + let pos = Pos2::new(center_x - galley.size().x / 2.0, y); + ui.painter().add(TextShape::new(pos, galley, text_color)); } Axis::Y => { - let x = match self.hints.placement { - Placement::LeftBottom => self.rect.max.x - galley.size().x, - Placement::RightTop => self.rect.min.x, - }; + thickness = thickness.max(galley.size().x); + let projected_point = super::PlotPoint::new(0.0, step.value); - Pos2 { - x, - y: transform.position_from_point(&projected_point).y - - galley.size().y / 2.0, - } + let center_y = transform.position_from_point(&projected_point).y; + + match HPlacement::from(self.hints.placement) { + HPlacement::Left => { + let angle = 0.0; // TODO: allow users to rotate text + + if angle == 0.0 { + let x = self.rect.max.x - galley.size().x; + let pos = Pos2::new(x, center_y - galley.size().y / 2.0); + ui.painter().add(TextShape::new(pos, galley, text_color)); + } else { + let right = Pos2::new( + self.rect.max.x, + center_y - galley.size().y / 2.0, + ); + let width = galley.size().x; + let left = + right - Rot2::from_angle(angle) * Vec2::new(width, 0.0); + + ui.painter().add( + TextShape::new(left, galley, text_color).with_angle(angle), + ); + } + } + HPlacement::Right => { + let x = self.rect.min.x; + let pos = Pos2::new(x, center_y - galley.size().y / 2.0); + ui.painter().add(TextShape::new(pos, galley, text_color)); + } + }; } }; - - ui.painter() - .add(Shape::galley(text_pos, galley, text_color)); } } - response + (response, thickness) } } diff --git a/crates/egui_plot/src/lib.rs b/crates/egui_plot/src/lib.rs index 4fe89ff36..2225ff132 100644 --- a/crates/egui_plot/src/lib.rs +++ b/crates/egui_plot/src/lib.rs @@ -768,84 +768,29 @@ impl Plot { .at_least(min_size.y); vec2(width, height) }; + // Determine complete rect of widget. let complete_rect = Rect { min: pos, max: pos + size, }; - // Next we want to create this layout. - // Indices are only examples. - // - // left right - // +---+---------x----------+ + - // | | X-axis 3 | - // | +--------------------+ top - // | | X-axis 2 | - // +-+-+--------------------+-+-+ - // |y|y| |y|y| - // |-|-| |-|-| - // |A|A| |A|A| - // y|x|x| Plot Window |x|x| - // |i|i| |i|i| - // |s|s| |s|s| - // |1|0| |2|3| - // +-+-+--------------------+-+-+ - // | X-axis 0 | | - // +--------------------+ | bottom - // | X-axis 1 | | - // + +--------------------+---+ - // - - let mut plot_rect: Rect = { - // Calcuclate the space needed for each axis labels. - let mut margin = Margin::ZERO; - if show_axes.x { - for cfg in &x_axes { - match cfg.placement { - axis::Placement::LeftBottom => { - margin.bottom += cfg.thickness(Axis::X); - } - axis::Placement::RightTop => { - margin.top += cfg.thickness(Axis::X); - } - } - } - } - if show_axes.y { - for cfg in &y_axes { - match cfg.placement { - axis::Placement::LeftBottom => { - margin.left += cfg.thickness(Axis::Y); - } - axis::Placement::RightTop => { - margin.right += cfg.thickness(Axis::Y); - } - } - } - } - - // determine plot rectangle - margin.shrink_rect(complete_rect) - }; - let [mut x_axis_widgets, mut y_axis_widgets] = - axis_widgets(show_axes, plot_rect, [&x_axes, &y_axes]); + let plot_id = id.unwrap_or_else(|| ui.make_persistent_id(id_source)); - // If too little space, remove axis widgets - if plot_rect.width() <= 0.0 || plot_rect.height() <= 0.0 { - y_axis_widgets.clear(); - x_axis_widgets.clear(); - plot_rect = complete_rect; - } + let ([x_axis_widgets, y_axis_widgets], plot_rect) = axis_widgets( + PlotMemory::load(ui.ctx(), plot_id).as_ref(), // TODO: avoid loading plot memory twice + show_axes, + complete_rect, + [&x_axes, &y_axes], + ); // Allocate the plot window. let response = ui.allocate_rect(plot_rect, Sense::click_and_drag()); - let rect = plot_rect; // Load or initialize the memory. - let plot_id = id.unwrap_or_else(|| ui.make_persistent_id(id_source)); - ui.ctx().check_for_id_clash(plot_id, rect, "Plot"); - let memory = if reset { + ui.ctx().check_for_id_clash(plot_id, plot_rect, "Plot"); + + let mut mem = if reset { if let Some((name, _)) = linked_axes.as_ref() { ui.data_mut(|data| { let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL); @@ -860,24 +805,20 @@ impl Plot { auto_bounds: default_auto_bounds, hovered_item: None, hidden_items: Default::default(), - transform: PlotTransform::new(rect, min_auto_bounds, center_axis.x, center_axis.y), + transform: PlotTransform::new(plot_rect, min_auto_bounds, center_axis.x, center_axis.y), last_click_pos_for_zoom: None, + x_axis_thickness: Default::default(), + y_axis_thickness: Default::default(), }); - let PlotMemory { - mut auto_bounds, - mut hovered_item, - mut hidden_items, - transform: last_plot_transform, - mut last_click_pos_for_zoom, - } = memory; + let last_plot_transform = mem.transform; // Call the plot build function. let mut plot_ui = PlotUi { items: Vec::new(), next_auto_color_idx: 0, last_plot_transform, - last_auto_bounds: auto_bounds, + last_auto_bounds: mem.auto_bounds, response, bounds_modifications: Vec::new(), ctx: ui.ctx().clone(), @@ -894,9 +835,9 @@ impl Plot { // Background if show_background { ui.painter() - .with_clip_rect(rect) + .with_clip_rect(plot_rect) .add(epaint::RectShape::new( - rect, + plot_rect, Rounding::same(2.0), ui.visuals().extreme_bg_color, ui.visuals().widgets.noninteractive.bg_stroke, @@ -905,16 +846,16 @@ impl Plot { // --- Legend --- let legend = legend_config - .and_then(|config| LegendWidget::try_new(rect, config, &items, &hidden_items)); + .and_then(|config| LegendWidget::try_new(plot_rect, config, &items, &mem.hidden_items)); // Don't show hover cursor when hovering over legend. - if hovered_item.is_some() { + if mem.hovered_item.is_some() { show_x = false; show_y = false; } // Remove the deselected items. - items.retain(|item| !hidden_items.contains(item.name())); + items.retain(|item| !mem.hidden_items.contains(item.name())); // Highlight the hovered items. - if let Some(hovered_name) = &hovered_item { + if let Some(hovered_name) = &mem.hovered_item { items .iter_mut() .filter(|entry| entry.name() == hovered_name) @@ -961,11 +902,11 @@ impl Plot { if let Some(linked_bounds) = link_groups.0.get(id) { if axes.x { bounds.set_x(&linked_bounds.bounds); - auto_bounds.x = linked_bounds.auto_bounds.x; + mem.auto_bounds.x = linked_bounds.auto_bounds.x; } if axes.y { bounds.set_y(&linked_bounds.bounds); - auto_bounds.y = linked_bounds.auto_bounds.y; + mem.auto_bounds.y = linked_bounds.auto_bounds.y; } }; }); @@ -973,7 +914,7 @@ impl Plot { // Allow double-clicking to reset to the initial bounds. if allow_double_click_reset && response.double_clicked() { - auto_bounds = true.into(); + mem.auto_bounds = true.into(); } // Apply bounds modifications. @@ -981,30 +922,32 @@ impl Plot { match modification { BoundsModification::Set(new_bounds) => { bounds = new_bounds; - auto_bounds = false.into(); + mem.auto_bounds = false.into(); } BoundsModification::Translate(delta) => { bounds.translate(delta); - auto_bounds = false.into(); + mem.auto_bounds = false.into(); + } + BoundsModification::AutoBounds(new_auto_bounds) => { + mem.auto_bounds = new_auto_bounds; } - BoundsModification::AutoBounds(new_auto_bounds) => auto_bounds = new_auto_bounds, BoundsModification::Zoom(zoom_factor, center) => { bounds.zoom(zoom_factor, center); - auto_bounds = false.into(); + mem.auto_bounds = false.into(); } } } // Reset bounds to initial bounds if they haven't been modified. - if auto_bounds.x { + if mem.auto_bounds.x { bounds.set_x(&min_auto_bounds); } - if auto_bounds.y { + if mem.auto_bounds.y { bounds.set_y(&min_auto_bounds); } - let auto_x = auto_bounds.x && (!min_auto_bounds.is_valid_x() || default_auto_bounds.x); - let auto_y = auto_bounds.y && (!min_auto_bounds.is_valid_y() || default_auto_bounds.y); + let auto_x = mem.auto_bounds.x && (!min_auto_bounds.is_valid_x() || default_auto_bounds.x); + let auto_y = mem.auto_bounds.y && (!min_auto_bounds.is_valid_y() || default_auto_bounds.y); // Set bounds automatically based on content. if auto_x || auto_y { @@ -1027,17 +970,19 @@ impl Plot { } } - let mut transform = PlotTransform::new(rect, bounds, center_axis.x, center_axis.y); + mem.transform = PlotTransform::new(plot_rect, bounds, center_axis.x, center_axis.y); // Enforce aspect ratio if let Some(data_aspect) = data_aspect { 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); + mem.transform + .set_aspect_by_changing_axis(data_aspect as f64, change_x); } else if default_auto_bounds.any() { - transform.set_aspect_by_expanding(data_aspect as f64); + mem.transform.set_aspect_by_expanding(data_aspect as f64); } else { - transform.set_aspect_by_changing_axis(data_aspect as f64, false); + mem.transform + .set_aspect_by_changing_axis(data_aspect as f64, false); } } @@ -1051,8 +996,8 @@ impl Plot { if !allow_drag.y { delta.y = 0.0; } - transform.translate_bounds(delta); - auto_bounds = !allow_drag; + mem.transform.translate_bounds(delta); + mem.auto_bounds = !allow_drag; } // Zooming @@ -1061,9 +1006,9 @@ impl Plot { // Save last click to allow boxed zooming if response.drag_started() && response.dragged_by(boxed_zoom_pointer_button) { // it would be best for egui that input has a memory of the last click pos because it's a common pattern - last_click_pos_for_zoom = response.hover_pos(); + mem.last_click_pos_for_zoom = response.hover_pos(); } - let box_start_pos = last_click_pos_for_zoom; + let box_start_pos = mem.last_click_pos_for_zoom; let box_end_pos = response.hover_pos(); if let (Some(box_start_pos), Some(box_end_pos)) = (box_start_pos, box_end_pos) { // while dragging prepare a Shape and draw it later on top of the plot @@ -1085,8 +1030,8 @@ impl Plot { } // when the click is release perform the zoom if response.drag_released() { - let box_start_pos = transform.value_from_position(box_start_pos); - let box_end_pos = transform.value_from_position(box_end_pos); + let box_start_pos = mem.transform.value_from_position(box_start_pos); + let box_end_pos = mem.transform.value_from_position(box_end_pos); let new_bounds = PlotBounds { min: [ box_start_pos.x.min(box_end_pos.x), @@ -1098,11 +1043,11 @@ impl Plot { ], }; if new_bounds.is_valid() { - transform.set_bounds(new_bounds); - auto_bounds = false.into(); + mem.transform.set_bounds(new_bounds); + mem.auto_bounds = false.into(); } // reset the boxed zoom state - last_click_pos_for_zoom = None; + mem.last_click_pos_for_zoom = None; } } } @@ -1122,15 +1067,15 @@ impl Plot { zoom_factor.y = 1.0; } if zoom_factor != Vec2::splat(1.0) { - transform.zoom(zoom_factor, hover_pos); - auto_bounds = !allow_zoom; + mem.transform.zoom(zoom_factor, hover_pos); + mem.auto_bounds = !allow_zoom; } } if allow_scroll { let scroll_delta = ui.input(|i| i.smooth_scroll_delta); if scroll_delta != Vec2::ZERO { - transform.translate_bounds(-scroll_delta); - auto_bounds = false.into(); + mem.transform.translate_bounds(-scroll_delta); + mem.auto_bounds = false.into(); } } } @@ -1138,12 +1083,12 @@ impl Plot { // --- transform initialized // Add legend widgets to plot - let bounds = transform.bounds(); + let bounds = mem.transform.bounds(); let x_axis_range = bounds.range_x(); let x_steps = Arc::new({ let input = GridInput { bounds: (bounds.min[0], bounds.max[0]), - base_step_size: transform.dvalue_dpos()[0].abs() * grid_spacing.min as f64, + base_step_size: mem.transform.dvalue_dpos()[0].abs() * grid_spacing.min as f64, }; (grid_spacers[0])(input) }); @@ -1151,26 +1096,28 @@ impl Plot { let y_steps = Arc::new({ let input = GridInput { bounds: (bounds.min[1], bounds.max[1]), - base_step_size: transform.dvalue_dpos()[1].abs() * grid_spacing.min as f64, + base_step_size: mem.transform.dvalue_dpos()[1].abs() * grid_spacing.min as f64, }; (grid_spacers[1])(input) }); - for mut widget in x_axis_widgets { + for (i, mut widget) in x_axis_widgets.into_iter().enumerate() { widget.range = x_axis_range.clone(); - widget.transform = Some(transform); + widget.transform = Some(mem.transform); widget.steps = x_steps.clone(); - widget.ui(ui, Axis::X); + let (_response, thickness) = widget.ui(ui, Axis::X); + mem.x_axis_thickness.insert(i, thickness); } - for mut widget in y_axis_widgets { + for (i, mut widget) in y_axis_widgets.into_iter().enumerate() { widget.range = y_axis_range.clone(); - widget.transform = Some(transform); + widget.transform = Some(mem.transform); widget.steps = y_steps.clone(); - widget.ui(ui, Axis::Y); + let (_response, thickness) = widget.ui(ui, Axis::Y); + mem.y_axis_thickness.insert(i, thickness); } // Initialize values from functions. for item in &mut items { - item.initialize(transform.bounds().range_x()); + item.initialize(mem.transform.bounds().range_x()); } let prepared = PreparedPlot { @@ -1181,7 +1128,7 @@ impl Plot { coordinates_formatter, show_grid, grid_spacing, - transform, + transform: mem.transform, draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.1.x), draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.1.y), draw_cursors, @@ -1193,14 +1140,18 @@ impl Plot { 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); - ui.painter().with_clip_rect(rect).add(boxed_zoom_rect.1); + ui.painter() + .with_clip_rect(plot_rect) + .add(boxed_zoom_rect.0); + ui.painter() + .with_clip_rect(plot_rect) + .add(boxed_zoom_rect.1); } if let Some(mut legend) = legend { ui.add(&mut legend); - hidden_items = legend.hidden_items(); - hovered_item = legend.hovered_item_name(); + mem.hidden_items = legend.hidden_items(); + mem.hovered_item = legend.hovered_item_name(); } if let Some((id, _)) = linked_cursors.as_ref() { @@ -1222,28 +1173,24 @@ impl Plot { link_groups.0.insert( *id, LinkedBounds { - bounds: *transform.bounds(), - auto_bounds, + bounds: *mem.transform.bounds(), + auto_bounds: mem.auto_bounds, }, ); }); } - let memory = PlotMemory { - auto_bounds, - hovered_item, - hidden_items, - transform, - last_click_pos_for_zoom, - }; - memory.store(ui.ctx(), plot_id); + let transform = mem.transform; + mem.store(ui.ctx(), plot_id); let response = if show_x || show_y { response.on_hover_cursor(CursorIcon::Crosshair) } else { response }; + ui.advance_cursor_after_rect(complete_rect); + PlotResponse { inner, response, @@ -1252,77 +1199,115 @@ impl Plot { } } +/// Returns the rect left after adding axes. fn axis_widgets( + mem: Option<&PlotMemory>, show_axes: Vec2b, - plot_rect: Rect, + complete_rect: Rect, [x_axes, y_axes]: [&[AxisHints]; 2], -) -> [Vec; 2] { +) -> ([Vec; 2], Rect) { + // Next we want to create this layout. + // Indices are only examples. + // + // left right + // +---+---------x----------+ + + // | | X-axis 3 | + // | +--------------------+ top + // | | X-axis 2 | + // +-+-+--------------------+-+-+ + // |y|y| |y|y| + // |-|-| |-|-| + // |A|A| |A|A| + // y|x|x| Plot Window |x|x| + // |i|i| |i|i| + // |s|s| |s|s| + // |1|0| |2|3| + // +-+-+--------------------+-+-+ + // | X-axis 0 | | + // +--------------------+ | bottom + // | X-axis 1 | | + // + +--------------------+---+ + // + let mut x_axis_widgets = Vec::::new(); let mut y_axis_widgets = Vec::::new(); - // Widget count per border of plot in order left, top, right, bottom - struct NumWidgets { - left: usize, - top: usize, - right: usize, - bottom: usize, - } - let mut num_widgets = NumWidgets { - left: 0, - top: 0, - right: 0, - bottom: 0, - }; + // Will shrink as we add more axes. + let mut rect_left = complete_rect; + if show_axes.x { - for cfg in x_axes { - let size_y = Vec2::new(0.0, cfg.thickness(Axis::X)); - let rect = match cfg.placement { - axis::Placement::LeftBottom => { - let off = num_widgets.bottom as f32; - num_widgets.bottom += 1; - Rect { - min: plot_rect.left_bottom() + size_y * off, - max: plot_rect.right_bottom() + size_y * (off + 1.0), - } + // We will fix this later, once we know how much space the y axes take up. + let initial_x_range = complete_rect.x_range(); + + for (i, cfg) in x_axes.iter().enumerate().rev() { + let mut height = cfg.thickness(Axis::X); + if let Some(mem) = mem { + // If the labels took up too much space the previous frame, give them more space now: + height = height.max(mem.x_axis_thickness.get(&i).copied().unwrap_or_default()); + } + + let rect = match VPlacement::from(cfg.placement) { + VPlacement::Bottom => { + let bottom = rect_left.bottom(); + *rect_left.bottom_mut() -= height; + let top = rect_left.bottom(); + Rect::from_x_y_ranges(initial_x_range, top..=bottom) } - axis::Placement::RightTop => { - let off = num_widgets.top as f32; - num_widgets.top += 1; - Rect { - min: plot_rect.left_top() - size_y * (off + 1.0), - max: plot_rect.right_top() - size_y * off, - } + VPlacement::Top => { + let top = rect_left.top(); + *rect_left.top_mut() += height; + let bottom = rect_left.top(); + Rect::from_x_y_ranges(initial_x_range, top..=bottom) } }; x_axis_widgets.push(AxisWidget::new(cfg.clone(), rect)); } } if show_axes.y { - for cfg in y_axes { - let size_x = Vec2::new(cfg.thickness(Axis::Y), 0.0); - let rect = match cfg.placement { - axis::Placement::LeftBottom => { - let off = num_widgets.left as f32; - num_widgets.left += 1; - Rect { - min: plot_rect.left_top() - size_x * (off + 1.0), - max: plot_rect.left_bottom() - size_x * off, - } + // We know this, since we've already allocated space for the x axes. + let plot_y_range = rect_left.y_range(); + + for (i, cfg) in y_axes.iter().enumerate().rev() { + let mut width = cfg.thickness(Axis::Y); + if let Some(mem) = mem { + // If the labels took up too much space the previous frame, give them more space now: + width = width.max(mem.y_axis_thickness.get(&i).copied().unwrap_or_default()); + } + + let rect = match HPlacement::from(cfg.placement) { + HPlacement::Left => { + let left = rect_left.left(); + *rect_left.left_mut() += width; + let right = rect_left.left(); + Rect::from_x_y_ranges(left..=right, plot_y_range) } - axis::Placement::RightTop => { - let off = num_widgets.right as f32; - num_widgets.right += 1; - Rect { - min: plot_rect.right_top() + size_x * off, - max: plot_rect.right_bottom() + size_x * (off + 1.0), - } + HPlacement::Right => { + let right = rect_left.right(); + *rect_left.right_mut() -= width; + let left = rect_left.right(); + Rect::from_x_y_ranges(left..=right, plot_y_range) } }; y_axis_widgets.push(AxisWidget::new(cfg.clone(), rect)); } } - [x_axis_widgets, y_axis_widgets] + let mut plot_rect = rect_left; + + // If too little space, remove axis widgets + if plot_rect.width() <= 0.0 || plot_rect.height() <= 0.0 { + y_axis_widgets.clear(); + x_axis_widgets.clear(); + plot_rect = complete_rect; + } + + // Bow that we know the final x_range of the plot_rect, + // assign it to the x_axis_widgets (they are currently too wide): + for widget in &mut x_axis_widgets { + widget.rect = Rect::from_x_y_ranges(plot_rect.x_range(), widget.rect.y_range()); + } + + ([x_axis_widgets, y_axis_widgets], plot_rect) } /// User-requested modifications to the plot bounds. We collect them in the plot build function to later apply diff --git a/crates/egui_plot/src/memory.rs b/crates/egui_plot/src/memory.rs index c334f1152..df3a1a5e2 100644 --- a/crates/egui_plot/src/memory.rs +++ b/crates/egui_plot/src/memory.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use egui::{ahash, Context, Id, Pos2, Vec2b}; use crate::{PlotBounds, PlotTransform}; @@ -23,6 +25,13 @@ pub struct PlotMemory { /// Allows to remember the first click position when performing a boxed zoom pub(crate) last_click_pos_for_zoom: Option, + + /// The thickness of each of the axes the previous frame. + /// + /// This is used in the next frame to make the axes thicker + /// in order to fit the labels, if necessary. + pub(crate) x_axis_thickness: BTreeMap, + pub(crate) y_axis_thickness: BTreeMap, } impl PlotMemory { diff --git a/crates/emath/src/rot2.rs b/crates/emath/src/rot2.rs index 61ff36541..da67acb0c 100644 --- a/crates/emath/src/rot2.rs +++ b/crates/emath/src/rot2.rs @@ -28,6 +28,7 @@ pub struct Rot2 { /// Identity rotation impl Default for Rot2 { /// Identity rotation + #[inline] fn default() -> Self { Self { s: 0.0, c: 1.0 } } @@ -39,29 +40,35 @@ impl Rot2 { /// Angle is clockwise in radians. /// A 𝞃/4 = 90° rotation means rotating the X axis to the Y axis. + #[inline] pub fn from_angle(angle: f32) -> Self { let (s, c) = angle.sin_cos(); Self { s, c } } + #[inline] pub fn angle(self) -> f32 { self.s.atan2(self.c) } /// The factor by which vectors will be scaled. + #[inline] pub fn length(self) -> f32 { self.c.hypot(self.s) } + #[inline] pub fn length_squared(self) -> f32 { self.c.powi(2) + self.s.powi(2) } + #[inline] pub fn is_finite(self) -> bool { self.c.is_finite() && self.s.is_finite() } #[must_use] + #[inline] pub fn inverse(self) -> Self { Self { s: -self.s, @@ -70,6 +77,7 @@ impl Rot2 { } #[must_use] + #[inline] pub fn normalized(self) -> Self { let l = self.length(); let ret = Self { @@ -95,6 +103,7 @@ impl std::fmt::Debug for Rot2 { impl std::ops::Mul for Rot2 { type Output = Self; + #[inline] fn mul(self, r: Self) -> Self { /* |lc -ls| * |rc -rs| @@ -111,6 +120,7 @@ impl std::ops::Mul for Rot2 { impl std::ops::Mul for Rot2 { type Output = Vec2; + #[inline] fn mul(self, v: Vec2) -> Vec2 { Vec2 { x: self.c * v.x - self.s * v.y, @@ -123,6 +133,7 @@ impl std::ops::Mul for Rot2 { impl std::ops::Mul for f32 { type Output = Rot2; + #[inline] fn mul(self, r: Rot2) -> Rot2 { Rot2 { c: self * r.c, @@ -135,6 +146,7 @@ impl std::ops::Mul for f32 { impl std::ops::Mul for Rot2 { type Output = Self; + #[inline] fn mul(self, r: f32) -> Self { Self { c: self.c * r, @@ -147,6 +159,7 @@ impl std::ops::Mul for Rot2 { impl std::ops::Div for Rot2 { type Output = Self; + #[inline] fn div(self, r: f32) -> Self { Self { c: self.c / r,