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