panel: Add title_suffix to panel and write a custom container panel example. (#669)

- Add to support `xsmall` size for TextInput.
- Add `global`, `global_mut`, `build_panel` static method to
`PanelRegistry`.

<img width="1011" alt="image"
src="https://github.com/user-attachments/assets/1a6e61c4-502a-4d1c-a4ea-2644777fd4d8"
/>
This commit is contained in:
Jason Lee 2025-02-27 14:52:28 +08:00 committed by GitHub
parent e61f6204cc
commit e443a219be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 218 additions and 31 deletions

View file

@ -1,10 +1,15 @@
use anyhow::{Context as _, Result};
use gpui::*;
use gpui_component::{
dock::{DockArea, DockAreaState, DockEvent, DockItem},
ActiveTheme, Root, TitleBar,
dock::{
register_panel, DockArea, DockAreaState, DockEvent, DockItem, Panel, PanelEvent, PanelInfo,
PanelRegistry, PanelState, PanelView,
},
input::TextInput,
ActiveTheme, Root, Sizable, TitleBar,
};
use std::time::Duration;
use serde::{Deserialize, Serialize};
use std::{sync::Arc, time::Duration};
use story::{Assets, ButtonStory, IconStory, StoryContainer};
actions!(main_menu, [Quit]);
@ -14,6 +19,124 @@ const TILES_DOCK_AREA: DockAreaTab = DockAreaTab {
version: 1,
};
/// A specification for a container panel for wrapping other panels to add some common functionality.
///
/// For example:
///
/// - Add a search bar to all panels.
struct ContainerPanel {
panel: Arc<dyn PanelView>,
search_input: Entity<TextInput>,
}
#[derive(Clone, Serialize, Deserialize)]
struct ContainerPanelState {
/// The state of the child panel.
child: PanelState,
}
impl ContainerPanelState {
fn new(child: PanelState) -> Self {
Self { child }
}
fn to_value(&self) -> serde_json::Value {
serde_json::to_value(self).unwrap()
}
fn from_value(value: serde_json::Value) -> Result<Self> {
serde_json::from_value(value).context("failed to deserialize ContainerPanelState")
}
}
impl ContainerPanel {
fn init(cx: &mut App) {
register_panel(
cx,
"ContainerPanel",
|dock_area, _, info, window, cx| match info {
PanelInfo::Panel(panel_info) => {
let container_state =
ContainerPanelState::from_value(panel_info.clone()).unwrap();
let child_state = container_state.child;
let view = PanelRegistry::build_panel(
&child_state.panel_name,
dock_area,
&child_state,
&child_state.info,
window,
cx,
);
Box::new(ContainerPanel::new(view.into(), window, cx))
}
_ => unreachable!(),
},
);
}
fn new(panel: Arc<dyn PanelView>, window: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| {
let search_input = cx.new(|cx| {
TextInput::new(window, cx)
.xsmall()
.appearance(false)
.placeholder("Search...")
});
Self {
panel,
search_input,
}
})
}
}
impl Panel for ContainerPanel {
fn panel_name(&self) -> &'static str {
"ContainerPanel"
}
fn title(&self, window: &Window, cx: &App) -> AnyElement {
self.panel.title(window, cx)
}
fn title_suffix(&self, _: &mut Window, cx: &mut App) -> Option<AnyElement> {
Some(
div()
.w_24()
.h_5()
.px_0p5()
.rounded_lg()
.border_1()
.border_color(cx.theme().input)
.child(self.search_input.clone())
.into_any_element(),
)
}
fn dump(&self, cx: &App) -> PanelState {
let mut state = PanelState::new(self);
let panel_state = self.panel.dump(cx);
let json_value = ContainerPanelState::new(panel_state).to_value();
state.info = PanelInfo::panel(json_value);
state
}
}
impl EventEmitter<PanelEvent> for ContainerPanel {}
impl Focusable for ContainerPanel {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.panel.focus_handle(cx)
}
}
impl Render for ContainerPanel {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
self.panel.view().clone()
}
}
actions!(workspace, [Open, CloseWindow]);
pub fn init(cx: &mut App) {
@ -182,13 +305,21 @@ impl StoryTiles {
DockItem::tiles(
vec![
DockItem::tab(
StoryContainer::panel::<ButtonStory>(window, cx),
ContainerPanel::new(
Arc::new(StoryContainer::panel::<ButtonStory>(window, cx)),
window,
cx,
),
dock_area,
window,
cx,
),
DockItem::tab(
StoryContainer::panel::<IconStory>(window, cx),
ContainerPanel::new(
Arc::new(StoryContainer::panel::<IconStory>(window, cx)),
window,
cx,
),
dock_area,
window,
cx,
@ -292,6 +423,7 @@ fn main() {
app.run(move |cx| {
gpui_component::init(cx);
story::init(cx);
ContainerPanel::init(cx);
cx.on_action(quit);

View file

@ -22,7 +22,7 @@ pub use tab_panel::*;
pub use tiles::*;
pub fn init(cx: &mut App) {
cx.set_global(PanelRegistry::new());
PanelRegistry::init(cx);
}
actions!(dock, [ToggleZoom, ClosePanel]);
@ -206,6 +206,12 @@ impl DockItem {
TileItem::new(Arc::new(view), meta.bounds).z_index(meta.z_index);
tiles.add_item(tile_item, dock_area, window, cx);
}
DockItem::Panel { view } => {
let meta: TileMeta = metas[ix].into();
let tile_item =
TileItem::new(view.clone(), meta.bounds).z_index(meta.z_index);
tiles.add_item(tile_item, dock_area, window, cx);
}
_ => {
// Ignore non-tabs items
}

View file

@ -2,13 +2,13 @@ use std::{collections::HashMap, sync::Arc};
use crate::{button::Button, popup_menu::PopupMenu};
use gpui::{
AnyElement, AnyView, App, Entity, EntityId, EventEmitter, FocusHandle, Focusable, Global, Hsla,
IntoElement, Render, SharedString, WeakEntity, Window,
AnyElement, AnyView, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle,
Focusable, Global, Hsla, IntoElement, Render, SharedString, WeakEntity, Window,
};
use rust_i18n::t;
use super::{DockArea, PanelInfo, PanelState};
use super::{invalid_panel::InvalidPanel, DockArea, PanelInfo, PanelState};
pub enum PanelEvent {
ZoomIn,
@ -69,6 +69,13 @@ pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
None
}
/// The suffix of the panel title, default is `None`.
///
/// This is used to add a suffix element to the panel title.
fn title_suffix(&self, window: &mut Window, cx: &mut App) -> Option<AnyElement> {
None
}
/// Whether the panel can be closed, default is `true`.
///
/// This method called in Panel render, we should make sure it is fast.
@ -131,6 +138,7 @@ pub trait PanelView: 'static + Send + Sync {
fn panel_name(&self, cx: &App) -> &'static str;
fn panel_id(&self, cx: &App) -> EntityId;
fn title(&self, window: &Window, cx: &App) -> AnyElement;
fn title_suffix(&self, window: &mut Window, cx: &mut App) -> Option<AnyElement>;
fn title_style(&self, cx: &App) -> Option<TitleStyle>;
fn closable(&self, cx: &App) -> bool;
fn zoomable(&self, cx: &App) -> Option<PanelControl>;
@ -158,6 +166,10 @@ impl<T: Panel> PanelView for Entity<T> {
self.read(cx).title(window, cx)
}
fn title_suffix(&self, window: &mut Window, cx: &mut App) -> Option<AnyElement> {
self.update(cx, |this, cx| this.title_suffix(window, cx))
}
fn title_style(&self, cx: &App) -> Option<TitleStyle> {
self.read(cx).title_style(cx)
}
@ -244,11 +256,50 @@ pub struct PanelRegistry {
>,
}
impl PanelRegistry {
/// Initialize the panel registry.
pub(crate) fn init(cx: &mut App) {
if let None = cx.try_global::<PanelRegistry>() {
cx.set_global(PanelRegistry::new());
}
}
pub fn new() -> Self {
Self {
items: HashMap::new(),
}
}
pub fn global(cx: &App) -> &Self {
cx.global::<PanelRegistry>()
}
pub fn global_mut(cx: &mut App) -> &mut Self {
cx.global_mut::<PanelRegistry>()
}
/// Build a panel by name.
///
/// If not registered, return InvalidPanel.
pub fn build_panel(
panel_name: &str,
dock_area: WeakEntity<DockArea>,
panel_state: &PanelState,
panel_info: &PanelInfo,
window: &mut Window,
cx: &mut App,
) -> Box<dyn PanelView> {
if let Some(view) = Self::global(cx)
.items
.get(panel_name)
.cloned()
.map(|f| f(dock_area, panel_state, panel_info, window, cx))
{
return view;
} else {
// Show an invalid panel if the panel is not registered.
Box::new(cx.new(|cx| InvalidPanel::new(&panel_name, panel_state.clone(), window, cx)))
}
}
}
impl Global for PanelRegistry {}
@ -264,11 +315,8 @@ where
) -> Box<dyn PanelView>
+ 'static,
{
if let None = cx.try_global::<PanelRegistry>() {
cx.set_global(PanelRegistry::new());
}
cx.global_mut::<PanelRegistry>()
PanelRegistry::init(cx);
PanelRegistry::global_mut(cx)
.items
.insert(panel_name.to_string(), Arc::new(deserialize));
}

View file

@ -2,9 +2,7 @@ use gpui::{point, px, size, App, AppContext, Axis, Bounds, Entity, Pixels, WeakE
use itertools::Itertools as _;
use serde::{Deserialize, Serialize};
use super::{
invalid_panel::InvalidPanel, Dock, DockArea, DockItem, DockPlacement, Panel, PanelRegistry,
};
use super::{Dock, DockArea, DockItem, DockPlacement, Panel, PanelRegistry};
/// Used to serialize and deserialize the DockArea
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
@ -224,20 +222,14 @@ impl PanelState {
DockItem::tabs(items, Some(active_index), &dock_area, window, cx)
}
PanelInfo::Panel(_) => {
let view = if let Some(f) = cx
.global::<PanelRegistry>()
.items
.get(&self.panel_name)
.cloned()
{
f(dock_area.clone(), self, &info, window, cx)
} else {
// Show an invalid panel if the panel is not registered.
Box::new(
cx.new(|cx| InvalidPanel::new(&self.panel_name, self.clone(), window, cx)),
)
};
let view = PanelRegistry::build_panel(
&self.panel_name,
dock_area.clone(),
self,
&info,
window,
cx,
);
DockItem::tabs(vec![view.into()], None, &dock_area, window, cx)
}
PanelInfo::Tiles { metas } => DockItem::tiles(items, metas, &dock_area, window, cx),

View file

@ -626,6 +626,7 @@ impl TabPanel {
)
}),
)
.children(panel.title_suffix(window, cx))
.child(
h_flex()
.flex_shrink_0()
@ -747,6 +748,10 @@ impl TabPanel {
.bg(cx.theme().tab_bar)
.px_2()
.gap_1()
.children(
self.active_panel(cx)
.and_then(|panel| panel.title_suffix(window, cx)),
)
.child(self.render_toolbar(state, window, cx))
.when_some(right_dock_button, |this, btn| this.child(btn)),
)

View file

@ -322,6 +322,8 @@ impl<T: Styled> StyleSized<T> for T {
match size {
Size::Large => self.py_5(),
Size::Medium => self.py_2(),
Size::Small => self.py_1(),
Size::XSmall => self.py_0(),
_ => self.py_1(),
}
}
@ -331,6 +333,8 @@ impl<T: Styled> StyleSized<T> for T {
match size {
Size::Large => self.h_11(),
Size::Medium => self.h_8(),
Size::Small => self.h(px(26.)),
Size::XSmall => self.h(px(20.)),
_ => self.h(px(26.)),
}
.input_text_size(size)