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