From 04a7d866367aff7feb27779ff6d176704eaf79bd Mon Sep 17 00:00:00 2001 From: filipriec Date: Wed, 16 Apr 2025 16:30:11 +0200 Subject: [PATCH] better movement --- .../src/components/admin/admin_panel_admin.rs | 142 ++++++++++-------- client/src/functions/modes.rs | 1 + client/src/functions/modes/navigation.rs | 2 +- .../functions/modes/navigation/admin_nav.rs | 124 +++++++++++++++ client/src/modes/handlers/event.rs | 21 ++- 5 files changed, 224 insertions(+), 66 deletions(-) diff --git a/client/src/components/admin/admin_panel_admin.rs b/client/src/components/admin/admin_panel_admin.rs index ebdf735..e351376 100644 --- a/client/src/components/admin/admin_panel_admin.rs +++ b/client/src/components/admin/admin_panel_admin.rs @@ -6,12 +6,12 @@ use crate::state::app::state::AppState; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::Style, - text::{Line, Span}, + text::{Line, Span, Text}, // Added Text widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph}, Frame, }; -/// Renders the view specific to admin users. +/// Renders the view specific to admin users with a three-pane layout. pub fn render_admin_panel_admin( f: &mut Frame, area: Rect, @@ -19,16 +19,19 @@ pub fn render_admin_panel_admin( admin_state: &mut AdminState, theme: &Theme, ) { - let inner_area = area.inner(ratatui::layout::Margin { vertical: 1, horizontal: 1 }); - - // Split the inner area into two panes + // Split the area into three panes: Profiles | Tables | Dependencies let chunks = Layout::default() .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref()) - .split(inner_area); + .constraints([ + Constraint::Percentage(25), // Profiles + Constraint::Percentage(40), // Tables + Constraint::Percentage(35), // Dependencies + ].as_ref()) + .split(area); // Use the whole area directly - let left_pane = chunks[0]; - let right_pane = chunks[1]; + let profiles_pane = chunks[0]; + let tables_pane = chunks[1]; + let deps_pane = chunks[2]; // --- Profiles Pane (Left) --- let profile_focus = admin_state.current_focus == AdminFocus::Profiles; @@ -38,6 +41,16 @@ pub fn render_admin_panel_admin( Style::default().fg(theme.border) }; + // Block for the profiles pane + let profiles_block = Block::default() + .title(" Profiles ") + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(profile_border_style); + let profiles_inner_area = profiles_block.inner(profiles_pane); // Get inner area for list + f.render_widget(profiles_block, profiles_pane); // Render the block itself + + // Create profile list items let profile_list_items: Vec = app_state .profile_tree .profiles @@ -45,30 +58,27 @@ pub fn render_admin_panel_admin( .enumerate() .map(|(i, profile)| { let is_selected = admin_state.profile_list_state.selected() == Some(i); - let prefix = if is_selected { "[*] " } else { "[ ] " }; + let prefix = if is_selected { "[*] " } else { "[ ] " }; // Use [*] for selected let style = if is_selected { Style::default().fg(theme.highlight).add_modifier(ratatui::style::Modifier::BOLD) } else { Style::default().fg(theme.fg) }; - ListItem::new(Line::from(vec![Span::styled(prefix, style), Span::styled(&profile.name, style)])) + ListItem::new(Line::from(vec![ + Span::styled(prefix, style), + Span::styled(&profile.name, style) + ])) }) .collect(); + // Build and render profile list inside the block's inner area let profile_list = List::new(profile_list_items) - .block( - Block::default() - .title(" Profiles ") - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(profile_border_style), - ) .highlight_style(Style::default().add_modifier(ratatui::style::Modifier::REVERSED)) - .highlight_symbol(if profile_focus { "> " } else { " " }); + .highlight_symbol(if profile_focus { "> " } else { " " }); // Focus indicator - f.render_stateful_widget(profile_list, left_pane, &mut admin_state.profile_list_state); + f.render_stateful_widget(profile_list, profiles_inner_area, &mut admin_state.profile_list_state); - // --- Tables & Dependencies Pane (Right) --- + // --- Tables Pane (Middle) --- let table_focus = admin_state.current_focus == AdminFocus::Tables; let table_border_style = if table_focus { Style::default().fg(theme.highlight) @@ -76,21 +86,24 @@ pub fn render_admin_panel_admin( Style::default().fg(theme.border) }; - let right_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(0), Constraint::Length(3)]) - .split(right_pane); - - let tables_area = right_chunks[0]; - let deps_area = right_chunks[1]; - + // Get selected profile information let selected_profile_idx = admin_state.profile_list_state.selected(); let selected_profile_name = app_state .profile_tree .profiles - .get(selected_profile_idx.unwrap_or(usize::MAX)) + .get(selected_profile_idx.unwrap_or(usize::MAX)) // Use index, provide default if None .map_or("None", |p| &p.name); + // Block for the tables pane + let tables_block = Block::default() + .title(format!(" Tables (Profile: {}) ", selected_profile_name)) + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(table_border_style); + let tables_inner_area = tables_block.inner(tables_pane); // Get inner area for list + f.render_widget(tables_block, tables_pane); // Render the block itself + + // Create table list items and get dependencies for the selected table let (table_list_items, selected_table_deps): (Vec, Vec) = if let Some( profile, ) = selected_profile_idx.and_then(|idx| app_state.profile_tree.profiles.get(idx)) { @@ -100,12 +113,8 @@ pub fn render_admin_panel_admin( .enumerate() .map(|(i, table)| { let is_selected = admin_state.table_list_state.selected() == Some(i); - let prefix = if is_selected { "[*] " } else { "[ ] " }; - let deps_str = if !table.depends_on.is_empty() { - format!(" -> [{}]", table.depends_on.join(", ")) - } else { - String::new() - }; + let prefix = if is_selected { "[*] " } else { "[ ] " }; // Use [*] for selected + // Don't show dependencies inline anymore let style = if is_selected { Style::default().fg(theme.highlight).add_modifier(ratatui::style::Modifier::BOLD) } else { @@ -114,51 +123,62 @@ pub fn render_admin_panel_admin( ListItem::new(Line::from(vec![ Span::styled(prefix, style), Span::styled(&table.name, style), - Span::styled(deps_str, style.fg(theme.secondary)), ])) }) .collect(); + // Get dependencies only for the currently selected/highlighted table let deps = admin_state.table_list_state.selected() .and_then(|idx| profile.tables.get(idx)) .map_or(vec![], |t| t.depends_on.clone()); (items, deps) } else { - (vec![ListItem::new("No profile selected or profile has no tables")], vec![]) + // Default when no profile is selected + (vec![ListItem::new("Select a profile to see tables")], vec![]) }; + // Build and render table list inside the block's inner area let table_list = List::new(table_list_items) - .block( - Block::default() - .title(format!(" Tables & Dependencies (Profile: {}) ", selected_profile_name)) - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(table_border_style), - ) .highlight_style(Style::default().add_modifier(ratatui::style::Modifier::REVERSED)) - .highlight_symbol(if table_focus { "> " } else { " " }); + .highlight_symbol(if table_focus { "> " } else { " " }); // Focus indicator - f.render_stateful_widget(table_list, tables_area, &mut admin_state.table_list_state); + f.render_stateful_widget(table_list, tables_inner_area, &mut admin_state.table_list_state); - // --- Dependencies Display --- + // --- Dependencies Pane (Right) --- let selected_table_name = selected_profile_idx .and_then(|p_idx| app_state.profile_tree.profiles.get(p_idx)) .and_then(|p| admin_state.table_list_state.selected().and_then(|t_idx| p.tables.get(t_idx))) - .map_or("N/A", |t| &t.name); + .map_or("N/A", |t| &t.name); // Get name of the selected table - let deps_text = if !selected_table_deps.is_empty() { - format!("Dependencies for {}: {}", selected_table_name, selected_table_deps.join(", ")) + // Block for the dependencies pane + let deps_block = Block::default() + .title(format!(" Dependencies (Table: {}) ", selected_table_name)) + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(theme.border)); // No focus highlight for deps pane + let deps_inner_area = deps_block.inner(deps_pane); // Get inner area for content + f.render_widget(deps_block, deps_pane); // Render the block itself + + // Prepare content for the dependencies paragraph + let mut deps_content = Text::default(); + deps_content.lines.push(Line::from(Span::styled( + "Depends On:", + Style::default().fg(theme.accent), // Use accent color for the label + ))); + + if !selected_table_deps.is_empty() { + for dep in selected_table_deps { + // List each dependency + deps_content.lines.push(Line::from(Span::styled(format!("- {}", dep), theme.fg))); + } } else { - format!("Dependencies for {}: None", selected_table_name) - }; + // Indicate if there are no dependencies + deps_content.lines.push(Line::from(Span::styled(" None", theme.secondary))); + } - let deps_paragraph = Paragraph::new(deps_text) - .style(Style::default().fg(theme.secondary)) - .block( - Block::default() - .borders(Borders::TOP) - .border_style(Style::default().fg(theme.border)), - ); - f.render_widget(deps_paragraph, deps_area); + // Build and render dependencies paragraph inside the block's inner area + let deps_paragraph = Paragraph::new(deps_content); + f.render_widget(deps_paragraph, deps_inner_area); } + diff --git a/client/src/functions/modes.rs b/client/src/functions/modes.rs index b3a329a..dda082b 100644 --- a/client/src/functions/modes.rs +++ b/client/src/functions/modes.rs @@ -6,3 +6,4 @@ pub mod navigation; pub use read_only::*; pub use edit::*; +pub use navigation::*; diff --git a/client/src/functions/modes/navigation.rs b/client/src/functions/modes/navigation.rs index b2fa295..7b6121e 100644 --- a/client/src/functions/modes/navigation.rs +++ b/client/src/functions/modes/navigation.rs @@ -1,3 +1,3 @@ // src/functions/modes/navigation.rs -// pub mod admin_nav; +pub mod admin_nav; diff --git a/client/src/functions/modes/navigation/admin_nav.rs b/client/src/functions/modes/navigation/admin_nav.rs index 0d30d14..2f00fba 100644 --- a/client/src/functions/modes/navigation/admin_nav.rs +++ b/client/src/functions/modes/navigation/admin_nav.rs @@ -1 +1,125 @@ // src/functions/modes/navigation/admin_nav.rs + +use crate::config::binds::config::Config; +use crate::state::{ + app::state::AppState, + pages::admin::{AdminFocus, AdminState}, +}; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + +/// Handles navigation events specifically for the Admin Panel view. +/// Returns true if the event was handled, false otherwise. +pub fn handle_admin_navigation( + key: KeyEvent, + config: &Config, + app_state: &AppState, // Read-only access needed for counts + admin_state: &mut AdminState, // Mutable to change focus/selection + command_message: &mut String, +) -> bool { + let action = config.get_general_action(key.code, key.modifiers); + let current_focus = admin_state.current_focus; + let profile_count = app_state.profile_tree.profiles.len(); + + match action { + // --- Vertical Navigation (Up/Down) --- + Some("move_up") => { + match current_focus { + AdminFocus::Profiles => { + if profile_count > 0 { + admin_state.previous_profile(profile_count); + // Reset table selection when profile changes + admin_state.table_list_state.select(None); + *command_message = "Navigated profiles".to_string(); + } + } + AdminFocus::Tables => { + if let Some(selected_profile_index) = admin_state.profile_list_state.selected() { + if let Some(profile) = app_state.profile_tree.profiles.get(selected_profile_index) { + let table_count = profile.tables.len(); + if table_count > 0 { + admin_state.previous_table(table_count); + *command_message = "Navigated tables".to_string(); + } + } + } + } + } + true // Event handled + } + Some("move_down") => { + match current_focus { + AdminFocus::Profiles => { + if profile_count > 0 { + admin_state.next_profile(profile_count); + // Reset table selection when profile changes + admin_state.table_list_state.select(None); + *command_message = "Navigated profiles".to_string(); + } + } + AdminFocus::Tables => { + if let Some(selected_profile_index) = admin_state.profile_list_state.selected() { + if let Some(profile) = app_state.profile_tree.profiles.get(selected_profile_index) { + let table_count = profile.tables.len(); + if table_count > 0 { + admin_state.next_table(table_count); + *command_message = "Navigated tables".to_string(); + } + } + } + } + } + true // Event handled + } + + // --- Horizontal Navigation (Focus Change) --- + Some("next_option") | Some("previous_option") => { + admin_state.toggle_focus(); + *command_message = format!("Focus set to {:?}", admin_state.current_focus); + true // Event handled + } + + // --- Selection --- + Some("select") => { + match current_focus { + AdminFocus::Profiles => { + // If a profile is selected, move focus to tables + if admin_state.profile_list_state.selected().is_some() { + admin_state.toggle_focus(); // Move focus to Tables + // Optionally select the first table if available + if let Some(selected_profile_index) = admin_state.profile_list_state.selected() { + if let Some(profile) = app_state.profile_tree.profiles.get(selected_profile_index) { + if !profile.tables.is_empty() { + admin_state.table_list_state.select(Some(0)); + } + } + } + *command_message = "Selected profile, focus on Tables".to_string(); + } else { + *command_message = "No profile selected".to_string(); + } + } + AdminFocus::Tables => { + // Currently, selecting a table just confirms the highlight. + // Future: Could be used for multi-select toggle or other actions. + if let Some(idx) = admin_state.table_list_state.selected() { + *command_message = format!("Table {} highlighted", idx); + } else { + *command_message = "No table highlighted".to_string(); + } + // We don't change focus here for now. + } + } + true // Event handled + } + + // --- Other General Keys (Ignore for admin nav) --- + Some("toggle_sidebar") | Some("toggle_buffer_list") | Some("next_field") | Some("prev_field") => { + // These are handled globally or not applicable here. + false + } + + // --- No matching action --- + _ => false, // Event not handled by admin navigation + } +} + diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 67de433..2a2a6cc 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -39,6 +39,7 @@ use crate::modes::{ highlight::highlight, general::{navigation, dialog}, }; +use crate::functions::modes::navigation::admin_nav; use crate::config::binds::key_sequences::KeySequenceTracker; #[derive(Debug, Clone, PartialEq, Eq)] @@ -159,6 +160,19 @@ impl EventHandler { match current_mode { AppMode::General => { + // Prioritize Admin Panel navigation if it's visible + if app_state.ui.show_admin { + if admin_nav::handle_admin_navigation( + key, + config, + app_state, + admin_state, + &mut self.command_message, + ) { + return Ok(EventOutcome::Ok(self.command_message.clone())); + } + } + let nav_outcome = navigation::handle_navigation_event( key, config, @@ -179,10 +193,9 @@ impl EventHandler { UiContext::Intro => { intro::handle_intro_selection(app_state, buffer_state, index); if app_state.ui.show_admin { - let profile_names = app_state.profile_tree.profiles.iter() - .map(|p| p.name.clone()) - .collect(); - admin_state.set_profiles(profile_names); + if !app_state.profile_tree.profiles.is_empty() { + admin_state.profile_list_state.select(Some(0)); + } } message = format!("Intro Option {} selected", index); }