1use crate::{StatusItemView, Workspace};
2use gpui::{
3 elements::*, impl_actions, platform::CursorStyle, platform::MouseButton, AnyViewHandle,
4 AppContext, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
5};
6use serde::Deserialize;
7use settings::Settings;
8use std::rc::Rc;
9
10pub trait Panel: View {
11 fn should_activate_item_on_event(&self, _: &Self::Event, _: &AppContext) -> bool {
12 false
13 }
14 fn should_show_badge(&self, _: &AppContext) -> bool {
15 false
16 }
17 fn contains_focused_view(&self, _: &AppContext) -> bool {
18 false
19 }
20}
21
22pub trait PanelHandle {
23
24 fn id(&self) -> usize;
25 fn should_show_badge(&self, cx: &WindowContext) -> bool;
26 fn is_focused(&self, cx: &WindowContext) -> bool;
27 fn as_any(&self) -> &AnyViewHandle;
28}
29
30impl<T> PanelHandle for ViewHandle<T>
31where
32 T: Panel,
33{
34 fn id(&self) -> usize {
35 self.id()
36 }
37
38 fn should_show_badge(&self, cx: &WindowContext) -> bool {
39 self.read(cx).should_show_badge(cx)
40 }
41
42 fn is_focused(&self, cx: &WindowContext) -> bool {
43 ViewHandle::is_focused(self, cx) || self.read(cx).contains_focused_view(cx)
44 }
45
46 fn as_any(&self) -> &AnyViewHandle {
47 self
48 }
49}
50
51impl From<&dyn PanelHandle> for AnyViewHandle {
52 fn from(val: &dyn PanelHandle) -> Self {
53 val.as_any().clone()
54 }
55}
56
57pub struct Dock {
58 position: DockPosition,
59 items: Vec<Item>,
60 is_open: bool,
61 active_item_ix: usize,
62}
63
64#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
65pub enum DockPosition {
66 Left,
67 Right,
68}
69
70impl DockPosition {
71 fn to_resizable_side(self) -> Side {
72 match self {
73 Self::Left => Side::Right,
74 Self::Right => Side::Left,
75 }
76 }
77}
78
79struct Item {
80 icon_path: &'static str,
81 tooltip: String,
82 view: Rc<dyn PanelHandle>,
83 _subscriptions: [Subscription; 2],
84}
85
86pub struct PanelButtons {
87 dock: ViewHandle<Dock>,
88 workspace: WeakViewHandle<Workspace>,
89}
90
91#[derive(Clone, Debug, Deserialize, PartialEq)]
92pub struct TogglePanel {
93 pub dock_position: DockPosition,
94 pub item_index: usize,
95}
96
97impl_actions!(workspace, [TogglePanel]);
98
99impl Dock {
100 pub fn new(position: DockPosition) -> Self {
101 Self {
102 position,
103 items: Default::default(),
104 active_item_ix: 0,
105 is_open: false,
106 }
107 }
108
109 pub fn is_open(&self) -> bool {
110 self.is_open
111 }
112
113 pub fn active_item_ix(&self) -> usize {
114 self.active_item_ix
115 }
116
117 pub fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
118 if open != self.is_open {
119 self.is_open = open;
120 cx.notify();
121 }
122 }
123
124 pub fn toggle_open(&mut self, cx: &mut ViewContext<Self>) {
125 if self.is_open {}
126 self.is_open = !self.is_open;
127 cx.notify();
128 }
129
130 pub fn add_item<T: Panel>(
131 &mut self,
132 icon_path: &'static str,
133 tooltip: String,
134 view: ViewHandle<T>,
135 cx: &mut ViewContext<Self>,
136 ) {
137 let subscriptions = [
138 cx.observe(&view, |_, _, cx| cx.notify()),
139 cx.subscribe(&view, |this, view, event, cx| {
140 if view.read(cx).should_activate_item_on_event(event, cx) {
141 if let Some(ix) = this
142 .items
143 .iter()
144 .position(|item| item.view.id() == view.id())
145 {
146 this.activate_item(ix, cx);
147 }
148 }
149 }),
150 ];
151
152 self.items.push(Item {
153 icon_path,
154 tooltip,
155 view: Rc::new(view),
156 _subscriptions: subscriptions,
157 });
158 cx.notify()
159 }
160
161 pub fn activate_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
162 self.active_item_ix = item_ix;
163 cx.notify();
164 }
165
166 pub fn toggle_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
167 if self.active_item_ix == item_ix {
168 self.is_open = false;
169 } else {
170 self.active_item_ix = item_ix;
171 }
172 cx.notify();
173 }
174
175 pub fn active_item(&self) -> Option<&Rc<dyn PanelHandle>> {
176 if self.is_open {
177 self.items.get(self.active_item_ix).map(|item| &item.view)
178 } else {
179 None
180 }
181 }
182}
183
184impl Entity for Dock {
185 type Event = ();
186}
187
188impl View for Dock {
189 fn ui_name() -> &'static str {
190 "Dock"
191 }
192
193 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
194 if let Some(active_item) = self.active_item() {
195 enum ResizeHandleTag {}
196 let style = &cx.global::<Settings>().theme.workspace.dock;
197 ChildView::new(active_item.as_any(), cx)
198 .contained()
199 .with_style(style.container)
200 .with_resize_handle::<ResizeHandleTag>(
201 self.position as usize,
202 self.position.to_resizable_side(),
203 4.,
204 style.initial_size,
205 cx,
206 )
207 .into_any()
208 } else {
209 Empty::new().into_any()
210 }
211 }
212}
213
214impl PanelButtons {
215 pub fn new(
216 dock: ViewHandle<Dock>,
217 workspace: WeakViewHandle<Workspace>,
218 cx: &mut ViewContext<Self>,
219 ) -> Self {
220 cx.observe(&dock, |_, _, cx| cx.notify()).detach();
221 Self { dock, workspace }
222 }
223}
224
225impl Entity for PanelButtons {
226 type Event = ();
227}
228
229impl View for PanelButtons {
230 fn ui_name() -> &'static str {
231 "DockToggleButton"
232 }
233
234 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
235 let theme = &cx.global::<Settings>().theme;
236 let tooltip_style = theme.tooltip.clone();
237 let theme = &theme.workspace.status_bar.panel_buttons;
238 let dock = self.dock.read(cx);
239 let item_style = theme.button.clone();
240 let badge_style = theme.badge;
241 let active_ix = dock.active_item_ix;
242 let is_open = dock.is_open;
243 let dock_position = dock.position;
244 let group_style = match dock_position {
245 DockPosition::Left => theme.group_left,
246 DockPosition::Right => theme.group_right,
247 };
248
249 #[allow(clippy::needless_collect)]
250 let items = dock
251 .items
252 .iter()
253 .map(|item| (item.icon_path, item.tooltip.clone(), item.view.clone()))
254 .collect::<Vec<_>>();
255
256 Flex::row()
257 .with_children(items.into_iter().enumerate().map(
258 |(ix, (icon_path, tooltip, item_view))| {
259 let action = TogglePanel {
260 dock_position,
261 item_index: ix,
262 };
263 MouseEventHandler::<Self, _>::new(ix, cx, |state, cx| {
264 let is_active = is_open && ix == active_ix;
265 let style = item_style.style_for(state, is_active);
266 Stack::new()
267 .with_child(Svg::new(icon_path).with_color(style.icon_color))
268 .with_children(if !is_active && item_view.should_show_badge(cx) {
269 Some(
270 Empty::new()
271 .collapsed()
272 .contained()
273 .with_style(badge_style)
274 .aligned()
275 .bottom()
276 .right(),
277 )
278 } else {
279 None
280 })
281 .constrained()
282 .with_width(style.icon_size)
283 .with_height(style.icon_size)
284 .contained()
285 .with_style(style.container)
286 })
287 .with_cursor_style(CursorStyle::PointingHand)
288 .on_click(MouseButton::Left, {
289 let action = action.clone();
290 move |_, this, cx| {
291 if let Some(workspace) = this.workspace.upgrade(cx) {
292 let action = action.clone();
293 cx.window_context().defer(move |cx| {
294 workspace.update(cx, |workspace, cx| {
295 workspace.toggle_panel(&action, cx)
296 });
297 });
298 }
299 }
300 })
301 .with_tooltip::<Self>(
302 ix,
303 tooltip,
304 Some(Box::new(action)),
305 tooltip_style.clone(),
306 cx,
307 )
308 },
309 ))
310 .contained()
311 .with_style(group_style)
312 .into_any()
313 }
314}
315
316impl StatusItemView for PanelButtons {
317 fn set_active_pane_item(
318 &mut self,
319 _: Option<&dyn crate::ItemHandle>,
320 _: &mut ViewContext<Self>,
321 ) {
322 }
323}