1use gpui::{
2 AnyElement, App, ElementId, IntoElement, ParentElement, RenderOnce, SharedString,
3 StyleRefinement, Styled, Window, div, prelude::FluentBuilder as _,
4};
5use smallvec::SmallVec;
6
7use gpui_base::{Toolbar as BaseToolbar, ToolbarGroup as BaseToolbarGroup};
8
9use crate::{Sizable, Size, StyleSized as _, StyledExt as _};
10
11enum ToolbarItem {
12 Sized(Box<dyn FnOnce(Size) -> AnyElement>),
13 Content(AnyElement),
14}
15
16impl ToolbarItem {
17 fn sized(item: impl Sizable + IntoElement + 'static) -> Self {
18 Self::Sized(Box::new(move |size| {
19 item.prepare_for_toolbar()
20 .with_size(size)
21 .into_any_element()
22 }))
23 }
24
25 fn content(content: impl IntoElement) -> Self {
26 Self::Content(content.into_any_element())
27 }
28
29 fn into_element(self, size: Size) -> AnyElement {
30 match self {
31 Self::Sized(item) => div()
32 .flex()
33 .items_center()
34 .input_h(size)
35 .child(item(size))
36 .into_any_element(),
37 Self::Content(content) => content,
38 }
39 }
40}
41
42#[derive(IntoElement)]
45pub struct ToolbarGroup {
46 id: ElementId,
47 style: StyleRefinement,
48 size: Size,
49 label: Option<SharedString>,
50 children: SmallVec<[ToolbarItem; 4]>,
51}
52
53impl ToolbarGroup {
54 pub fn new(id: impl Into<ElementId>) -> Self {
55 Self {
56 id: id.into(),
57 style: StyleRefinement::default(),
58 size: Size::Small,
59 label: None,
60 children: SmallVec::new(),
61 }
62 }
63
64 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
66 self.label = Some(label.into());
67 self
68 }
69
70 pub fn child(mut self, child: impl Sizable + IntoElement + 'static) -> Self {
72 self.children.push(ToolbarItem::sized(child));
73 self
74 }
75
76 pub fn children<T>(mut self, children: impl IntoIterator<Item = T>) -> Self
78 where
79 T: Sizable + IntoElement + 'static,
80 {
81 self.children
82 .extend(children.into_iter().map(ToolbarItem::sized));
83 self
84 }
85
86 pub fn content(mut self, content: impl IntoElement) -> Self {
88 self.children.push(ToolbarItem::content(content));
89 self
90 }
91}
92
93impl ParentElement for ToolbarGroup {
94 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
95 self.children
96 .extend(elements.into_iter().map(ToolbarItem::Content));
97 }
98}
99
100impl Styled for ToolbarGroup {
101 fn style(&mut self) -> &mut StyleRefinement {
102 &mut self.style
103 }
104}
105
106impl Sizable for ToolbarGroup {
107 fn with_size(mut self, size: impl Into<Size>) -> Self {
108 self.size = match size.into() {
109 Size::Large => Size::Medium,
110 size => size,
111 };
112 self
113 }
114}
115
116impl RenderOnce for ToolbarGroup {
117 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
118 let size = self.size;
119 BaseToolbarGroup::new(self.id)
120 .when_some(self.label, |this, label| this.label(label))
121 .children(
122 self.children
123 .into_iter()
124 .map(|item| item.into_element(size)),
125 )
126 .refine_style(&self.style)
127 }
128}
129
130#[derive(IntoElement)]
158pub struct Toolbar {
159 id: ElementId,
160 style: StyleRefinement,
161 size: Size,
162 disabled: bool,
163 items: SmallVec<[ToolbarItem; 4]>,
164}
165
166impl Toolbar {
167 pub fn new(id: impl Into<ElementId>) -> Self {
169 Self {
170 id: id.into(),
171 style: StyleRefinement::default(),
172 size: Size::Small,
173 disabled: false,
174 items: SmallVec::new(),
175 }
176 }
177
178 pub fn disabled(mut self, disabled: bool) -> Self {
181 self.disabled = disabled;
182 self
183 }
184
185 pub fn child(mut self, child: impl Sizable + IntoElement + 'static) -> Self {
188 self.items.push(ToolbarItem::sized(child));
189 self
190 }
191
192 pub fn children<T>(mut self, children: impl IntoIterator<Item = T>) -> Self
194 where
195 T: Sizable + IntoElement + 'static,
196 {
197 self.items
198 .extend(children.into_iter().map(ToolbarItem::sized));
199 self
200 }
201
202 pub fn content(mut self, content: impl IntoElement) -> Self {
204 self.items.push(ToolbarItem::content(content));
205 self
206 }
207
208 pub fn contents(mut self, contents: impl IntoIterator<Item = AnyElement>) -> Self {
210 self.items
211 .extend(contents.into_iter().map(ToolbarItem::Content));
212 self
213 }
214}
215
216impl ParentElement for Toolbar {
220 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
221 self.items
222 .extend(elements.into_iter().map(ToolbarItem::Content));
223 }
224}
225
226impl Styled for Toolbar {
227 fn style(&mut self) -> &mut StyleRefinement {
228 &mut self.style
229 }
230}
231
232impl Sizable for Toolbar {
233 fn with_size(mut self, size: impl Into<Size>) -> Self {
234 self.size = match size.into() {
235 Size::Large => Size::Medium,
236 size => size,
237 };
238 self
239 }
240}
241
242impl RenderOnce for Toolbar {
243 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
244 let size = self.size;
245 let items = self.items.into_iter().map(|item| item.into_element(size));
246
247 BaseToolbar::new(self.id)
248 .disabled(self.disabled)
249 .flex()
250 .items_center()
251 .flex_shrink_0()
252 .map(|this| match size {
253 Size::XSmall => this.h_7().p_1().gap_1().text_xs(),
254 Size::Small => this.h_8().p_1().gap_1().text_sm(),
255 _ => this.h_12().p_2().gap_2().text_sm(),
256 })
257 .refine_style(&self.style)
258 .children(items)
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use crate::button::{Button, ButtonVariant};
266 use gpui::{Context, Render, TestAppContext, div};
267 use std::sync::{Arc, Mutex};
268
269 #[derive(IntoElement)]
270 struct SizeProbe {
271 size: Size,
272 observed: Arc<Mutex<Option<Size>>>,
273 }
274
275 impl SizeProbe {
276 fn new(observed: Arc<Mutex<Option<Size>>>) -> Self {
277 Self {
278 size: Size::default(),
279 observed,
280 }
281 }
282 }
283
284 impl Sizable for SizeProbe {
285 fn with_size(mut self, size: impl Into<Size>) -> Self {
286 self.size = size.into();
287 self
288 }
289 }
290
291 impl RenderOnce for SizeProbe {
292 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
293 *self.observed.lock().unwrap() = Some(self.size);
294 div()
295 }
296 }
297
298 struct ToolbarHarness {
299 items: [Arc<Mutex<Option<Size>>>; 3],
300 }
301
302 struct ToolbarGroupHarness {
303 item: Arc<Mutex<Option<Size>>>,
304 }
305
306 impl Render for ToolbarGroupHarness {
307 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
308 ToolbarGroup::new("group")
309 .child(SizeProbe::new(self.item.clone()))
310 .small()
311 }
312 }
313
314 impl Render for ToolbarHarness {
315 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
316 Toolbar::new("toolbar")
317 .children(self.items.iter().cloned().map(SizeProbe::new))
318 .small()
319 }
320 }
321
322 #[test]
323 fn test_toolbar_builder() {
324 let toolbar = Toolbar::new("toolbar")
325 .content("New")
326 .content("Open")
327 .content("Settings")
328 .small();
329
330 assert_eq!(toolbar.items.len(), 3);
331 assert_eq!(toolbar.size, Size::Small);
332 }
333
334 #[test]
335 fn test_toolbar_default() {
336 let toolbar = Toolbar::new("toolbar");
337
338 assert_eq!(toolbar.size, Size::Small);
339 assert!(!toolbar.disabled);
340 assert!(toolbar.items.is_empty());
341 }
342
343 #[test]
344 fn large_size_falls_back_to_medium() {
345 assert_eq!(Toolbar::new("toolbar").large().size, Size::Medium);
346 assert_eq!(ToolbarGroup::new("group").large().size, Size::Medium);
347 }
348
349 #[test]
350 fn toolbar_prepares_buttons_as_compact_ghost_commands() {
351 let button = Button::new("command").prepare_for_toolbar();
352
353 assert_eq!(button.variant(), ButtonVariant::Ghost);
354 assert!(button.is_compact());
355 }
356
357 #[gpui::test]
358 fn toolbar_size_propagates_to_items_independent_of_builder_order(cx: &mut TestAppContext) {
359 cx.update(crate::init);
360 let expected = std::array::from_fn(|_| Arc::new(Mutex::new(None)));
361 let items = expected.clone();
362 let (_, cx) = cx.add_window_view(move |_, _| ToolbarHarness { items });
363 cx.update(|window, cx| window.draw(cx).clear(cx));
364
365 for observed in expected {
366 assert_eq!(*observed.lock().unwrap(), Some(Size::Small));
367 }
368 }
369
370 #[gpui::test]
371 fn toolbar_group_propagates_its_size_to_items(cx: &mut TestAppContext) {
372 let item = Arc::new(Mutex::new(None));
373 let observed = item.clone();
374 let (_, cx) = cx.add_window_view(move |_, _| ToolbarGroupHarness { item });
375 cx.update(|window, cx| window.draw(cx).clear(cx));
376
377 assert_eq!(*observed.lock().unwrap(), Some(Size::Small));
378 }
379}