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,93 @@
1
+ // The decoded pixels of an <Image>, and how they fill a frame.
2
+ //
3
+ // Device-independent on purpose. Direct2D's own ID2D1Bitmap belongs to the
4
+ // render target that made it, so holding one would tie an image to a window and
5
+ // make it useless to the offscreen snapshot -- and would have to be rebuilt
6
+ // whenever a device is lost. A WIC bitmap is the Windows equivalent of GTK's
7
+ // GdkTexture and macOS's CGImage: decoded once, owned by nobody in particular,
8
+ // converted to a device bitmap at paint time and cached there.
9
+ //
10
+ // Only the decode and the draw are here. Fetching the bytes -- file, data URI,
11
+ // http -- is `core/ImageBytes.cpp` in the shared half, because a URI means the
12
+ // same thing on every desktop; see `docs/DECISIONS.md`.
13
+
14
+ #pragma once
15
+
16
+ #include <cstdint>
17
+ #include <memory>
18
+ #include <string>
19
+
20
+ struct ID2D1RenderTarget;
21
+ struct IWICBitmap;
22
+ struct ID2D1Bitmap;
23
+
24
+ namespace basalt::win32 {
25
+
26
+ // How an image fills its frame. Mirrors React Native's ImageResizeMode, minus
27
+ // Repeat, which needs a tiled draw rather than one image draw.
28
+ enum class RnImageFit {
29
+ Cover,
30
+ Contain,
31
+ Stretch,
32
+ Center,
33
+ // Tiled from the top left at the image's own size, which is what CSS
34
+ // `repeat` and React Native's `resizeMode: 'repeat'` mean.
35
+ Repeat,
36
+ };
37
+
38
+ const char *imageFitName(RnImageFit fit);
39
+
40
+ class RnWin32Image {
41
+ public:
42
+ // Straight from pixels, premultiplied BGRA with a stride of width * 4. What
43
+ // the tests use, and what a decoder that is not WIC would hand over.
44
+ static std::shared_ptr<RnWin32Image>
45
+ fromPixels(unsigned width, unsigned height, const uint8_t *bgra);
46
+
47
+ // Decodes PNG, JPEG, GIF, BMP, TIFF and anything else WIC has a codec for.
48
+ // Null if the bytes are not an image WIC understands.
49
+ static std::shared_ptr<RnWin32Image> fromEncodedBytes(const uint8_t *data, size_t size);
50
+
51
+ ~RnWin32Image();
52
+
53
+ RnWin32Image(const RnWin32Image &) = delete;
54
+ RnWin32Image &operator=(const RnWin32Image &) = delete;
55
+
56
+ unsigned width() const { return width_; }
57
+ unsigned height() const { return height_; }
58
+
59
+ // Draws into a box `boxWidth` by `boxHeight` at the target's current origin,
60
+ // scaled and positioned by `fit`.
61
+ // `tint`, when given, recolours the image keeping its alpha -- one silhouette
62
+ // asset drawn in any colour, which is what `tintColor` is for. Four floats,
63
+ // red green blue alpha, which is what the caller already holds.
64
+ //
65
+ // Not a `D2D1_COLOR_F`, though that is what it becomes: this header declares
66
+ // its Direct2D types rather than including d2d1.h, and that one is a typedef
67
+ // of a typedef -- `D3DCOLORVALUE`, itself `_D3DCOLORVALUE` -- so it cannot be
68
+ // forward-declared at all. Declaring `struct D2D_COLOR_F` instead defines a
69
+ // *different* type of that name and every later include of d2d1.h fails on
70
+ // the redefinition, which is exactly what it did. Floats cannot collide with
71
+ // anything, and the colour is built where d2d1.h is already in scope.
72
+ void draw(ID2D1RenderTarget *target,
73
+ float boxWidth,
74
+ float boxHeight,
75
+ RnImageFit fit,
76
+ const float *tint = nullptr) const;
77
+
78
+ private:
79
+ RnWin32Image() = default;
80
+
81
+ IWICBitmap *bitmap_ = nullptr;
82
+ unsigned width_ = 0;
83
+ unsigned height_ = 0;
84
+
85
+ // The device bitmap for the target that last drew this image. An <Image> in a
86
+ // list is drawn into the same target every frame, so this converts once; a
87
+ // second target -- the snapshot -- replaces it rather than growing a map,
88
+ // because nothing here draws into two targets in the same frame.
89
+ mutable ID2D1Bitmap *deviceBitmap_ = nullptr;
90
+ mutable ID2D1RenderTarget *deviceTarget_ = nullptr;
91
+ };
92
+
93
+ } // namespace basalt::win32
@@ -0,0 +1,310 @@
1
+ #include "RnWin32TextLayout.h"
2
+
3
+ #include "Win32Strings.h"
4
+
5
+ #include <windows.h>
6
+
7
+ #include <d2d1.h>
8
+ #include <dwrite.h>
9
+ #include <wrl/client.h>
10
+
11
+ #include <algorithm>
12
+ #include <vector>
13
+
14
+ using Microsoft::WRL::ComPtr;
15
+
16
+ namespace basalt::win32 {
17
+ namespace {
18
+
19
+ // Whether a run needs a brush of its own. Exact rather than tolerant: these
20
+ // numbers come from the same ColorComponents on both sides of the comparison,
21
+ // so anything but equality would be inventing a tolerance to hide a bug.
22
+ bool sameColour(const float a[4], const float b[4]) {
23
+ return a[0] == b[0] && a[1] == b[1] && a[2] == b[2] && a[3] == b[3];
24
+ }
25
+
26
+ // One shared DirectWrite factory for the process.
27
+ //
28
+ // DWRITE_FACTORY_TYPE_SHARED rather than ISOLATED: the shared factory caches
29
+ // font data across everything in the process and is documented thread-safe,
30
+ // which is what lets measurement happen on Fabric's layout thread and painting
31
+ // on the UI thread with no lock between them. The GTK side cannot do this --
32
+ // see the header.
33
+ IDWriteFactory *dwriteFactory() {
34
+ static IDWriteFactory *factory = [] {
35
+ IDWriteFactory *created = nullptr;
36
+ DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED,
37
+ __uuidof(IDWriteFactory),
38
+ reinterpret_cast<IUnknown **>(&created));
39
+ return created;
40
+ }();
41
+ return factory;
42
+ }
43
+
44
+ DWRITE_TEXT_ALIGNMENT toDWriteAlignment(RnTextAlign align) {
45
+ switch (align) {
46
+ case RnTextAlign::Center:
47
+ return DWRITE_TEXT_ALIGNMENT_CENTER;
48
+ case RnTextAlign::Right:
49
+ return DWRITE_TEXT_ALIGNMENT_TRAILING;
50
+ case RnTextAlign::Justified:
51
+ return DWRITE_TEXT_ALIGNMENT_JUSTIFIED;
52
+ case RnTextAlign::Left:
53
+ break;
54
+ }
55
+ return DWRITE_TEXT_ALIGNMENT_LEADING;
56
+ }
57
+
58
+ // What "unconstrained" means to DirectWrite. It has no notion of an infinite
59
+ // width and will happily produce NaN metrics from one, so this is a width no
60
+ // paragraph reaches rather than FLT_MAX.
61
+ constexpr float kUnconstrained = 1.0e6f;
62
+
63
+ } // namespace
64
+
65
+ std::shared_ptr<RnWin32TextLayout>
66
+ RnWin32TextLayout::create(std::string utf8Text, const RnTextStyle &style, int maximumNumberOfLines) {
67
+ IDWriteFactory *factory = dwriteFactory();
68
+ if (factory == nullptr) {
69
+ return nullptr;
70
+ }
71
+
72
+ const std::wstring family = widen(style.fontFamily);
73
+
74
+ IDWriteTextFormat *format = nullptr;
75
+ const HRESULT hr = factory->CreateTextFormat(
76
+ family.empty() ? L"Segoe UI" : family.c_str(),
77
+ nullptr, // the system font collection
78
+ style.bold ? DWRITE_FONT_WEIGHT_BOLD : DWRITE_FONT_WEIGHT_NORMAL,
79
+ style.italic ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL,
80
+ DWRITE_FONT_STRETCH_NORMAL,
81
+ // In DIPs at 96 dpi, which is React Native's density-independent pixel
82
+ // exactly. Nothing here multiplies by a scale factor, and nothing should:
83
+ // the render target carries the display scale, the same division of
84
+ // labour `docs/DECISIONS.md` records for Pango's absolute sizes.
85
+ style.fontSize,
86
+ L"",
87
+ &format);
88
+ if (FAILED(hr) || format == nullptr) {
89
+ return nullptr;
90
+ }
91
+
92
+ format->SetTextAlignment(toDWriteAlignment(style.align));
93
+ format->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR);
94
+ format->SetWordWrapping(DWRITE_WORD_WRAPPING_WRAP);
95
+ if (style.lineHeight > 0.0f) {
96
+ // Baseline at 80% of the line box is DirectWrite's own recommendation for
97
+ // uniform spacing, and it is what an unset baseline would have produced.
98
+ format->SetLineSpacing(
99
+ DWRITE_LINE_SPACING_METHOD_UNIFORM, style.lineHeight, style.lineHeight * 0.8f);
100
+ }
101
+
102
+ auto layout = std::shared_ptr<RnWin32TextLayout>(new RnWin32TextLayout());
103
+ layout->utf8_ = std::move(utf8Text);
104
+ layout->utf16_ = widen(layout->utf8_);
105
+ layout->style_ = style;
106
+ layout->maximumNumberOfLines_ = maximumNumberOfLines > 0 ? maximumNumberOfLines : 0;
107
+ layout->format_ = format;
108
+ return layout;
109
+ }
110
+
111
+ std::shared_ptr<RnWin32TextLayout>
112
+ RnWin32TextLayout::createFromRuns(const std::vector<RnTextRun> &runs, int maximumNumberOfLines) {
113
+ if (runs.empty()) {
114
+ return create(std::string{}, RnTextStyle{}, maximumNumberOfLines);
115
+ }
116
+
117
+ // The first run's style is the paragraph's: alignment and line spacing belong
118
+ // to the whole thing rather than to a span, and React Native applies them
119
+ // that way too.
120
+ std::string joined;
121
+ for (const auto &run : runs) {
122
+ joined += run.text;
123
+ }
124
+
125
+ auto layout = create(joined, runs.front().style, maximumNumberOfLines);
126
+ if (layout == nullptr) {
127
+ return nullptr;
128
+ }
129
+
130
+ // One style needs no ranges, and skipping them keeps the common case -- a
131
+ // plain <Text> -- free of per-range work.
132
+ if (runs.size() == 1) {
133
+ return layout;
134
+ }
135
+
136
+ // Ranges are counted in UTF-16 code units, not bytes and not code points, so
137
+ // each run's extent has to be measured after conversion. Getting this wrong
138
+ // shifts every style after the first emoji.
139
+ unsigned start = 0;
140
+ for (const auto &run : runs) {
141
+ const unsigned length = static_cast<unsigned>(widen(run.text).size());
142
+ layout->runs_.push_back(ResolvedRun{start, length, run.style});
143
+ start += length;
144
+ }
145
+ return layout;
146
+ }
147
+
148
+ RnWin32TextLayout::~RnWin32TextLayout() {
149
+ if (format_ != nullptr) {
150
+ format_->Release();
151
+ format_ = nullptr;
152
+ }
153
+ }
154
+
155
+ IDWriteTextLayout *RnWin32TextLayout::buildLayout(float maxWidth, float maxHeight) const {
156
+ IDWriteFactory *factory = dwriteFactory();
157
+ if (factory == nullptr || format_ == nullptr) {
158
+ return nullptr;
159
+ }
160
+
161
+ const float width = maxWidth < 0.0f ? kUnconstrained : maxWidth;
162
+ const float height = maxHeight < 0.0f ? kUnconstrained : maxHeight;
163
+
164
+ IDWriteTextLayout *layout = nullptr;
165
+ const HRESULT hr = factory->CreateTextLayout(
166
+ utf16_.c_str(), static_cast<UINT32>(utf16_.size()), format_, width, height, &layout);
167
+ if (FAILED(hr)) {
168
+ return nullptr;
169
+ }
170
+
171
+ // Per-span styling, applied here rather than at creation so that measurement
172
+ // and painting see exactly the same runs -- which is the whole reason this
173
+ // function exists.
174
+ for (const auto &run : runs_) {
175
+ const DWRITE_TEXT_RANGE range{run.start, run.length};
176
+ const std::wstring family = widen(run.style.fontFamily);
177
+ if (!family.empty()) {
178
+ layout->SetFontFamilyName(family.c_str(), range);
179
+ }
180
+ layout->SetFontSize(run.style.fontSize, range);
181
+ layout->SetFontWeight(
182
+ run.style.bold ? DWRITE_FONT_WEIGHT_BOLD : DWRITE_FONT_WEIGHT_NORMAL, range);
183
+ layout->SetFontStyle(
184
+ run.style.italic ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL, range);
185
+ }
186
+ return layout;
187
+ }
188
+
189
+ float RnWin32TextLayout::applyLineLimit(IDWriteTextLayout *layout) const {
190
+ if (layout == nullptr || maximumNumberOfLines_ <= 0) {
191
+ return -1.0f;
192
+ }
193
+
194
+ UINT32 lineCount = 0;
195
+ // The documented two-call form: the first is expected to fail with
196
+ // E_NOT_SUFFICIENT_BUFFER and to fill in the count.
197
+ layout->GetLineMetrics(nullptr, 0, &lineCount);
198
+ if (lineCount == 0) {
199
+ return -1.0f;
200
+ }
201
+
202
+ std::vector<DWRITE_LINE_METRICS> lines(lineCount);
203
+ if (FAILED(layout->GetLineMetrics(lines.data(), lineCount, &lineCount))) {
204
+ return -1.0f;
205
+ }
206
+
207
+ const UINT32 limit = static_cast<UINT32>(maximumNumberOfLines_);
208
+ if (lineCount <= limit) {
209
+ // A limit the paragraph already fits inside is not a truncation, and must
210
+ // not become one. This is the shape of the trap `docs/DECISIONS.md` records
211
+ // Pango springing: there, setting the *default* ellipsize mode with no
212
+ // height collapsed every wrapping paragraph to a single line. DirectWrite
213
+ // will not trim without a height, so declining to set one here is what
214
+ // makes the same default safe.
215
+ return -1.0f;
216
+ }
217
+
218
+ float limitHeight = 0.0f;
219
+ for (UINT32 i = 0; i < limit; i++) {
220
+ limitHeight += lines[i].height;
221
+ }
222
+
223
+ layout->SetMaxHeight(limitHeight);
224
+
225
+ // An ellipsis on the last line that fits, which is React Native's default
226
+ // `ellipsizeMode: 'tail'`.
227
+ ComPtr<IDWriteInlineObject> ellipsis;
228
+ if (SUCCEEDED(dwriteFactory()->CreateEllipsisTrimmingSign(layout, ellipsis.GetAddressOf()))) {
229
+ DWRITE_TRIMMING trimming{};
230
+ trimming.granularity = DWRITE_TRIMMING_GRANULARITY_CHARACTER;
231
+ layout->SetTrimming(&trimming, ellipsis.Get());
232
+ }
233
+ return limitHeight;
234
+ }
235
+
236
+ RnTextSize RnWin32TextLayout::measure(float maxWidth) const {
237
+ ComPtr<IDWriteTextLayout> layout;
238
+ layout.Attach(buildLayout(maxWidth, -1.0f));
239
+ if (!layout) {
240
+ return {};
241
+ }
242
+
243
+ const float limitHeight = applyLineLimit(layout.Get());
244
+
245
+ DWRITE_TEXT_METRICS metrics{};
246
+ if (FAILED(layout->GetMetrics(&metrics))) {
247
+ return {};
248
+ }
249
+
250
+ // `width` rather than `widthIncludingTrailingWhitespace`, so a trailing space
251
+ // does not widen the box -- which is what every other platform reports and
252
+ // what makes empty text measure to zero.
253
+ RnTextSize size{metrics.width, metrics.height};
254
+ if (limitHeight >= 0.0f) {
255
+ size.height = std::min(size.height, limitHeight);
256
+ }
257
+ return size;
258
+ }
259
+
260
+ void RnWin32TextLayout::draw(ID2D1RenderTarget *target, float width, float height) const {
261
+ if (target == nullptr) {
262
+ return;
263
+ }
264
+
265
+ ComPtr<IDWriteTextLayout> layout;
266
+ layout.Attach(buildLayout(width, height));
267
+ if (!layout) {
268
+ return;
269
+ }
270
+ applyLineLimit(layout.Get());
271
+
272
+ ComPtr<ID2D1SolidColorBrush> brush;
273
+ const D2D1_COLOR_F colour =
274
+ D2D1::ColorF(style_.color[0], style_.color[1], style_.color[2], style_.color[3]);
275
+ if (FAILED(target->CreateSolidColorBrush(colour, brush.GetAddressOf()))) {
276
+ return;
277
+ }
278
+
279
+ // Per-run colour, which DirectWrite carries as a *drawing effect* rather than
280
+ // as a range attribute like family or weight. That sounds like it needs a
281
+ // custom IDWriteTextRenderer, and it does for anything else you might attach
282
+ // -- but Direct2D's own renderer has one special case, and this is it: a
283
+ // drawing effect that is an ID2D1Brush is used to paint that range.
284
+ //
285
+ // So `Hello <Text style={{color:'red'}}>world</Text>` renders in two colours
286
+ // here as it does on the other two desktops, without this file learning what
287
+ // a glyph run is.
288
+ //
289
+ // The brushes have to outlive DrawTextLayout, which is why they are held
290
+ // rather than created and dropped in the loop.
291
+ std::vector<ComPtr<ID2D1SolidColorBrush>> runBrushes;
292
+ for (const ResolvedRun &run : runs_) {
293
+ if (run.length == 0 || sameColour(run.style.color, style_.color)) {
294
+ continue;
295
+ }
296
+ ComPtr<ID2D1SolidColorBrush> runBrush;
297
+ const D2D1_COLOR_F runColour = D2D1::ColorF(
298
+ run.style.color[0], run.style.color[1], run.style.color[2], run.style.color[3]);
299
+ if (FAILED(target->CreateSolidColorBrush(runColour, runBrush.GetAddressOf()))) {
300
+ continue;
301
+ }
302
+ layout->SetDrawingEffect(runBrush.Get(), DWRITE_TEXT_RANGE{run.start, run.length});
303
+ runBrushes.push_back(std::move(runBrush));
304
+ }
305
+
306
+ target->DrawTextLayout(
307
+ D2D1::Point2F(0.0f, 0.0f), layout.Get(), brush.Get(), D2D1_DRAW_TEXT_OPTIONS_NONE);
308
+ }
309
+
310
+ } // namespace basalt::win32
@@ -0,0 +1,160 @@
1
+ // A laid-out paragraph, and nothing that knows what React Native is.
2
+ //
3
+ // Deliberately separate from whatever turns an `AttributedString` into one of
4
+ // these, for the same reason `appkit/RnTextLayout.h` is separate from
5
+ // `CoreTextLayout.h`: the view layer draws paragraphs and must not gain a React
6
+ // Native dependency to do it. `basalt_win32_view` builds and is tested on a
7
+ // Windows box with nothing but a compiler and the SDK, and that is worth
8
+ // keeping -- it is the whole reason there is a test suite on this platform
9
+ // before there is a host.
10
+ //
11
+ // One object serves measurement and painting. That is not tidiness: if the two
12
+ // built layouts differently -- a different default font, a different wrap mode
13
+ // -- Yoga would allot a box computed one way and the view would paint text laid
14
+ // out another, and the result is clipped or overlapping text that looks like a
15
+ // rendering bug rather than a measurement one. The GTK side makes the same
16
+ // argument in `docs/DECISIONS.md`.
17
+ //
18
+ // Unlike Pango, DirectWrite needs no mutex around any of this. Pango's font map
19
+ // is not documented as reentrant, so GTK serialises every measurement on one
20
+ // lock and hides the cost behind a cache; a DWRITE_FACTORY_TYPE_SHARED factory
21
+ // is documented thread-safe, and this builds a fresh IDWriteTextLayout per call
22
+ // rather than mutating a held one. Fabric's layout thread and the UI thread can
23
+ // therefore measure at the same time, which on GTK they cannot.
24
+
25
+ #pragma once
26
+
27
+ #include <memory>
28
+ #include <vector>
29
+ #include <string>
30
+
31
+ // Forward-declared rather than included, so a translation unit that only builds
32
+ // or measures a paragraph does not pull in <d2d1.h> and <dwrite.h> and
33
+ // windows.h behind them. The tests do exactly that.
34
+ struct ID2D1RenderTarget;
35
+ struct IDWriteTextFormat;
36
+ struct IDWriteTextLayout;
37
+
38
+ namespace basalt::win32 {
39
+
40
+ enum class RnTextAlign {
41
+ Left,
42
+ Center,
43
+ Right,
44
+ Justified,
45
+ };
46
+
47
+ // The attributes this platform honours. React Native has many more; each one
48
+ // added here is a line in `DirectWriteLayout`, which is the file that will
49
+ // translate an AttributedString once there is a mounting manager to deliver
50
+ // one.
51
+ struct RnTextStyle {
52
+ std::string fontFamily = "Segoe UI";
53
+ float fontSize = 14.0f;
54
+ bool bold = false;
55
+ bool italic = false;
56
+ // 0 means the font's own line spacing, which is what React Native means by an
57
+ // unset lineHeight.
58
+ float lineHeight = 0.0f;
59
+ RnTextAlign align = RnTextAlign::Left;
60
+ float color[4] = {0.0f, 0.0f, 0.0f, 1.0f};
61
+ };
62
+
63
+ struct RnTextSize {
64
+ float width = 0.0f;
65
+ float height = 0.0f;
66
+ };
67
+
68
+ // One styled span of a paragraph.
69
+ //
70
+ // React Native's `<Text>` is not one string with one style: a
71
+ // ParagraphShadowNode folds its whole subtree into an AttributedString of
72
+ // fragments, each with its own font, size and weight. A layout built from only
73
+ // the first fragment's style renders `Hello <b>world</b>` entirely unbold,
74
+ // which measures wrong as well as looking wrong.
75
+ struct RnTextRun {
76
+ std::string text;
77
+ RnTextStyle style;
78
+ };
79
+
80
+ class RnWin32TextLayout {
81
+ public:
82
+ // `maximumNumberOfLines` of 0 means no limit, matching
83
+ // ParagraphAttributes::maximumNumberOfLines. Returns null if DirectWrite
84
+ // could not produce a text format, which in practice means the process has no
85
+ // DirectWrite at all.
86
+ static std::shared_ptr<RnWin32TextLayout>
87
+ create(std::string utf8Text, const RnTextStyle &style, int maximumNumberOfLines);
88
+
89
+ // The same thing for a paragraph of differently styled spans. The first run's
90
+ // style sets the paragraph defaults -- alignment and line height, which
91
+ // DirectWrite has no per-range form of -- and each run's font family, size,
92
+ // weight, slant and colour are applied over its own character range.
93
+ //
94
+ // Colour is the odd one, and `draw` is where it happens: DirectWrite carries
95
+ // it as a drawing effect rather than as a range attribute, and a drawing
96
+ // effect is only meaningful to whoever renders the layout.
97
+ static std::shared_ptr<RnWin32TextLayout>
98
+ createFromRuns(const std::vector<RnTextRun> &runs, int maximumNumberOfLines);
99
+
100
+ ~RnWin32TextLayout();
101
+
102
+ RnWin32TextLayout(const RnWin32TextLayout &) = delete;
103
+ RnWin32TextLayout &operator=(const RnWin32TextLayout &) = delete;
104
+
105
+ // The original UTF-8, for `describeTree`. Kept alongside the UTF-16 copy
106
+ // DirectWrite needs so that the cross-host dump does not depend on a
107
+ // conversion round-tripping.
108
+ const std::string &text() const { return utf8_; }
109
+
110
+ const RnTextStyle &style() const { return style_; }
111
+ int maximumNumberOfLines() const { return maximumNumberOfLines_; }
112
+
113
+ // The size this paragraph needs at `maxWidth`. Pass a negative width for
114
+ // unconstrained. In React Native's density-independent pixels, like every
115
+ // other coordinate here: DirectWrite measures in DIPs at 96 dpi and nothing
116
+ // multiplies by a scale factor, which is what keeps a `fontSize` of 16 the
117
+ // same sixteen units it is on the other two desktops.
118
+ RnTextSize measure(float maxWidth) const;
119
+
120
+ // Draws at the target's current origin, into a box `width` by `height`.
121
+ void draw(ID2D1RenderTarget *target, float width, float height) const;
122
+
123
+ private:
124
+ RnWin32TextLayout() = default;
125
+
126
+ // The one place a layout is configured. Both `measure` and `draw` go through
127
+ // it, which is what makes the size Yoga is told the size the text is painted
128
+ // at.
129
+ //
130
+ // Returns a reference the caller owns and must release; the two callers do it
131
+ // with a ComPtr. A fresh layout per call rather than one held and mutated,
132
+ // because `SetMaxWidth` on a shared layout is exactly the race that a
133
+ // thread-safe factory would otherwise have saved us from.
134
+ IDWriteTextLayout *buildLayout(float maxWidth, float maxHeight) const;
135
+
136
+ // Applies `maximumNumberOfLines` to a built layout, and reports the height it
137
+ // is now limited to, or a negative number for "no limit applied". Separate
138
+ // because DirectWrite has no line-count property: the limit has to be turned
139
+ // into a height by adding up the line metrics, which means the layout must
140
+ // already exist.
141
+ float applyLineLimit(IDWriteTextLayout *layout) const;
142
+
143
+ // A styled span, resolved to UTF-16 character positions, which is what
144
+ // DirectWrite's ranges are counted in. Empty for a single-style paragraph,
145
+ // where the format alone says everything.
146
+ struct ResolvedRun {
147
+ unsigned start = 0;
148
+ unsigned length = 0;
149
+ RnTextStyle style;
150
+ };
151
+
152
+ std::string utf8_;
153
+ std::wstring utf16_;
154
+ RnTextStyle style_;
155
+ std::vector<ResolvedRun> runs_;
156
+ int maximumNumberOfLines_ = 0;
157
+ IDWriteTextFormat *format_ = nullptr;
158
+ };
159
+
160
+ } // namespace basalt::win32