Skip to main content

ui/
slider.rs

1//! Portable integer slider mapping. The logarithmic path uses fixed-point binary
2//! logarithms, so native and WebAssembly have identical rounding and need no libm.
3use crate::consts::{Range, RG_LOG};
4
5// log2(value) in Q40, for strictly positive i32 values. Q63 squaring fits in u128.
6// This precision keeps even adjacent positive i32 values distinguishable.
7fn log(value: i32) -> i64 {
8    log_positive(value as u64)
9}
10
11fn log_positive(value: u64) -> i64 {
12    let exponent = 63 - value.leading_zeros();
13    let mut x = (value as u128) << (63 - exponent);
14    let mut result = i64::from(exponent) << 40;
15    for bit in (0..40).rev() {
16        x = (x * x) >> 63;
17        if x >= 1 << 64 {
18            x >>= 1;
19            result |= 1 << bit;
20        }
21    }
22    result
23}
24
25impl Range {
26    /// Map a value to the slider's usable pixel travel. RG_LOG applies to positive,
27    /// increasing bounds; all other bounds use linear mapping. Degenerate ranges
28    /// or travel return zero. Values outside the range are clamped.
29    pub fn position(&self, value: i32, travel: i32) -> i32 {
30        if travel <= 0 || self.max <= self.min { return 0; }
31        let value = value.clamp(self.min, self.max);
32        let (offset, span) = if self.flags & RG_LOG != 0 && self.min > 0 {
33            (log(value) - log(self.min), log(self.max) - log(self.min))
34        } else { (i64::from(value) - i64::from(self.min), i64::from(self.max) - i64::from(self.min)) };
35        (i128::from(offset) * i128::from(travel) / i128::from(span)) as i32
36    }
37
38    /// Map pixel travel to the nearest integer value, before step snapping. The
39    /// logarithmic inverse uses bounded binary search (at most 31 iterations).
40    /// Keyboard steps and semantic values always remain in the original units.
41    pub fn value_at(&self, position: i32, travel: i32) -> i32 {
42        if travel <= 0 || self.max <= self.min { return self.min; }
43        let position = position.clamp(0, travel);
44        if position == 0 { return self.min; }
45        if position == travel { return self.max; }
46        if self.flags & RG_LOG == 0 || self.min <= 0 {
47            let span = i64::from(self.max) - i64::from(self.min);
48            return (i64::from(self.min) + (i64::from(position) * span + i64::from(travel / 2)) / i64::from(travel)) as i32;
49        }
50        let target = log(self.min) + (i128::from(log(self.max) - log(self.min)) * i128::from(position) / i128::from(travel)) as i64;
51        let (mut low, mut high) = (self.min, self.max);
52        while high - low > 1 {
53            let mid = low + (high - low) / 2;
54            if log(mid) <= target { low = mid; } else { high = mid; }
55        }
56        // Round in value space, not in logarithmic space: the boundary between
57        // integers is low + 0.5, not their geometric mean.
58        let midpoint = log_positive(low as u64 * 2 + 1) - (1_i64 << 40);
59        if target < midpoint { low } else { high }
60    }
61}