// Copyright (c) Wojciech Figat. All rights reserved. using System; using System.Globalization; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.GUI.Input { /// /// Float value editor with fixed size text box and slider. /// [HideInEditor] public class SliderControl : ContainerControl { /// /// The horizontal slider control. /// /// [HideInEditor] protected class Slider : Control { /// /// The default size. /// public const int DefaultSize = 16; /// /// The default thickness. /// public const int DefaultThickness = 6; /// /// The minimum value (constant) /// public const float Minimum = 0.0f; /// /// The maximum value (constant). /// public const float Maximum = 100.0f; private float _value; private Rectangle _thumbRect; private float _thumbCenter, _thumbSize; private bool _isSliding; /// /// Gets or sets the value (normalized to range 0-100). /// public float Value { get => _value; set { value = Mathf.Clamp(value, Minimum, Maximum); if (value != _value) { _value = value; // Update UpdateThumb(); ValueChanged?.Invoke(); } } } /// /// Occurs when value gets changed. /// public Action ValueChanged; /// /// The color of the slider track line. /// public Color TrackLineColor { get; set; } /// /// The color of the slider thumb when it's not selected. /// public Color ThumbColor { get; set; } /// /// The color of the slider thumb when it's selected. /// public Color ThumbColorSelected { get; set; } /// /// The color of the slider thumb when it's hovered. /// public Color ThumbColorHovered { get; set; } /// /// Gets a value indicating whether user is using a slider. /// public bool IsSliding => _isSliding; /// /// Occurs when sliding starts. /// public Action SlidingStart; /// /// Occurs when sliding ends. /// public Action SlidingEnd; /// /// Initializes a new instance of the class. /// /// The width. /// The height. public Slider(float width, float height) : base(0, 0, width, height) { var style = Style.Current; TrackLineColor = style.BackgroundHighlighted; ThumbColor = style.BackgroundNormal; ThumbColorSelected = style.BackgroundSelected; ThumbColorHovered = style.BackgroundHighlighted; } private void UpdateThumb() { // Cache data float trackSize = TrackSize; float range = Maximum - Minimum; _thumbSize = Mathf.Min(trackSize, Mathf.Max(trackSize / range * 10.0f, 30.0f)); float pixelRange = trackSize - _thumbSize; float perc = (_value - Minimum) / range; float thumbPosition = (int)(perc * pixelRange); _thumbCenter = thumbPosition + _thumbSize / 2; _thumbRect = new Rectangle(thumbPosition + 4, (Height - DefaultThickness) / 2, _thumbSize - 8, DefaultThickness); } private void EndSliding() { _isSliding = false; EndMouseCapture(); SlidingEnd?.Invoke(); Defocus(); Parent?.Focus(); } /// /// Gets the size of the track. /// private float TrackSize => Width; /// public override void Draw() { base.Draw(); // Draw track line var lineRect = new Rectangle(4, Height / 2, Width - 8, 1); Render2D.FillRectangle(lineRect, TrackLineColor); // Draw thumb bool mouseOverThumb = _thumbRect.Contains(PointFromWindow(Root.MousePosition)); Render2D.FillRectangle(_thumbRect, _isSliding ? ThumbColorSelected : mouseOverThumb ? ThumbColorHovered : ThumbColor); } /// public override void OnLostFocus() { if (_isSliding) { EndSliding(); } base.OnLostFocus(); } /// public override bool OnMouseDown(Float2 location, MouseButton button) { if (button == MouseButton.Left) { Focus(); float mousePosition = location.X; if (_thumbRect.Contains(ref location)) { // Start sliding _isSliding = true; StartMouseCapture(); SlidingStart?.Invoke(); return true; } else { // Click change Value += (mousePosition < _thumbCenter ? -1 : 1) * 10; Defocus(); Parent?.Focus(); } } return base.OnMouseDown(location, button); } /// public override void OnMouseMove(Float2 location) { if (_isSliding) { // Update sliding var slidePosition = location + Root.TrackingMouseOffset; Value = Mathf.Remap(slidePosition.X, 4, TrackSize - 4, Minimum, Maximum); if (Mathf.NearEqual(Value, Maximum)) Value = Maximum; else if (Mathf.NearEqual(Value, Minimum)) Value = Minimum; } else { base.OnMouseMove(location); } } /// public override bool OnMouseUp(Float2 location, MouseButton button) { if (button == MouseButton.Left && _isSliding) { EndSliding(); return true; } return base.OnMouseUp(location, button); } /// public override void OnEndMouseCapture() { // Check if was sliding if (_isSliding) { EndSliding(); } else { base.OnEndMouseCapture(); } } /// protected override void OnSizeChanged() { base.OnSizeChanged(); UpdateThumb(); } } /// /// The slider. /// protected Slider _slider; /// /// The text box. /// protected TextBox _textBox; /// /// The text box size (rest will be the slider area). /// protected const float TextBoxSize = 30.0f; private float _value; private float _min, _max; private bool _valueIsChanging; /// /// Occurs when value gets changed. /// public event Action ValueChanged; /// /// Gets or sets the value. /// public float Value { get => _value; set { value = Mathf.Clamp(value, _min, _max); if (Math.Abs(_value - value) > Mathf.Epsilon) { // Set value _value = value; // Update _valueIsChanging = true; UpdateText(); UpdateSlider(); _valueIsChanging = false; OnValueChanged(); } } } /// /// Gets or sets the minimum value. /// public float MinValue { get => _min; set { if (_min != value) { if (value > _max) throw new ArgumentException(); _min = value; Value = Value; } } } /// /// Gets or sets the maximum value. /// public float MaxValue { get => _max; set { if (_max != value) { if (value < _min) throw new ArgumentException(); _max = value; Value = Value; } } } /// /// Gets a value indicating whether user is using a slider. /// public bool IsSliding => _slider.IsSliding; /// /// Occurs when sliding starts. /// public event Action SlidingStart; /// /// Occurs when sliding ends. /// public event Action SlidingEnd; /// /// Initializes a new instance of the class. /// /// The value. /// The position x. /// The position y. /// The width. /// The minimum value. /// The maximum value. public SliderControl(float value, float x = 0, float y = 0, float width = 120, float min = float.MinValue, float max = float.MaxValue) : base(x, y, width, TextBox.DefaultHeight) { _min = min; _max = max; _value = Mathf.Clamp(value, min, max); float split = Width - TextBoxSize; _slider = new Slider(split, Height) { Parent = this, }; _slider.ValueChanged += SliderOnValueChanged; _slider.SlidingStart += SlidingStart; _slider.SlidingEnd += SliderOnSliderEnd; _textBox = new TextBox(false, split, 0) { Text = _value.ToString(CultureInfo.InvariantCulture), Parent = this, Location = new Float2(split, 0), Size = new Float2(Height, TextBoxSize), }; _textBox.EditEnd += OnTextBoxEditEnd; } private void SliderOnSliderEnd() { SlidingEnd?.Invoke(); Defocus(); Parent?.Focus(); } private void SliderOnValueChanged() { if (_valueIsChanging) return; Value = Mathf.Remap(_slider.Value, Slider.Minimum, Slider.Maximum, MinValue, MaxValue); } private void OnTextBoxEditEnd() { if (_valueIsChanging) return; var text = _textBox.Text.Replace(',', '.'); if (double.TryParse(text, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value)) { Value = (float)Math.Round(value, 5); } else { UpdateText(); } Defocus(); Parent?.Focus(); } /// /// Sets the limits from the attribute. /// /// The limits. public void SetLimits(RangeAttribute limits) { _min = limits.Min; _max = Mathf.Max(_min, limits.Max); Value = Value; } /// /// Sets the limits from the attribute. /// /// The limits. public void SetLimits(LimitAttribute limits) { _min = limits.Min; _max = Mathf.Max(_min, limits.Max); Value = Value; } /// /// Updates the text of the textbox. /// protected virtual void UpdateText() { _textBox.Text = _value.ToString(CultureInfo.InvariantCulture); } /// /// Updates the slider value. /// protected virtual void UpdateSlider() { _slider.Value = Mathf.Remap(_value, MinValue, MaxValue, Slider.Minimum, Slider.Maximum); } /// /// Called when value gets changed. /// protected virtual void OnValueChanged() { ValueChanged?.Invoke(); } /// protected override void PerformLayoutBeforeChildren() { base.PerformLayoutBeforeChildren(); float split = Width - TextBoxSize; _slider.Bounds = new Rectangle(0, 0, split, Height); _textBox.Bounds = new Rectangle(split, 0, TextBoxSize, Height); } /// public override void OnDestroy() { _slider = null; _textBox = null; base.OnDestroy(); } } }