debugger: Make debug panes zoomable (#29365)

Cole Miller and Anthony Eid created

- [x] Buttons
- [x] Make it keyboard-driven

Co-authored-by: Anthony <anthony@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>

Change summary

crates/debugger_ui/src/debugger_panel.rs  |   7 
crates/debugger_ui/src/session/running.rs | 261 +++++++++++++++---------
2 files changed, 168 insertions(+), 100 deletions(-)

Detailed changes

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -39,7 +39,7 @@ use task::{
 use terminal_view::TerminalView;
 use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
 use workspace::{
-    Workspace,
+    Pane, Workspace,
     dock::{DockPosition, Panel, PanelEvent},
 };
 
@@ -1097,9 +1097,14 @@ impl Panel for DebugPanel {
         Box::new(ToggleFocus)
     }
 
+    fn pane(&self) -> Option<Entity<Pane>> {
+        None
+    }
+
     fn activation_priority(&self) -> u32 {
         9
     }
+
     fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
 }
 

crates/debugger_ui/src/session/running.rs 🔗

@@ -29,9 +29,10 @@ use settings::Settings;
 use stack_frame_list::StackFrameList;
 use terminal_view::TerminalView;
 use ui::{
-    ActiveTheme, AnyElement, App, Context, ContextMenu, DropdownMenu, FluentBuilder,
-    InteractiveElement, IntoElement, Label, LabelCommon as _, ParentElement, Render, SharedString,
-    StatefulInteractiveElement, Styled, Tab, Window, div, h_flex, v_flex,
+    ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, ContextMenu,
+    DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, InteractiveElement, IntoElement,
+    Label, LabelCommon as _, ParentElement, Render, SharedString, StatefulInteractiveElement,
+    Styled, Tab, Tooltip, Window, div, h_flex, v_flex,
 };
 use util::ResultExt;
 use variable_list::VariableList;
@@ -62,8 +63,16 @@ pub struct RunningState {
 
 impl Render for RunningState {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let zoomed_pane = self
+            .panes
+            .panes()
+            .into_iter()
+            .find(|pane| pane.read(cx).is_zoomed());
+
         let active = self.panes.panes().into_iter().next();
-        let x = if let Some(active) = active {
+        let x = if let Some(ref zoomed_pane) = zoomed_pane {
+            zoomed_pane.update(cx, |pane, cx| pane.render(window, cx).into_any_element())
+        } else if let Some(active) = active {
             self.panes
                 .render(
                     None,
@@ -257,109 +266,148 @@ pub(crate) fn new_debugger_pane(
             window,
             cx,
         );
-        pane.set_can_split(Some(Arc::new(move |pane, dragged_item, _window, cx| {
-            if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
-                let is_current_pane = tab.pane == cx.entity();
-                let Some(can_drag_away) = weak_running
-                    .update(cx, |running_state, _| {
-                        let current_panes = running_state.panes.panes();
-                        !current_panes.contains(&&tab.pane)
-                            || current_panes.len() > 1
-                            || (!is_current_pane || pane.items_len() > 1)
-                    })
-                    .ok()
-                else {
-                    return false;
-                };
-                if can_drag_away {
-                    let item = if is_current_pane {
-                        pane.item_for_index(tab.ix)
-                    } else {
-                        tab.pane.read(cx).item_for_index(tab.ix)
+        let focus_handle = pane.focus_handle(cx);
+        pane.set_can_split(Some(Arc::new({
+            let weak_running = weak_running.clone();
+            move |pane, dragged_item, _window, cx| {
+                if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
+                    let is_current_pane = tab.pane == cx.entity();
+                    let Some(can_drag_away) = weak_running
+                        .update(cx, |running_state, _| {
+                            let current_panes = running_state.panes.panes();
+                            !current_panes.contains(&&tab.pane)
+                                || current_panes.len() > 1
+                                || (!is_current_pane || pane.items_len() > 1)
+                        })
+                        .ok()
+                    else {
+                        return false;
                     };
-                    if let Some(item) = item {
-                        return item.downcast::<SubView>().is_some();
+                    if can_drag_away {
+                        let item = if is_current_pane {
+                            pane.item_for_index(tab.ix)
+                        } else {
+                            tab.pane.read(cx).item_for_index(tab.ix)
+                        };
+                        if let Some(item) = item {
+                            return item.downcast::<SubView>().is_some();
+                        }
                     }
                 }
+                false
             }
-            false
         })));
         pane.display_nav_history_buttons(None);
         pane.set_custom_drop_handle(cx, custom_drop_handle);
         pane.set_should_display_tab_bar(|_, _| true);
         pane.set_render_tab_bar_buttons(cx, |_, _, _| (None, None));
-        pane.set_render_tab_bar(cx, |pane, window, cx| {
-            let active_pane_item = pane.active_item();
-            h_flex()
-                .w_full()
-                .px_2()
-                .gap_1()
-                .h(Tab::container_height(cx))
-                .drag_over::<DraggedTab>(|bar, _, _, cx| {
-                    bar.bg(cx.theme().colors().drop_target_background)
-                })
-                .on_drop(
-                    cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
-                        this.drag_split_direction = None;
-                        this.handle_tab_drop(dragged_tab, this.items_len(), window, cx)
-                    }),
-                )
-                .bg(cx.theme().colors().tab_bar_background)
-                .border_b_1()
-                .border_color(cx.theme().colors().border)
-                .children(pane.items().enumerate().map(|(ix, item)| {
-                    let selected = active_pane_item
-                        .as_ref()
-                        .map_or(false, |active| active.item_id() == item.item_id());
-                    let item_ = item.boxed_clone();
-                    div()
-                        .id(SharedString::from(format!(
-                            "debugger_tab_{}",
-                            item.item_id().as_u64()
-                        )))
-                        .p_1()
-                        .rounded_md()
-                        .cursor_pointer()
-                        .map(|this| {
-                            if selected {
-                                this.bg(cx.theme().colors().tab_active_background)
+        pane.set_render_tab_bar(cx, {
+            move |pane, window, cx| {
+                let active_pane_item = pane.active_item();
+                h_flex()
+                    .justify_between()
+                    .bg(cx.theme().colors().tab_bar_background)
+                    .border_b_1()
+                    .border_color(cx.theme().colors().border)
+                    .track_focus(&focus_handle)
+                    .child(
+                        h_flex()
+                            .w_full()
+                            .px_2()
+                            .gap_1()
+                            .h(Tab::container_height(cx))
+                            .drag_over::<DraggedTab>(|bar, _, _, cx| {
+                                bar.bg(cx.theme().colors().drop_target_background)
+                            })
+                            .on_drop(cx.listener(
+                                move |this, dragged_tab: &DraggedTab, window, cx| {
+                                    this.drag_split_direction = None;
+                                    this.handle_tab_drop(dragged_tab, this.items_len(), window, cx)
+                                },
+                            ))
+                            .children(pane.items().enumerate().map(|(ix, item)| {
+                                let selected = active_pane_item
+                                    .as_ref()
+                                    .map_or(false, |active| active.item_id() == item.item_id());
+                                let item_ = item.boxed_clone();
+                                div()
+                                    .id(SharedString::from(format!(
+                                        "debugger_tab_{}",
+                                        item.item_id().as_u64()
+                                    )))
+                                    .p_1()
+                                    .rounded_md()
+                                    .cursor_pointer()
+                                    .map(|this| {
+                                        if selected {
+                                            this.bg(cx.theme().colors().tab_active_background)
+                                        } else {
+                                            let hover_color = cx.theme().colors().element_hover;
+                                            this.hover(|style| style.bg(hover_color))
+                                        }
+                                    })
+                                    .on_click(cx.listener(move |this, _, window, cx| {
+                                        let index = this.index_for_item(&*item_);
+                                        if let Some(index) = index {
+                                            this.activate_item(index, true, true, window, cx);
+                                        }
+                                    }))
+                                    .child(item.tab_content(
+                                        TabContentParams {
+                                            selected,
+                                            ..Default::default()
+                                        },
+                                        window,
+                                        cx,
+                                    ))
+                                    .on_drop(cx.listener(
+                                        move |this, dragged_tab: &DraggedTab, window, cx| {
+                                            this.drag_split_direction = None;
+                                            this.handle_tab_drop(dragged_tab, ix, window, cx)
+                                        },
+                                    ))
+                                    .on_drag(
+                                        DraggedTab {
+                                            item: item.boxed_clone(),
+                                            pane: cx.entity().clone(),
+                                            detail: 0,
+                                            is_active: selected,
+                                            ix,
+                                        },
+                                        |tab, _, _, cx| cx.new(|_| tab.clone()),
+                                    )
+                            })),
+                    )
+                    .child({
+                        let zoomed = pane.is_zoomed();
+                        IconButton::new(
+                            "debug-toggle-zoom",
+                            if zoomed {
+                                IconName::Minimize
                             } else {
-                                let hover_color = cx.theme().colors().element_hover;
-                                this.hover(|style| style.bg(hover_color))
-                            }
-                        })
-                        .on_click(cx.listener(move |this, _, window, cx| {
-                            let index = this.index_for_item(&*item_);
-                            if let Some(index) = index {
-                                this.activate_item(index, true, true, window, cx);
-                            }
-                        }))
-                        .child(item.tab_content(
-                            TabContentParams {
-                                selected,
-                                ..Default::default()
-                            },
-                            window,
-                            cx,
-                        ))
-                        .on_drop(
-                            cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
-                                this.drag_split_direction = None;
-                                this.handle_tab_drop(dragged_tab, ix, window, cx)
-                            }),
-                        )
-                        .on_drag(
-                            DraggedTab {
-                                item: item.boxed_clone(),
-                                pane: cx.entity().clone(),
-                                detail: 0,
-                                is_active: selected,
-                                ix,
+                                IconName::Maximize
                             },
-                            |tab, _, _, cx| cx.new(|_| tab.clone()),
                         )
-                }))
-                .into_any_element()
+                        .icon_size(IconSize::Small)
+                        .on_click(cx.listener(move |pane, _, window, cx| {
+                            pane.toggle_zoom(&workspace::ToggleZoom, window, cx);
+                        }))
+                        .tooltip({
+                            let focus_handle = focus_handle.clone();
+                            move |window, cx| {
+                                let zoomed_text = if zoomed { "Zoom Out" } else { "Zoom In" };
+                                Tooltip::for_action_in(
+                                    zoomed_text,
+                                    &workspace::ToggleZoom,
+                                    &focus_handle,
+                                    window,
+                                    cx,
+                                )
+                            }
+                        })
+                    })
+                    .into_any_element()
+            }
         });
         pane
     });
@@ -730,10 +778,25 @@ impl RunningState {
         cx: &mut Context<RunningState>,
     ) {
         this.serialize_layout(window, cx);
-        if let Event::Remove { .. } = event {
-            let _did_find_pane = this.panes.remove(&source_pane).is_ok();
-            debug_assert!(_did_find_pane);
-            cx.notify();
+        match event {
+            Event::Remove { .. } => {
+                let _did_find_pane = this.panes.remove(&source_pane).is_ok();
+                debug_assert!(_did_find_pane);
+                cx.notify();
+            }
+            Event::ZoomIn => {
+                source_pane.update(cx, |pane, cx| {
+                    pane.set_zoomed(true, cx);
+                });
+                cx.notify();
+            }
+            Event::ZoomOut => {
+                source_pane.update(cx, |pane, cx| {
+                    pane.set_zoomed(false, cx);
+                });
+                cx.notify();
+            }
+            _ => {}
         }
     }