bms_rs/chart_process/
bms_processor.rs

1//! Bms Processor Module.
2
3use std::collections::{BTreeMap, HashMap};
4use std::num::NonZeroU64;
5use std::path::Path;
6use std::time::SystemTime;
7
8use crate::bms::prelude::*;
9use crate::chart_process::{
10    ChartEvent, ChartEventWithPosition, ChartProcessor, ControlEvent, VisibleEvent,
11    types::{BmpId, DisplayRatio, WavId, YCoordinate},
12};
13use std::str::FromStr;
14
15/// ChartProcessor of Bms files.
16pub struct BmsProcessor {
17    bms: Bms,
18
19    // Playback state
20    started_at: Option<SystemTime>,
21    last_poll_at: Option<SystemTime>,
22    /// Accumulated displacement progressed (y, actual movement distance unit)
23    progressed_y: Decimal,
24
25    /// Pending external events queue
26    inbox: Vec<ControlEvent>,
27
28    /// All events mapping (sorted by Y coordinate)
29    all_events: BTreeMap<YCoordinate, Vec<ChartEvent>>,
30
31    /// Preloaded events list (all events in current visible area)
32    preloaded_events: Vec<ChartEventWithPosition>,
33
34    // Flow parameters
35    default_visible_y_length: YCoordinate,
36    current_bpm: Decimal,
37    current_speed: Decimal,
38    current_scroll: Decimal,
39}
40
41impl BmsProcessor {
42    /// Create processor, initialize default parameters
43    #[must_use]
44    pub fn new<T: KeyLayoutMapper>(bms: Bms) -> Self {
45        // Initialize BPM: prefer chart initial BPM, otherwise 120
46        let init_bpm = bms
47            .bpm
48            .bpm
49            .as_ref()
50            .cloned()
51            .unwrap_or_else(|| Decimal::from(120));
52
53        // Calculate visible Y length based on starting BPM and 600ms reaction time
54        // Formula: visible Y length = (BPM / 120.0) * 0.6 seconds
55        // Where 0.6 seconds = 600ms, 120.0 is the base BPM
56        let reaction_time_seconds = Decimal::from_str("0.6").unwrap(); // 600ms
57        let base_bpm = Decimal::from(120);
58        let visible_y_length = (init_bpm.clone() / base_bpm) * reaction_time_seconds;
59
60        let all_events = Self::precompute_all_events::<T>(&bms);
61
62        Self {
63            bms,
64            started_at: None,
65            last_poll_at: None,
66            progressed_y: Decimal::from(0),
67            inbox: Vec::new(),
68            all_events,
69            preloaded_events: Vec::new(),
70            default_visible_y_length: YCoordinate::from(visible_y_length),
71            current_bpm: init_bpm,
72            current_speed: Decimal::from(1),
73            current_scroll: Decimal::from(1),
74        }
75    }
76
77    /// Precompute all events, store grouped by Y coordinate
78    /// Note: Speed effects are calculated into event positions during initialization, ensuring event trigger times remain unchanged
79    fn precompute_all_events<T: KeyLayoutMapper>(
80        bms: &Bms,
81    ) -> BTreeMap<YCoordinate, Vec<ChartEvent>> {
82        let mut events_map: BTreeMap<YCoordinate, Vec<ChartEvent>> = BTreeMap::new();
83
84        // Note / Wav arrival events
85        for obj in bms.notes().all_notes() {
86            let y = Self::y_of_time_static(bms, obj.offset, &bms.speed.speed_factor_changes);
87            let event = Self::event_for_note_static::<T>(bms, obj, y.clone());
88
89            events_map
90                .entry(YCoordinate::from(y))
91                .or_default()
92                .push(event);
93        }
94
95        // BPM change events
96        for change in bms.bpm.bpm_changes.values() {
97            let y = Self::y_of_time_static(bms, change.time, &bms.speed.speed_factor_changes);
98            let event = ChartEvent::BpmChange {
99                bpm: change.bpm.clone(),
100            };
101
102            events_map
103                .entry(YCoordinate::from(y))
104                .or_default()
105                .push(event);
106        }
107
108        // Scroll change events
109        for change in bms.scroll.scrolling_factor_changes.values() {
110            let y = Self::y_of_time_static(bms, change.time, &bms.speed.speed_factor_changes);
111            let event = ChartEvent::ScrollChange {
112                factor: change.factor.clone(),
113            };
114
115            events_map
116                .entry(YCoordinate::from(y))
117                .or_default()
118                .push(event);
119        }
120
121        // Speed change events
122        for change in bms.speed.speed_factor_changes.values() {
123            let y = Self::y_of_time_static(bms, change.time, &bms.speed.speed_factor_changes);
124            let event = ChartEvent::SpeedChange {
125                factor: change.factor.clone(),
126            };
127
128            events_map
129                .entry(YCoordinate::from(y))
130                .or_default()
131                .push(event);
132        }
133
134        // Stop events
135        for stop in bms.stop.stops.values() {
136            let y = Self::y_of_time_static(bms, stop.time, &bms.speed.speed_factor_changes);
137            let event = ChartEvent::Stop {
138                duration: stop.duration.clone(),
139            };
140
141            events_map
142                .entry(YCoordinate::from(y))
143                .or_default()
144                .push(event);
145        }
146
147        // BGA change events
148        for bga_obj in bms.bmp.bga_changes.values() {
149            let y = Self::y_of_time_static(bms, bga_obj.time, &bms.speed.speed_factor_changes);
150            let bmp_index = bga_obj.id.as_u16() as usize;
151            let event = ChartEvent::BgaChange {
152                layer: bga_obj.layer,
153                bmp_id: Some(BmpId::from(bmp_index)),
154            };
155
156            events_map
157                .entry(YCoordinate::from(y))
158                .or_default()
159                .push(event);
160        }
161
162        // BGA opacity change events (requires minor-command feature)
163
164        for (layer, opacity_changes) in &bms.bmp.bga_opacity_changes {
165            for opacity_obj in opacity_changes.values() {
166                let y =
167                    Self::y_of_time_static(bms, opacity_obj.time, &bms.speed.speed_factor_changes);
168                let event = ChartEvent::BgaOpacityChange {
169                    layer: *layer,
170                    opacity: opacity_obj.opacity,
171                };
172
173                events_map
174                    .entry(YCoordinate::from(y))
175                    .or_default()
176                    .push(event);
177            }
178        }
179
180        // BGA ARGB color change events (requires minor-command feature)
181
182        for (layer, argb_changes) in &bms.bmp.bga_argb_changes {
183            for argb_obj in argb_changes.values() {
184                let y = Self::y_of_time_static(bms, argb_obj.time, &bms.speed.speed_factor_changes);
185                let argb = ((argb_obj.argb.alpha as u32) << 24)
186                    | ((argb_obj.argb.red as u32) << 16)
187                    | ((argb_obj.argb.green as u32) << 8)
188                    | (argb_obj.argb.blue as u32);
189                let event = ChartEvent::BgaArgbChange {
190                    layer: *layer,
191                    argb,
192                };
193
194                events_map
195                    .entry(YCoordinate::from(y))
196                    .or_default()
197                    .push(event);
198            }
199        }
200
201        // BGM volume change events
202        for bgm_volume_obj in bms.volume.bgm_volume_changes.values() {
203            let y =
204                Self::y_of_time_static(bms, bgm_volume_obj.time, &bms.speed.speed_factor_changes);
205            let event = ChartEvent::BgmVolumeChange {
206                volume: bgm_volume_obj.volume,
207            };
208
209            events_map
210                .entry(YCoordinate::from(y))
211                .or_default()
212                .push(event);
213        }
214
215        // KEY volume change events
216        for key_volume_obj in bms.volume.key_volume_changes.values() {
217            let y =
218                Self::y_of_time_static(bms, key_volume_obj.time, &bms.speed.speed_factor_changes);
219            let event = ChartEvent::KeyVolumeChange {
220                volume: key_volume_obj.volume,
221            };
222
223            events_map
224                .entry(YCoordinate::from(y))
225                .or_default()
226                .push(event);
227        }
228
229        // Text display events
230        for text_obj in bms.text.text_events.values() {
231            let y = Self::y_of_time_static(bms, text_obj.time, &bms.speed.speed_factor_changes);
232            let event = ChartEvent::TextDisplay {
233                text: text_obj.text.clone(),
234            };
235
236            events_map
237                .entry(YCoordinate::from(y))
238                .or_default()
239                .push(event);
240        }
241
242        // Judge level change events
243        for judge_obj in bms.judge.judge_events.values() {
244            let y = Self::y_of_time_static(bms, judge_obj.time, &bms.speed.speed_factor_changes);
245            let event = ChartEvent::JudgeLevelChange {
246                level: judge_obj.judge_level,
247            };
248
249            events_map
250                .entry(YCoordinate::from(y))
251                .or_default()
252                .push(event);
253        }
254
255        // Minor-command feature events
256
257        {
258            // Video seek events
259            for seek_obj in bms.video.seek_events.values() {
260                let y = Self::y_of_time_static(bms, seek_obj.time, &bms.speed.speed_factor_changes);
261                let event = ChartEvent::VideoSeek {
262                    seek_time: seek_obj.position.to_string().parse::<f64>().unwrap_or(0.0),
263                };
264
265                events_map
266                    .entry(YCoordinate::from(y))
267                    .or_default()
268                    .push(event);
269            }
270
271            // BGA key binding events
272            for bga_keybound_obj in bms.bmp.bga_keybound_events.values() {
273                let y = Self::y_of_time_static(
274                    bms,
275                    bga_keybound_obj.time,
276                    &bms.speed.speed_factor_changes,
277                );
278                let event = ChartEvent::BgaKeybound {
279                    event: bga_keybound_obj.event.clone(),
280                };
281
282                events_map
283                    .entry(YCoordinate::from(y))
284                    .or_default()
285                    .push(event);
286            }
287
288            // Option change events
289            for option_obj in bms.option.option_events.values() {
290                let y =
291                    Self::y_of_time_static(bms, option_obj.time, &bms.speed.speed_factor_changes);
292                let event = ChartEvent::OptionChange {
293                    option: option_obj.option.clone(),
294                };
295
296                events_map
297                    .entry(YCoordinate::from(y))
298                    .or_default()
299                    .push(event);
300            }
301        }
302
303        // Generate measure lines - generated last but not exceeding other objects
304        Self::generate_barlines_for_bms(bms, &mut events_map);
305
306        events_map
307    }
308
309    /// Generate measure lines for BMS (generated for each track, but not exceeding other objects' Y values)
310    fn generate_barlines_for_bms(
311        bms: &Bms,
312        events_map: &mut BTreeMap<YCoordinate, Vec<ChartEvent>>,
313    ) {
314        // Find the maximum Y value of all events
315        let max_y = events_map
316            .keys()
317            .map(|y_coord| y_coord.value())
318            .max()
319            .cloned()
320            .unwrap_or_else(|| Decimal::from(0));
321
322        if max_y <= Decimal::from(0) {
323            return;
324        }
325
326        // Get the track number of the last object
327        let last_obj_time = bms.last_obj_time().unwrap_or_else(|| {
328            ObjTime::new(
329                0,
330                0,
331                NonZeroU64::new(4).expect("4 should be a valid NonZeroU64"),
332            )
333        });
334
335        // Generate measure lines for each track, but not exceeding maximum Y value
336        for track in 0..=last_obj_time.track().0 {
337            let track_y = Self::y_of_time_static(
338                bms,
339                ObjTime::new(
340                    track,
341                    0,
342                    NonZeroU64::new(1).expect("1 should be a valid NonZeroU64"),
343                ),
344                &bms.speed.speed_factor_changes,
345            );
346
347            if track_y <= max_y {
348                let y_coord = YCoordinate::from(track_y);
349                let event = ChartEvent::BarLine;
350                events_map.entry(y_coord).or_default().push(event);
351            }
352        }
353    }
354
355    /// Static version of y_of_time, considers Speed effects (used for event position precomputation)
356    /// Speed effects are calculated into event positions during initialization, ensuring event trigger times remain unchanged
357    fn y_of_time_static(
358        bms: &Bms,
359        time: ObjTime,
360        speed_changes: &std::collections::BTreeMap<ObjTime, crate::bms::model::obj::SpeedObj>,
361    ) -> Decimal {
362        let mut y = Decimal::from(0);
363        // Accumulate complete measures
364        for t in 0..time.track().0 {
365            let section_len = bms
366                .section_len
367                .section_len_changes
368                .get(&Track(t))
369                .map(|s| &s.length)
370                .cloned()
371                .unwrap_or_else(|| Decimal::from(1));
372            y += section_len;
373        }
374        // Accumulate proportionally within current measure
375        let current_len = bms
376            .section_len
377            .section_len_changes
378            .get(&time.track())
379            .map(|s| &s.length)
380            .cloned()
381            .unwrap_or_else(|| Decimal::from(1));
382        if time.denominator().get() > 0 {
383            let fraction =
384                Decimal::from(time.numerator()) / Decimal::from(time.denominator().get());
385            y += current_len * fraction;
386        }
387
388        // Find the last Speed change before current time point as the currently effective Speed factor
389        let mut current_speed_factor = Decimal::from(1);
390        for (change_time, change) in speed_changes {
391            if *change_time <= time {
392                current_speed_factor = change.factor.clone();
393            }
394        }
395
396        // Speed affects both y distance and progression speed, but must keep trigger time unchanged
397        // y_new = y_base * current_speed_factor
398        y * current_speed_factor
399    }
400
401    /// Static version of event_for_note, used for precomputation
402    fn event_for_note_static<T: KeyLayoutMapper>(
403        bms: &Bms,
404        obj: &WavObj,
405        y: Decimal,
406    ) -> ChartEvent {
407        let Some((side, key, kind)) = Self::lane_of_channel_id::<T>(obj.channel_id) else {
408            let wav_id = Some(WavId::from(obj.wav_id.as_u16() as usize));
409            return ChartEvent::Bgm { wav_id };
410        };
411        let wav_id = Some(WavId::from(obj.wav_id.as_u16() as usize));
412        let length = (kind == NoteKind::Long)
413            .then(|| {
414                // Long note: find the next note in the same channel to calculate length
415                bms.notes()
416                    .next_obj_by_key(obj.channel_id, obj.offset)
417                    .map(|next_obj| {
418                        let next_y = Self::y_of_time_static(
419                            bms,
420                            next_obj.offset,
421                            &bms.speed.speed_factor_changes,
422                        );
423                        YCoordinate::from(next_y - y.clone())
424                    })
425            })
426            .flatten();
427        ChartEvent::Note {
428            side,
429            key,
430            kind,
431            wav_id,
432            length,
433            continue_play: false, // Fixed as false for BMS
434        }
435    }
436
437    /// Get the length of specified Track (SectionLength), default 1.0
438    fn section_length_of(&self, track: Track) -> Decimal {
439        self.bms
440            .section_len
441            .section_len_changes
442            .get(&track)
443            .map(|s| s.length.clone())
444            .unwrap_or_else(|| Decimal::from(1))
445    }
446
447    /// Convert `ObjTime` to cumulative displacement y (unit: measure, default 4/4 one measure equals 1; linearly converted by `#SECLEN`).
448    fn y_of_time(&self, time: ObjTime) -> Decimal {
449        let mut y = Decimal::from(0);
450        // Accumulate complete measures
451        for t in 0..time.track().0 {
452            y += self.section_length_of(Track(t));
453        }
454        // Accumulate proportionally within current measure
455        let current_len = self.section_length_of(time.track());
456        let fraction = if time.denominator().get() > 0 {
457            Decimal::from(time.numerator()) / Decimal::from(time.denominator().get())
458        } else {
459            Default::default()
460        };
461        y += current_len * fraction;
462        y
463    }
464
465    /// Current instantaneous displacement velocity (y units per second).
466    /// Model: v = (current_bpm / 120.0) * speed_factor (using fixed base BPM 120)
467    /// Note: Speed affects y progression speed, but does not change actual time progression; Scroll only affects display positions.
468    fn current_velocity(&self) -> Decimal {
469        let base_bpm = Decimal::from(120);
470        let velocity = if self.current_bpm > Decimal::from(0) {
471            let velocity = self.current_bpm.clone() / base_bpm;
472            let speed_factor = self.current_speed.clone();
473            velocity * speed_factor
474        } else {
475            Default::default()
476        };
477        velocity.max(Decimal::from(f64::EPSILON)) // speed must be positive
478    }
479
480    /// Get the next event that affects speed (sorted by y ascending): BPM/SCROLL/SPEED changes.
481    fn next_flow_event_after(&self, y_from_exclusive: Decimal) -> Option<(Decimal, FlowEvent)> {
482        // Collect three event sources, find the minimum item with y greater than threshold
483        let mut best: Option<(Decimal, FlowEvent)> = None;
484
485        for change in self.bms.bpm.bpm_changes.values() {
486            let y = self.y_of_time(change.time);
487            if y > y_from_exclusive {
488                let bpm = change.bpm.clone();
489                best = min_by_y_decimal(best, (y, FlowEvent::Bpm(bpm)));
490            }
491        }
492        for change in self.bms.scroll.scrolling_factor_changes.values() {
493            let y = self.y_of_time(change.time);
494            if y > y_from_exclusive {
495                let factor = change.factor.clone();
496                best = min_by_y_decimal(best, (y, FlowEvent::Scroll(factor)));
497            }
498        }
499        for change in self.bms.speed.speed_factor_changes.values() {
500            let y = self.y_of_time(change.time);
501            if y > y_from_exclusive {
502                let factor = change.factor.clone();
503                best = min_by_y_decimal(best, (y, FlowEvent::Speed(factor)));
504            }
505        }
506
507        best
508    }
509
510    /// Advance time to `now`, perform segmented integration of progress and speed by events.
511    fn step_to(&mut self, now: SystemTime) {
512        let Some(started) = self.started_at else {
513            return;
514        };
515        let last = self.last_poll_at.unwrap_or(started);
516        if now <= last {
517            return;
518        }
519
520        let mut remaining_secs =
521            Decimal::from(now.duration_since(last).unwrap_or_default().as_secs_f64());
522        let mut cur_vel = self.current_velocity();
523        let mut cur_y = self.progressed_y.clone();
524        // Advance in segments until time slice is used up
525        loop {
526            // The next event that affects speed
527            let next_event = self.next_flow_event_after(cur_y.clone());
528            if next_event.is_none()
529                || cur_vel <= Decimal::from(0)
530                || remaining_secs <= Decimal::from(0)
531            {
532                // Advance directly to the end
533                cur_y += cur_vel * remaining_secs;
534                break;
535            }
536            let (event_y, evt) = next_event.unwrap();
537            if event_y.clone() <= cur_y.clone() {
538                // Defense: avoid infinite loop if event position doesn't advance
539                self.apply_flow_event(evt);
540                cur_vel = self.current_velocity();
541                continue;
542            }
543            // Time required to reach event
544            let distance = event_y.clone() - cur_y.clone();
545            if cur_vel > Decimal::from(0) {
546                let time_to_event_secs = distance / cur_vel.clone();
547                if time_to_event_secs <= remaining_secs {
548                    // First advance to event point
549                    cur_y = event_y;
550                    remaining_secs -= time_to_event_secs;
551                    self.apply_flow_event(evt);
552                    cur_vel = self.current_velocity();
553                    continue;
554                }
555            }
556            // Time not enough to reach event, advance and end
557            cur_y += cur_vel * remaining_secs;
558            break;
559        }
560
561        self.progressed_y = cur_y;
562        self.last_poll_at = Some(now);
563    }
564
565    fn apply_flow_event(&mut self, evt: FlowEvent) {
566        match evt {
567            FlowEvent::Bpm(bpm) => self.current_bpm = bpm,
568            FlowEvent::Speed(s) => self.current_speed = s,
569            FlowEvent::Scroll(s) => self.current_scroll = s,
570        }
571    }
572
573    /// Calculate visible window length (y units): based on current BPM and 600ms reaction time
574    fn visible_window_y(&self) -> Decimal {
575        // Dynamically calculate visible window length based on current BPM and 600ms reaction time
576        // Formula: visible Y length = (current BPM / 120.0) * 0.6 seconds
577        let reaction_time_seconds = Decimal::from_str("0.6").unwrap(); // 600ms
578        let base_bpm = Decimal::from(120);
579        (self.current_bpm.clone() / base_bpm) * reaction_time_seconds
580    }
581
582    fn lane_of_channel_id<T: KeyLayoutMapper>(
583        channel_id: NoteChannelId,
584    ) -> Option<(PlayerSide, Key, NoteKind)> {
585        let map = channel_id.try_into_map::<T>()?;
586        let side = map.side();
587        let key = map.key();
588        let kind = map.kind();
589        Some((side, key, kind))
590    }
591}
592
593impl ChartProcessor for BmsProcessor {
594    fn audio_files(&self) -> HashMap<WavId, &Path> {
595        self.bms
596            .wav
597            .wav_files
598            .iter()
599            .map(|(obj_id, path)| (WavId::from(obj_id.as_u16() as usize), path.as_path()))
600            .collect()
601    }
602
603    fn bmp_files(&self) -> HashMap<BmpId, &Path> {
604        self.bms
605            .bmp
606            .bmp_files
607            .iter()
608            .map(|(obj_id, bmp)| (BmpId::from(obj_id.as_u16() as usize), bmp.file.as_path()))
609            .collect()
610    }
611
612    fn default_visible_y_length(&self) -> YCoordinate {
613        self.default_visible_y_length.clone()
614    }
615
616    fn current_bpm(&self) -> Decimal {
617        self.current_bpm.clone()
618    }
619    fn current_speed(&self) -> Decimal {
620        self.current_speed.clone()
621    }
622    fn current_scroll(&self) -> Decimal {
623        self.current_scroll.clone()
624    }
625
626    fn start_play(&mut self, now: SystemTime) {
627        self.started_at = Some(now);
628        self.last_poll_at = Some(now);
629        self.progressed_y = Decimal::from(0);
630        self.preloaded_events.clear();
631        // Initialize current_bpm to header or default
632        self.current_bpm = self
633            .bms
634            .bpm
635            .bpm
636            .as_ref()
637            .cloned()
638            .unwrap_or_else(|| Decimal::from(120));
639    }
640
641    fn update(&mut self, now: SystemTime) -> impl Iterator<Item = ChartEventWithPosition> {
642        // Process external events delivered through post_events
643        let incoming = std::mem::take(&mut self.inbox);
644        for evt in &incoming {
645            match evt {
646                ControlEvent::SetDefaultVisibleYLength { length } => {
647                    self.default_visible_y_length = length.clone();
648                }
649            }
650        }
651
652        let prev_y = self.progressed_y.clone();
653        self.step_to(now);
654        let cur_y = self.progressed_y.clone();
655
656        // Calculate preload range: current y + visible y range
657        let visible_y_length = self.visible_window_y();
658        let preload_end_y = cur_y.clone() + visible_y_length;
659
660        // Collect events triggered at current moment
661        let mut triggered_events: Vec<ChartEventWithPosition> = Vec::new();
662
663        // Collect events within preload range
664        let mut new_preloaded_events: Vec<ChartEventWithPosition> = Vec::new();
665
666        // Get events from precomputed event Map
667        for (y_coord, events) in &self.all_events {
668            let y = y_coord.value();
669
670            // If event is triggered at current moment
671            if *y > prev_y && *y <= cur_y {
672                for event in events {
673                    triggered_events
674                        .push(ChartEventWithPosition::new(y_coord.clone(), event.clone()));
675                }
676            }
677
678            // If event is within preload range
679            if *y > cur_y && *y <= preload_end_y {
680                for event in events {
681                    new_preloaded_events
682                        .push(ChartEventWithPosition::new(y_coord.clone(), event.clone()));
683                }
684            }
685        }
686
687        // Sort events
688        triggered_events.sort_by(|a, b| {
689            a.position()
690                .value()
691                .partial_cmp(b.position().value())
692                .unwrap_or(std::cmp::Ordering::Equal)
693        });
694
695        new_preloaded_events.sort_by(|a, b| {
696            a.position()
697                .value()
698                .partial_cmp(b.position().value())
699                .unwrap_or(std::cmp::Ordering::Equal)
700        });
701
702        // Update preloaded events list
703        self.preloaded_events = new_preloaded_events;
704
705        triggered_events.into_iter()
706    }
707
708    fn post_events(&mut self, events: &[ControlEvent]) {
709        self.inbox.extend_from_slice(events);
710    }
711
712    fn visible_events(&mut self, now: SystemTime) -> impl Iterator<Item = VisibleEvent> {
713        self.step_to(now);
714        let current_y = self.progressed_y.clone();
715        let visible_window_y = self.visible_window_y();
716        let scroll_factor = self.current_scroll.clone();
717
718        self.preloaded_events.iter().map(move |event_with_pos| {
719            let event_y = event_with_pos.position().value();
720            // Calculate display ratio: (event_y - current_y) / visible_window_y * scroll_factor
721            // Note: scroll can be non-zero positive or negative values
722            let display_ratio_value = if visible_window_y > Decimal::from(0) {
723                ((event_y.clone() - current_y.clone()) / visible_window_y.clone())
724                    * scroll_factor.clone()
725            } else {
726                Default::default()
727            };
728            let display_ratio = DisplayRatio::from(display_ratio_value);
729            VisibleEvent::new(
730                event_with_pos.position().clone(),
731                event_with_pos.event().clone(),
732                display_ratio,
733            )
734        })
735    }
736}
737
738#[derive(Debug, Clone)]
739enum FlowEvent {
740    Bpm(Decimal),
741    Speed(Decimal),
742    Scroll(Decimal),
743}
744
745fn min_by_y_decimal(
746    best: Option<(Decimal, FlowEvent)>,
747    candidate: (Decimal, FlowEvent),
748) -> Option<(Decimal, FlowEvent)> {
749    match best {
750        None => Some(candidate),
751        Some((y, _)) if candidate.0 < y => Some(candidate),
752        Some(other) => Some(other),
753    }
754}