Initial explorations into docks

Mikayla Maki created

Change summary

crates/terminal/src/modal.rs      | 100 ++++++++++++++++----------------
crates/workspace/src/dock.rs      |  35 +++++++++++
crates/workspace/src/pane.rs      |   4 +
crates/workspace/src/programs.rs  |  75 ------------------------
crates/workspace/src/workspace.rs |  44 +++++++++++---
5 files changed, 123 insertions(+), 135 deletions(-)

Detailed changes

crates/terminal/src/modal.rs 🔗

@@ -1,6 +1,6 @@
 use gpui::{ModelHandle, ViewContext};
 use settings::{Settings, WorkingDirectory};
-use workspace::{programs::Dock, Workspace};
+use workspace::{dock::Dock, Workspace};
 
 use crate::{
     terminal_container_view::{
@@ -10,55 +10,55 @@ use crate::{
 };
 
 pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext<Workspace>) {
-    let window = cx.window_id();
+    // let window = cx.window_id();
 
-    // Pull the terminal connection out of the global if it has been stored
-    let possible_terminal = Dock::remove::<Terminal, _>(window, cx);
+    // // Pull the terminal connection out of the global if it has been stored
+    // let possible_terminal = Dock::remove::<Terminal, _>(window, cx);
 
-    if let Some(terminal_handle) = possible_terminal {
-        workspace.toggle_modal(cx, |_, cx| {
-            // Create a view from the stored connection if the terminal modal is not already shown
-            cx.add_view(|cx| TerminalContainer::from_terminal(terminal_handle.clone(), true, cx))
-        });
-        // Toggle Modal will dismiss the terminal modal if it is currently shown, so we must
-        // store the terminal back in the global
-        Dock::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
-    } else {
-        // No connection was stored, create a new terminal
-        if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| {
-            // No terminal modal visible, construct a new one.
-            let wd_strategy = cx
-                .global::<Settings>()
-                .terminal_overrides
-                .working_directory
-                .clone()
-                .unwrap_or(WorkingDirectory::CurrentProjectDirectory);
+    // if let Some(terminal_handle) = possible_terminal {
+    //     workspace.toggle_modal(cx, |_, cx| {
+    //         // Create a view from the stored connection if the terminal modal is not already shown
+    //         cx.add_view(|cx| TerminalContainer::from_terminal(terminal_handle.clone(), true, cx))
+    //     });
+    //     // Toggle Modal will dismiss the terminal modal if it is currently shown, so we must
+    //     // store the terminal back in the global
+    //     Dock::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
+    // } else {
+    //     // No connection was stored, create a new terminal
+    //     if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| {
+    //         // No terminal modal visible, construct a new one.
+    //         let wd_strategy = cx
+    //             .global::<Settings>()
+    //             .terminal_overrides
+    //             .working_directory
+    //             .clone()
+    //             .unwrap_or(WorkingDirectory::CurrentProjectDirectory);
 
-            let working_directory = get_working_directory(workspace, cx, wd_strategy);
+    //         let working_directory = get_working_directory(workspace, cx, wd_strategy);
 
-            let this = cx.add_view(|cx| TerminalContainer::new(working_directory, true, cx));
+    //         let this = cx.add_view(|cx| TerminalContainer::new(working_directory, true, cx));
 
-            if let TerminalContainerContent::Connected(connected) = &this.read(cx).content {
-                let terminal_handle = connected.read(cx).handle();
-                cx.subscribe(&terminal_handle, on_event).detach();
-                // Set the global immediately if terminal construction was successful,
-                // in case the user opens the command palette
-                Dock::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
-            }
+    //         if let TerminalContainerContent::Connected(connected) = &this.read(cx).content {
+    //             let terminal_handle = connected.read(cx).handle();
+    //             cx.subscribe(&terminal_handle, on_event).detach();
+    //             // Set the global immediately if terminal construction was successful,
+    //             // in case the user opens the command palette
+    //             Dock::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
+    //         }
 
-            this
-        }) {
-            // Terminal modal was dismissed and the terminal view is connected, store the terminal
-            if let TerminalContainerContent::Connected(connected) =
-                &closed_terminal_handle.read(cx).content
-            {
-                let terminal_handle = connected.read(cx).handle();
-                // Set the global immediately if terminal construction was successful,
-                // in case the user opens the command palette
-                Dock::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
-            }
-        }
-    }
+    //         this
+    //     }) {
+    //         // Terminal modal was dismissed and the terminal view is connected, store the terminal
+    //         if let TerminalContainerContent::Connected(connected) =
+    //             &closed_terminal_handle.read(cx).content
+    //         {
+    //             let terminal_handle = connected.read(cx).handle();
+    //             // Set the global immediately if terminal construction was successful,
+    //             // in case the user opens the command palette
+    //             Dock::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
+    //         }
+    //     }
+    // }
 }
 
 pub fn on_event(
@@ -68,11 +68,11 @@ pub fn on_event(
     cx: &mut ViewContext<Workspace>,
 ) {
     // Dismiss the modal if the terminal quit
-    if let Event::CloseTerminal = event {
-        Dock::remove::<Terminal, _>(cx.window_id(), cx);
+    // if let Event::CloseTerminal = event {
+    //     Dock::remove::<Terminal, _>(cx.window_id(), cx);
 
-        if workspace.modal::<TerminalContainer>().is_some() {
-            workspace.dismiss_modal(cx)
-        }
-    }
+    //     if workspace.modal::<TerminalContainer>().is_some() {
+    //         workspace.dismiss_modal(cx)
+    //     }
+    // }
 }

crates/workspace/src/dock.rs 🔗

@@ -0,0 +1,35 @@
+use gpui::{elements::ChildView, Element, ElementBox, ViewContext, ViewHandle};
+use theme::Theme;
+
+use crate::{Pane, Workspace};
+
+#[derive(PartialEq, Eq)]
+pub enum DockPosition {
+    Bottom,
+    Right,
+    Fullscreen,
+    Hidden,
+}
+
+pub struct Dock {
+    position: DockPosition,
+    pane: ViewHandle<Pane>,
+}
+
+impl Dock {
+    pub fn new(cx: &mut ViewContext<Workspace>) -> Self {
+        let pane = cx.add_view(Pane::new);
+        Self {
+            pane,
+            position: DockPosition::Bottom,
+        }
+    }
+
+    pub fn render(&self, _theme: &Theme, position: DockPosition) -> Option<ElementBox> {
+        if position == self.position {
+            Some(ChildView::new(self.pane.clone()).boxed())
+        } else {
+            None
+        }
+    }
+}

crates/workspace/src/pane.rs 🔗

@@ -1412,6 +1412,10 @@ impl View for Pane {
                     .on_down(MouseButton::Left, |_, cx| {
                         cx.focus_parent_view();
                     })
+                    .on_up(MouseButton::Left, {
+                        let pane = this.clone();
+                        move |_, cx: &mut EventContext| Pane::handle_dropped_item(&pane, 0, cx)
+                    })
                     .boxed()
                 })
                 .on_navigate_mouse_down(move |direction, cx| {

crates/workspace/src/programs.rs 🔗

@@ -1,75 +0,0 @@
-// TODO: Need to put this basic structure in workspace, and make 'program handles'
-// based off of the 'searchable item' pattern except with models. This way, the workspace's clients
-// can register their models as programs with a specific identity and capable of notifying the workspace
-// Programs are:
-//  - Kept alive by the program manager, they need to emit an event to get dropped from it
-//  - Can be interacted with directly, (closed, activated, etc.) by the program manager, bypassing
-//    associated view(s)
-//  - Have special rendering methods that the program manager requires them to implement to fill out
-//    the status bar
-//  - Can emit events for the program manager which:
-//    - Add a jewel (notification, change, etc.)
-//    - Drop the program
-//    - ???
-//  - Program Manager is kept in a global, listens for window drop so it can drop all it's program handles
-
-use collections::HashMap;
-use gpui::{AnyModelHandle, Entity, ModelHandle, View, ViewContext};
-
-/// This struct is going to be the starting point for the 'program manager' feature that will
-/// eventually be implemented to provide a collaborative way of engaging with identity-having
-/// features like the terminal.
-pub struct Dock {
-    // TODO: Make this a hashset or something
-    modals: HashMap<usize, AnyModelHandle>,
-}
-
-impl Dock {
-    pub fn insert_or_replace<T: Entity, V: View>(
-        window: usize,
-        program: ModelHandle<T>,
-        cx: &mut ViewContext<V>,
-    ) -> Option<AnyModelHandle> {
-        cx.update_global::<Dock, _, _>(|pm, _| pm.insert_or_replace_internal::<T>(window, program))
-    }
-
-    pub fn remove<T: Entity, V: View>(
-        window: usize,
-        cx: &mut ViewContext<V>,
-    ) -> Option<ModelHandle<T>> {
-        cx.update_global::<Dock, _, _>(|pm, _| pm.remove_internal::<T>(window))
-    }
-
-    pub fn new() -> Self {
-        Self {
-            modals: Default::default(),
-        }
-    }
-
-    /// Inserts or replaces the model at the given location.
-    fn insert_or_replace_internal<T: Entity>(
-        &mut self,
-        window: usize,
-        program: ModelHandle<T>,
-    ) -> Option<AnyModelHandle> {
-        self.modals.insert(window, AnyModelHandle::from(program))
-    }
-
-    /// Remove the program associated with this window, if it's of the given type
-    fn remove_internal<T: Entity>(&mut self, window: usize) -> Option<ModelHandle<T>> {
-        let program = self.modals.remove(&window);
-        if let Some(program) = program {
-            if program.is::<T>() {
-                // Guaranteed to be some, but leave it in the option
-                // anyway for the API
-                program.downcast()
-            } else {
-                // Model is of the incorrect type, put it back
-                self.modals.insert(window, program);
-                None
-            }
-        } else {
-            None
-        }
-    }
-}

crates/workspace/src/workspace.rs 🔗

@@ -1,3 +1,4 @@
+pub mod dock;
 /// NOTE: Focus only 'takes' after an update has flushed_effects. Pane sends an event in on_focus_in
 /// which the workspace uses to change the activated pane.
 ///
@@ -5,7 +6,6 @@
 /// specific locations.
 pub mod pane;
 pub mod pane_group;
-pub mod programs;
 pub mod searchable;
 pub mod sidebar;
 mod status_bar;
@@ -18,6 +18,7 @@ use client::{
 };
 use clock::ReplicaId;
 use collections::{hash_map, HashMap, HashSet};
+use dock::{Dock, DockPosition};
 use drag_and_drop::DragAndDrop;
 use futures::{channel::oneshot, FutureExt};
 use gpui::{
@@ -37,7 +38,6 @@ use log::error;
 pub use pane::*;
 pub use pane_group::*;
 use postage::prelude::Stream;
-use programs::Dock;
 use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
 use searchable::SearchableItemHandle;
 use serde::Deserialize;
@@ -146,9 +146,6 @@ impl_internal_actions!(
 impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]);
 
 pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
-    // Initialize the program manager immediately
-    cx.set_global(Dock::new());
-
     pane::init(cx);
 
     cx.add_global_action(open);
@@ -893,6 +890,7 @@ pub struct Workspace {
     panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
     active_pane: ViewHandle<Pane>,
     status_bar: ViewHandle<StatusBar>,
+    dock: Dock,
     notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
     project: ModelHandle<Project>,
     leader_state: LeaderState,
@@ -998,10 +996,13 @@ impl Workspace {
             drag_and_drop.register_container(weak_self.clone());
         });
 
+        let dock = Dock::new(cx);
+
         let mut this = Workspace {
             modal: None,
             weak_self,
             center: PaneGroup::new(pane.clone()),
+            dock,
             panes: vec![pane.clone()],
             panes_by_item: Default::default(),
             active_pane: pane.clone(),
@@ -2557,14 +2558,36 @@ impl View for Workspace {
                                         },
                                     )
                                     .with_child(
-                                        FlexItem::new(self.center.render(
-                                            &theme,
-                                            &self.follower_states_by_leader,
-                                            self.project.read(cx).collaborators(),
-                                        ))
+                                        FlexItem::new(
+                                            Flex::column()
+                                                .with_child(
+                                                    FlexItem::new(self.center.render(
+                                                        &theme,
+                                                        &self.follower_states_by_leader,
+                                                        self.project.read(cx).collaborators(),
+                                                    ))
+                                                    .flex(1., true)
+                                                    .boxed(),
+                                                )
+                                                .with_children(
+                                                    self.dock
+                                                        .render(&theme, DockPosition::Bottom)
+                                                        .map(|dock| {
+                                                            FlexItem::new(dock)
+                                                                .flex(1., true)
+                                                                .boxed()
+                                                        }),
+                                                )
+                                                .boxed(),
+                                        )
                                         .flex(1., true)
                                         .boxed(),
                                     )
+                                    .with_children(
+                                        self.dock
+                                            .render(&theme, DockPosition::Right)
+                                            .map(|dock| FlexItem::new(dock).flex(1., true).boxed()),
+                                    )
                                     .with_children(
                                         if self.right_sidebar.read(cx).active_item().is_some() {
                                             Some(
@@ -2578,6 +2601,7 @@ impl View for Workspace {
                                     )
                                     .boxed()
                             })
+                            .with_children(self.dock.render(&theme, DockPosition::Fullscreen))
                             .with_children(self.modal.as_ref().map(|m| {
                                 ChildView::new(m)
                                     .contained()