1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//! A module containing the `Input` component.

use std::cmp;

use tui::{text::Spans as TuiSpans, widgets::Paragraph};

use crate::{
  component,
  components::{stack::Flex::*, Empty, Section, VStack},
  element::{Any as AnyElement, Element},
  event::{KeyHandler, MouseHandler},
  on_key, render,
  state::use_state,
  style::Style,
  terminal::{Frame, Rect},
  text::Spans,
};

/// A single-line input component with cursor controls.
///
/// This is a smart input box with the following features:
///   - rendering a cursor
///   - scrolling on overflow
///   - supports navigating with arrow keys
///   - supports navigating with `ctrl+a` and `ctrl+e`
///   - has a fixed single-line height of 3 rows
///
/// ## Vertical Alignment
///
/// Since this component always renders as three blocks high,
/// when more space is available it vertically centers itself.
/// In order to align this element to the top, you will need
/// to use [`VStack`] along with [`Flex::Block`] and properly
/// route the key events:
///
/// ```rust
/// # use intuitive::{
/// #   component,
/// #   components::{stack::Flex::*, Embed, Empty, experimental::input::Input, VStack},
/// #   element,
/// #   error::Result,
/// #   on_key, render,
/// #   terminal::Terminal,
/// # };
/// #
/// #[component(Root)]
/// fn render() {
///   let input: element::Any = render! {
///     Input(title: "Input Box")
///   };
///
///   let on_key = on_key! { [input]
///     KeyEvent { code: Esc, .. } => event::quit(),
///     event => input.on_key(event),
///   };
///
///   render! {
///     VStack(flex: [Block(3), Grow(1)], on_key) {
///       Embed(content: input)
///       Empty()
///     }
///   }
/// }
/// ```
///
/// [`VStack`]: ../../struct.VStack.html
/// [`Flex::Block`]: ../../stack/enum.Flex.html#variant.Block
#[component(Input)]
pub fn render(title: Spans, border: Style, on_key: KeyHandler, on_mouse: MouseHandler) {
  let cursor = use_state(|| 0usize);
  let text = use_state(|| String::new());

  let on_key = on_key.then(on_key! { [cursor, text]
    KeyEvent { code: Char('a'), modifiers: KeyModifiers::CONTROL, .. } => cursor.set(0),
    KeyEvent { code: Char('e'), modifiers: KeyModifiers::CONTROL, .. } => cursor.set(text.get().len()),

    KeyEvent { code: Left, .. } => {
      cursor.update(|cursor| cursor.saturating_sub(1));
    },

    KeyEvent { code: Right, .. } => {
      cursor.update(|cursor| cmp::min(cursor + 1, text.get().len()));
    },

    KeyEvent { code: Char(c), .. } => {
      text.mutate(|text| text.insert(cursor.get(), c));
      cursor.update(|cursor| cursor + 1);
    },

    KeyEvent { code: Backspace, .. } => {
      if cursor.get() > 0 && text.get().len() > 0 {
        text.mutate(|text| text.remove(cursor.get() - 1));
        cursor.update(|cursor| cursor - 1);
      }
    },
  });

  // TODO(enricozb): consider adding (Vertical)Alignment as a property, that will
  // align the input box to the top/middle/bottom.
  render! {
    VStack(flex: [Grow(1), Block(3), Grow(1)], on_key) {
      Empty()
      Section(title, border, on_mouse) {
        Inner(cursor: cursor.get(), text: text.get())
      }
      Empty()
    }
  }
}

#[component(Inner)]
fn render(cursor: usize, text: String) {
  AnyElement::new(Frozen {
    cursor: *cursor as u16,
    text: text.clone(),
  })
}

struct Frozen {
  cursor: u16,
  text: String,
}

impl Element for Frozen {
  fn draw(&self, rect: Rect, frame: &mut Frame) {
    let (text, cursor) = if self.cursor < rect.width {
      (self.text.clone().into(), rect.x + self.cursor)
    } else {
      let offset = (self.cursor - rect.width) as usize + 1;
      (self.text[offset..].into(), rect.right() - 1)
    };

    let widget = Paragraph::new::<TuiSpans>(text);
    frame.render_widget(widget, rect);
    frame.set_cursor(cursor, rect.y);
  }
}