Skip to main content

gpui_authoring/elements/
gpu_canvas.rs

1//! `GpuCanvas`: the authoring surface for pixels produced outside GPUI.
2//!
3//! A map, a viewport, a video frame, a 3D scene — content a producer renders offscreen and hands
4//! to the window — composites through this element. It is an ordinary box (it *is* a [`Div`]) with
5//! a GPU surface painted after the box's own background and border, so the content stacks like any
6//! other layer.
7
8use crate::{
9    App, Bounds, Corners, Div, Element, ElementId, GlobalElementId, GpuRenderer, HitboxBehavior,
10    HitboxFilter, ImportedTextureHandle, InteractiveElement, Interactivity, IntoElement, LayoutId,
11    PaintSurface, ParentElement, Pixels, Point, StyleRefinement, Styled, SurfaceSource, Window,
12    div,
13};
14use std::rc::Rc;
15
16/// Build a GPU canvas, an ordinary box whose content a paint-time callback supplies.
17///
18/// The callback receives a [`GpuCanvasContext`], the only place raw GPU import is reachable: call
19/// [`GpuCanvasContext::paint_surface`] to composite a surface produced outside GPUI, or
20/// [`GpuCanvasContext::paint_texture`] for a texture made on the window renderer's own device. The
21/// callback is `FnOnce` and consumed once, matching [`crate::Canvas`]: the element tree is rebuilt
22/// every frame, so it is used exactly once. Paint time is when the window's renderer — and so its
23/// device — is guaranteed to exist.
24pub fn gpu_canvas(content: impl 'static + FnOnce(&mut GpuCanvasContext)) -> GpuCanvas {
25    GpuCanvas {
26        div: div(),
27        content: Some(Box::new(content)),
28        covers: None,
29    }
30}
31
32/// A slice of `Window` — the only place raw GPU import is reachable.
33///
34/// Handed to a `gpu_canvas` callback. A plain `&mut Window` or an `App` cannot composite a foreign
35/// surface; a canvas narrows the window to exactly the operations that can. Bounded by the canvas,
36/// so `paint_surface`/`paint_texture` composite into its bounds.
37pub struct GpuCanvasContext<'a, 'b> {
38    window: &'a mut Window<'b>,
39    cx: &'a mut App,
40    bounds: Bounds<Pixels>,
41}
42
43impl GpuCanvasContext<'_, '_> {
44    /// Composite `source` (a dma-buf, a Direct3D texture/view, or a CoreVideo buffer) into the
45    /// canvas's bounds for this frame.
46    pub fn paint_surface(&mut self, source: impl Into<SurfaceSource>) {
47        self.window.core.invalidator.debug_assert_paint();
48
49        let bounds = self.window.snap_bounds(self.bounds);
50        let content_mask = self.window.snapped_content_mask();
51        self.window
52            .frame_state
53            .next_frame
54            .scene
55            .insert_primitive(PaintSurface {
56                order: 0,
57                bounds,
58                content_mask,
59                source: source.into(),
60            });
61    }
62
63    /// Composite a texture produced on the window renderer's own device into the canvas's bounds.
64    pub fn paint_texture(
65        &mut self,
66        handle: ImportedTextureHandle,
67        corner_radii: Corners<Pixels>,
68        opacity: f32,
69        flip_v: bool,
70    ) {
71        self.window
72            .paint_imported_texture(handle, self.bounds, corner_radii, opacity, flip_v);
73    }
74
75    /// The canvas's bounds.
76    pub fn bounds(&self) -> Bounds<Pixels> {
77        self.bounds
78    }
79
80    /// The window's scale factor. A canvas that owns a backing surface sizes it in physical
81    /// pixels, so the source and the element have the same aspect at any display scale.
82    pub fn scale_factor(&self) -> f32 {
83        self.window.scale_factor()
84    }
85
86    /// The window renderer's device, downcast to `R`.
87    ///
88    /// This is the same-device producer's door: it hands back an owned handle (`ID3D11Device`, a
89    /// `metal::Device`, a wgpu device and queue) that the caller may keep, because a borrow of the
90    /// renderer's device cannot outlive the call. Naming `R` is the assertion that the window draws
91    /// through that backend — a `#[cfg]`-selected producer already knows — so a mismatch panics; use
92    /// [`try_device`](Self::try_device) to handle it instead.
93    pub fn device<R: GpuRenderer>(&mut self) -> R::Device {
94        self.try_device::<R>().unwrap_or_else(|| {
95            panic!(
96                "the window's renderer is not a {}: use try_device to handle this",
97                std::any::type_name::<R>(),
98            )
99        })
100    }
101
102    /// The window renderer's device, downcast to `R`, or `None` when the renderer is not `R` or has
103    /// no device to lend right now.
104    ///
105    /// The fallible form of [`device`](Self::device), for a producer that degrades — paints a
106    /// dma-buf instead of a same-device texture, or skips the frame — rather than asserting the
107    /// backend it is running under.
108    pub fn try_device<R: GpuRenderer>(&mut self) -> Option<R::Device> {
109        let mut lent = None;
110        self.window
111            .core
112            .platform_window
113            .with_renderer(&mut |renderer| {
114                lent = renderer.as_renderer::<R>().and_then(GpuRenderer::device);
115            });
116        lent
117    }
118
119    /// The application, mirroring the `cx` a `canvas()` callback receives.
120    pub fn cx(&mut self) -> &mut App {
121        self.cx
122    }
123}
124
125impl<'a, 'b> GpuCanvasContext<'a, 'b> {
126    pub(crate) fn new(window: &'a mut Window<'b>, cx: &'a mut App, bounds: Bounds<Pixels>) -> Self {
127        Self { window, cx, bounds }
128    }
129}
130
131/// An element that composites a surface produced outside GPUI, on top of an ordinary box.
132///
133/// All the layout, styling and interactivity are a [`Div`]'s — this forwards every trait to the
134/// box it holds, so a canvas behaves like any other element — and only the paint phase adds the
135/// one thing a `Div` cannot: a surface primitive pushed after the box.
136pub struct GpuCanvas {
137    div: Div,
138    content: Option<Box<dyn FnOnce(&mut GpuCanvasContext)>>,
139    /// Where the surface's content is, so the canvas claims the pointer only there. `None` claims
140    /// nothing, which is what an ordinary box does.
141    covers: Option<Rc<dyn Fn(Point<f32>) -> bool>>,
142}
143
144impl GpuCanvas {
145    /// Claim the pointer only where the surface's content is, instead of its whole rectangle.
146    ///
147    /// `covers` is asked with the pointer in the canvas's own coordinates — `0..1` across its bounds,
148    /// origin top-left, the frame the composited surface is indexed by — and answers whether the
149    /// content is there. Where it is, the canvas holds the pointer the way
150    /// [`InteractiveElement::occlude`] does and nothing behind it is hovered; where it is not, the
151    /// canvas is an ordinary box and hit-testing carries on to whatever is drawn behind it.
152    ///
153    /// It is asked at the position of the event being hit-tested, not at the pointer's position when
154    /// a frame was drawn, so the answer neither lags an event nor depends on the window redrawing.
155    ///
156    /// The default — no `covers` — is the ordinary box: the canvas receives its own events, and so
157    /// does anything under it. A fully opaque surface wants [`InteractiveElement::occlude`], which
158    /// needs no callback.
159    pub fn occlude_where(mut self, covers: impl Fn(Point<f32>) -> bool + 'static) -> Self {
160        self.covers = Some(Rc::new(covers));
161        self
162    }
163}
164
165impl Styled for GpuCanvas {
166    fn style(&mut self) -> &mut StyleRefinement {
167        self.div.style()
168    }
169}
170
171impl InteractiveElement for GpuCanvas {
172    fn interactivity(&mut self) -> &mut Interactivity {
173        self.div.interactivity()
174    }
175}
176
177impl ParentElement for GpuCanvas {
178    fn extend(&mut self, elements: impl IntoIterator<Item = crate::AnyElement>) {
179        self.div.extend(elements)
180    }
181}
182
183impl IntoElement for GpuCanvas {
184    type Element = Self;
185
186    fn into_element(self) -> Self::Element {
187        self
188    }
189}
190
191impl Element for GpuCanvas {
192    type RequestLayoutState = <Div as Element>::RequestLayoutState;
193    type PrepaintState = <Div as Element>::PrepaintState;
194
195    fn id(&self) -> Option<ElementId> {
196        <Div as Element>::id(&self.div)
197    }
198
199    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
200        self.div.source_location()
201    }
202
203    fn a11y_role(&self) -> Option<accesskit::Role> {
204        self.div.a11y_role()
205    }
206
207    fn write_a11y_info(&self, node: &mut accesskit::Node) {
208        self.div.write_a11y_info(node);
209    }
210
211    fn a11y_synthetic_children(
212        &mut self,
213        prepaint: &mut Self::PrepaintState,
214        builder: &mut crate::A11ySubtreeBuilder,
215    ) {
216        self.div.a11y_synthetic_children(prepaint, builder);
217    }
218
219    fn request_layout(
220        &mut self,
221        id: Option<&GlobalElementId>,
222        window: &mut Window,
223        cx: &mut App,
224    ) -> (LayoutId, Self::RequestLayoutState) {
225        self.div.request_layout(id, window, cx)
226    }
227
228    fn prepaint(
229        &mut self,
230        id: Option<&GlobalElementId>,
231        bounds: Bounds<Pixels>,
232        request_layout: &mut Self::RequestLayoutState,
233        window: &mut Window,
234        cx: &mut App,
235    ) -> Self::PrepaintState {
236        // Claim the pointer the way `occlude` does, but only where the content is: a blocking hitbox
237        // narrowed by a filter, so over the gaps hit-testing carries on to the layer behind. The
238        // predicate is asked at the *event's* position — a frame's hitboxes are built once, but a
239        // filter is not frozen with them — so the answer neither lags an event nor needs a redraw.
240        if let Some(covers) = self.covers.clone() {
241            let (origin, size) = (bounds.origin, bounds.size);
242            let (width, height) = (size.width.as_f32(), size.height.as_f32());
243            let interactivity = self.div.interactivity();
244            interactivity.hitbox_behavior = HitboxBehavior::BlockMouse;
245            interactivity.hitbox_filter = Some(HitboxFilter::new(move |position| {
246                if width <= 0.0 || height <= 0.0 {
247                    return false;
248                }
249                let uv = Point {
250                    x: (position.x - origin.x).as_f32() / width,
251                    y: (position.y - origin.y).as_f32() / height,
252                };
253                covers(uv)
254            }));
255        }
256
257        self.div.prepaint(id, bounds, request_layout, window, cx)
258    }
259
260    fn paint(
261        &mut self,
262        id: Option<&GlobalElementId>,
263        bounds: Bounds<Pixels>,
264        request_layout: &mut Self::RequestLayoutState,
265        prepaint: &mut Self::PrepaintState,
266        window: &mut Window,
267        cx: &mut App,
268    ) {
269        // The box paints first — background, border, children — so the GPU content sits above
270        // them and below any later sibling, which is how a control stack over a map works at all.
271        self.div
272            .paint(id, bounds, request_layout, prepaint, window, cx);
273
274        if let Some(content) = self.content.take() {
275            content(&mut GpuCanvasContext::new(window, cx, bounds));
276        }
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::{
284        AnyWindowHandle, AppContext as _, Context, InputEvent, Modifiers, MouseButton,
285        MouseDownEvent, MouseUpEvent, Render, StatefulInteractiveElement, TestAppContext, point,
286        px, size,
287    };
288    use std::{cell::Cell, rc::Rc};
289
290    struct OccludeWhereTestView {
291        clicks: Rc<Cell<usize>>,
292        painted: Rc<Cell<Option<Bounds<Pixels>>>>,
293    }
294
295    impl Render for OccludeWhereTestView {
296        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
297            let clicks = self.clicks.clone();
298            let painted = self.painted.clone();
299            div()
300                .relative()
301                .size_full()
302                // The control the canvas sits over: an ordinary GPUI button filling the window.
303                .child(
304                    div()
305                        .id("behind")
306                        .absolute()
307                        .size_full()
308                        .on_click(move |_, _, _| clicks.set(clicks.get() + 1)),
309                )
310                // The canvas over it, claiming the pointer only over its left half.
311                .child(
312                    gpu_canvas(move |gpu| painted.set(Some(gpu.bounds())))
313                        .absolute()
314                        .size_full()
315                        .occlude_where(|uv| uv.x < 0.5),
316                )
317        }
318    }
319
320    #[gpui::test]
321    fn occlude_where_claims_the_pointer_only_over_content(cx: &mut TestAppContext) {
322        let clicks = Rc::new(Cell::new(0));
323        let painted = Rc::new(Cell::new(None));
324        let window = cx.open_window(size(px(200.), px(200.)), {
325            let clicks = clicks.clone();
326            let painted = painted.clone();
327            move |_, _| OccludeWhereTestView { clicks, painted }
328        });
329        let window = AnyWindowHandle::from(window);
330
331        let draw = |cx: &mut TestAppContext| {
332            cx.update_window(window, |_, window, cx| window.draw(cx).clear(cx))
333                .unwrap();
334        };
335        let click = |cx: &mut TestAppContext, position| {
336            cx.update_window(window, |_, window, cx| {
337                window.dispatch_event(
338                    MouseDownEvent {
339                        position,
340                        modifiers: Modifiers::default(),
341                        button: MouseButton::Left,
342                        click_count: 1,
343                        first_mouse: false,
344                    }
345                    .to_platform_input(),
346                    cx,
347                );
348                window.dispatch_event(
349                    MouseUpEvent {
350                        position,
351                        modifiers: Modifiers::default(),
352                        button: MouseButton::Left,
353                        click_count: 1,
354                    }
355                    .to_platform_input(),
356                    cx,
357                );
358            })
359            .unwrap();
360        };
361
362        // One frame, so the canvas has painted and its bounds are known: the predicate is asked in
363        // the canvas's own 0..1 frame, so the probe points are derived from those bounds rather than
364        // assumed.
365        draw(cx);
366        let painted = painted.get().expect("the canvas painted");
367        let over_content = point(
368            painted.origin.x + px(painted.size.width.as_f32() * 0.25),
369            painted.origin.y + px(painted.size.height.as_f32() * 0.5),
370        );
371        let over_nothing = point(
372            painted.origin.x + px(painted.size.width.as_f32() * 0.75),
373            painted.origin.y + px(painted.size.height.as_f32() * 0.5),
374        );
375
376        // No move and no redraw between that frame and these clicks: the filter is asked at each
377        // event's own position, so the frame drawn before is enough and the answer cannot lag a move.
378        // A control behind is reached only where the canvas declines.
379        click(cx, over_content);
380        assert_eq!(
381            clicks.get(),
382            0,
383            "a click over the canvas's content must not reach the control behind it"
384        );
385
386        click(cx, over_nothing);
387        assert_eq!(
388            clicks.get(),
389            1,
390            "a click over the empty half must pass through to the control behind it"
391        );
392    }
393}