private/output/New-OutputWindow.ps1


function New-OutputWindow {
    <#
    .SYNOPSIS
        Creates a themed output window with custom chrome (no white flash on dark themes).
    #>

    [CmdletBinding()]
    param(
        [string]$Title = 'Output',
        [int]$Width = 900,
        [int]$Height = 600,
        [System.Windows.Window]$ParentWindow,
        [hashtable]$Colors,
        [string]$CustomLogo
    )

    # Shadow adds padding around the visible window
    $shadowPadding = 16
    
    # Create borderless transparent window for custom chrome
    $window = [System.Windows.Window]@{
        Title                 = $Title
        Width                 = $Width + ($shadowPadding * 2)
        Height                = $Height + ($shadowPadding * 2)
        MinWidth              = 850 + ($shadowPadding * 2)
        MinHeight             = 300 + ($shadowPadding * 2)
        WindowStartupLocation = 'CenterScreen'
        FontFamily            = [System.Windows.Media.FontFamily]::new('Segoe UI')
        Background            = [System.Windows.Media.Brushes]::Transparent
        Foreground            = ConvertTo-UiBrush $Colors.ControlFg
        ResizeMode            = 'CanResize'
        WindowStyle           = 'None'
        AllowsTransparency    = $true
        Opacity               = 0
    }

    if ($ParentWindow) {
        try {
            $window.Owner = $ParentWindow
        }
        catch {
            Write-Verbose "[New-OutputWindow] Could not set Owner: $_"
        }
        
        # Use manual centering - CenterOwner doesn't work reliably with borderless windows
        [PsUi.WindowManager]::CenterOnParent($window, $ParentWindow)
    }

    # Set unique AppUserModelID to separate from PowerShell in taskbar
    $appId = "PsUi.OutputWindow." + [Guid]::NewGuid().ToString("N").Substring(0, 8)
    [PsUi.WindowManager]::SetWindowAppId($window, $appId)
    
    # Hook WM_GETMINMAXINFO to enable proper maximize behavior (respects taskbar)
    [PsUi.WindowManager]::EnableBorderlessMaximize($window)

    # Attach WindowChrome for resize borders on borderless window
    $windowChrome = [System.Windows.Shell.WindowChrome]@{
        CaptionHeight         = 0
        ResizeBorderThickness = [System.Windows.Thickness]::new($shadowPadding + 4)
        GlassFrameThickness   = [System.Windows.Thickness]::new(0)
        CornerRadius          = [System.Windows.CornerRadius]::new(0)
    }
    [System.Windows.Shell.WindowChrome]::SetWindowChrome($window, $windowChrome)

    $shadowBorder = [System.Windows.Controls.Border]@{
        Margin          = [System.Windows.Thickness]::new($shadowPadding)
        Background      = ConvertTo-UiBrush $Colors.WindowBg
        BorderBrush     = ConvertTo-UiBrush $Colors.Border
        BorderThickness = [System.Windows.Thickness]::new(1)
    }

    $shadow = [System.Windows.Media.Effects.DropShadowEffect]@{
        BlurRadius  = 16
        ShadowDepth = 2
        Opacity     = 0.3
        Color       = [System.Windows.Media.Colors]::Black
        Direction   = 270
    }
    $shadowBorder.Effect = $shadow
    $window.Content = $shadowBorder

    $mainGrid = [System.Windows.Controls.Grid]::new()
    $mainGrid.RowDefinitions.Add([System.Windows.Controls.RowDefinition]@{ Height = 'Auto' })
    $mainGrid.RowDefinitions.Add([System.Windows.Controls.RowDefinition]@{ Height = '*' })
    $shadowBorder.Child = $mainGrid

    $titleBar = [System.Windows.Controls.Border]@{
        Height = 32
        Tag    = 'HeaderBorder'
    }
    # Use DynamicResource for background so it updates with theme
    $titleBar.SetResourceReference([System.Windows.Controls.Border]::BackgroundProperty, 'HeaderBackgroundBrush')
    [System.Windows.Controls.Grid]::SetRow($titleBar, 0)

    $titleBarGrid = [System.Windows.Controls.Grid]::new()
    $titleBarGrid.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]@{ Width = 'Auto' })
    $titleBarGrid.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]@{ Width = '*' })
    $titleBarGrid.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]@{ Width = 'Auto' })
    $titleBar.Child = $titleBarGrid

    # Titlebar icon image (set later once we have the icon)
    $titleBarIcon = [System.Windows.Controls.Image]@{
        Width             = 16
        Height            = 16
        Margin            = [System.Windows.Thickness]::new(10, 0, 0, 0)
        VerticalAlignment = 'Center'
    }
    [System.Windows.Media.RenderOptions]::SetBitmapScalingMode($titleBarIcon, 'HighQuality')
    [System.Windows.Controls.Grid]::SetColumn($titleBarIcon, 0)
    [void]$titleBarGrid.Children.Add($titleBarIcon)

    $titleText = [System.Windows.Controls.TextBlock]@{
        Text              = $Title
        FontSize          = 12
        VerticalAlignment = 'Center'
        Margin            = [System.Windows.Thickness]::new(8, 0, 0, 0)
        Tag               = 'HeaderText'
    }
    # Use DynamicResource for foreground so it updates with theme
    $titleText.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, 'HeaderForegroundBrush')
    [System.Windows.Controls.Grid]::SetColumn($titleText, 1)
    [void]$titleBarGrid.Children.Add($titleText)

    $buttonPanel = [System.Windows.Controls.StackPanel]@{ Orientation = 'Horizontal' }
    [System.Windows.Controls.Grid]::SetColumn($buttonPanel, 2)

    $createWindowBtn = {
        param([string]$Glyph, [scriptblock]$OnClick, [bool]$IsClose = $false)
        
        $btn = [System.Windows.Controls.Button]@{
            Content         = $Glyph
            FontFamily      = [System.Windows.Media.FontFamily]::new('Segoe MDL2 Assets')
            FontSize        = 10
            Width           = 46
            Height          = 32
            BorderThickness = [System.Windows.Thickness]::new(0)
            Cursor          = [System.Windows.Input.Cursors]::Arrow
            Padding         = [System.Windows.Thickness]::new(0)
            Tag             = 'WindowControlButton'
        }
        $btn.OverridesDefaultStyle = $true
        
        # Foreground is set inside the template via TextElement.Foreground on ContentPresenter.
        # Do NOT use SetResourceReference on the button - it creates a local value that
        # overrides template trigger setters after theme changes.
        
        if ($IsClose) {
            # Close button: red hover with white X, darker red pressed
            $templateXaml = @'
<ControlTemplate xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" TargetType="Button">
    <Border x:Name="border" Background="Transparent">
        <ContentPresenter x:Name="content" HorizontalAlignment="Center" VerticalAlignment="Center"
                          TextElement.Foreground="{DynamicResource HeaderForegroundBrush}"/>
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
            <Setter TargetName="border" Property="Background" Value="#E81123"/>
            <Setter TargetName="content" Property="TextElement.Foreground" Value="White"/>
        </Trigger>
        <Trigger Property="IsPressed" Value="True">
            <Setter TargetName="border" Property="Background" Value="#C50F1F"/>
            <Setter TargetName="content" Property="TextElement.Foreground" Value="White"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>
'@

        }
        else {
            # Min/Max buttons: use WindowControlHoverBrush, slightly darker on press
            $templateXaml = @'
<ControlTemplate xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" TargetType="Button">
    <Border x:Name="border" Background="Transparent">
        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"
                          TextElement.Foreground="{DynamicResource HeaderForegroundBrush}"/>
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
            <Setter TargetName="border" Property="Background" Value="{DynamicResource WindowControlHoverBrush}"/>
        </Trigger>
        <Trigger Property="IsPressed" Value="True">
            <Setter TargetName="border" Property="Background" Value="{DynamicResource WindowControlHoverBrush}"/>
            <Setter TargetName="border" Property="Opacity" Value="0.7"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>
'@

        }
        # Cache parsed templates per session — avoids re-parsing identical XAML for every window
        if ($IsClose) {
            if (!$script:_closeBtnTemplate) { $script:_closeBtnTemplate = [System.Windows.Markup.XamlReader]::Parse($templateXaml) }
            $btn.Template = $script:_closeBtnTemplate
        }
        else {
            if (!$script:_windowCtrlBtnTemplate) { $script:_windowCtrlBtnTemplate = [System.Windows.Markup.XamlReader]::Parse($templateXaml) }
            $btn.Template = $script:_windowCtrlBtnTemplate
        }
        $btn.Add_Click($OnClick)
        
        # Mark button as hit-testable within WindowChrome area (critical for maximized state)
        [System.Windows.Shell.WindowChrome]::SetIsHitTestVisibleInChrome($btn, $true)
        
        return $btn
    }

    $capturedWindow = $window
    $capturedShadow = $shadowBorder

    $minimizeBtn = & $createWindowBtn ([PsUi.ModuleContext]::GetIcon('ChromeMinimize')) { $capturedWindow.WindowState = 'Minimized' }.GetNewClosure() $false
    [void]$buttonPanel.Children.Add($minimizeBtn)

    $maximizeBtn = & $createWindowBtn ([PsUi.ModuleContext]::GetIcon('ChromeMaximize')) {
        if ($capturedWindow.WindowState -eq 'Maximized') {
            $capturedWindow.WindowState = 'Normal'
        }
        else {
            $capturedWindow.WindowState = 'Maximized'
        }
    }.GetNewClosure() $false
    [void]$buttonPanel.Children.Add($maximizeBtn)

    $closeBtn = & $createWindowBtn ([PsUi.ModuleContext]::GetIcon('ChromeClose')) { 
        # Just close the window - let the Closing event handler deal with confirmations
        # and closing the ReadKey dialog at the appropriate time
        $capturedWindow.Close() 
    }.GetNewClosure() $true
    [void]$buttonPanel.Children.Add($closeBtn)

    [void]$titleBarGrid.Children.Add($buttonPanel)
    [void]$mainGrid.Children.Add($titleBar)

    $capturedMaxBtn       = $maximizeBtn
    $capturedPadding      = $shadowPadding
    $capturedShadowEffect = $shadow
    $capturedChrome       = $windowChrome
    $window.Add_StateChanged({
        if ($capturedWindow.WindowState -eq 'Maximized') {
            $capturedMaxBtn.Content = [PsUi.ModuleContext]::GetIcon('ChromeRestore')
            $capturedShadow.Margin  = [System.Windows.Thickness]::new(0)
            $capturedShadow.Effect  = $null
            
            # Remove resize borders so titlebar and scrollbars remain clickable
            $capturedChrome.ResizeBorderThickness = [System.Windows.Thickness]::new(0)
        }
        else {
            $capturedMaxBtn.Content = [PsUi.ModuleContext]::GetIcon('ChromeMaximize')
            $capturedShadow.Margin  = [System.Windows.Thickness]::new($capturedPadding)
            $capturedShadow.Effect  = $capturedShadowEffect
            
            # Restore resize borders for normal window state
            $capturedChrome.ResizeBorderThickness = [System.Windows.Thickness]::new($capturedPadding + 4)
        }
    }.GetNewClosure())

    # Shared drag state for restore-on-drag
    $dragState = @{ StartPoint = $null }
    
    $titleBar.Add_MouseLeftButtonDown({
        param($sender, $eventArgs)
        if ($eventArgs.ClickCount -eq 2) {
            if ($capturedWindow.WindowState -eq 'Maximized') {
                $capturedWindow.WindowState = 'Normal'
            }
            else {
                $capturedWindow.WindowState = 'Maximized'
            }
        }
        elseif ($eventArgs.ClickCount -eq 1) {
            if ($capturedWindow.WindowState -eq 'Maximized') {
                # Capture start point for drag detection
                $dragState.StartPoint = $eventArgs.GetPosition($capturedWindow)
                $sender.CaptureMouse()
            }
            else {
                $capturedWindow.DragMove()
            }
        }
    }.GetNewClosure())
    
    $titleBar.Add_MouseMove({
        param($sender, $eventArgs)
        if ($dragState.StartPoint -eq $null) { return }
        if ($eventArgs.LeftButton -ne 'Pressed') { return }
        
        # Check if mouse moved enough to count as a drag (5px threshold)
        $currentPos = $eventArgs.GetPosition($capturedWindow)
        $deltaX = [Math]::Abs($currentPos.X - $dragState.StartPoint.X)
        $deltaY = [Math]::Abs($currentPos.Y - $dragState.StartPoint.Y)
        
        if ($deltaX -gt 5 -or $deltaY -gt 5) {
            $sender.ReleaseMouseCapture()
            
            # Capture screen position and relative X BEFORE restoring
            $screenPos = $capturedWindow.PointToScreen($dragState.StartPoint)
            $relativeX = $dragState.StartPoint.X / $capturedWindow.ActualWidth
            
            $capturedWindow.WindowState = 'Normal'
            
            # Position window so mouse stays on titlebar at same relative X
            $capturedWindow.Left = $screenPos.X - ($capturedWindow.ActualWidth * $relativeX)
            $capturedWindow.Top  = $screenPos.Y - ($capturedPadding + 16)
            
            $dragState.StartPoint = $null
            $capturedWindow.DragMove()
        }
    }.GetNewClosure())
    
    $titleBar.Add_MouseLeftButtonUp({
        param($sender, $eventArgs)
        $dragState.StartPoint = $null
        $sender.ReleaseMouseCapture()
    }.GetNewClosure())

    $contentArea = [System.Windows.Controls.Border]@{ Name = 'ContentArea' }
    [System.Windows.Controls.Grid]::SetRow($contentArea, 1)
    [void]$mainGrid.Children.Add($contentArea)

    # Create window icon (use custom logo if provided)
    $iconForLoaded = $null
    try {
        if ($CustomLogo -and (Test-Path $CustomLogo)) {
            $iconForLoaded = Get-CustomLogoIcon -Path $CustomLogo
        }
        else {
            $iconForLoaded = New-WindowIcon -Colors $Colors
        }
        if ($iconForLoaded) {
            $window.Icon         = $iconForLoaded
            $titleBarIcon.Source = $iconForLoaded
        }
    }
    catch { Write-Verbose "Failed to create window icon: $_" }

    $capturedIcon   = $iconForLoaded
    $capturedColors = $Colors

    $window.Add_Loaded({
        $anim = [System.Windows.Media.Animation.DoubleAnimation]@{
            From     = 0
            To       = 1
            Duration = [System.Windows.Duration]::new([TimeSpan]::FromMilliseconds(150))
        }
        $capturedWindow.BeginAnimation([System.Windows.Window]::OpacityProperty, $anim)

        if ($capturedIcon) {
            [PsUi.WindowManager]::SetTaskbarIcon($capturedWindow, $capturedIcon)
        }

        try {
            $runningOverlay = New-TaskbarOverlayIcon -GlyphChar ([PsUi.ModuleContext]::GetIcon('Sync')) -Color $capturedColors.Accent -BackgroundColor '#FFFFFF'
            if ($runningOverlay) {
                [PsUi.WindowManager]::SetTaskbarOverlay($capturedWindow, $runningOverlay, 'Running...')
            }
        }
        catch { Write-Debug "Suppressed taskbar running overlay error: $_" }
    }.GetNewClosure())

    # Store references for caller
    $window.Tag = @{
        ShadowBorder = $shadowBorder
        MainGrid     = $mainGrid
        ContentArea  = $contentArea
        TitleText    = $titleText
        TitleBarIcon = $titleBarIcon
    }

    return $window
}