diff --git a/src/Myra/Graphics2D/UI/Desktop.cs b/src/Myra/Graphics2D/UI/Desktop.cs
index 7618ec83..229f09a3 100644
--- a/src/Myra/Graphics2D/UI/Desktop.cs
+++ b/src/Myra/Graphics2D/UI/Desktop.cs
@@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
+using System.Linq;
+using FontStashSharp.RichText;
using Myra.Graphics2D.UI.Styles;
using Myra.Utility;
using Myra.Events;
@@ -541,7 +543,11 @@ public void RenderVisual()
widget.Render(_renderContext);
}
}
-
+
+#if DEBUG
+ RenderDebugInfo(_renderContext);
+#endif
+
_renderContext.End();
_renderContext.DeviceScissor = oldDeviceScissor;
@@ -585,6 +591,83 @@ public void Render()
// Render run
RenderVisual();
}
+
+ ///
+ /// Draws debug information on the screen.
+ /// This function can be called after the normal render procedures conclude
+ /// and before the context is closed.
+ ///
+ /// This allows us to render on top of everything else.
+ ///
+ /// The current render context
+ private void RenderDebugInfo(RenderContext context)
+ {
+ if (!MyraEnvironment.DrawMouseHoveredWidgetInfo ||
+ MyraEnvironment.DefaultDebugFont == null ||
+ MousePosition == null
+ )
+ return;
+
+ // Look for the deepest child being hit. That'd be the actual widget we're hovering over as opposed to one of its parents
+ Widget widget = null;
+ var children = ChildrenCopy;
+ for (var i = children.Count - 1; i >= 0; --i)
+ {
+ var w = children[i].HitTest(MousePosition);
+ if (w != null)
+ {
+ widget = w;
+ break;
+ }
+ }
+
+ // Nothing under the cursor, nothing to render
+ if (widget == null)
+ return;
+
+ // Get the widget's current transformed rectangle; That should effectively be the actual on-screen position and size
+ // in the context of the monitor being used (i.e., ignores other monitors)
+ Rectangle transformedPos = widget.Transform.Apply(widget.Bounds);
+ var text = new RichTextLayout
+ {
+ Text = $"""
+ {GetDebugTypeName(widget.GetType())}
+ eH: {widget.Height ?? transformedPos.Height}
+ eW: {widget.Width ?? transformedPos.Width}
+ eX: {transformedPos.X}
+ eY: {transformedPos.Y}
+ """,
+ Font = MyraEnvironment.DefaultDebugFont
+ };
+
+ context.DrawRichText(
+ text,
+ new Vector2(MousePosition.X - 60, MousePosition.Y - 30),
+ Color.Yellow
+ );
+ }
+
+ ///
+ /// Gets a type's 'clean' name, replacing generic type arguments with their actual types
+ ///
+ /// The type to get the name of
+ /// The type's clean display name
+ private static string GetDebugTypeName(Type type)
+ {
+ if (!type.IsGenericType)
+ {
+ return type.Name;
+ }
+
+ var name = type.Name;
+ var tickIndex = name.IndexOf('`');
+ if (tickIndex >= 0)
+ {
+ name = name.Substring(0, tickIndex);
+ }
+
+ return $"{name}<{string.Join(", ", type.GetGenericArguments().Select(GetDebugTypeName))}>";
+ }
private void InvalidateTransform()
{
diff --git a/src/Myra/Graphics2D/UI/Misc/Window.cs b/src/Myra/Graphics2D/UI/Misc/Window.cs
index 92230a2c..b19af9e7 100644
--- a/src/Myra/Graphics2D/UI/Misc/Window.cs
+++ b/src/Myra/Graphics2D/UI/Misc/Window.cs
@@ -24,7 +24,7 @@ namespace Myra.Graphics2D.UI
public class Window : ContentControl
{
private readonly StackPanelLayout _layout = new StackPanelLayout(Orientation.Vertical);
- private readonly Label _titleLabel;
+ protected readonly Label _titleLabel;
private Widget _content;
private Widget _previousKeyboardFocus;
diff --git a/src/Myra/Graphics2D/UI/WrapPanel/WrapPanel.cs b/src/Myra/Graphics2D/UI/WrapPanel/WrapPanel.cs
new file mode 100644
index 00000000..bbd358e6
--- /dev/null
+++ b/src/Myra/Graphics2D/UI/WrapPanel/WrapPanel.cs
@@ -0,0 +1,166 @@
+using System.ComponentModel;
+using Microsoft.Xna.Framework;
+using Myra.Graphics2D.UI;
+using Container = Myra.Graphics2D.UI.Container;
+
+namespace Myra.Graphics2D.UI.WrapPanel;
+
+///
+/// A container that arranges its child widgets in a sequence that wraps at the edge of the container.
+///
+public class WrapPanel : Container
+{
+ private readonly WrapPanelLayout _layout = new();
+
+ ///
+ /// Gets or sets the orientation of the layout (Horizontal or Vertical).
+ ///
+ [Category("Layout")]
+ [DefaultValue(Orientation.Horizontal)]
+ public Orientation Orientation
+ {
+ get => _layout.Orientation;
+ set
+ {
+ if (value == _layout.Orientation)
+ return;
+
+ _layout.Orientation = value;
+ InvalidateMeasure();
+ }
+ }
+
+ ///
+ /// Gets or sets the horizontal spacing between child widgets.
+ ///
+ [Category("Layout")]
+ [DefaultValue(0)]
+ public int HorizontalSpacing
+ {
+ get => _layout.HorizontalSpacing;
+ set
+ {
+ if (value == _layout.HorizontalSpacing)
+ return;
+
+ _layout.HorizontalSpacing = value;
+ InvalidateMeasure();
+ }
+ }
+
+ ///
+ /// Gets or sets the vertical spacing between child widgets.
+ ///
+ [Category("Layout")]
+ [DefaultValue(0)]
+ public int VerticalSpacing
+ {
+ get => _layout.VerticalSpacing;
+ set
+ {
+ if (value == _layout.VerticalSpacing)
+ return;
+
+ _layout.VerticalSpacing = value;
+ InvalidateMeasure();
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether child widgets in a row/column should be aligned to the row height/column width.
+ ///
+ [Category("Layout")]
+ [DefaultValue(true)]
+ public bool Aligned
+ {
+ get => _layout.Aligned;
+ set
+ {
+ if (value == _layout.Aligned)
+ return;
+
+ _layout.Aligned = value;
+ InvalidateArrange();
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether all child widgets should have the same size, based on the largest child.
+ ///
+ [Category("Layout")]
+ [DefaultValue(true)]
+ public bool UniformSizing
+ {
+ get => _layout.UniformSizing;
+ set
+ {
+ if (value == _layout.UniformSizing)
+ return;
+
+ _layout.UniformSizing = value;
+ InvalidateMeasure();
+ }
+ }
+
+ ///
+ /// Gets or sets the preferred width of the wrap panel.
+ ///
+ [Category("Layout")]
+ [DefaultValue(null)]
+ public int? PreferredWidth
+ {
+ get => _layout.PreferredWidth;
+ set
+ {
+ if (value == _layout.PreferredWidth)
+ return;
+
+ _layout.PreferredWidth = value;
+ InvalidateMeasure();
+ }
+ }
+
+ ///
+ /// Gets or sets the preferred height of the wrap panel.
+ ///
+ [Category("Layout")]
+ [DefaultValue(null)]
+ public int? PreferredHeight
+ {
+ get => _layout.PreferredHeight;
+ set
+ {
+ if (value == _layout.PreferredHeight)
+ return;
+
+ _layout.PreferredHeight = value;
+ InvalidateMeasure();
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public WrapPanel()
+ {
+ ChildrenLayout = _layout;
+ }
+
+ ///
+ /// Copies the properties from another widget into this one.
+ ///
+ /// The widget to copy from.
+ protected internal override void CopyFrom(Widget w)
+ {
+ base.CopyFrom(w);
+
+ var wrapPanel = (WrapPanel)w;
+ Orientation = wrapPanel.Orientation;
+ HorizontalSpacing = wrapPanel.HorizontalSpacing;
+ VerticalSpacing = wrapPanel.VerticalSpacing;
+ Aligned = wrapPanel.Aligned;
+ UniformSizing = wrapPanel.UniformSizing;
+ PreferredWidth = wrapPanel.PreferredWidth;
+ PreferredHeight = wrapPanel.PreferredHeight;
+ }
+}
diff --git a/src/Myra/Graphics2D/UI/WrapPanel/WrapPanelLayout.cs b/src/Myra/Graphics2D/UI/WrapPanel/WrapPanelLayout.cs
new file mode 100644
index 00000000..3e26dcbf
--- /dev/null
+++ b/src/Myra/Graphics2D/UI/WrapPanel/WrapPanelLayout.cs
@@ -0,0 +1,413 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Myra.Graphics2D.UI;
+using Myra.Utility;
+
+namespace Myra.Graphics2D.UI.WrapPanel;
+
+///
+/// Implements a layout that arranges widgets in rows or columns, wrapping when the edge is reached.
+///
+public class WrapPanelLayout : ILayout
+{
+ ///
+ /// Gets or sets the orientation of the layout.
+ ///
+ ///
+ /// The orientation determines what axis of space will be used first;
+ /// For , horizontal space will be used first, wrapping
+ /// to a new row when necessary.
+ /// For , vertical space will be used first, wrapping
+ /// to a new column when necessary.
+ ///
+ public Orientation Orientation { get; set; } = Orientation.Horizontal;
+
+ ///
+ /// Gets or sets the horizontal spacing between widgets.
+ ///
+ public int HorizontalSpacing { get; set; }
+
+ ///
+ /// Gets or sets the vertical spacing between widgets.
+ ///
+ public int VerticalSpacing { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether widgets in a row/column should be aligned to the row height/column width.
+ ///
+ public bool Aligned { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether all widgets should have the same size, based on the largest widget.
+ ///
+ public bool UniformSizing { get; set; } = true;
+
+ ///
+ /// Gets or sets the preferred width for measurement and arrangement.
+ ///
+ public int? PreferredWidth { get; set; }
+
+ ///
+ /// Gets or sets the preferred height for measurement and arrangement.
+ ///
+ public int? PreferredHeight { get; set; }
+
+ ///
+ /// Calculates the uniform size for all widgets based on the largest widget's measurements.
+ ///
+ /// The collection of widgets from which to determine the 'max' widget size
+ /// The available size for measurement.
+ /// The uniform size for widgets.
+ private static Point GetUniformSize(IEnumerable widgets, Point availableSize)
+ {
+ int maxChildWidth = 0;
+ int maxChildHeight = 0;
+
+ foreach (Widget widget in widgets)
+ {
+ if (!widget.Visible)
+ continue;
+
+ Point size = widget.Measure(availableSize);
+ maxChildWidth = Math.Max(maxChildWidth, size.X);
+ maxChildHeight = Math.Max(maxChildHeight, size.Y);
+ }
+
+ return new Point(maxChildWidth, maxChildHeight);
+ }
+
+ ///
+ /// Measures the total size required for the widgets given the available size.
+ ///
+ /// The collection of widgets to measure.
+ /// The available size for measurement.
+ /// The required size for the layout.
+ public Point Measure(IEnumerable widgets, Point availableSize)
+ {
+ // The measurement methods are pretty difficult to keep DRY;
+ // We either it somewhat duplicated or we start having to pass around
+ // many parameters.
+ return Orientation == Orientation.Horizontal
+ ? MeasureWithHorizontalBias(widgets, availableSize)
+ : MeasureWithVerticalBias(widgets, availableSize);
+ }
+
+ ///
+ /// Measures the total size required for the widgets given the available size.
+ /// This has a horizontal bias, meaning it'll attempt to use whatever horizontal space is
+ /// available prior to wrapping into a new row
+ ///
+ /// The collection of widgets to measure.
+ /// The available size for measurement.
+ /// The required size for the layout.
+ private Point MeasureWithHorizontalBias(IEnumerable widgets, Point availableSize)
+ {
+ Point result = Point.Zero;
+ int rowWidth = 0;
+ int rowHeight = 0;
+
+ Point effectiveAvailableSize = GetEffectiveAvailableSize(availableSize);
+ Widget[] widgetsArr = widgets.ToArray();
+
+ // Determine the uniform size if uniform sizing is enabled
+ Point uniformSize = UniformSizing ? GetUniformSize(widgetsArr, effectiveAvailableSize) : Point.Zero;
+
+ bool firstInRow = true;
+ foreach (Widget widget in widgetsArr)
+ {
+ if (!widget.Visible)
+ continue;
+
+ Point widgetSize = UniformSizing ? uniformSize : widget.Measure(effectiveAvailableSize);
+
+ // Check if the current widget exceeds the available row width
+ if (!firstInRow && effectiveAvailableSize.X > 0 &&
+ rowWidth + HorizontalSpacing + widgetSize.X > effectiveAvailableSize.X)
+ {
+ // Move to the next row
+ result.X = Math.Max(result.X, rowWidth);
+ result.Y += rowHeight + VerticalSpacing;
+ rowWidth = widgetSize.X;
+ rowHeight = widgetSize.Y;
+ }
+ else
+ {
+ // Add widget to the current row
+ if (!firstInRow)
+ rowWidth += HorizontalSpacing;
+ rowWidth += widgetSize.X;
+ rowHeight = Math.Max(rowHeight, widgetSize.Y);
+ }
+
+ firstInRow = false;
+ }
+
+ // Finalize measurement for the last row
+ result.X = Math.Max(result.X, rowWidth);
+ result.Y += rowHeight;
+
+ if (PreferredWidth.HasValue && availableSize.X is <= 0 or >= 1000000)
+ result.X = Math.Max(result.X, PreferredWidth.Value);
+
+ return result;
+ }
+
+ ///
+ /// Measures the total size required for the widgets given the available size.
+ /// This has a vertical bias, meaning it'll attempt to use whatever vertical space is
+ /// available prior to wrapping into a new column
+ ///
+ /// The collection of widgets to measure.
+ /// The available size for measurement.
+ /// The required size for the layout.
+ private Point MeasureWithVerticalBias(IEnumerable widgets, Point availableSize)
+ {
+ Point result = Point.Zero;
+ int rowWidth = 0;
+ int rowHeight = 0;
+
+ Point effectiveAvailableSize = GetEffectiveAvailableSize(availableSize);
+ Widget[] widgetsArr = widgets.ToArray();
+
+ // Determine the uniform size if uniform sizing is enabled
+ Point uniformSize = UniformSizing ? GetUniformSize(widgetsArr, effectiveAvailableSize) : Point.Zero;
+
+ bool firstInCol = true;
+ foreach (Widget widget in widgetsArr)
+ {
+ if (!widget.Visible)
+ continue;
+
+ Point widgetSize = UniformSizing ? uniformSize : widget.Measure(effectiveAvailableSize);
+
+ // Check if the current widget exceeds the available column height
+ if (!firstInCol && effectiveAvailableSize.Y > 0 &&
+ rowHeight + VerticalSpacing + widgetSize.Y > effectiveAvailableSize.Y)
+ {
+ // Move to the next column
+ result.Y = Math.Max(result.Y, rowHeight);
+ result.X += rowWidth + HorizontalSpacing;
+ rowHeight = widgetSize.Y;
+ rowWidth = widgetSize.X;
+ }
+ else
+ {
+ // Add widget to the current column
+ if (!firstInCol)
+ rowHeight += VerticalSpacing;
+ rowHeight += widgetSize.Y;
+ rowWidth = Math.Max(rowWidth, widgetSize.X);
+ }
+
+ firstInCol = false;
+ }
+
+ // Finalize measurement for the last column
+ result.Y = Math.Max(result.Y, rowHeight);
+ result.X += rowWidth;
+
+ if (PreferredHeight.HasValue && availableSize.Y is <= 0 or >= 1000000)
+ result.Y = Math.Max(result.Y, PreferredHeight.Value);
+
+ return result;
+ }
+
+ ///
+ /// Gets the effective available size, considering the preferred width/height and orientation.
+ ///
+ /// The original available size.
+ /// The effective available size for layout calculations.
+ private Point GetEffectiveAvailableSize(Point availableSize)
+ {
+ Point effectiveAvailableSize = availableSize;
+ switch (Orientation)
+ {
+ case Orientation.Horizontal:
+ default:
+ {
+ if (PreferredWidth.HasValue)
+ effectiveAvailableSize.X = availableSize.X is <= 0 or >= 1000000
+ ? PreferredWidth.Value
+ : Math.Min(availableSize.X, PreferredWidth.Value);
+ break;
+ }
+ case Orientation.Vertical:
+ {
+ if (PreferredHeight.HasValue)
+ effectiveAvailableSize.Y = availableSize.Y is <= 0 or >= 1000000
+ ? PreferredHeight.Value
+ : Math.Min(availableSize.Y, PreferredHeight.Value);
+ break;
+ }
+ }
+
+ return effectiveAvailableSize;
+ }
+
+ ///
+ /// Arranges the widgets within the specified bounds.
+ ///
+ /// The collection of widgets to arrange.
+ /// The bounds to arrange the widgets within.
+ public void Arrange(IEnumerable widgets, Rectangle bounds)
+ {
+ if (Orientation == Orientation.Horizontal)
+ ArrangeHorizontal(widgets, bounds);
+ else
+ ArrangeVertical(widgets, bounds);
+ }
+
+ ///
+ /// Arranges widgets horizontally in rows.
+ ///
+ /// The collection of widgets to arrange.
+ /// The bounds to arrange the widgets within.
+ private void ArrangeHorizontal(IEnumerable widgets, Rectangle bounds)
+ {
+ Point actualAvailableSize = bounds.Size();
+ Point effectiveAvailableSize = actualAvailableSize;
+
+ if (PreferredWidth.HasValue)
+ effectiveAvailableSize.X = actualAvailableSize.X is <= 0 or >= 1000000
+ ? PreferredWidth.Value
+ : Math.Min(actualAvailableSize.X, PreferredWidth.Value);
+
+ int x = bounds.X;
+ int y = bounds.Y;
+ int rowHeight = 0;
+ var rowWidgets = new List();
+
+ Widget[] widgetsArr = widgets.ToArray();
+ Point uniformSize = UniformSizing ? GetUniformSize(widgetsArr, effectiveAvailableSize) : Point.Zero;
+
+ foreach (Widget widget in widgetsArr)
+ {
+ if (!widget.Visible) continue;
+
+ Point size = UniformSizing ? uniformSize : widget.Measure(effectiveAvailableSize);
+
+ // Wrap to next row if current widget exceeds horizontal bounds
+ if (rowWidgets.Count > 0 && effectiveAvailableSize.X > 0 &&
+ x + HorizontalSpacing + size.X > bounds.X + effectiveAvailableSize.X)
+ {
+ // Arrange previous row
+ ArrangeRow(rowWidgets, bounds.X, y, rowHeight, effectiveAvailableSize, uniformSize);
+
+ y += rowHeight + VerticalSpacing;
+ x = bounds.X;
+ rowHeight = 0;
+ rowWidgets.Clear();
+ }
+
+ if (rowWidgets.Count > 0) x += HorizontalSpacing;
+
+ rowWidgets.Add(widget);
+ x += size.X;
+ rowHeight = Math.Max(rowHeight, size.Y);
+ }
+
+ // Arrange the last remaining row
+ if (rowWidgets.Count > 0)
+ ArrangeRow(rowWidgets, bounds.X, y, rowHeight, effectiveAvailableSize, uniformSize);
+ }
+
+ ///
+ /// Arranges widgets vertically in columns.
+ ///
+ /// The collection of widgets to arrange.
+ /// The bounds to arrange the widgets within.
+ private void ArrangeVertical(IEnumerable widgets, Rectangle bounds)
+ {
+ Point actualAvailableSize = bounds.Size();
+ Point effectiveAvailableSize = actualAvailableSize;
+
+ if (PreferredHeight.HasValue)
+ effectiveAvailableSize.Y = actualAvailableSize.Y is <= 0 or >= 1000000
+ ? PreferredHeight.Value
+ : Math.Min(actualAvailableSize.Y, PreferredHeight.Value);
+
+ int x = bounds.X;
+ int y = bounds.Y;
+ int rowWidth = 0;
+ var colWidgets = new List();
+
+ Widget[] widgetsArr = widgets.ToArray();
+ Point uniformSize = UniformSizing ? GetUniformSize(widgetsArr, effectiveAvailableSize) : Point.Zero;
+
+ foreach (Widget widget in widgetsArr)
+ {
+ if (!widget.Visible) continue;
+
+ Point size = UniformSizing ? uniformSize : widget.Measure(effectiveAvailableSize);
+
+ // Wrap to next column if current widget exceeds vertical bounds
+ if (colWidgets.Count > 0 && effectiveAvailableSize.Y > 0 &&
+ y + VerticalSpacing + size.Y > bounds.Y + effectiveAvailableSize.Y)
+ {
+ // Arrange previous column
+ ArrangeCol(colWidgets, x, bounds.Y, rowWidth, effectiveAvailableSize, uniformSize);
+
+ x += rowWidth + HorizontalSpacing;
+ y = bounds.Y;
+ rowWidth = 0;
+ colWidgets.Clear();
+ }
+
+ if (colWidgets.Count > 0) y += VerticalSpacing;
+
+ colWidgets.Add(widget);
+ y += size.Y;
+ rowWidth = Math.Max(rowWidth, size.X);
+ }
+
+ // Arrange the last remaining column
+ if (colWidgets.Count > 0)
+ ArrangeCol(colWidgets, x, bounds.Y, rowWidth, effectiveAvailableSize, uniformSize);
+ }
+
+ ///
+ /// Finalizes the arrangement of a single row.
+ ///
+ /// The widgets in the row.
+ /// The starting X coordinate.
+ /// The starting Y coordinate.
+ /// The height of the row.
+ /// The total available size for measurement.
+ /// The uniform size for widgets, if enabled.
+ private void ArrangeRow(List widgets, int x, int y, int rowHeight, Point availableSize, Point uniformSize)
+ {
+ foreach (Widget widget in widgets)
+ {
+ Point size = UniformSizing ? uniformSize : widget.Measure(availableSize);
+ int height = Aligned ? rowHeight : size.Y;
+ var widgetBounds = new Rectangle(x, y, size.X, height);
+ widget.Arrange(widgetBounds);
+
+ x += size.X + HorizontalSpacing;
+ }
+ }
+
+ ///
+ /// Finalizes the arrangement of a single column.
+ ///
+ /// The widgets in the column.
+ /// The starting X coordinate.
+ /// The starting Y coordinate.
+ /// The width of the column.
+ /// The total available size for measurement.
+ /// The uniform size for widgets, if enabled.
+ private void ArrangeCol(List widgets, int x, int y, int colWidth, Point availableSize, Point uniformSize)
+ {
+ foreach (Widget widget in widgets)
+ {
+ Point size = UniformSizing ? uniformSize : widget.Measure(availableSize);
+ int width = Aligned ? colWidth : size.X;
+ var widgetBounds = new Rectangle(x, y, width, size.Y);
+ widget.Arrange(widgetBounds);
+
+ y += size.Y + VerticalSpacing;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Myra/MyraEnvironment.cs b/src/Myra/MyraEnvironment.cs
index da4ac925..393529ad 100644
--- a/src/Myra/MyraEnvironment.cs
+++ b/src/Myra/MyraEnvironment.cs
@@ -5,6 +5,7 @@
using AssetManagementBase;
using Myra.Graphics2D.UI;
using System.Collections.Generic;
+using FontStashSharp;
#if MONOGAME || FNA
using Microsoft.Xna.Framework;
@@ -224,11 +225,26 @@ public static AssetManager DefaultAssetManager
_defaultAssetManager = value;
}
}
-
+
+ ///
+ /// Highlights the border of all widgets
+ ///
public static bool DrawWidgetsFrames { get; set; }
public static bool DrawKeyboardFocusedWidgetFrame { get; set; }
+ ///
+ /// Highlights the borders of the widget hierarchy currently being hovered over
+ ///
public static bool DrawMouseHoveredWidgetFrame { get; set; }
public static bool DrawTextGlyphsFrames { get; set; }
+ ///
+ /// Draws information about the widget under the mouse cursor
+ ///
+ public static bool DrawMouseHoveredWidgetInfo { get; set; }
+ ///
+ /// The default font to use when rendering text in overlays.
+ /// If unset, some debug overlays may not work.
+ ///
+ public static SpriteFontBase DefaultDebugFont { get; set; }
public static bool DisableClipping { get; set; }
public static Func MouseInfoGetter { get; set; } = DefaultMouseInfoGetter;