1use crate::ButtonLabelExt as _;
15use crate::control::ControlMetricsExt as _;
16use crate::decoration::{DecoratedExt as _, Decoration};
17use crate::form::{ChoiceEvent, ChoiceGroup, ChoiceOption};
18use crate::handlers::SharedHandler;
19use crate::surface::{CardFrameExt as _, description, meta, title};
20use gpui::{
21 App, ClickEvent, ElementId, InteractiveElement as _, IntoElement, ParentElement as _,
22 RenderOnce, Role, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled,
23 Window, div, prelude::FluentBuilder as _,
24};
25use gpui_component::{
26 ActiveTheme as _, Disableable as _, Sizable as _, StyledExt as _,
27 button::{Button, ButtonVariants as _},
28 h_flex, v_flex,
29};
30use std::rc::Rc;
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct Question {
35 id: SharedString,
36 prompt: SharedString,
37 note: Option<SharedString>,
38 options: Vec<ChoiceOption>,
39 answer: Option<SharedString>,
40 optional: bool,
41}
42
43impl Question {
44 pub fn new(id: impl Into<SharedString>, prompt: impl Into<SharedString>) -> Self {
46 Self {
47 id: id.into(),
48 prompt: prompt.into(),
49 note: None,
50 options: Vec::new(),
51 answer: None,
52 optional: false,
53 }
54 }
55
56 pub fn note(mut self, note: impl Into<SharedString>) -> Self {
58 self.note = Some(note.into());
59 self
60 }
61
62 pub fn options(mut self, options: impl IntoIterator<Item = ChoiceOption>) -> Self {
64 self.options = options.into_iter().collect();
65 self
66 }
67
68 pub fn answer(mut self, option: impl Into<SharedString>) -> Self {
70 self.answer = Some(option.into());
71 self
72 }
73
74 pub fn answered(mut self, option: Option<SharedString>) -> Self {
76 self.answer = option;
77 self
78 }
79
80 pub fn optional(mut self, optional: bool) -> Self {
82 self.optional = optional;
83 self
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum QuestionFlowEvent {
90 Answered {
92 flow: SharedString,
94 question: SharedString,
96 option: SharedString,
98 },
99 Skipped {
101 flow: SharedString,
103 question: SharedString,
105 },
106 Advanced {
108 flow: SharedString,
110 step: usize,
112 },
113 Completed {
115 flow: SharedString,
117 },
118}
119
120#[derive(IntoElement)]
139pub struct QuestionFlow {
140 id: SharedString,
141 title: Option<SharedString>,
142 questions: Vec<Question>,
143 step: usize,
144 skip_label: SharedString,
145 continue_label: SharedString,
146 finish_label: SharedString,
147 on_event: Option<SharedHandler<QuestionFlowEvent>>,
148 style: StyleRefinement,
149 decoration: Decoration,
150}
151
152impl QuestionFlow {
153 pub fn decoration(mut self, decoration: Decoration) -> Self {
159 self.decoration = decoration;
160 self
161 }
162
163 pub fn new(id: impl Into<SharedString>, title: impl Into<SharedString>) -> Self {
165 Self {
166 id: id.into(),
167 title: Some(title.into()),
168 questions: Vec::new(),
169 step: 0,
170 skip_label: "Skip".into(),
171 continue_label: "Continue".into(),
172 finish_label: "Done".into(),
173 on_event: None,
174 style: StyleRefinement::default(),
175 decoration: Decoration::default(),
176 }
177 }
178
179 pub fn questions(mut self, questions: impl IntoIterator<Item = Question>) -> Self {
181 self.questions = questions.into_iter().collect();
182 self
183 }
184
185 pub fn step(mut self, step: usize) -> Self {
187 self.step = step;
188 self
189 }
190
191 pub fn skip_label(mut self, label: impl Into<SharedString>) -> Self {
193 self.skip_label = label.into();
194 self
195 }
196
197 pub fn continue_label(mut self, label: impl Into<SharedString>) -> Self {
199 self.continue_label = label.into();
200 self
201 }
202
203 pub fn finish_label(mut self, label: impl Into<SharedString>) -> Self {
205 self.finish_label = label.into();
206 self
207 }
208
209 pub fn on_event(
211 mut self,
212 handler: impl Fn(&QuestionFlowEvent, &mut Window, &mut App) + 'static,
213 ) -> Self {
214 self.on_event = Some(Rc::new(handler));
215 self
216 }
217
218 fn shown(&self) -> usize {
221 self.step.min(self.questions.len().saturating_sub(1))
222 }
223}
224
225impl Styled for QuestionFlow {
226 fn style(&mut self) -> &mut StyleRefinement {
227 &mut self.style
228 }
229}
230
231impl RenderOnce for QuestionFlow {
232 fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement {
233 let tokens = cx.theme().semantic_tokens();
234 let mut decoration = std::mem::take(&mut self.decoration);
237 let total = self.questions.len();
238 let shown = self.shown();
239 let root_id = ElementId::from(self.id.clone());
240 let flow = self.id.clone();
241 let handler = self.on_event;
242 let card_title = self.title.clone();
243 let debug_id = self.id.clone();
244
245 let Some(question) = self.questions.into_iter().nth(shown) else {
246 return div().into_any_element();
249 };
250
251 let answered = question.answer.is_some();
252 let last = shown + 1 == total;
253 let question_id = question.id.clone();
254 let group = ChoiceGroup::unlabelled(format!("{flow}-{question_id}"))
255 .options(question.options.clone())
256 .selection(question.answer.clone())
257 .on_event({
258 let handler = handler.clone();
259 let flow = flow.clone();
260 let question_id = question_id.clone();
261 move |event, window, cx| {
262 let ChoiceEvent::Chosen { option, .. } = event;
263 if let Some(handler) = handler.as_ref() {
264 handler(
265 &QuestionFlowEvent::Answered {
266 flow: flow.clone(),
267 question: question_id.clone(),
268 option: option.clone(),
269 },
270 window,
271 cx,
272 );
273 }
274 }
275 });
276
277 let skip_event = QuestionFlowEvent::Skipped {
278 flow: flow.clone(),
279 question: question_id.clone(),
280 };
281 let advance_event = if last {
282 QuestionFlowEvent::Completed { flow: flow.clone() }
283 } else {
284 QuestionFlowEvent::Advanced {
285 flow: flow.clone(),
286 step: shown + 1,
287 }
288 };
289 let advance_label = if last {
290 self.finish_label.clone()
291 } else {
292 self.continue_label.clone()
293 };
294 let counter = format!("{} of {total}", shown + 1);
295 let counter_debug = debug_id.clone();
296 let skip_handler = handler.clone();
297 let advance_handler = handler;
298
299 v_flex()
300 .id(root_id.clone())
301 .debug_selector(move || format!("question-flow-{debug_id}"))
302 .card_frame(cx)
303 .decoration_under(&mut decoration)
304 .p(tokens.spacing.lg)
305 .gap(tokens.spacing.md)
306 .role(Role::Group)
307 .aria_label(
308 card_title
309 .clone()
310 .unwrap_or_else(|| question.prompt.clone()),
311 )
312 .decoration_over(&mut decoration)
313 .refine_style(&self.style)
314 .when_some(card_title, |card, text| card.child(title(text, cx)))
315 .child(
316 v_flex()
317 .gap(tokens.spacing.xs)
318 .child(description(question.prompt.clone(), cx))
319 .when_some(question.note.clone(), |body, note| {
320 body.child(crate::surface::hint(note, cx))
321 }),
322 )
323 .child(group)
324 .child(
325 h_flex()
326 .w_full()
327 .items_center()
328 .justify_between()
329 .gap(tokens.spacing.sm)
330 .child(
331 div()
335 .id(ElementId::from((root_id.clone(), "counter")))
336 .debug_selector(move || {
337 format!("question-flow-counter-{counter_debug}")
338 })
339 .role(Role::Status)
340 .aria_label(format!("Question {counter}"))
341 .child(meta(counter.clone(), cx)),
342 )
343 .child(
344 h_flex()
345 .gap(tokens.spacing.sm)
346 .when(question.optional || !answered, |row| {
347 row.child(
348 Button::new(ElementId::from((root_id.clone(), "skip")))
349 .ghost()
350 .small()
351 .control_metrics(cx)
352 .accessibility_id(format!("{flow}-skip"))
353 .debug_selector({
354 let flow = flow.clone();
355 move || format!("question-flow-skip-{flow}")
356 })
357 .text_label(self.skip_label.clone())
358 .when_some(skip_handler, |button, handler| {
359 button.on_click(move |_: &ClickEvent, window, cx| {
360 handler(&skip_event, window, cx)
361 })
362 }),
363 )
364 })
365 .child(
366 Button::new(ElementId::from((root_id, "advance")))
367 .primary()
368 .small()
369 .control_metrics(cx)
370 .accessibility_id(format!("{flow}-advance"))
371 .debug_selector({
372 let flow = flow.clone();
373 move || format!("question-flow-advance-{flow}")
374 })
375 .text_label(advance_label)
376 .disabled(!answered)
377 .when_some(
378 advance_handler.filter(|_| answered),
379 |button, handler| {
380 button.on_click(move |_: &ClickEvent, window, cx| {
381 handler(&advance_event, window, cx)
382 })
383 },
384 ),
385 ),
386 ),
387 )
388 .into_any_element()
389 }
390}