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