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. Activation goes through [`QuestionnaireState::choose`], so a
15/// `Radio` confirms its item once chosen. The skin decides what the control
16/// looks like and what it contains.
17// The value is handed straight to the caller's `match` and dropped into an
18// element; boxing either variant would buy an allocation on every choice to
19// even out a difference that never outlives one render.
20#[allow(clippy::large_enum_variant)]
21pub enum QuestionnaireChoiceControl {
22    Checkbox(Checkbox),
23    Radio(Radio),
24}
25
26impl QuestionnaireChoiceControl {
27    /// Returns `None` when the item or the choice is not part of the schema.
28    pub fn new(
29        state: &Entity<QuestionnaireState>,
30        item: impl Into<SharedString>,
31        value: impl Into<SharedString>,
32        id: impl Into<ElementId>,
33        cx: &gpui::App,
34    ) -> Option<Self> {
35        let item = item.into();
36        let value = value.into();
37        let snapshot = state.read(cx);
38        let choice = snapshot.choice_state(&item, &value)?;
39        let multiple = snapshot.item_state(&item)?.is_multiple();
40        let definition = snapshot.choice_definition(&item, &value)?;
41        let label = definition.accessibility_label().clone();
42        let description = definition.description().cloned();
43        let position = snapshot.choice_position(&item, &value);
44        let focus_handle = snapshot.choice_focus_handle(&item, &value).cloned();
45        let selected = choice.is_selected();
46        let disabled = choice.is_disabled();
47
48        // Enter confirms an answer that is already selected; an unselected
49        // control keeps Enter for activation.
50        let confirm_state = state.clone();
51        let confirm =
52            move |event: &gpui::KeyDownEvent, window: &mut gpui::Window, cx: &mut gpui::App| {
53                if selected
54                    && !window.default_prevented()
55                    && !event.is_held
56                    && event.keystroke.key == "enter"
57                    && event.keystroke.modifiers.number_of_modifiers() == 0
58                    && confirm_state.update(cx, |state, cx| state.confirm_current(window, cx))
59                {
60                    window.prevent_default();
61                }
62            };
63
64        let id = id.into();
65        Some(if multiple {
66            let change_state = state.clone();
67            let change_item = item.clone();
68            let change_value = value.clone();
69            Self::Checkbox(
70                Checkbox::new(id)
71                    .state(if selected {
72                        CheckboxState::Checked
73                    } else {
74                        CheckboxState::Unchecked
75                    })
76                    .disabled(disabled)
77                    .accessibility_label(label)
78                    .when_some(description, |this, description| {
79                        this.aria_description(description)
80                    })
81                    .when_some(position, |this, (position, total)| {
82                        this.aria_position_in_set(position).aria_size_of_set(total)
83                    })
84                    .when_some(focus_handle, |this, focus_handle| {
85                        this.track_focus(&focus_handle)
86                    })
87                    .capture_key_down(confirm)
88                    .on_change(move |_, _, window, cx| {
89                        let _ = change_state.update(cx, |state, cx| {
90                            state.choose(&change_item, &change_value, window, cx)
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` would leave a checked radio inert, but
112                    // choosing the selected answer again still confirms it.
113                    .when(!disabled, |this| {
114                        this.on_click(move |_, window, cx| {
115                            let _ = change_state
116                                .update(cx, |state, cx| state.choose(&item, &value, window, cx));
117                        })
118                    }),
119            )
120        })
121    }
122}