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,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
|