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    can_navigate: bool,
 57    pane: WeakViewHandle<Pane>,
 58    items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
 59}
 60
 61impl Entity for Toolbar {
 62    type Event = ();
 63}
 64
 65impl View for Toolbar {
 66    fn ui_name() -> &'static str {
 67        "Toolbar"
 68    }
 69
 70    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
 71        let theme = &theme::current(cx).workspace.toolbar;
 72
 73        let mut primary_left_items = Vec::new();
 74        let mut primary_right_items = Vec::new();
 75        let mut secondary_item = None;
 76        let spacing = theme.item_spacing;
 77        let mut primary_items_row_count = 1;
 78
 79        for (item, position) in &self.items {
 80            match *position {
 81                ToolbarItemLocation::Hidden => {}
 82
 83                ToolbarItemLocation::PrimaryLeft { flex } => {
 84                    primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
 85                    let left_item = ChildView::new(item.as_any(), cx)
 86                        .aligned()
 87                        .contained()
 88                        .with_margin_right(spacing);
 89                    if let Some((flex, expanded)) = flex {
 90                        primary_left_items.push(left_item.flex(flex, expanded).into_any());
 91                    } else {
 92                        primary_left_items.push(left_item.into_any());
 93                    }
 94                }
 95
 96                ToolbarItemLocation::PrimaryRight { flex } => {
 97                    primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
 98                    let right_item = ChildView::new(item.as_any(), cx)
 99                        .aligned()
100                        .contained()
101                        .with_margin_left(spacing)
102                        .flex_float();
103                    if let Some((flex, expanded)) = flex {
104                        primary_right_items.push(right_item.flex(flex, expanded).into_any());
105                    } else {
106                        primary_right_items.push(right_item.into_any());
107                    }
108                }
109
110                ToolbarItemLocation::Secondary => {
111                    secondary_item = Some(
112                        ChildView::new(item.as_any(), cx)
113                            .constrained()
114                            .with_height(theme.height * item.row_count(cx) as f32)
115                            .into_any(),
116                    );
117                }
118            }
119        }
120
121        let pane = self.pane.clone();
122        let mut enable_go_backward = false;
123        let mut enable_go_forward = false;
124        if let Some(pane) = pane.upgrade(cx) {
125            let pane = pane.read(cx);
126            enable_go_backward = pane.can_navigate_backward();
127            enable_go_forward = pane.can_navigate_forward();
128        }
129
130        let container_style = theme.container;
131        let height = theme.height * primary_items_row_count as f32;
132        let nav_button_height = theme.height;
133        let button_style = theme.nav_button;
134        let tooltip_style = theme::current(cx).tooltip.clone();
135
136        let mut primary_items = Flex::row();
137        if self.can_navigate {
138            primary_items.add_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            primary_items.add_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        }
197        primary_items.extend(primary_left_items);
198        primary_items.extend(primary_right_items);
199
200        let mut toolbar = Flex::column();
201        if !primary_items.is_empty() {
202            toolbar.add_child(primary_items.constrained().with_height(height));
203        }
204        if let Some(secondary_item) = secondary_item {
205            toolbar.add_child(secondary_item);
206        }
207
208        if toolbar.is_empty() {
209            toolbar.into_any_named("toolbar")
210        } else {
211            toolbar
212                .contained()
213                .with_style(container_style)
214                .into_any_named("toolbar")
215        }
216    }
217}
218
219#[allow(clippy::too_many_arguments)]
220fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
221    svg_path: &'static str,
222    style: theme::Interactive<theme::IconButton>,
223    nav_button_height: f32,
224    tooltip_style: TooltipStyle,
225    enabled: bool,
226    spacing: f32,
227    on_click: F,
228    tooltip_action: A,
229    action_name: &str,
230    cx: &mut ViewContext<Toolbar>,
231) -> AnyElement<Toolbar> {
232    MouseEventHandler::<A, _>::new(0, cx, |state, _| {
233        let style = if enabled {
234            style.style_for(state, false)
235        } else {
236            style.disabled_style()
237        };
238        Svg::new(svg_path)
239            .with_color(style.color)
240            .constrained()
241            .with_width(style.icon_width)
242            .aligned()
243            .contained()
244            .with_style(style.container)
245            .constrained()
246            .with_width(style.button_width)
247            .with_height(nav_button_height)
248            .aligned()
249            .top()
250    })
251    .with_cursor_style(if enabled {
252        CursorStyle::PointingHand
253    } else {
254        CursorStyle::default()
255    })
256    .on_click(MouseButton::Left, move |_, toolbar, cx| {
257        on_click(toolbar, cx)
258    })
259    .with_tooltip::<A>(
260        0,
261        action_name.to_string(),
262        Some(Box::new(tooltip_action)),
263        tooltip_style,
264        cx,
265    )
266    .contained()
267    .with_margin_right(spacing)
268    .into_any_named("nav button")
269}
270
271impl Toolbar {
272    pub fn new(pane: WeakViewHandle<Pane>) -> Self {
273        Self {
274            active_pane_item: None,
275            pane,
276            items: Default::default(),
277            hidden: false,
278            can_navigate: true,
279        }
280    }
281
282    pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
283        self.can_navigate = can_navigate;
284        cx.notify();
285    }
286
287    pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
288    where
289        T: 'static + ToolbarItemView,
290    {
291        let location = item.set_active_pane_item(self.active_pane_item.as_deref(), cx);
292        cx.subscribe(&item, |this, item, event, cx| {
293            if let Some((_, current_location)) =
294                this.items.iter_mut().find(|(i, _)| i.id() == item.id())
295            {
296                let new_location = item
297                    .read(cx)
298                    .location_for_event(event, *current_location, cx);
299                if new_location != *current_location {
300                    *current_location = new_location;
301                    cx.notify();
302                }
303            }
304        })
305        .detach();
306        self.items.push((Box::new(item), location));
307        cx.notify();
308    }
309
310    pub fn set_active_pane_item(
311        &mut self,
312        pane_item: Option<&dyn ItemHandle>,
313        cx: &mut ViewContext<Self>,
314    ) {
315        self.active_pane_item = pane_item.map(|item| item.boxed_clone());
316        self.hidden = self
317            .active_pane_item
318            .as_ref()
319            .map(|item| !item.show_toolbar(cx))
320            .unwrap_or(false);
321
322        for (toolbar_item, current_location) in self.items.iter_mut() {
323            let new_location = toolbar_item.set_active_pane_item(pane_item, cx);
324            if new_location != *current_location {
325                *current_location = new_location;
326                cx.notify();
327            }
328        }
329    }
330
331    pub fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut ViewContext<Self>) {
332        for (toolbar_item, _) in self.items.iter_mut() {
333            toolbar_item.pane_focus_update(pane_focused, cx);
334        }
335    }
336
337    pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> {
338        self.items
339            .iter()
340            .find_map(|(item, _)| item.as_any().clone().downcast())
341    }
342
343    pub fn hidden(&self) -> bool {
344        self.hidden
345    }
346}
347
348impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
349    fn id(&self) -> usize {
350        self.id()
351    }
352
353    fn as_any(&self) -> &AnyViewHandle {
354        self
355    }
356
357    fn set_active_pane_item(
358        &self,
359        active_pane_item: Option<&dyn ItemHandle>,
360        cx: &mut WindowContext,
361    ) -> ToolbarItemLocation {
362        self.update(cx, |this, cx| {
363            this.set_active_pane_item(active_pane_item, cx)
364        })
365    }
366
367    fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext) {
368        self.update(cx, |this, cx| {
369            this.pane_focus_update(pane_focused, cx);
370            cx.notify();
371        });
372    }
373
374    fn row_count(&self, cx: &WindowContext) -> usize {
375        self.read(cx).row_count()
376    }
377}
378
379impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle {
380    fn from(val: &dyn ToolbarItemViewHandle) -> Self {
381        val.as_any().clone()
382    }
383}