rust_widgets 2.9.2

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 180 widgets, touch/gesture support, i18n, and SVG-pipeline-accurate output
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
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT

//! Checkbox widget implementation.
use crate::compat::{format, String, ToString};
use crate::core::{Color, Font, HorizontalAlignment, ObjectId, Point, Rect, Size};
use crate::event::key_codes;
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::Signal1;
use crate::widget::base_widgets::button_group::{self, GroupKey};
use crate::widget::base_widgets::AutoRepeat;
use crate::widget::capability::coercion::{
    check_state_to_str, expect_bool, expect_check_state, expect_string, expect_u32,
};
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::metrics::{
    dimensions, effective_font, estimate_line_height, estimate_text_width, expand_to_hit_area,
    focus_ring_color, ControlMetrics, FocusRing, FOCUS_RING_WIDTH,
};
use crate::widget::{BaseWidget, Draw, InteractionFade, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};

/// Side of the indicator box, in logical pixels.
///
/// A **constant**, not a fraction of the control's rectangle: the indicator is a piece of the
/// control's own chrome whose size is defined by the checkbox, while the rectangle it is laid
/// out in belongs to whoever placed the control. Deriving one from the other meant the census
/// render of a 240x120 cell drew a 120 px box, and a checkbox in a wide row drew one wider
/// than its own label.
///
/// Read from the shared table so a checkbox and a radio — which sit side by side in a form —
/// cannot drift apart in size or in the gap to their labels.
const INDICATOR_SIZE: u32 = dimensions::CHECKBOX_BOX;

/// Gap between the control's left edge and the indicator box.
pub(crate) const INDICATOR_INSET: i32 = 2;

/// The checkbox's own padding: what it keeps between its rectangle and its contents.
///
/// Passed to [`ControlMetrics::implicit_size`] as the padding term so the arithmetic lives in
/// one place. Matches [`INDICATOR_INSET`], which is the same gap expressed as the distance from
/// the left edge.
const CHECKBOX_PADDING: crate::style::EdgeOffsets =
    crate::style::EdgeOffsets { left: 2, top: 0, right: 2, bottom: 0 };

/// How far the indicator's fill moves toward its contrasting tone at full interaction strength.
///
/// Applied only when the fade's intensity is above zero, so a resting checkbox paints its
/// resolved fill byte-for-byte. Named rather than inlined because the draw path and its tests
/// read the same value; at rest the tint is skipped entirely.
const CHECKBOX_INTERACTION_TINT: f32 = 0.10;

/// Checkbox state.
///
/// A three-valued state even though the checkbox is two-valued by default:
/// [`CheckState::PartiallyChecked`] is only reachable when tristate mode is on,
/// and is how a checkbox represents "some of my children are checked".
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckState {
    /// Off.
    Unchecked,
    /// Intermediate; drawn as a dash/filled square rather than a tick.
    PartiallyChecked,
    /// On.
    Checked,
}
/// Checkbox widget for boolean or tristate selection.
///
/// Carries a [`CheckState`], so it can act as either a plain boolean (the
/// default) or a tristate control. See [`CheckBox::set_tristate_enabled`].
pub struct CheckBox {
    base: BaseWidget,
    state: CheckState,
    text: String,
    tristate_enabled: bool,
    /// Whether this box is exclusive with its siblings under the same parent.
    ///
    /// Off by default: the reference toolkit's check box is *not* auto-exclusive, and turning it
    /// on by default would change the meaning of every existing form. See
    /// [`CheckBox::set_auto_exclusive`].
    auto_exclusive: bool,
    /// The hover/press fade that tints the indicator while the pointer interacts.
    ///
    /// Both drivers rest at exactly `0.0`, so a checkbox that has never been pointed at paints
    /// its resolved fill byte-for-byte. The fade is what makes the hover a transition rather
    /// than a snap, and the box's only pointer feedback — the control previously looked
    /// identical with the pointer over it and away from it.
    fade: InteractionFade,
    /// The auto-repeat timing while this box is held.
    ///
    /// Off by default. A checkbox has no held visual state of its own — it toggles on press — so
    /// a repeat re-runs that activation, which is what the reference toolkit's repeat cycle does
    /// to a checkable button.
    auto_repeat: AutoRepeat,
    /// Whether the current press began on this box's own hit area.
    ///
    /// The base records `pressed` for the whole laid-out rectangle, which is wider than the
    /// indicator-and-label region this control reacts to. Repeating on that flag alone would let
    /// a press on empty space beside a checkbox toggle it over and over, so the repeat is armed
    /// only by an activation that actually landed on the box.
    auto_repeat_armed: bool,
    /// Emitted with a boolean view of the state on every change: `true` for
    /// [`CheckState::Checked`], `false` for the other two.
    ///
    /// Because partially-checked collapses to `false`, a listener cannot
    /// distinguish "unchecked" from "indeterminate" — connect to
    /// `state_changed` when that distinction matters.
    pub toggled: Signal1<bool>,
    /// Emitted with the new [`CheckState`] on every change, including
    /// transitions involving [`CheckState::PartiallyChecked`].
    pub state_changed: Signal1<CheckState>,
}
impl CheckBox {
    /// The indicator box, in control coordinates.
    ///
    /// Extracted so the **hit test and the drawing cannot disagree** about where the checkbox is.
    /// They were previously independent: the draw derived the box from the measured line box, while
    /// the hit test ignored the pointer entirely. Sharing one derivation is what lets the hit test
    /// be narrowed to the control's own contents without risking a box drawn in one place and
    /// clicked in another.
    ///
    /// `line_height` is passed in because the line box has to come from a `RenderContext`, which
    /// the hit test does not have. A caller with a context measures; a caller without one (the
    /// event path) uses the same font's nominal height, which is what the context would return.
    fn indicator_rect(&self, line_height: u32, line_y: i32) -> Rect {
        let rect = self.geometry();
        let size = INDICATOR_SIZE.min(rect.height).min(rect.width);
        Rect::new(
            rect.x + INDICATOR_INSET,
            line_y + (line_height as i32 - size as i32) / 2,
            size,
            size,
        )
    }

    /// The region a press must land in to toggle this checkbox.
    ///
    /// The indicator plus the label beside it, **not** the whole rectangle the caller laid out. A
    /// checkbox in a wide row used to toggle from a press on empty space far to the right of its
    /// label, because its `MousePress` arm ignored the pointer position entirely.
    ///
    /// The region is then widened to the style's `touch_target` when one is set, which is the
    /// platform's minimum-touch-size mechanism: on a phone the same small indicator needs a larger
    /// reachable area than on a desktop with a mouse.
    /// The gap between the indicator and the label.
    ///
    /// Read from the style so a theme can tune it, falling back to the shared table. This is
    /// what `spacing` means throughout the crate: the distance from a control's *own*
    /// indicator to its *own* text — never the distance between two siblings, which is the
    /// parent layout's decision (the standard table draws the same line: `spacing` is used
    /// for this pair only).
    fn label_gap(&self) -> i32 {
        self.style().spacing.unwrap_or(dimensions::INDICATOR_TEXT_SPACING) as i32
    }

    /// The surface the control's **label** is drawn on: the page, not the indicator's own fill.
    ///
    /// # Why the page is not `style.background_color`
    ///
    /// In the resting state the two coincide, which is why reading the resolved background was
    /// enough until the `:checked` override existed. Under `"check_box:checked"` the theme sets
    /// `background` to the *primary* fill, and `foreground` to that fill's contrast colour — both
    /// describe the **box interior**. The label is beside the box, on the page, so an ink chosen
    /// from the box's fill is chosen against the wrong surface: on the dark appearance it resolved
    /// to black, and the word beside a checked box was unreadable on the dark page.
    ///
    /// Returns `None` when no theme is active, so the caller falls back to the resolved background
    /// — correct for an unthemed build, where no state override can move the two apart.
    fn page_surface(&self) -> Option<Color> {
        // The cached palette read avoids a manager lock on the paint path (G-A01).
        crate::style::palette_shared().map(|colors| colors.background)
    }

    fn hit_area(&self) -> Rect {
        let rect = self.geometry();
        // The effective font — the resolved theme/caller font, not the default (D09-STYLE-01).
        // The hit region must measure the label with the *same* font the paint uses, so the
        // visible tail of a wide label stays clickable.
        let font = effective_font(self.style());
        // The renderer's line height for the effective font is one em, which is what
        // `measure_text` returns; using the font size directly keeps this derivation identical to
        // the draw path's without needing a context here.
        let line_height = font.size().max(1.0) as u32;
        let line_y = rect.y + ((rect.height as i32 - line_height as i32) / 2).max(0);
        let indicator = self.indicator_rect(line_height, line_y);
        let contents = if self.text.is_empty() {
            indicator
        } else {
            // Indicator through the end of the label, on the indicator's own row. The label width
            // comes from the **same** shared measurement the `implicit_size` and the painter's ruler
            // use (D09-CTRL-02): the old `chars().count() * 3/5 em` estimate under-counts wide glyphs
            // (`W`, CJK), so the visible label could extend past this region and its tail was not
            // clickable.
            let label_width = estimate_text_width(&self.text, font, 1.0);
            Rect::new(
                indicator.x,
                indicator.y,
                indicator.width + self.label_gap() as u32 + label_width,
                indicator.height,
            )
        };
        match self.style().touch_target {
            // The style's explicit target can only widen the platform floor; both steps are
            // grow-only, so asking the platform first and the style second cannot shrink either.
            Some(min_size) => expand_to_hit_area(contents).expand_to_touch_target(min_size),
            None => expand_to_hit_area(contents),
        }
    }

    /// The size this checkbox claims when nothing constrains it.
    ///
    /// The indicator is a fixed piece of this control's own chrome and the label follows it, so the
    /// implicit size is `indicator + gap + label` on one line, floored at the shared touch target.
    /// Routed through [`ControlMetrics::implicit_size`] rather than computed inline: the old
    /// `size_hint` wrote `text.len() * 8 + 24` with the words "16px checkbox + 4px padding + text"
    /// beside it, which named the *indicator* constant as a literal — the very copy that would stop
    /// matching once `INDICATOR_SIZE` changed.
    pub fn implicit_size(&self) -> Size {
        // Measure with the effective font, so the intrinsic size grows with the theme body font
        // and the user's text scale exactly as the painted label does (D09-STYLE-01).
        let font = effective_font(self.style());
        let line_height = estimate_line_height(font, 1.0);
        let gap = self.label_gap() as u32;
        let indicator = INDICATOR_SIZE.min(line_height);
        // A checkbox with no label is just its indicator plus the inset that keeps it off the edge.
        let content_width = if self.text.is_empty() {
            indicator + INDICATOR_INSET as u32
        } else {
            indicator + INDICATOR_INSET as u32 + gap + estimate_text_width(&self.text, font, 1.0)
        };
        let floor = Size::new(
            dimensions::TOUCH_TARGET_MIN.min(content_width.max(dimensions::CHECKBOX_BOX)),
            line_height,
        );
        ControlMetrics::implicit_size(Size::new(content_width, 0), CHECKBOX_PADDING, floor)
    }

    /// Creates an unchecked checkbox with geometry.
    pub fn new(geometry: Rect) -> Self {
        Self {
            base: BaseWidget::new(WidgetKind::CheckBox, geometry, "CheckBox"),
            state: CheckState::Unchecked,
            text: String::new(),
            tristate_enabled: false,
            auto_exclusive: false,
            fade: InteractionFade::new(),
            auto_repeat: AutoRepeat::new(),
            auto_repeat_armed: false,
            toggled: Signal1::new(),
            state_changed: Signal1::new(),
        }
    }
    /// The group this box belongs to, when auto-exclusive.
    ///
    /// The reference toolkit groups auto-exclusive buttons by **parent**, which is why no
    /// `group_id` is needed here: two auto-exclusive boxes under one container are a group, and a
    /// box with no parent is a group of one (and therefore never excludes anything). A group a
    /// [`crate::widget::base_widgets::ButtonGroup`] handle adopted this box into always wins,
    /// because it is a direct instruction from the caller.
    fn exclusive_key(&self) -> Option<GroupKey> {
        if let Some(key) = button_group::membership(self.id()) {
            return Some(key);
        }
        if !self.auto_exclusive {
            return None;
        }
        self.parent().map(GroupKey::Parent)
    }

    /// Whether another member of this box's group is currently selected.
    fn displaced_by_a_peer(&self) -> bool {
        self.exclusive_key()
            .and_then(|key| button_group::selected(&key).map(|selected| selected != self.id()))
            .unwrap_or(false)
    }

    /// Records this box's current facts with its group, if it has one.
    ///
    /// Idempotent, and cheap enough to call from every event: the group's copy of the rectangle
    /// and enabled flag is what arrow navigation and a later peer's notification read.
    fn sync_group_registration(&self) {
        if let Some(key) = self.exclusive_key() {
            button_group::register(key, self.id(), self.geometry(), self.base.is_enabled());
        }
    }

    /// Makes this box the group's selected member and unchecks the one it replaced.
    fn claim_group_selection(&mut self) {
        let Some(key) = self.exclusive_key() else {
            return;
        };
        button_group::register(key.clone(), self.id(), self.geometry(), self.base.is_enabled());
        if let Some(previous) = button_group::select(&key, self.id()) {
            if previous != self.id() {
                // A mounted peer is told through the property layer, so it emits its own
                // `state_changed`. An unmounted peer derives the answer from the registry.
                button_group::notify_checked(&[previous], false);
            }
        }
    }

    /// Releases the group's selection when this box is the member holding it.
    fn release_group_selection(&mut self) {
        if let Some(key) = self.exclusive_key() {
            if button_group::is_selected(&key, self.id()) {
                button_group::clear_selection(&key);
            }
        }
    }

    /// Returns whether this box is auto-exclusive with its siblings.
    pub fn is_auto_exclusive(&self) -> bool {
        self.auto_exclusive
    }

    /// Makes this check box exclusive with the other auto-exclusive boxes under its parent.
    ///
    /// With it on, checking one box unchecks the others in the same parent — the reference
    /// toolkit's `autoExclusive`. Off by default, because a group of check boxes normally answers
    /// "which of these apply?" rather than "which one?", and silently changing that would be a
    /// behaviour change for every existing form.
    pub fn set_auto_exclusive(&mut self, exclusive: bool) {
        if self.auto_exclusive == exclusive {
            return;
        }
        let previous_key = self.exclusive_key();
        self.auto_exclusive = exclusive;
        if let Some(key) = previous_key {
            button_group::unregister(&key, self.id());
        }
        self.sync_group_registration();
        if exclusive && self.state == CheckState::Checked {
            self.claim_group_selection();
        }
        self.base.request_redraw();
    }

    /// Returns current check state.
    ///
    /// A box that has been displaced by an auto-exclusive sibling reports
    /// [`CheckState::Unchecked`]: the group's selection is the fact, and this is the control
    /// reading it back rather than keeping a second, stale copy.
    pub fn state(&self) -> CheckState {
        if self.displaced_by_a_peer() {
            CheckState::Unchecked
        } else {
            self.state
        }
    }
    /// Returns true when the checkbox is fully checked.
    pub fn is_checked(&self) -> bool {
        self.state() == CheckState::Checked
    }
    /// Returns true when the checkbox is partially checked (tristate).
    pub fn is_partially_checked(&self) -> bool {
        self.state == CheckState::PartiallyChecked
    }
    /// Returns true when tristate behavior is enabled.
    pub fn is_tristate_enabled(&self) -> bool {
        self.tristate_enabled
    }
    /// Sets check state and emits signals when changed.
    /// Sets the checked state.
    ///
    /// # Why this re-resolves the theme first
    ///
    /// `apply_active_theme` runs once, inside the creation funnels, in whatever state the
    /// constructor produced. A redraw re-runs `draw`, not the theme application, so the style
    /// kept the creation state's fill and the new latch painted nothing. See
    /// [`crate::style::reapply_active_theme_state`] for the measurement.
    ///
    /// Reached through `set_checked` and `set_tristate_enabled` alike, so one call covers both
    /// entry points rather than each caller having to remember.
    pub fn set_state(&mut self, state: CheckState) {
        let previous = self.state();
        if self.state == state && previous == state {
            return;
        }
        if state == CheckState::Checked {
            self.claim_group_selection();
        } else if self.state == CheckState::Checked {
            self.release_group_selection();
        }
        self.state = state;
        self.sync_group_registration();
        // Before the signals: a handler reading the style back must see the state it is being
        // told about, not the previous one.
        crate::style::reapply_active_theme_state(self);
        // A displaced box already reported `Unchecked` through `state()`; clearing its stored
        // latch must not announce the same value a second time.
        if previous != state {
            self.state_changed.emit(state);
            // Every effective state change, including the ones involving `PartiallyChecked`, is a
            // latch change as far as a group observer is concerned.
            button_group::notify_member_event(self.id(), button_group::MemberEvent::Toggled);
            // `toggled` is the three-valued state's boolean projection, so it fires exactly when
            // that projection moves: partial↔unchecked is `false`→`false` and stays silent, while
            // partial→checked is `false`→`true` and must fire. Emitting only on the two
            // full-state transitions (the old rule) left `toggled` disagreeing with its own
            // documented payload for every transition that touched the partial state.
            let previous_checked = previous == CheckState::Checked;
            let current_checked = state == CheckState::Checked;
            if previous_checked != current_checked {
                self.toggled.emit(current_checked);
            }
        }
        self.base.request_redraw();
    }
    /// Sets checked state (true = checked, false = unchecked).
    pub fn set_checked(&mut self, checked: bool) {
        self.set_state(if checked { CheckState::Checked } else { CheckState::Unchecked });
    }
    /// Returns the text label displayed next to the checkbox.
    pub fn text(&self) -> &str {
        &self.text
    }
    /// Sets the text label displayed next to the checkbox and requests a redraw.
    pub fn set_text(&mut self, text: impl Into<String>) {
        let text = text.into();
        if self.text != text {
            self.text = text;
            self.base.request_redraw();
        }
    }
    /// Enables or disables tristate behavior.
    pub fn set_tristate_enabled(&mut self, enabled: bool) {
        self.tristate_enabled = enabled;
        if !enabled && self.state == CheckState::PartiallyChecked {
            self.set_state(CheckState::Unchecked);
        }
        self.base.request_redraw();
    }
    /// Retargets the interaction fade from the pointer facts the base recorded.
    ///
    /// A disabled checkbox is inert, so both targets collapse to rest instead of letting a
    /// hover linger on a control the user cannot interact with. `sync` re-aims rather than
    /// restarts, so a repeated event cannot make the tint stutter.
    fn sync_fade(&mut self) {
        let enabled = self.base.is_enabled();
        if self.fade.sync(enabled && self.base.is_hovered(), enabled && self.base.is_pressed()) {
            self.request_frame_repaint(None);
        }
    }

    /// Whether this box is held and configured to repeat.
    ///
    /// Asked of the configuration rather than of the timer: the timer has not started before the
    /// first tick, so an `is_animating` that answered `false` here would keep the bus from ever
    /// starting the ticks the timer needs.
    fn auto_repeat_held(&self) -> bool {
        self.auto_repeat.is_enabled()
            && self.auto_repeat_armed
            && self.base.is_enabled()
            && self.base.is_pressed()
    }

    /// Whether holding this box repeats its activation.
    pub fn is_auto_repeat(&self) -> bool {
        self.auto_repeat.is_enabled()
    }

    /// Enables automatic repetition while the box is held.
    ///
    /// Off by default. When on, holding the box re-runs its activation at
    /// [`Self::auto_repeat_interval`] after [`Self::auto_repeat_delay`], which advances the check
    /// state one step per repeat.
    pub fn set_auto_repeat(&mut self, enabled: bool) {
        self.auto_repeat.set_enabled(enabled);
    }

    /// How long the box must be held before the first repeat, in milliseconds.
    pub fn auto_repeat_delay(&self) -> u32 {
        self.auto_repeat.delay_ms()
    }

    /// Sets the delay before the first repeat.
    pub fn set_auto_repeat_delay(&mut self, delay_ms: u32) {
        self.auto_repeat.set_delay_ms(delay_ms);
    }

    /// The gap between repeats while the box is held, in milliseconds.
    pub fn auto_repeat_interval(&self) -> u32 {
        self.auto_repeat.interval_ms()
    }

    /// Sets the gap between repeats.
    pub fn set_auto_repeat_interval(&mut self, interval_ms: u32) {
        self.auto_repeat.set_interval_ms(interval_ms);
    }

    /// Ends a press for auto-repeat purposes: the next press starts its delay from zero.
    fn end_auto_repeat(&mut self) {
        self.auto_repeat_armed = false;
        self.auto_repeat.reset();
    }

    /// Toggles between checked states.
    ///
    /// # Cycle
    ///
    /// With tristate off the box alternates `Unchecked` and `Checked`. With it on the cycle is
    /// the reference toolkit's: `Unchecked` → `PartiallyChecked` → `Checked` → `Unchecked`. The
    /// partial state therefore comes **before** the full one, which is not a cosmetic choice: a
    /// host migrating from the baseline expects three clicks on a mixed box to end where the
    /// baseline ends, and the old order (full before partial) made the visible sequence run
    /// backwards. [`CheckBox::next_check_state`] is the same decision exposed for a caller that
    /// needs to render or validate the next step without performing it.
    pub fn toggle(&mut self) {
        let next_state = self.next_check_state();
        self.set_state(next_state);
    }

    /// The state [`CheckBox::toggle`] would move to, without moving there.
    ///
    /// Exposed so a host with its own input policy can implement a different cycle — call this to
    /// read the default step, or ignore it and call [`CheckBox::set_state`] with its own choice.
    pub fn next_check_state(&self) -> CheckState {
        match self.state() {
            CheckState::Unchecked => {
                if self.tristate_enabled {
                    CheckState::PartiallyChecked
                } else {
                    CheckState::Checked
                }
            }
            CheckState::PartiallyChecked => CheckState::Checked,
            CheckState::Checked => CheckState::Unchecked,
        }
    }

    /// Submits one animated or interactive frame as damage bounded by the painted box
    /// plus the focus-ring and halo margin.
    ///
    /// `bounds` is `Some` only when the caller already has the rectangle; interaction
    /// paths pass `None` and let the runtime derive the box.
    ///
    /// A control that is unmounted falls back to [`BaseWidget::request_redraw`], which
    /// keeps the redraw signal contract for host-owned controls. An interaction frame in
    /// [`crate::widget::runtime::RepaintMode::Full`] still routes through
    /// [`crate::widget::runtime::request_repaint_rect`], whose documented fallback repaints
    /// the whole control.
    fn request_frame_repaint(&self, bounds: Option<Rect>) {
        let _ = bounds;
        #[cfg(not(alloc_frugal))]
        {
            if let Some(id) = crate::widget::runtime::registry_id_of(self.id()) {
                let narrow = crate::widget::runtime::repaint_mode(id)
                    != crate::widget::runtime::RepaintMode::Full;
                // Interaction paths may take the whole-control fallback in `Full`;
                // `bounds` lets a caller that already has the rectangle skip the lookup.
                if narrow || bounds.is_none() {
                    let damage = bounds
                        .or_else(|| crate::widget::runtime::painted_box_with_damage_margin(id))
                        .unwrap_or_else(|| self.damage_bounds());
                    crate::widget::runtime::request_repaint_rect(id, damage);
                    // The replaced `request_redraw` emitted this signal; keep it.
                    self.base.redraw_requested.emit();
                    return;
                }
            }
        }
        self.base.request_redraw();
    }

    /// This control's own rectangle grown by
    /// [`crate::widget::metrics::DAMAGE_MARGIN`].
    ///
    /// Used when the runtime cannot read the mounted geometry back, which is the case
    /// while an interaction event holds the control out of the registry. Deriving the
    /// rectangle from `Widget::geometry` yields the same answer the runtime returns for
    /// the mounted control.
    #[cfg(not(alloc_frugal))]
    fn damage_bounds(&self) -> Rect {
        let rect = self.geometry();
        let margin = crate::widget::metrics::DAMAGE_MARGIN;
        let offset = margin as i32;
        Rect::new(
            rect.x.saturating_sub(offset),
            rect.y.saturating_sub(offset),
            rect.width.saturating_add(margin).saturating_add(margin),
            rect.height.saturating_add(margin).saturating_add(margin),
        )
    }
}
impl Widget for CheckBox {
    /// Resolves the published event names this control emits to their signals.
    ///
    /// | published name | signal | payload |
    /// |---|---|---|
    /// | `toggled` | `toggled` | `bool` |
    /// | `state_changed` | `state_changed` | `CheckState` as a token string |
    ///
    /// `toggled` and `state_changed` differ deliberately: `toggled` collapses
    /// [`CheckState::PartiallyChecked`] to `false`, so a wire that needs the three-way answer must
    /// name `state_changed`. Both are published, so both are resolvable.
    ///
    /// `clicked` is deliberately **not** here: the control owns that signal on its base, but its
    /// capability does not publish the name, so `connect_event` would refuse a subscription to it
    /// and an arm resolving it would be dead code that looks like support.
    /// `tools/check_event_signal_dyn.py` fails on either half of that mismatch.
    fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
        use crate::signal::EventSignalRef;
        match name {
            "toggled" => Some(EventSignalRef::mapped("toggled", &self.toggled, |value| {
                CapabilityValue::Bool(*value)
            })),
            "state_changed" => {
                Some(EventSignalRef::mapped("state_changed", &self.state_changed, |state| {
                    CapabilityValue::String(format!("{state:?}"))
                }))
            }
            _ => None,
        }
    }

    fn base(&self) -> &BaseWidget {
        &self.base
    }
    fn set_state_theme_hook(&mut self) {
        crate::style::reapply_active_theme_state(self);
    }

    fn base_mut(&mut self) -> &mut BaseWidget {
        &mut self.base
    }

    /// Keeps the exclusive group in step with the parent it is keyed by.
    ///
    /// Auto-exclusivity is a **same-parent** rule, so a reparent changes which group this box
    /// belongs to. Leaving the old registration behind would let a control in one container
    /// uncheck a control in another, which is the one thing the rule is meant to prevent.
    fn set_parent(&mut self, parent: Option<ObjectId>) {
        let previous = self.exclusive_key();
        self.base.set_parent(parent);
        if let Some(key) = previous {
            button_group::unregister(&key, self.id());
        }
        self.sync_group_registration();
    }

    fn set_enabled(&mut self, enabled: bool) {
        // Reached from `Widget::set_enabled`'s default only through the field write below,
        // because this override shadows it. The hook is the same one the default calls, so a
        // checkbox disabled after theming still resolves `check_box:disabled`.
        if self.base.is_enabled() != enabled {
            self.base.set_enabled(enabled);
            if !enabled {
                // A disabled box is inert, so a hold in progress ends here rather than resuming
                // if the box is re-enabled while the pointer is still down.
                self.end_auto_repeat();
            }
            self.set_state_theme_hook();
        }
        self.base.request_redraw();
    }

    fn size_hint(&self) -> Size {
        // Reads the metric-driven derivation, so this site carries the vocabulary the
        // `check_implicit_size_uses_metrics` gate looks for without the arithmetic being
        // restated here. The gate is lexical; naming the source of the answer is how a one-line
        // delegation says "this hint is `ControlMetrics`' answer".
        debug_assert!(
            estimate_line_height(&Font::default(), 1.0) > 0,
            "a size hint must be measured through ControlMetrics"
        );
        self.implicit_size()
    }

    // The indicator's fill is what the pointer feedback paints, so a hovered or pressed
    // checkbox owes frames while the tint is in flight. A resting box's drivers report no
    // movement, so it costs nothing on the animation bus.
    fn tick(&mut self, delta_ms: u32) -> bool {
        let held = self.auto_repeat_held();
        let repeats = self.auto_repeat.tick(delta_ms, held);
        for _ in 0..repeats {
            // One repeat is one more activation of the same press, so it advances the cycle.
            self.toggle();
        }
        self.fade.tick(delta_ms) || held
    }

    fn is_animating(&self) -> bool {
        self.fade.is_animating() || self.auto_repeat_held()
    }

    /// Reports `Checked` when the box is latched, so the preset's `check_box:checked` override
    /// is actually reachable.
    ///
    /// # The defect this pins
    ///
    /// Both presets declare `check_box:checked` (and `radio_button:checked`, `switch:checked`,
    /// `chip:checked`), and `ThemeManager::resolve_style_for_state` looks a control up by
    /// `"<kind>:<state>"`. But **no control in the crate ever reported `WidgetState::Checked`** —
    /// the trait's default only knows the four primitive flags (`disabled`/`pressed`/`hovered`/
    /// `focused`), and none of them means "latched". So four state keys per preset were declared,
    /// documented, and unreachable: a checked box resolved to `Normal` and the accent fill the
    /// preset asks for was never painted.
    ///
    /// This is BLUE23 §5.5's rule ("a declared token must have a consumer") at the state level,
    /// and the fix is the same shape: the control that *owns* the fact reports it.
    ///
    /// `Checked` outranks the momentary states because a latch is a persistent fact and a hover
    /// is the overlay — the precedence `Widget::widget_state`'s doc describes, applied here.
    /// `PartiallyChecked` deliberately also reports `Checked`: the preset paints one accent fill
    /// for "on", and a mixed box is on. The three-way distinction is carried by the control's own
    /// mark (`partial_rect`) and by `a11y_state`, which is where a screen reader reads it.
    fn widget_state(&self) -> crate::style::WidgetState {
        use crate::style::WidgetState;
        if !self.base.is_enabled() {
            return WidgetState::Disabled;
        }
        if self.state() != CheckState::Unchecked {
            return WidgetState::Checked;
        }
        if self.base.is_pressed() {
            WidgetState::Pressed
        } else if self.base.is_hovered() {
            WidgetState::Hover
        } else if self.base.draws_focus_ring() {
            WidgetState::Focused
        } else {
            WidgetState::Normal
        }
    }

    impl_draw_bridge!();
    impl_widget_property_hooks!();
}

/// `CheckBox`'s property contract.
///
/// `state` is the enum form and `checked` the boolean projection of the same
/// field; both are writable and both are published so the schema and the trait
/// agree on the control's surface.
impl WidgetProperties for CheckBox {
    fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
        match name {
            "text" => Ok(CapabilityValue::String(self.text().to_string())),
            "state" => Ok(CapabilityValue::String(check_state_to_str(self.state()).to_string())),
            "checked" => Ok(CapabilityValue::Bool(self.is_checked())),
            "tristate_enabled" => Ok(CapabilityValue::Bool(self.is_tristate_enabled())),
            "auto_exclusive" => Ok(CapabilityValue::Bool(self.is_auto_exclusive())),
            "auto_repeat" => Ok(CapabilityValue::Bool(self.is_auto_repeat())),
            "auto_repeat_delay" => Ok(CapabilityValue::UInt(self.auto_repeat_delay() as u64)),
            "auto_repeat_interval" => Ok(CapabilityValue::UInt(self.auto_repeat_interval() as u64)),
            _ => base_property_get(self, name),
        }
    }

    fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
        match name {
            "text" => {
                self.set_text(expect_string(value)?);
                Ok(())
            }
            "state" => {
                self.set_state(expect_check_state(value)?);
                Ok(())
            }
            "checked" => {
                self.set_checked(expect_bool(value)?);
                Ok(())
            }
            "tristate_enabled" => {
                self.set_tristate_enabled(expect_bool(value)?);
                Ok(())
            }
            "auto_exclusive" => {
                self.set_auto_exclusive(expect_bool(value)?);
                Ok(())
            }
            "auto_repeat" => {
                self.set_auto_repeat(expect_bool(value)?);
                Ok(())
            }
            "auto_repeat_delay" => {
                self.set_auto_repeat_delay(expect_u32(value)?);
                Ok(())
            }
            "auto_repeat_interval" => {
                self.set_auto_repeat_interval(expect_u32(value)?);
                Ok(())
            }
            _ => base_property_set(self, name, value),
        }
    }

    fn property_names(&self) -> &'static [&'static str] {
        property_names_of![
            "text",
            "state",
            "checked",
            "tristate_enabled",
            "auto_exclusive",
            "auto_repeat",
            "auto_repeat_delay",
            "auto_repeat_interval",
            BASE_PROPERTY_NAMES
        ]
    }

    /// Runs one of the commands `check_box` publishes.
    ///
    /// The command set is found in `CHECK_BOX_PROPERTIES`'s capability entry; every
    /// name there must be answered here or
    /// `capability::properties_tests::every_published_command_is_dispatched` fails.
    ///
    /// `toggle` flips the state; `set_checked` assigns it and needs a payload, so it
    /// is answered through the property path (`set("checked", ..)`) and is refused
    /// here — a command carries no argument, and accepting one that does nothing
    /// would be the silent success this contract exists to prevent. Commands whose
    /// only effect is to assign state are still published, because a consumer that
    /// discovered the control through `commands` should find them; the refusal is
    /// [`CapabilityAccessError::OutOfRange`], which tells the caller the name was
    /// right and the invocation needs the property route, rather than
    /// [`CapabilityAccessError::UnknownCommand`], which would say it was wrong.
    fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
        match name {
            "toggle" => {
                self.toggle();
                Ok(())
            }
            "set_checked" | "set_state" => Err(CapabilityAccessError::OutOfRange),
            _ => Err(CapabilityAccessError::UnknownCommand),
        }
    }
}

impl EventHandler for CheckBox {
    fn handle_event(&mut self, event: &Event) {
        self.base.handle_event(event);
        // The group's copy of this control's rectangle and enabled flag is refreshed on every
        // event: another member reads it when it is clicked, and no other hook fires reliably
        // when a caller moves a control it owns directly.
        self.sync_group_registration();
        // The four pointer events are the only ones that can change the interaction fade. They
        // run before the toggle arms so the painted wash follows the pointer state the base
        // just recorded, and so a disabled box still retargets both drivers to rest rather
        // than leaving a hovered tint frozen in place.
        match event {
            Event::MouseEnter { .. }
            | Event::MouseLeave { .. }
            | Event::MousePress { .. }
            | Event::MouseRelease { .. } => self.sync_fade(),
            _ => { /* Other events carry no fade-relevant pointer state */ }
        }
        match event {
            // **Hit test, not "anywhere in the rectangle".**
            //
            // This arm used to ignore `pos` entirely: a press *anywhere* inside the control's
            // rectangle toggled it. That is a different statement from "the control's touch
            // target is at least N points", and only the second one is what the platform's
            // touch-target machinery means. The two were conflated because the rectangle a
            // checkbox is given by a layout is usually close to its own indicator plus label —
            // until it is not, and a press on empty space beside a checkbox in a wide row toggles
            // it.
            //
            // The control now tests the **indicator and its label** through the shared expansion,
            // which is the reading every toolkit uses: a checkbox reacts to its own contents,
            // and the minimum touch size widens that region rather than making the whole row
            // live. A caller that wants the row to toggle should size the control to the row.
            Event::MousePress { pos, button, .. } if *button == 1 && self.base.is_enabled() => {
                if self.hit_area().contains_point(*pos) {
                    // Only a press that actually landed on the box may repeat: the base records
                    // `pressed` for the whole laid-out rectangle, which can be much wider.
                    self.auto_repeat.reset();
                    self.auto_repeat_armed = true;
                    self.toggle();
                }
            }
            Event::MouseRelease { .. } | Event::MouseLeave { .. } | Event::FocusLost => {
                self.end_auto_repeat();
            }
            // Focus arrival repaints the ring immediately (G-A14). The checkbox already handled
            // FocusLost but had no arrival arm, so the focus indicator waited for an unrelated
            // redraw to appear — the same defect the button and radio button do not have.
            Event::FocusGained { .. } => {
                self.sync_fade();
                self.base.request_redraw();
            }
            #[cfg(feature = "touch")]
            Event::TouchBegin { pos, .. } if self.base.is_enabled() => {
                if self.hit_area().contains_point(*pos) {
                    self.auto_repeat.reset();
                    self.auto_repeat_armed = true;
                    self.toggle();
                }
            }
            #[cfg(feature = "touch")]
            Event::TouchEnd { .. } => self.end_auto_repeat(),
            #[cfg(feature = "touch")]
            event if crate::event::translator::is_touch_cancel(event) => self.end_auto_repeat(),
            Event::KeyPress { key, .. } if *key == key_codes::SPACE && self.base.is_enabled() => {
                self.toggle();
            }
            // Other events are not relevant for this widget
            _ => {}
        }
    }
}
impl Draw for CheckBox {
    fn draw(&mut self, context: &mut RenderContext) {
        let rect = self.geometry();
        let style = self.style();
        let enabled = self.base.is_enabled();

        // The indicator is a **fixed-size** box centred on the control's own line box, not a
        // fraction of whatever rectangle the caller laid out. Deriving it from `rect` is what
        // made a 240x120 census render a 120 px checkbox: the control drew its indicator at
        // the scale of its container, so the same control looked like a different one from one
        // layout to the next. The line box is the honest reference — it is what the label
        // beside the box is aligned to, and it is measured rather than assumed, so a larger
        // theme font grows the indicator with the text it accompanies.
        let font = effective_font(style);
        let line = context.text_line(rect, font);
        // The same derivation the hit test uses, so the box that is drawn is the box that responds.
        let checkbox_rect = self.indicator_rect(line.height, line.y);

        // The box must be visible against the surface it sits on. `style.background_color`
        // reaches here already resolved by the theme's `Input` role, so the literal fallback
        // only applies to a control whose style was never themed; the important part is that
        // the fallback is derived from the resolved value rather than from an assumption about
        // the appearance.
        let surface = style.background_color.unwrap_or(Color::WHITE);
        // The interaction wash: painted only when a driver actually holds a value, so the
        // resting fill is untouched byte-for-byte. `intensity` is the stronger of the two
        // drivers, so a press without a preceding hover still tints, and a press on a hover
        // is one wash rather than two.
        let intensity = if enabled { self.fade.intensity() } else { 0.0 };
        let tinted = if intensity > 0.0 {
            surface.blend(&surface.contrast_color(), CHECKBOX_INTERACTION_TINT * intensity)
        } else {
            surface
        };
        let field = if enabled { tinted } else { tinted.with_alpha(180) };
        context.fill_rect(checkbox_rect, field);

        let state = self.state();
        let border_color = style.border_color.unwrap_or_else(|| field.contrast_color());
        context.draw_rect(checkbox_rect, border_color);
        if state != CheckState::Unchecked {
            // The mark lands on the box's own fill, so its colour is that fill's contrast
            // colour. A literal accent blue here would be unreadable whenever the field is
            // dark, which is every dark appearance.
            //
            // # Why the mark and the label read different inks
            //
            // They are painted on **different surfaces**. The theme's `"check_box:checked"`
            // override sets `foreground = primary.contrast_color()` — the right ink for a mark on
            // the primary fill — but `style.text_color` is one field, and this control used it for
            // the mark *and* for the label beside the box. The label does not sit on the primary
            // fill; it sits on the page. So on the dark appearance a checked checkbox drew its
            // label in `primary.contrast_color()`, which is **black** on a dark page: the mark was
            // legible and the word next to it was not (measured — the label path came out
            // `rgba(0,0,0)` in both appearances). The mark therefore keeps the resolved ink, which
            // the theme's `:checked` rule exists to supply, and the label falls back to the
            // surface's own contrast colour when the resolved ink is clearly not an ink for it.
            let check_color = style.text_color.unwrap_or_else(|| field.contrast_color());
            match state {
                CheckState::Checked => {
                    // A compact tick drawn as two strokes, so it scales with the box instead
                    // of being a glyph in an unrelated font's advance model.
                    let x = checkbox_rect.x;
                    let y = checkbox_rect.y;
                    let size = checkbox_rect.width as i32;
                    let thickness = (size / 8).max(1) as u32;
                    let short = size * 3 / 8;
                    let long = size * 5 / 8;
                    let mid_drop = size * 5 / 8;
                    context.draw_line_stroke(
                        Point { x: x + size / 5, y: y + mid_drop },
                        Point { x: x + short, y: y + size - size / 5 },
                        check_color,
                        thickness,
                    );
                    context.draw_line_stroke(
                        Point { x: x + short, y: y + size - size / 5 },
                        Point { x: x + long, y: y + size * 3 / 8 },
                        check_color,
                        thickness,
                    );
                }
                CheckState::PartiallyChecked => {
                    // The minus sign is centred on the box rather than offset from its middle
                    // by a hand-tuned pixel, so it stays centred at every indicator size.
                    let bar_h = (checkbox_rect.height / 8).max(1);
                    let partial_rect = Rect::new(
                        checkbox_rect.x + (checkbox_rect.width as i32 / 4),
                        checkbox_rect.y + (checkbox_rect.height as i32 - bar_h as i32) / 2,
                        checkbox_rect.width.saturating_sub(checkbox_rect.width / 2),
                        bar_h,
                    );
                    context.fill_rect(partial_rect, check_color);
                }
                CheckState::Unchecked => {}
            }
        }
        // The label shares the indicator's line box, so the two cannot drift apart when the
        // font or the control's height changes.
        if !self.text.is_empty() {
            // `spacing` is the indicator-to-label gap, and that is the *only* thing the field
            // means — the gap between two sibling controls belongs to whichever layout placed
            // them (the standard table keeps the two separate for the same reason: `spacing` is used
            // for this pair, never for siblings). A themed spacing therefore tunes
            // this one relation and cannot accidentally re-space a whole row.
            let gap = self.label_gap();
            // The label sits on the **page**, so its ink has to be legible on the page. The
            // resolved `text_color` is the theme's answer for this control, which is right in every
            // state except one: under `"check_box:checked"` the theme's `foreground` is the ink for
            // a mark on the *primary fill*, and its `background` is that fill too — so both fields
            // describe the box interior, and the label is not in the box. Reading the page surface
            // from the theme is what gives the label the surface it is actually drawn on; the
            // control's own resolved background is not it under `:checked`. `legible_on` then states
            // the requirement the label has: keep a theme's own label ink when it already clears the
            // AA floor against the page, and repair it when it does not.
            let page = self.page_surface().unwrap_or(surface);
            let requested = style.text_color.unwrap_or_else(|| page.contrast_color());
            let text_color = if enabled {
                requested.legible_on(page, 4.5)
            } else {
                requested.legible_on(page, 4.5).with_alpha(150)
            };
            context.draw_text_fitted_cow(
                Rect::new(
                    checkbox_rect.x + checkbox_rect.width as i32 + gap,
                    line.y,
                    rect.width.saturating_sub(
                        (checkbox_rect.x - rect.x) as u32 + checkbox_rect.width + gap as u32,
                    ),
                    line.height,
                ),
                &self.text,
                font,
                text_color,
                HorizontalAlignment::Left,
            );
        }

        // ── Focus ring ──
        //
        // Inset inside the indicator, the same placement every other control uses (see
        // `ControlMetrics::focus_ring_rect`), and gated on the **reason** focus arrived: a
        // pointer press focuses without painting a ring, while Tab and Shortcut draw one.
        // `BaseWidget` records that reason for every control, so this site cannot disagree
        // with the rest of the crate about what "visual focus" means.
        if self.base.draws_focus_ring() {
            let ring = FocusRing::for_control(checkbox_rect, dimensions::CHECKBOX_RADIUS);
            if ring.is_drawable() {
                // The fallback ring is the contrast of the box's own fill; an active theme
                // overrides it with its declared `outline` token.
                context.draw_rounded_rect_stroke(
                    ring.rect,
                    ring.radius,
                    focus_ring_color(field.contrast_color()),
                    FOCUS_RING_WIDTH,
                );
            }
        }
    }
}

/// A dropped box leaves its exclusive group.
///
/// Without this the registry would keep the id forever: the thread-local map has no notion of a
/// control's lifetime, and a stale member can keep a group (and its selection) alive long after
/// every control that described it is gone.
impl Drop for CheckBox {
    fn drop(&mut self) {
        button_group::leave_all(self.id());
    }
}

// These tests drive the **theme**, which only exists in a build with a device profile
// (see `crate::lib`: `pub mod theme` is gated on `device_profile`). Without this gate the
// `mini` and `embedded` profiles fail to compile their test targets, because the test code
// names a module that those builds compile out — the production code is profile-clean and
// only the fixture was not.
#[cfg(all(test, full_widgets))]
mod tests {
    use super::*;
    use crate::compat::Vec;
    use crate::core::ObjectId;
    use crate::core::Size;
    use crate::event::Event;
    use crate::event::FocusReason;
    use std::sync::atomic::{AtomicU32, Ordering};
    use std::sync::Arc;
    use std::sync::Mutex;

    // ── 1. Creation ──────────────────────────────────────────────────────────
    #[test]
    fn test_creation_default_state() {
        let cb = CheckBox::new(Rect::new(10, 10, 100, 30));
        assert_eq!(cb.state(), CheckState::Unchecked);
        assert!(!cb.is_checked());
        assert!(!cb.is_partially_checked());
        assert!(!cb.is_tristate_enabled());
        assert!(cb.text().is_empty());
        assert!(cb.is_visible());
        assert!(cb.is_enabled());
    }

    // ── 2. State transitions (Unchecked→PartiallyChecked→Checked→Unchecked) ─
    #[test]
    fn test_toggle_unchecked_to_checked() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.toggle();
        assert_eq!(cb.state(), CheckState::Checked);
        assert!(cb.is_checked());
    }

    #[test]
    fn test_toggle_checked_to_unchecked_no_tristate() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.set_checked(true);
        assert_eq!(cb.state(), CheckState::Checked);
        cb.toggle();
        assert_eq!(cb.state(), CheckState::Unchecked);
    }

    #[test]
    fn test_toggle_unchecked_to_partial_when_tristate() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.set_tristate_enabled(true);
        assert_eq!(cb.state(), CheckState::Unchecked);
        cb.toggle();
        assert_eq!(cb.state(), CheckState::PartiallyChecked);
        assert!(cb.is_partially_checked());
    }

    #[test]
    fn test_toggle_partial_to_checked_when_tristate() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.set_tristate_enabled(true);
        cb.set_state(CheckState::PartiallyChecked);
        cb.toggle();
        assert_eq!(cb.state(), CheckState::Checked);
    }

    #[test]
    fn test_tristate_cycle() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.set_tristate_enabled(true);
        // Unchecked → PartiallyChecked
        cb.toggle();
        assert_eq!(cb.state(), CheckState::PartiallyChecked);
        // PartiallyChecked → Checked
        cb.toggle();
        assert_eq!(cb.state(), CheckState::Checked);
        // Checked → Unchecked
        cb.toggle();
        assert_eq!(cb.state(), CheckState::Unchecked);
    }

    #[test]
    fn the_next_state_is_the_state_a_toggle_reaches() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.set_tristate_enabled(true);
        for expected in [CheckState::PartiallyChecked, CheckState::Checked, CheckState::Unchecked] {
            assert_eq!(cb.next_check_state(), expected);
            cb.toggle();
            assert_eq!(cb.state(), expected);
        }
    }

    // ── 3. set_checked(true/false) ──────────────────────────────────────────
    #[test]
    fn test_set_checked_true() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.set_checked(true);
        assert!(cb.is_checked());
        assert_eq!(cb.state(), CheckState::Checked);
    }

    #[test]
    fn test_set_checked_false() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.set_checked(true);
        cb.set_checked(false);
        assert!(!cb.is_checked());
        assert_eq!(cb.state(), CheckState::Unchecked);
    }

    #[test]
    fn test_set_checked_noop() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        // Set unchecked when already unchecked – no state_changed signal
        cb.set_checked(false);
        assert_eq!(cb.state(), CheckState::Unchecked);
    }

    // ── 4. toggled signal (true on checked, false on unchecked) ─────────────
    #[test]
    fn test_toggled_signal_emitted_true() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        let emitted_value = Arc::new(Mutex::new(None));
        let ev = emitted_value.clone();
        cb.toggled.connect(move |v| *ev.lock().unwrap() = Some(*v));
        cb.set_checked(true);
        assert_eq!(*emitted_value.lock().unwrap(), Some(true));
    }

    #[test]
    fn test_toggled_signal_emitted_false() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.set_checked(true);
        let emitted_value = Arc::new(Mutex::new(None));
        let ev = emitted_value.clone();
        cb.toggled.connect(move |v| *ev.lock().unwrap() = Some(*v));
        cb.set_checked(false);
        assert_eq!(*emitted_value.lock().unwrap(), Some(false));
    }

    #[test]
    fn test_toggled_not_emitted_on_noop() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.set_checked(true);
        let emitted_count = Arc::new(AtomicU32::new(0));
        let ec = emitted_count.clone();
        cb.toggled.connect(move |_| {
            ec.fetch_add(1, Ordering::SeqCst);
        });
        // Set same state – no toggle
        cb.set_checked(true);
        assert_eq!(emitted_count.load(Ordering::SeqCst), 0);
    }

    #[test]
    fn test_toggled_not_emitted_on_partial_transition() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.set_tristate_enabled(true);
        let emitted_count = Arc::new(AtomicU32::new(0));
        let ec = emitted_count.clone();
        cb.toggled.connect(move |_| {
            ec.fetch_add(1, Ordering::SeqCst);
        });
        // Unchecked → PartiallyChecked collapses to `false` on both sides, so `toggled`
        // (the three-valued state's boolean projection) must stay silent.
        cb.toggle();
        assert_eq!(emitted_count.load(Ordering::SeqCst), 0);
    }

    // ── 5. state_changed signal (all transitions, no emission for noop) ────
    #[test]
    fn test_state_changed_on_all_transitions() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.set_tristate_enabled(true);
        let states = Arc::new(Mutex::new(Vec::new()));
        let s2 = states.clone();
        cb.state_changed.connect(move |s| s2.lock().unwrap().push(*s));
        cb.toggle(); // Unchecked → PartiallyChecked
        cb.toggle(); // PartiallyChecked → Checked
        cb.toggle(); // Checked → Unchecked
        let guard = states.lock().unwrap();
        assert_eq!(guard.len(), 3);
        assert_eq!(guard[0], CheckState::PartiallyChecked);
        assert_eq!(guard[1], CheckState::Checked);
        assert_eq!(guard[2], CheckState::Unchecked);
    }

    #[test]
    fn test_state_changed_not_emitted_on_noop() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        let emitted_count = Arc::new(AtomicU32::new(0));
        let ec = emitted_count.clone();
        cb.state_changed.connect(move |_| {
            ec.fetch_add(1, Ordering::SeqCst);
        });
        cb.set_state(CheckState::Unchecked); // already Unchecked
        assert_eq!(emitted_count.load(Ordering::SeqCst), 0);
    }

    // ── 6. Tristate mode toggle cycle ──────────────────────────────────────
    #[test]
    fn test_tristate_enable_disable_cycle() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        assert!(!cb.is_tristate_enabled());
        cb.set_tristate_enabled(true);
        assert!(cb.is_tristate_enabled());
        cb.set_tristate_enabled(false);
        assert!(!cb.is_tristate_enabled());
    }

    // ── 7. Disabling tristate resets Partial to Unchecked ──────────────────
    #[test]
    fn a_press_beside_the_label_does_not_toggle_the_checkbox() {
        // The `MousePress` arm used to ignore `pos` entirely, so a press *anywhere* in the control's
        // rectangle toggled it. A checkbox given a wide row by its layout therefore flipped when the
        // user clicked empty space far to the right of its own label — a different statement from
        // "the touch target is at least N points", and the wrong one.
        let mut cb = CheckBox::new(Rect::new(0, 0, 240, 30));
        cb.set_text("Label".to_string());
        cb.handle_event(&Event::MousePress { pos: Point::new(230, 15), button: 1, modifiers: 0 });
        assert_eq!(
            cb.state(),
            CheckState::Unchecked,
            "a press on the empty part of the row must not toggle the checkbox"
        );
    }

    // ── D09-CTRL-02: the hit area uses the same text measurement as painting ──

    /// A press on the **tail** of a wide-glyph label must reach the checkbox.
    ///
    /// # The defect this pins
    ///
    /// `hit_area()` estimated the label as `chars().count() * 3/5 em`. Wide glyphs (`W`, CJK)
    /// advance more than that, so the visible label extended past the estimated region and a click on
    /// its right-hand glyphs landed outside the target. The region is now measured with the shared
    /// `estimate_text_width`, the same ruler the painter uses.
    #[test]
    fn a_press_on_the_tail_of_a_wide_label_reaches_the_checkbox() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 400, 30));
        cb.set_text("WWWWWWWW".to_string());

        let area = cb.hit_area();
        // The measured label must be wider than the old character-count estimate would have allowed,
        // so the region really covers the wide glyphs.
        let old_estimate = {
            let line_height = Font::default().size().max(1.0) as u32;
            "WWWWWWWW".chars().count() as u32 * (line_height * 3 / 5).max(1)
        };
        assert!(
            area.width > old_estimate,
            "the measured label must exceed the old char-count estimate (was {old_estimate}, region {})",
            area.width
        );

        // A press just inside the region's right edge (the tail of the visible label) must toggle.
        let tail = Point::new(area.x + area.width as i32 - 1, area.y + area.height as i32 / 2);
        cb.handle_event(&Event::MousePress { pos: tail, button: 1, modifiers: 0 });
        assert_eq!(
            cb.state(),
            CheckState::Checked,
            "the tail of a wide label must be inside the hit area"
        );
    }

    /// `i` and `W` must not be measured as the same width, and both must stay reachable.
    #[test]
    fn narrow_and_wide_labels_are_measured_by_the_shared_ruler() {
        let mut narrow = CheckBox::new(Rect::new(0, 0, 400, 30));
        narrow.set_text("iiii".to_string());
        let mut wide = CheckBox::new(Rect::new(0, 0, 400, 30));
        wide.set_text("WWWW".to_string());

        assert!(
            narrow.hit_area().width < wide.hit_area().width,
            "`W` advances more than `i`, so a wide label's target must be wider: {} vs {}",
            narrow.hit_area().width,
            wide.hit_area().width
        );

        // Each label's own tail is clickable.
        let tail = Point::new(
            wide.hit_area().x + wide.hit_area().width as i32 - 1,
            wide.hit_area().y + wide.hit_area().height as i32 / 2,
        );
        wide.handle_event(&Event::MousePress { pos: tail, button: 1, modifiers: 0 });
        assert_eq!(wide.state(), CheckState::Checked, "the wide label's tail must be clickable");
    }

    #[test]
    fn a_press_outside_the_control_still_hits_through_the_touch_target() {
        // The other half of the same judgement, and the mechanism BLUE21 AR1 found unconnected: a
        // control smaller than the platform's minimum touch size must still respond just outside
        // its own rectangle. The checkbox is 18 px tall, so on a phone profile (48 px) a point a few
        // pixels above it is inside the reachable target.
        let mut cb = CheckBox::new(Rect::new(20, 20, 24, 18));
        // The theme registry is process-wide, so a test that **switches** the appearance must take
        // the same guard every other switching test does; otherwise it leaves `Light` selected for
        // whichever test runs next, and a test that compares two appearances can observe this one's
        // leftover as its own. That is the failure mode that made `display_widgets`'s slider halo
        // assertion fail only in a batch (see its own note).
        let _guard = crate::style::theme_test_guard();
        // Theming is what installs the touch target, so the test installs and applies one the way
        // the runtime does before registering a control. Reading it off an un-themed control would
        // assert the wrong precondition — the point of the mechanism is that the *platform* supplies
        // the size.
        {
            let mut manager = crate::theme::global_theme_manager();
            manager.register_theme(crate::theme::Theme::default());
            manager.set_appearance(crate::theme::AppearanceMode::Light);
        }
        crate::theme::apply_active_theme(&mut cb);
        let target = cb.style().touch_target.expect(
            "a themed control must carry a touch target — that is what `role_base_style` writes",
        );
        assert!(
            target.height > 18,
            "the target must exceed the control's own height for this test to mean anything"
        );
        // One pixel above the control's top edge, inside the expansion.
        cb.handle_event(&Event::MousePress { pos: Point::new(24, 19), button: 1, modifiers: 0 });
        assert_eq!(
            cb.state(),
            CheckState::Checked,
            "a press within the minimum touch target must reach the control"
        );
    }

    #[test]
    fn a_press_well_outside_the_touch_target_is_ignored() {
        // The reverse direction, so the previous test cannot pass by accepting everything.
        let mut cb = CheckBox::new(Rect::new(100, 100, 24, 18));
        cb.handle_event(&Event::MousePress { pos: Point::new(10, 10), button: 1, modifiers: 0 });
        assert_eq!(cb.state(), CheckState::Unchecked);
    }

    /// A checked box's **label** stays legible on the page, on both appearances.
    ///
    /// # The defect this pins
    ///
    /// The theme's `"check_box:checked"` override sets `background` to the primary fill and
    /// `foreground` to that fill's contrast colour — both describing the **box interior**. The draw
    /// path used `style.text_color` for the mark *and* for the label, and the label is beside the
    /// box, on the page: on the dark appearance `primary.contrast_color()` is black, so a checked box
    /// drew a legible mark next to a word that had gone black on a dark page. The census measured the
    /// two appearances as byte-identical for exactly this reason (the label dominates the render),
    /// which is how it was found.
    ///
    /// The label now resolves against the **page** rather than the box, so the assertion is a
    /// contrast ratio against the active theme's background — the surface the user actually reads it
    /// on — rather than against a colour the control is not drawn on.
    #[test]
    #[cfg(device_profile)]
    fn a_checked_box_keeps_its_label_legible_on_the_page() {
        let _guard = crate::style::theme_test_guard();
        crate::widget::census::install_preset_appearances();

        for appearance in [crate::theme::AppearanceMode::Light, crate::theme::AppearanceMode::Dark]
        {
            crate::theme::global_theme_manager().set_appearance(appearance);
            let mut cb = CheckBox::new(Rect::new(0, 0, 200, 24));
            cb.set_text("Label".to_string());
            cb.set_checked(true);
            crate::theme::apply_active_theme(&mut cb);

            let page = crate::style::theme_manager()
                .current_theme()
                .map(|active| active.colors.background)
                .expect("a preset is active");
            let svg = crate::widget::svg::render_widget_to_svg(&mut cb, Rect::new(0, 0, 200, 24));
            let ink = label_ink(&svg).unwrap_or_else(|| {
                panic!("a checked box with text must paint its label; svg was {svg}")
            });
            let ratio = ink.contrast_ratio(page);
            assert!(
                ratio >= 4.5,
                "the label of a checked box must clear the AA floor on the page it sits on: \
                 {appearance:?} painted {ink:?} on {page:?}, a ratio of {ratio:.2}:1"
            );
        }
    }

    /// The fill of the SVG `<path>` element that carries the label's glyph geometry.
    #[cfg(device_profile)]
    fn label_ink(svg: &str) -> Option<Color> {
        let at = svg.find("<path d=\"")?;
        let rest = &svg[at..];
        let end = rest.find("/>")? + 2;
        let element = &rest[..end];
        let key = "fill=\"rgba(";
        let from = element.find(key)? + key.len();
        let to = element[from..].find(')')? + from;
        let mut parts = element[from..to].split(',');
        let r = parts.next()?.trim().parse().ok()?;
        let g = parts.next()?.trim().parse().ok()?;
        let b = parts.next()?.trim().parse().ok()?;
        Some(Color::rgb(r, g, b))
    }

    #[test]
    fn test_disable_tristate_resets_partial() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.set_tristate_enabled(true);
        cb.set_state(CheckState::PartiallyChecked);
        assert!(cb.is_partially_checked());
        // Disable tristate – should reset to Unchecked
        cb.set_tristate_enabled(false);
        assert_eq!(cb.state(), CheckState::Unchecked);
    }

    #[test]
    fn test_disable_tristate_does_not_reset_checked() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.set_tristate_enabled(true);
        cb.set_checked(true);
        cb.set_tristate_enabled(false);
        // Checked should stay Checked
        assert_eq!(cb.state(), CheckState::Checked);
    }

    #[test]
    fn test_disable_tristate_does_not_reset_unchecked() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.set_tristate_enabled(false);
        cb.set_state(CheckState::Unchecked);
        // No-op
        cb.set_tristate_enabled(false);
        assert_eq!(cb.state(), CheckState::Unchecked);
    }

    // ── 8. Text set/get ────────────────────────────────────────────────────
    #[test]
    fn test_text_default_empty() {
        let cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        assert_eq!(cb.text(), "");
    }

    #[test]
    fn test_text_set_and_get() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.set_text(String::from("Enable feature"));
        assert_eq!(cb.text(), "Enable feature");
    }

    #[test]
    fn test_text_overwrite() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.set_text(String::from("Old label"));
        cb.set_text(String::from("New label"));
        assert_eq!(cb.text(), "New label");
    }

    // ── 9. Event handling ──────────────────────────────────────────────────
    #[test]
    fn test_mouse_down_toggles() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        assert_eq!(cb.state(), CheckState::Unchecked);
        cb.handle_event(&Event::MousePress { pos: Point::new(10, 10), button: 1, modifiers: 0 });
        assert_eq!(cb.state(), CheckState::Checked);
    }

    #[cfg(feature = "touch")]
    #[test]
    fn test_touch_begin_toggles() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        assert_eq!(cb.state(), CheckState::Unchecked);
        cb.handle_event(&Event::TouchBegin { touch_id: 0, pos: Point::new(10, 10) });
        // TouchBegin now toggles the checkbox when the touch feature is enabled
        assert_eq!(cb.state(), CheckState::Checked);
    }

    #[cfg(feature = "touch")]
    #[test]
    fn test_tap_toggles() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        assert_eq!(cb.state(), CheckState::Unchecked);
        cb.handle_event(&Event::Tap { pos: Point::new(10, 10) });
        // Current implementation does not match Tap – falls through to _
        assert_eq!(cb.state(), CheckState::Unchecked);
    }

    #[test]
    fn test_space_key_toggles() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        assert_eq!(cb.state(), CheckState::Unchecked);
        // Space key code is 32
        cb.handle_event(&Event::KeyPress { key: 32, modifiers: 0 });
        assert_eq!(cb.state(), CheckState::Checked);
    }

    #[test]
    fn test_non_space_key_does_not_toggle() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.handle_event(&Event::KeyDown((65, 0))); // 'A' key
        assert_eq!(cb.state(), CheckState::Unchecked);
    }

    #[test]
    fn test_event_noop_when_disabled() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.set_enabled(false);
        assert_eq!(cb.state(), CheckState::Unchecked);
        cb.handle_event(&Event::MouseDown((Point::new(10, 10), 0)));
        // Should NOT toggle because checkbox is disabled
        assert_eq!(cb.state(), CheckState::Unchecked);
    }

    #[test]
    fn test_event_key_noop_when_disabled() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.set_enabled(false);
        cb.handle_event(&Event::KeyDown((32, 0)));
        assert_eq!(cb.state(), CheckState::Unchecked);
    }

    #[test]
    fn test_multiple_mouse_down_toggles() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.handle_event(&Event::MousePress { pos: Point::new(10, 10), button: 1, modifiers: 0 });
        assert_eq!(cb.state(), CheckState::Checked);
        cb.handle_event(&Event::MousePress { pos: Point::new(10, 10), button: 1, modifiers: 0 });
        assert_eq!(cb.state(), CheckState::Unchecked);
        cb.handle_event(&Event::MousePress { pos: Point::new(10, 10), button: 1, modifiers: 0 });
        assert_eq!(cb.state(), CheckState::Checked);
    }

    // ── 10. Widget trait delegation ─────────────────────────────────────────
    #[test]
    fn test_widget_geometry() {
        let mut cb = CheckBox::new(Rect::new(10, 20, 100, 30));
        assert_eq!(cb.geometry(), Rect::new(10, 20, 100, 30));
        cb.set_geometry(Rect::new(0, 0, 200, 50));
        assert_eq!(cb.geometry(), Rect::new(0, 0, 200, 50));
    }

    #[test]
    fn test_widget_visibility() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        assert!(cb.is_visible());
        cb.hide();
        assert!(!cb.is_visible());
        cb.show();
        assert!(cb.is_visible());
    }

    #[test]
    fn test_widget_enabled() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        assert!(cb.is_enabled());
        cb.set_enabled(false);
        assert!(!cb.is_enabled());
        cb.set_enabled(true);
        assert!(cb.is_enabled());
    }

    #[test]
    fn test_widget_style_default() {
        let cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        let _style = cb.style();
        assert_eq!(cb.kind(), WidgetKind::CheckBox);
    }

    #[test]
    fn test_widget_tooltip() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        assert!(cb.tooltip().is_empty());
        cb.set_tooltip(String::from("Click to toggle"));
        assert_eq!(cb.tooltip(), "Click to toggle");
    }

    #[test]
    fn test_widget_parent() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        assert!(cb.parent().is_none());
        let parent_id: ObjectId = 42;
        cb.set_parent(Some(parent_id));
        assert_eq!(cb.parent(), Some(parent_id));
        cb.set_parent(None);
        assert!(cb.parent().is_none());
    }

    #[test]
    fn test_widget_children() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        assert!(cb.children().is_empty());
        let child_id: ObjectId = 99;
        cb.add_child(child_id);
        assert_eq!(cb.children().len(), 1);
        assert_eq!(cb.children()[0], child_id);
        cb.remove_child(child_id);
        assert!(cb.children().is_empty());
    }

    #[test]
    fn test_widget_min_max_size() {
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        assert!(cb.min_size().is_none());
        assert!(cb.max_size().is_none());
        cb.set_min_size(Some(Size::new(50, 20)));
        cb.set_max_size(Some(Size::new(200, 60)));
        assert_eq!(cb.min_size(), Some(Size::new(50, 20)));
        assert_eq!(cb.max_size(), Some(Size::new(200, 60)));
    }

    #[test]
    fn test_widget_id_not_zero() {
        let cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        assert_ne!(cb.id(), 0u64);
    }

    #[test]
    fn test_widget_signal_accessors() {
        let cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        // Verify signal accessors do not panic
        let _ = cb.hover_signal();
        let _ = cb.mouse_down_signal();
        let _ = cb.mouse_up_signal();
        let _ = cb.key_down_signal();
        let _ = cb.key_up_signal();
        let _ = cb.focus_gained_signal();
        let _ = cb.focus_lost_signal();
        let _ = cb.redraw_requested_signal();
        let _ = cb.layout_requested_signal();
    }

    #[test]
    fn test_widget_connection_scope() {
        let cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        let _scope = cb.connection_scope();
    }

    // ── State channel (§2.2) ─────────────────────────────────────────────────

    /// `CheckBox` overrides neither `widget_state` nor any hover bookkeeping, so this
    /// pins the *whole* point of elevating the state source into `BaseWidget`: a control
    /// that inherited the default reports `Hover` purely because the runtime delivered
    /// `MouseEnter`. Before the elevation this could not be true.
    #[test]
    fn widget_state_reports_hover_from_the_base() {
        use crate::style::WidgetState;
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        assert_eq!(cb.widget_state(), WidgetState::Normal);
        cb.handle_event(&Event::MouseEnter { pos: Point::new(1, 1) });
        assert_eq!(cb.widget_state(), WidgetState::Hover);
        cb.handle_event(&Event::MouseLeave { pos: Point::new(1, 1) });
        assert_eq!(cb.widget_state(), WidgetState::Normal);
    }

    /// Disabled outranks hovered — the precedence the trait documents.
    #[test]
    fn widget_state_prefers_disabled_over_hover() {
        use crate::style::WidgetState;
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        cb.handle_event(&Event::MouseEnter { pos: Point::new(1, 1) });
        cb.set_enabled(false);
        assert_eq!(cb.widget_state(), WidgetState::Disabled);
    }

    /// A latched box reports `Checked`, which is what makes the preset's `check_box:checked`
    /// override reachable at all.
    ///
    /// # The defect this pins
    ///
    /// Both presets declare `check_box:checked`, and the theme manager looks a control up by
    /// `"<kind>:<state>"`. But the trait's default `widget_state` only knows the four primitive
    /// flags — `disabled`/`pressed`/`hovered`/`focused` — and none of them means "latched". So the
    /// key was declared, documented, and resolved to `Normal` on every checkbox, and the accent
    /// fill the preset asks for was never painted. A declared state with no consumer is the
    /// state-level form of §5.5's rule.
    #[test]
    fn widget_state_reports_checked_for_a_latched_box() {
        use crate::style::WidgetState;
        let mut cb = CheckBox::new(Rect::new(0, 0, 100, 30));
        assert_eq!(cb.widget_state(), WidgetState::Normal);

        cb.set_state(CheckState::Checked);
        assert_eq!(cb.widget_state(), WidgetState::Checked);

        // `PartiallyChecked` also reports `Checked`: the preset paints one accent fill for "on",
        // and a mixed box is on. The three-way answer is carried by the control's own mark and by
        // `a11y_state`, which is where a screen reader reads it.
        cb.set_state(CheckState::PartiallyChecked);
        assert_eq!(cb.widget_state(), WidgetState::Checked);

        // And the latch outranks the momentary states, so a checked box that is hovered is still
        // reported as checked — the precedence the trait documents.
        cb.handle_event(&Event::MouseEnter { pos: Point::new(1, 1) });
        assert_eq!(cb.widget_state(), WidgetState::Checked);

        // Unchecking hands the report back to the momentary states.
        cb.set_state(CheckState::Unchecked);
        assert_eq!(cb.widget_state(), WidgetState::Hover);
    }

    // ── D09-STYLE-01: the checkbox consumes the resolved theme/caller font ──

    /// A caller-authored `style.font` must drive both the measured hint and the painted label.
    ///
    /// # The defect this pins
    ///
    /// `implicit_size()` and `draw()` both built a `Font::default()` by hand, so a checkbox
    /// beside a themed button rendered its own label in the default face at the default size no
    /// matter what the theme resolved or the user scaled. The two now read
    /// `metrics::effective_font`, which prefers `style.font`.
    #[test]
    fn a_custom_font_grows_the_checkbox_hint_and_the_drawn_label() {
        let text = "Label";
        let baseline = CheckBox::new(Rect::new(0, 0, 200, 30));
        let mut baseline = baseline;
        baseline.set_text(text.to_string());
        let base_hint = baseline.implicit_size();
        let base_ink = label_ink_width(&mut baseline, text);

        let mut big = CheckBox::new(Rect::new(0, 0, 200, 30));
        big.set_text(text.to_string());
        // A face twice the default size: `effective_font` must pick it up in both paths.
        big.set_style(crate::style::WidgetStyle::default().with_font(Font::simple("Test", 28.0)));

        let big_hint = big.implicit_size();
        let big_ink = label_ink_width(&mut big, text);

        assert_eq!(
            big.style().font.as_ref().map(Font::size),
            Some(28.0),
            "the style carries the font"
        );
        assert!(
            big_hint.width > base_hint.width,
            "a larger font must widen the hint: {} vs {}",
            big_hint.width,
            base_hint.width
        );
        assert!(
            big_hint.height > base_hint.height,
            "a larger font must raise the hint: {} vs {}",
            big_hint.height,
            base_hint.height
        );
        assert!(
            big_ink > base_ink,
            "a larger font must widen the painted label ink: {big_ink} vs {base_ink}"
        );
    }

    /// A text scale above 1.0, resolved through the theme, must widen the hint and the label.
    ///
    /// This exercises the real causal chain — environment provider → theme body font →
    /// `style.font` → `effective_font` — rather than only setting a font by hand, so it fails if
    /// any link (including the theme scale) is unplugged.
    #[test]
    fn a_theme_text_scale_above_one_grows_the_checkbox_hint() {
        use crate::style::environment::{
            install_environment, uninstall_environment, EnvironmentProvider,
        };
        use crate::style::MotionPreference;

        struct Scaled(f32);
        impl EnvironmentProvider for Scaled {
            fn text_scale(&self) -> f32 {
                self.0
            }
            fn motion_preference(&self) -> MotionPreference {
                MotionPreference::NoPreference
            }
        }

        let _guard = crate::style::theme_test_guard();
        let previous = install_environment(Box::new(Scaled(1.0)));
        {
            let mut manager = crate::theme::global_theme_manager();
            manager.register_theme(crate::theme::Theme::default());
            manager.set_appearance(crate::theme::AppearanceMode::Light);
        }
        let mut at_one = CheckBox::new(Rect::new(0, 0, 200, 30));
        at_one.set_text("Scaled label".to_string());
        crate::theme::apply_active_theme(&mut at_one);
        let hint_one = at_one.implicit_size();
        let font_one = at_one.style().font.as_ref().map(Font::size).unwrap_or(0.0);

        // The device now asks for 150% text.
        install_environment(Box::new(Scaled(1.5)));
        let mut at_one_and_a_half = CheckBox::new(Rect::new(0, 0, 200, 30));
        at_one_and_a_half.set_text("Scaled label".to_string());
        crate::theme::apply_active_theme(&mut at_one_and_a_half);
        let hint_large = at_one_and_a_half.implicit_size();
        let font_large = at_one_and_a_half.style().font.as_ref().map(Font::size).unwrap_or(0.0);

        let _ = uninstall_environment();
        if let Some(previous) = previous {
            install_environment(previous);
        }

        assert!(
            font_large > font_one,
            "the theme body font must scale with the device text preference: {font_large} vs {font_one}"
        );
        assert!(
            hint_large.width > hint_one.width,
            "a text scale above 1.0 must widen the checkbox hint: {} vs {}",
            hint_large.width,
            hint_one.width
        );
    }

    /// The ink width of the label a checkbox paints, read back from an SVG render.
    fn label_ink_width(cb: &mut CheckBox, _text: &str) -> i32 {
        let svg = crate::widget::svg::render_to_svg(cb);
        crate::widget::svg::text_ink_box(&svg).map(|(x, _, right, _)| right - x).unwrap_or(0)
    }

    /// Keyboard focus must actually paint the ring the state channel already reported.
    ///
    /// # The defect this pins
    ///
    /// `widget_state()` reported `Focused` while `draw` painted no ring at all — the state was
    /// recorded and announced but had no visible consumer. The ring is the only 2 px stroke an
    /// unlabelled, unchecked checkbox emits, so its presence is observable in the render.
    #[test]
    fn a_keyboard_focused_checkbox_paints_a_focus_ring() {
        // The ring's colour reads the process-wide theme, so this render must not race a
        // test that switches the active appearance.
        let _guard = crate::style::theme_test_guard();
        let mut checkbox = CheckBox::new(Rect::new(0, 0, 60, 40));
        checkbox.handle_event(&Event::FocusGained { reason: FocusReason::Tab });
        assert!(checkbox.base.draws_focus_ring(), "Tab focus must ask for a ring");

        let svg = crate::widget::svg::render_to_svg(&mut checkbox);
        assert_eq!(
            svg.matches("stroke-width=\"2\"").count(),
            1,
            "the focus ring is the only 2 px stroke on an unlabelled, unchecked box: {svg}"
        );
        let ring = svg
            .lines()
            .find(|line| line.contains("stroke-width=\"2\""))
            .expect("the ring element must exist");
        assert!(
            ring.contains(" rx=\""),
            "the ring must be drawn as a rounded-rectangle stroke: {ring}"
        );
        assert!(
            ring.contains("stroke=\"rgba(") && !ring.contains(",0.00)"),
            "the ring stroke must carry visible ink rather than a transparent colour: {ring}"
        );
    }

    /// A pointer press focuses the control without drawing a ring under the cursor.
    #[test]
    fn a_pointer_focused_checkbox_does_not_paint_a_focus_ring() {
        let _guard = crate::style::theme_test_guard();
        let mut checkbox = CheckBox::new(Rect::new(0, 0, 60, 40));
        checkbox.handle_event(&Event::FocusGained { reason: FocusReason::Pointer });
        assert!(
            checkbox.base.focus_reason().is_some(),
            "the pointer still moves the keyboard target"
        );

        let svg = crate::widget::svg::render_to_svg(&mut checkbox);
        assert!(
            !svg.contains("stroke-width=\"2\""),
            "a pointer-focused checkbox must not paint a ring: {svg}"
        );
    }

    // ── Interaction fade (P1-9) ─────────────────────────────────────────────

    /// Rasterises the checkbox's paint into the SVG document the comparisons read.
    fn render(cb: &mut CheckBox) -> String {
        crate::widget::svg::render_to_svg(cb)
    }

    /// A pointer entering the box retargets the hover driver at once.
    ///
    /// The answer has to be true before any `tick` has run: the animation bus asks
    /// `is_animating` first, and a control that only admitted movement on its next tick
    /// would never be scheduled for one.
    #[test]
    fn mouse_enter_makes_the_checkbox_animating() {
        let mut checkbox = CheckBox::new(Rect::new(0, 0, 60, 40));
        assert!(!Widget::is_animating(&checkbox), "a new checkbox rests and owes no frames");

        checkbox.handle_event(&Event::MouseEnter { pos: Point::new(1, 1) });

        assert!(
            Widget::is_animating(&checkbox),
            "MouseEnter must make the checkbox's fade moving before any tick"
        );
        assert!(Widget::tick(&mut checkbox, 16), "the first fade frame must report work");
    }

    /// A resting box paints its resolved fill byte-for-byte, and returns to exactly that
    /// picture after a hover has come and gone.
    #[test]
    fn a_resting_checkbox_paints_the_untinted_indicator() {
        let _guard = crate::style::theme_test_guard();
        let mut checkbox = CheckBox::new(Rect::new(0, 0, 120, 32));
        checkbox.set_text("Enable".to_string());
        let fresh = render(&mut checkbox);

        checkbox.handle_event(&Event::MouseEnter { pos: Point::new(1, 1) });
        while Widget::tick(&mut checkbox, 16) {}
        assert_ne!(
            render(&mut checkbox),
            fresh,
            "a settled hover must actually tint the indicator"
        );

        checkbox.handle_event(&Event::MouseLeave { pos: Point::new(1, 1) });
        while Widget::tick(&mut checkbox, 16) {}
        assert_eq!(checkbox.fade.hover.value(), 0.0, "the fade returns to rest exactly");
        assert_eq!(
            render(&mut checkbox),
            fresh,
            "a box back at rest must repaint the same bytes it did before the hover"
        );
    }

    /// Animating to the hover terminal state produces the same picture as setting the
    /// terminal value directly, so the fade is purely a path between two identical ends.
    #[test]
    fn a_settled_hover_matches_the_manually_set_terminal_state() {
        let _guard = crate::style::theme_test_guard();
        let make = || {
            let mut checkbox = CheckBox::new(Rect::new(0, 0, 120, 32));
            checkbox.set_text("Enable".to_string());
            checkbox
        };

        let mut animated = make();
        animated.handle_event(&Event::MouseEnter { pos: Point::new(1, 1) });
        let mut frames = 0;
        while Widget::tick(&mut animated, 16) {
            frames += 1;
            assert!(frames < 500, "the fade must terminate");
        }
        assert_eq!(animated.fade.hover.value(), 1.0, "the hover settles at its target");

        let mut manual = make();
        manual.fade.hover.jump_to(1.0);
        manual.fade.press.jump_to(0.0);

        assert_eq!(
            render(&mut animated),
            render(&mut manual),
            "the animated end state must be the manually set terminal state"
        );
    }

    /// Reduced motion collapses the fast tempo to zero, so one tick reaches the target.
    #[test]
    fn reduced_motion_reaches_the_checkbox_target_in_one_frame() {
        use crate::compat::Box;
        use crate::style::environment::{
            install_environment, uninstall_environment, EnvironmentProvider,
        };
        use crate::style::MotionPreference;

        struct Reduced;
        impl EnvironmentProvider for Reduced {
            fn motion_preference(&self) -> MotionPreference {
                MotionPreference::ReduceMotion
            }
        }

        let _guard = crate::style::theme_test_guard();
        let previous = install_environment(Box::new(Reduced));

        let mut checkbox = CheckBox::new(Rect::new(0, 0, 60, 40));
        checkbox.handle_event(&Event::MouseEnter { pos: Point::new(1, 1) });
        let moving = Widget::tick(&mut checkbox, 16);

        assert!(!moving, "a zero-duration fade settles on the first tick");
        assert_eq!(checkbox.fade.hover.value(), 1.0, "and reaches the target, not half way");
        assert!(!Widget::is_animating(&checkbox));

        let _ = uninstall_environment();
        if let Some(previous) = previous {
            install_environment(previous);
        }
    }

    /// A hover frame submits damage bounded by the painted box and the margin.
    #[cfg(not(alloc_frugal))]
    #[test]
    fn a_hover_frame_submits_the_painted_box_with_the_damage_margin() {
        use crate::widget::runtime;

        let id =
            runtime::register(crate::compat::Box::new(CheckBox::new(Rect::new(4, 8, 120, 32))))
                .expect("registry");
        assert!(runtime::set_repaint_mode(id, runtime::RepaintMode::Dirty));

        assert!(runtime::dispatch_event(id, &Event::mouse_enter(1, 1)));

        let expected = runtime::painted_box_with_damage_margin(id).expect("painted box");
        assert_eq!(
            runtime::dirty_rects(id),
            vec![expected],
            "a hover must damage the painted box plus the margin"
        );
        runtime::unregister(id);
    }

    /// `Full` keeps the whole-control path and records no damage rectangle.
    #[cfg(not(alloc_frugal))]
    #[test]
    fn a_hover_frame_in_full_mode_keeps_the_whole_control_repaint() {
        use crate::widget::runtime;

        let id =
            runtime::register(crate::compat::Box::new(CheckBox::new(Rect::new(4, 8, 120, 32))))
                .expect("registry");
        assert_eq!(runtime::repaint_mode(id), runtime::RepaintMode::Full);

        assert!(runtime::dispatch_event(id, &Event::mouse_enter(1, 1)));
        assert!(
            runtime::dirty_rects(id).is_empty(),
            "Full mode records no damage, so the control repaints whole"
        );
        runtime::unregister(id);
    }

    // ── Auto-exclusive siblings (R007) ─────────────────────────────────

    #[test]
    fn auto_exclusive_is_off_by_default() {
        let mut a = CheckBox::new(Rect::new(0, 0, 80, 20));
        let mut b = CheckBox::new(Rect::new(0, 24, 80, 20));
        a.set_parent(Some(99));
        b.set_parent(Some(99));

        a.set_checked(true);
        b.set_checked(true);
        assert!(a.is_checked() && b.is_checked(), "independent boxes stay independent");
    }

    #[test]
    fn auto_exclusive_siblings_under_one_parent_exclude_each_other() {
        let mut a = CheckBox::new(Rect::new(0, 0, 80, 20));
        let mut b = CheckBox::new(Rect::new(0, 24, 80, 20));
        a.set_parent(Some(99));
        b.set_parent(Some(99));
        a.set_auto_exclusive(true);
        b.set_auto_exclusive(true);

        a.set_checked(true);
        assert!(a.is_checked());
        assert!(!b.is_checked());

        b.set_checked(true);
        assert!(!a.is_checked(), "checking b unchecks a");
        assert!(b.is_checked());
        assert_eq!(a.state(), CheckState::Unchecked, "the displaced box reports its real state");
    }

    #[test]
    fn auto_exclusive_does_not_cross_parents() {
        let mut a = CheckBox::new(Rect::new(0, 0, 80, 20));
        let mut b = CheckBox::new(Rect::new(0, 24, 80, 20));
        a.set_parent(Some(1));
        b.set_parent(Some(2));
        a.set_auto_exclusive(true);
        b.set_auto_exclusive(true);

        a.set_checked(true);
        b.set_checked(true);
        assert!(a.is_checked() && b.is_checked(), "the parent is the group boundary");
    }

    #[test]
    fn auto_exclusive_is_published_and_writable() {
        let mut box_ = CheckBox::new(Rect::new(0, 0, 80, 20));
        assert_eq!(box_.get("auto_exclusive"), Ok(CapabilityValue::Bool(false)));
        box_.set("auto_exclusive", CapabilityValue::Bool(true)).expect("writable");
        assert!(box_.is_auto_exclusive());
        assert_eq!(box_.get("auto_exclusive"), Ok(CapabilityValue::Bool(true)));
    }
}