Created
December 28, 2019 14:58
-
-
Save pixtur/c5af8d98e8f26442c8319e303fdd0d47 to your computer and use it in GitHub Desktop.
An alternative ImGui component for editing float values with a jog dial interaction
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Data; | |
using System.Numerics; | |
using ImGuiNET; | |
using T3.Gui.InputUi; | |
using UiHelpers; | |
namespace T3.Gui.Interaction | |
{ | |
/// <summary> | |
/// An alternative ImGui component for editing float values | |
/// </summary> | |
public static class SingleValueEdit | |
{ | |
/// <summary> | |
/// Wrapper coll for int type | |
/// </summary> | |
public static InputEditStateFlags Draw(ref int value, | |
Vector2 size, | |
int min = int.MinValue, | |
int max = int.MaxValue, | |
float scale = 0.1f, | |
string format = "{0:0}") | |
{ | |
double doubleValue = value; | |
var result = Draw(ref doubleValue, size, min, max, scale, format); | |
value = (int)doubleValue; | |
return result; | |
} | |
/// <summary> | |
/// Wrapper call for float type | |
/// </summary> | |
public static InputEditStateFlags Draw(ref float value, | |
Vector2 size, | |
float min = float.NegativeInfinity, | |
float max = float.PositiveInfinity, | |
float scale = 0.01f, | |
string format = "{0:0.00}") | |
{ | |
double floatValue = value; | |
var result = Draw(ref floatValue, size, min, max, scale, format); | |
value = (float)floatValue; | |
return result; | |
} | |
public static InputEditStateFlags Draw(ref double value, | |
Vector2 size, | |
double min = double.NegativeInfinity, | |
double max = double.PositiveInfinity, | |
float scale = 1, | |
string format = "{0:0.00}") | |
{ | |
var io = ImGui.GetIO(); | |
var id = ImGui.GetID("jog"); | |
_numberFormat = format; | |
if (id == _activeJogDialId) | |
{ | |
switch (_state) | |
{ | |
case JogDialStates.Dialing: | |
ImGui.PushStyleColor(ImGuiCol.Button, Color.Black.Rgba); | |
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Color.Black.Rgba); | |
ImGui.PushStyleColor(ImGuiCol.ButtonActive, Color.Black.Rgba); | |
DrawButtonWithDynamicLabel(FormatValueForButton(ref _editValue), ref size); | |
ImGui.PopStyleColor(3); | |
if (ImGui.IsMouseReleased(0)) | |
{ | |
var wasClick = ImGui.GetIO().MouseDragMaxDistanceSqr[0] < 4; | |
if (wasClick) | |
{ | |
if (io.KeyCtrl) | |
{ | |
SetState(JogDialStates.Inactive); | |
return InputEditStateFlags.ResetToDefault; | |
} | |
else | |
{ | |
SetState(JogDialStates.StartedTextInput); | |
} | |
} | |
else | |
{ | |
SetState(JogDialStates.Inactive); | |
} | |
break; | |
} | |
if (ImGui.IsItemDeactivated()) | |
{ | |
SetState(JogDialStates.Inactive); | |
break; | |
} | |
JogDialOverlay.Draw(io, min, max, scale); | |
break; | |
case JogDialStates.StartedTextInput: | |
ImGui.SetKeyboardFocusHere(); | |
SetState(JogDialStates.TextInput); | |
goto case JogDialStates.TextInput; | |
case JogDialStates.TextInput: | |
ImGui.PushStyleColor(ImGuiCol.Text, double.IsNaN(_editValue) | |
? Color.Red.Rgba | |
: Color.White.Rgba); | |
ImGui.SetNextItemWidth(size.X); | |
ImGui.InputText("##dialInput", ref _jogDialText, 20); | |
ImGui.PopStyleColor(); | |
if (ImGui.IsItemDeactivated()) | |
{ | |
SetState(JogDialStates.Inactive); | |
if (double.IsNaN(_editValue)) | |
_editValue = _startValue; | |
} | |
_editValue = Evaluate(_jogDialText); | |
break; | |
} | |
value = _editValue; | |
if (_state == JogDialStates.Inactive) | |
{ | |
return InputEditStateFlags.Finished; | |
} | |
_editValue = Math.Round(_editValue * 100) / 100; | |
return Math.Abs(_editValue - _startValue) > 0.0001f ? InputEditStateFlags.Modified : InputEditStateFlags.Started; | |
} | |
DrawButtonWithDynamicLabel(FormatValueForButton(ref value), ref size); | |
if (ImGui.IsItemActivated()) | |
{ | |
_activeJogDialId = id; | |
_editValue = value; | |
_startValue = value; | |
_jogDialText = FormatValueForButton(ref value); | |
SetState(JogDialStates.Dialing); | |
} | |
return InputEditStateFlags.Nothing; | |
} | |
private static void SetState(JogDialStates newState) | |
{ | |
switch (newState) | |
{ | |
case JogDialStates.Inactive: | |
{ | |
_activeJogDialId = 0; | |
break; | |
} | |
case JogDialStates.Dialing: | |
_center = ImGui.GetMousePos(); | |
break; | |
case JogDialStates.StartedTextInput: | |
break; | |
case JogDialStates.TextInput: | |
break; | |
} | |
_state = newState; | |
} | |
private static double Evaluate(string expression) | |
{ | |
try | |
{ | |
var table = new DataTable(); | |
table.Columns.Add("expression", typeof(string), expression); | |
var row = table.NewRow(); | |
table.Rows.Add(row); | |
return double.Parse((string)row["expression"]); | |
} | |
catch | |
{ | |
return float.NaN; | |
} | |
} | |
private static string FormatValueForButton(ref double value) | |
{ | |
return string.Format(_numberFormat, value); | |
} | |
/// <summary> | |
/// A horrible ImGui work around to have button that stays active while its label changes. | |
/// </summary> | |
private static void DrawButtonWithDynamicLabel(string label, ref Vector2 size) | |
{ | |
var color1 = Color.GetStyleColor(ImGuiCol.Text); | |
var keepPos = ImGui.GetCursorScreenPos(); | |
ImGui.Button("##dial", size); | |
ImGui.GetWindowDrawList().AddText(keepPos + new Vector2(4, 4), color1, label); | |
} | |
private enum JogDialStates | |
{ | |
Inactive, | |
Dialing, | |
StartedTextInput, | |
TextInput, | |
} | |
private static uint _activeJogDialId; | |
private static Vector2 _center; | |
private static double _editValue; | |
private static double _startValue; | |
private static string _jogDialText = ""; | |
private static JogDialStates _state = JogDialStates.Inactive; | |
private static string _numberFormat = "{0:0.00}"; | |
/// <summary> | |
/// Draws a circular dial to manipulate values with various speeds | |
/// </summary> | |
private static class JogDialOverlay | |
{ | |
public static void Draw(ImGuiIOPtr io, double min, double max, float scale) | |
{ | |
var foreground = ImGui.GetForegroundDrawList(); | |
ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); | |
var pLast = io.MousePos - io.MouseDelta - _center; | |
var pNow = io.MousePos - _center; | |
var distanceToCenter = pNow.Length(); | |
var r = NeutralRadius; | |
float activeSpeed = 0; | |
int index = 0; | |
var rot = (float)Im.Fmod(((_editValue - _startValue) * RadialIndicatorSpeed), 2 * Math.PI); | |
foreach (var segmentSpeed in SegmentSpeeds) | |
{ | |
var isLastSegment = index == SegmentSpeeds.Length - 1; | |
var isActive = | |
(distanceToCenter > r && distanceToCenter < r + SegmentWidth) || | |
(isLastSegment && distanceToCenter > r + SegmentWidth); | |
if (isActive) | |
activeSpeed = segmentSpeed; | |
if (isActive) | |
{ | |
const float opening = 3.14f * 1.75f; | |
foreground.PathArcTo( | |
_center, | |
radius: r + SegmentWidth / 2, | |
rot, | |
rot + opening, | |
num_segments: 64); | |
foreground.PathStroke(ActiveSegmentColor, false, SegmentWidth - Padding); | |
} | |
else | |
{ | |
foreground.AddCircle(_center, | |
radius: r + SegmentWidth / 2, | |
SegmentColor, | |
num_segments: 64, | |
thickness: SegmentWidth - Padding); | |
} | |
r += SegmentWidth; | |
index++; | |
} | |
var aLast = (float)Math.Atan2(pLast.X, pLast.Y); | |
var aNow = (float)Math.Atan2(pNow.X, pNow.Y); | |
var delta = aLast - aNow; | |
if (delta > 1.5f) | |
{ | |
delta -= (float)(2 * Math.PI); | |
} | |
else if (delta < -1.5f) | |
{ | |
delta += (float)(2 * Math.PI); | |
} | |
delta = (float)Math.Round(delta * 50) / 50; | |
_editValue += delta * activeSpeed * scale * 100; | |
_editValue = _editValue.Clamp(min, max); | |
} | |
const float SegmentWidth = 90; | |
const float NeutralRadius = 10; | |
const float RadialIndicatorSpeed = (float)(2 * Math.PI / 20); | |
const float Padding = 2; | |
private static float[] SegmentSpeeds = new[] | |
{ | |
(float)(0.5f / Math.PI), | |
(float)(20 * 0.5f / Math.PI) | |
}; | |
private static Color SegmentColor = new Color(1f, 1f, 1f, 0.05f); | |
private static Color ActiveSegmentColor = new Color(1f, 1f, 1f, 0.05f); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi,
I am porting the code to LuaJIT_ImGui.
I cant find the ImGui value for InputEditStateFlags.Started and similars.
Which is the equivalence?
There is an issue with several controls: second control mimics the first one.
In the example found in ocornut/imgui#2951 it does not happen, could be because you are wrapping them with something that assigns a different ID for each control?