toolbar.rs

  1use crate::{ItemHandle, Pane};
  2use gpui::{
  3    elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyElement, AnyViewHandle,
  4    AppContext, Entity, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
  5};
  6
  7pub trait ToolbarItemView: View {
  8    fn set_active_pane_item(
  9        &mut self,
 10        active_pane_item: Option<&dyn crate::ItemHandle>,
 11        cx: &mut ViewContext<Self>,
 12    ) -> ToolbarItemLocation;
 13
 14    fn location_for_event(
 15        &self,
 16        _event: &Self::Event,
 17        current_location: ToolbarItemLocation,
 18        _cx: &AppContext,
 19    ) -> ToolbarItemLocation {
 20        current_location
 21    }
 22
 23    fn pane_focus_update(&mut self, _pane_focused: bool, _cx: &mut ViewContext<Self>) {}
 24
 25    /// Number of times toolbar's height will be repeated to get the effective height.
 26    /// Useful when multiple rows one under each other are needed.
 27    /// The rows have the same width and act as a whole when reacting to resizes and similar events.
 28    fn row_count(&self) -> usize {
 29        1
 30    }
 31}
 32
 33trait ToolbarItemViewHandle {
 34    fn id(&self) -> usize;
 35    fn as_any(&self) -> &AnyViewHandle;
 36    fn set_active_pane_item(
 37        &self,
 38        active_pane_item: Option<&dyn ItemHandle>,
 39        cx: &mut WindowContext,
 40    ) -> ToolbarItemLocation;
 41    fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext);
 42    fn row_count(&self, cx: &WindowContext) -> usize;
 43}
 44
 45#[derive(Copy, Clone, Debug, PartialEq)]
 46pub enum ToolbarItemLocation {
 47    Hidden,
 48    PrimaryLeft { flex: Option<(f32, bool)> },
 49    PrimaryRight { flex: Option<(f32, bool)> },
 50    Secondary,
 51}
 52
 53pub struct Toolbar {
 54    active_pane_item: Option<Box<dyn ItemHandle>>,
 55    hidden: bool,
 56    pane: WeakViewHandle<Pane>,
 57    items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
 58}
 59
 60impl Entity for Toolbar {
 61    type Event = ();
 62}
 63
 64impl View for Toolbar {
 65    fn ui_name() -> &'static str {
 66        "Toolbar"
 67    }
 68
 69    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
 70        let theme = &theme::current(cx).workspace.toolbar;
 71
 72        let mut primary_left_items = Vec::new();
 73        let mut primary_right_items = Vec::new();
 74        let mut secondary_item = None;
 75        let spacing = theme.item_spacing;
 76        let mut primary_items_row_count = 1;
 77
 78        for (item, position) in &self.items {
 79            match *position {
 80                ToolbarItemLocation::Hidden => {}
 81
 82                ToolbarItemLocation::PrimaryLeft { flex } => {
 83                    primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
 84                    let left_item = ChildView::new(item.as_any(), cx)
 85                        .aligned()
 86                        .contained()
 87                        .with_margin_right(spacing);
 88                    if let Some((flex, expanded)) = flex {
 89                        primary_left_items.push(left_item.flex(flex, expanded).into_any());
 90                    } else {
 91                        primary_left_items.push(left_item.into_any());
 92                    }
 93                }
 94
 95                ToolbarItemLocation::PrimaryRight { flex } => {
 96                    primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
 97                    let right_item = ChildView::new(item.as_any(), cx)
 98                        .aligned()
 99                        .contained()
100                        .with_margin_left(spacing)
101                        .flex_float();
102                    if let Some((flex, expanded)) = flex {
103                        primary_right_items.push(right_item.flex(flex, expanded).into_any());
104                    } else {
105                        primary_right_items.push(right_item.into_any());
106                    }
107                }
108
109                ToolbarItemLocation::Secondary => {
110                    secondary_item = Some(
111                        ChildView::new(item.as_any(), cx)
112                            .constrained()
113                            .with_height(theme.height * item.row_count(cx) as f32)
114                            .into_any(),
115                    );
116                }
117            }
118        }
119
120        let pane = self.pane.clone();
121        let mut enable_go_backward = false;
122        let mut enable_go_forward = false;
123        if let Some(pane) = pane.upgrade(cx) {
124            let pane = pane.read(cx);
125            enable_go_backward = pane.can_navigate_backward();
126            enable_go_forward = pane.can_navigate_forward();
127        }
128
129        let container_style = theme.container;
130        let height = theme.height * primary_items_row_count as f32;
131        let nav_button_height = theme.height;
132        let button_style = theme.nav_button;
133        let tooltip_style = theme::current(cx).tooltip.clone();
134
135        Flex::column()
136            .with_child(
137                Flex::row()
138                    .with_child(nav_button(
139                        "icons/arrow_left_16.svg",
140                        button_style,
141                        nav_button_height,
142                        tooltip_style.clone(),
143                        enable_go_backward,
144                        spacing,
145                        {
146                            let pane = pane.clone();
147                            move |toolbar, cx| {
148                                if let Some(workspace) = toolbar
149                                    .pane
150                                    .upgrade(cx)
151                                    .and_then(|pane| pane.read(cx).workspace().upgrade(cx))
152                                {
153                                    let pane = pane.clone();
154                                    cx.window_context().defer(move |cx| {
155                                        workspace.update(cx, |workspace, cx| {
156                                            Pane::go_back(workspace, Some(pane.clone()), cx)
157                                                .detach_and_log_err(cx);
158                                        });
159                                    })
160                                }
161                            }
162                        },
163                        super::GoBack { pane: None },
164                        "Go Back",
165                        cx,
166                    ))
167                    .with_child(nav_button(
168                        "icons/arrow_right_16.svg",
169                        button_style,
170                        nav_button_height,
171                        tooltip_style,
172                        enable_go_forward,
173                        spacing,
174                        {
175                            let pane = pane.clone();
176                            move |toolbar, cx| {
177                                if let Some(workspace) = toolbar
178                                    .pane
179                                    .upgrade(cx)
180                                    .and_then(|pane| pane.read(cx).workspace().upgrade(cx))
181                                {
182                                    let pane = pane.clone();
183                                    cx.window_context().defer(move |cx| {
184                                        workspace.update(cx, |workspace, cx| {
185                                            Pane::go_forward(workspace, Some(pane.clone()), cx)
186                                                .detach_and_log_err(cx);
187                                        });
188                                    });
189                                }
190                            }
191                        },
192                        super::GoForward { pane: None },
193                        "Go Forward",
194                        cx,
195                    ))
196                    .with_children(primary_left_items)
197                    .with_children(primary_right_items)
198                    .constrained()
199                    .with_height(height),
200            )
201            .with_children(secondary_item)
202            .contained()
203            .with_style(container_style)
204            .into_any_named("toolbar")
205    }
206}
207
208#[allow(clippy::too_many_arguments)]
209fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
210    svg_path: &'static str,
211    style: theme::Interactive<theme::IconButton>,
212    nav_button_height: f32,
213    tooltip_style: TooltipStyle,
214    enabled: bool,
215    spacing: f32,
216    on_click: F,
217    tooltip_action: A,
218    action_name: &str,
219    cx: &mut ViewContext<Toolbar>,
220) -> AnyElement<Toolbar> {
221    MouseEventHandler::<A, _>::new(0, cx, |state, _| {
222        let style = if enabled {
223            style.style_for(state, false)
224        } else {
225            style.disabled_style()
226        };
227        Svg::new(svg_path)
228            .with_color(style.color)
229            .constrained()
230            .with_width(style.icon_width)
231            .aligned()
232            .contained()
233            .with_style(style.container)
234            .constrained()
235            .with_width(style.button_width)
236            .with_height(nav_button_height)
237            .aligned()
238            .top()
239    })
240    .with_cursor_style(if enabled {
241        CursorStyle::PointingHand
242    } else {
243        CursorStyle::default()
244    })
245    .on_click(MouseButton::Left, move |_, toolbar, cx| {
246        on_click(toolbar, cx)
247    })
248    .with_tooltip::<A>(
249        0,
250        action_name.to_string(),
251        Some(Box::new(tooltip_action)),
252        tooltip_style,
253        cx,
254    )
255    .contained()
256    .with_margin_right(spacing)
257    .into_any_named("nav button")
258}
259
260impl Toolbar {
261    pub fn new(pane: WeakViewHandle<Pane>) -> Self {
262        Self {
263            active_pane_item: None,
264            pane,
265            items: Default::default(),
266            hidden: false,
267        }
268    }
269
270    pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
271    where
272        T: 'static + ToolbarItemView,
273    {
274        let location = item.set_active_pane_item(self.active_pane_item.as_deref(), cx);
275        cx.subscribe(&item, |this, item, event, cx| {
276            if let Some((_, current_location)) =
277                this.items.iter_mut().find(|(i, _)| i.id() == item.id())
278            {
279                let new_location = item
280                    .read(cx)
281                    .location_for_event(event, *current_location, cx);
282                if new_location != *current_location {
283                    *current_location = new_location;
284                    cx.notify();
285                }
286            }
287        })
288        .detach();
289        self.items.push((Box::new(item), location));
290        cx.notify();
291    }
292
293    pub fn set_active_pane_item(
294        &mut self,
295        pane_item: Option<&dyn ItemHandle>,
296        cx: &mut ViewContext<Self>,
297    ) {
298        self.active_pane_item = pane_item.map(|item| item.boxed_clone());
299        self.hidden = self
300            .active_pane_item
301            .as_ref()
302            .map(|item| !item.show_toolbar(cx))
303            .unwrap_or(false);
304
305        for (toolbar_item, current_location) in self.items.iter_mut() {
306            let new_location = toolbar_item.set_active_pane_item(pane_item, cx);
307            if new_location != *current_location {
308                *current_location = new_location;
309                cx.notify();
310            }
311        }
312    }
313
314    pub fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut ViewContext<Self>) {
315        for (toolbar_item, _) in self.items.iter_mut() {
316            toolbar_item.pane_focus_update(pane_focused, cx);
317        }
318    }
319
320    pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> {
321        self.items
322            .iter()
323            .find_map(|(item, _)| item.as_any().clone().downcast())
324    }
325
326    pub fn hidden(&self) -> bool {
327        self.hidden
328    }
329}
330
331impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
332    fn id(&self) -> usize {
333        self.id()
334    }
335
336    fn as_any(&self) -> &AnyViewHandle {
337        self
338    }
339
340    fn set_active_pane_item(
341        &self,
342        active_pane_item: Option<&dyn ItemHandle>,
343        cx: &mut WindowContext,
344    ) -> ToolbarItemLocation {
345        self.update(cx, |this, cx| {
346            this.set_active_pane_item(active_pane_item, cx)
347        })
348    }
349
350    fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext) {
351        self.update(cx, |this, cx| {
352            this.pane_focus_update(pane_focused, cx);
353            cx.notify();
354        });
355    }
356
357    fn row_count(&self, cx: &WindowContext) -> usize {
358        self.read(cx).row_count()
359    }
360}
361
362impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle {
363    fn from(val: &dyn ToolbarItemViewHandle) -> Self {
364        val.as_any().clone()
365    }
366}