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