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