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