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 num_timestamps = waves.safe_num_timestamps();
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 &num_timestamps,
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 ctx: &mut DrawingContext,
136) {
137 let AnalogDrawingCommands::Ready {
138 viewport_min,
139 viewport_max,
140 global_min,
141 global_max,
142 type_limits,
143 values,
144 min_valid_pixel,
145 max_valid_pixel,
146 analog_settings,
147 } = analog_commands
148 else {
149 draw_building_indicator(offset, height_scaling_factor, ctx);
150 return;
151 };
152
153 let (min_val, max_val) = select_value_range(
154 *viewport_min,
155 *viewport_max,
156 *global_min,
157 *global_max,
158 *type_limits,
159 analog_settings,
160 );
161
162 let render_ctx = RenderContext::new(
163 color,
164 min_val,
165 max_val,
166 *min_valid_pixel,
167 *max_valid_pixel,
168 offset,
169 height_scaling_factor,
170 ctx,
171 );
172
173 match analog_settings.render_style {
175 crate::displayed_item::AnalogRenderStyle::Step => {
176 let mut strategy = StepStrategy::default();
177 render_with_strategy(values, &render_ctx, &mut strategy, ctx);
178 }
179 crate::displayed_item::AnalogRenderStyle::Interpolated => {
180 let mut strategy = InterpolatedStrategy::default();
181 render_with_strategy(values, &render_ctx, &mut strategy, ctx);
182 }
183 }
184
185 draw_amplitude_labels(&render_ctx, ctx);
186}
187
188fn draw_building_indicator(offset: f32, height_scaling_factor: f32, ctx: &mut DrawingContext) {
190 let elapsed = ctx.painter.ctx().input(|i| i.time);
192 let dot_index = (elapsed / 0.333) as usize % 3;
193 let text = ["Building. ", "Building.. ", "Building..."][dot_index];
194
195 let row_height = ctx.cfg.line_height * height_scaling_factor;
196 let center_y = offset + row_height / 2.0;
197 let center_x = ctx.cfg.canvas_size.x / 2.0;
198 let pos = (ctx.to_screen)(center_x, center_y);
199
200 ctx.painter.text(
201 pos,
202 Align2::CENTER_CENTER,
203 text,
204 egui::FontId::monospace(ctx.cfg.text_size),
205 ctx.theme.foreground.gamma_multiply(0.6),
206 );
207}
208
209fn select_value_range(
210 viewport_min: f64,
211 viewport_max: f64,
212 global_min: f64,
213 global_max: f64,
214 type_limits: Option<NumericRange>,
215 settings: &AnalogSettings,
216) -> (f64, f64) {
217 let (min, max) = match settings.y_axis_scale {
218 crate::displayed_item::AnalogYAxisScale::Viewport => (viewport_min, viewport_max),
219 crate::displayed_item::AnalogYAxisScale::Global => (global_min, global_max),
220 crate::displayed_item::AnalogYAxisScale::TypeLimits => {
221 type_limits.map_or((global_min, global_max), |r| (r.min, r.max))
222 }
223 };
224
225 if !min.is_finite() || !max.is_finite() || min > max {
227 return (-0.5, 0.5);
228 }
229
230 if (max - min).abs() < f64::EPSILON {
232 (min - 0.5, max + 0.5)
233 } else {
234 (min, max)
235 }
236}
237
238struct CommandBuilder<'a> {
240 cache: &'a AnalogSignalCache,
241 viewport: &'a Viewport,
242 num_timestamps: &'a BigInt,
243 view_width: f32,
244 min_valid_pixel: f32,
245 max_valid_pixel: f32,
246 output: CommandOutput,
247 analog_settings: AnalogSettings,
248 type_limits: Option<NumericRange>,
249}
250
251struct CommandOutput {
253 commands: Vec<AnalogDrawingCommand>,
254 pending_flat: Option<(f32, f64)>,
255 viewport_min: f64,
256 viewport_max: f64,
257}
258
259impl CommandOutput {
260 fn new() -> Self {
261 Self {
262 commands: Vec::new(),
263 pending_flat: None,
264 viewport_min: f64::INFINITY,
265 viewport_max: f64::NEG_INFINITY,
266 }
267 }
268
269 fn update_bounds(&mut self, value: f64) {
270 if value.is_finite() {
271 self.viewport_min = self.viewport_min.min(value);
272 self.viewport_max = self.viewport_max.max(value);
273 }
274 }
275
276 fn emit_flat(&mut self, px: f32, value: f64) {
277 match self.pending_flat {
278 Some((_, v)) if v.to_bits() == value.to_bits() => {
280 }
282 Some((start, start_val)) => {
283 let end_val = if start_val.is_finite() && value.is_finite() {
285 value
286 } else {
287 start_val
288 };
289 self.commands.push(AnalogDrawingCommand::Flat {
290 start_px: start,
291 start_val,
292 end_px: px,
293 end_val,
294 });
295 self.pending_flat = Some((px, value));
296 }
297 None => self.pending_flat = Some((px, value)),
298 }
299 }
300
301 fn emit_range(&mut self, px: f32, min: f64, max: f64, entry_val: f64, exit_val: f64) {
302 if let Some((start, start_val)) = self.pending_flat.take() {
305 let end_val = if start_val.is_finite() && entry_val.is_finite() {
306 entry_val
307 } else {
308 start_val
309 };
310 self.commands.push(AnalogDrawingCommand::Flat {
311 start_px: start,
312 start_val,
313 end_px: px,
314 end_val,
315 });
316 }
317 self.commands.push(AnalogDrawingCommand::Range {
318 px,
319 min_val: min,
320 max_val: max,
321 });
322 self.pending_flat = Some((px + 1.0, exit_val));
324 }
325}
326
327impl<'a> CommandBuilder<'a> {
328 fn new(
329 cache: &'a AnalogSignalCache,
330 viewport: &'a Viewport,
331 num_timestamps: &'a BigInt,
332 view_width: f32,
333 analog_settings: AnalogSettings,
334 type_limits: Option<NumericRange>,
335 ) -> Self {
336 let min_valid_pixel =
337 viewport.pixel_from_time(&BigInt::from(0), view_width, num_timestamps);
338 let max_valid_pixel = viewport.pixel_from_time(num_timestamps, view_width, num_timestamps);
339
340 Self {
341 cache,
342 viewport,
343 num_timestamps,
344 view_width,
345 min_valid_pixel,
346 max_valid_pixel,
347 output: CommandOutput::new(),
348 analog_settings,
349 type_limits,
350 }
351 }
352
353 fn build(mut self) -> AnalogDrawingCommands {
354 let end_px = self.view_width.floor().max(0.0) + 1.0;
355
356 let before_px = self.add_before_viewport_sample();
357 self.iterate_pixels(0.0, end_px);
358 self.add_after_viewport_sample(end_px);
359
360 self.finalize(before_px)
361 }
362
363 fn time_at_pixel(&self, px: f64) -> u64 {
364 self.viewport
365 .as_absolute_time(px, self.view_width, self.num_timestamps)
366 .0
367 .to_u64()
368 .unwrap_or(0)
369 }
370
371 fn pixel_at_time(&self, time: u64) -> f32 {
372 self.viewport
373 .pixel_from_time(&BigInt::from(time), self.view_width, self.num_timestamps)
374 }
375
376 fn query(&self, time: u64) -> CacheQueryResult {
377 self.cache.query_at_time(time)
378 }
379
380 fn add_before_viewport_sample(&mut self) -> Option<f32> {
384 let query = self.query(self.time_at_pixel(0.0));
385
386 if let Some((time, value)) = query.current {
387 let px = self.pixel_at_time(time);
388 if px < 0.0 {
389 self.output.update_bounds(value);
390 self.output.pending_flat = Some((px, value));
391 return Some(px);
392 }
393 }
394 None
395 }
396
397 fn iterate_pixels(&mut self, start_px: f32, end_px: f32) {
398 let mut px = start_px as u32;
399 let end = end_px as u32;
400 let mut next_query_time: Option<u64> = None;
401 let mut last_queried_time: Option<u64> = None;
402
403 while px < end {
404 let jumped_to_transition = next_query_time.is_some();
406 let t0 = next_query_time.unwrap_or_else(|| self.time_at_pixel(f64::from(px)));
407 let t1 = self.time_at_pixel(f64::from(px) + 1.0);
408 next_query_time = None;
409
410 if !jumped_to_transition && last_queried_time == Some(t0) {
414 px += 1;
415 continue;
416 }
417
418 let query = self.query(t0);
419 last_queried_time = Some(t0);
420 let next_change = query.next;
421 let is_flat = next_change.is_none_or(|nc| nc >= t1);
422
423 if is_flat {
424 px = self.process_flat(px, end, &query, next_change, &mut next_query_time);
425 } else {
426 self.process_range(px, t0, t1);
427 px += 1;
428 }
429 }
430 }
431
432 fn process_flat(
433 &mut self,
434 px: u32,
435 end: u32,
436 query: &CacheQueryResult,
437 next_change: Option<u64>,
438 next_query_time: &mut Option<u64>,
439 ) -> u32 {
440 if let Some((_, value)) = query.current {
441 self.output.update_bounds(value);
442 self.output.emit_flat(px as f32, value);
443 }
444
445 if let Some(next) = next_change {
447 let next_px = self.pixel_at_time(next);
448 if next_px.is_finite() {
449 let jump = next_px.floor().max(0.0) as u32;
450 if jump > px {
451 *next_query_time = Some(next);
452 return jump.min(end);
453 }
454 }
455 (px + 1).min(end)
456 } else {
457 end
458 }
459 }
460
461 fn process_range(&mut self, px: u32, t0: u64, t1: u64) {
462 if let Some((min, max)) = self.cache.query_time_range(t0, t1.saturating_sub(1)) {
463 self.output.update_bounds(min);
464 self.output.update_bounds(max);
465
466 let t0_query = self.query(t0);
469 let entry_val = match t0_query.current {
470 Some((time, value)) if time == t0 => value,
473 _ => {
475 if let Some(first_change) = t0_query.next {
476 self.query(first_change).current.map_or(min, |(_, v)| v)
477 } else {
478 min
479 }
480 }
481 };
482
483 let exit_query = self.query(t1.saturating_sub(1));
485 let exit_val = exit_query.current.map_or(max, |(_, v)| v);
486
487 self.output
488 .emit_range(px as f32, min, max, entry_val, exit_val);
489 }
490 }
491
492 fn add_after_viewport_sample(&mut self, end_px: f32) {
494 let query = self.query(self.time_at_pixel(f64::from(end_px)));
495
496 let Some(next_time) = query.next else {
497 return;
498 };
499
500 let after_px = self.pixel_at_time(next_time);
501 if after_px <= end_px {
502 return;
503 }
504
505 let after_query = self.query(next_time);
506
507 if let Some((_, value)) = after_query.current {
508 self.output.update_bounds(value);
509
510 if let Some((start, start_val)) = self.output.pending_flat.take() {
511 self.output.commands.push(AnalogDrawingCommand::Flat {
512 start_px: start,
513 start_val,
514 end_px: after_px,
515 end_val: value,
516 });
517 }
518 }
519 }
520
521 fn finalize(mut self, before_px: Option<f32>) -> AnalogDrawingCommands {
522 if let Some((start, start_val)) = self.output.pending_flat.take() {
524 self.output.commands.push(AnalogDrawingCommand::Flat {
525 start_px: start,
526 start_val,
527 end_px: self.max_valid_pixel,
528 end_val: start_val, });
530 }
531
532 if let Some(before) = before_px
534 && let Some(AnalogDrawingCommand::Flat { start_px, .. }) =
535 self.output.commands.first_mut()
536 {
537 *start_px = (*start_px).min(before);
538 }
539
540 AnalogDrawingCommands::Ready {
541 viewport_min: self.output.viewport_min,
542 viewport_max: self.output.viewport_max,
543 global_min: self.cache.global_min,
544 global_max: self.cache.global_max,
545 type_limits: self.type_limits,
546 values: self.output.commands,
547 min_valid_pixel: self.min_valid_pixel,
548 max_valid_pixel: self.max_valid_pixel,
549 analog_settings: self.analog_settings,
550 }
551 }
552}
553
554pub trait RenderStrategy {
556 fn reset_state(&mut self);
558
559 fn last_point(&self) -> Option<Pos2>;
561
562 fn set_last_point(&mut self, point: Pos2);
564
565 fn render_flat(
569 &mut self,
570 ctx: &mut DrawingContext,
571 render_ctx: &RenderContext,
572 start_px: f32,
573 start_val: f64,
574 end_px: f32,
575 end_val: f64,
576 );
577
578 fn render_range(
581 &mut self,
582 ctx: &mut DrawingContext,
583 render_ctx: &RenderContext,
584 px: f32,
585 min_val: f64,
586 max_val: f64,
587 ) {
588 if !min_val.is_finite() || !max_val.is_finite() {
589 let nan = if min_val.is_finite() {
590 max_val
591 } else {
592 min_val
593 };
594 render_ctx.draw_undefined(px, px + 1.0, nan, ctx);
595 self.reset_state();
596 return;
597 }
598
599 let p_min = render_ctx.to_screen(px, min_val, ctx);
600 let p_max = render_ctx.to_screen(px, max_val, ctx);
601
602 let (connect, other) = match self.last_point() {
604 Some(prev) if (prev.y - p_min.y).abs() < (prev.y - p_max.y).abs() => (p_min, p_max),
605 _ => (p_max, p_min),
606 };
607
608 if let Some(prev) = self.last_point() {
609 render_ctx.draw_line(prev, connect, ctx);
610 }
611
612 render_ctx.draw_line(connect, other, ctx);
614 self.set_last_point(other);
615 }
616}
617
618pub struct RenderContext {
621 pub stroke: Stroke,
622 pub min_val: f64,
623 pub max_val: f64,
624 pub min_valid_pixel: f32,
626 pub max_valid_pixel: f32,
628 pub offset: f32,
629 pub height_scale: f32,
630 pub line_height: f32,
631}
632
633impl RenderContext {
634 #[allow(clippy::too_many_arguments)]
635 fn new(
636 color: Color32,
637 min_val: f64,
638 max_val: f64,
639 min_valid_pixel: f32,
640 max_valid_pixel: f32,
641 offset: f32,
642 height_scale: f32,
643 ctx: &DrawingContext,
644 ) -> Self {
645 Self {
646 stroke: Stroke::new(ctx.theme.linewidth, color),
647 min_val,
648 max_val,
649 min_valid_pixel,
650 max_valid_pixel,
651 offset,
652 height_scale,
653 line_height: ctx.cfg.line_height,
654 }
655 }
656
657 #[must_use]
660 pub fn normalize(&self, value: f64) -> f32 {
661 debug_assert!(
662 self.min_val.is_finite() && self.max_val.is_finite(),
663 "RenderContext min_val and max_val must be finite"
664 );
665 let range = self.max_val - self.min_val;
666 if range.abs() <= f64::EPSILON {
667 0.5
668 } else {
669 ((value - self.min_val) / range) as f32
670 }
671 }
672
673 #[must_use]
675 pub fn to_screen(&self, x: f32, y: f64, ctx: &DrawingContext) -> Pos2 {
676 let y_norm = self.normalize(y);
677 (ctx.to_screen)(
678 x,
679 (1.0 - y_norm) * self.line_height * self.height_scale + self.offset,
680 )
681 }
682
683 #[must_use]
685 pub fn clamp_x(&self, x: f32) -> f32 {
686 x.clamp(self.min_valid_pixel, self.max_valid_pixel)
687 }
688
689 pub fn draw_line(&self, from: Pos2, to: Pos2, ctx: &mut DrawingContext) {
690 ctx.painter
691 .add(PathShape::line(vec![from, to], self.stroke));
692 }
693
694 pub fn draw_undefined(&self, start_x: f32, end_x: f32, value: f64, ctx: &mut DrawingContext) {
695 let color = if value == f64::INFINITY {
696 ctx.theme.accent_error.background
697 } else if value == f64::NEG_INFINITY {
698 ctx.theme.variable_dontcare
699 } else if is_nan_highimp(value) {
700 ctx.theme.variable_highimp
701 } else {
702 ctx.theme.variable_undef
703 };
704 let min = (ctx.to_screen)(start_x, self.offset);
705 let max = (ctx.to_screen)(end_x, self.offset + self.line_height * self.height_scale);
706 ctx.painter
707 .rect_filled(Rect::from_min_max(min, max), CornerRadius::ZERO, color);
708 }
709}
710
711#[derive(Default)]
713pub struct StepStrategy {
714 last_point: Option<Pos2>,
715}
716
717impl RenderStrategy for StepStrategy {
718 fn reset_state(&mut self) {
719 self.last_point = None;
720 }
721
722 fn last_point(&self) -> Option<Pos2> {
723 self.last_point
724 }
725
726 fn set_last_point(&mut self, point: Pos2) {
727 self.last_point = Some(point);
728 }
729
730 fn render_flat(
731 &mut self,
732 ctx: &mut DrawingContext,
733 render_ctx: &RenderContext,
734 start_px: f32,
735 start_val: f64,
736 end_px: f32,
737 _end_val: f64, ) {
739 let start_px = render_ctx.clamp_x(start_px);
740 let end_px = render_ctx.clamp_x(end_px);
741
742 if !start_val.is_finite() {
743 render_ctx.draw_undefined(start_px, end_px, start_val, ctx);
744 self.reset_state();
745 return;
746 }
747
748 let p1 = render_ctx.to_screen(start_px, start_val, ctx);
749 let p2 = render_ctx.to_screen(end_px, start_val, ctx);
750
751 if let Some(prev) = self.last_point {
753 render_ctx.draw_line(Pos2::new(p1.x, prev.y), p1, ctx);
754 }
755
756 render_ctx.draw_line(p1, p2, ctx);
758 self.last_point = Some(p2);
759 }
760}
761
762#[derive(Default)]
764pub struct InterpolatedStrategy {
765 last_point: Option<Pos2>,
766 started: bool,
767}
768
769impl RenderStrategy for InterpolatedStrategy {
770 fn reset_state(&mut self) {
771 self.last_point = None;
772 self.started = true;
773 }
774
775 fn last_point(&self) -> Option<Pos2> {
776 self.last_point
777 }
778
779 fn set_last_point(&mut self, point: Pos2) {
780 self.last_point = Some(point);
781 self.started = true;
782 }
783
784 fn render_flat(
785 &mut self,
786 ctx: &mut DrawingContext,
787 render_ctx: &RenderContext,
788 start_px: f32,
789 start_val: f64,
790 end_px: f32,
791 end_val: f64,
792 ) {
793 let start_px = render_ctx.clamp_x(start_px);
794 let end_px = render_ctx.clamp_x(end_px);
795
796 if !start_val.is_finite() {
797 render_ctx.draw_undefined(start_px, end_px, start_val, ctx);
798 self.reset_state();
799 return;
800 }
801
802 let end_val = if end_val.is_finite() {
804 end_val
805 } else {
806 start_val
807 };
808
809 let p1 = render_ctx.to_screen(start_px, start_val, ctx);
810 let p2 = render_ctx.to_screen(end_px, end_val, ctx);
811
812 if let Some(prev) = self.last_point {
814 render_ctx.draw_line(prev, p1, ctx);
815 } else if !self.started {
816 let edge = render_ctx.to_screen(render_ctx.min_valid_pixel.max(0.0), start_val, ctx);
818 render_ctx.draw_line(edge, p1, ctx);
819 }
820
821 render_ctx.draw_line(p1, p2, ctx);
822 self.last_point = Some(p2);
823 self.started = true;
824 }
825}
826
827fn render_with_strategy<S: RenderStrategy>(
829 commands: &[AnalogDrawingCommand],
830 render_ctx: &RenderContext,
831 strategy: &mut S,
832 ctx: &mut DrawingContext,
833) {
834 for cmd in commands {
835 match cmd {
836 AnalogDrawingCommand::Flat {
837 start_px,
838 start_val,
839 end_px,
840 end_val,
841 } => {
842 strategy.render_flat(ctx, render_ctx, *start_px, *start_val, *end_px, *end_val);
843 }
844 AnalogDrawingCommand::Range {
845 px,
846 min_val,
847 max_val,
848 } => {
849 strategy.render_range(ctx, render_ctx, *px, *min_val, *max_val);
850 }
851 }
852 }
853}
854
855fn format_amplitude_value(value: f64) -> String {
857 const SCIENTIFIC_THRESHOLD_HIGH: f64 = 1e4;
858 const SCIENTIFIC_THRESHOLD_LOW: f64 = 1e-3;
859 let abs_val = value.abs();
860 if abs_val == 0.0 {
861 "0.00".to_string()
862 } else if !(SCIENTIFIC_THRESHOLD_LOW..SCIENTIFIC_THRESHOLD_HIGH).contains(&abs_val) {
863 format!("{value:.2e}")
864 } else {
865 format!("{value:.2}")
866 }
867}
868
869fn draw_amplitude_labels(render_ctx: &RenderContext, ctx: &mut DrawingContext) {
870 const SPLIT_LABEL_HEIGHT_THRESHOLD: f32 = 2.0;
871 const BACKGROUND_ALPHA: u8 = 200;
872
873 let canvas_bg = ctx.theme.canvas_colors.background;
874 let text_color = ctx.theme.canvas_colors.foreground;
875 let bg_color = Color32::from_rgba_unmultiplied(
876 canvas_bg.r(),
877 canvas_bg.g(),
878 canvas_bg.b(),
879 BACKGROUND_ALPHA,
880 );
881 let font = egui::FontId::monospace(ctx.cfg.text_size);
882
883 if render_ctx.height_scale < SPLIT_LABEL_HEIGHT_THRESHOLD {
884 let combined_text = format!(
885 "[{}, {}]",
886 format_amplitude_value(render_ctx.min_val),
887 format_amplitude_value(render_ctx.max_val)
888 );
889 let galley = ctx
890 .painter
891 .layout_no_wrap(combined_text.clone(), font.clone(), text_color);
892
893 let label_x = ctx.cfg.canvas_size.x - galley.size().x - 5.0;
894 let label_pos = render_ctx.to_screen(
895 label_x,
896 f64::midpoint(render_ctx.min_val, render_ctx.max_val),
897 ctx,
898 );
899
900 let rect = Rect::from_min_size(
901 Pos2::new(label_pos.x - 2.0, label_pos.y - galley.size().y / 2.0 - 2.0),
902 Vec2::new(galley.size().x + 4.0, galley.size().y + 4.0),
903 );
904 ctx.painter
905 .rect_filled(rect, CornerRadius::same(2), bg_color);
906 ctx.painter.text(
907 Pos2::new(label_pos.x, label_pos.y - galley.size().y / 2.0),
908 Align2::LEFT_TOP,
909 combined_text,
910 font,
911 text_color,
912 );
913 } else {
914 let max_text = format_amplitude_value(render_ctx.max_val);
915 let min_text = format_amplitude_value(render_ctx.min_val);
916
917 let max_galley = ctx
918 .painter
919 .layout_no_wrap(max_text.clone(), font.clone(), text_color);
920 let min_galley = ctx
921 .painter
922 .layout_no_wrap(min_text.clone(), font.clone(), text_color);
923
924 let label_x = ctx.cfg.canvas_size.x - max_galley.size().x.max(min_galley.size().x) - 5.0;
925
926 let max_pos = render_ctx.to_screen(label_x, render_ctx.max_val, ctx);
927 let max_rect = Rect::from_min_size(
928 Pos2::new(max_pos.x - 2.0, max_pos.y - 2.0),
929 Vec2::new(max_galley.size().x + 4.0, max_galley.size().y + 4.0),
930 );
931 ctx.painter
932 .rect_filled(max_rect, CornerRadius::same(2), bg_color);
933 ctx.painter.text(
934 max_pos,
935 Align2::LEFT_TOP,
936 max_text,
937 font.clone(),
938 text_color,
939 );
940
941 let min_pos = render_ctx.to_screen(label_x, render_ctx.min_val, ctx);
942 let min_rect = Rect::from_min_size(
943 Pos2::new(min_pos.x - 2.0, min_pos.y - min_galley.size().y - 2.0),
944 Vec2::new(min_galley.size().x + 4.0, min_galley.size().y + 4.0),
945 );
946 ctx.painter
947 .rect_filled(min_rect, CornerRadius::same(2), bg_color);
948 ctx.painter
949 .text(min_pos, Align2::LEFT_BOTTOM, min_text, font, text_color);
950 }
951}