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