add logic now using general movement

This commit is contained in:
filipriec
2025-08-30 22:38:48 +02:00
parent 0a7f032028
commit 22926b7266
4 changed files with 235 additions and 194 deletions

View File

@@ -321,18 +321,6 @@ impl EventHandler {
if !outcome.get_message_if_ok().is_empty() {
return Ok(outcome);
}
} else if let Page::AddLogic(add_logic_page) = &mut router.current {
let outcome = add_logic::event::handle_add_logic_event(
event,
config,
app_state,
add_logic_page,
self.grpc_client.clone(),
self.save_logic_result_sender.clone(),
)?;
if !outcome.get_message_if_ok().is_empty() {
return Ok(outcome);
}
} else if let Page::Form(path) = &router.current {
let outcome = forms::event::handle_form_event(
event,
@@ -497,6 +485,22 @@ impl EventHandler {
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
}
if let Page::AddLogic(add_logic_page) = &mut router.current {
let client_clone = self.grpc_client.clone();
let sender_clone = self.save_logic_result_sender.clone();
if add_logic::event::handle_add_logic_event(
key_event,
movement_action,
config,
app_state,
add_logic_page,
client_clone,
sender_clone,
&mut self.command_message,
) {
return Ok(EventOutcome::Ok(self.command_message.clone()));
}
}
// Generic navigation for the rest (Intro/Login/Register/Form)
let nav_outcome = if matches!(&router.current, Page::AddTable(_) | Page::AddLogic(_)) {

View File

@@ -1,199 +1,149 @@
// src/pages/admin_panel/add_logic/event.rs
use anyhow::Result;
use crossterm::event::{Event, KeyCode, KeyModifiers};
use crate::config::binds::config::Config;
use crate::modes::handlers::event::EventOutcome;
use crate::pages::admin_panel::add_logic::state::{AddLogicFormState, AddLogicFocus};
use crate::movement::{move_focus, MovementAction};
use crate::pages::admin_panel::add_logic::nav::SaveLogicResultSender;
use crate::pages::admin_panel::add_logic::state::{AddLogicFocus, AddLogicFormState};
use crate::components::common::text_editor::TextEditor;
use crate::services::grpc_client::GrpcClient;
use crate::pages::admin_panel::add_logic::nav::SaveLogicResultSender; // keep type alias
use crate::state::app::state::AppState;
use canvas::DataProvider;
use crossterm::event::KeyEvent;
/// Focus traversal order for non-canvas navigation
const ADD_LOGIC_FOCUS_ORDER: [AddLogicFocus; 6] = [
AddLogicFocus::InputLogicName,
AddLogicFocus::InputTargetColumn,
AddLogicFocus::InputDescription,
AddLogicFocus::ScriptContentPreview,
AddLogicFocus::SaveButton,
AddLogicFocus::CancelButton,
];
/// Return true if the event was handled and UI should be redrawn.
pub fn handle_add_logic_event(
event: Event,
key_event: KeyEvent,
movement: Option<MovementAction>,
config: &Config,
app_state: &mut AppState,
add_logic_page: &mut AddLogicFormState,
grpc_client: GrpcClient,
save_logic_sender: SaveLogicResultSender,
) -> Result<EventOutcome> {
if let Event::Key(key_event) = event {
let st = &mut add_logic_page.state;
let key_code = key_event.code;
let modifiers = key_event.modifiers;
// 1) Fullscreen Script Editor mode
if st.current_focus == AddLogicFocus::InsideScriptContent {
match key_code {
KeyCode::Esc if modifiers == KeyModifiers::NONE => {
st.current_focus = AddLogicFocus::ScriptContentPreview;
command_message: &mut String,
) -> bool {
// 1) Script editor fullscreen mode
if add_logic_page.state.current_focus == AddLogicFocus::InsideScriptContent {
match key_event.code {
crossterm::event::KeyCode::Esc => {
add_logic_page.state.current_focus = AddLogicFocus::ScriptContentPreview;
app_state.ui.focus_outside_canvas = true;
return Ok(EventOutcome::Ok("Exited script editing.".into()));
*command_message = "Exited script editing.".to_string();
return true;
}
_ => {
let changed = {
let mut editor_borrow = st.script_content_editor.borrow_mut();
let mut editor_borrow =
add_logic_page.state.script_content_editor.borrow_mut();
TextEditor::handle_input(
&mut editor_borrow,
key_event,
&st.editor_keybinding_mode,
&mut st.vim_state,
&add_logic_page.state.editor_keybinding_mode,
&mut add_logic_page.state.vim_state,
)
};
if changed {
st.has_unsaved_changes = true;
return Ok(EventOutcome::Ok("Script updated".into()));
} else {
return Ok(EventOutcome::Ok(String::new()));
add_logic_page.state.has_unsaved_changes = true;
*command_message = "Script updated".to_string();
}
return true;
}
}
}
// 2) Canvas inputs (three fields) forward to FormEditor
// 2) Inside canvas: forward to FormEditor
let inside_canvas_inputs = matches!(
st.current_focus,
add_logic_page.state.current_focus,
AddLogicFocus::InputLogicName
| AddLogicFocus::InputTargetColumn
| AddLogicFocus::InputDescription
);
if inside_canvas_inputs {
// Handoff from last field to Script Preview on "down"/"next"
let last_idx = add_logic_page
.editor
.data_provider()
.field_count()
.saturating_sub(1);
if let Some(ma) = movement {
let last_idx = add_logic_page.editor.data_provider().field_count().saturating_sub(1);
let at_last = add_logic_page.editor.current_field() >= last_idx;
// Map to generic "down/next" via config or raw keys
let action_opt = config.get_general_action(key_code, modifiers);
let is_down_or_next = matches!(
action_opt,
Some("down") | Some("next") | Some("move_down") | Some("next_field")
)
|| matches!(key_code, KeyCode::Down)
|| matches!(key_code, KeyCode::Char('j') if modifiers.is_empty());
if at_last && is_down_or_next {
st.last_canvas_field = last_idx;
st.current_focus = AddLogicFocus::ScriptContentPreview;
add_logic_page.focus_outside_canvas = true;
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;
return Ok(EventOutcome::Ok(
"Moved to Script Preview".to_string(),
));
*command_message = "Moved to Script Preview".to_string();
return true;
}
}
// Normal canvas input
match add_logic_page.editor.handle_key_event(key_event) {
match add_logic_page.handle_key_event(key_event) {
canvas::keymap::KeyEventOutcome::Consumed(Some(msg)) => {
add_logic_page.sync_from_editor();
return Ok(EventOutcome::Ok(msg));
if !msg.is_empty() {
*command_message = msg;
}
return true;
}
canvas::keymap::KeyEventOutcome::Consumed(None) => {
add_logic_page.sync_from_editor();
return Ok(EventOutcome::Ok("Input updated".into()));
}
canvas::keymap::KeyEventOutcome::Pending => {
return Ok(EventOutcome::Ok(String::new()));
}
canvas::keymap::KeyEventOutcome::NotMatched => {
// fall through to outside handling
return true;
}
canvas::keymap::KeyEventOutcome::Pending => return true,
canvas::keymap::KeyEventOutcome::NotMatched => {}
}
}
// 3) Outside-canvas focus (Script Preview, Save, Cancel)
let action_opt = config.get_general_action(key_code, modifiers);
match action_opt {
Some("up") | Some("move_up") | Some("previous") | Some("previous_option") | Some("prev_field") => {
match st.current_focus {
AddLogicFocus::ScriptContentPreview => {
st.current_focus = AddLogicFocus::InputDescription;
add_logic_page.focus_outside_canvas = false;
app_state.ui.focus_outside_canvas = false;
return Ok(EventOutcome::Ok(
"Back to Description".to_string(),
));
}
AddLogicFocus::SaveButton => {
st.current_focus = AddLogicFocus::ScriptContentPreview;
return Ok(EventOutcome::Ok(
"Back to Script Preview".to_string(),
));
}
AddLogicFocus::CancelButton => {
st.current_focus = AddLogicFocus::SaveButton;
return Ok(EventOutcome::Ok("Back to Save".to_string()));
}
_ => {}
}
}
Some("down") | Some("move_down") | Some("next") | Some("next_option") | Some("next_field") => {
match st.current_focus {
AddLogicFocus::ScriptContentPreview => {
st.current_focus = AddLogicFocus::SaveButton;
return Ok(EventOutcome::Ok("Focus: Save".to_string()));
}
AddLogicFocus::SaveButton => {
st.current_focus = AddLogicFocus::CancelButton;
return Ok(EventOutcome::Ok("Focus: Cancel".to_string()));
}
_ => {}
}
}
Some("select") => {
match st.current_focus {
AddLogicFocus::ScriptContentPreview => {
st.current_focus = AddLogicFocus::InsideScriptContent;
add_logic_page.focus_outside_canvas = false;
app_state.ui.focus_outside_canvas = false;
return Ok(EventOutcome::Ok(
"Fullscreen script editing. Esc to exit.".into(),
));
}
AddLogicFocus::SaveButton => {
if let Some(msg) = st.save_logic() {
return Ok(EventOutcome::Ok(msg));
}
return Ok(EventOutcome::Ok(
"Saved (no changes)".to_string(),
));
}
AddLogicFocus::CancelButton => {
// Keep this simple: you can wire buffer/view navigation where needed
return Ok(EventOutcome::Ok(
"Cancelled Add Logic".to_string(),
));
}
// 3) Outside canvas
if let Some(ma) = movement {
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.state.current_focus,
AddLogicFocus::InputLogicName
| AddLogicFocus::InputTargetColumn
| AddLogicFocus::InputDescription => {
add_logic_page.focus_outside_canvas = false;
| AddLogicFocus::InputDescription
);
return true;
}
match ma {
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;
return Ok(EventOutcome::Ok(String::new()));
*command_message = "Fullscreen script editing. Esc to exit.".to_string();
return true;
}
AddLogicFocus::SaveButton => {
if let Some(msg) = add_logic_page.state.save_logic() {
*command_message = msg;
} else {
*command_message = "Saved (no changes)".to_string();
}
return true;
}
AddLogicFocus::CancelButton => {
*command_message = "Cancelled Add Logic".to_string();
return true;
}
_ => {}
}
}
Some("esc") => {
if st.current_focus == AddLogicFocus::ScriptContentPreview {
// Go back to Description (last canvas field)
st.current_focus = AddLogicFocus::InputDescription;
add_logic_page.focus_outside_canvas = false;
},
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;
return Ok(EventOutcome::Ok(
"Back to Description".to_string(),
));
*command_message = "Back to Description".to_string();
return true;
}
}
_ => {}
}
}
Ok(EventOutcome::Ok(String::new()))
false
}

View File

@@ -1,7 +1,8 @@
// src/pages/admin_panel/add_logic/state.rs
use crate::config::binds::config::{EditorConfig, EditorKeybindingMode};
use crate::components::common::text_editor::{TextEditor, VimState};
use canvas::{DataProvider, AppMode, FormEditor};
use canvas::{DataProvider, AppMode, FormEditor, SuggestionItem};
use crossterm::event::KeyCode;
use std::cell::RefCell;
use std::rc::Rc;
use tui_textarea::TextArea;
@@ -98,6 +99,19 @@ impl AddLogicState {
pub const INPUT_FIELD_COUNT: usize = 3;
/// Build canvas SuggestionItem list for target column
pub fn column_suggestions_sync(&self, query: &str) -> Vec<SuggestionItem> {
let q = query.to_lowercase();
self.table_columns_for_suggestions
.iter()
.filter(|c| q.is_empty() || c.to_lowercase().contains(&q))
.map(|c| SuggestionItem {
display_text: c.clone(),
value_to_store: c.clone(),
})
.collect()
}
/// Updates the target_column_suggestions based on current input.
pub fn update_target_column_suggestions(&mut self) {
let current_input = self.target_column_input.to_lowercase();
@@ -450,6 +464,84 @@ impl AddLogicFormState {
&mut self,
key_event: crossterm::event::KeyEvent,
) -> canvas::keymap::KeyEventOutcome {
// Customize behavior for Target Column (field index 1) in Edit mode,
// mirroring how Register page does suggestions for Role.
let in_target_col_field = self.editor.current_field() == 1;
let in_edit_mode = self.editor.mode() == canvas::AppMode::Edit;
if in_target_col_field && in_edit_mode {
match key_event.code {
// Tab: open suggestions if inactive; otherwise cycle next
KeyCode::Tab => {
if !self.editor.is_suggestions_active() {
if let Some(query) = self.editor.start_suggestions(1) {
let items = self.state.column_suggestions_sync(&query);
let applied =
self.editor.apply_suggestions_result(1, &query, items);
if applied {
self.editor.update_inline_completion();
}
}
} else {
self.editor.suggestions_next();
}
return canvas::keymap::KeyEventOutcome::Consumed(None);
}
// Shift+Tab: cycle suggestions too (fallback to next)
KeyCode::BackTab => {
if self.editor.is_suggestions_active() {
self.editor.suggestions_next();
return canvas::keymap::KeyEventOutcome::Consumed(None);
}
}
// Enter: apply selected suggestion (if active)
KeyCode::Enter => {
if self.editor.is_suggestions_active() {
let _ = self.editor.apply_suggestion();
return canvas::keymap::KeyEventOutcome::Consumed(None);
}
}
// Esc: close suggestions if active
KeyCode::Esc => {
if self.editor.is_suggestions_active() {
self.editor.close_suggestions();
return canvas::keymap::KeyEventOutcome::Consumed(None);
}
}
// Character input: mutate then refresh suggestions if active
KeyCode::Char(_) => {
let outcome = self.editor.handle_key_event(key_event);
if self.editor.is_suggestions_active() {
if let Some(query) = self.editor.start_suggestions(1) {
let items = self.state.column_suggestions_sync(&query);
let applied =
self.editor.apply_suggestions_result(1, &query, items);
if applied {
self.editor.update_inline_completion();
}
}
}
return outcome;
}
// Backspace/Delete: mutate then refresh suggestions if active
KeyCode::Backspace | KeyCode::Delete => {
let outcome = self.editor.handle_key_event(key_event);
if self.editor.is_suggestions_active() {
if let Some(query) = self.editor.start_suggestions(1) {
let items = self.state.column_suggestions_sync(&query);
let applied =
self.editor.apply_suggestions_result(1, &query, items);
if applied {
self.editor.update_inline_completion();
}
}
}
return outcome;
}
_ => { /* fall through */ }
}
}
// Default: let canvas handle it
self.editor.handle_key_event(key_event)
}
}

View File

@@ -2,7 +2,7 @@
use crate::config::colors::themes::Theme;
use crate::state::app::state::AppState;
use crate::pages::admin_panel::add_logic::state::{AddLogicFocus, AddLogicState, AddLogicFormState};
use canvas::{render_canvas, FormEditor};
use canvas::{render_canvas, render_suggestions_dropdown, DefaultCanvasTheme, FormEditor};
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
@@ -168,23 +168,18 @@ pub fn render_add_logic(
let editor = &add_logic_state.editor;
let active_field_rect = render_canvas(f, canvas_area, editor, theme);
// --- Render Autocomplete for Target Column ---
if editor.mode() == canvas::AppMode::Edit && editor.current_field() == 1 { // Target Column field
if add_logic_state.in_target_column_suggestion_mode() && add_logic_state.show_target_column_suggestions() {
if !add_logic_state.target_column_suggestions().is_empty() {
// --- Canvas suggestions dropdown (Target Column, etc.) ---
if editor.mode() == canvas::AppMode::Edit {
if let Some(input_rect) = active_field_rect {
autocomplete::render_autocomplete_dropdown(
render_suggestions_dropdown(
f,
f.area(),
input_rect,
f.area(), // Full frame area for clamping
theme,
add_logic_state.target_column_suggestions(),
add_logic_state.selected_target_column_suggestion_index(),
&DefaultCanvasTheme,
editor,
);
}
}
}
}
// Script content preview
{