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