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