1//! The `ExampleInput` view — a single-line text input component.
2//!
3//! Composes `ExampleEditorText` inside a styled container with focus ring, border,
4//! and action handlers. Implements the `View` trait with `#[derive(Hash)]`
5//! so that prop changes (color, width) automatically invalidate the render
6//! cache via `ViewElement::cached()`.
7
8use std::time::Duration;
9
10use gpui::{
11 Animation, AnimationExt as _, App, BoxShadow, CursorStyle, Entity, Hsla, IntoViewElement,
12 Pixels, SharedString, StyleRefinement, Window, bounce, div, ease_in_out, hsla, point,
13 prelude::*, px, white,
14};
15
16use crate::example_editor::ExampleEditor;
17use crate::example_editor::ExampleEditorView;
18use crate::{Backspace, Delete, End, Enter, Home, Left, Right};
19
20struct FlashState {
21 count: usize,
22}
23
24#[derive(Hash, IntoViewElement)]
25pub struct ExampleInput {
26 editor: Entity<ExampleEditor>,
27 width: Option<Pixels>,
28 color: Option<Hsla>,
29}
30
31impl ExampleInput {
32 pub fn new(editor: Entity<ExampleEditor>) -> Self {
33 Self {
34 editor,
35 width: None,
36 color: None,
37 }
38 }
39
40 pub fn width(mut self, width: Pixels) -> Self {
41 self.width = Some(width);
42 self
43 }
44
45 pub fn color(mut self, color: Hsla) -> Self {
46 self.color = Some(color);
47 self
48 }
49}
50
51impl gpui::View for ExampleInput {
52 type Entity = ExampleEditor;
53
54 fn entity(&self) -> Option<Entity<ExampleEditor>> {
55 Some(self.editor.clone())
56 }
57
58 fn style(&self) -> Option<StyleRefinement> {
59 let mut style = StyleRefinement::default();
60 if let Some(w) = self.width {
61 style.size.width = Some(w.into());
62 }
63 style.size.height = Some(px(36.).into());
64 Some(style)
65 }
66
67 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
68 let flash_state = window.use_state(cx, |_window, _cx| FlashState { count: 0 });
69 let count = flash_state.read(cx).count;
70
71 let focus_handle = self.editor.read(cx).focus_handle.clone();
72 let is_focused = focus_handle.is_focused(window);
73 let text_color = self.color.unwrap_or(hsla(0., 0., 0.1, 1.));
74 let box_width = self.width.unwrap_or(px(300.));
75 let editor = self.editor;
76
77 let focused_border = hsla(220. / 360., 0.8, 0.5, 1.);
78 let unfocused_border = hsla(0., 0., 0.75, 1.);
79 let normal_border = if is_focused {
80 focused_border
81 } else {
82 unfocused_border
83 };
84 let highlight_border = hsla(140. / 360., 0.8, 0.5, 1.);
85
86 let base = div()
87 .id("input")
88 .key_context("TextInput")
89 .track_focus(&focus_handle)
90 .cursor(CursorStyle::IBeam)
91 .on_action({
92 let editor = editor.clone();
93 move |action: &Backspace, _window, cx| {
94 editor.update(cx, |state, cx| state.backspace(action, _window, cx));
95 }
96 })
97 .on_action({
98 let editor = editor.clone();
99 move |action: &Delete, _window, cx| {
100 editor.update(cx, |state, cx| state.delete(action, _window, cx));
101 }
102 })
103 .on_action({
104 let editor = editor.clone();
105 move |action: &Left, _window, cx| {
106 editor.update(cx, |state, cx| state.left(action, _window, cx));
107 }
108 })
109 .on_action({
110 let editor = editor.clone();
111 move |action: &Right, _window, cx| {
112 editor.update(cx, |state, cx| state.right(action, _window, cx));
113 }
114 })
115 .on_action({
116 let editor = editor.clone();
117 move |action: &Home, _window, cx| {
118 editor.update(cx, |state, cx| state.home(action, _window, cx));
119 }
120 })
121 .on_action({
122 let editor = editor.clone();
123 move |action: &End, _window, cx| {
124 editor.update(cx, |state, cx| state.end(action, _window, cx));
125 }
126 })
127 .on_action({
128 let flash_state = flash_state;
129 move |_: &Enter, _window, cx| {
130 flash_state.update(cx, |state, cx| {
131 state.count += 1;
132 cx.notify();
133 });
134 }
135 })
136 .w(box_width)
137 .h(px(36.))
138 .px(px(8.))
139 .bg(white())
140 .border_1()
141 .border_color(normal_border)
142 .when(is_focused, |this| {
143 this.shadow(vec![BoxShadow {
144 color: hsla(220. / 360., 0.8, 0.5, 0.3),
145 offset: point(px(0.), px(0.)),
146 blur_radius: px(4.),
147 spread_radius: px(1.),
148 }])
149 })
150 .rounded(px(4.))
151 .overflow_hidden()
152 .flex()
153 .items_center()
154 .line_height(px(20.))
155 .text_size(px(14.))
156 .text_color(text_color)
157 .child(ExampleEditorView::new(editor).text_color(text_color));
158
159 if count > 0 {
160 base.with_animation(
161 SharedString::from(format!("enter-bounce-{count}")),
162 Animation::new(Duration::from_millis(300)).with_easing(bounce(ease_in_out)),
163 move |this, delta| {
164 let h = normal_border.h + (highlight_border.h - normal_border.h) * delta;
165 let s = normal_border.s + (highlight_border.s - normal_border.s) * delta;
166 let l = normal_border.l + (highlight_border.l - normal_border.l) * delta;
167 this.border_color(hsla(h, s, l, 1.0))
168 },
169 )
170 .into_any_element()
171 } else {
172 base.into_any_element()
173 }
174 }
175}