liora_components/descriptions.rs
1use gpui::{AnyElement, App, IntoElement, RenderOnce, SharedString, Window, div, prelude::*, px};
2use liora_core::Config;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5pub enum DescriptionsDirection {
6 #[default]
7 Horizontal,
8 Vertical,
9}
10
11pub struct DescriptionItem {
12 pub label: SharedString,
13 pub value: AnyElement,
14 pub span: u32,
15}
16
17pub struct Descriptions {
18 title: Option<SharedString>,
19 extra: Option<AnyElement>,
20 column: u32,
21 direction: DescriptionsDirection,
22 border: bool,
23 items: Vec<DescriptionItem>,
24}
25
26impl Descriptions {
27 pub fn new() -> Self {
28 Self {
29 title: None,
30 extra: None,
31 column: 3,
32 direction: DescriptionsDirection::Horizontal,
33 border: false,
34 items: vec![],
35 }
36 }
37
38 pub fn title(mut self, title: impl Into<SharedString>) -> Self {
39 self.title = Some(title.into());
40 self
41 }
42
43 pub fn extra(mut self, extra: impl IntoElement) -> Self {
44 self.extra = Some(extra.into_any_element());
45 self
46 }
47
48 pub fn column(mut self, c: u32) -> Self {
49 self.column = c.max(1);
50 self
51 }
52
53 pub fn direction(mut self, d: DescriptionsDirection) -> Self {
54 self.direction = d;
55 self
56 }
57
58 pub fn border(mut self, b: bool) -> Self {
59 self.border = b;
60 self
61 }
62
63 pub fn item(
64 mut self,
65 label: impl Into<SharedString>,
66 value: impl IntoElement,
67 span: u32,
68 ) -> Self {
69 self.items.push(DescriptionItem {
70 label: label.into(),
71 value: value.into_any_element(),
72 span: span.max(1),
73 });
74 self
75 }
76}
77
78impl RenderOnce for Descriptions {
79 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
80 let theme = cx.global::<Config>().theme.clone();
81 let column = self.column;
82 let border = self.border;
83 let direction = self.direction;
84
85 // Group items into rows
86 let mut rows: Vec<Vec<DescriptionItem>> = vec![];
87 let mut current_row: Vec<DescriptionItem> = vec![];
88 let mut current_span = 0;
89
90 for item in self.items {
91 let item_span = item.span.min(column);
92 if current_span + item_span > column {
93 if let Some(last) = current_row.last_mut() {
94 last.span += column - current_span;
95 }
96 rows.push(current_row);
97 current_row = vec![];
98 current_span = 0;
99 }
100 current_span += item_span;
101 current_row.push(item);
102 }
103 if !current_row.is_empty() {
104 if let Some(last) = current_row.last_mut() {
105 last.span += column - current_span;
106 }
107 rows.push(current_row);
108 }
109
110 div()
111 .flex()
112 .flex_col()
113 .w_full()
114 .gap_4()
115 .when(self.title.is_some() || self.extra.is_some(), |s| {
116 s.child(
117 div()
118 .flex()
119 .flex_row()
120 .items_center()
121 .justify_between()
122 .when_some(self.title, |s, t| {
123 s.child(
124 div()
125 .text_lg()
126 .font_weight(gpui::FontWeight::BOLD)
127 .text_color(theme.neutral.text_1)
128 .child(t),
129 )
130 })
131 .when_some(self.extra, |s, e| s.child(e)),
132 )
133 })
134 .child(
135 div()
136 .flex()
137 .flex_col()
138 .when(border, |s| {
139 s.border_1()
140 .border_color(theme.neutral.border)
141 .rounded(px(theme.radius.sm))
142 .overflow_hidden()
143 })
144 .children(rows.into_iter().enumerate().map(|(row_idx, row)| {
145 div()
146 .flex()
147 .flex_row()
148 .w_full()
149 .when(border && row_idx > 0, |s| {
150 s.border_t_1().border_color(theme.neutral.border)
151 })
152 .children(row.into_iter().enumerate().map(|(col_idx, item)| {
153 let width = gpui::relative(item.span as f32 / column as f32);
154 let cell = div().w(width).flex().when(border && col_idx > 0, |s| {
155 s.border_l_1().border_color(theme.neutral.border)
156 });
157
158 match direction {
159 DescriptionsDirection::Horizontal => {
160 if border {
161 cell.flex_row()
162 .items_start()
163 .child(
164 div()
165 .p_3()
166 .bg(theme.neutral.hover)
167 .border_r_1()
168 .border_color(theme.neutral.border)
169 .flex()
170 .items_center()
171 .child(
172 div()
173 .text_sm()
174 .font_weight(gpui::FontWeight::BOLD)
175 .text_color(theme.neutral.text_2)
176 .child(item.label),
177 ),
178 )
179 .child(
180 div()
181 .flex_1()
182 .p_3()
183 .flex()
184 .items_center()
185 .child(
186 div()
187 .text_sm()
188 .text_color(theme.neutral.text_1)
189 .child(item.value),
190 ),
191 )
192 } else {
193 cell.flex_row()
194 .items_center()
195 .gap_2()
196 .p_1()
197 .child(
198 div()
199 .text_sm()
200 .text_color(theme.neutral.text_3)
201 .child(format!("{}:", item.label)),
202 )
203 .child(
204 div()
205 .text_sm()
206 .text_color(theme.neutral.text_1)
207 .child(item.value),
208 )
209 }
210 }
211 DescriptionsDirection::Vertical => {
212 if border {
213 cell.flex_col()
214 .child(
215 div()
216 .p_3()
217 .bg(theme.neutral.hover)
218 .border_b_1()
219 .border_color(theme.neutral.border)
220 .child(
221 div()
222 .text_sm()
223 .font_weight(gpui::FontWeight::BOLD)
224 .text_color(theme.neutral.text_2)
225 .child(item.label),
226 ),
227 )
228 .child(
229 div().p_3().child(
230 div()
231 .text_sm()
232 .text_color(theme.neutral.text_1)
233 .child(item.value),
234 ),
235 )
236 } else {
237 cell.flex_col()
238 .gap_1()
239 .p_1()
240 .child(
241 div()
242 .text_xs()
243 .text_color(theme.neutral.text_3)
244 .child(item.label),
245 )
246 .child(
247 div()
248 .text_sm()
249 .text_color(theme.neutral.text_1)
250 .child(item.value),
251 )
252 }
253 }
254 }
255 }))
256 })),
257 )
258 }
259}
260
261impl IntoElement for Descriptions {
262 type Element = gpui::Component<Self>;
263 fn into_element(self) -> Self::Element {
264 gpui::Component::new(self)
265 }
266}