Private/Console/Layout.psm1

using namespace System
using namespace System.Collections.Generic
using namespace System.Linq

using module ..\Enums.psm1
using module ..\Abstracts.psm1
using module .\Rendering.psm1
using module .\Widgets.psm1
using module .\Tables.psm1
using module .\TableRenderer.psm1
using module .\Table.psm1

class Rows : IRenderable {
  hidden [List[IRenderable]] $_children
  [bool]$Expand

  Rows([IEnumerable[IRenderable]]$items) {
    $this._children = [List[IRenderable]]::new($items)
    $this.Expand = $false
  }

  Rows([IRenderable[]]$items) {
    $this._children = [List[IRenderable]]::new($items)
    $this.Expand = $false
  }

  [Measurement] Measure([RenderOptions]$options, [int]$maxWidth) {
    if ($this.Expand) { return [Measurement]::new($maxWidth, $maxWidth) }
    if ($this._children.Count -eq 0) { return [Measurement]::new(0, 0) }

    $measurements = [List[Measurement]]::new()
    foreach ($c in $this._children) { $measurements.Add($c.Measure($options, $maxWidth)) }

    $min = 0; $max = 0
    foreach ($m in $measurements) {
      if ($m.Min -gt $min) { $min = $m.Min }
      if ($m.Max -gt $max) { $max = $m.Max }
    }
    return [Measurement]::new($min, $max)
  }

  [object[]] Render([RenderOptions]$options, [int]$maxWidth) {
    $result = [List[Segment]]::new()
    foreach ($child in $this._children) {
      $rendered = $child.Render($options, $maxWidth)
      $result.AddRange($rendered)
      if ($rendered.Count -gt 0 -and !$rendered[$rendered.Count - 1].IsLineBreak) {
        $result.Add([Segment]::LineBreak)
      }
    }
    return $result.ToArray()
  }
}

class Columns : IRenderable {
  hidden [List[IRenderable]] $_items
  [Padding]$Padding
  [bool]$Expand

  Columns([IEnumerable[IRenderable]]$items) {
    $this._items = [List[IRenderable]]::new($items)
    $this.Padding = [Padding]::new(0, 0, 1, 0)
    $this.Expand = $true
  }

  [Measurement] Measure([RenderOptions]$options, [int]$maxWidth) {
    if ($this._items.Count -eq 0) { return [Measurement]::new(0, 0) }
    $maxPadding = [Math]::Max($this.Padding.Left, $this.Padding.Right)

    $itemWidths = [List[int]]::new()
    foreach ($item in $this._items) { $itemWidths.Add($item.Measure($options, $maxWidth).Max) }

    $columnCount = $this.CalculateColumnCount($maxWidth, $itemWidths.ToArray(), $this._items.Count, $maxPadding)
    if ($columnCount -le 0) { return [Measurement]::new($maxWidth, $maxWidth) }

    $rows = [Math]::Ceiling($this._items.Count / $columnCount)
    $greatestWidth = 0
    for ($r = 0; $r -lt $rows; $r++) {
      $start = $r * $columnCount
      $end = [Math]::Min($start + $columnCount, $this._items.Count)
      $rowWidth = 0
      for ($i = $start; $i -lt $end; $i++) { $rowWidth += $itemWidths[$i] }
      $rowWidth += ($maxPadding * ($end - $start - 1))
      if ($rowWidth -gt $greatestWidth) { $greatestWidth = $rowWidth }
    }

    return [Measurement]::new($greatestWidth, $greatestWidth)
  }

  [object[]] Render([RenderOptions]$options, [int]$maxWidth) {
    if ($this._items.Count -eq 0) { return [object[]]@() }
    $maxPadding = [Math]::Max($this.Padding.Left, $this.Padding.Right)

    $itemWidths = [List[int]]::new()
    foreach ($item in $this._items) { $itemWidths.Add($item.Measure($options, $maxWidth).Max) }

    $columnCount = $this.CalculateColumnCount($maxWidth, $itemWidths.ToArray(), $this._items.Count, $maxPadding)
    if ($columnCount -le 0) { $columnCount = 1 }

    $table = [Table]::new()
    $table.Border = [NoTableBorder]::new()
    $table.ShowHeaders = $false
    $table.PadRightCell = $false
    if ($this.Expand) { $table.Expand = $true }

    for ($i = 0; $i -lt $columnCount; $i++) {
      $col = [TableColumn]::new("")
      $col.Padding = $this.Padding
      $col.NoWrap = $true
      $table.AddColumn($col)
    }

    for ($start = 0; $start -lt $this._items.Count; $start += $columnCount) {
      $rowItems = [List[IRenderable]]::new()
      for ($i = $start; $i -lt [Math]::Min($start + $columnCount, $this._items.Count); $i++) {
        $rowItems.Add($this._items[$i])
      }
      # Fill remaining columns with empty text if any
      while ($rowItems.Count -lt $columnCount) { $rowItems.Add([Text]::new("")) }
      $table.AddRow($rowItems)
    }

    return $table.Render($options, $maxWidth)
  }

  hidden [int] CalculateColumnCount([int]$maxWidth, [int[]]$itemWidths, [int]$columnCount, [int]$padding) {
    $currentCount = $columnCount
    while ($currentCount -gt 1) {
      $widths = @{}
      $exceeded = $false
      for ($i = 0; $i -lt $itemWidths.Length; $i++) {
        $colIdx = $i % $currentCount
        if (!$widths.ContainsKey($colIdx)) { $widths[$colIdx] = 0 }
        $widths[$colIdx] = [Math]::Max($widths[$colIdx], $itemWidths[$i])

        $total = 0
        foreach ($w in $widths.Values) { $total += $w }
        $total += ($padding * ($widths.Count - 1))
        if ($total -gt $maxWidth) {
          $currentCount = $widths.Count - 1
          $exceeded = $true
          break
        }
      }
      if (!$exceeded) { break }
    }
    return $currentCount
  }
}

class GridColumn {
  [Nullable[int]]$Width
  [bool]$NoWrap
  [Padding]$Padding
  [Nullable[Justify]]$Alignment
  [bool]$HasExplicitPadding

  GridColumn() {
    $this.Padding = [Padding]::new(0, 0, 2, 0)
    $this.NoWrap = $false
    $this.HasExplicitPadding = $false
  }

  [GridColumn] PadRight([int]$padding) {
    $this.Padding = [Padding]::new($this.Padding.Left, $this.Padding.Top, $padding, $this.Padding.Bottom)
    $this.HasExplicitPadding = $true
    return $this
  }

  [GridColumn] NoWrap() {
    $this.NoWrap = $true
    return $this
  }
}

class Grid : IRenderable {
  hidden [List[GridColumn]] $_columns
  hidden [List[List[IRenderable]]] $_rows
  [bool]$Expand
  [Nullable[int]]$Width

  Grid() {
    $this._columns = [List[GridColumn]]::new()
    $this._rows = [List[List[IRenderable]]]::new()
    $this.Expand = $false
  }

  [Grid] AddColumn() {
    $this._columns.Add([GridColumn]::new())
    return $this
  }

  [Grid] AddColumn([GridColumn]$column) {
    if ($this._rows.Count -gt 0) { throw [InvalidOperationException]::new("Cannot add columns after rows.") }
    $this._columns.Add($column)
    return $this
  }

  [Grid] AddRow([IRenderable[]]$items) {
    if ($items.Length -gt $this._columns.Count) { throw [InvalidOperationException]::new("Row has more items than columns.") }
    $this._rows.Add([List[IRenderable]]::new($items))
    return $this
  }

  [Grid] AddRow([object[]]$items) {
    $list = [List[IRenderable]]::new()
    foreach ($item in $items) {
      if ($item -is [IRenderable]) {
        $list.Add([IRenderable]$item)
      } else {
        $text = if ($null -eq $item) { [string]::Empty } else { [string]$item }
        $list.Add([Markup]::new($text))
      }
    }
    return $this.AddRow($list.ToArray())
  }

  [Measurement] Measure([RenderOptions]$options, [int]$maxWidth) {
    $table = $this.BuildTable()
    return $table.Measure($options, $maxWidth)
  }

  [Segment[]] Render([RenderOptions]$options, [int]$maxWidth) {
    $table = $this.BuildTable()
    return $table.Render($options, $maxWidth)
  }

  hidden [Table] BuildTable() {
    $table = [Table]::new()
    $table.Border = [NoTableBorder]::new()
    $table.ShowHeaders = $false
    $table.IsGrid = $true
    $table.Width = $this.Width
    $table.Expand = $this.Expand

    foreach ($col in $this._columns) {
      $tc = [TableColumn]::new("")
      $tc.Width = $col.Width
      $tc.NoWrap = $col.NoWrap
      $tc.Padding = $col.Padding
      $tc.Alignment = $col.Alignment
      $table.AddColumn($tc)
    }

    foreach ($row in $this._rows) {
      $table.AddRow($row)
    }

    return $table
  }
}