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