aboutsummaryrefslogtreecommitdiff
path: root/V3/Widgets
diff options
context:
space:
mode:
Diffstat (limited to 'V3/Widgets')
-rw-r--r--V3/Widgets/AbstractMenu.cs96
-rw-r--r--V3/Widgets/AbstractTextWidget.cs113
-rw-r--r--V3/Widgets/AchievementBox.cs91
-rw-r--r--V3/Widgets/Button.cs111
-rw-r--r--V3/Widgets/EmptyWidget.cs41
-rw-r--r--V3/Widgets/FormMenu.cs75
-rw-r--r--V3/Widgets/HorizontalOrientation.cs9
-rw-r--r--V3/Widgets/IBasicWidgetFactory.cs38
-rw-r--r--V3/Widgets/IClickable.cs14
-rw-r--r--V3/Widgets/IImageWidget.cs15
-rw-r--r--V3/Widgets/IMenu.cs33
-rw-r--r--V3/Widgets/IMenuFactory.cs9
-rw-r--r--V3/Widgets/ISelectable.cs27
-rw-r--r--V3/Widgets/ITextWidget.cs43
-rw-r--r--V3/Widgets/IWidget.cs34
-rw-r--r--V3/Widgets/Label.cs14
-rw-r--r--V3/Widgets/SelectButton.cs108
-rw-r--r--V3/Widgets/VerticalMenu.cs42
-rw-r--r--V3/Widgets/WidgetFactory.cs78
19 files changed, 991 insertions, 0 deletions
diff --git a/V3/Widgets/AbstractMenu.cs b/V3/Widgets/AbstractMenu.cs
new file mode 100644
index 0000000..f179b5e
--- /dev/null
+++ b/V3/Widgets/AbstractMenu.cs
@@ -0,0 +1,96 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using System.Collections.Generic;
+using System.Linq;
+using V3.Input;
+
+namespace V3.Widgets
+{
+ /// <summary>
+ /// An abstract menu that handles the updating and drawing of widgtes.
+ /// </summary>
+ public abstract class AbstractMenu : IMenu
+ {
+ public List<IWidget> Widgets { get; } = new List<IWidget>();
+ public Vector2 Size { get; private set; }
+ public Vector2 Position { get; private set; }
+
+ private readonly GraphicsDeviceManager mGraphicsDeviceManager;
+
+ protected AbstractMenu(GraphicsDeviceManager graphicsDeviceManager)
+ {
+ mGraphicsDeviceManager = graphicsDeviceManager;
+ }
+
+ public void HandleMouseEvent(IMouseEvent mouseEvent)
+ {
+ foreach (var clickable in Widgets.OfType<IClickable>())
+ clickable.HandleMouseEvent(mouseEvent);
+ }
+
+ public void Draw(SpriteBatch spriteBatch)
+ {
+ UpdateWidgetRelativePositions();
+ UpdateWidgetAbsolutePositions();
+ spriteBatch.Begin();
+ Widgets.ForEach(w => w.Draw(spriteBatch));
+ spriteBatch.End();
+ }
+
+ public void Update()
+ {
+ ResetClicked();
+ UpdateMouseSelection();
+ UpdateWidgetSizes();
+ }
+
+ protected abstract void UpdateWidgetSizes();
+
+ protected abstract void UpdateWidgetRelativePositions();
+
+ protected abstract Vector2 GetTotalSize();
+
+ private void UpdateWidgetAbsolutePositions()
+ {
+ var viewport = mGraphicsDeviceManager.GraphicsDevice.Viewport;
+ Size = GetTotalSize();
+ var viewportSize = new Vector2(viewport.Bounds.Width, viewport.Bounds.Height);
+ Position = (viewportSize - Size) / 2;
+ Widgets.ForEach(w => w.Position = w.Position + Position);
+ }
+
+ private void ResetClicked()
+ {
+ foreach (var clickable in Widgets.OfType<IClickable>())
+ clickable.IsClicked = false;
+ }
+
+ private void UpdateMouseSelection()
+ {
+ var position = Mouse.GetState().Position;
+ foreach (var selectable in Widgets.OfType<ISelectable>())
+ selectable.IsSelected = selectable.CheckSelected(position);
+ }
+
+ protected static void MakeWidgetsSameSize(IEnumerable<IWidget> widgets)
+ {
+ var xMax = 0f;
+ var yMax = 0f;
+
+ var widgetsCopy = widgets as IList<IWidget> ?? widgets.ToList();
+ foreach (var widget in widgetsCopy)
+ {
+ var size = widget.GetMinimumSize();
+ if (size.X > xMax)
+ xMax = size.X;
+ if (size.Y > yMax)
+ yMax = size.Y;
+ }
+
+ var sizeMax = new Vector2(xMax, yMax);
+ foreach (var widget in widgetsCopy)
+ widget.Size = sizeMax;
+ }
+ }
+}
diff --git a/V3/Widgets/AbstractTextWidget.cs b/V3/Widgets/AbstractTextWidget.cs
new file mode 100644
index 0000000..1f28742
--- /dev/null
+++ b/V3/Widgets/AbstractTextWidget.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Text;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using Ninject;
+
+namespace V3.Widgets
+{
+ public abstract class AbstractTextWidget : ITextWidget, IInitializable
+ {
+ private string mText;
+
+ public Vector2 Position { get; set; }
+
+ public Vector2 Size { get; set; }
+
+ public string Text
+ {
+ get { return mText; }
+ set { mText = ReplaceUmlauteWhenOnUnix(value); }
+ }
+
+ public float PaddingX { get; set; } = 80;
+
+ public float PaddingY { get; set; } = 5;
+
+ public Color Color { get; set; } = Color.Black;
+
+ public HorizontalOrientation HorizontalOrientation { get; set; } = HorizontalOrientation.Center;
+
+ public SpriteFont Font { get; set; }
+
+ private readonly ContentManager mContentManager;
+
+ public AbstractTextWidget(ContentManager contentManager)
+ {
+ mContentManager = contentManager;
+ Text = "";
+ }
+
+ public virtual void Initialize()
+ {
+ Font = mContentManager.Load<SpriteFont>("Fonts/MenuFont");
+ }
+
+ public virtual void Draw(SpriteBatch spriteBatch)
+ {
+ var calculatedSize = Font.MeasureString(Text);
+ var position = Position;
+ switch (HorizontalOrientation)
+ {
+ case HorizontalOrientation.Left:
+ position.X += PaddingX;
+ break;
+ case HorizontalOrientation.Center:
+ position.X += (Size - calculatedSize).X / 2;
+ break;
+ case HorizontalOrientation.Right:
+ position.X += (Size - calculatedSize).X;
+ position.X -= PaddingX;
+ break;
+ }
+ position.Y += (Size - calculatedSize).Y / 2;
+ spriteBatch.DrawString(Font, Text, position, GetColor());
+ }
+
+ public virtual Vector2 GetMinimumSize()
+ {
+ var size = Font.MeasureString(Text);
+ size.X += 2 * PaddingX;
+ size.Y += 2 * PaddingY;
+ return size;
+ }
+
+ public bool CheckSelected(Point position)
+ {
+ var rectangle = new Rectangle((int) Position.X, (int) Position.Y, (int) Size.X, (int) Size.Y);
+ return rectangle.Contains(position);
+ }
+
+ protected virtual Color GetColor()
+ {
+ return Color;
+ }
+
+ /// <summary>
+ /// Test if execution platform is UNIX and replace german Umlaute
+ /// accordingly because a strange ArgumentException is thrown otherwise.
+ /// </summary>
+ /// <param name="originalString">The original input string.</param>
+ /// <returns></returns>
+ private string ReplaceUmlauteWhenOnUnix(string originalString)
+ {
+ // Taken from <http://mono.wikia.com/wiki/Detecting_the_execution_platform>.
+ int p = (int) Environment.OSVersion.Platform;
+ if ((p == 4) || (p == 6) || (p == 128)) // Running on Unix
+ {
+ var sb = new StringBuilder();
+ foreach (char c in originalString)
+ {
+ if (c < 128)
+ {
+ sb.Append(c);
+ }
+ }
+ return sb.ToString();
+ }
+ // NOT running on Unix.
+ return originalString;
+ }
+ }
+}
diff --git a/V3/Widgets/AchievementBox.cs b/V3/Widgets/AchievementBox.cs
new file mode 100644
index 0000000..000894a
--- /dev/null
+++ b/V3/Widgets/AchievementBox.cs
@@ -0,0 +1,91 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace V3.Widgets
+{
+ // ReSharper disable once ClassNeverInstantiated.Global
+ public sealed class AchievementBox : AbstractTextWidget
+ {
+ public bool IsEnabled { private get; set; }
+ private Color BackgroundColor { get; } = Color.Gray;
+
+ private readonly ContentManager mContentManager;
+ private readonly WidgetFactory mWidgetFactory;
+ private SpriteFont mTitleFont;
+ private SpriteFont mDescriptionFont;
+
+ private Label mButtonTitle;
+ private Label mButtonDescription;
+
+ private Texture2D mRectangle;
+
+ public AchievementBox(ContentManager contentManager, WidgetFactory widgetFactory) : base(contentManager)
+ {
+ mContentManager = contentManager;
+ mWidgetFactory = widgetFactory;
+ }
+
+ public override void Initialize()
+ {
+ mRectangle = mContentManager.Load<Texture2D>("Sprites/WhiteRectangle");
+ mTitleFont = mContentManager.Load<SpriteFont>("Fonts/MenuFont");
+ mDescriptionFont = mContentManager.Load<SpriteFont>("Fonts/UnitFont");
+
+ mButtonTitle = mWidgetFactory.CreateLabel("");
+ mButtonTitle.PaddingY = 0;
+ mButtonTitle.PaddingX = 10;
+ mButtonTitle.HorizontalOrientation = HorizontalOrientation.Left;
+ mButtonDescription = mWidgetFactory.CreateLabel("");
+ mButtonDescription.PaddingX = 10;
+ mButtonDescription.PaddingY = 0;
+ mButtonDescription.HorizontalOrientation = HorizontalOrientation.Left;
+
+ base.Initialize();
+ }
+
+ public override void Draw(SpriteBatch spriteBatch)
+ {
+ var rectangle = new Rectangle((int)Position.X, (int)Position.Y, (int)Size.X, (int)Size.Y);
+
+ if (IsEnabled)
+ {
+ var borderRectangle = new Rectangle((int)Position.X - 2, (int)Position.Y - 2, (int)Size.X + 4, (int)Size.Y + 4);
+ spriteBatch.Draw(mRectangle, borderRectangle, Color.Gray);
+ }
+
+ mButtonTitle.Color = IsEnabled ? Color.Black : Color.Gray;
+ mButtonDescription.Color = IsEnabled ? Color.Black : Color.Gray;
+
+ spriteBatch.Draw(mRectangle, rectangle, GetBackgroundColor());
+ mButtonTitle.Position = Position + new Vector2(0, 10);
+ mButtonDescription.Position = mButtonTitle.Position + new Vector2(0, mButtonDescription.Size.Y);
+ mButtonDescription.Draw(spriteBatch);
+ mButtonTitle.Draw(spriteBatch);
+ base.Draw(spriteBatch);
+ }
+
+ public override Vector2 GetMinimumSize()
+ {
+ var titleSize = mButtonTitle.GetMinimumSize();
+ var descriptionSize = mButtonDescription.GetMinimumSize();
+
+ return new Vector2(MathHelper.Max(titleSize.X, descriptionSize.X), titleSize.Y + descriptionSize.Y + 20);
+ }
+
+ private Color GetBackgroundColor()
+ {
+ return IsEnabled ? BackgroundColor : Color.LightGray;
+ }
+
+ public void SetText(string title, string description)
+ {
+ mButtonTitle.Text = title;
+ mButtonDescription.Text = description;
+ mButtonTitle.Size = mButtonTitle.GetMinimumSize();
+ mButtonDescription.Size = mButtonDescription.GetMinimumSize();
+ mButtonTitle.Font = mTitleFont;
+ mButtonDescription.Font = mDescriptionFont;
+ }
+ }
+}
diff --git a/V3/Widgets/Button.cs b/V3/Widgets/Button.cs
new file mode 100644
index 0000000..04307d0
--- /dev/null
+++ b/V3/Widgets/Button.cs
@@ -0,0 +1,111 @@
+using Castle.Core.Internal;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using V3.Input;
+
+namespace V3.Widgets
+{
+ // ReSharper disable once ClassNeverInstantiated.Global
+ public sealed class Button : AbstractTextWidget, IClickable, IImageWidget, ISelectable
+ {
+ public bool IsClicked { get; set; }
+
+ public bool IsEnabled { get; set; } = true;
+
+ public bool IsSelected { get; set; }
+
+ public Color BackgroundColor { private get; set; } = Color.Gray;
+
+ public Texture2D Image { get; set; }
+
+ public string Tooltip { get; set; }
+ public string TooltipTitle { private get; set; }
+
+ public Rectangle Rectangle
+ {
+ get { return new Rectangle((int)Position.X, (int)Position.Y, (int)Size.X, (int)Size.Y); }
+ }
+
+ private readonly ContentManager mContentManager;
+ private readonly WidgetFactory mWidgetFactory;
+
+ private Texture2D mRectangle;
+ private AchievementBox mTooltipBox;
+
+ public Button(ContentManager contentManager, WidgetFactory widgetFactory) : base(contentManager)
+ {
+ mContentManager = contentManager;
+ mWidgetFactory = widgetFactory;
+ }
+
+ public override void Initialize()
+ {
+ mRectangle = mContentManager.Load<Texture2D>("Sprites/WhiteRectangle");
+ mTooltipBox = mWidgetFactory.CreateAchievementBox();
+
+ base.Initialize();
+ }
+
+ public void HandleMouseEvent(IMouseEvent mouseEvent)
+ {
+ if (!IsEnabled)
+ return;
+ if (mouseEvent.MouseButton == MouseButton.Left && mouseEvent.ButtonState == ButtonState.Released)
+ {
+ if (!mouseEvent.PositionReleased.HasValue)
+ return;
+ if (Rectangle.Contains(mouseEvent.PositionReleased.Value))
+ IsClicked = true;
+ }
+ }
+
+ public override void Draw(SpriteBatch spriteBatch)
+ {
+ var rectangle = new Rectangle((int)Position.X, (int)Position.Y, (int)Size.X, (int)Size.Y);
+
+ if (IsSelected && IsEnabled)
+ {
+ var borderRectangle = new Rectangle((int)Position.X - 2, (int)Position.Y - 2, (int)Size.X + 4, (int)Size.Y + 4);
+ spriteBatch.Draw(mRectangle, borderRectangle, Color.Red);
+ }
+
+ if (Image == null)
+ {
+ spriteBatch.Draw(mRectangle, rectangle, GetBackgroundColor());
+ }
+ else
+ {
+ spriteBatch.Draw(Image, rectangle, Color.White);
+ if (!IsEnabled)
+ {
+ spriteBatch.Draw(mRectangle, rectangle, Color.Black * 0.7f);
+ }
+ }
+
+ if (IsSelected && !TooltipTitle.IsNullOrEmpty() && !Tooltip.IsNullOrEmpty())
+ {
+ mTooltipBox.SetText(TooltipTitle, Tooltip);
+ mTooltipBox.Size = mTooltipBox.GetMinimumSize();
+ mTooltipBox.Position = Position - new Vector2(0, mTooltipBox.Size.Y);
+ mTooltipBox.IsEnabled = true;
+ if (mTooltipBox.Position.Y < 0)
+ mTooltipBox.Position = Position + new Vector2(0, Size.Y);
+ mTooltipBox.Draw(spriteBatch);
+ }
+
+ base.Draw(spriteBatch);
+ }
+
+ protected override Color GetColor()
+ {
+ return IsEnabled ? Color : Color.Gray;
+ }
+
+ private Color GetBackgroundColor()
+ {
+ return IsEnabled ? BackgroundColor : Color.LightGray;
+ }
+ }
+}
diff --git a/V3/Widgets/EmptyWidget.cs b/V3/Widgets/EmptyWidget.cs
new file mode 100644
index 0000000..d9ad8d8
--- /dev/null
+++ b/V3/Widgets/EmptyWidget.cs
@@ -0,0 +1,41 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace V3.Widgets
+{
+ /// <summary>
+ /// A placeholder widget.
+ /// </summary>
+ // ReSharper disable once ClassNeverInstantiated.Global
+ public sealed class EmptyWidget : IWidget
+ {
+ /// <summary>
+ /// The current position of this widget on the screen.
+ /// </summary>
+ public Vector2 Position { get; set; }
+
+ /// <summary>
+ /// The size of this widget on the screen. The widget should try to
+ /// fill this size. Should not be smaller than the value returned by
+ /// GetMinimumSize().
+ /// </summary>
+ public Vector2 Size { get; set; }
+
+ /// <summary>
+ /// Returns the minimum size this widgets needs.
+ /// </summary>
+ public Vector2 GetMinimumSize()
+ {
+ return new Vector2(0, 0);
+ }
+
+ /// <summary>
+ /// Draws this widget on the given sprite batch. It is assumed that
+ /// spriteBatch.Begin() has already been called.
+ /// </summary>
+ public void Draw(SpriteBatch spriteBatch)
+ {
+ }
+ }
+}
+
diff --git a/V3/Widgets/FormMenu.cs b/V3/Widgets/FormMenu.cs
new file mode 100644
index 0000000..746f8f4
--- /dev/null
+++ b/V3/Widgets/FormMenu.cs
@@ -0,0 +1,75 @@
+using Microsoft.Xna.Framework;
+using System;
+using System.Linq;
+
+namespace V3.Widgets
+{
+ /// <summary>
+ /// A menu that arranges widgets in two columns. All widgets in on column
+ /// are made the same size (the maximum widget size).
+ /// </summary>
+ // ReSharper disable once ClassNeverInstantiated.Global
+ public sealed class FormMenu : AbstractMenu
+ {
+ private const float PaddingInnerX = 10;
+ private const float PaddingOuterX = 10;
+ private const float PaddingInnerY = 10;
+ private const float PaddingOuterY = 10;
+
+ public FormMenu(GraphicsDeviceManager graphicsDeviceManager) : base(graphicsDeviceManager)
+ {
+ }
+
+ protected override Vector2 GetTotalSize()
+ {
+ var size = new Vector2(2 * PaddingOuterX, 2 * PaddingOuterY);
+ var maxXLeft = 0f;
+ var maxXRight = 0f;
+ for (var i = 0; i < Widgets.Count; i += 2)
+ {
+ maxXLeft = Math.Max(maxXLeft, Widgets[i].Size.X);
+ if (i + 1 < Widgets.Count)
+ {
+ maxXRight = Math.Max(maxXRight, Widgets[i + 1].Size.X);
+ size.Y += Math.Max(Widgets[i].Size.Y, Widgets[i + 1].Size.Y);
+ }
+ else
+ {
+ size.Y += Widgets[i].Size.Y;
+ }
+ if (i + 2 < Widgets.Count)
+ size.Y += PaddingInnerY;
+ }
+ size.X += maxXLeft + maxXRight + PaddingInnerX;
+ return size;
+ }
+
+ protected override void UpdateWidgetRelativePositions()
+ {
+ var y = PaddingOuterY;
+ for (var i = 0; i < Widgets.Count; i += 2)
+ {
+ var x = PaddingOuterX;
+ Widgets[i].Position = new Vector2(x, y);
+ if (i + 1 < Widgets.Count)
+ {
+ x += Widgets[i].Size.X;
+ x += PaddingInnerX;
+ Widgets[i + 1].Position = new Vector2(x, y);
+ y += Math.Max(Widgets[i].Size.Y, Widgets[i + 1].Size.Y);
+ }
+ else
+ {
+ y += Widgets[i].Size.Y;
+ }
+ y += PaddingInnerY;
+ }
+ }
+
+ protected override void UpdateWidgetSizes()
+ {
+ MakeWidgetsSameSize(Widgets.Where((w, i) => i % 2 == 0));
+ MakeWidgetsSameSize(Widgets.Where((w, i) => i % 2 == 1));
+ }
+ }
+}
diff --git a/V3/Widgets/HorizontalOrientation.cs b/V3/Widgets/HorizontalOrientation.cs
new file mode 100644
index 0000000..489667a
--- /dev/null
+++ b/V3/Widgets/HorizontalOrientation.cs
@@ -0,0 +1,9 @@
+namespace V3.Widgets
+{
+ public enum HorizontalOrientation
+ {
+ Left,
+ Center,
+ Right
+ }
+}
diff --git a/V3/Widgets/IBasicWidgetFactory.cs b/V3/Widgets/IBasicWidgetFactory.cs
new file mode 100644
index 0000000..dd0a28d
--- /dev/null
+++ b/V3/Widgets/IBasicWidgetFactory.cs
@@ -0,0 +1,38 @@
+namespace V3.Widgets
+{
+ /// <summary>
+ /// A widget factory that is automatically implemented by Ninject.
+ /// </summary>
+ public interface IBasicWidgetFactory
+ {
+ /// <summary>
+ /// Creates a new button widget.
+ /// </summary>
+ /// <returns>a new button widget</returns>
+ Button CreateButton();
+
+ /// <summary>
+ /// Creates a new empty widget.
+ /// </summary>
+ /// <returns>a new empty widget</returns>
+ EmptyWidget CreateEmptyWidget();
+
+ /// <summary>
+ /// Creates a new select button widget.
+ /// </summary>
+ /// <returns>a new select button widget</returns>
+ SelectButton CreateSelectButton();
+
+ /// <summary>
+ /// Creates a new label widget.
+ /// </summary>
+ /// <returns>a new label widget</returns>
+ Label CreateLabel();
+
+ /// <summary>
+ /// Creates a new Achievement Box.
+ /// </summary>
+ /// <returns>a new achievement box widget.</returns>
+ AchievementBox CreateAchievementBox();
+ }
+}
diff --git a/V3/Widgets/IClickable.cs b/V3/Widgets/IClickable.cs
new file mode 100644
index 0000000..276c4a1
--- /dev/null
+++ b/V3/Widgets/IClickable.cs
@@ -0,0 +1,14 @@
+using V3.Input;
+
+namespace V3.Widgets
+{
+ /// <summary>
+ /// An element that can be clicked on.
+ /// </summary>
+ public interface IClickable : IMouseEventHandler
+ {
+ bool IsClicked { get; set; }
+
+ bool IsEnabled { get; set; }
+ }
+}
diff --git a/V3/Widgets/IImageWidget.cs b/V3/Widgets/IImageWidget.cs
new file mode 100644
index 0000000..5a5b368
--- /dev/null
+++ b/V3/Widgets/IImageWidget.cs
@@ -0,0 +1,15 @@
+using Microsoft.Xna.Framework.Graphics;
+
+namespace V3.Widgets
+{
+ /// <summary>
+ /// A widget that displays an image.
+ /// </summary>
+ public interface IImageWidget : IWidget
+ {
+ /// <summary>
+ /// The image to display.
+ /// </summary>
+ Texture2D Image { get; set; }
+ }
+}
diff --git a/V3/Widgets/IMenu.cs b/V3/Widgets/IMenu.cs
new file mode 100644
index 0000000..ad121e0
--- /dev/null
+++ b/V3/Widgets/IMenu.cs
@@ -0,0 +1,33 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using System.Collections.Generic;
+using V3.Input;
+
+namespace V3.Widgets
+{
+ /// <summary>
+ /// A menu that displays a list of widgets.
+ /// </summary>
+ public interface IMenu : IMouseEventHandler
+ {
+ /// <summary>
+ /// The widgets in this menu. The order of the widgets in this list
+ /// is the order in which they are displayed.
+ /// </summary>
+ List<IWidget> Widgets { get; }
+
+ /// <summary>
+ /// The total size of the widgets in this menu.
+ /// </summary>
+ Vector2 Size { get; }
+
+ /// <summary>
+ /// The current position of the widgets.
+ /// </summary>
+ Vector2 Position { get; }
+
+ void Draw(SpriteBatch spriteBatch);
+
+ void Update();
+ }
+}
diff --git a/V3/Widgets/IMenuFactory.cs b/V3/Widgets/IMenuFactory.cs
new file mode 100644
index 0000000..24ed311
--- /dev/null
+++ b/V3/Widgets/IMenuFactory.cs
@@ -0,0 +1,9 @@
+namespace V3.Widgets
+{
+ public interface IMenuFactory
+ {
+ FormMenu CreateFormMenu();
+
+ VerticalMenu CreateVerticalMenu();
+ }
+} \ No newline at end of file
diff --git a/V3/Widgets/ISelectable.cs b/V3/Widgets/ISelectable.cs
new file mode 100644
index 0000000..6eb78dc
--- /dev/null
+++ b/V3/Widgets/ISelectable.cs
@@ -0,0 +1,27 @@
+using Microsoft.Xna.Framework;
+
+namespace V3.Widgets
+{
+ /// <summary>
+ /// An element that can be selected.
+ /// </summary>
+ public interface ISelectable
+ {
+ bool IsSelected { get; set; }
+
+ /// <summary>
+ /// The tooltip to show if this widget is selected. Can be null or
+ /// empty if no tooltip should be shown.
+ /// </summary>
+ string Tooltip { get; set; }
+
+ /// <summary>
+ /// Checks whether this element is selected by a mouse click at the
+ /// given position.
+ /// </summary>
+ /// <param name="position">the position of the mouse click</param>
+ /// <returns>true if the element is selected by the mouse click at
+ /// that position, otherwise false</returns>
+ bool CheckSelected(Point position);
+ }
+}
diff --git a/V3/Widgets/ITextWidget.cs b/V3/Widgets/ITextWidget.cs
new file mode 100644
index 0000000..e5f8203
--- /dev/null
+++ b/V3/Widgets/ITextWidget.cs
@@ -0,0 +1,43 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace V3.Widgets
+{
+ /// <summary>
+ /// A widget that contains some text.
+ /// </summary>
+ public interface ITextWidget : IWidget
+ {
+ /// <summary>
+ /// The text of this widget.
+ /// </summary>
+ string Text { get; set; }
+
+ /// <summary>
+ /// The space that is added before and after the text in the
+ /// horizontal direction.
+ /// </summary>
+ float PaddingX { get; set; }
+
+ /// <summary>
+ /// The space that is added before and after the text in the
+ /// vertical direction.
+ /// </summary>
+ float PaddingY { get; set; }
+
+ /// <summary>
+ /// The horizontal orientation of the text within the size.
+ /// </summary>
+ HorizontalOrientation HorizontalOrientation { get; set; }
+
+ /// <summary>
+ /// The default color of the text.
+ /// </summary>
+ Color Color { get; set; }
+
+ /// <summary>
+ /// The font for text rendering.
+ /// </summary>
+ SpriteFont Font { get; set; }
+ }
+}
diff --git a/V3/Widgets/IWidget.cs b/V3/Widgets/IWidget.cs
new file mode 100644
index 0000000..23466fe
--- /dev/null
+++ b/V3/Widgets/IWidget.cs
@@ -0,0 +1,34 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace V3.Widgets
+{
+ /// <summary>
+ /// A simple widget that has a size and a position and that can be drawn.
+ /// </summary>
+ public interface IWidget
+ {
+ /// <summary>
+ /// The current position of this widget on the screen.
+ /// </summary>
+ Vector2 Position { get; set; }
+
+ /// <summary>
+ /// The size of this widget on the screen. The widget should try to
+ /// fill this size. Should not be smaller than the value returned by
+ /// GetMinimumSize().
+ /// </summary>
+ Vector2 Size { get; set; }
+
+ /// <summary>
+ /// Returns the minimum size this widgets needs.
+ /// </summary>
+ Vector2 GetMinimumSize();
+
+ /// <summary>
+ /// Drawst this widget on the given sprite batch. It is assumed that
+ /// spriteBatch.Begin() has already been called.
+ /// </summary>
+ void Draw(SpriteBatch spriteBatch);
+ }
+}
diff --git a/V3/Widgets/Label.cs b/V3/Widgets/Label.cs
new file mode 100644
index 0000000..6b60d4b
--- /dev/null
+++ b/V3/Widgets/Label.cs
@@ -0,0 +1,14 @@
+using Microsoft.Xna.Framework.Content;
+
+namespace V3.Widgets
+{
+ // ReSharper disable once ClassNeverInstantiated.Global
+ public sealed class Label : AbstractTextWidget
+ {
+ public Label(ContentManager contentManager) : base(contentManager)
+ {
+ HorizontalOrientation = HorizontalOrientation.Left;
+ PaddingX = 20;
+ }
+ }
+}
diff --git a/V3/Widgets/SelectButton.cs b/V3/Widgets/SelectButton.cs
new file mode 100644
index 0000000..9079c6d
--- /dev/null
+++ b/V3/Widgets/SelectButton.cs
@@ -0,0 +1,108 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using System.Collections.Generic;
+using System.Linq;
+using V3.Input;
+
+namespace V3.Widgets
+{
+ // ReSharper disable once ClassNeverInstantiated.Global
+ public sealed class SelectButton : AbstractTextWidget, IClickable
+ {
+ public List<string> Values { get; } = new List<string>();
+
+ public int SelectedIndex { get; set; }
+
+ public bool IsClicked { get; set; }
+
+ public bool IsEnabled { get; set; } = true;
+
+ private readonly ContentManager mContentManager;
+
+ private Texture2D mTriangle;
+ private Rectangle mBoxArrowLeft;
+ private Rectangle mBoxArrowRight;
+
+ public SelectButton(ContentManager contentManager) : base(contentManager)
+ {
+ mContentManager = contentManager;
+ }
+
+ public override void Initialize()
+ {
+ mTriangle = mContentManager.Load<Texture2D>("Menu/arrow_white");
+ base.Initialize();
+ }
+
+ public void HandleMouseEvent(IMouseEvent mouseEvent)
+ {
+ if (mouseEvent.MouseButton == MouseButton.Left && mouseEvent.ButtonState == ButtonState.Released)
+ {
+ if (!mouseEvent.PositionReleased.HasValue)
+ return;
+ var rectangle = new Rectangle((int)Position.X, (int)Position.Y, (int)Size.X, (int)Size.Y);
+ var position = mouseEvent.PositionReleased.Value;
+ if (rectangle.Contains(position))
+ IsClicked = true;
+
+ int change;
+ if (mBoxArrowLeft.Contains(position))
+ change = -1;
+ else if (mBoxArrowRight.Contains(position))
+ change = 1;
+ else
+ return;
+
+ SelectedIndex += change;
+ if (SelectedIndex < 0)
+ SelectedIndex += Values.Count;
+ if (SelectedIndex >= Values.Count)
+ SelectedIndex -= Values.Count;
+ }
+ }
+
+ public override void Draw(SpriteBatch spriteBatch)
+ {
+ UpdateSelection();
+
+ var arrowPadding = 30;
+ var arrowLength = 20;
+ var arrowY = (int)Position.Y + (int)(Size.Y / 2) - arrowLength / 2;
+ mBoxArrowLeft = new Rectangle((int)Position.X + arrowPadding,
+ arrowY, arrowLength, arrowLength);
+ mBoxArrowRight = new Rectangle((int)Position.X + (int)Size.X - arrowPadding,
+ arrowY, arrowLength, arrowLength);
+
+
+ spriteBatch.Draw(mTriangle, mBoxArrowLeft, null, Color.Gray, 0, new Vector2(0, 0), SpriteEffects.FlipHorizontally, 0);
+ spriteBatch.Draw(mTriangle, mBoxArrowRight, Color.Gray);
+
+ base.Draw(spriteBatch);
+ }
+
+ public override Vector2 GetMinimumSize()
+ {
+ Vector2 size;
+ try
+ {
+ size = new Vector2(Values.Max(v => Font.MeasureString(v).X), Font.MeasureString(Text).Y);
+ }
+ catch (ArgumentException)
+ {
+ // Return whatever.
+ size = new Vector2(100, 40);
+ }
+ size.X += 2 * PaddingX;
+ size.Y += 2 * PaddingY;
+ return size;
+ }
+
+ private void UpdateSelection()
+ {
+ Text = Values[SelectedIndex];
+ }
+ }
+}
diff --git a/V3/Widgets/VerticalMenu.cs b/V3/Widgets/VerticalMenu.cs
new file mode 100644
index 0000000..bc18439
--- /dev/null
+++ b/V3/Widgets/VerticalMenu.cs
@@ -0,0 +1,42 @@
+using Microsoft.Xna.Framework;
+using System.Linq;
+
+namespace V3.Widgets
+{
+ /// <summary>
+ /// A simple menu with vertically arranged widgets. All widgets are made
+ /// the same size (the maximum widget size).
+ /// </summary>
+ // ReSharper disable once ClassNeverInstantiated.Global
+ public sealed class VerticalMenu : AbstractMenu
+ {
+ private float mPadding = 10;
+
+ public VerticalMenu(GraphicsDeviceManager graphicsDeviceManager) : base(graphicsDeviceManager)
+ {
+ }
+
+ protected override void UpdateWidgetRelativePositions()
+ {
+ var y = 0f;
+ foreach (var widget in Widgets)
+ {
+ widget.Position = new Vector2(0, y);
+ y += widget.Size.Y;
+ y += mPadding;
+ }
+ }
+
+ protected override Vector2 GetTotalSize()
+ {
+ var totalX = Widgets.Max(w => w.Size.X);
+ var totalY = (Widgets.Count - 1) * mPadding + Widgets.Sum(w => w.Size.Y);
+ return new Vector2(totalX, totalY);
+ }
+
+ protected override void UpdateWidgetSizes()
+ {
+ MakeWidgetsSameSize(Widgets);
+ }
+ }
+}
diff --git a/V3/Widgets/WidgetFactory.cs b/V3/Widgets/WidgetFactory.cs
new file mode 100644
index 0000000..35e8e1b
--- /dev/null
+++ b/V3/Widgets/WidgetFactory.cs
@@ -0,0 +1,78 @@
+namespace V3.Widgets
+{
+ /// <summary>
+ /// A widget factory.
+ /// </summary>
+ // ReSharper disable once ClassNeverInstantiated.Global
+ public sealed class WidgetFactory
+ {
+ private IBasicWidgetFactory mFactory;
+
+ public WidgetFactory(IBasicWidgetFactory factory)
+ {
+ mFactory = factory;
+ }
+
+ /// <summary>
+ /// Creates a new button with the given text.
+ /// </summary>
+ /// <param name="text">the text of the button</param>
+ /// <returns>the created button</returns>
+ public Button CreateButton(string text)
+ {
+ return CreateTextWidget(mFactory.CreateButton(), text);
+ }
+
+ /// <summary>
+ /// Creates a new empty widget.
+ /// </summary>
+ /// <returns>the created widget</returns>
+ public EmptyWidget CreateEmptyWidget()
+ {
+ return mFactory.CreateEmptyWidget();
+ }
+
+ /// <summary>
+ /// Creates a new select button with the given text options.
+ /// </summary>
+ /// <param name="values">the values of the button</param>
+ /// <returns>the created button</returns>
+ public SelectButton CreateSelectButton(string[] values)
+ {
+ var widget = CreateTextWidget(mFactory.CreateSelectButton(), "");
+ foreach (var val in values)
+ widget.Values.Add(val);
+ return widget;
+ }
+
+ /// <summary>
+ /// Creates a new select button without options.
+ /// </summary>
+ /// <returns>the created button</returns>
+ public SelectButton CreateSelectButton()
+ {
+ return mFactory.CreateSelectButton();
+ }
+
+ /// <summary>
+ /// Creates a new label with the given text.
+ /// </summary>
+ /// <param name="text">the text of the label</param>
+ /// <returns>the created label</returns>
+ public Label CreateLabel(string text)
+ {
+ return CreateTextWidget(mFactory.CreateLabel(), text);
+ }
+
+ private T CreateTextWidget<T>(T widget, string text) where T : ITextWidget
+ {
+ widget.Text = text;
+ return widget;
+ }
+
+ public AchievementBox CreateAchievementBox()
+ {
+ return mFactory.CreateAchievementBox();
+ }
+ }
+}