Add more detail to panel switcher interaction (#3344)

Conrad Irwin created

Also featuring:
* IconButton#action()
* gpui::ManagedView
* `menu_handle()`

Release Notes:

- N/A

Change summary

Cargo.lock                                     |  11 
Cargo.toml                                     |   1 
crates/command_palette2/src/command_palette.rs |  16 
crates/file_finder2/src/file_finder.rs         |  21 
crates/go_to_line2/src/go_to_line.rs           |  19 
crates/gpui2/src/elements/div.rs               |  12 
crates/gpui2/src/elements/overlay.rs           |  39 +
crates/gpui2/src/platform/mac/window.rs        |  36 
crates/gpui2/src/window.rs                     |  26 +
crates/picker2/src/picker2.rs                  |  10 
crates/storybook2/src/storybook2.rs            |   2 
crates/storybook3/Cargo.toml                   |  17 
crates/storybook3/src/storybook3.rs            |  73 +++
crates/terminal_view2/src/terminal_view.rs     |  16 
crates/ui2/src/components/context_menu.rs      | 384 ++++++++++++++++---
crates/ui2/src/components/icon_button.rs       |  23 +
crates/ui2/src/components/list.rs              |  24 +
crates/ui2/src/story.rs                        |   1 
crates/ui2/src/styled_ext.rs                   |   1 
crates/workspace2/src/dock.rs                  |  58 ++
crates/workspace2/src/modal_layer.rs           |  22 
crates/workspace2/src/workspace2.rs            |  37 +
22 files changed, 663 insertions(+), 186 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -8823,6 +8823,17 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "storybook3"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui2",
+ "settings2",
+ "theme2",
+ "ui2",
+]
+
 [[package]]
 name = "stringprep"
 version = "0.1.4"

Cargo.toml 🔗

@@ -95,6 +95,7 @@ members = [
     "crates/sqlez_macros",
     "crates/rich_text",
     "crates/storybook2",
+    "crates/storybook3",
     "crates/sum_tree",
     "crates/terminal",
     "crates/terminal2",

crates/command_palette2/src/command_palette.rs 🔗

@@ -1,9 +1,8 @@
 use collections::{CommandPaletteFilter, HashMap};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle,
-    Keystroke, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
-    WindowContext,
+    actions, div, prelude::*, Action, AppContext, Component, Dismiss, Div, FocusHandle, Keystroke,
+    ManagedView, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use std::{
@@ -16,7 +15,7 @@ use util::{
     channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
     ResultExt,
 };
-use workspace::{Modal, ModalEvent, Workspace};
+use workspace::Workspace;
 use zed_actions::OpenZedURL;
 
 actions!(Toggle);
@@ -69,10 +68,9 @@ impl CommandPalette {
     }
 }
 
-impl EventEmitter<ModalEvent> for CommandPalette {}
-impl Modal for CommandPalette {
-    fn focus(&self, cx: &mut WindowContext) {
-        self.picker.update(cx, |picker, cx| picker.focus(cx));
+impl ManagedView for CommandPalette {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
     }
 }
 
@@ -267,7 +265,7 @@ impl PickerDelegate for CommandPaletteDelegate {
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
         self.command_palette
-            .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
+            .update(cx, |_, cx| cx.emit(Dismiss))
             .log_err();
     }
 

crates/file_finder2/src/file_finder.rs 🔗

@@ -2,9 +2,9 @@ use collections::HashMap;
 use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
 use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
 use gpui::{
-    actions, div, AppContext, Component, Div, EventEmitter, InteractiveComponent, Model,
-    ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
-    WindowContext,
+    actions, div, AppContext, Component, Dismiss, Div, FocusHandle, InteractiveComponent,
+    ManagedView, Model, ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext,
+    WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
@@ -19,7 +19,7 @@ use text::Point;
 use theme::ActiveTheme;
 use ui::{v_stack, HighlightedLabel, StyledExt};
 use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
-use workspace::{Modal, ModalEvent, Workspace};
+use workspace::Workspace;
 
 actions!(Toggle);
 
@@ -111,10 +111,9 @@ impl FileFinder {
     }
 }
 
-impl EventEmitter<ModalEvent> for FileFinder {}
-impl Modal for FileFinder {
-    fn focus(&self, cx: &mut WindowContext) {
-        self.picker.update(cx, |picker, cx| picker.focus(cx))
+impl ManagedView for FileFinder {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
     }
 }
 impl Render for FileFinder {
@@ -689,9 +688,7 @@ impl PickerDelegate for FileFinderDelegate {
                                 .log_err();
                         }
                     }
-                    finder
-                        .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed))
-                        .ok()?;
+                    finder.update(&mut cx, |_, cx| cx.emit(Dismiss)).ok()?;
 
                     Some(())
                 })
@@ -702,7 +699,7 @@ impl PickerDelegate for FileFinderDelegate {
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
         self.file_finder
-            .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
+            .update(cx, |_, cx| cx.emit(Dismiss))
             .log_err();
     }
 

crates/go_to_line2/src/go_to_line.rs 🔗

@@ -1,13 +1,13 @@
 use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
 use gpui::{
-    actions, div, prelude::*, AppContext, Div, EventEmitter, ParentComponent, Render, SharedString,
-    Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
+    actions, div, prelude::*, AppContext, Dismiss, Div, FocusHandle, ManagedView, ParentComponent,
+    Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
 };
 use text::{Bias, Point};
 use theme::ActiveTheme;
 use ui::{h_stack, v_stack, Label, StyledExt, TextColor};
 use util::paths::FILE_ROW_COLUMN_DELIMITER;
-use workspace::{Modal, ModalEvent, Workspace};
+use workspace::Workspace;
 
 actions!(Toggle);
 
@@ -23,10 +23,9 @@ pub struct GoToLine {
     _subscriptions: Vec<Subscription>,
 }
 
-impl EventEmitter<ModalEvent> for GoToLine {}
-impl Modal for GoToLine {
-    fn focus(&self, cx: &mut WindowContext) {
-        self.line_editor.update(cx, |editor, cx| editor.focus(cx))
+impl ManagedView for GoToLine {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.line_editor.focus_handle(cx)
     }
 }
 
@@ -88,7 +87,7 @@ impl GoToLine {
     ) {
         match event {
             // todo!() this isn't working...
-            editor::Event::Blurred => cx.emit(ModalEvent::Dismissed),
+            editor::Event::Blurred => cx.emit(Dismiss),
             editor::Event::BufferEdited { .. } => self.highlight_current_line(cx),
             _ => {}
         }
@@ -123,7 +122,7 @@ impl GoToLine {
     }
 
     fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(ModalEvent::Dismissed);
+        cx.emit(Dismiss);
     }
 
     fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
@@ -140,7 +139,7 @@ impl GoToLine {
             self.prev_scroll_position.take();
         }
 
-        cx.emit(ModalEvent::Dismissed);
+        cx.emit(Dismiss);
     }
 }
 

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

@@ -960,11 +960,11 @@ where
                             cx.background_executor().timer(TOOLTIP_DELAY).await;
                             view.update(&mut cx, move |view_state, cx| {
                                 active_tooltip.borrow_mut().replace(ActiveTooltip {
-                                    waiting: None,
                                     tooltip: Some(AnyTooltip {
                                         view: tooltip_builder(view_state, cx),
                                         cursor_offset: cx.mouse_position(),
                                     }),
+                                    _task: None,
                                 });
                                 cx.notify();
                             })
@@ -972,12 +972,17 @@ where
                         }
                     });
                     active_tooltip.borrow_mut().replace(ActiveTooltip {
-                        waiting: Some(task),
                         tooltip: None,
+                        _task: Some(task),
                     });
                 }
             });
 
+            let active_tooltip = element_state.active_tooltip.clone();
+            cx.on_mouse_event(move |_, _: &MouseDownEvent, _, _| {
+                active_tooltip.borrow_mut().take();
+            });
+
             if let Some(active_tooltip) = element_state.active_tooltip.borrow().as_ref() {
                 if active_tooltip.tooltip.is_some() {
                     cx.active_tooltip = active_tooltip.tooltip.clone()
@@ -1207,9 +1212,8 @@ pub struct InteractiveElementState {
 }
 
 pub struct ActiveTooltip {
-    #[allow(unused)] // used to drop the task
-    waiting: Option<Task<()>>,
     tooltip: Option<AnyTooltip>,
+    _task: Option<Task<()>>,
 }
 
 /// Whether or not the element or a group that contains it is clicked by the mouse.

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

@@ -1,8 +1,9 @@
 use smallvec::SmallVec;
+use taffy::style::{Display, Position};
 
 use crate::{
-    point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentComponent, Pixels, Point,
-    Size, Style,
+    point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels,
+    Point, Size, Style,
 };
 
 pub struct OverlayState {
@@ -14,7 +15,7 @@ pub struct Overlay<V> {
     anchor_corner: AnchorCorner,
     fit_mode: OverlayFitMode,
     // todo!();
-    // anchor_position: Option<Vector2F>,
+    anchor_position: Option<Point<Pixels>>,
     // position_mode: OverlayPositionMode,
 }
 
@@ -25,6 +26,7 @@ pub fn overlay<V: 'static>() -> Overlay<V> {
         children: SmallVec::new(),
         anchor_corner: AnchorCorner::TopLeft,
         fit_mode: OverlayFitMode::SwitchAnchor,
+        anchor_position: None,
     }
 }
 
@@ -35,6 +37,13 @@ impl<V> Overlay<V> {
         self
     }
 
+    /// Sets the position in window co-ordinates
+    /// (otherwise the location the overlay is rendered is used)
+    pub fn position(mut self, anchor: Point<Pixels>) -> Self {
+        self.anchor_position = Some(anchor);
+        self
+    }
+
     /// Snap to window edge instead of switching anchor corner when an overflow would occur.
     pub fn snap_to_window(mut self) -> Self {
         self.fit_mode = OverlayFitMode::SnapToWindow;
@@ -48,6 +57,12 @@ impl<V: 'static> ParentComponent<V> for Overlay<V> {
     }
 }
 
+impl<V: 'static> Component<V> for Overlay<V> {
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
+    }
+}
+
 impl<V: 'static> Element<V> for Overlay<V> {
     type ElementState = OverlayState;
 
@@ -66,7 +81,12 @@ impl<V: 'static> Element<V> for Overlay<V> {
             .iter_mut()
             .map(|child| child.layout(view_state, cx))
             .collect::<SmallVec<_>>();
-        let layout_id = cx.request_layout(&Style::default(), child_layout_ids.iter().copied());
+
+        let mut overlay_style = Style::default();
+        overlay_style.position = Position::Absolute;
+        overlay_style.display = Display::Flex;
+
+        let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied());
 
         (layout_id, OverlayState { child_layout_ids })
     }
@@ -90,7 +110,7 @@ impl<V: 'static> Element<V> for Overlay<V> {
             child_max = child_max.max(&child_bounds.lower_right());
         }
         let size: Size<Pixels> = (child_max - child_min).into();
-        let origin = bounds.origin;
+        let origin = self.anchor_position.unwrap_or(bounds.origin);
 
         let mut desired = self.anchor_corner.get_bounds(origin, size);
         let limits = Bounds {
@@ -184,6 +204,15 @@ impl AnchorCorner {
         Bounds { origin, size }
     }
 
+    pub fn corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
+        match self {
+            Self::TopLeft => bounds.origin,
+            Self::TopRight => bounds.upper_right(),
+            Self::BottomLeft => bounds.lower_left(),
+            Self::BottomRight => bounds.lower_right(),
+        }
+    }
+
     fn switch_axis(self, axis: Axis) -> Self {
         match axis {
             Axis::Vertical => match self {

crates/gpui2/src/platform/mac/window.rs 🔗

@@ -1141,7 +1141,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
     let event = unsafe { InputEvent::from_native(native_event, Some(window_height)) };
 
     if let Some(mut event) = event {
-        let synthesized_second_event = match &mut event {
+        match &mut event {
             InputEvent::MouseDown(
                 event @ MouseDownEvent {
                     button: MouseButton::Left,
@@ -1149,6 +1149,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
                     ..
                 },
             ) => {
+                // On mac, a ctrl-left click should be handled as a right click.
                 *event = MouseDownEvent {
                     button: MouseButton::Right,
                     modifiers: Modifiers {
@@ -1158,26 +1159,30 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
                     click_count: 1,
                     ..*event
                 };
-
-                Some(InputEvent::MouseDown(MouseDownEvent {
-                    button: MouseButton::Right,
-                    ..*event
-                }))
             }
 
             // Because we map a ctrl-left_down to a right_down -> right_up let's ignore
             // the ctrl-left_up to avoid having a mismatch in button down/up events if the
             // user is still holding ctrl when releasing the left mouse button
-            InputEvent::MouseUp(MouseUpEvent {
-                button: MouseButton::Left,
-                modifiers: Modifiers { control: true, .. },
-                ..
-            }) => {
-                lock.synthetic_drag_counter += 1;
-                return;
+            InputEvent::MouseUp(
+                event @ MouseUpEvent {
+                    button: MouseButton::Left,
+                    modifiers: Modifiers { control: true, .. },
+                    ..
+                },
+            ) => {
+                *event = MouseUpEvent {
+                    button: MouseButton::Right,
+                    modifiers: Modifiers {
+                        control: false,
+                        ..event.modifiers
+                    },
+                    click_count: 1,
+                    ..*event
+                };
             }
 
-            _ => None,
+            _ => {}
         };
 
         match &event {
@@ -1227,9 +1232,6 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
         if let Some(mut callback) = lock.event_callback.take() {
             drop(lock);
             callback(event);
-            if let Some(event) = synthesized_second_event {
-                callback(event);
-            }
             window_state.lock().event_callback = Some(callback);
         }
     }

crates/gpui2/src/window.rs 🔗

@@ -185,10 +185,27 @@ impl Drop for FocusHandle {
     }
 }
 
+/// FocusableView allows users of your view to easily
+/// focus it (using cx.focus_view(view))
 pub trait FocusableView: Render {
     fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
 }
 
+/// ManagedView is a view (like a Modal, Popover, Menu, etc.)
+/// where the lifecycle of the view is handled by another view.
+pub trait ManagedView: Render {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
+}
+
+pub struct Dismiss;
+impl<T: ManagedView> EventEmitter<Dismiss> for T {}
+
+impl<T: ManagedView> FocusableView for T {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.focus_handle(cx)
+    }
+}
+
 // Holds the state for a specific window.
 pub struct Window {
     pub(crate) handle: AnyWindowHandle,
@@ -574,6 +591,7 @@ impl<'a> WindowContext<'a> {
         result
     }
 
+    #[must_use]
     /// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which
     /// layout is being requested, along with the layout ids of any children. This method is called during
     /// calls to the `Element::layout` trait method and enables any element to participate in layout.
@@ -1150,6 +1168,14 @@ impl<'a> WindowContext<'a> {
                 self.window.mouse_position = mouse_move.position;
                 InputEvent::MouseMove(mouse_move)
             }
+            InputEvent::MouseDown(mouse_down) => {
+                self.window.mouse_position = mouse_down.position;
+                InputEvent::MouseDown(mouse_down)
+            }
+            InputEvent::MouseUp(mouse_up) => {
+                self.window.mouse_position = mouse_up.position;
+                InputEvent::MouseUp(mouse_up)
+            }
             // Translate dragging and dropping of external files from the operating system
             // to internal drag and drop events.
             InputEvent::FileDrop(file_drop) => match file_drop {

crates/picker2/src/picker2.rs 🔗

@@ -1,7 +1,7 @@
 use editor::Editor;
 use gpui::{
-    div, prelude::*, uniform_list, Component, Div, MouseButton, Render, Task,
-    UniformListScrollHandle, View, ViewContext, WindowContext,
+    div, prelude::*, uniform_list, AppContext, Component, Div, FocusHandle, FocusableView,
+    MouseButton, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
 };
 use std::{cmp, sync::Arc};
 use ui::{prelude::*, v_stack, Divider, Label, TextColor};
@@ -35,6 +35,12 @@ pub trait PickerDelegate: Sized + 'static {
     ) -> Self::ListItem;
 }
 
+impl<D: PickerDelegate> FocusableView for Picker<D> {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.editor.focus_handle(cx)
+    }
+}
+
 impl<D: PickerDelegate> Picker<D> {
     pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
         let editor = cx.build_view(|cx| {

crates/storybook2/src/storybook2.rs 🔗

@@ -66,7 +66,6 @@ fn main() {
             story_selector.unwrap_or(StorySelector::Component(ComponentStory::Workspace));
 
         let theme_registry = cx.global::<ThemeRegistry>();
-
         let mut theme_settings = ThemeSettings::get_global(cx).clone();
         theme_settings.active_theme = theme_registry.get(&theme_name).unwrap();
         ThemeSettings::override_global(theme_settings, cx);
@@ -114,6 +113,7 @@ impl Render for StoryWrapper {
             .flex()
             .flex_col()
             .size_full()
+            .font("Zed Mono")
             .child(self.story.clone())
     }
 }

crates/storybook3/Cargo.toml 🔗

@@ -0,0 +1,17 @@
+[package]
+name = "storybook3"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[[bin]]
+name = "storybook"
+path = "src/storybook3.rs"
+
+[dependencies]
+anyhow.workspace = true
+
+gpui = { package = "gpui2", path = "../gpui2" }
+ui = { package = "ui2", path = "../ui2", features = ["stories"] }
+theme = { package = "theme2", path = "../theme2", features = ["stories"] }
+settings = { package = "settings2", path = "../settings2"}

crates/storybook3/src/storybook3.rs 🔗

@@ -0,0 +1,73 @@
+use anyhow::Result;
+use gpui::AssetSource;
+use gpui::{
+    div, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds,
+    WindowOptions,
+};
+use settings::{default_settings, Settings, SettingsStore};
+use std::borrow::Cow;
+use std::sync::Arc;
+use theme::ThemeSettings;
+use ui::{prelude::*, ContextMenuStory};
+
+struct Assets;
+
+impl AssetSource for Assets {
+    fn load(&self, _path: &str) -> Result<Cow<[u8]>> {
+        todo!();
+    }
+
+    fn list(&self, _path: &str) -> Result<Vec<SharedString>> {
+        Ok(vec![])
+    }
+}
+
+fn main() {
+    let asset_source = Arc::new(Assets);
+    gpui::App::production(asset_source).run(move |cx| {
+        let mut store = SettingsStore::default();
+        store
+            .set_default_settings(default_settings().as_ref(), cx)
+            .unwrap();
+        cx.set_global(store);
+        ui::settings::init(cx);
+        theme::init(theme::LoadThemes::JustBase, cx);
+
+        cx.open_window(
+            WindowOptions {
+                bounds: WindowBounds::Fixed(Bounds {
+                    origin: Default::default(),
+                    size: size(px(1500.), px(780.)).into(),
+                }),
+                ..Default::default()
+            },
+            move |cx| {
+                let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
+                cx.set_rem_size(ui_font_size);
+
+                cx.build_view(|cx| TestView {
+                    story: cx.build_view(|_| ContextMenuStory).into(),
+                })
+            },
+        );
+
+        cx.activate(true);
+    })
+}
+
+struct TestView {
+    story: AnyView,
+}
+
+impl Render for TestView {
+    type Element = Div<Self>;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        div()
+            .flex()
+            .flex_col()
+            .size_full()
+            .font("Helvetica")
+            .child(self.story.clone())
+    }
+}

crates/terminal_view2/src/terminal_view.rs 🔗

@@ -32,7 +32,7 @@ use workspace::{
     notifications::NotifyResultExt,
     register_deserializable_item,
     searchable::{SearchEvent, SearchOptions, SearchableItem},
-    ui::{ContextMenu, ContextMenuItem, Label},
+    ui::{ContextMenu, Label},
     CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
 };
 
@@ -85,7 +85,7 @@ pub struct TerminalView {
     has_new_content: bool,
     //Currently using iTerm bell, show bell emoji in tab until input is received
     has_bell: bool,
-    context_menu: Option<ContextMenu>,
+    context_menu: Option<View<ContextMenu>>,
     blink_state: bool,
     blinking_on: bool,
     blinking_paused: bool,
@@ -300,10 +300,14 @@ impl TerminalView {
         position: gpui::Point<Pixels>,
         cx: &mut ViewContext<Self>,
     ) {
-        self.context_menu = Some(ContextMenu::new(vec![
-            ContextMenuItem::entry(Label::new("Clear"), Clear),
-            ContextMenuItem::entry(Label::new("Close"), CloseActiveItem { save_intent: None }),
-        ]));
+        self.context_menu = Some(cx.build_view(|cx| {
+            ContextMenu::new(cx)
+                .entry(Label::new("Clear"), Box::new(Clear))
+                .entry(
+                    Label::new("Close"),
+                    Box::new(CloseActiveItem { save_intent: None }),
+                )
+        }));
         dbg!(&position);
         // todo!()
         //     self.context_menu

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

@@ -1,82 +1,258 @@
-use crate::{prelude::*, ListItemVariant};
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use crate::prelude::*;
 use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader};
+use gpui::{
+    overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div,
+    FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View,
+};
 
-pub enum ContextMenuItem {
-    Header(SharedString),
-    Entry(Label, Box<dyn gpui::Action>),
-    Separator,
+pub struct ContextMenu {
+    items: Vec<ListItem>,
+    focus_handle: FocusHandle,
 }
 
-impl Clone for ContextMenuItem {
-    fn clone(&self) -> Self {
-        match self {
-            ContextMenuItem::Header(name) => ContextMenuItem::Header(name.clone()),
-            ContextMenuItem::Entry(label, action) => {
-                ContextMenuItem::Entry(label.clone(), action.boxed_clone())
-            }
-            ContextMenuItem::Separator => ContextMenuItem::Separator,
-        }
+impl ManagedView for ContextMenu {
+    fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle {
+        self.focus_handle.clone()
     }
 }
-impl ContextMenuItem {
-    fn to_list_item<V: 'static>(self) -> ListItem {
-        match self {
-            ContextMenuItem::Header(label) => ListSubHeader::new(label).into(),
-            ContextMenuItem::Entry(label, action) => ListEntry::new(label)
-                .variant(ListItemVariant::Inset)
-                .on_click(action)
-                .into(),
-            ContextMenuItem::Separator => ListSeparator::new().into(),
+
+impl ContextMenu {
+    pub fn new(cx: &mut WindowContext) -> Self {
+        Self {
+            items: Default::default(),
+            focus_handle: cx.focus_handle(),
         }
     }
 
-    pub fn header(label: impl Into<SharedString>) -> Self {
-        Self::Header(label.into())
+    pub fn header(mut self, title: impl Into<SharedString>) -> Self {
+        self.items.push(ListItem::Header(ListSubHeader::new(title)));
+        self
+    }
+
+    pub fn separator(mut self) -> Self {
+        self.items.push(ListItem::Separator(ListSeparator));
+        self
+    }
+
+    pub fn entry(mut self, label: Label, action: Box<dyn Action>) -> Self {
+        self.items.push(ListEntry::new(label).action(action).into());
+        self
     }
 
-    pub fn separator() -> Self {
-        Self::Separator
+    pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
+        // todo!()
+        cx.emit(Dismiss);
     }
 
-    pub fn entry(label: Label, action: impl Action) -> Self {
-        Self::Entry(label, Box::new(action))
+    pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
+        cx.emit(Dismiss);
     }
 }
 
-#[derive(Component, Clone)]
-pub struct ContextMenu {
-    items: Vec<ContextMenuItem>,
+impl Render for ContextMenu {
+    type Element = Div<Self>;
+    // todo!()
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        div().elevation_2(cx).flex().flex_row().child(
+            v_stack()
+                .min_w(px(200.))
+                .track_focus(&self.focus_handle)
+                .on_mouse_down_out(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx))
+                // .on_action(ContextMenu::select_first)
+                // .on_action(ContextMenu::select_last)
+                // .on_action(ContextMenu::select_next)
+                // .on_action(ContextMenu::select_prev)
+                .on_action(ContextMenu::confirm)
+                .on_action(ContextMenu::cancel)
+                .flex_none()
+                // .bg(cx.theme().colors().elevated_surface_background)
+                // .border()
+                // .border_color(cx.theme().colors().border)
+                .child(List::new(self.items.clone())),
+        )
+    }
 }
 
-impl ContextMenu {
-    pub fn new(items: impl IntoIterator<Item = ContextMenuItem>) -> Self {
-        Self {
-            items: items.into_iter().collect(),
+pub struct MenuHandle<V: 'static, M: ManagedView> {
+    id: Option<ElementId>,
+    child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement<V> + 'static>>,
+    menu_builder: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static>>,
+
+    anchor: Option<AnchorCorner>,
+    attach: Option<AnchorCorner>,
+}
+
+impl<V: 'static, M: ManagedView> MenuHandle<V, M> {
+    pub fn id(mut self, id: impl Into<ElementId>) -> Self {
+        self.id = Some(id.into());
+        self
+    }
+
+    pub fn menu(mut self, f: impl Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static) -> Self {
+        self.menu_builder = Some(Rc::new(f));
+        self
+    }
+
+    pub fn child<R: Component<V>>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
+        self.child_builder = Some(Box::new(|b| f(b).render()));
+        self
+    }
+
+    /// anchor defines which corner of the menu to anchor to the attachment point
+    /// (by default the cursor position, but see attach)
+    pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
+        self.anchor = Some(anchor);
+        self
+    }
+
+    /// attach defines which corner of the handle to attach the menu's anchor to
+    pub fn attach(mut self, attach: AnchorCorner) -> Self {
+        self.attach = Some(attach);
+        self
+    }
+}
+
+pub fn menu_handle<V: 'static, M: ManagedView>() -> MenuHandle<V, M> {
+    MenuHandle {
+        id: None,
+        child_builder: None,
+        menu_builder: None,
+        anchor: None,
+        attach: None,
+    }
+}
+
+pub struct MenuHandleState<V, M> {
+    menu: Rc<RefCell<Option<View<M>>>>,
+    position: Rc<RefCell<Point<Pixels>>>,
+    child_layout_id: Option<LayoutId>,
+    child_element: Option<AnyElement<V>>,
+    menu_element: Option<AnyElement<V>>,
+}
+impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
+    type ElementState = MenuHandleState<V, M>;
+
+    fn element_id(&self) -> Option<gpui::ElementId> {
+        Some(self.id.clone().expect("menu_handle must have an id()"))
+    }
+
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        cx: &mut crate::ViewContext<V>,
+    ) -> (gpui::LayoutId, Self::ElementState) {
+        let (menu, position) = if let Some(element_state) = element_state {
+            (element_state.menu, element_state.position)
+        } else {
+            (Rc::default(), Rc::default())
+        };
+
+        let mut menu_layout_id = None;
+
+        let menu_element = menu.borrow_mut().as_mut().map(|menu| {
+            let mut overlay = overlay::<V>().snap_to_window();
+            if let Some(anchor) = self.anchor {
+                overlay = overlay.anchor(anchor);
+            }
+            overlay = overlay.position(*position.borrow());
+
+            let mut view = overlay.child(menu.clone()).render();
+            menu_layout_id = Some(view.layout(view_state, cx));
+            view
+        });
+
+        let mut child_element = self
+            .child_builder
+            .take()
+            .map(|child_builder| (child_builder)(menu.borrow().is_some()));
+
+        let child_layout_id = child_element
+            .as_mut()
+            .map(|child_element| child_element.layout(view_state, cx));
+
+        let layout_id = cx.request_layout(
+            &gpui::Style::default(),
+            menu_layout_id.into_iter().chain(child_layout_id),
+        );
+
+        (
+            layout_id,
+            MenuHandleState {
+                menu,
+                position,
+                child_element,
+                child_layout_id,
+                menu_element,
+            },
+        )
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<gpui::Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut crate::ViewContext<V>,
+    ) {
+        if let Some(child) = element_state.child_element.as_mut() {
+            child.paint(view_state, cx);
         }
+
+        if let Some(menu) = element_state.menu_element.as_mut() {
+            menu.paint(view_state, cx);
+            return;
+        }
+
+        let Some(builder) = self.menu_builder.clone() else {
+            return;
+        };
+        let menu = element_state.menu.clone();
+        let position = element_state.position.clone();
+        let attach = self.attach.clone();
+        let child_layout_id = element_state.child_layout_id.clone();
+
+        cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| {
+            if phase == DispatchPhase::Bubble
+                && event.button == MouseButton::Right
+                && bounds.contains_point(&event.position)
+            {
+                cx.stop_propagation();
+                cx.prevent_default();
+
+                let new_menu = (builder)(view_state, cx);
+                let menu2 = menu.clone();
+                cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
+                    &Dismiss => {
+                        *menu2.borrow_mut() = None;
+                        cx.notify();
+                    }
+                })
+                .detach();
+                *menu.borrow_mut() = Some(new_menu);
+
+                *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
+                    attach
+                        .unwrap()
+                        .corner(cx.layout_bounds(child_layout_id.unwrap()))
+                } else {
+                    cx.mouse_position()
+                };
+                cx.notify();
+            }
+        });
     }
-    // todo!()
-    // cx.add_action(ContextMenu::select_first);
-    // cx.add_action(ContextMenu::select_last);
-    // cx.add_action(ContextMenu::select_next);
-    // cx.add_action(ContextMenu::select_prev);
-    // cx.add_action(ContextMenu::confirm);
-    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        v_stack()
-            .flex()
-            .bg(cx.theme().colors().elevated_surface_background)
-            .border()
-            .border_color(cx.theme().colors().border)
-            .child(List::new(
-                self.items
-                    .into_iter()
-                    .map(ContextMenuItem::to_list_item::<V>)
-                    .collect(),
-            ))
-            .on_mouse_down_out(|_, _, cx| cx.dispatch_action(Box::new(menu::Cancel)))
+}
+
+impl<V: 'static, M: ManagedView> Component<V> for MenuHandle<V, M> {
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
     }
 }
 
-use gpui::Action;
 #[cfg(feature = "stories")]
 pub use stories::*;
 
@@ -84,8 +260,18 @@ pub use stories::*;
 mod stories {
     use super::*;
     use crate::story::Story;
-    use gpui::{Div, Render};
-    use serde::Deserialize;
+    use gpui::{actions, Div, Render, VisualContext};
+
+    actions!(PrintCurrentDate);
+
+    fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<ContextMenu> {
+        cx.build_view(|cx| {
+            ContextMenu::new(cx).header(header).separator().entry(
+                Label::new("Print current time"),
+                PrintCurrentDate.boxed_clone(),
+            )
+        })
+    }
 
     pub struct ContextMenuStory;
 
@@ -93,22 +279,84 @@ mod stories {
         type Element = Div<Self>;
 
         fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-            #[derive(PartialEq, Clone, Deserialize, gpui::Action)]
-            struct PrintCurrentDate {}
-
             Story::container(cx)
-                .child(Story::title_for::<_, ContextMenu>(cx))
-                .child(Story::label(cx, "Default"))
-                .child(ContextMenu::new([
-                    ContextMenuItem::header("Section header"),
-                    ContextMenuItem::Separator,
-                    ContextMenuItem::entry(Label::new("Print current time"), PrintCurrentDate {}),
-                ]))
                 .on_action(|_, _: &PrintCurrentDate, _| {
                     if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
                         println!("Current Unix time is {:?}", unix_time.as_secs());
                     }
                 })
+                .flex()
+                .flex_row()
+                .justify_between()
+                .child(
+                    div()
+                        .flex()
+                        .flex_col()
+                        .justify_between()
+                        .child(
+                            menu_handle()
+                                .id("test2")
+                                .child(|is_open| {
+                                    Label::new(if is_open {
+                                        "TOP LEFT"
+                                    } else {
+                                        "RIGHT CLICK ME"
+                                    })
+                                    .render()
+                                })
+                                .menu(move |_, cx| build_menu(cx, "top left")),
+                        )
+                        .child(
+                            menu_handle()
+                                .id("test1")
+                                .child(|is_open| {
+                                    Label::new(if is_open {
+                                        "BOTTOM LEFT"
+                                    } else {
+                                        "RIGHT CLICK ME"
+                                    })
+                                    .render()
+                                })
+                                .anchor(AnchorCorner::BottomLeft)
+                                .attach(AnchorCorner::TopLeft)
+                                .menu(move |_, cx| build_menu(cx, "bottom left")),
+                        ),
+                )
+                .child(
+                    div()
+                        .flex()
+                        .flex_col()
+                        .justify_between()
+                        .child(
+                            menu_handle()
+                                .id("test3")
+                                .child(|is_open| {
+                                    Label::new(if is_open {
+                                        "TOP RIGHT"
+                                    } else {
+                                        "RIGHT CLICK ME"
+                                    })
+                                    .render()
+                                })
+                                .anchor(AnchorCorner::TopRight)
+                                .menu(move |_, cx| build_menu(cx, "top right")),
+                        )
+                        .child(
+                            menu_handle()
+                                .id("test4")
+                                .child(|is_open| {
+                                    Label::new(if is_open {
+                                        "BOTTOM RIGHT"
+                                    } else {
+                                        "RIGHT CLICK ME"
+                                    })
+                                    .render()
+                                })
+                                .anchor(AnchorCorner::BottomRight)
+                                .attach(AnchorCorner::TopRight)
+                                .menu(move |_, cx| build_menu(cx, "bottom right")),
+                        ),
+                )
         }
     }
 }

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

@@ -1,5 +1,5 @@
 use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement};
-use gpui::{prelude::*, AnyView, MouseButton};
+use gpui::{prelude::*, Action, AnyView, MouseButton};
 use std::sync::Arc;
 
 struct IconButtonHandlers<V: 'static> {
@@ -19,6 +19,7 @@ pub struct IconButton<V: 'static> {
     color: TextColor,
     variant: ButtonVariant,
     state: InteractionState,
+    selected: bool,
     tooltip: Option<Box<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>>,
     handlers: IconButtonHandlers<V>,
 }
@@ -31,6 +32,7 @@ impl<V: 'static> IconButton<V> {
             color: TextColor::default(),
             variant: ButtonVariant::default(),
             state: InteractionState::default(),
+            selected: false,
             tooltip: None,
             handlers: IconButtonHandlers::default(),
         }
@@ -56,6 +58,11 @@ impl<V: 'static> IconButton<V> {
         self
     }
 
+    pub fn selected(mut self, selected: bool) -> Self {
+        self.selected = selected;
+        self
+    }
+
     pub fn tooltip(
         mut self,
         tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
@@ -69,6 +76,10 @@ impl<V: 'static> IconButton<V> {
         self
     }
 
+    pub fn action(self, action: Box<dyn Action>) -> Self {
+        self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
+    }
+
     fn render(mut self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let icon_color = match (self.state, self.color) {
             (InteractionState::Disabled, _) => TextColor::Disabled,
@@ -76,7 +87,7 @@ impl<V: 'static> IconButton<V> {
             _ => self.color,
         };
 
-        let (bg_color, bg_hover_color, bg_active_color) = match self.variant {
+        let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant {
             ButtonVariant::Filled => (
                 cx.theme().colors().element_background,
                 cx.theme().colors().element_hover,
@@ -89,6 +100,10 @@ impl<V: 'static> IconButton<V> {
             ),
         };
 
+        if self.selected {
+            bg_color = bg_hover_color;
+        }
+
         let mut button = h_stack()
             .id(self.id.clone())
             .justify_center()
@@ -108,7 +123,9 @@ impl<V: 'static> IconButton<V> {
         }
 
         if let Some(tooltip) = self.tooltip.take() {
-            button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx))
+            if !self.selected {
+                button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx))
+            }
         }
 
         button

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

@@ -117,7 +117,7 @@ impl ListHeader {
     }
 }
 
-#[derive(Component)]
+#[derive(Component, Clone)]
 pub struct ListSubHeader {
     label: SharedString,
     left_icon: Option<Icon>,
@@ -172,7 +172,7 @@ pub enum ListEntrySize {
     Medium,
 }
 
-#[derive(Component)]
+#[derive(Component, Clone)]
 pub enum ListItem {
     Entry(ListEntry),
     Separator(ListSeparator),
@@ -234,6 +234,24 @@ pub struct ListEntry {
     on_click: Option<Box<dyn Action>>,
 }
 
+impl Clone for ListEntry {
+    fn clone(&self) -> Self {
+        Self {
+            disabled: self.disabled,
+            // TODO: Reintroduce this
+            // disclosure_control_style: DisclosureControlVisibility,
+            indent_level: self.indent_level,
+            label: self.label.clone(),
+            left_slot: self.left_slot.clone(),
+            overflow: self.overflow,
+            size: self.size,
+            toggle: self.toggle,
+            variant: self.variant,
+            on_click: self.on_click.as_ref().map(|opt| opt.boxed_clone()),
+        }
+    }
+}
+
 impl ListEntry {
     pub fn new(label: Label) -> Self {
         Self {
@@ -249,7 +267,7 @@ impl ListEntry {
         }
     }
 
-    pub fn on_click(mut self, action: impl Into<Box<dyn Action>>) -> Self {
+    pub fn action(mut self, action: impl Into<Box<dyn Action>>) -> Self {
         self.on_click = Some(action.into());
         self
     }

crates/ui2/src/story.rs 🔗

@@ -12,7 +12,6 @@ impl Story {
             .flex_col()
             .pt_2()
             .px_4()
-            .font("Zed Mono")
             .bg(cx.theme().colors().background)
     }
 

crates/ui2/src/styled_ext.rs 🔗

@@ -5,6 +5,7 @@ use crate::{ElevationIndex, UITextSize};
 
 fn elevated<E: Styled, V: 'static>(this: E, cx: &mut ViewContext<V>, index: ElevationIndex) -> E {
     this.bg(cx.theme().colors().elevated_surface_background)
+        .z_index(index.z_index())
         .rounded_lg()
         .border()
         .border_color(cx.theme().colors().border_variant)

crates/workspace2/src/dock.rs 🔗

@@ -1,14 +1,14 @@
 use crate::{status_bar::StatusItemView, Axis, Workspace};
 use gpui::{
-    div, px, Action, AnyView, AppContext, Component, Div, Entity, EntityId, EventEmitter,
-    FocusHandle, FocusableView, ParentComponent, Render, Styled, Subscription, View, ViewContext,
-    WeakView, WindowContext,
+    div, px, Action, AnchorCorner, AnyView, AppContext, Component, Div, Entity, EntityId,
+    EventEmitter, FocusHandle, FocusableView, ParentComponent, Render, SharedString, Styled,
+    Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
 use theme2::ActiveTheme;
-use ui::{h_stack, IconButton, InteractionState, Tooltip};
+use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};
 
 pub enum PanelEvent {
     ChangePosition,
@@ -417,6 +417,14 @@ impl Dock {
         }
     }
 
+    pub fn toggle_action(&self) -> Box<dyn Action> {
+        match self.position {
+            DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
+            DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
+            DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
+        }
+    }
+
     //     pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement<Workspace> {
     //         todo!()
     // if let Some(active_entry) = self.visible_entry() {
@@ -655,6 +663,7 @@ impl PanelButtons {
 //     }
 // }
 
+// here be kittens
 impl Render for PanelButtons {
     type Element = Div<Self>;
 
@@ -664,6 +673,13 @@ impl Render for PanelButtons {
         let active_index = dock.active_panel_index;
         let is_open = dock.is_open;
 
+        let (menu_anchor, menu_attach) = match dock.position {
+            DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft),
+            DockPosition::Bottom | DockPosition::Right => {
+                (AnchorCorner::BottomRight, AnchorCorner::TopRight)
+            }
+        };
+
         let buttons = dock
             .panel_entries
             .iter()
@@ -671,15 +687,33 @@ impl Render for PanelButtons {
             .filter_map(|(i, panel)| {
                 let icon = panel.panel.icon(cx)?;
                 let name = panel.panel.persistent_name();
-                let action = panel.panel.toggle_action(cx);
-                let action2 = action.boxed_clone();
-
-                let mut button = IconButton::new(panel.panel.persistent_name(), icon)
-                    .when(i == active_index, |el| el.state(InteractionState::Active))
-                    .on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
-                    .tooltip(move |_, cx| Tooltip::for_action(name, &*action2, cx));
 
-                Some(button)
+                let mut button: IconButton<Self> = if i == active_index && is_open {
+                    let action = dock.toggle_action();
+                    let tooltip: SharedString =
+                        format!("Close {} dock", dock.position.to_label()).into();
+                    IconButton::new(name, icon)
+                        .state(InteractionState::Active)
+                        .action(action.boxed_clone())
+                        .tooltip(move |_, cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
+                } else {
+                    let action = panel.panel.toggle_action(cx);
+
+                    IconButton::new(name, icon)
+                        .action(action.boxed_clone())
+                        .tooltip(move |_, cx| Tooltip::for_action(name, &*action, cx))
+                };
+
+                Some(
+                    menu_handle()
+                        .id(name)
+                        .menu(move |_, cx| {
+                            cx.build_view(|cx| ContextMenu::new(cx).header("SECTION"))
+                        })
+                        .anchor(menu_anchor)
+                        .attach(menu_attach)
+                        .child(|is_open| button.selected(is_open)),
+                )
             });
 
         h_stack().gap_0p5().children(buttons)

crates/workspace2/src/modal_layer.rs 🔗

@@ -1,6 +1,6 @@
 use gpui::{
-    div, prelude::*, px, AnyView, Div, EventEmitter, FocusHandle, Render, Subscription, View,
-    ViewContext, WindowContext,
+    div, prelude::*, px, AnyView, Div, FocusHandle, ManagedView, Render, Subscription, View,
+    ViewContext,
 };
 use ui::{h_stack, v_stack};
 
@@ -15,14 +15,6 @@ pub struct ModalLayer {
     active_modal: Option<ActiveModal>,
 }
 
-pub trait Modal: Render + EventEmitter<ModalEvent> {
-    fn focus(&self, cx: &mut WindowContext);
-}
-
-pub enum ModalEvent {
-    Dismissed,
-}
-
 impl ModalLayer {
     pub fn new() -> Self {
         Self { active_modal: None }
@@ -30,7 +22,7 @@ impl ModalLayer {
 
     pub fn toggle_modal<V, B>(&mut self, cx: &mut ViewContext<Self>, build_view: B)
     where
-        V: Modal,
+        V: ManagedView,
         B: FnOnce(&mut ViewContext<V>) -> V,
     {
         if let Some(active_modal) = &self.active_modal {
@@ -46,17 +38,15 @@ impl ModalLayer {
 
     pub fn show_modal<V>(&mut self, new_modal: View<V>, cx: &mut ViewContext<Self>)
     where
-        V: Modal,
+        V: ManagedView,
     {
         self.active_modal = Some(ActiveModal {
             modal: new_modal.clone().into(),
-            subscription: cx.subscribe(&new_modal, |this, modal, e, cx| match e {
-                ModalEvent::Dismissed => this.hide_modal(cx),
-            }),
+            subscription: cx.subscribe(&new_modal, |this, modal, e, cx| this.hide_modal(cx)),
             previous_focus_handle: cx.focused(),
             focus_handle: cx.focus_handle(),
         });
-        new_modal.update(cx, |modal, cx| modal.focus(cx));
+        cx.focus_view(&new_modal);
         cx.notify();
     }
 

crates/workspace2/src/workspace2.rs 🔗

@@ -31,9 +31,9 @@ use futures::{
 use gpui::{
     actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext,
     AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle,
-    FocusableView, GlobalPixels, InteractiveComponent, KeyContext, Model, ModelContext,
-    ParentComponent, Point, Render, Size, Styled, Subscription, Task, View, ViewContext,
-    VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
+    FocusableView, GlobalPixels, InteractiveComponent, KeyContext, ManagedView, Model,
+    ModelContext, ParentComponent, Point, Render, Size, Styled, Subscription, Task, View,
+    ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
@@ -3212,8 +3212,8 @@ impl Workspace {
         })
     }
 
-    fn actions(div: Div<Self>) -> Div<Self> {
-        div
+    fn actions(&self, div: Div<Self>) -> Div<Self> {
+        self.add_workspace_actions_listeners(div)
             //     cx.add_async_action(Workspace::open);
             //     cx.add_async_action(Workspace::follow_next_collaborator);
             //     cx.add_async_action(Workspace::close);
@@ -3262,15 +3262,15 @@ impl Workspace {
             .on_action(|this, e: &ToggleLeftDock, cx| {
                 this.toggle_dock(DockPosition::Left, cx);
             })
-        //     cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
-        //         workspace.toggle_dock(DockPosition::Right, cx);
-        //     });
-        //     cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
-        //         workspace.toggle_dock(DockPosition::Bottom, cx);
-        //     });
-        //     cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
-        //         workspace.close_all_docks(cx);
-        //     });
+            .on_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
+                workspace.toggle_dock(DockPosition::Right, cx);
+            })
+            .on_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
+                workspace.toggle_dock(DockPosition::Bottom, cx);
+            })
+            .on_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
+                workspace.close_all_docks(cx);
+            })
         //     cx.add_action(Workspace::activate_pane_at_index);
         //     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
         //         workspace.reopen_closed_item(cx).detach();
@@ -3380,11 +3380,14 @@ impl Workspace {
         div
     }
 
-    pub fn active_modal<V: Modal + 'static>(&mut self, cx: &ViewContext<Self>) -> Option<View<V>> {
+    pub fn active_modal<V: ManagedView + 'static>(
+        &mut self,
+        cx: &ViewContext<Self>,
+    ) -> Option<View<V>> {
         self.modal_layer.read(cx).active_modal()
     }
 
-    pub fn toggle_modal<V: Modal, B>(&mut self, cx: &mut ViewContext<Self>, build: B)
+    pub fn toggle_modal<V: ManagedView, B>(&mut self, cx: &mut ViewContext<Self>, build: B)
     where
         B: FnOnce(&mut ViewContext<V>) -> V,
     {
@@ -3624,7 +3627,7 @@ impl Render for Workspace {
 
         cx.set_rem_size(ui_font_size);
 
-        self.add_workspace_actions_listeners(div())
+        self.actions(div())
             .key_context(context)
             .relative()
             .size_full()