Private/RenderMenuBox.ps1
|
function Render-InTUIMenuBox { <# .SYNOPSIS Renders menu items inside a rounded-corner box with in-place redraw. .DESCRIPTION Draws menu items with numbered indicators and chevrons inside a Unicode box. Uses Console.SetCursorPosition + Console.Write for flicker-free in-place updates. Supports single-select and multi-select modes with viewport scrolling for long lists. The caller renders the top border and title area before the loop; this function renders: empty line, items, empty line, gradient separator, hint line, and bottom border. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string[]]$Items, [Parameter(Mandatory)] [int]$SelectedIndex, [Parameter(Mandatory)] [int]$AnchorTop, [Parameter()] [bool[]]$Checked, [Parameter()] [switch]$IncludeBack, [Parameter()] [switch]$IncludeQuit, [Parameter()] [switch]$MultiSelect, [Parameter()] [int]$ViewportOffset = 0, [Parameter()] [int]$ViewportSize = 0 ) $palette = Get-InTUIColorPalette $reset = $palette.Reset $innerWidth = Get-InTUIConsoleInnerWidth $border = [char]0x2502 $hLine = [char]0x2500 $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.SurfaceFg, $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 in the first slot 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.SurfaceFg, $border, $palette.Dim, $indicatorPadded, $reset, ('{0}{1}{2}' -f $palette.SurfaceFg, $border, $reset) [Console]::Write($line) $row++ continue } # Scroll-down indicator in the last slot 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.SurfaceFg, $border, $palette.Dim, $indicatorPadded, $reset, ('{0}{1}{2}' -f $palette.SurfaceFg, $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) # Strip any [color]...[/] markup -- the renderer controls item colors $label = Strip-InTUIMarkup -Text $Items[$i] if ($MultiSelect -and $Checked) { if ($Checked[$i]) { $checkChar = [char]0x2611 # checked box } else { $checkChar = [char]0x2610 # unchecked box } if ($isHighlighted) { $chevron = [char]0x276F $itemText = ' {0} {1} {2} {3} {4}' -f $chevron, $checkChar, $num, ([char]0x25B8), $label $padded = (& $fitText $itemText).PadRight($innerWidth) $line = ' {0}{1}{2}{3}{4}{5}{6}{7}' -f $palette.SurfaceFg, $border, $palette.BgSelect, $palette.Mauve, $palette.Bold, $padded, $reset, ('{0}{1}{2}' -f $palette.SurfaceFg, $border, $reset) } else { $checkColor = if ($Checked[$i]) { $palette.Green } else { $palette.Dim } $itemText = ' {0} {1} {2} {3}' -f $checkChar, $num, ([char]0x25B8), $label $padded = (& $fitText $itemText).PadRight($innerWidth) $line = ' {0}{1}{2}{3}{4}{5}' -f $palette.SurfaceFg, $border, $checkColor, $padded, $reset, ('{0}{1}{2}' -f $palette.SurfaceFg, $border, $reset) } } else { if ($isHighlighted) { $chevron = [char]0x276F $itemText = ' {0} {1} {2} {3}' -f $chevron, $num, ([char]0x25B8), $label $padded = (& $fitText $itemText).PadRight($innerWidth) $line = ' {0}{1}{2}{3}{4}{5}{6}{7}' -f $palette.SurfaceFg, $border, $palette.BgSelect, $palette.Mauve, $palette.Bold, $padded, $reset, ('{0}{1}{2}' -f $palette.SurfaceFg, $border, $reset) } else { $itemText = ' {0} {1} {2}' -f $num, ([char]0x25B8), $label $padded = (& $fitText $itemText).PadRight($innerWidth) $line = ' {0}{1}{2}{3}{4}{5}' -f $palette.SurfaceFg, $border, $palette.Text, $padded, $reset, ('{0}{1}{2}' -f $palette.SurfaceFg, $border, $reset) } } [Console]::Write($line) $row++ } # Empty line if ($row -lt $bufferHeight) { [Console]::SetCursorPosition(0, $row) [Console]::Write($emptyLine) } $row++ # Gradient separator line (├──gradient──┤) if ($row -lt $bufferHeight) { $sepGradient = Get-InTUIGradientLine -Character $hLine -Width $innerWidth $sepLine = ' {0}{1}{2}{3}{4}' -f $palette.SurfaceFg, ([char]0x251C), $sepGradient, ([char]0x2524), $reset [Console]::SetCursorPosition(0, $row) [Console]::Write($sepLine) } $row++ # Hint line inside the box if ($MultiSelect) { $hintText = ' Space to toggle, A for all, Enter to confirm' } else { $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.SurfaceFg, $border, $palette.Dim, $hintPadded, $reset, ('{0}{1}{2}' -f $palette.SurfaceFg, $border, $reset) [Console]::SetCursorPosition(0, $row) [Console]::Write($hintLine) } $row++ # Bottom border with gradient (╰──gradient──╯) if ($row -lt $bufferHeight) { $bottomGradient = Get-InTUIGradientLine -Character $hLine -Width $innerWidth $bottomLine = ' {0}{1}{2}{3}{4}' -f $palette.SurfaceFg, ([char]0x2570), $bottomGradient, ([char]0x256F), $reset [Console]::SetCursorPosition(0, $row) [Console]::Write($bottomLine) } $row++ # Move cursor below the box if ($row -lt $bufferHeight) { [Console]::SetCursorPosition(0, $row) } } |