From e443a219bed92778b9577e2ba7bc8f2e83d506dd Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 27 Feb 2025 14:52:28 +0800 Subject: [PATCH] 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`. image --- crates/story/examples/tiles.rs | 142 ++++++++++++++++++++++++++++++-- crates/ui/src/dock/mod.rs | 8 +- crates/ui/src/dock/panel.rs | 64 ++++++++++++-- crates/ui/src/dock/state.rs | 26 ++---- crates/ui/src/dock/tab_panel.rs | 5 ++ crates/ui/src/styled.rs | 4 + 6 files changed, 218 insertions(+), 31 deletions(-) diff --git a/crates/story/examples/tiles.rs b/crates/story/examples/tiles.rs index ece92804..657fafab 100644 --- a/crates/story/examples/tiles.rs +++ b/crates/story/examples/tiles.rs @@ -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, + search_input: Entity, +} + +#[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 { + 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, window: &mut Window, cx: &mut App) -> Entity { + 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 { + 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 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) -> 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::(window, cx), + ContainerPanel::new( + Arc::new(StoryContainer::panel::(window, cx)), + window, + cx, + ), dock_area, window, cx, ), DockItem::tab( - StoryContainer::panel::(window, cx), + ContainerPanel::new( + Arc::new(StoryContainer::panel::(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); diff --git a/crates/ui/src/dock/mod.rs b/crates/ui/src/dock/mod.rs index aadd69de..f7bb5e32 100644 --- a/crates/ui/src/dock/mod.rs +++ b/crates/ui/src/dock/mod.rs @@ -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 } diff --git a/crates/ui/src/dock/panel.rs b/crates/ui/src/dock/panel.rs index 0586a1c5..27195544 100644 --- a/crates/ui/src/dock/panel.rs +++ b/crates/ui/src/dock/panel.rs @@ -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 + 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 { + 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; fn title_style(&self, cx: &App) -> Option; fn closable(&self, cx: &App) -> bool; fn zoomable(&self, cx: &App) -> Option; @@ -158,6 +166,10 @@ impl PanelView for Entity { self.read(cx).title(window, cx) } + fn title_suffix(&self, window: &mut Window, cx: &mut App) -> Option { + self.update(cx, |this, cx| this.title_suffix(window, cx)) + } + fn title_style(&self, cx: &App) -> Option { 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::() { + cx.set_global(PanelRegistry::new()); + } + } + pub fn new() -> Self { Self { items: HashMap::new(), } } + + pub fn global(cx: &App) -> &Self { + cx.global::() + } + + pub fn global_mut(cx: &mut App) -> &mut Self { + cx.global_mut::() + } + + /// Build a panel by name. + /// + /// If not registered, return InvalidPanel. + pub fn build_panel( + panel_name: &str, + dock_area: WeakEntity, + panel_state: &PanelState, + panel_info: &PanelInfo, + window: &mut Window, + cx: &mut App, + ) -> Box { + 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 + 'static, { - if let None = cx.try_global::() { - cx.set_global(PanelRegistry::new()); - } - - cx.global_mut::() + PanelRegistry::init(cx); + PanelRegistry::global_mut(cx) .items .insert(panel_name.to_string(), Arc::new(deserialize)); } diff --git a/crates/ui/src/dock/state.rs b/crates/ui/src/dock/state.rs index a4b96dea..e8259160 100644 --- a/crates/ui/src/dock/state.rs +++ b/crates/ui/src/dock/state.rs @@ -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::() - .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), diff --git a/crates/ui/src/dock/tab_panel.rs b/crates/ui/src/dock/tab_panel.rs index e356848d..79c9bfe5 100644 --- a/crates/ui/src/dock/tab_panel.rs +++ b/crates/ui/src/dock/tab_panel.rs @@ -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)), ) diff --git a/crates/ui/src/styled.rs b/crates/ui/src/styled.rs index a78053a0..ee85f074 100644 --- a/crates/ui/src/styled.rs +++ b/crates/ui/src/styled.rs @@ -322,6 +322,8 @@ impl StyleSized 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 StyleSized 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)