utility_pane.rs

  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}