зеркало из
https://github.com/glebtv/tui-textarea.git
synced 2026-09-07 16:05:50 +03:00
separate widget implementation to src/widget.rs
Этот коммит содержится в:
@@ -11,6 +11,7 @@ mod input;
|
|||||||
mod search;
|
mod search;
|
||||||
mod textarea;
|
mod textarea;
|
||||||
mod util;
|
mod util;
|
||||||
|
mod widget;
|
||||||
mod word;
|
mod word;
|
||||||
|
|
||||||
pub use cursor::CursorMove;
|
pub use cursor::CursorMove;
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ use crate::history::{Edit, EditKind, History};
|
|||||||
use crate::input::{Input, Key};
|
use crate::input::{Input, Key};
|
||||||
#[cfg(feature = "search")]
|
#[cfg(feature = "search")]
|
||||||
use crate::search::Search;
|
use crate::search::Search;
|
||||||
use crate::util::{num_digits, spaces};
|
use crate::util::spaces;
|
||||||
|
use crate::widget::{Renderer, ScrollTop};
|
||||||
use crate::word::{find_word_end_forward, find_word_start_backward};
|
use crate::word::{find_word_end_forward, find_word_start_backward};
|
||||||
use std::sync::atomic::{AtomicU16, Ordering};
|
|
||||||
use tui::buffer::Buffer;
|
|
||||||
use tui::layout::Rect;
|
|
||||||
use tui::style::{Modifier, Style};
|
use tui::style::{Modifier, Style};
|
||||||
use tui::text::{Spans, Text};
|
use tui::text::Spans;
|
||||||
use tui::widgets::{Block, Paragraph, Widget};
|
use tui::widgets::{Block, Widget};
|
||||||
|
|
||||||
/// A type to manage state of textarea.
|
/// A type to manage state of textarea.
|
||||||
///
|
///
|
||||||
@@ -43,7 +41,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>,
|
||||||
scroll_top: (AtomicU16, AtomicU16),
|
pub(crate) scroll_top: ScrollTop,
|
||||||
cursor_style: Style,
|
cursor_style: Style,
|
||||||
yank: String,
|
yank: String,
|
||||||
#[cfg(feature = "search")]
|
#[cfg(feature = "search")]
|
||||||
@@ -139,7 +137,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: (AtomicU16::new(0), AtomicU16::new(0)),
|
scroll_top: ScrollTop::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")]
|
||||||
@@ -916,7 +914,7 @@ impl<'a> TextArea<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line_spans<'b>(&'b self, line: &'b str, row: usize, lnum_len: u8) -> Spans<'b> {
|
pub(crate) fn line_spans<'b>(&'b self, line: &'b str, row: usize, lnum_len: u8) -> Spans<'b> {
|
||||||
let mut hl = LineHighlighter::new(line, self.cursor_style, self.tab_len);
|
let mut hl = LineHighlighter::new(line, self.cursor_style, self.tab_len);
|
||||||
|
|
||||||
if let Some(style) = self.line_number_style {
|
if let Some(style) = self.line_number_style {
|
||||||
@@ -962,19 +960,7 @@ impl<'a> TextArea<'a> {
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn widget(&'a self) -> impl Widget + 'a {
|
pub fn widget(&'a self) -> impl Widget + 'a {
|
||||||
let mut lines = Vec::with_capacity(self.lines.len());
|
Renderer::new(self)
|
||||||
let lnum_len = num_digits(self.lines.len());
|
|
||||||
for (i, l) in self.lines.iter().map(String::as_str).enumerate() {
|
|
||||||
lines.push(self.line_spans(l, i, lnum_len));
|
|
||||||
}
|
|
||||||
|
|
||||||
let inner = Paragraph::new(Text::from(lines)).style(self.style);
|
|
||||||
Renderer {
|
|
||||||
scroll_top: &self.scroll_top,
|
|
||||||
cursor: (self.cursor.0 as u16, self.cursor.1 as u16),
|
|
||||||
block: self.block.clone(),
|
|
||||||
inner,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the style of textarea. By default, textarea is not styled.
|
/// Set the style of textarea. By default, textarea is not styled.
|
||||||
@@ -1470,57 +1456,3 @@ impl<'a> TextArea<'a> {
|
|||||||
self.search.style = style;
|
self.search.style = style;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Renderer<'a> {
|
|
||||||
// &mut 'a (u16, u16) is not available since TextAreaWidget 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
|
|
||||||
// borrowed.
|
|
||||||
//
|
|
||||||
// `tui::terminal::Frame::render_stateful_widget` would be an assumed way to render a stateful widget. But at this
|
|
||||||
// point we stick with using `tui::terminal::Frame::render_widget` because it is simpler API. Users don't need to
|
|
||||||
// manage states of textarea instances separately.
|
|
||||||
// https://docs.rs/tui/latest/tui/terminal/struct.Frame.html#method.render_stateful_widget
|
|
||||||
scroll_top: &'a (AtomicU16, AtomicU16),
|
|
||||||
cursor: (u16, u16),
|
|
||||||
block: Option<Block<'a>>,
|
|
||||||
inner: Paragraph<'a>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Widget for Renderer<'a> {
|
|
||||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
|
||||||
let inner_area = if let Some(b) = self.block.take() {
|
|
||||||
let area = b.inner(area);
|
|
||||||
self.inner = self.inner.block(b);
|
|
||||||
area
|
|
||||||
} else {
|
|
||||||
area
|
|
||||||
};
|
|
||||||
|
|
||||||
let top_row = self.scroll_top.0.load(Ordering::Relaxed);
|
|
||||||
let top_col = self.scroll_top.1.load(Ordering::Relaxed);
|
|
||||||
|
|
||||||
fn next_scroll_top(prev_top: u16, cursor: u16, width: u16) -> u16 {
|
|
||||||
if cursor < prev_top {
|
|
||||||
cursor
|
|
||||||
} else if prev_top + width <= cursor {
|
|
||||||
cursor + 1 - width
|
|
||||||
} else {
|
|
||||||
prev_top
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let row = next_scroll_top(top_row, self.cursor.0, inner_area.height);
|
|
||||||
let col = next_scroll_top(top_col, self.cursor.1, inner_area.width);
|
|
||||||
|
|
||||||
let scroll = (row, col);
|
|
||||||
if scroll != (0, 0) {
|
|
||||||
self.inner = self.inner.scroll(scroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store scroll top position for rendering on the next tick
|
|
||||||
self.scroll_top.0.store(row, Ordering::Relaxed);
|
|
||||||
self.scroll_top.1.store(col, Ordering::Relaxed);
|
|
||||||
|
|
||||||
self.inner.render(area, buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
95
src/widget.rs
Обычный файл
95
src/widget.rs
Обычный файл
@@ -0,0 +1,95 @@
|
|||||||
|
use crate::textarea::TextArea;
|
||||||
|
use crate::util::num_digits;
|
||||||
|
use std::sync::atomic::{AtomicU16, Ordering};
|
||||||
|
use tui::buffer::Buffer;
|
||||||
|
use tui::layout::Rect;
|
||||||
|
use tui::text::Text;
|
||||||
|
use tui::widgets::{Block, Paragraph, Widget};
|
||||||
|
|
||||||
|
// &mut 'a (u16, u16) is not available since TextAreaWidget 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
|
||||||
|
// borrowed.
|
||||||
|
//
|
||||||
|
// `tui::terminal::Frame::render_stateful_widget` would be an assumed way to render a stateful widget. But at this
|
||||||
|
// point we stick with using `tui::terminal::Frame::render_widget` because it is simpler API. Users don't need to
|
||||||
|
// manage states of textarea instances separately.
|
||||||
|
// https://docs.rs/tui/latest/tui/terminal/struct.Frame.html#method.render_stateful_widget
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ScrollTop(AtomicU16, AtomicU16);
|
||||||
|
|
||||||
|
impl ScrollTop {
|
||||||
|
fn load(&self) -> (u16, u16) {
|
||||||
|
let row = self.0.load(Ordering::Relaxed);
|
||||||
|
let col = self.1.load(Ordering::Relaxed);
|
||||||
|
(row, col)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store(&self, row: u16, col: u16) {
|
||||||
|
self.0.store(row, Ordering::Relaxed);
|
||||||
|
self.1.store(col, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Renderer<'a> {
|
||||||
|
scroll_top: &'a ScrollTop,
|
||||||
|
cursor: (u16, u16),
|
||||||
|
block: Option<Block<'a>>,
|
||||||
|
inner: Paragraph<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Renderer<'a> {
|
||||||
|
pub fn new(textarea: &'a TextArea<'a>) -> Self {
|
||||||
|
let lnum_len = num_digits(textarea.lines().len());
|
||||||
|
let lines: Vec<_> = textarea
|
||||||
|
.lines()
|
||||||
|
.iter()
|
||||||
|
.map(String::as_str)
|
||||||
|
.enumerate()
|
||||||
|
.map(|(row, line)| textarea.line_spans(line, row, lnum_len))
|
||||||
|
.collect();
|
||||||
|
let inner = Paragraph::new(Text::from(lines)).style(textarea.style());
|
||||||
|
let cursor = textarea.cursor();
|
||||||
|
Self {
|
||||||
|
scroll_top: &textarea.scroll_top,
|
||||||
|
cursor: (cursor.0 as u16, cursor.1 as u16),
|
||||||
|
block: textarea.block().cloned(),
|
||||||
|
inner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for Renderer<'a> {
|
||||||
|
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let inner_area = if let Some(b) = self.block.take() {
|
||||||
|
let area = b.inner(area);
|
||||||
|
self.inner = self.inner.block(b);
|
||||||
|
area
|
||||||
|
} else {
|
||||||
|
area
|
||||||
|
};
|
||||||
|
|
||||||
|
fn next_scroll_top(prev_top: u16, cursor: u16, length: u16) -> u16 {
|
||||||
|
if cursor < prev_top {
|
||||||
|
cursor
|
||||||
|
} else if prev_top + length <= cursor {
|
||||||
|
cursor + 1 - length
|
||||||
|
} else {
|
||||||
|
prev_top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (top_row, top_col) = self.scroll_top.load();
|
||||||
|
let row = next_scroll_top(top_row, self.cursor.0, inner_area.height);
|
||||||
|
let col = next_scroll_top(top_col, self.cursor.1, inner_area.width);
|
||||||
|
|
||||||
|
let scroll = (row, col);
|
||||||
|
if scroll != (0, 0) {
|
||||||
|
self.inner = self.inner.scroll(scroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store scroll top position for rendering on the next tick
|
||||||
|
self.scroll_top.store(row, col);
|
||||||
|
|
||||||
|
self.inner.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
Ссылка в новой задаче
Block a user