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 anyhow::{Context as _, Result};
use gpui::*; use gpui::*;
use gpui_component::{ use gpui_component::{
dock::{DockArea, DockAreaState, DockEvent, DockItem}, dock::{
ActiveTheme, Root, TitleBar, 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}; use story::{Assets, ButtonStory, IconStory, StoryContainer};
actions!(main_menu, [Quit]); actions!(main_menu, [Quit]);
@ -14,6 +19,124 @@ const TILES_DOCK_AREA: DockAreaTab = DockAreaTab {
version: 1, 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]); actions!(workspace, [Open, CloseWindow]);
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
@ -182,13 +305,21 @@ impl StoryTiles {
DockItem::tiles( DockItem::tiles(
vec![ vec![
DockItem::tab( DockItem::tab(
StoryContainer::panel::<ButtonStory>(window, cx), ContainerPanel::new(
Arc::new(StoryContainer::panel::<ButtonStory>(window, cx)),
window,
cx,
),
dock_area, dock_area,
window, window,
cx, cx,
), ),
DockItem::tab( DockItem::tab(
StoryContainer::panel::<IconStory>(window, cx), ContainerPanel::new(
Arc::new(StoryContainer::panel::<IconStory>(window, cx)),
window,
cx,
),
dock_area, dock_area,
window, window,
cx, cx,
@ -292,6 +423,7 @@ fn main() {
app.run(move |cx| { app.run(move |cx| {
gpui_component::init(cx); gpui_component::init(cx);
story::init(cx); story::init(cx);
ContainerPanel::init(cx);
cx.on_action(quit); cx.on_action(quit);

View file

@ -22,7 +22,7 @@ pub use tab_panel::*;
pub use tiles::*; pub use tiles::*;
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
cx.set_global(PanelRegistry::new()); PanelRegistry::init(cx);
} }
actions!(dock, [ToggleZoom, ClosePanel]); actions!(dock, [ToggleZoom, ClosePanel]);
@ -206,6 +206,12 @@ impl DockItem {
TileItem::new(Arc::new(view), meta.bounds).z_index(meta.z_index); TileItem::new(Arc::new(view), meta.bounds).z_index(meta.z_index);
tiles.add_item(tile_item, dock_area, window, cx); 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 // Ignore non-tabs items
} }

View file

@ -2,13 +2,13 @@ use std::{collections::HashMap, sync::Arc};
use crate::{button::Button, popup_menu::PopupMenu}; use crate::{button::Button, popup_menu::PopupMenu};
use gpui::{ use gpui::{
AnyElement, AnyView, App, Entity, EntityId, EventEmitter, FocusHandle, Focusable, Global, Hsla, AnyElement, AnyView, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle,
IntoElement, Render, SharedString, WeakEntity, Window, Focusable, Global, Hsla, IntoElement, Render, SharedString, WeakEntity, Window,
}; };
use rust_i18n::t; use rust_i18n::t;
use super::{DockArea, PanelInfo, PanelState}; use super::{invalid_panel::InvalidPanel, DockArea, PanelInfo, PanelState};
pub enum PanelEvent { pub enum PanelEvent {
ZoomIn, ZoomIn,
@ -69,6 +69,13 @@ pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
None 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`. /// Whether the panel can be closed, default is `true`.
/// ///
/// This method called in Panel render, we should make sure it is fast. /// 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_name(&self, cx: &App) -> &'static str;
fn panel_id(&self, cx: &App) -> EntityId; fn panel_id(&self, cx: &App) -> EntityId;
fn title(&self, window: &Window, cx: &App) -> AnyElement; 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 title_style(&self, cx: &App) -> Option<TitleStyle>;
fn closable(&self, cx: &App) -> bool; fn closable(&self, cx: &App) -> bool;
fn zoomable(&self, cx: &App) -> Option<PanelControl>; fn zoomable(&self, cx: &App) -> Option<PanelControl>;
@ -158,6 +166,10 @@ impl<T: Panel> PanelView for Entity<T> {
self.read(cx).title(window, cx) 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> { fn title_style(&self, cx: &App) -> Option<TitleStyle> {
self.read(cx).title_style(cx) self.read(cx).title_style(cx)
} }
@ -244,11 +256,50 @@ pub struct PanelRegistry {
>, >,
} }
impl 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 { pub fn new() -> Self {
Self { Self {
items: HashMap::new(), 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 {} impl Global for PanelRegistry {}
@ -264,11 +315,8 @@ where
) -> Box<dyn PanelView> ) -> Box<dyn PanelView>
+ 'static, + 'static,
{ {
if let None = cx.try_global::<PanelRegistry>() { PanelRegistry::init(cx);
cx.set_global(PanelRegistry::new()); PanelRegistry::global_mut(cx)
}
cx.global_mut::<PanelRegistry>()
.items .items
.insert(panel_name.to_string(), Arc::new(deserialize)); .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 itertools::Itertools as _;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{ use super::{Dock, DockArea, DockItem, DockPlacement, Panel, PanelRegistry};
invalid_panel::InvalidPanel, Dock, DockArea, DockItem, DockPlacement, Panel, PanelRegistry,
};
/// Used to serialize and deserialize the DockArea /// Used to serialize and deserialize the DockArea
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
@ -224,20 +222,14 @@ impl PanelState {
DockItem::tabs(items, Some(active_index), &dock_area, window, cx) DockItem::tabs(items, Some(active_index), &dock_area, window, cx)
} }
PanelInfo::Panel(_) => { PanelInfo::Panel(_) => {
let view = if let Some(f) = cx let view = PanelRegistry::build_panel(
.global::<PanelRegistry>() &self.panel_name,
.items dock_area.clone(),
.get(&self.panel_name) self,
.cloned() &info,
{ window,
f(dock_area.clone(), self, &info, window, cx) 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)),
)
};
DockItem::tabs(vec![view.into()], None, &dock_area, window, cx) DockItem::tabs(vec![view.into()], None, &dock_area, window, cx)
} }
PanelInfo::Tiles { metas } => DockItem::tiles(items, metas, &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( .child(
h_flex() h_flex()
.flex_shrink_0() .flex_shrink_0()
@ -747,6 +748,10 @@ impl TabPanel {
.bg(cx.theme().tab_bar) .bg(cx.theme().tab_bar)
.px_2() .px_2()
.gap_1() .gap_1()
.children(
self.active_panel(cx)
.and_then(|panel| panel.title_suffix(window, cx)),
)
.child(self.render_toolbar(state, window, cx)) .child(self.render_toolbar(state, window, cx))
.when_some(right_dock_button, |this, btn| this.child(btn)), .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 { match size {
Size::Large => self.py_5(), Size::Large => self.py_5(),
Size::Medium => self.py_2(), Size::Medium => self.py_2(),
Size::Small => self.py_1(),
Size::XSmall => self.py_0(),
_ => self.py_1(), _ => self.py_1(),
} }
} }
@ -331,6 +333,8 @@ impl<T: Styled> StyleSized<T> for T {
match size { match size {
Size::Large => self.h_11(), Size::Large => self.h_11(),
Size::Medium => self.h_8(), Size::Medium => self.h_8(),
Size::Small => self.h(px(26.)),
Size::XSmall => self.h(px(20.)),
_ => self.h(px(26.)), _ => self.h(px(26.)),
} }
.input_text_size(size) .input_text_size(size)