@@ -7,8 +7,7 @@ use ratatui::{
77} ;
88
99use super :: app:: {
10- all_columns, App , CounterRate , DeviceClass , PortThroughput , TableColumn , BAR_WIDTH ,
11- EXTRA_COUNTERS ,
10+ all_columns, App , CounterRate , DeviceClass , PortThroughput , TableColumn , EXTRA_COUNTERS ,
1211} ;
1312use super :: theme:: ThemeColors ;
1413use crate :: gpu:: GpuMetrics ;
@@ -257,11 +256,11 @@ fn draw_tab_bar(frame: &mut Frame, app: &App, area: Rect, tc: &ThemeColors) {
257256 frame. render_widget ( Paragraph :: new ( tab_bar_line ( app, tc) ) , area) ;
258257}
259258
260- fn gbps_bar ( gbps : f64 , link_gbps : Option < f64 > ) -> String {
259+ fn gbps_bar ( gbps : f64 , link_gbps : Option < f64 > , width : usize ) -> String {
261260 // Scale to the port's line rate; fall back to a default when unknown.
262261 let max = link_gbps. filter ( |& r| r > 0.0 ) . unwrap_or ( RDMA_LINK_GBPS ) ;
263262 let ratio = ( gbps / max) . clamp ( 0.0 , 1.0 ) ;
264- ratio_bar ( ratio, BAR_WIDTH )
263+ ratio_bar ( ratio, width )
265264}
266265
267266/// Meter string for a 0..=1 ratio, `width` chars, glyph-mode aware.
@@ -275,25 +274,98 @@ fn ratio_bar(ratio: f64, width: usize) -> String {
275274}
276275
277276/// "██████░░░░ 87%" gauge text for a 0-100 percent, or "-" when None.
278- fn pct_gauge_text ( pct : Option < u32 > ) -> String {
277+ /// `bar_w` sizes the meter; the trailing " {:>3}%" text is fixed.
278+ fn pct_gauge_text ( pct : Option < u32 > , bar_w : usize ) -> String {
279279 match pct {
280- Some ( p) => format ! ( "{} {:>3}%" , ratio_bar( p as f64 / 100.0 , 10 ) , p) ,
280+ Some ( p) => format ! ( "{} {:>3}%" , ratio_bar( p as f64 / 100.0 , bar_w ) , p) ,
281281 None => "-" . to_string ( ) ,
282282 }
283283}
284284
285- fn column_cell ( col : & TableColumn , t : & PortThroughput , tc : & ThemeColors ) -> Cell < ' static > {
285+ /// Scale column widths by a common fraction of their naturals to fit
286+ /// `avail` (incl. one-cell gaps), flooring each at `mins`. Fits whenever
287+ /// `avail >= sum(mins) + gaps`; below that all columns sit at their min
288+ /// and ratatui clips. See the shave pass for how floor overshoot is paid.
289+ fn scaled_col_widths ( natural : & [ u16 ] , mins : & [ u16 ] , avail : u16 ) -> Vec < u16 > {
290+ debug_assert_eq ! ( natural. len( ) , mins. len( ) ) ;
291+ if natural. is_empty ( ) {
292+ return Vec :: new ( ) ;
293+ }
294+ let gaps = natural. len ( ) . saturating_sub ( 1 ) as u16 ;
295+ let total: u16 = natural. iter ( ) . sum :: < u16 > ( ) + gaps;
296+ if avail >= total {
297+ return natural. to_vec ( ) ;
298+ }
299+ // Rounded division avoids a systematic downward bias; overshoot from
300+ // rounding or min-flooring is paid back by the shave pass below.
301+ let mut out: Vec < u16 > = natural
302+ . iter ( )
303+ . zip ( mins)
304+ . map ( |( & nat, & min) | {
305+ let scaled = ( nat as u32 * avail as u32 + total as u32 / 2 ) / total as u32 ;
306+ ( scaled as u16 ) . max ( min)
307+ } )
308+ . collect ( ) ;
309+ // Reclaim overshoot from columns still above their floor, largest
310+ // excess first, so the result fits whenever sum(mins) does.
311+ let mut sum: u16 = out. iter ( ) . sum ( ) ;
312+ while sum + gaps > avail {
313+ let Some ( i) = ( 0 ..out. len ( ) )
314+ . filter ( |& i| out[ i] > mins[ i] )
315+ . max_by_key ( |& i| out[ i] - mins[ i] )
316+ else {
317+ break ; // all at min: avail < sum(mins)+gaps, clipping unavoidable
318+ } ;
319+ out[ i] -= 1 ;
320+ sum -= 1 ;
321+ }
322+ out
323+ }
324+
325+ /// Natural per-column widths for the GPU table, scaled to fit `avail`
326+ /// (inner table width) by a common fraction, each floored at a minimum
327+ /// so labels stay legible before ratatui clips whole columns.
328+ fn gpu_col_widths ( avail : u16 ) -> [ u16 ; 12 ] {
329+ const BAR : u16 = super :: app:: BAR_WIDTH as u16 ;
330+ const NATURAL : [ u16 ; 12 ] = [ 9 , 16 , 16 , 16 , 5 , 6 , BAR , 7 , BAR , 7 , 6 , 5 ] ;
331+ const MIN : [ u16 ; 12 ] = [ 7 , 8 , 9 , 9 , 4 , 4 , 6 , 6 , 6 , 6 , 5 , 4 ] ;
332+ scaled_col_widths ( & NATURAL , & MIN , avail)
333+ . try_into ( )
334+ . expect ( "12 naturals in, 12 widths out" )
335+ }
336+
337+ /// Minimum width a scaled RDMA column may shrink to: bars stay 6 cells
338+ /// wide so the meter reads; everything else keeps 3/5 of its natural
339+ /// width (at least 4) so values stay legible.
340+ fn rdma_col_min ( col : & TableColumn ) -> u16 {
341+ match col {
342+ TableColumn :: TxBar | TableColumn :: RxBar => 6 ,
343+ _ => {
344+ // 3/5 of natural, floored at 4 cells; capped at natural so a
345+ // future column narrower than 4 can't get a min wider than itself.
346+ let n = col. width ( ) ;
347+ ( n * 3 / 5 ) . max ( 4 ) . min ( n)
348+ }
349+ }
350+ }
351+
352+ fn column_cell (
353+ col : & TableColumn ,
354+ t : & PortThroughput ,
355+ tc : & ThemeColors ,
356+ col_w : u16 ,
357+ ) -> Cell < ' static > {
286358 match col {
287359 TableColumn :: Device => Cell :: from ( t. dev_name . clone ( ) ) . style ( Style :: default ( ) . fg ( tc. fg ) ) ,
288360 TableColumn :: Port => {
289361 let port_text = t. port_label . clone ( ) . unwrap_or_else ( || t. port . to_string ( ) ) ;
290362 Cell :: from ( port_text) . style ( Style :: default ( ) . fg ( tc. muted ) )
291363 }
292- TableColumn :: TxBar => Cell :: from ( gbps_bar ( t. tx_gbps , t. link_gbps ) )
364+ TableColumn :: TxBar => Cell :: from ( gbps_bar ( t. tx_gbps , t. link_gbps , col_w as usize ) )
293365 . style ( Style :: default ( ) . fg ( throughput_color ( t. tx_gbps , t. link_gbps , tc) ) ) ,
294366 TableColumn :: TxGbps => Cell :: from ( format ! ( "{:.2}" , t. tx_gbps) )
295367 . style ( Style :: default ( ) . fg ( throughput_color ( t. tx_gbps , t. link_gbps , tc) ) ) ,
296- TableColumn :: RxBar => Cell :: from ( gbps_bar ( t. rx_gbps , t. link_gbps ) )
368+ TableColumn :: RxBar => Cell :: from ( gbps_bar ( t. rx_gbps , t. link_gbps , col_w as usize ) )
297369 . style ( Style :: default ( ) . fg ( throughput_color ( t. rx_gbps , t. link_gbps , tc) ) ) ,
298370 TableColumn :: RxGbps => Cell :: from ( format ! ( "{:.2}" , t. rx_gbps) )
299371 . style ( Style :: default ( ) . fg ( throughput_color ( t. rx_gbps , t. link_gbps , tc) ) ) ,
@@ -373,7 +445,7 @@ fn gpu_metrics(t: &PortThroughput) -> Option<&GpuMetrics> {
373445 nv. or_else ( || t. xgmi . as_ref ( ) . and_then ( |m| m. metrics . as_ref ( ) ) )
374446}
375447
376- fn gpu_row_cells ( t : & PortThroughput , tc : & ThemeColors ) -> Vec < Cell < ' static > > {
448+ fn gpu_row_cells ( t : & PortThroughput , tc : & ThemeColors , widths : & [ u16 ; 12 ] ) -> Vec < Cell < ' static > > {
377449 let m = gpu_metrics ( t) ;
378450 let util = m. and_then ( |m| m. util_pct ) ;
379451 let mem = m. and_then ( |m| match ( m. vram_used_mb , m. vram_total_mb ) {
@@ -382,12 +454,14 @@ fn gpu_row_cells(t: &PortThroughput, tc: &ThemeColors) -> Vec<Cell<'static>> {
382454 }
383455 _ => None ,
384456 } ) ;
385- let pct_cell = |p : Option < u32 > | {
457+ // Gauge bar fills its column minus the fixed " 100%" suffix (5 chars).
458+ let pct_cell = |p : Option < u32 > , col_w : u16 | {
386459 let color = match p {
387460 Some ( v) => capacity_color ( v as f64 / 100.0 , tc) ,
388461 None => tc. muted ,
389462 } ;
390- Cell :: from ( pct_gauge_text ( p) ) . style ( Style :: default ( ) . fg ( color) )
463+ let bar_w = ( col_w as usize ) . saturating_sub ( 5 ) . max ( 3 ) ;
464+ Cell :: from ( pct_gauge_text ( p, bar_w) ) . style ( Style :: default ( ) . fg ( color) )
391465 } ;
392466 let temp_cell = match m. and_then ( |m| m. temp_c ) {
393467 Some ( t) => Cell :: from ( format ! ( "{}C" , t) ) . style ( Style :: default ( ) . fg ( temp_color ( t, tc) ) ) ,
@@ -402,25 +476,19 @@ fn gpu_row_cells(t: &PortThroughput, tc: &ThemeColors) -> Vec<Cell<'static>> {
402476 vec ! [
403477 Cell :: from( t. dev_name. clone( ) ) . style( Style :: default ( ) . fg( tc. fg) ) ,
404478 Cell :: from( gpu_meta_name( t) ) . style( Style :: default ( ) . fg( tc. muted) ) ,
405- pct_cell( util) ,
406- pct_cell( mem) ,
479+ pct_cell( util, widths [ 2 ] ) ,
480+ pct_cell( mem, widths [ 3 ] ) ,
407481 temp_cell,
408482 pwr_cell,
409- Cell :: from( gbps_bar( t. tx_gbps, t. link_gbps) ) . style( Style :: default ( ) . fg( throughput_color(
410- t. tx_gbps,
411- t. link_gbps,
412- tc,
413- ) ) ) ,
483+ Cell :: from( gbps_bar( t. tx_gbps, t. link_gbps, widths[ 6 ] as usize ) )
484+ . style( Style :: default ( ) . fg( throughput_color( t. tx_gbps, t. link_gbps, tc) ) ) ,
414485 Cell :: from( format!( "{:.2}" , t. tx_gbps) ) . style( Style :: default ( ) . fg( throughput_color(
415486 t. tx_gbps,
416487 t. link_gbps,
417488 tc,
418489 ) ) ) ,
419- Cell :: from( gbps_bar( t. rx_gbps, t. link_gbps) ) . style( Style :: default ( ) . fg( throughput_color(
420- t. rx_gbps,
421- t. link_gbps,
422- tc,
423- ) ) ) ,
490+ Cell :: from( gbps_bar( t. rx_gbps, t. link_gbps, widths[ 8 ] as usize ) )
491+ . style( Style :: default ( ) . fg( throughput_color( t. rx_gbps, t. link_gbps, tc) ) ) ,
424492 Cell :: from( format!( "{:.2}" , t. rx_gbps) ) . style( Style :: default ( ) . fg( throughput_color(
425493 t. rx_gbps,
426494 t. link_gbps,
@@ -487,29 +555,17 @@ fn draw_gpu_table(frame: &mut Frame, app: &mut App, area: Rect, tc: &ThemeColors
487555 )
488556 . height ( 1 ) ;
489557
558+ // Block borders (2) and the "▶ " highlight column (2) shrink what the
559+ // Table layout actually distributes among columns; scale to fit that.
560+ let avail = area. width . saturating_sub ( 2 + 2 ) ;
561+ let widths = gpu_col_widths ( avail) ;
562+
490563 let rows: Vec < Row > = display
491564 . iter ( )
492- . map ( |t| Row :: new ( gpu_row_cells ( t, tc) ) )
565+ . map ( |t| Row :: new ( gpu_row_cells ( t, tc, & widths ) ) )
493566 . collect ( ) ;
494567
495- // Util/Mem hold a 10-char gauge + " 100%" (15 chars); bars reuse
496- // BAR_WIDTH so the TX/RX meters match the RDMA tab visually.
497- let widths = [
498- Constraint :: Length ( 9 ) ,
499- Constraint :: Length ( 16 ) ,
500- Constraint :: Length ( 16 ) ,
501- Constraint :: Length ( 16 ) ,
502- Constraint :: Length ( 5 ) ,
503- Constraint :: Length ( 6 ) ,
504- Constraint :: Length ( BAR_WIDTH as u16 ) ,
505- Constraint :: Length ( 7 ) ,
506- Constraint :: Length ( BAR_WIDTH as u16 ) ,
507- Constraint :: Length ( 7 ) ,
508- Constraint :: Length ( 6 ) ,
509- Constraint :: Length ( 5 ) ,
510- ] ;
511-
512- let table = Table :: new ( rows, widths)
568+ let table = Table :: new ( rows, widths. map ( Constraint :: Length ) )
513569 . header ( header)
514570 . block (
515571 Block :: default ( )
@@ -633,22 +689,28 @@ fn draw_table(frame: &mut Frame, app: &mut App, area: Rect, tc: &ThemeColors) {
633689 )
634690 . height ( 1 ) ;
635691
692+ // Scale the rendered set to fit, like the GPU table. Normal-mode
693+ // windowing already fits naturals, so scaling only bites when it
694+ // doesn't (detail mode's fixed default set, or a too-narrow pane).
695+ let natural: Vec < u16 > = cols_to_render. iter ( ) . map ( |c| c. width ( ) ) . collect ( ) ;
696+ let mins: Vec < u16 > = cols_to_render. iter ( ) . map ( |c| rdma_col_min ( c) ) . collect ( ) ;
697+ // Block borders (2) and the highlight column (2), as in draw_gpu_table.
698+ let col_widths = scaled_col_widths ( & natural, & mins, area. width . saturating_sub ( 2 + 2 ) ) ;
699+
636700 let rows: Vec < Row > = display
637701 . iter ( )
638702 . map ( |t| {
639703 Row :: new (
640704 cols_to_render
641705 . iter ( )
642- . map ( |c| column_cell ( c, t, tc) )
706+ . zip ( & col_widths)
707+ . map ( |( c, & w) | column_cell ( c, t, tc, w) )
643708 . collect :: < Vec < _ > > ( ) ,
644709 )
645710 } )
646711 . collect ( ) ;
647712
648- let widths: Vec < Constraint > = cols_to_render
649- . iter ( )
650- . map ( |c| Constraint :: Length ( c. width ( ) ) )
651- . collect ( ) ;
713+ let widths: Vec < Constraint > = col_widths. iter ( ) . map ( |& w| Constraint :: Length ( w) ) . collect ( ) ;
652714
653715 let table = Table :: new ( rows, widths)
654716 . header ( header)
@@ -2436,14 +2498,54 @@ mod gauge_tests {
24362498
24372499 #[ test]
24382500 fn pct_gauge_text_formats_percent_with_bar ( ) {
2439- let text = pct_gauge_text ( Some ( 87 ) ) ;
2501+ let text = pct_gauge_text ( Some ( 87 ) , 10 ) ;
24402502 assert ! ( text. contains( "87%" ) , "{text:?}" ) ;
24412503 let ( fill, _) = crate :: tui:: glyphs:: meter ( ) ;
24422504 assert ! ( text. contains( fill) , "expected bar glyph in {text:?}" ) ;
24432505 }
24442506
24452507 #[ test]
24462508 fn pct_gauge_text_none_is_dash ( ) {
2447- assert_eq ! ( pct_gauge_text( None ) , "-" ) ;
2509+ assert_eq ! ( pct_gauge_text( None , 10 ) , "-" ) ;
2510+ }
2511+
2512+ #[ test]
2513+ fn gpu_col_widths_wide_returns_natural ( ) {
2514+ const BAR : u16 = crate :: tui:: app:: BAR_WIDTH as u16 ;
2515+ assert_eq ! (
2516+ gpu_col_widths( 200 ) ,
2517+ [ 9 , 16 , 16 , 16 , 5 , 6 , BAR , 7 , BAR , 7 , 6 , 5 ]
2518+ ) ;
2519+ }
2520+
2521+ #[ test]
2522+ fn gpu_col_widths_narrow_fits_avail_and_respects_minimums ( ) {
2523+ const MIN : [ u16 ; 12 ] = [ 7 , 8 , 9 , 9 , 4 , 4 , 6 , 6 , 6 , 6 , 5 , 4 ] ;
2524+ let widths = gpu_col_widths ( 100 ) ;
2525+ let total: u16 = widths. iter ( ) . sum :: < u16 > ( ) + 11 ; // 11 column gaps
2526+ assert ! ( total <= 100 , "scaled widths too wide: {total}" ) ;
2527+ for ( i, ( & w, & min) ) in widths. iter ( ) . zip ( MIN . iter ( ) ) . enumerate ( ) {
2528+ assert ! ( w >= min, "col {i}: width {w} below min {min}" ) ;
2529+ }
2530+ }
2531+
2532+ #[ test]
2533+ fn gpu_col_widths_below_min_floor_pins_every_column_at_min ( ) {
2534+ const MIN : [ u16 ; 12 ] = [ 7 , 8 , 9 , 9 , 4 , 4 , 6 , 6 , 6 , 6 , 5 , 4 ] ;
2535+ // sum(MIN) + 11 gaps = 85 > 80: nothing left to shave, all at min.
2536+ assert_eq ! ( gpu_col_widths( 80 ) , MIN ) ;
2537+ }
2538+
2539+ #[ test]
2540+ fn scaled_col_widths_naturals_when_wide_fits_when_narrow ( ) {
2541+ let natural = [ 10u16 , 20 , 6 ] ;
2542+ let mins = [ 4u16 , 8 , 3 ] ;
2543+ assert_eq ! ( scaled_col_widths( & natural, & mins, 100 ) , natural) ;
2544+ let scaled = scaled_col_widths ( & natural, & mins, 24 ) ;
2545+ let total: u16 = scaled. iter ( ) . sum :: < u16 > ( ) + 2 ; // 2 column gaps
2546+ assert ! ( total <= 24 , "scaled widths too wide: {total}" ) ;
2547+ for ( i, ( & w, & min) ) in scaled. iter ( ) . zip ( mins. iter ( ) ) . enumerate ( ) {
2548+ assert ! ( w >= min, "col {i}: width {w} below min {min}" ) ;
2549+ }
24482550 }
24492551}
0 commit comments