ui.rs

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