use std::fmt::Debug;
use std::ops::RangeInclusive;
use std::sync::Arc;
use egui::Color32;
use egui::FontId;
use egui::Pos2;
use egui::Rangef;
use egui::Rect;
use egui::Response;
use egui::Sense;
use egui::TextStyle;
use egui::TextWrapMode;
use egui::Ui;
use egui::Vec2;
use egui::WidgetText;
use egui::emath::Rot2;
use egui::emath::remap_clamp;
use egui::epaint::TextShape;
use emath::Vec2b;
use emath::pos2;
use emath::remap;
use crate::bounds::PlotBounds;
use crate::bounds::PlotPoint;
use crate::grid::GridMark;
use crate::placement::HPlacement;
use crate::placement::Placement;
use crate::placement::VPlacement;
const AXIS_LABEL_GAP: f32 = 0.25;
pub(super) type AxisFormatterFn<'a> = dyn Fn(GridMark, &RangeInclusive<f64>) -> String + 'a;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Axis {
X = 0,
Y = 1,
}
impl From<Axis> for usize {
#[inline]
fn from(value: Axis) -> Self {
match value {
Axis::X => 0,
Axis::Y => 1,
}
}
}
#[derive(Clone)]
pub struct AxisHints<'a> {
pub(super) label: WidgetText,
pub(super) formatter: Arc<AxisFormatterFn<'a>>,
pub(super) min_thickness: f32,
pub(super) placement: Placement,
pub(super) label_spacing: Rangef,
pub(super) tick_label_color: Option<Color32>,
pub(super) tick_label_font: Option<FontId>,
}
impl<'a> AxisHints<'a> {
pub fn new_x() -> Self {
Self::new(Axis::X)
}
pub fn new_y() -> Self {
Self::new(Axis::Y)
}
pub fn new(axis: Axis) -> Self {
Self {
label: Default::default(),
formatter: Arc::new(Self::default_formatter),
min_thickness: 14.0,
placement: Placement::LeftBottom,
label_spacing: match axis {
Axis::X => Rangef::new(60.0, 80.0), Axis::Y => Rangef::new(20.0, 30.0), },
tick_label_color: None,
tick_label_font: None,
}
}
#[inline]
pub fn formatter(mut self, fmt: impl Fn(GridMark, &RangeInclusive<f64>) -> String + 'a) -> Self {
self.formatter = Arc::new(fmt);
self
}
fn default_formatter(mark: GridMark, _range: &RangeInclusive<f64>) -> String {
let num_decimals = -mark.step_size.log10().round() as usize;
emath::format_with_decimals_in_range(mark.value, num_decimals..=num_decimals)
}
#[inline]
pub fn label(mut self, label: impl Into<WidgetText>) -> Self {
self.label = label.into();
self
}
#[inline]
pub fn min_thickness(mut self, min_thickness: f32) -> Self {
self.min_thickness = min_thickness;
self
}
#[inline]
#[deprecated = "Use `min_thickness` instead"]
pub fn max_digits(self, digits: usize) -> Self {
self.min_thickness(12.0 * digits as f32)
}
#[inline]
pub fn placement(mut self, placement: impl Into<Placement>) -> Self {
self.placement = placement.into();
self
}
#[inline]
pub fn label_spacing(mut self, range: impl Into<Rangef>) -> Self {
self.label_spacing = range.into();
self
}
#[inline]
pub fn tick_label_color(mut self, color: impl Into<Color32>) -> Self {
self.tick_label_color = Some(color.into());
self
}
#[inline]
pub fn tick_label_font(mut self, font: FontId) -> Self {
self.tick_label_font = Some(font);
self
}
}
#[derive(Clone)]
pub(super) struct AxisWidget<'a> {
pub range: RangeInclusive<f64>,
pub hints: AxisHints<'a>,
pub rect: Rect,
pub transform: Option<PlotTransform>,
pub steps: Arc<Vec<GridMark>>,
}
impl<'a> AxisWidget<'a> {
pub fn new(hints: AxisHints<'a>, rect: Rect) -> Self {
Self {
range: (0.0..=0.0),
hints,
rect,
transform: None,
steps: Default::default(),
}
}
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, 0.0);
}
let Some(transform) = self.transform else {
return (response, 0.0);
};
let tick_labels_thickness = self.add_tick_labels(ui, transform, axis);
if self.hints.label.is_empty() {
return (response, tick_labels_thickness);
}
let galley = self
.hints
.label
.into_galley(ui, Some(TextWrapMode::Extend), f32::INFINITY, TextStyle::Body);
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 * 0.5,
y: pos.y - galley.size().y * (1.0 + AXIS_LABEL_GAP),
}
}
Axis::Y => {
let pos = response.rect.left_center();
Pos2 {
x: pos.x - galley.size().y * AXIS_LABEL_GAP,
y: pos.y + galley.size().x * 0.5,
}
}
},
Placement::RightTop => match axis {
Axis::X => {
let pos = response.rect.center_top();
Pos2 {
x: pos.x - galley.size().x * 0.5,
y: pos.y + galley.size().y * AXIS_LABEL_GAP,
}
}
Axis::Y => {
let pos = response.rect.right_center();
Pos2 {
x: pos.x - galley.size().y * (1.0 - AXIS_LABEL_GAP),
y: pos.y + galley.size().x * 0.5,
}
}
},
};
let axis_label_thickness = galley.size().y * (1.0 + AXIS_LABEL_GAP);
let angle = match axis {
Axis::X => 0.0,
Axis::Y => -std::f32::consts::FRAC_PI_2,
};
ui.painter()
.add(TextShape::new(text_pos, galley, ui.visuals().text_color()).with_angle(angle));
(response, tick_labels_thickness + axis_label_thickness)
}
fn add_tick_labels(&self, ui: &Ui, transform: PlotTransform, axis: Axis) -> f32 {
let font_id = TextStyle::Body.resolve(ui.style());
let label_spacing = self.hints.label_spacing;
let mut thickness: f32 = 0.0;
const SIDE_MARGIN: f32 = 4.0; let painter = ui.painter();
for step in self.steps.iter() {
let text = (self.hints.formatter)(*step, &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 {
continue;
}
let strength = remap_clamp(spacing_in_points, label_spacing, 0.0..=1.0);
let text_color = if let Some(color) = self.hints.tick_label_color {
color.gamma_multiply(strength.sqrt())
} else {
super::color_from_strength(ui, strength)
};
let label_font_id = self.hints.tick_label_font.clone().unwrap_or_else(|| font_id.clone());
let galley = painter.layout_no_wrap(text, label_font_id, text_color);
let galley_size = match axis {
Axis::X => galley.size(),
Axis::Y => galley.size() + 2.0 * SIDE_MARGIN * Vec2::X,
};
if spacing_in_points < galley_size[axis as usize] {
continue; }
match axis {
Axis::X => {
thickness = thickness.max(galley_size.y);
let projected_point = super::PlotPoint::new(step.value, 0.0);
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);
painter.add(TextShape::new(pos, galley, text_color));
}
Axis::Y => {
thickness = thickness.max(galley_size.x);
let projected_point = super::PlotPoint::new(0.0, step.value);
let center_y = transform.position_from_point(&projected_point).y;
match HPlacement::from(self.hints.placement) {
HPlacement::Left => {
let angle = 0.0;
if angle == 0.0 {
let x = self.rect.max.x - galley_size.x + SIDE_MARGIN;
let pos = Pos2::new(x, center_y - galley_size.y / 2.0);
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);
painter.add(TextShape::new(left, galley, text_color).with_angle(angle));
}
}
HPlacement::Right => {
let x = self.rect.min.x + SIDE_MARGIN;
let pos = Pos2::new(x, center_y - galley_size.y / 2.0);
painter.add(TextShape::new(pos, galley, text_color));
}
}
}
}
}
}
thickness
}
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug)]
pub struct PlotTransform {
frame: Rect,
bounds: PlotBounds,
centered: Vec2b,
inverted_axis: Vec2b,
}
impl PlotTransform {
pub fn new(frame: Rect, bounds: PlotBounds, center_axis: impl Into<Vec2b>) -> Self {
debug_assert!(
0.0 <= frame.width() && 0.0 <= frame.height(),
"Bad plot frame: {frame:?}"
);
let center_axis = center_axis.into();
let mut new_bounds = bounds;
if !bounds.is_finite_x() {
new_bounds.set_x(&PlotBounds::new_symmetrical(1.0));
} else if bounds.width() <= 0.0 {
new_bounds.set_x_center_width(
bounds.center().x,
if bounds.is_valid_y() { bounds.height() } else { 1.0 },
);
}
if !bounds.is_finite_y() {
new_bounds.set_y(&PlotBounds::new_symmetrical(1.0));
} else if bounds.height() <= 0.0 {
new_bounds.set_y_center_height(
bounds.center().y,
if bounds.is_valid_x() { bounds.width() } else { 1.0 },
);
}
if center_axis.x {
new_bounds.make_x_symmetrical();
}
if center_axis.y {
new_bounds.make_y_symmetrical();
}
debug_assert!(new_bounds.is_valid(), "Bad final plot bounds: {new_bounds:?}");
Self {
frame,
bounds: new_bounds,
centered: center_axis,
inverted_axis: Vec2b::new(false, false),
}
}
pub fn new_with_invert_axis(
frame: Rect,
bounds: PlotBounds,
center_axis: impl Into<Vec2b>,
invert_axis: impl Into<Vec2b>,
) -> Self {
let mut new = Self::new(frame, bounds, center_axis);
new.inverted_axis = invert_axis.into();
new
}
#[inline]
pub fn frame(&self) -> &Rect {
&self.frame
}
#[inline]
pub fn bounds(&self) -> &PlotBounds {
&self.bounds
}
#[inline]
pub fn set_bounds(&mut self, bounds: PlotBounds) {
self.bounds = bounds;
}
pub fn translate_bounds(&mut self, mut delta_pos: (f64, f64)) {
if self.centered.x {
delta_pos.0 = 0.;
}
if self.centered.y {
delta_pos.1 = 0.;
}
delta_pos.0 *= self.dvalue_dpos()[0];
delta_pos.1 *= self.dvalue_dpos()[1];
self.bounds.translate((delta_pos.0, delta_pos.1));
}
pub fn zoom(&mut self, zoom_factor: Vec2, center: Pos2) {
let center = self.value_from_position(center);
let mut new_bounds = self.bounds;
new_bounds.zoom(zoom_factor, center);
if new_bounds.is_valid() {
self.bounds = new_bounds;
}
}
pub fn position_from_point_x(&self, value: f64) -> f32 {
remap(
value,
self.bounds.min[0]..=self.bounds.max[0],
if self.inverted_axis[0] {
(self.frame.right() as f64)..=(self.frame.left() as f64)
} else {
(self.frame.left() as f64)..=(self.frame.right() as f64)
},
) as f32
}
pub fn position_from_point_y(&self, value: f64) -> f32 {
remap(
value,
self.bounds.min[1]..=self.bounds.max[1],
if self.inverted_axis[1] {
(self.frame.top() as f64)..=(self.frame.bottom() as f64)
} else {
(self.frame.bottom() as f64)..=(self.frame.top() as f64)
},
) as f32
}
pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 {
pos2(self.position_from_point_x(value.x), self.position_from_point_y(value.y))
}
pub fn value_from_position(&self, pos: Pos2) -> PlotPoint {
let x = remap(
pos.x as f64,
if self.inverted_axis[0] {
(self.frame.right() as f64)..=(self.frame.left() as f64)
} else {
(self.frame.left() as f64)..=(self.frame.right() as f64)
},
self.bounds.range_x(),
);
let y = remap(
pos.y as f64,
if self.inverted_axis[1] {
(self.frame.top() as f64)..=(self.frame.bottom() as f64)
} else {
(self.frame.bottom() as f64)..=(self.frame.top() as f64)
},
self.bounds.range_y(),
);
PlotPoint::new(x, y)
}
pub fn rect_from_values(&self, value1: &PlotPoint, value2: &PlotPoint) -> Rect {
let pos1 = self.position_from_point(value1);
let pos2 = self.position_from_point(value2);
let mut rect = Rect::NOTHING;
rect.extend_with(pos1);
rect.extend_with(pos2);
rect
}
pub fn dpos_dvalue_x(&self) -> f64 {
let flip = if self.inverted_axis[0] { -1.0 } else { 1.0 };
flip * (self.frame.width() as f64) / self.bounds.width()
}
pub fn dpos_dvalue_y(&self) -> f64 {
let flip = if self.inverted_axis[1] { 1.0 } else { -1.0 };
flip * (self.frame.height() as f64) / self.bounds.height()
}
pub fn dpos_dvalue(&self) -> [f64; 2] {
[self.dpos_dvalue_x(), self.dpos_dvalue_y()]
}
pub fn dvalue_dpos(&self) -> [f64; 2] {
[1.0 / self.dpos_dvalue_x(), 1.0 / self.dpos_dvalue_y()]
}
fn aspect(&self) -> f64 {
let rw = self.frame.width() as f64;
let rh = self.frame.height() as f64;
(self.bounds.width() / rw) / (self.bounds.height() / rh)
}
pub(crate) fn set_aspect_by_expanding(&mut self, aspect: f64) {
let current_aspect = self.aspect();
let epsilon = 1e-5;
if (current_aspect - aspect).abs() < epsilon {
return;
}
if current_aspect < aspect {
self.bounds
.expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5);
} else {
self.bounds
.expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5);
}
}
pub(crate) fn set_aspect_by_changing_axis(&mut self, aspect: f64, axis: Axis) {
let current_aspect = self.aspect();
let epsilon = 1e-5;
if (current_aspect - aspect).abs() < epsilon {
return;
}
match axis {
Axis::X => {
self.bounds
.expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5);
}
Axis::Y => {
self.bounds
.expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5);
}
}
}
}