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