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