Files
2026-07-13 13:13:17 +08:00

509 lines
25 KiB
C#

using ImGuiNET;
using T3.Core.DataTypes;
using T3.Core.DataTypes.Vector;
using T3.Core.Utils;
using T3.Editor.Gui.Interaction;
using T3.Editor.Gui.Styling;
using T3.Editor.UiModel.InputsAndTypes;
// ReSharper disable RedundantArgumentDefaultValue
namespace T3.Editor.Gui.UiHelpers;
[HelpUiID("GradientEditor")]
public static class GradientEditor
{
/// <summary>
/// Draw a gradient control that returns true, if gradient has been modified
/// </summary>
public static InputEditStateFlags Draw(ref Gradient gradientRef, ImDrawListPtr drawList, ImRect areaOnScreen, bool cloneIfModified = false)
{
var anyStepBeingDragged = false;
Gradient.Step draggedStep = null;
var gradientForEditing = gradientRef;
if (cloneIfModified)
{
// gradientForEditing = gradientRef.TypedClone();
gradientForEditing = gradientRef != _hoveredGradientRef
? gradientRef.TypedClone()
: _hoveredGradientForEditing;
}
gradientForEditing.SortHandles();
DrawGradient(gradientForEditing, drawList, areaOnScreen);
if (!(areaOnScreen.GetHeight() >= RequiredHeightForHandles))
return InputEditStateFlags.Nothing;
Gradient.Step hoveredStep = null;
// Draw handles
var editResult = InputEditStateFlags.Nothing;
Gradient.Step removedStep = null;
foreach (var step in gradientForEditing.Steps)
{
var result = DrawHandle(step);
if(result != InputEditStateFlags.Nothing)
{
editResult = result;
}
}
// Trash zone
if (anyStepBeingDragged)
{
var foregroundDrawList = ImGui.GetForegroundDrawList();
var centerX = (int)areaOnScreen.GetCenter().X;
var y = areaOnScreen.Max.Y;
var intrashzone = ImGui.IsMousePosValid() && ImGui.GetMousePos().Y > y + RemoveThreshold * T3Ui.UiScaleFactor;
var color = intrashzone
? UiColors.ForegroundFull
: UiColors.StatusWarning;
var bordercolor = intrashzone ? 1.0f : 0.0f;
var trashZoneRect = new ImRect(
new Vector2(areaOnScreen.Min.X, y + 4 * T3Ui.UiScaleFactor),
new Vector2(areaOnScreen.Max.X, y + 30 * T3Ui.UiScaleFactor)
);
foregroundDrawList.AddRectFilled(trashZoneRect.Min, trashZoneRect.Max, UiColors.BackgroundFull.Fade(0.6f));
foregroundDrawList.AddRect(trashZoneRect.Min, trashZoneRect.Max, UiColors.StatusWarning.Fade(bordercolor));
Icons.DrawIconAtScreenPosition(
Icon.Trash,
new Vector2(centerX, y + 10 * T3Ui.UiScaleFactor),
foregroundDrawList,
color
);
}
if (removedStep != null)
{
gradientForEditing.Steps.Remove(removedStep);
editResult = InputEditStateFlags.ModifiedAndFinished;
}
if (cloneIfModified && hoveredStep != null)
{
_hoveredGradientForEditing = gradientForEditing;
_hoveredGradientRef = gradientRef;
}
// Insert step area...
var insertRangeMin = new Vector2(areaOnScreen.Min.X, areaOnScreen.Max.Y - StepHandleSize.Y);
ImGui.SetCursorScreenPos(insertRangeMin);
var canInsertNewStep = areaOnScreen.GetHeight() > MinInsertHeight && hoveredStep == null;
var normalizedPosition = (ImGui.GetMousePos().X - insertRangeMin.X) / areaOnScreen.GetWidth();
if (ImGui.InvisibleButton("insertRange", areaOnScreen.Max - insertRangeMin) && canInsertNewStep)
{
gradientForEditing.Steps.Add(new Gradient.Step()
{
NormalizedPosition = normalizedPosition,
Id = Guid.NewGuid(),
Color = gradientForEditing.Sample(normalizedPosition)
});
editResult = InputEditStateFlags.ModifiedAndFinished;
}
if (canInsertNewStep && ImGui.IsItemHovered() && !ImGui.IsItemActive())
{
var handleArea = GetHandleAreaForPosition(normalizedPosition);
// drawList.AddRect(handleArea.Min + Vector2.One, handleArea.Max - Vector2.One, new Color(1f, 1f, 1f, 0.4f));
drawList.AddLine(handleArea.Min + new Vector2(StepHandleSize.X/2,-14), handleArea.Max - new Vector2(StepHandleSize.X / 2, 0), new Color(0.5f, 0.5f, 0.5f, 1.0f));
}
// Draw CurveOverlays
if (ImGui.IsItemHovered() || editResult != InputEditStateFlags.Nothing)
{
DrawCurveLines(gradientRef, drawList, areaOnScreen);
}
CustomComponents.ContextMenuForItem(() =>
{
if (ImGui.MenuItem("Reverse"))
{
foreach (var s in gradientForEditing.Steps)
{
s.NormalizedPosition = 1f - s.NormalizedPosition;
}
gradientForEditing.SortHandles();
editResult = InputEditStateFlags.ModifiedAndFinished;
}
var isHold = gradientForEditing.Interpolation == Gradient.Interpolations.Hold;
var stepCount = gradientForEditing.Steps.Count;
var canDistribute = stepCount >= 2 || (stepCount == 1 && !isHold);
if (ImGui.MenuItem("Distribute evenly", canDistribute))
{
var steps = gradientForEditing.Steps;
if (isHold)
{
// Hold: split the gradient into equal-width color bands
for (var index = 0; index < steps.Count; index++)
{
steps[index].NormalizedPosition = (float)index / steps.Count;
}
}
else if (steps.Count == 1)
{
// Single stop with smooth interpolation -> center
steps[0].NormalizedPosition = 0.5f;
}
else
{
// Smooth: anchor first/last to the corners, spread the rest evenly
var divisor = steps.Count - 1;
for (var index = 0; index < steps.Count; index++)
{
steps[index].NormalizedPosition = (float)index / divisor;
}
}
gradientForEditing.SortHandles();
editResult = InputEditStateFlags.ModifiedAndFinished;
}
if (ImGui.BeginMenu("Gradient presets..."))
{
var foregroundDrawList = ImGui.GetForegroundDrawList();
for (var index = 0; index < GradientPresets.Presets.Count; index++)
{
ImGui.PushID(index);
var preset = GradientPresets.Presets[index];
if (ImGui.InvisibleButton("" + index, new Vector2(100, ImGui.GetFrameHeight())))
{
var clone = preset.TypedClone();
gradientForEditing.Steps = clone.Steps;
gradientForEditing.Interpolation = clone.Interpolation;
editResult = InputEditStateFlags.ModifiedAndFinished;
}
var rect = new ImRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax());
DrawGradient(preset, foregroundDrawList, rect);
ImGui.SameLine();
if (CustomComponents.IconButton(Icon.Trash,
Vector2.One * ImGui.GetFrameHeight()))
{
GradientPresets.Presets.Remove(preset);
GradientPresets.Save();
ImGui.PopID();
break;
}
ImGui.PopID();
}
if (ImGui.MenuItem("Save"))
{
GradientPresets.Presets.Add(gradientForEditing.TypedClone());
GradientPresets.Save();
}
ImGui.EndMenu();
}
if (ImGui.BeginMenu("Interpolation..."))
{
foreach (Gradient.Interpolations value in Enum.GetValues(typeof(Gradient.Interpolations)))
{
var isSelected = gradientForEditing.Interpolation == value;
if (ImGui.MenuItem(value.ToString(), "", isSelected))
{
gradientForEditing.Interpolation = value;
editResult = InputEditStateFlags.ModifiedAndFinished;
}
}
ImGui.EndMenu();
}
}, "Gradient", "gradientContextMenu");
if ((editResult & InputEditStateFlags.Modified) != 0 && cloneIfModified)
{
gradientRef = gradientForEditing;
_hoveredGradientRef = null;
_hoveredGradientForEditing = null;
}
return editResult;
InputEditStateFlags DrawHandle(Gradient.Step step)
{
//var StepHandleSize = new Vector2(12, 16) * T3Ui.UiScaleFactor;
var stepModified = InputEditStateFlags.Nothing;
ImGui.PushID(step.Id.GetHashCode());
var handleArea = GetHandleAreaForPosition(step.NormalizedPosition);
handleArea.Min.X = areaOnScreen.Min.X - StepHandleSize.X / 2f + areaOnScreen.GetWidth() * step.NormalizedPosition;
handleArea.Min.Y = areaOnScreen.Max.Y - StepHandleSize.Y;
// Interaction
ImGui.SetCursorScreenPos(handleArea.Min);
ImGui.InvisibleButton("gradientStep", new Vector2(StepHandleSize.X, areaOnScreen.GetHeight()));
// Stub for ColorEditButton that allows quick sliders. Sadly this doesn't work with right mouse button drag.
//stepModified |= ColorEditButton.Draw(ref step.Color, new Vector2(StepHandleSize.X, areaOnScreen.GetHeight()));
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByPopup))
{
hoveredStep = step;
handleArea.Min.Y = areaOnScreen.Max.Y - StepHandleSize.Y -2*T3Ui.UiScaleFactor;
}
var isDraggedOutside = false;
if (ImGui.IsItemActive())
{
stepModified |= InputEditStateFlags.Started;
}
if (ImGui.IsItemActive() && ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
anyStepBeingDragged = true;
draggedStep = step;
FrameStats.Current.OpenedPopupCapturedMouse = true;
if (ImGui.GetIO().KeyAlt)
{
var previousColor = step.Color;
ColorEditButton.VerticalColorSlider(step.Color, handleArea.GetCenter(), step.Color.W);
var mouseDragDelta = ImGui.GetMouseDragDelta().Y / 100;
ImGui.ResetMouseDragDelta();
step.Color.W = (previousColor.W - mouseDragDelta).Clamp(0, 1);
}
else
{
step.NormalizedPosition = ((ImGui.GetMousePos().X - areaOnScreen.Min.X) / areaOnScreen.GetWidth()).Clamp(0, 1);
}
isDraggedOutside = ImGui.GetMousePos().Y > areaOnScreen.Max.Y + RemoveThreshold;
stepModified = InputEditStateFlags.Modified;
}
// Draw handle
if (isDraggedOutside)
{
handleArea.Min.Y += 25;
handleArea.Max.Y += 25;
}
if (ImGui.IsItemDeactivated())
{
var mouseOutsideThresholdAfterDrag = ImGui.GetMousePos().Y > areaOnScreen.Max.Y + RemoveThreshold;
if (mouseOutsideThresholdAfterDrag && gradientForEditing.Steps.Count > 1)
removedStep = step;
stepModified = InputEditStateFlags.ModifiedAndFinished;
}
/*var points = new[]
{
new Vector2(handleArea.Min.X, handleArea.Max.Y),
handleArea.Max,
new Vector2(handleArea.Max.X , handleArea.Min.Y),
};
drawList.AddConvexPolyFilled(ref points[0], 3, new Color(0.15f, 0.15f, 0.15f, 1));*/
drawList.AddRectFilled(handleArea.Min, handleArea.Max, ImGui.ColorConvertFloat4ToU32(step.Color));
if (MathUtils.HasHdrRange(step.Color, out var intensity))
{
drawList.DrawTriangleUp(handleArea.GetCenter(),
Color.Black,
intensity *4);
}
var borders = new Vector2(T3Ui.UiScaleFactor-.5f);
drawList.AddRect(handleArea.Min + borders, handleArea.Max + borders, UiColors.BackgroundFull.Fade(0.7f), 0, ImDrawFlags.None, T3Ui.UiScaleFactor*2);
drawList.AddRect(handleArea.Min , handleArea.Max , UiColors.ForegroundFull, 0, ImDrawFlags.None, T3Ui.UiScaleFactor);
if (ImGui.IsItemHovered()
&& ImGui.IsMouseReleased(0)
&& ImGui.GetIO().MouseDragMaxDistanceAbs[0].Length() < UserSettings.Config.ClickThreshold
&& !ImGui.IsPopupOpen("##colorEdit"))
{
//FrameStats.Current.OpenedPopUpName = "##colorEdit";
ImGui.OpenPopup("##colorEdit");
ImGui.SetNextWindowPos(new Vector2(handleArea.Min.X, handleArea.Max.Y));
}
var popUpResult = ColorEditPopup.DrawPopup(ref step.Color, step.Color);
if (popUpResult != InputEditStateFlags.Nothing)
{
stepModified = popUpResult;
}
ImGui.PopID();
return stepModified;
}
ImRect GetHandleAreaForPosition(float normalizedStepPosition)
{
var x = areaOnScreen.Min.X - StepHandleSize.X / 2f + areaOnScreen.GetWidth() * normalizedStepPosition;
return new ImRect(new Vector2(x, areaOnScreen.Max.Y - StepHandleSize.Y), new Vector2(x + StepHandleSize.X, areaOnScreen.Max.Y + 2));
}
}
private static void DrawCurveLines(Gradient gradientRef, ImDrawListPtr drawList, ImRect areaOnScreen)
{
var curveIndicationArea = areaOnScreen;
var count = 80;
var fadeCurves = 0.7f;
var pR = new Vector2[count];
var pG = new Vector2[count];
var pB = new Vector2[count];
var pA = new Vector2[count];
for (var i = 0; i <count; i++)
{
var f = (float)i / (count - 1);
var c = gradientRef.Sample(f);
c = Vector4.Clamp(c, Vector4.One * -0.1f, Vector4.One * 1.1f);
pR[i] = new Vector2( MathUtils.Lerp(curveIndicationArea.Min.X, curveIndicationArea.Max.X,f),
MathUtils.Lerp(curveIndicationArea.Max.Y, curveIndicationArea.Min.Y,c.X));
pG[i] = new Vector2( MathUtils.Lerp(curveIndicationArea.Min.X, curveIndicationArea.Max.X,f),
MathUtils.Lerp(curveIndicationArea.Max.Y, curveIndicationArea.Min.Y,c.Y));
pB[i] = new Vector2( MathUtils.Lerp(curveIndicationArea.Min.X, curveIndicationArea.Max.X,f),
MathUtils.Lerp(curveIndicationArea.Max.Y, curveIndicationArea.Min.Y,c.Z));
pA[i] = new Vector2( MathUtils.Lerp(curveIndicationArea.Min.X, curveIndicationArea.Max.X,f),
MathUtils.Lerp(curveIndicationArea.Max.Y, curveIndicationArea.Min.Y,c.W));
}
var shadow = Color.Black.Fade(0.2f);
drawList.AddPolyline(ref pR[0], count, shadow, ImDrawFlags.None, 5);
drawList.AddPolyline(ref pG[0], count, shadow, ImDrawFlags.None, 5);
drawList.AddPolyline(ref pB[0], count, shadow, ImDrawFlags.None, 5);
drawList.AddPolyline(ref pA[0], count, shadow, ImDrawFlags.None, 5);
drawList.AddPolyline(ref pR[0], count, Color.Red.Fade(fadeCurves), ImDrawFlags.None, 1);
drawList.AddPolyline(ref pG[0], count, Color.Green.Fade(fadeCurves), ImDrawFlags.None, 1);
drawList.AddPolyline(ref pB[0], count, Color.Blue.Fade(fadeCurves), ImDrawFlags.None, 1);
drawList.AddPolyline(ref pA[0], count, Color.White.Fade(fadeCurves), ImDrawFlags.None, 1);
}
/// <summary>
/// Dealing with default reference values is complex:
/// 1. We want to render the default gradient.
/// 2. We directly want to start manipulating from the default without modifying the default.
/// 3. We NEVER ever want to modify the references default gradient.
/// 4. To manipulate a gradient step we have to have a stable step GUID
///
/// To do this, we...
/// - clone gradients before rendering (which results in new random ids for steps)
/// - reused a previously cloned gradient one of it's steps is being hovered.
/// </summary>
private static Gradient _hoveredGradientRef;
private static Gradient _hoveredGradientForEditing;
private static void DrawGradient(Gradient gradient, ImDrawListPtr drawList, ImRect areaOnScreen)
{
drawList.AddRect(areaOnScreen.Min, areaOnScreen.Max, Color.Black);
drawList.AddRectFilled(areaOnScreen.Min, areaOnScreen.Max, new Color(0.15f, 0.15f, 0.15f, 1));
if (gradient.Steps == null || gradient.Steps.Count == 0)
{
drawList.AddText(areaOnScreen.Min + areaOnScreen.GetSize() * 0.5f - new Vector2(Fonts.FontNormal.FontSize * 0.5f) , UiColors.ForegroundFull.Fade(0.4f), "?");
//Log.Warning("Can't draw invalid gradient");
return;
}
// Draw Gradient background
{
CustomComponents.FillWithStripes(drawList, areaOnScreen,1);
}
// Draw Gradient
var minPos = areaOnScreen.Min;
var maxPos = areaOnScreen.Max;
var leftColor = ImGui.ColorConvertFloat4ToU32(gradient.Steps[0].Color);
// Draw complex gradient
if (gradient.Interpolation == Gradient.Interpolations.Smooth || gradient.Interpolation == Gradient.Interpolations.OkLab)
{
var f = 0f;
for (var stepIndex = 0; stepIndex < gradient.Steps.Count; stepIndex++)
{
var step = gradient.Steps[stepIndex];
var steps = 5;
var rightF = step.NormalizedPosition;
var rangeF = (rightF - f);
var stepSizeF = rangeF / steps;
var pixelStepSize = (areaOnScreen.GetWidth() * rangeF) / steps;
maxPos.X = minPos.X + pixelStepSize;
for (var i = 0; i < steps; i++)
{
var nextF = f + stepSizeF;
var nextColor = ImGui.ColorConvertFloat4ToU32(gradient.Sample(nextF));
drawList.AddRectFilledMultiColor(minPos,
maxPos,
leftColor,
nextColor,
nextColor,
leftColor);
maxPos.X += pixelStepSize;
minPos.X += pixelStepSize;
f = nextF;
leftColor = nextColor;
}
}
}
// Linear gradient
else
{
foreach (var step in gradient.Steps)
{
var rightColor = ImGui.ColorConvertFloat4ToU32(step.Color);
maxPos.X = areaOnScreen.Min.X + areaOnScreen.GetWidth() * step.NormalizedPosition;
if (gradient.Interpolation == Gradient.Interpolations.Hold)
{
drawList.AddRectFilledMultiColor(minPos,
maxPos,
leftColor,
leftColor,
leftColor,
leftColor);
}
else
{
drawList.AddRectFilledMultiColor(minPos,
maxPos,
leftColor,
rightColor,
rightColor,
leftColor);
}
minPos.X = maxPos.X;
leftColor = rightColor;
}
}
if (minPos.X < areaOnScreen.Max.X)
{
drawList.AddRectFilled(minPos, areaOnScreen.Max, leftColor);
}
}
private const float RemoveThreshold = 15;
private const float RequiredHeightForHandles = 20;
private const int MinInsertHeight = 15;
public static Vector2 StepHandleSize => new Vector2(9, 15) * T3Ui.UiScaleFactor;
}