diff --git a/Cargo.lock b/Cargo.lock index 513862d2ce832f8a35c874fab10f61e6bdafc4ac..68221f37bd0f2d6681d8f920b44e565769c4b768 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1571,6 +1571,14 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "drag_and_drop" +version = "0.1.0" +dependencies = [ + "collections", + "gpui", +] + [[package]] name = "dwrote" version = "0.11.0" @@ -6935,6 +6943,7 @@ dependencies = [ "clock", "collections", "context_menu", + "drag_and_drop", "futures", "gpui", "language", diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index d97810f106be04d2d52443094d7fc395ae15f025..fde304cd358414c6ac83e91a0ddf6cbde21c524e 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -566,7 +566,7 @@ impl ContactsPanel { button .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, cx| { - let project = project_handle.upgrade(cx.deref_mut()); + let project = project_handle.upgrade(cx.app); cx.dispatch_action(ToggleProjectOnline { project }) }) .with_tooltip::( diff --git a/crates/drag_and_drop/Cargo.toml b/crates/drag_and_drop/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..2fd8ce27b8b48af1ce241aa98076c329a4e39c93 --- /dev/null +++ b/crates/drag_and_drop/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "drag_and_drop" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/drag_and_drop.rs" +doctest = false + +[dependencies] +collections = { path = "../collections" } +gpui = { path = "../gpui" } + +[dev-dependencies] +gpui = { path = "../gpui", features = ["test-support"] } \ No newline at end of file diff --git a/crates/drag_and_drop/src/drag_and_drop.rs b/crates/drag_and_drop/src/drag_and_drop.rs new file mode 100644 index 0000000000000000000000000000000000000000..28e30f97a73ff88d43d26f0bb7088bd5cff5d3bc --- /dev/null +++ b/crates/drag_and_drop/src/drag_and_drop.rs @@ -0,0 +1,154 @@ +use std::{any::Any, rc::Rc}; + +use gpui::{ + elements::{Container, MouseEventHandler}, + geometry::vector::Vector2F, + scene::DragRegionEvent, + Element, ElementBox, EventContext, MouseButton, RenderContext, View, ViewContext, + WeakViewHandle, +}; + +struct State { + position: Vector2F, + region_offset: Vector2F, + payload: Rc, + render: Rc, &mut RenderContext) -> ElementBox>, +} + +impl Clone for State { + fn clone(&self) -> Self { + Self { + position: self.position.clone(), + region_offset: self.region_offset.clone(), + payload: self.payload.clone(), + render: self.render.clone(), + } + } +} + +pub struct DragAndDrop { + parent: WeakViewHandle, + currently_dragged: Option>, +} + +impl DragAndDrop { + pub fn new(parent: WeakViewHandle, cx: &mut ViewContext) -> Self { + // TODO: Figure out if detaching here would result in a memory leak + cx.observe_global::(|cx| { + if let Some(parent) = cx.global::().parent.upgrade(cx) { + parent.update(cx, |_, cx| cx.notify()) + } + }) + .detach(); + + Self { + parent, + currently_dragged: None, + } + } + + pub fn currently_dragged(&self) -> Option<(Vector2F, Rc)> { + self.currently_dragged.as_ref().and_then( + |State { + position, payload, .. + }| { + payload + .clone() + .downcast::() + .ok() + .map(|payload| (position.clone(), payload)) + }, + ) + } + + pub fn dragging( + event: DragRegionEvent, + payload: Rc, + cx: &mut EventContext, + render: Rc) -> ElementBox>, + ) { + cx.update_global::(|this, cx| { + let region_offset = if let Some(previous_state) = this.currently_dragged.as_ref() { + previous_state.region_offset + } else { + event.region.origin() - event.prev_mouse_position + }; + + this.currently_dragged = Some(State { + region_offset, + position: event.position, + payload, + render: Rc::new(move |payload, cx| { + render(payload.downcast_ref::().unwrap(), cx) + }), + }); + + if let Some(parent) = this.parent.upgrade(cx) { + parent.update(cx, |_, cx| cx.notify()) + } + }); + } + + pub fn render(cx: &mut RenderContext) -> Option { + let currently_dragged = cx.global::().currently_dragged.clone(); + + currently_dragged.map( + |State { + region_offset, + position, + payload, + render, + }| { + let position = position + region_offset; + + MouseEventHandler::new::(0, cx, |_, cx| { + Container::new(render(payload, cx)) + .with_margin_left(position.x()) + .with_margin_top(position.y()) + .aligned() + .top() + .left() + .boxed() + }) + .on_up(MouseButton::Left, |_, cx| { + cx.defer(|cx| { + cx.update_global::(|this, _| this.currently_dragged.take()); + }); + cx.propogate_event(); + }) + // Don't block hover events or invalidations + .with_hoverable(false) + .boxed() + }, + ) + } +} + +pub trait Draggable { + fn as_draggable( + self, + payload: P, + render: impl 'static + Fn(&P, &mut RenderContext) -> ElementBox, + ) -> Self + where + Self: Sized; +} + +impl Draggable for MouseEventHandler { + fn as_draggable( + self, + payload: P, + render: impl 'static + Fn(&P, &mut RenderContext) -> ElementBox, + ) -> Self + where + Self: Sized, + { + let payload = Rc::new(payload); + let render = Rc::new(render); + self.on_drag(MouseButton::Left, move |e, cx| { + let payload = payload.clone(); + let render = render.clone(); + DragAndDrop::::dragging(e, payload, cx, render) + }) + } +} diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 6da4111a76841c14d31a827404376d88f6b61eaf..f16dfd18ad144af4bdaef7994d134a1308bf6dd6 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -9,7 +9,7 @@ use crate::{ platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions}, presenter::Presenter, util::post_inc, - AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseRegionId, + AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton, MouseRegionId, PathPromptOptions, TextLayoutCache, }; pub use action::*; @@ -490,6 +490,7 @@ impl TestAppContext { keystroke: keystroke.clone(), is_held, }), + false, cx, ) { return true; @@ -576,8 +577,7 @@ impl TestAppContext { view_type: PhantomData, titlebar_height: 0., hovered_region_ids: Default::default(), - clicked_region_id: None, - right_clicked_region_id: None, + clicked_region_ids: None, refreshing: false, }; f(view, &mut render_cx) @@ -1285,8 +1285,7 @@ impl MutableAppContext { view_id, titlebar_height, hovered_region_ids: Default::default(), - clicked_region_id: None, - right_clicked_region_id: None, + clicked_region_ids: None, refreshing: false, }) .unwrap(), @@ -1970,7 +1969,7 @@ impl MutableAppContext { } } - presenter.borrow_mut().dispatch_event(event, cx) + presenter.borrow_mut().dispatch_event(event, false, cx) } else { false } @@ -4029,8 +4028,7 @@ pub struct RenderParams { pub view_id: usize, pub titlebar_height: f32, pub hovered_region_ids: HashSet, - pub clicked_region_id: Option, - pub right_clicked_region_id: Option, + pub clicked_region_ids: Option<(Vec, MouseButton)>, pub refreshing: bool, } @@ -4039,8 +4037,7 @@ pub struct RenderContext<'a, T: View> { pub(crate) view_id: usize, pub(crate) view_type: PhantomData, pub(crate) hovered_region_ids: HashSet, - pub(crate) clicked_region_id: Option, - pub(crate) right_clicked_region_id: Option, + pub(crate) clicked_region_ids: Option<(Vec, MouseButton)>, pub app: &'a mut MutableAppContext, pub titlebar_height: f32, pub refreshing: bool, @@ -4049,8 +4046,7 @@ pub struct RenderContext<'a, T: View> { #[derive(Clone, Copy, Default)] pub struct MouseState { pub hovered: bool, - pub clicked: bool, - pub right_clicked: bool, + pub clicked: Option, } impl<'a, V: View> RenderContext<'a, V> { @@ -4062,8 +4058,7 @@ impl<'a, V: View> RenderContext<'a, V> { view_type: PhantomData, titlebar_height: params.titlebar_height, hovered_region_ids: params.hovered_region_ids.clone(), - clicked_region_id: params.clicked_region_id, - right_clicked_region_id: params.right_clicked_region_id, + clicked_region_ids: params.clicked_region_ids.clone(), refreshing: params.refreshing, } } @@ -4087,8 +4082,13 @@ impl<'a, V: View> RenderContext<'a, V> { }; MouseState { hovered: self.hovered_region_ids.contains(®ion_id), - clicked: self.clicked_region_id == Some(region_id), - right_clicked: self.right_clicked_region_id == Some(region_id), + clicked: self.clicked_region_ids.as_ref().and_then(|(ids, button)| { + if ids.contains(®ion_id) { + Some(*button) + } else { + None + } + }), } } @@ -6041,6 +6041,7 @@ mod tests { cmd: false, click_count: 1, }), + false, cx, ); assert_eq!(mouse_down_count.load(SeqCst), 1); diff --git a/crates/gpui/src/elements/container.rs b/crates/gpui/src/elements/container.rs index a581e60b74e933f4d0720e42f2c9b21fb499a985..326efdfd19f82205bb5b77356dbbdc1b0647b8ff 100644 --- a/crates/gpui/src/elements/container.rs +++ b/crates/gpui/src/elements/container.rs @@ -24,6 +24,8 @@ pub struct ContainerStyle { pub padding: Padding, #[serde(rename = "background")] pub background_color: Option, + #[serde(rename = "overlay")] + pub overlay_color: Option, #[serde(default)] pub border: Border, #[serde(default)] @@ -104,6 +106,11 @@ impl Container { self } + pub fn with_padding_top(mut self, padding: f32) -> Self { + self.style.padding.top = padding; + self + } + pub fn with_padding_bottom(mut self, padding: f32) -> Self { self.style.padding.bottom = padding; self @@ -114,6 +121,11 @@ impl Container { self } + pub fn with_overlay_color(mut self, color: Color) -> Self { + self.style.overlay_color = Some(color); + self + } + pub fn with_border(mut self, border: Border) -> Self { self.style.border = border; self @@ -240,7 +252,7 @@ impl Element for Container { cx.scene.push_layer(None); cx.scene.push_quad(Quad { bounds: quad_bounds, - background: Default::default(), + background: self.style.overlay_color, border: self.style.border, corner_radius: self.style.corner_radius, }); @@ -259,6 +271,17 @@ impl Element for Container { self.style.border.top_width(), ); self.child.paint(child_origin, visible_bounds, cx); + + if self.style.overlay_color.is_some() { + cx.scene.push_layer(None); + cx.scene.push_quad(Quad { + bounds: quad_bounds, + background: self.style.overlay_color, + border: Default::default(), + corner_radius: 0., + }); + cx.scene.pop_layer(); + } } } diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index d3c15c4e2b73bd167e450020258581173733e745..df480005b58151f978e4fb370f413aff163c7e03 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -699,7 +699,7 @@ mod tests { 40., vec2f(0., -54.), true, - &mut presenter.build_event_context(cx), + &mut presenter.build_event_context(&mut Default::default(), cx), ); let (_, logical_scroll_top) = list.layout( constraint, @@ -808,7 +808,7 @@ mod tests { height, delta, true, - &mut presenter.build_event_context(cx), + &mut presenter.build_event_context(&mut Default::default(), cx), ); } 30..=34 => { diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 7ad43c47defc320ba7654800a676cb765865940c..9e5465e91a28da9d053757531b55794db9534efb 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -5,10 +5,12 @@ use crate::{ vector::{vec2f, Vector2F}, }, platform::CursorStyle, - scene::{CursorRegion, HandlerSet}, + scene::{ + ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragRegionEvent, + HandlerSet, HoverRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent, + }, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MeasurementContext, - MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion, MouseState, PaintContext, - RenderContext, SizeConstraint, View, + MouseButton, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View, }; use serde_json::json; use std::{any::TypeId, ops::Range}; @@ -18,6 +20,7 @@ pub struct MouseEventHandler { discriminant: (TypeId, usize), cursor_style: Option, handlers: HandlerSet, + hoverable: bool, padding: Padding, } @@ -33,6 +36,7 @@ impl MouseEventHandler { cursor_style: None, discriminant: (TypeId::of::(), id), handlers: Default::default(), + hoverable: true, padding: Default::default(), } } @@ -42,19 +46,41 @@ impl MouseEventHandler { self } + pub fn capture_all(mut self) -> Self { + self.handlers = HandlerSet::capture_all(); + self + } + + pub fn on_move( + mut self, + handler: impl Fn(MoveRegionEvent, &mut EventContext) + 'static, + ) -> Self { + self.handlers = self.handlers.on_move(handler); + self + } + pub fn on_down( mut self, button: MouseButton, - handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static, + handler: impl Fn(DownRegionEvent, &mut EventContext) + 'static, ) -> Self { self.handlers = self.handlers.on_down(button, handler); self } + pub fn on_up( + mut self, + button: MouseButton, + handler: impl Fn(UpRegionEvent, &mut EventContext) + 'static, + ) -> Self { + self.handlers = self.handlers.on_up(button, handler); + self + } + pub fn on_click( mut self, button: MouseButton, - handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static, + handler: impl Fn(ClickRegionEvent, &mut EventContext) + 'static, ) -> Self { self.handlers = self.handlers.on_click(button, handler); self @@ -63,16 +89,25 @@ impl MouseEventHandler { pub fn on_down_out( mut self, button: MouseButton, - handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static, + handler: impl Fn(DownOutRegionEvent, &mut EventContext) + 'static, ) -> Self { self.handlers = self.handlers.on_down_out(button, handler); self } + pub fn on_up_out( + mut self, + button: MouseButton, + handler: impl Fn(UpOutRegionEvent, &mut EventContext) + 'static, + ) -> Self { + self.handlers = self.handlers.on_up_out(button, handler); + self + } + pub fn on_drag( mut self, button: MouseButton, - handler: impl Fn(Vector2F, MouseMovedEvent, &mut EventContext) + 'static, + handler: impl Fn(DragRegionEvent, &mut EventContext) + 'static, ) -> Self { self.handlers = self.handlers.on_drag(button, handler); self @@ -80,12 +115,17 @@ impl MouseEventHandler { pub fn on_hover( mut self, - handler: impl Fn(bool, MouseMovedEvent, &mut EventContext) + 'static, + handler: impl Fn(HoverRegionEvent, &mut EventContext) + 'static, ) -> Self { self.handlers = self.handlers.on_hover(handler); self } + pub fn with_hoverable(mut self, is_hoverable: bool) -> Self { + self.hoverable = is_hoverable; + self + } + pub fn with_padding(mut self, padding: Padding) -> Self { self.padding = padding; self @@ -127,12 +167,15 @@ impl Element for MouseEventHandler { }); } - cx.scene.push_mouse_region(MouseRegion::from_handlers( - cx.current_view_id(), - Some(self.discriminant), - hit_bounds, - self.handlers.clone(), - )); + cx.scene.push_mouse_region( + MouseRegion::from_handlers( + cx.current_view_id(), + Some(self.discriminant), + hit_bounds, + self.handlers.clone(), + ) + .with_hoverable(self.hoverable), + ); self.child.paint(bounds.origin(), visible_bounds, cx); } diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 68605fb6b359195395634af3dc99598d8dbba6b6..26f8a450db644dd348da6ce0ea2e60d776ee476d 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -7,8 +7,8 @@ use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::json, presenter::MeasurementContext, - Action, Axis, ElementStateHandle, LayoutContext, MouseMovedEvent, PaintContext, RenderContext, - SizeConstraint, Task, View, + Action, Axis, ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint, + Task, View, }; use serde::Deserialize; use std::{ @@ -93,10 +93,11 @@ impl Tooltip { }; let child = MouseEventHandler::new::, _, _>(id, cx, |_, _| child) - .on_hover(move |hover, MouseMovedEvent { position, .. }, cx| { + .on_hover(move |e, cx| { + let position = e.position; let window_id = cx.window_id(); if let Some(view_id) = cx.view_id() { - if hover { + if e.started { if !state.visible.get() { state.position.set(position); diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 6836d157fe117ea22d1caf26e54922d2033e0347..4b96a3bb47d5d20828c4a230994da952e97239aa 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -6,12 +6,15 @@ use crate::{ json::{self, ToJson}, keymap::Keystroke, platform::{CursorStyle, Event}, - scene::{CursorRegion, MouseRegionEvent}, + scene::{ + ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragRegionEvent, + HoverRegionEvent, MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent, + }, text_layout::TextLayoutCache, Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, Entity, - FontSystem, ModelHandle, MouseButtonEvent, MouseMovedEvent, MouseRegion, MouseRegionId, - ParentId, ReadModel, ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle, - UpgradeViewHandle, View, ViewHandle, WeakModelHandle, WeakViewHandle, + FontSystem, ModelHandle, MouseButton, MouseMovedEvent, MouseRegion, MouseRegionId, ParentId, + ReadModel, ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle, + View, ViewHandle, WeakModelHandle, WeakViewHandle, }; use collections::{HashMap, HashSet}; use pathfinder_geometry::vector::{vec2f, Vector2F}; @@ -31,11 +34,11 @@ pub struct Presenter { font_cache: Arc, text_layout_cache: TextLayoutCache, asset_cache: Arc, - last_mouse_moved_event: Option, + last_mouse_moved_event: Option, hovered_region_ids: HashSet, - clicked_region: Option, - right_clicked_region: Option, - prev_drag_position: Option, + clicked_regions: Vec, + clicked_button: Option, + mouse_position: Vector2F, titlebar_height: f32, } @@ -58,30 +61,13 @@ impl Presenter { asset_cache, last_mouse_moved_event: None, hovered_region_ids: Default::default(), - clicked_region: None, - right_clicked_region: None, - prev_drag_position: None, + clicked_regions: Vec::new(), + clicked_button: None, + mouse_position: vec2f(0., 0.), titlebar_height, } } - // pub fn dispatch_path(&self, app: &AppContext) -> Vec { - // let mut path = Vec::new(); - // if let Some(view_id) = app.focused_view_id(self.window_id) { - // self.compute_dispatch_path_from(view_id, &mut path) - // } - // path - // } - - // pub(crate) fn compute_dispatch_path_from(&self, mut view_id: usize, path: &mut Vec) { - // path.push(view_id); - // while let Some(parent_id) = self.parents.get(&view_id).copied() { - // path.push(parent_id); - // view_id = parent_id; - // } - // path.reverse(); - // } - pub fn invalidate( &mut self, invalidation: &mut WindowInvalidation, @@ -100,11 +86,15 @@ impl Presenter { view_id: *view_id, titlebar_height: self.titlebar_height, hovered_region_ids: self.hovered_region_ids.clone(), - clicked_region_id: self.clicked_region.as_ref().and_then(MouseRegion::id), - right_clicked_region_id: self - .right_clicked_region - .as_ref() - .and_then(MouseRegion::id), + clicked_region_ids: self.clicked_button.map(|button| { + ( + self.clicked_regions + .iter() + .filter_map(MouseRegion::id) + .collect(), + button, + ) + }), refreshing: false, }) .unwrap(), @@ -122,11 +112,15 @@ impl Presenter { view_id: *view_id, titlebar_height: self.titlebar_height, hovered_region_ids: self.hovered_region_ids.clone(), - clicked_region_id: self.clicked_region.as_ref().and_then(MouseRegion::id), - right_clicked_region_id: self - .right_clicked_region - .as_ref() - .and_then(MouseRegion::id), + clicked_region_ids: self.clicked_button.map(|button| { + ( + self.clicked_regions + .iter() + .filter_map(MouseRegion::id) + .collect(), + button, + ) + }), refreshing: true, }) .unwrap(); @@ -157,12 +151,7 @@ impl Presenter { if cx.window_is_active(self.window_id) { if let Some(event) = self.last_mouse_moved_event.clone() { - let mut invalidated_views = Vec::new(); - self.handle_hover_events(&event, &mut invalidated_views, cx); - - for view_id in invalidated_views { - cx.notify_view(self.window_id, view_id); - } + self.dispatch_event(event, true, cx); } } } else { @@ -195,8 +184,15 @@ impl Presenter { view_stack: Vec::new(), refreshing, hovered_region_ids: self.hovered_region_ids.clone(), - clicked_region_id: self.clicked_region.as_ref().and_then(MouseRegion::id), - right_clicked_region_id: self.right_clicked_region.as_ref().and_then(MouseRegion::id), + clicked_region_ids: self.clicked_button.map(|button| { + ( + self.clicked_regions + .iter() + .filter_map(MouseRegion::id) + .collect(), + button, + ) + }), titlebar_height: self.titlebar_height, window_size, app: cx, @@ -231,246 +227,249 @@ impl Presenter { }) } - pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) -> bool { + pub fn dispatch_event( + &mut self, + event: Event, + event_reused: bool, + cx: &mut MutableAppContext, + ) -> bool { if let Some(root_view_id) = cx.root_view_id(self.window_id) { - let mut invalidated_views = Vec::new(); - let mut mouse_down_out_handlers = Vec::new(); - let mut mouse_moved_region = None; - let mut mouse_down_region = None; - let mut mouse_up_region = None; - let mut clicked_region = None; - let mut dragged_region = None; + let mut events_to_send = Vec::new(); + // 1. Allocate the correct set of GPUI events generated from the platform events + // -> These are usually small: [Mouse Down] or [Mouse up, Click] or [Mouse Moved, Mouse Dragged?] + // -> Also moves around mouse related state match &event { - Event::MouseDown( - e @ MouseButtonEvent { - position, button, .. - }, - ) => { - let mut hit = false; - for (region, _) in self.mouse_regions.iter().rev() { - if region.bounds.contains_point(*position) { - if !hit { - hit = true; - invalidated_views.push(region.view_id); - mouse_down_region = - Some((region.clone(), MouseRegionEvent::Down(e.clone()))); - self.clicked_region = Some(region.clone()); - self.prev_drag_position = Some(*position); - } - } else if let Some(handler) = region - .handlers - .get(&(MouseRegionEvent::down_out_disc(), Some(*button))) - { - mouse_down_out_handlers.push(( - handler, - region.view_id, - MouseRegionEvent::DownOut(e.clone()), - )); - } + Event::MouseDown(e) => { + // Click events are weird because they can be fired after a drag event. + // MDN says that browsers handle this by starting from 'the most + // specific ancestor element that contained both [positions]' + // So we need to store the overlapping regions on mouse down. + + // If there is already clicked_button stored, don't replace it. + if self.clicked_button.is_none() { + self.clicked_regions = self + .mouse_regions + .iter() + .filter_map(|(region, _)| { + region + .bounds + .contains_point(e.position) + .then(|| region.clone()) + }) + .collect(); + self.clicked_button = Some(e.button); } - } - &Event::MouseUp( - e @ MouseButtonEvent { - position, button, .. + events_to_send.push(MouseRegionEvent::Down(DownRegionEvent { + region: Default::default(), + platform_event: e.clone(), + })); + events_to_send.push(MouseRegionEvent::DownOut(DownOutRegionEvent { + region: Default::default(), + platform_event: e.clone(), + })); + } + Event::MouseUp(e) => { + // NOTE: The order of event pushes is important! MouseUp events MUST be fired + // before click events, and so the UpRegionEvent events need to be pushed before + // ClickRegionEvents + events_to_send.push(MouseRegionEvent::Up(UpRegionEvent { + region: Default::default(), + platform_event: e.clone(), + })); + events_to_send.push(MouseRegionEvent::UpOut(UpOutRegionEvent { + region: Default::default(), + platform_event: e.clone(), + })); + events_to_send.push(MouseRegionEvent::Click(ClickRegionEvent { + region: Default::default(), + platform_event: e.clone(), + })); + } + Event::MouseMoved( + e @ MouseMovedEvent { + position, + pressed_button, + .. }, ) => { - self.prev_drag_position.take(); - if let Some(region) = self.clicked_region.take() { - invalidated_views.push(region.view_id); - if region.bounds.contains_point(position) { - clicked_region = Some((region, MouseRegionEvent::Click(e.clone()))); - } - } - - for (region, _) in self.mouse_regions.iter().rev() { - if region.bounds.contains_point(position) { - invalidated_views.push(region.view_id); - mouse_up_region = - Some((region.clone(), MouseRegionEvent::Up(e.clone()))); + let mut style_to_assign = CursorStyle::Arrow; + for region in self.cursor_regions.iter().rev() { + if region.bounds.contains_point(*position) { + style_to_assign = region.style; break; } } - - if let Some(moved) = &mut self.last_mouse_moved_event { - if moved.pressed_button == Some(button) { - moved.pressed_button = None; + cx.platform().set_cursor_style(style_to_assign); + + if !event_reused { + if pressed_button.is_some() { + events_to_send.push(MouseRegionEvent::Drag(DragRegionEvent { + region: Default::default(), + prev_mouse_position: self.mouse_position, + platform_event: e.clone(), + })); } - } - } - - Event::MouseMoved(e @ MouseMovedEvent { position, .. }) => { - if let Some((clicked_region, prev_drag_position)) = self - .clicked_region - .as_ref() - .zip(self.prev_drag_position.as_mut()) - { - dragged_region = Some(( - clicked_region.clone(), - MouseRegionEvent::Drag(*prev_drag_position, *e), - )); - *prev_drag_position = *position; + events_to_send.push(MouseRegionEvent::Move(MoveRegionEvent { + region: Default::default(), + platform_event: e.clone(), + })); } - for (region, _) in self.mouse_regions.iter().rev() { - if region.bounds.contains_point(*position) { - invalidated_views.push(region.view_id); - mouse_moved_region = - Some((region.clone(), MouseRegionEvent::Move(e.clone()))); - break; - } - } + events_to_send.push(MouseRegionEvent::Hover(HoverRegionEvent { + region: Default::default(), + platform_event: e.clone(), + started: false, + })); - self.last_mouse_moved_event = Some(e.clone()); + self.last_mouse_moved_event = Some(event.clone()); } _ => {} } - let (mut handled, mut event_cx) = if let Event::MouseMoved(e) = &event { - self.handle_hover_events(e, &mut invalidated_views, cx) - } else { - (false, self.build_event_context(cx)) - }; - - for (handler, view_id, region_event) in mouse_down_out_handlers { - event_cx.with_current_view(view_id, |event_cx| handler(region_event, event_cx)) + if let Some(position) = event.position() { + self.mouse_position = position; } - if let Some((mouse_down_region, region_event)) = mouse_down_region { - handled = true; - if let Some(mouse_down_callback) = - mouse_down_region.handlers.get(®ion_event.handler_key()) - { - event_cx.with_current_view(mouse_down_region.view_id, |event_cx| { - mouse_down_callback(region_event, event_cx); - }) - } - } + let mut invalidated_views: HashSet = Default::default(); + let mut any_event_handled = false; + // 2. Process the raw mouse events into region events + for mut region_event in events_to_send { + let mut valid_regions = Vec::new(); + + // GPUI elements are arranged by depth but sibling elements can register overlapping + // mouse regions. As such, hover events are only fired on overlapping elements which + // are at the same depth as the topmost element which overlaps with the mouse. + + match ®ion_event { + MouseRegionEvent::Hover(_) => { + let mut top_most_depth = None; + let mouse_position = self.mouse_position.clone(); + for (region, depth) in self.mouse_regions.iter().rev() { + // Allow mouse regions to appear transparent to hovers + if !region.hoverable { + continue; + } - if let Some((move_moved_region, region_event)) = mouse_moved_region { - handled = true; - if let Some(mouse_moved_callback) = - move_moved_region.handlers.get(®ion_event.handler_key()) - { - event_cx.with_current_view(move_moved_region.view_id, |event_cx| { - mouse_moved_callback(region_event, event_cx); - }) - } - } + let contains_mouse = region.bounds.contains_point(mouse_position); - if let Some((mouse_up_region, region_event)) = mouse_up_region { - handled = true; - if let Some(mouse_up_callback) = - mouse_up_region.handlers.get(®ion_event.handler_key()) - { - event_cx.with_current_view(mouse_up_region.view_id, |event_cx| { - mouse_up_callback(region_event, event_cx); - }) - } - } + if contains_mouse && top_most_depth.is_none() { + top_most_depth = Some(depth); + } - if let Some((clicked_region, region_event)) = clicked_region { - handled = true; - if let Some(click_callback) = - clicked_region.handlers.get(®ion_event.handler_key()) - { - event_cx.with_current_view(clicked_region.view_id, |event_cx| { - click_callback(region_event, event_cx); - }) - } - } + if let Some(region_id) = region.id() { + // This unwrap relies on short circuiting boolean expressions + // The right side of the && is only executed when contains_mouse + // is true, and we know above that when contains_mouse is true + // top_most_depth is set + if contains_mouse && depth == top_most_depth.unwrap() { + //Ensure that hover entrance events aren't sent twice + if self.hovered_region_ids.insert(region_id) { + valid_regions.push(region.clone()); + invalidated_views.insert(region.view_id); + } + } else { + // Ensure that hover exit events aren't sent twice + if self.hovered_region_ids.remove(®ion_id) { + valid_regions.push(region.clone()); + invalidated_views.insert(region.view_id); + } + } + } + } + } + MouseRegionEvent::Click(e) => { + if e.button == self.clicked_button.unwrap() { + // Clear clicked regions and clicked button + let clicked_regions = + std::mem::replace(&mut self.clicked_regions, Vec::new()); + self.clicked_button = None; + + // Find regions which still overlap with the mouse since the last MouseDown happened + for clicked_region in clicked_regions.into_iter().rev() { + if clicked_region.bounds.contains_point(e.position) { + valid_regions.push(clicked_region); + } + } + } + } + MouseRegionEvent::Drag(_) => { + for clicked_region in self.clicked_regions.iter().rev() { + valid_regions.push(clicked_region.clone()); + } + } - if let Some((dragged_region, region_event)) = dragged_region { - handled = true; - if let Some(drag_callback) = - dragged_region.handlers.get(®ion_event.handler_key()) - { - event_cx.with_current_view(dragged_region.view_id, |event_cx| { - drag_callback(region_event, event_cx); - }) + MouseRegionEvent::UpOut(_) | MouseRegionEvent::DownOut(_) => { + for (mouse_region, _) in self.mouse_regions.iter().rev() { + // NOT contains + if !mouse_region.bounds.contains_point(self.mouse_position) { + valid_regions.push(mouse_region.clone()); + } + } + } + _ => { + for (mouse_region, _) in self.mouse_regions.iter().rev() { + // Contains + if mouse_region.bounds.contains_point(self.mouse_position) { + valid_regions.push(mouse_region.clone()); + } + } + } } - } - if !handled { - handled = event_cx.dispatch_event(root_view_id, &event); - } - - invalidated_views.extend(event_cx.invalidated_views); + //3. Fire region events + let hovered_region_ids = self.hovered_region_ids.clone(); + for valid_region in valid_regions.into_iter() { + region_event.set_region(valid_region.bounds); + if let MouseRegionEvent::Hover(e) = &mut region_event { + e.started = valid_region + .id() + .map(|region_id| hovered_region_ids.contains(®ion_id)) + .unwrap_or(false) + } - for view_id in invalidated_views { - cx.notify_view(self.window_id, view_id); - } + if let Some(callback) = valid_region.handlers.get(®ion_event.handler_key()) { + invalidated_views.insert(valid_region.view_id); - handled - } else { - false - } - } + let mut event_cx = self.build_event_context(&mut invalidated_views, cx); + event_cx.handled = true; + event_cx.with_current_view(valid_region.view_id, { + let region_event = region_event.clone(); + |cx| { + callback(region_event, cx); + } + }); - fn handle_hover_events<'a>( - &'a mut self, - e @ MouseMovedEvent { - position, - pressed_button, - .. - }: &MouseMovedEvent, - invalidated_views: &mut Vec, - cx: &'a mut MutableAppContext, - ) -> (bool, EventContext<'a>) { - let mut hover_regions = Vec::new(); - - if pressed_button.is_none() { - let mut style_to_assign = CursorStyle::Arrow; - for region in self.cursor_regions.iter().rev() { - if region.bounds.contains_point(*position) { - style_to_assign = region.style; - break; - } - } - cx.platform().set_cursor_style(style_to_assign); - - let mut hover_depth = None; - for (region, depth) in self.mouse_regions.iter().rev() { - if region.bounds.contains_point(*position) - && hover_depth.map_or(true, |hover_depth| hover_depth == *depth) - { - hover_depth = Some(*depth); - if let Some(region_id) = region.id() { - if !self.hovered_region_ids.contains(®ion_id) { - invalidated_views.push(region.view_id); - hover_regions.push((region.clone(), MouseRegionEvent::Hover(true, *e))); - self.hovered_region_ids.insert(region_id); + any_event_handled = any_event_handled || event_cx.handled; + // For bubbling events, if the event was handled, don't continue dispatching + // This only makes sense for local events. + if event_cx.handled && region_event.is_capturable() { + break; } } - } else if let Some(region_id) = region.id() { - if self.hovered_region_ids.contains(®ion_id) { - invalidated_views.push(region.view_id); - hover_regions.push((region.clone(), MouseRegionEvent::Hover(false, *e))); - self.hovered_region_ids.remove(®ion_id); - } } } - } - let mut event_cx = self.build_event_context(cx); - let mut handled = false; + if !any_event_handled && !event_reused { + let mut event_cx = self.build_event_context(&mut invalidated_views, cx); + any_event_handled = event_cx.dispatch_event(root_view_id, &event); + } - for (hover_region, region_event) in hover_regions { - handled = true; - if let Some(hover_callback) = hover_region.handlers.get(®ion_event.handler_key()) { - event_cx.with_current_view(hover_region.view_id, |event_cx| { - hover_callback(region_event, event_cx); - }) + for view_id in invalidated_views { + cx.notify_view(self.window_id, view_id); } - } - (handled, event_cx) + any_event_handled + } else { + false + } } pub fn build_event_context<'a>( &'a mut self, + invalidated_views: &'a mut HashSet, cx: &'a mut MutableAppContext, ) -> EventContext<'a> { EventContext { @@ -478,8 +477,9 @@ impl Presenter { font_cache: &self.font_cache, text_layout_cache: &self.text_layout_cache, view_stack: Default::default(), - invalidated_views: Default::default(), + invalidated_views, notify_count: 0, + handled: false, window_id: self.window_id, app: cx, } @@ -514,8 +514,7 @@ pub struct LayoutContext<'a> { pub window_size: Vector2F, titlebar_height: f32, hovered_region_ids: HashSet, - clicked_region_id: Option, - right_clicked_region_id: Option, + clicked_region_ids: Option<(Vec, MouseButton)>, } impl<'a> LayoutContext<'a> { @@ -586,8 +585,7 @@ impl<'a> LayoutContext<'a> { view_type: PhantomData, titlebar_height: self.titlebar_height, hovered_region_ids: self.hovered_region_ids.clone(), - clicked_region_id: self.clicked_region_id, - right_clicked_region_id: self.right_clicked_region_id, + clicked_region_ids: self.clicked_region_ids.clone(), refreshing: self.refreshing, }; f(view, &mut render_cx) @@ -699,7 +697,8 @@ pub struct EventContext<'a> { pub window_id: usize, pub notify_count: usize, view_stack: Vec, - invalidated_views: HashSet, + handled: bool, + invalidated_views: &'a mut HashSet, } impl<'a> EventContext<'a> { @@ -765,6 +764,10 @@ impl<'a> EventContext<'a> { pub fn notify_count(&self) -> usize { self.notify_count } + + pub fn propogate_event(&mut self) { + self.handled = false; + } } impl<'a> Deref for EventContext<'a> { diff --git a/crates/gpui/src/presenter/event_context.rs b/crates/gpui/src/presenter/event_context.rs new file mode 100644 index 0000000000000000000000000000000000000000..d4258a6d1e724b8a9b30e742b6aef31648565c91 --- /dev/null +++ b/crates/gpui/src/presenter/event_context.rs @@ -0,0 +1,100 @@ +use std::ops::{Deref, DerefMut}; + +use collections::{HashMap, HashSet}; + +use crate::{Action, ElementBox, Event, FontCache, MutableAppContext, TextLayoutCache}; + +pub struct EventContext<'a> { + rendered_views: &'a mut HashMap, + pub font_cache: &'a FontCache, + pub text_layout_cache: &'a TextLayoutCache, + pub app: &'a mut MutableAppContext, + pub window_id: usize, + pub notify_count: usize, + view_stack: Vec, + pub(crate) handled: bool, + pub(crate) invalidated_views: HashSet, +} + +impl<'a> EventContext<'a> { + pub(crate) fn dispatch_event(&mut self, view_id: usize, event: &Event) -> bool { + if let Some(mut element) = self.rendered_views.remove(&view_id) { + let result = + self.with_current_view(view_id, |this| element.dispatch_event(event, this)); + self.rendered_views.insert(view_id, element); + result + } else { + false + } + } + + pub(crate) 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 window_id(&self) -> usize { + self.window_id + } + + pub fn view_id(&self) -> Option { + self.view_stack.last().copied() + } + + pub fn is_parent_view_focused(&self) -> bool { + if let Some(parent_view_id) = self.view_stack.last() { + self.app.focused_view_id(self.window_id) == Some(*parent_view_id) + } else { + false + } + } + + pub fn focus_parent_view(&mut self) { + if let Some(parent_view_id) = self.view_stack.last() { + self.app.focus(self.window_id, Some(*parent_view_id)) + } + } + + pub fn dispatch_any_action(&mut self, action: Box) { + self.app + .dispatch_any_action_at(self.window_id, *self.view_stack.last().unwrap(), action) + } + + pub fn dispatch_action(&mut self, action: A) { + self.dispatch_any_action(Box::new(action)); + } + + pub fn notify(&mut self) { + self.notify_count += 1; + if let Some(view_id) = self.view_stack.last() { + self.invalidated_views.insert(*view_id); + } + } + + pub fn notify_count(&self) -> usize { + self.notify_count + } + + pub fn propogate_event(&mut self) { + self.handled = false; + } +} + +impl<'a> Deref for EventContext<'a> { + type Target = MutableAppContext; + + fn deref(&self) -> &Self::Target { + self.app + } +} + +impl<'a> DerefMut for EventContext<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.app + } +} diff --git a/crates/gpui/src/presenter/event_dispatcher.rs b/crates/gpui/src/presenter/event_dispatcher.rs new file mode 100644 index 0000000000000000000000000000000000000000..4c72334910b514a257be8213410d1aa9dbb05b79 --- /dev/null +++ b/crates/gpui/src/presenter/event_dispatcher.rs @@ -0,0 +1,308 @@ +use std::sync::Arc; + +use collections::{HashMap, HashSet}; +use pathfinder_geometry::vector::Vector2F; + +use crate::{ + scene::{ + ClickRegionEvent, DownOutRegionEvent, DownRegionEvent, DragRegionEvent, HoverRegionEvent, + MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent, + }, + CursorRegion, CursorStyle, ElementBox, Event, EventContext, FontCache, MouseButton, + MouseMovedEvent, MouseRegion, MouseRegionId, MutableAppContext, Scene, TextLayoutCache, +}; + +pub struct EventDispatcher { + window_id: usize, + font_cache: Arc, + + last_mouse_moved_event: Option, + cursor_regions: Vec, + mouse_regions: Vec<(MouseRegion, usize)>, + clicked_regions: Vec, + clicked_button: Option, + mouse_position: Vector2F, + hovered_region_ids: HashSet, +} + +impl EventDispatcher { + pub fn new(window_id: usize, font_cache: Arc) -> Self { + Self { + window_id, + font_cache, + + last_mouse_moved_event: Default::default(), + cursor_regions: Default::default(), + mouse_regions: Default::default(), + clicked_regions: Default::default(), + clicked_button: Default::default(), + mouse_position: Default::default(), + hovered_region_ids: Default::default(), + } + } + + pub fn clicked_region_ids(&self) -> Option<(Vec, MouseButton)> { + self.clicked_button.map(|button| { + ( + self.clicked_regions + .iter() + .filter_map(MouseRegion::id) + .collect(), + button, + ) + }) + } + + pub fn hovered_region_ids(&self) -> HashSet { + self.hovered_region_ids.clone() + } + + pub fn update_mouse_regions(&mut self, scene: &Scene) { + self.cursor_regions = scene.cursor_regions(); + self.mouse_regions = scene.mouse_regions(); + } + + pub fn redispatch_mouse_moved_event<'a>(&'a mut self, cx: &mut EventContext<'a>) { + if let Some(event) = self.last_mouse_moved_event.clone() { + self.dispatch_event(event, true, cx); + } + } + + pub fn dispatch_event<'a>( + &'a mut self, + event: Event, + event_reused: bool, + cx: &mut EventContext<'a>, + ) -> bool { + let root_view_id = cx.root_view_id(self.window_id); + if root_view_id.is_none() { + return false; + } + + let root_view_id = root_view_id.unwrap(); + //1. Allocate the correct set of GPUI events generated from the platform events + // -> These are usually small: [Mouse Down] or [Mouse up, Click] or [Mouse Moved, Mouse Dragged?] + // -> Also moves around mouse related state + let events_to_send = self.select_region_events(&event, cx, event_reused); + + // For a given platform event, potentially multiple mouse region events can be created. For a given + // region event, dispatch continues until a mouse region callback fails to propogate (handled is set to true) + // If no region handles any of the produced platform events, we fallback to the old dispatch event style. + let mut invalidated_views: HashSet = Default::default(); + let mut any_event_handled = false; + for mut region_event in events_to_send { + //2. Find mouse regions relevant to each region_event. For example, if the event is click, select + // the clicked_regions that overlap with the mouse position + let valid_regions = self.select_relevant_mouse_regions(®ion_event); + let hovered_region_ids = self.hovered_region_ids.clone(); + + //3. Dispatch region event ot each valid mouse region + for valid_region in valid_regions.into_iter() { + region_event.set_region(valid_region.bounds); + if let MouseRegionEvent::Hover(e) = &mut region_event { + e.started = valid_region + .id() + .map(|region_id| hovered_region_ids.contains(®ion_id)) + .unwrap_or(false) + } + + if let Some(callback) = valid_region.handlers.get(®ion_event.handler_key()) { + if !event_reused { + invalidated_views.insert(valid_region.view_id); + } + + cx.handled = true; + cx.with_current_view(valid_region.view_id, { + let region_event = region_event.clone(); + |cx| { + callback(region_event, cx); + } + }); + + // For bubbling events, if the event was handled, don't continue dispatching + // This only makes sense for local events. + if cx.handled && region_event.is_local() { + break; + } + } + } + + // Keep track if any platform event was handled + any_event_handled = any_event_handled && cx.handled; + } + + if !any_event_handled { + // No platform event was handled, so fall back to old mouse event dispatch style + any_event_handled = cx.dispatch_event(root_view_id, &event); + } + + // Notify any views which have been validated from event callbacks + for view_id in invalidated_views { + cx.notify_view(self.window_id, view_id); + } + + any_event_handled + } + + fn select_region_events( + &mut self, + event: &Event, + cx: &mut MutableAppContext, + event_reused: bool, + ) -> Vec { + let mut events_to_send = Vec::new(); + match event { + Event::MouseDown(e) => { + //Click events are weird because they can be fired after a drag event. + //MDN says that browsers handle this by starting from 'the most + //specific ancestor element that contained both [positions]' + //So we need to store the overlapping regions on mouse down. + self.clicked_regions = self + .mouse_regions + .iter() + .filter_map(|(region, _)| { + region + .bounds + .contains_point(e.position) + .then(|| region.clone()) + }) + .collect(); + self.clicked_button = Some(e.button); + + events_to_send.push(MouseRegionEvent::Down(DownRegionEvent { + region: Default::default(), + platform_event: e.clone(), + })); + events_to_send.push(MouseRegionEvent::DownOut(DownOutRegionEvent { + region: Default::default(), + platform_event: e.clone(), + })); + } + Event::MouseUp(e) => { + //NOTE: The order of event pushes is important! MouseUp events MUST be fired + //before click events, and so the UpRegionEvent events need to be pushed before + //ClickRegionEvents + events_to_send.push(MouseRegionEvent::Up(UpRegionEvent { + region: Default::default(), + platform_event: e.clone(), + })); + events_to_send.push(MouseRegionEvent::UpOut(UpOutRegionEvent { + region: Default::default(), + platform_event: e.clone(), + })); + events_to_send.push(MouseRegionEvent::Click(ClickRegionEvent { + region: Default::default(), + platform_event: e.clone(), + })); + } + Event::MouseMoved( + e @ MouseMovedEvent { + position, + pressed_button, + .. + }, + ) => { + let mut style_to_assign = CursorStyle::Arrow; + for region in self.cursor_regions.iter().rev() { + if region.bounds.contains_point(*position) { + style_to_assign = region.style; + break; + } + } + cx.platform().set_cursor_style(style_to_assign); + + if !event_reused { + if pressed_button.is_some() { + events_to_send.push(MouseRegionEvent::Drag(DragRegionEvent { + region: Default::default(), + prev_mouse_position: self.mouse_position, + platform_event: e.clone(), + })); + } + events_to_send.push(MouseRegionEvent::Move(MoveRegionEvent { + region: Default::default(), + platform_event: e.clone(), + })); + } + + events_to_send.push(MouseRegionEvent::Hover(HoverRegionEvent { + region: Default::default(), + platform_event: e.clone(), + started: false, + })); + + self.last_mouse_moved_event = Some(event.clone()); + } + _ => {} + } + if let Some(position) = event.position() { + self.mouse_position = position; + } + events_to_send + } + + fn select_relevant_mouse_regions( + &mut self, + region_event: &MouseRegionEvent, + ) -> Vec { + let mut valid_regions = Vec::new(); + //GPUI elements are arranged by depth but sibling elements can register overlapping + //mouse regions. As such, hover events are only fired on overlapping elements which + //are at the same depth as the deepest element which overlaps with the mouse. + if let MouseRegionEvent::Hover(_) = *region_event { + let mut top_most_depth = None; + let mouse_position = self.mouse_position.clone(); + for (region, depth) in self.mouse_regions.iter().rev() { + let contains_mouse = region.bounds.contains_point(mouse_position); + + if contains_mouse && top_most_depth.is_none() { + top_most_depth = Some(depth); + } + + if let Some(region_id) = region.id() { + //This unwrap relies on short circuiting boolean expressions + //The right side of the && is only executed when contains_mouse + //is true, and we know above that when contains_mouse is true + //top_most_depth is set + if contains_mouse && depth == top_most_depth.unwrap() { + //Ensure that hover entrance events aren't sent twice + if self.hovered_region_ids.insert(region_id) { + valid_regions.push(region.clone()); + } + } else { + //Ensure that hover exit events aren't sent twice + if self.hovered_region_ids.remove(®ion_id) { + valid_regions.push(region.clone()); + } + } + } + } + } else if let MouseRegionEvent::Click(e) = region_event { + //Clear stored clicked_regions + let clicked_regions = std::mem::replace(&mut self.clicked_regions, Vec::new()); + self.clicked_button = None; + + //Find regions which still overlap with the mouse since the last MouseDown happened + for clicked_region in clicked_regions.into_iter().rev() { + if clicked_region.bounds.contains_point(e.position) { + valid_regions.push(clicked_region); + } + } + } else if region_event.is_local() { + for (mouse_region, _) in self.mouse_regions.iter().rev() { + //Contains + if mouse_region.bounds.contains_point(self.mouse_position) { + valid_regions.push(mouse_region.clone()); + } + } + } else { + for (mouse_region, _) in self.mouse_regions.iter().rev() { + //NOT contains + if !mouse_region.bounds.contains_point(self.mouse_position) { + valid_regions.push(mouse_region.clone()); + } + } + } + valid_regions + } +} diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index a1f12b76d344e9ce10fe9164d5ec3145581131c8..086af5f64d1e93170a55ec8f0733f860cc098c1c 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -1,4 +1,5 @@ mod mouse_region; +mod mouse_region_event; use serde::Deserialize; use serde_json::json; @@ -13,6 +14,7 @@ use crate::{ ImageData, }; pub use mouse_region::*; +pub use mouse_region_event::*; pub struct Scene { scale_factor: f32, diff --git a/crates/gpui/src/scene/mouse_region.rs b/crates/gpui/src/scene/mouse_region.rs index ba7173b4a1cef04f684abf02d0ccf23cde1ed59f..d64551e30699ba1817d23175130b580a5390a2d3 100644 --- a/crates/gpui/src/scene/mouse_region.rs +++ b/crates/gpui/src/scene/mouse_region.rs @@ -2,9 +2,14 @@ use std::{any::TypeId, mem::Discriminant, rc::Rc}; use collections::HashMap; -use pathfinder_geometry::{rect::RectF, vector::Vector2F}; +use pathfinder_geometry::rect::RectF; -use crate::{EventContext, MouseButton, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent}; +use crate::{EventContext, MouseButton}; + +use super::mouse_region_event::{ + ClickRegionEvent, DownOutRegionEvent, DownRegionEvent, DragRegionEvent, HoverRegionEvent, + MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent, +}; #[derive(Clone, Default)] pub struct MouseRegion { @@ -12,6 +17,7 @@ pub struct MouseRegion { pub discriminant: Option<(TypeId, usize)>, pub bounds: RectF, pub handlers: HandlerSet, + pub hoverable: bool, } impl MouseRegion { @@ -30,6 +36,7 @@ impl MouseRegion { discriminant, bounds, handlers, + hoverable: true, } } @@ -42,14 +49,15 @@ impl MouseRegion { view_id, discriminant, bounds, - handlers: HandlerSet::handle_all(), + handlers: HandlerSet::capture_all(), + hoverable: true, } } pub fn on_down( mut self, button: MouseButton, - handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static, + handler: impl Fn(DownRegionEvent, &mut EventContext) + 'static, ) -> Self { self.handlers = self.handlers.on_down(button, handler); self @@ -58,7 +66,7 @@ impl MouseRegion { pub fn on_up( mut self, button: MouseButton, - handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static, + handler: impl Fn(UpRegionEvent, &mut EventContext) + 'static, ) -> Self { self.handlers = self.handlers.on_up(button, handler); self @@ -67,7 +75,7 @@ impl MouseRegion { pub fn on_click( mut self, button: MouseButton, - handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static, + handler: impl Fn(ClickRegionEvent, &mut EventContext) + 'static, ) -> Self { self.handlers = self.handlers.on_click(button, handler); self @@ -76,16 +84,25 @@ impl MouseRegion { pub fn on_down_out( mut self, button: MouseButton, - handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static, + handler: impl Fn(DownOutRegionEvent, &mut EventContext) + 'static, ) -> Self { self.handlers = self.handlers.on_down_out(button, handler); self } + pub fn on_up_out( + mut self, + button: MouseButton, + handler: impl Fn(UpOutRegionEvent, &mut EventContext) + 'static, + ) -> Self { + self.handlers = self.handlers.on_up_out(button, handler); + self + } + pub fn on_drag( mut self, button: MouseButton, - handler: impl Fn(Vector2F, MouseMovedEvent, &mut EventContext) + 'static, + handler: impl Fn(DragRegionEvent, &mut EventContext) + 'static, ) -> Self { self.handlers = self.handlers.on_drag(button, handler); self @@ -93,7 +110,7 @@ impl MouseRegion { pub fn on_hover( mut self, - handler: impl Fn(bool, MouseMovedEvent, &mut EventContext) + 'static, + handler: impl Fn(HoverRegionEvent, &mut EventContext) + 'static, ) -> Self { self.handlers = self.handlers.on_hover(handler); self @@ -101,14 +118,19 @@ impl MouseRegion { pub fn on_move( mut self, - handler: impl Fn(MouseMovedEvent, &mut EventContext) + 'static, + handler: impl Fn(MoveRegionEvent, &mut EventContext) + 'static, ) -> Self { self.handlers = self.handlers.on_move(handler); self } + + pub fn with_hoverable(mut self, is_hoverable: bool) -> Self { + self.hoverable = is_hoverable; + self + } } -#[derive(Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] pub struct MouseRegionId { pub view_id: usize, pub discriminant: (TypeId, usize), @@ -124,7 +146,7 @@ pub struct HandlerSet { } impl HandlerSet { - pub fn handle_all() -> Self { + pub fn capture_all() -> Self { #[allow(clippy::type_complexity)] let mut set: HashMap< (Discriminant, Option), @@ -154,6 +176,10 @@ impl HandlerSet { (MouseRegionEvent::down_out_disc(), Some(button)), Rc::new(|_, _| {}), ); + set.insert( + (MouseRegionEvent::up_out_disc(), Some(button)), + Rc::new(|_, _| {}), + ); } set.insert( (MouseRegionEvent::scroll_wheel_disc(), None), @@ -170,15 +196,32 @@ impl HandlerSet { self.set.get(key).cloned() } + pub fn on_move( + mut self, + handler: impl Fn(MoveRegionEvent, &mut EventContext) + 'static, + ) -> Self { + self.set.insert((MouseRegionEvent::move_disc(), None), + Rc::new(move |region_event, cx| { + if let MouseRegionEvent::Move(e) = region_event { + handler(e, cx); + } else { + panic!( + "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Move, found {:?}", + region_event); + } + })); + self + } + pub fn on_down( mut self, button: MouseButton, - handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static, + handler: impl Fn(DownRegionEvent, &mut EventContext) + 'static, ) -> Self { self.set.insert((MouseRegionEvent::down_disc(), Some(button)), Rc::new(move |region_event, cx| { - if let MouseRegionEvent::Down(mouse_button_event) = region_event { - handler(mouse_button_event, cx); + if let MouseRegionEvent::Down(e) = region_event { + handler(e, cx); } else { panic!( "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Down, found {:?}", @@ -191,12 +234,12 @@ impl HandlerSet { pub fn on_up( mut self, button: MouseButton, - handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static, + handler: impl Fn(UpRegionEvent, &mut EventContext) + 'static, ) -> Self { self.set.insert((MouseRegionEvent::up_disc(), Some(button)), Rc::new(move |region_event, cx| { - if let MouseRegionEvent::Up(mouse_button_event) = region_event { - handler(mouse_button_event, cx); + if let MouseRegionEvent::Up(e) = region_event { + handler(e, cx); } else { panic!( "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Up, found {:?}", @@ -209,12 +252,12 @@ impl HandlerSet { pub fn on_click( mut self, button: MouseButton, - handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static, + handler: impl Fn(ClickRegionEvent, &mut EventContext) + 'static, ) -> Self { self.set.insert((MouseRegionEvent::click_disc(), Some(button)), Rc::new(move |region_event, cx| { - if let MouseRegionEvent::Click(mouse_button_event) = region_event { - handler(mouse_button_event, cx); + if let MouseRegionEvent::Click(e) = region_event { + handler(e, cx); } else { panic!( "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Click, found {:?}", @@ -227,12 +270,12 @@ impl HandlerSet { pub fn on_down_out( mut self, button: MouseButton, - handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static, + handler: impl Fn(DownOutRegionEvent, &mut EventContext) + 'static, ) -> Self { self.set.insert((MouseRegionEvent::down_out_disc(), Some(button)), Rc::new(move |region_event, cx| { - if let MouseRegionEvent::DownOut(mouse_button_event) = region_event { - handler(mouse_button_event, cx); + if let MouseRegionEvent::DownOut(e) = region_event { + handler(e, cx); } else { panic!( "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::DownOut, found {:?}", @@ -242,123 +285,56 @@ impl HandlerSet { self } - pub fn on_drag( + pub fn on_up_out( mut self, button: MouseButton, - handler: impl Fn(Vector2F, MouseMovedEvent, &mut EventContext) + 'static, + handler: impl Fn(UpOutRegionEvent, &mut EventContext) + 'static, ) -> Self { - self.set.insert((MouseRegionEvent::drag_disc(), Some(button)), + self.set.insert((MouseRegionEvent::up_out_disc(), Some(button)), Rc::new(move |region_event, cx| { - if let MouseRegionEvent::Drag(prev_drag_position, mouse_moved_event) = region_event { - handler(prev_drag_position, mouse_moved_event, cx); + if let MouseRegionEvent::UpOut(e) = region_event { + handler(e, cx); } else { panic!( - "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Drag, found {:?}", + "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::UpOut, found {:?}", region_event); } })); self } - pub fn on_hover( + pub fn on_drag( mut self, - handler: impl Fn(bool, MouseMovedEvent, &mut EventContext) + 'static, + button: MouseButton, + handler: impl Fn(DragRegionEvent, &mut EventContext) + 'static, ) -> Self { - self.set.insert((MouseRegionEvent::hover_disc(), None), + self.set.insert((MouseRegionEvent::drag_disc(), Some(button)), Rc::new(move |region_event, cx| { - if let MouseRegionEvent::Hover(hover, mouse_moved_event) = region_event { - handler(hover, mouse_moved_event, cx); + if let MouseRegionEvent::Drag(e) = region_event { + handler(e, cx); } else { panic!( - "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Hover, found {:?}", + "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Drag, found {:?}", region_event); } })); self } - pub fn on_move( + pub fn on_hover( mut self, - handler: impl Fn(MouseMovedEvent, &mut EventContext) + 'static, + handler: impl Fn(HoverRegionEvent, &mut EventContext) + 'static, ) -> Self { - self.set.insert((MouseRegionEvent::move_disc(), None), + self.set.insert((MouseRegionEvent::hover_disc(), None), Rc::new(move |region_event, cx| { - if let MouseRegionEvent::Move(move_event)= region_event { - handler(move_event, cx); - } else { + if let MouseRegionEvent::Hover(e) = region_event { + handler(e, cx); + } else { panic!( - "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Move, found {:?}", + "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Hover, found {:?}", region_event); } })); self } } - -#[derive(Debug)] -pub enum MouseRegionEvent { - Move(MouseMovedEvent), - Drag(Vector2F, MouseMovedEvent), - Hover(bool, MouseMovedEvent), - Down(MouseButtonEvent), - Up(MouseButtonEvent), - Click(MouseButtonEvent), - DownOut(MouseButtonEvent), - ScrollWheel(ScrollWheelEvent), -} - -impl MouseRegionEvent { - pub fn move_disc() -> Discriminant { - std::mem::discriminant(&MouseRegionEvent::Move(Default::default())) - } - pub fn drag_disc() -> Discriminant { - std::mem::discriminant(&MouseRegionEvent::Drag( - Default::default(), - Default::default(), - )) - } - pub fn hover_disc() -> Discriminant { - std::mem::discriminant(&MouseRegionEvent::Hover( - Default::default(), - Default::default(), - )) - } - pub fn down_disc() -> Discriminant { - std::mem::discriminant(&MouseRegionEvent::Down(Default::default())) - } - pub fn up_disc() -> Discriminant { - std::mem::discriminant(&MouseRegionEvent::Up(Default::default())) - } - pub fn click_disc() -> Discriminant { - std::mem::discriminant(&MouseRegionEvent::Click(Default::default())) - } - pub fn down_out_disc() -> Discriminant { - std::mem::discriminant(&MouseRegionEvent::DownOut(Default::default())) - } - pub fn scroll_wheel_disc() -> Discriminant { - std::mem::discriminant(&MouseRegionEvent::ScrollWheel(Default::default())) - } - - pub fn handler_key(&self) -> (Discriminant, Option) { - match self { - MouseRegionEvent::Move(_) => (Self::move_disc(), None), - MouseRegionEvent::Drag(_, MouseMovedEvent { pressed_button, .. }) => { - (Self::drag_disc(), *pressed_button) - } - MouseRegionEvent::Hover(_, _) => (Self::hover_disc(), None), - MouseRegionEvent::Down(MouseButtonEvent { button, .. }) => { - (Self::down_disc(), Some(*button)) - } - MouseRegionEvent::Up(MouseButtonEvent { button, .. }) => { - (Self::up_disc(), Some(*button)) - } - MouseRegionEvent::Click(MouseButtonEvent { button, .. }) => { - (Self::click_disc(), Some(*button)) - } - MouseRegionEvent::DownOut(MouseButtonEvent { button, .. }) => { - (Self::down_out_disc(), Some(*button)) - } - MouseRegionEvent::ScrollWheel(_) => (Self::scroll_wheel_disc(), None), - } - } -} diff --git a/crates/gpui/src/scene/mouse_region_event.rs b/crates/gpui/src/scene/mouse_region_event.rs new file mode 100644 index 0000000000000000000000000000000000000000..a375651c7aca9e282caec411c47b708e19837f1a --- /dev/null +++ b/crates/gpui/src/scene/mouse_region_event.rs @@ -0,0 +1,233 @@ +use std::{ + mem::{discriminant, Discriminant}, + ops::Deref, +}; + +use pathfinder_geometry::{rect::RectF, vector::Vector2F}; + +use crate::{MouseButton, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent}; + +#[derive(Debug, Default, Clone)] +pub struct MoveRegionEvent { + pub region: RectF, + pub platform_event: MouseMovedEvent, +} + +impl Deref for MoveRegionEvent { + type Target = MouseMovedEvent; + + fn deref(&self) -> &Self::Target { + &self.platform_event + } +} + +#[derive(Debug, Default, Clone)] +pub struct DragRegionEvent { + pub region: RectF, + pub prev_mouse_position: Vector2F, + pub platform_event: MouseMovedEvent, +} + +impl Deref for DragRegionEvent { + type Target = MouseMovedEvent; + + fn deref(&self) -> &Self::Target { + &self.platform_event + } +} + +#[derive(Debug, Default, Clone)] +pub struct HoverRegionEvent { + pub region: RectF, + pub started: bool, + pub platform_event: MouseMovedEvent, +} + +impl Deref for HoverRegionEvent { + type Target = MouseMovedEvent; + + fn deref(&self) -> &Self::Target { + &self.platform_event + } +} + +#[derive(Debug, Default, Clone)] +pub struct DownRegionEvent { + pub region: RectF, + pub platform_event: MouseButtonEvent, +} + +impl Deref for DownRegionEvent { + type Target = MouseButtonEvent; + + fn deref(&self) -> &Self::Target { + &self.platform_event + } +} + +#[derive(Debug, Default, Clone)] +pub struct UpRegionEvent { + pub region: RectF, + pub platform_event: MouseButtonEvent, +} + +impl Deref for UpRegionEvent { + type Target = MouseButtonEvent; + + fn deref(&self) -> &Self::Target { + &self.platform_event + } +} + +#[derive(Debug, Default, Clone)] +pub struct ClickRegionEvent { + pub region: RectF, + pub platform_event: MouseButtonEvent, +} + +impl Deref for ClickRegionEvent { + type Target = MouseButtonEvent; + + fn deref(&self) -> &Self::Target { + &self.platform_event + } +} + +#[derive(Debug, Default, Clone)] +pub struct DownOutRegionEvent { + pub region: RectF, + pub platform_event: MouseButtonEvent, +} + +impl Deref for DownOutRegionEvent { + type Target = MouseButtonEvent; + + fn deref(&self) -> &Self::Target { + &self.platform_event + } +} + +#[derive(Debug, Default, Clone)] +pub struct UpOutRegionEvent { + pub region: RectF, + pub platform_event: MouseButtonEvent, +} + +impl Deref for UpOutRegionEvent { + type Target = MouseButtonEvent; + + fn deref(&self) -> &Self::Target { + &self.platform_event + } +} + +#[derive(Debug, Default, Clone)] +pub struct ScrollWheelRegionEvent { + pub region: RectF, + pub platform_event: ScrollWheelEvent, +} + +impl Deref for ScrollWheelRegionEvent { + type Target = ScrollWheelEvent; + + fn deref(&self) -> &Self::Target { + &self.platform_event + } +} + +#[derive(Debug, Clone)] +pub enum MouseRegionEvent { + Move(MoveRegionEvent), + Drag(DragRegionEvent), + Hover(HoverRegionEvent), + Down(DownRegionEvent), + Up(UpRegionEvent), + Click(ClickRegionEvent), + DownOut(DownOutRegionEvent), + UpOut(UpOutRegionEvent), + ScrollWheel(ScrollWheelRegionEvent), +} + +impl MouseRegionEvent { + pub fn set_region(&mut self, region: RectF) { + match self { + MouseRegionEvent::Move(r) => r.region = region, + MouseRegionEvent::Drag(r) => r.region = region, + MouseRegionEvent::Hover(r) => r.region = region, + MouseRegionEvent::Down(r) => r.region = region, + MouseRegionEvent::Up(r) => r.region = region, + MouseRegionEvent::Click(r) => r.region = region, + MouseRegionEvent::DownOut(r) => r.region = region, + MouseRegionEvent::UpOut(r) => r.region = region, + MouseRegionEvent::ScrollWheel(r) => r.region = region, + } + } + + /// When true, mouse event handlers must call cx.propagate_event() to bubble + /// the event to handlers they are painted on top of. + pub fn is_capturable(&self) -> bool { + match self { + MouseRegionEvent::Move(_) => true, + MouseRegionEvent::Drag(_) => false, + MouseRegionEvent::Hover(_) => true, + MouseRegionEvent::Down(_) => true, + MouseRegionEvent::Up(_) => true, + MouseRegionEvent::Click(_) => true, + MouseRegionEvent::DownOut(_) => false, + MouseRegionEvent::UpOut(_) => false, + MouseRegionEvent::ScrollWheel(_) => true, + } + } +} + +impl MouseRegionEvent { + pub fn move_disc() -> Discriminant { + discriminant(&MouseRegionEvent::Move(Default::default())) + } + + pub fn drag_disc() -> Discriminant { + discriminant(&MouseRegionEvent::Drag(Default::default())) + } + + pub fn hover_disc() -> Discriminant { + discriminant(&MouseRegionEvent::Hover(Default::default())) + } + + pub fn down_disc() -> Discriminant { + discriminant(&MouseRegionEvent::Down(Default::default())) + } + + pub fn up_disc() -> Discriminant { + discriminant(&MouseRegionEvent::Up(Default::default())) + } + + pub fn up_out_disc() -> Discriminant { + discriminant(&MouseRegionEvent::UpOut(Default::default())) + } + + pub fn click_disc() -> Discriminant { + discriminant(&MouseRegionEvent::Click(Default::default())) + } + + pub fn down_out_disc() -> Discriminant { + discriminant(&MouseRegionEvent::DownOut(Default::default())) + } + + pub fn scroll_wheel_disc() -> Discriminant { + discriminant(&MouseRegionEvent::ScrollWheel(Default::default())) + } + + pub fn handler_key(&self) -> (Discriminant, Option) { + match self { + MouseRegionEvent::Move(_) => (Self::move_disc(), None), + MouseRegionEvent::Drag(e) => (Self::drag_disc(), e.pressed_button), + MouseRegionEvent::Hover(_) => (Self::hover_disc(), None), + MouseRegionEvent::Down(e) => (Self::down_disc(), Some(e.button)), + MouseRegionEvent::Up(e) => (Self::up_disc(), Some(e.button)), + MouseRegionEvent::Click(e) => (Self::click_disc(), Some(e.button)), + MouseRegionEvent::UpOut(e) => (Self::up_out_disc(), Some(e.button)), + MouseRegionEvent::DownOut(e) => (Self::down_out_disc(), Some(e.button)), + MouseRegionEvent::ScrollWheel(_) => (Self::scroll_wheel_disc(), None), + } + } +} diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index dff51fe2bb1e99ad901115165e41002aefaa1d92..bc314d81be4770bbfdcda88c5c43a26684177b6c 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -12,8 +12,7 @@ use gpui::{ impl_internal_actions, keymap, platform::CursorStyle, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton, - MouseButtonEvent, MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, - ViewHandle, + MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; @@ -1074,25 +1073,22 @@ impl ProjectPanel { .with_padding_left(padding) .boxed() }) - .on_click( - MouseButton::Left, - move |MouseButtonEvent { click_count, .. }, cx| { - if kind == EntryKind::Dir { - cx.dispatch_action(ToggleExpanded(entry_id)) - } else { - cx.dispatch_action(Open { - entry_id, - change_focus: click_count > 1, - }) - } - }, - ) - .on_down( - MouseButton::Right, - move |MouseButtonEvent { position, .. }, cx| { - cx.dispatch_action(DeployContextMenu { entry_id, position }) - }, - ) + .on_click(MouseButton::Left, move |e, cx| { + if kind == EntryKind::Dir { + cx.dispatch_action(ToggleExpanded(entry_id)) + } else { + cx.dispatch_action(Open { + entry_id, + change_focus: e.click_count > 1, + }) + } + }) + .on_down(MouseButton::Right, move |e, cx| { + cx.dispatch_action(DeployContextMenu { + entry_id, + position: e.position, + }) + }) .with_cursor_style(CursorStyle::PointingHand) .boxed() } @@ -1139,16 +1135,16 @@ impl View for ProjectPanel { .expanded() .boxed() }) - .on_down( - MouseButton::Right, - move |MouseButtonEvent { position, .. }, cx| { - // When deploying the context menu anywhere below the last project entry, - // act as if the user clicked the root of the last worktree. - if let Some(entry_id) = last_worktree_root_id { - cx.dispatch_action(DeployContextMenu { entry_id, position }) - } - }, - ) + .on_down(MouseButton::Right, move |e, cx| { + // When deploying the context menu anywhere below the last project entry, + // act as if the user clicked the root of the last worktree. + if let Some(entry_id) = last_worktree_root_id { + cx.dispatch_action(DeployContextMenu { + entry_id, + position: e.position, + }) + } + }) .boxed(), ) .with_child(ChildView::new(&self.context_menu).boxed()) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 17078826b9dee2bf6ea72d7d529e8b4437311af7..89fec264470e2ce0dae04c335a6679622b3c57c3 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -39,13 +39,11 @@ use std::{ use thiserror::Error; use gpui::{ - geometry::{ - rect::RectF, - vector::{vec2f, Vector2F}, - }, + geometry::vector::{vec2f, Vector2F}, keymap::Keystroke, - ClipboardItem, Entity, ModelContext, MouseButton, MouseButtonEvent, MouseMovedEvent, - MutableAppContext, ScrollWheelEvent, + scene::{ClickRegionEvent, DownRegionEvent, DragRegionEvent, UpRegionEvent}, + ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext, + ScrollWheelEvent, }; use crate::mappings::{ @@ -676,7 +674,7 @@ impl Terminal { } } - pub fn mouse_drag(&mut self, e: MouseMovedEvent, origin: Vector2F, bounds: RectF) { + pub fn mouse_drag(&mut self, e: DragRegionEvent, origin: Vector2F) { let position = e.position.sub(origin); if !self.mouse_mode(e.shift) { @@ -687,8 +685,8 @@ impl Terminal { // Doesn't make sense to scroll the alt screen if !self.last_mode.contains(TermMode::ALT_SCREEN) { //TODO: Why do these need to be doubled? - let top = bounds.origin_y() + (self.cur_size.line_height * 2.); - let bottom = bounds.lower_left().y() - (self.cur_size.line_height * 2.); + let top = e.region.origin_y() + (self.cur_size.line_height * 2.); + let bottom = e.region.lower_left().y() - (self.cur_size.line_height * 2.); let scroll_delta = if e.position.y() < top { (top - e.position.y()).powf(1.1) @@ -705,7 +703,7 @@ impl Terminal { } } - pub fn mouse_down(&mut self, e: &MouseButtonEvent, origin: Vector2F) { + pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) { let position = e.position.sub(origin); let point = mouse_point(position, self.cur_size, self.last_offset); let side = mouse_side(position, self.cur_size); @@ -719,7 +717,7 @@ impl Terminal { } } - pub fn left_click(&mut self, e: &MouseButtonEvent, origin: Vector2F) { + pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) { let position = e.position.sub(origin); if !self.mouse_mode(e.shift) { @@ -743,7 +741,7 @@ impl Terminal { } } - pub fn mouse_up(&mut self, e: &MouseButtonEvent, origin: Vector2F) { + pub fn mouse_up(&mut self, e: &UpRegionEvent, origin: Vector2F) { let position = e.position.sub(origin); if self.mouse_mode(e.shift) { let point = mouse_point(position, self.cur_size, self.last_offset); diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index a2f2cc7d4c7cc7a41ef178497fef1de0dcf00cef..7cb3f032e4066ffdbfc9d8b7eb28dba941733323 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -18,9 +18,8 @@ use gpui::{ }, serde_json::json, text_layout::{Line, RunStyle}, - Element, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton, - MouseButtonEvent, MouseRegion, PaintContext, Quad, TextLayoutCache, WeakModelHandle, - WeakViewHandle, + Element, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton, MouseRegion, + PaintContext, Quad, TextLayoutCache, WeakModelHandle, WeakViewHandle, }; use itertools::Itertools; use ordered_float::OrderedFloat; @@ -410,11 +409,11 @@ impl TerminalElement { } } - fn generic_button_handler( + fn generic_button_handler( connection: WeakModelHandle, origin: Vector2F, - f: impl Fn(&mut Terminal, Vector2F, MouseButtonEvent, &mut ModelContext), - ) -> impl Fn(MouseButtonEvent, &mut EventContext) { + f: impl Fn(&mut Terminal, Vector2F, E, &mut ModelContext), + ) -> impl Fn(E, &mut EventContext) { move |event, cx| { cx.focus_parent_view(); if let Some(conn_handle) = connection.upgrade(cx.app) { @@ -453,11 +452,11 @@ impl TerminalElement { ), ) // Update drag selections - .on_drag(MouseButton::Left, move |_prev, event, cx| { + .on_drag(MouseButton::Left, move |event, cx| { if cx.is_parent_view_focused() { if let Some(conn_handle) = connection.upgrade(cx.app) { conn_handle.update(cx.app, |terminal, cx| { - terminal.mouse_drag(event, origin, visible_bounds); + terminal.mouse_drag(event, origin); cx.notify(); }) } @@ -486,20 +485,19 @@ impl TerminalElement { ), ) // Context menu - .on_click( - MouseButton::Right, - move |e @ MouseButtonEvent { position, .. }, cx| { - let mouse_mode = if let Some(conn_handle) = connection.upgrade(cx.app) { - conn_handle.update(cx.app, |terminal, _cx| terminal.mouse_mode(e.shift)) - } else { - // If we can't get the model handle, probably can't deploy the context menu - true - }; - if !mouse_mode { - cx.dispatch_action(DeployContextMenu { position }); - } - }, - ); + .on_click(MouseButton::Right, move |e, cx| { + let mouse_mode = if let Some(conn_handle) = connection.upgrade(cx.app) { + conn_handle.update(cx.app, |terminal, _cx| terminal.mouse_mode(e.shift)) + } else { + // If we can't get the model handle, probably can't deploy the context menu + true + }; + if !mouse_mode { + cx.dispatch_action(DeployContextMenu { + position: e.position, + }); + } + }); // Mouse mode handlers: // All mouse modes need the extra click handlers diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 1aaeed2d7829739c4006f6aa645a89f739012367..be89148985a1f453f89dcae03b770e79fa488b31 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -75,7 +75,25 @@ pub struct TabBar { pub pane_button: Interactive, pub active_pane: TabStyles, pub inactive_pane: TabStyles, + pub dragged_tab: Tab, pub height: f32, + pub drop_target_overlay_color: Color, +} + +impl TabBar { + pub fn tab_style(&self, pane_active: bool, tab_active: bool) -> &Tab { + let tabs = if pane_active { + &self.active_pane + } else { + &self.inactive_pane + }; + + if tab_active { + &tabs.active_tab + } else { + &tabs.inactive_tab + } + } } #[derive(Clone, Deserialize, Default)] diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 3534e293f8d86f1ec1efface052e246e76bfc0f6..c40ce5638947b63bedf05ecd071592539065009a 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -15,6 +15,7 @@ client = { path = "../client" } clock = { path = "../clock" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } +drag_and_drop = { path = "../drag_and_drop" } gpui = { path = "../gpui" } language = { path = "../language" } menu = { path = "../menu" } diff --git a/crates/workspace/src/drag_and_drop.rs b/crates/workspace/src/drag_and_drop.rs deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 34108eef5e0abbbd5939c1347c76c9d2ebc63662..d8e1ea8e2f3c8925573c61868cdd535060d37fd8 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -3,9 +3,11 @@ use crate::{toolbar::Toolbar, Item, NewFile, NewSearch, NewTerminal, WeakItemHan use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use context_menu::{ContextMenu, ContextMenuItem}; +use drag_and_drop::{DragAndDrop, Draggable}; use futures::StreamExt; use gpui::{ actions, + color::Color, elements::*, geometry::{ rect::RectF, @@ -14,13 +16,14 @@ use gpui::{ impl_actions, impl_internal_actions, platform::{CursorStyle, NavigationDirection}, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext, - ModelHandle, MouseButton, MouseButtonEvent, MutableAppContext, PromptLevel, Quad, - RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, + ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View, + ViewContext, ViewHandle, WeakViewHandle, }; use project::{Project, ProjectEntryId, ProjectPath}; use serde::Deserialize; use settings::{Autosave, Settings}; -use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc}; +use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc}; +use theme::Theme; use util::ResultExt; #[derive(Clone, Deserialize, PartialEq)] @@ -48,6 +51,14 @@ pub struct CloseItem { pub pane: WeakViewHandle, } +#[derive(Clone, PartialEq)] +pub struct MoveItem { + pub item_id: usize, + pub from: WeakViewHandle, + pub to: WeakViewHandle, + pub destination_index: usize, +} + #[derive(Clone, Deserialize, PartialEq)] pub struct GoBack { #[serde(skip_deserializing)] @@ -71,16 +82,16 @@ pub struct DeployNewMenu { } impl_actions!(pane, [GoBack, GoForward, ActivateItem]); -impl_internal_actions!(pane, [CloseItem, DeploySplitMenu, DeployNewMenu]); +impl_internal_actions!(pane, [CloseItem, DeploySplitMenu, DeployNewMenu, MoveItem]); const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; pub fn init(cx: &mut MutableAppContext) { cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { - pane.activate_item(action.0, true, true, false, cx); + pane.activate_item(action.0, true, true, cx); }); cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| { - pane.activate_item(pane.items.len() - 1, true, true, false, cx); + pane.activate_item(pane.items.len() - 1, true, true, cx); }); cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| { pane.activate_prev_item(cx); @@ -98,6 +109,32 @@ pub fn init(cx: &mut MutableAppContext) { Ok(()) })) }); + cx.add_action( + |workspace, + MoveItem { + from, + to, + item_id, + destination_index, + }, + cx| { + // Get item handle to move + let from = if let Some(from) = from.upgrade(cx) { + from + } else { + return; + }; + + // Add item to new pane at given index + let to = if let Some(to) = to.upgrade(cx) { + to + } else { + return; + }; + + Pane::move_item(workspace, from, to, *item_id, *destination_index, cx) + }, + ); cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)); cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)); cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)); @@ -186,6 +223,17 @@ pub struct NavigationEntry { pub data: Option>, } +struct DraggedItem { + item: Box, + pane: WeakViewHandle, +} + +pub enum ReorderBehavior { + None, + MoveAfterActive, + MoveToIndex(usize), +} + impl Pane { pub fn new(cx: &mut ViewContext) -> Self { let handle = cx.weak_handle(); @@ -300,7 +348,7 @@ impl Pane { { let prev_active_item_index = pane.active_item_index; pane.nav_history.borrow_mut().set_mode(mode); - pane.activate_item(index, true, true, false, cx); + pane.activate_item(index, true, true, cx); pane.nav_history .borrow_mut() .set_mode(NavigationMode::Normal); @@ -387,63 +435,100 @@ impl Pane { build_item: impl FnOnce(&mut ViewContext) -> Box, ) -> Box { let existing_item = pane.update(cx, |pane, cx| { - for (ix, item) in pane.items.iter().enumerate() { + for item in pane.items.iter() { if item.project_path(cx).is_some() && item.project_entry_ids(cx).as_slice() == [project_entry_id] { let item = item.boxed_clone(); - pane.activate_item(ix, true, focus_item, true, cx); return Some(item); } } None }); - if let Some(existing_item) = existing_item { - existing_item - } else { - let item = pane.update(cx, |_, cx| build_item(cx)); - Self::add_item(workspace, pane, item.boxed_clone(), true, focus_item, cx); - item - } + + // Even if the item exists, we re-add it to reorder it after the active item. + // We may revisit this behavior after adding an "activation history" for pane items. + let item = existing_item.unwrap_or_else(|| pane.update(cx, |_, cx| build_item(cx))); + Pane::add_item(workspace, &pane, item.clone(), true, focus_item, None, cx); + item } - pub(crate) fn add_item( + pub fn add_item( workspace: &mut Workspace, - pane: ViewHandle, + pane: &ViewHandle, item: Box, activate_pane: bool, focus_item: bool, + destination_index: Option, cx: &mut ViewContext, ) { - // Prevent adding the same item to the pane more than once. - // If there is already an active item, reorder the desired item to be after it - // and activate it. - if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) { + // If no destination index is specified, add or move the item after the active item. + let mut insertion_index = { + let pane = pane.read(cx); + cmp::min( + if let Some(destination_index) = destination_index { + destination_index + } else { + pane.active_item_index + 1 + }, + pane.items.len(), + ) + }; + + // Does the item already exist? + if let Some(existing_item_index) = pane.read(cx).items.iter().position(|existing_item| { + let existing_item_entry_ids = existing_item.project_entry_ids(cx); + let added_item_entry_ids = item.project_entry_ids(cx); + let entries_match = !existing_item_entry_ids.is_empty() + && existing_item_entry_ids == added_item_entry_ids; + + existing_item.id() == item.id() || entries_match + }) { + // If the item already exists, move it to the desired destination and activate it pane.update(cx, |pane, cx| { - pane.activate_item(item_ix, activate_pane, focus_item, true, cx) - }); - return; - } + if existing_item_index != insertion_index { + cx.reparent(&item); + let existing_item_is_active = existing_item_index == pane.active_item_index; + + // If the caller didn't specify a destination and the added item is already + // the active one, don't move it + if existing_item_is_active && destination_index.is_none() { + insertion_index = existing_item_index; + } else { + pane.items.remove(existing_item_index); + if existing_item_index < pane.active_item_index { + pane.active_item_index -= 1; + } + insertion_index = insertion_index.min(pane.items.len()); - item.added_to_pane(workspace, pane.clone(), cx); - pane.update(cx, |pane, cx| { - // If there is already an active item, then insert the new item - // right after it. Otherwise, adjust the `active_item_index` field - // before activating the new item, so that in the `activate_item` - // method, we can detect that the active item is changing. - let item_ix; - if pane.active_item_index < pane.items.len() { - item_ix = pane.active_item_index + 1 - } else { - item_ix = pane.items.len(); - pane.active_item_index = usize::MAX; - }; + pane.items.insert(insertion_index, item.clone()); - cx.reparent(&item); - pane.items.insert(item_ix, item); - pane.activate_item(item_ix, activate_pane, focus_item, false, cx); - cx.notify(); - }); + if existing_item_is_active { + pane.active_item_index = insertion_index; + } else if insertion_index <= pane.active_item_index { + pane.active_item_index += 1; + } + } + + cx.notify(); + } + + pane.activate_item(insertion_index, activate_pane, focus_item, cx); + }); + } else { + // If the item doesn't already exist, add it and activate it + item.added_to_pane(workspace, pane.clone(), cx); + pane.update(cx, |pane, cx| { + cx.reparent(&item); + pane.items.insert(insertion_index, item); + if insertion_index <= pane.active_item_index { + pane.active_item_index += 1; + } + + pane.activate_item(insertion_index, activate_pane, focus_item, cx); + cx.notify(); + }); + } } pub fn items(&self) -> impl Iterator> { @@ -480,31 +565,13 @@ impl Pane { pub fn activate_item( &mut self, - mut index: usize, + index: usize, activate_pane: bool, focus_item: bool, - move_after_current_active: bool, cx: &mut ViewContext, ) { use NavigationMode::{GoingBack, GoingForward}; if index < self.items.len() { - if move_after_current_active { - // If there is already an active item, reorder the desired item to be after it - // and activate it. - if self.active_item_index != index && self.active_item_index < self.items.len() { - let pane_to_activate = self.items.remove(index); - if self.active_item_index < index { - index = self.active_item_index + 1; - } else if self.active_item_index < self.items.len() + 1 { - index = self.active_item_index; - // Index is less than active_item_index. Reordering will decrement the - // active_item_index, so adjust it accordingly - self.active_item_index = index - 1; - } - self.items.insert(index, pane_to_activate); - } - } - let prev_active_item_ix = mem::replace(&mut self.active_item_index, index); if prev_active_item_ix != self.active_item_index || matches!(self.nav_history.borrow().mode, GoingBack | GoingForward) @@ -535,7 +602,7 @@ impl Pane { } else if !self.items.is_empty() { index = self.items.len() - 1; } - self.activate_item(index, true, true, false, cx); + self.activate_item(index, true, true, cx); } pub fn activate_next_item(&mut self, cx: &mut ViewContext) { @@ -545,7 +612,7 @@ impl Pane { } else { index = 0; } - self.activate_item(index, true, true, false, cx); + self.activate_item(index, true, true, cx); } pub fn close_active_item( @@ -672,48 +739,7 @@ impl Pane { // Remove the item from the pane. pane.update(&mut cx, |pane, cx| { if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) { - if item_ix == pane.active_item_index { - // Activate the previous item if possible. - // This returns the user to the previously opened tab if they closed - // a ne item they just navigated to. - if item_ix > 0 { - pane.activate_prev_item(cx); - } else if item_ix + 1 < pane.items.len() { - pane.activate_next_item(cx); - } - } - - let item = pane.items.remove(item_ix); - cx.emit(Event::RemoveItem); - if pane.items.is_empty() { - item.deactivated(cx); - pane.update_toolbar(cx); - cx.emit(Event::Remove); - } - - if item_ix < pane.active_item_index { - pane.active_item_index -= 1; - } - - pane.nav_history - .borrow_mut() - .set_mode(NavigationMode::ClosingItem); - item.deactivated(cx); - pane.nav_history - .borrow_mut() - .set_mode(NavigationMode::Normal); - - if let Some(path) = item.project_path(cx) { - pane.nav_history - .borrow_mut() - .paths_by_item - .insert(item.id(), path); - } else { - pane.nav_history - .borrow_mut() - .paths_by_item - .remove(&item.id()); - } + pane.remove_item(item_ix, cx); } }); } @@ -723,6 +749,53 @@ impl Pane { }) } + fn remove_item(&mut self, item_ix: usize, cx: &mut ViewContext) { + if item_ix == self.active_item_index { + // Activate the previous item if possible. + // This returns the user to the previously opened tab if they closed + // a new item they just navigated to. + if item_ix > 0 { + self.activate_prev_item(cx); + } else if item_ix + 1 < self.items.len() { + self.activate_next_item(cx); + } + } + + let item = self.items.remove(item_ix); + cx.emit(Event::RemoveItem); + if self.items.is_empty() { + item.deactivated(cx); + self.update_toolbar(cx); + cx.emit(Event::Remove); + } + + if item_ix < self.active_item_index { + self.active_item_index -= 1; + } + + self.nav_history + .borrow_mut() + .set_mode(NavigationMode::ClosingItem); + item.deactivated(cx); + self.nav_history + .borrow_mut() + .set_mode(NavigationMode::Normal); + + if let Some(path) = item.project_path(cx) { + self.nav_history + .borrow_mut() + .paths_by_item + .insert(item.id(), path); + } else { + self.nav_history + .borrow_mut() + .paths_by_item + .remove(&item.id()); + } + + cx.notify(); + } + pub async fn save_item( project: ModelHandle, pane: &ViewHandle, @@ -746,7 +819,7 @@ impl Pane { if has_conflict && can_save { let mut answer = pane.update(cx, |pane, cx| { - pane.activate_item(item_ix, true, true, false, cx); + pane.activate_item(item_ix, true, true, cx); cx.prompt( PromptLevel::Warning, CONFLICT_MESSAGE, @@ -767,7 +840,7 @@ impl Pane { }); let should_save = if should_prompt_for_save && !will_autosave { let mut answer = pane.update(cx, |pane, cx| { - pane.activate_item(item_ix, true, true, false, cx); + pane.activate_item(item_ix, true, true, cx); cx.prompt( PromptLevel::Warning, DIRTY_MESSAGE, @@ -829,6 +902,42 @@ impl Pane { } } + fn move_item( + workspace: &mut Workspace, + from: ViewHandle, + to: ViewHandle, + item_to_move: usize, + destination_index: usize, + cx: &mut ViewContext, + ) { + let (item_ix, item_handle) = from + .read(cx) + .items() + .enumerate() + .find(|(_, item_handle)| item_handle.id() == item_to_move) + .expect("Tried to move item handle which was not in from pane"); + + // This automatically removes duplicate items in the pane + Pane::add_item( + workspace, + &to, + item_handle.clone(), + true, + true, + Some(destination_index), + cx, + ); + + if from != to { + // Close item from previous pane + from.update(cx, |from, cx| { + from.remove_item(item_ix, cx); + }); + } + + cx.focus(to); + } + pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext) { cx.emit(Event::Split(direction)); } @@ -876,144 +985,58 @@ impl Pane { }); } - fn render_tabs(&mut self, cx: &mut RenderContext) -> impl Element { + fn render_tab_bar(&mut self, cx: &mut RenderContext) -> impl Element { let theme = cx.global::().theme.clone(); + let filler_index = self.items.len(); enum Tabs {} enum Tab {} + enum Filler {} let pane = cx.handle(); - MouseEventHandler::new::(0, cx, |mouse_state, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { let autoscroll = if mem::take(&mut self.autoscroll) { Some(self.active_item_index) } else { None }; - let is_pane_active = self.is_active; - - let tab_styles = match is_pane_active { - true => theme.workspace.tab_bar.active_pane.clone(), - false => theme.workspace.tab_bar.inactive_pane.clone(), - }; - let filler_style = tab_styles.inactive_tab.clone(); + let pane_active = self.is_active; let mut row = Flex::row().scrollable::(1, autoscroll, cx); - for (ix, (item, detail)) in self.items.iter().zip(self.tab_details(cx)).enumerate() { - let item_id = item.id(); + for (ix, (item, detail)) in self + .items + .iter() + .cloned() + .zip(self.tab_details(cx)) + .enumerate() + { let detail = if detail == 0 { None } else { Some(detail) }; - let is_tab_active = ix == self.active_item_index; - - let close_tab_callback = { - let pane = pane.clone(); - move |_, cx: &mut EventContext| { - cx.dispatch_action(CloseItem { - item_id, - pane: pane.clone(), - }) - } - }; + let tab_active = ix == self.active_item_index; row.add_child({ - let mut tab_style = match is_tab_active { - true => tab_styles.active_tab.clone(), - false => tab_styles.inactive_tab.clone(), - }; - - let title = item.tab_content(detail, &tab_style, cx); - - if ix == 0 { - tab_style.container.border.left = false; - } - - MouseEventHandler::new::(ix, cx, |_, cx| { - Container::new( - Flex::row() - .with_child( - Align::new({ - let diameter = 7.0; - let icon_color = if item.has_conflict(cx) { - Some(tab_style.icon_conflict) - } else if item.is_dirty(cx) { - Some(tab_style.icon_dirty) - } else { - None - }; - - ConstrainedBox::new( - Canvas::new(move |bounds, _, cx| { - if let Some(color) = icon_color { - let square = RectF::new( - bounds.origin(), - vec2f(diameter, diameter), - ); - cx.scene.push_quad(Quad { - bounds: square, - background: Some(color), - border: Default::default(), - corner_radius: diameter / 2., - }); - } - }) - .boxed(), - ) - .with_width(diameter) - .with_height(diameter) - .boxed() - }) - .boxed(), - ) - .with_child( - Container::new(Align::new(title).boxed()) - .with_style(ContainerStyle { - margin: Margin { - left: tab_style.spacing, - right: tab_style.spacing, - ..Default::default() - }, - ..Default::default() - }) - .boxed(), - ) - .with_child( - Align::new( - ConstrainedBox::new(if mouse_state.hovered { - enum TabCloseButton {} - let icon = Svg::new("icons/x_mark_thin_8.svg"); - MouseEventHandler::new::( - item_id, - cx, - |mouse_state, _| { - if mouse_state.hovered { - icon.with_color(tab_style.icon_close_active) - .boxed() - } else { - icon.with_color(tab_style.icon_close) - .boxed() - } - }, - ) - .with_padding(Padding::uniform(4.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, close_tab_callback.clone()) - .on_click( - MouseButton::Middle, - close_tab_callback.clone(), - ) - .named("close-tab-icon") - } else { - Empty::new().boxed() - }) - .with_width(tab_style.icon_width) - .boxed(), - ) - .boxed(), - ) - .boxed(), - ) - .with_style(tab_style.container) - .boxed() + MouseEventHandler::new::(ix, cx, { + let item = item.clone(); + let pane = pane.clone(); + let detail = detail.clone(); + + let theme = cx.global::().theme.clone(); + + move |mouse_state, cx| { + let tab_style = + theme.workspace.tab_bar.tab_style(pane_active, tab_active); + let hovered = mouse_state.hovered; + Self::render_tab( + &item, + pane, + detail, + hovered, + Self::tab_overlay_color(hovered, theme.as_ref(), cx), + tab_style, + cx, + ) + } }) - .with_cursor_style(if is_tab_active && is_pane_active { + .with_cursor_style(if pane_active && tab_active { CursorStyle::Arrow } else { CursorStyle::PointingHand @@ -1021,22 +1044,73 @@ impl Pane { .on_down(MouseButton::Left, move |_, cx| { cx.dispatch_action(ActivateItem(ix)); }) - .on_click(MouseButton::Middle, close_tab_callback) + .on_click(MouseButton::Middle, { + let item = item.clone(); + let pane = pane.clone(); + move |_, cx: &mut EventContext| { + cx.dispatch_action(CloseItem { + item_id: item.id(), + pane: pane.clone(), + }) + } + }) + .on_up(MouseButton::Left, { + let pane = pane.clone(); + move |_, cx: &mut EventContext| Pane::handle_dropped_item(&pane, ix, cx) + }) + .as_draggable( + DraggedItem { + item, + pane: pane.clone(), + }, + { + let theme = cx.global::().theme.clone(); + + let detail = detail.clone(); + move |dragged_item, cx: &mut RenderContext| { + let tab_style = &theme.workspace.tab_bar.dragged_tab; + Pane::render_tab( + &dragged_item.item, + dragged_item.pane.clone(), + detail, + false, + None, + &tab_style, + cx, + ) + } + }, + ) .boxed() }) } + // Use the inactive tab style along with the current pane's active status to decide how to render + // the filler + let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false); row.add_child( - Empty::new() - .contained() - .with_style(filler_style.container) - .with_border(filler_style.container.border) - .flex(0., true) - .named("filler"), + MouseEventHandler::new::(0, cx, |mouse_state, cx| { + let mut filler = Empty::new() + .contained() + .with_style(filler_style.container) + .with_border(filler_style.container.border); + + if let Some(overlay) = Self::tab_overlay_color(mouse_state.hovered, &theme, cx) + { + filler = filler.with_overlay_color(overlay); + } + + filler.boxed() + }) + .flex(1., true) + .named("filler"), ); row.boxed() }) + .on_up(MouseButton::Left, move |_, cx| { + Pane::handle_dropped_item(&pane, filler_index, cx) + }) } fn tab_details(&self, cx: &AppContext) -> Vec { @@ -1075,6 +1149,142 @@ impl Pane { tab_details } + + fn render_tab( + item: &Box, + pane: WeakViewHandle, + detail: Option, + hovered: bool, + overlay: Option, + tab_style: &theme::Tab, + cx: &mut RenderContext, + ) -> ElementBox { + let title = item.tab_content(detail, &tab_style, cx); + + let mut tab = Flex::row() + .with_child( + Align::new({ + let diameter = 7.0; + let icon_color = if item.has_conflict(cx) { + Some(tab_style.icon_conflict) + } else if item.is_dirty(cx) { + Some(tab_style.icon_dirty) + } else { + None + }; + + ConstrainedBox::new( + Canvas::new(move |bounds, _, cx| { + if let Some(color) = icon_color { + let square = RectF::new(bounds.origin(), vec2f(diameter, diameter)); + cx.scene.push_quad(Quad { + bounds: square, + background: Some(color), + border: Default::default(), + corner_radius: diameter / 2., + }); + } + }) + .boxed(), + ) + .with_width(diameter) + .with_height(diameter) + .boxed() + }) + .boxed(), + ) + .with_child( + Container::new(Align::new(title).boxed()) + .with_style(ContainerStyle { + margin: Margin { + left: tab_style.spacing, + right: tab_style.spacing, + ..Default::default() + }, + ..Default::default() + }) + .boxed(), + ) + .with_child( + Align::new( + ConstrainedBox::new(if hovered { + let item_id = item.id(); + enum TabCloseButton {} + let icon = Svg::new("icons/x_mark_thin_8.svg"); + MouseEventHandler::new::( + item_id, + cx, + |mouse_state, _| { + if mouse_state.hovered { + icon.with_color(tab_style.icon_close_active).boxed() + } else { + icon.with_color(tab_style.icon_close).boxed() + } + }, + ) + .with_padding(Padding::uniform(4.)) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, { + let pane = pane.clone(); + move |_, cx| { + cx.dispatch_action(CloseItem { + item_id, + pane: pane.clone(), + }) + } + }) + .on_click(MouseButton::Middle, |_, cx| cx.propogate_event()) + .named("close-tab-icon") + } else { + Empty::new().boxed() + }) + .with_width(tab_style.icon_width) + .boxed(), + ) + .boxed(), + ) + .contained() + .with_style(tab_style.container); + + if let Some(overlay) = overlay { + tab = tab.with_overlay_color(overlay); + } + + tab.constrained().with_height(tab_style.height).boxed() + } + + fn handle_dropped_item(pane: &WeakViewHandle, index: usize, cx: &mut EventContext) { + if let Some((_, dragged_item)) = cx + .global::>() + .currently_dragged::() + { + cx.dispatch_action(MoveItem { + item_id: dragged_item.item.id(), + from: dragged_item.pane.clone(), + to: pane.clone(), + destination_index: index, + }) + } else { + cx.propogate_event(); + } + } + + fn tab_overlay_color( + hovered: bool, + theme: &Theme, + cx: &mut RenderContext, + ) -> Option { + if hovered + && cx + .global::>() + .currently_dragged::() + .is_some() + { + Some(theme.workspace.tab_bar.drop_target_overlay_color) + } else { + None + } + } } impl Entity for Pane { @@ -1097,7 +1307,7 @@ impl View for Pane { Flex::column() .with_child({ let mut tab_row = Flex::row() - .with_child(self.render_tabs(cx).flex(1., true).named("tabs")); + .with_child(self.render_tab_bar(cx).flex(1., true).named("tabs")); if self.is_active { tab_row.add_children([ @@ -1124,12 +1334,11 @@ impl View for Pane { }, ) .with_cursor_style(CursorStyle::PointingHand) - .on_down( - MouseButton::Left, - |MouseButtonEvent { position, .. }, cx| { - cx.dispatch_action(DeployNewMenu { position }); - }, - ) + .on_down(MouseButton::Left, |e, cx| { + cx.dispatch_action(DeployNewMenu { + position: e.position, + }); + }) .boxed(), MouseEventHandler::new::( 1, @@ -1154,12 +1363,11 @@ impl View for Pane { }, ) .with_cursor_style(CursorStyle::PointingHand) - .on_down( - MouseButton::Left, - |MouseButtonEvent { position, .. }, cx| { - cx.dispatch_action(DeploySplitMenu { position }); - }, - ) + .on_down(MouseButton::Left, |e, cx| { + cx.dispatch_action(DeploySplitMenu { + position: e.position, + }); + }) .boxed(), ]) } @@ -1167,7 +1375,7 @@ impl View for Pane { tab_row .constrained() .with_height(cx.global::().theme.workspace.tab_bar.height) - .boxed() + .named("tab bar") }) .with_child(ChildView::new(&self.toolbar).boxed()) .with_child(ChildView::new(active_item).flex(1., true).boxed()) @@ -1324,3 +1532,252 @@ impl NavHistory { } } } + +#[cfg(test)] +mod tests { + use gpui::TestAppContext; + use project::FakeFs; + + use crate::tests::TestItem; + + use super::*; + + #[gpui::test] + async fn test_add_item_with_new_item(cx: &mut TestAppContext) { + cx.foreground().forbid_parking(); + Settings::test_async(cx); + let fs = FakeFs::new(cx.background()); + + let project = Project::test(fs, None, cx).await; + let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx)); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + // 1. Add with a destination index + // a. Add before the active item + set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx); + workspace.update(cx, |workspace, cx| { + Pane::add_item( + workspace, + &pane, + Box::new(cx.add_view(|_| TestItem::new().with_label("D"))), + false, + false, + Some(0), + cx, + ); + }); + assert_item_labels(&pane, ["D*", "A", "B", "C"], cx); + + // b. Add after the active item + set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx); + workspace.update(cx, |workspace, cx| { + Pane::add_item( + workspace, + &pane, + Box::new(cx.add_view(|_| TestItem::new().with_label("D"))), + false, + false, + Some(2), + cx, + ); + }); + assert_item_labels(&pane, ["A", "B", "D*", "C"], cx); + + // c. Add at the end of the item list (including off the length) + set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx); + workspace.update(cx, |workspace, cx| { + Pane::add_item( + workspace, + &pane, + Box::new(cx.add_view(|_| TestItem::new().with_label("D"))), + false, + false, + Some(5), + cx, + ); + }); + assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); + + // 2. Add without a destination index + // a. Add with active item at the start of the item list + set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx); + workspace.update(cx, |workspace, cx| { + Pane::add_item( + workspace, + &pane, + Box::new(cx.add_view(|_| TestItem::new().with_label("D"))), + false, + false, + None, + cx, + ); + }); + set_labeled_items(&workspace, &pane, ["A", "D*", "B", "C"], cx); + + // b. Add with active item at the end of the item list + set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx); + workspace.update(cx, |workspace, cx| { + Pane::add_item( + workspace, + &pane, + Box::new(cx.add_view(|_| TestItem::new().with_label("D"))), + false, + false, + None, + cx, + ); + }); + assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); + } + + #[gpui::test] + async fn test_add_item_with_existing_item(cx: &mut TestAppContext) { + cx.foreground().forbid_parking(); + Settings::test_async(cx); + let fs = FakeFs::new(cx.background()); + + let project = Project::test(fs, None, cx).await; + let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx)); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + // 1. Add with a destination index + // 1a. Add before the active item + let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx); + workspace.update(cx, |workspace, cx| { + Pane::add_item(workspace, &pane, d, false, false, Some(0), cx); + }); + assert_item_labels(&pane, ["D*", "A", "B", "C"], cx); + + // 1b. Add after the active item + let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx); + workspace.update(cx, |workspace, cx| { + Pane::add_item(workspace, &pane, d, false, false, Some(2), cx); + }); + assert_item_labels(&pane, ["A", "B", "D*", "C"], cx); + + // 1c. Add at the end of the item list (including off the length) + let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx); + workspace.update(cx, |workspace, cx| { + Pane::add_item(workspace, &pane, a, false, false, Some(5), cx); + }); + assert_item_labels(&pane, ["B", "C", "D", "A*"], cx); + + // 1d. Add same item to active index + let [_, b, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx); + workspace.update(cx, |workspace, cx| { + Pane::add_item(workspace, &pane, b, false, false, Some(1), cx); + }); + assert_item_labels(&pane, ["A", "B*", "C"], cx); + + // 1e. Add item to index after same item in last position + let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx); + workspace.update(cx, |workspace, cx| { + Pane::add_item(workspace, &pane, c, false, false, Some(2), cx); + }); + assert_item_labels(&pane, ["A", "B", "C*"], cx); + + // 2. Add without a destination index + // 2a. Add with active item at the start of the item list + let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A*", "B", "C", "D"], cx); + workspace.update(cx, |workspace, cx| { + Pane::add_item(workspace, &pane, d, false, false, None, cx); + }); + assert_item_labels(&pane, ["A", "D*", "B", "C"], cx); + + // 2b. Add with active item at the end of the item list + let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B", "C", "D*"], cx); + workspace.update(cx, |workspace, cx| { + Pane::add_item(workspace, &pane, a, false, false, None, cx); + }); + assert_item_labels(&pane, ["B", "C", "D", "A*"], cx); + + // 2c. Add active item to active item at end of list + let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx); + workspace.update(cx, |workspace, cx| { + Pane::add_item(workspace, &pane, c, false, false, None, cx); + }); + assert_item_labels(&pane, ["A", "B", "C*"], cx); + + // 2d. Add active item to active item at start of list + let [a, _, _] = set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx); + workspace.update(cx, |workspace, cx| { + Pane::add_item(workspace, &pane, a, false, false, None, cx); + }); + assert_item_labels(&pane, ["A*", "B", "C"], cx); + } + + fn set_labeled_items( + workspace: &ViewHandle, + pane: &ViewHandle, + labels: [&str; COUNT], + cx: &mut TestAppContext, + ) -> [Box>; COUNT] { + pane.update(cx, |pane, _| { + pane.items.clear(); + }); + + workspace.update(cx, |workspace, cx| { + let mut active_item_index = 0; + + let mut index = 0; + let items = labels.map(|mut label| { + if label.ends_with("*") { + label = label.trim_end_matches("*"); + active_item_index = index; + } + + let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label))); + Pane::add_item( + workspace, + pane, + labeled_item.clone(), + false, + false, + None, + cx, + ); + index += 1; + labeled_item + }); + + pane.update(cx, |pane, cx| { + pane.activate_item(active_item_index, false, false, cx) + }); + + items + }) + } + + // Assert the item label, with the active item label suffixed with a '*' + fn assert_item_labels( + pane: &ViewHandle, + expected_states: [&str; COUNT], + cx: &mut TestAppContext, + ) { + pane.read_with(cx, |pane, cx| { + let actual_states = pane + .items + .iter() + .enumerate() + .map(|(ix, item)| { + let mut state = item + .to_any() + .downcast::() + .unwrap() + .read(cx) + .label + .clone(); + if ix == pane.active_item_index { + state.push('*'); + } + state + }) + .collect::>(); + + assert_eq!( + actual_states, expected_states, + "pane items do not match expectation" + ); + }) + } +} diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index 6749de36a9710f9318aaf53edf4c4a1a7868d8bc..2071d3802bc3c3d526d03c7f0031995462c81de2 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -1,7 +1,7 @@ use crate::StatusItemView; use gpui::{ elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, AppContext, Entity, - MouseButton, MouseMovedEvent, RenderContext, Subscription, View, ViewContext, ViewHandle, + MouseButton, RenderContext, Subscription, View, ViewContext, ViewHandle, }; use serde::Deserialize; use settings::Settings; @@ -189,26 +189,18 @@ impl Sidebar { }) .with_cursor_style(CursorStyle::ResizeLeftRight) .on_down(MouseButton::Left, |_, _| {}) // This prevents the mouse down event from being propagated elsewhere - .on_drag( - MouseButton::Left, - move |old_position, - MouseMovedEvent { - position: new_position, - .. - }, - cx| { - let delta = new_position.x() - old_position.x(); - let prev_width = *actual_width.borrow(); - *custom_width.borrow_mut() = 0f32 - .max(match side { - Side::Left => prev_width + delta, - Side::Right => prev_width - delta, - }) - .round(); + .on_drag(MouseButton::Left, move |e, cx| { + let delta = e.position.x() - e.prev_mouse_position.x(); + let prev_width = *actual_width.borrow(); + *custom_width.borrow_mut() = 0f32 + .max(match side { + Side::Left => prev_width + delta, + Side::Right => prev_width - delta, + }) + .round(); - cx.notify(); - }, - ) + cx.notify(); + }) .boxed() } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index eb697b92cca10b3c9e784d84e72b7c9a6c58ee79..1261a89f959a8da2149c8998b27ecb3c2403abba 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -16,6 +16,7 @@ use client::{ }; use clock::ReplicaId; use collections::{hash_map, HashMap, HashSet}; +use drag_and_drop::DragAndDrop; use futures::{channel::oneshot, FutureExt}; use gpui::{ actions, @@ -901,6 +902,9 @@ impl Workspace { status_bar }); + let drag_and_drop = DragAndDrop::new(cx.weak_handle(), cx); + cx.set_global(drag_and_drop); + let mut this = Workspace { modal: None, weak_self, @@ -1444,8 +1448,8 @@ impl Workspace { } pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { - let pane = self.active_pane().clone(); - Pane::add_item(self, pane, item, true, true, cx); + let active_pane = self.active_pane().clone(); + Pane::add_item(self, &active_pane, item, true, true, None, cx); } pub fn open_path( @@ -1531,7 +1535,7 @@ impl Workspace { .map(|ix| (pane.clone(), ix)) }); if let Some((pane, ix)) = result { - pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, false, cx)); + pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx)); true } else { false @@ -1645,7 +1649,7 @@ impl Workspace { pane.read(cx).active_item().map(|item| { let new_pane = self.add_pane(cx); if let Some(clone) = item.clone_on_split(cx.as_mut()) { - Pane::add_item(self, new_pane.clone(), clone, true, true, cx); + Pane::add_item(self, &new_pane, clone, true, true, None, cx); } self.center.split(&pane, &new_pane, direction).unwrap(); cx.notify(); @@ -2081,11 +2085,11 @@ impl Workspace { } } - fn render_disconnected_overlay(&self, cx: &AppContext) -> Option { + fn render_disconnected_overlay(&self, cx: &mut RenderContext) -> Option { if self.project.read(cx).is_read_only() { - let theme = &cx.global::().theme; Some( - EventHandler::new( + MouseEventHandler::new::(0, cx, |_, cx| { + let theme = &cx.global::().theme; Label::new( "Your connection to the remote project has been lost.".to_string(), theme.workspace.disconnected_overlay.text.clone(), @@ -2093,9 +2097,9 @@ impl Workspace { .aligned() .contained() .with_style(theme.workspace.disconnected_overlay.container) - .boxed(), - ) - .capture_all::(0) + .boxed() + }) + .capture_all() .boxed(), ) } else { @@ -2388,7 +2392,7 @@ impl Workspace { } for (pane, item) in items_to_add { - Pane::add_item(self, pane.clone(), item.boxed_clone(), false, false, cx); + Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx); if pane == self.active_pane { pane.update(cx, |pane, cx| pane.focus_active_item(cx)); } @@ -2488,6 +2492,7 @@ impl View for Workspace { .with_background_color(theme.workspace.background) .boxed(), ) + .with_children(DragAndDrop::render(cx)) .with_children(self.render_disconnected_overlay(cx)) .named("workspace") } @@ -2999,7 +3004,7 @@ mod tests { let close_items = workspace.update(cx, |workspace, cx| { pane.update(cx, |pane, cx| { - pane.activate_item(1, true, true, false, cx); + pane.activate_item(1, true, true, cx); assert_eq!(pane.active_item().unwrap().id(), item2.id()); }); @@ -3101,7 +3106,7 @@ mod tests { workspace.add_item(Box::new(cx.add_view(|_| item.clone())), cx); } left_pane.update(cx, |pane, cx| { - pane.activate_item(2, true, true, false, cx); + pane.activate_item(2, true, true, cx); }); workspace @@ -3325,8 +3330,9 @@ mod tests { }); } - struct TestItem { + pub struct TestItem { state: String, + pub label: String, save_count: usize, save_as_count: usize, reload_count: usize, @@ -3340,7 +3346,7 @@ mod tests { tab_detail: Cell>, } - enum TestItemEvent { + pub enum TestItemEvent { Edit, } @@ -3348,6 +3354,7 @@ mod tests { fn clone(&self) -> Self { Self { state: self.state.clone(), + label: self.label.clone(), save_count: self.save_count, save_as_count: self.save_as_count, reload_count: self.reload_count, @@ -3364,9 +3371,10 @@ mod tests { } impl TestItem { - fn new() -> Self { + pub fn new() -> Self { Self { state: String::new(), + label: String::new(), save_count: 0, save_as_count: 0, reload_count: 0, @@ -3381,6 +3389,11 @@ mod tests { } } + pub fn with_label(mut self, state: &str) -> Self { + self.label = state.to_string(); + self + } + fn set_state(&mut self, state: String, cx: &mut ViewContext) { self.push_to_nav_history(cx); self.state = state; diff --git a/styles/package-lock.json b/styles/package-lock.json index 5499f1852cb4330467268dee6436b53589a90e9b..582f1c84968a5c1a25ddac5fd3c21ba907353c6d 100644 --- a/styles/package-lock.json +++ b/styles/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "styles", "version": "1.0.0", "license": "ISC", "dependencies": { diff --git a/styles/src/styleTree/components.ts b/styles/src/styleTree/components.ts index 10e70ba3ef15ea230e3b915ab3b1acdb78690ef5..f0d1dd84129f39a3da889eb7afed0b32bb0a60b5 100644 --- a/styles/src/styleTree/components.ts +++ b/styles/src/styleTree/components.ts @@ -94,3 +94,11 @@ export function popoverShadow(theme: Theme) { offset: [1, 2], }; } + +export function draggedShadow(theme: Theme) { + return { + blur: 6, + color: theme.shadow, + offset: [1, 2], + }; +} diff --git a/styles/src/styleTree/tabBar.ts b/styles/src/styleTree/tabBar.ts index 66da26d7ed7c3134a5a9b9a089ff78136e9d52de..1e50d7fff4820fc5611ad1f9a921aa6827280db2 100644 --- a/styles/src/styleTree/tabBar.ts +++ b/styles/src/styleTree/tabBar.ts @@ -1,5 +1,6 @@ import Theme from "../themes/common/theme"; -import { iconColor, text, border, backgroundColor } from "./components"; +import { withOpacity } from "../utils/color"; +import { iconColor, text, border, backgroundColor, draggedShadow } from "./components"; export default function tabBar(theme: Theme) { const height = 32; @@ -55,9 +56,23 @@ export default function tabBar(theme: Theme) { }, } + const draggedTab = { + ...activePaneActiveTab, + background: withOpacity(tab.background, 0.8), + border: { + ...tab.border, + top: false, + left: false, + right: false, + bottom: false, + }, + shadow: draggedShadow(theme), + } + return { height, background: backgroundColor(theme, 300), + dropTargetOverlayColor: withOpacity(theme.textColor.muted, 0.8), border: border(theme, "primary", { left: true, bottom: true, @@ -71,6 +86,7 @@ export default function tabBar(theme: Theme) { activeTab: inactivePaneActiveTab, inactiveTab: inactivePaneInactiveTab, }, + draggedTab, paneButton: { color: iconColor(theme, "secondary"), border: {