Skip to main content

ui/
timeline.rs

1// timeline -- a scrubber for recorded events: a time axis, one lane of event ticks per track
2// and a playhead (the event timeline of a simulation)
3//
4//   timeline(id, &mut st, span, tracks) -> EV_*
5//        span: the recorded time range in seconds (span.1 is the live end); a track is a
6//        label and its ticks (time, 0xAARRGGBB; colour 0 = the chart palette colour of the
7//        track), sorted by time.  Natural size 480 DIP x (the axis + 22 DIP per track);
8//        next_size sets another (the lanes share the height).  The axis has 1 / 2 / 5 steps
9//        with labels in seconds (the UI's decimal separator), down to nanoseconds (nine
10//        decimals; the narrowest view is 10 ns: simulated networks).  Each pixel column of a lane
11//        draws the ticks that fall into it as one bar (taller for more ticks, their colours
12//        stacked by share; a column alone is two wide), so many thousands of ticks stay
13//        cheap.  The playhead (st.t) is an accent line with its time on the axis.
14//        Mouse: a press in the lanes seeks and drags the playhead, so does its time on the
15//        axis; dragging the axis elsewhere pans the view, a click there seeks, a double
16//        click shows the whole span; Shift+drag in the lanes selects a range (st.sel,
17//        Shift+click clears it); Ctrl+wheel zooms around the pointer, Shift+wheel and the
18//        horizontal wheel pan (the plain wheel is left to scroll views around it).  Keys
19//        (focused): Left / Right the previous / next tick of any track, Home / End the
20//        start / the live end (End follows it again), PgUp / PgDn a view width, + / - zoom
21//        at the playhead, Esc clears the range.
22//        EV_ACT: the user moved st.t; EV_COMMIT: a drag of the playhead ended (a key or a
23//        click gives both).  st.follow keeps the playhead at the live end and the view
24//        with it; moving the playhead by hand ends that.  st.view is the visible range
25//        (empty: the whole span; zoomed out fully it grows with the span).  Seeks by the
26//        pointer are rounded to the decimal resolution of a pixel.
27//
28// Ids: id (the lanes; focus), part(id, 1) the axis, part(id, 2) the playhead's time.
29// Records: K_SLIDER id (value, min, max: t and the span in ms), K_TEXT per track label
30// (value: its number of ticks), K_TEXT part(id, 2) with the playhead's time ("1,250 s").
31// Everything keeps its left-to-right order in mirrored windows (ltr_begin).
32
33use alloc::format;
34use alloc::rc::Rc;
35use alloc::string::String;
36use alloc::vec::Vec;
37
38use crate::cmd::{Draw, DrawRef, Gfx};
39use crate::consts::*;
40use crate::keys::*;
41use crate::stock::text_at;
42use crate::Ui;
43
44const LANE_H: i32 = 22; // DIP per track
45const MIN_VIEW: f64 = 1e-8; // the narrowest view (s): its axis labels still differ in 9 decimals (ns)
46const ZOOM: f64 = 1.25; // per wheel notch / key
47const SCAN: usize = 64; // the ticks of a column whose colours are counted
48
49// what a press started
50const M_NONE: u8 = 0;
51const M_SCRUB: u8 = 1;
52const M_PAN: u8 = 2;
53const M_SELECT: u8 = 3;
54
55/// one lane of a timeline: a label and its event ticks
56#[derive(Clone, Copy, Debug)]
57pub struct TlTrack<'a> {
58    pub label: &'a str,
59    /// (time in s, 0xAARRGGBB; 0 = the chart palette colour of the track), sorted by time
60    pub ticks: &'a [(f64, u32)],
61}
62
63/// the app-owned state of a timeline: playhead, visible range, selected range, following
64#[derive(Clone, Default)]
65pub struct TimelineState {
66    /// the playhead (s)
67    pub t: f64,
68    /// the visible time range (s); empty (the default) = the whole span
69    pub view: (f64, f64),
70    /// a selected time range (Shift+drag), start <= end
71    pub sel: Option<(f64, f64)>,
72    /// keep the playhead at the live end of the span (moving it by hand ends this)
73    pub follow: bool,
74    /// modifier keys (MOD_*) treated as held: scripts cannot hold Ctrl or Shift during a
75    /// `wheel` (an app maps a test command to it)
76    pub test_mods: u32,
77    mode: u8,
78    part: u8,
79    dragged: bool,
80    click: Option<(u32, i32)>,
81    fitted: bool,
82    seen: (f64, f64),
83    grab_t: f64,
84    grab_dx: i32,
85    grab_view: (f64, f64),
86    cache: Option<(u64, DrawRef)>,
87}
88
89impl TimelineState {
90    /// show the whole span again
91    pub fn fit(&mut self) {
92        self.view = (0.0, 0.0);
93    }
94}
95
96fn part(id: u32, k: u32) -> u32 {
97    id.wrapping_mul(8).wrapping_add(k) | 0x8000_0000
98}
99
100fn floor_f(x: f64) -> f64 {
101    let f = x as i64 as f64;
102    if f > x { f - 1.0 } else { f }
103}
104
105// x in ms, rounded (records)
106fn ms(x: f64) -> i32 {
107    let v = x * 1000.0;
108    (if v < 0.0 { v - 0.5 } else { v + 0.5 }) as i32
109}
110
111// the axis step: the smallest 1 / 2 / 5 x 10^k that is at least min -> (step, minor ticks
112// per step)
113fn nice_step(min: f64) -> (f64, u32) {
114    if !min.is_finite() || min > 1e12 {
115        return (1e12, 5);
116    }
117    let min = if min > 1e-9 { min } else { 1e-9 };
118    let mut p = 1.0f64;
119    while p * 10.0 <= min {
120        p *= 10.0;
121    }
122    while p > min {
123        p /= 10.0;
124    }
125    if p >= min {
126        (p, 5)
127    } else if p * 2.0 >= min {
128        (p * 2.0, 4)
129    } else if p * 5.0 >= min {
130        (p * 5.0, 5)
131    } else {
132        (p * 10.0, 5)
133    }
134}
135
136// the decimals that show multiples of step (0..9: down to nanoseconds, simulated networks)
137fn decimals(step: f64) -> u32 {
138    let mut d = 0;
139    let mut s = step;
140    while d < 9 && s < 0.999_999 {
141        s *= 10.0;
142        d += 1;
143    }
144    d
145}
146
147// t rounded to the power of ten not above the time of a pixel
148fn snap(t: f64, per_px: f64) -> f64 {
149    if !(per_px > 1e-9) {
150        return t;
151    }
152    let mut q = 1.0f64;
153    while q > per_px {
154        q /= 10.0;
155    }
156    while q * 10.0 <= per_px {
157        q *= 10.0;
158    }
159    floor_f(t / q + 0.5) * q
160}
161
162// log2 of n, linear between the powers of two (0 for n <= 1)
163fn log2f(n: u32) -> f64 {
164    if n <= 1 {
165        return 0.0;
166    }
167    let e = 31 - n.leading_zeros();
168    e as f64 + (n as f64 / (1u64 << e) as f64 - 1.0)
169}
170
171// the zoom factor of a number of wheel notches (> 1: in), ZOOM per notch
172fn zoom_factor(notches: f64) -> f64 {
173    let mut k = if notches < 0.0 { -notches } else { notches };
174    let mut f = 1.0;
175    while k >= 1.0 {
176        f *= ZOOM;
177        k -= 1.0;
178    }
179    f *= 1.0 + (ZOOM - 1.0) * k;
180    if notches < 0.0 { 1.0 / f } else { f }
181}
182
183// the view moved inside full (as wide as it was, at most as wide as full)
184fn clamp_view(v: (f64, f64), full: (f64, f64)) -> (f64, f64) {
185    let fw = full.1 - full.0;
186    let w = (v.1 - v.0).min(fw).max(MIN_VIEW.min(fw));
187    let mut a = v.0;
188    if a + w > full.1 {
189        a = full.1 - w;
190    }
191    if a < full.0 {
192        a = full.0;
193    }
194    (a, a + w)
195}
196
197// the view zoomed by f (> 1: in) around time at, which keeps its place
198fn zoom_view(v: (f64, f64), at: f64, f: f64, full: (f64, f64)) -> (f64, f64) {
199    let w = v.1 - v.0;
200    let fw = full.1 - full.0;
201    let nw = (w / f).min(fw).max(MIN_VIEW.min(fw));
202    let k = if w > 0.0 { (at - v.0) / w } else { 0.5 };
203    let a = at - k * nw;
204    clamp_view((a, a + nw), full)
205}
206
207// the view moved so that t is inside it (a tenth of its width from the edge it came in at)
208fn reveal(v: (f64, f64), t: f64, full: (f64, f64)) -> (f64, f64) {
209    let w = v.1 - v.0;
210    if t < v.0 {
211        clamp_view((t - w * 0.1, t + w * 0.9), full)
212    } else if t > v.1 {
213        clamp_view((t - w * 0.9, t + w * 0.1), full)
214    } else {
215        v
216    }
217}
218
219// the latest tick of any track before t / the earliest after it
220fn tick_before(tracks: &[TlTrack], t: f64) -> Option<f64> {
221    let mut best: Option<f64> = None;
222    for tr in tracks {
223        let i = tr.ticks.partition_point(|k| k.0 < t);
224        if i > 0 {
225            let x = tr.ticks[i - 1].0;
226            if best.is_none_or(|b| x > b) {
227                best = Some(x);
228            }
229        }
230    }
231    best
232}
233
234fn tick_after(tracks: &[TlTrack], t: f64) -> Option<f64> {
235    let mut best: Option<f64> = None;
236    for tr in tracks {
237        let i = tr.ticks.partition_point(|k| k.0 <= t);
238        if let Some(k) = tr.ticks.get(i) {
239            if best.is_none_or(|b| k.0 < b) {
240                best = Some(k.0);
241            }
242        }
243    }
244    best
245}
246
247// a column of a lane: its x (px from the plot), how many ticks, up to three colours with the
248// number of ticks of each among the first SCAN
249#[derive(Clone, Copy)]
250struct Col {
251    lane: u32,
252    x: i32,
253    n: u32,
254    c: [(u32, u32); 3],
255}
256
257// the columns (bw px wide) of a lane's visible ticks: one partition_point per column
258#[allow(clippy::too_many_arguments)]
259fn bin_lane(ticks: &[(f64, u32)], lane: u32, v0: f64, pps: f64, bw: i32, pw: i32, pal: u32, out: &mut Vec<Col>) {
260    let ncol = ((pw + bw - 1) / bw).max(1);
261    let v1 = v0 + pw as f64 / pps;
262    let a = ticks.partition_point(|k| k.0 < v0);
263    let b = a + ticks[a..].partition_point(|k| k.0 <= v1);
264    let vis = &ticks[a..b];
265    let cps = pps / bw as f64; // columns per second
266    let mut i = 0;
267    while i < vis.len() {
268        let f = (vis[i].0 - v0) * cps;
269        let c = if f > 0.0 { (f as i64).min(ncol as i64 - 1) as i32 } else { 0 };
270        let j = if c + 1 >= ncol {
271            vis.len()
272        } else {
273            let end = v0 + (c + 1) as f64 / cps;
274            i + 1 + vis[i + 1..].partition_point(|k| k.0 < end)
275        };
276        let mut col = Col { lane, x: c * bw, n: (j - i) as u32, c: [(0, 0); 3] };
277        for k in &vis[i..j.min(i + SCAN)] {
278            let cc = if k.1 == 0 { pal } else { k.1 };
279            if let Some(s) = col.c.iter_mut().find(|s| s.1 != 0 && s.0 == cc) {
280                s.1 += 1;
281            } else if let Some(s) = col.c.iter_mut().find(|s| s.1 == 0) {
282                *s = (cc, 1);
283            } else {
284                col.c[2].1 += 1;
285            }
286        }
287        out.push(col);
288        i = j;
289    }
290}
291
292// the bars of the columns (x, y, w, h, colour from the top left of the lanes): the height
293// grows with log2 of the ticks (relative to the fullest column of all lanes), the colours
294// are stacked by share, the most frequent at the bottom, each at least a pixel
295fn bars(cols: &[Col], lane_h: i32, pad: i32, bw: i32) -> Vec<(i32, i32, i32, i32, u32)> {
296    let maxn = cols.iter().map(|c| c.n).max().unwrap_or(1);
297    let lmax = log2f(maxn);
298    let avail = (lane_h - pad * 2).max(2);
299    let hmin = (avail * 2 / 5).max(2).min(avail);
300    let mut out = Vec::with_capacity(cols.len() + cols.len() / 4);
301    for (k, c) in cols.iter().enumerate() {
302        let h = if lmax > 0.0 { hmin + ((avail - hmin) as f64 * log2f(c.n) / lmax) as i32 } else { hmin };
303        let y = c.lane as i32 * lane_h + (lane_h - h) / 2;
304        // a column alone in its lane is drawn two columns wide
305        let before = k > 0 && cols[k - 1].lane == c.lane && cols[k - 1].x + bw >= c.x;
306        let after = k + 1 < cols.len() && cols[k + 1].lane == c.lane && cols[k + 1].x <= c.x + bw;
307        let (x, w) = if before || after { (c.x, bw) } else { (c.x - bw / 2, bw * 2) };
308        let mut s = c.c;
309        s.sort_by(|a, b| b.1.cmp(&a.1));
310        let total: u32 = s.iter().map(|x| x.1).sum();
311        let used = s.iter().filter(|x| x.1 > 0).count() as i32;
312        let (mut top, mut rest) = (y, h);
313        for (i, &(cc, cn)) in s.iter().filter(|x| x.1 > 0).rev().enumerate() {
314            let left = used - 1 - i as i32; // segments below this one
315            let hs = if left == 0 { rest } else { ((h as i64 * cn as i64 / total.max(1) as i64) as i32).max(1).min(rest - left).max(0) };
316            if hs > 0 {
317                out.push((x, top, w, hs, cc));
318            }
319            top += hs;
320            rest -= hs;
321        }
322    }
323    out
324}
325
326// the bars of the lanes (drawn at render time; kept while they do not change)
327struct Bars(Vec<(i32, i32, i32, i32, u32)>);
328
329impl Draw for Bars {
330    fn draw(&self, g: &mut Gfx, r: [i32; 4]) {
331        for &(x, y, w, h, c) in &self.0 {
332            g.dc.set_color(c);
333            g.dc.fill(g.sf, r[0] + x, r[1] + y, w, h);
334        }
335    }
336}
337
338struct Fnv(u64);
339
340impl Fnv {
341    fn u32(&mut self, v: u32) {
342        for b in v.to_le_bytes() {
343            self.0 ^= b as u64;
344            self.0 = self.0.wrapping_mul(0x0000_0100_0000_01B3);
345        }
346    }
347}
348
349impl Ui {
350    // a time as the timeline shows it: "1,25 s"; below a microsecond with up to nine decimals
351    // ("0,000000722 s"; fmt_f64 stops at six)
352    fn tl_text(&self, t: f64, d: u32) -> String {
353        if d <= 6 {
354            return format!("{} s", self.fmt_f64(t, d));
355        }
356        let neg = t < 0.0;
357        let ns = ((if neg { -t } else { t }) * 1e9 + 0.5) as u64;
358        let frac = format!("{:09}", ns % 1_000_000_000);
359        let sep = char::from_u32(self.m.value.decimal_sep).unwrap_or(',');
360        format!("{}{}{}{} s", if neg { "-" } else { "" }, ns / 1_000_000_000, sep, &frac[..(d as usize).min(9)])
361    }
362
363    // the axis of a view over pw px -> (step, minor ticks per step, decimals): labels at
364    // least 80 DIP apart and never closer than their width
365    fn tl_axis(&mut self, v: (f64, f64), pw: i32) -> (f64, u32, u32) {
366        let pps = pw as f64 / (v.1 - v.0);
367        let big = if -v.0 > v.1 { v.0 } else { v.1 };
368        self.ts_font(TS_CAPTION);
369        let mut r = nice_step(self.dp(80) as f64 / pps);
370        for _ in 0..3 {
371            let s = self.tl_text(big, decimals(r.0));
372            let need = (self.text.width(&s) + self.dp(16)) as f64 / pps;
373            if r.0 >= need {
374                break;
375            }
376            r = nice_step(need);
377        }
378        (r.0, r.1, decimals(r.0))
379    }
380
381    // the playhead's time on the axis: its rectangle (x centred on cx, inside lo .. hi)
382    fn tl_head(&mut self, s: &str, cx: i32, y: i32, lo: i32, hi: i32) -> [i32; 4] {
383        self.ts_font(TS_CAPTION);
384        let w = self.text.width(s) + self.dp(12);
385        let h = self.ts_line(TS_CAPTION) + self.dp(2);
386        let x = (cx - w / 2).min(hi - w).max(lo);
387        [x, y, w, h]
388    }
389
390    /// ui_timeline(id, &mut st, span, tracks) -> EV_*: a time axis, a lane of event ticks per
391    /// track and a playhead (st.t) to seek with (see the module header of timeline.rs)
392    #[cfg_attr(jfgui_dev, track_caller)]
393    pub fn timeline(&mut self, id: u32, st: &mut TimelineState, span: (f64, f64), tracks: &[TlTrack]) -> u32 {
394        let _src = self.src_scope();
395        let n = tracks.len() as i32;
396        let line = self.ts_line(TS_CAPTION);
397        let axis_h = line + self.dp(10);
398        let r = self.place(self.dp(480), axis_h + self.dp(LANE_H) * n.max(1));
399        let lane_h = if n > 0 { ((r[3] - axis_h) / n).clamp(self.dp(12), self.dp(48)) } else { 0 };
400        let hair = self.hair();
401        // the label column, the plot (the pixels of the time axis), the lanes
402        self.ts_font(TS_CAPTION);
403        let mut lw = 0;
404        for tr in tracks {
405            lw = lw.max(self.text.width(crate::basic::cstr(tr.label)));
406        }
407        let lw = if lw > 0 { (lw + self.dp(16)).min(r[2] * 3 / 10) } else { 0 };
408        let m = self.dp(12);
409        let px = r[0] + lw + m;
410        let pw = (r[2] - lw - m * 2).max(1);
411        let ly = r[1] + axis_h;
412        let lh = (r[3] - axis_h).max(1);
413        let (head_lo, head_hi) = (r[0] + lw + hair, r[0] + r[2] - hair);
414        let head_y = r[1] + self.dp(3);
415        // the span, the playhead and the view
416        let s0 = if span.0.is_finite() { span.0 } else { 0.0 };
417        let s1 = if span.1.is_finite() && span.1 > s0 { span.1 } else { s0 };
418        let full = (s0, if s1 > s0 { s1 } else { s0 + 1.0 });
419        let sel_in = st.sel;
420        let mut t = if st.t.is_finite() { st.t.clamp(s0, s1) } else { s0 };
421        // zoomed out fully (and not changed by the app since), the view grows with the span
422        let mut v = st.view;
423        if (st.fitted && v == st.seen) || !(v.0.is_finite() && v.1.is_finite() && v.1 > v.0) {
424            v = full;
425        }
426        v = clamp_view(v, full);
427        if st.follow {
428            t = s1;
429            v = reveal(v, t, full);
430        }
431        // input maps the pointer with the view as it shows now
432        let v_in = v;
433        let pps = pw as f64 / (v.1 - v.0);
434        let per_px = 1.0 / pps;
435        let time_at = |x: i32| v_in.0 + ((x - px) as f64 + 0.5) / pps;
436        let x_at = |v: (f64, f64), t: f64| px + floor_f(((t - v.0) * (pw as f64 / (v.1 - v.0))).clamp(-1e6, 1e6)) as i32;
437        // hit boxes: the axis, the lanes (focus), the playhead's time on top
438        let pid_axis = part(id, 1);
439        let pid_head = part(id, 2);
440        self.ltr_begin(r[0], r[2]);
441        let rtl = self.rtl;
442        let lx = |x: i32, w: i32| if rtl { r[0] + r[0] + r[2] - x - w } else { x }; // logical x of a span drawn LTR
443        let mx = self.ltr_x(self.wp.ctx.mx);
444        let pmx = self.ltr_x(self.wp.ctx.press_x);
445        let (_, _, dec) = self.tl_axis(v, pw);
446        let ht = self.tl_text(t, dec.max(3));
447        let hr = self.tl_head(&ht, x_at(v, t), head_y, head_lo, head_hi);
448        let ev_axis = self.hit_rect(pid_axis, r[0], r[1], r[2], axis_h, HB_MOVES);
449        let ev_lane = self.hit_rect(id, r[0], ly, r[2], lh, HB_FOCUS | HB_MOVES);
450        let ev_head = self.hit_rect(pid_head, lx(hr[0], hr[2]), hr[1], hr[2], hr[3], HB_MOVES | CUR_SIZE_WE << HB_CURSOR_SHIFT);
451        let evs = ev_axis | ev_lane | ev_head;
452        let mut out = 0u32;
453        let mut user_t: Option<f64> = None;
454        let mut commit = false;
455        let mut keyed = false;
456        let mut live = false;
457        // a press: the keyboard focus and what the drag does
458        let (act, pressed) = (self.wp.ctx.active, self.wp.ctx.pressed & (1 << BTN_LEFT) != 0);
459        let ours = act == id || act == pid_axis || act == pid_head;
460        if pressed && ours {
461            let c = &mut self.wp.ctx;
462            c.focus = id;
463            c.focus_visible = false;
464            st.dragged = false;
465            st.part = if act == pid_axis { 1 } else if act == pid_head { 2 } else { 0 };
466            // a second click on the axis (the first one put the playhead's time under the
467            // pointer, so it may land there): the whole span
468            let now = self.now_ms;
469            let again = st.click.is_some_and(|(ms, x)| now.wrapping_sub(ms) <= self.dbl_ms && (mx - x).abs() <= self.dp(4));
470            st.click = None;
471            st.mode = if st.part != 0 && again {
472                v = full;
473                M_NONE
474            } else if st.part == 1 {
475                st.grab_view = v;
476                M_PAN
477            } else if st.part == 0 && self.mods & MOD_SHIFT != 0 {
478                let a = snap(time_at(mx).clamp(v.0, v.1), per_px);
479                st.grab_t = a;
480                st.sel = Some((a, a));
481                M_SELECT
482            } else {
483                // the playhead's time: grabbed where it is (no jump)
484                st.grab_dx = if st.part == 2 { mx - x_at(v, t).clamp(px, px + pw) } else { 0 };
485                M_SCRUB
486            };
487        }
488        if evs & EV_HELD != 0 {
489            if evs & EV_DRAG != 0 {
490                st.dragged = true;
491            }
492            match st.mode {
493                M_SCRUB if st.part != 2 || st.dragged => {
494                    user_t = Some(snap(time_at(mx - st.grab_dx).clamp(v.0, v.1), per_px));
495                }
496                M_PAN if st.dragged => {
497                    let g = st.grab_view;
498                    let dt = (mx - pmx) as f64 * (g.1 - g.0) / pw as f64;
499                    v = clamp_view((g.0 - dt, g.1 - dt), full);
500                }
501                M_SELECT => {
502                    let (a, b) = (st.grab_t, snap(time_at(mx).clamp(v.0, v.1), per_px));
503                    st.sel = Some(if a <= b { (a, b) } else { (b, a) });
504                }
505                _ => {}
506            }
507        }
508        if evs & EV_COMMIT != 0 {
509            match st.mode {
510                M_SCRUB => commit = true,
511                M_PAN if !st.dragged => {
512                    user_t = Some(snap(time_at(pmx).clamp(v_in.0, v_in.1), per_px));
513                    commit = true;
514                }
515                M_SELECT if st.sel.is_some_and(|s| s.1 <= s.0) => st.sel = None,
516                _ => {}
517            }
518            // a click on the axis or the playhead's time (a double click's first half)
519            if st.mode != M_NONE && st.part != 0 && !st.dragged {
520                st.click = Some((self.now_ms, pmx));
521            }
522            st.mode = M_NONE;
523        } else if !ours {
524            st.mode = M_NONE;
525        }
526        // the wheel: Ctrl zooms around the pointer, Shift / the horizontal wheel pan
527        if evs & EV_HOVER != 0 {
528            let mods = self.mods | st.test_mods;
529            let c = &mut self.wp.ctx;
530            let (wx, wy) = (c.wheel_x, c.wheel_y);
531            if mods & MOD_CTRL != 0 && wy != 0 {
532                v = zoom_view(v, time_at(mx).clamp(v.0, v.1), zoom_factor(wy as f64 / 120.0), full);
533                c.wheel_x = 0;
534                c.wheel_y = 0;
535            } else if wx != 0 || (mods & MOD_SHIFT != 0 && wy != 0) {
536                let d = (wx - if mods & MOD_SHIFT != 0 { wy } else { 0 }) as f64 / 120.0;
537                let dt = (v.1 - v.0) * 0.1 * d;
538                let nv = clamp_view((v.0 + dt, v.1 + dt), full);
539                if nv != v {
540                    v = nv;
541                    c.wheel_x = 0;
542                    c.wheel_y = 0;
543                }
544            }
545        }
546        // keys: ticks, ends, pages, zoom, Esc
547        if ev_lane & EV_FOCUS != 0 {
548            let swap = rtl && self.mods & (MOD_CTRL | MOD_ALT) == 0; // the arrows as drawn
549            let (kp, kn) = if swap { (VK_RIGHT, VK_LEFT) } else { (VK_LEFT, VK_RIGHT) };
550            let w = v.1 - v.0;
551            let mut k: Option<Option<f64>> = None;
552            if self.key(kp) {
553                k = Some(tick_before(tracks, t));
554            }
555            if self.key(kn) {
556                k = Some(tick_after(tracks, t));
557            }
558            if self.key(VK_HOME) {
559                k = Some(Some(s0));
560            }
561            if self.key(VK_END) {
562                k = Some(Some(s1));
563                live = true;
564            }
565            if self.key(VK_PRIOR) {
566                k = Some(Some(t - w));
567            }
568            if self.key(VK_NEXT) {
569                k = Some(Some(t + w));
570            }
571            if let Some(k) = k {
572                self.wp.ctx.focus_visible = true;
573                if let Some(x) = k {
574                    user_t = Some(x);
575                    keyed = true;
576                    commit = true;
577                }
578            }
579            let zin = self.key(VK_OEM_PLUS) || self.key(VK_ADD);
580            let zout = self.key(VK_OEM_MINUS) || self.key(VK_SUBTRACT);
581            if zin || zout {
582                let at = if t >= v.0 && t <= v.1 { t } else { (v.0 + v.1) / 2.0 };
583                v = zoom_view(v, at, if zin { ZOOM } else { 1.0 / ZOOM }, full);
584                self.wp.ctx.focus_visible = true;
585            }
586            if st.sel.is_some() && self.key(VK_ESCAPE) {
587                st.sel = None;
588            }
589        }
590        // a screen reader set the value (ms)
591        if let Some(x) = self.a11y_value(id) {
592            user_t = Some(x as f64 / 1000.0);
593            keyed = true;
594            commit = true;
595        }
596        if let Some(x) = user_t {
597            let x = x.clamp(s0, s1);
598            st.follow = false;
599            if x != t {
600                t = x;
601                out |= EV_ACT;
602            }
603            if keyed {
604                v = reveal(v, t, full);
605            }
606        }
607        if commit {
608            out |= EV_COMMIT;
609        }
610        if live {
611            st.follow = true;
612        }
613        if st.follow {
614            t = s1;
615            v = reveal(v, t, full);
616        }
617        if t != st.t || v != st.view || st.sel != sel_in {
618            self.repass();
619        }
620        st.t = t;
621        st.view = v;
622        st.seen = v;
623        st.fitted = v.0 <= full.0 && v.1 >= full.1;
624        // the record of the scrubber (first: a screen reader names it from the text before it)
625        let mut ss = 0u32;
626        if evs & EV_HOVER != 0 {
627            ss |= SS_HOVER as u32;
628        }
629        if evs & EV_HELD != 0 {
630            ss |= SS_PRESSED as u32;
631        }
632        if ev_lane & EV_FOCUS != 0 {
633            ss |= SS_FOCUSED as u32;
634        }
635        self.wp.ctx.last_rect = r;
636        self.wp.ctx.last_state = ss;
637        let rec = self.sem(K_SLIDER, id, None, ms(t));
638        rec.min = ms(s0);
639        rec.max = ms(s1);
640
641        // ---- drawing (the view and the playhead after the input)
642        let pps = pw as f64 / (v.1 - v.0);
643        let (step, minor, dec) = self.tl_axis(v, pw);
644        let (ctrl, stroke, div) = (self.color(T_CTRL), self.color(T_CTRL_STROKE), self.color(T_DIVIDER));
645        let (t2, t3, acc) = (self.color(T_TEXT2), self.color(T_TEXT3), self.color(T_ACCENT));
646        self.cmd_rrect(r[0], r[1], r[2], r[3], self.dp(4), hair, ctrl, stroke, stroke);
647        self.clip_push(r[0] + hair, r[1] + hair, r[2] - hair * 2, r[3] - hair * 2);
648        // the axis line, the label column, the lanes' separators and labels
649        self.draw_color(div);
650        self.draw_rect(r[0], ly - hair, r[2], hair);
651        if lw > 0 {
652            self.draw_rect(r[0] + lw, ly, hair, lh);
653        }
654        for (k, tr) in tracks.iter().enumerate() {
655            let y = ly + lane_h * k as i32;
656            if k > 0 {
657                self.draw_color(div);
658                self.draw_rect(r[0], y, r[2], hair);
659            }
660            if lw > 0 {
661                let label = crate::basic::cstr(tr.label);
662                let maxw = lw - self.dp(12);
663                self.ts_font(TS_CAPTION);
664                let s = if self.text.width(label) > maxw { self.ellipsize(label.as_bytes(), maxw).0 } else { String::from(label) };
665                let x = r[0] + self.dp(8);
666                text_at(self, TS_CAPTION, x, [x, y, maxw, lane_h], &s, t2, 0);
667                self.wp.ctx.last_rect = [lx(r[0], lw), y, lw, lane_h];
668                self.wp.ctx.last_state = 0;
669                self.sem(K_TEXT, 0, Some(label), tr.ticks.len().min(i32::MAX as usize) as i32);
670            }
671        }
672        // ticks of the axis with their labels, grid lines through the lanes
673        let (sub, nsub) = if step / minor as f64 * pps >= self.dp(6) as f64 { (step / minor as f64, minor as i64) } else { (step, 1) };
674        let mut j = floor_f(v.0 / sub) as i64;
675        let (big, small) = (self.dp(5), self.dp(3));
676        for _ in 0..4096 {
677            let tt = j as f64 * sub;
678            let major = j % nsub == 0;
679            j += 1;
680            if tt < v.0 - sub * 1e-6 {
681                continue;
682            }
683            if tt > v.1 + sub * 1e-6 {
684                break;
685            }
686            let x = x_at(v, tt);
687            if !major {
688                self.draw_color(div);
689                self.draw_rect(x, ly - small, hair, small);
690                continue;
691            }
692            self.draw_color(div);
693            self.draw_rect(x, ly, hair, lh);
694            self.draw_color(t3);
695            self.draw_rect(x, ly - big, hair, big);
696            let s = self.tl_text(tt, dec);
697            self.ts_font(TS_CAPTION);
698            let w = self.text.width(&s);
699            let tx = x + self.dp(4);
700            if tx + w <= r[0] + r[2] - self.dp(2) {
701                text_at(self, TS_CAPTION, tx, [tx, head_y, w, line + self.dp(2)], &s, t2, 0);
702            }
703        }
704        // the selected range under the ticks
705        let sel_x = st.sel.map(|(a, b)| (x_at(v, a).max(px - hair), x_at(v, b).min(px + pw)));
706        if let Some((xa, xb)) = sel_x {
707            if xb >= xa {
708                self.draw_color((acc & 0x00FF_FFFF) | 0x3000_0000);
709                self.draw_rect(xa, ly, (xb - xa).max(hair), lh);
710            }
711        }
712        // the ticks: a column per pixel (or per hair at higher dpi), one cached draw object
713        let pal = self.style.data().chart_palette(self);
714        let mut cols = Vec::new();
715        if lane_h > 0 {
716            for (k, tr) in tracks.iter().enumerate() {
717                bin_lane(tr.ticks, k as u32, v.0, pps, hair, pw, pal[k % 8], &mut cols);
718            }
719        }
720        if !cols.is_empty() {
721            let bars = bars(&cols, lane_h, self.dp(3), hair);
722            let mut h = Fnv(0xCBF2_9CE4_8422_2325);
723            for &(x, y, w, bh, c) in &bars {
724                for q in [x as u32, y as u32, w as u32, bh as u32, c] {
725                    h.u32(q);
726                }
727            }
728            h.u32(bars.len() as u32);
729            let hash = h.0;
730            let d = match &st.cache {
731                Some((k, d)) if *k == hash => d.clone(),
732                _ => {
733                    let d: DrawRef = Rc::new(Bars(bars));
734                    st.cache = Some((hash, d.clone()));
735                    d
736                }
737            };
738            self.cmd_custom(px, ly, pw, lane_h * n, Some(d), 0, hash as u32);
739        } else {
740            st.cache = None;
741        }
742        if let Some((xa, xb)) = sel_x {
743            if xb >= xa {
744                self.draw_color(acc);
745                self.draw_rect(xa, ly, hair, lh);
746                self.draw_rect(xb, ly, hair, lh);
747            }
748        }
749        // where a click would seek
750        if evs & EV_HOVER != 0 && st.mode == M_NONE && ev_head & EV_HOVER == 0 && mx >= px && mx < px + pw {
751            self.draw_color(t3);
752            self.draw_rect(mx, r[1] + hair, hair, r[3] - hair * 2);
753        }
754        // the playhead: a line through the lanes, its time on the axis
755        let ht = self.tl_text(t, dec.max(3));
756        let x = x_at(v, t);
757        let hr = self.tl_head(&ht, x, head_y, head_lo, head_hi);
758        let pwid = (hair * 2).max(2);
759        if x >= px - hair && x <= px + pw {
760            self.draw_color(acc);
761            self.draw_rect(x - pwid / 2, hr[1] + hr[3], pwid, r[1] + r[3] - hr[1] - hr[3]);
762        }
763        self.cmd_rrect(hr[0], hr[1], hr[2], hr[3], hr[3] / 2, 0, acc, 0, 0);
764        let on = self.color(T_ON_ACCENT);
765        text_at(self, TS_CAPTION, hr[0] + self.dp(6), hr, &ht, on, 0);
766        self.wp.ctx.last_rect = [lx(hr[0], hr[2]), hr[1], hr[2], hr[3]];
767        self.wp.ctx.last_state = 0;
768        self.sem(K_TEXT, pid_head, Some(&ht), ms(t));
769        self.clip_pop();
770        self.ltr_end();
771        if self.focus_ring(ev_lane) {
772            let s = self.style;
773            s.focus_ring(self, r, 4);
774        }
775        self.wp.ctx.last_rect = r;
776        self.wp.ctx.last_state = ss;
777        out | (evs & (EV_HOVER | EV_HELD)) | (ev_lane & EV_FOCUS)
778    }
779}