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