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