Skip to main content

gpui_liveplot/gpui_backend/
link.rs

1use std::sync::{Arc, RwLock};
2
3use crate::view::{Range, Viewport};
4
5const LINK_EPSILON: f64 = 1e-9;
6
7/// Member identifier inside a plot link group.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub struct LinkMemberId(u64);
10
11/// Link behavior switches for multi-plot synchronization.
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct PlotLinkOptions {
14    /// Synchronize X-axis range updates.
15    pub link_x: bool,
16    /// Synchronize Y-axis range updates.
17    pub link_y: bool,
18    /// Synchronize cursor X position (crosshair).
19    pub link_cursor: bool,
20    /// Synchronize brush X range selections.
21    pub link_brush: bool,
22    /// Synchronize reset-view actions (double click reset).
23    pub link_reset: bool,
24}
25
26impl Default for PlotLinkOptions {
27    fn default() -> Self {
28        Self {
29            link_x: true,
30            link_y: false,
31            link_cursor: false,
32            link_brush: false,
33            link_reset: true,
34        }
35    }
36}
37
38/// Shared link group used to synchronize multiple `GpuiPlotView` instances.
39#[derive(Debug, Clone, Default)]
40pub struct PlotLinkGroup {
41    inner: Arc<RwLock<LinkGroupState>>,
42}
43
44impl PlotLinkGroup {
45    /// Create an empty link group.
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    pub(crate) fn register_member(&self) -> LinkMemberId {
51        let mut state = self.inner.write().expect("link group lock");
52        state.next_member_id = state.next_member_id.wrapping_add(1);
53        LinkMemberId(state.next_member_id)
54    }
55
56    pub(crate) fn publish_manual_view(
57        &self,
58        source: LinkMemberId,
59        viewport: Viewport,
60        sync_x: bool,
61        sync_y: bool,
62    ) {
63        if !sync_x && !sync_y {
64            return;
65        }
66        let mut state = self.inner.write().expect("link group lock");
67        if let Some(current) = state.view_update
68            && let ViewSyncKind::Manual {
69                viewport: current_viewport,
70                sync_x: current_sync_x,
71                sync_y: current_sync_y,
72            } = current.kind
73            && current.source == source
74            && current_sync_x == sync_x
75            && current_sync_y == sync_y
76            && viewport_approx_eq(current_viewport, viewport)
77        {
78            return;
79        }
80        let seq = state.next_seq();
81        state.view_update = Some(ViewLinkUpdate {
82            seq,
83            source,
84            kind: ViewSyncKind::Manual {
85                viewport,
86                sync_x,
87                sync_y,
88            },
89        });
90    }
91
92    pub(crate) fn publish_reset(&self, source: LinkMemberId) {
93        let mut state = self.inner.write().expect("link group lock");
94        if let Some(current) = state.view_update
95            && matches!(current.kind, ViewSyncKind::Reset)
96            && current.source == source
97        {
98            return;
99        }
100        let seq = state.next_seq();
101        state.view_update = Some(ViewLinkUpdate {
102            seq,
103            source,
104            kind: ViewSyncKind::Reset,
105        });
106    }
107
108    pub(crate) fn publish_cursor_x(&self, source: LinkMemberId, x: Option<f64>) {
109        let mut state = self.inner.write().expect("link group lock");
110        if let Some(current) = state.cursor_update
111            && current.source == source
112            && option_f64_approx_eq(current.x, x)
113        {
114            return;
115        }
116        let seq = state.next_seq();
117        state.cursor_update = Some(CursorLinkUpdate { seq, source, x });
118    }
119
120    pub(crate) fn publish_brush_x(&self, source: LinkMemberId, x_range: Option<Range>) {
121        let mut state = self.inner.write().expect("link group lock");
122        if let Some(current) = state.brush_update
123            && current.source == source
124            && option_range_approx_eq(current.x_range, x_range)
125        {
126            return;
127        }
128        let seq = state.next_seq();
129        state.brush_update = Some(BrushLinkUpdate {
130            seq,
131            source,
132            x_range,
133        });
134    }
135
136    pub(crate) fn latest_view_update(&self) -> Option<ViewLinkUpdate> {
137        self.inner.read().expect("link group lock").view_update
138    }
139
140    pub(crate) fn latest_cursor_update(&self) -> Option<CursorLinkUpdate> {
141        self.inner.read().expect("link group lock").cursor_update
142    }
143
144    pub(crate) fn latest_brush_update(&self) -> Option<BrushLinkUpdate> {
145        self.inner.read().expect("link group lock").brush_update
146    }
147}
148
149#[derive(Debug, Default)]
150struct LinkGroupState {
151    next_member_id: u64,
152    next_seq: u64,
153    view_update: Option<ViewLinkUpdate>,
154    cursor_update: Option<CursorLinkUpdate>,
155    brush_update: Option<BrushLinkUpdate>,
156}
157
158impl LinkGroupState {
159    fn next_seq(&mut self) -> u64 {
160        self.next_seq = self.next_seq.wrapping_add(1);
161        self.next_seq
162    }
163}
164
165#[derive(Debug, Clone)]
166pub(crate) struct LinkBinding {
167    pub(crate) group: PlotLinkGroup,
168    pub(crate) member_id: LinkMemberId,
169    pub(crate) options: PlotLinkOptions,
170}
171
172#[derive(Debug, Clone, Copy)]
173pub(crate) struct ViewLinkUpdate {
174    pub(crate) seq: u64,
175    pub(crate) source: LinkMemberId,
176    pub(crate) kind: ViewSyncKind,
177}
178
179#[derive(Debug, Clone, Copy)]
180pub(crate) enum ViewSyncKind {
181    Manual {
182        viewport: Viewport,
183        sync_x: bool,
184        sync_y: bool,
185    },
186    Reset,
187}
188
189#[derive(Debug, Clone, Copy)]
190pub(crate) struct CursorLinkUpdate {
191    pub(crate) seq: u64,
192    pub(crate) source: LinkMemberId,
193    pub(crate) x: Option<f64>,
194}
195
196#[derive(Debug, Clone, Copy)]
197pub(crate) struct BrushLinkUpdate {
198    pub(crate) seq: u64,
199    pub(crate) source: LinkMemberId,
200    pub(crate) x_range: Option<Range>,
201}
202
203fn approx_eq(a: f64, b: f64) -> bool {
204    (a - b).abs() <= LINK_EPSILON
205}
206
207fn option_f64_approx_eq(a: Option<f64>, b: Option<f64>) -> bool {
208    match (a, b) {
209        (Some(a), Some(b)) => approx_eq(a, b),
210        (None, None) => true,
211        _ => false,
212    }
213}
214
215fn range_approx_eq(a: Range, b: Range) -> bool {
216    approx_eq(a.min, b.min) && approx_eq(a.max, b.max)
217}
218
219fn option_range_approx_eq(a: Option<Range>, b: Option<Range>) -> bool {
220    match (a, b) {
221        (Some(a), Some(b)) => range_approx_eq(a, b),
222        (None, None) => true,
223        _ => false,
224    }
225}
226
227fn viewport_approx_eq(a: Viewport, b: Viewport) -> bool {
228    range_approx_eq(a.x, b.x) && range_approx_eq(a.y, b.y)
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn manual_view_publish_deduplicates_same_payload() {
237        let group = PlotLinkGroup::new();
238        let member = group.register_member();
239        let viewport = Viewport::new(Range::new(0.0, 10.0), Range::new(-1.0, 1.0));
240
241        group.publish_manual_view(member, viewport, true, false);
242        let first = group.latest_view_update().expect("view update");
243        group.publish_manual_view(member, viewport, true, false);
244        let second = group.latest_view_update().expect("view update");
245
246        assert_eq!(first.seq, second.seq);
247    }
248
249    #[test]
250    fn reset_publish_replaces_previous_view_event() {
251        let group = PlotLinkGroup::new();
252        let member = group.register_member();
253        let viewport = Viewport::new(Range::new(0.0, 5.0), Range::new(0.0, 1.0));
254        group.publish_manual_view(member, viewport, true, false);
255        let first = group.latest_view_update().expect("view update").seq;
256
257        group.publish_reset(member);
258        let update = group.latest_view_update().expect("view update");
259        assert!(update.seq > first);
260        assert!(matches!(update.kind, ViewSyncKind::Reset));
261    }
262}