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.core.platform_window.with_renderer(&mut |renderer| {
111 lent = renderer.as_renderer::<R>().and_then(GpuRenderer::device);
112 });
113 lent
114 }
115
116 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
136pub struct GpuCanvas {
142 div: Div,
143 content: Option<Box<dyn FnOnce(&mut GpuCanvasContext)>>,
144 covers: Option<Rc<dyn Fn(Point<f32>) -> bool>>,
147}
148
149impl GpuCanvas {
150 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 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 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 .child(
309 div()
310 .id("behind")
311 .absolute()
312 .size_full()
313 .on_click(move |_, _, _| clicks.set(clicks.get() + 1)),
314 )
315 .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 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 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}