Files
tooll3--t3/Editor/Gui/Styling/CustomComponents.Menus.cs
2026-07-13 13:13:17 +08:00

291 lines
13 KiB
C#

using ImGuiNET;
using T3.Core.DataTypes.Vector;
using T3.Core.Utils;
using T3.Editor.Gui.Input;
using T3.Editor.Gui.UiHelpers;
namespace T3.Editor.Gui.Styling;
internal static partial class CustomComponents
{
private static Action _cachedDrawMenuItems;
/// <summary>Screen position of the right-click that opened the most recent scroll-canvas context menu.</summary>
internal static Vector2 ScreenPosOnOpeningContextMenu { get; private set; }
public static void ContextMenuForItem(Action drawMenuItems, string title = null, string id = "context_menu",
ImGuiPopupFlags flags = ImGuiPopupFlags.MouseButtonRight)
{
// prevent the context menu from opening when dragging
{
var wasDraggingRight = ImGui.GetMouseDragDelta(ImGuiMouseButton.Right).Length() > UserSettings.Config.ClickThreshold;
if (wasDraggingRight)
return;
}
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(6, 6));
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(6, 6));
ImGui.PushStyleVar(ImGuiStyleVar.PopupRounding, 6);
if (ImGui.BeginPopupContextItem(id, flags))
{
FrameStats.Current.IsItemContextMenuOpen = true;
if (title != null)
{
ImGui.PushFont(Fonts.FontSmall);
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.Gray.Rgba);
ImGui.TextUnformatted(title);
ImGui.PopStyleColor();
ImGui.PopFont();
}
// Assign to static field to avoid closure allocations
_cachedDrawMenuItems = drawMenuItems;
_cachedDrawMenuItems.Invoke();
ImGui.EndPopup();
}
ImGui.PopStyleVar(3);
}
/// <summary>
/// Small muted label to group menu items into sections, aligned with the icon column.
/// </summary>
public static void DrawMenuGroupLabel(string label)
{
FormInputs.AddVerticalSpace(2);
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().ItemSpacing.X + Icons.FontSize * 1.4f);
ImGui.PushFont(Fonts.FontSmall);
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
ImGui.TextUnformatted(label);
ImGui.PopStyleColor();
ImGui.PopFont();
}
public static bool DrawMenuItem(int id, string label, ref bool isChecked, string keyboardShortCut = null, bool reserveIconColumn = true)
{
var clicked = DrawMenuItem(id, label, keyboardShortCut, isChecked, reserveIconColumn: reserveIconColumn);
if (clicked)
{
isChecked = !isChecked;
}
return clicked;
}
public static bool DrawMenuItem(int id, string label, string keyboardShortCut = null, bool isChecked = false, bool isEnabled = true,
bool reserveIconColumn = true)
{
return DrawMenuItem(id, Icon.None, label, keyboardShortCut, isChecked, isEnabled, reserveIconColumn: reserveIconColumn);
}
/// <summary>
/// Menu item with the checkbox slot on the left, an optional icon, label and an optional
/// right-aligned keyboard shortcut. <paramref name="state"/> controls the label brightness:
/// <see cref="ButtonStates.Emphasized"/> reads as the primary/active row, <see cref="ButtonStates.Default"/>
/// as a muted secondary row. A disabled item always renders greyed regardless of <paramref name="state"/>.
/// </summary>
public static bool DrawMenuItem(int id, Icon icon, string label, string keyboardShortCut = null, bool isChecked = false, bool isEnabled = true,
bool reserveCheckmarkColumn = true, bool reserveIconColumn = true,
ButtonStates state = ButtonStates.Default)
{
var h = ImGui.GetFrameHeight();
var imguiPadding = ImGui.GetStyle().ItemSpacing;
var shortCutWidth = string.IsNullOrEmpty(keyboardShortCut) ? 0 : ImGui.CalcTextSize(keyboardShortCut).X;
var labelWidth = ImGui.CalcTextSize(label).X;
var paddingFactor = 1.4f;
var iconSlotWidth = Icons.FontSize * paddingFactor;
// Each column is reserved only when the menu uses it: the checkmark column for menus with
// toggles, the icon column for menus with icons. A menu without either sits flush left.
var checkmarkSlot = reserveCheckmarkColumn ? iconSlotWidth : 0f;
var leftPaddingIcon = imguiPadding.X + checkmarkSlot;
var hasIcon = icon != Icon.None;
// Reserve the icon column so icon-less items (e.g. Rename) stay aligned with icon ones.
var leftPaddingText = leftPaddingIcon + (reserveIconColumn ? iconSlotWidth : 0f);
var width = leftPaddingText + labelWidth + imguiPadding.X * 2;
if (shortCutWidth > 0)
{
width += shortCutWidth + h;
}
var windowWidth = ImGui.GetColumnWidth();
//var windowWidth = ImGui.GetWindowWidth();
if (width < windowWidth)
width = windowWidth;
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
ImGui.PushID(id);
// The label seeds the imgui id too: int ids alone can collapse to 0 after hot reload
// (initializers of newly added static fields don't run), which made all items conflict.
var clicked = ImGui.InvisibleButton(label, new Vector2(width, h)) && isEnabled;
ImGui.PopID();
ImGui.PopStyleVar();
var min = ImGui.GetItemRectMin();
var max = ImGui.GetItemRectMax();
var drawList = ImGui.GetWindowDrawList();
if (isEnabled && ImGui.IsItemHovered())
{
drawList.AddRectFilled(min, max, UiColors.BackgroundActive.Fade(0.25f), 4);
}
var textColor = GetMenuTextColor(isEnabled, state);
if (isChecked)
{
Icons.DrawIconAtScreenPosition(Icon.Checkmark,
(min + new Vector2(imguiPadding.X,
h / 2 - Icons.FontSize / 2)).Floor(),
drawList, textColor);
}
if (hasIcon)
{
// Emphasized when actionable, Default (faded) when disabled.
var iconColor = GetStateColor(isEnabled ? ButtonStates.Emphasized : ButtonStates.Default);
Icons.DrawIconAtScreenPosition(icon,
(min + new Vector2(leftPaddingIcon,
h / 2 - Icons.FontSize / 2)).Floor(),
drawList, iconColor);
}
var textHeight = ImGui.GetFontSize();
drawList.AddText(min + new Vector2(leftPaddingText,
h / 2 - textHeight / 2),
textColor,
label);
if (!string.IsNullOrEmpty(keyboardShortCut))
{
drawList.AddText(min
+ new Vector2(windowWidth - shortCutWidth,
h / 2 - textHeight / 2),
UiColors.TextMuted.Fade(isEnabled ? 0.5f : 0.2f),
keyboardShortCut);
}
if (clicked)
{
ImGui.CloseCurrentPopup();
}
return clicked;
}
/// <summary>
/// A submenu header styled to match <see cref="DrawMenuItem"/>: it reserves the same left padding and
/// draws a right chevron instead of ImGui's default triangle. Returns true while the submenu is open —
/// pair it with <see cref="ImGui.EndMenu"/> exactly like <see cref="ImGui.BeginMenu(string)"/>.
/// </summary>
public static bool DrawSubMenu(int id, string label, bool isEnabled = true, bool reserveCheckmarkColumn = true)
{
var iconSlotWidth = Icons.FontSize * 1.4f;
var imguiPadding = ImGui.GetStyle().ItemSpacing;
// Match DrawMenuItem's label start (checkmark column reserved, no icon column) so headers align with rows.
var leftPaddingText = imguiPadding.X + (reserveCheckmarkColumn ? iconSlotWidth : 0f);
// ImGui's menu selectable bleeds into the window padding, so GetItemRectMin sits a few px left of a
// normal row. Anchor instead to the row's start cursor — the same reference DrawMenuItem uses — so
// headers line up with rows exactly. Capture before BeginMenu, which switches to the child window when open.
var rowStart = ImGui.GetCursorScreenPos();
var columnWidth = ImGui.GetColumnWidth();
var parentDrawList = ImGui.GetWindowDrawList();
var h = ImGui.GetFrameHeight();
// Hide ImGui's own label, arrow and selectable highlight; we overdraw to match DrawMenuItem.
// No PushID here: when the submenu opens, BeginMenu switches to the child window before we could
// pop, so a PushID/PopID pair would straddle two per-window id stacks ("Missing PopID()"). The
// label (plus ##id for uniqueness) is the menu's id; the style-color stack is global, so its pops are safe.
var transparent = UiColors.ForegroundFull.Fade(0f).Rgba;
ImGui.PushStyleColor(ImGuiCol.Text, transparent);
ImGui.PushStyleColor(ImGuiCol.Header, transparent);
ImGui.PushStyleColor(ImGuiCol.HeaderHovered, transparent);
ImGui.PushStyleColor(ImGuiCol.HeaderActive, transparent);
var isOpen = ImGui.BeginMenu($"{label}##{id}", isEnabled);
ImGui.PopStyleColor(4);
if (isEnabled && (isOpen || ImGui.IsItemHovered()))
{
parentDrawList.AddRectFilled(rowStart, rowStart + new Vector2(columnWidth, h), UiColors.BackgroundActive.Fade(0.25f), 4);
}
var textColor = GetMenuTextColor(isEnabled, ButtonStates.Default);
var textHeight = ImGui.GetFontSize();
parentDrawList.AddText(rowStart + new Vector2(leftPaddingText, h / 2 - textHeight / 2), textColor, label);
Icons.DrawIconAtScreenPosition(Icon.ChevronRight,
(rowStart + new Vector2(columnWidth - iconSlotWidth, h / 2 - Icons.FontSize / 2)).Floor(),
parentDrawList, textColor);
return isOpen;
}
// Menu-row label color. Distinct from GetStateColor (tool-icon tints) so the menu look can evolve on its own.
private static Color GetMenuTextColor(bool isEnabled, ButtonStates state)
{
if (!isEnabled)
return UiColors.Text.Fade(0.15f);
return state == ButtonStates.Emphasized
? UiColors.ForegroundFull
: UiColors.Text.Fade(0.7f);
}
public static void DrawContextMenuForScrollCanvas(Action drawMenuContent, ref bool contextMenuIsOpen)
{
if (!contextMenuIsOpen)
{
if (FrameStats.Current.IsItemContextMenuOpen)
return;
var wasDraggingRight = ImGui.GetMouseDragDelta(ImGuiMouseButton.Right).Length() > UserSettings.Config.ClickThreshold;
if (wasDraggingRight)
return;
if (!ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByPopup))
return;
}
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(6, 6));
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(6, 6));
ImGui.PushStyleVar(ImGuiStyleVar.PopupRounding, 6);
if (ImGui.BeginPopupContextWindow("windows_context_menu"))
{
ScreenPosOnOpeningContextMenu = ImGui.GetMousePosOnOpeningCurrentPopup();
contextMenuIsOpen = true;
// Assign to static field to avoid closure allocations
_cachedDrawMenuItems = drawMenuContent;
_cachedDrawMenuItems.Invoke();
//drawMenuContent.Invoke();
ImGui.EndPopup();
}
else
{
contextMenuIsOpen = false;
}
ImGui.PopStyleVar(3);
}
public static bool DrawMultilineTextEdit(ref string value)
{
var lineCount = value.LineCount().Clamp(1, 30) + 1;
var lineHeight = Fonts.Code.FontSize;
ImGui.PushStyleColor(ImGuiCol.FrameBg, UiColors.BackgroundInputField.Rgba);
var requestedContentHeight = lineCount * lineHeight;
var clampedToWindowHeight = MathF.Min(requestedContentHeight, ImGui.GetWindowSize().Y * 0.5f);
var changed = ImGui.InputTextMultiline("##textEdit", ref value, 16384, new Vector2(-10, clampedToWindowHeight));
ImGui.PopStyleColor();
FormInputs.AddVerticalSpace(3);
return changed;
}
}