A game framework written with osu! in mind.
at master 1424 lines 52 kB view raw
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}