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

1094 lines
42 KiB
C#

#nullable enable
using ImGuiNET;
using T3.Core.DataTypes.Vector;
using T3.Core.Operator;
using T3.Core.SystemUi;
using T3.Core.Utils;
using T3.Editor.Gui.Dialogs;
using T3.Editor.Gui.Help;
using T3.Editor.Gui.Legacy.Interaction.Connections;
using T3.Editor.Gui.Input;
using T3.Editor.Gui.Styling;
using T3.Editor.Gui.UiHelpers;
using T3.Editor.Gui.UiHelpers.Thumbnails;
using T3.Editor.Gui.Windows.AssetLib;
using T3.Editor.UiModel;
using T3.Editor.UiModel.Helpers;
using T3.Editor.UiModel.InputsAndTypes;
using T3.Editor.UiModel.ProjectHandling;
namespace T3.Editor.Gui.Windows.SymbolLib;
/// <summary>
/// The <c>SymbolLibrary</c> window displays a hierarchical tree of all defined symbols, organized by namespace.
/// It provides search, filtering, and management features for symbols, including drag-and-drop, renaming namespaces,
/// deleting symbols, and visual feedback for selection and usage dependencies.
/// </summary>
/// <remarks>
/// This class is the main UI for browsing, searching, and managing operator symbols in the editor.
/// It supports advanced features such as dependency scanning, random prompt suggestions, and context menus for symbol actions.
/// </remarks>
[HelpUiID("SymbolLibrary")]
internal sealed class SymbolLibrary : Window
{
/// <summary>
/// Initializes a new instance of the <see cref="SymbolLibrary"/> window.
/// Sets up symbol filtering, random prompt generation, and filtering UI logic.
/// Also populates the symbol tree with all available symbols.
/// </summary>
internal SymbolLibrary()
{
_filter.SearchString = "";
_randomPromptGenerator = new RandomPromptGenerator(_filter);
_libraryFiltering = new LibraryFiltering(this);
Config.Title = "Symbol Library";
_treeNode.PopulateCompleteTree();
}
/// <summary>
/// Opens the window, expands the namespace tree to <paramref name="symbolId"/>, and scrolls it into
/// view (without touching the search filter). Used by operator links in markdown.
/// </summary>
internal void Reveal(Guid symbolId)
{
_expandToSymbolTargetId = symbolId;
_expandToSymbolTriggered = true;
_scrollToSymbolId = symbolId;
// The highlight normally mirrors the graph selection. Hold the revealed symbol
// highlighted until that selection changes, so reveals from documentation links
// (which don't select anything) stay visible.
_revealHighlightActive = true;
_graphSelectionAtReveal = TryGetSingleSelectedSymbolId();
Config.Visible = true;
}
/// <summary>
/// Draws the main content of the Symbol Library window, including dialogs and the symbol tree or usage view.
/// </summary>
protected override void DrawContent()
{
// Update highlight/aim icon state for selected symbol
UpdateSelectedSymbolHighlight();
// Show rename namespace dialog if needed
if (_subtreeNodeToRename != null)
_renameNamespaceDialog.Draw(_subtreeNodeToRename);
// Show delete symbol dialog if needed
if (_symbolToDelete != null)
_deleteSymbolDialog.Draw(_symbolToDelete);
ImGui.PushStyleVar(ImGuiStyleVar.IndentSpacing, 10);
// Show usages view if a symbol's usage is being inspected
if (_symbolUsageReferenceFilter != null)
{
DrawUsagesAReferencedSymbol();
}
else
{
DrawView();
}
ImGui.PopStyleVar(1);
}
// Indicates if a refresh of the symbol library is needed
//private static bool _refreshTriggered;
private static readonly TreeHandler _treeHandler = new();
/// <summary>
/// Draws the main symbol library view, including search bar, filters, and the result tree.
/// </summary>
private void DrawView()
{
_treeHandler.Update();
var iconCount = 2;
if (_wasScanned)
iconCount++;
// Draw search input field
CustomComponents.DrawInputFieldWithPlaceholder(
"Search symbols...",
ref _filter.SearchString,
-ImGui.GetFrameHeight() * iconCount + 16);
// Collapse icon
{
ImGui.SameLine();
var collapseIconState = _treeHandler.NoFolderOpen
? CustomComponents.ButtonStates.Default
: CustomComponents.ButtonStates.Emphasized;
if (CustomComponents.IconButton(Icon.TreeCollapse, Vector2.Zero, collapseIconState))
{
_treeHandler.CollapseAll();
}
}
ImGui.SameLine();
// Draw refresh button and handle refresh logic
if (CustomComponents.IconButton(Icon.Refresh, Vector2.Zero, CustomComponents.ButtonStates.Default) || NeedsRebuild)
{
UpdateSymbolLibraryState();
}
CustomComponents.TooltipForLastItem(
"Scan usage dependencies for symbols",
"This can be useful for cleaning up operator name spaces.");
// Draw filter toggles if scan was performed
if (_wasScanned)
{
_libraryFiltering.DrawSymbolFilters();
}
var textColor = UiColors.Text.Fade(0.8f);
ImGui.PushStyleColor(ImGuiCol.Text, textColor.Rgba);
ImGui.BeginChild("scrolling", Vector2.Zero, ImGuiChildFlags.None, ImGuiWindowFlags.NoBackground);
{
// Show filtered or full tree depending on filter/search state
if (_libraryFiltering.AnyFilterActive)
{
DrawNode(FilteredTree);
}
else if (string.IsNullOrEmpty(_filter.SearchString))
{
DrawNode(_treeNode);
}
else if (_filter.SearchString.Contains('?'))
{
_randomPromptGenerator.DrawRandomPromptList();
}
else
{
DrawFilteredList();
}
}
ImGui.PopStyleColor();
CustomComponents.HandleDragScrolling(this);
ImGui.EndChild();
}
/// <summary>
/// Updates the symbol library state by repopulating the tree and updating analysis details.
/// </summary>
private void UpdateSymbolLibraryState()
{
_treeHandler.Reset();
_treeNode.PopulateCompleteTree();
ExampleSymbolLinking.UpdateExampleLinks();
SymbolAnalysis.UpdateDetails();
_wasScanned = true;
_lastUpdateVersion = EditorSymbolPackage.SymbolStructureVersionCounter;
}
private bool NeedsRebuild => _lastUpdateVersion < EditorSymbolPackage.SymbolStructureVersionCounter;
private int _lastUpdateVersion;
/// <summary>
/// Shows a list of usages for a referenced symbol if the "used by" indicator was clicked.
/// </summary>
private void DrawUsagesAReferencedSymbol()
{
if (_symbolUsageReferenceFilter == null)
return;
ImGui.Text("Usages of " + _symbolUsageReferenceFilter.Name + ":");
if (ImGui.Button("Clear"))
{
_symbolUsageReferenceFilter = null;
}
else
{
ImGui.Separator();
ImGui.BeginChild("scrolling");
{
if (SymbolAnalysis.DetailsInitialized &&
SymbolAnalysis.InformationForSymbolIds.TryGetValue(_symbolUsageReferenceFilter.Id, out var info))
{
// TODO: this should be cached...
var allSymbols = EditorSymbolPackage.AllSymbols.ToDictionary(s => s.Id);
foreach (var id in info.DependingSymbols)
{
if (allSymbols.TryGetValue(id, out var symbol))
{
// Use instance method
this.DrawSymbolItemInstance(symbol);
}
}
}
}
CustomComponents.HandleDragScrolling(this);
ImGui.EndChild();
}
}
/// <summary>
/// Draws a flat list of matching symbols when search is active.
/// </summary>
private void DrawFilteredList()
{
_filter.UpdateIfNecessary(null);
foreach (var symbolUi in _filter.MatchingSymbolUis)
{
this.DrawSymbolItemInstance(symbolUi.Symbol);
}
}
// --- Expand-to-symbol logic ---
// Indicates if the tree should expand to reveal a specific symbol
private bool _expandToSymbolTriggered;
// The target symbol ID to expand to
private Guid? _expandToSymbolTargetId;
// Set by Reveal() and the aim icon: the symbol item scrolls itself into view once, then clears this.
private Guid? _scrollToSymbolId;
/// <summary>
/// Checks if a <see cref="NamespaceTreeNode"/> is in the path to a symbol, returning the path if found.
/// </summary>
private static bool IsInPathToSymbol(NamespaceTreeNode node, Guid symbolId, out List<NamespaceTreeNode> path)
{
path = new List<NamespaceTreeNode>();
return FindPathRecursive(node, symbolId, path);
}
/// <summary>
/// Recursively searches for a symbol in the tree and builds the path to it.
/// </summary>
private static bool FindPathRecursive(NamespaceTreeNode node, Guid symbolId, List<NamespaceTreeNode> path)
{
if (node.Symbols.Any(s => s.Id == symbolId))
{
path.Add(node);
return true;
}
foreach (var child in node.Children)
{
if (FindPathRecursive(child, symbolId, path))
{
path.Add(node);
return true;
}
}
return false;
}
private static string _draggedNamespace = string.Empty;
/// <summary>
/// Recursively draws namespace nodes and their symbols in the tree view.
/// </summary>
/// <param name="subtree">The subtree node to draw.</param>
private void DrawNode(NamespaceTreeNode subtree)
{
if (subtree.Name == NamespaceTreeNode.RootNodeId)
{
DrawNodeItems(subtree);
}
else
{
ImGui.PushID(subtree.Id);
ImGui.SetNextItemWidth(10);
if (subtree.Name == "Lib" && !_openedLibFolderOnce)
{
ImGui.SetNextItemOpen(true);
_openedLibFolderOnce = true;
}
// --- Aim icon logic for tree nodes ---
var selectedSymbolId = _lastSelectedSymbolId;
var containsSelected = selectedSymbolId != null && ContainsSymbolRecursive(subtree, selectedSymbolId.Value);
// Expand all nodes in the path to the target symbol if triggered
if (_expandToSymbolTriggered && _expandToSymbolTargetId.HasValue)
{
if (IsInPathToSymbol(subtree, _expandToSymbolTargetId.Value, out var path) && path.Contains(subtree))
{
ImGui.SetNextItemOpen(true, ImGuiCond.Always);
}
}
_treeHandler.UpdateForNode(subtree.Id);
var isProject = subtree.FolderType == NamespaceTreeNode.SymbolFolderTypes.Project;
if (isProject)
{
ImGui.PushFont(Fonts.FontBold);
}
else
{
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.Text.Fade(0.7f).Rgba);
}
// Tight rows and subtle hover matching the folder styling of the asset library
ImGui.PushStyleColor(ImGuiCol.HeaderHovered, Color.Transparent.Rgba);
ImGui.PushStyleColor(ImGuiCol.HeaderActive, Color.Transparent.Rgba);
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
var isOpen = ImGui.TreeNode(subtree.Name);
ImGui.PopStyleVar(2);
ImGui.PopStyleColor(2);
CustomComponents.DrawHoverHighlightOnLastItem();
if (isProject)
{
ImGui.PopFont();
}
else
{
ImGui.PopStyleColor();
}
if (DragAndDropHandling.HandleDragSourceForLastItem(
DragAndDropHandling.DragTypes.SymbolNameSpace,
subtree.Namespace))
{
_draggedNamespace = subtree.Namespace;
}
// Draw aim icon if this node contains the selected symbol and is not open
if (!isOpen && containsSelected)
{
var h = ImGui.GetFontSize();
CustomComponents.RightAlign(h);
var clicked = ImGui.InvisibleButton("Reveal", new Vector2(h));
if (ImGui.IsItemHovered())
{
CustomComponents.TooltipForLastItem("Reveal selected operator");
}
// Animate aim icon
var timeSinceChange = (float)(ImGui.GetTime() - _lastSelectionTime);
var fadeProgress = Clamp01(timeSinceChange / 0.5f);
var blinkFade = -MathF.Cos(timeSinceChange * 15f) * (1f - fadeProgress) * 0.7f + 0.75f;
var color = UiColors.StatusActivated.Fade(blinkFade);
Icons.DrawIconOnLastItem(Icon.Aim, color);
// Optionally, scroll to item if just selected
if (_expandToSymbolTriggered && selectedSymbolId.HasValue)
{
ImGui.SetScrollHereY();
}
// Set expand trigger if clicked
if (clicked && selectedSymbolId.HasValue)
{
_expandToSymbolTriggered = true;
_expandToSymbolTargetId = selectedSymbolId;
_scrollToSymbolId = selectedSymbolId;
}
}
// Context menu for namespace node
CustomComponents.ContextMenuForItem(() =>
{
if (ImGui.MenuItem("Rename Namespace"))
{
_subtreeNodeToRename = subtree;
_renameNamespaceDialog.ShowNextFrame();
}
});
HandleDroppingNamespace(subtree);
if (isOpen)
{
_treeHandler.NoFolderOpen = false;
// Reset expand trigger after expanding and target is visible
if (_expandToSymbolTriggered && _expandToSymbolTargetId.HasValue && ContainsSymbolRecursive(subtree, _expandToSymbolTargetId.Value))
{
// If this is the last node in the path, reset
if (subtree.Symbols.Any(s => s.Id == _expandToSymbolTargetId.Value))
{
_expandToSymbolTriggered = false;
_expandToSymbolTargetId = null;
}
}
HandleDroppingSymbol(subtree);
DrawNodeItems(subtree);
ImGui.TreePop();
}
// else
// {
// // Small helper button for quickly dropping dragged symbols into unopened namespaces.
// if (DragAndDropHandling.IsDragging)
// {
// ImGui.SameLine();
// ImGui.PushID("DropButton");
// ImGui.Button(" <-", new Vector2(50, 15));
// HandleDropTarget(subtree);
// ImGui.PopID();
// }
// }
ImGui.PopID();
}
}
/// <summary>
/// Checks if a <see cref="NamespaceTreeNode"/> contains a symbol with the given ID, recursively.
/// </summary>
private static bool ContainsSymbolRecursive(NamespaceTreeNode node, Guid symbolId)
{
foreach (var s in node.Symbols)
{
if (s.Id == symbolId)
return true;
}
foreach (var child in node.Children)
{
if (ContainsSymbolRecursive(child, symbolId))
return true;
}
return false;
}
/// <summary>
/// Draws all child namespaces and symbols of a subtree node.
/// </summary>
private void DrawNodeItems(NamespaceTreeNode subtree)
{
// Using a for loop to prevent modification during iteration exception
for (var index = 0; index < subtree.Children.Count; index++)
{
var subspace = subtree.Children[index];
DrawNode(subspace);
}
// Use a copy of the list to avoid modification issues when symbols are moved/deleted.
for (var index = 0; index < subtree.Symbols.ToList().Count; index++)
{
var symbol = subtree.Symbols.ToList()[index];
this.DrawSymbolItemInstance(symbol);
}
}
/// <summary>
/// Handles drag-and-drop onto a namespace node to move symbols between namespaces.
/// </summary>
private static void HandleDroppingSymbol(NamespaceTreeNode subtree)
{
var resultSymbol = DragAndDropHandling.TryHandleDropOnItem(DragAndDropHandling.DragTypes.Symbol, out var data);
if (resultSymbol != DragAndDropHandling.DragInteractionResult.Dropped)
return;
if (!Guid.TryParse(data, out var symbolId))
return;
if (!MoveSymbolToNamespace(symbolId, subtree.GetAsString(), out var reason))
BlockingWindow.Instance.ShowMessageBox(reason, "Could not move symbol's namespace");
EditorSymbolPackage.NotifySymbolStructureChange();
}
/// <summary>
/// Handles drag-and-drop onto a namespace node to move symbols between namespaces.
/// </summary>
private static void HandleDroppingNamespace(NamespaceTreeNode subtree)
{
if (subtree.Namespace.StartsWith(_draggedNamespace))
return;
var targetNamespace = subtree.Namespace;
var resultSymbol = DragAndDropHandling.TryHandleDropOnItem(DragAndDropHandling.DragTypes.SymbolNameSpace, out var data);
if (resultSymbol != DragAndDropHandling.DragInteractionResult.Dropped)
return;
if (!EditableSymbolProject.TryGetEditableProjectOfNamespace(_draggedNamespace, out var sourceProject))
{
Log.Warning($"Can't find editable project for {_draggedNamespace}");
return;
}
if (!EditableSymbolProject.TryGetEditableProjectOfNamespace(targetNamespace, out var targetProject))
{
Log.Warning($"Can't find editable project for {_draggedNamespace}");
return;
}
List<Symbol> draggedSymbols = [];
foreach (var symbol in sourceProject.Symbols.Values)
{
if (!symbol.Namespace.StartsWith(_draggedNamespace, StringComparison.OrdinalIgnoreCase))
continue;
draggedSymbols.Add(symbol);
}
var lastNamespacePartIndex = _draggedNamespace.LastIndexOf('.');
var lastNamespacePart = lastNamespacePartIndex > 0
? _draggedNamespace[(lastNamespacePartIndex + 1)..]
: _draggedNamespace;
for (var index = 0; index < draggedSymbols.Count; index++)
{
var symbol = draggedSymbols[index];
var target = targetNamespace;
if (symbol.Namespace.StartsWith(_draggedNamespace, StringComparison.OrdinalIgnoreCase)
&& symbol.Namespace.Length > _draggedNamespace.Length)
{
target += (symbol.Namespace[_draggedNamespace.Length..]);
}
else
{
target = targetNamespace + "." + lastNamespacePart;
}
var skipProjectReload = index < draggedSymbols.Count - 1;
if (!MoveSymbolToNamespace(symbol.Id, target, out var reason, skipProjectReload))
{
Log.Warning($"Failed to move {symbol} to {target}: {reason}");
}
}
EditorSymbolPackage.NotifySymbolStructureChange();
}
/// <summary>
/// Moves a symbol to a new namespace, respecting read-only package restrictions.
/// </summary>
private static bool MoveSymbolToNamespace(Guid symbolId, string nameSpace, out string reason, bool skipProjectReload = false)
{
if (!SymbolUiRegistry.TryGetSymbolUi(symbolId, out var symbolUi))
{
reason = $"Could not find symbol with id '{symbolId}'";
return false;
}
if (symbolUi.Symbol.Namespace == nameSpace)
{
reason = string.Empty;
return true;
}
if (symbolUi.Symbol.SymbolPackage.IsReadOnly)
{
reason = $"Could not move symbol [{symbolUi.Symbol.Name}] because its package is not modifiable";
return false;
}
return EditableSymbolProject.ChangeSymbolNamespace(symbolUi.Symbol, nameSpace, out reason, skipProjectReload);
}
/// <summary>
/// Returns an empty list, as only one instance of SymbolLibrary is supported.
/// </summary>
internal override List<Window> GetInstances()
{
return [];
}
// --- State fields ---
// Indicates if the library was scanned for dependencies
private bool _wasScanned;
/// <summary>
/// The filtered tree of namespaces and symbols, updated by filters.
/// </summary>
internal readonly NamespaceTreeNode FilteredTree = new(NamespaceTreeNode.RootNodeId);
// The namespace node currently being renamed
private NamespaceTreeNode? _subtreeNodeToRename;
// Tracks if the Lib folder was opened once
private bool _openedLibFolderOnce;
// The root node of the full symbol tree
private readonly NamespaceTreeNode _treeNode = new(NamespaceTreeNode.RootNodeId);
// Corner rounding (unscaled px) for symbol item buttons and their badges
private const float ItemRounding = 3f;
// The symbol filter for search and matching
private readonly SymbolFilter _filter = new();
// Dialog for renaming namespaces
private static readonly RenameNamespaceDialog _renameNamespaceDialog = new();
// The symbol currently being inspected for usages
private static Symbol? _symbolUsageReferenceFilter;
// Generator for random prompt suggestions
private readonly RandomPromptGenerator _randomPromptGenerator;
// Filtering UI and logic for the library
private readonly LibraryFiltering _libraryFiltering;
// Dialog for deleting symbols
private static readonly DeleteSymbolDialog _deleteSymbolDialog = new();
// Controls visibility of the delete dialog
private static bool _showDeleteDialog = true;
// The symbol currently selected for deletion
private static Symbol? _symbolToDelete;
// --- Highlight and Aim Icon for selected operator in node graph ---
// Store the last selected symbol id and time for highlight/aim icon animation
private static Guid? _lastSelectedSymbolId;
private static double _lastSelectionTime;
// A highlight from Reveal() persists until the graph selection changes away from this snapshot.
private static bool _revealHighlightActive;
private static Guid? _graphSelectionAtReveal;
/// <summary>
/// Updates the highlight/aim icon state for the currently selected symbol in the node graph.
/// </summary>
private void UpdateSelectedSymbolHighlight()
{
if (ProjectView.Focused?.NodeSelection == null)
return;
var selectedSymbolId = TryGetSingleSelectedSymbolId();
// A Reveal() highlight is not backed by the graph selection - keep it
// (set via _scrollToSymbolId in DrawSymbolItemInstance) until that selection changes.
if (_revealHighlightActive)
{
if (selectedSymbolId == _graphSelectionAtReveal)
return;
_revealHighlightActive = false;
}
// Only highlight if exactly one operator is selected in the node graph
if (selectedSymbolId.HasValue)
{
if (_lastSelectedSymbolId != selectedSymbolId)
{
_lastSelectedSymbolId = selectedSymbolId;
_lastSelectionTime = ImGui.GetTime();
}
}
else
{
_lastSelectedSymbolId = null;
}
}
private static Guid? TryGetSingleSelectedSymbolId()
{
var nodeSelection = ProjectView.Focused?.NodeSelection;
if (nodeSelection == null)
return null;
var selectedChildUis = nodeSelection.GetSelectedChildUis().ToList();
if (selectedChildUis.Count != 1)
return null;
return selectedChildUis[0].SymbolChild?.Symbol.Id;
}
// Helper for clamping float/double values between 0 and 1
private static float Clamp01(float v) => v < 0 ? 0 : v > 1 ? 1 : v;
private static float Clamp01(double v) => v < 0 ? 0 : v > 1 ? 1 : (float)v;
/// <summary>
/// Static wrapper for drawing a symbol item, used for external static calls.
/// </summary>
internal static void DrawSymbolItemStatic(Symbol symbol)
{
// Use WindowManager to get all windows and find the first SymbolLibrary instance
var symbolLibraryInstance = T3.Editor.Gui.Windows.Layouts.WindowManager.GetAllWindows().OfType<SymbolLibrary>().FirstOrDefault();
symbolLibraryInstance?.DrawSymbolItemInstance(symbol);
}
/// <summary>
/// Static method for legacy external calls.
/// </summary>
internal static void DrawSymbolItem(Symbol symbol)
{
DrawSymbolItemStatic(symbol);
}
/// <summary>
/// Instance method for drawing a symbol item in the tree, including highlight, context menu, and dependency badges.
/// </summary>
/// <param name="symbol">The symbol to draw.</param>
internal void DrawSymbolItemInstance(Symbol symbol)
{
if (!symbol.TryGetSymbolUi(out var symbolUi))
return;
ImGui.PushID(symbol.Id.GetHashCode());
{
var color = symbol.OutputDefinitions.Count > 0
? TypeUiRegistry.GetPropertiesForType(symbol.OutputDefinitions[0]?.ValueType).Color
: UiColors.Gray;
// A pending Reveal() scrolls this item into view and highlights it. The path nodes are
// expanded by the _expandToSymbolTargetId logic above, so by here the item is laid out.
if (_scrollToSymbolId == symbol.Id)
{
ImGui.SetScrollHereY(0.5f);
_lastSelectedSymbolId = symbol.Id;
_lastSelectionTime = ImGui.GetTime();
_scrollToSymbolId = null;
}
// Skip layout and badge work for rows outside the visible scroll region — the tree
// can hold thousands of symbols. A dummy of the same height keeps scroll extents stable.
var rowHeight = ImGui.GetFrameHeight();
if (!ImGui.IsRectVisible(new Vector2(1, rowHeight)))
{
ImGui.Dummy(new Vector2(1, rowHeight));
ImGui.PopID();
return;
}
// --- Highlight and Aim Icon for selected symbol ---
var isSelected = false;
var timeSinceSelection = 0f;
if (_lastSelectedSymbolId.HasValue && symbol.Id == _lastSelectedSymbolId.Value)
{
isSelected = true;
timeSinceSelection = (float)(ImGui.GetTime() - _lastSelectionTime);
}
// Tag “bookmark” button in front of symbol button.
if (ParameterWindow.DrawSymbolTagsButton(symbolUi))
symbolUi.FlagAsModified();
ImGui.SameLine();
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, ItemRounding * T3Ui.UiScaleFactor);
ImGui.PushStyleColor(ImGuiCol.Button, ColorVariations.OperatorBackground.Apply(color).Rgba);
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, ColorVariations.OperatorBackgroundHover.Apply(color).Rgba);
ImGui.PushStyleColor(ImGuiCol.ButtonActive, ColorVariations.OperatorBackgroundHover.Apply(color).Rgba);
ImGui.PushStyleColor(ImGuiCol.Text, ColorVariations.OperatorLabel.Apply(color).Rgba);
bool buttonPressed = ImGui.Button(symbol.Name.AddSpacesForImGuiOutput());
ImGui.PopStyleVar();
if (buttonPressed)
HelpWindow.ShowTopic(HelpTopic.ForOperator(symbol.Id));
// Get button rect for icon and highlight
var buttonMin = ImGui.GetItemRectMin();
var buttonMax = ImGui.GetItemRectMax();
// Draw highlight border if selected (drawn last, on top)
if (isSelected)
{
var fadeProgress = Clamp01(timeSinceSelection / 0.3f);
var blinkFade = MathUtils.Lerp(-MathF.Cos(timeSinceSelection * 15f) * 0.8f + 0.2f, 1, fadeProgress);
var highlightColor = UiColors.StatusActivated.Fade(blinkFade);
ImGui.GetWindowDrawList().AddRect(buttonMin, buttonMax, highlightColor, ItemRounding * T3Ui.UiScaleFactor);
}
// Show tooltip with description if hovered
if (ImGui.IsItemHovered())
{
// Let the Help window follow the symbol the mouse is over here, just like hovering it in the graph.
HoveredHelpTarget.SetOperator(symbol.Id);
var thumbnailRect = ThumbnailManager.GetThumbnail(symbol.Id, symbol.SymbolPackage, ThumbnailManager.Categories.PackageMeta);
ImGui.SetMouseCursor(ImGuiMouseCursor.ResizeAll);
// The Help window already previews the hovered symbol — the tooltip would double it.
if (!HelpWindow.HoverPreviewActive
&& (!string.IsNullOrEmpty(symbolUi.Description) || thumbnailRect.IsReady))
{
CustomComponents.BeginTooltip(600);
{
if (!string.IsNullOrEmpty(symbolUi.Description))
{
ImGui.BeginGroup();
{
ImGui.PushTextWrapPos(ImGui.GetFontSize() * 25.0f);
ImGui.TextUnformatted(symbolUi.Description);
ImGui.PopTextWrapPos();
}
ImGui.EndGroup();
}
if (thumbnailRect.IsReady)
{
ImGui.SameLine(0, 10);
ImGui.BeginGroup();
{
thumbnailRect.AsImguiImage();
}
ImGui.EndGroup();
}
}
CustomComponents.EndTooltip();
}
}
ImGui.PopStyleColor(4);
HandleDragAndDropForSymbolItem(symbol);
// Styled context menu with symbol name as header and proper popup padding.
CustomComponents.ContextMenuForItem(
drawMenuItems: () =>
{
// Existing symbol-specific menu
CustomComponents.DrawSymbolCodeContextMenuItem(symbol);
ImGui.Separator();
// Delete symbol menu entry
if (ImGui.MenuItem("Delete Symbol"))
{
_symbolToDelete = symbol;
_deleteSymbolDialog.ShowNextFrame();
}
},
title: symbol.Name,
id: "##symbolTreeSymbolContextMenu");
// Experimental Thumbnails
// var keepCursor = ImGui.GetCursorPos();
// CustomComponents.RightAlign(50);
// if (!ThumbnailManager.GetThumbnail(symbol.Id, symbol.SymbolPackage).AsImguiImage(30))
// {
// ImGui.Dummy(new Vector2(10));
// }
// ImGui.SetCursorPos(keepCursor);
// Draw dependency badges if analysis is available
if (SymbolAnalysis.DetailsInitialized &&
SymbolAnalysis.InformationForSymbolIds.TryGetValue(symbol.Id, out var info))
{
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
ListSymbolSetWithTooltip(
250,
Icon.Dependencies,
"{0}",
string.Empty,
"requires...",
info.RequiredSymbolIds.ToList());
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.StatusAttention.Rgba);
ListSymbolSetWithTooltip(
300,
Icon.None,
"{0}",
string.Empty,
"has invalid references...",
info.InvalidRequiredIds);
ImGui.PopStyleColor();
if (ListSymbolSetWithTooltip(
340,
Icon.Referenced,
"{0}",
" NOT USED",
"used by...",
info.DependingSymbols.ToList()))
{
_symbolUsageReferenceFilter = symbol;
}
ImGui.PopStyleColor();
}
// Draw example badges if available
if (ExampleSymbolLinking.TryGetExamples(symbol.Id, out var examples))
{
ImGui.PushFont(Fonts.FontSmall);
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, ItemRounding * T3Ui.UiScaleFactor);
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.5f * ImGui.GetStyle().Alpha);
for (var index = 0; index < examples.Count; index++)
{
var exampleSymbolUi = examples[index];
ImGui.SameLine();
ImGui.Button("EXAMPLE");
HandleDragAndDropForSymbolItem(exampleSymbolUi.Symbol);
}
ImGui.PopStyleVar(2);
ImGui.PopFont();
}
}
ImGui.PopID();
// Modal delete confirmation dialog for the symbol selected via context menu.
if (_symbolToDelete != null && ImGui.BeginPopupModal("DeleteSymbol", ref _showDeleteDialog))
{
_deleteSymbolDialog.Draw(_symbolToDelete);
if (!_showDeleteDialog) // Dialog closed
{
_symbolToDelete = null;
}
ImGui.EndPopup();
}
}
/// <summary>
/// Draws small badges for symbol dependencies, invalid references, or usages, with tooltips.
/// </summary>
private static bool ListSymbolSetWithTooltip(float x,
Icon icon,
string setTitleFormat,
string emptySetTitle,
string toolTopTitle,
List<Guid> symbolSet)
{
var activated = false;
ImGui.PushID(icon.ToString());
ImGui.SameLine(x, 10);
if (symbolSet.Count > 0)
{
icon.DrawAtCursor();
CustomComponents.TooltipForLastItem(DrawTooltip);
ImGui.SameLine(0, 0);
}
if (symbolSet.Count == 0)
{
ImGui.TextUnformatted(emptySetTitle);
}
else
{
ImGui.TextUnformatted(string.Format(setTitleFormat, symbolSet.Count));
CustomComponents.TooltipForLastItem(DrawTooltip);
if (ImGui.IsItemClicked())
{
activated = true;
}
}
ImGui.PopID();
return activated;
// Tooltip callback to show detailed symbol list
void DrawTooltip()
{
var allSymbolUis = EditorSymbolPackage.AllSymbolUis;
var matches = allSymbolUis
.Where(s => symbolSet.Contains(s.Symbol.Id))
.OrderBy(s => s.Symbol.Namespace)
.ThenBy(s => s.Symbol.Name);
ImGui.BeginTooltip();
ImGui.TextUnformatted(toolTopTitle);
FormInputs.AddVerticalSpace();
ListSymbols(matches);
ImGui.EndTooltip();
}
}
/// <summary>
/// Helper to render grouped symbol lists inside dependency tooltips.
/// </summary>
private static void ListSymbols(IOrderedEnumerable<SymbolUi> symbolUis)
{
var lastGroupName = string.Empty;
ColumnLayout.StartLayout(25);
foreach (var required in symbolUis)
{
var projectName = required.Symbol.SymbolPackage.RootNamespace;
if (projectName != lastGroupName)
{
lastGroupName = projectName;
FormInputs.AddVerticalSpace(5);
ImGui.PushFont(Fonts.FontSmall);
ImGui.TextUnformatted(projectName);
ImGui.PopFont();
}
var hasIssues = required.Tags.HasFlag(SymbolUi.SymbolTags.Obsolete)
| required.Tags.HasFlag(SymbolUi.SymbolTags.NeedsFix);
var color = hasIssues ? UiColors.StatusAttention : UiColors.Text;
ImGui.PushStyleColor(ImGuiCol.Text, color.Rgba);
ColumnLayout.StartGroupAndWrapIfRequired(1);
ImGui.TextUnformatted(required.Symbol.Name);
ColumnLayout.ExtendWidth(ImGui.GetItemRectSize().X);
ImGui.PopStyleColor();
}
}
/// <summary>
/// Handles drag-and-drop source for symbol items and click-to-insert behavior.
/// </summary>
internal static void HandleDragAndDropForSymbolItem(Symbol symbol)
{
if (IsSymbolCurrentCompositionOrAParent(symbol))
return;
DragAndDropHandling.HandleDragSourceForLastItem(
DragAndDropHandling.DragTypes.Symbol,
symbol.Id.ToString());
if (!ImGui.IsItemDeactivated())
return;
var wasClick = ImGui.GetMouseDragDelta().Length() < 4;
if (wasClick)
{
var components = ProjectView.Focused;
if (components == null)
{
Log.Error($"No focused graph window found");
}
else if (components.NodeSelection.GetSelectedChildUis().Count() == 1)
{
ConnectionMaker.InsertSymbolInstance(components, symbol);
}
}
}
/// <summary>
/// Prevents dragging the current composition or any of its parents into itself.
/// </summary>
internal static bool IsSymbolCurrentCompositionOrAParent(Symbol symbol)
{
var components = ProjectView.Focused;
if (components?.CompositionInstance == null)
return false;
var comp = components.CompositionInstance;
if (comp.Symbol.Id == symbol.Id)
{
return true;
}
var instance = comp;
while (instance != null)
{
if (instance.Symbol.Id == symbol.Id)
return true;
instance = instance.Parent;
}
return false;
}
}