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