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