1use crate::cues::{self, Cue};
9use crate::{
10 control::{PressReleaseExt as _, composed_button},
11 handlers::SharedHandler,
12 motion::reveal_staggered,
13 theme::SemanticStyledExt as _,
14};
15use gpui::{
16 App, ClickEvent, ElementId, InteractiveElement as _, IntoElement, ParentElement as _,
17 RenderOnce, Role, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled,
18 Window, div, prelude::FluentBuilder as _,
19};
20use gpui_component::{ActiveTheme as _, StyledExt as _, h_flex};
21use std::rc::Rc;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct Suggestion {
26 id: SharedString,
27 label: SharedString,
28 description: Option<SharedString>,
29}
30
31impl Suggestion {
32 pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
34 Self {
35 id: id.into(),
36 label: label.into(),
37 description: None,
38 }
39 }
40
41 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
43 self.description = Some(description.into());
44 self
45 }
46
47 pub fn id(&self) -> &SharedString {
49 &self.id
50 }
51
52 pub fn label(&self) -> &SharedString {
54 &self.label
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum SuggestionsEvent {
61 Selected {
63 id: SharedString,
65 },
66}
67
68#[derive(IntoElement)]
82pub struct Suggestions {
83 id: ElementId,
84 style: StyleRefinement,
85 items: Vec<Suggestion>,
86 on_event: Option<SharedHandler<SuggestionsEvent>>,
87}
88
89impl Suggestions {
90 pub fn new(id: impl Into<ElementId>) -> Self {
92 Self {
93 id: id.into(),
94 style: StyleRefinement::default(),
95 items: Vec::new(),
96 on_event: None,
97 }
98 }
99
100 pub fn items(mut self, items: impl IntoIterator<Item = Suggestion>) -> Self {
102 self.items = items.into_iter().collect();
103 self
104 }
105
106 pub fn on_event(
108 mut self,
109 handler: impl Fn(&SuggestionsEvent, &mut Window, &mut App) + 'static,
110 ) -> Self {
111 self.on_event = Some(Rc::new(handler));
112 self
113 }
114}
115
116impl Styled for Suggestions {
117 fn style(&mut self) -> &mut StyleRefinement {
118 &mut self.style
119 }
120}
121
122impl RenderOnce for Suggestions {
123 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
124 let tokens = cx.theme().semantic_tokens();
125 let root_id = self.id.clone();
126 let handler = self.on_event;
127 h_flex()
128 .id(self.id)
129 .role(Role::Group)
130 .aria_label("Suggestions")
131 .flex_wrap()
132 .gap(tokens.spacing.xs)
133 .children(self.items.into_iter().enumerate().map(|(ix, item)| {
134 let chip_id = ElementId::from((root_id.clone(), format!("chip-{}", item.id)));
135 let debug_id = item.id.clone();
136 let chip = match handler.clone() {
137 Some(handler) => {
138 let event = SuggestionsEvent::Selected {
139 id: item.id.clone(),
140 };
141 composed_button(chip_id.clone(), item.label.clone())
142 .debug_selector(move || format!("suggestion-{debug_id}"))
143 .when_some(item.description.clone(), |this, description| {
144 this.aria_description(description)
145 })
146 .flex()
147 .items_center()
148 .h(crate::sizing::control_lg(cx))
156 .px(tokens.spacing.md)
157 .border_1()
158 .border_color(cx.theme().border)
159 .rounded(tokens.radius.full)
160 .bg(tokens.colors.surface)
161 .text_token(tokens.typography.sm)
162 .text_color(cx.theme().foreground)
163 .hover(|style| style.bg(cx.theme().accent.opacity(0.6)))
166 .active(|style| style.bg(cx.theme().accent))
167 .focus_visible(|style| style.border_color(cx.theme().ring))
168 .press_release(
169 ElementId::from((chip_id, "press")),
170 tokens.radius.full,
171 window,
172 cx,
173 )
174 .child({
175 let label_debug = item.id.clone();
176 div()
177 .debug_selector(move || {
178 format!("suggestion-label-{label_debug}")
179 })
180 .child(item.label)
181 })
182 .on_click(move |_: &ClickEvent, window, cx| {
183 cues::emit(cx, Cue::SuggestionSelected);
184 handler(&event, window, cx)
185 })
186 .into_any_element()
187 }
188 None => div()
189 .id(chip_id)
190 .role(Role::ListItem)
191 .aria_label(item.label.clone())
192 .flex()
193 .items_center()
194 .h(crate::sizing::control_lg(cx))
195 .px(tokens.spacing.md)
196 .border_1()
197 .border_color(cx.theme().border)
198 .rounded(tokens.radius.full)
199 .bg(tokens.colors.surface)
200 .text_token(tokens.typography.sm)
201 .text_color(cx.theme().muted_foreground)
202 .child(item.label)
203 .into_any_element(),
204 };
205 reveal_staggered(
206 div().child(chip),
207 (root_id.clone(), format!("reveal-{}", item.id)),
208 ix,
209 window,
210 cx,
211 )
212 }))
213 .refine_style(&self.style)
214 }
215}