add right sidebar auto-open to dock panels
This commit is contained in:
+111
-3
@@ -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>,
|
||||
|
||||
+119
-1
@@ -3,8 +3,10 @@ use gpui::{
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Render, Styled as _,
|
||||
TestAppContext, VisualTestContext, Window, div, point, px,
|
||||
};
|
||||
use gpui_native_layout::dock::{Dock, DockState, PanelDescriptor, SplitDirection};
|
||||
use gpui_native_layout::dock::{Dock, DockSidebar, DockState, PanelDescriptor, SplitDirection};
|
||||
use gpui_native_theme::Theme;
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
struct Panel {
|
||||
tag: &'static str,
|
||||
@@ -18,10 +20,28 @@ impl Render for Panel {
|
||||
}
|
||||
}
|
||||
|
||||
/// The right sidebar view a panel declares.
|
||||
struct SidePanel {
|
||||
tag: &'static str,
|
||||
}
|
||||
|
||||
impl Render for SidePanel {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.debug_selector(move || format!("side-{}", self.tag))
|
||||
}
|
||||
}
|
||||
|
||||
fn descriptor(tag: &'static str) -> PanelDescriptor {
|
||||
PanelDescriptor::new(tag, tag, move |_, cx| cx.new(|_| Panel { tag }).into())
|
||||
}
|
||||
|
||||
fn descriptor_with_right_sidebar(tag: &'static str) -> PanelDescriptor {
|
||||
PanelDescriptor::new(tag, tag, move |_, cx| cx.new(|_| Panel { tag }).into())
|
||||
.right_sidebar(move |_, cx| cx.new(|_| SidePanel { tag }).into())
|
||||
}
|
||||
|
||||
struct Harness {
|
||||
dock: Entity<DockState>,
|
||||
}
|
||||
@@ -420,3 +440,101 @@ fn close_others_keeps_one_and_close_pane_empties() {
|
||||
open(&mut visual, &dock, "inbox");
|
||||
assert!(bounds(&mut visual, "test-dock-pane-0-tabs-tab-inbox").is_some());
|
||||
}
|
||||
|
||||
struct SidebarHarness {
|
||||
dock: Entity<DockState>,
|
||||
}
|
||||
|
||||
impl Render for SidebarHarness {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.child(Dock::new("test-dock", self.dock.clone())),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(px(160.))
|
||||
.child(DockSidebar::new("test-dock-sidebar", self.dock.clone())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn sidebar_window(
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<DockState>, Rc<Cell<bool>>, VisualTestContext) {
|
||||
cx.update(Theme::init);
|
||||
let dock = cx.update(DockState::new);
|
||||
let opened = Rc::new(Cell::new(false));
|
||||
let opened_for_dock = opened.clone();
|
||||
let dock_for_window = dock.clone();
|
||||
let window = cx.add_window(move |_, _| SidebarHarness {
|
||||
dock: dock_for_window,
|
||||
});
|
||||
dock.update(cx, |dock, _| {
|
||||
dock.on_open_right_sidebar(move |cx| {
|
||||
opened_for_dock.set(true);
|
||||
let _ = cx;
|
||||
});
|
||||
});
|
||||
let mut visual = VisualTestContext::from_window(window.into(), cx);
|
||||
visual.update(|window, cx| window.draw(cx).clear(cx));
|
||||
(dock, opened, visual)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn activating_a_panel_with_a_right_view_opens_the_sidebar() {
|
||||
let mut cx = TestAppContext::single();
|
||||
let (dock, opened, mut visual) = sidebar_window(&mut cx);
|
||||
cx.update(|cx| {
|
||||
dock.update(cx, |dock, cx| {
|
||||
dock.register_panel(descriptor_with_right_sidebar("overview"), cx);
|
||||
dock.register_panel(descriptor("inbox"), cx);
|
||||
});
|
||||
});
|
||||
|
||||
open(&mut visual, &dock, "overview");
|
||||
assert!(
|
||||
opened.get(),
|
||||
"activation must ask the app to open the sidebar"
|
||||
);
|
||||
assert!(
|
||||
bounds(&mut visual, "side-overview").is_some(),
|
||||
"the sidebar shows the panel's own view"
|
||||
);
|
||||
|
||||
// A panel without a right view leaves the sidebar showing the last one.
|
||||
open(&mut visual, &dock, "inbox");
|
||||
assert!(opened.get());
|
||||
assert!(bounds(&mut visual, "side-overview").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_the_displayed_tab_falls_back_to_another_right_view() {
|
||||
let mut cx = TestAppContext::single();
|
||||
let (dock, opened, mut visual) = sidebar_window(&mut cx);
|
||||
cx.update(|cx| {
|
||||
dock.update(cx, |dock, cx| {
|
||||
dock.register_panel(descriptor_with_right_sidebar("overview"), cx);
|
||||
dock.register_panel(descriptor_with_right_sidebar("explore"), cx);
|
||||
});
|
||||
});
|
||||
open(&mut visual, &dock, "overview");
|
||||
open(&mut visual, &dock, "explore");
|
||||
assert!(bounds(&mut visual, "side-explore").is_some());
|
||||
|
||||
cx.update(|cx| {
|
||||
dock.update(cx, |dock, cx| dock.close_panel("explore", cx));
|
||||
});
|
||||
redraw(&mut visual);
|
||||
|
||||
assert!(
|
||||
bounds(&mut visual, "side-overview").is_some(),
|
||||
"the sidebar falls back to a remaining panel's view"
|
||||
);
|
||||
let _ = opened;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user