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