toolbar.rs

  1use crate::ItemHandle;
  2use gpui::{
  3    AnyView, App, Context, Entity, EntityId, EventEmitter, KeyContext, ParentElement as _, Render,
  4    Styled, Window,
  5};
  6use ui::prelude::*;
  7use ui::{h_flex, v_flex};
  8
  9#[derive(Copy, Clone, Debug, PartialEq)]
 10pub enum ToolbarItemEvent {
 11    ChangeLocation(ToolbarItemLocation),
 12}
 13
 14pub trait ToolbarItemView: Render + EventEmitter<ToolbarItemEvent> {
 15    fn set_active_pane_item(
 16        &mut self,
 17        active_pane_item: Option<&dyn crate::ItemHandle>,
 18        window: &mut Window,
 19        cx: &mut Context<Self>,
 20    ) -> ToolbarItemLocation;
 21
 22    fn pane_focus_update(
 23        &mut self,
 24        _pane_focused: bool,
 25        _window: &mut Window,
 26        _cx: &mut Context<Self>,
 27    ) {
 28    }
 29
 30    fn contribute_context(&self, _context: &mut KeyContext, _cx: &App) {}
 31}
 32
 33trait ToolbarItemViewHandle: Send {
 34    fn id(&self) -> EntityId;
 35    fn to_any(&self) -> AnyView;
 36    fn set_active_pane_item(
 37        &self,
 38        active_pane_item: Option<&dyn ItemHandle>,
 39        window: &mut Window,
 40        cx: &mut App,
 41    ) -> ToolbarItemLocation;
 42    fn focus_changed(&mut self, pane_focused: bool, window: &mut Window, cx: &mut App);
 43    fn contribute_context(&self, context: &mut KeyContext, cx: &App);
 44}
 45
 46#[derive(Copy, Clone, Debug, PartialEq)]
 47pub enum ToolbarItemLocation {
 48    Hidden,
 49    PrimaryLeft,
 50    PrimaryRight,
 51    Secondary,
 52}
 53
 54pub struct Toolbar {
 55    active_item: Option<Box<dyn ItemHandle>>,
 56    hidden: bool,
 57    can_navigate: bool,
 58    items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
 59}
 60
 61impl Toolbar {
 62    fn has_any_visible_items(&self) -> bool {
 63        self.items
 64            .iter()
 65            .any(|(_item, location)| *location != ToolbarItemLocation::Hidden)
 66    }
 67
 68    fn left_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> {
 69        self.items.iter().filter_map(|(item, location)| {
 70            if *location == ToolbarItemLocation::PrimaryLeft {
 71                Some(item.as_ref())
 72            } else {
 73                None
 74            }
 75        })
 76    }
 77
 78    fn right_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> {
 79        self.items.iter().filter_map(|(item, location)| {
 80            if *location == ToolbarItemLocation::PrimaryRight {
 81                Some(item.as_ref())
 82            } else {
 83                None
 84            }
 85        })
 86    }
 87
 88    fn secondary_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> {
 89        self.items.iter().rev().filter_map(|(item, location)| {
 90            if *location == ToolbarItemLocation::Secondary {
 91                Some(item.as_ref())
 92            } else {
 93                None
 94            }
 95        })
 96    }
 97}
 98
 99impl Render for Toolbar {
100    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
101        if !self.has_any_visible_items() {
102            return div();
103        }
104
105        let secondary_items = self.secondary_items().map(|item| item.to_any());
106
107        let has_left_items = self.left_items().count() > 0;
108        let has_right_items = self.right_items().count() > 0;
109
110        v_flex()
111            .group("toolbar")
112            .relative()
113            .p(DynamicSpacing::Base08.rems(cx))
114            .when(has_left_items || has_right_items, |this| {
115                this.gap(DynamicSpacing::Base08.rems(cx))
116            })
117            .border_b_1()
118            .border_color(cx.theme().colors().border_variant)
119            .bg(cx.theme().colors().toolbar_background)
120            .when(has_left_items || has_right_items, |this| {
121                this.child(
122                    h_flex()
123                        .items_start()
124                        .justify_between()
125                        .gap(DynamicSpacing::Base08.rems(cx))
126                        .when(has_left_items, |this| {
127                            this.child(
128                                h_flex()
129                                    .min_h_8()
130                                    .flex_auto()
131                                    .justify_start()
132                                    .overflow_x_hidden()
133                                    .children(self.left_items().map(|item| item.to_any())),
134                            )
135                        })
136                        .when(has_right_items, |this| {
137                            this.child(
138                                h_flex()
139                                    .h_8()
140                                    .flex_row_reverse()
141                                    .map(|el| {
142                                        if has_left_items {
143                                            // We're using `flex_none` here to prevent some flickering that can occur when the
144                                            // size of the left items container changes.
145                                            el.flex_none()
146                                        } else {
147                                            el.flex_auto()
148                                        }
149                                    })
150                                    .justify_end()
151                                    .children(self.right_items().map(|item| item.to_any())),
152                            )
153                        }),
154                )
155            })
156            .children(secondary_items)
157    }
158}
159
160impl Default for Toolbar {
161    fn default() -> Self {
162        Self::new()
163    }
164}
165
166impl Toolbar {
167    pub fn new() -> Self {
168        Self {
169            active_item: None,
170            items: Default::default(),
171            hidden: false,
172            can_navigate: true,
173        }
174    }
175
176    pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut Context<Self>) {
177        self.can_navigate = can_navigate;
178        cx.notify();
179    }
180
181    pub fn add_item<T>(&mut self, item: Entity<T>, window: &mut Window, cx: &mut Context<Self>)
182    where
183        T: 'static + ToolbarItemView,
184    {
185        let location = item.set_active_pane_item(self.active_item.as_deref(), window, cx);
186        cx.subscribe(&item, |this, item, event, cx| {
187            if let Some((_, current_location)) = this
188                .items
189                .iter_mut()
190                .find(|(i, _)| i.id() == item.entity_id())
191            {
192                match event {
193                    ToolbarItemEvent::ChangeLocation(new_location) => {
194                        if new_location != current_location {
195                            *current_location = *new_location;
196                            cx.notify();
197                        }
198                    }
199                }
200            }
201        })
202        .detach();
203        self.items.push((Box::new(item), location));
204        cx.notify();
205    }
206
207    pub fn set_active_item(
208        &mut self,
209        item: Option<&dyn ItemHandle>,
210        window: &mut Window,
211        cx: &mut Context<Self>,
212    ) {
213        self.active_item = item.map(|item| item.boxed_clone());
214        self.hidden = self
215            .active_item
216            .as_ref()
217            .map(|item| !item.show_toolbar(cx))
218            .unwrap_or(false);
219
220        for (toolbar_item, current_location) in self.items.iter_mut() {
221            let new_location = toolbar_item.set_active_pane_item(item, window, cx);
222            if new_location != *current_location {
223                *current_location = new_location;
224                cx.notify();
225            }
226        }
227    }
228
229    pub fn focus_changed(&mut self, focused: bool, window: &mut Window, cx: &mut Context<Self>) {
230        for (toolbar_item, _) in self.items.iter_mut() {
231            toolbar_item.focus_changed(focused, window, cx);
232        }
233    }
234
235    pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<Entity<T>> {
236        self.items
237            .iter()
238            .find_map(|(item, _)| item.to_any().downcast().ok())
239    }
240
241    pub fn hidden(&self) -> bool {
242        self.hidden
243    }
244
245    pub fn contribute_context(&self, context: &mut KeyContext, cx: &App) {
246        for (item, location) in &self.items {
247            if *location != ToolbarItemLocation::Hidden {
248                item.contribute_context(context, cx);
249            }
250        }
251    }
252}
253
254impl<T: ToolbarItemView> ToolbarItemViewHandle for Entity<T> {
255    fn id(&self) -> EntityId {
256        self.entity_id()
257    }
258
259    fn to_any(&self) -> AnyView {
260        self.clone().into()
261    }
262
263    fn set_active_pane_item(
264        &self,
265        active_pane_item: Option<&dyn ItemHandle>,
266        window: &mut Window,
267        cx: &mut App,
268    ) -> ToolbarItemLocation {
269        self.update(cx, |this, cx| {
270            this.set_active_pane_item(active_pane_item, window, cx)
271        })
272    }
273
274    fn focus_changed(&mut self, pane_focused: bool, window: &mut Window, cx: &mut App) {
275        self.update(cx, |this, cx| {
276            this.pane_focus_update(pane_focused, window, cx);
277            cx.notify();
278        });
279    }
280
281    fn contribute_context(&self, context: &mut KeyContext, cx: &App) {
282        self.read(cx).contribute_context(context, cx)
283    }
284}