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