rt_graph/
graph.rs

1use crate::{Color, DataSource, observable_value, Point, Result, Store, Time, Value};
2use gdk::prelude::*;
3use glib::source::Continue;
4use gtk::prelude::*;
5use std::{
6    cell::{Cell, RefCell, RefMut},
7    rc::Rc,
8    time::Instant,
9};
10
11const BYTES_PER_PIXEL: usize = 4;
12const BACKGROUND_COLOR: (f64, f64, f64) = (0.4, 0.4, 0.4);
13const DRAWN_AREA_BACKGROUND_COLOR: (f64, f64, f64) = (0.0, 0.0, 0.0);
14
15struct State {
16    backing_surface: RefCell<cairo::Surface>,
17    temp_surface: RefCell<cairo::Surface>,
18
19    store: RefCell<Store>,
20
21    drawing_area: gtk::DrawingArea,
22
23    view_write: RefCell<observable_value::WriteHalf<View>>,
24    view_read: RefCell<observable_value::ReadHalf<View>>,
25
26    fps_count: Cell<u16>,
27    fps_timer: Cell<Instant>,
28
29    config: Config,
30
31    tick_id: RefCell<TickId>,
32}
33
34enum TickId {
35    IngestOnly(glib::source::SourceId),
36    EveryFrame(gtk::TickCallbackId),
37    None,
38}
39
40/// Describes what is currently showing on the graph.
41#[derive(Clone, Debug)]
42pub struct View {
43    /// Zoom level, in units of t per x pixel
44    pub zoom_x: f64,
45
46    /// The most recently drawn time value.
47    pub last_drawn_t: Time,
48
49    /// The most recently drawn x pixel.
50    pub last_drawn_x: u32,
51
52    /// The longest ago time value that is still stored. Note
53    /// that the oldest data is discarded to keep memory usage bounded.
54    pub min_t: Time,
55
56    /// The most recent time value.
57    pub max_t: Time,
58
59    /// The display mode.
60    pub mode: ViewMode,
61}
62
63/// Describes the display mode of the graph
64#[derive(Clone, Debug, Eq, PartialEq)]
65pub enum ViewMode {
66    /// Graph is following the latest data
67    Following,
68
69    /// Graph is scrolled to a particular point in time
70    Scrolled,
71}
72
73impl View {
74    fn default_from_config(c: &Config) -> View {
75        View {
76            zoom_x: c.base_zoom_x,
77            last_drawn_t: 0,
78            last_drawn_x: 0,
79            min_t: 0,
80            max_t: 0,
81            mode: ViewMode::Following,
82        }
83    }
84}
85
86/// The configuration required by a `Graph` or `GraphWithControls`
87///
88/// Create an instance using a `ConfigBuilder`:
89///
90/// ```
91/// use rt_graph::{Config, ConfigBuilder, TestDataGenerator};
92///
93/// let config: Config =
94///     ConfigBuilder::default()
95///         // Chain ConfigBuilder methods here
96///         .data_source(TestDataGenerator::new())
97///         .build()
98///         .unwrap();
99/// ```
100#[derive(Builder, Debug)]
101#[builder(pattern = "owned")]
102pub struct Config {
103    /// Maximum zoom out, in units of t per x pixel
104    #[builder(default = "1000.0")]
105    base_zoom_x: f64,
106
107    /// Maximum zoom in, in units of t per x pixel
108    #[builder(default = "1.0")]
109    max_zoom_x: f64,
110
111    /// Graph width in pixels
112    #[builder(default = "800")]
113    graph_width: u32,
114
115    /// Graph height in pixels
116    #[builder(default = "200")]
117    graph_height: u32,
118
119    #[builder(private, setter(name = "data_source_internal"))]
120    data_source: RefCell<Box<dyn DataSource>>,
121
122    /// How many windows width of data to store at maximum zoom out.
123    #[builder(default = "100")]
124    windows_to_store: u32,
125
126    /// The style of point to draw
127    #[builder(default = "PointStyle::Point")]
128    point_style: PointStyle,
129}
130
131/// The style of point to draw
132#[derive(Clone, Copy, Debug)]
133pub enum PointStyle {
134    /// A point, a single pixel.
135    Point,
136
137    /// A cross of 5 pixels in the shape of an 'x'.
138    Cross,
139}
140
141impl ConfigBuilder {
142    /// The data source for the graph.
143    pub fn data_source<T: DataSource + 'static>(self, ds: T) -> Self {
144        self.data_source_internal(RefCell::new(Box::new(ds)))
145    }
146}
147
148/// A GTK widget that draws a graph.
149///
150/// `Graph` contains no controls to navigate it, you are expected to
151/// provide your own controls using the methods and signals it provides.
152/// Or you can use `GraphWithControls` that comes with a set of controls.
153pub struct Graph {
154    s: Rc<State>,
155}
156
157impl Graph {
158    /// Build and show a `Graph` widget in the target `gtk::Container`.
159    pub fn build_ui<C>(config: Config, container: &C, gdk_window: &gdk::Window) -> Graph
160        where C: IsA<gtk::Container> + IsA<gtk::Widget>
161    {
162
163        let drawing_area = gtk::DrawingAreaBuilder::new()
164            .height_request(config.graph_height as i32)
165            .width_request(config.graph_width as i32)
166            .build();
167        container.add(&drawing_area);
168
169        // Initialise State
170
171        let backing_surface = create_backing_surface(gdk_window,
172                                                     config.graph_width, config.graph_height);
173        let temp_surface = create_backing_surface(gdk_window,
174                                                  config.graph_width, config.graph_height);
175        let store = Store::new(config.data_source.borrow().get_num_values().unwrap() as u8);
176        let view = View::default_from_config(&config);
177        let (view_read, view_write) =
178            observable_value::ObservableValue::new(view.clone()).split();
179        let s = Rc::new(State {
180            backing_surface: RefCell::new(backing_surface),
181            temp_surface: RefCell::new(temp_surface),
182
183            store: RefCell::new(store),
184
185            drawing_area: drawing_area.clone(),
186
187            view_read: RefCell::new(view_read),
188            view_write: RefCell::new(view_write),
189
190            fps_count: Cell::new(0),
191            fps_timer: Cell::new(Instant::now()),
192
193            config,
194
195            tick_id: RefCell::new(TickId::None),
196        });
197        let graph = Graph {
198            s: s.clone(),
199        };
200
201        // Set signal handlers that require State
202        let sc = s.clone();
203        drawing_area.connect_draw(move |ctrl, ctx| {
204            graph_draw(ctrl, ctx, &*sc)
205        });
206
207        graph.set_frame_tick();
208
209        // Show everything recursively
210        drawing_area.show_all();
211
212        graph
213    }
214
215    fn set_frame_tick(&self) {
216        // Take the old value.
217        let old_tick_id = self.s.tick_id.replace(TickId::None);
218        match old_tick_id {
219            TickId::IngestOnly(id) => {
220                glib::source::source_remove(id);
221            },
222            TickId::EveryFrame(id) => {
223                // Already as desired, put old value back.
224                self.s.tick_id.replace(TickId::EveryFrame(id));
225                return;
226            },
227            TickId::None => (),
228        }
229
230        let sc = self.s.clone();
231        let frame_tick_id = self.s.drawing_area.add_tick_callback(move |_ctrl, _clock| {
232            tick(&*sc);
233            Continue(true)
234        });
235        *self.s.tick_id.borrow_mut() = TickId::EveryFrame(frame_tick_id);
236    }
237
238    fn set_ingest_tick(&self) {
239        // Take the old value.
240        let old_tick_id = self.s.tick_id.replace(TickId::None);
241        match old_tick_id {
242            TickId::EveryFrame(id) => {
243                id.remove();
244            },
245            TickId::IngestOnly(id) => {
246                // Already as desired, put old value back.
247                self.s.tick_id.replace(TickId::IngestOnly(id));
248                return;
249            }
250            TickId::None => (),
251        }
252
253        let sc = self.s.clone();
254        let ingest_tick_id =
255            glib::source::timeout_add_seconds_local(
256                1 /* seconds */,
257                move || {
258                    tick(&*sc);
259                    Continue(true)
260                });
261        *self.s.tick_id.borrow_mut() = TickId::IngestOnly(ingest_tick_id);
262    }
263
264    /// Show the graph.
265    pub fn show(&self) {
266        self.set_frame_tick();
267        self.s.drawing_area.show();
268    }
269
270    /// Hide the graph.
271    pub fn hide(&self) {
272        self.set_ingest_tick();
273        self.s.drawing_area.hide();
274    }
275
276    /// Return the width of the graph
277    pub fn width(&self) -> u32 {
278        self.s.config.graph_width
279    }
280
281    /// Return the height of the graph
282    pub fn height(&self) -> u32 {
283        self.s.config.graph_height
284    }
285
286    /// Return the initial and maximally zoomed out zoom level, in
287    /// units of time per x pixel.
288    pub fn base_zoom_x(&self) -> f64 {
289        self.s.config.base_zoom_x
290    }
291
292    /// Return the maximally zoomed in zoom level, in
293    /// units of time per x pixel.
294    pub fn max_zoom_x(&self) -> f64 {
295        self.s.config.max_zoom_x
296    }
297
298    /// Return a description of the current view
299    pub fn view(&self) -> View {
300        self.s.view_read.borrow().get()
301    }
302
303    /// Return the most recent time value.
304    pub fn last_t(&self) -> Time {
305        self.s.store.borrow().last_t()
306    }
307
308    /// Return the longest ago time value that is still stored. Note
309    /// that the oldest data is discarded to keep memory usage bounded.
310    pub fn first_t(&self) -> Time {
311        self.s.store.borrow().first_t()
312    }
313
314    fn _clone(&self) -> Graph {
315        Graph {
316            s: self.s.clone()
317        }
318    }
319
320    /// Change the zoom level on the graph.
321    ///
322    /// Any value you pass in will be clamped between `base_zoom_x` and `max_zoom_x`.
323    pub fn set_zoom_x(&self, new_zoom_x: f64) {
324        debug!("set_zoom_x new_zoom_x={}", new_zoom_x);
325        let new_zoom_x = new_zoom_x.min(self.s.config.base_zoom_x)
326            .max(self.s.config.max_zoom_x);
327        {
328            // Scope the mutable borrow of view.
329            let new_view = View {
330                zoom_x: new_zoom_x,
331                .. self.s.view_read.borrow().get()
332            };
333            self.s.view_write.borrow_mut().set(&new_view);
334        }
335
336        redraw_graph(&*self.s);
337    }
338
339    /// Sets the graph to follow the latest data.
340    pub fn set_follow(&self) {
341        debug!("set_follow");
342        {
343            // Scope the mutable borrow of view.
344            let new_view = View {
345                mode: ViewMode::Following,
346                last_drawn_t: self.s.store.borrow().last_t(),
347                .. self.s.view_read.borrow().get()
348            };
349            self.s.view_write.borrow_mut().set(&new_view);
350        }
351        redraw_graph(&*self.s);
352    }
353
354    /// Scrolls the graph to view a certain time value.
355    pub fn scroll(&self, new_val: f64) {
356        debug!("scroll new_val={}", new_val);
357        {
358            // Scope the borrow_mut on view
359            let mut view = self.s.view_read.borrow().get();
360            view.mode = ViewMode::Scrolled;
361            let new_t = (new_val as u32 +
362                         ((view.zoom_x * self.s.config.graph_width as f64) as u32))
363                .min(self.s.store.borrow().last_t());
364            // Snap new_t to a whole pixel.
365            let new_t = (((new_t as f64) / view.zoom_x).floor() * view.zoom_x) as u32;
366            view.last_drawn_t = new_t;
367            view.last_drawn_x = 0;
368            self.s.view_write.borrow_mut().set(&view);
369            debug!("scroll_change, v={:?} view={:?}", new_val, view);
370        }
371        // TODO: Maybe keep the section of the graph that's still valid when scrolling.
372        redraw_graph(&self.s);
373    }
374
375    /// Return an observable that lets you track the current `View`,
376    /// which describes what is currently showing on the graph.
377    pub fn view_observable(&mut self) -> RefMut<observable_value::ReadHalf<View>> {
378        self.s.view_read.borrow_mut()
379    }
380
381    /// Returns the `DrawingArea` gtk widget the graph is drawn on, so
382    /// you can connect to its signals.
383    pub fn drawing_area(&self) -> gtk::DrawingArea {
384        self.s.drawing_area.clone()
385    }
386
387    /// Maps a position on `drawing_area` to the data point that is
388    /// currently drawn there. Useful for handling clicks on the graph.
389    ///
390    /// Returns None if no appropriate point can be found, for example
391    /// if the data point for a scroll position has already been
392    /// discarded.
393    pub fn drawing_area_pos_to_point(&self, x: f64, _y: f64) -> Option<Point> {
394        let view = self.s.view_read.borrow().get();
395        let t = (view.last_drawn_t as i64 +
396                 ((x - (view.last_drawn_x as f64)) * view.zoom_x) as i64)
397            .max(0).min(view.last_drawn_t as i64)
398            as u32;
399        let pt = self.s.store.borrow().query_point(t).unwrap()?;
400
401        // If we are getting a point >= 10 pixels away, return None instead.
402        // This can happen when old data has been discarded but is still on screen.
403        let pt: Option<Point> = if (pt.t - t) >= (view.zoom_x * 10.0) as u32 {
404            None
405        } else {
406            Some(pt)
407        };
408
409        pt
410    }
411}
412
413/// Handle the graph's draw signal.
414fn graph_draw(_ctrl: &gtk::DrawingArea, ctx: &cairo::Context, s: &State) -> Inhibit {
415    trace!("graph_draw");
416
417    // Copy from the backing_surface, which was updated elsewhere
418    ctx.rectangle(0.0, 0.0, s.config.graph_width as f64, s.config.graph_height as f64);
419    ctx.set_source_surface(&s.backing_surface.borrow(),
420                           0.0 /* offset x */, 0.0 /* offset y */);
421    ctx.fill();
422
423    // Calculate FPS, log it once a second.
424    s.fps_count.set(s.fps_count.get() + 1);
425    let now = Instant::now();
426    if (now - s.fps_timer.get()).as_secs() >= 1 {
427        debug!("fps: {}", s.fps_count.get());
428        s.fps_count.set(0);
429        s.fps_timer.set(now);
430    }
431
432    Inhibit(false)
433}
434
435/// Redraw the whole graph to the backing store
436fn redraw_graph(s: &State) {
437    trace!("redraw_graph");
438    let backing_surface = s.backing_surface.borrow();
439    {
440        // Clear backing_surface
441        let c = cairo::Context::new(&*backing_surface);
442        c.set_source_rgb(BACKGROUND_COLOR.0,
443                         BACKGROUND_COLOR.1,
444                         BACKGROUND_COLOR.2);
445        c.rectangle(0.0, 0.0, s.config.graph_width as f64, s.config.graph_height as f64);
446        c.fill();
447    }
448
449    let mut view = s.view_read.borrow().get();
450    let cols = s.config.data_source.borrow().get_colors().unwrap();
451    let t1: u32 = view.last_drawn_t;
452    let t0: u32 = (t1 as i64 - (s.config.graph_width as f64 * view.zoom_x) as i64).max(0) as u32;
453    let patch_dims = ((((t1-t0) as f64 / view.zoom_x).floor() as u32)
454                          .min(s.config.graph_width) as usize,
455                      s.config.graph_height as usize);
456    if patch_dims.0 > 0 {
457        let x = match view.mode {
458            ViewMode::Following => (s.config.graph_width as usize) - patch_dims.0,
459            ViewMode::Scrolled => 0,
460        };
461        render_patch(&*backing_surface,
462                     &s.store.borrow(),
463                     &cols,
464                     patch_dims.0 /* w */, patch_dims.1 /* h */,
465                     x /* x */, 0 /* y */,
466                     t0, t1,
467                     0 /* v0 */, std::u16::MAX /* v1 */,
468                     s.config.point_style);
469        view.last_drawn_x = (x + patch_dims.0) as u32;
470        view.last_drawn_t = t1;
471        s.view_write.borrow_mut().set(&view);
472    }
473    s.drawing_area.queue_draw();
474}
475
476fn tick(s: &State) {
477    trace!("tick");
478    // Ingest new data
479    let new_data = s.config.data_source.borrow_mut().get_data().unwrap();
480
481
482    if new_data.len() > 0 {
483        s.store.borrow_mut().ingest(&*new_data).unwrap();
484        let t_latest = s.store.borrow().last_t();
485
486        // Discard old data if there is any
487        let window_base_dt = (s.config.graph_width as f64 * s.config.base_zoom_x) as u32;
488        let keep_window = s.config.windows_to_store * window_base_dt;
489        let discard_start = if t_latest >= keep_window { t_latest - keep_window } else { 0 };
490        if discard_start > 0 {
491            s.store.borrow_mut().discard(0, discard_start).unwrap();
492        }
493
494        let mut view = s.view_read.borrow().get();
495
496        view.min_t = s.store.borrow().first_t();
497        view.max_t = t_latest;
498        s.view_write.borrow_mut().set(&view);
499
500        if view.mode == ViewMode::Following ||
501            (view.mode == ViewMode::Scrolled && view.last_drawn_x < s.config.graph_width) {
502
503            // Draw the new data.
504
505            // Calculate the size of the latest patch to render.
506            // TODO: Handle when patch_dims.0 >= s.config.graph_width.
507            // TODO: Handle scrolled when new data is offscreen (don't draw)
508            let patch_dims =
509                ((((t_latest - view.last_drawn_t) as f64 / view.zoom_x)
510                  .floor() as usize)
511                 .min(s.config.graph_width as usize),
512                 s.config.graph_height as usize);
513            // If there is more than a pixel's worth of data to render since we last drew,
514            // then draw it.
515            if patch_dims.0 > 0 {
516                let new_t = view.last_drawn_t + (patch_dims.0 as f64 * view.zoom_x) as u32;
517
518                let patch_offset_x = match view.mode {
519                    ViewMode::Following => s.config.graph_width - (patch_dims.0 as u32),
520                    ViewMode::Scrolled => view.last_drawn_x,
521                };
522
523                if view.mode == ViewMode::Following {
524                    // Copy existing graph to the temp surface, offsetting it to the left.
525                    let c = cairo::Context::new(&*s.temp_surface.borrow());
526                    c.set_source_surface(&*s.backing_surface.borrow(),
527                                         -(patch_dims.0 as f64) /* x offset*/, 0.0 /* y offset */);
528                    c.rectangle(0.0, // x offset
529                                0.0, // y offset
530                                patch_offset_x as f64, // width
531                                s.config.graph_height as f64); // height
532                    c.fill();
533
534                    // Present new graph by swapping the surfaces.
535                    s.backing_surface.swap(&s.temp_surface);
536                }
537
538                let cols = s.config.data_source.borrow().get_colors().unwrap();
539                render_patch(&s.backing_surface.borrow(),
540                             &s.store.borrow(),
541                             &cols,
542                             patch_dims.0 /* w */, patch_dims.1 /* h */,
543                             patch_offset_x as usize, 0 /* y */,
544                             view.last_drawn_t, new_t,
545                             0 /* v0 */, std::u16::MAX /* v1 */,
546                             s.config.point_style);
547
548                view.last_drawn_t = new_t;
549                view.last_drawn_x = (patch_offset_x + patch_dims.0 as u32)
550                                    .min(s.config.graph_width);
551                s.view_write.borrow_mut().set(&view);
552            }
553
554            // Invalidate the graph widget so we get a draw request.
555            s.drawing_area.queue_draw();
556        }
557    }
558}
559
560fn render_patch(
561    surface: &cairo::Surface,
562    store: &Store, cols: &[Color],
563    pw: usize, ph: usize,
564    x: usize, y: usize,
565    t0: Time, t1: Time, v0: Value, v1: Value,
566    point_style: PointStyle,
567) {
568    trace!("render_patch: pw={}, ph={} x={} y={}", pw, ph, x, y);
569    let mut patch_bytes = vec![0u8; pw * ph * BYTES_PER_PIXEL];
570    render_patch_to_bytes(store, cols, &mut patch_bytes,
571                          pw, ph,
572                          t0, t1,
573                          v0, v1,
574                          point_func_select(point_style)
575                          ).unwrap();
576    copy_patch(surface, patch_bytes,
577               pw, ph,
578               x, y);
579}
580
581fn point_func_select(s: PointStyle) -> &'static dyn Fn(usize, usize, usize, usize, &mut [u8], Color) {
582    match s {
583        PointStyle::Point => &point_func_point,
584        PointStyle::Cross => &point_func_cross,
585    }
586}
587
588fn point_func_point(x: usize, y: usize, pbw: usize, pbh: usize, pb: &mut [u8], col: Color) {
589    if x < pbw && y < pbh {
590        let i = BYTES_PER_PIXEL * (pbw * y + x);
591        pb[i+2] = col.0; // R
592        pb[i+1] = col.1; // G
593        pb[i+0] = col.2; // B
594        pb[i+3] = 255;   // A
595    }
596}
597
598fn point_func_cross(x: usize, y: usize, pbw: usize, pbh: usize, pb: &mut [u8], col: Color) {
599    let mut pixel = |px: usize, py: usize| {
600        if px < pbw && py < pbh {
601            let i = BYTES_PER_PIXEL * (pbw * py + px);
602            pb[i+2] = col.0; // R
603            pb[i+1] = col.1; // G
604            pb[i+0] = col.2; // B
605            pb[i+3] = 255;   // A
606        }
607    };
608
609    pixel(x+1, y+1);
610    if y >= 1 {
611        pixel(x+1, y-1);
612    }
613    pixel(x  , y  );
614    if x >= 1 {
615        if y >= 1 {
616            pixel(x-1, y-1);
617        }
618        pixel(x-1, y+1);
619    }
620}
621
622fn render_patch_to_bytes(
623    store: &Store, cols: &[Color],
624    pb: &mut [u8], pbw: usize, pbh: usize,
625    t0: Time, t1: Time, v0: Value, v1: Value,
626    point_func: &dyn Fn(usize, usize, usize, usize, &mut [u8], Color),
627) -> Result<()>
628{
629    trace!("render_patch_to_bytes: pbw={}", pbw);
630    assert!(pbw >= 1);
631
632    let points = store.query_range(t0, t1)?;
633    for p in points {
634        assert!(p.t >= t0 && p.t <= t1);
635
636        let x = (((p.t-t0) as f32 / (t1-t0) as f32) * pbw as f32) as usize;
637        if !(x < pbw) {
638            // Should be guaranteed by store.query.
639            panic!("x < pbw: x={} pbw={}", x, pbw);
640        }
641
642        for ch in 0..store.val_len() {
643            let col = cols[ch as usize % cols.len()];
644            let y = (((p.vals()[ch as usize]-v0) as f32 / (v1-v0) as f32) * pbh as f32) as usize;
645            if y >= pbh {
646                // Skip points that are outside our render patch.
647                continue;
648            }
649            // Mirror the y-axis
650            let y = pbh - y;
651
652            point_func(x, y, pbw, pbh, pb, col);
653        }
654    }
655
656    Ok(())
657}
658
659fn copy_patch(
660    backing_surface: &cairo::Surface,
661    bytes: Vec<u8>,
662    w: usize, h: usize,
663    x: usize, y: usize
664) {
665
666    trace!("copy_patch w={} x={}", w, x);
667
668    // Create an ImageSurface from our bytes
669    let patch_surface = cairo::ImageSurface::create_for_data(
670        bytes,
671        cairo::Format::ARgb32,
672        w as i32,
673        h as i32,
674        (w * BYTES_PER_PIXEL) as i32 /* stride */
675    ).unwrap();
676
677    // Copy from the ImageSurface to backing_surface
678    let c = cairo::Context::new(&backing_surface);
679    // Fill target area with background colour.
680    c.rectangle(x as f64,
681                y as f64,
682                w as f64, // width
683                h as f64  /* height */);
684    c.set_source_rgb(DRAWN_AREA_BACKGROUND_COLOR.0,
685                     DRAWN_AREA_BACKGROUND_COLOR.1,
686                     DRAWN_AREA_BACKGROUND_COLOR.2);
687    c.fill_preserve();
688    // Fill target area with patch data.
689    c.set_source_surface(&patch_surface,
690                         x as f64,
691                         y as f64);
692    c.fill();
693}
694
695fn create_backing_surface(win: &gdk::Window, w: u32, h: u32) -> cairo::Surface {
696    let surface =
697        win.create_similar_image_surface(
698            cairo::Format::Rgb24.into(),
699            w as i32 /* width */,
700            h as i32 /* height */,
701            1 /* scale */).unwrap();
702    {
703        // Clear backing_surface
704        let c = cairo::Context::new(&surface);
705        c.set_source_rgb(BACKGROUND_COLOR.0,
706                         BACKGROUND_COLOR.1,
707                         BACKGROUND_COLOR.2);
708        c.rectangle(0.0, 0.0, w as f64, h as f64);
709        c.fill();
710    }
711    surface
712}