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}