using System; using Engine.Core; using SDL3; namespace Engine.Integration.SDL3; public class Sdl3Window : Behaviour, IWindow, IUpdate, IDisposable, IEnterUniverse, IExitUniverse { public Event OnStateChanged { get; } = new(); public Event OnFocusStateChanged { get; } = new(); public Event OnModeChanged { get; } = new(); public Event OnShowStateChanged { get; } = new(); public Event OnResizeModeChanged { get; } = new(); public Event OnTitleChanged { get; } = new(); public Event OnTargetFpsChanged { get; } = new(); public Event OnSizeChanged { get; } = new(); public Event OnBackgroundColorChanged { get; } = new(); private IntPtr window = IntPtr.Zero; private IntPtr renderer = IntPtr.Zero; private ulong nextRenderTick = 0; private ulong renderInterval = (ulong)(1f / 60f * 1000f); public uint TargetFps { get => field; set { if (value == field) return; uint previousTargetFps = field; field = value; renderInterval = value == 0 ? 0 : (ulong)(1f / value * 1000f); OnTargetFpsChanged.Invoke(this, new(previousTargetFps)); } } = 0; public WindowState State { get => field; set { if (value == field) return; switch (value) { case WindowState.Open: OpenWindow(); break; case WindowState.Closed: default: CloseWindow(); break; } WindowState previousState = field; field = value; OnStateChanged.Invoke(this, new(previousState)); } } = WindowState.Open; public WindowFocusState FocusState { get => field; private set { if (value == field) return; WindowFocusState previousFocusState = field; field = value; OnFocusStateChanged.Invoke(this, new(previousFocusState)); } } = WindowFocusState.Unfocused; public WindowMode Mode { get => field; set { if (value == field) return; if (value is WindowMode.Windowed or WindowMode.BorderlessWindowed) { SDL.SetWindowFullscreen(window, false); SDL.SyncWindow(window); } if (window != IntPtr.Zero) switch (value) { case WindowMode.Windowed: SDL.SetWindowBordered(window, true); break; case WindowMode.BorderlessWindowed: SDL.SetWindowBordered(window, false); break; case WindowMode.BorderlessFullscreen: SDL.SetWindowFullscreen(window, true); break; case WindowMode.Fullscreen: SDL.SetWindowFullscreen(window, true); break; } SDL.SyncWindow(window); WindowMode previousMode = field; field = value; OnModeChanged.Invoke(this, new(previousMode)); } } = WindowMode.Fullscreen; public WindowShowState ShowState { get => field; set { if (value == field) return; if (window != IntPtr.Zero) switch (value) { case WindowShowState.Normal: SDL.RestoreWindow(window); break; case WindowShowState.Minimized: SDL.MinimizeWindow(window); break; case WindowShowState.Maximized: SDL.MaximizeWindow(window); break; } WindowShowState previousShowState = field; field = value; OnShowStateChanged.Invoke(this, new(previousShowState)); } } = WindowShowState.Normal; public WindowResizeMode ResizeMode { get => field; set { if (value == field) return; if (window != IntPtr.Zero) switch (value) { case WindowResizeMode.Fixed: SDL.SetWindowResizable(window, false); break; case WindowResizeMode.Resizable: case WindowResizeMode.AspectLocked: SDL.SetWindowResizable(window, true); break; } WindowResizeMode previousResizeMode = field; field = value; OnResizeModeChanged.Invoke(this, new(previousResizeMode)); } } = WindowResizeMode.Fixed; public ColorRGB BackgroundColor { get => field; set { if (value == field) return; ColorRGB previousBackgroundColor = field; field = value; OnBackgroundColorChanged.Invoke(this, new(previousBackgroundColor)); } } = new ColorRGB(35, 20, 35); public string Title { get => field; set { if (value == field) return; string previousTitle = field; field = value; OnTitleChanged.Invoke(this, new(previousTitle)); } } = nameof(Sdl3Window); public Vector2DInt Size { get => field; set { if (value == field) return; if (value.Max(Vector2DInt.One) == Vector2DInt.One) throw new Exception($"Window size can not be smaller than 1x1"); Vector2DInt previousSize = field; field = value; OnSizeChanged.Invoke(this, new(previousSize)); } } = Vector2DInt.Zero; public void Update() { while (SDL.PollEvent(out SDL.Event e)) ProcessInternalEvents((SDL.EventType)e.Type); ulong currentTick = SDL.GetTicks(); if (currentTick >= nextRenderTick) { nextRenderTick = currentTick + renderInterval; SDL.SetRenderDrawColor(renderer, BackgroundColor.R, BackgroundColor.G, BackgroundColor.B, 255); SDL.RenderClear(renderer); // TODO Render SDL.RenderPresent(renderer); } } public void ExitUniverse(IUniverse universe) => CloseWindow(); public void EnterUniverse(IUniverse universe) { if (State == WindowState.Open) OpenWindow(); } private void OpenWindow() { if (!UniverseObject.IsInUniverse) throw new Exception($"{UniverseObject.Name} is not in a universe. The window can not be opened."); SDL.WindowFlags windowFlags = 0; switch (Mode) { case WindowMode.Windowed: break; case WindowMode.Fullscreen: windowFlags |= SDL.WindowFlags.Fullscreen; break; case WindowMode.BorderlessWindowed: windowFlags |= SDL.WindowFlags.Borderless; break; case WindowMode.BorderlessFullscreen: windowFlags |= SDL.WindowFlags.Fullscreen | SDL.WindowFlags.Borderless; break; } if (!SDL.Init(SDL.InitFlags.Video)) throw new Exception($"SDL_Init failed: {SDL.GetError()}"); if (!SDL.CreateWindowAndRenderer(Title, Size.X, Size.Y, windowFlags, out window, out renderer)) throw new Exception($"SDL_CreateWindowAndRenderer failed: {SDL.GetError()}"); } private void CloseWindow() { if (renderer != IntPtr.Zero) SDL.DestroyRenderer(renderer); if (window != IntPtr.Zero) SDL.DestroyWindow(window); renderer = IntPtr.Zero; window = IntPtr.Zero; } private void ProcessInternalEvents(SDL.EventType eventType) { switch (eventType) { case SDL.EventType.WindowRestored: ShowState = WindowShowState.Normal; break; case SDL.EventType.WindowMaximized: ShowState = WindowShowState.Maximized; break; case SDL.EventType.WindowMinimized: ShowState = WindowShowState.Minimized; break; case SDL.EventType.WindowFocusGained: FocusState = WindowFocusState.Focused; break; case SDL.EventType.WindowFocusLost: FocusState = WindowFocusState.Unfocused; break; case SDL.EventType.Quit: State = WindowState.Closed; BehaviourController.RemoveBehaviour(this); break; } } public void Dispose() { CloseWindow(); // SDL.Quit(); // TODO This possibly prevents multi window GC.SuppressFinalize(this); } }