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            .py(DynamicSpacing::Base06.rems(cx))
114            .px(DynamicSpacing::Base08.rems(cx))
115            .when(has_left_items || has_right_items, |this| {
116                this.gap(DynamicSpacing::Base06.rems(cx))
117            })
118            .border_b_1()
119            .border_color(cx.theme().colors().border_variant)
120            .bg(cx.theme().colors().toolbar_background)
121            .when(has_left_items || has_right_items, |this| {
122                this.child(
123                    h_flex()
124                        .items_start()
125                        .justify_between()
126                        .gap(DynamicSpacing::Base08.rems(cx))
127                        .when(has_left_items, |this| {
128                            this.child(
129                                h_flex()
130                                    .min_h_8()
131                                    .flex_auto()
132                                    .justify_start()
133                                    .overflow_x_hidden()
134                                    .children(self.left_items().map(|item| item.to_any())),
135                            )
136                        })
137                        .when(has_right_items, |this| {
138                            this.child(
139                                h_flex()
140                                    .h_8()
141                                    .flex_row_reverse()
142                                    .when(has_left_items, |this| this.flex_none())
143                                    .justify_end()
144                                    .children(self.right_items().map(|item| item.to_any())),
145                            )
146                        }),
147                )
148            })
149            .children(secondary_items)
150    }
151}
152
153impl Default for Toolbar {
154    fn default() -> Self {
155        Self::new()
156    }
157}
158
159impl Toolbar {
160    pub fn new() -> Self {
161        Self {
162            active_item: None,
163            items: Default::default(),
164            hidden: false,
165            can_navigate: true,
166        }
167    }
168
169    pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut Context<Self>) {
170        self.can_navigate = can_navigate;
171        cx.notify();
172    }
173
174    pub fn add_item<T>(&mut self, item: Entity<T>, window: &mut Window, cx: &mut Context<Self>)
175    where
176        T: 'static + ToolbarItemView,
177    {
178        let location = item.set_active_pane_item(self.active_item.as_deref(), window, cx);
179        cx.subscribe(&item, |this, item, event, cx| {
180            if let Some((_, current_location)) = this
181                .items
182                .iter_mut()
183                .find(|(i, _)| i.id() == item.entity_id())
184            {
185                match event {
186                    ToolbarItemEvent::ChangeLocation(new_location) => {
187                        if new_location != current_location {
188                            *current_location = *new_location;
189                            cx.notify();
190                        }
191                    }
192                }
193            }
194        })
195        .detach();
196        self.items.push((Box::new(item), location));
197        cx.notify();
198    }
199
200    pub fn set_active_item(
201        &mut self,
202        item: Option<&dyn ItemHandle>,
203        window: &mut Window,
204        cx: &mut Context<Self>,
205    ) {
206        self.active_item = item.map(|item| item.boxed_clone());
207        self.hidden = self
208            .active_item
209            .as_ref()
210            .map(|item| !item.show_toolbar(cx))
211            .unwrap_or(false);
212
213        for (toolbar_item, current_location) in self.items.iter_mut() {
214            let new_location = toolbar_item.set_active_pane_item(item, window, cx);
215            if new_location != *current_location {
216                *current_location = new_location;
217                cx.notify();
218            }
219        }
220    }
221
222    pub fn focus_changed(&mut self, focused: bool, window: &mut Window, cx: &mut Context<Self>) {
223        for (toolbar_item, _) in self.items.iter_mut() {
224            toolbar_item.focus_changed(focused, window, cx);
225        }
226    }
227
228    pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<Entity<T>> {
229        self.items
230            .iter()
231            .find_map(|(item, _)| item.to_any().downcast().ok())
232    }
233
234    pub fn hidden(&self) -> bool {
235        self.hidden
236    }
237
238    pub fn contribute_context(&self, context: &mut KeyContext, cx: &App) {
239        for (item, location) in &self.items {
240            if *location != ToolbarItemLocation::Hidden {
241                item.contribute_context(context, cx);
242            }
243        }
244    }
245}
246
247impl<T: ToolbarItemView> ToolbarItemViewHandle for Entity<T> {
248    fn id(&self) -> EntityId {
249        self.entity_id()
250    }
251
252    fn to_any(&self) -> AnyView {
253        self.clone().into()
254    }
255
256    fn set_active_pane_item(
257        &self,
258        active_pane_item: Option<&dyn ItemHandle>,
259        window: &mut Window,
260        cx: &mut App,
261    ) -> ToolbarItemLocation {
262        self.update(cx, |this, cx| {
263            this.set_active_pane_item(active_pane_item, window, cx)
264        })
265    }
266
267    fn focus_changed(&mut self, pane_focused: bool, window: &mut Window, cx: &mut App) {
268        self.update(cx, |this, cx| {
269            this.pane_focus_update(pane_focused, window, cx);
270            cx.notify();
271        });
272    }
273
274    fn contribute_context(&self, context: &mut KeyContext, cx: &App) {
275        self.read(cx).contribute_context(context, cx)
276    }
277}