Add validation to textarea

This commit is contained in:
2025-10-16 18:51:24 +08:00
parent 5f0238d3e3
commit 71122e87ae
5 changed files with 102 additions and 42 deletions

View File

@@ -115,6 +115,7 @@ impl FolderAddCommand {
.path_config
.dlsite_paths
.push(abs_path.to_str().unwrap().to_string());
config.write_to_file(&APP_CONIFG_FILE_PATH.to_path_buf())
config.save()?;
Ok(())
}
}

View File

@@ -50,7 +50,7 @@ impl ApplicationConfig {
conf
}
pub fn write_to_file(self, path: &PathBuf) -> Result<()> {
fn write_to_file(self, path: &PathBuf) -> Result<()> {
let mut conf = Ini::new();
conf.with_section(Some("Basic"))
.set("DBPath", self.basic_config.db_path)
@@ -67,4 +67,8 @@ impl ApplicationConfig {
conf.write_to_file(path)?;
Ok(())
}
pub fn save(&self) -> Result<()> {
self.clone().write_to_file(&APP_CONIFG_FILE_PATH.to_path_buf())
}
}

View File

@@ -1,3 +1,4 @@
use std::sync::Arc;
use color_eyre::Result;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use rat_cursor::HasScreenCursor;
@@ -12,13 +13,20 @@ use tui_input::Input;
#[derive(Clone)]
pub struct TextArea {
input: Input,
title: String,
style: TextAreaStyle,
auto_scroll: bool,
validate_fn: Arc<dyn Fn(&str) -> bool>,
pub state: TextAreaState,
}
#[derive(Clone)]
pub struct TextAreaState {
pub is_active: bool,
input_area: Option<Rect>,
scroll_offset: u16,
auto_scroll: bool,
pub active: bool,
input: Input,
is_valid: bool
}
#[derive(Clone)]
@@ -28,7 +36,7 @@ pub enum TextAreaStyle {
}
impl StatefulWidget for TextArea {
type State = TextArea;
type State = TextAreaState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
where
Self: Sized,
@@ -48,20 +56,16 @@ impl StatefulWidget for TextArea {
let label_text = Text::from(self.title);
let label = Paragraph::new(label_text);
// let scroll_offset = if self.input.cursor() > chunks[1].width as usize {
// self.input.cursor() - chunks[1].width as usize
// } else { 0 };
let input_text = Span::from(input_value.clone()).fg(Color::White);
let input_line = Line::from(input_text);
let paragraph = Paragraph::new(input_line)
.bg(Color::Green)
.scroll((0, self.scroll_offset));
.bg(if state.is_valid { Color::Green } else { Color::Red })
.scroll((0, state.scroll_offset));
if self.input_area.is_none() {
if state.input_area.is_none() {
state.input_area = Some(chunks[1]);
}
else if let Some(area) = self.input_area && area != chunks[1] {
else if let Some(area) = self.state.input_area && area != chunks[1] {
state.input_area = Some(chunks[1]);
}
@@ -73,48 +77,72 @@ impl StatefulWidget for TextArea {
impl HasScreenCursor for TextArea {
fn screen_cursor(&self) -> Option<(u16, u16)> {
if self.input_area.is_none() {
if self.state.input_area.is_none() {
return None;
}
let area = self.input_area.unwrap();
let scroll = self.input.visual_scroll(1);
let x = self.input.visual_cursor().max(scroll) as u16 - self.scroll_offset;
let area = self.state.input_area.unwrap();
let scroll = self.state.input.visual_scroll(1);
let x = self.state.input.visual_cursor().max(scroll) as u16 - self.state.scroll_offset;
Some((area.x + x, area.y))
}
}
impl TextArea {
pub fn new(title: &str, placeholder_text: &str, style: Option<TextAreaStyle>, auto_scroll: Option<bool>) -> Self {
pub fn new(title: &str,
placeholder_text: &str,
validate_fn: fn(&str) -> bool,
style: Option<TextAreaStyle>,
auto_scroll: Option<bool>,
)
-> Self
{
let func = Arc::new(validate_fn);
Self {
input: Input::new(placeholder_text.to_string()),
title: title.to_string(),
active: false,
style: style.unwrap_or(TextAreaStyle::SingleLine),
input_area: None,
auto_scroll: auto_scroll.unwrap_or(true),
scroll_offset: 0
validate_fn: func,
state: TextAreaState {
input: Input::new(placeholder_text.to_string()),
is_active: false,
input_area: None,
scroll_offset: 0,
is_valid: false
}
}
}
pub fn handle_input(&mut self, event: &Event) -> Result<()> {
let _ = self.input.handle_event(event);
let _ = self.state.input.handle_event(event);
self.state.is_valid = (self.validate_fn)(self.state.input.value());
if let Event::Key(key) = event &&
!matches!(key.kind, KeyEventKind::Release) &&
let Some(area) = self.input_area {
let cursor_pos = self.input.cursor() as u16;
if self.scroll_offset > cursor_pos {
self.scroll_offset = cursor_pos;
} else if cursor_pos >= area.width + self.scroll_offset {
self.scroll_offset = cursor_pos - area.width;
} else if self.auto_scroll && self.scroll_offset > 0 && (key.code.is_delete() || key.code.is_backspace()) {
self.scroll_offset -= 1;
let Some(area) = self.state.input_area
{
let scroll_offset = self.state.scroll_offset;
let cursor_pos = self.state.input.cursor() as u16;
if scroll_offset> cursor_pos {
self.state.scroll_offset = cursor_pos;
} else if cursor_pos >= area.width + scroll_offset {
self.state.scroll_offset = cursor_pos - area.width;
} else if self.auto_scroll && scroll_offset > 0 && key.code.is_delete() {
self.state.scroll_offset -= 1;
// HACK: with_cursor function requires to be owned so use handle event
let _ = self.input.handle_event(&Event::Key(KeyEvent::new(KeyCode::Left, KeyModifiers::empty())));
let _ = self.state.input.handle_event(&Event::Key(KeyEvent::new(KeyCode::Left, KeyModifiers::empty())));
}
}
Ok(())
}
pub fn get_value(&self) -> Option<String> {
if self.state.is_valid {
return Some(self.state.input.value().to_string());
}
None
}
pub fn reset_value(&mut self) -> Result<()> {
Ok(self.state.input.reset())
}
}

View File

@@ -1,3 +1,4 @@
use std::path::Path;
use crate::widgets::components::TextArea;
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Direction, Layout, Margin, Rect};
@@ -11,10 +12,30 @@ pub struct AddFolderPopup {
impl AddFolderPopup {
pub fn new() -> Self {
let mut textarea = TextArea::new("Folder Path", "", None, None);
textarea.active = true;
let mut textarea = TextArea::new(
"Folder Path",
"",
|x| {
let path = Path::new(x);
path.exists() && path.is_dir()
},
None,
None
);
textarea.state.is_active = true;
Self { textarea }
}
pub fn get_folder_value(&mut self) -> Option<String> {
let value = self.textarea.get_value();
if value.is_none() {
return None;
}
if let Some(path) = value && !path.is_empty() {
return Some(path);
}
None
}
}
impl StatefulWidget for AddFolderPopup {
@@ -37,6 +58,6 @@ impl StatefulWidget for AddFolderPopup {
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1)])
.split(popup_area.inner(Margin::new(1, 1)));
self.textarea.render(chunks[0], buf, &mut state.textarea);
self.textarea.render(chunks[0], buf, &mut state.textarea.state);
}
}

View File

@@ -1,5 +1,4 @@
use crate::config::types::ApplicationConfig;
use crate::constants::APP_CONIFG_FILE_PATH;
use crate::widgets::popups::folder::AddFolderPopup;
use crate::widgets::views::View;
use crossterm::event::KeyCode::Char;
@@ -43,9 +42,7 @@ impl MainView {
fn quit(&mut self) -> color_eyre::Result<()> {
if self.state.popup.is_none() {
self.state.status = Status::Exiting;
self.app_config
.clone()
.write_to_file(&APP_CONIFG_FILE_PATH.to_path_buf())?;
self.app_config.save()?;
}
Ok(())
}
@@ -71,6 +68,15 @@ impl View for MainView {
self.state.status = Status::Running;
self.state.popup = None;
}
if let Some(any) = self.state.popup.as_mut() &&
let Some(popup) = any.downcast_mut::<AddFolderPopup>() &&
let Some(value) = popup.get_folder_value() &&
key.code.is_enter()
{
self.app_config.path_config.dlsite_paths.push(value);
popup.textarea.reset_value()?;
self.app_config.save()?;
}
if !matches!(self.state.status, Status::Popup) && matches!(key.kind, KeyEventKind::Press) {
match key.code {
Char('q') => self.quit()?,