Skip to main content

gpui_liveplot/gpui_backend/
view.rs

1use std::sync::{Arc, RwLock};
2use std::time::{Duration, Instant};
3
4use gpui::prelude::*;
5use gpui::{
6    MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollWheelEvent,
7    Window, canvas, div, px,
8};
9
10use crate::geom::{Point as DataPoint, ScreenPoint, ScreenRect};
11use crate::interaction::{
12    HitRegion, pan_viewport, toggle_pin, zoom_factor_from_drag, zoom_to_rect, zoom_viewport,
13};
14use crate::plot::Plot;
15use crate::transform::Transform;
16use crate::view::{Range, Viewport};
17
18use super::config::PlotViewConfig;
19use super::constants::DOUBLE_CLICK_PIN_GRACE_MS;
20use super::frame::build_frame;
21use super::geometry::{distance_sq, normalized_rect};
22use super::hover::{compute_hover_target, hover_target_within_threshold};
23use super::link::{LinkBinding, PlotLinkGroup, PlotLinkOptions, ViewSyncKind};
24use super::paint::{paint_frame, to_hsla};
25use super::state::{ClickState, DragMode, DragState, PinToggle, PlotUiState};
26
27/// A GPUI view that renders a [`Plot`] with interactive controls.
28///
29/// This view handles pan/zoom/box-zoom, hover readouts, and pin interactions
30/// while delegating data management to the underlying [`Plot`].
31#[derive(Clone)]
32pub struct GpuiPlotView {
33    plot: Arc<RwLock<Plot>>,
34    state: Arc<RwLock<PlotUiState>>,
35    config: PlotViewConfig,
36    link: Option<LinkBinding>,
37}
38
39impl GpuiPlotView {
40    /// Create a new GPUI plot view for the given plot.
41    ///
42    /// Uses the default [`PlotViewConfig`].
43    pub fn new(plot: Plot) -> Self {
44        Self {
45            plot: Arc::new(RwLock::new(plot)),
46            state: Arc::new(RwLock::new(PlotUiState::default())),
47            config: PlotViewConfig::default(),
48            link: None,
49        }
50    }
51
52    /// Create a new GPUI plot view with a custom configuration.
53    pub fn with_config(plot: Plot, config: PlotViewConfig) -> Self {
54        Self {
55            plot: Arc::new(RwLock::new(plot)),
56            state: Arc::new(RwLock::new(PlotUiState::default())),
57            config,
58            link: None,
59        }
60    }
61
62    /// Attach this view to a multi-plot link group.
63    ///
64    /// Link groups synchronize viewport/cursor/brush state between views.
65    pub fn with_link_group(mut self, group: PlotLinkGroup, options: PlotLinkOptions) -> Self {
66        self.link = Some(LinkBinding {
67            member_id: group.register_member(),
68            group,
69            options,
70        });
71        self
72    }
73
74    /// Get a handle for mutating the underlying plot.
75    ///
76    /// This is useful for streaming updates from async tasks.
77    pub fn plot_handle(&self) -> PlotHandle {
78        PlotHandle {
79            plot: Arc::clone(&self.plot),
80        }
81    }
82
83    fn publish_manual_view_link(&self, viewport: Viewport) {
84        let Some(link) = self.link.as_ref() else {
85            return;
86        };
87        link.group.publish_manual_view(
88            link.member_id,
89            viewport,
90            link.options.link_x,
91            link.options.link_y,
92        );
93    }
94
95    fn publish_reset_link(&self) {
96        let Some(link) = self.link.as_ref() else {
97            return;
98        };
99        if link.options.link_reset {
100            link.group.publish_reset(link.member_id);
101        }
102    }
103
104    fn publish_cursor_link(&self, x: Option<f64>) {
105        let Some(link) = self.link.as_ref() else {
106            return;
107        };
108        if link.options.link_cursor {
109            link.group.publish_cursor_x(link.member_id, x);
110        }
111    }
112
113    fn publish_brush_link(&self, x_range: Option<Range>) {
114        let Some(link) = self.link.as_ref() else {
115            return;
116        };
117        if link.options.link_brush {
118            link.group.publish_brush_x(link.member_id, x_range);
119        }
120    }
121
122    fn apply_manual_view_with_link(
123        &self,
124        plot: &mut Plot,
125        state: &mut PlotUiState,
126        rect: ScreenRect,
127        viewport: Viewport,
128    ) {
129        apply_manual_view(plot, state, rect, viewport);
130        state.linked_brush_x = None;
131        self.publish_manual_view_link(viewport);
132        self.publish_brush_link(None);
133    }
134
135    fn on_mouse_down(&mut self, ev: &MouseDownEvent, cx: &mut Context<Self>) {
136        let pos = screen_point(ev.position);
137        let mut state = self.state.write().expect("plot state lock");
138        state.last_cursor = Some(pos);
139
140        if let Some(series_id) = state.legend_hit(pos) {
141            if ev.button == MouseButton::Left && ev.click_count == 1 {
142                if let Ok(mut plot) = self.plot.write() {
143                    if let Some(series) = plot
144                        .series_mut()
145                        .iter_mut()
146                        .find(|series| series.id() == series_id)
147                    {
148                        series.set_visible(!series.is_visible());
149                    }
150                }
151            }
152            state.clear_interaction();
153            state.hover = None;
154            state.hover_target = None;
155            cx.notify();
156            return;
157        }
158
159        let region = state.regions.hit_test(pos);
160        if ev.button == MouseButton::Left && ev.click_count >= 2 && region == HitRegion::Plot {
161            let last_toggle = state.last_pin_toggle.take();
162            if let Ok(mut plot) = self.plot.write() {
163                if let Some(last_toggle) = last_toggle {
164                    if last_toggle.at.elapsed() <= Duration::from_millis(DOUBLE_CLICK_PIN_GRACE_MS)
165                        && distance_sq(last_toggle.screen_pos, pos)
166                            <= self.config.pin_threshold_px.powi(2)
167                    {
168                        revert_pin_toggle(&mut plot, last_toggle);
169                    }
170                }
171                plot.reset_view();
172                state.linked_brush_x = None;
173                self.publish_reset_link();
174                self.publish_brush_link(None);
175            }
176            state.clear_interaction();
177            cx.notify();
178            return;
179        }
180
181        state.pending_click = Some(ClickState {
182            region,
183            button: ev.button,
184        });
185
186        match (ev.button, region) {
187            (MouseButton::Left, HitRegion::XAxis) => {
188                state.drag = Some(DragState::new(DragMode::ZoomX, pos, true));
189            }
190            (MouseButton::Left, HitRegion::YAxis) => {
191                state.drag = Some(DragState::new(DragMode::ZoomY, pos, true));
192            }
193            (MouseButton::Left, HitRegion::Plot) => {
194                state.drag = Some(DragState::new(DragMode::Pan, pos, false));
195            }
196            (MouseButton::Right, HitRegion::Plot) => {
197                state.drag = Some(DragState::new(DragMode::ZoomRect, pos, true));
198                state.selection_rect = Some(ScreenRect::new(pos, pos));
199            }
200            _ => {}
201        }
202
203        cx.notify();
204    }
205
206    fn on_mouse_move(&mut self, ev: &MouseMoveEvent, cx: &mut Context<Self>) {
207        let pos = screen_point(ev.position);
208        let mut state = self.state.write().expect("plot state lock");
209        state.last_cursor = Some(pos);
210
211        if state.legend_hit(pos).is_some() {
212            state.hover = None;
213        } else if state.regions.hit_test(pos) == HitRegion::Plot {
214            state.hover = Some(pos);
215        } else {
216            state.hover = None;
217        }
218        let linked_cursor_x = state.hover.and_then(|_| {
219            state
220                .transform
221                .as_ref()
222                .and_then(|transform| transform.screen_to_data(pos))
223                .map(|point| point.x)
224        });
225        self.publish_cursor_link(linked_cursor_x);
226
227        let Some(mut drag) = state.drag.clone() else {
228            cx.notify();
229            return;
230        };
231
232        let moved_sq = distance_sq(drag.start, pos);
233        if !drag.active && moved_sq > self.config.drag_threshold_px.powi(2) {
234            drag.active = true;
235        }
236
237        if !drag.active {
238            state.drag = Some(drag);
239            cx.notify();
240            return;
241        }
242
243        let delta = ScreenPoint::new(pos.x - drag.last.x, pos.y - drag.last.y);
244        let plot_rect = state.plot_rect;
245        let transform = state.transform.clone();
246
247        match drag.mode {
248            DragMode::Pan => {
249                if let (Some(rect), Some(transform)) = (plot_rect, transform) {
250                    if let Ok(mut plot) = self.plot.write() {
251                        if let Some(viewport) = plot.viewport() {
252                            if let Some(next) = pan_viewport(viewport, delta, &transform) {
253                                self.apply_manual_view_with_link(&mut plot, &mut state, rect, next);
254                            }
255                        }
256                    }
257                }
258            }
259            DragMode::ZoomRect => {
260                state.selection_rect = Some(ScreenRect::new(drag.start, pos));
261            }
262            DragMode::ZoomX => {
263                if let (Some(rect), Some(transform)) = (plot_rect, transform) {
264                    let axis_pixels = rect.width().max(1.0);
265                    let factor = zoom_factor_from_drag(delta.x, axis_pixels);
266                    if let Ok(mut plot) = self.plot.write() {
267                        if let Some(viewport) = plot.viewport() {
268                            let center = transform
269                                .screen_to_data(pos)
270                                .unwrap_or_else(|| viewport.x_center());
271                            let next = zoom_viewport(viewport, center, factor, 1.0);
272                            self.apply_manual_view_with_link(&mut plot, &mut state, rect, next);
273                        }
274                    }
275                }
276            }
277            DragMode::ZoomY => {
278                if let (Some(rect), Some(transform)) = (plot_rect, transform) {
279                    let axis_pixels = rect.height().max(1.0);
280                    let factor = zoom_factor_from_drag(-delta.y, axis_pixels);
281                    if let Ok(mut plot) = self.plot.write() {
282                        if let Some(viewport) = plot.viewport() {
283                            let center = transform
284                                .screen_to_data(pos)
285                                .unwrap_or_else(|| viewport.y_center());
286                            let next = zoom_viewport(viewport, center, 1.0, factor);
287                            self.apply_manual_view_with_link(&mut plot, &mut state, rect, next);
288                        }
289                    }
290                }
291            }
292        }
293
294        drag.last = pos;
295        state.drag = Some(drag);
296        state.pending_click = None;
297        cx.notify();
298    }
299
300    fn on_mouse_up(&mut self, ev: &MouseUpEvent, cx: &mut Context<Self>) {
301        let pos = screen_point(ev.position);
302        let mut state = self.state.write().expect("plot state lock");
303        let drag = state.drag.clone();
304
305        if let Some(drag_state) = drag.as_ref() {
306            if drag_state.active && drag_state.mode == DragMode::ZoomRect {
307                if let (Some(rect), Some(transform)) =
308                    (state.selection_rect.take(), state.transform.clone())
309                {
310                    let rect = normalized_rect(rect);
311                    if let Ok(mut plot) = self.plot.write() {
312                        if let Some(viewport) = plot.viewport() {
313                            if let Some(next) = zoom_to_rect(viewport, rect, &transform) {
314                                self.apply_manual_view_with_link(
315                                    &mut plot,
316                                    &mut state,
317                                    transform.screen(),
318                                    next,
319                                );
320                                self.publish_brush_link(Some(next.x));
321                            }
322                        }
323                    }
324                }
325            }
326        }
327
328        let click = state.pending_click.take();
329        let should_toggle = click.as_ref().is_some_and(|click| {
330            click.button == MouseButton::Left && click.region == HitRegion::Plot
331        }) && drag.as_ref().is_none_or(|drag| !drag.active)
332            && ev.click_count == 1;
333
334        if should_toggle {
335            if let Some(transform) = state.transform.clone() {
336                if let Ok(mut plot) = self.plot.write() {
337                    let target = state
338                        .hover_target
339                        .filter(|target| hover_target_within_threshold(target, pos, &self.config))
340                        .or_else(|| {
341                            compute_hover_target(
342                                &plot,
343                                &transform,
344                                pos,
345                                state.plot_rect,
346                                self.config.pin_threshold_px,
347                                self.config.unpin_threshold_px,
348                            )
349                        });
350
351                    if let Some(target) = target {
352                        let added = toggle_pin(plot.pins_mut(), target.pin);
353                        let now = Instant::now();
354                        state.last_pin_toggle = Some(PinToggle {
355                            pin: target.pin,
356                            added,
357                            at: now,
358                            screen_pos: target.screen,
359                        });
360                    }
361                }
362            }
363        } else if ev.click_count > 1 {
364            state.last_pin_toggle = None;
365        }
366
367        state.drag = None;
368        state.selection_rect = None;
369        self.publish_cursor_link(None);
370        cx.notify();
371    }
372
373    fn on_scroll(&mut self, ev: &ScrollWheelEvent, _window: &Window, cx: &mut Context<Self>) {
374        let pos = screen_point(ev.position);
375        let mut state = self.state.write().expect("plot state lock");
376        if state.legend_hit(pos).is_some() {
377            return;
378        }
379        let region = state.regions.hit_test(pos);
380        let Some(transform) = state.transform.clone() else {
381            return;
382        };
383
384        let line_height = px(16.0);
385        let delta = ev.delta.pixel_delta(line_height);
386        let zoom_delta = -f32::from(delta.y);
387        if zoom_delta.abs() < 0.01 {
388            return;
389        }
390        let factor = (1.0 - (zoom_delta as f64 * 0.002)).clamp(0.1, 10.0);
391
392        if let Ok(mut plot) = self.plot.write() {
393            if let Some(viewport) = plot.viewport() {
394                let center = transform
395                    .screen_to_data(pos)
396                    .unwrap_or_else(|| viewport.center());
397                let (factor_x, factor_y) = match region {
398                    HitRegion::XAxis => (factor, 1.0),
399                    HitRegion::YAxis => (1.0, factor),
400                    HitRegion::Plot => (factor, factor),
401                    HitRegion::Outside => (1.0, 1.0),
402                };
403                if factor_x != 1.0 || factor_y != 1.0 {
404                    let next = zoom_viewport(viewport, center, factor_x, factor_y);
405                    if let Some(rect) = state.plot_rect {
406                        self.apply_manual_view_with_link(&mut plot, &mut state, rect, next);
407                    }
408                }
409            }
410        }
411
412        cx.notify();
413    }
414}
415
416impl Render for GpuiPlotView {
417    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
418        let plot = Arc::clone(&self.plot);
419        let state = Arc::clone(&self.state);
420        let config = self.config.clone();
421        let link = self.link.clone();
422        let theme = plot.read().expect("plot lock").theme().clone();
423
424        div()
425            .size_full()
426            .bg(to_hsla(theme.background))
427            .child(
428                canvas(
429                    move |bounds, window, _| {
430                        let mut plot = plot.write().expect("plot lock");
431                        let mut state = state.write().expect("plot state lock");
432                        if let Some(link) = &link {
433                            apply_link_updates(link, &mut plot, &mut state);
434                        }
435                        build_frame(&mut plot, &mut state, &config, bounds, window)
436                    },
437                    move |_, frame, window, cx| {
438                        paint_frame(&frame, window, cx);
439                    },
440                )
441                .size_full(),
442            )
443            .on_mouse_down(
444                MouseButton::Left,
445                cx.listener(|this, ev, _, cx| {
446                    this.on_mouse_down(ev, cx);
447                }),
448            )
449            .on_mouse_down(
450                MouseButton::Right,
451                cx.listener(|this, ev, _, cx| {
452                    this.on_mouse_down(ev, cx);
453                }),
454            )
455            .on_mouse_move(cx.listener(|this, ev, _, cx| {
456                this.on_mouse_move(ev, cx);
457            }))
458            .on_mouse_up(
459                MouseButton::Left,
460                cx.listener(|this, ev, _, cx| {
461                    this.on_mouse_up(ev, cx);
462                }),
463            )
464            .on_mouse_up(
465                MouseButton::Right,
466                cx.listener(|this, ev, _, cx| {
467                    this.on_mouse_up(ev, cx);
468                }),
469            )
470            .on_scroll_wheel(cx.listener(|this, ev, window, cx| {
471                this.on_scroll(ev, window, cx);
472            }))
473    }
474}
475
476/// A handle for mutating a [`Plot`] held inside a `GpuiPlotView`.
477///
478/// The handle clones cheaply and can be moved into async tasks.
479#[derive(Clone)]
480pub struct PlotHandle {
481    plot: Arc<RwLock<Plot>>,
482}
483
484impl PlotHandle {
485    /// Read the plot state.
486    ///
487    /// The plot is locked for the duration of the callback.
488    pub fn read<R>(&self, f: impl FnOnce(&Plot) -> R) -> R {
489        let plot = self.plot.read().expect("plot lock");
490        f(&plot)
491    }
492
493    /// Mutate the plot state.
494    ///
495    /// The plot is locked for the duration of the callback.
496    pub fn write<R>(&self, f: impl FnOnce(&mut Plot) -> R) -> R {
497        let mut plot = self.plot.write().expect("plot lock");
498        f(&mut plot)
499    }
500}
501
502fn apply_link_updates(link: &LinkBinding, plot: &mut Plot, state: &mut PlotUiState) {
503    if let Some(update) = link.group.latest_view_update()
504        && update.seq > state.link_view_seq
505    {
506        state.link_view_seq = update.seq;
507        if update.source != link.member_id {
508            match update.kind {
509                ViewSyncKind::Reset => {
510                    if link.options.link_reset {
511                        plot.reset_view();
512                        state.viewport = None;
513                        state.transform = None;
514                        state.linked_brush_x = None;
515                    }
516                }
517                ViewSyncKind::Manual {
518                    viewport,
519                    sync_x,
520                    sync_y,
521                } => {
522                    let mut next = plot
523                        .viewport()
524                        .or_else(|| plot.data_bounds())
525                        .unwrap_or(viewport);
526                    let mut changed = false;
527                    if sync_x && link.options.link_x {
528                        next.x = viewport.x;
529                        changed = true;
530                    }
531                    if sync_y && link.options.link_y {
532                        next.y = viewport.y;
533                        changed = true;
534                    }
535                    if changed {
536                        plot.set_manual_view(next);
537                        state.viewport = Some(next);
538                        if let Some(rect) = state.plot_rect {
539                            state.transform = Transform::new(next, rect);
540                        }
541                    }
542                }
543            }
544        }
545    }
546
547    if let Some(update) = link.group.latest_cursor_update()
548        && update.seq > state.link_cursor_seq
549    {
550        state.link_cursor_seq = update.seq;
551        if update.source != link.member_id && link.options.link_cursor {
552            state.linked_cursor_x = update.x;
553        }
554    }
555
556    if let Some(update) = link.group.latest_brush_update()
557        && update.seq > state.link_brush_seq
558    {
559        state.link_brush_seq = update.seq;
560        if update.source != link.member_id && link.options.link_brush {
561            state.linked_brush_x = update.x_range;
562            if let Some(x_range) = update.x_range {
563                let y_range = plot
564                    .viewport()
565                    .or_else(|| plot.data_bounds())
566                    .map(|viewport| viewport.y)
567                    .unwrap_or_else(|| Range::new(0.0, 1.0));
568                let next = Viewport::new(x_range, y_range);
569                plot.set_manual_view(next);
570                state.viewport = Some(next);
571                if let Some(rect) = state.plot_rect {
572                    state.transform = Transform::new(next, rect);
573                }
574            }
575        }
576    }
577}
578
579fn screen_point(point: Point<Pixels>) -> ScreenPoint {
580    ScreenPoint::new(f32::from(point.x), f32::from(point.y))
581}
582
583fn apply_manual_view(
584    plot: &mut Plot,
585    state: &mut PlotUiState,
586    rect: ScreenRect,
587    viewport: Viewport,
588) {
589    plot.set_manual_view(viewport);
590    state.viewport = Some(viewport);
591    state.transform = Transform::new(viewport, rect);
592}
593
594fn revert_pin_toggle(plot: &mut Plot, toggle: PinToggle) {
595    let pins = plot.pins_mut();
596    if toggle.added {
597        if let Some(index) = pins.iter().position(|pin| *pin == toggle.pin) {
598            pins.swap_remove(index);
599        }
600    } else if !pins.contains(&toggle.pin) {
601        pins.push(toggle.pin);
602    }
603}
604
605trait ViewportCenter {
606    fn center(&self) -> DataPoint;
607    fn x_center(&self) -> DataPoint;
608    fn y_center(&self) -> DataPoint;
609}
610
611impl ViewportCenter for Viewport {
612    fn center(&self) -> DataPoint {
613        DataPoint::new(
614            (self.x.min + self.x.max) * 0.5,
615            (self.y.min + self.y.max) * 0.5,
616        )
617    }
618
619    fn x_center(&self) -> DataPoint {
620        DataPoint::new(
621            (self.x.min + self.x.max) * 0.5,
622            (self.y.min + self.y.max) * 0.5,
623        )
624    }
625
626    fn y_center(&self) -> DataPoint {
627        DataPoint::new(
628            (self.x.min + self.x.max) * 0.5,
629            (self.y.min + self.y.max) * 0.5,
630        )
631    }
632}