1use crate::analog_signal_cache::{AnalogSignalCache, CacheQueryResult, is_nan_highimp};
4use crate::displayed_item::{
5 AnalogSettings, DisplayedFieldRef, DisplayedItemRef, DisplayedVariable,
6};
7use crate::drawing_canvas::{AnalogDrawingCommands, DrawingCommands, VariableDrawCommands};
8use crate::message::Message;
9use crate::translation::TranslatorList;
10use crate::view::DrawingContext;
11use crate::viewport::Viewport;
12use crate::wave_data::{TimeRange, WaveData};
13use ecolor::Color32;
14use emath::{Align2, Pos2, Rect, Vec2};
15use epaint::{CornerRadius, PathShape, Stroke};
16use num::{BigInt, ToPrimitive};
17use std::collections::HashMap;
18use surfer_translation_types::NumericRange;
19
20pub enum AnalogDrawingCommand {
21 Flat {
25 start_px: f32,
26 start_val: f64,
27 end_px: f32,
28 end_val: f64,
29 },
30 Range { px: f32, min_val: f64, max_val: f64 },
33}
34
35pub(crate) fn variable_analog_draw_commands(
38 displayed_variable: &DisplayedVariable,
39 display_id: DisplayedItemRef,
40 waves: &WaveData,
41 translators: &TranslatorList,
42 view_width: f32,
43 viewport_idx: usize,
44) -> Option<VariableDrawCommands> {
45 let render_mode = displayed_variable.analog.as_ref()?;
46
47 let wave_container = waves.inner.as_waves()?;
48 let displayed_field_ref: DisplayedFieldRef = display_id.into();
49 let translator = waves.variable_translator(&displayed_field_ref, translators);
50 let viewport = &waves.viewports[viewport_idx];
51 let range = waves.time_range();
52
53 let signal_id = wave_container
54 .signal_id(&displayed_variable.variable_ref)
55 .ok()?;
56 let translator_name = translator.name();
57 let cache_key = (signal_id, translator_name.clone());
58
59 let cache = match &render_mode.cache {
61 Some(entry)
62 if entry.generation == waves.cache_generation && entry.cache_key == cache_key =>
63 {
64 if let Some(cache) = entry.get() {
65 cache
66 } else {
67 let mut local_commands = HashMap::new();
69 local_commands.insert(
70 vec![],
71 DrawingCommands::Analog(AnalogDrawingCommands::Loading),
72 );
73 return Some(VariableDrawCommands {
74 draw_clock_edges: false,
75 clock_edges: vec![],
76 display_id,
77 local_commands,
78 local_msgs: vec![],
79 });
80 }
81 }
82 _ => {
83 let mut local_commands = HashMap::new();
85 local_commands.insert(
86 vec![],
87 DrawingCommands::Analog(AnalogDrawingCommands::Loading),
88 );
89 return Some(VariableDrawCommands {
90 draw_clock_edges: false,
91 clock_edges: vec![],
92 display_id,
93 local_commands,
94 local_msgs: vec![Message::BuildAnalogCache {
95 display_id,
96 cache_key,
97 }],
98 });
99 }
100 };
101
102 let meta = wave_container
103 .variable_meta(&displayed_variable.variable_ref)
104 .ok();
105 let type_limits = meta.as_ref().and_then(|m| translator.numeric_range(m));
106
107 let analog_commands = CommandBuilder::new(
108 cache,
109 viewport,
110 range,
111 view_width,
112 render_mode.settings,
113 type_limits,
114 )
115 .build();
116
117 let mut local_commands = HashMap::new();
118 local_commands.insert(vec![], DrawingCommands::Analog(analog_commands));
119
120 Some(VariableDrawCommands {
121 draw_clock_edges: false,
122 clock_edges: vec![],
123 display_id,
124 local_commands,
125 local_msgs: vec![],
126 })
127}
128
129pub fn draw_analog(
131 analog_commands: &AnalogDrawingCommands,
132 color: Color32,
133 offset: f32,
134 height_scaling_factor: f32,
135 brightness_shift: Option<f32>,
136 ctx: &mut DrawingContext,
137) {
138 let color = crate::drawing_canvas::apply_brightness_shift(
139 color,
140 brightness_shift,
141 ctx.theme.canvas_colors.background,
142 );
143 let AnalogDrawingCommands::Ready {
144 viewport_min,
145 viewport_max,
146 global_min,
147 global_max,
148 type_limits,
149 values,
150 min_valid_pixel,
151 max_valid_pixel,
152 analog_settings,
153 } = analog_commands
154 else {
155 draw_building_indicator(offset, height_scaling_factor, ctx);
156 return;
157 };
158
159 let (min_val, max_val) = select_value_range(
160 *viewport_min,
161 *viewport_max,
162 *global_min,
163 *global_max,
164 *type_limits,
165 analog_settings,
166 );
167
168 let render_ctx = RenderContext::new(
169 color,
170 min_val,
171 max_val,
172 *min_valid_pixel,
173 *max_valid_pixel,
174 offset,
175 height_scaling_factor,
176 brightness_shift,
177 ctx,
178 );
179
180 match analog_settings.render_style {
182 crate::displayed_item::AnalogRenderStyle::Step => {
183 let mut strategy = StepStrategy::default();
184 render_with_strategy(values, &render_ctx, &mut strategy, ctx);
185 }
186 crate::displayed_item::AnalogRenderStyle::Interpolated => {
187 let mut strategy = InterpolatedStrategy::default();
188 render_with_strategy(values, &render_ctx, &mut strategy, ctx);
189 }
190 }
191
192 draw_amplitude_labels(&render_ctx, ctx);
193}
194
195fn draw_building_indicator(offset: f32, height_scaling_factor: f32, ctx: &mut DrawingContext) {
197 let elapsed = ctx.painter.ctx().input(|i| i.time);
199 let dot_index = (elapsed * 3.) as usize % 3;
200 let text = ["Building. ", "Building.. ", "Building..."][dot_index];
201
202 let row_height = ctx.cfg.line_height * height_scaling_factor;
203 let center_y = offset + row_height * 0.5;
204 let center_x = ctx.cfg.canvas_size.x * 0.5;
205 let pos = (ctx.to_screen)(center_x, center_y);
206
207 ctx.painter.text(
208 pos,
209 Align2::CENTER_CENTER,
210 text,
211 egui::FontId::monospace(ctx.cfg.text_size),
212 ctx.theme.foreground.gamma_multiply(0.6),
213 );
214}
215
216fn select_value_range(
217 viewport_min: f64,
218 viewport_max: f64,
219 global_min: f64,
220 global_max: f64,
221 type_limits: Option<NumericRange>,
222 settings: &AnalogSettings,
223) -> (f64, f64) {
224 let (min, max) = match settings.y_axis_scale {
225 crate::displayed_item::AnalogYAxisScale::Viewport => (viewport_min, viewport_max),
226 crate::displayed_item::AnalogYAxisScale::Global => (global_min, global_max),
227 crate::displayed_item::AnalogYAxisScale::TypeLimits => {
228 type_limits.map_or((global_min, global_max), |r| (r.min, r.max))
229 }
230 };
231
232 if !min.is_finite() || !max.is_finite() || min > max {
234 return (-0.5, 0.5);
235 }
236
237 if (max - min).abs() < f64::EPSILON {
239 (min - 0.5, max + 0.5)
240 } else {
241 (min, max)
242 }
243}
244
245struct CommandBuilder<'a> {
247 cache: &'a AnalogSignalCache,
248 viewport: &'a Viewport,
249 range: &'a TimeRange,
250 view_width: f32,
251 min_valid_pixel: f32,
252 max_valid_pixel: f32,
253 output: CommandOutput,
254 analog_settings: AnalogSettings,
255 type_limits: Option<NumericRange>,
256}
257
258struct CommandOutput {
260 commands: Vec<AnalogDrawingCommand>,
261 pending_flat: Option<(f32, f64)>,
262 viewport_min: f64,
263 viewport_max: f64,
264}
265
266impl CommandOutput {
267 fn new() -> Self {
268 Self {
269 commands: Vec::new(),
270 pending_flat: None,
271 viewport_min: f64::INFINITY,
272 viewport_max: f64::NEG_INFINITY,
273 }
274 }
275
276 fn update_bounds(&mut self, value: f64) {
277 if value.is_finite() {
278 self.viewport_min = self.viewport_min.min(value);
279 self.viewport_max = self.viewport_max.max(value);
280 }
281 }
282
283 fn emit_flat(&mut self, px: f32, value: f64) {
284 match self.pending_flat {
285 Some((_, v)) if v.to_bits() == value.to_bits() => {
287 }
289 Some((start, start_val)) => {
290 let end_val = if start_val.is_finite() && value.is_finite() {
292 value
293 } else {
294 start_val
295 };
296 self.commands.push(AnalogDrawingCommand::Flat {
297 start_px: start,
298 start_val,
299 end_px: px,
300 end_val,
301 });
302 self.pending_flat = Some((px, value));
303 }
304 None => self.pending_flat = Some((px, value)),
305 }
306 }
307
308 fn emit_range(&mut self, px: f32, min: f64, max: f64, entry_val: f64, exit_val: f64) {
309 if let Some((start, start_val)) = self.pending_flat.take() {
312 let end_val = if start_val.is_finite() && entry_val.is_finite() {
313 entry_val
314 } else {
315 start_val
316 };
317 self.commands.push(AnalogDrawingCommand::Flat {
318 start_px: start,
319 start_val,
320 end_px: px,
321 end_val,
322 });
323 }
324 self.commands.push(AnalogDrawingCommand::Range {
325 px,
326 min_val: min,
327 max_val: max,
328 });
329 self.pending_flat = Some((px + 1.0, exit_val));
331 }
332}
333
334impl<'a> CommandBuilder<'a> {
335 fn new(
336 cache: &'a AnalogSignalCache,
337 viewport: &'a Viewport,
338 range: &'a TimeRange,
339 view_width: f32,
340 analog_settings: AnalogSettings,
341 type_limits: Option<NumericRange>,
342 ) -> Self {
343 let min_valid_pixel = viewport.pixel_from_time(&BigInt::from(0), view_width, range);
344 let max_valid_pixel = viewport.pixel_from_time(&range.end, view_width, range);
345
346 Self {
347 cache,
348 viewport,
349 range,
350 view_width,
351 min_valid_pixel,
352 max_valid_pixel,
353 output: CommandOutput::new(),
354 analog_settings,
355 type_limits,
356 }
357 }
358
359 fn build(mut self) -> AnalogDrawingCommands {
360 let end_px = self.view_width.floor().max(0.0) + 1.0;
361
362 let before_px = self.add_before_viewport_sample();
363 self.iterate_pixels(0.0, end_px);
364 self.add_after_viewport_sample(end_px);
365
366 self.finalize(before_px)
367 }
368
369 fn time_at_pixel(&self, px: f64) -> u64 {
370 self.viewport
371 .as_absolute_time(px, self.view_width, self.range)
372 .0
373 .to_u64()
374 .unwrap_or(0)
375 }
376
377 fn pixel_at_time(&self, time: u64) -> f32 {
378 self.viewport
379 .pixel_from_time(&BigInt::from(time), self.view_width, self.range)
380 }
381
382 fn query(&self, time: u64) -> CacheQueryResult {
383 self.cache.query_at_time(time)
384 }
385
386 fn add_before_viewport_sample(&mut self) -> Option<f32> {
390 let query = self.query(self.time_at_pixel(0.0));
391
392 if let Some((time, value)) = query.current {
393 let px = self.pixel_at_time(time);
394 if px < 0.0 {
395 self.output.update_bounds(value);
396 self.output.pending_flat = Some((px, value));
397 return Some(px);
398 }
399 }
400 None
401 }
402
403 fn iterate_pixels(&mut self, start_px: f32, end_px: f32) {
404 let mut px = start_px as u32;
405 let end = end_px as u32;
406 let mut next_query_time: Option<u64> = None;
407 let mut last_queried_time: Option<u64> = None;
408
409 while px < end {
410 let jumped_to_transition = next_query_time.is_some();
412 let t0 = next_query_time.unwrap_or_else(|| self.time_at_pixel(f64::from(px)));
413 let t1 = self.time_at_pixel(f64::from(px) + 1.0);
414 next_query_time = None;
415
416 if !jumped_to_transition && last_queried_time == Some(t0) {
420 px += 1;
421 continue;
422 }
423
424 let query = self.query(t0);
425 last_queried_time = Some(t0);
426 let next_change = query.next;
427 let is_flat = next_change.is_none_or(|nc| nc >= t1);
428
429 if is_flat {
430 px = self.process_flat(px, end, &query, next_change, &mut next_query_time);
431 } else {
432 self.process_range(px, t0, t1);
433 px += 1;
434 }
435 }
436 }
437
438 fn process_flat(
439 &mut self,
440 px: u32,
441 end: u32,
442 query: &CacheQueryResult,
443 next_change: Option<u64>,
444 next_query_time: &mut Option<u64>,
445 ) -> u32 {
446 if let Some((_, value)) = query.current {
447 self.output.update_bounds(value);
448 self.output.emit_flat(px as f32, value);
449 }
450
451 if let Some(next) = next_change {
453 let next_px = self.pixel_at_time(next);
454 if next_px.is_finite() {
455 let jump = next_px.floor().max(0.0) as u32;
456 if jump > px {
457 *next_query_time = Some(next);
458 return jump.min(end);
459 }
460 }
461 (px + 1).min(end)
462 } else {
463 end
464 }
465 }
466
467 fn process_range(&mut self, px: u32, t0: u64, t1: u64) {
468 if let Some((min, max)) = self.cache.query_time_range(t0, t1.saturating_sub(1)) {
469 self.output.update_bounds(min);
470 self.output.update_bounds(max);
471
472 let t0_query = self.query(t0);
475 let entry_val = match t0_query.current {
476 Some((time, value)) if time == t0 => value,
479 _ => {
481 if let Some(first_change) = t0_query.next {
482 self.query(first_change).current.map_or(min, |(_, v)| v)
483 } else {
484 min
485 }
486 }
487 };
488
489 let exit_query = self.query(t1.saturating_sub(1));
491 let exit_val = exit_query.current.map_or(max, |(_, v)| v);
492
493 self.output
494 .emit_range(px as f32, min, max, entry_val, exit_val);
495 }
496 }
497
498 fn add_after_viewport_sample(&mut self, end_px: f32) {
500 let query = self.query(self.time_at_pixel(f64::from(end_px)));
501
502 let Some(next_time) = query.next else {
503 return;
504 };
505
506 let after_px = self.pixel_at_time(next_time);
507 if after_px <= end_px {
508 return;
509 }
510
511 let after_query = self.query(next_time);
512
513 if let Some((_, value)) = after_query.current {
514 self.output.update_bounds(value);
515
516 if let Some((start, start_val)) = self.output.pending_flat.take() {
517 self.output.commands.push(AnalogDrawingCommand::Flat {
518 start_px: start,
519 start_val,
520 end_px: after_px,
521 end_val: value,
522 });
523 }
524 }
525 }
526
527 fn finalize(mut self, before_px: Option<f32>) -> AnalogDrawingCommands {
528 if let Some((start, start_val)) = self.output.pending_flat.take() {
530 self.output.commands.push(AnalogDrawingCommand::Flat {
531 start_px: start,
532 start_val,
533 end_px: self.max_valid_pixel,
534 end_val: start_val, });
536 }
537
538 if let Some(before) = before_px
540 && let Some(AnalogDrawingCommand::Flat { start_px, .. }) =
541 self.output.commands.first_mut()
542 {
543 *start_px = (*start_px).min(before);
544 }
545
546 AnalogDrawingCommands::Ready {
547 viewport_min: self.output.viewport_min,
548 viewport_max: self.output.viewport_max,
549 global_min: self.cache.global_min,
550 global_max: self.cache.global_max,
551 type_limits: self.type_limits,
552 values: self.output.commands,
553 min_valid_pixel: self.min_valid_pixel,
554 max_valid_pixel: self.max_valid_pixel,
555 analog_settings: self.analog_settings,
556 }
557 }
558}
559
560pub trait RenderStrategy {
562 fn reset_state(&mut self);
564
565 fn last_point(&self) -> Option<Pos2>;
567
568 fn set_last_point(&mut self, point: Pos2);
570
571 fn render_flat(
575 &mut self,
576 ctx: &mut DrawingContext,
577 render_ctx: &RenderContext,
578 start_px: f32,
579 start_val: f64,
580 end_px: f32,
581 end_val: f64,
582 );
583
584 fn render_range(
587 &mut self,
588 ctx: &mut DrawingContext,
589 render_ctx: &RenderContext,
590 px: f32,
591 min_val: f64,
592 max_val: f64,
593 ) {
594 if !min_val.is_finite() || !max_val.is_finite() {
595 let nan = if min_val.is_finite() {
596 max_val
597 } else {
598 min_val
599 };
600 render_ctx.draw_undefined(px, px + 1.0, nan, ctx);
601 self.reset_state();
602 return;
603 }
604
605 let p_min = render_ctx.to_screen(px, min_val, ctx);
606 let p_max = render_ctx.to_screen(px, max_val, ctx);
607
608 let (connect, other) = match self.last_point() {
610 Some(prev) if (prev.y - p_min.y).abs() < (prev.y - p_max.y).abs() => (p_min, p_max),
611 _ => (p_max, p_min),
612 };
613
614 if let Some(prev) = self.last_point() {
615 render_ctx.draw_line(prev, connect, ctx);
616 }
617
618 render_ctx.draw_line(connect, other, ctx);
620 self.set_last_point(other);
621 }
622}
623
624pub struct RenderContext {
627 pub stroke: Stroke,
628 pub min_val: f64,
629 pub max_val: f64,
630 pub min_valid_pixel: f32,
632 pub max_valid_pixel: f32,
634 pub offset: f32,
635 pub height_scale: f32,
636 pub line_height: f32,
637 pub brightness_shift: Option<f32>,
638}
639
640impl RenderContext {
641 #[allow(clippy::too_many_arguments)]
642 fn new(
643 color: Color32,
644 min_val: f64,
645 max_val: f64,
646 min_valid_pixel: f32,
647 max_valid_pixel: f32,
648 offset: f32,
649 height_scale: f32,
650 brightness_shift: Option<f32>,
651 ctx: &DrawingContext,
652 ) -> Self {
653 Self {
654 stroke: Stroke::new(ctx.theme.linewidth, color),
655 min_val,
656 max_val,
657 min_valid_pixel,
658 max_valid_pixel,
659 offset,
660 height_scale,
661 line_height: ctx.cfg.line_height,
662 brightness_shift,
663 }
664 }
665
666 #[must_use]
669 pub fn normalize(&self, value: f64) -> f32 {
670 debug_assert!(
671 self.min_val.is_finite() && self.max_val.is_finite(),
672 "RenderContext min_val and max_val must be finite"
673 );
674 let range = self.max_val - self.min_val;
675 if range.abs() <= f64::EPSILON {
676 0.5
677 } else {
678 ((value - self.min_val) / range) as f32
679 }
680 }
681
682 #[must_use]
684 pub fn to_screen(&self, x: f32, y: f64, ctx: &DrawingContext) -> Pos2 {
685 let y_norm = self.normalize(y);
686 (ctx.to_screen)(
687 x,
688 (1.0 - y_norm) * self.line_height * self.height_scale + self.offset,
689 )
690 }
691
692 #[must_use]
694 pub fn clamp_x(&self, x: f32) -> f32 {
695 x.clamp(self.min_valid_pixel, self.max_valid_pixel)
696 }
697
698 pub fn draw_line(&self, from: Pos2, to: Pos2, ctx: &mut DrawingContext) {
699 ctx.painter
700 .add(PathShape::line(vec![from, to], self.stroke));
701 }
702
703 pub fn draw_undefined(&self, start_x: f32, end_x: f32, value: f64, ctx: &mut DrawingContext) {
704 let color = if value == f64::INFINITY {
705 ctx.theme.accent_error.background
706 } else if value == f64::NEG_INFINITY {
707 ctx.theme.variable_dontcare
708 } else if is_nan_highimp(value) {
709 ctx.theme.variable_highimp
710 } else {
711 ctx.theme.variable_undef
712 };
713 let color = crate::drawing_canvas::apply_brightness_shift(
714 color,
715 self.brightness_shift,
716 ctx.theme.canvas_colors.background,
717 );
718 let min = (ctx.to_screen)(start_x, self.offset);
719 let max = (ctx.to_screen)(end_x, self.offset + self.line_height * self.height_scale);
720 ctx.painter
721 .rect_filled(Rect::from_min_max(min, max), CornerRadius::ZERO, color);
722 }
723}
724
725#[derive(Default)]
727pub struct StepStrategy {
728 last_point: Option<Pos2>,
729}
730
731impl RenderStrategy for StepStrategy {
732 fn reset_state(&mut self) {
733 self.last_point = None;
734 }
735
736 fn last_point(&self) -> Option<Pos2> {
737 self.last_point
738 }
739
740 fn set_last_point(&mut self, point: Pos2) {
741 self.last_point = Some(point);
742 }
743
744 fn render_flat(
745 &mut self,
746 ctx: &mut DrawingContext,
747 render_ctx: &RenderContext,
748 start_px: f32,
749 start_val: f64,
750 end_px: f32,
751 _end_val: f64, ) {
753 let start_px = render_ctx.clamp_x(start_px);
754 let end_px = render_ctx.clamp_x(end_px);
755
756 if !start_val.is_finite() {
757 render_ctx.draw_undefined(start_px, end_px, start_val, ctx);
758 self.reset_state();
759 return;
760 }
761
762 let p1 = render_ctx.to_screen(start_px, start_val, ctx);
763 let p2 = render_ctx.to_screen(end_px, start_val, ctx);
764
765 if let Some(prev) = self.last_point {
767 render_ctx.draw_line(Pos2::new(p1.x, prev.y), p1, ctx);
768 }
769
770 render_ctx.draw_line(p1, p2, ctx);
772 self.last_point = Some(p2);
773 }
774}
775
776#[derive(Default)]
778pub struct InterpolatedStrategy {
779 last_point: Option<Pos2>,
780 started: bool,
781}
782
783impl RenderStrategy for InterpolatedStrategy {
784 fn reset_state(&mut self) {
785 self.last_point = None;
786 self.started = true;
787 }
788
789 fn last_point(&self) -> Option<Pos2> {
790 self.last_point
791 }
792
793 fn set_last_point(&mut self, point: Pos2) {
794 self.last_point = Some(point);
795 self.started = true;
796 }
797
798 fn render_flat(
799 &mut self,
800 ctx: &mut DrawingContext,
801 render_ctx: &RenderContext,
802 start_px: f32,
803 start_val: f64,
804 end_px: f32,
805 end_val: f64,
806 ) {
807 let start_px = render_ctx.clamp_x(start_px);
808 let end_px = render_ctx.clamp_x(end_px);
809
810 if !start_val.is_finite() {
811 render_ctx.draw_undefined(start_px, end_px, start_val, ctx);
812 self.reset_state();
813 return;
814 }
815
816 let end_val = if end_val.is_finite() {
818 end_val
819 } else {
820 start_val
821 };
822
823 let p1 = render_ctx.to_screen(start_px, start_val, ctx);
824 let p2 = render_ctx.to_screen(end_px, end_val, ctx);
825
826 if let Some(prev) = self.last_point {
828 render_ctx.draw_line(prev, p1, ctx);
829 } else if !self.started {
830 let edge = render_ctx.to_screen(render_ctx.min_valid_pixel.max(0.0), start_val, ctx);
832 render_ctx.draw_line(edge, p1, ctx);
833 }
834
835 render_ctx.draw_line(p1, p2, ctx);
836 self.last_point = Some(p2);
837 self.started = true;
838 }
839}
840
841fn render_with_strategy<S: RenderStrategy>(
843 commands: &[AnalogDrawingCommand],
844 render_ctx: &RenderContext,
845 strategy: &mut S,
846 ctx: &mut DrawingContext,
847) {
848 for cmd in commands {
849 match cmd {
850 AnalogDrawingCommand::Flat {
851 start_px,
852 start_val,
853 end_px,
854 end_val,
855 } => {
856 strategy.render_flat(ctx, render_ctx, *start_px, *start_val, *end_px, *end_val);
857 }
858 AnalogDrawingCommand::Range {
859 px,
860 min_val,
861 max_val,
862 } => {
863 strategy.render_range(ctx, render_ctx, *px, *min_val, *max_val);
864 }
865 }
866 }
867}
868
869fn format_amplitude_value(value: f64) -> String {
871 const SCIENTIFIC_THRESHOLD_HIGH: f64 = 1e4;
872 const SCIENTIFIC_THRESHOLD_LOW: f64 = 1e-3;
873 let abs_val = value.abs();
874 if abs_val == 0.0 {
875 "0.00".to_string()
876 } else if !(SCIENTIFIC_THRESHOLD_LOW..SCIENTIFIC_THRESHOLD_HIGH).contains(&abs_val) {
877 format!("{value:.2e}")
878 } else {
879 format!("{value:.2}")
880 }
881}
882
883fn draw_amplitude_labels(render_ctx: &RenderContext, ctx: &mut DrawingContext) {
884 const SPLIT_LABEL_HEIGHT_THRESHOLD: f32 = 2.0;
885 const BACKGROUND_ALPHA: u8 = 200;
886
887 let canvas_bg = ctx.theme.canvas_colors.background;
888 let text_color = ctx.theme.canvas_colors.foreground;
889 let bg_color = Color32::from_rgba_unmultiplied(
890 canvas_bg.r(),
891 canvas_bg.g(),
892 canvas_bg.b(),
893 BACKGROUND_ALPHA,
894 );
895 let font = egui::FontId::monospace(ctx.cfg.text_size);
896
897 if render_ctx.height_scale < SPLIT_LABEL_HEIGHT_THRESHOLD {
898 let combined_text = format!(
899 "[{}, {}]",
900 format_amplitude_value(render_ctx.min_val),
901 format_amplitude_value(render_ctx.max_val)
902 );
903 let galley = ctx
904 .painter
905 .layout_no_wrap(combined_text.clone(), font.clone(), text_color);
906
907 let label_x = ctx.cfg.canvas_size.x - galley.size().x - 5.0;
908 let label_pos = render_ctx.to_screen(
909 label_x,
910 f64::midpoint(render_ctx.min_val, render_ctx.max_val),
911 ctx,
912 );
913
914 let rect = Rect::from_min_size(
915 Pos2::new(label_pos.x - 2.0, label_pos.y - galley.size().y * 0.5 - 2.0),
916 Vec2::new(galley.size().x + 4.0, galley.size().y + 4.0),
917 );
918 ctx.painter
919 .rect_filled(rect, CornerRadius::same(2), bg_color);
920 ctx.painter.text(
921 Pos2::new(label_pos.x, label_pos.y - galley.size().y * 0.5),
922 Align2::LEFT_TOP,
923 combined_text,
924 font,
925 text_color,
926 );
927 } else {
928 let max_text = format_amplitude_value(render_ctx.max_val);
929 let min_text = format_amplitude_value(render_ctx.min_val);
930
931 let max_galley = ctx
932 .painter
933 .layout_no_wrap(max_text.clone(), font.clone(), text_color);
934 let min_galley = ctx
935 .painter
936 .layout_no_wrap(min_text.clone(), font.clone(), text_color);
937
938 let label_x = ctx.cfg.canvas_size.x - max_galley.size().x.max(min_galley.size().x) - 5.0;
939
940 let max_pos = render_ctx.to_screen(label_x, render_ctx.max_val, ctx);
941 let max_rect = Rect::from_min_size(
942 Pos2::new(max_pos.x - 2.0, max_pos.y - 2.0),
943 Vec2::new(max_galley.size().x + 4.0, max_galley.size().y + 4.0),
944 );
945 ctx.painter
946 .rect_filled(max_rect, CornerRadius::same(2), bg_color);
947 ctx.painter.text(
948 max_pos,
949 Align2::LEFT_TOP,
950 max_text,
951 font.clone(),
952 text_color,
953 );
954
955 let min_pos = render_ctx.to_screen(label_x, render_ctx.min_val, ctx);
956 let min_rect = Rect::from_min_size(
957 Pos2::new(min_pos.x - 2.0, min_pos.y - min_galley.size().y - 2.0),
958 Vec2::new(min_galley.size().x + 4.0, min_galley.size().y + 4.0),
959 );
960 ctx.painter
961 .rect_filled(min_rect, CornerRadius::same(2), bg_color);
962 ctx.painter
963 .text(min_pos, Align2::LEFT_BOTTOM, min_text, font, text_color);
964 }
965}