2019-08-12 19:44:16 +02:00
using System ;
2019-08-21 17:00:22 +02:00
using System.Linq ;
2019-08-12 19:44:16 +02:00
using Microsoft.Xna.Framework ;
using Microsoft.Xna.Framework.Graphics ;
2019-08-31 19:32:22 +02:00
using Microsoft.Xna.Framework.Input.Touch ;
2022-04-25 15:25:58 +02:00
using MLEM.Graphics ;
2019-08-13 16:02:29 +02:00
using MLEM.Input ;
2019-11-02 14:53:59 +01:00
using MLEM.Misc ;
2019-08-12 19:44:16 +02:00
using MLEM.Textures ;
using MLEM.Ui.Style ;
2022-08-20 11:39:28 +02:00
#if FNA
using MLEM.Extensions ;
#endif
2019-08-12 19:44:16 +02:00
namespace MLEM.Ui.Elements {
2020-05-22 17:02:24 +02:00
/// <summary>
/// A scroll bar element to be used inside of a <see cref="UiSystem"/>.
/// A scroll bar is an element that features a smaller scroller indicator inside of it that can move up and down.
/// A scroll bar can be scrolled using the mouse or by using the scroll wheel while hovering over its <see cref="Element.Parent"/> or any of its siblings.
/// </summary>
2019-08-12 19:44:16 +02:00
public class ScrollBar : Element {
2021-10-30 15:01:04 +02:00
/// <summary>
/// Whether this scroll bar is horizontal
/// </summary>
public readonly bool Horizontal ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The background texture for this scroll bar
/// </summary>
2019-10-14 21:28:12 +02:00
public StyleProp < NinePatch > Background ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The texture of this scroll bar's scroller indicator
/// </summary>
2019-10-14 21:28:12 +02:00
public StyleProp < NinePatch > ScrollerTexture ;
2022-09-13 16:14:36 +02:00
/// <summary>
/// Whether smooth scrolling should be enabled for this scroll bar.
/// Smooth scrolling causes the <see cref="CurrentValue"/> to change gradually rather than instantly when scrolling.
/// </summary>
public StyleProp < bool > SmoothScrolling ;
/// <summary>
/// The factor with which <see cref="SmoothScrolling"/> happens.
/// </summary>
public StyleProp < float > SmoothScrollFactor ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The scroller's width and height
/// </summary>
2019-11-02 14:53:59 +01:00
public Vector2 ScrollerSize ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The max value that this scroll bar should be able to scroll to.
/// Note that the maximum value does not change the height of the scroll bar.
/// </summary>
2019-08-12 19:44:16 +02:00
public float MaxValue {
get = > this . maxValue ;
set {
this . maxValue = Math . Max ( 0 , value ) ;
// force current value to be clamped
2020-03-07 22:09:11 +01:00
this . CurrentValue = this . CurrentValue ;
2021-03-29 08:28:49 +02:00
// auto-hide if necessary
2022-06-15 11:38:11 +02:00
var shouldHide = this . maxValue < = Element . Epsilon ;
2021-03-29 08:28:49 +02:00
if ( this . AutoHideWhenEmpty & & this . IsHidden ! = shouldHide ) {
this . IsHidden = shouldHide ;
2019-12-14 14:00:12 +01:00
this . OnAutoHide ? . Invoke ( this ) ;
}
2019-08-12 19:44:16 +02:00
}
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// The current value of the scroll bar.
/// This is between 0 and <see cref="MaxValue"/> at all times.
/// </summary>
2019-08-12 19:44:16 +02:00
public float CurrentValue {
2020-03-07 22:09:11 +01:00
get = > this . currValue - this . scrollAdded ;
2019-08-12 19:44:16 +02:00
set {
var val = MathHelper . Clamp ( value , 0 , this . maxValue ) ;
if ( this . currValue ! = val ) {
2020-03-07 22:09:11 +01:00
if ( this . SmoothScrolling )
this . scrollAdded = val - this . currValue ;
2019-08-12 19:44:16 +02:00
this . currValue = val ;
this . OnValueChanged ? . Invoke ( this , val ) ;
}
}
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// The amount added or removed from <see cref="CurrentValue"/> per single movement of the scroll wheel
/// </summary>
2019-08-12 19:44:16 +02:00
public float StepPerScroll = 1 ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// An event that is called when <see cref="CurrentValue"/> changes
/// </summary>
2019-08-12 19:44:16 +02:00
public ValueChanged OnValueChanged ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// An event that is called when this scroll bar is automatically hidden from a <see cref="Panel"/>
/// </summary>
2019-12-14 14:00:12 +01:00
public GenericCallback OnAutoHide ;
2020-06-16 22:51:31 +02:00
/// <summary>
/// This property is true while the user scrolls on the scroll bar using the mouse or touch input
/// </summary>
2022-09-13 16:14:36 +02:00
public bool IsBeingScrolled = > this . isMouseScrolling | | this . isMouseDragging | | this . isTouchDragging | | this . isTouchScrolling ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// This field determines if this scroll bar should automatically be hidden from a <see cref="Panel"/> if there aren't enough children to allow for scrolling.
/// </summary>
2019-09-01 19:53:52 +02:00
public bool AutoHideWhenEmpty ;
2020-05-22 17:02:24 +02:00
/// <summary>
2021-11-14 21:32:13 +01:00
/// The position that this scroll bar's scroller is currently at.
/// This value takes the <see cref="Element.DisplayArea"/>, as well as the <see cref="Element.Scale"/> into account.
/// </summary>
public Vector2 ScrollerPosition = > this . DisplayArea . Location + new Vector2 (
! this . Horizontal ? 0 : this . CurrentValue / this . maxValue * ( this . DisplayArea . Width - this . ScrollerSize . X * this . Scale ) ,
this . Horizontal ? 0 : this . CurrentValue / this . maxValue * ( this . DisplayArea . Height - this . ScrollerSize . Y * this . Scale ) ) ;
/// <summary>
2022-09-13 16:14:36 +02:00
/// Whether this scroll bar should allow dragging the mouse over its attached <see cref="Panel"/>'s content while holding the left mouse button to scroll, similarly to how scrolling using touch input works.
2020-05-22 17:02:24 +02:00
/// </summary>
2022-09-13 16:14:36 +02:00
public bool MouseDragScrolling ;
2019-08-31 19:32:22 +02:00
2022-09-13 16:14:36 +02:00
private bool isMouseScrolling ;
private bool isMouseDragging ;
private bool isTouchScrolling ;
private bool isTouchDragging ;
2021-10-30 15:01:04 +02:00
private float maxValue ;
private float scrollAdded ;
private float currValue ;
2021-11-14 21:32:13 +01:00
private Vector2 scrollStartOffset ;
2021-10-30 15:01:04 +02:00
2019-08-31 19:32:22 +02:00
static ScrollBar ( ) {
InputHandler . EnableGestures ( GestureType . HorizontalDrag , GestureType . VerticalDrag ) ;
}
2019-08-12 19:44:16 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// Creates a new scroll bar with the given settings
/// </summary>
/// <param name="anchor">The scroll bar's anchor</param>
/// <param name="size">The scroll bar's size</param>
/// <param name="scrollerSize"></param>
/// <param name="maxValue"></param>
/// <param name="horizontal"></param>
2019-08-16 19:08:36 +02:00
public ScrollBar ( Anchor anchor , Vector2 size , int scrollerSize , float maxValue , bool horizontal = false ) : base ( anchor , size ) {
2019-08-12 19:44:16 +02:00
this . maxValue = maxValue ;
2019-08-16 19:08:36 +02:00
this . Horizontal = horizontal ;
2019-11-02 14:53:59 +01:00
this . ScrollerSize = new Vector2 ( horizontal ? scrollerSize : size . X , ! horizontal ? scrollerSize : size . Y ) ;
2019-08-28 18:27:17 +02:00
this . CanBeSelected = false ;
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 Update ( GameTime time ) {
base . Update ( time ) ;
2019-08-31 19:32:22 +02:00
2024-06-02 13:26:20 +02:00
if ( ! this . IsHidden ) {
// MOUSE INPUT
var moused = this . Controls . MousedElement ;
var wasMouseUp = this . Input . WasUp ( MouseButton . Left ) ;
var isMouseDown = this . Input . IsDown ( MouseButton . Left ) ;
if ( moused = = this & & wasMouseUp & & isMouseDown ) {
this . isMouseScrolling = true ;
this . scrollStartOffset = this . TransformInverseAll ( this . Input . ViewportMousePosition . ToVector2 ( ) ) - this . ScrollerPosition ;
} else if ( ! isMouseDown ) {
this . isMouseScrolling = false ;
}
if ( this . isMouseScrolling )
this . ScrollToPos ( this . TransformInverseAll ( this . Input . ViewportMousePosition . ToVector2 ( ) ) ) ;
if ( ! this . Horizontal ) {
if ( this . IsMousedForScrolling ( moused ) ) {
var scroll = this . Input . LastScrollWheel - this . Input . ScrollWheel ;
if ( scroll ! = 0 )
this . CurrentValue + = this . StepPerScroll * Math . Sign ( scroll ) ;
2022-09-13 16:14:36 +02:00
2024-06-02 13:26:20 +02:00
if ( this . MouseDragScrolling & & moused ! = this & & wasMouseUp & & isMouseDown )
this . isMouseDragging = true ;
}
if ( ! isMouseDown )
this . isMouseDragging = false ;
if ( this . isMouseDragging )
this . CurrentValue - = ( this . Input . MousePosition . Y - this . Input . LastMousePosition . Y ) / this . Scale ;
2022-09-13 16:14:36 +02:00
}
2019-08-16 19:08:36 +02:00
2024-06-02 13:26:20 +02:00
// TOUCH INPUT
if ( ! this . Horizontal ) {
// are we dragging on top of the panel?
if ( this . Input . GetViewportGesture ( GestureType . VerticalDrag , out var drag ) ) {
// if the element under the drag's start position is on top of the panel, start dragging
var touched = this . Parent . GetElementUnderPos ( this . TransformInverseAll ( drag . Position ) ) ;
if ( touched ! = null & & touched ! = this )
this . isTouchDragging = true ;
2019-08-31 19:32:22 +02:00
2024-06-02 13:26:20 +02:00
// if we're dragging at all, then move the scroller
if ( this . isTouchDragging )
this . CurrentValue - = drag . Delta . Y / this . Scale ;
} else {
this . isTouchDragging = false ;
}
2019-08-31 19:32:22 +02:00
}
2024-06-02 13:26:20 +02:00
if ( this . Input . ViewportTouchState . Count < = 0 ) {
// if no touch has occured this tick, then reset the variable
this . isTouchScrolling = false ;
} else {
foreach ( var loc in this . Input . ViewportTouchState ) {
var pos = this . TransformInverseAll ( loc . Position ) ;
// if we just started touching and are on top of the scroller, then we should start scrolling
if ( this . DisplayArea . Contains ( pos ) & & ! loc . TryGetPreviousLocation ( out _ ) ) {
this . isTouchScrolling = true ;
this . scrollStartOffset = pos - this . ScrollerPosition ;
break ;
}
// scroll no matter if we're on the scroller right now
if ( this . isTouchScrolling )
this . ScrollToPos ( pos ) ;
2019-08-31 19:32:22 +02:00
}
2019-08-16 19:08:36 +02:00
}
2024-06-02 13:26:20 +02:00
} else {
this . isMouseScrolling = false ;
this . isMouseDragging = false ;
this . isTouchScrolling = false ;
this . isTouchDragging = false ;
2019-08-13 16:02:29 +02:00
}
2020-03-07 22:09:11 +01:00
if ( this . SmoothScrolling & & this . scrollAdded ! = 0 ) {
this . scrollAdded * = this . SmoothScrollFactor ;
2022-06-15 11:38:11 +02:00
if ( Math . Abs ( this . scrollAdded ) < = Element . Epsilon )
2020-03-07 22:09:11 +01:00
this . scrollAdded = 0 ;
this . OnValueChanged ? . Invoke ( this , this . CurrentValue ) ;
}
2019-08-31 19:32:22 +02:00
}
2019-08-16 19:08:36 +02:00
2021-11-14 21:32:13 +01:00
private void ScrollToPos ( Vector2 position ) {
2022-06-24 14:01:26 +02:00
var size = this . ScrollerSize * this . Scale ;
2019-08-31 19:32:22 +02:00
if ( this . Horizontal ) {
2022-06-24 14:01:26 +02:00
var offset = this . scrollStartOffset . X > = 0 & & this . scrollStartOffset . X < = size . X ? this . scrollStartOffset . X : size . X / 2 ;
this . CurrentValue = ( position . X - this . Area . X - offset ) / ( this . Area . Width - size . X ) * this . MaxValue ;
2019-08-31 19:32:22 +02:00
} else {
2022-06-24 14:01:26 +02:00
var offset = this . scrollStartOffset . Y > = 0 & & this . scrollStartOffset . Y < = size . Y ? this . scrollStartOffset . Y : size . Y / 2 ;
this . CurrentValue = ( position . Y - this . Area . Y - offset ) / ( this . Area . Height - size . Y ) * this . MaxValue ;
2019-08-12 19:46:43 +02:00
}
2019-08-12 19:44:16 +02: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 ) {
2019-09-04 17:19:31 +02:00
batch . Draw ( this . Background , this . DisplayArea , Color . White * alpha , this . Scale ) ;
2019-12-26 12:35:47 +01:00
if ( this . MaxValue > 0 ) {
2021-11-14 21:32:13 +01:00
var scrollerRect = new RectangleF ( this . ScrollerPosition , this . ScrollerSize * this . Scale ) ;
2019-12-26 12:35:47 +01:00
batch . Draw ( this . ScrollerTexture , scrollerRect , Color . White * alpha , this . Scale ) ;
}
2022-04-25 15:25:58 +02:00
base . Draw ( time , batch , alpha , context ) ;
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
protected override void InitStyle ( UiStyle style ) {
base . InitStyle ( style ) ;
2021-12-21 11:54:32 +01:00
this . Background = this . Background . OrStyle ( style . ScrollBarBackground ) ;
this . ScrollerTexture = this . ScrollerTexture . OrStyle ( style . ScrollBarScrollerTexture ) ;
this . SmoothScrolling = this . SmoothScrolling . OrStyle ( style . ScrollBarSmoothScrolling ) ;
this . SmoothScrollFactor = this . SmoothScrollFactor . OrStyle ( style . ScrollBarSmoothScrollFactor ) ;
2019-08-12 19:44:16 +02:00
}
2023-08-14 17:37:26 +02:00
private bool IsMousedForScrolling ( Element moused ) {
if ( moused = = null | | ( moused ! = this . Parent & & ! moused . GetParentTree ( ) . Contains ( this . Parent ) ) )
return false ;
// if we're moused, check if there are any scroll bars deeper than us that should take precedence
var foundMe = false ;
foreach ( var child in this . Parent . GetChildren ( regardGrandchildren : true ) ) {
if ( foundMe ) {
2024-05-29 23:34:23 +02:00
if ( child is ScrollBar b & & ! b . IsHidden & & ! b . Horizontal & & b . IsMousedForScrolling ( moused ) )
2023-08-14 17:37:26 +02:00
return false ;
} else if ( child = = this ) {
// once we found ourselves, all subsequent children are deeper/older!
foundMe = true ;
}
}
return true ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// A delegate method used for <see cref="ScrollBar.OnValueChanged"/>
/// </summary>
/// <param name="element">The element whose current value changed</param>
/// <param name="value">The element's new <see cref="ScrollBar.CurrentValue"/></param>
2019-08-12 19:44:16 +02:00
public delegate void ValueChanged ( Element element , float value ) ;
}
2022-06-17 18:23:47 +02:00
}