1use gpui::{elements::SafeStylable, Action};
2
3use crate::{Interactive, Toggleable};
4
5use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, toggle::Toggle};
6
7pub type ToggleIconButtonStyle = Toggleable<Interactive<ButtonStyle<SvgStyle>>>;
8
9pub trait ComponentExt<C: SafeStylable> {
10 fn toggleable(self, active: bool) -> Toggle<C, ()>;
11 fn disclosable(self, disclosed: Option<bool>, action: Box<dyn Action>) -> Disclosable<C, ()>;
12}
13
14impl<C: SafeStylable> ComponentExt<C> for C {
15 fn toggleable(self, active: bool) -> Toggle<C, ()> {
16 Toggle::new(self, active)
17 }
18
19 /// Some(True) => disclosed => content is visible
20 /// Some(false) => closed => content is hidden
21 /// None => No disclosure button, but reserve disclosure spacing
22 fn disclosable(self, disclosed: Option<bool>, action: Box<dyn Action>) -> Disclosable<C, ()> {
23 Disclosable::new(disclosed, self, action)
24 }
25}
26
27pub mod disclosure {
28
29 use gpui::{
30 elements::{Component, Empty, Flex, ParentElement, SafeStylable},
31 Action, Element,
32 };
33 use schemars::JsonSchema;
34 use serde_derive::Deserialize;
35
36 use super::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle};
37
38 #[derive(Clone, Default, Deserialize, JsonSchema)]
39 pub struct DisclosureStyle<S> {
40 pub button: ToggleIconButtonStyle,
41 pub spacing: f32,
42 #[serde(flatten)]
43 content: S,
44 }
45
46 impl<S> DisclosureStyle<S> {
47 pub fn button_space(&self) -> f32 {
48 self.spacing + self.button.button_width.unwrap()
49 }
50 }
51
52 pub struct Disclosable<C, S> {
53 disclosed: Option<bool>,
54 action: Box<dyn Action>,
55 id: usize,
56 content: C,
57 style: S,
58 }
59
60 impl Disclosable<(), ()> {
61 pub fn new<C>(
62 disclosed: Option<bool>,
63 content: C,
64 action: Box<dyn Action>,
65 ) -> Disclosable<C, ()> {
66 Disclosable {
67 disclosed,
68 content,
69 action,
70 id: 0,
71 style: (),
72 }
73 }
74 }
75
76 impl<C> Disclosable<C, ()> {
77 pub fn with_id(self, id: usize) -> Disclosable<C, ()> {
78 Disclosable { id, ..self }
79 }
80 }
81
82 impl<C: SafeStylable> SafeStylable for Disclosable<C, ()> {
83 type Style = DisclosureStyle<C::Style>;
84
85 type Output = Disclosable<C, Self::Style>;
86
87 fn with_style(self, style: Self::Style) -> Self::Output {
88 Disclosable {
89 disclosed: self.disclosed,
90 action: self.action,
91 content: self.content,
92 id: self.id,
93 style,
94 }
95 }
96 }
97
98 impl<C: SafeStylable> Component for Disclosable<C, DisclosureStyle<C::Style>> {
99 fn render<V: gpui::View>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
100 Flex::row()
101 .with_child(if let Some(disclosed) = self.disclosed {
102 Button::dynamic_action(self.action)
103 .with_id(self.id)
104 .with_contents(Svg::new(if disclosed {
105 "icons/file_icons/chevron_down.svg"
106 } else {
107 "icons/file_icons/chevron_right.svg"
108 }))
109 .toggleable(disclosed)
110 .with_style(self.style.button)
111 .element()
112 .into_any()
113 } else {
114 Empty::new()
115 .into_any()
116 .constrained()
117 // TODO: Why is this optional at all?
118 .with_width(self.style.button.button_width.unwrap())
119 .into_any()
120 })
121 .with_child(Empty::new().constrained().with_width(self.style.spacing))
122 .with_child(
123 self.content
124 .with_style(self.style.content)
125 .render(cx)
126 .flex(1., true),
127 )
128 .align_children_center()
129 .into_any()
130 }
131 }
132}
133
134pub mod toggle {
135 use gpui::elements::{Component, SafeStylable};
136
137 use crate::Toggleable;
138
139 pub struct Toggle<C, S> {
140 style: S,
141 active: bool,
142 component: C,
143 }
144
145 impl<C: SafeStylable> Toggle<C, ()> {
146 pub fn new(component: C, active: bool) -> Self {
147 Toggle {
148 active,
149 component,
150 style: (),
151 }
152 }
153 }
154
155 impl<C: SafeStylable> SafeStylable for Toggle<C, ()> {
156 type Style = Toggleable<C::Style>;
157
158 type Output = Toggle<C, Self::Style>;
159
160 fn with_style(self, style: Self::Style) -> Self::Output {
161 Toggle {
162 active: self.active,
163 component: self.component,
164 style,
165 }
166 }
167 }
168
169 impl<C: SafeStylable> Component for Toggle<C, Toggleable<C::Style>> {
170 fn render<V: gpui::View>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
171 self.component
172 .with_style(self.style.in_state(self.active).clone())
173 .render(cx)
174 }
175 }
176}
177
178pub mod action_button {
179 use std::borrow::Cow;
180
181 use gpui::{
182 elements::{Component, ContainerStyle, MouseEventHandler, SafeStylable, TooltipStyle},
183 platform::{CursorStyle, MouseButton},
184 Action, Element, TypeTag, View,
185 };
186 use schemars::JsonSchema;
187 use serde_derive::Deserialize;
188
189 use crate::Interactive;
190
191 #[derive(Clone, Deserialize, Default, JsonSchema)]
192 pub struct ButtonStyle<C> {
193 #[serde(flatten)]
194 pub container: ContainerStyle,
195 // TODO: These are incorrect for the intended usage of the buttons.
196 // The size should be constant, but putting them here duplicates them
197 // across the states the buttons can be in
198 pub button_width: Option<f32>,
199 pub button_height: Option<f32>,
200 #[serde(flatten)]
201 contents: C,
202 }
203
204 pub struct Button<C, S> {
205 action: Box<dyn Action>,
206 tooltip: Option<(Cow<'static, str>, TooltipStyle)>,
207 tag: TypeTag,
208 id: usize,
209 contents: C,
210 style: Interactive<S>,
211 }
212
213 impl Button<(), ()> {
214 pub fn dynamic_action(action: Box<dyn Action>) -> Self {
215 Self {
216 contents: (),
217 tag: action.type_tag(),
218 style: Interactive::new_blank(),
219 tooltip: None,
220 id: 0,
221 action,
222 }
223 }
224
225 pub fn action<A: Action + Clone>(action: A) -> Self {
226 Self::dynamic_action(Box::new(action))
227 }
228
229 pub fn with_tooltip(
230 mut self,
231 tooltip: impl Into<Cow<'static, str>>,
232 tooltip_style: TooltipStyle,
233 ) -> Self {
234 self.tooltip = Some((tooltip.into(), tooltip_style));
235 self
236 }
237
238 pub fn with_id(mut self, id: usize) -> Self {
239 self.id = id;
240 self
241 }
242
243 pub fn with_contents<C: SafeStylable>(self, contents: C) -> Button<C, ()> {
244 Button {
245 action: self.action,
246 tag: self.tag,
247 style: self.style,
248 tooltip: self.tooltip,
249 id: self.id,
250 contents,
251 }
252 }
253 }
254
255 impl<C: SafeStylable> SafeStylable for Button<C, ()> {
256 type Style = Interactive<ButtonStyle<C::Style>>;
257 type Output = Button<C, ButtonStyle<C::Style>>;
258
259 fn with_style(self, style: Self::Style) -> Self::Output {
260 Button {
261 action: self.action,
262 tag: self.tag,
263 contents: self.contents,
264 tooltip: self.tooltip,
265 id: self.id,
266 style,
267 }
268 }
269 }
270
271 impl<C: SafeStylable> Component for Button<C, ButtonStyle<C::Style>> {
272 fn render<V: View>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
273 let mut button = MouseEventHandler::new_dynamic(self.tag, self.id, cx, |state, cx| {
274 let style = self.style.style_for(state);
275 let mut contents = self
276 .contents
277 .with_style(style.contents.to_owned())
278 .render(cx)
279 .contained()
280 .with_style(style.container)
281 .constrained();
282
283 if let Some(height) = style.button_height {
284 contents = contents.with_height(height);
285 }
286
287 if let Some(width) = style.button_width {
288 contents = contents.with_width(width);
289 }
290
291 contents.into_any()
292 })
293 .on_click(MouseButton::Left, {
294 let action = self.action.boxed_clone();
295 move |_, _, cx| {
296 let window = cx.window();
297 let view = cx.view_id();
298 let action = action.boxed_clone();
299 cx.spawn(|_, mut cx| async move {
300 window.dispatch_action(view, action.as_ref(), &mut cx)
301 })
302 .detach();
303 }
304 })
305 .with_cursor_style(CursorStyle::PointingHand)
306 .into_any();
307
308 if let Some((tooltip, style)) = self.tooltip {
309 button = button
310 .with_dynamic_tooltip(self.tag, 0, tooltip, Some(self.action), style, cx)
311 .into_any()
312 }
313
314 button
315 }
316 }
317}
318
319pub mod svg {
320 use std::borrow::Cow;
321
322 use gpui::{
323 elements::{Component, Empty, SafeStylable},
324 Element,
325 };
326 use schemars::JsonSchema;
327 use serde::Deserialize;
328
329 #[derive(Clone, Default, JsonSchema)]
330 pub struct SvgStyle {
331 icon_width: f32,
332 icon_height: f32,
333 color: gpui::color::Color,
334 }
335
336 impl<'de> Deserialize<'de> for SvgStyle {
337 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
338 where
339 D: serde::Deserializer<'de>,
340 {
341 #[derive(Deserialize)]
342 #[serde(untagged)]
343 pub enum IconSize {
344 IconSize { icon_size: f32 },
345 Dimensions { width: f32, height: f32 },
346 IconDimensions { icon_width: f32, icon_height: f32 },
347 }
348
349 #[derive(Deserialize)]
350 struct SvgStyleHelper {
351 #[serde(flatten)]
352 size: IconSize,
353 color: gpui::color::Color,
354 }
355
356 let json = SvgStyleHelper::deserialize(deserializer)?;
357 let color = json.color;
358
359 let result = match json.size {
360 IconSize::IconSize { icon_size } => SvgStyle {
361 icon_width: icon_size,
362 icon_height: icon_size,
363 color,
364 },
365 IconSize::Dimensions { width, height } => SvgStyle {
366 icon_width: width,
367 icon_height: height,
368 color,
369 },
370 IconSize::IconDimensions {
371 icon_width,
372 icon_height,
373 } => SvgStyle {
374 icon_width,
375 icon_height,
376 color,
377 },
378 };
379
380 Ok(result)
381 }
382 }
383
384 pub struct Svg<S> {
385 path: Option<Cow<'static, str>>,
386 style: S,
387 }
388
389 impl Svg<()> {
390 pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
391 Self {
392 path: Some(path.into()),
393 style: (),
394 }
395 }
396
397 pub fn optional(path: Option<impl Into<Cow<'static, str>>>) -> Self {
398 Self {
399 path: path.map(Into::into),
400 style: (),
401 }
402 }
403 }
404
405 impl SafeStylable for Svg<()> {
406 type Style = SvgStyle;
407
408 type Output = Svg<SvgStyle>;
409
410 fn with_style(self, style: Self::Style) -> Self::Output {
411 Svg {
412 path: self.path,
413 style,
414 }
415 }
416 }
417
418 impl Component for Svg<SvgStyle> {
419 fn render<V: gpui::View>(self, _: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
420 if let Some(path) = self.path {
421 gpui::elements::Svg::new(path)
422 .with_color(self.style.color)
423 .constrained()
424 } else {
425 Empty::new().constrained()
426 }
427 .constrained()
428 .with_width(self.style.icon_width)
429 .with_height(self.style.icon_height)
430 .into_any()
431 }
432 }
433}
434
435pub mod label {
436 use std::borrow::Cow;
437
438 use gpui::{
439 elements::{Component, LabelStyle, SafeStylable},
440 Element,
441 };
442
443 pub struct Label<S> {
444 text: Cow<'static, str>,
445 style: S,
446 }
447
448 impl Label<()> {
449 pub fn new(text: impl Into<Cow<'static, str>>) -> Self {
450 Self {
451 text: text.into(),
452 style: (),
453 }
454 }
455 }
456
457 impl SafeStylable for Label<()> {
458 type Style = LabelStyle;
459
460 type Output = Label<LabelStyle>;
461
462 fn with_style(self, style: Self::Style) -> Self::Output {
463 Label {
464 text: self.text,
465 style,
466 }
467 }
468 }
469
470 impl Component for Label<LabelStyle> {
471 fn render<V: gpui::View>(self, _: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
472 gpui::elements::Label::new(self.text, self.style).into_any()
473 }
474 }
475}