Text Editor Widget
April 11, 2026 · View on GitHub
Overview
The EditorWidget provides the main text editing interface for Ferrite. It wraps egui's TextEdit::multiline widget with additional functionality for cursor position tracking, scroll persistence, and integration with the application state.
Key Files
src/editor/mod.rs- Module declaration and exportssrc/editor/widget.rs- EditorWidget implementation with builder pattern and line numberssrc/editor/line_numbers.rs- Line counting utilitiessrc/editor/stats.rs- Text statistics (word/char/line counting)
Implementation Details
EditorWidget Structure
The widget uses a builder pattern for flexible configuration:
EditorWidget::new(tab)
.font_size(14.0)
.word_wrap(true)
.show_line_numbers(true)
.theme_colors(theme_colors)
.id(egui::Id::new("main_editor"))
.show(ui);
Core Features
| Feature | Implementation |
|---|---|
| Text editing | egui TextEdit::multiline handles input natively |
| Cursor movement | Arrow keys, Home/End, Page Up/Down via egui |
| Text selection | Mouse drag and Shift+Arrow supported natively |
| Clipboard | Ctrl+C/X/V handled by egui |
| Scrolling | Wrapped in ScrollArea with offset persistence |
| Line numbers | Optional gutter with sync scrolling (see Line Numbers) |
Cursor Position Tracking
The widget converts egui's character-based cursor index to (line, column) coordinates:
fn char_index_to_line_col(text: &str, char_index: usize) -> (usize, usize) {
let mut line = 0;
let mut col = 0;
for (i, ch) in text.chars().enumerate() {
if i >= char_index { break; }
if ch == '\n' {
line += 1;
col = 0;
} else {
col += 1;
}
}
(line, col)
}
Integration with Tab State
The editor integrates directly with the Tab struct:
- Content: Mutates
tab.contentdirectly via TextEdit - Cursor: Updates
tab.cursor_positionas(line, col) - Scroll: Persists
tab.scroll_offsetfrom ScrollArea
EditorOutput
The show() method returns useful information:
pub struct EditorOutput {
pub response: Response, // egui interaction response
pub changed: bool, // Whether content was modified
pub cursor_position: (usize, usize), // Current (line, col)
}
Scroll Persistence & egui Widget IDs
egui persists ScrollArea state (scroll offset) per widget Id. To prevent scroll positions leaking across tab switches, every central-panel editor/preview widget ID must be scoped with tab.id:
// ✅ DO: Scope widget ID with tab.id — each tab gets independent scroll state
let editor_widget_id = egui::Id::new("main_editor_raw").with(tab.id);
EditorWidget::new(tab)
.id(editor_widget_id)
.show(ui);
// ✅ DO: MarkdownEditor can inline it (only borrows tab.content, not tab)
MarkdownEditor::new(&mut tab.content)
.id(egui::Id::new("main_editor_rendered").with(tab.id))
.show(ui);
// ❌ DON'T: Fixed ID leaks scroll offset between tabs
EditorWidget::new(tab)
.id(egui::Id::new("main_editor_raw"))
.show(ui);
Note: For EditorWidget, capture the ID before the builder chain since EditorWidget::new(tab) takes a mutable borrow of the entire Tab, making tab.id inaccessible in the chain.
The four scoped IDs in src/app/central_panel.rs:
"main_editor_raw"— Raw mode, single pane"main_editor_rendered"— Rendered mode, single pane"split_editor_raw"— Split view, left (raw) pane"split_preview_rendered"— Split view, right (preview) pane
FerriteEditor storage (raw editor) is separately keyed by tab_id in a HashMap inside FerriteEditorStorage (see src/editor/widget.rs), so its virtual scrolling is already per-tab independent of the widget ID.
Dependencies Used
egui- TextEdit::multiline, ScrollArea, FontIdeframe- Re-exports egui types
Usage
In app.rs, the editor is rendered in the central panel:
let font_size = self.state.settings.font_size;
let word_wrap = self.state.settings.word_wrap;
if let Some(tab) = self.state.active_tab_mut() {
let editor_output = EditorWidget::new(tab)
.font_size(font_size)
.word_wrap(word_wrap)
.id(egui::Id::new("main_editor"))
.show(ui);
}
Tests
Run editor-specific tests:
cargo test editor::widget::tests
Test coverage includes:
test_char_index_to_line_col_*- Cursor position conversiontest_line_col_to_char_index_*- Reverse conversiontest_roundtrip_conversion- Bidirectional consistency
Related Documentation
- Line Numbers - Line number gutter implementation
- Line Number Alignment - Technical fix for alignment drift
- Text Statistics - Word/character counting in status bar
- WYSIWYG Editor - Rendered view mode using MarkdownEditor