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 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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue