A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using System.Collections.Generic;
6using System.IO;
7using System.Linq;
8using System.Runtime.InteropServices;
9using JetBrains.Annotations;
10using osu.Framework.Bindables;
11using osu.Framework.Configuration;
12using osu.Framework.Extensions.EnumExtensions;
13using osu.Framework.Extensions.ImageExtensions;
14using osu.Framework.Input;
15using osu.Framework.Platform.SDL2;
16using osu.Framework.Platform.Windows.Native;
17using osu.Framework.Threading;
18using osuTK;
19using osuTK.Input;
20using SDL2;
21using SixLabors.ImageSharp;
22using SixLabors.ImageSharp.PixelFormats;
23using Image = SixLabors.ImageSharp.Image;
24using Point = System.Drawing.Point;
25using Rectangle = System.Drawing.Rectangle;
26using Size = System.Drawing.Size;
27
28// ReSharper disable UnusedParameter.Local (Class regularly handles native events where we don't consume all parameters)
29
30namespace osu.Framework.Platform
31{
32 /// <summary>
33 /// Default implementation of a desktop window, using SDL for windowing and graphics support.
34 /// </summary>
35 public class SDL2DesktopWindow : IWindow
36 {
37 internal IntPtr SDLWindowHandle { get; private set; } = IntPtr.Zero;
38
39 private readonly IGraphicsBackend graphicsBackend;
40
41 private bool focused;
42
43 /// <summary>
44 /// Whether the window currently has focus.
45 /// </summary>
46 public bool Focused
47 {
48 get => focused;
49 private set
50 {
51 if (value == focused)
52 return;
53
54 isActive.Value = focused = value;
55 }
56 }
57
58 /// <summary>
59 /// Enables or disables vertical sync.
60 /// </summary>
61 public bool VerticalSync
62 {
63 get => graphicsBackend.VerticalSync;
64 set => graphicsBackend.VerticalSync = value;
65 }
66
67 /// <summary>
68 /// Returns true if window has been created.
69 /// Returns false if the window has not yet been created, or has been closed.
70 /// </summary>
71 public bool Exists { get; private set; }
72
73 public WindowMode DefaultWindowMode => Configuration.WindowMode.Windowed;
74
75 /// <summary>
76 /// Returns the window modes that the platform should support by default.
77 /// </summary>
78 protected virtual IEnumerable<WindowMode> DefaultSupportedWindowModes => Enum.GetValues(typeof(WindowMode)).OfType<WindowMode>();
79
80 private Point position;
81
82 /// <summary>
83 /// Returns or sets the window's position in screen space. Only valid when in <see cref="osu.Framework.Configuration.WindowMode.Windowed"/>
84 /// </summary>
85 public Point Position
86 {
87 get => position;
88 set
89 {
90 position = value;
91 ScheduleCommand(() => SDL.SDL_SetWindowPosition(SDLWindowHandle, value.X, value.Y));
92 }
93 }
94
95 private bool resizable = true;
96
97 /// <summary>
98 /// Returns or sets whether the window is resizable or not. Only valid when in <see cref="osu.Framework.Platform.WindowState.Normal"/>.
99 /// </summary>
100 public bool Resizable
101 {
102 get => resizable;
103 set
104 {
105 if (resizable == value)
106 return;
107
108 resizable = value;
109 ScheduleCommand(() => SDL.SDL_SetWindowResizable(SDLWindowHandle, value ? SDL.SDL_bool.SDL_TRUE : SDL.SDL_bool.SDL_FALSE));
110 }
111 }
112
113 private bool relativeMouseMode;
114
115 /// <summary>
116 /// Set the state of SDL2's RelativeMouseMode (https://wiki.libsdl.org/SDL_SetRelativeMouseMode).
117 /// On all platforms, this will lock the mouse to the window (although escaping by setting <see cref="ConfineMouseMode"/> is still possible via a local implementation).
118 /// On windows, this will use raw input if available.
119 /// </summary>
120 public bool RelativeMouseMode
121 {
122 get => relativeMouseMode;
123 set
124 {
125 if (relativeMouseMode == value)
126 return;
127
128 if (value && !CursorState.HasFlagFast(CursorState.Hidden))
129 throw new InvalidOperationException($"Cannot set {nameof(RelativeMouseMode)} to true when the cursor is not hidden via {nameof(CursorState)}.");
130
131 relativeMouseMode = value;
132 ScheduleCommand(() => SDL.SDL_SetRelativeMouseMode(value ? SDL.SDL_bool.SDL_TRUE : SDL.SDL_bool.SDL_FALSE));
133 }
134 }
135
136 private Size size = new Size(default_width, default_height);
137
138 /// <summary>
139 /// Returns or sets the window's internal size, before scaling.
140 /// </summary>
141 public Size Size
142 {
143 get => size;
144 private set
145 {
146 if (value.Equals(size)) return;
147
148 size = value;
149 ScheduleEvent(() => Resized?.Invoke());
150 }
151 }
152
153 /// <summary>
154 /// Provides a bindable that controls the window's <see cref="CursorStateBindable"/>.
155 /// </summary>
156 public Bindable<CursorState> CursorStateBindable { get; } = new Bindable<CursorState>();
157
158 public CursorState CursorState
159 {
160 get => CursorStateBindable.Value;
161 set => CursorStateBindable.Value = value;
162 }
163
164 public Bindable<Display> CurrentDisplayBindable { get; } = new Bindable<Display>();
165
166 public Bindable<WindowMode> WindowMode { get; } = new Bindable<WindowMode>();
167
168 private readonly BindableBool isActive = new BindableBool();
169
170 public IBindable<bool> IsActive => isActive;
171
172 private readonly BindableBool cursorInWindow = new BindableBool();
173
174 public IBindable<bool> CursorInWindow => cursorInWindow;
175
176 public IBindableList<WindowMode> SupportedWindowModes { get; }
177
178 public BindableSafeArea SafeAreaPadding { get; } = new BindableSafeArea();
179
180 public virtual Point PointToClient(Point point) => point;
181
182 public virtual Point PointToScreen(Point point) => point;
183
184 private const int default_width = 1366;
185 private const int default_height = 768;
186
187 private const int default_icon_size = 256;
188
189 private readonly Scheduler commandScheduler = new Scheduler();
190 private readonly Scheduler eventScheduler = new Scheduler();
191
192 private readonly Dictionary<int, SDL2ControllerBindings> controllers = new Dictionary<int, SDL2ControllerBindings>();
193
194 private string title = string.Empty;
195
196 /// <summary>
197 /// Gets and sets the window title.
198 /// </summary>
199 public string Title
200 {
201 get => title;
202 set
203 {
204 title = value;
205 ScheduleCommand(() => SDL.SDL_SetWindowTitle(SDLWindowHandle, title));
206 }
207 }
208
209 private bool visible;
210
211 /// <summary>
212 /// Enables or disables the window visibility.
213 /// </summary>
214 public bool Visible
215 {
216 get => visible;
217 set
218 {
219 visible = value;
220 ScheduleCommand(() =>
221 {
222 if (value)
223 SDL.SDL_ShowWindow(SDLWindowHandle);
224 else
225 SDL.SDL_HideWindow(SDLWindowHandle);
226 });
227 }
228 }
229
230 private void updateCursorVisibility(bool visible) =>
231 ScheduleCommand(() => SDL.SDL_ShowCursor(visible ? SDL.SDL_ENABLE : SDL.SDL_DISABLE));
232
233 private void updateCursorConfined(bool confined) =>
234 ScheduleCommand(() => SDL.SDL_SetWindowGrab(SDLWindowHandle, confined ? SDL.SDL_bool.SDL_TRUE : SDL.SDL_bool.SDL_FALSE));
235
236 private WindowState windowState = WindowState.Normal;
237
238 private WindowState? pendingWindowState;
239
240 /// <summary>
241 /// Returns or sets the window's current <see cref="WindowState"/>.
242 /// </summary>
243 public WindowState WindowState
244 {
245 get => windowState;
246 set
247 {
248 if (pendingWindowState == null && windowState == value)
249 return;
250
251 pendingWindowState = value;
252 }
253 }
254
255 /// <summary>
256 /// Stores whether the window used to be in maximised state or not.
257 /// Used to properly decide what window state to pick when switching to windowed mode (see <see cref="WindowMode"/> change event)
258 /// </summary>
259 private bool windowMaximised;
260
261 /// <summary>
262 /// Returns the drawable area, after scaling.
263 /// </summary>
264 public Size ClientSize => new Size(Size.Width, Size.Height);
265
266 public float Scale = 1;
267
268 /// <summary>
269 /// Queries the physical displays and their supported resolutions.
270 /// </summary>
271 public IEnumerable<Display> Displays => Enumerable.Range(0, SDL.SDL_GetNumVideoDisplays()).Select(displayFromSDL);
272
273 /// <summary>
274 /// Gets the <see cref="Display"/> that has been set as "primary" or "default" in the operating system.
275 /// </summary>
276 public virtual Display PrimaryDisplay => Displays.First();
277
278 private Display currentDisplay;
279 private int displayIndex = -1;
280
281 /// <summary>
282 /// Gets or sets the <see cref="Display"/> that this window is currently on.
283 /// </summary>
284 public Display CurrentDisplay { get; private set; }
285
286 public readonly Bindable<ConfineMouseMode> ConfineMouseMode = new Bindable<ConfineMouseMode>();
287
288 private readonly Bindable<DisplayMode> currentDisplayMode = new Bindable<DisplayMode>();
289
290 /// <summary>
291 /// The <see cref="DisplayMode"/> for the display that this window is currently on.
292 /// </summary>
293 public IBindable<DisplayMode> CurrentDisplayMode => currentDisplayMode;
294
295 /// <summary>
296 /// Gets the native window handle as provided by the operating system.
297 /// </summary>
298 public IntPtr WindowHandle
299 {
300 get
301 {
302 if (SDLWindowHandle == IntPtr.Zero)
303 return IntPtr.Zero;
304
305 var wmInfo = getWindowWMInfo();
306
307 // Window handle is selected per subsystem as defined at:
308 // https://wiki.libsdl.org/SDL_SysWMinfo
309 switch (wmInfo.subsystem)
310 {
311 case SDL.SDL_SYSWM_TYPE.SDL_SYSWM_WINDOWS:
312 return wmInfo.info.win.window;
313
314 case SDL.SDL_SYSWM_TYPE.SDL_SYSWM_X11:
315 return wmInfo.info.x11.window;
316
317 case SDL.SDL_SYSWM_TYPE.SDL_SYSWM_DIRECTFB:
318 return wmInfo.info.dfb.window;
319
320 case SDL.SDL_SYSWM_TYPE.SDL_SYSWM_COCOA:
321 return wmInfo.info.cocoa.window;
322
323 case SDL.SDL_SYSWM_TYPE.SDL_SYSWM_UIKIT:
324 return wmInfo.info.uikit.window;
325
326 case SDL.SDL_SYSWM_TYPE.SDL_SYSWM_WAYLAND:
327 return wmInfo.info.wl.shell_surface;
328
329 case SDL.SDL_SYSWM_TYPE.SDL_SYSWM_ANDROID:
330 return wmInfo.info.android.window;
331
332 default:
333 return IntPtr.Zero;
334 }
335 }
336 }
337
338 private SDL.SDL_SysWMinfo getWindowWMInfo()
339 {
340 if (SDLWindowHandle == IntPtr.Zero)
341 return default;
342
343 var wmInfo = new SDL.SDL_SysWMinfo();
344 SDL.SDL_GetWindowWMInfo(SDLWindowHandle, ref wmInfo);
345 return wmInfo;
346 }
347
348 private Rectangle windowDisplayBounds
349 {
350 get
351 {
352 SDL.SDL_GetDisplayBounds(displayIndex, out var rect);
353 return new Rectangle(rect.x, rect.y, rect.w, rect.h);
354 }
355 }
356
357 public bool CapsLockPressed => SDL.SDL_GetModState().HasFlagFast(SDL.SDL_Keymod.KMOD_CAPS);
358
359 private bool firstDraw = true;
360
361 private readonly BindableSize sizeFullscreen = new BindableSize();
362 private readonly BindableSize sizeWindowed = new BindableSize();
363 private readonly BindableDouble windowPositionX = new BindableDouble();
364 private readonly BindableDouble windowPositionY = new BindableDouble();
365 private readonly Bindable<DisplayIndex> windowDisplayIndexBindable = new Bindable<DisplayIndex>();
366
367 public SDL2DesktopWindow()
368 {
369 SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_GAMECONTROLLER);
370
371 graphicsBackend = CreateGraphicsBackend();
372
373 SupportedWindowModes = new BindableList<WindowMode>(DefaultSupportedWindowModes);
374
375 CursorStateBindable.ValueChanged += evt =>
376 {
377 updateCursorVisibility(!evt.NewValue.HasFlagFast(CursorState.Hidden));
378 updateCursorConfined(evt.NewValue.HasFlagFast(CursorState.Confined));
379 };
380
381 populateJoysticks();
382 }
383
384 /// <summary>
385 /// Creates the window and initialises the graphics backend.
386 /// </summary>
387 public virtual void Create()
388 {
389 SDL.SDL_WindowFlags flags = SDL.SDL_WindowFlags.SDL_WINDOW_OPENGL |
390 SDL.SDL_WindowFlags.SDL_WINDOW_RESIZABLE |
391 SDL.SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI |
392 SDL.SDL_WindowFlags.SDL_WINDOW_HIDDEN | // shown after first swap to avoid white flash on startup (windows)
393 WindowState.ToFlags();
394
395 SDL.SDL_SetHint(SDL.SDL_HINT_WINDOWS_NO_CLOSE_ON_ALT_F4, "1");
396 SDL.SDL_SetHint(SDL.SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "1");
397
398 SDLWindowHandle = SDL.SDL_CreateWindow(title, Position.X, Position.Y, Size.Width, Size.Height, flags);
399
400 Exists = true;
401
402 MouseEntered += () => cursorInWindow.Value = true;
403 MouseLeft += () => cursorInWindow.Value = false;
404
405 graphicsBackend.Initialise(this);
406
407 updateWindowSpecifics();
408 updateWindowSize();
409 WindowMode.TriggerChange();
410 }
411
412 // reference must be kept to avoid GC, see https://stackoverflow.com/a/6193914
413 [UsedImplicitly]
414 private SDL.SDL_EventFilter eventFilterDelegate;
415
416 /// <summary>
417 /// Starts the window's run loop.
418 /// </summary>
419 public void Run()
420 {
421 // polling via SDL_PollEvent blocks on resizes (https://stackoverflow.com/a/50858339)
422 SDL.SDL_SetEventFilter(eventFilterDelegate = (_, eventPtr) =>
423 {
424 // ReSharper disable once PossibleNullReferenceException
425 var e = (SDL.SDL_Event)Marshal.PtrToStructure(eventPtr, typeof(SDL.SDL_Event));
426
427 if (e.type == SDL.SDL_EventType.SDL_WINDOWEVENT && e.window.windowEvent == SDL.SDL_WindowEventID.SDL_WINDOWEVENT_RESIZED)
428 {
429 updateWindowSize();
430 }
431
432 return 1;
433 }, IntPtr.Zero);
434
435 while (Exists)
436 {
437 commandScheduler.Update();
438
439 if (!Exists)
440 break;
441
442 if (pendingWindowState != null)
443 updateWindowSpecifics();
444
445 pollSDLEvents();
446
447 if (!cursorInWindow.Value)
448 pollMouse();
449
450 eventScheduler.Update();
451
452 Update?.Invoke();
453 }
454
455 Exited?.Invoke();
456
457 if (SDLWindowHandle != IntPtr.Zero)
458 SDL.SDL_DestroyWindow(SDLWindowHandle);
459
460 SDL.SDL_Quit();
461 }
462
463 /// <summary>
464 /// Updates the client size and the scale according to the window.
465 /// </summary>
466 /// <returns>Whether the window size has been changed after updating.</returns>
467 private void updateWindowSize()
468 {
469 SDL.SDL_GL_GetDrawableSize(SDLWindowHandle, out var w, out var h);
470 SDL.SDL_GetWindowSize(SDLWindowHandle, out var actualW, out var _);
471
472 Scale = (float)w / actualW;
473 Size = new Size(w, h);
474
475 // This function may be invoked before the SDL internal states are all changed. (as documented here: https://wiki.libsdl.org/SDL_SetEventFilter)
476 // Scheduling the store to config until after the event poll has run will ensure the window is in the correct state.
477 eventScheduler.Add(storeWindowSizeToConfig, true);
478 }
479
480 /// <summary>
481 /// Forcefully closes the window.
482 /// </summary>
483 public void Close() => ScheduleCommand(() => Exists = false);
484
485 /// <summary>
486 /// Attempts to close the window.
487 /// </summary>
488 public void RequestClose() => ScheduleEvent(() =>
489 {
490 if (ExitRequested?.Invoke() != true)
491 Close();
492 });
493
494 public void SwapBuffers()
495 {
496 graphicsBackend.SwapBuffers();
497
498 if (firstDraw)
499 {
500 Visible = true;
501 firstDraw = false;
502 }
503 }
504
505 /// <summary>
506 /// Requests that the graphics backend become the current context.
507 /// May not be required for some backends.
508 /// </summary>
509 public void MakeCurrent() => graphicsBackend.MakeCurrent();
510
511 /// <summary>
512 /// Requests that the current context be cleared.
513 /// </summary>
514 public void ClearCurrent() => graphicsBackend.ClearCurrent();
515
516 private void enqueueJoystickAxisInput(JoystickAxisSource axisSource, short axisValue)
517 {
518 // SDL reports axis values in the range short.MinValue to short.MaxValue, so we scale and clamp it to the range of -1f to 1f
519 var clamped = Math.Clamp((float)axisValue / short.MaxValue, -1f, 1f);
520 ScheduleEvent(() => JoystickAxisChanged?.Invoke(new JoystickAxis(axisSource, clamped)));
521 }
522
523 private void enqueueJoystickButtonInput(JoystickButton button, bool isPressed)
524 {
525 if (isPressed)
526 ScheduleEvent(() => JoystickButtonDown?.Invoke(button));
527 else
528 ScheduleEvent(() => JoystickButtonUp?.Invoke(button));
529 }
530
531 /// <summary>
532 /// Attempts to set the window's icon to the specified image.
533 /// </summary>
534 /// <param name="image">An <see cref="Image{Rgba32}"/> to set as the window icon.</param>
535 private unsafe void setSDLIcon(Image<Rgba32> image)
536 {
537 var pixelMemory = image.CreateReadOnlyPixelMemory();
538 var imageSize = image.Size();
539
540 ScheduleCommand(() =>
541 {
542 var pixelSpan = pixelMemory.Span;
543
544 IntPtr surface;
545 fixed (Rgba32* ptr = pixelSpan)
546 surface = SDL.SDL_CreateRGBSurfaceFrom(new IntPtr(ptr), imageSize.Width, imageSize.Height, 32, imageSize.Width * 4, 0xff, 0xff00, 0xff0000, 0xff000000);
547
548 SDL.SDL_SetWindowIcon(SDLWindowHandle, surface);
549 SDL.SDL_FreeSurface(surface);
550 });
551 }
552
553 private Point previousPolledPoint = Point.Empty;
554
555 private void pollMouse()
556 {
557 SDL.SDL_GetGlobalMouseState(out var x, out var y);
558 if (previousPolledPoint.X == x && previousPolledPoint.Y == y)
559 return;
560
561 previousPolledPoint = new Point(x, y);
562
563 var pos = WindowMode.Value == Configuration.WindowMode.Windowed ? Position : windowDisplayBounds.Location;
564 var rx = x - pos.X;
565 var ry = y - pos.Y;
566
567 ScheduleEvent(() => MouseMove?.Invoke(new Vector2(rx * Scale, ry * Scale)));
568 }
569
570 #region SDL Event Handling
571
572 /// <summary>
573 /// Adds an <see cref="Action"/> to the <see cref="Scheduler"/> expected to handle event callbacks.
574 /// </summary>
575 /// <param name="action">The <see cref="Action"/> to execute.</param>
576 protected void ScheduleEvent(Action action) => eventScheduler.Add(action, false);
577
578 protected void ScheduleCommand(Action action) => commandScheduler.Add(action, false);
579
580 /// <summary>
581 /// Poll for all pending events.
582 /// </summary>
583 private void pollSDLEvents()
584 {
585 while (SDL.SDL_PollEvent(out var e) > 0)
586 handleSDLEvent(e);
587 }
588
589 private void handleSDLEvent(SDL.SDL_Event e)
590 {
591 switch (e.type)
592 {
593 case SDL.SDL_EventType.SDL_QUIT:
594 case SDL.SDL_EventType.SDL_APP_TERMINATING:
595 handleQuitEvent(e.quit);
596 break;
597
598 case SDL.SDL_EventType.SDL_WINDOWEVENT:
599 handleWindowEvent(e.window);
600 break;
601
602 case SDL.SDL_EventType.SDL_KEYDOWN:
603 case SDL.SDL_EventType.SDL_KEYUP:
604 handleKeyboardEvent(e.key);
605 break;
606
607 case SDL.SDL_EventType.SDL_TEXTEDITING:
608 handleTextEditingEvent(e.edit);
609 break;
610
611 case SDL.SDL_EventType.SDL_TEXTINPUT:
612 handleTextInputEvent(e.text);
613 break;
614
615 case SDL.SDL_EventType.SDL_MOUSEMOTION:
616 handleMouseMotionEvent(e.motion);
617 break;
618
619 case SDL.SDL_EventType.SDL_MOUSEBUTTONDOWN:
620 case SDL.SDL_EventType.SDL_MOUSEBUTTONUP:
621 handleMouseButtonEvent(e.button);
622 break;
623
624 case SDL.SDL_EventType.SDL_MOUSEWHEEL:
625 handleMouseWheelEvent(e.wheel);
626 break;
627
628 case SDL.SDL_EventType.SDL_JOYAXISMOTION:
629 handleJoyAxisEvent(e.jaxis);
630 break;
631
632 case SDL.SDL_EventType.SDL_JOYBALLMOTION:
633 handleJoyBallEvent(e.jball);
634 break;
635
636 case SDL.SDL_EventType.SDL_JOYHATMOTION:
637 handleJoyHatEvent(e.jhat);
638 break;
639
640 case SDL.SDL_EventType.SDL_JOYBUTTONDOWN:
641 case SDL.SDL_EventType.SDL_JOYBUTTONUP:
642 handleJoyButtonEvent(e.jbutton);
643 break;
644
645 case SDL.SDL_EventType.SDL_JOYDEVICEADDED:
646 case SDL.SDL_EventType.SDL_JOYDEVICEREMOVED:
647 handleJoyDeviceEvent(e.jdevice);
648 break;
649
650 case SDL.SDL_EventType.SDL_CONTROLLERAXISMOTION:
651 handleControllerAxisEvent(e.caxis);
652 break;
653
654 case SDL.SDL_EventType.SDL_CONTROLLERBUTTONDOWN:
655 case SDL.SDL_EventType.SDL_CONTROLLERBUTTONUP:
656 handleControllerButtonEvent(e.cbutton);
657 break;
658
659 case SDL.SDL_EventType.SDL_CONTROLLERDEVICEADDED:
660 case SDL.SDL_EventType.SDL_CONTROLLERDEVICEREMOVED:
661 case SDL.SDL_EventType.SDL_CONTROLLERDEVICEREMAPPED:
662 handleControllerDeviceEvent(e.cdevice);
663 break;
664
665 case SDL.SDL_EventType.SDL_FINGERDOWN:
666 case SDL.SDL_EventType.SDL_FINGERUP:
667 case SDL.SDL_EventType.SDL_FINGERMOTION:
668 handleTouchFingerEvent(e.tfinger);
669 break;
670
671 case SDL.SDL_EventType.SDL_DROPFILE:
672 case SDL.SDL_EventType.SDL_DROPTEXT:
673 case SDL.SDL_EventType.SDL_DROPBEGIN:
674 case SDL.SDL_EventType.SDL_DROPCOMPLETE:
675 handleDropEvent(e.drop);
676 break;
677 }
678 }
679
680 private void handleQuitEvent(SDL.SDL_QuitEvent evtQuit) => RequestClose();
681
682 private void handleDropEvent(SDL.SDL_DropEvent evtDrop)
683 {
684 switch (evtDrop.type)
685 {
686 case SDL.SDL_EventType.SDL_DROPFILE:
687 var str = SDL.UTF8_ToManaged(evtDrop.file, true);
688 if (str != null)
689 ScheduleEvent(() => DragDrop?.Invoke(str));
690
691 break;
692 }
693 }
694
695 private void handleTouchFingerEvent(SDL.SDL_TouchFingerEvent evtTfinger)
696 {
697 }
698
699 private void handleControllerDeviceEvent(SDL.SDL_ControllerDeviceEvent evtCdevice)
700 {
701 switch (evtCdevice.type)
702 {
703 case SDL.SDL_EventType.SDL_CONTROLLERDEVICEADDED:
704 addJoystick(evtCdevice.which);
705 break;
706
707 case SDL.SDL_EventType.SDL_CONTROLLERDEVICEREMOVED:
708 SDL.SDL_GameControllerClose(controllers[evtCdevice.which].ControllerHandle);
709 controllers.Remove(evtCdevice.which);
710 break;
711
712 case SDL.SDL_EventType.SDL_CONTROLLERDEVICEREMAPPED:
713 if (controllers.TryGetValue(evtCdevice.which, out var state))
714 state.PopulateBindings();
715
716 break;
717 }
718 }
719
720 private void handleControllerButtonEvent(SDL.SDL_ControllerButtonEvent evtCbutton)
721 {
722 var button = ((SDL.SDL_GameControllerButton)evtCbutton.button).ToJoystickButton();
723
724 switch (evtCbutton.type)
725 {
726 case SDL.SDL_EventType.SDL_CONTROLLERBUTTONDOWN:
727 enqueueJoystickButtonInput(button, true);
728 break;
729
730 case SDL.SDL_EventType.SDL_CONTROLLERBUTTONUP:
731 enqueueJoystickButtonInput(button, false);
732 break;
733 }
734 }
735
736 private void handleControllerAxisEvent(SDL.SDL_ControllerAxisEvent evtCaxis) =>
737 enqueueJoystickAxisInput(((SDL.SDL_GameControllerAxis)evtCaxis.axis).ToJoystickAxisSource(), evtCaxis.axisValue);
738
739 private void addJoystick(int which)
740 {
741 var instanceID = SDL.SDL_JoystickGetDeviceInstanceID(which);
742
743 // if the joystick is already opened, ignore it
744 if (controllers.ContainsKey(instanceID))
745 return;
746
747 var joystick = SDL.SDL_JoystickOpen(which);
748
749 var controller = IntPtr.Zero;
750 if (SDL.SDL_IsGameController(which) == SDL.SDL_bool.SDL_TRUE)
751 controller = SDL.SDL_GameControllerOpen(which);
752
753 controllers[instanceID] = new SDL2ControllerBindings(joystick, controller);
754 }
755
756 /// <summary>
757 /// Populates <see cref="controllers"/> with joysticks that are already connected.
758 /// </summary>
759 private void populateJoysticks()
760 {
761 for (int i = 0; i < SDL.SDL_NumJoysticks(); i++)
762 {
763 addJoystick(i);
764 }
765 }
766
767 private void handleJoyDeviceEvent(SDL.SDL_JoyDeviceEvent evtJdevice)
768 {
769 switch (evtJdevice.type)
770 {
771 case SDL.SDL_EventType.SDL_JOYDEVICEADDED:
772 addJoystick(evtJdevice.which);
773 break;
774
775 case SDL.SDL_EventType.SDL_JOYDEVICEREMOVED:
776 // if the joystick is already closed, ignore it
777 if (!controllers.ContainsKey(evtJdevice.which))
778 break;
779
780 SDL.SDL_JoystickClose(controllers[evtJdevice.which].JoystickHandle);
781 controllers.Remove(evtJdevice.which);
782 break;
783 }
784 }
785
786 private void handleJoyButtonEvent(SDL.SDL_JoyButtonEvent evtJbutton)
787 {
788 // if this button exists in the controller bindings, skip it
789 if (controllers.TryGetValue(evtJbutton.which, out var state) && state.GetButtonForIndex(evtJbutton.button) != SDL.SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID)
790 return;
791
792 var button = JoystickButton.FirstButton + evtJbutton.button;
793
794 switch (evtJbutton.type)
795 {
796 case SDL.SDL_EventType.SDL_JOYBUTTONDOWN:
797 enqueueJoystickButtonInput(button, true);
798 break;
799
800 case SDL.SDL_EventType.SDL_JOYBUTTONUP:
801 enqueueJoystickButtonInput(button, false);
802 break;
803 }
804 }
805
806 private void handleJoyHatEvent(SDL.SDL_JoyHatEvent evtJhat)
807 {
808 }
809
810 private void handleJoyBallEvent(SDL.SDL_JoyBallEvent evtJball)
811 {
812 }
813
814 private void handleJoyAxisEvent(SDL.SDL_JoyAxisEvent evtJaxis)
815 {
816 // if this axis exists in the controller bindings, skip it
817 if (controllers.TryGetValue(evtJaxis.which, out var state) && state.GetAxisForIndex(evtJaxis.axis) != SDL.SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_INVALID)
818 return;
819
820 enqueueJoystickAxisInput(JoystickAxisSource.Axis1 + evtJaxis.axis, evtJaxis.axisValue);
821 }
822
823 private void handleMouseWheelEvent(SDL.SDL_MouseWheelEvent evtWheel) =>
824 ScheduleEvent(() => TriggerMouseWheel(new Vector2(evtWheel.x, evtWheel.y), false));
825
826 private void handleMouseButtonEvent(SDL.SDL_MouseButtonEvent evtButton)
827 {
828 MouseButton button = mouseButtonFromEvent(evtButton.button);
829
830 switch (evtButton.type)
831 {
832 case SDL.SDL_EventType.SDL_MOUSEBUTTONDOWN:
833 ScheduleEvent(() => MouseDown?.Invoke(button));
834 break;
835
836 case SDL.SDL_EventType.SDL_MOUSEBUTTONUP:
837 ScheduleEvent(() => MouseUp?.Invoke(button));
838 break;
839 }
840 }
841
842 private void handleMouseMotionEvent(SDL.SDL_MouseMotionEvent evtMotion)
843 {
844 if (SDL.SDL_GetRelativeMouseMode() == SDL.SDL_bool.SDL_FALSE)
845 ScheduleEvent(() => MouseMove?.Invoke(new Vector2(evtMotion.x * Scale, evtMotion.y * Scale)));
846 else
847 ScheduleEvent(() => MouseMoveRelative?.Invoke(new Vector2(evtMotion.xrel * Scale, evtMotion.yrel * Scale)));
848 }
849
850 private unsafe void handleTextInputEvent(SDL.SDL_TextInputEvent evtText)
851 {
852 var ptr = new IntPtr(evtText.text);
853 if (ptr == IntPtr.Zero)
854 return;
855
856 string text = Marshal.PtrToStringUTF8(ptr) ?? "";
857
858 foreach (char c in text)
859 ScheduleEvent(() => KeyTyped?.Invoke(c));
860 }
861
862 private void handleTextEditingEvent(SDL.SDL_TextEditingEvent evtEdit)
863 {
864 }
865
866 private void handleKeyboardEvent(SDL.SDL_KeyboardEvent evtKey)
867 {
868 Key key = evtKey.keysym.ToKey();
869
870 if (key == Key.Unknown)
871 return;
872
873 switch (evtKey.type)
874 {
875 case SDL.SDL_EventType.SDL_KEYDOWN:
876 ScheduleEvent(() => KeyDown?.Invoke(key));
877 break;
878
879 case SDL.SDL_EventType.SDL_KEYUP:
880 ScheduleEvent(() => KeyUp?.Invoke(key));
881 break;
882 }
883 }
884
885 private void handleWindowEvent(SDL.SDL_WindowEvent evtWindow)
886 {
887 updateWindowSpecifics();
888
889 switch (evtWindow.windowEvent)
890 {
891 case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_MOVED:
892 // explicitly requery as there are occasions where what SDL has provided us with is not up-to-date.
893 SDL.SDL_GetWindowPosition(SDLWindowHandle, out int x, out int y);
894 var newPosition = new Point(x, y);
895
896 if (!newPosition.Equals(Position))
897 {
898 position = newPosition;
899 ScheduleEvent(() => Moved?.Invoke(newPosition));
900
901 if (WindowMode.Value == Configuration.WindowMode.Windowed)
902 storeWindowPositionToConfig();
903 }
904
905 break;
906
907 case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_SIZE_CHANGED:
908 updateWindowSize();
909 break;
910
911 case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_ENTER:
912 cursorInWindow.Value = true;
913 ScheduleEvent(() => MouseEntered?.Invoke());
914 break;
915
916 case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_LEAVE:
917 cursorInWindow.Value = false;
918 ScheduleEvent(() => MouseLeft?.Invoke());
919 break;
920
921 case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_RESTORED:
922 case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_FOCUS_GAINED:
923 ScheduleEvent(() => Focused = true);
924 break;
925
926 case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_MINIMIZED:
927 case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_FOCUS_LOST:
928 ScheduleEvent(() => Focused = false);
929 break;
930
931 case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE:
932 break;
933 }
934 }
935
936 /// <summary>
937 /// Should be run on a regular basis to check for external window state changes.
938 /// </summary>
939 private void updateWindowSpecifics()
940 {
941 // don't attempt to run before the window is initialised, as Create() will do so anyway.
942 if (SDLWindowHandle == IntPtr.Zero)
943 return;
944
945 var stateBefore = windowState;
946
947 // check for a pending user state change and give precedence.
948 if (pendingWindowState != null)
949 {
950 windowState = pendingWindowState.Value;
951 pendingWindowState = null;
952
953 updateWindowStateAndSize();
954 }
955 else
956 {
957 windowState = ((SDL.SDL_WindowFlags)SDL.SDL_GetWindowFlags(SDLWindowHandle)).ToWindowState();
958 }
959
960 if (windowState != stateBefore)
961 {
962 ScheduleEvent(() => WindowStateChanged?.Invoke(windowState));
963 updateMaximisedState();
964 }
965
966 int newDisplayIndex = SDL.SDL_GetWindowDisplayIndex(SDLWindowHandle);
967
968 if (displayIndex != newDisplayIndex)
969 {
970 displayIndex = newDisplayIndex;
971 currentDisplay = Displays.ElementAtOrDefault(displayIndex) ?? PrimaryDisplay;
972 ScheduleEvent(() =>
973 {
974 CurrentDisplayBindable.Value = currentDisplay;
975 });
976 }
977 }
978
979 /// <summary>
980 /// Should be run after a local window state change, to propagate the correct SDL actions.
981 /// </summary>
982 private void updateWindowStateAndSize()
983 {
984 // this reset is required even on changing from one fullscreen resolution to another.
985 // if it is not included, the GL context will not get the correct size.
986 // this is mentioned by multiple sources as an SDL issue, which seems to resolve by similar means (see https://discourse.libsdl.org/t/sdl-setwindowsize-does-not-work-in-fullscreen/20711/4).
987 SDL.SDL_SetWindowBordered(SDLWindowHandle, SDL.SDL_bool.SDL_TRUE);
988 SDL.SDL_SetWindowFullscreen(SDLWindowHandle, (uint)SDL.SDL_bool.SDL_FALSE);
989
990 switch (windowState)
991 {
992 case WindowState.Normal:
993 Size = (sizeWindowed.Value * Scale).ToSize();
994
995 SDL.SDL_RestoreWindow(SDLWindowHandle);
996 SDL.SDL_SetWindowSize(SDLWindowHandle, sizeWindowed.Value.Width, sizeWindowed.Value.Height);
997 SDL.SDL_SetWindowResizable(SDLWindowHandle, Resizable ? SDL.SDL_bool.SDL_TRUE : SDL.SDL_bool.SDL_FALSE);
998
999 readWindowPositionFromConfig();
1000 break;
1001
1002 case WindowState.Fullscreen:
1003 var closestMode = getClosestDisplayMode(sizeFullscreen.Value, currentDisplayMode.Value.RefreshRate, currentDisplay.Index);
1004
1005 Size = new Size(closestMode.w, closestMode.h);
1006
1007 SDL.SDL_SetWindowDisplayMode(SDLWindowHandle, ref closestMode);
1008 SDL.SDL_SetWindowFullscreen(SDLWindowHandle, (uint)SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN);
1009 break;
1010
1011 case WindowState.FullscreenBorderless:
1012 Size = SetBorderless();
1013 break;
1014
1015 case WindowState.Maximised:
1016 SDL.SDL_RestoreWindow(SDLWindowHandle);
1017 SDL.SDL_MaximizeWindow(SDLWindowHandle);
1018
1019 SDL.SDL_GL_GetDrawableSize(SDLWindowHandle, out int w, out int h);
1020 Size = new Size(w, h);
1021 break;
1022
1023 case WindowState.Minimised:
1024 SDL.SDL_MinimizeWindow(SDLWindowHandle);
1025 break;
1026 }
1027
1028 updateMaximisedState();
1029
1030 if (SDL.SDL_GetWindowDisplayMode(SDLWindowHandle, out var mode) >= 0)
1031 currentDisplayMode.Value = new DisplayMode(mode.format.ToString(), new Size(mode.w, mode.h), 32, mode.refresh_rate, displayIndex, displayIndex);
1032 }
1033
1034 private void updateMaximisedState()
1035 {
1036 if (windowState == WindowState.Normal || windowState == WindowState.Maximised)
1037 windowMaximised = windowState == WindowState.Maximised;
1038 }
1039
1040 private void readWindowPositionFromConfig()
1041 {
1042 if (WindowState != WindowState.Normal)
1043 return;
1044
1045 var configPosition = new Vector2((float)windowPositionX.Value, (float)windowPositionY.Value);
1046
1047 var displayBounds = CurrentDisplay.Bounds;
1048 var windowSize = sizeWindowed.Value;
1049 var windowX = (int)Math.Round((displayBounds.Width - windowSize.Width) * configPosition.X);
1050 var windowY = (int)Math.Round((displayBounds.Height - windowSize.Height) * configPosition.Y);
1051
1052 Position = new Point(windowX + displayBounds.X, windowY + displayBounds.Y);
1053 }
1054
1055 private void storeWindowPositionToConfig()
1056 {
1057 if (WindowState != WindowState.Normal)
1058 return;
1059
1060 var displayBounds = CurrentDisplay.Bounds;
1061
1062 var windowX = Position.X - displayBounds.X;
1063 var windowY = Position.Y - displayBounds.Y;
1064
1065 var windowSize = sizeWindowed.Value;
1066
1067 windowPositionX.Value = displayBounds.Width > windowSize.Width ? (float)windowX / (displayBounds.Width - windowSize.Width) : 0;
1068 windowPositionY.Value = displayBounds.Height > windowSize.Height ? (float)windowY / (displayBounds.Height - windowSize.Height) : 0;
1069 }
1070
1071 /// <summary>
1072 /// Set to <c>true</c> while the window size is being stored to config to avoid bindable feedback.
1073 /// </summary>
1074 private bool storingSizeToConfig;
1075
1076 private void storeWindowSizeToConfig()
1077 {
1078 if (WindowState != WindowState.Normal)
1079 return;
1080
1081 storingSizeToConfig = true;
1082 sizeWindowed.Value = (Size / Scale).ToSize();
1083 storingSizeToConfig = false;
1084 }
1085
1086 /// <summary>
1087 /// Prepare display of a borderless window.
1088 /// </summary>
1089 /// <returns>
1090 /// The size of the borderless window's draw area.
1091 /// </returns>
1092 protected virtual Size SetBorderless()
1093 {
1094 // this is a generally sane method of handling borderless, and works well on macOS and linux.
1095 SDL.SDL_SetWindowFullscreen(SDLWindowHandle, (uint)SDL.SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP);
1096
1097 return currentDisplay.Bounds.Size;
1098 }
1099
1100 private MouseButton mouseButtonFromEvent(byte button)
1101 {
1102 switch ((uint)button)
1103 {
1104 default:
1105 case SDL.SDL_BUTTON_LEFT:
1106 return MouseButton.Left;
1107
1108 case SDL.SDL_BUTTON_RIGHT:
1109 return MouseButton.Right;
1110
1111 case SDL.SDL_BUTTON_MIDDLE:
1112 return MouseButton.Middle;
1113
1114 case SDL.SDL_BUTTON_X1:
1115 return MouseButton.Button1;
1116
1117 case SDL.SDL_BUTTON_X2:
1118 return MouseButton.Button2;
1119 }
1120 }
1121
1122 #endregion
1123
1124 protected virtual IGraphicsBackend CreateGraphicsBackend() => new SDL2GraphicsBackend();
1125
1126 public void SetupWindow(FrameworkConfigManager config)
1127 {
1128 CurrentDisplayBindable.ValueChanged += evt =>
1129 {
1130 windowDisplayIndexBindable.Value = (DisplayIndex)evt.NewValue.Index;
1131 windowPositionX.Value = 0.5;
1132 windowPositionY.Value = 0.5;
1133 };
1134
1135 config.BindWith(FrameworkSetting.LastDisplayDevice, windowDisplayIndexBindable);
1136 windowDisplayIndexBindable.BindValueChanged(evt => CurrentDisplay = Displays.ElementAtOrDefault((int)evt.NewValue) ?? PrimaryDisplay, true);
1137
1138 sizeFullscreen.ValueChanged += evt =>
1139 {
1140 if (storingSizeToConfig) return;
1141 if (windowState != WindowState.Fullscreen) return;
1142
1143 pendingWindowState = windowState;
1144 };
1145
1146 sizeWindowed.ValueChanged += evt =>
1147 {
1148 if (storingSizeToConfig) return;
1149 if (windowState != WindowState.Normal) return;
1150
1151 pendingWindowState = windowState;
1152 };
1153
1154 config.BindWith(FrameworkSetting.SizeFullscreen, sizeFullscreen);
1155 config.BindWith(FrameworkSetting.WindowedSize, sizeWindowed);
1156
1157 config.BindWith(FrameworkSetting.WindowedPositionX, windowPositionX);
1158 config.BindWith(FrameworkSetting.WindowedPositionY, windowPositionY);
1159
1160 config.BindWith(FrameworkSetting.WindowMode, WindowMode);
1161 config.BindWith(FrameworkSetting.ConfineMouseMode, ConfineMouseMode);
1162
1163 WindowMode.BindValueChanged(evt =>
1164 {
1165 switch (evt.NewValue)
1166 {
1167 case Configuration.WindowMode.Fullscreen:
1168 WindowState = WindowState.Fullscreen;
1169 break;
1170
1171 case Configuration.WindowMode.Borderless:
1172 WindowState = WindowState.FullscreenBorderless;
1173 break;
1174
1175 case Configuration.WindowMode.Windowed:
1176 WindowState = windowMaximised ? WindowState.Maximised : WindowState.Normal;
1177 break;
1178 }
1179
1180 updateConfineMode();
1181 });
1182
1183 ConfineMouseMode.BindValueChanged(_ => updateConfineMode());
1184 }
1185
1186 public void CycleMode()
1187 {
1188 var currentValue = WindowMode.Value;
1189
1190 do
1191 {
1192 switch (currentValue)
1193 {
1194 case Configuration.WindowMode.Windowed:
1195 currentValue = Configuration.WindowMode.Borderless;
1196 break;
1197
1198 case Configuration.WindowMode.Borderless:
1199 currentValue = Configuration.WindowMode.Fullscreen;
1200 break;
1201
1202 case Configuration.WindowMode.Fullscreen:
1203 currentValue = Configuration.WindowMode.Windowed;
1204 break;
1205 }
1206 } while (!SupportedWindowModes.Contains(currentValue) && currentValue != WindowMode.Value);
1207
1208 WindowMode.Value = currentValue;
1209 }
1210
1211 /// <summary>
1212 /// Update the host window manager's cursor position based on a location relative to window coordinates.
1213 /// </summary>
1214 /// <param name="position">A position inside the window.</param>
1215 public void UpdateMousePosition(Vector2 position) => ScheduleCommand(() =>
1216 SDL.SDL_WarpMouseInWindow(SDLWindowHandle, (int)(position.X / Scale), (int)(position.Y / Scale)));
1217
1218 public void SetIconFromStream(Stream stream)
1219 {
1220 using (var ms = new MemoryStream())
1221 {
1222 stream.CopyTo(ms);
1223 ms.Position = 0;
1224
1225 var imageInfo = Image.Identify(ms);
1226
1227 if (imageInfo != null)
1228 SetIconFromImage(Image.Load<Rgba32>(ms.GetBuffer()));
1229 else if (IconGroup.TryParse(ms.GetBuffer(), out var iconGroup))
1230 SetIconFromGroup(iconGroup);
1231 }
1232 }
1233
1234 internal virtual void SetIconFromGroup(IconGroup iconGroup)
1235 {
1236 // LoadRawIcon returns raw PNG data if available, which avoids any Windows-specific pinvokes
1237 var bytes = iconGroup.LoadRawIcon(default_icon_size, default_icon_size);
1238 if (bytes == null)
1239 return;
1240
1241 SetIconFromImage(Image.Load<Rgba32>(bytes));
1242 }
1243
1244 internal virtual void SetIconFromImage(Image<Rgba32> iconImage) => setSDLIcon(iconImage);
1245
1246 private void updateConfineMode()
1247 {
1248 bool confine = false;
1249
1250 switch (ConfineMouseMode.Value)
1251 {
1252 case Input.ConfineMouseMode.Fullscreen:
1253 confine = WindowMode.Value != Configuration.WindowMode.Windowed;
1254 break;
1255
1256 case Input.ConfineMouseMode.Always:
1257 confine = true;
1258 break;
1259 }
1260
1261 if (confine)
1262 CursorStateBindable.Value |= CursorState.Confined;
1263 else
1264 CursorStateBindable.Value &= ~CursorState.Confined;
1265 }
1266
1267 #region Helper functions
1268
1269 private SDL.SDL_DisplayMode getClosestDisplayMode(Size size, int refreshRate, int displayIndex)
1270 {
1271 var targetMode = new SDL.SDL_DisplayMode { w = size.Width, h = size.Height, refresh_rate = refreshRate };
1272
1273 if (SDL.SDL_GetClosestDisplayMode(displayIndex, ref targetMode, out var mode) != IntPtr.Zero)
1274 return mode;
1275
1276 // fallback to current display's native bounds
1277 targetMode.w = currentDisplay.Bounds.Width;
1278 targetMode.h = currentDisplay.Bounds.Height;
1279 targetMode.refresh_rate = 0;
1280
1281 if (SDL.SDL_GetClosestDisplayMode(displayIndex, ref targetMode, out mode) != IntPtr.Zero)
1282 return mode;
1283
1284 // finally return the current mode if everything else fails.
1285 // not sure this is required.
1286 if (SDL.SDL_GetWindowDisplayMode(SDLWindowHandle, out mode) >= 0)
1287 return mode;
1288
1289 throw new InvalidOperationException("couldn't retrieve valid display mode");
1290 }
1291
1292 private static Display displayFromSDL(int displayIndex)
1293 {
1294 var displayModes = Enumerable.Range(0, SDL.SDL_GetNumDisplayModes(displayIndex))
1295 .Select(modeIndex =>
1296 {
1297 SDL.SDL_GetDisplayMode(displayIndex, modeIndex, out var mode);
1298 return displayModeFromSDL(mode, displayIndex, modeIndex);
1299 })
1300 .ToArray();
1301
1302 SDL.SDL_GetDisplayBounds(displayIndex, out var rect);
1303 return new Display(displayIndex, SDL.SDL_GetDisplayName(displayIndex), new Rectangle(rect.x, rect.y, rect.w, rect.h), displayModes);
1304 }
1305
1306 private static DisplayMode displayModeFromSDL(SDL.SDL_DisplayMode mode, int displayIndex, int modeIndex)
1307 {
1308 SDL.SDL_PixelFormatEnumToMasks(mode.format, out var bpp, out _, out _, out _, out _);
1309 return new DisplayMode(SDL.SDL_GetPixelFormatName(mode.format), new Size(mode.w, mode.h), bpp, mode.refresh_rate, modeIndex, displayIndex);
1310 }
1311
1312 #endregion
1313
1314 #region Events
1315
1316 /// <summary>
1317 /// Invoked once every window event loop.
1318 /// </summary>
1319 public event Action Update;
1320
1321 /// <summary>
1322 /// Invoked after the window has resized.
1323 /// </summary>
1324 public event Action Resized;
1325
1326 /// <summary>
1327 /// Invoked after the window's state has changed.
1328 /// </summary>
1329 public event Action<WindowState> WindowStateChanged;
1330
1331 /// <summary>
1332 /// Invoked when the user attempts to close the window. Return value of true will cancel exit.
1333 /// </summary>
1334 public event Func<bool> ExitRequested;
1335
1336 /// <summary>
1337 /// Invoked when the window is about to close.
1338 /// </summary>
1339 public event Action Exited;
1340
1341 /// <summary>
1342 /// Invoked when the mouse cursor enters the window.
1343 /// </summary>
1344 public event Action MouseEntered;
1345
1346 /// <summary>
1347 /// Invoked when the mouse cursor leaves the window.
1348 /// </summary>
1349 public event Action MouseLeft;
1350
1351 /// <summary>
1352 /// Invoked when the window moves.
1353 /// </summary>
1354 public event Action<Point> Moved;
1355
1356 /// <summary>
1357 /// Invoked when the user scrolls the mouse wheel over the window.
1358 /// </summary>
1359 public event Action<Vector2, bool> MouseWheel;
1360
1361 protected void TriggerMouseWheel(Vector2 delta, bool precise) => MouseWheel?.Invoke(delta, precise);
1362
1363 /// <summary>
1364 /// Invoked when the user moves the mouse cursor within the window.
1365 /// </summary>
1366 public event Action<Vector2> MouseMove;
1367
1368 /// <summary>
1369 /// Invoked when the user moves the mouse cursor within the window (via relative / raw input).
1370 /// </summary>
1371 public event Action<Vector2> MouseMoveRelative;
1372
1373 /// <summary>
1374 /// Invoked when the user presses a mouse button.
1375 /// </summary>
1376 public event Action<MouseButton> MouseDown;
1377
1378 /// <summary>
1379 /// Invoked when the user releases a mouse button.
1380 /// </summary>
1381 public event Action<MouseButton> MouseUp;
1382
1383 /// <summary>
1384 /// Invoked when the user presses a key.
1385 /// </summary>
1386 public event Action<Key> KeyDown;
1387
1388 /// <summary>
1389 /// Invoked when the user releases a key.
1390 /// </summary>
1391 public event Action<Key> KeyUp;
1392
1393 /// <summary>
1394 /// Invoked when the user types a character.
1395 /// </summary>
1396 public event Action<char> KeyTyped;
1397
1398 /// <summary>
1399 /// Invoked when a joystick axis changes.
1400 /// </summary>
1401 public event Action<JoystickAxis> JoystickAxisChanged;
1402
1403 /// <summary>
1404 /// Invoked when the user presses a button on a joystick.
1405 /// </summary>
1406 public event Action<JoystickButton> JoystickButtonDown;
1407
1408 /// <summary>
1409 /// Invoked when the user releases a button on a joystick.
1410 /// </summary>
1411 public event Action<JoystickButton> JoystickButtonUp;
1412
1413 /// <summary>
1414 /// Invoked when the user drops a file into the window.
1415 /// </summary>
1416 public event Action<string> DragDrop;
1417
1418 #endregion
1419
1420 public void Dispose()
1421 {
1422 }
1423 }
1424}