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