dock: Add visible method to Panel trait to control panel visible. (#503)

Closes #502 #499 

## Added

- Add `visible` method to `Panel` trait, if return false this panel will
disappear.

## Break changes

- Renamed `closeable` to `closable`.
- The `title_style`, `closable`, `zoomable` in `Panel` trait have been
change `cx: &WindowContext` to `cx: &AppContext`.
This commit is contained in:
Jason Lee 2024-12-20 10:21:21 +08:00 committed by GitHub
parent 4b211bfe21
commit 1a4ee348f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 256 additions and 87 deletions

View file

@ -50,7 +50,7 @@ impl super::Story for ButtonStory {
"Displays a button or a component that looks like a button."
}
fn closeable() -> bool {
fn closable() -> bool {
false
}

View file

@ -55,7 +55,7 @@ impl super::Story for InputStory {
"Input"
}
fn closeable() -> bool {
fn closable() -> bool {
false
}

View file

@ -43,9 +43,10 @@ pub use tooltip_story::TooltipStory;
pub use webview_story::WebViewStory;
use gpui::{
actions, div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Div,
EventEmitter, FocusableView, Hsla, InteractiveElement, IntoElement, ParentElement, Render,
SharedString, Styled as _, View, ViewContext, VisualContext, WindowContext,
actions, div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Context as _,
Div, EventEmitter, FocusableView, Global, Hsla, InteractiveElement, IntoElement, Model,
ParentElement, Render, SharedString, Styled as _, View, ViewContext, VisualContext,
WindowContext,
};
use ui::{
@ -62,7 +63,30 @@ use ui::{
const PANEL_NAME: &str = "StoryContainer";
pub struct AppState {
pub invisible_panels: Model<Vec<SharedString>>,
}
impl AppState {
fn init(cx: &mut AppContext) {
let state = Self {
invisible_panels: cx.new_model(|_| Vec::new()),
};
cx.set_global::<AppState>(state);
}
pub fn global(cx: &AppContext) -> &Self {
cx.global::<Self>()
}
pub fn global_mut(cx: &mut AppContext) -> &mut Self {
cx.global_mut::<Self>()
}
}
impl Global for AppState {}
pub fn init(cx: &mut AppContext) {
AppState::init(cx);
input_story::init(cx);
dropdown_story::init(cx);
popup_story::init(cx);
@ -76,7 +100,7 @@ pub fn init(cx: &mut AppContext) {
};
let view = cx.new_view(|cx| {
let (title, description, closeable, zoomable, story) = story_state.to_story(cx);
let (title, description, closable, zoomable, story) = story_state.to_story(cx);
let mut container = StoryContainer::new(cx).story(story, story_state.story_klass);
cx.on_focus_in(&container.focus_handle, |this: &mut StoryContainer, _| {
@ -86,7 +110,7 @@ pub fn init(cx: &mut AppContext) {
container.name = title.into();
container.description = description.into();
container.closeable = closeable;
container.closable = closable;
container.zoomable = zoomable;
container
});
@ -122,7 +146,7 @@ pub struct StoryContainer {
height: Option<gpui::Pixels>,
story: Option<AnyView>,
story_klass: Option<SharedString>,
closeable: bool,
closable: bool,
zoomable: bool,
}
@ -140,7 +164,7 @@ pub trait Story: FocusableView {
fn description() -> &'static str {
""
}
fn closeable() -> bool {
fn closable() -> bool {
true
}
fn zoomable() -> bool {
@ -167,7 +191,7 @@ impl StoryContainer {
height: None,
story: None,
story_klass: None,
closeable: true,
closable: true,
zoomable: true,
}
}
@ -182,7 +206,7 @@ impl StoryContainer {
let view = cx.new_view(|cx| {
let mut story = Self::new(cx).story(story.into(), story_klass);
story.focus_handle = focus_handle;
story.closeable = S::closeable();
story.closable = S::closable();
story.zoomable = S::zoomable();
story.name = name.into();
story.description = description.into();
@ -242,7 +266,7 @@ impl StoryState {
(
$klass::title(),
$klass::description(),
$klass::closeable(),
$klass::closable(),
$klass::zoomable(),
$klass::view(cx).into(),
)
@ -285,7 +309,7 @@ impl Panel for StoryContainer {
self.name.clone().into_any_element()
}
fn title_style(&self, cx: &WindowContext) -> Option<TitleStyle> {
fn title_style(&self, cx: &AppContext) -> Option<TitleStyle> {
if let Some(bg) = self.title_bg {
Some(TitleStyle {
background: bg,
@ -296,14 +320,21 @@ impl Panel for StoryContainer {
}
}
fn closeable(&self, _cx: &WindowContext) -> bool {
self.closeable
fn closable(&self, _cx: &AppContext) -> bool {
self.closable
}
fn zoomable(&self, _cx: &WindowContext) -> bool {
fn zoomable(&self, _cx: &AppContext) -> bool {
self.zoomable
}
fn visible(&self, cx: &AppContext) -> bool {
!AppState::global(cx)
.invisible_panels
.read(cx)
.contains(&self.name)
}
fn set_zoomed(&self, zoomed: bool, _cx: &ViewContext<Self>) {
println!("panel: {} zoomed: {}", self.name, zoomed);
}

View file

@ -4,9 +4,10 @@ use prelude::FluentBuilder as _;
use serde::Deserialize;
use std::{sync::Arc, time::Duration};
use story::{
AccordionStory, Assets, ButtonStory, CalendarStory, DropdownStory, IconStory, ImageStory,
InputStory, ListStory, ModalStory, PopupStory, ProgressStory, ResizableStory, ScrollableStory,
SidebarStory, StoryContainer, SwitchStory, TableStory, TextStory, TooltipStory,
AccordionStory, AppState, Assets, ButtonStory, CalendarStory, DropdownStory, IconStory,
ImageStory, InputStory, ListStory, ModalStory, PopupStory, ProgressStory, ResizableStory,
ScrollableStory, SidebarStory, StoryContainer, SwitchStory, TableStory, TextStory,
TooltipStory,
};
use ui::{
button::{Button, ButtonVariants as _},
@ -36,9 +37,18 @@ struct SelectFont(usize);
#[derive(Clone, PartialEq, Eq, Deserialize)]
struct AddPanel(DockPlacement);
#[derive(Clone, PartialEq, Eq, Deserialize)]
struct TogglePanelVisible(SharedString);
impl_actions!(
story,
[SelectLocale, SelectFont, AddPanel, SelectScrollbarShow]
[
SelectLocale,
SelectFont,
AddPanel,
SelectScrollbarShow,
TogglePanelVisible
]
);
actions!(main_menu, [Quit]);
@ -395,6 +405,24 @@ impl StoryWorkspace {
dock_area.add_panel(panel, action.0, cx);
});
}
fn on_action_toggle_panel_visible(
&mut self,
action: &TogglePanelVisible,
cx: &mut ViewContext<Self>,
) {
let panel_name = action.0.clone();
let invisible_panels = AppState::global(cx).invisible_panels.clone();
invisible_panels.update(cx, |names, cx| {
if names.contains(&panel_name) {
names.retain(|id| id != &panel_name);
} else {
names.push(panel_name);
}
cx.notify();
});
cx.notify();
}
}
pub fn open_new(
@ -417,10 +445,12 @@ impl Render for StoryWorkspace {
let modal_layer = Root::render_modal_layer(cx);
let notification_layer = Root::render_notification_layer(cx);
let notifications_count = cx.notifications().len();
let invisible_panels = AppState::global(cx).invisible_panels.clone();
div()
.id("story-workspace")
.on_action(cx.listener(Self::on_action_add_panel))
.on_action(cx.listener(Self::on_action_toggle_panel_visible))
.relative()
.size_full()
.flex()
@ -442,7 +472,7 @@ impl Render for StoryWorkspace {
.icon(IconName::LayoutDashboard)
.small()
.ghost()
.popup_menu(|menu, _| {
.popup_menu(move |menu, cx| {
menu.menu(
"Add Panel to Center",
Box::new(AddPanel(DockPlacement::Center)),
@ -460,6 +490,43 @@ impl Render for StoryWorkspace {
"Add Panel to Bottom",
Box::new(AddPanel(DockPlacement::Bottom)),
)
.separator()
.menu_with_check(
"Sidebar",
!invisible_panels
.read(cx)
.contains(&SharedString::from("Sidebar")),
Box::new(TogglePanelVisible(SharedString::from(
"Sidebar",
))),
)
.menu_with_check(
"Modal",
!invisible_panels
.read(cx)
.contains(&SharedString::from("SidebModalar")),
Box::new(TogglePanelVisible(SharedString::from(
"Modal",
))),
)
.menu_with_check(
"Accordion",
!invisible_panels
.read(cx)
.contains(&SharedString::from("Accordion")),
Box::new(TogglePanelVisible(SharedString::from(
"Accordion",
))),
)
.menu_with_check(
"List",
!invisible_panels
.read(cx)
.contains(&SharedString::from("List")),
Box::new(TogglePanelVisible(SharedString::from(
"List",
))),
)
})
.anchor(Corner::TopRight),
)

View file

@ -505,7 +505,7 @@ impl super::Story for TableStory {
Self::view(cx)
}
fn closeable() -> bool {
fn closable() -> bool {
false
}
}

View file

@ -81,7 +81,7 @@ impl Dock {
) -> Self {
let panel = cx.new_view(|cx| {
let mut tab = TabPanel::new(None, dock_area.clone(), cx);
tab.closeable = false;
tab.closable = false;
tab
});

View file

@ -45,17 +45,28 @@ pub trait Panel: EventEmitter<PanelEvent> + FocusableView {
}
/// The theme of the panel title, default is `None`.
fn title_style(&self, cx: &WindowContext) -> Option<TitleStyle> {
fn title_style(&self, cx: &AppContext) -> Option<TitleStyle> {
None
}
/// Whether the panel can be closed, default is `true`.
fn closeable(&self, cx: &WindowContext) -> bool {
///
/// This method called in Panel render, we should make sure it is fast.
fn closable(&self, cx: &AppContext) -> bool {
true
}
/// Return true if the panel is zoomable, default is `false`.
fn zoomable(&self, cx: &WindowContext) -> bool {
///
/// This method called in Panel render, we should make sure it is fast.
fn zoomable(&self, cx: &AppContext) -> bool {
true
}
/// Return false to hide panel, true to show panel, default is `true`.
///
/// This method called in Panel render, we should make sure it is fast.
fn visible(&self, cx: &AppContext) -> bool {
true
}
@ -95,9 +106,10 @@ pub trait Panel: EventEmitter<PanelEvent> + FocusableView {
pub trait PanelView: 'static + Send + Sync {
fn panel_name(&self, cx: &AppContext) -> &'static str;
fn title(&self, cx: &WindowContext) -> AnyElement;
fn title_style(&self, cx: &WindowContext) -> Option<TitleStyle>;
fn closeable(&self, cx: &WindowContext) -> bool;
fn zoomable(&self, cx: &WindowContext) -> bool;
fn title_style(&self, cx: &AppContext) -> Option<TitleStyle>;
fn closable(&self, cx: &AppContext) -> bool;
fn zoomable(&self, cx: &AppContext) -> bool;
fn visible(&self, cx: &AppContext) -> bool;
fn set_active(&self, active: bool, cx: &mut WindowContext);
fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext);
fn popup_menu(&self, menu: PopupMenu, cx: &WindowContext) -> PopupMenu;
@ -116,18 +128,22 @@ impl<T: Panel> PanelView for View<T> {
self.read(cx).title(cx)
}
fn title_style(&self, cx: &WindowContext) -> Option<TitleStyle> {
fn title_style(&self, cx: &AppContext) -> Option<TitleStyle> {
self.read(cx).title_style(cx)
}
fn closeable(&self, cx: &WindowContext) -> bool {
self.read(cx).closeable(cx)
fn closable(&self, cx: &AppContext) -> bool {
self.read(cx).closable(cx)
}
fn zoomable(&self, cx: &WindowContext) -> bool {
fn zoomable(&self, cx: &AppContext) -> bool {
self.read(cx).zoomable(cx)
}
fn visible(&self, cx: &AppContext) -> bool {
self.read(cx).visible(cx)
}
fn set_active(&self, active: bool, cx: &mut WindowContext) {
self.update(cx, |this, cx| {
this.set_active(active, cx);

View file

@ -172,6 +172,7 @@ impl StackPanel {
fn new_resizable_panel(panel: Arc<dyn PanelView>, size: Option<Pixels>) -> ResizablePanel {
resizable_panel()
.content_view(panel.view())
.content_visible(move |cx| panel.visible(cx))
.when_some(size, |this, size| this.size(size))
}

View file

@ -26,7 +26,7 @@ use super::{
#[derive(Clone, Copy)]
struct TabState {
closeable: bool,
closable: bool,
zoomable: bool,
draggable: bool,
droppable: bool,
@ -71,9 +71,9 @@ pub struct TabPanel {
stack_panel: Option<WeakView<StackPanel>>,
pub(crate) panels: Vec<Arc<dyn PanelView>>,
pub(crate) active_ix: usize,
/// If this is true, the Panel closeable will follow the active panel's closeable,
/// If this is true, the Panel closable will follow the active panel's closable,
/// otherwise this TabPanel will not able to close
pub(crate) closeable: bool,
pub(crate) closable: bool,
tab_bar_scroll_handle: ScrollHandle,
is_zoomed: bool,
@ -88,29 +88,33 @@ impl Panel for TabPanel {
}
fn title(&self, cx: &WindowContext) -> gpui::AnyElement {
self.active_panel()
self.active_panel(cx)
.map(|panel| panel.title(cx))
.unwrap_or("Empty Tab".into_any_element())
}
fn closeable(&self, cx: &WindowContext) -> bool {
if !self.closeable {
fn closable(&self, cx: &AppContext) -> bool {
if !self.closable {
return false;
}
self.active_panel()
.map(|panel| panel.closeable(cx))
self.active_panel(cx)
.map(|panel| panel.closable(cx))
.unwrap_or(false)
}
fn zoomable(&self, cx: &WindowContext) -> bool {
self.active_panel()
fn zoomable(&self, cx: &AppContext) -> bool {
self.active_panel(cx)
.map(|panel| panel.zoomable(cx))
.unwrap_or(false)
}
fn visible(&self, cx: &AppContext) -> bool {
self.visible_panels(cx).next().is_some()
}
fn popup_menu(&self, menu: PopupMenu, cx: &WindowContext) -> PopupMenu {
if let Some(panel) = self.active_panel() {
if let Some(panel) = self.active_panel(cx) {
panel.popup_menu(menu, cx)
} else {
menu
@ -118,7 +122,7 @@ impl Panel for TabPanel {
}
fn toolbar_buttons(&self, cx: &WindowContext) -> Vec<Button> {
if let Some(panel) = self.active_panel() {
if let Some(panel) = self.active_panel(cx) {
panel.toolbar_buttons(cx)
} else {
vec![]
@ -151,7 +155,7 @@ impl TabPanel {
will_split_placement: None,
is_zoomed: false,
is_collapsed: false,
closeable: true,
closable: true,
}
}
@ -160,8 +164,19 @@ impl TabPanel {
}
/// Return current active_panel View
pub fn active_panel(&self) -> Option<Arc<dyn PanelView>> {
self.panels.get(self.active_ix).cloned()
pub fn active_panel(&self, cx: &AppContext) -> Option<Arc<dyn PanelView>> {
let panel = self.panels.get(self.active_ix);
if let Some(panel) = panel {
if panel.visible(cx) {
Some(panel.clone())
} else {
// Return the first visible panel
self.visible_panels(cx).next()
}
} else {
None
}
}
fn set_active_ix(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
@ -335,6 +350,20 @@ impl TabPanel {
self.panels.len() <= 1
}
/// Return all visible panels
fn visible_panels<'a>(
&'a self,
cx: &'a AppContext,
) -> impl Iterator<Item = Arc<dyn PanelView>> + 'a {
self.panels.iter().filter_map(|panel| {
if panel.visible(cx) {
Some(panel.clone())
} else {
None
}
})
}
/// Return true if the tab panel is draggable.
///
/// E.g. if the parent and self only have one panel, it is not draggable.
@ -392,7 +421,7 @@ impl TabPanel {
};
this.separator().menu(name, Box::new(ToggleZoom))
})
.when(state.closeable, |this| {
.when(state.closable, |this| {
this.separator()
.menu(t!("Dock.Close"), Box::new(ClosePanel))
})
@ -496,6 +525,11 @@ impl TabPanel {
if self.panels.len() == 1 && panel_style == PanelStyle::Default {
let panel = self.panels.get(0).unwrap();
if !panel.visible(cx) {
return div().into_any_element();
}
let title_style = panel.title_style(cx);
return h_flex()
@ -580,47 +614,53 @@ impl TabPanel {
)
},
)
.children(self.panels.iter().enumerate().map(|(ix, panel)| {
.children(self.panels.iter().enumerate().filter_map(|(ix, panel)| {
let mut active = ix == self.active_ix;
let disabled = self.is_collapsed;
if !panel.visible(cx) {
return None;
}
// Always not show active tab style, if the panel is collapsed
if self.is_collapsed {
active = false;
}
Tab::new(("tab", ix), panel.title(cx))
.py_2()
.selected(active)
.disabled(disabled)
.when(!disabled, |this| {
this.on_click(cx.listener(move |view, _, cx| {
view.set_active_ix(ix, cx);
}))
.when(state.draggable, |this| {
this.on_drag(
DragPanel::new(panel.clone(), view.clone()),
|drag, _, cx| {
cx.stop_propagation();
cx.new_view(|_| drag.clone())
},
)
})
.when(state.droppable, |this| {
this.drag_over::<DragPanel>(|this, _, cx| {
this.rounded_l_none()
.border_l_2()
.border_r_0()
.border_color(cx.theme().drag_border)
Some(
Tab::new(("tab", ix), panel.title(cx))
.py_2()
.selected(active)
.disabled(disabled)
.when(!disabled, |this| {
this.on_click(cx.listener(move |view, _, cx| {
view.set_active_ix(ix, cx);
}))
.when(state.draggable, |this| {
this.on_drag(
DragPanel::new(panel.clone(), view.clone()),
|drag, _, cx| {
cx.stop_propagation();
cx.new_view(|_| drag.clone())
},
)
})
.on_drop(cx.listener(
move |this, drag: &DragPanel, cx| {
this.will_split_placement = None;
this.on_drop(drag, Some(ix), true, cx)
},
))
})
})
.when(state.droppable, |this| {
this.drag_over::<DragPanel>(|this, _, cx| {
this.rounded_l_none()
.border_l_2()
.border_r_0()
.border_color(cx.theme().drag_border)
})
.on_drop(cx.listener(
move |this, drag: &DragPanel, cx| {
this.will_split_placement = None;
this.on_drop(drag, Some(ix), true, cx)
},
))
})
}),
)
}))
.child(
// empty space to allow move to last tab right
@ -667,7 +707,7 @@ impl TabPanel {
return Empty {}.into_any_element();
}
self.active_panel()
self.active_panel(cx)
.map(|panel| {
div()
.id("tab-content")
@ -885,7 +925,7 @@ impl TabPanel {
}
fn focus_active_panel(&self, cx: &mut ViewContext<Self>) {
if let Some(active_panel) = self.active_panel() {
if let Some(active_panel) = self.active_panel(cx) {
active_panel.focus_handle(cx).focus(cx);
}
}
@ -916,7 +956,7 @@ impl TabPanel {
}
fn on_action_close_panel(&mut self, _: &ClosePanel, cx: &mut ViewContext<Self>) {
if let Some(panel) = self.active_panel() {
if let Some(panel) = self.active_panel(cx) {
self.remove_panel(panel, cx);
}
}
@ -924,7 +964,7 @@ impl TabPanel {
impl FocusableView for TabPanel {
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
if let Some(active_panel) = self.active_panel() {
if let Some(active_panel) = self.active_panel(cx) {
active_panel.focus_handle(cx)
} else {
self.focus_handle.clone()
@ -937,13 +977,13 @@ impl Render for TabPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl gpui::IntoElement {
let focus_handle = self.focus_handle(cx);
let mut state = TabState {
closeable: self.closeable(cx),
closable: self.closable(cx),
draggable: self.draggable(cx),
droppable: self.droppable(cx),
zoomable: self.zoomable(cx),
};
if !state.draggable {
state.closeable = false;
state.closable = false;
}
v_flex()

View file

@ -285,6 +285,7 @@ pub struct ResizablePanel {
axis: Axis,
content_builder: Option<Rc<dyn Fn(&mut WindowContext) -> AnyElement>>,
content_view: Option<AnyView>,
content_visible: Rc<Box<dyn Fn(&WindowContext) -> bool>>,
/// The bounds of the resizable panel, when render the bounds will be updated.
bounds: Bounds<Pixels>,
resize_handle: Option<AnyElement>,
@ -300,6 +301,7 @@ impl ResizablePanel {
axis: Axis::Horizontal,
content_builder: None,
content_view: None,
content_visible: Rc::new(Box::new(|_| true)),
bounds: Bounds::default(),
resize_handle: None,
}
@ -313,6 +315,14 @@ impl ResizablePanel {
self
}
pub(crate) fn content_visible<F>(mut self, content_visible: F) -> Self
where
F: Fn(&WindowContext) -> bool + 'static,
{
self.content_visible = Rc::new(Box::new(content_visible));
self
}
pub fn content_view(mut self, content: AnyView) -> Self {
self.content_view = Some(content);
self
@ -350,6 +360,10 @@ impl FluentBuilder for ResizablePanel {}
impl Render for ResizablePanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
if !(self.content_visible)(cx) {
return div();
}
let view = cx.view().clone();
let total_size = self
.group