use ropey::Rope;
use unicode_width::UnicodeWidthStr;
use crate::{
layout::Layout,
text::{cursor, CharIndex, TextStorage},
AnyCharacter, Bindings, Callback, Canvas, Colour, Component, ComponentLink, Key, Rect,
ShouldRender, Style,
};
pub use crate::text::Cursor;
#[derive(Clone, PartialEq)]
pub struct InputProperties {
pub style: InputStyle,
pub content: Rope,
pub cursor: Cursor,
pub on_change: Option<Callback<InputChange>>,
pub focused: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct InputStyle {
pub content: Style,
pub cursor: Style,
}
impl Default for InputStyle {
fn default() -> Self {
const DARK0_SOFT: Colour = Colour::rgb(50, 48, 47);
const LIGHT2: Colour = Colour::rgb(213, 196, 161);
const BRIGHT_BLUE: Colour = Colour::rgb(131, 165, 152);
Self {
content: Style::normal(DARK0_SOFT, LIGHT2),
cursor: Style::normal(BRIGHT_BLUE, DARK0_SOFT),
}
}
}
#[derive(Clone, Debug)]
pub struct InputChange {
pub content: Option<Rope>,
pub cursor: Cursor,
}
pub struct Input {
properties: InputProperties,
frame: Rect,
}
impl Component for Input {
type Message = Message;
type Properties = InputProperties;
fn create(properties: Self::Properties, frame: Rect, _link: ComponentLink<Self>) -> Self {
let mut content = properties.content.clone();
cursor::ensure_trailing_newline_with_content(&mut content);
Self { properties, frame }
}
fn change(&mut self, properties: Self::Properties) -> ShouldRender {
if self.properties != properties {
self.properties = properties;
ShouldRender::Yes
} else {
ShouldRender::No
}
}
fn resize(&mut self, frame: Rect) -> ShouldRender {
self.frame = frame;
ShouldRender::Yes
}
fn update(&mut self, message: Self::Message) -> ShouldRender {
let mut cursor = self.properties.cursor.clone();
let mut content_change = None;
match message {
Message::CursorLeft => {
cursor.move_left(&self.properties.content);
}
Message::CursorRight => {
cursor.move_right(&self.properties.content);
}
Message::StartOfLine => {
cursor.move_to_start_of_line(&self.properties.content);
}
Message::EndOfLine => {
cursor.move_to_end_of_buffer(&self.properties.content);
}
Message::InsertChar(character) => {
let mut new_content = self.properties.content.clone();
cursor.insert_char(&mut new_content, character);
cursor.move_right(&new_content);
content_change = Some(new_content);
}
Message::DeleteBackward => {
let mut new_content = self.properties.content.clone();
cursor.backspace(&mut new_content);
content_change = Some(new_content);
}
Message::DeleteForward => {
let mut new_content = self.properties.content.clone();
cursor.delete(&mut new_content);
content_change = Some(new_content);
}
}
if let Some(on_change) = self.properties.on_change.as_mut() {
on_change.emit(InputChange {
cursor,
content: content_change,
});
}
ShouldRender::Yes
}
fn view(&self) -> Layout {
let Self {
properties:
InputProperties {
ref content,
ref cursor,
ref style,
..
},
..
} = *self;
let mut canvas = Canvas::new(self.frame.size);
canvas.clear(style.content);
let mut char_offset = 0;
let mut visual_offset = 0;
for grapheme in content.graphemes() {
let len_chars = grapheme.len_chars();
let grapheme = grapheme.as_str().unwrap();
let grapheme_width = UnicodeWidthStr::width(grapheme);
canvas.draw_str(
visual_offset,
0,
if cursor.range().contains(&CharIndex(char_offset)) {
style.cursor
} else {
style.content
},
if grapheme_width > 0 { grapheme } else { " " },
);
visual_offset += grapheme_width;
char_offset += len_chars;
}
canvas.into()
}
fn bindings(&self, bindings: &mut Bindings<Self>) {
bindings.set_focus(self.properties.focused);
if !bindings.is_empty() {
return;
}
bindings
.command("left", || Message::CursorLeft)
.with([Key::Ctrl('b')])
.with([Key::Left]);
bindings
.command("right", || Message::CursorRight)
.with([Key::Ctrl('f')])
.with([Key::Right]);
bindings
.command("start-of-line", || Message::StartOfLine)
.with([Key::Ctrl('a')])
.with([Key::Home]);
bindings
.command("end-of-line", || Message::EndOfLine)
.with([Key::Ctrl('e')])
.with([Key::End]);
bindings
.command("delete-forward", || Message::DeleteForward)
.with([Key::Ctrl('d')])
.with([Key::Delete]);
bindings.add("delete-backward", [Key::Backspace], || {
Message::DeleteBackward
});
bindings.add(
"insert-character",
AnyCharacter,
|keys: &[Key]| match keys {
&[Key::Char(character)]
if character != '\n' && character != '\r' && character != '\t' =>
{
Some(Message::InsertChar(character))
}
_ => None,
},
);
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Message {
CursorLeft,
CursorRight,
InsertChar(char),
DeleteBackward,
DeleteForward,
StartOfLine,
EndOfLine,
}