зеркало из
https://github.com/glebtv/tui-textarea.git
synced 2026-09-07 16:05:50 +03:00
Merge branch 'scroll' (fix #2)
Этот коммит содержится в:
@@ -15,6 +15,7 @@ text editor can be easily put as part of your TUI application.
|
|||||||
- Line number
|
- Line number
|
||||||
- Cursor line highlight
|
- Cursor line highlight
|
||||||
- Search with regular expressions
|
- Search with regular expressions
|
||||||
|
- 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][], and your own backend are all supported
|
- Backend agnostic. [crossterm][], [termion][], and your own backend are all supported
|
||||||
- Multiple textarea widgets in the same screen
|
- Multiple textarea widgets in the same screen
|
||||||
@@ -410,9 +411,11 @@ notify how to move the cursor.
|
|||||||
| `textarea.move_cursor(CursorMove::Top)` | Move cursor to top of lines |
|
| `textarea.move_cursor(CursorMove::Top)` | Move cursor to top of lines |
|
||||||
| `textarea.move_cursor(CursorMove::Bottom)` | Move cursor to bottom of lines |
|
| `textarea.move_cursor(CursorMove::Bottom)` | Move cursor to bottom of lines |
|
||||||
| `textarea.move_cursor(CursorMove::Jump(row, col))` | Move cursor to (row, col) position |
|
| `textarea.move_cursor(CursorMove::Jump(row, col))` | Move cursor to (row, col) position |
|
||||||
|
| `textarea.move_cursor(CursorMove::InViewport)` | Move cursor to stay in the viewport |
|
||||||
| `textarea.set_search_pattern(pattern)` | Set a pattern for text search |
|
| `textarea.set_search_pattern(pattern)` | Set a pattern for text search |
|
||||||
| `textarea.search_forward(match_cursor)` | Move cursor to next match of text search |
|
| `textarea.search_forward(match_cursor)` | Move cursor to next match of text search |
|
||||||
| `textarea.search_back(match_cursor)` | Move cursor to previous match of text search |
|
| `textarea.search_back(match_cursor)` | Move cursor to previous match of text search |
|
||||||
|
| `textarea.scroll(rows, cols)` | Scroll the viewport |
|
||||||
|
|
||||||
To define your own key mappings, simply call the above methods in your code instead of `TextArea::input()` method. The
|
To define your own key mappings, simply call the above methods in your code instead of `TextArea::input()` method. The
|
||||||
following example defines modal key mappings like Vim.
|
following example defines modal key mappings like Vim.
|
||||||
@@ -465,6 +468,8 @@ loop {
|
|||||||
Input { key: Key::Esc, .. } => textarea.set_search_pattern("").unwrap(),
|
Input { key: Key::Esc, .. } => textarea.set_search_pattern("").unwrap(),
|
||||||
Input { key: Key::Char('n'), .. } => textarea.search_forward(),
|
Input { key: Key::Char('n'), .. } => textarea.search_forward(),
|
||||||
Input { key: Key::Char('N'), .. } => textarea.search_back(),
|
Input { key: Key::Char('N'), .. } => textarea.search_back(),
|
||||||
|
Input { key: Key::Char('e'), ctrl: true .. } => textarea.scroll(1, 0),
|
||||||
|
Input { key: Key::Char('y'), ctrl: true .. } => textarea.scroll(-1, 0),
|
||||||
_ => {},
|
_ => {},
|
||||||
},
|
},
|
||||||
Mode::Insert => match read()?.into() {
|
Mode::Insert => match read()?.into() {
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ use std::io;
|
|||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use termion::event::Key;
|
use termion::event::Event as TermEvent;
|
||||||
use termion::input::{MouseTerminal, TermRead};
|
use termion::input::{MouseTerminal, TermRead};
|
||||||
use termion::raw::IntoRawMode;
|
use termion::raw::IntoRawMode;
|
||||||
use termion::screen::AlternateScreen;
|
use termion::screen::AlternateScreen;
|
||||||
use tui::backend::TermionBackend;
|
use tui::backend::TermionBackend;
|
||||||
use tui::widgets::{Block, Borders};
|
use tui::widgets::{Block, Borders};
|
||||||
use tui::Terminal;
|
use tui::Terminal;
|
||||||
use tui_textarea::TextArea;
|
use tui_textarea::{Input, Key, TextArea};
|
||||||
|
|
||||||
enum Event {
|
enum Event {
|
||||||
Key(Key),
|
Term(TermEvent),
|
||||||
Tick,
|
Tick,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,12 +25,12 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let mut term = Terminal::new(backend)?;
|
let mut term = Terminal::new(backend)?;
|
||||||
|
|
||||||
let events = {
|
let events = {
|
||||||
let keys = io::stdin().keys();
|
let events = io::stdin().events();
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
let keys_tx = tx.clone();
|
let keys_tx = tx.clone();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
for key in keys.flatten() {
|
for event in events.flatten() {
|
||||||
keys_tx.send(Event::Key(key)).unwrap();
|
keys_tx.send(Event::Term(event)).unwrap();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
thread::spawn(move || loop {
|
thread::spawn(move || loop {
|
||||||
@@ -49,10 +49,12 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
match events.recv()? {
|
match events.recv()? {
|
||||||
Event::Key(Key::Esc) => break,
|
Event::Term(event) => match event.into() {
|
||||||
Event::Key(key) => {
|
Input { key: Key::Esc, .. } => break,
|
||||||
textarea.input(key);
|
input => {
|
||||||
}
|
textarea.input(input);
|
||||||
|
}
|
||||||
|
},
|
||||||
Event::Tick => {}
|
Event::Tick => {}
|
||||||
}
|
}
|
||||||
term.draw(|f| {
|
term.draw(|f| {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::widget::Viewport;
|
||||||
use crate::word::{find_word_start_backward, find_word_start_forward};
|
use crate::word::{find_word_start_backward, find_word_start_forward};
|
||||||
#[cfg(feature = "arbitrary")]
|
#[cfg(feature = "arbitrary")]
|
||||||
use arbitrary::Arbitrary;
|
use arbitrary::Arbitrary;
|
||||||
@@ -187,6 +188,38 @@ pub enum CursorMove {
|
|||||||
/// assert_eq!(textarea.cursor(), (2, 4));
|
/// assert_eq!(textarea.cursor(), (2, 4));
|
||||||
/// ```
|
/// ```
|
||||||
Jump(u16, u16),
|
Jump(u16, u16),
|
||||||
|
/// Move cursor to keep it within the viewport. For example, when a viewport displays line 8 to line 16:
|
||||||
|
///
|
||||||
|
/// - cursor at line 4 is moved to line 8
|
||||||
|
/// - cursor at line 20 is moved to line 16
|
||||||
|
/// - cursor at line 12 is not moved
|
||||||
|
///
|
||||||
|
/// This is useful when you moved a cursor but you don't want to move the viewport.
|
||||||
|
/// ```
|
||||||
|
/// # use tui::buffer::Buffer;
|
||||||
|
/// # use tui::layout::Rect;
|
||||||
|
/// # use tui::widgets::Widget;
|
||||||
|
/// use tui_textarea::{TextArea, CursorMove};
|
||||||
|
///
|
||||||
|
/// // Let's say terminal height is 8.
|
||||||
|
///
|
||||||
|
/// // Create textarea with 20 lines "0", "1", "2", "3", ...
|
||||||
|
/// // The viewport is displaying from line 1 to line 8.
|
||||||
|
/// let mut textarea: TextArea = (0..20).into_iter().map(|i| i.to_string()).collect();
|
||||||
|
/// # // Call `render` at least once to populate terminal size
|
||||||
|
/// # let r = Rect { x: 0, y: 0, width: 24, height: 8 };
|
||||||
|
/// # let mut b = Buffer::empty(r.clone());
|
||||||
|
/// # textarea.widget().render(r, &mut b);
|
||||||
|
///
|
||||||
|
/// // Move cursor to the end of lines (line 20). It is outside the viewport (line 1 to line 8)
|
||||||
|
/// textarea.move_cursor(CursorMove::Bottom);
|
||||||
|
/// assert_eq!(textarea.cursor(), (19, 0));
|
||||||
|
///
|
||||||
|
/// // Cursor is moved to line 8 to enter the viewport
|
||||||
|
/// textarea.move_cursor(CursorMove::InViewport);
|
||||||
|
/// assert_eq!(textarea.cursor(), (7, 0));
|
||||||
|
/// ```
|
||||||
|
InViewport,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CursorMove {
|
impl CursorMove {
|
||||||
@@ -194,6 +227,7 @@ impl CursorMove {
|
|||||||
&self,
|
&self,
|
||||||
(row, col): (usize, usize),
|
(row, col): (usize, usize),
|
||||||
lines: &[String],
|
lines: &[String],
|
||||||
|
viewport: &Viewport,
|
||||||
) -> Option<(usize, usize)> {
|
) -> Option<(usize, usize)> {
|
||||||
use CursorMove::*;
|
use CursorMove::*;
|
||||||
|
|
||||||
@@ -271,6 +305,16 @@ impl CursorMove {
|
|||||||
let col = fit_col(*col as usize, &lines[row]);
|
let col = fit_col(*col as usize, &lines[row]);
|
||||||
Some((row, col))
|
Some((row, col))
|
||||||
}
|
}
|
||||||
|
InViewport => {
|
||||||
|
let (row_top, col_top, row_bottom, col_bottom) = viewport.position();
|
||||||
|
|
||||||
|
let row = row.clamp(row_top as usize, row_bottom as usize);
|
||||||
|
let row = cmp::min(row, lines.len() - 1);
|
||||||
|
let col = col.clamp(col_top as usize, col_bottom as usize);
|
||||||
|
let col = fit_col(col, &lines[row]);
|
||||||
|
|
||||||
|
Some((row, col))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
src/input.rs
64
src/input.rs
@@ -1,9 +1,12 @@
|
|||||||
#[cfg(feature = "arbitrary")]
|
#[cfg(feature = "arbitrary")]
|
||||||
use arbitrary::Arbitrary;
|
use arbitrary::Arbitrary;
|
||||||
#[cfg(feature = "crossterm")]
|
#[cfg(feature = "crossterm")]
|
||||||
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{
|
||||||
|
Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers, MouseEvent as CrosstermMouseEvent,
|
||||||
|
MouseEventKind as CrosstermMouseEventKind,
|
||||||
|
};
|
||||||
#[cfg(feature = "termion")]
|
#[cfg(feature = "termion")]
|
||||||
use termion::event::{Event as TerimonEvent, Key as TermionKey};
|
use termion::event::{Event as TermionEvent, Key as TermionKey, MouseEvent as TermionMouseEvent};
|
||||||
|
|
||||||
/// Backend-agnostic key input kind.
|
/// Backend-agnostic key input kind.
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
@@ -27,6 +30,10 @@ pub enum Key {
|
|||||||
PageUp,
|
PageUp,
|
||||||
PageDown,
|
PageDown,
|
||||||
Esc,
|
Esc,
|
||||||
|
/// Virtual key to scroll down by mouse
|
||||||
|
MouseScrollDown,
|
||||||
|
/// Virtual key to scroll up by mouse
|
||||||
|
MouseScrollUp,
|
||||||
/// An invalid key input (this key is always ignored by [`TextArea`](crate::TextArea)).
|
/// An invalid key input (this key is always ignored by [`TextArea`](crate::TextArea)).
|
||||||
Null,
|
Null,
|
||||||
}
|
}
|
||||||
@@ -90,10 +97,10 @@ impl Default for Input {
|
|||||||
impl From<CrosstermEvent> for Input {
|
impl From<CrosstermEvent> for Input {
|
||||||
/// Convert [`crossterm::event::Event`] to [`Input`].
|
/// Convert [`crossterm::event::Event`] to [`Input`].
|
||||||
fn from(event: CrosstermEvent) -> Self {
|
fn from(event: CrosstermEvent) -> Self {
|
||||||
if let CrosstermEvent::Key(key) = event {
|
match event {
|
||||||
Self::from(key)
|
CrosstermEvent::Key(key) => Self::from(key),
|
||||||
} else {
|
CrosstermEvent::Mouse(mouse) => Self::from(mouse),
|
||||||
Self::default()
|
_ => Self::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,14 +133,29 @@ impl From<KeyEvent> for Input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "crossterm")]
|
||||||
|
impl From<CrosstermMouseEvent> for Input {
|
||||||
|
/// Convert [`crossterm::event::MouseEvent`] to [`Input`].
|
||||||
|
fn from(mouse: CrosstermMouseEvent) -> Self {
|
||||||
|
let key = match mouse.kind {
|
||||||
|
CrosstermMouseEventKind::ScrollDown => Key::MouseScrollDown,
|
||||||
|
CrosstermMouseEventKind::ScrollUp => Key::MouseScrollUp,
|
||||||
|
_ => return Self::default(),
|
||||||
|
};
|
||||||
|
let ctrl = mouse.modifiers.contains(KeyModifiers::CONTROL);
|
||||||
|
let alt = mouse.modifiers.contains(KeyModifiers::ALT);
|
||||||
|
Self { key, ctrl, alt }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "termion")]
|
#[cfg(feature = "termion")]
|
||||||
impl From<TerimonEvent> for Input {
|
impl From<TermionEvent> for Input {
|
||||||
/// Convert [`termion::event::Event`] to [`Input`].
|
/// Convert [`termion::event::Event`] to [`Input`].
|
||||||
fn from(event: TerimonEvent) -> Self {
|
fn from(event: TermionEvent) -> Self {
|
||||||
if let TerimonEvent::Key(key) = event {
|
match event {
|
||||||
Self::from(key)
|
TermionEvent::Key(key) => Self::from(key),
|
||||||
} else {
|
TermionEvent::Mouse(mouse) => Self::from(mouse),
|
||||||
Self::default()
|
_ => Self::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,3 +198,21 @@ impl From<TermionKey> for Input {
|
|||||||
Input { key, ctrl, alt }
|
Input { key, ctrl, alt }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "termion")]
|
||||||
|
impl From<TermionMouseEvent> for Input {
|
||||||
|
/// Convert [`termion::event::MouseEvent`] to [`Input`].
|
||||||
|
fn from(mouse: TermionMouseEvent) -> Self {
|
||||||
|
use termion::event::MouseButton;
|
||||||
|
let key = match mouse {
|
||||||
|
TermionMouseEvent::Press(MouseButton::WheelUp, ..) => Key::MouseScrollUp,
|
||||||
|
TermionMouseEvent::Press(MouseButton::WheelDown, ..) => Key::MouseScrollDown,
|
||||||
|
_ => return Self::default(),
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
key,
|
||||||
|
ctrl: false,
|
||||||
|
alt: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::input::{Input, Key};
|
|||||||
#[cfg(feature = "search")]
|
#[cfg(feature = "search")]
|
||||||
use crate::search::Search;
|
use crate::search::Search;
|
||||||
use crate::util::spaces;
|
use crate::util::spaces;
|
||||||
use crate::widget::{Renderer, ScrollTop};
|
use crate::widget::{Renderer, Viewport};
|
||||||
use crate::word::{find_word_end_forward, find_word_start_backward};
|
use crate::word::{find_word_end_forward, find_word_start_backward};
|
||||||
use tui::style::{Modifier, Style};
|
use tui::style::{Modifier, Style};
|
||||||
use tui::text::Spans;
|
use tui::text::Spans;
|
||||||
@@ -42,7 +42,7 @@ pub struct TextArea<'a> {
|
|||||||
history: History,
|
history: History,
|
||||||
cursor_line_style: Style,
|
cursor_line_style: Style,
|
||||||
line_number_style: Option<Style>,
|
line_number_style: Option<Style>,
|
||||||
pub(crate) scroll_top: ScrollTop,
|
pub(crate) viewport: Viewport,
|
||||||
cursor_style: Style,
|
cursor_style: Style,
|
||||||
yank: String,
|
yank: String,
|
||||||
#[cfg(feature = "search")]
|
#[cfg(feature = "search")]
|
||||||
@@ -138,7 +138,7 @@ impl<'a> TextArea<'a> {
|
|||||||
history: History::new(50),
|
history: History::new(50),
|
||||||
cursor_line_style: Style::default().add_modifier(Modifier::UNDERLINED),
|
cursor_line_style: Style::default().add_modifier(Modifier::UNDERLINED),
|
||||||
line_number_style: None,
|
line_number_style: None,
|
||||||
scroll_top: ScrollTop::default(),
|
viewport: Viewport::default(),
|
||||||
cursor_style: Style::default().add_modifier(Modifier::REVERSED),
|
cursor_style: Style::default().add_modifier(Modifier::REVERSED),
|
||||||
yank: String::new(),
|
yank: String::new(),
|
||||||
#[cfg(feature = "search")]
|
#[cfg(feature = "search")]
|
||||||
@@ -441,6 +441,20 @@ impl<'a> TextArea<'a> {
|
|||||||
ctrl: true,
|
ctrl: true,
|
||||||
alt: false,
|
alt: false,
|
||||||
} => self.paste(),
|
} => self.paste(),
|
||||||
|
Input {
|
||||||
|
key: Key::MouseScrollDown,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.scroll(1, 0);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::MouseScrollUp,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.scroll(-1, 0);
|
||||||
|
false
|
||||||
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -505,6 +519,20 @@ impl<'a> TextArea<'a> {
|
|||||||
self.insert_newline();
|
self.insert_newline();
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::MouseScrollDown,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.scroll(1, 0);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::MouseScrollUp,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.scroll(-1, 0);
|
||||||
|
false
|
||||||
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -868,7 +896,7 @@ impl<'a> TextArea<'a> {
|
|||||||
/// assert_eq!(textarea.cursor(), (1, 1));
|
/// assert_eq!(textarea.cursor(), (1, 1));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn move_cursor(&mut self, m: CursorMove) {
|
pub fn move_cursor(&mut self, m: CursorMove) {
|
||||||
if let Some(cursor) = m.next_cursor(self.cursor, &self.lines) {
|
if let Some(cursor) = m.next_cursor(self.cursor, &self.lines, &self.viewport) {
|
||||||
self.cursor = cursor;
|
self.cursor = cursor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1446,6 +1474,7 @@ impl<'a> TextArea<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the text style at matches of text search. The default style is colored with blue in background.
|
/// Set the text style at matches of text search. The default style is colored with blue in background.
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use tui::style::{Style, Color};
|
/// use tui::style::{Style, Color};
|
||||||
/// use tui_textarea::TextArea;
|
/// use tui_textarea::TextArea;
|
||||||
@@ -1462,4 +1491,48 @@ impl<'a> TextArea<'a> {
|
|||||||
pub fn set_search_style(&mut self, style: Style) {
|
pub fn set_search_style(&mut self, style: Style) {
|
||||||
self.search.style = style;
|
self.search.style = style;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scroll the textarea by `delta_row` rows (vertically) and `delta_col` columns (horizontally). Positive scroll
|
||||||
|
/// amount means scrolling down or right. And negative scroll amount means scrolling up or left. The cursor will not
|
||||||
|
/// move until it goes out the viewport. When the cursor position is outside the viewport after scroll, the cursor
|
||||||
|
/// position will be adjusted to stay in the viewport using the same logic as [`CursorMove::InViewport`].
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use tui::buffer::Buffer;
|
||||||
|
/// # use tui::layout::Rect;
|
||||||
|
/// # use tui::widgets::Widget;
|
||||||
|
/// use tui_textarea::TextArea;
|
||||||
|
///
|
||||||
|
/// // Let's say terminal height is 8.
|
||||||
|
///
|
||||||
|
/// // Create textarea with 20 lines "0", "1", "2", "3", ...
|
||||||
|
/// let mut textarea: TextArea = (0..20).into_iter().map(|i| i.to_string()).collect();
|
||||||
|
/// # // Call `render` at least once to populate terminal size
|
||||||
|
/// # let r = Rect { x: 0, y: 0, width: 24, height: 8 };
|
||||||
|
/// # let mut b = Buffer::empty(r.clone());
|
||||||
|
/// # textarea.widget().render(r, &mut b);
|
||||||
|
///
|
||||||
|
/// // Scroll down by 15 lines. Since terminal height is 8, cursor will go out
|
||||||
|
/// // the viewport.
|
||||||
|
/// textarea.scroll(15, 0);
|
||||||
|
/// // So the cursor position was updated to stay in the viewport after the scrolling.
|
||||||
|
/// assert_eq!(textarea.cursor(), (15, 0));
|
||||||
|
///
|
||||||
|
/// // Scroll up by 5 lines. Since the scroll amount is smaller than the terminal
|
||||||
|
/// // height, cursor position will not be updated.
|
||||||
|
/// textarea.scroll(-5, 0);
|
||||||
|
/// assert_eq!(textarea.cursor(), (15, 0));
|
||||||
|
///
|
||||||
|
/// // Scroll up by 5 lines again. The terminal height is 8. So a cursor reaches to
|
||||||
|
/// // the top of viewport after scrolling up by 7 lines. Since we have already
|
||||||
|
/// // scrolled up by 5 lines, scrolling up by 5 lines again makes the cursor overrun
|
||||||
|
/// // the viewport by 5 - 2 = 3 lines. To keep the cursor stay in the viewport, the
|
||||||
|
/// // cursor position will be adjusted from line 15 to line 12.
|
||||||
|
/// textarea.scroll(-5, 0);
|
||||||
|
/// assert_eq!(textarea.cursor(), (12, 0));
|
||||||
|
/// ```
|
||||||
|
pub fn scroll(&mut self, delta_row: i16, delta_col: i16) {
|
||||||
|
self.viewport.scroll(delta_row, delta_col);
|
||||||
|
self.move_cursor(CursorMove::InViewport);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
use crate::textarea::TextArea;
|
use crate::textarea::TextArea;
|
||||||
use crate::util::num_digits;
|
use crate::util::num_digits;
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use tui::buffer::Buffer;
|
use tui::buffer::Buffer;
|
||||||
use tui::layout::Rect;
|
use tui::layout::Rect;
|
||||||
use tui::text::Text;
|
use tui::text::Text;
|
||||||
use tui::widgets::{Paragraph, Widget};
|
use tui::widgets::{Paragraph, Widget};
|
||||||
|
|
||||||
// &mut 'a (u16, u16) is not available since TextAreaWidget instance totally takes over the ownership of TextArea
|
// &mut 'a (u16, u16, u16, u16) is not available since Renderer instance totally takes over the ownership of TextArea
|
||||||
// instance. In the case, the TextArea instance cannot be accessed from any other objects since it is mutablly
|
// instance. In the case, the TextArea instance cannot be accessed from any other objects since it is mutablly
|
||||||
// borrowed.
|
// borrowed.
|
||||||
//
|
//
|
||||||
@@ -16,25 +16,60 @@ use tui::widgets::{Paragraph, Widget};
|
|||||||
// manage states of textarea instances separately.
|
// manage states of textarea instances separately.
|
||||||
// https://docs.rs/tui/latest/tui/terminal/struct.Frame.html#method.render_stateful_widget
|
// https://docs.rs/tui/latest/tui/terminal/struct.Frame.html#method.render_stateful_widget
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ScrollTop(AtomicU32);
|
pub struct Viewport(AtomicU64);
|
||||||
|
|
||||||
impl Clone for ScrollTop {
|
impl Clone for Viewport {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
let u = self.0.load(Ordering::Relaxed);
|
let u = self.0.load(Ordering::Relaxed);
|
||||||
ScrollTop(AtomicU32::new(u))
|
Viewport(AtomicU64::new(u))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScrollTop {
|
impl Viewport {
|
||||||
fn load(&self) -> (u16, u16) {
|
pub fn scroll_top(&self) -> (u16, u16) {
|
||||||
let u = self.0.load(Ordering::Relaxed);
|
let u = self.0.load(Ordering::Relaxed);
|
||||||
((u >> 16) as u16, u as u16)
|
((u >> 16) as u16, u as u16)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn store(&self, row: u16, col: u16) {
|
pub fn position(&self) -> (u16, u16, u16, u16) {
|
||||||
let u = ((row as u32) << 16) | col as u32;
|
let u = self.0.load(Ordering::Relaxed);
|
||||||
|
let width = (u >> 48) as u16;
|
||||||
|
let height = (u >> 32) as u16;
|
||||||
|
let row_top = (u >> 16) as u16;
|
||||||
|
let col_top = u as u16;
|
||||||
|
|
||||||
|
let row_bottom = row_top.saturating_add(height).saturating_sub(1);
|
||||||
|
let col_bottom = col_top.saturating_add(width).saturating_sub(1);
|
||||||
|
|
||||||
|
(
|
||||||
|
row_top,
|
||||||
|
col_top,
|
||||||
|
cmp::max(row_top, row_bottom),
|
||||||
|
cmp::max(col_top, col_bottom),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store(&self, row: u16, col: u16, width: u16, height: u16) {
|
||||||
|
// Pack four u16 values into one u64 value
|
||||||
|
let u =
|
||||||
|
((width as u64) << 48) | ((height as u64) << 32) | ((row as u64) << 16) | col as u64;
|
||||||
self.0.store(u, Ordering::Relaxed);
|
self.0.store(u, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scroll(&mut self, delta_row: i16, delta_col: i16) {
|
||||||
|
fn apply_scroll(pos: u16, delta: i16) -> u16 {
|
||||||
|
if delta >= 0 {
|
||||||
|
pos.saturating_add(delta as u16)
|
||||||
|
} else {
|
||||||
|
pos.saturating_sub(-delta as u16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let u = self.0.get_mut();
|
||||||
|
let row = apply_scroll((*u >> 16) as u16, delta_row);
|
||||||
|
let col = apply_scroll(*u as u16, delta_col);
|
||||||
|
*u = (*u & 0xffff_ffff_0000_0000) | ((row as u64) << 16) | (col as u64);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Renderer<'a>(&'a TextArea<'a>);
|
pub struct Renderer<'a>(&'a TextArea<'a>);
|
||||||
@@ -59,7 +94,7 @@ impl<'a> Renderer<'a> {
|
|||||||
|
|
||||||
impl<'a> Widget for Renderer<'a> {
|
impl<'a> Widget for Renderer<'a> {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
let inner_area = if let Some(b) = self.0.block() {
|
let Rect { width, height, .. } = if let Some(b) = self.0.block() {
|
||||||
b.inner(area)
|
b.inner(area)
|
||||||
} else {
|
} else {
|
||||||
area
|
area
|
||||||
@@ -76,11 +111,11 @@ impl<'a> Widget for Renderer<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let cursor = self.0.cursor();
|
let cursor = self.0.cursor();
|
||||||
let (top_row, top_col) = self.0.scroll_top.load();
|
let (top_row, top_col) = self.0.viewport.scroll_top();
|
||||||
let top_row = next_scroll_top(top_row, cursor.0 as u16, inner_area.height);
|
let top_row = next_scroll_top(top_row, cursor.0 as u16, height);
|
||||||
let top_col = next_scroll_top(top_col, cursor.1 as u16, inner_area.width);
|
let top_col = next_scroll_top(top_col, cursor.1 as u16, width);
|
||||||
|
|
||||||
let text = self.text(top_row as usize, inner_area.height as usize);
|
let text = self.text(top_row as usize, height as usize);
|
||||||
let mut inner = Paragraph::new(text).style(self.0.style());
|
let mut inner = Paragraph::new(text).style(self.0.style());
|
||||||
if let Some(b) = self.0.block() {
|
if let Some(b) = self.0.block() {
|
||||||
inner = inner.block(b.clone());
|
inner = inner.block(b.clone());
|
||||||
@@ -90,7 +125,7 @@ impl<'a> Widget for Renderer<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store scroll top position for rendering on the next tick
|
// Store scroll top position for rendering on the next tick
|
||||||
self.0.scroll_top.store(top_row, top_col);
|
self.0.viewport.store(top_row, top_col, width, height);
|
||||||
|
|
||||||
inner.render(area, buf);
|
inner.render(area, buf);
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user