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    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}