1use std::rc::Rc;
4
5use gpui::{
6 App, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window,
7 div, prelude::FluentBuilder, px,
8};
9use gpui_kit_assets::{Icon, icon};
10use gpui_kit_semantics::{NodeSpec, Role, Semantic};
11use gpui_kit_theme::{ActiveTheme, Radius, Space, TypeScale};
12
13use crate::display::badge::Tone;
14use crate::foundation::{Disableable, FocusRing, Ident, Pressable, Selectable, StyledExt, text};
15use crate::strings::{ActiveStrings, StringKey};
16
17type RemoveHandler = Rc<dyn Fn(&mut Window, &mut App)>;
18
19#[derive(IntoElement)]
24pub struct Tag {
25 ident: Ident,
26 label: SharedString,
27 tone: Tone,
28 disabled: bool,
29 selected: bool,
30 on_remove: Option<RemoveHandler>,
31}
32
33impl std::fmt::Debug for Tag {
34 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 formatter
36 .debug_struct("Tag")
37 .field("ident", &self.ident)
38 .field("label", &self.label)
39 .field("tone", &self.tone)
40 .field("removable", &self.on_remove.is_some())
41 .finish()
42 }
43}
44
45impl Tag {
46 pub fn new(ident: impl Into<Ident>, label: impl Into<SharedString>) -> Self {
47 Self {
48 ident: ident.into(),
49 label: label.into(),
50 tone: Tone::Neutral,
51 disabled: false,
52 selected: false,
53 on_remove: None,
54 }
55 }
56
57 pub fn tone(mut self, tone: Tone) -> Self {
58 self.tone = tone;
59 self
60 }
61
62 pub fn on_remove(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
63 self.on_remove = Some(Rc::new(handler));
64 self
65 }
66}
67
68impl Disableable for Tag {
69 fn disabled(mut self, disabled: bool) -> Self {
70 self.disabled = disabled;
71 self
72 }
73}
74
75impl Selectable for Tag {
78 fn selected(mut self, selected: bool) -> Self {
79 self.selected = selected;
80 self
81 }
82}
83
84impl RenderOnce for Tag {
85 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
86 let theme = cx.theme().clone();
87 let color = self.tone.color(&theme);
88 let removable = !self.disabled && self.on_remove.is_some();
89 let remove_ident = self.ident.child("remove");
90
91 let remove = removable.then(|| {
92 let handler = self.on_remove.clone().expect("checked above");
93 let mut button = div()
94 .id(remove_ident.element_id())
95 .flex()
96 .items_center()
97 .justify_center()
98 .cursor_pointer()
99 .tab_index(0)
100 .focus_ring(&theme)
101 .pressable(cx)
102 .child(icon(Icon::Close).size(px(10.0)).text_color(color))
103 .semantic_in(
104 cx,
105 NodeSpec::new(remove_ident.semantic_id(), Role::Button)
106 .parent(self.ident.semantic_id())
107 .text(cx.strings().format(StringKey::TagRemove, &[&self.label])),
108 );
109 let click = Rc::clone(&handler);
110 button
111 .interactivity()
112 .on_click(move |_, window, cx| click(window, cx));
113 button
114 .interactivity()
115 .on_key_down(move |event, window, cx| {
116 if matches!(event.keystroke.key.as_str(), "enter" | "space") {
117 handler(window, cx);
118 cx.stop_propagation();
119 }
120 });
121 button
122 });
123
124 div()
125 .flex()
126 .flex_row()
127 .items_center()
128 .flex_none()
129 .gap(px(theme.space(Space::Xs)))
130 .px(px(theme.space(Space::Sm)))
131 .py(px(2.0))
132 .radius(&theme, Radius::Pill)
133 .bg(color.opacity(if self.selected { 0.34 } else { 0.14 }))
137 .when(self.selected, |element| {
138 element.shadow(theme.selected_ring())
139 })
140 .when(self.disabled, |element| {
141 element.opacity(theme.opacity.disabled)
142 })
143 .child(text(&theme, TypeScale::Label, self.label.clone()).text_color(color))
144 .children(remove)
145 .semantic_in(
146 cx,
147 NodeSpec::new(self.ident.semantic_id(), Role::Text)
148 .disabled(self.disabled)
149 .selected(self.selected)
150 .text(self.label.clone()),
151 )
152 }
153}