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
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT

//! Message box dialog widget.
use crate::compat::{Rc, RefCell};
use crate::core::{Color, Font, HorizontalAlignment, ObjectId, Point, Rect, Size};
use crate::event::key_codes;
use crate::event::{Event, EventHandler};
use crate::impl_widget_property_hooks;
use crate::layout::hints::Hints;
use crate::property_names_of;
use crate::render::RenderContext;
use crate::signal::{GenericSignal, Signal1};
use crate::style::{MotionSlot, PropertyDriver, SemanticColor};
use crate::widget::capability::coercion::{
    expect_bool, expect_message_box_icon, expect_string, message_box_icon_to_str,
};
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::dialog::alert_dialogs::wrap_words;
use crate::widget::metrics::{dimensions, effective_font, ControlMetrics};
use crate::widget::{BaseWidget, Draw, IconName, Widget, WidgetKind};
/// Message box icon type.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessageBoxIcon {
    /// No icon is drawn; the text fills the full content area.
    NoIcon,
    /// Informational "i" glyph, drawn in blue.
    Information,
    /// A question-mark glyph, drawn in the same blue as
    /// [`Self::Information`].
    Question,
    /// A warning triangle glyph, drawn in orange.
    Warning,
    /// A critical cross glyph, drawn in red; use for errors that blocked an
    /// action.
    Critical,
}
/// Standard buttons for message boxes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StandardButton {
    /// Acknowledge and dismiss.
    Ok,
    /// Abandon the operation and dismiss.
    Cancel,
    /// Affirm the prompt.
    Yes,
    /// Decline the prompt.
    No,
    /// Affirm, and stop asking for the same kind of prompt in this run.
    YesAll,
    /// Decline, and stop asking for the same kind of prompt in this run.
    NoAll,
    /// Persist the current state.
    Save,
    /// Abandon the current changes.
    Discard,
    /// Keep the current changes without dismissing.
    Apply,
    /// Dismiss without an accept/reject judgement. Note that
    /// [`MessageBox::click_button`] still counts it as a rejection.
    Close,
    /// Stop an in-progress operation because it cannot succeed.
    Abort,
    /// Repeat the operation that just failed.
    Retry,
    /// Continue past a non-fatal problem.
    Ignore,
    /// Open contextual help instead of answering.
    Help,
}
impl StandardButton {
    /// The English label shown on the button, e.g. `"OK"` or `"Yes to All"`.
    ///
    /// This is the untranslated string; use [`Self::translated_label`] for a
    /// locale-aware label. `'static` because the labels are compile-time
    /// constants.
    pub fn label(&self) -> &'static str {
        match self {
            StandardButton::Ok => "OK",
            StandardButton::Cancel => "Cancel",
            StandardButton::Yes => "Yes",
            StandardButton::No => "No",
            StandardButton::YesAll => "Yes to All",
            StandardButton::NoAll => "No to All",
            StandardButton::Save => "Save",
            StandardButton::Discard => "Discard",
            StandardButton::Apply => "Apply",
            StandardButton::Close => "Close",
            StandardButton::Abort => "Abort",
            StandardButton::Retry => "Retry",
            StandardButton::Ignore => "Ignore",
            StandardButton::Help => "Help",
        }
    }

    /// Returns the translated label for this button using the i18n system.
    ///
    /// # The fallback is the English label, not the catalogue key
    ///
    /// `tr!`/`translate_key` answer with the key itself when no catalogue is initialised (or when
    /// the key is absent). That value is not user-facing copy: an 80 px action button elided
    /// `common.button.ok` to `common....`, which is how the raw key reached the screen. The
    /// display fallback is therefore [`Self::label`], resolved through the dialog module's one
    /// `localized_label` helper so every dialog that draws a standard button shares the rule.
    ///
    /// # Why there is no `cfg` split here
    ///
    /// `crate::tr!` compiles to a real catalogue lookup when the `i18n` capability is
    /// on and to the key's own English text otherwise (see the macro's stub in
    /// `src/lib.rs`, whose doc says it exists "so call sites need no `cfg` of their
    /// own"). Splitting on `desktop` instead meant `tablet`/`mobile` — which enable
    /// `i18n` without `desktop` — silently took the untranslated branch.
    pub fn translated_label(&self) -> String {
        super::localized_label(self.translation_key(), self.label())
    }

    /// The catalogue key this button's label comes from.
    ///
    /// Exposed so [`Self::translated_label`] can compare the resolved text against the key it
    /// asked for — the comparison that decides whether the English fallback is needed. It is also
    /// the key a host should pass to its own translation lookup if it draws the button itself.
    pub fn translation_key(&self) -> &'static str {
        match self {
            StandardButton::Ok => "common.button.ok",
            StandardButton::Cancel => "common.button.cancel",
            StandardButton::Yes => "common.button.yes",
            StandardButton::No => "common.button.no",
            StandardButton::YesAll => "common.button.yes_all",
            StandardButton::NoAll => "common.button.no_all",
            StandardButton::Save => "common.button.save",
            StandardButton::Discard => "common.button.discard",
            StandardButton::Apply => "common.button.apply",
            StandardButton::Close => "common.button.close",
            StandardButton::Abort => "common.button.abort",
            StandardButton::Retry => "common.button.retry",
            StandardButton::Ignore => "common.button.ignore",
            StandardButton::Help => "common.button.help",
        }
    }
}
/// The stable token for a standard button, used by the property route.
fn standard_button_token(button: StandardButton) -> &'static str {
    match button {
        StandardButton::Ok => "ok",
        StandardButton::Cancel => "cancel",
        StandardButton::Yes => "yes",
        StandardButton::No => "no",
        StandardButton::YesAll => "yes_all",
        StandardButton::NoAll => "no_all",
        StandardButton::Save => "save",
        StandardButton::Discard => "discard",
        StandardButton::Apply => "apply",
        StandardButton::Close => "close",
        StandardButton::Abort => "abort",
        StandardButton::Retry => "retry",
        StandardButton::Ignore => "ignore",
        StandardButton::Help => "help",
    }
}

/// Parses a token produced by [`standard_button_token`].
fn standard_button_from_token(token: &str) -> Option<StandardButton> {
    Some(match token {
        "ok" => StandardButton::Ok,
        "cancel" => StandardButton::Cancel,
        "yes" => StandardButton::Yes,
        "no" => StandardButton::No,
        "yes_all" => StandardButton::YesAll,
        "no_all" => StandardButton::NoAll,
        "save" => StandardButton::Save,
        "discard" => StandardButton::Discard,
        "apply" => StandardButton::Apply,
        "close" => StandardButton::Close,
        "abort" => StandardButton::Abort,
        "retry" => StandardButton::Retry,
        "ignore" => StandardButton::Ignore,
        "help" => StandardButton::Help,
        _ => return None,
    })
}

/// Message box dialog.
///
/// A ready-made modal prompt: it owns its title, body text, icon and the list of
/// standard buttons, and reports the user's answer through three signals rather
/// than a return value.
///
/// Note the widget does not dismiss itself: after `click_button` (or an Enter/
/// Escape key press) the signals fire but the dialog stays on screen, so the
/// owner is expected to react to `accepted`/`rejected` and close it.
///
/// As with every widget it inherits geometry, visibility, enablement and the
/// shared event plumbing from its [`BaseWidget`].
pub struct MessageBox {
    base: BaseWidget,
    title: String,
    text: String,
    /// A second, stronger line below the body (`informativeText`).
    informative_text: String,
    /// A muted detail block below the informative line (`detailedText`).
    detailed_text: String,
    /// Whether the detail block is expanded. Collapsed by default: the detail toggle is the
    /// control that owns this value, and `detailedText` is the text it reveals.
    detailed_text_expanded: bool,
    /// An optional check box above the action row: its label and its state.
    check_box_text: String,
    check_box_checked: bool,
    /// A caller-supplied picture that replaces the severity glyph.
    pixmap: Option<crate::image::Image>,
    /// The resampled pixmap: the square side it was resampled to and its RGBA bytes.
    ///
    /// # Why the raster is cached here rather than recomputed in `draw`
    ///
    /// `pixmap_pixels` allocates `side * side * 4` bytes and walks every pixel. Doing that on
    /// every frame made a static image cost the same as an animation. The key is the target
    /// side alone: `set_pixmap` clears this cache, so a different source can never be served
    /// from it.
    pixmap_raster: Option<(u32, Vec<u8>)>,
    icon: MessageBoxIcon,
    buttons: Vec<StandardButton>,
    default_button: Option<StandardButton>,
    /// Cached translated labels, keyed by the button set (G-P09).
    ///
    /// The draw path, `intrinsic_size` and the press hit test all need the same translated
    /// strings; translating them once per button-set change instead of once per call keeps
    /// the catalogue lookup off every frame.
    label_cache: RefCell<Option<(Vec<StandardButton>, Rc<Vec<String>>)>>,
    /// The button Escape activates; `None` keeps the Cancel/No/Close fallback chain.
    escape_button: Option<StandardButton>,
    /// The action button under the pointer, for hover feedback.
    hovered: Option<usize>,
    /// The action button currently held down, for pressed feedback.
    pressed: Option<usize>,
    modal: bool,
    /// How far the box has revealed, `0.0` hidden and `1.0` fully shown.
    ///
    /// # Why this box needs a driver while `Dialog` reads its own flag
    ///
    /// A `MessageBox` has no `show`/`hide` of its own: the box is created hidden and
    /// [`crate::app::handle::MessageBoxHandle::show_modal`] calls the crate-level
    /// `show_widget`, which sets the *base* visibility. So the transition this driver follows is
    /// the **inherited** `visible` flag, read once per [`Widget::tick`] and once per draw. That is
    /// the only place the box can learn that it has appeared, and deriving the target from it means
    /// no new API is needed for the animation to exist.
    ///
    /// The driver starts at `0.0` because a freshly built box is hidden, and `new_visible` starts
    /// at `false` so the first tick of a hidden box is not mistaken for a reveal.
    reveal: PropertyDriver,
    /// The base visibility the driver was last aimed from.
    ///
    /// Clocked in [`Widget::tick`], which is the one place both the frame bus and the tests reach.
    /// Kept so a `show_widget` that arrives between two ticks is picked up on the next one.
    new_visible: bool,
    /// Emitted for every button activation, including ones that also trigger
    /// [`Self::accepted`] or [`Self::rejected`]. Carries the button that was
    /// activated.
    pub button_clicked: Signal1<StandardButton>,
    /// Emitted when the box was answered affirmatively, i.e. with OK, Yes, Save
    /// or Apply. Carries no payload; read the button from
    /// [`Self::button_clicked`] if the distinction matters.
    pub accepted: GenericSignal,
    /// Emitted for every other button — Cancel, No, YesAll, NoAll, Discard,
    /// Close, Abort, Retry, Ignore and Help. Note that YesAll and NoAll are
    /// therefore *both* rejections by this rule.
    pub rejected: GenericSignal,
    /// Emitted when the detail block is expanded or collapsed, carrying the new state.
    pub details_toggled: Signal1<bool>,
    /// Emitted when Tab moves the focus ring; the payload is this box's own object id (the box
    /// is one `ObjectId`; its self-drawn buttons are not separate objects).
    pub focus_changed: Signal1<ObjectId>,
    /// The action button the keyboard focus ring is on.
    focus: Option<usize>,
}
impl MessageBox {
    /// Creates an empty message box with no title or text, no icon, a single OK
    /// button as both the only and the default button, and modal mode enabled.
    pub fn new(geometry: Rect) -> Self {
        Self {
            base: BaseWidget::new(WidgetKind::MessageBox, geometry, "MessageBox"),
            title: String::new(),
            text: String::new(),
            informative_text: String::new(),
            detailed_text: String::new(),
            detailed_text_expanded: false,
            check_box_text: String::new(),
            check_box_checked: false,
            pixmap: None,
            pixmap_raster: None,
            icon: MessageBoxIcon::NoIcon,
            buttons: vec![StandardButton::Ok],
            default_button: Some(StandardButton::Ok),
            label_cache: RefCell::new(None),
            escape_button: None,
            hovered: None,
            pressed: None,
            modal: true,
            // At rest at the hidden end: a freshly built box is not shown, so it must not animate
            // itself away on its first frame.
            reveal: PropertyDriver::at(0.0, MotionSlot::Normal),
            new_visible: false,
            button_clicked: Signal1::new(),
            accepted: GenericSignal::new(),
            rejected: GenericSignal::new(),
            details_toggled: Signal1::new(),
            focus_changed: Signal1::new(),
            focus: None,
        }
    }
    /// Creates a question prompt: [`MessageBoxIcon::Question`] with Yes and No
    /// buttons, Yes being the default.
    pub fn question(geometry: Rect, title: impl Into<String>, text: impl Into<String>) -> Self {
        let mut mb = Self::new(geometry);
        mb.title = title.into();
        mb.text = text.into();
        mb.icon = MessageBoxIcon::Question;
        mb.buttons = vec![StandardButton::Yes, StandardButton::No];
        mb.default_button = Some(StandardButton::Yes);
        mb
    }
    /// Creates an informational box: [`MessageBoxIcon::Information`], leaving the
    /// default OK button and modal flag from [`Self::new`] in place.
    pub fn information(geometry: Rect, title: impl Into<String>, text: impl Into<String>) -> Self {
        let mut mb = Self::new(geometry);
        mb.title = title.into();
        mb.text = text.into();
        mb.icon = MessageBoxIcon::Information;
        mb
    }
    /// Creates a warning box: [`MessageBoxIcon::Warning`], still with a single,
    /// default OK button.
    pub fn warning(geometry: Rect, title: impl Into<String>, text: impl Into<String>) -> Self {
        let mut mb = Self::new(geometry);
        mb.title = title.into();
        mb.text = text.into();
        mb.icon = MessageBoxIcon::Warning;
        mb
    }
    /// Creates an error box: [`MessageBoxIcon::Critical`], still with a single,
    /// default OK button.
    pub fn critical(geometry: Rect, title: impl Into<String>, text: impl Into<String>) -> Self {
        let mut mb = Self::new(geometry);
        mb.title = title.into();
        mb.text = text.into();
        mb.icon = MessageBoxIcon::Critical;
        mb
    }
    /// The current title, empty if none was set.
    pub fn title(&self) -> &str {
        &self.title
    }
    /// The current body text, empty if none was set.
    pub fn text(&self) -> &str {
        &self.text
    }
    /// The current icon, [`MessageBoxIcon::NoIcon`] by default.
    pub fn icon(&self) -> MessageBoxIcon {
        self.icon
    }
    /// The configured buttons, in the order they were supplied.
    ///
    /// May be empty, in which case the user has no way to answer the box.
    pub fn buttons(&self) -> &[StandardButton] {
        &self.buttons
    }
    /// The button activated by Enter, or `None` if Enter should do nothing.
    ///
    /// Not validated against [`Self::buttons`]: the default can name a button that
    /// is not currently shown, and Enter will still activate it.
    pub fn default_button(&self) -> Option<StandardButton> {
        self.default_button
    }
    /// Replaces the title and requests a redraw.
    pub fn set_title(&mut self, title: impl Into<String>) {
        let title = title.into();
        if self.title == title {
            return;
        }
        self.title = title;
        self.base.request_redraw();
    }
    /// Replaces the body text and requests a redraw.
    pub fn set_text(&mut self, text: impl Into<String>) {
        let text = text.into();
        // A same-value write cannot change the painted message, so it must not queue a frame
        // either (WDGT-12).
        if self.text == text {
            return;
        }
        self.text = text;
        self.base.request_redraw();
    }

    /// The informative text painted below the body.
    pub fn informative_text(&self) -> &str {
        &self.informative_text
    }

    /// Replaces the informative text and requests a redraw when it changes.
    pub fn set_informative_text(&mut self, text: impl Into<String>) {
        let text = text.into();
        if self.informative_text == text {
            return;
        }
        self.informative_text = text;
        self.base.request_redraw();
    }

    /// The detailed text painted below the informative line.
    pub fn detailed_text(&self) -> &str {
        &self.detailed_text
    }

    /// Replaces the detailed text and requests a redraw when it changes.
    pub fn set_detailed_text(&mut self, text: impl Into<String>) {
        let text = text.into();
        if self.detailed_text == text {
            return;
        }
        self.detailed_text = text;
        // An empty block has nothing to reveal, so an expanded flag left behind would be a
        // state no visible control can clear.
        if self.detailed_text.is_empty() && self.detailed_text_expanded {
            self.detailed_text_expanded = false;
            self.details_toggled.emit(false);
        }
        self.base.request_redraw();
    }

    /// Whether the detail block is expanded.
    pub fn is_detailed_text_expanded(&self) -> bool {
        self.detailed_text_expanded
    }

    /// Expands or collapses the detail block, emitting `details_toggled` on a real change.
    ///
    /// A blank `detailed_text` has nothing to reveal, so the write is refused rather than
    /// recording an expanded state no toggle can reach.
    pub fn set_detailed_text_expanded(&mut self, expanded: bool) {
        if self.detailed_text.is_empty() || self.detailed_text_expanded == expanded {
            return;
        }
        self.detailed_text_expanded = expanded;
        self.base.request_redraw();
        self.details_toggled.emit(expanded);
    }

    /// The optional check box row's label, or `""` when no row is painted.
    pub fn check_box_text(&self) -> &str {
        &self.check_box_text
    }

    /// Installs a check box row above the actions. An empty label removes it.
    pub fn set_check_box(&mut self, label: impl Into<String>) {
        let label = label.into();
        if self.check_box_text == label {
            return;
        }
        self.check_box_text = label;
        self.base.request_redraw();
    }

    /// Whether the check box is checked.
    pub fn is_check_box_checked(&self) -> bool {
        self.check_box_checked
    }

    /// Sets the check box state and requests a redraw when it changes.
    pub fn set_check_box_checked(&mut self, checked: bool) {
        if self.check_box_checked == checked {
            return;
        }
        self.check_box_checked = checked;
        self.base.request_redraw();
    }

    /// The picture drawn in place of the severity glyph, if one was set.
    pub fn pixmap(&self) -> Option<&crate::image::Image> {
        self.pixmap.as_ref()
    }

    /// Replaces the severity picture and requests a redraw when it changes.
    ///
    /// The picture takes precedence over [`MessageBox::icon`] when both are set.
    pub fn set_pixmap(&mut self, pixmap: Option<crate::image::Image>) {
        let unchanged = match (&self.pixmap, &pixmap) {
            (None, None) => true,
            (Some(current), Some(next)) => {
                current.width() == next.width()
                    && current.height() == next.height()
                    && current.rgba8_data() == next.rgba8_data()
            }
            _ => false,
        };
        if unchanged {
            return;
        }
        self.pixmap = pixmap;
        self.pixmap_raster = None;
        self.base.request_redraw();
    }

    /// The button Escape activates, or `None` for the Cancel/No/Close fallback.
    pub fn escape_button(&self) -> Option<StandardButton> {
        self.escape_button
    }

    /// Sets the button Escape activates. `None` restores the fallback chain.
    pub fn set_escape_button(&mut self, button: Option<StandardButton>) {
        if self.escape_button == button {
            return;
        }
        self.escape_button = button;
        self.base.request_redraw();
    }

    /// Sets the button Enter activates, or clears it with `None`.
    pub fn set_default_button_option(&mut self, button: Option<StandardButton>) {
        if self.default_button == button {
            return;
        }
        self.default_button = button;
        self.base.request_redraw();
    }
    /// Replaces the icon and requests a redraw.
    pub fn set_icon(&mut self, icon: MessageBoxIcon) {
        if self.icon == icon {
            return;
        }
        self.icon = icon;
        self.base.request_redraw();
    }
    /// Replaces the visible buttons and requests a redraw.
    ///
    /// Does not touch [`Self::default_button`], so a stale default may remain
    /// reachable via Enter after this call.
    pub fn set_buttons(&mut self, buttons: Vec<StandardButton>) {
        self.buttons = buttons;
        self.base.request_redraw();
    }
    /// Sets which button Enter activates and requests a redraw.
    ///
    /// Takes a value and keeps the button set. Use
    /// [`MessageBox::set_default_button_option`] (or the writable `default_button`
    /// property with `Null`) to clear it so Enter does nothing.
    pub fn set_default_button(&mut self, btn: StandardButton) {
        self.default_button = Some(btn);
        self.base.request_redraw();
    }
    /// How far the box has revealed: `0.0` hidden, `1.0` fully shown.
    ///
    /// The value the draw measures with, as distinct from the inherited [`Widget::is_visible`],
    /// which answers the logical state the instant it changes.
    pub fn reveal_progress(&self) -> f32 {
        self.reveal.value()
    }

    /// Whether the box is marked modal. Defaults to `true`.
    ///
    /// This records the intent; enforcement is the modal stack in
    /// [`crate::widget::runtime`] (`enter_modal` / `exit_modal`), which
    /// `MessageBoxHandle::show_modal` drives so input outside the box is blocked.
    pub fn is_modal(&self) -> bool {
        self.modal
    }

    /// Marks the box modal or not and requests a redraw.
    pub fn set_modal(&mut self, modal: bool) {
        if self.modal == modal {
            return;
        }
        self.modal = modal;
        self.base.request_redraw();
    }

    /// Simulates the user activating `btn`: emits [`Self::button_clicked`], then
    /// either [`Self::accepted`] or [`Self::rejected`].
    ///
    /// The accept/reject split is by button identity, not by context: OK, Yes,
    /// Save and Apply accept, and *every* other button rejects — including YesAll,
    /// Close and Help. Any button at all may be passed, even one not in
    /// [`Self::buttons`]; no validation is performed. The dialog is not dismissed
    /// and no redraw is requested.
    pub fn click_button(&mut self, btn: StandardButton) {
        self.button_clicked.emit(btn);
        match btn {
            StandardButton::Ok
            | StandardButton::Yes
            | StandardButton::Save
            | StandardButton::Apply => {
                self.accepted.emit();
            }
            _ => {
                self.rejected.emit();
            }
        }
    }
    /// The icon drawn for the current severity: `Info` for information, `Help` for a
    /// question, `Warning` for a warning, `Error` for critical, and `None` for
    /// [`MessageBoxIcon::NoIcon`].
    ///
    /// These are `IconName` **outlines**, not text symbols: `ℹ`/`⚠`/`✗` are covered by no
    /// bundled face, so drawing them through the text pipeline fell back to 8x8 bitmap blocks.
    fn icon_name(&self) -> Option<IconName> {
        match self.icon {
            MessageBoxIcon::Information => Some(IconName::Info),
            MessageBoxIcon::Question => Some(IconName::Help),
            MessageBoxIcon::Warning => Some(IconName::Warning),
            MessageBoxIcon::Critical => Some(IconName::Error),
            MessageBoxIcon::NoIcon => None,
        }
    }
    /// The colour used to draw [`Self::icon_symbol`]: the theme's semantic token for
    /// the icon's severity, so the indicator moves with the appearance instead of
    /// being pinned to a literal. The literals that used to be here were the *light*
    /// palette's values, so a severity indicator stayed light-blue on a dark dialog.
    ///
    /// [`MessageBoxIcon::NoIcon`] draws no glyph at all, so its arm never reaches the
    /// pixels; it still resolves a token rather than a literal so the match stays total.
    fn icon_color(&self) -> Color {
        let token = match self.icon {
            MessageBoxIcon::Information | MessageBoxIcon::Question => SemanticColor::Info,
            MessageBoxIcon::Warning => SemanticColor::Warning,
            MessageBoxIcon::Critical => SemanticColor::Error,
            MessageBoxIcon::NoIcon => SemanticColor::Info,
        };
        // `semantic_color` takes and releases the manager's lock and returns an owned
        // colour, so no guard outlives the call.
        crate::style::semantic_color(token).unwrap_or_else(|| self.icon_color_fallback())
    }

    /// The literal last resort behind [`Self::icon_color`], used only when no theme is
    /// active and therefore no token can be resolved.
    fn icon_color_fallback(&self) -> Color {
        match self.icon {
            MessageBoxIcon::Information => Color::rgb(0, 120, 215),
            MessageBoxIcon::Question => Color::rgb(0, 120, 215),
            MessageBoxIcon::Warning => Color::rgb(255, 140, 0),
            MessageBoxIcon::Critical => Color::rgb(196, 43, 28),
            MessageBoxIcon::NoIcon => Color::rgb(0, 0, 0),
        }
    }
}
impl Widget for MessageBox {
    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
    }

    fn size_hint(&self) -> Size {
        crate::core::Size::new(
            crate::widget::metrics::dimensions::MESSAGE_BOX_DEFAULT_WIDTH,
            crate::widget::metrics::dimensions::MESSAGE_BOX_DEFAULT_HEIGHT,
        )
    }

    /// Reports this widget as the object that paints it.
    ///
    /// `MessageBox` implements `Draw`, so `Some(self)` is total and cannot be
    /// wrong.
    fn as_draw_mut(&mut self) -> Option<&mut dyn crate::widget::Draw> {
        Some(self)
    }

    /// Advances the reveal by `delta_ms`; `true` while it is still moving.
    ///
    /// The base visibility is sampled here rather than in the draw, because this is the one point
    /// both the frame bus and the caller reach with `&mut self` and a delta — a target derived in
    /// the draw would be re-derived on every repaint and could never "arrive".
    fn tick(&mut self, delta_ms: u32) -> bool {
        let visible = self.base.is_visible();
        if visible != self.new_visible {
            self.new_visible = visible;
            self.reveal.set_target(if visible { 1.0 } else { 0.0 });
        }
        self.reveal.tick(delta_ms)
    }

    /// Whether the box is between two reveal states -- answers only, never advances.
    fn is_animating(&self) -> bool {
        self.reveal.is_moving()
    }

    impl_widget_property_hooks!();
    /// Resolves a published event name to the control's own signal.
    ///
    /// # Why this is explicit
    ///
    /// `connect_event` validates a name against the capability table and registers a hub slot; only
    /// `event_signal_dyn` joins that name to the signal the control actually **emits**. Without an
    /// arm a published name is valid and inert, which is the silent failure
    /// `tools/check_event_signal_dyn.sh` exists to make impossible: the arm set is compared against
    /// the capability's published names, so the two cannot drift.
    ///
    /// # Payload mapping
    ///
    /// A hub name carries no value, so a `Signal1<T>` needs a conversion. The one used here is the
    /// same spelling the property side uses, so a designer reads one representation per kind: a
    /// number as `Int`, a float as `Float`, a flag as `Bool`, text as `String`, and a structured
    /// payload as its debug spelling. A `None` maps to `Null`, which is the same value an absent
    /// optional property uses — so "no selection" is distinguishable from "selection 0".
    fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
        use crate::signal::EventSignalRef;
        match name {
            "button_clicked" => {
                Some(EventSignalRef::mapped("button_clicked", &self.button_clicked, |v| {
                    CapabilityValue::String(format!("{v:?}"))
                }))
            }
            "accepted" => Some(EventSignalRef::unit("accepted", &self.accepted)),
            "rejected" => Some(EventSignalRef::unit("rejected", &self.rejected)),
            "details_toggled" => {
                Some(EventSignalRef::mapped("details_toggled", &self.details_toggled, |v| {
                    CapabilityValue::Bool(*v)
                }))
            }
            "focus_changed" => {
                Some(EventSignalRef::mapped("focus_changed", &self.focus_changed, |v| {
                    CapabilityValue::UInt(*v)
                }))
            }
            _ => None,
        }
    }
}

/// `MessageBox`'s property contract.
///
/// Read semantics are carried over unchanged from the centralised
/// `access_read_dialog.in.rs` dispatch. Both properties are read-only: the old
/// write layer had no arm for this kind.
impl WidgetProperties for MessageBox {
    /// Returns `"title"`, `"text"` and `"icon"`, delegating anything else to the
    /// base widget's shared properties such as geometry and visibility.
    ///
    /// `"icon"` answers the current [`MessageBoxIcon`] as its lowercase spelling
    /// (`"warning"`, `"critical"`, …) — see [`expect_message_box_icon`] for the
    /// accepted set, which is the same one the JSON loader uses.
    fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
        match name {
            "title" => Ok(CapabilityValue::String(self.title().to_string())),
            "text" => Ok(CapabilityValue::String(self.text().to_string())),
            "icon" => Ok(CapabilityValue::String(message_box_icon_to_str(self.icon()).to_string())),
            "modal" => Ok(CapabilityValue::Bool(self.is_modal())),
            "informative_text" => Ok(CapabilityValue::String(self.informative_text.clone())),
            "detailed_text" => Ok(CapabilityValue::String(self.detailed_text.clone())),
            "check_box" => Ok(CapabilityValue::String(self.check_box_text.clone())),
            "check_box_checked" => Ok(CapabilityValue::Bool(self.check_box_checked)),
            "default_button" => Ok(match self.default_button {
                Some(button) => CapabilityValue::String(standard_button_token(button).to_string()),
                None => CapabilityValue::Null,
            }),
            "escape_button" => Ok(match self.escape_button {
                Some(button) => CapabilityValue::String(standard_button_token(button).to_string()),
                None => CapabilityValue::Null,
            }),
            _ => base_property_get(self, name),
        }
    }

    /// Accepts `"title"`, `"text"` and `"icon"`, requiring a string value for each
    /// and rejecting other types with [`CapabilityAccessError`]. Other names fall
    /// through to the base widget's setters. Every write requests a redraw.
    ///
    /// The icon was previously unreachable from this route even though the control
    /// draws it and offers `warning()` / `critical()` constructors: a caller using
    /// the documented property API got `UnknownProperty` for a property the widget
    /// plainly has.
    fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
        match name {
            "title" => {
                self.set_title(expect_string(value)?);
                Ok(())
            }
            "text" => {
                self.set_text(expect_string(value)?);
                Ok(())
            }
            "icon" => {
                self.set_icon(expect_message_box_icon(value)?);
                Ok(())
            }
            "modal" => {
                self.set_modal(expect_bool(value)?);
                Ok(())
            }
            "informative_text" => {
                self.set_informative_text(expect_string(value)?);
                Ok(())
            }
            "detailed_text" => {
                self.set_detailed_text(expect_string(value)?);
                Ok(())
            }
            "check_box" => {
                self.set_check_box(expect_string(value)?);
                Ok(())
            }
            "check_box_checked" => {
                self.set_check_box_checked(expect_bool(value)?);
                Ok(())
            }
            "default_button" => {
                let button = match value {
                    CapabilityValue::Null => None,
                    other => Some(
                        standard_button_from_token(&expect_string(other)?)
                            .ok_or(CapabilityAccessError::OutOfRange)?,
                    ),
                };
                self.set_default_button_option(button);
                Ok(())
            }
            "escape_button" => {
                let button = match value {
                    CapabilityValue::Null => None,
                    other => Some(
                        standard_button_from_token(&expect_string(other)?)
                            .ok_or(CapabilityAccessError::OutOfRange)?,
                    ),
                };
                self.set_escape_button(button);
                Ok(())
            }
            _ => base_property_set(self, name, value),
        }
    }

    /// `"title"`, `"text"`, `"icon"`, `"modal"`, then every universally supported base
    /// property.
    fn property_names(&self) -> &'static [&'static str] {
        // `modal` is listed because the schema publishes it: the getter and setter exist,
        // so a name that is declared but absent here is a property the designer offers and
        // the control cannot answer. This is the BLUE20 layer 2 Q1 defect the alignment gate
        // exists to find, and `BASE_PROPERTY_NAMES` does not carry it.
        property_names_of![
            "title",
            "text",
            "icon",
            "modal",
            "informative_text",
            "detailed_text",
            "check_box",
            "check_box_checked",
            "default_button",
            "escape_button",
            BASE_PROPERTY_NAMES
        ]
    }

    /// Runs one of the commands `message_box` publishes.
    ///
    /// The convenience constructors (`warning()`, `critical()`, `question()`,
    /// `information()`) cover the common cases, but a caller holding an already-built
    /// box needs a way to switch its severity — and a generic consumer offering a
    /// command palette needs a *name* for that. Both carry a payload, so both are
    /// answered through the property route, which is where the value goes.
    fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
        match name {
            "set_text" | "set_title" | "set_icon" => Err(CapabilityAccessError::OutOfRange),
            _ => Err(CapabilityAccessError::UnknownCommand),
        }
    }
}
/// Keyboard handling: Enter activates the default button and Escape activates
/// Cancel, falling back to No and then Close.
///
/// The event is always forwarded to the base widget first (which handles
/// enable/disable bookkeeping), and everything is ignored entirely while the
/// widget is disabled. Key codes are raw values — `13` for Enter and `27` for
/// Escape — rather than a key enum. Escape does nothing if none of Cancel, No or
/// Close is among the configured buttons, and Enter does nothing when
/// [`MessageBox::default_button`] is `None`.
///
/// A primary press is resolved against the **same** button rects the draw path painted (see
/// `action_row_geometry`), so the button a user sees under the pointer is the button that is
/// activated. Before this the box answered only keys: it painted an OK button that could not be
/// clicked, which is a control whose ink and whose hit region were two different things — the
/// defect rule 5 of the assembly spec names, and the one this crate has already recorded twice
/// (`tag_input`'s unreachable close button, `scroll_bar`'s non-inverse value mapping).
impl EventHandler for MessageBox {
    fn handle_event(&mut self, event: &Event) {
        self.base.handle_event(event);
        if !self.base.is_enabled() {
            return;
        }
        match event {
            Event::MouseMove { pos } => {
                let hovered = self.action_button_at(*pos);
                if hovered != self.hovered {
                    self.hovered = hovered;
                    self.base.request_redraw();
                }
            }
            Event::MousePress { pos, button: 1, .. } => {
                if let Some(index) = self.action_button_at(*pos) {
                    self.pressed = Some(index);
                    self.focus = Some(index);
                    self.base.request_redraw();
                    if let Some(activated) = self.buttons.get(index).copied() {
                        self.click_button(activated);
                    }
                    return;
                }
                // The detail toggle reveals or collapses the muted block above the actions.
                if self.details_toggle_rect().is_some_and(|rect| rect.contains_point(*pos)) {
                    self.set_detailed_text_expanded(!self.detailed_text_expanded);
                    return;
                }
                // A press on the check box toggles it, which is the row's whole purpose.
                if !self.check_box_text.is_empty()
                    && self.check_box_rect().is_some_and(|rect| rect.contains_point(*pos))
                {
                    self.check_box_checked = !self.check_box_checked;
                    self.base.request_redraw();
                }
            }
            Event::MouseRelease { .. } => {
                if self.pressed.take().is_some() {
                    self.base.request_redraw();
                }
            }
            Event::KeyPress { key, .. } => {
                if *key == key_codes::TAB {
                    self.move_focus();
                } else if *key == key_codes::ENTER || *key == key_codes::LINE_FEED {
                    // A focused button wins over the default; with no ring yet, Enter keeps
                    // activating the configured default button.
                    let button = self
                        .focus
                        .and_then(|index| self.buttons.get(index).copied())
                        .or(self.default_button);
                    if let Some(btn) = button {
                        self.click_button(btn);
                    }
                } else if *key == key_codes::ESCAPE {
                    // An explicitly configured escape button wins; otherwise the conventional
                    // Cancel/No/Close fallback chain applies.
                    if let Some(btn) = self.escape_button {
                        self.click_button(btn);
                    } else if self.buttons.contains(&StandardButton::Cancel) {
                        self.click_button(StandardButton::Cancel);
                    } else if self.buttons.contains(&StandardButton::No) {
                        self.click_button(StandardButton::No);
                    } else if self.buttons.contains(&StandardButton::Close) {
                        self.click_button(StandardButton::Close);
                    }
                }
            }
            _ => { /* Other events are not relevant */ }
        }
    }
}
/// The width the action buttons of every dialog in this module are given.
///
/// # Why this is a named constant and not a literal at each draw site
///
/// Eight dialogs declare an OK/Cancel pair, and before this each spelled the button's
/// width itself (`80`, sometimes as `BTN_W`). One fact with eight spellings is what rule
/// #101 forbids, and it is how the message box's row and the file dialog's row came to be
/// measured differently against the same frame.
///
/// The button is **not** allowed to grow with its label: unlike a toolbar item, a dialog's
/// action row is a fixed set of standard commands (`OK`, `Cancel`, `Yes to No`), and the
/// platform convention — and the standard button-box — is that they are all the same width
/// so the row reads as one control. `draw_text_fitted` elides a locale whose translation is
/// longer than the box, so a longer label loses characters rather than moving its siblings.
pub(crate) const DIALOG_BUTTON_WIDTH: i32 = 80;

/// The gap between two adjacent action buttons, and between the row and the content box's
/// edge: [`dimensions::BUTTON_ICON_SPACING`], the same 6 px that separates a control's own
/// parts elsewhere in this crate.
///
/// It is deliberately *not* `DIALOG_PADDING`: that is the frame's edge-to-content distance, and
/// rule 4 of the assembly spec keeps "edge to content" and "element to element" apart. Using
/// the frame padding for both made the row's right inset twice the gap between its buttons.
pub(crate) const DIALOG_BUTTON_SPACING: u32 = dimensions::BUTTON_ICON_SPACING;

/// The height of an action button's label: one line box.
///
/// Derived from the text layer rather than from [`dimensions::DIALOG_BUTTON_HEIGHT`], which is
/// the *row* height other dialogs reserve. Presenting this through [`Hints::preferred`] is what
/// lets the row's layout answer "how wide are you" from a real measurement, and it keeps a
/// button's own height tied to the font it draws its label in.
pub(crate) fn action_button_hints(context: &RenderContext, font: &Font, label: &str) -> Hints {
    let line = context.measure_text(label, font).height.max(1);
    Hints::fixed(DIALOG_BUTTON_WIDTH as u32, line)
}

impl MessageBox {
    /// The frame the message box actually paints: at most its intrinsic size, centred in
    /// the area it was given.
    ///
    /// # Why the frame is not the caller's rectangle
    ///
    /// `rect` is the area the box is **offered** — a census cell, a parent's client area.
    /// Painting it verbatim drew a 240x120 census cell as a 240x120 slab whose title bar,
    /// message and button row were each pinned to a literal written for a wider default
    /// size. [`ControlMetrics::painted_box`] caps each axis at the box's own intrinsic size
    /// and centres what is left, and it clamps up to one pixel so a squeezed box stays
    /// visible. Every band below — the title strip, the icon/message row and the button row
    /// — is derived from this one rect.
    ///
    /// # Why the width is also content-driven here
    ///
    /// `DIALOG_MIN_WIDTH` is a floor, not the intrinsic size: the layout below is asked for
    /// the size of a row of buttons, and a row wider than 280 px grows the frame instead of
    /// running past it. A dialog whose action row left its own frame was the defect the old
    /// `rect.x + rect.width - count * (80 + 8)` derivation produced once a caller set a
    /// longer label.
    fn frame_rect(&self, context: &RenderContext) -> Rect {
        ControlMetrics::painted_box(self.base.geometry(), self.intrinsic_size(context))
    }

    /// The box this dialog needs: the action row's own width and the floor, component-wise.
    ///
    /// This is the shared implicit-size formula
    /// `implicitWidth = max(implicitBackgroundWidth + inset, implicitContentWidth + padding)`
    /// spelled with this crate's primitives, where the floor
    /// is the background and the measured row is the content.
    /// The translated action labels, served from a cache keyed by the button set.
    fn labels(&self) -> Rc<Vec<String>> {
        if let Some((buttons, labels)) = self.label_cache.borrow().as_ref() {
            if *buttons == self.buttons {
                return Rc::clone(labels);
            }
        }
        let labels = Rc::new(
            self.buttons.iter().map(|button| button.translated_label()).collect::<Vec<_>>(),
        );
        *self.label_cache.borrow_mut() = Some((self.buttons.clone(), Rc::clone(&labels)));
        labels
    }

    fn intrinsic_size(&self, context: &RenderContext) -> Size {
        // The labels are the *translated* ones, because those are the strings the row draws —
        // measuring the English `label()` while painting `translated_label()` is how a row
        // sized for "OK" ends up eliding "Annuler".
        let labels = self.labels();
        let row = action_row_geometry(
            context,
            effective_font(self.base.style()),
            &labels,
            Rect::new(0, 0, 0, 0),
            false,
        );
        let row_height = dimensions::DIALOG_TITLE_BAR_HEIGHT.max(row.row.height);
        let height = dimensions::DIALOG_TITLE_BAR_HEIGHT
            .saturating_add(row_height)
            .max(dimensions::DIALOG_MIN_HEIGHT);
        Size::new(row.row.width.max(dimensions::DIALOG_MIN_WIDTH), height)
    }

    /// The check box row's rectangle, when one is installed.
    fn check_box_rect(&self) -> Option<Rect> {
        if self.check_box_text.is_empty() {
            return None;
        }
        let backend_size =
            Size::new(self.base.geometry().width.max(1), self.base.geometry().height.max(1));
        let mut backend = crate::render::SoftwarePaintBackend::new(backend_size, 1.0);
        let context = RenderContext::new(&mut backend);
        let rect = self.frame_rect(&context);
        let body =
            ControlMetrics::content_below_top_band(rect, dimensions::DIALOG_TITLE_BAR_HEIGHT);
        let button_band = inset_action_band(
            ControlMetrics::bottom_band(body, dimensions::DIALOG_BUTTON_HEIGHT),
            body.y,
        );
        let row_h = dimensions::DIALOG_BUTTON_HEIGHT.min(rect.height);
        let row_y = button_band.y.saturating_sub(row_h as i32).saturating_sub(4);
        Some(Rect::new(button_band.x, row_y, button_band.width, row_h))
    }

    /// The detail toggle's rectangle, when there is detail text to toggle.
    ///
    /// The toggle sits immediately above the check box row (or above the action row when no
    /// check box is installed), so the two optional rows stack without overlapping.
    fn details_toggle_rect(&self) -> Option<Rect> {
        if self.detailed_text.is_empty() {
            return None;
        }
        let backend_size =
            Size::new(self.base.geometry().width.max(1), self.base.geometry().height.max(1));
        let mut backend = crate::render::SoftwarePaintBackend::new(backend_size, 1.0);
        let context = RenderContext::new(&mut backend);
        let rect = self.frame_rect(&context);
        let body =
            ControlMetrics::content_below_top_band(rect, dimensions::DIALOG_TITLE_BAR_HEIGHT);
        let button_band = inset_action_band(
            ControlMetrics::bottom_band(body, dimensions::DIALOG_BUTTON_HEIGHT),
            body.y,
        );
        let row_h = dimensions::DIALOG_BUTTON_HEIGHT.min(rect.height);
        let mut row_y = button_band.y.saturating_sub(row_h as i32).saturating_sub(4);
        if !self.check_box_text.is_empty() {
            row_y = row_y.saturating_sub(row_h as i32).saturating_sub(4);
        }
        if row_y < body.y {
            return None;
        }
        Some(Rect::new(button_band.x, row_y, button_band.width, row_h))
    }

    /// The toggle's label: the resource label for its next state.
    fn details_toggle_label(&self) -> String {
        if self.detailed_text_expanded {
            super::localized_label("dialog.message_box.hide_details", "Hide Details...")
        } else {
            super::localized_label("dialog.message_box.show_details", "Show Details...")
        }
    }

    /// Moves the focus ring across the action buttons and announces the move.
    fn move_focus(&mut self) {
        let next = super::move_focus(self.focus, self.buttons.len(), true);
        if next != self.focus {
            self.focus = next;
            self.focus_changed.emit(self.id());
            self.base.request_redraw();
        }
    }

    /// The index of the action button under `pos`, or `None`.
    ///
    /// # Why the band is reconstructed rather than stored
    ///
    /// The rects the buttons were painted in live only for the duration of a `draw`, and a
    /// press is delivered by an event that has no render context of its own. Rebuilding the
    /// band from the same derivations the draw used — `painted_box`, `top_band`,
    /// `bottom_band` — is what keeps the press and the ink on one geometry: those helpers are
    /// pure functions of the control's rectangle, so the same input gives the same band at any
    /// time. A *cached* rect would be the other option and it is the worse one, because a cache
    /// is a second copy of the truth that a geometry change can leave stale — which is
    /// precisely how a hit region comes to disagree with the drawing it is meant to describe.
    ///
    /// The measurement only needs a backend to ask for the line height, and the band only needs
    /// the control's rectangle, so the press path costs no state and no redraw.
    fn action_button_at(&self, pos: Point) -> Option<usize> {
        let backend_size =
            Size::new(self.base.geometry().width.max(1), self.base.geometry().height.max(1));
        let mut backend = crate::render::SoftwarePaintBackend::new(backend_size, 1.0);
        let context = RenderContext::new(&mut backend);
        let rect = self.frame_rect(&context);
        // The band the buttons are drawn in: the strip the title bar leaves, minus the row the
        // message occupies — the same two derivations `draw` uses, in the same order, and the
        // same `inset_action_band` that keeps the row off the frame's rounded corners.
        let body =
            ControlMetrics::content_below_top_band(rect, dimensions::DIALOG_TITLE_BAR_HEIGHT);
        let button_band = inset_action_band(
            ControlMetrics::bottom_band(body, dimensions::DIALOG_BUTTON_HEIGHT),
            body.y,
        );
        // `place` is true exactly when the box has buttons, which is the same condition `draw`
        // passes: a box with no buttons has no row to hit and `hit` on an empty row is `None`
        // anyway, but the flag also keeps the two call sites reading the same way.
        let labels = self.labels();
        let place = !self.buttons.is_empty();
        action_row_geometry(
            &context,
            effective_font(self.base.style()),
            &labels,
            button_band,
            place,
        )
        .hit(pos)
    }
}

impl Draw for MessageBox {
    fn draw(&mut self, context: &mut RenderContext) {
        // How far the box has revealed. A hidden box paints **nothing at all**: a message box is
        // created hidden and then shown, so painting a full frame while it is not shown would put a
        // prompt on the page the runtime is not routing input to. Before this it drew the scrim and
        // the whole frame unconditionally from the moment it was constructed.
        let reveal = self.reveal.value();
        if reveal <= 0.0 {
            return;
        }

        // ── The modal scrim ──
        //
        // Same backdrop as `Dialog`'s, for the same reason: a modal prompt's whole meaning is
        // that the page behind it is inert, and that is only visible if the page is dimmed. The
        // `modal` flag already existed and was enforced by the modal stack, so the intent was
        // recorded and the *appearance* of it was not. The scrim fades in with the reveal, and
        // both are driven by the one value, so they cannot get out of step.
        if self.modal {
            super::draw_modal_scrim_scaled(context, self.geometry(), reveal);
        }

        // The **frame**, not the control's rectangle: see `frame_rect`.
        let rect = self.frame_rect(context);
        // The frame **scales up** from its own centre as the reveal progresses, so the appear reads
        // as a growth rather than a fade-in of a static box. At `reveal = 1.0` the scale is exactly
        // 1.0, so a settled box is pixel-identical to the pre-animation one — which is what keeps
        // the resting snapshot valid. Shared arithmetic with `Dialog`'s reveal, so the two modal
        // families settle the same way.
        let rect = super::scale_about_centre(
            rect,
            super::REVEAL_MIN_SCALE + (1.0 - super::REVEAL_MIN_SCALE) * reveal,
        );
        if rect.width == 0 || rect.height == 0 {
            return;
        }
        // Chrome colours resolve explicit style first, then the theme's resolved style for
        // this control, and only then a literal. The surface already read the style, but the
        // title bar and the buttons were literals, so a light/dark switch left them
        // unchanged and the census reported the control as theme-blind.
        //
        // The theme reads take and release the global manager's lock internally, so no
        // guard is held across the draw (the mutex is not re-entrant).
        let style = self.base.style();
        let theme = crate::style::resolved_theme_style_shared("message_box");
        // `message_box` is absent from `WidgetRole::for_kind_name`'s table — the variant
        // spells as `message_box`, but only `errordialog` is classified — so it resolves to
        // `theme.colors.background`: the colour the window behind it is already filled with.
        // A frame painted in that colour would be indistinguishable from the window, so a
        // resolved surface equal to the window fill is re-derived a visible step away from
        // it, the same distinction `Colors::input_background` draws for a field.
        let window_fill = {
            let manager = crate::style::theme_manager();
            manager.current_theme().map(|active| active.colors.background).unwrap_or(Color::WHITE)
        };
        let ink = style
            .text_color
            .or_else(|| theme.as_ref().and_then(|t| t.text_color))
            .unwrap_or(Color::rgb(40, 40, 40));
        let surface = match style
            .background_color
            .or_else(|| theme.as_ref().and_then(|t| t.background_color))
        {
            Some(resolved) if resolved != window_fill => resolved,
            _ => window_fill.blend(&ink, 0.06),
        };
        let border = style
            .border_color
            .or_else(|| theme.as_ref().and_then(|t| t.border_color))
            .filter(|resolved| *resolved != surface)
            .unwrap_or_else(|| surface.blend(&ink, 0.45));
        // The title bar is a distinct band on the frame, derived from it so the two stay one
        // visible step apart in either appearance. It used to be the literal brand blue.
        let title_bar = surface.blend(&ink, 0.08);
        // An ordinary button is a raised step on the frame; the default button is the
        // theme's action colour with text chosen for contrast against it.
        //
        // The default button used to read `theme.background_color` — the *window* fill, not the
        // action colour — so the one affirmative action in the box was painted the same colour as
        // the frame behind it and the census sample showed no primary button at all. `primary` is
        // the token every other dialog's accept button uses.
        let button_fill = surface.blend(&ink, 0.12);
        let primary = {
            let manager = crate::style::theme_manager();
            manager
                .current_theme()
                .map(|active| active.colors.primary)
                .unwrap_or(Color::rgb(0, 120, 215))
        };
        let primary_ink = primary.contrast_color();

        // A dialog is a normal widget: every element it emits has to stay inside its own
        // rectangle. None of these labels was bounded, so a long title, message or button
        // word ran past the frame — the raster backends clip that away, but the SVG
        // backend emits absolute coordinates and showed the overflow as drawing outside
        // the picture. Each label is fitted to the band it belongs to instead.
        // These labels are drawn with the **effective font** — the resolved theme/caller font — so
        // the title, body and buttons honour the theme body font and the user's text scale
        // (D09-STYLE-01).
        let font = effective_font(style);

        // Dialog background. Rounded by [`dimensions::DIALOG_RADIUS`] so a message box reads
        // as the same class of object as every other dialog; the radius is clamped to the
        // frame so a box smaller than its own corner is not drawn with an inverted one.
        let radius = dimensions::DIALOG_RADIUS.min(rect.width / 2).min(rect.height / 2);
        if radius > 0 {
            context.fill_rounded_rect(rect, radius, surface);
            context.draw_rounded_rect_stroke(rect, radius, border, 1);
        } else {
            context.face_with_gradient(
                rect,
                surface,
                self.style().background_gradient.as_ref(),
                self.style().surface.unwrap_or_default(),
                self.style().border_radius.unwrap_or(0),
                Color::BLACK,
            );
            context.draw_rect(rect, border);
        }
        // Title bar. The band is the label's own box, so the fit is measured against the
        // bar rather than against the dialog: a title longer than the bar truncates at the
        // bar's edge instead of leaving the control. The strip comes from `top_band`, which
        // keeps its thickness and clamps to the frame.
        let title_bar_band = ControlMetrics::top_band(rect, dimensions::DIALOG_TITLE_BAR_HEIGHT);
        context.fill_rect(title_bar_band, title_bar);
        if !self.title.is_empty() {
            let title_font = font;
            let title_line = context.text_line(title_bar_band, title_font);
            context.draw_text_fitted_cow(
                Rect::new(
                    rect.x + 8,
                    title_line.y,
                    rect.width.saturating_sub(16),
                    title_line.height.max(1),
                ),
                &self.title,
                title_font,
                ink,
                HorizontalAlignment::Left,
            );
        }

        // The icon/message row and the button row both live inside the space the title bar
        // leaves, which is what keeps a long message from starting under the strip and a
        // button row from overlapping the message on a short frame.
        //
        // The icon and the message share one band: the icon is a glyph at the left, the
        // message starts after it (or at the frame's own margin when there is no icon).
        let body =
            ControlMetrics::content_below_top_band(rect, dimensions::DIALOG_TITLE_BAR_HEIGHT);
        let icon = self.icon_name();
        let body_font = font;
        let body_line_h = context.measure_text("M", body_font).height.max(1) as i32;
        // The icon column is a fixed square at the frame's own margin, so a wide scalar cannot
        // overlap the message beside it and the message's start is the same whether or not a
        // severity icon is shown.
        let icon_left = 12.min(body.width as i32);
        let icon_side = body_line_h.max(1) as u32;
        // A caller-supplied picture replaces the glyph entirely, so the two never compete for
        // the same square. The pixels are resampled to the square because the draw primitive
        // takes a destination rectangle and a buffer already at that size.
        let pixmap_drawn = if self.pixmap.is_some() {
            let side = icon_side.max(16).min(body.width).min(body.height);
            if side > 0 {
                if self.pixmap_raster.as_ref().map(|(cached, _)| *cached) != Some(side) {
                    self.pixmap_raster = self
                        .pixmap
                        .as_ref()
                        .and_then(|pixmap| pixmap_pixels(pixmap, side, side))
                        .map(|pixels| (side, pixels));
                }
                self.pixmap_raster.as_ref().map(|(_, pixels)| {
                    let rect = Rect::new(body.x + icon_left, body.y, side, side);
                    context.draw_image(rect.x, rect.y, rect.width, rect.height, pixels);
                    icon_left + side as i32 + 8
                })
            } else {
                None
            }
        } else {
            None
        };
        let gutter = if let Some(gutter) = pixmap_drawn {
            gutter
        } else if let Some(name) = icon {
            // Centred on the body band's first line, so the icon and the first message line share a
            // centre line.
            let icon_rect = Rect::new(body.x + icon_left, body.y, icon_side, icon_side);
            crate::widget::draw_icon_at(context, icon_rect, self.icon_color(), name);
            icon_left + icon_side as i32 + 8
        } else {
            icon_left
        };

        // The button row is the bottom band; the message occupies what is left above it.
        //
        // The band is inset from the frame's own edges: a row flush with them put the last
        // button's corner on top of the frame's rounded corner (the census sample showed the OK
        // button cutting the bottom-right radius). `inset_action_band` is shared with
        // `action_button_at`, so the press and the ink still describe one geometry.
        let bottom_band = ControlMetrics::bottom_band(body, dimensions::DIALOG_BUTTON_HEIGHT);
        let button_band = inset_action_band(bottom_band, body.y);
        // The row is computed **before** the message band, because the message band's own
        // trailing edge is derived from the row. That is the sibling-column relation — the
        // text side's padding is the sibling column's width — and it is the whole reason
        // [`ActionRowGeometry::leading_inset`] exists: the message must stop where the buttons
        // begin, whatever the buttons' widths and their labels' translations turn out to be.
        let labels = self.labels();
        let row =
            action_row_geometry(context, font, &labels, button_band, !self.buttons.is_empty());
        let message_area =
            ControlMetrics::content_above_bottom_band(body, dimensions::DIALOG_BUTTON_HEIGHT);
        // The message band ends where the row begins, measured from the row rather than from a
        // literal written for one button count. `saturating_sub` because a row that fills the
        // frame leaves the message no trailing room, which is a message that elides rather than
        // one that runs under the buttons.
        let message_right = (message_area.width as i32 - row.leading_inset as i32).max(0);
        let message_band = Rect::new(
            message_area.x + gutter,
            message_area.y,
            (message_right - gutter).max(0) as u32,
            message_area.height,
        );
        // Guarded on the text being non-empty: an unguarded draw emits `<text …></text>`,
        // an empty element the rasteriser never produces. The line box is centred on the
        // message band through the shared primitive rather than at a literal `y`.
        // The message wraps at the band's own width instead of eliding: a long body is what a
        // message box exists for, and the baseline toolkit wraps it (DLG-03). The column
        // budget is derived from one measured glyph, the same rule the notice cards use.
        let glyph = context.measure_text("M", body_font).width.max(1);
        let columns = (message_band.width / glyph).max(1) as usize;
        let text_bottom = message_band.y + message_band.height as i32;
        // The first line keeps the vertical centre the single-line paint used, so a short
        // message is byte-identical to the pre-wrap frame; wrapped lines then advance by the
        // measured line height.
        let folded = wrap_words(&self.text, columns);
        let mut text_y = context.text_line(message_band, body_font).y;
        for line in folded.iter() {
            if text_y + body_line_h > text_bottom {
                break;
            }
            let band =
                Rect::new(message_band.x, text_y, message_band.width.max(1), body_line_h as u32);
            let line_box = context.text_line(band, body_font);
            context.draw_text_fitted_cow(
                Rect::new(band.x, line_box.y, band.width.max(1), line_box.height.max(1)),
                line,
                body_font,
                ink,
                HorizontalAlignment::Left,
            );
            text_y += body_line_h;
        }
        // The two extra text levels share the message's column and continue below it. Each is
        // painted only when it has ink, so a box that never sets them is byte-identical to the
        // one-row box this control always drew.
        let mut extra_y = if folded.len() <= 1 { message_band.y + body_line_h } else { text_y };
        let detail_text =
            if self.detailed_text_expanded { self.detailed_text.as_str() } else { "" };
        for (text, color) in
            [(self.informative_text.as_str(), ink), (detail_text, ink.with_alpha_f32(0.75))]
        {
            for line in wrap_words(text, columns).iter() {
                if line.is_empty() || extra_y + body_line_h > text_bottom {
                    continue;
                }
                let band = Rect::new(
                    message_band.x,
                    extra_y,
                    message_band.width.max(1),
                    body_line_h as u32,
                );
                let line_box = context.text_line(band, body_font);
                context.draw_text_fitted_cow(
                    Rect::new(band.x, line_box.y, band.width.max(1), line_box.height.max(1)),
                    line,
                    body_font,
                    color,
                    HorizontalAlignment::Left,
                );
                extra_y += body_line_h;
            }
        }

        // The check box row sits immediately above the action row, matching the notice cards.
        if !self.check_box_text.is_empty() {
            let row_h = dimensions::DIALOG_BUTTON_HEIGHT.min(rect.height);
            let row_y = button_band.y.saturating_sub(row_h as i32).saturating_sub(4);
            let row = Rect::new(button_band.x, row_y, button_band.width, row_h);
            let box_side = row_h.saturating_sub(8).min(14);
            let box_rect =
                Rect::new(row.x, row.y + ((row_h - box_side) / 2) as i32, box_side, box_side);
            context.fill_rect(box_rect, surface);
            context.draw_rect(box_rect, border);
            if self.check_box_checked {
                let inset = 3.min(box_side as i32 / 2);
                context.fill_rect(
                    Rect::new(
                        box_rect.x + inset,
                        box_rect.y + inset,
                        box_side.saturating_sub((inset * 2) as u32),
                        box_side.saturating_sub((inset * 2) as u32),
                    ),
                    primary,
                );
            }
            let label_band = Rect::new(
                box_rect.x + box_side as i32 + 6,
                row.y,
                row.width.saturating_sub(box_side + 6),
                row_h,
            );
            context.draw_text_line(
                label_band,
                &self.check_box_text,
                body_font,
                ink,
                HorizontalAlignment::Left,
            );
        }

        // The detail toggle is a text button, not a filled pad: it is a disclosure
        // affordance, not an action, and the muted label keeps that distinction visible.
        if let Some(toggle) = self.details_toggle_rect() {
            let toggle_band =
                Rect::new(toggle.x + 2, toggle.y, toggle.width.saturating_sub(4), toggle.height);
            context.draw_text_line(
                toggle_band,
                &self.details_toggle_label(),
                body_font,
                primary,
                HorizontalAlignment::Left,
            );
        }

        // Buttons, right-aligned as a row.
        //
        // # Why this is not `rect.x + rect.width - count * (80 + 8)`
        //
        // The old form computed each button's x from a fixed 88 px stride measured back from
        // the frame's right edge, and gave every button the same 80 px whatever its label
        // said. Two defects followed and both were visible in the census snapshot: the row's
        // total width was assumed rather than measured, so a row wider than the frame started
        // left of the dialog and the `max(rect.x)` floor silently overlapped its neighbours; and
        // a locale whose `Cancel` is twice as long elided its label from the middle of a
        // button it did not fit. Rule 4 of the assembly spec is that a segment's size comes
        // from its siblings, not from a literal — the sibling-column relation is the reference, where
        // the text side's padding is the *button column's own width*.
        //
        // The row is therefore a real layout: it is handed the buttons' measured hints plus
        // their inter-button margins, it computes each one's rect, and the *same* rects are what
        // is painted here and what [`ActionRowGeometry::hit`] resolves a press against — so the
        // ink and the hit test cannot disagree about where a button is.
        // `body_line_h` records the line box the rows were measured against, so a later
        // change to the reserved rows and this measurement cannot silently disagree.
        debug_assert!(body_line_h > 0);
        for (index, (button, button_rect)) in
            self.buttons.iter().zip(row.button_rects()).enumerate()
        {
            let is_default = self.default_button == Some(*button);
            let is_pressed = self.pressed == Some(index);
            let is_hovered = self.hovered == Some(index);
            let base_fill = if is_default { primary } else { button_fill };
            // Pressed ink is the fill stepped toward the ink it sits under; hover is a light
            // veil. Both are opt-in in the sense that they only appear while the pointer is
            // actually there, so a resting box is unchanged.
            let bg = if is_pressed {
                base_fill.blend(&ink, 0.22)
            } else if is_hovered {
                base_fill.blend(&ink, 0.08)
            } else {
                base_fill
            };
            let fg = if is_default { primary_ink } else { ink };
            context.fill_rect(*button_rect, bg);

            context.draw_rect(*button_rect, border);

            context.draw_text_line_cow(
                *button_rect,
                &button.translated_label(),
                font,
                fg,
                HorizontalAlignment::Center,
            );

            if self.focus == Some(index) {
                context.draw_rounded_rect_stroke(*button_rect, 3, primary, 2);
            }
        }
    }
}

/// Resamples `image` to `width x height` RGBA8 using nearest-neighbour sampling.
///
/// The draw primitive takes a destination rectangle but no source grid, so passing a
/// differently sized buffer is a crop on the raster backend. A message box is the only party
/// that knows both the source size and the square it painted, so it hands over a buffer that
/// is already the destination size. Returns `None` for a non-RGBA or empty image, which is
/// the honest "nothing to paint" answer.
pub(crate) fn pixmap_pixels(
    image: &crate::image::Image,
    width: u32,
    height: u32,
) -> Option<Vec<u8>> {
    let source = image.rgba8_data()?;
    let source_width = image.width();
    let source_height = image.height();
    if source.is_empty() || source_width == 0 || source_height == 0 || width == 0 || height == 0 {
        return None;
    }
    let mut pixels = vec![0u8; width as usize * height as usize * 4];
    for y in 0..height {
        let source_y = (y as u64 * source_height as u64 / height as u64) as u32;
        for x in 0..width {
            let source_x = (x as u64 * source_width as u64 / width as u64) as u32;
            let source_index = ((source_y * source_width + source_x) * 4) as usize;
            let target_index = ((y * width + x) * 4) as usize;
            if source_index + 4 <= source.len() {
                pixels[target_index..target_index + 4]
                    .copy_from_slice(&source[source_index..source_index + 4]);
            }
        }
    }
    Some(pixels)
}

/// The action-button row a dialog paints, computed once and used by both the ink and the
/// hit test.
/// # Why a type rather than three loose numbers
///
/// Every dialog needs the *same three facts* about its row — where the row starts, how wide it
/// is and where each button sits — and a caller that takes one of them without the others is
/// how a paint path and a hit test come to disagree about the same button. `buttons` is what
/// both consumers index.
pub(crate) struct ActionRowGeometry {
    /// The row's rect: the union of the buttons, inset to the content edge and right-anchored.
    pub row: Rect,
    /// Each button's rect, in the caller's own order.
    pub buttons: Vec<Rect>,
    /// The width from the row's leading edge to the content box's: the *derived* left inset the
    /// row leaves for whatever shares its band.
    pub leading_inset: u32,
}

impl ActionRowGeometry {
    /// The button rects as a slice.
    pub fn button_rects(&self) -> &[Rect] {
        &self.buttons
    }

    /// The button whose rect contains `pos`, if any.
    ///
    /// The hit test is deliberately a lookup in the rects the paint used rather than a second
    /// derivation from the index: a press must resolve to the button the user can see, and a
    /// separately computed region is how the two drift apart (this crate has recorded that
    /// defect twice — `tag_input`'s unreachable close button and `scroll_bar`'s non-inverse
    /// value mapping).
    pub fn hit(&self, pos: Point) -> Option<usize> {
        self.buttons.iter().position(|button| button.contains_point(pos))
    }
}

/// Lays `labels` out as a right-aligned row of action buttons inside `band`.
///
/// # What the row derives from
///
/// Each button's width is [`DIALOG_BUTTON_WIDTH`] and each gap is [`DIALOG_BUTTON_SPACING`],
/// both declared once so all eight dialogs that draw an OK/Cancel pair draw the same row. The
/// buttons' *hints* are measured from the labels the caller is about to paint, which is what
/// makes the row answer "how wide am I" truthfully instead of assuming a count.
///
/// # Why the row is anchored by its siblings
///
/// The row is placed by the running sum of the preceding buttons' widths plus the gaps between
/// them, measured back from the content box's trailing edge. A stride literal (`i * 88`) is the
/// defect this removes: it silently assumes every button is the same width *and* that the gaps
/// are, so the first change to either moves every later button by a multiple of the error, and
/// the row's own extent stops being a fact anything else can read — which is exactly what
/// `leading_inset` here gives back to the caller.
///
/// When `place` is false the row is returned in *unplaced* coordinates: it keeps the total
/// width a caller needs for an intrinsic-size calculation but does not pretend to know where a
/// zero-extent band is.
///
/// # Why the row is one pass over measured widths
///
/// BLUE22 §B.6 rule 2 is that a composite's children are positioned by a `Layout`, not by
/// `rect.x + k` — but this row is not a composite: it is eight dialogs' worth of *measurement*,
/// read by `draw`, by `intrinsic_size` and by the hit test on every press. It used to build a
/// `FlexLayout` (with an `ObjectId` per button and a `find` per result) on each of those calls,
/// for children that are all `Hints::fixed`: nothing grows, nothing shrinks, no `fill` is set, and
/// the row's measure box is exactly the children's own total — so the solver's entire answer is
/// the running sum written below. The width it spends comes from `action_button_hints`, which is
/// where the measurement (and therefore the scaling) already lives.
/// Insets a dialog's action row from the frame's own edges.
///
/// A row flush with the frame puts the last button's corner on top of the frame's rounded
/// corner — the census snapshot showed the OK button cutting the bottom-right radius. The same
/// inset is used by the paint and by the hit test, so widening or moving the row cannot make
/// the clickable button differ from the painted one.
pub(crate) fn inset_action_band(band: Rect, frame_top: i32) -> Rect {
    const MARGIN_X: u32 = 8;
    const MARGIN_BOTTOM: i32 = 4;
    Rect::new(
        band.x + MARGIN_X as i32,
        (band.y - MARGIN_BOTTOM).max(frame_top),
        band.width.saturating_sub(MARGIN_X * 2),
        band.height,
    )
}

pub(crate) fn action_row_geometry(
    context: &RenderContext,
    font: &Font,
    labels: &[String],
    band: Rect,
    place: bool,
) -> ActionRowGeometry {
    if labels.is_empty() {
        return ActionRowGeometry { row: band, buttons: Vec::new(), leading_inset: 0 };
    }
    // Each button's own wish. `Hints::fixed` rather than the widget's `size_hint` because an
    // action button is a standard command of a standard width: it does not grow with its
    // translation, it elides.
    let widths: Vec<u32> =
        labels.iter().map(|label| action_button_hints(context, font, label).width.pref).collect();
    // The gaps come out of the band *before* the anchor is computed, and only between buttons:
    // the row's own leading edge is a button's edge, not a gap.
    let gaps = DIALOG_BUTTON_SPACING.saturating_mul(widths.len().saturating_sub(1) as u32);
    let row_extent = widths.iter().fold(gaps, |total, width| total.saturating_add(*width)).max(1);
    // Where the row lands inside the band it was offered: `band` is the room the caller has,
    // `row_extent` is what the row needs, and the difference is anchored to the trailing edge —
    // which is what "right-aligned" means, and why the buttons sit at the dialog's own right
    // edge in every dialog that calls this rather than at an offset each one recomputes.
    //
    // A row *wider* than the band keeps its own width and is anchored to the band's leading
    // edge (`shift` floors at zero). Squeezing it instead would make the buttons narrower than
    // the labels they are about to draw, so every one of them would elide: a row of unreadable
    // commands is a worse answer than a row that overhangs the panel it sits in, and the latter
    // is what a dialog whose content is bigger than its frame already does everywhere else. The
    // frame itself is sized from this row's own width (`intrinsic_size`), so in practice the
    // overhang only arises when a host forces a smaller rectangle on the control.
    let shift = if place { (band.width as i32 - row_extent as i32).max(0) } else { 0 };
    let mut cursor = band.x + shift;
    let mut buttons = Vec::with_capacity(widths.len());
    for width in widths {
        buttons.push(Rect::new(cursor, band.y, width, band.height));
        cursor += width as i32 + DIALOG_BUTTON_SPACING as i32;
    }
    let first = buttons.first().copied().unwrap_or(band);
    let last = buttons.last().copied().unwrap_or(band);
    let row_left = first.x;
    let row_right = last.x.saturating_add(last.width as i32);
    let row = Rect::new(row_left, band.y, (row_right - row_left).max(0) as u32, band.height);
    // What the row leaves *before* it: the padding this dialog's own content must carry so its
    // title, message and icon cannot overlap the buttons that share their band. This is the
    // sibling-column relation — the text side's padding is the sibling column's width —
    // and it is why the row reports it instead of leaving each call site to re-subtract.
    let leading_inset = if place { (row_left - band.x).max(0) as u32 } else { 0 };
    ActionRowGeometry { row, buttons, leading_inset }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::{Point, Rect, Size};
    use crate::event::Event;
    use crate::render::SoftwarePaintBackend;
    use crate::widget::svg::render_to_svg;
    use std::sync::{Arc, Mutex};

    // ── 1. Creating default message box ─────────────────────────────

    /// A translated label must resolve through the catalogue, not echo its key.
    ///
    /// The catalogue is a process-global, so this initialises it explicitly rather
    /// than relying on some other test having done so.
    ///
    /// Gated on `i18n`: without the capability the English label is the whole answer,
    /// and that behaviour is asserted separately below so it is pinned in every profile.
    ///
    /// This is the assertion that the `desktop`-gated copy of `translated_label`
    /// would have failed on a `tablet`/`mobile` build: there the function was compiled
    /// into its "fallback" body, so `"common.button.ok"` came back as the literal key
    /// even though `i18n` was enabled and initialised.
    #[cfg(feature = "i18n")]
    #[test]
    fn translated_label_resolves_through_the_catalogue() {
        let _lock = crate::i18n::global_i18n_test_lock();
        crate::i18n::init();
        assert_eq!(
            StandardButton::Ok.translated_label(),
            "OK",
            "a built-in button label must translate rather than echo its catalogue key"
        );
        assert_eq!(StandardButton::Cancel.translated_label(), "Cancel");
    }

    /// The translated label must differ from the key for a key the catalogue knows.
    ///
    /// Stated as a separate property so a catalogue regression that made `translate`
    /// return its input would fail here and not only in the equality above.
    #[cfg(feature = "i18n")]
    #[test]
    fn translated_label_is_not_the_catalogue_key() {
        let _lock = crate::i18n::global_i18n_test_lock();
        crate::i18n::init();
        let translated = StandardButton::Yes.translated_label();
        assert_ne!(translated, "common.button.yes");
        assert!(!translated.is_empty(), "an unknown key still yields the key itself");
    }

    /// A build with `i18n` but **no initialised catalogue** must still show English.
    ///
    /// This is the defect the snapshot exposed: the exporter never called
    /// `i18n::init`, so `translate` returned `common.button.ok` and the 80 px button
    /// elided it to `common....`. The display fallback is the English label, not the
    /// key; `translated_label` is what every dialog draws.
    #[cfg(feature = "i18n")]
    #[test]
    fn translated_label_falls_back_to_english_without_a_catalogue() {
        let _lock = crate::i18n::global_i18n_test_lock();
        *crate::compat::lock(&crate::i18n::GLOBAL_I18N) = None;
        assert_eq!(StandardButton::Ok.translated_label(), "OK");
        assert_eq!(StandardButton::YesAll.translated_label(), "Yes to All");
        // Restore a catalogue so this test cannot change what a later one sees.
        crate::i18n::init();
    }

    /// Without the `i18n` capability the English label is the whole answer.
    #[cfg(not(feature = "i18n"))]
    #[test]
    fn translated_label_without_i18n_returns_the_english_label() {
        assert_eq!(StandardButton::Ok.translated_label(), "OK");
        assert_eq!(StandardButton::Cancel.translated_label(), "Cancel");
    }

    /// The action row is right-anchored to the content box's trailing edge, and every button
    /// after the first is offset by the preceding button's own width plus one spacing.
    ///
    /// # What this pins
    ///
    /// It used to be pinned as "the row is `count * (80 + 8)` wide starting at the frame's right
    /// edge", which a hardcoded row satisfies without ever measuring its buttons. The assertion
    /// here is instead the *derivation*: the second button's offset from the first is a function
    /// of the first button's width, so a change to either the spacing constant or the button
    /// width moves the pair together rather than desynchronising the stride from the buttons.
    #[test]
    fn the_action_row_is_right_anchored_and_each_button_follows_its_sibling() {
        let mut backend = SoftwarePaintBackend::new(Size::new(240, 120), 1.0);
        let ctx = RenderContext::new(&mut backend);
        let labels = vec!["OK".to_string(), "Cancel".to_string()];
        let band = Rect::new(0, 92, 240, 28);
        let row = action_row_geometry(&ctx, &Font::default(), &labels, band, true);
        assert_eq!(row.buttons.len(), 2, "one rect per label");
        // The last button ends at the content box's trailing edge: that is what "right-anchored"
        // means, and it is the property a count-based stride only satisfies by coincidence.
        let last = row.buttons[1];
        assert_eq!(
            last.x + last.width as i32,
            band.x + band.width as i32,
            "the row must be anchored to the band's trailing edge, not to a count-derived offset"
        );
        // The second button begins one spacing after the first ends.
        let first = row.buttons[0];
        assert_eq!(
            last.x - (first.x + first.width as i32),
            DIALOG_BUTTON_SPACING as i32,
            "the gap between two buttons must be the declared spacing"
        );
        assert_eq!(
            first.width, DIALOG_BUTTON_WIDTH as u32,
            "a standard command keeps its declared width instead of growing into the band"
        );
        assert_eq!(
            row.leading_inset,
            (row.row.x - band.x) as u32,
            "the inset a dialog's own content carries is measured from the band's leading edge"
        );
        assert_eq!(
            band.x + row.leading_inset as i32,
            row.row.x,
            "the leading inset ends exactly where the row begins, so nothing can overlap it"
        );
        assert_eq!(
            row.leading_inset + row.row.width,
            band.width,
            "the inset and the row tile the band, leaving no gap between them"
        );
    }

    /// A row wider than the band keeps its own widths instead of squeezing every label.
    ///
    /// This replaces a test that pinned the old `max(rect.x)` clamp as "no button leaves the
    /// band". That clamp did not achieve it — it moved the row's start while every button was
    /// still stepped by a fixed 88 px, so the buttons overlapped each other *and* the last one
    /// still ran past the frame's right edge. What is pinned now is the contract the helper
    /// actually implements and documents: a row too wide for its band is anchored to the band's
    /// leading edge and keeps the widths its labels were measured at, so nothing elides and no
    /// button overlaps its neighbour. `intrinsic_size` sizes the frame from this same row, so a
    /// dialog only meets this case when a host forces a smaller rectangle on it.
    #[test]
    fn a_row_wider_than_its_band_keeps_its_widths_and_its_leading_edge() {
        let mut backend = SoftwarePaintBackend::new(Size::new(240, 120), 1.0);
        let ctx = RenderContext::new(&mut backend);
        let labels: Vec<String> = (0..6).map(|i| format!("Button {i}")).collect();
        let band = Rect::new(0, 92, 120, 28);
        let row = action_row_geometry(&ctx, &Font::default(), &labels, band, true);
        assert_eq!(
            row.row.x, band.x,
            "an oversized row is anchored to the band's leading edge, not centred on it"
        );
        for (index, button) in row.button_rects().iter().enumerate() {
            assert_eq!(
                button.width, DIALOG_BUTTON_WIDTH as u32,
                "a squeezed band must not shrink the buttons: an elided command is unreadable"
            );
            if index > 0 {
                let previous = row.button_rects()[index - 1];
                assert_eq!(
                    button.x - (previous.x + previous.width as i32),
                    DIALOG_BUTTON_SPACING as i32,
                    "the gap between two buttons survives an oversized row"
                );
            }
        }
    }

    /// A press resolves against the rects that were painted, not against a second derivation.
    #[test]
    fn a_press_hits_the_button_that_was_drawn_there() {
        let mut backend = SoftwarePaintBackend::new(Size::new(240, 120), 1.0);
        let ctx = RenderContext::new(&mut backend);
        let labels = vec!["OK".to_string(), "Cancel".to_string()];
        let band = Rect::new(0, 92, 240, 28);
        let row = action_row_geometry(&ctx, &Font::default(), &labels, band, true);
        for (index, button) in row.button_rects().iter().enumerate() {
            let centre =
                Point::new(button.x + button.width as i32 / 2, button.y + button.height as i32 / 2);
            assert_eq!(row.hit(centre), Some(index), "the centre of {button:?} belongs to it");
        }
        // A point in the leftover room the row did not take belongs to no button.
        assert_eq!(row.hit(Point::new(band.x + 1, band.y + band.height as i32 / 2)), None);
    }

    #[test]
    fn test_default_creation() {
        let mb = MessageBox::new(Rect::new(100, 100, 300, 150));

        assert_eq!(mb.kind(), WidgetKind::MessageBox);
        assert_eq!(mb.geometry(), Rect::new(100, 100, 300, 150));
        assert!(mb.title().is_empty());
        assert!(mb.text().is_empty());
        assert_eq!(mb.icon(), MessageBoxIcon::NoIcon);
        assert_eq!(mb.buttons(), &[StandardButton::Ok]);
        assert_eq!(mb.default_button(), Some(StandardButton::Ok));
        assert!(mb.is_modal());
        assert!(mb.is_visible());
        assert!(mb.is_enabled());
    }

    // ── 2. Setting title and message text ───────────────────────────

    #[test]
    fn test_set_title_and_text() {
        let mut mb = MessageBox::new(Rect::new(0, 0, 300, 150));

        assert!(mb.title().is_empty());
        assert!(mb.text().is_empty());

        mb.set_title("Warning");
        assert_eq!(mb.title(), "Warning");

        mb.set_text("Are you sure?");
        assert_eq!(mb.text(), "Are you sure?");
    }

    // ── 3. Setting icon type ────────────────────────────────────────

    #[test]
    fn test_set_icon() {
        let mut mb = MessageBox::new(Rect::new(0, 0, 300, 150));

        assert_eq!(mb.icon(), MessageBoxIcon::NoIcon);

        mb.set_icon(MessageBoxIcon::Information);
        assert_eq!(mb.icon(), MessageBoxIcon::Information);

        mb.set_icon(MessageBoxIcon::Warning);
        assert_eq!(mb.icon(), MessageBoxIcon::Warning);

        mb.set_icon(MessageBoxIcon::Critical);
        assert_eq!(mb.icon(), MessageBoxIcon::Critical);

        mb.set_icon(MessageBoxIcon::Question);
        assert_eq!(mb.icon(), MessageBoxIcon::Question);

        mb.set_icon(MessageBoxIcon::NoIcon);
        assert_eq!(mb.icon(), MessageBoxIcon::NoIcon);
    }

    /// The icon must be reachable through the **property route**, not only through
    /// the inherent `set_icon`.
    ///
    /// A warning/error prompt carries a severity glyph, and the control always drew
    /// one — but `get`/`set` did not answer `"icon"`, `property_names` omitted it and
    /// the capability table did not declare it, so a caller using the documented
    /// property API got `UnknownProperty` for a property the widget plainly has,
    /// while the JSON loader accepted an `icon` key. This pins all four halves.
    #[test]
    fn message_box_icon_is_reachable_through_the_property_route() {
        use crate::widget::capability::types::CapabilityValue;

        let mut mb = MessageBox::new(Rect::new(0, 0, 300, 150));

        // Readable, and the default reads back as the `none` token.
        assert_eq!(
            mb.get("icon").expect("`icon` must be readable"),
            CapabilityValue::String("none".to_string())
        );

        // Writable, by the same spellings the JSON loader accepts.
        for (token, expected) in [
            ("information", MessageBoxIcon::Information),
            ("question", MessageBoxIcon::Question),
            ("warning", MessageBoxIcon::Warning),
            ("critical", MessageBoxIcon::Critical),
            ("none", MessageBoxIcon::NoIcon),
        ] {
            mb.set("icon", CapabilityValue::String(token.to_string()))
                .unwrap_or_else(|e| panic!("set(\"icon\", {token:?}) must be accepted: {e:?}"));
            assert_eq!(mb.icon(), expected, "token {token:?} must select {expected:?}");
            // Round-trips: what was written is what is read back.
            assert_eq!(mb.get("icon").unwrap(), CapabilityValue::String(token.to_string()));
        }

        // `error` is an accepted synonym for `critical`, because that is what a
        // caller writing an error prompt will reach for first.
        mb.set("icon", CapabilityValue::String("error".to_string()))
            .expect("`error` must be accepted as a synonym for `critical`");
        assert_eq!(mb.icon(), MessageBoxIcon::Critical);

        // The set is published, so a generic consumer can discover it.
        assert!(
            mb.property_names().contains(&"icon"),
            "`icon` must appear in property_names, or the schema promises a property the \
             contract does not publish"
        );

        // A bad token is refused rather than silently becoming `NoIcon` — the JSON
        // loader's `_ =>` fallback is deliberate there, but a direct write must not
        // guess.
        assert!(
            mb.set("icon", CapabilityValue::String("chartreuse".to_string())).is_err(),
            "an unknown icon token must be refused"
        );
        // And a wrong type is a type error, not a silent coercion.
        assert!(mb.set("icon", CapabilityValue::Bool(true)).is_err());
    }

    /// The convenience constructors set the severity a caller expects. A `warning()`
    /// that produced `NoIcon` would be the worst kind of silent bug: the prompt still
    /// appears, just without telling the user how serious it is.
    #[test]
    fn message_box_constructors_carry_their_severity() {
        let cases: [(MessageBox, MessageBoxIcon); 4] = [
            (
                MessageBox::warning(Rect::new(0, 0, 300, 150), "Disk almost full", "12 MB left"),
                MessageBoxIcon::Warning,
            ),
            (
                MessageBox::critical(Rect::new(0, 0, 300, 150), "Save failed", "Disk is full"),
                MessageBoxIcon::Critical,
            ),
            (
                MessageBox::information(Rect::new(0, 0, 300, 150), "Done", "Export finished"),
                MessageBoxIcon::Information,
            ),
            (
                MessageBox::question(
                    Rect::new(0, 0, 300, 150),
                    "Discard?",
                    "This cannot be undone",
                ),
                MessageBoxIcon::Question,
            ),
        ];

        for (mb, expected) in cases {
            assert_eq!(mb.icon(), expected);
            // The severity is also readable through the property route, so a caller
            // that builds a box one way can inspect it the other.
            assert_eq!(
                mb.get("icon").unwrap(),
                CapabilityValue::String(message_box_icon_to_str(expected).to_string())
            );
        }
    }

    // ── 4. Setting buttons ──────────────────────────────────────────

    #[test]
    fn test_set_buttons() {
        let mut mb = MessageBox::new(Rect::new(0, 0, 300, 150));

        // Default is Ok
        assert_eq!(mb.buttons(), &[StandardButton::Ok]);

        // OK/Cancel
        mb.set_buttons(vec![StandardButton::Ok, StandardButton::Cancel]);
        assert_eq!(mb.buttons(), &[StandardButton::Ok, StandardButton::Cancel]);

        // Yes/No
        mb.set_buttons(vec![StandardButton::Yes, StandardButton::No]);
        assert_eq!(mb.buttons(), &[StandardButton::Yes, StandardButton::No]);

        // Yes/No/Cancel
        mb.set_buttons(vec![StandardButton::Yes, StandardButton::No, StandardButton::Cancel]);
        assert_eq!(mb.buttons().len(), 3);
    }

    // ── 5. Default button ───────────────────────────────────────────

    #[test]
    fn test_default_button() {
        let mut mb = MessageBox::new(Rect::new(0, 0, 300, 150));

        assert_eq!(mb.default_button(), Some(StandardButton::Ok));

        mb.set_default_button(StandardButton::Cancel);
        assert_eq!(mb.default_button(), Some(StandardButton::Cancel));
    }

    // ── 6. Signal accessors ─────────────────────────────────────────

    #[test]
    fn test_accepted_signal() {
        let mut mb = MessageBox::new(Rect::new(0, 0, 300, 150));

        let fired = Arc::new(Mutex::new(false));
        mb.accepted.connect({
            let fired = Arc::clone(&fired);
            move || {
                *fired.lock().unwrap() = true;
            }
        });

        mb.click_button(StandardButton::Ok);
        assert!(*fired.lock().unwrap());
    }

    #[test]
    fn test_rejected_signal() {
        let mut mb = MessageBox::new(Rect::new(0, 0, 300, 150));

        let fired = Arc::new(Mutex::new(false));
        mb.rejected.connect({
            let fired = Arc::clone(&fired);
            move || {
                *fired.lock().unwrap() = true;
            }
        });

        mb.click_button(StandardButton::Cancel);
        assert!(*fired.lock().unwrap());
    }

    #[test]
    fn test_button_clicked_signal() {
        let mut mb = MessageBox::new(Rect::new(0, 0, 300, 150));

        let captured = Arc::new(Mutex::new(None::<StandardButton>));
        mb.button_clicked.connect({
            let captured = Arc::clone(&captured);
            move |val: Arc<StandardButton>| {
                *captured.lock().unwrap() = Some(*val);
            }
        });

        mb.click_button(StandardButton::Yes);
        assert_eq!(*captured.lock().unwrap(), Some(StandardButton::Yes));
    }

    // ── 7. Result retrieval (accepted/rejected) ─────────────────────

    #[test]
    fn test_click_button_accept_reject_pattern() {
        let mut mb = MessageBox::new(Rect::new(0, 0, 300, 150));

        let accepted = Arc::new(Mutex::new(false));
        let rejected = Arc::new(Mutex::new(false));

        mb.accepted.connect({
            let accepted = Arc::clone(&accepted);
            move || {
                *accepted.lock().unwrap() = true;
            }
        });
        mb.rejected.connect({
            let rejected = Arc::clone(&rejected);
            move || {
                *rejected.lock().unwrap() = true;
            }
        });

        // Ok, Yes, Save, Apply emit accepted
        mb.click_button(StandardButton::Ok);
        assert!(*accepted.lock().unwrap());
        assert!(!*rejected.lock().unwrap());

        // Reset
        *accepted.lock().unwrap() = false;

        mb.click_button(StandardButton::Cancel);
        assert!(!*accepted.lock().unwrap());
        assert!(*rejected.lock().unwrap());
    }

    // ── 8. Factory constructors ─────────────────────────────────────

    #[test]
    fn test_factory_constructors() {
        let info = MessageBox::information(Rect::new(0, 0, 300, 150), "Information", "File saved.");
        assert_eq!(info.title(), "Information");
        assert_eq!(info.text(), "File saved.");
        assert_eq!(info.icon(), MessageBoxIcon::Information);
        assert_eq!(info.buttons(), &[StandardButton::Ok]);
        assert_eq!(info.default_button(), Some(StandardButton::Ok));

        let warn = MessageBox::warning(Rect::new(0, 0, 300, 150), "Warning", "Low disk space");
        assert_eq!(warn.title(), "Warning");
        assert_eq!(warn.text(), "Low disk space");
        assert_eq!(warn.icon(), MessageBoxIcon::Warning);

        let err = MessageBox::critical(Rect::new(0, 0, 300, 150), "Error", "Operation failed");
        assert_eq!(err.title(), "Error");
        assert_eq!(err.text(), "Operation failed");
        assert_eq!(err.icon(), MessageBoxIcon::Critical);

        let q = MessageBox::question(Rect::new(0, 0, 300, 150), "Question", "Continue?");
        assert_eq!(q.title(), "Question");
        assert_eq!(q.text(), "Continue?");
        assert_eq!(q.icon(), MessageBoxIcon::Question);
        assert_eq!(q.buttons(), &[StandardButton::Yes, StandardButton::No]);
        assert_eq!(q.default_button(), Some(StandardButton::Yes));
    }

    // ── 9. Geometry delegation ─────────────────────────────────────

    #[test]
    fn test_geometry_delegation() {
        let mut mb = MessageBox::new(Rect::new(10, 20, 300, 150));

        assert_eq!(mb.geometry(), Rect::new(10, 20, 300, 150));

        mb.set_geometry(Rect::new(0, 0, 400, 200));
        assert_eq!(mb.geometry(), Rect::new(0, 0, 400, 200));
        assert_eq!(mb.geometry(), Rect::new(0, 0, 400, 200));
        assert_eq!(mb.position(), Point::new(0, 0));
        assert_eq!(mb.size(), crate::core::Size::new(400, 200));
    }

    // ── 10. Widget ID and kind ──────────────────────────────────────

    #[test]
    fn test_widget_id_and_kind() {
        let mb = MessageBox::new(Rect::new(0, 0, 300, 150));

        assert_eq!(mb.kind(), WidgetKind::MessageBox);
        assert_ne!(mb.id(), 0);

        let mb2 = MessageBox::new(Rect::new(0, 0, 200, 100));
        assert_ne!(mb.id(), mb2.id());
    }

    // ── 11. SVG output ──────────────────────────────────────────────

    #[test]
    fn test_svg_output() {
        let mut mb = MessageBox::new(Rect::new(0, 0, 300, 150));
        // A message box is created hidden and shown by the runtime; a hidden one paints nothing,
        // so a box under test is shown and settled first. An unshown box would make every
        // assertion below pass on an empty document.
        mb.show();
        while mb.tick(1000) {}

        // Default output
        let svg = render_to_svg(&mut mb);
        assert!(svg.starts_with("<svg"));
        assert!(svg.contains("xmlns=\"http://www.w3.org/2000/svg\""));
        assert!(svg.contains("width=\"300\""));
        assert!(svg.contains("height=\"150\""));
        // And it actually painted its chrome: the frame is a rounded rect and the default OK
        // button leaves a glyph run.
        assert!(svg.contains("rx=\""), "the shown box paints its frame: {svg}");
        assert!(
            crate::widget::svg::text_ink_box(&svg).is_some(),
            "the default OK button paints its label: {svg}"
        );

        // With title and text set
        let mut mb2 = MessageBox::new(Rect::new(0, 0, 400, 200));
        mb2.set_title("Test Title");
        mb2.set_text("Hello");
        mb2.set_icon(MessageBoxIcon::Warning);
        mb2.show();
        while mb2.tick(1000) {}
        let svg2 = render_to_svg(&mut mb2);
        assert!(svg2.starts_with("<svg"));
    }

    /// A hidden message box paints nothing, and showing it reveals the box rather than
    /// placing it there at full size.
    ///
    /// # The defect this pins
    ///
    /// The box has no `show`/`hide` of its own — the runtime's `show_widget` sets the *base*
    /// visibility — and the draw ignored both that flag and any notion of arrival, so a box that
    /// had been constructed but not yet shown painted a full frame with buttons on it, and a shown
    /// one appeared whole on a single frame. The assertions are in three parts, because a progress
    /// nothing reads is not a reveal: the model must take an interior value, the *painted* frame
    /// must be smaller at that moment, and a hidden box must paint nothing.
    #[test]
    fn showing_a_message_box_reveals_it() {
        let mut mb = MessageBox::new(Rect::new(0, 0, 300, 150));
        assert_eq!(mb.reveal_progress(), 0.0, "a fresh box is hidden");
        assert!(!mb.is_animating(), "and owes no frames");
        let hidden = render_to_svg(&mut mb);
        assert!(
            !hidden.contains("rx=\"") && crate::widget::svg::text_ink_box(&hidden).is_none(),
            "a hidden box paints nothing: {hidden}"
        );

        // The runtime's show: the base flag, then the frames that follow it.
        mb.show();
        assert!(mb.tick(20), "still revealing after one step");
        let mid = mb.reveal_progress();
        assert!(mid > 0.0 && mid < 1.0, "the box must pass through an interior reveal (got {mid})");
        let mid_width = frame_width(&render_to_svg(&mut mb));

        while mb.tick(1000) {}
        assert_eq!(mb.reveal_progress(), 1.0, "and settle fully shown");
        let settled_width = frame_width(&render_to_svg(&mut mb));

        assert!(
            mid_width > 0 && mid_width < settled_width,
            "a revealing frame must be smaller than a settled one: mid={mid_width} settled={settled_width}"
        );

        // And hiding aims it back, so the box does not stay latched open.
        mb.hide();
        assert!(mb.tick(20), "hiding owes frames too");
        while mb.tick(1000) {}
        assert_eq!(mb.reveal_progress(), 0.0, "a hidden box settles closed");
    }

    /// The widest rounded rectangle in the document — the box's frame.
    fn frame_width(svg: &str) -> u32 {
        svg.split("<rect ")
            .filter(|chunk| chunk.contains("rx=\""))
            .filter_map(|chunk| {
                let w = chunk.split("width=\"").nth(1)?;
                w.split('"').next()?.parse::<u32>().ok()
            })
            .max()
            .unwrap_or(0)
    }

    // ── 12. Modality setting ────────────────────────────────────────

    #[test]
    fn test_modality_setting() {
        let mut mb = MessageBox::new(Rect::new(0, 0, 300, 150));

        assert!(mb.is_modal());

        mb.set_modal(false);
        assert!(!mb.is_modal());

        mb.set_modal(true);
        assert!(mb.is_modal());
    }

    // ── 13. Disabled state ──────────────────────────────────────────

    #[test]
    fn test_disabled_state_blocks_events() {
        let mut mb = MessageBox::new(Rect::new(0, 0, 300, 150));

        mb.set_enabled(false);
        assert!(!mb.is_enabled());

        // Enter key should not trigger default button when disabled
        let accepted_fired = Arc::new(Mutex::new(false));
        mb.accepted.connect({
            let accepted_fired = Arc::clone(&accepted_fired);
            move || {
                *accepted_fired.lock().unwrap() = true;
            }
        });

        mb.handle_event(&Event::KeyPress { key: 13, modifiers: 0 });
        assert!(!*accepted_fired.lock().unwrap());

        // Re-enable and verify it works
        mb.set_enabled(true);
        mb.handle_event(&Event::KeyPress { key: 13, modifiers: 0 });
        assert!(*accepted_fired.lock().unwrap());
    }

    // ── 14. Keyboard event handling (Enter/ESC) ─────────────────────

    #[test]
    fn test_keyboard_enter_triggers_default_button() {
        let mut mb = MessageBox::new(Rect::new(0, 0, 300, 150));
        mb.set_buttons(vec![StandardButton::Yes, StandardButton::No]);
        mb.set_default_button(StandardButton::Yes);

        let captured = Arc::new(Mutex::new(None::<StandardButton>));
        mb.button_clicked.connect({
            let captured = Arc::clone(&captured);
            move |val: Arc<StandardButton>| {
                *captured.lock().unwrap() = Some(*val);
            }
        });

        // Enter (key 13) triggers default button (Yes)
        mb.handle_event(&Event::KeyPress { key: 13, modifiers: 0 });
        assert_eq!(*captured.lock().unwrap(), Some(StandardButton::Yes));
    }

    #[test]
    fn test_keyboard_escape_triggers_cancel() {
        let mut mb = MessageBox::new(Rect::new(0, 0, 300, 150));
        mb.set_buttons(vec![StandardButton::Ok, StandardButton::Cancel]);

        let captured = Arc::new(Mutex::new(None::<StandardButton>));
        mb.button_clicked.connect({
            let captured = Arc::clone(&captured);
            move |val: Arc<StandardButton>| {
                *captured.lock().unwrap() = Some(*val);
            }
        });

        // Escape (key 27) triggers Cancel
        mb.handle_event(&Event::KeyPress { key: 27, modifiers: 0 });
        assert_eq!(*captured.lock().unwrap(), Some(StandardButton::Cancel));
    }

    #[test]
    fn test_keyboard_escape_falls_back_to_no_then_close() {
        // No Cancel button → falls back to No
        let mut mb = MessageBox::new(Rect::new(0, 0, 300, 150));
        mb.set_buttons(vec![StandardButton::Yes, StandardButton::No]);

        let captured = Arc::new(Mutex::new(None::<StandardButton>));
        mb.button_clicked.connect({
            let captured = Arc::clone(&captured);
            move |val: Arc<StandardButton>| {
                *captured.lock().unwrap() = Some(*val);
            }
        });

        mb.handle_event(&Event::KeyPress { key: 27, modifiers: 0 });
        assert_eq!(*captured.lock().unwrap(), Some(StandardButton::No));

        // Neither Cancel nor No → Close
        let mut mb2 = MessageBox::new(Rect::new(0, 0, 300, 150));
        mb2.set_buttons(vec![StandardButton::Ok]);
        mb2.set_default_button(StandardButton::Ok);

        let captured2 = Arc::new(Mutex::new(false));
        mb2.button_clicked.connect({
            let captured2 = Arc::clone(&captured2);
            move |_: Arc<StandardButton>| {
                *captured2.lock().unwrap() = true;
            }
        });

        mb2.handle_event(&Event::KeyPress { key: 27, modifiers: 0 });
        // With only Ok, no Cancel/No/Close, ESC is no-op
        // (the event handler only checks Cancel/No/Close)
        // Actually it won't match anything since buttons only contains Ok
        // Let's just verify button_clicked wasn't called
        assert!(!*captured2.lock().unwrap());
    }

    // ── 15. StandardButton label ────────────────────────────────────

    #[test]
    fn test_standard_button_labels() {
        assert_eq!(StandardButton::Ok.label(), "OK");
        assert_eq!(StandardButton::Cancel.label(), "Cancel");
        assert_eq!(StandardButton::Yes.label(), "Yes");
        assert_eq!(StandardButton::No.label(), "No");
        assert_eq!(StandardButton::Save.label(), "Save");
        assert_eq!(StandardButton::Apply.label(), "Apply");
        assert_eq!(StandardButton::Close.label(), "Close");
        assert_eq!(StandardButton::Abort.label(), "Abort");
        assert_eq!(StandardButton::Retry.label(), "Retry");
        assert_eq!(StandardButton::Ignore.label(), "Ignore");
        assert_eq!(StandardButton::Help.label(), "Help");
        assert_eq!(StandardButton::YesAll.label(), "Yes to All");
        assert_eq!(StandardButton::NoAll.label(), "No to All");
        assert_eq!(StandardButton::Discard.label(), "Discard");
    }

    #[test]
    fn the_extra_text_levels_add_painted_lines() {
        let mut dialog = MessageBox::new(Rect::new(0, 0, 360, 220));
        dialog.show();
        while dialog.tick(1000) {}
        let before = crate::widget::svg::text_ink_boxes(&render_to_svg(&mut dialog)).len();
        dialog.set_informative_text("This explains the message");
        dialog.set_detailed_text("And this is the detail");
        let after = crate::widget::svg::text_ink_boxes(&render_to_svg(&mut dialog)).len();
        assert!(after > before, "both extra levels must paint: {before} -> {after}");
        assert_eq!(dialog.informative_text(), "This explains the message");
        assert_eq!(dialog.detailed_text(), "And this is the detail");
    }

    #[test]
    fn the_check_box_row_toggles() {
        let mut dialog = MessageBox::new(Rect::new(0, 0, 360, 200));
        dialog.set_check_box("Do not ask again");
        assert!(!dialog.is_check_box_checked());
        let row = dialog.check_box_rect().expect("the row exists");
        dialog.handle_event(&Event::MousePress {
            pos: Point::new(row.x + 2, row.y + 2),
            button: 1,
            modifiers: 0,
        });
        assert!(dialog.is_check_box_checked());
    }

    #[test]
    fn escape_uses_the_configured_escape_button() {
        let mut dialog = MessageBox::new(Rect::new(0, 0, 360, 180));
        dialog.set_buttons(vec![StandardButton::Ok, StandardButton::Help]);
        dialog.set_escape_button(Some(StandardButton::Help));
        let clicked = Arc::new(Mutex::new(None));
        let sink = Arc::clone(&clicked);
        dialog.button_clicked.connect(move |button: Arc<StandardButton>| {
            *sink.lock().expect("lock") = Some(*button);
        });
        dialog.handle_event(&Event::KeyPress { key: 27, modifiers: 0 });
        assert_eq!(*clicked.lock().expect("lock"), Some(StandardButton::Help));
    }

    #[test]
    fn the_default_button_can_be_cleared() {
        let mut dialog = MessageBox::new(Rect::new(0, 0, 360, 180));
        dialog.set("default_button", CapabilityValue::Null).expect("clearing the default is valid");
        assert_eq!(dialog.default_button(), None);
        let clicked = Arc::new(Mutex::new(0usize));
        let sink = Arc::clone(&clicked);
        dialog.button_clicked.connect(move |_| {
            *sink.lock().expect("lock") += 1;
        });
        dialog.handle_event(&Event::KeyPress { key: 13, modifiers: 0 });
        assert_eq!(*clicked.lock().expect("lock"), 0, "Enter with no default does nothing");
    }

    #[test]
    fn a_press_paints_pressed_feedback_and_release_clears_it() {
        let mut dialog = MessageBox::new(Rect::new(0, 0, 360, 180));
        dialog.show();
        while dialog.tick(1000) {}
        let _ = render_to_svg(&mut dialog);
        let rect = {
            // The single OK button is the last element of the action row.
            let labels: Vec<String> =
                dialog.buttons.iter().map(|button| button.translated_label()).collect();
            let size = Size::new(
                dialog.base.geometry().width.max(1),
                dialog.base.geometry().height.max(1),
            );
            let mut backend = SoftwarePaintBackend::new(size, 1.0);
            let context = RenderContext::new(&mut backend);
            let frame = dialog.frame_rect(&context);
            let body =
                ControlMetrics::content_below_top_band(frame, dimensions::DIALOG_TITLE_BAR_HEIGHT);
            let band = inset_action_band(
                ControlMetrics::bottom_band(body, dimensions::DIALOG_BUTTON_HEIGHT),
                body.y,
            );
            action_row_geometry(&context, effective_font(dialog.base.style()), &labels, band, true)
                .buttons[0]
        };
        dialog.handle_event(&Event::MousePress {
            pos: Point::new(rect.x + 2, rect.y + 2),
            button: 1,
            modifiers: 0,
        });
        assert_eq!(dialog.pressed, Some(0));
        dialog.handle_event(&Event::MouseRelease {
            pos: Point::new(rect.x + 2, rect.y + 2),
            button: 1,
        });
        assert_eq!(dialog.pressed, None);
    }

    #[test]
    fn a_pixmap_replaces_the_severity_glyph() {
        let mut dialog = MessageBox::new(Rect::new(0, 0, 360, 180));
        dialog.set_icon(MessageBoxIcon::Warning);
        dialog.show();
        while dialog.tick(1000) {}
        let glyph = render_to_svg(&mut dialog);
        let image = crate::image::Image::try_from_rgba(vec![255, 0, 0, 255], 1, 1)
            .expect("a 1x1 RGBA image is valid");
        dialog.set_pixmap(Some(image));
        let picture = render_to_svg(&mut dialog);
        assert_ne!(glyph, picture, "the picture must be painted instead of the glyph");
        assert!(dialog.pixmap().is_some());
        assert!(dialog.pixmap_raster.is_some(), "the first draw fills the resample cache");
        let cached = dialog.pixmap_raster.as_ref().map(|(side, _)| *side);
        render_to_svg(&mut dialog);
        assert_eq!(
            dialog.pixmap_raster.as_ref().map(|(side, _)| *side),
            cached,
            "a static picture must not be resampled every frame"
        );
    }

    #[test]
    fn detailed_text_is_collapsed_until_the_toggle_opens_it() {
        let mut dialog = MessageBox::new(Rect::new(0, 0, 420, 220));
        dialog.show();
        while dialog.tick(1000) {}
        dialog.set_detailed_text("technical detail");
        let collapsed = render_to_svg(&mut dialog);
        let collapsed_runs = collapsed.matches("data-text").count();

        let toggled = Arc::new(Mutex::new(Vec::<bool>::new()));
        let sink = Arc::clone(&toggled);
        dialog.details_toggled.connect(move |expanded| {
            sink.lock().expect("lock").push(*expanded);
        });
        let rect = dialog.details_toggle_rect().expect("the toggle has a rectangle");
        dialog.handle_event(&Event::MousePress {
            pos: Point::new(rect.x + 2, rect.y + 2),
            button: 1,
            modifiers: 0,
        });
        assert!(dialog.is_detailed_text_expanded());
        assert_eq!(*toggled.lock().expect("lock"), vec![true]);
        let expanded = render_to_svg(&mut dialog);
        assert!(
            expanded.matches("data-text").count() > collapsed_runs,
            "the toggle reveals the detail block as an extra text run"
        );
    }

    #[test]
    fn tab_moves_the_focus_ring_and_enter_activates_the_focused_button() {
        let mut dialog = MessageBox::new(Rect::new(0, 0, 360, 180));
        dialog.set_buttons(vec![StandardButton::Ok, StandardButton::Cancel]);
        dialog.set_default_button(StandardButton::Ok);
        let seen = Arc::new(Mutex::new(Vec::<StandardButton>::new()));
        let sink = Arc::clone(&seen);
        dialog.button_clicked.connect(move |button| {
            sink.lock().expect("lock").push(*button);
        });
        let focused = Arc::new(Mutex::new(0usize));
        let sink = Arc::clone(&focused);
        dialog.focus_changed.connect(move |_| {
            *sink.lock().expect("lock") += 1;
        });
        dialog.handle_event(&Event::key_press(key_codes::TAB, 0));
        assert_eq!(*focused.lock().expect("lock"), 1);
        dialog.handle_event(&Event::key_press(key_codes::TAB, 0));
        assert_eq!(*focused.lock().expect("lock"), 2);
        // The ring is on Cancel now, so Enter activates it rather than the default.
        dialog.handle_event(&Event::key_press(key_codes::ENTER, 0));
        assert_eq!(*seen.lock().expect("lock"), vec![StandardButton::Cancel]);
    }
}