#nullable enable using ImGuiNET; using T3.Core.DataTypes.Vector; using T3.Core.Operator; using T3.Editor.Gui.Help; using T3.Editor.Gui.UiHelpers; using T3.Editor.Gui.Windows; using T3.Editor.Gui.Windows.Layouts; using T3.Editor.Gui.Windows.SymbolLib; using T3.Editor.UiModel; using T3.Editor.UiModel.InputsAndTypes; namespace T3.Editor.Gui.Styling.Markdown; /// /// Supplies : resolves a bare [OpName] markdown /// reference to a symbol, shows a hover tooltip (namespace + description), on click reveals the /// operator in the Symbol Library, and acts as a drag source so the chip can be dropped onto the /// graph to create an instance. Unresolved names get no interaction (the renderer already shows /// them as plain text via the missing UrlIndex). /// internal static class MarkdownOperatorLinks { /// /// Hover/click handler for [OpName] fragments — pass as MarkdownView's onOperatorRef. /// Set when the markdown is itself drawn inside a tooltip, since /// ImGui can't nest tooltips. /// internal static void HandleOperatorRef(string opName, bool suppressTooltip = false) { if (opName.StartsWith("ui:", StringComparison.Ordinal)) { HandleTopicRef(opName, suppressTooltip); return; } if (!TryResolve(opName, out var symbol)) return; // The chip is a drag source like a Symbol Library entry — dropping it onto the graph // creates an instance. Skipped for the current composition (or a parent) to avoid recursion. if (!SymbolLibrary.IsSymbolCurrentCompositionOrAParent(symbol)) { DragAndDropHandling.HandleDragSourceForLastItem(DragAndDropHandling.DragTypes.Symbol, symbol.Id.ToString()); } // A release without notable movement is a click and reveals the operator; any real // drag is handled above and must not trigger navigation. if (ImGui.IsItemDeactivated() && ImGui.GetMouseDragDelta().Length() < 4) { HelpWindow.ShowTopic(HelpTopic.ForOperator(symbol.Id)); WindowManager.SymbolLibrary.Reveal(symbol.Id); } if (!ImGui.IsItemHovered() || DragAndDropHandling.IsDragging) return; ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); // While the Help window previews hovered topics, a tooltip would double the content. Links inside // the Help window itself keep their tooltip — previewing would swap the body under the cursor. if (HelpWindow.HoverPreviewActive && !HelpWindow.IsDrawingContent) { HoveredHelpTarget.SetOperator(symbol.Id); return; } // suppressTooltip avoids nesting when the markdown is already inside a tooltip (e.g. the // operator's own description, which can itself contain [OpName] references). if (!suppressTooltip) { var scale = T3Ui.UiScaleFactor; var tooltipWidth = TooltipWidth * scale; // Pin the width before BeginTooltip so the auto-resizing tooltip doesn't flash at viewport // width on its first frame. ImGui.SetNextWindowSizeConstraints(new Vector2(tooltipWidth, 0), new Vector2(tooltipWidth, float.MaxValue)); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(10, 8) * scale); ImGui.BeginTooltip(); ImGui.PushFont(Fonts.FontBold); ImGui.TextUnformatted(symbol.Name); ImGui.PopFont(); ImGui.TextColored(UiColors.TextMuted, symbol.Namespace); if (symbol.TryGetSymbolUi(out var symbolUi) && !string.IsNullOrWhiteSpace(symbolUi.Description)) { ImGui.Spacing(); // Render the description as markdown too — code spans, lists, and [OpName] refs read // correctly. Operator refs inside the tooltip suppress their own tooltip to avoid nesting. _tooltipMarkdown.Draw(symbolUi.Description, onOperatorRef: op => HandleOperatorRef(op, suppressTooltip: true), operatorColor: GetOperatorColor); } ImGui.EndTooltip(); ImGui.PopStyleVar(); } } /// /// Hover/click handler for a [ui:Id] fragment — the help index's UI topic (editor concept/component). /// Clicking shows the topic in the Help window; hovering previews it there (or falls back to a tooltip /// when the window's hover mode is off). An unknown id gets no interaction. /// private static void HandleTopicRef(string topicKey, bool suppressTooltip) { if (!ImGui.IsItemHovered()) return; if (!HelpIndex.TryGetTopic(topicKey, out var topic) || topic == null) return; ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) HelpWindow.ShowTopic(HelpTopic.ForUiTopic(topicKey), showWindow: true); // While the Help window previews hovered topics, a tooltip would double the content. Links inside // the Help window itself keep their tooltip — previewing would swap the body under the cursor. if (HelpWindow.HoverPreviewActive && !HelpWindow.IsDrawingContent) { HoveredHelpTarget.SetTopic(topicKey); return; } if (suppressTooltip) return; var scale = T3Ui.UiScaleFactor; var tooltipWidth = TooltipWidth * scale; ImGui.SetNextWindowSizeConstraints(new Vector2(tooltipWidth, 0), new Vector2(tooltipWidth, float.MaxValue)); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(10, 8) * scale); ImGui.BeginTooltip(); ImGui.PushFont(Fonts.FontBold); ImGui.TextUnformatted(topic.Term); ImGui.PopFont(); ImGui.TextColored(UiColors.TextMuted, "UI topic"); if (!string.IsNullOrWhiteSpace(topic.Doc)) { ImGui.Spacing(); _tooltipMarkdown.Draw(topic.Doc, onOperatorRef: op => HandleOperatorRef(op, suppressTooltip: true), operatorColor: GetOperatorColor); } ImGui.EndTooltip(); ImGui.PopStyleVar(); } /// /// Text color for an [OpName] / [ui:Id] fragment — the operator's output-type color, a fixed /// accent for UI topics, or the plain body color for an unknown name. Pass as operatorColor. /// internal static Color GetOperatorColor(string opName) { if (opName.StartsWith("ui:", StringComparison.Ordinal)) return HelpIndex.TryGetTopic(opName, out _) ? UiColors.StatusAutomated : UiColors.Text; if (!TryResolve(opName, out var symbol)) return UiColors.Text; return symbol.OutputDefinitions.Count > 0 ? TypeUiRegistry.GetPropertiesForType(symbol.OutputDefinitions[0]?.ValueType).Color : UiColors.Gray; } private static bool TryResolve(string opName, out Symbol symbol) { // Rebuilt when the symbol structure changes so references to ops from // later-loaded projects resolve too. var structureVersion = EditorSymbolPackage.SymbolStructureVersionCounter; if (_symbolsByName == null || structureVersion != _lookupVersion) { _symbolsByName = BuildLookup(); _lookupVersion = structureVersion; } return _symbolsByName.TryGetValue(opName, out symbol!); } private static Dictionary BuildLookup() { var map = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var symbol in EditorSymbolPackage.AllSymbols) map[symbol.Name] = symbol; // duplicate names: last wins — fine for documentation links return map; } private static Dictionary? _symbolsByName; private static int _lookupVersion = -1; private const float TooltipWidth = 320f; private static readonly MarkdownView _tooltipMarkdown = new(new MarkdownView.Options()); }