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