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