add dock layout component

This commit is contained in:
2026-11-02 16:24:20 +07:00
parent 9bce2820cc
commit 545d0986ed
11 changed files with 1914 additions and 67 deletions
+161 -9
View File
@@ -31,9 +31,10 @@ use gpui_native_core::toggle::Toggle;
use gpui_native_core::tooltip::Tooltip;
use gpui_native_data::list::{List, ListItem};
use gpui_native_data::sidebar::{Sidebar, SidebarEntry};
use gpui_native_data::tabs::{Tab, Tabs};
use gpui_native_data::tabs::{Tab, Tabs, TabsVariant};
use gpui_native_data::tree::{Tree, TreeItem};
use gpui_native_icons::{IconAssets, IconRole, icon};
use gpui_native_layout::dock::{Dock, DockState, PanelDescriptor, SplitDirection};
use gpui_native_layout::resize_handle::ResizeHandle;
use gpui_native_layout::shell::{LayoutState, Shell};
use gpui_native_theme::{ActiveTheme as _, Size, Theme};
@@ -51,6 +52,7 @@ struct Gallery {
notes_state: Entity<InputState>,
split_width: Pixels,
layout_state: Entity<LayoutState>,
dock_state: Entity<DockState>,
shell_selection: Option<usize>,
popover_open: bool,
menu_open: bool,
@@ -67,6 +69,8 @@ struct Gallery {
tree_nodes: Vec<GalleryNode>,
tree_selected: Option<usize>,
tab_selected: usize,
card_tabs: Vec<SharedString>,
card_tab_selected: usize,
sidebar_selected: Option<usize>,
}
@@ -202,19 +206,51 @@ impl Gallery {
.text_sm()
.text_color(colors.muted_foreground);
let open_panel = |dock: &Entity<DockState>, id: &'static str, label: &'static str| {
let dock = dock.clone();
Button::new(SharedString::from(format!("gallery-dock-open-{id}")))
.label(label)
.variant(ButtonVariant::Secondary)
.small()
.on_click(move |_, window, cx| {
dock.update(cx, |dock, cx| dock.open_panel(id, window, cx));
})
};
let center = v_flex()
.gap_2()
.items_center()
.justify_center()
.size_full()
.gap_2()
.child(
h_flex()
.flex_none()
.flex_wrap()
.gap_2()
.child(toggle("left"))
.child(toggle("right")),
.child(toggle("right"))
.child(open_panel(&self.dock_state, "inbox", "Open inbox"))
.child(open_panel(&self.dock_state, "settings", "Open settings"))
.child({
let dock = self.dock_state.clone();
Button::new("gallery-dock-split")
.label("Split active right")
.variant(ButtonVariant::Secondary)
.small()
.on_click(move |_, _, cx| {
dock.update(cx, |dock, cx| {
if let Some(active) = dock.active_panel() {
dock.split_panel(&active, SplitDirection::Right, cx);
}
});
})
}),
)
.child("Center \u{2014} the dock lands here (L3)")
.text_color(colors.muted_foreground);
.child(
div()
.flex_1()
.min_h_0()
.overflow_hidden()
.child(Dock::new("gallery-dock", self.dock_state.clone())),
);
Shell::new("gallery-shell", state)
.left_sidebar(left_sidebar)
@@ -223,6 +259,30 @@ impl Gallery {
}
}
/// A stand-in panel view for the dock demo.
struct DemoPanel {
title: &'static str,
body: &'static str,
}
impl Render for DemoPanel {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
v_flex()
.size_full()
.gap_1()
.p_4()
.bg(colors.background)
.child(div().child(self.title))
.child(
div()
.text_sm()
.text_color(colors.muted_foreground)
.child(self.body),
)
}
}
impl Render for Gallery {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme();
@@ -548,7 +608,7 @@ impl Render for Gallery {
.text_xs()
.text_color(colors.muted_foreground)
.child(format!(
"left: {} · right: {} \u{2014} buttons toggle, seams resize",
"left: {} · right: {} · dock: {} \u{2014} buttons toggle, seams resize, tabs close and split",
if self.layout_state.read(cx).is_left_sidebar_visible() {
"on"
} else {
@@ -559,6 +619,11 @@ impl Render for Gallery {
} else {
"off"
},
if self.dock_state.read(cx).active_panel().is_some() {
"open"
} else {
"empty"
},
)),
),
)
@@ -957,7 +1022,53 @@ impl Render for Gallery {
_ => div()
.child("Settings: keyboard focus rides the strip; arrows switch tabs.")
.into_any_element(),
}),
})
.child({
let gallery = gallery.clone();
let gallery_for_close = gallery.clone();
Tabs::new("gallery-pill-tabs")
.variant(TabsVariant::Pill)
.selected(self.card_tab_selected)
.on_change(move |selected, _, cx| {
gallery.update(cx, |gallery, cx| {
gallery.card_tab_selected = selected;
cx.notify();
});
})
.on_close(move |index, _, cx| {
gallery_for_close.update(cx, |gallery, cx| {
if index < gallery.card_tabs.len() {
gallery.card_tabs.remove(index);
gallery.card_tab_selected = gallery
.card_tab_selected
.min(gallery.card_tabs.len().saturating_sub(1));
cx.notify();
}
});
})
.tabs({
let labels = self.card_tabs.clone();
move |_, _| {
labels
.iter()
.map(|label| {
Tab::new(label.clone())
.closeable(true)
.busy(label == "Syncing")
})
.collect()
}
})
})
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!(
"pill variant (the signed dock tab) — {} tabs left; the ring on \"Syncing\" is the Spinner placeholder",
self.card_tabs.len()
)),
),
)
.child(
v_flex()
@@ -1653,6 +1764,39 @@ fn main() {
let notes_state = InputState::new(cx);
notes_state.update(cx, |state, cx| state.set_multiline(true, cx));
let layout_state = LayoutState::new(cx);
let dock_state = DockState::new(cx);
dock_state.update(cx, |dock, cx| {
dock.register_panel(
PanelDescriptor::new("overview", "Overview", |_, cx| {
cx.new(|_| DemoPanel {
title: "Overview",
body: "View state survives tab switches and pane moves.",
})
.into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("inbox", "Inbox", |_, cx| {
cx.new(|_| DemoPanel {
title: "Inbox",
body: "Right-click the tab bar for Close, Close Others, Close Pane.",
})
.into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("settings", "Settings", |_, cx| {
cx.new(|_| DemoPanel {
title: "Settings",
body: "Drag the seam between panes; double-click it to reset.",
})
.into()
}),
cx,
);
});
let bounds = Bounds::centered(None, size(px(720.0), px(560.0)), cx);
if let Err(err) = cx.open_window(
WindowOptions {
@@ -1674,6 +1818,7 @@ fn main() {
cx.refresh_windows();
})
.detach();
dock_state.update(cx, |dock, cx| dock.open_panel("overview", window, cx));
let toasts = cx.new(|_| Toasts::default());
let alerts = cx.new(|_| Alerts::default());
cx.new(|cx| {
@@ -1693,6 +1838,7 @@ fn main() {
notes_state,
split_width: px(200.),
layout_state,
dock_state,
shell_selection: None,
popover_open: false,
menu_open: false,
@@ -1708,6 +1854,12 @@ fn main() {
list_selected: None,
tree_selected: None,
tab_selected: 0,
card_tabs: vec![
SharedString::from("Explorer"),
SharedString::from("Syncing"),
SharedString::from("Blame"),
],
card_tab_selected: 0,
sidebar_selected: None,
tree_nodes: vec![
GalleryNode {