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