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