зеркало из
https://github.com/glebtv/tui-textarea.git
synced 2026-08-28 11:36:17 +03:00
feat(textarea): add native cursor render mode (#26)
* feat(textarea): add native cursor render mode
- Add CursorRenderMode::{Cell, Hidden} and TextArea accessors for render mode plus rendered_cursor_position().
- Store the terminal-relative cursor position from a shared render plan that accounts for blocks, line numbers, scroll, wrapping, tabs, and wide Unicode.
- Let hidden mode suppress only the textarea-owned cursor cell while preserving default Cell rendering and set_cursor_style behavior.
- Document native cursor integration and add focused tests for default rendering, hidden rendering, placeholders, and cursor-position edge cases.
* chore: prepare 0.12.1 release
Этот коммит содержится в:
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.12.1] - 2026-07-10 [Changes][v0.12.1]
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Native cursor integration** (#26, @srothgan): Add opt-in `CursorRenderMode::Hidden` and `TextArea::rendered_cursor_position()` for placing backend-owned terminal cursors while preserving the default buffer-drawn cursor behavior.
|
||||||
|
|
||||||
## [0.12.0] - 2026-06-30 [Changes][v0.12.0]
|
## [0.12.0] - 2026-06-30 [Changes][v0.12.0]
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
@@ -491,6 +497,7 @@ First release :tada:
|
|||||||
- docs.rs: https://docs.rs/tui-textarea/latest/tui_textarea/
|
- docs.rs: https://docs.rs/tui-textarea/latest/tui_textarea/
|
||||||
|
|
||||||
|
|
||||||
|
[v0.12.1]: https://github.com/srothgan/tui-textarea/compare/v0.12.0...v0.12.1
|
||||||
[v0.12.0]: https://github.com/srothgan/tui-textarea/compare/v0.11.0...v0.12.0
|
[v0.12.0]: https://github.com/srothgan/tui-textarea/compare/v0.11.0...v0.12.0
|
||||||
[v0.11.0]: https://github.com/srothgan/tui-textarea/compare/v0.10.2...v0.11.0
|
[v0.11.0]: https://github.com/srothgan/tui-textarea/compare/v0.10.2...v0.11.0
|
||||||
[v0.10.2]: https://github.com/srothgan/tui-textarea/compare/v0.10.1...v0.10.2
|
[v0.10.2]: https://github.com/srothgan/tui-textarea/compare/v0.10.1...v0.10.2
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name = "tui-textarea-2"
|
|||||||
homepage = "https://github.com/srothgan/tui-textarea#readme"
|
homepage = "https://github.com/srothgan/tui-textarea#readme"
|
||||||
repository = "https://github.com/srothgan/tui-textarea"
|
repository = "https://github.com/srothgan/tui-textarea"
|
||||||
documentation = "https://docs.rs/tui-textarea-2/latest/tui_textarea/"
|
documentation = "https://docs.rs/tui-textarea-2/latest/tui_textarea/"
|
||||||
version = "0.12.0"
|
version = "0.12.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.88.0" # for Ratatui 0.30 split crates
|
rust-version = "1.88.0" # for Ratatui 0.30 split crates
|
||||||
authors = ["Simon Peter Rothgang <simonrothgang@icloud.com>", "rhysd <lin90162@yahoo.co.jp>"]
|
authors = ["Simon Peter Rothgang <simonrothgang@icloud.com>", "rhysd <lin90162@yahoo.co.jp>"]
|
||||||
|
|||||||
25
README.md
25
README.md
@@ -20,6 +20,7 @@ Since this fork was created, it has added or integrated:
|
|||||||
- Row measurement via `TextAreaMeasure`, `TextArea::measure()`, `set_min_rows()`, and `set_max_rows()`
|
- Row measurement via `TextAreaMeasure`, `TextArea::measure()`, `set_min_rows()`, and `set_max_rows()`
|
||||||
- Bulk whole-buffer replacement via `TextArea::set_lines()`
|
- Bulk whole-buffer replacement via `TextArea::set_lines()`
|
||||||
- Opt-in atomic ranges for caller-owned placeholders, mentions, or other indivisible spans
|
- Opt-in atomic ranges for caller-owned placeholders, mentions, or other indivisible spans
|
||||||
|
- Opt-in native terminal cursor integration via `CursorRenderMode::Hidden` and `rendered_cursor_position()`
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ Since this fork was created, it has added or integrated:
|
|||||||
- Custom highlighted ranges
|
- Custom highlighted ranges
|
||||||
- Opt-in atomic ranges for indivisible editing spans
|
- Opt-in atomic ranges for indivisible editing spans
|
||||||
- Placeholder and masking support
|
- Placeholder and masking support
|
||||||
|
- Optional native terminal cursor placement while keeping backend-specific cursor shape control outside the widget
|
||||||
- Mouse scrolling
|
- Mouse scrolling
|
||||||
- Yank support. Paste text deleted with `C-k`, `C-j`, ...
|
- Yank support. Paste text deleted with `C-k`, `C-j`, ...
|
||||||
- Backend agnostic. [crossterm][], [termion][], [termwiz][], and your own backend are all supported
|
- Backend agnostic. [crossterm][], [termion][], [termwiz][], and your own backend are all supported
|
||||||
@@ -387,6 +389,26 @@ use ratatui::style::{Style, Modifier};
|
|||||||
textarea.set_cursor_line_style(Style::default());
|
textarea.set_cursor_line_style(Style::default());
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Use a native terminal cursor
|
||||||
|
|
||||||
|
By default, `TextArea` draws its cursor as a styled cell in the Ratatui buffer. This keeps existing rendering behavior
|
||||||
|
unchanged. Applications that want a native terminal cursor, such as a blinking bar, can hide the drawn cursor and place
|
||||||
|
the terminal cursor after rendering.
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
use tui_textarea::{CursorRenderMode, TextArea};
|
||||||
|
|
||||||
|
textarea.set_cursor_render_mode(CursorRenderMode::Hidden);
|
||||||
|
frame.render_widget(&textarea, area);
|
||||||
|
|
||||||
|
if let Some(position) = textarea.rendered_cursor_position() {
|
||||||
|
frame.set_cursor_position(position);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`tui-textarea-2` does not set backend-specific cursor shapes. Configure those in the application, for example with
|
||||||
|
`crossterm::cursor::SetCursorStyle::BlinkingBar` during terminal setup and reset the shape during teardown.
|
||||||
|
|
||||||
### Configure tab width
|
### Configure tab width
|
||||||
|
|
||||||
The default tab width is 4. To change it, use `TextArea::set_tab_length()` method. The following sets 2 to tab width.
|
The default tab width is 4. To change it, use `TextArea::set_tab_length()` method. The following sets 2 to tab width.
|
||||||
@@ -648,6 +670,9 @@ Useful state/configuration helpers:
|
|||||||
| `textarea.remove_block()` | Remove block chrome |
|
| `textarea.remove_block()` | Remove block chrome |
|
||||||
| `textarea.set_line_number_style(style)` | Enable or restyle line numbers |
|
| `textarea.set_line_number_style(style)` | Enable or restyle line numbers |
|
||||||
| `textarea.remove_line_number()` | Disable line numbers |
|
| `textarea.remove_line_number()` | Disable line numbers |
|
||||||
|
| `textarea.set_cursor_render_mode(mode)` | Draw or hide the textarea-owned cursor cell |
|
||||||
|
| `textarea.cursor_render_mode()` | Read the current cursor render mode |
|
||||||
|
| `textarea.rendered_cursor_position()` | Get the last rendered terminal cursor position |
|
||||||
| `textarea.set_placeholder_text(text)` | Set or disable placeholder text |
|
| `textarea.set_placeholder_text(text)` | Set or disable placeholder text |
|
||||||
| `textarea.set_placeholder_style(style)` | Change placeholder style |
|
| `textarea.set_placeholder_style(style)` | Change placeholder style |
|
||||||
| `textarea.set_mask_char(ch)` | Enable character masking |
|
| `textarea.set_mask_char(ch)` | Enable character masking |
|
||||||
|
|||||||
@@ -41,6 +41,18 @@ impl Boundary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub(crate) enum CursorCellRender {
|
||||||
|
Draw(Style),
|
||||||
|
Hidden,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Style> for CursorCellRender {
|
||||||
|
fn from(style: Style) -> Self {
|
||||||
|
Self::Draw(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct DisplayTextBuilder {
|
struct DisplayTextBuilder {
|
||||||
tab_len: u8,
|
tab_len: u8,
|
||||||
width: usize,
|
width: usize,
|
||||||
@@ -98,7 +110,7 @@ pub struct LineHighlighter<'a> {
|
|||||||
boundaries: Vec<(Boundary, usize)>,
|
boundaries: Vec<(Boundary, usize)>,
|
||||||
style_begin: Style,
|
style_begin: Style,
|
||||||
cursor_at_end: bool,
|
cursor_at_end: bool,
|
||||||
cursor_style: Style,
|
cursor_cell: CursorCellRender,
|
||||||
tab_len: u8,
|
tab_len: u8,
|
||||||
mask: Option<char>,
|
mask: Option<char>,
|
||||||
select_at_end: bool,
|
select_at_end: bool,
|
||||||
@@ -108,7 +120,7 @@ pub struct LineHighlighter<'a> {
|
|||||||
impl<'a> LineHighlighter<'a> {
|
impl<'a> LineHighlighter<'a> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
line: &'a str,
|
line: &'a str,
|
||||||
cursor_style: Style,
|
cursor_cell: impl Into<CursorCellRender>,
|
||||||
tab_len: u8,
|
tab_len: u8,
|
||||||
mask: Option<char>,
|
mask: Option<char>,
|
||||||
select_style: Style,
|
select_style: Style,
|
||||||
@@ -119,7 +131,7 @@ impl<'a> LineHighlighter<'a> {
|
|||||||
boundaries: vec![],
|
boundaries: vec![],
|
||||||
style_begin: Style::default(),
|
style_begin: Style::default(),
|
||||||
cursor_at_end: false,
|
cursor_at_end: false,
|
||||||
cursor_style,
|
cursor_cell: cursor_cell.into(),
|
||||||
tab_len,
|
tab_len,
|
||||||
mask,
|
mask,
|
||||||
select_at_end: false,
|
select_at_end: false,
|
||||||
@@ -138,12 +150,14 @@ impl<'a> LineHighlighter<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn cursor_line(&mut self, cursor_col: usize, style: Style) {
|
pub fn cursor_line(&mut self, cursor_col: usize, style: Style) {
|
||||||
if let Some((start, c)) = self.line.char_indices().nth(cursor_col) {
|
if let CursorCellRender::Draw(cursor_style) = self.cursor_cell {
|
||||||
self.boundaries
|
if let Some((start, c)) = self.line.char_indices().nth(cursor_col) {
|
||||||
.push((Boundary::Cursor(self.cursor_style), start));
|
self.boundaries
|
||||||
self.boundaries.push((Boundary::End, start + c.len_utf8()));
|
.push((Boundary::Cursor(cursor_style), start));
|
||||||
} else {
|
self.boundaries.push((Boundary::End, start + c.len_utf8()));
|
||||||
self.cursor_at_end = true;
|
} else {
|
||||||
|
self.cursor_at_end = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.style_begin = style;
|
self.style_begin = style;
|
||||||
}
|
}
|
||||||
@@ -247,7 +261,7 @@ impl<'a> LineHighlighter<'a> {
|
|||||||
mut boundaries,
|
mut boundaries,
|
||||||
tab_len,
|
tab_len,
|
||||||
style_begin,
|
style_begin,
|
||||||
cursor_style,
|
cursor_cell,
|
||||||
cursor_at_end,
|
cursor_at_end,
|
||||||
mask,
|
mask,
|
||||||
select_at_end,
|
select_at_end,
|
||||||
@@ -260,7 +274,7 @@ impl<'a> LineHighlighter<'a> {
|
|||||||
if !built.is_empty() {
|
if !built.is_empty() {
|
||||||
spans.push(Span::styled(built, style_begin));
|
spans.push(Span::styled(built, style_begin));
|
||||||
}
|
}
|
||||||
if cursor_at_end {
|
if let (true, CursorCellRender::Draw(cursor_style)) = (cursor_at_end, cursor_cell) {
|
||||||
spans.push(Span::styled(" ", cursor_style));
|
spans.push(Span::styled(" ", cursor_style));
|
||||||
} else if select_at_end {
|
} else if select_at_end {
|
||||||
spans.push(Span::styled(" ", select_style));
|
spans.push(Span::styled(" ", select_style));
|
||||||
@@ -295,7 +309,7 @@ impl<'a> LineHighlighter<'a> {
|
|||||||
spans.push(Span::styled(builder.build(&line[start..]), style));
|
spans.push(Span::styled(builder.build(&line[start..]), style));
|
||||||
}
|
}
|
||||||
|
|
||||||
if cursor_at_end {
|
if let (true, CursorCellRender::Draw(cursor_style)) = (cursor_at_end, cursor_cell) {
|
||||||
spans.push(Span::styled(" ", cursor_style));
|
spans.push(Span::styled(" ", cursor_style));
|
||||||
} else if select_at_end {
|
} else if select_at_end {
|
||||||
spans.push(Span::styled(" ", select_style));
|
spans.push(Span::styled(" ", select_style));
|
||||||
|
|||||||
@@ -44,6 +44,6 @@ pub use input::{Input, Key};
|
|||||||
pub use scroll::Scrolling;
|
pub use scroll::Scrolling;
|
||||||
pub use textarea::{
|
pub use textarea::{
|
||||||
AtomicCursorBias, AtomicDeleteDirection, AtomicRange, AtomicRangeError,
|
AtomicCursorBias, AtomicDeleteDirection, AtomicRange, AtomicRangeError,
|
||||||
AtomicRangeRejectReason, RejectedAtomicRange, TextArea, TextAreaMeasure,
|
AtomicRangeRejectReason, CursorRenderMode, RejectedAtomicRange, TextArea, TextAreaMeasure,
|
||||||
};
|
};
|
||||||
pub use wrap::WrapMode;
|
pub use wrap::WrapMode;
|
||||||
|
|||||||
@@ -51,11 +51,11 @@ impl Search {
|
|||||||
|
|
||||||
// Search current line after cursor
|
// Search current line after cursor
|
||||||
let start_col = if match_cursor { col } else { col + 1 };
|
let start_col = if match_cursor { col } else { col + 1 };
|
||||||
if let Some((i, _)) = current_line.char_indices().nth(start_col) {
|
if let Some((i, _)) = current_line.char_indices().nth(start_col)
|
||||||
if let Some(m) = pat.find_at(current_line, i) {
|
&& let Some(m) = pat.find_at(current_line, i)
|
||||||
let col = start_col + current_line[i..m.start()].chars().count();
|
{
|
||||||
return Some((row, col));
|
let col = start_col + current_line[i..m.start()].chars().count();
|
||||||
}
|
return Some((row, col));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search lines after cursor
|
// Search lines after cursor
|
||||||
@@ -108,15 +108,14 @@ impl Search {
|
|||||||
// Search current line before cursor
|
// Search current line before cursor
|
||||||
if col > 0 || match_cursor {
|
if col > 0 || match_cursor {
|
||||||
let start_col = if match_cursor { col } else { col - 1 };
|
let start_col = if match_cursor { col } else { col - 1 };
|
||||||
if let Some((i, _)) = current_line.char_indices().nth(start_col) {
|
if let Some((i, _)) = current_line.char_indices().nth(start_col)
|
||||||
if let Some(m) = pat
|
&& let Some(m) = pat
|
||||||
.find_iter(current_line)
|
.find_iter(current_line)
|
||||||
.take_while(|m| m.start() <= i)
|
.take_while(|m| m.start() <= i)
|
||||||
.last()
|
.last()
|
||||||
{
|
{
|
||||||
let col = current_line[..m.start()].chars().count();
|
let col = current_line[..m.start()].chars().count();
|
||||||
return Some((row, col));
|
return Some((row, col));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,15 +136,14 @@ impl Search {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Search current line after cursor
|
// Search current line after cursor
|
||||||
if let Some((i, _)) = current_line.char_indices().nth(col) {
|
if let Some((i, _)) = current_line.char_indices().nth(col)
|
||||||
if let Some(m) = pat
|
&& let Some(m) = pat
|
||||||
.find_iter(current_line)
|
.find_iter(current_line)
|
||||||
.skip_while(|m| m.start() < i)
|
.skip_while(|m| m.start() < i)
|
||||||
.last()
|
.last()
|
||||||
{
|
{
|
||||||
let col = col + current_line[i..m.start()].chars().count();
|
let col = col + current_line[i..m.start()].chars().count();
|
||||||
return Some((row, col));
|
return Some((row, col));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::cursor::{CursorMove, DataCursor, ScreenCursor};
|
use crate::cursor::{CursorMove, DataCursor, ScreenCursor};
|
||||||
use crate::highlight::LineHighlighter;
|
use crate::highlight::{CursorCellRender, LineHighlighter};
|
||||||
use crate::history::{Edit, EditKind, History};
|
use crate::history::{Edit, EditKind, History};
|
||||||
use crate::input::{Input, Key};
|
use crate::input::{Input, Key};
|
||||||
use crate::ratatui::layout::{Alignment, Rect};
|
use crate::ratatui::layout::{Alignment, Position, Rect};
|
||||||
use crate::ratatui::style::{Color, Modifier, Style};
|
use crate::ratatui::style::{Color, Modifier, Style};
|
||||||
use crate::ratatui::text::Line;
|
use crate::ratatui::text::Line;
|
||||||
use crate::ratatui::widgets::{Block, Widget};
|
use crate::ratatui::widgets::{Block, Widget};
|
||||||
@@ -135,6 +135,20 @@ pub struct TextAreaMeasure {
|
|||||||
pub max_rows: u16,
|
pub max_rows: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Controls whether [`TextArea`] draws its own visual cursor into the rendered buffer.
|
||||||
|
///
|
||||||
|
/// This does not control terminal cursor shape or blinking. Backend-specific cursor
|
||||||
|
/// shape setup, such as crossterm cursor styles, remains caller-owned.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum CursorRenderMode {
|
||||||
|
/// Draw the textarea cursor as a styled cell in the Ratatui buffer.
|
||||||
|
#[default]
|
||||||
|
Cell,
|
||||||
|
/// Do not draw a textarea-owned cursor cell.
|
||||||
|
Hidden,
|
||||||
|
}
|
||||||
|
|
||||||
/// A type to manage state of textarea. These are some important methods:
|
/// A type to manage state of textarea. These are some important methods:
|
||||||
///
|
///
|
||||||
/// - [`TextArea::default`] creates an empty textarea.
|
/// - [`TextArea::default`] creates an empty textarea.
|
||||||
@@ -196,6 +210,7 @@ pub struct TextArea<'a> {
|
|||||||
line_number_style: Option<Style>,
|
line_number_style: Option<Style>,
|
||||||
pub(crate) viewport: Viewport,
|
pub(crate) viewport: Viewport,
|
||||||
pub(crate) cursor_style: Style,
|
pub(crate) cursor_style: Style,
|
||||||
|
cursor_render_mode: CursorRenderMode,
|
||||||
yank: YankText,
|
yank: YankText,
|
||||||
#[cfg(feature = "search")]
|
#[cfg(feature = "search")]
|
||||||
search: Search,
|
search: Search,
|
||||||
@@ -214,6 +229,7 @@ pub struct TextArea<'a> {
|
|||||||
pub(crate) screen_lines: RefCell<Vec<ScreenLine>>,
|
pub(crate) screen_lines: RefCell<Vec<ScreenLine>>,
|
||||||
pub(crate) data_pointers: RefCell<Vec<DataLine>>,
|
pub(crate) data_pointers: RefCell<Vec<DataLine>>,
|
||||||
pub(crate) area: Cell<Rect>,
|
pub(crate) area: Cell<Rect>,
|
||||||
|
pub(crate) rendered_cursor_position: Cell<Option<Position>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert any iterator whose elements can be converted into [`String`] into [`TextArea`]. Each [`String`] element is
|
/// Convert any iterator whose elements can be converted into [`String`] into [`TextArea`]. Each [`String`] element is
|
||||||
@@ -314,6 +330,7 @@ impl<'a> TextArea<'a> {
|
|||||||
line_number_style: None,
|
line_number_style: None,
|
||||||
viewport: Viewport::default(),
|
viewport: Viewport::default(),
|
||||||
cursor_style: Style::default().add_modifier(Modifier::REVERSED),
|
cursor_style: Style::default().add_modifier(Modifier::REVERSED),
|
||||||
|
cursor_render_mode: CursorRenderMode::Cell,
|
||||||
yank: YankText::default(),
|
yank: YankText::default(),
|
||||||
#[cfg(feature = "search")]
|
#[cfg(feature = "search")]
|
||||||
search: Search::default(),
|
search: Search::default(),
|
||||||
@@ -332,6 +349,7 @@ impl<'a> TextArea<'a> {
|
|||||||
screen_lines: RefCell::new(Vec::new()),
|
screen_lines: RefCell::new(Vec::new()),
|
||||||
data_pointers: RefCell::new(Vec::new()),
|
data_pointers: RefCell::new(Vec::new()),
|
||||||
area: Cell::new(Rect::default()),
|
area: Cell::new(Rect::default()),
|
||||||
|
rendered_cursor_position: Cell::new(None),
|
||||||
};
|
};
|
||||||
textarea.screen_map_load();
|
textarea.screen_map_load();
|
||||||
textarea
|
textarea
|
||||||
@@ -2032,9 +2050,13 @@ impl<'a> TextArea<'a> {
|
|||||||
lnum_len: u8,
|
lnum_len: u8,
|
||||||
) -> Line<'b> {
|
) -> Line<'b> {
|
||||||
let fragment = &line[wrapped.start_byte..wrapped.end_byte];
|
let fragment = &line[wrapped.start_byte..wrapped.end_byte];
|
||||||
|
let cursor_cell = match self.cursor_render_mode {
|
||||||
|
CursorRenderMode::Cell => CursorCellRender::Draw(self.cursor_style),
|
||||||
|
CursorRenderMode::Hidden => CursorCellRender::Hidden,
|
||||||
|
};
|
||||||
let mut hl = LineHighlighter::new(
|
let mut hl = LineHighlighter::new(
|
||||||
fragment,
|
fragment,
|
||||||
self.cursor_style,
|
cursor_cell,
|
||||||
self.tab_len,
|
self.tab_len,
|
||||||
self.mask,
|
self.mask,
|
||||||
self.select_style,
|
self.select_style,
|
||||||
@@ -2497,8 +2519,11 @@ impl<'a> TextArea<'a> {
|
|||||||
self.mask
|
self.mask
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the style of cursor. By default, a cursor is rendered in the reversed color. Setting the same style as
|
/// Set the style of the buffer-drawn cursor cell.
|
||||||
/// cursor line hides a cursor.
|
///
|
||||||
|
/// By default, a cursor is rendered in the reversed color. This style is visible
|
||||||
|
/// when [`CursorRenderMode::Cell`] is active. It is stored but has no visual
|
||||||
|
/// effect while [`CursorRenderMode::Hidden`] is active.
|
||||||
/// ```
|
/// ```
|
||||||
/// use ratatui::style::{Style, Color};
|
/// use ratatui::style::{Style, Color};
|
||||||
/// use tui_textarea::TextArea;
|
/// use tui_textarea::TextArea;
|
||||||
@@ -2518,6 +2543,46 @@ impl<'a> TextArea<'a> {
|
|||||||
self.cursor_style
|
self.cursor_style
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set how the textarea-owned cursor is rendered into the Ratatui buffer.
|
||||||
|
///
|
||||||
|
/// [`CursorRenderMode::Cell`] is the default and preserves the historical
|
||||||
|
/// behavior. [`CursorRenderMode::Hidden`] suppresses only the textarea-owned
|
||||||
|
/// cursor cell so applications can place a native terminal cursor with
|
||||||
|
/// [`TextArea::rendered_cursor_position`].
|
||||||
|
pub fn set_cursor_render_mode(&mut self, mode: CursorRenderMode) {
|
||||||
|
self.cursor_render_mode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current cursor render mode.
|
||||||
|
pub fn cursor_render_mode(&self) -> CursorRenderMode {
|
||||||
|
self.cursor_render_mode
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the cursor position calculated during the most recent render.
|
||||||
|
///
|
||||||
|
/// This returns terminal-relative coordinates suitable for
|
||||||
|
/// `ratatui_core::terminal::Frame::set_cursor_position` after rendering this
|
||||||
|
/// widget. It returns `None` before the first render or when the cursor cannot be
|
||||||
|
/// represented inside the most recent render area. Cursor shape and blinking are
|
||||||
|
/// backend concerns and are not controlled by `tui-textarea-2`.
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// use tui_textarea::{CursorRenderMode, TextArea};
|
||||||
|
///
|
||||||
|
/// let mut textarea = TextArea::default();
|
||||||
|
/// textarea.set_cursor_render_mode(CursorRenderMode::Hidden);
|
||||||
|
///
|
||||||
|
/// # let area = ratatui::layout::Rect::default();
|
||||||
|
/// # let mut frame: ratatui::Frame = unimplemented!();
|
||||||
|
/// frame.render_widget(&textarea, area);
|
||||||
|
/// if let Some(position) = textarea.rendered_cursor_position() {
|
||||||
|
/// frame.set_cursor_position(position);
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn rendered_cursor_position(&self) -> Option<Position> {
|
||||||
|
self.rendered_cursor_position.get()
|
||||||
|
}
|
||||||
|
|
||||||
/// Get slice of line texts. This method borrows the content, but not moves. Note that the returned slice will
|
/// Get slice of line texts. This method borrows the content, but not moves. Note that the returned slice will
|
||||||
/// never be empty because an empty text means a slice containing one empty line. This is correct since any text
|
/// never be empty because an empty text means a slice containing one empty line. This is correct since any text
|
||||||
/// file must end with a newline.
|
/// file must end with a newline.
|
||||||
@@ -2747,10 +2812,10 @@ impl<'a> TextArea<'a> {
|
|||||||
/// assert_eq!(m.preferred_rows, 2);
|
/// assert_eq!(m.preferred_rows, 2);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn measure(&mut self, width_cols: u16) -> TextAreaMeasure {
|
pub fn measure(&mut self, width_cols: u16) -> TextAreaMeasure {
|
||||||
if let Some((cached_width, cached_result)) = self.measure_cache {
|
if let Some((cached_width, cached_result)) = self.measure_cache
|
||||||
if cached_width == width_cols {
|
&& cached_width == width_cols
|
||||||
return cached_result;
|
{
|
||||||
}
|
return cached_result;
|
||||||
}
|
}
|
||||||
|
|
||||||
let area = Rect {
|
let area = Rect {
|
||||||
|
|||||||
117
src/widget.rs
117
src/widget.rs
@@ -1,8 +1,8 @@
|
|||||||
use crate::ratatui::buffer::Buffer;
|
use crate::ratatui::buffer::Buffer;
|
||||||
use crate::ratatui::layout::Rect;
|
use crate::ratatui::layout::{Position, Rect};
|
||||||
use crate::ratatui::text::{Line, Span, Text};
|
use crate::ratatui::text::{Line, Span, Text};
|
||||||
use crate::ratatui::widgets::{Paragraph, Widget};
|
use crate::ratatui::widgets::{Paragraph, Widget};
|
||||||
use crate::textarea::TextArea;
|
use crate::textarea::{CursorRenderMode, TextArea};
|
||||||
use crate::util::num_digits;
|
use crate::util::num_digits;
|
||||||
use crate::wrap::WrapMode;
|
use crate::wrap::WrapMode;
|
||||||
use portable_atomic::{AtomicU64, Ordering};
|
use portable_atomic::{AtomicU64, Ordering};
|
||||||
@@ -88,6 +88,14 @@ fn next_scroll_top(prev_top: u16, cursor: u16, len: u16) -> u16 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct RenderPlan {
|
||||||
|
inner_area: Rect,
|
||||||
|
top_row: u16,
|
||||||
|
top_col: u16,
|
||||||
|
rendered_cursor_position: Option<Position>,
|
||||||
|
placeholder_active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> TextArea<'a> {
|
impl<'a> TextArea<'a> {
|
||||||
fn text_widget(&'a self, top_row: usize, height: usize) -> Text<'a> {
|
fn text_widget(&'a self, top_row: usize, height: usize) -> Text<'a> {
|
||||||
let lnum_len = num_digits(self.lines().len());
|
let lnum_len = num_digits(self.lines().len());
|
||||||
@@ -102,9 +110,13 @@ impl<'a> TextArea<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn placeholder_widget(&'a self) -> Text<'a> {
|
fn placeholder_widget(&'a self) -> Text<'a> {
|
||||||
let cursor = Span::styled(" ", self.cursor_style);
|
|
||||||
let text = Span::raw(self.placeholder.as_str());
|
let text = Span::raw(self.placeholder.as_str());
|
||||||
Text::from(Line::from(vec![cursor, text]))
|
if self.cursor_render_mode() == CursorRenderMode::Cell {
|
||||||
|
let cursor = Span::styled(" ", self.cursor_style);
|
||||||
|
Text::from(Line::from(vec![cursor, text]))
|
||||||
|
} else {
|
||||||
|
Text::from(Line::from(vec![text]))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_top_row(&self, prev_top: u16, height: u16) -> u16 {
|
fn scroll_top_row(&self, prev_top: u16, height: u16) -> u16 {
|
||||||
@@ -125,24 +137,25 @@ impl<'a> TextArea<'a> {
|
|||||||
}
|
}
|
||||||
next_scroll_top(prev_top, cursor, width)
|
next_scroll_top(prev_top, cursor, width)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for &TextArea<'_> {
|
fn render_plan(&self, area: Rect) -> RenderPlan {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
|
||||||
let inner_area = if let Some(b) = self.block() {
|
let inner_area = if let Some(b) = self.block() {
|
||||||
b.inner(area)
|
b.inner(area)
|
||||||
} else {
|
} else {
|
||||||
area
|
area
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.area.get() != inner_area {
|
if self.area.get() != inner_area {
|
||||||
self.area.set(inner_area);
|
self.area.set(inner_area);
|
||||||
self.screen_map_load();
|
self.screen_map_load();
|
||||||
}
|
}
|
||||||
let Rect { width, height, .. } = inner_area;
|
|
||||||
|
|
||||||
|
let Rect { width, height, .. } = inner_area;
|
||||||
|
let placeholder_active = !self.placeholder.is_empty() && self.is_empty();
|
||||||
let (prev_top_row, prev_top_col) = self.viewport.scroll_top();
|
let (prev_top_row, prev_top_col) = self.viewport.scroll_top();
|
||||||
let (text, style, top_row, top_col) = if !self.placeholder.is_empty() && self.is_empty() {
|
|
||||||
(self.placeholder_widget(), self.placeholder_style, 0, 0)
|
let (top_row, top_col) = if placeholder_active {
|
||||||
|
(0, 0)
|
||||||
} else {
|
} else {
|
||||||
let top_row = self.scroll_top_row(prev_top_row, height);
|
let top_row = self.scroll_top_row(prev_top_row, height);
|
||||||
let top_col = if self.wrap_mode() == WrapMode::None {
|
let top_col = if self.wrap_mode() == WrapMode::None {
|
||||||
@@ -150,30 +163,98 @@ impl Widget for &TextArea<'_> {
|
|||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
(top_row, top_col)
|
||||||
|
};
|
||||||
|
|
||||||
|
let rendered_cursor_position =
|
||||||
|
self.rendered_position_in(inner_area, top_row, top_col, placeholder_active);
|
||||||
|
|
||||||
|
RenderPlan {
|
||||||
|
inner_area,
|
||||||
|
top_row,
|
||||||
|
top_col,
|
||||||
|
rendered_cursor_position,
|
||||||
|
placeholder_active,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rendered_position_in(
|
||||||
|
&self,
|
||||||
|
inner_area: Rect,
|
||||||
|
top_row: u16,
|
||||||
|
top_col: u16,
|
||||||
|
placeholder_active: bool,
|
||||||
|
) -> Option<Position> {
|
||||||
|
if inner_area.width == 0 || inner_area.height == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if placeholder_active {
|
||||||
|
return Some(Position {
|
||||||
|
x: inner_area.x,
|
||||||
|
y: inner_area.y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let cursor = self.screen_cursor();
|
||||||
|
let cursor_row = cursor.row;
|
||||||
|
let top_row = top_row as usize;
|
||||||
|
let height = inner_area.height as usize;
|
||||||
|
if cursor_row < top_row || top_row.saturating_add(height) <= cursor_row {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cursor_col = cursor.col;
|
||||||
|
if self.line_number_style().is_some() {
|
||||||
|
cursor_col = cursor_col.saturating_add(num_digits(self.lines().len()) as usize + 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let top_col = top_col as usize;
|
||||||
|
let width = inner_area.width as usize;
|
||||||
|
if cursor_col < top_col || top_col.saturating_add(width) <= cursor_col {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Position {
|
||||||
|
x: inner_area.x + (cursor_col - top_col) as u16,
|
||||||
|
y: inner_area.y + (cursor_row - top_row) as u16,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for &TextArea<'_> {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let plan = self.render_plan(area);
|
||||||
|
let Rect { width, height, .. } = plan.inner_area;
|
||||||
|
|
||||||
|
let (text, style) = if plan.placeholder_active {
|
||||||
|
(self.placeholder_widget(), self.placeholder_style)
|
||||||
|
} else {
|
||||||
(
|
(
|
||||||
self.text_widget(top_row as _, height as _),
|
self.text_widget(plan.top_row as _, height as _),
|
||||||
self.style(),
|
self.style(),
|
||||||
top_row,
|
|
||||||
top_col,
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// To get fine control over the text color and the surrrounding block they have to be rendered separately
|
// To get fine control over the text color and the surrrounding block they have to be rendered separately
|
||||||
// see https://github.com/ratatui/ratatui/issues/144
|
// see https://github.com/ratatui/ratatui/issues/144
|
||||||
let mut text_area = area;
|
let mut text_area = plan.inner_area;
|
||||||
let mut inner = Paragraph::new(text)
|
let mut inner = Paragraph::new(text)
|
||||||
.style(style)
|
.style(style)
|
||||||
.alignment(self.alignment());
|
.alignment(self.alignment());
|
||||||
if let Some(b) = self.block() {
|
if let Some(b) = self.block() {
|
||||||
text_area = b.inner(area);
|
text_area = plan.inner_area;
|
||||||
b.render(area, buf)
|
b.render(area, buf)
|
||||||
}
|
}
|
||||||
if top_col != 0 {
|
if plan.top_col != 0 {
|
||||||
inner = inner.scroll((0, top_col));
|
inner = inner.scroll((0, plan.top_col));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store scroll top position for rendering on the next tick
|
// Store scroll top position for rendering on the next tick
|
||||||
self.viewport.store(top_row, top_col, width, height);
|
self.viewport
|
||||||
|
.store(plan.top_row, plan.top_col, width, height);
|
||||||
|
self.rendered_cursor_position
|
||||||
|
.set(plan.rendered_cursor_position);
|
||||||
|
|
||||||
inner.render(text_area, buf);
|
inner.render(text_area, buf);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
use ratatui::layout::Alignment;
|
use ratatui::buffer::Buffer;
|
||||||
|
use ratatui::layout::{Alignment, Position, Rect};
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
|
use ratatui::widgets::Widget as _;
|
||||||
use ratatui::widgets::{Block, Borders};
|
use ratatui::widgets::{Block, Borders};
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use tui_textarea::{CursorMove, TextArea};
|
use tui_textarea::{CursorMove, CursorRenderMode, TextArea, WrapMode};
|
||||||
|
|
||||||
fn assert_undo_redo<T: Debug>(
|
fn assert_undo_redo<T: Debug>(
|
||||||
before_pos: (usize, usize),
|
before_pos: (usize, usize),
|
||||||
@@ -34,6 +36,12 @@ fn assert_no_undo_redo<T: Debug>(t: &mut TextArea<'_>, context: T) {
|
|||||||
assert_eq!(t.cursor(), pos, "pos after redo: {context:?}");
|
assert_eq!(t.cursor(), pos, "pos after redo: {context:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_textarea(textarea: &TextArea<'_>, area: Rect) -> Buffer {
|
||||||
|
let mut buf = Buffer::empty(area);
|
||||||
|
textarea.render(area, &mut buf);
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_insert_soft_tab() {
|
fn test_insert_soft_tab() {
|
||||||
for test in [
|
for test in [
|
||||||
@@ -1621,6 +1629,7 @@ fn test_set_lines_preserves_non_content_configuration() {
|
|||||||
t.set_block(Block::default().borders(Borders::ALL).title("Input"));
|
t.set_block(Block::default().borders(Borders::ALL).title("Input"));
|
||||||
t.set_style(base_style);
|
t.set_style(base_style);
|
||||||
t.set_cursor_style(cursor_style);
|
t.set_cursor_style(cursor_style);
|
||||||
|
t.set_cursor_render_mode(CursorRenderMode::Hidden);
|
||||||
t.set_cursor_line_style(cursor_line_style);
|
t.set_cursor_line_style(cursor_line_style);
|
||||||
t.set_selection_style(selection_style);
|
t.set_selection_style(selection_style);
|
||||||
t.set_tab_length(8);
|
t.set_tab_length(8);
|
||||||
@@ -1642,6 +1651,7 @@ fn test_set_lines_preserves_non_content_configuration() {
|
|||||||
assert!(t.block().is_some());
|
assert!(t.block().is_some());
|
||||||
assert_eq!(t.style(), base_style);
|
assert_eq!(t.style(), base_style);
|
||||||
assert_eq!(t.cursor_style(), cursor_style);
|
assert_eq!(t.cursor_style(), cursor_style);
|
||||||
|
assert_eq!(t.cursor_render_mode(), CursorRenderMode::Hidden);
|
||||||
assert_eq!(t.cursor_line_style(), cursor_line_style);
|
assert_eq!(t.cursor_line_style(), cursor_line_style);
|
||||||
assert_eq!(t.selection_style(), selection_style);
|
assert_eq!(t.selection_style(), selection_style);
|
||||||
assert_eq!(t.tab_length(), 8);
|
assert_eq!(t.tab_length(), 8);
|
||||||
@@ -1656,6 +1666,195 @@ fn test_set_lines_preserves_non_content_configuration() {
|
|||||||
assert_eq!(t.max_histories(), 3);
|
assert_eq!(t.max_histories(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cursor_render_mode_defaults_to_cell() {
|
||||||
|
assert_eq!(
|
||||||
|
TextArea::default().cursor_render_mode(),
|
||||||
|
CursorRenderMode::Cell
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_cursor_render_mode_round_trips() {
|
||||||
|
let mut textarea = TextArea::default();
|
||||||
|
textarea.set_cursor_render_mode(CursorRenderMode::Hidden);
|
||||||
|
assert_eq!(textarea.cursor_render_mode(), CursorRenderMode::Hidden);
|
||||||
|
|
||||||
|
textarea.set_cursor_render_mode(CursorRenderMode::Cell);
|
||||||
|
assert_eq!(textarea.cursor_render_mode(), CursorRenderMode::Cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cell_mode_keeps_existing_cursor_rendering() {
|
||||||
|
let mut textarea = TextArea::from(["abc"]);
|
||||||
|
let cursor_style = Style::default().bg(Color::Red);
|
||||||
|
textarea.set_cursor_style(cursor_style);
|
||||||
|
textarea.set_cursor_line_style(Style::default());
|
||||||
|
textarea.move_cursor(CursorMove::Jump(0, 1));
|
||||||
|
|
||||||
|
let buf = render_textarea(&textarea, Rect::new(0, 0, 5, 1));
|
||||||
|
|
||||||
|
assert_eq!(buf[(1, 0)].symbol(), "b");
|
||||||
|
assert_eq!(buf[(1, 0)].style().bg, cursor_style.bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hidden_mode_does_not_draw_cursor_cell_inside_text() {
|
||||||
|
let mut textarea = TextArea::from(["abc"]);
|
||||||
|
textarea.set_cursor_style(Style::default().bg(Color::Red));
|
||||||
|
textarea.set_cursor_line_style(Style::default());
|
||||||
|
textarea.set_cursor_render_mode(CursorRenderMode::Hidden);
|
||||||
|
textarea.move_cursor(CursorMove::Jump(0, 1));
|
||||||
|
|
||||||
|
let buf = render_textarea(&textarea, Rect::new(0, 0, 5, 1));
|
||||||
|
|
||||||
|
assert_eq!(buf[(1, 0)].symbol(), "b");
|
||||||
|
assert_ne!(buf[(1, 0)].style().bg, Some(Color::Red));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hidden_mode_does_not_append_cursor_space_at_line_end() {
|
||||||
|
let mut textarea = TextArea::from(["abc"]);
|
||||||
|
let cursor_style = Style::default().bg(Color::Red);
|
||||||
|
textarea.set_cursor_style(cursor_style);
|
||||||
|
textarea.set_cursor_line_style(Style::default());
|
||||||
|
textarea.set_cursor_render_mode(CursorRenderMode::Hidden);
|
||||||
|
textarea.move_cursor(CursorMove::End);
|
||||||
|
|
||||||
|
let buf = render_textarea(&textarea, Rect::new(0, 0, 5, 1));
|
||||||
|
|
||||||
|
assert_eq!(buf[(0, 0)].symbol(), "a");
|
||||||
|
assert_eq!(buf[(1, 0)].symbol(), "b");
|
||||||
|
assert_eq!(buf[(2, 0)].symbol(), "c");
|
||||||
|
assert_eq!(buf[(3, 0)].symbol(), " ");
|
||||||
|
assert_ne!(buf[(3, 0)].style().bg, cursor_style.bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hidden_mode_placeholder_does_not_draw_cursor_cell() {
|
||||||
|
let mut textarea = TextArea::default();
|
||||||
|
textarea.set_placeholder_text("Type here");
|
||||||
|
textarea.set_cursor_style(Style::default().bg(Color::Red));
|
||||||
|
textarea.set_cursor_render_mode(CursorRenderMode::Hidden);
|
||||||
|
|
||||||
|
let buf = render_textarea(&textarea, Rect::new(0, 0, 12, 1));
|
||||||
|
|
||||||
|
assert_eq!(buf[(0, 0)].symbol(), "T");
|
||||||
|
assert_eq!(
|
||||||
|
textarea.rendered_cursor_position(),
|
||||||
|
Some(Position { x: 0, y: 0 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rendered_cursor_position_returns_none_before_first_render() {
|
||||||
|
let textarea = TextArea::default();
|
||||||
|
assert_eq!(textarea.rendered_cursor_position(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rendered_cursor_position_after_default_render() {
|
||||||
|
let mut textarea = TextArea::from(["abc"]);
|
||||||
|
textarea.move_cursor(CursorMove::Jump(0, 2));
|
||||||
|
|
||||||
|
render_textarea(&textarea, Rect::new(10, 5, 20, 3));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
textarea.rendered_cursor_position(),
|
||||||
|
Some(Position { x: 12, y: 5 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rendered_cursor_position_accounts_for_block() {
|
||||||
|
let mut textarea = TextArea::from(["abc"]);
|
||||||
|
textarea.set_block(Block::default().borders(Borders::ALL));
|
||||||
|
textarea.move_cursor(CursorMove::Jump(0, 2));
|
||||||
|
|
||||||
|
render_textarea(&textarea, Rect::new(10, 5, 20, 5));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
textarea.rendered_cursor_position(),
|
||||||
|
Some(Position { x: 13, y: 6 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rendered_cursor_position_accounts_for_line_numbers() {
|
||||||
|
let mut textarea = TextArea::from(["abc"]);
|
||||||
|
textarea.set_line_number_style(Style::default());
|
||||||
|
textarea.move_cursor(CursorMove::Jump(0, 1));
|
||||||
|
|
||||||
|
render_textarea(&textarea, Rect::new(10, 5, 20, 3));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
textarea.rendered_cursor_position(),
|
||||||
|
Some(Position { x: 14, y: 5 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rendered_cursor_position_accounts_for_horizontal_scroll() {
|
||||||
|
let mut textarea = TextArea::from(["abcdefghi"]);
|
||||||
|
textarea.move_cursor(CursorMove::End);
|
||||||
|
|
||||||
|
render_textarea(&textarea, Rect::new(0, 0, 5, 1));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
textarea.rendered_cursor_position(),
|
||||||
|
Some(Position { x: 4, y: 0 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rendered_cursor_position_accounts_for_wrap() {
|
||||||
|
let mut textarea = TextArea::from(["abcdefghij"]);
|
||||||
|
textarea.set_wrap_mode(WrapMode::WordOrGlyph);
|
||||||
|
textarea.move_cursor(CursorMove::Jump(0, 7));
|
||||||
|
|
||||||
|
render_textarea(&textarea, Rect::new(4, 3, 5, 3));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
textarea.rendered_cursor_position(),
|
||||||
|
Some(Position { x: 6, y: 4 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rendered_cursor_position_returns_none_for_empty_area() {
|
||||||
|
let textarea = TextArea::default();
|
||||||
|
|
||||||
|
render_textarea(&textarea, Rect::new(0, 0, 0, 1));
|
||||||
|
|
||||||
|
assert_eq!(textarea.rendered_cursor_position(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rendered_cursor_position_handles_wide_unicode() {
|
||||||
|
let mut textarea = TextArea::from(["aあb"]);
|
||||||
|
textarea.move_cursor(CursorMove::Jump(0, 2));
|
||||||
|
|
||||||
|
render_textarea(&textarea, Rect::new(0, 0, 10, 1));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
textarea.rendered_cursor_position(),
|
||||||
|
Some(Position { x: 3, y: 0 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rendered_cursor_position_handles_tabs() {
|
||||||
|
let mut textarea = TextArea::from(["a\tb"]);
|
||||||
|
textarea.move_cursor(CursorMove::Jump(0, 2));
|
||||||
|
|
||||||
|
render_textarea(&textarea, Rect::new(0, 0, 10, 1));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
textarea.rendered_cursor_position(),
|
||||||
|
Some(Position { x: 4, y: 0 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_set_lines_clamps_cursor_and_clears_selection_and_history() {
|
fn test_set_lines_clamps_cursor_and_clears_selection_and_history() {
|
||||||
let mut t = TextArea::from(["hello", "world"]);
|
let mut t = TextArea::from(["hello", "world"]);
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user