Skip to main content

gpui_kit/controls/
field.rs

1//! Field chrome.
2//!
3//! One frame carries the surface, focus and invalid treatment every editable
4//! control wears. The editable surface itself arrives with `TextInput`.
5
6use gpui::{Styled, div, prelude::FluentBuilder, px};
7use gpui_kit_theme::{ControlSize, Radius, Space, Theme};
8
9use crate::foundation::StyledExt;
10
11/// What an editable surface currently reports about itself.
12///
13/// The chrome is drawn from this alone, so every field in the library says
14/// the same thing the same way.
15#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
16pub struct FieldState {
17    pub focused: bool,
18    pub invalid: bool,
19    pub disabled: bool,
20}
21
22impl FieldState {
23    pub fn focused(mut self, focused: bool) -> Self {
24        self.focused = focused;
25        self
26    }
27
28    pub fn invalid(mut self, invalid: bool) -> Self {
29        self.invalid = invalid;
30        self
31    }
32
33    pub fn disabled(mut self, disabled: bool) -> Self {
34        self.disabled = disabled;
35        self
36    }
37}
38
39/// The surface, focus and invalid treatment every editable control wears.
40///
41/// `TextInput` renders inside it, and the composed fields — `NumberInput`,
42/// `Combobox`, `TagInput` — wrap a bare input in one of these so a composed
43/// control is not two nested frames.
44pub fn field_shell(theme: &Theme, size: ControlSize, state: FieldState) -> gpui::Div {
45    let metrics = theme.control.get(size);
46    div()
47        .w_full()
48        .flex()
49        .flex_row()
50        .items_center()
51        .gap(px(theme.space(Space::Sm)))
52        .min_h(px(metrics.height))
53        .px(px(metrics.padding_x))
54        .radius(theme, Radius::Control)
55        .well(theme)
56        // Invalidity is the one thing a field says with a line, because it is
57        // the one thing no amount of surface colour can say: a well that is
58        // wrong looks exactly like a well that is right. Focus stays a ring,
59        // which is the same ring every other focusable thing in the library
60        // wears and costs the layout nothing.
61        .when(state.invalid, |field| {
62            field.border_color(theme.colors.danger)
63        })
64        .when(state.focused, |field| field.shadow(theme.focus_ring()))
65        .when(state.disabled, |field| {
66            field.opacity(theme.opacity.disabled)
67        })
68        .text_size(px(metrics.font_size))
69        .text_color(if state.disabled {
70            theme.colors.text_faint
71        } else {
72            theme.colors.text
73        })
74}