Word wrap support allows long lines to wrap to fit within the available editor width, improving readability without requiring horizontal scrolling. This is implemented in Phase 2 of the Ferrite editor development.
-
ViewState (
src/editor/ferrite/view.rs)- Tracks wrap width and wrap state
- Stores per-line wrap information (
WrapInfo) - Calculates cumulative heights for wrapped lines
- Converts between logical lines/columns and visual rows
-
LineCache (
src/editor/ferrite/line_cache.rs)- Caches wrapped galleys keyed by content, font, color, AND wrap width
- Uses egui's
painter.layout()with wrap width for wrapped text
-
FerriteEditor (
src/editor/ferrite/editor.rs)- Manages wrap enabled state
- Integrates wrap width from available text area
- Renders wrapped lines with proper y-positioning
-
Cursor Rendering (
src/editor/ferrite/rendering/cursor.rs)- Positions cursor within wrapped galleys
- Calculates correct visual row for cursor
-
Keyboard Navigation (
src/editor/ferrite/input/keyboard.rs)- Visual row-based up/down navigation when wrap is enabled
- Maintains approximate column position across visual rows
pub struct WrapInfo {
/// Number of visual rows this logical line occupies
pub visual_rows: usize,
/// Total height of this line in pixels
pub height: f32,
}// New fields in ViewState
wrap_width: Option<f32>, // None = no wrapping
wrap_info: Vec<WrapInfo>, // Per-line wrap info
cumulative_heights: Vec<f32>, // For fast y-offset lookup
total_content_height: f32, // Cached total height| Method | Description |
|---|---|
enable_wrap(width) |
Enable word wrap at specified width |
disable_wrap() |
Disable word wrap |
is_wrap_enabled() |
Check if wrap is enabled |
set_line_wrap_info(line, rows, height) |
Update wrap info for a line |
rebuild_height_cache(total_lines) |
Rebuild cumulative heights after wrap changes |
get_line_height(line) |
Get height of a line (wrapped or default) |
get_visual_rows(line) |
Get visual row count for a line |
get_line_y_offset(line) |
Get y-offset for a line (uses cumulative heights) |
total_content_height(total_lines) |
Get total document height |
logical_to_visual_row(line, col, chars_per_row) |
Convert logical position to visual row |
visual_row_to_logical(visual_row, total_lines) |
Convert visual row to logical position |
| Method | Description |
|---|---|
get_galley_wrapped(content, painter, font, color, wrap_width) |
Get or create wrapped galley |
| Method | Description |
|---|---|
enable_wrap() |
Enable word wrap |
disable_wrap() |
Disable word wrap |
set_wrap_enabled(enabled) |
Set wrap state |
is_wrap_enabled() |
Check wrap state |
- Editor calculates available text area width
- If wrap is enabled, calls
view.enable_wrap(text_area_width) - For each visible line:
- If wrap enabled: use
line_cache.get_galley_wrapped()with wrap width - Update
view.set_line_wrap_info()with galley's row count and height - Position line using cumulative heights from ViewState
- If wrap enabled: use
- After rendering visible lines, call
view.rebuild_height_cache()
When word wrap is enabled, cursor positioning requires finding:
- Which visual row the cursor column falls on
- The x-offset within that visual row
- The y-offset from the line's top (accounting for the visual row)
The cursor rendering module (src/editor/ferrite/rendering/cursor.rs) uses egui's built-in cursor positioning API for accurate results:
// Convert cursor column to egui's CCursor (character cursor)
let ccursor = egui::text::CCursor::new(cursor_col);
// Get the galley cursor which tracks position within wrapped text
let galley_cursor = galley.from_ccursor(ccursor);
// Get the cursor rectangle relative to galley origin
// CRITICAL: cursor_rect.min.y contains the Y offset from galley top,
// which accounts for which visual row the cursor is on
let cursor_rect = galley.pos_from_cursor(&galley_cursor);
// Final cursor position
let cursor_x = text_start_x + cursor_rect.min.x; // X within visual row
let cursor_y = line_top_y + cursor_rect.min.y; // Y accounts for wrapped rowsThe galley.pos_from_cursor() method returns a Rect where:
min.xis the X offset within the current visual rowmin.yis the Y offset from the galley's top, automatically accounting for which visual row contains the cursor
For example, if a line wraps to 4 visual rows (each ~16px tall) and the cursor is on row 3:
cursor_rect.min.y≈ 48.0 (row index 3 × ~16px per row)- Final
cursor_y = line_top_y + 48.0correctly positions the cursor on row 3
Previous implementations attempted manual byte offset iteration through galley rows, which was error-prone:
- Byte offset calculations don't always match egui's internal layout
- Edge cases with Unicode, combining characters, and ligatures
- Maintenance burden for complex logic
Using galley.pos_from_cursor() delegates to egui's battle-tested text layout engine.
For selection rendering and other features that need cursor position without rendering:
pub fn get_cursor_position(
painter: &egui::Painter,
buffer: &TextBuffer,
cursor: &Cursor,
view: &ViewState,
font_id: &FontId,
text_start_x: f32,
line_top_y: f32,
wrap_width: f32,
) -> (f32, f32, f32) // Returns (x, y, height)Up/down arrow keys move by visual row when wrap is enabled:
- If cursor is not on the first/last visual row of a line, it moves within the same logical line
- If cursor is on first visual row, up moves to the last visual row of the previous line
- If cursor is on last visual row, down moves to the first visual row of the next line
Total document height for scrollbar sizing uses:
- Sum of all wrapped line heights (from cumulative_heights)
- Falls back to
total_lines * line_heightif no wrap info available
- Galley Caching: Wrapped galleys are cached by content + wrap width to avoid re-layout each frame
- Incremental Updates: Only visible lines update their wrap info
- Cumulative Heights: Pre-computed for O(1) y-offset lookup
- Cache Invalidation: Cache is invalidated when:
- Content changes
- Wrap width changes
- Wrap is enabled/disabled
Word wrap tests cover:
- Enable/disable wrap
- Minimum wrap width enforcement
- Per-line wrap info tracking
- Cumulative height calculation
- Visual row conversion
- Logical to visual row mapping
See src/editor/ferrite/view.rs test module for comprehensive tests.
- Preferred Column Tracking: Maintain x-position when moving between visual rows
- Word Break Points: Integrate with selection to select whole words
- Zen Mode Integration: Center wrapped content with configurable margins
- Soft vs Hard Wrap: Option for soft (display-only) vs hard (insert newlines) wrap