Freshness: 2026-08-05 · commit
c77517c· coversControls/,Animations/,Composition/,Converters/,Themes/Template equivalent: frontend.
Everything the user sees. Two stacks meet here — WPF for layout and chrome, Windows.UI.Composition for
the live window previews. They are bridged by CompositionHost, an HwndHost.
| File | Lines | Role |
|---|---|---|
CaptureSession.cs |
584 | one live window → one composition visual tree |
D3DDeviceHolder.cs |
211 | shared D3D11 device + IDirect3DDevice, device-lost recovery |
CompositionHost.cs |
130 | HwndHost child window with WS_EX_NOREDIRECTIONBITMAP; Root visual setter |
CompositorFactory.cs |
65 | per-thread Compositor + DesktopWindowTarget |
DispatcherQueueHelper.cs |
57 | CreateDispatcherQueueController so WPF's thread can host a compositor |
Interop/CompositionInterop.cs |
73 | ICompositorInterop, ICompositorDesktopInterop, ICompositionDrawingSurfaceInterop |
Interop/Direct3DInterop.cs |
75 | CreateDirect3D11DeviceFromDXGIDevice, IDirect3DDxgiInterfaceAccess |
Interop/GraphicsCaptureItemInterop.cs |
69 | IGraphicsCaptureItemInterop.CreateForWindow |
Three layers, because Composition only foreshortens a child visual's 3D rotation — perspective must sit alone on an ancestor.
_rootContainer ContainerVisual TransformMatrix = pure perspective, M34 = -1/depth
└ _content ContainerVisual TransformMatrix = affine (shear, scale, translate)
└ _sprite SpriteVisual RotationAngle about Y, brush = capture surface
GeometricClip = rounded rect
Public setters, all idempotent and dirty-checked by callers:
Start() Pause() Resume() Dispose()
SetVisualSize(hwndPixels, basePixels)
SetTransformMatrix(Matrix4x4) -> _content
SetSpriteRotationY(degrees) -> _sprite
SetPerspective(depthPx) -> _rootContainer
SetOpacity(float) SetCornerRadius(radiusPx, sizePx)
RootVisual : Visual? event TargetClosed
Pause/Resume take a per-HWND SemaphoreSlim with a non-blocking Wait(0); contention defers to
the threadpool. This is deliberate — a blocking wait on the UI thread during WM_CLOSE deadlocks, and
disposing a WinRT session inside an input-synchronous message pump throws RPC_E_CANTCALLOUT_ININPUTSYNCCALL
(0x8001010D), which is unhandleable. See Controls/CompositionThumbnail.ShutdownAll.
| File | Lines | Role |
|---|---|---|
CompositionThumbnail.xaml.cs |
640 | the sidebar tile; owns a CaptureSession, solves tile geometry |
IconOverlayManager.cs |
458 | per-scene app icons and labels in layered windows |
IconOverlayWindow.xaml.cs |
73 | one layered, click-through overlay window |
LayeredOverlayWindowBase.cs |
45 | `WS_EX_LAYERED |
Dependency properties: PreviewHandle, CornerRadius, TopEdgeDegrees, BottomEdgeDegrees,
MirrorScale, MirrorOpacity.
Constants:
| Name | Value | Meaning |
|---|---|---|
TrayTiltDegrees |
2.0 |
resting shear of a sidebar tile |
PerspectiveDepthPx |
220.0 |
vanishing distance; fixed for tile and flying card alike |
HoverHeadroom |
0.30 |
per-side HWND inflation (1.6× total) so the near edge isn't clipped |
SolveEdgeAngles(topDeg, bottomDeg, baseHeightPx) -> (shearDeg, spriteRotationDeg) is the single
source of truth for the trapezoid. Given the two requested edge slopes it splits them into a shear
component and a convergence component, then inverts Q = H·tan(θ)/(2·depth) for θ. Because θ is solved
against the live height, the look is size- and DPI-independent.
Both ApplyTransform (resting tile) and LiveCardHost.SetEdgeShape (flying card) call it. Nothing else
may compute a rotation angle.
Static shutdown registry: s_live + s_shuttingDown, drained by ShutdownAll() from
MainWindow.OnClosing. OnIsVisibleChanged, OnUnloaded and StartCaptureIfReady all early-out once
the flag is set.
Visual lending: BorrowRootVisual() / ReturnRootVisual() let an animation take the tile's visual and
give it back. Session is exposed for the same purpose.
| File | Lines | Role |
|---|---|---|
DragDropManager.cs |
285 | stage → sidebar drag; BufferWidthLogical = 120.0 |
LiveCardHost.cs |
226 | flying card backed by a real capture session |
SceneTransitionAnimator.cs |
178 | drives the overlay animation, owns TransitionOverlayWindow |
DragGhostWindow.cs |
141 | layered ghost window during a stage drag |
SidebarDragGhost.cs |
100 | sidebar → stage ghost |
DebugZoneOverlay.cs |
73 | visualises drag zones when enabled |
PlaceholderFactory.cs |
48 | static fallback card |
BorderCard.cs |
43 | IFlyingCard that draws a plain border, no capture |
Anim.cs |
35 | DoubleAnimation factory — To, From, Storyboard (target/property pre-bound) |
IFlyingCard.cs |
22 | Update(baseRect, skewDegrees), SetVisible, Release |
IFlyingCard has three implementations: LiveCardHost (real frames), BorderCard, and the
PlaceholderFactory output. Callers never branch on which.
Two flavours: borrowed (TryBorrow takes a tile's running session; Release returns the visual) and
owned (TryCreateOwned starts a fresh session for a window with no tile, e.g. the outgoing scene;
Release disposes it).
Mount states SetPerspective(PerspectiveDepthPx) once. Update then calls SetEdgeShape, which
re-solves the trapezoid against the card's current height every frame. Owned cards carry
_restTop/_restBottomEdgeDegrees == 0 and take an early-out to a plain shear.
Why the re-solve exists: the card previously kept the rotation the tile solved for a ~200 px sprite while growing to ~1 600 px. Q scales with H, so the perspective divide ran away — one vertical edge scaled ~1.85×, the other ~0.69×, a 2.7:1 trapezoid taller than the display.
| File | Role |
|---|---|
MainWindow.xaml |
sidebar ItemsControl, Grid ItemsPanel, tray NotifyIcon, context menu |
Animations/TransitionOverlayWindow.xaml |
full-screen transparent canvas that hosts flying cards |
Controls/CompositionThumbnail.xaml |
grid + CompositionHost placement |
Controls/IconOverlayWindow.xaml |
icon/label layer |
Themes/{DarkColors,LightColors,TrayMenuTheme}.xaml |
resource dictionaries swapped by ThemeManager |
Converters/IndexToOffsetMarginConverter.cs |
per-row sidebar offset |
Converters/IndexToZIndexConverter.cs |
stacking order within a scene |
Tiles use HorizontalAlignment="Left" — WPF's default Stretch centres a fixed-width child, which
left tiles of differing widths ragged.
Sidebar hit-test. CompositionThumbnail.xaml:12,23 set Background="#01000000". The sidebar is a
layered top-level window, so it must register non-zero alpha at tile locations or WindowFromPoint
falls through to whatever is behind it. Alpha 1/255 is invisible and sufficient.
Sidebar parking. MainWindow.ApplyWindowMode (:1329) moves the sidebar to Left = -Width in
WindowMode.OffScreen. Opacity = 0 alone would stop DWM compositing the tiles; parking keeps frames
flowing. Slide-in animates Left back to 0 on hover — the edge trigger is e.Data.X <= 44 (:1429).
WindowMode.Flyover sets Topmost.
The transition overlay carries WS_EX_TOOLWINDOW | WS_EX_TRANSPARENT so it stays out of Alt-Tab and
never intercepts clicks.
- Every capture visual belongs to the compositor of the thread that created it. Cross-thread mounting
throws;
CompositorFactoryis per-thread for this reason. HwndHostchildren do not clip to WPF layout. HenceHoverHeadroominflation instead of overflow.- Device loss is recovered inside
D3DDeviceHolder; sessions rebuild their frame pool onResume. - .NET 10 WPF removed
PlaneProjection— noted atAnimations/BorderCard.cs:13andAnimations/PlaceholderFactory.cs:24. There is no framework route back to 3D tilt.