1322 lines
48 KiB
C#
1322 lines
48 KiB
C#
#nullable enable
|
||
using ImGuiNET;
|
||
using T3.Editor.Gui.Interaction;
|
||
using T3.Editor.Gui.Styling;
|
||
using T3.Editor.Gui.Styling.Markdown;
|
||
using T3.Editor.Gui.UiHelpers;
|
||
using T3.Editor.UiModel.InputsAndTypes;
|
||
|
||
namespace T3.Editor.Gui.Input;
|
||
|
||
/// <summary>
|
||
/// A set of custom widgets that allow to quickly draw dialogs with a more "traditional" labels on the left side of input fields.
|
||
/// It also provides a bunch of helper methods for minimal layout control.
|
||
/// </summary>
|
||
internal static class FormInputs
|
||
{
|
||
public static void BeginFrame()
|
||
{
|
||
SetIndentToParameters();
|
||
}
|
||
|
||
public static void AddSectionHeader(string label)
|
||
{
|
||
//AddVerticalSpace(1);
|
||
ImGui.PushFont(Fonts.FontLarge);
|
||
ImGui.Text(label);
|
||
ImGui.PopFont();
|
||
//AddVerticalSpace(20);
|
||
}
|
||
|
||
public static void AddSectionHeaderParam(string label)
|
||
{
|
||
ImGui.PushFont(Fonts.FontLarge);
|
||
ImGui.Text(label.AddSpacesForImGuiOutput());
|
||
ImGui.PopFont();
|
||
}
|
||
|
||
public static void AddSectionSubHeader(string label)
|
||
{
|
||
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
|
||
ImGui.PushFont(Fonts.FontBold);
|
||
|
||
AddVerticalSpace(15 * T3Ui.UiScaleFactor);
|
||
ImGui.TextUnformatted(label);
|
||
AddVerticalSpace(5 * T3Ui.UiScaleFactor);
|
||
ImGui.PopFont();
|
||
ImGui.PopStyleColor();
|
||
}
|
||
|
||
public static void DrawFieldSetHeader(string label, bool useParamIndent = false)
|
||
{
|
||
// ImGui.SameLine(0,_paramIndent);
|
||
|
||
|
||
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
|
||
ImGui.PushFont(Fonts.FontSmall);
|
||
|
||
AddVerticalSpace(5);
|
||
|
||
if (useParamIndent)
|
||
ImGui.SetCursorPosX(DefaultParameterIndent);
|
||
|
||
ImGui.TextUnformatted(label.ToUpperInvariant());
|
||
AddVerticalSpace(1);
|
||
ImGui.PopFont();
|
||
ImGui.PopStyleColor();
|
||
}
|
||
|
||
public static bool BeginGroup(string label)
|
||
{
|
||
AddVerticalSpace(5);
|
||
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
|
||
// var isNotCollapsable = !label.EndsWith("...");
|
||
// if (isNotCollapsable)
|
||
// {
|
||
// ImGui.Text(label);
|
||
// ImGui.PopStyleColor();
|
||
// return true;
|
||
// }
|
||
|
||
// var id = ImGui.GetID(label);
|
||
// if (isNotCollapsable && !_openedGroups.Contains(id))
|
||
// {
|
||
// ImGui.SetNextItemOpen(true);
|
||
// _openedGroups.Add(id);
|
||
// }
|
||
|
||
var isOpen = ImGui.TreeNode(label);
|
||
ImGui.PopStyleColor();
|
||
if (isOpen)
|
||
ImGui.PushStyleVar(ImGuiStyleVar.IndentSpacing, 0);
|
||
|
||
return isOpen;
|
||
}
|
||
|
||
// private static HashSet<uint> _openedGroups = new();
|
||
|
||
public static void EndGroup()
|
||
{
|
||
ImGui.PopStyleVar();
|
||
ImGui.TreePop();
|
||
}
|
||
|
||
public static bool AddInt(string label,
|
||
ref int value,
|
||
int min = int.MinValue,
|
||
int max = int.MaxValue,
|
||
float scale = 1,
|
||
string? tooltip = null,
|
||
int defaultValue = NotADefaultValue)
|
||
{
|
||
var hasReset = defaultValue != NotADefaultValue;
|
||
var isDefault = hasReset && value == defaultValue;
|
||
|
||
// Fade the row while it holds its default value, so an unchanged field reads as inactive (matches AddFloat).
|
||
if (isDefault)
|
||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, DefaultFadeAlpha * ImGui.GetStyle().Alpha);
|
||
|
||
DrawInputLabel(label);
|
||
|
||
ImGui.PushID(label);
|
||
|
||
var size = GetAvailableInputSize(tooltip, hasReset, fillWidth: true, maxWidth: MaxNumberInputWidth);
|
||
var result = SingleValueEdit.Draw(ref value, size, min, max, true, true, scale);
|
||
ImGui.PopID();
|
||
|
||
AppendTooltip(tooltip);
|
||
if (AppendResetButton(hasReset && !isDefault, label))
|
||
{
|
||
value = defaultValue;
|
||
result |= InputEditStateFlags.ModifiedAndFinished;
|
||
}
|
||
|
||
if (isDefault)
|
||
ImGui.PopStyleVar();
|
||
|
||
var modified = (result & InputEditStateFlags.Modified) != InputEditStateFlags.Nothing;
|
||
return modified;
|
||
}
|
||
|
||
private const float DefaultFadeAlpha = 0.7f;
|
||
|
||
// Number fields fill the row on narrow panels but cap here so they don't stretch across a wide settings
|
||
// panel; the value-range indicator stays readable and float/int rows line up. (Unscaled px.)
|
||
private const float MaxNumberInputWidth = 280f;
|
||
|
||
public static bool AddFloat(string label,
|
||
ref float value,
|
||
float min = float.NegativeInfinity,
|
||
float max = float.PositiveInfinity,
|
||
float scale = 0.01f,
|
||
bool clampMin = false,
|
||
bool clampMax = false,
|
||
string? tooltip = null,
|
||
float defaultValue = float.NaN)
|
||
{
|
||
var hasReset = !float.IsNaN(defaultValue);
|
||
var isDefault = hasReset && Math.Abs(value - defaultValue) < 0.0001f;
|
||
if (isDefault)
|
||
{
|
||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, DefaultFadeAlpha * ImGui.GetStyle().Alpha);
|
||
}
|
||
|
||
DrawInputLabel(label);
|
||
|
||
var size = GetAvailableInputSize(tooltip, hasReset, fillWidth: true, maxWidth: MaxNumberInputWidth);
|
||
|
||
ImGui.PushID(label);
|
||
|
||
var result = SingleValueEdit.Draw(ref value, size, min, max, clampMin, clampMax, scale);
|
||
ImGui.PopID();
|
||
|
||
AppendTooltip(tooltip);
|
||
if (AppendResetButton(hasReset && !isDefault, label))
|
||
{
|
||
value = defaultValue;
|
||
result |= InputEditStateFlags.ModifiedAndFinished;
|
||
}
|
||
|
||
if (isDefault)
|
||
{
|
||
ImGui.PopStyleVar();
|
||
}
|
||
|
||
var modified = (result & InputEditStateFlags.Modified) != InputEditStateFlags.Nothing;
|
||
return modified;
|
||
}
|
||
|
||
/// <param name="itemLabel">Optional display-name formatter for each enum value (e.g. "HapQ" → "Hap Q").
|
||
/// When null, the raw enum member names are shown.</param>
|
||
/// <param name="itemSuffix">Optional secondary text drawn muted after each item's label (e.g. a size/time
|
||
/// estimate). When provided, the dropdown renders items itself so the suffix can be de-emphasised.</param>
|
||
public static bool AddEnumDropdown<T>(ref T selectedValue, string? label, string? tooltip = null,
|
||
T defaultValue = default, Func<T, string>? itemLabel = null, Func<T, string>? itemSuffix = null)
|
||
where T : struct, Enum, IConvertible, IFormattable
|
||
{
|
||
DrawInputLabel(label);
|
||
|
||
var inputSize = GetAvailableInputSize(tooltip, false, true);
|
||
ImGui.SetNextItemWidth(inputSize.X);
|
||
|
||
var modified = DrawEnumDropdown(ref selectedValue, label, defaultValue, itemLabel, itemSuffix);
|
||
|
||
AppendTooltip(tooltip);
|
||
|
||
return modified;
|
||
}
|
||
|
||
public static bool DrawEnumDropdown<T>(ref T selectedValue, string? label, T defaultValue = default,
|
||
Func<T, string>? itemLabel = null, Func<T, string>? itemSuffix = null)
|
||
where T : struct, Enum, IConvertible, IFormattable, IComparable
|
||
{
|
||
var values = Enum.GetValues<T>();
|
||
var names = Enum.GetNames<T>();
|
||
if (itemLabel != null)
|
||
{
|
||
for (var i = 0; i < values.Length; i++)
|
||
names[i] = itemLabel(values[i]);
|
||
}
|
||
|
||
var selectedIndex = 0;
|
||
for (var i = 0; i < values.Length; i++)
|
||
{
|
||
if (values[i].Equals(selectedValue))
|
||
{
|
||
selectedIndex = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
ImGui.PushStyleColor(ImGuiCol.FrameBg, UiColors.BackgroundButton.Rgba);
|
||
ImGui.PushStyleColor(ImGuiCol.Text,
|
||
selectedValue.Equals(defaultValue) ? UiColors.TextMuted.Rgba : UiColors.ForegroundFull.Rgba);
|
||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 5);
|
||
|
||
bool modified;
|
||
if (itemSuffix != null)
|
||
modified = DrawEnumComboWithMutedSuffix(ref selectedValue, label, values, names, selectedIndex, itemSuffix);
|
||
else
|
||
{
|
||
modified = ImGui.Combo($"##dropDown{typeof(T)}{label}", ref selectedIndex, names, names.Length, names.Length);
|
||
if (modified)
|
||
selectedValue = values[selectedIndex];
|
||
}
|
||
|
||
ImGui.PopStyleVar();
|
||
ImGui.PopStyleColor(2);
|
||
|
||
return modified;
|
||
}
|
||
|
||
// Custom combo so the per-item suffix (e.g. "→ ~95 MB, ~34s") can render in TextMuted while the label keeps the
|
||
// normal colour. The closed preview shows label + suffix in one colour (BeginCombo can't two-tone it); the open
|
||
// list two-tones each row via a full-width hidden Selectable with the label/suffix drawn over it.
|
||
private static bool DrawEnumComboWithMutedSuffix<T>(ref T selectedValue, string? label, T[] values, string[] names,
|
||
int selectedIndex, Func<T, string> itemSuffix)
|
||
where T : struct, Enum
|
||
{
|
||
var preview = names[selectedIndex] + itemSuffix(values[selectedIndex]);
|
||
var modified = false;
|
||
if (ImGui.BeginCombo($"##dropDown{typeof(T)}{label}", preview))
|
||
{
|
||
for (var i = 0; i < values.Length; i++)
|
||
{
|
||
var startX = ImGui.GetCursorPosX();
|
||
if (ImGui.Selectable($"##enumItem{i}", i == selectedIndex))
|
||
{
|
||
selectedValue = values[i];
|
||
modified = true;
|
||
}
|
||
|
||
ImGui.SameLine(startX);
|
||
ImGui.TextUnformatted(names[i]);
|
||
|
||
var suffix = itemSuffix(values[i]);
|
||
if (!string.IsNullOrEmpty(suffix))
|
||
{
|
||
ImGui.SameLine();
|
||
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
|
||
ImGui.TextUnformatted(suffix);
|
||
ImGui.PopStyleColor();
|
||
}
|
||
}
|
||
|
||
ImGui.EndCombo();
|
||
}
|
||
|
||
return modified;
|
||
}
|
||
|
||
|
||
public static bool AddDropdown(ref string selectedValue, IEnumerable<string?> values, string label,
|
||
string? tooltip = null)
|
||
{
|
||
DrawInputLabel(label);
|
||
var spaceForTooltip = 0f;
|
||
if (tooltip != null)
|
||
{
|
||
spaceForTooltip = 30f;
|
||
}
|
||
|
||
var inputSize = GetAvailableInputSize(null, false, true, spaceForTooltip);
|
||
ImGui.SetNextItemWidth(inputSize.X);
|
||
|
||
var modified = false;
|
||
if (ImGui.BeginCombo("##SelectTheme",
|
||
selectedValue,
|
||
ImGuiComboFlags.HeightLarge))
|
||
{
|
||
foreach (var value in values)
|
||
{
|
||
if (value == null)
|
||
continue;
|
||
|
||
var isSelected = value == selectedValue;
|
||
if (!ImGui.Selectable($"{value}",
|
||
isSelected,
|
||
ImGuiSelectableFlags.NoAutoClosePopups))
|
||
continue;
|
||
|
||
ImGui.CloseCurrentPopup();
|
||
selectedValue = value;
|
||
modified = true;
|
||
}
|
||
|
||
ImGui.EndCombo();
|
||
}
|
||
|
||
AppendTooltip(tooltip);
|
||
return modified;
|
||
}
|
||
|
||
public static bool AddDropdown<T>(ref T selectedValue,
|
||
IEnumerable<T> values,
|
||
string label,
|
||
Func<T, string> getDisplayTextFunc,
|
||
string? tooltip = null)
|
||
{
|
||
DrawInputLabel(label);
|
||
|
||
const string imguiLabelFmt = "##Select{0}{1}";
|
||
var imguiLabel = string.Format(imguiLabelFmt, label, nameof(T));
|
||
|
||
// const string defaultDisplayTextFmt = "Select {0}";
|
||
// defaultDisplayText ??= string.Format(defaultDisplayTextFmt, selectedValue);
|
||
|
||
var previewLabel = selectedValue == null ? "please select" : getDisplayTextFunc(selectedValue);
|
||
|
||
// Constrain to the standard input column (reserving room for the tooltip icon) like the other inputs —
|
||
// otherwise the combo spans the whole content region and overflows narrow panels.
|
||
var inputSize = GetAvailableInputSize(tooltip, false, true);
|
||
ImGui.SetNextItemWidth(inputSize.X);
|
||
|
||
var modified = false;
|
||
if (ImGui.BeginCombo(imguiLabel,
|
||
previewLabel,
|
||
ImGuiComboFlags.HeightLarge))
|
||
{
|
||
foreach (var value in values)
|
||
{
|
||
if (value == null)
|
||
continue;
|
||
|
||
var equalityComparer = EqualityComparer<T>.Default;
|
||
var isSelected = equalityComparer.Equals(value, selectedValue);
|
||
// if (!ImGui.Selectable($"{value}", isSelected, ImGuiSelectableFlags.NoAutoClosePopups))
|
||
// continue;
|
||
|
||
if (!ImGui.Selectable(getDisplayTextFunc(value), isSelected, ImGuiSelectableFlags.NoAutoClosePopups))
|
||
continue;
|
||
|
||
ImGui.CloseCurrentPopup();
|
||
selectedValue = value;
|
||
modified = true;
|
||
}
|
||
|
||
ImGui.EndCombo();
|
||
}
|
||
|
||
AppendTooltip(tooltip);
|
||
return modified;
|
||
}
|
||
|
||
private static readonly Dictionary<string, string> _listBoxAddInputs = new();
|
||
|
||
// Dragging state for reorder: persistent across frames while mouse button is down
|
||
private static int _draggingIndex = -1;
|
||
private static bool _isDragging;
|
||
|
||
public static bool AddEditableListBox(ref string selectedValue,
|
||
IList<string> values,
|
||
string label,
|
||
Predicate<string>? warningPredicate,
|
||
String? warningText,
|
||
string? tooltip = null)
|
||
{
|
||
DrawInputLabel(label);
|
||
|
||
var imguiLabel = "##ListBox" + label;
|
||
|
||
// compute input size early
|
||
var inputSize = GetAvailableInputSize(tooltip, false, true);
|
||
|
||
// Compute box height: account for all items plus the bottom transient add-row; add 1.5 lines total so the add-row is visible
|
||
var lineHeight = ImGui.GetTextLineHeightWithSpacing();
|
||
var listBoxHeight = MathF.Min(300, (values.Count + 2.5f) * lineHeight);
|
||
var size = inputSize with {Y = listBoxHeight};
|
||
|
||
var modified = false;
|
||
if (ImGui.BeginListBox(imguiLabel, size))
|
||
{
|
||
try
|
||
{
|
||
// Remember the list content start Y so we can compute item indices based on mouse Y while dragging
|
||
var listStartY = ImGui.GetCursorScreenPos().Y;
|
||
// Top: existing items
|
||
for (var i = 0; i < values.Count; i++)
|
||
{
|
||
ImGui.PushID(i);
|
||
ImGui.AlignTextToFramePadding();
|
||
|
||
// Drag handle
|
||
ImGui.Button($"{i}.");
|
||
// start drag if user presses the handle
|
||
if (ImGui.IsItemActive() && !_isDragging && ImGui.IsMouseDown(0))
|
||
{
|
||
_isDragging = true;
|
||
_draggingIndex = i;
|
||
}
|
||
|
||
// continue drag while mouse down
|
||
if (_isDragging && ImGui.IsMouseDown(0) && _draggingIndex >= 0)
|
||
{
|
||
var mouseY = ImGui.GetMousePos().Y;
|
||
var targetIndex = (int) ((mouseY - listStartY) / lineHeight);
|
||
targetIndex = Math.Clamp(targetIndex, 0, values.Count - 1);
|
||
if (targetIndex != _draggingIndex)
|
||
{
|
||
(values[targetIndex], values[_draggingIndex]) =
|
||
(values[_draggingIndex], values[targetIndex]);
|
||
modified = true;
|
||
_draggingIndex = targetIndex;
|
||
}
|
||
}
|
||
|
||
// stop dragging when mouse released
|
||
if (_isDragging && !ImGui.IsMouseDown(0))
|
||
{
|
||
_isDragging = false;
|
||
_draggingIndex = -1;
|
||
}
|
||
|
||
ImGui.SameLine(30 * T3Ui.UiScaleFactor);
|
||
|
||
var value = values[i];
|
||
|
||
// warning icon on the left
|
||
var hasWarning = warningPredicate != null && !warningPredicate(value);
|
||
if (hasWarning)
|
||
{
|
||
AddIcon(Icon.Warning);
|
||
if (ImGui.IsItemHovered())
|
||
{
|
||
ImGui.BeginTooltip();
|
||
ImGui.TextWrapped(warningText ?? "This entry may cause issues.");
|
||
ImGui.EndTooltip();
|
||
}
|
||
|
||
ImGui.SameLine();
|
||
}
|
||
|
||
// compute reserve space for two small buttons
|
||
var style = ImGui.GetStyle();
|
||
var padX = style.FramePadding.X * 2;
|
||
var wX = ImGui.CalcTextSize("×").X + padX;
|
||
var wPlus = ImGui.CalcTextSize("+").X + padX;
|
||
var buttonSpacing = style.ItemSpacing.X;
|
||
var reservedRight = wX + wPlus + buttonSpacing;
|
||
|
||
// set width for input so buttons fit on same line
|
||
var regionAvail = ImGui.GetContentRegionAvail().X;
|
||
ImGui.SetNextItemWidth(MathF.Max(20, regionAvail - reservedRight));
|
||
|
||
var temp = value;
|
||
if (ImGui.InputText("##item" + i + imguiLabel, ref temp, 1024u))
|
||
{
|
||
values[i] = temp;
|
||
modified = true;
|
||
if (selectedValue == value)
|
||
selectedValue = temp;
|
||
}
|
||
|
||
// draw delete/insert on same line, right after the input
|
||
ImGui.SameLine();
|
||
if (ImGui.Button("×"))
|
||
{
|
||
if (selectedValue == value)
|
||
selectedValue = string.Empty;
|
||
|
||
values.RemoveAt(i);
|
||
modified = true;
|
||
ImGui.PopID();
|
||
i--;
|
||
continue;
|
||
}
|
||
|
||
ImGui.SameLine();
|
||
if (ImGui.Button("+"))
|
||
{
|
||
values.Insert(i, value);
|
||
modified = true;
|
||
i++; // skip the inserted duplicate
|
||
}
|
||
|
||
ImGui.PopID();
|
||
}
|
||
|
||
// Bottom: add new item input
|
||
ImGui.Separator();
|
||
ImGui.PushID("add" + imguiLabel);
|
||
if (!_listBoxAddInputs.TryGetValue(imguiLabel, out var addInput))
|
||
addInput = string.Empty;
|
||
|
||
// Shift cursor horizontally to match the per-item input left offset (drag-handle width)
|
||
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 30 * T3Ui.UiScaleFactor);
|
||
|
||
// Compute reserved right width same as per-item (X + + spacing) so plus aligns exactly
|
||
var styleBottom = ImGui.GetStyle();
|
||
var padXBottom = styleBottom.FramePadding.X * 2;
|
||
var wXBottom = ImGui.CalcTextSize("×").X + padXBottom;
|
||
var wPlusBottom = ImGui.CalcTextSize("+").X + padXBottom;
|
||
var buttonSpacingBottom = styleBottom.ItemSpacing.X;
|
||
var reservedRightSame = wXBottom + wPlusBottom + buttonSpacingBottom;
|
||
|
||
var regionAvailBottom = ImGui.GetContentRegionAvail().X;
|
||
ImGui.SetNextItemWidth(MathF.Max(20, regionAvailBottom - reservedRightSame));
|
||
|
||
if (ImGui.InputText("##add" + imguiLabel, ref addInput, 1024u))
|
||
{
|
||
_listBoxAddInputs[imguiLabel] = addInput;
|
||
}
|
||
|
||
ImGui.SameLine();
|
||
if (ImGui.Button("+"))
|
||
{
|
||
var trimmed = (addInput ?? string.Empty).Trim();
|
||
if (!string.IsNullOrEmpty(trimmed))
|
||
{
|
||
values.Add(trimmed);
|
||
selectedValue = trimmed;
|
||
_listBoxAddInputs[imguiLabel] = string.Empty;
|
||
modified = true;
|
||
}
|
||
}
|
||
|
||
ImGui.PopID();
|
||
}
|
||
finally
|
||
{
|
||
ImGui.EndListBox();
|
||
}
|
||
}
|
||
|
||
AppendTooltip(tooltip);
|
||
return modified;
|
||
}
|
||
|
||
public static bool AddSegmentedButtonWithLabel<T>(ref T selectedValue, string label, float columnWidth = 0, string? tooltip = null)
|
||
where T : struct, Enum
|
||
{
|
||
DrawInputLabel(label);
|
||
var modified = SegmentedButton(ref selectedValue, columnWidth);
|
||
AppendTooltip(tooltip);
|
||
return modified;
|
||
}
|
||
|
||
public static bool SegmentedButton<T>(ref T selectedValue, float columnWidth = 0, Func<T, bool>? isItemDisabled = null)
|
||
where T : struct, Enum
|
||
{
|
||
if (columnWidth <= 0)
|
||
return SegmentedButtonPill(ref selectedValue, isItemDisabled);
|
||
|
||
// Fixed-width vertical mode (category selectors) keeps the simple per-button look.
|
||
var modified = false;
|
||
var selectedValueString = selectedValue.ToString();
|
||
foreach (var value in Enum.GetValues<T>())
|
||
{
|
||
var name = CustomComponents.HumanReadablePascalCase(Enum.GetName(value));
|
||
var isSelected = selectedValueString == value.ToString();
|
||
if (DrawSelectButton(name, isSelected, columnWidth))
|
||
{
|
||
modified = true;
|
||
selectedValue = value;
|
||
}
|
||
}
|
||
|
||
return modified;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Horizontal segmented control drawn as one rounded track with the active option floating inside it.
|
||
/// Custom draw-list rendering (rather than per-option ImGui.Button) so the track and the active pill
|
||
/// share corners and the active label can use a heavier font.
|
||
/// </summary>
|
||
private static bool SegmentedButtonPill<T>(ref T selectedValue, Func<T, bool>? isItemDisabled = null) where T : struct, Enum
|
||
{
|
||
var scale = T3Ui.UiScaleFactor;
|
||
var h = ImGui.GetFrameHeight();
|
||
var segPadding = 14 * scale;
|
||
var rounding = 4 * scale;
|
||
var inset = 2 * scale;
|
||
|
||
var values = Enum.GetValues<T>();
|
||
var selectedString = selectedValue.ToString();
|
||
var dl = ImGui.GetWindowDrawList();
|
||
var start = ImGui.GetCursorScreenPos();
|
||
|
||
var total = 0f;
|
||
foreach (var value in values)
|
||
{
|
||
var name = CustomComponents.HumanReadablePascalCase(Enum.GetName(value));
|
||
total += ImGui.CalcTextSize(name).X + segPadding * 2;
|
||
}
|
||
|
||
dl.AddRectFilled(start, start + new Vector2(total, h), UiColors.BackgroundInputField, rounding);
|
||
|
||
// Scope ids by enum type so two unrelated segmented controls in the same window don't collide.
|
||
ImGui.PushID(typeof(T).Name);
|
||
|
||
var modified = false;
|
||
var isFirst = true;
|
||
var x = start.X;
|
||
foreach (var value in values)
|
||
{
|
||
var name = CustomComponents.HumanReadablePascalCase(Enum.GetName(value));
|
||
var segWidth = ImGui.CalcTextSize(name).X + segPadding * 2;
|
||
var isSelected = selectedString == value.ToString();
|
||
var isDisabled = isItemDisabled?.Invoke(value) ?? false;
|
||
|
||
if (!isFirst)
|
||
ImGui.SameLine(0, 0);
|
||
|
||
// Disabled options still reserve their slot but ignore clicks and don't highlight on hover.
|
||
if (ImGui.InvisibleButton(name, new Vector2(segWidth, h)) && !isDisabled)
|
||
{
|
||
selectedValue = value;
|
||
modified = true;
|
||
}
|
||
|
||
var isHovered = !isDisabled && ImGui.IsItemHovered();
|
||
|
||
var segMin = new Vector2(x, start.Y);
|
||
if (isSelected)
|
||
{
|
||
dl.AddRectFilled(segMin + new Vector2(inset, inset),
|
||
segMin + new Vector2(segWidth - inset, h - inset),
|
||
UiColors.BackgroundButton, rounding);
|
||
}
|
||
else if (isHovered)
|
||
{
|
||
dl.AddRectFilled(segMin + new Vector2(inset, inset),
|
||
segMin + new Vector2(segWidth - inset, h - inset),
|
||
UiColors.BackgroundButton.Fade(0.4f), rounding);
|
||
}
|
||
|
||
var font = isSelected ? Fonts.FontBold : Fonts.FontNormal;
|
||
var textColor = isDisabled
|
||
? UiColors.TextMuted.Fade(0.4f)
|
||
: isSelected
|
||
? UiColors.ForegroundFull
|
||
: (isHovered ? UiColors.Text : UiColors.TextMuted);
|
||
ImGui.PushFont(font);
|
||
var textSize = ImGui.CalcTextSize(name);
|
||
var textPos = new Vector2(segMin.X + (segWidth - textSize.X) * 0.5f, segMin.Y + (h - textSize.Y) * 0.5f);
|
||
dl.AddText(textPos, ImGui.GetColorU32(textColor.Rgba), name);
|
||
ImGui.PopFont();
|
||
|
||
x += segWidth;
|
||
isFirst = false;
|
||
}
|
||
|
||
ImGui.PopID();
|
||
return modified;
|
||
}
|
||
|
||
private static bool DrawSelectButton(string name, bool isSelected, float width = 0)
|
||
{
|
||
ImGui.PushStyleColor(ImGuiCol.Button,
|
||
isSelected ? UiColors.BackgroundActive.Fade(0.7f).Rgba : UiColors.BackgroundButton.Rgba);
|
||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered,
|
||
isSelected ? UiColors.BackgroundActive.Rgba : UiColors.BackgroundButton.Fade(0.7f).Rgba);
|
||
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.Text.Rgba);
|
||
ImGui.PushStyleColor(ImGuiCol.ButtonActive,
|
||
isSelected ? UiColors.BackgroundActive.Fade(0.7f).Rgba : UiColors.BackgroundButton.Fade(0.7f).Rgba);
|
||
|
||
var clicked = ImGui.Button(name, new Vector2(width, 0));
|
||
ImGui.PopStyleColor(4);
|
||
return clicked;
|
||
}
|
||
|
||
private const string NoDefaultString = "_";
|
||
|
||
/// <summary>
|
||
/// Draws string input or file picker.
|
||
/// </summary>
|
||
public static bool AddStringInput(string label,
|
||
ref string value,
|
||
string? placeHolder = null,
|
||
string? warning = null,
|
||
string? tooltip = null,
|
||
string? defaultValue = NoDefaultString,
|
||
bool autoFocus = false)
|
||
{
|
||
if (string.IsNullOrEmpty(label))
|
||
{
|
||
Log.Error("AddStringInput() requires an id to work. Use ## prefix to hide.");
|
||
label = "##fallback";
|
||
}
|
||
|
||
var hasDefault = defaultValue != NoDefaultString;
|
||
var isDefault = hasDefault && value == defaultValue;
|
||
|
||
if (isDefault)
|
||
{
|
||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, DefaultFadeAlpha * ImGui.GetStyle().Alpha);
|
||
}
|
||
|
||
DrawInputLabel(label);
|
||
var wasNull = value == null!; // Support legacy calls
|
||
if (wasNull)
|
||
value = string.Empty;
|
||
|
||
var inputSize = GetAvailableInputSize(tooltip, false, true);
|
||
ImGui.SetNextItemWidth(inputSize.X);
|
||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 5);
|
||
|
||
|
||
var modified = ImGui.InputText("##" + label, ref value, 1000);
|
||
if (!modified && wasNull)
|
||
value = null!; // Support legacy calls
|
||
|
||
if (autoFocus)
|
||
{
|
||
// Todo - how the hell do you make this not select the entire text?
|
||
ImGui.SetKeyboardFocusHere(-1);
|
||
}
|
||
|
||
ImGui.PopStyleVar();
|
||
|
||
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(placeHolder))
|
||
{
|
||
var drawList = ImGui.GetWindowDrawList();
|
||
var minPos = ImGui.GetItemRectMin();
|
||
var maxPos = ImGui.GetItemRectMax();
|
||
drawList.PushClipRect(minPos, maxPos);
|
||
drawList.AddText(minPos + new Vector2(8, 3) * T3Ui.UiScaleFactor, UiColors.ForegroundFull.Fade(0.25f),
|
||
placeHolder);
|
||
drawList.PopClipRect();
|
||
}
|
||
|
||
AppendTooltip(tooltip);
|
||
if (isDefault)
|
||
{
|
||
ImGui.PopStyleVar();
|
||
}
|
||
|
||
if (AppendResetButton(hasDefault && !isDefault, label))
|
||
{
|
||
value = defaultValue ?? string.Empty;
|
||
modified = true;
|
||
}
|
||
|
||
DrawWarningBelowField(warning);
|
||
|
||
return modified;
|
||
}
|
||
|
||
public static bool AddStringInputWithSuggestions(string label,
|
||
ref string value,
|
||
IOrderedEnumerable<string> items,
|
||
string? placeHolder = null,
|
||
string? warning = null,
|
||
string? tooltip = null,
|
||
string? defaultValue = NoDefaultString,
|
||
bool autoFocus = false)
|
||
{
|
||
if (string.IsNullOrEmpty(label))
|
||
{
|
||
Log.Error("AddStringInput() requires an id to work. Use ## prefix to hide.");
|
||
label = "##fallback";
|
||
}
|
||
|
||
defaultValue ??= NoDefaultString;
|
||
|
||
var hasDefault = defaultValue != NoDefaultString;
|
||
var isDefault = hasDefault && value == defaultValue;
|
||
|
||
if (isDefault)
|
||
{
|
||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, DefaultFadeAlpha * ImGui.GetStyle().Alpha);
|
||
}
|
||
|
||
DrawInputLabel(label);
|
||
|
||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
||
var wasNull = value == null;
|
||
if (wasNull)
|
||
value = string.Empty;
|
||
|
||
var inputSize = GetAvailableInputSize(tooltip, false, true);
|
||
ImGui.SetNextItemWidth(inputSize.X);
|
||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 5);
|
||
|
||
var modified = InputWithTypeAheadSearch.Draw("##typeAheadSearch",
|
||
items,
|
||
!string.IsNullOrEmpty(warning),
|
||
ref value!, out _, out var _);
|
||
|
||
if (!modified && wasNull)
|
||
value = null!;
|
||
|
||
if (autoFocus)
|
||
{
|
||
// Todo - how the hell do you make this not select the entire text?
|
||
ImGui.SetKeyboardFocusHere(-1);
|
||
}
|
||
|
||
ImGui.PopStyleVar();
|
||
|
||
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(placeHolder))
|
||
{
|
||
var drawList = ImGui.GetWindowDrawList();
|
||
var minPos = ImGui.GetItemRectMin();
|
||
var maxPos = ImGui.GetItemRectMax();
|
||
drawList.PushClipRect(minPos, maxPos);
|
||
drawList.AddText(minPos + new Vector2(8, 3) * T3Ui.UiScaleFactor, UiColors.ForegroundFull.Fade(0.25f),
|
||
placeHolder);
|
||
drawList.PopClipRect();
|
||
}
|
||
|
||
AppendTooltip(tooltip);
|
||
if (isDefault)
|
||
{
|
||
ImGui.PopStyleVar();
|
||
}
|
||
|
||
if (AppendResetButton(hasDefault && !isDefault, label))
|
||
{
|
||
value = defaultValue;
|
||
modified = true;
|
||
}
|
||
|
||
DrawWarningBelowField(warning);
|
||
|
||
return modified;
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Draws string input or file picker.
|
||
/// </summary>
|
||
public static bool AddFilePicker(string label,
|
||
ref string? value,
|
||
string? placeHolder = null,
|
||
string? warning = null,
|
||
string? tooltip = null,
|
||
FileOperations.FilePickerTypes showFilePicker = FileOperations.FilePickerTypes.None)
|
||
{
|
||
DrawInputLabel(label);
|
||
|
||
var isFilePickerVisible = showFilePicker != FileOperations.FilePickerTypes.None;
|
||
float spaceForFilePicker = isFilePickerVisible ? 80 * T3Ui.UiScaleFactor : 0;
|
||
var inputSize = GetAvailableInputSize(null, false, true, spaceForFilePicker);
|
||
ImGui.SetNextItemWidth(inputSize.X);
|
||
|
||
var wasNull = value == null;
|
||
if (wasNull)
|
||
value = string.Empty;
|
||
|
||
var modified = ImGui.InputText("##" + label, ref value, 1000);
|
||
if (!modified && wasNull)
|
||
value = null;
|
||
|
||
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(placeHolder))
|
||
{
|
||
var drawList = ImGui.GetWindowDrawList();
|
||
var minPos = ImGui.GetItemRectMin();
|
||
var maxPos = ImGui.GetItemRectMax();
|
||
drawList.PushClipRect(minPos, maxPos);
|
||
drawList.AddText(minPos + new Vector2(8, 3), UiColors.ForegroundFull.Fade(0.25f), placeHolder);
|
||
drawList.PopClipRect();
|
||
}
|
||
|
||
if (isFilePickerVisible)
|
||
{
|
||
ImGui.PushID(label);
|
||
modified |= FileOperations.DrawFileSelector(showFilePicker, ref value);
|
||
ImGui.PopID();
|
||
}
|
||
|
||
AppendTooltip(tooltip);
|
||
|
||
|
||
DrawWarningBelowField(warning);
|
||
return modified;
|
||
}
|
||
|
||
public static bool AddCheckBox(string label,
|
||
ref bool value,
|
||
string? tooltip = null,
|
||
bool? defaultValue = null)
|
||
{
|
||
var hasDefault = defaultValue != null;
|
||
var isDefault = defaultValue != null && value == (bool) defaultValue;
|
||
|
||
if (isDefault)
|
||
{
|
||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, DefaultFadeAlpha * ImGui.GetStyle().Alpha);
|
||
}
|
||
|
||
ImGui.SetCursorPosX(MathF.Max(LeftParameterPadding, 0) + 20 * T3Ui.UiScaleFactor);
|
||
var modified = DrawCheckbox(label, ref value);
|
||
|
||
AppendTooltip(tooltip);
|
||
if (isDefault)
|
||
{
|
||
ImGui.PopStyleVar();
|
||
}
|
||
|
||
if (AppendResetButton(hasDefault && !isDefault, label))
|
||
{
|
||
value = defaultValue ?? false;
|
||
modified = true;
|
||
}
|
||
|
||
return modified;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Rounded checkbox with a white atlas checkmark when on — replaces ImGui.Checkbox so checkboxes
|
||
/// match the editor's look (rounded field, baked glyph) instead of the default tick.
|
||
/// </summary>
|
||
private static bool DrawCheckbox(string label, ref bool value)
|
||
{
|
||
var scale = T3Ui.UiScaleFactor;
|
||
var h = ImGui.GetFrameHeight();
|
||
var boxSize = MathF.Round(h * 0.8f);
|
||
|
||
var hasLabel = !string.IsNullOrEmpty(label) && !label.StartsWith("##");
|
||
var labelSize = hasLabel ? ImGui.CalcTextSize(label) : Vector2.Zero;
|
||
var spacing = hasLabel ? 8 * scale : 0;
|
||
|
||
var id = string.IsNullOrEmpty(label) ? "##checkbox" : label;
|
||
var clicked = ImGui.InvisibleButton(id, new Vector2(boxSize + spacing + labelSize.X, h));
|
||
if (clicked)
|
||
value = !value;
|
||
|
||
var min = ImGui.GetItemRectMin();
|
||
var isHovered = ImGui.IsItemHovered();
|
||
var dl = ImGui.GetWindowDrawList();
|
||
|
||
var boxMin = new Vector2(min.X, min.Y + (h - boxSize) * 0.5f);
|
||
var boxMax = boxMin + new Vector2(boxSize, boxSize);
|
||
var boxColor = isHovered ? UiColors.BackgroundInputFieldHover : UiColors.BackgroundInputField;
|
||
dl.AddRectFilled(boxMin, boxMax, boxColor, 4 * scale);
|
||
|
||
if (value)
|
||
{
|
||
var iconSize = boxSize * 0.9f;
|
||
var iconPos = boxMin + new Vector2((boxSize - iconSize) * 0.5f);
|
||
Icons.DrawIconAtScreenPosition(Icon.Checkmark, iconPos, new Vector2(iconSize), dl, UiColors.ForegroundFull);
|
||
}
|
||
|
||
if (hasLabel)
|
||
{
|
||
var textPos = new Vector2(boxMax.X + spacing, min.Y + (h - labelSize.Y) * 0.5f);
|
||
dl.AddText(textPos, ImGui.GetColorU32(UiColors.Text.Rgba), label);
|
||
}
|
||
|
||
return clicked;
|
||
}
|
||
|
||
public static void AddHint(string label)
|
||
{
|
||
if (string.IsNullOrEmpty(label))
|
||
return;
|
||
|
||
AddVerticalSpace(5);
|
||
ApplyIndent();
|
||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.5f * ImGui.GetStyle().Alpha);
|
||
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(10, 20));
|
||
|
||
AddIcon(Icon.Tip);
|
||
|
||
ImGui.SameLine();
|
||
ImGui.TextWrapped(label);
|
||
//ImGui.Indent(-13);
|
||
ImGui.PopStyleVar(2);
|
||
}
|
||
|
||
public static void AddVerticalSpace(float size = 10)
|
||
{
|
||
// This breaks ImGui.Indent for some reason so I commented it out and brought back the Dummy call
|
||
/*ImGui.SetCursorPos( new Vector2( ImGui.GetCursorStartPos().X,
|
||
ImGui.GetCursorPosY()
|
||
+size * T3Ui.UiScaleFactor));*/
|
||
ImGui.Dummy(new Vector2(1, size * T3Ui.UiScaleFactor));
|
||
}
|
||
|
||
#region layout helpers
|
||
|
||
public static void SetIndent(float newIndent)
|
||
{
|
||
_paramIndent = newIndent;
|
||
}
|
||
|
||
public static void Indent(float delta)
|
||
{
|
||
_paramIndent += delta;
|
||
}
|
||
|
||
public static void Unindent(float delta)
|
||
{
|
||
_paramIndent -= delta;
|
||
}
|
||
|
||
public static void SetIndentToLeft()
|
||
{
|
||
_paramIndent = 0;
|
||
}
|
||
|
||
public static void SetIndentToParameters()
|
||
{
|
||
_paramIndent = DefaultParameterIndent;
|
||
}
|
||
|
||
public static void ApplyIndent()
|
||
{
|
||
ImGui.SetCursorPosX(LeftParameterPadding + ParameterSpacing);
|
||
}
|
||
|
||
public static void SetWidth(float ratio)
|
||
{
|
||
_widthRatio = ratio;
|
||
}
|
||
|
||
public static void ResetWidth()
|
||
{
|
||
_widthRatio = 1;
|
||
}
|
||
|
||
public static void DrawInputLabel(string? label)
|
||
{
|
||
if (string.IsNullOrEmpty(label) || label.StartsWith("##"))
|
||
return;
|
||
|
||
var labelSize = ImGui.CalcTextSize(label);
|
||
var p = ImGui.GetCursorPos();
|
||
ImGui.SetCursorPosX(MathF.Max(LeftParameterPadding - labelSize.X, 0) + 10);
|
||
ImGui.AlignTextToFramePadding();
|
||
|
||
ImGui.TextUnformatted(label);
|
||
ImGui.SetCursorPos(p);
|
||
|
||
ImGui.SameLine();
|
||
SetCursorToParameterEdit();
|
||
}
|
||
|
||
public static void SetCursorToParameterEdit()
|
||
{
|
||
ImGui.SetCursorPosX(LeftParameterPadding + ParameterSpacing);
|
||
}
|
||
|
||
public static bool DrawValueRangeControl(ref float min, ref float max, ref float scale, ref bool clampedMin,
|
||
ref bool clampedMax, float defaultMin,
|
||
float defaultMax, float defaultScale)
|
||
{
|
||
var modified = false;
|
||
var flexWidth = ComputeFlexWidth(2, 3);
|
||
if (CustomComponents.RoundedIconButton("clampMin",
|
||
clampedMin ? Icon.ClampMinOn : Icon.ClampMinOff, 0,
|
||
ImDrawFlags.RoundCornersLeft,
|
||
clampedMin
|
||
? CustomComponents.ButtonStates.NeedsAttention
|
||
: CustomComponents.ButtonStates.Default))
|
||
{
|
||
modified = true;
|
||
clampedMin = !clampedMin;
|
||
}
|
||
|
||
modified |= SimpleFloatEdit(1, ref min, defaultMin, flexWidth);
|
||
modified |= SimpleFloatEdit(2, ref scale, defaultScale, flexWidth);
|
||
modified |= SimpleFloatEdit(3, ref max, defaultMax, flexWidth);
|
||
|
||
ImGui.SameLine();
|
||
|
||
if (CustomComponents.RoundedIconButton("clampMax",
|
||
clampedMax ? Icon.ClampMaxOn : Icon.ClampMaxOff, 0,
|
||
ImDrawFlags.RoundCornersRight,
|
||
clampedMax
|
||
? CustomComponents.ButtonStates.NeedsAttention
|
||
: CustomComponents.ButtonStates.Default))
|
||
{
|
||
modified = true;
|
||
clampedMax = !clampedMax;
|
||
}
|
||
|
||
return modified;
|
||
}
|
||
|
||
// TODO: This could become obsolete if SingleValueEdit would handle fading on default
|
||
private static bool SimpleFloatEdit(int id, ref float max, float defaultValue, float flexWidth)
|
||
{
|
||
var modified = false;
|
||
ImGui.SameLine();
|
||
ImGui.PushID(id);
|
||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, Math.Abs(max - defaultValue) < 0.0001f ? 0.5f : 1.0f);
|
||
if (SingleValueEdit.Draw(ref max,
|
||
new Vector2(flexWidth, ImGui.GetFrameHeight()),
|
||
format: "{0:G7}",
|
||
defaultValue: defaultValue, horizontalAlign: 0.5f)
|
||
.HasFlag(InputEditStateFlags.Modified))
|
||
{
|
||
modified = true;
|
||
}
|
||
|
||
ImGui.PopStyleVar();
|
||
ImGui.PopID();
|
||
return modified;
|
||
}
|
||
|
||
public static bool DrawIntValueRangeControl(ref int min, ref int max, ref float scale, ref bool clampedMin,
|
||
ref bool clampedMax)
|
||
{
|
||
var modified = false;
|
||
var flexWidth = ComputeFlexWidth(2, 3);
|
||
if (CustomComponents.RoundedIconButton("clampMin",
|
||
clampedMin ? Icon.ClampMinOn : Icon.ClampMinOff, 0,
|
||
ImDrawFlags.RoundCornersLeft,
|
||
clampedMin
|
||
? CustomComponents.ButtonStates.NeedsAttention
|
||
: CustomComponents.ButtonStates.Default))
|
||
{
|
||
modified = true;
|
||
clampedMin = !clampedMin;
|
||
}
|
||
|
||
modified |= SimpleIntEdit(1, ref min, int.MinValue, flexWidth);
|
||
modified |= SimpleFloatEdit(2, ref scale, 0, flexWidth);
|
||
modified |= SimpleIntEdit(3, ref max, int.MaxValue, flexWidth);
|
||
|
||
ImGui.SameLine();
|
||
|
||
if (CustomComponents.RoundedIconButton("clampMax",
|
||
clampedMax ? Icon.ClampMaxOn : Icon.ClampMaxOff, 0,
|
||
ImDrawFlags.RoundCornersRight,
|
||
clampedMax
|
||
? CustomComponents.ButtonStates.NeedsAttention
|
||
: CustomComponents.ButtonStates.Default))
|
||
{
|
||
modified = true;
|
||
clampedMax = !clampedMax;
|
||
}
|
||
|
||
return modified;
|
||
}
|
||
|
||
// TODO: This could become obsolete if SingleValueEdit would handle fading on default
|
||
private static bool SimpleIntEdit(int id, ref int value, int defaultValue, float flexWidth)
|
||
{
|
||
var modified = false;
|
||
ImGui.SameLine();
|
||
ImGui.PushID(id);
|
||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, value == defaultValue ? 0.5f : 1.0f);
|
||
if (SingleValueEdit.Draw(ref value,
|
||
new Vector2(flexWidth, ImGui.GetFrameHeight()), defaultValue: defaultValue, horizontalAlign: 0.5f)
|
||
.HasFlag(InputEditStateFlags.Modified))
|
||
{
|
||
modified = true;
|
||
}
|
||
|
||
ImGui.PopStyleVar();
|
||
ImGui.PopID();
|
||
return modified;
|
||
}
|
||
|
||
|
||
/**
|
||
* Computes the fill width for input group segments
|
||
*/
|
||
private static float ComputeFlexWidth(int fixedWidthItemCount, int flexItemCount)
|
||
{
|
||
var totalWidth = ImGui.GetContentRegionAvail().X;
|
||
var height = ImGui.GetFrameHeight();
|
||
return (totalWidth - fixedWidthItemCount * height) / flexItemCount;
|
||
}
|
||
|
||
private static void DrawWarningBelowField(string? warning)
|
||
{
|
||
if (string.IsNullOrEmpty(warning))
|
||
return;
|
||
|
||
ImGui.SetCursorPosX(MathF.Max(LeftParameterPadding, 0) + 20 * T3Ui.UiScaleFactor);
|
||
ImGui.PushFont(Fonts.FontSmall);
|
||
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.StatusError.Rgba);
|
||
ImGui.TextUnformatted(warning);
|
||
ImGui.PopStyleColor();
|
||
ImGui.PopFont();
|
||
AddVerticalSpace(4);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region internal helpers
|
||
|
||
/// <param name="maxWidth">When > 0, caps the requested width (in unscaled px, scaled internally). Used by
|
||
/// number fields so they fill the row on narrow panels but don't stretch across very wide ones. Dropdowns
|
||
/// and string inputs leave this at 0 to keep filling the full width.</param>
|
||
public static Vector2 GetAvailableInputSize(string? tooltip, bool hasReset, bool fillWidth = false,
|
||
float rightPadding = 0, float maxWidth = 0)
|
||
{
|
||
var toolWidth = 20f * T3Ui.UiScaleFactor;
|
||
var sizeForResetToDefault = hasReset ? toolWidth : 0;
|
||
var sizeForTooltip = !string.IsNullOrEmpty(tooltip) ? toolWidth : 0;
|
||
|
||
var requestedWidth = fillWidth ? ImGui.GetContentRegionAvail().X * _widthRatio : 200;
|
||
if (maxWidth > 0)
|
||
requestedWidth = MathF.Min(requestedWidth, maxWidth * T3Ui.UiScaleFactor);
|
||
var availableWidth = MathF.Min(requestedWidth, ImGui.GetContentRegionAvail().X + 20);
|
||
|
||
var vector2 = new Vector2(availableWidth
|
||
- 20
|
||
- rightPadding
|
||
- sizeForResetToDefault
|
||
- sizeForTooltip,
|
||
ImGui.GetFrameHeight());
|
||
return vector2;
|
||
}
|
||
|
||
/// <summary>Draws the small help-marker icon on the current line and shows <paramref name="tooltip"/> when
|
||
/// hovered. Used by the input rows here, and reusable for ad-hoc info markers next to custom labels.</summary>
|
||
internal static void AppendTooltip(string? tooltip)
|
||
{
|
||
if (string.IsNullOrEmpty(tooltip))
|
||
return;
|
||
|
||
// A small fixed gap to the input — the default item spacing reads as detached.
|
||
ImGui.SameLine(0, 3 * T3Ui.UiScaleFactor);
|
||
|
||
ImGui.PushFont(Icons.IconFont);
|
||
ImGui.AlignTextToFramePadding();
|
||
ImGui.TextUnformatted("" + (char) Icon.Help);
|
||
ImGui.PopFont();
|
||
|
||
if (!ImGui.IsItemHovered())
|
||
return;
|
||
|
||
// Tooltip — rendered as Markdown so parameter docs can use **bold**, "- " lists and [OpRef] links.
|
||
// HardLineBreaks keeps plain multi-line tooltips (single "\n") looking as before. The finite max height
|
||
// lets the auto-resizing window fit its content without a spurious scrollbar.
|
||
var scale = T3Ui.UiScaleFactor;
|
||
var width = 280 * scale;
|
||
var maxHeight = ImGui.GetMainViewport().WorkSize.Y - 40 * scale;
|
||
ImGui.SetNextWindowSizeConstraints(new Vector2(width, 0), new Vector2(width, maxHeight));
|
||
ImGui.PushStyleColor(ImGuiCol.PopupBg, UiColors.BackgroundFull.Rgba);
|
||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha);
|
||
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(10, 10) * scale);
|
||
ImGui.BeginTooltip();
|
||
|
||
// Wrap to a fixed inner width rather than the live content region. The window is auto-resizing,
|
||
// so on its first frame ImGui briefly shows a vertical scrollbar before it knows the content
|
||
// height; that scrollbar steals width, which would reflow the text taller and make the scrollbar
|
||
// stick. A stable wrap width (inner width minus padding and a scrollbar's worth of slack) keeps
|
||
// the layout from reflowing, so the spurious bar disappears on the next frame.
|
||
var innerWrap = width - 2 * 10 * scale - ImGui.GetStyle().ScrollbarSize;
|
||
_tooltipMarkdown.Draw(tooltip, wrapWidthPx: innerWrap);
|
||
ImGui.EndTooltip();
|
||
ImGui.PopStyleVar(2);
|
||
ImGui.PopStyleColor();
|
||
}
|
||
|
||
private static readonly MarkdownView _tooltipMarkdown = new(new MarkdownView.Options
|
||
{
|
||
MutedBodyText = true,
|
||
HardLineBreaks = true,
|
||
});
|
||
|
||
private static bool AppendResetButton(bool hasReset, string? id)
|
||
{
|
||
if (!hasReset)
|
||
return false;
|
||
|
||
ImGui.SameLine();
|
||
ImGui.PushID(id ?? "fallback");
|
||
var clicked = CustomComponents.IconButton(Icon.Revert,
|
||
new Vector2(Math.Min(.8f, T3Ui.UiScaleFactor)) * ImGui.GetFrameHeight());
|
||
ImGui.PopID();
|
||
return clicked;
|
||
}
|
||
|
||
private static void AddIcon(Icon icon)
|
||
{
|
||
ImGui.PushFont(Icons.IconFont);
|
||
ImGui.PushStyleVar(ImGuiStyleVar.ButtonTextAlign, new Vector2(0.5f, 0.5f));
|
||
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
|
||
|
||
ImGui.TextUnformatted("" + (char) (icon));
|
||
|
||
ImGui.PopFont();
|
||
ImGui.PopStyleVar(2);
|
||
}
|
||
|
||
#endregion
|
||
|
||
private const int NotADefaultValue = Int32.MinValue;
|
||
|
||
private const float DefaultParameterIndent = 150;
|
||
private static float _paramIndent = DefaultParameterIndent;
|
||
private static float _widthRatio = 1;
|
||
private static float LeftParameterPadding => _paramIndent * T3Ui.UiScaleFactor;
|
||
public static float ParameterSpacing => 20 * T3Ui.UiScaleFactor;
|
||
} |