Skip to content

Add split views within a group - #86

Merged
leafOfTree merged 79 commits into
mainfrom
split-view
Oct 22, 2026
Merged

leafOfTree merged 79 commits into
mainfrom
split-view

Conversation

@leafOfTree

Copy link
Copy Markdown
Owner

Shows two or more of a group's windows side by side or stacked, as a browser or editor does, within the group's frame. Closes #48.

What it does

  • Ways in: right-click a tab → Split view → a window on the right, left, above or below, chosen from a searchable list; or drag a tab (from this group or another) to a window's edge, with a preview of the half it takes. Any pane can be split again, so layouts form a tree with no fixed number of panes; a drop just inside the group's own edge adds a column or row to the whole group.
  • While split: the panes' tabs sit together in layout order, with a button on each seam that shows the split parting its two tabs and opens that split's menu (swap, equalize, switch direction, end). Each split has a divider: drag for the ratio, double-click to equalize, right-click for the menu. A pane's own border, maximize and move are followed. Clicking another tab parks the panes; clicking any pane's tab brings the whole tree back. Closing or moving a pane window away gives its room to its neighbour.
  • Shortcuts (unset by default, in a card of their own): a window on the right or below, and focus to the left, right, above or below.
  • Session: splits come back when their windows regroup after a restart, and with "Restore the last session after sign-in" after a sign-in; saved workspaces keep them too.
  • Taskbar: a combined group's icon shows every app in it.

How it works

A pure model (Shared/SplitView.fs: a tree of splits addressed by path, commands, geometry, placement plans) that the SplitView suite checks without windows; WindowGroup places the panes through the existing placement queue and keeps the tabs behind under a backdrop window, so apps that paint less than their frame show no seam; the divider and the empty pane run on a thread of their own and the divider places the panes itself while dragged, so it keeps up with the pointer beside slow apps. Design and testing notes: docs/split-view-design.md, docs/split-view-testing.md.

Tests

SplitView, GroupOperations, TabInteraction, GroupLifecycle, SettingsTheme, SettingsEditors and the rest of the default suites pass; SplitViewDesktop (real pointer input, opt-in) covers edge and divider drags beside slow apps and other processes, and switching between the panes and a tab behind.

Two tabs of a group can now be shown side by side or stacked, as browsers'
split views do.

- Split from the tab menu, by dragging a tab to the edge of a group's window,
  through an empty pane, or with shortcuts that are unbound by default.
- An empty pane opens out from the group's edge, takes a tab from any group,
  a window dragged by its title bar or one chosen from a searchable list,
  resizes by its edges, and closes with Esc while the tab beside it is active.
- The pair's tabs stay together with a split button on their seam. The
  divider drags, equalizes on double-click and opens the split menu; dragging
  the pair's tabs into the other order swaps the panes.
- Choosing another tab parks the pair and choosing either of its tabs shows
  it again, with the tabs kept steady over both panes and the focused pane's
  tab shown in front.
- Panes follow native moves and resizes live, tolerate apps that round their
  size, and keep the tabs on top when an app raises its own window.
- The split model is pure and has its own regression suite; an opt-in suite
  checks drags and switching on the real desktop.

Alongside:

- Activating a window of a group with a combined taskbar button keeps the
  group's windows in their order.
- With groups shown as one item, Alt+Tab draws each group with its taskbar
  icon.
- The grouping settings list a dragged group's windows three to a line, and
  its icon no longer lights up on hover.
- The auto-hide choice reads "Maximized".
- The split view documents are in English, as repository documents are from
  now on.
The divider now places both panes itself, straight into the placement queue from its own thread, and sends the group's layout one ratio at a time, the next only once the group has reported the one before. Sent through the group's thread, every pointer move queued a ratio there behind the WinEvents the apps raise as they resize, and the panes trailed the pointer by hundreds of milliseconds.
Selecting a tab behind a split parks the pair and gives that tab the whole
group, but the tab sometimes stayed at the size of the pane it had waited
under. As the group put the tab in front, it cancelled the placements still
pending for the new front window, meant for the ones that put it under
another window, and the request for the whole frame went with them whenever
the placement worker had not started it yet. The window coming to the front
now keeps a placement that leads and loses only the ones that follow.

GroupOperations switches between a pane and the third tab, with slow
windows and at once, and once with every thread-pool worker held so the
placement cannot start before the order changes. The design document notes
the rule; the WinEvent hook filtering left on the group thread is recorded
as a possible follow-up.
The split view's pair of panes becomes a tree: any pane can be split again
in either direction, with no fixed number of panes, only the 120px minimum
each. Split in group on a tab divides the shown tab's pane, an empty pane
opens beside the focused pane, and a tab dragged to a pane's edge splits
that pane; within 18px of the group's own edge it adds a column or row to
the whole group. Panes parked by the dragged tab count too, and the drop
brings them back with the new pane among them. Each split has a divider of
its own, dragged, double-clicked and right-clicked for that split alone.
Closing a pane, from the menu, by shortcut or by its window going, gives
its room to its neighbour; the last other pane fills the group. The panes'
tabs stay together in layout order with a seam button between each two,
and each carries the badge of its own split.

Native moves and resizes of a pane follow the nearest seam along the edge
moved, or the group's own edge, worked out from the frame and tree at the
start of the drag at every step so the other panes keep their size.

New shortcuts, unbound by default: focus the pane to the left, right, above
or below, and close the focused pane.

The SplitView suite covers the tree's geometry, resizes at seams and at the
group's edge, drops at pane and group edges, commands by path, parking,
removal and plans; GroupOperations nests a third window into a pane with a
divider of its own and closes it again; SplitViewDesktop fills an empty
pane from another process through the new drop kinds. The design and
testing documents describe the tree.
With panes split again, the button between two neighbouring pane tabs took
its direction from the second tab's own split, which parted two tabs from
different splits in a direction that meant nothing. Each pane tab now
carries the splits above it, root first, with the side taken at each; the
button shows the deepest split holding both tabs, where their trails first
part, and its menu acts on that split: in two rows of two, the button
between the top row's tabs shows side by side, the one between the rows
stacked.
The tabs behind waited under the first pane, but whether they lay above or
below the other panes was left to the order the placements finished in. An
app that keeps to its own grid, a terminal or an editor on character cells,
is a little larger than the pane it waits under, and above another pane its
edge, with a line of its text, showed through at the seam. Once placement
settles, and whenever the guard finds one above a pane, a tab behind goes
below the lowest pane.
Some apps paint less than the frame they report: a frameless Electron
window keeps a clear resize margin a few pixels wide inside it, and others
catch up with a new size a moment late. At a seam that margin showed
whatever lay behind the panes, a tab behind with a line of its text, say,
so the panes looked apart although their frames touch. A plain window the
colour of the theme's surface now covers the group's frame just below the
lowest pane while split, and the tabs behind wait below it, so a seam shows
at most a thin dark line.
Each pane window keeps its place in its group's split on itself, as the
group token is kept: its path through the splits with the axis at each
level, a ratio per split above it, the group's frame, and whether it was
in focus or the panes were parked. As the windows regroup after a restart,
the split comes back once two of them have arrived and grows as the rest
do; a window closed meanwhile leaves its room to its neighbour, and empty
panes are not kept. The places are saved as the layout changes and cleared
as a split ends or a window leaves, and left alone for a few seconds after
a restore so a window still to arrive finds the layout it was saved with.

The SplitView suite rounds a nested layout, parked panes, ratios and a
window gone through the saved places; GroupOperations takes a split group
apart and lets another group regroup its windows, which bring the split
back at its frame.
Window properties die with the windows, so a sign-in lost every group's
members, positions and splits. Workspaces keep a group's split now, by the
windows' places in its list with the splits above each pane and their
ratios, the frame, the focus and whether the panes were parked, and bring
it back with the windows found. On that record a session is written on its
own: as the groups change, every two minutes as windows move, and at exit
from the layouts last read. At a start whose first scan finds no window
from the last run, a sign-in, the session puts the groups, positions and
splits back, among the windows open or, as the new setting asks, starting
their apps too; a restart of WindowTabs alone leaves it to the group tokens
on the windows.

The setting, Restore the last session after sign-in, is off by default.
GroupLifecycle splits a pair, saves the session, takes the pair apart and
has the session put it back split at its frame.
The groups the first scan formed on its own were saved over the last
session three seconds in, a second before the restore read it, so a
sign-in brought back the new groups as they already were. Nothing is
written now until the start has decided about the last session: at once
when a window still carries a group token or the setting is off, else
once the restore has run.

The split's frame is also kept on screen on its own rather than moved
with the placement's correction, which allows for a window frame that
sticks out by its invisible border and so nudged the whole split a few
pixels in.
The drop preview is a clear tint with its edge drawn and a label in its
middle: a new column or row for the group, a split of the pane and which
side, or an empty pane filled. Pressing a tab behind the panes parks them,
so a drag aimed at panes that could not be seen; once the drag takes the
tab out of the group the panes come back. A tab dragged out onto the
desktop takes the size its window had before it first joined a group, kept
on the window, or three fifths of the work area when that too filled the
screen.

Restoring the last session says so in a notice at the top of the main
monitor, then how many windows came back and how many apps are being
started, so the windows are left alone while they move. The session is
written when Windows signs out, while the apps are still open, and not
again at exit; a window frame that sticks out of the work area by its
invisible border is no longer pulled in on restore.
A split's dividers, empty panes and backdrop are created on the group's chrome
thread, but their handles were read afterwards on the group thread. WinForms
refuses that under a debugger, so starting a split while debugging stopped the
program with a cross-thread exception. The handles are now read inside the
same call that creates the windows and kept beside them.

WINDOWTABS_CHECK_THREADS=1 turns that same refusal on without a debugger, in
the app and in the test hosts, so a run of the suites finds any other control
touched from the wrong thread; the lifecycle suite read a handle of its own
that way and now reads it on its thread.
With a third tab shown over a split, a tab dragged to the group's edges was
offered places beside the parked panes, out of sight behind it, and the preview
said so. The drop now goes beside the window shown, which splits with the
dropped tab, and the parked panes are let go as by any new split.

The preview names the window the drop goes beside, "Left of Notes" rather than
"Split: left", for a single window and for a pane alike; a drop at an empty
pane's edge says that it fills the pane, which it does.
A tab dragged onto the strip of a split group joined with the focus its drag
gave it, but as a tab behind: placed under the panes, it kept the focus while
the panes stayed in view with another tab shown as selected. A window joining
with the focus from outside the panes now parks them and fills the group, as
choosing its tab does.
While split, a drop that adds a column or row to the whole group had to land
within 18px of the group's own edge; everything further in went to the pane
there, and the whole-group split was hard to hit. The zone is now 36px deep at
an edge's ends and deepens towards the edge's middle, where a drop aims, to 8%
of the frame and at most 72px, in the shape of a pane's own zone; the corners
stay with the panes.
A tab dragged out onto the desktop came back at a smaller size than its group,
but was placed by the tab's position over the group's width: with centred tabs
the window landed hundreds of pixels to one side of the pointer, and near a
screen's edge partly off it or below the taskbar. The monitor was also taken
from the window's corner rather than the pointer, so such a drop kept the full
size as well.

The monitor under the pointer now decides the size; the tab sits under the
pointer as the strip's alignment places a lone tab at that width; the frame is
moved wholly into the work area; and the window's own size is kept for its
visible frame, borders around it. The drag image shows the window at the size
it will land at.
A tab dragged over another window split it as soon as the pointer entered an
edge zone, the middle fifth of each edge included: carried across a window on
its way elsewhere, or let go where no free desktop was left, it split that
window instead of coming out on its own, often into its lower half.

The preview now shows once the pointer has rested in a zone for 350ms, within
a few pixels, the same for every group; from then on it follows the pointer
over that window until it leaves the zones. Let go before it shows, the tab
comes out as a window of its own. An empty pane still takes a drop at once.
Dragged out of a group whose split it had been shown over, a tab landed under
that split: as the tab left the strip, the group showed the parked panes again
and gave one of them the foreground, and the window let go was only placed.
It is now made active and brought in front where it lands.
The label saying what letting go does sat in the middle of the highlighted
place, over what it named and away from where the eye is. It now follows the
pointer beside the dragged tab's image, naming the window the drop goes
beside and the side, kept on the screen; the place itself shows only its tint
and edge.

A split is now offered once the pointer has rested in a drop zone for 100ms
rather than 350ms.
The dragged image was the window and its tab scaled to half together, so the
tab's title was hard to read. The window is now drawn at half the size it
would land at and the tab over it at full size, under the pointer as pressed.

The label saying what letting go does sits just left of that tab, level with
it, and follows it; where the screen ends first it goes to the tab's right.
Strips of groups at the top of the screen lie over one another, a maximized
group's across the whole width. A tab carried along them joined the first
strip it crossed and stayed there while the pointer was anywhere in that
strip's width, over another group's tabs in sight too, so letting go put it
in a group out of sight. A tab held in one strip now moves to another group's
strip as soon as that is the one under the pointer.
The label beside a dragged tab only showed while a split was offered. It now
says the tab comes out as a new window wherever no split is offered, so every
place tells what letting go there does.
A tab dragged out came back at the size its window had when it first joined a
group, read once then; a group resized since came out at that old size, and
maximized or not made no difference. It now comes out at the size its group
has now, and only where that fills most of the screen at its own earlier size
or three fifths of the screen.

The dragged image was see-through as a whole, the tab with it, and the tab sat
off the middle of the window shown small. The window is now only a little
see-through and the tab solid, centred over the small window as the strip
centres a lone tab.
A tab dragged out of a group snapped to half the screen came out the screen's
full height and was pushed up to its top, away from the pointer: the group
was only judged screen-sized when both its width and height were, so a full
height alone was kept. Each side is now judged on its own: one that spans most
of the screen takes the window's earlier length or three fifths of the screen,
and the other keeps the group's.
A tab dragged out of a group that spans most of the screen came out with each
side sized apart, or at its size from before any group, so the window and its
image in the drag were out of proportion with what was dragged. The whole
window is now scaled down, proportions kept, until its widest share of the
screen is three fifths; a group of modest size comes out as it is.
Just below another group's tabs, over its title bar, a dragged tab was
offered that window's top edge, and let go after a moment it split the window
instead of joining its tabs. Within 24px of a group's tabs in sight a tab now
counts as in their strip: it joins there, and leaves only beyond the same
distance.
The label saying what letting go does, beside the dragged tab, added little
to the place lit up under the pointer, which already shows it. It is gone,
with its words; the lit place and the dragged image remain.
The tab menu opened an empty pane on the right or below, which then waited
for a window to be dropped on it or chosen from its list. The menu now offers
a window on the right, below or on the left: the searchable window list opens
over the group first, and the window picked goes on that side of the clicked
tab, splitting its pane when the group is split already. A window from
another group, or from none, is handed over first. Esc leaves the group as it
was, so no empty pane is left behind. Empty panes remain for their shortcuts.
The list a tab's menu opens to split now begins with an empty pane, set apart
by a divider and marked with a plus, for a window to be put in later. Chosen,
it opens on the side asked for, the left too, beside the tab or in its pane.
Enter with nothing typed still takes the first window, and the list opens
with only that row when there is no other window.
The list a tab's menu opens to split began with an empty pane, for a window
to be put in later. A new window can as well be opened and dragged over, so
the list offers windows only again.
Thirteen split view entries crowded the Shortcuts page among the tabs'
keys. Six remain, in their own card: a window on the right or below, which
open the tab menu's window list for the tab in front with an empty pane
first in it rather than opening an empty pane at once, and the focus to
the left, right, above or below. The next pane, swapping, equalizing,
closing a pane and ending the split stay on the tab menu, the seam buttons
and the dividers; the next tab's own shortcut already walks the panes in
order, and splitting with the next tab is one choice in the list.
# Conflicts:
#	WtProgram/Shared/Strings.fs
Its description promised an empty pane at the top of the window list,
which the list no longer offers, and ran to two lines on the Shortcuts
page. It now says only that it lists the windows to put beside the tab in
front; the documents say the same.
The tab menu lists windows first and the shortcuts that opened an empty
pane are gone, so the document no longer describes opening one; it keeps
what the empty pane is, in the model, its window and its drop handling,
for an entry that brings it back.
A tab dragged out of a group whose panes were parked brought the panes
back, whether its own press had parked them or another tab chosen over
them long before had. In the second case the panes' focused window took
the place of the tab the user had left for the dragged one. Now only panes
the press itself parked come back as the tab leaves, for a drop to aim at;
otherwise the tab shown before the dragged one comes back, which the park
had stacked under it in no particular order with every other tab.
The band taking the pointer was the 4px bar alone, so a press a few
pixels off it landed on a pane's invisible resize border and started that
app's own resize instead of the divider's drag, with nothing to tell the
two apart by eye. The band is 20px now, centred on the seam, wide enough
to cover both panes' borders: a press near the seam always drags the
divider. The bar drawn stays 4px.
At 20px the band covered a scroll bar beside the seam. 14px is just enough
for the 7px invisible resize border of either pane at the seam, so a press
there still drags the divider and no more of a pane is covered.
A restore keeps the frame on screen, so on a small screen the frame that
comes back is not the one saved. The session check in GroupLifecycle now
expects the fitted frame, as it got on a runner with one screen of 768
lines, where the saved frame reached past its bottom.
@leafOfTree
leafOfTree merged commit 9ad1282 into main Oct 22, 2026
6 of 7 checks passed
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.

[Feature request] Split tabs

1 participant