using DotSDL.Events ;
using DotSDL.Input ;
using DotSDL.Interop.Core ;
using System ;
using System.Collections.Generic ;
using System.Linq ;
namespace DotSDL.Graphics {
/// <summary>
/// Represents an SDL window.
/// </summary>
public class SdlWindow : IResourceObject {
private readonly SdlInit _ sdlInit = SdlInit . Instance ;
private readonly ResourceManager _ resources = ResourceManager . Instance ;
private string _ windowTitle ;
private readonly IntPtr _ window ;
private readonly IntPtr _ renderer ;
private IntPtr _ texture ;
private bool _ hasTexture ;
private bool _ running ;
private uint _ nextVideoUpdate ;
private uint _ nextGameUpdate ;
private ScalingQuality _ scalingQuality = ScalingQuality . Nearest ;
/// <summary>Gets the background layer of this window. This is equivalent to accessing Layers[0].</summary>
public Canvas Background = > Layers [ 0 ] ;
/// <summary>Gets the list of background layers for this window.</summary>
public List < Canvas > Layers { get ; }
/// <summary>
/// Gets the number of background layers for this windows. This number will always be greater
/// than or equal to 1.
/// </summary>
public int LayerCount = > Layers . Count ;
/// <summary><c>true</c> if this <see cref="SdlWindow"/> instance has been destroyed, othersize <c>false</c>.</summary>
public bool IsDestroyed { get ; set ; }
/// <summary><c>true</c> if this <see cref="SdlWindow"/> has been minimized, othersize <c>false</c>.</summary>
public bool IsMinimized { get ; set ; }
public ResourceType ResourceType = > ResourceType . Window ;
/// <summary>Gets the width of this <see cref="SdlWindow"/>.</summary>
public int WindowWidth { get ; }
/// <summary>Gets the height of this <see cref="SdlWindow"/>.</summary>
public int WindowHeight { get ; }
/// <summary>Gets or sets the title of this <see cref="SdlWindow"/>.</summary>
public string WindowTitle {
get = > _ windowTitle ;
set {
_ windowTitle = value ;
Video . SetWindowTitle ( _ window , _ windowTitle ) ;
}
}
/// <summary>Gets the width of the rendering target used by this <see cref="SdlWindow"/>.</summary>
public int RenderWidth { get ; }
/// <summary>Gets the height of the rendering target used by this <see cref="SdlWindow"/>.</summary>
public int RenderHeight { get ; }
/// <summary>The amount of time, in milliseconds, from when the application was started.</summary>
public uint TicksElapsed = > Timer . GetTicks ( ) ;
/// <summary>Gets or sets the amount of time, in milliseconds, between video updates.</summary>
public uint VideoUpdateTicks { get ; set ; }
/// <summary>Gets or sets the amount of time, in milliseconds, between game (logic) updates.</summary>
public uint GameUpdateTicks { get ; set ; }
/// <summary>Gets a <see cref="Rectangle"/> that can be manipulated to modify how much of the scene is displayed.</summary>
public Rectangle CameraView { get ; }
/// <summary>The list of active <see cref="Sprite"/> objects.</summary>
public SpriteList Sprites { get ; }
/// <summary>Indicates that the window manager should position the window. To place the window on a specific display, use the <see cref="WindowPosCenteredDisplay"/> function.</summary>
public const int WindowPosUndefined = 0x1FFF0000 ;
/// <summary>Fired when tboolhe window's close button is clicked.</summary>
public event EventHandler < WindowEvent > Closed ;
/// <summary>Fired when a key is pressed.</summary>
public event EventHandler < KeyboardEvent > KeyPressed ;
/// <summary>Fired when a key is released.</summary>
public event EventHandler < KeyboardEvent > KeyReleased ;
/// <summary>Fired when the window's minimize button is clicked.</summary>
public event EventHandler < WindowEvent > Minimized ;
/// <summary>Fired when the window is restored.</summary>
public event EventHandler < WindowEvent > Restored ;
public ScalingQuality ScalingQuality {
get = > _ scalingQuality ;
set {
_ scalingQuality = value ;
if ( _ hasTexture )
CreateTexture ( ) ;
}
}
/// <summary>
/// Calculates a value that allows the window to be placed on a specific display, with its exact position determined by the window manager.
/// </summary>
/// <param name="display">The index of the display to place the window on.</param>
/// <returns>A coordinate value that should be passed to the <see cref="SdlWindow"/> constructor.</returns>
public static int WindowPosUndefinedDisplay ( uint display ) {
return ( int ) ( WindowPosUndefined | display ) ;
}
/// <summary>
/// Indicates that the window should be in the center of the screen. To center the window on a specific display, use the <see cref="WindowPosCenteredDisplay"/> function.
/// </summary>
public const int WindowPosCentered = 0x2FFF0000 ;
/// <summary>
/// Calculates a value that allows the window to be placed in the center of a specified display.
/// </summary>
/// <param name="display">The index of the display to place the window on.</param>
/// <returns>A coordinate value that should be passed to the <see cref="SdlWindow"/> constructor.</returns>
public static int WindowPosCenteredDisplay ( uint display ) {
return ( int ) ( WindowPosCentered | display ) ;
}
/// <summary>
/// Creates a new <see cref="SdlWindow"/>.readonly
/// </summary>
/// <param name="title">The text that is displayed on the window's title bar.</param>
/// <param name="position">A <see cref="Point"/> representing the starting position of the window. The X and Y coordinates of the Point can be set to <see cref="WindowPosUndefined"/> or <see cref="WindowPosCentered"/>.</param>
/// <param name="windowWidth">The width of the window.</param>
/// <param name="windowHeight">The height of the window.</param>
public SdlWindow ( string title , Point position , int windowWidth , int windowHeight ) : this ( title , position , windowWidth , windowHeight , windowWidth , windowHeight , ScalingQuality . Nearest ) { }
/// <summary>
/// Creates a new <see cref="SdlWindow"/>.
/// </summary>
/// <param name="title">The text that is displayed on the window's title bar.</param>
/// <param name="position">A <see cref="Point"/> representing the starting position of the window. The X and Y coordinates of the Point can be set to <see cref="WindowPosUndefined"/> or <see cref="WindowPosCentered"/>.</param>
/// <param name="windowWidth">The width of the window.</param>
/// <param name="windowHeight">The height of the window.</param>
/// <param name="scalingQuality">The scaling (filtering) method to use for the background canvas texture.</param>
public SdlWindow ( string title , Point position , int windowWidth , int windowHeight , ScalingQuality scalingQuality ) : this ( title , position , windowWidth , windowHeight , windowWidth , windowHeight , scalingQuality ) { }
/// <summary>
/// Creates a new <see cref="SdlWindow"/>.
/// </summary>
/// <param name="title">The text that is displayed on the window's title bar.</param>
/// <param name="position">A <see cref="Point"/> representing the starting position of the window. The X and Y coordinates of the Point can be set to <see cref="WindowPosUndefined"/> or <see cref="WindowPosCentered"/>.</param>
/// <param name="windowWidth">The width of the window.</param>
/// <param name="windowHeight">The height of the window.</param>
/// <param name="renderWidth">The width of the rendering target.</param>
/// <param name="renderHeight">The height of the rendering target.</param>
public SdlWindow ( string title , Point position , int windowWidth , int windowHeight , int renderWidth , int renderHeight ) : this ( title , position , windowWidth , windowHeight , renderWidth , renderHeight , ScalingQuality . Nearest ) { }
/// <summary>
/// Creates a new <see cref="SdlWindow"/>.
/// </summary>
/// <param name="title">The text that is displayed on the window's title bar.</param>
/// <param name="position">A <see cref="Point"/> representing the starting position of the window. The X and Y coordinates of the Point can be set to <see cref="WindowPosUndefined"/> or <see cref="WindowPosCentered"/>.</param>
/// <param name="windowWidth">The width of the window.</param>
/// <param name="windowHeight">The height of the window.</param>
/// <param name="renderWidth">The width of the rendering target.</param>
/// <param name="renderHeight">The height of the rendering target.</param>
/// <param name="scalingQuality">The scaling (filtering) method to use for the background canvas texture.</param>
public SdlWindow ( string title , Point position , int windowWidth , int windowHeight , int renderWidth , int renderHeight , ScalingQuality scalingQuality ) {
_ sdlInit . InitSubsystem ( Init . SubsystemFlags . Video ) ;
_ windowTitle = title ;
_ 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 ;
RenderWidth = renderWidth ;
RenderHeight = renderHeight ;
ScalingQuality = scalingQuality ;
CreateTexture ( ) ;
Layers = new List < Canvas > {
new Background ( renderWidth , renderHeight ) {
Renderer = _ renderer ,
BlendMode = BlendMode . None
}
} ;
Background . CreateTexture ( ) ;
CameraView = new Rectangle (
new Point ( 0 , 0 ) ,
new Point ( WindowWidth , WindowHeight )
) ;
Sprites = new SpriteList ( _ renderer ) ;
IsDestroyed = false ;
_ resources . RegisterResource ( this ) ;
}
/// <summary>
/// Releases resources used by the <see cref="SdlWindow"/> instance.
/// </summary>
~ SdlWindow ( ) {
DestroyObject ( ) ;
_ resources . UnregisterResource ( this ) ;
}
/// <summary>
/// Adds a background layer to the top of the layer stack.
/// </summary>
/// <param name="width">The width of the new layer's texture.</param>
/// <param name="height">The height of the new layer's texture.</param>
/// <param name="blendMode">The blending mode of the new layer.</param>
/// <returns>An integer identifying the new layer.</returns>
public int AddLayer ( int width , int height , BlendMode blendMode = BlendMode . Alpha ) {
var layer = new Background ( width , height ) {
Renderer = _ renderer ,
BlendMode = blendMode
} ;
layer . CreateTexture ( ) ;
Layers . Add ( layer ) ;
return Layers . Count - 1 ;
}
/// <summary>
/// Handles calling the user draw function and passing the CLR objects to SDL2.
/// </summary>
private void BaseDraw ( ) {
if ( IsDestroyed | | IsMinimized ) return ;
OnDraw ( ) ;
Render . SetRenderTarget ( _ renderer , _ texture ) ;
// Blit the background canvases to the target texture.
foreach ( var canvas in Layers ) {
canvas . Clipping . Position = CameraView . Position ;
canvas . Clipping . Size = CameraView . Size ;
canvas . UpdateTexture ( ) ;
unsafe {
var canvasClippingRect = canvas . Clipping . SdlRect ;
Render . RenderCopy ( _ renderer , canvas . 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 ) ;
}
/// <summary>
/// Handles setting up the <see cref="SdlWindow"/>.
/// </summary>
private void BaseLoad ( ) {
OnLoad ( ) ; // Call the overridden Load function.
}
/// <summary>
/// Handles updating the application logic for the <see cref="SdlWindow"/>.
/// </summary>
private void BaseUpdate ( ) {
if ( IsDestroyed ) return ;
Events . EventHandler . ProcessEvents ( ) ;
OnUpdate ( ) ; // Call the overridden Update function.
}
/// <summary>
/// Creates the rendering target that all of the layers will be drawn to prior to rendering.
/// </summary>
private void CreateTexture ( ) {
DestroyTexture ( ) ;
Hints . SetHint ( Hints . RenderScaleQuality , ScalingQuality . ToString ( ) ) ;
_ texture = Render . CreateTexture ( _ renderer , Pixels . PixelFormatArgb8888 , Render . TextureAccess . Target , RenderWidth , RenderHeight ) ;
_ hasTexture = true ;
}
/// <summary>
/// Destroys this <see cref="SdlWindow"/>.
/// </summary>
public void DestroyObject ( ) {
Video . DestroyWindow ( _ window ) ;
IsDestroyed = true ;
}
/// <summary>
/// Destroys the render target associated with this <see cref="SdlWindow"/>.
/// </summary>
private void DestroyTexture ( ) {
if ( ! _ hasTexture ) return ;
Render . DestroyTexture ( _ texture ) ;
_ hasTexture = false ;
}
/// <summary>
/// Plots the sprites stored in <see cref="Sprites"/> 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.
/// </summary>
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 = sprite . DrawSize ;
Rectangle dest ;
if ( sprite . CoordinateSystem = = CoordinateSystem . ScreenSpace ) {
dest = new Rectangle ( sprite . Position , drawSize ) ;
} else {
// Create a set of world coordinates based on the position of the camera
// and this sprite.
var relPosition = new Point ( sprite . Position - CameraView . Position ) ;
var screenPosition = new Point (
( int ) ( ( float ) relPosition . X / CameraView . Size . X * RenderWidth ) ,
( int ) ( ( float ) relPosition . Y / CameraView . Size . Y * RenderHeight )
) ;
var scaleFactorX = ( float ) RenderWidth / CameraView . Size . X ;
var scaleFactorY = ( float ) RenderHeight / CameraView . Size . Y ;
var size = new Point (
( int ) ( drawSize . X * scaleFactorX ) ,
( int ) ( drawSize . Y * scaleFactorY )
) ;
dest = new Rectangle ( screenPosition , size ) ;
}
var destRect = dest . 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
) ;
}
}
}
/// <summary>
/// Retrieves the SDL resource ID for this <see cref="SdlWindow"/>.
/// </summary>
/// <returns></returns>
public uint GetResourceId ( ) {
return Video . GetWindowId ( _ window ) ;
}
/// <summary>
/// Triggers this window to handle a specified <see cref="KeyboardEvent"/>.
/// </summary>
/// <param name="ev">The <see cref="KeyboardEvent"/> to handle.</param>
internal void HandleEvent ( KeyboardEvent ev ) {
switch ( ev . State ) {
case ButtonState . Pressed :
KeyPressed ? . Invoke ( this , ev ) ;
break ;
case ButtonState . Released :
KeyReleased ? . Invoke ( this , ev ) ;
break ;
}
}
/// <summary>
/// Triggers this window to handle a specified <see cref="WindowEvent"/>.
/// </summary>
/// <param name="ev">The <see cref="WindowEvent"/> to handle.</param>
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 ;
}
}
/// <summary>
/// A game loop that calls the <see cref="SdlWindow"/> update and draw functions.
/// </summary>
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 ) ;
}
}
/// <summary>
/// Called when the window's close button is clicked.
/// </summary>
private void OnClose ( WindowEvent ev ) {
if ( Closed is null ) Stop ( ) ;
else Closed ( this , ev ) ;
}
/// <summary>
/// Called every time the window is drawn to.
/// </summary>
protected virtual void OnDraw ( ) { }
/// <summary>
/// Called before the window is shown.
/// </summary>
protected virtual void OnLoad ( ) { }
/// <summary>
/// Called when the window is minimized.
/// </summary>
private void OnMinimize ( WindowEvent ev ) {
IsMinimized = true ;
Minimized ? . Invoke ( this , ev ) ;
}
/// <summary>
/// Called when the window is restored.
/// </summary>
private void OnRestore ( WindowEvent ev ) {
IsMinimized = false ;
Restored ? . Invoke ( this , ev ) ;
}
/// <summary>
/// Called every time the application logic update runs.
/// </summary>
protected virtual void OnUpdate ( ) { }
/// <summary>
/// Removes a layer from the layer stack.
/// </summary>
/// <param name="id">The unique identifier of the layer to remove.</param>
/// <remarks>The background layer (layer 0) cannot be deleted.</remarks>
/// <exception cref="ArgumentOutOfRangeException"><c>id</c> is less than 0.</exception>
/// /// <exception cref="ArgumentOutOfRangeException"><c>id</c> is equal to or greater than <see cref="LayerCount"/>.</exception>
public void RemoveLayer ( int id ) {
if ( id = = 0 ) throw new ArgumentOutOfRangeException ( nameof ( id ) , "The background object (layer 0) cannot be deleted." ) ;
Layers . RemoveAt ( id ) ;
}
/// <summary>
/// Displays the window and begins executing code that's associated with it.
/// </summary>
public void Start ( ) {
Start ( 0 , 0 ) ;
}
/// <summary>
/// Displays the window and begins executing code that's associated with it.
/// </summary>
/// <param name="updateRate">The desired number of milliseconds between frames and game logic updates. 0 causes the display and game to be continuously updated.</param>
public void Start ( uint updateRate ) {
Start ( updateRate , updateRate ) ;
}
/// <summary>
/// Displays the window and begins executing code that's associated with it.
/// </summary>
/// <param name="drawRate">The desired number of milliseconds between draw calls. 0 causes the display to be continuously updated.</param>
/// <param name="updateRate">The desired number of milliseconds between game logic updates. 0 causes the game to be continuously updated.</param>
public void Start ( uint drawRate , uint updateRate ) {
VideoUpdateTicks = drawRate ;
GameUpdateTicks = updateRate ;
BaseLoad ( ) ;
Video . ShowWindow ( _ window ) ;
Loop ( ) ;
}
/// <summary>
/// Stops executing the game loop and destroys the window.
/// </summary>
public void Stop ( ) {
_ running = false ;
DestroyObject ( ) ;
}
}
}