wip: picker

Mikayla , nathan , and max created

co-authored-by: nathan <nathan@zed.dev>
co-authored-by: max <max@zed.dev>

Change summary

Cargo.lock                                      |   4 
crates/gpui2/src/color.rs                       |   9 +
crates/gpui2/src/elements/div.rs                |  80 +++++----
crates/gpui2/src/elements/img.rs                |  42 ++--
crates/gpui2/src/elements/list.rs               | 122 ++++++++++-----
crates/gpui2/src/elements/svg.rs                |  36 ++--
crates/gpui2/src/interactive.rs                 | 129 +++++++++------
crates/gpui2/src/style.rs                       |   4 
crates/gpui2/src/text_system/line.rs            |   2 
crates/gpui2/src/window.rs                      |   4 
crates/menu2/Cargo.toml                         |   3 
crates/menu2/src/menu2.rs                       |  18 +-
crates/picker2/src/picker2.rs                   |  79 +++++++--
crates/storybook2/Cargo.toml                    |   1 
crates/storybook2/src/stories/focus.rs          |  42 ++---
crates/storybook2/src/stories/kitchen_sink.rs   |   4 
crates/storybook2/src/stories/picker.rs         | 151 ++++++++++++++----
crates/storybook2/src/stories/scroll.rs         |   4 
crates/ui2/src/components/checkbox.rs           |   4 
crates/zed/src/languages/racket/highlights.scm  |   1 
crates/zed2/src/languages/racket/highlights.scm |   1 
21 files changed, 461 insertions(+), 279 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4970,7 +4970,8 @@ dependencies = [
 name = "menu2"
 version = "0.1.0"
 dependencies = [
- "gpui2",
+ "serde",
+ "serde_derive",
 ]
 
 [[package]]
@@ -8542,6 +8543,7 @@ dependencies = [
  "gpui2",
  "itertools 0.11.0",
  "log",
+ "menu2",
  "picker2",
  "rust-embed",
  "serde",

crates/gpui2/src/color.rs 🔗

@@ -194,6 +194,15 @@ pub fn red() -> Hsla {
     }
 }
 
+pub fn blue() -> Hsla {
+    Hsla {
+        h: 0.6,
+        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 🔗

@@ -1,28 +1,28 @@
 use crate::{
     point, AnyElement, BorrowWindow, Bounds, Component, Element, ElementFocus, ElementId,
-    ElementInteraction, FocusDisabled, FocusEnabled, FocusHandle, FocusListeners, Focusable,
+    ElementInteractivity, FocusDisabled, FocusEnabled, FocusHandle, FocusListeners, Focusable,
     GlobalElementId, GroupBounds, InteractiveElementState, LayoutId, Overflow, ParentElement,
-    Pixels, Point, SharedString, StatefulInteraction, StatefulInteractive, StatelessInteraction,
-    StatelessInteractive, Style, StyleRefinement, Styled, ViewContext, Visibility,
+    Pixels, Point, SharedString, StatefulInteractive, StatefulInteractivity, StatelessInteractive,
+    StatelessInteractivity, Style, StyleRefinement, Styled, ViewContext, Visibility,
 };
 use refineable::Refineable;
 use smallvec::SmallVec;
 
 pub struct Div<
     V: 'static,
-    I: ElementInteraction<V> = StatelessInteraction<V>,
+    I: ElementInteractivity<V> = StatelessInteractivity<V>,
     F: ElementFocus<V> = FocusDisabled,
 > {
-    interaction: I,
+    interactivity: I,
     focus: F,
     children: SmallVec<[AnyElement<V>; 2]>,
     group: Option<SharedString>,
     base_style: StyleRefinement,
 }
 
-pub fn div<V: 'static>() -> Div<V, StatelessInteraction<V>, FocusDisabled> {
+pub fn div<V: 'static>() -> Div<V, StatelessInteractivity<V>, FocusDisabled> {
     Div {
-        interaction: StatelessInteraction::default(),
+        interactivity: StatelessInteractivity::default(),
         focus: FocusDisabled,
         children: SmallVec::new(),
         group: None,
@@ -30,14 +30,14 @@ pub fn div<V: 'static>() -> Div<V, StatelessInteraction<V>, FocusDisabled> {
     }
 }
 
-impl<V, F> Div<V, StatelessInteraction<V>, F>
+impl<V, F> Div<V, StatelessInteractivity<V>, F>
 where
     V: 'static,
     F: ElementFocus<V>,
 {
-    pub fn id(self, id: impl Into<ElementId>) -> Div<V, StatefulInteraction<V>, F> {
+    pub fn id(self, id: impl Into<ElementId>) -> Div<V, StatefulInteractivity<V>, F> {
         Div {
-            interaction: id.into().into(),
+            interactivity: id.into().into(),
             focus: self.focus,
             children: self.children,
             group: self.group,
@@ -48,7 +48,7 @@ where
 
 impl<V, I, F> Div<V, I, F>
 where
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
     F: ElementFocus<V>,
 {
     pub fn group(mut self, group: impl Into<SharedString>) -> Self {
@@ -98,16 +98,20 @@ where
         let mut computed_style = Style::default();
         computed_style.refine(&self.base_style);
         self.focus.refine_style(&mut computed_style, cx);
-        self.interaction
-            .refine_style(&mut computed_style, bounds, &element_state.interactive, cx);
+        self.interactivity.refine_style(
+            &mut computed_style,
+            bounds,
+            &element_state.interactive,
+            cx,
+        );
         computed_style
     }
 }
 
-impl<V: 'static> Div<V, StatefulInteraction<V>, FocusDisabled> {
-    pub fn focusable(self) -> Div<V, StatefulInteraction<V>, FocusEnabled<V>> {
+impl<V: 'static> Div<V, StatefulInteractivity<V>, FocusDisabled> {
+    pub fn focusable(self) -> Div<V, StatefulInteractivity<V>, FocusEnabled<V>> {
         Div {
-            interaction: self.interaction,
+            interactivity: self.interactivity,
             focus: FocusEnabled::new(),
             children: self.children,
             group: self.group,
@@ -118,9 +122,9 @@ impl<V: 'static> Div<V, StatefulInteraction<V>, FocusDisabled> {
     pub fn track_focus(
         self,
         handle: &FocusHandle,
-    ) -> Div<V, StatefulInteraction<V>, FocusEnabled<V>> {
+    ) -> Div<V, StatefulInteractivity<V>, FocusEnabled<V>> {
         Div {
-            interaction: self.interaction,
+            interactivity: self.interactivity,
             focus: FocusEnabled::tracked(handle),
             children: self.children,
             group: self.group,
@@ -145,13 +149,13 @@ impl<V: 'static> Div<V, StatefulInteraction<V>, FocusDisabled> {
     }
 }
 
-impl<V: 'static> Div<V, StatelessInteraction<V>, FocusDisabled> {
+impl<V: 'static> Div<V, StatelessInteractivity<V>, FocusDisabled> {
     pub fn track_focus(
         self,
         handle: &FocusHandle,
-    ) -> Div<V, StatefulInteraction<V>, FocusEnabled<V>> {
+    ) -> Div<V, StatefulInteractivity<V>, FocusEnabled<V>> {
         Div {
-            interaction: self.interaction.into_stateful(handle),
+            interactivity: self.interactivity.into_stateful(handle),
             focus: handle.clone().into(),
             children: self.children,
             group: self.group,
@@ -163,7 +167,7 @@ impl<V: 'static> Div<V, StatelessInteraction<V>, FocusDisabled> {
 impl<V, I> Focusable<V> for Div<V, I, FocusEnabled<V>>
 where
     V: 'static,
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
 {
     fn focus_listeners(&mut self) -> &mut FocusListeners<V> {
         &mut self.focus.focus_listeners
@@ -191,13 +195,13 @@ pub struct DivState {
 
 impl<V, I, F> Element<V> for Div<V, I, F>
 where
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
     F: ElementFocus<V>,
 {
     type ElementState = DivState;
 
     fn id(&self) -> Option<ElementId> {
-        self.interaction
+        self.interactivity
             .as_stateful()
             .map(|identified| identified.id.clone())
     }
@@ -212,7 +216,7 @@ where
         self.focus
             .initialize(element_state.focus_handle.take(), cx, |focus_handle, cx| {
                 element_state.focus_handle = focus_handle;
-                self.interaction.initialize(cx, |cx| {
+                self.interactivity.initialize(cx, |cx| {
                     for child in &mut self.children {
                         child.initialize(view_state, cx);
                     }
@@ -281,11 +285,11 @@ where
                 (child_max - child_min).into()
             };
 
-            cx.stack(z_index, |cx| {
-                cx.stack(0, |cx| {
+            cx.with_z_index(z_index, |cx| {
+                cx.with_z_index(0, |cx| {
                     style.paint(bounds, cx);
                     this.focus.paint(bounds, cx);
-                    this.interaction.paint(
+                    this.interactivity.paint(
                         bounds,
                         content_size,
                         style.overflow,
@@ -293,7 +297,7 @@ where
                         cx,
                     );
                 });
-                cx.stack(1, |cx| {
+                cx.with_z_index(1, |cx| {
                     style.apply_text_style(cx, |cx| {
                         style.apply_overflow(bounds, cx, |cx| {
                             let scroll_offset = element_state.interactive.scroll_offset();
@@ -316,7 +320,7 @@ where
 
 impl<V, I, F> Component<V> for Div<V, I, F>
 where
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
     F: ElementFocus<V>,
 {
     fn render(self) -> AnyElement<V> {
@@ -326,7 +330,7 @@ where
 
 impl<V, I, F> ParentElement<V> for Div<V, I, F>
 where
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
     F: ElementFocus<V>,
 {
     fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
@@ -336,7 +340,7 @@ where
 
 impl<V, I, F> Styled for Div<V, I, F>
 where
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
     F: ElementFocus<V>,
 {
     fn style(&mut self) -> &mut StyleRefinement {
@@ -346,19 +350,19 @@ where
 
 impl<V, I, F> StatelessInteractive<V> for Div<V, I, F>
 where
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
     F: ElementFocus<V>,
 {
-    fn stateless_interaction(&mut self) -> &mut StatelessInteraction<V> {
-        self.interaction.as_stateless_mut()
+    fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V> {
+        self.interactivity.as_stateless_mut()
     }
 }
 
-impl<V, F> StatefulInteractive<V> for Div<V, StatefulInteraction<V>, F>
+impl<V, F> StatefulInteractive<V> for Div<V, StatefulInteractivity<V>, F>
 where
     F: ElementFocus<V>,
 {
-    fn stateful_interaction(&mut self) -> &mut StatefulInteraction<V> {
-        &mut self.interaction
+    fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V> {
+        &mut self.interactivity
     }
 }

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

@@ -1,17 +1,15 @@
-use std::sync::Arc;
-
 use crate::{
     div, AnyElement, BorrowWindow, Bounds, Component, Div, DivState, Element, ElementFocus,
-    ElementId, ElementInteraction, FocusDisabled, FocusEnabled, FocusListeners, Focusable,
-    LayoutId, Pixels, SharedString, StatefulInteraction, StatefulInteractive, StatelessInteraction,
-    StatelessInteractive, StyleRefinement, Styled, ViewContext,
+    ElementId, ElementInteractivity, FocusDisabled, FocusEnabled, FocusListeners, Focusable,
+    LayoutId, Pixels, SharedString, StatefulInteractive, StatefulInteractivity,
+    StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, ViewContext,
 };
 use futures::FutureExt;
 use util::ResultExt;
 
 pub struct Img<
     V: 'static,
-    I: ElementInteraction<V> = StatelessInteraction<V>,
+    I: ElementInteractivity<V> = StatelessInteractivity<V>,
     F: ElementFocus<V> = FocusDisabled,
 > {
     base: Div<V, I, F>,
@@ -19,7 +17,7 @@ pub struct Img<
     grayscale: bool,
 }
 
-pub fn img<V: 'static>() -> Img<V, StatelessInteraction<V>, FocusDisabled> {
+pub fn img<V: 'static>() -> Img<V, StatelessInteractivity<V>, FocusDisabled> {
     Img {
         base: div(),
         uri: None,
@@ -30,7 +28,7 @@ pub fn img<V: 'static>() -> Img<V, StatelessInteraction<V>, FocusDisabled> {
 impl<V, I, F> Img<V, I, F>
 where
     V: 'static,
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
     F: ElementFocus<V>,
 {
     pub fn uri(mut self, uri: impl Into<SharedString>) -> Self {
@@ -44,11 +42,11 @@ where
     }
 }
 
-impl<V, F> Img<V, StatelessInteraction<V>, F>
+impl<V, F> Img<V, StatelessInteractivity<V>, F>
 where
     F: ElementFocus<V>,
 {
-    pub fn id(self, id: impl Into<ElementId>) -> Img<V, StatefulInteraction<V>, F> {
+    pub fn id(self, id: impl Into<ElementId>) -> Img<V, StatefulInteractivity<V>, F> {
         Img {
             base: self.base.id(id),
             uri: self.uri,
@@ -59,7 +57,7 @@ where
 
 impl<V, I, F> Component<V> for Img<V, I, F>
 where
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
     F: ElementFocus<V>,
 {
     fn render(self) -> AnyElement<V> {
@@ -69,7 +67,7 @@ where
 
 impl<V, I, F> Element<V> for Img<V, I, F>
 where
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
     F: ElementFocus<V>,
 {
     type ElementState = DivState;
@@ -103,7 +101,7 @@ where
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     ) {
-        cx.stack(0, |cx| {
+        cx.with_z_index(0, |cx| {
             self.base.paint(bounds, view, element_state, cx);
         });
 
@@ -120,7 +118,7 @@ where
                 .and_then(ResultExt::log_err)
             {
                 let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
-                cx.stack(1, |cx| {
+                cx.with_z_index(1, |cx| {
                     cx.paint_image(bounds, corner_radii, data, self.grayscale)
                         .log_err()
                 });
@@ -138,7 +136,7 @@ where
 
 impl<V, I, F> Styled for Img<V, I, F>
 where
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
     F: ElementFocus<V>,
 {
     fn style(&mut self) -> &mut StyleRefinement {
@@ -148,27 +146,27 @@ where
 
 impl<V, I, F> StatelessInteractive<V> for Img<V, I, F>
 where
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
     F: ElementFocus<V>,
 {
-    fn stateless_interaction(&mut self) -> &mut StatelessInteraction<V> {
-        self.base.stateless_interaction()
+    fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V> {
+        self.base.stateless_interactivity()
     }
 }
 
-impl<V, F> StatefulInteractive<V> for Img<V, StatefulInteraction<V>, F>
+impl<V, F> StatefulInteractive<V> for Img<V, StatefulInteractivity<V>, F>
 where
     F: ElementFocus<V>,
 {
-    fn stateful_interaction(&mut self) -> &mut StatefulInteraction<V> {
-        self.base.stateful_interaction()
+    fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V> {
+        self.base.stateful_interactivity()
     }
 }
 
 impl<V, I> Focusable<V> for Img<V, I, FocusEnabled<V>>
 where
     V: 'static,
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
 {
     fn focus_listeners(&mut self) -> &mut FocusListeners<V> {
         self.base.focus_listeners()

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

@@ -1,15 +1,12 @@
-use std::{cmp, ops::Range};
-
-use smallvec::SmallVec;
-
 use crate::{
     point, px, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, ElementId,
-    LayoutId, Pixels, Size, StyleRefinement, Styled, ViewContext,
+    ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Size, StatefulInteractive,
+    StatefulInteractivity, StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled,
+    ViewContext,
 };
-
-// We want to support uniform and non-uniform height
-// We need to make the ID mandatory, to replace the 'state' field
-// Previous implementation measured the first element as early as possible
+use smallvec::SmallVec;
+use std::{cmp, ops::Range};
+use taffy::style::Overflow;
 
 pub fn list<Id, V, C>(
     id: Id,
@@ -21,8 +18,9 @@ where
     V: 'static,
     C: Component<V>,
 {
+    let id = id.into();
     List {
-        id: id.into(),
+        id: id.clone(),
         style: Default::default(),
         item_count,
         render_items: Box::new(move |view, visible_range, cx| {
@@ -31,10 +29,11 @@ where
                 .map(|component| component.render())
                 .collect()
         }),
+        interactivity: id.into(),
     }
 }
 
-pub struct List<V> {
+pub struct List<V: 'static> {
     id: ElementId,
     style: StyleRefinement,
     item_count: usize,
@@ -45,19 +44,12 @@ pub struct List<V> {
             &'a mut ViewContext<V>,
         ) -> SmallVec<[AnyElement<V>; 64]>,
     >,
+    interactivity: StatefulInteractivity<V>,
 }
 
-// #[derive(Debug)]
-// pub enum ScrollTarget {
-//     Show(usize),
-//     Center(usize),
-// }
-
 #[derive(Default)]
 pub struct ListState {
-    scroll_top: f32,
-    // todo
-    // scroll_to: Option<ScrollTarget>,
+    interactive: InteractiveElementState,
 }
 
 impl<V: 'static> Styled for List<V> {
@@ -111,30 +103,66 @@ impl<V: 'static> Element<V> for List<V> {
                 - point(border.right + padding.right, border.bottom + padding.bottom),
         );
 
-        if self.item_count > 0 {
-            let item_height = self.measure_item_height(view_state, padded_bounds, cx);
-            let visible_item_count = (padded_bounds.size.height / item_height).ceil() as usize;
-            let visible_range = 0..cmp::min(visible_item_count, self.item_count);
-
-            let mut items = (self.render_items)(view_state, visible_range, cx);
-
-            dbg!(items.len(), self.item_count, visible_item_count);
+        cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
+            let content_size;
+            if self.item_count > 0 {
+                let item_height = self.measure_item_height(view_state, padded_bounds, cx);
+                let visible_item_count =
+                    (padded_bounds.size.height / item_height).ceil() as usize + 1;
+                let scroll_offset = element_state
+                    .interactive
+                    .scroll_offset()
+                    .map_or((0.0).into(), |offset| offset.y);
+                let first_visible_element_ix = (-scroll_offset / item_height).floor() as usize;
+                let visible_range = first_visible_element_ix
+                    ..cmp::min(
+                        first_visible_element_ix + visible_item_count,
+                        self.item_count,
+                    );
+
+                let mut items = (self.render_items)(view_state, visible_range.clone(), cx);
+
+                content_size = Size {
+                    width: padded_bounds.size.width,
+                    height: item_height * self.item_count,
+                };
+
+                cx.with_z_index(1, |cx| {
+                    for (item, ix) in items.iter_mut().zip(visible_range) {
+                        item.initialize(view_state, cx);
+
+                        let layout_id = item.layout(view_state, cx);
+                        cx.compute_layout(
+                            layout_id,
+                            Size {
+                                width: AvailableSpace::Definite(bounds.size.width),
+                                height: AvailableSpace::Definite(item_height),
+                            },
+                        );
+                        let offset =
+                            padded_bounds.origin + point(px(0.), item_height * ix + scroll_offset);
+                        cx.with_element_offset(Some(offset), |cx| item.paint(view_state, cx))
+                    }
+                });
+            } else {
+                content_size = Size {
+                    width: bounds.size.width,
+                    height: px(0.),
+                };
+            }
 
-            for (ix, item) in items.iter_mut().enumerate() {
-                item.initialize(view_state, cx);
+            let overflow = point(style.overflow.x, Overflow::Scroll);
 
-                let layout_id = item.layout(view_state, cx);
-                cx.compute_layout(
-                    layout_id,
-                    Size {
-                        width: AvailableSpace::Definite(bounds.size.width),
-                        height: AvailableSpace::Definite(item_height),
-                    },
+            cx.with_z_index(0, |cx| {
+                self.interactivity.paint(
+                    bounds,
+                    content_size,
+                    overflow,
+                    &mut element_state.interactive,
+                    cx,
                 );
-                let offset = padded_bounds.origin + point(px(0.), item_height * ix);
-                cx.with_element_offset(Some(offset), |cx| item.paint(view_state, cx))
-            }
-        }
+            });
+        })
     }
 }
 
@@ -161,6 +189,18 @@ impl<V> List<V> {
     }
 }
 
+impl<V: 'static> StatelessInteractive<V> for List<V> {
+    fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V> {
+        self.interactivity.as_stateless_mut()
+    }
+}
+
+impl<V: 'static> StatefulInteractive<V> for List<V> {
+    fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V> {
+        &mut self.interactivity
+    }
+}
+
 impl<V: 'static> Component<V> for List<V> {
     fn render(self) -> AnyElement<V> {
         AnyElement::new(self)

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

@@ -1,21 +1,21 @@
 use crate::{
     div, AnyElement, Bounds, Component, Div, DivState, Element, ElementFocus, ElementId,
-    ElementInteraction, FocusDisabled, FocusEnabled, FocusListeners, Focusable, LayoutId, Pixels,
-    SharedString, StatefulInteraction, StatefulInteractive, StatelessInteraction,
-    StatelessInteractive, StyleRefinement, Styled, ViewContext,
+    ElementInteractivity, FocusDisabled, FocusEnabled, FocusListeners, Focusable, LayoutId, Pixels,
+    SharedString, StatefulInteractive, StatefulInteractivity, StatelessInteractive,
+    StatelessInteractivity, StyleRefinement, Styled, ViewContext,
 };
 use util::ResultExt;
 
 pub struct Svg<
     V: 'static,
-    I: ElementInteraction<V> = StatelessInteraction<V>,
+    I: ElementInteractivity<V> = StatelessInteractivity<V>,
     F: ElementFocus<V> = FocusDisabled,
 > {
     base: Div<V, I, F>,
     path: Option<SharedString>,
 }
 
-pub fn svg<V: 'static>() -> Svg<V, StatelessInteraction<V>, FocusDisabled> {
+pub fn svg<V: 'static>() -> Svg<V, StatelessInteractivity<V>, FocusDisabled> {
     Svg {
         base: div(),
         path: None,
@@ -24,7 +24,7 @@ pub fn svg<V: 'static>() -> Svg<V, StatelessInteraction<V>, FocusDisabled> {
 
 impl<V, I, F> Svg<V, I, F>
 where
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
     F: ElementFocus<V>,
 {
     pub fn path(mut self, path: impl Into<SharedString>) -> Self {
@@ -33,11 +33,11 @@ where
     }
 }
 
-impl<V, F> Svg<V, StatelessInteraction<V>, F>
+impl<V, F> Svg<V, StatelessInteractivity<V>, F>
 where
     F: ElementFocus<V>,
 {
-    pub fn id(self, id: impl Into<ElementId>) -> Svg<V, StatefulInteraction<V>, F> {
+    pub fn id(self, id: impl Into<ElementId>) -> Svg<V, StatefulInteractivity<V>, F> {
         Svg {
             base: self.base.id(id),
             path: self.path,
@@ -47,7 +47,7 @@ where
 
 impl<V, I, F> Component<V> for Svg<V, I, F>
 where
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
     F: ElementFocus<V>,
 {
     fn render(self) -> AnyElement<V> {
@@ -57,7 +57,7 @@ where
 
 impl<V, I, F> Element<V> for Svg<V, I, F>
 where
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
     F: ElementFocus<V>,
 {
     type ElementState = DivState;
@@ -107,7 +107,7 @@ where
 
 impl<V, I, F> Styled for Svg<V, I, F>
 where
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
     F: ElementFocus<V>,
 {
     fn style(&mut self) -> &mut StyleRefinement {
@@ -117,27 +117,27 @@ where
 
 impl<V, I, F> StatelessInteractive<V> for Svg<V, I, F>
 where
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
     F: ElementFocus<V>,
 {
-    fn stateless_interaction(&mut self) -> &mut StatelessInteraction<V> {
-        self.base.stateless_interaction()
+    fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V> {
+        self.base.stateless_interactivity()
     }
 }
 
-impl<V, F> StatefulInteractive<V> for Svg<V, StatefulInteraction<V>, F>
+impl<V, F> StatefulInteractive<V> for Svg<V, StatefulInteractivity<V>, F>
 where
     V: 'static,
     F: ElementFocus<V>,
 {
-    fn stateful_interaction(&mut self) -> &mut StatefulInteraction<V> {
-        self.base.stateful_interaction()
+    fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V> {
+        self.base.stateful_interactivity()
     }
 }
 
 impl<V: 'static, I> Focusable<V> for Svg<V, I, FocusEnabled<V>>
 where
-    I: ElementInteraction<V>,
+    I: ElementInteractivity<V>,
 {
     fn focus_listeners(&mut self) -> &mut FocusListeners<V> {
         self.base.focus_listeners()

crates/gpui2/src/interactive.rs 🔗

@@ -25,13 +25,13 @@ const TOOLTIP_DELAY: Duration = Duration::from_millis(500);
 const TOOLTIP_OFFSET: Point<Pixels> = Point::new(px(10.0), px(8.0));
 
 pub trait StatelessInteractive<V: 'static>: Element<V> {
-    fn stateless_interaction(&mut self) -> &mut StatelessInteraction<V>;
+    fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V>;
 
     fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
     where
         Self: Sized,
     {
-        self.stateless_interaction().hover_style = f(StyleRefinement::default());
+        self.stateless_interactivity().hover_style = f(StyleRefinement::default());
         self
     }
 
@@ -43,7 +43,7 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     where
         Self: Sized,
     {
-        self.stateless_interaction().group_hover_style = Some(GroupStyle {
+        self.stateless_interactivity().group_hover_style = Some(GroupStyle {
             group: group_name.into(),
             style: f(StyleRefinement::default()),
         });
@@ -58,7 +58,7 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     where
         Self: Sized,
     {
-        self.stateless_interaction()
+        self.stateless_interactivity()
             .mouse_down_listeners
             .push(Box::new(move |view, event, bounds, phase, cx| {
                 if phase == DispatchPhase::Bubble
@@ -79,7 +79,7 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     where
         Self: Sized,
     {
-        self.stateless_interaction()
+        self.stateless_interactivity()
             .mouse_up_listeners
             .push(Box::new(move |view, event, bounds, phase, cx| {
                 if phase == DispatchPhase::Bubble
@@ -100,7 +100,7 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     where
         Self: Sized,
     {
-        self.stateless_interaction()
+        self.stateless_interactivity()
             .mouse_down_listeners
             .push(Box::new(move |view, event, bounds, phase, cx| {
                 if phase == DispatchPhase::Capture
@@ -121,7 +121,7 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     where
         Self: Sized,
     {
-        self.stateless_interaction()
+        self.stateless_interactivity()
             .mouse_up_listeners
             .push(Box::new(move |view, event, bounds, phase, cx| {
                 if phase == DispatchPhase::Capture
@@ -141,7 +141,7 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     where
         Self: Sized,
     {
-        self.stateless_interaction()
+        self.stateless_interactivity()
             .mouse_move_listeners
             .push(Box::new(move |view, event, bounds, phase, cx| {
                 if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
@@ -158,7 +158,7 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     where
         Self: Sized,
     {
-        self.stateless_interaction()
+        self.stateless_interactivity()
             .scroll_wheel_listeners
             .push(Box::new(move |view, event, bounds, phase, cx| {
                 if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
@@ -174,23 +174,48 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
         C: TryInto<DispatchContext>,
         C::Error: Debug,
     {
-        self.stateless_interaction().dispatch_context =
+        self.stateless_interactivity().dispatch_context =
             context.try_into().expect("invalid dispatch context");
         self
     }
 
+    /// Capture the given action, fires during the capture phase
+    fn capture_action<A: 'static>(
+        mut self,
+        listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.stateless_interactivity().key_listeners.push((
+            TypeId::of::<A>(),
+            Box::new(move |view, event, _, phase, cx| {
+                let event = event.downcast_ref().unwrap();
+                if phase == DispatchPhase::Capture {
+                    listener(view, event, cx)
+                }
+                None
+            }),
+        ));
+        self
+    }
+
+    /// Add a listener for the given action, fires during the bubble event phase
     fn on_action<A: 'static>(
         mut self,
-        listener: impl Fn(&mut V, &A, DispatchPhase, &mut ViewContext<V>) + 'static,
+        listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
     ) -> Self
     where
         Self: Sized,
     {
-        self.stateless_interaction().key_listeners.push((
+        self.stateless_interactivity().key_listeners.push((
             TypeId::of::<A>(),
             Box::new(move |view, event, _, phase, cx| {
                 let event = event.downcast_ref().unwrap();
-                listener(view, event, phase, cx);
+                if phase == DispatchPhase::Bubble {
+                    listener(view, event, cx)
+                }
+
                 None
             }),
         ));
@@ -204,7 +229,7 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     where
         Self: Sized,
     {
-        self.stateless_interaction().key_listeners.push((
+        self.stateless_interactivity().key_listeners.push((
             TypeId::of::<KeyDownEvent>(),
             Box::new(move |view, event, _, phase, cx| {
                 let event = event.downcast_ref().unwrap();
@@ -222,7 +247,7 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     where
         Self: Sized,
     {
-        self.stateless_interaction().key_listeners.push((
+        self.stateless_interactivity().key_listeners.push((
             TypeId::of::<KeyUpEvent>(),
             Box::new(move |view, event, _, phase, cx| {
                 let event = event.downcast_ref().unwrap();
@@ -237,7 +262,7 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     where
         Self: Sized,
     {
-        self.stateless_interaction()
+        self.stateless_interactivity()
             .drag_over_styles
             .push((TypeId::of::<S>(), f(StyleRefinement::default())));
         self
@@ -251,7 +276,7 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     where
         Self: Sized,
     {
-        self.stateless_interaction().group_drag_over_styles.push((
+        self.stateless_interactivity().group_drag_over_styles.push((
             TypeId::of::<S>(),
             GroupStyle {
                 group: group_name.into(),
@@ -268,7 +293,7 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     where
         Self: Sized,
     {
-        self.stateless_interaction().drop_listeners.push((
+        self.stateless_interactivity().drop_listeners.push((
             TypeId::of::<W>(),
             Box::new(move |view, dragged_view, cx| {
                 listener(view, dragged_view.downcast().unwrap(), cx);
@@ -279,13 +304,13 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
 }
 
 pub trait StatefulInteractive<V: 'static>: StatelessInteractive<V> {
-    fn stateful_interaction(&mut self) -> &mut StatefulInteraction<V>;
+    fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V>;
 
     fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
     where
         Self: Sized,
     {
-        self.stateful_interaction().active_style = f(StyleRefinement::default());
+        self.stateful_interactivity().active_style = f(StyleRefinement::default());
         self
     }
 
@@ -297,7 +322,7 @@ pub trait StatefulInteractive<V: 'static>: StatelessInteractive<V> {
     where
         Self: Sized,
     {
-        self.stateful_interaction().group_active_style = Some(GroupStyle {
+        self.stateful_interactivity().group_active_style = Some(GroupStyle {
             group: group_name.into(),
             style: f(StyleRefinement::default()),
         });
@@ -311,7 +336,7 @@ pub trait StatefulInteractive<V: 'static>: StatelessInteractive<V> {
     where
         Self: Sized,
     {
-        self.stateful_interaction()
+        self.stateful_interactivity()
             .click_listeners
             .push(Box::new(move |view, event, cx| listener(view, event, cx)));
         self
@@ -326,10 +351,10 @@ pub trait StatefulInteractive<V: 'static>: StatelessInteractive<V> {
         W: 'static + Render,
     {
         debug_assert!(
-            self.stateful_interaction().drag_listener.is_none(),
+            self.stateful_interactivity().drag_listener.is_none(),
             "calling on_drag more than once on the same element is not supported"
         );
-        self.stateful_interaction().drag_listener =
+        self.stateful_interactivity().drag_listener =
             Some(Box::new(move |view_state, cursor_offset, cx| AnyDrag {
                 view: listener(view_state, cx).into(),
                 cursor_offset,
@@ -342,10 +367,10 @@ pub trait StatefulInteractive<V: 'static>: StatelessInteractive<V> {
         Self: Sized,
     {
         debug_assert!(
-            self.stateful_interaction().hover_listener.is_none(),
+            self.stateful_interactivity().hover_listener.is_none(),
             "calling on_hover more than once on the same element is not supported"
         );
-        self.stateful_interaction().hover_listener = Some(Box::new(listener));
+        self.stateful_interactivity().hover_listener = Some(Box::new(listener));
         self
     }
 
@@ -358,10 +383,10 @@ pub trait StatefulInteractive<V: 'static>: StatelessInteractive<V> {
         W: 'static + Render,
     {
         debug_assert!(
-            self.stateful_interaction().tooltip_builder.is_none(),
+            self.stateful_interactivity().tooltip_builder.is_none(),
             "calling tooltip more than once on the same element is not supported"
         );
-        self.stateful_interaction().tooltip_builder = Some(Arc::new(move |view_state, cx| {
+        self.stateful_interactivity().tooltip_builder = Some(Arc::new(move |view_state, cx| {
             build_tooltip(view_state, cx).into()
         }));
 
@@ -369,11 +394,11 @@ pub trait StatefulInteractive<V: 'static>: StatelessInteractive<V> {
     }
 }
 
-pub trait ElementInteraction<V: 'static>: 'static {
-    fn as_stateless(&self) -> &StatelessInteraction<V>;
-    fn as_stateless_mut(&mut self) -> &mut StatelessInteraction<V>;
-    fn as_stateful(&self) -> Option<&StatefulInteraction<V>>;
-    fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteraction<V>>;
+pub trait ElementInteractivity<V: 'static>: 'static {
+    fn as_stateless(&self) -> &StatelessInteractivity<V>;
+    fn as_stateless_mut(&mut self) -> &mut StatelessInteractivity<V>;
+    fn as_stateful(&self) -> Option<&StatefulInteractivity<V>>;
+    fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity<V>>;
 
     fn initialize<R>(
         &mut self,
@@ -735,11 +760,11 @@ pub trait ElementInteraction<V: 'static>: 'static {
 }
 
 #[derive(Deref, DerefMut)]
-pub struct StatefulInteraction<V> {
+pub struct StatefulInteractivity<V> {
     pub id: ElementId,
     #[deref]
     #[deref_mut]
-    stateless: StatelessInteraction<V>,
+    stateless: StatelessInteractivity<V>,
     click_listeners: SmallVec<[ClickListener<V>; 2]>,
     active_style: StyleRefinement,
     group_active_style: Option<GroupStyle>,
@@ -748,29 +773,29 @@ pub struct StatefulInteraction<V> {
     tooltip_builder: Option<TooltipBuilder<V>>,
 }
 
-impl<V: 'static> ElementInteraction<V> for StatefulInteraction<V> {
-    fn as_stateful(&self) -> Option<&StatefulInteraction<V>> {
+impl<V: 'static> ElementInteractivity<V> for StatefulInteractivity<V> {
+    fn as_stateful(&self) -> Option<&StatefulInteractivity<V>> {
         Some(self)
     }
 
-    fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteraction<V>> {
+    fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity<V>> {
         Some(self)
     }
 
-    fn as_stateless(&self) -> &StatelessInteraction<V> {
+    fn as_stateless(&self) -> &StatelessInteractivity<V> {
         &self.stateless
     }
 
-    fn as_stateless_mut(&mut self) -> &mut StatelessInteraction<V> {
+    fn as_stateless_mut(&mut self) -> &mut StatelessInteractivity<V> {
         &mut self.stateless
     }
 }
 
-impl<V> From<ElementId> for StatefulInteraction<V> {
+impl<V> From<ElementId> for StatefulInteractivity<V> {
     fn from(id: ElementId) -> Self {
         Self {
             id,
-            stateless: StatelessInteraction::default(),
+            stateless: StatelessInteractivity::default(),
             click_listeners: SmallVec::new(),
             drag_listener: None,
             hover_listener: None,
@@ -783,7 +808,7 @@ impl<V> From<ElementId> for StatefulInteraction<V> {
 
 type DropListener<V> = dyn Fn(&mut V, AnyView, &mut ViewContext<V>) + 'static;
 
-pub struct StatelessInteraction<V> {
+pub struct StatelessInteractivity<V> {
     pub dispatch_context: DispatchContext,
     pub mouse_down_listeners: SmallVec<[MouseDownListener<V>; 2]>,
     pub mouse_up_listeners: SmallVec<[MouseUpListener<V>; 2]>,
@@ -797,9 +822,9 @@ pub struct StatelessInteraction<V> {
     drop_listeners: SmallVec<[(TypeId, Box<DropListener<V>>); 2]>,
 }
 
-impl<V> StatelessInteraction<V> {
-    pub fn into_stateful(self, id: impl Into<ElementId>) -> StatefulInteraction<V> {
-        StatefulInteraction {
+impl<V> StatelessInteractivity<V> {
+    pub fn into_stateful(self, id: impl Into<ElementId>) -> StatefulInteractivity<V> {
+        StatefulInteractivity {
             id: id.into(),
             stateless: self,
             click_listeners: SmallVec::new(),
@@ -877,7 +902,7 @@ impl InteractiveElementState {
     }
 }
 
-impl<V> Default for StatelessInteraction<V> {
+impl<V> Default for StatelessInteractivity<V> {
     fn default() -> Self {
         Self {
             dispatch_context: DispatchContext::default(),
@@ -895,20 +920,20 @@ impl<V> Default for StatelessInteraction<V> {
     }
 }
 
-impl<V: 'static> ElementInteraction<V> for StatelessInteraction<V> {
-    fn as_stateful(&self) -> Option<&StatefulInteraction<V>> {
+impl<V: 'static> ElementInteractivity<V> for StatelessInteractivity<V> {
+    fn as_stateful(&self) -> Option<&StatefulInteractivity<V>> {
         None
     }
 
-    fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteraction<V>> {
+    fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity<V>> {
         None
     }
 
-    fn as_stateless(&self) -> &StatelessInteraction<V> {
+    fn as_stateless(&self) -> &StatelessInteractivity<V> {
         self
     }
 
-    fn as_stateless_mut(&mut self) -> &mut StatelessInteraction<V> {
+    fn as_stateless_mut(&mut self) -> &mut StatelessInteractivity<V> {
         self
     }
 }

crates/gpui2/src/style.rs 🔗

@@ -277,7 +277,7 @@ impl Style {
     pub fn paint<V: 'static>(&self, bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) {
         let rem_size = cx.rem_size();
 
-        cx.stack(0, |cx| {
+        cx.with_z_index(0, |cx| {
             cx.paint_shadows(
                 bounds,
                 self.corner_radii.to_pixels(bounds.size, rem_size),
@@ -287,7 +287,7 @@ impl Style {
 
         let background_color = self.background.as_ref().and_then(Fill::color);
         if background_color.is_some() || self.is_border_visible() {
-            cx.stack(1, |cx| {
+            cx.with_z_index(1, |cx| {
                 cx.paint_quad(
                     bounds,
                     self.corner_radii.to_pixels(bounds.size, rem_size),

crates/gpui2/src/window.rs 🔗

@@ -1698,8 +1698,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         &mut self.window_cx
     }
 
-    pub fn stack<R>(&mut self, order: u32, f: impl FnOnce(&mut Self) -> R) -> R {
-        self.window.z_index_stack.push(order);
+    pub fn with_z_index<R>(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R {
+        self.window.z_index_stack.push(z_index);
         let result = f(self);
         self.window.z_index_stack.pop();
         result

crates/menu2/Cargo.toml 🔗

@@ -9,4 +9,5 @@ path = "src/menu2.rs"
 doctest = false
 
 [dependencies]
-gpui = { package = "gpui2", path = "../gpui2" }
+serde.workspace = true
+serde_derive.workspace = true

crates/menu2/src/menu2.rs 🔗

@@ -1,25 +1,25 @@
-// todo!(use actions! macro)
+use serde_derive::Deserialize;
 
-#[derive(Clone, Debug, Default, PartialEq)]
+#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
 pub struct Cancel;
 
-#[derive(Clone, Debug, Default, PartialEq)]
+#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
 pub struct Confirm;
 
-#[derive(Clone, Debug, Default, PartialEq)]
+#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
 pub struct SecondaryConfirm;
 
-#[derive(Clone, Debug, Default, PartialEq)]
+#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
 pub struct SelectPrev;
 
-#[derive(Clone, Debug, Default, PartialEq)]
+#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
 pub struct SelectNext;
 
-#[derive(Clone, Debug, Default, PartialEq)]
+#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
 pub struct SelectFirst;
 
-#[derive(Clone, Debug, Default, PartialEq)]
+#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
 pub struct SelectLast;
 
-#[derive(Clone, Debug, Default, PartialEq)]
+#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
 pub struct ShowContextMenu;

crates/picker2/src/picker2.rs 🔗

@@ -18,11 +18,11 @@
 //     Dismiss,
 // }
 
-use std::ops::Range;
+use std::cmp;
 
 use gpui::{
-    div, list, red, AppContext, Component, Div, Element, ElementId, ParentElement, Render, Styled,
-    ViewContext,
+    div, list, Component, ElementId, FocusHandle, Focusable, ParentElement, StatelessInteractive,
+    Styled, ViewContext,
 };
 
 // pub struct Picker<D> {
@@ -43,8 +43,9 @@ pub trait PickerDelegate: Sized + 'static {
 
     fn match_count(&self, picker_id: ElementId) -> usize;
 
-    //     fn selected_index(&self) -> usize;
-    //     fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
+    fn selected_index(&self, picker_id: ElementId) -> usize;
+    fn set_selected_index(&mut self, ix: usize, picker_id: ElementId, cx: &mut ViewContext<Self>);
+
     //     fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
     //     fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
     //     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
@@ -53,8 +54,6 @@ pub trait PickerDelegate: Sized + 'static {
     fn render_match(
         &self,
         ix: usize,
-        active: bool,
-        hovered: bool,
         selected: bool,
         picker_id: ElementId,
         cx: &mut ViewContext<Self>,
@@ -84,32 +83,72 @@ pub trait PickerDelegate: Sized + 'static {
 #[derive(Component)]
 pub struct Picker<V: PickerDelegate> {
     id: ElementId,
+    focus_handle: FocusHandle,
     phantom: std::marker::PhantomData<V>,
 }
 
 impl<V: PickerDelegate> Picker<V> {
-    pub fn new(id: impl Into<ElementId>) -> Self {
+    pub fn new(id: impl Into<ElementId>, focus_handle: FocusHandle) -> Self {
         Self {
             id: id.into(),
+            focus_handle,
             phantom: std::marker::PhantomData,
         }
     }
 }
 
 impl<V: 'static + PickerDelegate> Picker<V> {
-    pub fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        div().size_full().id(self.id.clone()).child(
-            list(
-                "candidates",
-                view.match_count(self.id.clone()),
-                move |this: &mut V, visible_range, cx| {
-                    visible_range
-                        .map(|ix| this.render_match(ix, false, false, false, self.id.clone(), cx))
-                        .collect()
-                },
+    pub fn render(self, view: &mut V, _cx: &mut ViewContext<V>) -> impl Component<V> {
+        let id = self.id.clone();
+        div()
+            .size_full()
+            .id(self.id.clone())
+            .track_focus(&self.focus_handle)
+            .context("picker")
+            .on_focus(|v, e, cx| {
+                dbg!("FOCUSED!");
+            })
+            .on_blur(|v, e, cx| {
+                dbg!("BLURRED!");
+            })
+            .on_action({
+                let id = id.clone();
+                move |view: &mut V, _: &menu::SelectNext, cx| {
+                    let index = view.selected_index(id.clone());
+                    let count = view.match_count(id.clone());
+                    if count > 0 {
+                        view.set_selected_index(cmp::min(index + 1, count - 1), id.clone(), cx);
+                    }
+                }
+            })
+            .on_action({
+                let id = id.clone();
+                move |view, _: &menu::SelectPrev, cx| {
+                    let index = view.selected_index(id.clone());
+                    let count = view.match_count(id.clone());
+                    if count > 0 {
+                        view.set_selected_index((index + 1) % count, id.clone(), cx);
+                    }
+                }
+            })
+            .on_action(|view, _: &menu::SelectFirst, cx| {})
+            .on_action(|view, _: &menu::SelectLast, cx| {})
+            .on_action(|view, _: &menu::Cancel, cx| {})
+            .on_action(|view, _: &menu::Confirm, cx| {})
+            .on_action(|view, _: &menu::SecondaryConfirm, cx| {})
+            .child(
+                list(
+                    "candidates",
+                    view.match_count(self.id.clone()),
+                    move |view: &mut V, visible_range, cx| {
+                        let selected_ix = view.selected_index(self.id.clone());
+                        visible_range
+                            .map(|ix| view.render_match(ix, ix == selected_ix, self.id.clone(), cx))
+                            .collect()
+                    },
+                )
+                .size_full(),
             )
-            .size_full(),
-        )
     }
 }
 

crates/storybook2/Cargo.toml 🔗

@@ -25,6 +25,7 @@ smallvec.workspace = true
 strum = { version = "0.25.0", features = ["derive"] }
 theme = { path = "../theme" }
 theme2 = { path = "../theme2" }
+menu = { package = "menu2", path = "../menu2" }
 ui = { package = "ui2", path = "../ui2", features = ["stories"] }
 util = { path = "../util" }
 picker = { package = "picker2", path = "../picker2" }

crates/storybook2/src/stories/focus.rs 🔗

@@ -1,5 +1,5 @@
 use gpui::{
-    div, Div, FocusEnabled, Focusable, KeyBinding, ParentElement, Render, StatefulInteraction,
+    div, Div, FocusEnabled, Focusable, KeyBinding, ParentElement, Render, StatefulInteractivity,
     StatelessInteractive, Styled, View, VisualContext, WindowContext,
 };
 use serde::Deserialize;
@@ -31,7 +31,7 @@ impl FocusStory {
 }
 
 impl Render for FocusStory {
-    type Element = Div<Self, StatefulInteraction<Self>, FocusEnabled<Self>>;
+    type Element = Div<Self, StatefulInteractivity<Self>, FocusEnabled<Self>>;
 
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
         let theme = cx.theme();
@@ -48,20 +48,18 @@ impl Render for FocusStory {
             .id("parent")
             .focusable()
             .context("parent")
-            .on_action(|_, action: &ActionA, phase, cx| {
-                println!("Action A dispatched on parent during {:?}", phase);
+            .on_action(|_, action: &ActionA, cx| {
+                println!("Action A dispatched on parent during");
             })
-            .on_action(|_, action: &ActionB, phase, cx| {
-                println!("Action B dispatched on parent during {:?}", phase);
+            .on_action(|_, action: &ActionB, cx| {
+                println!("Action B dispatched on parent during");
             })
             .on_focus(|_, _, _| println!("Parent focused"))
             .on_blur(|_, _, _| println!("Parent blurred"))
             .on_focus_in(|_, _, _| println!("Parent focus_in"))
             .on_focus_out(|_, _, _| println!("Parent focus_out"))
-            .on_key_down(|_, event, phase, _| {
-                println!("Key down on parent {:?} {:?}", phase, event)
-            })
-            .on_key_up(|_, event, phase, _| println!("Key up on parent {:?} {:?}", phase, event))
+            .on_key_down(|_, event, phase, _| println!("Key down on parent {:?}", event))
+            .on_key_up(|_, event, phase, _| println!("Key up on parent {:?}", event))
             .size_full()
             .bg(color_1)
             .focus(|style| style.bg(color_2))
@@ -70,8 +68,8 @@ impl Render for FocusStory {
                 div()
                     .track_focus(&child_1)
                     .context("child-1")
-                    .on_action(|_, action: &ActionB, phase, cx| {
-                        println!("Action B dispatched on child 1 during {:?}", phase);
+                    .on_action(|_, action: &ActionB, cx| {
+                        println!("Action B dispatched on child 1 during");
                     })
                     .w_full()
                     .h_6()
@@ -82,20 +80,16 @@ impl Render for FocusStory {
                     .on_blur(|_, _, _| println!("Child 1 blurred"))
                     .on_focus_in(|_, _, _| println!("Child 1 focus_in"))
                     .on_focus_out(|_, _, _| println!("Child 1 focus_out"))
-                    .on_key_down(|_, event, phase, _| {
-                        println!("Key down on child 1 {:?} {:?}", phase, event)
-                    })
-                    .on_key_up(|_, event, phase, _| {
-                        println!("Key up on child 1 {:?} {:?}", phase, event)
-                    })
+                    .on_key_down(|_, event, phase, _| println!("Key down on child 1 {:?}", event))
+                    .on_key_up(|_, event, phase, _| println!("Key up on child 1 {:?}", event))
                     .child("Child 1"),
             )
             .child(
                 div()
                     .track_focus(&child_2)
                     .context("child-2")
-                    .on_action(|_, action: &ActionC, phase, cx| {
-                        println!("Action C dispatched on child 2 during {:?}", phase);
+                    .on_action(|_, action: &ActionC, cx| {
+                        println!("Action C dispatched on child 2 during");
                     })
                     .w_full()
                     .h_6()
@@ -104,12 +98,8 @@ impl Render for FocusStory {
                     .on_blur(|_, _, _| println!("Child 2 blurred"))
                     .on_focus_in(|_, _, _| println!("Child 2 focus_in"))
                     .on_focus_out(|_, _, _| println!("Child 2 focus_out"))
-                    .on_key_down(|_, event, phase, _| {
-                        println!("Key down on child 2 {:?} {:?}", phase, event)
-                    })
-                    .on_key_up(|_, event, phase, _| {
-                        println!("Key up on child 2 {:?} {:?}", phase, event)
-                    })
+                    .on_key_down(|_, event, phase, _| println!("Key down on child 2 {:?}", event))
+                    .on_key_up(|_, event, phase, _| println!("Key up on child 2 {:?}", event))
                     .child("Child 2"),
             )
     }

crates/storybook2/src/stories/kitchen_sink.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{story::Story, story_selector::ComponentStory};
-use gpui::{Div, Render, StatefulInteraction, View, VisualContext};
+use gpui::{Div, Render, StatefulInteractivity, View, VisualContext};
 use strum::IntoEnumIterator;
 use ui::prelude::*;
 
@@ -12,7 +12,7 @@ impl KitchenSinkStory {
 }
 
 impl Render for KitchenSinkStory {
-    type Element = Div<Self, StatefulInteraction<Self>>;
+    type Element = Div<Self, StatefulInteractivity<Self>>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         let component_stories = ComponentStory::iter()

crates/storybook2/src/stories/picker.rs 🔗

@@ -1,15 +1,18 @@
 use gpui::{
-    black, div, red, Div, Fill, ParentElement, Render, SharedString, Styled, View, VisualContext,
-    WindowContext,
+    div, Component, Div, FocusHandle, KeyBinding, ParentElement, Render, SharedString,
+    StatelessInteractive, Styled, View, VisualContext, WindowContext,
 };
 use picker::{Picker, PickerDelegate};
+use theme2::ActiveTheme;
 
 pub struct PickerStory {
+    selected_ix: usize,
     candidates: Vec<SharedString>,
+    focus_handle: FocusHandle,
 }
 
 impl PickerDelegate for PickerStory {
-    type ListItem = SharedString;
+    type ListItem = Div<Self>;
 
     fn match_count(&self, _picker_id: gpui::ElementId) -> usize {
         self.candidates.len()
@@ -18,46 +21,118 @@ impl PickerDelegate for PickerStory {
     fn render_match(
         &self,
         ix: usize,
-        _active: bool,
-        _hovered: bool,
-        _selected: bool,
+        selected: bool,
         _picker_id: gpui::ElementId,
         cx: &mut gpui::ViewContext<Self>,
     ) -> Self::ListItem {
-        self.candidates[ix].clone()
+        let colors = cx.theme().colors();
+
+        div()
+            .text_color(colors.text)
+            .when(selected, |s| {
+                s.border_l_10().border_color(colors.terminal_ansi_yellow)
+            })
+            .hover(|style| {
+                style
+                    .bg(colors.element_active)
+                    .text_color(colors.text_accent)
+            })
+            .child(self.candidates[ix].clone())
+    }
+
+    fn selected_index(&self, picker_id: gpui::ElementId) -> usize {
+        self.selected_ix
+    }
+
+    fn set_selected_index(
+        &mut self,
+        ix: usize,
+        _picker_id: gpui::ElementId,
+        _cx: &mut gpui::ViewContext<Self>,
+    ) {
+        self.selected_ix = ix;
     }
 }
 
 impl PickerStory {
     pub fn new(cx: &mut WindowContext) -> View<Self> {
-        cx.build_view(|cx| PickerStory {
-            candidates: vec![
-                "Pizza (Italy)".into(),
-                "Sushi (Japan)".into(),
-                "Paella (Spain)".into(),
-                "Tacos (Mexico)".into(),
-                "Peking Duck (China)".into(),
-                "Fish and Chips (UK)".into(),
-                "Croissant (France)".into(),
-                "Bratwurst (Germany)".into(),
-                "Poutine (Canada)".into(),
-                "Chicken Tikka Masala (India)".into(),
-                "Feijoada (Brazil)".into(),
-                "Kimchi (Korea)".into(),
-                "Borscht (Ukraine)".into(),
-                "Falafel (Middle East)".into(),
-                "Baklava (Turkey)".into(),
-                "Shepherd's Pie (Ireland)".into(),
-                "Rendang (Indonesia)".into(),
-                "Kebab (Middle East)".into(),
-                "Ceviche (Peru)".into(),
-                "Pierogi (Poland)".into(),
-                "Churrasco (Brazil)".into(),
-                "Moussaka (Greece)".into(),
-                "Lasagna (Italy)".into(),
-                "Pad Thai (Thailand)".into(),
-                "Pho (Vietnam)".into(),
-            ],
+        cx.build_view(|cx| {
+            cx.bind_keys([
+                KeyBinding::new("up", menu::SelectPrev, Some("picker")),
+                KeyBinding::new("pageup", menu::SelectFirst, Some("picker")),
+                KeyBinding::new("shift-pageup", menu::SelectFirst, Some("picker")),
+                KeyBinding::new("ctrl-p", menu::SelectPrev, Some("picker")),
+                KeyBinding::new("down", menu::SelectNext, Some("picker")),
+                KeyBinding::new("pagedown", menu::SelectLast, Some("picker")),
+                KeyBinding::new("shift-pagedown", menu::SelectFirst, Some("picker")),
+                KeyBinding::new("ctrl-n", menu::SelectNext, Some("picker")),
+                KeyBinding::new("cmd-up", menu::SelectFirst, Some("picker")),
+                KeyBinding::new("cmd-down", menu::SelectLast, Some("picker")),
+                KeyBinding::new("enter", menu::Confirm, Some("picker")),
+                KeyBinding::new("ctrl-enter", menu::ShowContextMenu, Some("picker")),
+                KeyBinding::new("cmd-enter", menu::SecondaryConfirm, Some("picker")),
+                KeyBinding::new("escape", menu::Cancel, Some("picker")),
+                KeyBinding::new("ctrl-c", menu::Cancel, Some("picker")),
+            ]);
+
+            let fh = cx.focus_handle();
+            cx.focus(&fh);
+
+            PickerStory {
+                focus_handle: fh,
+                candidates: vec![
+                    "Baguette (France)".into(),
+                    "Baklava (Turkey)".into(),
+                    "Beef Wellington (UK)".into(),
+                    "Biryani (India)".into(),
+                    "Borscht (Ukraine)".into(),
+                    "Bratwurst (Germany)".into(),
+                    "Bulgogi (Korea)".into(),
+                    "Burrito (USA)".into(),
+                    "Ceviche (Peru)".into(),
+                    "Chicken Tikka Masala (India)".into(),
+                    "Churrasco (Brazil)".into(),
+                    "Couscous (North Africa)".into(),
+                    "Croissant (France)".into(),
+                    "Dim Sum (China)".into(),
+                    "Empanada (Argentina)".into(),
+                    "Fajitas (Mexico)".into(),
+                    "Falafel (Middle East)".into(),
+                    "Feijoada (Brazil)".into(),
+                    "Fish and Chips (UK)".into(),
+                    "Fondue (Switzerland)".into(),
+                    "Goulash (Hungary)".into(),
+                    "Haggis (Scotland)".into(),
+                    "Kebab (Middle East)".into(),
+                    "Kimchi (Korea)".into(),
+                    "Lasagna (Italy)".into(),
+                    "Maple Syrup Pancakes (Canada)".into(),
+                    "Moussaka (Greece)".into(),
+                    "Pad Thai (Thailand)".into(),
+                    "Paella (Spain)".into(),
+                    "Pancakes (USA)".into(),
+                    "Pasta Carbonara (Italy)".into(),
+                    "Pavlova (Australia)".into(),
+                    "Peking Duck (China)".into(),
+                    "Pho (Vietnam)".into(),
+                    "Pierogi (Poland)".into(),
+                    "Pizza (Italy)".into(),
+                    "Poutine (Canada)".into(),
+                    "Pretzel (Germany)".into(),
+                    "Ramen (Japan)".into(),
+                    "Rendang (Indonesia)".into(),
+                    "Sashimi (Japan)".into(),
+                    "Satay (Indonesia)".into(),
+                    "Shepherd's Pie (Ireland)".into(),
+                    "Sushi (Japan)".into(),
+                    "Tacos (Mexico)".into(),
+                    "Tandoori Chicken (India)".into(),
+                    "Tortilla (Spain)".into(),
+                    "Tzatziki (Greece)".into(),
+                    "Wiener Schnitzel (Austria)".into(),
+                ],
+                selected_ix: 0,
+            }
         })
     }
 }
@@ -66,9 +141,11 @@ impl Render for PickerStory {
     type Element = Div<Self>;
 
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
+        let theme = cx.theme();
+
         div()
-            .text_color(red())
+            .bg(theme.styles.colors.background)
             .size_full()
-            .child(Picker::new("picker_story"))
+            .child(Picker::new("picker_story", self.focus_handle.clone()))
     }
 }

crates/storybook2/src/stories/scroll.rs 🔗

@@ -1,5 +1,5 @@
 use gpui::{
-    div, px, Component, Div, ParentElement, Render, SharedString, StatefulInteraction, Styled,
+    div, px, Component, Div, ParentElement, Render, SharedString, StatefulInteractivity, Styled,
     View, VisualContext, WindowContext,
 };
 use theme2::ActiveTheme;
@@ -13,7 +13,7 @@ impl ScrollStory {
 }
 
 impl Render for ScrollStory {
-    type Element = Div<Self, StatefulInteraction<Self>>;
+    type Element = Div<Self, StatefulInteractivity<Self>>;
 
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
         let theme = cx.theme();

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

@@ -128,7 +128,7 @@ impl<V: 'static> Checkbox<V> {
             // click area for the checkbox.
             .size_5()
             // Because we've enlarged the click area, we need to create a
-            // `group` to pass down interaction events to the checkbox.
+            // `group` to pass down interactivity events to the checkbox.
             .group(group_id.clone())
             .child(
                 div()
@@ -148,7 +148,7 @@ impl<V: 'static> Checkbox<V> {
                     .bg(bg_color)
                     .border()
                     .border_color(border_color)
-                    // We only want the interaction states to fire when we
+                    // We only want the interactivity states to fire when we
                     // are in a checkbox that isn't disabled.
                     .when(!self.disabled, |this| {
                         // Here instead of `hover()` we use `group_hover()`