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
28pub 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
44pub 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 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 pub fn placeholder(mut self, placeholder: &'a str) -> Self {
74 self.placeholder = placeholder;
75 self
76 }
77
78 pub fn password(mut self, password: bool) -> Self {
80 self.password = password;
81 self
82 }
83
84 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 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 pub fn on_submit(mut self, message: T) -> Self {
110 self.on_submit.replace(message);
111 self
112 }
113
114 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 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 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 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 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}