Skip to main content

gpui_base/input/base/
mod.rs

1use crate::{StateStyle, StyledExt 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: 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),
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    pub fn styles(mut self, build: impl FnOnce(InputStyles) -> InputStyles) -> Self {
165        self.semantic_styles = build(self.semantic_styles);
166        self
167    }
168
169    pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
170        self.base = self.base.aria_label(label.into());
171        self
172    }
173
174    fn resolved_style(&self) -> StyleRefinement {
175        crate::state_style::resolve_style(
176            &self.style,
177            [
178                self.focused.then_some(&self.semantic_styles.focused),
179                self.disabled.then_some(&self.semantic_styles.disabled),
180            ]
181            .into_iter()
182            .flatten(),
183        )
184    }
185}
186
187#[derive(Default)]
188pub struct InputStyles {
189    focused: StyleRefinement,
190    disabled: StyleRefinement,
191}
192
193impl InputStyles {
194    pub fn focused(mut self, build: impl FnOnce(StateStyle) -> StateStyle) -> Self {
195        self.focused
196            .refine(&build(StateStyle::default()).into_refinement());
197        self
198    }
199
200    pub fn disabled(mut self, build: impl FnOnce(StateStyle) -> StateStyle) -> Self {
201        self.disabled
202            .refine(&build(StateStyle::default()).into_refinement());
203        self
204    }
205}
206
207impl Styled for InputBase {
208    fn style(&mut self) -> &mut StyleRefinement {
209        &mut self.style
210    }
211}
212
213impl ParentElement for InputBase {
214    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
215        self.children.extend(elements);
216    }
217}
218
219impl InteractiveElement for InputBase {
220    fn interactivity(&mut self) -> &mut Interactivity {
221        self.base.interactivity()
222    }
223}
224
225impl StatefulInteractiveElement for InputBase {}
226
227impl RenderOnce for InputBase {
228    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
229        let style = self.resolved_style();
230        self.base
231            .when_some(self.role.resolve(|| Role::TextInput), |this, role| {
232                this.role(role)
233            })
234            .children(self.children)
235            .refine_style(&style)
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn frame_accepts_application_owned_content_and_style() {
245        let _ = InputBase::new("input")
246            .focused(true)
247            .disabled(false)
248            .styles(|styles| {
249                styles
250                    .focused(|style| style.border_1())
251                    .disabled(|style| style.opacity(0.5))
252            })
253            .child("value")
254            .opacity(0.8);
255    }
256
257    #[test]
258    fn semantic_state_styles_override_the_normal_style() {
259        let focused = InputBase::new("focused")
260            .focused(true)
261            .border_color(gpui::red())
262            .styles(|styles| styles.focused(|style| style.border_color(gpui::blue())));
263        assert_eq!(focused.resolved_style().border_color, Some(gpui::blue()));
264
265        let disabled = InputBase::new("disabled")
266            .focused(true)
267            .disabled(true)
268            .opacity(1.)
269            .styles(|styles| {
270                styles
271                    .focused(|style| style.opacity(0.8))
272                    .disabled(|style| style.opacity(0.5))
273            });
274        assert_eq!(disabled.resolved_style().opacity, Some(0.5));
275    }
276}