1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-10-31 21:00:51 +01:00
MLEM/MLEM.Ui/Elements/Panel.cs

289 lines
14 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
2019-08-28 18:58:05 +02:00
using System.Linq;
2019-08-09 19:28:48 +02:00
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Extensions;
using MLEM.Misc;
2019-08-09 19:28:48 +02:00
using MLEM.Textures;
2019-08-10 21:37:10 +02:00
using MLEM.Ui.Style;
2019-08-09 19:28:48 +02:00
namespace MLEM.Ui.Elements {
2020-05-22 17:02:24 +02:00
/// <summary>
/// A panel element to be used inside of a <see cref="UiSystem"/>.
/// The panel is a complex element that displays a box as a background to all of its child elements.
/// Additionally, a panel can be set to <see cref="scrollOverflow"/> on construction, which causes all elements that don't fit into the panel to be hidden until scrolled to using a <see cref="ScrollBar"/>.
/// As this behavior is accomplished using a <see cref="RenderTarget2D"/>, scrolling panels need to have their <see cref="DrawEarly"/> methods called using <see cref="UiSystem.DrawEarly"/>.
/// </summary>
public class Panel : Element {
2019-08-09 19:28:48 +02:00
/// <summary>
/// The scroll bar that this panel contains.
/// This is only nonnull if <see cref="scrollOverflow"/> is true.
/// Note that some scroll bar styling is controlled by this panel, namely <see cref="StepPerScroll"/> and <see cref="ScrollerSize"/>.
/// </summary>
public readonly ScrollBar ScrollBar;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The texture that this panel should have, or null if it should be invisible.
/// </summary>
public StyleProp<NinePatch> Texture;
2020-05-22 17:02:24 +02:00
/// <summary>
2021-06-22 01:14:06 +02:00
/// The color that this panel's <see cref="Texture"/> should be drawn with.
/// If this style property has no value, <see cref="Color.White"/> is used.
/// </summary>
public StyleProp<Color> DrawColor;
/// <summary>
/// The amount that the scrollable area is moved per single movement of the scroll wheel
/// This value is passed to the <see cref="ScrollBar"/>'s <see cref="Elements.ScrollBar.StepPerScroll"/>
/// </summary>
public StyleProp<float> StepPerScroll;
/// <summary>
/// The size that the <see cref="ScrollBar"/>'s scroller should have, in pixels.
/// The scroller size's height specified here is the minimum height, otherwise, it is automatically calculated based on panel content.
/// </summary>
public StyleProp<Vector2> ScrollerSize;
private readonly List<Element> relevantChildren = new List<Element>();
private readonly bool scrollOverflow;
private RenderTarget2D renderTarget;
private bool relevantChildrenDirty;
2019-08-09 19:28:48 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// Creates a new panel with the given settings.
/// </summary>
/// <param name="anchor">The panel's anchor</param>
/// <param name="size">The panel's default size</param>
/// <param name="positionOffset">The panel's offset from its anchor point</param>
/// <param name="setHeightBasedOnChildren">Whether the panel should automatically calculate its height based on its children's size</param>
/// <param name="scrollOverflow">Whether this panel should automatically add a scroll bar to scroll towards elements that are beyond the area this panel covers</param>
/// <param name="autoHideScrollbar">Whether the scroll bar should be hidden automatically if the panel does not contain enough children to allow for scrolling</param>
public Panel(Anchor anchor, Vector2 size, Vector2 positionOffset, bool setHeightBasedOnChildren = false, bool scrollOverflow = false, bool autoHideScrollbar = true) : base(anchor, size) {
this.PositionOffset = positionOffset;
2019-08-11 18:50:39 +02:00
this.SetHeightBasedOnChildren = setHeightBasedOnChildren;
this.scrollOverflow = scrollOverflow;
2019-08-28 18:27:17 +02:00
this.CanBeSelected = false;
if (scrollOverflow) {
this.ScrollBar = new ScrollBar(Anchor.TopRight, Vector2.Zero, 0, 0) {
OnValueChanged = (element, value) => this.ScrollChildren(),
CanAutoAnchorsAttach = false,
AutoHideWhenEmpty = autoHideScrollbar,
IsHidden = autoHideScrollbar
};
2021-10-30 15:33:38 +02:00
if (autoHideScrollbar) {
this.ScrollBar.OnAutoHide += e => {
this.ChildPadding += new Padding(0, this.ScrollerSize.Value.X, 0, 0) * (e.IsHidden ? -1 : 1);
this.SetAreaDirty();
};
}
2019-08-28 18:58:05 +02:00
// handle automatic element selection, the scroller needs to scroll to the right location
this.OnSelectedElementChanged += (element, otherElement) => {
2019-09-09 17:12:36 +02:00
if (!this.Controls.IsAutoNavMode)
2019-08-28 18:58:05 +02:00
return;
if (otherElement == null || !otherElement.GetParentTree().Contains(this))
return;
var firstChild = this.Children.First(c => c != this.ScrollBar);
this.ScrollBar.CurrentValue = (otherElement.Area.Bottom - firstChild.Area.Top - this.Area.Height / 2) / this.Scale;
2019-08-28 18:58:05 +02:00
};
this.AddChild(this.ScrollBar);
}
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
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})");
}
}
base.ForceUpdateArea();
2020-05-22 17:02:24 +02:00
this.ScrollChildren();
2020-05-17 00:59:15 +02:00
this.ScrollSetup();
this.SetScrollBarStyle();
}
private void ScrollChildren() {
2019-08-24 22:27:47 +02:00
if (!this.scrollOverflow)
return;
var offset = new Vector2(0, -this.ScrollBar.CurrentValue);
foreach (var child in this.GetChildren(c => c != this.ScrollBar, true)) {
if (child.ScrollOffset != offset) {
child.ScrollOffset = offset;
this.relevantChildrenDirty = true;
}
}
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
public override void ForceUpdateSortedChildren() {
base.ForceUpdateSortedChildren();
if (this.scrollOverflow)
this.ForceUpdateRelevantChildren();
}
/// <inheritdoc />
public override void RemoveChild(Element element) {
if (element == this.ScrollBar)
throw new NotSupportedException("A panel that scrolls overflow cannot have its scroll bar removed from its list of children");
base.RemoveChild(element);
}
/// <inheritdoc />
public override void RemoveChildren(Func<Element, bool> condition = null) {
base.RemoveChildren(e => e != this.ScrollBar && (condition == null || condition(e)));
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
protected override IList<Element> GetRelevantChildren() {
var relevant = base.GetRelevantChildren();
if (this.scrollOverflow) {
if (this.relevantChildrenDirty)
this.ForceUpdateRelevantChildren();
relevant = this.relevantChildren;
}
return relevant;
2019-08-24 22:27:47 +02:00
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
2020-02-06 17:36:51 +01:00
if (this.Texture.HasValue())
2021-06-22 01:14:06 +02:00
batch.Draw(this.Texture, this.DisplayArea, this.DrawColor.OrDefault(Color.White) * alpha, this.Scale);
// if we handle overflow, draw using the render target in DrawUnbound
2019-12-24 17:52:16 +01:00
if (!this.scrollOverflow || this.renderTarget == null) {
base.Draw(time, batch, alpha, blendState, samplerState, matrix);
2019-12-24 17:52:16 +01:00
} 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(), Color.White);
}
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
public override void DrawEarly(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
this.UpdateAreaIfDirty();
2019-12-24 17:48:57 +01:00
if (this.scrollOverflow && this.renderTarget != null) {
// draw children onto the render target
2020-04-02 17:54:10 +02:00
using (batch.GraphicsDevice.WithRenderTarget(this.renderTarget)) {
batch.GraphicsDevice.Clear(Color.Transparent);
// offset children by the render target's location
var area = this.GetRenderTargetArea();
var trans = Matrix.CreateTranslation(-area.X, -area.Y, 0);
// do the usual draw, but within the render target
batch.Begin(SpriteSortMode.Deferred, blendState, samplerState, null, null, null, trans);
base.Draw(time, batch, alpha, blendState, samplerState, trans);
batch.End();
}
}
base.DrawEarly(time, batch, alpha, blendState, samplerState, matrix);
2019-08-09 19:28:48 +02:00
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
public override Element GetElementUnderPos(Vector2 position) {
// if overflow is handled, don't propagate mouse checks to hidden children
if (this.scrollOverflow && !this.GetRenderTargetArea().Contains(position))
return !this.IsHidden && this.CanBeMoused && this.DisplayArea.Contains(position) ? this : null;
return base.GetElementUnderPos(position);
}
private RectangleF GetRenderTargetArea() {
var area = this.ChildPaddedArea;
area.X = this.DisplayArea.X;
area.Width = this.DisplayArea.Width;
return area;
2019-08-09 19:28:48 +02:00
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
2019-08-10 21:37:10 +02:00
protected override void InitStyle(UiStyle style) {
base.InitStyle(style);
this.Texture.SetFromStyle(style.PanelTexture);
this.StepPerScroll.SetFromStyle(style.PanelStepPerScroll);
this.ScrollerSize.SetFromStyle(style.PanelScrollerSize);
2021-10-30 15:33:38 +02:00
this.ChildPadding.SetFromStyle(style.PanelChildPadding);
this.SetScrollBarStyle();
2019-08-10 21:37:10 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Prepares the panel for auto-scrolling, creating the render target and setting up the scroll bar's maximum value.
/// </summary>
2020-05-17 00:59:15 +02:00
protected virtual void ScrollSetup() {
if (!this.scrollOverflow || this.IsHidden)
2020-05-17 00:59:15 +02:00
return;
// if there is only one child, then we have just the scroll bar
if (this.Children.Count == 1)
return;
2020-05-17 00:59:15 +02:00
// the "real" first child is the scroll bar, which we want to ignore
var firstChild = this.Children.First(c => c != this.ScrollBar);
var lowestChild = this.GetLowestChild(c => c != this.ScrollBar && !c.IsHidden);
2020-05-17 00:59:15 +02:00
var childrenHeight = lowestChild.Area.Bottom - firstChild.Area.Top;
// the max value of the scrollbar is the amount of non-scaled pixels taken up by overflowing components
var scrollBarMax = (childrenHeight - this.ChildPaddedArea.Height) / this.Scale;
if (!this.ScrollBar.MaxValue.Equals(scrollBarMax, Epsilon)) {
this.ScrollBar.MaxValue = scrollBarMax;
this.relevantChildrenDirty = true;
}
// the scroller height has the same relation to the scroll bar height as the visible area has to the total height of the panel's content
var scrollerHeight = Math.Min(this.ChildPaddedArea.Height / childrenHeight / this.Scale, 1) * this.ScrollBar.Area.Height;
this.ScrollBar.ScrollerSize = new Vector2(this.ScrollerSize.Value.X, Math.Max(this.ScrollerSize.Value.Y, scrollerHeight));
2020-05-17 00:59:15 +02:00
// update the render target
var targetArea = (Rectangle) this.GetRenderTargetArea();
if (targetArea.Width <= 0 || targetArea.Height <= 0)
return;
2020-05-17 00:59:15 +02:00
if (this.renderTarget == null || targetArea.Width != this.renderTarget.Width || targetArea.Height != this.renderTarget.Height) {
if (this.renderTarget != null)
this.renderTarget.Dispose();
this.renderTarget = targetArea.IsEmpty ? null : new RenderTarget2D(this.System.Game.GraphicsDevice, targetArea.Width, targetArea.Height);
this.relevantChildrenDirty = true;
2020-05-17 00:59:15 +02:00
}
}
2021-06-08 00:29:51 +02:00
/// <inheritdoc />
public override void Dispose() {
2021-06-08 00:29:51 +02:00
if (this.renderTarget != null) {
this.renderTarget.Dispose();
this.renderTarget = null;
}
base.Dispose();
2021-06-08 00:29:51 +02:00
}
private void SetScrollBarStyle() {
if (this.ScrollBar == null)
return;
this.ScrollBar.StepPerScroll = this.StepPerScroll;
this.ScrollBar.Size = new Vector2(this.ScrollerSize.Value.X, 1);
this.ScrollBar.PositionOffset = new Vector2(-this.ScrollerSize.Value.X - 1, 0);
}
private void ForceUpdateRelevantChildren() {
this.relevantChildrenDirty = false;
this.relevantChildren.Clear();
var visible = this.GetRenderTargetArea();
foreach (var child in this.SortedChildren) {
if (child.Area.Intersects(visible)) {
this.relevantChildren.Add(child);
} else {
foreach (var c in child.GetChildren(regardGrandchildren: true)) {
if (c.Area.Intersects(visible)) {
this.relevantChildren.Add(child);
break;
}
}
}
}
}
2019-08-09 19:28:48 +02:00
}
}