diff options
author | chai <215380520@qq.com> | 2024-06-03 10:15:45 +0800 |
---|---|---|
committer | chai <215380520@qq.com> | 2024-06-03 10:15:45 +0800 |
commit | acea7b2e728787a0d83bbf83c8c1f042d2c32e7e (patch) | |
tree | 0bfec05c1ca2d71be2c337bcd110a0421f19318b /Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui | |
parent | 88febcb02bf127d961c6471d9e846c0e1315f5c3 (diff) |
+ plugins project
Diffstat (limited to 'Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui')
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 |