Follow-up to #540 (#448-C), under parent #448.
The GTK main-loop flip to ShellApp (#540) merged to develop but left several click-handling and rendering regressions. This issue tracks fixing them.
Confirmed broken: tab switching, explorer tree clicks, settings panel clicks, search sidebar clicks, git sidebar clicks — any sidebar panel click. Also: sidebar scrolling, explorer double-click (open file), explorer drag. Explorer tree shows + fallback icons instead of nerd font folder glyphs.
Root Cause Analysis
Bug 1a — Tabs: tab_slot_positions never populated in ShellApp rendering path
File: src/gtk/mod.rs → render_content() (~line 7249)
The old Relm4 path called draw::draw_tab_bar() which recovered slot hit positions via backend.tab_bar_layout() and stored them in self.tab_slot_positions. The new render_content() path draws the tab bar via:
frame.push(Surface::TabBar { rect: tb_rect, bar: &screen.tab_bar_primitive, … });
frame.draw(backend);
…but never calls backend.tab_bar_layout() to recover the slot positions. So self.tab_slot_positions stays empty every frame. When you click a tab:
screen_zone_hit_test() → ScreenZone::TabBar { local_x, … }
tab_bar_inner_hit_test() → tab_slot_positions.get(group_id) → None
- Falls through without calling
engine.goto_tab() → silent no-op
Fix: After frame.draw(backend) for the tab bar in render_content(), call:
let hits = backend.tab_bar_layout(tb_rect, &screen.tab_bar_primitive);
Then populate self.tab_slot_positions from hits. The return type is quadraui::TabBarHits; slot positions are in 0..width (bar-relative) space. The same reshaping logic that draw.rs::draw_tab_bar() does for diff_btn_map, split_btn_map, action_btn_map needs to happen here too.
Bug 1b — Tabs: coordinate mismatch for single-group tab bar
File: src/render.rs → screen_zone_hit_test() (~line 11985)
Multi-group case correctly offsets: local_x: x - b.x (subtracts group's window-absolute x). Single-group case passes local_x: x — raw window-absolute x. But tab slot positions are in 0..width (bar-relative) space.
When the sidebar is visible (e.g., activity bar = 48px, sidebar = 250px → main.x = 298), a click at window-x = 400 gives local_x = 400 but the slot lives at bar-relative x = 102. Every tab miss.
Current single-group code:
return ScreenZone::TabBar {
group_id: active_group,
local_x: x, // ← wrong: window-absolute, not bar-relative
bar_width,
};
Fix: Use local_x: x - content_x where content_x = the window x at which the content area starts. This is available without adding new parameters: layout.windows.first().map(|w| w.rect.x).unwrap_or(0.0) gives the correct origin (windows are laid out starting from main.x).
Bug 2 — All sidebar panel clicks silently dropped
File: src/gtk/mod.rs → ShellApp::handle() (~line 7462)
Root cause: In the old Relm4 system, each sidebar panel (explorer, settings, search, git, AI, ext) had a dedicated GTK DrawingArea widget. GDK clicks on those DAs fired specific messages: Msg::ExplorerUiEvent, Msg::ScSidebarEvent, Msg::SearchSidebarEvent, etc.
In ShellApp mode, panels are rendered by quadraui controllers (TreeController, FormController, MultiSectionView) into sidebar_content_bounds via render_content(). The old DA widgets no longer receive GDK events. All mouse events arrive via ShellApp::handle().
Flow for a sidebar click today:
AppShell::handle() → AppShellEvent::Ignored (shell only handles activity bar, divider, bottom-panel grip)
ShellApp::handle() → self.dispatch(Msg::MouseClick { x, y, width, height, … }) (always — no sidebar check)
handle_mouse_click_msg() → pixel_to_click_target() → screen_zone_hit_test() → no editor zone matches sidebar area → ScreenZone::None → click dropped
Note: Msg::ExplorerUiEvent → handle_explorer_msg() → engine.explorer_tree.borrow_mut().handle(&ev, backend, rect) is already the correct backend-neutral path through TreeController. The wiring to reach it is simply absent.
Fix: In ShellApp::handle(), before dispatching Msg::MouseClick, check if position is inside ctx.layout.sidebar_content_bounds. If yes, look at the active panel and dispatch to the appropriate existing handler:
// pseudo-code in ShellApp::handle(), UiEvent::MouseDown branch
if let Some(sb_bounds) = ctx.layout.sidebar_content_bounds {
if rect_contains(sb_bounds, position.x, position.y) {
let panel = self.active_sidebar_panel_id(); // from engine.app_shell
match panel.as_str() {
PANEL_EXPLORER => self.dispatch(Msg::ExplorerUiEvent(event)),
PANEL_SEARCH => self.dispatch(Msg::SearchSidebarEvent(event)),
PANEL_DEBUG => self.dispatch(Msg::DebugSidebarEvent(event)),
PANEL_GIT => self.dispatch(Msg::ScSidebarEvent(event)),
PANEL_SETTINGS => self.dispatch(Msg::SettingsUiEvent(event)),
// PANEL_AI, ext panels …
}
self.draw_needed.set(true);
return Reaction::Redraw;
}
}
// Fall through → editor click
self.dispatch(Msg::MouseClick { … });
The same routing gap applies to:
UiEvent::Scroll → dispatched as Msg::MouseScroll (editor-only); sidebar panels can't scroll
UiEvent::DoubleClick → dispatched as Msg::MouseDoubleClick (editor-only); explorer double-click to open files doesn't work
UiEvent::MouseMoved (left button held) → dispatched as Msg::MouseDrag (editor-only); explorer tree drag/select broken
UiEvent::MouseUp → dispatched as Msg::MouseUp (editor-only)
All four need the same sidebar-bounds check + panel dispatch added to ShellApp::handle().
Bug 3 — Explorer shows + fallback icons instead of nerd font folder glyphs
Files: src/gtk/mod.rs render_content(), quadraui Backend trait
Root cause: ShellApp mode has two separate GtkBackend instances:
| Instance |
Owner |
set_nerd_fonts() called? |
self.backend (legacy) |
App |
✅ yes — in Msg::Resize handler |
runner backend (backend: &mut dyn Backend in render_content) |
quadraui runner |
❌ never |
TreeController.render(backend, q_sb) in render_content() uses the runner's backend with nerd_fonts_enabled = false. GTK tree renderer → icon.fallback = "+" for folders (see src/icons.rs: pub const FOLDER: Icon = Icon::new("\u{f07b}", "+")).
TUI is unaffected: TUI doesn't use ShellApp yet; tui_main/mod.rs calls backend.set_nerd_fonts() at startup on its single backend instance.
Blocker: set_nerd_fonts() is a concrete method on GtkBackend and TuiBackend, not on the Backend trait. So render_content(backend: &mut dyn Backend) cannot call it without a per-backend downcast (Platform-Neutrality violation).
Fix (requires quadraui change): See quadraui#391 — add fn set_nerd_fonts(&mut self, enabled: bool) to the Backend trait with a default no-op. Once that lands, add to render_content():
backend.set_nerd_fonts(engine.settings.use_nerd_fonts);
Do NOT add a per-backend workaround in vimcode — this is a genuine quadraui infrastructure gap.
Files to change
| File |
Change |
src/gtk/mod.rs → render_content() |
After drawing tab bar, call backend.tab_bar_layout() and populate self.tab_slot_positions (Bug 1a). Also add backend.set_nerd_fonts() once quadraui trait is updated (Bug 3). |
src/gtk/mod.rs → ShellApp::handle() |
Add sidebar-bounds check before editor click dispatch; route to panel-specific message for all four event kinds: MouseDown, Scroll, DoubleClick, MouseMoved, MouseUp (Bug 2). |
src/render.rs → screen_zone_hit_test() |
Single-group tab bar: change local_x: x to local_x: x - content_x (Bug 1b). |
Dependencies
- Bug 3 fix depends on quadraui#391 (
Backend::set_nerd_fonts on trait). File that first; Bugs 1 and 2 can land independently.
Exit criterion
- Click any tab → tab switches
- Click any explorer tree item → item selects / highlights
- Double-click file in explorer → file opens in editor
- Scroll within explorer/settings panel → panel scrolls
- Click any settings panel item → item responds
- Nerd font folder icons appear in GTK explorer tree (post quadraui fix)
cargo test --no-default-features passes
- GTK app boots and all above interactions work
Parent: #448 · Predecessor: #540 (#448-C)
Follow-up to #540 (#448-C), under parent #448.
The GTK main-loop flip to ShellApp (#540) merged to
developbut left several click-handling and rendering regressions. This issue tracks fixing them.Confirmed broken: tab switching, explorer tree clicks, settings panel clicks, search sidebar clicks, git sidebar clicks — any sidebar panel click. Also: sidebar scrolling, explorer double-click (open file), explorer drag. Explorer tree shows
+fallback icons instead of nerd font folder glyphs.Root Cause Analysis
Bug 1a — Tabs:
tab_slot_positionsnever populated in ShellApp rendering pathFile:
src/gtk/mod.rs→render_content()(~line 7249)The old Relm4 path called
draw::draw_tab_bar()which recovered slot hit positions viabackend.tab_bar_layout()and stored them inself.tab_slot_positions. The newrender_content()path draws the tab bar via:…but never calls
backend.tab_bar_layout()to recover the slot positions. Soself.tab_slot_positionsstays empty every frame. When you click a tab:screen_zone_hit_test()→ScreenZone::TabBar { local_x, … }tab_bar_inner_hit_test()→tab_slot_positions.get(group_id)→Noneengine.goto_tab()→ silent no-opFix: After
frame.draw(backend)for the tab bar inrender_content(), call:Then populate
self.tab_slot_positionsfromhits. The return type isquadraui::TabBarHits; slot positions are in 0..width (bar-relative) space. The same reshaping logic thatdraw.rs::draw_tab_bar()does fordiff_btn_map,split_btn_map,action_btn_mapneeds to happen here too.Bug 1b — Tabs: coordinate mismatch for single-group tab bar
File:
src/render.rs→screen_zone_hit_test()(~line 11985)Multi-group case correctly offsets:
local_x: x - b.x(subtracts group's window-absolute x). Single-group case passeslocal_x: x— raw window-absolute x. But tab slot positions are in 0..width (bar-relative) space.When the sidebar is visible (e.g., activity bar = 48px, sidebar = 250px →
main.x = 298), a click at window-x = 400 giveslocal_x = 400but the slot lives at bar-relative x = 102. Every tab miss.Current single-group code:
Fix: Use
local_x: x - content_xwherecontent_x= the window x at which the content area starts. This is available without adding new parameters:layout.windows.first().map(|w| w.rect.x).unwrap_or(0.0)gives the correct origin (windows are laid out starting frommain.x).Bug 2 — All sidebar panel clicks silently dropped
File:
src/gtk/mod.rs→ShellApp::handle()(~line 7462)Root cause: In the old Relm4 system, each sidebar panel (explorer, settings, search, git, AI, ext) had a dedicated GTK DrawingArea widget. GDK clicks on those DAs fired specific messages:
Msg::ExplorerUiEvent,Msg::ScSidebarEvent,Msg::SearchSidebarEvent, etc.In ShellApp mode, panels are rendered by quadraui controllers (
TreeController,FormController,MultiSectionView) intosidebar_content_boundsviarender_content(). The old DA widgets no longer receive GDK events. All mouse events arrive viaShellApp::handle().Flow for a sidebar click today:
AppShell::handle()→AppShellEvent::Ignored(shell only handles activity bar, divider, bottom-panel grip)ShellApp::handle()→self.dispatch(Msg::MouseClick { x, y, width, height, … })(always — no sidebar check)handle_mouse_click_msg()→pixel_to_click_target()→screen_zone_hit_test()→ no editor zone matches sidebar area →ScreenZone::None→ click droppedNote:
Msg::ExplorerUiEvent→handle_explorer_msg()→engine.explorer_tree.borrow_mut().handle(&ev, backend, rect)is already the correct backend-neutral path throughTreeController. The wiring to reach it is simply absent.Fix: In
ShellApp::handle(), before dispatchingMsg::MouseClick, check ifpositionis insidectx.layout.sidebar_content_bounds. If yes, look at the active panel and dispatch to the appropriate existing handler:The same routing gap applies to:
UiEvent::Scroll→ dispatched asMsg::MouseScroll(editor-only); sidebar panels can't scrollUiEvent::DoubleClick→ dispatched asMsg::MouseDoubleClick(editor-only); explorer double-click to open files doesn't workUiEvent::MouseMoved(left button held) → dispatched asMsg::MouseDrag(editor-only); explorer tree drag/select brokenUiEvent::MouseUp→ dispatched asMsg::MouseUp(editor-only)All four need the same sidebar-bounds check + panel dispatch added to
ShellApp::handle().Bug 3 — Explorer shows
+fallback icons instead of nerd font folder glyphsFiles:
src/gtk/mod.rsrender_content(), quadrauiBackendtraitRoot cause: ShellApp mode has two separate
GtkBackendinstances:set_nerd_fonts()called?self.backend(legacy)AppMsg::Resizehandlerbackend: &mut dyn Backendinrender_content)TreeController.render(backend, q_sb)inrender_content()uses the runner's backend withnerd_fonts_enabled = false. GTK tree renderer →icon.fallback = "+"for folders (seesrc/icons.rs:pub const FOLDER: Icon = Icon::new("\u{f07b}", "+")).TUI is unaffected: TUI doesn't use
ShellAppyet;tui_main/mod.rscallsbackend.set_nerd_fonts()at startup on its single backend instance.Blocker:
set_nerd_fonts()is a concrete method onGtkBackendandTuiBackend, not on theBackendtrait. Sorender_content(backend: &mut dyn Backend)cannot call it without a per-backend downcast (Platform-Neutrality violation).Fix (requires quadraui change): See quadraui#391 — add
fn set_nerd_fonts(&mut self, enabled: bool)to theBackendtrait with a default no-op. Once that lands, add torender_content():Do NOT add a per-backend workaround in vimcode — this is a genuine quadraui infrastructure gap.
Files to change
src/gtk/mod.rs→render_content()backend.tab_bar_layout()and populateself.tab_slot_positions(Bug 1a). Also addbackend.set_nerd_fonts()once quadraui trait is updated (Bug 3).src/gtk/mod.rs→ShellApp::handle()src/render.rs→screen_zone_hit_test()local_x: xtolocal_x: x - content_x(Bug 1b).Dependencies
Backend::set_nerd_fontson trait). File that first; Bugs 1 and 2 can land independently.Exit criterion
cargo test --no-default-featurespassesParent: #448 · Predecessor: #540 (#448-C)