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