Add validation to textarea
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()?,
|
||||
|
||||
Reference in New Issue
Block a user