Browse Source

Feature: Added ColorGradient picker window

BearishSun 7 years ago
parent
commit
f393323a58

+ 1 - 1
CMakeLists.txt

@@ -28,7 +28,7 @@ add_subdirectory(${BSF_SOURCE_DIR})
 
 
 set (BS_PREBUILT_DEPENDENCIES_VERSION 24)
 set (BS_PREBUILT_DEPENDENCIES_VERSION 24)
 set (BS_SRC_DEPENDENCIES_VERSION 15)
 set (BS_SRC_DEPENDENCIES_VERSION 15)
-set (BS_BUILTIN_ASSETS_VERSION 8)
+set (BS_BUILTIN_ASSETS_VERSION 9)
 
 
 # Options
 # Options
 set(GENERATE_SCRIPT_BINDINGS OFF CACHE BOOL "If true, script binding files will be generated. Script bindings are required for the project to build properly, however they take a while to generate. If you are sure the script bindings are up to date, you can turn off their generation (temporarily) to speed up the build.")
 set(GENERATE_SCRIPT_BINDINGS OFF CACHE BOOL "If true, script binding files will be generated. Script bindings are required for the project to build properly, however they take a while to generate. If you are sure the script bindings are up to date, you can turn off their generation (temporarily) to speed up the build.")

+ 5 - 0
Data/Raw/DataList.json

@@ -1546,6 +1546,11 @@
             "Path": "ProgressBarSmallFill.png",
             "Path": "ProgressBarSmallFill.png",
             "SpriteUUID": "808b9443-46a8-1a63-b194-46a8ee5e8f25",
             "SpriteUUID": "808b9443-46a8-1a63-b194-46a8ee5e8f25",
             "TextureUUID": "067a1430-4bfd-8c01-2284-4bfd42b28159"
             "TextureUUID": "067a1430-4bfd-8c01-2284-4bfd42b28159"
+        },
+        {
+            "Path": "ContainerBg.png",
+            "SpriteUUID": "e15e000f-416e-b09e-2194-416eee62bfa9",
+            "TextureUUID": "64b57435-4bd1-f789-93b1-4bd18dfbb67e"
         }
         }
     ]
     ]
 }
 }

+ 20 - 0
Data/Raw/GUISkin.json

@@ -3067,5 +3067,25 @@
     },
     },
     {
     {
         "name": "Canvas"
         "name": "Canvas"
+    },
+	{
+        "border": {
+            "bottom": 4,
+            "left": 2,
+            "right": 2,
+            "top": 2
+        },
+        "contentOffset": {
+            "bottom": 4,
+            "left": 2,
+            "right": 2,
+            "top": 2
+        },
+        "fixedHeight": false,
+        "fixedWidth": false,
+        "name": "ContainerBg",
+        "normal": {
+            "texture": "ContainerBg.png"
+        }
     }
     }
 ]
 ]

+ 1 - 0
Source/Scripting/MBansheeEditor/GUI/EditorStyles.cs

@@ -45,6 +45,7 @@ namespace BansheeEditor
         public const string InspectorTitleBg = "InspectorTitleBg";
         public const string InspectorTitleBg = "InspectorTitleBg";
         public const string InspectorContentBg = "InspectorContentBg";
         public const string InspectorContentBg = "InspectorContentBg";
         public const string InspectorContentBgAlternate = "InspectorContentBgAlternate";
         public const string InspectorContentBgAlternate = "InspectorContentBgAlternate";
+        public const string ContainerBg = "ContainerBg";
     }
     }
 
 
     /** @} */
     /** @} */

+ 1 - 1
Source/Scripting/MBansheeEditor/MBansheeEditor.csproj

@@ -90,7 +90,7 @@
     <Compile Include="Inspectors\SliderJointInspector.cs" />
     <Compile Include="Inspectors\SliderJointInspector.cs" />
     <Compile Include="Inspectors\SphereColliderInspector.cs" />
     <Compile Include="Inspectors\SphereColliderInspector.cs" />
     <Compile Include="Inspectors\SphericalJointInspector.cs" />
     <Compile Include="Inspectors\SphericalJointInspector.cs" />
-    <Compile Include="Windows\DbgWindow.cs" />
+    <Compile Include="Windows\GradientPicker.cs" />
     <Compile Include="Windows\Inspector\Style\InspectableFieldRangeStyle.cs" />
     <Compile Include="Windows\Inspector\Style\InspectableFieldRangeStyle.cs" />
     <Compile Include="Windows\Inspector\Style\InspectableFieldStepStyle.cs" />
     <Compile Include="Windows\Inspector\Style\InspectableFieldStepStyle.cs" />
     <Compile Include="Windows\Inspector\Style\InspectableFieldStyle.cs" />
     <Compile Include="Windows\Inspector\Style\InspectableFieldStyle.cs" />

+ 592 - 0
Source/Scripting/MBansheeEditor/Windows/GradientPicker.cs

@@ -0,0 +1,592 @@
+//********************************** Banshee Engine (www.banshee4d.com) **************************************************//
+//**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
+using System;
+using System.Collections.Generic;
+using BansheeEngine;
+
+namespace BansheeEditor
+{
+    /** @addtogroup Windows
+     *  @{
+     */
+
+    /// <summary>
+    /// A color gradient editor window that allows the user to add or modify colors from a color gradient.
+    /// </summary>
+    public class GradientPicker : ModalWindow
+    {
+        private int EDITOR_HORZ_PADDING = 10;
+
+        private ColorGradient gradient;
+        private GradientKeyEditor editor;
+        private GUIColorGradient guiGradient;
+        private GUICanvas overlayCanvas;
+
+        private GUIPanel editorPanel;
+        private GUIButton guiOK;
+        private GUIButton guiCancel;
+
+        private Action<bool, ColorGradient> closedCallback;
+
+        /// <summary>
+        /// Shows the gradient picker window.
+        /// </summary>
+        /// <param name="closedCallback">Optional callback to trigger when the user finishes editing the gradient or
+        ///                              cancels out of the dialog.</param>
+        /// <returns>An instance of the gradient picker window.</returns>
+        public static GradientPicker Show(Action<bool, ColorGradient> closedCallback = null)
+        {
+            GradientPicker picker = new GradientPicker(new ColorGradient(), closedCallback);
+            return picker;
+        }
+
+        /// <summary>
+        /// Shows the gradient picker window and sets the initial gradient to show.
+        /// </summary>
+        /// <param name="gradient">Gradient to initially display in the window.</param>
+        /// <param name="closedCallback">Optional callback to trigger when the user finishes editing the gradient or
+        ///                              cancels out of the dialog.</param>
+        /// <returns>A. instance of the gradient picker window.</returns>
+        public static GradientPicker Show(ColorGradient gradient, Action<bool, ColorGradient> closedCallback = null)
+        {
+            GradientPicker picker = new GradientPicker(gradient, closedCallback);
+            return picker;
+        }
+
+        /// <summary>
+        /// Constructs a new gradient picker window.
+        /// </summary>
+        /// <param name="gradient">Gradient to initially display in the window.</param>
+        /// <param name="closedCallback">Optional callback to trigger when the user finishes editing the gradient or
+        ///                              cancels out of the dialog.</param>
+        protected GradientPicker(ColorGradient gradient, Action<bool, ColorGradient> closedCallback = null)
+            : base(false)
+        {
+            Title = new LocEdString("Gradient Picker");
+            Width = 270;
+            Height = 135;
+
+            this.gradient = gradient;
+            this.closedCallback = closedCallback;
+        }
+
+        private void OnInitialize()
+        {
+            guiOK = new GUIButton(new LocEdString("OK"));
+            guiCancel = new GUIButton(new LocEdString("Cancel"));
+
+            guiOK.OnClick += OnOK;
+            guiCancel.OnClick += OnCancel;
+
+            GUILayout mainVertLayout = GUI.AddLayoutY();
+            mainVertLayout.AddSpace(10);
+
+            GUILayout editorHorzLayout = mainVertLayout.AddLayoutX();
+            editorHorzLayout.AddSpace(EDITOR_HORZ_PADDING);
+            GUIPanel gradientEditorPanel = editorHorzLayout.AddPanel();
+            editorHorzLayout.AddSpace(EDITOR_HORZ_PADDING);
+
+            mainVertLayout.AddSpace(15);
+
+            GUILayout buttonHorzLayout = mainVertLayout.AddLayoutX();
+            buttonHorzLayout.AddFlexibleSpace();
+            buttonHorzLayout.AddElement(guiOK);
+            buttonHorzLayout.AddSpace(10);
+            buttonHorzLayout.AddElement(guiCancel);
+            buttonHorzLayout.AddFlexibleSpace();
+
+            mainVertLayout.AddFlexibleSpace();
+
+            editorPanel = gradientEditorPanel.AddPanel(0);
+            GUIPanel editorOverlay = gradientEditorPanel.AddPanel(-1);
+
+            overlayCanvas = new GUICanvas();
+            editorOverlay.AddElement(overlayCanvas);
+
+            GUILayout editorVertLayout = editorPanel.AddLayoutY();
+
+            GUILayout guiGradientLayout = editorVertLayout.AddLayoutX();
+            guiGradientLayout.AddSpace(GradientKeyEditor.RECT_WIDTH / 2);
+
+            guiGradient = new GUIColorGradient();
+            guiGradient.Gradient = gradient;
+            guiGradient.SetHeight(30);
+
+            guiGradientLayout.AddElement(guiGradient);
+            guiGradientLayout.AddSpace(GradientKeyEditor.RECT_WIDTH / 2);
+
+            editorVertLayout.AddSpace(10);
+
+            editor = new GradientKeyEditor(editorVertLayout, gradient.GetKeys(), Width - EDITOR_HORZ_PADDING * 2, 20);
+            editor.OnGradientModified += colorGradient =>
+            {
+                gradient = colorGradient;
+
+                guiGradient.Gradient = gradient;
+                UpdateKeyLines();
+            };
+
+            editorVertLayout.AddFlexibleSpace();
+
+            GUITexture containerBg = new GUITexture(null, EditorStylesInternal.ContainerBg);
+            Rect2I containerBounds = editor.GetBounds(GUI);
+            containerBounds.x -= 2;
+            containerBounds.y -= 2;
+            containerBounds.width += 4;
+            containerBounds.height += 6;
+            containerBg.Bounds = containerBounds;
+
+            GUIPanel editorUnderlay = GUI.AddPanel(1);
+            editorUnderlay.AddElement(containerBg);
+
+            UpdateKeyLines();
+
+            EditorInput.OnPointerPressed += OnPointerPressed;
+            EditorInput.OnPointerDoubleClick += OnPointerDoubleClicked;
+            EditorInput.OnPointerMoved += OnPointerMoved;
+            EditorInput.OnPointerReleased += OnPointerReleased;
+            EditorInput.OnButtonUp += OnButtonUp;
+        }
+
+        private void OnDestroy()
+        {
+            EditorInput.OnPointerPressed -= OnPointerPressed;
+            EditorInput.OnPointerDoubleClick -= OnPointerDoubleClicked;
+            EditorInput.OnPointerMoved -= OnPointerMoved;
+            EditorInput.OnPointerReleased -= OnPointerReleased;
+            EditorInput.OnButtonUp -= OnButtonUp;
+        }
+
+        /// <summary>
+        /// Draws lines connecting the individual key color over the gradient itself.
+        /// </summary>
+        private void UpdateKeyLines()
+        {
+            overlayCanvas.Clear();
+
+            ColorGradientKey[] keys = gradient.GetKeys();
+            for (int i = 0; i < keys.Length; i++)
+            {
+                int pixel = editor.TimeToPixel(keys[i].time);
+
+                overlayCanvas.DrawLine(new Vector2I(pixel, 0), new Vector2I(pixel, 40), Color.DarkGray);
+            }
+        }
+
+        /// <summary>
+        /// Triggered when the user selects a gradient and closes the dialog.
+        /// </summary>
+        void OnOK()
+        {
+            closedCallback?.Invoke(true, gradient);
+
+            Close();
+        }
+
+        /// <summary>
+        /// Triggered when the user cancels gradient editing and closes the dialog.
+        /// </summary>
+        void OnCancel()
+        {
+            closedCallback?.Invoke(false, gradient);
+
+            Close();
+        }
+
+        #region Input callbacks
+        /// <summary>
+        /// Triggered when the user presses a mouse button.
+        /// </summary>
+        /// <param name="ev">Information about the mouse press event.</param>
+        private void OnPointerPressed(PointerEvent ev)
+        {
+            if (ev.IsUsed)
+                return;
+
+            editor.OnPointerPressed(ScreenToKeyEditorPos(ev.ScreenPos), ev.Button);
+        }
+
+        /// <summary>
+        /// Triggered when the user double clicks the left mouse button.
+        /// </summary>
+        /// <param name="ev">Information about the mouse event.</param>
+        private void OnPointerDoubleClicked(PointerEvent ev)
+        {
+            if (ev.IsUsed)
+                return;
+
+            Vector2I panelPos = ScreenToKeyEditorPos(ev.ScreenPos);
+            Rect2I guiGradientBounds = guiGradient.Bounds;
+
+            if (guiGradientBounds.Contains(panelPos))
+            {
+                Vector2I canvasPos = panelPos - new Vector2I(guiGradientBounds.x, guiGradientBounds.y);
+
+                float time = canvasPos.x / (float) Math.Max(guiGradientBounds.width - 1, 1);
+                if (time >= 0.0f && time <= 1.0f)
+                {
+                    List<ColorGradientKey> keys = new List<ColorGradientKey>(gradient.GetKeys());
+
+                    keys.Add(new ColorGradientKey(Color.Black, time));
+                    keys.Sort((lhs, rhs) => lhs.time.CompareTo(rhs.time));
+
+                    gradient = new ColorGradient(keys.ToArray());
+
+                    guiGradient.Gradient = gradient;
+                    editor.Rebuild(keys);
+                    UpdateKeyLines();
+                }
+            }
+            else
+                editor.OnPointerDoubleClicked(panelPos, ev.Button);
+        }
+
+        /// <summary>
+        /// Triggered when the user moves the mouse.
+        /// </summary>
+        /// <param name="ev">Information about the mouse move event.</param>
+        private void OnPointerMoved(PointerEvent ev)
+        {
+            if (ev.IsUsed)
+                return;
+
+            editor.OnPointerMoved(ScreenToKeyEditorPos(ev.ScreenPos), ev.Button);
+        }
+
+        /// <summary>
+        /// Triggered when the user releases a mouse button.
+        /// </summary>
+        /// <param name="ev">Information about the mouse release event.</param>
+        private void OnPointerReleased(PointerEvent ev)
+        {
+            if (ev.IsUsed)
+                return;
+
+            editor.OnPointerReleased(ScreenToKeyEditorPos(ev.ScreenPos));
+        }
+
+        /// <summary>
+        /// Triggered when the user releases a keyboard button.
+        /// </summary>
+        /// <param name="ev">Information about the keyboard release event.</param>
+        private void OnButtonUp(ButtonEvent ev)
+        {
+            editor.OnButtonUp(ev);
+        }
+
+        /// <summary>
+        /// Converts screen coordinates in coordinates relative to the panel containing the key editor control.
+        /// </summary>
+        /// <param name="screenPos">Coordinates in screen space.</param>
+        /// <returns>Coordinates relative to the key editor control.</returns>
+        private Vector2I ScreenToKeyEditorPos(Vector2I screenPos)
+        {
+            Vector2I windowPos = ScreenToWindowPos(screenPos);
+
+            Rect2I elementBounds = GUIUtility.CalculateBounds(editorPanel, GUI);
+            return windowPos - new Vector2I(elementBounds.x, elementBounds.y);
+        }
+        #endregion
+
+        /// <summary>
+        /// Handles drawing and editing of individual keys present on the color gradient.
+        /// </summary>
+        public class GradientKeyEditor
+        {
+            public const int RECT_WIDTH = 14;
+            private static readonly Color SELECTED_COLOR = Color.BansheeOrange;
+            private static readonly Color PLAIN_COLOR = Color.DarkGray;
+            private const int DRAG_START_DISTANCE = 3;
+
+            private GUICanvas canvas;
+            private List<ColorGradientKey> keys = new List<ColorGradientKey>();
+            private int width;
+            private int height;
+            private int selectedIdx = -1;
+
+            private bool isMousePressedOverKey;
+            private bool isDragInProgress;
+            private Vector2I dragStart;
+
+            /// <summary>
+            /// Triggered whenever keyframe in a gradient is modified (added, removed or edited).
+            /// </summary>
+            public Action<ColorGradient> OnGradientModified;
+
+            /// <summary>
+            /// Constructs a new gradient key editor control.
+            /// </summary>
+            /// <param name="parent">GUI layout to attach the child GUI controls to.</param>
+            /// <param name="keys">Set of keys to initially display on the editor.</param>
+            /// <param name="width">Width of the editor in pixels.</param>
+            /// <param name="height">Height of the editor in pixels.</param>
+            public GradientKeyEditor(GUILayout parent, ColorGradientKey[] keys, int width, int height)
+            {
+                canvas = new GUICanvas();
+                parent.AddElement(canvas);
+
+                Rebuild(new List<ColorGradientKey>(keys), width, height);
+            }
+
+            /// <summary>
+            /// Rebuilds the editor display using the provided color keys.
+            /// </summary>
+            /// <param name="keys">Set of keys to initially display on the editor.</param>
+            public void Rebuild(List<ColorGradientKey> keys)
+            {
+                this.keys = keys;
+
+                canvas.Clear();
+                for (int i = 0; i < keys.Count; i++)
+                    DrawColor(keys[i].color, MathEx.Clamp01(keys[i].time), i == selectedIdx);
+            }
+
+            /// <summary>
+            /// Returns the bounds of the GUI element relative to the provided panel.
+            /// </summary>
+            public Rect2I GetBounds(GUIPanel relativeTo)
+            {
+                return GUIUtility.CalculateBounds(canvas, relativeTo);
+            }
+
+            /// <summary>
+            /// Rebuilds the editor display using the provided size and color keys.
+            /// </summary>
+            /// <param name="keys">Set of keys to initially display on the editor.</param>
+            /// <param name="width">Width of the editor in pixels.</param>
+            /// <param name="height">Height of the editor in pixels.</param>
+            private void Rebuild(List<ColorGradientKey> keys, int width, int height)
+            {
+                this.width = width;
+                this.height = height;
+
+                canvas.SetWidth(width);
+                canvas.SetHeight(height);
+
+                Rebuild(keys);
+            }
+
+            /// <summary>
+            /// Rebuilds the editor display using the currently set properties.
+            /// </summary>
+            private void Rebuild()
+            {
+                Rebuild(keys, width, height);
+            }
+
+            /// <summary>
+            /// Attempts to find a color key rectangle at the specified location.
+            /// </summary>
+            /// <param name="pixelCoords">Pixel position to look at, relative to the canvas element.</param>
+            /// <param name="keyIdx">Index of the color key, if any was found.</param>
+            /// <returns>True if the key was found under the provided position, false otherwise.</returns>
+            public bool FindKey(Vector2I pixelCoords, out int keyIdx)
+            {
+                keyIdx = -1;
+
+                if (pixelCoords.y < 0 || pixelCoords.y >= height)
+                    return false;
+
+                for (int i = 0; i < keys.Count; i++)
+                {
+                    float t = MathEx.Clamp01(keys[i].time);
+                    int x = (int)(t * Math.Max(width - RECT_WIDTH, 0));
+
+                    if (pixelCoords.x >= x & pixelCoords.x < (x + RECT_WIDTH))
+                    {
+                        keyIdx = i;
+                        return true;
+                    }
+                }
+
+                return false;
+            }
+
+            /// <summary>
+            /// Handles input. Should be called by the owning window whenever a pointer is pressed.
+            /// </summary>
+            /// <param name="panelPos">Position of the pointer relative to the panel parent to this element.</param>
+            /// <param name="button">Pointer button involved in the event.</param>
+            internal void OnPointerPressed(Vector2I panelPos, PointerButton button)
+            {
+                Rect2I canvasBounds = canvas.Bounds;
+                Vector2I canvasPos = panelPos - new Vector2I(canvasBounds.x, canvasBounds.y);
+
+                if (button == PointerButton.Left)
+                {
+                    int keyIdx;
+                    if (FindKey(canvasPos, out keyIdx))
+                    {
+                        selectedIdx = keyIdx;
+                        isMousePressedOverKey = true;
+                        dragStart = canvasPos;
+                    }
+                    else
+                        selectedIdx = -1;
+                    
+                    Rebuild();
+                }
+            }
+
+            /// <summary>
+            /// Handles input. Should be called by the owning window whenever a pointer is double-clicked.
+            /// </summary>
+            /// <param name="panelPos">Position of the pointer relative to the panel parent to this element.</param>
+            /// <param name="button">Pointer button involved in the event.</param>
+            internal void OnPointerDoubleClicked(Vector2I panelPos, PointerButton button)
+            {
+                Rect2I canvasBounds = canvas.Bounds;
+                if (!canvasBounds.Contains(panelPos))
+                    return;
+
+                Vector2I canvasPos = panelPos - new Vector2I(canvasBounds.x, canvasBounds.y);
+
+                int keyIdx;
+                if (FindKey(canvasPos, out keyIdx))
+                {
+                    ColorPicker.Show(keys[keyIdx].color,
+                        (b, color) =>
+                        {
+                            if (b)
+                            {
+                                ColorGradientKey key = keys[keyIdx];
+                                key.color = color;
+                                keys[keyIdx] = key;
+
+                                OnGradientModified?.Invoke(new ColorGradient(keys.ToArray()));
+                                Rebuild();
+                            }
+                        });
+                }
+                else
+                {
+                    float time = PixelToTime(canvasPos.x);
+                    if (time >= 0.0f && time <= 1.0f)
+                    {
+                        keys.Add(new ColorGradientKey(Color.Black, time));
+                        keys.Sort((lhs, rhs) => lhs.time.CompareTo(rhs.time));
+
+                        OnGradientModified?.Invoke(new ColorGradient(keys.ToArray()));
+                    }
+
+                    Rebuild();
+                }
+            }
+
+            /// <summary>
+            /// Handles input. Should be called by the owning window whenever a pointer is moved.
+            /// </summary>
+            /// <param name="panelPos">Position of the pointer relative to the panel parent to this element.</param>
+            /// <param name="button">Pointer button involved in the event.</param>
+            internal void OnPointerMoved(Vector2I panelPos, PointerButton button)
+            {
+                if (button != PointerButton.Left)
+                    return;
+
+                if (isMousePressedOverKey)
+                {
+                    Rect2I canvasBounds = canvas.Bounds;
+                    Vector2I canvasPos = panelPos - new Vector2I(canvasBounds.x, canvasBounds.y);
+
+                    if (!isDragInProgress)
+                    {
+                        int distance = Vector2I.Distance(canvasPos, dragStart);
+                        if (distance >= DRAG_START_DISTANCE)
+                            isDragInProgress = true;
+                    }
+
+                    if (isDragInProgress)
+                    {
+                        float time = PixelToTime(canvasPos.x);
+                        if (time >= 0.0f && time <= 1.0f)
+                        {
+                            ColorGradientKey key = keys[selectedIdx];
+                            key.time = time;
+                            keys[selectedIdx] = key;
+
+                            OnGradientModified?.Invoke(new ColorGradient(keys.ToArray()));
+                        }
+
+                        Rebuild();
+                    }
+                }
+            }
+
+            /// <summary>
+            /// Handles input. Should be called by the owning window whenever a pointer is released.
+            /// </summary>
+            /// <param name="panelPos">Position of the pointer relative to the panel parent to this element.</param>
+            internal void OnPointerReleased(Vector2I panelPos)
+            {
+                isDragInProgress = false;
+                isMousePressedOverKey = false;
+            }
+
+            /// <summary>
+            /// Handles input. Should be called by the owning window whenever a button is released.
+            /// </summary>
+            /// <param name="ev">Object containing button release event information.</param>
+            internal void OnButtonUp(ButtonEvent ev)
+            {
+                if (ev.Button == ButtonCode.Delete && selectedIdx != -1 && !isMousePressedOverKey)
+                {
+                    if (selectedIdx < keys.Count)
+                    {
+                        keys.RemoveAt(selectedIdx);
+                        OnGradientModified?.Invoke(new ColorGradient(keys.ToArray()));
+                    }
+
+                    selectedIdx = -1;
+                    Rebuild();
+                }
+            }
+
+            /** Converts a pixel position over the editor (on x axis) to a time in range [0, 1]. */
+            internal float PixelToTime(int x)
+            {
+                x -= RECT_WIDTH / 2;
+
+                return x / (float) Math.Max(this.width - RECT_WIDTH, 0);
+            }
+
+            /** Converts a time in range [0, 1] to a pixel position over the editor (on x axis). */
+            internal int TimeToPixel(float t)
+            {
+                return (int) Math.Min(t * Math.Max(width - RECT_WIDTH, 0), width - RECT_WIDTH - 1) + RECT_WIDTH / 2;
+            }
+
+            /// <summary>
+            /// Draws a rectangle representing a single color key in the gradient.
+            /// </summary>
+            /// <param name="color">Color to display.</param>
+            /// <param name="t">Time at which to draw the rectangle, in range [0, 1].</param>
+            /// <param name="selected">True to draw the rectangle as selected, plain otherwise.</param>
+            private void DrawColor(Color color, float t, bool selected)
+            {
+                int x = TimeToPixel(t);
+
+                Vector2I a = new Vector2I(x + RECT_WIDTH / 2, 0);
+                Vector2I b = new Vector2I(x + RECT_WIDTH / 2, height - 1);
+                Vector2I c = new Vector2I(x - RECT_WIDTH / 2, 0);
+                Vector2I d = new Vector2I(x - RECT_WIDTH / 2, height - 1);
+
+                Vector2I[] linePoints = new Vector2I[] { a, b, d, c, a };
+                Vector2I[] trianglePoints = new Vector2I[] { a, b, c, d };
+
+                Color colorNoAlpha = color;
+                colorNoAlpha.a = 1.0f;
+
+                canvas.DrawTriangleStrip(trianglePoints, colorNoAlpha, 102);
+                canvas.DrawPolyLine(linePoints, selected ? SELECTED_COLOR : PLAIN_COLOR, 100);
+
+                Vector2I alphaA = new Vector2I(x - 1, height - 1);
+                Vector2I alphaB = new Vector2I(x + RECT_WIDTH / 2, height / 2);
+                Vector2I alphaC = new Vector2I(x + RECT_WIDTH / 2, height - 1);
+
+                canvas.DrawTriangleList(new [] { alphaA, alphaB, alphaC }, Color.White * color.a, 101);
+            }
+        };
+    }
+
+    /** @} */
+}

+ 1 - 1
Source/bsf

@@ -1 +1 @@
-Subproject commit 895bec84fbfcbf204c0ca0f8effb7adefeb679bb
+Subproject commit f133f3785da3058bff7d293a30276c895d3e5cf6