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