1use component::{example_group_with_title, single_example};
2use gpui::{ClickEvent, transparent_black};
3use smallvec::SmallVec;
4use ui::{prelude::*, utils::CornerSolver};
5
6#[derive(IntoElement, RegisterComponent)]
7pub struct SelectableTile {
8 id: ElementId,
9 width: DefiniteLength,
10 height: DefiniteLength,
11 parent_focused: bool,
12 selected: bool,
13 children: SmallVec<[AnyElement; 2]>,
14 on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
15}
16
17impl SelectableTile {
18 pub fn new(id: impl Into<ElementId>) -> Self {
19 Self {
20 id: id.into(),
21 width: px(120.).into(),
22 height: px(120.).into(),
23 parent_focused: false,
24 selected: false,
25 children: SmallVec::new(),
26 on_click: None,
27 }
28 }
29
30 pub fn w(mut self, width: impl Into<DefiniteLength>) -> Self {
31 self.width = width.into();
32 self
33 }
34
35 pub fn h(mut self, height: impl Into<DefiniteLength>) -> Self {
36 self.height = height.into();
37 self
38 }
39
40 pub fn parent_focused(mut self, focused: bool) -> Self {
41 self.parent_focused = focused;
42 self
43 }
44
45 pub fn selected(mut self, selected: bool) -> Self {
46 self.selected = selected;
47 self
48 }
49
50 pub fn on_click(
51 mut self,
52 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
53 ) -> Self {
54 self.on_click = Some(Box::new(handler));
55 self
56 }
57}
58
59impl RenderOnce for SelectableTile {
60 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
61 let ring_corner_radius = px(8.);
62 let ring_width = px(1.);
63 let padding = px(2.);
64 let content_border_width = px(0.);
65 let content_border_radius = CornerSolver::child_radius(
66 ring_corner_radius,
67 ring_width,
68 padding,
69 content_border_width,
70 );
71
72 let mut element = h_flex()
73 .id(self.id)
74 .w(self.width)
75 .h(self.height)
76 .overflow_hidden()
77 .rounded(ring_corner_radius)
78 .border(ring_width)
79 .border_color(if self.selected && self.parent_focused {
80 cx.theme().colors().border
81 } else if self.selected {
82 cx.theme().status().info
83 } else {
84 transparent_black()
85 })
86 .p(padding)
87 .child(
88 h_flex()
89 .size_full()
90 .rounded(content_border_radius)
91 .items_center()
92 .justify_center()
93 .shadow_hairline()
94 .bg(cx.theme().colors().surface_background)
95 .children(self.children),
96 );
97
98 if let Some(on_click) = self.on_click {
99 element = element.on_click(move |event, window, cx| {
100 on_click(event, window, cx);
101 });
102 }
103
104 element
105 }
106}
107
108impl ParentElement for SelectableTile {
109 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
110 self.children.extend(elements)
111 }
112}
113
114impl Component for SelectableTile {
115 fn scope() -> ComponentScope {
116 ComponentScope::Layout
117 }
118
119 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
120 let states = example_group_with_title(
121 "SelectableTile States",
122 vec![
123 single_example(
124 "Default",
125 SelectableTile::new("default")
126 .w(px(120.))
127 .h(px(120.))
128 .parent_focused(false)
129 .selected(false)
130 .child(
131 div()
132 .p_4()
133 .child(Icon::new(IconName::Check).size(IconSize::Medium)),
134 )
135 .into_any_element(),
136 ),
137 single_example(
138 "Selected",
139 SelectableTile::new("selected")
140 .w(px(120.))
141 .h(px(120.))
142 .parent_focused(false)
143 .selected(true)
144 .child(
145 div()
146 .p_4()
147 .child(Icon::new(IconName::Check).size(IconSize::Medium)),
148 )
149 .into_any_element(),
150 ),
151 single_example(
152 "Selected & Parent Focused",
153 SelectableTile::new("selected_focused")
154 .w(px(120.))
155 .h(px(120.))
156 .parent_focused(true)
157 .selected(true)
158 .child(
159 div()
160 .p_4()
161 .child(Icon::new(IconName::Check).size(IconSize::Medium)),
162 )
163 .into_any_element(),
164 ),
165 ],
166 );
167
168 Some(v_flex().p_4().gap_4().child(states).into_any_element())
169 }
170}