mod cursor;
mod edit;
mod history;
mod input;
mod render;
mod wrap;
#[cfg(test)]
mod tests;
use std::cell::Cell;
use ratatui::style::{Modifier, Style};
pub use cursor::CursorMove;
pub use input::{Input, Key};
use history::History;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(super) struct Viewport {
pub row: usize,
pub col: usize,
pub height: u16,
pub width: u16,
}
#[derive(Debug, Clone)]
pub struct TextArea {
lines: Vec<String>,
cursor: (usize, usize),
selection_anchor: Option<(usize, usize)>,
yank: String,
history: History,
style: Style,
cursor_style: Style,
selection_style: Style,
cursor_visible: bool,
tab_len: usize,
wrap: bool,
viewport: Cell<Viewport>,
}
impl Default for TextArea {
fn default() -> Self {
Self::new()
}
}
impl TextArea {
pub fn new() -> Self {
Self {
lines: vec![String::new()],
cursor: (0, 0),
selection_anchor: None,
yank: String::new(),
history: History::default(),
style: Style::default(),
cursor_style: Style::default().add_modifier(Modifier::REVERSED),
selection_style: Style::default().add_modifier(Modifier::REVERSED),
cursor_visible: true,
tab_len: 4,
wrap: false,
viewport: Cell::new(Viewport::default()),
}
}
#[cfg(test)]
pub fn from_text(text: &str) -> Self {
let mut ta = Self::new();
ta.set_text(text);
ta
}
pub fn set_text(&mut self, text: &str) {
self.lines = split_lines(text);
let row = self.lines.len() - 1;
let col = char_count(&self.lines[row]);
self.cursor = (row, col);
self.selection_anchor = None;
self.history.clear();
let size = self.viewport.get();
self.viewport.set(Viewport {
width: size.width,
height: size.height,
..Viewport::default()
});
}
pub fn clear(&mut self) {
self.set_text("");
}
pub fn lines(&self) -> &[String] {
&self.lines
}
pub fn text(&self) -> String {
self.lines.join("\n")
}
pub fn line_count(&self) -> usize {
self.lines.len()
}
pub fn line(&self, row: usize) -> Option<&str> {
self.lines.get(row).map(String::as_str)
}
#[cfg(test)]
pub fn is_empty(&self) -> bool {
self.lines.len() == 1 && self.lines[0].is_empty()
}
pub fn cursor(&self) -> (usize, usize) {
self.cursor
}
#[cfg(test)]
pub fn set_cursor(&mut self, row: usize, col: usize) {
self.selection_anchor = None;
self.cursor = self.clamp_position((row, col));
}
pub fn selection(&self) -> Option<((usize, usize), (usize, usize))> {
let anchor = self.selection_anchor?;
if anchor == self.cursor {
return None;
}
Some(if anchor <= self.cursor {
(anchor, self.cursor)
} else {
(self.cursor, anchor)
})
}
#[cfg(test)]
pub fn start_selection(&mut self) {
self.selection_anchor = Some(self.cursor);
}
pub fn cancel_selection(&mut self) {
self.selection_anchor = None;
}
pub fn select_all(&mut self) {
self.selection_anchor = Some((0, 0));
let row = self.lines.len() - 1;
self.cursor = (row, char_count(&self.lines[row]));
}
#[cfg(test)]
pub fn yanked_text(&self) -> &str {
&self.yank
}
pub fn set_style(&mut self, style: Style) {
self.style = style;
}
pub fn set_cursor_style(&mut self, style: Style) {
self.cursor_style = style;
}
pub fn set_selection_style(&mut self, style: Style) {
self.selection_style = style;
}
pub fn set_cursor_visible(&mut self, visible: bool) {
self.cursor_visible = visible;
}
#[cfg(test)]
pub fn set_tab_len(&mut self, len: usize) {
self.tab_len = len.max(1);
}
pub fn scroll_offsets(&self) -> (usize, usize) {
let vp = self.viewport.get();
(vp.row, vp.col)
}
fn line_len(&self, row: usize) -> usize {
self.lines.get(row).map(|l| char_count(l)).unwrap_or(0)
}
fn clamp_position(&self, (row, col): (usize, usize)) -> (usize, usize) {
let row = row.min(self.lines.len() - 1);
(row, col.min(self.line_len(row)))
}
}
fn split_lines(text: &str) -> Vec<String> {
if text.is_empty() {
return vec![String::new()];
}
text.replace("\r\n", "\n")
.split('\n')
.map(|s| s.replace('\r', ""))
.collect()
}
fn char_count(line: &str) -> usize {
line.chars().count()
}
fn byte_of_char(line: &str, col: usize) -> usize {
line.char_indices()
.nth(col)
.map(|(i, _)| i)
.unwrap_or(line.len())
}