example with debug stuff
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
// examples/canvas_gui_demo.rs
|
||||
|
||||
use std::io;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
@@ -9,225 +8,456 @@ use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Clear},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
|
||||
// Import canvas library components
|
||||
use canvas::{
|
||||
canvas::{
|
||||
state::{CanvasState, ActionContext},
|
||||
gui::render_canvas,
|
||||
theme::Theme,
|
||||
modes::{AppMode, HighlightState, ModeManager},
|
||||
state::{ActionContext, CanvasState},
|
||||
theme::CanvasTheme,
|
||||
},
|
||||
autocomplete::{
|
||||
state::AutocompleteCanvasState,
|
||||
gui::render_autocomplete,
|
||||
types::{AutocompleteState, SuggestionItem},
|
||||
},
|
||||
config::config::CanvasConfig,
|
||||
config::CanvasConfig,
|
||||
dispatcher::ActionDispatcher,
|
||||
CanvasAction,
|
||||
};
|
||||
|
||||
// Example form data structure
|
||||
#[derive(Debug)]
|
||||
struct LoginForm {
|
||||
fields: Vec<String>,
|
||||
field_labels: Vec<String>,
|
||||
current_field: usize,
|
||||
cursor_position: usize,
|
||||
autocomplete_state: AutocompleteState,
|
||||
// Simple theme implementation
|
||||
#[derive(Clone)]
|
||||
struct DemoTheme;
|
||||
|
||||
impl CanvasTheme for DemoTheme {
|
||||
fn bg(&self) -> Color { Color::Reset }
|
||||
fn fg(&self) -> Color { Color::White }
|
||||
fn accent(&self) -> Color { Color::Cyan }
|
||||
fn secondary(&self) -> Color { Color::Gray }
|
||||
fn highlight(&self) -> Color { Color::Yellow }
|
||||
fn highlight_bg(&self) -> Color { Color::DarkGray }
|
||||
fn warning(&self) -> Color { Color::Red }
|
||||
fn border(&self) -> Color { Color::Gray }
|
||||
}
|
||||
|
||||
impl LoginForm {
|
||||
// Demo form state
|
||||
struct DemoFormState {
|
||||
fields: Vec<String>,
|
||||
field_names: Vec<String>,
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
mode: AppMode,
|
||||
highlight_state: HighlightState,
|
||||
has_changes: bool,
|
||||
ideal_cursor_column: usize,
|
||||
last_action: Option<String>,
|
||||
debug_message: String,
|
||||
}
|
||||
|
||||
impl DemoFormState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
fields: vec![
|
||||
String::new(), // username
|
||||
String::new(), // password
|
||||
String::new(), // email
|
||||
"John Doe".to_string(), // Name - has words to test with
|
||||
"john.doe@example.com".to_string(), // Email - has punctuation
|
||||
"+1 234 567 8900".to_string(), // Phone - has spaces and numbers
|
||||
"123 Main Street Apt 4B".to_string(), // Address - multiple words
|
||||
"San Francisco".to_string(), // City - two words
|
||||
"This is a test comment with multiple words".to_string(), // Comments - lots of words
|
||||
],
|
||||
field_labels: vec![
|
||||
"Username".to_string(),
|
||||
"Password".to_string(),
|
||||
field_names: vec![
|
||||
"Name".to_string(),
|
||||
"Email".to_string(),
|
||||
"Phone".to_string(),
|
||||
"Address".to_string(),
|
||||
"City".to_string(),
|
||||
"Comments".to_string(),
|
||||
],
|
||||
current_field: 0,
|
||||
cursor_position: 0,
|
||||
autocomplete_state: AutocompleteState::default(),
|
||||
cursor_pos: 0,
|
||||
mode: AppMode::ReadOnly,
|
||||
highlight_state: HighlightState::Off,
|
||||
has_changes: false,
|
||||
ideal_cursor_column: 0,
|
||||
last_action: None,
|
||||
debug_message: "Ready - Form loaded with sample data".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_edit_mode(&mut self) {
|
||||
if ModeManager::can_enter_edit_mode(self.mode) {
|
||||
self.mode = AppMode::Edit;
|
||||
self.debug_message = "Entered EDIT mode".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_readonly_mode(&mut self) {
|
||||
if ModeManager::can_enter_read_only_mode(self.mode) {
|
||||
self.mode = AppMode::ReadOnly;
|
||||
self.highlight_state = HighlightState::Off;
|
||||
self.debug_message = "Entered READ-ONLY mode".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_highlight_mode(&mut self) {
|
||||
if ModeManager::can_enter_highlight_mode(self.mode) {
|
||||
self.mode = AppMode::Highlight;
|
||||
self.highlight_state = HighlightState::Characterwise {
|
||||
anchor: (self.current_field, self.cursor_pos),
|
||||
};
|
||||
self.debug_message = "Entered VISUAL mode".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
fn log_action(&mut self, action: &str) {
|
||||
self.last_action = Some(action.to_string());
|
||||
self.debug_message = format!("Action: {}", action);
|
||||
}
|
||||
}
|
||||
|
||||
// Implement CanvasState trait for your form
|
||||
impl CanvasState for LoginForm {
|
||||
fn field_count(&self) -> usize {
|
||||
self.fields.len()
|
||||
}
|
||||
|
||||
impl CanvasState for DemoFormState {
|
||||
fn current_field(&self) -> usize {
|
||||
self.current_field
|
||||
}
|
||||
|
||||
fn set_current_field(&mut self, field_index: usize) {
|
||||
if field_index < self.fields.len() {
|
||||
self.current_field = field_index;
|
||||
}
|
||||
fn current_cursor_pos(&self) -> usize {
|
||||
self.cursor_pos
|
||||
}
|
||||
|
||||
fn cursor_position(&self) -> usize {
|
||||
self.cursor_position
|
||||
fn set_current_field(&mut self, index: usize) {
|
||||
self.current_field = index.min(self.fields.len().saturating_sub(1));
|
||||
// Reset cursor to end of field when switching
|
||||
self.cursor_pos = self.fields[self.current_field].len();
|
||||
}
|
||||
|
||||
fn set_cursor_position(&mut self, position: usize) {
|
||||
if let Some(field) = self.fields.get(self.current_field) {
|
||||
self.cursor_position = position.min(field.len());
|
||||
}
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||
let max_pos = self.fields[self.current_field].len();
|
||||
self.cursor_pos = pos.min(max_pos);
|
||||
}
|
||||
|
||||
fn field_value(&self, field_index: usize) -> Option<&str> {
|
||||
self.fields.get(field_index).map(|s| s.as_str())
|
||||
fn current_mode(&self) -> AppMode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
fn set_field_value(&mut self, field_index: usize, value: String) {
|
||||
if let Some(field) = self.fields.get_mut(field_index) {
|
||||
*field = value;
|
||||
}
|
||||
fn get_current_input(&self) -> &str {
|
||||
&self.fields[self.current_field]
|
||||
}
|
||||
|
||||
fn field_label(&self, field_index: usize) -> Option<&str> {
|
||||
self.field_labels.get(field_index).map(|s| s.as_str())
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
&mut self.fields[self.current_field]
|
||||
}
|
||||
|
||||
fn handle_action(&mut self, _action: &str, _context: ActionContext) -> Result<(), Box<dyn Error>> {
|
||||
// Custom action handling can go here
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Implement autocomplete support
|
||||
impl AutocompleteCanvasState for LoginForm {
|
||||
type SuggestionData = String;
|
||||
|
||||
fn supports_autocomplete(&self, field_index: usize) -> bool {
|
||||
// Only username and email fields support autocomplete
|
||||
field_index == 0 || field_index == 2
|
||||
fn inputs(&self) -> Vec<&String> {
|
||||
self.fields.iter().collect()
|
||||
}
|
||||
|
||||
fn autocomplete_state(&self) -> &AutocompleteState {
|
||||
&self.autocomplete_state
|
||||
fn fields(&self) -> Vec<&str> {
|
||||
self.field_names.iter().map(|s| s.as_str()).collect()
|
||||
}
|
||||
|
||||
fn autocomplete_state_mut(&mut self) -> &mut AutocompleteState {
|
||||
&mut self.autocomplete_state
|
||||
fn has_unsaved_changes(&self) -> bool {
|
||||
self.has_changes
|
||||
}
|
||||
|
||||
fn activate_autocomplete(&mut self) {
|
||||
if self.supports_autocomplete(self.current_field) {
|
||||
self.autocomplete_state.activate(self.current_field);
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
self.has_changes = changed;
|
||||
}
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
// FOCUS: Debug specifically for 'w' key (move_word_next)
|
||||
if let CanvasAction::MoveWordNext = action {
|
||||
let current_input = self.get_current_input();
|
||||
let old_cursor = self.cursor_pos;
|
||||
self.debug_message = format!("🔍 MoveWordNext: cursor {} -> text '{}' (len {})",
|
||||
old_cursor, current_input, current_input.len());
|
||||
|
||||
// Simulate loading suggestions
|
||||
let suggestions = match self.current_field {
|
||||
0 => vec![ // Username suggestions
|
||||
SuggestionItem::simple("admin"),
|
||||
SuggestionItem::simple("user"),
|
||||
SuggestionItem::simple("guest"),
|
||||
],
|
||||
2 => vec![ // Email suggestions
|
||||
SuggestionItem::simple("user@example.com"),
|
||||
SuggestionItem::simple("admin@domain.com"),
|
||||
SuggestionItem::simple("test@test.org"),
|
||||
],
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
self.autocomplete_state.set_suggestions(suggestions);
|
||||
// Return None to let the handler process it, but we'll see this debug message
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_autocomplete_selection(&mut self) {
|
||||
if let Some(suggestion) = self.autocomplete_state.selected_suggestion() {
|
||||
self.set_field_value(self.current_field, suggestion.insert_value.clone());
|
||||
self.cursor_position = suggestion.insert_value.len();
|
||||
self.autocomplete_state.deactivate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Simple theme implementation
|
||||
struct SimpleTheme;
|
||||
|
||||
impl Theme for SimpleTheme {
|
||||
fn field_style(&self, is_current: bool, _is_highlighted: bool) -> Style {
|
||||
if is_current {
|
||||
Style::default().bg(Color::DarkGray).fg(Color::White)
|
||||
} else {
|
||||
Style::default().fg(Color::Gray)
|
||||
}
|
||||
}
|
||||
|
||||
fn label_style(&self, is_current: bool) -> Style {
|
||||
if is_current {
|
||||
Style::default().fg(Color::Cyan)
|
||||
} else {
|
||||
Style::default().fg(Color::Blue)
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_style(&self) -> Style {
|
||||
Style::default().bg(Color::White).fg(Color::Black)
|
||||
}
|
||||
}
|
||||
|
||||
struct App {
|
||||
form: LoginForm,
|
||||
config: CanvasConfig,
|
||||
dispatcher: ActionDispatcher,
|
||||
theme: SimpleTheme,
|
||||
should_quit: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> Result<Self, Box<dyn Error>> {
|
||||
Ok(App {
|
||||
form: LoginForm::new(),
|
||||
config: CanvasConfig::default(),
|
||||
dispatcher: ActionDispatcher::new(),
|
||||
theme: SimpleTheme,
|
||||
should_quit: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: KeyCode) -> Result<(), Box<dyn Error>> {
|
||||
match key {
|
||||
KeyCode::Char('q') | KeyCode::Esc => {
|
||||
self.should_quit = true;
|
||||
}
|
||||
KeyCode::Tab => {
|
||||
// Activate autocomplete on tab
|
||||
self.form.activate_autocomplete();
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Apply autocomplete selection or just move to next field
|
||||
if self.form.autocomplete_state().is_ready() {
|
||||
self.form.apply_autocomplete_selection();
|
||||
} else {
|
||||
let next_field = (self.form.current_field() + 1) % self.form.field_count();
|
||||
self.form.set_current_field(next_field);
|
||||
self.form.set_cursor_position(0);
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => {
|
||||
match cmd.as_str() {
|
||||
"enter_edit_mode" => {
|
||||
self.enter_edit_mode();
|
||||
Some("Entered edit mode".to_string())
|
||||
}
|
||||
"enter_readonly_mode" => {
|
||||
self.enter_readonly_mode();
|
||||
Some("Entered read-only mode".to_string())
|
||||
}
|
||||
"enter_highlight_mode" => {
|
||||
self.enter_highlight_mode();
|
||||
Some("Entered highlight mode".to_string())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Use canvas dispatcher for all other keys
|
||||
self.dispatcher.dispatch_key(key, &mut self.form, &self.config)?;
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
async fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut state: DemoFormState, config: CanvasConfig) -> io::Result<()> {
|
||||
let theme = DemoTheme;
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &state, &theme))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
// BASIC DEBUG: Show EVERY key press for j, k, w
|
||||
match key.code {
|
||||
KeyCode::Char('j') | KeyCode::Char('k') | KeyCode::Char('w') => {
|
||||
println!("🔥 KEY PRESSED: {:?} with modifiers {:?}", key.code, key.modifiers);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Handle quit - multiple options
|
||||
if (key.code == KeyCode::Char('q') && key.modifiers.contains(KeyModifiers::CONTROL)) ||
|
||||
(key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL)) ||
|
||||
key.code == KeyCode::F(10) {
|
||||
break;
|
||||
}
|
||||
|
||||
let is_edit_mode = state.mode == AppMode::Edit;
|
||||
let mut handled = false;
|
||||
|
||||
// Debug: Show what key was pressed and check config lookup
|
||||
let key_debug = format!("{:?}", key.code);
|
||||
let config_action = if is_edit_mode {
|
||||
config.get_edit_action(key.code, key.modifiers)
|
||||
} else {
|
||||
config.get_read_only_action(key.code, key.modifiers)
|
||||
};
|
||||
|
||||
// FOCUS: Special debug for j, k, w keys
|
||||
match key.code {
|
||||
KeyCode::Char('j') => {
|
||||
println!("🔥 J KEY: Config action: {:?}", config_action);
|
||||
state.debug_message = format!("🔍 'j' KEY: Mode={} | Config action: {:?}",
|
||||
if is_edit_mode { "EDIT" } else { "READ-ONLY" }, config_action);
|
||||
}
|
||||
KeyCode::Char('k') => {
|
||||
println!("🔥 K KEY: Config action: {:?}", config_action);
|
||||
state.debug_message = format!("🔍 'k' KEY: Mode={} | Config action: {:?}",
|
||||
if is_edit_mode { "EDIT" } else { "READ-ONLY" }, config_action);
|
||||
}
|
||||
KeyCode::Char('w') => {
|
||||
println!("🔥 W KEY: Config action: {:?}", config_action);
|
||||
state.debug_message = format!("🔍 'w' KEY: Mode={} | Config action: {:?} | Current pos: {} | Text: '{}'",
|
||||
if is_edit_mode { "EDIT" } else { "READ-ONLY" },
|
||||
config_action,
|
||||
state.cursor_pos,
|
||||
state.get_current_input());
|
||||
}
|
||||
_ => {
|
||||
state.debug_message = format!("Key: {} | Mods: {:?} | Mode: {} | Config found: {:?}",
|
||||
key_debug, key.modifiers,
|
||||
if is_edit_mode { "EDIT" } else { "READ-ONLY" },
|
||||
config_action);
|
||||
}
|
||||
}
|
||||
|
||||
// First priority: Try to dispatch through your config system
|
||||
let mut ideal_cursor = state.ideal_cursor_column;
|
||||
let old_cursor_pos = state.cursor_pos; // Track cursor before action
|
||||
|
||||
// EXTRA DEBUG for w key
|
||||
if key.code == KeyCode::Char('w') {
|
||||
println!("🔥 W KEY: About to call ActionDispatcher::dispatch_key");
|
||||
println!("🔥 W KEY: cursor before = {}, text = '{}'", old_cursor_pos, state.get_current_input());
|
||||
}
|
||||
|
||||
if let Ok(Some(result)) = ActionDispatcher::dispatch_key(
|
||||
key.code,
|
||||
key.modifiers,
|
||||
&mut state,
|
||||
&mut ideal_cursor,
|
||||
is_edit_mode,
|
||||
false, // no autocomplete suggestions
|
||||
).await {
|
||||
state.ideal_cursor_column = ideal_cursor;
|
||||
|
||||
let new_cursor_pos = state.cursor_pos; // Track cursor after action
|
||||
|
||||
// FOCUS: Special debug for 'w' key
|
||||
if key.code == KeyCode::Char('w') {
|
||||
println!("SUCCESS W KEY PROCESSED: cursor {} -> {} | text: '{}'", old_cursor_pos, new_cursor_pos, state.get_current_input());
|
||||
state.debug_message = format!("SUCCESS 'w' PROCESSED: cursor {} -> {} | text: '{}'",
|
||||
old_cursor_pos, new_cursor_pos, state.get_current_input());
|
||||
} else {
|
||||
state.debug_message = format!("SUCCESS Config handled: {} -> {}", key_debug,
|
||||
result.message().unwrap_or("success"));
|
||||
}
|
||||
|
||||
// Mark as changed for text modification keys in edit mode
|
||||
if is_edit_mode {
|
||||
match key.code {
|
||||
KeyCode::Char(_) | KeyCode::Backspace | KeyCode::Delete => {
|
||||
state.set_has_unsaved_changes(true);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
handled = true;
|
||||
} else {
|
||||
// Debug dispatch failures
|
||||
if key.code == KeyCode::Char('w') {
|
||||
println!("FAILED W KEY: ActionDispatcher::dispatch_key returned None or Error");
|
||||
|
||||
// Try calling dispatch_with_config directly to see the error
|
||||
let action = CanvasAction::MoveWordNext;
|
||||
println!("FAILED W KEY: Trying direct dispatch of MoveWordNext action");
|
||||
|
||||
match ActionDispatcher::dispatch_with_config(
|
||||
action,
|
||||
&mut state,
|
||||
&mut ideal_cursor,
|
||||
Some(&config),
|
||||
).await {
|
||||
Ok(result) => {
|
||||
println!("FAILED W KEY: Direct dispatch SUCCESS: {:?}", result);
|
||||
state.debug_message = "Direct dispatch worked!".to_string();
|
||||
}
|
||||
Err(e) => {
|
||||
println!("FAILED W KEY: Direct dispatch ERROR: {:?}", e);
|
||||
state.debug_message = format!("Direct dispatch error: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second priority: Handle character input in edit mode (if not handled by config)
|
||||
if !handled && is_edit_mode {
|
||||
if let KeyCode::Char(c) = key.code {
|
||||
if !key.modifiers.contains(KeyModifiers::CONTROL) && !key.modifiers.contains(KeyModifiers::ALT) {
|
||||
let action = CanvasAction::InsertChar(c);
|
||||
let mut ideal_cursor = state.ideal_cursor_column;
|
||||
if let Ok(_) = ActionDispatcher::dispatch_with_config(
|
||||
action,
|
||||
&mut state,
|
||||
&mut ideal_cursor,
|
||||
Some(&config),
|
||||
).await {
|
||||
state.ideal_cursor_column = ideal_cursor;
|
||||
state.set_has_unsaved_changes(true);
|
||||
state.debug_message = format!("Inserted char: '{}'", c);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Third priority: Fallback mode transitions (if not handled by config)
|
||||
if !handled {
|
||||
match (state.mode, key.code) {
|
||||
// ReadOnly -> Edit mode fallbacks
|
||||
(AppMode::ReadOnly, KeyCode::Char('i') | KeyCode::Char('a') | KeyCode::Insert) => {
|
||||
state.enter_edit_mode();
|
||||
if key.code == KeyCode::Char('a') {
|
||||
state.cursor_pos = state.fields[state.current_field].len();
|
||||
}
|
||||
state.debug_message = format!("Fallback: entered edit mode via {:?}", key.code);
|
||||
handled = true;
|
||||
}
|
||||
// ReadOnly -> Visual mode fallback
|
||||
(AppMode::ReadOnly, KeyCode::Char('v')) => {
|
||||
state.enter_highlight_mode();
|
||||
state.debug_message = "Fallback: entered visual mode via 'v'".to_string();
|
||||
handled = true;
|
||||
}
|
||||
// Any mode -> ReadOnly fallback
|
||||
(_, KeyCode::Esc) => {
|
||||
state.enter_readonly_mode();
|
||||
state.debug_message = "Fallback: entered read-only mode via Esc".to_string();
|
||||
handled = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing handled the key, show more debug info
|
||||
if !handled {
|
||||
let available_actions: Vec<String> = if is_edit_mode {
|
||||
config.keybindings.edit.keys().cloned().collect()
|
||||
} else {
|
||||
config.keybindings.read_only.keys().cloned().collect()
|
||||
};
|
||||
|
||||
state.debug_message = format!("❌ Unhandled: {} | Available actions: {}",
|
||||
key_debug,
|
||||
available_actions.join(", "));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, state: &DemoFormState, theme: &DemoTheme) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Min(8), // Main form area
|
||||
Constraint::Length(4), // Status area (increased for debug info)
|
||||
])
|
||||
.split(f.area());
|
||||
|
||||
// Render the canvas form
|
||||
render_canvas(
|
||||
f,
|
||||
chunks[0],
|
||||
state,
|
||||
theme,
|
||||
state.mode == AppMode::Edit,
|
||||
&state.highlight_state,
|
||||
);
|
||||
|
||||
// Render status bar with debug info
|
||||
let mode_text = match state.mode {
|
||||
AppMode::Edit => "EDIT",
|
||||
AppMode::ReadOnly => "NORMAL",
|
||||
AppMode::Highlight => "VISUAL",
|
||||
AppMode::General => "GENERAL",
|
||||
AppMode::Command => "COMMAND",
|
||||
};
|
||||
|
||||
let status_text = if state.has_changes {
|
||||
format!("-- {} -- [Modified]", mode_text)
|
||||
} else {
|
||||
format!("-- {} --", mode_text)
|
||||
};
|
||||
|
||||
let position_text = format!("Field: {}/{} | Cursor: {} | Column: {}",
|
||||
state.current_field + 1,
|
||||
state.fields.len(),
|
||||
state.cursor_pos,
|
||||
state.ideal_cursor_column);
|
||||
|
||||
let help_text = match state.mode {
|
||||
AppMode::ReadOnly => "hjkl/arrows: Move | Tab/Shift+Tab: Fields | w/b/e: Words | 0/$: Line | gg/G: File | i/a: Edit | v: Visual | F10: Quit",
|
||||
AppMode::Edit => "Type to edit | hjkl/arrows: Move | Tab/Enter: Next field | Backspace/Delete: Delete | Home/End: Line | Esc: Normal | F10: Quit",
|
||||
AppMode::Highlight => "hjkl/arrows: Select | w/b/e: Words | 0/$: Line | Esc: Normal | F10: Quit",
|
||||
_ => "Esc: Normal | F10: Quit",
|
||||
};
|
||||
|
||||
let status = Paragraph::new(vec![
|
||||
Line::from(Span::styled(status_text, Style::default().fg(theme.accent()))),
|
||||
Line::from(Span::styled(position_text, Style::default().fg(theme.fg()))),
|
||||
Line::from(Span::styled(state.debug_message.clone(), Style::default().fg(theme.warning()))),
|
||||
Line::from(Span::styled(help_text, Style::default().fg(theme.secondary()))),
|
||||
])
|
||||
.block(Block::default().borders(Borders::ALL).title("Status & Debug"));
|
||||
|
||||
f.render_widget(status, chunks[1]);
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Load configuration
|
||||
let config = CanvasConfig::load();
|
||||
|
||||
// Setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
@@ -235,9 +465,11 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// Create app and run
|
||||
let mut app = App::new()?;
|
||||
let res = run_app(&mut terminal, &mut app);
|
||||
// Create demo state
|
||||
let state = DemoFormState::new();
|
||||
|
||||
// Run app
|
||||
let res = run_app(&mut terminal, state, config).await;
|
||||
|
||||
// Restore terminal
|
||||
disable_raw_mode()?;
|
||||
@@ -249,68 +481,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
println!("{:?}", err);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<(), Box<dyn Error>> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
app.handle_key(key.code)?;
|
||||
}
|
||||
|
||||
if app.should_quit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(10),
|
||||
Constraint::Length(3),
|
||||
])
|
||||
.split(f.size());
|
||||
|
||||
// Header
|
||||
let header_block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Canvas Library - Login Form Demo");
|
||||
f.render_widget(header_block, chunks[0]);
|
||||
|
||||
// Main form area - use canvas GUI rendering
|
||||
let form_area = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints([Constraint::Min(0)])
|
||||
.split(chunks[1])[0];
|
||||
|
||||
// Use canvas library's GUI rendering
|
||||
render_canvas(f, form_area, &app.form, &app.theme);
|
||||
|
||||
// Render autocomplete overlay if active
|
||||
if app.form.autocomplete_state().is_active() {
|
||||
render_autocomplete(f, form_area, &app.form, &app.theme);
|
||||
}
|
||||
|
||||
// Footer with help
|
||||
let footer_block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Controls");
|
||||
|
||||
let help_text = ratatui::widgets::Paragraph::new(
|
||||
"↑↓ - Navigate fields | ←→ - Move cursor | Tab - Autocomplete | Enter - Select/Next | Esc/q - Quit"
|
||||
)
|
||||
.block(footer_block)
|
||||
.style(Style::default().fg(Color::Gray));
|
||||
|
||||
f.render_widget(help_text, chunks[2]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user