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 pane: WeakViewHandle<Pane>,
57 items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
58}
59
60impl Entity for Toolbar {
61 type Event = ();
62}
63
64impl View for Toolbar {
65 fn ui_name() -> &'static str {
66 "Toolbar"
67 }
68
69 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
70 let theme = &theme::current(cx).workspace.toolbar;
71
72 let mut primary_left_items = Vec::new();
73 let mut primary_right_items = Vec::new();
74 let mut secondary_item = None;
75 let spacing = theme.item_spacing;
76 let mut primary_items_row_count = 1;
77
78 for (item, position) in &self.items {
79 match *position {
80 ToolbarItemLocation::Hidden => {}
81
82 ToolbarItemLocation::PrimaryLeft { flex } => {
83 primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
84 let left_item = ChildView::new(item.as_any(), cx)
85 .aligned()
86 .contained()
87 .with_margin_right(spacing);
88 if let Some((flex, expanded)) = flex {
89 primary_left_items.push(left_item.flex(flex, expanded).into_any());
90 } else {
91 primary_left_items.push(left_item.into_any());
92 }
93 }
94
95 ToolbarItemLocation::PrimaryRight { flex } => {
96 primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
97 let right_item = ChildView::new(item.as_any(), cx)
98 .aligned()
99 .contained()
100 .with_margin_left(spacing)
101 .flex_float();
102 if let Some((flex, expanded)) = flex {
103 primary_right_items.push(right_item.flex(flex, expanded).into_any());
104 } else {
105 primary_right_items.push(right_item.into_any());
106 }
107 }
108
109 ToolbarItemLocation::Secondary => {
110 secondary_item = Some(
111 ChildView::new(item.as_any(), cx)
112 .constrained()
113 .with_height(theme.height * item.row_count(cx) as f32)
114 .into_any(),
115 );
116 }
117 }
118 }
119
120 let pane = self.pane.clone();
121 let mut enable_go_backward = false;
122 let mut enable_go_forward = false;
123 if let Some(pane) = pane.upgrade(cx) {
124 let pane = pane.read(cx);
125 enable_go_backward = pane.can_navigate_backward();
126 enable_go_forward = pane.can_navigate_forward();
127 }
128
129 let container_style = theme.container;
130 let height = theme.height * primary_items_row_count as f32;
131 let nav_button_height = theme.height;
132 let button_style = theme.nav_button;
133 let tooltip_style = theme::current(cx).tooltip.clone();
134
135 Flex::column()
136 .with_child(
137 Flex::row()
138 .with_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 Pane::go_back(workspace, Some(pane.clone()), cx)
157 .detach_and_log_err(cx);
158 });
159 })
160 }
161 }
162 },
163 super::GoBack { pane: None },
164 "Go Back",
165 cx,
166 ))
167 .with_child(nav_button(
168 "icons/arrow_right_16.svg",
169 button_style,
170 nav_button_height,
171 tooltip_style,
172 enable_go_forward,
173 spacing,
174 {
175 let pane = pane.clone();
176 move |toolbar, cx| {
177 if let Some(workspace) = toolbar
178 .pane
179 .upgrade(cx)
180 .and_then(|pane| pane.read(cx).workspace().upgrade(cx))
181 {
182 let pane = pane.clone();
183 cx.window_context().defer(move |cx| {
184 workspace.update(cx, |workspace, cx| {
185 Pane::go_forward(workspace, Some(pane.clone()), cx)
186 .detach_and_log_err(cx);
187 });
188 });
189 }
190 }
191 },
192 super::GoForward { pane: None },
193 "Go Forward",
194 cx,
195 ))
196 .with_children(primary_left_items)
197 .with_children(primary_right_items)
198 .constrained()
199 .with_height(height),
200 )
201 .with_children(secondary_item)
202 .contained()
203 .with_style(container_style)
204 .into_any_named("toolbar")
205 }
206}
207
208#[allow(clippy::too_many_arguments)]
209fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
210 svg_path: &'static str,
211 style: theme::Interactive<theme::IconButton>,
212 nav_button_height: f32,
213 tooltip_style: TooltipStyle,
214 enabled: bool,
215 spacing: f32,
216 on_click: F,
217 tooltip_action: A,
218 action_name: &str,
219 cx: &mut ViewContext<Toolbar>,
220) -> AnyElement<Toolbar> {
221 MouseEventHandler::<A, _>::new(0, cx, |state, _| {
222 let style = if enabled {
223 style.style_for(state, false)
224 } else {
225 style.disabled_style()
226 };
227 Svg::new(svg_path)
228 .with_color(style.color)
229 .constrained()
230 .with_width(style.icon_width)
231 .aligned()
232 .contained()
233 .with_style(style.container)
234 .constrained()
235 .with_width(style.button_width)
236 .with_height(nav_button_height)
237 .aligned()
238 .top()
239 })
240 .with_cursor_style(if enabled {
241 CursorStyle::PointingHand
242 } else {
243 CursorStyle::default()
244 })
245 .on_click(MouseButton::Left, move |_, toolbar, cx| {
246 on_click(toolbar, cx)
247 })
248 .with_tooltip::<A>(
249 0,
250 action_name.to_string(),
251 Some(Box::new(tooltip_action)),
252 tooltip_style,
253 cx,
254 )
255 .contained()
256 .with_margin_right(spacing)
257 .into_any_named("nav button")
258}
259
260impl Toolbar {
261 pub fn new(pane: WeakViewHandle<Pane>) -> Self {
262 Self {
263 active_pane_item: None,
264 pane,
265 items: Default::default(),
266 hidden: false,
267 }
268 }
269
270 pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
271 where
272 T: 'static + ToolbarItemView,
273 {
274 let location = item.set_active_pane_item(self.active_pane_item.as_deref(), cx);
275 cx.subscribe(&item, |this, item, event, cx| {
276 if let Some((_, current_location)) =
277 this.items.iter_mut().find(|(i, _)| i.id() == item.id())
278 {
279 let new_location = item
280 .read(cx)
281 .location_for_event(event, *current_location, cx);
282 if new_location != *current_location {
283 *current_location = new_location;
284 cx.notify();
285 }
286 }
287 })
288 .detach();
289 self.items.push((Box::new(item), location));
290 cx.notify();
291 }
292
293 pub fn set_active_pane_item(
294 &mut self,
295 pane_item: Option<&dyn ItemHandle>,
296 cx: &mut ViewContext<Self>,
297 ) {
298 self.active_pane_item = pane_item.map(|item| item.boxed_clone());
299 self.hidden = self
300 .active_pane_item
301 .as_ref()
302 .map(|item| !item.show_toolbar(cx))
303 .unwrap_or(false);
304
305 for (toolbar_item, current_location) in self.items.iter_mut() {
306 let new_location = toolbar_item.set_active_pane_item(pane_item, cx);
307 if new_location != *current_location {
308 *current_location = new_location;
309 cx.notify();
310 }
311 }
312 }
313
314 pub fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut ViewContext<Self>) {
315 for (toolbar_item, _) in self.items.iter_mut() {
316 toolbar_item.pane_focus_update(pane_focused, cx);
317 }
318 }
319
320 pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> {
321 self.items
322 .iter()
323 .find_map(|(item, _)| item.as_any().clone().downcast())
324 }
325
326 pub fn hidden(&self) -> bool {
327 self.hidden
328 }
329}
330
331impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
332 fn id(&self) -> usize {
333 self.id()
334 }
335
336 fn as_any(&self) -> &AnyViewHandle {
337 self
338 }
339
340 fn set_active_pane_item(
341 &self,
342 active_pane_item: Option<&dyn ItemHandle>,
343 cx: &mut WindowContext,
344 ) -> ToolbarItemLocation {
345 self.update(cx, |this, cx| {
346 this.set_active_pane_item(active_pane_item, cx)
347 })
348 }
349
350 fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext) {
351 self.update(cx, |this, cx| {
352 this.pane_focus_update(pane_focused, cx);
353 cx.notify();
354 });
355 }
356
357 fn row_count(&self, cx: &WindowContext) -> usize {
358 self.read(cx).row_count()
359 }
360}
361
362impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle {
363 fn from(val: &dyn ToolbarItemViewHandle) -> Self {
364 val.as_any().clone()
365 }
366}