1use gpui::{
2 div, prelude::FluentBuilder as _, px, AnyElement, App, Axis, DefiniteLength, IntoElement,
3 ParentElement, RenderOnce, SharedString, Styled, Window,
4};
5
6use crate::{h_flex, text::Text, v_flex, ActiveTheme as _, AxisExt, Sizable, Size};
7
8#[derive(IntoElement)]
10pub struct DescriptionList {
11 items: Vec<DescriptionItem>,
12 size: Size,
13 layout: Axis,
14 label_width: DefiniteLength,
15 bordered: bool,
16 columns: usize,
17}
18
19pub enum DescriptionItem {
21 Item {
22 label: DescriptionText,
23 value: DescriptionText,
24 span: usize,
25 },
26 Divider,
27}
28
29#[derive(IntoElement)]
31pub enum DescriptionText {
32 String(SharedString),
33 Text(Text),
34 AnyElement(AnyElement),
35}
36
37impl From<&str> for DescriptionText {
38 fn from(text: &str) -> Self {
39 DescriptionText::String(SharedString::from(text.to_string()))
40 }
41}
42
43impl From<Text> for DescriptionText {
44 fn from(text: Text) -> Self {
45 DescriptionText::Text(text)
46 }
47}
48
49impl From<AnyElement> for DescriptionText {
50 fn from(element: AnyElement) -> Self {
51 DescriptionText::AnyElement(element)
52 }
53}
54
55impl From<SharedString> for DescriptionText {
56 fn from(text: SharedString) -> Self {
57 DescriptionText::String(text)
58 }
59}
60
61impl From<String> for DescriptionText {
62 fn from(text: String) -> Self {
63 DescriptionText::String(SharedString::from(text))
64 }
65}
66
67impl RenderOnce for DescriptionText {
68 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
69 match self {
70 DescriptionText::String(text) => div().child(text).into_any_element(),
71 DescriptionText::Text(text) => text.into_any_element(),
72 DescriptionText::AnyElement(element) => element,
73 }
74 }
75}
76
77impl DescriptionItem {
78 pub fn new(label: impl Into<DescriptionText>) -> Self {
82 DescriptionItem::Item {
83 label: label.into(),
84 value: "".into(),
85 span: 1,
86 }
87 }
88
89 pub fn value(mut self, value: impl Into<DescriptionText>) -> Self {
91 let new_value = value.into();
92 if let DescriptionItem::Item { value, .. } = &mut self {
93 *value = new_value;
94 }
95 self
96 }
97
98 pub fn span(mut self, span: usize) -> Self {
102 let val = span;
103 if let DescriptionItem::Item { span, .. } = &mut self {
104 *span = val;
105 }
106 self
107 }
108
109 fn _label(&self) -> Option<&DescriptionText> {
110 match self {
111 DescriptionItem::Item { label, .. } => Some(label),
112 _ => None,
113 }
114 }
115
116 fn _span(&self) -> Option<usize> {
117 match self {
118 DescriptionItem::Item { span, .. } => Some(*span),
119 _ => None,
120 }
121 }
122}
123
124impl DescriptionList {
125 pub fn new() -> Self {
127 Self {
128 items: Vec::new(),
129 layout: Axis::Horizontal,
130 label_width: px(120.).into(),
131 size: Size::default(),
132 bordered: true,
133 columns: 3,
134 }
135 }
136
137 pub fn vertical() -> Self {
139 Self::new().layout(Axis::Vertical)
140 }
141
142 pub fn horizontal() -> Self {
144 Self::new().layout(Axis::Horizontal)
145 }
146
147 pub fn label_width(mut self, label_width: impl Into<DefiniteLength>) -> Self {
151 self.label_width = label_width.into();
152 self
153 }
154
155 pub fn layout(mut self, layout: Axis) -> Self {
157 self.layout = layout;
158 self
159 }
160
161 pub fn bordered(mut self, bordered: bool) -> Self {
165 self.bordered = bordered;
166 self
167 }
168
169 pub fn columns(mut self, columns: usize) -> Self {
173 self.columns = columns.clamp(1, 10);
174 self
175 }
176
177 pub fn item(
179 mut self,
180 label: impl Into<DescriptionText>,
181 value: impl Into<DescriptionText>,
182 span: usize,
183 ) -> Self {
184 self.items.push(DescriptionItem::Item {
185 label: label.into(),
186 value: value.into(),
187 span,
188 });
189 self
190 }
191
192 pub fn child(mut self, child: impl Into<DescriptionItem>) -> Self {
194 self.items.push(child.into());
195 self
196 }
197
198 pub fn children(
200 mut self,
201 children: impl IntoIterator<Item = impl Into<DescriptionItem>>,
202 ) -> Self {
203 self.items
204 .extend(children.into_iter().map(Into::into).collect::<Vec<_>>());
205 self
206 }
207
208 pub fn divider(mut self) -> Self {
210 self.items.push(DescriptionItem::Divider);
211 self
212 }
213
214 fn group_item_rows(items: Vec<DescriptionItem>, columns: usize) -> Vec<Vec<DescriptionItem>> {
215 let mut rows = vec![];
216 let mut current_span = 0;
217 for item in items.into_iter() {
218 let span = item._span().unwrap_or(columns);
219 if rows.is_empty() {
220 rows.push(vec![]);
221 }
222 if current_span + span > columns {
223 rows.push(vec![]);
224 current_span = 0;
225 }
226 let last_group = rows.last_mut().unwrap();
227 last_group.push(item);
228 current_span += span;
229 }
230 while let Some(last_group) = rows.last() {
232 if !last_group.is_empty() {
233 break;
234 }
235
236 rows.pop();
237 }
238
239 rows
240 }
241}
242
243impl Sizable for DescriptionList {
244 fn with_size(mut self, size: impl Into<Size>) -> Self {
245 self.size = size.into();
246 self
247 }
248}
249
250impl RenderOnce for DescriptionList {
251 fn render(self, _: &mut Window, cx: &mut gpui::App) -> impl gpui::IntoElement {
252 let base_gap = match self.size {
253 Size::XSmall | Size::Small => px(2.),
254 Size::Medium => px(4.),
255 Size::Large => px(8.),
256 _ => px(4.),
257 };
258
259 let (mut padding_x, mut padding_y) = match self.size {
261 Size::XSmall | Size::Small => (px(4.), px(2.)),
262 Size::Medium => (px(8.), px(4.)),
263 Size::Large => (px(12.), px(6.)),
264 _ => (px(8.), px(4.)),
265 };
266
267 let label_width = if self.layout.is_horizontal() {
268 Some(self.label_width)
269 } else {
270 None
271 };
272 if !self.bordered {
273 padding_x = px(0.);
274 padding_y = px(0.);
275 }
276 let gap = if self.bordered { px(0.) } else { base_gap };
277
278 let rows = Self::group_item_rows(self.items, self.columns);
280 let rows_len = rows.len();
281
282 v_flex()
283 .gap(gap)
284 .overflow_hidden()
285 .when(self.bordered, |this| {
286 this.rounded(padding_x)
287 .border_1()
288 .border_color(cx.theme().border)
289 })
290 .children(rows.into_iter().enumerate().map(|(ix, items)| {
291 let is_last = ix == rows_len - 1;
292 h_flex()
293 .when(self.bordered && !is_last, |this| {
294 this.border_b_1().border_color(cx.theme().border)
295 })
296 .children({
297 items.into_iter().enumerate().map(|(item_ix, item)| {
298 let is_first_col = item_ix == 0;
299
300 match item {
301 DescriptionItem::Item { label, value, .. } => {
302 let el = if self.layout.is_vertical() {
303 v_flex()
304 } else {
305 div().flex().flex_row().h_full()
306 };
307
308 el.flex_1()
309 .overflow_x_hidden()
310 .child(
311 div()
312 .when(self.layout.is_horizontal(), |this| {
313 this.h_full()
314 })
315 .text_color(
316 cx.theme().description_list_label_foreground,
317 )
318 .text_sm()
319 .px(padding_x)
320 .py(padding_y)
321 .when(self.bordered, |this| {
322 this.when(self.layout.is_horizontal(), |this| {
323 this.border_r_1()
324 .when(!is_first_col, |this| {
325 this.border_l_1()
326 })
327 })
328 .when(self.layout.is_vertical(), |this| {
329 this.border_b_1()
330 })
331 .border_color(cx.theme().border)
332 .bg(cx.theme().description_list_label)
333 })
334 .map(|this| match label_width {
335 Some(label_width) => {
336 this.w(label_width).flex_shrink_0()
337 }
338 None => this,
339 })
340 .child(label),
341 )
342 .child(
343 div()
344 .flex_1()
345 .px(padding_x)
346 .py(padding_y)
347 .overflow_hidden()
348 .child(value),
349 )
350 }
351 _ => div().h_2().w_full().when(self.bordered, |this| {
352 this.bg(cx.theme().description_list_label)
353 }),
354 }
355 })
356 })
357 }))
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::DescriptionItem;
364
365 #[test]
366 fn test_group_item_rows() {
367 let items = vec![
368 DescriptionItem::new("test1"),
369 DescriptionItem::new("test2").span(2),
370 DescriptionItem::new("test3"),
371 DescriptionItem::new("test4"),
372 DescriptionItem::new("test5"),
373 DescriptionItem::new("test6").span(3),
374 DescriptionItem::new("test7"),
375 ];
376 let rows = super::DescriptionList::group_item_rows(items, 3);
377 assert_eq!(rows.len(), 4);
378 assert_eq!(rows[0].len(), 2);
379 assert_eq!(rows[1].len(), 3);
380 assert_eq!(rows[2].len(), 1);
381 assert_eq!(rows[3].len(), 1);
382 }
383}