1use derive_more::Display;
3use ecolor::Color32;
4use egui::{Button, Key, RichText, Ui};
5use egui_remixicon::icons;
6use emath::{Align2, Pos2};
7use enum_iterator::Sequence;
8use epaint::{FontId, Stroke};
9use ftr_parser::types::Timescale;
10use itertools::Itertools;
11use num::{BigInt, BigRational, ToPrimitive, Zero};
12use pure_rust_locales::{Locale, locale_match};
13use serde::{Deserialize, Serialize};
14use std::sync::OnceLock;
15use sys_locale::get_locale;
16
17use crate::config::SurferConfig;
18use crate::viewport::Viewport;
19use crate::wave_data::WaveData;
20use crate::{
21 Message, SystemState,
22 translation::group_n_chars,
23 view::{DrawConfig, DrawingContext},
24};
25
26#[derive(Serialize, Deserialize, Clone)]
27pub struct TimeScale {
28 pub unit: TimeUnit,
29 pub multiplier: Option<u32>,
30}
31
32impl TimeScale {
33 pub(crate) fn multiplier_digits(&self) -> u8 {
34 match self.multiplier {
35 Some(1) => 0,
36 Some(10) => 1,
37 Some(100) => 2,
38 Some(multiplier) => multiplier.ilog10() as u8,
39 None => 0,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, Display, Eq, PartialEq, Serialize, Deserialize, Sequence)]
45pub enum TimeUnit {
46 #[display("zs")]
47 ZeptoSeconds,
48
49 #[display("as")]
50 AttoSeconds,
51
52 #[display("fs")]
53 FemtoSeconds,
54
55 #[display("ps")]
56 PicoSeconds,
57
58 #[display("ns")]
59 NanoSeconds,
60
61 #[display("μs")]
62 MicroSeconds,
63
64 #[display("ms")]
65 MilliSeconds,
66
67 #[display("s")]
68 Seconds,
69
70 #[display("No unit")]
71 None,
72
73 #[display("Auto")]
75 Auto,
76}
77
78pub const DEFAULT_TIMELINE_NAME: &str = "Time";
79const THIN_SPACE: &str = "\u{2009}";
80
81pub const TICK_STEPS: [f64; 8] = [1., 2., 2.5, 5., 10., 20., 25., 50.];
83
84struct LocaleFormatCache {
86 grouping: &'static [i64],
87 thousands_sep: String,
88 decimal_point: String,
89}
90
91static LOCALE_FORMAT_CACHE: OnceLock<LocaleFormatCache> = OnceLock::new();
92
93fn get_locale_format_cache() -> &'static LocaleFormatCache {
95 LOCALE_FORMAT_CACHE.get_or_init(|| {
96 let locale = get_locale()
97 .unwrap_or_else(|| "en-US".to_string())
98 .as_str()
99 .try_into()
100 .unwrap_or(Locale::en_US);
101 create_cache(locale)
102 })
103}
104
105fn create_cache(locale: Locale) -> LocaleFormatCache {
106 let grouping = locale_match!(locale => LC_NUMERIC::GROUPING);
107 let thousands_sep =
108 locale_match!(locale => LC_NUMERIC::THOUSANDS_SEP).replace('\u{202f}', THIN_SPACE);
109 let decimal_point = locale_match!(locale => LC_NUMERIC::DECIMAL_POINT).to_string();
110
111 LocaleFormatCache {
112 grouping,
113 thousands_sep,
114 decimal_point,
115 }
116}
117
118impl From<wellen::TimescaleUnit> for TimeUnit {
119 fn from(timescale: wellen::TimescaleUnit) -> Self {
120 match timescale {
121 wellen::TimescaleUnit::ZeptoSeconds => TimeUnit::ZeptoSeconds,
122 wellen::TimescaleUnit::AttoSeconds => TimeUnit::AttoSeconds,
123 wellen::TimescaleUnit::FemtoSeconds => TimeUnit::FemtoSeconds,
124 wellen::TimescaleUnit::PicoSeconds => TimeUnit::PicoSeconds,
125 wellen::TimescaleUnit::NanoSeconds => TimeUnit::NanoSeconds,
126 wellen::TimescaleUnit::MicroSeconds => TimeUnit::MicroSeconds,
127 wellen::TimescaleUnit::MilliSeconds => TimeUnit::MilliSeconds,
128 wellen::TimescaleUnit::Seconds => TimeUnit::Seconds,
129 wellen::TimescaleUnit::Unknown => TimeUnit::None,
130 }
131 }
132}
133
134impl From<ftr_parser::types::Timescale> for TimeUnit {
135 fn from(timescale: Timescale) -> Self {
136 match timescale {
137 Timescale::Fs => TimeUnit::FemtoSeconds,
138 Timescale::Ps => TimeUnit::PicoSeconds,
139 Timescale::Ns => TimeUnit::NanoSeconds,
140 Timescale::Us => TimeUnit::MicroSeconds,
141 Timescale::Ms => TimeUnit::MilliSeconds,
142 Timescale::S => TimeUnit::Seconds,
143 Timescale::Unit => TimeUnit::None,
144 Timescale::None => TimeUnit::None,
145 }
146 }
147}
148
149impl TimeUnit {
150 fn exponent(self) -> i8 {
152 match self {
153 TimeUnit::ZeptoSeconds => -21,
154 TimeUnit::AttoSeconds => -18,
155 TimeUnit::FemtoSeconds => -15,
156 TimeUnit::PicoSeconds => -12,
157 TimeUnit::NanoSeconds => -9,
158 TimeUnit::MicroSeconds => -6,
159 TimeUnit::MilliSeconds => -3,
160 TimeUnit::Seconds => 0,
161 TimeUnit::None => 0,
162 TimeUnit::Auto => 0,
163 }
164 }
165 fn from_exponent(exponent: i8) -> Option<Self> {
167 match exponent {
168 -21 => Some(TimeUnit::ZeptoSeconds),
169 -18 => Some(TimeUnit::AttoSeconds),
170 -15 => Some(TimeUnit::FemtoSeconds),
171 -12 => Some(TimeUnit::PicoSeconds),
172 -9 => Some(TimeUnit::NanoSeconds),
173 -6 => Some(TimeUnit::MicroSeconds),
174 -3 => Some(TimeUnit::MilliSeconds),
175 0 => Some(TimeUnit::Seconds),
176 _ => None,
177 }
178 }
179}
180
181pub fn timeunit_menu(ui: &mut Ui, msgs: &mut Vec<Message>, wanted_timeunit: &TimeUnit) {
183 for timeunit in enum_iterator::all::<TimeUnit>() {
184 if ui
185 .radio(*wanted_timeunit == timeunit, timeunit.to_string())
186 .clicked()
187 {
188 msgs.push(Message::SetTimeUnit(timeunit));
189 }
190 }
191}
192
193#[derive(Debug, Deserialize, Serialize, Clone)]
195pub struct TimeFormat {
196 format: TimeStringFormatting,
198 show_space: bool,
200 show_unit: bool,
202}
203
204impl Default for TimeFormat {
205 fn default() -> Self {
206 TimeFormat {
207 format: TimeStringFormatting::No,
208 show_space: true,
209 show_unit: true,
210 }
211 }
212}
213
214impl TimeFormat {
215 #[must_use]
217 pub fn new(format: TimeStringFormatting, show_space: bool, show_unit: bool) -> Self {
218 TimeFormat {
219 format,
220 show_space,
221 show_unit,
222 }
223 }
224
225 #[must_use]
227 pub fn with_format(mut self, format: TimeStringFormatting) -> Self {
228 self.format = format;
229 self
230 }
231
232 #[must_use]
234 pub fn with_space(mut self, show_space: bool) -> Self {
235 self.show_space = show_space;
236 self
237 }
238
239 #[must_use]
241 pub fn with_unit(mut self, show_unit: bool) -> Self {
242 self.show_unit = show_unit;
243 self
244 }
245}
246
247pub fn timeformat_menu(ui: &mut Ui, msgs: &mut Vec<Message>, current_timeformat: &TimeFormat) {
249 for time_string_format in enum_iterator::all::<TimeStringFormatting>() {
250 if ui
251 .radio(
252 current_timeformat.format == time_string_format,
253 if time_string_format == TimeStringFormatting::Locale {
254 format!(
255 "{time_string_format} ({locale})",
256 locale = get_locale().unwrap_or_else(|| "unknown".to_string())
257 )
258 } else {
259 time_string_format.to_string()
260 },
261 )
262 .clicked()
263 {
264 msgs.push(Message::SetTimeStringFormatting(Some(time_string_format)));
265 }
266 }
267}
268
269#[derive(Debug, Clone, Copy, Display, Eq, PartialEq, Serialize, Deserialize, Sequence)]
271pub enum TimeStringFormatting {
272 No,
274
275 Locale,
277
278 SI,
281}
282
283fn strip_trailing_zeros_and_period(time: &str) -> String {
286 if !time.contains('.') {
287 return time.to_string();
288 }
289 time.trim_end_matches('0').trim_end_matches('.').to_string()
290}
291
292fn split_and_format_number(time: &str, format: TimeStringFormatting) -> String {
295 match format {
296 TimeStringFormatting::No => time.to_string(),
297 TimeStringFormatting::Locale => format_locale(time, get_locale_format_cache()),
298 TimeStringFormatting::SI => format_si(time),
299 }
300}
301
302fn format_si(time: &str) -> String {
303 if let Some((integer_part, fractional_part)) = time.split_once('.') {
304 let integer_result = if integer_part.len() > 4 {
305 group_n_chars(integer_part, 3).join(THIN_SPACE)
306 } else {
307 integer_part.to_string()
308 };
309 if fractional_part.len() > 4 {
310 let reversed = fractional_part.chars().rev().collect::<String>();
311 let reversed_fractional_parts = group_n_chars(&reversed, 3).join(THIN_SPACE);
312 let fractional_result = reversed_fractional_parts.chars().rev().collect::<String>();
313 format!("{integer_result}.{fractional_result}")
314 } else {
315 format!("{integer_result}.{fractional_part}")
316 }
317 } else if time.len() > 4 {
318 group_n_chars(time, 3).join(THIN_SPACE)
319 } else {
320 time.to_string()
321 }
322}
323
324fn format_locale(time: &str, cache: &LocaleFormatCache) -> String {
325 if cache.grouping[0] > 0 {
326 if let Some((integer_part, fractional_part)) = time.split_once('.') {
327 let integer_result = group_n_chars(integer_part, cache.grouping[0] as usize)
328 .join(cache.thousands_sep.as_str());
329 format!(
330 "{integer_result}{decimal_point}{fractional_part}",
331 decimal_point = cache.decimal_point
332 )
333 } else {
334 group_n_chars(time, cache.grouping[0] as usize).join(cache.thousands_sep.as_str())
335 }
336 } else {
337 time.to_string()
338 }
339}
340
341fn find_auto_scale(time: &BigInt, timescale: &TimeScale) -> TimeUnit {
343 if matches!(timescale.unit, TimeUnit::Seconds) {
346 return TimeUnit::Seconds;
347 }
348 let multiplier_digits = timescale.multiplier_digits();
349 let start_digits = -timescale.unit.exponent() as u8;
350 for e in (3..=start_digits).step_by(3).rev() {
351 if (time % pow10(e - multiplier_digits)).is_zero()
352 && let Some(unit) = TimeUnit::from_exponent(e as i8 - start_digits as i8)
353 {
354 return unit;
355 }
356 }
357 timescale.unit
358}
359
360pub struct TimeFormatter {
363 timescale: TimeScale,
364 wanted_unit: TimeUnit,
365 time_format: TimeFormat,
366 exponent_diff: i8,
368 unit_string: String,
370 space_string: String,
372}
373
374impl TimeFormatter {
375 #[must_use]
377 pub(crate) fn new(
378 timescale: &TimeScale,
379 wanted_unit: &TimeUnit,
380 time_format: &TimeFormat,
381 ) -> Self {
382 let (exponent_diff, unit_string) = if *wanted_unit == TimeUnit::Auto {
384 (0i8, String::new())
386 } else {
387 let wanted_exponent = wanted_unit.exponent();
388 let data_exponent = timescale.unit.exponent();
389 let exponent_diff = wanted_exponent - data_exponent;
390
391 let unit_string = if time_format.show_unit {
392 wanted_unit.to_string()
393 } else {
394 String::new()
395 };
396
397 (exponent_diff, unit_string)
398 };
399
400 TimeFormatter {
401 timescale: timescale.clone(),
402 wanted_unit: *wanted_unit,
403 time_format: time_format.clone(),
404 exponent_diff,
405 unit_string,
406 space_string: if time_format.show_space {
407 " ".to_string()
408 } else {
409 String::new()
410 },
411 }
412 }
413
414 #[must_use]
416 pub(crate) fn format(&self, time: &BigInt) -> String {
417 if self.wanted_unit == TimeUnit::None {
418 return split_and_format_number(&time.to_string(), self.time_format.format);
419 }
420
421 let (exponent_diff, unit_string) = if self.wanted_unit == TimeUnit::Auto {
423 let auto_unit = find_auto_scale(time, &self.timescale);
424 let wanted_exponent = auto_unit.exponent();
425 let data_exponent = self.timescale.unit.exponent();
426 let exp_diff = wanted_exponent - data_exponent;
427
428 let unit_str = if self.time_format.show_unit {
429 auto_unit.to_string()
430 } else {
431 String::new()
432 };
433
434 (exp_diff, unit_str)
435 } else {
436 (self.exponent_diff, self.unit_string.clone())
437 };
438
439 let timestring = if exponent_diff >= 0 {
440 let precision = exponent_diff as usize;
441 let scaledtime = BigRational::new(
442 time * self.timescale.multiplier.unwrap_or(1),
443 pow10(exponent_diff as u8),
444 )
445 .to_f64()
446 .unwrap_or(f64::NAN);
447
448 let time = format!("{scaledtime:.precision$}",);
449 strip_trailing_zeros_and_period(&time)
450 } else {
451 (time * self.timescale.multiplier.unwrap_or(1) * pow10((-exponent_diff) as u8))
452 .to_string()
453 };
454
455 format!(
456 "{scaledtime}{space}{unit}",
457 scaledtime = split_and_format_number(×tring, self.time_format.format),
458 space = if unit_string.is_empty() {
459 ""
460 } else {
461 &self.space_string
462 },
463 unit = unit_string
464 )
465 }
466}
467
468fn pow10(exp: u8) -> BigInt {
471 match exp {
472 0 => BigInt::from(1),
473 1 => BigInt::from(10),
474 2 => BigInt::from(100),
475 3 => BigInt::from(1000),
476 4 => BigInt::from(10_000),
477 5 => BigInt::from(100_000),
478 6 => BigInt::from(1_000_000),
479 7 => BigInt::from(10_000_000),
480 8 => BigInt::from(100_000_000),
481 9 => BigInt::from(1_000_000_000),
482 10 => BigInt::from(10_000_000_000i64),
483 11 => BigInt::from(100_000_000_000i64),
484 12 => BigInt::from(1_000_000_000_000i64),
485 13 => BigInt::from(10_000_000_000_000i64),
486 14 => BigInt::from(100_000_000_000_000i64),
487 15 => BigInt::from(1_000_000_000_000_000i64),
488 16 => BigInt::from(10_000_000_000_000_000i64),
489 17 => BigInt::from(100_000_000_000_000_000i64),
490 18 => BigInt::from(1_000_000_000_000_000_000i64),
491 19 => BigInt::from(10_000_000_000_000_000_000i128),
492 20 => BigInt::from(100_000_000_000_000_000_000i128),
493 21 => BigInt::from(1_000_000_000_000_000_000_000i128),
494 _ => BigInt::from(10).pow(exp as u32),
495 }
496}
497
498#[must_use]
501pub(crate) fn time_string(
502 time: &BigInt,
503 timescale: &TimeScale,
504 wanted_timeunit: &TimeUnit,
505 wanted_time_format: &TimeFormat,
506) -> String {
507 let formatter = TimeFormatter::new(timescale, wanted_timeunit, wanted_time_format);
508 formatter.format(time)
509}
510
511fn parse_time_input(input: &str) -> (String, Option<TimeUnit>) {
516 let sorted_units =
517 [
519 ("zs", TimeUnit::ZeptoSeconds),
520 ("as", TimeUnit::AttoSeconds),
521 ("fs", TimeUnit::FemtoSeconds),
522 ("ps", TimeUnit::PicoSeconds),
523 ("ns", TimeUnit::NanoSeconds),
524 ("μs", TimeUnit::MicroSeconds),
525 ("us", TimeUnit::MicroSeconds), ("ms", TimeUnit::MilliSeconds),
527 ("s", TimeUnit::Seconds),
528 ];
529
530 let trimmed = input.trim();
531
532 for (unit_str, unit) in sorted_units {
533 if trimmed.ends_with(unit_str) {
535 let after_number = trimmed.len() - unit_str.len();
536
537 if after_number > 0 {
539 let numeric = trimmed[..after_number].trim_end();
540 if let Some(last_char) = numeric.chars().next_back()
541 && (last_char.is_ascii_digit() || last_char == '.')
542 {
543 return (numeric.to_string(), Some(unit));
544 }
545 }
546 }
547 }
548
549 (trimmed.to_string(), None)
550}
551
552fn split_numeric_parts(numeric_str: &str) -> Result<(String, String), String> {
556 let trimmed = numeric_str.trim();
557 if trimmed.is_empty() {
558 return Err("Empty input".to_string());
559 }
560 if trimmed.starts_with('-') {
561 return Err("Negative numbers not supported".to_string());
562 }
563 let normalized = trimmed.strip_prefix('+').unwrap_or(trimmed);
564 let mut parts = normalized.split('.');
565 let integer_part = parts.next().unwrap_or("");
566 let fractional_part = parts.next().unwrap_or("");
567 if parts.next().is_some() {
568 return Err("Invalid number: multiple decimal points".to_string());
569 }
570
571 let all_valid =
573 (integer_part.chars().chain(fractional_part.chars())).all(|c| c.is_ascii_digit());
574 if !all_valid {
575 return Err(format!("Failed to parse '{numeric_str}' as number"));
576 }
577
578 let integer = if integer_part.is_empty() {
579 "0".to_string()
580 } else {
581 integer_part.to_string()
582 };
583 Ok((integer, fractional_part.to_string()))
584}
585
586fn normalize_numeric_with_unit(
591 numeric_str: &str,
592 unit: TimeUnit,
593) -> Result<(BigInt, TimeUnit), String> {
594 let (integer_part, mut fractional_part) = split_numeric_parts(numeric_str)?;
595
596 while fractional_part.ends_with('0') {
598 fractional_part.pop();
599 }
600
601 if fractional_part.is_empty() {
602 let value =
603 BigInt::parse_bytes(integer_part.as_bytes(), 10).unwrap_or_else(|| BigInt::from(0));
604 return Ok((value, unit));
605 }
606
607 let fractional_len = fractional_part.len();
608 if fractional_len > 21 {
610 return Err("Too many decimal places (max 21 supported)".to_string());
611 }
612
613 let steps = fractional_len.div_ceil(3) as i8; let new_exponent = unit.exponent() - (steps * 3);
615 let new_unit = TimeUnit::from_exponent(new_exponent)
616 .ok_or_else(|| "Too much precision for available time units".to_string())?;
617
618 let mut combined = integer_part;
619 combined.push_str(&fractional_part);
620 let mut value = BigInt::parse_bytes(combined.as_bytes(), 10).unwrap_or_else(|| BigInt::from(0));
621
622 let extra_zeros = (steps as usize * 3).saturating_sub(fractional_len);
623 if extra_zeros > 0 {
624 let scale = pow10(extra_zeros as u8);
625 value *= scale;
626 }
627
628 Ok((value, new_unit))
629}
630
631#[derive(Clone, Debug)]
635pub(crate) struct TimeInputState {
636 input_text: String,
638 parsed_value: Option<BigInt>,
640 input_unit: Option<TimeUnit>,
642 normalized_unit: Option<TimeUnit>,
644 selected_unit: TimeUnit,
646 error: Option<String>,
648}
649
650impl Default for TimeInputState {
651 fn default() -> Self {
652 Self {
653 input_text: String::new(),
654 parsed_value: None,
655 input_unit: None,
656 normalized_unit: None,
657 selected_unit: TimeUnit::NanoSeconds,
658 error: None,
659 }
660 }
661}
662
663impl TimeInputState {
664 #[cfg(test)]
666 pub(crate) fn new() -> Self {
667 Self::default()
668 }
669
670 fn update_input(&mut self, input: String) {
672 self.input_text = input;
673 let (numeric_str, unit) = parse_time_input(&self.input_text);
674 self.input_unit = unit;
675
676 let base_unit = self.input_unit.unwrap_or(self.selected_unit);
677 match normalize_numeric_with_unit(&numeric_str, base_unit) {
678 Ok((val, normalized_unit)) => {
679 self.parsed_value = Some(val);
680 self.normalized_unit = Some(normalized_unit);
681 self.error = None;
682 }
683 Err(e) => {
684 self.parsed_value = None;
685 self.normalized_unit = None;
686 self.error = Some(e);
687 }
688 }
689 }
690
691 fn effective_unit(&self) -> TimeUnit {
693 self.normalized_unit
694 .or(self.input_unit)
695 .unwrap_or(self.selected_unit)
696 }
697
698 fn to_timescale_ticks(&self, timescale: &TimeScale) -> Option<BigInt> {
702 let value = self.parsed_value.clone()?;
703 let unit = self.effective_unit();
704 let base_unit = if unit == TimeUnit::None {
705 timescale.unit
706 } else {
707 unit
708 };
709 let unit_exp = base_unit.exponent();
710 let data_exp = timescale.unit.exponent();
711 let diff = unit_exp - data_exp;
712
713 scale_time(&value, diff, timescale)
714 }
715}
716
717impl SystemState {
732 pub(crate) fn time_input_widget(
734 &self,
735 ui: &mut Ui,
736 id_prefix: &str,
737 waves: &WaveData,
738 msgs: &mut Vec<Message>,
739 ) {
740 ui.horizontal(|ui| {
741 let mut widgets = self.time_widgets.borrow_mut();
743 let state = widgets.entry(id_prefix.to_string()).or_default();
744
745 let on_commit = |time_stamp: BigInt| {
746 Message::GoToTime(Some(time_stamp), 0)
748 };
749 self.time_input_controls(ui, state, id_prefix, waves, msgs, &on_commit);
751
752 let button_enabled = state.parsed_value.is_some();
754 let goto_button = Button::new(RichText::new(icons::TARGET_FILL).heading()).frame(false);
755 if ui
756 .add_enabled(button_enabled, goto_button)
757 .on_hover_text("Go to time")
758 .clicked()
759 && let Some(time_stamp) =
760 state.to_timescale_ticks(&waves.inner.metadata().timescale)
761 {
762 msgs.push(on_commit(time_stamp));
763 }
764
765 let cursor_button =
767 Button::new(RichText::new(icons::CURSOR_FILL).heading()).frame(false);
768 if ui
769 .add_enabled(button_enabled, cursor_button)
770 .on_hover_text("Set cursor at time")
771 .clicked()
772 && let Some(time_stamp) =
773 state.to_timescale_ticks(&waves.inner.metadata().timescale)
774 {
775 msgs.push(Message::CursorSet(time_stamp));
776 }
777 });
778 }
779
780 fn time_input_controls<F>(
782 &self,
783 ui: &mut Ui,
784 state: &mut TimeInputState,
785 id_prefix: &str,
786 waves: &WaveData,
787 msgs: &mut Vec<Message>,
788 on_commit: &F,
789 ) where
790 F: Fn(BigInt) -> Message,
791 {
792 let mut input = state.input_text.clone();
794 let text_response = ui.add(
795 egui::TextEdit::singleline(&mut input)
796 .desired_width(100.0)
797 .hint_text("e.g., 1.5ms"),
798 );
799
800 let request_focus = *self
802 .text_edit_request_focus
803 .get(id_prefix)
804 .unwrap_or(&false);
805 if request_focus && !text_response.has_focus() {
806 text_response.request_focus();
807 msgs.push(Message::SetRequestTextEditFocus(
808 id_prefix.to_string(),
809 false,
810 ));
811 }
812
813 if text_response.changed() {
814 state.update_input(input);
815 }
816
817 let dropdown_enabled = state.input_unit.is_none();
819
820 if dropdown_enabled {
821 let combo_id = format!("{id_prefix}-unit");
823 egui::ComboBox::new(combo_id, "")
824 .width(32.0)
825 .selected_text(state.selected_unit.to_string())
826 .show_ui(ui, |ui| {
827 for unit in enum_iterator::all::<TimeUnit>() {
828 if !matches!(unit, TimeUnit::Auto | TimeUnit::None) {
830 ui.selectable_value(&mut state.selected_unit, unit, unit.to_string());
831 }
832 }
833 });
834 }
835
836 if text_response.gained_focus() {
838 msgs.push(Message::SetTextEditFocused(id_prefix.to_string(), true));
839 }
840 if text_response.lost_focus() {
841 if text_response.ctx.input(|i| i.key_pressed(Key::Enter)) {
842 if let Some(time_stamp) =
844 state.to_timescale_ticks(&waves.inner.metadata().timescale)
845 {
846 msgs.push(on_commit(time_stamp));
847 }
848 }
849 msgs.push(Message::SetTextEditFocused(id_prefix.to_string(), false));
850 }
851 }
852}
853
854impl WaveData {
855 pub(crate) fn draw_tick_line(&self, x: f32, ctx: &mut DrawingContext, stroke: &Stroke) {
856 let Pos2 {
857 x: x_pos,
858 y: y_start,
859 } = (ctx.to_screen)(x, 0.);
860 ctx.painter.vline(
861 x_pos,
862 (y_start)..=(y_start + ctx.cfg.canvas_size.y),
863 *stroke,
864 );
865 }
866 pub(crate) fn draw_ticks(
868 &self,
869 color: Color32,
870 ticks: &[(String, f32, i64)],
871 ctx: &DrawingContext<'_>,
872 y_offset: f32,
873 align: Align2,
874 ) {
875 for (tick_text, x, _) in ticks {
876 ctx.painter.text(
877 (ctx.to_screen)(*x, y_offset),
878 align,
879 tick_text,
880 FontId::proportional(ctx.cfg.text_size),
881 color,
882 );
883 }
884 }
885
886 pub fn draw_divider_text(
888 &self,
889 color: Option<Color32>,
890 text: &str,
891 ticks: &[(String, f32, i64)],
892 ctx: &DrawingContext<'_>,
893 y_offset: f32,
894 config: &SurferConfig,
895 ) {
896 let font = FontId::monospace(ctx.cfg.text_size);
897 let color = color.unwrap_or(config.theme.foreground);
898
899 let layout = ctx
900 .painter
901 .layout_no_wrap(text.to_string(), font.clone(), color);
902 let text_width = layout.rect.width() + (font.size * 2.);
903
904 let (next_tick, next_stamp) = ticks
905 .get(1)
906 .map_or_else(|| (1., 1), |&(_, dist, stamp)| (dist, stamp));
907
908 let (first_tick, first_stamp) = ticks
909 .first()
910 .map_or_else(|| (0., 0), |&(_, dist, stamp)| (dist, stamp));
911
912 let tick_delta = (next_tick - first_tick).abs();
913 let stamp_delta = next_stamp - first_stamp;
914 let tick_stride = (text_width / tick_delta).ceil();
915 let stamp_stride = stamp_delta * tick_stride as i64;
916 let elapsed = first_stamp / stamp_stride;
917 let mut last_stamp = (elapsed * stamp_stride) - (stamp_stride / 2);
918
919 for (_, x, stamp) in ticks {
920 if (*stamp < last_stamp + stamp_stride) || *stamp < 0 {
921 continue;
922 }
923 last_stamp = *stamp;
924
925 ctx.painter.text(
926 (ctx.to_screen)(*x, y_offset),
927 Align2::CENTER_TOP,
928 text.to_string(),
929 font.clone(),
930 color,
931 );
932 }
933 }
934}
935
936impl SystemState {
937 pub(crate) fn get_time_format(&self) -> TimeFormat {
938 let time_format = self.user.config.default_time_format.clone();
939 if let Some(time_string_format) = self.user.time_string_format {
940 time_format.with_format(time_string_format)
941 } else {
942 time_format
943 }
944 }
945
946 pub(crate) fn get_ticks_for_viewport_idx(
947 &self,
948 waves: &WaveData,
949 viewport_idx: usize,
950 cfg: &DrawConfig,
951 ) -> Vec<(String, f32, i64)> {
952 self.get_ticks_for_viewport(waves, &waves.viewports[viewport_idx], cfg)
953 }
954
955 pub(crate) fn get_ticks_for_viewport(
956 &self,
957 waves: &WaveData,
958 viewport: &Viewport,
959 cfg: &DrawConfig,
960 ) -> Vec<(String, f32, i64)> {
961 let time_offset = waves.time_offset();
962 get_ticks_internal(
963 viewport,
964 &waves.inner.metadata().timescale,
965 cfg.canvas_size.x,
966 cfg.text_size,
967 &self.user.wanted_timeunit,
968 &self.get_time_format(),
969 self.user.config.theme.ticks.density,
970 &waves.safe_max_timestamp(),
971 time_offset,
972 )
973 }
974}
975
976#[allow(clippy::too_many_arguments)]
980#[must_use]
981fn get_ticks_internal(
982 viewport: &Viewport,
983 timescale: &TimeScale,
984 frame_width: f32,
985 text_size: f32,
986 wanted_timeunit: &TimeUnit,
987 time_format: &TimeFormat,
988 density: f32,
989 max_timestamp: &BigInt,
990 time_offset: &BigInt,
991) -> Vec<(String, f32, i64)> {
992 let char_width = text_size * (20. / 31.);
993 let rightexp = viewport
994 .curr_right
995 .absolute(max_timestamp, time_offset)
996 .inner()
997 .abs()
998 .log10()
999 .round() as i16;
1000 let leftexp = viewport
1001 .curr_left
1002 .absolute(max_timestamp, time_offset)
1003 .inner()
1004 .abs()
1005 .log10()
1006 .round() as i16;
1007 let max_labelwidth = f32::from(rightexp.max(leftexp) + 3) * char_width;
1008 let max_labels = ((frame_width * density) / max_labelwidth).floor() + 2.;
1009 let viewport_width = viewport.width_absolute(max_timestamp, time_offset);
1010 let scale = 10.0f64.powf(
1011 (viewport_width.inner() / f64::from(max_labels))
1012 .log10()
1013 .floor(),
1014 );
1015
1016 let mut ticks: Vec<(String, f32, i64)> = [].to_vec();
1017 for step in &TICK_STEPS {
1018 let scaled_step = scale * step;
1019 let left_abs = viewport
1020 .curr_left
1021 .absolute(max_timestamp, time_offset)
1022 .inner();
1023 let right_abs = viewport
1024 .curr_right
1025 .absolute(max_timestamp, time_offset)
1026 .inner();
1027 let rounded_min_label_time = (left_abs / scaled_step).floor() * scaled_step;
1028 let high = ((right_abs - rounded_min_label_time) / scaled_step).ceil() as f32 + 1.;
1029
1030 if high <= max_labels {
1031 let time_formatter = TimeFormatter::new(timescale, wanted_timeunit, time_format);
1032 ticks = (0..high as i16)
1033 .map(|v| {
1034 BigInt::from((f64::from(v) * scaled_step + rounded_min_label_time) as i128)
1035 })
1036 .unique()
1037 .map(|tick| {
1038 (
1039 time_formatter.format(&tick),
1041 viewport.pixel_from_time(&tick, frame_width, max_timestamp, time_offset),
1043 tick.to_i64().unwrap_or_default(),
1045 )
1046 })
1047 .collect::<Vec<(String, f32, i64)>>();
1048 break;
1049 }
1050 }
1051 ticks
1052}
1053
1054pub(crate) fn parse_time_string_to_ticks(input: &str, timescale: &TimeScale) -> Option<BigInt> {
1061 let (numeric_str, unit_opt) = parse_time_input(input);
1062
1063 let base_unit = unit_opt.unwrap_or(TimeUnit::None);
1064 let (value, normalized_unit) = normalize_numeric_with_unit(&numeric_str, base_unit).ok()?;
1065
1066 if normalized_unit == TimeUnit::None {
1067 return Some(value);
1069 }
1070
1071 let unit_exp = normalized_unit.exponent();
1072 let data_exp = timescale.unit.exponent();
1073 let diff = unit_exp - data_exp;
1074
1075 scale_time(&value, diff, timescale)
1076}
1077
1078fn scale_time(value: &BigInt, exponent_diff: i8, timescale: &TimeScale) -> Option<BigInt> {
1079 let exponent_diff = exponent_diff - timescale.multiplier_digits() as i8;
1080 let result = if exponent_diff > 0 {
1081 let scale = pow10(exponent_diff as u8);
1082 value * scale
1083 } else if exponent_diff < 0 {
1084 let scale = pow10((-exponent_diff) as u8);
1085 value / scale
1086 } else {
1087 value.clone()
1088 };
1089
1090 Some(result)
1091}
1092
1093#[cfg(test)]
1094mod test {
1095 use num::BigInt;
1096
1097 use crate::time::{TimeFormat, TimeScale, TimeStringFormatting, TimeUnit, time_string};
1098
1099 #[test]
1100 fn print_time_standard() {
1101 assert_eq!(
1102 time_string(
1103 &BigInt::from(103),
1104 &TimeScale {
1105 multiplier: Some(1),
1106 unit: TimeUnit::FemtoSeconds
1107 },
1108 &TimeUnit::FemtoSeconds,
1109 &TimeFormat::default()
1110 ),
1111 "103 fs"
1112 );
1113 assert_eq!(
1114 time_string(
1115 &BigInt::from(2200),
1116 &TimeScale {
1117 multiplier: Some(1),
1118 unit: TimeUnit::MicroSeconds
1119 },
1120 &TimeUnit::MicroSeconds,
1121 &TimeFormat::default()
1122 ),
1123 "2200 μs"
1124 );
1125 assert_eq!(
1126 time_string(
1127 &BigInt::from(2200),
1128 &TimeScale {
1129 multiplier: Some(1),
1130 unit: TimeUnit::MicroSeconds
1131 },
1132 &TimeUnit::MilliSeconds,
1133 &TimeFormat::default()
1134 ),
1135 "2.2 ms"
1136 );
1137 assert_eq!(
1138 time_string(
1139 &BigInt::from(2200),
1140 &TimeScale {
1141 multiplier: Some(1),
1142 unit: TimeUnit::MicroSeconds
1143 },
1144 &TimeUnit::NanoSeconds,
1145 &TimeFormat::default()
1146 ),
1147 "2200000 ns"
1148 );
1149 assert_eq!(
1150 time_string(
1151 &BigInt::from(2200),
1152 &TimeScale {
1153 multiplier: Some(1),
1154 unit: TimeUnit::NanoSeconds
1155 },
1156 &TimeUnit::PicoSeconds,
1157 &TimeFormat {
1158 format: TimeStringFormatting::No,
1159 show_space: false,
1160 show_unit: true
1161 }
1162 ),
1163 "2200000ps"
1164 );
1165 assert_eq!(
1166 time_string(
1167 &BigInt::from(2200),
1168 &TimeScale {
1169 multiplier: Some(10),
1170 unit: TimeUnit::MicroSeconds
1171 },
1172 &TimeUnit::MicroSeconds,
1173 &TimeFormat {
1174 format: TimeStringFormatting::No,
1175 show_space: false,
1176 show_unit: false
1177 }
1178 ),
1179 "22000"
1180 );
1181 }
1182 #[test]
1183 fn print_time_si() {
1184 assert_eq!(
1185 time_string(
1186 &BigInt::from(123456789010i128),
1187 &TimeScale {
1188 multiplier: Some(1),
1189 unit: TimeUnit::MicroSeconds
1190 },
1191 &TimeUnit::Seconds,
1192 &TimeFormat {
1193 format: TimeStringFormatting::SI,
1194 show_space: true,
1195 show_unit: true
1196 }
1197 ),
1198 "123\u{2009}456.789\u{2009}01 s"
1199 );
1200 assert_eq!(
1201 time_string(
1202 &BigInt::from(1456789100i128),
1203 &TimeScale {
1204 multiplier: Some(1),
1205 unit: TimeUnit::MicroSeconds
1206 },
1207 &TimeUnit::Seconds,
1208 &TimeFormat {
1209 format: TimeStringFormatting::SI,
1210 show_space: true,
1211 show_unit: true
1212 }
1213 ),
1214 "1456.7891 s"
1215 );
1216 assert_eq!(
1217 time_string(
1218 &BigInt::from(2200),
1219 &TimeScale {
1220 multiplier: Some(1),
1221 unit: TimeUnit::MicroSeconds
1222 },
1223 &TimeUnit::MicroSeconds,
1224 &TimeFormat {
1225 format: TimeStringFormatting::SI,
1226 show_space: true,
1227 show_unit: true
1228 }
1229 ),
1230 "2200 μs"
1231 );
1232 assert_eq!(
1233 time_string(
1234 &BigInt::from(22200),
1235 &TimeScale {
1236 multiplier: Some(1),
1237 unit: TimeUnit::MicroSeconds
1238 },
1239 &TimeUnit::MicroSeconds,
1240 &TimeFormat {
1241 format: TimeStringFormatting::SI,
1242 show_space: true,
1243 show_unit: true
1244 }
1245 ),
1246 "22\u{2009}200 μs"
1247 );
1248 }
1249 #[test]
1250 fn print_time_auto() {
1251 assert_eq!(
1252 time_string(
1253 &BigInt::from(2200),
1254 &TimeScale {
1255 multiplier: Some(1),
1256 unit: TimeUnit::MicroSeconds
1257 },
1258 &TimeUnit::Auto,
1259 &TimeFormat {
1260 format: TimeStringFormatting::SI,
1261 show_space: true,
1262 show_unit: true
1263 }
1264 ),
1265 "2200 μs"
1266 );
1267 assert_eq!(
1268 time_string(
1269 &BigInt::from(22000),
1270 &TimeScale {
1271 multiplier: Some(1),
1272 unit: TimeUnit::MicroSeconds
1273 },
1274 &TimeUnit::Auto,
1275 &TimeFormat {
1276 format: TimeStringFormatting::SI,
1277 show_space: true,
1278 show_unit: true
1279 }
1280 ),
1281 "22 ms"
1282 );
1283 assert_eq!(
1284 time_string(
1285 &BigInt::from(1500000000),
1286 &TimeScale {
1287 multiplier: Some(1),
1288 unit: TimeUnit::PicoSeconds
1289 },
1290 &TimeUnit::Auto,
1291 &TimeFormat {
1292 format: TimeStringFormatting::SI,
1293 show_space: true,
1294 show_unit: true
1295 }
1296 ),
1297 "1500 μs"
1298 );
1299 assert_eq!(
1300 time_string(
1301 &BigInt::from(22000),
1302 &TimeScale {
1303 multiplier: Some(10),
1304 unit: TimeUnit::MicroSeconds
1305 },
1306 &TimeUnit::Auto,
1307 &TimeFormat {
1308 format: TimeStringFormatting::SI,
1309 show_space: true,
1310 show_unit: true
1311 }
1312 ),
1313 "220 ms"
1314 );
1315 assert_eq!(
1316 time_string(
1317 &BigInt::from(220000),
1318 &TimeScale {
1319 multiplier: Some(100),
1320 unit: TimeUnit::MicroSeconds
1321 },
1322 &TimeUnit::Auto,
1323 &TimeFormat {
1324 format: TimeStringFormatting::SI,
1325 show_space: true,
1326 show_unit: true
1327 }
1328 ),
1329 "22 s"
1330 );
1331 assert_eq!(
1332 time_string(
1333 &BigInt::from(22000),
1334 &TimeScale {
1335 multiplier: Some(10),
1336 unit: TimeUnit::Seconds
1337 },
1338 &TimeUnit::Auto,
1339 &TimeFormat {
1340 format: TimeStringFormatting::No,
1341 show_space: true,
1342 show_unit: true
1343 }
1344 ),
1345 "220000 s"
1346 );
1347 }
1348 #[test]
1349 fn print_time_none() {
1350 assert_eq!(
1351 time_string(
1352 &BigInt::from(2200),
1353 &TimeScale {
1354 multiplier: Some(1),
1355 unit: TimeUnit::MicroSeconds
1356 },
1357 &TimeUnit::None,
1358 &TimeFormat {
1359 format: TimeStringFormatting::No,
1360 show_space: true,
1361 show_unit: true
1362 }
1363 ),
1364 "2200"
1365 );
1366 assert_eq!(
1367 time_string(
1368 &BigInt::from(220),
1369 &TimeScale {
1370 multiplier: Some(10),
1371 unit: TimeUnit::MicroSeconds
1372 },
1373 &TimeUnit::None,
1374 &TimeFormat {
1375 format: TimeStringFormatting::No,
1376 show_space: true,
1377 show_unit: true
1378 }
1379 ),
1380 "220"
1381 );
1382 }
1383
1384 #[test]
1385 fn test_strip_trailing_zeros_and_period() {
1386 use crate::time::strip_trailing_zeros_and_period;
1387
1388 assert_eq!(strip_trailing_zeros_and_period("123.000"), "123");
1389 assert_eq!(strip_trailing_zeros_and_period("123.450"), "123.45");
1390 assert_eq!(strip_trailing_zeros_and_period("123.456"), "123.456");
1391 assert_eq!(strip_trailing_zeros_and_period("123."), "123");
1392 assert_eq!(strip_trailing_zeros_and_period("123"), "123");
1393 assert_eq!(strip_trailing_zeros_and_period("0.000"), "0");
1394 assert_eq!(strip_trailing_zeros_and_period("0.100"), "0.1");
1395 assert_eq!(strip_trailing_zeros_and_period(""), "");
1396 }
1397
1398 #[test]
1399 fn test_format_si() {
1400 use crate::time::format_si;
1401
1402 assert_eq!(format_si("1234.56"), "1234.56");
1404 assert_eq!(format_si("123.4"), "123.4");
1405
1406 assert_eq!(format_si("12345.67"), "12\u{2009}345.67");
1408 assert_eq!(format_si("1234567.89"), "1\u{2009}234\u{2009}567.89");
1409 assert_eq!(format_si("12345"), "12\u{2009}345");
1411 assert_eq!(format_si("123"), "123");
1412
1413 assert_eq!(format_si("0.123"), "0.123");
1415 assert_eq!(format_si(""), "");
1416
1417 assert_eq!(format_si("123.4567890"), "123.456\u{2009}789\u{2009}0");
1419 }
1420
1421 #[test]
1422 fn test_time_unit_exponent() {
1423 assert_eq!(TimeUnit::Seconds.exponent(), 0);
1425 assert_eq!(TimeUnit::MilliSeconds.exponent(), -3);
1426 assert_eq!(TimeUnit::MicroSeconds.exponent(), -6);
1427 assert_eq!(TimeUnit::NanoSeconds.exponent(), -9);
1428 assert_eq!(TimeUnit::PicoSeconds.exponent(), -12);
1429 assert_eq!(TimeUnit::FemtoSeconds.exponent(), -15);
1430 assert_eq!(TimeUnit::AttoSeconds.exponent(), -18);
1431 assert_eq!(TimeUnit::ZeptoSeconds.exponent(), -21);
1432
1433 for unit in [
1435 TimeUnit::Seconds,
1436 TimeUnit::MilliSeconds,
1437 TimeUnit::MicroSeconds,
1438 TimeUnit::NanoSeconds,
1439 TimeUnit::PicoSeconds,
1440 TimeUnit::FemtoSeconds,
1441 TimeUnit::AttoSeconds,
1442 TimeUnit::ZeptoSeconds,
1443 ] {
1444 assert_eq!(TimeUnit::from_exponent(unit.exponent()), Some(unit));
1445 }
1446
1447 assert_eq!(TimeUnit::from_exponent(-5), None);
1449 assert_eq!(TimeUnit::from_exponent(1), None);
1450 }
1451
1452 #[test]
1453 fn test_time_string_zero() {
1454 assert_eq!(
1456 time_string(
1457 &BigInt::from(0),
1458 &TimeScale {
1459 multiplier: Some(1),
1460 unit: TimeUnit::MicroSeconds
1461 },
1462 &TimeUnit::MicroSeconds,
1463 &TimeFormat::default()
1464 ),
1465 "0 μs"
1466 );
1467
1468 assert_eq!(
1469 time_string(
1470 &BigInt::from(0),
1471 &TimeScale {
1472 multiplier: Some(1),
1473 unit: TimeUnit::Seconds
1474 },
1475 &TimeUnit::Auto,
1476 &TimeFormat::default()
1477 ),
1478 "0 s"
1479 );
1480 }
1481
1482 #[test]
1483 fn test_time_string_large_numbers() {
1484 assert_eq!(
1486 time_string(
1487 &BigInt::from(999_999_999_999i64),
1488 &TimeScale {
1489 multiplier: Some(1),
1490 unit: TimeUnit::NanoSeconds
1491 },
1492 &TimeUnit::Seconds,
1493 &TimeFormat {
1494 format: TimeStringFormatting::SI,
1495 show_space: true,
1496 show_unit: true
1497 }
1498 ),
1499 "999.999\u{2009}999\u{2009}999 s"
1500 );
1501 }
1502
1503 #[test]
1504 fn test_time_string_no_multiplier() {
1505 assert_eq!(
1507 time_string(
1508 &BigInt::from(1234),
1509 &TimeScale {
1510 multiplier: None,
1511 unit: TimeUnit::NanoSeconds
1512 },
1513 &TimeUnit::NanoSeconds,
1514 &TimeFormat::default()
1515 ),
1516 "1234 ns"
1517 );
1518 }
1519
1520 #[test]
1521 fn test_time_format_variations() {
1522 let value = BigInt::from(123456);
1523 let scale = TimeScale {
1524 multiplier: Some(1),
1525 unit: TimeUnit::NanoSeconds,
1526 };
1527
1528 assert_eq!(
1530 time_string(
1531 &value,
1532 &scale,
1533 &TimeUnit::NanoSeconds,
1534 &TimeFormat {
1535 format: TimeStringFormatting::No,
1536 show_space: true,
1537 show_unit: true
1538 }
1539 ),
1540 "123456 ns"
1541 );
1542
1543 assert_eq!(
1544 time_string(
1545 &value,
1546 &scale,
1547 &TimeUnit::NanoSeconds,
1548 &TimeFormat {
1549 format: TimeStringFormatting::No,
1550 show_space: false,
1551 show_unit: true
1552 }
1553 ),
1554 "123456ns"
1555 );
1556
1557 assert_eq!(
1558 time_string(
1559 &value,
1560 &scale,
1561 &TimeUnit::NanoSeconds,
1562 &TimeFormat {
1563 format: TimeStringFormatting::No,
1564 show_space: true,
1565 show_unit: false
1566 }
1567 ),
1568 "123456"
1569 );
1570
1571 assert_eq!(
1572 time_string(
1573 &value,
1574 &scale,
1575 &TimeUnit::NanoSeconds,
1576 &TimeFormat {
1577 format: TimeStringFormatting::SI,
1578 show_space: true,
1579 show_unit: true
1580 }
1581 ),
1582 "123\u{2009}456 ns"
1583 );
1584 }
1585
1586 #[test]
1587 fn test_find_auto_scale_seconds_passthrough() {
1588 use crate::time::find_auto_scale;
1589
1590 let ts = TimeScale {
1591 unit: TimeUnit::Seconds,
1592 multiplier: Some(1),
1593 };
1594 assert_eq!(find_auto_scale(&BigInt::from(1), &ts), TimeUnit::Seconds);
1595 assert_eq!(
1596 find_auto_scale(&BigInt::from(1_234_567), &ts),
1597 TimeUnit::Seconds
1598 );
1599 }
1600
1601 #[test]
1602 fn test_find_auto_scale_nanoseconds() {
1603 use crate::time::find_auto_scale;
1604
1605 let ts = TimeScale {
1606 unit: TimeUnit::NanoSeconds,
1607 multiplier: Some(1),
1608 };
1609
1610 assert_eq!(
1612 find_auto_scale(&BigInt::from(1_000_000_000i64), &ts),
1613 TimeUnit::Seconds
1614 );
1615 assert_eq!(
1617 find_auto_scale(&BigInt::from(1_000_000), &ts),
1618 TimeUnit::MilliSeconds
1619 );
1620 assert_eq!(
1622 find_auto_scale(&BigInt::from(1_000), &ts),
1623 TimeUnit::MicroSeconds
1624 );
1625 assert_eq!(
1627 find_auto_scale(&BigInt::from(1234), &ts),
1628 TimeUnit::NanoSeconds
1629 );
1630 }
1631
1632 #[test]
1633 fn test_find_auto_scale_microseconds_with_multiplier() {
1634 use crate::time::find_auto_scale;
1635
1636 let ts_none = TimeScale {
1638 unit: TimeUnit::MicroSeconds,
1639 multiplier: None,
1640 };
1641 assert_eq!(
1642 find_auto_scale(&BigInt::from(1_000_000), &ts_none),
1643 TimeUnit::Seconds
1644 );
1645 assert_eq!(
1646 find_auto_scale(&BigInt::from(1_000), &ts_none),
1647 TimeUnit::MilliSeconds
1648 );
1649 assert_eq!(
1650 find_auto_scale(&BigInt::from(123), &ts_none),
1651 TimeUnit::MicroSeconds
1652 );
1653
1654 let ts_mul10 = TimeScale {
1656 unit: TimeUnit::MicroSeconds,
1657 multiplier: Some(10),
1658 };
1659 assert_eq!(
1660 find_auto_scale(&BigInt::from(100_000), &ts_mul10),
1661 TimeUnit::Seconds
1662 );
1663 assert_eq!(
1664 find_auto_scale(&BigInt::from(100), &ts_mul10),
1665 TimeUnit::MilliSeconds
1666 );
1667 assert_eq!(
1668 find_auto_scale(&BigInt::from(123), &ts_mul10),
1669 TimeUnit::MicroSeconds
1670 );
1671 }
1672
1673 #[test]
1674 fn test_find_auto_scale_femtoseconds() {
1675 use crate::time::find_auto_scale;
1676
1677 let ts = TimeScale {
1678 unit: TimeUnit::FemtoSeconds,
1679 multiplier: Some(1),
1680 };
1681 assert_eq!(
1683 find_auto_scale(&BigInt::from(10_i128.pow(15)), &ts),
1684 TimeUnit::Seconds
1685 );
1686 assert_eq!(
1688 find_auto_scale(&BigInt::from(10_i128.pow(12)), &ts),
1689 TimeUnit::MilliSeconds
1690 );
1691 assert_eq!(
1693 find_auto_scale(&BigInt::from(10_i128.pow(9)), &ts),
1694 TimeUnit::MicroSeconds
1695 );
1696 assert_eq!(
1698 find_auto_scale(&BigInt::from(10_i128.pow(6)), &ts),
1699 TimeUnit::NanoSeconds
1700 );
1701 assert_eq!(
1703 find_auto_scale(&BigInt::from(10_i128.pow(3)), &ts),
1704 TimeUnit::PicoSeconds
1705 );
1706 assert_eq!(
1708 find_auto_scale(&BigInt::from(1), &ts),
1709 TimeUnit::FemtoSeconds
1710 );
1711 }
1712
1713 #[test]
1714 fn test_locale_cache_en_us() {
1715 use crate::time::{create_cache, format_locale};
1716 use pure_rust_locales::Locale;
1717
1718 let locale = Locale::en_US;
1719 let cache = create_cache(locale);
1720
1721 let result = format_locale("1234567.89", &cache);
1723 assert_eq!(result, "1,234,567.89");
1724 }
1725
1726 #[test]
1727 fn test_locale_cache_de_de() {
1728 use crate::time::{create_cache, format_locale};
1729 use pure_rust_locales::Locale;
1730
1731 let locale = Locale::de_DE;
1732 let cache = create_cache(locale);
1733
1734 let result = format_locale("1234567.89", &cache);
1735 assert_eq!(result, "1.234.567,89");
1736 }
1737
1738 #[test]
1739 fn test_locale_cache_fr_fr() {
1740 use crate::time::{create_cache, format_locale};
1741 use pure_rust_locales::Locale;
1742
1743 let locale = Locale::fr_FR;
1744 let cache = create_cache(locale);
1745
1746 let result = format_locale("1234567.89", &cache);
1748 assert_eq!(result, "1\u{2009}234\u{2009}567,89");
1750 }
1751
1752 #[test]
1753 fn test_locale_cache_small_numbers() {
1754 use crate::time::{create_cache, format_locale};
1755 use pure_rust_locales::Locale;
1756
1757 let locale = Locale::en_US;
1758 let cache = create_cache(locale);
1759
1760 assert_eq!(format_locale("123", &cache), "123");
1762 assert_eq!(format_locale("12.34", &cache), "12.34");
1763 assert_eq!(format_locale("0", &cache), "0");
1764 }
1765
1766 #[test]
1767 fn test_locale_cache_consistency_across_locales() {
1768 use crate::time::create_cache;
1769 use pure_rust_locales::Locale;
1770
1771 let cache1 = create_cache(Locale::en_US);
1773 let cache2 = create_cache(Locale::en_US);
1774
1775 assert_eq!(cache1.thousands_sep, cache2.thousands_sep);
1776 assert_eq!(cache1.decimal_point, cache2.decimal_point);
1777 assert_eq!(cache1.grouping, cache2.grouping);
1778 }
1779
1780 #[test]
1781 fn test_create_cache_from_various_locales() {
1782 use crate::time::{create_cache, format_locale};
1783 use pure_rust_locales::Locale;
1784
1785 let locales = vec![
1787 Locale::en_US,
1788 Locale::de_DE,
1789 Locale::fr_FR,
1790 Locale::es_ES,
1791 Locale::it_IT,
1792 Locale::pt_BR,
1793 Locale::pt_PT,
1794 Locale::ja_JP,
1795 Locale::zh_CN,
1796 Locale::zh_TW,
1797 Locale::ru_RU,
1798 Locale::ko_KR,
1799 Locale::pl_PL,
1800 Locale::tr_TR,
1801 Locale::nl_NL,
1802 Locale::sv_SE,
1803 Locale::da_DK,
1804 Locale::fi_FI,
1805 Locale::el_GR,
1806 Locale::hu_HU,
1807 Locale::cs_CZ,
1808 Locale::ro_RO,
1809 Locale::th_TH,
1810 Locale::vi_VN,
1811 Locale::ar_SA,
1812 Locale::he_IL,
1813 Locale::id_ID,
1814 Locale::uk_UA,
1815 Locale::en_GB,
1816 Locale::en_AU,
1817 Locale::en_CA,
1818 Locale::en_NZ,
1819 Locale::en_IN,
1820 Locale::fr_CA,
1821 Locale::de_AT,
1822 Locale::de_CH,
1823 Locale::fr_CH,
1824 Locale::it_CH,
1825 Locale::es_MX,
1826 Locale::es_AR,
1827 ];
1828
1829 for locale in locales {
1830 let cache = create_cache(locale);
1831 assert!(
1833 !format_locale("1234567.89", &cache).is_empty(),
1834 "Failed for {locale:?}"
1835 );
1836 }
1837 }
1838}
1839
1840#[cfg(test)]
1841mod get_ticks_tests {
1842 use super::*;
1843 use itertools::Itertools;
1844 use num::BigInt;
1845
1846 #[test]
1849 fn get_ticks_basic() {
1850 let vp = crate::viewport::Viewport::default();
1851 let timescale = TimeScale {
1852 unit: TimeUnit::MicroSeconds,
1853 multiplier: Some(1),
1854 };
1855 let frame_width = 800.0_f32;
1856 let text_size = 12.0_f32;
1857 let wanted = TimeUnit::MicroSeconds;
1858 let time_format = TimeFormat::default();
1859 let config = crate::config::SurferConfig::default();
1860 let max_timestamp = BigInt::from(1_000_000i64);
1861
1862 let ticks = get_ticks_internal(
1863 &vp,
1864 ×cale,
1865 frame_width,
1866 text_size,
1867 &wanted,
1868 &time_format,
1869 config.theme.ticks.density,
1870 &max_timestamp,
1871 &BigInt::from(0),
1872 );
1873
1874 assert!(!ticks.is_empty(), "expected at least one tick");
1875
1876 let mut last_x = -1.0_f32;
1878 let mut labels: Vec<String> = Vec::with_capacity(ticks.len());
1879 for (label, x, _) in &ticks {
1880 assert!(
1881 *x >= last_x,
1882 "tick x not monotonic: {x} < {last_x} for label {label}"
1883 );
1884 last_x = *x;
1885 assert!(*x >= 0.0, "tick x < 0: {x}");
1886 assert!(
1887 *x <= frame_width,
1888 "tick x > frame_width: {x} > {frame_width}"
1889 );
1890 labels.push(label.clone());
1891 }
1892 let unique_labels = labels.iter().unique().count();
1894 assert_eq!(labels.len(), unique_labels, "duplicate tick labels found");
1895 }
1896
1897 #[test]
1900 fn get_ticks_respects_frame_width_and_density() {
1901 let mut vp = crate::viewport::Viewport::default();
1902 vp.curr_left = crate::viewport::Relative(0.0);
1904 vp.curr_right = crate::viewport::Relative(0.1);
1905
1906 let timescale = TimeScale {
1907 unit: TimeUnit::NanoSeconds,
1908 multiplier: Some(1),
1909 };
1910 let frame_width = 200.0_f32;
1911 let text_size = 10.0_f32;
1912 let wanted = TimeUnit::Auto;
1913 let time_format = TimeFormat {
1914 format: TimeStringFormatting::SI,
1915 show_space: true,
1916 show_unit: true,
1917 };
1918
1919 let mut config = crate::config::SurferConfig::default();
1920 config.theme.ticks.density = 1.0;
1922
1923 let max_timestamp = BigInt::from(1_000_000i64);
1924
1925 let ticks = get_ticks_internal(
1926 &vp,
1927 ×cale,
1928 frame_width,
1929 text_size,
1930 &wanted,
1931 &time_format,
1932 config.theme.ticks.density,
1933 &max_timestamp,
1934 &BigInt::from(0),
1935 );
1936
1937 assert!(!ticks.is_empty(), "expected ticks even for narrow view");
1938 assert!(ticks.len() < 200, "too many ticks: {}", ticks.len());
1940
1941 let mut last_x = -1.0_f32;
1943 let mut labels: Vec<String> = Vec::with_capacity(ticks.len());
1944 for (label, x, _) in &ticks {
1945 assert!(
1946 *x >= last_x,
1947 "tick x not monotonic: {x} < {last_x} for label {label}"
1948 );
1949 last_x = *x;
1950 assert!(*x >= 0.0, "tick x < 0: {x}");
1951 assert!(
1952 *x <= frame_width,
1953 "tick x > frame_width: {x} > {frame_width}"
1954 );
1955 labels.push(label.clone());
1956 }
1957 let unique_labels = labels.iter().unique().count();
1958 assert_eq!(labels.len(), unique_labels, "duplicate tick labels found");
1959 }
1960}
1961
1962#[cfg(test)]
1963mod time_input_tests {
1964 use super::*;
1965
1966 #[test]
1967 fn test_parse_time_input_simple() {
1968 let (num, unit) = parse_time_input("100");
1969 assert_eq!(num, "100");
1970 assert_eq!(unit, None);
1971 }
1972
1973 #[test]
1974 fn test_parse_time_input_unit_only_no_panic() {
1975 let (num, unit) = parse_time_input("ns");
1977 assert_eq!(num, "ns");
1978 assert_eq!(unit, None);
1979 }
1980
1981 #[test]
1982 fn test_parse_time_input_with_unit_no_space() {
1983 let (num, unit) = parse_time_input("100ns");
1984 assert_eq!(num, "100");
1985 assert_eq!(unit, Some(TimeUnit::NanoSeconds));
1986
1987 let (num, unit) = parse_time_input("50ps");
1988 assert_eq!(num, "50");
1989 assert_eq!(unit, Some(TimeUnit::PicoSeconds));
1990
1991 let (num, unit) = parse_time_input("1.5ms");
1992 assert_eq!(num, "1.5");
1993 assert_eq!(unit, Some(TimeUnit::MilliSeconds));
1994 }
1995
1996 #[test]
1997 fn test_parse_time_input_with_unit_space() {
1998 let (num, unit) = parse_time_input("100 ns");
1999 assert_eq!(num, "100");
2000 assert_eq!(unit, Some(TimeUnit::NanoSeconds));
2001
2002 let (num, unit) = parse_time_input("1.5 ms");
2003 assert_eq!(num, "1.5");
2004 assert_eq!(unit, Some(TimeUnit::MilliSeconds));
2005
2006 let (num, unit) = parse_time_input("100\tms");
2007 assert_eq!(num, "100");
2008 assert_eq!(unit, Some(TimeUnit::MilliSeconds));
2009
2010 let (num, unit) = parse_time_input("100 ns");
2011 assert_eq!(num, "100");
2012 assert_eq!(unit, Some(TimeUnit::NanoSeconds));
2013 }
2014
2015 #[test]
2016 fn test_parse_time_input_microseconds_unicode() {
2017 let (num, unit) = parse_time_input("100μs");
2018 assert_eq!(num, "100");
2019 assert_eq!(unit, Some(TimeUnit::MicroSeconds));
2020
2021 let (num, unit) = parse_time_input("50 μs");
2022 assert_eq!(num, "50");
2023 assert_eq!(unit, Some(TimeUnit::MicroSeconds));
2024 }
2025
2026 #[test]
2027 fn test_parse_time_input_microseconds_ascii() {
2028 let (num, unit) = parse_time_input("100us");
2030 assert_eq!(num, "100");
2031 assert_eq!(unit, Some(TimeUnit::MicroSeconds));
2032 }
2033
2034 #[test]
2035 fn test_parse_time_input_seconds() {
2036 let (num, unit) = parse_time_input("10s");
2037 assert_eq!(num, "10");
2038 assert_eq!(unit, Some(TimeUnit::Seconds));
2039
2040 let (num, unit) = parse_time_input("0.5s");
2041 assert_eq!(num, "0.5");
2042 assert_eq!(unit, Some(TimeUnit::Seconds));
2043 }
2044
2045 #[test]
2046 fn test_parse_time_input_femtoseconds() {
2047 let (num, unit) = parse_time_input("1000000fs");
2048 assert_eq!(num, "1000000");
2049 assert_eq!(unit, Some(TimeUnit::FemtoSeconds));
2050 }
2051
2052 #[test]
2053 fn test_parse_time_input_with_whitespace() {
2054 let (num, unit) = parse_time_input(" 100ns ");
2055 assert_eq!(num, "100");
2056 assert_eq!(unit, Some(TimeUnit::NanoSeconds));
2057 }
2058
2059 #[test]
2060 fn test_split_numeric_parts() {
2061 assert_eq!(
2062 split_numeric_parts("100").ok(),
2063 Some(("100".to_string(), String::new()))
2064 );
2065 assert_eq!(
2066 split_numeric_parts("100.").ok(),
2067 Some(("100".to_string(), String::new()))
2068 );
2069 assert_eq!(
2070 split_numeric_parts(".5").ok(),
2071 Some(("0".to_string(), "5".to_string()))
2072 );
2073 assert_eq!(
2074 split_numeric_parts("1.5").ok(),
2075 Some(("1".to_string(), "5".to_string()))
2076 );
2077 }
2078
2079 #[test]
2080 fn test_split_numeric_parts_invalid() {
2081 assert!(split_numeric_parts("").is_err());
2082 assert!(split_numeric_parts("abc").is_err());
2083 assert!(split_numeric_parts("12.34.56").is_err());
2084 assert!(split_numeric_parts("-1").is_err());
2085 }
2086
2087 #[test]
2088 fn test_normalize_numeric_with_unit_integer() {
2089 let (val, unit) = normalize_numeric_with_unit("100", TimeUnit::NanoSeconds).unwrap();
2090 assert_eq!(val, BigInt::from(100));
2091 assert_eq!(unit, TimeUnit::NanoSeconds);
2092 }
2093
2094 #[test]
2095 fn test_normalize_numeric_with_unit_decimal_single_step() {
2096 let (val, unit) = normalize_numeric_with_unit("1.5", TimeUnit::NanoSeconds).unwrap();
2097 assert_eq!(val, BigInt::from(1500));
2098 assert_eq!(unit, TimeUnit::PicoSeconds);
2099 }
2100
2101 #[test]
2102 fn test_normalize_numeric_with_unit_decimal_multi_step() {
2103 let (val, unit) = normalize_numeric_with_unit("1.2345", TimeUnit::NanoSeconds).unwrap();
2104 assert_eq!(val, BigInt::from(1_234_500));
2105 assert_eq!(unit, TimeUnit::FemtoSeconds);
2106 }
2107
2108 #[test]
2109 fn test_time_input_state_default() {
2110 let state = TimeInputState::default();
2111 assert_eq!(state.input_text, "");
2112 assert_eq!(state.parsed_value, None);
2113 assert_eq!(state.input_unit, None);
2114 assert_eq!(state.normalized_unit, None);
2115 assert_eq!(state.selected_unit, TimeUnit::NanoSeconds);
2116 assert_eq!(state.error, None);
2117 }
2118
2119 #[test]
2120 fn test_time_input_state_update_valid() {
2121 let mut state = TimeInputState::new();
2122 state.update_input("100ns".to_string());
2123
2124 assert_eq!(state.input_text, "100ns");
2125 assert_eq!(state.parsed_value, Some(BigInt::from(100)));
2126 assert_eq!(state.input_unit, Some(TimeUnit::NanoSeconds));
2127 assert_eq!(state.normalized_unit, Some(TimeUnit::NanoSeconds));
2128 assert_eq!(state.error, None);
2129 }
2130
2131 #[test]
2132 fn test_time_input_state_update_invalid() {
2133 let mut state = TimeInputState::new();
2134 state.update_input("abc".to_string());
2135
2136 assert_eq!(state.parsed_value, None);
2137 assert!(state.error.is_some());
2138 }
2139
2140 #[test]
2141 fn test_time_input_state_update_decimal_unit_normalization() {
2142 let mut state = TimeInputState::new();
2143 state.update_input("1.5ns".to_string());
2144
2145 assert_eq!(state.parsed_value, Some(BigInt::from(1500)));
2146 assert_eq!(state.input_unit, Some(TimeUnit::NanoSeconds));
2147 assert_eq!(state.normalized_unit, Some(TimeUnit::PicoSeconds));
2148 assert_eq!(state.effective_unit(), TimeUnit::PicoSeconds);
2149 assert_eq!(state.error, None);
2150 }
2151
2152 #[test]
2153 fn test_time_input_state_to_timescale_ticks_invalid() {
2154 let state = TimeInputState::new();
2155 let timescale = TimeScale {
2156 unit: TimeUnit::NanoSeconds,
2157 multiplier: None,
2158 };
2159
2160 assert_eq!(state.to_timescale_ticks(×cale), None);
2161 }
2162
2163 #[test]
2164 fn test_time_input_comprehensive_example() {
2165 let mut state = TimeInputState::new();
2167 state.update_input("2.5 ms".to_string());
2168
2169 assert_eq!(state.parsed_value, Some(BigInt::from(2500)));
2171 assert_eq!(state.input_unit, Some(TimeUnit::MilliSeconds));
2172 assert_eq!(state.effective_unit(), TimeUnit::MicroSeconds);
2173 assert_eq!(state.error, None);
2174
2175 let timescale = TimeScale {
2177 unit: TimeUnit::MicroSeconds,
2178 multiplier: None,
2179 };
2180 assert_eq!(
2181 state.to_timescale_ticks(×cale),
2182 Some(BigInt::from(2500))
2183 );
2184 }
2185
2186 #[test]
2187 fn test_parse_time_longest_match_first() {
2188 let (num, unit) = parse_time_input("100ms");
2190 assert_eq!(num, "100");
2191 assert_eq!(unit, Some(TimeUnit::MilliSeconds));
2192
2193 let (_, unit) = parse_time_input("100s");
2195 assert_ne!(unit, Some(TimeUnit::MilliSeconds));
2196 }
2197
2198 #[test]
2199 fn test_parse_time_no_false_positives() {
2200 let (num, unit) = parse_time_input("mass");
2202 assert_eq!(num, "mass");
2203 assert_eq!(unit, None);
2204
2205 let (num, unit) = parse_time_input("uses");
2206 assert_eq!(num, "uses");
2207 assert_eq!(unit, None);
2208 }
2209
2210 #[test]
2211 fn test_time_input_state_clear() {
2212 let mut state = TimeInputState::new();
2213 state.update_input("100ns".to_string());
2214 assert!(state.parsed_value.is_some());
2215
2216 state.update_input(String::new());
2218 assert!(state.error.is_some());
2219 }
2220
2221 fn ns_timescale() -> TimeScale {
2224 TimeScale {
2225 unit: TimeUnit::NanoSeconds,
2226 multiplier: None,
2227 }
2228 }
2229
2230 fn ps_timescale() -> TimeScale {
2231 TimeScale {
2232 unit: TimeUnit::PicoSeconds,
2233 multiplier: None,
2234 }
2235 }
2236
2237 #[test]
2238 fn test_parse_time_string_to_ticks_plain_integer() {
2239 let ts = ns_timescale();
2241 assert_eq!(
2242 parse_time_string_to_ticks("100", &ts),
2243 Some(BigInt::from(100))
2244 );
2245 assert_eq!(parse_time_string_to_ticks("0", &ts), Some(BigInt::from(0)));
2246 }
2247
2248 #[test]
2249 fn test_parse_time_string_to_ticks_same_unit() {
2250 let ts = ns_timescale();
2252 assert_eq!(
2253 parse_time_string_to_ticks("100ns", &ts),
2254 Some(BigInt::from(100))
2255 );
2256 assert_eq!(
2257 parse_time_string_to_ticks("100 ns", &ts),
2258 Some(BigInt::from(100))
2259 );
2260 }
2261
2262 #[test]
2263 fn test_parse_time_string_to_ticks_coarser_unit() {
2264 let ts = ns_timescale();
2266 assert_eq!(
2267 parse_time_string_to_ticks("1us", &ts),
2268 Some(BigInt::from(1000))
2269 );
2270 assert_eq!(
2271 parse_time_string_to_ticks("1μs", &ts),
2272 Some(BigInt::from(1000))
2273 );
2274 assert_eq!(
2276 parse_time_string_to_ticks("1ms", &ts),
2277 Some(BigInt::from(1_000_000))
2278 );
2279 assert_eq!(
2281 parse_time_string_to_ticks("1s", &ts),
2282 Some(BigInt::from(1_000_000_000))
2283 );
2284 }
2285
2286 #[test]
2287 fn test_parse_time_string_to_ticks_finer_unit() {
2288 let ts = ns_timescale();
2290 assert_eq!(
2291 parse_time_string_to_ticks("1ps", &ts),
2292 Some(BigInt::from(0))
2293 );
2294 assert_eq!(
2296 parse_time_string_to_ticks("1000ps", &ts),
2297 Some(BigInt::from(1))
2298 );
2299 }
2300
2301 #[test]
2302 fn test_parse_time_string_to_ticks_decimal() {
2303 let ts = ns_timescale();
2305 assert_eq!(
2306 parse_time_string_to_ticks("1.5us", &ts),
2307 Some(BigInt::from(1500))
2308 );
2309 let ts_ps = ps_timescale();
2311 assert_eq!(
2312 parse_time_string_to_ticks("0.5ns", &ts_ps),
2313 Some(BigInt::from(500))
2314 );
2315 }
2316
2317 #[test]
2318 fn test_parse_time_string_to_ticks_with_whitespace() {
2319 let ts = ns_timescale();
2320 assert_eq!(
2321 parse_time_string_to_ticks(" 100 ns ", &ts),
2322 Some(BigInt::from(100))
2323 );
2324 }
2325
2326 #[test]
2327 fn test_parse_time_string_to_ticks_invalid() {
2328 let ts = ns_timescale();
2329 assert_eq!(parse_time_string_to_ticks("abc", &ts), None);
2330 assert_eq!(parse_time_string_to_ticks("", &ts), None);
2331 assert_eq!(parse_time_string_to_ticks("-5ns", &ts), None);
2332 }
2333
2334 #[test]
2335 fn test_parse_time_string_to_ticks_with_multiplier() {
2336 let ts = TimeScale {
2338 unit: TimeUnit::NanoSeconds,
2339 multiplier: Some(10),
2340 };
2341 assert_eq!(
2342 parse_time_string_to_ticks("100ns", &ts),
2343 Some(BigInt::from(10))
2344 );
2345 }
2346
2347 #[test]
2348 fn test_parse_time_string_to_ticks_ps_timescale() {
2349 let ts = ps_timescale();
2351 assert_eq!(
2352 parse_time_string_to_ticks("1ns", &ts),
2353 Some(BigInt::from(1000))
2354 );
2355 assert_eq!(
2357 parse_time_string_to_ticks("42", &ts),
2358 Some(BigInt::from(42))
2359 );
2360 }
2361}