Detailed changes
@@ -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",
@@ -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::<ToggleOnline, _>(
@@ -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"] }
@@ -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<V: View> {
+ position: Vector2F,
+ region_offset: Vector2F,
+ payload: Rc<dyn Any + 'static>,
+ render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
+}
+
+impl<V: View> Clone for State<V> {
+ 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<V: View> {
+ parent: WeakViewHandle<V>,
+ currently_dragged: Option<State<V>>,
+}
+
+impl<V: View> DragAndDrop<V> {
+ pub fn new(parent: WeakViewHandle<V>, cx: &mut ViewContext<V>) -> Self {
+ // TODO: Figure out if detaching here would result in a memory leak
+ cx.observe_global::<Self, _>(|cx| {
+ if let Some(parent) = cx.global::<Self>().parent.upgrade(cx) {
+ parent.update(cx, |_, cx| cx.notify())
+ }
+ })
+ .detach();
+
+ Self {
+ parent,
+ currently_dragged: None,
+ }
+ }
+
+ pub fn currently_dragged<T: Any>(&self) -> Option<(Vector2F, Rc<T>)> {
+ self.currently_dragged.as_ref().and_then(
+ |State {
+ position, payload, ..
+ }| {
+ payload
+ .clone()
+ .downcast::<T>()
+ .ok()
+ .map(|payload| (position.clone(), payload))
+ },
+ )
+ }
+
+ pub fn dragging<T: Any>(
+ event: DragRegionEvent,
+ payload: Rc<T>,
+ cx: &mut EventContext,
+ render: Rc<impl 'static + Fn(&T, &mut RenderContext<V>) -> ElementBox>,
+ ) {
+ cx.update_global::<Self, _, _>(|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::<T>().unwrap(), cx)
+ }),
+ });
+
+ if let Some(parent) = this.parent.upgrade(cx) {
+ parent.update(cx, |_, cx| cx.notify())
+ }
+ });
+ }
+
+ pub fn render(cx: &mut RenderContext<V>) -> Option<ElementBox> {
+ let currently_dragged = cx.global::<Self>().currently_dragged.clone();
+
+ currently_dragged.map(
+ |State {
+ region_offset,
+ position,
+ payload,
+ render,
+ }| {
+ let position = position + region_offset;
+
+ MouseEventHandler::new::<Self, _, _>(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::<Self, _, _>(|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<V: View, P: Any>(
+ self,
+ payload: P,
+ render: impl 'static + Fn(&P, &mut RenderContext<V>) -> ElementBox,
+ ) -> Self
+ where
+ Self: Sized;
+}
+
+impl Draggable for MouseEventHandler {
+ fn as_draggable<V: View, P: Any>(
+ self,
+ payload: P,
+ render: impl 'static + Fn(&P, &mut RenderContext<V>) -> 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::<V>::dragging(e, payload, cx, render)
+ })
+ }
+}
@@ -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<MouseRegionId>,
- pub clicked_region_id: Option<MouseRegionId>,
- pub right_clicked_region_id: Option<MouseRegionId>,
+ pub clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
pub refreshing: bool,
}
@@ -4039,8 +4037,7 @@ pub struct RenderContext<'a, T: View> {
pub(crate) view_id: usize,
pub(crate) view_type: PhantomData<T>,
pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
- pub(crate) clicked_region_id: Option<MouseRegionId>,
- pub(crate) right_clicked_region_id: Option<MouseRegionId>,
+ pub(crate) clicked_region_ids: Option<(Vec<MouseRegionId>, 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<MouseButton>,
}
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);
@@ -24,6 +24,8 @@ pub struct ContainerStyle {
pub padding: Padding,
#[serde(rename = "background")]
pub background_color: Option<Color>,
+ #[serde(rename = "overlay")]
+ pub overlay_color: Option<Color>,
#[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();
+ }
}
}
@@ -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 => {
@@ -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<CursorStyle>,
handlers: HandlerSet,
+ hoverable: bool,
padding: Padding,
}
@@ -33,6 +36,7 @@ impl MouseEventHandler {
cursor_style: None,
discriminant: (TypeId::of::<Tag>(), 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);
}
@@ -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::<MouseEventHandlerState<Tag>, _, _>(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);
@@ -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<FontCache>,
text_layout_cache: TextLayoutCache,
asset_cache: Arc<AssetCache>,
- last_mouse_moved_event: Option<MouseMovedEvent>,
+ last_mouse_moved_event: Option<Event>,
hovered_region_ids: HashSet<MouseRegionId>,
- clicked_region: Option<MouseRegion>,
- right_clicked_region: Option<MouseRegion>,
- prev_drag_position: Option<Vector2F>,
+ clicked_regions: Vec<MouseRegion>,
+ clicked_button: Option<MouseButton>,
+ 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<usize> {
- // 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<usize>) {
- // 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<usize> = 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<usize>,
- 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<usize>,
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<MouseRegionId>,
- clicked_region_id: Option<MouseRegionId>,
- right_clicked_region_id: Option<MouseRegionId>,
+ clicked_region_ids: Option<(Vec<MouseRegionId>, 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<usize>,
- invalidated_views: HashSet<usize>,
+ handled: bool,
+ invalidated_views: &'a mut HashSet<usize>,
}
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> {
@@ -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<usize, ElementBox>,
+ 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<usize>,
+ pub(crate) handled: bool,
+ pub(crate) invalidated_views: HashSet<usize>,
+}
+
+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<F, T>(&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<usize> {
+ 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<dyn Action>) {
+ self.app
+ .dispatch_any_action_at(self.window_id, *self.view_stack.last().unwrap(), action)
+ }
+
+ pub fn dispatch_action<A: 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
+ }
+}
@@ -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<FontCache>,
+
+ last_mouse_moved_event: Option<Event>,
+ cursor_regions: Vec<CursorRegion>,
+ mouse_regions: Vec<(MouseRegion, usize)>,
+ clicked_regions: Vec<MouseRegion>,
+ clicked_button: Option<MouseButton>,
+ mouse_position: Vector2F,
+ hovered_region_ids: HashSet<MouseRegionId>,
+}
+
+impl EventDispatcher {
+ pub fn new(window_id: usize, font_cache: Arc<FontCache>) -> 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<MouseRegionId>, MouseButton)> {
+ self.clicked_button.map(|button| {
+ (
+ self.clicked_regions
+ .iter()
+ .filter_map(MouseRegion::id)
+ .collect(),
+ button,
+ )
+ })
+ }
+
+ pub fn hovered_region_ids(&self) -> HashSet<MouseRegionId> {
+ 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<usize> = 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<MouseRegionEvent> {
+ 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<MouseRegion> {
+ 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
+ }
+}
@@ -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,
@@ -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<MouseRegionEvent>, Option<MouseButton>),
@@ -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<MouseRegionEvent> {
- std::mem::discriminant(&MouseRegionEvent::Move(Default::default()))
- }
- pub fn drag_disc() -> Discriminant<MouseRegionEvent> {
- std::mem::discriminant(&MouseRegionEvent::Drag(
- Default::default(),
- Default::default(),
- ))
- }
- pub fn hover_disc() -> Discriminant<MouseRegionEvent> {
- std::mem::discriminant(&MouseRegionEvent::Hover(
- Default::default(),
- Default::default(),
- ))
- }
- pub fn down_disc() -> Discriminant<MouseRegionEvent> {
- std::mem::discriminant(&MouseRegionEvent::Down(Default::default()))
- }
- pub fn up_disc() -> Discriminant<MouseRegionEvent> {
- std::mem::discriminant(&MouseRegionEvent::Up(Default::default()))
- }
- pub fn click_disc() -> Discriminant<MouseRegionEvent> {
- std::mem::discriminant(&MouseRegionEvent::Click(Default::default()))
- }
- pub fn down_out_disc() -> Discriminant<MouseRegionEvent> {
- std::mem::discriminant(&MouseRegionEvent::DownOut(Default::default()))
- }
- pub fn scroll_wheel_disc() -> Discriminant<MouseRegionEvent> {
- std::mem::discriminant(&MouseRegionEvent::ScrollWheel(Default::default()))
- }
-
- pub fn handler_key(&self) -> (Discriminant<MouseRegionEvent>, Option<MouseButton>) {
- 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),
- }
- }
-}
@@ -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<MouseRegionEvent> {
+ discriminant(&MouseRegionEvent::Move(Default::default()))
+ }
+
+ pub fn drag_disc() -> Discriminant<MouseRegionEvent> {
+ discriminant(&MouseRegionEvent::Drag(Default::default()))
+ }
+
+ pub fn hover_disc() -> Discriminant<MouseRegionEvent> {
+ discriminant(&MouseRegionEvent::Hover(Default::default()))
+ }
+
+ pub fn down_disc() -> Discriminant<MouseRegionEvent> {
+ discriminant(&MouseRegionEvent::Down(Default::default()))
+ }
+
+ pub fn up_disc() -> Discriminant<MouseRegionEvent> {
+ discriminant(&MouseRegionEvent::Up(Default::default()))
+ }
+
+ pub fn up_out_disc() -> Discriminant<MouseRegionEvent> {
+ discriminant(&MouseRegionEvent::UpOut(Default::default()))
+ }
+
+ pub fn click_disc() -> Discriminant<MouseRegionEvent> {
+ discriminant(&MouseRegionEvent::Click(Default::default()))
+ }
+
+ pub fn down_out_disc() -> Discriminant<MouseRegionEvent> {
+ discriminant(&MouseRegionEvent::DownOut(Default::default()))
+ }
+
+ pub fn scroll_wheel_disc() -> Discriminant<MouseRegionEvent> {
+ discriminant(&MouseRegionEvent::ScrollWheel(Default::default()))
+ }
+
+ pub fn handler_key(&self) -> (Discriminant<MouseRegionEvent>, Option<MouseButton>) {
+ 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),
+ }
+ }
+}
@@ -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())
@@ -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);
@@ -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<E>(
connection: WeakModelHandle<Terminal>,
origin: Vector2F,
- f: impl Fn(&mut Terminal, Vector2F, MouseButtonEvent, &mut ModelContext<Terminal>),
- ) -> impl Fn(MouseButtonEvent, &mut EventContext) {
+ f: impl Fn(&mut Terminal, Vector2F, E, &mut ModelContext<Terminal>),
+ ) -> 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
@@ -75,7 +75,25 @@ pub struct TabBar {
pub pane_button: Interactive<IconButton>,
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)]
@@ -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" }
@@ -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<Pane>,
}
+#[derive(Clone, PartialEq)]
+pub struct MoveItem {
+ pub item_id: usize,
+ pub from: WeakViewHandle<Pane>,
+ pub to: WeakViewHandle<Pane>,
+ 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<Box<dyn Any>>,
}
+struct DraggedItem {
+ item: Box<dyn ItemHandle>,
+ pane: WeakViewHandle<Pane>,
+}
+
+pub enum ReorderBehavior {
+ None,
+ MoveAfterActive,
+ MoveToIndex(usize),
+}
+
impl Pane {
pub fn new(cx: &mut ViewContext<Self>) -> 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<Pane>) -> Box<dyn ItemHandle>,
) -> Box<dyn ItemHandle> {
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>,
+ pane: &ViewHandle<Pane>,
item: Box<dyn ItemHandle>,
activate_pane: bool,
focus_item: bool,
+ destination_index: Option<usize>,
cx: &mut ViewContext<Workspace>,
) {
- // 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<Item = &Box<dyn ItemHandle>> {
@@ -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<Self>,
) {
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<Self>) {
@@ -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<Self>) {
+ 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<Project>,
pane: &ViewHandle<Pane>,
@@ -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<Pane>,
+ to: ViewHandle<Pane>,
+ item_to_move: usize,
+ destination_index: usize,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ 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<Self>) {
cx.emit(Event::Split(direction));
}
@@ -876,144 +985,58 @@ impl Pane {
});
}
- fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
+ fn render_tab_bar(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
let theme = cx.global::<Settings>().theme.clone();
+ let filler_index = self.items.len();
enum Tabs {}
enum Tab {}
+ enum Filler {}
let pane = cx.handle();
- MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
+ MouseEventHandler::new::<Tabs, _, _>(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::<Tabs, _>(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::<Tab, _, _>(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::<TabCloseButton, _, _>(
- 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::<Tab, _, _>(ix, cx, {
+ let item = item.clone();
+ let pane = pane.clone();
+ let detail = detail.clone();
+
+ let theme = cx.global::<Settings>().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::<Settings>().theme.clone();
+
+ let detail = detail.clone();
+ move |dragged_item, cx: &mut RenderContext<Workspace>| {
+ 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::<Filler, _, _>(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<usize> {
@@ -1075,6 +1149,142 @@ impl Pane {
tab_details
}
+
+ fn render_tab<V: View>(
+ item: &Box<dyn ItemHandle>,
+ pane: WeakViewHandle<Pane>,
+ detail: Option<usize>,
+ hovered: bool,
+ overlay: Option<Color>,
+ tab_style: &theme::Tab,
+ cx: &mut RenderContext<V>,
+ ) -> 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::<TabCloseButton, _, _>(
+ 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<Pane>, index: usize, cx: &mut EventContext) {
+ if let Some((_, dragged_item)) = cx
+ .global::<DragAndDrop<Workspace>>()
+ .currently_dragged::<DraggedItem>()
+ {
+ 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<Self>,
+ ) -> Option<Color> {
+ if hovered
+ && cx
+ .global::<DragAndDrop<Workspace>>()
+ .currently_dragged::<DraggedItem>()
+ .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::<SplitIcon, _, _>(
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::<Settings>().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<const COUNT: usize>(
+ workspace: &ViewHandle<Workspace>,
+ pane: &ViewHandle<Pane>,
+ labels: [&str; COUNT],
+ cx: &mut TestAppContext,
+ ) -> [Box<ViewHandle<TestItem>>; 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<const COUNT: usize>(
+ pane: &ViewHandle<Pane>,
+ 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::<TestItem>()
+ .unwrap()
+ .read(cx)
+ .label
+ .clone();
+ if ix == pane.active_item_index {
+ state.push('*');
+ }
+ state
+ })
+ .collect::<Vec<_>>();
+
+ assert_eq!(
+ actual_states, expected_states,
+ "pane items do not match expectation"
+ );
+ })
+ }
+}
@@ -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()
}
}
@@ -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<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
- 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<ElementBox> {
+ fn render_disconnected_overlay(&self, cx: &mut RenderContext<Workspace>) -> Option<ElementBox> {
if self.project.read(cx).is_read_only() {
- let theme = &cx.global::<Settings>().theme;
Some(
- EventHandler::new(
+ MouseEventHandler::new::<Workspace, _, _>(0, cx, |_, cx| {
+ let theme = &cx.global::<Settings>().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::<Self>(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<Option<usize>>,
}
- 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>) {
self.push_to_nav_history(cx);
self.state = state;
@@ -5,6 +5,7 @@
"requires": true,
"packages": {
"": {
+ "name": "styles",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
@@ -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],
+ };
+}
@@ -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: {