Skip to main content

gpui_base/questionnaire/
control.rs

1use gpui::{
2    ElementId, Entity, InteractiveElement as _, SharedString, StatefulInteractiveElement as _,
3    prelude::FluentBuilder as _,
4};
5
6use super::QuestionnaireState;
7use crate::{Checkbox, CheckboxState, Radio};
8
9/// The answer control for one choice, wired to the questionnaire's behavior.
10///
11/// A multiple-answer item hands back a `Checkbox` and a single-answer item a
12/// `Radio`, each already carrying its checked state, disabled state,
13/// accessibility name, position in set, focus handle, confirm key and change
14/// handler. The skin decides what the control looks like and what it contains.
15// The value is handed straight to the caller's `match` and dropped into an
16// element; boxing either variant would buy an allocation on every choice to
17// even out a difference that never outlives one render.
18#[allow(clippy::large_enum_variant)]
19pub enum QuestionnaireChoiceControl {
20    Checkbox(Checkbox),
21    Radio(Radio),
22}
23
24impl QuestionnaireChoiceControl {
25    /// Returns `None` when the item or the choice is not part of the schema.
26    pub fn new(
27        state: &Entity<QuestionnaireState>,
28        item: impl Into<SharedString>,
29        value: impl Into<SharedString>,
30        id: impl Into<ElementId>,
31        cx: &gpui::App,
32    ) -> Option<Self> {
33        let item = item.into();
34        let value = value.into();
35        let snapshot = state.read(cx);
36        let choice = snapshot.choice_state(&item, &value)?;
37        let multiple = snapshot.item_state(&item)?.is_multiple();
38        let definition = snapshot.choice_definition(&item, &value)?;
39        let label = definition.accessibility_label().clone();
40        let description = definition.description().cloned();
41        let position = snapshot.choice_position(&item, &value);
42        let focus_handle = snapshot.choice_focus_handle(&item, &value).cloned();
43        let selected = choice.is_selected();
44        let disabled = choice.is_disabled();
45
46        // Enter confirms an answer that is already selected; an unselected
47        // control keeps Enter for activation.
48        let confirm_state = state.clone();
49        let confirm =
50            move |event: &gpui::KeyDownEvent, window: &mut gpui::Window, cx: &mut gpui::App| {
51                if selected
52                    && !window.default_prevented()
53                    && !event.is_held
54                    && event.keystroke.key == "enter"
55                    && event.keystroke.modifiers.number_of_modifiers() == 0
56                    && confirm_state.update(cx, |state, cx| state.confirm_current(window, cx))
57                {
58                    window.prevent_default();
59                }
60            };
61
62        let id = id.into();
63        Some(if multiple {
64            let change_state = state.clone();
65            let change_item = item.clone();
66            let change_value = value.clone();
67            Self::Checkbox(
68                Checkbox::new(id)
69                    .state(if selected {
70                        CheckboxState::Checked
71                    } else {
72                        CheckboxState::Unchecked
73                    })
74                    .disabled(disabled)
75                    .accessibility_label(label)
76                    .when_some(description, |this, description| {
77                        this.aria_description(description)
78                    })
79                    .when_some(position, |this, (position, total)| {
80                        this.aria_position_in_set(position).aria_size_of_set(total)
81                    })
82                    .when_some(focus_handle, |this, focus_handle| {
83                        this.track_focus(&focus_handle)
84                    })
85                    .capture_key_down(confirm)
86                    .on_change(move |_, _, window, cx| {
87                        let _ = change_state.update(cx, |state, cx| {
88                            let result = state.activate_choice(&change_item, &change_value, cx);
89                            state.focus_choice(&change_item, &change_value, window, cx);
90                            result
91                        });
92                    }),
93            )
94        } else {
95            let change_state = state.clone();
96            Self::Radio(
97                Radio::new(id)
98                    .checked(selected)
99                    .disabled(disabled)
100                    .accessibility_label(label)
101                    .when_some(description, |this, description| {
102                        this.aria_description(description)
103                    })
104                    .when_some(position, |this, (position, total)| {
105                        this.set_position(position, total)
106                    })
107                    .when_some(focus_handle, |this, focus_handle| {
108                        this.track_focus(&focus_handle)
109                    })
110                    .capture_key_down(confirm)
111                    .on_change(move |_, _, window, cx| {
112                        let _ = change_state.update(cx, |state, cx| {
113                            let result = state.activate_choice(&item, &value, cx);
114                            state.focus_choice(&item, &value, window, cx);
115                            result
116                        });
117                    }),
118            )
119        })
120    }
121}