components.rs

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