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 schemars::JsonSchema;
 16use serde::Deserialize;
 17
 18use crate::{ContainedText, Interactive};
 19
 20#[derive(Clone, Deserialize, Default, JsonSchema)]
 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
 96pub fn svg<V: View>(style: &SvgStyle) -> ConstrainedBox<V> {
 97    Svg::new(style.asset.clone())
 98        .with_color(style.color)
 99        .constrained()
100        .with_width(style.dimensions.width)
101        .with_height(style.dimensions.height)
102}
103
104#[derive(Clone, Deserialize, Default, JsonSchema)]
105pub struct IconStyle {
106    pub icon: SvgStyle,
107    pub container: ContainerStyle,
108}
109
110pub fn icon<V: View>(style: &IconStyle) -> Container<V> {
111    svg(&style.icon).contained().with_style(style.container)
112}
113
114pub fn keystroke_label<V: View>(
115    label_text: &'static str,
116    label_style: &ContainedText,
117    keystroke_style: &ContainedText,
118    action: Box<dyn Action>,
119    cx: &mut ViewContext<V>,
120) -> Container<V> {
121    // FIXME: Put the theme in it's own global so we can
122    // query the keystroke style on our own
123    Flex::row()
124        .with_child(Label::new(label_text, label_style.text.clone()).contained())
125        .with_child(
126            KeystrokeLabel::new(
127                cx.view_id(),
128                action,
129                keystroke_style.container,
130                keystroke_style.text.clone(),
131            )
132            .flex_float(),
133        )
134        .contained()
135        .with_style(label_style.container)
136}
137
138pub type ButtonStyle = Interactive<ContainedText>;
139
140pub fn cta_button<Tag, L, V, F>(
141    label: L,
142    max_width: f32,
143    style: &ButtonStyle,
144    cx: &mut ViewContext<V>,
145    f: F,
146) -> MouseEventHandler<Tag, V>
147where
148    Tag: 'static,
149    L: Into<Cow<'static, str>>,
150    V: View,
151    F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
152{
153    MouseEventHandler::<Tag, V>::new(0, cx, |state, _| {
154        let style = style.style_for(state);
155        Label::new(label, style.text.to_owned())
156            .aligned()
157            .contained()
158            .with_style(style.container)
159            .constrained()
160            .with_max_width(max_width)
161    })
162    .on_click(MouseButton::Left, f)
163    .with_cursor_style(platform::CursorStyle::PointingHand)
164}
165
166#[derive(Clone, Deserialize, Default, JsonSchema)]
167pub struct ModalStyle {
168    close_icon: Interactive<IconStyle>,
169    container: ContainerStyle,
170    titlebar: ContainerStyle,
171    title_text: Interactive<TextStyle>,
172    dimensions: Dimensions,
173}
174
175impl ModalStyle {
176    pub fn dimensions(&self) -> Vector2F {
177        self.dimensions.to_vec()
178    }
179}
180
181pub fn modal<Tag, V, I, D, F>(
182    title: I,
183    style: &ModalStyle,
184    cx: &mut ViewContext<V>,
185    build_modal: F,
186) -> impl Element<V>
187where
188    Tag: 'static,
189    V: View,
190    I: Into<Cow<'static, str>>,
191    D: Element<V>,
192    F: FnOnce(&mut gpui::ViewContext<V>) -> D,
193{
194    const TITLEBAR_HEIGHT: f32 = 28.;
195    // let active = cx.window_is_active(cx.window_id());
196
197    Flex::column()
198        .with_child(
199            Stack::new()
200                .with_child(Label::new(
201                    title,
202                    style
203                        .title_text
204                        .style_for(&mut MouseState::default())
205                        .clone(),
206                ))
207                .with_child(
208                    // FIXME: Get a better tag type
209                    MouseEventHandler::<Tag, V>::new(999999, cx, |state, _cx| {
210                        let style = style.close_icon.style_for(state);
211                        icon(style)
212                    })
213                    .on_click(platform::MouseButton::Left, move |_, _, cx| {
214                        cx.remove_window();
215                    })
216                    .with_cursor_style(platform::CursorStyle::PointingHand)
217                    .aligned()
218                    .right(),
219                )
220                .contained()
221                .with_style(style.titlebar)
222                .constrained()
223                .with_height(TITLEBAR_HEIGHT),
224        )
225        .with_child(
226            build_modal(cx)
227                .contained()
228                .with_style(style.container)
229                .constrained()
230                .with_width(style.dimensions().x())
231                .with_height(style.dimensions().y() - TITLEBAR_HEIGHT),
232        )
233        .constrained()
234        .with_height(style.dimensions().y())
235}