Skip to main content

gpui_component/plot/
path_cache.rs

1use std::hash::{DefaultHasher, Hash, Hasher};
2
3use gpui::{App, ElementId, Entity, Path, Pixels, Point, Window};
4
5/// A tessellated path reused across frames while its shape is unchanged.
6///
7/// A chart repaints on every frame it is on screen — a scrolling list moves
8/// it — and tessellating its strokes (Catmull-Rom curves, dashes) is the bulk
9/// of that work, while the vertices only depend on the projected points
10/// relative to the chart's origin. A plot keeps one cache per shape and paints
11/// through [`Line::paint_cached`](super::shape::Line::paint_cached) or
12/// [`Area::paint_cached`](super::shape::Area::paint_cached): the path is built
13/// once per shape key, at a zero origin, and moved to the frame's origin on
14/// every paint.
15#[derive(Default)]
16pub struct PathCache {
17    key: Option<u64>,
18    /// Built relative to a zero origin.
19    path: Option<Path<Pixels>>,
20}
21
22impl PathCache {
23    /// The path for `key`, moved to `origin`. `build` runs only when the key
24    /// differs from the last call's; it must build relative to a zero origin.
25    pub fn get(
26        &mut self,
27        key: u64,
28        origin: Point<Pixels>,
29        build: impl FnOnce() -> Option<Path<Pixels>>,
30    ) -> Option<Path<Pixels>> {
31        if self.key != Some(key) {
32            self.path = build();
33            self.key = Some(key);
34        }
35        self.path.as_ref().map(|path| translated(path, origin))
36    }
37
38    /// Whether the last [`Self::get`] reused the path built by an earlier one.
39    pub fn is_warm(&self) -> bool {
40        self.key.is_some()
41    }
42}
43
44/// `path` moved by `offset`. A finished path is only its bounds and vertices;
45/// the builder cursor it keeps is not read again.
46fn translated(path: &Path<Pixels>, offset: Point<Pixels>) -> Path<Pixels> {
47    let mut path = path.clone();
48    path.bounds.origin = path.bounds.origin + offset;
49    for vertex in &mut path.vertices {
50        vertex.xy_position = vertex.xy_position + offset;
51    }
52    path
53}
54
55/// The [`PathCache`]s of a plot that is rebuilt on every render, kept in the
56/// window's element state so they outlive the plot value.
57///
58/// Plots are plain values built by `render` and painted once, so a cache
59/// held by the plot would be empty every frame; this keeps them under the
60/// element id the plot paints in (plus `key`), for as long as the plot is
61/// painted on consecutive frames.
62///
63/// ```ignore
64/// fn paint(&mut self, bounds: Bounds<Pixels>, window: &mut Window, cx: &mut App) {
65///     let caches = PathCaches::for_paint("lines", window, cx);
66///     caches.update(cx, |caches, _| {
67///         for (ix, line) in lines.iter().enumerate() {
68///             line.paint_cached(&bounds, caches.slot(ix), window);
69///         }
70///     });
71/// }
72/// ```
73#[derive(Default)]
74pub struct PathCaches {
75    slots: Vec<PathCache>,
76}
77
78impl PathCaches {
79    /// The caches for the plot painting under the window's current element
80    /// id; `key` tells apart several groups of shapes in one plot.
81    pub fn for_paint(key: impl Into<ElementId>, window: &mut Window, cx: &mut App) -> Entity<Self> {
82        window.use_keyed_state(key, cx, |_, _| Self::default())
83    }
84
85    /// The `index`-th cache, created on first use. Paint each shape through
86    /// the same index every frame.
87    pub fn slot(&mut self, index: usize) -> &mut PathCache {
88        if self.slots.len() <= index {
89            self.slots.resize_with(index + 1, PathCache::default);
90        }
91        &mut self.slots[index]
92    }
93
94    /// Two caches for a shape that keeps a fill and a stroke, such as
95    /// [`Area::paint_cached`](super::shape::Area::paint_cached), at
96    /// `2 * index` and `2 * index + 1`.
97    pub fn slot_pair(&mut self, index: usize) -> (&mut PathCache, &mut PathCache) {
98        let first = 2 * index;
99        if self.slots.len() <= first + 1 {
100            self.slots.resize_with(first + 2, PathCache::default);
101        }
102        let (head, tail) = self.slots.split_at_mut(first + 1);
103        (&mut head[first], &mut tail[0])
104    }
105}
106
107/// A shape key from its projected points (origin-relative) and whatever else
108/// shapes the tessellation (stroke width, curve style, dash pattern).
109pub struct ShapeKey(DefaultHasher);
110
111impl ShapeKey {
112    pub fn new(extra: impl Hash) -> Self {
113        let mut hasher = DefaultHasher::new();
114        extra.hash(&mut hasher);
115        Self(hasher)
116    }
117
118    pub fn point(&mut self, point: Point<Pixels>) -> &mut Self {
119        point.x.as_f32().to_bits().hash(&mut self.0);
120        point.y.as_f32().to_bits().hash(&mut self.0);
121        self
122    }
123
124    pub fn f32(&mut self, value: f32) -> &mut Self {
125        value.to_bits().hash(&mut self.0);
126        self
127    }
128
129    pub fn finish(&self) -> u64 {
130        self.0.finish()
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use gpui::{PathBuilder, point, px};
138
139    fn diagonal() -> Option<Path<Pixels>> {
140        let mut builder = PathBuilder::stroke(px(2.));
141        builder.move_to(point(px(0.), px(0.)));
142        builder.line_to(point(px(10.), px(10.)));
143        builder.build().ok()
144    }
145
146    #[test]
147    fn builds_once_per_key_and_moves_to_each_origin() {
148        let mut cache = PathCache::default();
149        let mut builds = 0;
150        let first = cache
151            .get(1, point(px(100.), px(50.)), || {
152                builds += 1;
153                diagonal()
154            })
155            .unwrap();
156        let second = cache
157            .get(1, point(px(200.), px(50.)), || {
158                builds += 1;
159                diagonal()
160            })
161            .unwrap();
162        assert_eq!(builds, 1);
163        assert_eq!(first.vertices.len(), second.vertices.len());
164        for (a, b) in first.vertices.iter().zip(&second.vertices) {
165            assert_eq!(b.xy_position.x - a.xy_position.x, px(100.));
166            assert_eq!(b.xy_position.y, a.xy_position.y);
167            assert_eq!(a.st_position, b.st_position);
168        }
169        assert_eq!(second.bounds.origin.x - first.bounds.origin.x, px(100.));
170        assert_eq!(first.bounds.size, second.bounds.size);
171
172        cache.get(2, point(px(0.), px(0.)), || {
173            builds += 1;
174            diagonal()
175        });
176        assert_eq!(builds, 2);
177    }
178
179    #[test]
180    fn keys_follow_points_and_extras() {
181        let a = ShapeKey::new(("linear", 1.0f32.to_bits()))
182            .point(point(px(1.), px(2.)))
183            .finish();
184        let same = ShapeKey::new(("linear", 1.0f32.to_bits()))
185            .point(point(px(1.), px(2.)))
186            .finish();
187        let moved = ShapeKey::new(("linear", 1.0f32.to_bits()))
188            .point(point(px(1.), px(3.)))
189            .finish();
190        let thicker = ShapeKey::new(("linear", 2.0f32.to_bits()))
191            .point(point(px(1.), px(2.)))
192            .finish();
193        assert_eq!(a, same);
194        assert_ne!(a, moved);
195        assert_ne!(a, thicker);
196    }
197}