use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::component::Component;
use tuirealm::props::{
AttrValue, Attribute, Borders, Color, LineStatic, Props, QueryResult, Style, TextModifiers,
TextStatic, Title,
};
use tuirealm::ratatui::Frame;
use tuirealm::ratatui::layout::Rect;
use tuirealm::ratatui::widgets::{List, ListItem, ListState};
use tuirealm::state::{State, StateValue};
use crate::prop_ext::CommonProps;
use crate::utils::borrow_clone_line;
#[derive(Default)]
pub struct TextareaStates {
pub list_index: usize,
pub list_len: usize,
}
impl TextareaStates {
pub fn set_list_len(&mut self, len: usize) {
self.list_len = len;
self.fix_list_index();
}
pub fn incr_list_index(&mut self) {
if self.list_index + 1 < self.list_len {
self.list_index += 1;
}
}
pub fn decr_list_index(&mut self) {
if self.list_index > 0 {
self.list_index -= 1;
}
}
pub fn fix_list_index(&mut self) {
if self.list_index >= self.list_len && self.list_len > 0 {
self.list_index = self.list_len - 1;
} else if self.list_len == 0 {
self.list_index = 0;
}
}
pub fn list_index_at_first(&mut self) {
self.list_index = 0;
}
pub fn list_index_at_last(&mut self) {
if self.list_len > 0 {
self.list_index = self.list_len - 1;
} else {
self.list_index = 0;
}
}
fn calc_max_step_ahead(&self, max: usize) -> usize {
let remaining: usize = match self.list_len {
0 => 0,
len => len - 1 - self.list_index,
};
if remaining > max { max } else { remaining }
}
fn calc_max_step_behind(&self, max: usize) -> usize {
if self.list_index > max {
max
} else {
self.list_index
}
}
}
#[derive(Default)]
#[must_use]
pub struct Textarea {
common: CommonProps,
props: Props,
pub states: TextareaStates,
}
impl Textarea {
pub fn foreground(mut self, fg: Color) -> Self {
self.attr(Attribute::Foreground, AttrValue::Color(fg));
self
}
pub fn background(mut self, bg: Color) -> Self {
self.attr(Attribute::Background, AttrValue::Color(bg));
self
}
pub fn modifiers(mut self, m: TextModifiers) -> Self {
self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
self
}
pub fn style(mut self, style: Style) -> Self {
self.attr(Attribute::Style, AttrValue::Style(style));
self
}
pub fn inactive(mut self, s: Style) -> Self {
self.attr(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
self
}
pub fn borders(mut self, b: Borders) -> Self {
self.attr(Attribute::Borders, AttrValue::Borders(b));
self
}
pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
self.attr(Attribute::Title, AttrValue::Title(title.into()));
self
}
pub fn step(mut self, step: usize) -> Self {
self.attr(Attribute::ScrollStep, AttrValue::Length(step));
self
}
pub fn highlight_str<S: Into<LineStatic>>(mut self, s: S) -> Self {
self.attr(Attribute::HighlightedStr, AttrValue::TextLine(s.into()));
self
}
pub fn text_rows<T>(self, text: impl IntoIterator<Item = T>) -> Self
where
T: Into<LineStatic>,
{
let text = TextStatic::from_iter(text);
self.text(text)
}
pub fn text(mut self, text: impl Into<TextStatic>) -> Self {
let text = text.into();
self.states.set_list_len(text.lines.len());
self.attr(Attribute::Text, AttrValue::Text(text));
self
}
}
impl Component for Textarea {
fn view(&mut self, render: &mut Frame, area: Rect) {
if !self.common.display {
return;
}
let hg_str = self
.props
.get(Attribute::HighlightedStr)
.and_then(|x| x.as_textline());
let wrap_width = (area.width as usize) - hg_str.as_ref().map_or(0, |x| x.width()) - 2;
let lines: Vec<ListItem> = self
.props
.get(Attribute::Text)
.and_then(AttrValue::as_text)
.map(|text| {
text.iter()
.map(|x| crate::utils::wrap_lines(&[x], wrap_width))
.map(ListItem::new)
.collect()
})
.unwrap_or_default();
let mut state: ListState = ListState::default();
state.select(Some(self.states.list_index));
let mut list = List::new(lines)
.direction(tuirealm::ratatui::widgets::ListDirection::TopToBottom)
.style(self.common.style);
if let Some(block) = self.common.get_block() {
list = list.block(block);
}
if let Some(hg_str) = hg_str {
list = list.highlight_symbol(borrow_clone_line(hg_str));
}
render.render_stateful_widget(list, area, &mut state);
}
fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
if let Some(value) = self.common.get_for_query(attr) {
return Some(value);
}
self.props.get_for_query(attr)
}
fn attr(&mut self, attr: Attribute, value: AttrValue) {
if let Some(value) = self.common.set(attr, value) {
self.props.set(attr, value);
self.states.set_list_len(
self.props
.get(Attribute::Text)
.and_then(AttrValue::as_text)
.map_or(0, |text| text.lines.len()),
);
self.states.fix_list_index();
}
}
fn state(&self) -> State {
State::Single(StateValue::Usize(self.states.list_index))
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
let prev = self.states.list_index;
match cmd {
Cmd::Move(Direction::Down) => {
self.states.incr_list_index();
}
Cmd::Move(Direction::Up) => {
self.states.decr_list_index();
}
Cmd::Scroll(Direction::Down) => {
let step = self
.props
.get(Attribute::ScrollStep)
.and_then(AttrValue::as_length)
.unwrap_or(8);
let step = self.states.calc_max_step_ahead(step);
(0..step).for_each(|_| self.states.incr_list_index());
}
Cmd::Scroll(Direction::Up) => {
let step = self
.props
.get(Attribute::ScrollStep)
.and_then(AttrValue::as_length)
.unwrap_or(8);
let step = self.states.calc_max_step_behind(step);
(0..step).for_each(|_| self.states.decr_list_index());
}
Cmd::GoTo(Position::Begin) => {
self.states.list_index_at_first();
}
Cmd::GoTo(Position::End) => {
self.states.list_index_at_last();
}
_ => return CmdResult::Invalid(cmd),
}
if prev != self.states.list_index {
CmdResult::Changed(self.state())
} else {
CmdResult::NoChange
}
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use tuirealm::props::HorizontalAlignment;
use tuirealm::ratatui::text::{Line, Span, Text};
use tuirealm::state::StateValue;
use super::*;
#[test]
fn test_components_textarea() {
let mut component = Textarea::default()
.foreground(Color::Red)
.background(Color::Blue)
.modifiers(TextModifiers::BOLD)
.borders(Borders::default())
.highlight_str("🚀")
.step(4)
.title(Title::from("textarea").alignment(HorizontalAlignment::Center))
.text_rows([Line::from("welcome to "), Line::from("tui-realm")]);
component.states.list_index += 1;
assert_eq!(component.states.list_index, 1);
component.attr(
Attribute::Text,
AttrValue::Text(TextStatic::from_iter([
Line::from("welcome"),
Line::from("to"),
Line::from("tui-realm"),
])),
);
assert_eq!(component.states.list_index, 1); assert_eq!(component.states.list_len, 3);
assert_eq!(component.state(), State::Single(StateValue::Usize(1)));
assert_eq!(component.states.list_index, 1);
assert_eq!(
component.perform(Cmd::Move(Direction::Down)),
CmdResult::Changed(State::Single(StateValue::Usize(2)))
);
assert_eq!(component.states.list_index, 2);
assert_eq!(
component.perform(Cmd::Move(Direction::Up)),
CmdResult::Changed(State::Single(StateValue::Usize(1)))
);
assert_eq!(component.states.list_index, 1);
assert_eq!(
component.perform(Cmd::Scroll(Direction::Down)),
CmdResult::Changed(State::Single(StateValue::Usize(2)))
);
assert_eq!(component.states.list_index, 2);
assert_eq!(
component.perform(Cmd::Scroll(Direction::Up)),
CmdResult::Changed(State::Single(StateValue::Usize(0)))
);
assert_eq!(component.states.list_index, 0);
assert_eq!(
component.perform(Cmd::GoTo(Position::End)),
CmdResult::Changed(State::Single(StateValue::Usize(2)))
);
assert_eq!(component.states.list_index, 2);
assert_eq!(
component.perform(Cmd::GoTo(Position::Begin)),
CmdResult::Changed(State::Single(StateValue::Usize(0)))
);
assert_eq!(component.states.list_index, 0);
assert_eq!(
component.perform(Cmd::GoTo(Position::Begin)),
CmdResult::NoChange
);
assert_eq!(
component.perform(Cmd::Delete),
CmdResult::Invalid(Cmd::Delete)
);
}
#[test]
fn various_textrows_types() {
let _ = Textarea::default().text_rows(vec![Span::raw("hello")]);
let _ = Textarea::default().text_rows([Span::raw("hello")]);
let _ = Textarea::default().text_rows(vec![Span::raw("hello")].into_boxed_slice());
let _ = Textarea::default().text_rows(["Hello"].map(Span::raw));
let _ = Textarea::default().text_rows(vec![Line::raw("hello")]);
let _ = Textarea::default().text_rows([Line::raw("hello")]);
let _ = Textarea::default().text_rows(vec![Line::raw("hello")].into_boxed_slice());
let _ = Textarea::default().text_rows(["Hello"].map(Line::raw));
}
#[test]
fn various_text_types() {
let _ = Textarea::default().text(Text::raw("hello"));
let _ = Textarea::default().text(Line::raw("hello"));
let _ = Textarea::default().text(Span::raw("hello"));
let _ = Textarea::default().text("hello");
let _ = Textarea::default().text("hello".to_string());
}
}