1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-05-23 17:13:38 +02:00

added scroll bar and overflow handling panel

This commit is contained in:
Ellpeck 2019-08-12 19:44:16 +02:00
parent 09fbdfd54e
commit 4624219b4e
13 changed files with 245 additions and 60 deletions

View file

@ -26,7 +26,7 @@ namespace Examples {
this.testPatch = new NinePatch(new TextureRegion(this.testTexture, 0, 8, 24, 24), 8);
base.LoadContent();
var style = new UiStyle {
var style = new UntexturedStyle(this.SpriteBatch) {
Font = new GenericSpriteFont(LoadContent<SpriteFont>("Fonts/TestFont")),
TextScale = 0.2F,
PanelTexture = this.testPatch,
@ -39,7 +39,7 @@ namespace Examples {
this.UiSystem.Style = style;
this.UiSystem.GlobalScale = 5;
var root = new Panel(Anchor.Center, new Vector2(100, 120), Point.Zero, true);
var root = new Panel(Anchor.Center, new Vector2(100, 80), Point.Zero, false, true, new Point(5, 10));
this.UiSystem.Add("Test", root);
root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "This is a test text that is hopefully long enough to cover at least a few lines, otherwise it would be very sad."));
@ -53,7 +53,7 @@ namespace Examples {
root.AddChild(new Button(Anchor.AutoCenter, new Vector2(1, 15), "Change Style") {
OnClicked = (element, button) => {
if (button == MouseButton.Left)
this.UiSystem.Style = this.UiSystem.Style is UntexturedStyle ? style : untexturedStyle;
this.UiSystem.Style = this.UiSystem.Style == untexturedStyle ? style : untexturedStyle;
},
HasCustomStyle = true,
Texture = this.testPatch,
@ -75,6 +75,7 @@ namespace Examples {
}
});
root.AddChild(new Button(Anchor.AutoInline, new Vector2(30, 15), "Woop") {
PositionOffset = new Point(2, 0),
OnClicked = (element, button) => CoroutineHandler.Start(Woop(element))
});
}

View file

@ -38,10 +38,10 @@ namespace MLEM.Ui.Elements {
} while (measure.X <= this.DisplayArea.Size.X / this.Scale && measure.Y <= this.DisplayArea.Size.Y / this.Scale);
}
public override void Draw(GameTime time, SpriteBatch batch, float alpha) {
var pos = this.DisplayArea.Location.ToVector2() + this.DisplayArea.Size.ToVector2() / 2;
public override void Draw(GameTime time, SpriteBatch batch, float alpha, Point offset) {
var pos = this.DisplayArea.Location.ToVector2() + this.DisplayArea.Size.ToVector2() / 2 + offset.ToVector2();
this.font.DrawCenteredString(batch, this.Text, pos, this.textScale * this.Scale, this.Color * alpha, true, true);
base.Draw(time, batch, alpha);
base.Draw(time, batch, alpha, offset);
}
protected override void InitStyle(UiStyle style) {

View file

@ -21,7 +21,7 @@ namespace MLEM.Ui.Elements {
}
}
public override void Draw(GameTime time, SpriteBatch batch, float alpha) {
public override void Draw(GameTime time, SpriteBatch batch, float alpha, Point offset) {
var tex = this.Texture;
var color = Color.White * alpha;
if (this.IsMouseOver) {
@ -29,8 +29,8 @@ namespace MLEM.Ui.Elements {
tex = this.HoveredTexture;
color = this.HoveredColor * alpha;
}
batch.Draw(tex, this.DisplayArea, color, this.Scale);
base.Draw(time, batch, alpha);
batch.Draw(tex, this.DisplayArea.OffsetCopy(offset), color, this.Scale);
base.Draw(time, batch, alpha, offset);
}
protected override void InitStyle(UiStyle style) {

View file

@ -63,6 +63,15 @@ namespace MLEM.Ui.Elements {
this.SetAreaDirty();
}
}
public Rectangle ChildPaddedArea {
get {
var padded = this.Area;
padded.Location += this.ScaledChildPadding;
padded.Width -= this.ScaledChildPadding.X * 2;
padded.Height -= this.ScaledChildPadding.Y * 2;
return padded;
}
}
public Point ScaledChildPadding => this.childPadding.Multiply(this.Scale);
public MouseClickCallback OnClicked;
@ -143,38 +152,42 @@ namespace MLEM.Ui.Elements {
this.SetAreaDirty();
}
public T AddChild<T>(T element, int index = -1) where T : Element {
public T AddChild<T>(T element, int index = -1, bool propagateInfo = true) where T : Element {
if (index < 0 || index > this.Children.Count)
index = this.Children.Count;
this.Children.Insert(index, element);
element.Parent = this;
element.PropagateRoot(this.Root);
element.PropagateUiSystem(this.System);
if (propagateInfo) {
element.Parent = this;
element.PropagateRoot(this.Root);
element.PropagateUiSystem(this.System);
}
this.SetSortedChildrenDirty();
this.SetAreaDirty();
return element;
}
public void RemoveChild(Element element) {
public void RemoveChild(Element element, bool propagateInfo = true) {
this.Children.Remove(element);
element.Parent = null;
element.PropagateRoot(null);
element.PropagateUiSystem(null);
if (propagateInfo) {
element.Parent = null;
element.PropagateRoot(null);
element.PropagateUiSystem(null);
}
this.SetSortedChildrenDirty();
this.SetAreaDirty();
}
public void MoveToFront() {
if (this.Parent != null) {
this.Parent.RemoveChild(this);
this.Parent.AddChild(this);
this.Parent.RemoveChild(this, false);
this.Parent.AddChild(this, -1, false);
}
}
public void MoveToBack() {
if (this.Parent != null) {
this.Parent.RemoveChild(this);
this.Parent.AddChild(this, 0);
this.Parent.RemoveChild(this, false);
this.Parent.AddChild(this, 0, false);
}
}
@ -206,15 +219,7 @@ namespace MLEM.Ui.Elements {
public virtual void ForceUpdateArea() {
this.areaDirty = false;
Rectangle parentArea;
if (this.Parent != null) {
parentArea = this.Parent.area;
parentArea.Location += this.Parent.ScaledChildPadding;
parentArea.Width -= this.Parent.ScaledChildPadding.X * 2;
parentArea.Height -= this.Parent.ScaledChildPadding.Y * 2;
} else {
parentArea = this.system.GraphicsDevice.Viewport.Bounds;
}
var parentArea = this.Parent != null ? this.Parent.ChildPaddedArea : this.system.Viewport;
var parentCenterX = parentArea.X + parentArea.Width / 2;
var parentCenterY = parentArea.Y + parentArea.Height / 2;
@ -338,17 +343,17 @@ namespace MLEM.Ui.Elements {
child.Update(time);
}
public virtual void Draw(GameTime time, SpriteBatch batch, float alpha) {
public virtual void Draw(GameTime time, SpriteBatch batch, float alpha, Point offset) {
foreach (var child in this.SortedChildren) {
if (!child.IsHidden)
child.Draw(time, batch, alpha * child.DrawAlpha);
child.Draw(time, batch, alpha * child.DrawAlpha, offset);
}
}
public virtual void DrawUnbound(GameTime time, SpriteBatch batch, float alpha, float scale, BlendState blendState = null, SamplerState samplerState = null) {
public virtual void DrawUnbound(GameTime time, SpriteBatch batch, float alpha, BlendState blendState = null, SamplerState samplerState = null) {
foreach (var child in this.SortedChildren) {
if (!child.IsHidden)
child.DrawUnbound(time, batch, alpha * child.DrawAlpha, scale, blendState, samplerState);
child.DrawUnbound(time, batch, alpha * child.DrawAlpha, blendState, samplerState);
}
}

View file

@ -19,9 +19,9 @@ namespace MLEM.Ui.Elements {
return this.scaleToImage ? this.texture.Size : base.CalcActualSize(parentArea);
}
public override void Draw(GameTime time, SpriteBatch batch, float alpha) {
batch.Draw(this.texture, this.DisplayArea, this.Color * alpha);
base.Draw(time, batch, alpha);
public override void Draw(GameTime time, SpriteBatch batch, float alpha, Point offset) {
batch.Draw(this.texture, this.DisplayArea.OffsetCopy(offset), this.Color * alpha);
base.Draw(time, batch, alpha, offset);
}
}

View file

@ -1,5 +1,8 @@
using System;
using System.Diagnostics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
using MLEM.Textures;
using MLEM.Ui.Style;
@ -7,17 +10,107 @@ namespace MLEM.Ui.Elements {
public class Panel : Element {
public NinePatch Texture;
public readonly ScrollBar ScrollBar;
private readonly bool scrollOverflow;
private RenderTarget2D renderTarget;
public Panel(Anchor anchor, Vector2 size, Point positionOffset, bool setHeightBasedOnChildren = false, NinePatch texture = null) : base(anchor, size) {
this.Texture = texture;
public Panel(Anchor anchor, Vector2 size, Point positionOffset, bool setHeightBasedOnChildren = false, bool scrollOverflow = false, Point? scrollerSize = null) : base(anchor, size) {
this.PositionOffset = positionOffset;
this.SetHeightBasedOnChildren = setHeightBasedOnChildren;
this.scrollOverflow = scrollOverflow;
this.ChildPadding = new Point(5);
if (scrollOverflow) {
var scrollSize = scrollerSize ?? Point.Zero;
this.ScrollBar = new ScrollBar(Anchor.TopRight, new Vector2(scrollSize.X, 1), scrollSize.Y, 0) {
StepPerScroll = 10,
OnValueChanged = (element, value) => {
var firstChild = this.Children[0];
// if the first child is the scrollbar, there are no other children
if (firstChild == element)
return;
// as all children have to be auto-aligned, moving the first one up will move all others
firstChild.PositionOffset = new Point(firstChild.PositionOffset.X, -value.Ceil());
this.ForceUpdateArea();
}
};
this.AddChild(this.ScrollBar);
// modify the padding so that the scroll bar isn't over top of something else
this.ScrollBar.PositionOffset -= new Point(scrollSize.X + 1, 0);
this.ChildPadding += new Point(scrollSize.X, 0);
}
}
public override void Draw(GameTime time, SpriteBatch batch, float alpha) {
batch.Draw(this.Texture, this.DisplayArea, Color.White * alpha, this.Scale);
base.Draw(time, batch, alpha);
public override void ForceUpdateArea() {
if (this.scrollOverflow) {
// sanity check
if (this.SetHeightBasedOnChildren)
throw new NotSupportedException("A panel can't both set height based on children and scroll overflow");
foreach (var child in this.Children) {
if (child != this.ScrollBar && child.Anchor < Anchor.AutoLeft)
throw new NotSupportedException($"A panel that handles overflow can't contain non-automatic anchors ({child})");
if (child is Panel panel && panel.scrollOverflow)
throw new NotSupportedException($"A panel that scrolls overflow cannot contain another panel that scrolls overflow ({child})");
}
// move the scrollbar to the front so it isn't used for auto-aligning
this.ScrollBar.MoveToFront();
}
base.ForceUpdateArea();
if (this.scrollOverflow) {
var firstChild = this.Children[0];
// if the first child is the scrollbar, then we know there's no other children
if (firstChild == this.ScrollBar)
return;
var lastChild = this.Children[this.Children.Count - 2];
// the max value of the scrollbar is the amount of non-scaled pixels taken up by overflowing components
var childrenHeight = lastChild.Area.Bottom - firstChild.Area.Top;
this.ScrollBar.MaxValue = (childrenHeight - this.Area.Height) / this.Scale + this.ChildPadding.Y * 2;
// update the render target
var targetArea = this.GetRenderTargetArea();
if (this.renderTarget == null || targetArea.Width != this.renderTarget.Width || targetArea.Height != this.renderTarget.Height) {
if (this.renderTarget != null)
this.renderTarget.Dispose();
this.renderTarget = new RenderTarget2D(this.System.GraphicsDevice, targetArea.Width, targetArea.Height);
}
}
}
public override void Draw(GameTime time, SpriteBatch batch, float alpha, Point offset) {
batch.Draw(this.Texture, this.DisplayArea.OffsetCopy(offset), Color.White * alpha, this.Scale);
// if we handle overflow, draw using the render target in DrawUnbound
if (!this.scrollOverflow) {
base.Draw(time, batch, alpha, offset);
} else {
// draw the actual render target (don't apply the alpha here because it's already drawn onto with alpha)
batch.Draw(this.renderTarget, this.GetRenderTargetArea().OffsetCopy(offset), Color.White);
}
}
public override void DrawUnbound(GameTime time, SpriteBatch batch, float alpha, BlendState blendState = null, SamplerState samplerState = null) {
if (this.scrollOverflow) {
// draw children onto the render target
batch.GraphicsDevice.SetRenderTarget(this.renderTarget);
batch.GraphicsDevice.Clear(Color.Transparent);
batch.Begin(SpriteSortMode.Deferred, blendState, samplerState);
// offset children by the render target's location
var area = this.GetRenderTargetArea();
base.Draw(time, batch, alpha, new Point(-area.X, -area.Y));
batch.End();
batch.GraphicsDevice.SetRenderTarget(null);
}
base.DrawUnbound(time, batch, alpha, blendState, samplerState);
}
private Rectangle GetRenderTargetArea() {
var area = this.ChildPaddedArea;
area.X = this.DisplayArea.X;
area.Width = this.DisplayArea.Width;
return area;
}
protected override void InitStyle(UiStyle style) {

View file

@ -46,19 +46,18 @@ namespace MLEM.Ui.Elements {
return new Point(size.X, height.Ceil());
}
public override void Draw(GameTime time, SpriteBatch batch, float alpha) {
base.Draw(time, batch, alpha);
public override void Draw(GameTime time, SpriteBatch batch, float alpha, Point offset) {
var pos = this.DisplayArea.Location.ToVector2();
var offset = new Vector2();
var off = offset.ToVector2();
foreach (var line in this.splitText) {
if (this.centerText) {
this.font.DrawCenteredString(batch, line, pos + offset + new Vector2(this.DisplayArea.Width / 2, 0), this.TextScale * this.Scale, Color.White * alpha);
this.font.DrawCenteredString(batch, line, pos + off + new Vector2(this.DisplayArea.Width / 2, 0), this.TextScale * this.Scale, Color.White * alpha);
} else {
this.font.DrawString(batch, line, pos + offset, Color.White * alpha, 0, Vector2.Zero, this.TextScale * this.Scale, SpriteEffects.None, 0);
this.font.DrawString(batch, line, pos + off, Color.White * alpha, 0, Vector2.Zero, this.TextScale * this.Scale, SpriteEffects.None, 0);
}
offset.Y += this.lineHeight + 1;
off.Y += this.lineHeight + 1;
}
base.Draw(time, batch, alpha, offset);
}
protected override void InitStyle(UiStyle style) {

View file

@ -0,0 +1,71 @@
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
using MLEM.Textures;
using MLEM.Ui.Style;
namespace MLEM.Ui.Elements {
public class ScrollBar : Element {
public NinePatch Background;
public NinePatch ScrollerTexture;
public Color HoveredColor;
public Point ScrollerOffset;
public Point ScrollerSize;
private float maxValue;
public float MaxValue {
get => this.maxValue;
set {
this.maxValue = Math.Max(0, value);
// force current value to be clamped
this.CurrentValue = this.currValue;
}
}
private float currValue;
public float CurrentValue {
get => this.currValue;
set {
var val = MathHelper.Clamp(value, 0, this.maxValue);
if (this.currValue != val) {
this.currValue = val;
this.OnValueChanged?.Invoke(this, val);
}
}
}
public float StepPerScroll = 1;
public ValueChanged OnValueChanged;
public ScrollBar(Anchor anchor, Vector2 size, int scrollerHeight, float maxValue) : base(anchor, size) {
this.maxValue = maxValue;
this.ScrollerSize = new Point(size.X.Floor(), scrollerHeight);
}
public override void Update(GameTime time) {
base.Update(time);
var scroll = this.Input.LastScrollWheel - this.Input.ScrollWheel;
if (scroll != 0)
this.CurrentValue += this.StepPerScroll * Math.Sign(scroll);
}
public override void Draw(GameTime time, SpriteBatch batch, float alpha, Point offset) {
batch.Draw(this.Background, this.DisplayArea.OffsetCopy(offset), Color.White * alpha, this.Scale);
var scrollerPos = new Point(this.DisplayArea.X + offset.X + this.ScrollerOffset.X, this.DisplayArea.Y + offset.Y + this.ScrollerOffset.Y);
var scrollerYOffset = (this.currValue / this.maxValue * (this.DisplayArea.Height - this.ScrollerSize.Y * this.Scale)).Floor();
var scrollerRect = new Rectangle(scrollerPos + new Point(0, scrollerYOffset), this.ScrollerSize.Multiply(this.Scale));
batch.Draw(this.ScrollerTexture, scrollerRect, Color.White * alpha, this.Scale);
base.Draw(time, batch, alpha, offset);
}
protected override void InitStyle(UiStyle style) {
base.InitStyle(style);
this.Background = style.ScrollBarBackground;
this.ScrollerTexture = style.ScrollBarScrollerTexture;
this.HoveredColor = style.ScrollBarHoveredColor;
}
public delegate void ValueChanged(Element element, float value);
}
}

View file

@ -3,6 +3,7 @@ using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MLEM.Extensions;
using MLEM.Font;
using MLEM.Textures;
using MLEM.Ui.Style;
@ -67,7 +68,7 @@ namespace MLEM.Ui.Elements {
this.caretBlinkTimer = 0;
}
public override void Draw(GameTime time, SpriteBatch batch, float alpha) {
public override void Draw(GameTime time, SpriteBatch batch, float alpha, Point offset) {
var tex = this.Texture;
var color = Color.White * alpha;
if (this.IsMouseOver) {
@ -75,11 +76,11 @@ namespace MLEM.Ui.Elements {
tex = this.HoveredTexture;
color = this.HoveredColor * alpha;
}
batch.Draw(tex, this.DisplayArea, color, this.Scale);
batch.Draw(tex, this.DisplayArea.OffsetCopy(offset), color, this.Scale);
var caret = this.IsSelected && this.caretBlinkTimer >= 0.5F ? "|" : "";
var text = this.Text.ToString(this.textStartIndex, this.Text.Length - this.textStartIndex) + caret;
this.font.DrawCenteredString(batch, text, this.DisplayArea.Location.ToVector2() + new Vector2(this.TextOffsetX * this.Scale, this.DisplayArea.Height / 2), this.TextScale * this.Scale, Color.White * alpha, false, true);
base.Draw(time, batch, alpha);
this.font.DrawCenteredString(batch, text, this.DisplayArea.Location.ToVector2() + new Vector2(offset.X + this.TextOffsetX * this.Scale, offset.Y + this.DisplayArea.Height / 2), this.TextScale * this.Scale, Color.White * alpha, false, true);
base.Draw(time, batch, alpha, offset);
}
protected override void InitStyle(UiStyle style) {

View file

@ -12,6 +12,9 @@ namespace MLEM.Ui.Style {
public NinePatch TextFieldTexture;
public NinePatch TextFieldHoveredTexture;
public Color TextFieldHoveredColor;
public NinePatch ScrollBarBackground;
public NinePatch ScrollBarScrollerTexture;
public Color ScrollBarHoveredColor;
public IGenericFont Font;
public float TextScale = 1;

View file

@ -14,6 +14,9 @@ namespace MLEM.Ui.Style {
this.PanelTexture = GenerateTexture(batch, Color.Gray);
this.TextFieldTexture = GenerateTexture(batch, Color.MediumBlue);
this.TextFieldHoveredColor = Color.LightGray;
this.ScrollBarBackground = GenerateTexture(batch, Color.LightBlue);
this.ScrollBarScrollerTexture = GenerateTexture(batch, Color.Blue);
this.ScrollBarHoveredColor = Color.LightGray;
this.Font = new EmptyFont();
}

View file

@ -12,6 +12,7 @@ namespace MLEM.Ui {
public class UiSystem {
public readonly GraphicsDevice GraphicsDevice;
public Rectangle Viewport { get; private set; }
private readonly List<RootElement> rootElements = new List<RootElement>();
public readonly InputHandler InputHandler;
private readonly bool isInputOurs;
@ -47,8 +48,10 @@ namespace MLEM.Ui {
this.InputHandler = inputHandler ?? new InputHandler();
this.isInputOurs = inputHandler == null;
this.style = style;
this.Viewport = device.Viewport.Bounds;
window.ClientSizeChanged += (sender, args) => {
this.Viewport = device.Viewport.Bounds;
foreach (var root in this.rootElements)
root.Element.ForceUpdateArea();
};
@ -92,16 +95,16 @@ namespace MLEM.Ui {
public void Draw(GameTime time, SpriteBatch batch) {
foreach (var root in this.rootElements) {
if (root.Element.IsHidden)
continue;
batch.Begin(SpriteSortMode.Deferred, this.BlendState, this.SamplerState);
root.Element.Draw(time, batch, this.DrawAlpha * root.Element.DrawAlpha);
batch.End();
if (!root.Element.IsHidden)
root.Element.DrawUnbound(time, batch, this.DrawAlpha * root.Element.DrawAlpha, this.BlendState, this.SamplerState);
}
foreach (var root in this.rootElements) {
if (!root.Element.IsHidden)
root.Element.DrawUnbound(time, batch, this.DrawAlpha * root.Element.DrawAlpha, root.ActualScale, this.BlendState, this.SamplerState);
if (root.Element.IsHidden)
continue;
batch.Begin(SpriteSortMode.Deferred, this.BlendState, this.SamplerState);
root.Element.Draw(time, batch, this.DrawAlpha * root.Element.DrawAlpha, Point.Zero);
batch.End();
}
}

View file

@ -28,5 +28,11 @@ namespace MLEM.Extensions {
return new Point((point.X * f).Floor(), (point.Y * f).Floor());
}
public static Rectangle OffsetCopy(this Rectangle rect, Point offset) {
rect.X += offset.X;
rect.Y += offset.Y;
return rect;
}
}
}