1
0
зеркало из https://github.com/glebtv/tui-textarea.git synced 2026-09-03 22:35:50 +03:00
* select, second go using new multiline code

* implemented internal delete_str, added tests

* delete_str_internal must not put non deleted strings into history. Ie ctrlc should not change history

* delete_str was taking lines out in the non delete case

* corrected comment

* fixes after feedback. Plus multichar bugs. change modal sample
Этот коммит содержится в:
pm100
2023-11-10 04:10:04 -08:00
коммит произвёл GitHub
родитель 9ec2f32edf
Коммит 5a2649abe9
10 изменённых файлов: 955 добавлений и 68 удалений

Просмотреть файл

@@ -10,10 +10,12 @@ use std::iter;
use tui::text::Spans as Line;
use unicode_width::UnicodeWidthChar as _;
#[derive(Debug)]
enum Boundary {
Cursor(Style),
#[cfg(feature = "search")]
Search(Style),
Select(Style),
End,
}
@@ -21,9 +23,10 @@ impl Boundary {
fn cmp(&self, other: &Boundary) -> Ordering {
fn rank(b: &Boundary) -> u8 {
match b {
Boundary::Cursor(_) => 2,
Boundary::Cursor(_) => 3,
#[cfg(feature = "search")]
Boundary::Search(_) => 1,
Boundary::Select(_) => 2,
Boundary::End => 0,
}
}
@@ -36,6 +39,7 @@ impl Boundary {
#[cfg(feature = "search")]
Boundary::Search(s) => Some(*s),
Boundary::End => None,
Boundary::Select(s) => Some(*s),
}
}
}
@@ -94,16 +98,24 @@ impl DisplayTextBuilder {
pub struct LineHighlighter<'a> {
line: &'a str,
spans: Vec<Span<'a>>,
// reminder - the usize is the start of a section, in BYTES, not chars
boundaries: Vec<(Boundary, usize)>, // TODO: Consider smallvec
style_begin: Style,
cursor_at_end: bool,
cursor_style: Style,
tab_len: u8,
mask: Option<char>,
select_style: Style,
}
impl<'a> LineHighlighter<'a> {
pub fn new(line: &'a str, cursor_style: Style, tab_len: u8, mask: Option<char>) -> Self {
pub fn new(
line: &'a str,
cursor_style: Style,
tab_len: u8,
mask: Option<char>,
select_style: Style,
) -> Self {
Self {
line,
spans: vec![],
@@ -113,6 +125,7 @@ impl<'a> LineHighlighter<'a> {
cursor_style,
tab_len,
mask,
select_style,
}
}
@@ -122,6 +135,11 @@ impl<'a> LineHighlighter<'a> {
.push(Span::styled(format!("{}{} ", pad, row + 1), style));
}
pub fn select(&mut self, start: usize, end: usize, style: Style) {
self.boundaries.push((Boundary::Select(style), start));
self.boundaries.push((Boundary::End, end));
}
pub fn cursor_line(&mut self, cursor_col: usize, style: Style) {
if let Some((start, c)) = self.line.char_indices().nth(cursor_col) {
self.boundaries
@@ -133,6 +151,60 @@ impl<'a> LineHighlighter<'a> {
self.style_begin = style;
}
pub(crate) fn select_line(
&mut self,
row: usize,
line: &str,
start: (usize, usize),
end: (usize, usize),
) {
// is this row selected?
// note that the input coordinates here are in char offsets not bytes
// so a lot of this code is converting from char offsets to byte offsets
if start.0 <= row && end.0 >= row {
let mut indices = line.char_indices();
let llen = line.len();
match (start.0 == row, end.0 == row) {
(true, true) => {
// only line
self.select(
indices.nth(start.1).unwrap_or((llen, 'x')).0,
indices.nth(end.1 - start.1).unwrap_or((llen, 'x')).0,
self.select_style,
);
}
(true, false) => {
// a line in the start of selection
// the +1 extends the highlight one beyond end, to show
// that the newline is included
// same done for middle rows below
self.select(
indices.nth(start.1).unwrap_or((llen, 'x')).0,
llen + 1,
self.select_style,
);
}
(false, true) => {
// last line
if end.1 > 0 {
self.select(
0,
indices.nth(end.1).unwrap_or((llen, 'x')).0,
self.select_style,
);
}
}
(false, false) => {
// in the middle of selection
self.select(0, llen + 1, self.select_style);
}
}
}
}
#[cfg(feature = "search")]
pub fn search(&mut self, matches: impl Iterator<Item = (usize, usize)>, style: Style) {
for (start, end) in matches {
@@ -153,6 +225,7 @@ impl<'a> LineHighlighter<'a> {
cursor_style,
cursor_at_end,
mask,
select_style,
} = self;
let mut builder = DisplayTextBuilder::new(tab_len, mask);
@@ -172,10 +245,18 @@ impl<'a> LineHighlighter<'a> {
let mut style = style_begin;
let mut start = 0;
let mut stack = vec![];
let mut dont_add_cursor = false;
for (next_boundary, end) in boundaries {
if start < end {
spans.push(Span::styled(builder.build(&line[start..end]), style));
// add extra select space at line end to indicate
// that the \n will be deleted / included
if end > line.len() && style == select_style {
spans.push(Span::styled(builder.build(&line[start..end - 1]), style));
spans.push(Span::styled(" ", style));
dont_add_cursor = true;
} else {
spans.push(Span::styled(builder.build(&line[start..end]), style));
}
}
style = if let Some(s) = next_boundary.style() {
@@ -186,13 +267,13 @@ impl<'a> LineHighlighter<'a> {
};
start = end;
}
if start != line.len() {
if start < line.len() {
spans.push(Span::styled(builder.build(&line[start..]), style));
}
if cursor_at_end {
if cursor_at_end && !dont_add_cursor {
spans.push(Span::styled(" ", cursor_style));
}
Line::from(spans)
}
}

Просмотреть файл

@@ -49,9 +49,15 @@ impl From<KeyEvent> for Input {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let alt = key.modifiers.contains(KeyModifiers::ALT);
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
let key = Key::from(key.code);
Self { key, ctrl, alt }
Self {
key,
ctrl,
alt,
shift,
}
}
}
@@ -72,7 +78,14 @@ impl From<MouseEvent> for Input {
let key = Key::from(mouse.kind);
let ctrl = mouse.modifiers.contains(KeyModifiers::CONTROL);
let alt = mouse.modifiers.contains(KeyModifiers::ALT);
Self { key, ctrl, alt }
let shift = mouse.modifiers.contains(KeyModifiers::SHIFT);
Self {
key,
ctrl,
alt,
shift,
}
}
}

Просмотреть файл

@@ -90,7 +90,7 @@ impl Default for Key {
/// textarea.input(Input {
/// key: Key::Char('a'),
/// ctrl: true,
/// alt: false,
/// alt: false,shift:false
/// });
/// ```
#[derive(Debug, Clone, Default, PartialEq, Hash)]
@@ -102,6 +102,8 @@ pub struct Input {
pub ctrl: bool,
/// Alt modifier key. `true` means Alt key was pressed.
pub alt: bool,
// Shift key. `true` means Shift key was pressed.
pub shift: bool,
}
#[cfg(test)]
@@ -110,7 +112,12 @@ mod tests {
#[allow(dead_code)]
pub(crate) fn input(key: Key, ctrl: bool, alt: bool) -> Input {
Input { key, ctrl, alt }
Input {
key,
ctrl,
alt,
shift: false,
}
}
#[test]

Просмотреть файл

@@ -17,6 +17,7 @@ impl From<KeyEvent> for Input {
fn from(key: KeyEvent) -> Self {
let mut ctrl = false;
let mut alt = false;
let shift = false;
let key = match key {
KeyEvent::Char('\n' | '\r') => Key::Enter,
KeyEvent::Char(c) => Key::Char(c),
@@ -44,7 +45,12 @@ impl From<KeyEvent> for Input {
_ => Key::Null,
};
Input { key, ctrl, alt }
Input {
key,
ctrl,
alt,
shift,
}
}
}
@@ -71,6 +77,7 @@ impl From<MouseEvent> for Input {
key,
ctrl: false,
alt: false,
shift: false,
}
}
}

Просмотреть файл

@@ -46,8 +46,13 @@ impl From<KeyEvent> for Input {
let key = Key::from(key);
let ctrl = modifiers.contains(Modifiers::CTRL);
let alt = modifiers.contains(Modifiers::ALT);
Self { key, ctrl, alt }
let shift = modifiers.contains(Modifiers::SHIFT);
Self {
key,
ctrl,
alt,
shift,
}
}
}
@@ -77,8 +82,13 @@ impl From<MouseEvent> for Input {
let key = Key::from(mouse_buttons);
let ctrl = modifiers.contains(Modifiers::CTRL);
let alt = modifiers.contains(Modifiers::ALT);
Self { key, ctrl, alt }
let shift = modifiers.contains(Modifiers::SHIFT);
Self {
key,
ctrl,
alt,
shift,
}
}
}
@@ -94,8 +104,13 @@ impl From<PixelMouseEvent> for Input {
let key = Key::from(mouse_buttons);
let ctrl = modifiers.contains(Modifiers::CTRL);
let alt = modifiers.contains(Modifiers::ALT);
Self { key, ctrl, alt }
let shift = modifiers.contains(Modifiers::SHIFT);
Self {
key,
ctrl,
alt,
shift,
}
}
}

Просмотреть файл

@@ -1,3 +1,5 @@
use std::cell::Cell;
use crate::cursor::CursorMove;
use crate::highlight::LineHighlighter;
use crate::history::{Edit, EditKind, History};
@@ -55,7 +57,7 @@ impl ToString for YankText {
/// let mut textarea = TextArea::default();
///
/// // Input 'a'
/// let input = Input { key: Key::Char('a'), ctrl: false, alt: false };
/// let input = Input { key: Key::Char('a'), ctrl: false, alt: false , shift:false};
/// textarea.input(input);
///
/// // Get widget to render.
@@ -84,6 +86,11 @@ pub struct TextArea<'a> {
pub(crate) placeholder: String,
pub(crate) placeholder_style: Style,
mask: Option<char>,
// these are Cells so that should_end_selection can be called
// in a non mutable function call - ie self.widget()
select_start: Cell<Option<(usize, usize)>>,
pending_end_selection: Cell<bool>,
select_style: Style,
}
/// Convert any iterator whose elements can be converted into [`String`] into [`TextArea`]. Each [`String`] element is
@@ -184,6 +191,9 @@ impl<'a> TextArea<'a> {
placeholder: String::new(),
placeholder_style: Style::default().fg(Color::DarkGray),
mask: None,
select_start: Default::default(),
select_style: Style::default().bg(Color::LightBlue),
pending_end_selection: Cell::new(false),
}
}
@@ -225,16 +235,19 @@ impl<'a> TextArea<'a> {
/// ```
pub fn input(&mut self, input: impl Into<Input>) -> bool {
let input = input.into();
self.should_select(&input);
let modified = match input {
Input {
key: Key::Char('m'),
ctrl: true,
alt: false,
..
}
| Input {
key: Key::Char('\n' | '\r'),
ctrl: false,
alt: false,
..
}
| Input {
key: Key::Enter, ..
@@ -246,6 +259,7 @@ impl<'a> TextArea<'a> {
key: Key::Char(c),
ctrl: false,
alt: false,
..
} => {
self.insert_char(c);
true
@@ -254,124 +268,146 @@ impl<'a> TextArea<'a> {
key: Key::Tab,
ctrl: false,
alt: false,
..
} => self.insert_tab(),
Input {
key: Key::Char('h'),
ctrl: true,
alt: false,
..
}
| Input {
key: Key::Backspace,
ctrl: false,
alt: false,
..
} => self.delete_char(),
Input {
key: Key::Char('d'),
ctrl: true,
alt: false,
..
}
| Input {
key: Key::Delete,
ctrl: false,
alt: false,
..
} => self.delete_next_char(),
Input {
key: Key::Char('k'),
ctrl: true,
alt: false,
..
} => self.delete_line_by_end(),
Input {
key: Key::Char('j'),
ctrl: true,
alt: false,
..
} => self.delete_line_by_head(),
Input {
key: Key::Char('w'),
ctrl: true,
alt: false,
..
}
| Input {
key: Key::Char('h'),
ctrl: false,
alt: true,
..
}
| Input {
key: Key::Backspace,
ctrl: false,
alt: true,
..
} => self.delete_word(),
Input {
key: Key::Delete,
ctrl: false,
alt: true,
..
}
| Input {
key: Key::Char('d'),
ctrl: false,
alt: true,
..
} => self.delete_next_word(),
Input {
key: Key::Char('n'),
key: Key::Char('n' | 'N'),
ctrl: true,
alt: false,
..
}
| Input {
key: Key::Down,
ctrl: false,
alt: false,
..
} => {
self.move_cursor(CursorMove::Down);
false
}
Input {
key: Key::Char('p'),
key: Key::Char('p' | 'P'),
ctrl: true,
alt: false,
..
}
| Input {
key: Key::Up,
ctrl: false,
alt: false,
..
} => {
self.move_cursor(CursorMove::Up);
false
}
Input {
key: Key::Char('f'),
key: Key::Char('f' | 'F'),
ctrl: true,
alt: false,
..
}
| Input {
key: Key::Right,
ctrl: false,
alt: false,
..
} => {
self.move_cursor(CursorMove::Forward);
false
}
Input {
key: Key::Char('b'),
key: Key::Char('b' | 'B'),
ctrl: true,
alt: false,
..
}
| Input {
key: Key::Left,
ctrl: false,
alt: false,
..
} => {
self.move_cursor(CursorMove::Back);
false
}
Input {
key: Key::Char('a'),
key: Key::Char('a' | 'A'),
ctrl: true,
alt: false,
..
}
| Input { key: Key::Home, .. }
| Input {
key: Key::Left | Key::Char('b'),
key: Key::Left | Key::Char('b' | 'B'),
ctrl: true,
alt: true,
..
} => {
self.move_cursor(CursorMove::Head);
false
@@ -380,12 +416,14 @@ impl<'a> TextArea<'a> {
key: Key::Char('e'),
ctrl: true,
alt: false,
..
}
| Input { key: Key::End, .. }
| Input {
key: Key::Right | Key::Char('f'),
key: Key::Right | Key::Char('f' | 'F'),
ctrl: true,
alt: true,
..
} => {
self.move_cursor(CursorMove::End);
false
@@ -394,11 +432,13 @@ impl<'a> TextArea<'a> {
key: Key::Char('<'),
ctrl: false,
alt: true,
..
}
| Input {
key: Key::Up | Key::Char('p'),
key: Key::Up | Key::Char('p' | 'P'),
ctrl: true,
alt: true,
..
} => {
self.move_cursor(CursorMove::Top);
false
@@ -407,37 +447,43 @@ impl<'a> TextArea<'a> {
key: Key::Char('>'),
ctrl: false,
alt: true,
..
}
| Input {
key: Key::Down | Key::Char('n'),
key: Key::Down | Key::Char('n' | 'N'),
ctrl: true,
alt: true,
..
} => {
self.move_cursor(CursorMove::Bottom);
false
}
Input {
key: Key::Char('f'),
key: Key::Char('f' | 'F'),
ctrl: false,
alt: true,
..
}
| Input {
key: Key::Right,
ctrl: true,
alt: false,
..
} => {
self.move_cursor(CursorMove::WordForward);
false
}
Input {
key: Key::Char('b'),
key: Key::Char('b' | 'B'),
ctrl: false,
alt: true,
..
}
| Input {
key: Key::Left,
ctrl: true,
alt: false,
..
} => {
self.move_cursor(CursorMove::WordBack);
false
@@ -446,16 +492,19 @@ impl<'a> TextArea<'a> {
key: Key::Char(']'),
ctrl: false,
alt: true,
..
}
| Input {
key: Key::Char('n'),
key: Key::Char('n' | 'N'),
ctrl: false,
alt: true,
..
}
| Input {
key: Key::Down,
ctrl: true,
alt: false,
..
} => {
self.move_cursor(CursorMove::ParagraphForward);
false
@@ -464,16 +513,19 @@ impl<'a> TextArea<'a> {
key: Key::Char('['),
ctrl: false,
alt: true,
..
}
| Input {
key: Key::Char('p'),
key: Key::Char('p' | 'P'),
ctrl: false,
alt: true,
..
}
| Input {
key: Key::Up,
ctrl: true,
alt: false,
..
} => {
self.move_cursor(CursorMove::ParagraphBack);
false
@@ -482,21 +534,43 @@ impl<'a> TextArea<'a> {
key: Key::Char('u'),
ctrl: true,
alt: false,
..
} => self.undo(),
Input {
key: Key::Char('r'),
ctrl: true,
alt: false,
..
} => self.redo(),
Input {
key: Key::Char('y'),
ctrl: true,
alt: false,
..
} => self.paste(),
Input {
key: Key::Char('v'),
key: Key::Char('c'),
ctrl: true,
alt: false,
..
} => {
self.copy();
false
}
Input {
key: Key::Char('x'),
ctrl: true,
alt: false,
..
} => {
self.cut();
false
}
Input {
key: Key::Char('v' | 'V'),
ctrl: true,
alt: false,
..
}
| Input {
key: Key::PageDown, ..
@@ -505,9 +579,10 @@ impl<'a> TextArea<'a> {
false
}
Input {
key: Key::Char('v'),
key: Key::Char('v' | 'V'),
ctrl: false,
alt: true,
..
}
| Input {
key: Key::PageUp, ..
@@ -531,7 +606,6 @@ impl<'a> TextArea<'a> {
}
_ => false,
};
// Check invariants
debug_assert!(!self.lines.is_empty(), "no line after {:?}", input);
let (r, c) = self.cursor;
@@ -550,7 +624,7 @@ impl<'a> TextArea<'a> {
self.lines[r],
input,
);
self.should_end_selection();
modified
}
@@ -567,11 +641,14 @@ impl<'a> TextArea<'a> {
/// This method is useful when you want to define your own key mappings and don't want default key mappings.
/// See 'Define your own key mappings' section in [the module document](./index.html).
pub fn input_without_shortcuts(&mut self, input: impl Into<Input>) -> bool {
match input.into() {
let input = input.into();
self.should_select(&input);
let modified = match input {
Input {
key: Key::Char(c),
ctrl: false,
alt: false,
..
} => {
self.insert_char(c);
true
@@ -580,6 +657,7 @@ impl<'a> TextArea<'a> {
key: Key::Tab,
ctrl: false,
alt: false,
..
} => self.insert_tab(),
Input {
key: Key::Backspace,
@@ -609,7 +687,9 @@ impl<'a> TextArea<'a> {
false
}
_ => false,
}
};
self.should_end_selection();
modified
}
fn push_history(&mut self, kind: EditKind, cursor_before: (usize, usize)) {
@@ -627,6 +707,8 @@ impl<'a> TextArea<'a> {
/// assert_eq!(textarea.lines(), ["a"]);
/// ```
pub fn insert_char(&mut self, c: char) {
self.delete_selection(false);
let (row, col) = self.cursor;
let line = &mut self.lines[row];
let i = line
@@ -652,6 +734,7 @@ impl<'a> TextArea<'a> {
/// assert_eq!(textarea.lines(), ["hello, world", "goodbye, world"]);
/// ```
pub fn insert_str<S: AsRef<str>>(&mut self, s: S) -> bool {
self.end_selection();
let mut lines: Vec<_> = s.as_ref().lines().map(ToString::to_string).collect();
match lines.len() {
0 => false,
@@ -727,6 +810,14 @@ impl<'a> TextArea<'a> {
/// assert_eq!(textarea.lines(), ["🐱", "🐮"]);
/// ```
pub fn delete_str(&mut self, chars: usize) -> bool {
self.delete_str_internal(chars, true, true)
}
// grabs the specified (maybe multi line string)
// sometimes yanked
// sometimes deleted from lines array and placed in history
fn delete_str_internal(&mut self, chars: usize, yank: bool, delete: bool) -> bool {
if chars == 0 {
return false;
}
@@ -759,14 +850,22 @@ impl<'a> TextArea<'a> {
// First line
if let Some(offset) = find_end(&line[first_start..]) {
let removed = self.lines[row]
.drain(first_start..first_start + offset)
.as_str()
.to_string();
self.yank = removed.clone().into();
self.push_history(EditKind::DeleteStr(removed, first_start), self.cursor);
let removed = if delete {
self.lines[row]
.drain(first_start..first_start + offset)
.as_str()
.to_string()
} else {
self.lines[row][first_start..first_start + offset].to_string()
};
if yank {
self.yank = removed.clone().into();
};
if delete {
self.push_history(EditKind::DeleteStr(removed, first_start), self.cursor);
}
return true;
}
};
let mut r = row + 1;
let mut i = 0;
@@ -780,23 +879,41 @@ impl<'a> TextArea<'a> {
r += 1;
}
let mut deleted = vec![self.lines[row].drain(first_start..).as_str().to_string()];
deleted.extend(self.lines.drain(row + 1..r));
let mut deleted = if delete {
vec![self.lines[row].drain(first_start..).as_str().to_string()]
} else {
vec![self.lines[row][first_start..].to_string()]
};
if delete {
deleted.extend(self.lines.drain(row + 1..r));
} else {
deleted.extend(self.lines[row + 1..r].iter().map(|s| s.to_string()));
}
if row + 1 < self.lines.len() {
let mut last_line = self.lines.remove(row + 1);
self.lines[row].push_str(&last_line[i..]);
let mut last_line = if delete {
let ll = self.lines.remove(row + 1);
self.lines[row].push_str(&ll[i..]);
ll
} else {
self.lines[row + 1].clone()
};
last_line.truncate(i);
deleted.push(last_line);
}
self.yank = YankText::Chunk(deleted.clone());
let edit = if deleted.len() == 1 {
EditKind::DeleteStr(deleted.remove(0), first_start)
} else {
EditKind::DeleteChunk(deleted, row, first_start)
if yank {
self.yank = YankText::Chunk(deleted.clone());
};
self.push_history(edit, self.cursor);
if delete {
let edit = if deleted.len() == 1 {
EditKind::DeleteStr(deleted.remove(0), first_start)
} else {
EditKind::DeleteChunk(deleted, row, first_start)
};
self.push_history(edit, self.cursor);
}
true
}
@@ -871,6 +988,7 @@ impl<'a> TextArea<'a> {
/// assert_eq!(textarea.lines(), ["h", "i"]);
/// ```
pub fn insert_newline(&mut self) {
self.delete_selection(false);
let (row, col) = self.cursor;
let line = &mut self.lines[row];
let idx = line
@@ -925,6 +1043,9 @@ impl<'a> TextArea<'a> {
/// assert_eq!(textarea.lines(), ["bc"]);
/// ```
pub fn delete_char(&mut self) -> bool {
if self.select_start.get().is_some() {
return self.delete_selection(false);
}
let (row, col) = self.cursor;
if col == 0 {
return self.delete_newline();
@@ -953,6 +1074,9 @@ impl<'a> TextArea<'a> {
/// assert_eq!(textarea.lines(), ["ac"]);
/// ```
pub fn delete_next_char(&mut self) -> bool {
if self.select_start.get().is_some() {
return self.delete_selection(false);
}
let before = self.cursor;
self.move_cursor(CursorMove::Forward);
if before == self.cursor {
@@ -1080,6 +1204,7 @@ impl<'a> TextArea<'a> {
/// assert_eq!(textarea.lines(), [" bbb cccaaa"]);
/// ```
pub fn paste(&mut self) -> bool {
self.delete_selection(false);
match self.yank.clone() {
YankText::Piece(s) => self.insert_piece(s),
YankText::Chunk(c) => self.insert_chunk(c),
@@ -1097,7 +1222,7 @@ impl<'a> TextArea<'a> {
/// assert_eq!(textarea.cursor(), (0, 1));
/// textarea.move_cursor(CursorMove::Down);
/// assert_eq!(textarea.cursor(), (1, 1));
/// ```
pub fn move_cursor(&mut self, m: CursorMove) {
if let Some(cursor) = m.next_cursor(self.cursor, &self.lines, &self.viewport) {
self.cursor = cursor;
@@ -1147,7 +1272,13 @@ impl<'a> TextArea<'a> {
}
pub(crate) fn line_spans<'b>(&'b self, line: &'b str, row: usize, lnum_len: u8) -> Line<'b> {
let mut hl = LineHighlighter::new(line, self.cursor_style, self.tab_len, self.mask);
let mut hl = LineHighlighter::new(
line,
self.cursor_style,
self.tab_len,
self.mask,
self.select_style,
);
if let Some(style) = self.line_number_style {
hl.line_number(row, lnum_len, style);
@@ -1162,6 +1293,12 @@ impl<'a> TextArea<'a> {
hl.search(matches, self.search.style);
}
// any selected text to highlight?
if self.select_start.get().is_some() {
let (start, end, _) = self.normalize_selection();
hl.select_line(row, line, start, end);
}
hl.into_spans()
}
@@ -1192,6 +1329,7 @@ impl<'a> TextArea<'a> {
/// }
/// ```
pub fn widget(&'a self) -> impl Widget + 'a {
self.should_end_selection();
Renderer::new(self)
}
@@ -1253,7 +1391,7 @@ impl<'a> TextArea<'a> {
/// use tui_textarea::{TextArea, Input, Key};
///
/// let mut textarea = TextArea::default();
/// let tab_input = Input { key: Key::Tab, ctrl: false, alt: false };
/// let tab_input = Input { key: Key::Tab, ctrl: false, alt:false, shift: false };
///
/// textarea.set_tab_length(8);
/// textarea.input(tab_input.clone());
@@ -1879,10 +2017,226 @@ impl<'a> TextArea<'a> {
scrolling.into().scroll(&mut self.viewport);
self.move_cursor(CursorMove::InViewport);
}
}
// public functions for select
/// Sets the style used for selected text
/// the default is LightBlue
///```
/// use tui_textarea::TextArea;
/// use ratatui::style::{Style, Color};
/// let mut textarea = TextArea::default();
/// // change the selection color from the default to Red
/// textarea.set_selection_style(Style::default().bg(Color::Red));
/// assert_eq!(textarea.get_selection_style(), Style::default().bg(Color::Red));
/// ```
pub fn set_selection_style(&mut self, style: Style) {
self.select_style = style;
}
/// Gets the style used for selected text
///
///```
/// use tui_textarea::TextArea;
/// use ratatui::style::{Style, Color};
/// let mut textarea = TextArea::default();
///
///
/// assert_eq!(textarea.get_selection_style(), Style::default().bg(Color::LightBlue));
/// ```
pub fn get_selection_style(&mut self) -> Style {
self.select_style
}
/// Copies the current selection to the yank buffer
///
///```
/// use tui_textarea::{TextArea, Key, Input};
///
///
/// let mut textarea = TextArea::default();
/// // load some text
/// textarea.insert_str("Hello World");
/// // select the word "World" by doing a ctrl-left with the shift key down
/// textarea.input(Input{key:Key::Left, ctrl:true, alt:false, shift:true});
///
/// textarea.copy();
/// assert_eq!(textarea.yank_text(), "World");
/// assert_eq!(textarea.lines(), ["Hello World"]);
///```
pub fn copy(&mut self) {
if self.select_start.get().is_some() {
let (start, end, swapped) = self.normalize_selection();
self.cursor = start;
self.delete_range(start, end, true, false);
self.cursor = if swapped { start } else { end };
}
}
/// Deletes the current selection and places it in the yank buffer
///```
/// use tui_textarea::{TextArea, Key, Input};
///
///
/// let mut textarea = TextArea::default();
/// // load some text
/// textarea.insert_str("Hello World");
/// // select the word "World" by doing a ctrl-left with the shift key down
/// textarea.input(Input{key:Key::Left, ctrl:true, alt:false, shift:true});
///
/// textarea.cut();
/// assert_eq!(textarea.yank_text(), "World");
/// assert_eq!(textarea.lines(), ["Hello "]);
///```
///
pub fn cut(&mut self) {
self.delete_selection(true);
}
/// Must be called before calling textarea functions if you are
/// not calling [`TextArea::input`] or [`TextArea::input_without_shortcuts`] to dispatch the keys.
/// It is used to detect whether or not a selection should be started, continued or ended
///
/// Handled automatically when inputs are passed to [`TextArea::input`] or [`TextArea::input_without_shortcuts`]
///
/// Note: it is harmless to call this function even if you have some logic paths that call [`TextArea::input`]
/// The state of the shift key (or whatever you want to signal start selection) should be passed in.
/// If you need to stop an upper case letter being treated as a selection character
/// then call this function with false
///
/// Look at the modal example for a demonstration of its use
pub fn should_select(&mut self, input: &Input) {
if input.key == Key::Null {
return;
}
// Should we start selecting text, stop the current selection, or do nothing?
// the end is handled after the ending keystroke
self.pending_end_selection =
Cell::new(match (self.select_start.get().is_some(), input.shift) {
(true, true) => {
// continue select
false
}
(true, false) => {
// end select
true
}
(false, true) => {
// start select
self.start_selection();
false
}
(false, false) => {
// ignore
false
}
});
}
/// used in conjunction with code that needs to use [`TextArea::should_select`]
/// Used to force the end selection mode
///
/// Needed if a simple keystroke is upper case and you want to stop it being treated as a selection
/// Look at the modal example for a demonstration of its use - look at the 'a' and 'A' keys
///
pub fn stop_selection(&mut self) {
self.end_selection();
}
// functions for select
fn should_end_selection(&self) {
// Should we end the current selection?
if self.pending_end_selection.take() {
self.end_selection();
}
}
fn start_selection(&mut self) {
self.select_start = Cell::new(Some(self.cursor));
}
fn end_selection(&self) {
self.select_start.set(Default::default());
}
fn normalize_selection(&self) -> ((usize, usize), (usize, usize), bool) {
// selection can be from right to left or left to right
// rest of the code always wants the leftmost part first
// bool indicates if swap was done
let start = self.select_start.get().unwrap();
let end = self.cursor;
if start.0 > end.0 || (start.0 == end.0 && start.1 > end.1) {
(end, start, true)
} else {
(start, end, false)
}
}
// delete the currect selection, sometimes yank it
fn delete_selection(&mut self, yank: bool) -> bool {
let deleted = match self.select_start.get() {
Some(_) => {
let (start, end, _) = self.normalize_selection();
if start != end {
self.delete_range(start, end, yank, true);
true
} else {
false
}
}
None => false,
};
self.select_start = Default::default();
deleted
}
fn line_length(&self, row: usize) -> usize {
self.lines[row].chars().count()
}
// erase a range of text, start and end must be normalized
// erased text may be put in history and yank buffer (by delete_str)
// the end range column is not included
fn delete_range(
&mut self,
start: (usize, usize),
end: (usize, usize),
yank: bool,
delete: bool,
) {
// calculate length of range
let (start_row, start_col) = start;
let (end_row, end_col) = end;
let mut col = start_col;
let mut len = 0;
// need +1 because the row range is inclusive
for r in start_row..end_row + 1 {
// we only take some of the first line
len += self.line_length(r) - col;
// but maybe all of subsequent lines
col = 0;
}
// we didnt take the whole last line
// so subtract the bit we didnt take
len -= self.line_length(end_row) - end_col;
// plus add the number of linefeeds
len += end_row - start_row;
self.cursor = start;
self.delete_str_internal(len, yank, delete);
}
}
#[cfg(test)]
mod tests {
use super::*;
// Seaparate tests for tui-rs support