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);
}
}