@soundor/vst3-host 0.0.0-stage → 0.1.0

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 (54) hide show
  1. package/CMakeLists.txt +194 -0
  2. package/CMakePresets.json +115 -0
  3. package/LICENSE +21 -0
  4. package/NOTICE.md +17 -0
  5. package/README.md +40 -2
  6. package/cmake/Vst3HostDependencies.cmake +179 -0
  7. package/dist/cli.d.mts +1 -0
  8. package/dist/cli.mjs +88 -0
  9. package/dist/cli.mjs.map +1 -0
  10. package/dist/index.d.mts +586 -0
  11. package/dist/index.d.mts.map +1 -0
  12. package/dist/index.mjs +1 -0
  13. package/dist/prepare-ayjTVhfk.mjs +1149 -0
  14. package/dist/prepare-ayjTVhfk.mjs.map +1 -0
  15. package/native/src/Bundle.cpp +253 -0
  16. package/native/src/Bundle.h +32 -0
  17. package/native/src/EditorArea.h +92 -0
  18. package/native/src/EditorAreaMac.mm +52 -0
  19. package/native/src/EditorAreaWindows.cpp +66 -0
  20. package/native/src/EditorAreaX11.cpp +512 -0
  21. package/native/src/EditorWindow.h +72 -0
  22. package/native/src/EditorWindowMac.mm +134 -0
  23. package/native/src/EditorWindowWindows.cpp +148 -0
  24. package/native/src/EventLoop.h +50 -0
  25. package/native/src/EventLoopLinux.cpp +204 -0
  26. package/native/src/EventLoopMac.mm +146 -0
  27. package/native/src/EventLoopWindows.cpp +132 -0
  28. package/native/src/HostContext.cpp +217 -0
  29. package/native/src/HostContext.h +116 -0
  30. package/native/src/HostInterface.cpp +1026 -0
  31. package/native/src/HostInterface.h +138 -0
  32. package/native/src/HostWindow.cpp +901 -0
  33. package/native/src/HostWindow.h +187 -0
  34. package/native/src/Ipc.cpp +158 -0
  35. package/native/src/Ipc.h +91 -0
  36. package/native/src/Json.cpp +426 -0
  37. package/native/src/Json.h +85 -0
  38. package/native/src/LiveAudio.cpp +416 -0
  39. package/native/src/LiveAudio.h +121 -0
  40. package/native/src/Main.cpp +178 -0
  41. package/native/src/Paths.h +20 -0
  42. package/native/src/Plugin.cpp +1865 -0
  43. package/native/src/Plugin.h +321 -0
  44. package/native/src/Session.cpp +656 -0
  45. package/native/src/Session.h +82 -0
  46. package/native/src/Settings.cpp +93 -0
  47. package/native/src/Settings.h +23 -0
  48. package/native/src/Version.h.in +11 -0
  49. package/native/src/X11Library.cpp +127 -0
  50. package/native/src/X11Library.h +83 -0
  51. package/native/tools/Embed.cpp +49 -0
  52. package/package.json +58 -3
  53. package/protocol.json +7 -0
  54. package/targets.json +33 -0
@@ -0,0 +1,52 @@
1
+ #include "EditorArea.h"
2
+
3
+ #import <Cocoa/Cocoa.h>
4
+ #include <SDL3/SDL.h>
5
+
6
+ namespace soundor::vst3host
7
+ {
8
+ namespace
9
+ {
10
+ class MacEditorArea final : public EditorArea
11
+ {
12
+ public:
13
+ explicit MacEditorArea(NSView* content) : content_([content retain])
14
+ {
15
+ view_ = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 1, 1)];
16
+ [content_ addSubview:view_ positioned:NSWindowAbove relativeTo:nil];
17
+ }
18
+
19
+ ~MacEditorArea() override
20
+ {
21
+ [view_ removeFromSuperview];
22
+ [view_ release];
23
+ [content_ release];
24
+ }
25
+
26
+ [[nodiscard]] const char* platformType() const override { return "NSView"; }
27
+ [[nodiscard]] void* handle() const override { return static_cast<void*>(view_); }
28
+
29
+ void setFrame(int x, int y, int width, int height) override
30
+ {
31
+ const CGFloat top = [content_ isFlipped] ? y : [content_ bounds].size.height - y - height;
32
+ [view_ setFrame:NSMakeRect(x, top, width, height)];
33
+ }
34
+
35
+ private:
36
+ NSView* content_;
37
+ NSView* view_ = nil;
38
+ };
39
+ } // namespace
40
+
41
+ std::unique_ptr<EditorArea> EditorArea::create(SDL_Window* window, std::string& error)
42
+ {
43
+ auto* nsWindow = static_cast<NSWindow*>(
44
+ SDL_GetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr));
45
+ if (nsWindow == nil || [nsWindow contentView] == nil)
46
+ {
47
+ error = "the host interface's window is not a Cocoa window";
48
+ return nullptr;
49
+ }
50
+ return std::make_unique<MacEditorArea>([nsWindow contentView]);
51
+ }
52
+ } // namespace soundor::vst3host
@@ -0,0 +1,66 @@
1
+ #include "EditorArea.h"
2
+
3
+ #include <SDL3/SDL.h>
4
+
5
+ #ifndef NOMINMAX
6
+ #define NOMINMAX
7
+ #endif
8
+ #include <windows.h>
9
+
10
+ #include <algorithm>
11
+
12
+ namespace soundor::vst3host
13
+ {
14
+ namespace
15
+ {
16
+ class WindowsEditorArea final : public EditorArea
17
+ {
18
+ public:
19
+ explicit WindowsEditorArea(HWND parent)
20
+ {
21
+ static const ATOM windowClass = []
22
+ {
23
+ WNDCLASSEXW description {};
24
+ description.cbSize = sizeof description;
25
+ description.lpfnWndProc = DefWindowProcW;
26
+ description.hInstance = GetModuleHandleW(nullptr);
27
+ description.hCursor = LoadCursorW(nullptr, IDC_ARROW);
28
+ description.hbrBackground = static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH));
29
+ description.lpszClassName = L"SoundorVst3HostEditorArea";
30
+ return RegisterClassExW(&description);
31
+ }();
32
+ // The interface must not draw over the editor.
33
+ SetWindowLongPtrW(parent, GWL_STYLE, GetWindowLongPtrW(parent, GWL_STYLE) | WS_CLIPCHILDREN);
34
+ window_ = CreateWindowExW(0, MAKEINTATOM(windowClass), L"",
35
+ WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, 0, 0, 1, 1, parent,
36
+ nullptr, GetModuleHandleW(nullptr), nullptr);
37
+ }
38
+
39
+ ~WindowsEditorArea() override { DestroyWindow(window_); }
40
+
41
+ [[nodiscard]] const char* platformType() const override { return "HWND"; }
42
+ [[nodiscard]] void* handle() const override { return window_; }
43
+
44
+ void setFrame(int x, int y, int width, int height) override
45
+ {
46
+ SetWindowPos(window_, nullptr, x, y, std::max(width, 1), std::max(height, 1),
47
+ SWP_NOZORDER | SWP_NOACTIVATE);
48
+ }
49
+
50
+ private:
51
+ HWND window_ = nullptr;
52
+ };
53
+ } // namespace
54
+
55
+ std::unique_ptr<EditorArea> EditorArea::create(SDL_Window* window, std::string& error)
56
+ {
57
+ auto* parent = static_cast<HWND>(
58
+ SDL_GetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr));
59
+ if (parent == nullptr)
60
+ {
61
+ error = "the host interface's window is not a Win32 window";
62
+ return nullptr;
63
+ }
64
+ return std::make_unique<WindowsEditorArea>(parent);
65
+ }
66
+ } // namespace soundor::vst3host
@@ -0,0 +1,512 @@
1
+ #include "EditorArea.h"
2
+ #include "X11Library.h"
3
+
4
+ #include <SDL3/SDL.h>
5
+
6
+ #include <algorithm>
7
+ #include <array>
8
+ #include <chrono>
9
+ #include <cstddef>
10
+ #include <cstdint>
11
+ #include <cstring>
12
+ #include <utility>
13
+
14
+ namespace soundor::vst3host
15
+ {
16
+ namespace
17
+ {
18
+ // How long a resized editor may take to paint before it is asked for
19
+ // another size anyway.
20
+ constexpr auto paintTimeout = std::chrono::milliseconds(100);
21
+ // How long its last image stands in before the new one is shown as it
22
+ // is: an editor can be black in all its corners.
23
+ constexpr auto unpaintedTimeout = std::chrono::milliseconds(500);
24
+ // A window resized this recently may be being resized still.
25
+ constexpr auto windowSettling = std::chrono::milliseconds(200);
26
+ // The host interface's stage (HostInterface.cpp).
27
+ constexpr unsigned long stageColour = 0x0b0c0f;
28
+
29
+ class X11EditorArea final : public EditorArea
30
+ {
31
+ public:
32
+ X11EditorArea(const Xlib& x, Display* display, Window parent) : x_(x), display_(display), parent_(parent)
33
+ {
34
+ x_.setWindowBackground(display_, parent_, stageColour);
35
+ const int screen = x_.defaultScreen(display_);
36
+ const auto black = x_.blackPixel(display_, screen);
37
+ window_ = x_.createSimpleWindow(display_, parent, 0, 0, 1, 1, 0, black, black);
38
+ // Never painted by the server: what is under an editor that
39
+ // has not drawn yet is the host's last frame, not a black box.
40
+ x_.setWindowBackgroundPixmap(display_, window_, None);
41
+ keepTopLeft(false);
42
+ x_.mapWindow(display_, window_);
43
+ x_.flush(display_);
44
+ }
45
+
46
+ ~X11EditorArea() override
47
+ {
48
+ if (live_)
49
+ endLive();
50
+ if (connection_ != nullptr)
51
+ {
52
+ setQuietDisplay(nullptr);
53
+ x_.closeDisplay(connection_);
54
+ }
55
+ x_.destroyWindow(display_, window_);
56
+ x_.flush(display_);
57
+ }
58
+
59
+ [[nodiscard]] const char* platformType() const override { return "X11EmbedWindowID"; }
60
+ [[nodiscard]] void* handle() const override
61
+ {
62
+ return reinterpret_cast<void*>(window_); // NOLINT(performance-no-int-to-ptr): an X11 id
63
+ }
64
+
65
+ void setFrame(int x, int y, int width, int height) override
66
+ {
67
+ // Still showing until a frame has drawn its image, its pixels
68
+ // stay where they are: where it moves to has none.
69
+ if (live_ && ! hidden_)
70
+ {
71
+ heldFrame_ = { x, y, width, height };
72
+ return;
73
+ }
74
+ applyFrame(x, y, width, height);
75
+ }
76
+
77
+ [[nodiscard]] bool isAt(int x, int y) const override
78
+ {
79
+ return heldFrame_.has_value() || place() == std::pair { x, y };
80
+ }
81
+
82
+ void keepTopLeft(bool keep) override
83
+ {
84
+ // A resized window keeps its pixels, which are the last frame
85
+ // and the editor's, and what the server fills the new part with
86
+ // until the next frame is the stage's colour, not black. By
87
+ // default it would discard them all, and fill the whole window.
88
+ XSetWindowAttributes gravity {};
89
+ gravity.bit_gravity = keep ? NorthWestGravity : CenterGravity;
90
+ gravity.win_gravity = gravity.bit_gravity;
91
+ x_.changeWindowAttributes(display_, parent_, CWBitGravity, &gravity);
92
+ x_.changeWindowAttributes(display_, window_, CWWinGravity, &gravity);
93
+ }
94
+
95
+ bool beginLive() override
96
+ {
97
+ if (live_)
98
+ return true;
99
+ if (! connect())
100
+ return false;
101
+ Window root = 0;
102
+ Window parent = 0;
103
+ Window* children = nullptr;
104
+ unsigned count = 0;
105
+ if (x_.queryTree(connection_, window_, &root, &parent, &children, &count) == 0)
106
+ return false;
107
+ editor_ = count > 0 ? children[count - 1] : 0;
108
+ if (children != nullptr)
109
+ x_.free(children);
110
+ if (editor_ == 0)
111
+ return false;
112
+ // The editor keeps its size and paints into a pixmap of its
113
+ // own, whatever the area does. Shaped to nothing, the area
114
+ // stops clipping the window's own drawing, and stays where
115
+ // it is: an editor tracking the pointer through a drag of its
116
+ // own keeps its coordinates.
117
+ live->redirectSubwindows(connection_, window_, CompositeRedirectManual);
118
+ damage_ = live->damageCreate(connection_, editor_, XDamageReportRawRectangles);
119
+ x_.sync(connection_, False);
120
+ // Its pixels now are what the server could copy of what showed,
121
+ // which may be nothing. It stays showing until it has painted
122
+ // into its own pixmap, which it is asked to. Not if the window
123
+ // is being resized, or has shrunk around it, as a window
124
+ // manager's restore does: the server has let go of what was on
125
+ // the screen, so there is nothing to keep there. Either way the
126
+ // view goes only once a frame has drawn its image.
127
+ askEditorToPaint();
128
+ live_ = true;
129
+ hidden_ = false;
130
+ showing_ = ! clippedByWindow() && std::chrono::steady_clock::now() - windowChangedAt_ >= windowSettling;
131
+ repainted_ = false;
132
+ beganAt_ = std::chrono::steady_clock::now();
133
+ firstImage_ = true;
134
+ dirty_ = ! showing_;
135
+ unpainted_ = false;
136
+ width_ = 0;
137
+ height_ = 0;
138
+ return true;
139
+ }
140
+
141
+ void endLive() override
142
+ {
143
+ if (! live_)
144
+ return;
145
+ live_ = false;
146
+ dropImage();
147
+ if (pixmap_ != 0)
148
+ x_.freePixmap(connection_, pixmap_);
149
+ pixmap_ = 0;
150
+ live->damageDestroy(connection_, damage_);
151
+ // Back first: the server hands a redirected window's pixels to
152
+ // its parent as it unredirects it.
153
+ if (hidden_)
154
+ {
155
+ keepingParentPixels(
156
+ [this] { live->shapeCombineMask(display_, window_, ShapeBounding, 0, 0, None, ShapeSet); });
157
+ }
158
+ hidden_ = false;
159
+ showing_ = false;
160
+ applyHeldFrame();
161
+ x_.sync(display_, False);
162
+ live->unredirectSubwindows(connection_, window_, CompositeRedirectManual);
163
+ x_.sync(connection_, False);
164
+ }
165
+
166
+ std::optional<Image> takeImage() override
167
+ {
168
+ if (! live_)
169
+ return std::nullopt;
170
+ x_.sync(connection_, False);
171
+ XWindowAttributes attributes {};
172
+ if (x_.getWindowAttributes(connection_, editor_, &attributes) == 0
173
+ || attributes.map_state != IsViewable)
174
+ return std::nullopt;
175
+ if (given_)
176
+ {
177
+ // Not there yet: its pixmap is the old size, part painted.
178
+ if ((attributes.width != given_->first || attributes.height != given_->second)
179
+ && std::chrono::steady_clock::now() - givenAt_ < paintTimeout)
180
+ return std::nullopt;
181
+ given_.reset();
182
+ }
183
+ if (attributes.width != width_ || attributes.height != height_)
184
+ {
185
+ // Each size has its own pixmap.
186
+ if (pixmap_ != 0)
187
+ x_.freePixmap(connection_, pixmap_);
188
+ pixmap_ = live->nameWindowPixmap(connection_, editor_);
189
+ unpainted_ = width_ != 0;
190
+ resizedAt_ = std::chrono::steady_clock::now();
191
+ width_ = attributes.width;
192
+ height_ = attributes.height;
193
+ }
194
+ while (x_.pending(connection_) > 0)
195
+ {
196
+ XEvent event {};
197
+ x_.nextEvent(connection_, &event);
198
+ if (event.type != damageEvent_ + XDamageNotify)
199
+ continue;
200
+ dirty_ = true;
201
+ const auto& notify = reinterpret_cast<const XDamageNotifyEvent&>(event);
202
+ if (notify.area.x == 0 && notify.area.y == 0 && notify.area.width >= width_
203
+ && notify.area.height >= height_)
204
+ repainted_ = true;
205
+ }
206
+ if (showing_)
207
+ {
208
+ // Whole, or as whole as it will get.
209
+ if (! repainted_ && windowChangedAt_ < beganAt_
210
+ && std::chrono::steady_clock::now() - beganAt_ < paintTimeout)
211
+ return std::nullopt;
212
+ dirty_ = true;
213
+ firstImage_ = ! repainted_;
214
+ }
215
+ if (! dirty_)
216
+ return std::nullopt;
217
+ dirty_ = false;
218
+ live->damageSubtract(connection_, damage_, None, None);
219
+ dropImage();
220
+ image_ = x_.getImage(connection_, pixmap_, 0, 0, static_cast<unsigned>(width_),
221
+ static_cast<unsigned>(height_), AllPlanes, ZPixmap);
222
+ if (image_ == nullptr)
223
+ return std::nullopt;
224
+ // A pixmap's image has no masks: the window's visual has them.
225
+ if (image_->bits_per_pixel != 32 || attributes.visual->red_mask != 0xff0000
226
+ || attributes.visual->blue_mask != 0xff)
227
+ return std::nullopt;
228
+ // A resized window's new pixmap is zeros, and the server's
229
+ // clearing it counts as damage: not painted while a corner or
230
+ // the middle is still zero. An editor changing its scale can
231
+ // leave zeros again in a later frame, so this goes on for a while.
232
+ if (! painted() && std::chrono::steady_clock::now() - resizedAt_ < unpaintedTimeout)
233
+ {
234
+ dirty_ = true;
235
+ return std::nullopt;
236
+ }
237
+ unpainted_ = false;
238
+ Image result { image_->data, width_, height_, image_->bytes_per_line };
239
+ showing_ = false;
240
+ if (std::exchange(firstImage_, false))
241
+ {
242
+ // Redirecting keeps what showed, and the window may already
243
+ // have shrunk: a window manager's restore starts that way.
244
+ result.first = true;
245
+ XWindowAttributes outer {};
246
+ if (x_.getWindowAttributes(display_, parent_, &outer) != 0)
247
+ {
248
+ const auto [x, y] = place();
249
+ result.validX = std::clamp(-x, 0, width_ - 1);
250
+ result.validY = std::clamp(-y, 0, height_ - 1);
251
+ result.validWidth = std::clamp(outer.width - x - result.validX, 1, width_ - result.validX);
252
+ result.validHeight = std::clamp(outer.height - y - result.validY, 1, height_ - result.validY);
253
+ result.shownX = x + result.validX;
254
+ result.shownY = y + result.validY;
255
+ result.partial = result.validWidth < width_ || result.validHeight < height_;
256
+ }
257
+ }
258
+ return result;
259
+ }
260
+
261
+ void allowOnlyMoveResizeClose() override
262
+ {
263
+ // As Motif defined it: flags (functions), functions (resize,
264
+ // move, close), decorations, input mode, status.
265
+ const std::array<unsigned long, 5> hints { 1, 2 | 4 | 32, 0, 0, 0 };
266
+ const Atom property = x_.internAtom(display_, "_MOTIF_WM_HINTS", False);
267
+ x_.changeProperty(display_, parent_, property, property, 32, PropModeReplace,
268
+ reinterpret_cast<const unsigned char*>(hints.data()), hints.size());
269
+ }
270
+
271
+ bool hideView() override
272
+ {
273
+ if (! live_ || showing_)
274
+ return false;
275
+ if (hidden_)
276
+ return true;
277
+ // The editor keeps its coordinates: shaped to nothing, the area
278
+ // stays where it is.
279
+ hidden_ = true;
280
+ keepingParentPixels(
281
+ [this] {
282
+ live->shapeCombineRectangles(display_, window_, ShapeBounding, 0, 0, nullptr, 0, ShapeSet,
283
+ Unsorted);
284
+ });
285
+ applyHeldFrame();
286
+ x_.sync(display_, False);
287
+ return true;
288
+ }
289
+
290
+ [[nodiscard]] bool awaitingEditor() const { return live_ && showing_; }
291
+
292
+ void windowChanged() override { windowChangedAt_ = std::chrono::steady_clock::now(); }
293
+
294
+ void editorGiven(int width, int height) override
295
+ {
296
+ if (! live_ || (width == width_ && height == height_))
297
+ return;
298
+ given_ = { width, height };
299
+ givenAt_ = std::chrono::steady_clock::now();
300
+ }
301
+
302
+ [[nodiscard]] bool whole() const override
303
+ {
304
+ const auto now = std::chrono::steady_clock::now();
305
+ if (awaitingEditor() || (given_ && now - givenAt_ < unpaintedTimeout))
306
+ return false;
307
+ return ! unpainted_ || now - resizedAt_ >= unpaintedTimeout;
308
+ }
309
+
310
+ [[nodiscard]] bool settled() const override
311
+ {
312
+ const auto now = std::chrono::steady_clock::now();
313
+ if (awaitingEditor() || (given_ && now - givenAt_ < paintTimeout))
314
+ return false;
315
+ return ! unpainted_ || now - resizedAt_ >= paintTimeout;
316
+ }
317
+
318
+ private:
319
+ /** A connection of its own: redirection ends with it, and SDL
320
+ does not see its events. */
321
+ bool connect()
322
+ {
323
+ if (connection_ != nullptr)
324
+ return true;
325
+ if (unavailable_)
326
+ return false;
327
+ unavailable_ = true;
328
+ live = loadXLive();
329
+ if (live == nullptr)
330
+ return false;
331
+ connection_ = x_.openDisplay(x_.displayString(display_));
332
+ if (connection_ == nullptr)
333
+ return false;
334
+ int event = 0;
335
+ int error = 0;
336
+ if (live->compositeQueryExtension(connection_, &event, &error) == 0
337
+ || live->damageQueryExtension(connection_, &damageEvent_, &error) == 0
338
+ || live->shapeQueryExtension(connection_, &event, &error) == 0)
339
+ {
340
+ x_.closeDisplay(connection_);
341
+ connection_ = nullptr;
342
+ return false;
343
+ }
344
+ setQuietDisplay(connection_);
345
+ return true;
346
+ }
347
+
348
+ void applyFrame(int x, int y, int width, int height)
349
+ {
350
+ const bool moved = ! isAt(x, y);
351
+ x_.moveResizeWindow(display_, window_, x, y, static_cast<unsigned>(std::max(width, 1)),
352
+ static_cast<unsigned>(std::max(height, 1)));
353
+ if (moved)
354
+ tellEditorItMoved();
355
+ x_.flush(display_);
356
+ }
357
+
358
+ void applyHeldFrame()
359
+ {
360
+ if (const auto frame = std::exchange(heldFrame_, std::nullopt))
361
+ applyFrame((*frame)[0], (*frame)[1], (*frame)[2], (*frame)[3]);
362
+ }
363
+
364
+ /** Showing or hiding the view exposes the window under it, which
365
+ must not fill with its background: what is there stays until
366
+ the next frame. */
367
+ template <typename Change>
368
+ void keepingParentPixels(Change change)
369
+ {
370
+ x_.setWindowBackgroundPixmap(display_, parent_, None);
371
+ change();
372
+ x_.setWindowBackground(display_, parent_, stageColour);
373
+ }
374
+
375
+ /** Where the view is now, which the window's gravity decides too. */
376
+ [[nodiscard]] std::pair<int, int> place() const
377
+ {
378
+ XWindowAttributes attributes {};
379
+ x_.getWindowAttributes(display_, window_, &attributes);
380
+ return { attributes.x, attributes.y };
381
+ }
382
+
383
+ [[nodiscard]] bool clippedByWindow() const
384
+ {
385
+ XWindowAttributes outer {};
386
+ XWindowAttributes editor {};
387
+ if (x_.getWindowAttributes(display_, parent_, &outer) == 0
388
+ || x_.getWindowAttributes(display_, editor_, &editor) == 0)
389
+ return true;
390
+ const auto [x, y] = place();
391
+ return x < 0 || y < 0 || x + editor.width > outer.width || y + editor.height > outer.height;
392
+ }
393
+
394
+ void askEditorToPaint()
395
+ {
396
+ XWindowAttributes attributes {};
397
+ if (x_.getWindowAttributes(display_, editor_, &attributes) == 0)
398
+ return;
399
+ XEvent event {};
400
+ event.xexpose.type = Expose;
401
+ event.xexpose.window = editor_;
402
+ event.xexpose.width = attributes.width;
403
+ event.xexpose.height = attributes.height;
404
+ x_.sendEvent(display_, editor_, False, ExposureMask, &event);
405
+ x_.flush(display_);
406
+ }
407
+
408
+ /** The server tells a window of its own moves only, and an editor
409
+ keeps its place on the screen from that (a drag of its own reads
410
+ the pointer's screen position): as a window manager does for a
411
+ window it moves, a synthetic ConfigureNotify. */
412
+ void tellEditorItMoved()
413
+ {
414
+ Window root = 0;
415
+ Window parent = 0;
416
+ Window* children = nullptr;
417
+ unsigned count = 0;
418
+ if (x_.queryTree(display_, window_, &root, &parent, &children, &count) == 0)
419
+ return;
420
+ for (unsigned index = 0; index < count; ++index)
421
+ {
422
+ XWindowAttributes attributes {};
423
+ Window child = 0;
424
+ int rootX = 0;
425
+ int rootY = 0;
426
+ if (x_.getWindowAttributes(display_, children[index], &attributes) == 0
427
+ || x_.translateCoordinates(display_, children[index], root, 0, 0, &rootX, &rootY, &child) == 0)
428
+ continue;
429
+ XEvent event {};
430
+ event.xconfigure.type = ConfigureNotify;
431
+ event.xconfigure.event = children[index];
432
+ event.xconfigure.window = children[index];
433
+ event.xconfigure.x = rootX;
434
+ event.xconfigure.y = rootY;
435
+ event.xconfigure.width = attributes.width;
436
+ event.xconfigure.height = attributes.height;
437
+ x_.sendEvent(display_, children[index], False, StructureNotifyMask, &event);
438
+ }
439
+ if (children != nullptr)
440
+ x_.free(children);
441
+ }
442
+
443
+ [[nodiscard]] bool painted() const
444
+ {
445
+ auto zero = [this](int x, int y)
446
+ {
447
+ const char* row = image_->data + static_cast<std::ptrdiff_t>(y) * image_->bytes_per_line;
448
+ uint32_t pixel = 0;
449
+ std::memcpy(&pixel, row + static_cast<std::ptrdiff_t>(x) * 4, sizeof pixel);
450
+ return (pixel & 0xffffffU) == 0;
451
+ };
452
+ return ! (zero(0, 0) || zero(width_ - 1, 0) || zero(0, height_ - 1) || zero(width_ - 1, height_ - 1)
453
+ || zero(width_ / 2, height_ / 2));
454
+ }
455
+
456
+ void dropImage()
457
+ {
458
+ if (image_ != nullptr)
459
+ image_->f.destroy_image(image_);
460
+ image_ = nullptr;
461
+ }
462
+
463
+ const Xlib& x_;
464
+ const XLive* live = nullptr;
465
+ Display* display_;
466
+ Display* connection_ = nullptr;
467
+ Window parent_ = 0;
468
+ Window window_ = 0;
469
+ Window editor_ = 0;
470
+ Pixmap pixmap_ = 0;
471
+ Damage damage_ = 0;
472
+ XImage* image_ = nullptr;
473
+ int damageEvent_ = 0;
474
+ int width_ = 0;
475
+ int height_ = 0;
476
+ std::chrono::steady_clock::time_point resizedAt_;
477
+ std::optional<std::pair<int, int>> given_;
478
+ std::chrono::steady_clock::time_point givenAt_;
479
+ bool unavailable_ = false;
480
+ bool live_ = false;
481
+ bool firstImage_ = false;
482
+ /** Live, with the view still showing until the editor has painted. */
483
+ bool showing_ = false;
484
+ /** Shaped to nothing: the window draws its image. */
485
+ bool hidden_ = false;
486
+ std::optional<std::array<int, 4>> heldFrame_;
487
+ bool repainted_ = false;
488
+ std::chrono::steady_clock::time_point beganAt_;
489
+ std::chrono::steady_clock::time_point windowChangedAt_;
490
+ bool dirty_ = false;
491
+ bool unpainted_ = false;
492
+ };
493
+ } // namespace
494
+
495
+ std::unique_ptr<EditorArea> EditorArea::create(SDL_Window* window, std::string& error)
496
+ {
497
+ const Xlib* x = loadXlib(error);
498
+ if (x == nullptr)
499
+ return nullptr;
500
+ const SDL_PropertiesID properties = SDL_GetWindowProperties(window);
501
+ auto* display =
502
+ static_cast<Display*>(SDL_GetPointerProperty(properties, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr));
503
+ const auto parent =
504
+ static_cast<Window>(SDL_GetNumberProperty(properties, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0));
505
+ if (display == nullptr || parent == 0)
506
+ {
507
+ error = "the host interface's window is not an X11 window";
508
+ return nullptr;
509
+ }
510
+ return std::make_unique<X11EditorArea>(*x, display, parent);
511
+ }
512
+ } // namespace soundor::vst3host