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    /// Number of times toolbar's height will be repeated to get the effective height.
 27    /// Useful when multiple rows one under each other are needed.
 28    /// The rows have the same width and act as a whole when reacting to resizes and similar events.
 29    fn row_count(&self) -> usize {
 30        1
 31    }
 32}
 33
 34trait ToolbarItemViewHandle {
 35    fn id(&self) -> usize;
 36    fn as_any(&self) -> &AnyViewHandle;
 37    fn set_active_pane_item(
 38        &self,
 39        active_pane_item: Option<&dyn ItemHandle>,
 40        cx: &mut WindowContext,
 41    ) -> ToolbarItemLocation;
 42    fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext);
 43    fn row_count(&self, cx: &WindowContext) -> usize;
 44}
 45
 46#[derive(Copy, Clone, Debug, PartialEq)]
 47pub enum ToolbarItemLocation {
 48    Hidden,
 49    PrimaryLeft { flex: Option<(f32, bool)> },
 50    PrimaryRight { flex: Option<(f32, bool)> },
 51    Secondary,
 52}
 53
 54pub struct Toolbar {
 55    active_pane_item: Option<Box<dyn ItemHandle>>,
 56    hidden: 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 = &cx.global::<Settings>().theme.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 = cx.global::<Settings>().theme.tooltip.clone();
135
136        Flex::column()
137            .with_child(
138                Flex::row()
139                    .with_child(nav_button(
140                        "icons/arrow_left_16.svg",
141                        button_style,
142                        nav_button_height,
143                        tooltip_style.clone(),
144                        enable_go_backward,
145                        spacing,
146                        {
147                            let pane = pane.clone();
148                            move |toolbar, cx| {
149                                if let Some(workspace) = toolbar
150                                    .pane
151                                    .upgrade(cx)
152                                    .and_then(|pane| pane.read(cx).workspace().upgrade(cx))
153                                {
154                                    let pane = pane.clone();
155                                    cx.window_context().defer(move |cx| {
156                                        workspace.update(cx, |workspace, cx| {
157                                            Pane::go_back(workspace, Some(pane.clone()), cx)
158                                                .detach_and_log_err(cx);
159                                        });
160                                    })
161                                }
162                            }
163                        },
164                        super::GoBack { pane: None },
165                        "Go Back",
166                        cx,
167                    ))
168                    .with_child(nav_button(
169                        "icons/arrow_right_16.svg",
170                        button_style,
171                        nav_button_height,
172                        tooltip_style,
173                        enable_go_forward,
174                        spacing,
175                        {
176                            let pane = pane.clone();
177                            move |toolbar, cx| {
178                                if let Some(workspace) = toolbar
179                                    .pane
180                                    .upgrade(cx)
181                                    .and_then(|pane| pane.read(cx).workspace().upgrade(cx))
182                                {
183                                    let pane = pane.clone();
184                                    cx.window_context().defer(move |cx| {
185                                        workspace.update(cx, |workspace, cx| {
186                                            Pane::go_forward(workspace, Some(pane.clone()), cx)
187                                                .detach_and_log_err(cx);
188                                        });
189                                    });
190                                }
191                            }
192                        },
193                        super::GoForward { pane: None },
194                        "Go Forward",
195                        cx,
196                    ))
197                    .with_children(primary_left_items)
198                    .with_children(primary_right_items)
199                    .constrained()
200                    .with_height(height),
201            )
202            .with_children(secondary_item)
203            .contained()
204            .with_style(container_style)
205            .into_any_named("toolbar")
206    }
207}
208
209#[allow(clippy::too_many_arguments)]
210fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
211    svg_path: &'static str,
212    style: theme::Interactive<theme::IconButton>,
213    nav_button_height: f32,
214    tooltip_style: TooltipStyle,
215    enabled: bool,
216    spacing: f32,
217    on_click: F,
218    tooltip_action: A,
219    action_name: &str,
220    cx: &mut ViewContext<Toolbar>,
221) -> AnyElement<Toolbar> {
222    MouseEventHandler::<A, _>::new(0, cx, |state, _| {
223        let style = if enabled {
224            style.style_for(state, false)
225        } else {
226            style.disabled_style()
227        };
228        Svg::new(svg_path)
229            .with_color(style.color)
230            .constrained()
231            .with_width(style.icon_width)
232            .aligned()
233            .contained()
234            .with_style(style.container)
235            .constrained()
236            .with_width(style.button_width)
237            .with_height(nav_button_height)
238            .aligned()
239            .top()
240    })
241    .with_cursor_style(if enabled {
242        CursorStyle::PointingHand
243    } else {
244        CursorStyle::default()
245    })
246    .on_click(MouseButton::Left, move |_, toolbar, cx| {
247        on_click(toolbar, cx)
248    })
249    .with_tooltip::<A>(
250        0,
251        action_name.to_string(),
252        Some(Box::new(tooltip_action)),
253        tooltip_style,
254        cx,
255    )
256    .contained()
257    .with_margin_right(spacing)
258    .into_any_named("nav button")
259}
260
261impl Toolbar {
262    pub fn new(pane: WeakViewHandle<Pane>) -> Self {
263        Self {
264            active_pane_item: None,
265            pane,
266            items: Default::default(),
267            hidden: false,
268        }
269    }
270
271    pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
272    where
273        T: 'static + ToolbarItemView,
274    {
275        let location = item.set_active_pane_item(self.active_pane_item.as_deref(), cx);
276        cx.subscribe(&item, |this, item, event, cx| {
277            if let Some((_, current_location)) =
278                this.items.iter_mut().find(|(i, _)| i.id() == item.id())
279            {
280                let new_location = item
281                    .read(cx)
282                    .location_for_event(event, *current_location, cx);
283                if new_location != *current_location {
284                    *current_location = new_location;
285                    cx.notify();
286                }
287            }
288        })
289        .detach();
290        self.items.push((Box::new(item), location));
291        cx.notify();
292    }
293
294    pub fn set_active_pane_item(
295        &mut self,
296        pane_item: Option<&dyn ItemHandle>,
297        cx: &mut ViewContext<Self>,
298    ) {
299        self.active_pane_item = pane_item.map(|item| item.boxed_clone());
300        self.hidden = self
301            .active_pane_item
302            .as_ref()
303            .map(|item| !item.show_toolbar(cx))
304            .unwrap_or(false);
305
306        for (toolbar_item, current_location) in self.items.iter_mut() {
307            let new_location = toolbar_item.set_active_pane_item(pane_item, cx);
308            if new_location != *current_location {
309                *current_location = new_location;
310                cx.notify();
311            }
312        }
313    }
314
315    pub fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut ViewContext<Self>) {
316        for (toolbar_item, _) in self.items.iter_mut() {
317            toolbar_item.pane_focus_update(pane_focused, cx);
318        }
319    }
320
321    pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> {
322        self.items
323            .iter()
324            .find_map(|(item, _)| item.as_any().clone().downcast())
325    }
326
327    pub fn hidden(&self) -> bool {
328        self.hidden
329    }
330}
331
332impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
333    fn id(&self) -> usize {
334        self.id()
335    }
336
337    fn as_any(&self) -> &AnyViewHandle {
338        self
339    }
340
341    fn set_active_pane_item(
342        &self,
343        active_pane_item: Option<&dyn ItemHandle>,
344        cx: &mut WindowContext,
345    ) -> ToolbarItemLocation {
346        self.update(cx, |this, cx| {
347            this.set_active_pane_item(active_pane_item, cx)
348        })
349    }
350
351    fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext) {
352        self.update(cx, |this, cx| {
353            this.pane_focus_update(pane_focused, cx);
354            cx.notify();
355        });
356    }
357
358    fn row_count(&self, cx: &WindowContext) -> usize {
359        self.read(cx).row_count()
360    }
361}
362
363impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle {
364    fn from(val: &dyn ToolbarItemViewHandle) -> Self {
365        val.as_any().clone()
366    }
367}