stabilize live speed indicator and normalize hi-res wheel handling

This commit is contained in:
2026-03-24 12:10:31 +00:00
parent 3684b24c50
commit 6e948d7b39
4 changed files with 245 additions and 32 deletions
+205
View File
@@ -0,0 +1,205 @@
use std::fmt::Debug;
use gsf_core::{ContextRef, SensXY, get_param_value_from_ctx, persist::ParamStore, sensitivity};
use crate::{action, component::TuiComponent};
use crossterm::event::KeyEvent;
use ratatui::{prelude::*, widgets::*};
use tracing::debug;
#[derive(Debug, Default)]
struct LastMouseMove {
in_speed: f64,
out_sens_x: f64,
out_sens_y: f64,
}
pub type GetNewYAxisBounds<PS> = dyn FnMut(ContextRef<PS>) -> [f64; 2];
pub struct SensitivityGraph<PS: ParamStore> {
last_mouse_move: LastMouseMove,
pub y_bounds: [f64; 2],
data: Vec<(f64, f64)>,
data_alt: Vec<(f64, f64)>,
title: &'static str,
data_name: String,
data_alt_name: String,
context: ContextRef<PS>,
on_y_axis_update_fn: Box<GetNewYAxisBounds<PS>>,
}
impl<PS: ParamStore> SensitivityGraph<PS> {
pub fn new(context: ContextRef<PS>) -> Self {
Self {
context: context.clone(),
last_mouse_move: Default::default(),
y_bounds: [0.0, 0.0],
data: vec![],
data_alt: vec![],
title: "Sensitivity Graph (Ratio = Speed_out / Speed_in)",
data_name: "🠠🠢 Sens".to_string(),
data_alt_name: "🠡🠣 Sens".to_string(),
on_y_axis_update_fn: Box::new(|ctx| {
[
0.0,
f64::from(get_param_value_from_ctx!(ctx, SensMult)) * 2.0,
]
}),
}
}
pub fn on_y_axix_bounds_update(
mut self,
updater: impl FnMut(ContextRef<PS>) -> [f64; 2] + 'static,
) -> Self {
self.on_y_axis_update_fn = Box::new(updater);
self
}
fn update_data(&mut self) {
self.data.clear();
self.data_alt.clear();
let params = self.context.get().params_snapshot();
for x in (0..128).map(|x| (x as f64) * 1.0 /* step size */) {
let (sens_x, sens_y) =
gsf_core::sensitivity(x, self.context.get().current_mode, &params);
self.data.push((x, sens_x));
if sens_x != sens_y {
self.data_alt.push((x, sens_y));
}
}
}
fn read_input_speed_and_resolved_sens(&self) -> (f64, SensXY) {
let input_speed = gsf_core::inputspeed::read_input_speed();
let params = self.context.get().params_snapshot();
debug!("last mouse move read at {} counts/ms", input_speed);
(
input_speed,
sensitivity(input_speed, self.context.get().current_mode, &params),
)
}
fn update_last_move(&mut self) {
let (in_speed, out_sens) = self.read_input_speed_and_resolved_sens();
self.last_mouse_move = LastMouseMove {
in_speed,
out_sens_x: out_sens.0,
out_sens_y: out_sens.1,
};
}
fn formatted_current_point(&self, x: f64, y: f64) -> String {
format!("• ({:0<6.3}, {:0<6.3})", x, y)
}
}
impl<PS: ParamStore> TuiComponent for SensitivityGraph<PS> {
fn handle_key_event(&mut self, _event: &KeyEvent, _actions: &mut action::Actions) {}
fn handle_mouse_event(
&mut self,
_event: &crossterm::event::MouseEvent,
_actions: &mut action::Actions,
) {
}
fn update(&mut self, action: &action::Action) {
if let action::Action::Tick = action {
debug!("updating graph on tick");
self.update_last_move();
self.update_data();
self.y_bounds = (self.on_y_axis_update_fn)(self.context.clone());
}
}
fn draw(&self, frame: &mut ratatui::Frame, area: ratatui::prelude::Rect) {
let (bounds, labels) = bounds_and_labels([0.0, 128.0], 16);
let x_axis = Axis::default()
.title("Speed_in".magenta())
.style(Style::default().white())
.bounds(bounds)
.labels(labels);
let (bounds, labels) = bounds_and_labels(self.y_bounds, 10);
let y_axis = Axis::default()
.title("Ratio".magenta())
.style(Style::default().white())
.bounds(bounds)
.labels(labels);
let clamped_x = self.last_mouse_move.in_speed.clamp(0.0, 128.0);
let highlight_point_x = &[(clamped_x, self.last_mouse_move.out_sens_x)];
let highlight_point_y = &[(clamped_x, self.last_mouse_move.out_sens_y)];
let mut chart_plots = vec![
Dataset::default()
.name(if self.data_alt.is_empty() {
"🠠🠢 🠡🠣Sens".to_owned()
} else {
self.data_name.clone()
})
.marker(symbols::Marker::Braille)
.graph_type(GraphType::Line)
.style(Style::default().green())
.data(&self.data),
// current instance of user input speed and output sensitivity
Dataset::default()
.name(self.formatted_current_point(
self.last_mouse_move.in_speed,
self.last_mouse_move.out_sens_x,
))
.marker(symbols::Marker::Braille)
.style(Style::default().red())
.data(highlight_point_x),
];
if !self.data_alt.is_empty() {
chart_plots.push(
Dataset::default()
.name(self.data_alt_name.clone())
.marker(symbols::Marker::Braille)
.graph_type(GraphType::Line)
.style(Style::default().yellow())
.data(&self.data_alt),
);
chart_plots.push(
Dataset::default()
.name(self.formatted_current_point(
self.last_mouse_move.in_speed,
self.last_mouse_move.out_sens_y,
))
.marker(symbols::Marker::Braille)
.style(Style::default().blue().bold())
.data(highlight_point_y),
);
}
let chart = Chart::new(chart_plots).x_axis(x_axis).y_axis(y_axis);
frame.render_widget(
chart.block(
Block::default()
.borders(Borders::NONE)
.title(self.title)
.bold(),
),
area,
);
}
}
fn bounds_and_labels(bounds: [f64; 2], div: usize) -> ([f64; 2], Vec<Span<'static>>) {
let [o, f] = bounds;
let d = f - o;
let step = d / (div as f64);
let labels = (0..=div)
.map(|i| o + i as f64 * step)
.map(|label| format!("{:.2}", label).into())
.collect();
(bounds, labels)
}