Files
Ely-GPUI-Components/src/forms/input.rs
T

193 lines
5.7 KiB
Rust

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);
})
}),
)
}
}