Private/RenderAccordionBox.ps1
|
function Render-InTUIAccordionBox { <# .SYNOPSIS Renders accordion menu items inside a box with in-place redraw. .DESCRIPTION Draws parent (section) and child (action) rows inside a Unicode box using Console.SetCursorPosition for flicker-free updates. Expanded parents show their children inline. Supports a single expanded section at a time. Visual indicators: - Collapsed parent: right-pointing triangle with child count - Expanded parent: down-pointing triangle in Teal - IsDirect parent: right arrow in Peach - Child item: angle bracket in Subtext #> [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Rows, [Parameter(Mandatory)] [int]$SelectedIndex, [Parameter(Mandatory)] [int]$AnchorTop, [Parameter()] [int]$PreviousRowCount = 0 ) $palette = Get-InTUIColorPalette $reset = $palette.Reset $innerWidth = Get-InTUIConsoleInnerWidth $border = [char]0x2502 $hLine = [char]0x2500 $maxRow = [Console]::BufferHeight - 1 $collapsedChar = [char]0x25B8 # right-pointing triangle $expandedChar = [char]0x25BE # down-pointing triangle $directChar = [char]0x2192 # right arrow $childChar = [char]0x203A # single right-pointing angle quotation mark $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) + '...' } $row = $AnchorTop try { # Empty line inside box $emptyLine = ' {0}{1}{2}{3}{4}' -f $palette.SurfaceFg, $border, (' ' * $innerWidth), $border, $reset [Console]::SetCursorPosition(0, $row) [Console]::Write($emptyLine) $row++ # Render each row for ($i = 0; $i -lt $Rows.Count; $i++) { if ($row -gt $maxRow) { break } [Console]::SetCursorPosition(0, $row) $currentRow = $Rows[$i] $isHighlighted = ($i -eq $SelectedIndex) # Strip any [color]...[/] markup -- the renderer controls colors $rowLabel = Strip-InTUIMarkup -Text $currentRow.Label if ($currentRow.Type -eq 'parent') { $sectionNum = $currentRow.SectionIndex + 1 if ($currentRow.IsDirect) { $indicator = $directChar } elseif ($currentRow.Expanded) { $indicator = $expandedChar } else { $indicator = $collapsedChar } # Build label with child count for collapsed expandable sections $displayLabel = $rowLabel if (-not $currentRow.IsDirect -and -not $currentRow.Expanded -and $currentRow.ChildCount -gt 0) { $displayLabel = '{0} ({1})' -f $rowLabel, $currentRow.ChildCount } if ($isHighlighted) { $chevron = [char]0x276F $itemText = ' {0} {1} {2} {3}' -f $chevron, $sectionNum, $indicator, $displayLabel $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) } elseif ($currentRow.Expanded) { $itemText = ' {0} {1} {2}' -f $sectionNum, $indicator, $displayLabel $padded = (& $fitText $itemText).PadRight($innerWidth) $line = ' {0}{1}{2}{3}{4}{5}' -f $palette.SurfaceFg, $border, $palette.Teal, $padded, $reset, ('{0}{1}{2}' -f $palette.SurfaceFg, $border, $reset) } elseif ($currentRow.IsDirect) { $itemText = ' {0} {1} {2}' -f $sectionNum, $indicator, $displayLabel $padded = (& $fitText $itemText).PadRight($innerWidth) $line = ' {0}{1}{2}{3}{4}{5}' -f $palette.SurfaceFg, $border, $palette.Peach, $padded, $reset, ('{0}{1}{2}' -f $palette.SurfaceFg, $border, $reset) } else { $itemText = ' {0} {1} {2}' -f $sectionNum, $indicator, $displayLabel $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) } } else { # Child row $childNum = '{0}.{1}' -f ($currentRow.SectionIndex + 1), ($currentRow.ChildIndex + 1) if ($isHighlighted) { $chevron = [char]0x276F $itemText = ' {0} {1} {2} {3}' -f $chevron, $childNum, $childChar, $rowLabel $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 $childNum, $childChar, $rowLabel $padded = (& $fitText $itemText).PadRight($innerWidth) $line = ' {0}{1}{2}{3}{4}{5}' -f $palette.SurfaceFg, $border, $palette.Subtext, $padded, $reset, ('{0}{1}{2}' -f $palette.SurfaceFg, $border, $reset) } } [Console]::Write($line) $row++ } # Blank excess rows from previous render (when collapsing) $currentRowCount = $Rows.Count if ($PreviousRowCount -gt $currentRowCount) { $blankCount = $PreviousRowCount - $currentRowCount for ($b = 0; $b -lt $blankCount; $b++) { [Console]::SetCursorPosition(0, $row) [Console]::Write($emptyLine) $row++ } } # Empty line [Console]::SetCursorPosition(0, $row) [Console]::Write($emptyLine) $row++ # Gradient separator line (├──gradient──┤) $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 $hintText = ' Arrows | Right/Left: expand | Enter | Esc' $hintPadded = (& $fitText $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──╯) $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++ # Clear one trailing line to avoid stale artifacts if ($row -lt [Console]::BufferHeight) { $windowWidth = [Console]::WindowWidth if ($windowWidth -gt 1) { [Console]::SetCursorPosition(0, $row) [Console]::Write(' ' * ($windowWidth - 1)) } } } catch [System.ArgumentOutOfRangeException] { # Terminal too small for full accordion — render truncated gracefully $null = $_ } # Move cursor below the box try { [Console]::SetCursorPosition(0, [math]::Min($row, $maxRow)) } catch { $null = $_ } } |