compose-rust 0.0.1

Declarative UI in Rust, rendered by an AOT-compiled Compose Multiplatform engine.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
package dev.darkpyonix.composerust.design

import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.Brush as ComposeBrush
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.TextUnit
import dev.darkpyonix.composerust.protocol.ColorRole
import dev.darkpyonix.composerust.protocol.ColorScheme
import dev.darkpyonix.composerust.protocol.DesignSystem
import dev.darkpyonix.composerust.protocol.DesignTokenTable
import dev.darkpyonix.composerust.protocol.DesignTokens
import androidx.compose.animation.core.FiniteAnimationSpec
import dev.darkpyonix.composerust.protocol.MaterialRole
import dev.darkpyonix.composerust.protocol.MotionRole
import dev.darkpyonix.composerust.protocol.Paint
import dev.darkpyonix.composerust.protocol.ShapeRole
import dev.darkpyonix.composerust.protocol.SpaceRole
import dev.darkpyonix.composerust.protocol.Theme
import dev.darkpyonix.composerust.protocol.TypeRole
import dev.darkpyonix.composerust.protocol.TypeToken
import dev.darkpyonix.composerust.protocol.WindowSizeClass
import java.lang.System
import dev.darkpyonix.composerust.protocol.TitleBar

/**
 * The platforms `Theme::adaptive` distinguishes.
 *
 * `LinuxGnome`, `LinuxKde` and `LinuxOther` exist because each maps to a design system of
 * its own (GNOME 50, KDE Breeze, Deepin); until those are implemented they resolve to the
 * Host's mandatory fallback.
 */
enum class HostPlatform { Android, MacOs, Ios, Windows, LinuxGnome, LinuxKde, LinuxOther, Web, Unknown }

/**
 * Reads the platform once, at startup: the OS first, then `XDG_CURRENT_DESKTOP` and, if
 * that is empty, `DESKTOP_SESSION` for the Linux desktop environment.
 */
internal fun detectHostPlatform(
    osName: String = System.getProperty("os.name").orEmpty(),
    androidRuntime: String? = System.getProperty("java.vm.vendor"),
    xdgCurrentDesktop: String? = System.getenv("XDG_CURRENT_DESKTOP"),
    desktopSession: String? = System.getenv("DESKTOP_SESSION"),
): HostPlatform {
    val os = osName.lowercase()
    return when {
        androidRuntime?.contains("Android", ignoreCase = true) == true -> HostPlatform.Android
        // The browser has no operating system to report. Its shim for this property answers
        // `web`, which is the only way a tab can say what it is: `navigator.platform` names
        // the machine underneath, so a browser on a Mac would come out as macOS and take
        // Cupertino, and the same page on Windows would take Fluent.
        os == "web" -> HostPlatform.Web
        os.contains("mac") || os.contains("darwin") -> HostPlatform.MacOs
        os.contains("ios") -> HostPlatform.Ios
        os.contains("win") -> HostPlatform.Windows
        os.contains("linux") || os.contains("nix") || os.contains("bsd") ->
            linuxDesktop(xdgCurrentDesktop, desktopSession)
        else -> HostPlatform.Unknown
    }
}

private fun linuxDesktop(xdgCurrentDesktop: String?, desktopSession: String?): HostPlatform {
    val value = xdgCurrentDesktop?.takeIf { it.isNotBlank() } ?: desktopSession.orEmpty()
    return when {
        value.contains("GNOME", ignoreCase = true) -> HostPlatform.LinuxGnome
        value.contains("KDE", ignoreCase = true) -> HostPlatform.LinuxKde
        else -> HostPlatform.LinuxOther
    }
}

/**
 * The system `adaptive` picks for a platform.
 *
 * A GNOME session takes Adwaita, a KDE session takes Breeze, and any other Linux session
 * takes Deepin. A platform with no design language of its own takes the Host's fallback. That is why `Theme::adaptive`
 * makes the fallback a required argument: there is no platform for which adaptive has
 * nothing to choose.
 */
internal fun adaptiveSystem(platform: HostPlatform, fallback: DesignSystem): DesignSystem =
    when (platform) {
        HostPlatform.Android -> DesignSystem.Material3
        // Apple's current language, the one macOS 26 and iOS 26 draw themselves in.
        // Cupertino is the previous one and is still current vocabulary elsewhere, so it
        // stays reachable, but an app that did not choose gets what the machine uses.
        HostPlatform.MacOs, HostPlatform.Ios -> DesignSystem.LiquidGlass
        HostPlatform.Windows -> DesignSystem.Fluent
        // A browser has no design language of its own, so the choice is arbitrary; Fluent 2
        // is the documented default and an app can say `unified` to be explicit.
        HostPlatform.Web -> DesignSystem.Fluent
        HostPlatform.LinuxGnome -> DesignSystem.Gnome
        HostPlatform.LinuxKde -> DesignSystem.Breeze
        // Any other Linux session, and one that cannot be identified at all, takes
        // Deepin. That is the job it was written for.
        HostPlatform.LinuxOther -> DesignSystem.Deepin
        HostPlatform.Unknown -> fallback
    }

/**
 * A design system bound to one colour scheme: the generated token table (colours, type,
 * shapes, spacing) plus the component rules the Renderer owns (elevation, button variants,
 * motion).
 */
class ResolvedTheme(
    val system: DesignSystem,
    val tokens: DesignTokenTable,
    val rules: ComponentRules,
    val dark: Boolean,
    /**
     * The class of window this theme is being resolved for.
     *
     * A design system is allowed to answer differently at different widths, and Apple's
     * does: macOS 26 puts glass on chrome over opaque document content, while a phone
     * sized window carries it much further. Nothing on the wire says this; the Renderer
     * already measures the window for `WindowSizeChanged`, and this is the same reading.
     */
    val sizeClass: WindowSizeClass = WindowSizeClass.Compact,
    /**
     * The faces the application registered, by the role each was registered for.
     *
     * Empty is the ordinary case and means every role is written in the machine's own UI
     * face. A role that is in here is written in the face the application shipped, and a
     * role that is not stays where it was: naming a font for titles must not quietly
     * change body text as well.
     */
    val fonts: Map<TypeRole, FontFamily> = emptyMap(),
    /**
     * What a registered brush id was registered as, or null for an id naming nothing.
     *
     * A lookup and not a map, because a brush is named per node rather than per theme and
     * copying every registration into the theme each time one changes would cost the
     * whole table for the one node that asked.
     */
    val brushes: (Int) -> ComposeBrush? = { null },
    /**
     * Whether the window this theme draws into has the platform's own material behind
     * it, so that what the renderer leaves unpainted shows the desktop.
     *
     * A design system that draws chrome as glass has to know, because the right recipe
     * differs. Over app content the glass carries its own tint; straight over the
     * platform's material that tint would cover the thing the platform was asked to show,
     * and the chrome should be mostly the desktop. False everywhere the window is opaque,
     * which is every platform but one and every window that did not ask.
     */
    val windowBackdrop: Boolean = false,

    /**
     * Which platform this is running on.
     *
     * A design system is not a platform and mostly does not need to know, which is why
     * this arrived late. One rule does: the Apple language is drawn on two platforms whose
     * answer for a set of destinations with no room differs, because one of them has its
     * own tab bar to hand the destinations to and the other has a sidebar that
     * is put away behind a button.
     */
    val platform: HostPlatform = HostPlatform.Unknown,

    /**
     * The application's own colours for this scheme, indexed by role ordinal, with null
     * where the design system's value stands. Empty where the application gave none, or
     * where the platform is in a high contrast mode.
     *
     * Only colour. Everything else this theme answers is still the design system's, so a
     * brand colour is drawn the way this system draws a colour.
     */
    val applicationColors: List<Color?> = emptyList(),
) {
    fun color(role: ColorRole): Color =
        applicationColors.getOrNull(role.ordinal) ?: systemColor(role)

    /** What the design system alone says a role is, before the application's palette. */
    fun systemColor(role: ColorRole): Color =
        rules.color(role, dark, sizeClass) ?: Color(tokens.color(role, dark))

    /**
     * A literal paints itself, a role goes through the table, a brush answers with the
     * colour it starts from.
     *
     * A gradient asked for as a colour has to become one somewhere, and the alternative
     * is a caller that has to know which kind of paint it was handed before it can use
     * it. Everywhere a brush can actually be drawn asks [brush] instead.
     */
    fun color(paint: Paint): Color = when (paint) {
        is Paint.Literal -> Color(paint.argb)
        is Paint.Role -> color(paint.role)
        is Paint.Asset -> brushes(paint.assetId)?.let(::startingColor)
            ?: color(ColorRole.Surface)
    }

    /**
     * What to fill with: a brush where the paint named one, a flat colour otherwise.
     *
     * Null where the paint named a brush that is not registered, so that the caller can
     * report it. Drawing a guess would leave the application with a screen that is only
     * subtly wrong and nothing to read about why.
     */
    fun brush(paint: Paint): ComposeBrush? = when (paint) {
        is Paint.Asset -> brushes(paint.assetId)
        else -> SolidColor(color(paint))
    }

    fun space(role: SpaceRole): Dp = tokens.space(role).dp

    /**
     * The spec a change of this importance runs on in this system.
     *
     * Reduced motion is answered here rather than by the Host: every role becomes
     * `Instant`, so each one finishes inside the frame it starts in, and the screen that
     * declared it is not involved and does not need to be recomposed for it.
     */
    fun <T> motion(role: MotionRole): FiniteAnimationSpec<T> = rules.motion.spec(
        if (dev.darkpyonix.composerust.ui.node.platformReducedMotion()) MotionRole.Instant else role,
    )

    fun radius(role: ShapeRole): Dp = tokens.radius(role).dp

    /** `Full` is stored as a very large radius, which is a pill at any height. */
    fun shape(role: ShapeRole): Shape = shapeOfRadius(tokens.radius(role))

    /**
     * True where this system's corners are continuous rather than circular.
     *
     * A circular corner joins the straight edge at a point where curvature jumps from
     * zero to 1/r, and the eye reads that jump as a pinch. Apple's corners do not have
     * it; every other system here draws a plain arc, and giving them a superellipse would
     * be a mistake rather than a refinement.
     */
    val continuousCorners: Boolean
        get() = system == DesignSystem.LiquidGlass || system == DesignSystem.Cupertino

    /**
     * One radius in dp, cut the way this system cuts corners.
     *
     * `Full` is a true pill in every system, including this one. A continuous corner
     * taken to its largest radius is not a capsule: the superellipse keeps its flattened
     * flanks and the end reads as a squircle, which is what an Apple button is not.
     * Apple's capsules are semicircular at the ends.
     */
    fun shapeOfRadius(radius: Float): Shape = when {
        radius >= FULL_RADIUS -> roundedShape(radius)
        continuousCorners -> ContinuousCornerShape(radius.dp)
        else -> roundedShape(radius)
    }

    /** Four radii in dp, cut the way this system cuts corners. */
    fun shapeOfRadii(topStart: Float, topEnd: Float, bottomEnd: Float, bottomStart: Float): Shape =
        if (continuousCorners) {
            ContinuousCornerShape(topStart.dp, topEnd.dp, bottomEnd.dp, bottomStart.dp)
        } else {
            RoundedCornerShape(topStart.dp, topEnd.dp, bottomEnd.dp, bottomStart.dp)
        }

    fun type(role: TypeRole): TypeToken = tokens.type(role)

    /**
     * The face this role is written in.
     *
     * A registered font wins, then code, then the machine's UI face. Code is second
     * because a monospace role that resolved to a proportional face is not a styling
     * difference, it is columns that no longer line up.
     */
    fun family(role: TypeRole): FontFamily =
        fonts[role] ?: if (tokens.type(role).monospace) FontFamily.Monospace else platformUiFamily

    companion object {
        internal fun roundedShape(radius: Float): Shape =
            if (radius >= FULL_RADIUS) RoundedCornerShape(percent = 50) else RoundedCornerShape(radius.dp)

        internal const val FULL_RADIUS = 1000.0f
    }
}

val TypeToken.fontSize: TextUnit get() = size.sp
val TypeToken.composeWeight: FontWeight get() = FontWeight(weight)
val TypeToken.composeLineHeight: TextUnit get() = lineHeight.sp
val TypeToken.composeLetterSpacing: TextUnit get() = letterSpacing.sp

/**
 * The face this machine writes its interfaces in.
 *
 * Held here and filled in by the platform, because finding it is platform work: the
 * desktop asks the font manager for the system's UI face by name, and a browser or a
 * phone has its own answer or none. Whoever knows assigns it before the first frame;
 * whoever does not leaves the toolkit's default, which is what every screen had before.
 *
 * Not the design system's choice. A window belongs to the machine it is on mostly because
 * its letters look like every other window's, and a Material 3 screen on a Mac is still
 * on a Mac.
 */
var platformUiFamily: FontFamily = FontFamily.Default

/**
 * The hand-written half of a design system: elevation rendering, `ButtonVariant` styling
 * and motion.
 *
 * A fourth design system is one `DesignTokenTable` in the generated protocol plus one
 * implementation of this interface, and nothing else. No widget, property, modifier or wire
 * format changes.
 */
interface ComponentRules {
    /**
     * A colour this system answers differently in a window of this class, or null to take
     * the generated table's value.
     *
     * Null for almost everything, because a design system has one palette. Apple is the
     * exception and only in one place: its page is a different colour on a phone and in a
     * desktop window, and both values are part of the same design language. The table can
     * carry one value per role, so the choice between two of Apple's own has to be made
     * here.
     *
     * This is not a way for a system to redecorate at will. A role answered here is one
     * the platform vendor specifies twice; anything else belongs in the table, where the
     * Host's own colour resolution can see it.
     */
    fun color(role: ColorRole, dark: Boolean, sizeClass: WindowSizeClass): Color? = null

    /** How `Modifier::Elevation(dp)` is drawn. The Host sends a dp value and nothing else. */
    fun elevation(
        modifier: androidx.compose.ui.Modifier,
        elevation: Dp,
        shape: Shape,
        theme: ResolvedTheme,
    ): androidx.compose.ui.Modifier

    /**
     * What that button becomes when it is a line in a menu rather than a control on a page.
     *
     * The default is that it becomes nothing: the system draws its menu out of its own
     * buttons. Override it where a menu row is its own thing, which on the Apple systems it
     * is: a row there runs the width of the menu, is cut shallow rather than into a capsule,
     * and is read as a list rather than as a stack of controls.
     */
    fun menuEntry(base: ButtonStyle, theme: ResolvedTheme): ButtonStyle = base

    /** How a `ButtonVariant` looks, resting and pressed. */
    fun button(
        variant: dev.darkpyonix.composerust.protocol.ButtonVariant,
        theme: ResolvedTheme,
    ): ButtonStyle

    /**
     * How a container or overlay looks. The widget says which kind of container it is and
     * nothing else: the background, the corner, the resting height, the scrim behind a
     * modal and the hairline under a bar are all decided here.
     */
    fun container(role: ContainerRole, theme: ResolvedTheme): ContainerStyle

    /** How a tab strip and its selection indicator look. */
    fun tabs(theme: ResolvedTheme): TabsStyle

    /**
     * How the selection controls and the two indicators are drawn.
     *
     * One call for the six of them because they are one family: something that holds a
     * state, something that shows it, and a reaction to being touched. A widget sends
     * whether it is on, where it sits between two ends, or which way it runs. The size of
     * the box, the shape of the tick, the dimensions of a track and a thumb, the speed of
     * an indeterminate sweep and the weight of a rule are all decided here, which is why
     * the same declaration comes out as a Material checkbox and as a Cupertino one.
     */
    fun controls(theme: ResolvedTheme): ControlsStyle

    /**
     * Who draws those six.
     *
     * The default draws them from [controls], so a new design system implements `controls()`
     * and nothing else. Material 3 overrides this because `androidx.compose.material3`
     * already draws Material's controls, and a copy of them would only drift from the
     * specification it claims to follow.
     */
    val controlWidgets: ControlWidgets get() = DrawnControlWidgets

    /**
     * The metrics this system's icon set is drawn to.
     *
     * The Host registers a meaning, never a picture or a system icon name, so the artwork
     * is chosen here. That is what makes one declaration come out as the shape Cupertino
     * draws and as the shape Material 3 draws, without the Host knowing either.
     */
    fun icon(role: dev.darkpyonix.composerust.protocol.IconRole, theme: ResolvedTheme): IconStyle

    /**
     * How this system asks for a date, a time and a choice.
     *
     * The three systems do not merely style their pickers differently, they are operated
     * differently: a calendar grid, a wheel, a flyout. The widget carries a value, a range
     * and a change event, so the way of picking is decided here and nowhere else.
     */
    val pickers: PickerRules

    /**
     * The frame around a text field.
     *
     * A field with nothing around it is the same field in all six systems, and it was:
     * no fill, no line, no inner room, no focus mark. Which of those a system draws, and
     * what changes when the caret goes in, is decided here.
     */
    fun field(theme: ResolvedTheme): FieldStyle

    /** State transition timing. Motion is a design system rule, not a Host parameter. */
    val motion: Motion

    /**
     * What a surface of this role is made of here.
     *
     * Four roles and no blur radius, because the systems disagree about what a material
     * even is: Apple's blur what is behind them, Material 3 lifts the surface and lays a
     * tone over it, and the GNOME and KDE systems draw an opaque panel. A radius from the
     * screen would be a blur instruction handed to systems that never blur.
     *
     * The default is the opaque reading, and it is the reading four of the seven want. It
     * is built from this system's own surface and outline tokens, so a screen asking for
     * the same material still comes out in this system's colours rather than in someone
     * else's.
     */
    fun material(role: MaterialRole, theme: ResolvedTheme): SurfaceMaterial =
        SurfaceMaterial.Opaque(opaqueMaterial(role, theme))

    // The three answers below have defaults, and that is the point of them. A design
    // system implements what it has an opinion about; what it says nothing about is
    // derived from its own token table, so it still comes out in its own colours rather
    // than in someone else's. A seventh design system is one more implementation of this
    // interface, and it does not stop compiling when the vocabulary grows again.

    /**
     * How a set of destinations is presented at this width, and what it is drawn with.
     *
     * The width decides between a bar across the bottom, a rail down the side and a
     * drawer standing open, because that is the correspondence every one of these design
     * systems already makes. The Host declared one navigation and said nothing about
     * which of the three it wanted.
     */
    fun navigation(sizeClass: WindowSizeClass, theme: ResolvedTheme): NavigationStyle =
        NavigationStyle(
            presentation = when (sizeClass) {
                WindowSizeClass.Compact -> NavigationPresentation.Bar
                WindowSizeClass.Medium -> NavigationPresentation.Rail
                WindowSizeClass.Expanded -> NavigationPresentation.Drawer
            },
            container = theme.color(ColorRole.SurfaceContainer),
            content = theme.color(ColorRole.OnSurfaceVariant),
            selectedContent = theme.color(ColorRole.Primary),
            indicator = Color.Transparent,
            indicatorShape = theme.shape(ShapeRole.Full),
            indicatorKind = NavigationIndicator.None,
            separator = theme.color(ColorRole.OutlineVariant),
            barHeight = 64.dp,
            railWidth = 80.dp,
            drawerWidth = 240.dp,
            itemSpacing = theme.space(SpaceRole.Xs),
            itemPadding = theme.space(SpaceRole.Sm),
            labelInRail = true,
            typeRole = TypeRole.Label,
        )

    /**
     * Which edge a sheet enters from at this width, and what it is drawn with.
     *
     * Up from the bottom where the window is narrow, in from the side where it is wide:
     * the same convention in all of these systems, and the same reason as the navigation
     * presentation. The Host has no property that could ask for one of them.
     */
    fun sheet(sizeClass: WindowSizeClass, theme: ResolvedTheme): SheetStyle = SheetStyle(
        edge = if (sizeClass == WindowSizeClass.Compact) SheetEdge.Bottom else SheetEdge.End,
        container = theme.color(ColorRole.SurfaceContainer),
        content = theme.color(ColorRole.OnSurface),
        shape = theme.shape(ShapeRole.Large),
        elevation = 6.dp,
        scrim = Color.Black.copy(alpha = 0.32f),
        handle = theme.color(ColorRole.OutlineVariant),
        widthFraction = 0.4f,
        heightFraction = 0.5f,
        padding = theme.space(SpaceRole.Lg),
        borderWidth = 0.dp,
        borderColor = Color.Transparent,
    )

    /**
     * How this system draws the window's own caption: the three buttons, and where the
     * title sits.
     *
     * Only the platforms where the renderer draws the caption itself use this. macOS
     * keeps its own buttons, and imitating them would be the most visible way to fail at
     * looking native. Everywhere else the window is undecorated, so these three are the
     * only way to minimise, maximise or close it: they are not decoration.
     *
     * The Host has no say here, deliberately. The colour, the position and the shape of a
     * window button belong to the design system and the platform.
     *
     * The default is a plain set at the trailing edge with a tinted hover, which is what
     * most of these systems do. A system overrides what it actually differs about.
     */
    fun caption(theme: ResolvedTheme, titleBar: TitleBar = TitleBar.Normal): CaptionStyle = CaptionStyle(
        side = CaptionSide.End,
        buttonWidth = 32.dp,
        buttonHeight = 32.dp,
        shape = theme.shape(ShapeRole.Small),
        minimiseContainer = Color.Transparent,
        maximiseContainer = Color.Transparent,
        closeContainer = Color.Transparent,
        hover = theme.color(ColorRole.SurfaceVariant),
        closeHover = theme.color(ColorRole.Error),
        glyph = theme.color(ColorRole.OnSurfaceVariant),
        closeHoverGlyph = theme.color(ColorRole.OnError),
        glyphStroke = 1.dp,
        glyphAtRest = true,
        spacing = theme.space(SpaceRole.Xs),
        edgePadding = theme.space(SpaceRole.Sm),
        titleAlignment = CaptionTitleAlignment.Start,
    )

    /**
     * How a transient message is drawn, and how long each of the two durations lasts.
     *
     * The milliseconds are here rather than on the wire because they are a rule of the
     * design language: Material's four and ten seconds are not Apple's, and a Host that
     * sent a number would be overruling whichever system it ended up under.
     */
    fun message(theme: ResolvedTheme): MessageStyle = MessageStyle(
        container = theme.color(ColorRole.OnSurface),
        content = theme.color(ColorRole.Surface),
        actionContent = theme.color(ColorRole.Primary),
        shape = theme.shape(ShapeRole.Small),
        elevation = 3.dp,
        placement = MessagePlacement.BottomCenter,
        horizontalPadding = theme.space(SpaceRole.Md),
        verticalPadding = theme.space(SpaceRole.Sm),
        inset = theme.space(SpaceRole.Md),
        shortMillis = 4_000,
        longMillis = 10_000,
        borderWidth = 0.dp,
        borderColor = Color.Transparent,
        typeRole = TypeRole.Body,
    )

    /**
     * How a chip looks, chosen and not.
     *
     * The chip sends a label, perhaps a glyph, and whether it is chosen, and nothing else.
     * The corner, the height, whether choosing it fills it or ticks it, and whether it has a
     * line around it are all answered here, which is why one declaration comes out as a
     * Material filter chip, an Apple filter pill and a Fluent tag button.
     *
     * The default is an outlined capsule that fills with the accent when chosen, made of
     * this system's own tokens.
     */
    fun chip(theme: ResolvedTheme): ChipStyle = ChipStyle(
        container = Color.Transparent,
        selectedContainer = theme.color(ColorRole.Primary),
        content = theme.color(ColorRole.OnSurface),
        selectedContent = theme.color(ColorRole.OnPrimary),
        borderWidth = 1.dp,
        borderColor = theme.color(ColorRole.Outline),
        selectedBorderColor = Color.Transparent,
        shape = theme.shape(ShapeRole.Full),
        height = 32.dp,
        horizontalPadding = theme.space(SpaceRole.Md),
        iconGap = theme.space(SpaceRole.Xs),
        typeRole = TypeRole.Label,
        leadingCheck = false,
        pressedAlpha = 0.7f,
        disabledAlpha = 0.38f,
    )

    /**
     * How the one action a screen is about is drawn, and where the frame it is in puts it.
     *
     * **Floating is one system's answer, not the concept.** The Host sends an icon, a label
     * and a click, so whether this is a disc over the corner of the page, a plus at the
     * end of the bar or an accent button at the head of a command bar is decided here and
     * nowhere else. The width is part of the question because a few systems answer it
     * differently on a phone, where the thumb is at the bottom, and on a desktop, where the
     * pointer is already at the top.
     *
     * The default floats a disc where the window is phone-shaped and sets a labelled button
     * at the trailing end of the top elsewhere, in this system's own colours.
     */
    fun floatingAction(sizeClass: WindowSizeClass, theme: ResolvedTheme): FloatingActionStyle {
        val floats = dev.darkpyonix.composerust.foundation.floatingActionFloats(sizeClass)
        return FloatingActionStyle(
            placement = if (floats) {
                FloatingActionPlacement.OverPageBottomEnd
            } else {
                FloatingActionPlacement.BarEnd
            },
            form = if (floats) FloatingActionForm.Disc else FloatingActionForm.Labelled,
            container = theme.color(ColorRole.Primary),
            content = theme.color(ColorRole.OnPrimary),
            shape = theme.shape(ShapeRole.Full),
            size = if (floats) 56.dp else 36.dp,
            horizontalPadding = theme.space(SpaceRole.Md),
            elevation = if (floats) 6.dp else 0.dp,
            borderWidth = 0.dp,
            borderColor = Color.Transparent,
            typeRole = TypeRole.Label,
            inset = if (floats) theme.space(SpaceRole.Lg) else theme.space(SpaceRole.Sm),
            pressedAlpha = 0.8f,
        )
    }

    /**
     * How a badge is drawn: where it sits on what it is attached to, what shape it is, and
     * how a large count is written.
     *
     * The Host sends a count, a word or neither, and a colour role. Whether the mark
     * overlaps the corner of an icon or waits at the end of a row, and whether 120 is
     * written out or cut to `99+`, are this system's answers, and the same declaration
     * comes out differently under each of them.
     *
     * The default is the overlapping capsule most of these systems draw, writing every
     * count out, in this system's own colours.
     */
    fun badge(theme: ResolvedTheme): BadgeStyle = BadgeStyle(
        placement = BadgePlacement.Overlap,
        maxCount = null,
        height = 16.dp,
        dotSize = 6.dp,
        horizontalPadding = 4.dp,
        shape = theme.shape(ShapeRole.Full),
        labelSize = 11.sp,
        labelWeight = FontWeight.Medium,
        offsetX = (-4).dp,
        offsetY = 4.dp,
        ringWidth = 0.dp,
        ring = Color.Transparent,
        gap = theme.space(SpaceRole.Sm),
    )

    /**
     * How a split pane is shown at a size class, and what its divider looks like.
     *
     * The size class is the split pane's own width, not the window's, because a split pane
     * can sit in a narrow place inside a wide window. The default stacks when compact and
     * puts the panes side by side otherwise; the split pane itself lays the side pane over
     * the body instead when the side pane's minimum and the body's minimum do not both fit.
     * A system that answers differently overrides this, and no widget code changes.
     */
    fun splitPane(sizeClass: WindowSizeClass, theme: ResolvedTheme): SplitPaneStyle = SplitPaneStyle(
        presentation = when (sizeClass) {
            WindowSizeClass.Compact -> SplitPanePresentation.Stacked
            WindowSizeClass.Medium -> SplitPanePresentation.SideBySide
            WindowSizeClass.Expanded -> SplitPanePresentation.SideBySide
        },
        defaultWidth = 280.dp,
        minWidth = 200.dp,
        maxWidth = 400.dp,
        bodyMinWidth = 360.dp,
        collapseDistance = 48.dp,
        keyStep = 8.dp,
        lineWidth = 1.dp,
        lineColor = theme.color(ColorRole.OutlineVariant),
        handle = null,
        grabWidth = 8.dp,
        resizeCursor = true,
        sideBackground = Color.Transparent,
        scrim = Color.Black.copy(alpha = 0.32f),
        backButton = true,
        edgeSwipeBack = false,
        focusRing = theme.color(ColorRole.Primary),
    )
}

/**
 * How a split pane shows its two panes in the room it was measured to have.
 *
 * `SideBySide` is two columns with a divider between them. `Overlay` gives the body the
 * whole width and lays the side pane over it from the leading edge. `Stacked` shows one
 * pane at a time, and the Host's `SelectedIndex` says which.
 */
enum class SplitPanePresentation { SideBySide, Overlay, Stacked }

/** The grip a system draws on a split pane's divider, where it draws one. */
data class SplitHandle(
    /** Across the divider. */
    val thickness: Dp,
    /** Along the divider. */
    val length: Dp,
    val color: Color,
    val shape: Shape,
)

/** Everything a split pane needs from the design system, answered in one call. */
data class SplitPaneStyle(
    val presentation: SplitPanePresentation,
    /** The side pane's width where the Host did not say one. */
    val defaultWidth: Dp,
    /** The narrowest and widest the side pane may be dragged where the Host did not say. */
    val minWidth: Dp,
    val maxWidth: Dp,
    /**
     * The narrowest the body may become beside the side pane. Below it, two columns are
     * not an answer in any system, and the side pane is laid over the body instead.
     */
    val bodyMinWidth: Dp,
    /** How far past its minimum the side pane has to be dragged before it folds. */
    val collapseDistance: Dp,
    /** How far one arrow key moves the divider. */
    val keyStep: Dp,
    /** The visible line between the panes. Zero for none. */
    val lineWidth: Dp,
    val lineColor: Color,
    /** A grip on the divider, or null where the line alone is the divider. */
    val handle: SplitHandle?,
    /** How wide the strip that takes a drag is. Usually wider than what is drawn. */
    val grabWidth: Dp,
    /** Whether the pointer shows the left and right resize shape over the divider. */
    val resizeCursor: Boolean,
    /** What the side pane is filled with. Transparent leaves it the page. */
    val sideBackground: Color,
    /** What is laid over the body behind a side pane drawn over it. */
    val scrim: Color,
    /** Whether the body shown on its own carries a back button this system draws. */
    val backButton: Boolean,
    /** Whether a swipe in from the leading edge of the body goes back. */
    val edgeSwipeBack: Boolean,
    /** The ring around the divider when the keyboard has it. */
    val focusRing: Color,
)

/**
 * Where a badge goes relative to the thing it is attached to.
 *
 * `Overlap` centres the mark on the top trailing corner and lets it hang over the edge,
 * the way an unread count sits on an icon. `Trailing` sets it beside the thing at the end
 * of its line, the way a sidebar row carries its count. A badge with nothing attached is
 * drawn on its own under either.
 */
enum class BadgePlacement { Overlap, Trailing }

/** Everything a badge needs from the design system, answered in one call. */
data class BadgeStyle(
    val placement: BadgePlacement,
    /**
     * The largest count written out in full. Above it the mark reads as this number and a
     * plus. Null where this system writes every count out.
     */
    val maxCount: Int?,
    /** The height of a mark that carries a count or a word. It is never narrower. */
    val height: Dp,
    /** The diameter of a mark that carries neither. */
    val dotSize: Dp,
    val horizontalPadding: Dp,
    val shape: Shape,
    val labelSize: TextUnit,
    val labelWeight: FontWeight,
    /**
     * How far an overlapping mark is moved from being centred on the corner. Negative x
     * moves it in towards the start, positive y moves it down onto the child.
     */
    val offsetX: Dp,
    val offsetY: Dp,
    /** A ring around the mark that separates it from what it overlaps. Zero for none. */
    val ringWidth: Dp,
    val ring: Color,
    /** The room between the thing and a trailing mark. */
    val gap: Dp,
) {
    /** What a count reads as here: written out, or cut at this system's ceiling. */
    fun label(count: Long): String {
        val ceiling = maxCount ?: return count.toString()
        return if (count > ceiling) "$ceiling+" else count.toString()
    }
}

/** Which end of the caption the window buttons sit at. */
enum class CaptionSide { Start, End }

/** Where the window's title sits along the caption. */
enum class CaptionTitleAlignment { Start, Center }

/**
 * How the window's own caption is drawn: the three buttons, and where the title sits.
 *
 * All seven systems are described by one shape, because the differences between them are
 * differences of value rather than of kind: a disc at the leading edge that shows its
 * glyph only under the pointer, a wide rectangle at the trailing edge that turns red when
 * the pointer is on close, a grey circle, a bare glyph. What none of them disagree about
 * is that there are three buttons and what each one does.
 *
 * Nothing here crosses the boundary. A window button is the design system's and the
 * platform's, and an application that could recolour one would be deciding for whichever
 * platform it ended up on.
 */
data class CaptionStyle(
    val side: CaptionSide,
    /** Wider than it is tall on the systems that give a button a hover zone. */
    val buttonWidth: Dp,
    val buttonHeight: Dp,
    val shape: Shape,
    /** What each button is filled with when the pointer is elsewhere. */
    val minimiseContainer: Color,
    val maximiseContainer: Color,
    val closeContainer: Color,
    val hover: Color,
    /** Close is the button most of these systems colour differently under the pointer. */
    val closeHover: Color,
    val glyph: Color,
    val closeHoverGlyph: Color,
    val glyphStroke: Dp,
    /**
     * Whether the glyph is drawn when the pointer is elsewhere.
     *
     * False on the one system whose buttons are coloured discs: their colour is what says
     * which is which, and the marks inside appear only when the pointer is over the set.
     */
    val glyphAtRest: Boolean,
    val spacing: Dp,
    val edgePadding: Dp,
    val titleAlignment: CaptionTitleAlignment,
    /**
     * How far in from the window's corner the platform's own buttons sit.
     *
     * macOS only, and only where the window's buttons are the system's. Everywhere else
     * the caption's buttons are ours and drawn where [side] says, so there is no corner to
     * come in from. Zero leaves them where the system put them.
     */
    val platformButtonInset: Dp = 0.dp,
    /**
     * How round the window's own corners are, or zero to leave the system's.
     *
     * macOS only, for the same reason. On Windows and Linux the window's outline belongs
     * to the compositor rather than to the application.
     */
    val windowCornerRadius: Dp = 0.dp,
    /**
     * How tall the caption is where the application draws it.
     *
     * Windows and Linux. Zero means the design system has nothing to say and the caption
     * is whatever it was, which is what every system answered before the two title bar
     * modes existed.
     */
    val height: Dp = 0.dp,
)

/**
 * What marks the selected destination.
 *
 * The three systems disagree, visibly: Material puts a filled pill behind the icon, Fluent
 * draws a short bar along the leading edge of the row, and a Cupertino tab bar marks the
 * selection with colour alone. A widget that only knew "selected" could not produce any of
 * the three on purpose.
 */
enum class NavigationIndicator { Pill, LeadingEdgeBar, None }

/**
 * How much of a destination the mark covers.
 *
 * Material's pill sits behind the icon and the label hangs below it, outside the fill.
 * A Plasma sidebar row, a Deepin lozenge and an Adwaita view switcher button all cover
 * the whole destination and write the label on the fill. The two are not interchangeable:
 * a label coloured to read on the mark, drawn beside a mark that does not reach it, comes
 * out white on white.
 */
enum class NavigationExtent { Icon, Destination }

/** Which of its three shapes a set of destinations has taken. */
enum class NavigationPresentation {
    /** A bar across the bottom of the window, destinations side by side. */
    Bar,

    /** A narrow column down the leading edge. */
    Rail,

    /** A wide column down the leading edge, labels beside their icons. */
    Drawer,

    /**
     * Not on the screen, with a button that brings it back over the page.
     *
     * What every macOS sidebar does when there is no room for it, and the only one of
     * these four where the page has the whole window. The button belongs with the window's
     * own buttons where the platform hands its caption over.
     */
    PutAway,
}

/**
 * How one presentation of a set of destinations is drawn.
 *
 * All three presentations are described by one style, because they are one concept: what
 * changes between them is where the strip sits and how much room a destination gets, not
 * what a destination is.
 */
data class NavigationStyle(
    val presentation: NavigationPresentation,
    val container: Color,
    val content: Color,
    val selectedContent: Color,
    /** What is drawn behind or beside the selected destination. */
    val indicator: Color,
    val indicatorShape: Shape,
    val indicatorKind: NavigationIndicator,
    /** Whether that mark covers the icon alone or the whole destination. */
    val indicatorExtent: NavigationExtent = NavigationExtent.Icon,
    /**
     * The ink a group's heading is set in, or null to take a quieter shade of the rows'.
     *
     * Named where the rows are set in the reading ink, because then there is a role for
     * this and a fraction of black is not it.
     */
    val headingContent: Color? = null,
    /** The hairline between the destinations and the screen, null where there is none. */
    val separator: Color?,
    val barHeight: Dp,
    val railWidth: Dp,
    val drawerWidth: Dp,
    val itemSpacing: Dp,
    val itemPadding: Dp,
    /** Whether a rail, which is narrow, still writes the label under the icon. */
    val labelInRail: Boolean,
    val typeRole: TypeRole,
    /**
     * The weight a drawer's rows are set in, or null to take the rung's own.
     *
     * Named where the rung a sidebar row borrows is heavier than a row should be. The
     * rungs that label something are set heavier in the systems that draw on glass,
     * because a label on a translucent surface competes with whatever shows through it,
     * and that reasoning is about the label on a button, a segment or a section head. A
     * sidebar row is not labelling a control; it is a line in a list you read.
     */
    val destinationWeight: FontWeight? = null,
    /** Optional page gradient behind both the destinations and their content. */
    val pageGradientStart: Color? = null,
    val pageGradientEnd: Color? = null,
    /**
     * What the strip is made of when it floats, or null for a strip painted [container]
     * straight onto the window's edge.
     */
    val stripMaterial: SurfaceMaterial? = null,
    /** The outline of a floating strip. A strip on the window's edge has none. */
    val stripShape: Shape = RectangleShape,
    /**
     * How far a floating strip stands off the edges of the window it is laid along.
     *
     * Zero is a strip attached to the edge, which is what every system but one draws. A
     * bar keeps this much room at its sides and under it; a rail or a drawer at its
     * leading edge, top and bottom.
     */
    val floatingInset: Dp = 0.dp,
    /**
     * Whether a rail or a drawer runs to the top of the window and carries the window
     * buttons on its own surface.
     *
     * Where it does, the strip is the top of the window on its side, the caption is laid
     * out inside it, and the page beside it starts its own content clear of the caption
     * without a band of page colour across the top of the whole window.
     */
    val carriesCaption: Boolean = false,
    /**
     * Whether the page gradient runs behind a rail or a drawer as well as behind the page.
     *
     * True where the strip is translucent over the page, so it takes the page's colour.
     * False where the strip sits straight on the window's own backdrop instead, and the
     * page is painted only beside it.
     */
    val pageBehindStrip: Boolean = true,
    /**
     * How far down the page the gradient's first colour holds before it starts to turn,
     * as a fraction of the page's height. Zero is an even gradient from top to bottom.
     */
    val pageGradientHold: Float = 0f,
    /**
     * The gap between one destination and the next down a rail or a drawer, or null to
     * use [itemSpacing]. Separate because [itemSpacing] is also the gap between an icon
     * and its label, and a drawer wants the rows closer together than that.
     */
    val destinationGap: Dp? = null,
    /**
     * The room the strip keeps around its destinations, or null to use [itemPadding].
     *
     * Separate because the two are measured against different things. The strip's own
     * inset is what sets how far the selected row's mark stops short of the panel's edge,
     * and the room inside a row is what sets where the icon column falls; a sidebar that
     * uses one number for both puts its icons wherever its mark happens to want to stop.
     */
    val stripPadding: Dp? = null,
    /**
     * The colour that rises from the window's two bottom corners, or null for a page whose
     * wash is level all the way across.
     *
     * A wash that turns at the same height at every horizontal position is a band, and a
     * band reads as a fill rather than as light. Measured across the reference, the height
     * its wash begins at runs from six hundred and forty two under the middle of the window
     * to four hundred and ninety six at the trailing edge: two glows anchored in the bottom
     * corners, arcing up over a level ramp.
     */
    val pageCornerGlow: Color? = null,
    /**
     * The room inside one drawer row, from the row's edge to its icon, or null to use
     * [itemPadding].
     */
    val destinationInset: Dp? = null,
    /**
     * How tall one drawer row is, or null to let what it holds decide.
     *
     * A row with an icon and a row with only a name are not the same height when nothing
     * says they are, and a list whose rows are two heights has no rhythm.
     */
    val destinationHeight: Dp? = null,
)

/** The edge a sheet comes in from. */
enum class SheetEdge { Bottom, End }

/**
 * How a sheet is drawn, and where it comes from.
 *
 * Nothing here is sent by the Host, including the edge: the Renderer measured the window,
 * so the Renderer decides.
 */
data class SheetStyle(
    val edge: SheetEdge,
    val container: Color,
    val content: Color,
    val shape: Shape,
    val elevation: Dp,
    val scrim: Color,
    /** The grab handle a sheet draws along its dragging edge, null where none is drawn. */
    val handle: Color?,
    /** How much of the window a sheet entering from the side takes. */
    val widthFraction: Float,
    /** How much of the window a sheet entering from the bottom takes. */
    val heightFraction: Float,
    val padding: Dp,
    val borderWidth: Dp,
    val borderColor: Color,
)

/**
 * Where a transient message appears.
 *
 * Four rather than one, because this is a place the systems genuinely disagree: Material
 * puts a snackbar low and to the leading side, GNOME centres a toast along the bottom
 * edge, Windows and Apple slide a banner in from the top corner, and Deepin drops a
 * lozenge in at the top middle. A widget that could only be told "show a message" would
 * have to pick one of the four for everybody.
 */
enum class MessagePlacement { BottomStart, BottomCenter, TopCenter, TopEnd;

    /** True where the message sits along the window's top edge. */
    val atTop: Boolean get() = this == TopCenter || this == TopEnd
}

/**
 * How a transient message is drawn and how long it stays.
 *
 * The durations are part of this because they are part of the design language, and because
 * the alternative is the Host holding a timer for an animation it cannot see.
 */
data class MessageStyle(
    val container: Color,
    val content: Color,
    val actionContent: Color,
    val shape: Shape,
    val elevation: Dp,
    val placement: MessagePlacement,
    val horizontalPadding: Dp,
    val verticalPadding: Dp,
    /** How far the message sits from the window's edges. */
    val inset: Dp,
    val shortMillis: Int,
    val longMillis: Int,
    val borderWidth: Dp,
    val borderColor: Color,
    val typeRole: TypeRole,
)

/**
 * The design system's state transition timing.
 *
 * The four named lengths are what a node's declared importance resolves to. A screen says
 * how much a change matters and never how many milliseconds it takes, because Material's
 * emphasized curve and Cupertino's softer, shorter one are different answers to the same
 * question and the screen is not the place to settle it.
 */
data class Motion(
    val pressMillis: Int,
    val releaseMillis: Int,
    val easing: androidx.compose.animation.core.Easing,
    /** How long a pointer rests on something before its explanation appears. */
    val tooltipDelayMillis: Int = 500,
    val quickMillis: Int = pressMillis,
    val standardMillis: Int = releaseMillis,
    val slowMillis: Int = releaseMillis * 2,
    val emphasizedMillis: Int = releaseMillis * 3 / 2,
    /** The curve for the one role that is allowed its own, and the same curve otherwise. */
    val emphasizedEasing: androidx.compose.animation.core.Easing = easing,
) {

    /** How long a change of this importance runs for. Instant is a change with no run. */
    fun millis(role: MotionRole): Int = when (role) {
        MotionRole.Instant -> 0
        MotionRole.Quick -> quickMillis
        MotionRole.Standard -> standardMillis
        MotionRole.Slow -> slowMillis
        MotionRole.Emphasized -> emphasizedMillis
    }

    fun easing(role: MotionRole): androidx.compose.animation.core.Easing =
        if (role == MotionRole.Emphasized) emphasizedEasing else easing

    /**
     * The spec a change of this importance runs on.
     *
     * A zero length tween finishes inside the frame it starts in, which is what a system
     * asked to reduce motion gets for every role, and what `Instant` means anyway.
     */
    fun <T> spec(role: MotionRole): androidx.compose.animation.core.FiniteAnimationSpec<T> =
        androidx.compose.animation.core.tween(millis(role), easing = easing(role))
}

/**
 * A line along a field's bottom edge, which is what Material and Fluent thicken when the
 * caret goes in.
 *
 * Null on the systems that mark focus with the box instead. Separate from the box because
 * Fluent draws both: a grey rectangle that stays grey, and a bottom line that goes accent.
 */
data class FieldUnderline(
    val color: Color,
    val focusedColor: Color,
    val width: Dp,
    val focusedWidth: Dp,
)

/**
 * The frame around a text field, resting and with the caret in it.
 *
 * Nothing here is sent by the Host. A field is one of the places these six languages
 * diverge most visibly, and a screen that painted its own fill and line would be deciding
 * for whichever system it ended up under.
 *
 * Focus is Renderer state, so the second half of every pair is reached without a boundary
 * call and the Host never learns that the caret moved.
 */
/**
 * The shape a field of this height should actually wear.
 *
 * A capsule is a shape for something one line tall. Liquid Glass asks for one, which is
 * right for a search field and wrong for a page: a full corner on an editor that fills
 * the window puts a radius of half the window on it, and the notepad drew its document
 * as an enormous lozenge with the first line of text cut off by the curve.
 *
 * Only the fully round corner is capped, and only where the field takes more than a line.
 * Everything else a design system asked for is left exactly as it asked.
 */
internal fun FieldStyle.forHeight(multiline: Boolean, theme: ResolvedTheme): FieldStyle =
    if (multiline && shape == theme.shape(ShapeRole.Full)) {
        copy(shape = theme.shape(ShapeRole.Large))
    } else {
        this
    }

data class FieldStyle(
    val container: Color,
    val containerFocused: Color,
    /** The box around the field. A zero width is no box, not a hairline. */
    val border: Color,
    val borderFocused: Color,
    val borderWidth: Dp,
    val borderWidthFocused: Dp,
    val underline: FieldUnderline?,
    val shape: Shape,
    val horizontalPadding: Dp,
    val verticalPadding: Dp,
    val cursor: Color,
    /** How tall an empty single line field is before anything is typed into it. */
    val minHeight: Dp,
    /**
     * The fill a field takes when it sits on a glass surface, or null to keep [container].
     *
     * A field inside a glass bar is part of the bar in the systems that draw glass: the
     * composer at the foot of a chat is one capsule with the text in it, not a capsule
     * holding a second, greyer capsule.
     */
    val containerOnGlass: Color? = null,
)

/**
 * The kinds of container the core vocabulary has. A widget emits the role; which colour,
 * corner and height that role means belongs to the design system.
 */
enum class ContainerRole { Card, Surface, TopAppBar, Dialog, Menu, Tooltip }

/**
 * How one container role is drawn.
 *
 * `scrim` is what covers what is behind a modal, and is transparent for the roles that
 * cover nothing. `separator` is the hairline a bar draws under itself, null where the
 * system draws none.
 */
data class ContainerStyle(
    val container: Color,
    val content: Color,
    val shape: Shape,
    val elevation: Dp,
    val borderWidth: Dp,
    val borderColor: Color,
    val horizontalPadding: Dp,
    val verticalPadding: Dp,
    val separator: Color?,
    val scrim: Color,
    val typeRole: TypeRole,
    /**
     * What the container is made of, when the design system has an opinion beyond a flat
     * colour.
     *
     * Null means [container] is painted straight on, which is what Material 3, Fluent and
     * the Linux systems mean by a surface. Liquid Glass answers with glass for the roles
     * that are glass in the window it is drawing into, and with [SurfaceMaterial.Opaque]
     * for the rest; a role that is glass here draws the translucent tint and the lit edge
     * instead of [container], and falls back to the stored opaque colour when the reader
     * has asked for reduced transparency.
     */
    val material: SurfaceMaterial? = null,
    /**
     * Whether this container is drawn as floating pieces rather than as a strip.
     *
     * Only a top app bar asks the question. Where it is true the bar paints nothing
     * across the window: its title sits on the page as it is, and each run of plain
     * actions in it is gathered into a capsule of [material] floating over the page,
     * which is how a toolbar looks in the systems that draw glass.
     */
    val floats: Boolean = false,
    /**
     * How far the floating pieces of a bar stand off the edges of the window, or null to
     * take the space ladder's own step.
     *
     * Named where a window already holds something else off its edge, so that the capsule
     * at one corner and the panel at the other are held off by one measurement rather than
     * by two that happen to be close.
     */
    val floatingInset: Dp? = null,
)

/**
 * How a tab strip is drawn.
 *
 * The three systems disagree about what marks the selection: Material underlines the tab,
 * Cupertino fills the selected segment, Fluent draws a short bar under the label. All three
 * are expressed here, so the widget only has to know which tab is selected.
 */
data class TabsStyle(
    val container: Color,
    val shape: Shape,
    val selectedContent: Color,
    val unselectedContent: Color,
    val selectedContainer: Color,
    val selectedShape: Shape,
    val indicator: Color,
    val indicatorHeight: Dp,
    val indicatorShape: Shape,
    /** True where the mark spans the whole tab, false where it is a short centred bar. */
    val indicatorFillsTab: Boolean,
    val horizontalPadding: Dp,
    val verticalPadding: Dp,
    val typeRole: TypeRole,
)

/**
 * The metrics one icon is drawn to.
 *
 * The geometry of each role is shared, because `Back` means the same thing everywhere. What
 * differs is how it is drawn: Material's heavier stem with flat ends, Cupertino's thin
 * stroke with rounded ends, Fluent's lighter stroke with square ends, each at its own
 * optical size.
 */
data class IconStyle(
    val size: Dp,
    val strokeWidth: Dp,
    val cap: androidx.compose.ui.graphics.StrokeCap,
    val join: androidx.compose.ui.graphics.StrokeJoin,
    /**
     * How far back from each corner the line starts to turn, or zero for a corner that is
     * met rather than turned.
     *
     * A join says what happens where two strokes meet at a point; this says that they do
     * not meet at a point at all. The rounded sets draw a frame as a square with its
     * corners cut into arcs a good deal wider than the stroke, and a join cannot say that
     * however round it is.
     */
    val corner: Dp = 0.dp,
)

/** Which of the three toggles is being drawn. */
enum class ToggleRole { Checkbox, RadioButton, Switch }

/**
 * How one toggle is drawn, in both of the states it holds.
 *
 * The unchecked and checked colours are separate fields rather than a second call so the
 * transition between them can be interpolated.
 */
data class ToggleStyle(
    /** The control's own footprint, before anything placed beside it. */
    val size: Dp,
    /** The box, circle or track that holds the state. */
    val container: Color,
    val containerChecked: Color,
    /** The tick, the dot or the thumb that shows it. */
    val mark: Color,
    val markUnchecked: Color,
    val border: Color,
    val borderWidth: Dp,
    val shape: Shape,
    /** A switch's travelling thumb. The other two roles leave it at zero. */
    val thumbSize: Dp,
    val trackWidth: Dp,
    val trackHeight: Dp,
    val disabledAlpha: Float,
)

/**
 * How a slider's track, its filled part and its thumb are drawn.
 *
 * `tick` is null where the system marks no discrete stops, which is the difference
 * between a Material slider and a Cupertino one at a glance.
 */
data class SliderStyle(
    val trackHeight: Dp,
    val track: Color,
    val activeTrack: Color,
    val thumbSize: Dp,
    val thumb: Color,
    val thumbBorder: Color,
    val thumbBorderWidth: Dp,
    val tick: Color?,
)

/** How progress is shown, as a bar or as a ring. */
data class ProgressStyle(
    val thickness: Dp,
    val track: Color,
    val indicator: Color,
    /** Diameter when circular. */
    val diameter: Dp,
    val rounded: Boolean,
    /** One full sweep of the indeterminate animation. */
    val periodMillis: Int,
)

/** A rule between two things: thin and full width in some systems, inset in others. */
data class DividerStyle(
    val thickness: Dp,
    val color: Color,
    /** How far it is held back from the leading edge. */
    val inset: Dp,
)

/** Everything the selection controls and the indicators need, answered in one call. */
data class ControlsStyle(
    val checkbox: ToggleStyle,
    val radioButton: ToggleStyle,
    val switch: ToggleStyle,
    val slider: SliderStyle,
    val progress: ProgressStyle,
    val divider: DividerStyle,
) {
    fun toggle(role: ToggleRole): ToggleStyle = when (role) {
        ToggleRole.Checkbox -> checkbox
        ToggleRole.RadioButton -> radioButton
        ToggleRole.Switch -> switch
    }
}

/** The way a date is picked. */
enum class DatePresentation {
    /** A month laid out as a grid of days. */
    CalendarGrid,

    /** Scrolling wheels, one per field. */
    Wheel,

    /** A field that opens a calendar over the page. */
    CalendarFlyout,
}

/** The way a time of day is picked. */
enum class TimePresentation {
    /** A clock face the hand is dragged around. */
    Dial,

    /** Scrolling wheels for the hour and the minute. */
    Wheel,

    /** A field with the hour and the minute stepped up and down. */
    Stepper,
}

/** The way one item out of a list is picked. */
enum class ChoicePresentation {
    /** A field that drops a menu below itself. */
    ExposedMenu,

    /** A wheel of the options, with the chosen one in the middle. */
    Wheel,

    /** A field that opens a flyout list over the page. */
    ComboBox,
}

/**
 * Which of the ways of picking this design system uses.
 *
 * There is no Host property that can override any of these. A widget that could ask for a
 * wheel would stop being a date and start being a piece of interface design, and the
 * system's own conventions would be the thing that loses.
 */
data class PickerRules(
    val date: DatePresentation,
    val time: TimePresentation,
    val choice: ChoicePresentation,
    /**
     * How tall one row of a wheel is.
     *
     * A wheel shows the chosen value between its neighbours, so this is what decides how
     * much of a window the control takes and how far a finger has to travel for one step.
     * It belongs to the design system for the same reason a button's minimum height does:
     * a roomy language spins a roomy wheel, and one number shared by every system means
     * two systems that both spin a wheel spin an identical one.
     */
    val wheelRowHeight: Dp = DEFAULT_WHEEL_ROW_HEIGHT,
)

/** The row height a system takes when it has no opinion about how roomy a wheel is. */
val DEFAULT_WHEEL_ROW_HEIGHT: Dp = 32.dp

/**
 * How one button variant is drawn. The pressed values are separate fields rather than a
 * second call, so the press animation can interpolate between the two.
 */
data class ButtonStyle(
    val container: Color,
    val pressedContainer: Color,
    val content: Color,
    val pressedContentAlpha: Float,
    val borderWidth: Dp,
    val borderColor: Color,
    val pressedBorderColor: Color,
    /** Fluent's lighter top edge; null where the system has no such stroke. */
    val topHighlight: Color?,
    val shape: Shape,
    val horizontalPadding: Dp,
    val verticalPadding: Dp,
    val minHeight: Dp,
    val typeRole: TypeRole,
    val restElevation: Dp,
    val pressedElevation: Dp,
    /**
     * How much of the button is left when it is disabled.
     *
     * One number for the whole control rather than a disabled colour per variant: four
     * more colours is four more answers every design system owes, and a seventh system
     * would owe them too. Fading the drawn button says the same thing in every system's
     * own palette, because it is that system's palette being faded.
     */
    val disabledAlpha: Float,
)

/**
 * How a chip is drawn, chosen and not.
 *
 * Both states are fields of one style rather than two calls, so a chip that the Host has
 * just chosen can move from one to the other without asking the rules again. The chip
 * never decides which of the two it is in: that arrives from the Host.
 */
data class ChipStyle(
    val container: Color,
    val selectedContainer: Color,
    val content: Color,
    val selectedContent: Color,
    val borderWidth: Dp,
    val borderColor: Color,
    /** The edge of a chosen chip. Transparent where choosing one drops the line. */
    val selectedBorderColor: Color,
    val shape: Shape,
    val height: Dp,
    val horizontalPadding: Dp,
    /** The room between a glyph and the label. */
    val iconGap: Dp,
    val typeRole: TypeRole,
    /**
     * Whether a chosen chip leads with a tick.
     *
     * Material's filter chip does, which is how it tells a chosen chip from a merely
     * tinted one without colour alone. The pills of the other systems say it with the fill.
     */
    val leadingCheck: Boolean,
    /** How much of the chip is left while it is held down. */
    val pressedAlpha: Float,
    val disabledAlpha: Float,
)

/**
 * Where the one action a screen is about is put.
 *
 * Read by whatever frame the action is in, never by the action itself: a Scaffold and the
 * Box that holds a page both know where their corners and their bar are, and the action
 * does not.
 */
enum class FloatingActionPlacement {
    /** Over the page at its bottom trailing corner, taking none of it. Material's answer. */
    OverPageBottomEnd,

    /** At the trailing end of the top of the page, where a bar's actions are. Apple's plus. */
    BarEnd,

    /**
     * At the leading end of the top of the page, where a command bar or a header bar
     * starts. The Fluent primary command and the GNOME header button sit here.
     */
    BarStart,
}

/** What the action is drawn as. */
enum class FloatingActionForm {
    /** A raised disc, or a rounded square, holding the glyph alone. */
    Disc,

    /**
     * The glyph alone, as a bar button is: on no container at all, or on the quiet fill
     * the system gives a bar's buttons. Never raised.
     */
    Glyph,

    /** A button carrying the glyph and the label side by side. */
    Labelled,
}

/** How the one action a screen is about is drawn, and where its frame puts it. */
data class FloatingActionStyle(
    val placement: FloatingActionPlacement,
    val form: FloatingActionForm,
    val container: Color,
    val content: Color,
    val shape: Shape,
    /** The height, and for a [FloatingActionForm.Disc] the width as well. */
    val size: Dp,
    val horizontalPadding: Dp,
    val elevation: Dp,
    val borderWidth: Dp,
    val borderColor: Color,
    val typeRole: TypeRole,
    /** How far in from the edges of its frame the action is put. */
    val inset: Dp,
    val pressedAlpha: Float,
)

/**
 * Resolves the Host's `SetTheme` into the table and rules used for this frame.
 *
 * The Host's choice is read, never second-guessed: `adaptive` follows the platform only
 * because the Host said `adaptive = true`.
 *
 * With no `SetTheme` at all the default follows the platform, with Material 3 as the
 * fallback. A Host that has sent a theme is a different case from one that has
 * not said anything yet, and only the second is this default.
 */
fun resolveTheme(
    theme: Theme?,
    platform: HostPlatform,
    systemDark: Boolean,
    sizeClass: WindowSizeClass = WindowSizeClass.Compact,
    /**
     * What a registered font asset became, or null where nothing was registered.
     *
     * A lookup rather than the cache itself, so that the design system stays a thing that
     * can be resolved in a test with nothing else standing up around it.
     */
    fontOf: (Int) -> FontFamily? = { null },
    /** What a registered brush became, or null for an id naming nothing. */
    brushOf: (Int) -> ComposeBrush? = { null },
    /** Whether the window has the platform's own material behind it. */
    windowBackdrop: Boolean = false,
    /**
     * Whether the platform is in a high contrast mode, where the application's palette is
     * set aside and the design system's own colours are drawn.
     */
    highContrast: () -> Boolean = { dev.darkpyonix.composerust.ui.node.platformHighContrast() },
): ResolvedTheme {
    val system = when {
        theme == null -> adaptiveSystem(platform, DesignSystem.Material3)
        theme.adaptive -> adaptiveSystem(platform, theme.fallback)
        else -> theme.designSystem
    }
    val dark = when (theme?.colorScheme ?: ColorScheme.FollowSystem) {
        ColorScheme.Light -> false
        ColorScheme.Dark -> true
        ColorScheme.FollowSystem -> systemDark
    }
    // Only the roles the application actually named. A role whose asset is missing or is
    // not a font is left out, so it stays on the machine's own face rather than on
    // nothing, and the report of the missing asset is what says it happened.
    val fonts = buildMap {
        theme?.let { asked ->
            for (role in TypeRole.entries) {
                val family = asked.font(role)?.let(fontOf) ?: continue
                put(role, family)
            }
        }
    }
    val tokens = DesignTokens.of(system)
    val rules = rulesFor(system)
    // The same palette goes onto whichever system was chosen, and both of its schemes are
    // already here, so following the platform from light to dark reads the other half
    // rather than asking the Host for anything.
    val application = applicationColors(theme, dark, highContrast) { role ->
        rules.color(role, dark, sizeClass) ?: Color(tokens.color(role, dark))
    }
    return ResolvedTheme(
        system,
        tokens,
        rules,
        dark,
        sizeClass,
        fonts,
        brushOf,
        windowBackdrop,
        platform,
        application,
    )
}

/**
 * The opaque reading of a material: the system's surface, moved towards its own outline.
 *
 * Thickness is how far it has moved. A thin material is barely separated from the page, a
 * chrome panel is the furthest, and every step is taken between two colours this system
 * already chose, so nothing here invents a colour.
 */
/**
 * The colour a brush starts from.
 *
 * Only for the places that can hold a colour and nothing else. A gradient's first stop is
 * the honest answer there: it is a colour the application actually chose.
 */
private fun startingColor(brush: ComposeBrush): Color = when (brush) {
    is SolidColor -> brush.value
    else -> Color.Unspecified
}

internal fun opaqueMaterial(role: MaterialRole, theme: ResolvedTheme): Color {
    val surface = theme.color(ColorRole.Surface)
    val toward = theme.color(ColorRole.SurfaceVariant)
    val distance = when (role) {
        MaterialRole.Thin -> 0.18f
        MaterialRole.Regular -> 0.38f
        MaterialRole.Thick -> 0.62f
        MaterialRole.Chrome -> 0.85f
    }
    return Color(
        red = surface.red + (toward.red - surface.red) * distance,
        green = surface.green + (toward.green - surface.green) * distance,
        blue = surface.blue + (toward.blue - surface.blue) * distance,
        alpha = 1f,
    )
}

internal fun rulesFor(system: DesignSystem): ComponentRules = when (system) {
    DesignSystem.Material3 -> Material3Rules
    DesignSystem.Cupertino -> CupertinoRules
    DesignSystem.Fluent -> FluentRules
    DesignSystem.Gnome -> GnomeRules
    DesignSystem.Breeze -> BreezeRules
    DesignSystem.Deepin -> DeepinRules
    DesignSystem.LiquidGlass -> LiquidGlassRules
}

/**
 * The theme every interpreted node reads. `SetTheme` changes it once and Compose invalidates
 * the readers, so a theme change is one record on the wire rather than a `SetProp` for
 * every node in the tree.
 */
val LocalDesignTheme = staticCompositionLocalOf {
    resolveTheme(theme = null, platform = HostPlatform.Unknown, systemDark = false)
}