src/UI/Viewport.ps1
|
# Viewport — Ventana de scroll sobre una lista de items. # # Responsabilidad ÚNICA: dado un cursor (selectedIndex) y un total (totalCount), # decidir qué slice [Start, EndExclusive) es visible, manteniendo un margen de # scrolloff alrededor del cursor. # # NO sabe de Console, Renderer ni rendering. Es lógica pura sobre enteros, fácil # de testear sin terminal real. # # Caller (MainScreen) calcula el available height (terminal − chrome) y el # rows-per-item (compact vs wide) y se los pasa al Resize. class Viewport { [int] $Start = 0 # Primer item visible (índice base 0 en la lista total). [int] $Capacity = 1 # Cuántos items entran en la ventana actual. [int] $Scrolloff = 2 # Margen mínimo de items entre el cursor y los bordes. Viewport() { } Viewport([int]$scrolloff) { if ($scrolloff -lt 0) { throw "Viewport: scrolloff no puede ser negativo (recibí $scrolloff)." } $this.Scrolloff = $scrolloff } # Recalcula Capacity según el alto disponible y cuántas filas físicas ocupa # cada item (1 en wide, 2 en compact). # No toca Start — eso lo decide EnsureVisible. Capacity nunca queda en 0. [void] Resize([int]$availableHeight, [int]$rowsPerItem) { if ($rowsPerItem -le 0) { $rowsPerItem = 1 } $cap = [Math]::Floor($availableHeight / $rowsPerItem) $this.Capacity = [Math]::Max(1, [int]$cap) } # Ajusta Start para que selectedIndex quede visible con scrolloff. # Patrón "scrolloff": el cursor mantiene un margen contra los bordes; cuando # los cruza, el viewport se mueve lo justo para devolverle el margen. # # Si scrolloff es mayor que la mitad de Capacity, lo bajamos a esa mitad — # sino el cursor se quedaría siempre en el centro y nunca alcanzaría los bordes. [void] EnsureVisible([int]$selectedIndex, [int]$totalCount) { if ($totalCount -le 0) { $this.Start = 0 return } $effectiveScrolloff = [Math]::Min($this.Scrolloff, [Math]::Floor($this.Capacity / 2)) # Borde inferior: cursor cae dentro de los últimos $effectiveScrolloff slots. if ($selectedIndex -ge $this.Start + $this.Capacity - $effectiveScrolloff) { $this.Start = $selectedIndex - $this.Capacity + 1 + $effectiveScrolloff } # Borde superior: cursor cae dentro de los primeros $effectiveScrolloff slots. if ($selectedIndex -lt $this.Start + $effectiveScrolloff) { $this.Start = $selectedIndex - $effectiveScrolloff } # Clamp final. $maxStart = [Math]::Max(0, $totalCount - $this.Capacity) if ($this.Start -gt $maxStart) { $this.Start = $maxStart } if ($this.Start -lt 0) { $this.Start = 0 } } # Índice exclusive del último item visible — listo para usar en for loops: # for ($i = $vp.Start; $i -lt $vp.EndExclusive($total); $i++) { ... } [int] EndExclusive([int]$totalCount) { return [Math]::Min($totalCount, $this.Start + $this.Capacity) } # Vuelve al top. Útil al cambiar de contexto (drill-in, reload total). [void] Reset() { $this.Start = 0 } } |