@@ -199,16 +199,7 @@ func (m DashboardModel) View() string {
199199 return mutedText ("No dashboard widgets configured for the active preset" )
200200 }
201201
202- layout := m .layoutWidgets (widgets , m .width , m .height , style )
203- rendered := make ([]string , 0 , len (layout ))
204- for _ , items := range layout {
205- columnParts := make ([]string , 0 , len (items ))
206- for _ , item := range items {
207- columnParts = append (columnParts , item .content )
208- }
209- rendered = append (rendered , strings .Join (columnParts , "\n " ))
210- }
211- return lipgloss .JoinHorizontal (lipgloss .Top , rendered ... )
202+ return padToHeight (m .layoutWidgets (widgets , m .width , m .height , style ), m .height )
212203}
213204
214205func (m DashboardModel ) HelpView () string {
@@ -299,41 +290,36 @@ func (m DashboardModel) widgetByID(id string) (Widget, bool) {
299290
300291func (m DashboardModel ) renderWidgetCard (widget Widget , width , height int , style string ) string {
301292 bodyHeight := max (3 , height - 3 )
302- body := widget .Render (width - 4 , bodyHeight , style )
293+ body := padToHeight ( widget .Render (width - 4 , bodyHeight , style ), bodyHeight )
303294 card := lipgloss .NewStyle ().
304295 Width (width ).
296+ Height (height ).
305297 Border (lipgloss .NormalBorder ()).
306298 BorderForeground (lipgloss .Color (ColorDeep )).
307299 Padding (0 , 1 )
308- if widgetExpandable (widget .ID ()) {
309- body = padToHeight (body , bodyHeight )
310- card = card .Height (height )
311- }
312300 return card .Render (lipgloss .NewStyle ().Foreground (lipgloss .Color (ColorGold )).Bold (true ).Render (widget .Title ()) + "\n " + body )
313301}
314302
315- func (m DashboardModel ) layoutWidgets (widgets []Widget , contentWidth , contentHeight int , style string ) [][] renderedWidget {
303+ func (m DashboardModel ) layoutWidgets (widgets []Widget , contentWidth , contentHeight int , style string ) string {
316304 if len (widgets ) == 0 {
317- return nil
305+ return ""
318306 }
319307 columns := min (dashboardColumnsForWidth (contentWidth ), len (widgets ))
320- columnWidth := max (22 , (contentWidth - (columns - 1 )* 2 )/ columns )
321- buckets := make ([][]Widget , columns )
322- for idx , widget := range widgets {
323- buckets [idx % columns ] = append (buckets [idx % columns ], widget )
324- }
325-
326- result := make ([][]renderedWidget , columns )
327- for idx , bucket := range buckets {
328- heights := distributeHeight (bucket , contentHeight )
329- for itemIdx , widget := range bucket {
330- result [idx ] = append (result [idx ], renderedWidget {
331- content : m .renderWidgetCard (widget , columnWidth , heights [itemIdx ], style ),
332- height : heights [itemIdx ],
333- })
308+ if columns <= 0 {
309+ return ""
310+ }
311+ rows := chunkWidgets (widgets , columns )
312+ rowHeights := distributeRowHeights (rows , contentHeight )
313+ renderedRows := make ([]string , 0 , len (rows ))
314+ for rowIdx , row := range rows {
315+ cardWidth := rowCardWidth (contentWidth , len (row ))
316+ cards := make ([]string , 0 , len (row ))
317+ for _ , widget := range row {
318+ cards = append (cards , m .renderWidgetCard (widget , cardWidth , rowHeights [rowIdx ], style ))
334319 }
320+ renderedRows = append (renderedRows , joinHorizontalCards (cards , rowHeights [rowIdx ]))
335321 }
336- return result
322+ return strings . Join ( renderedRows , " \n " )
337323}
338324
339325func (m DashboardModel ) renderGPUWidget (index , width , height int , style string ) string {
@@ -624,49 +610,109 @@ func dashboardColumnsForWidth(width int) int {
624610 }
625611}
626612
627- func distributeHeight (widgets []Widget , contentHeight int ) []int {
628- if len (widgets ) == 0 {
613+ func chunkWidgets (widgets []Widget , columns int ) [][] Widget {
614+ if len (widgets ) == 0 || columns <= 0 {
629615 return nil
630616 }
617+ rows := make ([][]Widget , 0 , (len (widgets )+ columns - 1 )/ columns )
618+ for start := 0 ; start < len (widgets ); start += columns {
619+ end := min (len (widgets ), start + columns )
620+ row := make ([]Widget , 0 , end - start )
621+ row = append (row , widgets [start :end ]... )
622+ rows = append (rows , row )
623+ }
624+ return rows
625+ }
631626
632- gaps := max (0 , len (widgets )- 1 )
633- available := max (len (widgets )* 4 , contentHeight - gaps )
634- heights := make ([]int , len (widgets ))
635- totalMin := 0
636- expandable := make ([]int , 0 , len (widgets ))
627+ func distributeRowHeights (rows [][]Widget , contentHeight int ) []int {
628+ if len (rows ) == 0 {
629+ return nil
630+ }
637631
638- for idx , widget := range widgets {
639- minHeight := widgetMinHeight (widget .ID ())
640- heights [idx ] = minHeight
641- totalMin += minHeight
642- if widgetExpandable (widget .ID ()) {
643- expandable = append (expandable , idx )
644- }
632+ rowGaps := max (0 , len (rows )- 1 )
633+ available := max (len (rows )* 4 , contentHeight - rowGaps )
634+ preferred := make ([]int , len (rows ))
635+ weights := make ([]int , len (rows ))
636+ totalPreferred := 0
637+ totalWeight := 0
638+ for idx , row := range rows {
639+ preferred [idx ] = rowPreferredHeight (row )
640+ weights [idx ] = rowWeight (row )
641+ totalPreferred += preferred [idx ]
642+ totalWeight += weights [idx ]
645643 }
646644
647- if len ( expandable ) == 0 {
648- return heights
645+ if totalPreferred > available {
646+ return evenHeights ( available , len ( rows ))
649647 }
650648
651- remaining := available - totalMin
652- if remaining <= 0 {
649+ heights := append ([]int (nil ), preferred ... )
650+ remaining := available - totalPreferred
651+ if remaining <= 0 || totalWeight <= 0 {
653652 return heights
654653 }
655- share := 0
656- extra := 0
657- if len (expandable ) > 0 {
658- share = remaining / len (expandable )
659- extra = remaining % len (expandable )
654+
655+ remainders := make ([]int , len (rows ))
656+ for idx := range rows {
657+ product := remaining * weights [idx ]
658+ heights [idx ] += product / totalWeight
659+ remainders [idx ] = product % totalWeight
660660 }
661- for idx , widgetIdx := range expandable {
662- heights [widgetIdx ] += share
663- if idx < extra {
664- heights [widgetIdx ]++
661+
662+ for assigned := sumInts (heights ) - totalPreferred ; assigned < remaining ; assigned ++ {
663+ bestIdx := - 1
664+ bestRemainder := - 1
665+ for idx , remainder := range remainders {
666+ if remainder > bestRemainder {
667+ bestIdx = idx
668+ bestRemainder = remainder
669+ }
665670 }
671+ if bestIdx < 0 {
672+ break
673+ }
674+ heights [bestIdx ]++
675+ remainders [bestIdx ] = - 1
666676 }
667677 return heights
668678}
669679
680+ func rowCardWidth (contentWidth , cards int ) int {
681+ if cards <= 0 {
682+ return contentWidth
683+ }
684+ gaps := max (0 , cards - 1 )
685+ return max (22 , (contentWidth - gaps )/ cards )
686+ }
687+
688+ func rowPreferredHeight (row []Widget ) int {
689+ height := 4
690+ for _ , widget := range row {
691+ height = max (height , widgetMinHeight (widget .ID ()))
692+ }
693+ return height
694+ }
695+
696+ func rowWeight (row []Widget ) int {
697+ weight := 1
698+ for _ , widget := range row {
699+ weight = max (weight , widgetWeight (widget .ID ()))
700+ }
701+ return weight
702+ }
703+
704+ func joinHorizontalCards (cards []string , height int ) string {
705+ if len (cards ) == 0 {
706+ return ""
707+ }
708+ row := cards [0 ]
709+ gap := lipgloss .NewStyle ().Width (1 ).Height (height ).Render ("" )
710+ for _ , card := range cards [1 :] {
711+ row = lipgloss .JoinHorizontal (lipgloss .Top , row , gap , card )
712+ }
713+ return row
714+ }
715+
670716func evenHeights (total , count int ) []int {
671717 if count <= 0 {
672718 return nil
@@ -686,15 +732,6 @@ func evenHeights(total, count int) []int {
686732 return heights
687733}
688734
689- func widgetExpandable (id string ) bool {
690- switch id {
691- case "gpu-graph" , "gpu-graph-2" :
692- return true
693- default :
694- return false
695- }
696- }
697-
698735func widgetMinHeight (id string ) int {
699736 switch id {
700737 case "gpu-graph" , "gpu-graph-2" :
@@ -710,6 +747,29 @@ func widgetMinHeight(id string) int {
710747 }
711748}
712749
750+ func widgetWeight (id string ) int {
751+ switch id {
752+ case "gpu-graph" , "gpu-graph-2" :
753+ return 4
754+ case "suite-status" , "flow-services" , "neural-containers" :
755+ return 3
756+ case "system-health" , "port-map" :
757+ return 2
758+ case "vram-bar" , "ram-bar" :
759+ return 1
760+ default :
761+ return 1
762+ }
763+ }
764+
765+ func sumInts (values []int ) int {
766+ total := 0
767+ for _ , value := range values {
768+ total += value
769+ }
770+ return total
771+ }
772+
713773func renderLabeledBlock (label , detail string , totalWidth int ) []string {
714774 labelWidth := 10
715775 detailWidth := max (12 , totalWidth - labelWidth - 1 )
0 commit comments