1use std::rc::Rc;
2
3use gpui::{
4 AnyElement, App, InteractiveElement, IntoElement, ParentElement, RenderOnce, Styled, Window,
5 div, prelude::FluentBuilder, px,
6};
7use gpui_kit_semantics::{NodeSpec, Role, Semantic};
8use gpui_kit_theme::{ActiveTheme, Elevation, Radius, Space, Surface};
9
10use crate::foundation::{FocusRing, HoverLift, Ident, Pressable, Selectable, StyledExt};
11
12type ClickHandler = Rc<dyn Fn(&mut Window, &mut App)>;
13
14#[derive(IntoElement)]
16pub struct Card {
17 ident: Option<Ident>,
18 children: Vec<AnyElement>,
19 padded: bool,
20 on_click: Option<ClickHandler>,
21}
22
23impl std::fmt::Debug for Card {
24 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 formatter
26 .debug_struct("Card")
27 .field("ident", &self.ident)
28 .field("children", &self.children.len())
29 .finish()
30 }
31}
32
33impl Default for Card {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl Card {
40 pub fn new() -> Self {
41 Self {
42 ident: None,
43 children: Vec::new(),
44 padded: false,
45 on_click: None,
46 }
47 }
48
49 pub fn id(mut self, ident: impl Into<Ident>) -> Self {
50 self.ident = Some(ident.into());
51 self
52 }
53
54 pub fn padded(mut self, padded: bool) -> Self {
57 self.padded = padded;
58 self
59 }
60
61 pub fn on_click(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
67 self.on_click = Some(Rc::new(handler));
68 self
69 }
70
71 fn actionable(&self) -> bool {
72 self.ident.is_some() && self.on_click.is_some()
73 }
74}
75
76impl ParentElement for Card {
77 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
78 self.children.extend(elements);
79 }
80}
81
82impl RenderOnce for Card {
83 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
84 let theme = cx.theme().clone();
85 let actionable = self.actionable();
86 let frame = div()
87 .w_full()
88 .radius(&theme, Radius::Card)
89 .frame(&theme, Surface::Panel, Elevation::Raised)
90 .overflow_hidden()
91 .column()
92 .when(self.padded, |element| element.p_token(&theme, Space::Lg))
93 .children(self.children);
94
95 let Some(ident) = self.ident else {
96 return frame.into_any_element();
97 };
98 if !actionable {
99 return frame
100 .semantic_in(cx, NodeSpec::new(ident.semantic_id(), Role::Group))
101 .into_any_element();
102 }
103
104 let mut card = frame
108 .id(ident.element_id())
109 .cursor_pointer()
110 .tab_index(0)
111 .focus_ring(&theme)
112 .hover_lift(cx)
113 .pressable(cx);
114 let handler = self.on_click.clone().expect("an actionable card has one");
115 let click = Rc::clone(&handler);
116 card.interactivity()
117 .on_click(move |_, window, cx| click(window, cx));
118 card.interactivity().on_key_down(move |event, window, cx| {
119 if matches!(event.keystroke.key.as_str(), "enter" | "space") {
120 handler(window, cx);
121 cx.stop_propagation();
122 }
123 });
124
125 card.semantic_in(cx, NodeSpec::new(ident.semantic_id(), Role::Button))
126 .into_any_element()
127 }
128}
129
130#[derive(IntoElement)]
132pub struct ListRow {
133 ident: Option<Ident>,
134 selected: bool,
135 children: Vec<AnyElement>,
136 on_click: Option<ClickHandler>,
137}
138
139impl std::fmt::Debug for ListRow {
140 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 formatter
142 .debug_struct("ListRow")
143 .field("ident", &self.ident)
144 .field("selected", &self.selected)
145 .finish()
146 }
147}
148
149impl Default for ListRow {
150 fn default() -> Self {
151 Self::new()
152 }
153}
154
155impl ListRow {
156 pub fn new() -> Self {
157 Self {
158 ident: None,
159 selected: false,
160 children: Vec::new(),
161 on_click: None,
162 }
163 }
164
165 pub fn id(mut self, ident: impl Into<Ident>) -> Self {
166 self.ident = Some(ident.into());
167 self
168 }
169
170 pub fn on_click(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
172 self.on_click = Some(Rc::new(handler));
173 self
174 }
175
176 fn actionable(&self) -> bool {
177 self.ident.is_some() && self.on_click.is_some()
178 }
179}
180
181impl Selectable for ListRow {
182 fn selected(mut self, selected: bool) -> Self {
183 self.selected = selected;
184 self
185 }
186}
187
188impl ParentElement for ListRow {
189 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
190 self.children.extend(elements);
191 }
192}
193
194impl RenderOnce for ListRow {
195 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
196 let theme = cx.theme().clone();
197 let selected = self.selected;
198 let actionable = self.actionable();
199 let row = div()
200 .w_full()
201 .px(px(theme.spacing.lg + theme.spacing.xs))
202 .py(px(theme.spacing.md + 2.0))
203 .when(selected, |element| element.bg(theme.colors.selected))
204 .when(!selected, |element| {
205 element.hover(|style| style.bg(theme.colors.hover.opacity(0.3)))
206 })
207 .row()
208 .gap(px(theme.spacing.md + 2.0))
209 .children(self.children);
210
211 let Some(ident) = self.ident else {
212 return row.into_any_element();
213 };
214 let spec = NodeSpec::new(ident.semantic_id(), Role::Row).selected(selected);
215 if !actionable {
216 return row.semantic_in(cx, spec).into_any_element();
217 }
218
219 let mut row = row
223 .id(ident.element_id())
224 .cursor_pointer()
225 .tab_index(0)
226 .focus_ring(&theme)
227 .pressable(cx);
228 let handler = self.on_click.clone().expect("an actionable row has one");
229 let click = Rc::clone(&handler);
230 row.interactivity()
231 .on_click(move |_, window, cx| click(window, cx));
232 row.interactivity().on_key_down(move |event, window, cx| {
233 if matches!(event.keystroke.key.as_str(), "enter" | "space") {
234 handler(window, cx);
235 cx.stop_propagation();
236 }
237 });
238 row.semantic_in(cx, spec).into_any_element()
239 }
240}