ui.rs

  1use std::borrow::Cow;
  2
  3use gpui::{
  4    color::Color,
  5    elements::{
  6        ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label,
  7        MouseEventHandler, ParentElement, Stack, Svg,
  8    },
  9    fonts::TextStyle,
 10    geometry::vector::{vec2f, Vector2F},
 11    platform,
 12    platform::MouseButton,
 13    scene::MouseClick,
 14    Action, Element, ElementBox, EventContext, MouseState, View, ViewContext,
 15};
 16use serde::Deserialize;
 17
 18use crate::{ContainedText, Interactive};
 19
 20#[derive(Clone, Deserialize, Default)]
 21pub struct CheckboxStyle {
 22    pub icon: SvgStyle,
 23    pub label: ContainedText,
 24    pub default: ContainerStyle,
 25    pub checked: ContainerStyle,
 26    pub hovered: ContainerStyle,
 27    pub hovered_and_checked: ContainerStyle,
 28}
 29
 30pub fn checkbox<Tag: 'static, V: View>(
 31    label: &'static str,
 32    style: &CheckboxStyle,
 33    checked: bool,
 34    cx: &mut ViewContext<V>,
 35    change: fn(checked: bool, cx: &mut EventContext<V>) -> (),
 36) -> MouseEventHandler<Tag, V> {
 37    let label = Label::new(label, style.label.text.clone())
 38        .contained()
 39        .with_style(style.label.container)
 40        .boxed();
 41
 42    checkbox_with_label(label, style, checked, cx, change)
 43}
 44
 45pub fn checkbox_with_label<Tag: 'static, V: View>(
 46    label: ElementBox<V>,
 47    style: &CheckboxStyle,
 48    checked: bool,
 49    cx: &mut ViewContext<V>,
 50    change: fn(checked: bool, cx: &mut EventContext<V>) -> (),
 51) -> MouseEventHandler<Tag, V> {
 52    MouseEventHandler::new(0, cx, |state, _| {
 53        let indicator = if checked {
 54            svg(&style.icon)
 55        } else {
 56            Empty::new()
 57                .constrained()
 58                .with_width(style.icon.dimensions.width)
 59                .with_height(style.icon.dimensions.height)
 60        };
 61
 62        Flex::row()
 63            .with_children([
 64                indicator
 65                    .contained()
 66                    .with_style(if checked {
 67                        if state.hovered() {
 68                            style.hovered_and_checked
 69                        } else {
 70                            style.checked
 71                        }
 72                    } else {
 73                        if state.hovered() {
 74                            style.hovered
 75                        } else {
 76                            style.default
 77                        }
 78                    })
 79                    .boxed(),
 80                label,
 81            ])
 82            .align_children_center()
 83            .boxed()
 84    })
 85    .on_click(platform::MouseButton::Left, move |_, _, cx| {
 86        change(!checked, cx)
 87    })
 88    .with_cursor_style(platform::CursorStyle::PointingHand)
 89}
 90
 91#[derive(Clone, Deserialize, Default)]
 92pub struct SvgStyle {
 93    pub color: Color,
 94    pub asset: String,
 95    pub dimensions: Dimensions,
 96}
 97
 98#[derive(Clone, Deserialize, Default)]
 99pub struct Dimensions {
100    pub width: f32,
101    pub height: f32,
102}
103
104impl Dimensions {
105    pub fn to_vec(&self) -> Vector2F {
106        vec2f(self.width, self.height)
107    }
108}
109
110pub fn svg<V: View>(style: &SvgStyle) -> ConstrainedBox<V> {
111    Svg::new(style.asset.clone())
112        .with_color(style.color)
113        .constrained()
114        .with_width(style.dimensions.width)
115        .with_height(style.dimensions.height)
116}
117
118#[derive(Clone, Deserialize, Default)]
119pub struct IconStyle {
120    icon: SvgStyle,
121    container: ContainerStyle,
122}
123
124pub fn icon<V: View>(style: &IconStyle) -> Container<V> {
125    svg(&style.icon).contained().with_style(style.container)
126}
127
128pub fn keystroke_label<V: View>(
129    label_text: &'static str,
130    label_style: &ContainedText,
131    keystroke_style: &ContainedText,
132    action: Box<dyn Action>,
133    cx: &mut ViewContext<V>,
134) -> Container<V> {
135    // FIXME: Put the theme in it's own global so we can
136    // query the keystroke style on our own
137    keystroke_label_for(
138        cx.window_id(),
139        cx.handle().id(),
140        label_text,
141        label_style,
142        keystroke_style,
143        action,
144    )
145}
146
147pub fn keystroke_label_for<V: View>(
148    window_id: usize,
149    view_id: usize,
150    label_text: &'static str,
151    label_style: &ContainedText,
152    keystroke_style: &ContainedText,
153    action: Box<dyn Action>,
154) -> Container<V> {
155    Flex::row()
156        .with_child(
157            Label::new(label_text, label_style.text.clone())
158                .contained()
159                .boxed(),
160        )
161        .with_child({
162            KeystrokeLabel::new(
163                window_id,
164                view_id,
165                action,
166                keystroke_style.container,
167                keystroke_style.text.clone(),
168            )
169            .flex_float()
170            .boxed()
171        })
172        .contained()
173        .with_style(label_style.container)
174}
175
176pub type ButtonStyle = Interactive<ContainedText>;
177
178pub fn cta_button<Tag, L, A, V>(
179    label: L,
180    action: A,
181    max_width: f32,
182    style: &ButtonStyle,
183    cx: &mut ViewContext<V>,
184) -> ElementBox<V>
185where
186    Tag: 'static,
187    L: Into<Cow<'static, str>>,
188    A: 'static + Action + Clone,
189    V: View,
190{
191    cta_button_with_click::<Tag, _, _, _>(label, max_width, style, cx, move |_, _, cx| {
192        cx.dispatch_action(action.clone())
193    })
194    .boxed()
195}
196
197pub fn cta_button_with_click<Tag, L, V, F>(
198    label: L,
199    max_width: f32,
200    style: &ButtonStyle,
201    cx: &mut ViewContext<V>,
202    f: F,
203) -> MouseEventHandler<Tag, V>
204where
205    Tag: 'static,
206    L: Into<Cow<'static, str>>,
207    V: View,
208    F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
209{
210    MouseEventHandler::<Tag, V>::new(0, cx, |state, _| {
211        let style = style.style_for(state, false);
212        Label::new(label, style.text.to_owned())
213            .aligned()
214            .contained()
215            .with_style(style.container)
216            .constrained()
217            .with_max_width(max_width)
218            .boxed()
219    })
220    .on_click(MouseButton::Left, f)
221    .with_cursor_style(platform::CursorStyle::PointingHand)
222}
223
224#[derive(Clone, Deserialize, Default)]
225pub struct ModalStyle {
226    close_icon: Interactive<IconStyle>,
227    container: ContainerStyle,
228    titlebar: ContainerStyle,
229    title_text: Interactive<TextStyle>,
230    dimensions: Dimensions,
231}
232
233impl ModalStyle {
234    pub fn dimensions(&self) -> Vector2F {
235        self.dimensions.to_vec()
236    }
237}
238
239pub fn modal<Tag, V, I, F>(
240    title: I,
241    style: &ModalStyle,
242    cx: &mut ViewContext<V>,
243    build_modal: F,
244) -> ElementBox<V>
245where
246    Tag: 'static,
247    V: View,
248    I: Into<Cow<'static, str>>,
249    F: FnOnce(&mut gpui::ViewContext<V>) -> ElementBox<V>,
250{
251    const TITLEBAR_HEIGHT: f32 = 28.;
252    // let active = cx.window_is_active(cx.window_id());
253
254    Flex::column()
255        .with_child(
256            Stack::new()
257                .with_children([
258                    Label::new(
259                        title,
260                        style
261                            .title_text
262                            .style_for(&mut MouseState::default(), false)
263                            .clone(),
264                    )
265                    .boxed(),
266                    // FIXME: Get a better tag type
267                    MouseEventHandler::<Tag, V>::new(999999, cx, |state, _cx| {
268                        let style = style.close_icon.style_for(state, false);
269                        icon(style).boxed()
270                    })
271                    .on_click(platform::MouseButton::Left, move |_, _, cx| {
272                        let window_id = cx.window_id();
273                        cx.remove_window(window_id);
274                    })
275                    .with_cursor_style(platform::CursorStyle::PointingHand)
276                    .aligned()
277                    .right()
278                    .boxed(),
279                ])
280                .contained()
281                .with_style(style.titlebar)
282                .constrained()
283                .with_height(TITLEBAR_HEIGHT)
284                .boxed(),
285        )
286        .with_child(
287            Container::new(build_modal(cx))
288                .with_style(style.container)
289                .constrained()
290                .with_width(style.dimensions().x())
291                .with_height(style.dimensions().y() - TITLEBAR_HEIGHT)
292                .boxed(),
293        )
294        .constrained()
295        .with_height(style.dimensions().y())
296        .boxed()
297}