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:
parent
e61f6204cc
commit
e443a219be
6 changed files with 218 additions and 31 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue