diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 085ec86791..c7d619fd7d 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -16,7 +16,7 @@ pub mod fonts; pub mod geometry; mod presenter; mod scene; -pub use scene::{Border, CursorRegion, Quad, Scene}; +pub use scene::{Border, CursorRegion, MouseRegion, MouseRegionId, Quad, Scene}; pub mod text_layout; pub use text_layout::TextLayoutCache; mod util; diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 2bc241daee..cbd7b2cb82 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -8,8 +8,9 @@ use crate::{ scene::CursorRegion, text_layout::TextLayoutCache, Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, - ElementStateContext, Entity, FontSystem, ModelHandle, ReadModel, ReadView, Scene, - UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle, WeakViewHandle, + ElementStateContext, Entity, FontSystem, ModelHandle, MouseRegion, MouseRegionId, ReadModel, + ReadView, Scene, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle, + WeakViewHandle, }; use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; @@ -24,10 +25,13 @@ pub struct Presenter { pub(crate) rendered_views: HashMap, parents: HashMap, cursor_regions: Vec, + mouse_regions: Vec, font_cache: Arc, text_layout_cache: TextLayoutCache, asset_cache: Arc, last_mouse_moved_event: Option, + hovered_region_id: Option, + clicked_region: Option, titlebar_height: f32, } @@ -45,10 +49,13 @@ impl Presenter { rendered_views: cx.render_views(window_id, titlebar_height), parents: HashMap::new(), cursor_regions: Default::default(), + mouse_regions: Default::default(), font_cache, text_layout_cache, asset_cache, last_mouse_moved_event: None, + hovered_region_id: None, + clicked_region: None, titlebar_height, } } @@ -122,6 +129,7 @@ impl Presenter { ); self.text_layout_cache.finish_frame(); self.cursor_regions = scene.cursor_regions(); + self.mouse_regions = scene.mouse_regions(); if cx.window_is_active(self.window_id) { if let Some(event) = self.last_mouse_moved_event.clone() { @@ -176,7 +184,30 @@ impl Presenter { pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) { if let Some(root_view_id) = cx.root_view_id(self.window_id) { + let mut unhovered_region = None; + let mut hovered_region = None; + let mut clicked_region = None; + match event { + Event::LeftMouseDown { position, .. } => { + for region in self.mouse_regions.iter().rev() { + if region.bounds.contains_point(position) { + self.clicked_region = Some(region.clone()); + break; + } + } + } + Event::LeftMouseUp { + position, + click_count, + .. + } => { + if let Some(region) = self.clicked_region.take() { + if region.bounds.contains_point(position) { + clicked_region = Some((region, position, click_count)); + } + } + } Event::MouseMoved { position, left_mouse_down, @@ -192,6 +223,18 @@ impl Presenter { } } cx.platform().set_cursor_style(style_to_assign); + + for region in self.mouse_regions.iter().rev() { + if region.bounds.contains_point(position) { + if hovered_region.is_none() { + hovered_region = Some(region.clone()); + } + } else { + if self.hovered_region_id == Some(region.id()) { + unhovered_region = Some(region.clone()) + } + } + } } } Event::LeftMouseDragged { position } => { @@ -203,7 +246,33 @@ impl Presenter { _ => {} } + self.hovered_region_id = hovered_region.as_ref().map(MouseRegion::id); + let mut event_cx = self.build_event_context(cx); + if let Some(unhovered_region) = unhovered_region { + if let Some(hover_callback) = unhovered_region.hover { + event_cx.with_current_view(unhovered_region.view_id, |event_cx| { + hover_callback(false, event_cx) + }) + } + } + + if let Some(hovered_region) = hovered_region { + if let Some(hover_callback) = hovered_region.hover { + event_cx.with_current_view(hovered_region.view_id, |event_cx| { + hover_callback(true, event_cx) + }) + } + } + + if let Some((clicked_region, position, click_count)) = clicked_region { + if let Some(click_callback) = clicked_region.click { + event_cx.with_current_view(clicked_region.view_id, |event_cx| { + click_callback(position, click_count, event_cx) + }) + } + } + event_cx.dispatch_event(root_view_id, &event); let invalidated_views = event_cx.invalidated_views; @@ -379,9 +448,8 @@ pub struct EventContext<'a> { impl<'a> EventContext<'a> { fn dispatch_event(&mut self, view_id: usize, event: &Event) -> bool { if let Some(mut element) = self.rendered_views.remove(&view_id) { - self.view_stack.push(view_id); - let result = element.dispatch_event(event, self); - self.view_stack.pop(); + let result = + self.with_current_view(view_id, |this| element.dispatch_event(event, this)); self.rendered_views.insert(view_id, element); result } else { @@ -389,6 +457,16 @@ impl<'a> EventContext<'a> { } } + fn with_current_view(&mut self, view_id: usize, f: F) -> T + where + F: FnOnce(&mut Self) -> T, + { + self.view_stack.push(view_id); + let result = f(self); + self.view_stack.pop(); + result + } + pub fn dispatch_any_action(&mut self, action: Box) { self.dispatched_actions.push(DispatchDirective { path: self.view_stack.clone(), diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 0f10256cb0..003d9b066b 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -1,6 +1,6 @@ use serde::Deserialize; use serde_json::json; -use std::{borrow::Cow, sync::Arc}; +use std::{any::TypeId, borrow::Cow, rc::Rc, sync::Arc}; use crate::{ color::Color, @@ -8,7 +8,7 @@ use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::ToJson, platform::CursorStyle, - ImageData, + EventContext, ImageData, }; pub struct Scene { @@ -34,6 +34,7 @@ pub struct Layer { icons: Vec, paths: Vec, cursor_regions: Vec, + mouse_regions: Vec, } #[derive(Copy, Clone)] @@ -42,6 +43,23 @@ pub struct CursorRegion { pub style: CursorStyle, } +#[derive(Clone)] +pub struct MouseRegion { + pub view_id: usize, + pub tag: TypeId, + pub region_id: usize, + pub bounds: RectF, + pub hover: Option>, + pub click: Option>, +} + +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct MouseRegionId { + pub view_id: usize, + pub tag: TypeId, + pub region_id: usize, +} + #[derive(Default, Debug)] pub struct Quad { pub bounds: RectF, @@ -188,6 +206,13 @@ impl Scene { .collect() } + pub fn mouse_regions(&self) -> Vec { + self.layers() + .flat_map(|layer| &layer.mouse_regions) + .cloned() + .collect() + } + pub fn push_stacking_context(&mut self, clip_bounds: Option) { self.active_stacking_context_stack .push(self.stacking_contexts.len()); @@ -305,6 +330,7 @@ impl Layer { icons: Default::default(), paths: Default::default(), cursor_regions: Default::default(), + mouse_regions: Default::default(), } } @@ -333,6 +359,17 @@ impl Layer { } } + fn push_mouse_region(&mut self, region: MouseRegion) { + if let Some(bounds) = region + .bounds + .intersection(self.clip_bounds.unwrap_or(region.bounds)) + { + if can_draw(bounds) { + self.mouse_regions.push(region); + } + } + } + fn push_underline(&mut self, underline: Underline) { if underline.width > 0. { self.underlines.push(underline); @@ -493,6 +530,16 @@ impl ToJson for Border { } } +impl MouseRegion { + pub fn id(&self) -> MouseRegionId { + MouseRegionId { + view_id: self.view_id, + tag: self.tag, + region_id: self.region_id, + } + } +} + fn can_draw(bounds: RectF) -> bool { let size = bounds.size(); size.x() > 0. && size.y() > 0.