summaryrefslogtreecommitdiff
path: root/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls
diff options
context:
space:
mode:
Diffstat (limited to 'Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls')
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Box.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Button.cs92
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Canvas.cs25
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CheckBox.cs65
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ComboBox.cs102
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CompositeControl.cs70
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ContentControl.cs82
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Control.cs271
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ControlCollection.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Dialog.cs41
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/DockPanel.cs104
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Form.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ItemsControl.cs60
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Label.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/LayoutControl.cs40
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ListBox.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ProgressBar.cs62
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/SelectorControl.cs173
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/StackPanel.cs69
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox.cs331
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox2.cs328
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ToggleButton.cs98
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/UniformGrid.cs86
23 files changed, 2236 insertions, 0 deletions
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Box.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Box.cs
new file mode 100644
index 0000000..1e5c06e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Box.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class Box : Control
+ {
+ public override IEnumerable<Control> Children { get; } = Enumerable.Empty<Control>();
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ return new Size(Width, Height);
+ }
+
+ public Color FillColor { get; set; } = Color.White;
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+ renderer.FillRectangle(ContentRectangle, FillColor);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Button.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Button.cs
new file mode 100644
index 0000000..310dc00
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Button.cs
@@ -0,0 +1,92 @@
+using System;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class Button : ContentControl
+ {
+ public Button()
+ {
+ }
+
+ public event EventHandler Clicked;
+ public event EventHandler PressedStateChanged;
+
+ private bool _isPressed;
+ public bool IsPressed
+ {
+ get => _isPressed;
+ set
+ {
+ if (_isPressed != value)
+ {
+ _isPressed = value;
+ PressedStyle?.ApplyIf(this, _isPressed);
+ PressedStateChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ private ControlStyle _pressedStyle;
+ public ControlStyle PressedStyle
+ {
+ get => _pressedStyle;
+ set
+ {
+ if (_pressedStyle != value)
+ {
+ _pressedStyle = value;
+ PressedStyle?.ApplyIf(this, _isPressed);
+ }
+ }
+ }
+
+ private bool _isPointerDown;
+
+ public override bool OnPointerDown(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsEnabled)
+ {
+ _isPointerDown = true;
+ IsPressed = true;
+ }
+
+ return base.OnPointerDown(context, args);
+ }
+
+ public override bool OnPointerUp(IGuiContext context, PointerEventArgs args)
+ {
+ _isPointerDown = false;
+
+ if (IsPressed)
+ {
+ IsPressed = false;
+
+ if (BoundingRectangle.Contains(args.Position) && IsEnabled)
+ Click();
+ }
+
+ return base.OnPointerUp(context, args);
+ }
+
+ public override bool OnPointerEnter(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsEnabled && _isPointerDown)
+ IsPressed = true;
+
+ return base.OnPointerEnter(context, args);
+ }
+
+ public override bool OnPointerLeave(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsEnabled)
+ IsPressed = false;
+
+ return base.OnPointerLeave(context, args);
+ }
+
+ public void Click()
+ {
+ Clicked?.Invoke(this, EventArgs.Empty);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Canvas.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Canvas.cs
new file mode 100644
index 0000000..d667a73
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Canvas.cs
@@ -0,0 +1,25 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class Canvas : LayoutControl
+ {
+ public Canvas()
+ {
+ }
+
+ protected override void Layout(IGuiContext context, Rectangle rectangle)
+ {
+ foreach (var control in Items)
+ {
+ var actualSize = control.CalculateActualSize(context);
+ PlaceControl(context, control, control.Position.X, control.Position.Y, actualSize.Width, actualSize.Height);
+ }
+ }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ return new Size();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CheckBox.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CheckBox.cs
new file mode 100644
index 0000000..c6c375f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CheckBox.cs
@@ -0,0 +1,65 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class CheckBox : CompositeControl
+ {
+ public CheckBox()
+ {
+ _contentLabel = new Label();
+ _checkLabel = new Box {Width = 20, Height = 20};
+
+ _toggleButton = new ToggleButton
+ {
+ Margin = 0,
+ Padding = 0,
+ BackgroundColor = Color.Transparent,
+ BorderThickness = 0,
+ HoverStyle = null,
+ CheckedStyle = null,
+ PressedStyle = null,
+ Content = new StackPanel
+ {
+ Margin = 0,
+ Orientation = Orientation.Horizontal,
+ Items =
+ {
+ _checkLabel,
+ _contentLabel
+ }
+ }
+ };
+
+ _toggleButton.CheckedStateChanged += (sender, args) => OnIsCheckedChanged();
+ Template = _toggleButton;
+ OnIsCheckedChanged();
+ }
+
+ private readonly Label _contentLabel;
+ private readonly ToggleButton _toggleButton;
+ private readonly Box _checkLabel;
+
+ protected override Control Template { get; }
+
+ public override object Content
+ {
+ get => _contentLabel.Content;
+ set => _contentLabel.Content = value;
+ }
+
+ public bool IsChecked
+ {
+ get => _toggleButton.IsChecked;
+ set
+ {
+ _toggleButton.IsChecked = value;
+ OnIsCheckedChanged();
+ }
+ }
+
+ private void OnIsCheckedChanged()
+ {
+ _checkLabel.FillColor = IsChecked ? Color.White : Color.Transparent;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ComboBox.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ComboBox.cs
new file mode 100644
index 0000000..f8867d2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ComboBox.cs
@@ -0,0 +1,102 @@
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGame.Extended.Input.InputListeners;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class ComboBox : SelectorControl
+ {
+ public ComboBox()
+ {
+ }
+
+ public bool IsOpen { get; set; }
+ public TextureRegion2D DropDownRegion { get; set; }
+ public Color DropDownColor { get; set; } = Color.White;
+
+ public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args)
+ {
+ if (args.Key == Keys.Enter)
+ IsOpen = false;
+
+ return base.OnKeyPressed(context, args);
+ }
+
+ public override bool OnPointerUp(IGuiContext context, PointerEventArgs args)
+ {
+ IsOpen = !IsOpen;
+ return base.OnPointerUp(context, args);
+ }
+
+ protected override Rectangle GetListAreaRectangle(IGuiContext context)
+ {
+ return GetDropDownRectangle(context);
+ }
+
+ public override bool Contains(IGuiContext context, Point point)
+ {
+ return base.Contains(context, point) || IsOpen && GetListAreaRectangle(context).Contains(point);
+ }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var width = 0;
+ var height = 0;
+
+ foreach (var item in Items)
+ {
+ var itemSize = GetItemSize(context, item);
+
+ if (itemSize.Width > width)
+ width = itemSize.Width;
+
+ if (itemSize.Height > height)
+ height = itemSize.Height;
+ }
+
+ return new Size(width + ClipPadding.Width, height + ClipPadding.Height);
+ }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ if (IsOpen)
+ {
+ var dropDownRectangle = GetListAreaRectangle(context);
+
+ if (DropDownRegion != null)
+ {
+ renderer.DrawRegion(DropDownRegion, dropDownRectangle, DropDownColor);
+ }
+ else
+ {
+ renderer.FillRectangle(dropDownRectangle, DropDownColor);
+ renderer.DrawRectangle(dropDownRectangle, BorderColor);
+ }
+
+ DrawItemList(context, renderer);
+ }
+
+ var selectedTextInfo = GetItemTextInfo(context, ContentRectangle, SelectedItem);
+
+ if (!string.IsNullOrWhiteSpace(selectedTextInfo.Text))
+ renderer.DrawText(selectedTextInfo.Font, selectedTextInfo.Text, selectedTextInfo.Position + TextOffset, selectedTextInfo.Color, selectedTextInfo.ClippingRectangle);
+ }
+
+ private Rectangle GetDropDownRectangle(IGuiContext context)
+ {
+ var dropDownRectangle = BoundingRectangle;
+
+ dropDownRectangle.Y = dropDownRectangle.Y + dropDownRectangle.Height;
+ dropDownRectangle.Height = (int) Items
+ .Select(item => GetItemSize(context, item))
+ .Select(itemSize => itemSize.Height)
+ .Sum();
+
+ return dropDownRectangle;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CompositeControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CompositeControl.cs
new file mode 100644
index 0000000..8217c69
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CompositeControl.cs
@@ -0,0 +1,70 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public abstract class CompositeControl : Control
+ {
+ protected CompositeControl()
+ {
+ }
+
+ protected bool IsDirty { get; set; } = true;
+
+ public abstract object Content { get; set; }
+
+ protected abstract Control Template { get; }
+
+ public override IEnumerable<Control> Children
+ {
+ get
+ {
+ var control = Template;
+
+ if (control != null)
+ yield return control;
+ }
+ }
+
+ public override void InvalidateMeasure()
+ {
+ base.InvalidateMeasure();
+ IsDirty = true;
+ }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var control = Template;
+
+ if (control != null)
+ return Template.CalculateActualSize(context);
+
+ return Size.Empty;
+ }
+
+ public override void Update(IGuiContext context, float deltaSeconds)
+ {
+ var control = Template;
+
+ if (control != null)
+ {
+ if (IsDirty)
+ {
+ control.Parent = this;
+ control.ActualSize = ContentRectangle.Size;
+ control.Position = new Point(Padding.Left, Padding.Top);
+ control.InvalidateMeasure();
+ IsDirty = false;
+ }
+ }
+ }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ var control = Template;
+ control?.Draw(context, renderer, deltaSeconds);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ContentControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ContentControl.cs
new file mode 100644
index 0000000..932f8e4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ContentControl.cs
@@ -0,0 +1,82 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class ContentControl : Control
+ {
+ private bool _contentChanged = true;
+
+ private object _content;
+ public object Content
+ {
+ get => _content;
+ set
+ {
+ if (_content != value)
+ {
+ _content = value;
+ _contentChanged = true;
+ }
+ }
+ }
+
+ public override IEnumerable<Control> Children
+ {
+ get
+ {
+ if (Content is Control control)
+ yield return control;
+ }
+ }
+
+ public bool HasContent => Content == null;
+
+ public override void InvalidateMeasure()
+ {
+ base.InvalidateMeasure();
+ _contentChanged = true;
+ }
+
+ public override void Update(IGuiContext context, float deltaSeconds)
+ {
+ if (_content is Control control && _contentChanged)
+ {
+ control.Parent = this;
+ control.ActualSize = ContentRectangle.Size;
+ control.Position = new Point(Padding.Left, Padding.Top);
+ control.InvalidateMeasure();
+ _contentChanged = false;
+ }
+ }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ if (Content is Control control)
+ {
+ control.Draw(context, renderer, deltaSeconds);
+ }
+ else
+ {
+ var text = Content?.ToString();
+ var textInfo = GetTextInfo(context, text, ContentRectangle, HorizontalTextAlignment, VerticalTextAlignment);
+
+ if (!string.IsNullOrWhiteSpace(textInfo.Text))
+ renderer.DrawText(textInfo.Font, textInfo.Text, textInfo.Position + TextOffset, textInfo.Color, textInfo.ClippingRectangle);
+ }
+ }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ if (Content is Control control)
+ return control.CalculateActualSize(context);
+
+ var text = Content?.ToString();
+ var font = Font ?? context.DefaultFont;
+ return (Size)font.MeasureString(text ?? string.Empty);
+ }
+ }
+
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Control.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Control.cs
new file mode 100644
index 0000000..5440fdf
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Control.cs
@@ -0,0 +1,271 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.BitmapFonts;
+using MonoGame.Extended.Input.InputListeners;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public abstract class Control : Element<Control>, IRectangular
+ {
+ protected Control()
+ {
+ BackgroundColor = Color.White;
+ TextColor = Color.White;
+ IsEnabled = true;
+ IsVisible = true;
+ Origin = Point.Zero;
+ Skin = Skin.Default;
+ }
+
+ private Skin _skin;
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Skin Skin
+ {
+ get => _skin;
+ set
+ {
+ if (_skin != value)
+ {
+ _skin = value;
+ _skin?.Apply(this);
+ }
+ }
+ }
+
+ public abstract IEnumerable<Control> Children { get; }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Thickness Margin { get; set; }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Thickness Padding { get; set; }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Thickness ClipPadding { get; set; }
+
+ public bool IsLayoutRequired { get; set; }
+
+ public Rectangle ClippingRectangle
+ {
+ get
+ {
+ var r = BoundingRectangle;
+ return new Rectangle(r.Left + ClipPadding.Left, r.Top + ClipPadding.Top, r.Width - ClipPadding.Width, r.Height - ClipPadding.Height);
+ }
+ }
+
+ public Rectangle ContentRectangle
+ {
+ get
+ {
+ var r = BoundingRectangle;
+ return new Rectangle(r.Left + Padding.Left, r.Top + Padding.Top, r.Width - Padding.Width, r.Height - Padding.Height);
+ }
+ }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public bool IsFocused { get; set; }
+
+ private bool _isHovered;
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public bool IsHovered
+ {
+ get => _isHovered;
+ private set
+ {
+ if (_isHovered != value)
+ {
+ _isHovered = value;
+ HoverStyle?.ApplyIf(this, _isHovered);
+ }
+ }
+ }
+
+ [JsonIgnore]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Guid Id { get; set; } = Guid.NewGuid();
+
+ public Vector2 Offset { get; set; }
+ public BitmapFont Font { get; set; }
+ public Color TextColor { get; set; }
+ public Vector2 TextOffset { get; set; }
+ public HorizontalAlignment HorizontalAlignment { get; set; } = HorizontalAlignment.Stretch;
+ public VerticalAlignment VerticalAlignment { get; set; } = VerticalAlignment.Stretch;
+ public HorizontalAlignment HorizontalTextAlignment { get; set; } = HorizontalAlignment.Centre;
+ public VerticalAlignment VerticalTextAlignment { get; set; } = VerticalAlignment.Centre;
+
+ public abstract Size GetContentSize(IGuiContext context);
+
+ public virtual Size CalculateActualSize(IGuiContext context)
+ {
+ var fixedSize = Size;
+ var desiredSize = GetContentSize(context) + Margin.Size + Padding.Size;
+
+ if (desiredSize.Width < MinWidth)
+ desiredSize.Width = MinWidth;
+
+ if (desiredSize.Height < MinHeight)
+ desiredSize.Height = MinHeight;
+
+ if (desiredSize.Width > MaxWidth)
+ desiredSize.Width = MaxWidth;
+
+ if (desiredSize.Height > MaxWidth)
+ desiredSize.Height = MaxHeight;
+
+ var width = fixedSize.Width == 0 ? desiredSize.Width : fixedSize.Width;
+ var height = fixedSize.Height == 0 ? desiredSize.Height : fixedSize.Height;
+ return new Size(width, height);
+ }
+
+ public virtual void InvalidateMeasure() { }
+
+ private bool _isEnabled;
+ public bool IsEnabled
+ {
+ get => _isEnabled;
+ set
+ {
+ if (_isEnabled != value)
+ {
+ _isEnabled = value;
+ DisabledStyle?.ApplyIf(this, !_isEnabled);
+ }
+ }
+ }
+
+ public bool IsVisible { get; set; }
+
+ private ControlStyle _hoverStyle;
+ public ControlStyle HoverStyle
+ {
+ get => _hoverStyle;
+ set
+ {
+ if (_hoverStyle != value)
+ {
+ _hoverStyle = value;
+ HoverStyle?.ApplyIf(this, _isHovered);
+ }
+ }
+ }
+
+ private ControlStyle _disabledStyle;
+ public ControlStyle DisabledStyle
+ {
+ get => _disabledStyle;
+ set
+ {
+ _disabledStyle = value;
+ DisabledStyle?.ApplyIf(this, !_isEnabled);
+ }
+ }
+
+ public virtual void OnScrolled(int delta) { }
+
+ public virtual bool OnKeyTyped(IGuiContext context, KeyboardEventArgs args) { return true; }
+ public virtual bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args) { return true; }
+
+ public virtual bool OnFocus(IGuiContext context) { return true; }
+ public virtual bool OnUnfocus(IGuiContext context) { return true; }
+
+ public virtual bool OnPointerDown(IGuiContext context, PointerEventArgs args) { return true; }
+ public virtual bool OnPointerMove(IGuiContext context, PointerEventArgs args) { return true; }
+ public virtual bool OnPointerUp(IGuiContext context, PointerEventArgs args) { return true; }
+
+ public virtual bool OnPointerEnter(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsEnabled && !IsHovered)
+ IsHovered = true;
+
+ return true;
+ }
+
+ public virtual bool OnPointerLeave(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsEnabled && IsHovered)
+ IsHovered = false;
+
+ return true;
+ }
+
+ public virtual bool Contains(IGuiContext context, Point point)
+ {
+ return BoundingRectangle.Contains(point);
+ }
+
+ public virtual void Update(IGuiContext context, float deltaSeconds) { }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ if (BackgroundRegion != null)
+ renderer.DrawRegion(BackgroundRegion, BoundingRectangle, BackgroundColor);
+ else if (BackgroundColor != Color.Transparent)
+ renderer.FillRectangle(BoundingRectangle, BackgroundColor);
+
+ if (BorderThickness != 0)
+ renderer.DrawRectangle(BoundingRectangle, BorderColor, BorderThickness);
+
+ // handy debug rectangles
+ //renderer.DrawRectangle(ContentRectangle, Color.Magenta);
+ //renderer.DrawRectangle(BoundingRectangle, Color.Lime);
+ }
+
+ public bool HasParent(Control control)
+ {
+ return Parent != null && (Parent == control || Parent.HasParent(control));
+ }
+
+ protected TextInfo GetTextInfo(IGuiContext context, string text, Rectangle targetRectangle, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment)
+ {
+ var font = Font ?? context.DefaultFont;
+ var textSize = (Size)font.GetStringRectangle(text ?? string.Empty, Vector2.Zero).Size;
+ var destinationRectangle = LayoutHelper.AlignRectangle(horizontalAlignment, verticalAlignment, textSize, targetRectangle);
+ var textPosition = destinationRectangle.Location.ToVector2();
+ var textInfo = new TextInfo(text, font, textPosition, textSize, TextColor, targetRectangle);
+ return textInfo;
+ }
+
+ public struct TextInfo
+ {
+ public TextInfo(string text, BitmapFont font, Vector2 position, Size size, Color color, Rectangle? clippingRectangle)
+ {
+ Text = text ?? string.Empty;
+ Font = font;
+ Size = size;
+ Color = color;
+ ClippingRectangle = clippingRectangle;
+ Position = position;
+ }
+
+ public string Text;
+ public BitmapFont Font;
+ public Size Size;
+ public Color Color;
+ public Rectangle? ClippingRectangle;
+ public Vector2 Position;
+ }
+
+ public Dictionary<string, object> AttachedProperties { get; } = new Dictionary<string, object>();
+
+ public object GetAttachedProperty(string name)
+ {
+ return AttachedProperties.TryGetValue(name, out var value) ? value : null;
+ }
+
+ public void SetAttachedProperty(string name, object value)
+ {
+ AttachedProperties[name] = value;
+ }
+
+ public virtual Type GetAttachedPropertyType(string propertyName)
+ {
+ return null;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ControlCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ControlCollection.cs
new file mode 100644
index 0000000..6135b4c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ControlCollection.cs
@@ -0,0 +1,15 @@
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class ControlCollection : ElementCollection<Control, Control>
+ {
+ public ControlCollection()
+ : base(null)
+ {
+ }
+
+ public ControlCollection(Control parent)
+ : base(parent)
+ {
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Dialog.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Dialog.cs
new file mode 100644
index 0000000..26f0fca
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Dialog.cs
@@ -0,0 +1,41 @@
+using System.Linq;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ //public class Dialog : LayoutControl
+ //{
+ // public Dialog()
+ // {
+ // HorizontalAlignment = HorizontalAlignment.Centre;
+ // VerticalAlignment = VerticalAlignment.Centre;
+ // }
+
+ // public Thickness Padding { get; set; }
+ // public Screen Owner { get; private set; }
+
+ // public void Show(Screen owner)
+ // {
+ // Owner = owner;
+ // Owner.Controls.Add(this);
+ // }
+
+ // public void Hide()
+ // {
+ // Owner.Controls.Remove(this);
+ // }
+
+ // protected override Size2 CalculateDesiredSize(IGuiContext context, Size2 availableSize)
+ // {
+ // var sizes = Items.Select(control => LayoutHelper.GetSizeWithMargins(control, context, availableSize)).ToArray();
+ // var width = sizes.Max(s => s.Width);
+ // var height = sizes.Max(s => s.Height);
+ // return new Size2(width, height) + Padding.Size;
+ // }
+
+ // public override void Layout(IGuiContext context, RectangleF rectangle)
+ // {
+ // foreach (var control in Items)
+ // PlaceControl(context, control, Padding.Left, Padding.Top, Width - Padding.Size.Width, Height - Padding.Size.Height);
+ // }
+ //}
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/DockPanel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/DockPanel.cs
new file mode 100644
index 0000000..7dfe1e6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/DockPanel.cs
@@ -0,0 +1,104 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public enum Dock
+ {
+ Left, Right, Top, Bottom
+ }
+
+ public class DockPanel : LayoutControl
+ {
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var size = new Size();
+
+ for (var i = 0; i < Items.Count; i++)
+ {
+ var control = Items[i];
+ var actualSize = control.CalculateActualSize(context);
+
+ if (LastChildFill && i == Items.Count - 1)
+ {
+ size.Width += actualSize.Width;
+ size.Height += actualSize.Height;
+ }
+ else
+ {
+ var dock = control.GetAttachedProperty(DockProperty) as Dock? ?? Dock.Left;
+
+ switch (dock)
+ {
+ case Dock.Left:
+ case Dock.Right:
+ size.Width += actualSize.Width;
+ break;
+ case Dock.Top:
+ case Dock.Bottom:
+ size.Height += actualSize.Height;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ }
+
+ return size;
+ }
+
+ protected override void Layout(IGuiContext context, Rectangle rectangle)
+ {
+ for (var i = 0; i < Items.Count; i++)
+ {
+ var control = Items[i];
+
+ if (LastChildFill && i == Items.Count - 1)
+ {
+ PlaceControl(context, control, rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+ }
+ else
+ {
+ var actualSize = control.CalculateActualSize(context);
+ var dock = control.GetAttachedProperty(DockProperty) as Dock? ?? Dock.Left;
+
+ switch (dock)
+ {
+ case Dock.Left:
+ PlaceControl(context, control, rectangle.Left, rectangle.Top, actualSize.Width, rectangle.Height);
+ rectangle.X += actualSize.Width;
+ rectangle.Width -= actualSize.Width;
+ break;
+ case Dock.Right:
+ PlaceControl(context, control, rectangle.Right - actualSize.Width, rectangle.Top, actualSize.Width, rectangle.Height);
+ rectangle.Width -= actualSize.Width;
+ break;
+ case Dock.Top:
+ PlaceControl(context, control, rectangle.Left, rectangle.Top, rectangle.Width, actualSize.Height);
+ rectangle.Y += actualSize.Height;
+ rectangle.Height -= actualSize.Height;
+ break;
+ case Dock.Bottom:
+ PlaceControl(context, control, rectangle.Left, rectangle.Bottom - actualSize.Height, rectangle.Width, actualSize.Height);
+ rectangle.Height -= actualSize.Height;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ }
+ }
+
+ public const string DockProperty = "Dock";
+
+ public override Type GetAttachedPropertyType(string propertyName)
+ {
+ if (string.Equals(DockProperty, propertyName, StringComparison.OrdinalIgnoreCase))
+ return typeof(Dock);
+
+ return base.GetAttachedPropertyType(propertyName);
+ }
+
+ public bool LastChildFill { get; set; } = true;
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Form.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Form.cs
new file mode 100644
index 0000000..97b984d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Form.cs
@@ -0,0 +1,42 @@
+using System.Linq;
+using MonoGame.Extended.Input.InputListeners;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ //public class Form : StackPanel
+ //{
+ // public Form()
+ // {
+ // }
+
+ // public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args)
+ // {
+ // if (args.Key == Keys.Tab)
+ // {
+ // var controls = FindControls<Control>();
+ // var index = controls.IndexOf(context.FocusedControl);
+ // if (index > -1)
+ // {
+ // index++;
+ // if (index >= controls.Count) index = 0;
+ // context.SetFocus(controls[index]);
+ // return true;
+ // }
+ // }
+
+ // if (args.Key == Keys.Enter)
+ // {
+ // var controls = FindControls<Submit>();
+ // if (controls.Count > 0)
+ // {
+ // var submit = controls.FirstOrDefault();
+ // submit.TriggerClicked();
+ // return true;
+ // }
+ // }
+
+ // return base.OnKeyPressed(context, args);
+ // }
+ //}
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ItemsControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ItemsControl.cs
new file mode 100644
index 0000000..f0babaf
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ItemsControl.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public abstract class ItemsControl : Control
+ {
+ protected ItemsControl()
+ {
+ Items = new ControlCollection(this);
+ //{
+ // ItemAdded = x => UpdateRootIsLayoutRequired(),
+ // ItemRemoved = x => UpdateRootIsLayoutRequired()
+ //};
+ }
+
+ public override IEnumerable<Control> Children => Items;
+
+ public ControlCollection Items { get; }
+
+ ///// <summary>
+ ///// Recursive Method to find the root element and update the IsLayoutRequired property. So that the screen knows that something in the controls
+ ///// have had a change to their layout. Also, it will reset the size of the element so that it can get a clean build so that the background patches
+ ///// can be rendered with the updates.
+ ///// </summary>
+ //private void UpdateRootIsLayoutRequired()
+ //{
+ // var parent = Parent as ItemsControl;
+
+ // if (parent == null)
+ // IsLayoutRequired = true;
+ // else
+ // parent.UpdateRootIsLayoutRequired();
+
+ // Size = Size2.Empty;
+ //}
+
+ //protected List<T> FindControls<T>()
+ // where T : Control
+ //{
+ // return FindControls<T>(Items);
+ //}
+
+ //protected List<T> FindControls<T>(ControlCollection controls)
+ // where T : Control
+ //{
+ // var results = new List<T>();
+ // foreach (var control in controls)
+ // {
+ // if (control is T)
+ // results.Add(control as T);
+
+ // var itemsControl = control as ItemsControl;
+
+ // if (itemsControl != null && itemsControl.Items.Any())
+ // results = results.Concat(FindControls<T>(itemsControl.Items)).ToList();
+ // }
+ // return results;
+ //}
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Label.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Label.cs
new file mode 100644
index 0000000..b949148
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Label.cs
@@ -0,0 +1,14 @@
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class Label : ContentControl
+ {
+ public Label()
+ {
+ }
+
+ public Label(string text = null)
+ {
+ Content = text ?? string.Empty;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/LayoutControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/LayoutControl.cs
new file mode 100644
index 0000000..3935baa
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/LayoutControl.cs
@@ -0,0 +1,40 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public abstract class LayoutControl : ItemsControl
+ {
+ protected LayoutControl()
+ {
+ HorizontalAlignment = HorizontalAlignment.Stretch;
+ VerticalAlignment = VerticalAlignment.Stretch;
+ BackgroundColor = Color.Transparent;
+ }
+
+ private bool _isLayoutValid;
+
+ public override void InvalidateMeasure()
+ {
+ base.InvalidateMeasure();
+ _isLayoutValid = false;
+ }
+
+ public override void Update(IGuiContext context, float deltaSeconds)
+ {
+ base.Update(context, deltaSeconds);
+
+ if (!_isLayoutValid)
+ {
+ Layout(context, new Rectangle(Padding.Left, Padding.Top, ContentRectangle.Width, ContentRectangle.Height));
+ _isLayoutValid = true;
+ }
+ }
+
+ protected abstract void Layout(IGuiContext context, Rectangle rectangle);
+
+ protected static void PlaceControl(IGuiContext context, Control control, float x, float y, float width, float height)
+ {
+ LayoutHelper.PlaceControl(context, control, x, y, width, height);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ListBox.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ListBox.cs
new file mode 100644
index 0000000..d456e07
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ListBox.cs
@@ -0,0 +1,42 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class ListBox : SelectorControl
+ {
+ public ListBox()
+ {
+ }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var width = 0;
+ var height = 0;
+
+ foreach (var item in Items)
+ {
+ var itemSize = GetItemSize(context, item);
+
+ if (itemSize.Width > width)
+ width = itemSize.Width;
+
+ height += itemSize.Height;
+ }
+
+ return new Size(width + ClipPadding.Width, height + ClipPadding.Height);
+ }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ ScrollIntoView(context);
+ DrawItemList(context, renderer);
+ }
+
+ protected override Rectangle GetListAreaRectangle(IGuiContext context)
+ {
+ return ContentRectangle;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ProgressBar.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ProgressBar.cs
new file mode 100644
index 0000000..6983d09
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ProgressBar.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class ProgressBar : Control
+ {
+ public ProgressBar()
+ {
+ }
+
+ private float _progress = 1.0f;
+ public float Progress
+ {
+ get { return _progress; }
+ set
+ {
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if(_progress != value)
+ {
+ _progress = value;
+ ProgressChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ public TextureRegion2D BarRegion { get; set; }
+ public Color BarColor { get; set; } = Color.White;
+
+ public event EventHandler ProgressChanged;
+
+ public override IEnumerable<Control> Children { get; } = Enumerable.Empty<Control>();
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ return new Size(5, 5);
+ }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ var boundingRectangle = ContentRectangle;
+ var clippingRectangle = new Rectangle(boundingRectangle.X, boundingRectangle.Y, (int)(boundingRectangle.Width * Progress), boundingRectangle.Height);
+
+ if (BarRegion != null)
+ renderer.DrawRegion(BarRegion, BoundingRectangle, BarColor, clippingRectangle);
+ else
+ renderer.FillRectangle(BoundingRectangle, BarColor, clippingRectangle);
+ }
+
+ //protected override void DrawBackground(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ //{
+ // base.DrawBackground(context, renderer, deltaSeconds);
+
+
+ //}
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/SelectorControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/SelectorControl.cs
new file mode 100644
index 0000000..49b39ba
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/SelectorControl.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGame.Extended.Input.InputListeners;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public abstract class SelectorControl : Control
+ {
+ protected SelectorControl()
+ {
+ }
+
+ private int _selectedIndex = -1;
+ public virtual int SelectedIndex
+ {
+ get { return _selectedIndex; }
+ set
+ {
+ if (_selectedIndex != value)
+ {
+ _selectedIndex = value;
+ SelectedIndexChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ public override IEnumerable<Control> Children => Items.OfType<Control>();
+
+ public virtual List<object> Items { get; } = new List<object>();
+ public virtual Color SelectedTextColor { get; set; } = Color.White;
+ public virtual Color SelectedItemColor { get; set; } = Color.CornflowerBlue;
+ public virtual Thickness ItemPadding { get; set; } = new Thickness(4, 2);
+ public virtual string NameProperty { get; set; }
+
+ public event EventHandler SelectedIndexChanged;
+
+ protected int FirstIndex;
+
+ public object SelectedItem
+ {
+ get { return SelectedIndex >= 0 && SelectedIndex <= Items.Count - 1 ? Items[SelectedIndex] : null; }
+ set { SelectedIndex = Items.IndexOf(value); }
+ }
+
+ public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args)
+ {
+ if (args.Key == Keys.Down) ScrollDown();
+ if (args.Key == Keys.Up) ScrollUp();
+
+ return base.OnKeyPressed(context, args);
+ }
+
+ public override void OnScrolled(int delta)
+ {
+ base.OnScrolled(delta);
+
+ if (delta < 0) ScrollDown();
+ if (delta > 0) ScrollUp();
+ }
+
+ private void ScrollDown()
+ {
+ if (SelectedIndex < Items.Count - 1)
+ SelectedIndex++;
+ }
+
+ private void ScrollUp()
+ {
+ if (SelectedIndex > 0)
+ SelectedIndex--;
+ }
+
+ public override bool OnPointerDown(IGuiContext context, PointerEventArgs args)
+ {
+ var contentRectangle = GetListAreaRectangle(context);
+
+ for (var i = FirstIndex; i < Items.Count; i++)
+ {
+ var itemRectangle = GetItemRectangle(context, i - FirstIndex, contentRectangle);
+
+ if (itemRectangle.Contains(args.Position))
+ {
+ SelectedIndex = i;
+ OnItemClicked(context, args);
+ break;
+ }
+ }
+
+ return base.OnPointerDown(context, args);
+ }
+
+ protected virtual void OnItemClicked(IGuiContext context, PointerEventArgs args) { }
+
+ protected TextInfo GetItemTextInfo(IGuiContext context, Rectangle itemRectangle, object item)
+ {
+ var textRectangle = new Rectangle(itemRectangle.X + ItemPadding.Left, itemRectangle.Y + ItemPadding.Top,
+ itemRectangle.Width - ItemPadding.Right, itemRectangle.Height - ItemPadding.Bottom);
+ var itemTextInfo = GetTextInfo(context, GetItemName(item), textRectangle, HorizontalTextAlignment, VerticalTextAlignment);
+ return itemTextInfo;
+ }
+
+ private string GetItemName(object item)
+ {
+ if (item != null)
+ {
+ if (NameProperty != null)
+ {
+ return item.GetType()
+ .GetRuntimeProperty(NameProperty)
+ .GetValue(item)
+ ?.ToString() ?? string.Empty;
+ }
+
+ return item.ToString();
+ }
+
+ return string.Empty;
+ }
+
+ protected Rectangle GetItemRectangle(IGuiContext context, int index, Rectangle contentRectangle)
+ {
+ var font = Font ?? context.DefaultFont;
+ var itemHeight = font.LineHeight + ItemPadding.Top + ItemPadding.Bottom;
+ return new Rectangle(contentRectangle.X, contentRectangle.Y + itemHeight * index, contentRectangle.Width, itemHeight);
+ }
+
+ protected void ScrollIntoView(IGuiContext context)
+ {
+ var contentRectangle = GetListAreaRectangle(context);
+ var selectedItemRectangle = GetItemRectangle(context, SelectedIndex - FirstIndex, contentRectangle);
+
+ if (selectedItemRectangle.Bottom > ClippingRectangle.Bottom)
+ FirstIndex++;
+
+ if (selectedItemRectangle.Top < ClippingRectangle.Top && FirstIndex > 0)
+ FirstIndex--;
+ }
+
+ protected Size GetItemSize(IGuiContext context, object item)
+ {
+ var text = GetItemName(item);
+ var font = Font ?? context.DefaultFont;
+ var textSize = (Size)font.MeasureString(text ?? string.Empty);
+ var itemWidth = textSize.Width + ItemPadding.Width;
+ var itemHeight = textSize.Height + ItemPadding.Height;
+ return new Size(itemWidth, itemHeight);
+ }
+
+ protected abstract Rectangle GetListAreaRectangle(IGuiContext context);
+
+ protected void DrawItemList(IGuiContext context, IGuiRenderer renderer)
+ {
+ var listRectangle = GetListAreaRectangle(context);
+
+ for (var i = FirstIndex; i < Items.Count; i++)
+ {
+ var item = Items[i];
+ var itemRectangle = GetItemRectangle(context, i - FirstIndex, listRectangle);
+ var itemTextInfo = GetItemTextInfo(context, itemRectangle, item);
+ var textColor = i == SelectedIndex ? SelectedTextColor : itemTextInfo.Color;
+
+ if (SelectedIndex == i)
+ renderer.FillRectangle(itemRectangle, SelectedItemColor, listRectangle);
+
+ renderer.DrawText(itemTextInfo.Font, itemTextInfo.Text, itemTextInfo.Position + TextOffset, textColor, itemTextInfo.ClippingRectangle);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/StackPanel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/StackPanel.cs
new file mode 100644
index 0000000..5d8926d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/StackPanel.cs
@@ -0,0 +1,69 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class StackPanel : LayoutControl
+ {
+ public StackPanel()
+ {
+ }
+
+ public Orientation Orientation { get; set; } = Orientation.Vertical;
+ public int Spacing { get; set; }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var width = 0;
+ var height = 0;
+
+ foreach (var control in Items)
+ {
+ var actualSize = control.CalculateActualSize(context);
+
+ switch (Orientation)
+ {
+ case Orientation.Horizontal:
+ width += actualSize.Width;
+ height = actualSize.Height > height ? actualSize.Height : height;
+ break;
+ case Orientation.Vertical:
+ width = actualSize.Width > width ? actualSize.Width : width;
+ height += actualSize.Height;
+ break;
+ default:
+ throw new InvalidOperationException($"Unexpected orientation {Orientation}");
+ }
+ }
+
+ width += Orientation == Orientation.Horizontal ? (Items.Count - 1) * Spacing : 0;
+ height += Orientation == Orientation.Vertical ? (Items.Count - 1) * Spacing : 0;
+
+ return new Size(width, height);
+ }
+
+ protected override void Layout(IGuiContext context, Rectangle rectangle)
+ {
+ foreach (var control in Items)
+ {
+ var actualSize = control.CalculateActualSize(context);
+
+ switch (Orientation)
+ {
+ case Orientation.Vertical:
+ PlaceControl(context, control, rectangle.X, rectangle.Y, rectangle.Width, actualSize.Height);
+ rectangle.Y += actualSize.Height + Spacing;
+ rectangle.Height -= actualSize.Height;
+ break;
+ case Orientation.Horizontal:
+ PlaceControl(context, control, rectangle.X, rectangle.Y, actualSize.Width, rectangle.Height);
+ rectangle.X += actualSize.Width + Spacing;
+ rectangle.Width -= actualSize.Width;
+ break;
+ default:
+ throw new InvalidOperationException($"Unexpected orientation {Orientation}");
+ }
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox.cs
new file mode 100644
index 0000000..75b6940
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox.cs
@@ -0,0 +1,331 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGame.Extended.Input.InputListeners;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public sealed class TextBox : Control
+ {
+ public TextBox(string text = null)
+ {
+ Text = text ?? string.Empty;
+ HorizontalTextAlignment = HorizontalAlignment.Left;
+ }
+
+ public TextBox()
+ : this(null)
+ {
+ }
+
+ public int SelectionStart { get; set; }
+ public char? PasswordCharacter { get; set; }
+
+ private string _text;
+
+ public string Text
+ {
+ get { return _text; }
+ set
+ {
+ if (_text != value)
+ {
+ _text = value;
+ OnTextChanged();
+ }
+ }
+ }
+
+ private void OnTextChanged()
+ {
+ if (!string.IsNullOrEmpty(Text) && SelectionStart > Text.Length)
+ SelectionStart = Text.Length;
+
+ TextChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ public event EventHandler TextChanged;
+
+ public override IEnumerable<Control> Children { get; } = Enumerable.Empty<Control>();
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var font = Font ?? context.DefaultFont;
+ var stringSize = (Size) font.MeasureString(Text ?? string.Empty);
+ return new Size(stringSize.Width,
+ stringSize.Height < font.LineHeight ? font.LineHeight : stringSize.Height);
+ }
+
+ //protected override Size2 CalculateDesiredSize(IGuiContext context, Size2 availableSize)
+ //{
+ // var font = Font ?? context.DefaultFont;
+ // return new Size2(Width + Padding.Left + Padding.Right, (Height <= 0.0f ? font.LineHeight + 2 : Height) + Padding.Top + Padding.Bottom);
+ //}
+
+ public override bool OnPointerDown(IGuiContext context, PointerEventArgs args)
+ {
+ SelectionStart = FindNearestGlyphIndex(context, args.Position);
+ _isCaretVisible = true;
+
+ //_selectionIndexes.Clear();
+ //_selectionIndexes.Push(SelectionStart);
+ //_startSelectionBox = Text.Length > 0;
+
+ return base.OnPointerDown(context, args);
+ }
+
+ //public override bool OnPointerMove(IGuiContext context, PointerEventArgs args)
+ //{
+ // if (_startSelectionBox)
+ // {
+ // var selection = FindNearestGlyphIndex(context, args.Position);
+ // if (selection != _selectionIndexes.Peek())
+ // {
+ // if (_selectionIndexes.Count == 1)
+ // {
+ // _selectionIndexes.Push(selection);
+ // }
+ // else if (_selectionIndexes.Last() < _selectionIndexes.Peek())
+ // {
+ // if (selection > _selectionIndexes.Peek()) _selectionIndexes.Pop();
+ // else _selectionIndexes.Push(selection);
+ // }
+ // else
+ // {
+ // if (selection < _selectionIndexes.Peek()) _selectionIndexes.Pop();
+ // else _selectionIndexes.Push(selection);
+ // }
+ // SelectionStart = selection;
+ // }
+ // }
+
+ // return base.OnPointerMove(context, args);
+ //}
+
+ //public override bool OnPointerLeave(IGuiContext context, PointerEventArgs args)
+ //{
+ // _startSelectionBox = false;
+
+ // return base.OnPointerLeave(context, args);
+ //}
+
+ //public override bool OnPointerUp(IGuiContext context, PointerEventArgs args)
+ //{
+ // _startSelectionBox = false;
+
+ // return base.OnPointerUp(context, args);
+ //}
+
+ private int FindNearestGlyphIndex(IGuiContext context, Point position)
+ {
+ var font = Font ?? context.DefaultFont;
+ var textInfo = GetTextInfo(context, Text, ContentRectangle, HorizontalTextAlignment, VerticalTextAlignment);
+ var i = 0;
+
+ foreach (var glyph in font.GetGlyphs(textInfo.Text, textInfo.Position))
+ {
+ var fontRegionWidth = glyph.FontRegion?.Width ?? 0;
+ var glyphMiddle = (int) (glyph.Position.X + fontRegionWidth * 0.5f);
+
+ if (position.X >= glyphMiddle)
+ {
+ i++;
+ continue;
+ }
+
+ return i;
+ }
+
+ return i;
+ }
+
+ public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args)
+ {
+ switch (args.Key)
+ {
+ case Keys.Tab:
+ case Keys.Enter:
+ return true;
+ case Keys.Back:
+ if (Text.Length > 0)
+ {
+ if (SelectionStart > 0) // && _selectionIndexes.Count <= 1)
+ {
+ SelectionStart--;
+ Text = Text.Remove(SelectionStart, 1);
+ }
+ //else
+ //{
+ // var start = MathHelper.Min(_selectionIndexes.Last(), _selectionIndexes.Peek());
+ // var end = MathHelper.Max(_selectionIndexes.Last(), _selectionIndexes.Peek());
+ // Text = Text.Remove(start, end - start);
+
+ // _selectionIndexes.Clear();
+ //}
+ }
+
+ break;
+ case Keys.Delete:
+ if (SelectionStart < Text.Length) // && _selectionIndexes.Count <= 1)
+ {
+ Text = Text.Remove(SelectionStart, 1);
+ }
+ //else if (_selectionIndexes.Count > 1)
+ //{
+ // var start = MathHelper.Min(_selectionIndexes.Last(), _selectionIndexes.Peek());
+ // var end = MathHelper.Max(_selectionIndexes.Last(), _selectionIndexes.Peek());
+ // Text = Text.Remove(start, end - start);
+ // SelectionStart = 0; // yeah, nah.
+
+ // _selectionIndexes.Clear();
+ //}
+ break;
+ case Keys.Left:
+ if (SelectionStart > 0)
+ {
+ //if (_selectionIndexes.Count > 1)
+ //{
+ // if (_selectionIndexes.Last() < SelectionStart) SelectionStart = _selectionIndexes.Last();
+ // _selectionIndexes.Clear();
+ //}
+ //else
+ {
+ SelectionStart--;
+ }
+ }
+
+ break;
+ case Keys.Right:
+ if (SelectionStart < Text.Length)
+ {
+ //if (_selectionIndexes.Count > 1)
+ //{
+ // if (_selectionIndexes.Last() > SelectionStart) SelectionStart = _selectionIndexes.Last();
+ // _selectionIndexes.Clear();
+ //}
+ //else
+ {
+ SelectionStart++;
+ }
+ }
+
+ break;
+ case Keys.Home:
+ SelectionStart = 0;
+ //_selectionIndexes.Clear();
+ break;
+ case Keys.End:
+ SelectionStart = Text.Length;
+ //_selectionIndexes.Clear();
+ break;
+ default:
+ if (args.Character != null)
+ {
+ //if (_selectionIndexes.Count > 1)
+ //{
+ // var start = MathHelper.Min(_selectionIndexes.Last(), _selectionIndexes.Peek());
+ // var end = MathHelper.Max(_selectionIndexes.Last(), _selectionIndexes.Peek());
+ // Text = Text.Remove(start, end - start);
+
+ // _selectionIndexes.Clear();
+ //}
+
+ Text = Text.Insert(SelectionStart, args.Character.ToString());
+ SelectionStart++;
+ }
+
+ break;
+ }
+
+ _isCaretVisible = true;
+ return base.OnKeyPressed(context, args);
+ }
+
+ private const float _caretBlinkRate = 0.53f;
+ private float _nextCaretBlink = _caretBlinkRate;
+ private bool _isCaretVisible = true;
+
+ //private bool _startSelectionBox = false;
+ //private Stack<int> _selectionIndexes = new Stack<int>();
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ var text = PasswordCharacter.HasValue ? new string(PasswordCharacter.Value, Text.Length) : Text;
+ var textInfo = GetTextInfo(context, text, ContentRectangle, HorizontalTextAlignment, VerticalTextAlignment);
+
+ if (!string.IsNullOrWhiteSpace(textInfo.Text))
+ renderer.DrawText(textInfo.Font, textInfo.Text, textInfo.Position + TextOffset, textInfo.Color,
+ textInfo.ClippingRectangle);
+
+ if (IsFocused)
+ {
+ var caretRectangle = GetCaretRectangle(textInfo, SelectionStart);
+
+ if (_isCaretVisible)
+ renderer.DrawRectangle(caretRectangle, TextColor);
+
+ _nextCaretBlink -= deltaSeconds;
+
+ if (_nextCaretBlink <= 0)
+ {
+ _isCaretVisible = !_isCaretVisible;
+ _nextCaretBlink = _caretBlinkRate;
+ }
+
+ //if (_selectionIndexes.Count > 1)
+ //{
+ // var start = 0;
+ // var end = 0;
+ // var point = Point2.Zero;
+ // if (_selectionIndexes.Last() > _selectionIndexes.Peek())
+ // {
+ // start = _selectionIndexes.Peek();
+ // end = _selectionIndexes.Last();
+ // point = caretRectangle.Position;
+ // }
+ // else
+ // {
+ // start = _selectionIndexes.Last();
+ // end = _selectionIndexes.Peek();
+ // point = GetCaretRectangle(textInfo, start).Position;
+ // }
+ // var selectionRectangle = textInfo.Font.GetStringRectangle(textInfo.Text.Substring(start, end - start), point);
+
+ // renderer.FillRectangle((Rectangle)selectionRectangle, Color.Black * 0.25f);
+ //}
+ }
+ }
+
+
+ //protected override string CreateBoxText(string text, BitmapFont font, float width)
+ //{
+ // return text;
+ //}
+
+ private Rectangle GetCaretRectangle(TextInfo textInfo, int index)
+ {
+ var caretRectangle = textInfo.Font.GetStringRectangle(textInfo.Text.Substring(0, index), textInfo.Position);
+
+ // TODO: Finish the caret position stuff when it's outside the clipping rectangle
+ if (caretRectangle.Right > ClippingRectangle.Right)
+ textInfo.Position.X -= caretRectangle.Right - ClippingRectangle.Right;
+
+ caretRectangle.X = caretRectangle.Right < ClippingRectangle.Right
+ ? caretRectangle.Right
+ : ClippingRectangle.Right;
+ caretRectangle.Width = 1;
+
+ if (caretRectangle.Left < ClippingRectangle.Left)
+ {
+ textInfo.Position.X += ClippingRectangle.Left - caretRectangle.Left;
+ caretRectangle.X = ClippingRectangle.Left;
+ }
+
+ return (Rectangle) caretRectangle;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox2.cs
new file mode 100644
index 0000000..e20b621
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox2.cs
@@ -0,0 +1,328 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGame.Extended.Input.InputListeners;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class TextBox2 : Control
+ {
+ public TextBox2()
+ : this(null)
+ {
+ }
+
+ public TextBox2(string text)
+ {
+ Text = text ?? string.Empty;
+ HorizontalTextAlignment = HorizontalAlignment.Left;
+ VerticalTextAlignment = VerticalAlignment.Top;
+ }
+
+
+ private const float _caretBlinkRate = 0.53f;
+ private float _nextCaretBlink = _caretBlinkRate;
+ private bool _isCaretVisible = true;
+
+ private readonly List<StringBuilder> _lines = new List<StringBuilder>();
+ public string Text
+ {
+ get => string.Concat(_lines.SelectMany(s => $"{s}\n"));
+ set
+ {
+ _lines.Clear();
+
+ var line = new StringBuilder();
+
+ for (var i = 0; i < value.Length; i++)
+ {
+ var c = value[i];
+
+ if (c == '\n')
+ {
+ _lines.Add(line);
+ line = new StringBuilder();
+ }
+ else if(c != '\r')
+ {
+ line.Append(c);
+ }
+ }
+
+ _lines.Add(line);
+ }
+ }
+
+ public int CaretIndex => ColumnIndex * LineIndex + ColumnIndex;
+ public int LineIndex { get; set; }
+ public int ColumnIndex { get; set; }
+ public int TabStops { get; set; } = 4;
+
+ public override IEnumerable<Control> Children => Enumerable.Empty<Control>();
+
+ public string GetLineText(int lineIndex) => _lines[lineIndex].ToString();
+ public int GetLineLength(int lineIndex) => _lines[lineIndex].Length;
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var font = Font ?? context.DefaultFont;
+ var stringSize = (Size)font.MeasureString(Text ?? string.Empty);
+ return new Size(stringSize.Width, stringSize.Height < font.LineHeight ? font.LineHeight : stringSize.Height);
+ }
+
+ public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args)
+ {
+ switch (args.Key)
+ {
+ case Keys.Tab:
+ Tab();
+ break;
+ case Keys.Back:
+ Backspace();
+ break;
+ case Keys.Delete:
+ Delete();
+ break;
+ case Keys.Left:
+ Left();
+ break;
+ case Keys.Right:
+ Right();
+ break;
+ case Keys.Up:
+ Up();
+ break;
+ case Keys.Down:
+ Down();
+ break;
+ case Keys.Home:
+ Home();
+ break;
+ case Keys.End:
+ End();
+ break;
+ case Keys.Enter:
+ Type('\n');
+ return true;
+ default:
+ if (args.Character.HasValue)
+ Type(args.Character.Value);
+
+ break;
+ }
+
+ _isCaretVisible = true;
+ return base.OnKeyPressed(context, args);
+ }
+
+ public void Type(char c)
+ {
+ switch (c)
+ {
+ case '\n':
+ var lineText = GetLineText(LineIndex);
+ var left = lineText.Substring(0, ColumnIndex);
+ var right = lineText.Substring(ColumnIndex);
+ _lines.Insert(LineIndex + 1, new StringBuilder(right));
+ _lines[LineIndex] = new StringBuilder(left);
+ LineIndex++;
+ Home();
+ break;
+ case '\t':
+ Tab();
+ break;
+ default:
+ _lines[LineIndex].Insert(ColumnIndex, c);
+ ColumnIndex++;
+ break;
+ }
+ }
+
+ public void Backspace()
+ {
+ if (ColumnIndex == 0 && LineIndex > 0)
+ {
+ var topLineLength = GetLineLength(LineIndex - 1);
+
+ if (RemoveLineBreak(LineIndex - 1))
+ {
+ LineIndex--;
+ ColumnIndex = topLineLength;
+ }
+ }
+ else if (Left())
+ {
+ RemoveCharacter(LineIndex, ColumnIndex);
+ }
+ }
+
+ public void Delete()
+ {
+ var lineLength = GetLineLength(LineIndex);
+
+ if (ColumnIndex == lineLength)
+ RemoveLineBreak(LineIndex);
+ else
+ RemoveCharacter(LineIndex, ColumnIndex);
+ }
+
+ public void RemoveCharacter(int lineIndex, int columnIndex)
+ {
+ _lines[lineIndex].Remove(columnIndex, 1);
+ }
+
+ public bool RemoveLineBreak(int lineIndex)
+ {
+ if (lineIndex < _lines.Count - 1)
+ {
+ var topLine = _lines[lineIndex];
+ var bottomLine = _lines[lineIndex + 1];
+ _lines.RemoveAt(lineIndex + 1);
+ topLine.Append(bottomLine);
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool Home()
+ {
+ ColumnIndex = 0;
+ return true;
+ }
+
+ public bool End()
+ {
+ ColumnIndex = GetLineLength(LineIndex);
+ return true;
+ }
+
+ public bool Up()
+ {
+ if (LineIndex > 0)
+ {
+ LineIndex--;
+
+ if (ColumnIndex > GetLineLength(LineIndex))
+ ColumnIndex = GetLineLength(LineIndex);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool Down()
+ {
+ if (LineIndex < _lines.Count - 1)
+ {
+ LineIndex++;
+
+ if (ColumnIndex > GetLineLength(LineIndex))
+ ColumnIndex = GetLineLength(LineIndex);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool Left()
+ {
+ if (ColumnIndex == 0)
+ {
+ if (LineIndex == 0)
+ return false;
+
+ LineIndex--;
+ ColumnIndex = GetLineLength(LineIndex);
+ }
+ else
+ {
+ ColumnIndex--;
+ }
+
+ return true;
+ }
+
+ public bool Right()
+ {
+ if (ColumnIndex == _lines[LineIndex].Length)
+ {
+ if (LineIndex == _lines.Count - 1)
+ return false;
+
+ LineIndex++;
+ ColumnIndex = 0;
+ }
+ else
+ {
+ ColumnIndex++;
+ }
+
+ return true;
+ }
+
+ public bool Tab()
+ {
+ var spaces = TabStops - ColumnIndex % TabStops;
+
+ for (var s = 0; s < spaces; s++)
+ Type(' ');
+
+ return spaces > 0;
+ }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ var text = Text;
+ var textInfo = GetTextInfo(context, text, ContentRectangle, HorizontalTextAlignment, VerticalTextAlignment);
+
+ if (!string.IsNullOrWhiteSpace(textInfo.Text))
+ renderer.DrawText(textInfo.Font, textInfo.Text, textInfo.Position + TextOffset, textInfo.Color, textInfo.ClippingRectangle);
+
+ if (IsFocused)
+ {
+ var caretRectangle = GetCaretRectangle(textInfo);
+
+ if (_isCaretVisible)
+ renderer.DrawRectangle(caretRectangle, TextColor);
+
+ _nextCaretBlink -= deltaSeconds;
+
+ if (_nextCaretBlink <= 0)
+ {
+ _isCaretVisible = !_isCaretVisible;
+ _nextCaretBlink = _caretBlinkRate;
+ }
+ }
+ }
+
+ private Rectangle GetCaretRectangle(TextInfo textInfo)
+ {
+ var font = textInfo.Font;
+ var text = GetLineText(LineIndex);
+ var offset = new Vector2(0, font.LineHeight * LineIndex);
+ var caretRectangle = font.GetStringRectangle(text.Substring(0, ColumnIndex), textInfo.Position + offset);
+
+ // TODO: Finish the caret position stuff when it's outside the clipping rectangle
+ if (caretRectangle.Right > ClippingRectangle.Right)
+ textInfo.Position.X -= caretRectangle.Right - ClippingRectangle.Right;
+
+ caretRectangle.X = caretRectangle.Right < ClippingRectangle.Right ? caretRectangle.Right : ClippingRectangle.Right;
+ caretRectangle.Width = 1;
+
+ if (caretRectangle.Left < ClippingRectangle.Left)
+ {
+ textInfo.Position.X += ClippingRectangle.Left - caretRectangle.Left;
+ caretRectangle.X = ClippingRectangle.Left;
+ }
+
+ return (Rectangle)caretRectangle;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ToggleButton.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ToggleButton.cs
new file mode 100644
index 0000000..5858894
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ToggleButton.cs
@@ -0,0 +1,98 @@
+using System;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class ToggleButton : Button
+ {
+ public ToggleButton()
+ {
+ }
+
+ public event EventHandler CheckedStateChanged;
+
+ private bool _isChecked;
+ public bool IsChecked
+ {
+ get { return _isChecked; }
+ set
+ {
+ if (_isChecked != value)
+ {
+ _isChecked = value;
+ CheckedStyle?.ApplyIf(this, _isChecked);
+ CheckedStateChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ private ControlStyle _checkedStyle;
+ public ControlStyle CheckedStyle
+ {
+ get { return _checkedStyle; }
+ set
+ {
+ if (_checkedStyle != value)
+ {
+ _checkedStyle = value;
+ CheckedStyle?.ApplyIf(this, _isChecked);
+ }
+ }
+ }
+
+ private ControlStyle _checkedHoverStyle;
+ public ControlStyle CheckedHoverStyle
+ {
+ get { return _checkedHoverStyle; }
+ set
+ {
+ if (_checkedHoverStyle != value)
+ {
+ _checkedHoverStyle = value;
+ CheckedHoverStyle?.ApplyIf(this, _isChecked && IsHovered);
+ }
+ }
+ }
+
+ public override bool OnPointerUp(IGuiContext context, PointerEventArgs args)
+ {
+ base.OnPointerUp(context, args);
+
+ if (BoundingRectangle.Contains(args.Position))
+ {
+ HoverStyle?.Revert(this);
+ CheckedHoverStyle?.Revert(this);
+
+ IsChecked = !IsChecked;
+
+ if (IsChecked)
+ CheckedHoverStyle?.Apply(this);
+ else
+ HoverStyle?.Apply(this);
+ }
+
+ return true;
+ }
+
+ public override bool OnPointerEnter(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsChecked)
+ {
+ CheckedHoverStyle?.Apply(this);
+ return true;
+ }
+
+ return base.OnPointerEnter(context, args);
+ }
+
+ public override bool OnPointerLeave(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsChecked)
+ {
+ CheckedHoverStyle?.Revert(this);
+ return true;
+ }
+
+ return base.OnPointerLeave(context, args);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/UniformGrid.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/UniformGrid.cs
new file mode 100644
index 0000000..829969d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/UniformGrid.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Linq;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class UniformGrid : LayoutControl
+ {
+ public UniformGrid()
+ {
+ }
+
+ public int Columns { get; set; }
+ public int Rows { get; set; }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var columns = Columns == 0 ? (int)Math.Ceiling(Math.Sqrt(Items.Count)) : Columns;
+ var rows = Rows == 0 ? (int)Math.Ceiling((float)Items.Count / columns) : Rows;
+ var sizes = Items
+ .Select(control => control.CalculateActualSize(context))
+ .ToArray();
+ var minCellWidth = sizes.Max(s => s.Width);
+ var minCellHeight = sizes.Max(s => s.Height);
+ return new Size(minCellWidth * columns, minCellHeight * rows);
+ }
+
+ protected override void Layout(IGuiContext context, Rectangle rectangle)
+ {
+ var gridInfo = CalculateGridInfo(context, rectangle.Size);
+ var columnIndex = 0;
+ var rowIndex = 0;
+ var cellWidth = HorizontalAlignment == HorizontalAlignment.Stretch ? gridInfo.MaxCellWidth : gridInfo.MinCellWidth;
+ var cellHeight = VerticalAlignment == VerticalAlignment.Stretch ? gridInfo.MaxCellHeight : gridInfo.MinCellHeight;
+
+ foreach (var control in Items)
+ {
+ var x = columnIndex * cellWidth + rectangle.X;
+ var y = rowIndex * cellHeight + rectangle.Y;
+
+ PlaceControl(context, control, x, y, cellWidth, cellHeight);
+ columnIndex++;
+
+ if (columnIndex > gridInfo.Columns - 1)
+ {
+ columnIndex = 0;
+ rowIndex++;
+ }
+ }
+ }
+
+ private struct GridInfo
+ {
+ public float MinCellWidth;
+ public float MinCellHeight;
+ public float MaxCellWidth;
+ public float MaxCellHeight;
+ public float Columns;
+ public float Rows;
+ public Size2 MinCellSize => new Size2(MinCellWidth * Columns, MinCellHeight * Rows);
+ }
+
+ private GridInfo CalculateGridInfo(IGuiContext context, Size2 availableSize)
+ {
+ var columns = Columns == 0 ? (int)Math.Ceiling(Math.Sqrt(Items.Count)) : Columns;
+ var rows = Rows == 0 ? (int)Math.Ceiling((float)Items.Count / columns) : Rows;
+ var maxCellWidth = availableSize.Width / columns;
+ var maxCellHeight = availableSize.Height / rows;
+ var sizes = Items
+ .Select(control => control.CalculateActualSize(context)) // LayoutHelper.GetSizeWithMargins(control, context, new Size2(maxCellWidth, maxCellHeight)))
+ .ToArray();
+ var maxControlWidth = sizes.Length == 0 ? 0 : sizes.Max(s => s.Width);
+ var maxControlHeight = sizes.Length == 0 ? 0 : sizes.Max(s => s.Height);
+
+ return new GridInfo
+ {
+ Columns = columns,
+ Rows = rows,
+ MinCellWidth = Math.Min(maxControlWidth, maxCellWidth),
+ MinCellHeight = Math.Min(maxControlHeight, maxCellHeight),
+ MaxCellWidth = maxCellWidth,
+ MaxCellHeight = maxCellHeight
+ };
+ }
+ }
+} \ No newline at end of file