Private/UI/Render-PSMBMenuBox.ps1
|
function Render-PSMBMenuBox { <# .SYNOPSIS Renders menu items inside a rounded-corner box with in-place redraw. .DESCRIPTION Draws menu items with highlight indicator inside a Unicode box. Uses Console.SetCursorPosition for flicker-free in-place updates. Supports viewport scrolling for long lists. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string[]]$Items, [Parameter(Mandatory)] [int]$SelectedIndex, [Parameter(Mandatory)] [int]$AnchorTop, [Parameter()] [switch]$IncludeBack, [Parameter()] [switch]$IncludeQuit, [Parameter()] [int]$ViewportOffset = 0, [Parameter()] [int]$ViewportSize = 0 ) $palette = Get-PSMBColorPalette $esc = [char]27 $reset = "${esc}[0m" $innerWidth = Get-PSMBConsoleInnerWidth $border = ([char]0x2502) $hLine = ([char]0x2500) $amberRGB = @(168, 213, 162) $roseRGB = @(94, 234, 212) $fitText = { param([string]$Text) if ($null -eq $Text) { return '' } if ($Text.Length -le $innerWidth) { return $Text } if ($innerWidth -le 3) { return $Text.Substring(0, $innerWidth) } return $Text.Substring(0, $innerWidth - 3) + '...' } $bufferHeight = [Console]::BufferHeight # Determine viewport boundaries $useViewport = ($ViewportSize -gt 0) -and ($ViewportSize -lt $Items.Count) if ($useViewport) { $showAbove = ($ViewportOffset -gt 0) $showBelow = (($ViewportOffset + $ViewportSize) -lt $Items.Count) $slotCount = $ViewportSize } else { $showAbove = $false $showBelow = $false $slotCount = $Items.Count } $row = $AnchorTop # Empty line inside box $emptyLine = ' {0}{1}{2}{3}{4}' -f $palette.Surface, $border, (' ' * $innerWidth), $border, $reset if ($row -lt $bufferHeight) { [Console]::SetCursorPosition(0, $row) [Console]::Write($emptyLine) } $row++ # Render item slots for ($slot = 0; $slot -lt $slotCount; $slot++) { if ($row -ge $bufferHeight) { break } [Console]::SetCursorPosition(0, $row) # Scroll-up indicator if ($useViewport -and $slot -eq 0 -and $showAbove) { $aboveCount = $ViewportOffset $indicatorText = (' {0} {1} more above' -f ([char]0x25B4), $aboveCount) $indicatorPadded = (& $fitText $indicatorText).PadRight($innerWidth) $line = ' {0}{1}{2}{3}{4}{5}' -f $palette.Surface, $border, $palette.Dim, $indicatorPadded, $reset, ('{0}{1}{2}' -f $palette.Surface, $border, $reset) [Console]::Write($line) $row++ continue } # Scroll-down indicator if ($useViewport -and $slot -eq ($slotCount - 1) -and $showBelow) { $belowCount = $Items.Count - ($ViewportOffset + $ViewportSize) $indicatorText = (' {0} {1} more below' -f ([char]0x25BE), $belowCount) $indicatorPadded = (& $fitText $indicatorText).PadRight($innerWidth) $line = ' {0}{1}{2}{3}{4}{5}' -f $palette.Surface, $border, $palette.Dim, $indicatorPadded, $reset, ('{0}{1}{2}' -f $palette.Surface, $border, $reset) [Console]::Write($line) $row++ continue } # Map slot to actual item index $i = if ($useViewport) { $ViewportOffset + $slot } else { $slot } $num = $i + 1 $isHighlighted = ($i -eq $SelectedIndex) if ($isHighlighted) { $chevron = [char]0x276F $itemText = (' {0} {1} {2} {3}' -f $chevron, $num, ([char]0x25B8), $Items[$i]) $padded = (& $fitText $itemText).PadRight($innerWidth) $line = ' {0}{1}{2}{3}{4}{5}{6}{7}' -f $palette.Surface, $border, $palette.BgSelect, $palette.Amber, $palette.Bold, $padded, $reset, ('{0}{1}{2}' -f $palette.Surface, $border, $reset) } else { $itemText = (' {0} {1} {2}' -f $num, ([char]0x25B8), $Items[$i]) $padded = (& $fitText $itemText).PadRight($innerWidth) $line = ' {0}{1}{2}{3}{4}{5}' -f $palette.Surface, $border, $palette.Text, $padded, $reset, ('{0}{1}{2}' -f $palette.Surface, $border, $reset) } [Console]::Write($line) $row++ } # Empty line if ($row -lt $bufferHeight) { [Console]::SetCursorPosition(0, $row) [Console]::Write($emptyLine) } $row++ # Separator line if ($row -lt $bufferHeight) { $sepGradient = Get-PSMBGradientLine -Character $hLine -Length $innerWidth -StartRGB $amberRGB -EndRGB $roseRGB $sepLine = ' {0}{1}{2}{3}{4}' -f $palette.Surface, ([char]0x251C), $sepGradient, ([char]0x2524), $reset [Console]::SetCursorPosition(0, $row) [Console]::Write($sepLine) } $row++ # Hint line $hintText = ' Use arrow keys to navigate, Enter to select' if ($IncludeBack) { $hintText += ', Esc to go back' } elseif ($IncludeQuit) { $hintText += ', Esc to quit' } if ($row -lt $bufferHeight) { $hintPadded = $hintText.PadRight($innerWidth) $hintLine = ' {0}{1}{2}{3}{4}{5}' -f $palette.Surface, $border, $palette.Dim, $hintPadded, $reset, ('{0}{1}{2}' -f $palette.Surface, $border, $reset) [Console]::SetCursorPosition(0, $row) [Console]::Write($hintLine) } $row++ # Bottom border if ($row -lt $bufferHeight) { $bottomGradient = Get-PSMBGradientLine -Character $hLine -Length $innerWidth -StartRGB $amberRGB -EndRGB $roseRGB $bottomLine = ' {0}{1}{2}{3}{4}' -f $palette.Surface, ([char]0x2570), $bottomGradient, ([char]0x256F), $reset [Console]::SetCursorPosition(0, $row) [Console]::Write($bottomLine) } $row++ if ($row -lt $bufferHeight) { [Console]::SetCursorPosition(0, $row) } } |