mirror of
https://seed.radicle.dev/rad:z39mP9rQAaGmERfUMPULfPUi473tY.git
synced 2026-11-01 01:10:51 +08:00
lib: Introduce tui-textarea
This commit is contained in:
Generated
+52
-2
@@ -478,6 +478,31 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-bigint"
|
||||
version = "0.5.5"
|
||||
@@ -1168,6 +1193,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
@@ -1838,6 +1864,7 @@ dependencies = [
|
||||
"timeago",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tui-textarea",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1878,6 +1905,7 @@ dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"cassowary",
|
||||
"compact_str",
|
||||
"crossterm",
|
||||
"indoc",
|
||||
"itertools",
|
||||
"lru",
|
||||
@@ -2109,6 +2137,17 @@ dependencies = [
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.1"
|
||||
@@ -2717,6 +2756,17 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tui-textarea"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e38ced1f941a9cfc923fbf2fe6858443c42cc5220bfd35bdd3648371e7bd8e"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"ratatui",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
@@ -2767,9 +2817,9 @@ checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.10"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
|
||||
@@ -44,3 +44,4 @@ textwrap = { version = "0.16.0" }
|
||||
thiserror = { version = "1" }
|
||||
tokio = { version = "1.32.0", features = ["full"] }
|
||||
tokio-stream = { version = "0.1.14" }
|
||||
tui-textarea = "0.4.0"
|
||||
+2
-2
@@ -8,7 +8,7 @@ use radicle_tui as tui;
|
||||
|
||||
use tui::store;
|
||||
use tui::ui::widget::container::{Column, Container, Header, HeaderProps};
|
||||
use tui::ui::widget::text::{TextArea, TextAreaProps};
|
||||
use tui::ui::widget::input::{TextArea, TextAreaProps};
|
||||
use tui::ui::widget::window::{Page, Shortcuts, ShortcutsProps, Window, WindowProps};
|
||||
use tui::ui::widget::ToWidget;
|
||||
use tui::{BoxedAny, Channel, Exit};
|
||||
@@ -73,7 +73,7 @@ pub async fn main() -> Result<()> {
|
||||
.content(TextArea::default().to_widget(sender.clone()).on_update(
|
||||
|state: &State| {
|
||||
TextAreaProps::default()
|
||||
.text(&state.content.clone().into())
|
||||
.content(state.content.clone())
|
||||
.can_scroll(false)
|
||||
.to_boxed_any()
|
||||
.into()
|
||||
|
||||
+3
-2
@@ -8,7 +8,7 @@ use ratatui::text::Text;
|
||||
use radicle_tui as tui;
|
||||
|
||||
use tui::store;
|
||||
use tui::ui::widget::text::{TextArea, TextAreaProps};
|
||||
use tui::ui::widget::input::{TextArea, TextAreaProps};
|
||||
use tui::ui::widget::ToWidget;
|
||||
use tui::{BoxedAny, Channel, Exit};
|
||||
|
||||
@@ -62,7 +62,8 @@ pub async fn main() -> Result<()> {
|
||||
})
|
||||
.on_update(|state: &State| {
|
||||
TextAreaProps::default()
|
||||
.text(&Text::styled(state.alien.clone(), Color::Rgb(85, 85, 255)))
|
||||
.content(Text::styled(state.alien.clone(), Color::Rgb(85, 85, 255)))
|
||||
.can_scroll(false)
|
||||
.to_boxed_any()
|
||||
.into()
|
||||
});
|
||||
|
||||
+15
-2
@@ -1,7 +1,6 @@
|
||||
pub mod container;
|
||||
pub mod input;
|
||||
pub mod list;
|
||||
pub mod text;
|
||||
pub mod utils;
|
||||
pub mod window;
|
||||
|
||||
@@ -63,7 +62,14 @@ impl From<&'static dyn Any> for ViewProps {
|
||||
pub enum ViewState {
|
||||
USize(usize),
|
||||
String(String),
|
||||
Table { selected: usize, scroll: usize },
|
||||
Table {
|
||||
selected: usize,
|
||||
scroll: usize,
|
||||
},
|
||||
TextArea {
|
||||
scroll: usize,
|
||||
cursor: (usize, usize),
|
||||
},
|
||||
}
|
||||
|
||||
impl ViewState {
|
||||
@@ -87,6 +93,13 @@ impl ViewState {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_textarea(&self) -> Option<(usize, (usize, usize))> {
|
||||
match self {
|
||||
ViewState::TextArea { scroll, cursor } => Some((*scroll, *cursor)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
|
||||
+237
-4
@@ -3,11 +3,11 @@ use std::marker::PhantomData;
|
||||
use ratatui::Frame;
|
||||
use termion::event::Key;
|
||||
|
||||
use ratatui::layout::{Constraint, Layout};
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::layout::{Alignment, Constraint, Layout};
|
||||
use ratatui::style::{Style, Stylize};
|
||||
use ratatui::text::{Line, Span, Text};
|
||||
|
||||
use super::{RenderProps, View, ViewProps, ViewState};
|
||||
use super::{utils, RenderProps, View, ViewProps, ViewState};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TextFieldProps {
|
||||
@@ -281,3 +281,236 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration of a `TextArea`'s internal progress display.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct TextAreaProgressInfo {
|
||||
scroll: bool,
|
||||
cursor: bool,
|
||||
}
|
||||
|
||||
impl TextAreaProgressInfo {
|
||||
pub fn scroll(mut self, scroll: bool) -> Self {
|
||||
self.scroll = scroll;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cursor(mut self, cursor: bool) -> Self {
|
||||
self.cursor = cursor;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_rendered(&self) -> bool {
|
||||
self.scroll || self.cursor
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TextAreaProps<'a> {
|
||||
content: Text<'a>,
|
||||
cursor: (usize, usize),
|
||||
can_scroll: bool,
|
||||
progress_info: TextAreaProgressInfo,
|
||||
}
|
||||
|
||||
impl<'a> Default for TextAreaProps<'a> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
content: String::new().into(),
|
||||
cursor: (0, 0),
|
||||
can_scroll: true,
|
||||
progress_info: TextAreaProgressInfo::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TextAreaProps<'a> {
|
||||
pub fn content<T>(mut self, content: T) -> Self
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
self.content = content.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cursor(mut self, cursor: (usize, usize)) -> Self {
|
||||
self.cursor = cursor;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn progress_info(mut self, progress_info: TextAreaProgressInfo) -> Self {
|
||||
self.progress_info = progress_info;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn can_scroll(mut self, can_scroll: bool) -> Self {
|
||||
self.can_scroll = can_scroll;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextArea<'a, S, M> {
|
||||
phantom: PhantomData<(S, M)>,
|
||||
textarea: tui_textarea::TextArea<'a>,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
impl<'a, S, M> Default for TextArea<'a, S, M> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
phantom: PhantomData,
|
||||
textarea: tui_textarea::TextArea::default(),
|
||||
height: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S, M> View for TextArea<'a, S, M> {
|
||||
type State = S;
|
||||
type Message = M;
|
||||
|
||||
fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
|
||||
let default = TextAreaProps::default();
|
||||
let props = props
|
||||
.and_then(|props| props.inner_ref::<TextAreaProps>())
|
||||
.unwrap_or(&default);
|
||||
|
||||
if props.can_scroll {
|
||||
match key {
|
||||
Key::Left => {
|
||||
self.textarea.input(tui_textarea::Input {
|
||||
key: tui_textarea::Key::Left,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
Key::Right => {
|
||||
self.textarea.input(tui_textarea::Input {
|
||||
key: tui_textarea::Key::Right,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
Key::Up => {
|
||||
self.textarea.input(tui_textarea::Input {
|
||||
key: tui_textarea::Key::Up,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
Key::Down => {
|
||||
self.textarea.input(tui_textarea::Input {
|
||||
key: tui_textarea::Key::Down,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn update(&mut self, props: Option<&ViewProps>, _state: &Self::State) {
|
||||
let default = TextAreaProps::default();
|
||||
let props = props
|
||||
.and_then(|props| props.inner_ref::<TextAreaProps>())
|
||||
.unwrap_or(&default);
|
||||
|
||||
self.textarea = tui_textarea::TextArea::new(
|
||||
props
|
||||
.content
|
||||
.lines
|
||||
.iter()
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
fn render(&mut self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
|
||||
let default = TextAreaProps::default();
|
||||
let props = props
|
||||
.and_then(|props| props.inner_ref::<TextAreaProps>())
|
||||
.unwrap_or(&default);
|
||||
|
||||
let [area] = Layout::default()
|
||||
.constraints([Constraint::Min(1)])
|
||||
.horizontal_margin(1)
|
||||
.areas(render.area);
|
||||
|
||||
let cursor_line_style = Style::default();
|
||||
|
||||
let cursor_style = if render.focus {
|
||||
Style::default().reversed()
|
||||
} else {
|
||||
cursor_line_style
|
||||
};
|
||||
|
||||
let content_style = if render.focus {
|
||||
Style::default()
|
||||
} else {
|
||||
Style::default().dim()
|
||||
};
|
||||
|
||||
self.height = render.area.height;
|
||||
|
||||
self.textarea.move_cursor(tui_textarea::CursorMove::Jump(
|
||||
props.cursor.0 as u16,
|
||||
props.cursor.1 as u16,
|
||||
));
|
||||
self.textarea.set_cursor_line_style(cursor_line_style);
|
||||
self.textarea.set_cursor_style(cursor_style);
|
||||
self.textarea.set_style(content_style);
|
||||
|
||||
if props.progress_info.is_rendered() {
|
||||
let [content_area, progress_area] =
|
||||
Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(area);
|
||||
|
||||
let mut progress_info = vec![];
|
||||
|
||||
if props.progress_info.scroll {
|
||||
progress_info.push(Span::styled(
|
||||
format!(
|
||||
"{}%",
|
||||
utils::scroll::percent_absolute(
|
||||
self.textarea.cursor().0,
|
||||
self.textarea.lines().len(),
|
||||
content_area.height.into()
|
||||
)
|
||||
),
|
||||
Style::default().dim(),
|
||||
))
|
||||
}
|
||||
|
||||
if props.progress_info.scroll && props.progress_info.cursor {
|
||||
progress_info.push(Span::raw(" "));
|
||||
}
|
||||
|
||||
if props.progress_info.cursor {
|
||||
progress_info.push(Span::styled(
|
||||
format!(
|
||||
"[{},{}]",
|
||||
self.textarea.cursor().0,
|
||||
self.textarea.cursor().1
|
||||
),
|
||||
Style::default().dim(),
|
||||
))
|
||||
}
|
||||
|
||||
let line = Line::from(progress_info).alignment(Alignment::Right);
|
||||
|
||||
frame.render_widget(self.textarea.widget(), content_area);
|
||||
frame.render_widget(line, progress_area);
|
||||
} else {
|
||||
frame.render_widget(self.textarea.widget(), area);
|
||||
}
|
||||
}
|
||||
|
||||
fn view_state(&self) -> Option<ViewState> {
|
||||
Some(ViewState::TextArea {
|
||||
scroll: utils::scroll::percent_absolute(
|
||||
self.textarea.cursor().0.saturating_sub(self.height.into()),
|
||||
self.textarea.lines().len(),
|
||||
self.height.into(),
|
||||
),
|
||||
cursor: self.textarea.cursor(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use termion::event::Key;
|
||||
|
||||
use ratatui::layout::{Constraint, Layout};
|
||||
use ratatui::text::Text;
|
||||
use ratatui::Frame;
|
||||
|
||||
use super::utils;
|
||||
use super::{RenderProps, View, ViewProps, ViewState};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TextAreaProps<'a> {
|
||||
pub content: Text<'a>,
|
||||
pub has_header: bool,
|
||||
pub has_footer: bool,
|
||||
pub progress: usize,
|
||||
pub can_scroll: bool,
|
||||
}
|
||||
|
||||
impl<'a> TextAreaProps<'a> {
|
||||
pub fn text(mut self, text: &Text<'a>) -> Self {
|
||||
self.content = text.clone();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn can_scroll(mut self, can_scroll: bool) -> Self {
|
||||
self.can_scroll = can_scroll;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Default for TextAreaProps<'a> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
content: Text::raw(""),
|
||||
has_header: false,
|
||||
has_footer: false,
|
||||
progress: 0,
|
||||
can_scroll: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TextAreaState {
|
||||
/// Internal offset
|
||||
pub offset: usize,
|
||||
/// Internal progress
|
||||
pub progress: usize,
|
||||
}
|
||||
|
||||
pub struct TextArea<S, M> {
|
||||
/// Internal state
|
||||
state: TextAreaState,
|
||||
/// Phantom
|
||||
phantom: PhantomData<(S, M)>,
|
||||
/// Current render height
|
||||
height: u16,
|
||||
}
|
||||
|
||||
impl<S, M> Default for TextArea<S, M> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: TextAreaState {
|
||||
offset: 0,
|
||||
progress: 0,
|
||||
},
|
||||
phantom: PhantomData,
|
||||
height: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, M> TextArea<S, M> {
|
||||
fn scroll(&self) -> (u16, u16) {
|
||||
(self.state.offset as u16, 0)
|
||||
}
|
||||
|
||||
fn prev(&mut self, len: usize, page_size: usize) -> (u16, u16) {
|
||||
self.state.offset = self.state.offset.saturating_sub(1);
|
||||
self.state.progress = utils::scroll::percent_absolute(self.state.offset, len, page_size);
|
||||
self.scroll()
|
||||
}
|
||||
|
||||
fn next(&mut self, len: usize, page_size: usize) -> (u16, u16) {
|
||||
if self.state.progress < 100 {
|
||||
self.state.offset = self.state.offset.saturating_add(1);
|
||||
self.state.progress =
|
||||
utils::scroll::percent_absolute(self.state.offset, len, page_size);
|
||||
}
|
||||
|
||||
self.scroll()
|
||||
}
|
||||
|
||||
fn prev_page(&mut self, len: usize, page_size: usize) -> (u16, u16) {
|
||||
self.state.offset = self.state.offset.saturating_sub(page_size);
|
||||
self.state.progress = utils::scroll::percent_absolute(self.state.offset, len, page_size);
|
||||
self.scroll()
|
||||
}
|
||||
|
||||
fn next_page(&mut self, len: usize, page_size: usize) -> (u16, u16) {
|
||||
let end = len.saturating_sub(page_size);
|
||||
|
||||
self.state.offset = std::cmp::min(self.state.offset.saturating_add(page_size), end);
|
||||
self.state.progress = utils::scroll::percent_absolute(self.state.offset, len, page_size);
|
||||
self.scroll()
|
||||
}
|
||||
|
||||
fn begin(&mut self, len: usize, page_size: usize) -> (u16, u16) {
|
||||
self.state.offset = 0;
|
||||
self.state.progress = utils::scroll::percent_absolute(self.state.offset, len, page_size);
|
||||
self.scroll()
|
||||
}
|
||||
|
||||
fn end(&mut self, len: usize, page_size: usize) -> (u16, u16) {
|
||||
self.state.offset = len.saturating_sub(page_size);
|
||||
self.state.progress = utils::scroll::percent_absolute(self.state.offset, len, page_size);
|
||||
self.scroll()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, M> View for TextArea<S, M>
|
||||
where
|
||||
S: 'static,
|
||||
M: 'static,
|
||||
{
|
||||
type Message = M;
|
||||
type State = S;
|
||||
|
||||
fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
|
||||
let default = TextAreaProps::default();
|
||||
let props = props
|
||||
.and_then(|props| props.inner_ref::<TextAreaProps>())
|
||||
.unwrap_or(&default);
|
||||
|
||||
let len = props.content.lines.len() + 1;
|
||||
let page_size = self.height as usize;
|
||||
|
||||
if props.can_scroll {
|
||||
match key {
|
||||
Key::Up | Key::Char('k') => {
|
||||
self.prev(len, page_size);
|
||||
}
|
||||
Key::Down | Key::Char('j') => {
|
||||
self.next(len, page_size);
|
||||
}
|
||||
Key::PageUp => {
|
||||
self.prev_page(len, page_size);
|
||||
}
|
||||
Key::PageDown => {
|
||||
self.next_page(len, page_size);
|
||||
}
|
||||
Key::Home => {
|
||||
self.begin(len, page_size);
|
||||
}
|
||||
Key::End => {
|
||||
self.end(len, page_size);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn render(&mut self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
|
||||
self.height = render.area.height;
|
||||
|
||||
let default = TextAreaProps::default();
|
||||
let props = props
|
||||
.and_then(|props| props.inner_ref::<TextAreaProps>())
|
||||
.unwrap_or(&default);
|
||||
|
||||
let [content_area] = Layout::horizontal([Constraint::Min(1)])
|
||||
.horizontal_margin(1)
|
||||
.areas(render.area);
|
||||
let content = ratatui::widgets::Paragraph::new(props.content.clone())
|
||||
.style(props.content.style)
|
||||
.scroll((self.state.offset as u16, 0));
|
||||
|
||||
frame.render_widget(content, content_area);
|
||||
}
|
||||
|
||||
fn view_state(&self) -> Option<ViewState> {
|
||||
Some(ViewState::USize(self.state.progress))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user