registering canvas functions now instead of internal state
This commit is contained in:
@@ -16,7 +16,7 @@ highlight_current_field = true
|
|||||||
move_left = ["h"]
|
move_left = ["h"]
|
||||||
move_right = ["l"]
|
move_right = ["l"]
|
||||||
move_up = ["k"]
|
move_up = ["k"]
|
||||||
move_down = ["p"]
|
move_down = ["j"]
|
||||||
move_word_next = ["w"]
|
move_word_next = ["w"]
|
||||||
move_word_end = ["e"]
|
move_word_end = ["e"]
|
||||||
move_word_prev = ["b"]
|
move_word_prev = ["b"]
|
||||||
|
|||||||
124
client/docs/canvas_add_functionality.md
Normal file
124
client/docs/canvas_add_functionality.md
Normal file
@@ -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<S: CanvasState>(
|
||||||
|
action: CanvasAction,
|
||||||
|
state: &mut S,
|
||||||
|
ideal_cursor_column: &mut usize,
|
||||||
|
) -> Result<ActionResult> {
|
||||||
|
// 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<String> {
|
||||||
|
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<String> {
|
||||||
|
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<String> {
|
||||||
|
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`
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/modes/canvas/edit.rs
|
// src/modes/canvas/edit.rs
|
||||||
use crate::config::binds::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::functions::modes::edit::{
|
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::modes::handlers::event::EventHandler;
|
||||||
use crate::services::grpc_client::GrpcClient;
|
use crate::services::grpc_client::GrpcClient;
|
||||||
@@ -127,6 +127,60 @@ pub async fn handle_form_edit_with_canvas(
|
|||||||
Ok(String::new())
|
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<S: CanvasState>(
|
||||||
|
key: KeyEvent,
|
||||||
|
config: &Config,
|
||||||
|
state: &mut S,
|
||||||
|
ideal_cursor_column: &mut usize,
|
||||||
|
) -> Result<String> {
|
||||||
|
// 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)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn handle_edit_event(
|
pub async fn handle_edit_event(
|
||||||
key: KeyEvent,
|
key: KeyEvent,
|
||||||
@@ -283,12 +337,12 @@ pub async fn handle_edit_event(
|
|||||||
return Ok(EditEventOutcome::ExitEditMode);
|
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 {
|
let msg = if app_state.ui.show_login {
|
||||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
|
||||||
auth_e::execute_edit_action(
|
handle_canvas_state_edit(
|
||||||
action_str,
|
|
||||||
key,
|
key,
|
||||||
|
config,
|
||||||
login_state,
|
login_state,
|
||||||
&mut event_handler.ideal_cursor_column,
|
&mut event_handler.ideal_cursor_column,
|
||||||
)
|
)
|
||||||
@@ -312,10 +366,10 @@ pub async fn handle_edit_event(
|
|||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
} else if app_state.ui.show_register {
|
} else if app_state.ui.show_register {
|
||||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
|
||||||
auth_e::execute_edit_action(
|
handle_canvas_state_edit(
|
||||||
action_str,
|
|
||||||
key,
|
key,
|
||||||
|
config,
|
||||||
register_state,
|
register_state,
|
||||||
&mut event_handler.ideal_cursor_column,
|
&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) ---
|
// --- FALLBACK FOR CHARACTER INSERTION (IF NO OTHER BINDING MATCHED) ---
|
||||||
if let KeyCode::Char(_) = key.code {
|
if let KeyCode::Char(_) = key.code {
|
||||||
let msg = if app_state.ui.show_login {
|
let msg = if app_state.ui.show_login {
|
||||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
|
||||||
auth_e::execute_edit_action(
|
handle_canvas_state_edit(
|
||||||
"insert_char",
|
|
||||||
key,
|
key,
|
||||||
|
config,
|
||||||
login_state,
|
login_state,
|
||||||
&mut event_handler.ideal_cursor_column,
|
&mut event_handler.ideal_cursor_column,
|
||||||
)
|
)
|
||||||
@@ -363,10 +417,10 @@ pub async fn handle_edit_event(
|
|||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
} else if app_state.ui.show_register {
|
} else if app_state.ui.show_register {
|
||||||
// FIX: Pass &mut event_handler.ideal_cursor_column
|
// NEW: Use unified canvas handler instead of auth_e::execute_edit_action
|
||||||
auth_e::execute_edit_action(
|
handle_canvas_state_edit(
|
||||||
"insert_char",
|
|
||||||
key,
|
key,
|
||||||
|
config,
|
||||||
register_state,
|
register_state,
|
||||||
&mut event_handler.ideal_cursor_column,
|
&mut event_handler.ideal_cursor_column,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user