Skip to main content

gpui_base/questionnaire/
state.rs

1use std::collections::HashSet;
2
3use crate::input::{InputEvent, InputState};
4use gpui::{
5    App, Context, Entity, EventEmitter, FocusHandle, Focusable as _, SharedString, Subscription,
6    Window,
7};
8
9use super::types::*;
10
11struct ItemRuntime {
12    disabled: bool,
13    choice_disabled: Vec<bool>,
14    input_disabled: bool,
15    answer: QuestionnaireAnswer,
16    initial_answer: QuestionnaireAnswer,
17    initial_input_value: Option<SharedString>,
18    skipped: bool,
19    validation_attempted: bool,
20    internal_error: Option<QuestionnaireValidationError>,
21    external_error: Option<QuestionnaireValidationError>,
22    focus_handle: FocusHandle,
23    choice_focus_handles: Vec<FocusHandle>,
24    input_focus_handle: Option<FocusHandle>,
25}
26
27/// Owns questionnaire answers, validation, navigation and focus state.
28pub struct QuestionnaireState {
29    items: Vec<QuestionnaireItemDefinition>,
30    runtime: Vec<ItemRuntime>,
31    current: Option<usize>,
32    initial_current: Option<SharedString>,
33    shortcut_mode: Option<QuestionnaireShortcutMode>,
34    complete: bool,
35    focus_handle: FocusHandle,
36    _subscriptions: Vec<Subscription>,
37}
38
39impl QuestionnaireState {
40    pub fn new(
41        items: Vec<QuestionnaireItemDefinition>,
42        cx: &mut Context<Self>,
43    ) -> Result<Self, QuestionnaireSchemaError> {
44        Self::validate_schema(&items)?;
45
46        let mut runtime = Vec::with_capacity(items.len());
47        let mut subscriptions = Vec::new();
48
49        for item in &items {
50            let mut answer = QuestionnaireAnswer::new().with_choices(
51                item.choices()
52                    .iter()
53                    .filter(|choice| choice.is_default_selected() && !choice.is_disabled())
54                    .map(|choice| choice.value().clone()),
55            );
56
57            let mut initial_input_value = None;
58            let mut input_focus_handle = None;
59            if let Some(input) = item.input() {
60                let state = input.state();
61                state.update(cx, |state, cx| {
62                    state.set_disabled(item.is_disabled() || input.is_disabled(), cx);
63                });
64
65                let value = state.read(cx).value();
66                initial_input_value = Some(value.clone());
67                input_focus_handle = Some(state.focus_handle(cx));
68                if !value.trim().is_empty() && !input.is_disabled() {
69                    if !item.is_multiple() {
70                        answer = QuestionnaireAnswer::new();
71                    }
72                    answer.freeform = Some(value);
73                }
74
75                subscriptions.push(cx.subscribe(state, |this, input, event: &InputEvent, cx| {
76                    if matches!(event, InputEvent::Change) {
77                        this.on_input_change(&input, cx);
78                    }
79                }));
80            }
81
82            runtime.push(ItemRuntime {
83                disabled: item.is_disabled(),
84                choice_disabled: item
85                    .choices()
86                    .iter()
87                    .map(QuestionnaireChoiceDefinition::is_disabled)
88                    .collect(),
89                input_disabled: item
90                    .input()
91                    .is_none_or(QuestionnaireInputDefinition::is_disabled),
92                initial_answer: answer.clone(),
93                initial_input_value,
94                answer,
95                skipped: false,
96                validation_attempted: false,
97                internal_error: None,
98                external_error: None,
99                focus_handle: cx.focus_handle(),
100                choice_focus_handles: item.choices().iter().map(|_| cx.focus_handle()).collect(),
101                input_focus_handle,
102            });
103        }
104
105        let current = runtime.iter().position(|item| !item.disabled);
106        let initial_current = current.map(|ix| items[ix].name().clone());
107
108        Ok(Self {
109            items,
110            runtime,
111            current,
112            initial_current,
113            shortcut_mode: None,
114            complete: false,
115            focus_handle: cx.focus_handle(),
116            _subscriptions: subscriptions,
117        })
118    }
119
120    fn validate_schema(
121        items: &[QuestionnaireItemDefinition],
122    ) -> Result<(), QuestionnaireSchemaError> {
123        let mut item_names = HashSet::new();
124        for item in items {
125            if !item_names.insert(item.name().to_string()) {
126                return Err(QuestionnaireSchemaError::DuplicateItem(item.name().clone()));
127            }
128
129            let mut choices = HashSet::new();
130            let mut defaults = 0;
131            for choice in item.choices() {
132                if !choices.insert(choice.value().to_string()) {
133                    return Err(QuestionnaireSchemaError::DuplicateChoice {
134                        item: item.name().clone(),
135                        choice: choice.value().clone(),
136                    });
137                }
138                defaults += usize::from(choice.is_default_selected());
139            }
140            if !item.is_multiple() && defaults > 1 {
141                return Err(QuestionnaireSchemaError::MultipleDefaultsForSingleItem(
142                    item.name().clone(),
143                ));
144            }
145        }
146        Ok(())
147    }
148
149    pub fn with_current_item(
150        mut self,
151        name: impl Into<SharedString>,
152    ) -> Result<Self, QuestionnaireSchemaError> {
153        let name = name.into();
154        let ix = self.item_ix(&name)?;
155        if !self.runtime[ix].disabled {
156            self.current = Some(ix);
157            self.initial_current = Some(name);
158        }
159        Ok(self)
160    }
161
162    pub fn with_shortcuts(mut self, mode: QuestionnaireShortcutMode) -> Self {
163        self.shortcut_mode = Some(mode);
164        self
165    }
166
167    pub fn current_item(&self) -> Option<&SharedString> {
168        self.current.map(|ix| self.items[ix].name())
169    }
170
171    pub fn current_ix(&self) -> Option<usize> {
172        let current = self.current?;
173        self.enabled_indices().position(|ix| ix == current)
174    }
175
176    pub fn total(&self) -> usize {
177        self.enabled_indices().count()
178    }
179
180    pub fn progress(&self) -> QuestionnaireProgressState {
181        QuestionnaireProgressState::new(self.current_ix().map_or(0, |ix| ix + 1), self.total())
182    }
183
184    pub fn item_definition(&self, name: &str) -> Option<&QuestionnaireItemDefinition> {
185        self.items.iter().find(|item| item.name().as_ref() == name)
186    }
187
188    pub fn choice_definition(
189        &self,
190        item: &str,
191        value: &str,
192    ) -> Option<&QuestionnaireChoiceDefinition> {
193        self.item_definition(item)?
194            .choices()
195            .iter()
196            .find(|choice| choice.value().as_ref() == value)
197    }
198
199    pub fn item_state(&self, name: &str) -> Option<QuestionnaireItemState> {
200        let ix = self.item_ix_opt(name)?;
201        let definition = &self.items[ix];
202        Some(QuestionnaireItemState::new(
203            definition.name().clone(),
204            self.status(ix),
205            definition.is_required(),
206            definition.is_multiple(),
207            self.runtime[ix].disabled,
208            self.error_at(ix).is_some(),
209            definition.input().is_some(),
210        ))
211    }
212
213    pub fn choice_state(&self, item: &str, value: &str) -> Option<QuestionnaireChoiceState> {
214        let item_ix = self.item_ix_opt(item)?;
215        let choice_ix = self.choice_ix_opt(item_ix, value)?;
216        let runtime = &self.runtime[item_ix];
217        let definition = &self.items[item_ix].choices()[choice_ix];
218        Some(QuestionnaireChoiceState::new(
219            definition.value().clone(),
220            runtime.answer.choices.contains(definition.value()),
221            runtime.disabled || runtime.choice_disabled[choice_ix],
222            self.error_at(item_ix).is_some(),
223            self.shortcut_for_choice(item, value),
224        ))
225    }
226
227    /// One-based position of a choice among its item's enabled choices, with
228    /// the enabled total. Assistive technology announces the pair.
229    pub fn choice_position(&self, item: &str, value: &str) -> Option<(usize, usize)> {
230        let definition = self.item_definition(item)?;
231        let enabled: Vec<_> = definition
232            .choices()
233            .iter()
234            .filter(|choice| {
235                self.choice_state(item, choice.value())
236                    .is_some_and(|choice| !choice.is_disabled())
237            })
238            .collect();
239        enabled
240            .iter()
241            .position(|choice| choice.value().as_ref() == value)
242            .map(|position| (position + 1, enabled.len()))
243    }
244
245    pub fn navigation_state(&self) -> QuestionnaireNavigationState {
246        let Some(ix) = self.current_ix() else {
247            return QuestionnaireNavigationState::default();
248        };
249        let total = self.total();
250        let item_ix = self
251            .current
252            .expect("current item exists when enabled index exists");
253        QuestionnaireNavigationState::new(
254            ix > 0,
255            ix + 1 < total,
256            !self.items[item_ix].is_required(),
257            ix + 1 == total,
258            self.status(item_ix) != QuestionnaireItemStatus::Unanswered,
259        )
260    }
261
262    pub fn answer(&self, name: &str) -> Option<QuestionnaireAnswer> {
263        let ix = self.item_ix_opt(name)?;
264        Some(self.effective_answer(ix))
265    }
266
267    pub fn answers(&self) -> QuestionnaireAnswers {
268        QuestionnaireAnswers::from_entries(
269            self.enabled_indices()
270                .map(|ix| (self.items[ix].name().clone(), self.effective_answer(ix)))
271                .collect(),
272        )
273    }
274
275    /// The active validation failure for an item, if any. Base reports the
276    /// reason; the presentation layer turns a built-in reason into text.
277    pub fn error(&self, name: &str) -> Option<&QuestionnaireValidationError> {
278        self.item_ix_opt(name).and_then(|ix| self.error_at(ix))
279    }
280
281    pub fn is_complete(&self) -> bool {
282        self.complete
283    }
284
285    pub fn input_state(&self, name: &str) -> Option<Entity<InputState>> {
286        self.item_definition(name)?
287            .input()
288            .map(|input| input.state().clone())
289    }
290
291    pub fn focus_handle(&self) -> &FocusHandle {
292        &self.focus_handle
293    }
294
295    pub fn item_focus_handle(&self, name: &str) -> Option<&FocusHandle> {
296        self.item_ix_opt(name)
297            .map(|ix| &self.runtime[ix].focus_handle)
298    }
299
300    pub fn choice_focus_handle(&self, item: &str, value: &str) -> Option<&FocusHandle> {
301        let item_ix = self.item_ix_opt(item)?;
302        let choice_ix = self.choice_ix_opt(item_ix, value)?;
303        Some(&self.runtime[item_ix].choice_focus_handles[choice_ix])
304    }
305
306    pub fn is_current_input_focused(&self, window: &Window) -> bool {
307        let Some(ix) = self.current else { return false };
308        self.runtime[ix]
309            .input_focus_handle
310            .as_ref()
311            .is_some_and(|handle| handle.is_focused(window))
312    }
313
314    /// Whether the active item's freeform input currently holds text. The skin
315    /// needs it to decide whether an arrow key moves focus or edits the draft.
316    pub fn current_input_has_text(&self, cx: &App) -> bool {
317        let Some(item_ix) = self.current else {
318            return false;
319        };
320        self.items[item_ix]
321            .input()
322            .is_some_and(|input| !input.state().read(cx).value().trim().is_empty())
323    }
324
325    pub fn focused_current_choice(&self, window: &Window) -> Option<&SharedString> {
326        let ix = self.current?;
327        self.runtime[ix]
328            .choice_focus_handles
329            .iter()
330            .position(|handle| handle.is_focused(window))
331            .map(|choice_ix| self.items[ix].choices()[choice_ix].value())
332    }
333
334    pub fn shortcut_mode(&self) -> Option<QuestionnaireShortcutMode> {
335        self.shortcut_mode
336    }
337
338    pub fn shortcut_for_choice(&self, item: &str, value: &str) -> Option<SharedString> {
339        let mode = self.shortcut_mode?;
340        let item_ix = self.item_ix_opt(item)?;
341        let choice_ix = self.choice_ix_opt(item_ix, value)?;
342        if self.runtime[item_ix].disabled || self.runtime[item_ix].choice_disabled[choice_ix] {
343            return None;
344        }
345        let enabled_position = (0..=choice_ix)
346            .filter(|ix| !self.runtime[item_ix].choice_disabled[*ix])
347            .count()
348            .checked_sub(1)?;
349        match mode {
350            QuestionnaireShortcutMode::Letters if enabled_position < 26 => {
351                Some(char::from(b'A' + enabled_position as u8).to_string().into())
352            }
353            QuestionnaireShortcutMode::Numbers if enabled_position < 9 => {
354                Some((enabled_position + 1).to_string().into())
355            }
356            _ => None,
357        }
358    }
359
360    pub fn choice_for_shortcut(&self, item: &str, key: &str) -> Option<&SharedString> {
361        let item_ix = self.item_ix_opt(item)?;
362        self.items[item_ix].choices().iter().find_map(|choice| {
363            self.shortcut_for_choice(item, choice.value())
364                .is_some_and(|shortcut| shortcut.as_ref().eq_ignore_ascii_case(key))
365                .then_some(choice.value())
366        })
367    }
368
369    pub fn activate_shortcut(
370        &mut self,
371        key: &str,
372        window: &mut Window,
373        cx: &mut Context<Self>,
374    ) -> bool {
375        let Some(item_ix) = self.current else {
376            return false;
377        };
378        let item = self.items[item_ix].name().clone();
379        let Some(choice) = self.choice_for_shortcut(&item, key).cloned() else {
380            return false;
381        };
382        if self.activate_choice(&item, &choice, cx).is_err() {
383            return false;
384        }
385        self.focus_choice(&item, &choice, window, cx)
386    }
387
388    pub fn set_current_item(
389        &mut self,
390        name: &str,
391        window: &mut Window,
392        cx: &mut Context<Self>,
393    ) -> Result<(), QuestionnaireSchemaError> {
394        let ix = self.item_ix(name)?;
395        if !self.runtime[ix].disabled {
396            self.current = Some(ix);
397            self.focus_current_item(window, cx);
398            cx.notify();
399        }
400        Ok(())
401    }
402
403    pub fn set_answer(
404        &mut self,
405        item: &str,
406        mut answer: QuestionnaireAnswer,
407        window: &mut Window,
408        cx: &mut Context<Self>,
409    ) -> Result<(), QuestionnaireSchemaError> {
410        let item_ix = self.item_ix(item)?;
411        let before = self.effective_answer(item_ix);
412        let before_status = self.status(item_ix);
413        if answer
414            .freeform
415            .as_ref()
416            .is_some_and(|value| value.trim().is_empty())
417        {
418            answer.freeform = None;
419        }
420        self.check_answer(item_ix, &answer)?;
421        answer.choices = self.items[item_ix]
422            .choices()
423            .iter()
424            .filter(|choice| answer.choices.contains(choice.value()))
425            .map(|choice| choice.value().clone())
426            .collect();
427        self.runtime[item_ix].answer = answer.clone();
428        self.runtime[item_ix].skipped = false;
429        if let (Some(input), Some(value)) =
430            (self.items[item_ix].input(), answer.freeform().cloned())
431        {
432            input
433                .state()
434                .update(cx, |input, cx| input.set_value(value, window, cx));
435        }
436        if before != self.effective_answer(item_ix) || before_status != self.status(item_ix) {
437            self.answer_did_change(item_ix, false, cx);
438        }
439        Ok(())
440    }
441
442    pub fn set_input_value(
443        &mut self,
444        item: &str,
445        value: impl Into<SharedString>,
446        window: &mut Window,
447        cx: &mut Context<Self>,
448    ) -> Result<(), QuestionnaireSchemaError> {
449        let item_ix = self.item_ix(item)?;
450        let Some(input) = self.items[item_ix]
451            .input()
452            .map(|input| input.state().clone())
453        else {
454            return Err(QuestionnaireSchemaError::AnswerDoesNotMatchItem(
455                self.items[item_ix].name().clone(),
456            ));
457        };
458        let value: SharedString = value.into();
459        input.update(cx, |input, cx| input.set_value(value, window, cx));
460        self.sync_input_answer(item_ix, false, cx);
461        Ok(())
462    }
463
464    pub fn set_item_disabled(
465        &mut self,
466        name: &str,
467        disabled: bool,
468        window: &mut Window,
469        cx: &mut Context<Self>,
470    ) -> Result<(), QuestionnaireSchemaError> {
471        let ix = self.item_ix(name)?;
472        if self.runtime[ix].disabled == disabled {
473            return Ok(());
474        }
475        self.runtime[ix].disabled = disabled;
476        if let Some(input) = self.items[ix].input() {
477            let input_disabled = disabled || self.runtime[ix].input_disabled;
478            input
479                .state()
480                .update(cx, |input, cx| input.set_disabled(input_disabled, cx));
481        }
482        self.complete = false;
483        if self.current == Some(ix) && disabled {
484            let next = self
485                .enabled_indices()
486                .find(|candidate| *candidate > ix)
487                .or_else(|| {
488                    self.enabled_indices()
489                        .rev()
490                        .find(|candidate| *candidate < ix)
491                });
492            self.current = next;
493            self.focus_current_item(window, cx);
494        } else if self.current.is_none() && !disabled {
495            self.current = Some(ix);
496            self.focus_current_item(window, cx);
497        }
498        cx.notify();
499        Ok(())
500    }
501
502    pub fn set_choice_disabled(
503        &mut self,
504        item: &str,
505        value: &str,
506        disabled: bool,
507        cx: &mut Context<Self>,
508    ) -> Result<(), QuestionnaireSchemaError> {
509        let item_ix = self.item_ix(item)?;
510        let choice_ix = self.choice_ix(item_ix, value)?;
511        if self.runtime[item_ix].choice_disabled[choice_ix] == disabled {
512            return Ok(());
513        }
514        self.runtime[item_ix].choice_disabled[choice_ix] = disabled;
515        self.answer_did_change(item_ix, false, cx);
516        Ok(())
517    }
518
519    pub fn set_external_error(
520        &mut self,
521        item: &str,
522        error: impl Into<SharedString>,
523        cx: &mut Context<Self>,
524    ) -> Result<(), QuestionnaireSchemaError> {
525        let ix = self.item_ix(item)?;
526        self.runtime[ix].external_error = Some(QuestionnaireValidationError::Message(error.into()));
527        self.complete = false;
528        cx.notify();
529        Ok(())
530    }
531
532    pub fn clear_external_error(
533        &mut self,
534        item: &str,
535        cx: &mut Context<Self>,
536    ) -> Result<(), QuestionnaireSchemaError> {
537        let ix = self.item_ix(item)?;
538        self.runtime[ix].external_error = None;
539        cx.notify();
540        Ok(())
541    }
542
543    pub fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
544        for ix in 0..self.items.len() {
545            let initial = self.runtime[ix].initial_answer.clone();
546            self.runtime[ix].answer = initial;
547            self.runtime[ix].skipped = false;
548            self.runtime[ix].validation_attempted = false;
549            self.runtime[ix].internal_error = None;
550            if let Some(input) = self.items[ix].input() {
551                let value = self.runtime[ix]
552                    .initial_input_value
553                    .as_ref()
554                    .cloned()
555                    .unwrap_or_default();
556                input
557                    .state()
558                    .update(cx, |input, cx| input.set_value(value, window, cx));
559            }
560        }
561        self.complete = false;
562        self.current = self
563            .initial_current
564            .as_ref()
565            .and_then(|name| self.item_ix_opt(name));
566        if self.current.is_some_and(|ix| self.runtime[ix].disabled) {
567            let next = self.enabled_indices().next();
568            self.current = next;
569        }
570        self.focus_current_item(window, cx);
571        cx.notify();
572    }
573
574    pub fn activate_choice(
575        &mut self,
576        item: &str,
577        value: &str,
578        cx: &mut Context<Self>,
579    ) -> Result<(), QuestionnaireSchemaError> {
580        let item_ix = self.item_ix(item)?;
581        let choice_ix = self.choice_ix(item_ix, value)?;
582        if self.runtime[item_ix].disabled || self.runtime[item_ix].choice_disabled[choice_ix] {
583            return Ok(());
584        }
585        let before = self.effective_answer(item_ix);
586        let before_status = self.status(item_ix);
587
588        if self.items[item_ix].is_multiple() {
589            let selected = self.runtime[item_ix]
590                .answer
591                .choices
592                .iter()
593                .position(|choice| choice.as_ref() == value);
594            if let Some(ix) = selected {
595                self.runtime[item_ix].answer.choices.remove(ix);
596            } else {
597                self.runtime[item_ix].answer.choices.push(value.into());
598            }
599        } else {
600            self.runtime[item_ix].answer.choices.clear();
601            self.runtime[item_ix].answer.choices.push(value.into());
602            self.runtime[item_ix].answer.freeform = None;
603        }
604        self.runtime[item_ix].skipped = false;
605        if before != self.effective_answer(item_ix) || before_status != self.status(item_ix) {
606            self.answer_did_change(item_ix, true, cx);
607        }
608        Ok(())
609    }
610
611    pub fn confirm_current(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
612        let Some(current_ix) = self.current_ix() else {
613            return false;
614        };
615        if current_ix + 1 == self.total() {
616            self.submit(window, cx)
617        } else {
618            self.go_next(window, cx)
619        }
620    }
621
622    pub fn go_previous(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
623        let Some(ix) = self.current_ix() else {
624            return false;
625        };
626        let enabled: Vec<_> = self.enabled_indices().collect();
627        if ix == 0 {
628            return false;
629        }
630        self.change_current(Some(enabled[ix - 1]), true, window, cx);
631        true
632    }
633
634    pub fn go_next(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
635        let Some(current) = self.current else {
636            return false;
637        };
638        if !self.validate_item(current) {
639            self.focus_invalid_item(self.items[current].name(), window, cx);
640            cx.notify();
641            return false;
642        }
643        let Some(ix) = self.current_ix() else {
644            return false;
645        };
646        let enabled: Vec<_> = self.enabled_indices().collect();
647        if ix + 1 >= enabled.len() {
648            return false;
649        }
650        self.change_current(Some(enabled[ix + 1]), true, window, cx);
651        true
652    }
653
654    pub fn skip_current(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
655        let Some(ix) = self.current else { return false };
656        if self.items[ix].is_required() {
657            return false;
658        }
659        self.runtime[ix].answer = QuestionnaireAnswer::new();
660        self.runtime[ix].skipped = true;
661        self.complete = false;
662        self.emit_answer_changed(ix, cx);
663        if self
664            .current_ix()
665            .is_some_and(|current| current + 1 == self.total())
666        {
667            self.submit(window, cx)
668        } else {
669            self.go_next(window, cx)
670        }
671    }
672
673    pub fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
674        let enabled: Vec<_> = self.enabled_indices().collect();
675        let mut first_invalid = None;
676        for ix in enabled {
677            if !self.validate_item(ix) && first_invalid.is_none() {
678                first_invalid = Some(ix);
679            }
680        }
681        if let Some(ix) = first_invalid {
682            self.change_current(Some(ix), true, window, cx);
683            self.focus_invalid_item(self.items[ix].name(), window, cx);
684            cx.notify();
685            return false;
686        }
687
688        let submission = self.submission();
689        if !self.complete {
690            self.complete = true;
691            cx.emit(QuestionnaireEvent::Completed(submission.clone()));
692        }
693        cx.emit(QuestionnaireEvent::Submit(submission));
694        cx.notify();
695        true
696    }
697
698    pub fn focus_current_item(&self, window: &mut Window, cx: &mut App) -> bool {
699        let Some(ix) = self.current else { return false };
700        self.runtime[ix].focus_handle.focus(window, cx);
701        true
702    }
703
704    pub fn focus_invalid_item(&self, item: &str, window: &mut Window, cx: &mut App) -> bool {
705        let Some(item_ix) = self.item_ix_opt(item) else {
706            return false;
707        };
708        if self.runtime[item_ix].answer.freeform().is_some()
709            && !self.runtime[item_ix].input_disabled
710            && let Some(focus_handle) = &self.runtime[item_ix].input_focus_handle
711        {
712            focus_handle.focus(window, cx);
713            return true;
714        }
715        for (choice_ix, choice) in self.items[item_ix].choices().iter().enumerate() {
716            if self.runtime[item_ix]
717                .answer
718                .choices
719                .contains(choice.value())
720                && !self.runtime[item_ix].choice_disabled[choice_ix]
721            {
722                self.runtime[item_ix].choice_focus_handles[choice_ix].focus(window, cx);
723                return true;
724            }
725        }
726        if let Some(choice_ix) = self.runtime[item_ix]
727            .choice_disabled
728            .iter()
729            .position(|disabled| !disabled)
730        {
731            self.runtime[item_ix].choice_focus_handles[choice_ix].focus(window, cx);
732            return true;
733        }
734        if !self.runtime[item_ix].input_disabled
735            && let Some(focus_handle) = &self.runtime[item_ix].input_focus_handle
736        {
737            focus_handle.focus(window, cx);
738            return true;
739        }
740        self.runtime[item_ix].focus_handle.focus(window, cx);
741        true
742    }
743
744    pub fn focus_choice(&self, item: &str, value: &str, window: &mut Window, cx: &mut App) -> bool {
745        let Some(item_ix) = self.item_ix_opt(item) else {
746            return false;
747        };
748        let Some(choice_ix) = self.choice_ix_opt(item_ix, value) else {
749            return false;
750        };
751        self.runtime[item_ix].choice_focus_handles[choice_ix].focus(window, cx);
752        true
753    }
754
755    pub fn focus_input(&self, item: &str, window: &mut Window, cx: &mut App) -> bool {
756        let Some(item_ix) = self.item_ix_opt(item) else {
757            return false;
758        };
759        let Some(focus_handle) = &self.runtime[item_ix].input_focus_handle else {
760            return false;
761        };
762        focus_handle.focus(window, cx);
763        true
764    }
765
766    pub fn focus_previous_answer(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
767        self.focus_adjacent_answer(-1, window, cx)
768    }
769
770    pub fn focus_next_answer(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
771        self.focus_adjacent_answer(1, window, cx)
772    }
773
774    pub fn move_current_radio(
775        &mut self,
776        direction: isize,
777        window: &mut Window,
778        cx: &mut Context<Self>,
779    ) -> bool {
780        let Some(item_ix) = self.current else {
781            return false;
782        };
783        if self.items[item_ix].is_multiple() || self.is_current_input_focused(window) {
784            return false;
785        }
786        let enabled: Vec<_> = self.runtime[item_ix]
787            .choice_disabled
788            .iter()
789            .enumerate()
790            .filter_map(|(ix, disabled)| (!disabled).then_some(ix))
791            .collect();
792        if enabled.is_empty() {
793            return false;
794        }
795        let current = enabled
796            .iter()
797            .position(|ix| self.runtime[item_ix].choice_focus_handles[*ix].is_focused(window))
798            .or_else(|| {
799                enabled.iter().position(|ix| {
800                    self.runtime[item_ix]
801                        .answer
802                        .choices
803                        .contains(self.items[item_ix].choices()[*ix].value())
804                })
805            });
806        let target = match (current, direction.is_negative()) {
807            (Some(ix), true) => ix.checked_sub(1).unwrap_or(enabled.len() - 1),
808            (Some(ix), false) => (ix + 1) % enabled.len(),
809            (None, true) => enabled.len() - 1,
810            (None, false) => 0,
811        };
812        let choice_ix = enabled[target];
813        let item = self.items[item_ix].name().clone();
814        let choice = self.items[item_ix].choices()[choice_ix].value().clone();
815        let _ = self.activate_choice(&item, &choice, cx);
816        self.runtime[item_ix].choice_focus_handles[choice_ix].focus(window, cx);
817        true
818    }
819
820    fn focus_adjacent_answer(
821        &mut self,
822        direction: isize,
823        window: &mut Window,
824        cx: &mut Context<Self>,
825    ) -> bool {
826        let Some(item_ix) = self.current else {
827            return false;
828        };
829        if self.is_current_input_focused(window) && self.current_input_has_text(cx) {
830            return false;
831        }
832
833        #[derive(Clone, Copy)]
834        enum Target {
835            Choice(usize),
836            Input,
837        }
838        let mut targets = Vec::new();
839        for choice_ix in 0..self.items[item_ix].choices().len() {
840            if !self.runtime[item_ix].choice_disabled[choice_ix] {
841                targets.push(Target::Choice(choice_ix));
842            }
843        }
844        if self.items[item_ix].input().is_some() && !self.runtime[item_ix].input_disabled {
845            targets.push(Target::Input);
846        }
847        if targets.is_empty() {
848            return false;
849        }
850
851        let focused = targets.iter().position(|target| match target {
852            Target::Choice(ix) => {
853                self.runtime[item_ix].choice_focus_handles[*ix].is_focused(window)
854            }
855            Target::Input => self.is_current_input_focused(window),
856        });
857
858        if focused.is_none() && self.runtime[item_ix].focus_handle.is_focused(window) {
859            let filled: Vec<_> = targets
860                .iter()
861                .enumerate()
862                .filter_map(|(ix, target)| match target {
863                    Target::Choice(choice_ix)
864                        if self.runtime[item_ix]
865                            .answer
866                            .choices
867                            .contains(self.items[item_ix].choices()[*choice_ix].value()) =>
868                    {
869                        Some(ix)
870                    }
871                    Target::Input if self.runtime[item_ix].answer.freeform().is_some() => Some(ix),
872                    _ => None,
873                })
874                .collect();
875            let filled_ix = if direction.is_negative() {
876                filled.last().copied()
877            } else {
878                filled.first().copied()
879            };
880            if let Some(filled_ix) = filled_ix {
881                match targets[filled_ix] {
882                    Target::Choice(choice_ix) => {
883                        self.runtime[item_ix].choice_focus_handles[choice_ix].focus(window, cx);
884                    }
885                    Target::Input => {
886                        if let Some(focus_handle) = &self.runtime[item_ix].input_focus_handle {
887                            focus_handle.focus(window, cx);
888                        }
889                    }
890                }
891                return true;
892            }
893        }
894
895        let target_ix = match (focused, direction.is_negative()) {
896            (Some(ix), true) => ix.checked_sub(1).unwrap_or(targets.len() - 1),
897            (Some(ix), false) => (ix + 1) % targets.len(),
898            (None, true) => targets.len() - 1,
899            (None, false) => 0,
900        };
901
902        // GPUI's Radio primitive intentionally leaves group arrow behavior to
903        // its owner. Let the root emulate native radio movement only when both
904        // adjacent answers are radios; crossing the boundary to an Input stays
905        // in this schema-ordered answer sequence.
906        if !self.items[item_ix].is_multiple()
907            && focused.is_some_and(|focused_ix| {
908                matches!(targets[focused_ix], Target::Choice(_))
909                    && matches!(targets[target_ix], Target::Choice(_))
910            })
911        {
912            return false;
913        }
914
915        match targets[target_ix] {
916            Target::Choice(choice_ix) => {
917                if !self.items[item_ix].is_multiple() {
918                    let item = self.items[item_ix].name().clone();
919                    let choice = self.items[item_ix].choices()[choice_ix].value().clone();
920                    let _ = self.activate_choice(&item, &choice, cx);
921                }
922                self.runtime[item_ix].choice_focus_handles[choice_ix].focus(window, cx);
923            }
924            Target::Input => {
925                if let Some(focus_handle) = &self.runtime[item_ix].input_focus_handle {
926                    focus_handle.focus(window, cx);
927                }
928            }
929        }
930        true
931    }
932
933    fn on_input_change(&mut self, input: &Entity<InputState>, cx: &mut Context<Self>) {
934        if let Some(ix) = self.input_item_ix(input) {
935            self.sync_input_answer(ix, true, cx);
936        }
937    }
938
939    fn sync_input_answer(&mut self, item_ix: usize, emit: bool, cx: &mut Context<Self>) {
940        if self.runtime[item_ix].disabled || self.runtime[item_ix].input_disabled {
941            return;
942        }
943        let before = self.effective_answer(item_ix);
944        let before_status = self.status(item_ix);
945        let Some(input) = self.items[item_ix].input() else {
946            return;
947        };
948        let value = input.state().read(cx).value();
949        if value.trim().is_empty() {
950            self.runtime[item_ix].answer.freeform = None;
951        } else {
952            if !self.items[item_ix].is_multiple() {
953                self.runtime[item_ix].answer.choices.clear();
954            }
955            self.runtime[item_ix].answer.freeform = Some(value);
956            self.runtime[item_ix].skipped = false;
957        }
958        if before != self.effective_answer(item_ix) || before_status != self.status(item_ix) {
959            self.answer_did_change(item_ix, emit, cx);
960        }
961    }
962
963    fn answer_did_change(&mut self, item_ix: usize, emit: bool, cx: &mut Context<Self>) {
964        if self.runtime[item_ix].validation_attempted {
965            self.validate_item(item_ix);
966        } else {
967            self.runtime[item_ix].internal_error = None;
968        }
969        self.complete = false;
970        if emit {
971            self.emit_answer_changed(item_ix, cx);
972        }
973        cx.notify();
974    }
975
976    fn emit_answer_changed(&self, item_ix: usize, cx: &mut Context<Self>) {
977        cx.emit(QuestionnaireEvent::AnswerChanged(
978            QuestionnaireAnswerChange::new(
979                self.items[item_ix].name().clone(),
980                self.effective_answer(item_ix),
981                self.status(item_ix),
982            ),
983        ));
984    }
985
986    fn validate_item(&mut self, item_ix: usize) -> bool {
987        if self.runtime[item_ix].disabled || self.runtime[item_ix].skipped {
988            return true;
989        }
990        self.runtime[item_ix].validation_attempted = true;
991        let answer = self.effective_answer(item_ix);
992        let error = if answer.is_empty() {
993            Some(if self.items[item_ix].is_required() {
994                QuestionnaireValidationError::Required
995            } else {
996                QuestionnaireValidationError::Unanswered
997            })
998        } else if let Some(validator) = self.items[item_ix].validator().cloned() {
999            validator(&QuestionnaireValidationContext::new(
1000                self.items[item_ix].name().clone(),
1001                answer,
1002                self.answers(),
1003            ))
1004            .err()
1005            .map(QuestionnaireValidationError::Message)
1006        } else {
1007            None
1008        };
1009        self.runtime[item_ix].internal_error = error;
1010        self.error_at(item_ix).is_none()
1011    }
1012
1013    fn submission(&self) -> QuestionnaireSubmission {
1014        QuestionnaireSubmission::new(
1015            self.enabled_indices()
1016                .map(|ix| {
1017                    QuestionnaireSubmissionItem::new(
1018                        self.items[ix].name().clone(),
1019                        self.status(ix),
1020                        self.effective_answer(ix),
1021                    )
1022                })
1023                .collect(),
1024        )
1025    }
1026
1027    fn status(&self, item_ix: usize) -> QuestionnaireItemStatus {
1028        if self.runtime[item_ix].skipped {
1029            QuestionnaireItemStatus::Skipped
1030        } else if self.effective_answer(item_ix).is_empty() {
1031            QuestionnaireItemStatus::Unanswered
1032        } else {
1033            QuestionnaireItemStatus::Answered
1034        }
1035    }
1036
1037    fn effective_answer(&self, item_ix: usize) -> QuestionnaireAnswer {
1038        if self.runtime[item_ix].disabled {
1039            return QuestionnaireAnswer::new();
1040        }
1041        let runtime = &self.runtime[item_ix];
1042        QuestionnaireAnswer {
1043            choices: self.items[item_ix]
1044                .choices()
1045                .iter()
1046                .filter(|choice| {
1047                    runtime.answer.choices.contains(choice.value())
1048                        && self
1049                            .choice_ix_opt(item_ix, choice.value())
1050                            .is_some_and(|ix| !runtime.choice_disabled[ix])
1051                })
1052                .map(|choice| choice.value().clone())
1053                .collect(),
1054            freeform: (!runtime.input_disabled)
1055                .then(|| runtime.answer.freeform.clone())
1056                .flatten(),
1057        }
1058    }
1059
1060    fn error_at(&self, item_ix: usize) -> Option<&QuestionnaireValidationError> {
1061        if self.runtime[item_ix].skipped || self.runtime[item_ix].disabled {
1062            return None;
1063        }
1064        self.runtime[item_ix].external_error.as_ref().or_else(|| {
1065            self.runtime[item_ix]
1066                .validation_attempted
1067                .then_some(self.runtime[item_ix].internal_error.as_ref())
1068                .flatten()
1069        })
1070    }
1071
1072    fn check_answer(
1073        &self,
1074        item_ix: usize,
1075        answer: &QuestionnaireAnswer,
1076    ) -> Result<(), QuestionnaireSchemaError> {
1077        let item = &self.items[item_ix];
1078        let sources = answer.choices.len() + usize::from(answer.freeform.is_some());
1079        if (!item.is_multiple() && sources > 1)
1080            || (answer.freeform.is_some() && item.input().is_none())
1081        {
1082            return Err(QuestionnaireSchemaError::AnswerDoesNotMatchItem(
1083                item.name().clone(),
1084            ));
1085        }
1086        for choice in &answer.choices {
1087            let choice_ix = self.choice_ix(item_ix, choice)?;
1088            if self.runtime[item_ix].choice_disabled[choice_ix] {
1089                return Err(QuestionnaireSchemaError::AnswerDoesNotMatchItem(
1090                    item.name().clone(),
1091                ));
1092            }
1093        }
1094        Ok(())
1095    }
1096
1097    fn change_current(
1098        &mut self,
1099        next: Option<usize>,
1100        emit: bool,
1101        window: &mut Window,
1102        cx: &mut Context<Self>,
1103    ) {
1104        if self.current == next {
1105            return;
1106        }
1107        let previous = self.current.map(|ix| self.items[ix].name().clone());
1108        self.current = next;
1109        self.focus_current_item(window, cx);
1110        if emit {
1111            cx.emit(QuestionnaireEvent::CurrentItemChanged {
1112                previous,
1113                current: next.map(|ix| self.items[ix].name().clone()),
1114            });
1115        }
1116        cx.notify();
1117    }
1118
1119    fn enabled_indices(&self) -> impl DoubleEndedIterator<Item = usize> + '_ {
1120        self.runtime
1121            .iter()
1122            .enumerate()
1123            .filter_map(|(ix, runtime)| (!runtime.disabled).then_some(ix))
1124    }
1125
1126    fn item_ix(&self, name: &str) -> Result<usize, QuestionnaireSchemaError> {
1127        self.item_ix_opt(name)
1128            .ok_or_else(|| QuestionnaireSchemaError::UnknownItem(name.into()))
1129    }
1130
1131    fn item_ix_opt(&self, name: &str) -> Option<usize> {
1132        self.items
1133            .iter()
1134            .position(|item| item.name().as_ref() == name)
1135    }
1136
1137    fn choice_ix(&self, item_ix: usize, value: &str) -> Result<usize, QuestionnaireSchemaError> {
1138        self.choice_ix_opt(item_ix, value)
1139            .ok_or_else(|| QuestionnaireSchemaError::UnknownChoice {
1140                item: self.items[item_ix].name().clone(),
1141                choice: value.into(),
1142            })
1143    }
1144
1145    fn choice_ix_opt(&self, item_ix: usize, value: &str) -> Option<usize> {
1146        self.items[item_ix]
1147            .choices()
1148            .iter()
1149            .position(|choice| choice.value().as_ref() == value)
1150    }
1151
1152    fn input_item_ix(&self, input: &Entity<InputState>) -> Option<usize> {
1153        self.items.iter().position(|item| {
1154            item.input()
1155                .is_some_and(|definition| definition.state().entity_id() == input.entity_id())
1156        })
1157    }
1158}
1159
1160impl EventEmitter<QuestionnaireEvent> for QuestionnaireState {}
1161
1162#[cfg(test)]
1163mod tests {
1164    use gpui::{
1165        AppContext as _, Context, Entity, IntoElement, Render, TestAppContext, VisualTestContext,
1166        div,
1167    };
1168
1169    use super::*;
1170
1171    struct Harness {
1172        state: Entity<QuestionnaireState>,
1173        first_input: Entity<InputState>,
1174        second_input: Entity<InputState>,
1175        events: Vec<&'static str>,
1176        _subscription: Subscription,
1177    }
1178
1179    impl Harness {
1180        fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
1181            let first_input = cx.new(|cx| InputState::new(window, cx));
1182            let second_input =
1183                cx.new(|cx| InputState::new(window, cx).default_value("initial draft"));
1184            let items = vec![
1185                QuestionnaireItemDefinition::new("first", "First question")
1186                    .with_required(true)
1187                    .with_choices([
1188                        QuestionnaireChoiceDefinition::new("a", "A"),
1189                        QuestionnaireChoiceDefinition::new("b", "B"),
1190                    ])
1191                    .with_input(QuestionnaireInputDefinition::new(
1192                        first_input.clone(),
1193                        "Another answer",
1194                    )),
1195                QuestionnaireItemDefinition::new("second", "Second question")
1196                    .with_multiple(true)
1197                    .with_choices([
1198                        QuestionnaireChoiceDefinition::new("x", "X"),
1199                        QuestionnaireChoiceDefinition::new("y", "Y"),
1200                    ])
1201                    .with_input(QuestionnaireInputDefinition::new(
1202                        second_input.clone(),
1203                        "Another answer",
1204                    ))
1205                    .with_validator(|context| {
1206                        (context.answer().freeform().map(SharedString::as_ref) == Some("valid"))
1207                            .then_some(())
1208                            .ok_or_else(|| SharedString::from("Use the valid answer"))
1209                    }),
1210                QuestionnaireItemDefinition::new("disabled", "Disabled").with_disabled(true),
1211            ];
1212            let state = cx.new(|cx| {
1213                QuestionnaireState::new(items, cx)
1214                    .unwrap()
1215                    .with_shortcuts(QuestionnaireShortcutMode::Letters)
1216            });
1217            let subscription = cx.subscribe(&state, |this, _, event, _| {
1218                this.events.push(match event {
1219                    QuestionnaireEvent::CurrentItemChanged { .. } => "current",
1220                    QuestionnaireEvent::AnswerChanged(_) => "answer",
1221                    QuestionnaireEvent::Completed(_) => "completed",
1222                    QuestionnaireEvent::Submit(_) => "submit",
1223                });
1224            });
1225            Self {
1226                state,
1227                first_input,
1228                second_input,
1229                events: Vec::new(),
1230                _subscription: subscription,
1231            }
1232        }
1233    }
1234
1235    impl Render for Harness {
1236        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1237            div()
1238        }
1239    }
1240
1241    fn harness(
1242        cx: &mut TestAppContext,
1243    ) -> (
1244        Entity<Harness>,
1245        Entity<QuestionnaireState>,
1246        Entity<InputState>,
1247        Entity<InputState>,
1248        &mut VisualTestContext,
1249    ) {
1250        cx.update(crate::init);
1251        let (harness, cx) = cx.add_window_view(Harness::new);
1252        let (state, first_input, second_input) = harness.read_with(cx, |harness, _| {
1253            (
1254                harness.state.clone(),
1255                harness.first_input.clone(),
1256                harness.second_input.clone(),
1257            )
1258        });
1259        (harness, state, first_input, second_input, cx)
1260    }
1261
1262    #[test]
1263    fn schema_rejects_duplicate_names_and_invalid_single_defaults() {
1264        let duplicate_items = vec![
1265            QuestionnaireItemDefinition::new("same", "One"),
1266            QuestionnaireItemDefinition::new("same", "Two"),
1267        ];
1268        assert_eq!(
1269            QuestionnaireState::validate_schema(&duplicate_items),
1270            Err(QuestionnaireSchemaError::DuplicateItem("same".into()))
1271        );
1272
1273        let invalid_default = vec![
1274            QuestionnaireItemDefinition::new("single", "Single").with_choices([
1275                QuestionnaireChoiceDefinition::new("a", "A").with_default_selected(true),
1276                QuestionnaireChoiceDefinition::new("b", "B").with_default_selected(true),
1277            ]),
1278        ];
1279        assert_eq!(
1280            QuestionnaireState::validate_schema(&invalid_default),
1281            Err(QuestionnaireSchemaError::MultipleDefaultsForSingleItem(
1282                "single".into()
1283            ))
1284        );
1285
1286        let duplicate_choice = vec![
1287            QuestionnaireItemDefinition::new("item", "Item").with_choices([
1288                QuestionnaireChoiceDefinition::new("same", "One"),
1289                QuestionnaireChoiceDefinition::new("same", "Two"),
1290            ]),
1291        ];
1292        assert_eq!(
1293            QuestionnaireState::validate_schema(&duplicate_choice),
1294            Err(QuestionnaireSchemaError::DuplicateChoice {
1295                item: "item".into(),
1296                choice: "same".into(),
1297            })
1298        );
1299    }
1300
1301    #[gpui::test]
1302    fn validates_navigates_skips_and_emits_completion_before_submit(cx: &mut TestAppContext) {
1303        let (harness, state, _, _, cx) = harness(cx);
1304
1305        assert_eq!(cx.read(|cx| state.read(cx).progress().current()), 1);
1306        assert_eq!(cx.read(|cx| state.read(cx).progress().total()), 2);
1307        assert_eq!(
1308            cx.read(|cx| state.read(cx).item_state("first").unwrap().status()),
1309            QuestionnaireItemStatus::Unanswered
1310        );
1311        assert!(!cx.update(|window, cx| state.update(cx, |state, cx| state.submit(window, cx))));
1312        assert!(cx.read(|cx| state.read(cx).error("first").is_some()));
1313        assert_eq!(
1314            cx.read(|cx| state.read(cx).error("second").unwrap().clone()),
1315            QuestionnaireValidationError::Message("Use the valid answer".into())
1316        );
1317
1318        cx.update(|window, cx| {
1319            state.update(cx, |state, cx| {
1320                state.set_input_value("first", "draft", window, cx).unwrap();
1321            });
1322        });
1323        assert!(cx.read(|cx| state.read(cx).error("first").is_none()));
1324        cx.update(|window, cx| {
1325            state.update(cx, |state, cx| {
1326                state.set_input_value("first", "", window, cx).unwrap();
1327            });
1328        });
1329        assert!(
1330            cx.read(|cx| state.read(cx).error("first").is_some()),
1331            "once validation has been attempted, clearing an answer updates the error live"
1332        );
1333
1334        state.update(cx, |state, cx| {
1335            state.activate_choice("first", "a", cx).unwrap()
1336        });
1337        assert!(
1338            cx.update(|window, cx| { state.update(cx, |state, cx| state.go_next(window, cx)) })
1339        );
1340        assert_eq!(
1341            cx.read(|cx| state.read(cx).current_item().unwrap().clone()),
1342            "second"
1343        );
1344        assert!(
1345            cx.update(|window, cx| {
1346                state.update(cx, |state, cx| state.skip_current(window, cx))
1347            })
1348        );
1349
1350        assert!(cx.read(|cx| state.read(cx).is_complete()));
1351        assert_eq!(
1352            cx.read(|cx| state.read(cx).item_state("second").unwrap().status()),
1353            QuestionnaireItemStatus::Skipped
1354        );
1355        assert_eq!(
1356            cx.read(|cx| harness.read(cx).events.clone()),
1357            vec!["answer", "current", "answer", "completed", "submit"]
1358        );
1359    }
1360
1361    #[gpui::test]
1362    fn keeps_input_draft_separate_and_synchronizes_silent_setters_and_reset(
1363        cx: &mut TestAppContext,
1364    ) {
1365        let (harness, state, first_input, second_input, cx) = harness(cx);
1366        let initial_events = cx.read(|cx| harness.read(cx).events.len());
1367
1368        cx.update(|window, cx| {
1369            state.update(cx, |state, cx| {
1370                state
1371                    .set_answer(
1372                        "first",
1373                        QuestionnaireAnswer::new().with_freeform("  custom  "),
1374                        window,
1375                        cx,
1376                    )
1377                    .unwrap();
1378            });
1379        });
1380        assert_eq!(
1381            cx.read(|cx| harness.read(cx).events.len()),
1382            initial_events,
1383            "programmatic setters are silent"
1384        );
1385        assert_eq!(cx.read(|cx| first_input.read(cx).value()), "  custom  ");
1386        assert_eq!(
1387            cx.read(|cx| {
1388                state
1389                    .read(cx)
1390                    .answer("first")
1391                    .unwrap()
1392                    .freeform()
1393                    .unwrap()
1394                    .clone()
1395            }),
1396            "  custom  "
1397        );
1398
1399        cx.update(|window, cx| {
1400            state.update(cx, |state, cx| {
1401                state
1402                    .set_answer(
1403                        "first",
1404                        QuestionnaireAnswer::new().with_choices(["a"]),
1405                        window,
1406                        cx,
1407                    )
1408                    .unwrap();
1409            });
1410        });
1411        assert_eq!(cx.read(|cx| first_input.read(cx).value()), "  custom  ");
1412        assert!(cx.read(|cx| { state.read(cx).answer("first").unwrap().freeform().is_none() }));
1413        assert_eq!(
1414            cx.read(|cx| state.read(cx).answer("first").unwrap().choices()[0].clone()),
1415            "a"
1416        );
1417
1418        cx.update(|window, cx| {
1419            first_input.update(cx, |input, cx| input.replace_all("", window, cx));
1420        });
1421        cx.run_until_parked();
1422        assert_eq!(
1423            cx.read(|cx| harness.read(cx).events.len()),
1424            initial_events,
1425            "editing an unselected draft does not change the semantic answer"
1426        );
1427        assert_eq!(
1428            cx.read(|cx| state.read(cx).answer("first").unwrap().choices()[0].clone()),
1429            "a"
1430        );
1431
1432        state.update(cx, |state, cx| {
1433            state.activate_choice("first", "b", cx).unwrap()
1434        });
1435
1436        cx.update(|window, cx| {
1437            state.update(cx, |state, cx| state.reset(window, cx));
1438        });
1439        assert_eq!(cx.read(|cx| first_input.read(cx).value()), "");
1440        assert_eq!(cx.read(|cx| second_input.read(cx).value()), "initial draft");
1441        assert!(cx.read(|cx| state.read(cx).answer("first").unwrap().is_empty()));
1442        assert_eq!(
1443            cx.read(|cx| {
1444                state
1445                    .read(cx)
1446                    .answer("second")
1447                    .unwrap()
1448                    .freeform()
1449                    .unwrap()
1450                    .clone()
1451            }),
1452            "initial draft"
1453        );
1454        assert_eq!(
1455            cx.read(|cx| harness.read(cx).events.len()),
1456            initial_events + 1
1457        );
1458    }
1459
1460    #[gpui::test]
1461    fn validates_all_items_and_returns_to_the_first_invalid_item(cx: &mut TestAppContext) {
1462        let (_, state, _, _, cx) = harness(cx);
1463        state.update(cx, |state, cx| {
1464            state.activate_choice("first", "a", cx).unwrap()
1465        });
1466
1467        assert!(
1468            !cx.update(|window, cx| { state.update(cx, |state, cx| state.submit(window, cx)) })
1469        );
1470        assert_eq!(
1471            cx.read(|cx| state.read(cx).current_item().unwrap().clone()),
1472            "second"
1473        );
1474        assert_eq!(
1475            cx.read(|cx| state.read(cx).error("second").unwrap().clone()),
1476            QuestionnaireValidationError::Message("Use the valid answer".into())
1477        );
1478
1479        state.update(cx, |state, cx| {
1480            state
1481                .set_external_error("first", "Server rejected it", cx)
1482                .unwrap()
1483        });
1484        assert!(
1485            !cx.update(|window, cx| { state.update(cx, |state, cx| state.submit(window, cx)) })
1486        );
1487        assert_eq!(
1488            cx.read(|cx| state.read(cx).current_item().unwrap().clone()),
1489            "first"
1490        );
1491
1492        cx.update(|window, cx| {
1493            state.update(cx, |state, cx| {
1494                state.clear_external_error("first", cx).unwrap();
1495                state
1496                    .set_input_value("second", "valid", window, cx)
1497                    .unwrap();
1498            });
1499        });
1500        assert!(cx.update(|window, cx| { state.update(cx, |state, cx| state.submit(window, cx)) }));
1501    }
1502
1503    #[gpui::test]
1504    fn preserves_schema_order_and_temporarily_excludes_disabled_answers(cx: &mut TestAppContext) {
1505        let (_, state, _, _, cx) = harness(cx);
1506
1507        cx.update(|window, cx| {
1508            state.update(cx, |state, cx| {
1509                state
1510                    .set_answer(
1511                        "second",
1512                        QuestionnaireAnswer::new()
1513                            .with_choices(["y", "x", "y"])
1514                            .with_freeform("valid"),
1515                        window,
1516                        cx,
1517                    )
1518                    .unwrap();
1519            });
1520        });
1521        assert_eq!(
1522            cx.read(|cx| state.read(cx).answer("second").unwrap().choices().to_vec()),
1523            vec![SharedString::from("x"), SharedString::from("y")]
1524        );
1525        cx.update(|window, cx| {
1526            state.update(cx, |state, cx| {
1527                state.set_current_item("second", window, cx).unwrap();
1528                state.focus_current_item(window, cx);
1529                assert!(state.focus_next_answer(window, cx));
1530            });
1531        });
1532        assert_eq!(
1533            cx.update(|window, cx| state.read(cx).focused_current_choice(window).cloned()),
1534            Some("x".into()),
1535            "filled multiple choices are focused in schema order"
1536        );
1537        assert_eq!(
1538            cx.read(|cx| state.read(cx).answer("second").unwrap().choices().to_vec()),
1539            vec![SharedString::from("x"), SharedString::from("y")],
1540            "focusing a filled choice does not toggle it"
1541        );
1542
1543        state.update(cx, |state, cx| {
1544            state.set_choice_disabled("second", "x", true, cx).unwrap()
1545        });
1546        assert_eq!(
1547            cx.read(|cx| state.read(cx).answer("second").unwrap().choices().to_vec()),
1548            vec![SharedString::from("y")]
1549        );
1550        state.update(cx, |state, cx| {
1551            state.set_choice_disabled("second", "x", false, cx).unwrap()
1552        });
1553        assert_eq!(
1554            cx.read(|cx| state.read(cx).answer("second").unwrap().choices().to_vec()),
1555            vec![SharedString::from("x"), SharedString::from("y")]
1556        );
1557
1558        let error = cx.update(|window, cx| {
1559            state.update(cx, |state, cx| {
1560                state.set_answer(
1561                    "second",
1562                    QuestionnaireAnswer::new().with_choices(["unknown"]),
1563                    window,
1564                    cx,
1565                )
1566            })
1567        });
1568        assert_eq!(
1569            error,
1570            Err(QuestionnaireSchemaError::UnknownChoice {
1571                item: "second".into(),
1572                choice: "unknown".into(),
1573            })
1574        );
1575    }
1576
1577    #[gpui::test]
1578    fn shortcuts_disabled_current_fallback_and_recompletion_are_deterministic(
1579        cx: &mut TestAppContext,
1580    ) {
1581        let (harness, state, first_input, _, cx) = harness(cx);
1582        cx.update(|window, cx| {
1583            state.update(cx, |state, cx| {
1584                state
1585                    .set_answer(
1586                        "first",
1587                        QuestionnaireAnswer::new().with_choices(["b"]),
1588                        window,
1589                        cx,
1590                    )
1591                    .unwrap();
1592                state.focus_current_item(window, cx);
1593                assert!(state.focus_next_answer(window, cx));
1594            });
1595        });
1596        assert_eq!(
1597            cx.update(|window, cx| state.read(cx).focused_current_choice(window).cloned()),
1598            Some("b".into()),
1599            "the first move from the item group focuses the existing answer"
1600        );
1601        assert_eq!(
1602            cx.read(|cx| state.read(cx).answer("first").unwrap().choices()[0].clone()),
1603            "b",
1604            "focusing the filled radio does not replace the answer"
1605        );
1606        cx.update(|window, cx| state.update(cx, |state, cx| state.reset(window, cx)));
1607
1608        cx.update(|window, cx| {
1609            state.update(cx, |state, cx| {
1610                state.focus_input("first", window, cx);
1611                assert!(!state.current_input_has_text(cx));
1612                assert!(state.focus_next_answer(window, cx));
1613            });
1614        });
1615        assert_eq!(
1616            cx.read(|cx| state.read(cx).answer("first").unwrap().choices()[0].clone()),
1617            "a",
1618            "an empty focused input may move to and activate a radio"
1619        );
1620        cx.update(|window, cx| state.update(cx, |state, cx| state.reset(window, cx)));
1621
1622        cx.update(|window, cx| {
1623            state.update(cx, |state, cx| {
1624                state.set_input_value("first", "draft", window, cx).unwrap();
1625                state.focus_input("first", window, cx);
1626                assert!(state.current_input_has_text(cx));
1627                assert!(!state.focus_next_answer(window, cx));
1628            });
1629        });
1630        assert!(cx.update(|window, cx| first_input.focus_handle(cx).is_focused(window)));
1631        cx.update(|window, cx| state.update(cx, |state, cx| state.reset(window, cx)));
1632
1633        assert!(cx.update(|window, cx| {
1634            state.update(cx, |state, cx| {
1635                state.focus_current_item(window, cx);
1636                state.focus_next_answer(window, cx)
1637            })
1638        }));
1639        assert_eq!(
1640            cx.read(|cx| state.read(cx).answer("first").unwrap().choices()[0].clone()),
1641            "a",
1642            "moving from the item group to a radio activates it"
1643        );
1644        cx.update(|window, cx| state.update(cx, |state, cx| state.reset(window, cx)));
1645
1646        assert_eq!(
1647            cx.read(|cx| state.read(cx).shortcut_for_choice("first", "a")),
1648            Some("A".into())
1649        );
1650        state.update(cx, |state, cx| {
1651            state.set_choice_disabled("first", "a", true, cx).unwrap()
1652        });
1653        assert_eq!(
1654            cx.read(|cx| state.read(cx).shortcut_for_choice("first", "b")),
1655            Some("A".into())
1656        );
1657        state.update(cx, |state, cx| {
1658            state.set_choice_disabled("first", "a", false, cx).unwrap()
1659        });
1660        cx.update(|window, cx| {
1661            state.update(cx, |state, cx| {
1662                assert!(state.activate_shortcut("a", window, cx));
1663                state.focus_choice("first", "a", window, cx);
1664                assert!(state.move_current_radio(1, window, cx));
1665                assert!(state.go_next(window, cx));
1666                assert!(state.skip_current(window, cx));
1667            });
1668        });
1669        assert!(cx.read(|cx| state.read(cx).is_complete()));
1670
1671        cx.update(|window, cx| {
1672            state.update(cx, |state, cx| {
1673                state
1674                    .set_input_value("second", "valid", window, cx)
1675                    .unwrap();
1676                state.activate_choice("second", "x", cx).unwrap();
1677            });
1678        });
1679        assert!(!cx.read(|cx| state.read(cx).is_complete()));
1680        assert!(cx.update(|window, cx| { state.update(cx, |state, cx| state.submit(window, cx)) }));
1681        let events = cx.read(|cx| harness.read(cx).events.clone());
1682        assert_eq!(
1683            events.iter().filter(|event| **event == "completed").count(),
1684            2
1685        );
1686
1687        let before_disable = events.len();
1688        cx.update(|window, cx| {
1689            state.update(cx, |state, cx| {
1690                state.set_item_disabled("second", true, window, cx).unwrap();
1691            });
1692        });
1693        assert_eq!(
1694            cx.read(|cx| state.read(cx).current_item().unwrap().clone()),
1695            "first"
1696        );
1697        assert_eq!(
1698            cx.read(|cx| harness.read(cx).events.len()),
1699            before_disable,
1700            "programmatic disable and fallback are silent"
1701        );
1702    }
1703}