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,80 @@
1
+ // Loading the pixels behind <Image>.
2
+ //
3
+ // React Native's cxx platform does not do this for us. `ImageManager` has a
4
+ // platform variant under `imagemanager/platform/cxx`, and every method in it is
5
+ // a stub that returns an empty `ImageRequest` -- so no `ImageResponse` ever
6
+ // arrives, and `ImageState` never carries anything to render. The platform view
7
+ // is expected to load its own image, which is also what Android does (Fresco,
8
+ // from `ReactImageView`, not from the shadow node).
9
+ //
10
+ // So the mounting manager reads the URI straight off `ImageProps::sources` and
11
+ // asks this to produce a CGImage.
12
+ //
13
+ // Which URIs work is in core/ImageBytes.h, shared with the GTK loader. What is
14
+ // here is the decode and the cache, which is the only part that differs --
15
+ // and one thing that genuinely differs beyond spelling: a CGImage is immutable
16
+ // and thread-safe, so decoding happens on the worker thread. The GTK side has
17
+ // to decode on the main thread, because a GdkTexture is a GObject.
18
+
19
+ #pragma once
20
+
21
+ #import <Cocoa/Cocoa.h>
22
+
23
+ #include <functional>
24
+ #include <string>
25
+ #include <unordered_map>
26
+
27
+ #include "ImageCache.h"
28
+
29
+ #include <react/io/IImageLoader.h>
30
+
31
+ namespace basalt {
32
+
33
+ // Also React Native's own `IImageLoader`, which is what `Image.getSize` and
34
+ // `Image.prefetch` reach. Upstream's `ImageLoaderModule` takes one and
35
+ // `ReactCxxTurboModuleProvider` never supplies it -- see
36
+ // docs/backlog/upstream.md -- so the host registers the module itself with
37
+ // this as the loader.
38
+ class AppKitImageLoader : public facebook::react::IImageLoader {
39
+ public:
40
+ // `image` is null when the load failed, and `error` says why. Always called
41
+ // on the main thread, possibly synchronously if the image is cached.
42
+ using Callback = std::function<void(CGImageRef image, const std::string &error)>;
43
+
44
+ AppKitImageLoader();
45
+ ~AppKitImageLoader();
46
+
47
+ AppKitImageLoader(const AppKitImageLoader &) = delete;
48
+ AppKitImageLoader &operator=(const AppKitImageLoader &) = delete;
49
+ AppKitImageLoader(AppKitImageLoader &&) = delete;
50
+ AppKitImageLoader &operator=(AppKitImageLoader &&) = delete;
51
+
52
+ void load(const std::string &uri, Callback &&callback);
53
+
54
+ // --- IImageLoader ----------------------------------------------------------
55
+ //
56
+ // The same decode as `load`, reporting the size rather than the pixels. It
57
+ // goes through the same cache, so `Image.getSize` on something already on
58
+ // screen answers without touching the disk or the network.
59
+ void loadImage(const std::string &uri,
60
+ const facebook::react::IImageLoaderOnLoadCallback &&onLoad) override;
61
+
62
+ facebook::react::IImageLoader::CacheStatus getCacheStatus(const std::string &uri) override;
63
+
64
+ private:
65
+ struct Pending;
66
+
67
+ // Decoded images, keyed by URI. CGImages are immutable and shareable, so two
68
+ // <Image>s with the same source draw the same object.
69
+ //
70
+ // Nothing evicts from this yet; see docs/BACKLOG.md.
71
+ // Caches an image and releases whatever the policy dropped.
72
+ void remember(const std::string &uri, CGImageRef image);
73
+
74
+ std::unordered_map<std::string, CGImageRef> cache_;
75
+ // Which URI goes next, and when. The images are this class's; the decision
76
+ // is shared with the other two hosts. See core/ImageCache.h.
77
+ basalt::ImageCachePolicy policy_;
78
+ };
79
+
80
+ } // namespace basalt
@@ -0,0 +1,156 @@
1
+ #import "AppKitImageLoader.h"
2
+
3
+ #include "ImageBytes.h"
4
+
5
+ #import <ImageIO/ImageIO.h>
6
+
7
+ #include <thread>
8
+ #include <utility>
9
+
10
+ namespace basalt {
11
+
12
+ // Carries one completed load from the worker thread to the main thread.
13
+ struct AppKitImageLoader::Pending {
14
+ AppKitImageLoader *loader;
15
+ std::string uri;
16
+ CGImageRef image;
17
+ std::string error;
18
+ Callback callback;
19
+ };
20
+
21
+ AppKitImageLoader::AppKitImageLoader() = default;
22
+
23
+ AppKitImageLoader::~AppKitImageLoader() {
24
+ for (auto &[uri, image] : cache_) {
25
+ (void)uri;
26
+ CGImageRelease(image);
27
+ }
28
+ cache_.clear();
29
+ }
30
+
31
+ namespace {
32
+
33
+ // ImageIO rather than NSImage. An NSImage is a list of representations at
34
+ // different sizes with a resolution attached, and asking one for a CGImage
35
+ // means telling it a size and a context -- so a 160x100 PNG can come back
36
+ // 320x200 on a Retina display, which then measures wrong. CGImageSource hands
37
+ // back exactly what is in the file.
38
+ CGImageRef decode(const std::string &bytes, std::string *error) {
39
+ if (bytes.empty()) {
40
+ *error = "no image data";
41
+ return nullptr;
42
+ }
43
+
44
+ CFDataRef data = CFDataCreate(kCFAllocatorDefault,
45
+ reinterpret_cast<const UInt8 *>(bytes.data()),
46
+ static_cast<CFIndex>(bytes.size()));
47
+ if (data == nullptr) {
48
+ *error = "could not wrap image data";
49
+ return nullptr;
50
+ }
51
+
52
+ CGImageSourceRef source = CGImageSourceCreateWithData(data, nullptr);
53
+ CFRelease(data);
54
+ if (source == nullptr) {
55
+ *error = "unrecognised image format";
56
+ return nullptr;
57
+ }
58
+
59
+ // Index 0: an animated GIF and a multi-page TIFF both have more, and drawing
60
+ // the first frame is the honest thing to do until there is an animator. See
61
+ // docs/BACKLOG.md.
62
+ CGImageRef image = CGImageSourceCreateImageAtIndex(source, 0, nullptr);
63
+ CFRelease(source);
64
+ if (image == nullptr) {
65
+ *error = "could not decode image";
66
+ return nullptr;
67
+ }
68
+ return image;
69
+ }
70
+
71
+ } // namespace
72
+
73
+ // Caches the image and releases whatever that pushed out. See core/ImageCache.h
74
+ // for the policy; the bytes are this file's to measure, because only Core
75
+ // Graphics knows how big a decoded image is.
76
+ void AppKitImageLoader::remember(const std::string &uri, CGImageRef image) {
77
+ cache_[uri] = image;
78
+
79
+ // Height times the row stride, which is what the decode actually allocated
80
+ // -- rows are padded, so width times four is an underestimate on some
81
+ // widths and this is not.
82
+ const size_t bytes = CGImageGetBytesPerRow(image) * CGImageGetHeight(image);
83
+
84
+ for (const std::string &evicted : policy_.insert(uri, bytes)) {
85
+ const auto it = cache_.find(evicted);
86
+ if (it != cache_.end()) {
87
+ CGImageRelease(it->second);
88
+ cache_.erase(it);
89
+ }
90
+ }
91
+ }
92
+
93
+ void AppKitImageLoader::load(const std::string &uri, Callback &&callback) {
94
+ if (uri.empty()) {
95
+ callback(nullptr, "empty source uri");
96
+ return;
97
+ }
98
+
99
+ if (const auto it = cache_.find(uri); it != cache_.end()) {
100
+ policy_.noteUse(uri);
101
+ callback(it->second, {});
102
+ return;
103
+ }
104
+
105
+ auto *pending = new Pending{this, uri, nullptr, {}, std::move(callback)};
106
+
107
+ std::thread([pending]() {
108
+ std::string bytes;
109
+ // Both the fetch and the decode run here. A CGImage is immutable and
110
+ // thread-safe, so unlike the GTK side there is nothing that has to wait for
111
+ // the main thread except the delivery itself.
112
+ if (fetchImageBytes(pending->uri, &bytes, &pending->error)) {
113
+ pending->image = decode(bytes, &pending->error);
114
+ }
115
+
116
+ dispatch_async_f(dispatch_get_main_queue(), pending, [](void *data) {
117
+ std::unique_ptr<Pending> done{static_cast<Pending *>(data)};
118
+ if (done->image != nullptr) {
119
+ // The cache takes the reference the decode produced; the callback
120
+ // borrows it, and a view that keeps the image retains its own.
121
+ done->loader->remember(done->uri, done->image);
122
+ }
123
+ done->callback(done->image, done->error);
124
+ });
125
+ }).detach();
126
+ }
127
+
128
+
129
+ void AppKitImageLoader::loadImage(const std::string &uri,
130
+ const facebook::react::IImageLoaderOnLoadCallback &&onLoad) {
131
+ // Copied because the callback outlives this call: `load` answers on a later
132
+ // turn of the main queue, and the reference it was handed is gone by then.
133
+ auto callback = onLoad;
134
+ load(uri, [callback](CGImageRef image, const std::string &error) {
135
+ if (image == nullptr) {
136
+ // The message rather than a size: `Image.getSize` rejects with it, and a
137
+ // zero-by-zero success would be indistinguishable from a 0x0 image.
138
+ callback(0.0, 0.0, error.empty() ? "could not load image" : error.c_str());
139
+ return;
140
+ }
141
+ callback(static_cast<double>(CGImageGetWidth(image)),
142
+ static_cast<double>(CGImageGetHeight(image)),
143
+ nullptr);
144
+ });
145
+ }
146
+
147
+ facebook::react::IImageLoader::CacheStatus AppKitImageLoader::getCacheStatus(
148
+ const std::string &uri) {
149
+ // Memory or nothing: there is no disk cache here, and saying `Disk` would be
150
+ // claiming a persistence this has not got.
151
+ return cache_.find(uri) != cache_.end()
152
+ ? facebook::react::IImageLoader::CacheStatus::Memory
153
+ : facebook::react::IImageLoader::CacheStatus::None;
154
+ }
155
+
156
+ } // namespace basalt
@@ -0,0 +1,26 @@
1
+ // An NSEvent as a key combination, in the vocabulary core/KeyEvents.h defines.
2
+ //
3
+ // Separate from AppKitFocus because it is a translation and nothing else: no
4
+ // registry, no focus, no decision. The three hosts each need one of these and
5
+ // they share no code, which is exactly why the *names* they translate into are
6
+ // constants in core rather than literals here.
7
+
8
+ #pragma once
9
+
10
+ #include "KeyEvents.h"
11
+
12
+ #include <optional>
13
+
14
+ #ifdef __OBJC__
15
+ @class NSEvent;
16
+ #endif
17
+
18
+ namespace basalt {
19
+
20
+ #ifdef __OBJC__
21
+ // The combination this key press represents, or nothing for a press with no key
22
+ // to speak of -- a bare modifier, or a dead key mid-composition.
23
+ std::optional<KeyCombination> keyCombinationFrom(NSEvent *event);
24
+ #endif
25
+
26
+ } // namespace basalt
@@ -0,0 +1,97 @@
1
+ #include "AppKitKeyEvents.h"
2
+
3
+ #import <AppKit/AppKit.h>
4
+
5
+ #include <string>
6
+
7
+ namespace basalt {
8
+ namespace {
9
+
10
+ // AppKit reports the arrows and the navigation block as characters in the
11
+ // private-use area, which is why these are compared against
12
+ // `charactersIgnoringModifiers` rather than against a key code: a key code is
13
+ // keyboard-layout dependent and these are not.
14
+ const char *specialNameFor(unichar character) {
15
+ switch (character) {
16
+ case NSUpArrowFunctionKey:
17
+ return kKeyArrowUp;
18
+ case NSDownArrowFunctionKey:
19
+ return kKeyArrowDown;
20
+ case NSLeftArrowFunctionKey:
21
+ return kKeyArrowLeft;
22
+ case NSRightArrowFunctionKey:
23
+ return kKeyArrowRight;
24
+ case NSPageUpFunctionKey:
25
+ return kKeyPageUp;
26
+ case NSPageDownFunctionKey:
27
+ return kKeyPageDown;
28
+ case NSHomeFunctionKey:
29
+ return kKeyHome;
30
+ case NSEndFunctionKey:
31
+ return kKeyEnd;
32
+ // Forward delete. The key most keyboards label "delete" is backspace, below.
33
+ case NSDeleteFunctionKey:
34
+ return kKeyDelete;
35
+ case NSCarriageReturnCharacter:
36
+ case NSEnterCharacter:
37
+ return kKeyEnter;
38
+ case NSTabCharacter:
39
+ // Shift-Tab arrives as its own character rather than as Tab with a modifier.
40
+ // Reported as Tab with shift set, because that is what a browser reports and
41
+ // what an app will have bound.
42
+ case NSBackTabCharacter:
43
+ return kKeyTab;
44
+ case 0x1B:
45
+ return kKeyEscape;
46
+ case NSDeleteCharacter:
47
+ return kKeyBackspace;
48
+ default:
49
+ return nullptr;
50
+ }
51
+ }
52
+
53
+ } // namespace
54
+
55
+ std::optional<KeyCombination> keyCombinationFrom(NSEvent *event) {
56
+ if (event == nil) {
57
+ return std::nullopt;
58
+ }
59
+
60
+ KeyModifiers modifiers;
61
+ const NSEventModifierFlags flags = event.modifierFlags;
62
+ modifiers.alt = (flags & NSEventModifierFlagOption) != 0;
63
+ modifiers.ctrl = (flags & NSEventModifierFlagControl) != 0;
64
+ // Command is `meta`, as it is in a browser on this platform and in
65
+ // react-native-macos. An app binding Cmd+Z writes metaKey.
66
+ modifiers.meta = (flags & NSEventModifierFlagCommand) != 0;
67
+ modifiers.shift = (flags & NSEventModifierFlagShift) != 0;
68
+
69
+ NSString *bare = event.charactersIgnoringModifiers;
70
+ if (bare.length > 0) {
71
+ if (const char *special = specialNameFor([bare characterAtIndex:0])) {
72
+ return KeyCombination{std::string(special), modifiers};
73
+ }
74
+ }
75
+
76
+ // A character, with its case. `characters` applies Shift, so Shift+A is "A"
77
+ // and its shift modifier is also set -- both true, as in a browser. It also
78
+ // applies Option, so Option+A is "å" on a US layout, which is again what a
79
+ // browser reports and what the person actually typed.
80
+ //
81
+ // Not `charactersIgnoringModifiers`, which would report "a" for Shift+A and
82
+ // make the two indistinguishable to an app that binds by character.
83
+ NSString *typed = event.characters;
84
+ if (typed.length == 0) {
85
+ // A bare modifier, or a dead key part-way through composing. Neither is a
86
+ // combination and neither should be offered to an app as one.
87
+ return std::nullopt;
88
+ }
89
+ // Control turns a letter into its control character -- Ctrl+A is 0x01 -- which
90
+ // is not a name anybody binds. The unmodified character is what an app means.
91
+ if (modifiers.ctrl && bare.length > 0) {
92
+ typed = bare;
93
+ }
94
+ return KeyCombination{std::string(typed.UTF8String), modifiers};
95
+ }
96
+
97
+ } // namespace basalt