improvements done by gpt5

This commit is contained in:
Priec
2025-08-08 23:10:23 +02:00
parent 8e3c85991c
commit 06106dc31b
3 changed files with 121 additions and 43 deletions

View File

@@ -42,9 +42,9 @@ required-features = ["suggestions", "gui", "cursor-style"]
path = "examples/suggestions.rs" path = "examples/suggestions.rs"
[[example]] [[example]]
name = "canvas_gui_demo" name = "canvas_cursor_auto"
required-features = ["gui", "cursor-style"] required-features = ["gui", "cursor-style"]
path = "examples/canvas_gui_demo.rs" path = "examples/canvas_cursor_auto.rs"
[[example]] [[example]]
name = "validation_1" name = "validation_1"

View File

@@ -15,6 +15,7 @@ use crate::canvas::theme::{CanvasTheme, DefaultCanvasTheme};
use crate::canvas::modes::HighlightState; use crate::canvas::modes::HighlightState;
use crate::data_provider::DataProvider; use crate::data_provider::DataProvider;
use crate::editor::FormEditor; use crate::editor::FormEditor;
use unicode_width::UnicodeWidthChar;
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
use std::cmp::{max, min}; use std::cmp::{max, min};
@@ -486,13 +487,21 @@ fn set_cursor_position(
field_rect: Rect, field_rect: Rect,
text: &str, text: &str,
current_cursor_pos: usize, current_cursor_pos: usize,
has_display_override: bool, _has_display_override: bool,
) { ) {
// BUG FIX: Use the correct display cursor position, not end of text // Sum display widths of the first current_cursor_pos characters
let cursor_x = field_rect.x + current_cursor_pos as u16; let mut cols: u16 = 0;
for (i, ch) in text.chars().enumerate() {
if i >= current_cursor_pos {
break;
}
cols = cols.saturating_add(UnicodeWidthChar::width(ch).unwrap_or(0) as u16);
}
let cursor_x = field_rect.x.saturating_add(cols);
let cursor_y = field_rect.y; let cursor_y = field_rect.y;
// SAFETY: Ensure cursor doesn't go beyond field bounds // Clamp to field bounds
let max_cursor_x = field_rect.x + field_rect.width.saturating_sub(1); let max_cursor_x = field_rect.x + field_rect.width.saturating_sub(1);
let safe_cursor_x = cursor_x.min(max_cursor_x); let safe_cursor_x = cursor_x.min(max_cursor_x);

View File

@@ -24,6 +24,19 @@ pub struct FormEditor<D: DataProvider> {
} }
impl<D: DataProvider> FormEditor<D> { impl<D: DataProvider> FormEditor<D> {
/// Convert a char index to a byte index in a string
fn char_to_byte_index(s: &str, char_idx: usize) -> usize {
s.char_indices()
.nth(char_idx)
.map(|(byte_idx, _)| byte_idx)
.unwrap_or_else(|| s.len())
}
/// Convert a byte index to a char index in a string
#[allow(dead_code)]
fn byte_to_char_index(s: &str, byte_idx: usize) -> usize {
s[..byte_idx].chars().count()
}
pub fn new(data_provider: D) -> Self { pub fn new(data_provider: D) -> Self {
let mut editor = Self { let mut editor = Self {
ui_state: EditorState::new(), ui_state: EditorState::new(),
@@ -341,7 +354,8 @@ impl<D: DataProvider> FormEditor<D> {
// 🔥 CRITICAL FIX 3: Validate the insertion won't break display/limit coordination // 🔥 CRITICAL FIX 3: Validate the insertion won't break display/limit coordination
let new_raw_text = { let new_raw_text = {
let mut temp = current_raw_text.to_string(); let mut temp = current_raw_text.to_string();
temp.insert(raw_cursor_pos, ch); let byte_pos = Self::char_to_byte_index(current_raw_text, raw_cursor_pos);
temp.insert(byte_pos, ch);
temp temp
}; };
@@ -467,22 +481,38 @@ impl<D: DataProvider> FormEditor<D> {
} }
/// Handle field navigation /// Handle field navigation
pub fn move_to_next_field(&mut self) { pub fn move_to_next_field(&mut self) -> Result<()> {
let field_count = self.data_provider.field_count(); let field_count = self.data_provider.field_count();
let next_field = (self.ui_state.current_field + 1) % field_count; if field_count == 0 {
return Ok(());
}
// Validate current field content before moving if validation is enabled // Check if field switching is allowed (minimum character enforcement)
#[cfg(feature = "validation")] #[cfg(feature = "validation")]
{ {
let current_text = self.current_text().to_string(); // Convert to String to avoid borrow conflicts let current_text = self.current_text();
if !self.ui_state.validation.allows_field_switch(self.ui_state.current_field, current_text) {
if let Some(reason) = self.ui_state.validation.field_switch_block_reason(
self.ui_state.current_field,
current_text
) {
tracing::debug!("Field switch blocked: {}", reason);
return Err(anyhow::anyhow!("Cannot switch fields: {}", reason));
}
}
}
// Validate current field before moving
#[cfg(feature = "validation")]
{
let current_text = self.current_text().to_string();
let _validation_result = self.ui_state.validation.validate_field_content( let _validation_result = self.ui_state.validation.validate_field_content(
self.ui_state.current_field, self.ui_state.current_field,
&current_text, &current_text,
); );
// Note: We don't prevent field switching on validation failure,
// just record the validation state
} }
let next_field = (self.ui_state.current_field + 1) % field_count;
self.ui_state.move_to_field(next_field, field_count); self.ui_state.move_to_field(next_field, field_count);
// Clamp cursor to new field // Clamp cursor to new field
@@ -493,6 +523,8 @@ impl<D: DataProvider> FormEditor<D> {
max_pos, max_pos,
self.ui_state.current_mode == AppMode::Edit self.ui_state.current_mode == AppMode::Edit
); );
Ok(())
} }
/// Change mode (for vim compatibility) /// Change mode (for vim compatibility)
@@ -1095,31 +1127,46 @@ impl<D: DataProvider> FormEditor<D> {
/// Delete character before cursor (vim x in insert mode / backspace) /// Delete character before cursor (vim x in insert mode / backspace)
pub fn delete_backward(&mut self) -> Result<()> { pub fn delete_backward(&mut self) -> Result<()> {
if self.ui_state.current_mode != AppMode::Edit { if self.ui_state.current_mode != AppMode::Edit {
return Ok(()); // Silently ignore in non-edit modes return Ok(());
} }
if self.ui_state.cursor_pos == 0 { if self.ui_state.cursor_pos == 0 {
return Ok(()); // Nothing to delete return Ok(());
} }
let field_index = self.ui_state.current_field; let field_index = self.ui_state.current_field;
let mut current_text = self.data_provider.field_value(field_index).to_string(); let mut current_text = self.data_provider.field_value(field_index).to_string();
if self.ui_state.cursor_pos <= current_text.len() { let new_cursor = self.ui_state.cursor_pos.saturating_sub(1);
current_text.remove(self.ui_state.cursor_pos - 1);
self.data_provider.set_field_value(field_index, current_text.clone());
self.ui_state.cursor_pos -= 1;
self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos;
// Validate the new content if validation is enabled let start = Self::char_to_byte_index(&current_text, self.ui_state.cursor_pos - 1);
let end = Self::char_to_byte_index(&current_text, self.ui_state.cursor_pos);
current_text.replace_range(start..end, "");
self.data_provider.set_field_value(field_index, current_text.clone());
// Always run reposition logic
let mut target_cursor = new_cursor;
#[cfg(feature = "validation")] #[cfg(feature = "validation")]
{ {
let _validation_result = self.ui_state.validation.validate_field_content( if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) {
if let Some(mask) = &cfg.display_mask {
let display_pos = mask.raw_pos_to_display_pos(new_cursor);
if let Some(prev_input) = mask.prev_input_position(display_pos) {
target_cursor = mask.display_pos_to_raw_pos(prev_input);
}
}
}
}
self.ui_state.cursor_pos = target_cursor;
self.ui_state.ideal_cursor_column = target_cursor;
#[cfg(feature = "validation")]
{
let _ = self.ui_state.validation.validate_field_content(
field_index, field_index,
&current_text, &current_text,
); );
} }
}
Ok(()) Ok(())
} }
@@ -1127,20 +1174,37 @@ impl<D: DataProvider> FormEditor<D> {
/// Delete character under cursor (vim x / delete key) /// Delete character under cursor (vim x / delete key)
pub fn delete_forward(&mut self) -> Result<()> { pub fn delete_forward(&mut self) -> Result<()> {
if self.ui_state.current_mode != AppMode::Edit { if self.ui_state.current_mode != AppMode::Edit {
return Ok(()); // Silently ignore in non-edit modes return Ok(());
} }
let field_index = self.ui_state.current_field; let field_index = self.ui_state.current_field;
let mut current_text = self.data_provider.field_value(field_index).to_string(); let mut current_text = self.data_provider.field_value(field_index).to_string();
if self.ui_state.cursor_pos < current_text.len() { if self.ui_state.cursor_pos < current_text.chars().count() {
current_text.remove(self.ui_state.cursor_pos); let start = Self::char_to_byte_index(&current_text, self.ui_state.cursor_pos);
let end = Self::char_to_byte_index(&current_text, self.ui_state.cursor_pos + 1);
current_text.replace_range(start..end, "");
self.data_provider.set_field_value(field_index, current_text.clone()); self.data_provider.set_field_value(field_index, current_text.clone());
// Validate the new content if validation is enabled let mut target_cursor = self.ui_state.cursor_pos;
#[cfg(feature = "validation")] #[cfg(feature = "validation")]
{ {
let _validation_result = self.ui_state.validation.validate_field_content( if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) {
if let Some(mask) = &cfg.display_mask {
let display_pos = mask.raw_pos_to_display_pos(self.ui_state.cursor_pos);
let next_input = mask.next_input_position(display_pos);
target_cursor = mask.display_pos_to_raw_pos(next_input)
.min(current_text.chars().count());
}
}
}
self.ui_state.cursor_pos = target_cursor;
self.ui_state.ideal_cursor_column = target_cursor;
#[cfg(feature = "validation")]
{
let _ = self.ui_state.validation.validate_field_content(
field_index, field_index,
&current_text, &current_text,
); );
@@ -1286,13 +1350,18 @@ impl<D: DataProvider> FormEditor<D> {
/// Get cursor position for display (maps raw cursor to display position with formatter/mask) /// Get cursor position for display (maps raw cursor to display position with formatter/mask)
pub fn display_cursor_position(&self) -> usize { pub fn display_cursor_position(&self) -> usize {
let current_text = self.current_text(); let current_text = self.current_text();
// Count characters, not bytes
let char_count = current_text.chars().count();
// Clamp raw_pos based on mode
let raw_pos = match self.ui_state.current_mode { let raw_pos = match self.ui_state.current_mode {
AppMode::Edit => self.ui_state.cursor_pos.min(current_text.len()), AppMode::Edit => self.ui_state.cursor_pos.min(char_count),
_ => { _ => {
if current_text.is_empty() { if char_count == 0 {
0 0
} else { } else {
self.ui_state.cursor_pos.min(current_text.len().saturating_sub(1)) self.ui_state.cursor_pos.min(char_count.saturating_sub(1))
} }
} }
}; };
@@ -1301,20 +1370,20 @@ impl<D: DataProvider> FormEditor<D> {
{ {
let field_index = self.ui_state.current_field; let field_index = self.ui_state.current_field;
if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) { if let Some(cfg) = self.ui_state.validation.get_field_config(field_index) {
// Only apply custom formatter cursor mapping when NOT editing // Apply custom formatter mapping if not editing
if !matches!(self.ui_state.current_mode, AppMode::Edit) { if !matches!(self.ui_state.current_mode, AppMode::Edit) {
if let Some((formatted, mapper, _warning)) = cfg.run_custom_formatter(current_text) { if let Some((formatted, mapper, _)) = cfg.run_custom_formatter(current_text) {
return mapper.raw_to_formatted(current_text, &formatted, raw_pos); return mapper.raw_to_formatted(current_text, &formatted, raw_pos);
} }
} }
// Fallback to display mask // Apply mask mapping using clamped raw_pos
if let Some(mask) = &cfg.display_mask { if let Some(mask) = &cfg.display_mask {
return mask.raw_pos_to_display_pos(self.ui_state.cursor_pos); return mask.raw_pos_to_display_pos(raw_pos);
} }
} }
} }
self.ui_state.cursor_pos raw_pos
} }
/// Cleanup cursor style (call this when shutting down) /// Cleanup cursor style (call this when shutting down)