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 ;
2019-08-12 19:44:16 +02:00
using MLEM.Extensions ;
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 ;
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 {
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 ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The <see cref="ScrollerTexture"/>'s offset from the calculated position of the scroller. Use this to pad the scroller.
/// </summary>
2019-11-02 14:53:59 +01:00
public Vector2 ScrollerOffset ;
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 ;
2019-08-12 19:44:16 +02:00
private float maxValue ;
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
var shouldHide = this . maxValue < = 0.01F ;
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-03-07 22:09:11 +01:00
private float scrollAdded ;
2019-08-12 19:44:16 +02:00
private float currValue ;
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>
/// Whether this scroll bar is horizontal
/// </summary>
2019-08-16 19:08:36 +02:00
public readonly bool Horizontal ;
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>
public bool IsBeingScrolled = > this . isMouseHeld | | this . isDragging | | this . isTouchHeld ;
2019-08-13 16:02:29 +02:00
private bool isMouseHeld ;
2019-08-31 19:32:22 +02:00
private bool isDragging ;
private bool isTouchHeld ;
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>
/// 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>
2020-03-07 22:09:11 +01:00
public bool SmoothScrolling ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The factor with which <see cref="SmoothScrolling"/> happens.
/// </summary>
2020-03-07 22:09:11 +01:00
public float SmoothScrollFactor = 0.75F ;
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
// MOUSE INPUT
2019-08-28 18:27:17 +02:00
var moused = this . Controls . MousedElement ;
2019-09-02 21:11:05 +02:00
if ( moused = = this & & this . Controls . Input . IsMouseButtonPressed ( MouseButton . Left ) ) {
2019-08-13 16:02:29 +02:00
this . isMouseHeld = true ;
2019-08-28 18:27:17 +02:00
} else if ( this . isMouseHeld & & ! this . Controls . Input . IsMouseButtonDown ( MouseButton . Left ) ) {
2019-08-13 16:02:29 +02:00
this . isMouseHeld = false ;
}
2019-08-31 19:32:22 +02:00
if ( this . isMouseHeld )
2019-09-02 19:55:26 +02:00
this . ScrollToPos ( this . Input . MousePosition . Transform ( this . Root . InvTransform ) ) ;
2019-08-31 19:32:22 +02:00
if ( ! this . Horizontal & & moused ! = null & & ( moused = = this . Parent | | moused . GetParentTree ( ) . Contains ( this . Parent ) ) ) {
2020-02-06 01:51:41 +01:00
var scroll = this . Input . LastScrollWheel - this . Input . ScrollWheel ;
2019-08-31 19:32:22 +02:00
if ( scroll ! = 0 )
this . CurrentValue + = this . StepPerScroll * Math . Sign ( scroll ) ;
}
2019-08-16 19:08:36 +02:00
2019-08-31 19:32:22 +02:00
// TOUCH INPUT
if ( ! this . Horizontal ) {
// are we dragging on top of the panel?
if ( this . Input . GetGesture ( GestureType . VerticalDrag , out var drag ) ) {
// if the element under the drag's start position is on top of the panel, start dragging
2019-11-02 14:53:59 +01:00
var touched = this . Parent . GetElementUnderPos ( Vector2 . Transform ( drag . Position , this . Root . InvTransform ) ) ;
2019-08-31 19:32:22 +02:00
if ( touched ! = null & & touched ! = this )
this . isDragging = true ;
// if we're dragging at all, then move the scroller
if ( this . isDragging )
this . CurrentValue - = drag . Delta . Y / this . Scale ;
2019-08-16 19:08:36 +02:00
} else {
2019-08-31 19:32:22 +02:00
this . isDragging = false ;
}
}
if ( this . Input . TouchState . Count < = 0 ) {
// if no touch has occured this tick, then reset the variable
this . isTouchHeld = false ;
} else {
foreach ( var loc in this . Input . TouchState ) {
2019-09-02 19:55:26 +02:00
var pos = Vector2 . Transform ( loc . Position , this . Root . InvTransform ) ;
2019-08-31 19:32:22 +02:00
// if we just started touching and are on top of the scroller, then we should start scrolling
2019-09-02 19:55:26 +02:00
if ( this . DisplayArea . Contains ( pos ) & & ! loc . TryGetPreviousLocation ( out _ ) ) {
2019-08-31 19:32:22 +02:00
this . isTouchHeld = true ;
break ;
}
// scroll no matter if we're on the scroller right now
if ( this . isTouchHeld )
2019-09-02 19:55:26 +02:00
this . ScrollToPos ( pos . ToPoint ( ) ) ;
2019-08-16 19:08:36 +02:00
}
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 ;
if ( Math . Abs ( this . scrollAdded ) < = 0.1F )
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
2019-08-31 19:32:22 +02:00
private void ScrollToPos ( Point position ) {
if ( this . Horizontal ) {
var internalX = position . X - this . Area . X - this . ScrollerSize . X * this . Scale / 2 ;
this . CurrentValue = internalX / ( this . Area . Width - this . ScrollerSize . X * this . Scale ) * this . MaxValue ;
} else {
var internalY = position . Y - this . Area . Y - this . ScrollerSize . Y * this . Scale / 2 ;
this . CurrentValue = internalY / ( this . Area . Height - this . ScrollerSize . Y * this . Scale ) * 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 />
2019-09-20 13:22:05 +02:00
public override void Draw ( GameTime time , SpriteBatch batch , float alpha , BlendState blendState , SamplerState samplerState , Matrix matrix ) {
2019-09-04 17:19:31 +02:00
batch . Draw ( this . Background , this . DisplayArea , Color . White * alpha , this . Scale ) ;
2019-08-12 19:44:16 +02:00
2019-12-26 12:35:47 +01:00
if ( this . MaxValue > 0 ) {
var scrollerPos = new Vector2 ( this . DisplayArea . X + this . ScrollerOffset . X , this . DisplayArea . Y + this . ScrollerOffset . Y ) ;
var scrollerOffset = new Vector2 (
2020-03-07 22:09:11 +01:00
! 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 ) ) ;
2019-12-26 12:35:47 +01:00
var scrollerRect = new RectangleF ( scrollerPos + scrollerOffset , this . ScrollerSize * this . Scale ) ;
batch . Draw ( this . ScrollerTexture , scrollerRect , Color . White * alpha , this . Scale ) ;
}
2019-09-20 13:22:05 +02:00
base . Draw ( time , batch , alpha , blendState , samplerState , matrix ) ;
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 ) ;
2019-10-14 21:28:12 +02:00
this . Background . SetFromStyle ( style . ScrollBarBackground ) ;
this . ScrollerTexture . SetFromStyle ( style . ScrollBarScrollerTexture ) ;
2019-08-12 19:44:16 +02:00
}
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 ) ;
}
}