diff --git a/Spectere.SdlKit.Demo/AppWindow.cs b/Spectere.SdlKit.Demo/AppWindow.cs index e234d20..a724c49 100644 --- a/Spectere.SdlKit.Demo/AppWindow.cs +++ b/Spectere.SdlKit.Demo/AppWindow.cs @@ -114,6 +114,51 @@ public AppWindow(decimal scale) : base( _fontImage.Destination = new SdlRect(0, 0, 40, 50); _fontImage.BlendMode = BlendMode.Alpha; AddRenderable(_fontImage); + + var testConsoleWidth = TargetPixelWidth / 2; + var testConsoleHeight = TargetPixelHeight / 2; + var smileConsole = new SdlKitConsole(this, testConsoleWidth, testConsoleHeight, + "Assets/SpectereFont-8x16.png", 8, 16); + smileConsole.ZOrder = 8; + smileConsole.DefaultGlyph = new Glyph { + GlyphIndex = ' ', + ForegroundColor = new SdlColor(192, 192, 192), + BackgroundColor = new SdlColor(0, 0, 192) + }; + smileConsole.Destination = new SdlRect(0, 150, 200, 150); + smileConsole.PaddingColor = new SdlColor(64, 64, 255); + smileConsole.GlyphPadding = new Padding(1, 0); + smileConsole.ConsolePadding = new Padding(2, 0); + smileConsole.CenterTextArea(); + smileConsole.Clear(); + for(var y = 0; y < smileConsole.ConsoleHeight; y++) + for(var x = 0; x < smileConsole.ConsoleWidth; x++) { + smileConsole.SetCell( + x, y, _rng.Next(1, 3), + new SdlColor((byte)_rng.Next(0, 256), (byte)_rng.Next(0, 256), (byte)_rng.Next(0, 256)), + null + ); + } + AddRenderable(smileConsole); + + var textConsole = new SdlKitConsole(this, testConsoleWidth, testConsoleHeight, + "Assets/SpectereFont-8x16.png", 8, 16); + textConsole.ZOrder = 1000; + textConsole.Destination = new SdlRect(200, 150, 200, 150); + textConsole.CenterTextArea(); + textConsole.Clear(); + textConsole.WriteLine("Line 1..."); + textConsole.WriteLine(" Line 2..."); + textConsole.WriteLine(" Line 3..."); + textConsole.WriteLine(" Line 4..."); + textConsole.WriteLine("Line 5..."); + textConsole.WriteLine(" Line 6..."); + textConsole.WriteLine(" Line 7..."); + textConsole.WriteLine(" Line 8..."); + textConsole.WriteLine("Line 9..."); + textConsole.WriteLine(" Line A..."); + textConsole.Write("lmao \x01\b\x03\rlove"); + AddRenderable(textConsole); } private bool _upPress; diff --git a/Spectere.SdlKit.Demo/Assets/LICENSE.txt b/Spectere.SdlKit.Demo/Assets/LICENSE.txt new file mode 100644 index 0000000..819bb7d --- /dev/null +++ b/Spectere.SdlKit.Demo/Assets/LICENSE.txt @@ -0,0 +1,3 @@ + SpectereFont Bitmap Fonts (c) 2024 by Ian Burgmyer is licensed under CC BY-SA 4.0. + To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/ + diff --git a/Spectere.SdlKit/Interop/Sdl/Render.cs b/Spectere.SdlKit/Interop/Sdl/Render.cs index cd250d8..b8bbe6e 100644 --- a/Spectere.SdlKit/Interop/Sdl/Render.cs +++ b/Spectere.SdlKit/Interop/Sdl/Render.cs @@ -139,6 +139,17 @@ internal static class Render { /// 0 on success, -1 on error. [DllImport(Lib.Sdl2, EntryPoint = "SDL_RenderClear", CallingConvention = CallingConvention.Cdecl)] internal static extern int RenderClear(SdlRenderer renderer); + + /// + /// Copy a portion of the texture to the current rendering target. + /// + /// The rendering context. + /// The source texture. + /// The source rectangle, or null for the entire texture. + /// The destination rectangle, or null For the entire rendering target. + /// 0 on success, or -1 on error. + [DllImport(Lib.Sdl2, EntryPoint = "SDL_RenderCopy", CallingConvention = CallingConvention.Cdecl)] + internal static extern int RenderCopy(SdlRenderer renderer, SdlTexture texture, ref SdlRect srcRect, ref SdlRect dstRect); /// /// Copy a portion of the texture to the current rendering target. @@ -162,6 +173,28 @@ internal static extern int RenderCopyEx( FlipDirection flip ); + /// + /// Fill a rectangle on the current rendering target with the drawing color. + /// + /// The rendering context. + /// The structure representing the rectangle to fill. + /// 0 on success or a negative error code on failure; call for more + /// information. + [DllImport(Lib.Sdl2, EntryPoint = "SDL_RenderFillRect", CallingConvention = CallingConvention.Cdecl)] + internal static extern int RenderFillRect(SdlRenderer renderer, ref SdlRect rect); + + /// + /// Fill some number of rectangles on the current rendering target with the drawing color. + /// + /// The rendering context. + /// An array of structures representing the rectangles to be + /// filled. + /// The number of rectangles. + /// 0 on success or a negative error code on failure; call for more + /// information. + [DllImport(Lib.Sdl2, EntryPoint = "SDL_RenderFillRects", CallingConvention = CallingConvention.Cdecl)] + internal static extern int RenderFillRects(SdlRenderer renderer, SdlRect[] rects, int count); + /// /// Update the screen with the rendering performed. /// @@ -189,6 +222,19 @@ FlipDirection flip [DllImport(Lib.Sdl2, EntryPoint = "SDL_SetTextureColorMod", CallingConvention = CallingConvention.Cdecl)] internal static extern int SetTextureColorMod(SdlTexture texture, byte r, byte g, byte b); + /// + /// Set the color used for drawing operations (rect, line, and clear). + /// + /// The rendering context. + /// The red value used to draw on the rendering target. + /// The green value used to draw on the rendering target. + /// The blue value used to draw on the rendering target. + /// The alpha value used to draw on the rendering target. + /// 0 on success or a negative error code on failure; call for more + /// information. + [DllImport(Lib.Sdl2, EntryPoint = "SDL_SetRenderDrawColor", CallingConvention = CallingConvention.Cdecl)] + internal static extern int SetRenderDrawColor(SdlRenderer renderer, byte r, byte g, byte b, byte a); + /// /// Sets a texture as the current rendering target. /// diff --git a/Spectere.SdlKit/Interop/SdlImage/Image.cs b/Spectere.SdlKit/Interop/SdlImage/Image.cs index f04a84d..dae5f79 100644 --- a/Spectere.SdlKit/Interop/SdlImage/Image.cs +++ b/Spectere.SdlKit/Interop/SdlImage/Image.cs @@ -15,7 +15,7 @@ public class Image { /// /// The to dispose of. [DllImport(Lib.Sdl2Image, EntryPoint = "IMG_FreeAnimation", CallingConvention = CallingConvention.Cdecl)] - internal static extern void FreeAnimation(ref Animation anim); + internal static extern void FreeAnimation(Animation anim); /// /// Initialize SDL_image. This function loads dynamic libraries that SDL_image needs, and prepares them for use. @@ -446,7 +446,7 @@ public class Image { /// A path on the filesystem to load an image from. /// A new , or null on error. [DllImport(Lib.Sdl2Image, EntryPoint = "IMG_LoadTexture", CallingConvention = CallingConvention.Cdecl)] - internal static extern ref SdlTexture LoadTexture(out SdlRenderer renderer, string file); + internal static extern ref SdlTexture LoadTexture(SdlRenderer renderer, string file); /// /// Loads an image from an SDL data source into a GPU texture. @@ -461,7 +461,7 @@ public class Image { /// Non-zero to close/free the SDL RWops before returning, zero to leave it open. /// A new , or null on error. [DllImport(Lib.Sdl2Image, EntryPoint = "IMG_LoadTexture_RW", CallingConvention = CallingConvention.Cdecl)] - internal static extern ref SdlTexture LoadTextureRw(out SdlRenderer renderer, SdlRwOps src, int freeSrc); + internal static extern ref SdlTexture LoadTextureRw(SdlRenderer renderer, SdlRwOps src, int freeSrc); /// /// Loads an image from an SDL data source into a GPU texture. @@ -477,7 +477,7 @@ public class Image { /// A filename extension that represents this data ("BMP", "GIF", "PNG", etc.). /// A new , or null on error. [DllImport(Lib.Sdl2Image, EntryPoint = "IMG_LoadTextureTyped_RW", CallingConvention = CallingConvention.Cdecl)] - internal static extern ref SdlTexture LoadTextureTypedRw(out SdlRenderer renderer, IntPtr src, int freeSrc, string type); + internal static extern ref SdlTexture LoadTextureTypedRw(SdlRenderer renderer, IntPtr src, int freeSrc, string type); /// /// Load an image in the TGA format directly. If you know the format of the image, you can call this function, diff --git a/Spectere.SdlKit/Padding.cs b/Spectere.SdlKit/Padding.cs new file mode 100644 index 0000000..006d2dc --- /dev/null +++ b/Spectere.SdlKit/Padding.cs @@ -0,0 +1,76 @@ +namespace Spectere.SdlKit; + +/// +/// Defines an amount of padding, in pixels. +/// +public struct Padding { + /// + /// The amount of padding on the bottom of an object, in pixels. + /// + public int Bottom; + + /// + /// The amount of padding to the left of an object, in pixels. + /// + public int Left; + + /// + /// The amount of padding to the right of an object, in pixels. + /// + public int Right; + + /// + /// The amount of padding on the top of an object, in pixels. + /// + public int Top; + + public static bool operator ==(Padding left, Padding right) => + left.Left == right.Left + && left.Right == right.Right + && left.Top == right.Top + && left.Bottom == right.Bottom; + + public static bool operator !=(Padding left, Padding right) => !(left == right); + + /// + /// Initializes a new structure. + /// + public Padding() {} + + /// + /// Initializes a new structure. + /// + /// The amount of horizontal padding, in pixels. + /// The amount of vertical padding, in pixels. + public Padding(int horizontal, int vertical) { + Left = Right = horizontal; + Top = Bottom = vertical; + } + + /// + /// Initializes a new structure. + /// + /// The amount of left padding, in pixels. + /// The amount of right padding, in pixels. + /// The amount of top padding, in pixels. + /// The amount of bottom padding, in pixels. + public Padding(int left, int right, int top, int bottom) { + Left = left; + Right = right; + Top = top; + Bottom = bottom; + } + + /// + /// Compares two structures for equality. + /// + /// The that should be compared to this instance. + /// true if the structures are equal, otherwise false. + public bool Equals(Padding other) => this == other; + + /// + public override bool Equals(object? obj) => obj is Padding other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(Bottom, Left, Right, Top); +} diff --git a/Spectere.SdlKit/Renderable.cs b/Spectere.SdlKit/Renderable.cs index e01dfba..bee2df4 100644 --- a/Spectere.SdlKit/Renderable.cs +++ b/Spectere.SdlKit/Renderable.cs @@ -1,4 +1,3 @@ -using SdlHints = Spectere.SdlKit.Interop.Sdl.Support.Hints; using Spectere.SdlKit.Exceptions; using Spectere.SdlKit.Interop.Sdl; using Spectere.SdlKit.Interop.Sdl.Support.Render; @@ -236,7 +235,7 @@ internal Renderable(SdlRenderer renderer, TextureAccess access, int width, int h // Set the scale quality (texture filtering) hint appropriately. TextureFiltering = textureFiltering; - SetTextureFilteringMode(TextureFiltering); + SdlHintHelper.SetTextureFilteringMode(TextureFiltering); // Finally, create the texture. SdlTexture = Render.CreateTexture(SdlRenderer, _pixelFormat, TextureAccess, width, height); @@ -334,19 +333,6 @@ public void Resize(int newWidth, int newHeight) { } } - /// - /// Sets the texture filtering hint. This must be used before a new SDL texture is created. - /// - /// The mode that should be set. - internal static void SetTextureFilteringMode(TextureFiltering textureFiltering) { - _ = Hints.SetHint( - SdlHints.RenderScaleQuality.Name, - textureFiltering == TextureFiltering.Nearest - ? SdlHints.RenderScaleQuality.Nearest - : SdlHints.RenderScaleQuality.Linear - ); - } - /// /// Updates this . This is usually done prior to drawing this object to the screen. /// diff --git a/Spectere.SdlKit/Renderables/Glyph.cs b/Spectere.SdlKit/Renderables/Glyph.cs new file mode 100644 index 0000000..9624656 --- /dev/null +++ b/Spectere.SdlKit/Renderables/Glyph.cs @@ -0,0 +1,21 @@ +namespace Spectere.SdlKit.Renderables; + +/// +/// Represents a single glyph in an . +/// +public struct Glyph { + /// + /// The foreground color of this . + /// + public SdlColor ForegroundColor; + + /// + /// The background color of this . + /// + public SdlColor BackgroundColor; + + /// + /// The character index of this glyph. + /// + public int GlyphIndex; +} diff --git a/Spectere.SdlKit/Renderables/Image.Static.cs b/Spectere.SdlKit/Renderables/Image.Static.cs index ad553c8..e150288 100644 --- a/Spectere.SdlKit/Renderables/Image.Static.cs +++ b/Spectere.SdlKit/Renderables/Image.Static.cs @@ -52,7 +52,7 @@ internal static Image FromFile(SdlRenderer renderer, string path, TextureFilteri Surface.FreeSurface(loadedSurface); // 4. Create a texture from the surface. - SetTextureFilteringMode(textureFiltering); + SdlHintHelper.SetTextureFilteringMode(textureFiltering); var newTexture = Render.CreateTextureFromSurface(renderer, rgba32Surface); if(newTexture.IsNull) { var sdlError = Error.GetError(); diff --git a/Spectere.SdlKit/Renderables/SdlKitConsole.cs b/Spectere.SdlKit/Renderables/SdlKitConsole.cs new file mode 100644 index 0000000..4ac24d2 --- /dev/null +++ b/Spectere.SdlKit/Renderables/SdlKitConsole.cs @@ -0,0 +1,704 @@ +using SdlImage = Spectere.SdlKit.Interop.SdlImage.Image; +using Spectere.SdlKit.Exceptions; +using Spectere.SdlKit.Interop.Sdl; +using Spectere.SdlKit.Interop.Sdl.Support.Render; +using System.Runtime.CompilerServices; + +namespace Spectere.SdlKit.Renderables; + +/// +/// Defines an SDL text console. This cannot be drawn to directly and is instead made up of a set of characters from +/// a given character set. +/// +public class SdlKitConsole : Renderable { + /// + /// The console buffer. + /// + private Glyph[] _consoleBuffer = []; + + /// + /// The current position of the cursor within the console buffer. + /// + private int _cursorPosition; + + /// + /// The number of glyphs present in the loaded font. + /// + private int _fontGlyphCount; + + /// + /// The number of glyphs per row of the font texture. + /// + private int _fontGlyphsPerRow; + + /// + /// The height of each glyph in the loaded font. + /// + private int _fontGlyphHeight; + + /// + /// The width of each glyph in the loaded font. + /// + private int _fontGlyphWidth; + + /// + /// An SDL texture that contains the font glyphs. + /// + private SdlTexture _fontTexture; + + /// + /// The total number of glyphs in the console text area. + /// + private int _glyphTotalCount; + + /// + /// The total height of each glyph, including padding. + /// + private int _glyphTotalHeight; + + /// + /// The total width of each glyph, including padding. + /// + private int _glyphTotalWidth; + + /// + /// The total height of the text area, in pixels. + /// + private int _textAreaHeight; + + /// + /// The total width of the text area, in pixels. + /// + private int _textAreaWidth; + + /// + /// If this is set to true the next call will redraw the console's padding area. + /// + private bool _updatePadding; + + /// + /// If this is set to true, the next call will redraw the entire text area. + /// + private bool _updateTextArea; + + /// + /// Gets or sets the auto-scroll value for this console. If this is set to true, the console will + /// automatically scroll up when the screen is full. This is useful for debug consoles or terminal simulations. + /// If this is set to false the cursor will wrap around when it reaches the end of the buffer. This mode is + /// useful for text-based games. This property defaults to true. + /// + public bool AutoScroll { get; set; } = true; + + /// + /// Gets the height of the console, in number of characters. This value is automatically calculated based on the + /// size of the console (in pixels) and the console and glyph padding. + /// + public int ConsoleHeight { get; private set; } + + /// + /// Gets the width of the console, in number of characters. This value is automatically calculated based on the + /// size of the console (in pixels) and the console and glyph padding. + /// + public int ConsoleWidth { get; private set; } + + /// + /// Gets or sets the padding around the main console. It is highly recommended that you the + /// console before changing this value, as failing to do so can cause existing text to display incorrectly. + /// + public Padding ConsolePadding { + get => _consolePadding; + set { + // Only update if we need to. + if(_consolePadding == value) { + return; + } + + _consolePadding = value; + RecalculateSizesAndBounds(); + } + } + private Padding _consolePadding; + + /// + /// The default glyph for this . All glyphs in this console will be replaced with this + /// whenever the screen is cleared. If is null, the background color for this + /// glyph will be used as the padding color. + /// + public Glyph DefaultGlyph { + get => _defaultGlyph; + set { + // If the padding color is not specified and the background color changes, redraw the padding area. + if(PaddingColor is null && _defaultGlyph.BackgroundColor != value.BackgroundColor) { + _updatePadding = true; + } + + _defaultGlyph = value; + } + } + private Glyph _defaultGlyph = new() { + BackgroundColor = new SdlColor(0, 0, 0), + ForegroundColor = new SdlColor(192, 192, 192), + GlyphIndex = ' ' + }; + + /// + /// Gets or sets the padding around glyphs (individual characters). It is highly recommended that you + /// the console before changing this value, as failing to do so can cause existing text to + /// display incorrectly. + /// + public Padding GlyphPadding { + get => _glyphPadding; + set { + // Only update if we need to. + if(_glyphPadding == value) { + return; + } + + _glyphPadding = value; + RecalculateSizesAndBounds(); + } + } + private Padding _glyphPadding; + + /// + /// Gets or sets the color of the padding region for this . Note that this will not + /// impact the padding between glyphs. + /// + public SdlColor? PaddingColor { + get => _paddingColor; + set { + if(_paddingColor != value) { + _updatePadding = true; + } + + _paddingColor = value; + } + } + private SdlColor? _paddingColor; + + /// + /// Creates a new . + /// + /// The that should be used to create the backing texture. + /// The width of the , in pixels. + /// The height of the , in pixels. + /// The name of the font that should be initially used by this console. + /// The width of each glyph in the font file, in pixels. + /// The height of each glyph in the font file, in pixels. + /// The texture filtering method that this should use. + /// Thrown when SDL is unable to create a texture. + internal SdlKitConsole(SdlRenderer renderer, int width, int height, string fontFilename, int glyphWidth, int glyphHeight, TextureFiltering textureFiltering = TextureFiltering.Nearest) + : base(renderer, TextureAccess.Target, width, height, textureFiltering) { + LoadFont(fontFilename, glyphWidth, glyphHeight); + Clear(); + } + + + /// + /// Creates a new . + /// + /// The whose renderer should be used to create the backing texture. + /// The width of the , in pixels. + /// The height of the , in pixels. + /// The name of the font that should be initially used by this console. + /// The width of each glyph in the font file, in pixels. + /// The height of each glyph in the font file, in pixels. + /// The texture filtering method that this should use. + /// Thrown when SDL is unable to create a texture. + public SdlKitConsole(Window window, int width, int height, string fontFilename, int glyphWidth, int glyphHeight, TextureFiltering textureFiltering = TextureFiltering.Nearest) + : this(window.SdlRenderer, width, height, fontFilename, glyphWidth, glyphHeight, textureFiltering) { } + + /// + /// This will automatically set the padding around the console so that the text is centered. By default, the text + /// area will be positioned in the upper-left corner of the . + /// + public void CenterTextArea() { + var leftPadding = (Width - _textAreaWidth) / 2; + var topPadding = (Height - _textAreaHeight) / 2; + + ConsolePadding = new Padding(leftPadding, 0, topPadding, 0); + } + + /// + /// Clears all glyphs from the console, replacing them with . This also resets the cursor + /// position to (0, 0). + /// + public void Clear() { + Array.Fill(_consoleBuffer, DefaultGlyph); + _updateTextArea = true; + _cursorPosition = 0; + } + + /// + public override void Dispose() { + base.Dispose(); + + if(!_fontTexture.IsNull) { + Render.DestroyTexture(_fontTexture); + } + } + + /// + /// Gets the current position of the cursor. + /// + /// A tuple containing the cursor's X and Y positions, in characters. + public (int x, int y) GetCursorPosition() => (_cursorPosition % ConsoleWidth, _cursorPosition / ConsoleWidth); + + /// + /// Loads a new font, replacing the existing one associated with this . It is highly + /// recommended that you the console before changing this value, as failing to do so can cause + /// existing text to display incorrectly. + /// + /// The name of the font that should be initially used by this console. + /// The width of each glyph in the font file, in pixels. + /// The height of each glyph in the font file, in pixels. + /// Thrown when the file passed in via does + /// not exist. + public void LoadFont(string fontFilename, int glyphWidth, int glyphHeight) { + if(!File.Exists(fontFilename)) { + throw new FileNotFoundException($"Font could not be found on disk: {fontFilename}"); + } + + if(!_fontTexture.IsNull) { + // Destroy the existing texture. + Render.DestroyTexture(_fontTexture); + } + + // Set the scale quality hint appropriately. + SdlHintHelper.SetTextureFilteringMode(TextureFiltering); + + // Load the font as a surface. + var loadedSurface = SdlImage.Load(fontFilename); + if(loadedSurface.IsNull) { + var sdlError = Error.GetError(); + throw new SdlImageLoadException(sdlError); + } + + // Convert the surface to RGBA32. + var rgba32Surface = Surface.ConvertSurfaceFormat(loadedSurface, GetRgba32TextureFormat()); + if(rgba32Surface.IsNull) { + var sdlError = Error.GetError(); + throw new SdlFormatConversionException(sdlError); + } + + // Free the original surface. + Surface.FreeSurface(loadedSurface); + + // Create the hardware accelerated texture. + SdlHintHelper.SetTextureFilteringMode(TextureFiltering); + _fontTexture = Render.CreateTextureFromSurface(SdlRenderer, rgba32Surface); + if(_fontTexture.IsNull) { + var sdlError = Error.GetError(); + throw new SdlTextureInitializationException(sdlError); + } + + // Free the RGBA32 surface. + Surface.FreeSurface(rgba32Surface); + + // Calculate the glyphs per row, total number of glyphs, etc. + _fontGlyphWidth = glyphWidth; + _fontGlyphHeight = glyphHeight; + + _ = Render.QueryTexture(_fontTexture, out _, out _, out var textureWidth, out var textureHeight); + _fontGlyphsPerRow = textureWidth / _fontGlyphWidth; + _fontGlyphCount = textureHeight / _fontGlyphHeight * _fontGlyphsPerRow; + + // Recalculate all computed values. + RecalculateSizesAndBounds(); + } + + /// + /// Recalculates all computed values, such as and . This + /// should be called whenever the font is changed or whenever padding values are changed. This will cause the next + /// call to completely redraw the console. + /// + private void RecalculateSizesAndBounds() { + // Update everything. + _updatePadding = true; + _updateTextArea = true; + + // Calculate total glyph size. + _glyphTotalWidth = _fontGlyphWidth + GlyphPadding.Left + GlyphPadding.Right; + _glyphTotalHeight = _fontGlyphHeight + GlyphPadding.Top + GlyphPadding.Bottom; + + // Calculate total text area. + _textAreaWidth = Width - ConsolePadding.Left - ConsolePadding.Right; + _textAreaHeight = Height - ConsolePadding.Top - ConsolePadding.Bottom; + + // Calculate the number of glyphs that can fit within the text area. + ConsoleWidth = _textAreaWidth / _glyphTotalWidth; + ConsoleHeight = _textAreaHeight / _glyphTotalHeight; + _glyphTotalCount = ConsoleWidth * ConsoleHeight; + + // Readjust the total text area based on the number of glyphs we can actually draw. + _textAreaWidth = ConsoleWidth * _glyphTotalWidth; + _textAreaHeight = ConsoleHeight * _glyphTotalHeight; + + // Only resize the buffer array if necessary. + if(_consoleBuffer.Length == _glyphTotalCount) return; + + var newBuffer = new Glyph[_glyphTotalCount]; + var glyphsToCopy = _glyphTotalCount < _consoleBuffer.Length ? _glyphTotalCount : _consoleBuffer.Length; + Array.Copy(_consoleBuffer, newBuffer, glyphsToCopy); + _consoleBuffer = newBuffer; + + // Pad the remainder of the array (if applicable) with DefaultGlyph. + if(_glyphTotalCount <= glyphsToCopy) return; + + var glyphsToFill = _consoleBuffer.Length - glyphsToCopy - 1; + Array.Fill(_consoleBuffer, DefaultGlyph, glyphsToCopy, glyphsToFill); + } + + /// + /// Sets the cursor to a particular position on the console. + /// + /// An containing the target coordinates. Note that both coordinates + /// are zero-index values. + /// Thrown if the requested coordinates exceed the bounds of the console. + public void SetCursorPosition(SdlPoint position) => SetCursorPosition(position.X, position.Y); + + /// + /// Sets the cursor to a particular position on the console. + /// + /// The X position to set the cursor to. Note that this is a zero-indexed value. + /// The Y position to set the cursor to. Note that this is a zero-indexed value. + /// Thrown if the requested coordinates exceed the bounds of the console. + public void SetCursorPosition(int x, int y) { + if((x < 0 || x >= ConsoleWidth) || (y < 0 || y >= ConsoleHeight)) { + throw new OverflowException("SdlKitConsole: Attempted to set the console cursor to an invalid value. " + + $"Position requested: ({x}, {y}); possible locations: (0, 0)-({ConsoleWidth - 1}, {ConsoleHeight - 1})"); + } + + _cursorPosition = y * ConsoleWidth + x; + } + + /// + /// Scrolls the console. + /// + /// The number of lines to scroll the console. + public void Scroll(int lines = 1) { + if(lines == 0) { + // >:| + return; + } + + if(lines >= ConsoleHeight || lines <= -ConsoleHeight) { + Clear(); + return; + } + + // Perform the copy operation. + var absLines = Math.Abs(lines); + var copyCount = ConsoleWidth * (ConsoleHeight - absLines); + var sourceIndex = lines > 0 ? ConsoleWidth * absLines : 0; + var destinationIndex = lines < 0 ? ConsoleWidth * absLines : 0; + Array.Copy(_consoleBuffer, sourceIndex, _consoleBuffer, destinationIndex, copyCount); + + // Fill in the blank part of the array. + sourceIndex = lines > 0 ? ConsoleWidth * (ConsoleHeight - absLines) : 0; + var fillCount = absLines * ConsoleWidth; + Array.Fill(_consoleBuffer, DefaultGlyph, sourceIndex, fillCount); + } + + /// + /// Sets the given cell to a particular glyph. This method does not move the console cursor. + /// + /// The X coordinate of the cell to set. + /// The Y coordinate of the cell to set. + /// The to set the cell to. + /// Thrown if the requested coordinates exceed the bounds of the console. + public void SetCell(int x, int y, Glyph glyph) => + SetCell(x, y, glyph.GlyphIndex, glyph.ForegroundColor, glyph.BackgroundColor); + + /// + /// Sets the given cell to a particular glyph. This method does not move the console cursor. + /// + /// The X coordinate of the cell to set. + /// The Y coordinate of the cell to set. + /// The index of the character that should be placed in the cell. + /// The color to set the foreground of the cell to. If this is null, the + /// existing value will be used. + /// The color to set the background of the cell to. If this is null, the + /// existing value will be used. + /// Thrown if the requested coordinates exceed the bounds of the console or if + /// too high of a glyph number is being referenced. + public void SetCell(int x, int y, int glyphIndex, SdlColor? foregroundColor, SdlColor? backgroundColor) { + if((x < 0 || x >= ConsoleWidth) || (y < 0 || y >= ConsoleHeight)) { + throw new OverflowException("SdlKitConsole: Attempted to set the console cursor to an invalid value. " + + $"Position requested: ({x}, {y}); possible locations: (0, 0)-({ConsoleWidth - 1}, {ConsoleHeight - 1})"); + } + + if(glyphIndex >= _glyphTotalCount) { + throw new OverflowException($"SdlKitConsole: Attempted to use glyph index {glyphIndex} (maximum " + + $"detected index is {_fontGlyphCount})."); + } + + var index = y * ConsoleWidth + x; + SetCell(index, glyphIndex, foregroundColor, backgroundColor); + } + + /// + /// Sets the given cell to a particular glyph. This method does not move the console cursor. This method also does + /// not perform any bounds checking and is intended for internal use only. + /// + /// The index of the console buffer to update. + /// The index of the character that should be placed in the cell. + /// The color to set the foreground of the cell to. If this is null, the + /// existing value will be used. + /// The color to set the background of the cell to. If this is null, the + /// existing value will be used. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetCell(int index, int glyphIndex, SdlColor? foregroundColor = null, SdlColor? backgroundColor = null) { + _consoleBuffer[index].GlyphIndex = glyphIndex; + + if(foregroundColor is not null) { + _consoleBuffer[index].ForegroundColor = foregroundColor.Value; + } + + if(backgroundColor is not null) { + _consoleBuffer[index].BackgroundColor = backgroundColor.Value; + } + + _updateTextArea = true; + } + + /// + /// Sets the cell at the cursor's position to a particular glyph. This method does not move the console cursor. + /// + /// The to set the cell to. + public void SetCellAtCursor(Glyph glyph) => + SetCellAtCursor(glyph.GlyphIndex, glyph.ForegroundColor, glyph.BackgroundColor); + + /// + /// Sets the cell at the cursor's position to a particular glyph. This method does not move the console cursor. + /// + /// The index of the character that should be placed in the cell. + /// The color to set the foreground of the cell to. If this is null, the + /// existing value will be used. + /// The color to set the background of the cell to. If this is null, the + /// existing value will be used. + public void SetCellAtCursor(int glyphIndex, SdlColor? foregroundColor = null, SdlColor? backgroundColor = null) { + _consoleBuffer[_cursorPosition].GlyphIndex = glyphIndex; + + if(foregroundColor is not null) { + _consoleBuffer[_cursorPosition].ForegroundColor = foregroundColor.Value; + } + + if(backgroundColor is not null) { + _consoleBuffer[_cursorPosition].BackgroundColor = backgroundColor.Value; + } + + _updateTextArea = true; + } + + /// + /// Updates this . + /// + internal override void Update() { + var paddingColor = PaddingColor ?? DefaultGlyph.BackgroundColor; + + if(SdlTexture.IsNull || _fontTexture.IsNull) { + return; + } + + if(_updatePadding || _updateTextArea) { + _ = Render.SetRenderTarget(SdlRenderer, SdlTexture); + } + + if(_updatePadding) { + _updatePadding = false; + var paddingRight = Width - _textAreaWidth - _consolePadding.Left; + var paddingBottom = Height - _textAreaHeight - _consolePadding.Top; + + var rects = new List(); + + // Top + if(_consolePadding.Top > 0) { + rects.Add(new SdlRect(0, 0, Width, _consolePadding.Top)); + } + + // Bottom + if(paddingBottom > 0) { + rects.Add(new SdlRect(0, Height - paddingBottom, Width, paddingBottom)); + } + + // Left + if(_consolePadding.Left > 0) { + rects.Add(new SdlRect(0, 0, _consolePadding.Left, Height)); + } + + // Right + if(paddingRight > 0) { + rects.Add(new SdlRect(Width - paddingRight, 0, paddingRight, Height)); + } + + // If any borders need updated, do it here. + if(rects.Count > 0) { + _ = Render.SetRenderDrawColor(SdlRenderer, paddingColor.R, paddingColor.G, paddingColor.B, paddingColor.A); + _ = Render.RenderFillRects(SdlRenderer, rects.ToArray(), rects.Count); + } + } + + if(_updateTextArea) { + _updateTextArea = false; + + var backgroundRect = new SdlRect( + ConsolePadding.Left, ConsolePadding.Top, + _glyphTotalWidth, _glyphTotalHeight + ); + var fontSourceRect = new SdlRect(0, 0, _fontGlyphWidth, _fontGlyphHeight); + var foregroundRect = new SdlRect( + backgroundRect.X + GlyphPadding.Left, backgroundRect.Y + GlyphPadding.Top, + _fontGlyphWidth, _fontGlyphHeight + ); + + for(var y = 0; y < ConsoleHeight; y++) { + for(var x = 0; x < ConsoleWidth; x++) { + var glyph = _consoleBuffer[y * ConsoleWidth + x]; + + fontSourceRect.X = glyph.GlyphIndex % _fontGlyphsPerRow * _fontGlyphWidth; + fontSourceRect.Y = glyph.GlyphIndex / _fontGlyphsPerRow * _fontGlyphHeight; + + _ = Render.SetRenderDrawColor(SdlRenderer, glyph.BackgroundColor.R, glyph.BackgroundColor.G, glyph.BackgroundColor.B, glyph.BackgroundColor.A); + _ = Render.RenderFillRect(SdlRenderer, ref backgroundRect); + + _ = Render.SetTextureColorMod(_fontTexture, glyph.ForegroundColor.R, glyph.ForegroundColor.G, glyph.ForegroundColor.B); + _ = Render.SetTextureAlphaMod(_fontTexture, glyph.ForegroundColor.A); + _ = Render.RenderCopy(SdlRenderer, _fontTexture, ref fontSourceRect, ref foregroundRect); + + backgroundRect.X += _glyphTotalWidth; + foregroundRect.X += _glyphTotalWidth; + } + + backgroundRect.X = ConsolePadding.Left; + foregroundRect.X = backgroundRect.X + GlyphPadding.Left; + + backgroundRect.Y += _glyphTotalHeight; + foregroundRect.Y += _glyphTotalHeight; + } + } + + if(_updatePadding || _updateTextArea) { + _ = Render.SetRenderDrawColor(SdlRenderer, 0, 0, 0, 255); + } + } + + /// + /// Prints a character to the screen at the current cursor position and advances the cursor or passes a control + /// character to the console. + /// + /// Prints the given character or interprets a control character. The following control + /// characters are supported: + /// + /// + /// 0x08 (\b) - Backspace. Moves the cursor back one position and deletes the + /// previous character. If the cursor is already at the beginning of the line this will do nothing. + /// + /// + /// 0x0A (\n) - Line feed. Advances the cursor to the next line and moves the + /// cursor to the beginning of the line. If the end of the buffer is reached, it will automatically be scrolled + /// up. + /// + /// + /// 0x0D (\b) - Carriage return. Moves the cursor to the beginning of the current + /// line. + /// + /// + /// The foreground color of the written character. If this is set to null, the + /// cell's current color values will be used. If a control character is passed, this will be ignored. + /// The background color of the written character. If this is set to null, the + /// cell's current color values will be used. If a control character is passed, this will be ignored. + /// If this is set to true, control characters will not be parsed. If + /// this is set to false they will be. This defaults to false. + public void Write(int character, SdlColor? foregroundColor = null, SdlColor? backgroundColor = null, bool ignoreControlCharacters = false) { + var (cursorX, cursorY) = GetCursorPosition(); + + if(character is 0x08 or 0x0A or 0x0D && !ignoreControlCharacters) { + // Interpret control character. + switch(character) { + case 0x08: + if(cursorX == 0) return; + SetCursorPosition(--cursorX, cursorY); + SetCellAtCursor(DefaultGlyph); + break; + + case 0x0A: + if(cursorY >= ConsoleHeight - 1) { + // The cursor is at the bottom of the console. Scroll or wrap, depending on settings. + if(AutoScroll) { + Scroll(); + SetCursorPosition(0, cursorY); // Return cursor to the beginning of the line. + } else { + _cursorPosition = 0; // Return cursor to the upper-left. + } + return; + } + + // The cursor is NOT at the bottom of the screen. Yay. + SetCursorPosition(0, ++cursorY); + break; + + case 0x0D: + SetCursorPosition(0, cursorY); + break; + } + + return; + } + + // Print the character and advance the cursor. + SetCellAtCursor(character, foregroundColor, backgroundColor); + + if(++cursorX >= ConsoleWidth) { + // Do we scroll or wrap? + if(AutoScroll) { + Scroll(); + SetCursorPosition(0, ++cursorY); + } else { + _cursorPosition = 0; + } + } else { + ++_cursorPosition; + } + } + + /// + /// Prints a string to the console. + /// + /// The string to print to the console. + /// The foreground color of the written string. If this is set to null, each + /// string character will use the default values of their respective cell. + /// The background color of the written string. If this is set to null, each + /// string character will use the default values of their respective cell. + /// If this is set to true, control characters within + /// will not be parsed. If this is set to false they will be. This defaults to + /// false. + public void Write(string str, SdlColor? foregroundColor = null, SdlColor? backgroundColor = null, bool ignoreControlCharacters = false) { + foreach(var ch in str) { + Write(ch, foregroundColor, backgroundColor, ignoreControlCharacters); + } + } + + /// + /// Prints a character to the screen at the current cursor position and advances the cursor. Note that control + /// characters will never be interpreted with this method. + /// + /// The glyph to print. + public void Write(Glyph glyph) => Write(glyph.GlyphIndex, glyph.ForegroundColor, glyph.BackgroundColor, true); + + /// + /// Prints a string and newline to the console. + /// + /// The string to print to the console. + /// The foreground color of the written string. If this is set to null, each + /// string character will use the default values of their respective cell. + /// The background color of the written string. If this is set to null, each + /// string character will use the default values of their respective cell. + /// If this is set to true, control characters within + /// will not be parsed. If this is set to false they will be. This defaults to + /// false. + public void WriteLine(string str, SdlColor? foregroundColor = null, SdlColor? backgroundColor = null, bool ignoreControlCharacters = false) { + Write(str, foregroundColor, backgroundColor, ignoreControlCharacters); + Write('\n'); + } +} diff --git a/Spectere.SdlKit/SdlColor.cs b/Spectere.SdlKit/SdlColor.cs index a17a925..3961460 100644 --- a/Spectere.SdlKit/SdlColor.cs +++ b/Spectere.SdlKit/SdlColor.cs @@ -59,4 +59,25 @@ public SdlColor(byte red, byte green, byte blue, byte alpha = byte.MaxValue) { B = blue; A = alpha; } + + public static bool operator ==(SdlColor left, SdlColor right) => + left.R == right.R + && left.G == right.G + && left.B == right.B + && left.A == right.A; + + public static bool operator !=(SdlColor left, SdlColor right) => !(left == right); + + /// + /// Compares two structures for equality. + /// + /// The that should be compared to this instance. + /// true if the structures are equal, otherwise false. + public bool Equals(SdlColor other) => this == other; + + /// + public override bool Equals(object? obj) => obj is Padding other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(R, G, B, A); } diff --git a/Spectere.SdlKit/SdlHintHelper.cs b/Spectere.SdlKit/SdlHintHelper.cs new file mode 100644 index 0000000..356007d --- /dev/null +++ b/Spectere.SdlKit/SdlHintHelper.cs @@ -0,0 +1,22 @@ +using SdlHints = Spectere.SdlKit.Interop.Sdl.Support.Hints; +using Spectere.SdlKit.Interop.Sdl; + +namespace Spectere.SdlKit; + +/// +/// Common functions for setting SDL hints. +/// +internal static class SdlHintHelper { + /// + /// Sets the texture filtering hint. This must be used before a new SDL texture is created. + /// + /// The mode that should be set. + internal static void SetTextureFilteringMode(TextureFiltering textureFiltering) { + _ = Hints.SetHint( + SdlHints.RenderScaleQuality.Name, + textureFiltering == TextureFiltering.Nearest + ? SdlHints.RenderScaleQuality.Nearest + : SdlHints.RenderScaleQuality.Linear + ); + } +}