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