diff --git a/client/src/dialog/functions.rs b/client/src/dialog/functions.rs index 69356b8..60946fd 100644 --- a/client/src/dialog/functions.rs +++ b/client/src/dialog/functions.rs @@ -19,7 +19,6 @@ impl AppState { self.ui.dialog.purpose = Some(purpose); self.ui.dialog.is_loading = false; self.ui.dialog.dialog_show = true; - self.ui.focus_outside_canvas = true; } pub fn show_loading_dialog(&mut self, title: &str, message: &str) { @@ -30,7 +29,6 @@ impl AppState { self.ui.dialog.purpose = None; self.ui.dialog.is_loading = true; self.ui.dialog.dialog_show = true; - self.ui.focus_outside_canvas = true; } pub fn update_dialog_content( @@ -55,7 +53,6 @@ impl AppState { self.ui.dialog.dialog_buttons.clear(); self.ui.dialog.dialog_active_button_index = 0; self.ui.dialog.purpose = None; - self.ui.focus_outside_canvas = false; self.ui.dialog.is_loading = false; } diff --git a/client/src/modes/general/navigation.rs b/client/src/modes/general/navigation.rs index 01895c0..9cf4bae 100644 --- a/client/src/modes/general/navigation.rs +++ b/client/src/modes/general/navigation.rs @@ -67,11 +67,11 @@ pub async fn handle_navigation_event( "select" => { let (context, index) = match &router.current { Page::Intro(state) => (UiContext::Intro, state.selected_option), - Page::Login(_) if app_state.ui.focus_outside_canvas => { - (UiContext::Login, app_state.focused_button_index) + Page::Login(state) if state.focus_outside_canvas => { + (UiContext::Login, state.focused_button_index) } - Page::Register(_) if app_state.ui.focus_outside_canvas => { - (UiContext::Register, app_state.focused_button_index) + Page::Register(state) if state.focus_outside_canvas => { + (UiContext::Register, state.focused_button_index) } Page::Admin(state) => { (UiContext::Admin, state.get_selected_index().unwrap_or(0)) @@ -91,24 +91,24 @@ pub async fn handle_navigation_event( pub fn up(app_state: &mut AppState, router: &mut Router) { match &mut router.current { - Page::Login(page) if app_state.ui.focus_outside_canvas => { - if app_state.focused_button_index == 0 { - app_state.ui.focus_outside_canvas = false; + Page::Login(page) if page.focus_outside_canvas => { + if page.focused_button_index == 0 { + page.focus_outside_canvas = false; let last_field_index = page.state.field_count().saturating_sub(1); page.state.set_current_field(last_field_index); } else { - app_state.focused_button_index = - app_state.focused_button_index.saturating_sub(1); + page.focused_button_index = + page.focused_button_index.saturating_sub(1); } } - Page::Register(state) if app_state.ui.focus_outside_canvas => { - if app_state.focused_button_index == 0 { - app_state.ui.focus_outside_canvas = false; + Page::Register(state) if state.focus_outside_canvas => { + if state.focused_button_index == 0 { + state.focus_outside_canvas = false; let last_field_index = state.state.field_count().saturating_sub(1); state.set_current_field(last_field_index); } else { - app_state.focused_button_index = - app_state.focused_button_index.saturating_sub(1); + state.focused_button_index = + state.focused_button_index.saturating_sub(1); } } Page::Intro(state) => state.previous_option(), @@ -119,10 +119,16 @@ pub fn up(app_state: &mut AppState, router: &mut Router) { pub fn down(app_state: &mut AppState, router: &mut Router) { match &mut router.current { - Page::Login(_) | Page::Register(_) if app_state.ui.focus_outside_canvas => { + Page::Login(state) if state.focus_outside_canvas => { let num_general_elements = 2; - if app_state.focused_button_index < num_general_elements - 1 { - app_state.focused_button_index += 1; + if state.focused_button_index < num_general_elements - 1 { + state.focused_button_index += 1; + } + } + Page::Register(state) if state.focus_outside_canvas => { + let num_general_elements = 2; + if state.focused_button_index < num_general_elements - 1 { + state.focused_button_index += 1; } } Page::Intro(state) => state.next_option(), @@ -134,11 +140,11 @@ pub fn down(app_state: &mut AppState, router: &mut Router) { pub fn next_option(app_state: &mut AppState, router: &mut Router) { match &mut router.current { Page::Intro(state) => state.next_option(), - Page::Admin(_) => { + Page::Admin(state) => { let option_count = app_state.profile_tree.profiles.len(); if option_count > 0 { - app_state.focused_button_index = - (app_state.focused_button_index + 1) % option_count; + state.focused_button_index = + (state.focused_button_index + 1) % option_count; } } _ => {} @@ -148,13 +154,13 @@ pub fn next_option(app_state: &mut AppState, router: &mut Router) { pub fn previous_option(app_state: &mut AppState, router: &mut Router) { match &mut router.current { Page::Intro(state) => state.previous_option(), - Page::Admin(_) => { + Page::Admin(state) => { let option_count = app_state.profile_tree.profiles.len(); if option_count > 0 { - app_state.focused_button_index = if app_state.focused_button_index == 0 { + state.focused_button_index = if state.focused_button_index == 0 { option_count.saturating_sub(1) } else { - app_state.focused_button_index - 1 + state.focused_button_index - 1 }; } } diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 6737cb1..dd5a617 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -346,7 +346,7 @@ impl EventHandler { self.command_input.clear(); self.command_message.clear(); self.key_sequence_tracker.reset(); - app_state.ui.focus_outside_canvas = true; + self.set_focus_outside(router, true); return Ok(EventOutcome::Ok(String::new())); } } @@ -392,7 +392,7 @@ impl EventHandler { self.command_input.clear(); self.command_message.clear(); self.key_sequence_tracker.reset(); - app_state.ui.focus_outside_canvas = true; + self.set_focus_outside(router, true); return Ok(EventOutcome::Ok(String::new())); } } @@ -523,7 +523,7 @@ impl EventHandler { app_state.ui.show_search_palette = true; app_state.search_state = Some(SearchState::new(table_name)); - app_state.ui.focus_outside_canvas = true; + self.set_focus_outside(router, true); return Ok(EventOutcome::Ok( "Search palette opened".to_string(), )); @@ -532,17 +532,17 @@ impl EventHandler { } // Allow ":" / ctrl+; to enter command mode only when outside canvas. if action == "enter_command_mode" { - if app_state.ui.focus_outside_canvas + if self.is_focus_outside(router) && !self.command_mode - && !app_state.ui.show_search_palette - && !self.navigation_state.active + && !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; + self.set_focus_outside(router, true); return Ok(EventOutcome::Ok(String::new())); } } @@ -955,4 +955,52 @@ impl EventHandler { "find_file_palette_toggle" ) } + + fn set_focus_outside(&mut self, router: &mut Router, outside: bool) { + match &mut router.current { + Page::Login(state) => state.focus_outside_canvas = outside, + Page::Register(state) => state.focus_outside_canvas = outside, + Page::Intro(state) => state.focus_outside_canvas = outside, + Page::Admin(state) => state.focus_outside_canvas = outside, + Page::AddLogic(state) => state.focus_outside_canvas = outside, + Page::AddTable(state) => state.focus_outside_canvas = outside, + _ => {} + } + } + + fn set_focused_button(&mut self, router: &mut Router, index: usize) { + match &mut router.current { + Page::Login(state) => state.focused_button_index = index, + Page::Register(state) => state.focused_button_index = index, + Page::Intro(state) => state.focused_button_index = index, + Page::Admin(state) => state.focused_button_index = index, + Page::AddLogic(state) => state.focused_button_index = index, + Page::AddTable(state) => state.focused_button_index = index, + _ => {} + } + } + + fn is_focus_outside(&self, router: &Router) -> bool { + match &router.current { + Page::Login(state) => state.focus_outside_canvas, + Page::Register(state) => state.focus_outside_canvas, + Page::Intro(state) => state.focus_outside_canvas, + Page::Admin(state) => state.focus_outside_canvas, + Page::AddLogic(state) => state.focus_outside_canvas, + Page::AddTable(state) => state.focus_outside_canvas, + _ => false, + } + } + + fn focused_button(&self, router: &Router) -> usize { + match &router.current { + Page::Login(state) => state.focused_button_index, + Page::Register(state) => state.focused_button_index, + Page::Intro(state) => state.focused_button_index, + Page::Admin(state) => state.focused_button_index, + Page::AddLogic(state) => state.focused_button_index, + Page::AddTable(state) => state.focused_button_index, + _ => 0, + } + } } diff --git a/client/src/modes/handlers/mode_manager.rs b/client/src/modes/handlers/mode_manager.rs index 8fc0716..4400e7e 100644 --- a/client/src/modes/handlers/mode_manager.rs +++ b/client/src/modes/handlers/mode_manager.rs @@ -40,14 +40,11 @@ impl ModeManager { // If focus is inside a canvas, we don't duplicate canvas modes here. // Canvas crate owns ReadOnly/Edit/Highlight internally. match &router.current { - Page::Form(_) - | Page::Login(_) - | Page::Register(_) - | Page::AddTable(_) - | Page::AddLogic(_) if !app_state.ui.focus_outside_canvas => { - // Canvas active β†’ let canvas handle its own AppMode - AppMode::General - } + Page::Form(_) => AppMode::General, // Form always has its own canvas + Page::Login(state) if !state.focus_outside_canvas => AppMode::General, + Page::Register(state) if !state.focus_outside_canvas => AppMode::General, + Page::AddTable(state) if !state.focus_outside_canvas => AppMode::General, + Page::AddLogic(state) if !state.focus_outside_canvas => AppMode::General, _ => AppMode::General, } } diff --git a/client/src/pages/admin/admin/state.rs b/client/src/pages/admin/admin/state.rs index ba53960..9b76e72 100644 --- a/client/src/pages/admin/admin/state.rs +++ b/client/src/pages/admin/admin/state.rs @@ -25,6 +25,8 @@ pub struct AdminState { pub selected_profile_index: Option, pub selected_table_index: Option, pub current_focus: AdminFocus, + pub focus_outside_canvas: bool, + pub focused_button_index: usize, } impl AdminState { diff --git a/client/src/pages/admin/main/logic.rs b/client/src/pages/admin/main/logic.rs index 91bd2cb..6812c5d 100644 --- a/client/src/pages/admin/main/logic.rs +++ b/client/src/pages/admin/main/logic.rs @@ -43,15 +43,29 @@ pub fn handle_admin_navigation( ) -> bool { let action = config.get_general_action(key.code, key.modifiers).map(String::from); - let Page::Admin(admin_state) = &mut router.current else { + // Check if we're in admin page, but don't borrow mutably yet + let is_admin = matches!(&router.current, Page::Admin(_)); + if !is_admin { + return false; + } + + // Get the current focus without borrowing mutably + let current_focus = if let Page::Admin(admin_state) = &router.current { + admin_state.current_focus + } else { return false; }; - let current_focus = admin_state.current_focus; + let profile_count = app_state.profile_tree.profiles.len(); let mut handled = false; match current_focus { AdminFocus::ProfilesPane => { + // Now we can borrow mutably since we're not reassigning router.current + let Page::Admin(admin_state) = &mut router.current else { + return false; + }; + match action.as_deref() { Some("select") => { admin_state.current_focus = AdminFocus::InsideProfilesList; @@ -69,7 +83,6 @@ pub fn handle_admin_navigation( handled = true; } Some("previous_option") | Some("move_up") => { - // No wrap-around: Stay on ProfilesPane if trying to go "before" it *command_message = "At first focusable pane.".to_string(); handled = true; } @@ -78,6 +91,10 @@ pub fn handle_admin_navigation( } AdminFocus::InsideProfilesList => { + let Page::Admin(admin_state) = &mut router.current else { + return false; + }; + match action.as_deref() { Some("move_up") => { if profile_count > 0 { @@ -95,11 +112,11 @@ pub fn handle_admin_navigation( } Some("select") => { admin_state.selected_profile_index = admin_state.profile_list_state.selected(); - admin_state.selected_table_index = None; // Deselect table when profile changes + admin_state.selected_table_index = None; if let Some(profile_idx) = admin_state.selected_profile_index { if let Some(profile) = app_state.profile_tree.profiles.get(profile_idx) { if !profile.tables.is_empty() { - admin_state.table_list_state.select(Some(0)); // Auto-select first table for nav + admin_state.table_list_state.select(Some(0)); } else { admin_state.table_list_state.select(None); } @@ -123,6 +140,10 @@ pub fn handle_admin_navigation( } AdminFocus::Tables => { + let Page::Admin(admin_state) = &mut router.current else { + return false; + }; + match action.as_deref() { Some("select") => { admin_state.current_focus = AdminFocus::InsideTablesList; @@ -152,7 +173,7 @@ pub fn handle_admin_navigation( } else { *command_message = "No tables in selected profile.".to_string(); } - admin_state.current_focus = AdminFocus::Tables; // Stay in Tables pane if no tables to enter + admin_state.current_focus = AdminFocus::Tables; } handled = true; } @@ -171,6 +192,10 @@ pub fn handle_admin_navigation( } AdminFocus::InsideTablesList => { + let Page::Admin(admin_state) = &mut router.current else { + return false; + }; + match action.as_deref() { Some("move_up") => { let current_profile_idx = admin_state.selected_profile_index @@ -210,7 +235,7 @@ pub fn handle_admin_navigation( handled = true; } } - Some("select") => { // This is for persistently selecting a table with [*] + Some("select") => { admin_state.selected_table_index = admin_state.table_list_state.selected(); let table_name = admin_state.selected_profile_index .and_then(|p_idx| app_state.profile_tree.profiles.get(p_idx)) @@ -230,10 +255,17 @@ pub fn handle_admin_navigation( AdminFocus::Button1 => { // Add Logic Button match action.as_deref() { - Some("select") => { // Typically "Enter" key - if let Some(p_idx) = admin_state.selected_profile_index { + Some("select") => { + // Extract needed data first, before any router reassignment + let (selected_profile_idx, selected_table_idx) = if let Page::Admin(admin_state) = &router.current { + (admin_state.selected_profile_index, admin_state.selected_table_index) + } else { + return false; + }; + + if let Some(p_idx) = selected_profile_idx { if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) { - if let Some(t_idx) = admin_state.selected_table_index { + if let Some(t_idx) = selected_table_idx { if let Some(table) = profile.tables.get(t_idx) { // Create AddLogic page with selected profile & table let add_logic_form = AddLogicFormState::new_with_table( @@ -243,16 +275,16 @@ pub fn handle_admin_navigation( table.name.clone(), ); - // Route to AddLogic - router.current = Page::AddLogic(add_logic_form); // Store table info for later fetching app_state.pending_table_structure_fetch = Some(( profile.name.clone(), table.name.clone(), )); - + + // Now it's safe to reassign router.current + router.current = Page::AddLogic(add_logic_form); buffer_state.update_history(AppView::AddLogic); - app_state.ui.focus_outside_canvas = false; + *command_message = format!( "Opening Add Logic for table '{}' in profile '{}'...", table.name, profile.name @@ -272,11 +304,17 @@ pub fn handle_admin_navigation( handled = true; } Some("previous_option") | Some("move_up") => { + let Page::Admin(admin_state) = &mut router.current else { + return false; + }; admin_state.current_focus = AdminFocus::Tables; *command_message = "Focus: Tables Pane".to_string(); handled = true; } Some("next_option") | Some("move_down") => { + let Page::Admin(admin_state) = &mut router.current else { + return false; + }; admin_state.current_focus = AdminFocus::Button2; *command_message = "Focus: Add Table Button".to_string(); handled = true; @@ -288,23 +326,32 @@ pub fn handle_admin_navigation( AdminFocus::Button2 => { // Add Table Button match action.as_deref() { Some("select") => { - if let Some(p_idx) = admin_state.selected_profile_index { + // Extract needed data first + let selected_profile_idx = if let Page::Admin(admin_state) = &router.current { + admin_state.selected_profile_index + } else { + return false; + }; + + if let Some(p_idx) = selected_profile_idx { if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) { let selected_profile_name = profile.name.clone(); // Prepare links from the selected profile's existing tables let available_links: Vec = profile.tables.iter() .map(|table| LinkDefinition { linked_table_name: table.name.clone(), - is_required: false, // Default, can be changed in AddTable screen + is_required: false, selected: false, }).collect(); // Build decoupled AddTable page and route into it let mut page = AddTableFormState::new(selected_profile_name.clone()); page.state.links = available_links; + + // Now safe to reassign router.current router.current = Page::AddTable(page); buffer_state.update_history(AppView::AddTable); - app_state.ui.focus_outside_canvas = false; + *command_message = format!( "Opening Add Table for profile '{}'...", selected_profile_name @@ -320,11 +367,17 @@ pub fn handle_admin_navigation( } } Some("previous_option") | Some("move_up") => { + let Page::Admin(admin_state) = &mut router.current else { + return false; + }; admin_state.current_focus = AdminFocus::Button1; *command_message = "Focus: Add Logic Button".to_string(); handled = true; } Some("next_option") | Some("move_down") => { + let Page::Admin(admin_state) = &mut router.current else { + return false; + }; admin_state.current_focus = AdminFocus::Button3; *command_message = "Focus: Change Table Button".to_string(); handled = true; @@ -336,17 +389,18 @@ pub fn handle_admin_navigation( AdminFocus::Button3 => { // Change Table Button match action.as_deref() { Some("select") => { - // Future: Logic to load selected table into AddTableState for editing *command_message = "Action: Change Table (Not Implemented)".to_string(); handled = true; } Some("previous_option") | Some("move_up") => { + let Page::Admin(admin_state) = &mut router.current else { + return false; + }; admin_state.current_focus = AdminFocus::Button2; *command_message = "Focus: Add Table Button".to_string(); handled = true; } Some("next_option") | Some("move_down") => { - // No wrap-around: Stay on Button3 if trying to go "after" it *command_message = "At last focusable button.".to_string(); handled = true; } diff --git a/client/src/pages/admin_panel/add_logic/event.rs b/client/src/pages/admin_panel/add_logic/event.rs index 37f8782..188e0e6 100644 --- a/client/src/pages/admin_panel/add_logic/event.rs +++ b/client/src/pages/admin_panel/add_logic/event.rs @@ -39,7 +39,7 @@ pub fn handle_add_logic_event( match key_event.code { crossterm::event::KeyCode::Esc => { add_logic_page.state.current_focus = AddLogicFocus::ScriptContentPreview; - app_state.ui.focus_outside_canvas = true; + add_logic_page.focus_outside_canvas = true; return Ok(EventOutcome::Ok("Exited script editing.".to_string())); } _ => { @@ -85,7 +85,7 @@ pub fn handle_add_logic_event( 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; + add_logic_page.focus_outside_canvas = true; return Ok(EventOutcome::Ok("Moved to Script Preview".to_string())); } } @@ -114,7 +114,7 @@ pub fn handle_add_logic_event( let mut current = add_logic_page.state.current_focus; if move_focus(&ADD_LOGIC_FOCUS_ORDER, &mut current, ma) { add_logic_page.state.current_focus = current; - app_state.ui.focus_outside_canvas = !matches!( + add_logic_page.focus_outside_canvas = !matches!( add_logic_page.state.current_focus, AddLogicFocus::InputLogicName | AddLogicFocus::InputTargetColumn @@ -127,7 +127,7 @@ pub fn handle_add_logic_event( MovementAction::Select => match add_logic_page.state.current_focus { AddLogicFocus::ScriptContentPreview => { add_logic_page.state.current_focus = AddLogicFocus::InsideScriptContent; - app_state.ui.focus_outside_canvas = false; + add_logic_page.focus_outside_canvas = false; return Ok(EventOutcome::Ok( "Fullscreen script editing. Esc to exit.".to_string(), )); @@ -147,7 +147,7 @@ pub fn handle_add_logic_event( MovementAction::Esc => { if add_logic_page.state.current_focus == AddLogicFocus::ScriptContentPreview { add_logic_page.state.current_focus = AddLogicFocus::InputDescription; - app_state.ui.focus_outside_canvas = false; + add_logic_page.focus_outside_canvas = false; return Ok(EventOutcome::Ok("Back to Description".to_string())); } } diff --git a/client/src/pages/admin_panel/add_logic/state.rs b/client/src/pages/admin_panel/add_logic/state.rs index c71ffce..eb1bbd7 100644 --- a/client/src/pages/admin_panel/add_logic/state.rs +++ b/client/src/pages/admin_panel/add_logic/state.rs @@ -335,6 +335,7 @@ pub struct AddLogicFormState { pub state: AddLogicState, pub editor: FormEditor, pub focus_outside_canvas: bool, + pub focused_button_index: usize, } // manual Debug because FormEditor may not implement Debug @@ -343,6 +344,7 @@ impl std::fmt::Debug for AddLogicFormState { f.debug_struct("AddLogicFormState") .field("state", &self.state) .field("focus_outside_canvas", &self.focus_outside_canvas) + .field("focused_button_index", &self.focused_button_index) .finish() } } @@ -355,6 +357,7 @@ impl AddLogicFormState { state, editor, focus_outside_canvas: false, + focused_button_index: 0, } } @@ -373,6 +376,7 @@ impl AddLogicFormState { state, editor, focus_outside_canvas: false, + focused_button_index: 0, } } @@ -382,6 +386,7 @@ impl AddLogicFormState { state, editor, focus_outside_canvas: false, + focused_button_index: 0, } } diff --git a/client/src/pages/admin_panel/add_logic/ui.rs b/client/src/pages/admin_panel/add_logic/ui.rs index 909b001..a3ae76b 100644 --- a/client/src/pages/admin_panel/add_logic/ui.rs +++ b/client/src/pages/admin_panel/add_logic/ui.rs @@ -252,11 +252,14 @@ pub fn render_add_logic( ]) .split(buttons_area); + let save_active = add_logic_state.focus_outside_canvas + && add_logic_state.focused_button_index == 0; let save_button = Paragraph::new(" Save Logic ") - .style(get_button_style( - AddLogicFocus::SaveButton, - add_logic_state.current_focus(), - )) + .style(if save_active { + Style::default().fg(theme.highlight).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(theme.secondary) + }) .alignment(Alignment::Center) .block( Block::default() @@ -269,11 +272,14 @@ pub fn render_add_logic( ); f.render_widget(save_button, button_chunks[0]); + let cancel_active = add_logic_state.focus_outside_canvas + && add_logic_state.focused_button_index == 1; let cancel_button = Paragraph::new(" Cancel ") - .style(get_button_style( - AddLogicFocus::CancelButton, - add_logic_state.current_focus(), - )) + .style(if cancel_active { + Style::default().fg(theme.highlight).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(theme.secondary) + }) .alignment(Alignment::Center) .block( Block::default() diff --git a/client/src/pages/admin_panel/add_table/event.rs b/client/src/pages/admin_panel/add_table/event.rs index 6ca3848..ae5c8ab 100644 --- a/client/src/pages/admin_panel/add_table/event.rs +++ b/client/src/pages/admin_panel/add_table/event.rs @@ -51,7 +51,7 @@ pub fn handle_add_table_event( if inside_canvas_inputs { // Disable global shortcuts while typing - app_state.ui.focus_outside_canvas = false; + page.focus_outside_canvas = false; // Only allow leaving the canvas with Down/Next when in ReadOnly mode let in_edit_mode = page.editor.mode() == CanvasMode::Edit; @@ -62,7 +62,7 @@ pub fn handle_add_table_event( if at_last && matches!(ma, MovementAction::Down | MovementAction::Next) { page.state.last_canvas_field = last_idx; page.set_current_focus(AddTableFocus::AddColumnButton); - app_state.ui.focus_outside_canvas = true; + page.focus_outside_canvas = true; return Ok(EventOutcome::Ok("Moved to Add button".to_string())); } } @@ -100,7 +100,7 @@ pub fn handle_add_table_event( } else if !page.state.columns.is_empty() { page.state.column_table_state.select(Some(0)); } - app_state.ui.focus_outside_canvas = true; + page.focus_outside_canvas = true; return Ok(EventOutcome::Ok(String::new())); } MovementAction::Down => { @@ -111,7 +111,7 @@ pub fn handle_add_table_event( } else if !page.state.columns.is_empty() { page.state.column_table_state.select(Some(0)); } - app_state.ui.focus_outside_canvas = true; + page.focus_outside_canvas = true; return Ok(EventOutcome::Ok(String::new())); } MovementAction::Select => { @@ -121,13 +121,13 @@ pub fn handle_add_table_event( page.state.has_unsaved_changes = true; } } - app_state.ui.focus_outside_canvas = true; + page.focus_outside_canvas = true; return Ok(EventOutcome::Ok(String::new())); } MovementAction::Esc => { page.state.column_table_state.select(None); page.set_current_focus(AddTableFocus::ColumnsTable); - app_state.ui.focus_outside_canvas = true; + page.focus_outside_canvas = true; return Ok(EventOutcome::Ok(String::new())); } MovementAction::Next | MovementAction::Previous => { @@ -146,7 +146,7 @@ pub fn handle_add_table_event( } else if !page.state.indexes.is_empty() { page.state.index_table_state.select(Some(0)); } - app_state.ui.focus_outside_canvas = true; + page.focus_outside_canvas = true; return Ok(EventOutcome::Ok(String::new())); } MovementAction::Down => { @@ -157,7 +157,7 @@ pub fn handle_add_table_event( } else if !page.state.indexes.is_empty() { page.state.index_table_state.select(Some(0)); } - app_state.ui.focus_outside_canvas = true; + page.focus_outside_canvas = true; return Ok(EventOutcome::Ok(String::new())); } MovementAction::Select => { @@ -167,13 +167,13 @@ pub fn handle_add_table_event( page.state.has_unsaved_changes = true; } } - app_state.ui.focus_outside_canvas = true; + page.focus_outside_canvas = true; return Ok(EventOutcome::Ok(String::new())); } MovementAction::Esc => { page.state.index_table_state.select(None); page.set_current_focus(AddTableFocus::IndexesTable); - app_state.ui.focus_outside_canvas = true; + page.focus_outside_canvas = true; return Ok(EventOutcome::Ok(String::new())); } MovementAction::Next | MovementAction::Previous => { @@ -191,7 +191,7 @@ pub fn handle_add_table_event( } else if !page.state.links.is_empty() { page.state.link_table_state.select(Some(0)); } - app_state.ui.focus_outside_canvas = true; + page.focus_outside_canvas = true; return Ok(EventOutcome::Ok(String::new())); } MovementAction::Down => { @@ -202,7 +202,7 @@ pub fn handle_add_table_event( } else if !page.state.links.is_empty() { page.state.link_table_state.select(Some(0)); } - app_state.ui.focus_outside_canvas = true; + page.focus_outside_canvas = true; return Ok(EventOutcome::Ok(String::new())); } MovementAction::Select => { @@ -212,13 +212,13 @@ pub fn handle_add_table_event( page.state.has_unsaved_changes = true; } } - app_state.ui.focus_outside_canvas = true; + page.focus_outside_canvas = true; return Ok(EventOutcome::Ok(String::new())); } MovementAction::Esc => { page.state.link_table_state.select(None); page.set_current_focus(AddTableFocus::LinksTable); - app_state.ui.focus_outside_canvas = true; + page.focus_outside_canvas = true; return Ok(EventOutcome::Ok(String::new())); } MovementAction::Next | MovementAction::Previous => { @@ -233,7 +233,7 @@ pub fn handle_add_table_event( let mut current = page.current_focus(); if move_focus(&ADD_TABLE_FOCUS_ORDER, &mut current, ma) { page.set_current_focus(current); - app_state.ui.focus_outside_canvas = !matches!( + page.focus_outside_canvas = !matches!( page.current_focus(), AddTableFocus::InputTableName | AddTableFocus::InputColumnName diff --git a/client/src/pages/admin_panel/add_table/state.rs b/client/src/pages/admin_panel/add_table/state.rs index b98a80d..842e390 100644 --- a/client/src/pages/admin_panel/add_table/state.rs +++ b/client/src/pages/admin_panel/add_table/state.rs @@ -251,6 +251,7 @@ pub struct AddTableFormState { pub state: AddTableState, pub editor: FormEditor, pub focus_outside_canvas: bool, + pub focused_button_index: usize, } impl std::fmt::Debug for AddTableFormState { @@ -258,6 +259,7 @@ impl std::fmt::Debug for AddTableFormState { f.debug_struct("AddTableFormState") .field("state", &self.state) .field("focus_outside_canvas", &self.focus_outside_canvas) + .field("focused_button_index", &self.focused_button_index) .finish() } } @@ -271,6 +273,7 @@ impl AddTableFormState { state, editor, focus_outside_canvas: false, + focused_button_index: 0, } } @@ -280,6 +283,7 @@ impl AddTableFormState { state, editor, focus_outside_canvas: false, + focused_button_index: 0, } } @@ -319,4 +323,10 @@ impl AddTableFormState { pub fn link_table_state(&mut self) -> &mut TableState { &mut self.state.link_table_state } + pub fn set_focused_button(&mut self, index: usize) { + self.focused_button_index = index; + } + pub fn focused_button(&self) -> usize { + self.focused_button_index + } } diff --git a/client/src/pages/admin_panel/add_table/ui.rs b/client/src/pages/admin_panel/add_table/ui.rs index d944f5f..4f79996 100644 --- a/client/src/pages/admin_panel/add_table/ui.rs +++ b/client/src/pages/admin_panel/add_table/ui.rs @@ -489,11 +489,14 @@ pub fn render_add_table( ]) .split(bottom_buttons_area); + let save_active = add_table_state.focus_outside_canvas + && add_table_state.focused_button_index == 0; let save_button = Paragraph::new(" Save table ") - .style(get_button_style( - AddTableFocus::SaveButton, - add_table_state.current_focus(), - )) + .style(if save_active { + Style::default().fg(theme.highlight).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(theme.secondary) + }) .alignment(Alignment::Center) .block( Block::default() @@ -506,37 +509,37 @@ pub fn render_add_table( ); f.render_widget(save_button, bottom_button_chunks[0]); + let delete_active = add_table_state.focus_outside_canvas + && add_table_state.focused_button_index == 1; let delete_button = Paragraph::new(" Delete Selected ") - .style(get_button_style( - AddTableFocus::DeleteSelectedButton, - add_table_state.current_focus(), - )) - .alignment(Alignment::Center) + .style(if delete_active { + Style::default().fg(theme.highlight).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(theme.secondary) + }) + .alignment(Alignment::Center) .block( Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(get_button_border_style( - add_table_state.current_focus() == AddTableFocus::DeleteSelectedButton, // Pass bool - theme, - )), + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(get_button_border_style(delete_active, theme)), ); f.render_widget(delete_button, bottom_button_chunks[1]); + let cancel_active = add_table_state.focus_outside_canvas + && add_table_state.focused_button_index == 2; let cancel_button = Paragraph::new(" Cancel ") - .style(get_button_style( - AddTableFocus::CancelButton, - add_table_state.current_focus(), - )) - .alignment(Alignment::Center) + .style(if cancel_active { + Style::default().fg(theme.highlight).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(theme.secondary) + }) + .alignment(Alignment::Center) .block( Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(get_button_border_style( - add_table_state.current_focus() == AddTableFocus::CancelButton, // Pass bool - theme, - )), + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(get_button_border_style(cancel_active, theme)), ); f.render_widget(cancel_button, bottom_button_chunks[2]); diff --git a/client/src/pages/intro/logic.rs b/client/src/pages/intro/logic.rs index e22982f..50594d9 100644 --- a/client/src/pages/intro/logic.rs +++ b/client/src/pages/intro/logic.rs @@ -67,9 +67,6 @@ pub fn handle_intro_selection( } 3 => { buffer_state.update_history(AppView::Register); - // Register view requires focus reset - app_state.ui.focus_outside_canvas = false; - app_state.focused_button_index = 0; } _ => return, } diff --git a/client/src/pages/intro/state.rs b/client/src/pages/intro/state.rs index 2f12343..fd76708 100644 --- a/client/src/pages/intro/state.rs +++ b/client/src/pages/intro/state.rs @@ -3,23 +3,27 @@ use crate::movement::MovementAction; #[derive(Default, Clone, Debug)] pub struct IntroState { - pub selected_option: usize, + pub focus_outside_canvas: bool, + pub focused_button_index: usize, } impl IntroState { pub fn new() -> Self { - Self::default() + Self { + focus_outside_canvas: true, + focused_button_index: 0, + } } pub fn next_option(&mut self) { - if self.selected_option < 3 { - self.selected_option += 1; + if self.focused_button_index < 3 { + self.focused_button_index += 1; } } pub fn previous_option(&mut self) { - if self.selected_option > 0 { - self.selected_option -= 1 + if self.focused_button_index > 0 { + self.focused_button_index -= 1; } } } diff --git a/client/src/pages/intro/ui.rs b/client/src/pages/intro/ui.rs index 4ada9ea..c9ac9fe 100644 --- a/client/src/pages/intro/ui.rs +++ b/client/src/pages/intro/ui.rs @@ -56,7 +56,8 @@ pub fn render_intro(f: &mut Frame, intro_state: &IntroState, area: Rect, theme: let buttons = ["Continue", "Admin", "Login", "Register"]; for (i, &text) in buttons.iter().enumerate() { - render_button(f, button_area[i], text, intro_state.selected_option == i, theme); + let active = intro_state.focus_outside_canvas && intro_state.focused_button_index == i; + render_button(f, button_area[i], text, active, theme); } } diff --git a/client/src/pages/login/event.rs b/client/src/pages/login/event.rs index 352348e..fa67e85 100644 --- a/client/src/pages/login/event.rs +++ b/client/src/pages/login/event.rs @@ -26,7 +26,6 @@ pub fn handle_login_event( && modifiers.is_empty() { login_page.focus_outside_canvas = false; - app_state.ui.focus_outside_canvas = false; // πŸ”‘ keep global in sync login_page.editor.set_mode(CanvasMode::ReadOnly); return Ok(EventOutcome::Ok(String::new())); } @@ -43,9 +42,7 @@ pub fn handle_login_event( ) { login_page.focus_outside_canvas = true; - login_page.focused_button_index = 0; // focus "Login" button - app_state.ui.focus_outside_canvas = true; - app_state.focused_button_index = 0; + login_page.focused_button_index = 0; login_page.editor.set_mode(CanvasMode::ReadOnly); return Ok(EventOutcome::Ok("Focus moved to buttons".into())); } diff --git a/client/src/pages/login/logic.rs b/client/src/pages/login/logic.rs index d424df4..1eabffe 100644 --- a/client/src/pages/login/logic.rs +++ b/client/src/pages/login/logic.rs @@ -125,9 +125,6 @@ pub async fn back_to_main( buffer_state.close_active_buffer(); buffer_state.update_history(AppView::Intro); - app_state.ui.focus_outside_canvas = false; - app_state.focused_button_index = 0; - "Returned to main menu".to_string() } diff --git a/client/src/pages/login/ui.rs b/client/src/pages/login/ui.rs index e5a3110..69483c4 100644 --- a/client/src/pages/login/ui.rs +++ b/client/src/pages/login/ui.rs @@ -80,11 +80,8 @@ pub fn render_login( // Login Button let login_button_index = 0; - let login_active = if login_page.focus_outside_canvas { - app_state.focused_button_index == login_button_index - } else { - false - }; + let login_active = login_page.focus_outside_canvas + && login_page.focused_button_index == login_button_index; let mut login_style = Style::default().fg(theme.fg); let mut login_border = Style::default().fg(theme.border); if login_active { @@ -107,11 +104,8 @@ pub fn render_login( // Return Button let return_button_index = 1; - let return_active = if app_state.ui.focus_outside_canvas { - app_state.focused_button_index == return_button_index - } else { - false - }; + let return_active = login_page.focus_outside_canvas + && login_page.focused_button_index == return_button_index; let mut return_style = Style::default().fg(theme.fg); let mut return_border = Style::default().fg(theme.border); if return_active { diff --git a/client/src/pages/register/event.rs b/client/src/pages/register/event.rs index a63d0e8..21b210a 100644 --- a/client/src/pages/register/event.rs +++ b/client/src/pages/register/event.rs @@ -28,8 +28,6 @@ pub fn handle_register_event( && modifiers.is_empty() { register_page.focus_outside_canvas = false; - // Keep global in sync for now (cursor styling elsewhere still reads it) - app_state.ui.focus_outside_canvas = false; register_page.editor.set_mode(CanvasMode::ReadOnly); return Ok(EventOutcome::Ok(String::new())); } @@ -47,9 +45,6 @@ pub fn handle_register_event( { register_page.focus_outside_canvas = true; register_page.focused_button_index = 0; // focus "Register" button - // Keep global in sync for now - app_state.ui.focus_outside_canvas = true; - app_state.focused_button_index = 0; register_page.editor.set_mode(CanvasMode::ReadOnly); return Ok(EventOutcome::Ok("Focus moved to buttons".into())); } diff --git a/client/src/pages/register/logic.rs b/client/src/pages/register/logic.rs index 6c216ac..2ad9743 100644 --- a/client/src/pages/register/logic.rs +++ b/client/src/pages/register/logic.rs @@ -56,8 +56,6 @@ pub async fn back_to_login( // Reset focus state register_state.focus_outside_canvas = false; register_state.focused_button_index = 0; - app_state.ui.focus_outside_canvas = false; - app_state.focused_button_index = 0; "Returned to main menu".to_string() } diff --git a/client/src/search/event.rs b/client/src/search/event.rs index 24a0454..8de2b1a 100644 --- a/client/src/search/event.rs +++ b/client/src/search/event.rs @@ -106,7 +106,6 @@ pub async fn handle_search_palette_event( if should_close { app_state.search_state = None; app_state.ui.show_search_palette = false; - app_state.ui.focus_outside_canvas = false; } Ok(outcome_message) diff --git a/client/src/state/app/state.rs b/client/src/state/app/state.rs index 8cb8440..cff0513 100644 --- a/client/src/state/app/state.rs +++ b/client/src/state/app/state.rs @@ -28,7 +28,6 @@ pub struct UiState { pub show_login: bool, pub show_register: bool, pub show_search_palette: bool, - pub focus_outside_canvas: bool, pub dialog: DialogState, } @@ -52,7 +51,6 @@ pub struct AppState { // NEW: The "Rulebook" cache. We use Arc for efficient sharing. pub schema_cache: HashMap>, - pub focused_button_index: usize, pub pending_table_structure_fetch: Option<(String, String)>, pub search_state: Option, @@ -77,7 +75,6 @@ impl AppState { current_view_table_name: None, current_mode: AppMode::General, schema_cache: HashMap::new(), // NEW: Initialize the cache - focused_button_index: 0, pending_table_structure_fetch: None, search_state: None, ui: UiState::default(), @@ -166,8 +163,7 @@ impl Default for UiState { show_login: false, show_register: false, show_buffer_list: true, - show_search_palette: false, // ADDED - focus_outside_canvas: false, + show_search_palette: false, dialog: DialogState::default(), } } diff --git a/client/src/tui/functions/common/logout.rs b/client/src/tui/functions/common/logout.rs index 1dc3bda..ec9cbb1 100644 --- a/client/src/tui/functions/common/logout.rs +++ b/client/src/tui/functions/common/logout.rs @@ -16,32 +16,27 @@ pub fn logout( auth_state.user_id = None; auth_state.role = None; auth_state.decoded_username = None; - + // Delete stored auth data if let Err(e) = delete_auth_data() { error!("Failed to delete stored auth data: {}", e); - // Continue anyway - user is logged out in memory } - + // Navigate to intro screen buffer_state.history = vec![AppView::Intro]; buffer_state.active_index = 0; - - // Reset UI state - app_state.ui.focus_outside_canvas = false; - app_state.focused_button_index = 0; - + // Hide any open dialogs app_state.hide_dialog(); - + // Show logout confirmation dialog app_state.show_dialog( "Logged Out", "You have been successfully logged out.", vec!["OK".to_string()], - DialogPurpose::LoginSuccess, // Reuse or create a new purpose + DialogPurpose::LoginSuccess, ); - + info!("User logged out successfully."); "Logged out successfully".to_string() } diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index 5f19473..3fb273b 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -227,8 +227,17 @@ pub async fn run_ui() -> Result<()> { || app_state.ui.show_search_palette || event_handler.navigation_state.active; if !overlay_active { - if let Page::Form(path) = &router.current { - if !app_state.ui.focus_outside_canvas { + let inside_canvas = match &router.current { + Page::Form(_) => true, + Page::Login(state) => !state.focus_outside_canvas, + Page::Register(state) => !state.focus_outside_canvas, + Page::AddTable(state) => !state.focus_outside_canvas, + Page::AddLogic(state) => !state.focus_outside_canvas, + _ => false, + }; + + if inside_canvas { + if let Page::Form(path) = &router.current { if let Some(editor) = app_state.editor_for_path(path) { match editor.handle_key_event(*key_event) { KeyEventOutcome::Consumed(Some(msg)) => { @@ -633,7 +642,15 @@ pub async fn run_ui() -> Result<()> { match current_mode { AppMode::General => { - if app_state.ui.focus_outside_canvas { + let outside_canvas = match &router.current { + Page::Login(state) => state.focus_outside_canvas, + Page::Register(state) => state.focus_outside_canvas, + Page::AddTable(state) => state.focus_outside_canvas, + Page::AddLogic(state) => state.focus_outside_canvas, + _ => false, // Form and Admin don’t use this flag + }; + + if outside_canvas { // Outside canvas β†’ app decides terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor()?;