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
+ );
+ }
+}