improvements done by gpt5
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
¤t_text,
|
¤t_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,52 +1127,84 @@ 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(¤t_text, self.ui_state.cursor_pos - 1);
|
||||||
#[cfg(feature = "validation")]
|
let end = Self::char_to_byte_index(¤t_text, self.ui_state.cursor_pos);
|
||||||
{
|
current_text.replace_range(start..end, "");
|
||||||
let _validation_result = self.ui_state.validation.validate_field_content(
|
self.data_provider.set_field_value(field_index, current_text.clone());
|
||||||
field_index,
|
|
||||||
¤t_text,
|
// 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,
|
||||||
|
¤t_text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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(¤t_text, self.ui_state.cursor_pos);
|
||||||
|
let end = Self::char_to_byte_index(¤t_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,
|
||||||
¤t_text,
|
¤t_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)
|
||||||
|
|||||||
Reference in New Issue
Block a user