summaryrefslogtreecommitdiff
path: root/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui
diff options
context:
space:
mode:
Diffstat (limited to 'Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui')
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ControlStyle.cs114
-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
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Cursor.cs11
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Element.cs125
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ElementCollection.cs98
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSpriteBatchRenderer.cs81
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSystem.cs265
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/LayoutHelper.cs65
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Markup/MarkupParser.cs147
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/MonoGame.Extended.Gui.csproj13
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Orientation.cs4
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/PointerEventArgs.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Screen.cs140
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ScreenCollection.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlJsonConverter.cs67
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlStyleJsonConverter.cs89
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs38
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiNinePatchRegion2DJsonConverter.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureAtlasJsonConverter.cs31
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureRegionService.cs16
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/HorizontalAlignmentConverter.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/SkinJsonConverter.cs57
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/VerticalAlignmentConverter.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Skin.cs221
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Window.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/WindowCollection.cs10
48 files changed, 4001 insertions, 0 deletions
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ControlStyle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ControlStyle.cs
new file mode 100644
index 0000000..1593da3
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ControlStyle.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui
+{
+ public class ControlStyle : IDictionary<string, object>
+ {
+ private readonly Dictionary<Guid, Dictionary<string, object>> _previousStates = new Dictionary<Guid, Dictionary<string, object>>();
+
+ public ControlStyle()
+ : this(typeof(Element))
+ {
+ }
+
+ public ControlStyle(Type targetType)
+ : this(targetType.FullName, targetType)
+ {
+ }
+
+ public ControlStyle(string name, Type targetType)
+ {
+ Name = name;
+ TargetType = targetType;
+ _setters = new Dictionary<string, object>();
+ }
+
+ public string Name { get; }
+ public Type TargetType { get; set; }
+
+ private readonly Dictionary<string, object> _setters;
+
+ public void ApplyIf(Control control, bool predicate)
+ {
+ if (predicate)
+ Apply(control);
+ else
+ Revert(control);
+ }
+
+ public void Apply(Control control)
+ {
+ _previousStates[control.Id] = _setters
+ .ToDictionary(i => i.Key, i => TargetType.GetRuntimeProperty(i.Key)?.GetValue(control));
+
+ ChangePropertyValues(control, _setters);
+ }
+
+ public void Revert(Control control)
+ {
+ if (_previousStates.ContainsKey(control.Id) && _previousStates[control.Id] != null)
+ ChangePropertyValues(control, _previousStates[control.Id]);
+
+ _previousStates[control.Id] = null;
+ }
+
+ private static void ChangePropertyValues(Control control, Dictionary<string, object> setters)
+ {
+ var targetType = control.GetType();
+
+ foreach (var propertyName in setters.Keys)
+ {
+ var propertyInfo = targetType.GetRuntimeProperty(propertyName);
+ var value = setters[propertyName];
+
+ if (propertyInfo != null)
+ {
+ if(propertyInfo.CanWrite)
+ propertyInfo.SetValue(control, value);
+
+ // special case when we have a list of items as objects (like on a list box)
+ if (propertyInfo.PropertyType == typeof(List<object>))
+ {
+ var items = (List<object>)value;
+ var addMethod = propertyInfo.PropertyType.GetRuntimeMethod("Add", new[] { typeof(object) });
+
+ foreach (var item in items)
+ addMethod.Invoke(propertyInfo.GetValue(control), new[] {item});
+ }
+ }
+ }
+ }
+
+ public object this[string key]
+ {
+ get { return _setters[key]; }
+ set { _setters[key] = value; }
+ }
+
+ public ICollection<string> Keys => _setters.Keys;
+ public ICollection<object> Values => _setters.Values;
+ public int Count => _setters.Count;
+ public bool IsReadOnly => false;
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ public IEnumerator<KeyValuePair<string, object>> GetEnumerator() => _setters.GetEnumerator();
+ public void Add(string key, object value) => _setters.Add(key, value);
+ public void Add(KeyValuePair<string, object> item) => _setters.Add(item.Key, item.Value);
+ public bool Remove(string key) => _setters.Remove(key);
+ public bool Remove(KeyValuePair<string, object> item) => _setters.Remove(item.Key);
+ public void Clear() => _setters.Clear();
+ public bool Contains(KeyValuePair<string, object> item) => _setters.Contains(item);
+ public bool ContainsKey(string key) => _setters.ContainsKey(key);
+ public bool TryGetValue(string key, out object value) => _setters.TryGetValue(key, out value);
+
+ public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
+ {
+ throw new NotSupportedException();
+ }
+ }
+} \ No newline at end of file
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
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Cursor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Cursor.cs
new file mode 100644
index 0000000..638baec
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Cursor.cs
@@ -0,0 +1,11 @@
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui
+{
+ public class Cursor
+ {
+ public TextureRegion2D TextureRegion { get; set; }
+ public Color Color { get; set; } = Color.White;
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Element.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Element.cs
new file mode 100644
index 0000000..0c364a9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Element.cs
@@ -0,0 +1,125 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Reflection;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui
+{
+ public class Binding
+ {
+ public Binding(object viewModel, string viewModelProperty, string viewProperty)
+ {
+ ViewModel = viewModel;
+ ViewModelProperty = viewModelProperty;
+ ViewProperty = viewProperty;
+ }
+
+ public object ViewModel { get; }
+ public string ViewModelProperty { get; }
+ public string ViewProperty { get; }
+ }
+
+ public abstract class Element
+ {
+ public string Name { get; set; }
+ public Point Position { get; set; }
+ public Point Origin { get; set; }
+ public Color BackgroundColor { get; set; }
+ public Color BorderColor { get; set; } = Color.White;
+ public int BorderThickness { get; set; } = 0;
+
+ private TextureRegion2D _backgroundRegion;
+ public TextureRegion2D BackgroundRegion
+ {
+ get => _backgroundRegion;
+ set
+ {
+ _backgroundRegion = value;
+
+ if (_backgroundRegion != null && BackgroundColor == Color.Transparent)
+ BackgroundColor = Color.White;
+ }
+ }
+
+ public List<Binding> Bindings { get; } = new List<Binding>();
+
+ protected void OnPropertyChanged(string propertyName)
+ {
+ foreach (var binding in Bindings)
+ {
+ if (binding.ViewProperty == propertyName)
+ {
+ var value = GetType()
+ .GetTypeInfo()
+ .GetDeclaredProperty(binding.ViewProperty)
+ .GetValue(this);
+
+ binding.ViewModel
+ .GetType()
+ .GetTypeInfo()
+ .GetDeclaredProperty(binding.ViewModelProperty)
+ .SetValue(binding.ViewModel, value);
+ }
+ }
+ }
+
+ private Size _size;
+ public Size Size
+ {
+ get => _size;
+ set
+ {
+ _size = value;
+ OnSizeChanged();
+ }
+ }
+
+ protected virtual void OnSizeChanged() { }
+
+ public int MinWidth { get; set; }
+ public int MinHeight { get; set; }
+ public int MaxWidth { get; set; } = int.MaxValue;
+ public int MaxHeight { get; set; } = int.MaxValue;
+
+ public int Width
+ {
+ get => Size.Width;
+ set => Size = new Size(value, Size.Height);
+ }
+
+ public int Height
+ {
+ get => Size.Height;
+ set => Size = new Size(Size.Width, value);
+ }
+
+ public Size ActualSize { get; internal set; }
+
+ public abstract void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds);
+ }
+
+ public abstract class Element<TParent> : Element, IRectangular
+ where TParent : IRectangular
+ {
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [JsonIgnore]
+ public TParent Parent { get; internal set; }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [JsonIgnore]
+ public Rectangle BoundingRectangle
+ {
+ get
+ {
+ var offset = Point.Zero;
+
+ if (Parent != null)
+ offset = Parent.BoundingRectangle.Location;
+
+ return new Rectangle(offset + Position - ActualSize * Origin, ActualSize);
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ElementCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ElementCollection.cs
new file mode 100644
index 0000000..009161d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ElementCollection.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui
+{
+ public abstract class ElementCollection<TChild, TParent> : IList<TChild>
+ where TParent : class, IRectangular
+ where TChild : Element<TParent>
+ {
+ private readonly TParent _parent;
+ private readonly List<TChild> _list = new List<TChild>();
+
+ public Action<TChild> ItemAdded { get; set; }
+ public Action<TChild> ItemRemoved { get; set; }
+
+ protected ElementCollection(TParent parent)
+ {
+ _parent = parent;
+ }
+
+ public IEnumerator<TChild> GetEnumerator()
+ {
+ return _list.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable)_list).GetEnumerator();
+ }
+
+ public void Add(TChild item)
+ {
+ item.Parent = _parent;
+ _list.Add(item);
+ ItemAdded?.Invoke(item);
+ }
+
+ public void Clear()
+ {
+ foreach (var child in _list)
+ {
+ child.Parent = null;
+ ItemRemoved?.Invoke(child);
+ }
+
+ _list.Clear();
+ }
+
+ public bool Contains(TChild item)
+ {
+ return _list.Contains(item);
+ }
+
+ public void CopyTo(TChild[] array, int arrayIndex)
+ {
+ _list.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(TChild item)
+ {
+ item.Parent = null;
+ ItemRemoved?.Invoke(item);
+ return _list.Remove(item);
+ }
+
+ public int Count => _list.Count;
+
+ public bool IsReadOnly => ((ICollection<Control>)_list).IsReadOnly;
+
+ public int IndexOf(TChild item)
+ {
+ return _list.IndexOf(item);
+ }
+
+ public void Insert(int index, TChild item)
+ {
+ item.Parent = _parent;
+ _list.Insert(index, item);
+ ItemAdded?.Invoke(item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ var child = _list[index];
+ child.Parent = null;
+ _list.RemoveAt(index);
+ ItemRemoved?.Invoke(child);
+ }
+
+ public TChild this[int index]
+ {
+ get { return _list[index]; }
+ set { _list[index] = value; }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSpriteBatchRenderer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSpriteBatchRenderer.cs
new file mode 100644
index 0000000..6d1e8ce
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSpriteBatchRenderer.cs
@@ -0,0 +1,81 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.BitmapFonts;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui
+{
+ public interface IGuiRenderer
+ {
+ void Begin();
+ void DrawRegion(TextureRegion2D textureRegion, Rectangle rectangle, Color color, Rectangle? clippingRectangle = null);
+ void DrawRegion(TextureRegion2D textureRegion, Vector2 position, Color color, Rectangle? clippingRectangle = null);
+ void DrawText(BitmapFont font, string text, Vector2 position, Color color, Rectangle? clippingRectangle = null);
+ void DrawRectangle(Rectangle rectangle, Color color, float thickness = 1f, Rectangle? clippingRectangle = null);
+ void FillRectangle(Rectangle rectangle, Color color, Rectangle? clippingRectangle = null);
+ void End();
+ }
+
+ public class GuiSpriteBatchRenderer : IGuiRenderer
+ {
+ private readonly Func<Matrix> _getTransformMatrix;
+ private readonly SpriteBatch _spriteBatch;
+
+ public GuiSpriteBatchRenderer(GraphicsDevice graphicsDevice, Func<Matrix> getTransformMatrix)
+ {
+ _getTransformMatrix = getTransformMatrix;
+ _spriteBatch = new SpriteBatch(graphicsDevice);
+ }
+
+ public SpriteSortMode SortMode { get; set; }
+ public BlendState BlendState { get; set; } = BlendState.AlphaBlend;
+ public SamplerState SamplerState { get; set; } = SamplerState.PointClamp;
+ public DepthStencilState DepthStencilState { get; set; } = DepthStencilState.Default;
+ public RasterizerState RasterizerState { get; set; } = RasterizerState.CullNone;
+ public Effect Effect { get; set; }
+
+ public void Begin()
+ {
+ _spriteBatch.Begin(SortMode, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect, _getTransformMatrix());
+ }
+
+ public void End()
+ {
+ _spriteBatch.End();
+ }
+
+ public void DrawRegion(TextureRegion2D textureRegion, Rectangle rectangle, Color color, Rectangle? clippingRectangle = null)
+ {
+ if (textureRegion != null)
+ _spriteBatch.Draw(textureRegion, rectangle, color, clippingRectangle);
+ }
+
+ public void DrawRegion(TextureRegion2D textureRegion, Vector2 position, Color color, Rectangle? clippingRectangle = null)
+ {
+ if (textureRegion != null)
+ _spriteBatch.Draw(textureRegion, position, color, clippingRectangle);
+ }
+
+ public void DrawText(BitmapFont font, string text, Vector2 position, Color color, Rectangle? clippingRectangle = null)
+ {
+ _spriteBatch.DrawString(font, text, position, color, clippingRectangle);
+ }
+
+ public void DrawRectangle(Rectangle rectangle, Color color, float thickness = 1f, Rectangle? clippingRectangle = null)
+ {
+ if (clippingRectangle.HasValue)
+ rectangle = rectangle.Clip(clippingRectangle.Value);
+
+ _spriteBatch.DrawRectangle(rectangle, color, thickness);
+ }
+
+ public void FillRectangle(Rectangle rectangle, Color color, Rectangle? clippingRectangle = null)
+ {
+ if (clippingRectangle.HasValue)
+ rectangle = rectangle.Clip(clippingRectangle.Value);
+
+ _spriteBatch.FillRectangle(rectangle, color);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSystem.cs
new file mode 100644
index 0000000..8b522fb
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSystem.cs
@@ -0,0 +1,265 @@
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.BitmapFonts;
+using MonoGame.Extended.Gui.Controls;
+using MonoGame.Extended.Input.InputListeners;
+using MonoGame.Extended.ViewportAdapters;
+using System;
+using System.Linq;
+
+namespace MonoGame.Extended.Gui
+{
+ public interface IGuiContext
+ {
+ BitmapFont DefaultFont { get; }
+ Vector2 CursorPosition { get; }
+ Control FocusedControl { get; }
+
+ void SetFocus(Control focusedControl);
+ }
+
+ public class GuiSystem : IGuiContext, IRectangular
+ {
+ private readonly ViewportAdapter _viewportAdapter;
+ private readonly IGuiRenderer _renderer;
+ private readonly MouseListener _mouseListener;
+ private readonly TouchListener _touchListener;
+ private readonly KeyboardListener _keyboardListener;
+
+ private Control _preFocusedControl;
+
+ public GuiSystem(ViewportAdapter viewportAdapter, IGuiRenderer renderer)
+ {
+ _viewportAdapter = viewportAdapter;
+ _renderer = renderer;
+
+ _mouseListener = new MouseListener(viewportAdapter);
+ _mouseListener.MouseDown += (s, e) => OnPointerDown(PointerEventArgs.FromMouseArgs(e));
+ _mouseListener.MouseMoved += (s, e) => OnPointerMoved(PointerEventArgs.FromMouseArgs(e));
+ _mouseListener.MouseUp += (s, e) => OnPointerUp(PointerEventArgs.FromMouseArgs(e));
+ _mouseListener.MouseWheelMoved += (s, e) => FocusedControl?.OnScrolled(e.ScrollWheelDelta);
+
+ _touchListener = new TouchListener(viewportAdapter);
+ _touchListener.TouchStarted += (s, e) => OnPointerDown(PointerEventArgs.FromTouchArgs(e));
+ _touchListener.TouchMoved += (s, e) => OnPointerMoved(PointerEventArgs.FromTouchArgs(e));
+ _touchListener.TouchEnded += (s, e) => OnPointerUp(PointerEventArgs.FromTouchArgs(e));
+
+ _keyboardListener = new KeyboardListener();
+ _keyboardListener.KeyTyped += (sender, args) => PropagateDown(FocusedControl, x => x.OnKeyTyped(this, args));
+ _keyboardListener.KeyPressed += (sender, args) => PropagateDown(FocusedControl, x => x.OnKeyPressed(this, args));
+ }
+
+ public Control FocusedControl { get; private set; }
+ public Control HoveredControl { get; private set; }
+
+ private Screen _activeScreen;
+ public Screen ActiveScreen
+ {
+ get => _activeScreen;
+ set
+ {
+ if (_activeScreen != value)
+ {
+ _activeScreen = value;
+
+ if(_activeScreen != null)
+ InitializeScreen(_activeScreen);
+ }
+ }
+ }
+
+ public Rectangle BoundingRectangle => _viewportAdapter.BoundingRectangle;
+
+ public Vector2 CursorPosition { get; set; }
+
+ public BitmapFont DefaultFont => Skin.Default?.DefaultFont;
+
+ private void InitializeScreen(Screen screen)
+ {
+ screen.Layout(this, BoundingRectangle);
+ }
+
+ public void ClientSizeChanged()
+ {
+ //ActiveScreen?.Content?.InvalidateMeasure();
+ ActiveScreen?.Layout(this, BoundingRectangle);
+ }
+
+ public void Update(GameTime gameTime)
+ {
+ if(ActiveScreen == null)
+ return;
+
+ _touchListener.Update(gameTime);
+ _mouseListener.Update(gameTime);
+ _keyboardListener.Update(gameTime);
+
+ var deltaSeconds = gameTime.GetElapsedSeconds();
+
+ if (ActiveScreen != null && ActiveScreen.IsVisible)
+ UpdateControl(ActiveScreen.Content, deltaSeconds);
+
+ //if (ActiveScreen.IsLayoutRequired)
+ // ActiveScreen.Layout(this, BoundingRectangle);
+
+ ActiveScreen.Update(gameTime);
+ }
+
+ public void Draw(GameTime gameTime)
+ {
+ var deltaSeconds = gameTime.GetElapsedSeconds();
+
+ _renderer.Begin();
+
+ if (ActiveScreen != null && ActiveScreen.IsVisible)
+ {
+ DrawControl(ActiveScreen.Content, deltaSeconds);
+ //DrawWindows(ActiveScreen.Windows, deltaSeconds);
+ }
+
+ var cursor = Skin.Default?.Cursor;
+
+ if (cursor != null)
+ _renderer.DrawRegion(cursor.TextureRegion, CursorPosition, cursor.Color);
+
+ _renderer.End();
+ }
+
+ //private void DrawWindows(WindowCollection windows, float deltaSeconds)
+ //{
+ // foreach (var window in windows)
+ // {
+ // window.Draw(this, _renderer, deltaSeconds);
+ // DrawChildren(window.Controls, deltaSeconds);
+ // }
+ //}
+
+ public void UpdateControl(Control control, float deltaSeconds)
+ {
+ if (control.IsVisible)
+ {
+ control.Update(this, deltaSeconds);
+
+ foreach (var childControl in control.Children)
+ UpdateControl(childControl, deltaSeconds);
+ }
+ }
+
+ private void DrawControl(Control control, float deltaSeconds)
+ {
+ if (control.IsVisible)
+ {
+ control.Draw(this, _renderer, deltaSeconds);
+
+ foreach (var childControl in control.Children)
+ DrawControl(childControl, deltaSeconds);
+ }
+ }
+
+ private void OnPointerDown(PointerEventArgs args)
+ {
+ if (ActiveScreen == null || !ActiveScreen.IsVisible)
+ return;
+
+ _preFocusedControl = FindControlAtPoint(args.Position);
+ PropagateDown(HoveredControl, x => x.OnPointerDown(this, args));
+ }
+
+ private void OnPointerUp(PointerEventArgs args)
+ {
+ if (ActiveScreen == null || !ActiveScreen.IsVisible)
+ return;
+
+ var postFocusedControl = FindControlAtPoint(args.Position);
+
+ if (_preFocusedControl == postFocusedControl)
+ {
+ SetFocus(postFocusedControl);
+ }
+
+ _preFocusedControl = null;
+ PropagateDown(HoveredControl, x => x.OnPointerUp(this, args));
+ }
+
+ private void OnPointerMoved(PointerEventArgs args)
+ {
+ CursorPosition = args.Position.ToVector2();
+
+ if (ActiveScreen == null || !ActiveScreen.IsVisible)
+ return;
+
+ var hoveredControl = FindControlAtPoint(args.Position);
+
+ if (HoveredControl != hoveredControl)
+ {
+ if (HoveredControl != null && (hoveredControl == null || !hoveredControl.HasParent(HoveredControl)))
+ PropagateDown(HoveredControl, x => x.OnPointerLeave(this, args));
+
+ HoveredControl = hoveredControl;
+ PropagateDown(HoveredControl, x => x.OnPointerEnter(this, args));
+ }
+ else
+ {
+ PropagateDown(HoveredControl, x => x.OnPointerMove(this, args));
+ }
+ }
+
+ public void SetFocus(Control focusedControl)
+ {
+ if (FocusedControl != focusedControl)
+ {
+ if (FocusedControl != null)
+ {
+ FocusedControl.IsFocused = false;
+ PropagateDown(FocusedControl, x => x.OnUnfocus(this));
+ }
+
+ FocusedControl = focusedControl;
+
+ if (FocusedControl != null)
+ {
+ FocusedControl.IsFocused = true;
+ PropagateDown(FocusedControl, x => x.OnFocus(this));
+ }
+ }
+ }
+
+ /// <summary>
+ /// Method is meant to loop down the parents control to find a suitable event control. If the predicate returns false
+ /// it will continue down the control tree.
+ /// </summary>
+ /// <param name="control">The control we want to check against</param>
+ /// <param name="predicate">A function to check if the propagation should resume, if returns false it will continue down the tree.</param>
+ private static void PropagateDown(Control control, Func<Control, bool> predicate)
+ {
+ while(control != null && predicate(control))
+ {
+ control = control.Parent;
+ }
+ }
+
+ private Control FindControlAtPoint(Point point)
+ {
+ if (ActiveScreen == null || !ActiveScreen.IsVisible)
+ return null;
+
+ return FindControlAtPoint(ActiveScreen.Content, point);
+ }
+
+ private Control FindControlAtPoint(Control control, Point point)
+ {
+ foreach (var controlChild in control.Children.Reverse())
+ {
+ var c = FindControlAtPoint(controlChild, point);
+
+ if (c != null)
+ return c;
+ }
+
+
+ if (control.IsVisible && control.Contains(this, point))
+ return control;
+
+ return null;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/LayoutHelper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/LayoutHelper.cs
new file mode 100644
index 0000000..1268bff
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/LayoutHelper.cs
@@ -0,0 +1,65 @@
+using System;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui
+{
+ public enum HorizontalAlignment { Left, Right, Centre, Stretch }
+ public enum VerticalAlignment { Top, Bottom, Centre, Stretch }
+
+ public static class LayoutHelper
+ {
+ public static void PlaceControl(IGuiContext context, Control control, float x, float y, float width, float height)
+ {
+ var rectangle = new Rectangle((int)x, (int)y, (int)width, (int)height);
+ var desiredSize = control.CalculateActualSize(context);
+ var alignedRectangle = AlignRectangle(control.HorizontalAlignment, control.VerticalAlignment, desiredSize, rectangle);
+
+ control.Position = new Point(control.Margin.Left + alignedRectangle.X, control.Margin.Top + alignedRectangle.Y);
+ control.ActualSize = (Size)alignedRectangle.Size - control.Margin.Size;
+ control.InvalidateMeasure();
+ }
+
+ public static Rectangle AlignRectangle(HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment, Size size, Rectangle targetRectangle)
+ {
+ var x = GetHorizontalPosition(horizontalAlignment, size, targetRectangle);
+ var y = GetVerticalPosition(verticalAlignment, size, targetRectangle);
+ var width = horizontalAlignment == HorizontalAlignment.Stretch ? targetRectangle.Width : size.Width;
+ var height = verticalAlignment == VerticalAlignment.Stretch ? targetRectangle.Height : size.Height;
+
+ return new Rectangle(x, y, width, height);
+ }
+
+ public static int GetHorizontalPosition(HorizontalAlignment horizontalAlignment, Size size, Rectangle targetRectangle)
+ {
+ switch (horizontalAlignment)
+ {
+ case HorizontalAlignment.Stretch:
+ case HorizontalAlignment.Left:
+ return targetRectangle.X;
+ case HorizontalAlignment.Right:
+ return targetRectangle.Right - size.Width;
+ case HorizontalAlignment.Centre:
+ return targetRectangle.X + targetRectangle.Width / 2 - size.Width / 2;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(horizontalAlignment), horizontalAlignment, $"{horizontalAlignment} is not supported");
+ }
+ }
+
+ public static int GetVerticalPosition(VerticalAlignment verticalAlignment, Size size, Rectangle targetRectangle)
+ {
+ switch (verticalAlignment)
+ {
+ case VerticalAlignment.Stretch:
+ case VerticalAlignment.Top:
+ return targetRectangle.Y;
+ case VerticalAlignment.Bottom:
+ return targetRectangle.Bottom - size.Height;
+ case VerticalAlignment.Centre:
+ return targetRectangle.Y + targetRectangle.Height / 2 - size.Height / 2;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(verticalAlignment), verticalAlignment, $"{verticalAlignment} is not supported");
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Markup/MarkupParser.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Markup/MarkupParser.cs
new file mode 100644
index 0000000..c2e8776
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Markup/MarkupParser.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Xml;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui.Markup
+{
+ public class MarkupParser
+ {
+ public MarkupParser()
+ {
+ }
+
+ private static readonly Dictionary<string, Type> _controlTypes =
+ typeof(Control).Assembly
+ .ExportedTypes
+ .Where(t => t.IsSubclassOf(typeof(Control)))
+ .ToDictionary(t => t.Name, StringComparer.OrdinalIgnoreCase);
+
+ private static readonly Dictionary<Type, Func<string, object>> _converters =
+ new Dictionary<Type, Func<string, object>>
+ {
+ {typeof(object), s => s},
+ {typeof(string), s => s},
+ {typeof(bool), s => bool.Parse(s)},
+ {typeof(int), s => int.Parse(s)},
+ {typeof(Color), s => s.StartsWith("#") ? ColorHelper.FromHex(s) : ColorHelper.FromName(s)}
+ };
+
+ private static object ConvertValue(Type propertyType, string input, object dataContext)
+ {
+ var value = ParseBinding(input, dataContext);
+
+ if (_converters.TryGetValue(propertyType, out var converter))
+ return converter(value); //property.SetValue(control, converter(value));
+
+ if (propertyType.IsEnum)
+ return
+ Enum.Parse(propertyType, value,
+ true); // property.SetValue(control, Enum.Parse(propertyType, value, true));
+
+ throw new InvalidOperationException($"Converter not found for {propertyType}");
+ }
+
+ private static object ParseChildNode(XmlNode node, Control parent, object dataContext)
+ {
+ if (node is XmlText)
+ return node.InnerText.Trim();
+
+ if (_controlTypes.TryGetValue(node.Name, out var type))
+ {
+ var typeInfo = type.GetTypeInfo();
+ var control = (Control) Activator.CreateInstance(type);
+
+ // ReSharper disable once AssignNullToNotNullAttribute
+ foreach (var attribute in node.Attributes.Cast<XmlAttribute>())
+ {
+ var property = typeInfo.GetProperty(attribute.Name);
+
+ if (property != null)
+ {
+ var value = ConvertValue(property.PropertyType, attribute.Value, dataContext);
+ property.SetValue(control, value);
+ }
+ else
+ {
+ var parts = attribute.Name.Split('.');
+ var parentType = parts[0];
+ var propertyName = parts[1];
+ var propertyType = parent.GetAttachedPropertyType(propertyName);
+ var propertyValue = ConvertValue(propertyType, attribute.Value, dataContext);
+
+ if (!string.Equals(parent.GetType().Name, parentType, StringComparison.OrdinalIgnoreCase))
+ throw new InvalidOperationException(
+ $"Attached properties are only supported on the immediate parent type {parentType}");
+
+ control.SetAttachedProperty(propertyName, propertyValue);
+ }
+ }
+
+
+ if (node.HasChildNodes)
+ {
+ switch (control)
+ {
+ case ContentControl contentControl:
+ if (node.ChildNodes.Count > 1)
+ throw new InvalidOperationException("A content control can only have one child");
+
+ contentControl.Content = ParseChildNode(node.ChildNodes[0], control, dataContext);
+ break;
+ case LayoutControl layoutControl:
+ foreach (var childControl in ParseChildNodes(node.ChildNodes, control, dataContext))
+ layoutControl.Items.Add(childControl as Control);
+ break;
+ }
+ }
+
+ return control;
+ }
+
+ throw new InvalidOperationException($"Unknown control type {node.Name}");
+ }
+
+ private static string ParseBinding(string expression, object dataContext)
+ {
+ if (dataContext != null && expression.StartsWith("{{") && expression.EndsWith("}}"))
+ {
+ var binding = expression.Substring(2, expression.Length - 4);
+ var bindingValue = dataContext
+ .GetType()
+ .GetProperty(binding)
+ ?.GetValue(dataContext);
+
+ return $"{bindingValue}";
+ }
+
+ return expression;
+ }
+
+ private static IEnumerable<object> ParseChildNodes(XmlNodeList nodes, Control parent, object dataContext)
+ {
+ foreach (var node in nodes.Cast<XmlNode>())
+ {
+ if (node.Name == "xml")
+ {
+ // TODO: Validate header
+ }
+ else
+ {
+ yield return ParseChildNode(node, parent, dataContext);
+ }
+ }
+ }
+
+ public Control Parse(string filePath, object dataContext)
+ {
+ var d = new XmlDocument();
+ d.Load(filePath);
+ return ParseChildNodes(d.ChildNodes, null, dataContext)
+ .LastOrDefault() as Control;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/MonoGame.Extended.Gui.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/MonoGame.Extended.Gui.csproj
new file mode 100644
index 0000000..d319cb4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/MonoGame.Extended.Gui.csproj
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>A GUI system for MonoGame written from the ground up to make MonoGame more awesome.</Description>
+ <PackageTags>monogame gui</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended.Input\MonoGame.Extended.Input.csproj" />
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Orientation.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Orientation.cs
new file mode 100644
index 0000000..a12599f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Orientation.cs
@@ -0,0 +1,4 @@
+namespace MonoGame.Extended.Gui
+{
+ public enum Orientation { Horizontal, Vertical }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/PointerEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/PointerEventArgs.cs
new file mode 100644
index 0000000..18cffac
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/PointerEventArgs.cs
@@ -0,0 +1,42 @@
+using System;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Input;
+using MonoGame.Extended.Input.InputListeners;
+
+namespace MonoGame.Extended.Gui
+{
+ public class PointerEventArgs : EventArgs
+ {
+ private PointerEventArgs()
+ {
+ }
+
+ public Point Position { get; private set; }
+ public MouseButton Button { get; private set; }
+ public int ScrollWheelDelta { get; private set; }
+ public int ScrollWheelValue { get; private set; }
+ public TimeSpan Time { get; private set; }
+
+ public static PointerEventArgs FromMouseArgs(MouseEventArgs args)
+ {
+ return new PointerEventArgs
+ {
+ Position = args.Position,
+ Button = args.Button,
+ ScrollWheelDelta = args.ScrollWheelDelta,
+ ScrollWheelValue = args.ScrollWheelValue,
+ Time = args.Time
+ };
+ }
+
+ public static PointerEventArgs FromTouchArgs(TouchEventArgs args)
+ {
+ return new PointerEventArgs
+ {
+ Position = args.Position,
+ Button = MouseButton.Left,
+ Time = args.Time
+ };
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Screen.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Screen.cs
new file mode 100644
index 0000000..821a1e2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Screen.cs
@@ -0,0 +1,140 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using MonoGame.Extended.Gui.Controls;
+using MonoGame.Extended.Gui.Serialization;
+
+namespace MonoGame.Extended.Gui
+{
+ public class Screen //: Element<GuiSystem>, IDisposable
+ {
+ public Screen()
+ {
+ //Windows = new WindowCollection(this) { ItemAdded = w => _isLayoutRequired = true };
+ }
+
+ public virtual void Dispose()
+ {
+ }
+
+ private Control _content;
+ [JsonPropertyOrder(1)]
+ public Control Content
+ {
+ get { return _content; }
+ set
+ {
+ if (_content != value)
+ {
+ _content = value;
+ _isLayoutRequired = true;
+ }
+ }
+ }
+
+ //[JsonIgnore]
+ //public WindowCollection Windows { get; }
+
+ public float Width { get; private set; }
+ public float Height { get; private set; }
+ public Size2 Size => new Size2(Width, Height);
+ public bool IsVisible { get; set; } = true;
+
+ private bool _isLayoutRequired;
+ [JsonIgnore]
+ public bool IsLayoutRequired => _isLayoutRequired || Content.IsLayoutRequired;
+
+ public virtual void Update(GameTime gameTime)
+ {
+
+ }
+
+ public void Show()
+ {
+ IsVisible = true;
+ }
+
+ public void Hide()
+ {
+ IsVisible = false;
+ }
+
+ public T FindControl<T>(string name)
+ where T : Control
+ {
+ return FindControl<T>(Content, name);
+ }
+
+ private static T FindControl<T>(Control rootControl, string name)
+ where T : Control
+ {
+ if (rootControl.Name == name)
+ return rootControl as T;
+
+ foreach (var childControl in rootControl.Children)
+ {
+ var control = FindControl<T>(childControl, name);
+
+ if (control != null)
+ return control;
+ }
+
+ return null;
+ }
+
+ public void Layout(IGuiContext context, Rectangle rectangle)
+ {
+ Width = rectangle.Width;
+ Height = rectangle.Height;
+
+ LayoutHelper.PlaceControl(context, Content, rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+
+ //foreach (var window in Windows)
+ // LayoutHelper.PlaceWindow(context, window, rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+
+ _isLayoutRequired = false;
+ Content.IsLayoutRequired = false;
+ }
+
+ //public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ //{
+ // renderer.DrawRectangle(BoundingRectangle, Color.Green);
+ //}
+
+ public static Screen FromStream(ContentManager contentManager, Stream stream, params Type[] customControlTypes)
+ {
+ return FromStream<Screen>(contentManager, stream, customControlTypes);
+ }
+
+ public static TScreen FromStream<TScreen>(ContentManager contentManager, Stream stream, params Type[] customControlTypes)
+ where TScreen : Screen
+ {
+ var skinService = new SkinService();
+ var options = GuiJsonSerializerOptionsProvider.GetOptions(contentManager, customControlTypes);
+ options.Converters.Add(new SkinJsonConverter(contentManager, skinService, customControlTypes));
+ options.Converters.Add(new ControlJsonConverter(skinService, customControlTypes));
+ return JsonSerializer.Deserialize<TScreen>(stream, options);
+ }
+
+ public static Screen FromFile(ContentManager contentManager, string path, params Type[] customControlTypes)
+ {
+ using (var stream = TitleContainer.OpenStream(path))
+ {
+ return FromStream<Screen>(contentManager, stream, customControlTypes);
+ }
+ }
+
+ public static TScreen FromFile<TScreen>(ContentManager contentManager, string path, params Type[] customControlTypes)
+ where TScreen : Screen
+ {
+ using (var stream = TitleContainer.OpenStream(path))
+ {
+ return FromStream<TScreen>(contentManager, stream, customControlTypes);
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ScreenCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ScreenCollection.cs
new file mode 100644
index 0000000..0572728
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ScreenCollection.cs
@@ -0,0 +1,10 @@
+namespace MonoGame.Extended.Gui
+{
+ //public class ScreenCollection : ElementCollection<Screen, GuiSystem>
+ //{
+ // public ScreenCollection(GuiSystem parent)
+ // : base(parent)
+ // {
+ // }
+ //}
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlJsonConverter.cs
new file mode 100644
index 0000000..a9c71f8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlJsonConverter.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui.Serialization
+{
+ public class ControlJsonConverter : JsonConverter<Control>
+ {
+ private readonly IGuiSkinService _guiSkinService;
+ private readonly ControlStyleJsonConverter _styleConverter;
+ private const string _styleProperty = "Style";
+
+ public ControlJsonConverter(IGuiSkinService guiSkinService, params Type[] customControlTypes)
+ {
+ _guiSkinService = guiSkinService;
+ _styleConverter = new ControlStyleJsonConverter(customControlTypes);
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Control);
+
+ /// <inheritdoc />
+ public override Control Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var style = _styleConverter.Read(ref reader, typeToConvert, options);
+ var template = GetControlTemplate(style);
+ var skin = _guiSkinService.Skin;
+ var control = skin.Create(style.TargetType, template);
+
+ var itemsControl = control as ItemsControl;
+ if (itemsControl != null)
+ {
+ object childControls;
+
+ if (style.TryGetValue(nameof(ItemsControl.Items), out childControls))
+ {
+ var controlCollection = childControls as ControlCollection;
+
+ if (controlCollection != null)
+ {
+ foreach (var child in controlCollection)
+ itemsControl.Items.Add(child);
+ }
+ }
+ }
+
+ style.Apply(control);
+ return control;
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, Control value, JsonSerializerOptions options) { }
+
+
+
+ private static string GetControlTemplate(ControlStyle style)
+ {
+ object template;
+
+ if (style.TryGetValue(_styleProperty, out template))
+ return template as string;
+
+ return null;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlStyleJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlStyleJsonConverter.cs
new file mode 100644
index 0000000..c31549b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlStyleJsonConverter.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using MonoGame.Extended.Collections;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui.Serialization
+{
+ public class ControlStyleJsonConverter : JsonConverter<ControlStyle>
+ {
+ private readonly Dictionary<string, Type> _controlTypes;
+ private const string _typeProperty = "Type";
+ private const string _nameProperty = "Name";
+
+ public ControlStyleJsonConverter(params Type[] customControlTypes)
+ {
+ _controlTypes = typeof(Control)
+ .GetTypeInfo()
+ .Assembly
+ .ExportedTypes
+ .Concat(customControlTypes)
+ .Where(t => t.GetTypeInfo().IsSubclassOf(typeof(Control)))
+ .ToDictionary(t => t.Name);
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(ControlStyle);
+
+ /// <inheritdoc />
+ public override ControlStyle Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var dictionary = JsonSerializer.Deserialize<Dictionary<string, object>>(ref reader, options);
+ var name = dictionary.GetValueOrDefault(_nameProperty) as string;
+ var typeName = dictionary.GetValueOrDefault(_typeProperty) as string;
+
+ if (!_controlTypes.TryGetValue(typeName, out Type controlType))
+ throw new FormatException("invalid control type: " + typeName);
+
+ var targetType = typeName != null ? controlType : typeof(Control);
+ var properties = targetType
+ .GetRuntimeProperties()
+ .ToDictionary(p => p.Name);
+ var style = new ControlStyle(name, targetType);
+
+ foreach (var keyValuePair in dictionary.Where(i => i.Key != _typeProperty))
+ {
+ var propertyName = keyValuePair.Key;
+ var rawValue = keyValuePair.Value;
+
+ PropertyInfo propertyInfo;
+ var value = properties.TryGetValue(propertyName, out propertyInfo)
+ ? DeserializeValueAs(rawValue, propertyInfo.PropertyType)
+ : DeserializeValueAs(rawValue, typeof(object));
+
+ style.Add(propertyName, value);
+ }
+
+ return style;
+ }
+
+ private static object DeserializeValueAs(object value, Type type)
+ {
+ var json = JsonSerializer.Serialize(value, type);
+ return JsonSerializer.Deserialize(json, type);
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, ControlStyle value, JsonSerializerOptions options)
+ {
+ var style = (ControlStyle)value;
+ var dictionary = new Dictionary<string, object> { [_typeProperty] = style.TargetType.Name };
+
+ foreach (var keyValuePair in style)
+ dictionary.Add(keyValuePair.Key, keyValuePair.Value);
+
+ JsonSerializer.Serialize(writer, dictionary);
+ }
+
+
+
+
+
+
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs
new file mode 100644
index 0000000..5ebc880
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework.Content;
+using MonoGame.Extended.BitmapFonts;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Gui.Serialization;
+
+public static class GuiJsonSerializerOptionsProvider
+{
+ public static JsonSerializerOptions GetOptions(ContentManager contentManager, params Type[] customControlTypes)
+ {
+ var options = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ var textureRegionService = new GuiTextureRegionService();
+
+ options.Converters.Add(new Vector2JsonConverter());
+ options.Converters.Add(new SizeJsonConverter());
+ options.Converters.Add(new Size2JsonConverter());
+ options.Converters.Add(new ColorJsonConverter());
+ options.Converters.Add(new ThicknessJsonConverter());
+ options.Converters.Add(new ContentManagerJsonConverter<BitmapFont>(contentManager, font => font.Name));
+ options.Converters.Add(new ControlStyleJsonConverter(customControlTypes));
+ options.Converters.Add(new GuiTextureAtlasJsonConverter(contentManager, textureRegionService));
+ options.Converters.Add(new GuiNinePatchRegion2DJsonConverter(textureRegionService));
+ options.Converters.Add(new TextureRegion2DJsonConverter(textureRegionService));
+ options.Converters.Add(new VerticalAlignmentConverter());
+ options.Converters.Add(new HorizontalAlignmentConverter());
+
+ return options;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiNinePatchRegion2DJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiNinePatchRegion2DJsonConverter.cs
new file mode 100644
index 0000000..9be58e5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiNinePatchRegion2DJsonConverter.cs
@@ -0,0 +1,15 @@
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Gui.Serialization
+{
+ public class GuiNinePatchRegion2DJsonConverter : NinePatchRegion2DJsonConverter
+ {
+ private readonly IGuiTextureRegionService _textureRegionService;
+
+ public GuiNinePatchRegion2DJsonConverter(IGuiTextureRegionService textureRegionService)
+ : base(textureRegionService)
+ {
+ _textureRegionService = textureRegionService;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureAtlasJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureAtlasJsonConverter.cs
new file mode 100644
index 0000000..a0617cf
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureAtlasJsonConverter.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Text.Json;
+using Microsoft.Xna.Framework.Content;
+using MonoGame.Extended.Serialization;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui.Serialization
+{
+ public class GuiTextureAtlasJsonConverter : ContentManagerJsonConverter<TextureAtlas>
+ {
+ private readonly IGuiTextureRegionService _textureRegionService;
+
+ public GuiTextureAtlasJsonConverter(ContentManager contentManager, IGuiTextureRegionService textureRegionService)
+ : base(contentManager, atlas => atlas.Name)
+ {
+ _textureRegionService = textureRegionService;
+ }
+
+ /// <inheritdoc />
+ public override TextureAtlas Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var textureAtlas = base.Read(ref reader, typeToConvert, options);
+ if (textureAtlas is not null)
+ {
+ _textureRegionService.TextureAtlases.Add(textureAtlas);
+ }
+
+ return textureAtlas;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureRegionService.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureRegionService.cs
new file mode 100644
index 0000000..cf9ab9e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureRegionService.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+using MonoGame.Extended.Serialization;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui.Serialization
+{
+ public interface IGuiTextureRegionService : ITextureRegionService
+ {
+ IList<TextureAtlas> TextureAtlases { get; }
+ IList<NinePatchRegion2D> NinePatches { get; }
+ }
+
+ public class GuiTextureRegionService : TextureRegionService, IGuiTextureRegionService
+ {
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/HorizontalAlignmentConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/HorizontalAlignmentConverter.cs
new file mode 100644
index 0000000..a696528
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/HorizontalAlignmentConverter.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Gui.Serialization;
+
+public class HorizontalAlignmentConverter : JsonConverter<HorizontalAlignment>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(HorizontalAlignment);
+
+ /// <inheritdoc />
+ public override HorizontalAlignment Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var value = reader.GetString();
+
+ if (value.Equals("Center", StringComparison.OrdinalIgnoreCase) || value.Equals("Centre", StringComparison.OrdinalIgnoreCase))
+ {
+ return HorizontalAlignment.Centre;
+ }
+
+ if (Enum.TryParse<HorizontalAlignment>(value, true, out var alignment))
+ {
+ return alignment;
+ }
+
+ throw new InvalidOperationException($"Invalid value for '{nameof(HorizontalAlignment)}'");
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, HorizontalAlignment value, JsonSerializerOptions options) { }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/SkinJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/SkinJsonConverter.cs
new file mode 100644
index 0000000..016ea6d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/SkinJsonConverter.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+
+namespace MonoGame.Extended.Gui.Serialization;
+
+public interface IGuiSkinService
+{
+ Skin Skin { get; set; }
+}
+
+public class SkinService : IGuiSkinService
+{
+ public Skin Skin { get; set; }
+}
+
+public class SkinJsonConverter : JsonConverter<Skin>
+{
+ private readonly ContentManager _contentManager;
+ private readonly IGuiSkinService _skinService;
+ private readonly Type[] _customControlTypes;
+
+ public SkinJsonConverter(ContentManager contentManager, IGuiSkinService skinService, params Type[] customControlTypes)
+ {
+ _contentManager = contentManager;
+ _skinService = skinService;
+ _customControlTypes = customControlTypes;
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Skin);
+
+ /// <inheritdoc />
+ public override Skin Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ var assetName = reader.GetString();
+
+ // TODO: Load this using the ContentManager instead.
+ using (var stream = TitleContainer.OpenStream(assetName))
+ {
+ var skin = Skin.FromStream(_contentManager, stream, _customControlTypes);
+ _skinService.Skin = skin;
+ return skin;
+ }
+
+ }
+
+ throw new InvalidOperationException($"{nameof(SkinJsonConverter)} can only convert from a string");
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, Skin value, JsonSerializerOptions options) { }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/VerticalAlignmentConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/VerticalAlignmentConverter.cs
new file mode 100644
index 0000000..bc55cda
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/VerticalAlignmentConverter.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Gui.Serialization;
+
+public class VerticalAlignmentConverter : JsonConverter<VerticalAlignment>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(VerticalAlignment);
+
+ /// <inheritdoc />
+ public override VerticalAlignment Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var value = reader.GetString();
+
+ if (value.Equals("Center", StringComparison.OrdinalIgnoreCase) || value.Equals("Centre", StringComparison.OrdinalIgnoreCase))
+ {
+ return VerticalAlignment.Centre;
+ }
+
+ if (Enum.TryParse<VerticalAlignment>(value, true, out var alignment))
+ {
+ return alignment;
+ }
+
+ throw new InvalidOperationException($"Invalid value for '{nameof(VerticalAlignment)}'");
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, VerticalAlignment value, JsonSerializerOptions options) { }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Skin.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Skin.cs
new file mode 100644
index 0000000..77476be
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Skin.cs
@@ -0,0 +1,221 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using MonoGame.Extended.BitmapFonts;
+using System.Linq;
+using System.Reflection;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using MonoGame.Extended.Collections;
+using MonoGame.Extended.Gui.Controls;
+using MonoGame.Extended.Gui.Serialization;
+using MonoGame.Extended.TextureAtlases;
+using System.Text.Json.Serialization;
+using System.Text.Json;
+
+namespace MonoGame.Extended.Gui
+{
+ public class Skin
+ {
+ public Skin()
+ {
+ TextureAtlases = new List<TextureAtlas>();
+ Fonts = new List<BitmapFont>();
+ NinePatches = new List<NinePatchRegion2D>();
+ Styles = new KeyedCollection<string, ControlStyle>(s => s.Name ?? s.TargetType.Name);
+ }
+
+ [JsonPropertyOrder(0)]
+ public string Name { get; set; }
+
+ [JsonPropertyOrder(1)]
+ public IList<TextureAtlas> TextureAtlases { get; set; }
+
+ [JsonPropertyOrder(2)]
+ public IList<BitmapFont> Fonts { get; set; }
+
+ [JsonPropertyOrder(3)]
+ public IList<NinePatchRegion2D> NinePatches { get; set; }
+
+ [JsonPropertyOrder(4)]
+ public BitmapFont DefaultFont => Fonts.FirstOrDefault();
+
+ [JsonPropertyOrder(5)]
+ public Cursor Cursor { get; set; }
+
+ [JsonPropertyOrder(6)]
+ public KeyedCollection<string, ControlStyle> Styles { get; private set; }
+
+ public ControlStyle GetStyle(string name)
+ {
+ if (Styles.TryGetValue(name, out var controlStyle))
+ return controlStyle;
+
+ return null;
+ }
+
+ public ControlStyle GetStyle(Type controlType)
+ {
+ return GetStyle(controlType.FullName);
+ }
+
+ public void Apply(Control control)
+ {
+ // TODO: This allocates memory on each apply because it needs to apply styles in reverse
+ var types = new List<Type>();
+ var controlType = control.GetType();
+
+ while (controlType != null)
+ {
+ types.Add(controlType);
+ controlType = controlType.GetTypeInfo().BaseType;
+ }
+
+ for (var i = types.Count - 1; i >= 0; i--)
+ {
+ var style = GetStyle(types[i]);
+ style?.Apply(control);
+ }
+ }
+
+ public static Skin FromFile(ContentManager contentManager, string path, params Type[] customControlTypes)
+ {
+ using (var stream = TitleContainer.OpenStream(path))
+ {
+ return FromStream(contentManager, stream, customControlTypes);
+ }
+ }
+
+ public static Skin FromStream(ContentManager contentManager, Stream stream, params Type[] customControlTypes)
+ {
+ var options = GuiJsonSerializerOptionsProvider.GetOptions(contentManager, customControlTypes);
+ return JsonSerializer.Deserialize<Skin>(stream, options);
+ }
+
+
+ public T Create<T>(string template, Action<T> onCreate)
+ where T : Control, new()
+ {
+ var control = new T();
+ GetStyle(template).Apply(control);
+ onCreate(control);
+ return control;
+ }
+
+ public Control Create(Type type, string template)
+ {
+ var control = (Control)Activator.CreateInstance(type);
+
+ if (template != null)
+ {
+ var style = GetStyle(template);
+ if (style != null)
+ style.Apply(control);
+ else
+ throw new FormatException($"invalid style {template} for control {type.Name}");
+ }
+
+ return control;
+ }
+
+ public static Skin Default { get; set; }
+
+ public static Skin CreateDefault(BitmapFont font)
+ {
+ Default = new Skin
+ {
+ Fonts = { font },
+ Styles =
+ {
+ new ControlStyle(typeof(Control)) {
+ {nameof(Control.BackgroundColor), new Color(51, 51, 55)},
+ {nameof(Control.BorderColor), new Color(67, 67, 70)},
+ {nameof(Control.BorderThickness), 1},
+ {nameof(Control.TextColor), new Color(241, 241, 241)},
+ {nameof(Control.Padding), new Thickness(5)},
+ {nameof(Control.DisabledStyle), new ControlStyle(typeof(Control)) {
+ { nameof(Control.TextColor), new Color(78,78,80) }
+ }
+ }
+ },
+ new ControlStyle(typeof(LayoutControl)) {
+ {nameof(Control.BackgroundColor), Color.Transparent},
+ {nameof(Control.BorderColor), Color.Transparent },
+ {nameof(Control.BorderThickness), 0},
+ {nameof(Control.Padding), new Thickness(0)},
+ {nameof(Control.Margin), new Thickness(0)},
+ },
+ new ControlStyle(typeof(ComboBox)) {
+ {nameof(ComboBox.DropDownColor), new Color(71, 71, 75)},
+ {nameof(ComboBox.SelectedItemColor), new Color(0, 122, 204)},
+ {nameof(ComboBox.HorizontalTextAlignment), HorizontalAlignment.Left }
+ },
+ new ControlStyle(typeof(CheckBox))
+ {
+ {nameof(CheckBox.HorizontalTextAlignment), HorizontalAlignment.Left },
+ {nameof(CheckBox.BorderThickness), 0},
+ {nameof(CheckBox.BackgroundColor), Color.Transparent},
+ },
+ new ControlStyle(typeof(ListBox))
+ {
+ {nameof(ListBox.SelectedItemColor), new Color(0, 122, 204)},
+ {nameof(ListBox.HorizontalTextAlignment), HorizontalAlignment.Left }
+ },
+ new ControlStyle(typeof(Label)) {
+ {nameof(Label.BackgroundColor), Color.Transparent},
+ {nameof(Label.TextColor), Color.White},
+ {nameof(Label.BorderColor), Color.Transparent},
+ {nameof(Label.BorderThickness), 0},
+ {nameof(Label.HorizontalTextAlignment), HorizontalAlignment.Left},
+ {nameof(Label.VerticalTextAlignment), VerticalAlignment.Bottom},
+ {nameof(Control.Margin), new Thickness(5,0)},
+ {nameof(Control.Padding), new Thickness(0)},
+ },
+ new ControlStyle(typeof(TextBox)) {
+ {nameof(Control.BackgroundColor), Color.DarkGray},
+ {nameof(Control.TextColor), Color.Black},
+ {nameof(Control.BorderColor), new Color(67, 67, 70)},
+ {nameof(Control.BorderThickness), 2},
+ },
+ new ControlStyle(typeof(TextBox2)) {
+ {nameof(Control.BackgroundColor), Color.DarkGray},
+ {nameof(Control.TextColor), Color.Black},
+ {nameof(Control.BorderColor), new Color(67, 67, 70)},
+ {nameof(Control.BorderThickness), 2},
+ },
+ new ControlStyle(typeof(Button)) {
+ {
+ nameof(Button.HoverStyle), new ControlStyle {
+ {nameof(Button.BackgroundColor), new Color(62, 62, 64)},
+ {nameof(Button.BorderColor), Color.WhiteSmoke }
+ }
+ },
+ {
+ nameof(Button.PressedStyle), new ControlStyle {
+ {nameof(Button.BackgroundColor), new Color(0, 122, 204)}
+ }
+ }
+ },
+ new ControlStyle(typeof(ToggleButton)) {
+ {
+ nameof(ToggleButton.CheckedStyle), new ControlStyle {
+ {nameof(Button.BackgroundColor), new Color(0, 122, 204)}
+ }
+ },
+ {
+ nameof(ToggleButton.CheckedHoverStyle), new ControlStyle {
+ {nameof(Button.BorderColor), Color.WhiteSmoke}
+ }
+ }
+ },
+ new ControlStyle(typeof(ProgressBar)) {
+ {nameof(ProgressBar.BarColor), new Color(0, 122, 204) },
+ {nameof(ProgressBar.Height), 32 },
+ {nameof(ProgressBar.Padding), new Thickness(5, 4)},
+ }
+ }
+ };
+ return Default;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Window.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Window.cs
new file mode 100644
index 0000000..12f4dcf
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Window.cs
@@ -0,0 +1,42 @@
+using System;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui
+{
+ //public class Window : Element<Screen>
+ //{
+ // public Window(Screen parent)
+ // {
+ // Parent = parent;
+ // }
+
+ // public ControlCollection Controls { get; } = new ControlCollection();
+
+ // public void Show()
+ // {
+ // Parent.Windows.Add(this);
+ // }
+
+ // public void Hide()
+ // {
+ // Parent.Windows.Remove(this);
+ // }
+
+ // public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ // {
+ // renderer.FillRectangle(BoundingRectangle, Color.Magenta);
+ // }
+
+ // public Size2 GetDesiredSize(IGuiContext context, Size2 availableSize)
+ // {
+ // return new Size2(Width, Height);
+ // }
+
+ // public void Layout(IGuiContext context, RectangleF rectangle)
+ // {
+ // foreach (var control in Controls)
+ // LayoutHelper.PlaceControl(context, control, rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+ // }
+ //}
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/WindowCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/WindowCollection.cs
new file mode 100644
index 0000000..d22f274
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/WindowCollection.cs
@@ -0,0 +1,10 @@
+namespace MonoGame.Extended.Gui
+{
+ //public class WindowCollection : ElementCollection<Window, Screen>
+ //{
+ // public WindowCollection(Screen parent)
+ // : base(parent)
+ // {
+ // }
+ //}
+} \ No newline at end of file