pixel_widgets/widget/
input.rs

1use std::borrow::Cow;
2use std::time::Instant;
3
4#[cfg(feature = "clipboard")]
5use clipboard::{ClipboardContext, ClipboardProvider};
6use rusttype::Scale;
7use smallvec::smallvec;
8
9use crate::draw::*;
10use crate::event::{Event, Key, Modifiers};
11use crate::layout::{Rectangle, Size};
12use crate::node::{GenericNode, IntoNode, Node};
13use crate::style::{StyleState, Stylesheet};
14use crate::text::{Text, TextWrap};
15use crate::widget::{Context, Widget};
16
17use super::StateVec;
18
19#[cfg(target_os = "macos")]
20const BACKWARDS_DELETE: char = '\x7f';
21#[cfg(not(target_os = "macos"))]
22const BACKWARDS_DELETE: char = '\x08';
23#[cfg(target_os = "macos")]
24const FORWARD_DELETE: char = '\x08';
25#[cfg(not(target_os = "macos"))]
26const FORWARD_DELETE: char = '\x7f';
27
28/// State for [`Input`](struct.Input.html)
29pub struct State {
30    scroll_x: f32,
31    scroll_y: f32,
32    modifiers: Modifiers,
33    inner: InnerState,
34    cursor: (f32, f32),
35}
36
37#[derive(Clone, Copy)]
38enum InnerState {
39    Dragging(usize, usize, Instant),
40    Focused(usize, usize, Instant),
41    Idle,
42}
43
44/// Editable text input
45pub struct Input<'a, T, F, S> {
46    placeholder: &'a str,
47    password: bool,
48    value: S,
49    on_change: F,
50    on_submit: Option<T>,
51    trigger: Option<Key>,
52}
53
54impl<'a, T, F, S> Input<'a, T, F, S>
55where
56    T: 'a + Send,
57    F: 'a + Send + Fn(String) -> T,
58    S: 'a + Send + AsRef<str>,
59{
60    /// Construct a new `Input`
61    pub fn new(placeholder: &'a str, value: S, on_change: F) -> Self {
62        Input {
63            placeholder,
64            password: false,
65            value,
66            on_change,
67            on_submit: None,
68            trigger: None,
69        }
70    }
71
72    /// Sets the placeholder text, which is displayed when the input has no value.
73    pub fn placeholder(mut self, placeholder: &'a str) -> Self {
74        self.placeholder = placeholder;
75        self
76    }
77
78    /// Construct a new `Input` that renders the text as dots, for passwords.
79    pub fn password(mut self, password: bool) -> Self {
80        self.password = password;
81        self
82    }
83
84    /// Sets the current text value of the input.
85    pub fn val<N: AsRef<str>>(self, value: N) -> Input<'a, T, F, N> {
86        Input {
87            placeholder: self.placeholder,
88            password: self.password,
89            value,
90            on_change: self.on_change,
91            on_submit: self.on_submit,
92            trigger: self.trigger,
93        }
94    }
95
96    /// Sets the message to post when the text value should be changed to a new value.
97    pub fn on_change<N: Fn(String) -> T>(self, on_change: N) -> Input<'a, T, N, S> {
98        Input {
99            placeholder: self.placeholder,
100            password: self.password,
101            value: self.value,
102            on_change,
103            on_submit: self.on_submit,
104            trigger: self.trigger,
105        }
106    }
107
108    /// Sets the message to post when the users submits using the enter key
109    pub fn on_submit(mut self, message: T) -> Self {
110        self.on_submit.replace(message);
111        self
112    }
113
114    /// Sets a keyboard key that will trigger input focus
115    pub fn trigger_key(mut self, key: Key) -> Self {
116        self.trigger.replace(key);
117        self
118    }
119
120    fn text(&self, stylesheet: &Stylesheet) -> Text {
121        Text {
122            text: Cow::Borrowed(self.value.as_ref()),
123            font: stylesheet.font.clone(),
124            size: stylesheet.text_size,
125            wrap: TextWrap::NoWrap,
126            color: stylesheet.color,
127        }
128    }
129
130    fn placeholder_text(&self, stylesheet: &Stylesheet) -> Text {
131        Text {
132            text: Cow::Borrowed(self.placeholder),
133            font: stylesheet.font.clone(),
134            size: stylesheet.text_size,
135            wrap: TextWrap::NoWrap,
136            color: stylesheet.color.with_alpha(0.5),
137        }
138    }
139
140    fn content_rect(&self, layout: Rectangle, stylesheet: &Stylesheet) -> Rectangle {
141        layout.after_padding(stylesheet.padding)
142    }
143}
144
145impl<'a, T> Default for Input<'a, T, fn(String) -> T, &'static str> {
146    fn default() -> Self {
147        Self {
148            placeholder: "",
149            password: false,
150            value: "",
151            on_change: |_| panic!("on_change of `Input` must be set"),
152            on_submit: None,
153            trigger: None,
154        }
155    }
156}
157
158impl<'a, T, F, S> Widget<'a, T> for Input<'a, T, F, S>
159where
160    T: 'a + Send,
161    F: 'a + Send + Fn(String) -> T,
162    S: 'a + Send + AsRef<str>,
163{
164    type State = State;
165
166    fn mount(&self) -> Self::State {
167        State::default()
168    }
169
170    fn widget(&self) -> &'static str {
171        "input"
172    }
173
174    fn state(&self, state: &State) -> StateVec {
175        match state.inner {
176            InnerState::Dragging(_, _, _) => smallvec![StyleState::Focused],
177            InnerState::Focused(_, _, _) => smallvec![StyleState::Focused],
178            InnerState::Idle => StateVec::new(),
179        }
180    }
181
182    fn len(&self) -> usize {
183        0
184    }
185
186    fn visit_children(&mut self, _: &mut dyn FnMut(&mut dyn GenericNode<'a, T>)) {}
187
188    fn size(&self, _: &State, stylesheet: &Stylesheet) -> (Size, Size) {
189        match (stylesheet.width, stylesheet.height) {
190            (Size::Shrink, Size::Shrink) => {
191                let width = self.placeholder_text(stylesheet).measure(None).width()
192                    + stylesheet.padding.left
193                    + stylesheet.padding.right;
194                let metrics = stylesheet.font.inner.v_metrics(Scale::uniform(stylesheet.text_size));
195                let height = metrics.ascent - metrics.descent + stylesheet.padding.top + stylesheet.padding.bottom;
196                (Size::Exact(width), Size::Exact(height))
197            }
198
199            (Size::Shrink, other) => {
200                let width = self.placeholder_text(stylesheet).measure(None).width()
201                    + stylesheet.padding.left
202                    + stylesheet.padding.right;
203                (Size::Exact(width), other)
204            }
205
206            (other, Size::Shrink) => {
207                let metrics = stylesheet.font.inner.v_metrics(Scale::uniform(stylesheet.text_size));
208                let height = metrics.ascent - metrics.descent + stylesheet.padding.top + stylesheet.padding.bottom;
209                (other, Size::Exact(height))
210            }
211
212            other => other,
213        }
214    }
215
216    fn event(
217        &mut self,
218        state: &mut State,
219        layout: Rectangle,
220        clip: Rectangle,
221        stylesheet: &Stylesheet,
222        event: Event,
223        context: &mut Context<T>,
224    ) {
225        let content_rect = self.content_rect(layout, stylesheet);
226        let value_len = self.value.as_ref().chars().count();
227        let mut new_text = None;
228
229        // sanity check on the state
230        state.inner = match state.inner {
231            InnerState::Dragging(mut from, mut to, since) => {
232                if from > value_len {
233                    from = value_len;
234                }
235                if to > value_len {
236                    to = value_len;
237                }
238                InnerState::Dragging(from, to, since)
239            }
240            InnerState::Focused(mut from, mut to, since) => {
241                if from > value_len {
242                    from = value_len;
243                }
244                if to > value_len {
245                    to = value_len;
246                }
247                InnerState::Focused(from, to, since)
248            }
249            InnerState::Idle => InnerState::Idle,
250        };
251
252        //if context.cursor.inside(&current) {
253        //    context.style = MouseStyle::Text;
254        //}
255
256        // event related state update
257        match event {
258            Event::Cursor(x, y) => {
259                state.cursor = (x, y);
260                if let InnerState::Dragging(from, _, _) = state.inner {
261                    let relative_cursor = (
262                        state.cursor.0 - content_rect.left + state.scroll_x,
263                        state.cursor.1 - content_rect.top + state.scroll_y,
264                    );
265                    let hit =
266                        text_display(self.text(stylesheet), self.password).hitdetect(relative_cursor, content_rect);
267                    state.inner = InnerState::Dragging(from, hit, Instant::now());
268                    context.redraw();
269                }
270            }
271
272            Event::Modifiers(modifiers) => {
273                state.modifiers = modifiers;
274            }
275
276            Event::Press(Key::LeftMouseButton) => {
277                context.redraw();
278                if layout.point_inside(state.cursor.0, state.cursor.1)
279                    && clip.point_inside(state.cursor.0, state.cursor.1)
280                {
281                    let relative_cursor = (
282                        state.cursor.0 - content_rect.left + state.scroll_x,
283                        state.cursor.1 - content_rect.top + state.scroll_y,
284                    );
285                    let hit =
286                        text_display(self.text(stylesheet), self.password).hitdetect(relative_cursor, content_rect);
287                    state.inner = InnerState::Dragging(hit, hit, Instant::now());
288                } else {
289                    state.inner = InnerState::Idle;
290                }
291            }
292
293            Event::Release(Key::LeftMouseButton) => {
294                state.inner = match state.inner {
295                    InnerState::Dragging(from, to, since) => {
296                        context.redraw();
297                        InnerState::Focused(from, to, since)
298                    }
299                    other => other,
300                }
301            }
302
303            event => match state.inner {
304                InnerState::Idle => match event {
305                    Event::Press(key) if Some(key) == self.trigger => {
306                        state.inner = InnerState::Focused(0, self.value.as_ref().len(), Instant::now());
307                        context.redraw();
308                    }
309                    _ => (),
310                },
311
312                InnerState::Focused(from, to, _) => match event {
313                    Event::Text(c) => match c {
314                        BACKWARDS_DELETE => {
315                            context.redraw();
316                            let (from, to) = (from.min(to), from.max(to));
317
318                            if to > from {
319                                state.inner = InnerState::Focused(from, from, Instant::now());
320                                let (head, tail) = self.value.as_ref().split_at(codepoint(self.value.as_ref(), from));
321                                new_text.replace(format!("{}{}", head, tail.split_at(codepoint(tail, to - from)).1));
322                            } else if from > 0 {
323                                state.inner = InnerState::Focused(from - 1, from - 1, Instant::now());
324                                let (head, tail) =
325                                    self.value.as_ref().split_at(codepoint(self.value.as_ref(), from - 1));
326                                new_text.replace(format!("{}{}", head, tail.split_at(codepoint(tail, 1)).1));
327                            }
328                        }
329                        FORWARD_DELETE => {
330                            context.redraw();
331                            let (from, to) = (from.min(to), from.max(to));
332                            state.inner = InnerState::Focused(from, from, Instant::now());
333
334                            let (head, tail) = self.value.as_ref().split_at(codepoint(self.value.as_ref(), from));
335                            if to > from {
336                                new_text.replace(format!("{}{}", head, tail.split_at(codepoint(tail, to - from)).1));
337                            } else if !tail.is_empty() {
338                                new_text.replace(format!("{}{}", head, tail.split_at(codepoint(tail, 1)).1));
339                            }
340                        }
341                        c => {
342                            if !c.is_control() {
343                                context.redraw();
344                                let (from, to) = (from.min(to), from.max(to));
345                                state.inner = InnerState::Focused(from + 1, from + 1, Instant::now());
346
347                                let (head, tail) = self.value.as_ref().split_at(codepoint(self.value.as_ref(), from));
348                                if to > from {
349                                    new_text.replace(format!(
350                                        "{}{}{}",
351                                        head,
352                                        c,
353                                        tail.split_at(codepoint(tail, to - from)).1
354                                    ));
355                                } else {
356                                    new_text.replace(format!("{}{}{}", head, c, tail));
357                                }
358                            }
359                        }
360                    },
361
362                    Event::Press(Key::Enter) if self.on_submit.is_some() => {
363                        if !state.modifiers.shift {
364                            context.redraw();
365                            context.extend(self.on_submit.take());
366                            state.inner = InnerState::Idle;
367                        }
368                    }
369
370                    #[cfg(feature = "clipboard")]
371                    Event::Press(Key::C) => {
372                        if state.modifiers.command {
373                            let (a, b) = (
374                                codepoint(self.value.as_ref(), from.min(to)),
375                                codepoint(self.value.as_ref(), from.max(to)),
376                            );
377                            let copy_text = self.value.as_ref()[a..b].to_string();
378                            ClipboardContext::new()
379                                .and_then(|mut cc| cc.set_contents(copy_text))
380                                .ok();
381                        }
382                    }
383
384                    #[cfg(feature = "clipboard")]
385                    Event::Press(Key::X) => {
386                        if state.modifiers.command {
387                            context.redraw();
388                            let (from, to) = (from.min(to), from.max(to));
389                            let (a, b) = (codepoint(self.value.as_ref(), from), codepoint(self.value.as_ref(), to));
390                            let cut_text = self.value.as_ref()[a..b].to_string();
391                            ClipboardContext::new()
392                                .and_then(|mut cc| cc.set_contents(cut_text))
393                                .ok();
394
395                            state.inner = InnerState::Focused(from, from, Instant::now());
396                            let (head, tail) = self.value.as_ref().split_at(codepoint(self.value.as_ref(), from));
397                            if to > from {
398                                new_text.replace(format!("{}{}", head, tail.split_at(codepoint(tail, to - from)).1));
399                            } else if !tail.is_empty() {
400                                new_text.replace(format!("{}{}", head, tail.split_at(codepoint(tail, 1)).1));
401                            }
402                        }
403                    }
404
405                    #[cfg(feature = "clipboard")]
406                    Event::Press(Key::V) => {
407                        if state.modifiers.command {
408                            context.redraw();
409                            let (from, to) = (from.min(to), from.max(to));
410                            let paste_text = ClipboardContext::new().and_then(|mut cc| cc.get_contents()).ok();
411
412                            if let Some(paste_text) = paste_text {
413                                let (head, tail) = self.value.as_ref().split_at(codepoint(self.value.as_ref(), from));
414                                state.inner = InnerState::Focused(
415                                    from + paste_text.len(),
416                                    from + paste_text.len(),
417                                    Instant::now(),
418                                );
419                                if to > from {
420                                    new_text.replace(format!(
421                                        "{}{}{}",
422                                        head,
423                                        paste_text,
424                                        tail.split_at(codepoint(tail, to - from)).1
425                                    ));
426                                } else {
427                                    new_text.replace(format!("{}{}{}", head, paste_text, tail));
428                                }
429                            }
430                        }
431                    }
432
433                    Event::Press(Key::Left) => {
434                        context.redraw();
435                        if state.modifiers.command {
436                            if state.modifiers.shift {
437                                state.inner = InnerState::Focused(from, 0, Instant::now());
438                            } else {
439                                state.inner = InnerState::Focused(0, 0, Instant::now());
440                            }
441                        } else if state.modifiers.shift {
442                            state.inner = InnerState::Focused(from, if to > 0 { to - 1 } else { 0 }, Instant::now());
443                        } else {
444                            let (from, to) = (from.min(to), from.max(to));
445                            if from != to || from == 0 {
446                                state.inner = InnerState::Focused(from, from, Instant::now());
447                            } else {
448                                state.inner = InnerState::Focused(from - 1, from - 1, Instant::now());
449                            }
450                        }
451                    }
452
453                    Event::Press(Key::Right) => {
454                        context.redraw();
455                        if state.modifiers.command {
456                            if state.modifiers.shift {
457                                state.inner = InnerState::Focused(from, value_len, Instant::now());
458                            } else {
459                                state.inner = InnerState::Focused(value_len, value_len, Instant::now());
460                            }
461                        } else if state.modifiers.shift {
462                            state.inner = InnerState::Focused(from, (to + 1).min(value_len), Instant::now());
463                        } else {
464                            let (from, to) = (from.min(to), from.max(to));
465                            if from != to || to >= value_len {
466                                state.inner = InnerState::Focused(to, to, Instant::now());
467                            } else {
468                                state.inner = InnerState::Focused(to + 1, to + 1, Instant::now());
469                            }
470                        }
471                    }
472
473                    Event::Press(Key::Home) => {
474                        context.redraw();
475                        if state.modifiers.shift {
476                            state.inner = InnerState::Focused(from, 0, Instant::now());
477                        } else {
478                            state.inner = InnerState::Focused(0, 0, Instant::now());
479                        }
480                    }
481
482                    Event::Press(Key::End) => {
483                        context.redraw();
484                        if state.modifiers.shift {
485                            state.inner = InnerState::Focused(from, value_len, Instant::now());
486                        } else {
487                            state.inner = InnerState::Focused(value_len, value_len, Instant::now());
488                        }
489                    }
490
491                    _ => (),
492                },
493
494                _ => (),
495            },
496        }
497
498        // update scroll state for current text and caret position
499        match state.inner {
500            InnerState::Dragging(_, pos, _) | InnerState::Focused(_, pos, _) => {
501                let mut measure_text = Text {
502                    text: Cow::Borrowed(new_text.as_deref().unwrap_or_else(|| self.value.as_ref())),
503                    font: stylesheet.font.clone(),
504                    size: stylesheet.text_size,
505                    wrap: TextWrap::NoWrap,
506                    color: stylesheet.color,
507                };
508
509                let measure_text_len = measure_text.text.chars().count();
510
511                if self.password {
512                    measure_text.text = Cow::Owned("\u{25cf}".repeat(measure_text_len));
513                }
514
515                let (caret, range) = measure_text.measure_range(pos, measure_text_len, content_rect);
516
517                if state.scroll_x + content_rect.width() > range.0 + 2.0 {
518                    context.redraw();
519                    state.scroll_x = (range.0 - content_rect.width() + 2.0).max(0.0);
520                }
521                if caret.0 - state.scroll_x > content_rect.width() - 2.0 {
522                    context.redraw();
523                    state.scroll_x = caret.0 - content_rect.width() + 2.0;
524                }
525                if caret.0 - state.scroll_x < 0.0 {
526                    context.redraw();
527                    state.scroll_x = caret.0;
528                }
529                if caret.1 - state.scroll_y > content_rect.height() - 2.0 {
530                    context.redraw();
531                    state.scroll_y = caret.1 - content_rect.height() + 2.0;
532                }
533                if caret.1 - state.scroll_y < 0.0 {
534                    context.redraw();
535                    state.scroll_y = caret.1;
536                }
537            }
538            _ => (),
539        };
540
541        if let Some(new_text) = new_text {
542            context.push((self.on_change)(new_text));
543        }
544    }
545
546    fn draw(
547        &mut self,
548        state: &mut State,
549        layout: Rectangle,
550        clip: Rectangle,
551        stylesheet: &Stylesheet,
552    ) -> Vec<Primitive<'a>> {
553        let mut result = Vec::new();
554
555        let content_rect = self.content_rect(layout, stylesheet);
556        let text_rect = content_rect.translate(-state.scroll_x, -state.scroll_y);
557        let text = text_display(self.text(stylesheet), self.password);
558
559        result.extend(stylesheet.background.render(layout).into_iter());
560        if let Some(clip) = content_rect.intersect(&clip) {
561            result.push(Primitive::PushClip(clip));
562            match state.inner {
563                InnerState::Dragging(from, to, since) | InnerState::Focused(from, to, since) => {
564                    let range = text.measure_range(from.min(to), from.max(to), text_rect);
565
566                    if to != from {
567                        result.push(Primitive::DrawRect(
568                            Rectangle {
569                                left: text_rect.left + (range.0).0,
570                                right: text_rect.left + (range.1).0,
571                                top: text_rect.top,
572                                bottom: text_rect.bottom,
573                            },
574                            Color {
575                                r: 0.0,
576                                g: 0.0,
577                                b: 0.5,
578                                a: 0.5,
579                            },
580                        ));
581                    }
582
583                    if since.elapsed().subsec_nanos() < 500_000_000 {
584                        let caret = if to > from { range.1 } else { range.0 };
585
586                        result.push(Primitive::DrawRect(
587                            Rectangle {
588                                left: text_rect.left + caret.0,
589                                right: text_rect.left + caret.0 + 1.0,
590                                top: text_rect.top,
591                                bottom: text_rect.bottom,
592                            },
593                            Color {
594                                r: 0.0,
595                                g: 0.0,
596                                b: 0.0,
597                                a: 1.0,
598                            },
599                        ));
600                    }
601                }
602                _ => (),
603            }
604            if self.value.as_ref().is_empty() {
605                result.push(Primitive::DrawText(
606                    self.placeholder_text(stylesheet).to_owned(),
607                    text_rect,
608                ));
609            } else {
610                result.push(Primitive::DrawText(text, text_rect));
611            }
612            result.push(Primitive::PopClip);
613        }
614
615        result
616    }
617}
618
619impl<'a, T, F, S> IntoNode<'a, T> for Input<'a, T, F, S>
620where
621    T: 'a + Send,
622    F: 'a + Send + Fn(String) -> T,
623    S: 'a + Send + AsRef<str>,
624{
625    fn into_node(self) -> Node<'a, T> {
626        Node::from_widget(self)
627    }
628}
629
630impl Default for State {
631    fn default() -> Self {
632        State {
633            scroll_x: 0.0,
634            scroll_y: 0.0,
635            modifiers: Modifiers {
636                ctrl: false,
637                alt: false,
638                shift: false,
639                logo: false,
640                command: false,
641            },
642            inner: InnerState::Idle,
643            cursor: (0.0, 0.0),
644        }
645    }
646}
647
648impl State {
649    /// Returns whether the input state is currently focused and accepting input
650    pub fn is_focused(&self) -> bool {
651        matches!(self.inner, InnerState::Focused(_, _, _))
652    }
653}
654
655fn text_display(buffer: Text<'_>, password: bool) -> Text<'static> {
656    if password {
657        Text {
658            text: Cow::Owned("\u{25cf}".repeat(buffer.text.chars().count())),
659            font: buffer.font.clone(),
660            size: buffer.size,
661            color: buffer.color,
662            wrap: buffer.wrap,
663        }
664    } else {
665        buffer.to_owned()
666    }
667}
668
669fn codepoint(s: &str, char_index: usize) -> usize {
670    s.char_indices().nth(char_index).map_or(s.len(), |(i, _)| i)
671}