T06a forms text: text input core, fields, numbers, masks, pin, inline, groups, tags, pairs, path, regex, expression, hotkey, mention
This commit is contained in:
41 files changed
+6233
-45
No files matched your search
@@ -0,0 +1,192 @@
|
||||
use gpui::{
|
||||
AnyElement, App, Entity, InteractiveElement, IntoElement, MouseButton, ParentElement,
|
||||
RenderOnce, Styled, Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::TextInput;
|
||||
use crate::{
|
||||
buttons::IconButton,
|
||||
primitives::{Icon, IconName},
|
||||
theme::{ActiveTheme, ControlSize, IconSize, Radius, TextSize},
|
||||
};
|
||||
|
||||
/// A text field's frame: border, focus, and room for things before and after.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Input {
|
||||
state: Entity<TextInput>,
|
||||
size: ControlSize,
|
||||
prefix: Option<AnyElement>,
|
||||
suffix: Option<AnyElement>,
|
||||
clearable: bool,
|
||||
invalid: bool,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
pub fn new(state: &Entity<TextInput>) -> Self {
|
||||
Self {
|
||||
state: state.clone(),
|
||||
size: ControlSize::default(),
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
clearable: false,
|
||||
invalid: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: ControlSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
||||
self.prefix = Some(prefix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
||||
self.suffix = Some(suffix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a clear button while there is text.
|
||||
pub fn clearable(mut self) -> Self {
|
||||
self.clearable = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Draws the frame in the danger tone.
|
||||
pub fn invalid(mut self, invalid: bool) -> Self {
|
||||
self.invalid = invalid;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn text_size(size: ControlSize) -> TextSize {
|
||||
match size {
|
||||
ControlSize::Sm => TextSize::Sm,
|
||||
ControlSize::Md => TextSize::Base,
|
||||
ControlSize::Lg => TextSize::Md,
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Input {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let input = self.state.read(cx);
|
||||
let focus = input.focus().clone();
|
||||
let (focused, empty, multi, disabled) = (
|
||||
focus.is_focused(window),
|
||||
input.is_empty(),
|
||||
input.is_multi_line(),
|
||||
input.is_disabled(),
|
||||
);
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let border = if self.invalid {
|
||||
colors.danger
|
||||
} else if focused {
|
||||
colors.focus
|
||||
} else {
|
||||
colors.border_strong
|
||||
};
|
||||
let (state, clear_id) = (self.state.clone(), self.state.entity_id());
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.px(theme.control_padding(self.size))
|
||||
.map(|frame| {
|
||||
if multi {
|
||||
frame.items_start().py_1p5()
|
||||
} else {
|
||||
frame.items_center().h(theme.control_height(self.size))
|
||||
}
|
||||
})
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.border_1()
|
||||
.border_color(border)
|
||||
.bg(if disabled {
|
||||
colors.sunken
|
||||
} else {
|
||||
colors.surface
|
||||
})
|
||||
.text_size(theme.text_size(text_size(self.size)))
|
||||
.text_color(if disabled {
|
||||
colors.fg_disabled
|
||||
} else {
|
||||
colors.fg
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, move |_, window, _| window.focus(&focus))
|
||||
.children(self.prefix)
|
||||
.child(div().flex_1().min_w_0().child(self.state))
|
||||
.when(self.clearable && !empty && !disabled, |frame| {
|
||||
frame.child(
|
||||
IconButton::new(("input-clear", clear_id), IconName::CircleX)
|
||||
.size(ControlSize::Sm)
|
||||
.tooltip("Clear")
|
||||
.on_click(move |_, window, cx| {
|
||||
state.update(cx, |input, cx| input.set_text("", cx));
|
||||
window.focus(&state.read(cx).focus().clone());
|
||||
}),
|
||||
)
|
||||
})
|
||||
.children(self.suffix)
|
||||
}
|
||||
}
|
||||
|
||||
/// A masked field with a lock before it and an eye that shows the text.
|
||||
#[derive(IntoElement)]
|
||||
pub struct PasswordInput {
|
||||
state: Entity<TextInput>,
|
||||
size: ControlSize,
|
||||
}
|
||||
|
||||
impl PasswordInput {
|
||||
/// Build the state with `TextInput::masked`.
|
||||
pub fn new(state: &Entity<TextInput>) -> Self {
|
||||
Self {
|
||||
state: state.clone(),
|
||||
size: ControlSize::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: ControlSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for PasswordInput {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let masked = self.state.read(cx).is_masked();
|
||||
let (toggle, subtle) = (self.state.clone(), cx.theme().colors.fg_subtle);
|
||||
Input::new(&self.state)
|
||||
.size(self.size)
|
||||
.prefix(Icon::new(IconName::Lock).size(IconSize::Sm).color(subtle))
|
||||
.suffix(
|
||||
IconButton::new(
|
||||
("password-eye", self.state.entity_id()),
|
||||
if masked {
|
||||
IconName::Eye
|
||||
} else {
|
||||
IconName::EyeOff
|
||||
},
|
||||
)
|
||||
.size(ControlSize::Sm)
|
||||
.tooltip(if masked {
|
||||
"Show password"
|
||||
} else {
|
||||
"Hide password"
|
||||
})
|
||||
.on_click(move |_, _, cx| {
|
||||
toggle.update(cx, |input, cx| {
|
||||
let masked = input.is_masked();
|
||||
log::info!(
|
||||
"password input: {}",
|
||||
if masked { "shown" } else { "hidden" }
|
||||
);
|
||||
input.set_masked(!masked, cx);
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user