Collab ui2 (#3357)

Conrad Irwin created

* Clickable context menus & movable panels – what will they think of
next?!

Release Notes:

- N/A

Change summary

crates/gpui2/src/app/entity_map.rs         |  11 
crates/gpui2/src/elements/div.rs           |   5 
crates/gpui2/src/platform/mac/window.rs    |   5 
crates/gpui2/src/view.rs                   |   4 
crates/project_panel2/src/project_panel.rs |  28 +-
crates/settings2/src/settings_file.rs      |   1 
crates/terminal_view2/src/terminal_view.rs |  13 
crates/ui2/src/components/context_menu.rs  | 116 +++++++++++---
crates/ui2/src/components/list.rs          |  74 +++++----
crates/ui2/src/static_data.rs              |   8 
crates/workspace2/src/dock.rs              | 183 ++++++++++++++++-------
crates/workspace2/src/workspace2.rs        |   4 
12 files changed, 302 insertions(+), 150 deletions(-)

Detailed changes

crates/gpui2/src/app/entity_map.rs 🔗

@@ -71,11 +71,12 @@ impl EntityMap {
     #[track_caller]
     pub fn lease<'a, T>(&mut self, model: &'a Model<T>) -> Lease<'a, T> {
         self.assert_valid_context(model);
-        let entity = Some(
-            self.entities
-                .remove(model.entity_id)
-                .expect("Circular entity lease. Is the entity already being updated?"),
-        );
+        let entity = Some(self.entities.remove(model.entity_id).unwrap_or_else(|| {
+            panic!(
+                "Circular entity lease of {}. Is it already being updated?",
+                std::any::type_name::<T>()
+            )
+        }));
         Lease {
             model,
             entity,

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

@@ -1124,9 +1124,14 @@ where
                     }
                 }
             }
+            // if self.hover_style.is_some() {
             if bounds.contains_point(&mouse_position) {
+                // eprintln!("div hovered {bounds:?} {mouse_position:?}");
                 style.refine(&self.hover_style);
+            } else {
+                // eprintln!("div NOT hovered {bounds:?} {mouse_position:?}");
             }
+            // }
 
             if let Some(drag) = cx.active_drag.take() {
                 for (state_type, group_drag_style) in &self.group_drag_over_styles {

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

@@ -1205,10 +1205,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
 
             InputEvent::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return,
 
-            InputEvent::MouseUp(MouseUpEvent {
-                button: MouseButton::Left,
-                ..
-            }) => {
+            InputEvent::MouseUp(MouseUpEvent { .. }) => {
                 lock.synthetic_drag_counter += 1;
             }
 

crates/gpui2/src/view.rs 🔗

@@ -191,6 +191,10 @@ impl AnyView {
         self.model.entity_type
     }
 
+    pub fn entity_id(&self) -> EntityId {
+        self.model.entity_id()
+    }
+
     pub(crate) fn draw(
         &self,
         origin: Point<Pixels>,

crates/project_panel2/src/project_panel.rs 🔗

@@ -1,6 +1,6 @@
 pub mod file_associations;
 mod project_panel_settings;
-use settings::Settings;
+use settings::{Settings, SettingsStore};
 
 use db::kvp::KEY_VALUE_STORE;
 use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor};
@@ -34,7 +34,7 @@ use ui::{h_stack, v_stack, IconElement, Label};
 use unicase::UniCase;
 use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
-    dock::{DockPosition, PanelEvent},
+    dock::{DockPosition, Panel, PanelEvent},
     Workspace,
 };
 
@@ -148,7 +148,6 @@ pub enum Event {
     SplitEntry {
         entry_id: ProjectEntryId,
     },
-    DockPositionChanged,
     Focus,
     NewSearchInDirectory {
         dir_entry: Entry,
@@ -244,16 +243,17 @@ impl ProjectPanel {
             this.update_visible_entries(None, cx);
 
             // Update the dock position when the setting changes.
-            // todo!()
-            // let mut old_dock_position = this.position(cx);
-            // cx.observe_global::<SettingsStore, _>(move |this, cx| {
-            //     let new_dock_position = this.position(cx);
-            //     if new_dock_position != old_dock_position {
-            //         old_dock_position = new_dock_position;
-            //         cx.emit(Event::DockPositionChanged);
-            //     }
-            // })
-            // .detach();
+            let mut old_dock_position = this.position(cx);
+            ProjectPanelSettings::register(cx);
+            cx.observe_global::<SettingsStore>(move |this, cx| {
+                dbg!("OLA!");
+                let new_dock_position = this.position(cx);
+                if new_dock_position != old_dock_position {
+                    old_dock_position = new_dock_position;
+                    cx.emit(PanelEvent::ChangePosition);
+                }
+            })
+            .detach();
 
             this
         });
@@ -1485,7 +1485,7 @@ impl EventEmitter<Event> for ProjectPanel {}
 
 impl EventEmitter<PanelEvent> for ProjectPanel {}
 
-impl workspace::dock::Panel for ProjectPanel {
+impl Panel for ProjectPanel {
     fn position(&self, cx: &WindowContext) -> DockPosition {
         match ProjectPanelSettings::get_global(cx).dock {
             ProjectPanelDockPosition::Left => DockPosition::Left,

crates/settings2/src/settings_file.rs 🔗

@@ -77,6 +77,7 @@ pub fn handle_settings_file_changes(
     });
     cx.spawn(move |mut cx| async move {
         while let Some(user_settings_content) = user_settings_file_rx.next().await {
+            eprintln!("settings file changed");
             let result = cx.update_global(|store: &mut SettingsStore, cx| {
                 store
                     .set_user_settings(&user_settings_content, cx)

crates/terminal_view2/src/terminal_view.rs 🔗

@@ -32,7 +32,7 @@ use workspace::{
     notifications::NotifyResultExt,
     register_deserializable_item,
     searchable::{SearchEvent, SearchOptions, SearchableItem},
-    ui::{ContextMenu, Label},
+    ui::{ContextMenu, Label, ListEntry},
     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<View<ContextMenu>>,
+    context_menu: Option<View<ContextMenu<Self>>>,
     blink_state: bool,
     blinking_on: bool,
     blinking_paused: bool,
@@ -300,11 +300,10 @@ impl TerminalView {
         position: gpui::Point<Pixels>,
         cx: &mut ViewContext<Self>,
     ) {
-        self.context_menu = Some(cx.build_view(|cx| {
-            ContextMenu::new(cx)
-                .entry(Label::new("Clear"), Box::new(Clear))
-                .entry(
-                    Label::new("Close"),
+        self.context_menu = Some(ContextMenu::build(cx, |menu, _| {
+            menu.action(ListEntry::new(Label::new("Clear")), Box::new(Clear))
+                .action(
+                    ListEntry::new(Label::new("Close")),
                     Box::new(CloseActiveItem { save_intent: None }),
                 )
         }));

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

@@ -6,42 +6,74 @@ use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHea
 use gpui::{
     overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div,
     FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View,
+    VisualContext, WeakView,
 };
 
-pub struct ContextMenu {
-    items: Vec<ListItem>,
+pub enum ContextMenuItem<V> {
+    Separator(ListSeparator),
+    Header(ListSubHeader),
+    Entry(
+        ListEntry<ContextMenu<V>>,
+        Rc<dyn Fn(&mut V, &mut ViewContext<V>)>,
+    ),
+}
+
+pub struct ContextMenu<V> {
+    items: Vec<ContextMenuItem<V>>,
     focus_handle: FocusHandle,
+    handle: WeakView<V>,
 }
 
-impl ManagedView for ContextMenu {
+impl<V: Render> ManagedView for ContextMenu<V> {
     fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle {
         self.focus_handle.clone()
     }
 }
 
-impl ContextMenu {
-    pub fn new(cx: &mut WindowContext) -> Self {
-        Self {
-            items: Default::default(),
-            focus_handle: cx.focus_handle(),
-        }
+impl<V: Render> ContextMenu<V> {
+    pub fn build(
+        cx: &mut ViewContext<V>,
+        f: impl FnOnce(Self, &mut ViewContext<Self>) -> Self,
+    ) -> View<Self> {
+        let handle = cx.view().downgrade();
+        cx.build_view(|cx| {
+            f(
+                Self {
+                    handle,
+                    items: Default::default(),
+                    focus_handle: cx.focus_handle(),
+                },
+                cx,
+            )
+        })
     }
 
     pub fn header(mut self, title: impl Into<SharedString>) -> Self {
-        self.items.push(ListItem::Header(ListSubHeader::new(title)));
+        self.items
+            .push(ContextMenuItem::Header(ListSubHeader::new(title)));
         self
     }
 
     pub fn separator(mut self) -> Self {
-        self.items.push(ListItem::Separator(ListSeparator));
+        self.items.push(ContextMenuItem::Separator(ListSeparator));
         self
     }
 
-    pub fn entry(mut self, label: Label, action: Box<dyn Action>) -> Self {
-        self.items.push(ListEntry::new(label).action(action).into());
+    pub fn entry(
+        mut self,
+        view: ListEntry<Self>,
+        on_click: impl Fn(&mut V, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.items
+            .push(ContextMenuItem::Entry(view, Rc::new(on_click)));
         self
     }
 
+    pub fn action(self, view: ListEntry<Self>, action: Box<dyn Action>) -> Self {
+        // todo: add the keybindings to the list entry
+        self.entry(view, move |_, cx| cx.dispatch_action(action.boxed_clone()))
+    }
+
     pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
         // todo!()
         cx.emit(Dismiss);
@@ -52,9 +84,9 @@ impl ContextMenu {
     }
 }
 
-impl Render for ContextMenu {
+impl<V: Render> Render for ContextMenu<V> {
     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()
@@ -71,7 +103,25 @@ impl Render for ContextMenu {
                 // .bg(cx.theme().colors().elevated_surface_background)
                 // .border()
                 // .border_color(cx.theme().colors().border)
-                .child(List::new(self.items.clone())),
+                .child(List::new(
+                    self.items
+                        .iter()
+                        .map(|item| match item {
+                            ContextMenuItem::Separator(separator) => {
+                                ListItem::Separator(separator.clone())
+                            }
+                            ContextMenuItem::Header(header) => ListItem::Header(header.clone()),
+                            ContextMenuItem::Entry(entry, callback) => {
+                                let callback = callback.clone();
+                                let handle = self.handle.clone();
+                                ListItem::Entry(entry.clone().on_click(move |this, cx| {
+                                    handle.update(cx, |view, cx| callback(view, cx)).ok();
+                                    cx.emit(Dismiss);
+                                }))
+                            }
+                        })
+                        .collect(),
+                )),
         )
     }
 }
@@ -232,6 +282,7 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
                     }
                 })
                 .detach();
+                cx.focus_view(&new_menu);
                 *menu.borrow_mut() = Some(new_menu);
 
                 *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
@@ -260,16 +311,25 @@ pub use stories::*;
 mod stories {
     use super::*;
     use crate::story::Story;
-    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(),
-            )
+    use gpui::{actions, Div, Render};
+
+    actions!(PrintCurrentDate, PrintBestFood);
+
+    fn build_menu<V: Render>(
+        cx: &mut ViewContext<V>,
+        header: impl Into<SharedString>,
+    ) -> View<ContextMenu<V>> {
+        let handle = cx.view().clone();
+        ContextMenu::build(cx, |menu, _| {
+            menu.header(header)
+                .separator()
+                .entry(ListEntry::new(Label::new("Print current time")), |v, cx| {
+                    println!("dispatching PrintCurrentTime action");
+                    cx.dispatch_action(PrintCurrentDate.boxed_clone())
+                })
+                .entry(ListEntry::new(Label::new("Print best food")), |v, cx| {
+                    cx.dispatch_action(PrintBestFood.boxed_clone())
+                })
         })
     }
 
@@ -281,10 +341,14 @@ mod stories {
         fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
                 .on_action(|_, _: &PrintCurrentDate, _| {
+                    println!("printing unix time!");
                     if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
                         println!("Current Unix time is {:?}", unix_time.as_secs());
                     }
                 })
+                .on_action(|_, _: &PrintBestFood, _| {
+                    println!("burrito");
+                })
                 .flex()
                 .flex_row()
                 .justify_between()

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

@@ -1,4 +1,6 @@
-use gpui::{div, Action};
+use std::rc::Rc;
+
+use gpui::{div, Div, Stateful, StatefulInteractiveComponent};
 
 use crate::settings::user_settings;
 use crate::{
@@ -172,35 +174,35 @@ pub enum ListEntrySize {
     Medium,
 }
 
-#[derive(Component, Clone)]
-pub enum ListItem {
-    Entry(ListEntry),
+#[derive(Clone)]
+pub enum ListItem<V: 'static> {
+    Entry(ListEntry<V>),
     Separator(ListSeparator),
     Header(ListSubHeader),
 }
 
-impl From<ListEntry> for ListItem {
-    fn from(entry: ListEntry) -> Self {
+impl<V: 'static> From<ListEntry<V>> for ListItem<V> {
+    fn from(entry: ListEntry<V>) -> Self {
         Self::Entry(entry)
     }
 }
 
-impl From<ListSeparator> for ListItem {
+impl<V: 'static> From<ListSeparator> for ListItem<V> {
     fn from(entry: ListSeparator) -> Self {
         Self::Separator(entry)
     }
 }
 
-impl From<ListSubHeader> for ListItem {
+impl<V: 'static> From<ListSubHeader> for ListItem<V> {
     fn from(entry: ListSubHeader) -> Self {
         Self::Header(entry)
     }
 }
 
-impl ListItem {
-    fn render<V: 'static>(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+impl<V: 'static> ListItem<V> {
+    fn render(self, view: &mut V, ix: usize, cx: &mut ViewContext<V>) -> impl Component<V> {
         match self {
-            ListItem::Entry(entry) => div().child(entry.render(view, cx)),
+            ListItem::Entry(entry) => div().child(entry.render(ix, cx)),
             ListItem::Separator(separator) => div().child(separator.render(view, cx)),
             ListItem::Header(header) => div().child(header.render(view, cx)),
         }
@@ -210,7 +212,7 @@ impl ListItem {
         Self::Entry(ListEntry::new(label))
     }
 
-    pub fn as_entry(&mut self) -> Option<&mut ListEntry> {
+    pub fn as_entry(&mut self) -> Option<&mut ListEntry<V>> {
         if let Self::Entry(entry) = self {
             Some(entry)
         } else {
@@ -219,8 +221,7 @@ impl ListItem {
     }
 }
 
-#[derive(Component)]
-pub struct ListEntry {
+pub struct ListEntry<V> {
     disabled: bool,
     // TODO: Reintroduce this
     // disclosure_control_style: DisclosureControlVisibility,
@@ -231,15 +232,13 @@ pub struct ListEntry {
     size: ListEntrySize,
     toggle: Toggle,
     variant: ListItemVariant,
-    on_click: Option<Box<dyn Action>>,
+    on_click: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) + 'static>>,
 }
 
-impl Clone for ListEntry {
+impl<V> Clone for ListEntry<V> {
     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(),
@@ -247,12 +246,12 @@ impl Clone for ListEntry {
             size: self.size,
             toggle: self.toggle,
             variant: self.variant,
-            on_click: self.on_click.as_ref().map(|opt| opt.boxed_clone()),
+            on_click: self.on_click.clone(),
         }
     }
 }
 
-impl ListEntry {
+impl<V: 'static> ListEntry<V> {
     pub fn new(label: Label) -> Self {
         Self {
             disabled: false,
@@ -267,8 +266,8 @@ impl ListEntry {
         }
     }
 
-    pub fn action(mut self, action: impl Into<Box<dyn Action>>) -> Self {
-        self.on_click = Some(action.into());
+    pub fn on_click(mut self, handler: impl Fn(&mut V, &mut ViewContext<V>) + 'static) -> Self {
+        self.on_click = Some(Rc::new(handler));
         self
     }
 
@@ -307,7 +306,7 @@ impl ListEntry {
         self
     }
 
-    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+    fn render(self, ix: usize, cx: &mut ViewContext<V>) -> Stateful<V, Div<V>> {
         let settings = user_settings(cx);
 
         let left_content = match self.left_slot.clone() {
@@ -328,21 +327,21 @@ impl ListEntry {
             ListEntrySize::Medium => div().h_7(),
         };
         div()
+            .id(ix)
             .relative()
             .hover(|mut style| {
                 style.background = Some(cx.theme().colors().editor_background.into());
                 style
             })
-            .on_mouse_down(gpui::MouseButton::Left, {
-                let action = self.on_click.map(|action| action.boxed_clone());
+            .on_click({
+                let on_click = self.on_click.clone();
 
-                move |entry: &mut V, event, cx| {
-                    if let Some(action) = action.as_ref() {
-                        cx.dispatch_action(action.boxed_clone());
+                move |view: &mut V, event, cx| {
+                    if let Some(on_click) = &on_click {
+                        (on_click)(view, cx)
                     }
                 }
             })
-            .group("")
             .bg(cx.theme().colors().surface_background)
             // TODO: Add focus state
             // .when(self.state == InteractionState::Focused, |this| {
@@ -391,8 +390,8 @@ impl ListSeparator {
 }
 
 #[derive(Component)]
-pub struct List {
-    items: Vec<ListItem>,
+pub struct List<V: 'static> {
+    items: Vec<ListItem<V>>,
     /// Message to display when the list is empty
     /// Defaults to "No items"
     empty_message: SharedString,
@@ -400,8 +399,8 @@ pub struct List {
     toggle: Toggle,
 }
 
-impl List {
-    pub fn new(items: Vec<ListItem>) -> Self {
+impl<V: 'static> List<V> {
+    pub fn new(items: Vec<ListItem<V>>) -> Self {
         Self {
             items,
             empty_message: "No items".into(),
@@ -425,9 +424,14 @@ impl List {
         self
     }
 
-    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+    fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let list_content = match (self.items.is_empty(), self.toggle) {
-            (false, _) => div().children(self.items),
+            (false, _) => div().children(
+                self.items
+                    .into_iter()
+                    .enumerate()
+                    .map(|(ix, item)| item.render(view, ix, cx)),
+            ),
             (true, Toggle::Toggled(false)) => div(),
             (true, _) => {
                 div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted))

crates/ui2/src/static_data.rs 🔗

@@ -478,7 +478,7 @@ pub fn static_new_notification_items_2<V: 'static>() -> Vec<Notification<V>> {
     ]
 }
 
-pub fn static_project_panel_project_items() -> Vec<ListItem> {
+pub fn static_project_panel_project_items<V>() -> Vec<ListItem<V>> {
     vec![
         ListEntry::new(Label::new("zed"))
             .left_icon(Icon::FolderOpen.into())
@@ -605,7 +605,7 @@ pub fn static_project_panel_project_items() -> Vec<ListItem> {
     .collect()
 }
 
-pub fn static_project_panel_single_items() -> Vec<ListItem> {
+pub fn static_project_panel_single_items<V>() -> Vec<ListItem<V>> {
     vec![
         ListEntry::new(Label::new("todo.md"))
             .left_icon(Icon::FileDoc.into())
@@ -622,7 +622,7 @@ pub fn static_project_panel_single_items() -> Vec<ListItem> {
     .collect()
 }
 
-pub fn static_collab_panel_current_call() -> Vec<ListItem> {
+pub fn static_collab_panel_current_call<V>() -> Vec<ListItem<V>> {
     vec![
         ListEntry::new(Label::new("as-cii")).left_avatar("http://github.com/as-cii.png?s=50"),
         ListEntry::new(Label::new("nathansobo"))
@@ -635,7 +635,7 @@ pub fn static_collab_panel_current_call() -> Vec<ListItem> {
     .collect()
 }
 
-pub fn static_collab_panel_channels() -> Vec<ListItem> {
+pub fn static_collab_panel_channels<V>() -> Vec<ListItem<V>> {
     vec![
         ListEntry::new(Label::new("zed"))
             .left_icon(Icon::Hash.into())

crates/workspace2/src/dock.rs 🔗

@@ -8,7 +8,9 @@ use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
 use theme2::ActiveTheme;
-use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};
+use ui::{
+    h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Label, ListEntry, Tooltip,
+};
 
 pub enum PanelEvent {
     ChangePosition,
@@ -40,7 +42,7 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
 }
 
 pub trait PanelHandle: Send + Sync {
-    fn id(&self) -> EntityId;
+    fn entity_id(&self) -> EntityId;
     fn persistent_name(&self) -> &'static str;
     fn position(&self, cx: &WindowContext) -> DockPosition;
     fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
@@ -62,8 +64,8 @@ impl<T> PanelHandle for View<T>
 where
     T: Panel,
 {
-    fn id(&self) -> EntityId {
-        self.entity_id()
+    fn entity_id(&self) -> EntityId {
+        Entity::entity_id(self)
     }
 
     fn persistent_name(&self) -> &'static str {
@@ -254,20 +256,19 @@ impl Dock {
         }
     }
 
-    // todo!()
-    // pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext<Self>) {
-    //     for entry in &mut self.panel_entries {
-    //         if entry.panel.as_any() == panel {
-    //             if zoomed != entry.panel.is_zoomed(cx) {
-    //                 entry.panel.set_zoomed(zoomed, cx);
-    //             }
-    //         } else if entry.panel.is_zoomed(cx) {
-    //             entry.panel.set_zoomed(false, cx);
-    //         }
-    //     }
+    pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext<Self>) {
+        for entry in &mut self.panel_entries {
+            if entry.panel.entity_id() == panel.entity_id() {
+                if zoomed != entry.panel.is_zoomed(cx) {
+                    entry.panel.set_zoomed(zoomed, cx);
+                }
+            } else if entry.panel.is_zoomed(cx) {
+                entry.panel.set_zoomed(false, cx);
+            }
+        }
 
-    //     cx.notify();
-    // }
+        cx.notify();
+    }
 
     pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
         for entry in &mut self.panel_entries {
@@ -277,42 +278,91 @@ impl Dock {
         }
     }
 
-    pub(crate) fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>) {
+    pub(crate) fn add_panel<T: Panel>(
+        &mut self,
+        panel: View<T>,
+        workspace: WeakView<Workspace>,
+        cx: &mut ViewContext<Self>,
+    ) {
         let subscriptions = [
             cx.observe(&panel, |_, _, cx| cx.notify()),
-            cx.subscribe(&panel, |this, panel, event, cx| {
-                match event {
-                    PanelEvent::ChangePosition => {
-                        //todo!()
-                        // see: Workspace::add_panel_with_extra_event_handler
-                    }
-                    PanelEvent::ZoomIn => {
-                        //todo!()
-                        // see: Workspace::add_panel_with_extra_event_handler
-                    }
-                    PanelEvent::ZoomOut => {
-                        // todo!()
-                        // // see: Workspace::add_panel_with_extra_event_handler
-                    }
-                    PanelEvent::Activate => {
-                        if let Some(ix) = this
-                            .panel_entries
-                            .iter()
-                            .position(|entry| entry.panel.id() == panel.id())
-                        {
-                            this.set_open(true, cx);
-                            this.activate_panel(ix, cx);
-                            //` todo!()
-                            // cx.focus(&panel);
+            cx.subscribe(&panel, move |this, panel, event, cx| match event {
+                PanelEvent::ChangePosition => {
+                    let new_position = panel.read(cx).position(cx);
+
+                    let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
+                        if panel.is_zoomed(cx) {
+                            workspace.zoomed_position = Some(new_position);
                         }
-                    }
-                    PanelEvent::Close => {
-                        if this.visible_panel().map_or(false, |p| p.id() == panel.id()) {
-                            this.set_open(false, cx);
+                        match new_position {
+                            DockPosition::Left => &workspace.left_dock,
+                            DockPosition::Bottom => &workspace.bottom_dock,
+                            DockPosition::Right => &workspace.right_dock,
+                        }
+                        .clone()
+                    }) else {
+                        return;
+                    };
+
+                    let was_visible = this.is_open()
+                        && this.visible_panel().map_or(false, |active_panel| {
+                            active_panel.entity_id() == Entity::entity_id(&panel)
+                        });
+
+                    this.remove_panel(&panel, cx);
+
+                    new_dock.update(cx, |new_dock, cx| {
+                        new_dock.add_panel(panel.clone(), workspace.clone(), cx);
+                        if was_visible {
+                            new_dock.set_open(true, cx);
+                            new_dock.activate_panel(this.panels_len() - 1, cx);
                         }
+                    });
+                }
+                PanelEvent::ZoomIn => {
+                    this.set_panel_zoomed(&panel.to_any(), true, cx);
+                    if !panel.has_focus(cx) {
+                        cx.focus_view(&panel);
+                    }
+                    workspace
+                        .update(cx, |workspace, cx| {
+                            workspace.zoomed = Some(panel.downgrade().into());
+                            workspace.zoomed_position = Some(panel.read(cx).position(cx));
+                        })
+                        .ok();
+                }
+                PanelEvent::ZoomOut => {
+                    this.set_panel_zoomed(&panel.to_any(), false, cx);
+                    workspace
+                        .update(cx, |workspace, cx| {
+                            if workspace.zoomed_position == Some(this.position) {
+                                workspace.zoomed = None;
+                                workspace.zoomed_position = None;
+                            }
+                            cx.notify();
+                        })
+                        .ok();
+                }
+                PanelEvent::Activate => {
+                    if let Some(ix) = this
+                        .panel_entries
+                        .iter()
+                        .position(|entry| entry.panel.entity_id() == Entity::entity_id(&panel))
+                    {
+                        this.set_open(true, cx);
+                        this.activate_panel(ix, cx);
+                        cx.focus_view(&panel);
+                    }
+                }
+                PanelEvent::Close => {
+                    if this
+                        .visible_panel()
+                        .map_or(false, |p| p.entity_id() == Entity::entity_id(&panel))
+                    {
+                        this.set_open(false, cx);
                     }
-                    PanelEvent::Focus => todo!(),
                 }
+                PanelEvent::Focus => todo!(),
             }),
         ];
 
@@ -335,7 +385,7 @@ impl Dock {
         if let Some(panel_ix) = self
             .panel_entries
             .iter()
-            .position(|entry| entry.panel.id() == panel.id())
+            .position(|entry| entry.panel.entity_id() == Entity::entity_id(panel))
         {
             if panel_ix == self.active_panel_index {
                 self.active_panel_index = 0;
@@ -396,7 +446,7 @@ impl Dock {
     pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
         self.panel_entries
             .iter()
-            .find(|entry| entry.panel.id() == panel.id())
+            .find(|entry| entry.panel.entity_id() == panel.entity_id())
             .map(|entry| entry.panel.size(cx))
     }
 
@@ -620,6 +670,7 @@ impl Render for PanelButtons {
         let dock = self.dock.read(cx);
         let active_index = dock.active_panel_index;
         let is_open = dock.is_open;
+        let dock_position = dock.position;
 
         let (menu_anchor, menu_attach) = match dock.position {
             DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft),
@@ -632,9 +683,10 @@ impl Render for PanelButtons {
             .panel_entries
             .iter()
             .enumerate()
-            .filter_map(|(i, panel)| {
-                let icon = panel.panel.icon(cx)?;
-                let name = panel.panel.persistent_name();
+            .filter_map(|(i, entry)| {
+                let icon = entry.panel.icon(cx)?;
+                let name = entry.panel.persistent_name();
+                let panel = entry.panel.clone();
 
                 let mut button: IconButton<Self> = if i == active_index && is_open {
                     let action = dock.toggle_action();
@@ -645,7 +697,7 @@ impl Render for PanelButtons {
                         .action(action.boxed_clone())
                         .tooltip(move |_, cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
                 } else {
-                    let action = panel.panel.toggle_action(cx);
+                    let action = entry.panel.toggle_action(cx);
 
                     IconButton::new(name, icon)
                         .action(action.boxed_clone())
@@ -656,7 +708,30 @@ impl Render for PanelButtons {
                     menu_handle()
                         .id(name)
                         .menu(move |_, cx| {
-                            cx.build_view(|cx| ContextMenu::new(cx).header("SECTION"))
+                            const POSITIONS: [DockPosition; 3] = [
+                                DockPosition::Left,
+                                DockPosition::Right,
+                                DockPosition::Bottom,
+                            ];
+                            ContextMenu::build(cx, |mut menu, cx| {
+                                for position in POSITIONS {
+                                    if position != dock_position
+                                        && panel.position_is_valid(position, cx)
+                                    {
+                                        let panel = panel.clone();
+                                        menu = menu.entry(
+                                            ListEntry::new(Label::new(format!(
+                                                "Dock {}",
+                                                position.to_label()
+                                            ))),
+                                            move |_, cx| {
+                                                panel.set_position(position, cx);
+                                            },
+                                        )
+                                    }
+                                }
+                                menu
+                            })
                         })
                         .anchor(menu_anchor)
                         .attach(menu_attach)

crates/workspace2/src/workspace2.rs 🔗

@@ -813,7 +813,9 @@ impl Workspace {
             DockPosition::Right => &self.right_dock,
         };
 
-        dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
+        dock.update(cx, |dock, cx| {
+            dock.add_panel(panel, self.weak_self.clone(), cx)
+        });
     }
 
     pub fn status_bar(&self) -> &View<StatusBar> {