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}