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