1use std::rc::Rc;
4
5use gpui::{
6 App, AppContext as _, Context, Entity, FocusHandle, Focusable, InteractiveElement, IntoElement,
7 ParentElement, Render, SharedString, Styled, Window, div, prelude::FluentBuilder,
8};
9use gpui_kit_assets::Icon;
10use gpui_kit_semantics::{NodeSpec, Role, Semantic};
11use gpui_kit_theme::ControlSize;
12
13use crate::controls::button::{Button, ButtonJoin, ButtonVariant, IconButton};
14use crate::foundation::{Disableable, Ident, Sizable, StyledExt};
15use crate::overlay::{Menu, MenuItem};
16use crate::strings::{ActiveStrings, StringKey};
17
18type ClickHandler = Rc<dyn Fn(&mut Window, &mut App)>;
19
20pub struct SplitButton {
29 ident: Ident,
30 label: SharedString,
31 icon: Option<Icon>,
32 variant: ButtonVariant,
33 size: ControlSize,
34 disabled: bool,
35 default_disabled: bool,
36 focus_handle: FocusHandle,
37 menu: Entity<Menu>,
38 applied_trigger: Option<(ButtonVariant, ControlSize)>,
41 on_click: Option<ClickHandler>,
42}
43
44impl std::fmt::Debug for SplitButton {
45 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 formatter
47 .debug_struct("SplitButton")
48 .field("ident", &self.ident)
49 .field("label", &self.label)
50 .field("disabled", &self.disabled)
51 .field("default_disabled", &self.default_disabled)
52 .field("has_handler", &self.on_click.is_some())
53 .finish()
54 }
55}
56
57impl SplitButton {
58 pub fn new(ident: impl Into<Ident>, window: &mut Window, cx: &mut Context<Self>) -> Self {
59 let ident = ident.into();
60 let menu = cx.new(|cx| {
61 Menu::new(ident.child("menu"), window, cx)
62 .trigger_icon(Icon::AltArrowDown)
63 .trigger_name(cx.strings().text(StringKey::MoreActions))
64 .trigger_join(ButtonJoin::Trailing)
65 });
66 Self {
67 ident,
68 label: SharedString::default(),
69 icon: None,
70 variant: ButtonVariant::Secondary,
71 size: ControlSize::Md,
72 disabled: false,
73 default_disabled: false,
74 focus_handle: cx.focus_handle(),
75 menu,
76 applied_trigger: None,
77 on_click: None,
78 }
79 }
80
81 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
82 self.label = label.into();
83 self
84 }
85
86 pub fn icon(mut self, glyph: Icon) -> Self {
87 self.icon = Some(glyph);
88 self
89 }
90
91 pub fn variant(mut self, variant: ButtonVariant) -> Self {
92 self.variant = variant;
93 self
94 }
95
96 pub fn primary(self) -> Self {
97 self.variant(ButtonVariant::Primary)
98 }
99
100 pub fn secondary(self) -> Self {
101 self.variant(ButtonVariant::Secondary)
102 }
103
104 pub fn items(self, items: impl IntoIterator<Item = MenuItem>, cx: &mut Context<Self>) -> Self {
107 let items = items.into_iter().collect();
108 self.menu.update(cx, |menu, cx| menu.set_items(items, cx));
109 self
110 }
111
112 pub fn menu_name(self, name: impl Into<SharedString>, cx: &mut Context<Self>) -> Self {
114 self.menu
115 .update(cx, |menu, cx| menu.set_trigger_name(name, cx));
116 self
117 }
118
119 pub fn default_disabled(mut self, disabled: bool) -> Self {
121 self.default_disabled = disabled;
122 self
123 }
124
125 pub fn on_click(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
126 self.on_click = Some(Rc::new(handler));
127 self
128 }
129
130 pub fn menu(&self) -> &Entity<Menu> {
131 &self.menu
132 }
133
134 pub fn is_open(&self, cx: &App) -> bool {
135 self.menu.read(cx).is_open()
136 }
137
138 pub fn open_menu(&self, window: &mut Window, cx: &mut Context<Self>) {
139 if self.disabled {
140 return;
141 }
142 self.menu.update(cx, |menu, cx| menu.open(window, cx));
143 }
144
145 pub fn set_disabled(&mut self, disabled: bool, window: &mut Window, cx: &mut Context<Self>) {
146 self.disabled = disabled;
147 if disabled {
148 self.menu.update(cx, |menu, cx| menu.close(window, cx));
149 }
150 cx.notify();
151 }
152}
153
154impl Disableable for SplitButton {
155 fn disabled(mut self, disabled: bool) -> Self {
156 self.disabled = disabled;
157 self
158 }
159}
160
161impl Sizable for SplitButton {
162 fn control_size(mut self, size: ControlSize) -> Self {
163 self.size = size;
164 self
165 }
166}
167
168impl Focusable for SplitButton {
169 fn focus_handle(&self, _cx: &App) -> FocusHandle {
170 self.focus_handle.clone()
171 }
172}
173
174impl Render for SplitButton {
175 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
176 let paint = (self.variant, self.size);
177 if self.applied_trigger != Some(paint) {
178 self.applied_trigger = Some(paint);
179 self.menu
180 .update(cx, |menu, cx| menu.set_trigger_style(paint.0, paint.1, cx));
181 }
182 let refused = self.disabled || self.default_disabled;
183 let action = Button::new(self.ident.child("action"))
184 .label(self.label.clone())
185 .variant(self.variant)
186 .control_size(self.size)
187 .join(ButtonJoin::Leading)
188 .semantic_parent(self.ident.semantic_id())
189 .disabled(refused)
190 .track_focus(&self.focus_handle)
191 .when_some(self.icon, |button, glyph| button.icon(glyph))
192 .when_some(self.on_click.clone(), |button, handler| {
193 button.on_click(move |window, cx| handler(window, cx))
194 });
195
196 let arrow = if self.disabled {
197 IconButton::new(
200 self.ident.child("menu").child("trigger"),
201 Icon::AltArrowDown,
202 cx.strings().text(StringKey::MoreActions),
203 )
204 .variant(self.variant)
205 .control_size(self.size)
206 .join(ButtonJoin::Trailing)
207 .semantic_parent(self.ident.semantic_id())
208 .disabled(true)
209 .into_any_element()
210 } else {
211 self.menu.clone().into_any_element()
212 };
213
214 div()
215 .id(self.ident.element_id())
216 .row()
217 .flex_none()
218 .items_start()
219 .child(action)
220 .child(arrow)
221 .semantic_in(
222 cx,
223 NodeSpec::new(self.ident.semantic_id(), Role::Group)
224 .disabled(self.disabled)
225 .text(self.label.clone()),
226 )
227 }
228}