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 focus_changed(&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_item: Option<Box<dyn ItemHandle>>,
 55    hidden: bool,
 56    can_navigate: bool,
 57    pane: Option<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.and_then(|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                    move |toolbar, cx| {
147                        if let Some(pane) = toolbar.pane.as_ref().and_then(|pane| pane.upgrade(cx))
148                        {
149                            if let Some(workspace) = pane.read(cx).workspace().upgrade(cx) {
150                                let pane = pane.downgrade();
151                                cx.window_context().defer(move |cx| {
152                                    workspace.update(cx, |workspace, cx| {
153                                        workspace.go_back(pane, cx).detach_and_log_err(cx);
154                                    });
155                                })
156                            }
157                        }
158                    }
159                },
160                super::GoBack,
161                "Go Back",
162                cx,
163            ));
164            primary_items.add_child(nav_button(
165                "icons/arrow_right_16.svg",
166                button_style,
167                nav_button_height,
168                tooltip_style,
169                enable_go_forward,
170                spacing,
171                {
172                    move |toolbar, cx| {
173                        if let Some(pane) = toolbar.pane.as_ref().and_then(|pane| pane.upgrade(cx))
174                        {
175                            if let Some(workspace) = pane.read(cx).workspace().upgrade(cx) {
176                                let pane = pane.downgrade();
177                                cx.window_context().defer(move |cx| {
178                                    workspace.update(cx, |workspace, cx| {
179                                        workspace.go_forward(pane, cx).detach_and_log_err(cx);
180                                    });
181                                })
182                            }
183                        }
184                    }
185                },
186                super::GoForward,
187                "Go Forward",
188                cx,
189            ));
190        }
191        primary_items.extend(primary_left_items);
192        primary_items.extend(primary_right_items);
193
194        let mut toolbar = Flex::column();
195        if !primary_items.is_empty() {
196            toolbar.add_child(primary_items.constrained().with_height(height));
197        }
198        if let Some(secondary_item) = secondary_item {
199            toolbar.add_child(secondary_item);
200        }
201
202        if toolbar.is_empty() {
203            toolbar.into_any_named("toolbar")
204        } else {
205            toolbar
206                .contained()
207                .with_style(container_style)
208                .into_any_named("toolbar")
209        }
210    }
211}
212
213#[allow(clippy::too_many_arguments)]
214fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
215    svg_path: &'static str,
216    style: theme::Interactive<theme::IconButton>,
217    nav_button_height: f32,
218    tooltip_style: TooltipStyle,
219    enabled: bool,
220    spacing: f32,
221    on_click: F,
222    tooltip_action: A,
223    action_name: &str,
224    cx: &mut ViewContext<Toolbar>,
225) -> AnyElement<Toolbar> {
226    MouseEventHandler::<A, _>::new(0, cx, |state, _| {
227        let style = if enabled {
228            style.style_for(state)
229        } else {
230            style.disabled_style()
231        };
232        Svg::new(svg_path)
233            .with_color(style.color)
234            .constrained()
235            .with_width(style.icon_width)
236            .aligned()
237            .contained()
238            .with_style(style.container)
239            .constrained()
240            .with_width(style.button_width)
241            .with_height(nav_button_height)
242            .aligned()
243            .top()
244    })
245    .with_cursor_style(if enabled {
246        CursorStyle::PointingHand
247    } else {
248        CursorStyle::default()
249    })
250    .on_click(MouseButton::Left, move |_, toolbar, cx| {
251        on_click(toolbar, cx)
252    })
253    .with_tooltip::<A>(
254        0,
255        action_name.to_string(),
256        Some(Box::new(tooltip_action)),
257        tooltip_style,
258        cx,
259    )
260    .contained()
261    .with_margin_right(spacing)
262    .into_any_named("nav button")
263}
264
265impl Toolbar {
266    pub fn new(pane: Option<WeakViewHandle<Pane>>) -> Self {
267        Self {
268            active_item: None,
269            pane,
270            items: Default::default(),
271            hidden: false,
272            can_navigate: true,
273        }
274    }
275
276    pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
277        self.can_navigate = can_navigate;
278        cx.notify();
279    }
280
281    pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
282    where
283        T: 'static + ToolbarItemView,
284    {
285        let location = item.set_active_pane_item(self.active_item.as_deref(), cx);
286        cx.subscribe(&item, |this, item, event, cx| {
287            if let Some((_, current_location)) =
288                this.items.iter_mut().find(|(i, _)| i.id() == item.id())
289            {
290                let new_location = item
291                    .read(cx)
292                    .location_for_event(event, *current_location, cx);
293                if new_location != *current_location {
294                    *current_location = new_location;
295                    cx.notify();
296                }
297            }
298        })
299        .detach();
300        self.items.push((Box::new(item), location));
301        cx.notify();
302    }
303
304    pub fn set_active_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
305        self.active_item = item.map(|item| item.boxed_clone());
306        self.hidden = self
307            .active_item
308            .as_ref()
309            .map(|item| !item.show_toolbar(cx))
310            .unwrap_or(false);
311
312        for (toolbar_item, current_location) in self.items.iter_mut() {
313            let new_location = toolbar_item.set_active_pane_item(item, cx);
314            if new_location != *current_location {
315                *current_location = new_location;
316                cx.notify();
317            }
318        }
319    }
320
321    pub fn focus_changed(&mut self, focused: bool, cx: &mut ViewContext<Self>) {
322        for (toolbar_item, _) in self.items.iter_mut() {
323            toolbar_item.focus_changed(focused, cx);
324        }
325    }
326
327    pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> {
328        self.items
329            .iter()
330            .find_map(|(item, _)| item.as_any().clone().downcast())
331    }
332
333    pub fn hidden(&self) -> bool {
334        self.hidden
335    }
336}
337
338impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
339    fn id(&self) -> usize {
340        self.id()
341    }
342
343    fn as_any(&self) -> &AnyViewHandle {
344        self
345    }
346
347    fn set_active_pane_item(
348        &self,
349        active_pane_item: Option<&dyn ItemHandle>,
350        cx: &mut WindowContext,
351    ) -> ToolbarItemLocation {
352        self.update(cx, |this, cx| {
353            this.set_active_pane_item(active_pane_item, cx)
354        })
355    }
356
357    fn focus_changed(&mut self, pane_focused: bool, cx: &mut WindowContext) {
358        self.update(cx, |this, cx| {
359            this.pane_focus_update(pane_focused, cx);
360            cx.notify();
361        });
362    }
363
364    fn row_count(&self, cx: &WindowContext) -> usize {
365        self.read(cx).row_count()
366    }
367}
368
369impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle {
370    fn from(val: &dyn ToolbarItemViewHandle) -> Self {
371        val.as_any().clone()
372    }
373}