// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; namespace FlaxEngine.GUI { /// /// Scroll Bars base class - allows to scroll contents of the GUI panel. /// /// [HideInEditor] public abstract class ScrollBar : Control { /// /// The default size. /// public const int DefaultSize = 14; /// /// The default minimum opacity. /// public const float DefaultMinimumOpacity = 0.75f; // Scrolling private float _clickChange = 20, _scrollChange = 50; private float _minimum, _maximum = 100; private float _startValue, _value, _targetValue; private readonly Orientation _orientation; private RootControl.UpdateDelegate _update; // Input private float _mouseOffset; // Thumb data private Rectangle _thumbRect, _trackRect; private bool _thumbClicked; private float _thumbCenter, _thumbSize; // Smoothing private float _thumbOpacity = DefaultMinimumOpacity; private float _scrollAnimationProgress = 0f; /// /// Gets the orientation. /// public Orientation Orientation => _orientation; /// /// Gets or sets the thumb box thickness. /// public float ThumbThickness { get; set; } = 8; /// /// Gets or sets the track line thickness. /// public float TrackThickness { get; set; } = 2.0f; /// /// The maximum time it takes to animate from current to target scroll position /// public float ScrollAnimationDuration { get; set; } = 0.18f; /// /// Gets a value indicating whether use scroll value smoothing. /// public bool UseSmoothing => EnableSmoothing && !Mathf.IsZero(ScrollAnimationDuration); /// /// Enables scroll smoothing /// public bool EnableSmoothing { get; set; } = true; /// /// Gets or sets the minimum value. /// public float Minimum { get => _minimum; set { if (value > _maximum) throw new ArgumentOutOfRangeException(); _minimum = value; if (Value < _minimum) Value = _minimum; } } /// /// Gets or sets the maximum value. /// public float Maximum { get => _maximum; set { if (value < _minimum) throw new ArgumentOutOfRangeException(); _maximum = value; if (Value > _maximum) Value = _maximum; } } /// /// Gets or sets the scroll value (current, smooth). /// public float Value { get => _value; set { value = Mathf.Clamp(value, _minimum, _maximum); if (!Mathf.NearEqual(value, _targetValue)) { _targetValue = value; _startValue = _value; _scrollAnimationProgress = 0f; // Check if skip smoothing if (!UseSmoothing) { _value = value; _startValue = value; _scrollAnimationProgress = 1f; OnValueChanged(); } else { SetUpdate(ref _update, OnUpdate); } } } } /// /// Gets or sets the target value (target, not smooth). /// public float TargetValue { get => _targetValue; set { value = Mathf.Clamp(value, _minimum, _maximum); if (!Mathf.NearEqual(value, _targetValue)) { _targetValue = value; _value = value; SetUpdate(ref _update, null); OnValueChanged(); } } } /// /// Gets or sets the speed for the scroll on mouse wheel. /// public float ScrollSpeedWheel { get => _scrollChange; set => _scrollChange = value; } /// /// Gets or sets the speed for the scroll on mouse click. /// public float ScrollSpeedClick { get => _clickChange; set => _clickChange = value; } /// /// Gets the value slow down. /// public float ValueSlowDown => _targetValue - _value; /// /// Gets a value indicating whether thumb is being clicked (scroll bar is in use). /// public bool IsThumbClicked => _thumbClicked; /// /// Occurs when value gets changed. /// public event Action ValueChanged; /// /// Enables/disabled scrolling by user. /// [NoSerialize, HideInEditor] public bool ThumbEnabled = true; /// /// Gets the size of the track. /// protected abstract float TrackSize { get; } /// /// Initializes a new instance of the class. /// /// The orientation. protected ScrollBar(Orientation orientation) { AutoFocus = false; _orientation = orientation; } /// /// Cuts the scroll bar value smoothing and imminently goes to the target scroll value. /// public void FastScroll() { if (!Mathf.NearEqual(_value, _targetValue)) { _value = _targetValue = _startValue; _scrollAnimationProgress = 0f; SetUpdate(ref _update, null); OnValueChanged(); } } /// /// Scrolls the view to the desire range (favors minimum value if cannot cover whole range in a bounds). /// /// The view minimum. /// The view maximum. /// True of scroll to the item quickly without smoothing. public void ScrollViewTo(float min, float max, bool fastScroll = false) { // Check if we need to change view float viewMin = _value; float viewSize = TrackSize; float viewMax = viewMin + viewSize; if (Mathf.IsNotInRange(min, viewMin, viewMax)) { if (fastScroll) TargetValue = min; else Value = min; } /*else if (Mathf.IsNotInRange(max, viewMin, viewMax)) { Value = max - viewSize; }*/ } private void UpdateThumb() { // Cache data var width = Width; var height = Height; float trackSize = TrackSize; float range = _maximum - _minimum; _thumbSize = Mathf.Min(trackSize - 10, Mathf.Max(trackSize / range * 100.0f, 50.0f)); float pixelRange = trackSize - _thumbSize; float percentage = (_value - _minimum) / range; float thumbPosition = (int)(percentage * pixelRange); _thumbCenter = thumbPosition + _thumbSize / 2; _thumbRect = _orientation == Orientation.Vertical ? new Rectangle((width - ThumbThickness) / 2, thumbPosition + 4, ThumbThickness, _thumbSize - 8) : new Rectangle(thumbPosition + 4, (height - ThumbThickness) / 2, _thumbSize - 8, ThumbThickness); _trackRect = _orientation == Orientation.Vertical ? new Rectangle((width - TrackThickness) / 2, 4, TrackThickness, height - 8) : new Rectangle(4, (height - TrackThickness) / 2, width - 8, TrackThickness); } private void EndTracking() { // Check flag if (_thumbClicked) { // Clear flag _thumbClicked = false; // End capturing mouse EndMouseCapture(); } } internal void Reset() { _value = _targetValue = _startValue = 0; _scrollAnimationProgress = 0f; } /// /// Called when value gets changed. /// protected virtual void OnValueChanged() { UpdateThumb(); ValueChanged?.Invoke(); } private void OnUpdate(float deltaTime) { bool isDeltaSlow = deltaTime > (1 / 20.0f); // Opacity smoothing float targetOpacity = IsMouseOver ? 1.0f : DefaultMinimumOpacity; _thumbOpacity = isDeltaSlow ? targetOpacity : Mathf.Lerp(_thumbOpacity, targetOpacity, deltaTime * 10.0f); bool needUpdate = Mathf.Abs(_thumbOpacity - targetOpacity) > 0.001f; // Ensure scroll bar is visible and smoothing is required if (Visible && Mathf.Abs(_targetValue - _value) > 0.01f) { // Interpolate or not if running slow float value; if (!isDeltaSlow && UseSmoothing) { // percentage of scroll from 0 to _scrollChange, ex. 0.5 at _scrollChange / 2 var minScrollChangeRatio = Mathf.Clamp(Mathf.Abs(_targetValue - _startValue) / _scrollChange, 0, 1); // shorten the duration if we scrolled less than _scrollChange var actualDuration = ScrollAnimationDuration * minScrollChangeRatio; var step = deltaTime / actualDuration; var progress = _scrollAnimationProgress; progress = Mathf.Clamp(progress + step, 0, 1); // https://easings.net/#easeOutSine var easedProgress = Mathf.Sin((progress * Mathf.Pi) / 2); value = Mathf.Lerp(_startValue, _targetValue, easedProgress); _scrollAnimationProgress = progress; } else { value = _targetValue; _startValue = _targetValue; _scrollAnimationProgress = 0f; } _value = value; OnValueChanged(); needUpdate = true; } // End updating if all animations are done if (!needUpdate) { SetUpdate(ref _update, null); } } /// /// Sets the scroll range (min and max at once). /// /// The minimum scroll range value (see ). /// The maximum scroll range value (see ). public void SetScrollRange(float minimum, float maximum) { if (minimum > maximum) throw new ArgumentOutOfRangeException(); _minimum = minimum; _maximum = maximum; if (Value < minimum) Value = minimum; else if (Value > maximum) Value = maximum; UpdateThumb(); } /// public override void Draw() { base.Draw(); var style = Style.Current; Render2D.FillRectangle(_trackRect, style.BackgroundHighlighted * _thumbOpacity); Render2D.FillRectangle(_thumbRect, (_thumbClicked ? style.BackgroundSelected : style.BackgroundNormal) * _thumbOpacity); } /// public override void OnLostFocus() { EndTracking(); base.OnLostFocus(); } /// public override void OnMouseMove(Float2 location) { if (_thumbClicked) { var slidePosition = location + Root.TrackingMouseOffset; if (Parent is ScrollableControl panel) slidePosition += panel.ViewOffset; // Hardcoded fix float mousePosition = _orientation == Orientation.Vertical ? slidePosition.Y : slidePosition.X; float percentage = (mousePosition - _mouseOffset - _thumbSize / 2) / (TrackSize - _thumbSize); TargetValue = _minimum + percentage * (_maximum - _minimum); } } /// public override bool OnMouseWheel(Float2 location, float delta) { if (ThumbEnabled) { // Scroll Value = _targetValue - delta * _scrollChange; } return true; } /// public override bool OnMouseDown(Float2 location, MouseButton button) { if (button == MouseButton.Left && ThumbEnabled) { // Remove focus var root = Root; root.FocusedControl?.Defocus(); float mousePosition = _orientation == Orientation.Vertical ? location.Y : location.X; if (_thumbRect.Contains(ref location)) { // Start moving thumb _thumbClicked = true; _mouseOffset = mousePosition - _thumbCenter; // Start capturing mouse StartMouseCapture(); } else { // Click change Value = _value + (mousePosition < _thumbCenter ? -1 : 1) * _clickChange; } } return base.OnMouseDown(location, button); } /// public override bool OnMouseUp(Float2 location, MouseButton button) { EndTracking(); return base.OnMouseUp(location, button); } /// public override void OnEndMouseCapture() { EndTracking(); } /// protected override void OnSizeChanged() { base.OnSizeChanged(); UpdateThumb(); } /// public override void OnMouseEnter(Float2 location) { base.OnMouseEnter(location); SetUpdate(ref _update, OnUpdate); } /// public override void OnMouseLeave() { base.OnMouseLeave(); SetUpdate(ref _update, OnUpdate); } /// protected override void AddUpdateCallbacks(RootControl root) { base.AddUpdateCallbacks(root); if (_update != null) root.UpdateCallbacksToAdd.Add(_update); } /// protected override void RemoveUpdateCallbacks(RootControl root) { base.RemoveUpdateCallbacks(root); if (_update != null) root.UpdateCallbacksToRemove.Add(_update); } } }