Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
7d7870d
Add split views within a group
leafOfTree Oct 19, 2026
973ec48
Keep the panes with the pointer while the divider is dragged
leafOfTree Oct 19, 2026
14af0e9
Fill the group again when a tab behind a parked split is shown
leafOfTree Oct 20, 2026
e881d5c
Split any pane again, side by side or stacked
leafOfTree Oct 20, 2026
04476ef
Show on each seam button the split that parts its two tabs
leafOfTree Oct 20, 2026
c7306ca
Keep the tabs behind a split under every pane
leafOfTree Oct 20, 2026
6beaa03
Put a backdrop behind the panes of a split
leafOfTree Oct 20, 2026
ef9c460
Keep a split across an exit or restart
leafOfTree Oct 20, 2026
7d92466
Keep the session across a sign-in and restore it at start
leafOfTree Oct 20, 2026
b165070
Hold the session until the start has restored it
leafOfTree Oct 20, 2026
65b0996
Say what a drop does, and what the session is doing
leafOfTree Oct 20, 2026
c0c7610
Read helper window handles on their own thread
leafOfTree Oct 21, 2026
1423447
Aim a drop at the window shown and name it in the preview
leafOfTree Oct 21, 2026
814cf53
Show a tab dropped onto a split group's strip over the panes
leafOfTree Oct 21, 2026
aa7ae4f
Widen the zone at a split group's edge for a new column or row
leafOfTree Oct 21, 2026
f16da00
Land a dragged-out tab under the pointer and on the screen
leafOfTree Oct 21, 2026
72925ba
Offer a split only once the pointer rests over a window
leafOfTree Oct 21, 2026
55a7377
Bring a tab dragged out onto the desktop to the front
leafOfTree Oct 21, 2026
3b64862
Say what a drop does beside the dragged tab, after a shorter rest
leafOfTree Oct 21, 2026
6803a9a
Keep the dragged tab readable and its drop label beside it
leafOfTree Oct 21, 2026
7026359
Let a dragged tab join the strip in sight under the pointer
leafOfTree Oct 21, 2026
262ac17
Say "New window" where letting go makes one
leafOfTree Oct 21, 2026
97aa358
Drag a tab out at its group's size under a solid, centred tab
leafOfTree Oct 21, 2026
96bfe4e
Size a dragged-out tab one side at a time
leafOfTree Oct 21, 2026
86e8005
Keep a dragged-out window's proportions when it is made smaller
leafOfTree Oct 21, 2026
41c64a5
Take a tab carried near a group's tabs into that group
leafOfTree Oct 21, 2026
9f967f3
Drop the label beside a dragged tab
leafOfTree Oct 21, 2026
b8ed56b
Choose the window first when splitting from a tab's menu
leafOfTree Oct 21, 2026
ce24da1
Offer an empty pane at the top of the window list
leafOfTree Oct 21, 2026
9538a1a
Take the empty pane out of the window list
leafOfTree Oct 21, 2026
fba897e
Keep each pane at least the size its window allows
leafOfTree Oct 21, 2026
25e2ae5
Move windows into a split smoothly
leafOfTree Oct 21, 2026
79c5b2a
Show every app of a group on its taskbar icon
leafOfTree Oct 22, 2026
d1bb1da
Move panes smoothly whenever a split changes
leafOfTree Oct 21, 2026
eb17247
Fade the drop preview in and out and glide it between places
leafOfTree Oct 21, 2026
bd34dd0
Take shrink, grow and fill out of the tab menu's split view items
leafOfTree Oct 22, 2026
4b4a4c1
Offer a window above and the divider's commands in the tab menu, with…
leafOfTree Oct 22, 2026
9eaba1b
Describe split view in the README
leafOfTree Oct 22, 2026
111b935
Keep a window's drop surface when its old group removes it late
leafOfTree Oct 22, 2026
5180d0e
Keep held windows in place through a transition that follows another
leafOfTree Oct 22, 2026
d351a20
Keep the panes' least sizes through a divider drag
leafOfTree Oct 22, 2026
7407875
Restore a rebuilt group's split once every transfer is settled
leafOfTree Oct 22, 2026
57772f4
Write the session only when a group's members change
leafOfTree Oct 22, 2026
cfeac74
Say in the agent guide which suites run only on request
leafOfTree Oct 22, 2026
8b1d703
Drop the window size kept for drag-out, which nothing read
leafOfTree Oct 22, 2026
f12f28e
Leave the panes' windows out of the empty pane's list on the pane's o…
leafOfTree Oct 22, 2026
87a1c50
Read the z-order once per tick of the split guard
leafOfTree Oct 22, 2026
9ab3ade
Check the least-size cache with a minimum the window still takes
leafOfTree Oct 22, 2026
6ccfc6e
Write no session after Windows starts signing out
leafOfTree Oct 22, 2026
6055ac0
Name a cached split's panes by window when the session is written at …
leafOfTree Oct 22, 2026
e4a6c54
Rebuild a split whose panes come back over several restore tries
leafOfTree Oct 22, 2026
79a34c4
Say what restoring the last session does and when
leafOfTree Oct 22, 2026
832a4c5
Shorten the restore-session setting's description
leafOfTree Oct 22, 2026
a3e3baf
Show the drop preview without a pause when a split is first offered
leafOfTree Oct 22, 2026
4930b1f
Take a tab carried anywhere along a group's row of tabs into the group
leafOfTree Oct 22, 2026
8b54f2c
Change layouts at once while a tab is dragged
leafOfTree Oct 22, 2026
c1bc193
Start a tab drag before its window's picture is printed
leafOfTree Oct 22, 2026
3c949ae
Ask a group once per pointer move what a dragged tab meets
leafOfTree Oct 22, 2026
b1a02fc
Carry the whole group when a pane is snapped with Win+Left or Win+Right
leafOfTree Oct 22, 2026
0f291eb
Cover the switch between the panes and a tab clicked over them
leafOfTree Oct 22, 2026
baec3cb
Switch between the panes and a clicked tab at once under the cover
leafOfTree Oct 22, 2026
f2d2156
Put the cover up before a clicked tab's window comes to the front
leafOfTree Oct 22, 2026
a7e1833
Switch between the panes and a clicked tab with a short animation
leafOfTree Oct 22, 2026
ead4d75
Put each further app's icon on a plate in a group's taskbar icon
leafOfTree Oct 22, 2026
e5d1553
Place the windows at once when a tab is clicked over the panes
leafOfTree Oct 22, 2026
6d97ee3
Keep the cover a moment after the windows are placed, and over tab cl…
leafOfTree Oct 22, 2026
e93785e
Fade the window shown into the next when switching tabs
leafOfTree Oct 22, 2026
f61763f
Repaint the tab strip only when its dimming changes
leafOfTree Oct 22, 2026
daaac33
Keep split covers off the tab strip
leafOfTree Oct 22, 2026
169115a
Light the chosen tab at once when the panes park or come back
leafOfTree Oct 22, 2026
a41062e
Find the split cover in tests with the tab strip cut out of it
leafOfTree Oct 22, 2026
cb502b1
Keep six split view shortcuts, in a card of their own
leafOfTree Oct 22, 2026
4c316f7
Merge branch 'main' into split-view
leafOfTree Oct 22, 2026
cabda26
Say in one line what the window-on-the-right shortcut does
leafOfTree Oct 22, 2026
e0b3652
Say in the design document that the empty pane has no way in
leafOfTree Oct 22, 2026
18f3ce7
Bring back the tab shown before one dragged out over parked panes
leafOfTree Oct 22, 2026
a16c3ef
Widen the divider's band over the panes' resize borders
leafOfTree Oct 22, 2026
cc7f6cd
Narrow the divider's band to the panes' resize borders
leafOfTree Oct 22, 2026
c869c34
Expect a restored split's frame fitted to the work area in the suite
leafOfTree Oct 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
matrix:
include:
- group: architecture
suites: 'Architecture,StartupGrouping,WindowIcon'
suites: 'Architecture,SplitView,StartupGrouping,WindowIcon'
- group: operations
suites: 'GroupOperations,SettingsEditors'
- group: ui
Expand Down
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command "& ./tests/Run-Tests.ps1
a later build), so build without exiting first. Starting any copy asks the running one to
exit as from its tray menu and takes over.
- New suites must be added to the `$Suites` default in `tests/Run-Tests.ps1`, start
with `TestInit.run main`, and reference the app as `#r "Debug/WindowTabs.exe"`.
with `TestInit.run main`, and reference the app as `#r "Debug/WindowTabs.exe"`. A suite
that moves the real pointer, such as `SplitViewDesktop`, goes in the `ValidateSet` only
and runs on request with `-VisibleDesktop` (see [docs/testing.md](docs/testing.md)).
- Desktop E2E (`tests/Run-DesktopE2E.ps1`) drives real input and takes over the
desktop; only run it when asked. See [docs/desktop-e2e.md](docs/desktop-e2e.md).
- CI (`.github/workflows/build.yml`) runs Release checks and three regression
Expand Down Expand Up @@ -83,6 +85,8 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command "& ./tests/Run-Tests.ps1

## Style

- Documents committed to the repository (`docs/`, `README.md`, this file) are written in
English; user-visible text in other languages lives only in `Shared/Strings.fs`.
- Match the surrounding F#: short `///` comments that say why, not what; no banner
comments; existing helpers (`List2`, `Set2`, `Map2`, `Cell`) where the file uses them.
- Commit subjects are imperative, sentence case, describe the user-visible effect and
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,16 @@ right-drag asks, and **Ctrl + Z** in Explorer undoes it.
- **App rules:** choose which running apps get tabs and which group automatically.
Under **Groups across apps**, create named groups so windows of different apps join
one group. Groups made by dragging tabs are remembered and listed there too.
- **Split view:** show two or more of a group's windows side by side or stacked, as in a
browser or editor. Right-click a tab → **Split view** and pick a window for each side, or
drag a tab to a window's edge. Drag the bar between the panes to resize them, right-click
it to swap, equalize, switch direction or end the split. Shortcuts for a window on the right
or below and for moving the focus between panes are under **Settings → Shortcuts**, unset by
default.
- **Workspaces:** save groups and window positions, then restore them onto open windows;
apps that are not running can be launched and their windows restored.
- **Taskbar:** optionally combine each group into one taskbar button.
- **Taskbar:** optionally combine each group into one taskbar button, which shows every
app in the group.
- **Languages:** English, 中文, 日本語.

<a href="docs/images/settings.png"><img src="docs/images/settings.png" width="600" alt="WindowTabs settings" /></a>
Expand Down
2 changes: 2 additions & 0 deletions Win32/DwmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public sealed class DwmApi
public static extern int DwmRegisterThumbnail(IntPtr dest, IntPtr src, out IntPtr thumb);
[DllImport("dwmapi.dll")]
public static extern int DwmUpdateThumbnailProperties(IntPtr hThumbnail, ref DWM_THUMBNAIL_PROPERTIES props);
[DllImport("dwmapi.dll")]
public static extern int DwmUnregisterThumbnail(IntPtr hThumbnail);
[DllImport("dwmapi.dll", EntryPoint = "#113", SetLastError = true)]
public static extern uint DwmpActivateLivePreview(bool doPeek, IntPtr hWnd, IntPtr hwndTop, bool unknown);

Expand Down
73 changes: 73 additions & 0 deletions Win32/Win32Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,22 @@ private static IntPtr QueryWindowIcon(IntPtr handle, int iconType)
return icon;
}

/// <summary>
/// The smallest size the window lets itself be sized to, its invisible borders included;
/// an empty size when it does not answer in time.
/// </summary>
public static Size GetMinTrackSize(IntPtr handle)
{
var info = new MINMAXINFO();
info.ptMaxTrackSize = new SIZE(int.MaxValue, int.MaxValue);
IntPtr result;
// Foreign windows can be busy or hung. Never block the group on one.
if (WinUserApi.SendMessageTimeout(handle, WindowMessages.WM_GETMINMAXINFO, IntPtr.Zero, ref info,
SendMessageTimeoutFlags.SMTO_ABORTIFHUNG, 100, out result) == IntPtr.Zero)
return Size.Empty;
return new Size(Math.Max(0, info.ptMinTrackSize.cx), Math.Max(0, info.ptMinTrackSize.cy));
}

public static IntPtr GetWindowIcon(IntPtr handle, int iconType)
{
bool small = iconType != IconTypeCodes.ICON_BIG;
Expand Down Expand Up @@ -503,6 +519,63 @@ public static void FlashWindow(IntPtr hwnd, uint flags, int count)
WinUserApi.FlashWindowEx(ref fInfo);
}
}
/// <summary>
/// A per-pixel layered window's picture kept between frames: drawn once per size into a
/// memory DC, so each frame only moves the window and sets its opacity. Drawn again at every
/// frame of a fade, a window the size of a pane cost a few milliseconds each time.
/// </summary>
public sealed class LayeredSurface : IDisposable
{
IntPtr dc;
IntPtr bitmap;
IntPtr original;
Size size;

public Size Size { get { return size; } }

public void Render(Bitmap picture)
{
Release();
dc = WinGdiApi.CreateCompatibleDC(IntPtr.Zero);
bitmap = picture.GetHbitmap(Color.FromArgb(0));
original = WinGdiApi.SelectObject(dc, bitmap);
size = picture.Size;
}

public void Update(IntPtr hwnd, Point location, byte alpha)
{
if (dc == IntPtr.Zero) return;
BLENDFUNCTION blend = new BLENDFUNCTION();
blend.BlendOp = AlphaChannelFlags.AC_SRC_OVER;
blend.BlendFlags = 0;
blend.AlphaFormat = AlphaChannelFlags.AC_SRC_ALPHA;
blend.SourceConstantAlpha = alpha;
POINT srcLocation = new POINT();
POINT dstLocation = POINT.FromPoint(location);
SIZE dstSize = SIZE.FromSize(size);
WinUserApi.UpdateLayeredWindow(hwnd, IntPtr.Zero, ref dstLocation, ref dstSize, dc, ref srcLocation, 0, ref blend, UpdateLayeredWindowFlags.ULW_ALPHA);
}

void Release()
{
if (dc != IntPtr.Zero)
{
WinGdiApi.SelectObject(dc, original);
WinGdiApi.DeleteObject(bitmap);
WinGdiApi.DeleteDC(dc);
dc = IntPtr.Zero;
bitmap = IntPtr.Zero;
original = IntPtr.Zero;
size = Size.Empty;
}
}

public void Dispose()
{
Release();
}
}

public static class ResourceExtractor
{
public static void ExtractResourceToFile(string resourceName, string filename)
Expand Down
2 changes: 2 additions & 0 deletions Win32/WinUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2742,6 +2742,8 @@ public static IntPtr GetClassLong(IntPtr hWnd, int nIndex)
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SendMessageTimeout(IntPtr hWnd, int message, IntPtr wParam, IntPtr lParam, int flags, int timeout, IntPtr result);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SendMessageTimeout(IntPtr hWnd, int message, IntPtr wParam, ref MINMAXINFO minMaxInfo, int flags, int timeout, out IntPtr result);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SendMessageTimeout(IntPtr hWnd, int message, int wParam, StringBuilder buffer, int flags, int timeout, out IntPtr result);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SendMessage(IntPtr hWnd, int message, IntPtr wParam, StringBuilder lParam);
Expand Down
Loading
Loading