diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 8555472..4f75eb4 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -21,6 +21,7 @@ use crate::modes::{ use crate::services::auth::AuthClient; use crate::services::grpc_client::GrpcClient; use canvas::AppMode as CanvasMode; +use canvas::DataProvider; use crate::state::app::state::AppState; use crate::pages::admin::AdminState; use crate::state::pages::auth::AuthState; @@ -279,20 +280,55 @@ impl EventHandler { let key_code = key_event.code; let modifiers = key_event.modifiers; - if let Page::Login(login_page) = &mut router.current { - match login_page.handle_key_event(key_event) { - KeyEventOutcome::Consumed(Some(msg)) => { - self.command_message = msg; - return Ok(EventOutcome::Ok("Login input updated".to_string())); + // LOGIN: canvas <-> buttons focus handoff + // Do not let Login canvas receive keys when overlays/palettes are active + let overlay_active = self.command_mode + || app_state.ui.show_search_palette + || self.navigation_state.active; + + if !overlay_active { + if let Page::Login(login_page) = &mut router.current { + use crossterm::event::{KeyCode, KeyModifiers}; + + // Inside canvas: at the last field, 'j' or Down moves focus to buttons + if !app_state.ui.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 + && matches!( + (key_code, modifiers), + (KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _) + ) + { + app_state.ui.focus_outside_canvas = true; + app_state.focused_button_index = 0; // focus "Login" button + // Ensure canvas mode is ReadOnly when leaving + login_page.editor.set_mode(CanvasMode::ReadOnly); + return Ok(EventOutcome::Ok("Focus moved to buttons".to_string())); + } } - KeyEventOutcome::Consumed(None) => { - return Ok(EventOutcome::Ok("Login input updated".to_string())); - } - KeyEventOutcome::Pending => { - return Ok(EventOutcome::Ok("Waiting for next key...".to_string())); - } - KeyEventOutcome::NotMatched => { - // fall through to other handlers (buttons, etc.) + + // Only forward to the canvas while focus is inside it + if !app_state.ui.focus_outside_canvas { + match login_page.handle_key_event(key_event) { + KeyEventOutcome::Consumed(Some(msg)) => { + self.command_message = msg; + return Ok(EventOutcome::Ok("Login input updated".to_string())); + } + KeyEventOutcome::Consumed(None) => { + return Ok(EventOutcome::Ok("Login input updated".to_string())); + } + KeyEventOutcome::Pending => { + return Ok(EventOutcome::Ok("Waiting for next key...".to_string())); + } + KeyEventOutcome::NotMatched => { + // fall through to other handlers (buttons, etc.) + } + } } } } @@ -407,6 +443,21 @@ impl EventHandler { // Let the current page handle decoupled movement first if let Some(ma) = movement_action { match &mut router.current { + // LOGIN: From buttons (general) back into the canvas with 'k' (Up), + // but ONLY from the left-most "Login" button. + Page::Login(page) => { + if app_state.ui.focus_outside_canvas { + if app_state.focused_button_index == 0 + && matches!(ma, crate::movement::MovementAction::Up) + { + app_state.ui.focus_outside_canvas = false; + // Enter canvas in ReadOnly mode (never jump straight to Edit) + page.editor.set_mode(CanvasMode::ReadOnly); + // Optional: keep current field (usually 0 initially) + return Ok(EventOutcome::Ok(String::new())); + } + } + } Page::AddTable(state) => { if state.handle_movement(ma) { // Keep UI focus consistent with inputs vs. outer elements @@ -414,8 +465,8 @@ impl EventHandler { let is_canvas_input = matches!( state.current_focus, AddTableFocus::InputTableName - | AddTableFocus::InputColumnName - | AddTableFocus::InputColumnType + | AddTableFocus::InputColumnName + | AddTableFocus::InputColumnType ); app_state.ui.focus_outside_canvas = !is_canvas_input; return Ok(EventOutcome::Ok(String::new())); diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index 32175b5..07850f0 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -202,25 +202,37 @@ pub async fn run_ui() -> Result<()> { let event = event_reader.read_event().context("Failed to read terminal event")?; event_processed = true; + // Decouple Command Line and palettes from canvas: + // Only forward keys to Form canvas when: + // - not in command mode + // - no search/palette active + // - focus is inside the canvas if let crossterm_event::Event::Key(key_event) = &event { - if let Page::Form(_) = &router.current { - if let Some(editor) = app_state.form_editor.as_mut() { - match editor.handle_key_event(*key_event) { - KeyEventOutcome::Consumed(Some(msg)) => { - event_handler.command_message = msg; - needs_redraw = true; - continue; - } - KeyEventOutcome::Consumed(None) => { - needs_redraw = true; - continue; - } - KeyEventOutcome::Pending => { - needs_redraw = true; - continue; - } - KeyEventOutcome::NotMatched => { - // fall through to client-level handling + let overlay_active = event_handler.command_mode + || app_state.ui.show_search_palette + || event_handler.navigation_state.active; + if !overlay_active { + if let Page::Form(_) = &router.current { + if !app_state.ui.focus_outside_canvas { + if let Some(editor) = app_state.form_editor.as_mut() { + match editor.handle_key_event(*key_event) { + KeyEventOutcome::Consumed(Some(msg)) => { + event_handler.command_message = msg; + needs_redraw = true; + continue; + } + KeyEventOutcome::Consumed(None) => { + needs_redraw = true; + continue; + } + KeyEventOutcome::Pending => { + needs_redraw = true; + continue; + } + KeyEventOutcome::NotMatched => { + // fall through to client-level handling + } + } } } } @@ -706,7 +718,15 @@ pub async fn run_ui() -> Result<()> { terminal .show_cursor() .context("Failed to show cursor in Command mode")?; + + // Enforce ReadOnly on any active canvases while in command mode + if let Some(editor) = &mut app_state.form_editor { + editor.set_mode(canvas::AppMode::ReadOnly); } + if let Page::Login(page) = &mut router.current { + page.editor.set_mode(canvas::AppMode::ReadOnly); + } + } } // Workaround for borrow checker