toolbar.rs

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