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