debugger: Make debug panes zoomable (#29365)

- [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>
This commit is contained in:
Cole Miller 2025-04-25 11:56:16 -04:00 committed by GitHub
parent a5405fcbd7
commit 65401d6d7b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 168 additions and 100 deletions

View file

@ -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))
IconName::Maximize
},
)
.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,
)
}
})
.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()),
)
}))
.into_any_element()
})
.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();
}
_ => {}
}
}