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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runWindows.js +35 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +398 -0
  5. package/native/tests/test_win32_accessibility.cpp +271 -0
  6. package/native/tests/test_win32_clipboard.cpp +121 -0
  7. package/native/tests/test_win32_focus.cpp +195 -0
  8. package/native/tests/test_win32_hittest.cpp +410 -0
  9. package/native/tests/test_win32_image.cpp +239 -0
  10. package/native/tests/test_win32_imageloader.cpp +208 -0
  11. package/native/tests/test_win32_input.cpp +361 -0
  12. package/native/tests/test_win32_key_events.cpp +84 -0
  13. package/native/tests/test_win32_mounting.cpp +410 -0
  14. package/native/tests/test_win32_paint.cpp +505 -0
  15. package/native/tests/test_win32_scrollview.cpp +385 -0
  16. package/native/tests/test_win32_text.cpp +258 -0
  17. package/native/tests/test_win32_textinput.cpp +609 -0
  18. package/native/tests/test_win32_titlebar.cpp +111 -0
  19. package/native/tests/test_win32_titlebar_actions.cpp +62 -0
  20. package/native/tests/test_win32_view.cpp +222 -0
  21. package/native/win32/ColorSchemeWin32.cpp +56 -0
  22. package/native/win32/ComponentRegistryWin32.cpp +124 -0
  23. package/native/win32/DirectWriteLayout.cpp +116 -0
  24. package/native/win32/DirectWriteLayout.h +36 -0
  25. package/native/win32/DirectWriteLayoutManager.cpp +124 -0
  26. package/native/win32/FontRegistryDirectWrite.cpp +117 -0
  27. package/native/win32/PlatformServicesWin32.cpp +643 -0
  28. package/native/win32/RnWin32Accessible.cpp +258 -0
  29. package/native/win32/RnWin32Accessible.h +91 -0
  30. package/native/win32/RnWin32Image.cpp +265 -0
  31. package/native/win32/RnWin32Image.h +93 -0
  32. package/native/win32/RnWin32TextLayout.cpp +310 -0
  33. package/native/win32/RnWin32TextLayout.h +160 -0
  34. package/native/win32/RnWin32View.cpp +1281 -0
  35. package/native/win32/RnWin32View.h +506 -0
  36. package/native/win32/Win32AnimationChoreographer.cpp +76 -0
  37. package/native/win32/Win32AnimationChoreographer.h +61 -0
  38. package/native/win32/Win32Clip.h +173 -0
  39. package/native/win32/Win32DropTarget.cpp +567 -0
  40. package/native/win32/Win32DropTarget.h +42 -0
  41. package/native/win32/Win32Focus.cpp +262 -0
  42. package/native/win32/Win32Focus.h +109 -0
  43. package/native/win32/Win32ImageLoader.cpp +167 -0
  44. package/native/win32/Win32ImageLoader.h +104 -0
  45. package/native/win32/Win32KeyEvents.cpp +126 -0
  46. package/native/win32/Win32KeyEvents.h +20 -0
  47. package/native/win32/Win32MenuBar.cpp +352 -0
  48. package/native/win32/Win32MenuBar.h +16 -0
  49. package/native/win32/Win32MountingManager.cpp +727 -0
  50. package/native/win32/Win32MountingManager.h +203 -0
  51. package/native/win32/Win32Packaging.cpp +161 -0
  52. package/native/win32/Win32Packaging.h +56 -0
  53. package/native/win32/Win32RunLoopObserver.cpp +48 -0
  54. package/native/win32/Win32RunLoopObserver.h +42 -0
  55. package/native/win32/Win32ScrollView.cpp +515 -0
  56. package/native/win32/Win32ScrollView.h +208 -0
  57. package/native/win32/Win32SkiaContext.cpp +221 -0
  58. package/native/win32/Win32SkiaContext.h +53 -0
  59. package/native/win32/Win32SkiaModule.cpp +80 -0
  60. package/native/win32/Win32SkiaModule.h +50 -0
  61. package/native/win32/Win32SkiaPeer.cpp +358 -0
  62. package/native/win32/Win32SkiaPeer.h +66 -0
  63. package/native/win32/Win32SkiaUnicode.cpp +42 -0
  64. package/native/win32/Win32Snapshot.cpp +201 -0
  65. package/native/win32/Win32Snapshot.h +66 -0
  66. package/native/win32/Win32Strings.h +54 -0
  67. package/native/win32/Win32TextInput.cpp +811 -0
  68. package/native/win32/Win32TextInput.h +232 -0
  69. package/native/win32/Win32TitleBar.cpp +562 -0
  70. package/native/win32/Win32TitleBar.h +146 -0
  71. package/native/win32/Win32TitleBarLayout.cpp +46 -0
  72. package/native/win32/Win32TitleBarLayout.h +58 -0
  73. package/native/win32/Win32TouchDispatcher.cpp +443 -0
  74. package/native/win32/Win32TouchDispatcher.h +153 -0
  75. package/native/win32/Win32UiThread.h +27 -0
  76. package/native/win32/Win32WindowControl.cpp +329 -0
  77. package/native/win32/Win32WindowModule.cpp +209 -0
  78. package/native/win32/Win32WindowModule.h +72 -0
  79. package/native/win32/demo_layout_win32.cpp +232 -0
  80. package/native/win32/main_win32.cpp +2481 -0
  81. package/native/win32/mount_harness_win32.cpp +316 -0
  82. package/package.json +26 -0
  83. package/react-native.config.js +22 -0
@@ -0,0 +1,506 @@
1
+ // The Windows view layer: a view Fabric's mounting can drive.
2
+ //
3
+ // Deliberately the same shape as `gtk/RnView.h` and `appkit/RnAppKitView.h` --
4
+ // a view owns a tag and an absolute frame, does no layout of its own, and
5
+ // places each child at the rect the shadow tree already resolved. Yoga has done
6
+ // the layout by the time a mutation arrives, and a second layout system
7
+ // underneath it is the thing to avoid, not to add.
8
+ //
9
+ // Three decisions carry the weight here, and the first is the one that makes
10
+ // this platform different from the other two.
11
+ //
12
+ // **A view is not a window.** On GTK a view is a GtkWidget and on macOS an
13
+ // NSView; the obvious Windows translation is a child HWND per view, and it is
14
+ // wrong. An HWND is a kernel object with a message queue association, USER32
15
+ // caps a process at ten thousand of them by default, and a list of a few
16
+ // hundred rows would spend the budget. Worse, none of what the analogy promises
17
+ // arrives: an HWND clips rectangularly and cannot be rotated, so `transform`
18
+ // and rounded `overflow: hidden` would both have to be reimplemented anyway.
19
+ // So the host owns one HWND for the surface, and a view is a plain C++ object
20
+ // painted by the recursive walk in `paint`. That is closer to GTK's snapshot
21
+ // than to AppKit's layer tree, and it means this file owes the platform
22
+ // nothing but Direct2D. The one place a real window is still the right answer
23
+ // is `<TextInput>`, where an EDIT peer brings input methods and every Windows
24
+ // key binding with it -- the same bargain GTK's GtkText and AppKit's
25
+ // NSTextField make.
26
+ //
27
+ // **No flip.** Win32 and Direct2D both put the origin at the top left, which is
28
+ // where React Native puts it. The AppKit side needs `isFlipped` and a test that
29
+ // states the coordinate in numbers; here the coordinate systems already agree,
30
+ // and the only reason to mention it is that a reader coming from that file will
31
+ // go looking for the flip.
32
+ //
33
+ // **Painting is immediate, not retained.** DirectComposition would give a
34
+ // visual per view, which is the closer analogue of CALayer, and it would put
35
+ // transform and opacity in the compositor. It would also make the offscreen
36
+ // snapshot -- the thing that proves this layer paints correctly at all -- a
37
+ // second, separate path. One Direct2D walk renders identically to a window and
38
+ // to a WIC bitmap, so the picture the tests compare is made by the same code
39
+ // that draws the app.
40
+
41
+ #pragma once
42
+
43
+ #include <windows.h>
44
+
45
+ #include <cstdint>
46
+ #include <memory>
47
+ #include <string>
48
+ #include <vector>
49
+
50
+ // For RnImageFit and RnAccessibleInfo, both of which a view stores by value.
51
+ // Nothing heavy: each forward-declares its own Windows types.
52
+ #include "RnWin32Accessible.h"
53
+ #include "RnWin32Image.h"
54
+
55
+ // Direct2D's interfaces are structs, so the paint entry point can be declared
56
+ // without dragging <d2d1.h> -- and windows.h behind it -- into every
57
+ // translation unit that only wants to build a tree. The tests do exactly that.
58
+ struct ID2D1RenderTarget;
59
+
60
+ namespace basalt::win32 {
61
+
62
+ class RnWin32TextLayout;
63
+
64
+ struct RnRect {
65
+ float x = 0.0f;
66
+ float y = 0.0f;
67
+ float width = 0.0f;
68
+ float height = 0.0f;
69
+ };
70
+
71
+ // Content this view layer cannot draw itself, drawn by whoever can.
72
+ //
73
+ // One implementor today: the Skia surface behind a `<Canvas>`. It exists
74
+ // because of the split this directory is built around -- `basalt_win32_view`
75
+ // knows Direct2D and nothing else, and Skia lives a layer up in
76
+ // `basalt_win32_mounting`, which is the target that may or may not have been
77
+ // built with it. A `sk_sp<SkSurface>` member here would drag Skia's headers
78
+ // into the demo, the layout tool and every test that builds a view tree, and
79
+ // would make a host without Skia fail to compile rather than fail to draw.
80
+ //
81
+ // So the view holds an interface and calls it in the paint walk, at the point
82
+ // where it would draw an image. What that buys beyond the include: a canvas is
83
+ // clipped, transformed, faded and scrolled by exactly the code that does those
84
+ // things to everything else, and it appears in the offscreen snapshot the
85
+ // tests compare, because the snapshot is the same walk.
86
+ class RnWin32Painter {
87
+ public:
88
+ virtual ~RnWin32Painter() = default;
89
+
90
+ // Draw into a box `boxWidth` by `boxHeight` at the target's current origin
91
+ // -- the same contract as RnWin32Image::draw, and the same units, which are
92
+ // the view's own points.
93
+ virtual void draw(ID2D1RenderTarget *target, float boxWidth, float boxHeight) = 0;
94
+ };
95
+
96
+ class RnWin32View {
97
+ public:
98
+ explicit RnWin32View(int32_t tag);
99
+ ~RnWin32View();
100
+
101
+ RnWin32View(const RnWin32View &) = delete;
102
+ RnWin32View &operator=(const RnWin32View &) = delete;
103
+
104
+ // Fabric's tag for this view. Set once, at creation.
105
+ int32_t tag() const { return tag_; }
106
+
107
+ // --- Geometry ------------------------------------------------------------
108
+
109
+ // The frame the shadow tree resolved, in the parent's coordinates, top-left
110
+ // origin. Applied directly: nothing here recomputes it.
111
+ void setFrame(float x, float y, float width, float height);
112
+ const RnRect &frame() const { return frame_; }
113
+
114
+ // Shifts this view's children by (-x, -y), which is how a ScrollView scrolls.
115
+ //
116
+ // An offset applied while painting rather than by moving every child: the
117
+ // frames the mounting manager wrote stay exactly the ones Yoga produced, and
118
+ // hit testing subtracts the same offset, so the two cannot disagree. Both
119
+ // other platforms arrive at the same arrangement by their own route.
120
+ void setScrollOffset(float x, float y);
121
+ float scrollX() const { return scrollX_; }
122
+ float scrollY() const { return scrollY_; }
123
+
124
+ // The overlay scrollbars, as core/ScrollIndicator.h decides them: an offset
125
+ // along the track and a length, per axis, with a length of zero meaning "do
126
+ // not draw one". Painted above the children and below everything that is not
127
+ // the app's own -- see `paintScrollIndicators`.
128
+ void setScrollIndicators(float verticalOffset,
129
+ float verticalLength,
130
+ float horizontalOffset,
131
+ float horizontalLength);
132
+
133
+ // --- Appearance ----------------------------------------------------------
134
+
135
+ // Components are premultiplied-free 0..1, as React Native's colour components
136
+ // arrive. Passing hasColor=false means "no background", which is not the same
137
+ // as transparent black: a view with no background does not paint at all.
138
+ void setBackgroundColor(float red, float green, float blue, float alpha, bool hasColor);
139
+ bool hasBackgroundColor() const { return hasBackgroundColor_; }
140
+
141
+ void setOpacity(float opacity);
142
+ float opacity() const { return opacity_; }
143
+
144
+ // React Native's transform, as sixteen floats in CSS `matrix3d` order. Null
145
+ // clears it.
146
+ //
147
+ // Only the 2D affine part is drawn -- Direct2D has no perspective and neither
148
+ // does the GTK side today -- and the anchor is the view's centre, which is
149
+ // what React Native means by an untouched `transformOrigin`.
150
+ void setTransform(const float *matrix16);
151
+ bool hasTransform() const { return hasTransform_; }
152
+
153
+ // `overflow: hidden`. The background is clipped to the corner radius whether
154
+ // or not this is set; this is only about the children.
155
+ void setClipsChildren(bool clips);
156
+ bool clipsChildren() const { return clipsChildren_; }
157
+
158
+ // One circular radius on all four corners: what `borderRadius` alone asks
159
+ // for, and what the paint tests use.
160
+ void setCornerRadius(float radius);
161
+ // Per corner, each with its own horizontal and vertical radius -- what React
162
+ // Native resolves every radius prop to -- in the order describeTree prints
163
+ // them: top-left, top-right, bottom-right, bottom-left, horizontal then
164
+ // vertical. This host drew one circular radius until it took eight.
165
+ void setCornerRadii(const float radii[8]);
166
+ const float *cornerRadii() const { return cornerRadii_; }
167
+
168
+ // Per edge, in CSS's order -- top, right, bottom, left -- with `colours` as
169
+ // four RGBA quads in the same order. Drawn inside the view's rounded box and
170
+ // over its content, as GTK and AppKit draw them. An edge with no width or no
171
+ // alpha draws nothing, and a view with no such edge has no border at all:
172
+ // which is also exactly when describeTree prints `borderw=` and `borderc=`,
173
+ // decided the way GTK decides it.
174
+ void setBorders(const float widths[4], const float colours[16]);
175
+ bool hasBorders() const { return hasBorders_; }
176
+
177
+ // zIndex reorders painting and never the child list: Fabric's Insert and
178
+ // Remove carry an index into that list, so it has to stay in mutation order.
179
+ void setZIndex(int zIndex);
180
+ int zIndex() const { return zIndex_; }
181
+
182
+ // `pointerEvents`, which decides what a press can land on rather than what is
183
+ // drawn. CSS's four values, and React Native's. Read by `hitTest` and by
184
+ // nothing else.
185
+ //
186
+ // On the view rather than in the mounting manager because hit testing is a
187
+ // pure function of the view tree -- `hitTest` takes no React Native types and
188
+ // has nowhere to look a prop up. The AppKit host arranges it the same way.
189
+ enum class PointerEvents { Auto, None, BoxNone, BoxOnly };
190
+ void setPointerEvents(PointerEvents mode) { pointerEvents_ = mode; }
191
+ PointerEvents pointerEvents() const { return pointerEvents_; }
192
+
193
+ // Whether this view takes keyboard focus, and therefore whether Tab stops on
194
+ // it.
195
+ //
196
+ // React Native has a `focusable` prop and it does not reach this platform:
197
+ // ReactCommon parses it only into Android's and tvOS's HostPlatformViewProps,
198
+ // and the C++ host's is a bare alias of BaseViewProps. What does reach here
199
+ // is `accessible`, which is what <Pressable> sets on everything it renders.
200
+ // See Win32Focus.h.
201
+ void setFocusable(bool focusable) { focusable_ = focusable; }
202
+ bool focusable() const { return focusable_; }
203
+
204
+ // Whether to paint a focus ring. A view is not its own window here, so there
205
+ // is no Win32 focus to ask about -- Win32FocusManager owns the answer and
206
+ // sets it, exactly as it owns the Tab order.
207
+ void setShowsFocusRing(bool shows) { showsFocusRing_ = shows; }
208
+ bool showsFocusRing() const { return showsFocusRing_; }
209
+
210
+ // `display: none`. A hidden view is neither painted nor hit, and neither are
211
+ // its children. Distinct from `opacity: 0`, which paints nothing and is still
212
+ // there to be pressed.
213
+ void setHidden(bool hidden);
214
+ bool hidden() const { return hidden_; }
215
+
216
+ // `backfaceVisibility: 'hidden'`. A view whose transform has turned it away
217
+ // from the viewer is not painted, and nor are its children -- which is what
218
+ // a card's back should do, and what takes it out of hit testing too, since
219
+ // `hitTest` skips a hidden view.
220
+ //
221
+ // Whether it has turned away is `basalt::facesAway`; see core/Backface.h for
222
+ // why the rule is shared rather than left to each toolkit.
223
+ void setHidesBackFace(bool hides);
224
+
225
+ // The EDIT control a <TextInput> mounted behind this view, or null.
226
+ //
227
+ // The view neither owns it nor draws it -- `Win32TextInputManager` does both
228
+ // -- and the only thing it is for is `describeTree`: a text field's content
229
+ // lives in its peer rather than in a layout, so without this it would be
230
+ // invisible to every test that reads the tree, and the Windows dump would
231
+ // differ from the other two hosts' for a field that was working perfectly.
232
+ // GTK keeps the same back pointer to its GtkText for the same reason.
233
+ void setEditablePeer(HWND control) { editablePeer_ = control; }
234
+ HWND editablePeer() const { return editablePeer_; }
235
+
236
+ // The view's `nativeID`. Kept for the one thing on this platform that reads
237
+ // it: a hidden title bar asks which views are drag regions, and
238
+ // <TitleBar.DragRegion> says so through this prop because it is the one a
239
+ // plain View already carries all the way to the host. See
240
+ // Win32TitleBarLayout.h.
241
+ void setNativeId(std::string nativeId) { nativeId_ = std::move(nativeId); }
242
+ const std::string &nativeId() const { return nativeId_; }
243
+
244
+ // --- Content ---------------------------------------------------------------
245
+
246
+ // The paragraph this view draws, or null for a view that draws none.
247
+ //
248
+ // Text sits above the background and below any children, which is the order
249
+ // `<Text>` with nested views expects and the order the GTK snapshot uses.
250
+ // Held as a shared_ptr because a paragraph is expensive to build and a
251
+ // mutation that changed only layout must not rebuild one -- the mounting
252
+ // manager will hand the same object back.
253
+ void setTextLayout(std::shared_ptr<RnWin32TextLayout> layout);
254
+ const std::shared_ptr<RnWin32TextLayout> &textLayout() const { return textLayout_; }
255
+
256
+ // The decoded pixels of an <Image>, and how they fill this view's frame. Pass
257
+ // null to clear.
258
+ //
259
+ // Shared rather than owned for the same reason as the paragraph: a mutation
260
+ // that changed only layout must not restart a load, or an <Image> flickers
261
+ // whenever its parent resizes.
262
+ void setImage(std::shared_ptr<RnWin32Image> image, RnImageFit fit);
263
+
264
+ // `tintColor`: recolours the image keeping its alpha. Separate from the image
265
+ // because the tint arrives from the props and the image from a loader, and
266
+ // either can land first.
267
+ void setImageTint(bool hasTint, const float components[4]);
268
+ const std::shared_ptr<RnWin32Image> &image() const { return image_; }
269
+ RnImageFit imageFit() const { return imageFit_; }
270
+
271
+ // The painter for this view, or null. Drawn just above where an image would
272
+ // be: a `<Canvas>` carries neither an image nor a paragraph, so the two
273
+ // never meet and the order is a decision rather than a conflict.
274
+ //
275
+ // Shared rather than owned because the thing on the other end outlives a
276
+ // single mutation -- it holds the surface the last frame was rendered into,
277
+ // and rebuilding it per transaction would throw that frame away.
278
+ void setPainter(std::shared_ptr<RnWin32Painter> painter);
279
+ const std::shared_ptr<RnWin32Painter> &painter() const { return painter_; }
280
+
281
+ // --- Controls ----------------------------------------------------------------
282
+ //
283
+ // <ActivityIndicator>, <RefreshControl> and <Switch>, which the other two
284
+ // hosts mount a real toolkit widget for and this one draws.
285
+ //
286
+ // Not an oversight and not laziness: Windows has no spinner control at all
287
+ // (the closest is a marquee progress bar, which is a moving stripe), and no
288
+ // switch either -- the Win32 approximation is a checkbox, and WinUI's toggle
289
+ // is not reachable from a plain HWND app. Both would also be child windows,
290
+ // which is the one thing this view layer is built to avoid; see the note at
291
+ // the top about USER32's handle budget. So they are painted, in the same
292
+ // Direct2D walk as everything else, which is also what makes them appear in
293
+ // the offscreen snapshot the tests compare.
294
+ enum class Control { None, Spinner, Switch };
295
+
296
+ // Everything the drawing needs, which is core/DesktopControls.h's
297
+ // ControlState with the parts that are not pixels left out. A plain struct
298
+ // rather than that type because this target does not link React Native --
299
+ // the demo and half the tests build a view tree with no Fabric anywhere.
300
+ struct ControlStyle {
301
+ bool on = false;
302
+ bool disabled = false;
303
+ bool large = false;
304
+ bool hidesWhenStopped = true;
305
+ bool hasThumb = false;
306
+ float thumb[4] = {0.0f, 0.0f, 0.0f, 0.0f};
307
+ bool hasTrackOn = false;
308
+ float trackOn[4] = {0.0f, 0.0f, 0.0f, 0.0f};
309
+ bool hasTrackOff = false;
310
+ float trackOff[4] = {0.0f, 0.0f, 0.0f, 0.0f};
311
+ };
312
+
313
+ // `description` is what describeTree prints, written by
314
+ // core/DesktopControls.h so that three hosts cannot describe the same switch
315
+ // differently.
316
+ void setControl(Control kind, const ControlStyle &style, std::string description);
317
+ Control control() const { return control_; }
318
+ const ControlStyle &controlStyle() const { return controlStyle_; }
319
+
320
+ // Whether anything in this subtree is a spinner that is turning, and so
321
+ // whether the window has to keep repainting. The host asks the surface root
322
+ // once a frame; see main_win32.cpp.
323
+ bool hasAnimatingSpinner() const;
324
+
325
+ // --- React DevTools' overlay -------------------------------------------------
326
+ //
327
+ // The rectangles a `DebuggingOverlay` draws: an inspected element's blue box,
328
+ // or an outline around everything that just re-rendered. Set from
329
+ // Win32MountingManager, which parses them; see core/DebuggingOverlay.h.
330
+ struct Highlight {
331
+ float x = 0.0f;
332
+ float y = 0.0f;
333
+ float width = 0.0f;
334
+ float height = 0.0f;
335
+ float color[4] = {0.0f, 0.0f, 0.0f, 0.0f};
336
+ bool filled = false;
337
+ };
338
+
339
+ void setHighlights(std::vector<Highlight> highlights);
340
+ const std::vector<Highlight> &highlights() const { return highlights_; }
341
+
342
+ // --- Accessibility ---------------------------------------------------------
343
+
344
+ // What a screen reader is told. Unlike GTK, where the role is a
345
+ // construct-only property and so cannot change after mount, every field here
346
+ // is free to change at any time -- UI Automation pulls rather than being
347
+ // pushed to, so the provider simply reads the current value. See
348
+ // RnWin32Accessible.h.
349
+ void setAccessibleInfo(const RnAccessibleInfo &info);
350
+ const RnAccessibleInfo &accessibleInfo() const { return accessible_; }
351
+
352
+ // A UIA provider over this view, or null when the view is not an
353
+ // accessibility element. The caller owns a reference and must Release it.
354
+ IRawElementProviderSimple *createAccessibleProvider() const;
355
+
356
+ // --- Tree ----------------------------------------------------------------
357
+ //
358
+ // A view does not own its children. The mounting registry owns every view,
359
+ // which is what makes Fabric's "a Remove detaches but must not destroy" rule
360
+ // structural here rather than a reference count that has to be got right: a
361
+ // detached view is simply a view with no parent, still in the registry, ready
362
+ // for the Insert that reparents it. Callers with no registry -- the demo, the
363
+ // tests -- hold their views in a vector of unique_ptr.
364
+
365
+ void insertChild(RnWin32View *child, int index);
366
+ void removeChild(RnWin32View *child);
367
+ const std::vector<RnWin32View *> &children() const { return children_; }
368
+ RnWin32View *parent() const { return parent_; }
369
+
370
+ // The children in the order they are painted: mutation order, restacked by
371
+ // zIndex where any child has one. Painting walks this forwards and hit
372
+ // testing walks it backwards, and they call the same function so that the
373
+ // view a click lands on is always the view drawn on top.
374
+ //
375
+ // Returns a copy. Painting used to avoid it in the common case; one
376
+ // definition of paint order is worth more than the allocation.
377
+ std::vector<RnWin32View *> childrenInPaintOrder() const;
378
+
379
+ // --- Geometry, resolved ----------------------------------------------------
380
+
381
+ // This view's local-to-parent transform, as the six numbers of a 2D affine
382
+ // matrix in Direct2D's Matrix3x2F order: the frame's translation, with any
383
+ // `transform` composed in about the view's centre.
384
+ //
385
+ // Public, and the only place that composition is written. `paint` builds its
386
+ // Direct2D matrix from these and `hitTest` inverts them, so the two cannot
387
+ // disagree about where a view is -- which would show up as a rotated button
388
+ // that is clickable where it used to be, and is exactly the bug
389
+ // `docs/DECISIONS.md` records GTK hitting.
390
+ void localToParent(float out[6]) const;
391
+
392
+ // A point in `root`'s coordinates, expressed in this view's own.
393
+ //
394
+ // Walks up composing `localToParent` and inverts the chain, which is the
395
+ // same matrix `hitTest` inverts one level at a time on the way down -- so a
396
+ // press and the coordinates it reports cannot disagree about where a view
397
+ // is. Answers false when the chain has no inverse, which `scale: 0` produces
398
+ // and which has no sensible point in it.
399
+ bool pageToLocal(const RnWin32View *root, float pageX, float pageY,
400
+ float &outX, float &outY) const;
401
+
402
+ // --- Painting ------------------------------------------------------------
403
+
404
+ // Paints this view and its subtree into `target`, which must be between
405
+ // BeginDraw and EndDraw. The target's transform on entry is the one this
406
+ // view's frame is relative to, and it is restored before returning.
407
+ void paint(ID2D1RenderTarget *target) const;
408
+
409
+ // --- Reporting -----------------------------------------------------------
410
+
411
+ // One line per view, two spaces of indent per level.
412
+ //
413
+ // The format is a cross-platform contract, not a debug convenience:
414
+ // `scripts/compare_hosts.sh` diffs two hosts' dumps line by line, so the
415
+ // fields and their order match `rn_view_describe_into` in gtk/RnView.cpp and
416
+ // `describeInto:depth:` in appkit/RnAppKitView.mm exactly. A field added in
417
+ // the wrong place makes every line differ.
418
+ std::string describeTree() const;
419
+
420
+ private:
421
+ // Paints the children into whatever space the target's transform is already
422
+ // in, which is this view's own. Separate from `paint` so the clip and the
423
+ // scroll offset have an obvious scope, and so no Direct2D type appears above.
424
+ void paintChildren(ID2D1RenderTarget *target) const;
425
+ // The spinner and the switch, drawn into this view's own coordinates.
426
+ void paintControl(ID2D1RenderTarget *target) const;
427
+ // React DevTools' overlay, over everything including the children.
428
+ void paintHighlights(ID2D1RenderTarget *target) const;
429
+ // The overlay scrollbars, in this view's own coordinates.
430
+ void paintScrollIndicators(ID2D1RenderTarget *target) const;
431
+ // Recomputes whether the back face is showing, and hides or shows the view.
432
+ void updateBackFace();
433
+ void applyVisibility();
434
+ void paintBorders(ID2D1RenderTarget *target) const;
435
+ void describeInto(std::string &out, int depth) const;
436
+
437
+ int32_t tag_;
438
+ RnRect frame_;
439
+ RnWin32View *parent_ = nullptr;
440
+ std::vector<RnWin32View *> children_;
441
+
442
+ bool hasBackgroundColor_ = false;
443
+ float backgroundColor_[4] = {0.0f, 0.0f, 0.0f, 0.0f};
444
+ float opacity_ = 1.0f;
445
+ bool clipsChildren_ = false;
446
+ // See setCornerRadii and setBorders for the orders these are kept in.
447
+ float cornerRadii_[8] = {};
448
+ bool hasCornerRadii_ = false;
449
+ float borderWidths_[4] = {};
450
+ float borderColours_[16] = {};
451
+ bool hasBorders_ = false;
452
+ HWND editablePeer_ = nullptr;
453
+ std::string nativeId_;
454
+ int zIndex_ = 0;
455
+ PointerEvents pointerEvents_ = PointerEvents::Auto;
456
+ bool focusable_ = false;
457
+ bool showsFocusRing_ = false;
458
+ float scrollX_ = 0.0f;
459
+ float scrollY_ = 0.0f;
460
+ float indicatorVerticalOffset_ = 0.0f;
461
+ float indicatorVerticalLength_ = 0.0f;
462
+ float indicatorHorizontalOffset_ = 0.0f;
463
+ float indicatorHorizontalLength_ = 0.0f;
464
+
465
+ bool hidden_ = false;
466
+ bool hidesBackFace_ = false;
467
+ // Set by the back-face rule rather than by the app, so that clearing the
468
+ // rule does not un-hide a view the app itself hid.
469
+ bool hiddenByBackFace_ = false;
470
+ bool hiddenByApp_ = false;
471
+ std::vector<Highlight> highlights_;
472
+ Control control_ = Control::None;
473
+ ControlStyle controlStyle_;
474
+ std::string controlDescription_;
475
+ std::shared_ptr<RnWin32TextLayout> textLayout_;
476
+ std::shared_ptr<RnWin32Image> image_;
477
+ std::shared_ptr<RnWin32Painter> painter_;
478
+ RnImageFit imageFit_ = RnImageFit::Cover;
479
+ bool hasImageTint_ = false;
480
+ float imageTint_[4] = {0.0f, 0.0f, 0.0f, 0.0f};
481
+ RnAccessibleInfo accessible_;
482
+ bool hasTransform_ = false;
483
+ // The 2D affine part, in the order Direct2D's Matrix3x2F stores it:
484
+ // _11, _12, _21, _22, _31, _32 -- which is also the order CSS writes a
485
+ // matrix() and the six numbers describeTree prints.
486
+ float transform_[6] = {1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f};
487
+ };
488
+
489
+ // The deepest view at a point, in `root`'s own coordinates, or null for a miss.
490
+ //
491
+ // A free function because it is a pure function of the view tree and nothing
492
+ // else, which is also what makes it testable: hit testing is the part of input
493
+ // most likely to be quietly wrong, and it needs no mouse to exercise. It lives
494
+ // here rather than with a touch dispatcher so that testing it needs no React
495
+ // Native, which is the arrangement the AppKit side settled on too.
496
+ //
497
+ // Windows differs from both other platforms in having to do this at all. GTK
498
+ // gets picking from `gtk_widget_pick` and macOS from AppKit's own hit testing,
499
+ // so on those two the transform has to be pushed *into* the toolkit's geometry
500
+ // or clicks land in the wrong place. Here there is no toolkit geometry to push
501
+ // it into, so this inverts each view's matrix on the way down -- which means a
502
+ // rotated view is clickable where it is drawn, and it is `localToParent` that
503
+ // guarantees "where it is drawn" means the same thing to both.
504
+ RnWin32View *hitTest(RnWin32View *root, float x, float y);
505
+
506
+ } // namespace basalt::win32
@@ -0,0 +1,76 @@
1
+ #include "Win32AnimationChoreographer.h"
2
+
3
+ #include <windows.h>
4
+
5
+ #include <chrono>
6
+
7
+ namespace basalt {
8
+ namespace {
9
+
10
+ // Roughly sixty hertz. SetTimer's resolution is the message queue's -- it
11
+ // cannot do better than about ten to sixteen milliseconds and it coalesces --
12
+ // so this is an upper bound on the frame rate rather than a promise of one.
13
+ // See the header for what would be needed to do it properly.
14
+ constexpr UINT kFrameIntervalMs = 16;
15
+
16
+ // SetTimer's callback carries the timer id and nothing else, so the
17
+ // choreographer has to be found from it. One host means one choreographer, and
18
+ // a map would be pretending otherwise.
19
+ Win32AnimationChoreographer *&currentChoreographer() {
20
+ static Win32AnimationChoreographer *instance = nullptr;
21
+ return instance;
22
+ }
23
+
24
+ void CALLBACK onTimer(HWND, UINT, UINT_PTR, DWORD) {
25
+ if (auto *choreographer = currentChoreographer()) {
26
+ choreographer->onFrame();
27
+ }
28
+ }
29
+
30
+ } // namespace
31
+
32
+ Win32AnimationChoreographer::~Win32AnimationChoreographer() {
33
+ detach();
34
+ }
35
+
36
+ void Win32AnimationChoreographer::resume() {
37
+ if (running_.exchange(true)) {
38
+ return;
39
+ }
40
+ currentChoreographer() = this;
41
+ // A timer with no window: the callback is delivered as WM_TIMER to the
42
+ // thread's queue and dispatched by the loop, which means it lands on the UI
43
+ // thread like everything else and needs no synchronisation of its own.
44
+ timer_ = static_cast<std::uintptr_t>(SetTimer(nullptr, 0, kFrameIntervalMs, onTimer));
45
+ }
46
+
47
+ void Win32AnimationChoreographer::pause() {
48
+ if (!running_.exchange(false)) {
49
+ return;
50
+ }
51
+ if (timer_ != 0) {
52
+ KillTimer(nullptr, static_cast<UINT_PTR>(timer_));
53
+ timer_ = 0;
54
+ }
55
+ }
56
+
57
+ void Win32AnimationChoreographer::detach() {
58
+ pause();
59
+ if (currentChoreographer() == this) {
60
+ currentChoreographer() = nullptr;
61
+ }
62
+ }
63
+
64
+ void Win32AnimationChoreographer::onFrame() {
65
+ if (!running_.load()) {
66
+ return;
67
+ }
68
+ // A steady clock rather than the wall clock: an animation must not jump when
69
+ // the machine's time is corrected or the clocks change.
70
+ const auto now = std::chrono::steady_clock::now().time_since_epoch();
71
+ const auto milliseconds =
72
+ std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(now);
73
+ onAnimationFrame(static_cast<facebook::react::AnimationTimestamp>(milliseconds.count()));
74
+ }
75
+
76
+ } // namespace basalt
@@ -0,0 +1,61 @@
1
+ // Animation frames, on Windows.
2
+ //
3
+ // React Native's `AnimationChoreographer` is asked to call back once per frame
4
+ // while something is animating, and to stop when nothing is. macOS gets that
5
+ // from a CADisplayLink and GTK from the widget's frame clock -- both of which
6
+ // are told by the compositor when a frame is due.
7
+ //
8
+ // Win32 has no such callback for an ordinary window. The honest options are a
9
+ // timer, or `DwmGetCompositionTimingInfo` plus `DwmFlush` on a thread that
10
+ // blocks until the compositor is ready. The second is genuinely display-locked
11
+ // and is what a serious animation implementation should use; it also needs its
12
+ // own thread, because DwmFlush blocks, and a blocked UI thread is worse than a
13
+ // slightly wrong frame interval.
14
+ //
15
+ // So this is a multimedia timer at sixty hertz, and it is worth being clear
16
+ // that this is a placeholder rather than a design. `docs/BACKLOG.md` already
17
+ // records that worklets and Reanimated run on a sixteen-millisecond timer on
18
+ // *both* other desktops for a related reason -- each platform's display link
19
+ // belongs to React Native's own choreographer and pauses when React Native has
20
+ // no animation of its own -- so this is a third instance of the same gap rather
21
+ // than a new one.
22
+ //
23
+ // What it does get right is the pausing. A timer that runs whether or not
24
+ // anything is animating wakes the process sixty times a second forever, which
25
+ // is the objection `docs/DECISIONS.md` raises against
26
+ // `gtk_widget_add_tick_callback` and which matters more on a laptop than the
27
+ // frame interval does.
28
+
29
+ #pragma once
30
+
31
+ #include <react/renderer/animationbackend/AnimationChoreographer.h>
32
+
33
+ #include <atomic>
34
+ #include <cstdint>
35
+
36
+ namespace basalt {
37
+
38
+ class Win32AnimationChoreographer final : public facebook::react::AnimationChoreographer {
39
+ public:
40
+ Win32AnimationChoreographer() = default;
41
+ ~Win32AnimationChoreographer() override;
42
+
43
+ void resume() override;
44
+ void pause() override;
45
+
46
+ // Stops the timer for good. Called on the way out, before the manager this
47
+ // feeds is released.
48
+ void detach();
49
+
50
+ // Called from the timer, on the UI thread.
51
+ void onFrame();
52
+
53
+ private:
54
+ std::atomic<bool> running_{false};
55
+ // A UINT_PTR from SetTimer, held as an integer so this header needs no
56
+ // windows.h -- which would otherwise reach every translation unit that
57
+ // constructs a host.
58
+ std::uintptr_t timer_{0};
59
+ };
60
+
61
+ } // namespace basalt