use crate::component::{Component, EventCx};
use crate::event::{Event, Key};
use crate::geom::{Pos, Rect, Size};
use crate::render::RenderCx;
use crate::style::{Color, Style};
use crate::text::Text;
const MAX_UNDO: usize = 100;
pub struct TextArea {
lines: Vec<String>,
cursor: (usize, usize), scroll_x: u16,
scroll_y: u16,
width: u16,
height: u16,
show_line_numbers: bool,
rect: Rect,
focused: bool,
style: Style,
focus_style: Style,
line_number_style: Style,
#[allow(dead_code)]
selection_style: Style,
undo_stack: Vec<(Vec<String>, (usize, usize))>,
redo_stack: Vec<(Vec<String>, (usize, usize))>,
}
impl TextArea {
pub fn new() -> Self {
Self {
lines: vec![String::new()],
cursor: (0, 0),
scroll_x: 0,
scroll_y: 0,
width: 40,
height: 10,
show_line_numbers: false,
rect: Rect::default(),
focused: false,
style: Style::default(),
focus_style: Style::default().bg(Color::White).fg(Color::Black),
line_number_style: Style::default().fg(Color::Gray),
selection_style: Style::default().bg(Color::White).fg(Color::Black),
undo_stack: Vec::new(),
redo_stack: Vec::new(),
}
}
pub fn text(mut self, text: impl Into<Text>) -> Self {
let text = text.into();
self.lines = text
.lines
.iter()
.map(|l| {
l.spans
.iter()
.map(|s| s.text.clone())
.collect::<Vec<_>>()
.join("")
})
.collect();
if self.lines.is_empty() {
self.lines = vec![String::new()];
}
self.cursor = (0, 0);
self
}
pub fn width(mut self, w: u16) -> Self {
self.width = w;
self
}
pub fn height(mut self, h: u16) -> Self {
self.height = h;
self
}
pub fn show_line_numbers(mut self, show: bool) -> Self {
self.show_line_numbers = show;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn focus_style(mut self, style: Style) -> Self {
self.focus_style = style;
self
}
pub fn text_content(&self) -> String {
self.lines.join("\n")
}
pub fn cursor_pos(&self) -> (usize, usize) {
self.cursor
}
fn save_undo(&mut self) {
if self.undo_stack.len() >= MAX_UNDO {
self.undo_stack.remove(0);
}
self.undo_stack.push((self.lines.clone(), self.cursor));
self.redo_stack.clear();
}
fn clamp_cursor(&mut self) {
if self.cursor.0 >= self.lines.len() {
self.cursor.0 = self.lines.len().saturating_sub(1);
}
let line_len = self.lines[self.cursor.0].len();
if self.cursor.1 > line_len {
self.cursor.1 = line_len;
}
}
fn move_left(&mut self) {
if self.cursor.1 > 0 {
self.cursor.1 -= 1;
} else if self.cursor.0 > 0 {
self.cursor.0 -= 1;
self.cursor.1 = self.lines[self.cursor.0].len();
}
}
fn move_right(&mut self) {
if self.cursor.1 < self.lines[self.cursor.0].len() {
self.cursor.1 += 1;
} else if self.cursor.0 + 1 < self.lines.len() {
self.cursor.0 += 1;
self.cursor.1 = 0;
}
}
fn move_up(&mut self) {
if self.cursor.0 > 0 {
self.cursor.0 -= 1;
let line_len = self.lines[self.cursor.0].len();
if self.cursor.1 > line_len {
self.cursor.1 = line_len;
}
}
}
fn move_down(&mut self) {
if self.cursor.0 + 1 < self.lines.len() {
self.cursor.0 += 1;
let line_len = self.lines[self.cursor.0].len();
if self.cursor.1 > line_len {
self.cursor.1 = line_len;
}
}
}
fn move_home(&mut self) {
self.cursor.1 = 0;
}
fn move_end(&mut self) {
self.cursor.1 = self.lines[self.cursor.0].len();
}
fn move_page_up(&mut self, page_size: usize) {
for _ in 0..page_size {
self.move_up();
}
}
fn move_page_down(&mut self, page_size: usize) {
for _ in 0..page_size {
self.move_down();
}
}
fn insert_char(&mut self, c: char) {
self.save_undo();
self.lines[self.cursor.0].insert(self.cursor.1, c);
self.cursor.1 = (self.cursor.1 + c.len_utf8()).min(self.lines[self.cursor.0].len());
}
fn delete_backward(&mut self) {
if self.cursor.1 > 0 {
self.save_undo();
if let Some(idx) = self.prev_char_boundary() {
self.lines[self.cursor.0].remove(idx);
self.cursor.1 = idx;
}
} else if self.cursor.0 > 0 {
self.save_undo();
let rest = self.lines.remove(self.cursor.0);
self.cursor.0 -= 1;
self.cursor.1 = self.lines[self.cursor.0].len();
self.lines[self.cursor.0].push_str(&rest);
}
}
fn delete_forward(&mut self) {
if self.cursor.1 < self.lines[self.cursor.0].len() {
self.save_undo();
self.lines[self.cursor.0].remove(self.cursor.1);
} else if self.cursor.0 + 1 < self.lines.len() {
self.save_undo();
let next = self.lines.remove(self.cursor.0 + 1);
self.lines[self.cursor.0].push_str(&next);
}
}
fn insert_newline(&mut self) {
self.save_undo();
let rest = self.lines[self.cursor.0].split_off(self.cursor.1);
self.lines.insert(self.cursor.0 + 1, rest);
self.cursor.0 += 1;
self.cursor.1 = 0;
}
fn undo(&mut self) {
if let Some((lines, cursor)) = self.undo_stack.pop() {
self.redo_stack.push((self.lines.clone(), self.cursor));
self.lines = lines;
self.cursor = cursor;
self.clamp_cursor();
}
}
fn redo(&mut self) {
if let Some((lines, cursor)) = self.redo_stack.pop() {
self.undo_stack.push((self.lines.clone(), self.cursor));
self.lines = lines;
self.cursor = cursor;
self.clamp_cursor();
}
}
fn prev_char_boundary(&self) -> Option<usize> {
let line = &self.lines[self.cursor.0];
let mut indices: Vec<usize> = line.char_indices().map(|(i, _)| i).collect();
indices.push(line.len());
indices.into_iter().rev().find(|&i| i < self.cursor.1)
}
}
impl Component for TextArea {
fn render(&self, cx: &mut RenderCx) {
let gutter_width: u16 = if self.show_line_numbers {
6
} else {
0
};
let visible_height = self.rect.height.min(self.height);
let visible_width = self.rect.width.min(self.width);
let _text_width = visible_width.saturating_sub(gutter_width);
for i in 0..visible_height as usize {
let line_idx = self.scroll_y as usize + i;
if line_idx >= self.lines.len() {
break;
}
let row_y = self.rect.y + i as u16;
let is_cursor_line = self.focused && line_idx == self.cursor.0;
if self.show_line_numbers {
let num_str = format!(" {:>3} │ ", line_idx + 1);
if line_idx == self.cursor.0 && self.focused {
cx.buffer.write_text(
Pos { x: self.rect.x, y: row_y },
self.rect,
&num_str,
&self.focus_style,
);
} else {
cx.buffer.write_text(
Pos { x: self.rect.x, y: row_y },
self.rect,
&num_str,
&self.line_number_style,
);
}
}
let line = &self.lines[line_idx];
let display = if line.len() > self.scroll_x as usize {
let start = line
.char_indices()
.nth(self.scroll_x as usize)
.map(|(i, _)| i)
.unwrap_or(line.len());
&line[start..]
} else {
""
};
let text_x = self.rect.x + gutter_width;
if is_cursor_line {
let cursor_char_idx = self.cursor.1.saturating_sub(self.scroll_x as usize);
if cursor_char_idx <= display.chars().count() {
let chars: Vec<char> = display.chars().collect();
let before: String = chars.iter().take(cursor_char_idx).collect();
let at = chars.get(cursor_char_idx).map(|c| c.to_string()).unwrap_or_default();
let after: String = chars.iter().skip(cursor_char_idx + 1).collect();
cx.buffer.write_text(
Pos { x: text_x, y: row_y },
self.rect,
&before,
&self.focus_style,
);
let at_x = text_x + str_width(&before);
cx.buffer.write_text(
Pos { x: at_x, y: row_y },
self.rect,
&at,
&self.focus_style,
);
cx.buffer.write_text(
Pos {
x: at_x + str_width(&at),
y: row_y,
},
self.rect,
&after,
&self.focus_style,
);
return; } else {
display.to_string()
};
}
cx.buffer.write_text(
Pos { x: text_x, y: row_y },
self.rect,
display,
&self.style,
);
}
}
fn measure(&self, _constraint: crate::layout::Constraint, _cx: &mut crate::component::MeasureCx) -> Size {
Size {
width: self.width,
height: self.height.min(self.lines.len() as u16),
}
}
fn event(&mut self, event: &Event, cx: &mut EventCx) {
match event {
Event::Focus => {
self.focused = true;
cx.invalidate_paint();
return;
}
Event::Blur => {
self.focused = false;
cx.invalidate_paint();
return;
}
_ => {}
}
if cx.phase() != crate::event::EventPhase::Target {
return;
}
if let Event::Key(key_event) = event {
let ctrl = key_event.modifiers.ctrl;
let shift = key_event.modifiers.shift;
match (&key_event.key, ctrl, shift) {
(Key::Left, false, _) => {
self.move_left();
cx.invalidate_paint();
}
(Key::Right, false, _) => {
self.move_right();
cx.invalidate_paint();
}
(Key::Up, false, _) => {
self.move_up();
cx.invalidate_paint();
}
(Key::Down, false, _) => {
self.move_down();
cx.invalidate_paint();
}
(Key::Home, false, _) => {
self.move_home();
cx.invalidate_paint();
}
(Key::End, false, _) => {
self.move_end();
cx.invalidate_paint();
}
(Key::PageUp, false, _) => {
let page = self.height.saturating_sub(1) as usize;
self.move_page_up(page);
cx.invalidate_paint();
}
(Key::PageDown, false, _) => {
let page = self.height.saturating_sub(1) as usize;
self.move_page_down(page);
cx.invalidate_paint();
}
(Key::Char(c), false, false) if *c != '\n' => {
self.insert_char(*c);
cx.invalidate_paint();
}
(Key::Backspace, false, _) => {
self.delete_backward();
cx.invalidate_paint();
}
(Key::Delete, false, _) => {
self.delete_forward();
cx.invalidate_paint();
}
(Key::Enter, false, _) => {
self.insert_newline();
cx.invalidate_paint();
}
(Key::Char('z'), true, _) => {
self.undo();
cx.invalidate_paint();
}
(Key::Char('y'), true, _) => {
self.redo();
cx.invalidate_paint();
}
_ => {}
}
}
}
fn layout(&mut self, rect: Rect, _cx: &mut crate::component::LayoutCx) {
self.rect = rect;
}
fn focusable(&self) -> bool {
true
}
fn style(&self) -> Style {
self.style.clone()
}
}
pub(crate) fn str_width(s: &str) -> u16 {
s.chars()
.map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16)
.sum()
}