using ImGuiNET; using T3.Core.Operator; using T3.Editor.Gui.Interaction; using T3.Editor.Gui.Styling; using T3.Editor.Gui.UiHelpers; using T3.Editor.UiModel.InputsAndTypes; namespace T3.Editor.Gui.InputUi.CombinedInputs; /// /// Custom UI for editing ADSR envelope parameters stored in a Vector4 (X=Attack, Y=Decay, Z=Sustain, W=Release). /// Used via MappedType attribute on Vector4 inputs. /// public sealed class AdsrEnvelopeInputUi : InputValueUi { public override IInputUi Clone() { return new AdsrEnvelopeInputUi { InputDefinition = InputDefinition, Parent = Parent, PosOnCanvas = PosOnCanvas, Relevancy = Relevancy, Size = Size, }; } protected override InputEditStateFlags DrawEditControl(string name, Symbol.Child.Input input, ref Vector4 envelope, bool readOnly) { var cloneIfModified = input.IsDefault; var modified = DrawAdsrControl(ref envelope, cloneIfModified); if (cloneIfModified && modified.HasFlag(InputEditStateFlags.Modified)) { input.IsDefault = false; } return modified; } /// /// Draws a compact ADSR envelope editor with visual curve display. /// Vector4 layout: X=Attack, Y=Decay, Z=Sustain, W=Release /// public static InputEditStateFlags DrawAdsrControl(ref Vector4 envelope, bool cloneIfModified) { var modified = InputEditStateFlags.Nothing; var drawList = ImGui.GetWindowDrawList(); var availableWidth = ImGui.GetContentRegionAvail().X; var frameHeight = ImGui.GetFrameHeight(); var envelopeHeight = frameHeight * 2f; var startPos = ImGui.GetCursorScreenPos(); var envelopeArea = new ImRect( startPos, new Vector2(startPos.X + availableWidth, startPos.Y + envelopeHeight) ); // Draw envelope background (match operator style) //drawList.AddRectFilled(envelopeArea.Min, envelopeArea.Max, UiColors.BackgroundFull.Fade(0.3f)); // --- Draw area under envelope curve with dark color --- var attack = Math.Max(0.001f, envelope.X); var decay = Math.Max(0.001f, envelope.Y); var sustain = Math.Clamp(envelope.Z, 0f, 1f); var release = Math.Max(0.001f, envelope.W); const float sustainDuration = 0.2f; var width = envelopeArea.GetWidth(); var height = envelopeArea.GetHeight(); var totalTime = attack + decay + sustainDuration + release; var attackX = envelopeArea.Min.X + width * (attack / totalTime); var decayX = envelopeArea.Min.X + width * ((attack + decay) / totalTime); var sustainX = envelopeArea.Min.X + width * ((attack + decay + sustainDuration) / totalTime); var baseY = envelopeArea.Max.Y - 2; var peakY = envelopeArea.Min.Y + 2; var sustainY = envelopeArea.Max.Y - (height - 4) * sustain - 2; var points = new Vector2[5]; points[0] = new Vector2(envelopeArea.Min.X, baseY); points[1] = new Vector2(attackX, peakY); points[2] = new Vector2(decayX, sustainY); points[3] = new Vector2(sustainX, sustainY); points[4] = new Vector2(envelopeArea.Max.X, baseY); var fillPoints = new Vector2[points.Length + 2]; fillPoints[0] = new Vector2(envelopeArea.Min.X, baseY); for (var i = 0; i < points.Length; i++) fillPoints[i + 1] = points[i]; fillPoints[^1] = new Vector2(envelopeArea.Max.X, baseY); drawList.AddConvexPolyFilled(ref fillPoints[0], fillPoints.Length, UiColors.BackgroundFull.Fade(0.3f)); // Draw envelope curve and handles modified |= DrawEnvelopeCurveWithHandles(drawList, envelopeArea, ref envelope); // Make the envelope area interactive for dragging (legacy drag) ImGui.SetCursorScreenPos(envelopeArea.Min); ImGui.InvisibleButton("##envelope_drag", envelopeArea.GetSize()); if (ImGui.IsItemActive() && _activeDragTarget == DragTarget.None) { modified |= HandleEnvelopeDrag(ref envelope, envelopeArea, cloneIfModified); } // Move cursor below the envelope graph ImGui.SetCursorScreenPos(new Vector2(startPos.X, envelopeArea.Max.Y + 2)); modified |= DrawParameterRow(ref envelope, availableWidth, cloneIfModified); return modified; } // --- Draw envelope curve with handles --- private static InputEditStateFlags DrawEnvelopeCurveWithHandles(ImDrawListPtr drawList, ImRect area, ref Vector4 envelope) { var modified = InputEditStateFlags.Nothing; var attack = Math.Max(0.001f, envelope.X); var decay = Math.Max(0.001f, envelope.Y); var sustain = Math.Clamp(envelope.Z, 0f, 1f); var release = Math.Max(0.001f, envelope.W); const float sustainDuration = 0.2f; var width = area.GetWidth(); var height = area.GetHeight(); var totalTime = attack + decay + sustainDuration + release; var attackX = area.Min.X + width * (attack / totalTime); var decayX = area.Min.X + width * ((attack + decay) / totalTime); var sustainX = area.Min.X + width * ((attack + decay + sustainDuration) / totalTime); var sustainY = area.Max.Y - (height - 4) * sustain - 2; var peakY = area.Min.Y + 2; // --- Draw envelope curve with 5 points (classic style) --- var points = new Vector2[5]; points[0] = new Vector2(area.Min.X, area.Max.Y - 2); // Start points[1] = new Vector2(attackX, peakY); // Attack peak points[2] = new Vector2(decayX, sustainY); // End of decay points[3] = new Vector2(sustainX, sustainY); // End of sustain points[4] = new Vector2(area.Max.X, area.Max.Y - 2); // End of release // Draw curve line drawList.AddPolyline(ref points[0], points.Length, UiColors.WidgetLine, ImDrawFlags.None, 1.5f); // Draw grid lines (matching AnimValueUi style) drawList.AddLine(new Vector2(attackX, area.Min.Y), new Vector2(attackX, area.Max.Y), UiColors.WidgetAxis, 1); drawList.AddLine(new Vector2(decayX, area.Min.Y), new Vector2(decayX, area.Max.Y), UiColors.WidgetAxis, 1); drawList.AddLine(new Vector2(sustainX, area.Min.Y), new Vector2(sustainX, area.Max.Y), UiColors.WidgetAxis, 1); // Draw sustain level line drawList.AddLine(new Vector2(decayX, sustainY), new Vector2(sustainX, sustainY), UiColors.WidgetAxis, 1); // Draw labels var labelColor = UiColors.TextMuted; ImGui.PushFont(Fonts.FontSmall); var labelY = area.Max.Y - 12; // Calculate label positions centered in each region var attackCenter = area.Min.X + (attackX - area.Min.X) / 2; var decayCenter = attackX + (decayX - attackX) / 2; var sustainCenter = decayX + (sustainX - decayX) / 2; var releaseCenter = sustainX + (area.Max.X - sustainX) / 2; // Get text width to center horizontally var aSize = ImGui.CalcTextSize("A"); var dSize = ImGui.CalcTextSize("D"); var sSize = ImGui.CalcTextSize("S"); var rSize = ImGui.CalcTextSize("R"); drawList.AddText(new Vector2(attackCenter - aSize.X / 2, labelY), labelColor, "A"); drawList.AddText(new Vector2(decayCenter - dSize.X / 2, labelY), labelColor, "D"); drawList.AddText(new Vector2(sustainCenter - sSize.X / 2, labelY), labelColor, "S"); drawList.AddText(new Vector2(releaseCenter - rSize.X / 2, labelY), labelColor, "R"); ImGui.PopFont(); // --- Handle logic --- var scale = new Vector2(1, 1); // No zoom in compact UI var handles = new[] { new HandleInfo(DragTarget.Attack, new Vector2(attackX, peakY), "Attack"), new HandleInfo(DragTarget.Decay, new Vector2(decayX, sustainY), "Decay"), new HandleInfo(DragTarget.Sustain, new Vector2((decayX + sustainX) / 2, sustainY), "Sustain"), new HandleInfo(DragTarget.Release, new Vector2(area.Max.X - 2, area.Max.Y - 2), "Release") }; modified |= DrawAndHandleHandles(drawList, area, handles, scale, ref envelope); return modified; } private enum DragTarget { None, Attack, Decay, Sustain, Release } private readonly struct HandleInfo(DragTarget target, Vector2 position, string label) { public readonly DragTarget Target = target; public readonly Vector2 Position = position; public readonly string Label = label; } // --- Draw and handle draggable handles --- private static InputEditStateFlags DrawAndHandleHandles(ImDrawListPtr drawList, ImRect area, HandleInfo[] handles, Vector2 scale, ref Vector4 envelope) { var modified = InputEditStateFlags.Nothing; var mousePos = ImGui.GetMousePos(); // Diamond size and style matching AdsrEnvelopeUi var diamondSize = 8f; var half = diamondSize / 2f; var anyHandleActive = false; var hoveredTarget = DragTarget.None; for (int i = 0; i < handles.Length; i++) { var handle = handles[i]; var id = $"##adsr_handle_{i}"; ImGui.SetCursorScreenPos(handle.Position - new Vector2(half, half)); ImGui.InvisibleButton(id, new Vector2(diamondSize, diamondSize)); var isHovered = ImGui.IsItemHovered(); var isActiveHandle = ImGui.IsItemActive(); if (isHovered && !anyHandleActive) { hoveredTarget = handle.Target; // Set cursor type based on handle switch (handle.Target) { case DragTarget.Attack: case DragTarget.Decay: case DragTarget.Release: ImGui.SetMouseCursor(ImGuiMouseCursor.ResizeEW); break; case DragTarget.Sustain: ImGui.SetMouseCursor(ImGuiMouseCursor.ResizeNS); break; } // Show tooltip for hovered handle ImGui.SetTooltip($"{handle.Label}: drag to adjust"); } if (isActiveHandle) { hoveredTarget = handle.Target; anyHandleActive = true; } // Draw diamond (rotated square) var fillColor = isHovered || isActiveHandle ? UiColors.ForegroundFull : UiColors.StatusAnimated; var outlineColor = 0xFF000000; // Black border var center = handle.Position; var diamond = new Vector2[4]; diamond[0] = new Vector2(center.X, center.Y - half); // top diamond[1] = new Vector2(center.X + half, center.Y); // right diamond[2] = new Vector2(center.X, center.Y + half); // bottom diamond[3] = new Vector2(center.X - half, center.Y); // left drawList.AddConvexPolyFilled(ref diamond[0], 4, fillColor); drawList.AddPolyline(ref diamond[0], 4, outlineColor, ImDrawFlags.Closed, 1f); // Handle dragging if (isActiveHandle) { var delta = ImGui.GetIO().MouseDelta; var width = area.GetWidth(); var height = area.GetHeight(); var timeSensitivity = 0.5f; if (ImGui.GetIO().KeyShift) timeSensitivity *= 0.1f; var newEnvelope = envelope; switch (handle.Target) { case DragTarget.Attack: newEnvelope.X = Math.Max(0.001f, newEnvelope.X + delta.X / width * timeSensitivity); break; case DragTarget.Decay: newEnvelope.Y = Math.Max(0.001f, newEnvelope.Y + delta.X / width * timeSensitivity); break; case DragTarget.Sustain: newEnvelope.Z = Math.Clamp(newEnvelope.Z - delta.Y / height, 0f, 1f); break; case DragTarget.Release: newEnvelope.W = Math.Max(0.001f, newEnvelope.W + delta.X / width * timeSensitivity); break; } envelope = newEnvelope; modified = InputEditStateFlags.Modified; } } return modified; } private static DragTarget _activeDragTarget = DragTarget.None; private static InputEditStateFlags HandleEnvelopeDrag(ref Vector4 envelope, ImRect area, bool cloneIfModified) { var modified = InputEditStateFlags.Nothing; var mousePos = ImGui.GetMousePos(); var mouseDelta = ImGui.GetIO().MouseDelta; if (Math.Abs(mouseDelta.X) < 0.001f && Math.Abs(mouseDelta.Y) < 0.001f) return modified; var attack = Math.Max(0.001f, envelope.X); var decay = Math.Max(0.001f, envelope.Y); var release = Math.Max(0.001f, envelope.W); const float sustainDuration = 0.2f; // Determine which segment we're in based on mouse position var width = area.GetWidth(); var relX = (mousePos.X - area.Min.X) / width; var totalTime = attack + decay + sustainDuration + release; var attackEnd = attack / totalTime; var decayEnd = (attack + decay) / totalTime; var sustainEnd = (attack + decay + sustainDuration) / totalTime; var sensitivity = 0.01f; if (ImGui.GetIO().KeyShift) sensitivity *= 0.1f; if (relX < attackEnd) { envelope.X = Math.Max(0.001f, envelope.X + mouseDelta.X * sensitivity); modified = InputEditStateFlags.Modified; } else if (relX < decayEnd) { envelope.Y = Math.Max(0.001f, envelope.Y + mouseDelta.X * sensitivity); modified = InputEditStateFlags.Modified; } else if (relX < sustainEnd) { envelope.Z = Math.Clamp(envelope.Z - mouseDelta.Y * 0.01f, 0f, 1f); modified = InputEditStateFlags.Modified; } else { envelope.W = Math.Max(0.001f, envelope.W + mouseDelta.X * sensitivity); modified = InputEditStateFlags.Modified; } return modified; } private static InputEditStateFlags DrawParameterRow(ref Vector4 envelope, float availableWidth, bool cloneIfModified) { var modified = InputEditStateFlags.Nothing; var paramWidth = (availableWidth - 12) / 4; var frameHeight = ImGui.GetFrameHeight(); var size = new Vector2(paramWidth, frameHeight); var attack = envelope.X; var decay = envelope.Y; var sustain = envelope.Z; var release = envelope.W; ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(3, 0)); ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(2, 2)); // Attack ImGui.PushID("A"); var editState = SingleValueEdit.Draw(ref attack, size, 0.001f, 10f, true, false, 0.01f, "A:{0:0.000}"); if (editState.HasFlag(InputEditStateFlags.Modified)) { modified |= InputEditStateFlags.Modified; envelope.X = Math.Max(0.001f, attack); } if (editState.HasFlag(InputEditStateFlags.Finished)) modified |= InputEditStateFlags.Finished; ImGui.PopID(); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Attack time (seconds)"); ImGui.SameLine(); // Decay ImGui.PushID("D"); editState = SingleValueEdit.Draw(ref decay, size, 0.001f, 10f, true, false, 0.01f, "D:{0:0.000}"); if (editState.HasFlag(InputEditStateFlags.Modified)) { modified |= InputEditStateFlags.Modified; envelope.Y = Math.Max(0.001f, decay); } if (editState.HasFlag(InputEditStateFlags.Finished)) modified |= InputEditStateFlags.Finished; ImGui.PopID(); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Decay time (seconds)"); ImGui.SameLine(); // Sustain ImGui.PushID("S"); editState = SingleValueEdit.Draw(ref sustain, size, 0f, 1f, true, true, 0.01f, "S:{0:0.00}"); if (editState.HasFlag(InputEditStateFlags.Modified)) { modified |= InputEditStateFlags.Modified; envelope.Z = Math.Clamp(sustain, 0f, 1f); } if (editState.HasFlag(InputEditStateFlags.Finished)) modified |= InputEditStateFlags.Finished; ImGui.PopID(); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Sustain level (0-1)"); ImGui.SameLine(); // Release ImGui.PushID("R"); editState = SingleValueEdit.Draw(ref release, size, 0.001f, 10f, true, false, 0.01f, "R:{0:0.000}"); if (editState.HasFlag(InputEditStateFlags.Modified)) { modified |= InputEditStateFlags.Modified; envelope.W = Math.Max(0.001f, release); } if (editState.HasFlag(InputEditStateFlags.Finished)) modified |= InputEditStateFlags.Finished; ImGui.PopID(); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Release time (seconds)"); ImGui.PopStyleVar(2); return modified; } protected override void DrawReadOnlyControl(string name, ref Vector4 value) { ImGui.TextUnformatted($"A:{value.X:F3} D:{value.Y:F3} S:{value.Z:F2} R:{value.W:F3}"); } }