1use crate::{ItemHandle, Pane};
2use gpui::{
3 elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyElement, AnyViewHandle,
4 AppContext, Entity, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
5};
6
7pub trait ToolbarItemView: View {
8 fn set_active_pane_item(
9 &mut self,
10 active_pane_item: Option<&dyn crate::ItemHandle>,
11 cx: &mut ViewContext<Self>,
12 ) -> ToolbarItemLocation;
13
14 fn location_for_event(
15 &self,
16 _event: &Self::Event,
17 current_location: ToolbarItemLocation,
18 _cx: &AppContext,
19 ) -> ToolbarItemLocation {
20 current_location
21 }
22
23 fn pane_focus_update(&mut self, _pane_focused: bool, _cx: &mut ViewContext<Self>) {}
24
25 /// Number of times toolbar's height will be repeated to get the effective height.
26 /// Useful when multiple rows one under each other are needed.
27 /// The rows have the same width and act as a whole when reacting to resizes and similar events.
28 fn row_count(&self) -> usize {
29 1
30 }
31}
32
33trait ToolbarItemViewHandle {
34 fn id(&self) -> usize;
35 fn as_any(&self) -> &AnyViewHandle;
36 fn set_active_pane_item(
37 &self,
38 active_pane_item: Option<&dyn ItemHandle>,
39 cx: &mut WindowContext,
40 ) -> ToolbarItemLocation;
41 fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext);
42 fn row_count(&self, cx: &WindowContext) -> usize;
43}
44
45#[derive(Copy, Clone, Debug, PartialEq)]
46pub enum ToolbarItemLocation {
47 Hidden,
48 PrimaryLeft { flex: Option<(f32, bool)> },
49 PrimaryRight { flex: Option<(f32, bool)> },
50 Secondary,
51}
52
53pub struct Toolbar {
54 active_pane_item: Option<Box<dyn ItemHandle>>,
55 hidden: bool,
56 can_navigate: bool,
57 pane: WeakViewHandle<Pane>,
58 items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
59}
60
61impl Entity for Toolbar {
62 type Event = ();
63}
64
65impl View for Toolbar {
66 fn ui_name() -> &'static str {
67 "Toolbar"
68 }
69
70 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
71 let theme = &theme::current(cx).workspace.toolbar;
72
73 let mut primary_left_items = Vec::new();
74 let mut primary_right_items = Vec::new();
75 let mut secondary_item = None;
76 let spacing = theme.item_spacing;
77 let mut primary_items_row_count = 1;
78
79 for (item, position) in &self.items {
80 match *position {
81 ToolbarItemLocation::Hidden => {}
82
83 ToolbarItemLocation::PrimaryLeft { flex } => {
84 primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
85 let left_item = ChildView::new(item.as_any(), cx)
86 .aligned()
87 .contained()
88 .with_margin_right(spacing);
89 if let Some((flex, expanded)) = flex {
90 primary_left_items.push(left_item.flex(flex, expanded).into_any());
91 } else {
92 primary_left_items.push(left_item.into_any());
93 }
94 }
95
96 ToolbarItemLocation::PrimaryRight { flex } => {
97 primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
98 let right_item = ChildView::new(item.as_any(), cx)
99 .aligned()
100 .contained()
101 .with_margin_left(spacing)
102 .flex_float();
103 if let Some((flex, expanded)) = flex {
104 primary_right_items.push(right_item.flex(flex, expanded).into_any());
105 } else {
106 primary_right_items.push(right_item.into_any());
107 }
108 }
109
110 ToolbarItemLocation::Secondary => {
111 secondary_item = Some(
112 ChildView::new(item.as_any(), cx)
113 .constrained()
114 .with_height(theme.height * item.row_count(cx) as f32)
115 .into_any(),
116 );
117 }
118 }
119 }
120
121 let pane = self.pane.clone();
122 let mut enable_go_backward = false;
123 let mut enable_go_forward = false;
124 if let Some(pane) = pane.upgrade(cx) {
125 let pane = pane.read(cx);
126 enable_go_backward = pane.can_navigate_backward();
127 enable_go_forward = pane.can_navigate_forward();
128 }
129
130 let container_style = theme.container;
131 let height = theme.height * primary_items_row_count as f32;
132 let nav_button_height = theme.height;
133 let button_style = theme.nav_button;
134 let tooltip_style = theme::current(cx).tooltip.clone();
135
136 let mut primary_items = Flex::row();
137 if self.can_navigate {
138 primary_items.add_child(nav_button(
139 "icons/arrow_left_16.svg",
140 button_style,
141 nav_button_height,
142 tooltip_style.clone(),
143 enable_go_backward,
144 spacing,
145 {
146 let pane = pane.clone();
147 move |toolbar, cx| {
148 if let Some(workspace) = toolbar
149 .pane
150 .upgrade(cx)
151 .and_then(|pane| pane.read(cx).workspace().upgrade(cx))
152 {
153 let pane = pane.clone();
154 cx.window_context().defer(move |cx| {
155 workspace.update(cx, |workspace, cx| {
156 workspace.go_back(pane.clone(), cx).detach_and_log_err(cx);
157 });
158 })
159 }
160 }
161 },
162 super::GoBack,
163 "Go Back",
164 cx,
165 ));
166 primary_items.add_child(nav_button(
167 "icons/arrow_right_16.svg",
168 button_style,
169 nav_button_height,
170 tooltip_style,
171 enable_go_forward,
172 spacing,
173 {
174 let pane = pane.clone();
175 move |toolbar, cx| {
176 if let Some(workspace) = toolbar
177 .pane
178 .upgrade(cx)
179 .and_then(|pane| pane.read(cx).workspace().upgrade(cx))
180 {
181 let pane = pane.clone();
182 cx.window_context().defer(move |cx| {
183 workspace.update(cx, |workspace, cx| {
184 workspace
185 .go_forward(pane.clone(), cx)
186 .detach_and_log_err(cx);
187 });
188 });
189 }
190 }
191 },
192 super::GoForward,
193 "Go Forward",
194 cx,
195 ));
196 }
197 primary_items.extend(primary_left_items);
198 primary_items.extend(primary_right_items);
199
200 let mut toolbar = Flex::column();
201 if !primary_items.is_empty() {
202 toolbar.add_child(primary_items.constrained().with_height(height));
203 }
204 if let Some(secondary_item) = secondary_item {
205 toolbar.add_child(secondary_item);
206 }
207
208 if toolbar.is_empty() {
209 toolbar.into_any_named("toolbar")
210 } else {
211 toolbar
212 .contained()
213 .with_style(container_style)
214 .into_any_named("toolbar")
215 }
216 }
217}
218
219#[allow(clippy::too_many_arguments)]
220fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
221 svg_path: &'static str,
222 style: theme::Interactive<theme::IconButton>,
223 nav_button_height: f32,
224 tooltip_style: TooltipStyle,
225 enabled: bool,
226 spacing: f32,
227 on_click: F,
228 tooltip_action: A,
229 action_name: &str,
230 cx: &mut ViewContext<Toolbar>,
231) -> AnyElement<Toolbar> {
232 MouseEventHandler::<A, _>::new(0, cx, |state, _| {
233 let style = if enabled {
234 style.style_for(state, false)
235 } else {
236 style.disabled_style()
237 };
238 Svg::new(svg_path)
239 .with_color(style.color)
240 .constrained()
241 .with_width(style.icon_width)
242 .aligned()
243 .contained()
244 .with_style(style.container)
245 .constrained()
246 .with_width(style.button_width)
247 .with_height(nav_button_height)
248 .aligned()
249 .top()
250 })
251 .with_cursor_style(if enabled {
252 CursorStyle::PointingHand
253 } else {
254 CursorStyle::default()
255 })
256 .on_click(MouseButton::Left, move |_, toolbar, cx| {
257 on_click(toolbar, cx)
258 })
259 .with_tooltip::<A>(
260 0,
261 action_name.to_string(),
262 Some(Box::new(tooltip_action)),
263 tooltip_style,
264 cx,
265 )
266 .contained()
267 .with_margin_right(spacing)
268 .into_any_named("nav button")
269}
270
271impl Toolbar {
272 pub fn new(pane: WeakViewHandle<Pane>) -> Self {
273 Self {
274 active_pane_item: None,
275 pane,
276 items: Default::default(),
277 hidden: false,
278 can_navigate: true,
279 }
280 }
281
282 pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
283 self.can_navigate = can_navigate;
284 cx.notify();
285 }
286
287 pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
288 where
289 T: 'static + ToolbarItemView,
290 {
291 let location = item.set_active_pane_item(self.active_pane_item.as_deref(), cx);
292 cx.subscribe(&item, |this, item, event, cx| {
293 if let Some((_, current_location)) =
294 this.items.iter_mut().find(|(i, _)| i.id() == item.id())
295 {
296 let new_location = item
297 .read(cx)
298 .location_for_event(event, *current_location, cx);
299 if new_location != *current_location {
300 *current_location = new_location;
301 cx.notify();
302 }
303 }
304 })
305 .detach();
306 self.items.push((Box::new(item), location));
307 cx.notify();
308 }
309
310 pub fn set_active_pane_item(
311 &mut self,
312 pane_item: Option<&dyn ItemHandle>,
313 cx: &mut ViewContext<Self>,
314 ) {
315 self.active_pane_item = pane_item.map(|item| item.boxed_clone());
316 self.hidden = self
317 .active_pane_item
318 .as_ref()
319 .map(|item| !item.show_toolbar(cx))
320 .unwrap_or(false);
321
322 for (toolbar_item, current_location) in self.items.iter_mut() {
323 let new_location = toolbar_item.set_active_pane_item(pane_item, cx);
324 if new_location != *current_location {
325 *current_location = new_location;
326 cx.notify();
327 }
328 }
329 }
330
331 pub fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut ViewContext<Self>) {
332 for (toolbar_item, _) in self.items.iter_mut() {
333 toolbar_item.pane_focus_update(pane_focused, cx);
334 }
335 }
336
337 pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> {
338 self.items
339 .iter()
340 .find_map(|(item, _)| item.as_any().clone().downcast())
341 }
342
343 pub fn hidden(&self) -> bool {
344 self.hidden
345 }
346}
347
348impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
349 fn id(&self) -> usize {
350 self.id()
351 }
352
353 fn as_any(&self) -> &AnyViewHandle {
354 self
355 }
356
357 fn set_active_pane_item(
358 &self,
359 active_pane_item: Option<&dyn ItemHandle>,
360 cx: &mut WindowContext,
361 ) -> ToolbarItemLocation {
362 self.update(cx, |this, cx| {
363 this.set_active_pane_item(active_pane_item, cx)
364 })
365 }
366
367 fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext) {
368 self.update(cx, |this, cx| {
369 this.pane_focus_update(pane_focused, cx);
370 cx.notify();
371 });
372 }
373
374 fn row_count(&self, cx: &WindowContext) -> usize {
375 self.read(cx).row_count()
376 }
377}
378
379impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle {
380 fn from(val: &dyn ToolbarItemViewHandle) -> Self {
381 val.as_any().clone()
382 }
383}