dock.rs

  1use collections::HashMap;
  2use gpui::{
  3    actions,
  4    elements::{ChildView, Container, Empty, Margin, MouseEventHandler, Side, Svg},
  5    impl_internal_actions, CursorStyle, Element, ElementBox, Entity, MouseButton,
  6    MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
  7};
  8use serde::Deserialize;
  9use settings::{DockAnchor, Settings};
 10use theme::Theme;
 11
 12use crate::{sidebar::SidebarSide, ItemHandle, Pane, StatusItemView, Workspace};
 13
 14#[derive(PartialEq, Clone, Deserialize)]
 15pub struct MoveDock(pub DockAnchor);
 16
 17#[derive(PartialEq, Clone)]
 18pub struct AddDefaultItemToDock;
 19
 20actions!(workspace, [ToggleDock]);
 21impl_internal_actions!(workspace, [MoveDock, AddDefaultItemToDock]);
 22
 23pub fn init(cx: &mut MutableAppContext) {
 24    cx.add_action(Dock::toggle);
 25    cx.add_action(Dock::move_dock);
 26}
 27
 28#[derive(Copy, Clone)]
 29pub enum DockPosition {
 30    Shown(DockAnchor),
 31    Hidden(DockAnchor),
 32}
 33
 34impl Default for DockPosition {
 35    fn default() -> Self {
 36        DockPosition::Hidden(Default::default())
 37    }
 38}
 39
 40pub fn icon_for_dock_anchor(anchor: DockAnchor) -> &'static str {
 41    match anchor {
 42        DockAnchor::Right => "icons/dock_right_12.svg",
 43        DockAnchor::Bottom => "icons/dock_bottom_12.svg",
 44        DockAnchor::Expanded => "icons/dock_modal_12.svg",
 45    }
 46}
 47
 48impl DockPosition {
 49    fn is_visible(&self) -> bool {
 50        match self {
 51            DockPosition::Shown(_) => true,
 52            DockPosition::Hidden(_) => false,
 53        }
 54    }
 55
 56    fn anchor(&self) -> DockAnchor {
 57        match self {
 58            DockPosition::Shown(anchor) | DockPosition::Hidden(anchor) => *anchor,
 59        }
 60    }
 61
 62    fn toggle(self) -> Self {
 63        match self {
 64            DockPosition::Shown(anchor) => DockPosition::Hidden(anchor),
 65            DockPosition::Hidden(anchor) => DockPosition::Shown(anchor),
 66        }
 67    }
 68
 69    fn hide(self) -> Self {
 70        match self {
 71            DockPosition::Shown(anchor) => DockPosition::Hidden(anchor),
 72            DockPosition::Hidden(_) => self,
 73        }
 74    }
 75}
 76
 77pub type DefaultItemFactory =
 78    fn(&mut Workspace, &mut ViewContext<Workspace>) -> Box<dyn ItemHandle>;
 79
 80pub struct Dock {
 81    position: DockPosition,
 82    panel_sizes: HashMap<DockAnchor, f32>,
 83    pane: ViewHandle<Pane>,
 84    default_item_factory: DefaultItemFactory,
 85}
 86
 87impl Dock {
 88    pub fn new(cx: &mut ViewContext<Workspace>, default_item_factory: DefaultItemFactory) -> Self {
 89        let anchor = cx.global::<Settings>().default_dock_anchor;
 90        let pane = cx.add_view(|cx| Pane::new(Some(anchor), cx));
 91        let pane_id = pane.id();
 92        cx.subscribe(&pane, move |workspace, _, event, cx| {
 93            workspace.handle_pane_event(pane_id, event, cx);
 94        })
 95        .detach();
 96
 97        Self {
 98            pane,
 99            panel_sizes: Default::default(),
100            position: DockPosition::Hidden(anchor),
101            default_item_factory,
102        }
103    }
104
105    pub fn pane(&self) -> &ViewHandle<Pane> {
106        &self.pane
107    }
108
109    pub fn visible_pane(&self) -> Option<&ViewHandle<Pane>> {
110        self.position.is_visible().then(|| self.pane())
111    }
112
113    pub fn is_anchored_at(&self, anchor: DockAnchor) -> bool {
114        self.position.is_visible() && self.position.anchor() == anchor
115    }
116
117    fn set_dock_position(
118        workspace: &mut Workspace,
119        new_position: DockPosition,
120        cx: &mut ViewContext<Workspace>,
121    ) {
122        workspace.dock.position = new_position;
123        // Tell the pane about the new anchor position
124        workspace.dock.pane.update(cx, |pane, cx| {
125            pane.set_docked(Some(new_position.anchor()), cx)
126        });
127
128        if workspace.dock.position.is_visible() {
129            // Close the right sidebar if the dock is on the right side and the right sidebar is open
130            if workspace.dock.position.anchor() == DockAnchor::Right {
131                if workspace.right_sidebar().read(cx).is_open() {
132                    workspace.toggle_sidebar(SidebarSide::Right, cx);
133                }
134            }
135
136            // Ensure that the pane has at least one item or construct a default item to put in it
137            let pane = workspace.dock.pane.clone();
138            if pane.read(cx).items().next().is_none() {
139                let item_to_add = (workspace.dock.default_item_factory)(workspace, cx);
140                Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
141            }
142            cx.focus(pane);
143        } else if let Some(last_active_center_pane) = workspace.last_active_center_pane.clone() {
144            cx.focus(last_active_center_pane);
145        }
146        cx.emit(crate::Event::DockAnchorChanged);
147        cx.notify();
148    }
149
150    pub fn hide(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
151        Self::set_dock_position(workspace, workspace.dock.position.hide(), cx);
152    }
153
154    fn toggle(workspace: &mut Workspace, _: &ToggleDock, cx: &mut ViewContext<Workspace>) {
155        Self::set_dock_position(workspace, workspace.dock.position.toggle(), cx);
156    }
157
158    fn move_dock(
159        workspace: &mut Workspace,
160        &MoveDock(new_anchor): &MoveDock,
161        cx: &mut ViewContext<Workspace>,
162    ) {
163        Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), cx);
164    }
165
166    pub fn render(
167        &self,
168        theme: &Theme,
169        anchor: DockAnchor,
170        cx: &mut RenderContext<Workspace>,
171    ) -> Option<ElementBox> {
172        let style = &theme.workspace.dock;
173
174        self.position
175            .is_visible()
176            .then(|| self.position.anchor())
177            .filter(|current_anchor| *current_anchor == anchor)
178            .map(|anchor| match anchor {
179                DockAnchor::Bottom | DockAnchor::Right => {
180                    let mut panel_style = style.panel.clone();
181                    let resize_side = if anchor == DockAnchor::Bottom {
182                        panel_style.margin = Margin {
183                            top: panel_style.margin.top,
184                            ..Default::default()
185                        };
186                        Side::Top
187                    } else {
188                        panel_style.margin = Margin {
189                            left: panel_style.margin.left,
190                            ..Default::default()
191                        };
192                        Side::Left
193                    };
194
195                    enum DockResizeHandle {}
196
197                    let resizable = Container::new(ChildView::new(self.pane.clone()).boxed())
198                        .with_style(panel_style)
199                        .with_resize_handle::<DockResizeHandle, _>(
200                            resize_side as usize,
201                            resize_side,
202                            4.,
203                            self.panel_sizes.get(&anchor).copied().unwrap_or(200.),
204                            cx,
205                        );
206
207                    let size = resizable.current_size();
208                    let workspace = cx.handle();
209                    cx.defer(move |cx| {
210                        if let Some(workspace) = workspace.upgrade(cx) {
211                            workspace.update(cx, |workspace, _| {
212                                workspace.dock.panel_sizes.insert(anchor, size);
213                            })
214                        }
215                    });
216
217                    resizable.flex(style.flex, false).boxed()
218                }
219                DockAnchor::Expanded => Container::new(
220                    MouseEventHandler::<Dock>::new(0, cx, |_state, _cx| {
221                        Container::new(ChildView::new(self.pane.clone()).boxed())
222                            .with_style(style.maximized)
223                            .boxed()
224                    })
225                    .capture_all()
226                    .with_cursor_style(CursorStyle::Arrow)
227                    .boxed(),
228                )
229                .with_background_color(style.wash_color)
230                .boxed(),
231            })
232    }
233}
234
235pub struct ToggleDockButton {
236    workspace: WeakViewHandle<Workspace>,
237}
238
239impl ToggleDockButton {
240    pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
241        // When dock moves, redraw so that the icon and toggle status matches.
242        cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach();
243
244        Self {
245            workspace: workspace.downgrade(),
246        }
247    }
248}
249
250impl Entity for ToggleDockButton {
251    type Event = ();
252}
253
254impl View for ToggleDockButton {
255    fn ui_name() -> &'static str {
256        "Dock Toggle"
257    }
258
259    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
260        let workspace = self.workspace.upgrade(cx);
261
262        if workspace.is_none() {
263            return Empty::new().boxed();
264        }
265
266        let dock_position = workspace.unwrap().read(cx).dock.position;
267
268        let theme = cx.global::<Settings>().theme.clone();
269        MouseEventHandler::<Self>::new(0, cx, {
270            let theme = theme.clone();
271            move |state, _| {
272                let style = theme
273                    .workspace
274                    .status_bar
275                    .sidebar_buttons
276                    .item
277                    .style_for(state, dock_position.is_visible());
278
279                Svg::new(icon_for_dock_anchor(dock_position.anchor()))
280                    .with_color(style.icon_color)
281                    .constrained()
282                    .with_width(style.icon_size)
283                    .with_height(style.icon_size)
284                    .contained()
285                    .with_style(style.container)
286                    .boxed()
287            }
288        })
289        .with_cursor_style(CursorStyle::PointingHand)
290        .on_click(MouseButton::Left, |_, cx| {
291            cx.dispatch_action(ToggleDock);
292        })
293        .with_tooltip::<Self, _>(
294            0,
295            "Toggle Dock".to_string(),
296            Some(Box::new(ToggleDock)),
297            theme.tooltip.clone(),
298            cx,
299        )
300        .boxed()
301    }
302}
303
304impl StatusItemView for ToggleDockButton {
305    fn set_active_pane_item(
306        &mut self,
307        _active_pane_item: Option<&dyn crate::ItemHandle>,
308        _cx: &mut ViewContext<Self>,
309    ) {
310        //Not applicable
311    }
312}