Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically expand plot axes thickness to fit their labels #3921

Merged
merged 9 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions crates/egui/src/painter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::{
Color32, Context, FontId,
};
use epaint::{
text::{Fonts, Galley},
text::{Fonts, Galley, LayoutJob},
CircleShape, ClippedShape, RectShape, Rounding, Shape, Stroke,
};

Expand Down Expand Up @@ -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<Galley> {
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.
///
Expand Down
204 changes: 126 additions & 78 deletions crates/egui_plot/src/axis.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -64,6 +64,16 @@ impl From<HPlacement> for Placement {
}
}

impl From<Placement> for HPlacement {
#[inline]
fn from(placement: Placement) -> Self {
match placement {
Placement::LeftBottom => Self::Left,
Placement::RightTop => Self::Right,
}
}
}

impl From<VPlacement> for Placement {
#[inline]
fn from(placement: VPlacement) -> Self {
Expand All @@ -74,6 +84,16 @@ impl From<VPlacement> for Placement {
}
}

impl From<Placement> 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.
Expand Down Expand Up @@ -211,16 +231,18 @@ impl AxisHints {

#[derive(Clone)]
pub(super) struct AxisWidget {
pub(super) range: RangeInclusive<f64>,
pub(super) hints: AxisHints,
pub(super) rect: Rect,
pub(super) transform: Option<PlotTransform>,
pub(super) steps: Arc<Vec<GridMark>>,
pub range: RangeInclusive<f64>,
pub hints: AxisHints,

/// The region where we draw the axis labels.
pub rect: Rect,
pub transform: Option<PlotTransform>,
pub steps: Arc<Vec<GridMark>>,
}

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,
Expand All @@ -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() {
Expand All @@ -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)
}
}
Loading
Loading