centralized general movement
This commit is contained in:
@@ -7,16 +7,14 @@ previous_buffer = ["space+b+p"]
|
|||||||
close_buffer = ["space+b+d"]
|
close_buffer = ["space+b+d"]
|
||||||
|
|
||||||
[keybindings.general]
|
[keybindings.general]
|
||||||
move_up = ["k", "Up"]
|
up = ["k", "Up"]
|
||||||
move_down = ["j", "Down"]
|
down = ["j", "Down"]
|
||||||
next_option = ["l", "Right"]
|
left = ["h", "Left"]
|
||||||
previous_option = ["h", "Left"]
|
right = ["l", "Right"]
|
||||||
|
next = ["Tab"]
|
||||||
|
previous = ["Shift+Tab"]
|
||||||
select = ["Enter"]
|
select = ["Enter"]
|
||||||
toggle_sidebar = ["ctrl+t"]
|
esc = ["esc"]
|
||||||
toggle_buffer_list = ["ctrl+b"]
|
|
||||||
next_field = ["Tab"]
|
|
||||||
prev_field = ["Shift+Tab"]
|
|
||||||
exit_table_scroll = ["esc"]
|
|
||||||
open_search = ["ctrl+f"]
|
open_search = ["ctrl+f"]
|
||||||
|
|
||||||
[keybindings.common]
|
[keybindings.common]
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub mod dialog;
|
|||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod bottom_panel;
|
pub mod bottom_panel;
|
||||||
pub mod pages;
|
pub mod pages;
|
||||||
|
pub mod movement;
|
||||||
|
|
||||||
pub use ui::run_ui;
|
pub use ui::run_ui;
|
||||||
|
|
||||||
|
|||||||
@@ -371,6 +371,58 @@ impl EventHandler {
|
|||||||
|
|
||||||
match current_mode {
|
match current_mode {
|
||||||
AppMode::General => {
|
AppMode::General => {
|
||||||
|
// Map keys to MovementAction
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
// Let the current page handle decoupled movement first
|
||||||
|
if let Some(ma) = movement_action {
|
||||||
|
match &mut router.current {
|
||||||
|
Page::AddTable(state) => {
|
||||||
|
if state.handle_movement(ma) {
|
||||||
|
// Keep UI focus consistent with inputs vs. outer elements
|
||||||
|
use crate::state::pages::add_table::AddTableFocus;
|
||||||
|
let is_canvas_input = matches!(
|
||||||
|
state.current_focus,
|
||||||
|
AddTableFocus::InputTableName
|
||||||
|
| AddTableFocus::InputColumnName
|
||||||
|
| AddTableFocus::InputColumnType
|
||||||
|
);
|
||||||
|
app_state.ui.focus_outside_canvas = !is_canvas_input;
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Page::Admin(state) => {
|
||||||
|
if state.handle_movement(app_state, ma) {
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Page::Intro(state) => {
|
||||||
|
if state.handle_movement(ma) {
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional page-specific handlers (non-movement or rich actions)
|
||||||
if let Page::Admin(admin_state) = &mut router.current {
|
if let Page::Admin(admin_state) = &mut router.current {
|
||||||
if auth_state.role.as_deref() == Some("admin") {
|
if auth_state.role.as_deref() == Some("admin") {
|
||||||
if admin_nav::handle_admin_navigation(
|
if admin_nav::handle_admin_navigation(
|
||||||
@@ -401,7 +453,7 @@ impl EventHandler {
|
|||||||
router,
|
router,
|
||||||
) {
|
) {
|
||||||
return Ok(EventOutcome::Ok(
|
return Ok(EventOutcome::Ok(
|
||||||
self.command_message.clone(),
|
self.command_message.clone(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,16 +475,25 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let nav_outcome = navigation::handle_navigation_event(
|
// Generic navigation for the rest (Intro/Login/Register/Form)
|
||||||
key_event,
|
let nav_outcome = if matches!(
|
||||||
config,
|
&router.current,
|
||||||
app_state,
|
Page::Admin(_) | Page::AddTable(_) | Page::AddLogic(_)
|
||||||
router,
|
) {
|
||||||
&mut self.command_mode,
|
Ok(EventOutcome::Ok(String::new()))
|
||||||
&mut self.command_input,
|
} else {
|
||||||
&mut self.command_message,
|
navigation::handle_navigation_event(
|
||||||
&mut self.navigation_state,
|
key_event,
|
||||||
).await;
|
config,
|
||||||
|
app_state,
|
||||||
|
router,
|
||||||
|
&mut self.command_mode,
|
||||||
|
&mut self.command_input,
|
||||||
|
&mut self.command_message,
|
||||||
|
&mut self.navigation_state,
|
||||||
|
).await
|
||||||
|
};
|
||||||
|
|
||||||
match nav_outcome {
|
match nav_outcome {
|
||||||
Ok(EventOutcome::ButtonSelected { context, index }) => {
|
Ok(EventOutcome::ButtonSelected { context, index }) => {
|
||||||
let message = match context {
|
let message = match context {
|
||||||
@@ -443,14 +504,8 @@ impl EventHandler {
|
|||||||
index,
|
index,
|
||||||
);
|
);
|
||||||
if let Page::Admin(admin_state) = &mut router.current {
|
if let Page::Admin(admin_state) = &mut router.current {
|
||||||
if !app_state
|
if !app_state.profile_tree.profiles.is_empty() {
|
||||||
.profile_tree
|
admin_state.profile_list_state.select(Some(0));
|
||||||
.profiles
|
|
||||||
.is_empty()
|
|
||||||
{
|
|
||||||
admin_state
|
|
||||||
.profile_list_state
|
|
||||||
.select(Some(0));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
format!("Intro Option {} selected", index)
|
format!("Intro Option {} selected", index)
|
||||||
@@ -506,8 +561,9 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
format!("Admin Option {} selected", index)
|
format!("Admin Option {} selected", index)
|
||||||
}
|
}
|
||||||
UiContext::Dialog => "Internal error: Unexpected dialog state"
|
UiContext::Dialog => {
|
||||||
.to_string(),
|
"Internal error: Unexpected dialog state".to_string()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return Ok(EventOutcome::Ok(message));
|
return Ok(EventOutcome::Ok(message));
|
||||||
}
|
}
|
||||||
@@ -515,37 +571,6 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AppMode::General => {
|
|
||||||
match &router.current {
|
|
||||||
Page::Form(_)
|
|
||||||
| Page::Login(_)
|
|
||||||
| Page::Register(_)
|
|
||||||
| Page::AddTable(_)
|
|
||||||
| Page::AddLogic(_) => {
|
|
||||||
if !app_state.ui.focus_outside_canvas {
|
|
||||||
if let Some(editor) = &mut app_state.form_editor {
|
|
||||||
editor.set_keymap(config.build_canvas_keymap());
|
|
||||||
match editor.handle_key_event(key_event) {
|
|
||||||
KeyEventOutcome::Consumed(Some(msg)) => {
|
|
||||||
return Ok(EventOutcome::Ok(msg));
|
|
||||||
}
|
|
||||||
KeyEventOutcome::Consumed(None) => {
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
KeyEventOutcome::Pending => {
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
KeyEventOutcome::NotMatched => {
|
|
||||||
// fall through to client actions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppMode::Command => {
|
AppMode::Command => {
|
||||||
if config.is_exit_command_mode(key_code, modifiers) {
|
if config.is_exit_command_mode(key_code, modifiers) {
|
||||||
self.command_input.clear();
|
self.command_input.clear();
|
||||||
|
|||||||
12
client/src/movement/actions.rs
Normal file
12
client/src/movement/actions.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// src/movement/actions.rs
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum MovementAction {
|
||||||
|
Next,
|
||||||
|
Previous,
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Select,
|
||||||
|
Esc,
|
||||||
|
}
|
||||||
32
client/src/movement/lib.rs
Normal file
32
client/src/movement/lib.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// src/movement/lib.rs
|
||||||
|
|
||||||
|
use crate::movement::MovementAction;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn move_focus<T: Copy + Eq>(
|
||||||
|
order: &[T],
|
||||||
|
current: &mut T,
|
||||||
|
action: MovementAction,
|
||||||
|
) -> bool {
|
||||||
|
if order.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Some(pos) = order.iter().position(|k| *k == *current) {
|
||||||
|
match action {
|
||||||
|
MovementAction::Previous | MovementAction::Up | MovementAction::Left => {
|
||||||
|
if pos > 0 {
|
||||||
|
*current = order[pos - 1];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MovementAction::Next | MovementAction::Down | MovementAction::Right => {
|
||||||
|
if pos + 1 < order.len() {
|
||||||
|
*current = order[pos + 1];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
6
client/src/movement/mod.rs
Normal file
6
client/src/movement/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// src/movement/mod.rs
|
||||||
|
pub mod actions;
|
||||||
|
pub mod lib;
|
||||||
|
|
||||||
|
pub use actions::MovementAction;
|
||||||
|
pub use lib::move_focus;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
// src/state/pages/intro.rs
|
// src/state/pages/intro.rs
|
||||||
|
use crate::movement::MovementAction;
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
#[derive(Default, Clone, Debug)]
|
||||||
pub struct IntroState {
|
pub struct IntroState {
|
||||||
@@ -23,3 +24,25 @@ impl IntroState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IntroState {
|
||||||
|
pub fn handle_movement(&mut self, action: MovementAction) -> bool {
|
||||||
|
match action {
|
||||||
|
MovementAction::Next | MovementAction::Right | MovementAction::Down => {
|
||||||
|
self.next_option();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MovementAction::Previous | MovementAction::Left | MovementAction::Up => {
|
||||||
|
self.previous_option();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MovementAction::Select => {
|
||||||
|
// Actual selection handled in event loop (UiContext::Intro)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MovementAction::Esc => {
|
||||||
|
// Nothing special for Intro, but could be used to quit
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use canvas::{DataProvider, AppMode};
|
use canvas::{DataProvider, AppMode};
|
||||||
use ratatui::widgets::TableState;
|
use ratatui::widgets::TableState;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::movement::{move_focus, MovementAction};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ColumnDefinition {
|
pub struct ColumnDefinition {
|
||||||
@@ -208,3 +209,175 @@ impl DataProvider for AddTableState {
|
|||||||
false // AddTableState doesn’t use suggestions
|
false // AddTableState doesn’t use suggestions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl AddTableState {
|
||||||
|
pub fn handle_movement(&mut self, action: MovementAction) -> bool {
|
||||||
|
use AddTableFocus::*;
|
||||||
|
|
||||||
|
// Linear outer focus order
|
||||||
|
const ORDER: [AddTableFocus; 10] = [
|
||||||
|
InputTableName,
|
||||||
|
InputColumnName,
|
||||||
|
InputColumnType,
|
||||||
|
AddColumnButton,
|
||||||
|
ColumnsTable,
|
||||||
|
IndexesTable,
|
||||||
|
LinksTable,
|
||||||
|
SaveButton,
|
||||||
|
DeleteSelectedButton,
|
||||||
|
CancelButton,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Enter "inside" on Select from outer panes
|
||||||
|
match (self.current_focus, action) {
|
||||||
|
(ColumnsTable, MovementAction::Select) => {
|
||||||
|
if !self.columns.is_empty() && self.column_table_state.selected().is_none() {
|
||||||
|
self.column_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
self.current_focus = InsideColumnsTable;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
(IndexesTable, MovementAction::Select) => {
|
||||||
|
if !self.indexes.is_empty() && self.index_table_state.selected().is_none() {
|
||||||
|
self.index_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
self.current_focus = InsideIndexesTable;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
(LinksTable, MovementAction::Select) => {
|
||||||
|
if !self.links.is_empty() && self.link_table_state.selected().is_none() {
|
||||||
|
self.link_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
self.current_focus = InsideLinksTable;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle "inside" states: Up/Down/Select/Esc; block outer movement keys
|
||||||
|
match self.current_focus {
|
||||||
|
InsideColumnsTable => {
|
||||||
|
match action {
|
||||||
|
MovementAction::Up => {
|
||||||
|
if let Some(i) = self.column_table_state.selected() {
|
||||||
|
let next = i.saturating_sub(1);
|
||||||
|
self.column_table_state.select(Some(next));
|
||||||
|
} else if !self.columns.is_empty() {
|
||||||
|
self.column_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Down => {
|
||||||
|
if let Some(i) = self.column_table_state.selected() {
|
||||||
|
let last = self.columns.len().saturating_sub(1);
|
||||||
|
let next = if i < last { i + 1 } else { i };
|
||||||
|
self.column_table_state.select(Some(next));
|
||||||
|
} else if !self.columns.is_empty() {
|
||||||
|
self.column_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Select => {
|
||||||
|
if let Some(i) = self.column_table_state.selected() {
|
||||||
|
if let Some(col) = self.columns.get_mut(i) {
|
||||||
|
col.selected = !col.selected;
|
||||||
|
self.has_unsaved_changes = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Esc => {
|
||||||
|
self.column_table_state.select(None);
|
||||||
|
self.current_focus = ColumnsTable;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Next | MovementAction::Previous => return true, // block outer moves
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InsideIndexesTable => {
|
||||||
|
match action {
|
||||||
|
MovementAction::Up => {
|
||||||
|
if let Some(i) = self.index_table_state.selected() {
|
||||||
|
let next = i.saturating_sub(1);
|
||||||
|
self.index_table_state.select(Some(next));
|
||||||
|
} else if !self.indexes.is_empty() {
|
||||||
|
self.index_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Down => {
|
||||||
|
if let Some(i) = self.index_table_state.selected() {
|
||||||
|
let last = self.indexes.len().saturating_sub(1);
|
||||||
|
let next = if i < last { i + 1 } else { i };
|
||||||
|
self.index_table_state.select(Some(next));
|
||||||
|
} else if !self.indexes.is_empty() {
|
||||||
|
self.index_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Select => {
|
||||||
|
if let Some(i) = self.index_table_state.selected() {
|
||||||
|
if let Some(ix) = self.indexes.get_mut(i) {
|
||||||
|
ix.selected = !ix.selected;
|
||||||
|
self.has_unsaved_changes = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Esc => {
|
||||||
|
self.index_table_state.select(None);
|
||||||
|
self.current_focus = IndexesTable;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Next | MovementAction::Previous => return true, // block outer moves
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InsideLinksTable => {
|
||||||
|
match action {
|
||||||
|
MovementAction::Up => {
|
||||||
|
if let Some(i) = self.link_table_state.selected() {
|
||||||
|
let next = i.saturating_sub(1);
|
||||||
|
self.link_table_state.select(Some(next));
|
||||||
|
} else if !self.links.is_empty() {
|
||||||
|
self.link_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Down => {
|
||||||
|
if let Some(i) = self.link_table_state.selected() {
|
||||||
|
let last = self.links.len().saturating_sub(1);
|
||||||
|
let next = if i < last { i + 1 } else { i };
|
||||||
|
self.link_table_state.select(Some(next));
|
||||||
|
} else if !self.links.is_empty() {
|
||||||
|
self.link_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Select => {
|
||||||
|
if let Some(i) = self.link_table_state.selected() {
|
||||||
|
if let Some(link) = self.links.get_mut(i) {
|
||||||
|
link.selected = !link.selected;
|
||||||
|
self.has_unsaved_changes = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Esc => {
|
||||||
|
self.link_table_state.select(None);
|
||||||
|
self.current_focus = LinksTable;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
MovementAction::Next | MovementAction::Previous => return true, // block outer moves
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: outer navigation via helper
|
||||||
|
move_focus(&ORDER, &mut self.current_focus, action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
use ratatui::widgets::ListState;
|
use ratatui::widgets::ListState;
|
||||||
use crate::state::pages::add_table::AddTableState;
|
use crate::state::pages::add_table::AddTableState;
|
||||||
use crate::state::pages::add_logic::AddLogicState;
|
use crate::state::pages::add_logic::AddLogicState;
|
||||||
|
use crate::movement::{move_focus, MovementAction};
|
||||||
|
use crate::state::app::state::AppState;
|
||||||
|
|
||||||
// Define the focus states for the admin panel panes
|
// Define the focus states for the admin panel panes
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
@@ -180,3 +182,121 @@ impl AdminState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AdminState {
|
||||||
|
pub fn handle_movement(
|
||||||
|
&mut self,
|
||||||
|
app: &AppState,
|
||||||
|
action: MovementAction,
|
||||||
|
) -> bool {
|
||||||
|
use AdminFocus::*;
|
||||||
|
|
||||||
|
const ORDER: [AdminFocus; 5] = [
|
||||||
|
ProfilesPane,
|
||||||
|
Tables,
|
||||||
|
Button1,
|
||||||
|
Button2,
|
||||||
|
Button3,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Enter "inside" on Select
|
||||||
|
match (self.current_focus, action) {
|
||||||
|
(ProfilesPane, MovementAction::Select) => {
|
||||||
|
if !app.profile_tree.profiles.is_empty()
|
||||||
|
&& self.profile_list_state.selected().is_none()
|
||||||
|
{
|
||||||
|
self.profile_list_state.select(Some(0));
|
||||||
|
}
|
||||||
|
self.current_focus = InsideProfilesList;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
(Tables, MovementAction::Select) => {
|
||||||
|
let p_idx = self
|
||||||
|
.selected_profile_index
|
||||||
|
.or_else(|| self.profile_list_state.selected());
|
||||||
|
if let Some(pi) = p_idx {
|
||||||
|
let len = app
|
||||||
|
.profile_tree
|
||||||
|
.profiles
|
||||||
|
.get(pi)
|
||||||
|
.map(|p| p.tables.len())
|
||||||
|
.unwrap_or(0);
|
||||||
|
if len > 0 && self.table_list_state.selected().is_none() {
|
||||||
|
self.table_list_state.select(Some(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.current_focus = InsideTablesList;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inside navigation + Esc to exit; don't consume Select (admin_nav handles it)
|
||||||
|
match self.current_focus {
|
||||||
|
InsideProfilesList => match action {
|
||||||
|
MovementAction::Up => {
|
||||||
|
if !app.profile_tree.profiles.is_empty() {
|
||||||
|
let curr = self.profile_list_state.selected().unwrap_or(0);
|
||||||
|
let next = curr.saturating_sub(1);
|
||||||
|
self.profile_list_state.select(Some(next));
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MovementAction::Down => {
|
||||||
|
let len = app.profile_tree.profiles.len();
|
||||||
|
if len > 0 {
|
||||||
|
let curr = self.profile_list_state.selected().unwrap_or(0);
|
||||||
|
let next = if curr + 1 < len { curr + 1 } else { curr };
|
||||||
|
self.profile_list_state.select(Some(next));
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MovementAction::Esc => {
|
||||||
|
self.current_focus = ProfilesPane;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MovementAction::Next | MovementAction::Previous => true, // block outer moves
|
||||||
|
MovementAction::Select => false,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
InsideTablesList => {
|
||||||
|
let tables_len = {
|
||||||
|
let p_idx = self
|
||||||
|
.selected_profile_index
|
||||||
|
.or_else(|| self.profile_list_state.selected());
|
||||||
|
p_idx.and_then(|pi| app.profile_tree.profiles.get(pi))
|
||||||
|
.map(|p| p.tables.len())
|
||||||
|
.unwrap_or(0)
|
||||||
|
};
|
||||||
|
match action {
|
||||||
|
MovementAction::Up => {
|
||||||
|
if tables_len > 0 {
|
||||||
|
let curr = self.table_list_state.selected().unwrap_or(0);
|
||||||
|
let next = curr.saturating_sub(1);
|
||||||
|
self.table_list_state.select(Some(next));
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MovementAction::Down => {
|
||||||
|
if tables_len > 0 {
|
||||||
|
let curr = self.table_list_state.selected().unwrap_or(0);
|
||||||
|
let next = if curr + 1 < tables_len { curr + 1 } else { curr };
|
||||||
|
self.table_list_state.select(Some(next));
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MovementAction::Esc => {
|
||||||
|
self.current_focus = Tables;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MovementAction::Next | MovementAction::Previous => true, // block outer moves
|
||||||
|
MovementAction::Select => false,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Default: outer navigation via helper
|
||||||
|
return move_focus(&ORDER, &mut self.current_focus, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user