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 backed by its own
5//! `ExampleInputState` entity, giving it an independent caching boundary
6//! from both its parent and the inner editor.
7
8use std::time::Duration;
9
10use gpui::{
11 Animation, AnimationExt as _, App, BoxShadow, Context, CursorStyle, Entity, FocusHandle, Hsla,
12 IntoViewElement, Pixels, SharedString, StyleRefinement, Subscription, ViewElement, Window,
13 bounce, div, ease_in_out, hsla, point, prelude::*, px, white,
14};
15
16use crate::example_editor::ExampleEditor;
17use crate::{Backspace, Delete, End, Enter, Home, Left, Right};
18
19pub struct ExampleInputState {
20 editor: Entity<ExampleEditor>,
21 focus_handle: FocusHandle,
22 is_focused: bool,
23 flash_count: usize,
24 _subscriptions: Vec<Subscription>,
25}
26
27impl ExampleInputState {
28 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
29 let editor = cx.new(|cx| ExampleEditor::new(cx));
30 let focus_handle = editor.read(cx).focus_handle.clone();
31
32 let focus_sub = cx.on_focus(&focus_handle, window, |this, _window, cx| {
33 this.is_focused = true;
34 cx.notify();
35 });
36 let blur_sub = cx.on_blur(&focus_handle, window, |this, _window, cx| {
37 this.is_focused = false;
38 cx.notify();
39 });
40
41 Self {
42 editor,
43 focus_handle,
44 is_focused: false,
45 flash_count: 0,
46 _subscriptions: vec![focus_sub, blur_sub],
47 }
48 }
49}
50
51#[derive(Hash, IntoViewElement)]
52pub struct ExampleInput {
53 state: Entity<ExampleInputState>,
54 width: Option<Pixels>,
55 color: Option<Hsla>,
56}
57
58impl ExampleInput {
59 pub fn new(state: Entity<ExampleInputState>) -> Self {
60 Self {
61 state,
62 width: None,
63 color: None,
64 }
65 }
66
67 pub fn width(mut self, width: Pixels) -> Self {
68 self.width = Some(width);
69 self
70 }
71
72 pub fn color(mut self, color: Hsla) -> Self {
73 self.color = Some(color);
74 self
75 }
76}
77
78impl gpui::View for ExampleInput {
79 type Entity = ExampleInputState;
80
81 fn entity(&self) -> Option<Entity<ExampleInputState>> {
82 Some(self.state.clone())
83 }
84
85 fn cache_style(&mut self, _window: &mut Window, _cx: &mut App) -> Option<StyleRefinement> {
86 let mut style = StyleRefinement::default();
87 if let Some(w) = self.width {
88 style.size.width = Some(w.into());
89 }
90 style.size.height = Some(px(36.).into());
91 Some(style)
92 }
93
94 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
95 let input_state = self.state.read(cx);
96 let count = input_state.flash_count;
97 let editor = input_state.editor.clone();
98 let focus_handle = input_state.focus_handle.clone();
99 let is_focused = input_state.is_focused;
100 let text_color = self.color.unwrap_or(hsla(0., 0., 0.1, 1.));
101 let box_width = self.width.unwrap_or(px(300.));
102 let state = self.state;
103
104 let focused_border = hsla(220. / 360., 0.8, 0.5, 1.);
105 let unfocused_border = hsla(0., 0., 0.75, 1.);
106 let normal_border = if is_focused {
107 focused_border
108 } else {
109 unfocused_border
110 };
111 let highlight_border = hsla(140. / 360., 0.8, 0.5, 1.);
112
113 let base = div()
114 .id("input")
115 .key_context("TextInput")
116 .track_focus(&focus_handle)
117 .cursor(CursorStyle::IBeam)
118 .on_action({
119 let editor = editor.clone();
120 move |action: &Backspace, _window, cx| {
121 editor.update(cx, |state, cx| state.backspace(action, _window, cx));
122 }
123 })
124 .on_action({
125 let editor = editor.clone();
126 move |action: &Delete, _window, cx| {
127 editor.update(cx, |state, cx| state.delete(action, _window, cx));
128 }
129 })
130 .on_action({
131 let editor = editor.clone();
132 move |action: &Left, _window, cx| {
133 editor.update(cx, |state, cx| state.left(action, _window, cx));
134 }
135 })
136 .on_action({
137 let editor = editor.clone();
138 move |action: &Right, _window, cx| {
139 editor.update(cx, |state, cx| state.right(action, _window, cx));
140 }
141 })
142 .on_action({
143 let editor = editor.clone();
144 move |action: &Home, _window, cx| {
145 editor.update(cx, |state, cx| state.home(action, _window, cx));
146 }
147 })
148 .on_action({
149 let editor = editor.clone();
150 move |action: &End, _window, cx| {
151 editor.update(cx, |state, cx| state.end(action, _window, cx));
152 }
153 })
154 .on_action({
155 move |_: &Enter, _window, cx| {
156 state.update(cx, |state, cx| {
157 state.flash_count += 1;
158 cx.notify();
159 });
160 }
161 })
162 .w(box_width)
163 .h(px(36.))
164 .px(px(8.))
165 .bg(white())
166 .border_1()
167 .border_color(normal_border)
168 .when(is_focused, |this| {
169 this.shadow(vec![BoxShadow {
170 color: hsla(220. / 360., 0.8, 0.5, 0.3),
171 offset: point(px(0.), px(0.)),
172 blur_radius: px(4.),
173 spread_radius: px(1.),
174 }])
175 })
176 .rounded(px(4.))
177 .overflow_hidden()
178 .flex()
179 .items_center()
180 .line_height(px(20.))
181 .text_size(px(14.))
182 .text_color(text_color)
183 .child(ViewElement::new(editor));
184
185 if count > 0 {
186 base.with_animation(
187 SharedString::from(format!("enter-bounce-{count}")),
188 Animation::new(Duration::from_millis(300)).with_easing(bounce(ease_in_out)),
189 move |this, delta| {
190 let h = normal_border.h + (highlight_border.h - normal_border.h) * delta;
191 let s = normal_border.s + (highlight_border.s - normal_border.s) * delta;
192 let l = normal_border.l + (highlight_border.l - normal_border.l) * delta;
193 this.border_color(hsla(h, s, l, 1.0))
194 },
195 )
196 .into_any_element()
197 } else {
198 base.into_any_element()
199 }
200 }
201}