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