Skip to main content

gpui_base/questionnaire/
types.rs

1use std::{error::Error, fmt, rc::Rc};
2
3use gpui::{Entity, SharedString};
4
5use crate::input::InputState;
6
7/// Validates one questionnaire item against the current questionnaire answers.
8pub type QuestionnaireValidator =
9    Rc<dyn Fn(&QuestionnaireValidationContext) -> Result<(), SharedString> + 'static>;
10
11/// Describes one selectable answer.
12#[derive(Clone, Debug)]
13pub struct QuestionnaireChoiceDefinition {
14    value: SharedString,
15    accessibility_label: SharedString,
16    description: Option<SharedString>,
17    disabled: bool,
18    default_selected: bool,
19}
20
21impl QuestionnaireChoiceDefinition {
22    pub fn new(
23        value: impl Into<SharedString>,
24        accessibility_label: impl Into<SharedString>,
25    ) -> Self {
26        Self {
27            value: value.into(),
28            accessibility_label: accessibility_label.into(),
29            description: None,
30            disabled: false,
31            default_selected: false,
32        }
33    }
34
35    pub fn with_description(mut self, description: impl Into<SharedString>) -> Self {
36        self.description = Some(description.into());
37        self
38    }
39
40    pub fn with_disabled(mut self, disabled: bool) -> Self {
41        self.disabled = disabled;
42        self
43    }
44
45    pub fn with_default_selected(mut self, selected: bool) -> Self {
46        self.default_selected = selected;
47        self
48    }
49
50    pub fn value(&self) -> &SharedString {
51        &self.value
52    }
53
54    pub fn accessibility_label(&self) -> &SharedString {
55        &self.accessibility_label
56    }
57
58    pub fn description(&self) -> Option<&SharedString> {
59        self.description.as_ref()
60    }
61
62    pub fn is_disabled(&self) -> bool {
63        self.disabled
64    }
65
66    pub fn is_default_selected(&self) -> bool {
67        self.default_selected
68    }
69}
70
71/// Describes the optional freeform answer owned by an item.
72#[derive(Clone, Debug)]
73pub struct QuestionnaireInputDefinition {
74    state: Entity<InputState>,
75    accessibility_label: SharedString,
76    disabled: bool,
77}
78
79impl QuestionnaireInputDefinition {
80    pub fn new(state: Entity<InputState>, accessibility_label: impl Into<SharedString>) -> Self {
81        Self {
82            state,
83            accessibility_label: accessibility_label.into(),
84            disabled: false,
85        }
86    }
87
88    pub fn with_disabled(mut self, disabled: bool) -> Self {
89        self.disabled = disabled;
90        self
91    }
92
93    pub fn state(&self) -> &Entity<InputState> {
94        &self.state
95    }
96
97    pub fn accessibility_label(&self) -> &SharedString {
98        &self.accessibility_label
99    }
100
101    pub fn is_disabled(&self) -> bool {
102        self.disabled
103    }
104}
105
106/// Describes one ordered questionnaire item.
107#[derive(Clone)]
108pub struct QuestionnaireItemDefinition {
109    name: SharedString,
110    accessibility_label: SharedString,
111    description: Option<SharedString>,
112    required: bool,
113    multiple: bool,
114    disabled: bool,
115    choices: Vec<QuestionnaireChoiceDefinition>,
116    input: Option<QuestionnaireInputDefinition>,
117    validator: Option<QuestionnaireValidator>,
118}
119
120impl fmt::Debug for QuestionnaireItemDefinition {
121    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
122        formatter
123            .debug_struct("QuestionnaireItemDefinition")
124            .field("name", &self.name)
125            .field("accessibility_label", &self.accessibility_label)
126            .field("description", &self.description)
127            .field("required", &self.required)
128            .field("multiple", &self.multiple)
129            .field("disabled", &self.disabled)
130            .field("choices", &self.choices)
131            .field("input", &self.input)
132            .field("validator", &self.validator.as_ref().map(|_| "<validator>"))
133            .finish()
134    }
135}
136
137impl QuestionnaireItemDefinition {
138    pub fn new(
139        name: impl Into<SharedString>,
140        accessibility_label: impl Into<SharedString>,
141    ) -> Self {
142        Self {
143            name: name.into(),
144            accessibility_label: accessibility_label.into(),
145            description: None,
146            required: false,
147            multiple: false,
148            disabled: false,
149            choices: Vec::new(),
150            input: None,
151            validator: None,
152        }
153    }
154
155    pub fn with_description(mut self, description: impl Into<SharedString>) -> Self {
156        self.description = Some(description.into());
157        self
158    }
159
160    pub fn with_required(mut self, required: bool) -> Self {
161        self.required = required;
162        self
163    }
164
165    pub fn with_multiple(mut self, multiple: bool) -> Self {
166        self.multiple = multiple;
167        self
168    }
169
170    pub fn with_disabled(mut self, disabled: bool) -> Self {
171        self.disabled = disabled;
172        self
173    }
174
175    pub fn with_choices(
176        mut self,
177        choices: impl IntoIterator<Item = QuestionnaireChoiceDefinition>,
178    ) -> Self {
179        self.choices = choices.into_iter().collect();
180        self
181    }
182
183    pub fn with_choice(mut self, choice: QuestionnaireChoiceDefinition) -> Self {
184        self.choices.push(choice);
185        self
186    }
187
188    pub fn with_input(mut self, input: QuestionnaireInputDefinition) -> Self {
189        self.input = Some(input);
190        self
191    }
192
193    pub fn with_validator(
194        mut self,
195        validator: impl Fn(&QuestionnaireValidationContext) -> Result<(), SharedString> + 'static,
196    ) -> Self {
197        self.validator = Some(Rc::new(validator));
198        self
199    }
200
201    pub fn name(&self) -> &SharedString {
202        &self.name
203    }
204
205    pub fn accessibility_label(&self) -> &SharedString {
206        &self.accessibility_label
207    }
208
209    pub fn description(&self) -> Option<&SharedString> {
210        self.description.as_ref()
211    }
212
213    pub fn is_required(&self) -> bool {
214        self.required
215    }
216
217    pub fn is_multiple(&self) -> bool {
218        self.multiple
219    }
220
221    pub fn is_disabled(&self) -> bool {
222        self.disabled
223    }
224
225    pub fn choices(&self) -> &[QuestionnaireChoiceDefinition] {
226        &self.choices
227    }
228
229    pub fn input(&self) -> Option<&QuestionnaireInputDefinition> {
230        self.input.as_ref()
231    }
232
233    pub fn validator(&self) -> Option<&QuestionnaireValidator> {
234        self.validator.as_ref()
235    }
236}
237
238#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
239pub enum QuestionnaireItemStatus {
240    #[default]
241    Unanswered,
242    Answered,
243    Skipped,
244}
245
246#[derive(Clone, Copy, Debug, PartialEq, Eq)]
247pub enum QuestionnaireShortcutMode {
248    Letters,
249    Numbers,
250}
251
252/// A serializable-in-spirit answer snapshot. Input drafts are deliberately excluded.
253#[derive(Clone, Debug, Default, PartialEq, Eq)]
254pub struct QuestionnaireAnswer {
255    pub(crate) choices: Vec<SharedString>,
256    pub(crate) freeform: Option<SharedString>,
257}
258
259impl QuestionnaireAnswer {
260    pub fn new() -> Self {
261        Self::default()
262    }
263
264    pub fn with_choices(
265        mut self,
266        choices: impl IntoIterator<Item = impl Into<SharedString>>,
267    ) -> Self {
268        self.choices.clear();
269        for choice in choices.into_iter().map(Into::into) {
270            if !self.choices.contains(&choice) {
271                self.choices.push(choice);
272            }
273        }
274        self
275    }
276
277    pub fn with_freeform(mut self, value: impl Into<SharedString>) -> Self {
278        let value = value.into();
279        self.freeform = (!value.trim().is_empty()).then_some(value);
280        self
281    }
282
283    pub fn choices(&self) -> &[SharedString] {
284        &self.choices
285    }
286
287    pub fn freeform(&self) -> Option<&SharedString> {
288        self.freeform.as_ref()
289    }
290
291    pub fn is_empty(&self) -> bool {
292        self.choices.is_empty() && self.freeform.is_none()
293    }
294}
295
296#[derive(Clone, Debug, Default, PartialEq, Eq)]
297pub struct QuestionnaireAnswers {
298    entries: Vec<(SharedString, QuestionnaireAnswer)>,
299}
300
301impl QuestionnaireAnswers {
302    pub(crate) fn from_entries(entries: Vec<(SharedString, QuestionnaireAnswer)>) -> Self {
303        Self { entries }
304    }
305
306    pub fn get(&self, name: &str) -> Option<&QuestionnaireAnswer> {
307        self.entries
308            .iter()
309            .find_map(|(item, answer)| (item.as_ref() == name).then_some(answer))
310    }
311
312    pub fn iter(&self) -> impl Iterator<Item = (&SharedString, &QuestionnaireAnswer)> {
313        self.entries.iter().map(|(name, answer)| (name, answer))
314    }
315
316    pub fn len(&self) -> usize {
317        self.entries.len()
318    }
319
320    pub fn is_empty(&self) -> bool {
321        self.entries.is_empty()
322    }
323}
324
325#[derive(Clone, Debug, PartialEq, Eq)]
326pub struct QuestionnaireProgressState {
327    current: usize,
328    total: usize,
329}
330
331impl QuestionnaireProgressState {
332    pub(crate) fn new(current: usize, total: usize) -> Self {
333        Self { current, total }
334    }
335
336    pub fn current(&self) -> usize {
337        self.current
338    }
339
340    pub fn total(&self) -> usize {
341        self.total
342    }
343}
344
345#[derive(Clone, Debug, PartialEq, Eq)]
346pub struct QuestionnaireItemState {
347    name: SharedString,
348    status: QuestionnaireItemStatus,
349    required: bool,
350    multiple: bool,
351    disabled: bool,
352    invalid: bool,
353    has_input: bool,
354}
355
356impl QuestionnaireItemState {
357    pub(crate) fn new(
358        name: SharedString,
359        status: QuestionnaireItemStatus,
360        required: bool,
361        multiple: bool,
362        disabled: bool,
363        invalid: bool,
364        has_input: bool,
365    ) -> Self {
366        Self {
367            name,
368            status,
369            required,
370            multiple,
371            disabled,
372            invalid,
373            has_input,
374        }
375    }
376
377    pub fn name(&self) -> &SharedString {
378        &self.name
379    }
380
381    pub fn status(&self) -> QuestionnaireItemStatus {
382        self.status
383    }
384
385    pub fn is_required(&self) -> bool {
386        self.required
387    }
388
389    pub fn is_multiple(&self) -> bool {
390        self.multiple
391    }
392
393    pub fn is_disabled(&self) -> bool {
394        self.disabled
395    }
396
397    pub fn is_invalid(&self) -> bool {
398        self.invalid
399    }
400
401    pub fn has_input(&self) -> bool {
402        self.has_input
403    }
404}
405
406#[derive(Clone, Debug, PartialEq, Eq)]
407pub struct QuestionnaireChoiceState {
408    value: SharedString,
409    selected: bool,
410    disabled: bool,
411    invalid: bool,
412    shortcut: Option<SharedString>,
413}
414
415impl QuestionnaireChoiceState {
416    pub(crate) fn new(
417        value: SharedString,
418        selected: bool,
419        disabled: bool,
420        invalid: bool,
421        shortcut: Option<SharedString>,
422    ) -> Self {
423        Self {
424            value,
425            selected,
426            disabled,
427            invalid,
428            shortcut,
429        }
430    }
431
432    pub fn value(&self) -> &SharedString {
433        &self.value
434    }
435
436    pub fn is_selected(&self) -> bool {
437        self.selected
438    }
439
440    pub fn is_disabled(&self) -> bool {
441        self.disabled
442    }
443
444    pub fn is_invalid(&self) -> bool {
445        self.invalid
446    }
447
448    pub fn shortcut(&self) -> Option<&SharedString> {
449        self.shortcut.as_ref()
450    }
451}
452
453#[derive(Clone, Debug, Default, PartialEq, Eq)]
454pub struct QuestionnaireNavigationState {
455    previous_visible: bool,
456    next_visible: bool,
457    skip_visible: bool,
458    submit_visible: bool,
459    confirmable: bool,
460}
461
462impl QuestionnaireNavigationState {
463    pub(crate) fn new(
464        previous_visible: bool,
465        next_visible: bool,
466        skip_visible: bool,
467        submit_visible: bool,
468        confirmable: bool,
469    ) -> Self {
470        Self {
471            previous_visible,
472            next_visible,
473            skip_visible,
474            submit_visible,
475            confirmable,
476        }
477    }
478
479    pub fn is_previous_visible(&self) -> bool {
480        self.previous_visible
481    }
482
483    pub fn is_next_visible(&self) -> bool {
484        self.next_visible
485    }
486
487    pub fn is_skip_visible(&self) -> bool {
488        self.skip_visible
489    }
490
491    pub fn is_submit_visible(&self) -> bool {
492        self.submit_visible
493    }
494
495    pub fn is_confirmable(&self) -> bool {
496        self.confirmable
497    }
498}
499
500/// Immutable validation input. Validators cannot mutate the questionnaire.
501#[derive(Clone, Debug)]
502pub struct QuestionnaireValidationContext {
503    item: SharedString,
504    answer: QuestionnaireAnswer,
505    answers: QuestionnaireAnswers,
506}
507
508impl QuestionnaireValidationContext {
509    pub(crate) fn new(
510        item: SharedString,
511        answer: QuestionnaireAnswer,
512        answers: QuestionnaireAnswers,
513    ) -> Self {
514        Self {
515            item,
516            answer,
517            answers,
518        }
519    }
520
521    pub fn item(&self) -> &SharedString {
522        &self.item
523    }
524
525    pub fn answer(&self) -> &QuestionnaireAnswer {
526        &self.answer
527    }
528
529    pub fn answers(&self) -> &QuestionnaireAnswers {
530        &self.answers
531    }
532}
533
534#[derive(Clone, Debug, PartialEq, Eq)]
535pub struct QuestionnaireAnswerChange {
536    item: SharedString,
537    answer: QuestionnaireAnswer,
538    status: QuestionnaireItemStatus,
539}
540
541impl QuestionnaireAnswerChange {
542    pub(crate) fn new(
543        item: SharedString,
544        answer: QuestionnaireAnswer,
545        status: QuestionnaireItemStatus,
546    ) -> Self {
547        Self {
548            item,
549            answer,
550            status,
551        }
552    }
553
554    pub fn item(&self) -> &SharedString {
555        &self.item
556    }
557
558    pub fn answer(&self) -> &QuestionnaireAnswer {
559        &self.answer
560    }
561
562    pub fn status(&self) -> QuestionnaireItemStatus {
563        self.status
564    }
565}
566
567#[derive(Clone, Debug, PartialEq, Eq)]
568pub struct QuestionnaireSubmissionItem {
569    name: SharedString,
570    status: QuestionnaireItemStatus,
571    answer: QuestionnaireAnswer,
572}
573
574impl QuestionnaireSubmissionItem {
575    pub(crate) fn new(
576        name: SharedString,
577        status: QuestionnaireItemStatus,
578        answer: QuestionnaireAnswer,
579    ) -> Self {
580        Self {
581            name,
582            status,
583            answer,
584        }
585    }
586
587    pub fn name(&self) -> &SharedString {
588        &self.name
589    }
590
591    pub fn status(&self) -> QuestionnaireItemStatus {
592        self.status
593    }
594
595    pub fn answer(&self) -> &QuestionnaireAnswer {
596        &self.answer
597    }
598}
599
600#[derive(Clone, Debug, Default, PartialEq, Eq)]
601pub struct QuestionnaireSubmission {
602    items: Vec<QuestionnaireSubmissionItem>,
603}
604
605impl QuestionnaireSubmission {
606    pub(crate) fn new(items: Vec<QuestionnaireSubmissionItem>) -> Self {
607        Self { items }
608    }
609
610    pub fn items(&self) -> &[QuestionnaireSubmissionItem] {
611        &self.items
612    }
613
614    pub fn answer(&self, name: &str) -> Option<&QuestionnaireAnswer> {
615        self.items
616            .iter()
617            .find_map(|item| (item.name.as_ref() == name).then_some(&item.answer))
618    }
619}
620
621#[derive(Clone, Debug, PartialEq, Eq)]
622#[non_exhaustive]
623pub enum QuestionnaireEvent {
624    CurrentItemChanged {
625        previous: Option<SharedString>,
626        current: Option<SharedString>,
627    },
628    AnswerChanged(QuestionnaireAnswerChange),
629    Completed(QuestionnaireSubmission),
630    Submit(QuestionnaireSubmission),
631}
632
633/// Why an item currently fails validation.
634///
635/// `Required` and `Unanswered` carry no text: base does not own product copy,
636/// so the presentation layer supplies the localized sentence. `Message` is the
637/// text a validator or the host already wrote.
638#[derive(Clone, Debug, PartialEq, Eq)]
639#[non_exhaustive]
640pub enum QuestionnaireValidationError {
641    /// A required item has no answer.
642    Required,
643    /// An optional item has no answer and has not been skipped.
644    Unanswered,
645    /// A validator or the host supplied this text.
646    Message(SharedString),
647}
648
649impl QuestionnaireValidationError {
650    /// The text the host or a validator wrote, if this is not a built-in reason.
651    pub fn message(&self) -> Option<&SharedString> {
652        match self {
653            Self::Message(message) => Some(message),
654            _ => None,
655        }
656    }
657}
658
659#[derive(Clone, Debug, PartialEq, Eq)]
660#[non_exhaustive]
661pub enum QuestionnaireSchemaError {
662    DuplicateItem(SharedString),
663    DuplicateChoice {
664        item: SharedString,
665        choice: SharedString,
666    },
667    MultipleDefaultsForSingleItem(SharedString),
668    UnknownItem(SharedString),
669    UnknownChoice {
670        item: SharedString,
671        choice: SharedString,
672    },
673    AnswerDoesNotMatchItem(SharedString),
674}
675
676impl fmt::Display for QuestionnaireSchemaError {
677    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
678        match self {
679            Self::DuplicateItem(item) => write!(formatter, "duplicate questionnaire item `{item}`"),
680            Self::DuplicateChoice { item, choice } => {
681                write!(formatter, "duplicate choice `{choice}` in item `{item}`")
682            }
683            Self::MultipleDefaultsForSingleItem(item) => write!(
684                formatter,
685                "single-choice item `{item}` has more than one default answer"
686            ),
687            Self::UnknownItem(item) => write!(formatter, "unknown questionnaire item `{item}`"),
688            Self::UnknownChoice { item, choice } => {
689                write!(formatter, "unknown choice `{choice}` in item `{item}`")
690            }
691            Self::AnswerDoesNotMatchItem(item) => {
692                write!(formatter, "answer does not match item `{item}`")
693            }
694        }
695    }
696}
697
698impl Error for QuestionnaireSchemaError {}