1use gpui::{
21 AnyElement, App, Entity, InteractiveElement, IntoElement, ParentElement, RenderOnce,
22 SharedString, Styled, Window, div, px,
23};
24use gpui_kit_assets::Icon;
25use gpui_kit_semantics::{NodeSpec, Role, Semantic};
26use gpui_kit_theme::{ActiveTheme, ControlSize, Elevation, Space, Surface, Theme};
27
28use crate::foundation::{Ident, Sizable, StyledExt};
29use crate::overlay::{Menu, MenuItem};
30use crate::strings::{ActiveStrings, StringKey};
31
32pub struct ToolbarItem {
37 id: SharedString,
38 label: SharedString,
39 icon: Option<Icon>,
40 shortcut: Option<SharedString>,
41 disabled: bool,
42 content: AnyElement,
43}
44
45impl std::fmt::Debug for ToolbarItem {
46 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 formatter
48 .debug_struct("ToolbarItem")
49 .field("id", &self.id)
50 .field("label", &self.label)
51 .field("disabled", &self.disabled)
52 .finish()
53 }
54}
55
56impl ToolbarItem {
57 pub fn new(
60 id: impl Into<SharedString>,
61 label: impl Into<SharedString>,
62 content: impl IntoElement,
63 ) -> Self {
64 Self {
65 id: id.into(),
66 label: label.into(),
67 icon: None,
68 shortcut: None,
69 disabled: false,
70 content: content.into_any_element(),
71 }
72 }
73
74 pub fn icon(mut self, glyph: Icon) -> Self {
76 self.icon = Some(glyph);
77 self
78 }
79
80 pub fn shortcut(mut self, keystroke: impl Into<SharedString>) -> Self {
81 self.shortcut = Some(keystroke.into());
82 self
83 }
84
85 pub fn disabled(mut self, disabled: bool) -> Self {
86 self.disabled = disabled;
87 self
88 }
89
90 pub fn id(&self) -> &SharedString {
91 &self.id
92 }
93
94 pub fn label(&self) -> &SharedString {
95 &self.label
96 }
97
98 fn menu_row(&self) -> MenuItem {
99 let mut row =
100 MenuItem::command(self.id.clone(), self.label.clone()).disabled(self.disabled);
101 if let Some(glyph) = self.icon {
102 row = row.icon(glyph);
103 }
104 if let Some(shortcut) = self.shortcut.clone() {
105 row = row.shortcut(shortcut);
106 }
107 row
108 }
109}
110
111enum Slot {
113 Group {
114 id: SharedString,
115 items: Vec<ToolbarItem>,
116 },
117 Spacer,
119}
120
121#[derive(IntoElement)]
123pub struct Toolbar {
124 ident: Ident,
125 label: Option<SharedString>,
126 slots: Vec<Slot>,
127 size: ControlSize,
128 overflow_after: Option<usize>,
129 overflow_menu: Option<Entity<Menu>>,
130}
131
132impl std::fmt::Debug for Toolbar {
133 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 formatter
135 .debug_struct("Toolbar")
136 .field("ident", &self.ident)
137 .field("items", &self.item_count())
138 .field("overflow_after", &self.overflow_after)
139 .field("has_overflow_menu", &self.overflow_menu.is_some())
140 .finish()
141 }
142}
143
144impl Toolbar {
145 pub fn new(ident: impl Into<Ident>) -> Self {
146 Self {
147 ident: ident.into(),
148 label: None,
149 slots: Vec::new(),
150 size: ControlSize::Md,
151 overflow_after: None,
152 overflow_menu: None,
153 }
154 }
155
156 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
158 self.label = Some(label.into());
159 self
160 }
161
162 pub fn group(
164 mut self,
165 id: impl Into<SharedString>,
166 items: impl IntoIterator<Item = ToolbarItem>,
167 ) -> Self {
168 self.slots.push(Slot::Group {
169 id: id.into(),
170 items: items.into_iter().collect(),
171 });
172 self
173 }
174
175 pub fn spacer(mut self) -> Self {
177 self.slots.push(Slot::Spacer);
178 self
179 }
180
181 pub fn overflow_after(mut self, count: usize) -> Self {
185 self.overflow_after = Some(count);
186 self
187 }
188
189 pub fn overflow_menu(mut self, menu: Entity<Menu>) -> Self {
193 self.overflow_menu = Some(menu);
194 self
195 }
196
197 pub fn item_count(&self) -> usize {
199 self.slots
200 .iter()
201 .map(|slot| match slot {
202 Slot::Group { items, .. } => items.len(),
203 Slot::Spacer => 0,
204 })
205 .sum()
206 }
207
208 fn cut(&self) -> usize {
211 match (self.overflow_after, self.overflow_menu.is_some()) {
212 (Some(cut), true) => cut,
213 _ => usize::MAX,
214 }
215 }
216}
217
218impl Sizable for Toolbar {
219 fn control_size(mut self, size: ControlSize) -> Self {
220 self.size = size;
221 self
222 }
223}
224
225impl RenderOnce for Toolbar {
226 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
227 let theme = cx.theme().clone();
228 let total = self.item_count();
229 let cut = self.cut();
230 let ident = self.ident.clone();
231
232 let mut drawn: Vec<AnyElement> = Vec::new();
233 let mut overflowed: Vec<MenuItem> = Vec::new();
234 let mut index = 0usize;
235 let mut previous_group = false;
236
237 for slot in self.slots {
238 match slot {
239 Slot::Spacer => {
240 drawn.push(div().flex_1().into_any_element());
241 previous_group = false;
242 }
243 Slot::Group { id, items } => {
244 let group = ident.child(id.as_ref());
245 let mut inline: Vec<AnyElement> = Vec::new();
246 for item in items {
247 if index >= cut {
248 overflowed.push(item.menu_row());
249 } else {
250 inline.push(item.content);
251 }
252 index += 1;
253 }
254 if inline.is_empty() {
255 continue;
256 }
257 if previous_group {
258 drawn.push(rule(&group, &theme, &ident, cx));
259 }
260 drawn.push(
261 div()
262 .flex()
263 .flex_row()
264 .items_center()
265 .gap(px(theme.space(Space::Xs)))
266 .children(inline)
267 .semantic_in(
268 cx,
269 NodeSpec::new(group.semantic_id(), Role::Group)
270 .parent(ident.semantic_id()),
271 )
272 .into_any_element(),
273 );
274 previous_group = true;
275 }
276 }
277 }
278
279 let hidden = overflowed.len();
280 let overflow = self.overflow_menu.filter(|_| hidden > 0).map(|menu| {
281 let rows = overflowed;
282 if menu.read(cx).offered() != rows.as_slice() {
283 menu.update(cx, |menu, cx| menu.set_items(rows, cx));
284 }
285 menu.clone()
286 });
287 let overflow_ident = ident.child("overflow");
288
289 div()
290 .id(ident.element_id())
291 .flex()
292 .flex_row()
293 .items_center()
294 .w_full()
295 .gap(px(theme.space(Space::Sm)))
296 .px(px(theme.space(Space::Sm)))
297 .py(px(theme.space(Space::Xs)))
298 .frame(&theme, Surface::Panel, Elevation::Raised)
299 .children(drawn)
300 .children(overflow.map(|menu| {
301 div()
302 .flex()
303 .flex_none()
304 .child(menu)
305 .semantic_in(
308 cx,
309 NodeSpec::new(overflow_ident.semantic_id(), Role::Group)
310 .parent(ident.semantic_id())
311 .text(cx.strings().text(StringKey::MoreActions))
312 .value(hidden.to_string()),
313 )
314 }))
315 .semantic_in(cx, {
316 let mut spec =
317 NodeSpec::new(ident.semantic_id(), Role::Toolbar).value(total.to_string());
318 if let Some(label) = self.label {
319 spec = spec.text(label);
320 }
321 spec
322 })
323 }
324}
325
326fn rule(group: &Ident, theme: &Theme, toolbar: &Ident, cx: &App) -> AnyElement {
327 div()
328 .flex_none()
329 .w(px(theme.borders.hairline))
330 .h(px(theme.control.get(ControlSize::Sm).height))
331 .bg(theme.colors.hairline)
332 .semantic_in(
333 cx,
334 NodeSpec::new(group.child("rule").semantic_id(), Role::Separator)
335 .parent(toolbar.semantic_id()),
336 )
337 .into_any_element()
338}