2019-08-12 19:44:16 +02:00
using System ;
2019-09-20 13:48:49 +02:00
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 ;
2019-08-12 19:44:16 +02:00
using MLEM.Extensions ;
2019-11-02 14:53:59 +01:00
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>
2019-08-09 19:28:48 +02:00
public class Panel : Element {
2020-05-22 17:02:24 +02:00
/// <summary>
/// The texture that this panel should have, or null if it should be invisible.
/// </summary>
2019-10-14 21:28:12 +02:00
public StyleProp < NinePatch > Texture ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The scroll bar that this panel contains.
/// This is only nonnull if <see cref="scrollOverflow"/> is true.
/// </summary>
2019-08-12 19:44:16 +02:00
public readonly ScrollBar ScrollBar ;
private readonly bool scrollOverflow ;
private RenderTarget2D renderTarget ;
2019-09-20 13:48:49 +02:00
private readonly List < Element > relevantChildren = new List < Element > ( ) ;
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="scrollerSize">The size of the <see cref="ScrollBar"/>'s scroller</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>
2019-12-14 14:10:38 +01:00
public Panel ( Anchor anchor , Vector2 size , Vector2 positionOffset , bool setHeightBasedOnChildren = false , bool scrollOverflow = false , Point ? scrollerSize = null , bool autoHideScrollbar = true ) : base ( anchor , size ) {
2019-08-09 22:04:26 +02:00
this . PositionOffset = positionOffset ;
2019-08-11 18:50:39 +02:00
this . SetHeightBasedOnChildren = setHeightBasedOnChildren ;
2019-08-12 19:44:16 +02:00
this . scrollOverflow = scrollOverflow ;
2019-09-26 22:16:21 +02:00
this . ChildPadding = new Vector2 ( 5 ) ;
2019-08-28 18:27:17 +02:00
this . CanBeSelected = false ;
2019-08-12 19:44:16 +02:00
if ( scrollOverflow ) {
var scrollSize = scrollerSize ? ? Point . Zero ;
this . ScrollBar = new ScrollBar ( Anchor . TopRight , new Vector2 ( scrollSize . X , 1 ) , scrollSize . Y , 0 ) {
StepPerScroll = 10 ,
2019-09-20 13:48:49 +02:00
OnValueChanged = ( element , value ) = > this . ScrollChildren ( ) ,
2019-09-01 19:53:52 +02:00
CanAutoAnchorsAttach = false ,
2019-12-14 14:10:38 +01:00
AutoHideWhenEmpty = autoHideScrollbar ,
IsHidden = autoHideScrollbar
2019-08-12 19:44:16 +02:00
} ;
// modify the padding so that the scroll bar isn't over top of something else
2019-08-13 23:54:29 +02:00
this . ScrollBar . PositionOffset - = new Vector2 ( scrollSize . X + 1 , 0 ) ;
2019-12-14 14:10:38 +01:00
if ( autoHideScrollbar )
this . ScrollBar . OnAutoHide + = e = > this . ChildPadding + = new Padding ( 0 , scrollSize . X , 0 , 0 ) * ( e . IsHidden ? - 1 : 1 ) ;
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 ;
2020-06-17 01:44:16 +02:00
var firstChild = this . Children . First ( c = > c ! = this . ScrollBar ) ;
2020-06-17 01:43:08 +02:00
this . ScrollBar . CurrentValue = ( otherElement . Area . Bottom - firstChild . Area . Top - this . Area . Height / 2 ) / this . Scale ;
2019-08-28 18:58:05 +02:00
} ;
2019-12-14 14:00:12 +01:00
this . AddChild ( this . ScrollBar ) ;
2019-08-12 19:44:16 +02:00
}
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
2019-08-12 19:44:16 +02:00
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
2019-09-20 13:48:49 +02:00
this . ScrollChildren ( ) ;
2020-05-17 00:59:15 +02:00
this . ScrollSetup ( ) ;
2019-08-12 19:44:16 +02:00
}
2019-09-20 13:48:49 +02:00
private void ScrollChildren ( ) {
2019-08-24 22:27:47 +02:00
if ( ! this . scrollOverflow )
return ;
2019-11-02 14:53:59 +01:00
var offset = - this . ScrollBar . CurrentValue ;
2019-08-24 22:27:47 +02:00
foreach ( var child in this . GetChildren ( c = > c ! = this . ScrollBar , true ) )
2019-09-26 22:16:21 +02:00
child . ScrollOffset = new Vector2 ( 0 , offset ) ;
2019-09-20 13:48:49 +02:00
this . relevantChildrenDirty = true ;
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
2019-11-18 22:36:55 +01:00
public override void ForceUpdateSortedChildren ( ) {
base . ForceUpdateSortedChildren ( ) ;
if ( this . scrollOverflow )
this . relevantChildrenDirty = true ;
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
2019-11-18 22:36:55 +01:00
protected override List < Element > GetRelevantChildren ( ) {
var relevant = base . GetRelevantChildren ( ) ;
if ( this . scrollOverflow ) {
if ( this . relevantChildrenDirty ) {
this . relevantChildrenDirty = false ;
2019-09-20 13:48:49 +02:00
2019-11-18 22:36:55 +01:00
var visible = this . GetRenderTargetArea ( ) ;
this . relevantChildren . Clear ( ) ;
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-09-20 13:48:49 +02:00
}
}
}
}
2019-11-18 22:36:55 +01:00
relevant = this . relevantChildren ;
2019-09-20 13:48:49 +02:00
}
2019-11-18 22:36:55 +01:00
return relevant ;
2019-08-24 22:27:47 +02:00
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
2019-09-20 13:22:05 +02:00
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 ( ) )
2020-02-06 01:51:41 +01:00
batch . Draw ( this . Texture , this . DisplayArea , Color . White * alpha , this . Scale ) ;
2019-08-12 19:44:16 +02:00
// 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 ) {
2019-09-20 13:22:05 +02:00
base . Draw ( time , batch , alpha , blendState , samplerState , matrix ) ;
2019-12-24 17:52:16 +01:00
} else {
2019-08-12 19:44:16 +02:00
// draw the actual render target (don't apply the alpha here because it's already drawn onto with alpha)
2019-09-04 17:19:31 +02:00
batch . Draw ( this . renderTarget , this . GetRenderTargetArea ( ) , Color . White ) ;
2019-08-12 19:44:16 +02:00
}
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
2019-09-02 19:55:26 +02:00
public override void DrawEarly ( GameTime time , SpriteBatch batch , float alpha , BlendState blendState , SamplerState samplerState , Matrix matrix ) {
2019-12-28 12:59:14 +01:00
this . UpdateAreaIfDirty ( ) ;
2019-12-24 17:48:57 +01:00
if ( this . scrollOverflow & & this . renderTarget ! = null ) {
2019-08-12 19:44:16 +02:00
// 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 ( ) ;
// also draw any children early within the render target with the translation applied
base . DrawEarly ( time , batch , alpha , blendState , samplerState , trans ) ;
}
2019-09-08 16:30:55 +02:00
} else {
base . DrawEarly ( time , batch , alpha , blendState , samplerState , matrix ) ;
2019-08-12 19:44:16 +02:00
}
2019-08-09 19:28:48 +02:00
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
2019-11-02 14:53:59 +01:00
public override Element GetElementUnderPos ( Vector2 position ) {
2019-08-20 21:41:22 +02:00
// if overflow is handled, don't propagate mouse checks to hidden children
2019-08-30 18:15:50 +02:00
if ( this . scrollOverflow & & ! this . GetRenderTargetArea ( ) . Contains ( position ) )
2020-11-05 01:16:01 +01:00
return ! this . IsHidden & & this . CanBeMoused & & this . DisplayArea . Contains ( position ) ? this : null ;
2019-08-30 18:15:50 +02:00
return base . GetElementUnderPos ( position ) ;
2019-08-20 21:41:22 +02:00
}
2019-11-02 14:53:59 +01:00
private RectangleF GetRenderTargetArea ( ) {
2019-08-12 19:44:16 +02:00
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 ) ;
2019-10-14 21:28:12 +02:00
this . Texture . SetFromStyle ( style . PanelTexture ) ;
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 ( ) {
2020-05-30 22:48:09 +02:00
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 ;
// the "real" first child is the scroll bar, which we want to ignore
2020-06-17 01:44:16 +02:00
var firstChild = this . Children . First ( c = > c ! = this . ScrollBar ) ;
2020-05-17 00:59:15 +02:00
var lowestChild = this . GetLowestChild ( e = > ! e . IsHidden ) ;
// the max value of the scrollbar is the amount of non-scaled pixels taken up by overflowing components
var childrenHeight = lowestChild . Area . Bottom - firstChild . Area . Top ;
this . ScrollBar . MaxValue = ( childrenHeight - this . Area . Height ) / this . Scale + this . ChildPadding . Height ;
// update the render target
var targetArea = ( Rectangle ) this . GetRenderTargetArea ( ) ;
2020-06-01 21:34:55 +02:00
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 . GraphicsDevice , targetArea . Width , targetArea . Height ) ;
}
}
2019-08-09 19:28:48 +02:00
}
}