Skip to content

windows: draw the selected tab as a folder joined to the terminal - #756

Open
deblasis wants to merge 2 commits into
fix/zero-config-themefrom
feat/folder-tab-seam
Open

windows: draw the selected tab as a folder joined to the terminal#756
deblasis wants to merge 2 commits into
fix/zero-config-themefrom
feat/folder-tab-seam

Conversation

@deblasis

@deblasis deblasis commented Aug 26, 2026

Copy link
Copy Markdown
Owner

The selected tab is stroked in the accent colour on the three sides that do not
meet the pane, and the strip behind it is that same colour, so the tab reads as
a card lifted out of the strip. On the fourth side a cover the width of the tab
is drawn over the pane border, so the tab's fill runs into the terminal with no
line across the join. Horizontal opens at the top, vertical at the left, both
get the same treatment.

Where the cover lives

Not in the strip. Content drawn there is clipped to the strip and never reaches
the pane row. It is drawn in the pane row, positioned from the selected tab's
offset within the strip viewport and clipped to that viewport, so a tab scrolled
half out of view gets half a cover rather than one hanging in space.

Three things move it, all handled:

  • opening or closing a tab. The new tab has no bounds on the first dispatcher
    pass, so a one-shot LayoutUpdated re-places it once it does.
  • resizing or moving the window.
  • switching the layout. Placed from the completion callback, since
    pre-animation geometry left it drifted by the delta.

Two fixes that came out of it

The covers are gated on whether vertical tabs are wanted, not on the hidden
strip's Visibility. That strip is Visible by XAML default and is
deliberately flipped by the priming pass, so the Visibility check hid the
cover on every launch.

UpdateCursorAccentColors ran before the layout snap and never reached the
vertical strip on the first pass, leaving the selected title at 1.11:1. It runs
after now, and measures 14.46:1.

Known limit

This holds for one pane per tab. PositionActiveBorderOverLeaf frames the
active leaf, not the tab, so with a split the folder can join to nothing.
# 753 tracks the two-border rework that makes the geometry deterministic, along
with the spawn animation.

Stack

  1. # 755 zero config, this branch's base for the rest
  2. # 756 folder tab and seam covers
  3. # 757 seam diagnostic

Merge in order. Each part is under the pr-gate size cap on its own.

Review

A subagent review over this branch found, and the second commit fixes: the
vertical seam clipping to the host rather than to the scrolling row list, so
the clamp did nothing and a scrolled-out row drew a bar into the title bar;
both covers surviving the quake strip being force-hidden; the layout retry
able to re-arm forever on a collapsed strip, and the layout-switch completion
asking the strip that had just left; no coalescing, so one new tab re-placed
the cover four to six times; and the two seam handlers not catching the COM
and null shapes XAML throws on a tree going down, from dispatcher callbacks
where an escape is unhandled.

It also produced a mechanism for the reported access violation inside
NavigationView.set_SelectedItem, and the fix for it. Read that commit
message before trusting it: the crash still has no reproduction.

The selected tab is stroked in the accent colour on the three sides that do
not meet the pane, and the strip behind it is the same colour as that
stroke, so the tab reads as a card lifted out of the strip. On the fourth
side, where the tab meets the terminal, a cover the width of the tab is
drawn over the pane border, so the tab fill runs into the terminal with no
line across the join. Horizontal tabs open at the top, vertical tabs at the
left, and both get the same treatment.

The cover cannot live in the strip: content drawn there is clipped to the
strip and never reaches the pane row. It is drawn in the pane row instead,
positioned from the selected tab's offset within the strip viewport and
clipped to that viewport, so a tab scrolled half out of view gets half a
cover rather than one hanging in space.

Three things move it, and all three are handled: opening or closing a tab
(the new tab has no bounds on the first dispatcher pass, so a one-shot
LayoutUpdated re-places it once it does), resizing or moving the window,
and switching the layout (placed from the completion callback, since
placing it from pre-animation geometry left it drifted by the delta).

The vertical strip's selected title also gains its accent colour at
startup. UpdateCursorAccentColors ran before the layout snap and so never
reached the strip on the first pass, leaving the selected title at 1.11:1.
It now runs after, and measures 14.46:1.

The covers are gated on whether vertical tabs are wanted, not on the hidden
strip's Visibility. The hidden strip is Visible by XAML default and is
deliberately flipped by the priming pass, so the Visibility check hid the
cover on every launch.
Crash. A user hit an access violation inside NavigationView.set_SelectedItem
from SyncSelectionFromManager on a new tab. In horizontal-tab mode the
vertical strip is collapsed and, by design, never primed -- priming it from
the constructor crashed XAML's measure walk, so it has no template for the
whole session. Assigning SelectedItem is where MUXC resolves the selected
item's container and selection indicator, and there is nothing to resolve on
a control that has never been laid out. The sync is now latched while the
strip is unloaded and replayed on Loaded.

This is a hypothesis with a mechanism behind it, not a reproduced fix: the
crash did not reproduce in 45 tab opens across three launches, and it is
still worth landing because driving a never-templated control was wrong
regardless.

The MenuItems mutations are also fenced with the sync latch now. An insert
before the current selection shifts what MUXC considers selected and raises
SelectionChanged for a tab the user did not pick, which activates the wrong
tab and comes back around to assign SelectedItem from inside MUXC's own
notification. Reachable from Move Tab Left/Right.

Vertical seam. The clip meant to keep the cover inside the scrolling row
list clipped to the host instead, which is Row 0 with RowSpan 2 and so
covers the whole window -- the clamp did nothing. Worse, the top half of it
clamped to the top of the caption row, so a row scrolled above the list drew
a bar starting in the title bar. It clips to the list's own scroller now.

Quake. SetStripHidden collapses both strips without re-raising either seam
event, leaving a bar of tab colour lying across a terminal with no strip
above it. Both covers come down with the strip.

Cost and lifetime. QueueBridgeUpdate had no coalescing, so one new tab
re-placed the cover four to six times. The layout retry could re-arm
forever: it fires when the selected tab reports zero width, a collapsed
never-primed strip reports that permanently, and the layout-switch
completion asked the strip that had just LEFT for a fresh placement. It asks
the one that arrived, refuses to arm on a collapsed strip, and caps the
re-arms. Both seam handlers now also catch the COM and null shapes XAML
interop throws on a tree that is going down, since they run from dispatcher
callbacks where an escape is an unhandled exception on the UI thread, and
both events are detached on close.

Reverted an unreferenced x:Name and a stray blank line in TabHost.xaml.

Not taken: hopping the collection-changed sync onto the dispatcher. The
argument for it was that it runs inside MUXC's own MenuItems processing,
which it does not -- MUXC has finished by the time our handler continues.
It also breaks a wiring test that pins the collection change as the only
signal reaching the strip when a non-active tab closes. The latch above is
what addresses the reported stack.
@deblasis
deblasis force-pushed the feat/folder-tab-seam branch from a38b1e6 to 1528a10 Compare August 26, 2026 10:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant