1use std::{fmt::Debug, ops::RangeInclusive, sync::Arc};
2
3use egui::{
4 Pos2, Rangef, Rect, Response, Sense, TextStyle, TextWrapMode, Ui, Vec2, WidgetText,
5 emath::{Rot2, remap_clamp},
6 epaint::TextShape,
7};
8
9use super::{GridMark, transform::PlotTransform};
10
11const AXIS_LABEL_GAP: f32 = 0.25;
13
14pub(super) type AxisFormatterFn<'a> = dyn Fn(GridMark, &RangeInclusive<f64>) -> String + 'a;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Axis {
19 X = 0,
21
22 Y = 1,
24}
25
26impl From<Axis> for usize {
27 #[inline]
28 fn from(value: Axis) -> Self {
29 match value {
30 Axis::X => 0,
31 Axis::Y => 1,
32 }
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum VPlacement {
39 Top,
40 Bottom,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum HPlacement {
46 Left,
47 Right,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum Placement {
53 LeftBottom,
55
56 RightTop,
58}
59
60impl From<HPlacement> for Placement {
61 #[inline]
62 fn from(placement: HPlacement) -> Self {
63 match placement {
64 HPlacement::Left => Self::LeftBottom,
65 HPlacement::Right => Self::RightTop,
66 }
67 }
68}
69
70impl From<Placement> for HPlacement {
71 #[inline]
72 fn from(placement: Placement) -> Self {
73 match placement {
74 Placement::LeftBottom => Self::Left,
75 Placement::RightTop => Self::Right,
76 }
77 }
78}
79
80impl From<VPlacement> for Placement {
81 #[inline]
82 fn from(placement: VPlacement) -> Self {
83 match placement {
84 VPlacement::Top => Self::RightTop,
85 VPlacement::Bottom => Self::LeftBottom,
86 }
87 }
88}
89
90impl From<Placement> for VPlacement {
91 #[inline]
92 fn from(placement: Placement) -> Self {
93 match placement {
94 Placement::LeftBottom => Self::Bottom,
95 Placement::RightTop => Self::Top,
96 }
97 }
98}
99
100#[derive(Clone)]
104pub struct AxisHints<'a> {
105 pub(super) label: WidgetText,
106 pub(super) formatter: Arc<AxisFormatterFn<'a>>,
107 pub(super) min_thickness: f32,
108 pub(super) placement: Placement,
109 pub(super) label_spacing: Rangef,
110}
111
112impl<'a> AxisHints<'a> {
113 pub fn new_x() -> Self {
115 Self::new(Axis::X)
116 }
117
118 pub fn new_y() -> Self {
120 Self::new(Axis::Y)
121 }
122
123 pub fn new(axis: Axis) -> Self {
128 Self {
129 label: Default::default(),
130 formatter: Arc::new(Self::default_formatter),
131 min_thickness: 14.0,
132 placement: Placement::LeftBottom,
133 label_spacing: match axis {
134 Axis::X => Rangef::new(60.0, 80.0), Axis::Y => Rangef::new(20.0, 30.0), },
137 }
138 }
139
140 pub fn formatter(
145 mut self,
146 fmt: impl Fn(GridMark, &RangeInclusive<f64>) -> String + 'a,
147 ) -> Self {
148 self.formatter = Arc::new(fmt);
149 self
150 }
151
152 fn default_formatter(mark: GridMark, _range: &RangeInclusive<f64>) -> String {
153 let num_decimals = -mark.step_size.log10().round() as usize;
155
156 emath::format_with_decimals_in_range(mark.value, num_decimals..=num_decimals)
157 }
158
159 #[inline]
163 pub fn label(mut self, label: impl Into<WidgetText>) -> Self {
164 self.label = label.into();
165 self
166 }
167
168 #[inline]
170 pub fn min_thickness(mut self, min_thickness: f32) -> Self {
171 self.min_thickness = min_thickness;
172 self
173 }
174
175 #[inline]
177 #[deprecated = "Use `min_thickness` instead"]
178 pub fn max_digits(self, digits: usize) -> Self {
179 self.min_thickness(12.0 * digits as f32)
180 }
181
182 #[inline]
187 pub fn placement(mut self, placement: impl Into<Placement>) -> Self {
188 self.placement = placement.into();
189 self
190 }
191
192 #[inline]
199 pub fn label_spacing(mut self, range: impl Into<Rangef>) -> Self {
200 self.label_spacing = range.into();
201 self
202 }
203}
204
205#[derive(Clone)]
206pub(super) struct AxisWidget<'a> {
207 pub range: RangeInclusive<f64>,
208 pub hints: AxisHints<'a>,
209
210 pub rect: Rect,
212 pub transform: Option<PlotTransform>,
213 pub steps: Arc<Vec<GridMark>>,
214}
215
216impl<'a> AxisWidget<'a> {
217 pub fn new(hints: AxisHints<'a>, rect: Rect) -> Self {
219 Self {
220 range: (0.0..=0.0),
221 hints,
222 rect,
223 transform: None,
224 steps: Default::default(),
225 }
226 }
227
228 pub fn ui(self, ui: &mut Ui, axis: Axis) -> (Response, f32) {
230 let response = ui.allocate_rect(self.rect, Sense::hover());
231
232 if !ui.is_rect_visible(response.rect) {
233 return (response, 0.0);
234 }
235
236 let Some(transform) = self.transform else {
237 return (response, 0.0);
238 };
239 let tick_labels_thickness = self.add_tick_labels(ui, transform, axis);
240
241 if self.hints.label.is_empty() {
242 return (response, tick_labels_thickness);
243 }
244
245 let galley = self.hints.label.into_galley(
246 ui,
247 Some(TextWrapMode::Extend),
248 f32::INFINITY,
249 TextStyle::Body,
250 );
251
252 let text_pos = match self.hints.placement {
253 Placement::LeftBottom => match axis {
254 Axis::X => {
255 let pos = response.rect.center_bottom();
256 Pos2 {
257 x: pos.x - galley.size().x * 0.5,
258 y: pos.y - galley.size().y * (1.0 + AXIS_LABEL_GAP),
259 }
260 }
261 Axis::Y => {
262 let pos = response.rect.left_center();
263 Pos2 {
264 x: pos.x - galley.size().y * AXIS_LABEL_GAP,
265 y: pos.y + galley.size().x * 0.5,
266 }
267 }
268 },
269 Placement::RightTop => match axis {
270 Axis::X => {
271 let pos = response.rect.center_top();
272 Pos2 {
273 x: pos.x - galley.size().x * 0.5,
274 y: pos.y + galley.size().y * AXIS_LABEL_GAP,
275 }
276 }
277 Axis::Y => {
278 let pos = response.rect.right_center();
279 Pos2 {
280 x: pos.x - galley.size().y * (1.0 - AXIS_LABEL_GAP),
281 y: pos.y + galley.size().x * 0.5,
282 }
283 }
284 },
285 };
286 let axis_label_thickness = galley.size().y * (1.0 + AXIS_LABEL_GAP);
287 let angle = match axis {
288 Axis::X => 0.0,
289 Axis::Y => -std::f32::consts::FRAC_PI_2,
290 };
291
292 ui.painter()
293 .add(TextShape::new(text_pos, galley, ui.visuals().text_color()).with_angle(angle));
294
295 (response, tick_labels_thickness + axis_label_thickness)
296 }
297
298 fn add_tick_labels(&self, ui: &Ui, transform: PlotTransform, axis: Axis) -> f32 {
300 let font_id = TextStyle::Body.resolve(ui.style());
301 let label_spacing = self.hints.label_spacing;
302 let mut thickness: f32 = 0.0;
303
304 const SIDE_MARGIN: f32 = 4.0; let painter = ui.painter();
306
307 for step in self.steps.iter() {
309 let text = (self.hints.formatter)(*step, &self.range);
310 if !text.is_empty() {
311 let spacing_in_points =
312 (transform.dpos_dvalue()[usize::from(axis)] * step.step_size).abs() as f32;
313
314 if spacing_in_points <= label_spacing.min {
315 continue;
317 }
318
319 let strength = remap_clamp(spacing_in_points, label_spacing, 0.0..=1.0);
321
322 let text_color = super::color_from_strength(ui, strength);
323 let galley = painter.layout_no_wrap(text, font_id.clone(), text_color);
324 let galley_size = match axis {
325 Axis::X => galley.size(),
326 Axis::Y => galley.size() + 2.0 * SIDE_MARGIN * Vec2::X,
327 };
328
329 if spacing_in_points < galley_size[axis as usize] {
330 continue; }
332
333 match axis {
334 Axis::X => {
335 thickness = thickness.max(galley_size.y);
336
337 let projected_point = super::PlotPoint::new(step.value, 0.0);
338 let center_x = transform.position_from_point(&projected_point).x;
339 let y = match VPlacement::from(self.hints.placement) {
340 VPlacement::Bottom => self.rect.min.y,
341 VPlacement::Top => self.rect.max.y - galley_size.y,
342 };
343 let pos = Pos2::new(center_x - galley_size.x / 2.0, y);
344 painter.add(TextShape::new(pos, galley, text_color));
345 }
346 Axis::Y => {
347 thickness = thickness.max(galley_size.x);
348
349 let projected_point = super::PlotPoint::new(0.0, step.value);
350 let center_y = transform.position_from_point(&projected_point).y;
351
352 match HPlacement::from(self.hints.placement) {
353 HPlacement::Left => {
354 let angle = 0.0; if angle == 0.0 {
357 let x = self.rect.max.x - galley_size.x + SIDE_MARGIN;
358 let pos = Pos2::new(x, center_y - galley_size.y / 2.0);
359 painter.add(TextShape::new(pos, galley, text_color));
360 } else {
361 let right =
362 Pos2::new(self.rect.max.x, center_y - galley_size.y / 2.0);
363 let width = galley_size.x;
364 let left =
365 right - Rot2::from_angle(angle) * Vec2::new(width, 0.0);
366
367 painter.add(
368 TextShape::new(left, galley, text_color).with_angle(angle),
369 );
370 }
371 }
372 HPlacement::Right => {
373 let x = self.rect.min.x + SIDE_MARGIN;
374 let pos = Pos2::new(x, center_y - galley_size.y / 2.0);
375 painter.add(TextShape::new(pos, galley, text_color));
376 }
377 }
378 }
379 }
380 }
381 }
382 thickness
383 }
384}