public/output/Out-TextEditor.ps1

function Out-TextEditor {
    <#
    .SYNOPSIS
        Opens a themed text editor window.
    .DESCRIPTION
        Displays text in a themed editor window with find, copy, and optional save.
        Accepts input via parameter or pipeline.
    .PARAMETER InputObject
        Text to display. Accepts string array from pipeline.
    .PARAMETER InitialText
        Initial text content (alias for backward compatibility).
    .PARAMETER TitleText
        Window title.
    .PARAMETER Theme
        Color theme to use. Defaults to Light.
    .PARAMETER ReadOnly
        When specified, the text editor opens in read-only mode. The Save button is hidden
        and the Cancel button becomes "Close" with accent styling.
    .PARAMETER NoWordWrap
        When specified, disables word wrap (shows horizontal scrollbar for long lines).
        Word wrap is enabled by default.
    .PARAMETER SpellCheck
        When specified, enables spell checking with red underlines for misspelled words.
        Spell check is disabled by default.
    .PARAMETER Width
        Window width in pixels.
    .PARAMETER Height
        Window height in pixels.
    .PARAMETER FontFamily
        Font face for the editor. Defaults to Consolas.
    .PARAMETER FontSize
        Font size in points.
    .EXAMPLE
        Out-TextEditor -InitialText "Hello World"
    .EXAMPLE
        Get-Content C:\file.txt | Out-TextEditor -Theme Dark
    .EXAMPLE
        "Line 1", "Line 2", "Line 3" | Out-TextEditor
    #>

    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true)]
        [string[]]$InputObject,

        [string]$InitialText = '',
        [string]$TitleText = 'Text Editor',
        [ArgumentCompleter({ [PsUi.ThemeEngine]::GetAvailableThemes() })]
        [string]$Theme = 'Light',
        [ValidateRange(300, 2000)]
        [int]$Width = 800,
        [ValidateRange(200, 1500)]
        [int]$Height = 600,
        [string]$FontFamily = 'Consolas',
        [ValidateRange(8, 24)]
        [int]$FontSize = 12,
        [switch]$ReadOnly,
        [switch]$NoWordWrap,
        [switch]$SpellCheck
    )

    begin {
        # ShowDialog requires the UI thread - block async button actions from calling this
        if ([PsUi.AsyncExecutor]::CurrentExecutor) {
            Write-Error 'Out-TextEditor cannot be called from an async button action (ShowDialog requires the UI thread). Use -NoAsync on your button, or call this function outside the DSL.'
            return
        }

        Write-Debug "Starting with Title='$TitleText', Theme='$Theme', Font='$FontFamily' $FontSize pt"
        $collectedLines = [System.Collections.Generic.List[string]]::new()
    }

    process {
        if ($InputObject) {
            foreach ($line in $InputObject) {
                [void]$collectedLines.Add($line)
            }
        }
    }

    end {
    # Combine pipeline input with InitialText parameter
    $finalText = if ($collectedLines.Count -gt 0) {
        Write-Debug "Collected $($collectedLines.Count) lines from pipeline"
        $collectedLines -join "`n"
    }
    elseif ($InitialText) {
        Write-Debug "Using InitialText parameter ($($InitialText.Length) chars)"
        $InitialText
    }
    else {
        Write-Debug "No initial content"
        ''
    }

    $isStandalone = !(Test-Path variable:__WPFThemeColors)
    Write-Debug "Context: isStandalone=$isStandalone, textLength=$($finalText.Length)"

    if ($isStandalone) {
        # Standalone: use -Theme parameter and initialize full theme resources
        $colors = Initialize-UITheme -Theme $Theme
    }
    else {
        # Child window: use injected theme colors from parent context
        $colors = Get-Variable -Name __WPFThemeColors -ValueOnly -ErrorAction SilentlyContinue
    }

    # Ultimate fallback theme colors
    if (!$colors) {
        $colors = @{
            WindowBg         = '#FFFFFF'
            WindowFg         = '#202020'
            ControlBg        = '#F3F3F3'
            ControlFg        = '#202020'
            HeaderBackground = '#F0F0F0'
            HeaderForeground = '#202020'
            Border           = '#D1D1D1'
            Accent           = '#0078D4'
            SecondaryText    = '#666666'
            ButtonBg         = '#FFFFFF'
            ButtonFg         = '#202020'
            ButtonHover      = '#EFEFEF'
        }
    }

    $window = [System.Windows.Window]@{
        Title                 = $TitleText
        Width                 = $Width
        Height                = $Height
        MinWidth              = 300
        MinHeight             = 200
        WindowStartupLocation = 'CenterScreen'
        FontFamily            = [System.Windows.Media.FontFamily]::new('Segoe UI')
        ResizeMode            = 'CanResizeWithGrip'
    }

    # Bind to parent for consistent window management
    $null = Set-WindowOwner -Window $window
    $window.SetResourceReference([System.Windows.Window]::BackgroundProperty, 'WindowBackgroundBrush')
    $window.SetResourceReference([System.Windows.Window]::ForegroundProperty, 'ControlForegroundBrush')

    Set-UIResources -Window $window -Colors $colors

    $appId = "PsUi.TextEditor." + [Guid]::NewGuid().ToString("N").Substring(0, 8)
    [PsUi.WindowManager]::SetWindowAppId($window, $appId)

    $editorWindowIcon = $null
    try {
        $editorWindowIcon = New-WindowIcon -Colors $colors
        if ($editorWindowIcon) {
            $window.Icon = $editorWindowIcon
        }
    }
    catch {
        Write-Verbose "Failed to create window icon: $_"
    }

    $overlayIcon = $null
    try {
        $docGlyph = [PsUi.ModuleContext]::GetIcon('Document')
        $overlayIcon = New-TaskbarOverlayIcon -GlyphChar $docGlyph -Color $colors.Accent
        # Store glyph in resources for theme updates
        $window.Resources['OverlayGlyph'] = $docGlyph
    }
    catch { Write-Debug "Taskbar overlay failed: $_" }

    $capturedEditorWindow = $window
    $capturedEditorIcon   = $editorWindowIcon
    $capturedOverlay      = $overlayIcon

    $window.Add_Loaded({
        if ($capturedEditorIcon) {
            [PsUi.WindowManager]::SetTaskbarIcon($capturedEditorWindow, $capturedEditorIcon)
        }
        if ($capturedOverlay) {
            [PsUi.WindowManager]::SetTaskbarOverlay($capturedEditorWindow, $capturedOverlay, 'Document')
        }
    }.GetNewClosure())

    $mainPanel = [System.Windows.Controls.DockPanel]@{
        LastChildFill = $true
    }
    $window.Content = $mainPanel

    # Header bar
    $headerBorder = [System.Windows.Controls.Border]@{
        Padding = [System.Windows.Thickness]::new(16, 12, 16, 12)
        Tag = 'HeaderBorder'
    }
    $headerBorder.SetResourceReference([System.Windows.Controls.Border]::BackgroundProperty, 'HeaderBackgroundBrush')
    [System.Windows.Controls.DockPanel]::SetDock($headerBorder, 'Top')

    $headerGrid = [System.Windows.Controls.Grid]::new()
    $col1 = [System.Windows.Controls.ColumnDefinition]@{
        Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star)
    }
    $col2 = [System.Windows.Controls.ColumnDefinition]@{
        Width = [System.Windows.GridLength]::Auto
    }
    [void]$headerGrid.ColumnDefinitions.Add($col1)
    [void]$headerGrid.ColumnDefinitions.Add($col2)

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

    $headerIcon = [System.Windows.Controls.TextBlock]@{
        Text              = [PsUi.ModuleContext]::GetIcon('Edit')
        FontFamily        = [System.Windows.Media.FontFamily]::new('Segoe MDL2 Assets')
        FontSize          = 24
        VerticalAlignment = 'Center'
        Width             = 32
        TextAlignment     = 'Center'
        Margin            = [System.Windows.Thickness]::new(0, 0, 12, 0)
        Tag               = 'AccentIcon'
    }
    # SetResourceReference is a method, not a property, so it must be called after object creation
    $headerIcon.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, 'AccentBrush')
    [void]$headerStack.Children.Add($headerIcon)

    $headerTitle = [System.Windows.Controls.TextBlock]@{
        Text              = $TitleText
        FontSize          = 18
        FontWeight        = [System.Windows.FontWeights]::SemiBold
        VerticalAlignment = 'Center'
        Tag               = 'HeaderText'
    }
    $headerTitle.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, 'HeaderForegroundBrush')
    [void]$headerStack.Children.Add($headerTitle)

    [void]$headerGrid.Children.Add($headerStack)

        # Add theme button to header - only when running standalone
        if ($isStandalone) {
            $themeButtonData = New-ThemePopupButton -Container $window -CurrentTheme $Theme
            [System.Windows.Controls.Grid]::SetColumn($themeButtonData.Button, 1)
            [void]$headerGrid.Children.Add($themeButtonData.Button)
        }

    $headerBorder.Child = $headerGrid
    [void]$mainPanel.Children.Add($headerBorder)

    # Content area
    $contentPanel = [System.Windows.Controls.DockPanel]@{
        Margin        = [System.Windows.Thickness]::new(12)
        LastChildFill = $true
    }
    [void]$mainPanel.Children.Add($contentPanel)

    # Toolbar at top
    $toolbar = [System.Windows.Controls.StackPanel]@{
        Orientation = 'Horizontal'
        Margin      = [System.Windows.Thickness]::new(0, 0, 0, 8)
        Height      = 32
    }
    [System.Windows.Controls.DockPanel]::SetDock($toolbar, 'Top')
    [void]$contentPanel.Children.Add($toolbar)

    # Copy All button with icon
    $copyAllBtn = [System.Windows.Controls.Button]::new()
    $copyAllPanel = [System.Windows.Controls.StackPanel]::new()
    $copyAllPanel.Orientation = 'Horizontal'
    $copyAllIcon = [System.Windows.Controls.TextBlock]::new()
    $copyAllIcon.Text = [PsUi.ModuleContext]::GetIcon('Copy')
    $copyAllIcon.FontFamily = [System.Windows.Media.FontFamily]::new('Segoe MDL2 Assets')
    $copyAllIcon.FontSize = 12
    $copyAllIcon.Margin = [System.Windows.Thickness]::new(0, 0, 4, 0)
    $copyAllIcon.VerticalAlignment = 'Center'
    [void]$copyAllPanel.Children.Add($copyAllIcon)
    $copyAllText = [System.Windows.Controls.TextBlock]::new()
    $copyAllText.Text = 'Copy All'
    $copyAllText.VerticalAlignment = 'Center'
    [void]$copyAllPanel.Children.Add($copyAllText)
    $copyAllBtn.Content = $copyAllPanel
    $copyAllBtn.Height = 28
    $copyAllBtn.Padding = [System.Windows.Thickness]::new(8, 4, 8, 4)
    $copyAllBtn.Margin = [System.Windows.Thickness]::new(0, 0, 4, 0)
    $copyAllBtn.VerticalAlignment = 'Center'
    $copyAllBtn.ToolTip = 'Copy all text to clipboard (Ctrl+Shift+C)'
    Set-ButtonStyle -Button $copyAllBtn
    [void]$toolbar.Children.Add($copyAllBtn)

    # Clear button with icon
    $clearBtn = [System.Windows.Controls.Button]::new()
    $clearPanel = [System.Windows.Controls.StackPanel]::new()
    $clearPanel.Orientation = 'Horizontal'
    $clearIcon = [System.Windows.Controls.TextBlock]::new()
    $clearIcon.Text = [PsUi.ModuleContext]::GetIcon('Delete')
    $clearIcon.FontFamily = [System.Windows.Media.FontFamily]::new('Segoe MDL2 Assets')
    $clearIcon.FontSize = 12
    $clearIcon.Margin = [System.Windows.Thickness]::new(0, 0, 4, 0)
    $clearIcon.VerticalAlignment = 'Center'
    [void]$clearPanel.Children.Add($clearIcon)
    $clearText = [System.Windows.Controls.TextBlock]::new()
    $clearText.Text = 'Clear'
    $clearText.VerticalAlignment = 'Center'
    [void]$clearPanel.Children.Add($clearText)
    $clearBtn.Content = $clearPanel
    $clearBtn.Height = 28
    $clearBtn.Padding = [System.Windows.Thickness]::new(8, 4, 8, 4)
    $clearBtn.Margin = [System.Windows.Thickness]::new(0, 0, 8, 0)
    $clearBtn.VerticalAlignment = 'Center'
    $clearBtn.ToolTip = 'Clear all text'
    Set-ButtonStyle -Button $clearBtn
    [void]$toolbar.Children.Add($clearBtn)

    $wrapCheck = [System.Windows.Controls.CheckBox]::new()
    $wrapCheck.Content = 'Word Wrap'
    $wrapCheck.IsChecked = !$NoWordWrap
    $wrapCheck.VerticalAlignment = 'Center'
    $wrapCheck.Margin = [System.Windows.Thickness]::new(0, 0, 8, 0)
    $wrapCheck.ToolTip = 'Toggle word wrapping'
    Set-CheckBoxStyle -CheckBox $wrapCheck
    $wrapCheck.SetResourceReference([System.Windows.Controls.CheckBox]::ForegroundProperty, 'ControlForegroundBrush')
    [void]$toolbar.Children.Add($wrapCheck)

    $spellCheckBox = [System.Windows.Controls.CheckBox]::new()
    $spellCheckBox.Content = 'Spell Check'
    $spellCheckBox.IsChecked = $SpellCheck
    $spellCheckBox.VerticalAlignment = 'Center'
    $spellCheckBox.Margin = [System.Windows.Thickness]::new(0, 0, 8, 0)
    $spellCheckBox.ToolTip = 'Toggle spell checking'
    Set-CheckBoxStyle -CheckBox $spellCheckBox
    $spellCheckBox.SetResourceReference([System.Windows.Controls.CheckBox]::ForegroundProperty, 'ControlForegroundBrush')
    [void]$toolbar.Children.Add($spellCheckBox)

    # Font size control with label beneath slider
    $fontSizePanel = [System.Windows.Controls.StackPanel]::new()
    $fontSizePanel.Orientation = 'Vertical'
    $fontSizePanel.VerticalAlignment = 'Center'
    $fontSizePanel.Margin = [System.Windows.Thickness]::new(8, 0, 0, 0)

    $fontSizeSlider = [System.Windows.Controls.Slider]::new()
    $fontSizeSlider.Minimum = 8
    $fontSizeSlider.Maximum = 32
    $fontSizeSlider.Value = $FontSize
    $fontSizeSlider.Width = 100
    $fontSizeSlider.TickFrequency = 1
    $fontSizeSlider.IsSnapToTickEnabled = $true
    $fontSizeSlider.ToolTip = 'Adjust font size (8-32pt). Ctrl+Scroll to change. Double-click to reset.'
    $fontSizeSlider.Tag = $FontSize
    Set-SliderStyle -Slider $fontSizeSlider
    [void]$fontSizePanel.Children.Add($fontSizeSlider)

    $fontLabel = [System.Windows.Controls.TextBlock]::new()
    $fontLabel.Text = 'Font Size'
    $fontLabel.HorizontalAlignment = 'Center'
    $fontLabel.FontSize = 10
    $fontLabel.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, 'ControlForegroundBrush')
    [void]$fontSizePanel.Children.Add($fontLabel)

    [void]$toolbar.Children.Add($fontSizePanel)

    # Status and search bar at bottom
    $statusBar = [System.Windows.Controls.Border]::new()
    $statusBar.SetResourceReference([System.Windows.Controls.Border]::BackgroundProperty, 'HeaderBackgroundBrush')
    $statusBar.SetResourceReference([System.Windows.Controls.Border]::BorderBrushProperty, 'BorderBrush')
    $statusBar.BorderThickness = [System.Windows.Thickness]::new(0, 1, 0, 0)
    $statusBar.Padding = [System.Windows.Thickness]::new(8, 4, 8, 4)
    $statusBar.Height = 32
    $statusBar.Tag = 'HeaderBorder'
    [System.Windows.Controls.DockPanel]::SetDock($statusBar, 'Bottom')

    $statusGrid = [System.Windows.Controls.Grid]::new()
    $statusCol1 = [System.Windows.Controls.ColumnDefinition]::new()
    $statusCol1.Width = [System.Windows.GridLength]::Auto
    $statusCol2 = [System.Windows.Controls.ColumnDefinition]::new()
    $statusCol2.Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star)
    $statusCol3 = [System.Windows.Controls.ColumnDefinition]::new()
    $statusCol3.Width = [System.Windows.GridLength]::Auto
    [void]$statusGrid.ColumnDefinitions.Add($statusCol1)
    [void]$statusGrid.ColumnDefinitions.Add($statusCol2)
    [void]$statusGrid.ColumnDefinitions.Add($statusCol3)

    $statusText = [System.Windows.Controls.TextBlock]::new()
    $statusText.Text = 'Line: 1 Col: 1'
    $statusText.FontSize = 11
    $statusText.VerticalAlignment = 'Center'
    $statusText.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, 'HeaderForegroundBrush')
    $statusText.Tag = 'HeaderText'
    [System.Windows.Controls.Grid]::SetColumn($statusText, 0)
    [void]$statusGrid.Children.Add($statusText)

    $findPanel = [System.Windows.Controls.StackPanel]::new()
    $findPanel.Orientation = 'Horizontal'
    $findPanel.HorizontalAlignment = 'Right'
    $findPanel.VerticalAlignment = 'Center'
    [System.Windows.Controls.Grid]::SetColumn($findPanel, 2)

    $findLabel = [System.Windows.Controls.TextBlock]::new()
    $findLabel.Text = 'Find:'
    $findLabel.FontSize = 11
    $findLabel.VerticalAlignment = 'Center'
    $findLabel.Margin = [System.Windows.Thickness]::new(0, 0, 4, 0)
    $findLabel.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, 'HeaderForegroundBrush')
    $findLabel.Tag = 'HeaderText'
    [void]$findPanel.Children.Add($findLabel)

    # Wrap findBox in Grid to overlay clear button
    $findBoxContainer = [System.Windows.Controls.Grid]::new()
    $findBoxContainer.Width = 200
    $findBoxContainer.Height = 20
    $findBoxContainer.VerticalAlignment = 'Center'

    $findBox = [System.Windows.Controls.TextBox]::new()
    $findBox.Height = 20
    $findBox.FontSize = 11
    $findBox.VerticalAlignment = 'Center'
    $findBox.Padding = [System.Windows.Thickness]::new(2, 0, 18, 0)
    $findBox.ToolTip = 'Search for text (case-insensitive by default)'
    Set-TextBoxStyle -TextBox $findBox
    $findBox.BorderThickness = [System.Windows.Thickness]::new(1)
    [void]$findBoxContainer.Children.Add($findBox)

    $findClearBtn = [System.Windows.Controls.Button]::new()
    $findClearBtn.Content = [PsUi.ModuleContext]::GetIcon('Cancel')
    $findClearBtn.FontFamily = [System.Windows.Media.FontFamily]::new('Segoe MDL2 Assets')
    $findClearBtn.FontSize = 9
    $findClearBtn.Width = 14
    $findClearBtn.Height = 14
    $findClearBtn.Padding = [System.Windows.Thickness]::new(0)
    $findClearBtn.Margin = [System.Windows.Thickness]::new(0, 0, 3, 0)
    $findClearBtn.HorizontalAlignment = 'Right'
    $findClearBtn.VerticalAlignment = 'Center'
    $findClearBtn.Background = [System.Windows.Media.Brushes]::Transparent
    $findClearBtn.BorderThickness = [System.Windows.Thickness]::new(0)
    $findClearBtn.SetResourceReference([System.Windows.Controls.Button]::ForegroundProperty, 'SecondaryTextBrush')
    $findClearBtn.Cursor = [System.Windows.Input.Cursors]::Hand
    $findClearBtn.Visibility = 'Collapsed'
    $findClearBtn.ToolTip = 'Clear'
    $findClearBtn.Tag = $findBox
    $findClearBtn.Add_Click({ $this.Tag.Text = ''; $this.Tag.Focus() }.GetNewClosure())
    [void]$findBoxContainer.Children.Add($findClearBtn)

    $findBox.Tag = @{ ClearButton = $findClearBtn }
    [void]$findPanel.Children.Add($findBoxContainer)

    $findPrevBtn = [System.Windows.Controls.Button]::new()
    $findPrevBtn.Width = 24
    $findPrevBtn.Height = 20
    $findPrevBtn.Padding = [System.Windows.Thickness]::new(0)
    $findPrevBtn.Margin = [System.Windows.Thickness]::new(2, 0, 0, 0)
    $findPrevBtn.VerticalAlignment = 'Center'
    $findPrevBtn.ToolTip = 'Find previous occurrence (Shift+F3)'
    $findPrevIcon = [System.Windows.Controls.TextBlock]::new()
    $findPrevIcon.Text = [PsUi.ModuleContext]::GetIcon('ArrowLeft')
    $findPrevIcon.FontFamily = [System.Windows.Media.FontFamily]::new('Segoe MDL2 Assets')
    $findPrevIcon.FontSize = 12
    $findPrevIcon.HorizontalAlignment = 'Center'
    $findPrevIcon.VerticalAlignment = 'Center'
    $findPrevBtn.Content = $findPrevIcon
    Set-ButtonStyle -Button $findPrevBtn
    [void]$findPanel.Children.Add($findPrevBtn)

    $findNextBtn = [System.Windows.Controls.Button]::new()
    $findNextBtn.Width = 24
    $findNextBtn.Height = 20
    $findNextBtn.Padding = [System.Windows.Thickness]::new(0)
    $findNextBtn.Margin = [System.Windows.Thickness]::new(2, 0, 0, 0)
    $findNextBtn.VerticalAlignment = 'Center'
    $findNextBtn.ToolTip = 'Find next occurrence (F3)'
    $findNextIcon = [System.Windows.Controls.TextBlock]::new()
    $findNextIcon.Text = [PsUi.ModuleContext]::GetIcon('ArrowRight')
    $findNextIcon.FontFamily = [System.Windows.Media.FontFamily]::new('Segoe MDL2 Assets')
    $findNextIcon.FontSize = 12
    $findNextIcon.HorizontalAlignment = 'Center'
    $findNextIcon.VerticalAlignment = 'Center'
    $findNextBtn.Content = $findNextIcon
    Set-ButtonStyle -Button $findNextBtn
    [void]$findPanel.Children.Add($findNextBtn)

    $matchCaseCheck = [System.Windows.Controls.CheckBox]::new()
    $matchCaseCheck.Content = 'Aa'
    $matchCaseCheck.ToolTip = 'Enable case-sensitive search'
    $matchCaseCheck.FontSize = 10
    $matchCaseCheck.VerticalAlignment = 'Center'
    $matchCaseCheck.VerticalContentAlignment = 'Center'
    $matchCaseCheck.Margin = [System.Windows.Thickness]::new(6, 0, 0, 0)
    Set-CheckBoxStyle -CheckBox $matchCaseCheck
    $matchCaseCheck.SetResourceReference([System.Windows.Controls.CheckBox]::ForegroundProperty, 'HeaderForegroundBrush')
    [void]$findPanel.Children.Add($matchCaseCheck)

    # Find counter label
    # Width sized for "99 found" or "9 of 99" - prevents layout shifts when text changes
    $FIND_COUNTER_WIDTH = 60
    $findCountLabel = [System.Windows.Controls.TextBlock]::new()
    $findCountLabel.Text = ''
    $findCountLabel.FontSize = 10
    $findCountLabel.VerticalAlignment = 'Center'
    $findCountLabel.Margin = [System.Windows.Thickness]::new(8, 1, 0, 0)
    $findCountLabel.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, 'HeaderForegroundBrush')
    $findCountLabel.Tag = 'HeaderText'
    $findCountLabel.ToolTip = 'Shows current match / total matches'
    $findCountLabel.Width = $FIND_COUNTER_WIDTH
    $findCountLabel.TextAlignment = 'Left'
    # Start hidden to reserve space
    $findCountLabel.Visibility = [System.Windows.Visibility]::Hidden
    [void]$findPanel.Children.Add($findCountLabel)

    [void]$statusGrid.Children.Add($findPanel)
    $statusBar.Child = $statusGrid
    [void]$contentPanel.Children.Add($statusBar)

    # Button panel
    $buttonPanel = [System.Windows.Controls.StackPanel]::new()
    $buttonPanel.Orientation = 'Horizontal'
    $buttonPanel.HorizontalAlignment = 'Right'
    $buttonPanel.Margin = [System.Windows.Thickness]::new(0, 8, 0, 0)
    [System.Windows.Controls.DockPanel]::SetDock($buttonPanel, 'Bottom')
    [void]$contentPanel.Children.Add($buttonPanel)

    $saveBtn = [System.Windows.Controls.Button]::new()
    $saveBtn.Content = 'Save'
    $saveBtn.Width = 90
    $saveBtn.Height = 32
    $saveBtn.Margin = [System.Windows.Thickness]::new(0, 0, 8, 8)
    Set-ButtonStyle -Button $saveBtn -Accent
    if ($ReadOnly) { $saveBtn.Visibility = 'Collapsed' }
    [void]$buttonPanel.Children.Add($saveBtn)

    $cancelBtn = [System.Windows.Controls.Button]::new()
    $cancelBtn.Content = if ($ReadOnly) { 'Close' } else { 'Cancel' }
    $cancelBtn.Width = 90
    $cancelBtn.Height = 32
    $cancelBtn.Margin = [System.Windows.Thickness]::new(0, 0, 0, 8)
    Set-ButtonStyle -Button $cancelBtn
    if ($ReadOnly) { Set-ButtonStyle -Button $cancelBtn -Accent }
    [void]$buttonPanel.Children.Add($cancelBtn)

    $scrollViewer = [System.Windows.Controls.ScrollViewer]::new()
    $scrollViewer.VerticalScrollBarVisibility = 'Auto'
    
    # Default to wrap on unless NoWordWrap specified
    $scrollViewer.HorizontalScrollBarVisibility = if ($NoWordWrap) { 'Auto' } else { 'Disabled' }
    [void]$contentPanel.Children.Add($scrollViewer)

    # Multi-line editor textbox - don't use Set-TextBoxStyle since it applies a single-line template
    # Instead, manually apply theme colors for proper multi-line behavior
    $textBox = [System.Windows.Controls.TextBox]::new()
    $textBox.Text = $finalText
    $textBox.AcceptsReturn = $true
    $textBox.AcceptsTab = $true
    $textBox.TextWrapping = if ($NoWordWrap) { 'NoWrap' } else { 'Wrap' }
    $textBox.VerticalScrollBarVisibility = 'Hidden'
    $textBox.HorizontalScrollBarVisibility = 'Hidden'
    $textBox.IsInactiveSelectionHighlightEnabled = $true
    $textBox.VerticalContentAlignment = 'Top'
    $textBox.BorderThickness = [System.Windows.Thickness]::new(0.5)
    $textBox.FontFamily = [System.Windows.Media.FontFamily]::new($FontFamily)
    $textBox.FontSize = $FontSize
    $textBox.Padding = [System.Windows.Thickness]::new(8)
    $textBox.IsReadOnly = $ReadOnly
    
    # Enable spell check only if explicitly requested
    [System.Windows.Controls.SpellCheck]::SetIsEnabled($textBox, $SpellCheck)
    
    # Create base context menu (we'll add spell suggestions dynamically)
    $baseContextMenu = New-TextBoxContextMenu -ReadOnly:$ReadOnly
    $textBox.ContextMenu = $baseContextMenu
    
    # Add spell check suggestions when context menu opens
    $textBox.Add_ContextMenuOpening({
        param($sender, $eventArgs)
        
        # Get the textbox and its context menu
        $tb = $sender
        $menu = $tb.ContextMenu
        
        # Remove any previous spell check items (they have Tag = 'SpellCheck')
        $itemsToRemove = @($menu.Items | Where-Object { $_.Tag -eq 'SpellCheck' })
        foreach ($item in $itemsToRemove) {
            [void]$menu.Items.Remove($item)
        }
        
        # Spell check suggestions go at the top of the context menu
        if ([System.Windows.Controls.SpellCheck]::GetIsEnabled($tb)) {
            # Get the character index at the mouse position (not caret position)
            $mousePos = [System.Windows.Input.Mouse]::GetPosition($tb)
            $charIndex = $tb.GetCharacterIndexFromPoint($mousePos, $true)
            
            if ($charIndex -ge 0) {
                $spellingError = $tb.GetSpellingError($charIndex)
                
                if ($spellingError) {
                    $suggestions = $spellingError.Suggestions
                    $insertIndex = 0
                    
                    # Add spelling suggestions
                    if ($suggestions) {
                        foreach ($suggestion in $suggestions) {
                            $suggestionItem = [System.Windows.Controls.MenuItem]::new()
                            $suggestionItem.Header = $suggestion
                            $suggestionItem.FontWeight = [System.Windows.FontWeights]::SemiBold
                            $suggestionItem.Tag = 'SpellCheck'
                            
                            # Capture values for the click handler
                            $capturedSuggestion = $suggestion
                            $capturedError = $spellingError
                            $capturedTextBox = $tb
                            $suggestionItem.Add_Click({
                                $capturedTextBox.BeginChange()
                                $capturedError.Correct($capturedSuggestion)
                                $capturedTextBox.EndChange()
                            }.GetNewClosure())
                            
                            [void]$menu.Items.Insert($insertIndex, $suggestionItem)
                            $insertIndex++
                        }
                        
                        # Add separator after spell check suggestions
                        $spellSeparator = [System.Windows.Controls.Separator]::new()
                        $spellSeparator.Tag = 'SpellCheck'
                        [void]$menu.Items.Insert($insertIndex, $spellSeparator)
                    }
                }
            }
        }
    }.GetNewClosure())

    # Apply theme colors via resource references
    $textBox.SetResourceReference([System.Windows.Controls.TextBox]::BackgroundProperty, 'ControlBackgroundBrush')
    $textBox.SetResourceReference([System.Windows.Controls.TextBox]::ForegroundProperty, 'ControlForegroundBrush')
    $textBox.SetResourceReference([System.Windows.Controls.TextBox]::BorderBrushProperty, 'BorderBrush')
    $textBox.SetResourceReference([System.Windows.Controls.TextBox]::CaretBrushProperty, 'ControlForegroundBrush')
    $textBox.SetResourceReference([System.Windows.Controls.Primitives.TextBoxBase]::SelectionBrushProperty, 'AccentBrush')
    $textBox.SelectionOpacity = 0.4

    $scrollViewer.Content = $textBox

    # Autofocus textbox when not in readonly mode
    if (!$ReadOnly) {
        $window.Add_ContentRendered({ $textBox.Focus() }.GetNewClosure())
    }

    $textBox.Add_SelectionChanged({
        try {
            $text = $textBox.Text
            $caretIndex = $textBox.CaretIndex
            $lineNumber = 1
            $colNumber = 1
            if ($caretIndex -gt 0 -and $text.Length -gt 0) {
                $beforeCaret = $text.Substring(0, [Math]::Min($caretIndex, $text.Length))
                $lineNumber = ($beforeCaret.ToCharArray() | Where-Object { $_ -eq "`n" }).Count + 1
                $lastNewLine = $beforeCaret.LastIndexOf("`n")
                $colNumber = if ($lastNewLine -ge 0) { $caretIndex - $lastNewLine } else { $caretIndex + 1 }
            }
            $statusText.Text = "Line: $lineNumber Col: $colNumber Length: $($text.Length)"
        }
        catch {
            Write-Verbose "Failed to update status text: $_"
        }
    }.GetNewClosure())

    $wrapCheck.Add_Checked({
        $textBox.TextWrapping = 'Wrap'
        $scrollViewer.HorizontalScrollBarVisibility = 'Disabled'
    }.GetNewClosure())

    $wrapCheck.Add_Unchecked({
        $textBox.TextWrapping = 'NoWrap'
        $scrollViewer.HorizontalScrollBarVisibility = 'Auto'
    }.GetNewClosure())

    $spellCheckBox.Add_Checked({
        [System.Windows.Controls.SpellCheck]::SetIsEnabled($textBox, $true)
    }.GetNewClosure())

    $spellCheckBox.Add_Unchecked({
        [System.Windows.Controls.SpellCheck]::SetIsEnabled($textBox, $false)
    }.GetNewClosure())

    $fontSizeSlider.Add_ValueChanged({
        $textBox.FontSize = $fontSizeSlider.Value
    }.GetNewClosure())

    # Double-click slider to reset to default (use Preview to catch before thumb handles it)
    $fontSizeSlider.Add_PreviewMouseDoubleClick({
        $fontSizeSlider.Value = $fontSizeSlider.Tag
    }.GetNewClosure())

    # Ctrl+scroll over textbox area to change font size
    $scrollViewer.Add_PreviewMouseWheel({
        param($sender, $wheelArgs)
        if ([System.Windows.Input.Keyboard]::Modifiers -eq 'Control') {
            $wheelArgs.Handled = $true
            $delta    = if ($wheelArgs.Delta -gt 0) { 1 } else { -1 }
            $newValue = $fontSizeSlider.Value + $delta

            if ($newValue -ge $fontSizeSlider.Minimum -and $newValue -le $fontSizeSlider.Maximum) {
                $fontSizeSlider.Value = $newValue
            }
        }
    }.GetNewClosure())

    $copyAllBtn.Add_Click({
        if ($textBox.Text.Length -gt 0) {
            [System.Windows.Clipboard]::SetText($textBox.Text)
            # Brief visual feedback - change text and flash accent color
            $copyAllText.Text = 'Copied!'
            $originalBg = $copyAllBtn.Background
            $copyAllBtn.Background = $window.TryFindResource('AccentBrush')

            # Use script-scoped variables for timer callback
            $script:_copyTimer = [System.Windows.Threading.DispatcherTimer]::new()
            $script:_copyTimer.Interval = [TimeSpan]::FromMilliseconds(2000)
            $script:_copyTimer.Tag = @{ Button = $copyAllBtn; Text = $copyAllText; OriginalBg = $originalBg }
            $script:_copyTimer.Add_Tick({
                param($sender, $eventArgs)
                $data = $sender.Tag
                $data.Button.Background = $data.OriginalBg
                $data.Text.Text = 'Copy All'
                $sender.Stop()
            })
            $script:_copyTimer.Start()
        }
    }.GetNewClosure())

    $clearBtn.Add_Click({
        if ($textBox.Text.Length -gt 0) {
            $result = Show-UiMessageDialog -Title 'Confirm Clear' -Message 'Clear all text? This cannot be undone.' -Buttons YesNo -Icon Question -ThemeColors $colors
            if ($result -eq 'Yes') {
                $textBox.Text = ''
                # Reset find state
                $findCountLabel.Visibility = [System.Windows.Visibility]::Hidden
                $findCountLabel.Text = ''
            }
        }
    }.GetNewClosure())

    # Text search with real-time highlighting and match counter
    # Use a hashtable to encapsulate state for this specific instance
    $findState = @{
        Matches = [System.Collections.Generic.List[int]]::new()
        CurrentIndex = -1
    }

    # Helper function to update selection with visual focus toggle
    # Forces WPF to render the selection by briefly focusing the textbox
    $updateSelectionWithFocus = {
        param(
            [int]$index,
            [int]$length
        )

        # Bail on invalid range
        if ($index -lt 0 -or $length -lt 0) {
            Write-Verbose "Invalid selection range: index=$index, length=$length"
            return
        }

        $textBox.SelectionStart = $index
        $textBox.SelectionLength = $length
        $textBox.Focus()  # Give focus to render selection
        $textBox.ScrollToLine($textBox.GetLineIndexFromCharacterIndex($index))
        $findBox.Focus()  # Immediately return focus to Find box
        # Selection remains visible due to IsInactiveSelectionHighlightEnabled
    }

    $updateFindMatches = {
        $searchText = $findBox.Text
        $findState.Matches = [System.Collections.Generic.List[int]]::new()
        $findState.CurrentIndex = -1
        $findCountLabel.Text = ''

        # Hide label when search is empty
        if ([string]::IsNullOrEmpty($searchText)) {
            $findCountLabel.Visibility = [System.Windows.Visibility]::Hidden
            return
        }

        $text = $textBox.Text
        $comparison = if ($matchCaseCheck.IsChecked) {
            [System.StringComparison]::Ordinal
        }
        else {
            [System.StringComparison]::OrdinalIgnoreCase
        }

        $index = 0
        while ($index -lt $text.Length) {
            $foundIndex = $text.IndexOf($searchText, $index, $comparison)
            if ($foundIndex -ge 0) {
                $findState.Matches.Add($foundIndex)
                $index = $foundIndex + 1
            }
            else {
                break
            }
        }

        if ($findState.Matches.Count -gt 0) {
            $findCountLabel.Text = "$($findState.Matches.Count) found"
            $findCountLabel.Visibility = [System.Windows.Visibility]::Visible
            $findState.CurrentIndex = 0

            # Update selection with visual focus toggle
            & $updateSelectionWithFocus $findState.Matches[0] $searchText.Length
        }
        else {
            $findCountLabel.Text = "0 found"
            $findCountLabel.Visibility = [System.Windows.Visibility]::Visible
        }
    }

    # Ctrl+Z/Ctrl+Y in findBox must not propagate through the focus toggle to the main editor.
    # The focus toggle in $updateSelectionWithFocus briefly gives focus to the textBox, and
    # if the undo keystroke is still being processed, WPF routes it there mid-undo — crash.
    $findBox.Add_PreviewKeyDown({
        param($sender, $eventArgs)
        $mod = [System.Windows.Input.Keyboard]::Modifiers
        if ($mod -eq 'Control' -and ($eventArgs.Key -eq 'Z' -or $eventArgs.Key -eq 'Y')) {
            try {
                if ($eventArgs.Key -eq 'Z') { $sender.Undo() }
                else { $sender.Redo() }
            }
            catch { <# Undo unit may be open from in-progress text change — not critical #> }
            $eventArgs.Handled = $true
        }
    })

    $findBox.Add_TextChanged({
        # Show/hide clear button
        $clearBtn = $findBox.Tag.ClearButton
        if ($clearBtn) {
            $clearBtn.Visibility = if ([string]::IsNullOrEmpty($findBox.Text)) { 'Collapsed' } else { 'Visible' }
        }
        & $updateFindMatches
    }.GetNewClosure())

    $matchCaseCheck.Add_Checked({
        & $updateFindMatches
    }.GetNewClosure())

    $matchCaseCheck.Add_Unchecked({
        & $updateFindMatches
    }.GetNewClosure())

    $findNextBtn.Add_Click({
        if ($findState.Matches.Count -gt 0) {
            $findState.CurrentIndex = ($findState.CurrentIndex + 1) % $findState.Matches.Count
            $searchText = $findBox.Text

            # Update selection with visual focus toggle
            & $updateSelectionWithFocus $findState.Matches[$findState.CurrentIndex] $searchText.Length

            $findCountLabel.Text = "$($findState.CurrentIndex + 1) of $($findState.Matches.Count)"
        }
    }.GetNewClosure())

    $findPrevBtn.Add_Click({
        if ($findState.Matches.Count -gt 0) {
            $findState.CurrentIndex = ($findState.CurrentIndex - 1)
            if ($findState.CurrentIndex -lt 0) {
                $findState.CurrentIndex = $findState.Matches.Count - 1
            }
            $searchText = $findBox.Text

            # Update selection with visual focus toggle
            & $updateSelectionWithFocus $findState.Matches[$findState.CurrentIndex] $searchText.Length

            $findCountLabel.Text = "$($findState.CurrentIndex + 1) of $($findState.Matches.Count)"
        }
    }.GetNewClosure())

    $cancelBtn.Add_Click({ $window.Tag = $null; $window.Close() }.GetNewClosure())
    $saveBtn.Add_Click({ $window.Tag = $textBox.Text; $window.Close() }.GetNewClosure())

    # Wire up standard window loaded behavior with icon
    Initialize-UiWindowLoaded -Window $window -SetIcon

    # Clean up session on window close (only if we created it)
    $window.Add_Closed({
        if ($isStandalone) {
            $sessionId = [PsUi.SessionManager]::CurrentSessionId
            if ($sessionId -ne [Guid]::Empty) {
                [PsUi.SessionManager]::DisposeSession($sessionId)
            }
        }
    }.GetNewClosure())

    [void]$window.ShowDialog()

    return $window.Tag
    } # end block
}