Skip to main content

gpui_base/input/base/
mod.rs

1use crate::{StateStyle, StyledExt as _, TestSupportExt as _};
2use gpui::{
3    AnyElement, App, Div, ElementId, InteractiveElement, Interactivity, IntoElement, ParentElement,
4    Refineable as _, RenderOnce, Role, SharedString, StatefulInteractiveElement, StyleRefinement,
5    Styled, Window, div, prelude::FluentBuilder as _,
6};
7
8/// What the input can offer to its context menu, at the moment it is opened.
9///
10/// Built by the input and read by the menu, the fields are private and reached
11/// through the methods below, so that a new capability can be added without
12/// breaking the menu builders.
13///
14/// ```
15/// use gpui_base::input::InputContextMenuCapabilities;
16///
17/// let capabilities = InputContextMenuCapabilities::new()
18///     .code_editor(true)
19///     .selection(true);
20///
21/// assert!(capabilities.is_editable());
22/// assert!(capabilities.has_selection());
23/// ```
24#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
25pub struct InputContextMenuCapabilities {
26    disabled: bool,
27    readonly: bool,
28    code_editor: bool,
29    selection: bool,
30    masked: bool,
31    go_to_definition: bool,
32    code_actions: bool,
33}
34
35impl InputContextMenuCapabilities {
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    pub fn disabled(mut self, disabled: bool) -> Self {
41        self.disabled = disabled;
42        self
43    }
44
45    pub fn readonly(mut self, readonly: bool) -> Self {
46        self.readonly = readonly;
47        self
48    }
49
50    pub fn code_editor(mut self, code_editor: bool) -> Self {
51        self.code_editor = code_editor;
52        self
53    }
54
55    /// Set whether the input has a non-empty selection.
56    pub fn selection(mut self, selection: bool) -> Self {
57        self.selection = selection;
58        self
59    }
60
61    /// Set whether the input renders its value masked.
62    pub fn masked(mut self, masked: bool) -> Self {
63        self.masked = masked;
64        self
65    }
66
67    pub fn go_to_definition(mut self, go_to_definition: bool) -> Self {
68        self.go_to_definition = go_to_definition;
69        self
70    }
71
72    pub fn code_actions(mut self, code_actions: bool) -> Self {
73        self.code_actions = code_actions;
74        self
75    }
76
77    pub fn is_disabled(&self) -> bool {
78        self.disabled
79    }
80
81    pub fn is_readonly(&self) -> bool {
82        self.readonly
83    }
84
85    /// Returns true if the user is allowed to change the text.
86    ///
87    /// The items that write to the input (Cut, Paste, Code Actions) belong to
88    /// this, the reading ones (Copy, Go to Definition) do not.
89    pub fn is_editable(&self) -> bool {
90        !self.disabled && !self.readonly
91    }
92
93    pub fn is_code_editor(&self) -> bool {
94        self.code_editor
95    }
96
97    pub fn has_selection(&self) -> bool {
98        self.selection
99    }
100
101    pub fn is_masked(&self) -> bool {
102        self.masked
103    }
104
105    /// Returns true if the user is allowed to copy the text out.
106    ///
107    /// A masked input keeps its value out of the clipboard, so Copy and Cut
108    /// are both unavailable while the value is hidden.
109    pub fn is_copyable(&self) -> bool {
110        self.selection && !self.masked
111    }
112
113    pub fn has_definition(&self) -> bool {
114        self.go_to_definition
115    }
116
117    pub fn has_code_actions(&self) -> bool {
118        self.code_actions
119    }
120}
121
122/// The foundational input frame.
123///
124/// It intentionally owns only input semantics, interaction forwarding, and
125/// normal children. Applications remain responsible for all presentation.
126#[derive(IntoElement)]
127pub struct InputBase {
128    base: crate::ObservedElement<gpui::Stateful<Div>>,
129    style: StyleRefinement,
130    semantic_styles: InputStyles,
131    children: Vec<AnyElement>,
132    focused: bool,
133    disabled: bool,
134    role: crate::RoleOverride,
135}
136
137impl InputBase {
138    pub fn new(id: impl Into<ElementId>) -> Self {
139        Self {
140            base: div().id(id).test_support(),
141            style: StyleRefinement::default(),
142            semantic_styles: InputStyles::default(),
143            children: Vec::new(),
144            focused: false,
145            disabled: false,
146            role: crate::RoleOverride::Implicit,
147        }
148    }
149    pub fn role(mut self, role: impl Into<crate::RoleOverride>) -> Self {
150        self.role = role.into();
151        self
152    }
153
154    pub fn focused(mut self, focused: bool) -> Self {
155        self.focused = focused;
156        self
157    }
158
159    pub fn disabled(mut self, disabled: bool) -> Self {
160        self.disabled = disabled;
161        self
162    }
163
164    /// Exposes an editor's native semantics through this frame's component ID.
165    ///
166    /// `editor` must identify the input, textarea or editor state rendered below
167    /// this frame, without intervening element IDs. Anonymous layout wrappers
168    /// are allowed. ID queries retain the frame's path, bounds and visibility.
169    /// Role, label, value, checked/indeterminate, selected, expanded, disabled
170    /// and focus facts come from the editor's existing native node and observed
171    /// focus scope, not independently focusable addon controls. Native snapshot
172    /// enumeration is unchanged, so role/label queries do not duplicate editors.
173    ///
174    /// This only declares an observation relationship with `test-support`; it
175    /// never adds a native accessibility node or registers a focus handle. The
176    /// relationship follows the frame's GPUI element-state lifetime.
177    pub fn semantic_owner(self, editor: gpui::EntityId) -> Self {
178        #[cfg(feature = "test-support")]
179        {
180            Self {
181                base: self.base.semantic_owner([
182                    ElementId::View(editor),
183                    super::state::EDITOR_ELEMENT_ID.into(),
184                ]),
185                ..self
186            }
187        }
188        #[cfg(not(feature = "test-support"))]
189        {
190            let _ = editor;
191            self
192        }
193    }
194
195    pub fn styles(mut self, build: impl FnOnce(InputStyles) -> InputStyles) -> Self {
196        self.semantic_styles = build(self.semantic_styles);
197        self
198    }
199
200    pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
201        self.base = self.base.aria_label(label.into());
202        self
203    }
204
205    fn resolved_style(&self) -> StyleRefinement {
206        crate::state_style::resolve_style(
207            &self.style,
208            [
209                self.focused.then_some(&self.semantic_styles.focused),
210                self.disabled.then_some(&self.semantic_styles.disabled),
211            ]
212            .into_iter()
213            .flatten(),
214        )
215    }
216}
217
218#[derive(Default)]
219pub struct InputStyles {
220    focused: StyleRefinement,
221    disabled: StyleRefinement,
222}
223
224impl InputStyles {
225    pub fn focused(mut self, build: impl FnOnce(StateStyle) -> StateStyle) -> Self {
226        self.focused
227            .refine(&build(StateStyle::default()).into_refinement());
228        self
229    }
230
231    pub fn disabled(mut self, build: impl FnOnce(StateStyle) -> StateStyle) -> Self {
232        self.disabled
233            .refine(&build(StateStyle::default()).into_refinement());
234        self
235    }
236}
237
238impl Styled for InputBase {
239    fn style(&mut self) -> &mut StyleRefinement {
240        &mut self.style
241    }
242}
243
244impl ParentElement for InputBase {
245    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
246        self.children.extend(elements);
247    }
248}
249
250impl InteractiveElement for InputBase {
251    fn track_focus(mut self, focus: &gpui::FocusHandle) -> Self {
252        self.base = self.base.track_focus(focus);
253        self
254    }
255
256    fn interactivity(&mut self) -> &mut Interactivity {
257        self.base.interactivity()
258    }
259}
260
261impl StatefulInteractiveElement for InputBase {}
262
263impl RenderOnce for InputBase {
264    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
265        let style = self.resolved_style();
266        self.base
267            .when_some(self.role.resolve(|| Role::TextInput), |this, role| {
268                this.role(role)
269            })
270            .children(self.children)
271            .refine_style(&style)
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn frame_accepts_application_owned_content_and_style() {
281        let _ = InputBase::new("input")
282            .focused(true)
283            .disabled(false)
284            .styles(|styles| {
285                styles
286                    .focused(|style| style.border_1())
287                    .disabled(|style| style.opacity(0.5))
288            })
289            .child("value")
290            .opacity(0.8);
291    }
292
293    #[test]
294    fn semantic_state_styles_override_the_normal_style() {
295        let focused = InputBase::new("focused")
296            .focused(true)
297            .border_color(gpui::red())
298            .styles(|styles| styles.focused(|style| style.border_color(gpui::blue())));
299        assert_eq!(focused.resolved_style().border_color, Some(gpui::blue()));
300
301        let disabled = InputBase::new("disabled")
302            .focused(true)
303            .disabled(true)
304            .opacity(1.)
305            .styles(|styles| {
306                styles
307                    .focused(|style| style.opacity(0.8))
308                    .disabled(|style| style.opacity(0.5))
309            });
310        assert_eq!(disabled.resolved_style().opacity, Some(0.5));
311    }
312}