You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This is the last of the four big pockets and the hardest, because unlike mouse (#733)
and keys (#734) the two sides are not pure logic — they end in genuinely
different rasterisers.
What is already shared, and what is not
Shared and healthy — the content.render.rs exports ~30 builders both composers
call: build_screen_layout, build_tab_bar_primitive, build_global_status_bar, build_window_status_line, build_activity_bar, build_minimap_data, build_terminal_draw_data, build_bottom_panel_tab_bar, build_command_center_view, build_toast_stack, quickfix_to_list_view, picker_panel_to_palette, settings_to_form, source_control_to_tree_view, ext_panel_to_tree_view, debug_output_to_text_display, the populate_*_system family, and more. Every surface
in PROJECT_STATE.md's cross-backend coverage table is ✅ on both backends.
Duplicated — the composition. Which surface is drawn, in what order, into which
rect, under what condition. Both composers walk the same ladder:
menu row → activity bar → sidebar (+ separator) → editor windows → group dividers
→ tab drag overlay → tab hover tooltip → editor popups (completion / hover /
editor-hover / diff-peek / signature-help) → quickfix → separated status line
→ bottom panel (tab bar + terminal | debug output) → debug toolbar → wildmenu
→ status / command line → panel hover popup → folder picker → find/replace
→ unified picker → dialog → context menu → toasts
TUI's is at render_impl.rs:285-1034 with a banner comment per rung; GTK's is the
equivalent run inside mod.rs:8177-9710. Z-order between them has already been
inverted once — #592's own body documents "modal z-order is exactly inverted between
the backends" for the four modals both painted. That is the failure mode: two hand-kept
orderings with nothing asserting they agree.
Why this is the hard one — read before scoping
Do not assume the #733route_* shape transfers. It does not, on its own:
Units differ. GTK composes in pixels, TUI in cells. render::compute_editor_layout
already handles this by taking line_height (GTK passes real px, TUI passes 1.0) —
the shared composer must do the same throughout, not just for chrome heights.
TUI has a raw-Buffer residue.#595 Stage 2 — TuiShellApp::render_content paints for real through &mut dyn Backend #601's scoping note (now in PLAN.md's completed-wave
section) established that render_content(&self, backend: &mut dyn Backend, ...) can
never obtain a raw ratatui::Frame. draw_framecan — it is the older path and
still takes frame. Some of what it paints was never portable to the trait. Enumerate
that set first; it determines whether this is one issue or three.
Painter model differs. Cairo painter-order vs ratatui cell coalescence is an
intrinsic divergence, called out in PROJECT_STATE.md. A shared composer must emit an order, and let each backend's rasteriser resolve overlap its own way — it must not
try to unify overdraw semantics.
Proposed shape
A shared render::compose_frame(&ScreenLayout, &AppShellLayout, Metrics) -> Vec<FrameOp>
— an ordered list of "draw surface S into rect R" instructions in unit-agnostic terms.
Each backend walks the list and dispatches each FrameOp to its Backend::draw_*
equivalent. Composition order and geometry become one artefact; rasterisation stays
per-backend, which is where the genuine difference lives.
The z-order test then becomes possible for the first time: assert the two backends
produce the same FrameOp sequence for the same ScreenLayout, which is a cheap
unit test and would have caught #592's inversion at authoring time.
Check quadraui first. If quadraui's AppShell/ShellApp seam can own the composition
list, that is where it belongs and this becomes an adoption issue (CLAUDE.md).
Also in scope: the fields GTK never reads
grep -rn across src/gtk/ finds zero readers for these ScreenLayout fields: diff_toolbar, tab_scroll_offset, group_dividers, tab_bar_primitive, menu_dropdown_open. Some are legitimately GTK-computed elsewhere (group_dividers is
discarded into _group_dividers at mod.rs:4743 and :5172); others may be #592-class
gaps that the epic's 14-field table did not cover. A shared composer makes "populated but
never composed" a structurally impossible state, so resolve each one on the way through
and record the verdict per field. (ai_panel is already tracked as #730 — do not
duplicate it here.)
Staging
Genuinely multi-session. Suggested cut:
Enumerate the raw-Buffer residue in draw_frame and file it separately if it is
more than a rung or two. Everything else waits on this answer.
Chrome band — menu row, activity bar, sidebar, status/command line, wildmenu
Editor band — windows, group dividers, minimap, breadcrumbs, tab bars
Bottom band — quickfix, separated status line, bottom panel, debug toolbar
Overlays and modals — the z-order-sensitive set; land the FrameOp-sequence equality
test with this slice
Delete draw_frame and collapse GTK's render_content to a FrameOp walk
Acceptance criteria
Per slice, plus:
A test asserting both backends emit the same FrameOp sequence for a given ScreenLayout, verified RED by reordering one rung on one backend.
Black-box coverage on both backends per CLAUDE.md, RED-verified against unfixed develop.
Both bespoke composition arms deleted in the same PR as the shared one lands.
Every field in the "never read" list above has a recorded verdict: composed, or
deliberately GTK-computed elsewhere with the reason.
Last of the four pockets. After #731, #732, #733 and #734 — those
delete ~5,000 lines that this issue would otherwise have to compose around, and #733
establishes the shared-router pattern this follows.
⚠️ Line numbers in this body drift — locate by symbol
Every line number above was measured on develop @ 4872ab9 (2026-09-01). They are
already wrong.#727 landed src/gtk/mod.rs +435/−44 within hours and moved enum Msg 1096→1131, fn dispatch 1799→1896, sync_scrollbar 2657→2754. Seven more
issues sit ahead of this one in the chain, each deleting or moving thousands of lines.
Treat the numbers as provenance — proof the claim was verified once — never as
coordinates. Locate by these anchors instead:
This is the same defect #734 exists to fix: src/tui_main/ carries 19 mirrors mod.rs:NNNN comments whose targets all drifted after #540, leaving the only
record of a cross-backend contract pointing at unrelated code. Do not re-point the
numbers when they rot — the anchors are the durable form.
Summary
Frame composition — the order and geometry in which a frame's surfaces are laid down —
is written twice, ~4,500 production lines:
render_content(src/gtk/mod.rs:8177)render_content(src/tui_main/shell_app.rs:1295)draw_frame(src/tui_main/render_impl.rs:285)panels.rsrender helpers (render_sidebar,render_source_control,render_ext_panel,render_ai_sidebar, …)This is the last of the four big pockets and the hardest, because unlike mouse (#733)
and keys (#734) the two sides are not pure logic — they end in genuinely
different rasterisers.
What is already shared, and what is not
Shared and healthy — the content.
render.rsexports ~30 builders both composerscall:
build_screen_layout,build_tab_bar_primitive,build_global_status_bar,build_window_status_line,build_activity_bar,build_minimap_data,build_terminal_draw_data,build_bottom_panel_tab_bar,build_command_center_view,build_toast_stack,quickfix_to_list_view,picker_panel_to_palette,settings_to_form,source_control_to_tree_view,ext_panel_to_tree_view,debug_output_to_text_display, thepopulate_*_systemfamily, and more. Every surfacein
PROJECT_STATE.md's cross-backend coverage table is ✅ on both backends.Duplicated — the composition. Which surface is drawn, in what order, into which
rect, under what condition. Both composers walk the same ladder:
TUI's is at
render_impl.rs:285-1034with a banner comment per rung; GTK's is theequivalent run inside
mod.rs:8177-9710. Z-order between them has already beeninverted once — #592's own body documents "modal z-order is exactly inverted between
the backends" for the four modals both painted. That is the failure mode: two hand-kept
orderings with nothing asserting they agree.
Why this is the hard one — read before scoping
Do not assume the #733
route_*shape transfers. It does not, on its own:render::compute_editor_layoutalready handles this by taking
line_height(GTK passes real px, TUI passes1.0) —the shared composer must do the same throughout, not just for chrome heights.
Bufferresidue. #595 Stage 2 — TuiShellApp::render_content paints for real through &mut dyn Backend #601's scoping note (now in PLAN.md's completed-wavesection) established that
render_content(&self, backend: &mut dyn Backend, ...)cannever obtain a raw
ratatui::Frame.draw_framecan — it is the older path andstill takes
frame. Some of what it paints was never portable to the trait. Enumeratethat set first; it determines whether this is one issue or three.
intrinsic divergence, called out in
PROJECT_STATE.md. A shared composer must emit anorder, and let each backend's rasteriser resolve overlap its own way — it must not
try to unify overdraw semantics.
Proposed shape
A shared
render::compose_frame(&ScreenLayout, &AppShellLayout, Metrics) -> Vec<FrameOp>— an ordered list of "draw surface S into rect R" instructions in unit-agnostic terms.
Each backend walks the list and dispatches each
FrameOpto itsBackend::draw_*equivalent. Composition order and geometry become one artefact; rasterisation stays
per-backend, which is where the genuine difference lives.
The z-order test then becomes possible for the first time: assert the two backends
produce the same
FrameOpsequence for the sameScreenLayout, which is a cheapunit test and would have caught #592's inversion at authoring time.
Check quadraui first. If quadraui's
AppShell/ShellAppseam can own the compositionlist, that is where it belongs and this becomes an adoption issue (
CLAUDE.md).Also in scope: the fields GTK never reads
grep -rnacrosssrc/gtk/finds zero readers for theseScreenLayoutfields:diff_toolbar,tab_scroll_offset,group_dividers,tab_bar_primitive,menu_dropdown_open. Some are legitimately GTK-computed elsewhere (group_dividersisdiscarded into
_group_dividersatmod.rs:4743and:5172); others may be #592-classgaps that the epic's 14-field table did not cover. A shared composer makes "populated but
never composed" a structurally impossible state, so resolve each one on the way through
and record the verdict per field. (
ai_panelis already tracked as #730 — do notduplicate it here.)
Staging
Genuinely multi-session. Suggested cut:
Bufferresidue indraw_frameand file it separately if it ismore than a rung or two. Everything else waits on this answer.
FrameOp-sequence equalitytest with this slice
draw_frameand collapse GTK'srender_contentto aFrameOpwalkAcceptance criteria
Per slice, plus:
FrameOpsequence for a givenScreenLayout, verified RED by reordering one rung on one backend.CLAUDE.md, RED-verified against unfixeddevelop.deliberately GTK-computed elsewhere with the reason.
cargo build && cargo test && cargo clippy -- -D warnings && cargo fmt --checkEXIT=0.Sequencing
Last of the four pockets. After #731, #732, #733 and #734 — those
delete ~5,000 lines that this issue would otherwise have to compose around, and #733
establishes the shared-router pattern this follows.
Files
src/render.rssrc/gtk/mod.rssrc/tui_main/shell_app.rs,src/tui_main/render_impl.rs,src/tui_main/panels.rssrc/gtk/testing.rsMilestone
#7 Platform-Neutral
Every line number above was measured on
develop @ 4872ab9(2026-09-01). They arealready wrong. #727 landed
src/gtk/mod.rs+435/−44 within hours and movedenum Msg1096→1131,fn dispatch1799→1896,sync_scrollbar2657→2754. Seven moreissues sit ahead of this one in the chain, each deleting or moving thousands of lines.
Treat the numbers as provenance — proof the claim was verified once — never as
coordinates. Locate by these anchors instead:
This is the same defect #734 exists to fix:
src/tui_main/carries 19mirrors mod.rs:NNNNcomments whose targets all drifted after #540, leaving the onlyrecord of a cross-backend contract pointing at unrelated code. Do not re-point the
numbers when they rot — the anchors are the durable form.