egui_plot: customizable spacing of grid and axis label spacing (#3896)

This lets users specify the spacing of the grid lines and the axis
labels, as well as when these start to fade out.

New:
* `AxisHints::new_x/new_y` (replaces `::default()`)
* `AxisHints::label_spacing`
* `Plot::grid_spacing`
This commit is contained in:
Emil Ernerfeldt 2024-01-26 13:36:49 +01:00 committed by GitHub
parent 6b0782c96b
commit 5d0bc2bf7d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 239 additions and 190 deletions

View File

@ -530,23 +530,27 @@ impl CustomAxesDemo {
100.0 * y 100.0 * y
} }
let x_fmt = |x, _digits, _range: &RangeInclusive<f64>| { let time_formatter = |mark: GridMark, _digits, _range: &RangeInclusive<f64>| {
if x < 0.0 * MINS_PER_DAY || x >= 5.0 * MINS_PER_DAY { let minutes = mark.value;
if minutes < 0.0 || 5.0 * MINS_PER_DAY <= minutes {
// No labels outside value bounds // No labels outside value bounds
String::new() String::new()
} else if is_approx_integer(x / MINS_PER_DAY) { } else if is_approx_integer(minutes / MINS_PER_DAY) {
// Days // Days
format!("Day {}", day(x)) format!("Day {}", day(minutes))
} else { } else {
// Hours and minutes // Hours and minutes
format!("{h}:{m:02}", h = hour(x), m = minute(x)) format!("{h}:{m:02}", h = hour(minutes), m = minute(minutes))
} }
}; };
let y_fmt = |y, _digits, _range: &RangeInclusive<f64>| { let percentage_formatter = |mark: GridMark, _digits, _range: &RangeInclusive<f64>| {
// Display only integer percentages let percent = 100.0 * mark.value;
if !is_approx_zero(y) && is_approx_integer(100.0 * y) { if is_approx_zero(percent) {
format!("{:.0}%", percent(y)) String::new() // skip zero
} else if is_approx_integer(percent) {
// Display only integer percentages
format!("{percent:.0}%")
} else { } else {
String::new() String::new()
} }
@ -565,15 +569,15 @@ impl CustomAxesDemo {
ui.label("Zoom in on the X-axis to see hours and minutes"); ui.label("Zoom in on the X-axis to see hours and minutes");
let x_axes = vec![ let x_axes = vec![
AxisHints::default().label("Time").formatter(x_fmt), AxisHints::new_x().label("Time").formatter(time_formatter),
AxisHints::default().label("Value"), AxisHints::new_x().label("Value"),
]; ];
let y_axes = vec![ let y_axes = vec![
AxisHints::default() AxisHints::new_y()
.label("Percent") .label("Percent")
.formatter(y_fmt) .formatter(percentage_formatter)
.max_digits(4), .max_digits(4),
AxisHints::default() AxisHints::new_y()
.label("Absolute") .label("Absolute")
.placement(egui_plot::HPlacement::Right), .placement(egui_plot::HPlacement::Right),
]; ];

View File

@ -1,22 +1,23 @@
use std::{fmt::Debug, ops::RangeInclusive, sync::Arc}; use std::{fmt::Debug, ops::RangeInclusive, sync::Arc};
use egui::emath::{remap_clamp, round_to_decimals, Pos2, Rect}; use egui::{
use egui::epaint::{Shape, TextShape}; emath::{remap_clamp, round_to_decimals},
epaint::TextShape,
use crate::{Response, Sense, TextStyle, Ui, WidgetText}; Pos2, Rangef, Rect, Response, Sense, Shape, TextStyle, Ui, WidgetText,
};
use super::{transform::PlotTransform, GridMark}; use super::{transform::PlotTransform, GridMark};
pub(super) type AxisFormatterFn = dyn Fn(f64, usize, &RangeInclusive<f64>) -> String; pub(super) type AxisFormatterFn = dyn Fn(GridMark, usize, &RangeInclusive<f64>) -> String;
/// X or Y axis. /// X or Y axis.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Axis { pub enum Axis {
/// Horizontal X-Axis /// Horizontal X-Axis
X, X = 0,
/// Vertical Y-axis /// Vertical Y-axis
Y, Y = 1,
} }
impl From<Axis> for usize { impl From<Axis> for usize {
@ -82,28 +83,41 @@ pub struct AxisHints {
pub(super) formatter: Arc<AxisFormatterFn>, pub(super) formatter: Arc<AxisFormatterFn>,
pub(super) digits: usize, pub(super) digits: usize,
pub(super) placement: Placement, pub(super) placement: Placement,
pub(super) label_spacing: Rangef,
} }
// TODO: this just a guess. It might cease to work if a user changes font size. // TODO: this just a guess. It might cease to work if a user changes font size.
const LINE_HEIGHT: f32 = 12.0; const LINE_HEIGHT: f32 = 12.0;
impl Default for AxisHints { impl AxisHints {
/// Initializes a default axis configuration for the X axis.
pub fn new_x() -> Self {
Self::new(Axis::X)
}
/// Initializes a default axis configuration for the X axis.
pub fn new_y() -> Self {
Self::new(Axis::Y)
}
/// Initializes a default axis configuration for the specified axis. /// Initializes a default axis configuration for the specified axis.
/// ///
/// `label` is empty. /// `label` is empty.
/// `formatter` is default float to string formatter. /// `formatter` is default float to string formatter.
/// maximum `digits` on tick label is 5. /// maximum `digits` on tick label is 5.
fn default() -> Self { pub fn new(axis: Axis) -> Self {
Self { Self {
label: Default::default(), label: Default::default(),
formatter: Arc::new(Self::default_formatter), formatter: Arc::new(Self::default_formatter),
digits: 5, digits: 5,
placement: Placement::LeftBottom, placement: Placement::LeftBottom,
label_spacing: match axis {
Axis::X => Rangef::new(60.0, 80.0), // labels can get pretty wide
Axis::Y => Rangef::new(20.0, 30.0), // text isn't very high
},
} }
} }
}
impl AxisHints {
/// Specify custom formatter for ticks. /// Specify custom formatter for ticks.
/// ///
/// The first parameter of `formatter` is the raw tick value as `f64`. /// The first parameter of `formatter` is the raw tick value as `f64`.
@ -111,13 +125,19 @@ impl AxisHints {
/// The second parameter of `formatter` is the currently shown range on this axis. /// The second parameter of `formatter` is the currently shown range on this axis.
pub fn formatter( pub fn formatter(
mut self, mut self,
fmt: impl Fn(f64, usize, &RangeInclusive<f64>) -> String + 'static, fmt: impl Fn(GridMark, usize, &RangeInclusive<f64>) -> String + 'static,
) -> Self { ) -> Self {
self.formatter = Arc::new(fmt); self.formatter = Arc::new(fmt);
self self
} }
fn default_formatter(tick: f64, max_digits: usize, _range: &RangeInclusive<f64>) -> String { fn default_formatter(
mark: GridMark,
max_digits: usize,
_range: &RangeInclusive<f64>,
) -> String {
let tick = mark.value;
if tick.abs() > 10.0_f64.powf(max_digits as f64) { if tick.abs() > 10.0_f64.powf(max_digits as f64) {
let tick_rounded = tick as isize; let tick_rounded = tick as isize;
return format!("{tick_rounded:+e}"); return format!("{tick_rounded:+e}");
@ -157,6 +177,18 @@ impl AxisHints {
self self
} }
/// Set the minimum spacing between labels
///
/// When labels get closer together than the given minimum, then they become invisible.
/// When they get further apart than the max, they are at full opacity.
///
/// Labels can never be closer together than the [`crate::Plot::grid_spacing`] setting.
#[inline]
pub fn label_spacing(mut self, range: impl Into<Rangef>) -> Self {
self.label_spacing = range.into();
self
}
pub(super) fn thickness(&self, axis: Axis) -> f32 { pub(super) fn thickness(&self, axis: Axis) -> f32 {
match axis { match axis {
Axis::X => { Axis::X => {
@ -201,114 +233,119 @@ impl AxisWidget {
pub fn ui(self, ui: &mut Ui, axis: Axis) -> Response { pub fn ui(self, ui: &mut Ui, axis: Axis) -> Response {
let response = ui.allocate_rect(self.rect, Sense::hover()); let response = ui.allocate_rect(self.rect, Sense::hover());
if ui.is_rect_visible(response.rect) { if !ui.is_rect_visible(response.rect) {
let visuals = ui.style().visuals.clone(); return response;
let text = self.hints.label; }
let galley = text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Body);
let text_color = visuals let visuals = ui.style().visuals.clone();
.override_text_color let text = self.hints.label;
.unwrap_or_else(|| ui.visuals().text_color()); let galley = text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Body);
let angle: f32 = match axis { let text_color = visuals
Axis::X => 0.0, .override_text_color
Axis::Y => -std::f32::consts::TAU * 0.25, .unwrap_or_else(|| ui.visuals().text_color());
}; let angle: f32 = match axis {
// select text_pos and angle depending on placement and orientation of widget Axis::X => 0.0,
let text_pos = match self.hints.placement { Axis::Y => -std::f32::consts::TAU * 0.25,
Placement::LeftBottom => match axis { };
Axis::X => { // select text_pos and angle depending on placement and orientation of widget
let pos = response.rect.center_bottom(); let text_pos = match self.hints.placement {
Pos2 { Placement::LeftBottom => match axis {
x: pos.x - galley.size().x / 2.0, Axis::X => {
y: pos.y - galley.size().y * 1.25, 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,
}
}
},
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,
}
}
},
};
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;
};
for step in self.steps.iter() {
let text = (self.hints.formatter)(step.value, self.hints.digits, &self.range);
if !text.is_empty() {
const MIN_TEXT_SPACING: f32 = 20.0;
const FULL_CONTRAST_SPACING: f32 = 40.0;
let spacing_in_points =
(transform.dpos_dvalue()[usize::from(axis)] * step.step_size).abs() as f32;
if spacing_in_points <= MIN_TEXT_SPACING {
continue;
}
let line_strength = remap_clamp(
spacing_in_points,
MIN_TEXT_SPACING..=FULL_CONTRAST_SPACING,
0.0..=1.0,
);
let line_color = super::color_from_strength(ui, line_strength);
let galley = ui
.painter()
.layout_no_wrap(text, font_id.clone(), line_color);
let text_pos = 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,
};
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,
}
}
Axis::Y => {
let x = match self.hints.placement {
Placement::LeftBottom => self.rect.max.x - galley.size().x,
Placement::RightTop => self.rect.min.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,
}
}
};
ui.painter()
.add(Shape::galley(text_pos, galley, text_color));
} }
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,
}
}
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));
// --- add ticks ---
let font_id = TextStyle::Body.resolve(ui.style());
let Some(transform) = self.transform else {
return response;
};
let label_spacing = self.hints.label_spacing;
for step in self.steps.iter() {
let text = (self.hints.formatter)(*step, self.hints.digits, &self.range);
if !text.is_empty() {
let spacing_in_points =
(transform.dpos_dvalue()[usize::from(axis)] * step.step_size).abs() as f32;
if spacing_in_points <= label_spacing.min {
// Labels are too close together - don't paint them.
continue;
}
// Fade in labels as they get further apart:
let strength = remap_clamp(spacing_in_points, label_spacing, 0.0..=1.0);
let text_color = super::color_from_strength(ui, strength);
let galley = ui
.painter()
.layout_no_wrap(text, font_id.clone(), text_color);
if spacing_in_points < galley.size()[axis as usize] {
continue; // the galley won't fit
}
let text_pos = 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,
};
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,
}
}
Axis::Y => {
let x = match self.hints.placement {
Placement::LeftBottom => self.rect.max.x - galley.size().x,
Placement::RightTop => self.rect.min.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,
}
}
};
ui.painter()
.add(Shape::galley(text_pos, galley, text_color));
} }
} }

View File

@ -79,10 +79,6 @@ impl Default for CoordinatesFormatter {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
const MIN_LINE_SPACING_IN_POINTS: f64 = 6.0; // TODO(emilk): large enough for a wide label
// ----------------------------------------------------------------------------
/// Indicates a vertical or horizontal cursor line in plot coordinates. /// Indicates a vertical or horizontal cursor line in plot coordinates.
#[derive(Copy, Clone, PartialEq)] #[derive(Copy, Clone, PartialEq)]
enum Cursor { enum Cursor {
@ -175,7 +171,9 @@ pub struct Plot {
legend_config: Option<Legend>, legend_config: Option<Legend>,
show_background: bool, show_background: bool,
show_axes: Vec2b, show_axes: Vec2b,
show_grid: Vec2b, show_grid: Vec2b,
grid_spacing: Rangef,
grid_spacers: [GridSpacer; 2], grid_spacers: [GridSpacer; 2],
sharp_grid_lines: bool, sharp_grid_lines: bool,
clamp_grid: bool, clamp_grid: bool,
@ -213,12 +211,14 @@ impl Plot {
show_y: true, show_y: true,
label_formatter: None, label_formatter: None,
coordinates_formatter: None, coordinates_formatter: None,
x_axes: vec![Default::default()], x_axes: vec![AxisHints::new(Axis::X)],
y_axes: vec![Default::default()], y_axes: vec![AxisHints::new(Axis::Y)],
legend_config: None, legend_config: None,
show_background: true, show_background: true,
show_axes: true.into(), show_axes: true.into(),
show_grid: true.into(), show_grid: true.into(),
grid_spacing: Rangef::new(8.0, 300.0),
grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)], grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)],
sharp_grid_lines: true, sharp_grid_lines: true,
clamp_grid: false, clamp_grid: false,
@ -453,6 +453,17 @@ impl Plot {
self self
} }
/// Set when the grid starts showing.
///
/// When grid lines are closer than the given minimum, they will be hidden.
/// When they get further apart they will fade in, until the reaches the given maximum,
/// at which point they are fully opaque.
#[inline]
pub fn grid_spacing(mut self, grid_spacing: impl Into<Rangef>) -> Self {
self.grid_spacing = grid_spacing.into();
self
}
/// Clamp the grid to only be visible at the range of data where we have values. /// Clamp the grid to only be visible at the range of data where we have values.
/// ///
/// Default: `false`. /// Default: `false`.
@ -624,12 +635,12 @@ impl Plot {
/// Specify custom formatter for ticks on the main X-axis. /// Specify custom formatter for ticks on the main X-axis.
/// ///
/// Arguments of `fmt`: /// Arguments of `fmt`:
/// * raw tick value as `f64`. /// * the grid mark to format
/// * maximum requested number of characters per tick label. /// * maximum requested number of characters per tick label.
/// * currently shown range on this axis. /// * currently shown range on this axis.
pub fn x_axis_formatter( pub fn x_axis_formatter(
mut self, mut self,
fmt: impl Fn(f64, usize, &RangeInclusive<f64>) -> String + 'static, fmt: impl Fn(GridMark, usize, &RangeInclusive<f64>) -> String + 'static,
) -> Self { ) -> Self {
if let Some(main) = self.x_axes.first_mut() { if let Some(main) = self.x_axes.first_mut() {
main.formatter = Arc::new(fmt); main.formatter = Arc::new(fmt);
@ -640,12 +651,12 @@ impl Plot {
/// Specify custom formatter for ticks on the main Y-axis. /// Specify custom formatter for ticks on the main Y-axis.
/// ///
/// Arguments of `fmt`: /// Arguments of `fmt`:
/// * raw tick value as `f64`. /// * the grid mark to format
/// * maximum requested number of characters per tick label. /// * maximum requested number of characters per tick label.
/// * currently shown range on this axis. /// * currently shown range on this axis.
pub fn y_axis_formatter( pub fn y_axis_formatter(
mut self, mut self,
fmt: impl Fn(f64, usize, &RangeInclusive<f64>) -> String + 'static, fmt: impl Fn(GridMark, usize, &RangeInclusive<f64>) -> String + 'static,
) -> Self { ) -> Self {
if let Some(main) = self.y_axes.first_mut() { if let Some(main) = self.y_axes.first_mut() {
main.formatter = Arc::new(fmt); main.formatter = Arc::new(fmt);
@ -723,6 +734,7 @@ impl Plot {
show_background, show_background,
show_axes, show_axes,
show_grid, show_grid,
grid_spacing,
linked_axes, linked_axes,
linked_cursors, linked_cursors,
@ -827,7 +839,7 @@ impl Plot {
} }
// Allocate the plot window. // Allocate the plot window.
let response = ui.allocate_rect(plot_rect, Sense::drag()); let response = ui.allocate_rect(plot_rect, Sense::click_and_drag());
let rect = plot_rect; let rect = plot_rect;
// Load or initialize the memory. // Load or initialize the memory.
@ -1131,7 +1143,7 @@ impl Plot {
let x_steps = Arc::new({ let x_steps = Arc::new({
let input = GridInput { let input = GridInput {
bounds: (bounds.min[0], bounds.max[0]), bounds: (bounds.min[0], bounds.max[0]),
base_step_size: transform.dvalue_dpos()[0] * MIN_LINE_SPACING_IN_POINTS * 2.0, base_step_size: transform.dvalue_dpos()[0].abs() * grid_spacing.min as f64,
}; };
(grid_spacers[0])(input) (grid_spacers[0])(input)
}); });
@ -1139,7 +1151,7 @@ impl Plot {
let y_steps = Arc::new({ let y_steps = Arc::new({
let input = GridInput { let input = GridInput {
bounds: (bounds.min[1], bounds.max[1]), bounds: (bounds.min[1], bounds.max[1]),
base_step_size: transform.dvalue_dpos()[1] * MIN_LINE_SPACING_IN_POINTS * 2.0, base_step_size: transform.dvalue_dpos()[1].abs() * grid_spacing.min as f64,
}; };
(grid_spacers[1])(input) (grid_spacers[1])(input)
}); });
@ -1168,6 +1180,7 @@ impl Plot {
label_formatter, label_formatter,
coordinates_formatter, coordinates_formatter,
show_grid, show_grid,
grid_spacing,
transform, transform,
draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.1.x), 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_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.1.y),
@ -1565,6 +1578,8 @@ pub struct GridInput {
/// ///
/// Computed as the ratio between the diagram's bounds (in plot coordinates) and the viewport /// Computed as the ratio between the diagram's bounds (in plot coordinates) and the viewport
/// (in frame/window coordinates), scaled up to represent the minimal possible step. /// (in frame/window coordinates), scaled up to represent the minimal possible step.
///
/// Always positive.
pub base_step_size: f64, pub base_step_size: f64,
} }
@ -1634,6 +1649,7 @@ struct PreparedPlot {
// axis_formatters: [AxisFormatter; 2], // axis_formatters: [AxisFormatter; 2],
transform: PlotTransform, transform: PlotTransform,
show_grid: Vec2b, show_grid: Vec2b,
grid_spacing: Rangef,
grid_spacers: [GridSpacer; 2], grid_spacers: [GridSpacer; 2],
draw_cursor_x: bool, draw_cursor_x: bool,
draw_cursor_y: bool, draw_cursor_y: bool,
@ -1648,10 +1664,10 @@ impl PreparedPlot {
let mut axes_shapes = Vec::new(); let mut axes_shapes = Vec::new();
if self.show_grid.x { if self.show_grid.x {
self.paint_grid(ui, &mut axes_shapes, Axis::X); self.paint_grid(ui, &mut axes_shapes, Axis::X, self.grid_spacing);
} }
if self.show_grid.y { if self.show_grid.y {
self.paint_grid(ui, &mut axes_shapes, Axis::Y); self.paint_grid(ui, &mut axes_shapes, Axis::Y, self.grid_spacing);
} }
// Sort the axes by strength so that those with higher strength are drawn in front. // Sort the axes by strength so that those with higher strength are drawn in front.
@ -1728,7 +1744,7 @@ impl PreparedPlot {
cursors cursors
} }
fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis) { fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis, fade_range: Rangef) {
#![allow(clippy::collapsible_else_if)] #![allow(clippy::collapsible_else_if)]
let Self { let Self {
transform, transform,
@ -1746,7 +1762,7 @@ impl PreparedPlot {
let input = GridInput { let input = GridInput {
bounds: (bounds.min[iaxis], bounds.max[iaxis]), bounds: (bounds.min[iaxis], bounds.max[iaxis]),
base_step_size: transform.dvalue_dpos()[iaxis] * MIN_LINE_SPACING_IN_POINTS, base_step_size: transform.dvalue_dpos()[iaxis].abs() * fade_range.min as f64,
}; };
let steps = (grid_spacers[iaxis])(input); let steps = (grid_spacers[iaxis])(input);
@ -1786,44 +1802,42 @@ impl PreparedPlot {
let pos_in_gui = transform.position_from_point(&value); let pos_in_gui = transform.position_from_point(&value);
let spacing_in_points = (transform.dpos_dvalue()[iaxis] * step.step_size).abs() as f32; let spacing_in_points = (transform.dpos_dvalue()[iaxis] * step.step_size).abs() as f32;
if spacing_in_points > MIN_LINE_SPACING_IN_POINTS as f32 { if spacing_in_points <= fade_range.min {
let line_strength = remap_clamp( continue; // Too close together
spacing_in_points, }
MIN_LINE_SPACING_IN_POINTS as f32..=300.0,
0.0..=1.0,
);
let line_color = color_from_strength(ui, line_strength); let line_strength = remap_clamp(spacing_in_points, fade_range, 0.0..=1.0);
let mut p0 = pos_in_gui; let line_color = color_from_strength(ui, line_strength);
let mut p1 = pos_in_gui;
p0[1 - iaxis] = transform.frame().min[1 - iaxis];
p1[1 - iaxis] = transform.frame().max[1 - iaxis];
if let Some(clamp_range) = clamp_range { let mut p0 = pos_in_gui;
match axis { let mut p1 = pos_in_gui;
Axis::X => { p0[1 - iaxis] = transform.frame().min[1 - iaxis];
p0.y = transform.position_from_point_y(clamp_range.min[1]); p1[1 - iaxis] = transform.frame().max[1 - iaxis];
p1.y = transform.position_from_point_y(clamp_range.max[1]);
} if let Some(clamp_range) = clamp_range {
Axis::Y => { match axis {
p0.x = transform.position_from_point_x(clamp_range.min[0]); Axis::X => {
p1.x = transform.position_from_point_x(clamp_range.max[0]); p0.y = transform.position_from_point_y(clamp_range.min[1]);
} p1.y = transform.position_from_point_y(clamp_range.max[1]);
}
Axis::Y => {
p0.x = transform.position_from_point_x(clamp_range.min[0]);
p1.x = transform.position_from_point_x(clamp_range.max[0]);
} }
} }
if self.sharp_grid_lines {
// Round to avoid aliasing
p0 = ui.painter().round_pos_to_pixels(p0);
p1 = ui.painter().round_pos_to_pixels(p1);
}
shapes.push((
Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)),
line_strength,
));
} }
if self.sharp_grid_lines {
// Round to avoid aliasing
p0 = ui.painter().round_pos_to_pixels(p0);
p1 = ui.painter().round_pos_to_pixels(p1);
}
shapes.push((
Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)),
line_strength,
));
} }
} }
@ -1932,12 +1946,6 @@ pub fn format_number(number: f64, num_decimals: usize) -> String {
/// Determine a color from a 0-1 strength value. /// Determine a color from a 0-1 strength value.
pub fn color_from_strength(ui: &Ui, strength: f32) -> Color32 { pub fn color_from_strength(ui: &Ui, strength: f32) -> Color32 {
let bg = ui.visuals().extreme_bg_color; let base_color = ui.visuals().text_color();
let fg = ui.visuals().widgets.open.fg_stroke.color; base_color.gamma_multiply(strength.sqrt())
let mix = 0.5 * strength.sqrt();
Color32::from_rgb(
lerp((bg.r() as f32)..=(fg.r() as f32), mix) as u8,
lerp((bg.g() as f32)..=(fg.g() as f32), mix) as u8,
lerp((bg.b() as f32)..=(fg.b() as f32), mix) as u8,
)
} }