end of the line fixed

This commit is contained in:
Priec
2025-08-18 00:22:09 +02:00
parent 25b54afff4
commit 6588f310f2
5 changed files with 304 additions and 208 deletions

View File

@@ -1,14 +1,15 @@
// src/textarea/mod.rs
// Module routing and re-exports only. No logic here.
pub mod provider;
pub mod state;
#[cfg(feature = "gui")]
pub mod widget;
#[cfg(feature = "keymaps")]
pub mod commands_impl;
pub use provider::TextAreaProvider;
pub use state::{TextAreaEditor, TextAreaState};
pub use state::{TextAreaEditor, TextAreaState, TextOverflowMode};
#[cfg(feature = "gui")]
pub use widget::TextArea;

View File

@@ -15,11 +15,17 @@ use unicode_width::UnicodeWidthChar;
pub type TextAreaEditor = FormEditor<TextAreaProvider>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextOverflowMode {
Indicator { ch: char }, // show trailing indicator (default '$')
Wrap, // soft wrap lines
}
pub struct TextAreaState {
pub(crate) editor: TextAreaEditor,
pub(crate) scroll_y: u16,
pub(crate) wrap: bool,
pub(crate) placeholder: Option<String>,
pub(crate) overflow_mode: TextOverflowMode,
}
impl Default for TextAreaState {
@@ -27,8 +33,8 @@ impl Default for TextAreaState {
Self {
editor: FormEditor::new(TextAreaProvider::default()),
scroll_y: 0,
wrap: false,
placeholder: None,
overflow_mode: TextOverflowMode::Indicator { ch: '$' },
}
}
}
@@ -54,8 +60,8 @@ impl TextAreaState {
Self {
editor: FormEditor::new(provider),
scroll_y: 0,
wrap: false,
placeholder: None,
overflow_mode: TextOverflowMode::Indicator { ch: '$' },
}
}
@@ -70,14 +76,20 @@ impl TextAreaState {
self.editor.ui_state.ideal_cursor_column = 0;
}
pub fn set_wrap(&mut self, wrap: bool) {
self.wrap = wrap;
}
pub fn set_placeholder<S: Into<String>>(&mut self, s: S) {
self.placeholder = Some(s.into());
}
// RUNTIME TOGGLES ----------------------------------------------------
pub fn use_overflow_indicator(&mut self, ch: char) {
self.overflow_mode = TextOverflowMode::Indicator { ch };
}
pub fn use_wrap(&mut self) {
self.overflow_mode = TextOverflowMode::Wrap;
}
// Textarea-specific primitive: split at cursor
pub fn insert_newline(&mut self) {
let line_idx = self.current_field();
@@ -106,10 +118,8 @@ impl TextAreaState {
return;
}
if let Some((prev_idx, new_col)) = self
.editor
.data_provider_mut()
.join_with_prev(line_idx)
if let Some((prev_idx, new_col)) =
self.editor.data_provider_mut().join_with_prev(line_idx)
{
let _ = self.transition_to_field(prev_idx);
self.set_cursor_position(new_col);
@@ -128,44 +138,14 @@ impl TextAreaState {
return;
}
if let Some(new_col) = self
.editor
.data_provider_mut()
.join_with_next(line_idx)
if let Some(new_col) =
self.editor.data_provider_mut().join_with_next(line_idx)
{
self.set_cursor_position(new_col);
self.enter_edit_mode();
}
}
// 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 {
@@ -199,7 +179,7 @@ impl TextAreaState {
self.move_line_end();
}
// Optional: word motions
// Optional: word motions (kept)
(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(),

View File

@@ -11,10 +11,13 @@ use ratatui::{
};
#[cfg(feature = "gui")]
use crate::data_provider::DataProvider; // bring trait into scope
use crate::data_provider::DataProvider;
#[cfg(feature = "gui")]
use crate::textarea::state::TextAreaState;
use crate::textarea::state::{TextAreaState, TextOverflowMode};
#[cfg(feature = "gui")]
use unicode_width::UnicodeWidthChar;
#[cfg(feature = "gui")]
#[derive(Debug, Clone)]
@@ -60,6 +63,38 @@ impl<'a> TextArea<'a> {
}
}
#[cfg(feature = "gui")]
fn display_width(s: &str) -> u16 {
s.chars()
.map(|c| UnicodeWidthChar::width(c).unwrap_or(0) as u16)
.sum()
}
#[cfg(feature = "gui")]
fn clip_with_indicator(s: &str, width: u16, indicator: char) -> Line<'static> {
if width == 0 {
return Line::from("");
}
if display_width(s) <= width {
return Line::from(Span::raw(s.to_string()));
}
let budget = width.saturating_sub(1);
let mut out = String::new();
let mut used: u16 = 0;
for ch in s.chars() {
let w = UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
if used + w > budget {
break;
}
out.push(ch);
used = used.saturating_add(w);
}
Line::from(vec![Span::raw(out), Span::raw(indicator.to_string())])
}
#[cfg(feature = "gui")]
impl<'a> StatefulWidget for TextArea<'a> {
type State = TextAreaState;
@@ -89,7 +124,14 @@ impl<'a> StatefulWidget for TextArea<'a> {
} else {
for i in start..end {
let s = state.editor.data_provider().field_value(i);
display_lines.push(Line::from(Span::raw(s.to_string())));
match state.overflow_mode {
TextOverflowMode::Wrap => {
display_lines.push(Line::from(Span::raw(s.to_string())));
}
TextOverflowMode::Indicator { ch } => {
display_lines.push(clip_with_indicator(s, inner.width, ch));
}
}
}
}
@@ -97,7 +139,7 @@ impl<'a> StatefulWidget for TextArea<'a> {
.alignment(Alignment::Left)
.style(self.style);
if state.wrap {
if matches!(state.overflow_mode, TextOverflowMode::Wrap) {
p = p.wrap(Wrap { trim: false });
}