Checkpoint

Nathan Sobo created

Change summary

crates/gpui3/src/elements.rs                    |   2 
crates/gpui3/src/elements/interactive.rs        | 152 +++++++++++++++++++
crates/gpui3/src/platform/mac/metal_atlas.rs    |   1 
crates/gpui3/src/platform/mac/metal_renderer.rs |  11 
crates/gpui3/src/platform/mac/platform.rs       |   2 
crates/gpui3/src/scene.rs                       |  13 
crates/gpui3/src/window.rs                      | 150 +++++++++++++++++-
7 files changed, 309 insertions(+), 22 deletions(-)

Detailed changes

crates/gpui3/src/elements.rs 🔗

@@ -1,11 +1,13 @@
 mod div;
 mod img;
+mod interactive;
 mod stateless;
 mod svg;
 mod text;
 
 pub use div::*;
 pub use img::*;
+pub use interactive::*;
 pub use stateless::*;
 pub use svg::*;
 pub use text::*;

crates/gpui3/src/elements/interactive.rs 🔗

@@ -0,0 +1,152 @@
+use crate::{
+    Bounds, DispatchPhase, MouseButton, MouseDownEvent, MouseUpEvent, Pixels, ViewContext,
+};
+use parking_lot::Mutex;
+use smallvec::SmallVec;
+use std::sync::Arc;
+
+pub trait Interactive<S: 'static + Send + Sync> {
+    fn interaction_listeners(&mut self) -> &mut InteractionHandlers<S>;
+
+    fn on_mouse_down(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(&mut S, &MouseDownEvent, &mut ViewContext<S>) + Send + Sync + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interaction_listeners()
+            .mouse_down
+            .push(Arc::new(move |view, event, phase, cx| {
+                if phase == DispatchPhase::Bubble && event.button == button {
+                    handler(view, event, cx)
+                }
+            }));
+        self
+    }
+
+    fn on_mouse_up(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(&mut S, &MouseUpEvent, &mut ViewContext<S>) + Send + Sync + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interaction_listeners()
+            .mouse_up
+            .push(Arc::new(move |view, event, phase, cx| {
+                if phase == DispatchPhase::Bubble && event.button == button {
+                    handler(view, event, cx)
+                }
+            }));
+        self
+    }
+
+    fn on_mouse_down_out(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(&mut S, &MouseDownEvent, &mut ViewContext<S>) + Send + Sync + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interaction_listeners()
+            .mouse_down
+            .push(Arc::new(move |view, event, phase, cx| {
+                if phase == DispatchPhase::Capture && event.button == button {
+                    handler(view, event, cx)
+                }
+            }));
+        self
+    }
+
+    fn on_mouse_up_out(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(&mut S, &MouseUpEvent, &mut ViewContext<S>) + Send + Sync + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interaction_listeners()
+            .mouse_up
+            .push(Arc::new(move |view, event, phase, cx| {
+                if event.button == button && phase == DispatchPhase::Capture {
+                    handler(view, event, cx);
+                }
+            }));
+        self
+    }
+
+    fn on_click(
+        self,
+        button: MouseButton,
+        handler: impl Fn(&mut S, &MouseDownEvent, &MouseUpEvent, &mut ViewContext<S>)
+            + Send
+            + Sync
+            + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        let down_event = Arc::new(Mutex::new(None));
+        self.on_mouse_down(button, {
+            let down_event = down_event.clone();
+            move |_, event, _| {
+                down_event.lock().replace(event.clone());
+            }
+        })
+        .on_mouse_up_out(button, {
+            let down_event = down_event.clone();
+            move |_, _, _| {
+                down_event.lock().take();
+            }
+        })
+        .on_mouse_up(button, move |view, event, cx| {
+            if let Some(down_event) = down_event.lock().take() {
+                handler(view, &down_event, event, cx);
+            }
+        })
+    }
+}
+
+type MouseDownHandler<V> = Arc<
+    dyn Fn(&mut V, &MouseDownEvent, DispatchPhase, &mut ViewContext<V>) + Send + Sync + 'static,
+>;
+type MouseUpHandler<V> =
+    Arc<dyn Fn(&mut V, &MouseUpEvent, DispatchPhase, &mut ViewContext<V>) + Send + Sync + 'static>;
+
+pub struct InteractionHandlers<V: 'static> {
+    mouse_down: SmallVec<[MouseDownHandler<V>; 2]>,
+    mouse_up: SmallVec<[MouseUpHandler<V>; 2]>,
+}
+
+impl<S: Send + Sync + 'static> InteractionHandlers<S> {
+    pub fn paint(&self, bounds: Bounds<Pixels>, cx: &mut ViewContext<S>) {
+        for handler in self.mouse_down.iter().cloned() {
+            cx.on_mouse_event(move |view, event: &MouseDownEvent, phase, cx| {
+                if bounds.contains_point(event.position) {
+                    handler(view, event, phase, cx);
+                }
+            })
+        }
+        for handler in self.mouse_up.iter().cloned() {
+            cx.on_mouse_event(move |view, event: &MouseUpEvent, phase, cx| {
+                if bounds.contains_point(event.position) {
+                    handler(view, event, phase, cx);
+                }
+            })
+        }
+    }
+}
+
+impl<V> Default for InteractionHandlers<V> {
+    fn default() -> Self {
+        Self {
+            mouse_down: Default::default(),
+            mouse_up: Default::default(),
+        }
+    }
+}

crates/gpui3/src/platform/mac/metal_renderer.rs 🔗

@@ -172,7 +172,7 @@ impl MetalRenderer {
         let command_buffer = command_queue.new_command_buffer();
         let mut instance_offset = 0;
 
-        let path_tiles = self.rasterize_paths(scene.paths(), &mut instance_offset, &command_buffer);
+        // let path_tiles = self.rasterize_paths(scene.paths(), &mut instance_offset, &command_buffer);
 
         let render_pass_descriptor = metal::RenderPassDescriptor::new();
         let color_attachment = render_pass_descriptor
@@ -208,7 +208,7 @@ impl MetalRenderer {
                 PrimitiveBatch::Quads(quads) => {
                     self.draw_quads(quads, &mut instance_offset, viewport_size, command_encoder);
                 }
-                PrimitiveBatch::Paths(paths) => {
+                PrimitiveBatch::Paths(_paths) => {
                     // self.draw_paths(paths, &mut instance_offset, viewport_size, command_encoder);
                 }
                 PrimitiveBatch::Underlines(underlines) => {
@@ -258,11 +258,12 @@ impl MetalRenderer {
         drawable.present();
     }
 
+    #[allow(dead_code)]
     fn rasterize_paths(
         &mut self,
         paths: &[Path<ScaledPixels>],
-        offset: &mut usize,
-        command_buffer: &metal::CommandBufferRef,
+        _offset: &mut usize,
+        _command_buffer: &metal::CommandBufferRef,
     ) -> HashMap<PathId, AtlasTile> {
         let mut tiles = HashMap::default();
         let mut vertices_by_texture_id = HashMap::default();
@@ -288,7 +289,7 @@ impl MetalRenderer {
             tiles.insert(path.id, tile);
         }
 
-        for (texture_id, vertices) in vertices_by_texture_id {
+        for (_texture_id, _vertices) in vertices_by_texture_id {
             todo!();
             // align_offset(offset);
             // let next_offset = *offset + vertices.len() * mem::size_of::<PathVertex<ScaledPixels>>();

crates/gpui3/src/platform/mac/platform.rs 🔗

@@ -923,7 +923,7 @@ extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
         if let Some(event) = Event::from_native(native_event, None) {
             let platform = get_foreground_platform(this);
             if let Some(callback) = platform.0.lock().event.as_mut() {
-                if callback(event) {
+                if !callback(event) {
                     return;
                 }
             }

crates/gpui3/src/scene.rs 🔗

@@ -1,17 +1,17 @@
 use crate::{
     point, AtlasTextureId, AtlasTile, Bounds, ContentMask, Corners, Edges, Hsla, Pixels, Point,
-    ScaledPixels,
+    ScaledPixels, StackingOrder,
 };
 use collections::BTreeMap;
 use etagere::euclid::{Point3D, Vector3D};
 use plane_split::{BspSplitter, Polygon as BspPolygon};
-use smallvec::SmallVec;
 use std::{fmt::Debug, iter::Peekable, mem, slice};
 
 // Exported to metal
 pub type PointF = Point<f32>;
-pub type StackingOrder = SmallVec<[u32; 16]>;
+
 pub type LayerId = u32;
+
 pub type DrawOrder = u32;
 
 pub(crate) struct SceneBuilder {
@@ -180,6 +180,7 @@ pub(crate) struct Scene {
 }
 
 impl Scene {
+    #[allow(dead_code)]
     pub fn paths(&self) -> &[Path<ScaledPixels>] {
         &self.paths
     }
@@ -731,9 +732,9 @@ mod tests {
         let mut scene = SceneBuilder::new();
         assert_eq!(scene.layers_by_order.len(), 0);
 
-        scene.insert(&smallvec![1], quad());
-        scene.insert(&smallvec![2], shadow());
-        scene.insert(&smallvec![3], quad());
+        scene.insert(&smallvec![1].into(), quad());
+        scene.insert(&smallvec![2].into(), shadow());
+        scene.insert(&smallvec![3].into(), quad());
 
         let mut batches_count = 0;
         for _ in scene.build().batches() {

crates/gpui3/src/window.rs 🔗

@@ -1,20 +1,45 @@
 use crate::{
     image_cache::RenderImageParams, px, size, AnyView, AppContext, AsyncWindowContext,
     AvailableSpace, BorrowAppContext, Bounds, BoxShadow, Context, Corners, DevicePixels, DisplayId,
-    Edges, Effect, Element, EntityId, FontId, GlyphId, Handle, Hsla, ImageData, IsZero, LayoutId,
-    MainThread, MainThreadOnly, MonochromeSprite, Path, Pixels, PlatformAtlas, PlatformWindow,
-    Point, PolychromeSprite, Quad, Reference, RenderGlyphParams, RenderSvgParams, ScaledPixels,
-    SceneBuilder, Shadow, SharedString, Size, StackingOrder, Style, TaffyLayoutEngine, Task,
+    Edges, Effect, Element, EntityId, Event, FontId, GlyphId, Handle, Hsla, ImageData, IsZero,
+    LayoutId, MainThread, MainThreadOnly, MonochromeSprite, Path, Pixels, PlatformAtlas,
+    PlatformWindow, Point, PolychromeSprite, Quad, Reference, RenderGlyphParams, RenderSvgParams,
+    ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, TaffyLayoutEngine, Task,
     Underline, UnderlineStyle, WeakHandle, WindowOptions, SUBPIXEL_VARIANTS,
 };
 use anyhow::Result;
+use collections::HashMap;
+use derive_more::{Deref, DerefMut};
 use smallvec::SmallVec;
 use std::{
-    any::TypeId, borrow::Cow, fmt::Debug, future::Future, marker::PhantomData, mem, sync::Arc,
+    any::{Any, TypeId},
+    borrow::Cow,
+    fmt::Debug,
+    future::Future,
+    marker::PhantomData,
+    mem,
+    sync::Arc,
 };
 use util::ResultExt;
 
-pub struct AnyWindow {}
+#[derive(Deref, DerefMut, Ord, PartialOrd, Eq, PartialEq, Clone, Default)]
+pub struct StackingOrder(pub(crate) SmallVec<[u32; 16]>);
+
+#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)]
+pub enum DispatchPhase {
+    /// After the capture phase comes the bubble phase, in which event handlers are
+    /// invoked front to back. This is the phase you'll usually want to use for event handlers.
+    #[default]
+    Bubble,
+    /// During the initial capture phase, event handlers are invoked back to front. This phase
+    /// is used for special purposes such as clearing the "pressed" state for click events. If
+    /// you stop event propagation during this phase, you need to know what you're doing. Handlers
+    /// outside of the immediate region may rely on detecting non-local events during this phase.
+    Capture,
+}
+
+type MouseEventHandler =
+    Arc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + Send + Sync + 'static>;
 
 pub struct Window {
     handle: AnyWindowHandle,
@@ -25,9 +50,11 @@ pub struct Window {
     content_size: Size<Pixels>,
     layout_engine: TaffyLayoutEngine,
     pub(crate) root_view: Option<AnyView<()>>,
-    mouse_position: Point<Pixels>,
     current_stacking_order: StackingOrder,
     content_mask_stack: Vec<ContentMask<Pixels>>,
+    mouse_event_handlers: HashMap<TypeId, Vec<(StackingOrder, MouseEventHandler)>>,
+    propagate_event: bool,
+    mouse_position: Point<Pixels>,
     scale_factor: f32,
     pub(crate) scene_builder: SceneBuilder,
     pub(crate) dirty: bool,
@@ -46,7 +73,6 @@ impl Window {
         let content_size = platform_window.content_size();
         let scale_factor = platform_window.scale_factor();
         platform_window.on_resize(Box::new({
-            let handle = handle;
             let cx = cx.to_async();
             move |content_size, scale_factor| {
                 cx.update_window(handle, |cx| {
@@ -65,6 +91,15 @@ impl Window {
             }
         }));
 
+        platform_window.on_event({
+            let cx = cx.to_async();
+            Box::new(move |event| {
+                cx.update_window(handle, |cx| cx.dispatch_event(event))
+                    .log_err()
+                    .unwrap_or(true)
+            })
+        });
+
         let platform_window = MainThreadOnly::new(Arc::new(platform_window), cx.executor.clone());
 
         Window {
@@ -76,9 +111,11 @@ impl Window {
             content_size,
             layout_engine: TaffyLayoutEngine::new(),
             root_view: None,
-            mouse_position,
-            current_stacking_order: SmallVec::new(),
+            current_stacking_order: StackingOrder(SmallVec::new()),
             content_mask_stack: Vec::new(),
+            mouse_event_handlers: HashMap::default(),
+            propagate_event: true,
+            mouse_position,
             scale_factor,
             scene_builder: SceneBuilder::new(),
             dirty: true,
@@ -241,6 +278,27 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         self.window.rem_size
     }
 
+    pub fn stop_event_propagation(&mut self) {
+        self.window.propagate_event = false;
+    }
+
+    pub fn on_mouse_event<Event: 'static>(
+        &mut self,
+        handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + Send + Sync + 'static,
+    ) {
+        let order = self.window.current_stacking_order.clone();
+        self.window
+            .mouse_event_handlers
+            .entry(TypeId::of::<Event>())
+            .or_default()
+            .push((
+                order,
+                Arc::new(move |event: &dyn Any, phase, cx| {
+                    handler(event.downcast_ref().unwrap(), phase, cx)
+                }),
+            ))
+    }
+
     pub fn mouse_position(&self) -> Point<Pixels> {
         self.window.mouse_position
     }
@@ -529,6 +587,11 @@ impl<'a, 'w> WindowContext<'a, 'w> {
     pub(crate) fn draw(&mut self) -> Result<()> {
         let unit_entity = self.unit_entity.clone();
         self.update_entity(&unit_entity, |view, cx| {
+            cx.window
+                .mouse_event_handlers
+                .values_mut()
+                .for_each(Vec::clear);
+
             let mut root_view = cx.window.root_view.take().unwrap();
             let (root_layout_id, mut frame_state) = root_view.layout(&mut (), cx)?;
             let available_space = cx.window.content_size.map(Into::into);
@@ -554,6 +617,54 @@ impl<'a, 'w> WindowContext<'a, 'w> {
             Ok(())
         })
     }
+
+    fn dispatch_event(&mut self, event: Event) -> bool {
+        if let Some(any_mouse_event) = event.mouse_event() {
+            if let Some(mut handlers) = self
+                .window
+                .mouse_event_handlers
+                .remove(&any_mouse_event.type_id())
+            {
+                // We sort these every time, because handlers may add handlers. Probably fast enough.
+                handlers.sort_by(|(a, _), (b, _)| a.cmp(b));
+
+                // Handlers may set this to false by calling `stop_propagation`;
+                self.window.propagate_event = true;
+
+                // Capture phase, events bubble from back to front. Handlers for this phase are used for
+                // special purposes, such as detecting events outside of a given Bounds.
+                for (_, handler) in &handlers {
+                    handler(any_mouse_event, DispatchPhase::Capture, self);
+                    if !self.window.propagate_event {
+                        break;
+                    }
+                }
+
+                // Bubble phase
+                if self.window.propagate_event {
+                    for (_, handler) in handlers.iter().rev() {
+                        handler(any_mouse_event, DispatchPhase::Bubble, self);
+                        if !self.window.propagate_event {
+                            break;
+                        }
+                    }
+                }
+
+                handlers.extend(
+                    self.window
+                        .mouse_event_handlers
+                        .get_mut(&any_mouse_event.type_id())
+                        .into_iter()
+                        .flat_map(|handlers| handlers.drain(..)),
+                );
+                self.window
+                    .mouse_event_handlers
+                    .insert(any_mouse_event.type_id(), handlers);
+            }
+        }
+
+        true
+    }
 }
 
 impl Context for WindowContext<'_, '_> {
@@ -786,6 +897,18 @@ impl<'a, 'w, S: Send + Sync + 'static> ViewContext<'a, 'w, S> {
         })
     }
 
+    pub fn on_mouse_event<Event: 'static>(
+        &mut self,
+        handler: impl Fn(&mut S, &Event, DispatchPhase, &mut ViewContext<S>) + Send + Sync + 'static,
+    ) {
+        let handle = self.handle().upgrade(self).unwrap();
+        self.window_cx.on_mouse_event(move |event, phase, cx| {
+            handle.update(cx, |view, cx| {
+                handler(view, event, phase, cx);
+            })
+        });
+    }
+
     pub(crate) fn erase_state<R>(&mut self, f: impl FnOnce(&mut ViewContext<()>) -> R) -> R {
         let entity_id = self.unit_entity.id;
         let mut cx = ViewContext::mutable(
@@ -874,3 +997,10 @@ pub struct AnyWindowHandle {
     pub(crate) id: WindowId,
     state_type: TypeId,
 }
+
+#[cfg(any(test, feature = "test"))]
+impl From<SmallVec<[u32; 16]>> for StackingOrder {
+    fn from(small_vec: SmallVec<[u32; 16]>) -> Self {
+        StackingOrder(small_vec)
+    }
+}