@@ -1059,78 +1059,81 @@ impl WgpuState {
10591059 bg_verts,
10601060 bg_idx,
10611061 ) ;
1062- add_string_verts (
1063- " N " ,
1064- 0.0 ,
1065- py,
1062+ // UI/UX v3 N-7c: the status bar is chrome, so its widths are measured.
1063+ // Every zone below used to advance by a character count times the cell
1064+ // width — a *character* count against a *display-width* advance, which
1065+ // are the same number only in Latin. `status_bar_text` comes from a
1066+ // Lua widget, so it is arbitrary user text; a Japanese one overlapped
1067+ // the zone beside it on the left and slid off the screen on the right.
1068+ let ramp = nexterm_config:: MetricTokens :: default ( ) . type_ramp ;
1069+ let ( prose_style, badge_style) = ( ramp. body , ramp. body_strong ) ;
1070+ let ( _, prose_line_h, _) = font. chrome_metrics ( & prose_style) ;
1071+ let ( _, badge_line_h, _) = font. chrome_metrics ( & badge_style) ;
1072+ let prose_y = py + ( cell_h - prose_line_h) * 0.5 ;
1073+ let badge_y = py + ( cell_h - badge_line_h) * 0.5 ;
1074+
1075+ // The "N" is centred in the icon zone rather than padded to it with
1076+ // spaces; a space is text, and it centres nothing once measured.
1077+ let n_w = measure_run ( "N" , & badge_style, font) ;
1078+ add_run_verts (
1079+ "N" ,
1080+ & badge_style,
1081+ ( icon_zone_w - n_w) * 0.5 ,
1082+ badge_y,
10661083 tokens. text_on_accent ,
1067- true ,
10681084 sw,
10691085 sh,
1070- cell_w,
10711086 font,
10721087 atlas,
10731088 & self . queue ,
10741089 text_verts,
10751090 text_idx,
10761091 ) ;
10771092
1078- // Zone 3: pane info text, starting just after the icon zone.
1079- let pane_id = state. focused_pane_id . unwrap_or ( 0 ) ;
1080- let activity_ids = state. active_pane_ids ( ) ;
1081- let pane_count = state. pane_layouts . len ( ) ;
1082- let info = if activity_ids. is_empty ( ) {
1083- format ! ( " nexterm │ pane {}/{}" , pane_id, pane_count)
1084- } else {
1085- let ids: Vec < String > = activity_ids. iter ( ) . map ( |id| id. to_string ( ) ) . collect ( ) ;
1086- format ! (
1087- " nexterm │ pane {}/{} │ ●{}" ,
1088- pane_id,
1089- pane_count,
1090- ids. join( "," )
1091- )
1092- } ;
1093- add_string_verts (
1094- & info,
1095- icon_zone_w,
1096- py,
1097- tokens. text_on ( SurfaceLevel :: S1 ) . secondary ,
1098- false ,
1099- sw,
1100- sh,
1101- cell_w,
1102- font,
1103- atlas,
1104- & self . queue ,
1105- text_verts,
1106- text_idx,
1107- ) ;
1108-
1109- // Zone 4: left widget (status_bar_text), rendered after the info block.
1110- if !state. status_bar_text . is_empty ( ) {
1111- let info_w = ( 1 + info. chars ( ) . count ( ) ) as f32 * cell_w;
1112- let left_x = icon_zone_w + info_w;
1113- let left_text = format ! ( "│ {} " , state. status_bar_text) ;
1114- add_string_verts (
1115- & left_text,
1116- left_x,
1117- py,
1118- tokens. text_on ( SurfaceLevel :: S1 ) . muted ,
1119- false ,
1093+ // Zone 5 (right edge) is built first, because its total width is the
1094+ // budget the left side has to live within. It used to be drawn last
1095+ // and the left side had no budget at all, so the two simply ran into
1096+ // each other on a narrow window.
1097+ //
1098+ // Each indicator measures, claims its width off the right edge, then
1099+ // draws at `sw - right_offset` — one shape, four times, so a fifth
1100+ // indicator cannot invent a fifth way of stacking.
1101+ let mut right_offset = 0.0f32 ;
1102+ let mut right_run = |text : & str ,
1103+ style : & nexterm_config:: TypeStyle ,
1104+ y : f32 ,
1105+ fg : [ f32 ; 4 ] ,
1106+ font : & mut FontManager ,
1107+ atlas : & mut GlyphAtlas ,
1108+ text_verts : & mut Vec < TextVertex > ,
1109+ text_idx : & mut Vec < u16 > | {
1110+ right_offset += measure_run ( text, style, font) ;
1111+ add_run_verts (
1112+ text,
1113+ style,
1114+ sw - right_offset,
1115+ y,
1116+ fg,
11201117 sw,
11211118 sh,
1122- cell_w,
11231119 font,
11241120 atlas,
11251121 & self . queue ,
11261122 text_verts,
11271123 text_idx,
11281124 ) ;
1129- }
1125+ } ;
11301126
1131- // Zone 5 (right edge): right widget, stacked indicators.
1132- // Source: prefer status_bar_right_text, fall back to status_bar_text when
1133- // status_bar_text is not also being shown on the left.
1127+ // The call order below is the stacking order, outermost first: each
1128+ // indicator claims its width off the edge and the next one lands to
1129+ // its left. It reproduces what the cell path drew — widget at the
1130+ // edge, then zoom, copy mode, and the scroll position furthest in.
1131+ // That order used to be an accident of which blocks incremented
1132+ // `right_offset`; the scroll indicator was the one that never did, so
1133+ // everything else stacked inside it. It is the call order now.
1134+
1135+ // Right widget. Source: prefer status_bar_right_text, fall back to
1136+ // status_bar_text when status_bar_text is not also shown on the left.
11341137 let right_widget_src = if !state. status_bar_right_text . is_empty ( ) {
11351138 & state. status_bar_right_text
11361139 } else if state. status_bar_text . is_empty ( ) {
@@ -1139,47 +1142,29 @@ impl WgpuState {
11391142 // status_bar_text is already shown on the left; don't duplicate on the right.
11401143 ""
11411144 } ;
1142- let right_widget_src = right_widget_src. to_owned ( ) ;
1143- let mut right_offset = 0.0f32 ;
11441145 if !right_widget_src. is_empty ( ) {
11451146 let widget_text = format ! ( " {} " , right_widget_src) ;
1146- let text_w = widget_text. chars ( ) . count ( ) as f32 * cell_w;
1147- right_offset = text_w;
1148- let right_px = sw - text_w;
1149- add_string_verts (
1147+ right_run (
11501148 & widget_text,
1151- right_px ,
1152- py ,
1149+ & prose_style ,
1150+ prose_y ,
11531151 tokens. accent_muted ,
1154- false ,
1155- sw,
1156- sh,
1157- cell_w,
11581152 font,
11591153 atlas,
1160- & self . queue ,
11611154 text_verts,
11621155 text_idx,
11631156 ) ;
11641157 }
11651158
11661159 // Zoom indicator — semantic_warning colour.
11671160 if state. is_zoomed {
1168- let zoom_text = " [Z] " ;
1169- right_offset += zoom_text. chars ( ) . count ( ) as f32 * cell_w;
1170- let right_px = sw - right_offset;
1171- add_string_verts (
1172- zoom_text,
1173- right_px,
1174- py,
1161+ right_run (
1162+ " [Z] " ,
1163+ & badge_style,
1164+ badge_y,
11751165 tokens. semantic_warning ,
1176- true ,
1177- sw,
1178- sh,
1179- cell_w,
11801166 font,
11811167 atlas,
1182- & self . queue ,
11831168 text_verts,
11841169 text_idx,
11851170 ) ;
@@ -1193,20 +1178,13 @@ impl WgpuState {
11931178 ViMode :: Visual => " VISUAL " ,
11941179 ViMode :: VisualLine => " V-LINE " ,
11951180 } ;
1196- right_offset += mode_label. chars ( ) . count ( ) as f32 * cell_w;
1197- let right_px = sw - right_offset;
1198- add_string_verts (
1181+ right_run (
11991182 mode_label,
1200- right_px ,
1201- py ,
1183+ & badge_style ,
1184+ badge_y ,
12021185 tokens. accent_primary ,
1203- true ,
1204- sw,
1205- sh,
1206- cell_w,
12071186 font,
12081187 atlas,
1209- & self . queue ,
12101188 text_verts,
12111189 text_idx,
12121190 ) ;
@@ -1217,16 +1195,70 @@ impl WgpuState {
12171195 && pane. scroll_offset > 0
12181196 {
12191197 let scroll_text = format ! ( " ↑{} " , pane. scroll_offset) ;
1220- let right_px = sw - scroll_text. chars ( ) . count ( ) as f32 * cell_w - right_offset;
1221- add_string_verts (
1198+ right_run (
12221199 & scroll_text,
1223- right_px ,
1224- py ,
1200+ & badge_style ,
1201+ badge_y ,
12251202 tokens. semantic_warning ,
1226- true ,
1203+ font,
1204+ atlas,
1205+ text_verts,
1206+ text_idx,
1207+ ) ;
1208+ }
1209+
1210+ // Zone 3: pane info text, starting just after the icon zone. What is
1211+ // left of the bar after the right edge has claimed its share is the
1212+ // budget for this and Zone 4 together.
1213+ let mut left_x = icon_zone_w;
1214+ let mut left_budget = ( sw - icon_zone_w - right_offset - cell_w) . max ( 0.0 ) ;
1215+ let pane_id = state. focused_pane_id . unwrap_or ( 0 ) ;
1216+ let activity_ids = state. active_pane_ids ( ) ;
1217+ let pane_count = state. pane_layouts . len ( ) ;
1218+ let info = if activity_ids. is_empty ( ) {
1219+ format ! ( " nexterm │ pane {}/{}" , pane_id, pane_count)
1220+ } else {
1221+ let ids: Vec < String > = activity_ids. iter ( ) . map ( |id| id. to_string ( ) ) . collect ( ) ;
1222+ format ! (
1223+ " nexterm │ pane {}/{} │ ●{}" ,
1224+ pane_id,
1225+ pane_count,
1226+ ids. join( "," )
1227+ )
1228+ } ;
1229+ let info = truncate_run_to_width ( & info, & prose_style, left_budget, font) ;
1230+ let info_w = add_run_verts (
1231+ & info,
1232+ & prose_style,
1233+ left_x,
1234+ prose_y,
1235+ tokens. text_on ( SurfaceLevel :: S1 ) . secondary ,
1236+ sw,
1237+ sh,
1238+ font,
1239+ atlas,
1240+ & self . queue ,
1241+ text_verts,
1242+ text_idx,
1243+ ) ;
1244+ left_x += info_w;
1245+ left_budget -= info_w;
1246+
1247+ // Zone 4: left widget (status_bar_text), rendered after the info
1248+ // block. Its x is the width the info block actually consumed, taken
1249+ // from the drawing call itself, so there is no second width formula
1250+ // that could disagree with what was drawn.
1251+ if !state. status_bar_text . is_empty ( ) {
1252+ let left_text = format ! ( "│ {} " , state. status_bar_text) ;
1253+ let left_text = truncate_run_to_width ( & left_text, & prose_style, left_budget, font) ;
1254+ add_run_verts (
1255+ & left_text,
1256+ & prose_style,
1257+ left_x,
1258+ prose_y,
1259+ tokens. text_on ( SurfaceLevel :: S1 ) . muted ,
12271260 sw,
12281261 sh,
1229- cell_w,
12301262 font,
12311263 atlas,
12321264 & self . queue ,
@@ -1779,4 +1811,32 @@ mod cell_path_gate_tests {
17791811 "the InfoBar places text by a byte count again; use measure_run (N-7a)"
17801812 ) ;
17811813 }
1814+
1815+ /// UI/UX v3 N-7c: no zone of the status bar advances by a character count.
1816+ ///
1817+ /// Every zone used to: the info block handed Zone 4 its `x` as
1818+ /// `(1 + info.chars().count()) * cell_w`, and the three right-edge
1819+ /// indicators stacked by adding the same product to `right_offset`. A
1820+ /// character count and a display-width advance are the same number only
1821+ /// in Latin, and `status_bar_text` comes from a Lua widget — arbitrary
1822+ /// user text, Japanese as often as not.
1823+ #[ test]
1824+ fn the_status_bar_measures_every_zone ( ) {
1825+ let body = builder_body ( "build_status_verts" ) ;
1826+ assert ! (
1827+ body. contains( "measure_run" ) && body. contains( "add_run_verts" ) ,
1828+ "builder_body did not return the status-bar builder; the gates \
1829+ below would pass vacuously"
1830+ ) ;
1831+ assert ! (
1832+ !body. contains( "add_string_verts" ) ,
1833+ "the status bar draws text on the cell path again (N-7c)"
1834+ ) ;
1835+ assert ! (
1836+ !body. contains( "chars().count()" ) ,
1837+ "a status-bar zone advances by a character count again; widths \
1838+ here come from measure_run or from what add_run_verts reports it \
1839+ drew (N-7c)"
1840+ ) ;
1841+ }
17821842}
0 commit comments