FreeRDP
Loading...
Searching...
No Matches
sdl_rail.cpp
1
19#include <algorithm>
20#include <tuple>
21#include <utility>
22#include <vector>
23
24#include <winpr/assert.h>
25#include <winpr/platform.h>
26#include <winpr/cast.h>
27
28#include <freerdp/freerdp.h>
29#include <freerdp/log.h>
30#include <freerdp/codec/color.h>
31#include <freerdp/codec/region.h>
32#include <freerdp/client/rail.h>
33
34#include "sdl_rail.hpp"
35#include "sdl_context.hpp"
36#include "sdl_rail_platform.hpp"
37#include "sdl_types.hpp"
38#include "sdl_utils.hpp"
39#include "sdl_wayland.hpp"
40#include "sdl_x11.hpp"
41
42#define TAG CLIENT_TAG("sdl.rail")
43
44SdlRail::SdlRail(SdlContext* context) : _context(context)
45{
46}
47
48SdlRail::~SdlRail() = default;
49
50SdlRail* SdlRail::get(rdpContext* context)
51{
52 auto sdl = get_context(context);
53 if (!sdl)
54 return nullptr;
55 return &sdl->getRailChannelContext();
56}
57
58SdlRailWindow* SdlRail::getWindow(uint64_t id)
59{
60 auto it = _windows.find(id);
61 return (it == _windows.end()) ? nullptr : &it->second;
62}
63
64SdlRailWindow* SdlRail::getWindowBySdlId(SDL_WindowID id)
65{
66 if (id == 0)
67 return nullptr;
68 for (auto& it : _windows)
69 {
70 if (it.second.sdlId() == id)
71 return &it.second;
72 }
73 return nullptr;
74}
75
76/* Test if popup adjoins window edge from outside. */
77static bool adjoinsOutside(const SDL_Rect& popup, const SDL_Rect& win)
78{
79 SDL_Rect hit{};
80 if (SDL_GetRectIntersection(&popup, &win, &hit))
81 return false;
82 const SDL_Rect grown = { win.x - 1, win.y - 1, win.w + 2, win.h + 2 };
83 return SDL_GetRectIntersection(&popup, &grown, &hit);
84}
85
86/* Fullscreen popups (e.g. slide shows) act as toplevels and can parent popups. */
87static bool canParentPopup(const SdlRailWindow& w)
88{
89 return (!w.isPopup() || w.isFullscreen()) && (w.window() != nullptr);
90}
91
92SdlRailWindow* SdlRail::resolveParent(uint64_t ownerId)
93{
94 auto* owner = getWindow(ownerId);
95 if (owner && canParentPopup(*owner))
96 return owner;
97 return nullptr;
98}
99
100/* Pick a popup's parent app window: ownerWindowId -> geometric containment -> focused -> any app.
101 */
102SdlRailWindow* SdlRail::resolvePopupParent(const SdlRailWindow& popup)
103{
104 SdlRailWindow* chosen = resolveParent(popup.owner());
105 if (!chosen)
106 {
107 const SDL_Rect pr = popup.windowRect();
108 const SDL_Point origin = { pr.x, pr.y };
109 for (uint32_t id : _zOrder)
110 {
111 auto* w = getWindow(id);
112 if (!w || !canParentPopup(*w))
113 continue;
114 const SDL_Rect wr = w->windowRect();
115 if (SDL_PointInRect(&origin, &wr))
116 {
117 chosen = w;
118 break;
119 }
120 }
121 if (!chosen)
122 {
123 for (auto& other : _windows)
124 {
125 auto& w = other.second;
126 if (!canParentPopup(w))
127 continue;
128 const SDL_Rect wr = w.windowRect();
129 if (SDL_PointInRect(&origin, &wr))
130 {
131 chosen = &w;
132 break;
133 }
134 }
135 }
136 }
137 if (!chosen)
138 chosen = resolveParent(_focusedAppId);
139 if (!chosen)
140 {
141 for (auto& other : _windows)
142 {
143 auto& w = other.second;
144 if (canParentPopup(w))
145 {
146 chosen = &w;
147 break;
148 }
149 }
150 }
151 return chosen;
152}
153
154/* Identify companion shadow frames adjoining owner window. */
155bool SdlRail::isShadowFrame(const SdlRailWindow& popup)
156{
157 if (!popup.isLayered())
158 return false;
159 const SDL_Rect pr = popup.windowRect();
160 for (auto& other : _windows)
161 {
162 auto& w = other.second;
163 if (canParentPopup(w) && adjoinsOutside(pr, w.windowRect()))
164 return true;
165 }
166 return false;
167}
168
169bool SdlRail::ownsWindow(SDL_WindowID id)
170{
171 std::unique_lock lock(_windowsLock);
172 return getWindowBySdlId(id) != nullptr;
173}
174
175void SdlRail::invalidateWindow(SDL_WindowID id)
176{
177 std::unique_lock lock(_windowsLock);
178 auto* appWindow = getWindowBySdlId(id);
179 if (!appWindow)
180 return;
181 appWindow->invalidateAll();
182 lock.unlock();
183 (void)sdl_push_user_event(SDL_EVENT_USER_UPDATE);
184}
185
186/* Caller holds _windowsLock. */
187void SdlRail::sendSystemCommand(SdlRailWindow* appWindow, uint16_t command)
188{
189 if (!_rail || !_rail->ClientSystemCommand)
190 return;
191 RAIL_SYSCOMMAND_ORDER syscommand = {};
192 syscommand.windowId = static_cast<UINT32>(appWindow->id());
193 syscommand.command = command;
194 std::ignore = _rail->ClientSystemCommand(_rail, &syscommand);
195}
196
197void SdlRail::sendWorkArea(const SDL_Rect& area)
198{
199 if (!_rail || !_rail->ClientSystemParam || (area.w <= 0) || (area.h <= 0))
200 return;
201 if (SDL_RectsEqual(&area, &_sentWorkArea))
202 return;
203
204 RAIL_SYSPARAM_ORDER param = {};
205 /* ClientSystemParam dispatches on the params mask, not .param. */
206 param.params = SPI_MASK_SET_WORK_AREA;
207 param.workArea.left = WINPR_ASSERTING_INT_CAST(UINT16, area.x);
208 param.workArea.top = WINPR_ASSERTING_INT_CAST(UINT16, area.y);
209 param.workArea.right = WINPR_ASSERTING_INT_CAST(UINT16, area.x + area.w);
210 param.workArea.bottom = WINPR_ASSERTING_INT_CAST(UINT16, area.y + area.h);
211 if (_rail->ClientSystemParam(_rail, &param) == CHANNEL_RC_OK)
212 _sentWorkArea = area;
213}
214
215void SdlRail::handleMaximized(SDL_WindowID id)
216{
217 std::unique_lock lock(_windowsLock);
218 auto* appWindow = getWindowBySdlId(id);
219 if (!appWindow || appWindow->isPopup() || !appWindow->window())
220 return;
221 /* Skip the echo of reconcile's own SDL_MaximizeWindow (railMaximized already set). */
222 if (appWindow->railMaximized())
223 {
224 WLog_DBG(TAG, "local maximize skipped id=0x%08" PRIx32 " (already rail-maximized)",
225 static_cast<UINT32>(appWindow->id()));
226 return;
227 }
228 appWindow->setRailMaximized(true);
229 /* Force full repaint (skips dirty-rect). */
230 appWindow->invalidateAll();
231 /* Defer SC_MAXIMIZE if modal move loop is still open. */
232 if (appWindow->loopEndPending())
233 {
234 appWindow->deferMaximize();
235 WLog_DBG(TAG, "local maximize deferred id=0x%08" PRIx32 " (modal loop open)",
236 static_cast<UINT32>(appWindow->id()));
237 }
238 else
239 {
240 WLog_DBG(TAG, "local maximize id=0x%08" PRIx32 " -> SC_MAXIMIZE",
241 static_cast<UINT32>(appWindow->id()));
242 sendSystemCommand(appWindow, SC_MAXIMIZE);
243 }
244 lock.unlock();
245 (void)sdl_push_user_event(SDL_EVENT_USER_UPDATE);
246}
247
248void SdlRail::handleMinimized(SDL_WindowID id)
249{
250 std::unique_lock lock(_windowsLock);
251 auto* appWindow = getWindowBySdlId(id);
252 if (!appWindow || appWindow->isPopup() || !appWindow->window())
253 return;
254 if (appWindow->railMinimized())
255 return; /* echo of reconcile's own SDL_MinimizeWindow */
256 appWindow->setRailMinimized(true);
257 WLog_DBG(TAG, "local minimize id=0x%08" PRIx32 " -> SC_MINIMIZE",
258 static_cast<UINT32>(appWindow->id()));
259 sendSystemCommand(appWindow, SC_MINIMIZE);
260}
261
262void SdlRail::handleClose(SDL_WindowID id)
263{
264 std::unique_lock lock(_windowsLock);
265 auto* appWindow = getWindowBySdlId(id);
266 if (!appWindow || !appWindow->window())
267 return;
268 WLog_DBG(TAG, "local close id=0x%08" PRIx32 " -> SC_CLOSE",
269 static_cast<UINT32>(appWindow->id()));
270 sendSystemCommand(appWindow, SC_CLOSE);
271}
272
273void SdlRail::handleRestored(SDL_WindowID id)
274{
275 std::unique_lock lock(_windowsLock);
276 auto* appWindow = getWindowBySdlId(id);
277 if (!appWindow || appWindow->isPopup() || !appWindow->window())
278 return;
279 /* Resolve which state a RESTORED event ends; both clear = echo of our own restore, skip. */
280 if (appWindow->railMinimized())
281 appWindow->setRailMinimized(false);
282 else if (appWindow->railMaximized())
283 appWindow->setRailMaximized(false);
284 else
285 return;
286 WLog_DBG(TAG, "local restore id=0x%08" PRIx32 " -> SC_RESTORE",
287 static_cast<UINT32>(appWindow->id()));
288 appWindow->invalidateAll();
289 sendSystemCommand(appWindow, SC_RESTORE);
290 lock.unlock();
291 (void)sdl_push_user_event(SDL_EVENT_USER_UPDATE);
292}
293
294void SdlRail::handleFocus(SDL_WindowID id, bool gained)
295{
296 std::unique_lock lock(_windowsLock);
297 auto appWindow = getWindowBySdlId(id);
298 if (!appWindow || (appWindow->isPopup() && !appWindow->isFullscreen()) || !appWindow->window())
299 return;
300
301 /* Ignore focus loss while dragging: mapping restore shadow frames can steal focus
302 * and cancel the server modal loop. */
303 if (!gained && (_localMove.id == static_cast<uint32_t>(appWindow->id())))
304 return;
305
306 /* Fallback parent for orphaned popups. */
307 if (gained)
308 _focusedAppId = appWindow->id();
309
310 /* Only report activation, never deactivation (see the X11 client, #13422).
311 * The server treats a deactivation of the owner as "no active window in
312 * this application" and closes popups/menus that have just been opened
313 * when the compositor moves focus to them. The activation that follows
314 * (or an explicit click, see ensureActive()) is enough to route input. */
315 if (!gained)
316 return;
317
318 /* A window owned by this one is currently active (e.g. a profile card or
319 * a transient dialog). On a real desktop the owner is only activated by a
320 * click; focus-follows-mouse compositors hand focus back on hover, which
321 * would make the server close the owned popup. Leave that to
322 * ensureActive() (explicit click). */
323 const auto* active = getWindow(_clientActiveId);
324 if (active && (active != appWindow) &&
325 (active->owner() == static_cast<uint32_t>(appWindow->id())))
326 return;
327
328 /* ClientActivate only. Do NOT SDL_RaiseWindow to avoid WM focus loops. */
329 const auto wid = static_cast<uint32_t>(appWindow->id());
330 sendClientActivate(wid, true);
331}
332
333/* Send RAIL_ACTIVATE_ORDER and track the active window id. Caller holds _windowsLock. */
334void SdlRail::sendClientActivate(uint32_t wid, bool enabled)
335{
336 if (!_rail || !_rail->ClientActivate)
337 return;
338 if (enabled)
339 _clientActiveId = wid;
340 RAIL_ACTIVATE_ORDER activate = {};
341 activate.windowId = wid;
342 activate.enabled = enabled;
343 std::ignore = _rail->ClientActivate(_rail, &activate);
344}
345
346/* Activate clicked window on server to route input correctly. */
347void SdlRail::ensureActive(SDL_WindowID id)
348{
349 std::unique_lock lock(_windowsLock);
350 auto* appWindow = getWindowBySdlId(id);
351 if (!appWindow || (appWindow->isPopup() && !appWindow->isFullscreen()) || !appWindow->window())
352 return;
353 const auto wid = static_cast<uint32_t>(appWindow->id());
354 if (wid == _clientActiveId)
355 return;
356 sendClientActivate(wid, true);
357}
358
359bool SdlRail::translateToServer(SDL_WindowID id, float& x, float& y)
360{
361 std::unique_lock lock(_windowsLock);
362 auto* appWindow = getWindowBySdlId(id);
363 if (!appWindow)
364 return false;
365 SDL_FPoint rpos = { x, y };
366 if (auto* renderer = appWindow->renderer())
367 (void)SDL_RenderCoordinatesFromWindow(renderer, x, y, &rpos.x, &rpos.y);
368 /* Window-local to server-absolute, aligned with surface blit anchor. */
369 const SDL_Point origin = appWindow->serverOrigin();
370 x = rpos.x + static_cast<float>(origin.x);
371 y = rpos.y + static_cast<float>(origin.y);
372 return true;
373}
374
375SdlRailWindow* SdlRail::addWindow(uint64_t id, const SDL_Rect& rect)
376{
377 /* Move deleted entry aside so new window can reuse the ID. */
378 auto it = _windows.find(id);
379 if ((it != _windows.end()) && it->second.isDeleted())
380 {
381 WLog_DBG(TAG, "window recreate id=0x%08" PRIx32 " reused a deleted entry",
382 static_cast<uint32_t>(id));
383 _deadWindows.insert(_windows.extract(it));
384 }
385 /* emplace is a no-op if the id already exists; either way first->second is the window. */
386 auto res = _windows.emplace(std::piecewise_construct, std::forward_as_tuple(id),
387 std::forward_as_tuple(id, rect));
388 return &res.first->second;
389}
390
391void SdlRail::enableRemoteAppMode(bool enable)
392{
393 _enabled = enable;
394}
395
396bool SdlRail::paint(SDL_Surface* primary, SDL_PixelFormat fallbackFormat,
397 const std::vector<SDL_Rect>& damage)
398{
399 if (!_enabled)
400 return true;
401
402 std::unique_lock lock(_windowsLock);
403
404 /* Report workarea once (avoids maximizing under local panels). */
405 if (_sentWorkArea.w == 0)
406 {
407 SDL_Rect usable{};
408 if (SDL_GetDisplayUsableBounds(SDL_GetPrimaryDisplay(), &usable))
409 sendWorkArea(usable);
410 }
411
412 /* Cascade deletion to child popups whose SDL parent was deleted. */
413 for (auto& [wid, win] : _windows)
414 {
415 if (win.isDeleted() || !win.window())
416 continue;
417 for (SDL_Window* p = SDL_GetWindowParent(win.window()); p; p = SDL_GetWindowParent(p))
418 {
419 auto* owner = getWindowBySdlId(SDL_GetWindowID(p));
420 if (!owner || owner->isDeleted())
421 {
422 win.markDeleted();
423 break;
424 }
425 }
426 }
427
428 /* Erase deleted windows deepest child first to avoid SDL child double-free. */
429 std::vector<std::pair<size_t, uint64_t>> dead;
430 for (auto& [wid, win] : _windows)
431 {
432 if (!win.isDeleted())
433 continue;
434 size_t depth = 0;
435 for (SDL_Window* p = win.window() ? SDL_GetWindowParent(win.window()) : nullptr; p;
436 p = SDL_GetWindowParent(p))
437 depth++;
438 dead.emplace_back(depth, wid);
439 }
440 std::sort(dead.begin(), dead.end(),
441 [](const auto& a, const auto& b) { return a.first > b.first; });
442 for (const auto& [depth, wid] : dead)
443 _windows.erase(wid);
444 /* Windows displaced by a same-id recreate (addWindow) also die here, on the main thread. */
445 _deadWindows.clear();
446
447 /* App windows first so popup parents exist before their popups. */
448 for (auto& it : _windows)
449 {
450 auto& win = it.second;
451 if (win.isPopup())
452 continue;
453 /* Parent owned dialogs to owner. */
454 SDL_Window* parent = nullptr;
455 SDL_Rect parentRect{};
456 auto* owner = resolveParent(win.owner());
457 if (owner && (owner != &win))
458 {
459 parent = owner->window();
460 parentRect = owner->outerRect(); /* the SDL window's on-screen geometry */
461 }
462 win.paint(primary, fallbackFormat, damage, parent, parentRect);
463 /* Adopt WM-refused geometry and update server. */
464 SDL_Rect refusedOuter{};
465 if (win.takeWmOverride(refusedOuter))
466 {
467 const SDL_Rect rect = win.serverRect(refusedOuter);
468 const SDL_Rect cur = win.windowRect();
469 WLog_DBG(TAG,
470 "wm-refused adopt id=0x%08" PRIx32
471 " outer=%dx%d -> srv=%d,%d %dx%d (was %dx%d)",
472 static_cast<uint32_t>(win.id()), refusedOuter.w, refusedOuter.h, rect.x,
473 rect.y, rect.w, rect.h, cur.w, cur.h);
474 if (!SDL_RectsEqual(&rect, &cur))
475 sendClientWindowMove(&win, rect);
476 win.adoptLocalGeometry(rect);
477 }
478 }
479
480 /* Request full repaint after first window is realized (fixes reconnect blank windows). */
481 if (!_refreshSent)
482 {
483 for (auto& it : _windows)
484 {
485 if (it.second.isPopup() || !it.second.window())
486 continue;
487 auto* ctx = &_context->common()->context;
488 const auto dw = static_cast<UINT16>(
489 freerdp_settings_get_uint32(ctx->settings, FreeRDP_DesktopWidth));
490 const auto dh = static_cast<UINT16>(
491 freerdp_settings_get_uint32(ctx->settings, FreeRDP_DesktopHeight));
492 const RECTANGLE_16 all = { 0, 0, dw, dh };
493 if (ctx->update && ctx->update->RefreshRect)
494 (void)ctx->update->RefreshRect(ctx, 1, &all);
495 _refreshSent = true;
496 WLog_DBG(TAG, "refresh-rect sent (first app window realized)");
497 break;
498 }
499 }
500
501 for (auto& it : _windows)
502 {
503 auto& popup = it.second;
504 if (!popup.isPopup())
505 continue;
506
507 /* Latch shadow frame classification once identified. */
508 if (!popup.isFrame() && isShadowFrame(popup))
509 popup.setFrame(true);
510
511 /* Anchor drop shadow to adjoining visible popup. */
512 if (popup.isLayered())
513 {
514 constexpr int reach = 48; /* shadow offset+blur spread from its popup, server px */
515 const SDL_Rect sr = popup.windowRect();
516 bool anchored = false;
517 for (auto& other : _windows)
518 {
519 auto& cand = other.second;
520 if ((&cand == &popup) || !cand.isPopup() || cand.isLayered() || !cand.window() ||
521 ((SDL_GetWindowFlags(cand.window()) & SDL_WINDOW_HIDDEN) != 0))
522 continue;
523 SDL_Rect zone = cand.windowRect();
524 zone.x -= reach;
525 zone.y -= reach;
526 zone.w += 2 * reach;
527 zone.h += 2 * reach;
528 /* Shadow must be contained within reach zone to distinguish popup shadows from
529 * app frame edge shadows. */
530 const bool inside = (sr.x >= zone.x) && (sr.y >= zone.y) &&
531 (sr.x + sr.w <= zone.x + zone.w) &&
532 (sr.y + sr.h <= zone.y + zone.h);
533 if (inside)
534 {
535 anchored = true;
536 break;
537 }
538 }
539 popup.setShadowAnchored(anchored);
540 }
541
542 SdlRailWindow* chosen = nullptr;
543 /* Existing popups have an immutable SDL parent; position against it. */
544 if (auto* pw = popup.window())
545 {
546 SDL_Window* p = SDL_GetWindowParent(pw);
547 chosen = p ? getWindowBySdlId(SDL_GetWindowID(p)) : nullptr;
548 }
549 else
550 {
551 chosen = resolvePopupParent(popup);
552 if (!chosen)
553 WLog_DBG(TAG, "popup id=0x%08" PRIx32 " has no parent app window",
554 static_cast<UINT32>(popup.id()));
555 }
556
557 SDL_Window* parent = nullptr;
558 SDL_Rect parentRect{};
559 if (chosen)
560 {
561 parent = chosen->window();
562 parentRect = chosen->outerRect(); /* the SDL window's on-screen geometry */
563 }
564 popup.paint(primary, fallbackFormat, damage, parent, parentRect);
565 }
566
567 /* All live windows now have real X11 handles: realize the server's z-order. */
568 applyZOrder();
569 return true;
570}
571
572/* Caller holds _windowsLock (main thread). */
573void SdlRail::applyZOrder()
574{
575 if (!_zOrderDirty)
576 return;
577 /* X11 only: Wayland/Win/macOS have no reparenting-safe, focus-neutral restack path here. */
578 if (!railPlatformCaps().supportsRestack)
579 {
580 _zOrderDirty = false;
581 return;
582 }
583 /* Never restack the window the WM is actively dragging; keep dirty and retry after the move. */
584 if (_localMove.id != 0)
585 {
586 WLog_VRB(TAG, "zorder apply deferred: local move 0x%08" PRIx32 " active", _localMove.id);
587 return;
588 }
589 /* Restack top-level windows (skip popups and hidden windows). */
590 std::vector<SDL_Window*> stack;
591 std::vector<uint32_t> applied;
592 stack.reserve(_zOrder.size());
593 applied.reserve(_zOrder.size());
594 for (uint32_t id : _zOrder)
595 {
596 auto* w = getWindow(id);
597 if (w && !w->isPopup() && w->window() &&
598 ((SDL_GetWindowFlags(w->window()) & SDL_WINDOW_HIDDEN) == 0))
599 {
600 stack.push_back(w->window());
601 applied.push_back(id);
602 }
603 }
604 /* Dedup against the restacked set; menus and unrealized windows are filtered out. */
605 if (applied == _appliedZOrder)
606 {
607 _zOrderDirty = false;
608 return;
609 }
610 if (stack.size() >= 2)
611 {
612 std::ignore = sdl_x11_restack_windows(stack);
613 }
614
615 _appliedZOrder = std::move(applied);
616 _zOrderDirty = false;
617}
618
619UINT SdlRail::updateWindowFromSurface(gdiGfxSurface* surface)
620{
621 if (!surface)
622 return CHANNEL_RC_OK;
623
624 std::unique_lock lock(_windowsLock);
625 auto appWindow = getWindow(surface->windowId);
626 if (!appWindow)
627 {
628 /* Drop GFX surface for unknown windows (repaints later). */
629 WLog_VRB(TAG, "gfx surface for untracked id=0x%08" PRIx64, surface->windowId);
630 return CHANNEL_RC_OK;
631 }
632
633 /* Clamp mapped dimensions to allocated surface bounds. */
634 const uint32_t w =
635 std::min(surface->mappedWidth ? surface->mappedWidth : surface->width, surface->width);
636 const uint32_t h =
637 std::min(surface->mappedHeight ? surface->mappedHeight : surface->height, surface->height);
638
639 /* Consume per-frame damage. */
640 UINT32 nbRects = 0;
641 const RECTANGLE_16* rects = region16_rects(&surface->invalidRegion, &nbRects);
642 /* Skip undamaged surfaces during EndFrame. */
643 const bool remapped = appWindow->takeSurfaceChange(surface->surfaceId);
644 if ((nbRects == 0) && !remapped)
645 return CHANNEL_RC_OK;
646 appWindow->updateGfxSurface(surface->data, surface->scanline, w, h, rects, nbRects,
647 surface->format);
648 region16_clear(&surface->invalidRegion);
649
650 (void)sdl_push_user_event(SDL_EVENT_USER_UPDATE);
651 return CHANNEL_RC_OK;
652}
653
654UINT SdlRail::UpdateWindowFromSurface(RdpgfxClientContext* context, gdiGfxSurface* surface)
655{
656 WINPR_ASSERT(context);
657 auto gdi = static_cast<rdpGdi*>(context->custom);
658 if (!gdi || !gdi->context)
659 return CHANNEL_RC_OK;
660 auto sdl = get_context(gdi->context);
661 if (!sdl)
662 return CHANNEL_RC_OK;
663 return sdl->getRailChannelContext().updateWindowFromSurface(surface);
664}
665
666bool SdlRail::init(RailClientContext* rail)
667{
668 _rail = rail;
669 if (!rail)
670 return false;
671
672 registerUpdateCallbacks(_context->context()->update);
673
674 {
675 std::unique_lock lock(_windowsLock);
676 /* Move previous windows to deadWindows for main-thread teardown. */
677 for (auto it = _windows.begin(); it != _windows.end();)
678 {
679 auto cur = it++;
680 _deadWindows.insert(_windows.extract(cur));
681 }
682 auto* settings = _context->context()->settings;
683 const uint32_t caches =
684 freerdp_settings_get_uint32(settings, FreeRDP_RemoteAppNumIconCaches);
685 _iconCacheEntries =
686 freerdp_settings_get_uint32(settings, FreeRDP_RemoteAppNumIconCacheEntries);
687 _iconCache.assign(static_cast<size_t>(caches) * _iconCacheEntries, {});
688 }
689
690 rail->custom = this;
691 rail->ServerExecuteResult = SdlRail::server_execute_result;
692 /* ServerSystemParam not implemented: the server only sends screensaver state here. */
693 rail->ServerLocalMoveSize = SdlRail::server_local_move_size;
694 rail->ServerMinMaxInfo = SdlRail::server_min_max_info;
695 /* Keep default ServerHandshake. */
696
697 WLog_WARN(TAG, "RemoteApp/RAIL support in the SDL client is experimental");
698 const RailPlatformCaps& caps = railPlatformCaps();
699 WLog_DBG(TAG, "RAIL channel initialized: driver=%s positionsReadable=%d transparentWindows=%d",
700 sdl::utils::isWaylandDriver() ? "wayland"
701 : (sdl::utils::isX11Driver() ? "x11" : "other"),
702 caps.positionsReadable ? 1 : 0, caps.supportsTransparentWindows ? 1 : 0);
703#if SDL_VERSION_ATLEAST(3, 4, 10)
704 /* Synchronize WM-driven X11 resizes via _NET_WM_SYNC_REQUEST to prevent torn frames. */
705 if (sdl::utils::isX11Driver())
706 SDL_SetHint(SDL_HINT_VIDEO_X11_ENABLE_XSYNC_EXT, "1");
707#endif
708 return true;
709}
710
711bool SdlRail::uninit(WINPR_ATTR_UNUSED RailClientContext* rail)
712{
713 WINPR_ASSERT(rail);
714 /* Leave rail->custom set: the server callbacks only assert it, so clearing it would crash a
715 * late order in a release build. _enabled gates them instead. */
716 std::unique_lock lock(_windowsLock);
717 _refreshSent = false;
718 /* Reset session and move state on reconnect. */
719 _localMove = {};
720 _sessionMargins = {};
721 _sentWorkArea = {};
722 _clientActiveId = 0;
723 _focusedAppId = 0;
724 _zOrder.clear();
725 _appliedZOrder.clear();
726 _zOrderDirty = false;
727 _iconCache.clear();
728 _iconScratch = {};
729 /* Mark windows for main-thread teardown. */
730 WLog_DBG(TAG, "RAIL channel uninit, marking %zu windows for main-thread teardown",
731 _windows.size());
732 for (auto& kv : _windows)
733 kv.second.markDeleted();
734 _rail = nullptr;
735 _enabled = false;
736 lock.unlock();
737 (void)sdl_push_user_event(SDL_EVENT_USER_UPDATE);
738 return true;
739}
740
741void SdlRail::registerUpdateCallbacks(rdpUpdate* update)
742{
743 WINPR_ASSERT(update);
744 auto window = update->window;
745 WINPR_ASSERT(window);
746
747 window->WindowCreate = SdlRail::window_common;
748 window->WindowUpdate = SdlRail::window_common;
749 window->WindowIcon = SdlRail::window_icon;
750 window->WindowCachedIcon = SdlRail::window_cached_icon;
751 window->WindowDelete = SdlRail::window_delete;
752 window->MonitoredDesktop = SdlRail::monitored_desktop;
753 window->NonMonitoredDesktop = SdlRail::non_monitored_desktop;
754}
755
756/* --- server callbacks --- */
757
758UINT SdlRail::server_execute_result(RailClientContext* context,
759 const RAIL_EXEC_RESULT_ORDER* execResult)
760{
761 WINPR_ASSERT(context);
762 WINPR_ASSERT(execResult);
763 auto rail = static_cast<SdlRail*>(context->custom);
764 WINPR_ASSERT(rail);
765
766 if (execResult->execResult != RAIL_EXEC_S_OK)
767 {
768 WLog_ERR(TAG, "RAIL exec error: execResult=0x%04" PRIx16 " rawResult=0x%08" PRIx32,
769 execResult->execResult, execResult->rawResult);
770 freerdp_abort_connect_context(rail->_context->context());
771 }
772 else
773 {
774 WLog_DBG(TAG, "RemoteApp exec OK, enabling RAIL mode");
775 rail->enableRemoteAppMode(true);
776 }
777 return CHANNEL_RC_OK;
778}
779
780UINT SdlRail::server_local_move_size(RailClientContext* context,
781 const RAIL_LOCALMOVESIZE_ORDER* localMoveSize)
782{
783 WINPR_ASSERT(context);
784 WINPR_ASSERT(localMoveSize);
785
786 /* Start native move/resize to avoid dual-authority geometry races. */
787 if (localMoveSize->isMoveSizeStart && (localMoveSize->moveSizeType >= RAIL_WMSZ_LEFT) &&
788 (localMoveSize->moveSizeType <= RAIL_WMSZ_MOVE))
789 {
790 WLog_DBG(TAG, "server move/size start id=0x%08" PRIx32 " type=%" PRIu16 " pos=%d,%d",
791 localMoveSize->windowId, localMoveSize->moveSizeType, localMoveSize->posX,
792 localMoveSize->posY);
793 (void)sdl_push_user_event(SDL_EVENT_USER_RAIL_MOVE, localMoveSize->windowId,
794 static_cast<int>(localMoveSize->moveSizeType));
795 }
796 else if (!localMoveSize->isMoveSizeStart)
797 {
798 WLog_DBG(TAG, "server move/size end id=0x%08" PRIx32, localMoveSize->windowId);
799 /* Server ended the move/size: resume applying geometry + input (covers Wayland move). */
800 auto rail = static_cast<SdlRail*>(context->custom);
801 if (rail)
802 {
803 std::unique_lock lock(rail->_windowsLock);
804 /* Ignore server END order during Wayland resize unless unstarted. */
805 const bool ownedByWaylandResize = rail->_localMove.wayland &&
806 (rail->_localMove.id == localMoveSize->windowId) &&
807 rail->_localMove.sawResize;
808 if (!ownedByWaylandResize)
809 {
810 if (auto* appWindow = rail->getWindow(localMoveSize->windowId))
811 appWindow->setLocalMoveActive(false);
812 if (rail->_localMove.id == localMoveSize->windowId)
813 {
814 if (rail->_localMove.wayland)
815 {
816 /* Wayland drag completed. */
817 rail->_localMove = {};
818 if (auto* appWindow = rail->getWindow(localMoveSize->windowId))
819 appWindow->invalidateAll();
820 (void)sdl_push_user_event(SDL_EVENT_USER_UPDATE);
821 }
822 else
823 {
824 /* Server ended drag loop early. */
825 rail->_localMove.serverEnded = true;
826 }
827 }
828 }
829 /* Execute deferred actions now that modal loop has closed. */
830 auto* appWindow = rail->getWindow(localMoveSize->windowId);
831 if (appWindow && appWindow->loopEndPending())
832 {
833 const auto actions = appWindow->takeLoopEnd();
834 if (actions.maximize && appWindow->railMaximized())
835 {
836 WLog_DBG(TAG, "deferred SC_MAXIMIZE id=0x%08" PRIx32, localMoveSize->windowId);
837 rail->sendSystemCommand(appWindow, SC_MAXIMIZE);
838 }
839 if (actions.snap)
840 {
841 /* Send deferred snap resize. */
842 WLog_DBG(TAG, "deferred snap resize id=0x%08" PRIx32 " rect=%d,%d %dx%d",
843 localMoveSize->windowId, actions.snapRect.x, actions.snapRect.y,
844 actions.snapRect.w, actions.snapRect.h);
845 rail->sendClientWindowMove(appWindow, actions.snapRect);
846 }
847 /* WM resize can land after button-up (e.g. KDE tiling) while syncGeometry was
848 * latched out. Sync post-loop geometry now; also needed for deferred snaps where
849 * snapRect preceded the WM's final tile size. */
850 if (appWindow->window() && !appWindow->effectivelyMaximized() &&
851 !appWindow->localMoveSizeChanged())
852 {
853 const SDL_WindowID sdlId = appWindow->sdlId();
854 lock.unlock();
855 rail->syncGeometry(sdlId);
856 }
857 }
858 }
859 }
860 return CHANNEL_RC_OK;
861}
862
863/* X11 only: suppress button-up during modal loop. */
864bool SdlRail::suppressServerInput(SDL_WindowID id)
865{
866 std::unique_lock lock(_windowsLock);
867 if (_localMove.id == 0)
868 return false;
869 auto* appWindow = getWindowBySdlId(id);
870 return appWindow && appWindow->localMoveActive() && !_localMove.wayland;
871}
872
873bool SdlRail::suppressServerMotion(SDL_WindowID id)
874{
875 std::unique_lock lock(_windowsLock);
876 if (_localMove.id == 0)
877 return false;
878 auto* appWindow = getWindowBySdlId(id);
879 /* Suppress motion events during active local move. */
880 return appWindow && appWindow->localMoveActive();
881}
882
883/* RAIL_WMSZ_* -> _NET_WM_MOVERESIZE direction (0..7 = resize edges, 8 = move). */
884static int railToNetDirection(uint16_t moveType)
885{
886 switch (moveType)
887 {
888 case RAIL_WMSZ_TOPLEFT:
889 return 0;
890 case RAIL_WMSZ_TOP:
891 return 1;
892 case RAIL_WMSZ_TOPRIGHT:
893 return 2;
894 case RAIL_WMSZ_RIGHT:
895 return 3;
896 case RAIL_WMSZ_BOTTOMRIGHT:
897 return 4;
898 case RAIL_WMSZ_BOTTOM:
899 return 5;
900 case RAIL_WMSZ_BOTTOMLEFT:
901 return 6;
902 case RAIL_WMSZ_LEFT:
903 return 7;
904 case RAIL_WMSZ_MOVE:
905 default:
906 return 8;
907 }
908}
909
910/* RAIL_WMSZ_* <-> XDG_TOPLEVEL_RESIZE_EDGE_* edge mappings. */
911static constexpr struct
912{
913 uint16_t rail;
914 uint32_t xdg;
915} kRailXdgEdges[] = {
916 { RAIL_WMSZ_TOP, 1 }, { RAIL_WMSZ_BOTTOM, 2 }, { RAIL_WMSZ_LEFT, 4 },
917 { RAIL_WMSZ_TOPLEFT, 5 }, { RAIL_WMSZ_BOTTOMLEFT, 6 }, { RAIL_WMSZ_RIGHT, 8 },
918 { RAIL_WMSZ_TOPRIGHT, 9 }, { RAIL_WMSZ_BOTTOMRIGHT, 10 },
919};
920
921/* RAIL_WMSZ_* -> XDG_TOPLEVEL_RESIZE_EDGE_* (0 = none, e.g. RAIL_WMSZ_MOVE). */
922static uint32_t railToXdgEdge(uint16_t moveType)
923{
924 for (const auto& e : kRailXdgEdges)
925 if (e.rail == moveType)
926 return e.xdg;
927 return 0;
928}
929
930/* Which window edges a RAIL_WMSZ_* drag moves; the opposite edges stay anchored. */
931struct RailEdges
932{
933 bool left = false;
934 bool right = false;
935 bool top = false;
936 bool bottom = false;
937};
938static RailEdges railEdges(uint16_t t)
939{
940 return { (t == RAIL_WMSZ_TOPLEFT) || (t == RAIL_WMSZ_LEFT) || (t == RAIL_WMSZ_BOTTOMLEFT),
941 (t == RAIL_WMSZ_TOPRIGHT) || (t == RAIL_WMSZ_RIGHT) || (t == RAIL_WMSZ_BOTTOMRIGHT),
942 (t == RAIL_WMSZ_TOPLEFT) || (t == RAIL_WMSZ_TOP) || (t == RAIL_WMSZ_TOPRIGHT),
943 (t == RAIL_WMSZ_BOTTOMLEFT) || (t == RAIL_WMSZ_BOTTOM) ||
944 (t == RAIL_WMSZ_BOTTOMRIGHT) };
945}
946
947void SdlRail::handleLocalMoveRequested(uint32_t windowId, uint16_t moveType)
948{
949 const bool wayland = sdl::utils::isWaylandDriver();
950 const bool x11 = sdl::utils::isX11Driver();
951 if (!wayland && !x11)
952 return;
953
954 std::unique_lock lock(_windowsLock);
955 auto appWindow = getWindow(windowId);
956 if (!appWindow || !appWindow->window() || appWindow->isPopup())
957 return;
958
959 const bool isMove = (moveType == RAIL_WMSZ_MOVE);
960 WLog_DBG(TAG, "local move start id=0x%08" PRIx32 " driver=%s type=%" PRIu16 " %s", windowId,
961 wayland ? "wayland" : "x11", moveType, isMove ? "move" : "resize");
962 bool started = false;
963 if (wayland)
964 {
965 /* Wayland: positions unreadable, sizes readable. */
966 if (isMove)
967 started = sdl_wayland_begin_move(appWindow->window());
968 else
969 started = sdl_wayland_begin_resize(appWindow->window(), railToXdgEdge(moveType));
970 if (started && !isMove)
971 {
972 _localMove = {}; /* clear all per-drag state as a unit before latching this drag */
973 _localMove.id = windowId;
974 _localMove.wayland = true;
975 _localMove.type = moveType;
976 }
977 }
978 else
979 {
980 /* X11: hand resize to WM, suppress input. */
981 started = sdl_x11_begin_move_size(appWindow->window(), railToNetDirection(moveType));
982 if (started)
983 {
984 _localMove = {}; /* clear all per-drag state as a unit before latching this drag */
985 _localMove.id = windowId;
986 _localMove.type = moveType;
987 /* Latch drag anchors for release calculation. */
988 _localMove.anchor = _lastPressServer;
989 _localMove.pointer = _lastPointerServer;
990 /* Clear prior loop-end state on new drag. */
991 appWindow->clearLoopEnd();
992 /* Set bit gravity to fixed edge. */
993 const RailEdges e = railEdges(moveType);
994 const int row = e.top ? 2 : (e.bottom ? 0 : 1);
995 const int col = e.left ? 2 : (e.right ? 0 : 1);
996 (void)sdl_x11_set_bit_gravity(appWindow->window(), row * 3 + col + 1);
997 }
998 }
999 /* Drive local frame during X11/Wayland drag. */
1000 if (started && (!wayland || !isMove))
1001 {
1002 appWindow->setLocalMoveActive(true);
1003 if (!isMove)
1004 {
1005 /* Anchor the stale frame to the fixed edge (opposite the dragged one). */
1006 const RailEdges e = railEdges(moveType);
1007 appWindow->setResizeAnchor(e.left, e.top);
1008 }
1009 }
1010 else if (!started)
1011 {
1012 WLog_WARN(TAG, "WM move failed for RAIL window 0x%08" PRIx32, windowId);
1013 }
1014}
1015
1016/* Send ClientWindowMove with full frame geometry including server margins (caller holds
1017 * _windowsLock). */
1018void SdlRail::sendClientWindowMove(SdlRailWindow* appWindow, const SDL_Rect& serverRect)
1019{
1020 const SDL_Rect fm = appWindow->frameMargins();
1021 RAIL_WINDOW_MOVE_ORDER move = {};
1022 move.windowId = static_cast<UINT32>(appWindow->id());
1023 move.left = WINPR_ASSERTING_INT_CAST(INT16, serverRect.x - fm.x);
1024 move.top = WINPR_ASSERTING_INT_CAST(INT16, serverRect.y - fm.y);
1025 move.right = WINPR_ASSERTING_INT_CAST(INT16, serverRect.x + serverRect.w + fm.w);
1026 move.bottom = WINPR_ASSERTING_INT_CAST(INT16, serverRect.y + serverRect.h + fm.h);
1027 if (_rail && _rail->ClientWindowMove &&
1028 (_rail->ClientWindowMove(_rail, &move) != CHANNEL_RC_OK))
1029 WLog_WARN(TAG, "ClientWindowMove failed for RAIL window 0x%08" PRIx32, move.windowId);
1030}
1031
1032/* Report final geometry and adopt locally. */
1033void SdlRail::reportAndAdopt(SdlRailWindow* appWindow, int x, int y, int w, int h)
1034{
1035 /* Local geometry -> server rect: strip the client-side band insets. */
1036 const SDL_Rect rect = appWindow->serverRect({ x, y, w, h });
1037 /* Report outer frame to server (inflate by margins). */
1038 const bool maximized = appWindow->effectivelyMaximized();
1039 /* Report geometry if changed. */
1040 const SDL_Rect cur = appWindow->windowRect();
1041 const bool unchanged = SDL_RectsEqual(&rect, &cur);
1042 if (!maximized && !unchanged)
1043 sendClientWindowMove(appWindow, rect);
1044
1045 /* Adopt WM final geometry and resume input routing. */
1046 if (maximized)
1047 appWindow->setLocalMoveActive(false); /* geometry is WM/server-owned while maximized */
1048 else
1049 appWindow->adoptLocalGeometry(rect);
1050 /* Repaint now: the last presented frame may still be the resize placeholder. */
1051 (void)sdl_push_user_event(SDL_EVENT_USER_UPDATE);
1052}
1053
1054void SdlRail::clampIntoDesktop(int& x, int& y, int w, int h) const
1055{
1056 auto* settings = _context->context()->settings;
1057 const int dw = static_cast<int>(freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth));
1058 const int dh = static_cast<int>(freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight));
1059 x = std::clamp(x, 0, std::max(0, dw - w));
1060 y = std::clamp(y, 0, std::max(0, dh - h));
1061}
1062
1063void SdlRail::noteDragResize(SDL_WindowID id, int w, int h)
1064{
1065 std::unique_lock lock(_windowsLock);
1066 if ((_localMove.id == 0) || _localMove.wayland || (_localMove.type != RAIL_WMSZ_MOVE))
1067 return;
1068 auto* appWindow = getWindowBySdlId(id);
1069 if (!appWindow || (static_cast<uint32_t>(appWindow->id()) != _localMove.id))
1070 return;
1071 /* Filter out reconcile echo from server drag-restore. */
1072 if (appWindow->localMoveSizeChanged())
1073 {
1074 const SDL_Rect outer = appWindow->outerRect();
1075 if ((w == outer.w) && (h == outer.h))
1076 return;
1077 }
1078 _localMove.wmSized = true;
1079 _localMove.wmSize = { w, h };
1080}
1081
1082/* Update server work area from maximized window bounds (caller holds _windowsLock). */
1083void SdlRail::reportMaximizedWorkArea(SdlRailWindow* appWindow)
1084{
1085 int w = 0;
1086 int h = 0;
1087 SDL_GetWindowSize(appWindow->window(), &w, &h);
1088 if ((w <= 0) || (h <= 0))
1089 return;
1090 int x = 0;
1091 int y = 0;
1092 if (railPlatformCaps().positionsReadable)
1093 SDL_GetWindowPosition(appWindow->window(), &x, &y);
1094
1095 /* Clip work area within primary display bounds. */
1096 SDL_Rect primary{};
1097 if (!SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &primary))
1098 return;
1099 const SDL_Rect area = { x, y, w, h };
1100 SDL_Rect overlap{};
1101 if (!SDL_GetRectIntersection(&area, &primary, &overlap) || !SDL_RectsEqual(&overlap, &area))
1102 return;
1103 sendWorkArea(area);
1104}
1105
1106void SdlRail::syncGeometry(SDL_WindowID id)
1107{
1108 std::unique_lock lock(_windowsLock);
1109 auto* w = getWindowBySdlId(id);
1110 if (!w || !w->window() || w->isDeleted() || w->isPopup())
1111 return;
1112
1113 /* Geometry is server/WM-owned when maximized or minimized. */
1114 if (w->effectivelyMaximized())
1115 {
1116 reportMaximizedWorkArea(w);
1117 return;
1118 }
1119 if (w->railMinimized())
1120 return;
1121
1122 const auto wid = static_cast<uint32_t>(w->id());
1123 /* Skip geometry sync while awaiting server restored rect. */
1124 if (w->localMoveActive() || (_localMove.id == wid) || w->loopEndPending() ||
1125 w->stateTransitionPending() || w->awaitingRestoreRect())
1126 return;
1127
1128 int lw = 0;
1129 int lh = 0;
1130 SDL_GetWindowSize(w->window(), &lw, &lh);
1131 if ((lw <= 0) || (lh <= 0))
1132 return;
1133
1134 /* Ignore invalid zero-content window size updates. */
1135 const SDL_Rect ins = w->insets();
1136 if (((lw - ins.x - ins.w) <= 0) || ((lh - ins.y - ins.h) <= 0))
1137 {
1138 WLog_DBG(TAG, "geometry sync id=0x%08" PRIx32 " %dx%d dropped (content collapsed)", wid, lw,
1139 lh);
1140 return;
1141 }
1142
1143 const SDL_Rect vis = w->outerRect();
1144 const bool posKnown = railPlatformCaps().positionsReadable;
1145 int lx = vis.x;
1146 int ly = vis.y;
1147 if (posKnown)
1148 SDL_GetWindowPosition(w->window(), &lx, &ly);
1149 else
1150 clampIntoDesktop(lx, ly, lw, lh);
1151
1152 /* Check if window settled at applied target. */
1153 if ((lw == vis.w) && (lh == vis.h) && (!posKnown || ((lx == vis.x) && (ly == vis.y))))
1154 {
1155 w->clearGeomApplyPending();
1156 return; /* Ignore self-echo when geometry converged. */
1157 }
1158
1159 /* Drop transient echo events while client geometry application is in flight. */
1160 if (w->geomApplyPending())
1161 {
1162 /* Allow live Wayland compositor resize to advance beyond in-flight apply. */
1163 const bool overtaken = !posKnown && ((lw > vis.w) || (lh > vis.h));
1164 if (!overtaken)
1165 return;
1166 w->clearGeomApplyPending();
1167 }
1168
1169 WLog_DBG(TAG, "geometry sync id=0x%08" PRIx32 " local=%d,%d %dx%d (sync)", wid, lx, ly, lw, lh);
1170 reportAndAdopt(w, lx, ly, lw, lh);
1171}
1172
1173void SdlRail::noteResizeGrab(SDL_WindowID id)
1174{
1175 std::unique_lock lock(_windowsLock);
1176 if (_localMove.id == 0)
1177 return;
1178 auto* appWindow = getWindowBySdlId(id);
1179 /* Confirm compositor pointer grab on mouse leave during active drag resize. */
1180 if (appWindow && (static_cast<uint32_t>(appWindow->id()) == _localMove.id) &&
1181 _localMove.wayland)
1182 _localMove.sawResize = true;
1183}
1184
1185void SdlRail::handleWaylandResize(SDL_WindowID id)
1186{
1187 if (!sdl::utils::isWaylandDriver())
1188 return;
1189 std::unique_lock lock(_windowsLock);
1190 auto* appWindow = getWindowBySdlId(id);
1191 if (!appWindow || appWindow->isPopup() || !appWindow->window())
1192 return;
1193 /* Track resize for active dragged window. */
1194 if (static_cast<uint32_t>(appWindow->id()) == _localMove.id)
1195 {
1196 if (_localMove.wayland)
1197 _localMove.sawResize = true;
1198 return;
1199 }
1200
1201 int w = 0;
1202 int h = 0;
1203 SDL_GetWindowSize(appWindow->window(), &w, &h);
1204 if ((w <= 0) || (h <= 0))
1205 return;
1206
1207 /* Local window size is the OUTER frame (rect + insets; identical off the inset paths). */
1208 const SDL_Rect vis = appWindow->outerRect();
1209 if ((w == vis.w) && (h == vis.h))
1210 return; /* echo of a size we already reported/applied - nothing new */
1211
1212 /* Compositor snap/tile: report via the debounced sync (never from transitional state). */
1213 lock.unlock();
1214 syncGeometry(id);
1215}
1216
1217void SdlRail::completeWaylandResize(bool definitive)
1218{
1219 std::unique_lock lock(_windowsLock);
1220 if ((_localMove.id == 0) || !_localMove.wayland)
1221 return;
1222 /* Finalize drag resize on window enter if resize occurred. */
1223 if (!_localMove.sawResize)
1224 {
1225 if (definitive)
1226 {
1227 if (auto* appWindow = getWindow(_localMove.id))
1228 {
1229 appWindow->setLocalMoveActive(false);
1230 /* Repaint now: the dashed placeholder is on screen and an undamaged window would
1231 * otherwise keep it until the next server frame. */
1232 appWindow->invalidateAll();
1233 (void)sdl_push_user_event(SDL_EVENT_USER_UPDATE);
1234 }
1235 _localMove = {}; /* Snapshot and clear drag state under lock. */
1236 }
1237 return;
1238 }
1239
1240 /* Snapshot and clear drag state atomically under lock. */
1241 const LocalMove drag = _localMove;
1242 _localMove = {};
1243
1244 auto appWindow = getWindow(drag.id);
1245 if (!appWindow || !appWindow->window())
1246 return;
1247
1248 (void)SDL_SyncWindow(appWindow->window());
1249 int w = 0;
1250 int h = 0;
1251 SDL_GetWindowSize(appWindow->window(), &w, &h);
1252
1253 /* Derive Wayland origin from anchor edge. */
1254 const SDL_Rect start = appWindow->windowRect();
1255 const RailEdges e = railEdges(drag.type);
1256 int x = e.left ? (start.x + start.w - w) : start.x;
1257 int y = e.top ? (start.y + start.h - h) : start.y;
1258 clampIntoDesktop(x, y, w, h);
1259 reportAndAdopt(appWindow, x, y, w, h);
1260}
1261
1262void SdlRail::completeLocalMoveIfPending()
1263{
1264 std::unique_lock lock(_windowsLock);
1265 if (_localMove.id == 0)
1266 return;
1267 /* Skip Wayland button-up finalize. */
1268 if (_localMove.wayland)
1269 return;
1270
1271 /* Snapshot and clear drag state atomically under lock. */
1272 const LocalMove drag = _localMove;
1273 _localMove = {};
1274
1275 auto appWindow = getWindow(drag.id);
1276 if (!appWindow || !appWindow->window() || appWindow->isDeleted())
1277 return; /* window gone mid-drag: nothing to release into or report for */
1278
1279 /* Send synthetic button release if WM grab consumed physical release. */
1280 if ((SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON_LMASK) &&
1281 !(SDL_GetGlobalMouseState(nullptr, nullptr) & SDL_BUTTON_LMASK))
1282 (void)sdl_x11_send_left_button_release(appWindow->window());
1283
1284 /* Finalize WM resize before reading final size. */
1285 (void)SDL_SyncWindow(appWindow->window());
1286
1287 int x = 0;
1288 int y = 0;
1289 int w = 0;
1290 int h = 0;
1291 SDL_GetWindowSize(appWindow->window(), &w, &h);
1292 SDL_GetWindowPosition(appWindow->window(), &x, &y);
1293 const bool isMove = (drag.type == RAIL_WMSZ_MOVE);
1294 const SDL_Rect start = appWindow->outerRect(); /* position frozen at drag start */
1295 /* Preserve server size during move; defer WM snap/tile size sync until loop END to avoid
1296 * server drag-restore conflict. */
1297 if (isMove)
1298 {
1299 if (!appWindow->effectivelyMaximized() && drag.wmSized &&
1300 !appWindow->localMoveSizeChanged())
1301 {
1302 /* Adopt WM size if window was resized during move. */
1303 w = drag.wmSize.x;
1304 h = drag.wmSize.y;
1305 appWindow->deferSnap(appWindow->serverRect({ x, y, w, h }));
1306 }
1307 else
1308 {
1309 w = start.w;
1310 h = start.h;
1311 }
1312 }
1313 /* Finalize modal loop unless server closed it early. */
1314 if (!drag.serverEnded)
1315 {
1316 int px = drag.anchor.x;
1317 int py = drag.anchor.y;
1318 if (isMove && appWindow->localMoveSizeChanged())
1319 {
1320 /* Measure delta from server anchor on drag-restore. */
1321 const SDL_Point sp = appWindow->localMoveServerPos();
1322 const SDL_Rect ii = appWindow->insets();
1323 px = drag.pointer.x + (x + ii.x) - sp.x;
1324 py = drag.pointer.y + (y + ii.y) - sp.y;
1325 }
1326 else if (!appWindow->effectivelyMaximized())
1327 {
1328 const RailEdges e = railEdges(drag.type);
1329 if (isMove || e.left)
1330 px += x - start.x;
1331 else if (e.right)
1332 px += (x + w) - (start.x + start.w);
1333 if (isMove || e.top)
1334 py += y - start.y;
1335 else if (e.bottom)
1336 py += (y + h) - (start.y + start.h);
1337 }
1338 (void)freerdp_client_send_button_event(_context->common(), FALSE, PTR_FLAGS_BUTTON1, px,
1339 py);
1340 /* Server loop unwinding: wait for explicit END order to gate deferred SC_MAXIMIZE. */
1341 appWindow->armLoopEnd();
1342
1343 /* Adopt local geometry without sending ClientWindowMove during mouse drags. */
1344 const SDL_Rect rect = appWindow->serverRect({ x, y, w, h });
1345 const bool restoredMidDrag = appWindow->localMoveSizeChanged();
1346 appWindow->adoptLocalGeometry(rect);
1347 /* Drag complete: apply pending server resize delayed during WM grab. */
1348 if (restoredMidDrag)
1349 appWindow->markGeometryDirty();
1350 (void)sdl_push_user_event(SDL_EVENT_USER_UPDATE);
1351 }
1352 else if (!isMove)
1353 {
1354 /* Sync final resize geometry if server closed loop early. */
1355 clampIntoDesktop(x, y, w, h);
1356 reportAndAdopt(appWindow, x, y, w, h);
1357 }
1358
1359 (void)sdl_x11_set_bit_gravity(appWindow->window(), 0 /* reset to default */);
1360}
1361
1362UINT SdlRail::server_min_max_info(RailClientContext* context,
1363 const RAIL_MINMAXINFO_ORDER* minMaxInfo)
1364{
1365 WINPR_ASSERT(context);
1366 WINPR_ASSERT(minMaxInfo);
1367 auto rail = static_cast<SdlRail*>(context->custom);
1368 WINPR_ASSERT(rail);
1369
1370 std::unique_lock lock(rail->_windowsLock);
1371 auto appWindow = rail->getWindow(minMaxInfo->windowId);
1372 if (appWindow)
1373 {
1374 WLog_VRB(TAG, "server minmax id=0x%08" PRIx32 " min=%dx%d max=%dx%d", minMaxInfo->windowId,
1375 minMaxInfo->minTrackWidth, minMaxInfo->minTrackHeight, minMaxInfo->maxTrackWidth,
1376 minMaxInfo->maxTrackHeight);
1377 appWindow->setMinMaxSize({ minMaxInfo->minTrackWidth, minMaxInfo->minTrackHeight },
1378 { minMaxInfo->maxTrackWidth, minMaxInfo->maxTrackHeight });
1379 lock.unlock();
1380 (void)sdl_push_user_event(SDL_EVENT_USER_UPDATE);
1381 }
1382 return CHANNEL_RC_OK;
1383}
1384
1385/* --- window order callbacks --- */
1386
1387/* Non-zero edge means margins exist; all-zero means "no band". */
1388static bool marginsSet(const SDL_Rect& m)
1389{
1390 return m.x || m.y || m.w || m.h;
1391}
1392
1393/* RESIZE_MARGIN fields: set this window's frame margins and seed/backfill the session BAND. */
1394void SdlRail::updateMargins(SdlRailWindow* appWindow, const WINDOW_ORDER_INFO* orderInfo,
1395 const WINDOW_STATE_ORDER* state)
1396{
1397 const UINT32 fieldFlags = orderInfo->fieldFlags;
1398 SDL_Rect m = appWindow->frameMargins(); /* raw server margins as the base */
1399 if (fieldFlags & WINDOW_ORDER_FIELD_RESIZE_MARGIN_X)
1400 {
1401 m.x = static_cast<int>(state->resizeMarginLeft);
1402 m.w = static_cast<int>(state->resizeMarginRight);
1403 }
1404 if (fieldFlags & WINDOW_ORDER_FIELD_RESIZE_MARGIN_Y)
1405 {
1406 m.y = static_cast<int>(state->resizeMarginTop);
1407 m.h = static_cast<int>(state->resizeMarginBottom);
1408 }
1409 /* Use window's own frame margins; session margin serves as fallback for band sizing. */
1410 appWindow->setFrameMargins(m);
1411 SDL_Rect& sm = _sessionMargins;
1412 if (marginsSet(m) && !marginsSet(sm))
1413 {
1414 sm = m;
1415 /* Backfill band of earlier zero-announce windows. */
1416 for (auto& [id, w] : _windows)
1417 if (!marginsSet(w.resizeMargins()))
1418 w.setResizeMargins(m.x, m.y, m.w, m.h);
1419 }
1420 const SDL_Rect bandSrc = marginsSet(m) ? m : sm;
1421 appWindow->setResizeMargins(bandSrc.x, bandSrc.y, bandSrc.w, bandSrc.h);
1422 WLog_DBG(TAG, "margins id=0x%08" PRIx32 " raw L%d T%d R%d B%d", orderInfo->windowId, m.x, m.y,
1423 m.w, m.h);
1424}
1425
1426/* SHOW field: mirror the server's show-state (visibility + maximize/minimize) locally. */
1427void SdlRail::updateShowState(SdlRailWindow* appWindow, const WINDOW_ORDER_INFO* orderInfo,
1428 const WINDOW_STATE_ORDER* state)
1429{
1430 WLog_DBG(TAG, "server showState id=0x%08" PRIx32 " state=0x%02" PRIx32, orderInfo->windowId,
1431 state->showState);
1432 appWindow->setVisible(state->showState != WINDOW_HIDE);
1433 switch (state->showState)
1434 {
1435 case WINDOW_SHOW_MAXIMIZED:
1436 appWindow->setServerMaximized(true);
1437 appWindow->setServerMinimized(false);
1438 break;
1439 case WINDOW_SHOW_MINIMIZED:
1440 appWindow->setServerMinimized(true);
1441 break;
1442 case WINDOW_SHOW:
1443 appWindow->setServerMaximized(false);
1444 appWindow->setServerMinimized(false);
1445 break;
1446 default:
1447 break;
1448 }
1449}
1450
1451/* VISIBILITY field: convert the server's visibility rects (window-relative) and store them. */
1452void SdlRail::updateVisRects(SdlRailWindow* appWindow, const WINDOW_ORDER_INFO* orderInfo,
1453 const WINDOW_STATE_ORDER* state)
1454{
1455 std::vector<SDL_Rect> rects;
1456 rects.reserve(state->numVisibilityRects);
1457 for (UINT32 i = 0; i < state->numVisibilityRects; i++)
1458 {
1459 const RECTANGLE_16& r = state->visibilityRects[i];
1460 rects.push_back({ r.left, r.top, r.right - r.left, r.bottom - r.top });
1461 }
1462 if (rects.empty())
1463 WLog_DBG(TAG, "visrects id=0x%08" PRIx32 " n=0", orderInfo->windowId);
1464 else
1465 WLog_DBG(TAG, "visrects id=0x%08" PRIx32 " n=%zu first=%dx%d+%d+%d", orderInfo->windowId,
1466 rects.size(), rects.at(0).w, rects.at(0).h, rects.at(0).x, rects.at(0).y);
1467 appWindow->setVisibilityRects(std::move(rects));
1468}
1469
1470BOOL SdlRail::window_common(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo,
1471 const WINDOW_STATE_ORDER* windowState)
1472{
1473 WINPR_ASSERT(orderInfo);
1474 WINPR_ASSERT(windowState);
1475 auto rail = SdlRail::get(context);
1476 if (!rail)
1477 return FALSE;
1478
1479 const UINT32 fieldFlags = orderInfo->fieldFlags;
1480 std::unique_lock lock(rail->_windowsLock);
1481 auto appWindow = rail->getWindow(orderInfo->windowId);
1482
1483 if (fieldFlags & WINDOW_ORDER_STATE_NEW)
1484 {
1485 const SDL_Rect rect = { static_cast<int>(windowState->windowOffsetX),
1486 static_cast<int>(windowState->windowOffsetY),
1487 static_cast<int>(windowState->windowWidth),
1488 static_cast<int>(windowState->windowHeight) };
1489 appWindow = rail->addWindow(orderInfo->windowId, rect);
1490 if (!appWindow)
1491 return FALSE;
1492
1493 WLog_DBG(TAG,
1494 "window create id=0x%08" PRIx32 " %dx%d+%d+%d style=0x%08" PRIx32
1495 " ex=0x%08" PRIx32 " owner=0x%08" PRIx32,
1496 orderInfo->windowId, rect.w, rect.h, rect.x, rect.y, windowState->style,
1497 windowState->extendedStyle, windowState->ownerWindowId);
1498 /* Seed resize band margins from session default. */
1499 const SDL_Rect& sm = rail->_sessionMargins;
1500 if (marginsSet(sm))
1501 appWindow->setResizeMargins(sm.x, sm.y, sm.w, sm.h);
1502 /* The SDL window is created lazily on the main thread (reconcile), as SDL requires. */
1503 }
1504
1505 if (!appWindow)
1506 return FALSE;
1507
1508 if (fieldFlags & (WINDOW_ORDER_FIELD_WND_OFFSET | WINDOW_ORDER_FIELD_WND_SIZE))
1509 {
1510 SDL_Rect r = appWindow->windowRect();
1511 if (fieldFlags & WINDOW_ORDER_FIELD_WND_OFFSET)
1512 {
1513 r.x = static_cast<int>(windowState->windowOffsetX);
1514 r.y = static_cast<int>(windowState->windowOffsetY);
1515 }
1516 if (fieldFlags & WINDOW_ORDER_FIELD_WND_SIZE)
1517 {
1518 r.w = static_cast<int>(windowState->windowWidth);
1519 r.h = static_cast<int>(windowState->windowHeight);
1520 }
1521 WLog_VRB(TAG, "server geom id=0x%08" PRIx32 " rect=%d,%d %dx%d", orderInfo->windowId, r.x,
1522 r.y, r.w, r.h);
1523 appWindow->updateWindowRect(r);
1524 }
1525 if (fieldFlags & WINDOW_ORDER_FIELD_OWNER)
1526 appWindow->setOwner(windowState->ownerWindowId);
1527 if (fieldFlags & (WINDOW_ORDER_FIELD_RESIZE_MARGIN_X | WINDOW_ORDER_FIELD_RESIZE_MARGIN_Y))
1528 rail->updateMargins(appWindow, orderInfo, windowState);
1529 if (fieldFlags & WINDOW_ORDER_FIELD_STYLE)
1530 appWindow->setStyle(windowState->style, windowState->extendedStyle);
1531 if (fieldFlags & WINDOW_ORDER_FIELD_TITLE)
1532 {
1533 char* title = rail_string_to_utf8_string(&windowState->titleInfo);
1534 if (title)
1535 {
1536 appWindow->setTitle(title);
1537 free(title);
1538 }
1539 }
1540
1541 if (fieldFlags & WINDOW_ORDER_FIELD_SHOW)
1542 updateShowState(appWindow, orderInfo, windowState);
1543
1544 if (fieldFlags & WINDOW_ORDER_FIELD_VIS_OFFSET)
1545 {
1546 WLog_DBG(TAG, "visoff id=0x%08" PRIx32 " %d,%d", orderInfo->windowId,
1547 windowState->visibleOffsetX, windowState->visibleOffsetY);
1548 appWindow->setVisibleOffset({ static_cast<int>(windowState->visibleOffsetX),
1549 static_cast<int>(windowState->visibleOffsetY) });
1550 }
1551 if (fieldFlags & WINDOW_ORDER_FIELD_VISIBILITY)
1552 updateVisRects(appWindow, orderInfo, windowState);
1553
1554 /* Wake the main thread to create/move/show/paint the SDL window(s). */
1555 (void)sdl_push_user_event(SDL_EVENT_USER_UPDATE);
1556 return TRUE;
1557}
1558
1559SdlRailIcon* SdlRail::iconCacheLookup(uint32_t cacheId, uint32_t cacheEntry)
1560{
1561 if (cacheId == 0xFF)
1562 return &_iconScratch;
1563 const size_t idx = static_cast<size_t>(cacheId) * _iconCacheEntries + cacheEntry;
1564 if ((_iconCacheEntries == 0) || (cacheEntry >= _iconCacheEntries) || (idx >= _iconCache.size()))
1565 return nullptr;
1566 return &_iconCache.at(idx);
1567}
1568
1569/* ICON_INFO (1/4/8/16/24/32 bpp + AND mask) -> BGRA32, like xf convert_rail_icon. */
1570static bool convertRailIcon(const ICON_INFO* info, SdlRailIcon& icon)
1571{
1572 icon.w = info->width;
1573 icon.h = info->height;
1574 icon.bgra.assign(4ULL * info->width * info->height, 0);
1575 return freerdp_image_copy_from_icon_data(
1576 icon.bgra.data(), PIXEL_FORMAT_BGRA32, 0, 0, 0,
1577 WINPR_ASSERTING_INT_CAST(UINT16, info->width),
1578 WINPR_ASSERTING_INT_CAST(UINT16, info->height), info->bitsColor,
1579 WINPR_ASSERTING_INT_CAST(UINT16, info->cbBitsColor), info->bitsMask,
1580 WINPR_ASSERTING_INT_CAST(UINT16, info->cbBitsMask), info->colorTable,
1581 WINPR_ASSERTING_INT_CAST(UINT16, info->cbColorTable), info->bpp) == TRUE;
1582}
1583
1584BOOL SdlRail::window_icon(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo,
1585 const WINDOW_ICON_ORDER* windowIcon)
1586{
1587 WINPR_ASSERT(orderInfo);
1588 WINPR_ASSERT(windowIcon);
1589 WINPR_ASSERT(windowIcon->iconInfo);
1590 auto rail = SdlRail::get(context);
1591 if (!rail)
1592 return FALSE;
1593
1594 std::unique_lock lock(rail->_windowsLock);
1595 auto appWindow = rail->getWindow(orderInfo->windowId);
1596 if (!appWindow)
1597 return TRUE;
1598
1599 /* Decode into the cache slot so a later WindowCachedIcon can reference it. */
1600 const ICON_INFO* info = windowIcon->iconInfo;
1601 auto* icon = rail->iconCacheLookup(info->cacheId, info->cacheEntry);
1602 if (!icon || !convertRailIcon(info, *icon))
1603 {
1604 WLog_WARN(TAG, "failed to decode icon %02" PRIX32 ":%04" PRIX32 " for window 0x%08" PRIx32,
1605 info->cacheId, info->cacheEntry, orderInfo->windowId);
1606 return TRUE;
1607 }
1608 appWindow->setIcon(*icon);
1609 lock.unlock();
1610 (void)sdl_push_user_event(SDL_EVENT_USER_UPDATE);
1611 return TRUE;
1612}
1613
1614BOOL SdlRail::window_cached_icon(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo,
1615 const WINDOW_CACHED_ICON_ORDER* windowCachedIcon)
1616{
1617 WINPR_ASSERT(orderInfo);
1618 WINPR_ASSERT(windowCachedIcon);
1619 auto rail = SdlRail::get(context);
1620 if (!rail)
1621 return FALSE;
1622
1623 std::unique_lock lock(rail->_windowsLock);
1624 auto appWindow = rail->getWindow(orderInfo->windowId);
1625 if (!appWindow)
1626 return TRUE;
1627
1628 const CACHED_ICON_INFO& cached = windowCachedIcon->cachedIcon;
1629 auto* icon = rail->iconCacheLookup(cached.cacheId, cached.cacheEntry);
1630 if (!icon || icon->bgra.empty())
1631 {
1632 WLog_WARN(TAG,
1633 "cached icon %02" PRIX32 ":%04" PRIX32 " not in cache (window 0x%08" PRIx32 ")",
1634 cached.cacheId, cached.cacheEntry, orderInfo->windowId);
1635 return TRUE;
1636 }
1637 appWindow->setIcon(*icon);
1638 lock.unlock();
1639 (void)sdl_push_user_event(SDL_EVENT_USER_UPDATE);
1640 return TRUE;
1641}
1642
1643BOOL SdlRail::window_delete(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo)
1644{
1645 WINPR_ASSERT(orderInfo);
1646 auto rail = SdlRail::get(context);
1647 if (!rail)
1648 return FALSE;
1649
1650 /* Mark only; erased on the main thread (paint) so the SDL window dies there. */
1651 std::unique_lock lock(rail->_windowsLock);
1652 auto appWindow = rail->getWindow(orderInfo->windowId);
1653 if (appWindow)
1654 {
1655 WLog_DBG(TAG, "window delete id=0x%08" PRIx32, orderInfo->windowId);
1656 appWindow->markDeleted();
1657 }
1658
1659 /* Clear cached references to deleted window id. */
1660 if (rail->_localMove.id == orderInfo->windowId)
1661 rail->_localMove = {};
1662 if (rail->_clientActiveId == orderInfo->windowId)
1663 rail->_clientActiveId = 0;
1664 if (rail->_focusedAppId == orderInfo->windowId)
1665 rail->_focusedAppId = 0;
1666 lock.unlock();
1667
1668 (void)sdl_push_user_event(SDL_EVENT_USER_UPDATE);
1669 return TRUE;
1670}
1671
1672BOOL SdlRail::monitored_desktop(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo,
1673 const MONITORED_DESKTOP_ORDER* monitoredDesktop)
1674{
1675 WINPR_ASSERT(orderInfo);
1676 WINPR_ASSERT(monitoredDesktop);
1677 auto rail = SdlRail::get(context);
1678 if (!rail)
1679 return FALSE;
1680
1681 /* Launch the RemoteApp on DESKTOP_ARC_COMPLETED, like xf_rail_monitored_desktop. */
1682 if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_DESKTOP_ARC_COMPLETED)
1683 {
1684 rail->enableRemoteAppMode(true);
1685 const char* app =
1686 freerdp_settings_get_string(context->settings, FreeRDP_RemoteApplicationProgram);
1687 if (app && (strnlen(app, 1) > 0))
1688 {
1689 WLog_DBG(TAG, "RAIL mode enabled (monitored desktop); launching '%s'", app);
1690 if (client_rail_server_start_cmd(rail->_rail) != CHANNEL_RC_OK)
1691 {
1692 WLog_ERR(TAG, "client_rail_server_start_cmd failed for '%s'", app);
1693 return FALSE;
1694 }
1695 }
1696 }
1697
1698 /* Authoritative top-level z-order (windowIds[0] topmost); capture it, paint() realizes it. */
1699 if (orderInfo->fieldFlags &
1700 (WINDOW_ORDER_FIELD_DESKTOP_ZORDER | WINDOW_ORDER_FIELD_DESKTOP_ACTIVE_WND))
1701 {
1702 std::unique_lock lock(rail->_windowsLock);
1703 if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_DESKTOP_ACTIVE_WND)
1704 {
1705 /* Server changed active window: sync our ClientActivate dedup to avoid skipping
1706 * re-clicks. */
1707 rail->_clientActiveId = monitoredDesktop->activeWindowId;
1708 }
1709 if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_DESKTOP_ZORDER)
1710 {
1711 if (monitoredDesktop->windowIds && (monitoredDesktop->numWindowIds > 0))
1712 rail->_zOrder.assign(monitoredDesktop->windowIds,
1713 monitoredDesktop->windowIds + monitoredDesktop->numWindowIds);
1714 else
1715 rail->_zOrder.clear();
1716 rail->_zOrderDirty = true;
1717 lock.unlock();
1718 (void)sdl_push_user_event(SDL_EVENT_USER_UPDATE); /* wake the main thread to restack */
1719 }
1720 }
1721 return TRUE;
1722}
1723
1724BOOL SdlRail::non_monitored_desktop(rdpContext* context,
1725 WINPR_ATTR_UNUSED const WINDOW_ORDER_INFO* orderInfo)
1726{
1727 WINPR_ASSERT(orderInfo);
1728 auto rail = SdlRail::get(context);
1729 if (rail)
1730 {
1731 WLog_DBG(TAG, "RAIL mode disabled (non-monitored desktop)");
1732 rail->enableRemoteAppMode(false);
1733 }
1734 return TRUE;
1735}
WINPR_ATTR_NODISCARD FREERDP_API const char * freerdp_settings_get_string(const rdpSettings *settings, FreeRDP_Settings_Keys_String id)
Returns a immutable string settings value.
WINPR_ATTR_NODISCARD FREERDP_API UINT32 freerdp_settings_get_uint32(const rdpSettings *settings, FreeRDP_Settings_Keys_UInt32 id)
Returns a UINT32 settings value.
-aad-helper