components.rs

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