Compare commits

..

2 commits

Author SHA1 Message Date
be6cc4ca44 feat: Add full in-page search functionality
Completed Phase 1 high priority task - comprehensive search system:

Search Features:
- Press '/' to enter search mode with dedicated search input
- Case-insensitive search across all content
- Enter to execute search and find all matches
- Real-time match highlighting:
  * Yellow background = current match
  * Blue background = other matches
- Navigate results with 'n' (next) and 'N' (previous)
- Smart scrolling - auto-scroll to current match
- Match counter in status bar (e.g., "Match 3/10")
- Esc to cancel search input
- Search state clears when loading new pages

Implementation Details:
- Added search state to MainWindow::Impl
  * search_mode, search_query, search_matches, current_match
- Search input component (similar to address bar)
- executeSearch() - finds all matching lines
- nextMatch()/previousMatch() - cycle through results
- Content renderer highlights matches dynamically
- Status panel shows search results with emoji indicator

User Experience:
- Intuitive vim-style '/' to search
- Visual feedback with color highlighting
- Match position indicator in status
- Non-intrusive - doesn't interfere with navigation
- Seamless integration with existing UI

Keyboard shortcuts:
- /: Start search
- Enter: Execute search
- n: Next match
- N: Previous match
- Esc: Cancel search

Documentation:
- Updated KEYBOARD.md with search section and usage example
- Updated STATUS.md to reflect completion
- Added search to interactive features list

The browser now has powerful in-page search! 🔍
2026-01-01 00:51:05 +08:00
159e299e96 feat: Add forward navigation with 'f' key
Completed Phase 1 high priority task:

Interactive Features:
- Add 'f' keyboard shortcut for forward navigation
- Forward button in UI now fully functional
- Works in tandem with Backspace (back) navigation
- Only enabled when browser can go forward

Documentation:
- Updated KEYBOARD.md with 'f' key
- Updated README.md keyboard shortcuts
- Updated STATUS.md to reflect completion
- Updated help text in main.cpp

Keyboard shortcuts:
- Backspace: Go back
- f: Go forward
- Both check navigation state before allowing action

The browser now has complete bidirectional navigation! 
2026-01-01 00:41:07 +08:00
5 changed files with 192 additions and 37 deletions

View file

@ -7,6 +7,7 @@
|-----|--------|
| `o` | Open address bar (type URL and press Enter) |
| `Backspace` | Go back |
| `f` | Go forward |
| `r` or `F5` | Refresh current page |
| `q` or `Esc` or `F10` | Quit browser |
@ -59,6 +60,18 @@
# 4. Press 'Esc' to cancel
```
### In-Page Search
```bash
# Search for text on current page
# 1. Press '/' to open search bar
# 2. Type your search query (case-insensitive)
# 3. Press 'Enter' to find all matches
# 4. Press 'n' to go to next match
# 5. Press 'N' to go to previous match
# 6. Matches are highlighted (yellow = current, blue = other matches)
# 7. Status bar shows "Match X/Y" count
```
## 🎨 UI Elements
### Top Bar
@ -92,17 +105,22 @@
4. **Efficient Browsing**: Use `g` to jump to top, `G` to jump to bottom
5. **Address Bar**: Type `o` quickly to enter a new URL
### Search
| Key | Action |
|-----|--------|
| `/` | Start search (type query and press Enter) |
| `n` | Next search result |
| `N` | Previous search result |
| `Esc` | Cancel search |
## 🐛 Known Limitations
- Ctrl+L not yet working for address bar (use 'o' instead)
- Forward navigation not yet implemented
- No search functionality yet (/ key)
- No bookmarks yet (Ctrl+D)
- No history panel yet (F3)
## 🚀 Coming Soon
- [ ] In-page search (`/` to search, `n`/`N` to navigate results)
- [ ] Bookmarks (add, remove, list)
- [ ] History (view and navigate)
- [ ] Better link highlighting

View file

@ -82,6 +82,7 @@ cmake --build build -j$(nproc)
| `g` | Go to top |
| `G` | Go to bottom |
| `Backspace` | Go back |
| `f` | Go forward |
### Links
| Key | Action |

View file

@ -34,9 +34,10 @@
- **Content Scrolling** - j/k, g/G, Space/b for navigation
- **Link Navigation** - Tab, number keys (1-9), Enter to follow
- **Address Bar** - 'o' to open, type URL, Enter to navigate
- **Browser Controls** - Backspace to go back, r/F5 to refresh
- **Real-time Status** - Load stats, scroll position, selected link
- **Visual Feedback** - Navigation button states, link highlighting
- **Browser Controls** - Backspace to go back, 'f' to go forward, r/F5 to refresh
- **In-Page Search** - '/' to search, n/N to navigate results, highlighted matches
- **Real-time Status** - Load stats, scroll position, selected link, search results
- **Visual Feedback** - Navigation button states, link highlighting, search highlighting
### Build & Deployment
- ✅ Binary size: **827KB** (well under 1MB target!)
@ -58,15 +59,6 @@
- No visual history panel (F3)
- No persistence across sessions
- ⚠️ **Search** - Not implemented
- / search command not working
- n/N navigation not working
- No highlight of matches
- ⚠️ **Forward Navigation** - Not yet wired up
- Forward button shows but doesn't work
- Engine supports it, just needs UI connection
### Feature Gaps
- ⚠️ No form support (input fields, buttons, etc.)
- ⚠️ No image rendering (even ASCII art)
@ -75,25 +67,15 @@
## 🎯 Next Steps Priority
### Phase 1: Polish Interactive Features (High Priority)
### Phase 1: Enhanced UX (High Priority)
1. **Wire Up Forward Navigation** (src/main.cpp)
- Connect forward button click to engine.goForward()
- Add keyboard shortcut (maybe Shift+Backspace or Alt+→)
### Phase 2: Enhanced UX (Medium Priority)
4. **Implement Search** (src/ui/content_view.cpp)
- Add / to start search
- Highlight matches
- n/N to navigate results
5. **Add Bookmark System** (new files)
1. **Add Bookmark System** (new files)
- Implement bookmark storage (JSON file)
- Create bookmark panel UI
- Add Ctrl+D to bookmark
- F2 to view bookmarks
6. **Add History** (new files)
2. **Add History** (new files)
- Implement history storage (JSON file)
- Create history panel UI
- F3 to view history
@ -131,6 +113,9 @@ Interactive test:
✅ Press '1' to jump to link 1 - WORKS
✅ Enter to follow link - WORKS
✅ Backspace to go back - WORKS
✅ 'f' to go forward - WORKS
✅ '/' to search - WORKS
✅ 'n'/'N' to navigate search results - WORKS
✅ 'r' to refresh - WORKS
✅ 'o' to open address bar - WORKS
```

View file

@ -49,6 +49,7 @@ void printHelp(const char* prog_name) {
<< " Shift+Tab Previous link\n"
<< " Enter Follow link\n"
<< " Backspace Go back\n"
<< " f Go forward\n"
<< " / Search in page\n"
<< " n/N Next/previous search result\n"
<< " Ctrl+L Focus address bar\n"

View file

@ -12,6 +12,7 @@
#include <sstream>
#include <algorithm>
#include <cctype>
namespace tut {
@ -40,6 +41,12 @@ public:
// Split content into lines for scrolling
std::vector<std::string> content_lines_;
// Search state
bool search_mode_{false};
std::string search_query_;
std::vector<int> search_matches_; // Line indices with matches
int current_match_{-1}; // Index into search_matches_
void setContent(const std::string& content) {
content_lines_.clear();
std::istringstream iss(content);
@ -48,6 +55,12 @@ public:
content_lines_.push_back(line);
}
scroll_offset_ = 0;
// Clear search state when new content is loaded
search_mode_ = false;
search_query_.clear();
search_matches_.clear();
current_match_ = -1;
}
void scrollDown(int lines = 1) {
@ -79,6 +92,54 @@ public:
selected_link_ = static_cast<int>(links_.size()) - 1;
}
}
void executeSearch() {
search_matches_.clear();
current_match_ = -1;
if (search_query_.empty()) {
return;
}
// Convert search query to lowercase for case-insensitive search
std::string query_lower = search_query_;
std::transform(query_lower.begin(), query_lower.end(), query_lower.begin(), ::tolower);
// Find all lines containing the search query
for (size_t i = 0; i < content_lines_.size(); ++i) {
std::string line_lower = content_lines_[i];
std::transform(line_lower.begin(), line_lower.end(), line_lower.begin(), ::tolower);
if (line_lower.find(query_lower) != std::string::npos) {
search_matches_.push_back(static_cast<int>(i));
}
}
if (!search_matches_.empty()) {
current_match_ = 0;
// Scroll to first match
scroll_offset_ = search_matches_[0];
}
}
void nextMatch() {
if (search_matches_.empty()) return;
current_match_ = (current_match_ + 1) % static_cast<int>(search_matches_.size());
// Scroll to the match
scroll_offset_ = search_matches_[current_match_];
}
void previousMatch() {
if (search_matches_.empty()) return;
current_match_--;
if (current_match_ < 0) {
current_match_ = static_cast<int>(search_matches_.size()) - 1;
}
// Scroll to the match
scroll_offset_ = search_matches_[current_match_];
}
};
MainWindow::MainWindow() : impl_(std::make_unique<Impl>()) {}
@ -99,6 +160,11 @@ int MainWindow::run() {
auto address_input = Input(&address_content, "Enter URL...");
bool address_focused = false;
// 搜索输入
std::string search_content;
auto search_input = Input(&search_content, "Search...");
bool search_focused = false;
// 内容渲染器
auto content_renderer = Renderer([this] {
Elements lines;
@ -115,7 +181,28 @@ int MainWindow::run() {
static_cast<int>(impl_->content_lines_.size()));
for (int i = start; i < end; i++) {
lines.push_back(text(impl_->content_lines_[i]));
// Check if this line is a search match
bool is_match = false;
bool is_current_match = false;
if (!impl_->search_matches_.empty()) {
auto it = std::find(impl_->search_matches_.begin(),
impl_->search_matches_.end(), i);
if (it != impl_->search_matches_.end()) {
is_match = true;
int match_index = std::distance(impl_->search_matches_.begin(), it);
is_current_match = (match_index == impl_->current_match_);
}
}
// Render line with appropriate highlighting
auto line_element = text(impl_->content_lines_[i]);
if (is_current_match) {
line_element = line_element | bgcolor(Color::Yellow) | color(Color::Black);
} else if (is_match) {
line_element = line_element | bgcolor(Color::Blue) | color(Color::White);
}
lines.push_back(line_element);
}
// Scroll indicator
@ -135,7 +222,16 @@ int MainWindow::run() {
auto status_panel = Renderer([this] {
Elements status_items;
if (impl_->loading_) {
// Search status takes priority
if (!impl_->search_matches_.empty()) {
std::string search_info = "🔍 Match " +
std::to_string(impl_->current_match_ + 1) + "/" +
std::to_string(impl_->search_matches_.size()) +
" for \"" + impl_->search_query_ + "\"";
status_items.push_back(text(search_info) | color(Color::Yellow));
} else if (impl_->search_mode_ && !impl_->search_query_.empty()) {
status_items.push_back(text("🔍 No matches for \"" + impl_->search_query_ + "\"") | dim);
} else if (impl_->loading_) {
status_items.push_back(text("⏳ Loading...") | dim);
} else if (impl_->load_time_ > 0) {
std::string stats = "" + std::to_string(impl_->load_bytes_ / 1024) + " KB " +
@ -146,7 +242,8 @@ int MainWindow::run() {
status_items.push_back(text("Ready") | dim);
}
if (impl_->selected_link_ >= 0 && impl_->selected_link_ < static_cast<int>(impl_->links_.size())) {
if (impl_->selected_link_ >= 0 && impl_->selected_link_ < static_cast<int>(impl_->links_.size()) &&
impl_->search_matches_.empty()) {
status_items.push_back(separator());
std::string link_info = "[" + std::to_string(impl_->selected_link_ + 1) + "] " +
impl_->links_[impl_->selected_link_].url;
@ -158,9 +255,18 @@ int MainWindow::run() {
// 主布局
auto main_renderer = Renderer([&] {
return vbox({
// 顶部栏
hbox({
Elements top_bar_elements;
if (search_focused) {
// Show search bar when in search mode
top_bar_elements.push_back(hbox({
text("🔍 "),
search_input->Render() | flex | border | focus,
text(" [Esc to cancel]") | dim,
}));
} else {
// Normal address bar
top_bar_elements.push_back(hbox({
text(impl_->can_go_back_ ? "[◀]" : "[◀]") | (impl_->can_go_back_ ? bold : dim),
text(" "),
text(impl_->can_go_forward_ ? "[▶]" : "[▶]") | (impl_->can_go_forward_ ? bold : dim),
@ -172,7 +278,12 @@ int MainWindow::run() {
text("[⚙]") | bold,
text(" "),
text("[?]") | bold,
}),
}));
}
return vbox({
// 顶部栏
vbox(top_bar_elements),
separator(),
// 内容区
content_renderer->Render() | flex,
@ -235,11 +346,44 @@ int MainWindow::run() {
return true;
}
// Don't handle other keys if address bar is focused
if (address_focused) {
// Search mode activation
if (event == Event::Character('/') && !address_focused && !search_focused) {
search_focused = true;
search_content.clear();
return true;
}
// Execute search
if (event == Event::Return && search_focused) {
impl_->search_mode_ = true;
impl_->search_query_ = search_content;
impl_->executeSearch();
search_focused = false;
return true;
}
// Exit search mode
if (event == Event::Escape && search_focused) {
search_focused = false;
search_content.clear();
return true;
}
// Don't handle other keys if address bar or search is focused
if (address_focused || search_focused) {
return false;
}
// Navigate search results (only when not in input mode)
if (event == Event::Character('n') && !impl_->search_matches_.empty()) {
impl_->nextMatch();
return true;
}
if (event == Event::Character('N') && !impl_->search_matches_.empty()) {
impl_->previousMatch();
return true;
}
// Scrolling
if (event == Event::Character('j') || event == Event::ArrowDown) {
impl_->scrollDown(1);
@ -309,6 +453,12 @@ int MainWindow::run() {
}
return true;
}
if (event == Event::Character('f') && impl_->can_go_forward_) {
if (impl_->on_event_) {
impl_->on_event_(WindowEvent::Forward);
}
return true;
}
// Refresh
if (event == Event::Character('r') || event == Event::F5) {