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 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}