// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using FlaxEditor.GUI.Dialogs; using FlaxEditor.GUI.Input; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.GUI.Timeline.GUI { /// /// The color gradient editing control for a timeline media event. Allows to edit the gradients stops to create the linear color animation over time. /// /// public class GradientEditor : ContainerControl { /// /// The gradient stop. /// public struct Stop { /// /// The gradient stop frame position (on time axis, relative to the event start). /// [EditorOrder(0), Tooltip("The gradient stop frame position (on time axis, relative to the event start).")] public int Frame; /// /// The color gradient value. /// [CustomEditor(typeof(CustomEditors.Editors.GenericEditor))] // Don't use default editor with color picker (focus change issue) [EditorOrder(1), Tooltip("The color gradient value.")] public Color Value; } /// /// The stop control type. /// /// public class StopControl : Control { private bool _isMoving; private Float2 _startMovePos; private IColorPickerDialog _currentDialog; /// /// The gradient editor reference. /// public GradientEditor Gradient; /// /// The gradient stop index. /// public int Index; /// public override void Draw() { base.Draw(); var isMouseOver = IsMouseOver; var color = Gradient._data[Index].Value; var icons = Editor.Instance.Icons; var icon = icons.VisjectBoxClosed32; Render2D.DrawSprite(icon, new Rectangle(0.0f, 0.0f, 10.0f, 10.0f), isMouseOver ? Color.Gray : Color.Black); Render2D.DrawSprite(icon, new Rectangle(1.0f, 1.0f, 8.0f, 8.0f), color); } /// public override bool OnMouseDown(Float2 location, MouseButton button) { if (button == MouseButton.Left) { Gradient.Select(this); _isMoving = true; _startMovePos = location; Gradient.OnEditingStart(); StartMouseCapture(); return true; } return base.OnMouseDown(location, button); } /// public override bool OnMouseUp(Float2 location, MouseButton button) { if (button == MouseButton.Left && _isMoving) { _isMoving = false; EndMouseCapture(); Gradient.OnEditingEnd(); } return base.OnMouseUp(location, button); } /// public override bool OnShowTooltip(out string text, out Float2 location, out Rectangle area) { // Don't show tooltip is user is moving the stop return base.OnShowTooltip(out text, out location, out area) && !_isMoving; } /// public override bool OnTestTooltipOverControl(ref Float2 location) { // Don't show tooltip is user is moving the stop return base.OnTestTooltipOverControl(ref location) && !_isMoving; } /// public override void OnMouseMove(Float2 location) { if (_isMoving && Float2.DistanceSquared(ref location, ref _startMovePos) > 25.0f) { _startMovePos = Float2.Minimum; var x = PointToParent(location).X; var frame = (int)((x - Width * 0.5f) / Gradient._scale); Gradient.SetStopFrame(Index, frame); } base.OnMouseMove(location); } /// public override bool OnMouseDoubleClick(Float2 location, MouseButton button) { if (base.OnMouseDoubleClick(location, button)) return true; if (button == MouseButton.Left) { // Show color picker dialog _currentDialog = ColorValueBox.ShowPickColorDialog?.Invoke(this, Gradient._data[Index].Value, OnColorChanged, null, false); return true; } return false; } private void OnColorChanged(Color color, bool sliding) { Gradient.OnEditingStart(); Gradient.SetStopColor(Index, color); Gradient.OnEditingEnd(); } /// public override void OnEndMouseCapture() { _isMoving = false; base.OnEndMouseCapture(); } /// public override void OnDestroy() { if (_currentDialog != null) { _currentDialog.ClosePicker(); _currentDialog = null; } base.OnDestroy(); } } private List _data = new List(); private List _stops = new List(); private StopControl _selected; private float _scale; private UpdateDelegate _update; /// /// Gets or sets the list of gradient stops. /// public List Stops { get => _data; set { if (value == null) throw new ArgumentNullException(); if (value.SequenceEqual(_data)) return; _data.Clear(); _data.AddRange(value); _data.Sort((a, b) => a.Frame > b.Frame ? 1 : 0); OnStopsChanged(); UpdateControls(); } } /// /// Occurs when stops collection gets changed (added/removed). /// public event Action StopsChanged; /// /// Occurs when stops collection gets modified (stop value or time modified). /// public event Action Edited; /// /// Occurs when gradient data editing starts (via UI). /// public event Action EditingStart; /// /// Occurs when gradient data editing ends (via UI). /// public event Action EditingEnd; /// /// Initializes a new instance of the class. /// public GradientEditor() { AutoFocus = false; SetUpdate(ref _update, OnUpdate); } private void OnUpdate(float deltaTime) { // Required to synchronize controls with Stops array edited via property edit context menu _data.Sort((a, b) => a.Frame > b.Frame ? 1 : 0); UpdateControls(); } /// /// Sets the scale factor (used to convert the gradient stops frame into control pixels). /// /// The scale. public void SetScale(float scale) { _scale = scale; UpdateControls(); } /// /// Called when timeline FPS gets changed. /// /// The before value. /// The after value. public void OnTimelineFpsChanged(float before, float after) { for (int i = 0; i < _data.Count; i++) { var stop = _data[i]; stop.Frame = (int)((stop.Frame / before) * after); _data[i] = stop; } UpdateControls(); } /// /// Called when gradient data editing starts (via UI). /// public void OnEditingStart() { EditingStart?.Invoke(); } /// /// Called when gradient data editing ends (via UI). /// public void OnEditingEnd() { EditingEnd?.Invoke(); } /// /// Called when stops collection gets changed (added/removed). /// protected virtual void OnStopsChanged() { StopsChanged?.Invoke(); } /// /// Called when stops collection gets modified (stop value or time modified). /// protected virtual void OnEdited() { Edited?.Invoke(); } private void Select(StopControl stop) { _selected = stop; UpdateControls(); } private void SetStopFrame(int index, int frame) { if (index != 0) { frame = Mathf.Max(frame, _data[index - 1].Frame); } if (index != _stops.Count - 1) { frame = Mathf.Min(frame, _data[index + 1].Frame); } var stop = _data[index]; if (stop.Frame == frame) return; stop.Frame = frame; _data[index] = stop; OnEdited(); } private void SetStopColor(int index, Color color) { var stop = _data[index]; if (stop.Value == color) return; stop.Value = color; _data[index] = stop; OnEdited(); } private void UpdateControls() { var count = _data.Count; // Remove unused stops while (_stops.Count > count) { var last = _stops.Count - 1; if (_selected == _stops[last]) _selected = null; _stops[last].Dispose(); _stops.RemoveAt(last); } // Add missing stops while (_stops.Count < count) { var stop = new StopControl { AutoFocus = false, Gradient = this, Size = new Float2(10.0f, 10.0f), Parent = this, }; _stops.Add(stop); } // Update stops var scale = _scale; var height = Height; for (var i = 0; i < count; i++) { var control = _stops[i]; var stop = _data[i]; control.Location = new Float2(stop.Frame * scale - control.Width * 0.5f, (height - control.Height) * 0.5f); control.Index = i; control.TooltipText = stop.Value + " at frame " + stop.Frame; } } /// protected override void AddUpdateCallbacks(RootControl root) { base.AddUpdateCallbacks(root); root.UpdateCallbacksToAdd.Add(_update); } /// protected override void RemoveUpdateCallbacks(RootControl root) { base.RemoveUpdateCallbacks(root); root.UpdateCallbacksToRemove.Add(_update); } /// public override void Draw() { // Push clipping mask GetDesireClientArea(out var clientArea); Render2D.PushClip(ref clientArea); var style = Style.Current; var bounds = new Rectangle(Float2.Zero, Size); var count = _data.Count; if (count == 0) { //Render2D.FillRectangle(bounds, Color.Black); } else if (count == 1) { Render2D.FillRectangle(bounds, _data[0].Value); } else { var prevStop = _data[0]; var scale = _scale; var width = Width; var height = Height; if (prevStop.Frame > 0.0f) { Render2D.FillRectangle(new Rectangle(Float2.Zero, prevStop.Frame * scale, height), prevStop.Value); } for (int i = 1; i < count; i++) { var curStop = _data[i]; Render2D.FillRectangle(new Rectangle(prevStop.Frame * scale, 0, (curStop.Frame - prevStop.Frame) * scale, height), prevStop.Value, curStop.Value, curStop.Value, prevStop.Value); prevStop = curStop; } if (prevStop.Frame * scale < width) { Render2D.FillRectangle(new Rectangle(prevStop.Frame * scale, 0, width - prevStop.Frame * scale, height), prevStop.Value); } } Render2D.DrawRectangle(bounds, IsMouseOver ? style.BackgroundHighlighted : style.Background); DrawChildren(); // Pop clipping mask Render2D.PopClip(); } /// public override bool OnMouseDoubleClick(Float2 location, MouseButton button) { if (base.OnMouseDoubleClick(location, button)) return true; if (button == MouseButton.Left) { // Add stop var frame = (int)(location.X / _scale); var stops = new List(Stops); var leftIdx = stops.FindLastIndex(x => x.Frame < frame); var rightIdx = stops.FindIndex(x => x.Frame > frame); var stop = new Stop { Frame = frame, Value = Color.White, }; if (leftIdx != -1 && rightIdx != -1) { var left = stops[leftIdx]; var right = stops[rightIdx]; float alpha = (float)(frame - left.Frame) / (right.Frame - left.Frame); stop.Value = Color.Lerp(left.Value, right.Value, alpha); stops.Insert(leftIdx + 1, stop); } else if (leftIdx != -1) { stop.Value = stops[leftIdx].Value; stops.Insert(leftIdx + 1, stop); } else if (rightIdx != -1) { stop.Value = stops[rightIdx].Value; stops.Insert(rightIdx, stop); } else { stops.Add(stop); } Stops = stops; return true; } return false; } /// public override void OnDestroy() { SetUpdate(ref _update, null); _stops.Clear(); _data.Clear(); _stops = null; _data = null; base.OnDestroy(); } } }