using DotSDL.Events; using DotSDL.Input; using DotSDL.Interop.Core; using System; using System.Linq; namespace DotSDL.Graphics { /// /// Represents an SDL window. /// public class SdlWindow : IResourceObject { private readonly SdlInit _sdlInit = SdlInit.Instance; private readonly ResourceManager _resources = ResourceManager.Instance; private readonly IntPtr _window; private readonly IntPtr _renderer; private IntPtr _texture; private bool _hasTexture; public readonly Canvas Background; private bool _running; private uint _nextVideoUpdate; private uint _nextGameUpdate; private ScalingQuality _scalingQuality = ScalingQuality.Nearest; /// true if this instance has been destroyed, othersize false. public bool IsDestroyed { get; set; } /// true if this has been minimized, othersize false. public bool IsMinimized { get; set; } public ResourceType ResourceType => ResourceType.Window; /// Gets the width of this . public int WindowWidth { get; } /// Gets the height of this . public int WindowHeight { get; } /// Gets the width of the rendering target used by this . public int TextureWidth { get; } /// Gets the height of the rendering target used by this . public int TextureHeight { get; } /// The amount of time, in milliseconds, from when the application was started. public uint TicksElapsed => Timer.GetTicks(); /// Gets or sets the amount of time, in milliseconds, between video updates. public uint VideoUpdateTicks { get; set; } /// Gets or sets the amount of time, in milliseconds, between game (logic) updates. public uint GameUpdateTicks { get; set; } /// The list of active objects. public SpriteList Sprites { get; } /// Indicates that the window manager should position the window. To place the window on a specific display, use the function. public const int WindowPosUndefined = 0x1FFF0000; /// Fired when tboolhe window's close button is clicked. public event EventHandler Closed; /// Fired when a key is pressed. public event EventHandler KeyPressed; /// Fired when a key is released. public event EventHandler KeyReleased; /// Fired when the window's minimize button is clicked. public event EventHandler Minimized; /// Fired when the window is restored. public event EventHandler Restored; public ScalingQuality ScalingQuality { get => _scalingQuality; set { _scalingQuality = value; if(_hasTexture) CreateTexture(); } } /// /// Calculates a value that allows the window to be placed on a specific display, with its exact position determined by the window manager. /// /// The index of the display to place the window on. /// A coordinate value that should be passed to the constructor. public static int WindowPosUndefinedDisplay(uint display) { return (int)(WindowPosUndefined | display); } /// /// Indicates that the window should be in the center of the screen. To center the window on a specific display, use the function. /// public const int WindowPosCentered = 0x2FFF0000; /// /// Calculates a value that allows the window to be placed in the center of a specified display. /// /// The index of the display to place the window on. /// A coordinate value that should be passed to the constructor. public static int WindowPosCenteredDisplay(uint display) { return (int)(WindowPosCentered | display); } /// /// Creates a new .readonly /// /// The text that is displayed on the window's title bar. /// A representing the starting position of the window. The X and Y coordinates of the Point can be set to or . /// The width of the window. /// The height of the window. public SdlWindow(string title, Point position, int windowWidth, int windowHeight) : this(title, position, windowWidth, windowHeight, windowWidth, windowHeight, ScalingQuality.Nearest) { } /// /// Creates a new . /// /// The text that is displayed on the window's title bar. /// A representing the starting position of the window. The X and Y coordinates of the Point can be set to or . /// The width of the window. /// The height of the window. /// The scaling (filtering) method to use for the background canvas texture. public SdlWindow(string title, Point position, int windowWidth, int windowHeight, ScalingQuality scalingQuality) : this(title, position, windowWidth, windowHeight, windowWidth, windowHeight, scalingQuality) { } /// /// Creates a new . /// /// The text that is displayed on the window's title bar. /// A representing the starting position of the window. The X and Y coordinates of the Point can be set to or . /// The width of the window. /// The height of the window. /// The width of the window's texture. /// The height of the window's texture. public SdlWindow(string title, Point position, int windowWidth, int windowHeight, int textureWidth, int textureHeight) : this(title, position, windowWidth, windowHeight, textureWidth, textureHeight, ScalingQuality.Nearest) { } /// /// Creates a new . /// /// The text that is displayed on the window's title bar. /// A representing the starting position of the window. The X and Y coordinates of the Point can be set to or . /// The width of the window. /// The height of the window. /// The width of the window's texture. /// The height of the window's texture. /// The scaling (filtering) method to use for the background canvas texture. public SdlWindow(string title, Point position, int windowWidth, int windowHeight, int textureWidth, int textureHeight, ScalingQuality scalingQuality) { _sdlInit.InitSubsystem(Init.SubsystemFlags.Video); _window = Video.CreateWindow(title, position.X, position.Y, windowWidth, windowHeight, Video.WindowFlags.Hidden); _renderer = Render.CreateRenderer(_window, -1, Render.RendererFlags.Accelerated); WindowWidth = windowWidth; WindowHeight = windowHeight; TextureWidth = textureWidth; TextureHeight = textureHeight; ScalingQuality = scalingQuality; CreateTexture(); Background = new Canvas(textureWidth, textureHeight) { Renderer = _renderer }; Background.CreateTexture(); Sprites = new SpriteList(_renderer); IsDestroyed = false; _resources.RegisterResource(this); } /// /// Releases resources used by the instance. /// ~SdlWindow() { DestroyObject(); _resources.UnregisterResource(this); } /// /// Handles calling the user draw function and passing the CLR objects to SDL2. /// private void BaseDraw() { if(IsDestroyed || IsMinimized) return; OnDraw(); Render.SetRenderTarget(_renderer, _texture); // Blit the Canvas to the target texture. Background.UpdateTexture(); unsafe { var canvasClippingRect = Background.Clipping.SdlRect; Render.RenderCopy(_renderer, Background.Texture, new IntPtr(&canvasClippingRect), IntPtr.Zero); } // Plot sprites on top of the background layer. if(Sprites.Count > 0) DrawSprites(); Render.SetRenderTarget(_renderer, IntPtr.Zero); Render.RenderCopy(_renderer, _texture, IntPtr.Zero, IntPtr.Zero); Render.RenderPresent(_renderer); } /// /// Handles setting up the . /// private void BaseLoad() { OnLoad(); // Call the overridden Load function. } /// /// Handles updating the application logic for the . /// private void BaseUpdate() { if(IsDestroyed) return; Events.EventHandler.ProcessEvents(); OnUpdate(); // Call the overridden Update function. } /// /// Creates the rendering target that all of the layers will be drawn to prior to rendering. /// private void CreateTexture() { DestroyTexture(); Hints.SetHint(Hints.RenderScaleQuality, ScalingQuality.ToString()); _texture = Render.CreateTexture(_renderer, Pixels.PixelFormatArgb8888, Render.TextureAccess.Target, TextureWidth, TextureHeight); _hasTexture = true; } /// /// Destroys this . /// public void DestroyObject() { Video.DestroyWindow(_window); IsDestroyed = true; } /// /// Destroys the render target associated with this . /// private void DestroyTexture() { if(!_hasTexture) return; Render.DestroyTexture(_texture); _hasTexture = false; } /// /// Plots the sprites stored in to the screen. Please note that this method is called by /// DotSDL's drawing routines and does not need to be called manually. Additionally, this method will not be /// called if there are no sprites defined. You usually do not need to override this method. /// public virtual void DrawSprites() { Render.SetRenderTarget(_renderer, _texture); foreach(var sprite in Sprites.Where(e => e.Shown).OrderBy(e => e.ZOrder)) { var srcRect = sprite.Clipping.SdlRect; var drawSize = new Point( (int)(srcRect.W * sprite.Scale.X), (int)(srcRect.H * sprite.Scale.Y) ); var destRect = new Rectangle(sprite.Position, drawSize).SdlRect; unsafe { var srcRectPtr = new IntPtr(&srcRect); var destRectPtr = new IntPtr(&destRect); Render.RenderCopyEx( renderer: _renderer, texture: sprite.Texture, srcRect: srcRectPtr, dstRect: destRectPtr, angle: sprite.Rotation, center: sprite.RotationCenter.SdlPoint, flip: sprite.Flip ); } } } /// /// Retrieves the SDL resource ID for this . /// /// public uint GetResourceId() { return Video.GetWindowId(_window); } /// /// Triggers this window to handle a specified . /// /// The to handle. internal void HandleEvent(KeyboardEvent ev) { switch(ev.State) { case ButtonState.Pressed: KeyPressed?.Invoke(this, ev); break; case ButtonState.Released: KeyReleased?.Invoke(this, ev); break; } } /// /// Triggers this window to handle a specified . /// /// The to handle. internal void HandleEvent(WindowEvent ev) { switch(ev.Event) { case WindowEventType.Close: OnClose(ev); break; case WindowEventType.Minimized: OnMinimize(ev); break; case WindowEventType.Restored: OnRestore(ev); break; } } /// /// A game loop that calls the update and draw functions. /// private void Loop() { _running = true; while(_running) { var ticks = TicksElapsed; if(ticks > _nextGameUpdate || GameUpdateTicks == 0) { _nextGameUpdate = ticks + GameUpdateTicks; BaseUpdate(); } if(ticks > _nextVideoUpdate || VideoUpdateTicks == 0) { _nextVideoUpdate = ticks + VideoUpdateTicks; BaseDraw(); } if(VideoUpdateTicks <= 0 && GameUpdateTicks <= 0) continue; // Cook the CPU! var updateTicks = (long)(_nextGameUpdate > _nextVideoUpdate ? _nextVideoUpdate : _nextGameUpdate) - TicksElapsed; if(updateTicks > 0) Timer.Delay((uint)updateTicks); } } /// /// Called when the window's close button is clicked. /// private void OnClose(WindowEvent ev) { if(Closed is null) Stop(); else Closed(this, ev); } /// /// Called every time the window is drawn to. /// protected virtual void OnDraw() { } /// /// Called before the window is shown. /// protected virtual void OnLoad() { } /// /// Called when the window is minimized. /// private void OnMinimize(WindowEvent ev) { IsMinimized = true; Minimized?.Invoke(this, ev); } /// /// Called when the window is restored. /// private void OnRestore(WindowEvent ev) { IsMinimized = false; Restored?.Invoke(this, ev); } /// /// Called every time the application logic update runs. /// protected virtual void OnUpdate() { } /// /// Displays the window and begins executing code that's associated with it. /// public void Start() { Start(0, 0); } /// /// Displays the window and begins executing code that's associated with it. /// /// The desired number of milliseconds between frames and game logic updates. 0 causes the display and game to be continuously updated. public void Start(uint updateRate) { Start(updateRate, updateRate); } /// /// Displays the window and begins executing code that's associated with it. /// /// The desired number of milliseconds between draw calls. 0 causes the display to be continuously updated. /// The desired number of milliseconds between game logic updates. 0 causes the game to be continuously updated. public void Start(uint drawRate, uint updateRate) { VideoUpdateTicks = drawRate; GameUpdateTicks = updateRate; BaseLoad(); Video.ShowWindow(_window); Loop(); } /// /// Stops executing the game loop and destroys the window. /// public void Stop() { _running = false; DestroyObject(); } } }