WIP

Antonio Scandurra created

Change summary

crates/gpui2/src/app.rs                     |  19 
crates/gpui2/src/color.rs                   |   9 
crates/gpui2/src/elements/div.rs            |   6 
crates/gpui2/src/elements/img.rs            |   8 
crates/gpui2/src/elements/svg.rs            |   8 
crates/gpui2/src/geometry.rs                |   4 
crates/gpui2/src/interactive.rs             | 347 ++++++++++++++++++----
crates/gpui2/src/taffy.rs                   |   4 
crates/gpui2/src/window.rs                  |  97 ++++--
crates/ui2/src/color.rs                     | 253 ++++++++++++++++
crates/ui2/src/components/breadcrumb.rs     |   1 
crates/ui2/src/components/buffer.rs         |   3 
crates/ui2/src/components/collab_panel.rs   |   1 
crates/ui2/src/components/list.rs           |   3 
crates/ui2/src/components/panes.rs          |   1 
crates/ui2/src/components/player_stack.rs   |   4 
crates/ui2/src/components/project_panel.rs  |   1 
crates/ui2/src/components/tab.rs            |  18 +
crates/ui2/src/components/title_bar.rs      |   1 
crates/ui2/src/components/traffic_lights.rs |   8 
crates/ui2/src/elements/input.rs            |   3 
crates/ui2/src/lib.rs                       |  21 +
crates/ui2/src/prelude.rs                   | 276 ------------------
23 files changed, 678 insertions(+), 418 deletions(-)

Detailed changes

crates/gpui2/src/app.rs 🔗

@@ -9,11 +9,11 @@ use refineable::Refineable;
 use smallvec::SmallVec;
 
 use crate::{
-    current_platform, image_cache::ImageCache, Action, AppMetadata, AssetSource, ClipboardItem,
-    Context, DispatchPhase, DisplayId, Executor, FocusEvent, FocusHandle, FocusId, KeyBinding,
-    Keymap, LayoutId, MainThread, MainThreadOnly, Platform, SharedString, SubscriberSet,
-    Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, Window,
-    WindowContext, WindowHandle, WindowId,
+    current_platform, image_cache::ImageCache, Action, AnyBox, AnyView, AppMetadata, AssetSource,
+    ClipboardItem, Context, DispatchPhase, DisplayId, Executor, FocusEvent, FocusHandle, FocusId,
+    KeyBinding, Keymap, LayoutId, MainThread, MainThreadOnly, Pixels, Platform, Point,
+    SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement,
+    TextSystem, View, Window, WindowContext, WindowHandle, WindowId,
 };
 use anyhow::{anyhow, Result};
 use collections::{HashMap, HashSet, VecDeque};
@@ -93,6 +93,7 @@ impl App {
                 quit_observers: SubscriberSet::new(),
                 layout_id_buffer: Default::default(),
                 propagate_event: true,
+                active_drag: None,
             })
         }))
     }
@@ -171,6 +172,7 @@ pub struct AppContext {
     text_system: Arc<TextSystem>,
     flushing_effects: bool,
     pending_updates: usize,
+    pub(crate) active_drag: Option<AnyDrag>,
     pub(crate) next_frame_callbacks: HashMap<DisplayId, Vec<FrameCallback>>,
     pub(crate) executor: Executor,
     pub(crate) svg_renderer: SvgRenderer,
@@ -766,6 +768,13 @@ pub(crate) enum Effect {
     },
 }
 
+pub(crate) struct AnyDrag {
+    pub drag_handle_view: AnyView,
+    pub cursor_offset: Point<Pixels>,
+    pub state: AnyBox,
+    pub state_type: TypeId,
+}
+
 #[cfg(test)]
 mod tests {
     use super::AppContext;

crates/gpui2/src/color.rs 🔗

@@ -155,6 +155,15 @@ pub fn white() -> Hsla {
     }
 }
 
+pub fn red() -> Hsla {
+    Hsla {
+        h: 0.,
+        s: 1.,
+        l: 0.5,
+        a: 1.,
+    }
+}
+
 impl Hsla {
     /// Returns true if the HSLA color is fully transparent, false otherwise.
     pub fn is_transparent(&self) -> bool {

crates/gpui2/src/elements/div.rs 🔗

@@ -298,7 +298,7 @@ where
                     style.apply_text_style(cx, |cx| {
                         style.apply_overflow(bounds, cx, |cx| {
                             let scroll_offset = element_state.interactive.scroll_offset();
-                            cx.with_scroll_offset(scroll_offset, |cx| {
+                            cx.with_element_offset(scroll_offset, |cx| {
                                 for child in &mut this.children {
                                     child.paint(view_state, cx);
                                 }
@@ -354,7 +354,7 @@ where
     F: ElementFocus<V>,
     V: 'static + Send + Sync,
 {
-    fn stateless_interactivity(&mut self) -> &mut StatelessInteraction<V> {
+    fn stateless_interaction(&mut self) -> &mut StatelessInteraction<V> {
         self.interaction.as_stateless_mut()
     }
 }
@@ -364,7 +364,7 @@ where
     F: ElementFocus<V>,
     V: 'static + Send + Sync,
 {
-    fn stateful_interactivity(&mut self) -> &mut StatefulInteraction<Self::ViewState> {
+    fn stateful_interaction(&mut self) -> &mut StatefulInteraction<Self::ViewState> {
         &mut self.interaction
     }
 }

crates/gpui2/src/elements/img.rs 🔗

@@ -156,8 +156,8 @@ where
     I: ElementInteraction<V>,
     F: ElementFocus<V>,
 {
-    fn stateless_interactivity(&mut self) -> &mut StatelessInteraction<V> {
-        self.base.stateless_interactivity()
+    fn stateless_interaction(&mut self) -> &mut StatelessInteraction<V> {
+        self.base.stateless_interaction()
     }
 }
 
@@ -166,8 +166,8 @@ where
     V: 'static + Send + Sync,
     F: ElementFocus<V>,
 {
-    fn stateful_interactivity(&mut self) -> &mut StatefulInteraction<Self::ViewState> {
-        self.base.stateful_interactivity()
+    fn stateful_interaction(&mut self) -> &mut StatefulInteraction<Self::ViewState> {
+        self.base.stateful_interaction()
     }
 }
 

crates/gpui2/src/elements/svg.rs 🔗

@@ -130,8 +130,8 @@ where
     I: ElementInteraction<V>,
     F: ElementFocus<V>,
 {
-    fn stateless_interactivity(&mut self) -> &mut StatelessInteraction<V> {
-        self.base.stateless_interactivity()
+    fn stateless_interaction(&mut self) -> &mut StatelessInteraction<V> {
+        self.base.stateless_interaction()
     }
 }
 
@@ -140,8 +140,8 @@ where
     V: 'static + Send + Sync,
     F: ElementFocus<V>,
 {
-    fn stateful_interactivity(&mut self) -> &mut StatefulInteraction<Self::ViewState> {
-        self.base.stateful_interactivity()
+    fn stateful_interaction(&mut self) -> &mut StatefulInteraction<Self::ViewState> {
+        self.base.stateful_interaction()
     }
 }
 

crates/gpui2/src/geometry.rs 🔗

@@ -40,6 +40,10 @@ impl Point<Pixels> {
             y: self.y.scale(factor),
         }
     }
+
+    pub fn magnitude(&self) -> f64 {
+        ((self.x.0.powi(2) + self.y.0.powi(2)) as f64).sqrt()
+    }
 }
 
 impl<T, Rhs> Mul<Rhs> for Point<T>

crates/gpui2/src/interactive.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
-    point, px, Action, AppContext, BorrowWindow, Bounds, DispatchContext, DispatchPhase, Element,
-    ElementId, FocusHandle, KeyMatch, Keystroke, Modifiers, Overflow, Pixels, Point, SharedString,
-    Size, Style, StyleRefinement, ViewContext,
+    point, px, view, Action, AnyBox, AnyDrag, AppContext, BorrowWindow, Bounds, DispatchContext,
+    DispatchPhase, Element, ElementId, FocusHandle, KeyMatch, Keystroke, Modifiers, Overflow,
+    Pixels, Point, SharedString, Size, Style, StyleRefinement, ViewContext,
 };
 use collections::HashMap;
 use derive_more::{Deref, DerefMut};
@@ -11,18 +11,21 @@ use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
     fmt::Debug,
+    marker::PhantomData,
     ops::Deref,
     sync::Arc,
 };
 
+const DRAG_THRESHOLD: f64 = 2.;
+
 pub trait StatelessInteractive: Element {
-    fn stateless_interactivity(&mut self) -> &mut StatelessInteraction<Self::ViewState>;
+    fn stateless_interaction(&mut self) -> &mut StatelessInteraction<Self::ViewState>;
 
     fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
     where
         Self: Sized,
     {
-        self.stateless_interactivity().hover_style = f(StyleRefinement::default());
+        self.stateless_interaction().hover_style = f(StyleRefinement::default());
         self
     }
 
@@ -34,7 +37,7 @@ pub trait StatelessInteractive: Element {
     where
         Self: Sized,
     {
-        self.stateless_interactivity().group_hover_style = Some(GroupStyle {
+        self.stateless_interaction().group_hover_style = Some(GroupStyle {
             group: group_name.into(),
             style: f(StyleRefinement::default()),
         });
@@ -52,7 +55,7 @@ pub trait StatelessInteractive: Element {
     where
         Self: Sized,
     {
-        self.stateless_interactivity()
+        self.stateless_interaction()
             .mouse_down_listeners
             .push(Arc::new(move |view, event, bounds, phase, cx| {
                 if phase == DispatchPhase::Bubble
@@ -76,7 +79,7 @@ pub trait StatelessInteractive: Element {
     where
         Self: Sized,
     {
-        self.stateless_interactivity()
+        self.stateless_interaction()
             .mouse_up_listeners
             .push(Arc::new(move |view, event, bounds, phase, cx| {
                 if phase == DispatchPhase::Bubble
@@ -100,7 +103,7 @@ pub trait StatelessInteractive: Element {
     where
         Self: Sized,
     {
-        self.stateless_interactivity()
+        self.stateless_interaction()
             .mouse_down_listeners
             .push(Arc::new(move |view, event, bounds, phase, cx| {
                 if phase == DispatchPhase::Capture
@@ -124,7 +127,7 @@ pub trait StatelessInteractive: Element {
     where
         Self: Sized,
     {
-        self.stateless_interactivity()
+        self.stateless_interaction()
             .mouse_up_listeners
             .push(Arc::new(move |view, event, bounds, phase, cx| {
                 if phase == DispatchPhase::Capture
@@ -147,7 +150,7 @@ pub trait StatelessInteractive: Element {
     where
         Self: Sized,
     {
-        self.stateless_interactivity()
+        self.stateless_interaction()
             .mouse_move_listeners
             .push(Arc::new(move |view, event, bounds, phase, cx| {
                 if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
@@ -167,7 +170,7 @@ pub trait StatelessInteractive: Element {
     where
         Self: Sized,
     {
-        self.stateless_interactivity()
+        self.stateless_interaction()
             .scroll_wheel_listeners
             .push(Arc::new(move |view, event, bounds, phase, cx| {
                 if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
@@ -183,7 +186,7 @@ pub trait StatelessInteractive: Element {
         C: TryInto<DispatchContext>,
         C::Error: Debug,
     {
-        self.stateless_interactivity().dispatch_context =
+        self.stateless_interaction().dispatch_context =
             context.try_into().expect("invalid dispatch context");
         self
     }
@@ -198,7 +201,7 @@ pub trait StatelessInteractive: Element {
     where
         Self: Sized,
     {
-        self.stateless_interactivity().key_listeners.push((
+        self.stateless_interaction().key_listeners.push((
             TypeId::of::<A>(),
             Arc::new(move |view, event, _, phase, cx| {
                 let event = event.downcast_ref().unwrap();
@@ -223,7 +226,7 @@ pub trait StatelessInteractive: Element {
     where
         Self: Sized,
     {
-        self.stateless_interactivity().key_listeners.push((
+        self.stateless_interaction().key_listeners.push((
             TypeId::of::<KeyDownEvent>(),
             Arc::new(move |view, event, _, phase, cx| {
                 let event = event.downcast_ref().unwrap();
@@ -244,7 +247,7 @@ pub trait StatelessInteractive: Element {
     where
         Self: Sized,
     {
-        self.stateless_interactivity().key_listeners.push((
+        self.stateless_interaction().key_listeners.push((
             TypeId::of::<KeyUpEvent>(),
             Arc::new(move |view, event, _, phase, cx| {
                 let event = event.downcast_ref().unwrap();
@@ -254,16 +257,63 @@ pub trait StatelessInteractive: Element {
         ));
         self
     }
+
+    fn drag_over<S: 'static>(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.stateless_interaction()
+            .drag_over_styles
+            .push((TypeId::of::<S>(), f(StyleRefinement::default())));
+        self
+    }
+
+    fn group_drag_over<S: 'static>(
+        mut self,
+        group_name: impl Into<SharedString>,
+        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.stateless_interaction().group_drag_over_styles.push((
+            TypeId::of::<S>(),
+            GroupStyle {
+                group: group_name.into(),
+                style: f(StyleRefinement::default()),
+            },
+        ));
+        self
+    }
+
+    fn on_drop<S: 'static>(
+        mut self,
+        listener: impl Fn(&mut Self::ViewState, S, &mut ViewContext<Self::ViewState>)
+            + Send
+            + Sync
+            + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.stateless_interaction().drop_listeners.push((
+            TypeId::of::<S>(),
+            Arc::new(move |view, drag_state, cx| {
+                listener(view, *drag_state.downcast().unwrap(), cx);
+            }),
+        ));
+        self
+    }
 }
 
 pub trait StatefulInteractive: StatelessInteractive {
-    fn stateful_interactivity(&mut self) -> &mut StatefulInteraction<Self::ViewState>;
+    fn stateful_interaction(&mut self) -> &mut StatefulInteraction<Self::ViewState>;
 
     fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
     where
         Self: Sized,
     {
-        self.stateful_interactivity().active_style = f(StyleRefinement::default());
+        self.stateful_interaction().active_style = f(StyleRefinement::default());
         self
     }
 
@@ -275,7 +325,7 @@ pub trait StatefulInteractive: StatelessInteractive {
     where
         Self: Sized,
     {
-        self.stateful_interactivity().group_active_style = Some(GroupStyle {
+        self.stateful_interaction().group_active_style = Some(GroupStyle {
             group: group_name.into(),
             style: f(StyleRefinement::default()),
         });
@@ -284,17 +334,55 @@ pub trait StatefulInteractive: StatelessInteractive {
 
     fn on_click(
         mut self,
-        handler: impl Fn(&mut Self::ViewState, &MouseClickEvent, &mut ViewContext<Self::ViewState>)
+        listener: impl Fn(&mut Self::ViewState, &ClickEvent, &mut ViewContext<Self::ViewState>)
+            + Send
+            + Sync
+            + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.stateful_interaction()
+            .click_listeners
+            .push(Arc::new(move |view, event, cx| listener(view, event, cx)));
+        self
+    }
+
+    fn on_drag<S, R, E>(
+        mut self,
+        listener: impl Fn(
+                &mut Self::ViewState,
+                &mut ViewContext<Self::ViewState>,
+            ) -> Drag<S, R, Self::ViewState, E>
             + Send
             + Sync
             + 'static,
     ) -> Self
     where
         Self: Sized,
+        S: 'static + Send + Sync,
+        R: 'static + Fn(&mut Self::ViewState, &mut ViewContext<Self::ViewState>) -> E + Send + Sync,
+        E: Element<ViewState = Self::ViewState>,
     {
-        self.stateful_interactivity()
-            .mouse_click_listeners
-            .push(Arc::new(move |view, event, cx| handler(view, event, cx)));
+        debug_assert!(
+            self.stateful_interaction().drag_listener.is_none(),
+            "calling on_drag more than once on the same element is not supported"
+        );
+        self.stateful_interaction().drag_listener =
+            Some(Arc::new(move |view_state, cursor_offset, cx| {
+                let drag = listener(view_state, cx);
+                let view_handle = cx.handle().upgrade().unwrap();
+                let drag_handle_view = view(view_handle, move |view_state, cx| {
+                    (drag.render_drag_handle)(view_state, cx)
+                })
+                .into_any();
+                AnyDrag {
+                    drag_handle_view,
+                    cursor_offset,
+                    state: Box::new(drag.state),
+                    state_type: TypeId::of::<S>(),
+                }
+            }));
         self
     }
 }
@@ -359,6 +447,26 @@ pub trait ElementInteraction<V: 'static + Send + Sync>: 'static + Send + Sync {
             style.refine(&stateless.hover_style);
         }
 
+        if let Some(drag) = cx.active_drag.take() {
+            for (state_type, group_drag_style) in &self.as_stateless().group_drag_over_styles {
+                if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
+                    if *state_type == drag.state_type
+                        && group_bounds.contains_point(&mouse_position)
+                    {
+                        style.refine(&group_drag_style.style);
+                    }
+                }
+            }
+
+            for (state_type, drag_over_style) in &self.as_stateless().drag_over_styles {
+                if *state_type == drag.state_type && bounds.contains_point(&mouse_position) {
+                    style.refine(drag_over_style);
+                }
+            }
+
+            cx.active_drag = Some(drag);
+        }
+
         if let Some(stateful) = self.as_stateful() {
             let active_state = element_state.active_state.lock();
             if active_state.group {
@@ -411,38 +519,104 @@ pub trait ElementInteraction<V: 'static + Send + Sync>: 'static + Send + Sync {
             .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx));
 
         if let Some(group_bounds) = hover_group_bounds {
-            paint_hover_listener(group_bounds, cx);
+            let hovered = group_bounds.contains_point(&cx.mouse_position());
+            cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
+                if phase == DispatchPhase::Capture {
+                    if group_bounds.contains_point(&event.position) != hovered {
+                        cx.notify();
+                    }
+                }
+            });
         }
 
-        if stateless.hover_style.is_some() {
-            paint_hover_listener(bounds, cx);
+        if stateless.hover_style.is_some()
+            || (cx.active_drag.is_some() && !stateless.drag_over_styles.is_empty())
+        {
+            let hovered = bounds.contains_point(&cx.mouse_position());
+            cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
+                if phase == DispatchPhase::Capture {
+                    if bounds.contains_point(&event.position) != hovered {
+                        cx.notify();
+                    }
+                }
+            });
         }
 
-        if let Some(stateful) = self.as_stateful() {
-            let click_listeners = stateful.mouse_click_listeners.clone();
-
-            let pending_click = element_state.pending_click.clone();
-            let mouse_down = pending_click.lock().clone();
-            if let Some(mouse_down) = mouse_down {
-                cx.on_mouse_event(move |state, event: &MouseUpEvent, phase, cx| {
-                    if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
-                        let mouse_click = MouseClickEvent {
-                            down: mouse_down.clone(),
-                            up: event.clone(),
-                        };
-                        for listener in &click_listeners {
-                            listener(state, &mouse_click, cx);
+        if cx.active_drag.is_some() {
+            let drop_listeners = stateless.drop_listeners.clone();
+            cx.on_mouse_event(move |view, event: &MouseUpEvent, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    if let Some(drag_state_type) =
+                        cx.active_drag.as_ref().map(|drag| drag.state_type)
+                    {
+                        for (drop_state_type, listener) in &drop_listeners {
+                            if *drop_state_type == drag_state_type {
+                                let drag = cx
+                                    .active_drag
+                                    .take()
+                                    .expect("checked for type drag state type above");
+                                listener(view, drag.state, cx);
+                                cx.notify();
+                                cx.stop_propagation();
+                            }
                         }
                     }
+                }
+            });
+        }
 
-                    *pending_click.lock() = None;
-                });
-            } else {
-                cx.on_mouse_event(move |_state, event: &MouseDownEvent, phase, _cx| {
-                    if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
-                        *pending_click.lock() = Some(event.clone());
+        if let Some(stateful) = self.as_stateful() {
+            let click_listeners = stateful.click_listeners.clone();
+            let drag_listener = stateful.drag_listener.clone();
+
+            if !click_listeners.is_empty() || drag_listener.is_some() {
+                let pending_mouse_down = element_state.pending_mouse_down.clone();
+                let mouse_down = pending_mouse_down.lock().clone();
+                if let Some(mouse_down) = mouse_down {
+                    if let Some(drag_listener) = drag_listener {
+                        let active_state = element_state.active_state.clone();
+
+                        cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
+                            if cx.active_drag.is_some() {
+                                if phase == DispatchPhase::Capture {
+                                    cx.notify();
+                                }
+                            } else if phase == DispatchPhase::Bubble
+                                && bounds.contains_point(&event.position)
+                                && (event.position - mouse_down.position).magnitude()
+                                    > DRAG_THRESHOLD
+                            {
+                                *active_state.lock() = ActiveState::default();
+                                let cursor_offset = event.position - bounds.origin;
+                                let drag = drag_listener(view_state, cursor_offset, cx);
+                                cx.active_drag = Some(drag);
+                                cx.notify();
+                                cx.stop_propagation();
+                            }
+                        });
                     }
-                });
+
+                    cx.on_mouse_event(move |view_state, event: &MouseUpEvent, phase, cx| {
+                        if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position)
+                        {
+                            let mouse_click = ClickEvent {
+                                down: mouse_down.clone(),
+                                up: event.clone(),
+                            };
+                            for listener in &click_listeners {
+                                listener(view_state, &mouse_click, cx);
+                            }
+                        }
+                        *pending_mouse_down.lock() = None;
+                    });
+                } else {
+                    cx.on_mouse_event(move |_state, event: &MouseDownEvent, phase, _cx| {
+                        if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position)
+                        {
+                            *pending_mouse_down.lock() = Some(event.clone());
+                        }
+                    });
+                }
             }
 
             let active_state = element_state.active_state.clone();
@@ -487,12 +661,12 @@ pub trait ElementInteraction<V: 'static + Send + Sync>: 'static + Send + Sync {
 
                         if overflow.x == Overflow::Scroll {
                             scroll_offset.x =
-                                (scroll_offset.x - delta.x).clamp(px(0.), scroll_max.width);
+                                (scroll_offset.x + delta.x).clamp(-scroll_max.width, px(0.));
                         }
 
                         if overflow.y == Overflow::Scroll {
                             scroll_offset.y =
-                                (scroll_offset.y - delta.y).clamp(px(0.), scroll_max.height);
+                                (scroll_offset.y + delta.y).clamp(-scroll_max.height, px(0.));
                         }
 
                         if *scroll_offset != old_scroll_offset {
@@ -506,29 +680,16 @@ pub trait ElementInteraction<V: 'static + Send + Sync>: 'static + Send + Sync {
     }
 }
 
-fn paint_hover_listener<V>(bounds: Bounds<Pixels>, cx: &mut ViewContext<V>)
-where
-    V: 'static + Send + Sync,
-{
-    let hovered = bounds.contains_point(&cx.mouse_position());
-    cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
-        if phase == DispatchPhase::Capture {
-            if bounds.contains_point(&event.position) != hovered {
-                cx.notify();
-            }
-        }
-    });
-}
-
 #[derive(Deref, DerefMut)]
 pub struct StatefulInteraction<V: 'static + Send + Sync> {
     pub id: ElementId,
     #[deref]
     #[deref_mut]
     stateless: StatelessInteraction<V>,
-    pub mouse_click_listeners: SmallVec<[MouseClickListener<V>; 2]>,
-    pub active_style: StyleRefinement,
-    pub group_active_style: Option<GroupStyle>,
+    click_listeners: SmallVec<[ClickListener<V>; 2]>,
+    active_style: StyleRefinement,
+    group_active_style: Option<GroupStyle>,
+    drag_listener: Option<DragListener<V>>,
 }
 
 impl<V> ElementInteraction<V> for StatefulInteraction<V>
@@ -560,13 +721,16 @@ where
         Self {
             id,
             stateless: StatelessInteraction::default(),
-            mouse_click_listeners: SmallVec::new(),
+            click_listeners: SmallVec::new(),
+            drag_listener: None,
             active_style: StyleRefinement::default(),
             group_active_style: None,
         }
     }
 }
 
+type DropListener<V> = dyn Fn(&mut V, AnyBox, &mut ViewContext<V>) + Send + Sync;
+
 pub struct StatelessInteraction<V> {
     pub dispatch_context: DispatchContext,
     pub mouse_down_listeners: SmallVec<[MouseDownListener<V>; 2]>,
@@ -576,6 +740,9 @@ pub struct StatelessInteraction<V> {
     pub key_listeners: SmallVec<[(TypeId, KeyListener<V>); 32]>,
     pub hover_style: StyleRefinement,
     pub group_hover_style: Option<GroupStyle>,
+    drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>,
+    group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>,
+    drop_listeners: SmallVec<[(TypeId, Arc<DropListener<V>>); 2]>,
 }
 
 impl<V> StatelessInteraction<V>
@@ -586,7 +753,8 @@ where
         StatefulInteraction {
             id: id.into(),
             stateless: self,
-            mouse_click_listeners: SmallVec::new(),
+            click_listeners: SmallVec::new(),
+            drag_listener: None,
             active_style: StyleRefinement::default(),
             group_active_style: None,
         }
@@ -642,7 +810,7 @@ impl ActiveState {
 #[derive(Default)]
 pub struct InteractiveElementState {
     active_state: Arc<Mutex<ActiveState>>,
-    pending_click: Arc<Mutex<Option<MouseDownEvent>>>,
+    pending_mouse_down: Arc<Mutex<Option<MouseDownEvent>>>,
     scroll_offset: Option<Arc<Mutex<Point<Pixels>>>>,
 }
 
@@ -665,6 +833,9 @@ impl<V> Default for StatelessInteraction<V> {
             key_listeners: SmallVec::new(),
             hover_style: StyleRefinement::default(),
             group_hover_style: None,
+            drag_over_styles: SmallVec::new(),
+            group_drag_over_styles: SmallVec::new(),
+            drop_listeners: SmallVec::new(),
         }
     }
 }
@@ -740,11 +911,39 @@ pub struct MouseUpEvent {
 }
 
 #[derive(Clone, Debug, Default)]
-pub struct MouseClickEvent {
+pub struct ClickEvent {
     pub down: MouseDownEvent,
     pub up: MouseUpEvent,
 }
 
+pub struct Drag<S, R, V, E>
+where
+    S: 'static + Send + Sync,
+    R: Fn(&mut V, &mut ViewContext<V>) -> E,
+    V: 'static + Send + Sync,
+    E: Element<ViewState = V>,
+{
+    pub state: S,
+    pub render_drag_handle: R,
+    view_type: PhantomData<V>,
+}
+
+impl<S, R, V, E> Drag<S, R, V, E>
+where
+    S: 'static + Send + Sync,
+    R: Fn(&mut V, &mut ViewContext<V>) -> E + Send + Sync,
+    V: 'static + Send + Sync,
+    E: Element<ViewState = V>,
+{
+    pub fn new(state: S, render_drag_handle: R) -> Self {
+        Drag {
+            state,
+            render_drag_handle,
+            view_type: PhantomData,
+        }
+    }
+}
+
 #[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
 pub enum MouseButton {
     Left,
@@ -919,8 +1118,6 @@ pub type MouseUpListener<V> = Arc<
         + Sync
         + 'static,
 >;
-pub type MouseClickListener<V> =
-    Arc<dyn Fn(&mut V, &MouseClickEvent, &mut ViewContext<V>) + Send + Sync + 'static>;
 
 pub type MouseMoveListener<V> = Arc<
     dyn Fn(&mut V, &MouseMoveEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>)
@@ -936,6 +1133,12 @@ pub type ScrollWheelListener<V> = Arc<
         + 'static,
 >;
 
+pub type ClickListener<V> =
+    Arc<dyn Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + Send + Sync + 'static>;
+
+pub(crate) type DragListener<V> =
+    Arc<dyn Fn(&mut V, Point<Pixels>, &mut ViewContext<V>) -> AnyDrag + Send + Sync + 'static>;
+
 pub type KeyListener<V> = Arc<
     dyn Fn(
             &mut V,

crates/gpui2/src/taffy.rs 🔗

@@ -125,11 +125,11 @@ impl TaffyLayoutEngine {
         // }
         // println!("");
 
-        let started_at = std::time::Instant::now();
+        // let started_at = std::time::Instant::now();
         self.taffy
             .compute_layout(id.into(), available_space.into())
             .expect(EXPECT_MESSAGE);
-        println!("compute_layout took {:?}", started_at.elapsed());
+        // println!("compute_layout took {:?}", started_at.elapsed());
     }
 
     pub fn layout_bounds(&mut self, id: LayoutId) -> Bounds<Pixels> {

crates/gpui2/src/window.rs 🔗

@@ -3,11 +3,11 @@ use crate::{
     BorrowAppContext, Bounds, BoxShadow, Context, Corners, DevicePixels, DispatchContext,
     DisplayId, Edges, Effect, Element, EntityId, EventEmitter, FocusEvent, FontId, GlobalElementId,
     GlyphId, Handle, Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeyMatcher,
-    Keystroke, LayoutId, MainThread, MainThreadOnly, MonochromeSprite, MouseMoveEvent, Path,
-    Pixels, Platform, PlatformAtlas, PlatformWindow, Point, PolychromeSprite, Quad, Reference,
-    RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow,
-    SharedString, Size, Style, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle,
-    WeakHandle, WindowOptions, SUBPIXEL_VARIANTS,
+    Keystroke, LayoutId, MainThread, MainThreadOnly, MonochromeSprite, MouseMoveEvent,
+    MouseUpEvent, Path, Pixels, Platform, PlatformAtlas, PlatformWindow, Point, PolychromeSprite,
+    Quad, Reference, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels,
+    SceneBuilder, Shadow, SharedString, Size, Style, Subscription, TaffyLayoutEngine, Task,
+    Underline, UnderlineStyle, WeakHandle, WindowOptions, SUBPIXEL_VARIANTS,
 };
 use anyhow::Result;
 use collections::HashMap;
@@ -159,7 +159,7 @@ pub struct Window {
     key_matchers: HashMap<GlobalElementId, KeyMatcher>,
     z_index_stack: StackingOrder,
     content_mask_stack: Vec<ContentMask<Pixels>>,
-    scroll_offset_stack: Vec<Point<Pixels>>,
+    element_offset_stack: Vec<Point<Pixels>>,
     mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyListener)>>,
     key_dispatch_stack: Vec<KeyDispatchStackFrame>,
     freeze_key_dispatch_stack: bool,
@@ -177,7 +177,7 @@ pub struct Window {
 }
 
 impl Window {
-    pub fn new(
+    pub(crate) fn new(
         handle: AnyWindowHandle,
         options: WindowOptions,
         cx: &mut MainThread<AppContext>,
@@ -234,7 +234,7 @@ impl Window {
             key_matchers: HashMap::default(),
             z_index_stack: StackingOrder(SmallVec::new()),
             content_mask_stack: Vec::new(),
-            scroll_offset_stack: Vec::new(),
+            element_offset_stack: Vec::new(),
             mouse_listeners: HashMap::default(),
             key_dispatch_stack: Vec::new(),
             freeze_key_dispatch_stack: false,
@@ -469,7 +469,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
             .layout_engine
             .layout_bounds(layout_id)
             .map(Into::into);
-        bounds.origin -= self.scroll_offset();
+        bounds.origin += self.element_offset();
         bounds
     }
 
@@ -805,14 +805,22 @@ impl<'a, 'w> WindowContext<'a, 'w> {
 
             let mut root_view = cx.window.root_view.take().unwrap();
 
-            if let Some(element_id) = root_view.id() {
-                cx.with_element_state(element_id, |element_state, cx| {
-                    let element_state = draw_with_element_state(&mut root_view, element_state, cx);
-                    ((), element_state)
+            cx.stack(0, |cx| {
+                let available_space = cx.window.content_size.map(Into::into);
+                draw_any_view(&mut root_view, available_space, cx);
+            });
+
+            if let Some(mut active_drag) = cx.active_drag.take() {
+                cx.stack(1, |cx| {
+                    let offset = cx.mouse_position() - active_drag.cursor_offset;
+                    cx.with_element_offset(Some(offset), |cx| {
+                        let available_space =
+                            size(AvailableSpace::MinContent, AvailableSpace::MinContent);
+                        draw_any_view(&mut active_drag.drag_handle_view, available_space, cx);
+                        cx.active_drag = Some(active_drag);
+                    });
                 });
-            } else {
-                draw_with_element_state(&mut root_view, None, cx);
-            };
+            }
 
             cx.window.root_view = Some(root_view);
             let scene = cx.window.scene_builder.build();
@@ -827,20 +835,21 @@ impl<'a, 'w> WindowContext<'a, 'w> {
             .detach();
         });
 
-        fn draw_with_element_state(
-            root_view: &mut AnyView,
-            element_state: Option<AnyBox>,
+        fn draw_any_view(
+            view: &mut AnyView,
+            available_space: Size<AvailableSpace>,
             cx: &mut ViewContext<()>,
-        ) -> AnyBox {
-            let mut element_state = root_view.initialize(&mut (), element_state, cx);
-            let layout_id = root_view.layout(&mut (), &mut element_state, cx);
-            let available_space = cx.window.content_size.map(Into::into);
-            cx.window
-                .layout_engine
-                .compute_layout(layout_id, available_space);
-            let bounds = cx.window.layout_engine.layout_bounds(layout_id);
-            root_view.paint(bounds, &mut (), &mut element_state, cx);
-            element_state
+        ) {
+            cx.with_optional_element_state(view.id(), |element_state, cx| {
+                let mut element_state = view.initialize(&mut (), element_state, cx);
+                let layout_id = view.layout(&mut (), &mut element_state, cx);
+                cx.window
+                    .layout_engine
+                    .compute_layout(layout_id, available_space);
+                let bounds = cx.window.layout_engine.layout_bounds(layout_id);
+                view.paint(bounds, &mut (), &mut element_state, cx);
+                ((), element_state)
+            });
         }
     }
 
@@ -916,6 +925,12 @@ impl<'a, 'w> WindowContext<'a, 'w> {
                     }
                 }
 
+                if self.app.propagate_event
+                    && any_mouse_event.downcast_ref::<MouseUpEvent>().is_some()
+                {
+                    self.active_drag = None;
+                }
+
                 // Just in case any handlers added new handlers, which is weird, but possible.
                 handlers.extend(
                     self.window
@@ -1206,7 +1221,7 @@ pub trait BorrowWindow: BorrowAppContext {
         result
     }
 
-    fn with_scroll_offset<R>(
+    fn with_element_offset<R>(
         &mut self,
         offset: Option<Point<Pixels>>,
         f: impl FnOnce(&mut Self) -> R,
@@ -1215,16 +1230,16 @@ pub trait BorrowWindow: BorrowAppContext {
             return f(self);
         };
 
-        let offset = self.scroll_offset() + offset;
-        self.window_mut().scroll_offset_stack.push(offset);
+        let offset = self.element_offset() + offset;
+        self.window_mut().element_offset_stack.push(offset);
         let result = f(self);
-        self.window_mut().scroll_offset_stack.pop();
+        self.window_mut().element_offset_stack.pop();
         result
     }
 
-    fn scroll_offset(&self) -> Point<Pixels> {
+    fn element_offset(&self) -> Point<Pixels> {
         self.window()
-            .scroll_offset_stack
+            .element_offset_stack
             .last()
             .copied()
             .unwrap_or_default()
@@ -1263,6 +1278,18 @@ pub trait BorrowWindow: BorrowAppContext {
         })
     }
 
+    fn with_optional_element_state<S: 'static + Send + Sync, R>(
+        &mut self,
+        element_id: Option<ElementId>,
+        f: impl FnOnce(Option<S>, &mut Self) -> (R, S),
+    ) -> R {
+        if let Some(element_id) = element_id {
+            self.with_element_state(element_id, f)
+        } else {
+            f(None, self).0
+        }
+    }
+
     fn content_mask(&self) -> ContentMask<Pixels> {
         self.window()
             .content_mask_stack

crates/ui2/src/color.rs 🔗

@@ -0,0 +1,253 @@
+pub use crate::{theme, ButtonVariant, ElementExt, Theme};
+use gpui2::{hsla, rgb, Hsla, WindowContext};
+use strum::EnumIter;
+
+#[derive(Clone, Copy)]
+pub struct PlayerThemeColors {
+    pub cursor: Hsla,
+    pub selection: Hsla,
+}
+
+impl PlayerThemeColors {
+    pub fn new(cx: &WindowContext, ix: usize) -> Self {
+        let theme = theme(cx);
+
+        if ix < theme.players.len() {
+            Self {
+                cursor: theme.players[ix].cursor,
+                selection: theme.players[ix].selection,
+            }
+        } else {
+            Self {
+                cursor: rgb::<Hsla>(0xff00ff),
+                selection: rgb::<Hsla>(0xff00ff),
+            }
+        }
+    }
+}
+
+#[derive(Clone, Copy)]
+pub struct SyntaxColor {
+    pub comment: Hsla,
+    pub string: Hsla,
+    pub function: Hsla,
+    pub keyword: Hsla,
+}
+
+impl SyntaxColor {
+    pub fn new(cx: &WindowContext) -> Self {
+        let theme = theme(cx);
+
+        Self {
+            comment: theme
+                .syntax
+                .get("comment")
+                .cloned()
+                .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
+            string: theme
+                .syntax
+                .get("string")
+                .cloned()
+                .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
+            function: theme
+                .syntax
+                .get("function")
+                .cloned()
+                .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
+            keyword: theme
+                .syntax
+                .get("keyword")
+                .cloned()
+                .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
+        }
+    }
+}
+
+/// ThemeColor is the primary interface for coloring elements in the UI.
+///
+/// It is a mapping layer between semantic theme colors and colors from the reference library.
+///
+/// While we are between zed and zed2 we use this to map semantic colors to the old theme.
+#[derive(Clone, Copy)]
+pub struct ThemeColor {
+    pub transparent: Hsla,
+    pub mac_os_traffic_light_red: Hsla,
+    pub mac_os_traffic_light_yellow: Hsla,
+    pub mac_os_traffic_light_green: Hsla,
+    pub border: Hsla,
+    pub border_variant: Hsla,
+    pub border_focused: Hsla,
+    pub border_transparent: Hsla,
+    /// The background color of an elevated surface, like a modal, tooltip or toast.
+    pub elevated_surface: Hsla,
+    pub surface: Hsla,
+    /// Window background color of the base app
+    pub background: Hsla,
+    /// Default background for elements like filled buttons,
+    /// text fields, checkboxes, radio buttons, etc.
+    /// - TODO: Map to step 3.
+    pub filled_element: Hsla,
+    /// The background color of a hovered element, like a button being hovered
+    /// with a mouse, or hovered on a touch screen.
+    /// - TODO: Map to step 4.
+    pub filled_element_hover: Hsla,
+    /// The background color of an active element, like a button being pressed,
+    /// or tapped on a touch screen.
+    /// - TODO: Map to step 5.
+    pub filled_element_active: Hsla,
+    /// The background color of a selected element, like a selected tab,
+    /// a button toggled on, or a checkbox that is checked.
+    pub filled_element_selected: Hsla,
+    pub filled_element_disabled: Hsla,
+    pub ghost_element: Hsla,
+    /// The background color of a hovered element with no default background,
+    /// like a ghost-style button or an interactable list item.
+    /// - TODO: Map to step 3.
+    pub ghost_element_hover: Hsla,
+    /// - TODO: Map to step 4.
+    pub ghost_element_active: Hsla,
+    pub ghost_element_selected: Hsla,
+    pub ghost_element_disabled: Hsla,
+    pub text: Hsla,
+    pub text_muted: Hsla,
+    pub text_placeholder: Hsla,
+    pub text_disabled: Hsla,
+    pub text_accent: Hsla,
+    pub icon_muted: Hsla,
+    pub syntax: SyntaxColor,
+
+    pub status_bar: Hsla,
+    pub title_bar: Hsla,
+    pub toolbar: Hsla,
+    pub tab_bar: Hsla,
+    /// The background of the editor
+    pub editor: Hsla,
+    pub editor_subheader: Hsla,
+    pub editor_active_line: Hsla,
+    pub terminal: Hsla,
+    pub image_fallback_background: Hsla,
+
+    pub git_created: Hsla,
+    pub git_modified: Hsla,
+    pub git_deleted: Hsla,
+    pub git_conflict: Hsla,
+    pub git_ignored: Hsla,
+    pub git_renamed: Hsla,
+
+    pub player: [PlayerThemeColors; 8],
+}
+
+impl ThemeColor {
+    pub fn new(cx: &WindowContext) -> Self {
+        let theme = theme(cx);
+        let transparent = hsla(0.0, 0.0, 0.0, 0.0);
+
+        let players = [
+            PlayerThemeColors::new(cx, 0),
+            PlayerThemeColors::new(cx, 1),
+            PlayerThemeColors::new(cx, 2),
+            PlayerThemeColors::new(cx, 3),
+            PlayerThemeColors::new(cx, 4),
+            PlayerThemeColors::new(cx, 5),
+            PlayerThemeColors::new(cx, 6),
+            PlayerThemeColors::new(cx, 7),
+        ];
+
+        Self {
+            transparent,
+            mac_os_traffic_light_red: rgb::<Hsla>(0xEC695E),
+            mac_os_traffic_light_yellow: rgb::<Hsla>(0xF4BF4F),
+            mac_os_traffic_light_green: rgb::<Hsla>(0x62C554),
+            border: theme.lowest.base.default.border,
+            border_variant: theme.lowest.variant.default.border,
+            border_focused: theme.lowest.accent.default.border,
+            border_transparent: transparent,
+            elevated_surface: theme.lowest.base.default.background,
+            surface: theme.middle.base.default.background,
+            background: theme.lowest.base.default.background,
+            filled_element: theme.lowest.base.default.background,
+            filled_element_hover: theme.lowest.base.hovered.background,
+            filled_element_active: theme.lowest.base.active.background,
+            filled_element_selected: theme.lowest.accent.default.background,
+            filled_element_disabled: transparent,
+            ghost_element: transparent,
+            ghost_element_hover: theme.lowest.base.default.background,
+            ghost_element_active: theme.lowest.base.hovered.background,
+            ghost_element_selected: theme.lowest.accent.default.background,
+            ghost_element_disabled: transparent,
+            text: theme.lowest.base.default.foreground,
+            text_muted: theme.lowest.variant.default.foreground,
+            /// TODO: map this to a real value
+            text_placeholder: theme.lowest.negative.default.foreground,
+            text_disabled: theme.lowest.base.disabled.foreground,
+            text_accent: theme.lowest.accent.default.foreground,
+            icon_muted: theme.lowest.variant.default.foreground,
+            syntax: SyntaxColor::new(cx),
+
+            status_bar: theme.lowest.base.default.background,
+            title_bar: theme.lowest.base.default.background,
+            toolbar: theme.highest.base.default.background,
+            tab_bar: theme.middle.base.default.background,
+            editor: theme.highest.base.default.background,
+            editor_subheader: theme.middle.base.default.background,
+            terminal: theme.highest.base.default.background,
+            editor_active_line: theme.highest.on.default.background,
+            image_fallback_background: theme.lowest.base.default.background,
+
+            git_created: theme.lowest.positive.default.foreground,
+            git_modified: theme.lowest.accent.default.foreground,
+            git_deleted: theme.lowest.negative.default.foreground,
+            git_conflict: theme.lowest.warning.default.foreground,
+            git_ignored: theme.lowest.base.disabled.foreground,
+            git_renamed: theme.lowest.warning.default.foreground,
+
+            player: players,
+        }
+    }
+}
+
+/// Colors used exclusively for syntax highlighting.
+///
+/// For now we deserialize these from a theme.
+/// These will be defined statically in the new theme.
+#[derive(Default, PartialEq, EnumIter, Clone, Copy)]
+pub enum HighlightColor {
+    #[default]
+    Default,
+    Comment,
+    String,
+    Function,
+    Keyword,
+}
+
+impl HighlightColor {
+    pub fn hsla(&self, theme: &Theme) -> Hsla {
+        match self {
+            Self::Default => theme
+                .syntax
+                .get("primary")
+                .cloned()
+                .expect("Couldn't find `primary` in theme.syntax"),
+            Self::Comment => theme
+                .syntax
+                .get("comment")
+                .cloned()
+                .expect("Couldn't find `comment` in theme.syntax"),
+            Self::String => theme
+                .syntax
+                .get("string")
+                .cloned()
+                .expect("Couldn't find `string` in theme.syntax"),
+            Self::Function => theme
+                .syntax
+                .get("function")
+                .cloned()
+                .expect("Couldn't find `function` in theme.syntax"),
+            Self::Keyword => theme
+                .syntax
+                .get("keyword")
+                .cloned()
+                .expect("Couldn't find `keyword` in theme.syntax"),
+        }
+    }
+}

crates/ui2/src/components/breadcrumb.rs 🔗

@@ -36,7 +36,6 @@ impl<S: 'static + Send + Sync> Breadcrumb<S> {
         cx: &mut ViewContext<S>,
     ) -> impl Element<ViewState = S> {
         let color = ThemeColor::new(cx);
-        let color = ThemeColor::new(cx);
 
         let symbols_len = self.symbols.len();
 

crates/ui2/src/components/buffer.rs 🔗

@@ -159,13 +159,12 @@ impl<S: 'static + Send + Sync + Clone> Buffer<S> {
     }
 
     fn render_row(row: BufferRow, cx: &WindowContext) -> impl Element<ViewState = S> {
-        let system_color = SystemColor::new();
         let color = ThemeColor::new(cx);
 
         let line_background = if row.current {
             color.editor_active_line
         } else {
-            system_color.transparent
+            color.transparent
         };
 
         let line_number_color = if row.current {

crates/ui2/src/components/collab_panel.rs 🔗

@@ -22,7 +22,6 @@ impl<S: 'static + Send + Sync> CollabPanel<S> {
 
     fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
         let color = ThemeColor::new(cx);
-        let color = ThemeColor::new(cx);
 
         v_stack()
             .id(self.id.clone())

crates/ui2/src/components/list.rs 🔗

@@ -94,8 +94,6 @@ impl<S: 'static + Send + Sync> ListHeader<S> {
 
     fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
         let color = ThemeColor::new(cx);
-        let system_color = SystemColor::new();
-        let color = ThemeColor::new(cx);
 
         let is_toggleable = self.toggleable != Toggleable::NotToggleable;
         let is_toggled = self.toggleable.is_toggled();
@@ -373,7 +371,6 @@ impl<S: 'static + Send + Sync> ListEntry<S> {
 
     fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
         let color = ThemeColor::new(cx);
-        let system_color = SystemColor::new();
         let color = ThemeColor::new(cx);
         let settings = user_settings(cx);
 

crates/ui2/src/components/panes.rs 🔗

@@ -24,7 +24,6 @@ pub struct Pane<S: 'static + Send + Sync> {
 impl<S: 'static + Send + Sync> Pane<S> {
     pub fn new(id: impl Into<ElementId>, size: Size<Length>) -> Self {
         // Fill is only here for debugging purposes, remove before release
-        let system_color = SystemColor::new();
 
         Self {
             id: id.into(),

crates/ui2/src/components/player_stack.rs 🔗

@@ -18,7 +18,7 @@ impl<S: 'static + Send + Sync> PlayerStack<S> {
     }
 
     fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
-        let system_color = SystemColor::new();
+        let color = ThemeColor::new(cx);
         let player = self.player_with_call_status.get_player();
         self.player_with_call_status.get_call_status();
 
@@ -54,7 +54,7 @@ impl<S: 'static + Send + Sync> PlayerStack<S> {
                     .pl_1()
                     .rounded_lg()
                     .bg(if followers.is_none() {
-                        system_color.transparent
+                        color.transparent
                     } else {
                         player.selection_color(cx)
                     })

crates/ui2/src/components/project_panel.rs 🔗

@@ -21,7 +21,6 @@ impl<S: 'static + Send + Sync> ProjectPanel<S> {
 
     fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
         let color = ThemeColor::new(cx);
-        let color = ThemeColor::new(cx);
 
         div()
             .id(self.id.clone())

crates/ui2/src/components/tab.rs 🔗

@@ -17,6 +17,11 @@ pub struct Tab<S: 'static + Send + Sync + Clone> {
     close_side: IconSide,
 }
 
+#[derive(Clone, Debug)]
+struct TabDragState {
+    title: String,
+}
+
 impl<S: 'static + Send + Sync + Clone> Tab<S> {
     pub fn new(id: impl Into<ElementId>) -> Self {
         Self {
@@ -111,8 +116,19 @@ impl<S: 'static + Send + Sync + Clone> Tab<S> {
             ),
         };
 
+        let drag_state = TabDragState {
+            title: self.title.clone(),
+        };
+
         div()
             .id(self.id.clone())
+            .on_drag(move |_view, _cx| {
+                Drag::new(drag_state.clone(), |view, cx| div().w_8().h_4().bg(red()))
+            })
+            .drag_over::<TabDragState>(|d| d.bg(black()))
+            .on_drop(|_view, state: TabDragState, cx| {
+                dbg!(state);
+            })
             .px_2()
             .py_0p5()
             .flex()
@@ -148,7 +164,7 @@ impl<S: 'static + Send + Sync + Clone> Tab<S> {
     }
 }
 
-use gpui2::ElementId;
+use gpui2::{black, red, Drag, ElementId};
 #[cfg(feature = "stories")]
 pub use stories::*;
 

crates/ui2/src/components/title_bar.rs 🔗

@@ -88,7 +88,6 @@ impl TitleBar {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element<ViewState = Self> {
-        let color = ThemeColor::new(cx);
         let color = ThemeColor::new(cx);
         let settings = user_settings(cx);
 

crates/ui2/src/components/traffic_lights.rs 🔗

@@ -1,7 +1,6 @@
 use std::marker::PhantomData;
 
 use crate::prelude::*;
-use crate::SystemColor;
 
 #[derive(Clone, Copy)]
 enum TrafficLightColor {
@@ -28,12 +27,11 @@ impl<S: 'static + Send + Sync> TrafficLight<S> {
 
     fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
         let color = ThemeColor::new(cx);
-        let system_color = SystemColor::new();
 
         let fill = match (self.window_has_focus, self.color) {
-            (true, TrafficLightColor::Red) => system_color.mac_os_traffic_light_red,
-            (true, TrafficLightColor::Yellow) => system_color.mac_os_traffic_light_yellow,
-            (true, TrafficLightColor::Green) => system_color.mac_os_traffic_light_green,
+            (true, TrafficLightColor::Red) => color.mac_os_traffic_light_red,
+            (true, TrafficLightColor::Yellow) => color.mac_os_traffic_light_yellow,
+            (true, TrafficLightColor::Green) => color.mac_os_traffic_light_green,
             (false, _) => color.filled_element,
         };
 

crates/ui2/src/elements/input.rs 🔗

@@ -62,7 +62,6 @@ impl<S: 'static + Send + Sync> Input<S> {
 
     fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
         let color = ThemeColor::new(cx);
-        let system_color = SystemColor::new();
 
         let (input_bg, input_hover_bg, input_active_bg) = match self.variant {
             InputVariant::Ghost => (
@@ -95,7 +94,7 @@ impl<S: 'static + Send + Sync> Input<S> {
             .w_full()
             .px_2()
             .border()
-            .border_color(system_color.transparent)
+            .border_color(color.transparent)
             .bg(input_bg)
             .hover(|style| style.bg(input_hover_bg))
             .active(|style| style.bg(input_active_bg))

crates/ui2/src/lib.rs 🔗

@@ -1,5 +1,26 @@
+//! # UI – Zed UI Primitives & Components
+//!
+//! This crate provides a set of UI primitives and components that are used to build all of the elements in Zed's UI.
+//!
+//! ## Work in Progress
+//!
+//! This crate is still a work in progress. The initial primitives and components are built for getting all the UI on the screen,
+//! much of the state and functionality is mocked or hard codeded, and performance has not been a focus.
+//!
+//! Expect some inconsistencies from component to component as we work out the best way to build these components.
+//!
+//! ## Getting Started
+//!
+//! - [ThemeColor](crate::color::ThemeColor) is your one stop shop for all colors in the UI.
+//!
+//! ## Design Philosophy
+//!
+//! Work in Progress!
+//!
+
 #![allow(dead_code, unused_variables)]
 
+mod color;
 mod components;
 mod element_ext;
 mod elements;

crates/ui2/src/prelude.rs 🔗

@@ -3,282 +3,13 @@ pub use gpui2::{
     StatelessInteractive, Styled, ViewContext, WindowContext,
 };
 
+pub use crate::color::*;
 use crate::settings::user_settings;
 pub use crate::{theme, ButtonVariant, ElementExt, Theme};
 
-use gpui2::{hsla, rems, rgb, Hsla, Rems};
+use gpui2::{rems, Hsla, Rems};
 use strum::EnumIter;
 
-// TODO Remove uses in favor of ThemeColor
-#[derive(Default)]
-pub struct SystemColor {
-    pub transparent: Hsla,
-    pub mac_os_traffic_light_red: Hsla,
-    pub mac_os_traffic_light_yellow: Hsla,
-    pub mac_os_traffic_light_green: Hsla,
-    pub state_hover_background: Hsla,
-    pub state_active_background: Hsla,
-}
-
-impl SystemColor {
-    pub fn new() -> SystemColor {
-        SystemColor {
-            transparent: hsla(0.0, 0.0, 0.0, 0.0),
-            mac_os_traffic_light_red: rgb::<Hsla>(0xEC695E),
-            mac_os_traffic_light_yellow: rgb::<Hsla>(0xF4BF4F),
-            mac_os_traffic_light_green: rgb::<Hsla>(0x62C554),
-            state_hover_background: hsla(0.0, 0.0, 0.0, 0.08),
-            state_active_background: hsla(0.0, 0.0, 0.0, 0.16),
-        }
-    }
-    pub fn color(&self) -> Hsla {
-        self.transparent
-    }
-}
-
-#[derive(Clone, Copy)]
-pub struct PlayerThemeColors {
-    pub cursor: Hsla,
-    pub selection: Hsla,
-}
-
-impl PlayerThemeColors {
-    pub fn new(cx: &WindowContext, ix: usize) -> Self {
-        let theme = theme(cx);
-
-        if ix < theme.players.len() {
-            Self {
-                cursor: theme.players[ix].cursor,
-                selection: theme.players[ix].selection,
-            }
-        } else {
-            Self {
-                cursor: rgb::<Hsla>(0xff00ff),
-                selection: rgb::<Hsla>(0xff00ff),
-            }
-        }
-    }
-}
-
-#[derive(Clone, Copy)]
-pub struct SyntaxColor {
-    pub comment: Hsla,
-    pub string: Hsla,
-    pub function: Hsla,
-    pub keyword: Hsla,
-}
-
-impl SyntaxColor {
-    pub fn new(cx: &WindowContext) -> Self {
-        let theme = theme(cx);
-
-        Self {
-            comment: theme
-                .syntax
-                .get("comment")
-                .cloned()
-                .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
-            string: theme
-                .syntax
-                .get("string")
-                .cloned()
-                .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
-            function: theme
-                .syntax
-                .get("function")
-                .cloned()
-                .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
-            keyword: theme
-                .syntax
-                .get("keyword")
-                .cloned()
-                .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
-        }
-    }
-}
-
-#[derive(Clone, Copy)]
-pub struct ThemeColor {
-    pub transparent: Hsla,
-    pub mac_os_traffic_light_red: Hsla,
-    pub mac_os_traffic_light_yellow: Hsla,
-    pub mac_os_traffic_light_green: Hsla,
-    pub border: Hsla,
-    pub border_variant: Hsla,
-    pub border_focused: Hsla,
-    pub border_transparent: Hsla,
-    /// The background color of an elevated surface, like a modal, tooltip or toast.
-    pub elevated_surface: Hsla,
-    pub surface: Hsla,
-    /// Window background color of the base app
-    pub background: Hsla,
-    /// Default background for elements like filled buttons,
-    /// text fields, checkboxes, radio buttons, etc.
-    /// - TODO: Map to step 3.
-    pub filled_element: Hsla,
-    /// The background color of a hovered element, like a button being hovered
-    /// with a mouse, or hovered on a touch screen.
-    /// - TODO: Map to step 4.
-    pub filled_element_hover: Hsla,
-    /// The background color of an active element, like a button being pressed,
-    /// or tapped on a touch screen.
-    /// - TODO: Map to step 5.
-    pub filled_element_active: Hsla,
-    /// The background color of a selected element, like a selected tab,
-    /// a button toggled on, or a checkbox that is checked.
-    pub filled_element_selected: Hsla,
-    pub filled_element_disabled: Hsla,
-    pub ghost_element: Hsla,
-    /// The background color of a hovered element with no default background,
-    /// like a ghost-style button or an interactable list item.
-    /// - TODO: Map to step 3.
-    pub ghost_element_hover: Hsla,
-    /// - TODO: Map to step 4.
-    pub ghost_element_active: Hsla,
-    pub ghost_element_selected: Hsla,
-    pub ghost_element_disabled: Hsla,
-    pub text: Hsla,
-    pub text_muted: Hsla,
-    pub text_placeholder: Hsla,
-    pub text_disabled: Hsla,
-    pub text_accent: Hsla,
-    pub icon_muted: Hsla,
-    pub syntax: SyntaxColor,
-
-    pub status_bar: Hsla,
-    pub title_bar: Hsla,
-    pub toolbar: Hsla,
-    pub tab_bar: Hsla,
-    /// The background of the editor
-    pub editor: Hsla,
-    pub editor_subheader: Hsla,
-    pub editor_active_line: Hsla,
-    pub terminal: Hsla,
-    pub image_fallback_background: Hsla,
-
-    pub git_created: Hsla,
-    pub git_modified: Hsla,
-    pub git_deleted: Hsla,
-    pub git_conflict: Hsla,
-    pub git_ignored: Hsla,
-    pub git_renamed: Hsla,
-
-    pub player: [PlayerThemeColors; 8],
-}
-
-impl ThemeColor {
-    pub fn new(cx: &WindowContext) -> Self {
-        let theme = theme(cx);
-        let system_color = SystemColor::new();
-
-        let players = [
-            PlayerThemeColors::new(cx, 0),
-            PlayerThemeColors::new(cx, 1),
-            PlayerThemeColors::new(cx, 2),
-            PlayerThemeColors::new(cx, 3),
-            PlayerThemeColors::new(cx, 4),
-            PlayerThemeColors::new(cx, 5),
-            PlayerThemeColors::new(cx, 6),
-            PlayerThemeColors::new(cx, 7),
-        ];
-
-        Self {
-            transparent: hsla(0.0, 0.0, 0.0, 0.0),
-            mac_os_traffic_light_red: rgb::<Hsla>(0xEC695E),
-            mac_os_traffic_light_yellow: rgb::<Hsla>(0xF4BF4F),
-            mac_os_traffic_light_green: rgb::<Hsla>(0x62C554),
-            border: theme.lowest.base.default.border,
-            border_variant: theme.lowest.variant.default.border,
-            border_focused: theme.lowest.accent.default.border,
-            border_transparent: system_color.transparent,
-            elevated_surface: theme.lowest.base.default.background,
-            surface: theme.middle.base.default.background,
-            background: theme.lowest.base.default.background,
-            filled_element: theme.lowest.base.default.background,
-            filled_element_hover: theme.lowest.base.hovered.background,
-            filled_element_active: theme.lowest.base.active.background,
-            filled_element_selected: theme.lowest.accent.default.background,
-            filled_element_disabled: system_color.transparent,
-            ghost_element: system_color.transparent,
-            ghost_element_hover: theme.lowest.base.default.background,
-            ghost_element_active: theme.lowest.base.hovered.background,
-            ghost_element_selected: theme.lowest.accent.default.background,
-            ghost_element_disabled: system_color.transparent,
-            text: theme.lowest.base.default.foreground,
-            text_muted: theme.lowest.variant.default.foreground,
-            /// TODO: map this to a real value
-            text_placeholder: theme.lowest.negative.default.foreground,
-            text_disabled: theme.lowest.base.disabled.foreground,
-            text_accent: theme.lowest.accent.default.foreground,
-            icon_muted: theme.lowest.variant.default.foreground,
-            syntax: SyntaxColor::new(cx),
-
-            status_bar: theme.lowest.base.default.background,
-            title_bar: theme.lowest.base.default.background,
-            toolbar: theme.highest.base.default.background,
-            tab_bar: theme.middle.base.default.background,
-            editor: theme.highest.base.default.background,
-            editor_subheader: theme.middle.base.default.background,
-            terminal: theme.highest.base.default.background,
-            editor_active_line: theme.highest.on.default.background,
-            image_fallback_background: theme.lowest.base.default.background,
-
-            git_created: theme.lowest.positive.default.foreground,
-            git_modified: theme.lowest.accent.default.foreground,
-            git_deleted: theme.lowest.negative.default.foreground,
-            git_conflict: theme.lowest.warning.default.foreground,
-            git_ignored: theme.lowest.base.disabled.foreground,
-            git_renamed: theme.lowest.warning.default.foreground,
-
-            player: players,
-        }
-    }
-}
-
-#[derive(Default, PartialEq, EnumIter, Clone, Copy)]
-pub enum HighlightColor {
-    #[default]
-    Default,
-    Comment,
-    String,
-    Function,
-    Keyword,
-}
-
-impl HighlightColor {
-    pub fn hsla(&self, theme: &Theme) -> Hsla {
-        let system_color = SystemColor::new();
-
-        match self {
-            Self::Default => theme
-                .syntax
-                .get("primary")
-                .cloned()
-                .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
-            Self::Comment => theme
-                .syntax
-                .get("comment")
-                .cloned()
-                .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
-            Self::String => theme
-                .syntax
-                .get("string")
-                .cloned()
-                .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
-            Self::Function => theme
-                .syntax
-                .get("function")
-                .cloned()
-                .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
-            Self::Keyword => theme
-                .syntax
-                .get("keyword")
-                .cloned()
-                .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
-        }
-    }
-}
-
 pub fn ui_size(cx: &mut WindowContext, size: f32) -> Rems {
     const UI_SCALE_RATIO: f32 = 0.875;
 
@@ -330,10 +61,9 @@ impl GitStatus {
 
     pub fn hsla(&self, cx: &WindowContext) -> Hsla {
         let color = ThemeColor::new(cx);
-        let system_color = SystemColor::new();
 
         match self {
-            Self::None => system_color.transparent,
+            Self::None => color.transparent,
             Self::Created => color.git_created,
             Self::Modified => color.git_modified,
             Self::Deleted => color.git_deleted,