dock: Add PanelControl for pinning the toolbar button (#537)

<img width="387" alt="image"
src="https://github.com/user-attachments/assets/b3d3f2a8-dc4e-49e6-8e82-e7f551d70bc7"
/>
This commit is contained in:
Floyd Wang 2025-01-09 19:18:59 +08:00
parent de42cc46a7
commit ceba66c22d
7 changed files with 81 additions and 43 deletions

View file

@ -3,6 +3,7 @@ use gpui::{
}; };
use ui::{ use ui::{
button::{Button, ButtonVariant, ButtonVariants}, button::{Button, ButtonVariant, ButtonVariants},
dock::PanelControl,
h_flex, h_flex,
theme::ActiveTheme as _, theme::ActiveTheme as _,
v_flex, Icon, IconName, v_flex, Icon, IconName,
@ -33,8 +34,8 @@ impl super::Story for IconStory {
Self::view(cx) Self::view(cx)
} }
fn zoomable() -> bool { fn zoomable() -> Option<PanelControl> {
false None
} }
} }

View file

@ -1,5 +1,5 @@
use gpui::{px, ParentElement as _, Render, Styled, View, VisualContext as _, WindowContext}; use gpui::{px, ParentElement as _, Render, Styled, View, VisualContext as _, WindowContext};
use ui::{h_flex, v_flex, SvgImg}; use ui::{dock::PanelControl, h_flex, v_flex, SvgImg};
const GOOGLE_LOGO: &str = include_str!("./fixtures/google.svg"); const GOOGLE_LOGO: &str = include_str!("./fixtures/google.svg");
const PIE_JSON: &str = include_str!("./fixtures/pie.json"); const PIE_JSON: &str = include_str!("./fixtures/pie.json");
@ -19,6 +19,10 @@ impl super::Story for ImageStory {
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx) Self::view(cx)
} }
fn zoomable() -> Option<PanelControl> {
Some(PanelControl::Toolbar)
}
} }
impl ImageStory { impl ImageStory {

View file

@ -52,7 +52,7 @@ use gpui::{
use ui::{ use ui::{
button::Button, button::Button,
divider::Divider, divider::Divider,
dock::{register_panel, Panel, PanelEvent, PanelInfo, PanelState, TitleStyle}, dock::{register_panel, Panel, PanelControl, PanelEvent, PanelInfo, PanelState, TitleStyle},
h_flex, h_flex,
label::Label, label::Label,
notification::Notification, notification::Notification,
@ -147,7 +147,7 @@ pub struct StoryContainer {
story: Option<AnyView>, story: Option<AnyView>,
story_klass: Option<SharedString>, story_klass: Option<SharedString>,
closable: bool, closable: bool,
zoomable: bool, zoomable: Option<PanelControl>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -167,8 +167,8 @@ pub trait Story: FocusableView {
fn closable() -> bool { fn closable() -> bool {
true true
} }
fn zoomable() -> bool { fn zoomable() -> Option<PanelControl> {
true Some(PanelControl::default())
} }
fn title_bg() -> Option<Hsla> { fn title_bg() -> Option<Hsla> {
None None
@ -192,7 +192,7 @@ impl StoryContainer {
story: None, story: None,
story_klass: None, story_klass: None,
closable: true, closable: true,
zoomable: true, zoomable: Some(PanelControl::default()),
} }
} }
@ -260,7 +260,13 @@ impl StoryState {
fn to_story( fn to_story(
&self, &self,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> (&'static str, &'static str, bool, bool, AnyView) { ) -> (
&'static str,
&'static str,
bool,
Option<PanelControl>,
AnyView,
) {
macro_rules! story { macro_rules! story {
($klass:tt) => { ($klass:tt) => {
( (
@ -324,7 +330,7 @@ impl Panel for StoryContainer {
self.closable self.closable
} }
fn zoomable(&self, _cx: &AppContext) -> bool { fn zoomable(&self, _cx: &AppContext) -> Option<PanelControl> {
self.zoomable self.zoomable
} }

View file

@ -172,10 +172,6 @@ impl super::Story for SidebarStory {
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx) Self::view(cx)
} }
fn zoomable() -> bool {
true
}
} }
impl gpui::FocusableView for SidebarStory { impl gpui::FocusableView for SidebarStory {
fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle { fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle {

View file

@ -6,6 +6,7 @@ use gpui::{
use ui::{ use ui::{
button::{Button, ButtonVariant, ButtonVariants}, button::{Button, ButtonVariant, ButtonVariants},
checkbox::Checkbox, checkbox::Checkbox,
dock::PanelControl,
h_flex, h_flex,
label::Label, label::Label,
tooltip::Tooltip, tooltip::Tooltip,
@ -37,8 +38,8 @@ impl super::Story for TooltipStory {
Self::view(cx) Self::view(cx)
} }
fn zoomable() -> bool { fn zoomable() -> Option<PanelControl> {
false None
} }
} }
impl gpui::FocusableView for TooltipStory { impl gpui::FocusableView for TooltipStory {

View file

@ -30,6 +30,26 @@ pub struct TitleStyle {
pub foreground: Hsla, pub foreground: Hsla,
} }
#[derive(Clone, Copy, Default)]
pub enum PanelControl {
Both,
#[default]
Menu,
Toolbar,
}
impl PanelControl {
#[inline]
pub fn toolbar_visible(&self) -> bool {
matches!(self, PanelControl::Both | PanelControl::Toolbar)
}
#[inline]
pub fn menu_visible(&self) -> bool {
matches!(self, PanelControl::Both | PanelControl::Menu)
}
}
/// The Panel trait used to define the panel. /// The Panel trait used to define the panel.
#[allow(unused_variables)] #[allow(unused_variables)]
pub trait Panel: EventEmitter<PanelEvent> + FocusableView { pub trait Panel: EventEmitter<PanelEvent> + FocusableView {
@ -56,11 +76,11 @@ pub trait Panel: EventEmitter<PanelEvent> + FocusableView {
true true
} }
/// Return true if the panel is zoomable, default is `false`. /// Return `PanelControl` if the panel is zoomable, default is `None`.
/// ///
/// 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.
fn zoomable(&self, cx: &AppContext) -> bool { fn zoomable(&self, cx: &AppContext) -> Option<PanelControl> {
true Some(PanelControl::Menu)
} }
/// Return false to hide panel, true to show panel, default is `true`. /// Return false to hide panel, true to show panel, default is `true`.
@ -108,7 +128,7 @@ pub trait PanelView: 'static + Send + Sync {
fn title(&self, cx: &WindowContext) -> AnyElement; fn title(&self, cx: &WindowContext) -> AnyElement;
fn title_style(&self, cx: &AppContext) -> Option<TitleStyle>; fn title_style(&self, cx: &AppContext) -> Option<TitleStyle>;
fn closable(&self, cx: &AppContext) -> bool; fn closable(&self, cx: &AppContext) -> bool;
fn zoomable(&self, cx: &AppContext) -> bool; fn zoomable(&self, cx: &AppContext) -> Option<PanelControl>;
fn visible(&self, cx: &AppContext) -> bool; fn visible(&self, cx: &AppContext) -> bool;
fn set_active(&self, active: bool, cx: &mut WindowContext); fn set_active(&self, active: bool, cx: &mut WindowContext);
fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext); fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext);
@ -136,7 +156,7 @@ impl<T: Panel> PanelView for View<T> {
self.read(cx).closable(cx) self.read(cx).closable(cx)
} }
fn zoomable(&self, cx: &AppContext) -> bool { fn zoomable(&self, cx: &AppContext) -> Option<PanelControl> {
self.read(cx).zoomable(cx) self.read(cx).zoomable(cx)
} }

View file

@ -20,14 +20,14 @@ use crate::{
}; };
use super::{ use super::{
ClosePanel, DockArea, DockPlacement, Panel, PanelEvent, PanelState, PanelStyle, PanelView, ClosePanel, DockArea, DockPlacement, Panel, PanelControl, PanelEvent, PanelState, PanelStyle,
StackPanel, ToggleZoom, PanelView, StackPanel, ToggleZoom,
}; };
#[derive(Clone)] #[derive(Clone)]
struct TabState { struct TabState {
closable: bool, closable: bool,
zoomable: bool, zoomable: Option<PanelControl>,
draggable: bool, draggable: bool,
droppable: bool, droppable: bool,
active_panel: Option<Arc<dyn PanelView>>, active_panel: Option<Arc<dyn PanelView>>,
@ -104,10 +104,8 @@ impl Panel for TabPanel {
.unwrap_or(false) .unwrap_or(false)
} }
fn zoomable(&self, cx: &AppContext) -> bool { fn zoomable(&self, cx: &AppContext) -> Option<PanelControl> {
self.active_panel(cx) self.active_panel(cx).and_then(|panel| panel.zoomable(cx))
.map(|panel| panel.zoomable(cx))
.unwrap_or(false)
} }
fn visible(&self, cx: &AppContext) -> bool { fn visible(&self, cx: &AppContext) -> bool {
@ -377,12 +375,12 @@ impl TabPanel {
} }
fn render_toolbar(&self, state: &TabState, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_toolbar(&self, state: &TabState, cx: &mut ViewContext<Self>) -> impl IntoElement {
let is_zoomed = self.is_zoomed && state.zoomable; let is_zoomed = self.is_zoomed;
let view = cx.view().clone(); let view = cx.view().clone();
let build_popup_menu = move |this, cx: &WindowContext| view.read(cx).popup_menu(this, cx); let build_popup_menu = move |this, cx: &WindowContext| view.read(cx).popup_menu(this, cx);
let zoomable_toolbar_visible = state.zoomable.map_or(false, |v| v.toolbar_visible());
// TODO: Do not show MenuButton if there is no menu items // TODO: Do not show MenuButton if there is no menu items
h_flex() h_flex()
.gap_2() .gap_2()
.occlude() .occlude()
@ -390,17 +388,29 @@ impl TabPanel {
.when_some(self.toolbar_buttons(cx), |this, buttons| { .when_some(self.toolbar_buttons(cx), |this, buttons| {
this.children(buttons.into_iter().map(|btn| btn.xsmall().ghost())) this.children(buttons.into_iter().map(|btn| btn.xsmall().ghost()))
}) })
.when(self.is_zoomed, |this| { .map(|this| {
this.child( let value = if is_zoomed {
Button::new("zoom") Some(("zoom-out", IconName::Minimize, t!("Dock.Zoom Out")))
.icon(IconName::Minimize) } else if zoomable_toolbar_visible {
.xsmall() Some(("zoom-in", IconName::Maximize, t!("Dock.Zoom In")))
.ghost() } else {
.tooltip(t!("Dock.Zoom Out")) None
.on_click( };
cx.listener(|view, _, cx| view.on_action_toggle_zoom(&ToggleZoom, cx)),
), if let Some((id, icon, tooltip)) = value {
) this.child(
Button::new(id)
.icon(icon)
.xsmall()
.ghost()
.tooltip(tooltip)
.on_click(cx.listener(|view, _, cx| {
view.on_action_toggle_zoom(&ToggleZoom, cx)
})),
)
} else {
this
}
}) })
.child( .child(
Button::new("menu") Button::new("menu")
@ -408,7 +418,7 @@ impl TabPanel {
.xsmall() .xsmall()
.ghost() .ghost()
.popup_menu({ .popup_menu({
let zoomable = state.zoomable; let zoomable = state.zoomable.map_or(false, |v| v.menu_visible());
let closable = state.closable; let closable = state.closable;
move |this, cx| { move |this, cx| {
@ -932,7 +942,7 @@ impl TabPanel {
} }
fn on_action_toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) { fn on_action_toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
if !self.zoomable(cx) { if self.zoomable(cx).is_none() {
return; return;
} }