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