docs/internal/event-dispatch-architecture.md
This document describes the current state of mouse/keyboard event handling in Fresh, identifies architectural issues, and proposes improvements.
Fresh uses ratatui for terminal UI rendering. Ratatui is intentionally a rendering library only - it does not handle input events. This is by design, as the maintainers want it to remain a library rather than a framework.
This means Fresh must implement its own event dispatch system to map screen coordinates back to UI components.
render.rs using ratatui's constraint systemmouse_input.rs with a mix of approaches:
cached_layout.tab_areas, cached_layout.status_bar_area, etc.if row == 0 for menu barSome components properly use cached_layout for hit testing:
| Component | Cached Layout Field | Notes |
|---|---|---|
| Tab bar | tab_areas: Vec<(split_id, buffer_id, row, start_col, end_col, close_start)> | Per-tab positions |
| Status bar | status_bar_area: Option<(row, x, width)> | With sub-indicator positions |
| File explorer | file_explorer_area: Option<Rect> | Area and resize border |
| Split separators | get_separator_areas() | For resize dragging |
Issue #832 exposed a fundamental problem: the menu bar check used hardcoded row == 0:
// BUG: Assumes menu bar is always at row 0
if row == 0 {
// Handle menu bar click...
}
When menu_bar_visible is false, row 0 becomes the tab bar, but clicks were still being intercepted by the menu bar handler.
Similar risks exist for:
row + 1 without checking menu bar visibility)There's no unified system for:
Add menu_bar_row to cached layout for consistency:
pub struct CachedLayout {
// Existing fields...
pub tab_areas: Vec<(SplitId, BufferId, u16, u16, u16, u16)>,
pub status_bar_area: Option<(u16, u16, u16)>,
// NEW: Menu bar position (None when hidden)
pub menu_bar_row: Option<u16>,
}
Then change hardcoded checks:
// Before
if row == 0 { ... }
// After
if self.cached_layout.menu_bar_row == Some(row) { ... }
Create a central hit-test structure built during rendering:
/// A clickable/hoverable region on screen
pub struct HitArea {
pub rect: Rect,
pub target: HitTarget,
pub z_index: u8, // Higher = on top (for overlapping popups)
}
/// What can be clicked
pub enum HitTarget {
MenuBarItem(usize),
Tab { split_id: SplitId, buffer_id: BufferId, close_button: bool },
EditorContent { split_id: SplitId },
Scrollbar { split_id: SplitId },
StatusBarIndicator(StatusIndicator),
FileExplorer { item_index: Option<usize> },
SplitSeparator { split_id: SplitId },
Dialog { dialog_id: DialogId, element: DialogElement },
// ...
}
impl CachedLayout {
/// Find the topmost hit target at (col, row)
pub fn hit_test(&self, col: u16, row: u16) -> Option<&HitTarget> {
self.hit_areas
.iter()
.filter(|area| area.rect.contains(Position { x: col, y: row }))
.max_by_key(|area| area.z_index)
.map(|area| &area.target)
}
}
Benefits:
For more complex UI (nested dialogs, transient popups, etc.), consider a compositor pattern like Helix:
pub trait Component {
fn render(&mut self, area: Rect, frame: &mut Frame, hit_areas: &mut Vec<HitArea>);
fn handle_event(&mut self, event: Event) -> EventResult;
}
pub enum EventResult {
Consumed,
Ignored,
Callback(Box<dyn FnOnce(&mut Compositor)>),
}
pub struct Compositor {
layers: Vec<Box<dyn Component>>, // Back to front
}
impl Compositor {
fn handle_event(&mut self, event: Event) {
// Events propagate front-to-back until consumed
for layer in self.layers.iter_mut().rev() {
match layer.handle_event(event.clone()) {
EventResult::Consumed => return,
EventResult::Callback(cb) => { cb(self); return; }
EventResult::Ignored => continue,
}
}
}
}
Benefits:
handle_event() and render()Outcome return typefocus_at(col, row) finds component at position| Approach | Complexity | Risk | Benefit |
|---|---|---|---|
| Phase 1: Add menu_bar_row | Low | Low | Fixes immediate bug pattern |
| Phase 2: Unified HitArea | Medium | Medium | Eliminates coordinate bugs |
| Phase 3: Compositor | High | High | Scalable for complex UI |
Key files involved in mouse event handling:
crates/fresh-editor/src/app/mouse_input.rs - Main mouse event dispatchcrates/fresh-editor/src/app/render.rs - Layout calculationcrates/fresh-editor/src/app/mod.rs - CachedLayout struct definitioncrates/fresh-editor/src/view/ui/split_rendering.rs - Tab bar rendering and position tracking