Research: how should a floating panel drawn inside the terminal widget win z-order over its own grid — and does “scope first, history after” match how shells rank their suggestion lists?

Date: 2026-11-06 Question: The suggestion menu painted beneath the terminal grid’s glyphs on wgpu because iced_wgpu renders a layer by primitive kind, not by call order. Is renderer.with_layer the proven fix, or should the menu have become an Overlay-trait widget — and is the menu’s amended ranking (seat-scoped completions above history) how fish, zsh, and fzf order theirs?

TL;DR

with_layer is not merely acceptable — it is the exact mechanism iced’s own overlay pipeline uses, and the fix matches libcosmic’s own menu recipe line for line. Every Overlay-trait widget (pick_list, combo_box, libcosmic’s popover/menu/dropdown/context_drawer) floats because overlay::Nested::draw wraps each overlay’s draw in renderer.with_layer(layout.bounds(), …) (iced/core/src/overlay/nested.rs:101-113) — the Overlay trait is event routing and layout plumbing layered on top of the same child-layer mechanism DropTerm called directly. iced’s first-class float widget (“a widget that can make its contents float over other widgets”, iced/widget/src/float.rs:15) draws a shadow quad plus content inside renderer.with_layer(self.viewport, …) (float.rs:339-369) — hecrj’s own 0.14 recipe, structurally identical to DropTerm’s panel quad + rows inside with_layer(view, …). The libcosmic menu draws its panel quad, highlight quad, and item rows inside renderer.with_layer(render_bounds, draw_menu) (src/widget/menu/menu_inner.rs:814-912) while itself being an Overlay — belt and suspenders. The root cause was verified line-by-line in the pinned sources: one wgpu layer renders all quads, then triangles, then shader primitives, then images, then all text (iced/wgpu/src/lib.rs:476-661), so a quad can never sit above text in the same layer regardless of call order or fill opacity — only a child layer renders wholly after its parent. The one correction this survey forces: the fix’s comment claims tiny_skia “rasterizes in call order within a layer” — false; iced_tiny_skia batches by kind per layer exactly like wgpu (iced/tiny_skia/src/lib.rs:107-195), so the explicit layer is load-bearing on both backends, not harmless belt-and-braces (moot in practice: the applet never renders the terminal). On ranking (light coverage): explicit-completion surfaces in every surveyed shell list scope completions and never merge history into them, while the single-choice ghosts are history-first — DropTerm’s ghost keeps history-first (zsh-autosuggestions’ exact default) and its menu putting the seat’s scope above history is the completion-surface norm; a combined scope+history list has no exact peer anywhere, the nearest analog being fzf’s recency-aware --scheme=history and zsh-autosuggestions’ ordered strategy fallback.

What DropTerm has today

Findings

1. The root cause, verified against the exact sources DropTerm builds

2. How iced itself draws floating menus

3. libcosmic: the popover/menu family, and what DropTerm’s closest peer does

4. The upstream paper trail: layers are the blessed z-order

5. Ranking provenance (light pass): scope-first, history-after

Recommendation

Keep with_layer(view, …) — it is the proven mechanism, reached the short way. The alternatives, graded:

  1. Direct with_layer inside the terminal widget’s draw() (what the fix does) — adopt. This is iced’s float widget’s own implementation and libcosmic’s own menu’s inner draw, applied without the overlay plumbing. It keeps the menu anchored to live grid state (cursor cell, metrics) with zero new widgets, and the keyboard-driven menu needs none of the event routing the Overlay trait would buy. Matches the ecosystem’s most precedented shape for a floating panel with shadow, border, and rows.
  2. Promote the menu to an Overlay-trait widget — the right next step, not the right first one. It would produce the same z-order (Nested::draw wraps it in the same with_layer), plus automatic event handling above the content — the day the menu wants mouse hover/click on rows, like pick_list and libcosmic’s menus. The cost is real: a widget Tree, an overlay() implementation anchored to terminal geometry, and state routing through the app — a refactor, not a fix. Defer until mouse interaction is actually wanted (and note libcosmic’s menu still needs its own with_layer inside its Overlay for multi-level stacking — the trait alone is not always enough even for them).
  3. Redraw the grid inside a layer beneath the menu / reorder calls — rejected. Reordering cannot work by construction (§1a); redrawing the grid per menu frame would copy the entire terminal buffer for a transient popup.
  4. Draw the menu into the grid’s own buffer / as text primitives — rejected. The autosuggest records’ standing verdict: overlays never touch the terminal’s buffer (selection, search, scrollback, and copy must see nothing). A same-kind (text-after-text) hack would also surrender the panel’s rounded quad, border, and shadow, which only the quad pipeline draws.
  5. Own the whole scene like alacritty/wezterm/kitty — not available. Their escape from this bug class is owning the renderer; DropTerm’s grid is one text primitive inside iced’s scene graph by design (that is what buys IME, a11y, theming, and the drawer/context menu for free).

On ranking: keep the amendment. The ghost stays history-first (zsh-autosuggestions’ and fish’s exact single-choice shape, and the locked merge tests), and the explicit menu leads with the seat’s scope (every shell’s Tab-surface behavior) with history as the second tier — fzf’s recency-aware history scheme is the closest single-list analog. The combined list is novel but contradicted nowhere.

One commit-time correction: rewrite the draw-site comment’s tiny_skia sentence — iced_tiny_skia also batches by kind per layer (iced/tiny_skia/src/lib.rs:107-195), so the explicit layer is necessary on both backends (and the applet renders no terminal at all).

Unverified / open questions

Sources