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