diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 1678586..5dfac30 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -332,6 +332,52 @@ impl EventHandler { if !outcome.get_message_if_ok().is_empty() { return Ok(outcome); } + } else if let Page::AddLogic(add_logic_page) = &mut router.current { + // Allow ":" (enter_command_mode) even when inside AddLogic canvas + if let Some(action) = + config.get_general_action(key_event.code, key_event.modifiers) + { + if action == "enter_command_mode" + && !self.command_mode + && !app_state.ui.show_search_palette + && !self.navigation_state.active + { + self.command_mode = true; + self.command_input.clear(); + self.command_message.clear(); + self.key_sequence_tracker.reset(); + app_state.ui.focus_outside_canvas = true; + return Ok(EventOutcome::Ok(String::new())); + } + } + let movement_action_early = if let Some(act) = + config.get_general_action(key_event.code, key_event.modifiers) + { + match act { + "up" => Some(MovementAction::Up), + "down" => Some(MovementAction::Down), + "left" => Some(MovementAction::Left), + "right" => Some(MovementAction::Right), + "next" => Some(MovementAction::Next), + "previous" => Some(MovementAction::Previous), + "select" => Some(MovementAction::Select), + "esc" => Some(MovementAction::Esc), + _ => None, + } + } else { None }; + + let outcome = add_logic::event::handle_add_logic_event( + key_event, + movement_action_early, + config, + app_state, + add_logic_page, + self.grpc_client.clone(), + self.save_logic_result_sender.clone(), + )?; + if !outcome.get_message_if_ok().is_empty() { + return Ok(outcome); + } } else if let Page::Admin(admin_state) = &mut router.current { if matches!(auth_state.role, Some(UserRole::Admin)) { if let Event::Key(key_event) = event { @@ -432,6 +478,22 @@ impl EventHandler { } } } + // Allow ":" / ctrl+; to enter command mode only when outside canvas. + if action == "enter_command_mode" { + if app_state.ui.focus_outside_canvas + && !self.command_mode + && !app_state.ui.show_search_palette + && !self.navigation_state.active + { + self.command_mode = true; + self.command_input.clear(); + self.command_message.clear(); + self.key_sequence_tracker.reset(); + // Keep focus outside so canvas won't receive keys + app_state.ui.focus_outside_canvas = true; + return Ok(EventOutcome::Ok(String::new())); + } + } } } @@ -485,22 +547,6 @@ impl EventHandler { return Ok(EventOutcome::Ok(self.command_message.clone())); } } - if let Page::AddLogic(add_logic_page) = &mut router.current { - let client_clone = self.grpc_client.clone(); - let sender_clone = self.save_logic_result_sender.clone(); - if add_logic::event::handle_add_logic_event( - key_event, - movement_action, - config, - app_state, - add_logic_page, - client_clone, - sender_clone, - &mut self.command_message, - ) { - return Ok(EventOutcome::Ok(self.command_message.clone())); - } - } // Generic navigation for the rest (Intro/Login/Register/Form) let nav_outcome = if matches!(&router.current, Page::AddTable(_) | Page::AddLogic(_)) { diff --git a/client/src/pages/admin_panel/add_logic/event.rs b/client/src/pages/admin_panel/add_logic/event.rs index ccc4ee6..37f8782 100644 --- a/client/src/pages/admin_panel/add_logic/event.rs +++ b/client/src/pages/admin_panel/add_logic/event.rs @@ -1,4 +1,6 @@ // src/pages/admin_panel/add_logic/event.rs + +use anyhow::Result; use crate::config::binds::config::Config; use crate::movement::{move_focus, MovementAction}; use crate::pages::admin_panel::add_logic::nav::SaveLogicResultSender; @@ -6,7 +8,8 @@ use crate::pages::admin_panel::add_logic::state::{AddLogicFocus, AddLogicFormSta use crate::components::common::text_editor::TextEditor; use crate::services::grpc_client::GrpcClient; use crate::state::app::state::AppState; -use canvas::DataProvider; +use crate::modes::handlers::event::EventOutcome; +use canvas::{AppMode as CanvasMode, DataProvider}; use crossterm::event::KeyEvent; /// Focus traversal order for non-canvas navigation @@ -19,7 +22,9 @@ const ADD_LOGIC_FOCUS_ORDER: [AddLogicFocus; 6] = [ AddLogicFocus::CancelButton, ]; -/// Return true if the event was handled and UI should be redrawn. +/// Handles all AddLogic page-specific events. +/// Return a non-empty Ok(message) only when the page actually consumed the key, +/// otherwise return Ok("") to let global handling proceed. pub fn handle_add_logic_event( key_event: KeyEvent, movement: Option, @@ -28,16 +33,14 @@ pub fn handle_add_logic_event( add_logic_page: &mut AddLogicFormState, grpc_client: GrpcClient, save_logic_sender: SaveLogicResultSender, - command_message: &mut String, -) -> bool { +) -> Result { // 1) Script editor fullscreen mode if add_logic_page.state.current_focus == AddLogicFocus::InsideScriptContent { match key_event.code { crossterm::event::KeyCode::Esc => { add_logic_page.state.current_focus = AddLogicFocus::ScriptContentPreview; app_state.ui.focus_outside_canvas = true; - *command_message = "Exited script editing.".to_string(); - return true; + return Ok(EventOutcome::Ok("Exited script editing.".to_string())); } _ => { let changed = { @@ -52,9 +55,9 @@ pub fn handle_add_logic_event( }; if changed { add_logic_page.state.has_unsaved_changes = true; - *command_message = "Script updated".to_string(); + return Ok(EventOutcome::Ok("Script updated".to_string())); } - return true; + return Ok(EventOutcome::Ok(String::new())); } } } @@ -68,32 +71,41 @@ pub fn handle_add_logic_event( ); if inside_canvas_inputs { - if let Some(ma) = movement { - let last_idx = add_logic_page.editor.data_provider().field_count().saturating_sub(1); - let at_last = add_logic_page.editor.current_field() >= last_idx; - if at_last && matches!(ma, MovementAction::Down | MovementAction::Next) { - add_logic_page.state.last_canvas_field = last_idx; - add_logic_page.state.current_focus = AddLogicFocus::ScriptContentPreview; - app_state.ui.focus_outside_canvas = true; - *command_message = "Moved to Script Preview".to_string(); - return true; + // Only allow leaving the canvas with Down/Next when the form editor + // is in ReadOnly mode. In Edit mode, keep focus inside the canvas. + let in_edit_mode = add_logic_page.editor.mode() == CanvasMode::Edit; + if !in_edit_mode { + if let Some(ma) = movement { + let last_idx = add_logic_page + .editor + .data_provider() + .field_count() + .saturating_sub(1); + let at_last = add_logic_page.editor.current_field() >= last_idx; + if at_last && matches!(ma, MovementAction::Down | MovementAction::Next) { + add_logic_page.state.last_canvas_field = last_idx; + add_logic_page.state.current_focus = AddLogicFocus::ScriptContentPreview; + app_state.ui.focus_outside_canvas = true; + return Ok(EventOutcome::Ok("Moved to Script Preview".to_string())); + } } } match add_logic_page.handle_key_event(key_event) { canvas::keymap::KeyEventOutcome::Consumed(Some(msg)) => { add_logic_page.sync_from_editor(); - if !msg.is_empty() { - *command_message = msg; - } - return true; + return Ok(EventOutcome::Ok(msg)); } canvas::keymap::KeyEventOutcome::Consumed(None) => { add_logic_page.sync_from_editor(); - return true; + return Ok(EventOutcome::Ok("Input updated".into())); + } + canvas::keymap::KeyEventOutcome::Pending => { + return Ok(EventOutcome::Ok(String::new())); + } + canvas::keymap::KeyEventOutcome::NotMatched => { + // fall through } - canvas::keymap::KeyEventOutcome::Pending => return true, - canvas::keymap::KeyEventOutcome::NotMatched => {} } } @@ -108,7 +120,7 @@ pub fn handle_add_logic_event( | AddLogicFocus::InputTargetColumn | AddLogicFocus::InputDescription ); - return true; + return Ok(EventOutcome::Ok(String::new())); } match ma { @@ -116,20 +128,19 @@ pub fn handle_add_logic_event( AddLogicFocus::ScriptContentPreview => { add_logic_page.state.current_focus = AddLogicFocus::InsideScriptContent; app_state.ui.focus_outside_canvas = false; - *command_message = "Fullscreen script editing. Esc to exit.".to_string(); - return true; + return Ok(EventOutcome::Ok( + "Fullscreen script editing. Esc to exit.".to_string(), + )); } AddLogicFocus::SaveButton => { if let Some(msg) = add_logic_page.state.save_logic() { - *command_message = msg; + return Ok(EventOutcome::Ok(msg)); } else { - *command_message = "Saved (no changes)".to_string(); + return Ok(EventOutcome::Ok("Saved (no changes)".to_string())); } - return true; } AddLogicFocus::CancelButton => { - *command_message = "Cancelled Add Logic".to_string(); - return true; + return Ok(EventOutcome::Ok("Cancelled Add Logic".to_string())); } _ => {} }, @@ -137,13 +148,12 @@ pub fn handle_add_logic_event( if add_logic_page.state.current_focus == AddLogicFocus::ScriptContentPreview { add_logic_page.state.current_focus = AddLogicFocus::InputDescription; app_state.ui.focus_outside_canvas = false; - *command_message = "Back to Description".to_string(); - return true; + return Ok(EventOutcome::Ok("Back to Description".to_string())); } } _ => {} } } - false + Ok(EventOutcome::Ok(String::new())) } diff --git a/client/src/pages/login/event.rs b/client/src/pages/login/event.rs index e96f86f..352348e 100644 --- a/client/src/pages/login/event.rs +++ b/client/src/pages/login/event.rs @@ -35,7 +35,8 @@ pub fn handle_login_event( if !login_page.focus_outside_canvas { let last_idx = login_page.editor.data_provider().field_count().saturating_sub(1); let at_last = login_page.editor.current_field() >= last_idx; - if at_last + if login_page.editor.mode() == CanvasMode::ReadOnly + && at_last && matches!( (key_code, modifiers), (KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _) diff --git a/client/src/pages/register/event.rs b/client/src/pages/register/event.rs index d32542f..a63d0e8 100644 --- a/client/src/pages/register/event.rs +++ b/client/src/pages/register/event.rs @@ -38,7 +38,8 @@ pub fn handle_register_event( if !register_page.focus_outside_canvas { let last_idx = register_page.editor.data_provider().field_count().saturating_sub(1); let at_last = register_page.editor.current_field() >= last_idx; - if at_last + if register_page.editor.mode() == CanvasMode::ReadOnly + && at_last && matches!( (key_code, modifiers), (KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _)