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 clock_edges: vec![],
75 display_id,
76 local_commands,
77 local_msgs: vec![],
78 });
79 }
80 }
81 _ => {
82 let mut local_commands = HashMap::new();
84 local_commands.insert(
85 vec![],
86 DrawingCommands::Analog(AnalogDrawingCommands::Loading),
87 );
88 return Some(VariableDrawCommands {
89 clock_edges: vec![],
90 display_id,
91 local_commands,
92 local_msgs: vec![Message::BuildAnalogCache {
93 display_id,
94 cache_key,
95 }],
96 });
97 }
98 };
99
100 let meta = wave_container
101 .variable_meta(&displayed_variable.variable_ref)
102 .ok();
103 let type_limits = meta.as_ref().and_then(|m| translator.numeric_range(m));
104
105 let analog_commands = CommandBuilder::new(
106 cache,
107 viewport,
108 &num_timestamps,
109 view_width,
110 render_mode.settings,
111 type_limits,
112 )
113 .build();
114
115 let mut local_commands = HashMap::new();
116 local_commands.insert(vec![], DrawingCommands::Analog(analog_commands));
117
118 Some(VariableDrawCommands {
119 clock_edges: vec![],
120 display_id,
121 local_commands,
122 local_msgs: vec![],
123 })
124}
125
126pub fn draw_analog(
128 analog_commands: &AnalogDrawingCommands,
129 color: Color32,
130 offset: f32,
131 height_scaling_factor: f32,
132 ctx: &mut DrawingContext,
133) {
134 let AnalogDrawingCommands::Ready {
135 viewport_min,
136 viewport_max,
137 global_min,
138 global_max,
139 type_limits,
140 values,
141 min_valid_pixel,
142 max_valid_pixel,
143 analog_settings,
144 } = analog_commands
145 else {
146 draw_building_indicator(offset, height_scaling_factor, ctx);
147 return;
148 };
149
150 let (min_val, max_val) = select_value_range(
151 *viewport_min,
152 *viewport_max,
153 *global_min,
154 *global_max,
155 *type_limits,
156 analog_settings,
157 );
158
159 let render_ctx = RenderContext::new(
160 color,
161 min_val,
162 max_val,
163 *min_valid_pixel,
164 *max_valid_pixel,
165 offset,
166 height_scaling_factor,
167 ctx,
168 );
169
170 match analog_settings.render_style {
172 crate::displayed_item::AnalogRenderStyle::Step => {
173 let mut strategy = StepStrategy::default();
174 render_with_strategy(values, &render_ctx, &mut strategy, ctx);
175 }
176 crate::displayed_item::AnalogRenderStyle::Interpolated => {
177 let mut strategy = InterpolatedStrategy::default();
178 render_with_strategy(values, &render_ctx, &mut strategy, ctx);
179 }
180 }
181
182 draw_amplitude_labels(&render_ctx, ctx);
183}
184
185fn draw_building_indicator(offset: f32, height_scaling_factor: f32, ctx: &mut DrawingContext) {
187 let elapsed = ctx.painter.ctx().input(|i| i.time);
189 let dot_index = (elapsed / 0.333) as usize % 3;
190 let text = ["Building. ", "Building.. ", "Building..."][dot_index];
191
192 let text_size = ctx.cfg.text_size;
193 let row_height = ctx.cfg.line_height * height_scaling_factor;
194 let center_y = offset + row_height / 2.0;
195 let center_x = ctx.cfg.canvas_width / 2.0;
196 let pos = (ctx.to_screen)(center_x, center_y);
197
198 ctx.painter.text(
199 pos,
200 Align2::CENTER_CENTER,
201 text,
202 egui::FontId::monospace(text_size),
203 ctx.theme.foreground.gamma_multiply(0.6),
204 );
205}
206
207fn select_value_range(
208 viewport_min: f64,
209 viewport_max: f64,
210 global_min: f64,
211 global_max: f64,
212 type_limits: Option<NumericRange>,
213 settings: &AnalogSettings,
214) -> (f64, f64) {
215 let (min, max) = match settings.y_axis_scale {
216 crate::displayed_item::AnalogYAxisScale::Viewport => (viewport_min, viewport_max),
217 crate::displayed_item::AnalogYAxisScale::Global => (global_min, global_max),
218 crate::displayed_item::AnalogYAxisScale::TypeLimits => {
219 type_limits.map_or((global_min, global_max), |r| (r.min, r.max))
220 }
221 };
222
223 if !min.is_finite() || !max.is_finite() || min > max {
225 return (-0.5, 0.5);
226 }
227
228 if (max - min).abs() < f64::EPSILON {
230 (min - 0.5, max + 0.5)
231 } else {
232 (min, max)
233 }
234}
235
236struct CommandBuilder<'a> {
238 cache: &'a AnalogSignalCache,
239 viewport: &'a Viewport,
240 num_timestamps: &'a BigInt,
241 view_width: f32,
242 min_valid_pixel: f32,
243 max_valid_pixel: f32,
244 output: CommandOutput,
245 analog_settings: AnalogSettings,
246 type_limits: Option<NumericRange>,
247}
248
249struct CommandOutput {
251 commands: Vec<AnalogDrawingCommand>,
252 pending_flat: Option<(f32, f64)>,
253 viewport_min: f64,
254 viewport_max: f64,
255}
256
257impl CommandOutput {
258 fn new() -> Self {
259 Self {
260 commands: Vec::new(),
261 pending_flat: None,
262 viewport_min: f64::INFINITY,
263 viewport_max: f64::NEG_INFINITY,
264 }
265 }
266
267 fn update_bounds(&mut self, value: f64) {
268 if value.is_finite() {
269 self.viewport_min = self.viewport_min.min(value);
270 self.viewport_max = self.viewport_max.max(value);
271 }
272 }
273
274 fn emit_flat(&mut self, px: f32, value: f64) {
275 match self.pending_flat {
276 Some((_, v)) if v.to_bits() == value.to_bits() => {
278 }
280 Some((start, start_val)) => {
281 let end_val = if start_val.is_finite() && value.is_finite() {
283 value
284 } else {
285 start_val
286 };
287 self.commands.push(AnalogDrawingCommand::Flat {
288 start_px: start,
289 start_val,
290 end_px: px,
291 end_val,
292 });
293 self.pending_flat = Some((px, value));
294 }
295 None => self.pending_flat = Some((px, value)),
296 }
297 }
298
299 fn emit_range(&mut self, px: f32, min: f64, max: f64, entry_val: f64, exit_val: f64) {
300 if let Some((start, start_val)) = self.pending_flat.take() {
303 let end_val = if start_val.is_finite() && entry_val.is_finite() {
304 entry_val
305 } else {
306 start_val
307 };
308 self.commands.push(AnalogDrawingCommand::Flat {
309 start_px: start,
310 start_val,
311 end_px: px,
312 end_val,
313 });
314 }
315 self.commands.push(AnalogDrawingCommand::Range {
316 px,
317 min_val: min,
318 max_val: max,
319 });
320 self.pending_flat = Some((px + 1.0, exit_val));
322 }
323}
324
325impl<'a> CommandBuilder<'a> {
326 fn new(
327 cache: &'a AnalogSignalCache,
328 viewport: &'a Viewport,
329 num_timestamps: &'a BigInt,
330 view_width: f32,
331 analog_settings: AnalogSettings,
332 type_limits: Option<NumericRange>,
333 ) -> Self {
334 let min_valid_pixel =
335 viewport.pixel_from_time(&BigInt::from(0), view_width, num_timestamps);
336 let max_valid_pixel = viewport.pixel_from_time(num_timestamps, view_width, num_timestamps);
337
338 Self {
339 cache,
340 viewport,
341 num_timestamps,
342 view_width,
343 min_valid_pixel,
344 max_valid_pixel,
345 output: CommandOutput::new(),
346 analog_settings,
347 type_limits,
348 }
349 }
350
351 fn build(mut self) -> AnalogDrawingCommands {
352 let end_px = self.view_width.floor().max(0.0) + 1.0;
353
354 let before_px = self.add_before_viewport_sample();
355 self.iterate_pixels(0.0, end_px);
356 self.add_after_viewport_sample(end_px);
357
358 self.finalize(before_px)
359 }
360
361 fn time_at_pixel(&self, px: f64) -> u64 {
362 self.viewport
363 .as_absolute_time(px, self.view_width, self.num_timestamps)
364 .0
365 .to_u64()
366 .unwrap_or(0)
367 }
368
369 fn pixel_at_time(&self, time: u64) -> f32 {
370 self.viewport
371 .pixel_from_time(&BigInt::from(time), self.view_width, self.num_timestamps)
372 }
373
374 fn query(&self, time: u64) -> CacheQueryResult {
375 self.cache.query_at_time(time)
376 }
377
378 fn add_before_viewport_sample(&mut self) -> Option<f32> {
382 let query = self.query(self.time_at_pixel(0.0));
383
384 if let Some((time, value)) = query.current {
385 let px = self.pixel_at_time(time);
386 if px < 0.0 {
387 self.output.update_bounds(value);
388 self.output.pending_flat = Some((px, value));
389 return Some(px);
390 }
391 }
392 None
393 }
394
395 fn iterate_pixels(&mut self, start_px: f32, end_px: f32) {
396 let mut px = start_px as u32;
397 let end = end_px as u32;
398 let mut next_query_time: Option<u64> = None;
399 let mut last_queried_time: Option<u64> = None;
400
401 while px < end {
402 let jumped_to_transition = next_query_time.is_some();
404 let t0 = next_query_time.unwrap_or_else(|| self.time_at_pixel(f64::from(px)));
405 let t1 = self.time_at_pixel(f64::from(px) + 1.0);
406 next_query_time = None;
407
408 if !jumped_to_transition && last_queried_time == Some(t0) {
412 px += 1;
413 continue;
414 }
415
416 let query = self.query(t0);
417 last_queried_time = Some(t0);
418 let next_change = query.next;
419 let is_flat = next_change.is_none_or(|nc| nc >= t1);
420
421 if is_flat {
422 px = self.process_flat(px, end, &query, next_change, &mut next_query_time);
423 } else {
424 self.process_range(px, t0, t1);
425 px += 1;
426 }
427 }
428 }
429
430 fn process_flat(
431 &mut self,
432 px: u32,
433 end: u32,
434 query: &CacheQueryResult,
435 next_change: Option<u64>,
436 next_query_time: &mut Option<u64>,
437 ) -> u32 {
438 if let Some((_, value)) = query.current {
439 self.output.update_bounds(value);
440 self.output.emit_flat(px as f32, value);
441 }
442
443 if let Some(next) = next_change {
445 let next_px = self.pixel_at_time(next);
446 if next_px.is_finite() {
447 let jump = next_px.floor().max(0.0) as u32;
448 if jump > px {
449 *next_query_time = Some(next);
450 return jump.min(end);
451 }
452 }
453 (px + 1).min(end)
454 } else {
455 end
456 }
457 }
458
459 fn process_range(&mut self, px: u32, t0: u64, t1: u64) {
460 if let Some((min, max)) = self.cache.query_time_range(t0, t1.saturating_sub(1)) {
461 self.output.update_bounds(min);
462 self.output.update_bounds(max);
463
464 let t0_query = self.query(t0);
467 let entry_val = match t0_query.current {
468 Some((time, value)) if time == t0 => value,
471 _ => {
473 if let Some(first_change) = t0_query.next {
474 self.query(first_change).current.map_or(min, |(_, v)| v)
475 } else {
476 min
477 }
478 }
479 };
480
481 let exit_query = self.query(t1.saturating_sub(1));
483 let exit_val = exit_query.current.map_or(max, |(_, v)| v);
484
485 self.output
486 .emit_range(px as f32, min, max, entry_val, exit_val);
487 }
488 }
489
490 fn add_after_viewport_sample(&mut self, end_px: f32) {
492 let query = self.query(self.time_at_pixel(f64::from(end_px)));
493
494 let Some(next_time) = query.next else {
495 return;
496 };
497
498 let after_px = self.pixel_at_time(next_time);
499 if after_px <= end_px {
500 return;
501 }
502
503 let after_query = self.query(next_time);
504
505 if let Some((_, value)) = after_query.current {
506 self.output.update_bounds(value);
507
508 if let Some((start, start_val)) = self.output.pending_flat.take() {
509 self.output.commands.push(AnalogDrawingCommand::Flat {
510 start_px: start,
511 start_val,
512 end_px: after_px,
513 end_val: value,
514 });
515 }
516 }
517 }
518
519 fn finalize(mut self, before_px: Option<f32>) -> AnalogDrawingCommands {
520 if let Some((start, start_val)) = self.output.pending_flat.take() {
522 self.output.commands.push(AnalogDrawingCommand::Flat {
523 start_px: start,
524 start_val,
525 end_px: self.max_valid_pixel,
526 end_val: start_val, });
528 }
529
530 if let Some(before) = before_px
532 && let Some(AnalogDrawingCommand::Flat { start_px, .. }) =
533 self.output.commands.first_mut()
534 {
535 *start_px = (*start_px).min(before);
536 }
537
538 AnalogDrawingCommands::Ready {
539 viewport_min: self.output.viewport_min,
540 viewport_max: self.output.viewport_max,
541 global_min: self.cache.global_min,
542 global_max: self.cache.global_max,
543 type_limits: self.type_limits,
544 values: self.output.commands,
545 min_valid_pixel: self.min_valid_pixel,
546 max_valid_pixel: self.max_valid_pixel,
547 analog_settings: self.analog_settings,
548 }
549 }
550}
551
552pub trait RenderStrategy {
554 fn reset_state(&mut self);
556
557 fn last_point(&self) -> Option<Pos2>;
559
560 fn set_last_point(&mut self, point: Pos2);
562
563 fn render_flat(
567 &mut self,
568 ctx: &mut DrawingContext,
569 render_ctx: &RenderContext,
570 start_px: f32,
571 start_val: f64,
572 end_px: f32,
573 end_val: f64,
574 );
575
576 fn render_range(
579 &mut self,
580 ctx: &mut DrawingContext,
581 render_ctx: &RenderContext,
582 px: f32,
583 min_val: f64,
584 max_val: f64,
585 ) {
586 if !min_val.is_finite() || !max_val.is_finite() {
587 let nan = if min_val.is_finite() {
588 max_val
589 } else {
590 min_val
591 };
592 render_ctx.draw_undefined(px, px + 1.0, nan, ctx);
593 self.reset_state();
594 return;
595 }
596
597 let p_min = render_ctx.to_screen(px, min_val, ctx);
598 let p_max = render_ctx.to_screen(px, max_val, ctx);
599
600 let (connect, other) = match self.last_point() {
602 Some(prev) if (prev.y - p_min.y).abs() < (prev.y - p_max.y).abs() => (p_min, p_max),
603 _ => (p_max, p_min),
604 };
605
606 if let Some(prev) = self.last_point() {
607 render_ctx.draw_line(prev, connect, ctx);
608 }
609
610 render_ctx.draw_line(connect, other, ctx);
612 self.set_last_point(other);
613 }
614}
615
616pub struct RenderContext {
619 pub stroke: Stroke,
620 pub min_val: f64,
621 pub max_val: f64,
622 pub min_valid_pixel: f32,
624 pub max_valid_pixel: f32,
626 pub offset: f32,
627 pub height_scale: f32,
628 pub line_height: f32,
629}
630
631impl RenderContext {
632 #[allow(clippy::too_many_arguments)]
633 fn new(
634 color: Color32,
635 min_val: f64,
636 max_val: f64,
637 min_valid_pixel: f32,
638 max_valid_pixel: f32,
639 offset: f32,
640 height_scale: f32,
641 ctx: &DrawingContext,
642 ) -> Self {
643 Self {
644 stroke: Stroke::new(ctx.theme.linewidth, color),
645 min_val,
646 max_val,
647 min_valid_pixel,
648 max_valid_pixel,
649 offset,
650 height_scale,
651 line_height: ctx.cfg.line_height,
652 }
653 }
654
655 #[must_use]
658 pub fn normalize(&self, value: f64) -> f32 {
659 debug_assert!(
660 self.min_val.is_finite() && self.max_val.is_finite(),
661 "RenderContext min_val and max_val must be finite"
662 );
663 let range = self.max_val - self.min_val;
664 if range.abs() <= f64::EPSILON {
665 0.5
666 } else {
667 ((value - self.min_val) / range) as f32
668 }
669 }
670
671 #[must_use]
673 pub fn to_screen(&self, x: f32, y: f64, ctx: &DrawingContext) -> Pos2 {
674 let y_norm = self.normalize(y);
675 (ctx.to_screen)(
676 x,
677 (1.0 - y_norm) * self.line_height * self.height_scale + self.offset,
678 )
679 }
680
681 #[must_use]
683 pub fn clamp_x(&self, x: f32) -> f32 {
684 x.clamp(self.min_valid_pixel, self.max_valid_pixel)
685 }
686
687 pub fn draw_line(&self, from: Pos2, to: Pos2, ctx: &mut DrawingContext) {
688 ctx.painter
689 .add(PathShape::line(vec![from, to], self.stroke));
690 }
691
692 pub fn draw_undefined(&self, start_x: f32, end_x: f32, value: f64, ctx: &mut DrawingContext) {
693 let color = if value == f64::INFINITY {
694 ctx.theme.accent_error.background
695 } else if value == f64::NEG_INFINITY {
696 ctx.theme.variable_dontcare
697 } else if is_nan_highimp(value) {
698 ctx.theme.variable_highimp
699 } else {
700 ctx.theme.variable_undef
701 };
702 let min = (ctx.to_screen)(start_x, self.offset);
703 let max = (ctx.to_screen)(end_x, self.offset + self.line_height * self.height_scale);
704 ctx.painter
705 .rect_filled(Rect::from_min_max(min, max), CornerRadius::ZERO, color);
706 }
707}
708
709#[derive(Default)]
711pub struct StepStrategy {
712 last_point: Option<Pos2>,
713}
714
715impl RenderStrategy for StepStrategy {
716 fn reset_state(&mut self) {
717 self.last_point = None;
718 }
719
720 fn last_point(&self) -> Option<Pos2> {
721 self.last_point
722 }
723
724 fn set_last_point(&mut self, point: Pos2) {
725 self.last_point = Some(point);
726 }
727
728 fn render_flat(
729 &mut self,
730 ctx: &mut DrawingContext,
731 render_ctx: &RenderContext,
732 start_px: f32,
733 start_val: f64,
734 end_px: f32,
735 _end_val: f64, ) {
737 let start_px = render_ctx.clamp_x(start_px);
738 let end_px = render_ctx.clamp_x(end_px);
739
740 if !start_val.is_finite() {
741 render_ctx.draw_undefined(start_px, end_px, start_val, ctx);
742 self.reset_state();
743 return;
744 }
745
746 let p1 = render_ctx.to_screen(start_px, start_val, ctx);
747 let p2 = render_ctx.to_screen(end_px, start_val, ctx);
748
749 if let Some(prev) = self.last_point {
751 render_ctx.draw_line(Pos2::new(p1.x, prev.y), p1, ctx);
752 }
753
754 render_ctx.draw_line(p1, p2, ctx);
756 self.last_point = Some(p2);
757 }
758}
759
760#[derive(Default)]
762pub struct InterpolatedStrategy {
763 last_point: Option<Pos2>,
764 started: bool,
765}
766
767impl RenderStrategy for InterpolatedStrategy {
768 fn reset_state(&mut self) {
769 self.last_point = None;
770 self.started = true;
771 }
772
773 fn last_point(&self) -> Option<Pos2> {
774 self.last_point
775 }
776
777 fn set_last_point(&mut self, point: Pos2) {
778 self.last_point = Some(point);
779 self.started = true;
780 }
781
782 fn render_flat(
783 &mut self,
784 ctx: &mut DrawingContext,
785 render_ctx: &RenderContext,
786 start_px: f32,
787 start_val: f64,
788 end_px: f32,
789 end_val: f64,
790 ) {
791 let start_px = render_ctx.clamp_x(start_px);
792 let end_px = render_ctx.clamp_x(end_px);
793
794 if !start_val.is_finite() {
795 render_ctx.draw_undefined(start_px, end_px, start_val, ctx);
796 self.reset_state();
797 return;
798 }
799
800 let end_val = if end_val.is_finite() {
802 end_val
803 } else {
804 start_val
805 };
806
807 let p1 = render_ctx.to_screen(start_px, start_val, ctx);
808 let p2 = render_ctx.to_screen(end_px, end_val, ctx);
809
810 if let Some(prev) = self.last_point {
812 render_ctx.draw_line(prev, p1, ctx);
813 } else if !self.started {
814 let edge = render_ctx.to_screen(render_ctx.min_valid_pixel.max(0.0), start_val, ctx);
816 render_ctx.draw_line(edge, p1, ctx);
817 }
818
819 render_ctx.draw_line(p1, p2, ctx);
820 self.last_point = Some(p2);
821 self.started = true;
822 }
823}
824
825fn render_with_strategy<S: RenderStrategy>(
827 commands: &[AnalogDrawingCommand],
828 render_ctx: &RenderContext,
829 strategy: &mut S,
830 ctx: &mut DrawingContext,
831) {
832 for cmd in commands {
833 match cmd {
834 AnalogDrawingCommand::Flat {
835 start_px,
836 start_val,
837 end_px,
838 end_val,
839 } => {
840 strategy.render_flat(ctx, render_ctx, *start_px, *start_val, *end_px, *end_val);
841 }
842 AnalogDrawingCommand::Range {
843 px,
844 min_val,
845 max_val,
846 } => {
847 strategy.render_range(ctx, render_ctx, *px, *min_val, *max_val);
848 }
849 }
850 }
851}
852
853fn format_amplitude_value(value: f64) -> String {
855 const SCIENTIFIC_THRESHOLD_HIGH: f64 = 1e4;
856 const SCIENTIFIC_THRESHOLD_LOW: f64 = 1e-3;
857 let abs_val = value.abs();
858 if abs_val == 0.0 {
859 "0.00".to_string()
860 } else if !(SCIENTIFIC_THRESHOLD_LOW..SCIENTIFIC_THRESHOLD_HIGH).contains(&abs_val) {
861 format!("{value:.2e}")
862 } else {
863 format!("{value:.2}")
864 }
865}
866
867fn draw_amplitude_labels(render_ctx: &RenderContext, ctx: &mut DrawingContext) {
868 const SPLIT_LABEL_HEIGHT_THRESHOLD: f32 = 2.0;
869 const BACKGROUND_ALPHA: u8 = 200;
870
871 let text_size = ctx.cfg.text_size;
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(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_width - 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_width - 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}