From ffe3c913e1aca5548cdfa0c952bc01b7c3ec336d Mon Sep 17 00:00:00 2001 From: Madhav Agarwal Date: Thu, 22 Oct 2026 19:05:30 +0530 Subject: [PATCH] ui: scrub hot text from the slider range (#2097) --- crates/ui-egui/src/panels/effect_controls.rs | 5 +---- crates/ui-egui/src/panels/timeline.rs | 2 +- crates/ui-egui/src/widgets.rs | 21 ++++++++++++++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/panels/effect_controls.rs b/crates/ui-egui/src/panels/effect_controls.rs index 03b3a1ac..79f414af 100644 --- a/crates/ui-egui/src/panels/effect_controls.rs +++ b/crates/ui-egui/src/panels/effect_controls.rs @@ -337,10 +337,7 @@ fn prop_row( ParamUi::Percent => "%", _ => "", }; - let speed = match &prop.ui { - ParamUi::Slider { slider_min, slider_max, .. } => ((slider_max - slider_min) / 300.0).clamp(0.001, 50.0), - _ => 0.5, - }; + let speed = widgets::slider_scrub_speed(&prop.ui); let (vr, nv, _) = widgets::hot_number_at( ui, pos2(vx, cy - 9.0), diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index b34fbd9d..af4f7e28 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -4105,7 +4105,7 @@ fn value_editor( match value { Value::Scalar(v) => { let (min, max, dec, speed) = match &prop.ui { - ParamUi::Slider { min, max, decimals, .. } => (*min, *max, *decimals as usize, ((max - min) / 400.0).clamp(0.01, 10.0)), + ParamUi::Slider { min, max, decimals, .. } => (*min, *max, *decimals as usize, widgets::slider_scrub_speed(&prop.ui)), ParamUi::Percent => (-1e6, 1e6, 1, 0.5), _ => (-1e9, 1e9, 1, 1.0), }; diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index c005224d..d3d59e2c 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -2,6 +2,8 @@ use egui::{Align2, Color32, Rect, Response, Sense, Stroke, StrokeKind, Ui, pos2, vec2}; +use effectcraft_project::ParamUi; + use crate::automation::Registry; use crate::icons::{self, Icon}; use crate::theme::Tokens; @@ -145,6 +147,16 @@ pub fn proportional_scrub(m: egui::Modifiers) -> bool { m.command && m.alt } +/// How far one pointer pixel scrubs a hot-text field: the property's slider (soft) range over +/// 300 px. The Timeline and Effect Controls share this so they cannot drift; a large hard range +/// only allows typing big values, it must not make dragging coarse (#2097). +pub fn slider_scrub_speed(ui: &ParamUi) -> f64 { + match ui { + ParamUi::Slider { slider_min, slider_max, .. } => ((slider_max - slider_min) / 300.0).clamp(0.001, 50.0), + _ => 0.5, + } +} + /// [`hot_number_at`] for a whole number (an angle's revolutions): a drag steps it once every /// `1 / speed` pixels however slowly the pointer moves (the fractions add up between frames), /// and a typed value is truncated. Returns (rect, Some(new value) when changed, drag finished). @@ -666,6 +678,15 @@ pub(crate) fn constrained(c: &[f64], d: usize, v: f64) -> Vec { mod tests { use super::*; + /// #2097: the drag speed comes from the slider range, not a much wider hard range. + #[test] + fn slider_scrub_speed_uses_the_slider_range() { + let slider = |min, max, slider_min, slider_max| ParamUi::Slider { min, max, slider_min, slider_max, decimals: 1 }; + assert!((slider_scrub_speed(&slider(0.0, 10_000.0, 0.0, 1_000.0)) - 1_000.0 / 300.0).abs() < 1e-9); + assert!((slider_scrub_speed(&slider(0.0, 1.0e9, 0.0, 1.0)) - 1.0 / 300.0).abs() < 1e-9); + assert_eq!(slider_scrub_speed(&ParamUi::Number), 0.5); + } + /// #488: an inline editor selects its whole text only as it opens. Should it be without the /// keyboard for a frame later (an input method, another handler), it takes the keyboard back /// with the caret where the user put it instead of selecting everything again, which made