Partial-execution of #480. Quadraui #257 (Toolbar primitive) and #259 (SidebarPanel + multi-row toolbar + hover tracker) have shipped, so the button-row chunk of the Source Control panel is now unblocked.
What this replaces
The SC panel currently hand-paints the same 4-button row twice, plus owns a manual hit-test:
| Path |
Lines |
What it does |
| `src/tui_main/panels.rs:858-925` |
~67 |
TUI button strip — equal-share widths, hover/focus bg, icon-only paint for Push/Pull/Sync |
| `src/gtk/draw.rs:2778-2845` |
~67 |
GTK button strip — same shape, Cairo +Pango |
| `src/core/engine/source_control.rs:1178-1189` |
~12 |
`sc_button_hit_test()` — hand-rolled `rel_x / total_width` arithmetic to resolve click → button index |
| `src/tui_main/mouse.rs:1653-1656, 2389-2392` |
~10 |
Mouse hover + click dispatch wiring |
~156 lines across four files, plus the engine state (`sc.button_focused: Option`, `sc.button_hovered: Option`) which today is bookkeeping the hover-tracker now owns.
Mapping to quadraui
Each existing button → `ToolbarButton::Action`:
| SC button |
ToolbarButton::Action |
| Commit (icon + label, ~50% width) |
`{ id: 'sc:commit', label: 'Commit', icon: Some(GIT_COMMIT), key_hint: Some('c'), enabled: !commit_message.is_empty(), .. }` |
| Push (icon-only, equal share) |
`{ id: 'sc:push', label: '', icon: Some(GIT_PUSH), .. }` — Gap-4 icon-only compaction kicks in here |
| Pull (icon-only, equal share) |
`{ id: 'sc:pull', label: '', icon: Some(GIT_PULL), .. }` |
| Sync (icon-only, equal share) |
`{ id: 'sc:sync', label: '', icon: Some(GIT_SYNC), .. }` |
`backend.draw_toolbar(button_row_rect, &bar, hover_tracker.hovered_id(), pressed_id)` replaces both hand-painters. Click routing collapses to `layout.hit_test(x, y)`. `sc_button_hit_test` deletes.
The toolbar's `enabled: !commit_message.is_empty()` on Commit is a genuine win — today the SC panel doesn't grey out Commit when the message is empty (it just no-ops on click). The new primitive makes "disabled with dim paint" first-class.
Bigger opportunity (optional, follow-up)
The bottom half of the SC panel — button row + sections list — has the exact shape of `quadraui::SidebarPanel`: optional toolbar header + content region with coordinated layout, hit-test, and slot-reservation. Migrating to:
```rust
let sc_panel = SidebarPanel {
id: WidgetId::new('sc:panel'),
toolbar: Some(sc_button_toolbar()), // 4-button row above
toolbar_height: Some(1.0), // 1 cell TUI, line_height GTK
};
let layout = backend.draw_sidebar_panel(rect, &sc_panel, hover.hovered_id(), pressed.as_ref());
// Paint sections into layout.content_bounds
sc_sidebar_system.render(backend, layout.content_bounds);
```
This eliminates the `y_commit` cursor + manual `section_start_y` math + the per-backend coordinate threading. Header + commit input stay separate (they don't fit SidebarPanel's shape) — only the button-row + sections pairing benefits.
Recommended order: do the Toolbar swap first (smallest, cleanest), evaluate the SidebarPanel adoption second once the Toolbar is wired through.
Other vimcode targets (separate issues)
This unblocks one of #480's four chunks. The debug toolbar (`debug_toolbar_to_quadraui_status_bar` at `src/render.rs:1954`) is the other obvious Toolbar adoption point but lives outside the SC panel — file separately when picked up.
Test plan
- Round-trip mouse hover → `hovered_id` → painted hover bg verified by harness.
- Click on each button fires the existing engine action (`sc_activate_button(idx)`).
- Disabled Commit no-ops on click (and renders dim).
- Both backends pass the same harness.
Partial-execution of #480. Quadraui #257 (Toolbar primitive) and #259 (SidebarPanel + multi-row toolbar + hover tracker) have shipped, so the button-row chunk of the Source Control panel is now unblocked.
What this replaces
The SC panel currently hand-paints the same 4-button row twice, plus owns a manual hit-test:
~156 lines across four files, plus the engine state (`sc.button_focused: Option`, `sc.button_hovered: Option`) which today is bookkeeping the hover-tracker now owns.
Mapping to quadraui
Each existing button → `ToolbarButton::Action`:
`backend.draw_toolbar(button_row_rect, &bar, hover_tracker.hovered_id(), pressed_id)` replaces both hand-painters. Click routing collapses to `layout.hit_test(x, y)`. `sc_button_hit_test` deletes.
The toolbar's `enabled: !commit_message.is_empty()` on Commit is a genuine win — today the SC panel doesn't grey out Commit when the message is empty (it just no-ops on click). The new primitive makes "disabled with dim paint" first-class.
Bigger opportunity (optional, follow-up)
The bottom half of the SC panel — button row + sections list — has the exact shape of `quadraui::SidebarPanel`: optional toolbar header + content region with coordinated layout, hit-test, and slot-reservation. Migrating to:
```rust
let sc_panel = SidebarPanel {
id: WidgetId::new('sc:panel'),
toolbar: Some(sc_button_toolbar()), // 4-button row above
toolbar_height: Some(1.0), // 1 cell TUI, line_height GTK
};
let layout = backend.draw_sidebar_panel(rect, &sc_panel, hover.hovered_id(), pressed.as_ref());
// Paint sections into layout.content_bounds
sc_sidebar_system.render(backend, layout.content_bounds);
```
This eliminates the `y_commit` cursor + manual `section_start_y` math + the per-backend coordinate threading. Header + commit input stay separate (they don't fit SidebarPanel's shape) — only the button-row + sections pairing benefits.
Recommended order: do the Toolbar swap first (smallest, cleanest), evaluate the SidebarPanel adoption second once the Toolbar is wired through.
Other vimcode targets (separate issues)
This unblocks one of #480's four chunks. The debug toolbar (`debug_toolbar_to_quadraui_status_bar` at `src/render.rs:1954`) is the other obvious Toolbar adoption point but lives outside the SC panel — file separately when picked up.
Test plan