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, Drawable, Element, 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: Element<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.handle().id(),
139 label_text,
140 label_style,
141 keystroke_style,
142 action,
143 )
144}
145
146pub fn keystroke_label_for<V: View>(
147 view_id: usize,
148 label_text: &'static str,
149 label_style: &ContainedText,
150 keystroke_style: &ContainedText,
151 action: Box<dyn Action>,
152) -> Container<V> {
153 Flex::row()
154 .with_child(
155 Label::new(label_text, label_style.text.clone())
156 .contained()
157 .boxed(),
158 )
159 .with_child({
160 KeystrokeLabel::new(
161 view_id,
162 action,
163 keystroke_style.container,
164 keystroke_style.text.clone(),
165 )
166 .flex_float()
167 .boxed()
168 })
169 .contained()
170 .with_style(label_style.container)
171}
172
173pub type ButtonStyle = Interactive<ContainedText>;
174
175pub fn cta_button<L, A, V>(
176 label: L,
177 action: A,
178 max_width: f32,
179 style: &ButtonStyle,
180 cx: &mut ViewContext<V>,
181) -> Element<V>
182where
183 L: Into<Cow<'static, str>>,
184 A: 'static + Action + Clone,
185 V: View,
186{
187 cta_button_with_click::<A, _, _, _>(label, max_width, style, cx, move |_, _, cx| {
188 cx.dispatch_action(action.clone())
189 })
190 .boxed()
191}
192
193pub fn cta_button_with_click<Tag, L, V, F>(
194 label: L,
195 max_width: f32,
196 style: &ButtonStyle,
197 cx: &mut ViewContext<V>,
198 f: F,
199) -> MouseEventHandler<Tag, V>
200where
201 Tag: 'static,
202 L: Into<Cow<'static, str>>,
203 V: View,
204 F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
205{
206 MouseEventHandler::<Tag, V>::new(0, cx, |state, _| {
207 let style = style.style_for(state, false);
208 Label::new(label, style.text.to_owned())
209 .aligned()
210 .contained()
211 .with_style(style.container)
212 .constrained()
213 .with_max_width(max_width)
214 .boxed()
215 })
216 .on_click(MouseButton::Left, f)
217 .with_cursor_style(platform::CursorStyle::PointingHand)
218}
219
220#[derive(Clone, Deserialize, Default)]
221pub struct ModalStyle {
222 close_icon: Interactive<IconStyle>,
223 container: ContainerStyle,
224 titlebar: ContainerStyle,
225 title_text: Interactive<TextStyle>,
226 dimensions: Dimensions,
227}
228
229impl ModalStyle {
230 pub fn dimensions(&self) -> Vector2F {
231 self.dimensions.to_vec()
232 }
233}
234
235pub fn modal<Tag, V, I, F>(
236 title: I,
237 style: &ModalStyle,
238 cx: &mut ViewContext<V>,
239 build_modal: F,
240) -> Element<V>
241where
242 Tag: 'static,
243 V: View,
244 I: Into<Cow<'static, str>>,
245 F: FnOnce(&mut gpui::ViewContext<V>) -> Element<V>,
246{
247 const TITLEBAR_HEIGHT: f32 = 28.;
248 // let active = cx.window_is_active(cx.window_id());
249
250 Flex::column()
251 .with_child(
252 Stack::new()
253 .with_children([
254 Label::new(
255 title,
256 style
257 .title_text
258 .style_for(&mut MouseState::default(), false)
259 .clone(),
260 )
261 .boxed(),
262 // FIXME: Get a better tag type
263 MouseEventHandler::<Tag, V>::new(999999, cx, |state, _cx| {
264 let style = style.close_icon.style_for(state, false);
265 icon(style).boxed()
266 })
267 .on_click(platform::MouseButton::Left, move |_, _, cx| {
268 let window_id = cx.window_id();
269 cx.remove_window(window_id);
270 })
271 .with_cursor_style(platform::CursorStyle::PointingHand)
272 .aligned()
273 .right()
274 .boxed(),
275 ])
276 .contained()
277 .with_style(style.titlebar)
278 .constrained()
279 .with_height(TITLEBAR_HEIGHT)
280 .boxed(),
281 )
282 .with_child(
283 Container::new(build_modal(cx))
284 .with_style(style.container)
285 .constrained()
286 .with_width(style.dimensions().x())
287 .with_height(style.dimensions().y() - TITLEBAR_HEIGHT)
288 .boxed(),
289 )
290 .constrained()
291 .with_height(style.dimensions().y())
292 .boxed()
293}