#[cfg(feature = "clipboard")]
use cli_clipboard::{ClipboardContext, ClipboardProvider};
use ratatui::text::Span;
use ratatui::widgets::Widget;
use tui_textarea::{CursorMove, TextArea as TextAreaWidget};
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::props::{
Alignment, AttrValue, Attribute, Borders, PropPayload, PropValue, Props, Style, TextModifiers,
};
use tuirealm::ratatui::layout::Rect;
use tuirealm::ratatui::widgets::Block;
use tuirealm::{Frame, MockComponent, State, StateValue};
pub const TEXTAREA_CURSOR_LINE_STYLE: &str = "cursor-line-style";
pub const TEXTAREA_CURSOR_STYLE: &str = "cursor-style";
pub const TEXTAREA_LINE_NUMBER_STYLE: &str = "line-number-style";
pub const TEXTAREA_MAX_HISTORY: &str = "max-history";
pub const TEXTAREA_TAB_SIZE: &str = "tab-size";
pub const TEXTAREA_HARD_TAB: &str = "hard-tab";
pub const TEXTAREA_SINGLE_LINE: &str = "single-line";
pub const TEXTAREA_LAYOUT_MARGIN: &str = "layout-margin";
pub const INACTIVE_BORDERS: &str = "inactive-borders";
pub const TITLE_STYLE: &str = "title-style";
pub const TEXTAREA_CMD_NEWLINE: &str = "0";
pub const TEXTAREA_CMD_DEL_LINE_BY_END: &str = "1";
pub const TEXTAREA_CMD_DEL_LINE_BY_HEAD: &str = "2";
pub const TEXTAREA_CMD_DEL_WORD: &str = "3";
pub const TEXTAREA_CMD_DEL_NEXT_WORD: &str = "4";
pub const TEXTAREA_CMD_MOVE_WORD_FORWARD: &str = "5";
pub const TEXTAREA_CMD_MOVE_WORD_BACK: &str = "6";
pub const TEXTAREA_CMD_MOVE_PARAGRAPH_FORWARD: &str = "7";
pub const TEXTAREA_CMD_MOVE_PARAGRAPH_BACK: &str = "8";
pub const TEXTAREA_CMD_MOVE_TOP: &str = "9";
pub const TEXTAREA_CMD_MOVE_BOTTOM: &str = "a";
pub const TEXTAREA_CMD_UNDO: &str = "b";
pub const TEXTAREA_CMD_REDO: &str = "c";
#[cfg(feature = "clipboard")]
pub const TEXTAREA_CMD_PASTE: &str = "d";
pub const TEXTAREA_CMD_CLEAR: &str = "e";
pub struct TextArea<'a> {
props: Props,
widget: TextAreaWidget<'a>,
single_line: bool,
}
impl<I> From<I> for TextArea<'_>
where
I: IntoIterator,
I::Item: Into<String>,
{
fn from(i: I) -> Self {
Self::new(i.into_iter().map(|s| s.into()).collect::<Vec<String>>())
}
}
impl Default for TextArea<'_> {
fn default() -> Self {
Self::new(Vec::default())
}
}
impl<'a> TextArea<'a> {
pub fn new(lines: Vec<String>) -> Self {
Self {
props: Props::default(),
widget: TextAreaWidget::new(lines),
single_line: false,
}
}
pub fn inactive(mut self, s: Style) -> Self {
self.attr(Attribute::FocusStyle, AttrValue::Style(s));
self
}
pub fn borders(mut self, b: Borders) -> Self {
self.attr(Attribute::Borders, AttrValue::Borders(b));
self
}
pub fn title<S: AsRef<str>>(mut self, t: S, a: Alignment) -> Self {
self.attr(
Attribute::Title,
AttrValue::Title((t.as_ref().to_string(), a)),
);
self
}
pub fn scroll_step(mut self, step: usize) -> Self {
self.attr(Attribute::ScrollStep, AttrValue::Length(step));
self
}
pub fn max_histories(mut self, max: usize) -> Self {
self.attr(
Attribute::Custom(TEXTAREA_MAX_HISTORY),
AttrValue::Payload(PropPayload::One(PropValue::Usize(max))),
);
self
}
pub fn cursor_style(mut self, s: Style) -> Self {
self.attr(
Attribute::Custom(TEXTAREA_CURSOR_STYLE),
AttrValue::Style(s),
);
self
}
pub fn cursor_line_style(mut self, s: Style) -> Self {
self.attr(
Attribute::Custom(TEXTAREA_CURSOR_LINE_STYLE),
AttrValue::Style(s),
);
self
}
pub fn line_number_style(mut self, s: Style) -> Self {
self.attr(
Attribute::Custom(TEXTAREA_LINE_NUMBER_STYLE),
AttrValue::Style(s),
);
self
}
pub fn style(mut self, s: Style) -> Self {
self.attr(Attribute::Style, AttrValue::Style(s));
self
}
pub fn tab_length(mut self, l: u8) -> Self {
self.attr(
Attribute::Custom(TEXTAREA_TAB_SIZE),
AttrValue::Size(l as u16),
);
self
}
pub fn hard_tab(mut self, enabled: bool) -> Self {
self.attr(
Attribute::Custom(TEXTAREA_HARD_TAB),
AttrValue::Flag(enabled),
);
self
}
pub fn single_line(mut self, single_line: bool) -> Self {
self.attr(
Attribute::Custom(TEXTAREA_SINGLE_LINE),
AttrValue::Flag(single_line),
);
self
}
pub fn layout_margin(mut self, margin: u16) -> Self {
self.attr(
Attribute::Custom(TEXTAREA_LAYOUT_MARGIN),
AttrValue::Size(margin),
);
self
}
fn get_block(&self) -> Option<Block<'a>> {
let mut block = Block::default();
if let Some(AttrValue::Title((title, alignment))) = self.query(Attribute::Title) {
let title_style = self
.props
.get_or(
Attribute::Custom(TITLE_STYLE),
AttrValue::Style(Style::new()),
)
.unwrap_style();
let title = Span::from(title).style(title_style);
block = block.title(title).title_alignment(alignment);
}
let focus = self
.props
.get_or(Attribute::Focus, AttrValue::Flag(false))
.unwrap_flag();
if focus {
if let Some(AttrValue::Borders(borders)) = self.query(Attribute::Borders) {
return Some(
block
.border_style(borders.style())
.border_type(borders.modifiers)
.borders(borders.sides),
);
}
} else if let Some(AttrValue::Borders(borders)) =
self.query(Attribute::Custom(INACTIVE_BORDERS))
{
return Some(
block
.border_style(borders.style())
.border_type(borders.modifiers)
.borders(borders.sides),
);
}
None
}
#[cfg(feature = "clipboard")]
fn paste(&mut self) {
if let Ok(Ok(yank)) = ClipboardContext::new().map(|mut ctx| ctx.get_contents()) {
if self.single_line {
self.widget.insert_str(yank);
} else {
for line in yank.lines() {
self.widget.insert_str(line);
self.widget.insert_newline();
}
}
}
}
}
impl MockComponent for TextArea<'_> {
fn view(&mut self, frame: &mut Frame, mut area: Rect) {
if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
let focus = self
.props
.get_or(Attribute::Focus, AttrValue::Flag(false))
.unwrap_flag();
if !focus {
self.widget.set_cursor_style(Style::reset());
} else {
let style = self
.props
.get_or(
Attribute::Custom(TEXTAREA_CURSOR_STYLE),
AttrValue::Style(Style::default().add_modifier(TextModifiers::REVERSED)),
)
.unwrap_style();
self.widget.set_cursor_style(style);
}
area.x += 3;
area.y += 1;
area.height -= 2;
area.width -= 4;
self.widget.set_block(Block::new());
frame.render_widget(&self.widget, area);
area.x -= 3;
let span = Span::raw(" > ");
span.render(area, frame.buffer_mut());
area.y -= 1;
area.width += 4;
area.height += 2;
if let Some(block) = self.get_block() {
block.render(area, frame.buffer_mut());
}
}
}
fn query(&self, attr: Attribute) -> Option<AttrValue> {
self.props.get(attr)
}
fn attr(&mut self, attr: Attribute, value: AttrValue) {
self.props.set(attr, value.clone());
match (attr, value) {
(Attribute::Custom(TEXTAREA_CURSOR_STYLE), AttrValue::Style(s)) => {
self.widget.set_cursor_style(s);
}
(Attribute::Custom(TEXTAREA_CURSOR_LINE_STYLE), AttrValue::Style(s)) => {
self.widget.set_cursor_line_style(s);
}
(
Attribute::Custom(TEXTAREA_MAX_HISTORY),
AttrValue::Payload(PropPayload::One(PropValue::Usize(max))),
) => {
self.widget.set_max_histories(max);
}
(Attribute::Custom(TEXTAREA_LINE_NUMBER_STYLE), AttrValue::Style(s)) => {
self.widget.set_line_number_style(s);
}
(Attribute::Custom(TEXTAREA_TAB_SIZE), AttrValue::Size(size)) => {
self.widget.set_tab_length(size as u8);
}
(Attribute::Custom(TEXTAREA_HARD_TAB), AttrValue::Flag(enabled)) => {
self.widget.set_hard_tab_indent(enabled);
}
(Attribute::Custom(TEXTAREA_SINGLE_LINE), AttrValue::Flag(single_line)) => {
self.single_line = single_line;
}
(Attribute::Style, AttrValue::Style(s)) => {
self.widget.set_style(s);
}
(_, _) => {
if let Some(block) = self.get_block() {
self.widget.set_block(block);
}
}
}
}
fn state(&self) -> State {
State::Vec(
self.widget
.lines()
.iter()
.map(|x| StateValue::String(x.to_string()))
.collect(),
)
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
match cmd {
Cmd::Cancel => {
self.widget.delete_next_char();
CmdResult::None
}
Cmd::Custom(TEXTAREA_CMD_CLEAR) => {
self.widget.select_all();
self.widget.cut();
CmdResult::None
}
Cmd::Custom(TEXTAREA_CMD_DEL_LINE_BY_END) => {
self.widget.delete_line_by_end();
CmdResult::None
}
Cmd::Custom(TEXTAREA_CMD_DEL_LINE_BY_HEAD) => {
self.widget.delete_line_by_head();
CmdResult::None
}
Cmd::Custom(TEXTAREA_CMD_DEL_NEXT_WORD) => {
self.widget.delete_next_word();
CmdResult::None
}
Cmd::Custom(TEXTAREA_CMD_DEL_WORD) => {
self.widget.delete_word();
CmdResult::None
}
Cmd::Custom(TEXTAREA_CMD_MOVE_PARAGRAPH_BACK) => {
self.widget.move_cursor(CursorMove::ParagraphBack);
CmdResult::None
}
Cmd::Custom(TEXTAREA_CMD_MOVE_PARAGRAPH_FORWARD) => {
self.widget.move_cursor(CursorMove::ParagraphForward);
CmdResult::None
}
Cmd::Custom(TEXTAREA_CMD_MOVE_WORD_BACK) => {
self.widget.move_cursor(CursorMove::WordBack);
CmdResult::None
}
Cmd::Custom(TEXTAREA_CMD_MOVE_WORD_FORWARD) => {
self.widget.move_cursor(CursorMove::WordForward);
CmdResult::None
}
Cmd::Custom(TEXTAREA_CMD_MOVE_BOTTOM) => {
if !self.single_line {
self.widget.move_cursor(CursorMove::Bottom);
}
CmdResult::None
}
Cmd::Custom(TEXTAREA_CMD_MOVE_TOP) => {
if !self.single_line {
self.widget.move_cursor(CursorMove::Top);
}
CmdResult::None
}
#[cfg(feature = "clipboard")]
Cmd::Custom(TEXTAREA_CMD_PASTE) => {
self.paste();
CmdResult::None
}
Cmd::Custom(TEXTAREA_CMD_REDO) => {
self.widget.redo();
CmdResult::None
}
Cmd::Custom(TEXTAREA_CMD_UNDO) => {
self.widget.undo();
CmdResult::None
}
Cmd::Delete => {
self.widget.delete_char();
CmdResult::None
}
Cmd::GoTo(Position::Begin) => {
self.widget.move_cursor(CursorMove::Head);
CmdResult::None
}
Cmd::GoTo(Position::End) => {
self.widget.move_cursor(CursorMove::End);
CmdResult::None
}
Cmd::Move(Direction::Down) => {
if !self.single_line {
self.widget.move_cursor(CursorMove::Down);
}
CmdResult::None
}
Cmd::Move(Direction::Left) => {
self.widget.move_cursor(CursorMove::Back);
CmdResult::None
}
Cmd::Move(Direction::Right) => {
self.widget.move_cursor(CursorMove::Forward);
CmdResult::None
}
Cmd::Move(Direction::Up) => {
if !self.single_line {
self.widget.move_cursor(CursorMove::Up);
}
CmdResult::None
}
Cmd::Scroll(Direction::Down) => {
if !self.single_line {
let step = self
.props
.get_or(Attribute::ScrollStep, AttrValue::Length(8))
.unwrap_length();
(0..step).for_each(|_| self.widget.move_cursor(CursorMove::Down));
}
CmdResult::None
}
Cmd::Scroll(Direction::Up) => {
if !self.single_line {
let step = self
.props
.get_or(Attribute::ScrollStep, AttrValue::Length(8))
.unwrap_length();
(0..step).for_each(|_| self.widget.move_cursor(CursorMove::Up));
}
CmdResult::None
}
Cmd::Type('\t') => {
self.widget.insert_tab();
CmdResult::None
}
Cmd::Type('\n') | Cmd::Custom(TEXTAREA_CMD_NEWLINE) => {
if !self.single_line {
self.widget.insert_newline();
}
CmdResult::None
}
Cmd::Type(ch) => {
self.widget.insert_char(ch);
CmdResult::None
}
Cmd::Submit => CmdResult::Submit(self.state()),
_ => CmdResult::None,
}
}
}