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