Implement zooming for panes and docks

Antonio Scandurra created

Change summary

assets/keymaps/default.json       |  2 
crates/gpui/src/elements.rs       |  7 -
crates/workspace/src/dock.rs      | 77 +++++++++++++++++++++------
crates/workspace/src/pane.rs      | 10 +--
crates/workspace/src/workspace.rs | 89 ++++++++++++++++++++------------
5 files changed, 122 insertions(+), 63 deletions(-)

Detailed changes

assets/keymaps/default.json 🔗

@@ -220,7 +220,7 @@
             "alt-cmd-c": "search::ToggleCaseSensitive",
             "alt-cmd-w": "search::ToggleWholeWord",
             "alt-cmd-r": "search::ToggleRegex",
-            "shift-escape": "pane::ToggleZoom"
+            "shift-escape": "workspace::ToggleZoom"
         }
     },
     // Bindings from VS Code

crates/gpui/src/elements.rs 🔗

@@ -33,11 +33,8 @@ use crate::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
-    json,
-    platform::MouseButton,
-    scene::MouseDown,
-    Action, EventContext, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
-    WeakViewHandle, WindowContext,
+    json, Action, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, WeakViewHandle,
+    WindowContext,
 };
 use anyhow::{anyhow, Result};
 use collections::HashMap;

crates/workspace/src/dock.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{StatusItemView, Workspace};
+use crate::{StatusItemView, ToggleZoom, Workspace};
 use context_menu::{ContextMenu, ContextMenuItem};
 use gpui::{
     elements::*, impl_actions, platform::CursorStyle, platform::MouseButton, AnyViewHandle,
@@ -10,7 +10,7 @@ use settings::Settings;
 use std::rc::Rc;
 
 pub fn init(cx: &mut AppContext) {
-    cx.capture_action(Dock::toggle_zoom);
+    cx.add_action(Dock::toggle_zoom);
 }
 
 pub trait Panel: View {
@@ -98,6 +98,10 @@ impl From<&dyn PanelHandle> for AnyViewHandle {
     }
 }
 
+pub enum Event {
+    ZoomIn,
+}
+
 pub struct Dock {
     position: DockPosition,
     panel_entries: Vec<PanelEntry>,
@@ -141,6 +145,7 @@ struct PanelEntry {
     panel: Rc<dyn PanelHandle>,
     size: f32,
     context_menu: ViewHandle<ContextMenu>,
+    zoomed: bool,
     _subscriptions: [Subscription; 2],
 }
 
@@ -187,6 +192,25 @@ impl Dock {
         cx.notify();
     }
 
+    pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
+        for (ix, entry) in self.panel_entries.iter_mut().enumerate() {
+            if ix == self.active_panel_index && entry.panel.can_zoom(cx) {
+                entry.zoomed = zoomed;
+            } else {
+                entry.zoomed = false;
+            }
+        }
+
+        cx.notify();
+    }
+
+    pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
+        cx.propagate_action();
+        if !self.active_entry().map_or(false, |entry| entry.zoomed) {
+            cx.emit(Event::ZoomIn);
+        }
+    }
+
     pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
         let subscriptions = [
             cx.observe(&panel, |_, _, cx| cx.notify()),
@@ -214,6 +238,7 @@ impl Dock {
         self.panel_entries.push(PanelEntry {
             panel: Rc::new(panel),
             size,
+            zoomed: false,
             context_menu: cx.add_view(|cx| {
                 let mut menu = ContextMenu::new(dock_view_id, cx);
                 menu.set_position_mode(OverlayPositionMode::Local);
@@ -260,10 +285,22 @@ impl Dock {
     }
 
     pub fn active_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
+        let entry = self.active_entry()?;
+        Some(&entry.panel)
+    }
+
+    fn active_entry(&self) -> Option<&PanelEntry> {
         if self.is_open {
-            self.panel_entries
-                .get(self.active_panel_index)
-                .map(|entry| &entry.panel)
+            self.panel_entries.get(self.active_panel_index)
+        } else {
+            None
+        }
+    }
+
+    pub fn zoomed_panel(&self) -> Option<AnyViewHandle> {
+        let entry = self.active_entry()?;
+        if entry.zoomed {
+            Some(entry.panel.as_any().clone())
         } else {
             None
         }
@@ -305,7 +342,7 @@ impl Dock {
 }
 
 impl Entity for Dock {
-    type Event = ();
+    type Event = Event;
 }
 
 impl View for Dock {
@@ -314,18 +351,22 @@ impl View for Dock {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        if let Some(active_panel) = self.active_panel() {
-            let size = self.active_panel_size().unwrap();
-            let style = &cx.global::<Settings>().theme.workspace.dock;
-            ChildView::new(active_panel.as_any(), cx)
-                .contained()
-                .with_style(style.container)
-                .resizable(
-                    self.position.to_resize_handle_side(),
-                    size,
-                    |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx),
-                )
-                .into_any()
+        if let Some(active_entry) = self.active_entry() {
+            if active_entry.zoomed {
+                Empty::new().into_any()
+            } else {
+                let size = self.active_panel_size().unwrap();
+                let style = &cx.global::<Settings>().theme.workspace.dock;
+                ChildView::new(active_entry.panel.as_any(), cx)
+                    .contained()
+                    .with_style(style.container)
+                    .resizable(
+                        self.position.to_resize_handle_side(),
+                        size,
+                        |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx),
+                    )
+                    .into_any()
+            }
         } else {
             Empty::new().into_any()
         }

crates/workspace/src/pane.rs 🔗

@@ -2,7 +2,8 @@ mod dragged_item_receiver;
 
 use super::{ItemHandle, SplitDirection};
 use crate::{
-    item::WeakItemHandle, toolbar::Toolbar, Item, NewFile, NewSearch, NewTerminal, Workspace,
+    item::WeakItemHandle, toolbar::Toolbar, Item, NewFile, NewSearch, NewTerminal, ToggleZoom,
+    Workspace,
 };
 use anyhow::{anyhow, Result};
 use collections::{HashMap, HashSet, VecDeque};
@@ -69,7 +70,6 @@ actions!(
         SplitUp,
         SplitRight,
         SplitDown,
-        ToggleZoom,
     ]
 );
 
@@ -135,7 +135,6 @@ pub enum Event {
     ChangeItemTitle,
     Focus,
     ZoomIn,
-    ZoomOut,
 }
 
 pub struct Pane {
@@ -662,9 +661,8 @@ impl Pane {
     }
 
     pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
-        if self.zoomed {
-            cx.emit(Event::ZoomOut);
-        } else {
+        cx.propagate_action();
+        if !self.zoomed {
             cx.emit(Event::ZoomIn);
         }
     }

crates/workspace/src/workspace.rs 🔗

@@ -119,7 +119,8 @@ actions!(
         NewSearch,
         Feedback,
         Restart,
-        Welcome
+        Welcome,
+        ToggleZoom,
     ]
 );
 
@@ -181,6 +182,7 @@ pub type WorkspaceId = i64;
 impl_actions!(workspace, [ActivatePane]);
 
 pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
+    dock::init(cx);
     pane::init(cx);
     notifications::init(cx);
 
@@ -230,6 +232,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
         },
     );
     cx.add_action(Workspace::toggle_panel);
+    cx.add_action(Workspace::toggle_zoom);
     cx.add_action(Workspace::focus_center);
     cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
         workspace.activate_previous_pane(cx)
@@ -441,7 +444,6 @@ pub struct Workspace {
     weak_self: WeakViewHandle<Self>,
     remote_entity_subscription: Option<client::Subscription>,
     modal: Option<AnyViewHandle>,
-    zoomed: Option<AnyViewHandle>,
     center: PaneGroup,
     left_dock: ViewHandle<Dock>,
     bottom_dock: ViewHandle<Dock>,
@@ -593,7 +595,7 @@ impl Workspace {
             active_call = Some((call, subscriptions));
         }
 
-        let subscriptions = vec![
+        let mut subscriptions = vec![
             cx.observe_fullscreen(|_, _, cx| cx.notify()),
             cx.observe_window_activation(Self::on_window_activation_changed),
             cx.observe_window_bounds(move |_, mut bounds, display, cx| {
@@ -613,24 +615,14 @@ impl Workspace {
                     .spawn(DB.set_window_bounds(workspace_id, bounds, display))
                     .detach_and_log_err(cx);
             }),
-            cx.observe(&left_dock, |this, _, cx| {
-                this.serialize_workspace(cx);
-                cx.notify();
-            }),
-            cx.observe(&bottom_dock, |this, _, cx| {
-                this.serialize_workspace(cx);
-                cx.notify();
-            }),
-            cx.observe(&right_dock, |this, _, cx| {
-                this.serialize_workspace(cx);
-                cx.notify();
-            }),
         ];
+        subscriptions.extend(Self::register_dock(&left_dock, cx));
+        subscriptions.extend(Self::register_dock(&bottom_dock, cx));
+        subscriptions.extend(Self::register_dock(&right_dock, cx));
 
         let mut this = Workspace {
             weak_self: weak_handle.clone(),
             modal: None,
-            zoomed: None,
             center: PaneGroup::new(center_pane.clone()),
             panes: vec![center_pane.clone()],
             panes_by_item: Default::default(),
@@ -1305,14 +1297,16 @@ impl Workspace {
         }
     }
 
-    pub fn zoom_in(&mut self, view: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        self.zoomed = Some(view);
-        cx.notify();
-    }
-
-    pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
-        self.zoomed.take();
-        cx.notify();
+    fn zoomed(&self, cx: &AppContext) -> Option<AnyViewHandle> {
+        self.left_dock
+            .read(cx)
+            .zoomed_panel()
+            .or(self.bottom_dock.read(cx).zoomed_panel())
+            .or(self.right_dock.read(cx).zoomed_panel())
+            .or_else(|| {
+                let pane = self.panes.iter().find(|pane| pane.read(cx).is_zoomed())?;
+                Some(pane.clone().into_any())
+            })
     }
 
     pub fn items<'a>(
@@ -1470,11 +1464,46 @@ impl Workspace {
         cx.notify();
     }
 
+    fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
+        // Any time the zoom is toggled we will zoom out all panes and docks. Then,
+        // the dock or pane that was zoomed will emit an event to zoom itself back in.
+        self.zoom_out(cx);
+    }
+
+    fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
+        for pane in &self.panes {
+            pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
+        }
+
+        self.left_dock
+            .update(cx, |dock, cx| dock.set_zoomed(false, cx));
+        self.bottom_dock
+            .update(cx, |dock, cx| dock.set_zoomed(false, cx));
+        self.right_dock
+            .update(cx, |dock, cx| dock.set_zoomed(false, cx));
+
+        cx.notify();
+    }
+
     pub fn focus_center(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
         cx.focus_self();
         cx.notify();
     }
 
+    fn register_dock(dock: &ViewHandle<Dock>, cx: &mut ViewContext<Self>) -> [Subscription; 2] {
+        [
+            cx.observe(dock, |this, _, cx| {
+                this.serialize_workspace(cx);
+                cx.notify();
+            }),
+            cx.subscribe(dock, |_, dock, event, cx| {
+                dock.update(cx, |dock, cx| match event {
+                    dock::Event::ZoomIn => dock.set_zoomed(true, cx),
+                })
+            }),
+        ]
+    }
+
     fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
         let pane =
             cx.add_view(|cx| Pane::new(self.weak_handle(), self.app_state.background_actions, cx));
@@ -1699,13 +1728,7 @@ impl Workspace {
             }
             pane::Event::ZoomIn => {
                 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
-                self.zoom_in(pane.into_any(), cx);
-            }
-            pane::Event::ZoomOut => {
-                if self.zoomed.as_ref().map_or(false, |zoomed| *zoomed == pane) {
-                    pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
-                    self.zoom_out(cx);
-                }
+                cx.notify();
             }
         }
 
@@ -2757,10 +2780,10 @@ impl View for Workspace {
                             })
                             .with_child(Overlay::new(
                                 Stack::new()
-                                    .with_children(self.zoomed.as_ref().map(|zoomed| {
+                                    .with_children(self.zoomed(cx).map(|zoomed| {
                                         enum ZoomBackground {}
 
-                                        ChildView::new(zoomed, cx)
+                                        ChildView::new(&zoomed, cx)
                                             .contained()
                                             .with_style(theme.workspace.zoomed_foreground)
                                             .aligned()