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}