Skip to main content

gpui_liveplot/render/
mod.rs

1//! Rendering primitives and clipping helpers.
2//!
3//! These types are backend-agnostic and are used by render backends (such as the
4//! GPUI backend) to describe how plots should be drawn.
5
6use crate::geom::{Point, ScreenPoint, ScreenRect};
7use crate::transform::Transform;
8use crate::view::Viewport;
9use gpui::Rgba;
10
11/// Line stroke styling.
12///
13/// The width is expressed in logical pixels.
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct LineStyle {
16    /// Stroke color.
17    pub color: Rgba,
18    /// Stroke width in pixels.
19    pub width: f32,
20}
21
22impl Default for LineStyle {
23    fn default() -> Self {
24        Self {
25            color: Rgba {
26                r: 0.0,
27                g: 0.0,
28                b: 0.0,
29                a: 1.0,
30            },
31            width: 1.0,
32        }
33    }
34}
35
36/// Marker shape for scatter plots.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum MarkerShape {
39    /// Circle marker.
40    Circle,
41    /// Square marker.
42    Square,
43    /// Cross marker.
44    Cross,
45}
46
47/// Marker styling for scatter plots.
48///
49/// Marker sizes are expressed in logical pixels.
50#[derive(Debug, Clone, Copy, PartialEq)]
51pub struct MarkerStyle {
52    /// Marker color.
53    pub color: Rgba,
54    /// Marker size in pixels.
55    pub size: f32,
56    /// Marker shape.
57    pub shape: MarkerShape,
58}
59
60impl Default for MarkerStyle {
61    fn default() -> Self {
62        Self {
63            color: Rgba {
64                r: 0.0,
65                g: 0.0,
66                b: 0.0,
67                a: 1.0,
68            },
69            size: 4.0,
70            shape: MarkerShape::Circle,
71        }
72    }
73}
74
75/// Rectangle styling.
76#[derive(Debug, Clone, Copy, PartialEq)]
77pub(crate) struct RectStyle {
78    /// Fill color.
79    pub fill: Rgba,
80    /// Stroke color.
81    pub stroke: Rgba,
82    /// Stroke width.
83    pub stroke_width: f32,
84}
85
86impl Default for RectStyle {
87    fn default() -> Self {
88        Self {
89            fill: Rgba {
90                r: 0.0,
91                g: 0.0,
92                b: 0.0,
93                a: 0.0,
94            },
95            stroke: Rgba {
96                r: 0.0,
97                g: 0.0,
98                b: 0.0,
99                a: 1.0,
100            },
101            stroke_width: 1.0,
102        }
103    }
104}
105
106/// Text styling.
107#[derive(Debug, Clone, PartialEq)]
108pub(crate) struct TextStyle {
109    /// Text color.
110    pub color: Rgba,
111    /// Font size in pixels.
112    pub size: f32,
113}
114
115impl Default for TextStyle {
116    fn default() -> Self {
117        Self {
118            color: Rgba {
119                r: 0.0,
120                g: 0.0,
121                b: 0.0,
122                a: 1.0,
123            },
124            size: 12.0,
125        }
126    }
127}
128
129/// A line segment in screen space.
130#[derive(Debug, Clone, Copy, PartialEq)]
131pub(crate) struct LineSegment {
132    /// Segment start.
133    pub start: ScreenPoint,
134    /// Segment end.
135    pub end: ScreenPoint,
136}
137
138impl LineSegment {
139    /// Create a new line segment.
140    pub(crate) fn new(start: ScreenPoint, end: ScreenPoint) -> Self {
141        Self { start, end }
142    }
143}
144
145/// Render command list.
146#[derive(Debug, Clone)]
147pub(crate) enum RenderCommand {
148    /// Start clipping to a rectangle.
149    ClipRect(ScreenRect),
150    /// End clipping.
151    ClipEnd,
152    /// Draw line segments.
153    LineSegments {
154        /// Segments to draw.
155        segments: Vec<LineSegment>,
156        /// Styling for the segments.
157        style: LineStyle,
158    },
159    /// Draw scatter points.
160    Points {
161        /// Points to draw.
162        points: Vec<ScreenPoint>,
163        /// Marker styling.
164        style: MarkerStyle,
165    },
166    /// Draw a rectangle.
167    Rect {
168        /// Rectangle bounds.
169        rect: ScreenRect,
170        /// Rectangle styling.
171        style: RectStyle,
172    },
173    /// Draw text.
174    Text {
175        /// Text position.
176        position: ScreenPoint,
177        /// Text content.
178        text: String,
179        /// Text styling.
180        style: TextStyle,
181    },
182}
183
184/// Aggregated render commands.
185#[derive(Debug, Default, Clone)]
186pub(crate) struct RenderList {
187    commands: Vec<RenderCommand>,
188}
189
190impl RenderList {
191    /// Create an empty render list.
192    pub(crate) fn new() -> Self {
193        Self::default()
194    }
195
196    /// Push a render command.
197    pub(crate) fn push(&mut self, command: RenderCommand) {
198        self.commands.push(command);
199    }
200
201    /// Access all render commands.
202    pub(crate) fn commands(&self) -> &[RenderCommand] {
203        &self.commands
204    }
205}
206
207/// Cache key for rendered series data.
208#[derive(Debug, Clone, PartialEq)]
209pub(crate) struct RenderCacheKey {
210    /// Viewport used for decimation.
211    pub viewport: Viewport,
212    /// Plot size in pixels.
213    pub size: (u32, u32),
214    /// Data generation for cache invalidation.
215    pub generation: u64,
216}
217
218/// Build clipped line segments from data points.
219pub(crate) fn build_line_segments(
220    points: &[Point],
221    transform: &Transform,
222    clip: ScreenRect,
223    out: &mut Vec<LineSegment>,
224) {
225    out.clear();
226    if points.len() < 2 {
227        return;
228    }
229    for window in points.windows(2) {
230        let Some(start) = transform.data_to_screen(window[0]) else {
231            continue;
232        };
233        let Some(end) = transform.data_to_screen(window[1]) else {
234            continue;
235        };
236        if let Some((clipped_start, clipped_end)) = clip_segment(start, end, clip) {
237            out.push(LineSegment::new(clipped_start, clipped_end));
238        }
239    }
240}
241
242/// Build clipped scatter points from data points.
243pub(crate) fn build_scatter_points(
244    points: &[Point],
245    transform: &Transform,
246    clip: ScreenRect,
247    out: &mut Vec<ScreenPoint>,
248) {
249    out.clear();
250    for point in points {
251        let Some(screen) = transform.data_to_screen(*point) else {
252            continue;
253        };
254        if screen.x >= clip.min.x
255            && screen.x <= clip.max.x
256            && screen.y >= clip.min.y
257            && screen.y <= clip.max.y
258        {
259            out.push(screen);
260        }
261    }
262}
263
264fn clip_segment(
265    mut start: ScreenPoint,
266    mut end: ScreenPoint,
267    rect: ScreenRect,
268) -> Option<(ScreenPoint, ScreenPoint)> {
269    const LEFT: u8 = 1;
270    const RIGHT: u8 = 2;
271    const TOP: u8 = 4;
272    const BOTTOM: u8 = 8;
273
274    let mut out_start = region_code(start, rect, LEFT, RIGHT, TOP, BOTTOM);
275    let mut out_end = region_code(end, rect, LEFT, RIGHT, TOP, BOTTOM);
276
277    loop {
278        if (out_start | out_end) == 0 {
279            return Some((start, end));
280        }
281        if (out_start & out_end) != 0 {
282            return None;
283        }
284
285        let out_code = if out_start != 0 { out_start } else { out_end };
286        let (mut x, mut y) = (0.0_f32, 0.0_f32);
287
288        if (out_code & TOP) != 0 {
289            x = start.x + (end.x - start.x) * (rect.min.y - start.y) / (end.y - start.y);
290            y = rect.min.y;
291        } else if (out_code & BOTTOM) != 0 {
292            x = start.x + (end.x - start.x) * (rect.max.y - start.y) / (end.y - start.y);
293            y = rect.max.y;
294        } else if (out_code & RIGHT) != 0 {
295            y = start.y + (end.y - start.y) * (rect.max.x - start.x) / (end.x - start.x);
296            x = rect.max.x;
297        } else if (out_code & LEFT) != 0 {
298            y = start.y + (end.y - start.y) * (rect.min.x - start.x) / (end.x - start.x);
299            x = rect.min.x;
300        }
301
302        let new_point = ScreenPoint::new(x, y);
303        if out_code == out_start {
304            start = new_point;
305            out_start = region_code(start, rect, LEFT, RIGHT, TOP, BOTTOM);
306        } else {
307            end = new_point;
308            out_end = region_code(end, rect, LEFT, RIGHT, TOP, BOTTOM);
309        }
310    }
311}
312
313fn region_code(
314    point: ScreenPoint,
315    rect: ScreenRect,
316    left: u8,
317    right: u8,
318    top: u8,
319    bottom: u8,
320) -> u8 {
321    let mut code = 0;
322    if point.x < rect.min.x {
323        code |= left;
324    } else if point.x > rect.max.x {
325        code |= right;
326    }
327    if point.y < rect.min.y {
328        code |= top;
329    } else if point.y > rect.max.y {
330        code |= bottom;
331    }
332    code
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338    use crate::geom::Point;
339    use crate::view::Range;
340    use crate::view::Viewport;
341
342    #[test]
343    fn clip_segment_inside() {
344        let rect = ScreenRect::new(ScreenPoint::new(0.0, 0.0), ScreenPoint::new(10.0, 10.0));
345        let start = ScreenPoint::new(2.0, 2.0);
346        let end = ScreenPoint::new(8.0, 8.0);
347        let clipped = clip_segment(start, end, rect).expect("segment should clip");
348        assert_eq!(clipped.0, start);
349        assert_eq!(clipped.1, end);
350    }
351
352    #[test]
353    fn build_segments_with_transform() {
354        let viewport = Viewport::new(Range::new(0.0, 1.0), Range::new(0.0, 1.0));
355        let rect = ScreenRect::new(ScreenPoint::new(0.0, 0.0), ScreenPoint::new(10.0, 10.0));
356        let transform = Transform::new(viewport, rect).expect("valid transform");
357        let points = [Point::new(0.0, 0.0), Point::new(1.0, 1.0)];
358        let mut out = Vec::new();
359        build_line_segments(&points, &transform, rect, &mut out);
360        assert_eq!(out.len(), 1);
361    }
362}