ek-ui 0.2.9

ek 生态常驻主界面(egui/eframe):插件即界面——观测页 + 日志页,车头与 UI 同插件
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
//! 唯一渲染入口(L2):真实 eframe App 与 kittest 建构走同一个 `render_body`。
//!
//! 布局 v5(BD-UI-29 面板化,官方面板组合;蓝本 = Python 主窗 mainwindow.py):
//! **TopBottomPanel(菜单) → TopBottomPanel(行情带) → SidePanel(下单侧) →
//! CentralPanel(2×2 监控格) → Window 弹窗**,两带物理分离。
//! - 行情带(TopBottomPanel::resizable = Python 分带 splitter 语义,缺省 240):
//!   交易所二级 tab(只列有 tick 的所)+ tick 表(唯一源 = `eTick.`
//!   订阅流,不从 bar 回);行内任意格单击 = 带入下单面板(ek 增值,方案钉;
//!   盘口四列带方向:买1 价/量 → 卖,卖1 价/量 → 买,价格列顺带填价,其余列
//!   方向保持默认——0.2.1 `import_tick`),行内任意格双击 = 弹 K线窗。
//! - 下单侧(SidePanel::resizable,缺省 250):下单面板常驻(Python
//!   TradingWidget 等价:9 字段 + 委托 + 全撤;表单 = 网格,按钮/提示语都是
//!   网格行;网格后置 err label 由 ScrollArea 吸收 sizing 退让,BD-UI-27)+
//!   盘口(买5卖5,Python TradingWidget 内序:字段钮 → 盘口)。
//! - 2×2 监控格(CentralPanel 内,响应式分配,无定宽定高,BD-UI-29)
//!   「委托|未成交」「成交」「持仓」「资金|日志」(撤单钮仅活动行,既有 s11
//!   钉口径;委托行双击 = 撤单,Python OrderMonitor 等价;格内宽表走横向滚动,
//!   D7 口径)。
//! - 弹窗两式(BD-UI-32):运维窗(菜单「运维」:插件/服务/依赖/流控查询)、
//!   CTP 配置窗(菜单「网关」,CTP 插件自己的界面)= egui::Window 依附主窗;
//!   查询合约窗(菜单「查询合约」:查询 + 勾选 + 订阅钮,Python ContractManager
//!   等价)、K线窗(双击弹出,按合约多开)= 独立 OS 窗(show_viewport_immediate,
//!   原生最小化/最大化/关闭,可拖出主窗;后端不支持多窗自动回退 egui::Window,
//!   kittest 走回退面断言)。
//! - 订阅两入口(用户裁定,网关窗不定死订阅):下单面板代码框**回车**订阅
//!   (Python returnPressed 等价);查询合约窗**勾选订阅**。合约数据 =
//!   `eContract.` 自动回流(ek-ctp 登录后自动查询,UI 只收不催)。
//! - 回灌表单与票据文本框已退役(界面面);总线口(`UiCmd::Reload` 等)保留。
//!
//! kittest 断言面:各单元格独立 Label(`get_by_label` 直查);操作走
//! `push_cmd` 入队(ui 插件 eTimer 辞行出队),确认状态就地翻位,零自制通道。
//! 隔离红标:行状态含 Quarantin(快照扫描,BD-UI-17)。

use ek_data::constant::{Direction, Offset, OrderType};
use ek_station::wire::PlugRow;

use crate::state::{
    BottomRightTab, CellView, ConfirmStep, TopLeftTab, UiCmd, UiShared, is_quarantined,
};

/// 清空日志按钮的唯一可访问性标签(测试与 UI 同源)。
pub const BTN_CLEAR_LOGS: &str = "清空日志";
/// 取消待确认按钮标签。
pub const BTN_CANCEL_CONFIRM: &str = "取消确认";
/// 下单提交按钮标签(v4 = Python「委托」钮)。
pub const BTN_SEND_ORDER: &str = "委托";
/// 全撤按钮标签(v4;活动委托逐个走既有撤单 ask)。
pub const BTN_CANCEL_ALL: &str = "全撤";
/// 流控查询按钮标签(S5;v4 收纳进运维窗)。
pub const BTN_QUERY_RATE: &str = "流控查询";
/// 撤单按钮前缀(S5;行级标签 = `撤单 {orderid}`)。
pub const BTN_CANCEL_ORDER: &str = "撤单";
/// CTP 连接按钮标签(S6;手工配置 → ask ctp.configure)。
pub const BTN_CTP_CONNECT: &str = "连接 CTP";
/// 菜单项:运维(弹运维窗)。
pub const MENU_OPS: &str = "运维";
/// 菜单项:网关(弹 CTP 配置窗)。
pub const MENU_CTP: &str = "网关";
/// 菜单项:帮助。
pub const MENU_HELP: &str = "帮助";
/// 菜单项:查询合约(弹查询合约窗;Python ContractManager 等价)。
pub const MENU_CONTRACTS: &str = "查询合约";
/// 菜单项 → 弹窗入口钮(BD-UI-31 一键直达:菜单钮即入口,一步弹窗,
/// 不再挂下拉层;入口钮标签 = 菜单名本身)。
/// 查询合约窗订阅钮(勾选集 → ask ctp.subscribe)。
pub const BTN_SUBSCRIBE_SELECTED: &str = "订阅选中";

fn remove_button_label(name: &str, armed_on: Option<&str>) -> String {
    if armed_on == Some(name) {
        format!("确认移除 {name} ?")
    } else {
        format!("移除 {name}")
    }
}

fn state_text(r: &PlugRow) -> String {
    r.state.clone()
}

/// DtNs → HH:MM:SS(UTC;表内时间列统一口径)。
fn fmt_dt(dt: Option<i64>) -> String {
    dt.map(|ns| {
        chrono::DateTime::<chrono::Utc>::from_timestamp_nanos(ns)
            .format("%H:%M:%S")
            .to_string()
    })
    .unwrap_or_else(|| "-".to_string())
}

// ---- BD-UI-31(R3)行高亮:悬停淡底 / 选中跳底 ----

/// 行底色绘制:行内各格响应矩形并集 → 选中 = 跳底(蓝调);否则悬停 = 淡灰底。
/// 半透明叠底(行文字先画,叠后仍可读);行交互钮单击选中与双击动作并存
/// (egui 双击先经 clicked,选中顺带发生,无冲突)。
fn paint_row_bg(ui: &egui::Ui, cell_rects: &[egui::Rect], selected: bool) {
    let Some(union) = cell_rects.iter().copied().reduce(|a, b| a.union(b)) else {
        return;
    };
    let hovered = ui
        .input(|i| i.pointer.interact_pos())
        .is_some_and(|p| union.expand(3.0).contains(p));
    if selected {
        ui.painter().rect_filled(
            union.expand(3.0),
            3.0,
            egui::Color32::from_rgba_unmultiplied(64, 108, 180, 82),
        );
    } else if hovered {
        ui.painter().rect_filled(
            union.expand(3.0),
            3.0,
            egui::Color32::from_rgba_unmultiplied(128, 128, 128, 38),
        );
    }
}

/// 委托是否可撤(与 ek-data ACTIVE_STATUSES 同口径)。
fn order_active(status: ek_data::constant::Status) -> bool {
    ek_data::constant::ACTIVE_STATUSES.contains(&status)
}

/// 格可点化(BD-UI-31 扩展):标签外观不变,在标签矩形上叠一层点击感应 ——
/// 行内任何一格都可单击/双击(用户裁定:动作归行,不归某一列)。
fn click_cell(ui: &egui::Ui, rect: egui::Rect, id_salt: impl std::hash::Hash) -> egui::Response {
    ui.interact(rect, ui.id().with(id_salt), egui::Sense::click())
}

/// 行情带入(0.2.1 扩展):任意列单击 = 带入合约+交易所;盘口四列带方向
/// (买1价/买1量 → 卖,卖1价/卖1量 → 买 —— 吃对手挂价口径),两个价格列
/// 顺带把该价填进价格格;其余列方向/价格保持表单默认(用户裁定)。
pub fn import_tick(shared: &UiShared, t: &ek_data::tick::TickData, exchange: &str, col: usize) {
    let mut f = shared.order_form();
    f.symbol = t.symbol.clone();
    f.exchange = exchange.to_string();
    match col {
        8 => {
            f.direction = Direction::SHORT;
            f.price = format!("{:.1}", t.bid_price_1);
        }
        9 => f.direction = Direction::SHORT,
        10 => {
            f.direction = Direction::LONG;
            f.price = format!("{:.1}", t.ask_price_1);
        }
        11 => f.direction = Direction::LONG,
        _ => {}
    }
    shared.set_order_form(f);
}

/// 按合约找最新 tick(下单面板名称/盘口/随价共用)。
/// 交易所非空 = 精确 EX.symbol 匹配(跨所同码不串);空 = 仅按代码找。
fn tick_of(shared: &UiShared, exchange: &str, symbol: &str) -> Option<ek_data::tick::TickData> {
    if symbol.is_empty() {
        return None;
    }
    shared.tick_rows().into_iter().find(|t| {
        t.symbol == symbol
            && (exchange.is_empty() || t.exchange.as_str().eq_ignore_ascii_case(exchange))
    })
}

/// 全局字号(BD-UI-32 R1,用户实看裁定「整体字偏小」,对照 Python 版):
/// body/button/strong 16px、small 14px、heading 22px、monospace 15px
/// (egui 缺省 13/12/18/12)。render_body 顶部统一施加,真实 App 与 kittest 同源。
pub fn apply_ui_style(ctx: &egui::Context) {
    apply_ui_style_for(ctx, crate::state::Theme::default());
}

/// 字号 + 主题视觉(BD-UI-34,用户问「PySide6 式可换主题」):主题 = UiShared
/// 运行期态(菜单钮一键循环),视觉在此统一施加——真实 App、独立子窗
/// (immediate 共享同一 Context)、kittest 三处同源。语义色(多红/空绿/
/// 隔离红/选中蓝)不在此列:渲染侧恒定直绘,跨主题不漂移(金融语义)。
pub fn apply_ui_style_for(ctx: &egui::Context, theme: crate::state::Theme) {
    ctx.style_mut(|style| {
        style
            .text_styles
            .insert(egui::TextStyle::Body, egui::FontId::proportional(16.0));
        style
            .text_styles
            .insert(egui::TextStyle::Button, egui::FontId::proportional(16.0));
        style
            .text_styles
            .insert(egui::TextStyle::Small, egui::FontId::proportional(14.0));
        style
            .text_styles
            .insert(egui::TextStyle::Heading, egui::FontId::proportional(22.0));
        style
            .text_styles
            .insert(egui::TextStyle::Monospace, egui::FontId::monospace(15.0));
    });
    ctx.set_visuals(theme_visuals(theme));
}

/// 四套主题视觉(BD-UI-34)。金融深色 = egui dark 骨架换深海军蓝底
/// (交易终端观感,对照 Python 版深色窗);Qt 灰白 = PySide6 Fusion 迁移用。
fn theme_visuals(t: crate::state::Theme) -> egui::Visuals {
    use egui::Color32;
    match t {
        crate::state::Theme::EguiDark => egui::Visuals::dark(),
        crate::state::Theme::EguiLight => egui::Visuals::light(),
        crate::state::Theme::QtClassic => {
            let mut v = egui::Visuals::light();
            v.window_fill = Color32::from_rgb(0xF6, 0xF6, 0xF6);
            v.panel_fill = Color32::from_rgb(0xEF, 0xEF, 0xEF);
            v.extreme_bg_color = Color32::from_rgb(0xFF, 0xFF, 0xFF);
            v.faint_bg_color = Color32::from_rgb(0xE4, 0xE4, 0xE4);
            v.selection.bg_fill = Color32::from_rgb(0xC8, 0xDC, 0xF0);
            v
        }
        crate::state::Theme::FinanceDark => {
            let mut v = egui::Visuals::dark();
            v.window_fill = Color32::from_rgb(0x18, 0x20, 0x2C);
            v.panel_fill = Color32::from_rgb(0x14, 0x1A, 0x24);
            v.extreme_bg_color = Color32::from_rgb(0x0C, 0x11, 0x18);
            v.faint_bg_color = Color32::from_rgb(0x10, 0x15, 0x1E);
            v.selection.bg_fill = Color32::from_rgb(0x2A, 0x44, 0x68);
            v.hyperlink_color = Color32::from_rgb(0x6A, 0x9C, 0xD8);
            v.widgets.noninteractive.bg_stroke =
                egui::Stroke::new(1.0, Color32::from_rgb(0x3A, 0x46, 0x58));
            v
        }
    }
}

/// 画一窗(官方面板组合:render_body 直接开顶层面板,不自嵌 CentralPanel)。
///
/// v5 框架(BD-UI-29 面板化):TopBottomPanel(菜单) → TopBottomPanel(行情带,
/// resizable = Python 分带 splitter 语义) → SidePanel(下单面板 + 盘口,
/// resizable) → CentralPanel(2×2 监控格,响应式分配,必须最后) → 弹窗。
/// 官方纪律(panel.rs 模块文档):顶层面板不得互相嵌套、CentralPanel 最后加、
/// Window 挂在所有面板之后。
pub fn render_body(ctx: &egui::Context, shared: &UiShared) {
    apply_ui_style_for(ctx, shared.theme());
    // ---- 菜单栏(顶层面板,最先 = 最外) ----
    egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
        render_menu_bar(ui, shared);
    });

    // ---- 上带:行情带(全宽独占;可拖分隔条,缺省 240 ≈ Python 30:70 分带) ----
    egui::TopBottomPanel::top("market_band")
        .resizable(true)
        .default_height(240.0)
        .show(ctx, |ui| {
            // 外层 ScrollArea auto_shrink(false) = resizable 面板吃满前提(BD-UI-27
            // 口径;s3 实锤:撤掉后空态带塌成一行,2×2 行位全漂移)。内层
            // pinned_table 自己会收缩适配 → 外层滚动条永不出现。
            egui::ScrollArea::both()
                .id_salt("market_band_scroll")
                .auto_shrink([false, false])
                .show(ui, |ui| {
                    render_market_band(ui, shared);
                });
        });

    // ---- 下带左侧:下单面板 + 盘口(Python TradingWidget 内序:字段钮 → 盘口) ----
    egui::SidePanel::left("order_side")
        .resizable(true)
        .default_width(250.0)
        .show(ctx, |ui| {
            // ScrollArea 吸收 sizing 退让(BD-UI-27):网格后置 err label 不再叠回网格
            egui::ScrollArea::vertical()
                .id_salt("order_side_scroll")
                .auto_shrink([false, false])
                .show(ui, |ui| {
                    render_order_panel(ui, shared);
                    ui.separator();
                    render_book_frame(ui, shared);
                });
        });

    // ---- 下带右侧:2×2 监控格(CentralPanel 最后;响应式分配,无定宽定高) ----
    egui::CentralPanel::default().show(ctx, |ui| {
        let avail = ui.available_size();
        let sp = ui.spacing().item_spacing;
        // 机制钉(dbg 实锤):horizontal 行成员的可用高度初始只有一行高
        // (interact_size),高度分配不沿横向向下传播 → 定高 max_height 失效、
        // 行按内容收缩(v4 死区根因)。官方解法 = 先精确预留矩形(group_frame:
        // allocate_exact_size + new_child;add_sized 收 impl Widget 不适用),
        // Frame+ScrollArea 在预留矩形内才吃满。
        let gw = ((avail.x - sp.x) / 2.0).max(220.0);
        let gh = ((avail.y - sp.y) / 2.0).max(120.0);
        ui.horizontal(|ui| {
            group_frame_plain(ui, "grp_topleft", gw, gh, |ui| {
                render_topleft_group(ui, shared);
            });
            group_frame_plain(ui, "grp_trades", gw, gh, |ui| {
                render_trades_group(ui, shared);
            });
        });
        ui.horizontal(|ui| {
            group_frame_plain(ui, "grp_positions", gw, gh, |ui| {
                render_positions_group(ui, shared);
            });
            // 资金/日志格:保留带填充 ScrollArea 的容器(0.2.6 钉):此格无
            // pinned_table,plain Frame 会收缩到内容宽(实测 810..1007)→ 右边框
            // 落在行中段 = 又一根「多出来的竖线」。外层 ScrollArea
            // auto_shrink(false) 让 Frame 吃满 gw,边界回到格位。
            group_frame(ui, "grp_bottomright", gw, gh, |ui| {
                render_bottomright_group(ui, shared);
            });
        });
    });

    // ---- 弹窗面(官方口径:Window 在顶层面板之后) ----
    render_windows(ctx, shared);
}

/// 2×2 格统一容器(响应式尺寸 + 双向滚动,BD-UI-27 可达性口径延续)。
/// 尺寸钉(BD-UI-29):`allocate_exact_size` 精确预留 w×h 矩形 + `new_child`
/// 在预留矩形内开 Frame+ScrollArea(dbg 实锤:horizontal 行成员的可用高度
/// 初始只有一行高,高度分配不沿横向向下传播 → 定高 max_height 失效、行按
/// 内容收缩 = v4 死区根因;先预留矩形,可用空间才吃满)。
/// 布局泄漏钉(第五轮审计):Frame::show 继承父级布局 → 格内内容强制
/// `ui.vertical`(短行成行,宽网格末位溢出走横向滚动)。
fn group_frame(ui: &mut egui::Ui, id: &str, w: f32, h: f32, content: impl FnOnce(&mut egui::Ui)) {
    let (rect, _resp) = ui.allocate_exact_size(egui::vec2(w, h), egui::Sense::hover());
    let mut child = ui.new_child(egui::UiBuilder::new().max_rect(rect));
    egui::Frame::group(ui.style()).show(&mut child, |ui| {
        egui::ScrollArea::both()
            .id_salt(id)
            .auto_shrink([false, false])
            .show(ui, |ui| {
                ui.vertical(content);
            });
    });
}

/// 表格格容器(BD-UI-34):预留矩形 + Frame,但**不开外层 ScrollArea**——
/// 滚动归 pinned_table 自己(表头固定 + 体滚 + 横滚同移),外层再包滚动
/// = 双滚动条且表头失去固定意义。
/// 吃满钉(0.2.6):Frame::show 的描边矩形按内容 min_rect 收缩(dbg 实锤:
/// 内容换成一行 label 时 Frame 从 gw 宽缩到 112)——本容器只配 pinned_table
/// (auto_shrink(false) 滚动区 = 不收缩填充物)使用;非表格内容走 group_frame。
fn group_frame_plain(
    ui: &mut egui::Ui,
    _id: &str,
    w: f32,
    h: f32,
    content: impl FnOnce(&mut egui::Ui),
) {
    let (rect, _resp) = ui.allocate_exact_size(egui::vec2(w, h), egui::Sense::hover());
    let mut child = ui.new_child(egui::UiBuilder::new().max_rect(rect));
    egui::Frame::group(ui.style()).show(&mut child, |ui| {
        ui.vertical(content);
    });
}

/// 表头固定表(BD-UI-34,用户裁定「每个表的表头都要固定」):
/// - 单 grid 实例(头行 + 体行)在 both ScrollArea 内——列对齐天然、无同 id
///   双 grid 病(dbg 实锤:两 grid 同 id 各从空白重建 State,save 交替
///   request_repaint → kittest max_steps 爆)。
/// - 表头固定 = 官方 DIY 模式(scroll_area.rs:412「painting a sticky header
///   on top of it」):ScrollArea 之后,用本拍捕获的表头格矩形在滚动区顶部
///   重画一条表头带(背景盖行 + 文字重画)。格矩形已含滚动平移 → 横向自动
///   跟随、纵向永远盖顶,零拍滞后。带高 = 表头格高 + 行间距(0.2.6 修:只盖
///   格高时,滚动行从行间距带里露 3px = 用户报「表头缝露内容」)。
/// - 覆盖带 = 纯 painter 绘制(0.2.6 修,用户报「资金右多一根竖线」根因):
///   旧版 ui.put 重画视口外表头格(16 列总宽撑到 x=1222),布局 min_rect
///   被撑宽 → Frame 描边矩形跟着撑 → 格右边界画在内容中段。painter.text/
///   rect_filled 零布局副作用;越界文字由 clip 裁掉。
///
/// body 闭包在体表 grid 内执行(行循环 + end_row 归闭包)。
fn pinned_table(
    ui: &mut egui::Ui,
    grid_id: &str,
    num_columns: usize,
    headers: &[&str],
    body: impl FnOnce(&mut egui::Ui),
) {
    let heads_cell: std::cell::RefCell<Vec<(egui::Rect, String)>> =
        std::cell::RefCell::new(Vec::new());
    let resp = egui::ScrollArea::both()
        .id_salt(format!("{grid_id}_sc"))
        .auto_shrink([false, false])
        .show(ui, |ui| {
            egui::Grid::new(grid_id)
                .striped(true)
                .num_columns(num_columns)
                .show(ui, |ui| {
                    for h in headers {
                        let r = ui.strong(*h);
                        heads_cell.borrow_mut().push((r.rect, h.to_string()));
                    }
                    ui.end_row();
                    body(ui);
                });
        });
    let heads = heads_cell.borrow();
    if heads.is_empty() {
        return;
    }
    let area = resp.inner_rect;
    let head_h = heads[0].0.height();
    // 覆盖带几何(0.2.8,BD-UI-35 缝隙根因):ScrollArea 内容裁剪 = inner_rect
    // 向外扩 visuals().clip_rect_margin(默认 3.0,style.rs:1435;
    // scroll_area.rs:771-772)——带子若只从 inner_rect 顶边起画,滚动行会从
    // 带子上方 3px 余量里露出(用户报「表头上面有缝」)。带子向上/左右各扩
    // 该余量,向下盖 表头格高 + grid 行间距。clip 同步 = 带矩形。
    let m = ui.visuals().clip_rect_margin;
    let strip = egui::Rect::from_min_max(
        egui::pos2(area.min.x - m, area.min.y - m),
        egui::pos2(
            area.max.x + m,
            area.min.y + head_h + ui.spacing().item_spacing.y,
        ),
    );
    ui.scope_builder(
        egui::UiBuilder::new()
            .id_salt(format!("{grid_id}_hdr_ov"))
            .max_rect(strip),
        |ui| {
            ui.set_clip_rect(strip);
            let painter = ui.painter();
            painter.rect_filled(strip, 0.0, ui.visuals().panel_fill);
            let font = ui.style().text_styles[&egui::TextStyle::Body].clone();
            let color = ui.visuals().strong_text_color();
            for (rect, text) in heads.iter() {
                painter.text(
                    egui::pos2(rect.min.x, area.min.y + head_h * 0.5),
                    egui::Align2::LEFT_CENTER,
                    text.as_str(),
                    font.clone(),
                    color,
                );
            }
        },
    );
}

// ================= 菜单栏 =================

fn render_menu_bar(ui: &mut egui::Ui, shared: &UiShared) {
    ui.horizontal(|ui| {
        // BD-UI-31(R1):一键直达 —— 菜单钮即入口,单击直接弹窗(去下拉层)。
        if ui.button(MENU_OPS).clicked() {
            shared.set_ops_window_open(true);
        }
        if ui.button(MENU_CTP).clicked() {
            shared.set_ctp_window_open(true);
        }
        if ui.button(MENU_CONTRACTS).clicked() {
            shared.set_contracts_window_open(true);
        }
        ui.menu_button(MENU_HELP, |ui| {
            ui.label(format!("ek-station 观测台 v{}", env!("CARGO_PKG_VERSION")));
            ui.label("布局 = Python 主窗框架等价(行情带/交易带/弹窗)");
        });
        // BD-UI-34:主题一键循环(金融深色→egui深色→egui浅色→Qt灰白→…;
        // 单层钮无下拉,与 R1「菜单钮即入口」同口径)。
        if ui
            .button(format!("主题:{}", shared.theme().label()))
            .clicked()
        {
            let t = shared.cycle_theme();
            shared.log_line(format!("[ops] 主题切换 → {}", t.label()));
        }
    });
}

// ================= 行情带 =================

fn render_market_band(ui: &mut egui::Ui, shared: &UiShared) {
    // BD-UI-31(R4):表题文字撤除(带位即身份,Python TickMonitor 无题同口径)。
    let groups = shared.tick_groups();
    if groups.is_empty() {
        ui.label("(等待 eTick 订阅)");
        return;
    }
    // 二级 tab:只列有 tick 的所
    let sel = shared.selected_exchange();
    let active = if groups.iter().any(|(e, _)| *e == sel) {
        sel.clone()
    } else {
        groups[0].0.clone()
    };
    ui.horizontal(|ui| {
        for (ex, _) in &groups {
            if ui.selectable_label(*ex == active, ex.clone()).clicked() {
                shared.set_selected_exchange(ex.clone());
            }
        }
    });
    let ticks = groups
        .iter()
        .find(|(e, _)| *e == active)
        .map(|(_, v)| v.clone())
        .unwrap_or_default();
    // 状态行「tick N 格 / 最后更新 …」撤除(BD-UI-35,用户裁定:此信息早已
    // 不需要,行情带 tab + 表格即全部)。
    pinned_table(
        ui,
        "tick_grid",
        14,
        &[
            "代码",
            "名称",
            "最新价",
            "涨跌幅",
            "成交量",
            "今开",
            "最高",
            "最低",
            "买1价",
            "买1量",
            "卖1价",
            "卖1量",
            "时间",
            "接口",
        ],
        |ui| {
            for t in &ticks {
                // 行交互(BD-UI-31 扩展):行内任何一格单击 = 带入下单面板
                // (盘口四列带方向:买1价/买1量 → 卖,卖1价/卖1量 → 买,价格列
                // 顺带填价;其余列方向/价格保持默认——用户裁定);任何一格双击
                // = 弹 K线窗。选中行 = 下单面板当前合约(带入即跳色)。
                let row_sel = shared.order_form().symbol == t.symbol;
                let mut cells = Vec::with_capacity(14);
                let mut dbl = false;
                let resp = ui.button(t.symbol.clone());
                if resp.clicked() {
                    shared.set_selected_exchange(active.clone());
                    import_tick(shared, t, &active, 0);
                }
                dbl |= resp.double_clicked();
                cells.push(resp.rect);
                macro_rules! cell {
                    ($base:expr, $col:expr) => {{
                        let base = $base;
                        let r = click_cell(ui, base.rect, ("tick", &t.symbol, $col));
                        if r.clicked() {
                            shared.set_selected_exchange(active.clone());
                            import_tick(shared, t, &active, $col);
                        }
                        dbl |= r.double_clicked();
                        cells.push(base.rect);
                    }};
                }
                cell!(
                    ui.label(if t.name.is_empty() {
                        "-".to_string()
                    } else {
                        t.name.clone()
                    }),
                    1
                );
                cell!(ui.label(format!("{:.1}", t.last_price)), 2);
                cell!(
                    ui.label(egui::RichText::new(format!("{:+.2}", t.pct_change)).color(
                        if t.pct_change < 0.0 {
                            egui::Color32::from_rgb(0x2E, 0x9E, 0x2E)
                        } else {
                            egui::Color32::from_rgb(0xE0, 0x50, 0x50)
                        },
                    )),
                    3
                );
                cell!(ui.label(format!("{:.0}", t.volume)), 4);
                cell!(ui.label(format!("{:.1}", t.open_price)), 5);
                cell!(ui.label(format!("{:.1}", t.high_price)), 6);
                cell!(ui.label(format!("{:.1}", t.low_price)), 7);
                cell!(
                    ui.label(
                        egui::RichText::new(format!("{:.1}", t.bid_price_1))
                            .color(egui::Color32::from_rgb(0xE0, 0x50, 0x50)),
                    ),
                    8
                );
                cell!(
                    ui.label(
                        egui::RichText::new(format!("{:.0}", t.bid_volume_1))
                            .color(egui::Color32::from_rgb(0xE0, 0x50, 0x50)),
                    ),
                    9
                );
                cell!(
                    ui.label(
                        egui::RichText::new(format!("{:.1}", t.ask_price_1))
                            .color(egui::Color32::from_rgb(0x2E, 0x9E, 0x2E)),
                    ),
                    10
                );
                cell!(
                    ui.label(
                        egui::RichText::new(format!("{:.0}", t.ask_volume_1))
                            .color(egui::Color32::from_rgb(0x2E, 0x9E, 0x2E)),
                    ),
                    11
                );
                cell!(
                    ui.label(if t.update_time.is_empty() {
                        fmt_dt(Some(t.datetime))
                    } else {
                        t.update_time.clone()
                    }),
                    12
                );
                cell!(ui.label(t.gateway_name.clone()), 13);
                if dbl {
                    shared.open_kline(t.symbol.clone());
                }
                paint_row_bg(ui, &cells, row_sel);
                ui.end_row();
            }
        },
    );
}

// ================= 下单面板(常驻) =================

fn render_order_panel(ui: &mut egui::Ui, shared: &UiShared) {
    // BD-UI-31(R4):面板题字撤除(下单侧栏即身份)。
    let mut f = shared.order_form();
    let before = f.clone();
    let mut err: Option<String> = None;

    // 随价勾选:价格框随该合约最新 tick(勾选态)
    if f.follow_price {
        if let Some(t) = tick_of(shared, &f.exchange, &f.symbol) {
            if t.last_price > 0.0 {
                f.price = format!("{:.1}", t.last_price);
            }
        }
    }

    // 交易所下拉:有 tick 的所 ∪ 六家常用所(操作面默认,不是配置面)
    let mut ex_opts: Vec<String> = shared
        .tick_groups()
        .iter()
        .map(|(e, _)| e.clone())
        .collect();
    for e in ["SHFE", "DCE", "CZCE", "CFFEX", "INE", "GFEX"] {
        if !ex_opts.iter().any(|x| x == e) {
            ex_opts.push(e.to_string());
        }
    }
    if !ex_opts.contains(&f.exchange) {
        ex_opts.insert(0, f.exchange.clone());
    }
    // 接口下拉:trade_snap 网关去重集 ∪ {sim, ctp}(审计钉:不新增总线口)
    let ts = shared.trade_snap();
    let mut gw_opts: Vec<String> = Vec::new();
    for g in ts
        .orders
        .iter()
        .map(|o| o.gateway_name.clone())
        .chain(ts.trades.iter().map(|t| t.gateway_name.clone()))
        .chain(ts.accounts.iter().map(|a| a.gateway_name.clone()))
    {
        if !g.is_empty() && !gw_opts.contains(&g) {
            gw_opts.push(g);
        }
    }
    for g in ["sim", "ctp"] {
        if !gw_opts.iter().any(|x| x == g) {
            gw_opts.push(g.to_string());
        }
    }
    if !gw_opts.contains(&f.gateway) {
        gw_opts.insert(0, f.gateway.clone());
    }

    // 表单网格(Python TradingWidget 字段排;委托/全撤 = 网格行;
    // 网格后不挂面板内容 —— 帧内 sizing 退让会把后置行叠回网格,盘口独立帧)。
    egui::Grid::new("order_form_grid")
        .num_columns(2)
        .show(ui, |ui| {
            ui.label("交易所");
            egui::ComboBox::from_id_salt("of_exchange")
                .selected_text(f.exchange.clone())
                .show_ui(ui, |ui| {
                    for e in &ex_opts {
                        ui.selectable_value(&mut f.exchange, e.clone(), e.clone());
                    }
                });
            ui.end_row();
            ui.label("代码");
            let sym_resp = ui.add(
                egui::TextEdit::singleline(&mut f.symbol)
                    .hint_text("cu2709")
                    .desired_width(150.0),
            );
            // 代码回车 = 订阅(Python TradingWidget returnPressed → set_en_symbol 等价)。
            // egui 单行 TextEdit 遇 Enter 即 surrender_focus,has_focus 在检查时已
            // false —— 规范口径 lost_focus + key_pressed(第五轮审计钉:s20 实测
            // has_focus 口径回车订阅从不触发)。
            if sym_resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
                let s = f.symbol.trim().to_string();
                // 与查询合约窗同口径:输入带 EX. 前缀则剥成裸代码(ctp.subscribe
                // 收裸码);剥后回写表单,名称/盘口/随价的 tick 查找同步可用。
                let bare = match s.split_once('.') {
                    Some((_, b)) => b.trim().to_string(),
                    None => s,
                };
                if !bare.is_empty() {
                    f.symbol = bare.clone();
                    shared.push_cmd(UiCmd::CtpSubscribe {
                        symbols: vec![bare.clone()],
                    });
                    shared.log_line(format!("[ops] 订阅已入列 {bare}(代码回车订阅)"));
                }
            }
            ui.end_row();
            ui.label("名称");
            ui.label(
                tick_of(shared, &f.exchange, &f.symbol)
                    .map(|t| {
                        if t.name.is_empty() {
                            "-".to_string()
                        } else {
                            t.name
                        }
                    })
                    .unwrap_or_else(|| "-".to_string()),
            );
            ui.end_row();
            ui.label("方向");
            egui::ComboBox::from_id_salt("of_direction")
                .selected_text(f.direction.as_str())
                .show_ui(ui, |ui| {
                    for d in [Direction::LONG, Direction::SHORT] {
                        ui.selectable_value(&mut f.direction, d, d.as_str());
                    }
                });
            ui.end_row();
            ui.label("开平");
            ui.horizontal(|ui| {
                egui::ComboBox::from_id_salt("of_offset")
                    .selected_text(f.offset.as_str())
                    .show_ui(ui, |ui| {
                        for o in [
                            Offset::OPEN,
                            Offset::CLOSE,
                            Offset::CLOSETODAY,
                            Offset::CLOSEYESTERDAY,
                        ] {
                            ui.selectable_value(&mut f.offset, o, o.as_str());
                        }
                    });
                // 0.1.1 BD-UI-30:自动开平勾选(开 = Auto 走拆单器;关 = None 原样)。
                ui.checkbox(&mut f.auto_split, "自动开平");
            });
            ui.end_row();
            ui.label("类型");
            egui::ComboBox::from_id_salt("of_order_type")
                .selected_text(f.order_type.as_str())
                .show_ui(ui, |ui| {
                    for ot in [OrderType::LIMIT, OrderType::MARKET] {
                        ui.selectable_value(&mut f.order_type, ot, ot.as_str());
                    }
                });
            ui.end_row();
            ui.label("价格");
            ui.horizontal(|ui| {
                ui.add(
                    egui::TextEdit::singleline(&mut f.price)
                        .hint_text("107660")
                        .desired_width(90.0),
                );
                ui.checkbox(&mut f.follow_price, "随价");
            });
            ui.end_row();
            ui.label("数量");
            ui.add(
                egui::TextEdit::singleline(&mut f.volume)
                    .hint_text("1")
                    .desired_width(90.0),
            );
            ui.end_row();
            ui.label("接口");
            egui::ComboBox::from_id_salt("of_gateway")
                .selected_text(if f.gateway.is_empty() {
                    "sim".to_string()
                } else {
                    f.gateway.clone()
                })
                .show_ui(ui, |ui| {
                    for g in &gw_opts {
                        ui.selectable_value(&mut f.gateway, g.clone(), g.clone());
                    }
                });
            ui.end_row();
            ui.label("操作");
            ui.horizontal(|ui| {
                if ui.button(BTN_SEND_ORDER).clicked() {
                    match f.to_request() {
                        Ok(req) => match crate::state::parse_gateway(&f.gateway) {
                            Ok(gateway) => {
                                shared.push_cmd(UiCmd::SendOrder {
                                    req,
                                    gateway,
                                    mode: if f.auto_split {
                                        ek_oms::LockMode::Auto
                                    } else {
                                        ek_oms::LockMode::None
                                    },
                                });
                                shared.log_line("[ops] 下单请求已入列(等 eTimer 请辞)");
                            }
                            Err(e) => {
                                shared.log_line(format!("[下单] {e}"));
                                err = Some(e);
                            }
                        },
                        Err(e) => {
                            shared.log_line(format!("[下单] {e}"));
                            err = Some(e);
                        }
                    }
                }
                if ui.button(BTN_CANCEL_ALL).clicked() {
                    // 全撤 = 活动委托逐个走 oms.cancel_order(界面侧扇出;
                    // 守卫逐单计数在 oms,不可绕过——0.1.1 BD-UI-30)
                    let mut n = 0usize;
                    for o in ts.orders.iter().filter(|o| order_active(o.status)) {
                        let gw = if o.gateway_name.is_empty() {
                            "sim".to_string()
                        } else {
                            o.gateway_name.clone()
                        };
                        shared.push_cmd(UiCmd::CancelOrder {
                            orderid: o.orderid.clone(),
                            gateway: gw,
                        });
                        n += 1;
                    }
                    shared.log_line(format!("[ops] 全撤入列 {n} 笔(等 eTimer 请辞)"));
                }
            });
            ui.end_row();
            ui.label("代码回车 = 订阅");
            ui.label("");
            ui.end_row();
        });
    if let Some(e) = err {
        ui.label(egui::RichText::new(e).color(egui::Color32::RED));
    }
    if f != before {
        shared.set_order_form(f.clone());
    }
}

/// 盘口帧(选中合约 tick 五档;Python 买5卖5 等价;独立帧避网格叠行)。
/// BD-UI-31(R4):题字撤除(含括号合约代码——选中合约已由下单面板代码格与
/// 行情带跳色行指认,盘口帧不重复报幕)。
fn render_book_frame(ui: &mut egui::Ui, shared: &UiShared) {
    let f = shared.order_form();
    match tick_of(shared, &f.exchange, &f.symbol) {
        Some(t) => {
            for i in (1..=5).rev() {
                let (p, v) = match i {
                    1 => (t.ask_price_1, t.ask_volume_1),
                    2 => (t.ask_price_2, t.ask_volume_2),
                    3 => (t.ask_price_3, t.ask_volume_3),
                    4 => (t.ask_price_4, t.ask_volume_4),
                    _ => (t.ask_price_5, t.ask_volume_5),
                };
                ui.label(
                    egui::RichText::new(format!("卖{i}  {:.1}  {:.0}", p, v))
                        .color(egui::Color32::from_rgb(0x2E, 0x9E, 0x2E)),
                );
            }
            for i in 1..=5 {
                let (p, v) = match i {
                    1 => (t.bid_price_1, t.bid_volume_1),
                    2 => (t.bid_price_2, t.bid_volume_2),
                    3 => (t.bid_price_3, t.bid_volume_3),
                    4 => (t.bid_price_4, t.bid_volume_4),
                    _ => (t.bid_price_5, t.bid_volume_5),
                };
                ui.label(
                    egui::RichText::new(format!("买{i}  {:.1}  {:.0}", p, v))
                        .color(egui::Color32::from_rgb(0xE0, 0x50, 0x50)),
                );
            }
        }
        None => {
            ui.label("(无 tick)");
        }
    }
}

// ================= 2×2 监控格 =================

fn render_topleft_group(ui: &mut egui::Ui, shared: &UiShared) {
    let tab = shared.topleft_tab();
    ui.horizontal(|ui| {
        if ui
            .selectable_label(tab == TopLeftTab::Orders, TopLeftTab::Orders.label())
            .clicked()
        {
            shared.set_topleft_tab(TopLeftTab::Orders);
        }
        if ui
            .selectable_label(tab == TopLeftTab::Active, TopLeftTab::Active.label())
            .clicked()
        {
            shared.set_topleft_tab(TopLeftTab::Active);
        }
    });
    render_orders_table(ui, shared, tab == TopLeftTab::Active);
}

/// 委托表(17 列逐值 = Python OrderMonitor 蓝图:代码/名称/交易所/类型/方向/
/// 开平/价格/总量/已成/成交均价/状态/状态描述/时间/委托号/来源/接口 + 操作;
/// 活动行撤单钮 + 双击撤单,终态行只读)。
fn render_orders_table(ui: &mut egui::Ui, shared: &UiShared, active_only: bool) {
    let ts = shared.trade_snap();
    // 表题撤除(第五轮审计):tab「委托|未成交」即 Python 口径的表身份,
    // 再挂同名 label = 三重撞名(下单钮/tab/表题),纯冗余。
    let rows: Vec<_> = ts
        .orders
        .iter()
        .filter(|o| !active_only || order_active(o.status))
        .collect();
    // 委托表列头单源(空/非空两分支同源,0.2.7 审计口径)。
    const ORDER_HEADERS: [&str; 17] = [
        "代码",
        "名称",
        "交易所",
        "类型",
        "方向",
        "开平",
        "价格",
        "总量",
        "已成",
        "成交均价",
        "状态",
        "状态描述",
        "时间",
        "委托号",
        "来源",
        "接口",
        "操作",
    ];
    if rows.is_empty() {
        // 空态提示移入表格 body(BD-UI-35,用户裁定:提示不得在表头上方,
        // 应在表头之下):label 成为 grid 首格 = 表头下第一行,格不缩。
        pinned_table(
            ui,
            if active_only {
                "active_grid"
            } else {
                "order_grid"
            },
            17,
            &ORDER_HEADERS,
            |ui| {
                ui.label(if active_only {
                    "(无未成交)"
                } else {
                    "(无委托:等网关 send_order)"
                });
            },
        );
        return;
    }
    pinned_table(
        ui,
        if active_only {
            "active_grid"
        } else {
            "order_grid"
        },
        17,
        &ORDER_HEADERS,
        |ui| {
            for o in &rows {
                // 行交互(BD-UI-31 扩展):行内任何一格单击 = 选中行(跳色);
                // 任何一格双击 = 撤单(Python OrderMonitor itemDoubleClicked→
                // cancel_order 等价,活动行限定;显式撤单钮同令并存)。
                let key = format!("{}.{}", o.gateway_name, o.orderid);
                let row_sel = shared.row_selected("orders", &key);
                let mut cells = Vec::with_capacity(17);
                let mut dbl = false;
                let sel = ui.selectable_label(row_sel, o.symbol.clone());
                if sel.clicked() {
                    shared.select_row("orders", &key);
                }
                dbl |= sel.double_clicked();
                cells.push(sel.rect);
                macro_rules! cell {
                    ($base:expr, $col:expr) => {{
                        let base = $base;
                        let r = click_cell(ui, base.rect, ("ord", &key, $col));
                        if r.clicked() {
                            shared.select_row("orders", &key);
                        }
                        dbl |= r.double_clicked();
                        cells.push(base.rect);
                    }};
                }
                cell!(
                    ui.label(if o.name.is_empty() {
                        "-".to_string()
                    } else {
                        o.name.clone()
                    }),
                    1
                );
                cell!(ui.label(o.exchange.as_str().to_string()), 2);
                cell!(ui.label(o.order_type.as_str().to_string()), 3);
                cell!(
                    ui.label(
                        egui::RichText::new(o.direction.map(|d| d.as_str()).unwrap_or("-")).color(
                            if o.direction == Some(Direction::SHORT) {
                                egui::Color32::from_rgb(0x2E, 0x9E, 0x2E)
                            } else {
                                egui::Color32::from_rgb(0xE0, 0x50, 0x50)
                            },
                        ),
                    ),
                    4
                );
                cell!(ui.label(o.offset.as_str().to_string()), 5);
                cell!(ui.label(format!("{:.1}", o.price)), 6);
                cell!(ui.label(format!("{:.0}", o.volume)), 7);
                cell!(ui.label(format!("{:.0}", o.traded)), 8);
                cell!(ui.label(format!("{:.1}", o.traded_price)), 9);
                cell!(
                    ui.label(egui::RichText::new(o.status.as_str()).color(
                        if o.status == ek_data::constant::Status::REJECTED {
                            egui::Color32::from_rgb(0xE0, 0x50, 0x50)
                        } else if o.status == ek_data::constant::Status::ALLTRADED {
                            egui::Color32::from_rgb(0x2E, 0x9E, 0x2E)
                        } else {
                            egui::Color32::from_rgb(0xE0, 0x88, 0x10)
                        },
                    )),
                    10
                );
                cell!(ui.label(o.status_msg.clone()), 11);
                cell!(ui.label(fmt_dt(o.datetime)), 12);
                cell!(ui.label(o.orderid.clone()), 13);
                cell!(
                    ui.label(if o.reference.is_empty() {
                        "-".to_string()
                    } else {
                        o.reference.clone()
                    }),
                    14
                );
                cell!(ui.label(o.gateway_name.clone()), 15);
                if dbl && order_active(o.status) {
                    let gw = if o.gateway_name.is_empty() {
                        "sim".to_string()
                    } else {
                        o.gateway_name.clone()
                    };
                    shared.push_cmd(UiCmd::CancelOrder {
                        orderid: o.orderid.clone(),
                        gateway: gw,
                    });
                }
                let hr = ui.horizontal(|ui| {
                    if order_active(o.status)
                        && ui
                            .small_button(format!("{BTN_CANCEL_ORDER} {}", o.orderid))
                            .clicked()
                    {
                        // 撤单跟随委托的来路网关(行级 gateway_name;空串兜底 sim)
                        let gw = if o.gateway_name.is_empty() {
                            "sim".to_string()
                        } else {
                            o.gateway_name.clone()
                        };
                        shared.push_cmd(UiCmd::CancelOrder {
                            orderid: o.orderid.clone(),
                            gateway: gw,
                        });
                    }
                });
                cells.push(hr.response.rect);
                paint_row_bg(ui, &cells, row_sel);
                ui.end_row();
            }
        },
    );
}

/// 成交组(12 列逐值 = Python TradeMonitor 蓝图)。
fn render_trades_group(ui: &mut egui::Ui, shared: &UiShared) {
    ui.label("成交");
    let ts = shared.trade_snap();
    // 成交表列头单源(空/非空两分支同源,0.2.7 审计口径)。
    const TRADE_HEADERS: [&str; 12] = [
        "成交号",
        "代码",
        "名称",
        "交易所",
        "方向",
        "开平",
        "价格",
        "数量",
        "成交额",
        "时间",
        "委托号",
        "接口",
    ];
    if ts.trades.is_empty() {
        // 空态提示移入表格 body(0.2.8,BD-UI-35,同未成交口径)
        pinned_table(ui, "trade_grid", 12, &TRADE_HEADERS, |ui| {
            ui.label("(无成交)");
        });
        return;
    }
    pinned_table(ui, "trade_grid", 12, &TRADE_HEADERS, |ui| {
        for t in &ts.trades {
            // 行交互(BD-UI-31 扩展):行内任何一格单击 = 选中行(跳色);
            // 方向列上色(R2,与委托/持仓表同口径:空绿/多红)。
            let key = format!("{}.{}", t.gateway_name, t.tradeid);
            let row_sel = shared.row_selected("trades", &key);
            let mut cells = Vec::with_capacity(12);
            let sel = ui.selectable_label(row_sel, t.tradeid.clone());
            if sel.clicked() {
                shared.select_row("trades", &key);
            }
            cells.push(sel.rect);
            macro_rules! cell {
                ($base:expr, $col:expr) => {{
                    let base = $base;
                    let r = click_cell(ui, base.rect, ("trd", &key, $col));
                    if r.clicked() {
                        shared.select_row("trades", &key);
                    }
                    cells.push(base.rect);
                }};
            }
            cell!(ui.label(t.symbol.clone()), 1);
            cell!(
                ui.label(if t.name.is_empty() {
                    "-".to_string()
                } else {
                    t.name.clone()
                }),
                2
            );
            cell!(ui.label(t.exchange.as_str().to_string()), 3);
            cell!(
                ui.label(
                    egui::RichText::new(t.direction.map(|d| d.as_str()).unwrap_or("-")).color(
                        if t.direction == Some(Direction::SHORT) {
                            egui::Color32::from_rgb(0x2E, 0x9E, 0x2E)
                        } else {
                            egui::Color32::from_rgb(0xE0, 0x50, 0x50)
                        },
                    ),
                ),
                4
            );
            cell!(ui.label(t.offset.as_str().to_string()), 5);
            cell!(ui.label(format!("{:.1}", t.price)), 6);
            cell!(ui.label(format!("{:.0}", t.volume)), 7);
            cell!(ui.label(format!("{:.1}", t.traded_amount)), 8);
            cell!(ui.label(fmt_dt(t.datetime)), 9);
            cell!(ui.label(t.orderid.clone()), 10);
            cell!(ui.label(t.gateway_name.clone()), 11);
            paint_row_bg(ui, &cells, row_sel);
            ui.end_row();
        }
    });
}

/// 持仓组(16 列逐值 = Python PositionMonitor 蓝图;双击行带入下单面板,
/// Python mainwindow.py:276 等价,不开 K线)。
fn render_positions_group(ui: &mut egui::Ui, shared: &UiShared) {
    ui.label("持仓");
    let ts = shared.trade_snap();
    // 持仓表列头单源(空/非空两分支同源,0.2.7 审计口径)。
    const POS_HEADERS: [&str; 16] = [
        "代码",
        "名称",
        "交易所",
        "方向",
        "数量",
        "昨仓",
        "可用",
        "冻结",
        "在途",
        "市值",
        "成本",
        "均价",
        "最新价",
        "盈亏",
        "盈亏比",
        "接口",
    ];
    if ts.positions.is_empty() {
        // 空态提示移入表格 body(0.2.8,BD-UI-35,同未成交口径)
        pinned_table(ui, "pos_grid", 16, &POS_HEADERS, |ui| {
            ui.label("(无持仓)");
        });
        return;
    }
    pinned_table(ui, "pos_grid", 16, &POS_HEADERS, |ui| {
        for p in &ts.positions {
            // 行交互(BD-UI-31 扩展):行内任何一格单击 = 选中行(跳色);
            // 任何一格双击 = 带入下单面板(Python mainwindow.py:276 等价,
            // 不开 K线;方向随持仓方向,Python 同口径)。
            let key = format!("{}.{}.{}", p.gateway_name, p.symbol, p.direction.as_str());
            let row_sel = shared.row_selected("positions", &key);
            let mut cells = Vec::with_capacity(16);
            let mut dbl = false;
            let sel = ui.selectable_label(row_sel, p.symbol.clone());
            if sel.clicked() {
                shared.select_row("positions", &key);
            }
            dbl |= sel.double_clicked();
            cells.push(sel.rect);
            macro_rules! cell {
                ($base:expr, $col:expr) => {{
                    let base = $base;
                    let r = click_cell(ui, base.rect, ("pos", &key, $col));
                    if r.clicked() {
                        shared.select_row("positions", &key);
                    }
                    dbl |= r.double_clicked();
                    cells.push(base.rect);
                }};
            }
            cell!(
                ui.label(if p.name.is_empty() {
                    "-".to_string()
                } else {
                    p.name.clone()
                }),
                1
            );
            cell!(ui.label(p.exchange.as_str().to_string()), 2);
            cell!(
                ui.label(egui::RichText::new(p.direction.as_str()).color(
                    if p.direction == ek_data::constant::Direction::SHORT {
                        egui::Color32::from_rgb(0x2E, 0x9E, 0x2E)
                    } else {
                        egui::Color32::from_rgb(0xE0, 0x50, 0x50)
                    },
                )),
                3
            );
            cell!(ui.label(format!("{:.0}", p.volume)), 4);
            cell!(ui.label(format!("{:.0}", p.yd_volume)), 5);
            cell!(ui.label(format!("{:.0}", p.can_use_volume)), 6);
            cell!(ui.label(format!("{:.0}", p.frozen)), 7);
            cell!(ui.label(format!("{:.0}", p.on_road_volume)), 8);
            cell!(ui.label(format!("{:.1}", p.market_value)), 9);
            cell!(ui.label(format!("{:.2}", p.price)), 10);
            cell!(ui.label(format!("{:.2}", p.avg_price)), 11);
            cell!(ui.label(format!("{:.2}", p.last_price)), 12);
            cell!(
                ui.label(
                    egui::RichText::new(format!("{:+.1}", p.pnl)).color(if p.pnl < 0.0 {
                        egui::Color32::from_rgb(0x2E, 0x9E, 0x2E)
                    } else {
                        egui::Color32::from_rgb(0xE0, 0x50, 0x50)
                    }),
                ),
                13
            );
            cell!(ui.label(format!("{:+.4}", p.profit_rate)), 14);
            cell!(ui.label(p.gateway_name.clone()), 15);
            if dbl {
                let mut f = shared.order_form();
                f.symbol = p.symbol.clone();
                f.exchange = p.exchange.as_str().to_string();
                f.direction = p.direction;
                shared.set_order_form(f);
                shared.log_line(format!("[ops] 持仓带入下单面板 {}", p.symbol));
            }
            paint_row_bg(ui, &cells, row_sel);
            ui.end_row();
        }
    });
}

fn render_bottomright_group(ui: &mut egui::Ui, shared: &UiShared) {
    let tab = shared.bottomright_tab();
    ui.horizontal(|ui| {
        if ui
            .selectable_label(
                tab == BottomRightTab::Account,
                BottomRightTab::Account.label(),
            )
            .clicked()
        {
            shared.set_bottomright_tab(BottomRightTab::Account);
        }
        if ui
            .selectable_label(tab == BottomRightTab::Logs, BottomRightTab::Logs.label())
            .clicked()
        {
            shared.set_bottomright_tab(BottomRightTab::Logs);
        }
    });
    match tab {
        BottomRightTab::Account => {
            let ts = shared.trade_snap();
            if ts.accounts.is_empty() {
                ui.label("(无资金:等网关 eAccount.)");
                return;
            }
            egui::Grid::new("acct_grid")
                .striped(true)
                .num_columns(5)
                .show(ui, |ui| {
                    for h in ["账号", "接口", "余额", "可用", "冻结"] {
                        ui.strong(h);
                    }
                    ui.end_row();
                    for a in &ts.accounts {
                        ui.label(a.accountid.clone());
                        ui.label(a.gateway_name.clone());
                        ui.label(format!("{:.2}", a.balance));
                        ui.label(format!("{:.2}", a.available()));
                        ui.label(format!("{:.2}", a.frozen));
                        ui.end_row();
                    }
                });
        }
        BottomRightTab::Logs => {
            render_logs(ui, shared);
        }
    }
}

fn render_logs(ui: &mut egui::Ui, shared: &UiShared) {
    ui.horizontal(|ui| {
        if ui.button(BTN_CLEAR_LOGS).clicked() {
            shared.clear_logs(); // LOCAL-FIRST:就地清,零往返
        }
        let (_, spilled) = shared.logs_snapshot();
        if spilled > 0 {
            ui.label(format!(
                "已丢弃 {spilled} 条(环形 {})",
                crate::state::RING_CAP
            ));
        }
    });
    let lines = shared.logs_snapshot().0;
    egui::ScrollArea::vertical()
        .id_salt("log_scroll")
        .max_height(180.0)
        .stick_to_bottom(true)
        .show(ui, |ui| {
            for line in lines {
                ui.monospace(line);
            }
        });
}

// ================= 独立窗面(BD-UI-32/33)=================

/// 独立窗几何(BD-UI-34:助手参数收敛,8→6 闸 clippy too_many_arguments)。
struct WinGeom {
    inner: [f32; 2],
    min_inner: [f32; 2],
    maximized: bool,
}

/// 独立 OS 窗统一开法(BD-UI-33,四窗一型:运维/CTP/查询合约/K线)。
///
/// - **守卫(0.2.3 教训内建):** 调用方按状态位辞行——位关不调本函数 =
///   viewport 本拍未 used = egui/eframe GC 销毁 OS 窗(销毁判据源码
///   context.rs「never used this pass」)。
/// - Embedded 回退(后端不支持多窗:kittest/WEB)= 主窗内 egui::Window 同本体,
///   回退面关闭 = embedded 闭包返回 true。
/// - Immediate 面 OS X = close_requested → 返回 true(调用方翻负状态位)。
/// - `geom.maximized` = 打开即最大化(BD-UI-34,K线窗用户裁定;OS 窗属性,
///   Embedded 回退面无此概念 = 忽略)。
/// - `body` 返回 true = 内容侧请求关闭(BD-UI-34:K线窗 ESC 键快捷关闭;
///   键事件按子窗焦点路由,主窗按键不会误关)。
///
/// 返回 = "本拍该窗被关"(调用方负责状态位/列表出表)。
fn show_independent_window(
    ctx: &egui::Context,
    id_salt: impl std::hash::Hash,
    title: &str,
    geom: &WinGeom,
    embedded: impl Fn(&egui::Context) -> bool,
    body: impl Fn(&mut egui::Ui) -> bool,
) -> bool {
    let closed = std::cell::Cell::new(false);
    let mut builder = egui::ViewportBuilder::default()
        .with_title(title)
        .with_inner_size(geom.inner)
        .with_min_inner_size(geom.min_inner)
        .with_minimize_button(true)
        .with_maximize_button(true)
        .with_close_button(true);
    if geom.maximized {
        builder = builder.with_maximized(true);
    }
    ctx.show_viewport_immediate(
        egui::ViewportId::from_hash_of(id_salt),
        builder,
        |ctx2, class| {
            if class == egui::ViewportClass::Embedded {
                closed.set(embedded(ctx2));
                return;
            }
            if ctx2.input(|i| i.viewport().close_requested()) {
                closed.set(true);
                return;
            }
            egui::CentralPanel::default().show(ctx2, |ui| {
                if body(ui) {
                    closed.set(true);
                }
            });
        },
    );
    closed.get()
}

fn render_windows(ctx: &egui::Context, shared: &UiShared) {
    // 运维窗(菜单「运维」;BD-UI-33 转独立 OS 窗,用户裁定「四窗皆可脱离主窗」)。
    if shared.ops_window_open() {
        let closed = show_independent_window(
            ctx,
            "ops_win",
            "运维窗",
            &WinGeom {
                inner: [680.0, 420.0],
                min_inner: [420.0, 260.0],
                maximized: false,
            },
            |ctx2| {
                let mut open = shared.ops_window_open();
                egui::Window::new("运维窗")
                    .id(egui::Id::new("ops_win"))
                    .default_pos([80.0, 320.0])
                    .default_size([640.0, 380.0])
                    .open(&mut open)
                    .show(ctx2, |ui| {
                        render_ops_window(ui, shared);
                    });
                shared.set_ops_window_open(open);
                !open
            },
            |ui| {
                render_ops_window(ui, shared);
                false
            },
        );
        if closed {
            shared.set_ops_window_open(false);
        }
    }

    // CTP 配置窗(菜单「网关」;CTP 插件自己的界面;BD-UI-33 转独立 OS 窗)。
    if shared.ctp_window_open() {
        let closed = show_independent_window(
            ctx,
            "ctp_win",
            "CTP 网关配置窗",
            &WinGeom {
                inner: [600.0, 380.0],
                min_inner: [420.0, 260.0],
                maximized: false,
            },
            |ctx2| {
                let mut open = shared.ctp_window_open();
                egui::Window::new("CTP 网关配置窗")
                    .id(egui::Id::new("ctp_win"))
                    .default_pos([640.0, 320.0])
                    .default_size([560.0, 330.0])
                    .open(&mut open)
                    .show(ctx2, |ui| {
                        render_ctp_window(ui, shared);
                    });
                shared.set_ctp_window_open(open);
                !open
            },
            |ui| {
                render_ctp_window(ui, shared);
                false
            },
        );
        if closed {
            shared.set_ctp_window_open(false);
        }
    }

    // 查询合约窗(菜单「查询合约」;Python ContractManager 等价:查询 + 勾选 + 订阅)。
    if shared.contracts_window_open() {
        let closed = show_independent_window(
            ctx,
            "contracts_win",
            "查询合约窗",
            &WinGeom {
                inner: [620.0, 460.0],
                min_inner: [420.0, 260.0],
                maximized: false,
            },
            |ctx2| {
                let mut open = shared.contracts_window_open();
                egui::Window::new("查询合约窗")
                    .id(egui::Id::new("contracts_win"))
                    .default_pos([420.0, 300.0])
                    .default_size([560.0, 420.0])
                    .open(&mut open)
                    .show(ctx2, |ui| {
                        render_contracts_window(ui, shared);
                    });
                shared.set_contracts_window_open(open);
                !open
            },
            |ui| {
                render_contracts_window(ui, shared);
                false
            },
        );
        if closed {
            shared.set_contracts_window_open(false);
        }
    }

    // K线窗(双击弹出,按合约多开;出 kline_symbols 列表 = 不再 show = 销毁)。
    // BD-UI-34:打开即最大化(maximized)+ ESC 键快捷关闭(body 请求关闭;
    // 键按子窗焦点路由,主窗按键不误关)。
    let mut closed: Vec<String> = Vec::new();
    for sym in shared.kline_symbols() {
        let is_closed = show_independent_window(
            ctx,
            format!("kline_win:{sym}"),
            &format!("K线 — {sym}"),
            &WinGeom {
                inner: [560.0, 360.0],
                min_inner: [360.0, 220.0],
                maximized: true,
            },
            |ctx2| {
                let esc = ctx2.input(|i| i.key_pressed(egui::Key::Escape));
                let resp = egui::Window::new(format!("K线 — {sym}"))
                    .id(egui::Id::new(("kline_win", sym.clone())))
                    .default_pos([300.0, 340.0])
                    .default_size([540.0, 330.0])
                    .show(ctx2, |ui| {
                        let cell = shared
                            .market()
                            .cells()
                            .into_iter()
                            .find(|c| c.symbol == sym);
                        render_kline_cell(ui, cell.as_ref());
                    });
                esc || resp.is_none()
            },
            |ui| {
                if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
                    return true;
                }
                let cell = shared
                    .market()
                    .cells()
                    .into_iter()
                    .find(|c| c.symbol == sym);
                render_kline_cell(ui, cell.as_ref());
                false
            },
        );
        if is_closed {
            closed.push(sym);
        }
    }
    for s in closed {
        shared.close_kline(&s);
    }
}

/// 运维窗:插件表(svctl 操作 + 二次确认)+ 待确认横幅 + 服务目录(三列)
/// + 依赖图 + 流控查询钮。回灌界面面已退役(总线口保留)。
fn render_ops_window(ui: &mut egui::Ui, shared: &UiShared) {
    ui.label("插件");
    let snap = shared.snapshot();
    egui::Grid::new("plug_grid")
        .striped(true)
        .num_columns(6)
        .show(ui, |ui| {
            ui.strong("名称");
            ui.strong("状态");
            ui.strong("版本");
            ui.strong("依赖");
            ui.strong("统计");
            ui.strong("操作");
            ui.end_row();
            if snap.rows.is_empty() {
                ui.label("(快照空:等 eTimer 首拉)");
                ui.end_row();
            }
            for r in &snap.rows {
                if is_quarantined(&r.state) {
                    ui.label(egui::RichText::new(r.name.clone()).color(egui::Color32::RED));
                    ui.label(
                        egui::RichText::new(state_text(r))
                            .color(egui::Color32::RED)
                            .strong(),
                    );
                } else {
                    ui.label(r.name.clone());
                    ui.label(state_text(r));
                }
                ui.label(r.version.clone());
                ui.label(format!("deps:[{}]", r.deps.join(",")));
                ui.label(format!("spilled:{} panics:{}", r.spilled, r.panics));
                ui.horizontal(|ui| {
                    if ui.small_button(format!("重注入 {}", r.name)).clicked() {
                        shared.push_cmd(UiCmd::Start {
                            factory: r.name.clone(),
                        });
                    }
                    let armed = shared.pending_confirm();
                    let armed_here = ui
                        .small_button(remove_button_label(&r.name, armed.as_deref()))
                        .clicked()
                        && shared.confirm_step(&r.name) == ConfirmStep::Fired;
                    if armed_here {
                        shared.push_cmd(UiCmd::Remove {
                            name: r.name.clone(),
                        });
                    }
                });
                ui.end_row();
            }
        });

    if let Some(name) = shared.pending_confirm() {
        ui.horizontal(|ui| {
            ui.label(
                egui::RichText::new(format!("待二次确认:再次点击『移除 {name}』完成"))
                    .color(egui::Color32::from_rgb(0xE0, 0x88, 0x10)),
            );
            if ui.small_button(BTN_CANCEL_CONFIRM).clicked() {
                shared.confirm_cancel();
            }
        });
    }

    ui.label("服务");
    let flat: Vec<String> = snap
        .services
        .iter()
        .flat_map(|(n, ids)| {
            if ids.is_empty() {
                vec![format!("{n} 服务"), "-".to_string()]
            } else {
                ids.iter()
                    .flat_map(|id| [format!("{n} 服务"), id.clone()])
                    .collect()
            }
        })
        .collect();
    if flat.is_empty() {
        ui.label("(无服务目录)");
    } else {
        let rows = flat.len().div_ceil(6); // 每列 2 字段 × 3 列
        egui::Grid::new("svc_grid")
            .striped(true)
            .num_columns(6)
            .show(ui, |ui| {
                for r in 0..rows {
                    for c in 0..3 {
                        let i = c * rows * 2 + r * 2;
                        if i < flat.len() {
                            ui.label(flat[i].clone());
                            ui.label(flat[i + 1].clone());
                        } else {
                            ui.label("");
                            ui.label("");
                        }
                    }
                    ui.end_row();
                }
            });
    }

    ui.label("依赖");
    if !snap.rows.iter().any(|r| !r.deps.is_empty()) {
        ui.label("(全部无依赖)");
    }
    for r in &snap.rows {
        if !r.deps.is_empty() {
            ui.label(format!("{} → {}", r.name, r.deps.join(" + ")));
        }
    }

    ui.horizontal(|ui| {
        if ui.small_button(BTN_QUERY_RATE).clicked() {
            shared.push_cmd(UiCmd::QueryRate);
        }
    });
}

/// CTP 配置窗渲染(S6 定盘口径:账号/前置全部界面手工输入,插件不读文件;
/// `连接 CTP` → ask ctp.configure;密码位掩码。**无订阅字段**(用户裁定:
/// 订阅不在网关窗定死,走下单面板代码回车或查询合约窗勾选))。
fn render_ctp_window(ui: &mut egui::Ui, shared: &UiShared) {
    ui.label("账号与前置手工输入(插件自带配置面;密码不回显)");
    let mut form: crate::state::CtpForm = shared.ctp_form();
    egui::Grid::new("ctp_grid").num_columns(6).show(ui, |ui| {
        ui.label("broker");
        ctp_edit(ui, shared, &mut form.broker_id, "9999", CtpField::Broker);
        ui.label("td 前置");
        ctp_edit(
            ui,
            shared,
            &mut form.td_addr,
            "tcp://host:40001",
            CtpField::TdAddr,
        );
        ui.label("td 用户");
        ctp_edit(ui, shared, &mut form.td_user, "用户号", CtpField::TdUser);
        ui.end_row();
        ui.label("td 密码");
        ctp_edit(
            ui,
            shared,
            &mut form.td_password,
            "密码",
            CtpField::TdPassword,
        );
        ui.label("AppID");
        ctp_edit(ui, shared, &mut form.td_app_id, "app", CtpField::AppId);
        ui.label("AuthCode");
        ctp_edit(
            ui,
            shared,
            &mut form.td_auth_code,
            "授权码",
            CtpField::AuthCode,
        );
        ui.end_row();
        ui.label("md 前置");
        ctp_edit(
            ui,
            shared,
            &mut form.md_addr,
            "tcp://host:40011",
            CtpField::MdAddr,
        );
        ui.label("md 用户");
        ctp_edit(
            ui,
            shared,
            &mut form.md_user,
            "空=td 回落",
            CtpField::MdUser,
        );
        ui.label("md 密码");
        ctp_edit(
            ui,
            shared,
            &mut form.md_password,
            "空=td 回落",
            CtpField::MdPassword,
        );
        ui.end_row();
    });
    ui.horizontal(|ui| {
        if ui.small_button(BTN_CTP_CONNECT).clicked() {
            shared.push_cmd(UiCmd::CtpConfigure(form.clone()));
            shared.log_line("[ops] CTP 配置已入列(等 eTimer 请辞 → ctp.configure)");
        }
        ui.label("订阅不在本网关窗定死:查询合约窗勾选订阅,或下单面板代码回车订阅(用户裁定)");
    });
}

/// 查询合约窗(Python ContractManager 等价):查询串(代码/交易所/名称模糊,
/// 留空 = 全部)+ 勾选表 + 订阅钮(勾选集 → UiCmd::CtpSubscribe → ask
/// ctp.subscribe)。合约数据 = `eContract.` 自动回流,窗不催查询。
fn render_contracts_window(ui: &mut egui::Ui, shared: &UiShared) {
    let mut q = shared.contract_query();
    ui.horizontal(|ui| {
        ui.label("代码或交易所(留空 = 全部):");
        if ui
            .add(
                egui::TextEdit::singleline(&mut q)
                    .hint_text("cu 或 SHFE")
                    .desired_width(200.0),
            )
            .changed()
        {
            shared.set_contract_query(q.clone());
        }
    });
    let rows = shared.contracts(&q);
    // 表头单源(0.2.6 审计修:空态分支曾误写另一套表头「代码/…/最后价」,
    // 与非空分支「选(代码)/…/最小变动」不一致——列语义两处必须同源)。
    const CONTRACT_HEADERS: [&str; 5] = ["选(代码)", "交易所", "名称", "乘数", "最小变动"];
    if rows.is_empty() {
        // 空态提示移入表格 body(0.2.8,BD-UI-35,同未成交口径);动作行无意义可省。
        pinned_table(ui, "contract_grid", 5, &CONTRACT_HEADERS, |ui| {
            ui.label("(无合约:先连接 CTP,合约随 eContract. 自动回流)");
        });
        return;
    }
    let selected = shared.selected_contracts();
    // 动作行在前、表格吃满剩余窗高(BD-UI-33,用户实看指出):旧
    // max_height(260) 是内嵌弹窗时代残留——OS 窗里表格定高 260px,
    // 放大窗口表格不跟着长,放大就没了意义。现在放大窗口 = 表格变高,
    // 行溢出才出滚动条。
    ui.horizontal(|ui| {
        ui.label(format!("已选 {} 项 / 共 {} 项", selected.len(), rows.len()));
        if ui.button(BTN_SUBSCRIBE_SELECTED).clicked() {
            if selected.is_empty() {
                shared.log_line("[查询合约] 未勾选任何合约,订阅未发");
            } else {
                // en_symbol 勾选集 → CTP 裸代码订阅(ask ctp.subscribe 收裸代码)
                let symbols: Vec<String> = selected
                    .iter()
                    .map(|k| {
                        k.split_once('.')
                            .map(|(_, s)| s.to_string())
                            .unwrap_or_else(|| k.clone())
                    })
                    .collect();
                shared.push_cmd(UiCmd::CtpSubscribe { symbols });
                shared.log_line(format!(
                    "[ops] 查询合约窗订阅 {} 项已入列(等 eTimer 请辞 → ctp.subscribe)",
                    selected.len()
                ));
            }
        }
    });
    pinned_table(ui, "contract_grid", 5, &CONTRACT_HEADERS, |ui| {
        for c in &rows {
            let key = format!("{}.{}", c.exchange.as_str(), c.symbol);
            let mut on = selected.contains(&key);
            // 勾选钮标签 = en_symbol(唯一可定位;订阅钮按勾选集发)。
            // BD-UI-31 扩展:勾选 = 订阅选择 + 行选中;其余格单击 =
            // 行选中(动作归行,不归某一列)。
            let row_sel = shared.row_selected("contracts", &key);
            let mut cells = Vec::with_capacity(5);
            let ck = ui.checkbox(&mut on, key.clone());
            if ck.changed() {
                shared.toggle_contract_selection(&key);
                shared.select_row("contracts", &key);
            }
            cells.push(ck.rect);
            macro_rules! cell {
                ($base:expr, $col:expr) => {{
                    let base = $base;
                    let r = click_cell(ui, base.rect, ("crt", &key, $col));
                    if r.clicked() {
                        shared.select_row("contracts", &key);
                    }
                    cells.push(base.rect);
                }};
            }
            cell!(ui.label(c.exchange.as_str().to_string()), 1);
            cell!(ui.label(c.name.clone()), 2);
            cell!(ui.label(format!("{:.0}", c.size)), 3);
            cell!(ui.label(format!("{:.4}", c.price_tick)), 4);
            paint_row_bg(ui, &cells, row_sel);
            ui.end_row();
        }
    });
}

/// CTP 表单字段枚举(ctp_edit 回写定位;订阅标的字段已撤出网关窗)。
enum CtpField {
    Broker,
    TdAddr,
    TdUser,
    TdPassword,
    AppId,
    AuthCode,
    MdAddr,
    MdUser,
    MdPassword,
}

/// 单行 TextEdit + changed 回写 CTP 表单(密码位掩码)。
fn ctp_edit(ui: &mut egui::Ui, shared: &UiShared, val: &mut String, hint: &str, field: CtpField) {
    let is_secret = matches!(
        field,
        CtpField::TdPassword | CtpField::MdPassword | CtpField::AuthCode
    );
    let mut w = egui::TextEdit::singleline(val)
        .hint_text(hint)
        .desired_width(110.0);
    if is_secret {
        w = w.password(true);
    }
    if ui.add(w).changed() {
        let mut f = shared.ctp_form();
        match field {
            CtpField::Broker => f.broker_id = val.clone(),
            CtpField::TdAddr => f.td_addr = val.clone(),
            CtpField::TdUser => f.td_user = val.clone(),
            CtpField::TdPassword => f.td_password = val.clone(),
            CtpField::AppId => f.td_app_id = val.clone(),
            CtpField::AuthCode => f.td_auth_code = val.clone(),
            CtpField::MdAddr => f.md_addr = val.clone(),
            CtpField::MdUser => f.md_user = val.clone(),
            CtpField::MdPassword => f.md_password = val.clone(),
        }
        shared.set_ctp_form(f);
    }
}

/// K线窗内容(dm bar 合成序列;格定点 = 该合约格;无格占位)。
///
/// 图实例每帧重建(BD-UI-20 口径:数据在 store 不丢,交互状态不跨帧)。
fn render_kline_cell(ui: &mut egui::Ui, cell: Option<&CellView>) {
    let Some(c) = cell else {
        ui.label("(K线无格:等 dm 合成)");
        return;
    };
    if c.bars.is_empty() {
        ui.label(format!("(K线:{}@{} 批空)", c.symbol, c.interval));
        return;
    }
    let mut data = egui_charts::model::bar::BarData::new();
    for b in &c.bars {
        let dt = chrono::DateTime::<chrono::Utc>::from_timestamp_nanos(b.datetime);
        data.push(egui_charts::model::bar::Bar::new(
            dt,
            b.open_price,
            b.high_price,
            b.low_price,
            b.close_price,
            b.volume,
        ));
    }
    ui.label(format!(
        "K线 {}@{}s({} 根)",
        c.symbol,
        c.interval,
        c.bars.len()
    ));
    let mut chart = egui_charts::widget::Chart::new(data).visible_bars(60);
    chart.set_symbol(&c.symbol);
    chart.set_timeframe_label(&format!("{}s", c.interval));
    chart.enable_tracking_mode();
    let _ = chart.show(ui);
}