Use RefinementCascade to compose pressability and hoverability

Nathan Sobo and Conrad Irwin created

Co-Authored-By: Conrad Irwin <conrad@zed.dev>

Change summary

crates/gpui/playground/src/div.rs                      | 23 ++-
crates/gpui/playground/src/element.rs                  | 32 ++--
crates/gpui/playground/src/hoverable.rs                | 33 ++--
crates/gpui/playground/src/playground.rs               |  5 
crates/gpui/playground/src/pressable.rs                | 81 ++++++++++++
crates/gpui/playground/src/style.rs                    | 38 ++--
crates/gpui/playground_macros/src/derive_element.rs    | 10 
crates/gpui/playground_macros/src/styleable_helpers.rs |  1 
crates/gpui/src/app.rs                                 |  1 
crates/refineable/src/refineable.rs                    | 46 ++++++
10 files changed, 202 insertions(+), 68 deletions(-)

Detailed changes

crates/gpui/playground/src/div.rs 🔗

@@ -3,21 +3,22 @@ use crate::{
     interactive::{InteractionHandlers, Interactive},
     layout_context::LayoutContext,
     paint_context::PaintContext,
-    style::{Style, StyleHelpers, StyleRefinement, Styleable},
+    style::{Style, StyleHelpers, Styleable},
 };
 use anyhow::Result;
 use gpui::LayoutId;
+use refineable::{Refineable, RefinementCascade};
 use smallvec::SmallVec;
 
 pub struct Div<V: 'static> {
-    style: StyleRefinement,
+    styles: RefinementCascade<Style>,
     handlers: InteractionHandlers<V>,
     children: SmallVec<[AnyElement<V>; 2]>,
 }
 
 pub fn div<V>() -> Div<V> {
     Div {
-        style: Default::default(),
+        styles: Default::default(),
         handlers: Default::default(),
         children: Default::default(),
     }
@@ -36,16 +37,16 @@ impl<V: 'static> Element<V> for Div<V> {
             .map(|child| child.layout(view, cx))
             .collect::<Result<Vec<LayoutId>>>()?;
 
-        cx.add_layout_node(self.style(), (), children)
+        let style = Style::from_refinement(&self.style_cascade().merged());
+        cx.add_layout_node(style.clone(), (), children)
     }
 
     fn paint(&mut self, view: &mut V, layout: &mut Layout<V, ()>, cx: &mut PaintContext<V>)
     where
         Self: Sized,
     {
-        let style = self.style();
-
-        style.paint_background::<V, Self>(layout, cx);
+        self.computed_style()
+            .paint_background(layout.bounds(cx), cx);
         for child in &mut self.children {
             child.paint(view, cx);
         }
@@ -55,8 +56,12 @@ impl<V: 'static> Element<V> for Div<V> {
 impl<V> Styleable for Div<V> {
     type Style = Style;
 
-    fn declared_style(&mut self) -> &mut StyleRefinement {
-        &mut self.style
+    fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style> {
+        &mut self.styles
+    }
+
+    fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement {
+        self.styles.base()
     }
 }
 

crates/gpui/playground/src/element.rs 🔗

@@ -1,5 +1,4 @@
-use anyhow::Result;
-use derive_more::{Deref, DerefMut};
+use anyhow::{anyhow, Result};
 use gpui::{geometry::rect::RectF, EngineLayout};
 use smallvec::SmallVec;
 use std::marker::PhantomData;
@@ -83,13 +82,10 @@ impl<V> AnyElement<V> {
     }
 }
 
-#[derive(Deref, DerefMut)]
 pub struct Layout<V, D> {
     id: LayoutId,
     engine_layout: Option<EngineLayout>,
-    #[deref]
-    #[deref_mut]
-    element_data: D,
+    element_data: Option<D>,
     view_type: PhantomData<V>,
 }
 
@@ -98,7 +94,7 @@ impl<V: 'static, D> Layout<V, D> {
         Self {
             id,
             engine_layout: None,
-            element_data: element_data,
+            element_data: Some(element_data),
             view_type: PhantomData,
         }
     }
@@ -111,20 +107,26 @@ impl<V: 'static, D> Layout<V, D> {
         self.engine_layout(cx).order
     }
 
+    pub fn update<F, T>(&mut self, update: F) -> Result<T>
+    where
+        F: FnOnce(&mut Self, &mut D) -> T,
+    {
+        self.element_data
+            .take()
+            .map(|mut element_data| {
+                let result = update(self, &mut element_data);
+                self.element_data = Some(element_data);
+                result
+            })
+            .ok_or_else(|| anyhow!("reentrant calls to Layout::update are not allowed"))
+    }
+
     fn engine_layout(&mut self, cx: &mut PaintContext<'_, '_, '_, '_, V>) -> &mut EngineLayout {
         self.engine_layout
             .get_or_insert_with(|| cx.computed_layout(self.id).log_err().unwrap_or_default())
     }
 }
 
-impl<V: 'static> Layout<V, Option<AnyElement<V>>> {
-    pub fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>) {
-        let mut element = self.element_data.take().unwrap();
-        element.paint(view, cx);
-        self.element_data = Some(element);
-    }
-}
-
 pub trait ParentElement<V: 'static> {
     fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]>;
 

crates/gpui/playground/src/hoverable.rs 🔗

@@ -2,24 +2,24 @@ use crate::{
     element::{Element, Layout},
     layout_context::LayoutContext,
     paint_context::PaintContext,
-    style::{Style, StyleHelpers, StyleRefinement, Styleable},
+    style::{Style, StyleHelpers, Styleable},
 };
 use anyhow::Result;
 use gpui::platform::MouseMovedEvent;
-use refineable::Refineable;
-use std::cell::Cell;
+use refineable::{CascadeSlot, Refineable, RefinementCascade};
+use std::{cell::Cell, rc::Rc};
 
 pub struct Hoverable<E: Styleable> {
-    hovered: Cell<bool>,
-    child_style: StyleRefinement,
-    hovered_style: StyleRefinement,
+    hovered: Rc<Cell<bool>>,
+    cascade_slot: CascadeSlot,
+    hovered_style: <E::Style as Refineable>::Refinement,
     child: E,
 }
 
 pub fn hoverable<E: Styleable>(mut child: E) -> Hoverable<E> {
     Hoverable {
-        hovered: Cell::new(false),
-        child_style: child.declared_style().clone(),
+        hovered: Rc::new(Cell::new(false)),
+        cascade_slot: child.style_cascade().reserve(),
         hovered_style: Default::default(),
         child,
     }
@@ -28,7 +28,11 @@ pub fn hoverable<E: Styleable>(mut child: E) -> Hoverable<E> {
 impl<E: Styleable> Styleable for Hoverable<E> {
     type Style = E::Style;
 
-    fn declared_style(&mut self) -> &mut crate::style::StyleRefinement {
+    fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style> {
+        self.child.style_cascade()
+    }
+
+    fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement {
         &mut self.hovered_style
     }
 }
@@ -55,13 +59,10 @@ impl<V: 'static, E: Element<V> + Styleable> Element<V> for Hoverable<E> {
         let order = layout.order(cx);
 
         self.hovered.set(bounds.contains_point(cx.mouse_position()));
-        if self.hovered.get() {
-            // If hovered, refine the child's style with this element's style.
-            self.child.declared_style().refine(&self.hovered_style);
-        } else {
-            // Otherwise, set the child's style back to its original style.
-            *self.child.declared_style() = self.child_style.clone();
-        }
+
+        let slot = self.cascade_slot;
+        let style = self.hovered.get().then_some(self.hovered_style.clone());
+        self.style_cascade().set(slot, style);
 
         let hovered = self.hovered.clone();
         cx.on_event(order, move |view, event: &MouseMovedEvent, cx| {

crates/gpui/playground/src/playground.rs 🔗

@@ -22,6 +22,7 @@ mod hoverable;
 mod interactive;
 mod layout_context;
 mod paint_context;
+mod pressable;
 mod style;
 mod text;
 mod themes;
@@ -54,8 +55,10 @@ fn playground<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
         .h_full()
         .w_1_2()
         .fill(theme.success(0.5))
-        .hoverable()
+        .hovered()
         .fill(theme.error(0.5))
+        .pressed()
+        .fill(theme.warning(0.5))
     // .child(button().label("Hello").click(|_, _, _| println!("click!")))
 }
 

crates/gpui/playground/src/pressable.rs 🔗

@@ -0,0 +1,81 @@
+use crate::{
+    element::{Element, Layout},
+    layout_context::LayoutContext,
+    paint_context::PaintContext,
+    style::{Style, StyleHelpers, Styleable},
+};
+use anyhow::Result;
+use gpui::platform::MouseButtonEvent;
+use refineable::{CascadeSlot, Refineable, RefinementCascade};
+use std::{cell::Cell, rc::Rc};
+
+pub struct Pressable<E: Styleable> {
+    pressed: Rc<Cell<bool>>,
+    pressed_style: <E::Style as Refineable>::Refinement,
+    cascade_slot: CascadeSlot,
+    child: E,
+}
+
+pub fn pressable<E: Styleable>(mut child: E) -> Pressable<E> {
+    Pressable {
+        pressed: Rc::new(Cell::new(false)),
+        pressed_style: Default::default(),
+        cascade_slot: child.style_cascade().reserve(),
+        child,
+    }
+}
+
+impl<E: Styleable> Styleable for Pressable<E> {
+    type Style = E::Style;
+
+    fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement {
+        &mut self.pressed_style
+    }
+
+    fn style_cascade(&mut self) -> &mut RefinementCascade<E::Style> {
+        self.child.style_cascade()
+    }
+}
+
+impl<V: 'static, E: Element<V> + Styleable> Element<V> for Pressable<E> {
+    type Layout = E::Layout;
+
+    fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<Layout<V, Self::Layout>>
+    where
+        Self: Sized,
+    {
+        self.child.layout(view, cx)
+    }
+
+    fn paint(
+        &mut self,
+        view: &mut V,
+        layout: &mut Layout<V, Self::Layout>,
+        cx: &mut PaintContext<V>,
+    ) where
+        Self: Sized,
+    {
+        let slot = self.cascade_slot;
+        let style = self.pressed.get().then_some(self.pressed_style.clone());
+        self.style_cascade().set(slot, style);
+
+        let bounds = layout.bounds(cx);
+        let order = layout.order(cx);
+        let pressed = self.pressed.clone();
+        cx.on_event(order, move |view, event: &MouseButtonEvent, cx| {
+            if event.is_down {
+                if bounds.contains_point(event.position) {
+                    pressed.set(true);
+                    cx.repaint();
+                }
+            } else if pressed.get() {
+                pressed.set(false);
+                cx.repaint();
+            }
+        });
+
+        self.child.paint(view, layout, cx);
+    }
+}
+
+impl<E: Styleable<Style = Style>> StyleHelpers for Pressable<E> {}

crates/gpui/playground/src/style.rs 🔗

@@ -1,18 +1,18 @@
 use crate::{
     color::Hsla,
-    element::{Element, Layout},
     hoverable::{hoverable, Hoverable},
     paint_context::PaintContext,
+    pressable::{pressable, Pressable},
 };
 use gpui::{
     fonts::TextStyleRefinement,
     geometry::{
-        AbsoluteLength, DefiniteLength, Edges, EdgesRefinement, Length, Point, PointRefinement,
-        Size, SizeRefinement,
+        rect::RectF, AbsoluteLength, DefiniteLength, Edges, EdgesRefinement, Length, Point,
+        PointRefinement, Size, SizeRefinement,
     },
 };
 use playground_macros::styleable_helpers;
-use refineable::Refineable;
+use refineable::{Refineable, RefinementCascade};
 pub use taffy::style::{
     AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent,
     Overflow, Position,
@@ -126,12 +126,7 @@ impl Style {
 
     /// Paints the background of an element styled with this style.
     /// Return the bounds in which to paint the content.
-    pub fn paint_background<V: 'static, E: Element<V>>(
-        &self,
-        layout: &mut Layout<V, E::Layout>,
-        cx: &mut PaintContext<V>,
-    ) {
-        let bounds = layout.bounds(cx);
+    pub fn paint_background<V: 'static>(&self, bounds: RectF, cx: &mut PaintContext<V>) {
         let rem_size = cx.rem_pixels();
         if let Some(color) = self.fill.as_ref().and_then(Fill::color) {
             cx.scene.push_quad(gpui::Quad {
@@ -202,7 +197,7 @@ impl OptionalTextStyle {
     }
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 pub enum Fill {
     Color(Hsla),
 }
@@ -247,22 +242,28 @@ impl CornerRadii {
 }
 
 pub trait Styleable {
-    type Style: refineable::Refineable;
+    type Style: Refineable + Default;
 
-    fn declared_style(&mut self) -> &mut playground::style::StyleRefinement;
+    fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style>;
+    fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement;
 
-    fn style(&mut self) -> playground::style::Style {
-        let mut style = playground::style::Style::default();
-        style.refine(self.declared_style());
-        style
+    fn computed_style(&mut self) -> Self::Style {
+        Self::Style::from_refinement(&self.style_cascade().merged())
     }
 
-    fn hoverable(self) -> Hoverable<Self>
+    fn hovered(self) -> Hoverable<Self>
     where
         Self: Sized,
     {
         hoverable(self)
     }
+
+    fn pressed(self) -> Pressable<Self>
+    where
+        Self: Sized,
+    {
+        pressable(self)
+    }
 }
 
 // Helpers methods that take and return mut self. This includes tailwind style methods for standard sizes etc.
@@ -270,7 +271,6 @@ pub trait Styleable {
 // Example:
 // // Sets the padding to 0.5rem, just like class="p-2" in Tailwind.
 // fn p_2(mut self) -> Self where Self: Sized;
-use crate as playground; // Macro invocation references this crate as playground.
 pub trait StyleHelpers: Styleable<Style = Style> {
     styleable_helpers!();
 

crates/gpui/playground_macros/src/derive_element.rs 🔗

@@ -62,16 +62,16 @@ pub fn derive_element(input: TokenStream) -> TokenStream {
         impl #impl_generics playground::element::Element<#view_type_name> for #type_name #type_generics
         #where_clause
         {
-            type Layout = Option<playground::element::AnyElement<#view_type_name #lifetimes>>;
+            type Layout = playground::element::AnyElement<#view_type_name #lifetimes>;
 
             fn layout(
                 &mut self,
                 view: &mut V,
                 cx: &mut playground::element::LayoutContext<V>,
             ) -> anyhow::Result<playground::element::Layout<V, Self::Layout>> {
-                let mut element = self.render(view, cx).into_any();
-                let layout_id = element.layout(view, cx)?;
-                Ok(playground::element::Layout::new(layout_id, Some(element)))
+                let mut rendered_element = self.render(view, cx).into_any();
+                let layout_id = rendered_element.layout(view, cx)?;
+                Ok(playground::element::Layout::new(layout_id, rendered_element))
             }
 
             fn paint(
@@ -80,7 +80,7 @@ pub fn derive_element(input: TokenStream) -> TokenStream {
                 layout: &mut playground::element::Layout<V, Self::Layout>,
                 cx: &mut playground::element::PaintContext<V>,
             ) {
-                layout.paint(view, cx);
+                layout.update(|_, rendered_element| rendered_element.paint(view, cx)).ok();
             }
         }
 

crates/gpui/src/app.rs 🔗

@@ -1905,7 +1905,6 @@ impl AppContext {
 
     fn handle_repaint_window_effect(&mut self, window: AnyWindowHandle) {
         self.update_window(window, |cx| {
-            cx.layout(false).log_err();
             if let Some(scene) = cx.paint().log_err() {
                 cx.window.platform_window.present_scene(scene);
             }

crates/refineable/src/refineable.rs 🔗

@@ -1,7 +1,7 @@
 pub use derive_refineable::Refineable;
 
-pub trait Refineable {
-    type Refinement: Default;
+pub trait Refineable: Clone {
+    type Refinement: Refineable<Refinement = Self::Refinement> + Default;
 
     fn refine(&mut self, refinement: &Self::Refinement);
     fn refined(mut self, refinement: &Self::Refinement) -> Self
@@ -11,4 +11,46 @@ pub trait Refineable {
         self.refine(refinement);
         self
     }
+    fn from_refinement(refinement: &Self::Refinement) -> Self
+    where
+        Self: Default + Sized,
+    {
+        Self::default().refined(refinement)
+    }
+}
+
+pub struct RefinementCascade<S: Refineable>(Vec<Option<S::Refinement>>);
+
+impl<S: Refineable + Default> Default for RefinementCascade<S> {
+    fn default() -> Self {
+        Self(vec![Some(Default::default())])
+    }
+}
+
+#[derive(Copy, Clone)]
+pub struct CascadeSlot(usize);
+
+impl<S: Refineable + Default> RefinementCascade<S> {
+    pub fn reserve(&mut self) -> CascadeSlot {
+        self.0.push(None);
+        return CascadeSlot(self.0.len() - 1);
+    }
+
+    pub fn base(&mut self) -> &mut S::Refinement {
+        self.0[0].as_mut().unwrap()
+    }
+
+    pub fn set(&mut self, slot: CascadeSlot, refinement: Option<S::Refinement>) {
+        self.0[slot.0] = refinement
+    }
+
+    pub fn merged(&self) -> S::Refinement {
+        let mut merged = self.0[0].clone().unwrap();
+        for refinement in self.0.iter().skip(1) {
+            if let Some(refinement) = refinement {
+                merged.refine(refinement);
+            }
+        }
+        merged
+    }
 }