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, Drawable, Element, 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: Element<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<L, A, V>(
179    label: L,
180    action: A,
181    max_width: f32,
182    style: &ButtonStyle,
183    cx: &mut ViewContext<V>,
184) -> Element<V>
185where
186    L: Into<Cow<'static, str>>,
187    A: 'static + Action + Clone,
188    V: View,
189{
190    cta_button_with_click::<A, _, _, _>(label, max_width, style, cx, move |_, _, cx| {
191        cx.dispatch_action(action.clone())
192    })
193    .boxed()
194}
195
196pub fn cta_button_with_click<Tag, L, V, F>(
197    label: L,
198    max_width: f32,
199    style: &ButtonStyle,
200    cx: &mut ViewContext<V>,
201    f: F,
202) -> MouseEventHandler<Tag, V>
203where
204    Tag: 'static,
205    L: Into<Cow<'static, str>>,
206    V: View,
207    F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
208{
209    MouseEventHandler::<Tag, V>::new(0, cx, |state, _| {
210        let style = style.style_for(state, false);
211        Label::new(label, style.text.to_owned())
212            .aligned()
213            .contained()
214            .with_style(style.container)
215            .constrained()
216            .with_max_width(max_width)
217            .boxed()
218    })
219    .on_click(MouseButton::Left, f)
220    .with_cursor_style(platform::CursorStyle::PointingHand)
221}
222
223#[derive(Clone, Deserialize, Default)]
224pub struct ModalStyle {
225    close_icon: Interactive<IconStyle>,
226    container: ContainerStyle,
227    titlebar: ContainerStyle,
228    title_text: Interactive<TextStyle>,
229    dimensions: Dimensions,
230}
231
232impl ModalStyle {
233    pub fn dimensions(&self) -> Vector2F {
234        self.dimensions.to_vec()
235    }
236}
237
238pub fn modal<Tag, V, I, F>(
239    title: I,
240    style: &ModalStyle,
241    cx: &mut ViewContext<V>,
242    build_modal: F,
243) -> Element<V>
244where
245    Tag: 'static,
246    V: View,
247    I: Into<Cow<'static, str>>,
248    F: FnOnce(&mut gpui::ViewContext<V>) -> Element<V>,
249{
250    const TITLEBAR_HEIGHT: f32 = 28.;
251    // let active = cx.window_is_active(cx.window_id());
252
253    Flex::column()
254        .with_child(
255            Stack::new()
256                .with_children([
257                    Label::new(
258                        title,
259                        style
260                            .title_text
261                            .style_for(&mut MouseState::default(), false)
262                            .clone(),
263                    )
264                    .boxed(),
265                    // FIXME: Get a better tag type
266                    MouseEventHandler::<Tag, V>::new(999999, cx, |state, _cx| {
267                        let style = style.close_icon.style_for(state, false);
268                        icon(style).boxed()
269                    })
270                    .on_click(platform::MouseButton::Left, move |_, _, cx| {
271                        let window_id = cx.window_id();
272                        cx.remove_window(window_id);
273                    })
274                    .with_cursor_style(platform::CursorStyle::PointingHand)
275                    .aligned()
276                    .right()
277                    .boxed(),
278                ])
279                .contained()
280                .with_style(style.titlebar)
281                .constrained()
282                .with_height(TITLEBAR_HEIGHT)
283                .boxed(),
284        )
285        .with_child(
286            Container::new(build_modal(cx))
287                .with_style(style.container)
288                .constrained()
289                .with_width(style.dimensions().x())
290                .with_height(style.dimensions().y() - TITLEBAR_HEIGHT)
291                .boxed(),
292        )
293        .constrained()
294        .with_height(style.dimensions().y())
295        .boxed()
296}