1 /*****************************************************************************
2  * Copyright (c) 2014-2020 OpenRCT2 developers
3  *
4  * For a complete list of all authors, please refer to contributors.md
5  * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
6  *
7  * OpenRCT2 is licensed under the GNU General Public License version 3.
8  *****************************************************************************/
9 
10 #include "UiContext.h"
11 
12 #include "CursorRepository.h"
13 #include "SDLException.h"
14 #include "TextComposition.h"
15 #include "WindowManager.h"
16 #include "drawing/engines/DrawingEngineFactory.hpp"
17 #include "input/ShortcutManager.h"
18 #include "interface/InGameConsole.h"
19 #include "interface/Theme.h"
20 #include "scripting/UiExtensions.h"
21 #include "title/TitleSequencePlayer.h"
22 
23 #include <SDL.h>
24 #include <algorithm>
25 #include <chrono>
26 #include <cmath>
27 #include <cstdlib>
28 #include <memory>
29 #include <openrct2-ui/input/InputManager.h>
30 #include <openrct2-ui/interface/Window.h>
31 #include <openrct2/Context.h>
32 #include <openrct2/Input.h>
33 #include <openrct2/Version.h>
34 #include <openrct2/audio/AudioMixer.h>
35 #include <openrct2/config/Config.h>
36 #include <openrct2/core/String.hpp>
37 #include <openrct2/drawing/Drawing.h>
38 #include <openrct2/drawing/IDrawingEngine.h>
39 #include <openrct2/interface/Chat.h>
40 #include <openrct2/interface/InteractiveConsole.h>
41 #include <openrct2/localisation/StringIds.h>
42 #include <openrct2/platform/Platform2.h>
43 #include <openrct2/scripting/ScriptEngine.h>
44 #include <openrct2/title/TitleSequencePlayer.h>
45 #include <openrct2/ui/UiContext.h>
46 #include <openrct2/ui/WindowManager.h>
47 #include <openrct2/world/Location.hpp>
48 #include <vector>
49 
50 using namespace OpenRCT2;
51 using namespace OpenRCT2::Drawing;
52 using namespace OpenRCT2::Scripting;
53 using namespace OpenRCT2::Ui;
54 
55 #ifdef __MACOSX__
56 // macOS uses COMMAND rather than CTRL for many keyboard shortcuts
57 #    define KEYBOARD_PRIMARY_MODIFIER KMOD_GUI
58 #else
59 #    define KEYBOARD_PRIMARY_MODIFIER KMOD_CTRL
60 #endif
61 
62 class UiContext final : public IUiContext
63 {
64 private:
65     constexpr static uint32_t TOUCH_DOUBLE_TIMEOUT = 300;
66 
67     IPlatformUiContext* const _platformUiContext;
68     IWindowManager* const _windowManager;
69 
70     CursorRepository _cursorRepository;
71 
72     SDL_Window* _window = nullptr;
73     int32_t _width = 0;
74     int32_t _height = 0;
75     ScaleQuality _scaleQuality = ScaleQuality::NearestNeighbour;
76 
77     std::vector<Resolution> _fsResolutions;
78 
79     bool _steamOverlayActive = false;
80 
81     // Input
82     InputManager _inputManager;
83     ShortcutManager _shortcutManager;
84     TextComposition _textComposition;
85     CursorState _cursorState = {};
86     uint32_t _lastKeyPressed = 0;
87     const uint8_t* _keysState = nullptr;
88     uint8_t _keysPressed[256] = {};
89     uint32_t _lastGestureTimestamp = 0;
90     float _gestureRadius = 0;
91 
92     InGameConsole _inGameConsole;
93     std::unique_ptr<ITitleSequencePlayer> _titleSequencePlayer;
94 
95 public:
GetInGameConsole()96     InGameConsole& GetInGameConsole()
97     {
98         return _inGameConsole;
99     }
100 
GetInputManager()101     InputManager& GetInputManager()
102     {
103         return _inputManager;
104     }
105 
GetShortcutManager()106     ShortcutManager& GetShortcutManager()
107     {
108         return _shortcutManager;
109     }
110 
UiContext(const std::shared_ptr<IPlatformEnvironment> & env)111     explicit UiContext(const std::shared_ptr<IPlatformEnvironment>& env)
112         : _platformUiContext(CreatePlatformUiContext())
113         , _windowManager(CreateWindowManager())
114         , _shortcutManager(env)
115     {
116         if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK) < 0)
117         {
118             SDLException::Throw("SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK)");
119         }
120         _cursorRepository.LoadCursors();
121         _shortcutManager.LoadUserBindings();
122     }
123 
~UiContext()124     ~UiContext() override
125     {
126         UiContext::CloseWindow();
127         delete _windowManager;
128         SDL_QuitSubSystem(SDL_INIT_VIDEO);
129         delete _platformUiContext;
130     }
131 
Initialise()132     void Initialise() override
133     {
134 #ifdef ENABLE_SCRIPTING
135         auto& scriptEngine = GetContext()->GetScriptEngine();
136         UiScriptExtensions::Extend(scriptEngine);
137 #endif
138     }
139 
Update()140     void Update() override
141     {
142         _inGameConsole.Update();
143     }
144 
Draw(rct_drawpixelinfo * dpi)145     void Draw(rct_drawpixelinfo* dpi) override
146     {
147         auto bgColour = ThemeGetColour(WC_CHAT, 0);
148         chat_draw(dpi, bgColour);
149         _inGameConsole.Draw(dpi);
150     }
151 
152     // Window
GetWindow()153     void* GetWindow() override
154     {
155         return _window;
156     }
157 
GetWidth()158     int32_t GetWidth() override
159     {
160         return _width;
161     }
162 
GetHeight()163     int32_t GetHeight() override
164     {
165         return _height;
166     }
167 
GetScaleQuality()168     ScaleQuality GetScaleQuality() override
169     {
170         return _scaleQuality;
171     }
172 
SetFullscreenMode(FULLSCREEN_MODE mode)173     void SetFullscreenMode(FULLSCREEN_MODE mode) override
174     {
175         static constexpr const int32_t SDLFSFlags[] = {
176             0,
177             SDL_WINDOW_FULLSCREEN,
178             SDL_WINDOW_FULLSCREEN_DESKTOP,
179         };
180         uint32_t windowFlags = SDLFSFlags[static_cast<int32_t>(mode)];
181 
182         // HACK Changing window size when in fullscreen usually has no effect
183         if (mode == FULLSCREEN_MODE::FULLSCREEN)
184         {
185             SDL_SetWindowFullscreen(_window, 0);
186 
187             // Set window size
188             UpdateFullscreenResolutions();
189             Resolution resolution = GetClosestResolution(gConfigGeneral.fullscreen_width, gConfigGeneral.fullscreen_height);
190             SDL_SetWindowSize(_window, resolution.Width, resolution.Height);
191         }
192         else if (mode == FULLSCREEN_MODE::WINDOWED)
193         {
194             SDL_SetWindowSize(_window, gConfigGeneral.window_width, gConfigGeneral.window_height);
195         }
196 
197         if (SDL_SetWindowFullscreen(_window, windowFlags))
198         {
199             log_fatal("SDL_SetWindowFullscreen %s", SDL_GetError());
200             exit(1);
201 
202             // TODO try another display mode rather than just exiting the game
203         }
204     }
205 
GetFullscreenResolutions()206     const std::vector<Resolution>& GetFullscreenResolutions() override
207     {
208         UpdateFullscreenResolutions();
209         return _fsResolutions;
210     }
211 
HasFocus()212     bool HasFocus() override
213     {
214         uint32_t windowFlags = GetWindowFlags();
215         return (windowFlags & SDL_WINDOW_INPUT_FOCUS) != 0;
216     }
217 
IsMinimised()218     bool IsMinimised() override
219     {
220         uint32_t windowFlags = GetWindowFlags();
221         return (windowFlags & SDL_WINDOW_MINIMIZED) || (windowFlags & SDL_WINDOW_HIDDEN);
222     }
223 
IsSteamOverlayActive()224     bool IsSteamOverlayActive() override
225     {
226         return _steamOverlayActive;
227     }
228 
229     // Input
GetCursorState()230     const CursorState* GetCursorState() override
231     {
232         return &_cursorState;
233     }
234 
GetKeysState()235     const uint8_t* GetKeysState() override
236     {
237         return _keysState;
238     }
239 
GetKeysPressed()240     const uint8_t* GetKeysPressed() override
241     {
242         return _keysPressed;
243     }
244 
GetCursor()245     CursorID GetCursor() override
246     {
247         return _cursorRepository.GetCurrentCursor();
248     }
249 
SetCursor(CursorID cursor)250     void SetCursor(CursorID cursor) override
251     {
252         _cursorRepository.SetCurrentCursor(cursor);
253     }
254 
SetCursorScale(uint8_t scale)255     void SetCursorScale(uint8_t scale) override
256     {
257         _cursorRepository.SetCursorScale(scale);
258     }
259 
SetCursorVisible(bool value)260     void SetCursorVisible(bool value) override
261     {
262         SDL_ShowCursor(value ? SDL_ENABLE : SDL_DISABLE);
263     }
264 
GetCursorPosition()265     ScreenCoordsXY GetCursorPosition() override
266     {
267         ScreenCoordsXY cursorPosition;
268         SDL_GetMouseState(&cursorPosition.x, &cursorPosition.y);
269         return cursorPosition;
270     }
271 
SetCursorPosition(const ScreenCoordsXY & cursorPosition)272     void SetCursorPosition(const ScreenCoordsXY& cursorPosition) override
273     {
274         SDL_WarpMouseInWindow(nullptr, cursorPosition.x, cursorPosition.y);
275     }
276 
SetCursorTrap(bool value)277     void SetCursorTrap(bool value) override
278     {
279         SDL_SetWindowGrab(_window, value ? SDL_TRUE : SDL_FALSE);
280     }
281 
SetKeysPressed(uint32_t keysym,uint8_t scancode)282     void SetKeysPressed(uint32_t keysym, uint8_t scancode) override
283     {
284         _lastKeyPressed = keysym;
285         _keysPressed[scancode] = 1;
286     }
287 
288     // Drawing
GetDrawingEngineFactory()289     std::shared_ptr<Drawing::IDrawingEngineFactory> GetDrawingEngineFactory() override
290     {
291         return std::make_shared<DrawingEngineFactory>();
292     }
293 
DrawWeatherAnimation(IWeatherDrawer * weatherDrawer,rct_drawpixelinfo * dpi,DrawWeatherFunc drawFunc)294     void DrawWeatherAnimation(IWeatherDrawer* weatherDrawer, rct_drawpixelinfo* dpi, DrawWeatherFunc drawFunc) override
295     {
296         int32_t left = dpi->x;
297         int32_t right = left + dpi->width;
298         int32_t top = dpi->y;
299         int32_t bottom = top + dpi->height;
300 
301         for (auto& w : g_window_list)
302         {
303             DrawWeatherWindow(dpi, weatherDrawer, w.get(), left, right, top, bottom, drawFunc);
304         }
305     }
306 
307     // Text input
IsTextInputActive()308     bool IsTextInputActive() override
309     {
310         return _textComposition.IsActive();
311     }
312 
StartTextInput(utf8 * buffer,size_t bufferSize)313     TextInputSession* StartTextInput(utf8* buffer, size_t bufferSize) override
314     {
315         return _textComposition.Start(buffer, bufferSize);
316     }
317 
StopTextInput()318     void StopTextInput() override
319     {
320         _textComposition.Stop();
321     }
322 
ProcessMessages()323     void ProcessMessages() override
324     {
325         _lastKeyPressed = 0;
326         _cursorState.left &= ~CURSOR_CHANGED;
327         _cursorState.middle &= ~CURSOR_CHANGED;
328         _cursorState.right &= ~CURSOR_CHANGED;
329         _cursorState.old = 0;
330 
331         SDL_Event e;
332         while (SDL_PollEvent(&e))
333         {
334             switch (e.type)
335             {
336                 case SDL_QUIT:
337                     context_quit();
338                     break;
339                 case SDL_WINDOWEVENT:
340                     // HACK: Fix #2158, OpenRCT2 does not draw if it does not think that the window is
341                     //                  visible - due a bug in SDL 2.0.3 this hack is required if the
342                     //                  window is maximised, minimised and then restored again.
343                     if (e.window.event == SDL_WINDOWEVENT_FOCUS_GAINED)
344                     {
345                         if (SDL_GetWindowFlags(_window) & SDL_WINDOW_MAXIMIZED)
346                         {
347                             SDL_RestoreWindow(_window);
348                             SDL_MaximizeWindow(_window);
349                         }
350                         if ((SDL_GetWindowFlags(_window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN_DESKTOP)
351                         {
352                             SDL_RestoreWindow(_window);
353                             SDL_SetWindowFullscreen(_window, SDL_WINDOW_FULLSCREEN_DESKTOP);
354                         }
355                     }
356 
357                     if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
358                     {
359                         OnResize(e.window.data1, e.window.data2);
360                     }
361 
362                     switch (e.window.event)
363                     {
364                         case SDL_WINDOWEVENT_SIZE_CHANGED:
365                         case SDL_WINDOWEVENT_MOVED:
366                         case SDL_WINDOWEVENT_MAXIMIZED:
367                         case SDL_WINDOWEVENT_RESTORED:
368                         {
369                             // Update default display index
370                             int32_t displayIndex = SDL_GetWindowDisplayIndex(_window);
371                             if (displayIndex != gConfigGeneral.default_display)
372                             {
373                                 gConfigGeneral.default_display = displayIndex;
374                                 config_save_default();
375                             }
376                             break;
377                         }
378                     }
379 
380                     if (gConfigSound.audio_focus)
381                     {
382                         if (e.window.event == SDL_WINDOWEVENT_FOCUS_GAINED)
383                         {
384                             Mixer_SetVolume(1);
385                         }
386                         if (e.window.event == SDL_WINDOWEVENT_FOCUS_LOST)
387                         {
388                             Mixer_SetVolume(0);
389                         }
390                     }
391                     break;
392                 case SDL_MOUSEMOTION:
393                     _cursorState.position = { static_cast<int32_t>(e.motion.x / gConfigGeneral.window_scale),
394                                               static_cast<int32_t>(e.motion.y / gConfigGeneral.window_scale) };
395                     break;
396                 case SDL_MOUSEWHEEL:
397                     if (_inGameConsole.IsOpen())
398                     {
399                         _inGameConsole.Scroll(e.wheel.y * 3); // Scroll 3 lines at a time
400                         break;
401                     }
402                     _cursorState.wheel -= e.wheel.y;
403                     break;
404                 case SDL_MOUSEBUTTONDOWN:
405                 {
406                     if (e.button.which == SDL_TOUCH_MOUSEID)
407                     {
408                         break;
409                     }
410                     ScreenCoordsXY mousePos = { static_cast<int32_t>(e.button.x / gConfigGeneral.window_scale),
411                                                 static_cast<int32_t>(e.button.y / gConfigGeneral.window_scale) };
412                     switch (e.button.button)
413                     {
414                         case SDL_BUTTON_LEFT:
415                             StoreMouseInput(MouseState::LeftPress, mousePos);
416                             _cursorState.left = CURSOR_PRESSED;
417                             _cursorState.old = 1;
418                             break;
419                         case SDL_BUTTON_MIDDLE:
420                             _cursorState.middle = CURSOR_PRESSED;
421                             break;
422                         case SDL_BUTTON_RIGHT:
423                             StoreMouseInput(MouseState::RightPress, mousePos);
424                             _cursorState.right = CURSOR_PRESSED;
425                             _cursorState.old = 2;
426                             break;
427                     }
428                     _cursorState.touch = false;
429 
430                     {
431                         InputEvent ie;
432                         ie.DeviceKind = InputDeviceKind::Mouse;
433                         ie.Modifiers = SDL_GetModState();
434                         ie.Button = e.button.button;
435                         ie.State = InputEventState::Down;
436                         _inputManager.QueueInputEvent(std::move(ie));
437                     }
438                     break;
439                 }
440                 case SDL_MOUSEBUTTONUP:
441                 {
442                     if (e.button.which == SDL_TOUCH_MOUSEID)
443                     {
444                         break;
445                     }
446                     ScreenCoordsXY mousePos = { static_cast<int32_t>(e.button.x / gConfigGeneral.window_scale),
447                                                 static_cast<int32_t>(e.button.y / gConfigGeneral.window_scale) };
448                     switch (e.button.button)
449                     {
450                         case SDL_BUTTON_LEFT:
451                             StoreMouseInput(MouseState::LeftRelease, mousePos);
452                             _cursorState.left = CURSOR_RELEASED;
453                             _cursorState.old = 3;
454                             break;
455                         case SDL_BUTTON_MIDDLE:
456                             _cursorState.middle = CURSOR_RELEASED;
457                             break;
458                         case SDL_BUTTON_RIGHT:
459                             StoreMouseInput(MouseState::RightRelease, mousePos);
460                             _cursorState.right = CURSOR_RELEASED;
461                             _cursorState.old = 4;
462                             break;
463                     }
464                     _cursorState.touch = false;
465 
466                     {
467                         InputEvent ie;
468                         ie.DeviceKind = InputDeviceKind::Mouse;
469                         ie.Modifiers = SDL_GetModState();
470                         ie.Button = e.button.button;
471                         ie.State = InputEventState::Release;
472                         _inputManager.QueueInputEvent(std::move(ie));
473                     }
474                     break;
475                 }
476                 // Apple sends touchscreen events for trackpads, so ignore these events on macOS
477 #ifndef __MACOSX__
478                 case SDL_FINGERMOTION:
479                     _cursorState.position = { static_cast<int32_t>(e.tfinger.x * _width),
480                                               static_cast<int32_t>(e.tfinger.y * _height) };
481                     break;
482                 case SDL_FINGERDOWN:
483                 {
484                     ScreenCoordsXY fingerPos = { static_cast<int32_t>(e.tfinger.x * _width),
485                                                  static_cast<int32_t>(e.tfinger.y * _height) };
486 
487                     _cursorState.touchIsDouble
488                         = (!_cursorState.touchIsDouble
489                            && e.tfinger.timestamp - _cursorState.touchDownTimestamp < TOUCH_DOUBLE_TIMEOUT);
490 
491                     if (_cursorState.touchIsDouble)
492                     {
493                         StoreMouseInput(MouseState::RightPress, fingerPos);
494                         _cursorState.right = CURSOR_PRESSED;
495                         _cursorState.old = 2;
496                     }
497                     else
498                     {
499                         StoreMouseInput(MouseState::LeftPress, fingerPos);
500                         _cursorState.left = CURSOR_PRESSED;
501                         _cursorState.old = 1;
502                     }
503                     _cursorState.touch = true;
504                     _cursorState.touchDownTimestamp = e.tfinger.timestamp;
505                     break;
506                 }
507                 case SDL_FINGERUP:
508                 {
509                     ScreenCoordsXY fingerPos = { static_cast<int32_t>(e.tfinger.x * _width),
510                                                  static_cast<int32_t>(e.tfinger.y * _height) };
511 
512                     if (_cursorState.touchIsDouble)
513                     {
514                         StoreMouseInput(MouseState::RightRelease, fingerPos);
515                         _cursorState.right = CURSOR_RELEASED;
516                         _cursorState.old = 4;
517                     }
518                     else
519                     {
520                         StoreMouseInput(MouseState::LeftRelease, fingerPos);
521                         _cursorState.left = CURSOR_RELEASED;
522                         _cursorState.old = 3;
523                     }
524                     _cursorState.touch = true;
525                     break;
526                 }
527 #endif
528                 case SDL_KEYDOWN:
529                 {
530 #ifndef __MACOSX__
531                     // Ignore winkey keydowns. Handles edge case where tiling
532                     // window managers don't eat the keypresses when changing
533                     // workspaces.
534                     if (SDL_GetModState() & KMOD_GUI)
535                     {
536                         break;
537                     }
538 #endif
539                     _textComposition.HandleMessage(&e);
540                     auto ie = GetInputEventFromSDLEvent(e);
541                     ie.State = InputEventState::Down;
542                     _inputManager.QueueInputEvent(std::move(ie));
543                     break;
544                 }
545                 case SDL_KEYUP:
546                 {
547                     auto ie = GetInputEventFromSDLEvent(e);
548                     ie.State = InputEventState::Release;
549                     _inputManager.QueueInputEvent(std::move(ie));
550                     break;
551                 }
552                 case SDL_MULTIGESTURE:
553                     if (e.mgesture.numFingers == 2)
554                     {
555                         if (e.mgesture.timestamp > _lastGestureTimestamp + 1000)
556                         {
557                             _gestureRadius = 0;
558                         }
559                         _lastGestureTimestamp = e.mgesture.timestamp;
560                         _gestureRadius += e.mgesture.dDist;
561 
562                         // Zoom gesture
563                         constexpr int32_t tolerance = 128;
564                         int32_t gesturePixels = static_cast<int32_t>(_gestureRadius * _width);
565                         if (abs(gesturePixels) > tolerance)
566                         {
567                             _gestureRadius = 0;
568                             main_window_zoom(gesturePixels > 0, true);
569                         }
570                     }
571                     break;
572                 case SDL_TEXTEDITING:
573                     _textComposition.HandleMessage(&e);
574                     break;
575                 case SDL_TEXTINPUT:
576                     _textComposition.HandleMessage(&e);
577                     break;
578                 default:
579                 {
580                     _inputManager.QueueInputEvent(e);
581                     break;
582                 }
583             }
584         }
585 
586         _cursorState.any = _cursorState.left | _cursorState.middle | _cursorState.right;
587 
588         // Updates the state of the keys
589         int32_t numKeys = 256;
590         _keysState = SDL_GetKeyboardState(&numKeys);
591     }
592 
593     /**
594      * Helper function to set various render target features.
595      * Does not get triggered on resize, but rather manually on config changes.
596      */
TriggerResize()597     void TriggerResize() override
598     {
599         char scaleQualityBuffer[4];
600         _scaleQuality = gConfigGeneral.scale_quality;
601         if (gConfigGeneral.window_scale == std::floor(gConfigGeneral.window_scale))
602         {
603             _scaleQuality = ScaleQuality::NearestNeighbour;
604         }
605 
606         ScaleQuality scaleQuality = _scaleQuality;
607         if (_scaleQuality == ScaleQuality::SmoothNearestNeighbour)
608         {
609             scaleQuality = ScaleQuality::Linear;
610         }
611         snprintf(scaleQualityBuffer, sizeof(scaleQualityBuffer), "%d", static_cast<int32_t>(scaleQuality));
612         SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, scaleQualityBuffer);
613 
614         int32_t width, height;
615         SDL_GetWindowSize(_window, &width, &height);
616         OnResize(width, height);
617     }
618 
CreateWindow()619     void CreateWindow() override
620     {
621         SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, gConfigGeneral.minimize_fullscreen_focus_loss ? "1" : "0");
622 
623         // Set window position to default display
624         int32_t defaultDisplay = std::clamp(gConfigGeneral.default_display, 0, 0xFFFF);
625         auto windowPos = ScreenCoordsXY{ static_cast<int32_t>(SDL_WINDOWPOS_UNDEFINED_DISPLAY(defaultDisplay)),
626                                          static_cast<int32_t>(SDL_WINDOWPOS_UNDEFINED_DISPLAY(defaultDisplay)) };
627 
628         CreateWindow(windowPos);
629 
630         // Check if steam overlay renderer is loaded into the process
631         _steamOverlayActive = _platformUiContext->IsSteamOverlayAttached();
632     }
633 
CloseWindow()634     void CloseWindow() override
635     {
636         drawing_engine_dispose();
637         if (_window != nullptr)
638         {
639             SDL_DestroyWindow(_window);
640             _window = nullptr;
641         }
642     }
643 
RecreateWindow()644     void RecreateWindow() override
645     {
646         // Use the position of the current window for the new window
647         ScreenCoordsXY windowPos;
648         SDL_SetWindowFullscreen(_window, 0);
649         SDL_GetWindowPosition(_window, &windowPos.x, &windowPos.y);
650 
651         CloseWindow();
652         CreateWindow(windowPos);
653     }
654 
ShowMessageBox(const std::string & message)655     void ShowMessageBox(const std::string& message) override
656     {
657         _platformUiContext->ShowMessageBox(_window, message);
658     }
659 
HasMenuSupport()660     bool HasMenuSupport() override
661     {
662         return _platformUiContext->HasMenuSupport();
663     }
664 
ShowMenuDialog(const std::vector<std::string> & options,const std::string & title,const std::string & text)665     int32_t ShowMenuDialog(const std::vector<std::string>& options, const std::string& title, const std::string& text) override
666     {
667         return _platformUiContext->ShowMenuDialog(options, title, text);
668     }
669 
OpenFolder(const std::string & path)670     void OpenFolder(const std::string& path) override
671     {
672         _platformUiContext->OpenFolder(path);
673     }
674 
OpenURL(const std::string & url)675     void OpenURL(const std::string& url) override
676     {
677         _platformUiContext->OpenURL(url);
678     }
679 
ShowFileDialog(const FileDialogDesc & desc)680     std::string ShowFileDialog(const FileDialogDesc& desc) override
681     {
682         return _platformUiContext->ShowFileDialog(_window, desc);
683     }
684 
ShowDirectoryDialog(const std::string & title)685     std::string ShowDirectoryDialog(const std::string& title) override
686     {
687         return _platformUiContext->ShowDirectoryDialog(_window, title);
688     }
689 
HasFilePicker() const690     bool HasFilePicker() const override
691     {
692         return _platformUiContext->HasFilePicker();
693     }
694 
GetWindowManager()695     IWindowManager* GetWindowManager() override
696     {
697         return _windowManager;
698     }
699 
SetClipboardText(const utf8 * target)700     bool SetClipboardText(const utf8* target) override
701     {
702         return (SDL_SetClipboardText(target) == 0);
703     }
704 
GetTitleSequencePlayer()705     ITitleSequencePlayer* GetTitleSequencePlayer() override
706     {
707         if (_titleSequencePlayer == nullptr)
708         {
709             auto context = GetContext();
710             auto gameState = context->GetGameState();
711             _titleSequencePlayer = CreateTitleSequencePlayer(*gameState);
712         }
713         return _titleSequencePlayer.get();
714     }
715 
716 private:
CreateWindow(const ScreenCoordsXY & windowPos)717     void CreateWindow(const ScreenCoordsXY& windowPos)
718     {
719         // Get saved window size
720         int32_t width = gConfigGeneral.window_width;
721         int32_t height = gConfigGeneral.window_height;
722         if (width <= 0)
723             width = 640;
724         if (height <= 0)
725             height = 480;
726 
727         // Create window in window first rather than fullscreen so we have the display the window is on first
728         uint32_t flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
729         if (gConfigGeneral.drawing_engine == DrawingEngine::OpenGL)
730         {
731             flags |= SDL_WINDOW_OPENGL;
732         }
733 
734         _window = SDL_CreateWindow(OPENRCT2_NAME, windowPos.x, windowPos.y, width, height, flags);
735         if (_window == nullptr)
736         {
737             SDLException::Throw("SDL_CreateWindow(...)");
738         }
739 
740         ApplyScreenSaverLockSetting();
741 
742         SDL_SetWindowMinimumSize(_window, 720, 480);
743         SetCursorTrap(gConfigGeneral.trap_cursor);
744         _platformUiContext->SetWindowIcon(_window);
745 
746         // Initialise the surface, palette and draw buffer
747         drawing_engine_init();
748         OnResize(width, height);
749 
750         UpdateFullscreenResolutions();
751 
752         // Fix #4022: Force Mac to windowed to avoid cursor offset on launch issue
753 #ifdef __MACOSX__
754         gConfigGeneral.fullscreen_mode = static_cast<int32_t>(OpenRCT2::Ui::FULLSCREEN_MODE::WINDOWED);
755 #else
756         SetFullscreenMode(static_cast<FULLSCREEN_MODE>(gConfigGeneral.fullscreen_mode));
757 #endif
758         TriggerResize();
759     }
760 
OnResize(int32_t width,int32_t height)761     void OnResize(int32_t width, int32_t height)
762     {
763         // Scale the native window size to the game's canvas size
764         _width = static_cast<int32_t>(width / gConfigGeneral.window_scale);
765         _height = static_cast<int32_t>(height / gConfigGeneral.window_scale);
766 
767         drawing_engine_resize();
768 
769         uint32_t flags = SDL_GetWindowFlags(_window);
770         if ((flags & SDL_WINDOW_MINIMIZED) == 0)
771         {
772             window_resize_gui(_width, _height);
773             window_relocate_windows(_width, _height);
774         }
775 
776         gfx_invalidate_screen();
777 
778         // Check if the window has been resized in windowed mode and update the config file accordingly
779         int32_t nonWindowFlags =
780 #ifndef __MACOSX__
781             SDL_WINDOW_MAXIMIZED |
782 #endif
783             SDL_WINDOW_MINIMIZED | SDL_WINDOW_FULLSCREEN | SDL_WINDOW_FULLSCREEN_DESKTOP;
784 
785         if (!(flags & nonWindowFlags))
786         {
787             if (width != gConfigGeneral.window_width || height != gConfigGeneral.window_height)
788             {
789                 gConfigGeneral.window_width = width;
790                 gConfigGeneral.window_height = height;
791                 config_save_default();
792             }
793         }
794     }
795 
UpdateFullscreenResolutions()796     void UpdateFullscreenResolutions()
797     {
798         // Query number of display modes
799         int32_t displayIndex = SDL_GetWindowDisplayIndex(_window);
800         int32_t numDisplayModes = SDL_GetNumDisplayModes(displayIndex);
801 
802         // Get desktop aspect ratio
803         SDL_DisplayMode mode;
804         SDL_GetDesktopDisplayMode(displayIndex, &mode);
805 
806         // Get resolutions
807         auto resolutions = std::vector<Resolution>();
808         float desktopAspectRatio = static_cast<float>(mode.w) / mode.h;
809         for (int32_t i = 0; i < numDisplayModes; i++)
810         {
811             SDL_GetDisplayMode(displayIndex, i, &mode);
812             if (mode.w > 0 && mode.h > 0)
813             {
814                 float aspectRatio = static_cast<float>(mode.w) / mode.h;
815                 if (std::fabs(desktopAspectRatio - aspectRatio) < 0.1f)
816                 {
817                     resolutions.push_back({ mode.w, mode.h });
818                 }
819             }
820         }
821 
822         // Sort by area
823         std::sort(resolutions.begin(), resolutions.end(), [](const Resolution& a, const Resolution& b) -> bool {
824             int32_t areaA = a.Width * a.Height;
825             int32_t areaB = b.Width * b.Height;
826             return areaA < areaB;
827         });
828 
829         // Remove duplicates
830         auto last = std::unique(resolutions.begin(), resolutions.end(), [](const Resolution& a, const Resolution& b) -> bool {
831             return (a.Width == b.Width && a.Height == b.Height);
832         });
833         resolutions.erase(last, resolutions.end());
834 
835         // Update config fullscreen resolution if not set
836         if (!resolutions.empty() && (gConfigGeneral.fullscreen_width == -1 || gConfigGeneral.fullscreen_height == -1))
837         {
838             gConfigGeneral.fullscreen_width = resolutions.back().Width;
839             gConfigGeneral.fullscreen_height = resolutions.back().Height;
840         }
841 
842         _fsResolutions = resolutions;
843     }
844 
GetClosestResolution(int32_t inWidth,int32_t inHeight)845     Resolution GetClosestResolution(int32_t inWidth, int32_t inHeight)
846     {
847         Resolution result = { 640, 480 };
848         int32_t closestAreaDiff = -1;
849         int32_t destinationArea = inWidth * inHeight;
850         for (const Resolution& resolution : _fsResolutions)
851         {
852             // Check if exact match
853             if (resolution.Width == inWidth && resolution.Height == inHeight)
854             {
855                 result = resolution;
856                 break;
857             }
858 
859             // Check if area is closer to best match
860             int32_t areaDiff = std::abs((resolution.Width * resolution.Height) - destinationArea);
861             if (closestAreaDiff == -1 || areaDiff < closestAreaDiff)
862             {
863                 closestAreaDiff = areaDiff;
864                 result = resolution;
865             }
866         }
867         return result;
868     }
869 
GetWindowFlags()870     uint32_t GetWindowFlags()
871     {
872         return SDL_GetWindowFlags(_window);
873     }
874 
DrawWeatherWindow(rct_drawpixelinfo * dpi,IWeatherDrawer * weatherDrawer,rct_window * original_w,int16_t left,int16_t right,int16_t top,int16_t bottom,DrawWeatherFunc drawFunc)875     static void DrawWeatherWindow(
876         rct_drawpixelinfo* dpi, IWeatherDrawer* weatherDrawer, rct_window* original_w, int16_t left, int16_t right, int16_t top,
877         int16_t bottom, DrawWeatherFunc drawFunc)
878     {
879         rct_window* w{};
880         auto itStart = window_get_iterator(original_w);
881         for (auto it = std::next(itStart);; it++)
882         {
883             if (it == g_window_list.end())
884             {
885                 // Loop ended, draw weather for original_w
886                 auto vp = original_w->viewport;
887                 if (vp != nullptr)
888                 {
889                     left = std::max<int16_t>(left, vp->pos.x);
890                     right = std::min<int16_t>(right, vp->pos.x + vp->width);
891                     top = std::max<int16_t>(top, vp->pos.y);
892                     bottom = std::min<int16_t>(bottom, vp->pos.y + vp->height);
893                     if (left < right && top < bottom)
894                     {
895                         auto width = right - left;
896                         auto height = bottom - top;
897                         drawFunc(dpi, weatherDrawer, left, top, width, height);
898                     }
899                 }
900                 return;
901             }
902 
903             w = it->get();
904             if (right <= w->windowPos.x || bottom <= w->windowPos.y)
905             {
906                 continue;
907             }
908 
909             if (RCT_WINDOW_RIGHT(w) <= left || RCT_WINDOW_BOTTOM(w) <= top)
910             {
911                 continue;
912             }
913 
914             if (left >= w->windowPos.x)
915             {
916                 break;
917             }
918 
919             DrawWeatherWindow(dpi, weatherDrawer, original_w, left, w->windowPos.x, top, bottom, drawFunc);
920 
921             left = w->windowPos.x;
922             DrawWeatherWindow(dpi, weatherDrawer, original_w, left, right, top, bottom, drawFunc);
923             return;
924         }
925 
926         int16_t w_right = RCT_WINDOW_RIGHT(w);
927         if (right > w_right)
928         {
929             DrawWeatherWindow(dpi, weatherDrawer, original_w, left, w_right, top, bottom, drawFunc);
930 
931             left = w_right;
932             DrawWeatherWindow(dpi, weatherDrawer, original_w, left, right, top, bottom, drawFunc);
933             return;
934         }
935 
936         if (top < w->windowPos.y)
937         {
938             DrawWeatherWindow(dpi, weatherDrawer, original_w, left, right, top, w->windowPos.y, drawFunc);
939 
940             top = w->windowPos.y;
941             DrawWeatherWindow(dpi, weatherDrawer, original_w, left, right, top, bottom, drawFunc);
942             return;
943         }
944 
945         int16_t w_bottom = RCT_WINDOW_BOTTOM(w);
946         if (bottom > w_bottom)
947         {
948             DrawWeatherWindow(dpi, weatherDrawer, original_w, left, right, top, w_bottom, drawFunc);
949 
950             top = w_bottom;
951             DrawWeatherWindow(dpi, weatherDrawer, original_w, left, right, top, bottom, drawFunc);
952             return;
953         }
954     }
955 
GetInputEventFromSDLEvent(const SDL_Event & e)956     InputEvent GetInputEventFromSDLEvent(const SDL_Event& e)
957     {
958         InputEvent ie;
959         ie.DeviceKind = InputDeviceKind::Keyboard;
960         ie.Modifiers = e.key.keysym.mod;
961         ie.Button = e.key.keysym.sym;
962 
963         // Handle dead keys
964         if (ie.Button == (SDLK_SCANCODE_MASK | 0))
965         {
966             switch (e.key.keysym.scancode)
967             {
968                 case SDL_SCANCODE_APOSTROPHE:
969                     ie.Button = '\'';
970                     break;
971                 case SDL_SCANCODE_GRAVE:
972                     ie.Button = '`';
973                     break;
974                 default:
975                     break;
976             }
977         }
978 
979         return ie;
980     }
981 };
982 
CreateUiContext(const std::shared_ptr<IPlatformEnvironment> & env)983 std::unique_ptr<IUiContext> OpenRCT2::Ui::CreateUiContext(const std::shared_ptr<IPlatformEnvironment>& env)
984 {
985     return std::make_unique<UiContext>(env);
986 }
987 
GetInGameConsole()988 InGameConsole& OpenRCT2::Ui::GetInGameConsole()
989 {
990     auto uiContext = std::static_pointer_cast<UiContext>(GetContext()->GetUiContext());
991     return uiContext->GetInGameConsole();
992 }
993 
GetInputManager()994 InputManager& OpenRCT2::Ui::GetInputManager()
995 {
996     auto uiContext = std::static_pointer_cast<UiContext>(GetContext()->GetUiContext());
997     return uiContext->GetInputManager();
998 }
999 
GetShortcutManager()1000 ShortcutManager& OpenRCT2::Ui::GetShortcutManager()
1001 {
1002     auto uiContext = std::static_pointer_cast<UiContext>(GetContext()->GetUiContext());
1003     return uiContext->GetShortcutManager();
1004 }
1005