From 58b7607074188e7fc9f351db0dcedddd081ca1c3 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Sun, 28 Dec 2025 00:03:39 +0800 Subject: [PATCH] feat: Add interactive dropdown selection for forms - Parse and store OPTION elements in SELECT fields - Display selected option text in dropdown UI - Add SELECT_OPTION input mode for dropdown navigation - Support Enter on SELECT to enter selection mode - Use j/k or arrow keys to navigate through options - Enter to confirm selection, Esc to cancel - Auto-select first option or option marked with 'selected' - Real-time option preview in status bar - Status bar shows "-- SELECT --" mode Data structure: - Added options vector to DomNode (value, text pairs) - Added selected_option index to track current selection Keyboard shortcuts in SELECT mode: - j/Down: Next option - k/Up: Previous option - Enter: Select current option - Esc: Cancel selection --- src/browser.cpp | 36 ++++++++++++++++++++++++++++++++++++ src/dom_tree.cpp | 25 ++++++++++++++++++++++++- src/dom_tree.h | 4 ++++ src/input_handler.cpp | 28 ++++++++++++++++++++++++++++ src/input_handler.h | 10 +++++++--- src/render/layout.cpp | 8 ++++++-- test_form.html | 15 +++++++++++++++ 7 files changed, 120 insertions(+), 6 deletions(-) diff --git a/src/browser.cpp b/src/browser.cpp index fb38dac..823b48d 100644 --- a/src/browser.cpp +++ b/src/browser.cpp @@ -457,6 +457,7 @@ public: case InputMode::COMMAND: case InputMode::SEARCH: mode_str = input_handler.get_buffer(); break; case InputMode::FORM_EDIT: mode_str = "-- INSERT -- " + input_handler.get_buffer(); break; + case InputMode::SELECT_OPTION: mode_str = "-- SELECT --"; break; default: mode_str = ""; break; } framebuffer->set_text(1, y, mode_str, colors::STATUSBAR_FG, colors::STATUSBAR_BG); @@ -554,6 +555,10 @@ public: // Toggle checkbox field->checked = !field->checked; status_message = field->checked ? "☑ Checked" : "☐ Unchecked"; + } else if (field->input_type == "select") { + // Enter dropdown selection mode + input_handler.set_mode(InputMode::SELECT_OPTION); + status_message = "-- SELECT -- (j/k to navigate, Enter to select, Esc to cancel)"; } else if (field->input_type == "submit" || field->element_type == ElementType::BUTTON) { // TODO: Submit form status_message = "Form submit (not yet implemented)"; @@ -688,6 +693,37 @@ public: } break; + case Action::NEXT_OPTION: + if (active_field >= 0 && active_field < static_cast(current_tree.form_fields.size())) { + auto* field = current_tree.form_fields[active_field]; + if (field && field->input_type == "select" && !field->options.empty()) { + field->selected_option = (field->selected_option + 1) % field->options.size(); + status_message = "Option: " + field->options[field->selected_option].second; + } + } + break; + + case Action::PREV_OPTION: + if (active_field >= 0 && active_field < static_cast(current_tree.form_fields.size())) { + auto* field = current_tree.form_fields[active_field]; + if (field && field->input_type == "select" && !field->options.empty()) { + field->selected_option = (field->selected_option - 1 + field->options.size()) % + field->options.size(); + status_message = "Option: " + field->options[field->selected_option].second; + } + } + break; + + case Action::SELECT_CURRENT_OPTION: + if (active_field >= 0 && active_field < static_cast(current_tree.form_fields.size())) { + auto* field = current_tree.form_fields[active_field]; + if (field && field->input_type == "select" && !field->options.empty()) { + field->value = field->options[field->selected_option].first; + status_message = "Selected: " + field->options[field->selected_option].second; + } + } + break; + case Action::QUIT: break; // 在main loop处理 diff --git a/src/dom_tree.cpp b/src/dom_tree.cpp index 0352379..a9d24ad 100644 --- a/src/dom_tree.cpp +++ b/src/dom_tree.cpp @@ -369,7 +369,30 @@ std::unique_ptr DomTreeBuilder::convert_node( } } } - + + // For SELECT, collect all OPTION children + if (element.tag == GUMBO_TAG_SELECT) { + for (const auto& child : node->children) { + if (child->element_type == ElementType::OPTION) { + std::string option_value = child->value.empty() ? child->get_all_text() : child->value; + std::string option_text = child->get_all_text(); + node->options.push_back({option_value, option_text}); + + // Set selected option if marked + if (child->checked) { + node->selected_option = node->options.size() - 1; + node->value = option_value; + } + } + } + + // Set default value to first option if no option is selected + if (!node->options.empty() && node->value.empty()) { + node->value = node->options[0].first; + node->selected_option = 0; + } + } + // Reset form ID if we are exiting a form if (element.tag == GUMBO_TAG_FORM) { g_current_form_id = -1; // Assuming no nested forms diff --git a/src/dom_tree.h b/src/dom_tree.h index c833e68..a8d7de0 100644 --- a/src/dom_tree.h +++ b/src/dom_tree.h @@ -58,6 +58,10 @@ struct DomNode { bool checked = false; int form_id = -1; + // SELECT元素的选项 + std::vector> options; // (value, text) pairs + int selected_option = 0; // 当前选中的选项索引 + // 辅助方法 bool is_block_element() const; bool is_inline_element() const; diff --git a/src/input_handler.cpp b/src/input_handler.cpp index 7e3f7d9..e1c1865 100644 --- a/src/input_handler.cpp +++ b/src/input_handler.cpp @@ -386,6 +386,32 @@ public: return result; } + InputResult process_select_option_mode(int ch) { + InputResult result; + result.action = Action::NONE; + + if (ch == 27) { + // ESC cancels selection + mode = InputMode::NORMAL; + return result; + } else if (ch == '\n' || ch == '\r') { + // Enter selects current option + result.action = Action::SELECT_CURRENT_OPTION; + mode = InputMode::NORMAL; + return result; + } else if (ch == 'j' || ch == KEY_DOWN) { + // Next option + result.action = Action::NEXT_OPTION; + return result; + } else if (ch == 'k' || ch == KEY_UP) { + // Previous option + result.action = Action::PREV_OPTION; + return result; + } + + return result; + } + }; InputHandler::InputHandler() : pImpl(std::make_unique()) {} @@ -406,6 +432,8 @@ InputResult InputHandler::handle_key(int ch) { return pImpl->process_link_hints_mode(ch); case InputMode::FORM_EDIT: return pImpl->process_form_edit_mode(ch); + case InputMode::SELECT_OPTION: + return pImpl->process_select_option_mode(ch); default: break; } diff --git a/src/input_handler.h b/src/input_handler.h index 88eacc7..efae508 100644 --- a/src/input_handler.h +++ b/src/input_handler.h @@ -9,8 +9,9 @@ enum class InputMode { COMMAND, SEARCH, LINK, - LINK_HINTS, // Vimium-style 'f' mode - FORM_EDIT // Form field editing mode + LINK_HINTS, // Vimium-style 'f' mode + FORM_EDIT, // Form field editing mode + SELECT_OPTION // Dropdown selection mode }; enum class Action { @@ -49,7 +50,10 @@ enum class Action { 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) + SUBMIT_FORM, // Submit form (Enter on submit button) + NEXT_OPTION, // Move to next dropdown option (j/down) + PREV_OPTION, // Move to previous dropdown option (k/up) + SELECT_CURRENT_OPTION // Select current dropdown option (Enter) }; struct InputResult { diff --git a/src/render/layout.cpp b/src/render/layout.cpp index 8b703a3..ea63704 100644 --- a/src/render/layout.cpp +++ b/src/render/layout.cpp @@ -432,9 +432,13 @@ void LayoutEngine::layout_form_element(const DomNode* node, Context& /*ctx*/, st span.field_index = node->field_index; line.spans.push_back(span); } else if (node->element_type == ElementType::SELECT) { - // 下拉选择 + // 下拉选择 - 显示当前选中的选项 StyledSpan span; - span.text = "[▼ Select]"; + std::string selected_text = "Select"; + if (node->selected_option >= 0 && node->selected_option < static_cast(node->options.size())) { + selected_text = node->options[node->selected_option].second; + } + span.text = "[▼ " + selected_text + "]"; span.fg = colors::INPUT_FOCUS; span.bg = colors::INPUT_BG; span.field_index = node->field_index; diff --git a/test_form.html b/test_form.html index 4379ba8..eea86d0 100644 --- a/test_form.html +++ b/test_form.html @@ -11,6 +11,21 @@

Email:

Password:

+

Country:

+ +

Age Group:

+

Subscribe to newsletter

I agree to terms