windui 0.7.0

轻量跨平台桌面 GUI:tiny-skia 渲染 + 平台原生窗口/文字(Windows: Win32+DirectWrite;macOS: Cocoa+CoreText)
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
//! 可排序数据表格的响应式驱动(`Element::table_sortable` 内部使用)。
//!
//! 两个响应式 widget 共享同一个 `Signal<Option<(列下标, SortOrder)>>` 排序状态:
//! - [`SortableHeader`]:挂在表头行上,排序状态变化时重建表头单元格(刷新箭头)。
//! - [`SortableBody`]:挂在滚动正文上,排序状态变化时按列重排并重建数据行。
//!
//! 表头单元格点击循环切换:无 → 升序 → 降序 → 无。数值型列(两侧都可解析为 f64)
//! 按数值比较,否则按字符串比较(与主流表格的 numeric-aware 排序一致)。
//!
//! 外部只通过 `Element::table_sortable` 使用,无需直接构造本模块类型。

use std::cell::{Cell, RefCell};
use std::cmp::Ordering;
use std::rc::Rc;

use crate::anim::{Easing, Transition};
use crate::core::{EventCtx, Widget};
use crate::event::{Event, Key, PointerKind};
use crate::geometry::{Color, Rect, Size};
use crate::render::{Canvas, Paint};
use crate::signal::Signal;
use crate::spec::{Align, Dimension};
use crate::style::{Role, Style};
use crate::text::TextEngine;

use super::{Element, SortOrder, Truncate, TABLE_CELL_PAD_X, TABLE_CELL_PAD_Y, TABLE_HEADER_PAD_Y};

/// 受控排序状态:`None` 无排序;`Some((列下标, 方向))` 按该列排序。
pub(super) type SortState = Signal<Option<(usize, SortOrder)>>;

/// 操作列配置:在表格尾部追加一列,单元格由 `build(行下标)` 按行生成任意控件
/// (如 查看/编辑/删除 按钮组)。表头显示 `title`(不可排序),列宽按 `weight` 参与分配。
///
/// 由 [`Element::actions`](super::Element::actions) 设置后透传给响应式表头与正文;
/// 排序/换页重建时按行重新调用 `build`。传给 `build` 的行下标:客户端表格
/// (`table_sortable`/`table_selectable`)为**原始行下标**(排序后仍锁定同一数据行,
/// 与选择语义一致);服务端表格(`table_sortable_server`)为当前页内的**显示下标**。
/// `build` 内用 `move` 捕获该下标即可为每行绑定独立回调。
#[derive(Clone)]
pub(super) struct ActionCol {
    title: String,
    weight: f32,
    build: Rc<dyn Fn(usize) -> Element>,
}

/// 排序指示器(表头箭头)的每实例样式覆盖。用 `Element::sort_indicator(SortStyle{..})` 链式设置;
/// 字段为 `None` 时回退到主题 [`TableTheme`](crate::theme::TableTheme),再回退到内置默认。
///
/// # 示例
/// ```ignore
/// use windui::prelude::*;
/// Element::table_sortable(cols, rows, sort)
///     .sort_indicator(SortStyle { asc: Some("↑".into()), desc: Some("↓".into()), ..Default::default() })
/// ```
#[derive(Clone, Default)]
pub struct SortStyle {
    /// 升序箭头字形(如 "↑" / "▲")。
    pub asc: Option<String>,
    /// 降序箭头字形(如 "↓" / "▼")。
    pub desc: Option<String>,
    /// 箭头字号 px。
    pub size: Option<f32>,
    /// 箭头颜色(定死色;不设则用主题 text_muted 并随换肤)。
    pub color: Option<Color>,
    /// 箭头槽宽度 px。
    pub slot: Option<i32>,
    /// 标题与箭头间距 px。
    pub gap: Option<i32>,
    /// 箭头置于标题左侧(默认右侧)。
    pub leading: Option<bool>,
}

/// 解析后的排序指示器样式(实例覆盖 → 主题 → 内置默认,合并完成)。
struct ResolvedSort {
    asc: String,
    desc: String,
    size: f32,
    /// `None` = 用 `Role::TextMuted`(随主题热切换);`Some` = 定死色。
    color: Option<Color>,
    slot: i32,
    gap: i32,
    leading: bool,
}

/// 按 实例覆盖 → 主题 `TableTheme` → 内置默认 的优先级合并出最终样式。
fn resolve_sort_style(ov: &SortStyle) -> ResolvedSort {
    let t = crate::theme::current();
    let tt = &t.table;
    ResolvedSort {
        asc: ov.asc.clone().unwrap_or_else(|| tt.sort_asc().to_string()),
        desc: ov
            .desc
            .clone()
            .unwrap_or_else(|| tt.sort_desc().to_string()),
        size: ov.size.unwrap_or_else(|| tt.sort_size()),
        color: ov.color.or(tt.sort_color),
        slot: ov.slot.unwrap_or_else(|| tt.sort_slot()),
        gap: ov.gap.unwrap_or_else(|| tt.sort_gap()),
        leading: ov.leading.unwrap_or_else(|| tt.sort_leading()),
    }
}

/// 排序意图变更回调(服务端排序模式):点表头更新 `sort` 后触发,携带新排序状态,
/// 由应用据此重新拉取"当前页 + 该排序"的数据并写回正文数据信号。多个表头单元格共享。
pub(super) type OnSort = Rc<RefCell<dyn FnMut(&mut EventCtx, Option<(usize, SortOrder)>)>>;

/// 单元格值比较:两侧都能解析为数值时按数值比,否则按字符串(区分大小写)。
fn cmp_cells(a: &str, b: &str) -> Ordering {
    match (a.trim().parse::<f64>(), b.trim().parse::<f64>()) {
        (Ok(x), Ok(y)) => x.partial_cmp(&y).unwrap_or(Ordering::Equal),
        _ => a.cmp(b),
    }
}

/// 依当前排序状态求行序(返回原始行下标的排列)。`None` 时保持原序(稳定)。
pub(super) fn sorted_order(rows: &[Vec<String>], sort: Option<(usize, SortOrder)>) -> Vec<usize> {
    let mut order: Vec<usize> = (0..rows.len()).collect();
    if let Some((col, ord)) = sort {
        // sort_by 稳定:等值行保持原相对次序。
        order.sort_by(|&a, &b| {
            let va = rows[a].get(col).map(String::as_str).unwrap_or("");
            let vb = rows[b].get(col).map(String::as_str).unwrap_or("");
            let c = cmp_cells(va, vb);
            match ord {
                SortOrder::Asc => c,
                SortOrder::Desc => c.reverse(),
            }
        });
    }
    order
}

/// 点击第 `ci` 列表头后的下一排序状态。循环:非活动列 → 升序;升序 → 降序;降序 → 取消。
pub(super) fn next_sort(
    current: Option<(usize, SortOrder)>,
    ci: usize,
) -> Option<(usize, SortOrder)> {
    match current {
        Some((c, SortOrder::Asc)) if c == ci => Some((ci, SortOrder::Desc)),
        Some((c, SortOrder::Desc)) if c == ci => None,
        _ => Some((ci, SortOrder::Asc)),
    }
}

/// 构建一个表头单元格:标题(单行、末尾省略)+ 定宽排序箭头槽,整格可点击循环切换排序。
/// 箭头独立渲染于单元格首/末(由 `rs.leading` 决定),空间不足时省略标题而非让箭头换行。
/// 样式(字形/字号/颜色/槽宽/间距/位置)由 `rs` 提供(实例覆盖 → 主题 → 默认)。
/// `on_sort` 为 `Some` 时(服务端模式),点击在更新 `sort` 后再触发回调(供应用重新拉取)。
fn header_cell(
    ci: usize,
    title: &str,
    weight: f32,
    sort: SortState,
    on_sort: Option<OnSort>,
    rs: &ResolvedSort,
) -> Element {
    let glyph = match sort.get() {
        Some((c, SortOrder::Asc)) if c == ci => rs.asc.clone(),
        Some((c, SortOrder::Desc)) if c == ci => rs.desc.clone(),
        _ => String::new(),
    };
    // 定宽箭头槽:始终预留,仅活动列显示字形。颜色未定死时用 TextMuted 角色随主题热切换。
    let mut arrow = Element::label(glyph)
        .font_size(rs.size)
        .width(rs.slot)
        .height(18);
    arrow = match rs.color {
        Some(c) => arrow.fg(c),
        None => arrow.fg_role(Role::TextMuted),
    };
    // 标题占剩余宽度,单行 + 末尾省略号(空间不足时截断标题,不影响箭头)。
    let title_el = Element::label(title.to_string())
        .font_size(13.0)
        .font_weight(600)
        .fg_role(Role::TextMuted)
        .max_lines(1)
        .truncate(Truncate::End)
        .weight(1.0)
        .height(18);
    let mut inner = Element::row()
        .width_match()
        .cross(Align::Center)
        .spacing(rs.gap);
    inner = if rs.leading {
        inner.child(arrow).child(title_el)
    } else {
        inner.child(title_el).child(arrow)
    };
    Element::stack()
        .weight(weight)
        .clickable()
        .on_click(move |ctx| {
            let next = next_sort(sort.get(), ci);
            sort.set(next);
            if let Some(cb) = &on_sort {
                (cb.borrow_mut())(ctx, next);
            }
        })
        .padding_xy(TABLE_CELL_PAD_X, TABLE_HEADER_PAD_Y)
        .child(inner)
}

/// 正文行悬停高亮的叠层不透明度(叠层取主题文字色,明暗自适应;"轻微"故低于 clickable 的 0.06)。
const ROW_HOVER_A: f32 = 0.05;

/// 只读表格正文行的悬停高亮 widget:悬停时在整行背景(斑马纹)之上、单元格内容之下绘制
/// 一层轻微半透明叠层(主题文字色低 alpha,明暗自适应),带淡入淡出。无点击/焦点/手型,
/// 纯视觉反馈。Enter/Leave 由框架沿祖先链派发(命中的是子单元格/标签,行仍收到)。
pub(super) struct HoverRow {
    hover: bool,
    /// 叠层不透明度补间(normal=0 / hover);首帧靠 `primed` 落定。
    overlay: Cell<Transition<f32>>,
    primed: Cell<bool>,
}

impl HoverRow {
    pub(super) fn new() -> Self {
        Self {
            hover: false,
            overlay: Cell::new(Transition::new(0.0)),
            primed: Cell::new(false),
        }
    }
}

impl Widget for HoverRow {
    fn paint(
        &self,
        bounds: Rect,
        _content: Rect,
        _focused: bool,
        enabled: bool,
        canvas: &mut dyn Canvas,
        style: &Style,
    ) {
        let th = crate::theme::current();
        let target = if enabled && self.hover {
            ROW_HOVER_A
        } else {
            0.0
        };
        let mut ov = self.overlay.get();
        if !self.primed.get() {
            ov = Transition::new(target);
            self.primed.set(true);
        } else if ov.target() != target {
            ov.retarget(target, th.anim.fast(), Easing::EaseOut);
        }
        let a = ov.animate();
        self.overlay.set(ov);
        if a > 0.001 {
            canvas.fill_round_rect(
                bounds.x as f32,
                bounds.y as f32,
                bounds.w as f32,
                bounds.h as f32,
                style.corner_radius,
                &Paint::fill(th.palette.text.scale_alpha(a)),
            );
        }
    }
    fn on_event(&mut self, ctx: &mut EventCtx, ev: &Event) -> bool {
        if let Event::Pointer(p) = ev {
            match p.kind {
                PointerKind::Enter => {
                    if !self.hover {
                        self.hover = true;
                        ctx.mark_dirty();
                    }
                    true
                }
                PointerKind::Leave => {
                    if self.hover {
                        self.hover = false;
                        ctx.mark_dirty();
                    }
                    true
                }
                _ => false,
            }
        } else {
            false
        }
    }
    fn reset_interaction(&mut self) {
        self.hover = false;
        self.primed.set(false); // 隐藏期不回放 hover 淡出,下次显示瞬时落定
    }
}

/// 操作列单元格:水平内边距 + 内容垂直居中,宽度按权重(内容自身决定高度,行随之等高)。
/// 与文本列的 `table_cell_pad` 不同——不强制 20px 高,故按钮等较高控件不被压扁。
fn action_cell(content: Element, weight: f32) -> Element {
    Element::row()
        .weight(weight)
        .cross(Align::Center)
        .padding_xy(TABLE_CELL_PAD_X, TABLE_CELL_PAD_Y)
        .child(content)
}

/// 操作列表头单元格:加粗弱化色标题(单行末尾省略),不可点击(操作列不参与排序)。
fn action_header_cell(title: &str, weight: f32) -> Element {
    Element::stack()
        .weight(weight)
        .padding_xy(TABLE_CELL_PAD_X, TABLE_HEADER_PAD_Y)
        .child(
            Element::label(title.to_string())
                .font_size(13.0)
                .font_weight(600)
                .fg_role(Role::TextMuted)
                .max_lines(1)
                .truncate(Truncate::End)
                .width_match()
                .height(18),
        )
}

/// 构建一行正文:`disp` 为显示位置(决定斑马纹),`orig` 为该行下标(传给操作列生成器),
/// `cells` 为该行各列文本。结构与 `table_custom` 一致:`col[ row(单元格…), divider ]`。
/// 行挂 `HoverRow` widget,悬停时整行轻微高亮。`actions` 为 `Some` 时在末尾追加操作单元格。
pub(super) fn body_row(
    disp: usize,
    orig: usize,
    cells: &[String],
    weights: &[f32],
    actions: Option<&ActionCol>,
) -> Element {
    let mut tr = Element::row().width_match().cross(Align::Stretch);
    // 斑马纹随显示位置交替(而非原始行号),排序后视觉仍规整。
    if disp % 2 == 1 {
        tr = tr.bg_role(Role::SurfaceAlt);
    }
    for (ci, cell) in cells.iter().enumerate() {
        let w = weights.get(ci).copied().unwrap_or(1.0);
        tr = tr
            .child(Element::table_cell_pad(Element::label(cell.clone()).font_size(13.0)).weight(w));
    }
    if let Some(a) = actions {
        tr = tr.child(action_cell((a.build)(orig), a.weight));
    }
    tr.widget = Box::new(HoverRow::new());
    Element::col()
        .width_match()
        .child(tr)
        .child(Element::divider())
}

/// 清空某节点的全部子节点(递归释放子树 arena slot)。
fn clear_children(tree: &mut crate::core::Tree, id: crate::core::NodeId) {
    let old: Vec<_> = tree.get(id).map(|n| n.children.clone()).unwrap_or_default();
    for c in old {
        tree.remove(c);
    }
    if let Some(n) = tree.get_mut(id) {
        n.children.clear();
    }
}

/// 响应式表头:首次布局构建单元格;排序状态变化时重建(刷新箭头方向)。
/// 单元格统一由本 widget 构建(不在构造期预建),故 `.sort_indicator(..)` 在 build 前设置的
/// 样式覆盖能被首次构建采纳。`on_sort` 在服务端模式下透传给单元格(点击时触发应用重新拉取)。
pub(super) struct SortableHeader {
    columns: Vec<(String, f32)>,
    sort: SortState,
    on_sort: Option<OnSort>,
    /// 每实例样式覆盖(由 `Element::sort_indicator` 设置);未设字段回退主题。
    style: SortStyle,
    /// 尾部操作列(由 `Element::actions` 设置);`Some` 时追加不可排序的操作表头单元格。
    actions: Option<ActionCol>,
    /// 是否已构建过单元格(首次 on_update 无条件构建)。
    built: bool,
    last_version: u64,
}

impl SortableHeader {
    pub(super) fn new(
        columns: Vec<(String, f32)>,
        sort: SortState,
        on_sort: Option<OnSort>,
    ) -> Self {
        Self {
            last_version: sort.version(),
            columns,
            sort,
            on_sort,
            style: SortStyle::default(),
            actions: None,
            built: false,
        }
    }

    /// 设置每实例样式覆盖(`Element::sort_indicator` 在 build 前调用)。
    pub(super) fn set_style(&mut self, style: SortStyle) {
        self.style = style;
    }

    /// 设置尾部操作列(`Element::actions` 在 build 前调用);置 `built=false` 令首次构建纳入。
    pub(super) fn set_actions(&mut self, actions: ActionCol) {
        self.actions = Some(actions);
        self.built = false;
    }
}

impl Widget for SortableHeader {
    fn on_update(&mut self, ctx: &mut EventCtx) {
        let ver = self.sort.version();
        if self.built && ver == self.last_version {
            return;
        }
        self.built = true;
        self.last_version = ver;
        let rs = resolve_sort_style(&self.style);
        let self_id = ctx.id();
        let tree = ctx.tree_mut();
        clear_children(tree, self_id);
        for (ci, (title, w)) in self.columns.iter().enumerate() {
            let mut el = header_cell(ci, title, *w, self.sort, self.on_sort.clone(), &rs);
            // 直接 build+add_child 绕过了父级线性容器 build 循环的 weight→主轴维度转换
            // (见 Element::build),此处手工复现:表头行恒为水平轴,故落到宽度。
            el.width = Dimension::Weight(*w);
            let id = el.build(tree);
            tree.add_child(self_id, id);
        }
        // 尾部操作列表头(不可排序),与正文操作单元格同权重对齐。
        if let Some(a) = &self.actions {
            let mut el = action_header_cell(&a.title, a.weight);
            el.width = Dimension::Weight(a.weight);
            let id = el.build(tree);
            tree.add_child(self_id, id);
        }
    }

    // 自身无视觉内容;背景/边框由容器 Style 处理(同 DynList)。
    fn measure(&self, _avail: Size, _style: &Style, _text: &mut dyn TextEngine) -> Size {
        Size::ZERO
    }
    fn paint(
        &self,
        _bounds: Rect,
        _content: Rect,
        _focused: bool,
        _enabled: bool,
        _canvas: &mut dyn Canvas,
        _style: &Style,
    ) {
    }
    fn on_event(&mut self, _ctx: &mut EventCtx, _ev: &Event) -> bool {
        false
    }
    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
        Some(self)
    }
}

/// 响应式正文:排序状态变化时按列重排并重建数据行(详见模块级说明)。
pub(super) struct SortableBody {
    rows: Vec<Vec<String>>,
    weights: Vec<f32>,
    sort: SortState,
    /// 尾部操作列(由 `Element::actions` 设置)。
    actions: Option<ActionCol>,
    /// 强制下次 on_update 重建(`set_actions` 置位——初始 eager 行不含操作列,需重建一次)。
    force: bool,
    last_version: u64,
}

impl SortableBody {
    pub(super) fn new(rows: Vec<Vec<String>>, weights: Vec<f32>, sort: SortState) -> Self {
        Self {
            last_version: sort.version(),
            rows,
            weights,
            sort,
            actions: None,
            force: false,
        }
    }

    /// 设置尾部操作列;置 `force` 令首次 on_update 重建(把操作列纳入,替换初始纯文本行)。
    pub(super) fn set_actions(&mut self, actions: ActionCol) {
        self.actions = Some(actions);
        self.force = true;
    }
}

impl Widget for SortableBody {
    fn on_update(&mut self, ctx: &mut EventCtx) {
        let ver = self.sort.version();
        if !self.force && ver == self.last_version {
            return;
        }
        self.force = false;
        self.last_version = ver;
        let self_id = ctx.id();
        let tree = ctx.tree_mut();
        clear_children(tree, self_id);
        let order = sorted_order(&self.rows, self.sort.get());
        for (disp, &ri) in order.iter().enumerate() {
            let el = body_row(
                disp,
                ri,
                &self.rows[ri],
                &self.weights,
                self.actions.as_ref(),
            );
            let id = el.build(tree);
            tree.add_child(self_id, id);
        }
    }

    fn measure(&self, _avail: Size, _style: &Style, _text: &mut dyn TextEngine) -> Size {
        Size::ZERO
    }
    fn paint(
        &self,
        _bounds: Rect,
        _content: Rect,
        _focused: bool,
        _enabled: bool,
        _canvas: &mut dyn Canvas,
        _style: &Style,
    ) {
    }
    fn on_event(&mut self, _ctx: &mut EventCtx, _ev: &Event) -> bool {
        false
    }
    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
        Some(self)
    }
}

/// 服务端排序模式的正文:绑定"当前页数据"信号,**不做内部排序**——排序由后端完成,
/// 前端只按应用给定的顺序渲染。数据信号版本变化(应用换页/换排序后写回)时重建行。
pub(super) struct PagedBody {
    rows: Signal<Vec<Vec<String>>>,
    weights: Vec<f32>,
    /// 尾部操作列(由 `Element::actions` 设置)。生成器收到当前页内显示下标。
    actions: Option<ActionCol>,
    /// 强制下次 on_update 重建(`set_actions` 置位)。
    force: bool,
    last_version: u64,
}

impl PagedBody {
    pub(super) fn new(rows: Signal<Vec<Vec<String>>>, weights: Vec<f32>) -> Self {
        Self {
            last_version: rows.version(),
            rows,
            weights,
            actions: None,
            force: false,
        }
    }

    /// 设置尾部操作列;置 `force` 令首次 on_update 重建(把操作列纳入)。
    pub(super) fn set_actions(&mut self, actions: ActionCol) {
        self.actions = Some(actions);
        self.force = true;
    }
}

impl Widget for PagedBody {
    fn on_update(&mut self, ctx: &mut EventCtx) {
        let ver = self.rows.version();
        if !self.force && ver == self.last_version {
            return;
        }
        self.force = false;
        self.last_version = ver;
        let self_id = ctx.id();
        let tree = ctx.tree_mut();
        clear_children(tree, self_id);
        let data = self.rows.get();
        for (disp, row) in data.iter().enumerate() {
            let el = body_row(disp, disp, row, &self.weights, self.actions.as_ref());
            let id = el.build(tree);
            tree.add_child(self_id, id);
        }
    }

    fn measure(&self, _avail: Size, _style: &Style, _text: &mut dyn TextEngine) -> Size {
        Size::ZERO
    }
    fn paint(
        &self,
        _bounds: Rect,
        _content: Rect,
        _focused: bool,
        _enabled: bool,
        _canvas: &mut dyn Canvas,
        _style: &Style,
    ) {
    }
    fn on_event(&mut self, _ctx: &mut EventCtx, _ev: &Event) -> bool {
        false
    }
    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
        Some(self)
    }
}

// ==== 行选择(复选框首列 + 全选表头 + 选中行高亮) ====

/// 选择列(复选框)固定宽度 px。
const SELECT_COL_W: i32 = 40;
/// 复选框方框边长 px(与 CheckBox 一致,便于自绘全选框视觉统一)。
const SEL_BOX: i32 = 18;
/// 选中行高亮叠层不透明度(accent 低 alpha;叠在斑马纹之上、悬停层之下)。
const ROW_SELECTED_A: f32 = 0.12;

/// 首列复选框单元格:固定宽,内含绑定该行选择信号的复选框(水平居中 + 垂直居中)。
fn select_cell(row_sel: Signal<bool>) -> Element {
    let pad = (SELECT_COL_W - SEL_BOX) / 2;
    Element::row()
        .width(SELECT_COL_W)
        .cross(Align::Center)
        .padding_xy(pad, 0)
        .child(Element::checkbox("", row_sel).width(SEL_BOX))
}

/// 构建一行可选正文:[复选框列] + 数据列;行挂 [`SelectableRow`](悬停 + 选中高亮)。
/// `row_sel` 为该行(原始行身份)的选择信号:复选框直接绑定,行高亮读取它。
pub(super) fn select_body_row(
    disp: usize,
    orig: usize,
    cells: &[String],
    weights: &[f32],
    row_sel: Signal<bool>,
    actions: Option<&ActionCol>,
) -> Element {
    let mut tr = Element::row().width_match().cross(Align::Stretch);
    if disp % 2 == 1 {
        tr = tr.bg_role(Role::SurfaceAlt);
    }
    tr = tr.child(select_cell(row_sel));
    for (ci, cell) in cells.iter().enumerate() {
        let w = weights.get(ci).copied().unwrap_or(1.0);
        tr = tr
            .child(Element::table_cell_pad(Element::label(cell.clone()).font_size(13.0)).weight(w));
    }
    if let Some(a) = actions {
        tr = tr.child(action_cell((a.build)(orig), a.weight));
    }
    tr.widget = Box::new(SelectableRow::new(row_sel));
    Element::col()
        .width_match()
        .child(tr)
        .child(Element::divider())
}

/// 可选正文行:在 [`HoverRow`] 基础上叠加"选中"高亮(accent 低 alpha 底,读取行选择信号)。
/// 选中底为瞬时(跟随信号),悬停层为淡入淡出,叠在其上。
pub(super) struct SelectableRow {
    sel: Signal<bool>,
    hover: bool,
    overlay: Cell<Transition<f32>>,
    primed: Cell<bool>,
}

impl SelectableRow {
    pub(super) fn new(sel: Signal<bool>) -> Self {
        Self {
            sel,
            hover: false,
            overlay: Cell::new(Transition::new(0.0)),
            primed: Cell::new(false),
        }
    }
}

impl Widget for SelectableRow {
    fn paint(
        &self,
        bounds: Rect,
        _content: Rect,
        _focused: bool,
        enabled: bool,
        canvas: &mut dyn Canvas,
        style: &Style,
    ) {
        let th = crate::theme::current();
        // 选中底(accent 低 alpha,瞬时跟随信号)。
        if enabled && self.sel.get() {
            canvas.fill_round_rect(
                bounds.x as f32,
                bounds.y as f32,
                bounds.w as f32,
                bounds.h as f32,
                style.corner_radius,
                &Paint::fill(th.palette.accent.scale_alpha(ROW_SELECTED_A)),
            );
        }
        // 悬停层(text 低 alpha,淡入淡出,叠在选中底之上)。
        let target = if enabled && self.hover {
            ROW_HOVER_A
        } else {
            0.0
        };
        let mut ov = self.overlay.get();
        if !self.primed.get() {
            ov = Transition::new(target);
            self.primed.set(true);
        } else if ov.target() != target {
            ov.retarget(target, th.anim.fast(), Easing::EaseOut);
        }
        let a = ov.animate();
        self.overlay.set(ov);
        if a > 0.001 {
            canvas.fill_round_rect(
                bounds.x as f32,
                bounds.y as f32,
                bounds.w as f32,
                bounds.h as f32,
                style.corner_radius,
                &Paint::fill(th.palette.text.scale_alpha(a)),
            );
        }
    }
    fn on_event(&mut self, ctx: &mut EventCtx, ev: &Event) -> bool {
        if let Event::Pointer(p) = ev {
            match p.kind {
                PointerKind::Enter => {
                    if !self.hover {
                        self.hover = true;
                        ctx.mark_dirty();
                    }
                    true
                }
                PointerKind::Leave => {
                    if self.hover {
                        self.hover = false;
                        ctx.mark_dirty();
                    }
                    true
                }
                _ => false,
            }
        } else {
            false
        }
    }
    fn reset_interaction(&mut self) {
        self.hover = false;
        self.primed.set(false);
    }
}

/// 全选表头单元格 widget:自绘三态复选框(全选 ✓ / 部分选 − / 未选 空),点击切换全选/清空。
/// 直接读写各行选择信号(单一数据源),故手动勾选行时表头聚合态实时刷新。
pub(super) struct SelectAllCheck {
    sel: Vec<Signal<bool>>,
}

impl SelectAllCheck {
    pub(super) fn new(sel: Vec<Signal<bool>>) -> Self {
        Self { sel }
    }
    /// 是否全选(非空且每行皆选)。
    fn all(&self) -> bool {
        !self.sel.is_empty() && self.sel.iter().all(|s| s.get())
    }
    /// 切换:全选→清空;否则→全选。
    fn toggle_all(&self, ctx: &mut EventCtx) {
        let target = !self.all();
        for s in &self.sel {
            s.set(target);
        }
        ctx.mark_dirty_all(); // 影响所有行,升整窗
    }
}

impl Widget for SelectAllCheck {
    fn paint(
        &self,
        bounds: Rect,
        _content: Rect,
        _focused: bool,
        enabled: bool,
        canvas: &mut dyn Canvas,
        _style: &Style,
    ) {
        let th = crate::theme::current();
        let (p, tg) = (&th.palette, &th.toggle);
        let n = self.sel.iter().filter(|s| s.get()).count();
        let all = n > 0 && n == self.sel.len();
        let some = n > 0 && !all;
        let box_x = bounds.x as f32 + ((SELECT_COL_W - SEL_BOX) / 2) as f32;
        let box_y = bounds.y as f32 + ((bounds.h - SEL_BOX) / 2) as f32;
        let sz = SEL_BOX as f32;
        let radius = 4.0;
        let accent = if enabled { tg.accent(p) } else { p.track };
        let filled = all || some;
        canvas.fill_round_rect(
            box_x,
            box_y,
            sz,
            sz,
            radius,
            &Paint::fill(if filled { accent } else { tg.knob(p) }),
        );
        if !filled {
            canvas.stroke_round_rect(box_x, box_y, sz, sz, radius, 1.5, &Paint::fill(tg.track(p)));
        }
        let mark = Paint::fill(p.on_accent);
        let s = sz / 18.0; // 18px 基准(与 CheckBox 对勾坐标一致)
        if all {
            canvas.draw_line(
                box_x + 4.0 * s,
                box_y + 9.0 * s,
                box_x + 8.0 * s,
                box_y + 13.0 * s,
                2.0,
                &mark,
            );
            canvas.draw_line(
                box_x + 8.0 * s,
                box_y + 13.0 * s,
                box_x + 14.0 * s,
                box_y + 5.0 * s,
                2.0,
                &mark,
            );
        } else if some {
            // 部分选中:横杠(indeterminate)。
            canvas.draw_line(
                box_x + 4.5 * s,
                box_y + 9.0 * s,
                box_x + 13.5 * s,
                box_y + 9.0 * s,
                2.0,
                &mark,
            );
        }
    }
    fn on_event(&mut self, ctx: &mut EventCtx, ev: &Event) -> bool {
        match ev {
            Event::Pointer(p) if p.kind == PointerKind::Up => {
                if ctx.bounds().contains(p.pos) {
                    self.toggle_all(ctx);
                }
                true
            }
            Event::Pointer(p) if p.kind == PointerKind::Down => {
                ctx.request_focus();
                true
            }
            Event::Key(k) if k.pressed && (k.key == Key::Space || k.key == Key::Enter) => {
                self.toggle_all(ctx);
                true
            }
            _ => false,
        }
    }
    fn focusable(&self) -> bool {
        true
    }
}

/// 可选表格正文:响应式(排序变化重排重建),每行含首列复选框 + 数据列,绑定按原始行身份。
/// 选择变化不触发本重建(复选框自更新、行高亮经信号重绘),仅排序变化重排。
pub(super) struct SelectableBody {
    rows: Vec<Vec<String>>,
    weights: Vec<f32>,
    sel: Vec<Signal<bool>>,
    sort: SortState,
    /// 尾部操作列(由 `Element::actions` 设置)。生成器收到原始行下标(与选择同身份)。
    actions: Option<ActionCol>,
    built: bool,
    last_version: u64,
}

impl SelectableBody {
    pub(super) fn new(
        rows: Vec<Vec<String>>,
        weights: Vec<f32>,
        sel: Vec<Signal<bool>>,
        sort: SortState,
    ) -> Self {
        Self {
            last_version: sort.version(),
            rows,
            weights,
            sel,
            sort,
            actions: None,
            built: false,
        }
    }

    /// 设置尾部操作列;置 `built=false` 令首次 on_update 把操作列纳入。
    pub(super) fn set_actions(&mut self, actions: ActionCol) {
        self.actions = Some(actions);
        self.built = false;
    }
}

impl Widget for SelectableBody {
    fn on_update(&mut self, ctx: &mut EventCtx) {
        let ver = self.sort.version();
        if self.built && ver == self.last_version {
            return;
        }
        self.built = true;
        self.last_version = ver;
        let self_id = ctx.id();
        let tree = ctx.tree_mut();
        clear_children(tree, self_id);
        let order = sorted_order(&self.rows, self.sort.get());
        for (disp, &ri) in order.iter().enumerate() {
            // 选择按原始行下标 ri 绑定(排序后仍按身份跟随)。越界回退(长度不匹配时容错)。
            let Some(&row_sel) = self.sel.get(ri) else {
                continue;
            };
            let el = select_body_row(
                disp,
                ri,
                &self.rows[ri],
                &self.weights,
                row_sel,
                self.actions.as_ref(),
            );
            let id = el.build(tree);
            tree.add_child(self_id, id);
        }
    }

    fn measure(&self, _avail: Size, _style: &Style, _text: &mut dyn TextEngine) -> Size {
        Size::ZERO
    }
    fn paint(
        &self,
        _bounds: Rect,
        _content: Rect,
        _focused: bool,
        _enabled: bool,
        _canvas: &mut dyn Canvas,
        _style: &Style,
    ) {
    }
    fn on_event(&mut self, _ctx: &mut EventCtx, _ev: &Event) -> bool {
        false
    }
    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
        Some(self)
    }
}

/// 选择列宽度(供构造器对齐表头全选列与正文复选框列)。
pub(super) const fn select_col_w() -> i32 {
    SELECT_COL_W
}

/// 构造操作列配置(供 `Element::actions` 组装)。
pub(super) fn action_col(
    title: String,
    weight: f32,
    build: impl Fn(usize) -> Element + 'static,
) -> ActionCol {
    ActionCol {
        title,
        weight,
        build: Rc::new(build),
    }
}

/// 若 `el` 挂的是 `SortableHeader` 则设入操作列并返回 true,否则 false(供定位表头)。
pub(super) fn set_header_actions(el: &mut Element, ac: &ActionCol) -> bool {
    if let Some(a) = el.widget.as_any_mut() {
        if let Some(h) = a.downcast_mut::<SortableHeader>() {
            h.set_actions(ac.clone());
            return true;
        }
    }
    false
}

/// 若 `el` 挂的是任一响应式正文(Sortable/Paged/Selectable)则设入操作列并返回 true。
pub(super) fn set_body_actions(el: &mut Element, ac: &ActionCol) -> bool {
    let Some(a) = el.widget.as_any_mut() else {
        return false;
    };
    if let Some(b) = a.downcast_mut::<SortableBody>() {
        b.set_actions(ac.clone());
        return true;
    }
    if let Some(b) = a.downcast_mut::<PagedBody>() {
        b.set_actions(ac.clone());
        return true;
    }
    if let Some(b) = a.downcast_mut::<SelectableBody>() {
        b.set_actions(ac.clone());
        return true;
    }
    false
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::Tree;
    use crate::event::{MouseButton, PointerEvent, PointerKind};
    use crate::geometry::Point;
    use crate::signal::signal;

    fn rows(data: &[&[&str]]) -> Vec<Vec<String>> {
        data.iter()
            .map(|r| r.iter().map(|s| s.to_string()).collect())
            .collect()
    }

    /// 取第 `col` 列在给定行序下的值序列,便于断言。
    fn col_values(rows: &[Vec<String>], order: &[usize], col: usize) -> Vec<String> {
        order.iter().map(|&i| rows[i][col].clone()).collect()
    }

    #[test]
    fn sorted_order_numeric_column_compares_as_numbers() {
        // 数值列:"1280" < "20480" 数值序,而非 "1280" > "20480" 的字典序。
        let r = rows(&[&["a", "1280"], &["b", "3"], &["c", "20480"], &["d", "12"]]);
        let asc = sorted_order(&r, Some((1, SortOrder::Asc)));
        assert_eq!(col_values(&r, &asc, 1), ["3", "12", "1280", "20480"]);
        let desc = sorted_order(&r, Some((1, SortOrder::Desc)));
        assert_eq!(col_values(&r, &desc, 1), ["20480", "1280", "12", "3"]);
    }

    #[test]
    fn sorted_order_string_column_lexicographic() {
        let r = rows(&[&["banana"], &["apple"], &["cherry"]]);
        let asc = sorted_order(&r, Some((0, SortOrder::Asc)));
        assert_eq!(col_values(&r, &asc, 0), ["apple", "banana", "cherry"]);
    }

    #[test]
    fn sorted_order_none_keeps_original_and_is_stable() {
        let r = rows(&[&["x", "5"], &["y", "5"], &["z", "5"]]);
        // 无排序:原序。
        assert_eq!(sorted_order(&r, None), [0, 1, 2]);
        // 等值列升序:稳定,等值行保持原相对次序。
        assert_eq!(sorted_order(&r, Some((1, SortOrder::Asc))), [0, 1, 2]);
    }

    #[test]
    fn next_sort_cycles_none_asc_desc_none() {
        assert_eq!(next_sort(None, 0), Some((0, SortOrder::Asc)));
        assert_eq!(
            next_sort(Some((0, SortOrder::Asc)), 0),
            Some((0, SortOrder::Desc))
        );
        assert_eq!(next_sort(Some((0, SortOrder::Desc)), 0), None);
        // 点另一列:从该列升序重新开始(不继承前列方向)。
        assert_eq!(
            next_sort(Some((0, SortOrder::Desc)), 1),
            Some((1, SortOrder::Asc))
        );
    }

    /// 布局一个 400×300 的可排序表格,返回 tree。
    fn layout(el: Element) -> Tree {
        let mut tree = Tree::new();
        let root = el.build(&mut tree);
        tree.root = Some(root);
        tree.layout_root(Size::new(400, 300), &mut crate::text::NullTextEngine);
        tree
    }

    /// 合成一次完整点击(Down→Up)于 `at`。
    fn click(tree: &mut Tree, at: Point) {
        let mut hover = None;
        let mut capture = None;
        tree.dispatch_pointer(
            PointerEvent::single(PointerKind::Down, at, MouseButton::Left),
            &mut hover,
            &mut capture,
        );
        tree.dispatch_pointer(
            PointerEvent::single(PointerKind::Up, at, MouseButton::Left),
            &mut hover,
            &mut capture,
        );
    }

    #[test]
    fn clicking_header_advances_sort_signal() {
        let sort = signal(None);
        let mut tree = layout(
            Element::table_sortable(
                vec![("名称", 2.0), ("大小", 1.0)],
                vec![vec!["a", "2"], vec!["b", "1"]],
                sort,
            )
            .width(400)
            .height(300),
        );
        // 首列表头在左上(含内边距),点击落在其可点击区。
        click(&mut tree, Point::new(40, 18));
        assert_eq!(sort.get(), Some((0, SortOrder::Asc)), "首次点击→升序");
        // 再次布局让响应式表头/正文按新状态重建,再点同列。
        tree.layout_root(Size::new(400, 300), &mut crate::text::NullTextEngine);
        click(&mut tree, Point::new(40, 18));
        assert_eq!(sort.get(), Some((0, SortOrder::Desc)), "再点同列→降序");
        tree.layout_root(Size::new(400, 300), &mut crate::text::NullTextEngine);
        click(&mut tree, Point::new(40, 18));
        assert_eq!(sort.get(), None, "三点同列→取消排序");
    }

    #[test]
    fn body_rebuilds_row_count_after_sort_change() {
        let sort = signal(Some((0usize, SortOrder::Asc)));
        let mut tree = layout(
            Element::table_sortable(
                vec![("名称", 1.0)],
                vec![vec!["c"], vec!["a"], vec!["b"]],
                sort,
            )
            .width(400)
            .height(300),
        );
        // 改排序方向 → 下次布局触发正文 on_update 重建(clear+rebuild),不 panic 即路径健康;
        // 行序正确性由 sorted_order 单测覆盖。
        sort.set(Some((0, SortOrder::Desc)));
        tree.layout_root(Size::new(400, 300), &mut crate::text::NullTextEngine);
    }

    #[test]
    fn resolve_sort_style_falls_back_to_theme_defaults() {
        // 无覆盖:回退主题/内置默认(▲/▼、10px、槽 14、间距 2、右侧、颜色随主题)。
        let rs = resolve_sort_style(&SortStyle::default());
        assert_eq!(rs.asc, "\u{25B2}");
        assert_eq!(rs.desc, "\u{25BC}");
        assert_eq!(rs.size, 10.0);
        assert_eq!(rs.slot, 14);
        assert_eq!(rs.gap, 2);
        assert!(!rs.leading);
        assert!(
            rs.color.is_none(),
            "默认颜色应为 None(用 TextMuted 角色随主题)"
        );
    }

    #[test]
    fn resolve_sort_style_instance_override_wins() {
        // 实例覆盖优先于主题/默认。
        let ov = SortStyle {
            asc: Some("".into()),
            desc: Some("".into()),
            size: Some(14.0),
            slot: Some(20),
            gap: Some(6),
            leading: Some(true),
            color: Some(Color::hex(0xFF0000)),
        };
        let rs = resolve_sort_style(&ov);
        assert_eq!(rs.asc, "");
        assert_eq!(rs.desc, "");
        assert_eq!(rs.size, 14.0);
        assert_eq!(rs.slot, 20);
        assert_eq!(rs.gap, 6);
        assert!(rs.leading);
        assert_eq!(rs.color, Some(Color::hex(0xFF0000)));
    }

    #[test]
    fn sort_indicator_builder_reaches_header_widget() {
        // .sort_indicator(..) 应能定位表头并设入覆盖,且首次布局用该覆盖构建(不 panic 即链路通)。
        let sort = signal(Some((0usize, SortOrder::Asc)));
        let el = Element::table_sortable(
            vec![("名称", 1.0), ("大小", 1.0)],
            vec![vec!["a", "2"], vec!["b", "1"]],
            sort,
        )
        .sort_indicator(SortStyle {
            asc: Some("".into()),
            leading: Some(true),
            ..Default::default()
        })
        .width(400)
        .height(300);
        let _tree = layout(el); // 触发首次 on_update:用覆盖样式构建表头单元格
    }

    #[test]
    fn reactive_table_scrolls_on_wheel() {
        // 回归:响应式表格的滚动容器须保留内置 ScrollWidget(滚轮 + 滚动条),
        // 正文 widget 挂在其内部 col 上——否则替换 scroll 的 widget 会丢滚轮/拖动能力。
        let sort = signal(None);
        let rows: Vec<Vec<String>> = (0..20).map(|i| vec![format!("r{i}")]).collect();
        let mut tree = layout(
            Element::table_sortable(vec![("", 1.0)], rows, sort)
                .width(400)
                .height(200),
        );
        // 结构:col[header, divider, scroll];scroll 保留 ScrollWidget。
        let root = tree.root.unwrap();
        let scroll = *tree.get(root).unwrap().children.last().unwrap();
        assert!(
            tree.scroll_range(scroll).is_some_and(|(_, max)| max > 0),
            "20 行应溢出,正文应可滚"
        );
        // 在正文区域派发向下滚轮(delta<0),正文应滚动。
        let mut h = None;
        let mut cap = None;
        tree.dispatch_pointer(
            PointerEvent::single(
                PointerKind::Wheel(-120),
                Point::new(80, 120),
                MouseButton::Left,
            ),
            &mut h,
            &mut cap,
        );
        tree.layout_root(Size::new(400, 300), &mut crate::text::NullTextEngine);
        assert!(
            tree.get(scroll).unwrap().scroll_y > 0,
            "滚轮应滚动响应式表格正文(ScrollWidget 未被替换)"
        );
    }

    #[test]
    fn clicking_row_checkbox_toggles_that_rows_selection() {
        let sort = signal(None);
        let sel: Vec<Signal<bool>> = (0..2).map(|_| signal(false)).collect();
        let mut tree = layout(
            Element::table_selectable(
                vec![("名称", 2.0), ("大小", 1.0)],
                vec![vec!["a", "2"], vec!["b", "1"]],
                sel.clone(),
                sort,
            )
            .width(400)
            .height(300),
        );
        assert!(!sel[0].get());
        // 首列复选框(x∈[0,40],首行在表头/分隔线之下约 y≈58)。
        click(&mut tree, Point::new(20, 58));
        assert!(sel[0].get(), "点击首行复选框应选中原始行 0");
        assert!(!sel[1].get(), "不应影响其它行");
    }

    #[test]
    fn select_all_selects_then_clears() {
        let sort = signal(None);
        let sel: Vec<Signal<bool>> = (0..3).map(|_| signal(false)).collect();
        let mut tree = layout(
            Element::table_selectable(
                vec![("名称", 1.0)],
                vec![vec!["a"], vec!["b"], vec!["c"]],
                sel.clone(),
                sort,
            )
            .width(400)
            .height(300),
        );
        // 全选框在表头首列(x∈[0,40],y≈19)。
        click(&mut tree, Point::new(20, 19));
        assert!(sel.iter().all(|s| s.get()), "点全选应选中所有行");
        click(&mut tree, Point::new(20, 19));
        assert!(sel.iter().all(|s| !s.get()), "再点全选应清空");
    }

    #[test]
    fn selection_tracks_original_row_across_sort() {
        // 选择按原始行身份绑定:选中某行后排序重排,该行仍选中(不随显示位置漂移)。
        let sort = signal(None);
        let sel: Vec<Signal<bool>> = (0..3).map(|_| signal(false)).collect();
        let mut tree = layout(
            Element::table_selectable(
                vec![("v", 1.0)],
                vec![vec!["3"], vec!["1"], vec!["2"]], // 原始行 0/1/2 值 3/1/2
                sel.clone(),
                sort,
            )
            .width(400)
            .height(300),
        );
        sel[0].set(true); // 选中原始行 0(值 3)
        sort.set(Some((0, SortOrder::Asc))); // 升序:显示序 1,2,3 → 行0 落到末尾
        tree.layout_root(Size::new(400, 300), &mut crate::text::NullTextEngine);
        assert!(
            sel[0].get(),
            "排序后原始行 0 仍选中(选择按身份跟随,不随位置)"
        );
        assert!(!sel[1].get() && !sel[2].get());
    }

    #[test]
    fn hovering_body_row_requests_repaint() {
        // 悬停正文行:Move 派发经祖先链到达行的 HoverRow → 置 hover + 请求重绘。
        let sort = signal(Some((0usize, SortOrder::Asc)));
        let mut tree = layout(
            Element::table_sortable(
                vec![("名称", 2.0), ("大小", 1.0)],
                vec![vec!["a", "2"], vec!["b", "1"]],
                sort,
            )
            .width(400)
            .height(300),
        );
        let mut hover = None;
        let mut capture = None;
        // 移到正文首行区域(表头约 38px 高,行在其下)。
        let res = tree.dispatch_pointer(
            PointerEvent::single(PointerKind::Move, Point::new(80, 70), MouseButton::Left),
            &mut hover,
            &mut capture,
        );
        assert!(res.repaint, "悬停正文行应触发重绘(整行高亮淡入)");
        assert!(hover.is_some(), "应有命中的悬停节点");
    }

    #[test]
    fn header_cells_keep_weighted_width_after_rebuild() {
        // 回归:点表头触发响应式重建后,表头单元格必须保留比例宽度
        // (weight→主轴维度)。曾因重建绕过父级 build 循环导致首格占满、其余消失。
        let sort = signal(None);
        let mut tree = layout(
            Element::table_sortable(
                vec![("A", 2.0), ("B", 1.0), ("C", 1.5)],
                vec![vec!["a", "b", "c"]],
                sort,
            )
            .width(400)
            .height(300),
        );
        click(&mut tree, Point::new(40, 18)); // 触发排序变更 → 重建表头
        tree.layout_root(Size::new(400, 300), &mut crate::text::NullTextEngine);
        let root = tree.root.unwrap();
        let header_id = tree.get(root).unwrap().children[0];
        let cells = tree.get(header_id).unwrap().children.clone();
        assert_eq!(cells.len(), 3, "重建后表头应仍有 3 个单元格");
        let widths: Vec<Dimension> = cells.iter().map(|&c| tree.get(c).unwrap().width).collect();
        assert_eq!(
            widths,
            vec![
                Dimension::Weight(2.0),
                Dimension::Weight(1.0),
                Dimension::Weight(1.5)
            ],
            "重建后各表头单元格应保持比例宽度"
        );
    }

    /// 从根 DFS 累加各层局部 bounds 偏移,求目标节点的**绝对**中心点(bounds 是局部坐标,
    /// 需累加祖先 origin 才对得上 hit_test 的绝对命中)。找不到返回 None。
    fn abs_center(tree: &Tree, target: crate::core::NodeId) -> Option<Point> {
        fn walk(
            tree: &Tree,
            id: crate::core::NodeId,
            ox: i32,
            oy: i32,
            target: crate::core::NodeId,
        ) -> Option<Point> {
            let n = tree.get(id)?;
            let (x, y) = (ox + n.bounds.x, oy + n.bounds.y);
            if id == target {
                return Some(Point::new(x + n.bounds.w / 2, y + n.bounds.h / 2));
            }
            for &c in &n.children {
                if let Some(p) = walk(tree, c, x, y, target) {
                    return Some(p);
                }
            }
            None
        }
        walk(tree, tree.root?, 0, 0, target)
    }

    #[test]
    fn actions_column_adds_header_and_body_cells() {
        // .actions 应给表头与每个正文行各追加一个操作单元格(数据列数 + 1)。
        let sort = signal(None);
        let tree = layout(
            Element::table_sortable(
                vec![("A", 1.0), ("B", 1.0)],
                vec![vec!["a", "b"], vec!["c", "d"]],
                sort,
            )
            .actions("操作", 1.0, |_row| Element::label("·"))
            .width(400)
            .height(300),
        );
        let root = tree.root.unwrap();
        let header = tree.get(root).unwrap().children[0];
        assert_eq!(
            tree.get(header).unwrap().children.len(),
            3,
            "表头应为 2 数据列 + 1 操作列"
        );
        let scroll = *tree.get(root).unwrap().children.last().unwrap();
        let body = tree.get(scroll).unwrap().children[0]; // 内层 col(挂 SortableBody)
        let first_row = tree.get(body).unwrap().children[0]; // col[tr, divider]
        let tr = tree.get(first_row).unwrap().children[0];
        assert_eq!(
            tree.get(tr).unwrap().children.len(),
            3,
            "正文行应为 2 数据单元格 + 1 操作单元格"
        );
    }

    #[test]
    fn action_button_reports_original_row_after_sort() {
        // 关键:操作按钮回调按**原始行下标**绑定——排序打乱显示序后,点某显示行的按钮
        // 仍回报该行的原始下标(可直接用作数据/选择索引)。
        use std::cell::Cell as StdCell;
        use std::rc::Rc;
        let sort = signal(Some((0usize, SortOrder::Asc)));
        let seen: Rc<StdCell<Option<usize>>> = Rc::new(StdCell::new(None));
        let seen_c = seen.clone();
        let mut tree = layout(
            Element::table_sortable(
                vec![("v", 1.0)],
                vec![vec!["3"], vec!["1"], vec!["2"]], // 原始行 0/1/2 = 值 3/1/2
                sort,
            )
            .actions("op", 1.0, move |row| {
                let s = seen_c.clone();
                Element::row()
                    .width(120)
                    .height(24)
                    .clickable()
                    .on_click(move |_| s.set(Some(row)))
                    .child(Element::label("x"))
            })
            .width(400)
            .height(300),
        );
        // 升序显示:值 1,2,3 → 显示序 = 原始行 1,2,0。首个显示行是原始行 1。
        let root = tree.root.unwrap();
        let scroll = *tree.get(root).unwrap().children.last().unwrap();
        let body = tree.get(scroll).unwrap().children[0];
        let first_row = tree.get(body).unwrap().children[0];
        let tr = tree.get(first_row).unwrap().children[0];
        // 操作单元格为该行末子(数据列之后);其内层为可点击控件。
        let action = *tree.get(tr).unwrap().children.last().unwrap();
        let clickable = tree.get(action).unwrap().children[0];
        let at = abs_center(&tree, clickable).unwrap();
        click(&mut tree, at);
        assert_eq!(
            seen.get(),
            Some(1),
            "点首个显示行的操作按钮应回报其原始行下标 1"
        );
    }

    #[test]
    fn server_mode_click_updates_sort_and_fires_callback() {
        use std::cell::Cell as StdCell;
        use std::rc::Rc;
        let sort = signal(None);
        let rows = signal(vec![vec!["a".to_string(), "2".to_string()]]);
        // 记录回调收到的排序意图(应与 sort 同步)。
        let seen: Rc<StdCell<Option<(usize, SortOrder)>>> = Rc::new(StdCell::new(None));
        let fired = Rc::new(StdCell::new(0u32));
        let (seen_c, fired_c) = (seen.clone(), fired.clone());
        let mut tree = layout(
            Element::table_sortable_server(
                vec![("名称", 2.0), ("大小", 1.0)],
                rows,
                sort,
                move |_ctx, new_sort| {
                    seen_c.set(new_sort);
                    fired_c.set(fired_c.get() + 1);
                },
            )
            .width(400)
            .height(300),
        );
        // 点首列表头:更新 sort → 升序,并触发回调携带同一值。
        click(&mut tree, Point::new(40, 18));
        assert_eq!(sort.get(), Some((0, SortOrder::Asc)), "sort 信号更新");
        assert_eq!(fired.get(), 1, "on_sort 回调被触发一次");
        assert_eq!(seen.get(), Some((0, SortOrder::Asc)), "回调收到新排序意图");
    }

    #[test]
    fn server_mode_body_renders_backend_order_without_internal_sort() {
        // 服务端模式:正文按数据信号给定顺序渲染,不做内部排序。
        // 给一份「已按后端逆序」的数据,前端应原样显示(若误做内部排序会被打乱)。
        let sort = signal(Some((0usize, SortOrder::Asc)));
        let rows = signal(vec![
            vec!["c".to_string()],
            vec!["b".to_string()],
            vec!["a".to_string()],
        ]);
        let mut tree = layout(
            Element::table_sortable_server(vec![("名称", 1.0)], rows, sort, |_, _| {})
                .width(400)
                .height(300),
        );
        // 应用换页/换排序:写回新一页数据 → 下次布局触发 PagedBody 重建,不 panic 即路径健康。
        rows.set(vec![vec!["z".to_string()], vec!["y".to_string()]]);
        tree.layout_root(Size::new(400, 300), &mut crate::text::NullTextEngine);
    }
}