private/Controls/Set-UiFillParentHeight.ps1

function Set-UiFillParentHeight {
    <#
    .SYNOPSIS
        Sizes a control to claim the rest of the window's vertical space.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Windows.FrameworkElement]$Control,

        # DataGrid/Tree/List already stretch via their own templates and don't need this
        [System.Windows.Controls.Panel]$TrackParentWidth,

        [double]$MaxHeight = [double]::PositiveInfinity,

        # Minimum on the computed height. Prevents siblings above from pushing the control below a usable amount
        [double]$MinHeight = 50
    )

    $ctlRef    = $Control
    $parentRef = $TrackParentWidth
    $maxOuter  = $MaxHeight
    $minOuter  = $MinHeight

    $Control.Add_Loaded({
        param($sender, $eventArgs)

        # Recatpure vars since nested GetNewClosure doesnt capture closure inherited vars.
        $ctl = $ctlRef
        $pnt = $parentRef
        $max = $maxOuter
        $min = $minOuter

        # Width tracker (Grid only)
        $widthHandler = $null
        if ($pnt) {
            $widthHandler = {
                param($sizeSender, $sizeArgs)
                if ($sizeSender.ActualWidth -gt 50) { $ctl.Width = $sizeSender.ActualWidth }
            }.GetNewClosure()
            $pnt.add_SizeChanged($widthHandler)
            if ($pnt.ActualWidth -gt 50) { $ctl.Width = $pnt.ActualWidth }
        }

        # Shrink so the natural content height doesn't push past the ScrollViewer, otherwise the math'll go in the neg
        $ctl.Height = 200

        # Walk outwards to the outter ScrollViewer, not the inner
        $sv = $null
        $walker = [System.Windows.Media.VisualTreeHelper]::GetParent($ctl)
        while ($walker) {
            if ($walker -is [System.Windows.Controls.ScrollViewer]) { $sv = $walker }
            if ($walker -is [System.Windows.Window]) { break }
            $walker = [System.Windows.Media.VisualTreeHelper]::GetParent($walker)
        }
        if (!$sv) { return }

        $svLocal  = $sv
        $ctlLocal = $ctl
        $maxL     = $max
        $minL     = $min

        $siblingCache = @{ Items = $null; ChildCount = -1 }
        $computeState = @{ FirstDone = $false }

        $computeHeight = {
            if (!$ctlLocal -or !$svLocal) { return }
            if (!$ctlLocal.IsVisible) { return }

            $vh = $svLocal.ViewportHeight
            if ($vh -le 0) { return }

            $offset = 80.0
            try {
                $pt = $ctlLocal.TranslatePoint([System.Windows.Point]::new(0, 0), $svLocal)
                $offset = [double]$pt.Y
            }
            catch { Write-Debug 'Set-UiFillParentHeight: TranslatePoint failed' }

            # Cached sibling list. Recompute when parent.Children.Count changes.
            $parentPanel = $ctlLocal.Parent
            if ($parentPanel -is [System.Windows.Controls.Panel]) {
                
                $cnt = $parentPanel.Children.Count
                
                if ($siblingCache.ChildCount -ne $cnt) {
                    $sibs  = [System.Collections.Generic.List[object]]::new()
                    $myIdx = $parentPanel.Children.IndexOf($ctlLocal)
                    if ($myIdx -ge 0) {
                        for ($i = $myIdx + 1; $i -lt $cnt; $i++) {
                            [void]$sibs.Add($parentPanel.Children[$i])
                        }
                    }
                    $siblingCache.Items = $sibs
                    $siblingCache.ChildCount = $cnt
                }
            }

            $siblingsBelow = 0.0
            if ($siblingCache.Items) {
                foreach ($sib in $siblingCache.Items) {
                    $h = [double]$sib.ActualHeight
                    if ($h -le 0) { $h = [double]$sib.DesiredSize.Height }
                    if ($sib -is [System.Windows.FrameworkElement]) {
                        $h += [double]$sib.Margin.Top + [double]$sib.Margin.Bottom
                    }
                    $siblingsBelow += $h
                }
            }

            $target = $vh - $offset - $siblingsBelow - 10
            if ($target -gt $maxL) { $target = $maxL }
            if ($target -lt $minL) { $target = $minL }

            $current = [double]$ctlLocal.ActualHeight
            Write-Debug "Set-UiFillParentHeight: vh=$vh offset=$offset siblings=$siblingsBelow max=$maxL min=$minL target=$target current=$current"

            # 5px deadband prevents bounce - setting Height fires LayoutUpdated which can reenter via SizeChanged. First compute always applies regardless of deadband. The shrink height seed isn't a real measurement, and long headers above the grid can leave target coincidentally near it.
            $shouldApply = $target -gt 0 -and (!$computeState.FirstDone -or [double]::IsNaN($current) -or [Math]::Abs($target - $current) -gt 5)
            if ($shouldApply) {
                try {
                    $ctlLocal.Height = $target
                    $computeState.FirstDone = $true
                }
                catch { Write-Debug "Set-UiFillParentHeight: Height assign failed: $($_.Exception.Message)" }
            }
        }.GetNewClosure()

        # Initial compute at Loaded priority. Checks after the current layout settles (~5ms), tight enough that the computed shrink doesn't give a jarring 'flash' kind of an effect
        [void]$ctlLocal.Dispatcher.BeginInvoke(  [System.Windows.Threading.DispatcherPriority]::Loaded, [Action]{ & $computeHeight }.GetNewClosure())

        # Crude but workable brute force retry timer for TabItem. Doesn't seem to always catch the right timing, so retry a few times until the first compute succeeds or 20 tries (~1 second) have elapsed, with 50ms inbetween. May need to be tweaked later on.
        $retryTimer = [System.Windows.Threading.DispatcherTimer]::new()
        $retryTimer.Interval = [TimeSpan]::FromMilliseconds(50)
        $retryState = @{ Count = 0; Timer = $retryTimer }
        $retryTimer.Add_Tick({
            $retryState.Count++
            & $computeHeight
            if ($computeState.FirstDone -or $retryState.Count -ge 20) {
                $retryState.Timer.Stop()
            }
        }.GetNewClosure())
        $retryTimer.Start()

        # SizeChanged handler, held as a variable so the Unloaded cleanup can detach
        $svHandlerSb = {
            param($sizeSender, $sizeArgs)
            Write-Debug "Set-UiFillParentHeight: SV.SizeChanged fired (new H=$($sizeSender.ActualHeight))"
            & $computeHeight
        }.GetNewClosure()
        $svLocal.add_SizeChanged($svHandlerSb)

        # Walk the visual tree to the window.
        $window = $null
        $walker = $ctlLocal
        while ($walker) {
            if ($walker -is [System.Windows.Window]) { $window = $walker; break }
            $walker = [System.Windows.Media.VisualTreeHelper]::GetParent($walker)
        }

        # Window.SizeChanged shrinks when content is taller than viewport, ScrollViewer.ActualHeight stalls and its own SizeChanged event never seems to fire
        $windowSizeHandlerSb = $null
        if ($window) {
            $windowSizeHandlerSb = {
                param($wSender, $wArgs)
                Write-Debug "Set-UiFillParentHeight: Window.SizeChanged fired (new $($wArgs.NewSize.Width)x$($wArgs.NewSize.Height))"
                $compute = $computeHeight
                if ($compute) { & $compute }
            }.GetNewClosure()
            $window.add_SizeChanged($windowSizeHandlerSb)
        }

        # Window.StateChanged for max/restore btns
        $stateHandlerSb = $null
        if ($window) {
            $stateHandlerSb = {
                param($wSender, $wArgs)
                $compute = $computeHeight
                if (!$compute) { return }
                [void]$wSender.Dispatcher.BeginInvoke(
                    [System.Windows.Threading.DispatcherPriority]::Loaded,
                    [Action]{ if ($compute) { & $compute } }.GetNewClosure())
            }.GetNewClosure()
            $window.add_StateChanged($stateHandlerSb)
        }

        # Unloaded cleanup
        $unloadedHolder = @{ Sb = $null }
        $unloadedHolder.Sb = {
            param($s, $a)
            try { $svLocal.remove_SizeChanged($svHandlerSb) } catch {}
            if ($retryState.Timer)                 { try { $retryState.Timer.Stop() } catch {} }
            if ($window -and $stateHandlerSb)      { try { $window.remove_StateChanged($stateHandlerSb) } catch {} }
            if ($window -and $windowSizeHandlerSb) { try { $window.remove_SizeChanged($windowSizeHandlerSb) } catch {} }
            if ($pnt -and $widthHandler)           { try { $pnt.remove_SizeChanged($widthHandler) } catch {} }
            try { $ctlLocal.remove_Unloaded($unloadedHolder.Sb) } catch {}
        }.GetNewClosure()
        $ctlLocal.add_Unloaded($unloadedHolder.Sb)
    }.GetNewClosure())
}