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"
[[example]]
name = "canvas_gui_demo"
name = "canvas_cursor_auto"
required-features = ["gui", "cursor-style"]
path = "examples/canvas_gui_demo.rs"
path = "examples/canvas_cursor_auto.rs"
[[example]]
name = "validation_1"

View File

@@ -15,6 +15,7 @@ use crate::canvas::theme::{CanvasTheme, DefaultCanvasTheme};
use crate::canvas::modes::HighlightState;
use crate::data_provider::DataProvider;
use crate::editor::FormEditor;
use unicode_width::UnicodeWidthChar;
#[cfg(feature = "gui")]
use std::cmp::{max, min};
@@ -486,16 +487,24 @@ fn set_cursor_position(
field_rect: Rect,
text: &str,
current_cursor_pos: usize,
has_display_override: bool,
_has_display_override: bool,
) {
// BUG FIX: Use the correct display cursor position, not end of text
let cursor_x = field_rect.x + current_cursor_pos as u16;
// Sum display widths of the first current_cursor_pos characters
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;
// 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 safe_cursor_x = cursor_x.min(max_cursor_x);
f.set_cursor_position((safe_cursor_x, cursor_y));
}

View File

@@ -24,6 +24,19 @@ pub struct FormEditor<D: DataProvider> {
}
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 {
let mut editor = Self {
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
let new_raw_text = {
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
};
@@ -467,22 +481,38 @@ impl<D: DataProvider> FormEditor<D> {
}
/// 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 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")]
{
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(
self.ui_state.current_field,
&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);
// Clamp cursor to new field
@@ -493,6 +523,8 @@ impl<D: DataProvider> FormEditor<D> {
max_pos,
self.ui_state.current_mode == AppMode::Edit
);
Ok(())
}
/// Change mode (for vim compatibility)
@@ -1095,52 +1127,84 @@ impl<D: DataProvider> FormEditor<D> {
/// Delete character before cursor (vim x in insert mode / backspace)
pub fn delete_backward(&mut self) -> Result<()> {
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 {
return Ok(()); // Nothing to delete
return Ok(());
}
let field_index = self.ui_state.current_field;
let mut current_text = self.data_provider.field_value(field_index).to_string();
if self.ui_state.cursor_pos <= current_text.len() {
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;
let new_cursor = self.ui_state.cursor_pos.saturating_sub(1);
// Validate the new content if validation is enabled
#[cfg(feature = "validation")]
{
let _validation_result = self.ui_state.validation.validate_field_content(
field_index,
&current_text,
);
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")]
{
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,
&current_text,
);
}
Ok(())
}
/// Delete character under cursor (vim x / delete key)
pub fn delete_forward(&mut self) -> Result<()> {
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 mut current_text = self.data_provider.field_value(field_index).to_string();
if self.ui_state.cursor_pos < current_text.len() {
current_text.remove(self.ui_state.cursor_pos);
if self.ui_state.cursor_pos < current_text.chars().count() {
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());
// Validate the new content if validation is enabled
let mut target_cursor = self.ui_state.cursor_pos;
#[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,
&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)
pub fn display_cursor_position(&self) -> usize {
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 {
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
} 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;
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 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);
}
}
// Fallback to display mask
// Apply mask mapping using clamped raw_pos
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)