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