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