login and register are now havving own handlers and loaders, moving logic out of event.rs and ui.rs

This commit is contained in:
filipriec
2025-08-30 19:13:12 +02:00
parent 46149c09db
commit 43f5c1a764
13 changed files with 317 additions and 104 deletions

View File

@@ -305,94 +305,33 @@ impl EventHandler {
if !overlay_active { if !overlay_active {
if let Page::Login(login_page) = &mut router.current { if let Page::Login(login_page) = &mut router.current {
use crossterm::event::{KeyCode, KeyModifiers}; let outcome =
login::event::handle_login_event(event, app_state, login_page)?;
// Inside canvas: at the last field, 'j' or Down moves focus to buttons // Only return if the login page actually consumed the key
if !app_state.ui.focus_outside_canvas { if !outcome.get_message_if_ok().is_empty() {
let last_idx = login_page return Ok(outcome);
.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()));
}
}
// 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.)
}
}
} }
} else if let Page::Register(register_page) = &mut router.current { } else if let Page::Register(register_page) = &mut router.current {
use crossterm::event::{KeyCode, KeyModifiers}; let outcome = crate::pages::register::event::handle_register_event(
event,
// Inside canvas: at the last field, 'j' or Down moves focus to buttons app_state,
if !app_state.ui.focus_outside_canvas { register_page,
let last_idx = register_page.editor.data_provider().field_count().saturating_sub(1); )?;
let at_last = register_page.editor.current_field() >= last_idx; // Only stop if page actually consumed the key; else fall through to global handling
if at_last if !outcome.get_message_if_ok().is_empty() {
&& matches!( return Ok(outcome);
(key_code, modifiers),
(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _)
)
{
app_state.ui.focus_outside_canvas = true;
app_state.focused_button_index = 0; // focus "Register" button
register_page.editor.set_mode(CanvasMode::ReadOnly);
return Ok(EventOutcome::Ok("Focus moved to buttons".to_string()));
}
}
// Only forward to the canvas while focus is inside it
if !app_state.ui.focus_outside_canvas {
match register_page.handle_key_event(key_event) {
KeyEventOutcome::Consumed(Some(msg)) => {
self.command_message = msg;
return Ok(EventOutcome::Ok("Register input updated".to_string()));
}
KeyEventOutcome::Consumed(None) => {
return Ok(EventOutcome::Ok("Register input updated".to_string()));
}
KeyEventOutcome::Pending => {
return Ok(EventOutcome::Ok("Waiting for next key...".to_string()));
}
KeyEventOutcome::NotMatched => {
// fall through
}
}
} }
} else if let Page::Form(path) = &router.current { } else if let Page::Form(path) = &router.current {
// NEW: Delegate Form event handling let outcome = forms::event::handle_form_event(
return forms::event::handle_form_event( event,
event, app_state,
app_state, path,
path, &mut self.ideal_cursor_column,
&mut self.ideal_cursor_column )?;
); // Only return if the form page actually consumed the key
if !outcome.get_message_if_ok().is_empty() {
return Ok(outcome);
}
} }
} }
if toggle_sidebar( if toggle_sidebar(
@@ -508,19 +447,6 @@ impl EventHandler {
match &mut router.current { match &mut router.current {
// LOGIN: From buttons (general) back into the canvas with 'k' (Up), // LOGIN: From buttons (general) back into the canvas with 'k' (Up),
// but ONLY from the left-most "Login" button. // 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) => { Page::AddTable(state) => {
if state.handle_movement(ma) { if state.handle_movement(ma) {
// Keep UI focus consistent with inputs vs. outer elements // Keep UI focus consistent with inputs vs. outer elements

View File

@@ -0,0 +1,60 @@
// src/pages/admin/admin/event.rs
use anyhow::Result;
use crossterm::event::KeyEvent;
use crate::buffer::state::BufferState;
use crate::config::binds::config::Config;
use crate::pages::admin::AdminState;
use crate::pages::admin::main::logic::handle_admin_navigation;
use crate::state::app::state::AppState;
/// Handle all Admin page-specific key events (movement + actions).
/// Returns true if the key was handled (so the caller should stop propagation).
pub fn handle_admin_event(
key_event: KeyEvent,
config: &Config,
app_state: &mut AppState,
admin_state: &mut AdminState,
buffer_state: &mut BufferState,
command_message: &mut String,
) -> Result<bool> {
// 1) Map general action to MovementAction (same mapping used in event.rs)
let movement_action = if let Some(act) =
config.get_general_action(key_event.code, key_event.modifiers)
{
use crate::movement::MovementAction;
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
};
if let Some(ma) = movement_action {
if admin_state.handle_movement(app_state, ma) {
return Ok(true);
}
}
// 2) Rich Admin navigation (buttons, selections, etc.)
if handle_admin_navigation(
key_event,
config,
app_state,
admin_state,
buffer_state,
command_message,
) {
return Ok(true);
}
Ok(false)
}

View File

@@ -0,0 +1,54 @@
// src/pages/admin/admin/loader.rs
use anyhow::{Context, Result};
use crate::pages::admin::{AdminFocus, AdminState};
use crate::services::grpc_client::GrpcClient;
use crate::state::app::state::AppState;
/// Refresh admin data and ensure focus and selections are valid.
pub async fn refresh_admin_state(
grpc_client: &mut GrpcClient,
app_state: &mut AppState,
admin_state: &mut AdminState,
) -> Result<()> {
// Fetch latest profile tree
let refreshed_tree = grpc_client
.get_profile_tree()
.await
.context("Failed to refresh profile tree for Admin panel")?;
app_state.profile_tree = refreshed_tree;
// Populate profile names for AdminState's list
let profile_names = app_state
.profile_tree
.profiles
.iter()
.map(|p| p.name.clone())
.collect::<Vec<_>>();
admin_state.set_profiles(profile_names);
// Ensure a sane focus
if admin_state.current_focus == AdminFocus::default()
|| !matches!(
admin_state.current_focus,
AdminFocus::InsideProfilesList
| AdminFocus::Tables
| AdminFocus::InsideTablesList
| AdminFocus::Button1
| AdminFocus::Button2
| AdminFocus::Button3
| AdminFocus::ProfilesPane
)
{
admin_state.current_focus = AdminFocus::ProfilesPane;
}
// Ensure a selection exists when profiles are present
if admin_state.profile_list_state.selected().is_none()
&& !app_state.profile_tree.profiles.is_empty()
{
admin_state.profile_list_state.select(Some(0));
}
Ok(())
}

View File

@@ -3,5 +3,7 @@
pub mod state; pub mod state;
pub mod ui; pub mod ui;
pub mod tui; pub mod tui;
pub mod event;
pub mod loader;
pub use state::{AdminState, AdminFocus}; pub use state::{AdminState, AdminFocus};

View File

@@ -0,0 +1,73 @@
// src/pages/login/event.rs
use anyhow::Result;
use crossterm::event::{Event, KeyCode, KeyModifiers};
use canvas::{keymap::KeyEventOutcome, AppMode as CanvasMode};
use crate::{
state::app::state::AppState,
pages::login::LoginFormState,
modes::handlers::event::EventOutcome,
};
use canvas::DataProvider;
/// Handles all Login page-specific events
pub fn handle_login_event(
event: Event,
app_state: &mut AppState,
login_page: &mut LoginFormState,
) -> Result<EventOutcome> {
if let Event::Key(key_event) = event {
let key_code = key_event.code;
let modifiers = key_event.modifiers;
// From buttons (outside) back into the canvas (ReadOnly) with Up/k from the left-most button
if login_page.focus_outside_canvas
&& login_page.focused_button_index == 0
&& matches!(key_code, KeyCode::Up | KeyCode::Char('k'))
&& 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()));
}
// Focus handoff: inside canvas → buttons
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
&& matches!(
(key_code, modifiers),
(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _)
)
{
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.editor.set_mode(CanvasMode::ReadOnly);
return Ok(EventOutcome::Ok("Focus moved to buttons".into()));
}
}
// Forward to canvas if focus is inside
if !login_page.focus_outside_canvas {
match login_page.handle_key_event(key_event) {
KeyEventOutcome::Consumed(Some(msg)) => {
return Ok(EventOutcome::Ok(msg));
}
KeyEventOutcome::Consumed(None) => {
return Ok(EventOutcome::Ok("Login input updated".into()));
}
KeyEventOutcome::Pending => {
return Ok(EventOutcome::Ok("Waiting for next key...".into()));
}
KeyEventOutcome::NotMatched => {
// fall through to button handling
}
}
}
}
Ok(EventOutcome::Ok(String::new()))
}

View File

@@ -3,7 +3,9 @@
pub mod state; pub mod state;
pub mod ui; pub mod ui;
pub mod logic; pub mod logic;
pub mod event;
pub use state::*; pub use state::*;
pub use ui::render_login; pub use ui::render_login;
pub use logic::*; pub use logic::*;
pub use event::*;

View File

@@ -127,6 +127,8 @@ impl DataProvider for LoginState {
pub struct LoginFormState { pub struct LoginFormState {
pub state: LoginState, pub state: LoginState,
pub editor: FormEditor<LoginState>, pub editor: FormEditor<LoginState>,
pub focus_outside_canvas: bool,
pub focused_button_index: usize,
} }
// manual debug because FormEditor doesnt implement debug // manual debug because FormEditor doesnt implement debug
@@ -150,7 +152,12 @@ impl LoginFormState {
pub fn new() -> Self { pub fn new() -> Self {
let state = LoginState::default(); let state = LoginState::default();
let editor = FormEditor::new(state.clone()); let editor = FormEditor::new(state.clone());
Self { state, editor } Self {
state,
editor,
focus_outside_canvas: false,
focused_button_index: 0,
}
} }
// === Delegates to LoginState fields === // === Delegates to LoginState fields ===

View File

@@ -80,7 +80,7 @@ pub fn render_login(
// Login Button // Login Button
let login_button_index = 0; let login_button_index = 0;
let login_active = if app_state.ui.focus_outside_canvas { let login_active = if login_page.focus_outside_canvas {
app_state.focused_button_index == login_button_index app_state.focused_button_index == login_button_index
} else { } else {
false false

View File

@@ -0,0 +1,76 @@
// src/pages/register/event.rs
use anyhow::Result;
use crossterm::event::{Event, KeyCode, KeyModifiers};
use canvas::{keymap::KeyEventOutcome, AppMode as CanvasMode};
use canvas::DataProvider;
use crate::{
state::app::state::AppState,
pages::register::RegisterFormState,
modes::handlers::event::EventOutcome,
};
/// Handles all Register 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_register_event(
event: Event,
app_state: &mut AppState,
register_page: &mut RegisterFormState,
)-> Result<EventOutcome> {
if let Event::Key(key_event) = event {
let key_code = key_event.code;
let modifiers = key_event.modifiers;
// From buttons (outside) back into the canvas (ReadOnly) with Up/k from the left-most button
if register_page.focus_outside_canvas
&& register_page.focused_button_index == 0
&& matches!(key_code, KeyCode::Up | KeyCode::Char('k'))
&& 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()));
}
// Focus handoff: inside canvas → buttons
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
&& matches!(
(key_code, modifiers),
(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _)
)
{
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()));
}
}
// Forward to canvas if focus is inside
if !register_page.focus_outside_canvas {
match register_page.handle_key_event(key_event) {
KeyEventOutcome::Consumed(Some(msg)) => {
return Ok(EventOutcome::Ok(msg));
}
KeyEventOutcome::Consumed(None) => {
return Ok(EventOutcome::Ok("Register input updated".into()));
}
KeyEventOutcome::Pending => {
return Ok(EventOutcome::Ok("Waiting for next key...".into()));
}
KeyEventOutcome::NotMatched => {
// fall through
}
}
}
}
Ok(EventOutcome::Ok(String::new()))
}

View File

@@ -54,6 +54,8 @@ pub async fn back_to_login(
buffer_state.update_history(AppView::Login); buffer_state.update_history(AppView::Login);
// Reset focus state // Reset focus state
register_state.focus_outside_canvas = false;
register_state.focused_button_index = 0;
app_state.ui.focus_outside_canvas = false; app_state.ui.focus_outside_canvas = false;
app_state.focused_button_index = 0; app_state.focused_button_index = 0;

View File

@@ -5,8 +5,10 @@ pub mod ui;
pub mod state; pub mod state;
pub mod logic; pub mod logic;
pub mod suggestions; pub mod suggestions;
pub mod event;
// pub use state::*; // pub use state::*;
pub use ui::render_register; pub use ui::render_register;
pub use logic::*; pub use logic::*;
pub use state::*; pub use state::*;
pub use event::*;

View File

@@ -146,6 +146,8 @@ impl DataProvider for RegisterState {
pub struct RegisterFormState { pub struct RegisterFormState {
pub state: RegisterState, pub state: RegisterState,
pub editor: FormEditor<RegisterState>, pub editor: FormEditor<RegisterState>,
pub focus_outside_canvas: bool,
pub focused_button_index: usize,
} }
impl Default for RegisterFormState { impl Default for RegisterFormState {
@@ -174,7 +176,12 @@ impl RegisterFormState {
pub fn new() -> Self { pub fn new() -> Self {
let state = RegisterState::default(); let state = RegisterState::default();
let editor = FormEditor::new(state.clone()); let editor = FormEditor::new(state.clone());
Self { state, editor } Self {
state,
editor,
focus_outside_canvas: false,
focused_button_index: 0,
}
} }
// === Delegates to RegisterState === // === Delegates to RegisterState ===

View File

@@ -80,8 +80,9 @@ pub fn render_register(
// Register Button // Register Button
let register_button_index = 0; let register_button_index = 0;
let register_active = app_state.ui.focus_outside_canvas let register_active =
&& app_state.focused_button_index == register_button_index; register_page.focus_outside_canvas
&& register_page.focused_button_index == register_button_index;
let mut register_style = Style::default().fg(theme.fg); let mut register_style = Style::default().fg(theme.fg);
let mut register_border = Style::default().fg(theme.border); let mut register_border = Style::default().fg(theme.border);
if register_active { if register_active {
@@ -104,8 +105,9 @@ pub fn render_register(
// Return Button // Return Button
let return_button_index = 1; let return_button_index = 1;
let return_active = app_state.ui.focus_outside_canvas let return_active =
&& app_state.focused_button_index == return_button_index; register_page.focus_outside_canvas
&& register_page.focused_button_index == return_button_index;
let mut return_style = Style::default().fg(theme.fg); let mut return_style = Style::default().fg(theme.fg);
let mut return_border = Style::default().fg(theme.border); let mut return_border = Style::default().fg(theme.border);
if return_active { if return_active {