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.
- package/LICENSE +21 -0
- package/cli/runMacos.js +33 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +298 -0
- package/native/appkit/AppKitAnimationChoreographer.h +54 -0
- package/native/appkit/AppKitAnimationChoreographer.mm +125 -0
- package/native/appkit/AppKitDropTarget.h +34 -0
- package/native/appkit/AppKitDropTarget.mm +292 -0
- package/native/appkit/AppKitFocus.h +120 -0
- package/native/appkit/AppKitFocus.mm +293 -0
- package/native/appkit/AppKitImageLoader.h +80 -0
- package/native/appkit/AppKitImageLoader.mm +156 -0
- package/native/appkit/AppKitKeyEvents.h +26 -0
- package/native/appkit/AppKitKeyEvents.mm +97 -0
- package/native/appkit/AppKitMenuBar.mm +424 -0
- package/native/appkit/AppKitMountingManager.h +135 -0
- package/native/appkit/AppKitMountingManager.mm +946 -0
- package/native/appkit/AppKitRunLoopObserver.h +34 -0
- package/native/appkit/AppKitRunLoopObserver.mm +42 -0
- package/native/appkit/AppKitScrollView.h +168 -0
- package/native/appkit/AppKitScrollView.mm +550 -0
- package/native/appkit/AppKitSkiaContext.h +33 -0
- package/native/appkit/AppKitSkiaContext.mm +255 -0
- package/native/appkit/AppKitSkiaModule.h +53 -0
- package/native/appkit/AppKitSkiaModule.mm +81 -0
- package/native/appkit/AppKitSkiaPeer.h +37 -0
- package/native/appkit/AppKitSkiaPeer.mm +123 -0
- package/native/appkit/AppKitSkiaView.h +62 -0
- package/native/appkit/AppKitSnapshot.h +21 -0
- package/native/appkit/AppKitSnapshot.mm +91 -0
- package/native/appkit/AppKitTextInput.h +140 -0
- package/native/appkit/AppKitTextInput.mm +588 -0
- package/native/appkit/AppKitTextPeer.h +74 -0
- package/native/appkit/AppKitTextPeer.mm +353 -0
- package/native/appkit/AppKitTitleBar.h +90 -0
- package/native/appkit/AppKitTitleBar.mm +186 -0
- package/native/appkit/AppKitTouchDispatcher.h +138 -0
- package/native/appkit/AppKitTouchDispatcher.mm +514 -0
- package/native/appkit/AppKitWindowControl.mm +222 -0
- package/native/appkit/AppKitWindowModule.h +62 -0
- package/native/appkit/AppKitWindowModule.mm +194 -0
- package/native/appkit/ColorSchemeAppKit.mm +71 -0
- package/native/appkit/ComponentRegistryAppKit.mm +121 -0
- package/native/appkit/CoreTextLayout.h +43 -0
- package/native/appkit/CoreTextLayout.mm +252 -0
- package/native/appkit/CoreTextLayoutManager.mm +138 -0
- package/native/appkit/FontRegistryCoreText.mm +148 -0
- package/native/appkit/PlatformServicesAppKit.mm +540 -0
- package/native/appkit/RnAppKitView.h +417 -0
- package/native/appkit/RnAppKitView.mm +1605 -0
- package/native/appkit/RnTextLayout.h +38 -0
- package/native/appkit/RnTextLayout.mm +168 -0
- package/native/appkit/demo_layout_appkit.mm +93 -0
- package/native/appkit/main_appkit.mm +1860 -0
- package/native/appkit/mount_harness_appkit.mm +197 -0
- package/native/tests/test_appkit_accessibility.mm +170 -0
- package/native/tests/test_appkit_image.mm +237 -0
- package/native/tests/test_appkit_input.mm +852 -0
- package/native/tests/test_appkit_key_events.mm +150 -0
- package/native/tests/test_appkit_mounting.mm +430 -0
- package/native/tests/test_appkit_scrollbar.mm +132 -0
- package/native/tests/test_appkit_services.mm +57 -0
- package/native/tests/test_appkit_skia.mm +212 -0
- package/native/tests/test_appkit_text.mm +180 -0
- package/native/tests/test_appkit_textinput.mm +748 -0
- package/native/tests/test_appkit_titlebar.mm +107 -0
- package/native/tests/test_appkit_view.mm +237 -0
- package/package.json +26 -0
- 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
|