378 lines
14 KiB
Rust
378 lines
14 KiB
Rust
use gpui::prelude::FluentBuilder as _;
|
|
use gpui::{
|
|
AnyElement, App, DefiniteLength, Edges, EdgesRefinement, Entity, Hsla, InteractiveElement as _,
|
|
IntoElement, MouseButton, ParentElement as _, Rems, RenderOnce, StyleRefinement, Styled,
|
|
TextAlign, Window, div, px, relative,
|
|
};
|
|
use theme::ActiveTheme;
|
|
|
|
use super::InputState;
|
|
use super::element::EditorScrollbar;
|
|
use crate::button::{Button, ButtonVariants as _};
|
|
use crate::indicator::Indicator;
|
|
use crate::input::clear_button;
|
|
use crate::{IconName, Selectable, Sizable, Size, StyleSized, StyledExt, h_flex, v_flex};
|
|
|
|
/// Returns `(background, foreground)` colors for input-like components.
|
|
pub(crate) fn input_style(disabled: bool, cx: &App) -> (Hsla, Hsla) {
|
|
if disabled {
|
|
(cx.theme().surface_background, cx.theme().text_muted)
|
|
} else {
|
|
(cx.theme().elevated_surface_background, cx.theme().text)
|
|
}
|
|
}
|
|
|
|
/// A text input element bind to an [`InputState`].
|
|
#[derive(IntoElement)]
|
|
pub struct Input {
|
|
state: Entity<InputState>,
|
|
style: StyleRefinement,
|
|
size: Size,
|
|
prefix: Option<AnyElement>,
|
|
suffix: Option<AnyElement>,
|
|
height: Option<DefiniteLength>,
|
|
appearance: bool,
|
|
cleanable: bool,
|
|
mask_toggle: bool,
|
|
disabled: bool,
|
|
bordered: bool,
|
|
focus_bordered: bool,
|
|
tab_index: isize,
|
|
selected: bool,
|
|
}
|
|
|
|
impl Sizable for Input {
|
|
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
|
self.size = size.into();
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Selectable for Input {
|
|
fn selected(mut self, selected: bool) -> Self {
|
|
self.selected = selected;
|
|
self
|
|
}
|
|
|
|
fn is_selected(&self) -> bool {
|
|
self.selected
|
|
}
|
|
}
|
|
|
|
impl Input {
|
|
/// Create a new [`Input`] element bind to the [`InputState`].
|
|
pub fn new(state: &Entity<InputState>) -> Self {
|
|
Self {
|
|
state: state.clone(),
|
|
size: Size::default(),
|
|
style: StyleRefinement::default(),
|
|
prefix: None,
|
|
suffix: None,
|
|
height: None,
|
|
appearance: true,
|
|
cleanable: false,
|
|
mask_toggle: false,
|
|
disabled: false,
|
|
bordered: true,
|
|
focus_bordered: true,
|
|
tab_index: 0,
|
|
selected: false,
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
/// Set full height of the input (Multi-line only).
|
|
pub fn h_full(mut self) -> Self {
|
|
self.height = Some(relative(1.));
|
|
self
|
|
}
|
|
|
|
/// Set height of the input (Multi-line only).
|
|
pub fn h(mut self, height: impl Into<DefiniteLength>) -> Self {
|
|
self.height = Some(height.into());
|
|
self
|
|
}
|
|
|
|
/// Set the appearance of the input field, if false the input field will no border, background.
|
|
pub fn appearance(mut self, appearance: bool) -> Self {
|
|
self.appearance = appearance;
|
|
self
|
|
}
|
|
|
|
/// Set the bordered for the input, default: true
|
|
pub fn bordered(mut self, bordered: bool) -> Self {
|
|
self.bordered = bordered;
|
|
self
|
|
}
|
|
|
|
/// Set focus border for the input, default is true.
|
|
pub fn focus_bordered(mut self, bordered: bool) -> Self {
|
|
self.focus_bordered = bordered;
|
|
self
|
|
}
|
|
|
|
/// Set whether to show the clear button when the input field is not empty, default is false.
|
|
pub fn cleanable(mut self, cleanable: bool) -> Self {
|
|
self.cleanable = cleanable;
|
|
self
|
|
}
|
|
|
|
/// Set to enable toggle button for password mask state.
|
|
pub fn mask_toggle(mut self) -> Self {
|
|
self.mask_toggle = true;
|
|
self
|
|
}
|
|
|
|
/// Set to disable the input field.
|
|
pub fn disabled(mut self, disabled: bool) -> Self {
|
|
self.disabled = disabled;
|
|
self
|
|
}
|
|
|
|
/// Set the tab index for the input, default is 0.
|
|
pub fn tab_index(mut self, index: isize) -> Self {
|
|
self.tab_index = index;
|
|
self
|
|
}
|
|
|
|
fn render_toggle_mask_button(state: &Entity<InputState>, cx: &App) -> impl IntoElement {
|
|
let _masked = state.read(cx).masked;
|
|
Button::new("toggle-mask")
|
|
.icon(IconName::Eye)
|
|
.xsmall()
|
|
.ghost()
|
|
.tab_stop(false)
|
|
.on_click({
|
|
let state = state.clone();
|
|
move |_, window, cx| {
|
|
state.update(cx, |state, cx| {
|
|
state.set_masked(!state.masked, window, cx);
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
/// This method must after the refine_style.
|
|
fn render_editor(
|
|
paddings: EdgesRefinement<DefiniteLength>,
|
|
input_state: &Entity<InputState>,
|
|
state: &InputState,
|
|
window: &Window,
|
|
) -> impl IntoElement {
|
|
let base_size = window.text_style().font_size;
|
|
let rem_size = window.rem_size();
|
|
|
|
let paddings = Edges {
|
|
left: paddings
|
|
.left
|
|
.map(|v| v.to_pixels(base_size, rem_size))
|
|
.unwrap_or(px(0.)),
|
|
right: paddings
|
|
.right
|
|
.map(|v| v.to_pixels(base_size, rem_size))
|
|
.unwrap_or(px(0.)),
|
|
top: paddings
|
|
.top
|
|
.map(|v| v.to_pixels(base_size, rem_size))
|
|
.unwrap_or(px(0.)),
|
|
bottom: paddings
|
|
.bottom
|
|
.map(|v| v.to_pixels(base_size, rem_size))
|
|
.unwrap_or(px(0.)),
|
|
};
|
|
|
|
state.editor_scrollbar_paddings.set(paddings);
|
|
state.editor_scrollbar_snapshot.set(None);
|
|
|
|
v_flex().size_full().child(
|
|
div()
|
|
.relative()
|
|
.flex_1()
|
|
.child(input_state.clone())
|
|
.child(EditorScrollbar::new(input_state.clone())),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Styled for Input {
|
|
fn style(&mut self) -> &mut StyleRefinement {
|
|
&mut self.style
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for Input {
|
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
const LINE_HEIGHT: Rems = Rems(1.25);
|
|
let text_align = self.style.text.text_align.unwrap_or(TextAlign::Left);
|
|
|
|
self.state.update(cx, |state, _| {
|
|
state.disabled = self.disabled;
|
|
state.size = self.size;
|
|
// Only for single line mode
|
|
if state.mode.is_single_line() {
|
|
state.text_align = text_align;
|
|
}
|
|
});
|
|
|
|
let state = self.state.read(cx);
|
|
let _focused = state.focus_handle.is_focused(window) && !state.disabled;
|
|
|
|
let gap_x = match self.size {
|
|
Size::Small => px(4.),
|
|
Size::Large => px(8.),
|
|
_ => px(6.),
|
|
};
|
|
|
|
let (bg, _) = input_style(state.disabled, cx);
|
|
|
|
let prefix = self.prefix;
|
|
let suffix = self.suffix;
|
|
let show_clear_button = self.cleanable
|
|
&& !state.disabled
|
|
&& !state.loading
|
|
&& state.text.len() > 0
|
|
&& state.mode.is_single_line();
|
|
let has_suffix = suffix.is_some() || state.loading || self.mask_toggle || show_clear_button;
|
|
|
|
div()
|
|
.id(("input", self.state.entity_id()))
|
|
.flex()
|
|
.key_context(crate::input::CONTEXT)
|
|
.track_focus(&state.focus_handle.clone())
|
|
.tab_index(self.tab_index)
|
|
.when(!state.disabled, |this| {
|
|
this.on_action(window.listener_for(&self.state, InputState::backspace))
|
|
.on_action(window.listener_for(&self.state, InputState::delete))
|
|
.on_action(
|
|
window.listener_for(&self.state, InputState::delete_to_beginning_of_line),
|
|
)
|
|
.on_action(window.listener_for(&self.state, InputState::delete_to_end_of_line))
|
|
.on_action(window.listener_for(&self.state, InputState::delete_previous_word))
|
|
.on_action(window.listener_for(&self.state, InputState::delete_next_word))
|
|
.on_action(window.listener_for(&self.state, InputState::enter))
|
|
.on_action(window.listener_for(&self.state, InputState::escape))
|
|
.on_action(window.listener_for(&self.state, InputState::paste))
|
|
.on_action(window.listener_for(&self.state, InputState::cut))
|
|
.on_action(window.listener_for(&self.state, InputState::undo))
|
|
.on_action(window.listener_for(&self.state, InputState::redo))
|
|
.when(state.mode.is_multi_line(), |this| {
|
|
this.on_action(window.listener_for(&self.state, InputState::indent_inline))
|
|
.on_action(window.listener_for(&self.state, InputState::outdent_inline))
|
|
.on_action(window.listener_for(&self.state, InputState::indent_block))
|
|
.on_action(window.listener_for(&self.state, InputState::outdent_block))
|
|
})
|
|
})
|
|
.on_action(window.listener_for(&self.state, InputState::left))
|
|
.on_action(window.listener_for(&self.state, InputState::right))
|
|
.on_action(window.listener_for(&self.state, InputState::select_left))
|
|
.on_action(window.listener_for(&self.state, InputState::select_right))
|
|
.when(state.mode.is_multi_line(), |this| {
|
|
this.on_action(window.listener_for(&self.state, InputState::up))
|
|
.on_action(window.listener_for(&self.state, InputState::down))
|
|
.on_action(window.listener_for(&self.state, InputState::select_up))
|
|
.on_action(window.listener_for(&self.state, InputState::select_down))
|
|
.on_action(window.listener_for(&self.state, InputState::page_up))
|
|
.on_action(window.listener_for(&self.state, InputState::page_down))
|
|
})
|
|
.on_action(window.listener_for(&self.state, InputState::select_all))
|
|
.on_action(window.listener_for(&self.state, InputState::select_to_start_of_line))
|
|
.on_action(window.listener_for(&self.state, InputState::select_to_end_of_line))
|
|
.on_action(window.listener_for(&self.state, InputState::select_to_previous_word))
|
|
.on_action(window.listener_for(&self.state, InputState::select_to_next_word))
|
|
.on_action(window.listener_for(&self.state, InputState::home))
|
|
.on_action(window.listener_for(&self.state, InputState::end))
|
|
.on_action(window.listener_for(&self.state, InputState::move_to_start))
|
|
.on_action(window.listener_for(&self.state, InputState::move_to_end))
|
|
.on_action(window.listener_for(&self.state, InputState::move_to_previous_word))
|
|
.on_action(window.listener_for(&self.state, InputState::move_to_next_word))
|
|
.on_action(window.listener_for(&self.state, InputState::select_to_start))
|
|
.on_action(window.listener_for(&self.state, InputState::select_to_end))
|
|
.on_action(window.listener_for(&self.state, InputState::show_character_palette))
|
|
.on_action(window.listener_for(&self.state, InputState::copy))
|
|
.on_key_down(window.listener_for(&self.state, InputState::on_key_down))
|
|
.on_mouse_down(
|
|
MouseButton::Left,
|
|
window.listener_for(&self.state, InputState::on_mouse_down),
|
|
)
|
|
.on_mouse_down(
|
|
MouseButton::Right,
|
|
window.listener_for(&self.state, InputState::on_mouse_down),
|
|
)
|
|
.on_mouse_up(
|
|
MouseButton::Left,
|
|
window.listener_for(&self.state, InputState::on_mouse_up),
|
|
)
|
|
.on_mouse_up(
|
|
MouseButton::Right,
|
|
window.listener_for(&self.state, InputState::on_mouse_up),
|
|
)
|
|
.on_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel))
|
|
.size_full()
|
|
.line_height(LINE_HEIGHT)
|
|
.input_px(self.size)
|
|
.input_py(self.size)
|
|
.input_h(self.size)
|
|
.input_font_size(self.size)
|
|
.when(!self.disabled, |this| this.cursor_text())
|
|
.items_center()
|
|
.when(state.mode.is_multi_line(), |this| {
|
|
this.h_auto()
|
|
.when_some(self.height, |this, height| this.h(height))
|
|
})
|
|
.when(self.appearance, |this| {
|
|
this.bg(bg)
|
|
.when(self.disabled, |this| this.opacity(0.5))
|
|
.rounded(cx.theme().radius)
|
|
.when(self.bordered, |this| {
|
|
this.border_color(cx.theme().border)
|
|
.border_1()
|
|
.when(cx.theme().shadow, |this| this.shadow_xs())
|
|
})
|
|
})
|
|
.items_center()
|
|
.gap(gap_x)
|
|
.refine_style(&self.style)
|
|
.children(prefix)
|
|
.when(state.mode.is_multi_line(), |mut this| {
|
|
let paddings = this.style().padding.clone();
|
|
this.child(Self::render_editor(paddings, &self.state, state, window))
|
|
})
|
|
.when(!state.mode.is_multi_line(), |this| {
|
|
this.child(self.state.clone())
|
|
})
|
|
.when(has_suffix, |this| {
|
|
this.pr_2().child(
|
|
h_flex()
|
|
.id("suffix")
|
|
.gap(gap_x)
|
|
.items_center()
|
|
.when(state.loading, |this| this.child(Indicator::new()))
|
|
.when(self.mask_toggle, |this| {
|
|
this.child(Self::render_toggle_mask_button(&self.state, cx))
|
|
})
|
|
.when(show_clear_button, |this| {
|
|
this.child(clear_button(cx).on_click({
|
|
let state = self.state.clone();
|
|
move |_, window, cx| {
|
|
state.update(cx, |state, cx| {
|
|
state.clean(window, cx);
|
|
state.focus(window, cx);
|
|
})
|
|
}
|
|
}))
|
|
})
|
|
.children(suffix),
|
|
)
|
|
})
|
|
}
|
|
}
|