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