audit input
This commit is contained in:
+175
-94
@@ -7,15 +7,17 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
App, AppContext as _, Bounds, ClipboardItem, ContentMask, Context, CursorStyle, DefiniteLength,
|
||||
DispatchPhase, ElementId, Entity, EntityInputHandler, EventEmitter, Font, HitboxBehavior,
|
||||
InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent,
|
||||
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, RenderOnce, Role, ScrollDelta,
|
||||
ScrollEvent, SharedString, StatefulInteractiveElement as _, Styled as _, Subscription, Task,
|
||||
TextAlign, TextRun, UTF16Selection, Window, WrappedLine, actions, canvas, div, fill, point, px,
|
||||
AnyElement, App, AppContext as _, Bounds, ClipboardItem, ContentMask, Context, CursorStyle,
|
||||
DefiniteLength, DispatchPhase, ElementId, Entity, EntityInputHandler, EventEmitter, Font,
|
||||
HitboxBehavior, InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, RenderOnce, Role,
|
||||
ScrollDelta, ScrollEvent, SharedString, StatefulInteractiveElement as _, Styled as _,
|
||||
Subscription, Task, TextAlign, TextRun, UTF16Selection, Window, WrappedLine, actions, canvas,
|
||||
div, fill, point, px,
|
||||
};
|
||||
use gpui_native_icons::IconRole;
|
||||
use gpui_native_theme::{ActiveTheme as _, ColorTokens, Size};
|
||||
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, ColorTokens, Size};
|
||||
use gpui_native_utils::focus_ring;
|
||||
|
||||
use crate::button::{Button, ButtonVariant};
|
||||
use crate::common::{Sizable, h_flex};
|
||||
@@ -1904,6 +1906,8 @@ pub struct Input {
|
||||
a11y_label: Option<SharedString>,
|
||||
/// Multiline field height in rows; ignored when the state is single-line.
|
||||
rows: usize,
|
||||
prefix: Option<AnyElement>,
|
||||
suffix: Option<AnyElement>,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
@@ -1920,6 +1924,8 @@ impl Input {
|
||||
disabled: false,
|
||||
a11y_label: None,
|
||||
rows: DEFAULT_ROWS,
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1952,6 +1958,24 @@ impl Input {
|
||||
self.a11y_label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Docks an element inside the field's leading edge.
|
||||
pub fn prefix<E>(mut self, prefix: E) -> Self
|
||||
where
|
||||
E: IntoElement,
|
||||
{
|
||||
self.prefix = Some(prefix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Docks an element inside the field's trailing edge.
|
||||
pub fn suffix<E>(mut self, suffix: E) -> Self
|
||||
where
|
||||
E: IntoElement,
|
||||
{
|
||||
self.suffix = Some(suffix.into_any_element());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for Input {
|
||||
@@ -1974,18 +1998,20 @@ impl RenderOnce for Input {
|
||||
let revealed = self.state.read(cx).is_revealed();
|
||||
let multiline = self.state.read(cx).is_multiline();
|
||||
|
||||
let right_padding = if is_password {
|
||||
metrics.dense_padding_x + metrics.dense_icon_size
|
||||
} else {
|
||||
metrics.dense_padding_x
|
||||
};
|
||||
let small = theme.sizes.resolve(Size::Small);
|
||||
let docked_height = small.control_height + BORDER_WIDTH * 2.0;
|
||||
let edge_inset = ((metrics.input_height - docked_height) / 2.0).max(Pixels::ZERO);
|
||||
|
||||
// The field's own breathing room; the text starts here from the top when multiline.
|
||||
let vertical_inset = metrics.dense_padding_x * 0.5;
|
||||
let reveal_width = small.dense_padding_x * 2.0 + small.icon_size;
|
||||
|
||||
let field_padding = FieldPadding {
|
||||
left: metrics.dense_padding_x,
|
||||
right: right_padding,
|
||||
left: Pixels::ZERO,
|
||||
right: if is_password {
|
||||
(edge_inset + reveal_width - metrics.dense_padding_x).max(Pixels::ZERO)
|
||||
} else {
|
||||
Pixels::ZERO
|
||||
},
|
||||
top: vertical_inset,
|
||||
};
|
||||
|
||||
@@ -1993,9 +2019,10 @@ impl RenderOnce for Input {
|
||||
DefiniteLength::Fraction(ratio) => metrics.text_size * ratio,
|
||||
DefiniteLength::Absolute(length) => length.to_pixels(window.rem_size()),
|
||||
});
|
||||
|
||||
let field_height = line_height.map_or(metrics.input_height, |line_height| {
|
||||
line_height * self.rows as f32 + vertical_inset * 2.0
|
||||
});
|
||||
}) + BORDER_WIDTH * 2.0;
|
||||
|
||||
let fallback_label = self
|
||||
.a11y_label
|
||||
@@ -2003,8 +2030,20 @@ impl RenderOnce for Input {
|
||||
.or_else(|| (!self.placeholder.is_empty()).then_some(self.placeholder.clone()))
|
||||
.unwrap_or_else(|| SharedString::from(self.id.to_string()));
|
||||
|
||||
let edge_zone = |name: &'static str, element: AnyElement| {
|
||||
div()
|
||||
.debug_selector(|| name.to_string())
|
||||
.flex_none()
|
||||
.h_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.child(element)
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id(self.id.clone())
|
||||
.debug_selector(|| "input-field".to_string())
|
||||
.key_context(INPUT_CONTEXT)
|
||||
.role(Role::TextInput)
|
||||
.aria_label(fallback_label)
|
||||
@@ -2012,117 +2051,159 @@ impl RenderOnce for Input {
|
||||
.overflow_hidden()
|
||||
.w_full()
|
||||
.h(field_height)
|
||||
.px(metrics.dense_padding_x)
|
||||
.pl(if self.prefix.is_some() {
|
||||
edge_inset
|
||||
} else {
|
||||
metrics.dense_padding_x
|
||||
})
|
||||
.pr(if self.suffix.is_some() {
|
||||
edge_inset
|
||||
} else {
|
||||
metrics.dense_padding_x
|
||||
})
|
||||
.gap_1p5()
|
||||
.rounded(radius.control)
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.bg(colors.input)
|
||||
.text_size(metrics.text_size)
|
||||
.text_color(colors.foreground)
|
||||
.when_some(line_height, |input, line_height| {
|
||||
input.items_start().line_height(line_height)
|
||||
.border_1()
|
||||
.border_color(if theme.borders.fields {
|
||||
colors.border
|
||||
} else {
|
||||
colors.input
|
||||
})
|
||||
.when(self.disabled, |input| input.opacity(0.5))
|
||||
.when(!self.disabled, |input| {
|
||||
input.track_focus(&focus_handle).tab_stop(true)
|
||||
})
|
||||
.when(focused, |input| input.border_color(colors.ring))
|
||||
.when(is_empty && !focused, |input| {
|
||||
input.child(
|
||||
div()
|
||||
.text_color(colors.muted_foreground)
|
||||
.when(multiline, |placeholder| placeholder.pt(vertical_inset))
|
||||
.child(self.placeholder.clone()),
|
||||
)
|
||||
.when(focused && !theme.borders.focus_ring, |input| {
|
||||
input.border_color(colors.ring)
|
||||
})
|
||||
.when(focused && theme.borders.focus_ring, |input| {
|
||||
input.shadow(focus_ring(colors.ring))
|
||||
})
|
||||
.when_some(self.prefix, |input, prefix| {
|
||||
input.child(edge_zone("input-prefix", prefix))
|
||||
})
|
||||
.child(
|
||||
canvas(
|
||||
{
|
||||
let state = self.state.clone();
|
||||
move |bounds, window, cx| {
|
||||
// The hitbox backs the I-beam hover cursor.
|
||||
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
|
||||
let wrap_width = multiline.then(|| {
|
||||
(bounds.size.width - field_padding.left - field_padding.right)
|
||||
.max(Pixels::ZERO)
|
||||
});
|
||||
let text = state.read(cx).shape_current_text(wrap_width, window);
|
||||
(text, hitbox)
|
||||
}
|
||||
},
|
||||
{
|
||||
let state = self.state.clone();
|
||||
let focus = focus_handle.clone();
|
||||
let disabled = self.disabled;
|
||||
div()
|
||||
.relative()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.h_full()
|
||||
.overflow_hidden()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.when_some(line_height, |region, line_height| {
|
||||
region.items_start().line_height(line_height)
|
||||
})
|
||||
.when(is_empty && !focused, |region| {
|
||||
region.child(
|
||||
div()
|
||||
.text_color(colors.muted_foreground)
|
||||
.when(multiline, |placeholder| placeholder.pt(vertical_inset))
|
||||
.child(self.placeholder.clone()),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
canvas(
|
||||
{
|
||||
let state = self.state.clone();
|
||||
move |bounds, window, cx| {
|
||||
// The hitbox backs the I-beam hover cursor.
|
||||
let hitbox =
|
||||
window.insert_hitbox(bounds, HitboxBehavior::Normal);
|
||||
let wrap_width = multiline.then(|| {
|
||||
(bounds.size.width
|
||||
- field_padding.left
|
||||
- field_padding.right)
|
||||
.max(Pixels::ZERO)
|
||||
});
|
||||
let text =
|
||||
state.read(cx).shape_current_text(wrap_width, window);
|
||||
(text, hitbox)
|
||||
}
|
||||
},
|
||||
{
|
||||
let state = self.state.clone();
|
||||
let focus = focus_handle.clone();
|
||||
let disabled = self.disabled;
|
||||
|
||||
move |bounds, (text, hitbox), window, cx| {
|
||||
if !disabled {
|
||||
window.set_cursor_style(CursorStyle::IBeam, &hitbox);
|
||||
}
|
||||
move |bounds, (text, hitbox), window, cx| {
|
||||
if !disabled {
|
||||
window.set_cursor_style(CursorStyle::IBeam, &hitbox);
|
||||
}
|
||||
|
||||
let dragging = state.update(cx, |state, cx| {
|
||||
state.paint(text, bounds, field_padding, colors, window, cx)
|
||||
});
|
||||
let dragging = state.update(cx, |state, cx| {
|
||||
state.paint(text, bounds, field_padding, colors, window, cx)
|
||||
});
|
||||
|
||||
if disabled {
|
||||
return;
|
||||
}
|
||||
|
||||
window.handle_input(
|
||||
&focus,
|
||||
gpui::ElementInputHandler::new(bounds, state.clone()),
|
||||
cx,
|
||||
);
|
||||
|
||||
window.on_mouse_event({
|
||||
let state = state.clone();
|
||||
move |event: &ScrollEvent, phase, window, cx| {
|
||||
if phase != DispatchPhase::Bubble
|
||||
|| !bounds.contains(&event.position)
|
||||
{
|
||||
if disabled {
|
||||
return;
|
||||
}
|
||||
|
||||
let delta = match event.delta {
|
||||
ScrollDelta::Pixels(delta) => delta,
|
||||
ScrollDelta::Lines(lines) => point(
|
||||
px(lines.x * SCROLL_LINES_HEIGHT),
|
||||
px(lines.y * SCROLL_LINES_HEIGHT),
|
||||
),
|
||||
};
|
||||
window.handle_input(
|
||||
&focus,
|
||||
gpui::ElementInputHandler::new(bounds, state.clone()),
|
||||
cx,
|
||||
);
|
||||
|
||||
let consumed = state
|
||||
.update(cx, |state, cx| state.scroll_by(delta, window, cx));
|
||||
window.on_mouse_event({
|
||||
let state = state.clone();
|
||||
move |event: &ScrollEvent, phase, window, cx| {
|
||||
if phase != DispatchPhase::Bubble
|
||||
|| !bounds.contains(&event.position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if consumed {
|
||||
cx.stop_propagation();
|
||||
let delta = match event.delta {
|
||||
ScrollDelta::Pixels(delta) => delta,
|
||||
ScrollDelta::Lines(lines) => point(
|
||||
px(lines.x * SCROLL_LINES_HEIGHT),
|
||||
px(lines.y * SCROLL_LINES_HEIGHT),
|
||||
),
|
||||
};
|
||||
|
||||
let consumed = state.update(cx, |state, cx| {
|
||||
state.scroll_by(delta, window, cx)
|
||||
});
|
||||
|
||||
if consumed {
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if dragging {
|
||||
InputState::register_drag_listeners(&state, window);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if dragging {
|
||||
InputState::register_drag_listeners(&state, window);
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.absolute()
|
||||
.inset_0(),
|
||||
},
|
||||
)
|
||||
.absolute()
|
||||
.inset_0(),
|
||||
),
|
||||
)
|
||||
.when_some(self.suffix, |input, suffix| {
|
||||
input.child(edge_zone("input-suffix", suffix))
|
||||
})
|
||||
.when(is_password && !self.disabled, |input| {
|
||||
input.child(
|
||||
div()
|
||||
.id((self.id.clone(), "input-reveal"))
|
||||
.debug_selector(|| "input-reveal".to_string())
|
||||
.absolute()
|
||||
.right(metrics.dense_padding_x / 2.0)
|
||||
.top_0p5()
|
||||
.bottom_0p5()
|
||||
.right(edge_inset)
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.flex()
|
||||
.items_center()
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.child(
|
||||
Button::new((self.id.clone(), "input-reveal-button"))
|
||||
.debug_selector(|| "input-reveal-button".to_string())
|
||||
.icon(if revealed {
|
||||
IconRole::EyeOff
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user