basalt-win32 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/runWindows.js +35 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +398 -0
- package/native/tests/test_win32_accessibility.cpp +271 -0
- package/native/tests/test_win32_clipboard.cpp +121 -0
- package/native/tests/test_win32_focus.cpp +195 -0
- package/native/tests/test_win32_hittest.cpp +410 -0
- package/native/tests/test_win32_image.cpp +239 -0
- package/native/tests/test_win32_imageloader.cpp +208 -0
- package/native/tests/test_win32_input.cpp +361 -0
- package/native/tests/test_win32_key_events.cpp +84 -0
- package/native/tests/test_win32_mounting.cpp +410 -0
- package/native/tests/test_win32_paint.cpp +505 -0
- package/native/tests/test_win32_scrollview.cpp +385 -0
- package/native/tests/test_win32_text.cpp +258 -0
- package/native/tests/test_win32_textinput.cpp +609 -0
- package/native/tests/test_win32_titlebar.cpp +111 -0
- package/native/tests/test_win32_titlebar_actions.cpp +62 -0
- package/native/tests/test_win32_view.cpp +222 -0
- package/native/win32/ColorSchemeWin32.cpp +56 -0
- package/native/win32/ComponentRegistryWin32.cpp +124 -0
- package/native/win32/DirectWriteLayout.cpp +116 -0
- package/native/win32/DirectWriteLayout.h +36 -0
- package/native/win32/DirectWriteLayoutManager.cpp +124 -0
- package/native/win32/FontRegistryDirectWrite.cpp +117 -0
- package/native/win32/PlatformServicesWin32.cpp +643 -0
- package/native/win32/RnWin32Accessible.cpp +258 -0
- package/native/win32/RnWin32Accessible.h +91 -0
- package/native/win32/RnWin32Image.cpp +265 -0
- package/native/win32/RnWin32Image.h +93 -0
- package/native/win32/RnWin32TextLayout.cpp +310 -0
- package/native/win32/RnWin32TextLayout.h +160 -0
- package/native/win32/RnWin32View.cpp +1281 -0
- package/native/win32/RnWin32View.h +506 -0
- package/native/win32/Win32AnimationChoreographer.cpp +76 -0
- package/native/win32/Win32AnimationChoreographer.h +61 -0
- package/native/win32/Win32Clip.h +173 -0
- package/native/win32/Win32DropTarget.cpp +567 -0
- package/native/win32/Win32DropTarget.h +42 -0
- package/native/win32/Win32Focus.cpp +262 -0
- package/native/win32/Win32Focus.h +109 -0
- package/native/win32/Win32ImageLoader.cpp +167 -0
- package/native/win32/Win32ImageLoader.h +104 -0
- package/native/win32/Win32KeyEvents.cpp +126 -0
- package/native/win32/Win32KeyEvents.h +20 -0
- package/native/win32/Win32MenuBar.cpp +352 -0
- package/native/win32/Win32MenuBar.h +16 -0
- package/native/win32/Win32MountingManager.cpp +727 -0
- package/native/win32/Win32MountingManager.h +203 -0
- package/native/win32/Win32Packaging.cpp +161 -0
- package/native/win32/Win32Packaging.h +56 -0
- package/native/win32/Win32RunLoopObserver.cpp +48 -0
- package/native/win32/Win32RunLoopObserver.h +42 -0
- package/native/win32/Win32ScrollView.cpp +515 -0
- package/native/win32/Win32ScrollView.h +208 -0
- package/native/win32/Win32SkiaContext.cpp +221 -0
- package/native/win32/Win32SkiaContext.h +53 -0
- package/native/win32/Win32SkiaModule.cpp +80 -0
- package/native/win32/Win32SkiaModule.h +50 -0
- package/native/win32/Win32SkiaPeer.cpp +358 -0
- package/native/win32/Win32SkiaPeer.h +66 -0
- package/native/win32/Win32SkiaUnicode.cpp +42 -0
- package/native/win32/Win32Snapshot.cpp +201 -0
- package/native/win32/Win32Snapshot.h +66 -0
- package/native/win32/Win32Strings.h +54 -0
- package/native/win32/Win32TextInput.cpp +811 -0
- package/native/win32/Win32TextInput.h +232 -0
- package/native/win32/Win32TitleBar.cpp +562 -0
- package/native/win32/Win32TitleBar.h +146 -0
- package/native/win32/Win32TitleBarLayout.cpp +46 -0
- package/native/win32/Win32TitleBarLayout.h +58 -0
- package/native/win32/Win32TouchDispatcher.cpp +443 -0
- package/native/win32/Win32TouchDispatcher.h +153 -0
- package/native/win32/Win32UiThread.h +27 -0
- package/native/win32/Win32WindowControl.cpp +329 -0
- package/native/win32/Win32WindowModule.cpp +209 -0
- package/native/win32/Win32WindowModule.h +72 -0
- package/native/win32/demo_layout_win32.cpp +232 -0
- package/native/win32/main_win32.cpp +2481 -0
- package/native/win32/mount_harness_win32.cpp +316 -0
- package/package.json +26 -0
- package/react-native.config.js +22 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#include "RnWin32Accessible.h"
|
|
2
|
+
|
|
3
|
+
#include "Win32Strings.h"
|
|
4
|
+
|
|
5
|
+
#include <windows.h>
|
|
6
|
+
|
|
7
|
+
// WIN32_LEAN_AND_MEAN keeps combaseapi.h out of windows.h, and that is where the
|
|
8
|
+
// `interface` macro lives -- without it UIAutomationCore.h fails on its own
|
|
9
|
+
// first typedef, which reads as a broken SDK rather than a missing include.
|
|
10
|
+
#include <objbase.h>
|
|
11
|
+
|
|
12
|
+
#include <uiautomation.h>
|
|
13
|
+
|
|
14
|
+
#include <atomic>
|
|
15
|
+
#include <unordered_map>
|
|
16
|
+
|
|
17
|
+
namespace basalt::win32 {
|
|
18
|
+
namespace {
|
|
19
|
+
|
|
20
|
+
// React Native's role vocabulary, mapped onto UIA control types. The names are
|
|
21
|
+
// exactly the ones `GtkMountingManager.cpp` accepts, in the same order, because
|
|
22
|
+
// a role this platform silently ignores and GTK honours is a difference no test
|
|
23
|
+
// comparing two trees would catch -- both print React Native's own string.
|
|
24
|
+
//
|
|
25
|
+
// Three of them are approximations UIA forces, and they are worth naming rather
|
|
26
|
+
// than leaving to be rediscovered. UIA has no switch, so a switch is a checkbox
|
|
27
|
+
// with a toggle state, which is what Narrator reads sensibly. It has no heading
|
|
28
|
+
// control type either -- headings are a *property*, UIA_HeadingLevelPropertyId,
|
|
29
|
+
// on a text element -- so "header" is text. And "alert" is not a control type
|
|
30
|
+
// at all in UIA; it is an event, so the nearest honest resting place is a group
|
|
31
|
+
// until there is a live-region implementation to raise it from.
|
|
32
|
+
const std::unordered_map<std::string, long> &roleTable() {
|
|
33
|
+
static const std::unordered_map<std::string, long> kRoles = {
|
|
34
|
+
{"button", UIA_ButtonControlTypeId},
|
|
35
|
+
{"togglebutton", UIA_ButtonControlTypeId},
|
|
36
|
+
{"link", UIA_HyperlinkControlTypeId},
|
|
37
|
+
{"search", UIA_EditControlTypeId},
|
|
38
|
+
{"image", UIA_ImageControlTypeId},
|
|
39
|
+
{"imagebutton", UIA_ButtonControlTypeId},
|
|
40
|
+
{"text", UIA_TextControlTypeId},
|
|
41
|
+
{"header", UIA_TextControlTypeId},
|
|
42
|
+
{"adjustable", UIA_SliderControlTypeId},
|
|
43
|
+
{"alert", UIA_GroupControlTypeId},
|
|
44
|
+
{"checkbox", UIA_CheckBoxControlTypeId},
|
|
45
|
+
{"combobox", UIA_ComboBoxControlTypeId},
|
|
46
|
+
{"menu", UIA_MenuControlTypeId},
|
|
47
|
+
{"menubar", UIA_MenuBarControlTypeId},
|
|
48
|
+
{"menuitem", UIA_MenuItemControlTypeId},
|
|
49
|
+
{"progressbar", UIA_ProgressBarControlTypeId},
|
|
50
|
+
{"radio", UIA_RadioButtonControlTypeId},
|
|
51
|
+
{"radiogroup", UIA_GroupControlTypeId},
|
|
52
|
+
{"scrollbar", UIA_ScrollBarControlTypeId},
|
|
53
|
+
{"spinbutton", UIA_SpinnerControlTypeId},
|
|
54
|
+
{"switch", UIA_CheckBoxControlTypeId},
|
|
55
|
+
{"tab", UIA_TabItemControlTypeId},
|
|
56
|
+
{"tablist", UIA_TabControlTypeId},
|
|
57
|
+
{"list", UIA_ListControlTypeId},
|
|
58
|
+
{"grid", UIA_DataGridControlTypeId},
|
|
59
|
+
{"toolbar", UIA_ToolBarControlTypeId},
|
|
60
|
+
{"tooltip", UIA_ToolTipControlTypeId},
|
|
61
|
+
};
|
|
62
|
+
return kRoles;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
BSTR toBstr(const std::string &text) {
|
|
66
|
+
const std::wstring wide = widen(text);
|
|
67
|
+
return SysAllocStringLen(wide.c_str(), static_cast<UINT>(wide.size()));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The property half of a UIA provider.
|
|
71
|
+
//
|
|
72
|
+
// ProviderOptions_ServerSideProvider because this lives in the process that
|
|
73
|
+
// owns the UI, which is the only arrangement that makes sense for a host that
|
|
74
|
+
// draws its own views.
|
|
75
|
+
class AccessibleProvider final : public IRawElementProviderSimple {
|
|
76
|
+
public:
|
|
77
|
+
explicit AccessibleProvider(const RnAccessibleInfo &info) : info_(info) {}
|
|
78
|
+
|
|
79
|
+
// --- IUnknown ---
|
|
80
|
+
|
|
81
|
+
ULONG STDMETHODCALLTYPE AddRef() override { return ++references_; }
|
|
82
|
+
|
|
83
|
+
ULONG STDMETHODCALLTYPE Release() override {
|
|
84
|
+
const ULONG remaining = --references_;
|
|
85
|
+
if (remaining == 0) {
|
|
86
|
+
delete this;
|
|
87
|
+
}
|
|
88
|
+
return remaining;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) override {
|
|
92
|
+
if (object == nullptr) {
|
|
93
|
+
return E_INVALIDARG;
|
|
94
|
+
}
|
|
95
|
+
if (riid == __uuidof(IUnknown) || riid == __uuidof(IRawElementProviderSimple)) {
|
|
96
|
+
*object = static_cast<IRawElementProviderSimple *>(this);
|
|
97
|
+
AddRef();
|
|
98
|
+
return S_OK;
|
|
99
|
+
}
|
|
100
|
+
*object = nullptr;
|
|
101
|
+
return E_NOINTERFACE;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- IRawElementProviderSimple ---
|
|
105
|
+
|
|
106
|
+
HRESULT STDMETHODCALLTYPE get_ProviderOptions(ProviderOptions *options) override {
|
|
107
|
+
if (options == nullptr) {
|
|
108
|
+
return E_INVALIDARG;
|
|
109
|
+
}
|
|
110
|
+
*options = ProviderOptions_ServerSideProvider;
|
|
111
|
+
return S_OK;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The control patterns -- Toggle, SelectionItem, ExpandCollapse, Invoke --
|
|
115
|
+
// land with the host, because a pattern that reports a state but cannot be
|
|
116
|
+
// driven is worse than none: it tells a screen reader the control can be
|
|
117
|
+
// operated and then does nothing. The *states* are answered as properties
|
|
118
|
+
// below, which is read-only and honest.
|
|
119
|
+
HRESULT STDMETHODCALLTYPE GetPatternProvider(PATTERNID, IUnknown **provider) override {
|
|
120
|
+
if (provider == nullptr) {
|
|
121
|
+
return E_INVALIDARG;
|
|
122
|
+
}
|
|
123
|
+
*provider = nullptr;
|
|
124
|
+
return S_OK;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
HRESULT STDMETHODCALLTYPE GetPropertyValue(PROPERTYID property, VARIANT *value) override {
|
|
128
|
+
if (value == nullptr) {
|
|
129
|
+
return E_INVALIDARG;
|
|
130
|
+
}
|
|
131
|
+
VariantInit(value);
|
|
132
|
+
|
|
133
|
+
switch (property) {
|
|
134
|
+
case UIA_ControlTypePropertyId:
|
|
135
|
+
value->vt = VT_I4;
|
|
136
|
+
value->lVal = controlTypeForRole(info_.role);
|
|
137
|
+
return S_OK;
|
|
138
|
+
|
|
139
|
+
case UIA_NamePropertyId:
|
|
140
|
+
if (!info_.label.empty()) {
|
|
141
|
+
value->vt = VT_BSTR;
|
|
142
|
+
value->bstrVal = toBstr(info_.label);
|
|
143
|
+
}
|
|
144
|
+
return S_OK;
|
|
145
|
+
|
|
146
|
+
case UIA_HelpTextPropertyId:
|
|
147
|
+
// React Native's `accessibilityHint` is "what happens if you do this",
|
|
148
|
+
// which is what UIA calls help text. AppKit puts it in the
|
|
149
|
+
// accessibility description and GTK in the description property; all
|
|
150
|
+
// three are the same idea under three names.
|
|
151
|
+
if (!info_.hint.empty()) {
|
|
152
|
+
value->vt = VT_BSTR;
|
|
153
|
+
value->bstrVal = toBstr(info_.hint);
|
|
154
|
+
}
|
|
155
|
+
return S_OK;
|
|
156
|
+
|
|
157
|
+
case UIA_IsEnabledPropertyId:
|
|
158
|
+
// Unset means enabled, which is UIA's default and React Native's.
|
|
159
|
+
value->vt = VT_BOOL;
|
|
160
|
+
value->boolVal =
|
|
161
|
+
info_.state.disabled == RnAccessibleFlag::True ? VARIANT_FALSE : VARIANT_TRUE;
|
|
162
|
+
return S_OK;
|
|
163
|
+
|
|
164
|
+
case UIA_ToggleToggleStatePropertyId:
|
|
165
|
+
// The property mirror of the Toggle pattern, so the state is reportable
|
|
166
|
+
// before the pattern exists. Left empty when unset, because
|
|
167
|
+
// ToggleState_Off is a claim that this is an unchecked checkbox.
|
|
168
|
+
if (info_.state.checked != RnAccessibleFlag::Unset) {
|
|
169
|
+
value->vt = VT_I4;
|
|
170
|
+
value->lVal = info_.state.checked == RnAccessibleFlag::True ? ToggleState_On
|
|
171
|
+
: ToggleState_Off;
|
|
172
|
+
}
|
|
173
|
+
return S_OK;
|
|
174
|
+
|
|
175
|
+
case UIA_SelectionItemIsSelectedPropertyId:
|
|
176
|
+
if (info_.state.selected != RnAccessibleFlag::Unset) {
|
|
177
|
+
value->vt = VT_BOOL;
|
|
178
|
+
value->boolVal =
|
|
179
|
+
info_.state.selected == RnAccessibleFlag::True ? VARIANT_TRUE : VARIANT_FALSE;
|
|
180
|
+
}
|
|
181
|
+
return S_OK;
|
|
182
|
+
|
|
183
|
+
case UIA_ExpandCollapseExpandCollapseStatePropertyId:
|
|
184
|
+
if (info_.state.expanded != RnAccessibleFlag::Unset) {
|
|
185
|
+
value->vt = VT_I4;
|
|
186
|
+
value->lVal = info_.state.expanded == RnAccessibleFlag::True
|
|
187
|
+
? ExpandCollapseState_Expanded
|
|
188
|
+
: ExpandCollapseState_Collapsed;
|
|
189
|
+
}
|
|
190
|
+
return S_OK;
|
|
191
|
+
|
|
192
|
+
case UIA_IsOffscreenPropertyId:
|
|
193
|
+
value->vt = VT_BOOL;
|
|
194
|
+
value->boolVal = info_.hidden ? VARIANT_TRUE : VARIANT_FALSE;
|
|
195
|
+
return S_OK;
|
|
196
|
+
|
|
197
|
+
case UIA_IsControlElementPropertyId:
|
|
198
|
+
case UIA_IsContentElementPropertyId:
|
|
199
|
+
value->vt = VT_BOOL;
|
|
200
|
+
value->boolVal = VARIANT_TRUE;
|
|
201
|
+
return S_OK;
|
|
202
|
+
|
|
203
|
+
default:
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// VT_EMPTY, which is UIA's "I have no opinion, use the default". Returning
|
|
208
|
+
// an error here instead makes a screen reader treat the whole element as
|
|
209
|
+
// broken rather than as quiet.
|
|
210
|
+
return S_OK;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Null because these views are not native Windows controls: there is no
|
|
214
|
+
// HWND-based provider underneath to defer to. The host's fragment root is
|
|
215
|
+
// where the one real HWND enters the picture.
|
|
216
|
+
HRESULT STDMETHODCALLTYPE
|
|
217
|
+
get_HostRawElementProvider(IRawElementProviderSimple **provider) override {
|
|
218
|
+
if (provider == nullptr) {
|
|
219
|
+
return E_INVALIDARG;
|
|
220
|
+
}
|
|
221
|
+
*provider = nullptr;
|
|
222
|
+
return S_OK;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private:
|
|
226
|
+
~AccessibleProvider() = default;
|
|
227
|
+
|
|
228
|
+
RnAccessibleInfo info_;
|
|
229
|
+
std::atomic<ULONG> references_{1};
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
} // namespace
|
|
233
|
+
|
|
234
|
+
long controlTypeForRole(const std::string &role) {
|
|
235
|
+
const auto &table = roleTable();
|
|
236
|
+
const auto it = table.find(role);
|
|
237
|
+
return it == table.end() ? UIA_GroupControlTypeId : it->second;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
bool roleIsPresentational(const std::string &role) {
|
|
241
|
+
return role == "none" || role == "presentation";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
bool isAccessibilityElement(const RnAccessibleInfo &info) {
|
|
245
|
+
if (info.hidden || roleIsPresentational(info.role)) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
return !info.role.empty() || !info.label.empty() || !info.hint.empty();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
IRawElementProviderSimple *createAccessibleProvider(const RnAccessibleInfo &info) {
|
|
252
|
+
if (!isAccessibilityElement(info)) {
|
|
253
|
+
return nullptr;
|
|
254
|
+
}
|
|
255
|
+
return new AccessibleProvider(info);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
} // namespace basalt::win32
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// What a screen reader is told about a view.
|
|
2
|
+
//
|
|
3
|
+
// This is the one place Windows is structurally different from the other two
|
|
4
|
+
// desktops rather than differently spelled. GTK and AppKit both take
|
|
5
|
+
// accessibility as *properties on a view*: `gtk_accessible_update_property`,
|
|
6
|
+
// `setAccessibilityRole:`. UI Automation inverts that -- it asks a *provider*
|
|
7
|
+
// object questions, and the provider answers by property id. So where the other
|
|
8
|
+
// two mounting managers push, this one has to be ready to be pulled.
|
|
9
|
+
//
|
|
10
|
+
// The consequence worth knowing: a GTK role is construct-only, which is why
|
|
11
|
+
// `docs/DECISIONS.md` records that `accessibilityRole` cannot change after
|
|
12
|
+
// mount there. Nothing here has that constraint, because nothing is baked into
|
|
13
|
+
// a widget class -- the provider reads the current value each time it is asked.
|
|
14
|
+
// This platform can therefore do something GTK cannot, and the mounting manager
|
|
15
|
+
// should not copy GTK's restriction when it arrives.
|
|
16
|
+
//
|
|
17
|
+
// What is here is the property half, which is all of it that can exist before
|
|
18
|
+
// there is an HWND: UIA's tree navigation needs a fragment root, and a fragment
|
|
19
|
+
// root is a window. `IRawElementProviderFragment` and the `Toggle`,
|
|
20
|
+
// `SelectionItem` and `ExpandCollapse` *patterns* therefore land with the host.
|
|
21
|
+
// The states those patterns expose are answerable now, because UIA mirrors each
|
|
22
|
+
// of them as a plain property id, and that is what these answer.
|
|
23
|
+
|
|
24
|
+
#pragma once
|
|
25
|
+
|
|
26
|
+
#include <string>
|
|
27
|
+
|
|
28
|
+
struct IRawElementProviderSimple;
|
|
29
|
+
|
|
30
|
+
namespace basalt::win32 {
|
|
31
|
+
|
|
32
|
+
// Each state is a tri-state on purpose. Leaving `checked` unset is not the same
|
|
33
|
+
// as setting it false: a view that never mentions being checked is not an
|
|
34
|
+
// unchecked checkbox, and a screen reader should not read it as one. Both other
|
|
35
|
+
// platforms make the same distinction.
|
|
36
|
+
enum class RnAccessibleFlag {
|
|
37
|
+
Unset,
|
|
38
|
+
False,
|
|
39
|
+
True,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
struct RnAccessibleState {
|
|
43
|
+
RnAccessibleFlag disabled = RnAccessibleFlag::Unset;
|
|
44
|
+
RnAccessibleFlag checked = RnAccessibleFlag::Unset;
|
|
45
|
+
RnAccessibleFlag selected = RnAccessibleFlag::Unset;
|
|
46
|
+
RnAccessibleFlag expanded = RnAccessibleFlag::Unset;
|
|
47
|
+
RnAccessibleFlag busy = RnAccessibleFlag::Unset;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Everything a view carries for assistive technology. Held by the view as plain
|
|
51
|
+
// data; the provider below reads it.
|
|
52
|
+
struct RnAccessibleInfo {
|
|
53
|
+
// React Native's own role string -- "button", "image", "text" -- not a UIA
|
|
54
|
+
// control type. The mapping is a platform decision and belongs beside the
|
|
55
|
+
// platform, and the string is what `describeTree` reports, so the three
|
|
56
|
+
// hosts' trees compare without one of them speaking UIA's vocabulary and
|
|
57
|
+
// another speaking GTK's.
|
|
58
|
+
std::string role;
|
|
59
|
+
std::string label;
|
|
60
|
+
std::string hint;
|
|
61
|
+
RnAccessibleState state;
|
|
62
|
+
// accessible={false} and accessibilityElementsHidden.
|
|
63
|
+
bool hidden = false;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// The UIA control type for a React Native role name, or
|
|
67
|
+
// UIA_GroupControlTypeId for one this platform does not recognise.
|
|
68
|
+
//
|
|
69
|
+
// A fallback rather than a guess: a wrong control type is worse than a vague
|
|
70
|
+
// one, because it makes a view announce itself as something it is not. Both
|
|
71
|
+
// other platforms fall back the same way.
|
|
72
|
+
long controlTypeForRole(const std::string &role);
|
|
73
|
+
|
|
74
|
+
// True for "none" and "presentation", which mean "do not report this at all".
|
|
75
|
+
bool roleIsPresentational(const std::string &role);
|
|
76
|
+
|
|
77
|
+
// True when this view should appear to a screen reader. A plain <View> with no
|
|
78
|
+
// role, no label and no hint is scaffolding, and a tree full of untyped groups
|
|
79
|
+
// is worse for a screen reader user than a tree without them.
|
|
80
|
+
bool isAccessibilityElement(const RnAccessibleInfo &info);
|
|
81
|
+
|
|
82
|
+
// A UI Automation provider over one view's RnAccessibleInfo, for the property
|
|
83
|
+
// half of the interface. Returns null when the view is not an accessibility
|
|
84
|
+
// element. The caller owns a reference and must Release it.
|
|
85
|
+
//
|
|
86
|
+
// The provider copies the info rather than pointing at the view: UIA can ask
|
|
87
|
+
// its questions from another thread and long after the view is gone, and a
|
|
88
|
+
// dangling read there is a crash inside somebody's screen reader.
|
|
89
|
+
IRawElementProviderSimple *createAccessibleProvider(const RnAccessibleInfo &info);
|
|
90
|
+
|
|
91
|
+
} // namespace basalt::win32
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#include "RnWin32Image.h"
|
|
2
|
+
|
|
3
|
+
#include "Win32Clip.h"
|
|
4
|
+
|
|
5
|
+
#include <windows.h>
|
|
6
|
+
|
|
7
|
+
#include <d2d1.h>
|
|
8
|
+
#include <wincodec.h>
|
|
9
|
+
#include <wrl/client.h>
|
|
10
|
+
|
|
11
|
+
#include <algorithm>
|
|
12
|
+
|
|
13
|
+
using Microsoft::WRL::ComPtr;
|
|
14
|
+
|
|
15
|
+
namespace basalt::win32 {
|
|
16
|
+
namespace {
|
|
17
|
+
|
|
18
|
+
bool ensureCom() {
|
|
19
|
+
const HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
|
20
|
+
return SUCCEEDED(hr) || hr == RPC_E_CHANGED_MODE;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
IWICImagingFactory *wicFactory() {
|
|
24
|
+
// Every call, not only the first: decoding happens on whichever worker thread
|
|
25
|
+
// the image loader put it on, and COM is per-thread. The factory itself is
|
|
26
|
+
// agile -- WIC registers it "Both" -- so one instance serves every thread,
|
|
27
|
+
// but a thread that has never called CoInitializeEx cannot use it.
|
|
28
|
+
// CoInitializeEx on an already-initialised thread returns S_FALSE and costs
|
|
29
|
+
// nothing.
|
|
30
|
+
if (!ensureCom()) {
|
|
31
|
+
return nullptr;
|
|
32
|
+
}
|
|
33
|
+
static IWICImagingFactory *factory = [] {
|
|
34
|
+
IWICImagingFactory *created = nullptr;
|
|
35
|
+
CoCreateInstance(
|
|
36
|
+
CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&created));
|
|
37
|
+
return created;
|
|
38
|
+
}();
|
|
39
|
+
return factory;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
} // namespace
|
|
43
|
+
|
|
44
|
+
const char *imageFitName(RnImageFit fit) {
|
|
45
|
+
switch (fit) {
|
|
46
|
+
case RnImageFit::Contain:
|
|
47
|
+
return "contain";
|
|
48
|
+
case RnImageFit::Stretch:
|
|
49
|
+
return "stretch";
|
|
50
|
+
case RnImageFit::Center:
|
|
51
|
+
return "center";
|
|
52
|
+
case RnImageFit::Repeat:
|
|
53
|
+
return "repeat";
|
|
54
|
+
case RnImageFit::Cover:
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
return "cover";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
std::shared_ptr<RnWin32Image>
|
|
61
|
+
RnWin32Image::fromPixels(unsigned width, unsigned height, const uint8_t *bgra) {
|
|
62
|
+
IWICImagingFactory *factory = wicFactory();
|
|
63
|
+
if (factory == nullptr || bgra == nullptr || width == 0 || height == 0) {
|
|
64
|
+
return nullptr;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const UINT stride = width * 4;
|
|
68
|
+
ComPtr<IWICBitmap> bitmap;
|
|
69
|
+
if (FAILED(factory->CreateBitmapFromMemory(width,
|
|
70
|
+
height,
|
|
71
|
+
GUID_WICPixelFormat32bppPBGRA,
|
|
72
|
+
stride,
|
|
73
|
+
stride * height,
|
|
74
|
+
const_cast<BYTE *>(bgra),
|
|
75
|
+
bitmap.GetAddressOf()))) {
|
|
76
|
+
return nullptr;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
auto image = std::shared_ptr<RnWin32Image>(new RnWin32Image());
|
|
80
|
+
image->bitmap_ = bitmap.Detach();
|
|
81
|
+
image->width_ = width;
|
|
82
|
+
image->height_ = height;
|
|
83
|
+
return image;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
std::shared_ptr<RnWin32Image> RnWin32Image::fromEncodedBytes(const uint8_t *data, size_t size) {
|
|
87
|
+
IWICImagingFactory *factory = wicFactory();
|
|
88
|
+
if (factory == nullptr || data == nullptr || size == 0) {
|
|
89
|
+
return nullptr;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// A stream over the caller's bytes. WIC copies what it needs during
|
|
93
|
+
// CreateBitmapFromSource below, so the caller's buffer does not have to
|
|
94
|
+
// outlive this function.
|
|
95
|
+
ComPtr<IWICStream> stream;
|
|
96
|
+
if (FAILED(factory->CreateStream(stream.GetAddressOf())) ||
|
|
97
|
+
FAILED(stream->InitializeFromMemory(const_cast<BYTE *>(data), static_cast<DWORD>(size)))) {
|
|
98
|
+
return nullptr;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
ComPtr<IWICBitmapDecoder> decoder;
|
|
102
|
+
if (FAILED(factory->CreateDecoderFromStream(
|
|
103
|
+
stream.Get(), nullptr, WICDecodeMetadataCacheOnLoad, decoder.GetAddressOf()))) {
|
|
104
|
+
return nullptr;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
ComPtr<IWICBitmapFrameDecode> frame;
|
|
108
|
+
if (FAILED(decoder->GetFrame(0, frame.GetAddressOf()))) {
|
|
109
|
+
return nullptr;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Whatever the file was, Direct2D wants premultiplied BGRA.
|
|
113
|
+
ComPtr<IWICFormatConverter> converter;
|
|
114
|
+
if (FAILED(factory->CreateFormatConverter(converter.GetAddressOf())) ||
|
|
115
|
+
FAILED(converter->Initialize(frame.Get(),
|
|
116
|
+
GUID_WICPixelFormat32bppPBGRA,
|
|
117
|
+
WICBitmapDitherTypeNone,
|
|
118
|
+
nullptr,
|
|
119
|
+
0.0,
|
|
120
|
+
WICBitmapPaletteTypeCustom))) {
|
|
121
|
+
return nullptr;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ComPtr<IWICBitmap> bitmap;
|
|
125
|
+
if (FAILED(factory->CreateBitmapFromSource(
|
|
126
|
+
converter.Get(), WICBitmapCacheOnLoad, bitmap.GetAddressOf()))) {
|
|
127
|
+
return nullptr;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
UINT width = 0;
|
|
131
|
+
UINT height = 0;
|
|
132
|
+
if (FAILED(bitmap->GetSize(&width, &height)) || width == 0 || height == 0) {
|
|
133
|
+
return nullptr;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
auto image = std::shared_ptr<RnWin32Image>(new RnWin32Image());
|
|
137
|
+
image->bitmap_ = bitmap.Detach();
|
|
138
|
+
image->width_ = width;
|
|
139
|
+
image->height_ = height;
|
|
140
|
+
return image;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
RnWin32Image::~RnWin32Image() {
|
|
144
|
+
if (deviceBitmap_ != nullptr) {
|
|
145
|
+
deviceBitmap_->Release();
|
|
146
|
+
}
|
|
147
|
+
if (bitmap_ != nullptr) {
|
|
148
|
+
bitmap_->Release();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
void RnWin32Image::draw(ID2D1RenderTarget *target,
|
|
153
|
+
float boxWidth,
|
|
154
|
+
float boxHeight,
|
|
155
|
+
RnImageFit fit,
|
|
156
|
+
const float *tint) const {
|
|
157
|
+
if (target == nullptr || bitmap_ == nullptr || boxWidth <= 0.0f || boxHeight <= 0.0f) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (deviceBitmap_ == nullptr || deviceTarget_ != target) {
|
|
162
|
+
if (deviceBitmap_ != nullptr) {
|
|
163
|
+
deviceBitmap_->Release();
|
|
164
|
+
deviceBitmap_ = nullptr;
|
|
165
|
+
}
|
|
166
|
+
ID2D1Bitmap *created = nullptr;
|
|
167
|
+
if (FAILED(target->CreateBitmapFromWicBitmap(bitmap_, nullptr, &created))) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
deviceBitmap_ = created;
|
|
171
|
+
deviceTarget_ = target;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const float imageWidth = static_cast<float>(width_);
|
|
175
|
+
const float imageHeight = static_cast<float>(height_);
|
|
176
|
+
|
|
177
|
+
// The same arithmetic rn_view_snapshot does on GTK, in the same order, so the
|
|
178
|
+
// three desktops crop and centre identically. A picture is the only thing
|
|
179
|
+
// that would catch a divergence here, which is why test_win32_image.cpp reads
|
|
180
|
+
// pixels rather than checking the destination rect.
|
|
181
|
+
D2D1_RECT_F destination = D2D1::RectF(0.0f, 0.0f, boxWidth, boxHeight);
|
|
182
|
+
if (fit != RnImageFit::Stretch) {
|
|
183
|
+
float scale = 1.0f;
|
|
184
|
+
switch (fit) {
|
|
185
|
+
case RnImageFit::Contain:
|
|
186
|
+
scale = std::min(boxWidth / imageWidth, boxHeight / imageHeight);
|
|
187
|
+
break;
|
|
188
|
+
case RnImageFit::Cover:
|
|
189
|
+
scale = std::max(boxWidth / imageWidth, boxHeight / imageHeight);
|
|
190
|
+
break;
|
|
191
|
+
case RnImageFit::Center:
|
|
192
|
+
// Centred at natural size, but never larger than the frame -- which is
|
|
193
|
+
// what React Native's `center` does.
|
|
194
|
+
scale = std::min(1.0f, std::min(boxWidth / imageWidth, boxHeight / imageHeight));
|
|
195
|
+
break;
|
|
196
|
+
case RnImageFit::Repeat:
|
|
197
|
+
// One tile, at natural size. Where the tiles go is decided below.
|
|
198
|
+
break;
|
|
199
|
+
case RnImageFit::Stretch:
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
const float drawnWidth = imageWidth * scale;
|
|
203
|
+
const float drawnHeight = imageHeight * scale;
|
|
204
|
+
// From the top left for `repeat`, which is what CSS does: the whole tiles
|
|
205
|
+
// start at the origin and the partial one is at the far edge.
|
|
206
|
+
const float x = fit == RnImageFit::Repeat ? 0.0f : (boxWidth - drawnWidth) / 2.0f;
|
|
207
|
+
const float y = fit == RnImageFit::Repeat ? 0.0f : (boxHeight - drawnHeight) / 2.0f;
|
|
208
|
+
destination = D2D1::RectF(x, y, x + drawnWidth, y + drawnHeight);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// cover and center can put pixels outside the frame, and an <Image> never
|
|
212
|
+
// paints beyond its own box on iOS or Android. A geometry clip rather than an
|
|
213
|
+
// axis-aligned one, because an <Image> inside a rotated view is not exotic;
|
|
214
|
+
// see Win32Clip.h.
|
|
215
|
+
const bool needsClip =
|
|
216
|
+
fit == RnImageFit::Cover || fit == RnImageFit::Center || fit == RnImageFit::Repeat;
|
|
217
|
+
{
|
|
218
|
+
const ScopedGeometryClip clip(needsClip ? target : nullptr,
|
|
219
|
+
D2D1::RectF(0.0f, 0.0f, boxWidth, boxHeight),
|
|
220
|
+
0.0f);
|
|
221
|
+
if (fit == RnImageFit::Repeat) {
|
|
222
|
+
// A bitmap brush in wrap mode, which is Direct2D's tiling: the brush
|
|
223
|
+
// repeats the bitmap across whatever is filled, and the clip above is
|
|
224
|
+
// what stops it at the view's edge.
|
|
225
|
+
//
|
|
226
|
+
// Nearest-neighbour on purpose. The default is linear, which blends the
|
|
227
|
+
// last column of one tile into the first of the next and leaves a seam
|
|
228
|
+
// on every boundary -- visible on exactly the small, sharp images people
|
|
229
|
+
// tile.
|
|
230
|
+
Microsoft::WRL::ComPtr<ID2D1BitmapBrush> brush;
|
|
231
|
+
const D2D1_BITMAP_BRUSH_PROPERTIES properties = D2D1::BitmapBrushProperties(
|
|
232
|
+
D2D1_EXTEND_MODE_WRAP,
|
|
233
|
+
D2D1_EXTEND_MODE_WRAP,
|
|
234
|
+
D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR);
|
|
235
|
+
// The four-argument form on the interface itself rather than one of
|
|
236
|
+
// d2d1.h's inline reference overloads, so which one is selected is not a
|
|
237
|
+
// thing to be wrong about.
|
|
238
|
+
if (SUCCEEDED(target->CreateBitmapBrush(
|
|
239
|
+
deviceBitmap_, &properties, nullptr, brush.GetAddressOf()))) {
|
|
240
|
+
target->FillRectangle(D2D1::RectF(0.0f, 0.0f, boxWidth, boxHeight), brush.Get());
|
|
241
|
+
}
|
|
242
|
+
} else if (tint != nullptr) {
|
|
243
|
+
// The bitmap becomes an opacity mask and the brush is what is painted.
|
|
244
|
+
// Direct2D requires aliased antialiasing for a mask whose content is
|
|
245
|
+
// graphics rather than text, and refuses the call otherwise -- so the
|
|
246
|
+
// mode is changed for the one draw and put back.
|
|
247
|
+
Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> brush;
|
|
248
|
+
const D2D1_COLOR_F color = D2D1::ColorF(tint[0], tint[1], tint[2], tint[3]);
|
|
249
|
+
if (SUCCEEDED(target->CreateSolidColorBrush(color, brush.GetAddressOf()))) {
|
|
250
|
+
const D2D1_ANTIALIAS_MODE previous = target->GetAntialiasMode();
|
|
251
|
+
target->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
|
|
252
|
+
target->FillOpacityMask(deviceBitmap_,
|
|
253
|
+
brush.Get(),
|
|
254
|
+
D2D1_OPACITY_MASK_CONTENT_GRAPHICS,
|
|
255
|
+
&destination,
|
|
256
|
+
nullptr);
|
|
257
|
+
target->SetAntialiasMode(previous);
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
target->DrawBitmap(deviceBitmap_, destination);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
} // namespace basalt::win32
|