add right sidebar auto-open to dock panels

This commit is contained in:
2026-11-02 20:26:49 +07:00
parent a6014b4ff3
commit 9d5cf5684f
4 changed files with 287 additions and 15 deletions
+111 -3
View File
@@ -2,6 +2,7 @@
//! registered panels, split two ways, collapsing as tabs close.
use std::collections::HashMap;
use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
use gpui::{
@@ -27,6 +28,8 @@ const DEFAULT_SPLIT_RATIO: f32 = 0.5;
/// Builds the view a panel opens as, once per open.
type PanelBuilder = Box<dyn Fn(&mut Window, &mut App) -> AnyView>;
/// Runs when a panel with a right sidebar activates, so the app can open it.
type OpenRightSidebarHandler = Rc<dyn Fn(&mut App)>;
/// A registered panel: its identity, chrome, and how its view is built.
pub struct PanelDescriptor {
@@ -34,6 +37,7 @@ pub struct PanelDescriptor {
title: SharedString,
icon: Option<IconRole>,
build: PanelBuilder,
right_sidebar: Option<PanelBuilder>,
}
impl PanelDescriptor {
@@ -49,6 +53,7 @@ impl PanelDescriptor {
title: title.into(),
icon: None,
build: Box::new(build),
right_sidebar: None,
}
}
@@ -57,6 +62,17 @@ impl PanelDescriptor {
self.icon = Some(role);
self
}
/// Sets the view the right sidebar shows while this panel is active.
///
/// Activating the panel opens the right sidebar.
pub fn right_sidebar<F>(mut self, build: F) -> Self
where
F: Fn(&mut Window, &mut App) -> AnyView + 'static,
{
self.right_sidebar = Some(Box::new(build));
self
}
}
/// Which side of its pane a split places the new pane on.
@@ -116,6 +132,7 @@ struct PanelTab {
title: SharedString,
icon: Option<IconRole>,
view: AnyView,
right_view: Option<AnyView>,
}
/// A leaf: the tabs of one region and which is active.
@@ -146,6 +163,10 @@ pub struct DockState {
/// The pane new panels open into, kept on the last activated pane.
active_pane: u64,
next_key: u64,
/// The right sidebar view on display, retained from its panel.
right_view: Option<AnyView>,
/// Runs when a panel with a right sidebar activates, so the app can open it.
on_open_right_sidebar: Option<OpenRightSidebarHandler>,
}
impl DockState {
@@ -160,9 +181,16 @@ impl DockState {
panels: HashMap::new(),
active_pane: 0,
next_key: 1,
right_view: None,
on_open_right_sidebar: None,
})
}
/// Runs when a panel with a right sidebar activates.
pub fn on_open_right_sidebar(&mut self, handler: impl Fn(&mut App) + 'static) {
self.on_open_right_sidebar = Some(Rc::new(handler));
}
/// Registers a panel descriptor, replacing any earlier one with its id.
pub fn register_panel(&mut self, panel: PanelDescriptor, cx: &mut Context<Self>) {
if self.panels.insert(panel.id.clone(), panel).is_none() {
@@ -181,11 +209,17 @@ impl DockState {
return;
};
let right_view = panel
.right_sidebar
.as_ref()
.map(|build| (build)(window, cx));
let tab = PanelTab {
panel: SharedString::from(id),
title: panel.title.clone(),
icon: panel.icon,
view: (panel.build)(window, cx),
right_view,
};
let pane_key = self.target_pane_key();
@@ -196,9 +230,22 @@ impl DockState {
}
self.active_pane = pane_key;
self.show_right_view_of(id, cx);
cx.notify();
}
/// Points the right sidebar at the panel's own view and asks the app to open it.
fn show_right_view_of(&mut self, id: &str, cx: &mut Context<Self>) {
let right_view = tab_in(&self.root, id).and_then(|tab| tab.right_view.clone());
let Some(right_view) = right_view else {
return;
};
self.right_view = Some(right_view);
if let Some(handler) = self.on_open_right_sidebar.clone() {
(handler)(cx);
}
}
/// Activates a panel's tab and makes its pane the active pane.
pub fn activate_panel(&mut self, id: &str, cx: &mut Context<Self>) {
let Some((pane_key, _)) = self.locate(id) else {
@@ -216,6 +263,7 @@ impl DockState {
};
self.active_pane = pane_key;
if changed {
self.show_right_view_of(id, cx);
cx.notify();
}
}
@@ -226,13 +274,13 @@ impl DockState {
return;
};
let emptied = {
let (closed_right_view, emptied) = {
let Some(pane) = self.pane_mut(pane_key) else {
return;
};
let was_active = pane.active.as_deref() == Some(id);
pane.tabs.remove(index);
let closed = pane.tabs.remove(index);
if was_active {
pane.active = pane
@@ -246,9 +294,14 @@ impl DockState {
.map(|tab| tab.panel.clone());
}
pane.tabs.is_empty()
(closed.right_view, pane.tabs.is_empty())
};
// The sidebar showed the closed panel's view.
if self.right_view.as_ref() == closed_right_view.as_ref() {
self.right_view = first_right_view(&self.root);
}
if emptied {
self.detach_pane(pane_key);
if self.active_pane == pane_key {
@@ -627,6 +680,22 @@ fn pane_in(node: &Node, key: u64) -> Option<&Pane> {
}
}
/// Finds an open tab by panel id, anywhere in the tree.
fn tab_in<'a>(node: &'a Node, id: &str) -> Option<&'a PanelTab> {
match node {
Node::Pane(pane) => pane.tabs.iter().find(|tab| tab.panel.as_ref() == id),
Node::Split(split) => tab_in(&split.a, id).or_else(|| tab_in(&split.b, id)),
}
}
/// The first open tab's right sidebar view, in display order.
fn first_right_view(node: &Node) -> Option<AnyView> {
match node {
Node::Pane(pane) => pane.tabs.iter().find_map(|tab| tab.right_view.clone()),
Node::Split(split) => first_right_view(&split.a).or_else(|| first_right_view(&split.b)),
}
}
/// The pane key of a node that is a pane, for seam-level tree edits.
fn direct_pane_key(node: &Node) -> Option<u64> {
match node {
@@ -744,6 +813,45 @@ struct Titlebar {
traffic_light_pad: bool,
}
/// The right sidebar content of the dock's active panel.
#[derive(IntoElement)]
pub struct DockSidebar {
id: ElementId,
state: Entity<DockState>,
}
impl DockSidebar {
/// Creates the sidebar content bound to `state`.
pub fn new<I>(id: I, state: Entity<DockState>) -> Self
where
I: Into<ElementId>,
{
Self {
id: id.into(),
state,
}
}
}
impl RenderOnce for DockSidebar {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
// The cache keeps an un-notified sidebar from re-rendering on frames.
let right_view = self.state.read(cx).right_view.clone();
div()
.id(self.id.clone())
.debug_selector({
let id = self.id.clone();
move || format!("{id}-view")
})
.size_full()
.overflow_hidden()
.when_some(right_view, |sidebar, view| {
sidebar.child(view.cached(StyleRefinement::default().absolute().size_full()))
})
}
}
fn render_node(
dock_id: &ElementId,
state: &Entity<DockState>,