diff --git a/canvas/canvas_config.toml b/canvas/canvas_config.toml index 7f561c0..3006cd5 100644 --- a/canvas/canvas_config.toml +++ b/canvas/canvas_config.toml @@ -16,7 +16,7 @@ highlight_current_field = true move_left = ["h"] move_right = ["l"] move_up = ["k"] -move_down = ["p"] +move_down = ["j"] move_word_next = ["w"] move_word_end = ["e"] move_word_prev = ["b"] diff --git a/client/docs/canvas_add_functionality.md b/client/docs/canvas_add_functionality.md new file mode 100644 index 0000000..39216ae --- /dev/null +++ b/client/docs/canvas_add_functionality.md @@ -0,0 +1,124 @@ +## How Canvas Library Custom Functionality Works + +### 1. **The Canvas Library Calls YOUR Custom Code First** + +When you call `ActionDispatcher::dispatch()`, here's what happens: + +```rust +// Inside canvas library (canvas/src/actions/edit.rs): +pub async fn execute_canvas_action( + action: CanvasAction, + state: &mut S, + ideal_cursor_column: &mut usize, +) -> Result { + // 1. FIRST: Canvas library calls YOUR custom handler + if let Some(result) = state.handle_feature_action(&action, &context) { + return Ok(ActionResult::HandledByFeature(result)); // YOUR code handled it + } + + // 2. ONLY IF your code returns None: Canvas handles generic actions + handle_generic_canvas_action(action, state, ideal_cursor_column).await +} +``` + +### 2. **Your Extension Point: `handle_feature_action`** + +You add custom functionality by implementing `handle_feature_action` in your states: + +```rust +// In src/state/pages/auth.rs +impl CanvasState for LoginState { + // ... other methods ... + + fn handle_feature_action(&mut self, action: &CanvasAction, context: &ActionContext) -> Option { + match action { + // Custom login-specific actions + CanvasAction::Custom(action_str) if action_str == "submit_login" => { + if self.username.is_empty() || self.password.is_empty() { + Some("Please fill in all required fields".to_string()) + } else { + // Trigger login process + Some(format!("Logging in user: {}", self.username)) + } + } + + CanvasAction::Custom(action_str) if action_str == "clear_form" => { + self.username.clear(); + self.password.clear(); + self.set_has_unsaved_changes(false); + Some("Login form cleared".to_string()) + } + + // Custom behavior for standard actions + CanvasAction::NextField => { + // Custom validation when moving between fields + if self.current_field == 0 && self.username.is_empty() { + Some("Username cannot be empty".to_string()) + } else { + None // Let canvas library handle the normal field movement + } + } + + // Let canvas library handle everything else + _ => None, + } + } +} +``` + +### 3. **Multiple Ways to Add Custom Functionality** + +#### A) **Custom Actions via Config** +```toml +# In config.toml +[keybindings.edit] +submit_login = ["ctrl+enter"] +clear_form = ["ctrl+r"] +``` + +#### B) **Override Standard Actions** +```rust +fn handle_feature_action(&mut self, action: &CanvasAction, context: &ActionContext) -> Option { + match action { + CanvasAction::InsertChar('p') if self.current_field == 1 => { + // Custom behavior when typing 'p' in password field + Some("Password field - use secure input".to_string()) + } + _ => None, // Let canvas handle normally + } +} +``` + +#### C) **Context-Aware Logic** +```rust +fn handle_feature_action(&mut self, action: &CanvasAction, context: &ActionContext) -> Option { + match action { + CanvasAction::MoveDown => { + // Custom logic based on current state + if context.current_field == 1 && context.current_input.len() < 8 { + Some("Password should be at least 8 characters".to_string()) + } else { + None // Normal field movement + } + } + _ => None, + } +} +``` + +## The Canvas Library Philosophy + +**Canvas Library = Generic behavior + Your extension points** + +- ✅ **Canvas handles**: Character insertion, cursor movement, field navigation, etc. +- ✅ **You handle**: Validation, submission, clearing, app-specific logic +- ✅ **You decide**: Return `Some(message)` to override, `None` to use canvas default + +## Summary + +You **don't communicate with the library elsewhere**. Instead: + +1. **Canvas library calls your code first** via `handle_feature_action` +2. **Your code decides** whether to handle the action or let canvas handle it +3. **Canvas library handles** generic form behavior when you return `None` + diff --git a/client/src/modes/canvas/edit.rs b/client/src/modes/canvas/edit.rs index ea9e420..2a01283 100644 --- a/client/src/modes/canvas/edit.rs +++ b/client/src/modes/canvas/edit.rs @@ -1,7 +1,7 @@ // src/modes/canvas/edit.rs use crate::config::binds::config::Config; use crate::functions::modes::edit::{ - add_logic_e, add_table_e, auth_e, form_e, + add_logic_e, add_table_e, form_e, }; use crate::modes::handlers::event::EventHandler; use crate::services::grpc_client::GrpcClient; @@ -127,6 +127,60 @@ pub async fn handle_form_edit_with_canvas( Ok(String::new()) } +/// NEW: Unified canvas action handler for any CanvasState (LoginState, RegisterState, etc.) +/// This replaces the old auth_e::execute_edit_action calls with the new canvas library +async fn handle_canvas_state_edit( + key: KeyEvent, + config: &Config, + state: &mut S, + ideal_cursor_column: &mut usize, +) -> Result { + // Try direct key mapping first (same pattern as FormState) + if let Some(canvas_action) = CanvasAction::from_key(key.code) { + match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await { + Ok(ActionResult::Success(msg)) => { + return Ok(msg.unwrap_or_default()); + } + Ok(ActionResult::HandledByFeature(msg)) => { + return Ok(msg); + } + Ok(ActionResult::Error(msg)) => { + return Ok(format!("Error: {}", msg)); + } + Ok(ActionResult::RequiresContext(msg)) => { + return Ok(format!("Context needed: {}", msg)); + } + Err(_) => { + // Fall through to try config mapping + } + } + } + + // Try config-mapped action (same pattern as FormState) + if let Some(action_str) = config.get_edit_action_for_key(key.code, key.modifiers) { + let canvas_action = CanvasAction::from_string(&action_str); + match ActionDispatcher::dispatch(canvas_action, state, ideal_cursor_column).await { + Ok(ActionResult::Success(msg)) => { + return Ok(msg.unwrap_or_default()); + } + Ok(ActionResult::HandledByFeature(msg)) => { + return Ok(msg); + } + Ok(ActionResult::Error(msg)) => { + return Ok(format!("Error: {}", msg)); + } + Ok(ActionResult::RequiresContext(msg)) => { + return Ok(format!("Context needed: {}", msg)); + } + Err(e) => { + return Ok(format!("Action failed: {}", e)); + } + } + } + + Ok(String::new()) +} + #[allow(clippy::too_many_arguments)] pub async fn handle_edit_event( key: KeyEvent, @@ -283,12 +337,12 @@ pub async fn handle_edit_event( return Ok(EditEventOutcome::ExitEditMode); } - // Handle all other edit actions + // Handle all other edit actions - NOW USING CANVAS LIBRARY let msg = if app_state.ui.show_login { - // FIX: Pass &mut event_handler.ideal_cursor_column - auth_e::execute_edit_action( - action_str, + // NEW: Use unified canvas handler instead of auth_e::execute_edit_action + handle_canvas_state_edit( key, + config, login_state, &mut event_handler.ideal_cursor_column, ) @@ -312,10 +366,10 @@ pub async fn handle_edit_event( ) .await? } else if app_state.ui.show_register { - // FIX: Pass &mut event_handler.ideal_cursor_column - auth_e::execute_edit_action( - action_str, + // NEW: Use unified canvas handler instead of auth_e::execute_edit_action + handle_canvas_state_edit( key, + config, register_state, &mut event_handler.ideal_cursor_column, ) @@ -336,10 +390,10 @@ pub async fn handle_edit_event( // --- FALLBACK FOR CHARACTER INSERTION (IF NO OTHER BINDING MATCHED) --- if let KeyCode::Char(_) = key.code { let msg = if app_state.ui.show_login { - // FIX: Pass &mut event_handler.ideal_cursor_column - auth_e::execute_edit_action( - "insert_char", + // NEW: Use unified canvas handler instead of auth_e::execute_edit_action + handle_canvas_state_edit( key, + config, login_state, &mut event_handler.ideal_cursor_column, ) @@ -363,10 +417,10 @@ pub async fn handle_edit_event( ) .await? } else if app_state.ui.show_register { - // FIX: Pass &mut event_handler.ideal_cursor_column - auth_e::execute_edit_action( - "insert_char", + // NEW: Use unified canvas handler instead of auth_e::execute_edit_action + handle_canvas_state_edit( key, + config, register_state, &mut event_handler.ideal_cursor_column, )