1use 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
16pub 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
32pub struct GpuCanvasContext<'a, 'b> {
38 window: &'a mut Window<'b>,
39 cx: &'a mut App,
40 bounds: Bounds<Pixels>,
41}
42
43impl GpuCanvasContext<'_, '_> {
44 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 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 pub fn bounds(&self) -> Bounds<Pixels> {
77 self.bounds
78 }
79
80 pub fn scale_factor(&self) -> f32 {
83 self.window.scale_factor()
84 }
85
86 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 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 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
131pub struct GpuCanvas {
137 div: Div,
138 content: Option<Box<dyn FnOnce(&mut GpuCanvasContext)>>,
139 covers: Option<Rc<dyn Fn(Point<f32>) -> bool>>,
142}
143
144impl GpuCanvas {
145 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 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 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 .child(
304 div()
305 .id("behind")
306 .absolute()
307 .size_full()
308 .on_click(move |_, _, _| clicks.set(clicks.get() + 1)),
309 )
310 .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 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 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}