From c042fc90f259d4f93b0ae6dac90a0260377759ad Mon Sep 17 00:00:00 2001 From: yuval-po Date: Fri, 1 May 2026 18:24:29 +0300 Subject: [PATCH 1/5] Add a component --- src/Myra/Graphics2D/UI/Misc/Window.cs | 2 +- src/Myra/Graphics2D/UI/WrapPanel/WrapPanel.cs | 135 +++++++++ .../UI/WrapPanel/WrapPanelLayout.cs | 267 ++++++++++++++++++ 3 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 src/Myra/Graphics2D/UI/WrapPanel/WrapPanel.cs create mode 100644 src/Myra/Graphics2D/UI/WrapPanel/WrapPanelLayout.cs 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..b2237ff5 --- /dev/null +++ b/src/Myra/Graphics2D/UI/WrapPanel/WrapPanel.cs @@ -0,0 +1,135 @@ +using System.ComponentModel; +using Microsoft.Xna.Framework; +using Myra.Graphics2D.UI; +using Container = Myra.Graphics2D.UI.Container; + +namespace Myra.Graphics2D.UI.WrapPanel; + +public class WrapPanel : Container +{ + private readonly WrapPanelLayout _layout = new(); + + [Category("Layout")] + [DefaultValue(Orientation.Horizontal)] + public Orientation Orientation + { + get => _layout.Orientation; + set + { + if (value == _layout.Orientation) + return; + + _layout.Orientation = value; + InvalidateMeasure(); + } + } + + [Category("Layout")] + [DefaultValue(0)] + public int HorizontalSpacing + { + get => _layout.HorizontalSpacing; + set + { + if (value == _layout.HorizontalSpacing) + return; + + _layout.HorizontalSpacing = value; + InvalidateMeasure(); + } + } + + [Category("Layout")] + [DefaultValue(0)] + public int VerticalSpacing + { + get => _layout.VerticalSpacing; + set + { + if (value == _layout.VerticalSpacing) + return; + + _layout.VerticalSpacing = value; + InvalidateMeasure(); + } + } + + [Category("Layout")] + [DefaultValue(true)] + public bool Aligned + { + get => _layout.Aligned; + set + { + if (value == _layout.Aligned) + return; + + _layout.Aligned = value; + InvalidateArrange(); + } + } + + [Category("Layout")] + [DefaultValue(true)] + public bool UniformSizing + { + get => _layout.UniformSizing; + set + { + if (value == _layout.UniformSizing) + return; + + _layout.UniformSizing = value; + InvalidateMeasure(); + } + } + + [Category("Layout")] + [DefaultValue(null)] + public int? PreferredWidth + { + get => _layout.PreferredWidth; + set + { + if (value == _layout.PreferredWidth) + return; + + _layout.PreferredWidth = value; + InvalidateMeasure(); + } + } + + [Category("Layout")] + [DefaultValue(null)] + public int? PreferredHeight + { + get => _layout.PreferredHeight; + set + { + if (value == _layout.PreferredHeight) + return; + + _layout.PreferredHeight = value; + InvalidateMeasure(); + } + } + + public WrapPanel() + { + ChildrenLayout = _layout; + } + + 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..c3eb6e82 --- /dev/null +++ b/src/Myra/Graphics2D/UI/WrapPanel/WrapPanelLayout.cs @@ -0,0 +1,267 @@ +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; + +public class WrapPanelLayout : ILayout +{ + public Orientation Orientation { get; set; } = Orientation.Horizontal; + public int HorizontalSpacing { get; set; } + public int VerticalSpacing { get; set; } + public bool Aligned { get; set; } = true; + public bool UniformSizing { get; set; } = true; + public int? PreferredWidth { get; set; } + public int? PreferredHeight { get; set; } + + 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); + } + + public Point Measure(IEnumerable widgets, Point availableSize) + { + Point result = Point.Zero; + int rowWidth = 0; + int rowHeight = 0; + + Point effectiveAvailableSize = GetEffectiveAvailableSize(availableSize); + Widget[] widgetsArr = widgets.ToArray(); + Point uniformSize = UniformSizing ? GetUniformSize(widgetsArr, effectiveAvailableSize) : Point.Zero; + + if (Orientation == Orientation.Horizontal) + { + bool firstInRow = true; + foreach (Widget widget in widgetsArr) + { + if (!widget.Visible) continue; + + Point size = UniformSizing ? uniformSize : widget.Measure(effectiveAvailableSize); + + if (!firstInRow && effectiveAvailableSize.X > 0 && + rowWidth + HorizontalSpacing + size.X > effectiveAvailableSize.X) + { + result.X = Math.Max(result.X, rowWidth); + result.Y += rowHeight + VerticalSpacing; + rowWidth = size.X; + rowHeight = size.Y; + firstInRow = true; + } + else + { + if (!firstInRow) rowWidth += HorizontalSpacing; + rowWidth += size.X; + rowHeight = Math.Max(rowHeight, size.Y); + firstInRow = false; + } + } + + 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); + } + else + { + bool firstInCol = true; + foreach (Widget widget in widgetsArr) + { + if (!widget.Visible) continue; + + Point size = UniformSizing ? uniformSize : widget.Measure(effectiveAvailableSize); + + if (!firstInCol && effectiveAvailableSize.Y > 0 && + rowHeight + VerticalSpacing + size.Y > effectiveAvailableSize.Y) + { + result.Y = Math.Max(result.Y, rowHeight); + result.X += rowWidth + HorizontalSpacing; + rowHeight = size.Y; + rowWidth = size.X; + firstInCol = true; + } + else + { + if (!firstInCol) rowHeight += VerticalSpacing; + rowHeight += size.Y; + rowWidth = Math.Max(rowWidth, size.X); + firstInCol = false; + } + } + + 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; + } + + private Point GetEffectiveAvailableSize(Point availableSize) + { + Point effectiveAvailableSize = availableSize; + switch (Orientation) + { + case Orientation.Horizontal: + { + 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; + } + + public void Arrange(IEnumerable widgets, Rectangle bounds) + { + if (Orientation == Orientation.Horizontal) + ArrangeHorizontal(widgets, bounds); + else + ArrangeVertical(widgets, bounds); + } + + 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(); + Point uniformSize = UniformSizing ? GetUniformSize(widgets, effectiveAvailableSize) : Point.Zero; + + foreach (Widget widget in widgets) + { + if (!widget.Visible) continue; + + Point size = UniformSizing ? uniformSize : widget.Measure(effectiveAvailableSize); + + 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); + } + + if (rowWidgets.Count > 0) + ArrangeRow(rowWidgets, bounds.X, y, rowHeight, effectiveAvailableSize, uniformSize); + } + + 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(); + Point uniformSize = UniformSizing ? GetUniformSize(widgets, effectiveAvailableSize) : Point.Zero; + + foreach (Widget widget in widgets) + { + if (!widget.Visible) continue; + + Point size = UniformSizing ? uniformSize : widget.Measure(effectiveAvailableSize); + + 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); + } + + if (colWidgets.Count > 0) + ArrangeCol(colWidgets, x, bounds.Y, rowWidth, effectiveAvailableSize, uniformSize); + } + + 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; + } + } + + 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; + } + } +} From 334356de8a4d9a83301d0f2ff21aed54b08cbe8a Mon Sep 17 00:00:00 2001 From: yuval-po Date: Fri, 1 May 2026 22:27:13 +0300 Subject: [PATCH 2/5] Added XML docs (LLM) --- src/Myra/Graphics2D/UI/WrapPanel/WrapPanel.cs | 27 +++++++ .../UI/WrapPanel/WrapPanelLayout.cs | 74 +++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/src/Myra/Graphics2D/UI/WrapPanel/WrapPanel.cs b/src/Myra/Graphics2D/UI/WrapPanel/WrapPanel.cs index b2237ff5..93e120cb 100644 --- a/src/Myra/Graphics2D/UI/WrapPanel/WrapPanel.cs +++ b/src/Myra/Graphics2D/UI/WrapPanel/WrapPanel.cs @@ -5,10 +5,16 @@ 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 @@ -24,6 +30,9 @@ public Orientation Orientation } } + /// + /// Gets or sets the horizontal spacing between child widgets. + /// [Category("Layout")] [DefaultValue(0)] public int HorizontalSpacing @@ -39,6 +48,9 @@ public int HorizontalSpacing } } + /// + /// Gets or sets the vertical spacing between child widgets. + /// [Category("Layout")] [DefaultValue(0)] public int VerticalSpacing @@ -54,6 +66,9 @@ public int VerticalSpacing } } + /// + /// 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 @@ -69,6 +84,9 @@ public bool Aligned } } + /// + /// 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 @@ -84,6 +102,9 @@ public bool UniformSizing } } + /// + /// Gets or sets the preferred width of the wrap panel. + /// [Category("Layout")] [DefaultValue(null)] public int? PreferredWidth @@ -99,6 +120,9 @@ public int? PreferredWidth } } + /// + /// Gets or sets the preferred height of the wrap panel. + /// [Category("Layout")] [DefaultValue(null)] public int? PreferredHeight @@ -114,6 +138,9 @@ public int? PreferredHeight } } + /// + /// Initializes a new instance of the class. + /// public WrapPanel() { ChildrenLayout = _layout; diff --git a/src/Myra/Graphics2D/UI/WrapPanel/WrapPanelLayout.cs b/src/Myra/Graphics2D/UI/WrapPanel/WrapPanelLayout.cs index c3eb6e82..dd796f5c 100644 --- a/src/Myra/Graphics2D/UI/WrapPanel/WrapPanelLayout.cs +++ b/src/Myra/Graphics2D/UI/WrapPanel/WrapPanelLayout.cs @@ -7,16 +7,56 @@ 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. + /// private static Point GetUniformSize(IEnumerable widgets, Point availableSize) { int maxChildWidth = 0; @@ -35,6 +75,9 @@ private static Point GetUniformSize(IEnumerable widgets, Point available return new Point(maxChildWidth, maxChildHeight); } + /// + /// Measures the total size required for the widgets given the available size. + /// public Point Measure(IEnumerable widgets, Point availableSize) { Point result = Point.Zero; @@ -43,6 +86,7 @@ public Point Measure(IEnumerable widgets, Point availableSize) 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; if (Orientation == Orientation.Horizontal) @@ -54,9 +98,11 @@ public Point Measure(IEnumerable widgets, Point availableSize) Point size = UniformSizing ? uniformSize : widget.Measure(effectiveAvailableSize); + // Check if the current widget exceeds the available row width if (!firstInRow && effectiveAvailableSize.X > 0 && rowWidth + HorizontalSpacing + size.X > effectiveAvailableSize.X) { + // Move to the next row result.X = Math.Max(result.X, rowWidth); result.Y += rowHeight + VerticalSpacing; rowWidth = size.X; @@ -65,6 +111,7 @@ public Point Measure(IEnumerable widgets, Point availableSize) } else { + // Add widget to the current row if (!firstInRow) rowWidth += HorizontalSpacing; rowWidth += size.X; rowHeight = Math.Max(rowHeight, size.Y); @@ -72,6 +119,7 @@ public Point Measure(IEnumerable widgets, Point availableSize) } } + // Finalize measurement for the last row result.X = Math.Max(result.X, rowWidth); result.Y += rowHeight; @@ -87,9 +135,11 @@ public Point Measure(IEnumerable widgets, Point availableSize) Point size = UniformSizing ? uniformSize : widget.Measure(effectiveAvailableSize); + // Check if the current widget exceeds the available column height if (!firstInCol && effectiveAvailableSize.Y > 0 && rowHeight + VerticalSpacing + size.Y > effectiveAvailableSize.Y) { + // Move to the next column result.Y = Math.Max(result.Y, rowHeight); result.X += rowWidth + HorizontalSpacing; rowHeight = size.Y; @@ -98,6 +148,7 @@ public Point Measure(IEnumerable widgets, Point availableSize) } else { + // Add widget to the current column if (!firstInCol) rowHeight += VerticalSpacing; rowHeight += size.Y; rowWidth = Math.Max(rowWidth, size.X); @@ -105,6 +156,7 @@ public Point Measure(IEnumerable widgets, Point availableSize) } } + // Finalize measurement for the last column result.Y = Math.Max(result.Y, rowHeight); result.X += rowWidth; @@ -115,6 +167,9 @@ public Point Measure(IEnumerable widgets, Point availableSize) return result; } + /// + /// Gets the effective available size, considering the preferred width/height and orientation. + /// private Point GetEffectiveAvailableSize(Point availableSize) { Point effectiveAvailableSize = availableSize; @@ -141,6 +196,9 @@ private Point GetEffectiveAvailableSize(Point availableSize) return effectiveAvailableSize; } + /// + /// Arranges the widgets within the specified bounds. + /// public void Arrange(IEnumerable widgets, Rectangle bounds) { if (Orientation == Orientation.Horizontal) @@ -149,6 +207,9 @@ public void Arrange(IEnumerable widgets, Rectangle bounds) ArrangeVertical(widgets, bounds); } + /// + /// Arranges widgets horizontally in rows. + /// private void ArrangeHorizontal(IEnumerable widgets, Rectangle bounds) { Point actualAvailableSize = bounds.Size(); @@ -171,6 +232,7 @@ private void ArrangeHorizontal(IEnumerable widgets, Rectangle bounds) 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) { @@ -190,10 +252,14 @@ private void ArrangeHorizontal(IEnumerable widgets, Rectangle bounds) 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. + /// private void ArrangeVertical(IEnumerable widgets, Rectangle bounds) { Point actualAvailableSize = bounds.Size(); @@ -216,6 +282,7 @@ private void ArrangeVertical(IEnumerable widgets, Rectangle bounds) 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) { @@ -235,10 +302,14 @@ private void ArrangeVertical(IEnumerable widgets, Rectangle bounds) 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. + /// private void ArrangeRow(List widgets, int x, int y, int rowHeight, Point availableSize, Point uniformSize) { foreach (Widget widget in widgets) @@ -252,6 +323,9 @@ private void ArrangeRow(List widgets, int x, int y, int rowHeight, Point } } + /// + /// Finalizes the arrangement of a single column. + /// private void ArrangeCol(List widgets, int x, int y, int colWidth, Point availableSize, Point uniformSize) { foreach (Widget widget in widgets) From b0cdced2b0b1862866b39564f0f4f8effac52dfe Mon Sep 17 00:00:00 2001 From: yuval-po Date: Sat, 2 May 2026 02:56:56 +0300 Subject: [PATCH 3/5] Review fix + missing xml docs --- src/Myra/Graphics2D/UI/WrapPanel/WrapPanel.cs | 4 + .../UI/WrapPanel/WrapPanelLayout.cs | 210 ++++++++++++------ 2 files changed, 145 insertions(+), 69 deletions(-) diff --git a/src/Myra/Graphics2D/UI/WrapPanel/WrapPanel.cs b/src/Myra/Graphics2D/UI/WrapPanel/WrapPanel.cs index 93e120cb..bbd358e6 100644 --- a/src/Myra/Graphics2D/UI/WrapPanel/WrapPanel.cs +++ b/src/Myra/Graphics2D/UI/WrapPanel/WrapPanel.cs @@ -146,6 +146,10 @@ 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); diff --git a/src/Myra/Graphics2D/UI/WrapPanel/WrapPanelLayout.cs b/src/Myra/Graphics2D/UI/WrapPanel/WrapPanelLayout.cs index dd796f5c..3e26dcbf 100644 --- a/src/Myra/Graphics2D/UI/WrapPanel/WrapPanelLayout.cs +++ b/src/Myra/Graphics2D/UI/WrapPanel/WrapPanelLayout.cs @@ -57,6 +57,9 @@ public class WrapPanelLayout : ILayout /// /// 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; @@ -78,7 +81,28 @@ private static Point GetUniformSize(IEnumerable widgets, Point available /// /// 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; @@ -86,96 +110,122 @@ public Point Measure(IEnumerable widgets, Point availableSize) 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; - if (Orientation == Orientation.Horizontal) + bool firstInRow = true; + foreach (Widget widget in widgetsArr) { - 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) { - if (!widget.Visible) continue; - - Point size = UniformSizing ? uniformSize : widget.Measure(effectiveAvailableSize); - - // Check if the current widget exceeds the available row width - if (!firstInRow && effectiveAvailableSize.X > 0 && - rowWidth + HorizontalSpacing + size.X > effectiveAvailableSize.X) - { - // Move to the next row - result.X = Math.Max(result.X, rowWidth); - result.Y += rowHeight + VerticalSpacing; - rowWidth = size.X; - rowHeight = size.Y; - firstInRow = true; - } - else - { - // Add widget to the current row - if (!firstInRow) rowWidth += HorizontalSpacing; - rowWidth += size.X; - rowHeight = Math.Max(rowHeight, size.Y); - firstInRow = false; - } + // 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); } - // 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); + firstInRow = false; } - else + + // 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) { - 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) { - if (!widget.Visible) continue; - - Point size = UniformSizing ? uniformSize : widget.Measure(effectiveAvailableSize); - - // Check if the current widget exceeds the available column height - if (!firstInCol && effectiveAvailableSize.Y > 0 && - rowHeight + VerticalSpacing + size.Y > effectiveAvailableSize.Y) - { - // Move to the next column - result.Y = Math.Max(result.Y, rowHeight); - result.X += rowWidth + HorizontalSpacing; - rowHeight = size.Y; - rowWidth = size.X; - firstInCol = true; - } - else - { - // Add widget to the current column - if (!firstInCol) rowHeight += VerticalSpacing; - rowHeight += size.Y; - rowWidth = Math.Max(rowWidth, size.X); - firstInCol = false; - } + // 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); } - // 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); + 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 @@ -199,6 +249,8 @@ private Point GetEffectiveAvailableSize(Point availableSize) /// /// 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) @@ -210,6 +262,8 @@ public void Arrange(IEnumerable widgets, Rectangle 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(); @@ -224,9 +278,11 @@ private void ArrangeHorizontal(IEnumerable widgets, Rectangle bounds) int y = bounds.Y; int rowHeight = 0; var rowWidgets = new List(); - Point uniformSize = UniformSizing ? GetUniformSize(widgets, effectiveAvailableSize) : Point.Zero; - foreach (Widget widget in widgets) + Widget[] widgetsArr = widgets.ToArray(); + Point uniformSize = UniformSizing ? GetUniformSize(widgetsArr, effectiveAvailableSize) : Point.Zero; + + foreach (Widget widget in widgetsArr) { if (!widget.Visible) continue; @@ -260,6 +316,8 @@ private void ArrangeHorizontal(IEnumerable widgets, Rectangle bounds) /// /// 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(); @@ -274,9 +332,11 @@ private void ArrangeVertical(IEnumerable widgets, Rectangle bounds) int y = bounds.Y; int rowWidth = 0; var colWidgets = new List(); - Point uniformSize = UniformSizing ? GetUniformSize(widgets, effectiveAvailableSize) : Point.Zero; - foreach (Widget widget in widgets) + Widget[] widgetsArr = widgets.ToArray(); + Point uniformSize = UniformSizing ? GetUniformSize(widgetsArr, effectiveAvailableSize) : Point.Zero; + + foreach (Widget widget in widgetsArr) { if (!widget.Visible) continue; @@ -310,6 +370,12 @@ private void ArrangeVertical(IEnumerable widgets, Rectangle bounds) /// /// 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) @@ -326,6 +392,12 @@ private void ArrangeRow(List widgets, int x, int y, int rowHeight, Point /// /// 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) @@ -338,4 +410,4 @@ private void ArrangeCol(List widgets, int x, int y, int colWidth, Point y += size.Y + VerticalSpacing; } } -} +} \ No newline at end of file From f25ce31213ea11fdd25acb4a74f2b7afb22a80e4 Mon Sep 17 00:00:00 2001 From: yuval-po Date: Sun, 21 Jun 2026 19:55:35 +0300 Subject: [PATCH 4/5] Add widget info overlay as debug option --- src/Myra/Graphics2D/UI/Desktop.cs | 85 ++++++++++++++++++++++++++++++- src/Myra/MyraEnvironment.cs | 3 ++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/Myra/Graphics2D/UI/Desktop.cs b/src/Myra/Graphics2D/UI/Desktop.cs index 7618ec83..90ae6ecf 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; + foreach (Widget child in ChildrenCopy) + { + Widget w = child.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.Height ?? 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/MyraEnvironment.cs b/src/Myra/MyraEnvironment.cs index da4ac925..73e1330e 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; @@ -229,6 +230,8 @@ public static AssetManager DefaultAssetManager public static bool DrawKeyboardFocusedWidgetFrame { get; set; } public static bool DrawMouseHoveredWidgetFrame { get; set; } public static bool DrawTextGlyphsFrames { get; set; } + public static bool DrawMouseHoveredWidgetInfo { get; set; } + public static SpriteFontBase DefaultDebugFont { get; set; } public static bool DisableClipping { get; set; } public static Func MouseInfoGetter { get; set; } = DefaultMouseInfoGetter; From b1c84323771b234f7208597d5b182a4f6cbf8966 Mon Sep 17 00:00:00 2001 From: yuval-po Date: Sun, 21 Jun 2026 20:08:06 +0300 Subject: [PATCH 5/5] Rabbit fixes --- src/Myra/Graphics2D/UI/Desktop.cs | 10 +++++----- src/Myra/MyraEnvironment.cs | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Myra/Graphics2D/UI/Desktop.cs b/src/Myra/Graphics2D/UI/Desktop.cs index 90ae6ecf..229f09a3 100644 --- a/src/Myra/Graphics2D/UI/Desktop.cs +++ b/src/Myra/Graphics2D/UI/Desktop.cs @@ -608,12 +608,12 @@ private void RenderDebugInfo(RenderContext context) ) 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 + // 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; - foreach (Widget child in ChildrenCopy) + var children = ChildrenCopy; + for (var i = children.Count - 1; i >= 0; --i) { - Widget w = child.HitTest(MousePosition); + var w = children[i].HitTest(MousePosition); if (w != null) { widget = w; @@ -633,7 +633,7 @@ private void RenderDebugInfo(RenderContext context) Text = $""" {GetDebugTypeName(widget.GetType())} eH: {widget.Height ?? transformedPos.Height} - eW: {widget.Height ?? transformedPos.Width} + eW: {widget.Width ?? transformedPos.Width} eX: {transformedPos.X} eY: {transformedPos.Y} """, diff --git a/src/Myra/MyraEnvironment.cs b/src/Myra/MyraEnvironment.cs index 73e1330e..393529ad 100644 --- a/src/Myra/MyraEnvironment.cs +++ b/src/Myra/MyraEnvironment.cs @@ -225,12 +225,25 @@ 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; }