1use gpui::{elements::SafeStylable, Action};
2
3use crate::{Interactive, Toggleable};
4
5use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, toggle::Toggle};
6
7pub type IconButtonStyle = Interactive<ButtonStyle<SvgStyle>>;
8pub type ToggleIconButtonStyle = Toggleable<IconButtonStyle>;
9
10pub trait ComponentExt<C: SafeStylable> {
11 fn toggleable(self, active: bool) -> Toggle<C, ()>;
12 fn disclosable(self, disclosed: Option<bool>, action: Box<dyn Action>) -> Disclosable<C, ()>;
13}
14
15impl<C: SafeStylable> ComponentExt<C> for C {
16 fn toggleable(self, active: bool) -> Toggle<C, ()> {
17 Toggle::new(self, active)
18 }
19
20 /// Some(True) => disclosed => content is visible
21 /// Some(false) => closed => content is hidden
22 /// None => No disclosure button, but reserve disclosure spacing
23 fn disclosable(self, disclosed: Option<bool>, action: Box<dyn Action>) -> Disclosable<C, ()> {
24 Disclosable::new(disclosed, self, action)
25 }
26}
27
28pub mod disclosure {
29 use gpui::{
30 elements::{Component, ContainerStyle, Empty, Flex, ParentElement, SafeStylable},
31 Action, Element,
32 };
33 use schemars::JsonSchema;
34 use serde_derive::Deserialize;
35
36 use super::{action_button::Button, svg::Svg, IconButtonStyle};
37
38 #[derive(Clone, Default, Deserialize, JsonSchema)]
39 pub struct DisclosureStyle<S> {
40 pub button: IconButtonStyle,
41 #[serde(flatten)]
42 pub container: ContainerStyle,
43 pub spacing: f32,
44 #[serde(flatten)]
45 content: S,
46 }
47
48 impl<S> DisclosureStyle<S> {
49 pub fn button_space(&self) -> f32 {
50 self.spacing + self.button.button_width.unwrap()
51 }
52 }
53
54 pub struct Disclosable<C, S> {
55 disclosed: Option<bool>,
56 action: Box<dyn Action>,
57 id: usize,
58 content: C,
59 style: S,
60 }
61
62 impl Disclosable<(), ()> {
63 pub fn new<C>(
64 disclosed: Option<bool>,
65 content: C,
66 action: Box<dyn Action>,
67 ) -> Disclosable<C, ()> {
68 Disclosable {
69 disclosed,
70 content,
71 action,
72 id: 0,
73 style: (),
74 }
75 }
76 }
77
78 impl<C> Disclosable<C, ()> {
79 pub fn with_id(mut self, id: usize) -> Disclosable<C, ()> {
80 self.id = id;
81 self
82 }
83 }
84
85 impl<C: SafeStylable> SafeStylable for Disclosable<C, ()> {
86 type Style = DisclosureStyle<C::Style>;
87
88 type Output = Disclosable<C, Self::Style>;
89
90 fn with_style(self, style: Self::Style) -> Self::Output {
91 Disclosable {
92 disclosed: self.disclosed,
93 action: self.action,
94 content: self.content,
95 id: self.id,
96 style,
97 }
98 }
99 }
100
101 impl<C: SafeStylable> Component for Disclosable<C, DisclosureStyle<C::Style>> {
102 fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
103 Flex::row()
104 .with_spacing(self.style.spacing)
105 .with_child(if let Some(disclosed) = self.disclosed {
106 Button::dynamic_action(self.action)
107 .with_id(self.id)
108 .with_contents(Svg::new(if disclosed {
109 "icons/file_icons/chevron_down.svg"
110 } else {
111 "icons/file_icons/chevron_right.svg"
112 }))
113 .with_style(self.style.button)
114 .element()
115 .into_any()
116 } else {
117 Empty::new()
118 .into_any()
119 .constrained()
120 // TODO: Why is this optional at all?
121 .with_width(self.style.button.button_width.unwrap())
122 .into_any()
123 })
124 .with_child(
125 self.content
126 .with_style(self.style.content)
127 .render(cx)
128 .flex(1., true),
129 )
130 .align_children_center()
131 .contained()
132 .with_style(self.style.container)
133 .into_any()
134 }
135 }
136}
137
138pub mod toggle {
139 use gpui::elements::{Component, SafeStylable};
140
141 use crate::Toggleable;
142
143 pub struct Toggle<C, S> {
144 style: S,
145 active: bool,
146 component: C,
147 }
148
149 impl<C: SafeStylable> Toggle<C, ()> {
150 pub fn new(component: C, active: bool) -> Self {
151 Toggle {
152 active,
153 component,
154 style: (),
155 }
156 }
157 }
158
159 impl<C: SafeStylable> SafeStylable for Toggle<C, ()> {
160 type Style = Toggleable<C::Style>;
161
162 type Output = Toggle<C, Self::Style>;
163
164 fn with_style(self, style: Self::Style) -> Self::Output {
165 Toggle {
166 active: self.active,
167 component: self.component,
168 style,
169 }
170 }
171 }
172
173 impl<C: SafeStylable> Component for Toggle<C, Toggleable<C::Style>> {
174 fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
175 self.component
176 .with_style(self.style.in_state(self.active).clone())
177 .render(cx)
178 }
179 }
180}
181
182pub mod action_button {
183 use std::borrow::Cow;
184
185 use gpui::{
186 elements::{Component, ContainerStyle, MouseEventHandler, SafeStylable, TooltipStyle},
187 platform::{CursorStyle, MouseButton},
188 Action, Element, TypeTag,
189 };
190 use schemars::JsonSchema;
191 use serde_derive::Deserialize;
192
193 use crate::Interactive;
194
195 #[derive(Clone, Deserialize, Default, JsonSchema)]
196 pub struct ButtonStyle<C> {
197 #[serde(flatten)]
198 pub container: ContainerStyle,
199 // TODO: These are incorrect for the intended usage of the buttons.
200 // The size should be constant, but putting them here duplicates them
201 // across the states the buttons can be in
202 pub button_width: Option<f32>,
203 pub button_height: Option<f32>,
204 #[serde(flatten)]
205 contents: C,
206 }
207
208 pub struct Button<C, S> {
209 action: Box<dyn Action>,
210 tooltip: Option<(Cow<'static, str>, TooltipStyle)>,
211 tag: TypeTag,
212 id: usize,
213 contents: C,
214 style: Interactive<S>,
215 }
216
217 impl Button<(), ()> {
218 pub fn dynamic_action(action: Box<dyn Action>) -> Button<(), ()> {
219 Self {
220 contents: (),
221 tag: action.type_tag(),
222 action,
223 style: Interactive::new_blank(),
224 tooltip: None,
225 id: 0,
226 }
227 }
228
229 pub fn action<A: Action + Clone>(action: A) -> Self {
230 Self::dynamic_action(Box::new(action))
231 }
232
233 pub fn with_tooltip(
234 mut self,
235 tooltip: impl Into<Cow<'static, str>>,
236 tooltip_style: TooltipStyle,
237 ) -> Self {
238 self.tooltip = Some((tooltip.into(), tooltip_style));
239 self
240 }
241
242 pub fn with_id(mut self, id: usize) -> Self {
243 self.id = id;
244 self
245 }
246
247 pub fn with_contents<C: SafeStylable>(self, contents: C) -> Button<C, ()> {
248 Button {
249 action: self.action,
250 tag: self.tag,
251 style: self.style,
252 tooltip: self.tooltip,
253 id: self.id,
254 contents,
255 }
256 }
257 }
258
259 impl<C: SafeStylable> SafeStylable for Button<C, ()> {
260 type Style = Interactive<ButtonStyle<C::Style>>;
261 type Output = Button<C, ButtonStyle<C::Style>>;
262
263 fn with_style(self, style: Self::Style) -> Self::Output {
264 Button {
265 action: self.action,
266 tag: self.tag,
267 contents: self.contents,
268 tooltip: self.tooltip,
269 id: self.id,
270 style,
271 }
272 }
273 }
274
275 impl<C: SafeStylable> Component for Button<C, ButtonStyle<C::Style>> {
276 fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
277 let mut button = MouseEventHandler::new_dynamic(self.tag, self.id, cx, |state, cx| {
278 let style = self.style.style_for(state);
279 let mut contents = self
280 .contents
281 .with_style(style.contents.to_owned())
282 .render(cx)
283 .contained()
284 .with_style(style.container)
285 .constrained();
286
287 if let Some(height) = style.button_height {
288 contents = contents.with_height(height);
289 }
290
291 if let Some(width) = style.button_width {
292 contents = contents.with_width(width);
293 }
294
295 contents.into_any()
296 })
297 .on_click(MouseButton::Left, {
298 let action = self.action.boxed_clone();
299 move |_, _, cx| {
300 let window = cx.window();
301 let view = cx.view_id();
302 let action = action.boxed_clone();
303 cx.spawn(|_, mut cx| async move {
304 window.dispatch_action(view, action.as_ref(), &mut cx)
305 })
306 .detach();
307 }
308 })
309 .with_cursor_style(CursorStyle::PointingHand)
310 .into_any();
311
312 if let Some((tooltip, style)) = self.tooltip {
313 button = button
314 .with_dynamic_tooltip(self.tag, 0, tooltip, Some(self.action), style, cx)
315 .into_any()
316 }
317
318 button
319 }
320 }
321}
322
323pub mod svg {
324 use std::borrow::Cow;
325
326 use gpui::{
327 elements::{Component, Empty, SafeStylable},
328 Element,
329 };
330 use schemars::JsonSchema;
331 use serde::Deserialize;
332
333 #[derive(Clone, Default, JsonSchema)]
334 pub struct SvgStyle {
335 icon_width: f32,
336 icon_height: f32,
337 color: gpui::color::Color,
338 }
339
340 impl<'de> Deserialize<'de> for SvgStyle {
341 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
342 where
343 D: serde::Deserializer<'de>,
344 {
345 #[derive(Deserialize)]
346 #[serde(untagged)]
347 pub enum IconSize {
348 IconSize { icon_size: f32 },
349 Dimensions { width: f32, height: f32 },
350 IconDimensions { icon_width: f32, icon_height: f32 },
351 }
352
353 #[derive(Deserialize)]
354 struct SvgStyleHelper {
355 #[serde(flatten)]
356 size: IconSize,
357 color: gpui::color::Color,
358 }
359
360 let json = SvgStyleHelper::deserialize(deserializer)?;
361 let color = json.color;
362
363 let result = match json.size {
364 IconSize::IconSize { icon_size } => SvgStyle {
365 icon_width: icon_size,
366 icon_height: icon_size,
367 color,
368 },
369 IconSize::Dimensions { width, height } => SvgStyle {
370 icon_width: width,
371 icon_height: height,
372 color,
373 },
374 IconSize::IconDimensions {
375 icon_width,
376 icon_height,
377 } => SvgStyle {
378 icon_width,
379 icon_height,
380 color,
381 },
382 };
383
384 Ok(result)
385 }
386 }
387
388 pub struct Svg<S> {
389 path: Option<Cow<'static, str>>,
390 style: S,
391 }
392
393 impl Svg<()> {
394 pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
395 Self {
396 path: Some(path.into()),
397 style: (),
398 }
399 }
400
401 pub fn optional(path: Option<impl Into<Cow<'static, str>>>) -> Self {
402 Self {
403 path: path.map(Into::into),
404 style: (),
405 }
406 }
407 }
408
409 impl SafeStylable for Svg<()> {
410 type Style = SvgStyle;
411
412 type Output = Svg<SvgStyle>;
413
414 fn with_style(self, style: Self::Style) -> Self::Output {
415 Svg {
416 path: self.path,
417 style,
418 }
419 }
420 }
421
422 impl Component for Svg<SvgStyle> {
423 fn render<V: 'static>(self, _: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
424 if let Some(path) = self.path {
425 gpui::elements::Svg::new(path)
426 .with_color(self.style.color)
427 .constrained()
428 } else {
429 Empty::new().constrained()
430 }
431 .constrained()
432 .with_width(self.style.icon_width)
433 .with_height(self.style.icon_height)
434 .into_any()
435 }
436 }
437}
438
439pub mod label {
440 use std::borrow::Cow;
441
442 use gpui::{
443 elements::{Component, LabelStyle, SafeStylable},
444 fonts::TextStyle,
445 Element,
446 };
447
448 pub struct Label<S> {
449 text: Cow<'static, str>,
450 style: S,
451 }
452
453 impl Label<()> {
454 pub fn new(text: impl Into<Cow<'static, str>>) -> Self {
455 Self {
456 text: text.into(),
457 style: (),
458 }
459 }
460 }
461
462 impl SafeStylable for Label<()> {
463 type Style = TextStyle;
464
465 type Output = Label<LabelStyle>;
466
467 fn with_style(self, style: Self::Style) -> Self::Output {
468 Label {
469 text: self.text,
470 style: style.into(),
471 }
472 }
473 }
474
475 impl Component for Label<LabelStyle> {
476 fn render<V: 'static>(self, _: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
477 gpui::elements::Label::new(self.text, self.style).into_any()
478 }
479 }
480}