canvas library usage instead of internal canvas on the form, others are still using canvas from state. Needed debugging because its not working yet
This commit is contained in:
@@ -8,6 +8,7 @@ license.workspace = true
|
|||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
async-trait = "0.1.88"
|
async-trait = "0.1.88"
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
|
canvas = { path = "../canvas" }
|
||||||
|
|
||||||
ratatui = { workspace = true }
|
ratatui = { workspace = true }
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true }
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::components::common::autocomplete;
|
|||||||
use crate::components::handlers::canvas::render_canvas;
|
use crate::components::handlers::canvas::render_canvas;
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::state::app::highlight::HighlightState;
|
use crate::state::app::highlight::HighlightState;
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
use canvas::CanvasState;
|
||||||
use crate::state::pages::form::FormState;
|
use crate::state::pages::form::FormState;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
|
||||||
@@ -64,7 +64,7 @@ pub fn render_form(
|
|||||||
f.render_widget(count_para, main_layout[0]);
|
f.render_widget(count_para, main_layout[0]);
|
||||||
|
|
||||||
// Get the active field's rect from render_canvas
|
// Get the active field's rect from render_canvas
|
||||||
let active_field_rect = render_canvas(
|
let active_field_rect = crate::components::handlers::canvas::render_canvas_library(
|
||||||
f,
|
f,
|
||||||
main_layout[1],
|
main_layout[1],
|
||||||
form_state,
|
form_state,
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ use ratatui::{
|
|||||||
};
|
};
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::state::app::highlight::HighlightState;
|
use crate::state::app::highlight::HighlightState;
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
use crate::state::pages::canvas_state::CanvasState as LegacyCanvasState;
|
||||||
|
use canvas::CanvasState as LibraryCanvasState;
|
||||||
use std::cmp::{max, min};
|
use std::cmp::{max, min};
|
||||||
|
|
||||||
|
/// Render canvas for legacy CanvasState (AddTableState, LoginState, RegisterState, AddLogicState)
|
||||||
pub fn render_canvas(
|
pub fn render_canvas(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
form_state: &impl CanvasState,
|
form_state: &impl LegacyCanvasState,
|
||||||
fields: &[&str],
|
fields: &[&str],
|
||||||
current_field_idx: &usize,
|
current_field_idx: &usize,
|
||||||
inputs: &[&String],
|
inputs: &[&String],
|
||||||
@@ -23,12 +25,75 @@ pub fn render_canvas(
|
|||||||
is_edit_mode: bool,
|
is_edit_mode: bool,
|
||||||
highlight_state: &HighlightState,
|
highlight_state: &HighlightState,
|
||||||
) -> Option<Rect> {
|
) -> Option<Rect> {
|
||||||
|
render_canvas_impl(
|
||||||
|
f,
|
||||||
|
area,
|
||||||
|
fields,
|
||||||
|
current_field_idx,
|
||||||
|
inputs,
|
||||||
|
theme,
|
||||||
|
is_edit_mode,
|
||||||
|
highlight_state,
|
||||||
|
form_state.current_cursor_pos(),
|
||||||
|
form_state.has_unsaved_changes(),
|
||||||
|
|i| form_state.get_display_value_for_field(i).to_string(),
|
||||||
|
|i| form_state.has_display_override(i),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render canvas for library CanvasState (FormState)
|
||||||
|
pub fn render_canvas_library(
|
||||||
|
f: &mut Frame,
|
||||||
|
area: Rect,
|
||||||
|
form_state: &impl LibraryCanvasState,
|
||||||
|
fields: &[&str],
|
||||||
|
current_field_idx: &usize,
|
||||||
|
inputs: &[&String],
|
||||||
|
theme: &Theme,
|
||||||
|
is_edit_mode: bool,
|
||||||
|
highlight_state: &HighlightState,
|
||||||
|
) -> Option<Rect> {
|
||||||
|
render_canvas_impl(
|
||||||
|
f,
|
||||||
|
area,
|
||||||
|
fields,
|
||||||
|
current_field_idx,
|
||||||
|
inputs,
|
||||||
|
theme,
|
||||||
|
is_edit_mode,
|
||||||
|
highlight_state,
|
||||||
|
form_state.current_cursor_pos(),
|
||||||
|
form_state.has_unsaved_changes(),
|
||||||
|
|i| form_state.get_display_value_for_field(i).to_string(),
|
||||||
|
|i| form_state.has_display_override(i),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal implementation shared by both render functions
|
||||||
|
fn render_canvas_impl<F1, F2>(
|
||||||
|
f: &mut Frame,
|
||||||
|
area: Rect,
|
||||||
|
fields: &[&str],
|
||||||
|
current_field_idx: &usize,
|
||||||
|
inputs: &[&String],
|
||||||
|
theme: &Theme,
|
||||||
|
is_edit_mode: bool,
|
||||||
|
highlight_state: &HighlightState,
|
||||||
|
current_cursor_pos: usize,
|
||||||
|
has_unsaved_changes: bool,
|
||||||
|
get_display_value: F1,
|
||||||
|
has_display_override: F2,
|
||||||
|
) -> Option<Rect>
|
||||||
|
where
|
||||||
|
F1: Fn(usize) -> String,
|
||||||
|
F2: Fn(usize) -> bool,
|
||||||
|
{
|
||||||
let columns = Layout::default()
|
let columns = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
|
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
let border_style = if form_state.has_unsaved_changes() {
|
let border_style = if has_unsaved_changes {
|
||||||
Style::default().fg(theme.warning)
|
Style::default().fg(theme.warning)
|
||||||
} else if is_edit_mode {
|
} else if is_edit_mode {
|
||||||
Style::default().fg(theme.accent)
|
Style::default().fg(theme.accent)
|
||||||
@@ -75,17 +140,16 @@ pub fn render_canvas(
|
|||||||
|
|
||||||
for (i, _input) in inputs.iter().enumerate() {
|
for (i, _input) in inputs.iter().enumerate() {
|
||||||
let is_active = i == *current_field_idx;
|
let is_active = i == *current_field_idx;
|
||||||
let current_cursor_pos = form_state.current_cursor_pos();
|
|
||||||
|
|
||||||
// Use the trait method to get display value
|
// Use the provided closure to get display value
|
||||||
let text = form_state.get_display_value_for_field(i);
|
let text = get_display_value(i);
|
||||||
let text_len = text.chars().count();
|
let text_len = text.chars().count();
|
||||||
let line: Line;
|
let line: Line;
|
||||||
|
|
||||||
match highlight_state {
|
match highlight_state {
|
||||||
HighlightState::Off => {
|
HighlightState::Off => {
|
||||||
line = Line::from(Span::styled(
|
line = Line::from(Span::styled(
|
||||||
text,
|
&text,
|
||||||
if is_active {
|
if is_active {
|
||||||
Style::default().fg(theme.highlight)
|
Style::default().fg(theme.highlight)
|
||||||
} else {
|
} else {
|
||||||
@@ -141,11 +205,11 @@ pub fn render_canvas(
|
|||||||
Span::styled(after, normal_style_in_highlight),
|
Span::styled(after, normal_style_in_highlight),
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
line = Line::from(Span::styled(text, highlight_style));
|
line = Line::from(Span::styled(&text, highlight_style));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
line = Line::from(Span::styled(
|
line = Line::from(Span::styled(
|
||||||
text,
|
&text,
|
||||||
if is_active { normal_style_in_highlight } else { normal_style_outside }
|
if is_active { normal_style_in_highlight } else { normal_style_outside }
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -158,10 +222,10 @@ pub fn render_canvas(
|
|||||||
let normal_style_outside = Style::default().fg(theme.fg);
|
let normal_style_outside = Style::default().fg(theme.fg);
|
||||||
|
|
||||||
if i >= start_field && i <= end_field {
|
if i >= start_field && i <= end_field {
|
||||||
line = Line::from(Span::styled(text, highlight_style));
|
line = Line::from(Span::styled(&text, highlight_style));
|
||||||
} else {
|
} else {
|
||||||
line = Line::from(Span::styled(
|
line = Line::from(Span::styled(
|
||||||
text,
|
&text,
|
||||||
if is_active { normal_style_in_highlight } else { normal_style_outside }
|
if is_active { normal_style_in_highlight } else { normal_style_outside }
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -174,14 +238,13 @@ pub fn render_canvas(
|
|||||||
if is_active {
|
if is_active {
|
||||||
active_field_input_rect = Some(input_rows[i]);
|
active_field_input_rect = Some(input_rows[i]);
|
||||||
|
|
||||||
// --- CORRECTED CURSOR POSITIONING LOGIC ---
|
// Use the provided closure to check for display override
|
||||||
// Use the new generic trait method to check for an override.
|
let cursor_x = if has_display_override(i) {
|
||||||
let cursor_x = if form_state.has_display_override(i) {
|
|
||||||
// If an override exists, place the cursor at the end.
|
// If an override exists, place the cursor at the end.
|
||||||
input_rows[i].x + text.chars().count() as u16
|
input_rows[i].x + text.chars().count() as u16
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, use the real cursor position.
|
// Otherwise, use the real cursor position.
|
||||||
input_rows[i].x + form_state.current_cursor_pos() as u16
|
input_rows[i].x + current_cursor_pos as u16
|
||||||
};
|
};
|
||||||
let cursor_y = input_rows[i].y;
|
let cursor_y = input_rows[i].y;
|
||||||
f.set_cursor_position((cursor_x, cursor_y));
|
f.set_cursor_position((cursor_x, cursor_y));
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
// src/functions/modes/edit/form_e.rs
|
// src/functions/modes/edit/form_e.rs
|
||||||
|
|
||||||
use crate::services::grpc_client::GrpcClient;
|
use crate::services::grpc_client::GrpcClient;
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
|
||||||
use crate::state::pages::form::FormState;
|
use crate::state::pages::form::FormState;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::tui::functions::common::form::{revert, save};
|
use crate::tui::functions::common::form::{revert, save};
|
||||||
use crate::tui::functions::common::form::SaveOutcome;
|
use crate::tui::functions::common::form::SaveOutcome;
|
||||||
use crate::modes::handlers::event::EventOutcome;
|
use crate::modes::handlers::event::EventOutcome;
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
|
use canvas::CanvasState;
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/functions/modes/read_only/form_ro.rs
|
// src/functions/modes/read_only/form_ro.rs
|
||||||
|
|
||||||
use crate::config::binds::key_sequences::KeySequenceTracker;
|
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
use canvas::CanvasState;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ use crate::state::app::state::AppState;
|
|||||||
use crate::state::pages::admin::AdminState;
|
use crate::state::pages::admin::AdminState;
|
||||||
use crate::state::pages::{
|
use crate::state::pages::{
|
||||||
auth::{LoginState, RegisterState},
|
auth::{LoginState, RegisterState},
|
||||||
canvas_state::CanvasState,
|
|
||||||
form::FormState,
|
form::FormState,
|
||||||
};
|
};
|
||||||
|
use canvas::CanvasState;
|
||||||
|
use canvas::{CanvasAction, ActionDispatcher, ActionResult};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use common::proto::komp_ac::search::search_response::Hit;
|
use common::proto::komp_ac::search::search_response::Hit;
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
@@ -74,6 +75,57 @@ async fn trigger_form_autocomplete_search(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn handle_form_edit_with_canvas(
|
||||||
|
key_event: KeyEvent,
|
||||||
|
config: &Config,
|
||||||
|
form_state: &mut FormState,
|
||||||
|
ideal_cursor_column: &mut usize,
|
||||||
|
) -> Result<String> {
|
||||||
|
// Try canvas action from key first
|
||||||
|
if let Some(canvas_action) = CanvasAction::from_key(key_event.code) {
|
||||||
|
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
|
||||||
|
Ok(ActionResult::Success(msg)) => {
|
||||||
|
return Ok(msg.unwrap_or_default());
|
||||||
|
}
|
||||||
|
Ok(ActionResult::HandledByFeature(msg)) => {
|
||||||
|
return Ok(msg);
|
||||||
|
}
|
||||||
|
Ok(ActionResult::Error(msg)) => {
|
||||||
|
return Ok(format!("Error: {}", msg));
|
||||||
|
}
|
||||||
|
Ok(ActionResult::RequiresContext(msg)) => {
|
||||||
|
return Ok(format!("Context needed: {}", msg));
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Fall through to try config mapping
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try config-mapped action
|
||||||
|
if let Some(action_str) = config.get_edit_action_for_key(key_event.code, key_event.modifiers) {
|
||||||
|
let canvas_action = CanvasAction::from_string(&action_str);
|
||||||
|
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
|
||||||
|
Ok(ActionResult::Success(msg)) => {
|
||||||
|
return Ok(msg.unwrap_or_default());
|
||||||
|
}
|
||||||
|
Ok(ActionResult::HandledByFeature(msg)) => {
|
||||||
|
return Ok(msg);
|
||||||
|
}
|
||||||
|
Ok(ActionResult::Error(msg)) => {
|
||||||
|
return Ok(format!("Error: {}", msg));
|
||||||
|
}
|
||||||
|
Ok(ActionResult::RequiresContext(msg)) => {
|
||||||
|
return Ok(format!("Context needed: {}", msg));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Ok(format!("Action failed: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(String::new())
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn handle_edit_event(
|
pub async fn handle_edit_event(
|
||||||
@@ -118,7 +170,7 @@ pub async fn handle_edit_event(
|
|||||||
return Ok(EditEventOutcome::Message(String::new()));
|
return Ok(EditEventOutcome::Message(String::new()));
|
||||||
}
|
}
|
||||||
"exit" => {
|
"exit" => {
|
||||||
form_state.deactivate_autocomplete();
|
form_state.deactivate_suggestions();
|
||||||
return Ok(EditEventOutcome::Message(
|
return Ok(EditEventOutcome::Message(
|
||||||
"Autocomplete cancelled".to_string(),
|
"Autocomplete cancelled".to_string(),
|
||||||
));
|
));
|
||||||
@@ -150,14 +202,14 @@ pub async fn handle_edit_event(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 4. Finalize state
|
// 4. Finalize state
|
||||||
form_state.deactivate_autocomplete();
|
form_state.deactivate_suggestions();
|
||||||
form_state.set_has_unsaved_changes(true);
|
form_state.set_has_unsaved_changes(true);
|
||||||
return Ok(EditEventOutcome::Message(
|
return Ok(EditEventOutcome::Message(
|
||||||
"Selection made".to_string(),
|
"Selection made".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
form_state.deactivate_autocomplete();
|
form_state.deactivate_suggestions();
|
||||||
// Fall through to default 'enter' behavior
|
// Fall through to default 'enter' behavior
|
||||||
}
|
}
|
||||||
_ => {} // Let other keys fall through to the live search logic
|
_ => {} // Let other keys fall through to the live search logic
|
||||||
|
|||||||
@@ -10,9 +10,62 @@ use crate::state::pages::add_logic::AddLogicState;
|
|||||||
use crate::state::pages::add_table::AddTableState;
|
use crate::state::pages::add_table::AddTableState;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::functions::modes::read_only::{add_logic_ro, auth_ro, form_ro, add_table_ro};
|
use crate::functions::modes::read_only::{add_logic_ro, auth_ro, form_ro, add_table_ro};
|
||||||
|
use canvas::{CanvasAction, ActionDispatcher, ActionResult};
|
||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
pub async fn handle_form_readonly_with_canvas(
|
||||||
|
key_event: KeyEvent,
|
||||||
|
config: &Config,
|
||||||
|
form_state: &mut FormState,
|
||||||
|
ideal_cursor_column: &mut usize,
|
||||||
|
) -> Result<String> {
|
||||||
|
// Try canvas action from key first
|
||||||
|
if let Some(canvas_action) = CanvasAction::from_key(key_event.code) {
|
||||||
|
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
|
||||||
|
Ok(ActionResult::Success(msg)) => {
|
||||||
|
return Ok(msg.unwrap_or_default());
|
||||||
|
}
|
||||||
|
Ok(ActionResult::HandledByFeature(msg)) => {
|
||||||
|
return Ok(msg);
|
||||||
|
}
|
||||||
|
Ok(ActionResult::Error(msg)) => {
|
||||||
|
return Ok(format!("Error: {}", msg));
|
||||||
|
}
|
||||||
|
Ok(ActionResult::RequiresContext(msg)) => {
|
||||||
|
return Ok(format!("Context needed: {}", msg));
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Fall through to try config mapping
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try config-mapped action
|
||||||
|
if let Some(action_str) = config.get_read_only_action_for_key(key_event.code, key_event.modifiers) {
|
||||||
|
let canvas_action = CanvasAction::from_string(&action_str);
|
||||||
|
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
|
||||||
|
Ok(ActionResult::Success(msg)) => {
|
||||||
|
return Ok(msg.unwrap_or_default());
|
||||||
|
}
|
||||||
|
Ok(ActionResult::HandledByFeature(msg)) => {
|
||||||
|
return Ok(msg);
|
||||||
|
}
|
||||||
|
Ok(ActionResult::Error(msg)) => {
|
||||||
|
return Ok(format!("Error: {}", msg));
|
||||||
|
}
|
||||||
|
Ok(ActionResult::RequiresContext(msg)) => {
|
||||||
|
return Ok(format!("Context needed: {}", msg));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Ok(format!("Action failed: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(String::new())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_read_only_event(
|
pub async fn handle_read_only_event(
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
key: KeyEvent,
|
key: KeyEvent,
|
||||||
@@ -36,18 +89,46 @@ pub async fn handle_read_only_event(
|
|||||||
|
|
||||||
if config.is_enter_edit_mode_after(key.code, key.modifiers) {
|
if config.is_enter_edit_mode_after(key.code, key.modifiers) {
|
||||||
// Determine target state to adjust cursor
|
// Determine target state to adjust cursor
|
||||||
let target_state: &mut dyn CanvasState = if app_state.ui.show_login { login_state }
|
|
||||||
else if app_state.ui.show_add_logic { add_logic_state }
|
|
||||||
else if app_state.ui.show_register { register_state }
|
|
||||||
else if app_state.ui.show_add_table { add_table_state }
|
|
||||||
else { form_state };
|
|
||||||
let current_input = target_state.get_current_input();
|
|
||||||
let current_pos = target_state.current_cursor_pos();
|
|
||||||
|
|
||||||
if !current_input.is_empty() && current_pos < current_input.len() {
|
if app_state.ui.show_login {
|
||||||
target_state.set_current_cursor_pos(current_pos + 1);
|
let current_input = login_state.get_current_input();
|
||||||
*ideal_cursor_column = target_state.current_cursor_pos();
|
let current_pos = login_state.current_cursor_pos();
|
||||||
|
if !current_input.is_empty() && current_pos < current_input.len() {
|
||||||
|
login_state.set_current_cursor_pos(current_pos + 1);
|
||||||
|
*ideal_cursor_column = login_state.current_cursor_pos();
|
||||||
|
}
|
||||||
|
} else if app_state.ui.show_add_logic {
|
||||||
|
let current_input = add_logic_state.get_current_input();
|
||||||
|
let current_pos = add_logic_state.current_cursor_pos();
|
||||||
|
if !current_input.is_empty() && current_pos < current_input.len() {
|
||||||
|
add_logic_state.set_current_cursor_pos(current_pos + 1);
|
||||||
|
*ideal_cursor_column = add_logic_state.current_cursor_pos();
|
||||||
|
}
|
||||||
|
} else if app_state.ui.show_register {
|
||||||
|
let current_input = register_state.get_current_input();
|
||||||
|
let current_pos = register_state.current_cursor_pos();
|
||||||
|
if !current_input.is_empty() && current_pos < current_input.len() {
|
||||||
|
register_state.set_current_cursor_pos(current_pos + 1);
|
||||||
|
*ideal_cursor_column = register_state.current_cursor_pos();
|
||||||
|
}
|
||||||
|
} else if app_state.ui.show_add_table {
|
||||||
|
let current_input = add_table_state.get_current_input();
|
||||||
|
let current_pos = add_table_state.current_cursor_pos();
|
||||||
|
if !current_input.is_empty() && current_pos < current_input.len() {
|
||||||
|
add_table_state.set_current_cursor_pos(current_pos + 1);
|
||||||
|
*ideal_cursor_column = add_table_state.current_cursor_pos();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle FormState (uses library CanvasState)
|
||||||
|
use canvas::CanvasState as LibraryCanvasState; // Import at the top of the function
|
||||||
|
let current_input = form_state.get_current_input();
|
||||||
|
let current_pos = form_state.current_cursor_pos();
|
||||||
|
if !current_input.is_empty() && current_pos < current_input.len() {
|
||||||
|
form_state.set_current_cursor_pos(current_pos + 1);
|
||||||
|
*ideal_cursor_column = form_state.current_cursor_pos();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
*edit_mode_cooldown = true;
|
*edit_mode_cooldown = true;
|
||||||
*command_message = "Entering Edit mode (after cursor)".to_string();
|
*command_message = "Entering Edit mode (after cursor)".to_string();
|
||||||
return Ok((false, command_message.clone()));
|
return Ok((false, command_message.clone()));
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
// src/client/modes/handlers.rs
|
// src/modes/handlers.rs
|
||||||
pub mod event;
|
pub mod event;
|
||||||
|
pub mod event_helper;
|
||||||
pub mod mode_manager;
|
pub mod mode_manager;
|
||||||
|
|||||||
@@ -15,8 +15,12 @@ use crate::modes::{
|
|||||||
general::{dialog, navigation},
|
general::{dialog, navigation},
|
||||||
handlers::mode_manager::{AppMode, ModeManager},
|
handlers::mode_manager::{AppMode, ModeManager},
|
||||||
};
|
};
|
||||||
|
use crate::state::pages::canvas_state::CanvasState as LegacyCanvasState;
|
||||||
use crate::services::auth::AuthClient;
|
use crate::services::auth::AuthClient;
|
||||||
use crate::services::grpc_client::GrpcClient;
|
use crate::services::grpc_client::GrpcClient;
|
||||||
|
use canvas::{CanvasAction, ActionDispatcher, ActionResult};
|
||||||
|
use canvas::CanvasState as LibraryCanvasState;
|
||||||
|
use super::event_helper::*;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
app::{
|
app::{
|
||||||
buffer::{AppView, BufferState},
|
buffer::{AppView, BufferState},
|
||||||
@@ -573,55 +577,106 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AppMode::ReadOnly => {
|
AppMode::ReadOnly => {
|
||||||
if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode_linewise") && ModeManager::can_enter_highlight_mode(current_mode) {
|
// Handle highlight mode transitions
|
||||||
let current_field_index = if app_state.ui.show_login { login_state.current_field() } else if app_state.ui.show_register { register_state.current_field() } else { form_state.current_field() };
|
if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode_linewise")
|
||||||
self.highlight_state = HighlightState::Linewise { anchor_line: current_field_index };
|
&& ModeManager::can_enter_highlight_mode(current_mode)
|
||||||
|
{
|
||||||
|
let current_field_index = get_current_field_for_state(
|
||||||
|
app_state,
|
||||||
|
login_state,
|
||||||
|
register_state,
|
||||||
|
form_state
|
||||||
|
);
|
||||||
|
self.highlight_state = HighlightState::Linewise {
|
||||||
|
anchor_line: current_field_index
|
||||||
|
};
|
||||||
self.command_message = "-- LINE HIGHLIGHT --".to_string();
|
self.command_message = "-- LINE HIGHLIGHT --".to_string();
|
||||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
||||||
} else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode") && ModeManager::can_enter_highlight_mode(current_mode) {
|
}
|
||||||
let current_field_index = if app_state.ui.show_login { login_state.current_field() } else if app_state.ui.show_register { register_state.current_field() } else { form_state.current_field() };
|
else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode")
|
||||||
let current_cursor_pos = if app_state.ui.show_login { login_state.current_cursor_pos() } else if app_state.ui.show_register { register_state.current_cursor_pos() } else { form_state.current_cursor_pos() };
|
&& ModeManager::can_enter_highlight_mode(current_mode)
|
||||||
|
{
|
||||||
|
let current_field_index = get_current_field_for_state(
|
||||||
|
app_state,
|
||||||
|
login_state,
|
||||||
|
register_state,
|
||||||
|
form_state
|
||||||
|
);
|
||||||
|
let current_cursor_pos = get_current_cursor_pos_for_state(
|
||||||
|
app_state,
|
||||||
|
login_state,
|
||||||
|
register_state,
|
||||||
|
form_state
|
||||||
|
);
|
||||||
let anchor = (current_field_index, current_cursor_pos);
|
let anchor = (current_field_index, current_cursor_pos);
|
||||||
self.highlight_state = HighlightState::Characterwise { anchor };
|
self.highlight_state = HighlightState::Characterwise { anchor };
|
||||||
self.command_message = "-- HIGHLIGHT --".to_string();
|
self.command_message = "-- HIGHLIGHT --".to_string();
|
||||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
||||||
} else if config.get_read_only_action_for_key(key_code, modifiers).as_deref() == Some("enter_edit_mode_before") && ModeManager::can_enter_edit_mode(current_mode) {
|
}
|
||||||
|
|
||||||
|
// Handle edit mode transitions
|
||||||
|
else if config.get_read_only_action_for_key(key_code, modifiers).as_deref() == Some("enter_edit_mode_before")
|
||||||
|
&& ModeManager::can_enter_edit_mode(current_mode)
|
||||||
|
{
|
||||||
self.is_edit_mode = true;
|
self.is_edit_mode = true;
|
||||||
self.edit_mode_cooldown = true;
|
self.edit_mode_cooldown = true;
|
||||||
self.command_message = "Edit mode".to_string();
|
self.command_message = "Edit mode".to_string();
|
||||||
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
|
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
|
||||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
||||||
} else if config.get_read_only_action_for_key(key_code, modifiers).as_deref() == Some("enter_edit_mode_after") && ModeManager::can_enter_edit_mode(current_mode) {
|
}
|
||||||
let current_input = if app_state.ui.show_login || app_state.ui.show_register { login_state.get_current_input() } else { form_state.get_current_input() };
|
else if config.get_read_only_action_for_key(key_code, modifiers).as_deref() == Some("enter_edit_mode_after")
|
||||||
let current_cursor_pos = if app_state.ui.show_login || app_state.ui.show_register { login_state.current_cursor_pos() } else { form_state.current_cursor_pos() };
|
&& ModeManager::can_enter_edit_mode(current_mode)
|
||||||
|
{
|
||||||
|
let current_input = get_current_input_for_state(
|
||||||
|
app_state,
|
||||||
|
login_state,
|
||||||
|
register_state,
|
||||||
|
form_state
|
||||||
|
);
|
||||||
|
let current_cursor_pos = get_cursor_pos_for_mixed_state(
|
||||||
|
app_state,
|
||||||
|
login_state,
|
||||||
|
form_state
|
||||||
|
);
|
||||||
|
|
||||||
|
// Move cursor forward if possible
|
||||||
if !current_input.is_empty() && current_cursor_pos < current_input.len() {
|
if !current_input.is_empty() && current_cursor_pos < current_input.len() {
|
||||||
if app_state.ui.show_login || app_state.ui.show_register {
|
let new_cursor_pos = current_cursor_pos + 1;
|
||||||
login_state.set_current_cursor_pos(current_cursor_pos + 1);
|
set_current_cursor_pos_for_state(
|
||||||
self.ideal_cursor_column = login_state.current_cursor_pos();
|
app_state,
|
||||||
} else {
|
login_state,
|
||||||
form_state.set_current_cursor_pos(current_cursor_pos + 1);
|
register_state,
|
||||||
self.ideal_cursor_column = form_state.current_cursor_pos();
|
form_state,
|
||||||
}
|
new_cursor_pos
|
||||||
|
);
|
||||||
|
self.ideal_cursor_column = get_current_cursor_pos_for_state(
|
||||||
|
app_state,
|
||||||
|
login_state,
|
||||||
|
register_state,
|
||||||
|
form_state
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.is_edit_mode = true;
|
self.is_edit_mode = true;
|
||||||
self.edit_mode_cooldown = true;
|
self.edit_mode_cooldown = true;
|
||||||
app_state.ui.focus_outside_canvas = false;
|
app_state.ui.focus_outside_canvas = false;
|
||||||
self.command_message = "Edit mode (after cursor)".to_string();
|
self.command_message = "Edit mode (after cursor)".to_string();
|
||||||
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
|
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
|
||||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
||||||
} else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_command_mode") && ModeManager::can_enter_command_mode(current_mode) {
|
}
|
||||||
|
else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_command_mode")
|
||||||
|
&& ModeManager::can_enter_command_mode(current_mode)
|
||||||
|
{
|
||||||
self.command_mode = true;
|
self.command_mode = true;
|
||||||
self.command_input.clear();
|
self.command_input.clear();
|
||||||
self.command_message.clear();
|
self.command_message.clear();
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(action) =
|
// Handle common actions (save, quit, etc.)
|
||||||
config.get_common_action(key_code, modifiers)
|
if let Some(action) = config.get_common_action(key_code, modifiers) {
|
||||||
{
|
|
||||||
match action {
|
match action {
|
||||||
"save" | "force_quit" | "save_and_quit"
|
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
||||||
| "revert" => {
|
|
||||||
return common_mode::handle_core_action(
|
return common_mode::handle_core_action(
|
||||||
action,
|
action,
|
||||||
form_state,
|
form_state,
|
||||||
@@ -639,23 +694,36 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (_should_exit, message) =
|
// Try canvas action for form first (NEW: Canvas library integration)
|
||||||
read_only::handle_read_only_event(
|
if app_state.ui.show_form {
|
||||||
app_state,
|
if let Ok(Some(canvas_message)) = self.handle_form_canvas_action(
|
||||||
key_event,
|
key_event,
|
||||||
config,
|
config,
|
||||||
form_state,
|
form_state,
|
||||||
login_state,
|
false, // not edit mode
|
||||||
register_state,
|
).await {
|
||||||
&mut admin_state.add_table_state,
|
return Ok(EventOutcome::Ok(canvas_message));
|
||||||
&mut admin_state.add_logic_state,
|
}
|
||||||
&mut self.key_sequence_tracker,
|
}
|
||||||
&mut self.grpc_client, // <-- FIX 1
|
|
||||||
&mut self.command_message,
|
// Fallback to legacy read-only event handling
|
||||||
&mut self.edit_mode_cooldown,
|
let (_should_exit, message) = read_only::handle_read_only_event(
|
||||||
&mut self.ideal_cursor_column,
|
app_state,
|
||||||
)
|
key_event,
|
||||||
.await?;
|
config,
|
||||||
|
form_state,
|
||||||
|
login_state,
|
||||||
|
register_state,
|
||||||
|
&mut admin_state.add_table_state,
|
||||||
|
&mut admin_state.add_logic_state,
|
||||||
|
&mut self.key_sequence_tracker,
|
||||||
|
&mut self.grpc_client,
|
||||||
|
&mut self.command_message,
|
||||||
|
&mut self.edit_mode_cooldown,
|
||||||
|
&mut self.ideal_cursor_column,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
return Ok(EventOutcome::Ok(message));
|
return Ok(EventOutcome::Ok(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,12 +763,10 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AppMode::Edit => {
|
AppMode::Edit => {
|
||||||
if let Some(action) =
|
// Handle common actions (save, quit, etc.)
|
||||||
config.get_common_action(key_code, modifiers)
|
if let Some(action) = config.get_common_action(key_code, modifiers) {
|
||||||
{
|
|
||||||
match action {
|
match action {
|
||||||
"save" | "force_quit" | "save_and_quit"
|
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
||||||
| "revert" => {
|
|
||||||
return common_mode::handle_core_action(
|
return common_mode::handle_core_action(
|
||||||
action,
|
action,
|
||||||
form_state,
|
form_state,
|
||||||
@@ -718,9 +784,25 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try canvas action for form first (NEW: Canvas library integration)
|
||||||
|
if app_state.ui.show_form {
|
||||||
|
if let Ok(Some(canvas_message)) = self.handle_form_canvas_action(
|
||||||
|
key_event,
|
||||||
|
config,
|
||||||
|
form_state,
|
||||||
|
true, // edit mode
|
||||||
|
).await {
|
||||||
|
if !canvas_message.is_empty() {
|
||||||
|
self.command_message = canvas_message.clone();
|
||||||
|
}
|
||||||
|
return Ok(EventOutcome::Ok(canvas_message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle legacy edit events
|
||||||
let mut current_position = form_state.current_position;
|
let mut current_position = form_state.current_position;
|
||||||
let total_count = form_state.total_count;
|
let total_count = form_state.total_count;
|
||||||
// --- MODIFIED: Pass `self` instead of `grpc_client` ---
|
|
||||||
let edit_result = edit::handle_edit_event(
|
let edit_result = edit::handle_edit_event(
|
||||||
key_event,
|
key_event,
|
||||||
config,
|
config,
|
||||||
@@ -739,30 +821,62 @@ impl EventHandler {
|
|||||||
Ok(edit::EditEventOutcome::ExitEditMode) => {
|
Ok(edit::EditEventOutcome::ExitEditMode) => {
|
||||||
self.is_edit_mode = false;
|
self.is_edit_mode = false;
|
||||||
self.edit_mode_cooldown = true;
|
self.edit_mode_cooldown = true;
|
||||||
let has_changes = if app_state.ui.show_login { login_state.has_unsaved_changes() } else if app_state.ui.show_register { register_state.has_unsaved_changes() } else { form_state.has_unsaved_changes() };
|
|
||||||
self.command_message = if has_changes { "Exited edit mode (unsaved changes remain)".to_string() } else { "Read-only mode".to_string() };
|
// Check for unsaved changes across all states
|
||||||
|
let has_changes = get_has_unsaved_changes_for_state(
|
||||||
|
app_state,
|
||||||
|
login_state,
|
||||||
|
register_state,
|
||||||
|
form_state
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set appropriate message based on changes
|
||||||
|
self.command_message = if has_changes {
|
||||||
|
"Exited edit mode (unsaved changes remain)".to_string()
|
||||||
|
} else {
|
||||||
|
"Read-only mode".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
|
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
|
||||||
let current_input = if app_state.ui.show_login { login_state.get_current_input() } else if app_state.ui.show_register { register_state.get_current_input() } else { form_state.get_current_input() };
|
|
||||||
let current_cursor_pos = if app_state.ui.show_login { login_state.current_cursor_pos() } else if app_state.ui.show_register { register_state.current_cursor_pos() } else { form_state.current_cursor_pos() };
|
// Get current input and cursor position
|
||||||
|
let current_input = get_current_input_for_state(
|
||||||
|
app_state,
|
||||||
|
login_state,
|
||||||
|
register_state,
|
||||||
|
form_state
|
||||||
|
);
|
||||||
|
let current_cursor_pos = get_current_cursor_pos_for_state(
|
||||||
|
app_state,
|
||||||
|
login_state,
|
||||||
|
register_state,
|
||||||
|
form_state
|
||||||
|
);
|
||||||
|
|
||||||
|
// Adjust cursor if it's beyond the input length
|
||||||
if !current_input.is_empty() && current_cursor_pos >= current_input.len() {
|
if !current_input.is_empty() && current_cursor_pos >= current_input.len() {
|
||||||
let new_pos = current_input.len() - 1;
|
let new_pos = current_input.len() - 1;
|
||||||
let target_state: &mut dyn CanvasState = if app_state.ui.show_login { login_state } else if app_state.ui.show_register { register_state } else { form_state };
|
set_current_cursor_pos_for_state(
|
||||||
target_state.set_current_cursor_pos(new_pos);
|
app_state,
|
||||||
|
login_state,
|
||||||
|
register_state,
|
||||||
|
form_state,
|
||||||
|
new_pos
|
||||||
|
);
|
||||||
self.ideal_cursor_column = new_pos;
|
self.ideal_cursor_column = new_pos;
|
||||||
}
|
}
|
||||||
return Ok(EventOutcome::Ok(
|
|
||||||
self.command_message.clone(),
|
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(edit::EditEventOutcome::Message(msg)) => {
|
Ok(edit::EditEventOutcome::Message(msg)) => {
|
||||||
if !msg.is_empty() {
|
if !msg.is_empty() {
|
||||||
self.command_message = msg;
|
self.command_message = msg;
|
||||||
}
|
}
|
||||||
self.key_sequence_tracker.reset();
|
self.key_sequence_tracker.reset();
|
||||||
return Ok(EventOutcome::Ok(
|
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
||||||
self.command_message.clone(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
@@ -906,4 +1020,96 @@ impl EventHandler {
|
|||||||
fn is_processed_command(&self, command: &str) -> bool {
|
fn is_processed_command(&self, command: &str) -> bool {
|
||||||
matches!(command, "w" | "q" | "q!" | "wq" | "r")
|
matches!(command, "w" | "q" | "q!" | "wq" | "r")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_form_canvas_action(
|
||||||
|
&mut self,
|
||||||
|
key_event: KeyEvent,
|
||||||
|
config: &Config,
|
||||||
|
form_state: &mut FormState,
|
||||||
|
is_edit_mode: bool,
|
||||||
|
) -> Result<Option<String>> {
|
||||||
|
// Handle suggestion actions first if suggestions are active
|
||||||
|
if form_state.autocomplete_active {
|
||||||
|
match key_event.code {
|
||||||
|
KeyCode::Up => {
|
||||||
|
if let Ok(result) = ActionDispatcher::dispatch(
|
||||||
|
CanvasAction::SuggestionUp,
|
||||||
|
form_state,
|
||||||
|
&mut self.ideal_cursor_column,
|
||||||
|
).await {
|
||||||
|
return Ok(result.message().map(|s| s.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
if let Ok(result) = ActionDispatcher::dispatch(
|
||||||
|
CanvasAction::SuggestionDown,
|
||||||
|
form_state,
|
||||||
|
&mut self.ideal_cursor_column,
|
||||||
|
).await {
|
||||||
|
return Ok(result.message().map(|s| s.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
if let Ok(result) = ActionDispatcher::dispatch(
|
||||||
|
CanvasAction::SelectSuggestion,
|
||||||
|
form_state,
|
||||||
|
&mut self.ideal_cursor_column,
|
||||||
|
).await {
|
||||||
|
return Ok(result.message().map(|s| s.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Esc => {
|
||||||
|
if let Ok(result) = ActionDispatcher::dispatch(
|
||||||
|
CanvasAction::ExitSuggestions,
|
||||||
|
form_state,
|
||||||
|
&mut self.ideal_cursor_column,
|
||||||
|
).await {
|
||||||
|
return Ok(result.message().map(|s| s.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to create canvas action from key
|
||||||
|
if let Some(canvas_action) = CanvasAction::from_key(key_event.code) {
|
||||||
|
match ActionDispatcher::dispatch(
|
||||||
|
canvas_action,
|
||||||
|
form_state,
|
||||||
|
&mut self.ideal_cursor_column,
|
||||||
|
).await {
|
||||||
|
Ok(result) => {
|
||||||
|
return Ok(result.message().map(|s| s.to_string()));
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Fall through to try config mapping
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try mapped actions from config
|
||||||
|
let action_str = if is_edit_mode {
|
||||||
|
config.get_edit_action_for_key(key_event.code, key_event.modifiers)
|
||||||
|
} else {
|
||||||
|
config.get_read_only_action_for_key(key_event.code, key_event.modifiers)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(action_str) = action_str {
|
||||||
|
let canvas_action = CanvasAction::from_string(&action_str);
|
||||||
|
match ActionDispatcher::dispatch(
|
||||||
|
canvas_action,
|
||||||
|
form_state,
|
||||||
|
&mut self.ideal_cursor_column,
|
||||||
|
).await {
|
||||||
|
Ok(result) => {
|
||||||
|
return Ok(result.message().map(|s| s.to_string()));
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Ignore error, let existing code handle it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
105
client/src/modes/handlers/event_helper.rs
Normal file
105
client/src/modes/handlers/event_helper.rs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
|
||||||
|
// src/modes/handlers/event_helper.rs
|
||||||
|
//! Helper functions to handle the differences between legacy and library CanvasState traits
|
||||||
|
|
||||||
|
use crate::state::app::state::AppState;
|
||||||
|
use crate::state::pages::{
|
||||||
|
form::FormState,
|
||||||
|
auth::{LoginState, RegisterState},
|
||||||
|
};
|
||||||
|
use crate::state::pages::canvas_state::CanvasState as LegacyCanvasState;
|
||||||
|
use canvas::CanvasState as LibraryCanvasState;
|
||||||
|
|
||||||
|
/// Get the current field index from the appropriate state based on which UI is active
|
||||||
|
pub fn get_current_field_for_state(
|
||||||
|
app_state: &AppState,
|
||||||
|
login_state: &LoginState,
|
||||||
|
register_state: &RegisterState,
|
||||||
|
form_state: &FormState,
|
||||||
|
) -> usize {
|
||||||
|
if app_state.ui.show_login {
|
||||||
|
login_state.current_field() // Uses LegacyCanvasState
|
||||||
|
} else if app_state.ui.show_register {
|
||||||
|
register_state.current_field() // Uses LegacyCanvasState
|
||||||
|
} else {
|
||||||
|
form_state.current_field() // Uses LibraryCanvasState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current cursor position from the appropriate state based on which UI is active
|
||||||
|
pub fn get_current_cursor_pos_for_state(
|
||||||
|
app_state: &AppState,
|
||||||
|
login_state: &LoginState,
|
||||||
|
register_state: &RegisterState,
|
||||||
|
form_state: &FormState,
|
||||||
|
) -> usize {
|
||||||
|
if app_state.ui.show_login {
|
||||||
|
login_state.current_cursor_pos() // Uses LegacyCanvasState
|
||||||
|
} else if app_state.ui.show_register {
|
||||||
|
register_state.current_cursor_pos() // Uses LegacyCanvasState
|
||||||
|
} else {
|
||||||
|
form_state.current_cursor_pos() // Uses LibraryCanvasState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the appropriate state has unsaved changes based on which UI is active
|
||||||
|
pub fn get_has_unsaved_changes_for_state(
|
||||||
|
app_state: &AppState,
|
||||||
|
login_state: &LoginState,
|
||||||
|
register_state: &RegisterState,
|
||||||
|
form_state: &FormState,
|
||||||
|
) -> bool {
|
||||||
|
if app_state.ui.show_login {
|
||||||
|
login_state.has_unsaved_changes() // Uses LegacyCanvasState
|
||||||
|
} else if app_state.ui.show_register {
|
||||||
|
register_state.has_unsaved_changes() // Uses LegacyCanvasState
|
||||||
|
} else {
|
||||||
|
form_state.has_unsaved_changes() // Uses LibraryCanvasState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current input from the appropriate state based on which UI is active
|
||||||
|
pub fn get_current_input_for_state<'a>(
|
||||||
|
app_state: &AppState,
|
||||||
|
login_state: &'a LoginState,
|
||||||
|
register_state: &'a RegisterState,
|
||||||
|
form_state: &'a FormState,
|
||||||
|
) -> &'a str {
|
||||||
|
if app_state.ui.show_login {
|
||||||
|
login_state.get_current_input() // Uses LegacyCanvasState
|
||||||
|
} else if app_state.ui.show_register {
|
||||||
|
register_state.get_current_input() // Uses LegacyCanvasState
|
||||||
|
} else {
|
||||||
|
form_state.get_current_input() // Uses LibraryCanvasState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the cursor position for the appropriate state based on which UI is active
|
||||||
|
pub fn set_current_cursor_pos_for_state(
|
||||||
|
app_state: &AppState,
|
||||||
|
login_state: &mut LoginState,
|
||||||
|
register_state: &mut RegisterState,
|
||||||
|
form_state: &mut FormState,
|
||||||
|
pos: usize,
|
||||||
|
) {
|
||||||
|
if app_state.ui.show_login {
|
||||||
|
login_state.set_current_cursor_pos(pos); // Uses LegacyCanvasState
|
||||||
|
} else if app_state.ui.show_register {
|
||||||
|
register_state.set_current_cursor_pos(pos); // Uses LegacyCanvasState
|
||||||
|
} else {
|
||||||
|
form_state.set_current_cursor_pos(pos); // Uses LibraryCanvasState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get cursor position for mixed login/register vs form logic
|
||||||
|
pub fn get_cursor_pos_for_mixed_state(
|
||||||
|
app_state: &AppState,
|
||||||
|
login_state: &LoginState,
|
||||||
|
form_state: &FormState,
|
||||||
|
) -> usize {
|
||||||
|
if app_state.ui.show_login || app_state.ui.show_register {
|
||||||
|
login_state.current_cursor_pos() // Uses LegacyCanvasState
|
||||||
|
} else {
|
||||||
|
form_state.current_cursor_pos() // Uses LibraryCanvasState
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::state::app::highlight::HighlightState;
|
use crate::state::app::highlight::HighlightState;
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
use canvas::{CanvasState, CanvasAction, ActionContext}; // CHANGED: Use canvas crate
|
||||||
use common::proto::komp_ac::search::search_response::Hit;
|
use common::proto::komp_ac::search::search_response::Hit;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
@@ -146,7 +146,7 @@ impl FormState {
|
|||||||
} else {
|
} else {
|
||||||
self.current_position = 1;
|
self.current_position = 1;
|
||||||
}
|
}
|
||||||
self.deactivate_autocomplete();
|
self.deactivate_suggestions(); // CHANGED: Use canvas trait method
|
||||||
self.link_display_map.clear();
|
self.link_display_map.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,14 +205,25 @@ impl FormState {
|
|||||||
self.has_unsaved_changes = false;
|
self.has_unsaved_changes = false;
|
||||||
self.current_field = 0;
|
self.current_field = 0;
|
||||||
self.current_cursor_pos = 0;
|
self.current_cursor_pos = 0;
|
||||||
self.deactivate_autocomplete();
|
self.deactivate_suggestions(); // CHANGED: Use canvas trait method
|
||||||
self.link_display_map.clear();
|
self.link_display_map.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deactivate_autocomplete(&mut self) {
|
// REMOVED: deactivate_autocomplete() - now using trait method
|
||||||
self.autocomplete_active = false;
|
|
||||||
self.autocomplete_suggestions.clear();
|
// NEW: Keep the rich suggestions methods for compatibility
|
||||||
self.selected_suggestion_index = None;
|
pub fn get_rich_suggestions(&self) -> Option<&[Hit]> {
|
||||||
|
if self.autocomplete_active {
|
||||||
|
Some(&self.autocomplete_suggestions)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_rich_suggestions(&mut self, suggestions: Vec<Hit>) {
|
||||||
|
self.autocomplete_suggestions = suggestions;
|
||||||
|
self.autocomplete_active = !self.autocomplete_suggestions.is_empty();
|
||||||
|
self.selected_suggestion_index = if self.autocomplete_active { Some(0) } else { None };
|
||||||
self.autocomplete_loading = false;
|
self.autocomplete_loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,49 +232,54 @@ impl CanvasState for FormState {
|
|||||||
fn current_field(&self) -> usize {
|
fn current_field(&self) -> usize {
|
||||||
self.current_field
|
self.current_field
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_cursor_pos(&self) -> usize {
|
fn current_cursor_pos(&self) -> usize {
|
||||||
self.current_cursor_pos
|
self.current_cursor_pos
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_unsaved_changes(&self) -> bool {
|
fn has_unsaved_changes(&self) -> bool {
|
||||||
self.has_unsaved_changes
|
self.has_unsaved_changes
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inputs(&self) -> Vec<&String> {
|
fn inputs(&self) -> Vec<&String> {
|
||||||
self.values.iter().collect()
|
self.values.iter().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_current_input(&self) -> &str {
|
fn get_current_input(&self) -> &str {
|
||||||
FormState::get_current_input(self)
|
FormState::get_current_input(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_current_input_mut(&mut self) -> &mut String {
|
fn get_current_input_mut(&mut self) -> &mut String {
|
||||||
FormState::get_current_input_mut(self)
|
FormState::get_current_input_mut(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fields(&self) -> Vec<&str> {
|
fn fields(&self) -> Vec<&str> {
|
||||||
self.fields
|
self.fields
|
||||||
.iter()
|
.iter()
|
||||||
.map(|f| f.display_name.as_str())
|
.map(|f| f.display_name.as_str())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_current_field(&mut self, index: usize) {
|
fn set_current_field(&mut self, index: usize) {
|
||||||
if index < self.fields.len() {
|
if index < self.fields.len() {
|
||||||
self.current_field = index;
|
self.current_field = index;
|
||||||
}
|
}
|
||||||
self.deactivate_autocomplete();
|
self.deactivate_suggestions(); // CHANGED: Use canvas trait method
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_current_cursor_pos(&mut self, pos: usize) {
|
fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||||
self.current_cursor_pos = pos;
|
self.current_cursor_pos = pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||||
self.has_unsaved_changes = changed;
|
self.has_unsaved_changes = changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- CANVAS CRATE SUGGESTIONS SUPPORT ---
|
||||||
fn get_suggestions(&self) -> Option<&[String]> {
|
fn get_suggestions(&self) -> Option<&[String]> {
|
||||||
None
|
None // We use rich suggestions instead
|
||||||
}
|
|
||||||
fn get_rich_suggestions(&self) -> Option<&[Hit]> {
|
|
||||||
if self.autocomplete_active {
|
|
||||||
Some(&self.autocomplete_suggestions)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
||||||
if self.autocomplete_active {
|
if self.autocomplete_active {
|
||||||
self.selected_suggestion_index
|
self.selected_suggestion_index
|
||||||
@@ -272,6 +288,52 @@ impl CanvasState for FormState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_selected_suggestion_index(&mut self, index: Option<usize>) {
|
||||||
|
if self.autocomplete_active {
|
||||||
|
self.selected_suggestion_index = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn activate_suggestions(&mut self, suggestions: Vec<String>) {
|
||||||
|
// For compatibility - convert to rich format if needed
|
||||||
|
self.autocomplete_active = true;
|
||||||
|
self.selected_suggestion_index = if suggestions.is_empty() { None } else { Some(0) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deactivate_suggestions(&mut self) {
|
||||||
|
self.autocomplete_active = false;
|
||||||
|
self.autocomplete_suggestions.clear();
|
||||||
|
self.selected_suggestion_index = None;
|
||||||
|
self.autocomplete_loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FEATURE-SPECIFIC ACTION HANDLING ---
|
||||||
|
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||||
|
match action {
|
||||||
|
CanvasAction::SelectSuggestion => {
|
||||||
|
if let Some(selected_idx) = self.selected_suggestion_index {
|
||||||
|
if let Some(hit) = self.autocomplete_suggestions.get(selected_idx).cloned() { // ADD .cloned()
|
||||||
|
// Extract the value from the selected suggestion
|
||||||
|
if let Ok(content_map) = serde_json::from_str::<HashMap<String, serde_json::Value>>(&hit.content_json) {
|
||||||
|
let current_field_def = &self.fields[self.current_field];
|
||||||
|
if let Some(value) = content_map.get(¤t_field_def.data_key) {
|
||||||
|
let new_value = json_value_to_string(value);
|
||||||
|
let display_name = self.get_display_name_for_hit(&hit); // Calculate first
|
||||||
|
*self.get_current_input_mut() = new_value.clone();
|
||||||
|
self.set_current_cursor_pos(new_value.len());
|
||||||
|
self.set_has_unsaved_changes(true);
|
||||||
|
self.deactivate_suggestions();
|
||||||
|
return Some(format!("Selected: {}", display_name)); // Use calculated value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
_ => None, // Let canvas handle other actions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_display_value_for_field(&self, index: usize) -> &str {
|
fn get_display_value_for_field(&self, index: usize) -> &str {
|
||||||
if let Some(display_text) = self.link_display_map.get(&index) {
|
if let Some(display_text) = self.link_display_map.get(&index) {
|
||||||
return display_text.as_str();
|
return display_text.as_str();
|
||||||
@@ -282,7 +344,6 @@ impl CanvasState for FormState {
|
|||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- IMPLEMENT THE NEW TRAIT METHOD ---
|
|
||||||
fn has_display_override(&self, index: usize) -> bool {
|
fn has_display_override(&self, index: usize) -> bool {
|
||||||
self.link_display_map.contains_key(&index)
|
self.link_display_map.contains_key(&index)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/tui/functions/form.rs
|
// src/tui/functions/form.rs
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
|
||||||
use crate::state::pages::form::FormState;
|
use crate::state::pages::form::FormState;
|
||||||
use crate::services::grpc_client::GrpcClient;
|
use crate::services::grpc_client::GrpcClient;
|
||||||
|
use canvas::CanvasState;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
|
||||||
pub async fn handle_action(
|
pub async fn handle_action(
|
||||||
|
|||||||
Reference in New Issue
Block a user