#[allow(deprecated)]
use crate::{
direction::Direction,
event::{Event, EventResult, Key, MouseButton, MouseEvent},
rect::Rect,
style::PaletteStyle,
utils::lines::simple::{prefix, simple_prefix, LinesIterator, Row},
view::{CannotFocus, ScrollBase, SizeCache, View},
Vec2, {Printer, With, XY},
};
use log::debug;
use std::cmp::min;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
pub struct TextArea {
content: String,
rows: Vec<Row>,
enabled: bool,
#[allow(deprecated)]
scrollbase: ScrollBase,
size_cache: Option<XY<SizeCache>>,
last_size: Vec2,
cursor: usize,
}
fn make_rows(text: &str, width: usize) -> Vec<Row> {
let width = usize::max(width, 1);
LinesIterator::new(text, width).show_spaces().collect()
}
new_default!(TextArea);
impl TextArea {
pub fn new() -> Self {
#[allow(deprecated)]
TextArea {
content: String::new(),
rows: Vec::new(),
enabled: true,
scrollbase: ScrollBase::new().right_padding(0),
size_cache: None,
last_size: Vec2::zero(),
cursor: 0,
}
.with(|area| area.compute_rows(Vec2::new(1, 1)))
}
pub fn get_content(&self) -> &str {
&self.content
}
fn invalidate(&mut self) {
self.size_cache = None;
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn set_cursor(&mut self, cursor: usize) {
self.cursor = cursor;
let focus = self.selected_row();
self.scrollbase.scroll_to(focus);
}
pub fn set_content<S: Into<String>>(&mut self, content: S) {
self.content = content.into();
self.cursor = min(self.cursor, self.content.len());
while !self.content.is_char_boundary(self.cursor) {
self.cursor -= 1;
}
if let Some(size) = self.size_cache.map(|s| s.map(|s| s.value)) {
self.invalidate();
self.compute_rows(size);
}
}
#[must_use]
pub fn content<S: Into<String>>(self, content: S) -> Self {
self.with(|s| s.set_content(content))
}
pub fn disable(&mut self) {
self.enabled = false;
}
#[must_use]
pub fn disabled(self) -> Self {
self.with(Self::disable)
}
pub fn enable(&mut self) {
self.enabled = true;
}
#[must_use]
pub fn enabled(self) -> Self {
self.with(Self::enable)
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
fn row_at(&self, byte_offset: usize) -> usize {
debug!("Offset: {}", byte_offset);
assert!(!self.rows.is_empty());
assert!(byte_offset >= self.rows[0].start);
self.rows
.iter()
.enumerate()
.take_while(|&(_, row)| row.start <= byte_offset)
.map(|(i, _)| i)
.last()
.unwrap()
}
fn col_at(&self, byte_offset: usize) -> usize {
let row_id = self.row_at(byte_offset);
let row = self.rows[row_id];
self.content[row.start..byte_offset].width()
}
fn selected_row(&self) -> usize {
assert!(!self.rows.is_empty(), "Rows should never be empty.");
self.row_at(self.cursor)
}
fn selected_col(&self) -> usize {
self.col_at(self.cursor)
}
fn page_up(&mut self) {
for _ in 0..5 {
self.move_up();
}
}
fn page_down(&mut self) {
for _ in 0..5 {
self.move_down();
}
}
fn move_up(&mut self) {
let row_id = self.selected_row();
if row_id == 0 {
return;
}
let x = self.col_at(self.cursor);
let prev_row = self.rows[row_id - 1];
let prev_text = &self.content[prev_row.start..prev_row.end];
let offset = prefix(prev_text.graphemes(true), x, "").length;
self.cursor = prev_row.start + offset;
}
fn move_down(&mut self) {
let row_id = self.selected_row();
if row_id + 1 == self.rows.len() {
return;
}
let x = self.col_at(self.cursor);
let next_row = self.rows[row_id + 1];
let next_text = &self.content[next_row.start..next_row.end];
let offset = prefix(next_text.graphemes(true), x, "").length;
self.cursor = next_row.start + offset;
}
fn move_left(&mut self) {
let len = {
let mut row = self.selected_row();
if self.rows[row].start == self.cursor {
row = row.saturating_sub(1);
}
let text = &self.content[self.rows[row].start..self.cursor];
text.graphemes(true).last().unwrap().len()
};
self.cursor -= len;
}
fn move_right(&mut self) {
let len = self.content[self.cursor..]
.graphemes(true)
.next()
.unwrap()
.len();
self.cursor += len;
}
fn is_cache_valid(&self, size: Vec2) -> bool {
match self.size_cache {
None => false,
Some(ref last) => last.x.accept(size.x) && last.y.accept(size.y),
}
}
fn fix_ghost_row(&mut self) {
if self.rows.is_empty() || self.rows.last().unwrap().end != self.content.len() {
self.rows.push(Row {
start: self.content.len(),
end: self.content.len(),
width: 0,
is_wrapped: false,
});
}
}
fn soft_compute_rows(&mut self, size: Vec2) {
if self.is_cache_valid(size) {
debug!("Cache is still valid.");
return;
}
debug!("Computing! Oh yeah!");
let mut available = size.x;
self.rows = make_rows(&self.content, available);
self.fix_ghost_row();
if self.rows.len() > size.y {
available = available.saturating_sub(1);
self.rows = make_rows(&self.content, available);
self.fix_ghost_row();
}
if !self.rows.is_empty() {
self.size_cache = Some(SizeCache::build(size, size));
}
}
fn compute_rows(&mut self, size: Vec2) {
self.soft_compute_rows(size);
self.scrollbase.set_heights(size.y, self.rows.len());
}
fn backspace(&mut self) {
self.move_left();
self.delete();
}
fn delete(&mut self) {
if self.cursor == self.content.len() {
return;
}
debug!("Rows: {:?}", self.rows);
let len = self.content[self.cursor..]
.graphemes(true)
.next()
.unwrap()
.len();
let start = self.cursor;
let end = self.cursor + len;
debug!("Start/end: {}/{}", start, end);
debug!("Content: `{}`", self.content);
for _ in self.content.drain(start..end) {}
debug!("Content: `{}`", self.content);
let selected_row = self.selected_row();
debug!("Selected row: {}", selected_row);
if self.cursor == self.rows[selected_row].end {
let new_end = self.rows[selected_row + 1].end;
self.rows[selected_row].end = new_end;
self.rows.remove(selected_row + 1);
}
self.rows[selected_row].end -= len;
for row in &mut self.rows.iter_mut().skip(1 + selected_row) {
row.rev_shift(len);
}
debug!("Rows: {:?}", self.rows);
self.fix_damages();
debug!("Rows: {:?}", self.rows);
}
fn insert(&mut self, ch: char) {
self.content.insert(self.cursor, ch);
let shift = ch.len_utf8();
let selected_row = self.selected_row();
self.rows[selected_row].end += shift;
for row in &mut self.rows.iter_mut().skip(1 + selected_row) {
row.shift(shift);
}
self.cursor += shift;
self.fix_damages();
}
fn fix_damages(&mut self) {
if self.size_cache.is_none() {
return;
}
let size = self.size_cache.unwrap().map(|s| s.value);
let first_row = self.selected_row().saturating_sub(1);
let first_byte = self.rows[first_row].start;
debug!("Cursor: {}", self.cursor);
let last_byte = self.content[self.cursor..]
.find('\n')
.map(|i| 1 + i + self.cursor);
let last_row = last_byte.map_or(self.rows.len(), |last_byte| self.row_at(last_byte));
let last_byte = last_byte.unwrap_or(self.content.len());
debug!("Content: `{}` (len={})", self.content, self.content.len());
debug!("start/end: {}/{}", first_byte, last_byte);
debug!("start/end rows: {}/{}", first_row, last_row);
let mut available = size.x;
let scrollable = self.rows.len() > size.y;
if scrollable {
available = available.saturating_sub(1);
}
debug!("Rows: {:?}", self.rows);
let new_rows = make_rows(&self.content[first_byte..last_byte], available);
debug!("New rows: {:?}", new_rows);
debug!("{}-{}", first_row, last_row);
let new_row_count = self.rows.len() + new_rows.len() + first_row - last_row;
if !scrollable && new_row_count > size.y {
self.invalidate();
self.compute_rows(size);
return;
}
let affected_rows = first_row..last_row;
let replacement_rows = new_rows.into_iter().map(|row| row.shifted(first_byte));
self.rows.splice(affected_rows, replacement_rows);
self.fix_ghost_row();
self.scrollbase.set_heights(size.y, self.rows.len());
}
}
impl View for TextArea {
fn required_size(&mut self, constraint: Vec2) -> Vec2 {
self.soft_compute_rows(constraint);
debug!("{:?}", self.rows);
let scroll_width = usize::from(self.rows.len() > constraint.y);
let content_width = if self.rows.iter().any(|row| row.is_wrapped) {
constraint.x.saturating_sub(1 + scroll_width)
} else {
self.rows.iter().map(|r| r.width).max().unwrap_or(1)
};
Vec2::new(scroll_width + 1 + content_width, self.rows.len())
}
fn draw(&self, printer: &Printer) {
let (style, cursor_style) = if self.enabled && printer.enabled {
(PaletteStyle::EditableText, PaletteStyle::EditableTextCursor)
} else {
(
PaletteStyle::EditableTextInactive,
PaletteStyle::EditableTextInactive,
)
};
let w = if self.scrollbase.scrollable() {
printer.size.x.saturating_sub(1)
} else {
printer.size.x
};
printer.with_style(style, |printer| {
for y in 0..printer.size.y {
printer.print_hline((0, y), w, " ");
}
});
debug!("Content: `{}`", &self.content);
self.scrollbase.draw(printer, |printer, i| {
debug!("Drawing row {}", i);
let row = &self.rows[i];
debug!("row: {:?}", row);
let text = &self.content[row.start..row.end];
debug!("row text: `{}`", text);
printer.with_style(style, |printer| {
printer.print((0, 0), text);
});
if printer.focused && i == self.selected_row() {
let cursor_offset = self.cursor - row.start;
let c = if cursor_offset == text.len() {
"_"
} else {
text[cursor_offset..]
.graphemes(true)
.next()
.expect("Found no char!")
};
let offset = text[..cursor_offset].width();
printer.with_style(cursor_style, |printer| {
printer.print((offset, 0), c);
});
}
});
}
fn on_event(&mut self, event: Event) -> EventResult {
if !self.enabled {
return EventResult::Ignored;
}
let mut fix_scroll = true;
match event {
Event::Char(ch) => self.insert(ch),
Event::Key(Key::Enter) => self.insert('\n'),
Event::Key(Key::Backspace) if self.cursor > 0 => self.backspace(),
Event::Key(Key::Del) if self.cursor < self.content.len() => self.delete(),
Event::Key(Key::End) => {
let row = self.selected_row();
self.cursor = self.rows[row].end;
if row + 1 < self.rows.len() && self.cursor == self.rows[row + 1].start {
self.move_left();
}
}
Event::Ctrl(Key::Home) => self.cursor = 0,
Event::Ctrl(Key::End) => self.cursor = self.content.len(),
Event::Key(Key::Home) => self.cursor = self.rows[self.selected_row()].start,
Event::Key(Key::Up) if self.selected_row() > 0 => self.move_up(),
Event::Key(Key::Down) if self.selected_row() + 1 < self.rows.len() => self.move_down(),
Event::Key(Key::PageUp) => self.page_up(),
Event::Key(Key::PageDown) => self.page_down(),
Event::Key(Key::Left) if self.cursor > 0 => self.move_left(),
Event::Key(Key::Right) if self.cursor < self.content.len() => self.move_right(),
Event::Mouse {
event: MouseEvent::WheelUp,
..
} if self.scrollbase.can_scroll_up() => {
fix_scroll = false;
self.scrollbase.scroll_up(5);
}
Event::Mouse {
event: MouseEvent::WheelDown,
..
} if self.scrollbase.can_scroll_down() => {
fix_scroll = false;
self.scrollbase.scroll_down(5);
}
Event::Mouse {
event: MouseEvent::Press(MouseButton::Left),
position,
offset,
} if position
.checked_sub(offset)
.map(|position| self.scrollbase.start_drag(position, self.last_size.x))
.unwrap_or(false) =>
{
fix_scroll = false;
}
Event::Mouse {
event: MouseEvent::Hold(MouseButton::Left),
position,
offset,
} => {
fix_scroll = false;
let position = position.saturating_sub(offset);
self.scrollbase.drag(position);
}
Event::Mouse {
event: MouseEvent::Press(_),
position,
offset,
} if !self.rows.is_empty() && position.fits_in_rect(offset, self.last_size) => {
if let Some(position) = position.checked_sub(offset) {
#[allow(deprecated)]
let y = position.y + self.scrollbase.start_line;
let y = min(y, self.rows.len() - 1);
let x = position.x;
let row = &self.rows[y];
let content = &self.content[row.start..row.end];
self.cursor = row.start + simple_prefix(content, x).length;
}
}
_ => return EventResult::Ignored,
}
debug!("Rows: {:?}", self.rows);
if fix_scroll {
let focus = self.selected_row();
self.scrollbase.scroll_to(focus);
}
EventResult::Consumed(None)
}
fn take_focus(&mut self, _: Direction) -> Result<EventResult, CannotFocus> {
self.enabled.then(EventResult::consumed).ok_or(CannotFocus)
}
fn layout(&mut self, size: Vec2) {
self.last_size = size;
self.compute_rows(size);
}
fn important_area(&self, _: Vec2) -> Rect {
let char_width = if self.cursor >= self.content.len() {
1
} else {
self.content[self.cursor..]
.graphemes(true)
.next()
.unwrap()
.width()
};
Rect::from_size((self.selected_col(), self.selected_row()), (char_width, 1))
}
}
#[crate::blueprint(TextArea::new())]
struct Blueprint {
content: Option<String>,
}