working textarea with example, time to prepare it for the future implementations

This commit is contained in:
Priec
2025-08-17 12:17:46 +02:00
parent 60cb45dcca
commit e36324af6f
3 changed files with 235 additions and 107 deletions

View File

@@ -90,6 +90,27 @@ impl TextAreaProvider {
self.lines[prev_idx].push_str(&curr);
Some((prev_idx, prev_len))
}
pub fn insert_blank_line_after(&mut self, idx: usize) -> usize {
let clamped = idx.min(self.lines.len());
let insert_at = if clamped >= self.lines.len() {
self.lines.len()
} else {
clamped + 1
};
if insert_at == self.lines.len() {
self.lines.push(String::new());
} else {
self.lines.insert(insert_at, String::new());
}
insert_at
}
pub fn insert_blank_line_before(&mut self, idx: usize) -> usize {
let insert_at = idx.min(self.lines.len());
self.lines.insert(insert_at, String::new());
insert_at
}
}
impl DataProvider for TextAreaProvider {

View File

@@ -1,9 +1,12 @@
// src/textarea/state.rs
use std::ops::{Deref, DerefMut};
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use crate::editor::FormEditor;
use crate::textarea::provider::TextAreaProvider;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
#[cfg(feature = "gui")]
use ratatui::{layout::Rect, widgets::Block};
@@ -30,6 +33,21 @@ impl Default for TextAreaState {
}
}
// Expose the entire FormEditor API directly on TextAreaState
impl Deref for TextAreaState {
type Target = TextAreaEditor;
fn deref(&self) -> &Self::Target {
&self.editor
}
}
impl DerefMut for TextAreaState {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.editor
}
}
impl TextAreaState {
pub fn from_text<S: Into<String>>(text: S) -> Self {
let provider = TextAreaProvider::from_text(text);
@@ -47,7 +65,6 @@ impl TextAreaState {
pub fn set_text<S: Into<String>>(&mut self, text: S) {
self.editor.data_provider_mut().set_text(text);
// Reset to first line and col 0
self.editor.ui_state.current_field = 0;
self.editor.ui_state.cursor_pos = 0;
self.editor.ui_state.ideal_cursor_column = 0;
@@ -61,29 +78,30 @@ impl TextAreaState {
self.placeholder = Some(s.into());
}
// Editing primitives specific to multi-line buffer
// Textarea-specific primitive: split at cursor
pub fn insert_newline(&mut self) {
let line_idx = self.editor.current_field();
let col = self.editor.cursor_position();
let line_idx = self.current_field();
let col = self.cursor_position();
let new_idx = self
.editor
.data_provider_mut()
.split_line_at(line_idx, col);
let _ = self.editor.transition_to_field(new_idx);
self.editor.move_line_start();
self.editor.enter_edit_mode();
let _ = self.transition_to_field(new_idx);
self.move_line_start();
self.enter_edit_mode();
}
// Textarea-specific primitive: backspace with line join at start-of-line
pub fn backspace(&mut self) {
let col = self.editor.cursor_position();
let col = self.cursor_position();
if col > 0 {
let _ = self.editor.delete_backward();
let _ = self.delete_backward();
return;
}
let line_idx = self.editor.current_field();
let line_idx = self.current_field();
if line_idx == 0 {
return;
}
@@ -93,19 +111,20 @@ impl TextAreaState {
.data_provider_mut()
.join_with_prev(line_idx)
{
let _ = self.editor.transition_to_field(prev_idx);
self.editor.set_cursor_position(new_col);
self.editor.enter_edit_mode();
let _ = self.transition_to_field(prev_idx);
self.set_cursor_position(new_col);
self.enter_edit_mode();
}
}
// Textarea-specific primitive: delete or join with next line at EOL
pub fn delete_forward_or_join(&mut self) {
let line_idx = self.editor.current_field();
let line_len = self.editor.current_text().chars().count();
let col = self.editor.cursor_position();
let line_idx = self.current_field();
let line_len = self.current_text().chars().count();
let col = self.cursor_position();
if col < line_len {
let _ = self.editor.delete_forward();
let _ = self.delete_forward();
return;
}
@@ -114,12 +133,40 @@ impl TextAreaState {
.data_provider_mut()
.join_with_next(line_idx)
{
self.editor.set_cursor_position(new_col);
self.editor.enter_edit_mode();
self.set_cursor_position(new_col);
self.enter_edit_mode();
}
}
// Drive the editor from key events
// Override for multiline: insert new blank line below and enter insert mode.
pub fn open_line_below(&mut self) -> Result<()> {
let line_idx = self.current_field();
let new_idx = self
.editor
.data_provider_mut()
.insert_blank_line_after(line_idx);
self.transition_to_field(new_idx)?;
self.move_line_start();
self.enter_edit_mode();
Ok(())
}
// Override for multiline: insert new blank line above and enter insert mode.
pub fn open_line_above(&mut self) -> Result<()> {
let line_idx = self.current_field();
let new_idx = self
.editor
.data_provider_mut()
.insert_blank_line_before(line_idx);
self.transition_to_field(new_idx)?;
self.move_line_start();
self.enter_edit_mode();
Ok(())
}
// Drive from KeyEvent; you can still call all FormEditor methods directly
pub fn input(&mut self, key: KeyEvent) {
if key.kind != KeyEventKind::Press {
return;
@@ -131,49 +178,43 @@ impl TextAreaState {
(KeyCode::Delete, _) => self.delete_forward_or_join(),
(KeyCode::Left, _) => {
let _ = self.editor.move_left();
let _ = self.move_left();
}
(KeyCode::Right, _) => {
let _ = self.editor.move_right();
let _ = self.move_right();
}
(KeyCode::Up, _) => {
let _ = self.editor.move_up();
let _ = self.move_up();
}
(KeyCode::Down, _) => {
let _ = self.editor.move_down();
let _ = self.move_down();
}
(KeyCode::Home, _)
| (KeyCode::Char('a'), KeyModifiers::CONTROL) => {
self.editor.move_line_start();
self.move_line_start();
}
(KeyCode::End, _)
| (KeyCode::Char('e'), KeyModifiers::CONTROL) => {
self.editor.move_line_end();
self.move_line_end();
}
// Optional: word motions
(KeyCode::Char('b'), KeyModifiers::ALT) => {
self.editor.move_word_prev();
}
(KeyCode::Char('f'), KeyModifiers::ALT) => {
self.editor.move_word_next();
}
(KeyCode::Char('e'), KeyModifiers::ALT) => {
self.editor.move_word_end();
}
(KeyCode::Char('b'), KeyModifiers::ALT) => self.move_word_prev(),
(KeyCode::Char('f'), KeyModifiers::ALT) => self.move_word_next(),
(KeyCode::Char('e'), KeyModifiers::ALT) => self.move_word_end(),
// Insert printable characters
// Printable characters
(KeyCode::Char(c), m) if m.is_empty() => {
self.editor.enter_edit_mode();
let _ = self.editor.insert_char(c);
self.enter_edit_mode();
let _ = self.insert_char(c);
}
// Tab: insert 4 spaces (simple default)
// Simple Tab policy
(KeyCode::Tab, _) => {
self.editor.enter_edit_mode();
self.enter_edit_mode();
for _ in 0..4 {
let _ = self.editor.insert_char(' ');
let _ = self.insert_char(' ');
}
}
@@ -185,20 +226,19 @@ impl TextAreaState {
#[cfg(feature = "gui")]
pub fn cursor(&self, area: Rect, block: Option<&Block<'_>>) -> (u16, u16) {
let inner = if let Some(b) = block { b.inner(area) } else { area };
let line_idx = self.editor.current_field() as u16;
let line_idx = self.current_field() as u16;
let y = inner.y + line_idx.saturating_sub(self.scroll_y);
let current_line = self.editor.current_text();
let col = self.editor.display_cursor_position();
let current_line = self.current_text();
let col = self.display_cursor_position();
let mut x_off: u16 = 0;
for (i, ch) in current_line.chars().enumerate() {
if i >= col {
break;
}
x_off = x_off.saturating_add(
UnicodeWidthChar::width(ch).unwrap_or(0) as u16,
);
x_off = x_off
.saturating_add(UnicodeWidthChar::width(ch).unwrap_or(0) as u16);
}
let x = inner.x.saturating_add(x_off);
(x, y)
@@ -214,7 +254,7 @@ impl TextAreaState {
if inner.height == 0 {
return;
}
let line_idx = self.editor.current_field() as u16;
let line_idx = self.current_field() as u16;
if line_idx < self.scroll_y {
self.scroll_y = line_idx;
} else if line_idx >= self.scroll_y + inner.height {