basalt-appkit 0.0.0-rc5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runMacos.js +33 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +298 -0
  5. package/native/appkit/AppKitAnimationChoreographer.h +54 -0
  6. package/native/appkit/AppKitAnimationChoreographer.mm +125 -0
  7. package/native/appkit/AppKitDropTarget.h +34 -0
  8. package/native/appkit/AppKitDropTarget.mm +292 -0
  9. package/native/appkit/AppKitFocus.h +120 -0
  10. package/native/appkit/AppKitFocus.mm +293 -0
  11. package/native/appkit/AppKitImageLoader.h +80 -0
  12. package/native/appkit/AppKitImageLoader.mm +156 -0
  13. package/native/appkit/AppKitKeyEvents.h +26 -0
  14. package/native/appkit/AppKitKeyEvents.mm +97 -0
  15. package/native/appkit/AppKitMenuBar.mm +424 -0
  16. package/native/appkit/AppKitMountingManager.h +135 -0
  17. package/native/appkit/AppKitMountingManager.mm +946 -0
  18. package/native/appkit/AppKitRunLoopObserver.h +34 -0
  19. package/native/appkit/AppKitRunLoopObserver.mm +42 -0
  20. package/native/appkit/AppKitScrollView.h +168 -0
  21. package/native/appkit/AppKitScrollView.mm +550 -0
  22. package/native/appkit/AppKitSkiaContext.h +33 -0
  23. package/native/appkit/AppKitSkiaContext.mm +255 -0
  24. package/native/appkit/AppKitSkiaModule.h +53 -0
  25. package/native/appkit/AppKitSkiaModule.mm +81 -0
  26. package/native/appkit/AppKitSkiaPeer.h +37 -0
  27. package/native/appkit/AppKitSkiaPeer.mm +123 -0
  28. package/native/appkit/AppKitSkiaView.h +62 -0
  29. package/native/appkit/AppKitSnapshot.h +21 -0
  30. package/native/appkit/AppKitSnapshot.mm +91 -0
  31. package/native/appkit/AppKitTextInput.h +140 -0
  32. package/native/appkit/AppKitTextInput.mm +588 -0
  33. package/native/appkit/AppKitTextPeer.h +74 -0
  34. package/native/appkit/AppKitTextPeer.mm +353 -0
  35. package/native/appkit/AppKitTitleBar.h +90 -0
  36. package/native/appkit/AppKitTitleBar.mm +186 -0
  37. package/native/appkit/AppKitTouchDispatcher.h +138 -0
  38. package/native/appkit/AppKitTouchDispatcher.mm +514 -0
  39. package/native/appkit/AppKitWindowControl.mm +222 -0
  40. package/native/appkit/AppKitWindowModule.h +62 -0
  41. package/native/appkit/AppKitWindowModule.mm +194 -0
  42. package/native/appkit/ColorSchemeAppKit.mm +71 -0
  43. package/native/appkit/ComponentRegistryAppKit.mm +121 -0
  44. package/native/appkit/CoreTextLayout.h +43 -0
  45. package/native/appkit/CoreTextLayout.mm +252 -0
  46. package/native/appkit/CoreTextLayoutManager.mm +138 -0
  47. package/native/appkit/FontRegistryCoreText.mm +148 -0
  48. package/native/appkit/PlatformServicesAppKit.mm +540 -0
  49. package/native/appkit/RnAppKitView.h +417 -0
  50. package/native/appkit/RnAppKitView.mm +1605 -0
  51. package/native/appkit/RnTextLayout.h +38 -0
  52. package/native/appkit/RnTextLayout.mm +168 -0
  53. package/native/appkit/demo_layout_appkit.mm +93 -0
  54. package/native/appkit/main_appkit.mm +1860 -0
  55. package/native/appkit/mount_harness_appkit.mm +197 -0
  56. package/native/tests/test_appkit_accessibility.mm +170 -0
  57. package/native/tests/test_appkit_image.mm +237 -0
  58. package/native/tests/test_appkit_input.mm +852 -0
  59. package/native/tests/test_appkit_key_events.mm +150 -0
  60. package/native/tests/test_appkit_mounting.mm +430 -0
  61. package/native/tests/test_appkit_scrollbar.mm +132 -0
  62. package/native/tests/test_appkit_services.mm +57 -0
  63. package/native/tests/test_appkit_skia.mm +212 -0
  64. package/native/tests/test_appkit_text.mm +180 -0
  65. package/native/tests/test_appkit_textinput.mm +748 -0
  66. package/native/tests/test_appkit_titlebar.mm +107 -0
  67. package/native/tests/test_appkit_view.mm +237 -0
  68. package/package.json +26 -0
  69. package/react-native.config.js +22 -0
@@ -0,0 +1,91 @@
1
+ #import "AppKitSnapshot.h"
2
+
3
+ #import <QuartzCore/QuartzCore.h>
4
+
5
+ #include <cstdio>
6
+
7
+ // Two things here are not obvious and cost an afternoon each if guessed at.
8
+ //
9
+ // The hierarchy needs a window, and the window needs to have displayed once.
10
+ // AppKit does not attach a subview's layer to its superview's layer when the
11
+ // subview is added -- it assembles the layer tree during a display cycle, and a
12
+ // view with no window never has one. Without this the snapshot comes out as the
13
+ // root's background colour and nothing else, which reads as "the children did
14
+ // not paint" rather than "the children are not in the layer tree". The window is
15
+ // never ordered front, so nothing appears on screen.
16
+ //
17
+ // The render goes through the layer tree rather than `cacheDisplayInRect:`,
18
+ // because these views draw nothing themselves: every prop the view layer
19
+ // supports is a CALayer property, so `renderInContext:` is what actually
20
+ // exercises the code under test.
21
+ bool RnAppKitWriteSnapshot(RnAppKitView *root, NSString *path) {
22
+ // AppKit has to be woken up before it will render: a view outside a running
23
+ // application draws nothing, and does so silently.
24
+ [NSApplication sharedApplication];
25
+
26
+ const NSRect bounds = root.bounds;
27
+ const NSInteger width = (NSInteger)bounds.size.width;
28
+ const NSInteger height = (NSInteger)bounds.size.height;
29
+ if (width <= 0 || height <= 0) {
30
+ std::fputs("nothing to snapshot: the root has no size\n", stderr);
31
+ return false;
32
+ }
33
+
34
+ // A view that is already in a window has a layer tree; one that is not needs
35
+ // a window made for it. Checking rather than always making one matters: a
36
+ // live host's root is the window's contentView, and putting it into a second
37
+ // window would take it out of the first.
38
+ NSWindow *offscreen = nil;
39
+ if (root.window != nil) {
40
+ [root.window displayIfNeeded];
41
+ } else {
42
+ offscreen = [[NSWindow alloc] initWithContentRect:bounds
43
+ styleMask:NSWindowStyleMaskBorderless
44
+ backing:NSBackingStoreBuffered
45
+ defer:NO];
46
+ offscreen.contentView = root;
47
+ [offscreen display];
48
+ }
49
+
50
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
51
+ CGContextRef context = CGBitmapContextCreate(nullptr,
52
+ (size_t)width,
53
+ (size_t)height,
54
+ 8,
55
+ 0,
56
+ colorSpace,
57
+ kCGImageAlphaPremultipliedLast);
58
+ CGColorSpaceRelease(colorSpace);
59
+ if (context == nullptr) {
60
+ std::fputs("could not create a bitmap context\n", stderr);
61
+ return false;
62
+ }
63
+
64
+ // Core Graphics' origin is bottom-left and the layer tree's, having come from
65
+ // a flipped view, is top-left. Flipping the context puts them back in
66
+ // agreement; without it the image comes out mirrored vertically, which is the
67
+ // same class of bug `isFlipped` exists to prevent one level up.
68
+ CGContextTranslateCTM(context, 0, (CGFloat)height);
69
+ CGContextScaleCTM(context, 1, -1);
70
+
71
+ [root.layer renderInContext:context];
72
+
73
+ CGImageRef image = CGBitmapContextCreateImage(context);
74
+ CGContextRelease(context);
75
+ if (image == nullptr) {
76
+ return false;
77
+ }
78
+
79
+ NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithCGImage:image];
80
+ CGImageRelease(image);
81
+ NSData *png = [rep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
82
+
83
+ // The root is handed back, because a caller that snapshots twice needs it out
84
+ // of the window it was just put in -- a view has one superview, and leaving it
85
+ // parented here would make the second snapshot's window silently steal it.
86
+ if (offscreen != nil) {
87
+ offscreen.contentView = [[NSView alloc] initWithFrame:bounds];
88
+ }
89
+
90
+ return [png writeToFile:path atomically:YES];
91
+ }
@@ -0,0 +1,140 @@
1
+ // <TextInput> on AppKit.
2
+ //
3
+ // The native half is React Native's own *iOS* TextInput: its shadow node,
4
+ // props, state and event emitter are pure C++ and measure through a
5
+ // TextLayoutManager, which on this platform is the Core Text one. Android's
6
+ // variant includes fbjni and calls into a Java FabricUIManager, so it is
7
+ // unusable here; see docs/DECISIONS.md. The component name is therefore
8
+ // "TextInput", which is what src/overrides/TextInput.js asks for -- and the
9
+ // same name the GTK side answers to.
10
+ //
11
+ // The editing itself is a real NSTextField rather than a caret drawn on a
12
+ // paragraph. That brings input methods, selection, the clipboard, and every
13
+ // key binding a Mac user expects, none of which is worth reimplementing.
14
+ //
15
+ // The awkward part of a text field is not typing, it is that React Native's
16
+ // <TextInput> is a controlled component: JavaScript owns the value, and the
17
+ // widget must not fight it. Every change is reported to JavaScript, which
18
+ // re-renders and sends the text back down as a prop -- so applying that prop
19
+ // must not itself look like the user typing, or the two chase each other.
20
+ // `applying` is what breaks that loop, exactly as on GTK.
21
+
22
+ #pragma once
23
+
24
+ #import "RnAppKitView.h"
25
+
26
+ #include <react/renderer/components/iostextinput/TextInputShadowNode.h>
27
+ #include <react/renderer/components/textinput/TextInputEventEmitter.h>
28
+ #include <react/renderer/core/EventEmitter.h>
29
+ #include <react/renderer/mounting/ShadowView.h>
30
+
31
+ #include <functional>
32
+ #include <optional>
33
+ #include <string>
34
+ #include <unordered_map>
35
+
36
+ namespace basalt {
37
+
38
+ class AppKitTextInputManager {
39
+ public:
40
+ using EmitterLookup = std::function<facebook::react::EventEmitter::Shared(facebook::react::Tag)>;
41
+
42
+ explicit AppKitTextInputManager(EmitterLookup lookup);
43
+ ~AppKitTextInputManager();
44
+
45
+ AppKitTextInputManager(const AppKitTextInputManager &) = delete;
46
+ AppKitTextInputManager &operator=(const AppKitTextInputManager &) = delete;
47
+
48
+ // Called for every mutation touching a TextInput. Creates the field the first
49
+ // time, then applies props.
50
+ void update(RnAppKitView *view, const facebook::react::ShadowView &shadowView);
51
+
52
+ void remove(facebook::react::Tag tag);
53
+
54
+ // focus, blur, and setTextAndSelection. Returns false for anything else.
55
+ bool dispatchCommand(facebook::react::Tag tag,
56
+ const std::string &name,
57
+ const folly::dynamic &args);
58
+
59
+ // Called by the Objective-C delegate. Public because the trampoline has to
60
+ // reach them; not part of anything a caller would use.
61
+ void handleChanged(facebook::react::Tag tag);
62
+ void handleSubmit(facebook::react::Tag tag);
63
+ void handleFocus(facebook::react::Tag tag);
64
+ void handleBlur(facebook::react::Tag tag);
65
+
66
+ // The field editor whose selection moved. An NSTextField has no selection of
67
+ // its own -- editing is done by a shared NSTextView on loan from the window
68
+ // -- so this arrives as a notification about that view, and the tag it
69
+ // belongs to has to be found by asking which field is currently using it.
70
+ void handleSelectionChanged(void *editor);
71
+
72
+ // A key on its way to whichever field has focus. Returns true when the key
73
+ // belonged to one of ours, which is only used to decide whether to bother.
74
+ bool handleKeyDown(void *event);
75
+
76
+ private:
77
+ struct Entry {
78
+ // The selection as JavaScript last saw it, so one movement is one event.
79
+ facebook::react::AttributedString::Range lastReportedSelection{0, 0};
80
+
81
+ // The last `selection` prop seen, for the same reason lastPropText exists.
82
+ std::optional<facebook::react::Selection> lastPropSelection{};
83
+ RnAppKitView *view{nil};
84
+ // An NSTextField or an NSTextView; see AppKitTextPeer.h.
85
+ NSView *field{nil};
86
+ facebook::react::Tag tag{0};
87
+
88
+ // React Native counts events so it can ignore a prop update that is older
89
+ // than what the user has since typed. Every emitted metric carries it.
90
+ int eventCount{0};
91
+
92
+ // True while a prop is being pushed into the field, so the change it
93
+ // provokes is not reported back as the user typing.
94
+ bool applying{false};
95
+
96
+ bool secure{false};
97
+ // A multiline field is an NSTextView rather than an NSTextField, so a
98
+ // change here rebuilds the peer exactly as `secure` does.
99
+ bool multiline{false};
100
+ std::string lastReportedText;
101
+
102
+ // The last `text` prop actually seen, and whether one has been seen at all.
103
+ //
104
+ // This is what tells a *controlled* field from an uncontrolled one, which
105
+ // props alone cannot: React Native's TextInput.js sends
106
+ // `text={value ?? defaultValue}`, and an uncontrolled field with no default
107
+ // sends undefined, which arrives here as the empty string -- exactly what a
108
+ // controlled field that JavaScript has cleared sends. Applying it whenever
109
+ // it differs from the field therefore wipes an uncontrolled field the
110
+ // moment anything else in the tree re-renders, because React Native
111
+ // re-sends `mostRecentEventCount` on every change and that alone produces
112
+ // an Update mutation.
113
+ //
114
+ // So the prop is applied when it *changes*, not when it differs from the
115
+ // field. A controlled field's value changes as the user types; an
116
+ // uncontrolled one's never does. Found on Windows.
117
+ std::string lastPropText;
118
+ bool sawProps{false};
119
+ };
120
+
121
+ Entry *entryFor(facebook::react::Tag tag);
122
+
123
+ // Fills in the parts of Metrics every event carries.
124
+ facebook::react::TextInputEventEmitter::Metrics metricsFor(const Entry &entry) const;
125
+
126
+ std::shared_ptr<const facebook::react::TextInputEventEmitter> emitterFor(
127
+ facebook::react::Tag tag) const;
128
+
129
+ // Builds the field, or rebuilds it when secureTextEntry changed: a secure
130
+ // field is a different class on AppKit, not a property.
131
+ void makeField(Entry &entry, bool secure, bool multiline);
132
+
133
+ EmitterLookup lookup_;
134
+ std::unordered_map<facebook::react::Tag, Entry> entries_;
135
+ // The delegate every field points at. One for all of them: it dispatches by
136
+ // tag, so there is nothing per-field to keep in step.
137
+ id delegate_;
138
+ };
139
+
140
+ } // namespace basalt