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, size: ControlSize, prefix: Option, suffix: Option, clearable: bool, invalid: bool, } impl Input { pub fn new(state: &Entity) -> 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, size: ControlSize, } impl PasswordInput { /// Build the state with `TextInput::masked`. pub fn new(state: &Entity) -> 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); }) }), ) } }