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