< Summary

Class:Imagini.AppExitEventArgs
Assembly:Imagini.Core
File(s):/home/razer/vscode-projects/project-grove/imagini/Imagini.Core/App.cs
Covered lines:1
Uncovered lines:0
Coverable lines:1
Total lines:484
Line coverage:100% (1 of 1)
Covered branches:0
Total branches:0

File(s)

/home/razer/vscode-projects/project-grove/imagini/Imagini.Core/App.cs

#LineLine coverage
 1using System;
 2
 3using static SDL2.SDL_events;
 4using static SDL2.SDL_mouse;
 5using static SDL2.SDL_timer;
 6using static SDL2.SDL_render;
 7using static Imagini.ErrorHandler;
 8using System.Diagnostics;
 9using System.Threading;
 10using System.Runtime.CompilerServices;
 11using System.Diagnostics.CodeAnalysis;
 12using System.Drawing;
 13using System.Runtime.InteropServices;
 14using static System.Runtime.InteropServices.RuntimeInformation;
 15using Imagini.Core.Internal;
 16
 17/// <summary>
 18/// The core namespace.
 19/// </summary>
 20namespace Imagini
 21{
 22  /// <summary>
 23  /// Base app class which instantiates a window and event loop.
 24  /// Derive from this if you want to provide your own renderer for the
 25  /// game loop.
 26  /// </summary>
 27  public abstract class AppBase : IDisposable
 28  {
 29    /// <summary>
 30    /// Returns the app's window.
 31    /// </summary>
 32    public Window Window { get; protected set; }
 33    /// <summary>
 34    /// Provides access to the events sent to app's window.
 35    /// </summary>
 36    public Events Events { get; private set; }
 37    private EventManager.EventQueue _eventQueue;
 38
 39    /// <summary>
 40    /// Returns total number of milliseconds since when the library was initialized.
 41    /// </summary>
 42    public static long TotalTime
 43    {
 44      get => SDL_GetTicks();
 45    }
 46
 47    /// <summary>
 48    /// Gets the total app running time.
 49    /// </summary>
 50    public TimeSpan ElapsedAppTime { get; private set; }
 51
 52    /// <summary>
 53    /// Gets or sets if the system mouse cursor should be visible.
 54    /// </summary>
 55    public bool IsMouseVisible
 56    {
 57      get => TryGet(() => SDL_ShowCursor(SDL_QUERY), "SDL_ShowCursor") > 0;
 58      set => TryGet(() => SDL_ShowCursor(value ? 1 : 0), "SDL_ShowCursor");
 59    }
 60
 61    /// <summary>
 62    /// Gets or sets whether the mouse should be captured by the window.
 63    /// If enabled, mouse events will report only relative movement and
 64    /// the cursor should stay fixed (useful for 3D cameras).
 65    /// </summary>
 66    /// <seealso cref="MousePosition" />
 67    public bool CaptureMouse
 68    {
 69      get => TryGet(() => SDL_GetRelativeMouseMode(), "SDL_GetRelativeMouseMode") > 0;
 70      set => Try(() => SDL_SetRelativeMouseMode(value ? 1 : 0), "SDL_SetRelativeMouseMode");
 71    }
 72
 73    /// <summary>
 74    /// Returns the last recorded mouse position.
 75    /// Can be used with <see cref="CaptureMouse" /> to query relative mouse movement.
 76    /// </summary>
 77    /// <seealso cref="CaptureMouse" />
 78    public Point MousePosition
 79    {
 80      get
 81      {
 82        int x = 0, y = 0;
 83        SDL_GetRelativeMouseState(ref x, ref y);
 84        return new Point(x, y);
 85      }
 86    }
 87
 88    /// <summary>
 89    /// Indicates if this app is visible and have input focus.
 90    /// </summary>
 91    public bool IsActive
 92    {
 93      get => Window.Current == Window;
 94    }
 95
 96    /// <summary>
 97    /// Gets or sets the target time between each frame if
 98    /// <see cref="IsFixedTimeStep" /> is set to true.
 99    /// </summary>
 100    /// <remarks>Default is ~16 ms (60 FPS).</remarks>
 101    public TimeSpan TargetElapsedTime
 102    {
 103      get => _targetElapsedTime;
 104      set
 105      {
 106        if (value > MaxElapsedTime)
 107          throw new ArgumentOutOfRangeException("Time must be less or equal to MaxElapsedTime");
 108        if (value <= TimeSpan.Zero)
 109          throw new ArgumentOutOfRangeException("Time must be greater than zero");
 110        if (value > InactiveSleepTime)
 111          throw new ArgumentOutOfRangeException("Time must be less or equal to InactiveSleepTime");
 112        _targetElapsedTime = value;
 113      }
 114    }
 115    private TimeSpan _targetElapsedTime = TimeSpan.FromMilliseconds(16.6667);
 116    private Stopwatch _appStopwatch = new Stopwatch();
 117
 118    /// <summary>
 119    /// Gets or sets the target time between each frame if the window is
 120    /// inactive and <see cref="IsFixedTimeStep" /> is set to true.
 121    /// </summary>
 122    /// <remarks>Default is ~33 ms (30 FPS).</remarks>
 123    public TimeSpan InactiveSleepTime
 124    {
 125      get => _inactiveSleepTime;
 126      set
 127      {
 128        if (value > MaxElapsedTime)
 129          throw new ArgumentOutOfRangeException("Time should be less or equal to MaxElapsedTime");
 130        if (value <= TimeSpan.Zero)
 131          throw new ArgumentOutOfRangeException("Time must be greater than zero");
 132        if (value < TargetElapsedTime)
 133          throw new ArgumentOutOfRangeException("Time should be greater or equal to TargetElapsedTime");
 134        _inactiveSleepTime = value;
 135      }
 136    }
 137    private TimeSpan _inactiveSleepTime = TimeSpan.FromMilliseconds(33.3334);
 138
 139    /// <summary>
 140    /// Gets or sets the maximum amout of time the app will frameskip.
 141    /// </summary>
 142    /// <remarks>Defaults to 500 milliseconds.</remarks>
 143    public TimeSpan MaxElapsedTime
 144    {
 145      get => _maxElapsedTime;
 146      set
 147      {
 148        if (value <= TimeSpan.Zero)
 149          throw new ArgumentOutOfRangeException("Time must be greater than zero");
 150        if (value < TargetElapsedTime)
 151          throw new ArgumentOutOfRangeException("Time must be greater or equal to TargetElapsedTime");
 152        if (value < InactiveSleepTime)
 153          throw new ArgumentOutOfRangeException("Time must be greater or equal to InactiveSleepTime");
 154      }
 155    }
 156    private TimeSpan _maxElapsedTime = TimeSpan.FromMilliseconds(500);
 157    /// <summary>
 158    /// Gets or sets if the time between each frame should be fixed.
 159    /// </summary>
 160    public bool IsFixedTimeStep { get; set; } = true;
 161    /// <summary>
 162    /// Indicates if the last app frame took longer than <see cref="TargetElapsedTime" />.
 163    /// </summary>
 164    public bool IsRunningSlowly { get; private set; }
 165    private int _slowFrameCount = 0;
 166    private const int c_MaxSlowFrameCount = 5;
 167    private long _previousTicks = 0;
 168    private long _accumulatedTicks = 0;
 169
 170
 171    /* -------------------------- Initialization ------------------------ */
 172    /// <summary>
 173    /// Creates a new app with the specified window settings.
 174    /// </summary>
 175    /// <remarks>
 176    /// If you have your own constructor, make sure to call this
 177    /// one because it initializes the window and the event queue.
 178    /// </remarks>
 179    public AppBase(WindowSettings windowSettings)
 180    : this(new Window(windowSettings ?? new WindowSettings()))
 181    {
 182    }
 183
 184    public AppBase(Window window)
 185    {
 186      Window = window;
 187      SetupEvents();
 188    }
 189
 190    protected AppBase()
 191    { }
 192
 193    public void SetupEvents()
 194    {
 195      _eventQueue = EventManager.CreateQueueFor(Window);
 196      Events = new Events(this);
 197      Events.Window.StateChanged += OnWindowStateChange;
 198    }
 199
 200    /// <summary>
 201    /// Called when the app starts. Use it for the initialization logic.
 202    /// </summary>
 203    protected virtual void Initialize() { }
 204
 205
 206    /* --------------------------- App events --------------------------- */
 207    private bool _suppressDraw = false;
 208    private bool _isExiting = false;
 209    internal bool IsExiting => _isExiting;
 210    private bool _isExited = false;
 211    internal bool IsExited => _isExited;
 212    private bool _isInitialized = false;
 213
 214    /// <summary>
 215    /// Fires when the app's window gets activated.
 216    /// </summary>
 217    public event EventHandler<EventArgs> Activated;
 218    /// <summary>
 219    /// Fires when the app's window gets deactivated.
 220    /// </summary>
 221    public event EventHandler<EventArgs> Deactivated;
 222    /// <summary>
 223    /// Fires when the app is disposed.
 224    /// </summary>
 225    public event EventHandler<EventArgs> Disposed;
 226    /// <summary>
 227    /// Fires when the user requests to exit the app. Can be cancelled
 228    /// through <see cref="AppExitEventArgs.Cancel" />.
 229    /// </summary>
 230    public event EventHandler<AppExitEventArgs> Exiting;
 231    /// <summary>
 232    /// Fires when the app's window gets resized.
 233    /// </summary>
 234    public event EventHandler<EventArgs> Resized;
 235
 236    private void ProcessEvents()
 237    {
 238      EventManager.Poll();
 239      _eventQueue.ProcessAll(Events);
 240    }
 241
 242    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 243    private void CheckIfInitialized()
 244    {
 245      if (!_isInitialized)
 246      {
 247        Initialize();
 248        _isInitialized = true;
 249        _appStopwatch.Start();
 250      }
 251    }
 252
 253    private void OnWindowStateChange(object sender, WindowStateChangeEventArgs args)
 254    {
 255      switch (args.State)
 256      {
 257        case WindowStateChange.Shown:
 258        case WindowStateChange.Exposed:
 259        case WindowStateChange.MouseEnter:
 260        case WindowStateChange.FocusGained:
 261          Activated?.Invoke(this, new EventArgs());
 262          break;
 263        case WindowStateChange.Hidden:
 264        case WindowStateChange.MouseLeave:
 265        case WindowStateChange.FocusLost:
 266          Deactivated?.Invoke(this, new EventArgs());
 267          break;
 268        case WindowStateChange.Maximized:
 269        case WindowStateChange.Restored:
 270        case WindowStateChange.SizeChanged:
 271          Resized?.Invoke(this, new EventArgs());
 272          break;
 273        case WindowStateChange.Closed:
 274          RequestExit();
 275          break;
 276      }
 277    }
 278
 279    /// <summary>
 280    /// Requests the app to exit. Can be cancelled by user's code.
 281    /// </summary>
 282    /// <seealso cref="Exiting" />
 283    public void RequestExit() => _isExiting = true;
 284
 285    /// <summary>
 286    /// Cancels the app exit request, if it exists.
 287    /// </summary>
 288    public void CancelExitRequest() => _isExiting = false;
 289
 290    /// <summary>
 291    /// Terminates the app loop and disposes the app.
 292    /// </summary>
 293    public void Terminate() => _isExited = true;
 294
 295    /// <summary>
 296    /// Suppresses the <see cref="Draw" /> call for the next frame.
 297    /// </summary>
 298    public void SuppressDraw() => _suppressDraw = true;
 299
 300    /* ---------------------------- App loop ---------------------------- */
 301    /// <summary>
 302    /// Resets the total elapsed app time.
 303    /// </summary>
 304    public void ResetElapsedTime()
 305    {
 306      ElapsedAppTime = TimeSpan.Zero;
 307      _previousTicks = 0;
 308      _accumulatedTicks = 0;
 309      _appStopwatch.Reset();
 310    }
 311    /// <summary>
 312    /// Runs the app loop.
 313    /// </summary>
 314    public void Run()
 315    {
 316      while (!_isExited)
 317        Tick();
 318    }
 319
 320    /// <summary>
 321    /// Performs a single app loop tick.
 322    /// </summary>
 323    public void Tick()
 324    {
 325      if (_isExited) return;
 326      CheckIfNotDisposed();
 327      CheckIfInitialized();
 328      TimeSpan elapsedFrameTime;
 329
 330    RetryTick:
 331      // Advance the current app time
 332      var currentTicks = _appStopwatch.Elapsed.Ticks;
 333      _appStopwatch.Start();
 334      _accumulatedTicks += (currentTicks - _previousTicks);
 335      _previousTicks = currentTicks;
 336      // If the frame took less time than specified, sleep for the
 337      // remaining time and try again
 338      if (IsFixedTimeStep && _accumulatedTicks < TargetElapsedTime.Ticks)
 339      {
 340        var sleepTime = TimeSpan.FromTicks(TargetElapsedTime.Ticks - _accumulatedTicks).TotalMilliseconds;
 341        if (!IsOSPlatform(OSPlatform.Windows))
 342        {
 343          if (sleepTime >= 2.0) Thread.Sleep(1);
 344        }
 345        else
 346        {
 347          Native.Windows.SleepAtMost(sleepTime);
 348        }
 349        goto RetryTick;
 350      }
 351      // Limit the maximum frameskip time
 352      if (_accumulatedTicks > _maxElapsedTime.Ticks)
 353        _accumulatedTicks = _maxElapsedTime.Ticks;
 354
 355      if (IsFixedTimeStep)
 356      {
 357        elapsedFrameTime = TargetElapsedTime;
 358        var stepCount = 0;
 359        // Perform as many fixed timestep updates as we can
 360        while (_accumulatedTicks >= TargetElapsedTime.Ticks && !_isExited)
 361        {
 362          ElapsedAppTime += TargetElapsedTime;
 363          _accumulatedTicks -= TargetElapsedTime.Ticks;
 364          ++stepCount;
 365          DoUpdate(TargetElapsedTime);
 366        }
 367        // If the frame took more time than specified, then we got
 368        // more than one update step
 369        _slowFrameCount += Math.Max(0, stepCount - 1);
 370        // Check if we should reset the IsRunningSlowly flag
 371        if (IsRunningSlowly)
 372        {
 373          if (_slowFrameCount == 0)
 374            IsRunningSlowly = false;
 375        } // If the flag is not set, check if we should set it
 376        else if (_slowFrameCount > c_MaxSlowFrameCount)
 377          IsRunningSlowly = true;
 378
 379        // If this frame is not too late, decrease the lag counter
 380        if (stepCount == 1 && _slowFrameCount > 0)
 381          _slowFrameCount--;
 382
 383        // Set the target time for the Draw method
 384        elapsedFrameTime = TimeSpan.FromTicks(TargetElapsedTime.Ticks * stepCount);
 385      }
 386      else
 387      {
 388        // Perform a non-fixed timestep update
 389        var accumulated = TimeSpan.FromTicks(_accumulatedTicks);
 390        ElapsedAppTime += accumulated;
 391        elapsedFrameTime = accumulated;
 392        accumulated = TimeSpan.Zero;
 393        DoUpdate(elapsedFrameTime);
 394      }
 395
 396      if (_suppressDraw)
 397        _suppressDraw = false;
 398      else
 399        DoDraw(elapsedFrameTime);
 400
 401      // The user requests us to exit
 402      if (_isExiting)
 403      {
 404        var args = new AppExitEventArgs();
 405        Exiting?.Invoke(this, args);
 406        if (!args.Cancel) Terminate();
 407      }
 408      if (_isExited && !IsDisposed)
 409        Dispose();
 410      _appStopwatch.Stop();
 411    }
 412
 413    private void DoUpdate(TimeSpan frameTime)
 414    {
 415      ProcessEvents();
 416      Update(frameTime);
 417    }
 418
 419    private void DoDraw(TimeSpan frameTime)
 420    {
 421      BeforeDraw(frameTime);
 422      Draw(frameTime);
 423      AfterDraw(frameTime);
 424    }
 425
 426    [ExcludeFromCodeCoverage]
 427    protected virtual void BeforeDraw(TimeSpan frameTime) { }
 428    [ExcludeFromCodeCoverage]
 429    protected virtual void AfterDraw(TimeSpan frameTime) { }
 430
 431    [ExcludeFromCodeCoverage]
 432    protected virtual void Update(TimeSpan frameTime) { }
 433    [ExcludeFromCodeCoverage]
 434    protected virtual void Draw(TimeSpan frameTime) { }
 435
 436    /* ------------------------- Disposing logic ------------------------ */
 437    /// <summary>
 438    /// Called when the app is being disposed (when exiting or by external code).
 439    /// </summary>
 440    [ExcludeFromCodeCoverage]
 441    protected virtual void OnDispose() { }
 442
 443    private bool _isDisposed;
 444    /// <summary>
 445    /// Indicates if this app is disposed.
 446    /// </summary>
 447    public bool IsDisposed => _isDisposed;
 448    /// <summary>
 449    /// Disposes the app object and its dependencies.
 450    /// </summary>
 451    public void Dispose()
 452    {
 453      if (_isDisposed) return;
 454      OnDispose();
 455      Events.Window.StateChanged -= OnWindowStateChange;
 456      EventManager.DeleteQueueFor(Window);
 457      Window.Destroy();
 458
 459      Events = null;
 460      _eventQueue = null;
 461      Window = null;
 462      _isDisposed = true;
 463      Disposed?.Invoke(this, new EventArgs());
 464    }
 465
 466    [ExcludeFromCodeCoverage]
 467    private void CheckIfNotDisposed()
 468    {
 469      if (IsDisposed)
 470        throw new ObjectDisposedException("This app is disposed");
 471    }
 472  }
 473
 474  /// <summary>
 475  /// App exit event args.
 476  /// </summary>
 477  public class AppExitEventArgs : EventArgs
 478  {
 479    /// <summary>
 480    /// Set to true if app should not exit if requested by user.
 481    /// </summary>
 5482    public bool Cancel { get; set; }
 483  }
 484}

Methods/Properties

Cancel()