ori_core/views/
text_input.rs

1use glam::Vec2;
2use ori_graphics::{Color, Quad, Rect, TextAlign, TextSection};
3use ori_macro::Build;
4
5use crate::{
6    BoxConstraints, Context, DrawContext, Event, EventContext, EventSignal, Key, KeyboardEvent,
7    LayoutContext, PointerEvent, Scope, SharedSignal, Signal, Style, View,
8};
9
10#[derive(Clone, Debug, Build)]
11pub struct TextInput {
12    #[prop]
13    placeholder: String,
14    #[bind]
15    text: SharedSignal<String>,
16    #[event]
17    on_input: Option<EventSignal<KeyboardEvent>>,
18    #[event]
19    on_submit: Option<EventSignal<String>>,
20}
21
22impl Default for TextInput {
23    fn default() -> Self {
24        Self {
25            placeholder: String::from("Enter text..."),
26            text: SharedSignal::new(String::new()),
27            on_input: None,
28            on_submit: None,
29        }
30    }
31}
32
33impl TextInput {
34    pub fn new(text: impl Into<String>) -> Self {
35        Self {
36            text: SharedSignal::new(text.into()),
37            ..Default::default()
38        }
39    }
40
41    pub fn with_text(self, text: impl Into<String>) -> Self {
42        self.text.set(text.into());
43        self
44    }
45
46    pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
47        self.placeholder = placeholder.into();
48        self
49    }
50
51    pub fn bind_text<'a>(self, cx: Scope<'a>, text: &'a Signal<String>) -> Self {
52        let signal = cx.alloc(self.text.clone());
53        cx.bind(text, &signal);
54        self
55    }
56
57    fn display_text(&self) -> String {
58        if self.text.get().is_empty() {
59            self.placeholder.clone()
60        } else {
61            self.text.cloned()
62        }
63    }
64
65    fn display_section(&self, state: &TextInputState, cx: &mut impl Context) -> TextSection {
66        let color = if self.text.get().is_empty() {
67            cx.style("placeholder-color")
68        } else {
69            cx.style("color")
70        };
71
72        TextSection {
73            rect: cx.rect().translate(Vec2::new(state.padding, 0.0)),
74            scale: state.font_size,
75            h_align: TextAlign::Start,
76            v_align: TextAlign::Center,
77            wrap: false,
78            text: self.display_text(),
79            font: cx.style("font"),
80            color,
81        }
82    }
83
84    fn section(&self, state: &TextInputState, cx: &mut impl Context) -> TextSection {
85        TextSection {
86            rect: cx.rect().translate(Vec2::new(state.padding, 0.0)),
87            scale: state.font_size,
88            h_align: TextAlign::Start,
89            v_align: TextAlign::Center,
90            wrap: false,
91            text: self.text.cloned(),
92            font: cx.style("font"),
93            ..Default::default()
94        }
95    }
96
97    fn handle_pointer_event(
98        &self,
99        state: &mut TextInputState,
100        cx: &mut EventContext,
101        event: &PointerEvent,
102    ) {
103        if event.is_press() && cx.hovered() {
104            let section = self.section(state, cx);
105            let hit = cx.renderer.hit_text(&section, event.position);
106
107            if let Some(hit) = hit {
108                if hit.delta.x > 0.0 {
109                    state.cursor = Some(hit.index + 1);
110                    cx.focus();
111                } else {
112                    state.cursor = Some(hit.index);
113                    cx.focus()
114                }
115            } else {
116                state.cursor = Some(0);
117                cx.focus();
118            }
119        } else if event.is_press() && !cx.hovered() {
120            state.cursor = None;
121            cx.unfocus();
122        }
123    }
124
125    fn handle_keyboard_input(
126        &self,
127        state: &mut TextInputState,
128        cx: &mut EventContext,
129        event: &KeyboardEvent,
130    ) {
131        if event.is_press() {
132            if let Some(on_input) = &self.on_input {
133                on_input.emit(event.clone());
134            }
135
136            self.handle_key(state, cx, event.key.unwrap());
137        }
138
139        if let Some(c) = event.text {
140            if !c.is_control() {
141                if let Some(on_input) = &self.on_input {
142                    on_input.emit(event.clone());
143                }
144
145                let mut text = self.text.modify();
146                if let Some(cursor) = state.cursor {
147                    if cursor < text.len() {
148                        text.insert(cursor, c);
149                    } else {
150                        text.push(c);
151                    }
152
153                    let new_cursor = cursor + c.len_utf8();
154                    state.cursor = Some(new_cursor);
155                }
156
157                cx.request_redraw();
158            }
159        }
160    }
161
162    fn handle_key(&self, state: &mut TextInputState, cx: &mut EventContext, key: Key) {
163        match key {
164            Key::Right => {
165                state.cursor_right(&self.text.get());
166                cx.request_redraw();
167            }
168            Key::Left => {
169                state.cursor_left();
170                cx.request_redraw();
171            }
172            Key::Backspace => {
173                if let Some(cursor) = state.cursor {
174                    if cursor > 0 {
175                        let mut text = self.text.modify();
176                        text.remove(cursor - 1);
177                        state.cursor_left();
178                        cx.request_redraw();
179                    }
180                }
181            }
182            Key::Escape => {
183                state.cursor = None;
184                cx.unfocus();
185            }
186            Key::Enter => {
187                if let Some(on_submit) = &self.on_submit {
188                    on_submit.emit(self.text.cloned());
189                    state.cursor = None;
190                    cx.unfocus();
191                }
192            }
193            _ => {}
194        }
195    }
196}
197
198#[derive(Clone, Debug, Default)]
199pub struct TextInputState {
200    font_size: f32,
201    blink: f32,
202    padding: f32,
203    cursor: Option<usize>,
204}
205
206impl TextInputState {
207    fn reset_blink(&mut self) {
208        self.blink = 0.0;
209    }
210
211    fn cursor_right(&mut self, text: &str) {
212        if let Some(cursor) = self.cursor {
213            if cursor < text.len() {
214                self.cursor = Some(cursor + 1);
215            }
216        }
217
218        self.reset_blink();
219    }
220
221    fn cursor_left(&mut self) {
222        if let Some(cursor) = self.cursor {
223            if cursor > 0 {
224                self.cursor = Some(cursor - 1);
225            }
226        }
227
228        self.reset_blink();
229    }
230}
231
232impl View for TextInput {
233    type State = TextInputState;
234
235    fn build(&self) -> Self::State {
236        TextInputState::default()
237    }
238
239    fn style(&self) -> Style {
240        Style::new("text-input")
241    }
242
243    fn event(&self, state: &mut Self::State, cx: &mut EventContext, event: &Event) {
244        if let Some(pointer_event) = event.get::<PointerEvent>() {
245            self.handle_pointer_event(state, cx, pointer_event);
246        }
247
248        if let Some(keyboard_event) = event.get::<KeyboardEvent>() {
249            self.handle_keyboard_input(state, cx, keyboard_event);
250        }
251    }
252
253    fn layout(&self, state: &mut Self::State, cx: &mut LayoutContext, bc: BoxConstraints) -> Vec2 {
254        let font_size = cx.style_range("font-size", 0.0..bc.max.y);
255
256        state.font_size = font_size;
257
258        let padding = cx.style_range("padding", 0.0..bc.max.min_element() / 2.0);
259        state.padding = padding;
260
261        let min_width = cx.style_range_group("width", "min-width", bc.width());
262        let max_width = cx.style_range_group("width", "max-width", bc.width());
263
264        let mut min_height = cx.style_range_group("height", "min-height", bc.height());
265        let max_height = cx.style_range_group("height", "max-height", bc.height());
266
267        min_height = min_height.max(font_size + padding * 2.0);
268
269        let min_size = bc.constrain(Vec2::new(min_width, min_height));
270        let max_size = bc.constrain(Vec2::new(max_width, max_height));
271
272        let section = self.display_section(state, cx);
273        let mut size = cx.messure_text(&section).unwrap_or_default().size();
274        size += padding * 2.0;
275        size.clamp(min_size, max_size)
276    }
277
278    fn draw(&self, state: &mut Self::State, cx: &mut DrawContext) {
279        cx.draw_quad();
280
281        let section = self.display_section(state, cx);
282        cx.draw(section);
283
284        if let Some(cursor) = state.cursor {
285            state.blink += cx.state.delta() * 10.0;
286            cx.request_redraw();
287
288            let section = TextSection {
289                text: self.text.get()[..cursor].into(),
290                ..self.section(state, cx)
291            };
292
293            let bounds = cx.renderer.messure_text(&section).unwrap_or_default();
294            let cursor = f32::max(bounds.max.x, cx.rect().min.x + state.padding);
295
296            let quad = Quad {
297                rect: Rect::min_size(
298                    Vec2::new(cursor, cx.rect().min.y + state.padding),
299                    Vec2::new(1.0, cx.rect().height() - state.padding * 2.0),
300                )
301                .round(),
302                background: cx.style::<Color>("color") * state.blink.cos(),
303                ..Quad::default()
304            };
305
306            cx.draw(quad);
307        }
308    }
309}