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 ;
2022-04-25 15:25:58 +02:00
using MLEM.Graphics ;
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.
2023-04-15 15:11:50 +02:00
/// Additionally, a panel can be set to scroll overflowing elements on construction, which causes all elements that don't fit into the panel to be hidden until scrolled to using a <see cref="ScrollBar"/>.
2020-05-22 17:02:24 +02:00
/// </summary>
2021-06-09 00:27:50 +02:00
public class Panel : Element {
2019-08-09 19:28:48 +02:00
2021-10-30 15:01:04 +02:00
/// <summary>
/// The scroll bar that this panel contains.
2023-04-15 15:11:50 +02:00
/// This is only nonnull if scrolling overflow was enabled in the constructor.
2021-10-30 15:01:04 +02:00
/// 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>
2019-10-14 21:28:12 +02:00
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>
2021-10-29 23:33:15 +02:00
/// 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>
2021-11-08 02:02:59 +01:00
/// 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.
2021-10-29 23:33:15 +02:00
/// </summary>
public StyleProp < Vector2 > ScrollerSize ;
2021-11-11 16:43:27 +01:00
/// <summary>
/// The amount of pixels of room there should be between the <see cref="ScrollBar"/> and the rest of the content
/// </summary>
2022-12-23 13:25:56 +01:00
public StyleProp < float > ScrollBarOffset {
get = > this . scrollBarOffset ;
set {
this . scrollBarOffset = value ;
this . SetAreaDirty ( ) ;
}
}
2021-10-30 15:01:04 +02:00
private readonly List < Element > relevantChildren = new List < Element > ( ) ;
2023-08-15 10:30:49 +02:00
private readonly HashSet < Element > scrolledChildren = new HashSet < Element > ( ) ;
2019-08-12 19:44:16 +02:00
private readonly bool scrollOverflow ;
2021-10-30 15:01:04 +02:00
2022-01-30 16:32:45 +01:00
private RenderTarget2D renderTarget ;
2019-09-20 13:48:49 +02:00
private bool relevantChildrenDirty ;
2021-11-14 20:28:53 +01:00
private float scrollBarChildOffset ;
2022-12-23 13:25:56 +01:00
private StyleProp < float > scrollBarOffset ;
2023-08-14 17:37:26 +02:00
private float lastScrollOffset ;
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>
2023-08-14 17:37:26 +02:00
/// <param name="autoHideScrollbar">Whether the scroll bar should be hidden automatically if the panel does not contain enough children to allow for scrolling. This only has an effect if <paramref name="scrollOverflow"/> is <see langword="true"/>.</param>
2021-10-29 23:33:15 +02:00
public Panel ( Anchor anchor , Vector2 size , Vector2 positionOffset , bool setHeightBasedOnChildren = false , bool scrollOverflow = false , 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-08-28 18:27:17 +02:00
this . CanBeSelected = false ;
2019-08-12 19:44:16 +02:00
if ( scrollOverflow ) {
2021-10-29 23:33:15 +02:00
this . ScrollBar = new ScrollBar ( Anchor . TopRight , Vector2 . Zero , 0 , 0 ) {
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
} ;
2019-08-28 18:58:05 +02:00
// handle automatic element selection, the scroller needs to scroll to the right location
2022-03-10 18:04:36 +01:00
this . OnSelectedElementChanged + = ( _ , e ) = > {
2019-09-09 17:12:36 +02:00
if ( ! this . Controls . IsAutoNavMode )
2019-08-28 18:58:05 +02:00
return ;
2022-03-10 18:04:36 +01:00
if ( e = = null | | ! e . GetParentTree ( ) . Contains ( this ) )
2019-08-28 18:58:05 +02:00
return ;
2022-09-24 18:46:33 +02:00
this . ScrollToElement ( e ) ;
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 ) {
2022-07-27 11:19:40 +02:00
if ( child ! = this . ScrollBar & & ! child . Anchor . IsAuto ( ) )
2019-08-12 19:44:16 +02:00
throw new NotSupportedException ( $"A panel that handles overflow can't contain non-automatic anchors ({child})" ) ;
}
}
base . ForceUpdateArea ( ) ;
2021-12-11 17:39:49 +01:00
this . SetScrollBarStyle ( ) ;
}
2020-05-22 17:02:24 +02:00
2021-12-11 17:39:49 +01:00
/// <inheritdoc />
public override void SetAreaAndUpdateChildren ( RectangleF area ) {
base . SetAreaAndUpdateChildren ( area ) ;
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
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
2019-11-18 22:36:55 +01:00
public override void ForceUpdateSortedChildren ( ) {
base . ForceUpdateSortedChildren ( ) ;
2021-03-24 22:01:02 +01:00
if ( this . scrollOverflow )
2021-11-05 23:22:39 +01:00
this . ForceUpdateRelevantChildren ( ) ;
2021-03-24 22:01:02 +01:00
}
/// <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 ) ;
2023-05-23 11:18:10 +02:00
// when removing children, our scroll bar might have to be hidden
// if we don't do this before adding children again, they might incorrectly assume that the scroll bar will still be visible and adjust their size accordingly
2023-05-26 23:08:45 +02:00
if ( this . System ! = null )
this . ScrollSetup ( ) ;
2021-03-24 22:01:02 +01:00
}
/// <inheritdoc />
public override void RemoveChildren ( Func < Element , bool > condition = null ) {
base . RemoveChildren ( e = > e ! = this . ScrollBar & & ( condition = = null | | condition ( e ) ) ) ;
2019-11-18 22:36:55 +01:00
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc />
2022-04-25 15:25:58 +02:00
public override void Draw ( GameTime time , SpriteBatch batch , float alpha , SpriteBatchContext context ) {
2022-03-26 21:13:05 +01:00
// draw children onto the render target if we have one
2022-01-30 16:32:45 +01:00
if ( this . scrollOverflow & & this . renderTarget ! = null ) {
2022-03-26 21:13:05 +01:00
this . UpdateAreaIfDirty ( ) ;
batch . End ( ) ;
// force render target usage to preserve so that previous content isn't cleared
var lastUsage = batch . GraphicsDevice . PresentationParameters . RenderTargetUsage ;
batch . GraphicsDevice . PresentationParameters . RenderTargetUsage = RenderTargetUsage . PreserveContents ;
2022-01-30 16:32:45 +01:00
using ( batch . GraphicsDevice . WithRenderTarget ( this . renderTarget ) ) {
batch . GraphicsDevice . Clear ( Color . Transparent ) ;
// offset children by the render target's location
var area = this . GetRenderTargetArea ( ) ;
// do the usual draw, but within the render target
2022-04-25 15:25:58 +02:00
var trans = context ;
trans . TransformMatrix = Matrix . CreateTranslation ( - area . X , - area . Y , 0 ) ;
batch . Begin ( trans ) ;
base . Draw ( time , batch , alpha , trans ) ;
2022-01-30 16:32:45 +01:00
batch . End ( ) ;
}
2022-03-26 21:13:05 +01:00
batch . GraphicsDevice . PresentationParameters . RenderTargetUsage = lastUsage ;
2022-04-25 15:25:58 +02:00
batch . Begin ( context ) ;
2022-03-26 21:13:05 +01:00
}
if ( this . Texture . HasValue ( ) )
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
if ( ! this . scrollOverflow | | this . renderTarget = = null ) {
2022-04-25 15:25:58 +02:00
base . Draw ( time , batch , alpha , context ) ;
2022-03-26 21:13:05 +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 ) ;
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
2021-11-22 15:13:08 +01:00
var transformed = this . TransformInverse ( position ) ;
2022-01-30 16:32:45 +01:00
if ( this . scrollOverflow & & ! this . GetRenderTargetArea ( ) . Contains ( transformed ) )
2021-11-22 15:13:08 +01:00
return ! this . IsHidden & & this . CanBeMoused & & this . DisplayArea . Contains ( transformed ) ? this : null ;
2019-08-30 18:15:50 +02:00
return base . GetElementUnderPos ( position ) ;
2019-08-20 21:41:22 +02:00
}
2022-09-24 18:46:33 +02:00
/// <summary>
/// Scrolls this panel's <see cref="ScrollBar"/> to the given <see cref="Element"/> in such a way that its center is positioned in the center of this panel.
/// </summary>
/// <param name="element">The element to scroll to.</param>
public void ScrollToElement ( Element element ) {
this . ScrollToElement ( element . Area . Center . Y ) ;
}
/// <summary>
/// Scrolls this panel's <see cref="ScrollBar"/> to the given <paramref name="elementY"/> coordinate in such a way that the coordinate is positioned in the center of this panel.
/// </summary>
/// <param name="elementY">The y coordinate to scroll to, which should have this element's <see cref="Element.Scale"/> applied.</param>
public void ScrollToElement ( float elementY ) {
var firstChild = this . Children . FirstOrDefault ( c = > c ! = this . ScrollBar ) ;
if ( firstChild = = null )
return ;
this . ScrollBar . CurrentValue = ( elementY - this . Area . Height / 2 - firstChild . Area . Top ) / this . Scale + this . ChildPadding . Value . Height / 2 ;
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 ) ;
2021-12-21 11:54:32 +01:00
this . Texture = this . Texture . OrStyle ( style . PanelTexture ) ;
2023-06-14 14:54:49 +02:00
this . DrawColor = this . DrawColor . OrStyle ( style . PanelColor ) ;
2021-12-21 11:54:32 +01:00
this . StepPerScroll = this . StepPerScroll . OrStyle ( style . PanelStepPerScroll ) ;
this . ScrollerSize = this . ScrollerSize . OrStyle ( style . PanelScrollerSize ) ;
this . ScrollBarOffset = this . ScrollBarOffset . OrStyle ( style . PanelScrollBarOffset ) ;
this . ChildPadding = this . ChildPadding . OrStyle ( style . PanelChildPadding ) ;
2021-10-29 23:33:15 +02:00
this . SetScrollBarStyle ( ) ;
2019-08-10 21:37:10 +02:00
}
2022-09-24 18:46:33 +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 ;
}
/// <inheritdoc />
protected override void OnChildAreaDirty ( Element child , bool grandchild ) {
base . OnChildAreaDirty ( child , grandchild ) ;
// we only need to scroll when a grandchild changes, since all of our children are forced
// to be auto-anchored and so will automatically propagate their changes up to us
2023-05-21 11:11:52 +02:00
if ( grandchild ) {
2022-09-24 18:46:33 +02:00
this . ScrollChildren ( ) ;
2023-05-21 11:11:52 +02:00
// we also need to re-setup here in case the child is involved in a special GetTotalCoveredArea
2023-05-21 11:28:07 +02:00
if ( ! this . AreaDirty )
this . ScrollSetup ( ) ;
2023-05-21 11:11:52 +02:00
}
2022-09-24 18:46:33 +02:00
}
2022-11-24 19:46:20 +01:00
/// <inheritdoc />
protected internal override void RemovedFromUi ( ) {
base . RemovedFromUi ( ) ;
// we dispose our render target when removing so that it doesn't cause a memory leak
// if we're added back afterwards, it'll be recreated in ScrollSetup anyway
this . renderTarget ? . Dispose ( ) ;
this . renderTarget = null ;
}
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 ;
2021-11-08 02:11:05 +01:00
2022-09-24 11:04:23 +02:00
float childrenHeight ;
if ( this . Children . Count > 1 ) {
var firstChild = this . Children . FirstOrDefault ( c = > c ! = this . ScrollBar ) ;
2023-05-21 11:11:52 +02:00
var lowestChild = this . GetLowestChild ( c = > c ! = this . ScrollBar & & ! c . IsHidden , true ) ;
childrenHeight = lowestChild . GetTotalCoveredArea ( false ) . Bottom - firstChild . Area . Top ;
2022-09-24 11:04:23 +02:00
} else {
// if we only have one child (the scroll bar), then the children take up no visual height
childrenHeight = 0 ;
}
2021-11-08 02:11:05 +01:00
2022-09-24 11:04:23 +02:00
// the max value of the scroll bar is the amount of non-scaled pixels taken up by overflowing components
2021-11-08 02:06:50 +01:00
var scrollBarMax = ( childrenHeight - this . ChildPaddedArea . Height ) / this . Scale ;
2022-06-15 11:38:11 +02:00
if ( ! this . ScrollBar . MaxValue . Equals ( scrollBarMax , Element . Epsilon ) ) {
2021-11-08 02:06:50 +01:00
this . ScrollBar . MaxValue = scrollBarMax ;
2021-11-14 20:28:53 +01:00
this . relevantChildrenDirty = true ;
2022-09-24 11:04:23 +02:00
}
2021-11-14 20:28:53 +01:00
2022-09-24 11:04:23 +02:00
// update child padding based on whether the scroll bar is visible
var childOffset = this . ScrollBar . IsHidden ? 0 : this . ScrollerSize . Value . X + this . ScrollBarOffset ;
2023-08-14 18:32:27 +02:00
var childOffsetDelta = childOffset - this . scrollBarChildOffset ;
if ( ! childOffsetDelta . Equals ( 0 , Element . Epsilon ) ) {
2022-09-24 11:04:23 +02:00
this . scrollBarChildOffset = childOffset ;
2023-08-14 18:32:27 +02:00
this . ChildPadding + = new Padding ( 0 , childOffsetDelta , 0 , 0 ) ;
2021-11-08 02:06:50 +01:00
}
2021-11-08 02:11:05 +01:00
2021-11-08 02:02:59 +01:00
// 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
2021-11-08 02:11:05 +01:00
var scrollerHeight = Math . Min ( this . ChildPaddedArea . Height / childrenHeight / this . Scale , 1 ) * this . ScrollBar . Area . Height ;
2021-11-08 02:02:59 +01:00
this . ScrollBar . ScrollerSize = new Vector2 ( this . ScrollerSize . Value . X , Math . Max ( this . ScrollerSize . Value . Y , scrollerHeight ) ) ;
2022-01-30 16:32:45 +01:00
// update the render target
2023-08-14 17:52:16 +02:00
var area = ( Rectangle ) this . GetRenderTargetArea ( ) ;
if ( area . Width < = 0 | | area . Height < = 0 ) {
2022-11-24 18:38:51 +01:00
this . renderTarget ? . Dispose ( ) ;
this . renderTarget = null ;
2022-01-30 16:32:45 +01:00
return ;
2022-11-24 18:38:51 +01:00
}
2023-08-14 17:52:16 +02:00
if ( this . renderTarget = = null | | area . Width ! = this . renderTarget . Width | | area . Height ! = this . renderTarget . Height ) {
2022-11-24 18:38:51 +01:00
this . renderTarget ? . Dispose ( ) ;
2023-08-14 17:52:16 +02:00
this . renderTarget = new RenderTarget2D ( this . System . Game . GraphicsDevice , area . Width , area . Height , false , SurfaceFormat . Color , DepthFormat . None , 0 , RenderTargetUsage . PreserveContents ) ;
2022-01-30 16:32:45 +01:00
this . relevantChildrenDirty = true ;
}
}
2021-10-29 23:33:15 +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 ) ;
2021-11-11 16:43:27 +01:00
this . ScrollBar . PositionOffset = new Vector2 ( - this . ScrollerSize . Value . X - this . ScrollBarOffset , 0 ) ;
2021-10-29 23:33:15 +02:00
}
2021-11-05 23:22:39 +01:00
private void ForceUpdateRelevantChildren ( ) {
this . relevantChildrenDirty = false ;
this . relevantChildren . Clear ( ) ;
2022-01-30 16:32:45 +01:00
var visible = this . GetRenderTargetArea ( ) ;
2021-11-05 23:22:39 +01:00
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 ;
}
}
}
}
}
2022-09-24 18:46:33 +02:00
private RectangleF GetRenderTargetArea ( ) {
2023-08-14 17:37:26 +02:00
var area = this . ChildPaddedArea . OffsetCopy ( this . ScaledScrollOffset ) ;
2022-09-24 18:46:33 +02:00
area . X = this . DisplayArea . X ;
area . Width = this . DisplayArea . Width ;
return area ;
}
private void ScrollChildren ( ) {
2023-08-15 10:30:49 +02:00
this . scrolledChildren . RemoveWhere ( c = > ! c . GetParentTree ( ) . Contains ( this ) ) ;
2022-09-24 18:46:33 +02:00
if ( ! this . scrollOverflow )
return ;
// we ignore false grandchildren so that the children of the scroll bar stay in place
2023-08-15 10:30:49 +02:00
foreach ( var child in this . GetChildren ( c = > c ! = this . ScrollBar , true , true ) ) {
// if a child was newly added later, the last scroll offset was never applied
if ( this . scrolledChildren . Add ( child ) )
child . ScrollOffset . Y - = this . lastScrollOffset ;
2023-08-14 17:37:26 +02:00
child . ScrollOffset . Y + = ( this . lastScrollOffset - this . ScrollBar . CurrentValue ) ;
2023-08-15 10:30:49 +02:00
}
2023-08-14 17:37:26 +02:00
this . lastScrollOffset = this . ScrollBar . CurrentValue ;
2022-09-24 18:46:33 +02:00
this . relevantChildrenDirty = true ;
}
2019-08-09 19:28:48 +02:00
}
2022-06-17 18:23:47 +02:00
}