dock.rs

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