feat: Add interactive form text input editing

- Add FORM_EDIT input mode for editing text fields
- Add actions: NEXT_FIELD, PREV_FIELD, EDIT_TEXT, ACTIVATE_FIELD
- Support 'i' key to focus first form field
- Tab/Shift+Tab to navigate between fields
- Enter on text input fields to edit them
- Real-time text editing with live preview
- Enter/Esc to exit edit mode
- Checkbox toggle support (press Enter on checkbox)
- Status bar shows "-- INSERT --" mode and current text
- Form fields highlighted when active

Keyboard shortcuts:
- i: Focus first form field
- Tab: Next field
- Shift+Tab: Previous field
- Enter: Activate/edit field or toggle checkbox
- Esc: Exit edit mode
This commit is contained in:
m1ngsama 2025-12-27 23:52:36 +08:00
parent 55fc7c79f5
commit 7e55ade793
4 changed files with 178 additions and 4 deletions

View file

@ -456,6 +456,7 @@ public:
case InputMode::NORMAL: mode_str = "NORMAL"; break; case InputMode::NORMAL: mode_str = "NORMAL"; break;
case InputMode::COMMAND: case InputMode::COMMAND:
case InputMode::SEARCH: mode_str = input_handler.get_buffer(); break; case InputMode::SEARCH: mode_str = input_handler.get_buffer(); break;
case InputMode::FORM_EDIT: mode_str = "-- INSERT -- " + input_handler.get_buffer(); break;
default: mode_str = ""; break; default: mode_str = ""; break;
} }
framebuffer->set_text(1, y, mode_str, colors::STATUSBAR_FG, colors::STATUSBAR_BG); framebuffer->set_text(1, y, mode_str, colors::STATUSBAR_FG, colors::STATUSBAR_BG);
@ -540,7 +541,25 @@ public:
break; break;
case Action::FOLLOW_LINK: case Action::FOLLOW_LINK:
if (active_link >= 0 && active_link < static_cast<int>(current_tree.links.size())) { // If on a form field, activate it instead of following link
if (active_field >= 0 && active_field < static_cast<int>(current_tree.form_fields.size())) {
auto* field = current_tree.form_fields[active_field];
if (field) {
if (field->input_type == "text" || field->input_type == "password") {
// Enter edit mode
input_handler.set_mode(InputMode::FORM_EDIT);
input_handler.set_buffer(field->value);
status_message = "-- INSERT --";
} else if (field->input_type == "checkbox") {
// Toggle checkbox
field->checked = !field->checked;
status_message = field->checked ? "☑ Checked" : "☐ Unchecked";
} else if (field->input_type == "submit" || field->element_type == ElementType::BUTTON) {
// TODO: Submit form
status_message = "Form submit (not yet implemented)";
}
}
} else if (active_link >= 0 && active_link < static_cast<int>(current_tree.links.size())) {
start_async_load(current_tree.links[active_link].url); start_async_load(current_tree.links[active_link].url);
} }
break; break;
@ -609,6 +628,66 @@ public:
show_history(); show_history();
break; break;
case Action::NEXT_FIELD:
if (!current_tree.form_fields.empty()) {
// Save current text if in edit mode
if (input_handler.get_mode() == InputMode::FORM_EDIT &&
active_field >= 0 && active_field < static_cast<int>(current_tree.form_fields.size())) {
auto* field = current_tree.form_fields[active_field];
if (field && (field->input_type == "text" || field->input_type == "password")) {
field->value = result.text;
}
}
// Move to next field
if (active_field < 0) {
active_field = 0; // First field
} else {
active_field = (active_field + 1) % current_tree.form_fields.size();
}
// Auto-scroll to field
// TODO: Implement scroll to field
status_message = "Field " + std::to_string(active_field + 1) + "/" +
std::to_string(current_tree.form_fields.size());
}
break;
case Action::PREV_FIELD:
if (!current_tree.form_fields.empty()) {
// Save current text if in edit mode
if (input_handler.get_mode() == InputMode::FORM_EDIT &&
active_field >= 0 && active_field < static_cast<int>(current_tree.form_fields.size())) {
auto* field = current_tree.form_fields[active_field];
if (field && (field->input_type == "text" || field->input_type == "password")) {
field->value = result.text;
}
}
// Move to previous field
if (active_field < 0) {
active_field = current_tree.form_fields.size() - 1; // Last field
} else {
active_field = (active_field - 1 + current_tree.form_fields.size()) %
current_tree.form_fields.size();
}
status_message = "Field " + std::to_string(active_field + 1) + "/" +
std::to_string(current_tree.form_fields.size());
}
break;
case Action::EDIT_TEXT:
// Update field value in real-time
if (active_field >= 0 && active_field < static_cast<int>(current_tree.form_fields.size())) {
auto* field = current_tree.form_fields[active_field];
if (field && (field->input_type == "text" || field->input_type == "password")) {
field->value = result.text;
status_message = "Editing: " + result.text;
}
}
break;
case Action::QUIT: case Action::QUIT:
break; // 在main loop处理 break; // 在main loop处理

View file

@ -125,6 +125,7 @@ public:
count_buffer.clear(); count_buffer.clear();
break; break;
case '\t': case '\t':
// Tab can navigate both links and fields - browser will decide
result.action = Action::NEXT_LINK; result.action = Action::NEXT_LINK;
count_buffer.clear(); count_buffer.clear();
break; break;
@ -135,7 +136,7 @@ public:
break; break;
case '\n': case '\n':
case '\r': case '\r':
// If count buffer has a number, jump to that link // Enter can follow links or activate fields - browser will decide
if (result.has_count) { if (result.has_count) {
result.action = Action::GOTO_LINK; result.action = Action::GOTO_LINK;
result.number = result.count; result.number = result.count;
@ -144,6 +145,11 @@ public:
} }
count_buffer.clear(); count_buffer.clear();
break; break;
case 'i':
// 'i' to focus on first form field (like vim insert mode)
result.action = Action::NEXT_FIELD;
count_buffer.clear();
break;
case 'f': case 'f':
// 'f' command: vimium-style link hints // 'f' command: vimium-style link hints
result.action = Action::SHOW_LINK_HINTS; result.action = Action::SHOW_LINK_HINTS;
@ -334,6 +340,52 @@ public:
return result; return result;
} }
InputResult process_form_edit_mode(int ch) {
InputResult result;
result.action = Action::NONE;
if (ch == 27) {
// ESC exits form edit mode
mode = InputMode::NORMAL;
buffer.clear();
return result;
} else if (ch == '\n' || ch == '\r') {
// Enter submits the text
result.action = Action::EDIT_TEXT;
result.text = buffer;
mode = InputMode::NORMAL;
buffer.clear();
return result;
} else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) {
// Backspace removes last character
if (!buffer.empty()) {
buffer.pop_back();
}
return result;
} else if (ch == '\t') {
// Tab moves to next field while saving current text
result.action = Action::NEXT_FIELD;
result.text = buffer;
buffer.clear();
return result;
} else if (ch == KEY_BTAB) {
// Shift+Tab moves to previous field while saving current text
result.action = Action::PREV_FIELD;
result.text = buffer;
buffer.clear();
return result;
} else if (std::isprint(ch)) {
// Add printable characters to buffer
buffer += static_cast<char>(ch);
// Return EDIT_TEXT to update in real-time
result.action = Action::EDIT_TEXT;
result.text = buffer;
return result;
}
return result;
}
}; };
InputHandler::InputHandler() : pImpl(std::make_unique<Impl>()) {} InputHandler::InputHandler() : pImpl(std::make_unique<Impl>()) {}
@ -352,6 +404,8 @@ InputResult InputHandler::handle_key(int ch) {
return pImpl->process_link_mode(ch); return pImpl->process_link_mode(ch);
case InputMode::LINK_HINTS: case InputMode::LINK_HINTS:
return pImpl->process_link_hints_mode(ch); return pImpl->process_link_hints_mode(ch);
case InputMode::FORM_EDIT:
return pImpl->process_form_edit_mode(ch);
default: default:
break; break;
} }
@ -375,6 +429,14 @@ void InputHandler::reset() {
pImpl->count_buffer.clear(); pImpl->count_buffer.clear();
} }
void InputHandler::set_mode(InputMode mode) {
pImpl->mode = mode;
}
void InputHandler::set_buffer(const std::string& buffer) {
pImpl->buffer = buffer;
}
void InputHandler::set_status_callback(std::function<void(const std::string&)> callback) { void InputHandler::set_status_callback(std::function<void(const std::string&)> callback) {
pImpl->status_callback = callback; pImpl->status_callback = callback;
} }

View file

@ -9,7 +9,8 @@ enum class InputMode {
COMMAND, COMMAND,
SEARCH, SEARCH,
LINK, LINK,
LINK_HINTS // Vimium-style 'f' mode LINK_HINTS, // Vimium-style 'f' mode
FORM_EDIT // Form field editing mode
}; };
enum class Action { enum class Action {
@ -42,7 +43,13 @@ enum class Action {
ADD_BOOKMARK, // Add current page to bookmarks (B) ADD_BOOKMARK, // Add current page to bookmarks (B)
REMOVE_BOOKMARK, // Remove current page from bookmarks (D) REMOVE_BOOKMARK, // Remove current page from bookmarks (D)
SHOW_BOOKMARKS, // Show bookmarks page (:bookmarks) SHOW_BOOKMARKS, // Show bookmarks page (:bookmarks)
SHOW_HISTORY // Show history page (:history) SHOW_HISTORY, // Show history page (:history)
NEXT_FIELD, // Move to next form field (Tab)
PREV_FIELD, // Move to previous form field (Shift+Tab)
ACTIVATE_FIELD, // Activate current field for editing (Enter)
TOGGLE_CHECKBOX, // Toggle checkbox state
EDIT_TEXT, // Edit text input (updates text buffer)
SUBMIT_FORM // Submit form (Enter on submit button)
}; };
struct InputResult { struct InputResult {
@ -62,6 +69,8 @@ public:
InputMode get_mode() const; InputMode get_mode() const;
std::string get_buffer() const; std::string get_buffer() const;
void reset(); void reset();
void set_mode(InputMode mode);
void set_buffer(const std::string& buffer);
void set_status_callback(std::function<void(const std::string&)> callback); void set_status_callback(std::function<void(const std::string&)> callback);
private: private:

24
test_form.html Normal file
View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title>Form Test</title>
</head>
<body>
<h1>Test Form</h1>
<form action="/submit" method="POST">
<p>Name: <input type="text" name="name" placeholder="Enter your name"></p>
<p>Email: <input type="text" name="email" placeholder="email@example.com"></p>
<p>Password: <input type="password" name="password"></p>
<p><input type="checkbox" name="subscribe"> Subscribe to newsletter</p>
<p><input type="checkbox" name="agree"> I agree to terms</p>
<p><input type="submit" value="Submit Form"></p>
<p><button>Click Me</button></p>
</form>
<h2>Links</h2>
<p><a href="https://example.com">Example Link</a></p>
</body>
</html>