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