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::{TimeRange, 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(u32::from(exp)),
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 range = waves.time_range();
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 range,
971 )
972 }
973}
974
975#[allow(clippy::too_many_arguments)]
979#[must_use]
980fn get_ticks_internal(
981 viewport: &Viewport,
982 timescale: &TimeScale,
983 frame_width: f32,
984 text_size: f32,
985 wanted_timeunit: &TimeUnit,
986 time_format: &TimeFormat,
987 density: f32,
988 range: &TimeRange,
989) -> Vec<(String, f32, i64)> {
990 let char_width = text_size * (20. / 31.);
991 let rightexp = viewport
992 .curr_right
993 .absolute(range)
994 .inner()
995 .abs()
996 .log10()
997 .round() as i16;
998 let leftexp = viewport
999 .curr_left
1000 .absolute(range)
1001 .inner()
1002 .abs()
1003 .log10()
1004 .round() as i16;
1005 let max_labelwidth = f32::from(rightexp.max(leftexp) + 3) * char_width;
1006 let max_labels = ((frame_width * density) / max_labelwidth).floor() + 2.;
1007 let viewport_width = viewport.width_absolute(range);
1008 let scale = 10.0f64.powf(
1009 (viewport_width.inner() / f64::from(max_labels))
1010 .log10()
1011 .floor(),
1012 );
1013
1014 let mut ticks: Vec<(String, f32, i64)> = [].to_vec();
1015 for step in &TICK_STEPS {
1016 let scaled_step = scale * step;
1017 let left_abs = viewport.curr_left.absolute(range).inner();
1018 let right_abs = viewport.curr_right.absolute(range).inner();
1019 let rounded_min_label_time = (left_abs / scaled_step).floor() * scaled_step;
1020 let high = ((right_abs - rounded_min_label_time) / scaled_step).ceil() as f32 + 1.;
1021
1022 if high <= max_labels {
1023 let time_formatter = TimeFormatter::new(timescale, wanted_timeunit, time_format);
1024 ticks = (0..high as i16)
1025 .map(|v| {
1026 BigInt::from((f64::from(v) * scaled_step + rounded_min_label_time) as i128)
1027 })
1028 .unique()
1029 .map(|tick| {
1030 (
1031 time_formatter.format(&tick),
1033 viewport.pixel_from_time(&tick, frame_width, range),
1035 tick.to_i64().unwrap_or_default(),
1037 )
1038 })
1039 .collect::<Vec<(String, f32, i64)>>();
1040 break;
1041 }
1042 }
1043 ticks
1044}
1045
1046pub(crate) fn parse_time_string_to_ticks(input: &str, timescale: &TimeScale) -> Option<BigInt> {
1053 let (numeric_str, unit_opt) = parse_time_input(input);
1054
1055 let base_unit = unit_opt.unwrap_or(TimeUnit::None);
1056 let (value, normalized_unit) = normalize_numeric_with_unit(&numeric_str, base_unit).ok()?;
1057
1058 if normalized_unit == TimeUnit::None {
1059 return Some(value);
1061 }
1062
1063 let unit_exp = normalized_unit.exponent();
1064 let data_exp = timescale.unit.exponent();
1065 let diff = unit_exp - data_exp;
1066
1067 scale_time(&value, diff, timescale)
1068}
1069
1070fn scale_time(value: &BigInt, exponent_diff: i8, timescale: &TimeScale) -> Option<BigInt> {
1071 let exponent_diff = exponent_diff - timescale.multiplier_digits() as i8;
1072 let result = if exponent_diff > 0 {
1073 let scale = pow10(exponent_diff as u8);
1074 value * scale
1075 } else if exponent_diff < 0 {
1076 let scale = pow10((-exponent_diff) as u8);
1077 value / scale
1078 } else {
1079 value.clone()
1080 };
1081
1082 Some(result)
1083}
1084
1085#[cfg(test)]
1086mod test {
1087 use num::BigInt;
1088
1089 use crate::time::{TimeFormat, TimeScale, TimeStringFormatting, TimeUnit, time_string};
1090
1091 #[test]
1092 fn print_time_standard() {
1093 assert_eq!(
1094 time_string(
1095 &BigInt::from(103),
1096 &TimeScale {
1097 multiplier: Some(1),
1098 unit: TimeUnit::FemtoSeconds
1099 },
1100 &TimeUnit::FemtoSeconds,
1101 &TimeFormat::default()
1102 ),
1103 "103 fs"
1104 );
1105 assert_eq!(
1106 time_string(
1107 &BigInt::from(2200),
1108 &TimeScale {
1109 multiplier: Some(1),
1110 unit: TimeUnit::MicroSeconds
1111 },
1112 &TimeUnit::MicroSeconds,
1113 &TimeFormat::default()
1114 ),
1115 "2200 μs"
1116 );
1117 assert_eq!(
1118 time_string(
1119 &BigInt::from(2200),
1120 &TimeScale {
1121 multiplier: Some(1),
1122 unit: TimeUnit::MicroSeconds
1123 },
1124 &TimeUnit::MilliSeconds,
1125 &TimeFormat::default()
1126 ),
1127 "2.2 ms"
1128 );
1129 assert_eq!(
1130 time_string(
1131 &BigInt::from(2200),
1132 &TimeScale {
1133 multiplier: Some(1),
1134 unit: TimeUnit::MicroSeconds
1135 },
1136 &TimeUnit::NanoSeconds,
1137 &TimeFormat::default()
1138 ),
1139 "2200000 ns"
1140 );
1141 assert_eq!(
1142 time_string(
1143 &BigInt::from(2200),
1144 &TimeScale {
1145 multiplier: Some(1),
1146 unit: TimeUnit::NanoSeconds
1147 },
1148 &TimeUnit::PicoSeconds,
1149 &TimeFormat {
1150 format: TimeStringFormatting::No,
1151 show_space: false,
1152 show_unit: true
1153 }
1154 ),
1155 "2200000ps"
1156 );
1157 assert_eq!(
1158 time_string(
1159 &BigInt::from(2200),
1160 &TimeScale {
1161 multiplier: Some(10),
1162 unit: TimeUnit::MicroSeconds
1163 },
1164 &TimeUnit::MicroSeconds,
1165 &TimeFormat {
1166 format: TimeStringFormatting::No,
1167 show_space: false,
1168 show_unit: false
1169 }
1170 ),
1171 "22000"
1172 );
1173 }
1174 #[test]
1175 fn print_time_si() {
1176 assert_eq!(
1177 time_string(
1178 &BigInt::from(123456789010i128),
1179 &TimeScale {
1180 multiplier: Some(1),
1181 unit: TimeUnit::MicroSeconds
1182 },
1183 &TimeUnit::Seconds,
1184 &TimeFormat {
1185 format: TimeStringFormatting::SI,
1186 show_space: true,
1187 show_unit: true
1188 }
1189 ),
1190 "123\u{2009}456.789\u{2009}01 s"
1191 );
1192 assert_eq!(
1193 time_string(
1194 &BigInt::from(1456789100i128),
1195 &TimeScale {
1196 multiplier: Some(1),
1197 unit: TimeUnit::MicroSeconds
1198 },
1199 &TimeUnit::Seconds,
1200 &TimeFormat {
1201 format: TimeStringFormatting::SI,
1202 show_space: true,
1203 show_unit: true
1204 }
1205 ),
1206 "1456.7891 s"
1207 );
1208 assert_eq!(
1209 time_string(
1210 &BigInt::from(2200),
1211 &TimeScale {
1212 multiplier: Some(1),
1213 unit: TimeUnit::MicroSeconds
1214 },
1215 &TimeUnit::MicroSeconds,
1216 &TimeFormat {
1217 format: TimeStringFormatting::SI,
1218 show_space: true,
1219 show_unit: true
1220 }
1221 ),
1222 "2200 μs"
1223 );
1224 assert_eq!(
1225 time_string(
1226 &BigInt::from(22200),
1227 &TimeScale {
1228 multiplier: Some(1),
1229 unit: TimeUnit::MicroSeconds
1230 },
1231 &TimeUnit::MicroSeconds,
1232 &TimeFormat {
1233 format: TimeStringFormatting::SI,
1234 show_space: true,
1235 show_unit: true
1236 }
1237 ),
1238 "22\u{2009}200 μs"
1239 );
1240 }
1241 #[test]
1242 fn print_time_auto() {
1243 assert_eq!(
1244 time_string(
1245 &BigInt::from(2200),
1246 &TimeScale {
1247 multiplier: Some(1),
1248 unit: TimeUnit::MicroSeconds
1249 },
1250 &TimeUnit::Auto,
1251 &TimeFormat {
1252 format: TimeStringFormatting::SI,
1253 show_space: true,
1254 show_unit: true
1255 }
1256 ),
1257 "2200 μs"
1258 );
1259 assert_eq!(
1260 time_string(
1261 &BigInt::from(22000),
1262 &TimeScale {
1263 multiplier: Some(1),
1264 unit: TimeUnit::MicroSeconds
1265 },
1266 &TimeUnit::Auto,
1267 &TimeFormat {
1268 format: TimeStringFormatting::SI,
1269 show_space: true,
1270 show_unit: true
1271 }
1272 ),
1273 "22 ms"
1274 );
1275 assert_eq!(
1276 time_string(
1277 &BigInt::from(1500000000),
1278 &TimeScale {
1279 multiplier: Some(1),
1280 unit: TimeUnit::PicoSeconds
1281 },
1282 &TimeUnit::Auto,
1283 &TimeFormat {
1284 format: TimeStringFormatting::SI,
1285 show_space: true,
1286 show_unit: true
1287 }
1288 ),
1289 "1500 μs"
1290 );
1291 assert_eq!(
1292 time_string(
1293 &BigInt::from(22000),
1294 &TimeScale {
1295 multiplier: Some(10),
1296 unit: TimeUnit::MicroSeconds
1297 },
1298 &TimeUnit::Auto,
1299 &TimeFormat {
1300 format: TimeStringFormatting::SI,
1301 show_space: true,
1302 show_unit: true
1303 }
1304 ),
1305 "220 ms"
1306 );
1307 assert_eq!(
1308 time_string(
1309 &BigInt::from(220000),
1310 &TimeScale {
1311 multiplier: Some(100),
1312 unit: TimeUnit::MicroSeconds
1313 },
1314 &TimeUnit::Auto,
1315 &TimeFormat {
1316 format: TimeStringFormatting::SI,
1317 show_space: true,
1318 show_unit: true
1319 }
1320 ),
1321 "22 s"
1322 );
1323 assert_eq!(
1324 time_string(
1325 &BigInt::from(22000),
1326 &TimeScale {
1327 multiplier: Some(10),
1328 unit: TimeUnit::Seconds
1329 },
1330 &TimeUnit::Auto,
1331 &TimeFormat {
1332 format: TimeStringFormatting::No,
1333 show_space: true,
1334 show_unit: true
1335 }
1336 ),
1337 "220000 s"
1338 );
1339 }
1340 #[test]
1341 fn print_time_none() {
1342 assert_eq!(
1343 time_string(
1344 &BigInt::from(2200),
1345 &TimeScale {
1346 multiplier: Some(1),
1347 unit: TimeUnit::MicroSeconds
1348 },
1349 &TimeUnit::None,
1350 &TimeFormat {
1351 format: TimeStringFormatting::No,
1352 show_space: true,
1353 show_unit: true
1354 }
1355 ),
1356 "2200"
1357 );
1358 assert_eq!(
1359 time_string(
1360 &BigInt::from(220),
1361 &TimeScale {
1362 multiplier: Some(10),
1363 unit: TimeUnit::MicroSeconds
1364 },
1365 &TimeUnit::None,
1366 &TimeFormat {
1367 format: TimeStringFormatting::No,
1368 show_space: true,
1369 show_unit: true
1370 }
1371 ),
1372 "220"
1373 );
1374 }
1375
1376 #[test]
1377 fn test_strip_trailing_zeros_and_period() {
1378 use crate::time::strip_trailing_zeros_and_period;
1379
1380 assert_eq!(strip_trailing_zeros_and_period("123.000"), "123");
1381 assert_eq!(strip_trailing_zeros_and_period("123.450"), "123.45");
1382 assert_eq!(strip_trailing_zeros_and_period("123.456"), "123.456");
1383 assert_eq!(strip_trailing_zeros_and_period("123."), "123");
1384 assert_eq!(strip_trailing_zeros_and_period("123"), "123");
1385 assert_eq!(strip_trailing_zeros_and_period("0.000"), "0");
1386 assert_eq!(strip_trailing_zeros_and_period("0.100"), "0.1");
1387 assert_eq!(strip_trailing_zeros_and_period(""), "");
1388 }
1389
1390 #[test]
1391 fn test_format_si() {
1392 use crate::time::format_si;
1393
1394 assert_eq!(format_si("1234.56"), "1234.56");
1396 assert_eq!(format_si("123.4"), "123.4");
1397
1398 assert_eq!(format_si("12345.67"), "12\u{2009}345.67");
1400 assert_eq!(format_si("1234567.89"), "1\u{2009}234\u{2009}567.89");
1401 assert_eq!(format_si("12345"), "12\u{2009}345");
1403 assert_eq!(format_si("123"), "123");
1404
1405 assert_eq!(format_si("0.123"), "0.123");
1407 assert_eq!(format_si(""), "");
1408
1409 assert_eq!(format_si("123.4567890"), "123.456\u{2009}789\u{2009}0");
1411 }
1412
1413 #[test]
1414 fn test_time_unit_exponent() {
1415 assert_eq!(TimeUnit::Seconds.exponent(), 0);
1417 assert_eq!(TimeUnit::MilliSeconds.exponent(), -3);
1418 assert_eq!(TimeUnit::MicroSeconds.exponent(), -6);
1419 assert_eq!(TimeUnit::NanoSeconds.exponent(), -9);
1420 assert_eq!(TimeUnit::PicoSeconds.exponent(), -12);
1421 assert_eq!(TimeUnit::FemtoSeconds.exponent(), -15);
1422 assert_eq!(TimeUnit::AttoSeconds.exponent(), -18);
1423 assert_eq!(TimeUnit::ZeptoSeconds.exponent(), -21);
1424
1425 for unit in [
1427 TimeUnit::Seconds,
1428 TimeUnit::MilliSeconds,
1429 TimeUnit::MicroSeconds,
1430 TimeUnit::NanoSeconds,
1431 TimeUnit::PicoSeconds,
1432 TimeUnit::FemtoSeconds,
1433 TimeUnit::AttoSeconds,
1434 TimeUnit::ZeptoSeconds,
1435 ] {
1436 assert_eq!(TimeUnit::from_exponent(unit.exponent()), Some(unit));
1437 }
1438
1439 assert_eq!(TimeUnit::from_exponent(-5), None);
1441 assert_eq!(TimeUnit::from_exponent(1), None);
1442 }
1443
1444 #[test]
1445 fn test_time_string_zero() {
1446 assert_eq!(
1448 time_string(
1449 &BigInt::from(0),
1450 &TimeScale {
1451 multiplier: Some(1),
1452 unit: TimeUnit::MicroSeconds
1453 },
1454 &TimeUnit::MicroSeconds,
1455 &TimeFormat::default()
1456 ),
1457 "0 μs"
1458 );
1459
1460 assert_eq!(
1461 time_string(
1462 &BigInt::from(0),
1463 &TimeScale {
1464 multiplier: Some(1),
1465 unit: TimeUnit::Seconds
1466 },
1467 &TimeUnit::Auto,
1468 &TimeFormat::default()
1469 ),
1470 "0 s"
1471 );
1472 }
1473
1474 #[test]
1475 fn test_time_string_large_numbers() {
1476 assert_eq!(
1478 time_string(
1479 &BigInt::from(999_999_999_999i64),
1480 &TimeScale {
1481 multiplier: Some(1),
1482 unit: TimeUnit::NanoSeconds
1483 },
1484 &TimeUnit::Seconds,
1485 &TimeFormat {
1486 format: TimeStringFormatting::SI,
1487 show_space: true,
1488 show_unit: true
1489 }
1490 ),
1491 "999.999\u{2009}999\u{2009}999 s"
1492 );
1493 }
1494
1495 #[test]
1496 fn test_time_string_no_multiplier() {
1497 assert_eq!(
1499 time_string(
1500 &BigInt::from(1234),
1501 &TimeScale {
1502 multiplier: None,
1503 unit: TimeUnit::NanoSeconds
1504 },
1505 &TimeUnit::NanoSeconds,
1506 &TimeFormat::default()
1507 ),
1508 "1234 ns"
1509 );
1510 }
1511
1512 #[test]
1513 fn test_time_format_variations() {
1514 let value = BigInt::from(123456);
1515 let scale = TimeScale {
1516 multiplier: Some(1),
1517 unit: TimeUnit::NanoSeconds,
1518 };
1519
1520 assert_eq!(
1522 time_string(
1523 &value,
1524 &scale,
1525 &TimeUnit::NanoSeconds,
1526 &TimeFormat {
1527 format: TimeStringFormatting::No,
1528 show_space: true,
1529 show_unit: true
1530 }
1531 ),
1532 "123456 ns"
1533 );
1534
1535 assert_eq!(
1536 time_string(
1537 &value,
1538 &scale,
1539 &TimeUnit::NanoSeconds,
1540 &TimeFormat {
1541 format: TimeStringFormatting::No,
1542 show_space: false,
1543 show_unit: true
1544 }
1545 ),
1546 "123456ns"
1547 );
1548
1549 assert_eq!(
1550 time_string(
1551 &value,
1552 &scale,
1553 &TimeUnit::NanoSeconds,
1554 &TimeFormat {
1555 format: TimeStringFormatting::No,
1556 show_space: true,
1557 show_unit: false
1558 }
1559 ),
1560 "123456"
1561 );
1562
1563 assert_eq!(
1564 time_string(
1565 &value,
1566 &scale,
1567 &TimeUnit::NanoSeconds,
1568 &TimeFormat {
1569 format: TimeStringFormatting::SI,
1570 show_space: true,
1571 show_unit: true
1572 }
1573 ),
1574 "123\u{2009}456 ns"
1575 );
1576 }
1577
1578 #[test]
1579 fn test_find_auto_scale_seconds_passthrough() {
1580 use crate::time::find_auto_scale;
1581
1582 let ts = TimeScale {
1583 unit: TimeUnit::Seconds,
1584 multiplier: Some(1),
1585 };
1586 assert_eq!(find_auto_scale(&BigInt::from(1), &ts), TimeUnit::Seconds);
1587 assert_eq!(
1588 find_auto_scale(&BigInt::from(1_234_567), &ts),
1589 TimeUnit::Seconds
1590 );
1591 }
1592
1593 #[test]
1594 fn test_find_auto_scale_nanoseconds() {
1595 use crate::time::find_auto_scale;
1596
1597 let ts = TimeScale {
1598 unit: TimeUnit::NanoSeconds,
1599 multiplier: Some(1),
1600 };
1601
1602 assert_eq!(
1604 find_auto_scale(&BigInt::from(1_000_000_000i64), &ts),
1605 TimeUnit::Seconds
1606 );
1607 assert_eq!(
1609 find_auto_scale(&BigInt::from(1_000_000), &ts),
1610 TimeUnit::MilliSeconds
1611 );
1612 assert_eq!(
1614 find_auto_scale(&BigInt::from(1_000), &ts),
1615 TimeUnit::MicroSeconds
1616 );
1617 assert_eq!(
1619 find_auto_scale(&BigInt::from(1234), &ts),
1620 TimeUnit::NanoSeconds
1621 );
1622 }
1623
1624 #[test]
1625 fn test_find_auto_scale_microseconds_with_multiplier() {
1626 use crate::time::find_auto_scale;
1627
1628 let ts_none = TimeScale {
1630 unit: TimeUnit::MicroSeconds,
1631 multiplier: None,
1632 };
1633 assert_eq!(
1634 find_auto_scale(&BigInt::from(1_000_000), &ts_none),
1635 TimeUnit::Seconds
1636 );
1637 assert_eq!(
1638 find_auto_scale(&BigInt::from(1_000), &ts_none),
1639 TimeUnit::MilliSeconds
1640 );
1641 assert_eq!(
1642 find_auto_scale(&BigInt::from(123), &ts_none),
1643 TimeUnit::MicroSeconds
1644 );
1645
1646 let ts_mul10 = TimeScale {
1648 unit: TimeUnit::MicroSeconds,
1649 multiplier: Some(10),
1650 };
1651 assert_eq!(
1652 find_auto_scale(&BigInt::from(100_000), &ts_mul10),
1653 TimeUnit::Seconds
1654 );
1655 assert_eq!(
1656 find_auto_scale(&BigInt::from(100), &ts_mul10),
1657 TimeUnit::MilliSeconds
1658 );
1659 assert_eq!(
1660 find_auto_scale(&BigInt::from(123), &ts_mul10),
1661 TimeUnit::MicroSeconds
1662 );
1663 }
1664
1665 #[test]
1666 fn test_find_auto_scale_femtoseconds() {
1667 use crate::time::find_auto_scale;
1668
1669 let ts = TimeScale {
1670 unit: TimeUnit::FemtoSeconds,
1671 multiplier: Some(1),
1672 };
1673 assert_eq!(
1675 find_auto_scale(&BigInt::from(10_i128.pow(15)), &ts),
1676 TimeUnit::Seconds
1677 );
1678 assert_eq!(
1680 find_auto_scale(&BigInt::from(10_i128.pow(12)), &ts),
1681 TimeUnit::MilliSeconds
1682 );
1683 assert_eq!(
1685 find_auto_scale(&BigInt::from(10_i128.pow(9)), &ts),
1686 TimeUnit::MicroSeconds
1687 );
1688 assert_eq!(
1690 find_auto_scale(&BigInt::from(10_i128.pow(6)), &ts),
1691 TimeUnit::NanoSeconds
1692 );
1693 assert_eq!(
1695 find_auto_scale(&BigInt::from(10_i128.pow(3)), &ts),
1696 TimeUnit::PicoSeconds
1697 );
1698 assert_eq!(
1700 find_auto_scale(&BigInt::from(1), &ts),
1701 TimeUnit::FemtoSeconds
1702 );
1703 }
1704
1705 #[test]
1706 fn test_locale_cache_en_us() {
1707 use crate::time::{create_cache, format_locale};
1708 use pure_rust_locales::Locale;
1709
1710 let locale = Locale::en_US;
1711 let cache = create_cache(locale);
1712
1713 let result = format_locale("1234567.89", &cache);
1715 assert_eq!(result, "1,234,567.89");
1716 }
1717
1718 #[test]
1719 fn test_locale_cache_de_de() {
1720 use crate::time::{create_cache, format_locale};
1721 use pure_rust_locales::Locale;
1722
1723 let locale = Locale::de_DE;
1724 let cache = create_cache(locale);
1725
1726 let result = format_locale("1234567.89", &cache);
1727 assert_eq!(result, "1.234.567,89");
1728 }
1729
1730 #[test]
1731 fn test_locale_cache_fr_fr() {
1732 use crate::time::{create_cache, format_locale};
1733 use pure_rust_locales::Locale;
1734
1735 let locale = Locale::fr_FR;
1736 let cache = create_cache(locale);
1737
1738 let result = format_locale("1234567.89", &cache);
1740 assert_eq!(result, "1\u{2009}234\u{2009}567,89");
1742 }
1743
1744 #[test]
1745 fn test_locale_cache_small_numbers() {
1746 use crate::time::{create_cache, format_locale};
1747 use pure_rust_locales::Locale;
1748
1749 let locale = Locale::en_US;
1750 let cache = create_cache(locale);
1751
1752 assert_eq!(format_locale("123", &cache), "123");
1754 assert_eq!(format_locale("12.34", &cache), "12.34");
1755 assert_eq!(format_locale("0", &cache), "0");
1756 }
1757
1758 #[test]
1759 fn test_locale_cache_consistency_across_locales() {
1760 use crate::time::create_cache;
1761 use pure_rust_locales::Locale;
1762
1763 let cache1 = create_cache(Locale::en_US);
1765 let cache2 = create_cache(Locale::en_US);
1766
1767 assert_eq!(cache1.thousands_sep, cache2.thousands_sep);
1768 assert_eq!(cache1.decimal_point, cache2.decimal_point);
1769 assert_eq!(cache1.grouping, cache2.grouping);
1770 }
1771
1772 #[test]
1773 fn test_create_cache_from_various_locales() {
1774 use crate::time::{create_cache, format_locale};
1775 use pure_rust_locales::Locale;
1776
1777 let locales = vec![
1779 Locale::en_US,
1780 Locale::de_DE,
1781 Locale::fr_FR,
1782 Locale::es_ES,
1783 Locale::it_IT,
1784 Locale::pt_BR,
1785 Locale::pt_PT,
1786 Locale::ja_JP,
1787 Locale::zh_CN,
1788 Locale::zh_TW,
1789 Locale::ru_RU,
1790 Locale::ko_KR,
1791 Locale::pl_PL,
1792 Locale::tr_TR,
1793 Locale::nl_NL,
1794 Locale::sv_SE,
1795 Locale::da_DK,
1796 Locale::fi_FI,
1797 Locale::el_GR,
1798 Locale::hu_HU,
1799 Locale::cs_CZ,
1800 Locale::ro_RO,
1801 Locale::th_TH,
1802 Locale::vi_VN,
1803 Locale::ar_SA,
1804 Locale::he_IL,
1805 Locale::id_ID,
1806 Locale::uk_UA,
1807 Locale::en_GB,
1808 Locale::en_AU,
1809 Locale::en_CA,
1810 Locale::en_NZ,
1811 Locale::en_IN,
1812 Locale::fr_CA,
1813 Locale::de_AT,
1814 Locale::de_CH,
1815 Locale::fr_CH,
1816 Locale::it_CH,
1817 Locale::es_MX,
1818 Locale::es_AR,
1819 ];
1820
1821 for locale in locales {
1822 let cache = create_cache(locale);
1823 assert!(
1825 !format_locale("1234567.89", &cache).is_empty(),
1826 "Failed for {locale:?}"
1827 );
1828 }
1829 }
1830}
1831
1832#[cfg(test)]
1833mod get_ticks_tests {
1834 use super::*;
1835 use itertools::Itertools;
1836 use num::BigInt;
1837
1838 #[test]
1841 fn get_ticks_basic() {
1842 let vp = crate::viewport::Viewport::default();
1843 let timescale = TimeScale {
1844 unit: TimeUnit::MicroSeconds,
1845 multiplier: Some(1),
1846 };
1847 let frame_width = 800.0_f32;
1848 let text_size = 12.0_f32;
1849 let wanted = TimeUnit::MicroSeconds;
1850 let time_format = TimeFormat::default();
1851 let config = crate::config::SurferConfig::default();
1852 let range = TimeRange {
1853 start: BigInt::from(0),
1854 end: BigInt::from(1_000_000i64),
1855 };
1856 let ticks = get_ticks_internal(
1857 &vp,
1858 ×cale,
1859 frame_width,
1860 text_size,
1861 &wanted,
1862 &time_format,
1863 config.theme.ticks.density,
1864 &range,
1865 );
1866
1867 assert!(!ticks.is_empty(), "expected at least one tick");
1868
1869 let mut last_x = -1.0_f32;
1871 let mut labels: Vec<String> = Vec::with_capacity(ticks.len());
1872 for (label, x, _) in &ticks {
1873 assert!(
1874 *x >= last_x,
1875 "tick x not monotonic: {x} < {last_x} for label {label}"
1876 );
1877 last_x = *x;
1878 assert!(*x >= 0.0, "tick x < 0: {x}");
1879 assert!(
1880 *x <= frame_width,
1881 "tick x > frame_width: {x} > {frame_width}"
1882 );
1883 labels.push(label.clone());
1884 }
1885 let unique_labels = labels.iter().unique().count();
1887 assert_eq!(labels.len(), unique_labels, "duplicate tick labels found");
1888 }
1889
1890 #[test]
1893 fn get_ticks_respects_frame_width_and_density() {
1894 let mut vp = crate::viewport::Viewport::default();
1895 vp.curr_left = crate::viewport::Relative(0.0);
1897 vp.curr_right = crate::viewport::Relative(0.1);
1898
1899 let timescale = TimeScale {
1900 unit: TimeUnit::NanoSeconds,
1901 multiplier: Some(1),
1902 };
1903 let frame_width = 200.0_f32;
1904 let text_size = 10.0_f32;
1905 let wanted = TimeUnit::Auto;
1906 let time_format = TimeFormat {
1907 format: TimeStringFormatting::SI,
1908 show_space: true,
1909 show_unit: true,
1910 };
1911
1912 let mut config = crate::config::SurferConfig::default();
1913 config.theme.ticks.density = 1.0;
1915
1916 let range = TimeRange {
1917 start: BigInt::from(0),
1918 end: BigInt::from(1_000_000i64),
1919 };
1920
1921 let ticks = get_ticks_internal(
1922 &vp,
1923 ×cale,
1924 frame_width,
1925 text_size,
1926 &wanted,
1927 &time_format,
1928 config.theme.ticks.density,
1929 &range,
1930 );
1931
1932 assert!(!ticks.is_empty(), "expected ticks even for narrow view");
1933 assert!(ticks.len() < 200, "too many ticks: {}", ticks.len());
1935
1936 let mut last_x = -1.0_f32;
1938 let mut labels: Vec<String> = Vec::with_capacity(ticks.len());
1939 for (label, x, _) in &ticks {
1940 assert!(
1941 *x >= last_x,
1942 "tick x not monotonic: {x} < {last_x} for label {label}"
1943 );
1944 last_x = *x;
1945 assert!(*x >= 0.0, "tick x < 0: {x}");
1946 assert!(
1947 *x <= frame_width,
1948 "tick x > frame_width: {x} > {frame_width}"
1949 );
1950 labels.push(label.clone());
1951 }
1952 let unique_labels = labels.iter().unique().count();
1953 assert_eq!(labels.len(), unique_labels, "duplicate tick labels found");
1954 }
1955}
1956
1957#[cfg(test)]
1958mod time_input_tests {
1959 use super::*;
1960
1961 #[test]
1962 fn test_parse_time_input_simple() {
1963 let (num, unit) = parse_time_input("100");
1964 assert_eq!(num, "100");
1965 assert_eq!(unit, None);
1966 }
1967
1968 #[test]
1969 fn test_parse_time_input_unit_only_no_panic() {
1970 let (num, unit) = parse_time_input("ns");
1972 assert_eq!(num, "ns");
1973 assert_eq!(unit, None);
1974 }
1975
1976 #[test]
1977 fn test_parse_time_input_with_unit_no_space() {
1978 let (num, unit) = parse_time_input("100ns");
1979 assert_eq!(num, "100");
1980 assert_eq!(unit, Some(TimeUnit::NanoSeconds));
1981
1982 let (num, unit) = parse_time_input("50ps");
1983 assert_eq!(num, "50");
1984 assert_eq!(unit, Some(TimeUnit::PicoSeconds));
1985
1986 let (num, unit) = parse_time_input("1.5ms");
1987 assert_eq!(num, "1.5");
1988 assert_eq!(unit, Some(TimeUnit::MilliSeconds));
1989 }
1990
1991 #[test]
1992 fn test_parse_time_input_with_unit_space() {
1993 let (num, unit) = parse_time_input("100 ns");
1994 assert_eq!(num, "100");
1995 assert_eq!(unit, Some(TimeUnit::NanoSeconds));
1996
1997 let (num, unit) = parse_time_input("1.5 ms");
1998 assert_eq!(num, "1.5");
1999 assert_eq!(unit, Some(TimeUnit::MilliSeconds));
2000
2001 let (num, unit) = parse_time_input("100\tms");
2002 assert_eq!(num, "100");
2003 assert_eq!(unit, Some(TimeUnit::MilliSeconds));
2004
2005 let (num, unit) = parse_time_input("100 ns");
2006 assert_eq!(num, "100");
2007 assert_eq!(unit, Some(TimeUnit::NanoSeconds));
2008 }
2009
2010 #[test]
2011 fn test_parse_time_input_microseconds_unicode() {
2012 let (num, unit) = parse_time_input("100μs");
2013 assert_eq!(num, "100");
2014 assert_eq!(unit, Some(TimeUnit::MicroSeconds));
2015
2016 let (num, unit) = parse_time_input("50 μs");
2017 assert_eq!(num, "50");
2018 assert_eq!(unit, Some(TimeUnit::MicroSeconds));
2019 }
2020
2021 #[test]
2022 fn test_parse_time_input_microseconds_ascii() {
2023 let (num, unit) = parse_time_input("100us");
2025 assert_eq!(num, "100");
2026 assert_eq!(unit, Some(TimeUnit::MicroSeconds));
2027 }
2028
2029 #[test]
2030 fn test_parse_time_input_seconds() {
2031 let (num, unit) = parse_time_input("10s");
2032 assert_eq!(num, "10");
2033 assert_eq!(unit, Some(TimeUnit::Seconds));
2034
2035 let (num, unit) = parse_time_input("0.5s");
2036 assert_eq!(num, "0.5");
2037 assert_eq!(unit, Some(TimeUnit::Seconds));
2038 }
2039
2040 #[test]
2041 fn test_parse_time_input_femtoseconds() {
2042 let (num, unit) = parse_time_input("1000000fs");
2043 assert_eq!(num, "1000000");
2044 assert_eq!(unit, Some(TimeUnit::FemtoSeconds));
2045 }
2046
2047 #[test]
2048 fn test_parse_time_input_with_whitespace() {
2049 let (num, unit) = parse_time_input(" 100ns ");
2050 assert_eq!(num, "100");
2051 assert_eq!(unit, Some(TimeUnit::NanoSeconds));
2052 }
2053
2054 #[test]
2055 fn test_split_numeric_parts() {
2056 assert_eq!(
2057 split_numeric_parts("100").ok(),
2058 Some(("100".to_string(), String::new()))
2059 );
2060 assert_eq!(
2061 split_numeric_parts("100.").ok(),
2062 Some(("100".to_string(), String::new()))
2063 );
2064 assert_eq!(
2065 split_numeric_parts(".5").ok(),
2066 Some(("0".to_string(), "5".to_string()))
2067 );
2068 assert_eq!(
2069 split_numeric_parts("1.5").ok(),
2070 Some(("1".to_string(), "5".to_string()))
2071 );
2072 }
2073
2074 #[test]
2075 fn test_split_numeric_parts_invalid() {
2076 assert!(split_numeric_parts("").is_err());
2077 assert!(split_numeric_parts("abc").is_err());
2078 assert!(split_numeric_parts("12.34.56").is_err());
2079 assert!(split_numeric_parts("-1").is_err());
2080 }
2081
2082 #[test]
2083 fn test_normalize_numeric_with_unit_integer() {
2084 let (val, unit) = normalize_numeric_with_unit("100", TimeUnit::NanoSeconds).unwrap();
2085 assert_eq!(val, BigInt::from(100));
2086 assert_eq!(unit, TimeUnit::NanoSeconds);
2087 }
2088
2089 #[test]
2090 fn test_normalize_numeric_with_unit_decimal_single_step() {
2091 let (val, unit) = normalize_numeric_with_unit("1.5", TimeUnit::NanoSeconds).unwrap();
2092 assert_eq!(val, BigInt::from(1500));
2093 assert_eq!(unit, TimeUnit::PicoSeconds);
2094 }
2095
2096 #[test]
2097 fn test_normalize_numeric_with_unit_decimal_multi_step() {
2098 let (val, unit) = normalize_numeric_with_unit("1.2345", TimeUnit::NanoSeconds).unwrap();
2099 assert_eq!(val, BigInt::from(1_234_500));
2100 assert_eq!(unit, TimeUnit::FemtoSeconds);
2101 }
2102
2103 #[test]
2104 fn test_time_input_state_default() {
2105 let state = TimeInputState::default();
2106 assert_eq!(state.input_text, "");
2107 assert_eq!(state.parsed_value, None);
2108 assert_eq!(state.input_unit, None);
2109 assert_eq!(state.normalized_unit, None);
2110 assert_eq!(state.selected_unit, TimeUnit::NanoSeconds);
2111 assert_eq!(state.error, None);
2112 }
2113
2114 #[test]
2115 fn test_time_input_state_update_valid() {
2116 let mut state = TimeInputState::new();
2117 state.update_input("100ns".to_string());
2118
2119 assert_eq!(state.input_text, "100ns");
2120 assert_eq!(state.parsed_value, Some(BigInt::from(100)));
2121 assert_eq!(state.input_unit, Some(TimeUnit::NanoSeconds));
2122 assert_eq!(state.normalized_unit, Some(TimeUnit::NanoSeconds));
2123 assert_eq!(state.error, None);
2124 }
2125
2126 #[test]
2127 fn test_time_input_state_update_invalid() {
2128 let mut state = TimeInputState::new();
2129 state.update_input("abc".to_string());
2130
2131 assert_eq!(state.parsed_value, None);
2132 assert!(state.error.is_some());
2133 }
2134
2135 #[test]
2136 fn test_time_input_state_update_decimal_unit_normalization() {
2137 let mut state = TimeInputState::new();
2138 state.update_input("1.5ns".to_string());
2139
2140 assert_eq!(state.parsed_value, Some(BigInt::from(1500)));
2141 assert_eq!(state.input_unit, Some(TimeUnit::NanoSeconds));
2142 assert_eq!(state.normalized_unit, Some(TimeUnit::PicoSeconds));
2143 assert_eq!(state.effective_unit(), TimeUnit::PicoSeconds);
2144 assert_eq!(state.error, None);
2145 }
2146
2147 #[test]
2148 fn test_time_input_state_to_timescale_ticks_invalid() {
2149 let state = TimeInputState::new();
2150 let timescale = TimeScale {
2151 unit: TimeUnit::NanoSeconds,
2152 multiplier: None,
2153 };
2154
2155 assert_eq!(state.to_timescale_ticks(×cale), None);
2156 }
2157
2158 #[test]
2159 fn test_time_input_comprehensive_example() {
2160 let mut state = TimeInputState::new();
2162 state.update_input("2.5 ms".to_string());
2163
2164 assert_eq!(state.parsed_value, Some(BigInt::from(2500)));
2166 assert_eq!(state.input_unit, Some(TimeUnit::MilliSeconds));
2167 assert_eq!(state.effective_unit(), TimeUnit::MicroSeconds);
2168 assert_eq!(state.error, None);
2169
2170 let timescale = TimeScale {
2172 unit: TimeUnit::MicroSeconds,
2173 multiplier: None,
2174 };
2175 assert_eq!(
2176 state.to_timescale_ticks(×cale),
2177 Some(BigInt::from(2500))
2178 );
2179 }
2180
2181 #[test]
2182 fn test_parse_time_longest_match_first() {
2183 let (num, unit) = parse_time_input("100ms");
2185 assert_eq!(num, "100");
2186 assert_eq!(unit, Some(TimeUnit::MilliSeconds));
2187
2188 let (_, unit) = parse_time_input("100s");
2190 assert_ne!(unit, Some(TimeUnit::MilliSeconds));
2191 }
2192
2193 #[test]
2194 fn test_parse_time_no_false_positives() {
2195 let (num, unit) = parse_time_input("mass");
2197 assert_eq!(num, "mass");
2198 assert_eq!(unit, None);
2199
2200 let (num, unit) = parse_time_input("uses");
2201 assert_eq!(num, "uses");
2202 assert_eq!(unit, None);
2203 }
2204
2205 #[test]
2206 fn test_time_input_state_clear() {
2207 let mut state = TimeInputState::new();
2208 state.update_input("100ns".to_string());
2209 assert!(state.parsed_value.is_some());
2210
2211 state.update_input(String::new());
2213 assert!(state.error.is_some());
2214 }
2215
2216 fn ns_timescale() -> TimeScale {
2219 TimeScale {
2220 unit: TimeUnit::NanoSeconds,
2221 multiplier: None,
2222 }
2223 }
2224
2225 fn ps_timescale() -> TimeScale {
2226 TimeScale {
2227 unit: TimeUnit::PicoSeconds,
2228 multiplier: None,
2229 }
2230 }
2231
2232 #[test]
2233 fn test_parse_time_string_to_ticks_plain_integer() {
2234 let ts = ns_timescale();
2236 assert_eq!(
2237 parse_time_string_to_ticks("100", &ts),
2238 Some(BigInt::from(100))
2239 );
2240 assert_eq!(parse_time_string_to_ticks("0", &ts), Some(BigInt::from(0)));
2241 }
2242
2243 #[test]
2244 fn test_parse_time_string_to_ticks_same_unit() {
2245 let ts = ns_timescale();
2247 assert_eq!(
2248 parse_time_string_to_ticks("100ns", &ts),
2249 Some(BigInt::from(100))
2250 );
2251 assert_eq!(
2252 parse_time_string_to_ticks("100 ns", &ts),
2253 Some(BigInt::from(100))
2254 );
2255 }
2256
2257 #[test]
2258 fn test_parse_time_string_to_ticks_coarser_unit() {
2259 let ts = ns_timescale();
2261 assert_eq!(
2262 parse_time_string_to_ticks("1us", &ts),
2263 Some(BigInt::from(1000))
2264 );
2265 assert_eq!(
2266 parse_time_string_to_ticks("1μs", &ts),
2267 Some(BigInt::from(1000))
2268 );
2269 assert_eq!(
2271 parse_time_string_to_ticks("1ms", &ts),
2272 Some(BigInt::from(1_000_000))
2273 );
2274 assert_eq!(
2276 parse_time_string_to_ticks("1s", &ts),
2277 Some(BigInt::from(1_000_000_000))
2278 );
2279 }
2280
2281 #[test]
2282 fn test_parse_time_string_to_ticks_finer_unit() {
2283 let ts = ns_timescale();
2285 assert_eq!(
2286 parse_time_string_to_ticks("1ps", &ts),
2287 Some(BigInt::from(0))
2288 );
2289 assert_eq!(
2291 parse_time_string_to_ticks("1000ps", &ts),
2292 Some(BigInt::from(1))
2293 );
2294 }
2295
2296 #[test]
2297 fn test_parse_time_string_to_ticks_decimal() {
2298 let ts = ns_timescale();
2300 assert_eq!(
2301 parse_time_string_to_ticks("1.5us", &ts),
2302 Some(BigInt::from(1500))
2303 );
2304 let ts_ps = ps_timescale();
2306 assert_eq!(
2307 parse_time_string_to_ticks("0.5ns", &ts_ps),
2308 Some(BigInt::from(500))
2309 );
2310 }
2311
2312 #[test]
2313 fn test_parse_time_string_to_ticks_with_whitespace() {
2314 let ts = ns_timescale();
2315 assert_eq!(
2316 parse_time_string_to_ticks(" 100 ns ", &ts),
2317 Some(BigInt::from(100))
2318 );
2319 }
2320
2321 #[test]
2322 fn test_parse_time_string_to_ticks_invalid() {
2323 let ts = ns_timescale();
2324 assert_eq!(parse_time_string_to_ticks("abc", &ts), None);
2325 assert_eq!(parse_time_string_to_ticks("", &ts), None);
2326 assert_eq!(parse_time_string_to_ticks("-5ns", &ts), None);
2327 }
2328
2329 #[test]
2330 fn test_parse_time_string_to_ticks_with_multiplier() {
2331 let ts = TimeScale {
2333 unit: TimeUnit::NanoSeconds,
2334 multiplier: Some(10),
2335 };
2336 assert_eq!(
2337 parse_time_string_to_ticks("100ns", &ts),
2338 Some(BigInt::from(10))
2339 );
2340 }
2341
2342 #[test]
2343 fn test_parse_time_string_to_ticks_ps_timescale() {
2344 let ts = ps_timescale();
2346 assert_eq!(
2347 parse_time_string_to_ticks("1ns", &ts),
2348 Some(BigInt::from(1000))
2349 );
2350 assert_eq!(
2352 parse_time_string_to_ticks("42", &ts),
2353 Some(BigInt::from(42))
2354 );
2355 }
2356}