docs/internal/unified-hit-test-theme-plan.md
The theme inspector popup (Ctrl+Right-Click) needs to know which theme keys
styled each screen cell. Currently this is solved by a per-cell theme key map
(cell_theme_map) on CachedLayout, populated during rendering. This works
but has drawbacks:
CachedLayout
already tracks via split_areas, status_bar_area, tab_layouts, etc.mouse_input.rs,
theme_inspect.rs, render.rs).Introduce a unified CachedLayout::region_at(col, row) -> Option<RegionInfo>
method that replaces all per-component hit testing with a single lookup. The
RegionInfo struct carries both interaction semantics (for mouse handling) and
theme key provenance (for the inspector), eliminating the separate cell_theme_map.
/// Describes the UI region at a screen position.
/// Used by mouse input, theme inspector, and accessibility.
pub struct RegionInfo {
/// What kind of region this is
pub kind: RegionKind,
/// Theme key for the foreground color
pub fg_theme_key: Option<&'static str>,
/// Theme key for the background color
pub bg_theme_key: Option<&'static str>,
/// Syntax highlight category (editor content only)
pub syntax_category: Option<&'static str>,
}
pub enum RegionKind {
/// Editor content area (text)
EditorContent {
split_id: LeafId,
buffer_id: BufferId,
byte_pos: Option<usize>,
},
/// Line number gutter
Gutter {
split_id: LeafId,
buffer_id: BufferId,
},
/// Tab bar
Tab {
split_id: LeafId,
buffer_id: BufferId,
is_active: bool,
is_close_button: bool,
},
/// Status bar
StatusBar {
sub_region: StatusBarSubRegion,
},
/// Menu bar
MenuBar {
menu_index: Option<usize>,
},
/// File explorer
FileExplorer,
/// Scrollbar
Scrollbar {
is_thumb: bool,
},
/// Split separator
SplitSeparator,
/// Popup / overlay UI
Popup {
popup_index: usize,
},
/// Empty / unoccupied
Empty,
}
CachedLayout::region_at(col, row) checks regions in priority order (popups
first, then menu, status bar, tabs, split content, file explorer). This is the
same priority order used by mouse input today, but consolidated into one place.
For editor content cells, the per-cell theme key info still needs to come from rendering (because overlays, syntax highlights, selection, and cursor state determine the final keys). The key insight is:
region_at returns them directly from the RegionKind.Keep the per-cell recording only for editor content areas (gutter + text),
not for the entire terminal. This reduces the map from width * height to
just the editor content rectangles.
/// Per-split editor cell theme info, indexed by (row_offset, col_offset)
/// within the split's content rectangle.
pub struct SplitCellThemeMap {
/// Theme info per cell, indexed as row * width + col
/// (relative to the split's content_rect origin)
cells: Vec<CellThemeInfo>,
width: u16,
}
Store as HashMap<LeafId, SplitCellThemeMap> on CachedLayout. The rendering
pipeline already has the split_id, so recording is straightforward.
region_at(col, row) ->
1. Check popups (highest z-order)
2. Check menu bar
3. Check status bar
4. Check tab bars (per split)
5. Check split separators
6. Check scrollbars (per split)
7. Check split content areas:
a. Determine split_id from split_areas
b. If in gutter: return Gutter region with theme keys
c. If in content: look up SplitCellThemeMap[split_id]
-> returns CellThemeInfo with exact fg/bg theme keys
8. Check file explorer
9. Return Empty
mouse_input.rs)Replace scattered if point_in_rect(...) / if row == bar_row checks with:
match self.cached_layout.region_at(col, row) {
Some(RegionInfo { kind: RegionKind::Tab { buffer_id, .. }, .. }) => { /* switch tab */ }
Some(RegionInfo { kind: RegionKind::StatusBar { sub_region }, .. }) => { /* status click */ }
Some(RegionInfo { kind: RegionKind::EditorContent { split_id, byte_pos, .. }, .. }) => { /* editor click */ }
...
}
theme_inspect.rs)Replace resolve_theme_key_at with:
fn resolve_theme_key_at(&self, col: u16, row: u16) -> Option<ThemeKeyInfo> {
let region = self.cached_layout.region_at(col, row)?;
let fg_color = region.fg_theme_key.and_then(|k| self.theme.resolve_theme_key(k));
let bg_color = region.bg_theme_key.and_then(|k| self.theme.resolve_theme_key(k));
Some(ThemeKeyInfo { fg_key: region.fg_theme_key.map(Into::into), ... })
}
Cursor style changes and tooltip display can also use region_at to determine
what's under the mouse.
RegionInfo, RegionKind structs to types.rsCachedLayout::region_at() for non-editor regions (status bar,
menu, tabs, scrollbar, file explorer, separators) using existing cached
layout fieldsrecord_non_editor_theme_regions() with region_at — the theme
inspector calls region_at instead of reading the cell map for these regionscell_theme_map allocation; keep per-split maps
only for editor contentmouse_input.rs with region_at callspoint_in_rect checks scattered across mouse handlersregion_atVec<CellThemeInfo> with HashMap<LeafId, SplitCellThemeMap>render_view_lines (already done, just
change the indexing to be split-relative instead of screen-absolute)RegionKind enum aligns with the HitTest trait proposed in
UNIFIED_UI_FRAMEWORK_PLAN.md. Each component's *Layout struct already
returns typed hit results; region_at is the top-level dispatcher that
delegates to them.region_at infrastructure.HitTest trait per component.
This plan adds the top-level dispatcher (region_at) and the theme key
provenance that the framework plan doesn't address.Pros:
Cons:
region_at introduces a priority-ordered cascade that must stay in sync with
rendering z-order (popups > menu > status bar > content)HashMap lookup vs the current flat array indexWhy not do it now:
cell_theme_map approach works correctly and has no heuristics