now finally end line working as intended
This commit is contained in:
@@ -61,7 +61,6 @@ pub(crate) fn compute_h_scroll_with_padding(
|
|||||||
cursor_cols: u16,
|
cursor_cols: u16,
|
||||||
width: u16,
|
width: u16,
|
||||||
) -> (u16, u16) {
|
) -> (u16, u16) {
|
||||||
// Returns (h_scroll, left_cols). left_cols = 1 if a left indicator is shown.
|
|
||||||
let mut h = 0u16;
|
let mut h = 0u16;
|
||||||
for _ in 0..2 {
|
for _ in 0..2 {
|
||||||
let left_cols = if h > 0 { 1 } else { 0 };
|
let left_cols = if h > 0 { 1 } else { 0 };
|
||||||
@@ -78,11 +77,9 @@ pub(crate) fn compute_h_scroll_with_padding(
|
|||||||
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
fn normalize_indent(width: u16, indent: u16) -> u16 {
|
fn normalize_indent(width: u16, indent: u16) -> u16 {
|
||||||
// Ensure continuation capacity stays >= 1
|
|
||||||
indent.min(width.saturating_sub(1))
|
indent.min(width.saturating_sub(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count visual rows for a single logical line using early-wrap and continuation indent
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
pub(crate) fn count_wrapped_rows_indented(
|
pub(crate) fn count_wrapped_rows_indented(
|
||||||
s: &str,
|
s: &str,
|
||||||
@@ -103,11 +100,10 @@ pub(crate) fn count_wrapped_rows_indented(
|
|||||||
let w = UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
|
let w = UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
|
||||||
let cap = if first { width } else { cont_cap };
|
let cap = if first { width } else { cont_cap };
|
||||||
|
|
||||||
// Early-wrap: avoid the "one char freeze" at the boundary
|
|
||||||
if used > 0 && used.saturating_add(w) >= cap {
|
if used > 0 && used.saturating_add(w) >= cap {
|
||||||
rows = rows.saturating_add(1);
|
rows = rows.saturating_add(1);
|
||||||
first = false;
|
first = false;
|
||||||
used = indent; // continuation indent occupies leading cells
|
used = indent;
|
||||||
}
|
}
|
||||||
used = used.saturating_add(w);
|
used = used.saturating_add(w);
|
||||||
}
|
}
|
||||||
@@ -115,7 +111,6 @@ pub(crate) fn count_wrapped_rows_indented(
|
|||||||
rows
|
rows
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute caret (subrow, x) for a given cursor index with indent + early-wrap
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
fn wrapped_rows_to_cursor_indented(
|
fn wrapped_rows_to_cursor_indented(
|
||||||
s: &str,
|
s: &str,
|
||||||
@@ -143,12 +138,11 @@ fn wrapped_rows_to_cursor_indented(
|
|||||||
if used > 0 && used.saturating_add(w) >= cap {
|
if used > 0 && used.saturating_add(w) >= cap {
|
||||||
row = row.saturating_add(1);
|
row = row.saturating_add(1);
|
||||||
first = false;
|
first = false;
|
||||||
used = indent; // place indent on continuation line
|
used = indent;
|
||||||
}
|
}
|
||||||
used = used.saturating_add(w);
|
used = used.saturating_add(w);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'used' already includes indent when on continuation rows
|
|
||||||
(row, used.min(width.saturating_sub(1)))
|
(row, used.min(width.saturating_sub(1)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,8 +150,8 @@ pub type TextAreaEditor = FormEditor<TextAreaProvider>;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum TextOverflowMode {
|
pub enum TextOverflowMode {
|
||||||
Indicator { ch: char }, // show trailing indicator (default '$')
|
Indicator { ch: char },
|
||||||
Wrap, // soft wrap lines
|
Wrap,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TextAreaState {
|
pub struct TextAreaState {
|
||||||
@@ -166,9 +160,10 @@ pub struct TextAreaState {
|
|||||||
pub(crate) placeholder: Option<String>,
|
pub(crate) placeholder: Option<String>,
|
||||||
pub(crate) overflow_mode: TextOverflowMode,
|
pub(crate) overflow_mode: TextOverflowMode,
|
||||||
pub(crate) h_scroll: u16,
|
pub(crate) h_scroll: u16,
|
||||||
// NEW: visual indentation for wrapped continuation rows (Vim-like)
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
pub(crate) wrap_indent_cols: u16,
|
pub(crate) wrap_indent_cols: u16,
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
|
pub(crate) edited_this_frame: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TextAreaState {
|
impl Default for TextAreaState {
|
||||||
@@ -180,12 +175,13 @@ impl Default for TextAreaState {
|
|||||||
overflow_mode: TextOverflowMode::Indicator { ch: '$' },
|
overflow_mode: TextOverflowMode::Indicator { ch: '$' },
|
||||||
h_scroll: 0,
|
h_scroll: 0,
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
wrap_indent_cols: 0, // default: no continuation indent
|
wrap_indent_cols: 0,
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
|
edited_this_frame: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose the entire FormEditor API directly on TextAreaState
|
|
||||||
impl Deref for TextAreaState {
|
impl Deref for TextAreaState {
|
||||||
type Target = TextAreaEditor;
|
type Target = TextAreaEditor;
|
||||||
|
|
||||||
@@ -211,6 +207,8 @@ impl TextAreaState {
|
|||||||
h_scroll: 0,
|
h_scroll: 0,
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
wrap_indent_cols: 0,
|
wrap_indent_cols: 0,
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
|
edited_this_frame: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,8 +227,6 @@ impl TextAreaState {
|
|||||||
self.placeholder = Some(s.into());
|
self.placeholder = Some(s.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// RUNTIME TOGGLES ----------------------------------------------------
|
|
||||||
|
|
||||||
pub fn use_overflow_indicator(&mut self, ch: char) {
|
pub fn use_overflow_indicator(&mut self, ch: char) {
|
||||||
self.overflow_mode = TextOverflowMode::Indicator { ch };
|
self.overflow_mode = TextOverflowMode::Indicator { ch };
|
||||||
}
|
}
|
||||||
@@ -239,7 +235,6 @@ impl TextAreaState {
|
|||||||
self.overflow_mode = TextOverflowMode::Wrap;
|
self.overflow_mode = TextOverflowMode::Wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional: set continuation indent for wrap mode (e.g. 3 like Vim)
|
|
||||||
pub fn set_wrap_indent_cols(&mut self, cols: u16) {
|
pub fn set_wrap_indent_cols(&mut self, cols: u16) {
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
{
|
{
|
||||||
@@ -247,8 +242,11 @@ impl TextAreaState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Textarea-specific primitive: split at cursor
|
|
||||||
pub fn insert_newline(&mut self) {
|
pub fn insert_newline(&mut self) {
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
|
{
|
||||||
|
self.edited_this_frame = true;
|
||||||
|
}
|
||||||
let line_idx = self.current_field();
|
let line_idx = self.current_field();
|
||||||
let col = self.cursor_position();
|
let col = self.cursor_position();
|
||||||
|
|
||||||
@@ -262,10 +260,13 @@ impl TextAreaState {
|
|||||||
self.enter_edit_mode();
|
self.enter_edit_mode();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Textarea-specific primitive: backspace with line join at start-of-line
|
|
||||||
pub fn backspace(&mut self) {
|
pub fn backspace(&mut self) {
|
||||||
let col = self.cursor_position();
|
let col = self.cursor_position();
|
||||||
if col > 0 {
|
if col > 0 {
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
|
{
|
||||||
|
self.edited_this_frame = true;
|
||||||
|
}
|
||||||
let _ = self.delete_backward();
|
let _ = self.delete_backward();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -278,19 +279,26 @@ impl TextAreaState {
|
|||||||
if let Some((prev_idx, new_col)) =
|
if let Some((prev_idx, new_col)) =
|
||||||
self.editor.data_provider_mut().join_with_prev(line_idx)
|
self.editor.data_provider_mut().join_with_prev(line_idx)
|
||||||
{
|
{
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
|
{
|
||||||
|
self.edited_this_frame = true;
|
||||||
|
}
|
||||||
let _ = self.transition_to_field(prev_idx);
|
let _ = self.transition_to_field(prev_idx);
|
||||||
self.set_cursor_position(new_col);
|
self.set_cursor_position(new_col);
|
||||||
self.enter_edit_mode();
|
self.enter_edit_mode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Textarea-specific primitive: delete or join with next line at EOL
|
|
||||||
pub fn delete_forward_or_join(&mut self) {
|
pub fn delete_forward_or_join(&mut self) {
|
||||||
let line_idx = self.current_field();
|
let line_idx = self.current_field();
|
||||||
let line_len = self.current_text().chars().count();
|
let line_len = self.current_text().chars().count();
|
||||||
let col = self.cursor_position();
|
let col = self.cursor_position();
|
||||||
|
|
||||||
if col < line_len {
|
if col < line_len {
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
|
{
|
||||||
|
self.edited_this_frame = true;
|
||||||
|
}
|
||||||
let _ = self.delete_forward();
|
let _ = self.delete_forward();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -298,12 +306,15 @@ impl TextAreaState {
|
|||||||
if let Some(new_col) =
|
if let Some(new_col) =
|
||||||
self.editor.data_provider_mut().join_with_next(line_idx)
|
self.editor.data_provider_mut().join_with_next(line_idx)
|
||||||
{
|
{
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
|
{
|
||||||
|
self.edited_this_frame = true;
|
||||||
|
}
|
||||||
self.set_cursor_position(new_col);
|
self.set_cursor_position(new_col);
|
||||||
self.enter_edit_mode();
|
self.enter_edit_mode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drive from KeyEvent; you can still call all FormEditor methods directly
|
|
||||||
pub fn input(&mut self, key: KeyEvent) {
|
pub fn input(&mut self, key: KeyEvent) {
|
||||||
if key.kind != KeyEventKind::Press {
|
if key.kind != KeyEventKind::Press {
|
||||||
return;
|
return;
|
||||||
@@ -336,20 +347,25 @@ impl TextAreaState {
|
|||||||
self.move_line_end();
|
self.move_line_end();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional: word motions (kept)
|
|
||||||
(KeyCode::Char('b'), KeyModifiers::ALT) => self.move_word_prev(),
|
(KeyCode::Char('b'), KeyModifiers::ALT) => self.move_word_prev(),
|
||||||
(KeyCode::Char('f'), KeyModifiers::ALT) => self.move_word_next(),
|
(KeyCode::Char('f'), KeyModifiers::ALT) => self.move_word_next(),
|
||||||
(KeyCode::Char('e'), KeyModifiers::ALT) => self.move_word_end(),
|
(KeyCode::Char('e'), KeyModifiers::ALT) => self.move_word_end(),
|
||||||
|
|
||||||
// Printable characters
|
|
||||||
(KeyCode::Char(c), m) if m.is_empty() => {
|
(KeyCode::Char(c), m) if m.is_empty() => {
|
||||||
self.enter_edit_mode();
|
self.enter_edit_mode();
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
|
{
|
||||||
|
self.edited_this_frame = true;
|
||||||
|
}
|
||||||
let _ = self.insert_char(c);
|
let _ = self.insert_char(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple Tab policy
|
|
||||||
(KeyCode::Tab, _) => {
|
(KeyCode::Tab, _) => {
|
||||||
self.enter_edit_mode();
|
self.enter_edit_mode();
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
|
{
|
||||||
|
self.edited_this_frame = true;
|
||||||
|
}
|
||||||
for _ in 0..4 {
|
for _ in 0..4 {
|
||||||
let _ = self.insert_char(' ');
|
let _ = self.insert_char(' ');
|
||||||
}
|
}
|
||||||
@@ -359,12 +375,8 @@ impl TextAreaState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
// Cursor helpers for GUI
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
fn visual_rows_before_line_indented(
|
fn visual_rows_before_line_and_intra_indented(
|
||||||
&self,
|
&self,
|
||||||
width: u16,
|
width: u16,
|
||||||
line_idx: usize,
|
line_idx: usize,
|
||||||
@@ -392,13 +404,13 @@ impl TextAreaState {
|
|||||||
let indent = self.wrap_indent_cols;
|
let indent = self.wrap_indent_cols;
|
||||||
|
|
||||||
if width == 0 {
|
if width == 0 {
|
||||||
let prefix = self.visual_rows_before_line_indented(1, line_idx);
|
let prefix = self.visual_rows_before_line_and_intra_indented(1, line_idx);
|
||||||
let y = y_top.saturating_add(prefix.saturating_sub(self.scroll_y));
|
let y = y_top.saturating_add(prefix.saturating_sub(self.scroll_y));
|
||||||
return (inner.x, y);
|
return (inner.x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
let prefix_rows =
|
let prefix_rows =
|
||||||
self.visual_rows_before_line_indented(width, line_idx);
|
self.visual_rows_before_line_and_intra_indented(width, line_idx);
|
||||||
let current_line = self.current_text();
|
let current_line = self.current_text();
|
||||||
let col_chars = self.display_cursor_position();
|
let col_chars = self.display_cursor_position();
|
||||||
|
|
||||||
@@ -419,14 +431,14 @@ impl TextAreaState {
|
|||||||
let current_line = self.current_text();
|
let current_line = self.current_text();
|
||||||
let col = self.display_cursor_position();
|
let col = self.display_cursor_position();
|
||||||
|
|
||||||
// Display columns up to caret
|
|
||||||
let mut x_cols: u16 = 0;
|
let mut x_cols: u16 = 0;
|
||||||
|
let mut total_cols: u16 = 0;
|
||||||
for (i, ch) in current_line.chars().enumerate() {
|
for (i, ch) in current_line.chars().enumerate() {
|
||||||
if i >= col {
|
let w = UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
|
||||||
break;
|
if i < col {
|
||||||
|
x_cols = x_cols.saturating_add(w);
|
||||||
}
|
}
|
||||||
x_cols = x_cols
|
total_cols = total_cols.saturating_add(w);
|
||||||
.saturating_add(UnicodeWidthChar::width(ch).unwrap_or(0) as u16);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let left_cols = if self.h_scroll > 0 { 1 } else { 0 };
|
let left_cols = if self.h_scroll > 0 { 1 } else { 0 };
|
||||||
@@ -436,6 +448,7 @@ impl TextAreaState {
|
|||||||
.saturating_add(left_cols);
|
.saturating_add(left_cols);
|
||||||
|
|
||||||
let limit = inner.width.saturating_sub(1 + RIGHT_PAD);
|
let limit = inner.width.saturating_sub(1 + RIGHT_PAD);
|
||||||
|
|
||||||
if x_off_visible > limit {
|
if x_off_visible > limit {
|
||||||
x_off_visible = limit;
|
x_off_visible = limit;
|
||||||
}
|
}
|
||||||
@@ -455,7 +468,6 @@ impl TextAreaState {
|
|||||||
|
|
||||||
match self.overflow_mode {
|
match self.overflow_mode {
|
||||||
TextOverflowMode::Indicator { .. } => {
|
TextOverflowMode::Indicator { .. } => {
|
||||||
// Logical-line vertical scroll
|
|
||||||
let line_idx_u16 = self.current_field() as u16;
|
let line_idx_u16 = self.current_field() as u16;
|
||||||
if line_idx_u16 < self.scroll_y {
|
if line_idx_u16 < self.scroll_y {
|
||||||
self.scroll_y = line_idx_u16;
|
self.scroll_y = line_idx_u16;
|
||||||
@@ -469,6 +481,16 @@ impl TextAreaState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let current_line = self.current_text();
|
let current_line = self.current_text();
|
||||||
|
let mut total_cols: u16 = 0;
|
||||||
|
for ch in current_line.chars() {
|
||||||
|
total_cols = total_cols
|
||||||
|
.saturating_add(UnicodeWidthChar::width(ch).unwrap_or(0) as u16);
|
||||||
|
}
|
||||||
|
if total_cols <= width {
|
||||||
|
self.h_scroll = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let col = self.display_cursor_position();
|
let col = self.display_cursor_position();
|
||||||
let mut cursor_cols: u16 = 0;
|
let mut cursor_cols: u16 = 0;
|
||||||
for (i, ch) in current_line.chars().enumerate() {
|
for (i, ch) in current_line.chars().enumerate() {
|
||||||
@@ -499,7 +521,7 @@ impl TextAreaState {
|
|||||||
let line_idx = self.current_field() as usize;
|
let line_idx = self.current_field() as usize;
|
||||||
|
|
||||||
let prefix_rows =
|
let prefix_rows =
|
||||||
self.visual_rows_before_line_indented(width, line_idx);
|
self.visual_rows_before_line_and_intra_indented(width, line_idx);
|
||||||
|
|
||||||
let current_line = self.current_text();
|
let current_line = self.current_text();
|
||||||
let col = self.display_cursor_position();
|
let col = self.display_cursor_position();
|
||||||
@@ -522,8 +544,15 @@ impl TextAreaState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.h_scroll = 0; // no horizontal scroll in wrap mode
|
self.h_scroll = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
|
pub(crate) fn take_edited_flag(&mut self) -> bool {
|
||||||
|
let v = self.edited_this_frame;
|
||||||
|
self.edited_this_frame = false;
|
||||||
|
v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ fn clip_with_indicator(s: &str, width: u16, indicator: char) -> Line<'static> {
|
|||||||
Line::from(vec![Span::raw(out), Span::raw(indicator.to_string())])
|
Line::from(vec![Span::raw(out), Span::raw(indicator.to_string())])
|
||||||
}
|
}
|
||||||
|
|
||||||
// anchor: near other helpers
|
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
fn slice_by_display_cols(s: &str, start_cols: u16, max_cols: u16) -> String {
|
fn slice_by_display_cols(s: &str, start_cols: u16, max_cols: u16) -> String {
|
||||||
if max_cols == 0 {
|
if max_cols == 0 {
|
||||||
@@ -299,6 +298,8 @@ impl<'a> StatefulWidget for TextArea<'a> {
|
|||||||
area
|
area
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let edited_now = state.take_edited_flag();
|
||||||
|
|
||||||
let wrap_mode = matches!(state.overflow_mode, TextOverflowMode::Wrap);
|
let wrap_mode = matches!(state.overflow_mode, TextOverflowMode::Wrap);
|
||||||
let provider = state.editor.data_provider();
|
let provider = state.editor.data_provider();
|
||||||
let total = provider.line_count();
|
let total = provider.line_count();
|
||||||
@@ -338,20 +339,26 @@ impl<'a> StatefulWidget for TextArea<'a> {
|
|||||||
match state.overflow_mode {
|
match state.overflow_mode {
|
||||||
TextOverflowMode::Wrap => unreachable!(),
|
TextOverflowMode::Wrap => unreachable!(),
|
||||||
TextOverflowMode::Indicator { ch } => {
|
TextOverflowMode::Indicator { ch } => {
|
||||||
// Same-frame h-scroll so text shifts immediately
|
let fits = display_width(&s) <= inner.width;
|
||||||
|
|
||||||
let start_cols = if i == state.current_field() {
|
let start_cols = if i == state.current_field() {
|
||||||
let col_idx = state.display_cursor_position();
|
let col_idx = state.display_cursor_position();
|
||||||
let cursor_cols = display_cols_up_to(s, col_idx);
|
let cursor_cols = display_cols_up_to(&s, col_idx);
|
||||||
let (target_h, _left_cols) =
|
let (target_h, _left_cols) =
|
||||||
compute_h_scroll_with_padding(cursor_cols, inner.width);
|
compute_h_scroll_with_padding(cursor_cols, inner.width);
|
||||||
|
|
||||||
|
if fits {
|
||||||
|
if edited_now { target_h } else { 0 }
|
||||||
|
} else {
|
||||||
target_h.max(state.h_scroll)
|
target_h.max(state.h_scroll)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
|
||||||
display_lines.push(clip_window_with_indicator_padded(
|
display_lines.push(clip_window_with_indicator_padded(
|
||||||
s,
|
&s,
|
||||||
inner.width, // full view width
|
inner.width,
|
||||||
ch,
|
ch,
|
||||||
start_cols,
|
start_cols,
|
||||||
));
|
));
|
||||||
|
|||||||
Reference in New Issue
Block a user