diff --git a/client/src/components/auth/login.rs b/client/src/components/auth/login.rs index ed3eb07..7182956 100644 --- a/client/src/components/auth/login.rs +++ b/client/src/components/auth/login.rs @@ -47,7 +47,6 @@ pub fn render_login( .split(inner_area); // --- FORM RENDERING --- - // Directly pass the form area to canvas for border handling crate::components::handlers::canvas::render_canvas( f, chunks[0], @@ -120,14 +119,17 @@ pub fn render_login( ); // --- DIALOG --- - if app_state.ui.dialog.show_dialog { + // Check the correct field name for showing the dialog + if app_state.ui.dialog.dialog_show { + // Pass all 7 arguments correctly dialog::render_dialog( f, - f.size(), + f.area(), theme, &app_state.ui.dialog.dialog_title, &app_state.ui.dialog.dialog_message, - app_state.ui.dialog.dialog_button_active, + &app_state.ui.dialog.dialog_buttons, // Pass buttons slice + app_state.ui.dialog.dialog_active_button_index, // Pass active index ); } } diff --git a/client/src/components/common/dialog.rs b/client/src/components/common/dialog.rs index 61cfd82..62fab00 100644 --- a/client/src/components/common/dialog.rs +++ b/client/src/components/common/dialog.rs @@ -1,101 +1,146 @@ // src/components/common/dialog.rs -use ratatui::{ - layout::{Constraint, Direction, Layout, Rect, Margin}, - style::{Modifier, Style}, - widgets::{Block, BorderType, Borders, Paragraph}, - Frame, - text::{Text, Line, Span} -}; -use ratatui::prelude::Alignment; use crate::config::colors::themes::Theme; +use ratatui::{ + layout::{Constraint, Direction, Layout, Margin, Rect}, + prelude::Alignment, + style::{Modifier, Style}, + text::{Line, Span, Text}, + widgets::{Block, BorderType, Borders, Paragraph, Clear}, // Added Clear + Frame, +}; pub fn render_dialog( f: &mut Frame, area: Rect, theme: &Theme, - title: &str, - message: &str, - is_active: bool, + dialog_title: &str, + dialog_message: &str, + dialog_buttons: &[String], + dialog_active_button_index: usize, ) { - // Create a centered rect for the dialog - let dialog_area = centered_rect(60, 25, area); + let message_lines: Vec<_> = dialog_message.lines().collect(); + let message_height = message_lines.len() as u16; + let button_row_height = if dialog_buttons.is_empty() { 0 } else { 3 }; + let vertical_padding = 2; // Block borders (top/bottom) + let inner_vertical_margin = 2; // Margin inside block (top/bottom) + + let required_inner_height = + message_height + button_row_height + inner_vertical_margin; + // Add block border height + let required_total_height = required_inner_height + vertical_padding; + + // Use a fixed percentage width, clamped to min/max + let width_percentage: u16 = 60; + let dialog_width = (area.width * width_percentage / 100) + .max(20) // Minimum width + .min(area.width); // Maximum width + + // Ensure height doesn't exceed available area + let dialog_height = required_total_height.min(area.height); + + // Calculate centered area manually + let dialog_x = area.x + (area.width.saturating_sub(dialog_width)) / 2; + let dialog_y = area.y + (area.height.saturating_sub(dialog_height)) / 2; + let dialog_area = Rect::new(dialog_x, dialog_y, dialog_width, dialog_height); + + // Clear the area first before drawing the dialog + f.render_widget(Clear, dialog_area); - // Main dialog container let block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(theme.accent)) - .title(title) + .title(format!(" {} ", dialog_title)) // Add padding to title .style(Style::default().bg(theme.bg)); - f.render_widget(&block, dialog_area); + f.render_widget(block, dialog_area); - // Inner content area - let inner_area = block.inner(dialog_area).inner(Margin { - horizontal: 2, - vertical: 1, + // Calculate inner area *after* defining the block + let inner_area = dialog_area.inner(Margin { + horizontal: 2, // Left/Right padding inside border + vertical: 1, // Top/Bottom padding inside border }); - // Split into message and button areas + // Layout for Message and Buttons + let mut constraints = vec![ + // Allocate space for message, ensuring at least 1 line height + Constraint::Min(message_height.max(1)), + ]; + if button_row_height > 0 { + constraints.push(Constraint::Length(button_row_height)); + } + let chunks = Layout::default() .direction(Direction::Vertical) - .constraints([ - Constraint::Min(3), // Message content - Constraint::Length(3), // Button - ]) + .constraints(constraints) .split(inner_area); - // Message text - let message_text = Text::from(message.lines().map(|l| Line::from(Span::styled( - l, - Style::default().fg(theme.fg) - ))).collect::>()); - - let message_paragraph = Paragraph::new(message_text) - .alignment(Alignment::Center); + // Render Message + let message_text = Text::from( + message_lines + .into_iter() + .map(|l| Line::from(Span::styled(l, Style::default().fg(theme.fg)))) + .collect::>(), + ); + + let message_paragraph = + Paragraph::new(message_text).alignment(Alignment::Center); + // Render message in the first chunk f.render_widget(message_paragraph, chunks[0]); - // OK Button - let button_style = if is_active { - Style::default() - .fg(theme.highlight) - .add_modifier(Modifier::BOLD) - } else { - Style::default().fg(theme.fg) - }; + // Render Buttons if they exist and there's a chunk for them + if !dialog_buttons.is_empty() && chunks.len() > 1 { + let button_area = chunks[1]; + let button_count = dialog_buttons.len(); - let button_block = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Plain) - .border_style(Style::default().fg(theme.accent)) - .style(Style::default().bg(theme.bg)); + // Use Ratio for potentially more even distribution with few buttons + let button_constraints = std::iter::repeat(Constraint::Ratio( + 1, + button_count as u32, + )) + .take(button_count) + .collect::>(); - f.render_widget( - Paragraph::new("OK") - .block(button_block) - .style(button_style) - .alignment(Alignment::Center), - chunks[1], - ); + let button_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints(button_constraints) + .horizontal_margin(1) // Add space between buttons + .split(button_area); + + for (i, button_label) in dialog_buttons.iter().enumerate() { + // Ensure we don't try to render into a non-existent chunk + if i >= button_chunks.len() { + break; + } + + let is_active = i == dialog_active_button_index; + let (button_style, border_style) = if is_active { + ( + Style::default() + .fg(theme.highlight) + .add_modifier(Modifier::BOLD), + Style::default().fg(theme.accent), // Highlight border + ) + } else { + ( + Style::default().fg(theme.fg), + Style::default().fg(theme.border), // Normal border + ) + }; + + let button_block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .border_style(border_style); + + f.render_widget( + Paragraph::new(button_label.as_str()) + .block(button_block) + .style(button_style) + .alignment(Alignment::Center), + button_chunks[i], + ); + } + } } -/// Helper function to center a rect with given percentage values -fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { - let popup_layout = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage((100 - percent_y) / 2), - Constraint::Percentage(percent_y), - Constraint::Percentage((100 - percent_y) / 2), - ]) - .split(r); - - Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage((100 - percent_x) / 2), - Constraint::Percentage(percent_x), - Constraint::Percentage((100 - percent_x) / 2), - ]) - .split(popup_layout[1])[1] -} diff --git a/client/src/state/state.rs b/client/src/state/state.rs index ff52d02..81c6036 100644 --- a/client/src/state/state.rs +++ b/client/src/state/state.rs @@ -5,12 +5,12 @@ use common::proto::multieko2::table_definition::ProfileTreeResponse; use crate::components::IntroState; use crate::modes::handlers::mode_manager::AppMode; -#[derive(Default)] pub struct DialogState { - pub show_dialog: bool, + pub dialog_show: bool, pub dialog_title: String, pub dialog_message: String, - pub dialog_button_active: bool, + pub dialog_buttons: Vec, + pub dialog_active_button_index: usize, } pub struct UiState { @@ -77,21 +77,55 @@ impl AppState { } // Add dialog helper methods - pub fn show_dialog(&mut self, title: &str, message: &str) { - self.ui.dialog.show_dialog = true; + /// Shows a dialog with the given title, message, and buttons. + /// The first button (index 0) is active by default. + pub fn show_dialog( + &mut self, + title: &str, + message: &str, + buttons: Vec, + ) { self.ui.dialog.dialog_title = title.to_string(); self.ui.dialog.dialog_message = message.to_string(); - self.ui.dialog.dialog_button_active = true; + self.ui.dialog.dialog_buttons = buttons; + self.ui.dialog.dialog_active_button_index = 0; // Default to first button + self.ui.dialog.dialog_show = true; // Use new name } + /// Hides the dialog and clears its content. pub fn hide_dialog(&mut self) { - self.ui.dialog.show_dialog = false; + self.ui.dialog.dialog_show = false; // Use new name self.ui.dialog.dialog_title.clear(); self.ui.dialog.dialog_message.clear(); + self.ui.dialog.dialog_buttons.clear(); + self.ui.dialog.dialog_active_button_index = 0; } - pub fn set_dialog_button_active(&mut self, active: bool) { - self.ui.dialog.dialog_button_active = active; + /// Sets the active button index, wrapping around if necessary. + pub fn next_dialog_button(&mut self) { + if !self.ui.dialog.dialog_buttons.is_empty() { + let next_index = (self.ui.dialog.dialog_active_button_index + 1) + % self.ui.dialog.dialog_buttons.len(); + self.ui.dialog.dialog_active_button_index = next_index; // Use new name + } + } + + /// Sets the active button index, wrapping around if necessary. + pub fn previous_dialog_button(&mut self) { + if !self.ui.dialog.dialog_buttons.is_empty() { + let len = self.ui.dialog.dialog_buttons.len(); + let prev_index = + (self.ui.dialog.dialog_active_button_index + len - 1) % len; + self.ui.dialog.dialog_active_button_index = prev_index; // Use new name + } + } + + /// Gets the label of the currently active button, if any. + pub fn get_active_dialog_button_label(&self) -> Option<&str> { + self.ui.dialog + .dialog_buttons // Use new name + .get(self.ui.dialog.dialog_active_button_index) // Use new name + .map(|s| s.as_str()) } } @@ -109,3 +143,16 @@ impl Default for UiState { } } } + +// Update the Default implementation for DialogState itself +impl Default for DialogState { + fn default() -> Self { + Self { + dialog_show: false, // Use new name + dialog_title: String::new(), // Use new name + dialog_message: String::new(), // Use new name + dialog_buttons: Vec::new(), // Use new name + dialog_active_button_index: 0, // Use new name + } + } +} diff --git a/client/src/tui/functions/common/login.rs b/client/src/tui/functions/common/login.rs index 986266c..96d3fbb 100644 --- a/client/src/tui/functions/common/login.rs +++ b/client/src/tui/functions/common/login.rs @@ -3,6 +3,8 @@ use crate::services::auth::AuthClient; use crate::state::pages::auth::AuthState; use crate::state::state::AppState; use crate::state::canvas_state::CanvasState; +// Remove unused import if CanvasState is not directly used here +// use crate::state::canvas_state::CanvasState; /// Attempts to log the user in using the provided credentials via gRPC. /// Updates AuthState and AppState on success or failure. @@ -16,19 +18,18 @@ pub async fn save( // Clear previous error/dialog state before attempting auth_state.error_message = None; - app_state.ui.dialog.show_dialog = false; + // Use the helper to ensure dialog is hidden and cleared properly + app_state.hide_dialog(); // Call the gRPC login method match auth_client.login(identifier, password).await { Ok(response) => { // Store authentication details on success - auth_state.auth_token = Some(response.access_token.clone()); // Clone response fields + auth_state.auth_token = Some(response.access_token.clone()); auth_state.user_id = Some(response.user_id.clone()); auth_state.role = Some(response.role.clone()); - auth_state.set_has_unsaved_changes(false); // Mark as "saved" + auth_state.set_has_unsaved_changes(false); - // Format the successful response for the dialog - // Note: Long tokens might make the dialog wide. Consider truncating if needed. let success_message = format!( "Login Successful!\n\n\ Access Token: {}\n\ @@ -38,36 +39,42 @@ pub async fn save( Role: {}", response.access_token, response.token_type, - response.expires_in, // Ensure this type implements Display or use .to_string() + response.expires_in, response.user_id, response.role ); - // Configure and show the success dialog - app_state.ui.dialog.dialog_title = "Login Success".to_string(); - app_state.ui.dialog.dialog_message = success_message; - app_state.ui.dialog.show_dialog = true; - app_state.ui.dialog.dialog_button_active = true; // Make OK button active + // Use the helper method to configure and show the dialog + app_state.show_dialog( + "Login Success", + &success_message, + vec!["OK".to_string()], // Pass buttons here + ); + // REMOVE these lines: + // app_state.ui.dialog.dialog_title = "Login Success".to_string(); + // app_state.ui.dialog.dialog_message = success_message; + // app_state.ui.dialog.dialog_show = true; + // app_state.ui.dialog.dialog_button_active = true; - // Return a simple success indicator; the dialog shows details Ok("Login successful, details shown in dialog.".to_string()) } Err(e) => { - // Format the error message for the dialog let error_message = format!("{}", e); - // Configure and show the error dialog - app_state.ui.dialog.dialog_title = "Login Failed".to_string(); - app_state.ui.dialog.dialog_message = error_message.clone(); // Clone for return - app_state.ui.dialog.show_dialog = true; - app_state.ui.dialog.dialog_button_active = true; // Make OK button active + // Use the helper method to configure and show the dialog + app_state.show_dialog( + "Login Failed", + &error_message, + vec!["OK".to_string()], // Pass buttons here + ); + // REMOVE these lines: + // app_state.ui.dialog.dialog_title = "Login Failed".to_string(); + // app_state.ui.dialog.dialog_message = error_message.clone(); + // app_state.ui.dialog.dialog_show = true; + // app_state.ui.dialog.dialog_button_active = true; - // Keep unsaved changes true if login fails, allowing retry/revert auth_state.set_has_unsaved_changes(true); - // Return the error message; the dialog also shows it - // Using Ok here because the 'save' operation itself didn't panic, - // even though the underlying login failed. Ok(format!("Login failed: {}", error_message)) } } @@ -76,17 +83,20 @@ pub async fn save( /// Reverts the login form fields to empty and returns to the previous screen (Intro). pub async fn revert( auth_state: &mut AuthState, - app_state: &mut AppState, + _app_state: &mut AppState, // Prefix unused variable ) -> String { // Clear the input fields auth_state.username.clear(); auth_state.password.clear(); - auth_state.error_message = None; // Clear any previous error - auth_state.set_has_unsaved_changes(false); // Fields are cleared, no unsaved changes + auth_state.error_message = None; + auth_state.set_has_unsaved_changes(false); - // TODO REDIRECT is now disabled - // app_state.ui.show_login = false; - // app_state.ui.show_intro = true; + // Ensure dialog is hidden if revert is called + // _app_state.hide_dialog(); // Uncomment if needed + + // Navigation logic (currently disabled in original code) + // _app_state.ui.show_login = false; + // _app_state.ui.show_intro = true; "Login reverted".to_string() }