use crate::{
App, Bounds, Corners, Div, Element, ElementId, GlobalElementId, GpuRenderer, HitboxBehavior,
HitboxFilter, ImportedTextureHandle, InteractiveElement, Interactivity, IntoElement, LayoutId,
PaintSurface, ParentElement, Pixels, Point, StyleRefinement, Styled, SurfaceSource, Window,
div,
};
use std::rc::Rc;
pub fn gpu_canvas(content: impl 'static + FnOnce(&mut GpuCanvasContext)) -> GpuCanvas {
GpuCanvas {
div: div(),
content: Some(Box::new(content)),
covers: None,
}
}
pub struct GpuCanvasContext<'a, 'b> {
window: &'a mut Window<'b>,
cx: &'a mut App,
bounds: Bounds<Pixels>,
}
impl GpuCanvasContext<'_, '_> {
pub fn paint_surface(&mut self, source: impl Into<SurfaceSource>) {
self.window.core.invalidator.debug_assert_paint();
let bounds = self.window.snap_bounds(self.bounds);
let content_mask = self.window.snapped_content_mask();
self.window
.frame_state
.next_frame
.scene
.insert_primitive(PaintSurface {
order: 0,
bounds,
content_mask,
source: source.into(),
});
}
pub fn paint_texture(
&mut self,
handle: ImportedTextureHandle,
corner_radii: Corners<Pixels>,
opacity: f32,
flip_v: bool,
) {
self.window
.paint_imported_texture(handle, self.bounds, corner_radii, opacity, flip_v);
}
pub fn bounds(&self) -> Bounds<Pixels> {
self.bounds
}
pub fn scale_factor(&self) -> f32 {
self.window.scale_factor()
}
pub fn device<R: GpuRenderer>(&mut self) -> R::Device {
self.try_device::<R>().unwrap_or_else(|| {
panic!(
"the window's renderer is not a {}: use try_device to handle this",
std::any::type_name::<R>(),
)
})
}
pub fn try_device<R: GpuRenderer>(&mut self) -> Option<R::Device> {
let mut lent = None;
self.window.core.platform_window.with_renderer(&mut |renderer| {
lent = renderer.as_renderer::<R>().and_then(GpuRenderer::device);
});
lent
}
pub fn cx(&mut self) -> &mut App {
self.cx
}
}
impl<'a, 'b> GpuCanvasContext<'a, 'b> {
pub(crate) fn new(
window: &'a mut Window<'b>,
cx: &'a mut App,
bounds: Bounds<Pixels>,
) -> Self {
Self {
window,
cx,
bounds,
}
}
}
pub struct GpuCanvas {
div: Div,
content: Option<Box<dyn FnOnce(&mut GpuCanvasContext)>>,
covers: Option<Rc<dyn Fn(Point<f32>) -> bool>>,
}
impl GpuCanvas {
pub fn occlude_where(mut self, covers: impl Fn(Point<f32>) -> bool + 'static) -> Self {
self.covers = Some(Rc::new(covers));
self
}
}
impl Styled for GpuCanvas {
fn style(&mut self) -> &mut StyleRefinement {
self.div.style()
}
}
impl InteractiveElement for GpuCanvas {
fn interactivity(&mut self) -> &mut Interactivity {
self.div.interactivity()
}
}
impl ParentElement for GpuCanvas {
fn extend(&mut self, elements: impl IntoIterator<Item = crate::AnyElement>) {
self.div.extend(elements)
}
}
impl IntoElement for GpuCanvas {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for GpuCanvas {
type RequestLayoutState = <Div as Element>::RequestLayoutState;
type PrepaintState = <Div as Element>::PrepaintState;
fn id(&self) -> Option<ElementId> {
<Div as Element>::id(&self.div)
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
self.div.source_location()
}
fn a11y_role(&self) -> Option<accesskit::Role> {
self.div.a11y_role()
}
fn write_a11y_info(&self, node: &mut accesskit::Node) {
self.div.write_a11y_info(node);
}
fn a11y_synthetic_children(
&mut self,
prepaint: &mut Self::PrepaintState,
builder: &mut crate::A11ySubtreeBuilder,
) {
self.div.a11y_synthetic_children(prepaint, builder);
}
fn request_layout(
&mut self,
id: Option<&GlobalElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
self.div.request_layout(id, window, cx)
}
fn prepaint(
&mut self,
id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
if let Some(covers) = self.covers.clone() {
let (origin, size) = (bounds.origin, bounds.size);
let (width, height) = (size.width.as_f32(), size.height.as_f32());
let interactivity = self.div.interactivity();
interactivity.hitbox_behavior = HitboxBehavior::BlockMouse;
interactivity.hitbox_filter = Some(HitboxFilter::new(move |position| {
if width <= 0.0 || height <= 0.0 {
return false;
}
let uv = Point {
x: (position.x - origin.x).as_f32() / width,
y: (position.y - origin.y).as_f32() / height,
};
covers(uv)
}));
}
self.div.prepaint(id, bounds, request_layout, window, cx)
}
fn paint(
&mut self,
id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
self.div
.paint(id, bounds, request_layout, prepaint, window, cx);
if let Some(content) = self.content.take() {
content(&mut GpuCanvasContext::new(window, cx, bounds));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
AnyWindowHandle, AppContext as _, Context, InputEvent, Modifiers, MouseButton,
MouseDownEvent, MouseUpEvent, Render, StatefulInteractiveElement, TestAppContext, point,
px, size,
};
use std::{cell::Cell, rc::Rc};
struct OccludeWhereTestView {
clicks: Rc<Cell<usize>>,
painted: Rc<Cell<Option<Bounds<Pixels>>>>,
}
impl Render for OccludeWhereTestView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let clicks = self.clicks.clone();
let painted = self.painted.clone();
div()
.relative()
.size_full()
.child(
div()
.id("behind")
.absolute()
.size_full()
.on_click(move |_, _, _| clicks.set(clicks.get() + 1)),
)
.child(
gpu_canvas(move |gpu| painted.set(Some(gpu.bounds())))
.absolute()
.size_full()
.occlude_where(|uv| uv.x < 0.5),
)
}
}
#[gpui::test]
fn occlude_where_claims_the_pointer_only_over_content(cx: &mut TestAppContext) {
let clicks = Rc::new(Cell::new(0));
let painted = Rc::new(Cell::new(None));
let window = cx.open_window(size(px(200.), px(200.)), {
let clicks = clicks.clone();
let painted = painted.clone();
move |_, _| OccludeWhereTestView { clicks, painted }
});
let window = AnyWindowHandle::from(window);
let draw = |cx: &mut TestAppContext| {
cx.update_window(window, |_, window, cx| window.draw(cx).clear(cx))
.unwrap();
};
let click = |cx: &mut TestAppContext, position| {
cx.update_window(window, |_, window, cx| {
window.dispatch_event(
MouseDownEvent {
position,
modifiers: Modifiers::default(),
button: MouseButton::Left,
click_count: 1,
first_mouse: false,
}
.to_platform_input(),
cx,
);
window.dispatch_event(
MouseUpEvent {
position,
modifiers: Modifiers::default(),
button: MouseButton::Left,
click_count: 1,
}
.to_platform_input(),
cx,
);
})
.unwrap();
};
draw(cx);
let painted = painted.get().expect("the canvas painted");
let over_content = point(
painted.origin.x + px(painted.size.width.as_f32() * 0.25),
painted.origin.y + px(painted.size.height.as_f32() * 0.5),
);
let over_nothing = point(
painted.origin.x + px(painted.size.width.as_f32() * 0.75),
painted.origin.y + px(painted.size.height.as_f32() * 0.5),
);
click(cx, over_content);
assert_eq!(
clicks.get(),
0,
"a click over the canvas's content must not reach the control behind it"
);
click(cx, over_nothing);
assert_eq!(
clicks.get(),
1,
"a click over the empty half must pass through to the control behind it"
);
}
}