ui.rs

  1use std::borrow::Cow;
  2
  3use gpui::{
  4    elements::{
  5        ConstrainedBox, Container, ContainerStyle, Dimensions, Empty, Flex, KeystrokeLabel, Label,
  6        MouseEventHandler, ParentElement, Stack, Svg, SvgStyle,
  7    },
  8    fonts::TextStyle,
  9    geometry::vector::Vector2F,
 10    platform,
 11    platform::MouseButton,
 12    scene::MouseClick,
 13    Action, Element, EventContext, MouseState, View, ViewContext,
 14};
 15use serde::Deserialize;
 16
 17use crate::{ContainedText, Interactive};
 18
 19#[derive(Clone, Deserialize, Default)]
 20pub struct CheckboxStyle {
 21    pub icon: SvgStyle,
 22    pub label: ContainedText,
 23    pub default: ContainerStyle,
 24    pub checked: ContainerStyle,
 25    pub hovered: ContainerStyle,
 26    pub hovered_and_checked: ContainerStyle,
 27}
 28
 29pub fn checkbox<Tag, V, F>(
 30    label: &'static str,
 31    style: &CheckboxStyle,
 32    checked: bool,
 33    id: usize,
 34    cx: &mut ViewContext<V>,
 35    change: F,
 36) -> MouseEventHandler<Tag, V>
 37where
 38    Tag: 'static,
 39    V: View,
 40    F: 'static + Fn(&mut V, bool, &mut EventContext<V>),
 41{
 42    let label = Label::new(label, style.label.text.clone())
 43        .contained()
 44        .with_style(style.label.container);
 45    checkbox_with_label(label, style, checked, id, cx, change)
 46}
 47
 48pub fn checkbox_with_label<Tag, D, V, F>(
 49    label: D,
 50    style: &CheckboxStyle,
 51    checked: bool,
 52    id: usize,
 53    cx: &mut ViewContext<V>,
 54    change: F,
 55) -> MouseEventHandler<Tag, V>
 56where
 57    Tag: 'static,
 58    D: Element<V>,
 59    V: View,
 60    F: 'static + Fn(&mut V, bool, &mut EventContext<V>),
 61{
 62    MouseEventHandler::new(id, cx, |state, _| {
 63        let indicator = if checked {
 64            svg(&style.icon)
 65        } else {
 66            Empty::new()
 67                .constrained()
 68                .with_width(style.icon.dimensions.width)
 69                .with_height(style.icon.dimensions.height)
 70        };
 71
 72        Flex::row()
 73            .with_child(indicator.contained().with_style(if checked {
 74                if state.hovered() {
 75                    style.hovered_and_checked
 76                } else {
 77                    style.checked
 78                }
 79            } else {
 80                if state.hovered() {
 81                    style.hovered
 82                } else {
 83                    style.default
 84                }
 85            }))
 86            .with_child(label)
 87            .align_children_center()
 88    })
 89    .on_click(platform::MouseButton::Left, move |_, view, cx| {
 90        change(view, !checked, cx)
 91    })
 92    .with_cursor_style(platform::CursorStyle::PointingHand)
 93}
 94
 95pub fn svg<V: View>(style: &SvgStyle) -> ConstrainedBox<V> {
 96    Svg::new(style.asset.clone())
 97        .with_color(style.color)
 98        .constrained()
 99        .with_width(style.dimensions.width)
100        .with_height(style.dimensions.height)
101}
102
103#[derive(Clone, Deserialize, Default)]
104pub struct IconStyle {
105    pub icon: SvgStyle,
106    pub container: ContainerStyle,
107}
108
109pub fn icon<V: View>(style: &IconStyle) -> Container<V> {
110    svg(&style.icon).contained().with_style(style.container)
111}
112
113pub fn keystroke_label<V: View>(
114    label_text: &'static str,
115    label_style: &ContainedText,
116    keystroke_style: &ContainedText,
117    action: Box<dyn Action>,
118    cx: &mut ViewContext<V>,
119) -> Container<V> {
120    // FIXME: Put the theme in it's own global so we can
121    // query the keystroke style on our own
122    Flex::row()
123        .with_child(Label::new(label_text, label_style.text.clone()).contained())
124        .with_child(
125            KeystrokeLabel::new(
126                cx.view_id(),
127                action,
128                keystroke_style.container,
129                keystroke_style.text.clone(),
130            )
131            .flex_float(),
132        )
133        .contained()
134        .with_style(label_style.container)
135}
136
137pub type ButtonStyle = Interactive<ContainedText>;
138
139pub fn cta_button<Tag, L, V, F>(
140    label: L,
141    max_width: f32,
142    style: &ButtonStyle,
143    cx: &mut ViewContext<V>,
144    f: F,
145) -> MouseEventHandler<Tag, V>
146where
147    Tag: 'static,
148    L: Into<Cow<'static, str>>,
149    V: View,
150    F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
151{
152    MouseEventHandler::<Tag, V>::new(0, cx, |state, _| {
153        let style = style.style_for(state);
154        Label::new(label, style.text.to_owned())
155            .aligned()
156            .contained()
157            .with_style(style.container)
158            .constrained()
159            .with_max_width(max_width)
160    })
161    .on_click(MouseButton::Left, f)
162    .with_cursor_style(platform::CursorStyle::PointingHand)
163}
164
165#[derive(Clone, Deserialize, Default)]
166pub struct ModalStyle {
167    close_icon: Interactive<IconStyle>,
168    container: ContainerStyle,
169    titlebar: ContainerStyle,
170    title_text: Interactive<TextStyle>,
171    dimensions: Dimensions,
172}
173
174impl ModalStyle {
175    pub fn dimensions(&self) -> Vector2F {
176        self.dimensions.to_vec()
177    }
178}
179
180pub fn modal<Tag, V, I, D, F>(
181    title: I,
182    style: &ModalStyle,
183    cx: &mut ViewContext<V>,
184    build_modal: F,
185) -> impl Element<V>
186where
187    Tag: 'static,
188    V: View,
189    I: Into<Cow<'static, str>>,
190    D: Element<V>,
191    F: FnOnce(&mut gpui::ViewContext<V>) -> D,
192{
193    const TITLEBAR_HEIGHT: f32 = 28.;
194    // let active = cx.window_is_active(cx.window_id());
195
196    Flex::column()
197        .with_child(
198            Stack::new()
199                .with_child(Label::new(
200                    title,
201                    style
202                        .title_text
203                        .style_for(&mut MouseState::default())
204                        .clone(),
205                ))
206                .with_child(
207                    // FIXME: Get a better tag type
208                    MouseEventHandler::<Tag, V>::new(999999, cx, |state, _cx| {
209                        let style = style.close_icon.style_for(state);
210                        icon(style)
211                    })
212                    .on_click(platform::MouseButton::Left, move |_, _, cx| {
213                        cx.remove_window();
214                    })
215                    .with_cursor_style(platform::CursorStyle::PointingHand)
216                    .aligned()
217                    .right(),
218                )
219                .contained()
220                .with_style(style.titlebar)
221                .constrained()
222                .with_height(TITLEBAR_HEIGHT),
223        )
224        .with_child(
225            build_modal(cx)
226                .contained()
227                .with_style(style.container)
228                .constrained()
229                .with_width(style.dimensions().x())
230                .with_height(style.dimensions().y() - TITLEBAR_HEIGHT),
231        )
232        .constrained()
233        .with_height(style.dimensions().y())
234}