FreeRDP
Loading...
Searching...
No Matches
SDL3/sdl_window.cpp
1
20#include <limits>
21#include <sstream>
22#include <cmath>
23
24#include "sdl_window.hpp"
25#include "sdl_utils.hpp"
26
27#include <freerdp/utils/string.h>
28
29SdlWindow::SdlWindow(SDL_DisplayID id, const std::string& title, const SDL_Rect& rect,
30 [[maybe_unused]] Uint32 flags)
31 : _initialW(rect.w), _initialH(rect.h), _displayID(id)
32{
33 float pd = SDL_GetDisplayContentScale(id);
34 if (pd <= 0.0f)
35 pd = 1.0f;
36 const int createW = static_cast<int>(std::ceil(static_cast<float>(rect.w) / pd));
37 const int createH = static_cast<int>(std::ceil(static_cast<float>(rect.h) / pd));
38
39 auto props = SDL_CreateProperties();
40 SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, title.c_str());
41 SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, rect.x);
42 SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, rect.y);
43 SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, createW);
44 SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, createH);
45 SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true);
46
47 if (flags & SDL_WINDOW_HIGH_PIXEL_DENSITY)
48 SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);
49
50 if (flags & SDL_WINDOW_FULLSCREEN)
51 SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, true);
52
53 if (flags & SDL_WINDOW_BORDERLESS)
54 SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, true);
55
56 if (flags & SDL_WINDOW_TRANSPARENT)
57 SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_TRANSPARENT_BOOLEAN, true);
58
59 /* RAIL windows are created hidden until first paint to avoid black flash. */
60 if (flags & SDL_WINDOW_HIDDEN)
61 SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIDDEN_BOOLEAN, true);
62
63 _window = SDL_CreateWindowWithProperties(props);
64 SDL_DestroyProperties(props);
65 SDL_SetHint(SDL_HINT_APP_NAME, "");
66 std::ignore = SDL_SyncWindow(_window);
67
68 _renderer = SDL_CreateRenderer(_window, nullptr);
69
70 std::ignore = resizeToScale();
71
72 _monitor = query(_window, id, true);
73}
74
75SdlWindow::SdlWindow(SdlWindow&& other) noexcept
76 : _window(other._window), _renderer(other._renderer), _renderTarget(other._renderTarget),
77 _gdiTexture(other._gdiTexture), _gdiTextureW(other._gdiTextureW),
78 _gdiTextureH(other._gdiTextureH), _initialW(other._initialW), _initialH(other._initialH),
79 _displayID(other._displayID), _offset_x(other._offset_x), _offset_y(other._offset_y),
80 _monitor(other._monitor)
81{
82 other._window = nullptr;
83 other._renderer = nullptr;
84 other._renderTarget = nullptr;
85 other._gdiTexture = nullptr;
86}
87
88SdlWindow::~SdlWindow()
89{
90 if (_gdiTexture)
91 SDL_DestroyTexture(_gdiTexture);
92 if (_renderTarget)
93 SDL_DestroyTexture(_renderTarget);
94 if (_renderer)
95 SDL_DestroyRenderer(_renderer);
96 if (_window)
97 SDL_DestroyWindow(_window);
98}
99
100SDL_WindowID SdlWindow::id() const
101{
102 if (!_window)
103 return 0;
104 return SDL_GetWindowID(_window);
105}
106
107SDL_DisplayID SdlWindow::displayIndex() const
108{
109 if (!_window)
110 return 0;
111 return SDL_GetDisplayForWindow(_window);
112}
113
114SDL_Rect SdlWindow::rect() const
115{
116 return rect(_window);
117}
118
119SDL_Rect SdlWindow::bounds() const
120{
121 SDL_Rect rect = {};
122 if (_window)
123 {
124 if (!SDL_GetWindowPosition(_window, &rect.x, &rect.y))
125 return {};
126 if (!SDL_GetWindowSize(_window, &rect.w, &rect.h))
127 return {};
128 }
129 return rect;
130}
131
132SDL_Window* SdlWindow::window() const
133{
134 return _window;
135}
136
137SDL_Renderer* SdlWindow::renderer() const
138{
139 return _renderer;
140}
141
142Sint32 SdlWindow::offsetX() const
143{
144 return _offset_x;
145}
146
147void SdlWindow::setOffsetX(Sint32 x)
148{
149 _offset_x = x;
150}
151
152void SdlWindow::setOffsetY(Sint32 y)
153{
154 _offset_y = y;
155}
156
157Sint32 SdlWindow::offsetY() const
158{
159 return _offset_y;
160}
161
162rdpMonitor SdlWindow::monitor(bool isPrimary) const
163{
164 auto m = _monitor;
165 if (isPrimary)
166 {
167 m.x = 0;
168 m.y = 0;
169 }
170 return m;
171}
172
173void SdlWindow::setMonitor(rdpMonitor monitor)
174{
175 _monitor = monitor;
176}
177
178float SdlWindow::scale() const
179{
180 return SDL_GetWindowDisplayScale(_window);
181}
182
183SDL_DisplayOrientation SdlWindow::orientation() const
184{
185 const auto did = displayIndex();
186 return SDL_GetCurrentDisplayOrientation(did);
187}
188
189bool SdlWindow::grabKeyboard(bool enable)
190{
191 if (!_window)
192 return false;
193 SDL_SetWindowKeyboardGrab(_window, enable);
194 return true;
195}
196
197bool SdlWindow::grabMouse(bool enable)
198{
199 if (!_window)
200 return false;
201 SDL_SetWindowMouseGrab(_window, enable);
202 return true;
203}
204
205void SdlWindow::setBordered(bool bordered)
206{
207 if (_window)
208 SDL_SetWindowBordered(_window, bordered);
209 /* No SDL_SyncWindow: this runs for every window event, and on macOS each one waited for SDL's
210 * sync timeout (2.5 s) while a full screen change was pending. SDL ignores an unchanged border
211 * anyway. */
212}
213
214void SdlWindow::raise()
215{
216 SDL_RaiseWindow(_window);
217 std::ignore = SDL_SyncWindow(_window);
218}
219
220void SdlWindow::resizeable(bool use)
221{
222 SDL_SetWindowResizable(_window, use);
223 std::ignore = SDL_SyncWindow(_window);
224}
225
226void SdlWindow::fullscreen(bool enter, bool forceOriginalDisplay)
227{
228 if (enter && forceOriginalDisplay && _displayID != 0)
229 {
230 /* Move the window to the desired display. We should not wait
231 * for the window to be moved, because some backends can refuse
232 * the move. The intent of moving the window is enough for SDL
233 * to decide which display will be used for fullscreen. */
234 SDL_Rect rect = {};
235 std::ignore = SDL_GetDisplayBounds(_displayID, &rect);
236 std::ignore = SDL_SetWindowPosition(_window, rect.x, rect.y);
237 }
238 std::ignore = SDL_SetWindowFullscreen(_window, enter);
239 std::ignore = SDL_SyncWindow(_window);
240}
241
242void SdlWindow::minimize()
243{
244 SDL_MinimizeWindow(_window);
245 std::ignore = SDL_SyncWindow(_window);
246}
247
248bool SdlWindow::resizeToScale()
249{
250 if (!_window || _initialW <= 0 || _initialH <= 0)
251 return false;
252 if ((SDL_GetWindowFlags(_window) & SDL_WINDOW_FULLSCREEN) != 0)
253 return true;
254
255 float pd = SDL_GetWindowPixelDensity(_window);
256 if (pd <= 0.0f)
257 pd = 1.0f;
258
259 const int targetW = static_cast<int>(std::ceil(static_cast<float>(_initialW) / pd));
260 const int targetH = static_cast<int>(std::ceil(static_cast<float>(_initialH) / pd));
261
262 int curW = 0;
263 int curH = 0;
264 if (!SDL_GetWindowSize(_window, &curW, &curH))
265 return false;
266
267 if (curW == targetW && curH == targetH)
268 return true;
269
270 return resize({ targetW, targetH });
271}
272
273bool SdlWindow::resize(const SDL_Point& size)
274{
275 return SDL_SetWindowSize(_window, size.x, size.y);
276}
277
278void SdlWindow::ensureRenderTarget()
279{
280 if (!_renderer)
281 return;
282
283 int w = 0;
284 int h = 0;
285 SDL_GetWindowSizeInPixels(_window, &w, &h);
286 if (w <= 0 || h <= 0)
287 return;
288
289 /* Recreate if missing or if window size changed */
290 if (_renderTarget)
291 {
292 float tw = 0;
293 float th = 0;
294 if (!SDL_GetTextureSize(_renderTarget, &tw, &th))
295 return;
296 if (static_cast<int>(tw) == w && static_cast<int>(th) == h)
297 return;
298 SDL_DestroyTexture(_renderTarget);
299 }
300
301 _renderTarget =
302 SDL_CreateTexture(_renderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_TARGET, w, h);
303 if (!_renderTarget)
304 {
305 SDL_LogError(SDL_LOG_CATEGORY_RENDER, "SDL_CreateTexture (render target): %s",
306 SDL_GetError());
307 return;
308 }
309 /* Verbatim, never blended: a transparent window's alpha<0xFF regions would render black. */
310 std::ignore = SDL_SetTextureBlendMode(_renderTarget, SDL_BLENDMODE_NONE);
311
312 /* Clear once: transparent so windows never flash black before painting. */
313 if (SDL_SetRenderTarget(_renderer, _renderTarget))
314 {
315 std::ignore = SDL_SetRenderDrawColor(_renderer, 0x00, 0x00, 0x00, 0x00);
316 std::ignore = SDL_RenderClear(_renderer);
317 }
318}
319
320bool SdlWindow::drawRect(SDL_Surface* surface, SDL_Point offset, const SDL_Rect& srcRect)
321{
322 WINPR_ASSERT(surface);
323 SDL_Rect dstRect = { offset.x + srcRect.x, offset.y + srcRect.y, srcRect.w, srcRect.h };
324 return blit(surface, srcRect, dstRect);
325}
326
327bool SdlWindow::drawRects(SDL_Surface* surface, SDL_Point offset,
328 const std::vector<SDL_Rect>& rects)
329{
330 if (rects.empty())
331 {
332 return drawRect(surface, offset, { 0, 0, surface->w, surface->h });
333 }
334 for (auto& srcRect : rects)
335 {
336 if (!drawRect(surface, offset, srcRect))
337 return false;
338 }
339 return true;
340}
341
342bool SdlWindow::drawScaledRect(SDL_Surface* surface, const SDL_FPoint& scale,
343 const SDL_Rect& srcRect)
344{
345 SDL_Rect dstRect = {};
346 float ix = 0.0f;
347 float iy = 0.0f;
348 const auto modx = std::modf(static_cast<float>(srcRect.x) * scale.x, &ix);
349 const auto mody = std::modf(static_cast<float>(srcRect.y) * scale.y, &iy);
350 auto sw = std::ceil(static_cast<float>(srcRect.w) * scale.x) + std::ceil(modx);
351 auto sh = std::ceil(static_cast<float>(srcRect.h) * scale.y) + std::ceil(mody);
352 dstRect.x = static_cast<Sint32>(ix);
353 dstRect.w = static_cast<Sint32>(sw);
354 dstRect.y = static_cast<Sint32>(iy);
355 dstRect.h = static_cast<Sint32>(sh);
356 return blit(surface, srcRect, dstRect);
357}
358
359bool SdlWindow::drawScaledRects(SDL_Surface* surface, const SDL_FPoint& scale,
360 const std::vector<SDL_Rect>& rects)
361{
362 if (rects.empty())
363 {
364 return drawScaledRect(surface, scale, { 0, 0, surface->w, surface->h });
365 }
366 for (const auto& srcRect : rects)
367 {
368 if (!drawScaledRect(surface, scale, srcRect))
369 return false;
370 }
371 return true;
372}
373
374bool SdlWindow::fill(Uint8 r, Uint8 g, Uint8 b, Uint8 a)
375{
376 if (_renderer)
377 {
378 ensureRenderTarget();
379 if (!SDL_SetRenderTarget(_renderer, _renderTarget))
380 return false;
381 if (!SDL_SetRenderDrawColor(_renderer, r, g, b, a))
382 return false;
383 return SDL_RenderClear(_renderer);
384 }
385 return fill(_window, r, g, b, a);
386}
387
388bool SdlWindow::fill(SDL_Window* window, Uint8 r, Uint8 g, Uint8 b, Uint8 a)
389{
390 auto surface = SDL_GetWindowSurface(window);
391 if (!surface)
392 return false;
393 SDL_Rect rect = { 0, 0, surface->w, surface->h };
394 auto color = SDL_MapSurfaceRGBA(surface, r, g, b, a);
395
396 return SDL_FillSurfaceRect(surface, &rect, color);
397}
398
399static rdpMonitor makeMonitor(SDL_DisplayID id, const SDL_Rect& r, float factor,
400 bool forceAsPrimary)
401{
402 const float dpi = std::roundf(factor * 100.0f);
403
404 WINPR_ASSERT(r.w > 0);
405 WINPR_ASSERT(r.h > 0);
406
407 const auto primary = SDL_GetPrimaryDisplay();
408 const auto orientation = SDL_GetCurrentDisplayOrientation(id);
409 const auto rdp_orientation = sdl::utils::orientaion_to_rdp(orientation);
410
411 rdpMonitor monitor{};
412 monitor.orig_screen = id;
413 monitor.x = r.x;
414 monitor.y = r.y;
415 monitor.width = r.w;
416 monitor.height = r.h;
417 monitor.is_primary = forceAsPrimary || (id == primary);
418 monitor.attributes.desktopScaleFactor = static_cast<UINT32>(dpi);
419 monitor.attributes.deviceScaleFactor = 100;
420 monitor.attributes.orientation = rdp_orientation;
421 /* The physical size is in millimetres and ignored outside 10 to 10000 ([MS-RDPBCGR]
422 * 2.2.1.3.9.1, [MS-RDPEDISP] 2.2.2.2.1). SDL3 does not know a display's physical size, so
423 * leave it at 0 (not given) instead of sending the size in pixels as millimetres. */
424 monitor.attributes.physicalWidth = 0;
425 monitor.attributes.physicalHeight = 0;
426
427 const auto cat = SDL_LOG_CATEGORY_APPLICATION;
428 SDL_LogDebug(cat, "monitor.orig_screen %" PRIu32, monitor.orig_screen);
429 SDL_LogDebug(cat, "monitor.x %" PRId32, monitor.x);
430 SDL_LogDebug(cat, "monitor.y %" PRId32, monitor.y);
431 SDL_LogDebug(cat, "monitor.width %" PRId32, monitor.width);
432 SDL_LogDebug(cat, "monitor.height %" PRId32, monitor.height);
433 SDL_LogDebug(cat, "monitor.is_primary %" PRIu32, monitor.is_primary);
434 SDL_LogDebug(cat, "monitor.attributes.desktopScaleFactor %" PRIu32,
435 monitor.attributes.desktopScaleFactor);
436 SDL_LogDebug(cat, "monitor.attributes.deviceScaleFactor %" PRIu32,
437 monitor.attributes.deviceScaleFactor);
438 SDL_LogDebug(cat, "monitor.attributes.orientation %s",
439 freerdp_desktop_rotation_flags_to_string(monitor.attributes.orientation));
440 SDL_LogDebug(cat, "monitor.attributes.physicalWidth %" PRIu32,
441 monitor.attributes.physicalWidth);
442 SDL_LogDebug(cat, "monitor.attributes.physicalHeight %" PRIu32,
443 monitor.attributes.physicalHeight);
444 return monitor;
445}
446
447rdpMonitor SdlWindow::query(SDL_Window* window, SDL_DisplayID id, bool forceAsPrimary)
448{
449 if (!window)
450 return {};
451 return makeMonitor(id, rect(window, forceAsPrimary), SDL_GetWindowDisplayScale(window),
452 forceAsPrimary);
453}
454
455/* Dummy windows report wrong sizes on Wayland and X11, use the display. */
456static rdpMonitor queryDisplay(SDL_DisplayID id, bool forceAsPrimary)
457{
458 SDL_Rect r = {};
459 const SDL_DisplayMode* mode = SDL_GetDesktopDisplayMode(id);
460 if (!mode || !SDL_GetDisplayBounds(id, &r))
461 {
462 SDL_LogWarn(SDL_LOG_CATEGORY_VIDEO, "display %" PRIu32 ": %s", id, SDL_GetError());
463 return {};
464 }
465 if (forceAsPrimary)
466 {
467 r.x = 0;
468 r.y = 0;
469 }
470 r.w = static_cast<int>(std::roundf(static_cast<float>(r.w) * mode->pixel_density));
471 r.h = static_cast<int>(std::roundf(static_cast<float>(r.h) * mode->pixel_density));
472 return makeMonitor(id, r, mode->pixel_density * SDL_GetDisplayContentScale(id), forceAsPrimary);
473}
474
475SDL_Rect SdlWindow::rect(SDL_Window* window, bool forceAsPrimary)
476{
477 SDL_Rect rect = {};
478 if (!window)
479 return {};
480
481 if (!forceAsPrimary)
482 {
483 if (!SDL_GetWindowPosition(window, &rect.x, &rect.y))
484 return {};
485 }
486
487 if (!SDL_GetWindowSizeInPixels(window, &rect.w, &rect.h))
488 return {};
489
490 const auto flags = SDL_GetWindowFlags(window);
491 const auto mask = SDL_WINDOW_FULLSCREEN;
492 const auto fs = (flags & mask) == mask;
493 if (tryFallback(fs))
494 {
495 /* On wlroots compositors (Sway, river, etc.), windows that are hidden/unmapped
496 * don't get their actual display dimensions. The dummy window returns its creation size
497 * (64x64) instead of the display size. This causes validation errors since we require >=
498 * 200px. Workaround: If we got dimensions that are too small, query the display directly.
499 */
500
501 const auto displayID = SDL_GetDisplayForWindow(window);
502 SDL_Rect displayBounds = {};
503 if (SDL_GetDisplayBounds(displayID, &displayBounds))
504 {
505 if (forceAsPrimary)
506 {
507 rect.x = 0;
508 rect.y = 0;
509 }
510 else
511 {
512 /* The dummy window is centered on the display by the compositor, so its
513 * position is not the display origin. Use the display bounds as for w/h. */
514 rect.x = displayBounds.x;
515 rect.y = displayBounds.y;
516 }
517 rect.w = displayBounds.w;
518 rect.h = displayBounds.h;
519
520 const float contentScale = SDL_GetDisplayContentScale(displayID);
521 if (contentScale > 1.0f)
522 {
523 const auto fw = static_cast<float>(rect.w);
524 const auto fh = static_cast<float>(rect.h);
525 rect.w = static_cast<int>(std::roundf(fw * contentScale));
526 rect.h = static_cast<int>(std::roundf(fh * contentScale));
527 }
528 }
529 }
530
531 return rect;
532}
533
534SdlWindow::HighDPIMode SdlWindow::isHighDPIWindowsMode(SDL_Window* window)
535{
536 if (!window)
537 return MODE_INVALID;
538
539 const auto id = SDL_GetDisplayForWindow(window);
540 if (id == 0)
541 return MODE_INVALID;
542
543 const auto cs = SDL_GetDisplayContentScale(id);
544 const auto ds = SDL_GetWindowDisplayScale(window);
545 const auto pd = SDL_GetWindowPixelDensity(window);
546
547 /* mac os x style, but no HighDPI display */
548 if ((cs == 1.0f) && (ds == 1.0f) && (pd == 1.0f))
549 return MODE_NONE;
550
551 /* mac os x style HighDPI */
552 if ((cs == 1.0f) && (ds > 1.0f) && (pd > 1.0f))
553 return MODE_MACOS;
554
555 /* rest is windows style */
556 return MODE_WINDOWS;
557}
558
559/* Lazily create or recreate the persistent streaming GDI texture to match `surface`. */
560bool SdlWindow::ensureGdiTexture(SDL_Surface* surface)
561{
562 if (_gdiTexture && (_gdiTextureW == surface->w) && (_gdiTextureH == surface->h))
563 return true;
564 if (_gdiTexture)
565 SDL_DestroyTexture(_gdiTexture);
566 _gdiTexture = SDL_CreateTexture(_renderer, surface->format, SDL_TEXTUREACCESS_STREAMING,
567 surface->w, surface->h);
568 if (!_gdiTexture)
569 {
570 SDL_LogError(SDL_LOG_CATEGORY_RENDER, "SDL_CreateTexture: %s", SDL_GetError());
571 return false;
572 }
573 std::ignore = SDL_SetTextureBlendMode(_gdiTexture, SDL_BLENDMODE_NONE);
574 _gdiTextureW = surface->w;
575 _gdiTextureH = surface->h;
576 return true;
577}
578
579bool SdlWindow::blit(SDL_Surface* surface, const SDL_Rect& srcRect, SDL_Rect& dstRect)
580{
581 if (!_renderer || !surface)
582 return false;
583
584 ensureRenderTarget();
585
586 if (!ensureGdiTexture(surface))
587 return false;
588
589 /* Upload only the dirty region */
590 const auto* details = SDL_GetPixelFormatDetails(surface->format);
591 const int bpp = details ? details->bytes_per_pixel : 4;
592 const auto* pixels = static_cast<const uint8_t*>(surface->pixels) +
593 (1ll * srcRect.y * surface->pitch) + (1ll * srcRect.x * bpp);
594 if (!SDL_UpdateTexture(_gdiTexture, &srcRect, pixels, surface->pitch))
595 {
596 SDL_LogError(SDL_LOG_CATEGORY_RENDER, "SDL_UpdateTexture: %s", SDL_GetError());
597 return false;
598 }
599
600 /* Render onto persistent render target to accumulate dirty rects */
601 if (!SDL_SetRenderTarget(_renderer, _renderTarget))
602 return false;
603
604 SDL_FRect fsrc = { static_cast<float>(srcRect.x), static_cast<float>(srcRect.y),
605 static_cast<float>(srcRect.w), static_cast<float>(srcRect.h) };
606 SDL_FRect fdst = { static_cast<float>(dstRect.x), static_cast<float>(dstRect.y),
607 static_cast<float>(dstRect.w), static_cast<float>(dstRect.h) };
608 if (!SDL_RenderTexture(_renderer, _gdiTexture, &fsrc, &fdst))
609 {
610 SDL_LogError(SDL_LOG_CATEGORY_RENDER, "SDL_RenderTexture: %s", SDL_GetError());
611 return false;
612 }
613 return true;
614}
615
616void SdlWindow::updateSurface()
617{
618 if (!_renderer)
619 return;
620
621 ensureRenderTarget();
622
623 /* Copy accumulated render target to screen and present */
624 if (!SDL_SetRenderTarget(_renderer, nullptr))
625 return;
626 if (!SDL_RenderTexture(_renderer, _renderTarget, nullptr, nullptr))
627 return;
628 if (!SDL_RenderPresent(_renderer))
629 return;
630}
631
632bool SdlWindow::paintResizeFrame(SDL_Surface* surface, SDL_Point off, bool contentChanged,
633 const SDL_Rect& inset, bool fillRevealed, bool dashedBorder)
634{
635 if (!_renderer || !surface)
636 return false;
637 ensureRenderTarget();
638
639 const int prevW = _gdiTextureW;
640 const int prevH = _gdiTextureH;
641 if (!ensureGdiTexture(surface))
642 return false;
643 /* A recreated texture is empty, so upload even when the content did not change. */
644 const bool recreated = (_gdiTextureW != prevW) || (_gdiTextureH != prevH);
645 if ((contentChanged || recreated) &&
646 !SDL_UpdateTexture(_gdiTexture, nullptr, surface->pixels, surface->pitch))
647 return false;
648
649 if (!SDL_SetRenderTarget(_renderer, _renderTarget))
650 return false;
651
652 int ww = 0;
653 int wh = 0;
654 SDL_GetWindowSizeInPixels(_window, &ww, &wh);
655 /* The visible frame; the ring outside it (the resize band) stays fully transparent. */
656 const SDL_FRect frame = { static_cast<float>(inset.x), static_cast<float>(inset.y),
657 static_cast<float>(ww - inset.x - inset.w),
658 static_cast<float>(wh - inset.y - inset.h) };
659
660 /* Translucent fill in revealed area awaiting server frame. */
661 constexpr Uint8 kFillAlpha = 0x80;
662 std::ignore = SDL_SetRenderDrawBlendMode(_renderer, SDL_BLENDMODE_NONE);
663 std::ignore = SDL_SetRenderDrawColor(_renderer, 0, 0, 0, 0);
664 std::ignore = SDL_RenderClear(_renderer);
665 if (fillRevealed)
666 {
667 std::ignore = SDL_SetRenderDrawColor(_renderer, 0x2B, 0x2B, 0x2B, kFillAlpha);
668 std::ignore = SDL_RenderFillRect(_renderer, &frame);
669 }
670 SDL_FRect fdst = { static_cast<float>(off.x), static_cast<float>(off.y),
671 static_cast<float>(surface->w), static_cast<float>(surface->h) };
672 /* Anchored frame clipped to visible bounds. */
673 const SDL_Rect clip = { inset.x, inset.y, ww - inset.x - inset.w, wh - inset.y - inset.h };
674 std::ignore = SDL_SetRenderClipRect(_renderer, &clip);
675 std::ignore = SDL_RenderTexture(_renderer, _gdiTexture, nullptr, &fdst);
676 std::ignore = SDL_SetRenderClipRect(_renderer, nullptr);
677
678 /* Dashed border indicating pending resize target. */
679 if (dashedBorder)
680 {
681 std::ignore = SDL_SetRenderDrawColor(_renderer, 0xC8, 0xC8, 0xC8, 0xFF);
682 const float dash = 8.0F;
683 const float gap = 5.0F;
684 const float fx2 = frame.x + frame.w;
685 const float fy2 = frame.y + frame.h;
686 const float lo = frame.y + 0.5F;
687 const float by = fy2 - 0.5F;
688 const float lx = frame.x + 0.5F;
689 const float rx = fx2 - 0.5F;
690 const float step = dash + gap;
691 const auto steps = [step](float len)
692 { return (len <= 0.0F) ? 0 : static_cast<int>(std::ceil(len / step)); };
693 for (int i = 0; i < steps(fx2 - frame.x); i++)
694 {
695 const float x = frame.x + (static_cast<float>(i) * step);
696 const float x2 = (x + dash < fx2) ? (x + dash) : rx;
697 std::ignore = SDL_RenderLine(_renderer, x, lo, x2, lo);
698 std::ignore = SDL_RenderLine(_renderer, x, by, x2, by);
699 }
700 for (int i = 0; i < steps(fy2 - frame.y); i++)
701 {
702 const float y = frame.y + (static_cast<float>(i) * step);
703 const float y2 = (y + dash < fy2) ? (y + dash) : by;
704 std::ignore = SDL_RenderLine(_renderer, lx, y, lx, y2);
705 std::ignore = SDL_RenderLine(_renderer, rx, y, rx, y2);
706 }
707 }
708
709 if (!SDL_SetRenderTarget(_renderer, nullptr))
710 return false;
711 std::ignore = SDL_RenderTexture(_renderer, _renderTarget, nullptr, nullptr);
712 std::ignore = SDL_RenderPresent(_renderer);
713 return true;
714}
715
716SdlWindow SdlWindow::create(SDL_DisplayID id, const std::string& title, Uint32 flags, Uint32 width,
717 Uint32 height)
718{
719 flags |= SDL_WINDOW_HIGH_PIXEL_DENSITY;
720
721 SDL_Rect rect = { static_cast<int>(SDL_WINDOWPOS_CENTERED_DISPLAY(id)),
722 static_cast<int>(SDL_WINDOWPOS_CENTERED_DISPLAY(id)), static_cast<int>(width),
723 static_cast<int>(height) };
724
725 if ((flags & SDL_WINDOW_FULLSCREEN) != 0)
726 {
727 std::ignore = SDL_GetDisplayBounds(id, &rect);
728 }
729
730 SdlWindow window{ id, title, rect, flags };
731
732 if ((flags & SDL_WINDOW_FULLSCREEN) != 0)
733 {
734 window.setOffsetX(rect.x);
735 window.setOffsetY(rect.y);
736 }
737
738 return window;
739}
740
741SdlWindow SdlWindow::create(SDL_DisplayID id, const std::string& title, Uint32 flags,
742 const SDL_Rect& rect)
743{
744 return SdlWindow{ id, title, rect, flags };
745}
746
747/* Popup constructor: positioned relative to the parent origin. */
748SdlWindow::SdlWindow(SDL_Window* parent, const SDL_Rect& rect, bool transparent, bool tooltip)
749 : _initialW(rect.w), _initialH(rect.h)
750{
751 auto props = SDL_CreateProperties();
752 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_CREATE_PARENT_POINTER, parent);
753 SDL_SetBooleanProperty(props,
754 tooltip ? SDL_PROP_WINDOW_CREATE_TOOLTIP_BOOLEAN
755 : SDL_PROP_WINDOW_CREATE_MENU_BOOLEAN,
756 true);
757 SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FOCUSABLE_BOOLEAN, false);
758#if defined(SDL_PROP_WINDOW_CREATE_CONSTRAIN_POPUP_BOOLEAN)
759 /* The server places RemoteApp popups; a moved one would map clicks to other points. */
760 SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_CONSTRAIN_POPUP_BOOLEAN, false);
761#endif
762 SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, true);
763 /* Transparent so menus' genuine per-pixel alpha (corners/shadow) isn't rendered black. */
764 if (transparent)
765 SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_TRANSPARENT_BOOLEAN, true);
766 SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, rect.x);
767 SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, rect.y);
768 SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, rect.w);
769 SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, rect.h);
770 /* Hidden until first paint, like the app constructor. */
771 SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIDDEN_BOOLEAN, true);
772
773 _window = SDL_CreateWindowWithProperties(props);
774 SDL_DestroyProperties(props);
775 if (_window)
776 {
777 std::ignore = SDL_SyncWindow(_window);
778 _renderer = SDL_CreateRenderer(_window, nullptr);
779 _displayID = SDL_GetDisplayForWindow(_window);
780 }
781}
782
783SdlWindow SdlWindow::createPopup(SDL_Window* parent, const SDL_Rect& rect, bool transparent,
784 bool tooltip)
785{
786 return SdlWindow{ parent, rect, transparent, tooltip };
787}
788
789static SDL_Window* createDummy(SDL_DisplayID id)
790{
791 const auto x = SDL_WINDOWPOS_CENTERED_DISPLAY(id);
792 const auto y = SDL_WINDOWPOS_CENTERED_DISPLAY(id);
793 const int w = 64;
794 const int h = 64;
795
796 auto props = SDL_CreateProperties();
797 std::stringstream ss;
798 ss << "SdlWindow::query(" << id << ")";
799 SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, ss.str().c_str());
800 SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, x);
801 SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, y);
802 SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, w);
803 SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, h);
804
805 SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);
806 SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, false);
807 SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, true);
808 SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIDDEN_BOOLEAN, false);
809
810 auto window = SDL_CreateWindowWithProperties(props);
811 SDL_DestroyProperties(props);
812
813 /* Workaround: we need to properly position the window on the correct monitor
814 * before going fullscreen. Otherwise we will get the primary monitor details.
815 */
816 if (window)
817 {
818 SDL_Rect rect = {};
819 std::ignore = SDL_GetDisplayBounds(id, &rect);
820 std::ignore = SDL_SetWindowPosition(window, rect.x, rect.y);
821 std::ignore = SDL_SetWindowFullscreen(window, true);
822 }
823 return window;
824}
825
826rdpMonitor SdlWindow::query(SDL_DisplayID id, bool forceAsPrimary)
827{
828 if (sdl::utils::isWaylandDriver() || sdl::utils::isX11Driver())
829 return queryDisplay(id, forceAsPrimary);
830
831 std::unique_ptr<SDL_Window, void (*)(SDL_Window*)> window(createDummy(id), SDL_DestroyWindow);
832 if (!window)
833 return {};
834
835 std::unique_ptr<SDL_Renderer, void (*)(SDL_Renderer*)> renderer(
836 SDL_CreateRenderer(window.get(), nullptr), SDL_DestroyRenderer);
837
838 if (!SDL_SyncWindow(window.get()))
839 return {};
840
841 SDL_Event event{};
842 while (SDL_PollEvent(&event))
843 ;
844
845 return query(window.get(), id, forceAsPrimary);
846}
847
848SDL_Rect SdlWindow::rect(SDL_DisplayID id, bool forceAsPrimary)
849{
850 std::unique_ptr<SDL_Window, void (*)(SDL_Window*)> window(createDummy(id), SDL_DestroyWindow);
851 if (!window)
852 return {};
853
854 std::unique_ptr<SDL_Renderer, void (*)(SDL_Renderer*)> renderer(
855 SDL_CreateRenderer(window.get(), nullptr), SDL_DestroyRenderer);
856
857 if (!SDL_SyncWindow(window.get()))
858 return {};
859
860 SDL_Event event{};
861 while (SDL_PollEvent(&event))
862 ;
863
864 return rect(window.get(), forceAsPrimary);
865}
866
867bool SdlWindow::tryFallback(bool isFullscreen)
868{
869 /* If we define a custom env variable to use the wlroots hack
870 * then enable/disable according to this setting only.
871 */
872 const auto wlroots_hack = SDL_getenv("FREERDP_WLROOTS_HACK");
873 if (wlroots_hack != nullptr)
874 {
875 const auto enabled = strcmp(wlroots_hack, "0") != 0;
876 if (strcmp(wlroots_hack, "force") == 0)
877 isFullscreen = true;
878 return enabled && isFullscreen;
879 }
880
881 const auto platform = SDL_GetPlatform();
882 if ((platform == nullptr) || (strcmp(platform, "Linux") != 0))
883 return false;
884
885 const auto driver = SDL_GetCurrentVideoDriver();
886 if ((driver == nullptr) || (strcmp(driver, "wayland") != 0))
887 return false;
888
889 /* Check XDG_SESSION_DESKTOP and XDG_CURRENT_DESKTOP for wlroots-based
890 * compositors. The original check only matched Sway, but other wlroots
891 * compositors (Hyprland, river, etc.) have the same dummy-window sizing
892 * behavior where hidden/unmapped windows return 64x64 instead of the
893 * display size. Use strstr for substring matching since XDG_CURRENT_DESKTOP
894 * can be a colon-separated list (e.g. "sway:wlroots", "Hyprland").
895 */
896 auto isWlrootsCompositor = [](const char* value) -> bool
897 {
898 if (!value)
899 return false;
900 if (strstr(value, "sway") || strstr(value, "Sway") || strstr(value, "Hyprland") ||
901 strstr(value, "hyprland") || strstr(value, "river") || strstr(value, "wlroots"))
902 return true;
903 return false;
904 };
905
906 const auto xdg_session = SDL_getenv("XDG_SESSION_DESKTOP");
907 if (isWlrootsCompositor(xdg_session))
908 return isFullscreen;
909
910 const auto xdg_desktop = SDL_getenv("XDG_CURRENT_DESKTOP");
911 if (isWlrootsCompositor(xdg_desktop))
912 return isFullscreen;
913
914 return false;
915}
SdlWindow(const std::string &title, Sint32 startupX, Sint32 startupY, Sint32 width, Sint32 height, Uint32 flags)