example_input.rs

  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}