1use std::rc::Rc;
9
10use gpui::{
11 App, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString,
12 StatefulInteractiveElement, Styled, Window, div, prelude::FluentBuilder, px, relative,
13};
14use gpui_kit_assets::{Icon, icon};
15use gpui_kit_semantics::{NodeSpec, Role, Semantic};
16use gpui_kit_theme::{ActiveTheme, Radius, Space, TypeScale};
17use unicode_segmentation::UnicodeSegmentation;
18
19use crate::foundation::{FocusRing, Ident, Pressable, StyledExt};
20use crate::strings::{ActiveStrings, StringKey};
21
22type CopyHandler = Rc<dyn Fn(SharedString, &mut Window, &mut App)>;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum DescriptionValue {
27 Text(SharedString),
28 Unknown,
30 NotApplicable,
32 Redacted(SharedString),
35}
36
37impl DescriptionValue {
38 pub fn text(value: impl Into<SharedString>) -> Self {
39 Self::Text(value.into())
40 }
41
42 pub fn redacted(shape: impl Into<SharedString>) -> Self {
48 Self::Redacted(shape.into())
49 }
50
51 pub fn redacted_from(secret: &str, cx: &App) -> Self {
56 Self::Redacted(cx.strings().format(
57 StringKey::DescriptionCharacters,
58 &[&secret.graphemes(true).count().to_string()],
59 ))
60 }
61
62 pub fn as_str(&self) -> &'static str {
64 match self {
65 Self::Text(_) => "text",
66 Self::Unknown => "unknown",
67 Self::NotApplicable => "not-applicable",
68 Self::Redacted(_) => "redacted",
69 }
70 }
71
72 fn published(&self) -> SharedString {
75 match self {
76 Self::Text(value) => value.clone(),
77 Self::Unknown => SharedString::new_static("unknown"),
78 Self::NotApplicable => SharedString::new_static("not applicable"),
79 Self::Redacted(shape) => SharedString::from(format!("redacted, {shape}")),
80 }
81 }
82
83 fn is_copyable(&self) -> bool {
85 matches!(self, Self::Text(_) | Self::Redacted(_))
86 }
87}
88
89impl From<SharedString> for DescriptionValue {
90 fn from(value: SharedString) -> Self {
91 Self::Text(value)
92 }
93}
94
95impl From<&'static str> for DescriptionValue {
96 fn from(value: &'static str) -> Self {
97 Self::Text(SharedString::new_static(value))
98 }
99}
100
101impl From<String> for DescriptionValue {
102 fn from(value: String) -> Self {
103 Self::Text(SharedString::from(value))
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct DescriptionItem {
110 id: SharedString,
111 term: SharedString,
112 value: DescriptionValue,
113 copyable: bool,
114}
115
116impl DescriptionItem {
117 pub fn new(
118 id: impl Into<SharedString>,
119 term: impl Into<SharedString>,
120 value: impl Into<DescriptionValue>,
121 ) -> Self {
122 Self {
123 id: id.into(),
124 term: term.into(),
125 value: value.into(),
126 copyable: false,
127 }
128 }
129
130 pub fn copyable(mut self, copyable: bool) -> Self {
132 self.copyable = copyable;
133 self
134 }
135
136 pub fn id(&self) -> &SharedString {
137 &self.id
138 }
139
140 pub fn value(&self) -> &DescriptionValue {
141 &self.value
142 }
143}
144
145#[derive(IntoElement)]
147pub struct DescriptionList {
148 ident: Ident,
149 items: Vec<DescriptionItem>,
150 columns: usize,
151 on_copy: Option<CopyHandler>,
152}
153
154impl std::fmt::Debug for DescriptionList {
155 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 formatter
157 .debug_struct("DescriptionList")
158 .field("ident", &self.ident)
159 .field("items", &self.items.len())
160 .field("columns", &self.columns)
161 .finish()
162 }
163}
164
165impl DescriptionList {
166 pub fn new(ident: impl Into<Ident>) -> Self {
167 Self {
168 ident: ident.into(),
169 items: Vec::new(),
170 columns: 1,
171 on_copy: None,
172 }
173 }
174
175 pub fn item(mut self, item: DescriptionItem) -> Self {
176 self.items.push(item);
177 self
178 }
179
180 pub fn items(mut self, items: impl IntoIterator<Item = DescriptionItem>) -> Self {
181 self.items.extend(items);
182 self
183 }
184
185 pub fn columns(mut self, columns: usize) -> Self {
188 self.columns = if columns >= 2 { 2 } else { 1 };
189 self
190 }
191
192 pub fn on_copy(
195 mut self,
196 handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
197 ) -> Self {
198 self.on_copy = Some(Rc::new(handler));
199 self
200 }
201}
202
203impl RenderOnce for DescriptionList {
204 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
205 let theme = cx.theme().clone();
206 let columns = self.columns;
207 let count = self.items.len();
208
209 let rows = self.items.into_iter().map(|item| {
210 let ident = self.ident.child(item.id.as_ref());
211 let copyable = item.copyable && item.value.is_copyable();
212 let value = match &item.value {
213 DescriptionValue::Text(text) => div()
214 .type_scale(&theme, TypeScale::Label)
215 .text_color(theme.colors.text)
216 .child(text.clone()),
217 DescriptionValue::Unknown => div()
218 .type_scale(&theme, TypeScale::Label)
219 .text_color(theme.colors.text_faint)
220 .child(cx.strings().text(StringKey::DescriptionUnknown)),
221 DescriptionValue::NotApplicable => div()
222 .type_scale(&theme, TypeScale::Label)
223 .text_color(theme.colors.text_faint)
224 .child(cx.strings().text(StringKey::DescriptionNotApplicable)),
225 DescriptionValue::Redacted(shape) => div()
229 .row()
230 .gap_token(&theme, Space::Sm)
231 .type_scale(&theme, TypeScale::Label)
232 .text_color(theme.colors.text_muted)
233 .child(SharedString::new_static("••••••••"))
234 .child(
235 div()
236 .type_scale(&theme, TypeScale::Caption)
237 .text_color(theme.colors.text_faint)
238 .child(shape.clone()),
239 ),
240 };
241
242 let copy = self.on_copy.clone().filter(|_| copyable).map(|handler| {
243 let copy_ident = ident.child("copy");
244 let id = item.id.clone();
245 let name = cx
246 .strings()
247 .format(StringKey::DescriptionCopy, &[&item.term]);
248 div()
249 .id(copy_ident.element_id())
250 .flex_none()
251 .flex()
252 .items_center()
253 .justify_center()
254 .size(px(theme.control.xs.height))
255 .radius(&theme, Radius::Small)
256 .cursor_pointer()
257 .tab_index(0)
258 .text_color(theme.colors.text_faint)
259 .hover(|style| style.bg(theme.colors.hover))
260 .pressable(cx)
261 .focus_ring(&theme)
262 .child(icon(Icon::Copy).size(px(theme.control.xs.icon_size)))
263 .on_click(move |_, window, cx| handler(id.clone(), window, cx))
264 .semantic_in(
265 cx,
266 NodeSpec::new(copy_ident.semantic_id(), Role::Button)
267 .parent(ident.semantic_id())
268 .text(name),
269 )
270 });
271
272 div()
273 .row()
274 .items_start()
275 .gap_token(&theme, Space::Md)
276 .py_token(&theme, Space::Xs)
277 .when(columns == 2, |element| element.w(relative(0.5)).flex_none())
278 .when(columns == 1, |element| element.w_full())
279 .child(
280 div()
281 .w(px(140.0))
282 .flex_none()
283 .type_scale(&theme, TypeScale::Caption)
284 .text_color(theme.colors.text_muted)
285 .child(item.term.clone()),
286 )
287 .child(div().flex_1().min_w_0().child(value))
288 .children(copy)
289 .semantic_in(
290 cx,
291 NodeSpec::new(ident.semantic_id(), Role::Row)
292 .parent(self.ident.semantic_id())
293 .text(item.term.clone())
294 .value(item.value.published()),
295 )
296 });
297
298 div()
299 .w_full()
300 .flex()
301 .flex_row()
302 .flex_wrap()
303 .children(rows)
304 .semantic_in(
305 cx,
306 NodeSpec::new(self.ident.semantic_id(), Role::List).value(count.to_string()),
307 )
308 }
309}