WIP

Nathan Sobo created

Change summary

crates/gpui3/src/app.rs                | 55 ++++++++++++--------
crates/gpui3/src/element.rs            | 11 +++
crates/gpui3/src/elements.rs           |  2 
crates/gpui3/src/elements/group.rs     | 75 ++++++++++++++++++++++++++++
crates/gpui3/src/elements/hoverable.rs | 28 +++++++--
crates/gpui3/src/gpui3.rs              | 14 +++-
crates/gpui3/src/styled.rs             | 13 ++++
crates/storybook2/src/theme.rs         |  8 +-
8 files changed, 164 insertions(+), 42 deletions(-)

Detailed changes

crates/gpui3/src/app.rs 🔗

@@ -60,7 +60,7 @@ impl App {
                 svg_renderer: SvgRenderer::new(asset_source),
                 image_cache: ImageCache::new(http_client),
                 text_style_stack: Vec::new(),
-                state_stacks_by_type: HashMap::default(),
+                global_stacks_by_type: HashMap::default(),
                 unit_entity,
                 entities,
                 windows: SlotMap::with_key(),
@@ -100,7 +100,7 @@ pub struct AppContext {
     pub(crate) svg_renderer: SvgRenderer,
     pub(crate) image_cache: ImageCache,
     pub(crate) text_style_stack: Vec<TextStyleRefinement>,
-    pub(crate) state_stacks_by_type: HashMap<TypeId, Vec<Box<dyn Any + Send + Sync>>>,
+    pub(crate) global_stacks_by_type: HashMap<TypeId, Vec<Box<dyn Any + Send + Sync>>>,
     pub(crate) unit_entity: Handle<()>,
     pub(crate) entities: EntityMap,
     pub(crate) windows: SlotMap<WindowId, Option<Window>>,
@@ -252,45 +252,56 @@ impl AppContext {
         style
     }
 
-    pub fn state<S: 'static>(&self) -> &S {
-        self.state_stacks_by_type
-            .get(&TypeId::of::<S>())
+    pub fn global<G: 'static>(&self) -> &G {
+        self.global_stacks_by_type
+            .get(&TypeId::of::<G>())
             .and_then(|stack| stack.last())
-            .and_then(|any_state| any_state.downcast_ref::<S>())
-            .ok_or_else(|| anyhow!("no state of type {} exists", type_name::<S>()))
+            .and_then(|any_state| any_state.downcast_ref::<G>())
+            .ok_or_else(|| anyhow!("no state of type {} exists", type_name::<G>()))
             .unwrap()
     }
 
-    pub fn state_mut<S: 'static>(&mut self) -> &mut S {
-        self.state_stacks_by_type
-            .get_mut(&TypeId::of::<S>())
+    pub fn global_mut<G: 'static>(&mut self) -> &mut G {
+        self.global_stacks_by_type
+            .get_mut(&TypeId::of::<G>())
             .and_then(|stack| stack.last_mut())
-            .and_then(|any_state| any_state.downcast_mut::<S>())
-            .ok_or_else(|| anyhow!("no state of type {} exists", type_name::<S>()))
+            .and_then(|any_state| any_state.downcast_mut::<G>())
+            .ok_or_else(|| anyhow!("no state of type {} exists", type_name::<G>()))
             .unwrap()
     }
 
-    pub(crate) fn push_text_style(&mut self, text_style: TextStyleRefinement) {
-        self.text_style_stack.push(text_style);
-    }
-
-    pub(crate) fn pop_text_style(&mut self) {
-        self.text_style_stack.pop();
+    pub fn default_global<G: 'static + Default + Sync + Send>(&mut self) -> &mut G {
+        let stack = self
+            .global_stacks_by_type
+            .entry(TypeId::of::<G>())
+            .or_default();
+        if stack.is_empty() {
+            stack.push(Box::new(G::default()));
+        }
+        stack.last_mut().unwrap().downcast_mut::<G>().unwrap()
     }
 
-    pub(crate) fn push_state<T: Send + Sync + 'static>(&mut self, state: T) {
-        self.state_stacks_by_type
+    pub(crate) fn push_global<T: Send + Sync + 'static>(&mut self, state: T) {
+        self.global_stacks_by_type
             .entry(TypeId::of::<T>())
             .or_default()
             .push(Box::new(state));
     }
 
-    pub(crate) fn pop_state<T: 'static>(&mut self) {
-        self.state_stacks_by_type
+    pub(crate) fn pop_global<T: 'static>(&mut self) {
+        self.global_stacks_by_type
             .get_mut(&TypeId::of::<T>())
             .and_then(|stack| stack.pop())
             .expect("state stack underflow");
     }
+
+    pub(crate) fn push_text_style(&mut self, text_style: TextStyleRefinement) {
+        self.text_style_stack.push(text_style);
+    }
+
+    pub(crate) fn pop_text_style(&mut self) {
+        self.text_style_stack.pop();
+    }
 }
 
 impl Context for AppContext {

crates/gpui3/src/element.rs 🔗

@@ -1,8 +1,8 @@
 use std::sync::Arc;
 
 use crate::{
-    BorrowWindow, Bounds, Clickable, ElementId, LayoutId, MouseDownEvent, MouseUpEvent, Pixels,
-    Point, ViewContext,
+    BorrowWindow, Bounds, Clickable, ElementGroup, ElementId, LayoutId, MouseDownEvent,
+    MouseUpEvent, Pixels, Point, SharedString, ViewContext,
 };
 use derive_more::{Deref, DerefMut};
 pub(crate) use smallvec::SmallVec;
@@ -27,6 +27,13 @@ pub trait Element: 'static + Send + Sync {
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<Self::ViewState>,
     );
+
+    fn group(self, name: impl Into<SharedString>) -> ElementGroup<Self>
+    where
+        Self: Sized,
+    {
+        ElementGroup::new(name.into(), self)
+    }
 }
 
 pub trait IdentifiedElement: Element {

crates/gpui3/src/elements.rs 🔗

@@ -1,5 +1,6 @@
 mod clickable;
 mod div;
+mod group;
 mod hoverable;
 mod identified;
 mod img;
@@ -9,6 +10,7 @@ mod text;
 
 pub use clickable::*;
 pub use div::*;
+pub use group::*;
 pub use hoverable::*;
 pub use identified::*;
 pub use img::*;

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

@@ -0,0 +1,75 @@
+use crate::{
+    AnyElement, AppContext, Bounds, Element, ElementId, IdentifiedElement, ParentElement, Pixels,
+    SharedString, ViewContext,
+};
+use collections::HashMap;
+use smallvec::SmallVec;
+
+#[derive(Default)]
+struct GroupBounds(HashMap<SharedString, SmallVec<[Bounds<Pixels>; 1]>>);
+
+pub fn element_group_bounds(name: &SharedString, cx: &mut AppContext) -> Option<Bounds<Pixels>> {
+    cx.default_global::<GroupBounds>()
+        .0
+        .get(name)
+        .and_then(|bounds_stack| bounds_stack.last().cloned())
+}
+
+pub struct ElementGroup<E> {
+    name: SharedString,
+    child: E,
+}
+
+impl<E> ElementGroup<E> {
+    pub fn new(name: SharedString, child: E) -> Self {
+        ElementGroup { name, child }
+    }
+}
+
+impl<E: Element> Element for ElementGroup<E> {
+    type ViewState = E::ViewState;
+    type ElementState = E::ElementState;
+
+    fn element_id(&self) -> Option<ElementId> {
+        self.child.element_id()
+    }
+
+    fn layout(
+        &mut self,
+        state: &mut Self::ViewState,
+        element_state: Option<Self::ElementState>,
+        cx: &mut ViewContext<Self::ViewState>,
+    ) -> (crate::LayoutId, Self::ElementState) {
+        self.child.layout(state, element_state, cx)
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        state: &mut Self::ViewState,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<Self::ViewState>,
+    ) {
+        cx.default_global::<GroupBounds>()
+            .0
+            .entry(self.name.clone())
+            .or_default()
+            .push(bounds);
+        self.child.paint(bounds, state, element_state, cx);
+        cx.default_global::<GroupBounds>()
+            .0
+            .get_mut(&self.name)
+            .unwrap()
+            .pop();
+    }
+}
+
+impl<E: ParentElement> ParentElement for ElementGroup<E> {
+    type State = E::State;
+
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<Self::State>; 2]> {
+        self.child.children_mut()
+    }
+}
+
+impl<E> IdentifiedElement for ElementGroup<E> where E: IdentifiedElement {}

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

@@ -1,6 +1,7 @@
 use crate::{
-    AnyElement, Bounds, DispatchPhase, Element, ElementId, IdentifiedElement, Interactive,
-    MouseEventListeners, MouseMoveEvent, ParentElement, Pixels, Styled, ViewContext,
+    element_group_bounds, AnyElement, Bounds, DispatchPhase, Element, ElementId, IdentifiedElement,
+    Interactive, MouseEventListeners, MouseMoveEvent, ParentElement, Pixels, SharedString, Styled,
+    ViewContext,
 };
 use refineable::{CascadeSlot, Refineable, RefinementCascade};
 use smallvec::SmallVec;
@@ -10,6 +11,7 @@ use std::sync::{
 };
 
 pub struct Hoverable<E: Styled> {
+    hover_group: Option<SharedString>,
     hovered: Arc<AtomicBool>,
     cascade_slot: CascadeSlot,
     hovered_style: <E::Style as Refineable>::Refinement,
@@ -17,8 +19,9 @@ pub struct Hoverable<E: Styled> {
 }
 
 impl<E: Styled> Hoverable<E> {
-    pub fn new(mut child: E) -> Self {
+    pub fn new(mut child: E, hover_group: Option<SharedString>) -> Self {
         Self {
+            hover_group,
             hovered: Arc::new(AtomicBool::new(false)),
             cascade_slot: child.style_cascade().reserve(),
             hovered_style: Default::default(),
@@ -77,17 +80,26 @@ where
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<Self::ViewState>,
     ) {
+        let bounds = self
+            .hover_group
+            .as_ref()
+            .and_then(|group| element_group_bounds(group, cx))
+            .unwrap_or(bounds);
         let hovered = bounds.contains_point(cx.mouse_position());
+
         let slot = self.cascade_slot;
         let style = hovered.then_some(self.hovered_style.clone());
         self.style_cascade().set(slot, style);
         self.hovered.store(hovered, SeqCst);
 
-        let hovered = self.hovered.clone();
-        cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
-            if phase == DispatchPhase::Capture {
-                if bounds.contains_point(event.position) != hovered.load(SeqCst) {
-                    cx.notify();
+        cx.on_mouse_event({
+            let hovered = self.hovered.clone();
+
+            move |_, event: &MouseMoveEvent, phase, cx| {
+                if phase == DispatchPhase::Capture {
+                    if bounds.contains_point(event.position) != hovered.load(SeqCst) {
+                        cx.notify();
+                    }
                 }
             }
         });

crates/gpui3/src/gpui3.rs 🔗

@@ -52,7 +52,7 @@ pub use view::*;
 pub use window::*;
 
 use std::{
-    any::Any,
+    any::{Any, TypeId},
     mem,
     ops::{Deref, DerefMut},
     sync::Arc,
@@ -77,6 +77,12 @@ pub trait Context {
     ) -> Self::Result<R>;
 }
 
+pub enum GlobalKey {
+    Numeric(usize),
+    View(EntityId),
+    Type(TypeId),
+}
+
 #[repr(transparent)]
 pub struct MainThread<T>(T);
 
@@ -143,13 +149,13 @@ pub trait BorrowAppContext {
         result
     }
 
-    fn with_state<T: Send + Sync + 'static, F, R>(&mut self, state: T, f: F) -> R
+    fn with_global<T: Send + Sync + 'static, F, R>(&mut self, state: T, f: F) -> R
     where
         F: FnOnce(&mut Self) -> R,
     {
-        self.app_mut().push_state(state);
+        self.app_mut().push_global(state);
         let result = f(self);
-        self.app_mut().pop_state::<T>();
+        self.app_mut().pop_global::<T>();
         result
     }
 }

crates/gpui3/src/styled.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{Hoverable, Pressable, Refineable, RefinementCascade};
+use crate::{Hoverable, Pressable, Refineable, RefinementCascade, SharedString};
 
 pub trait Styled {
     type Style: 'static + Refineable + Send + Sync + Default;
@@ -16,7 +16,16 @@ pub trait Styled {
         Self::Style: 'static + Refineable + Default + Send + Sync,
         <Self::Style as Refineable>::Refinement: 'static + Default + Send + Sync,
     {
-        Hoverable::new(self)
+        Hoverable::new(self, None)
+    }
+
+    fn group_hover(self, group_name: impl Into<SharedString>) -> Hoverable<Self>
+    where
+        Self: 'static + Sized + Send + Sync,
+        Self::Style: 'static + Refineable + Default + Send + Sync,
+        <Self::Style as Refineable>::Refinement: 'static + Default + Send + Sync,
+    {
+        Hoverable::new(self, Some(group_name.into()))
     }
 
     fn active(self) -> Pressable<Self>

crates/storybook2/src/theme.rs 🔗

@@ -134,7 +134,7 @@ where
     E: Element,
     F: FnOnce(&mut ViewContext<E::ViewState>) -> E,
 {
-    let child = cx.with_state(theme.clone(), |cx| build_child(cx));
+    let child = cx.with_global(theme.clone(), |cx| build_child(cx));
     Themed { theme, child }
 }
 
@@ -160,7 +160,7 @@ impl<E: Element> Element for Themed<E> {
     where
         Self: Sized,
     {
-        cx.with_state(self.theme.clone(), |cx| {
+        cx.with_global(self.theme.clone(), |cx| {
             self.child.layout(state, element_state, cx)
         })
     }
@@ -174,12 +174,12 @@ impl<E: Element> Element for Themed<E> {
     ) where
         Self: Sized,
     {
-        cx.with_state(self.theme.clone(), |cx| {
+        cx.with_global(self.theme.clone(), |cx| {
             self.child.paint(bounds, state, frame_state, cx);
         });
     }
 }
 
 pub fn theme<'a>(cx: &'a WindowContext) -> &'a Theme {
-    cx.state()
+    cx.global()
 }