1use gpui::{
2 AppContext as _, EntityId, MouseButton, Pixels, Render, StatefulInteractiveElement,
3 Subscription, WeakEntity, deferred, px,
4};
5use ui::{
6 ActiveTheme as _, Context, FluentBuilder as _, InteractiveElement as _, IntoElement,
7 ParentElement as _, RenderOnce, Styled as _, Window, div,
8};
9
10use crate::{
11 DockPosition, Workspace,
12 dock::{ClosePane, MinimizePane, UtilityPane, UtilityPaneHandle},
13};
14
15pub(crate) const UTILITY_PANE_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
16pub(crate) const UTILITY_PANE_MIN_WIDTH: Pixels = px(20.0);
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum UtilityPaneSlot {
20 Left,
21 Right,
22}
23
24struct UtilityPaneSlotState {
25 panel_id: EntityId,
26 utility_pane: Box<dyn UtilityPaneHandle>,
27 _subscriptions: Vec<Subscription>,
28}
29
30#[derive(Default)]
31pub struct UtilityPaneState {
32 left_slot: Option<UtilityPaneSlotState>,
33 right_slot: Option<UtilityPaneSlotState>,
34}
35
36#[derive(Clone)]
37pub struct DraggedUtilityPane(pub UtilityPaneSlot);
38
39impl Render for DraggedUtilityPane {
40 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
41 gpui::Empty
42 }
43}
44
45pub fn utility_slot_for_dock_position(position: DockPosition) -> UtilityPaneSlot {
46 match position {
47 DockPosition::Left => UtilityPaneSlot::Left,
48 DockPosition::Right => UtilityPaneSlot::Right,
49 DockPosition::Bottom => UtilityPaneSlot::Left,
50 }
51}
52
53impl Workspace {
54 pub fn utility_pane(&self, slot: UtilityPaneSlot) -> Option<&dyn UtilityPaneHandle> {
55 match slot {
56 UtilityPaneSlot::Left => self
57 .utility_panes
58 .left_slot
59 .as_ref()
60 .map(|s| s.utility_pane.as_ref()),
61 UtilityPaneSlot::Right => self
62 .utility_panes
63 .right_slot
64 .as_ref()
65 .map(|s| s.utility_pane.as_ref()),
66 }
67 }
68
69 pub fn toggle_utility_pane(
70 &mut self,
71 slot: UtilityPaneSlot,
72 window: &mut Window,
73 cx: &mut Context<Self>,
74 ) {
75 if let Some(handle) = self.utility_pane(slot) {
76 let current = handle.expanded(cx);
77 handle.set_expanded(!current, cx);
78 }
79 cx.notify();
80 self.serialize_workspace(window, cx);
81 }
82
83 pub fn register_utility_pane<T: UtilityPane>(
84 &mut self,
85 slot: UtilityPaneSlot,
86 panel_id: EntityId,
87 handle: gpui::Entity<T>,
88 cx: &mut Context<Self>,
89 ) {
90 let minimize_subscription =
91 cx.subscribe(&handle, move |this, _, _event: &MinimizePane, cx| {
92 if let Some(handle) = this.utility_pane(slot) {
93 handle.set_expanded(false, cx);
94 }
95 cx.notify();
96 });
97
98 let close_subscription = cx.subscribe(&handle, move |this, _, _event: &ClosePane, cx| {
99 this.clear_utility_pane(slot, cx);
100 });
101
102 let subscriptions = vec![minimize_subscription, close_subscription];
103 let boxed_handle: Box<dyn UtilityPaneHandle> = Box::new(handle);
104
105 match slot {
106 UtilityPaneSlot::Left => {
107 self.utility_panes.left_slot = Some(UtilityPaneSlotState {
108 panel_id,
109 utility_pane: boxed_handle,
110 _subscriptions: subscriptions,
111 });
112 }
113 UtilityPaneSlot::Right => {
114 self.utility_panes.right_slot = Some(UtilityPaneSlotState {
115 panel_id,
116 utility_pane: boxed_handle,
117 _subscriptions: subscriptions,
118 });
119 }
120 }
121 cx.notify();
122 }
123
124 pub fn clear_utility_pane(&mut self, slot: UtilityPaneSlot, cx: &mut Context<Self>) {
125 match slot {
126 UtilityPaneSlot::Left => {
127 self.utility_panes.left_slot = None;
128 }
129 UtilityPaneSlot::Right => {
130 self.utility_panes.right_slot = None;
131 }
132 }
133 cx.notify();
134 }
135
136 pub fn clear_utility_pane_if_provider(
137 &mut self,
138 slot: UtilityPaneSlot,
139 provider_panel_id: EntityId,
140 cx: &mut Context<Self>,
141 ) {
142 let should_clear = match slot {
143 UtilityPaneSlot::Left => self
144 .utility_panes
145 .left_slot
146 .as_ref()
147 .is_some_and(|slot| slot.panel_id == provider_panel_id),
148 UtilityPaneSlot::Right => self
149 .utility_panes
150 .right_slot
151 .as_ref()
152 .is_some_and(|slot| slot.panel_id == provider_panel_id),
153 };
154
155 if should_clear {
156 self.clear_utility_pane(slot, cx);
157 }
158 }
159
160 pub fn resize_utility_pane(
161 &mut self,
162 slot: UtilityPaneSlot,
163 new_width: Pixels,
164 window: &mut Window,
165 cx: &mut Context<Self>,
166 ) {
167 if let Some(handle) = self.utility_pane(slot) {
168 let max_width = self.max_utility_pane_width(window, cx);
169 let width = new_width.max(UTILITY_PANE_MIN_WIDTH).min(max_width);
170 handle.set_width(Some(width), cx);
171 cx.notify();
172 self.serialize_workspace(window, cx);
173 }
174 }
175
176 pub fn reset_utility_pane_width(
177 &mut self,
178 slot: UtilityPaneSlot,
179 window: &mut Window,
180 cx: &mut Context<Self>,
181 ) {
182 if let Some(handle) = self.utility_pane(slot) {
183 handle.set_width(None, cx);
184 cx.notify();
185 self.serialize_workspace(window, cx);
186 }
187 }
188}
189
190#[derive(IntoElement)]
191pub struct UtilityPaneFrame {
192 workspace: WeakEntity<Workspace>,
193 slot: UtilityPaneSlot,
194 handle: Box<dyn UtilityPaneHandle>,
195}
196
197impl UtilityPaneFrame {
198 pub fn new(
199 slot: UtilityPaneSlot,
200 handle: Box<dyn UtilityPaneHandle>,
201 cx: &mut Context<Workspace>,
202 ) -> Self {
203 let workspace = cx.weak_entity();
204 Self {
205 workspace,
206 slot,
207 handle,
208 }
209 }
210}
211
212impl RenderOnce for UtilityPaneFrame {
213 fn render(self, _window: &mut Window, cx: &mut ui::App) -> impl IntoElement {
214 let workspace = self.workspace.clone();
215 let slot = self.slot;
216 let width = self.handle.width(cx);
217
218 let create_resize_handle = || {
219 let workspace_handle = workspace.clone();
220 let handle = div()
221 .id(match slot {
222 UtilityPaneSlot::Left => "utility-pane-resize-handle-left",
223 UtilityPaneSlot::Right => "utility-pane-resize-handle-right",
224 })
225 .on_drag(DraggedUtilityPane(slot), move |pane, _, _, cx| {
226 cx.stop_propagation();
227 cx.new(|_| pane.clone())
228 })
229 .on_mouse_down(MouseButton::Left, move |_, _, cx| {
230 cx.stop_propagation();
231 })
232 .on_mouse_up(
233 MouseButton::Left,
234 move |e: &gpui::MouseUpEvent, window, cx| {
235 if e.click_count == 2 {
236 workspace_handle
237 .update(cx, |workspace, cx| {
238 workspace.reset_utility_pane_width(slot, window, cx);
239 })
240 .ok();
241 cx.stop_propagation();
242 }
243 },
244 )
245 .occlude();
246
247 match slot {
248 UtilityPaneSlot::Left => deferred(
249 handle
250 .absolute()
251 .right(-UTILITY_PANE_RESIZE_HANDLE_SIZE / 2.)
252 .top(px(0.))
253 .h_full()
254 .w(UTILITY_PANE_RESIZE_HANDLE_SIZE)
255 .cursor_col_resize(),
256 ),
257 UtilityPaneSlot::Right => deferred(
258 handle
259 .absolute()
260 .left(-UTILITY_PANE_RESIZE_HANDLE_SIZE / 2.)
261 .top(px(0.))
262 .h_full()
263 .w(UTILITY_PANE_RESIZE_HANDLE_SIZE)
264 .cursor_col_resize(),
265 ),
266 }
267 };
268
269 div()
270 .h_full()
271 .bg(cx.theme().colors().tab_bar_background)
272 .w(width)
273 .border_color(cx.theme().colors().border)
274 .when(self.slot == UtilityPaneSlot::Left, |this| this.border_r_1())
275 .when(self.slot == UtilityPaneSlot::Right, |this| {
276 this.border_l_1()
277 })
278 .child(create_resize_handle())
279 .child(self.handle.to_any())
280 .into_any_element()
281 }
282}