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