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