crcbl-shell 0.8.0

Crucible windowing shell: own Wayland, X11, Web/canvas and headless backends
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
//! The `NSView` subclass this backend builds at runtime: every responder method
//! input arrives through, the tracking area, and the `NSTextInputClient` that
//! makes text exist at all.
//!
//! # Decision: a view subclass, not an interception in the pump
//!
//! [`pump`](crate::Shell::pump) dequeues each `NSEvent` and hands it to
//! `-[NSApplication sendEvent:]`, so it *could* read `[event type]` and take the
//! input apart there instead — several engines do. This backend does not, for
//! three reasons that are all about what `sendEvent:` knows and a bystander does
//! not:
//!
//! 1. **`sendEvent:` is what routes an event to a window**, and a shell with two
//!    windows would otherwise have to reimplement that routing from
//!    `[event window]` — including the cases where AppKit sends an event
//!    somewhere else, which is exactly what happens to a key equivalent.
//! 2. **`mouseEntered:`/`mouseExited:` have no `NSEvent` in the queue to
//!    intercept.** They are delivered by the tracking area, to an owner, and the
//!    owner has to be an object.
//! 3. **Text is not in the event at all.** It comes back out of
//!    `interpretKeyEvents:` through a responder, which means an object again.
//!
//! So there is a class, and the events reach the shell the way everything else
//! in this backend does: recorded into [`Shared`](super::app::Shared) and
//! translated later. The view carries no instance storage, exactly as [`app`]'s
//! delegate does not — it finds its shell through `[[self window] delegate]`,
//! which is the delegate object that *is* in the registry.
//!
//! # The AppKit shape of the `TranslateMessage` gap, and it is four gaps
//!
//! The Win32 half of P5C shipped a backend whose in-crate tests all passed and
//! whose `TextCommit` was unreachable from a real keyboard, because the pump
//! never called `TranslateMessage`. The equivalent question here — *what does
//! the real event path do that a hand-constructed call to a responder method
//! would skip?* — has four answers, and every one of them is invisible to a test
//! that calls `keyDown:` itself:
//!
//! | Missing | What silently stops working | Why a direct call cannot see it |
//! | --- | --- | --- |
//! | `-[NSWindow setAcceptsMouseMovedEvents:YES]` | **all** pointer motion with no button held | the system never *generates* `NSEventTypeMouseMoved` for a window that has not asked; `mouseMoved:` called by hand works perfectly |
//! | `-[NSWindow makeFirstResponder:view]` | every key event | `sendEvent:` delivers key events to the first responder, which is the window itself by default; a direct `keyDown:` bypasses the question |
//! | the `NSTrackingArea` | `PointerFocus` in both directions | nothing else produces `mouseEntered:`, so a test that calls it is testing its own call |
//! | `interpretKeyEvents:` | **all** text, including dead keys | `insertText:` is called *by the input method*; a test that calls it is the input method |
//! | `-[NSView registerForDraggedTypes:]` | **every** file drop | AppKit sends no `NSDraggingDestination` message to an unregistered view; `performDragOperation:` called by hand reads a pasteboard perfectly well |
//!
//! All five are established in [`window`](super::window) and here, and the
//! honest statement about them is in `docs/backlog.md`: they are structural
//! rather than verified, because verifying them needs injected input at the
//! session level, which is M4. The registration is the one of the five with a
//! **readback** — `-[NSView registeredDraggedTypes]` — so it is asserted as a
//! mechanism in [`session_support`](super::session_support) rather than only
//! written down.
//!
//! # Decision: a real `NSTextInputClient`, not `-[NSEvent characters]`
//!
//! The short path to text is to read `characters` off the key event and be done.
//! It produces the right string for an unmodified letter and the wrong one — or
//! none — for everything that composes: a dead key's state lives in the
//! *input context*, not in the event, so `⌥e` followed by `e` yields `e` rather
//! than `é`, and an input method never gets to run at all. Half of Europe cannot
//! type its own language against that.
//!
//! `interpretKeyEvents:` is the documented path, and it requires
//! `-[NSResponder inputContext]` to be non-nil, which requires the responder to
//! **conform to `NSTextInputClient`**, every method of which is required. They
//! are implemented below. Marked text is sent as
//! [`TextPreedit`](crate::ShellEvent::TextPreedit) for a window whose field
//! composes, and `firstRectForCharacterRange:` answers the area
//! [`set_text_input_area`](crate::Shell::set_text_input_area) last gave, so the
//! candidate window opens under the caret. With
//! [`TEXT_COMPOSE`](crate::ShellCaps::TEXT_COMPOSE), that is why this backend
//! sets [`TEXT_IME`](crate::ShellCaps::TEXT_IME).
//!
//! # Decision: a field restricts the input context, it does not bypass it
//!
//! [`set_text_input`](crate::Shell::set_text_input) says that with no field
//! taking typing the input method keeps out of the way and keys reach the
//! window as keys, while plain typing and dead keys still commit — which is
//! what Win32 and Wayland do, since a dead key is the keyboard layout's and not
//! an input method's. There are two ways to get that here, and only one keeps
//! the second half:
//!
//! * **Skipping `interpretKeyEvents:`** when no field is set, as some engines
//!   do, takes the input context out entirely — and the dead-key state lives in
//!   the input context, so `⌥e` then `e` would stop composing in every window
//!   that had not named a field, and plain text would need a second path out
//!   of `-[NSEvent characters]` that the decision above exists to avoid.
//! * **Restricting the context to the Roman input sources** —
//!   `allowedInputSourceLocales` set to
//!   [`NSAllRomanInputSourcesLocaleIdentifier`](ffi::NSAllRomanInputSourcesLocaleIdentifier)
//!   alone — keeps every key on the one path and only changes what may answer
//!   it: keyboard layouts may, so typing and dead keys commit as before, and an
//!   input method may not, so a Japanese one in its native mode gives way to a
//!   layout while the window is key instead of turning WASD into a candidate
//!   list. It is the documented lever for restricting a context to Roman
//!   input.
//!
//! So every key goes through `interpretKeyEvents:`, and
//! [`allow_input_methods`] is the switch: off for a new window, on for an
//! ordinary field, off again for none and for a password or a PIN, as Win32
//! keeps its input context off for both. A dead key composes through marked
//! text in all of them; only a window whose field composes reports it as a
//! pre-edit, so a window with no field, or a sensitive one, sees a dead key
//! the way Win32 and Wayland show one — as the commit and nothing before it.
//! **Whether an input method that is already active gives way the moment the
//! restriction is set, rather than at the next activation of the context, is
//! unverified**: no session test runs a real input method.
//!
//! Secure event input for a sensitive field is the shell's, not the view's:
//! it is session-wide and keyed on which window has the keyboard, so it is
//! recomputed wherever focus changes, beside the pointer freeze and the cursor
//! hide. See [`set_text_input`](crate::Shell::set_text_input) on
//! [`AppKitShell`](super::AppKitShell).
//!
//! `doCommandBySelector:` is overridden to do **nothing**, which is not laziness:
//! the default implementation beeps at every command it does not recognize, so a
//! game whose player presses Escape or an arrow key would beep once per
//! keystroke.

use core::ffi::CStr;
use core::ptr;
use std::path::PathBuf;
use std::sync::OnceLock;

use crcbl_core::input::ButtonState;

use super::app;
use super::events::RawEvent;
use super::ffi::{self, Class, Id, Imp, NSPoint, NSRange, NSRect, NSUInteger, ObjcBool, Sel, YES};
use super::geometry;
use super::keys;
use super::pasteboard;

/// The name of the `NSView` subclass this module builds at runtime.
const VIEW_CLASS: &CStr = c"CrcblView";

/// The registration, once per process. `Err` carries what went wrong.
static VIEW: OnceLock<Result<usize, String>> = OnceLock::new();

// ---------------------------------------------------------------------------
// Reading an NSEvent
// ---------------------------------------------------------------------------

/// `-[NSEvent timestamp]`: seconds since the system booted.
///
/// # Safety
///
/// `event` must be a live `NSEvent`.
unsafe fn seconds(event: Id) -> f64 {
    // SAFETY: an `NSEvent` accessor returning an `NSTimeInterval`, which is a
    // `double`.
    unsafe { ffi::msg_f64(event, ffi::sel(c"timestamp")) }
}

/// `-[NSEvent modifierFlags]`.
///
/// # Safety
///
/// `event` must be a live `NSEvent`.
unsafe fn flags(event: Id) -> NSUInteger {
    // SAFETY: an `NSEvent` accessor returning an `NSEventModifierFlags`, which
    // is an `NSUInteger`.
    unsafe { ffi::msg_usize(event, ffi::sel(c"modifierFlags")) }
}

/// `-[NSEvent keyCode]`, which is an `unsigned short` and not an `NSUInteger`.
///
/// # Safety
///
/// `event` must be a live key or flags-changed `NSEvent`.
unsafe fn keycode(event: Id) -> u16 {
    // SAFETY: the signature written here is the method's; a wider read would
    // pick up whatever the register above it held.
    let send: unsafe extern "C" fn(Id, Sel) -> u16 = unsafe { ffi::msg_send() };
    unsafe { send(event, ffi::sel(c"keyCode")) }
}

/// `-[NSEvent locationInWindow]`, in AppKit's window space with Y **up**.
///
/// # Safety
///
/// `event` must be a live mouse `NSEvent`.
unsafe fn location(event: Id) -> NSPoint {
    // SAFETY: an `NSPoint` is 16 bytes and comes back in registers on both
    // ABIs, so this is `msg_send` and never `msg_send_stret`.
    let send: unsafe extern "C" fn(Id, Sel) -> NSPoint = unsafe { ffi::msg_send() };
    unsafe { send(event, ffi::sel(c"locationInWindow")) }
}

/// The first character of an event's `charactersIgnoringModifiers`.
///
/// Read **here**, in the callback, because the string belongs to an event that
/// will not outlive it — the one interpretation this backend performs before the
/// shell is in hand, and [`events`](super::events) says why it has to be.
///
/// `characterAtIndex:` rather than a `String`, so a keystroke costs no
/// allocation. A key whose symbol is outside the basic plane would be lost by
/// that; no keyboard has one, and the astral case that genuinely matters —
/// committed text — goes through `insertText:` where it is handled whole.
///
/// # Safety
///
/// `event` must be a live key `NSEvent`.
unsafe fn first_character(event: Id) -> Option<char> {
    // SAFETY: a key event answers this with an autoreleased `NSString` or nil.
    let string = unsafe { ffi::msg(event, ffi::sel(c"charactersIgnoringModifiers")) };
    if string.is_null() {
        return None;
    }
    // SAFETY: `length` on a live `NSString`.
    if unsafe { ffi::msg_usize(string, ffi::sel(c"length")) } == 0 {
        return None;
    }
    // SAFETY: index zero is in range for a string of non-zero length;
    // `characterAtIndex:` answers a `unichar`, which is a `u16`.
    let unit = unsafe {
        let send: unsafe extern "C" fn(Id, Sel, NSUInteger) -> u16 = ffi::msg_send();
        send(string, ffi::sel(c"characterAtIndex:"), 0)
    };
    char::from_u32(u32::from(unit))
}

/// An `NSString`'s contents, whether it arrived as one or inside an
/// `NSAttributedString`.
///
/// `insertText:` and `setMarkedText:` are both documented to hand over either,
/// and an input method really does use both — the attributed form carries the
/// underlining a pre-edit is drawn with.
///
/// # Safety
///
/// `string` must be a live `NSString` or `NSAttributedString`, or `nil`, with an
/// autorelease pool in scope.
unsafe fn plain_string(string: Id) -> Option<String> {
    if string.is_null() {
        return None;
    }
    let plain = match ffi::class(c"NSAttributedString") {
        // SAFETY: `isKindOfClass:` is on `NSObject` and takes a live class.
        Some(attributed) if unsafe { is_kind_of(string, attributed) } => {
            // SAFETY: `-[NSAttributedString string]` returns the backing
            // `NSString`, autoreleased into the caller's pool.
            unsafe { ffi::msg(string, ffi::sel(c"string")) }
        }
        _ => string,
    };
    // SAFETY: a live `NSString`, with a pool in scope.
    unsafe { ffi::string_from_nsstring(plain) }
}

/// `-[NSObject isKindOfClass:]`.
///
/// # Safety
///
/// `object` must be a live object and `class` a live class.
unsafe fn is_kind_of(object: Id, class: Class) -> bool {
    // SAFETY: on `NSObject`, so every object implements it with this signature.
    let send: unsafe extern "C" fn(Id, Sel, Class) -> ObjcBool = unsafe { ffi::msg_send() };
    unsafe { send(object, ffi::sel(c"isKindOfClass:"), class) != ffi::NO }
}

// ---------------------------------------------------------------------------
// Reaching the shell
// ---------------------------------------------------------------------------

/// The `NSWindow` a view is in, or null.
///
/// # Safety
///
/// `view` must be a live `NSView`.
unsafe fn window_of(view: Id) -> Id {
    // SAFETY: an `NSView` accessor that may answer nil for a view that is not
    // in a window.
    unsafe { ffi::msg(view, ffi::sel(c"window")) }
}

/// The delegate object a view's window routes its events through.
///
/// **This is the whole of the view-to-shell mapping**, and it is why the view
/// needs no registry entry of its own: `[window delegate]` is the object
/// [`app::Delegate`] created and registered, and
/// `release_window` clears it before the window closes — so a callback that
/// arrives afterwards finds `nil` and records nothing, which is exactly what it
/// should do.
///
/// # Safety
///
/// `view` must be a live `NSView`.
unsafe fn delegate_of(view: Id) -> Id {
    // SAFETY: a live view; `window` may be nil, and messaging nil answers nil.
    let window = unsafe { window_of(view) };
    if window.is_null() {
        return ptr::null_mut();
    }
    // SAFETY: an `NSWindow` accessor that may answer nil.
    unsafe { ffi::msg(window, ffi::sel(c"delegate")) }
}

/// Records what `make` builds, against the shell that owns this view's window.
///
/// # Safety
///
/// `view` must be a live `NSView`.
unsafe fn record(view: Id, make: impl FnOnce(usize) -> RawEvent) {
    // SAFETY: a live view.
    let window = unsafe { window_of(view) };
    if window.is_null() {
        return;
    }
    // SAFETY: as above.
    let delegate = unsafe { delegate_of(view) };
    app::record(delegate, make(window as usize));
}

// ---------------------------------------------------------------------------
// The responder methods
// ---------------------------------------------------------------------------

/// `acceptsFirstResponder` — **`YES`**, and without it no key event ever reaches
/// this view.
///
/// `NSView` answers `NO`, so `makeFirstResponder:` would silently refuse and the
/// window would stay first responder itself. See the [module docs](self).
///
/// # Safety
///
/// Called only by AppKit, on an instance of the class built below.
unsafe extern "C" fn accepts_first_responder(_view: Id, _cmd: Sel) -> ObjcBool {
    YES
}

/// `acceptsFirstMouse:` — **`YES`**.
///
/// The click that brings a background window to the front is otherwise swallowed
/// by the activation, so a player's first shot after alt-tabbing back does
/// nothing. Windows and X11 both deliver it.
///
/// # Safety
///
/// Called only by AppKit, on an instance of the class built below.
unsafe extern "C" fn accepts_first_mouse(_view: Id, _cmd: Sel, _event: Id) -> ObjcBool {
    YES
}

/// `keyDown:` — the key edge, and then the text.
///
/// In that order, so the consumer sees the keystroke before whatever it
/// committed. The event's timestamp is stashed first, because `insertText:` is
/// called from inside `interpretKeyEvents:` and carries no event of its own.
///
/// # Safety
///
/// Called only by AppKit, on an instance of the class built below.
unsafe extern "C" fn key_down(view: Id, _cmd: Sel, event: Id) {
    // SAFETY: AppKit passes a live key `NSEvent`, and the view is live.
    unsafe {
        let stamp = seconds(event);
        record(view, |window| RawEvent::Key {
            window,
            keycode: keycode(event),
            character: first_character(event),
            flags: flags(event),
            state: ButtonState::Pressed,
            repeat: ffi::msg_bool(event, ffi::sel(c"isARepeat")),
            seconds: stamp,
        });
        app::with_shared(delegate_of(view), |shared| shared.set_keying(stamp));
        interpret(view, event);
    }
}

/// Hands a key event to the input method.
///
/// `interpretKeyEvents:` takes an array, because an application that batched its
/// own event loop could hand over several; this one always has exactly one.
///
/// # Safety
///
/// `view` must be a live `NSView` in a window, `event` a live key `NSEvent`, and
/// an autorelease pool must be in scope — which it is, because this runs inside
/// [`pump`](crate::Shell::pump).
unsafe fn interpret(view: Id, event: Id) {
    let Some(array_class) = ffi::class(c"NSArray") else {
        return;
    };
    // SAFETY: `arrayWithObject:` returns an autoreleased array holding the
    // event, and `interpretKeyEvents:` takes exactly that.
    unsafe {
        let array = ffi::msg1(array_class, ffi::sel(c"arrayWithObject:"), event);
        ffi::msg1_void(view, ffi::sel(c"interpretKeyEvents:"), array);
    }
}

/// `keyUp:`.
///
/// # Safety
///
/// Called only by AppKit, on an instance of the class built below.
unsafe extern "C" fn key_up(view: Id, _cmd: Sel, event: Id) {
    // SAFETY: AppKit passes a live key `NSEvent`.
    unsafe {
        record(view, |window| RawEvent::Key {
            window,
            keycode: keycode(event),
            character: first_character(event),
            flags: flags(event),
            state: ButtonState::Released,
            repeat: false,
            seconds: seconds(event),
        });
    }
}

/// `flagsChanged:` — **the only notification a modifier key produces.**
///
/// # Safety
///
/// Called only by AppKit, on an instance of the class built below.
unsafe extern "C" fn flags_changed(view: Id, _cmd: Sel, event: Id) {
    // SAFETY: AppKit passes a live `NSEvent`.
    unsafe {
        record(view, |window| RawEvent::FlagsChanged {
            window,
            keycode: keycode(event),
            flags: flags(event),
            seconds: seconds(event),
        });
    }
}

/// `mouseMoved:` and the three `*MouseDragged:` methods, which are one fact.
///
/// **Dragging is not moving, to AppKit.** While any button is held the system
/// stops sending `mouseMoved:` and sends `mouseDragged:`, `rightMouseDragged:`
/// or `otherMouseDragged:` instead — so a backend that implements only the first
/// loses every sample of every drag, which is the whole of a click-and-drag
/// gesture.
///
/// # Safety
///
/// Called only by AppKit, on an instance of the class built below.
unsafe extern "C" fn mouse_moved(view: Id, _cmd: Sel, event: Id) {
    // SAFETY: AppKit passes a live mouse `NSEvent`.
    unsafe {
        record(view, |window| RawEvent::PointerMotion {
            window,
            location: location(event),
            dx: ffi::msg_f64(event, ffi::sel(c"deltaX")),
            dy: ffi::msg_f64(event, ffi::sel(c"deltaY")),
            seconds: seconds(event),
        });
    }
}

/// `scrollWheel:`.
///
/// # Safety
///
/// Called only by AppKit, on an instance of the class built below.
unsafe extern "C" fn scroll_wheel(view: Id, _cmd: Sel, event: Id) {
    // SAFETY: AppKit passes a live scroll `NSEvent`.
    unsafe {
        record(view, |window| RawEvent::Scroll {
            window,
            dx: ffi::msg_f64(event, ffi::sel(c"scrollingDeltaX")),
            dy: ffi::msg_f64(event, ffi::sel(c"scrollingDeltaY")),
            precise: ffi::msg_bool(event, ffi::sel(c"hasPreciseScrollingDeltas")),
            location: location(event),
            flags: flags(event),
            seconds: seconds(event),
        });
    }
}

/// The six button methods' shared body.
///
/// # Safety
///
/// `view` must be live and `event` a live mouse `NSEvent`.
unsafe fn button(view: Id, event: Id, state: ButtonState) {
    // SAFETY: `buttonNumber` is an `NSInteger`; a live mouse event answers it.
    unsafe {
        let send: unsafe extern "C" fn(Id, Sel) -> isize = ffi::msg_send();
        let number = send(event, ffi::sel(c"buttonNumber"));
        record(view, |window| RawEvent::Button {
            window,
            number,
            state,
            location: location(event),
            flags: flags(event),
            seconds: seconds(event),
        });
    }
}

/// `mouseDown:`, `rightMouseDown:` and `otherMouseDown:`.
///
/// One implementation for the three, because `buttonNumber` already says which —
/// unlike Win32, where the thumb pair share a message and are told apart by the
/// high word of `wParam`.
///
/// # Safety
///
/// Called only by AppKit, on an instance of the class built below.
unsafe extern "C" fn mouse_down(view: Id, _cmd: Sel, event: Id) {
    // SAFETY: AppKit passes a live mouse `NSEvent`.
    unsafe { button(view, event, ButtonState::Pressed) };
}

/// `mouseUp:`, `rightMouseUp:` and `otherMouseUp:`.
///
/// # Safety
///
/// Called only by AppKit, on an instance of the class built below.
unsafe extern "C" fn mouse_up(view: Id, _cmd: Sel, event: Id) {
    // SAFETY: AppKit passes a live mouse `NSEvent`.
    unsafe { button(view, event, ButtonState::Released) };
}

/// `mouseEntered:`, from the tracking area.
///
/// # Safety
///
/// Called only by AppKit, on an instance of the class built below.
unsafe extern "C" fn mouse_entered(view: Id, _cmd: Sel, event: Id) {
    // SAFETY: AppKit passes a live `NSEvent` of the tracking type.
    unsafe {
        record(view, |window| RawEvent::PointerFocus {
            window,
            entered: true,
            location: location(event),
            seconds: seconds(event),
        });
    }
}

/// `mouseExited:`, from the tracking area.
///
/// # Safety
///
/// Called only by AppKit, on an instance of the class built below.
unsafe extern "C" fn mouse_exited(view: Id, _cmd: Sel, event: Id) {
    // SAFETY: AppKit passes a live `NSEvent` of the tracking type.
    unsafe {
        record(view, |window| RawEvent::PointerFocus {
            window,
            entered: false,
            location: location(event),
            seconds: seconds(event),
        });
    }
}

/// `cursorUpdate:` — **the only moment a cursor shape can be applied.**
///
/// AppKit re-establishes the cursor from the cursor rects and tracking areas
/// under the pointer on every movement, so a `[cursor set]` issued from
/// [`set_cursor`](crate::Shell::set_cursor) is overwritten before the next frame.
/// This is where it sticks, and it is why the shape a window wants is recorded
/// on [`Shared`](super::app::Shared) rather than applied when it is asked for.
///
/// A window that has never asked gets the arrow, explicitly. Leaving the cursor
/// alone instead would let whatever the pointer crossed on the way in persist
/// over our window.
///
/// # Safety
///
/// Called only by AppKit, on an instance of the class built below.
unsafe extern "C" fn cursor_update(view: Id, _cmd: Sel, _event: Id) {
    // SAFETY: a live view of a live window.
    let window = unsafe { window_of(view) };
    if window.is_null() {
        return;
    }
    // SAFETY: as above.
    let wanted =
        unsafe { app::with_shared(delegate_of(view), |s| s.cursor_of(window as usize)) }.flatten();
    // SAFETY: an `NSCursor` from a class method is a shared singleton AppKit
    // owns for the life of the process, so a recorded pointer is always live.
    unsafe {
        match wanted {
            Some(cursor) => ffi::msg_void(cursor as Id, ffi::sel(c"set")),
            None => {
                if let Some(class) = ffi::class(c"NSCursor") {
                    let arrow = ffi::msg(class, ffi::sel(c"arrowCursor"));
                    ffi::msg_void(arrow, ffi::sel(c"set"));
                }
            }
        }
    }
}

// ---------------------------------------------------------------------------
// NSDraggingDestination
// ---------------------------------------------------------------------------

/// `-[NSDraggingInfo draggingLocation]`, in AppKit's window space with Y **up**
/// — the same space `locationInWindow` is in, and converted the same way.
///
/// # Safety
///
/// `sender` must be a live object conforming to `NSDraggingInfo`.
unsafe fn dragging_location(sender: Id) -> NSPoint {
    // SAFETY: an `NSPoint` is 16 bytes and comes back in registers on both
    // ABIs, so this is `msg_send` and never `msg_send_stret`.
    let send: unsafe extern "C" fn(Id, Sel) -> NSPoint = unsafe { ffi::msg_send() };
    unsafe { send(sender, ffi::sel(c"draggingLocation")) }
}

/// `draggingEntered:` and `draggingUpdated:` — **`NSDragOperationCopy`, with no
/// inspection of what is being dragged.**
///
/// That is not laziness, it is what `registerForDraggedTypes:` already
/// guarantees: **AppKit sends no `NSDraggingDestination` message at all for a
/// drag that carries none of the registered types**, and this backend registers
/// exactly [`FILE_URL`](super::pasteboard::FILE_URL). So a drag that reaches
/// here has a file URL on it, and re-deriving that from the pasteboard would be
/// asking a question the system has already answered — while getting it wrong
/// would show a "no drop" cursor over a file this backend can perfectly well
/// accept.
///
/// Both selectors share this implementation because the answers must agree:
/// AppKit reuses `draggingEntered:`'s value only while `draggingUpdated:` is
/// absent, and a destination that accepted on entry and refused on the next
/// mouse movement is one whose drop cursor flickers.
///
/// # Safety
///
/// Called only by AppKit, on an instance of the class built below.
unsafe extern "C" fn dragging_entered(_view: Id, _cmd: Sel, _sender: Id) -> NSUInteger {
    ffi::value::DRAG_OPERATION_COPY
}

/// `prepareForDragOperation:` — **`YES`**.
///
/// The last chance to refuse before the drop happens, and there is nothing left
/// to refuse on: whether the pasteboard yields a usable path is decided in
/// [`perform_drag_operation`], which is where the answer can also be *reported*.
/// Implemented rather than left out because a `NO` from the default is the
/// difference between a drop that does nothing and a drop that is delivered,
/// and inheriting an answer from `NSView` is not something to leave to a
/// release note.
///
/// # Safety
///
/// Called only by AppKit, on an instance of the class built below.
unsafe extern "C" fn prepare_for_drag_operation(_view: Id, _cmd: Sel, _sender: Id) -> ObjcBool {
    YES
}

/// `performDragOperation:` — **the drop itself.**
///
/// The paths are read *here*, in the callback, for the reason the Win32
/// backend's window procedure reads its `HDROP` there: the dragging pasteboard
/// belongs to the drag and is not valid after this returns. That produces the
/// one thing a [`RawEvent`] cannot carry — a `Vec<PathBuf>` in a [`Copy`] enum —
/// so the payload goes into a queue of its own on [`Shared`](super::app::Shared)
/// and the raw event is a **marker** that keeps the drop in its place in the
/// event stream, exactly as a text commit's string does.
///
/// The payload is pushed **before** the marker, so that a marker is never in
/// the queue with nothing behind it.
///
/// A drag with no local file on it — a URL from a browser, a remote
/// `file://host/…` — answers `NO`: nothing was accepted, and saying otherwise
/// would leave the source believing a copy happened.
///
/// # Safety
///
/// Called only by AppKit, on an instance of the class built below.
unsafe extern "C" fn perform_drag_operation(view: Id, _cmd: Sel, sender: Id) -> ObjcBool {
    // SAFETY: AppKit passes a live object conforming to `NSDraggingInfo`; the
    // pasteboard it answers is autoreleased into the pool `pump` holds.
    let pasteboard = unsafe { ffi::msg(sender, ffi::sel(c"draggingPasteboard")) };
    if pasteboard.is_null() {
        return ffi::NO;
    }
    // SAFETY: a live `NSPasteboard`, on the main thread with a pool in scope.
    let paths: Vec<PathBuf> = unsafe { pasteboard::file_urls(pasteboard) }
        .iter()
        .filter_map(|url| pasteboard::path_from_file_url(url))
        .collect();
    if paths.is_empty() {
        return ffi::NO;
    }
    // SAFETY: a live view.
    let window = unsafe { window_of(view) };
    if window.is_null() {
        return ffi::NO;
    }
    // SAFETY: as above.
    let delegate = unsafe { delegate_of(view) };
    if app::with_shared(delegate, |shared| shared.push_drop(window as usize, paths)).is_none() {
        // The window's delegate has already been cleared, so there is no shell
        // left to deliver this to — see `delegate_of`.
        return ffi::NO;
    }
    // SAFETY: takes nothing and returns a `CFTimeInterval`. A drag carries no
    // `NSEvent` and therefore no timestamp of its own, and this is the same
    // clock `-[NSEvent timestamp]` is measured against — see
    // [`TimeBase`](super::TimeBase), which is why a reading of *this* clock and
    // not of `systemUptime` is the one that rebases correctly.
    let seconds = unsafe { ffi::CACurrentMediaTime() };
    app::record(
        delegate,
        RawEvent::FilesDropped {
            window: window as usize,
            // SAFETY: a live `NSDraggingInfo`.
            location: unsafe { dragging_location(sender) },
            seconds,
        },
    );
    YES
}

// ---------------------------------------------------------------------------
// NSTextInputClient
// ---------------------------------------------------------------------------

/// `insertText:replacementRange:` — **the only place text exists.**
///
/// Called by the input method, from inside `interpretKeyEvents:`. A commit
/// while text is marked replaces it, so the pre-edit ends first. The string is
/// filtered for control characters, because the input method really does commit
/// `\r` for Return and `\t` for Tab, and
/// [`TextCommit`](crate::ShellEvent::TextCommit) means *text* — the same filter
/// the other three backends apply. A commit that is entirely control characters
/// produces no event at all, which is what keeps
/// [`TextCommit::text`](crate::ShellEvent::TextCommit) non-empty.
///
/// # Safety
///
/// Called only by the input method, on an instance of the class built below.
unsafe extern "C" fn insert_text(view: Id, _cmd: Sel, string: Id, _replacement: NSRange) {
    // A commit replaces whatever was marked, so the pre-edit ends here, ahead
    // of the commit — and only if there was one, so plain typing, which
    // commits with nothing marked, records nothing extra per keystroke.
    // SAFETY: a live view.
    if unsafe { end_marked(view) } {
        // SAFETY: a live view.
        unsafe { record_preedit(view, String::new(), None) };
    }
    // SAFETY: the input method passes an `NSString` or an `NSAttributedString`,
    // and the pool `pump` holds is in scope.
    let Some(text) = (unsafe { plain_string(string) }) else {
        return;
    };
    let text: String = text.chars().filter(|c| keys::is_text(*c)).collect();
    if text.is_empty() {
        return;
    }
    // SAFETY: a live view.
    let window = unsafe { window_of(view) };
    if window.is_null() {
        return;
    }
    // SAFETY: as above.
    let delegate = unsafe { delegate_of(view) };
    let stamp = app::with_shared(delegate, |shared| {
        shared.push_text(window as usize, text);
        // The keystroke's own timestamp, stashed by `keyDown:` before it handed
        // the event over — not the moment the input method got round to it.
        shared.keying()
    });
    let Some(seconds) = stamp else { return };
    app::record(
        delegate,
        RawEvent::TextCommitted {
            window: window as usize,
            seconds,
        },
    );
}

/// `doCommandBySelector:` — **deliberately nothing.**
///
/// The default implementation beeps at any command it does not recognize, and
/// `interpretKeyEvents:` turns Escape, Tab, Return and every arrow key into one.
/// A game would beep once per keystroke. The key edges themselves are already
/// reported from `keyDown:`, so nothing is lost by refusing to act here.
///
/// # Safety
///
/// Called only by the input method, on an instance of the class built below.
unsafe extern "C" fn do_command_by_selector(_view: Id, _cmd: Sel, _selector: Sel) {}

/// `setMarkedText:selectedRange:replacementRange:` — the pre-edit string.
///
/// Its length is always recorded, because an input method cannot compose at
/// all without something answering `hasMarkedText` and `markedRange`
/// consistently — and that includes a dead key, whose accent is marked text
/// until the next key. It is *reported* as a
/// [`TextPreedit`](crate::ShellEvent::TextPreedit) only for a window whose
/// field composes; see [`record_preedit`] and the [module docs](self). An
/// empty string is the composition deleted back to nothing, which ends the
/// pre-edit. The selection is the input method's cursor, in UTF-16 units of
/// this string, and [`keys::marked_preedit`] converts it.
///
/// # Safety
///
/// Called only by the input method, on an instance of the class built below.
unsafe extern "C" fn set_marked_text(
    view: Id,
    _cmd: Sel,
    string: Id,
    selected: NSRange,
    _replacement: NSRange,
) {
    // SAFETY: the input method passes an `NSString` or `NSAttributedString`.
    let text = unsafe { plain_string(string) }.unwrap_or_default();
    let units = text.encode_utf16().count();
    // SAFETY: a live view.
    let window = unsafe { window_of(view) } as usize;
    // SAFETY: as above.
    unsafe { app::with_shared(delegate_of(view), |s| s.set_marked(window, units)) };
    let (text, cursor) = keys::marked_preedit(&text, selected);
    // SAFETY: a live view.
    unsafe { record_preedit(view, text, cursor) };
}

/// `unmarkText` — the composition is over: committed as it stands, or given
/// up on. Either way the pre-edit ends.
///
/// # Safety
///
/// Called only by the input method, on an instance of the class built below.
unsafe extern "C" fn unmark_text(view: Id, _cmd: Sel) {
    // SAFETY: a live view.
    if unsafe { end_marked(view) } {
        // SAFETY: a live view.
        unsafe { record_preedit(view, String::new(), None) };
    }
}

/// Forgets the marked text this view's window held, answering whether it held
/// any — which is whether a pre-edit may have been reported that now has to
/// end.
///
/// # Safety
///
/// `view` must be a live `NSView`.
unsafe fn end_marked(view: Id) -> bool {
    // SAFETY: a live view.
    let window = unsafe { window_of(view) } as usize;
    // SAFETY: as above.
    let was = unsafe { app::with_shared(delegate_of(view), |s| s.take_marked(window)) };
    was.unwrap_or(0) > 0
}

/// How much text this view's window holds marked.
///
/// # Safety
///
/// `view` must be a live `NSView`.
unsafe fn marked_of(view: Id) -> usize {
    // SAFETY: a live view.
    let window = unsafe { window_of(view) } as usize;
    // SAFETY: as above.
    unsafe { app::with_shared(delegate_of(view), |s| s.marked(window)) }.unwrap_or(0)
}

/// Records a pre-edit against the shell that owns this view's window — if
/// that window's field composes.
///
/// The gate is read here, at the moment the input method speaks, rather than
/// when the pump translates: a window with no field, or a sensitive one, still
/// composes dead keys through marked text, and what it composes is never
/// queued. The payload goes on [`Shared`](super::app::Shared) **before** the
/// marker, for the reason [`perform_drag_operation`] gives. Ending a pre-edit
/// nobody was shown is filtered later, by the shell's
/// [`Composing`](crate::preedit::Composing), as on Win32.
///
/// # Safety
///
/// `view` must be a live `NSView`.
unsafe fn record_preedit(view: Id, text: String, cursor: Option<usize>) {
    // SAFETY: a live view.
    let window = unsafe { window_of(view) };
    if window.is_null() {
        return;
    }
    // SAFETY: as above.
    let delegate = unsafe { delegate_of(view) };
    let stamp = app::with_shared(delegate, |shared| {
        if !keys::field_composes(shared.field_of(window as usize)) {
            return None;
        }
        shared.push_preedit(window as usize, text, cursor);
        // The keystroke's own timestamp, as `insertText:` uses.
        Some(shared.keying())
    })
    .flatten();
    let Some(seconds) = stamp else { return };
    app::record(
        delegate,
        RawEvent::Preedit {
            window: window as usize,
            seconds,
        },
    );
}

/// `hasMarkedText`.
///
/// # Safety
///
/// Called only by the input method, on an instance of the class built below.
unsafe extern "C" fn has_marked_text(view: Id, _cmd: Sel) -> ObjcBool {
    // SAFETY: a live view.
    if unsafe { marked_of(view) } > 0 {
        YES
    } else {
        ffi::NO
    }
}

/// `markedRange`.
///
/// # Safety
///
/// Called only by the input method, on an instance of the class built below.
unsafe extern "C" fn marked_range(view: Id, _cmd: Sel) -> NSRange {
    // SAFETY: a live view.
    match unsafe { marked_of(view) } {
        0 => NSRange::NONE,
        units => NSRange::new(0, units),
    }
}

/// `selectedRange` — always empty.
///
/// This backend has no document to select in; the seam delivers committed text
/// and nothing else. `NSNotFound` is the documented way to say so, and a length
/// of zero at location zero would instead claim an empty selection at the start
/// of a document that does not exist.
///
/// # Safety
///
/// Called only by the input method, on an instance of the class built below.
unsafe extern "C" fn selected_range(_view: Id, _cmd: Sel) -> NSRange {
    NSRange::NONE
}

/// `attributedSubstringForProposedRange:actualRange:` — always `nil`.
///
/// There is no document to read back, and `nil` is what the protocol documents
/// for a client that cannot answer.
///
/// # Safety
///
/// Called only by the input method, on an instance of the class built below.
unsafe extern "C" fn attributed_substring(
    _view: Id,
    _cmd: Sel,
    _range: NSRange,
    _actual: *mut NSRange,
) -> Id {
    ptr::null_mut()
}

/// `validAttributesForMarkedText` — an empty array.
///
/// The attributes a client is willing to have applied to its pre-edit, and this
/// one draws no pre-edit at all.
///
/// # Safety
///
/// Called only by the input method, on an instance of the class built below.
unsafe extern "C" fn valid_attributes(_view: Id, _cmd: Sel) -> Id {
    let Some(class) = ffi::class(c"NSArray") else {
        return ptr::null_mut();
    };
    // SAFETY: `+[NSArray array]` returns an autoreleased empty array, which is
    // valid for the pool `pump` holds.
    unsafe { ffi::msg(class, ffi::sel(c"array")) }
}

/// `firstRectForCharacterRange:actualRange:` — where the candidate window goes.
///
/// The area [`set_text_input_area`](crate::Shell::set_text_input_area) last
/// gave for this window, in AppKit screen coordinates: points, Y up from the
/// bottom-left of the primary display. [`geometry::text_area`] takes it from
/// the window's client pixels to the view's points, and the window converts
/// that to the screen — the view is the content view, at the window's origin,
/// which [`warp_pointer`](crate::Shell::warp_pointer) relies on too. Every
/// range is answered with the same rectangle, because the seam names a caret
/// and not a document to measure characters in; `actualRange` is left alone,
/// which the protocol allows.
///
/// With no area given — or one handed back with `None` — it is the window's
/// own origin, and the candidate list opens at the bottom-left of the window:
/// the one place this backend can name without a caret.
///
/// # Safety
///
/// Called only by the input method, on an instance of the class built below.
unsafe extern "C" fn first_rect(
    view: Id,
    _cmd: Sel,
    _range: NSRange,
    _actual: *mut NSRange,
) -> NSRect {
    // SAFETY: a live view.
    let window = unsafe { window_of(view) };
    if window.is_null() {
        return NSRect::default();
    }
    // SAFETY: as above.
    let area = unsafe { app::with_shared(delegate_of(view), |s| s.area_of(window as usize)) };
    let local = match area.flatten() {
        // SAFETY: `bounds` and `backingScaleFactor` are accessors of the live
        // view and its live window, answering an `NSRect` and a `CGFloat`.
        Some(area) => unsafe {
            let bounds = ffi::msg_rect(view, ffi::sel(c"bounds"));
            let scale = ffi::msg_f64(window, ffi::sel(c"backingScaleFactor"));
            geometry::text_area(area, bounds.size.height, scale)
        },
        None => NSRect::new(0.0, 0.0, 0.0, 0.0),
    };
    // SAFETY: `convertRectToScreen:` takes and returns an `NSRect`, which is 32
    // bytes and therefore the one shape that needs the struct-return
    // trampoline on x86_64.
    unsafe {
        let convert: unsafe extern "C" fn(Id, Sel, NSRect) -> NSRect = ffi::msg_send_stret();
        convert(window, ffi::sel(c"convertRectToScreen:"), local)
    }
}

/// `characterIndexForPoint:` — always zero.
///
/// Asked when the user clicks inside a pre-edit. There is no document to index
/// into, and zero is the only in-range answer.
///
/// # Safety
///
/// Called only by the input method, on an instance of the class built below.
unsafe extern "C" fn character_index_for_point(
    _view: Id,
    _cmd: Sel,
    _point: NSPoint,
) -> NSUInteger {
    0
}

// ---------------------------------------------------------------------------
// Building the class
// ---------------------------------------------------------------------------

/// An `IMP` from the shape most of these methods have: `(id, SEL, NSEvent *)`.
///
/// One transmute site for a dozen callbacks rather than a dozen of them. Each
/// still names its own encoding in the table below, which is the part a wrong
/// answer would be invisible in.
fn event_imp(implementation: unsafe extern "C" fn(Id, Sel, Id)) -> Imp {
    // SAFETY: casting a correctly-typed `extern "C"` function to the runtime's
    // opaque `IMP`, which is how every method is installed; the encoding beside
    // it in the table describes the real signature.
    unsafe { core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Id), Imp>(implementation) }
}

/// An `IMP` from the `(id, SEL) -> BOOL` shape.
fn bool_imp(implementation: unsafe extern "C" fn(Id, Sel) -> ObjcBool) -> Imp {
    // SAFETY: as [`event_imp`].
    unsafe {
        core::mem::transmute::<unsafe extern "C" fn(Id, Sel) -> ObjcBool, Imp>(implementation)
    }
}

/// An `IMP` from the `(id, SEL, id) -> BOOL` shape — the first-mouse answer and
/// the two halves of a drop's acceptance.
fn bool_event_imp(implementation: unsafe extern "C" fn(Id, Sel, Id) -> ObjcBool) -> Imp {
    // SAFETY: as [`event_imp`].
    unsafe {
        core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Id) -> ObjcBool, Imp>(implementation)
    }
}

/// An `IMP` from the `(id, SEL, id) -> NSDragOperation` shape.
fn drag_imp(implementation: unsafe extern "C" fn(Id, Sel, Id) -> NSUInteger) -> Imp {
    // SAFETY: as [`event_imp`].
    unsafe {
        core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Id) -> NSUInteger, Imp>(implementation)
    }
}

/// An `IMP` from the `(id, SEL) -> NSRange` shape.
fn range_imp(implementation: unsafe extern "C" fn(Id, Sel) -> NSRange) -> Imp {
    // SAFETY: as [`event_imp`].
    unsafe { core::mem::transmute::<unsafe extern "C" fn(Id, Sel) -> NSRange, Imp>(implementation) }
}

/// Builds — or returns — the `NSView` subclass.
///
/// Same shape and same reasoning as [`app`]'s delegate class and
/// [`window`](super::window)'s window class, including the already-registered
/// fallback: a null from `objc_allocateClassPair` means the same code is loaded
/// twice in one process, and the class that is already there is ours by name
/// and by method table.
///
/// # Errors
///
/// [`ShellError::Connect`](crate::ShellError::Connect) if `NSView` is not in this
/// image or the runtime refused a method.
pub(super) fn view_class() -> Result<Class, crate::ShellError> {
    let outcome = VIEW.get_or_init(|| {
        let Some(superclass) = ffi::class(c"NSView") else {
            return Err(
                "the Objective-C runtime has no NSView, so this image has no AppKit in it"
                    .to_string(),
            );
        };
        // SAFETY: `NSView` is a live class, the name is a NUL-terminated
        // literal, and no extra instance storage is asked for — the
        // view-to-shell mapping goes through `[[self window] delegate]`, see
        // `delegate_of`.
        let class = unsafe { ffi::objc_allocateClassPair(superclass, VIEW_CLASS.as_ptr(), 0) };
        if class.is_null() {
            let Some(existing) = ffi::class(VIEW_CLASS) else {
                return Err(
                    "objc_allocateClassPair refused CrcblView and no class of that name exists"
                        .to_string(),
                );
            };
            crcbl_core::log::debug!(
                "the {VIEW_CLASS:?} class was already registered in this process"
            );
            return Ok(existing as usize);
        }
        install(class)?;
        // SAFETY: a complete class pair that has not been registered.
        unsafe { ffi::objc_registerClassPair(class) };
        Ok(class as usize)
    });
    match outcome {
        Ok(class) => Ok(*class as Class),
        Err(detail) => Err(crate::ShellError::Connect {
            backend: crate::ShellBackend::AppKit,
            detail: detail.clone(),
        }),
    }
}

/// Adds every method and the protocol conformance to an unregistered class.
///
/// Split from [`view_class`] because the table is long enough to bury the
/// registration it sits inside, and because the encodings are the part worth
/// reading on their own: the runtime reads each one to build an `NSInvocation`
/// if anything ever forwards the method, and a wrong return width there is a
/// wrong-sized read nothing in this crate would see and an input method might.
fn install(class: Class) -> Result<(), String> {
    let void_event = c"v@:@";
    let bool_nothing = std::ffi::CString::new(format!("{}@:", ffi::ENC_BOOL))
        .expect("the encoding characters contain no NUL");
    let bool_event = std::ffi::CString::new(format!("{}@:@", ffi::ENC_BOOL))
        .expect("the encoding characters contain no NUL");
    let range_nothing = std::ffi::CString::new(format!("{}@:", ffi::ENC_RANGE))
        .expect("the encoding characters contain no NUL");
    let insert = std::ffi::CString::new(format!("v@:@{}", ffi::ENC_RANGE))
        .expect("the encoding characters contain no NUL");
    let set_marked = std::ffi::CString::new(format!("v@:@{0}{0}", ffi::ENC_RANGE))
        .expect("the encoding characters contain no NUL");
    let substring = std::ffi::CString::new(format!("@@:{0}^{0}", ffi::ENC_RANGE))
        .expect("the encoding characters contain no NUL");
    let first_rect_enc =
        std::ffi::CString::new(format!("{}@:{1}^{1}", ffi::ENC_RECT, ffi::ENC_RANGE))
            .expect("the encoding characters contain no NUL");
    let index_for_point = std::ffi::CString::new(format!("Q@:{}", ffi::ENC_POINT))
        .expect("the encoding characters contain no NUL");
    // `NSDragOperation` is an `NSUInteger`, which encodes as `Q` on a 64-bit
    // target — the same character `characterIndexForPoint:` returns above, and
    // the reason both are spelled rather than shared: they are the same width
    // for different reasons and would change independently.
    let drag_event = c"Q@:@";

    // Every entry is `(selector, implementation, type encoding)`.
    let methods: [(&CStr, Imp, &CStr); 26] = [
        (
            c"acceptsFirstResponder",
            bool_imp(accepts_first_responder),
            bool_nothing.as_c_str(),
        ),
        (
            c"acceptsFirstMouse:",
            bool_event_imp(accepts_first_mouse),
            bool_event.as_c_str(),
        ),
        (c"keyDown:", event_imp(key_down), void_event),
        (c"keyUp:", event_imp(key_up), void_event),
        (c"flagsChanged:", event_imp(flags_changed), void_event),
        // The four motion methods, because a held button turns `mouseMoved:`
        // into one of the three drags and a backend with only the first loses
        // every sample of every drag.
        (c"mouseMoved:", event_imp(mouse_moved), void_event),
        (c"mouseDragged:", event_imp(mouse_moved), void_event),
        (c"rightMouseDragged:", event_imp(mouse_moved), void_event),
        (c"otherMouseDragged:", event_imp(mouse_moved), void_event),
        // The six button methods, which `buttonNumber` already tells apart.
        (c"mouseDown:", event_imp(mouse_down), void_event),
        (c"mouseUp:", event_imp(mouse_up), void_event),
        (c"rightMouseDown:", event_imp(mouse_down), void_event),
        (c"rightMouseUp:", event_imp(mouse_up), void_event),
        (c"otherMouseDown:", event_imp(mouse_down), void_event),
        (c"otherMouseUp:", event_imp(mouse_up), void_event),
        (c"scrollWheel:", event_imp(scroll_wheel), void_event),
        (c"mouseEntered:", event_imp(mouse_entered), void_event),
        (c"mouseExited:", event_imp(mouse_exited), void_event),
        (c"cursorUpdate:", event_imp(cursor_update), void_event),
        // `NSDraggingDestination`. Unreachable without the
        // `registerForDraggedTypes:` in `register_dragged_types` below, which
        // is what makes `WindowDesc::accept_drops` a gate the system enforces
        // rather than one this backend promises.
        (c"draggingEntered:", drag_imp(dragging_entered), drag_event),
        (c"draggingUpdated:", drag_imp(dragging_entered), drag_event),
        (
            c"prepareForDragOperation:",
            bool_event_imp(prepare_for_drag_operation),
            bool_event.as_c_str(),
        ),
        (
            c"performDragOperation:",
            bool_event_imp(perform_drag_operation),
            bool_event.as_c_str(),
        ),
        (
            c"insertText:replacementRange:",
            // SAFETY: as above.
            unsafe {
                core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Id, NSRange), Imp>(insert_text)
            },
            insert.as_c_str(),
        ),
        (
            c"doCommandBySelector:",
            // SAFETY: as above. A `SEL` argument encodes as `:`.
            unsafe {
                core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Sel), Imp>(
                    do_command_by_selector,
                )
            },
            c"v@::",
        ),
        (
            c"setMarkedText:selectedRange:replacementRange:",
            // SAFETY: as above.
            unsafe {
                core::mem::transmute::<unsafe extern "C" fn(Id, Sel, Id, NSRange, NSRange), Imp>(
                    set_marked_text,
                )
            },
            set_marked.as_c_str(),
        ),
    ];
    for (name, imp, types) in methods {
        add(class, name, imp, types)?;
    }

    // The rest of `NSTextInputClient`, whose signatures are each their own
    // shape and so are added one at a time.
    add(
        class,
        c"unmarkText",
        // SAFETY: as above.
        unsafe { core::mem::transmute::<unsafe extern "C" fn(Id, Sel), Imp>(unmark_text) },
        c"v@:",
    )?;
    add(
        class,
        c"hasMarkedText",
        bool_imp(has_marked_text),
        bool_nothing.as_c_str(),
    )?;
    add(
        class,
        c"markedRange",
        range_imp(marked_range),
        range_nothing.as_c_str(),
    )?;
    add(
        class,
        c"selectedRange",
        range_imp(selected_range),
        range_nothing.as_c_str(),
    )?;
    add(
        class,
        c"attributedSubstringForProposedRange:actualRange:",
        // SAFETY: as above.
        unsafe {
            core::mem::transmute::<unsafe extern "C" fn(Id, Sel, NSRange, *mut NSRange) -> Id, Imp>(
                attributed_substring,
            )
        },
        substring.as_c_str(),
    )?;
    add(
        class,
        c"validAttributesForMarkedText",
        // SAFETY: as above.
        unsafe {
            core::mem::transmute::<unsafe extern "C" fn(Id, Sel) -> Id, Imp>(valid_attributes)
        },
        c"@@:",
    )?;
    add(
        class,
        c"firstRectForCharacterRange:actualRange:",
        // SAFETY: as above. `NSRect` is the one return here the x86_64 ABI
        // hands back through a hidden pointer, which is a property of the
        // function's own signature and needs nothing special at this end.
        unsafe {
            core::mem::transmute::<
                unsafe extern "C" fn(Id, Sel, NSRange, *mut NSRange) -> NSRect,
                Imp,
            >(first_rect)
        },
        first_rect_enc.as_c_str(),
    )?;
    add(
        class,
        c"characterIndexForPoint:",
        // SAFETY: as above.
        unsafe {
            core::mem::transmute::<unsafe extern "C" fn(Id, Sel, NSPoint) -> NSUInteger, Imp>(
                character_index_for_point,
            )
        },
        index_for_point.as_c_str(),
    )?;

    // **Conformance is what makes `inputContext` non-nil**, and therefore what
    // makes `interpretKeyEvents:` reach an input method at all. Unlike the
    // window delegate's protocol — where declaring it is politeness — this one
    // is load-bearing: `-[NSResponder inputContext]` answers nil for a responder
    // that does not claim `NSTextInputClient`, and a nil input context means no
    // text, ever, with nothing anywhere reporting why.
    // SAFETY: the protocol is a linker-level object that may legitimately be
    // absent, which `objc_getProtocol` reports as null rather than crashing on.
    let protocol = unsafe { ffi::objc_getProtocol(c"NSTextInputClient".as_ptr()) };
    if protocol.is_null() {
        return Err(
            "the Objective-C runtime has no NSTextInputClient protocol, so no input method \
             could reach this view and text input would be silently dead"
                .to_string(),
        );
    }
    // SAFETY: `class` is still unregistered and `protocol` is live.
    if unsafe { ffi::class_addProtocol(class, protocol) } == ffi::NO {
        return Err("class_addProtocol refused NSTextInputClient on CrcblView".to_string());
    }
    Ok(())
}

/// One `class_addMethod`, with the refusal named.
fn add(class: Class, name: &CStr, imp: Imp, types: &CStr) -> Result<(), String> {
    // SAFETY: `class` is the class pair being built and has not been registered
    // yet, which is when methods may be added; the implementation matches the
    // encoding beside it at the call site.
    if unsafe { ffi::class_addMethod(class, ffi::sel(name), imp, types.as_ptr()) } == ffi::NO {
        return Err(format!("class_addMethod refused {name:?} on CrcblView"));
    }
    Ok(())
}

/// Attaches the tracking area that makes enter, exit and cursor updates exist.
///
/// [`IN_VISIBLE_RECT`](ffi::tracking::IN_VISIBLE_RECT) is why this is called
/// once, at creation, and never again: AppKit keeps the area glued to the view's
/// visible rectangle, so a resize needs no re-registration. Win32's equivalent —
/// `TrackMouseEvent` — is a **one-shot** request that has to be re-armed after
/// every leave, which is the same fact costing more.
///
/// The rectangle passed in is ignored for that reason, and is zero to say so.
///
/// # Safety
///
/// `view` must be a live `NSView`, on the main thread, with an autorelease pool
/// in scope.
pub(super) unsafe fn add_tracking_area(view: Id) {
    let Some(class) = ffi::class(c"NSTrackingArea") else {
        crcbl_core::log::warn!(
            "the Objective-C runtime has no NSTrackingArea; pointer enter and leave will not \
             be reported for this window"
        );
        return;
    };
    // SAFETY: `alloc` then the designated initialiser; the owner is the view,
    // which implements `mouseEntered:`, `mouseExited:` and `cursorUpdate:`, and
    // a nil `userInfo` is documented.
    unsafe {
        let allocated = ffi::msg(class, ffi::sel(c"alloc"));
        let init: unsafe extern "C" fn(Id, Sel, NSRect, NSUInteger, Id, Id) -> Id = ffi::msg_send();
        let area = init(
            allocated,
            ffi::sel(c"initWithRect:options:owner:userInfo:"),
            NSRect::new(0.0, 0.0, 0.0, 0.0),
            ffi::tracking::OPTIONS,
            view,
            ptr::null_mut(),
        );
        if area.is_null() {
            crcbl_core::log::warn!(
                "-[NSTrackingArea initWithRect:options:owner:userInfo:] returned nil"
            );
            return;
        }
        ffi::msg1_void(view, ffi::sel(c"addTrackingArea:"), area);
        // The view retains it, so this releases the one `alloc` gave us.
        ffi::msg_void(area, ffi::sel(c"release"));
    }
}

/// Turns a view's file drops on, which is the whole of
/// [`WindowDesc::accept_drops`](crate::WindowDesc::accept_drops) here.
///
/// **The gate is the system's.** No `NSDraggingDestination` message is
/// delivered to a view that has not registered — the same strength of gate as
/// Win32's `WS_EX_ACCEPTFILES`, and stronger than Wayland's, where the backend
/// has to record the refusal itself and answer `accept(null)`. The shell checks
/// the descriptor's flag again when it translates a drop, for the case the
/// system cannot cover: registration is a property of a *view*, and anything in
/// the process could register ours.
///
/// Only [`FILE_URL`](super::pasteboard::FILE_URL) is asked for, because that is
/// the only type this backend can turn into a
/// [`DroppedFile`](crate::ShellEvent::DroppedFile). Registering for more would
/// mean accepting drags this view then refuses at `performDragOperation:`,
/// which shows the user a copy cursor over something that will not drop.
///
/// # Safety
///
/// `view` must be a live `NSView`, on the main thread, with an autorelease pool
/// in scope.
pub(super) unsafe fn register_dragged_types(view: Id) {
    let Some(class) = ffi::class(c"NSArray") else {
        crcbl_core::log::warn!(
            "the Objective-C runtime has no NSArray; this window cannot register for file \
             drops and will receive none"
        );
        return;
    };
    // SAFETY: an autoreleased string, valid for the caller's pool. `None` is a
    // type identifier with an interior NUL, which this constant is not.
    let Some(kind) = (unsafe { ffi::nsstring(pasteboard::FILE_URL) }) else {
        crcbl_core::log::warn!("the dragged-type identifier could not be made into an NSString");
        return;
    };
    // SAFETY: `arrayWithObject:` returns an autoreleased array holding the type,
    // and `registerForDraggedTypes:` copies the list it is given.
    unsafe {
        let types = ffi::msg1(class, ffi::sel(c"arrayWithObject:"), kind);
        ffi::msg1_void(view, ffi::sel(c"registerForDraggedTypes:"), types);
    }
}

/// The view's `NSTextInputContext`, or null.
///
/// Non-nil because the class conforms to `NSTextInputClient` — see
/// [`install`] — and kept by the view once made, so the restriction
/// [`allow_input_methods`] sets on it lasts as long as the view.
///
/// # Safety
///
/// `view` must be a live `NSView`, on the main thread.
unsafe fn input_context(view: Id) -> Id {
    // SAFETY: an `NSResponder` accessor answering the view's own context.
    unsafe { ffi::msg(view, ffi::sel(c"inputContext")) }
}

/// Lets every input method compose in this view, or only the keyboard layouts
/// — the switch the [module docs](self) argue for.
///
/// Off, `allowedInputSourceLocales` is
/// [`NSAllRomanInputSourcesLocaleIdentifier`](ffi::NSAllRomanInputSourcesLocaleIdentifier)
/// alone, and whatever an input method was composing is discarded first, so
/// a composition cannot outlive the field that started it. On, it is nil,
/// which is the documented "every input source".
///
/// # Safety
///
/// `view` must be a live `NSView` of the class built here, on the main thread,
/// with an autorelease pool in scope.
pub(super) unsafe fn allow_input_methods(view: Id, allowed: bool) {
    // SAFETY: the caller's obligations, forwarded.
    let context = unsafe { input_context(view) };
    if context.is_null() {
        crcbl_core::log::warn!(
            "this view has no input context, so its input method cannot be switched for a text \
             field"
        );
        return;
    }
    let locales = if allowed {
        ptr::null_mut()
    } else {
        let Some(class) = ffi::class(c"NSArray") else {
            return;
        };
        // SAFETY: `discardMarkedText` takes nothing, and on a context with
        // nothing marked does nothing. `arrayWithObject:` answers an
        // autoreleased array holding AppKit's own constant string, valid for
        // the caller's pool.
        unsafe {
            ffi::msg_void(context, ffi::sel(c"discardMarkedText"));
            ffi::msg1(
                class,
                ffi::sel(c"arrayWithObject:"),
                ffi::NSAllRomanInputSourcesLocaleIdentifier,
            )
        }
    };
    // SAFETY: the setter copies the array it is given, and nil is documented.
    unsafe { ffi::msg1_void(context, ffi::sel(c"setAllowedInputSourceLocales:"), locales) };
}

/// Tells the input method that the caret area moved, so it asks
/// `firstRectForCharacterRange:actualRange:` again rather than leaving its
/// windows where the last answer put them.
///
/// # Safety
///
/// `view` must be a live `NSView` of the class built here, on the main thread.
pub(super) unsafe fn invalidate_caret(view: Id) {
    // SAFETY: the caller's obligations, forwarded.
    let context = unsafe { input_context(view) };
    if context.is_null() {
        return;
    }
    // SAFETY: an `NSTextInputContext` method taking nothing.
    unsafe { ffi::msg_void(context, ffi::sel(c"invalidateCharacterCoordinates")) };
}

/// What can be checked about this class without a window.
///
/// **The class is built by the same function the real path calls**, which is
/// what makes this more than a restatement of the table: `objc_allocateClassPair`
/// and `class_addMethod` are runtime calls with no AppKit in them, so a spawned
/// `libtest` thread may make them even though it may never own a window. Two
/// silent failure modes are reachable here and nowhere else:
///
/// * a method the runtime **refused** — a duplicate selector, a malformed
///   encoding — which `view_class` turns into an error nobody would otherwise
///   see until a keystroke went missing;
/// * a selector spelled wrong, which installs a method AppKit will never call
///   and leaves the real one inherited from `NSView`.
///
/// What it cannot check is whether an encoding *describes* its implementation.
/// The runtime reads those only when it forwards a method through an
/// `NSInvocation`, which nothing in this crate does and an input method might;
/// `docs/backlog.md` records it as a gap rather than pretending otherwise.
#[cfg(all(test, target_os = "macos"))]
mod tests {
    use super::*;

    #[test]
    fn the_view_class_installs_every_method_input_arrives_through() {
        let class = view_class().expect("CrcblView is built from NSObject-level runtime calls");

        // The responder half. A missing one here is a whole category of input
        // that silently does not exist — the four drag and motion methods most
        // of all, because `mouseMoved:` alone covers up its own absence for as
        // long as the player is holding a button.
        for selector in [
            c"acceptsFirstResponder",
            c"acceptsFirstMouse:",
            c"keyDown:",
            c"keyUp:",
            c"flagsChanged:",
            c"mouseMoved:",
            c"mouseDragged:",
            c"rightMouseDragged:",
            c"otherMouseDragged:",
            c"mouseDown:",
            c"mouseUp:",
            c"rightMouseDown:",
            c"rightMouseUp:",
            c"otherMouseDown:",
            c"otherMouseUp:",
            c"scrollWheel:",
            c"mouseEntered:",
            c"mouseExited:",
            c"cursorUpdate:",
            // `NSDraggingDestination`. A missing one here is a drop that is
            // refused, or one that is accepted and never delivered — and the
            // registration in `register_dragged_types` would still succeed,
            // because registering says nothing about what the view implements.
            c"draggingEntered:",
            c"draggingUpdated:",
            c"prepareForDragOperation:",
            c"performDragOperation:",
        ] {
            // SAFETY: a class is an object, and every object answers
            // `instancesRespondToSelector:`.
            let installed = unsafe {
                let send: unsafe extern "C" fn(Id, Sel, Sel) -> ObjcBool = ffi::msg_send();
                send(
                    class,
                    ffi::sel(c"instancesRespondToSelector:"),
                    ffi::sel(selector),
                ) != ffi::NO
            };
            assert!(installed, "CrcblView does not implement {selector:?}");
        }
    }

    #[test]
    fn the_view_class_is_a_text_input_client_and_answers_every_required_method() {
        // Conformance is what makes `-[NSResponder inputContext]` non-nil, and a
        // nil input context means `interpretKeyEvents:` reaches no input method
        // and **no text is ever committed** — with nothing anywhere saying so.
        // That is this platform's `TranslateMessage`, and this is the closest a
        // window-less test can get to it.
        let class = view_class().expect("CrcblView is built");
        // SAFETY: the protocol is a linker-level object that may be absent,
        // which `objc_getProtocol` reports as null.
        let protocol = unsafe { ffi::objc_getProtocol(c"NSTextInputClient".as_ptr()) };
        assert!(!protocol.is_null(), "this AppKit has no NSTextInputClient");
        // SAFETY: a class is an object; `conformsToProtocol:` is on `NSObject`.
        let conforms = unsafe {
            let send: unsafe extern "C" fn(Id, Sel, Id) -> ObjcBool = ffi::msg_send();
            send(class, ffi::sel(c"conformsToProtocol:"), protocol) != ffi::NO
        };
        assert!(conforms, "CrcblView does not claim NSTextInputClient");

        for selector in [
            c"insertText:replacementRange:",
            c"doCommandBySelector:",
            c"setMarkedText:selectedRange:replacementRange:",
            c"unmarkText",
            c"selectedRange",
            c"markedRange",
            c"hasMarkedText",
            c"attributedSubstringForProposedRange:actualRange:",
            c"validAttributesForMarkedText",
            c"firstRectForCharacterRange:actualRange:",
            c"characterIndexForPoint:",
        ] {
            // SAFETY: as above.
            let installed = unsafe {
                let send: unsafe extern "C" fn(Id, Sel, Sel) -> ObjcBool = ffi::msg_send();
                send(
                    class,
                    ffi::sel(c"instancesRespondToSelector:"),
                    ffi::sel(selector),
                ) != ffi::NO
            };
            assert!(
                installed,
                "NSTextInputClient requires {selector:?} and CrcblView does not have it"
            );
        }
    }

    #[test]
    fn the_class_is_built_once_and_answered_again_afterwards() {
        // `objc_allocateClassPair` returns null for a name that already exists,
        // which is what a second copy of this code in one process looks like.
        // The `OnceLock` and the fallback are what make that a shared class
        // rather than a failure, and asking twice is how that is visible.
        let first = view_class().expect("built");
        let second = view_class().expect("built again");
        assert_eq!(first, second);
        assert_eq!(
            ffi::class(VIEW_CLASS).expect("registered"),
            first,
            "the registered class is the one this module hands out"
        );
    }
}