tooll3--t3
518 行
18 KiB
C#
518 行
18 KiB
C#
#nullable enable
|
||
|
||
using System.Diagnostics;
|
||
using ImGuiNET;
|
||
|
||
using T3.Core.DataTypes.Vector;
|
||
using T3.Core.Model;
|
||
using T3.Core.Utils;
|
||
using T3.Editor.App;
|
||
using T3.Editor.Gui.Help;
|
||
using T3.Editor.Gui.MagGraph.Model;
|
||
using T3.Editor.Gui.MagGraph.States;
|
||
using T3.Editor.Gui.Styling;
|
||
using T3.Editor.Gui.UiHelpers;
|
||
using T3.Editor.Gui.Windows;
|
||
using T3.Editor.UiModel;
|
||
using T3.Editor.UiModel.Helpers;
|
||
using T3.Editor.UiModel.InputsAndTypes;
|
||
using T3.SystemUi;
|
||
|
||
namespace T3.Editor.Gui.MagGraph.Interaction;
|
||
|
||
internal static class PlaceHolderUi
|
||
{
|
||
internal static void Open(
|
||
GraphUiContext context,
|
||
MagGraphItem placeholderItem,
|
||
MagGraphItem.Directions connectionOrientation = MagGraphItem.Directions.Horizontal,
|
||
Type? inputFilter = null,
|
||
Type? outputFilter = null,
|
||
bool onlyMultiInputs = false)
|
||
{
|
||
_selectedSymbolUi = null;
|
||
_focusInputNextTime = true;
|
||
|
||
Filter.FilterInputType = inputFilter;
|
||
Filter.FilterOutputType = outputFilter;
|
||
Filter.OnlyMultiInputs = onlyMultiInputs;
|
||
Filter.WasUpdated = true;
|
||
Filter.SearchString = string.Empty;
|
||
Filter.UpdateIfNecessary(context.Selector, forceUpdate: true);
|
||
|
||
_placeholderItem = placeholderItem;
|
||
_connectionOrientation = connectionOrientation;
|
||
|
||
WindowContentExtend.GetLastAndReset();
|
||
SymbolBrowsing.Reset();
|
||
|
||
_rowHeight = 0;
|
||
}
|
||
|
||
internal static void Reset()
|
||
{
|
||
Filter.Reset();
|
||
_placeholderItem = null;
|
||
_selectedSymbolUi = null;
|
||
_rowHeight = 0;
|
||
_focusInputNextTime = false;
|
||
}
|
||
|
||
internal static UiResults Draw(GraphUiContext context, out SymbolUi? selectedUi)
|
||
{
|
||
selectedUi = null;
|
||
|
||
var drawList = ImGui.GetWindowDrawList();
|
||
var uiResult = UiResults.None;
|
||
|
||
if (_placeholderItem == null)
|
||
return uiResult;
|
||
|
||
FrameStats.Current.OpenedPopUpName = "SymbolBrowser";
|
||
|
||
Filter.UpdateIfNecessary(context.Selector);
|
||
|
||
uiResult |= DrawSearchInput(context, drawList);
|
||
|
||
var pMin = context.View.TransformPosition(_placeholderItem.PosOnCanvas);
|
||
var pMax = context.View.TransformPosition(_placeholderItem.Area.Max);
|
||
|
||
uiResult |= DrawResultsList(context, new ImRect(pMin, pMax), Filter, _connectionOrientation);
|
||
|
||
// Click outside = mouse click that is NOT in placeholder rect and NOT in results rect.
|
||
var clickedOutsidePlaceholder = ImGui.IsMouseClicked(ImGuiMouseButton.Left)
|
||
&& !_placeholderAreaOnScreen.Contains(ImGui.GetMousePos());
|
||
if (clickedOutsidePlaceholder && uiResult.HasFlag(UiResults.ClickedOutside))
|
||
{
|
||
uiResult |= UiResults.Cancel;
|
||
}
|
||
|
||
if (_focusInputNextTime)
|
||
{
|
||
ImGui.SetKeyboardFocusHere();
|
||
_focusInputNextTime = false;
|
||
}
|
||
|
||
selectedUi = _selectedSymbolUi;
|
||
return uiResult;
|
||
}
|
||
|
||
// TODO: Implement preset search
|
||
// if (_selectedSymbolUi != null)
|
||
// {
|
||
// if (Filter.PresetFilterString != string.Empty && (Filter.WasUpdated || _selectedItemChanged))
|
||
// {
|
||
//
|
||
// }
|
||
// }
|
||
|
||
private static UiResults DrawSearchInput(GraphUiContext context, ImDrawListPtr drawList)
|
||
{
|
||
var uiResult = UiResults.None;
|
||
Debug.Assert(_placeholderItem != null);
|
||
|
||
var canvasScale = context.View.Scale.X;
|
||
|
||
var item = _placeholderItem;
|
||
var pMin = context.View.TransformPosition(item.PosOnCanvas);
|
||
var pMax = context.View.TransformPosition(item.PosOnCanvas + item.Size);
|
||
|
||
var pMinVisible = pMin;
|
||
var pMaxVisible = pMax;
|
||
|
||
_placeholderAreaOnScreen = ImRect.RectBetweenPoints(pMin, pMax);
|
||
|
||
// Background and Outline
|
||
drawList.AddRectFilled(pMinVisible + Vector2.One * canvasScale,
|
||
pMaxVisible - Vector2.One,
|
||
UiColors.BackgroundFull,
|
||
2 * canvasScale);
|
||
|
||
if (_focusInputNextTime)
|
||
{
|
||
ImGui.SetKeyboardFocusHere();
|
||
_focusInputNextTime = false;
|
||
uiResult |= UiResults.SelectionChanged;
|
||
}
|
||
|
||
var labelPos = new Vector2(pMin.X,
|
||
(pMin.Y + pMax.Y) / 2 - ImGui.GetFrameHeight() / 2);
|
||
|
||
var posInWindow = labelPos - ImGui.GetWindowPos();
|
||
ImGui.SetCursorPos(posInWindow);
|
||
|
||
var favoriteGroup = SymbolBrowsing.IsFilterActive ? SymbolBrowsing.FilterString : string.Empty;
|
||
|
||
if (string.IsNullOrEmpty(favoriteGroup))
|
||
{
|
||
var padding = new Vector2(9, 3);
|
||
if (string.IsNullOrEmpty(Filter.SearchString))
|
||
{
|
||
drawList.AddText(labelPos + padding, UiColors.TextDisabled, "search...");
|
||
}
|
||
|
||
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, padding);
|
||
ImGui.PushStyleColor(ImGuiCol.FrameBg, Color.Transparent.Rgba);
|
||
ImGui.SetNextItemWidth(item.Size.X);
|
||
|
||
ImGui.PushID("SymbolBrowserSearch");
|
||
ImGui.InputText("##symbolBrowserFilter",
|
||
ref Filter.SearchString,
|
||
20,
|
||
ImGuiInputTextFlags.AutoSelectAll);
|
||
ImGui.PopID();
|
||
|
||
ImGui.PopStyleColor();
|
||
ImGui.PopStyleVar();
|
||
}
|
||
else
|
||
{
|
||
ImGui.SetCursorPos(ImGui.GetCursorPos() + new Vector2(5, 0));
|
||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10);
|
||
ImGui.PushStyleColor(ImGuiCol.Button, UiColors.ForegroundFull.Fade(0.1f).Rgba);
|
||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UiColors.ForegroundFull.Fade(0.2f).Rgba);
|
||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UiColors.ForegroundFull.Fade(0.3f).Rgba);
|
||
|
||
if (ImGui.Button(favoriteGroup + " ×"))
|
||
{
|
||
SymbolBrowsing.Reset();
|
||
}
|
||
|
||
ImGui.PopStyleColor(3);
|
||
ImGui.PopStyleVar();
|
||
}
|
||
|
||
if (ImGui.IsKeyPressed(Key.Return.ToImGuiKey()))
|
||
{
|
||
if (_selectedSymbolUi != null)
|
||
{
|
||
uiResult |= UiResults.Create;
|
||
}
|
||
}
|
||
|
||
if (Filter.WasUpdated)
|
||
{
|
||
_selectedSymbolUi = Filter.MatchingSymbolUis.Count > 0
|
||
? Filter.MatchingSymbolUis[0]
|
||
: null;
|
||
_rowHeight = 0;
|
||
uiResult |= UiResults.SelectionChanged;
|
||
}
|
||
|
||
// Preserve Esc-based cancel logic with shouldCancelConnectionMaker.
|
||
var clickedOutside = false; // TODO: wire this up if window-hover based cancel is reintroduced
|
||
var shouldCancelConnectionMaker = clickedOutside
|
||
// ImGui.IsMouseClicked(ImGuiMouseButton.Right)
|
||
|| ImGui.IsKeyDown(Key.Esc.ToImGuiKey());
|
||
|
||
if (shouldCancelConnectionMaker)
|
||
{
|
||
uiResult |= UiResults.Cancel;
|
||
//Cancel(context); // TODO: Implement cancel behavior if/when needed
|
||
}
|
||
|
||
if (!ImGui.IsItemActive())
|
||
return uiResult;
|
||
|
||
if (ImGui.IsMouseDragging(ImGuiMouseButton.Left))
|
||
{
|
||
_placeholderItem.PosOnCanvas += context.View.InverseTransformDirection(ImGui.GetIO().MouseDelta);
|
||
}
|
||
|
||
return uiResult;
|
||
}
|
||
|
||
private static UiResults DrawResultsList(
|
||
GraphUiContext context,
|
||
ImRect screenItemArea,
|
||
SymbolFilter filter,
|
||
MagGraphItem.Directions orientation)
|
||
{
|
||
var result = UiResults.None;
|
||
|
||
var popUpSize = new Vector2(150, 235) * T3Ui.UiScaleFactor;
|
||
var windowSize = ImGui.GetWindowSize();
|
||
var windowPos = ImGui.GetWindowPos();
|
||
|
||
Vector2 resultPosOnScreen = new Vector2(screenItemArea.Min.X, screenItemArea.Max.Y + 3);
|
||
if (orientation == MagGraphItem.Directions.Vertical)
|
||
{
|
||
var y = screenItemArea.GetCenter().Y - 0.1f * popUpSize.Y;
|
||
resultPosOnScreen.Y = y.Clamp(windowPos.Y + 10, windowSize.Y + windowPos.Y - popUpSize.Y - 10);
|
||
resultPosOnScreen.X = screenItemArea.Max.X.Clamp(windowPos.X + 10,
|
||
windowPos.X + windowSize.X - popUpSize.X - 10);
|
||
}
|
||
|
||
var resultPosOnWindow = resultPosOnScreen - ImGui.GetWindowPos();
|
||
ImGui.SetCursorPos(resultPosOnWindow);
|
||
|
||
ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 6);
|
||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 14);
|
||
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(6, 6));
|
||
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(3, 6));
|
||
|
||
ImGui.PushStyleColor(ImGuiCol.ChildBg, UiColors.BackgroundFull.Fade(0.8f).Rgba);
|
||
ImGui.PushStyleColor(ImGuiCol.ScrollbarBg, Color.Transparent.Rgba);
|
||
|
||
// Base size from previous content.
|
||
var last = WindowContentExtend.GetLastAndReset()
|
||
+ ImGui.GetStyle().WindowPadding * 2
|
||
+ new Vector2(10, 3);
|
||
|
||
// Target: show up to 12 rows on first open if we have enough items.
|
||
const int targetVisibleRows = 12;
|
||
if (filter.MatchingSymbolUis.Count >= targetVisibleRows)
|
||
{
|
||
// Ensure _rowHeight is initialized (approximate if needed).
|
||
if (_rowHeight <= 0)
|
||
{
|
||
var lineHeight = ImGui.GetTextLineHeightWithSpacing();
|
||
_rowHeight = lineHeight;
|
||
}
|
||
|
||
var desiredHeight = _rowHeight * targetVisibleRows;
|
||
// Add top/bottom padding similar to your existing layout.
|
||
desiredHeight += ImGui.GetStyle().WindowPadding.Y * 2;
|
||
|
||
if (desiredHeight > last.Y)
|
||
{
|
||
last.Y = desiredHeight;
|
||
}
|
||
}
|
||
|
||
last.Y = last.Y.Clamp(0, 250);
|
||
|
||
var resultAreaOnScreen = ImRect.RectWithSize(resultPosOnScreen, last);
|
||
|
||
bool childOpen = ImGui.BeginChild(
|
||
999,
|
||
last,
|
||
ImGuiChildFlags.Borders | ImGuiChildFlags.AlwaysUseWindowPadding,
|
||
ImGuiWindowFlags.NoResize);
|
||
|
||
if (childOpen)
|
||
{
|
||
FrameStats.Current.OpenedPopupHovered = ImGui.IsWindowHovered();
|
||
|
||
if (!string.IsNullOrEmpty(filter.SearchString)
|
||
|| filter.FilterInputType != null
|
||
|| filter.FilterOutputType != null)
|
||
{
|
||
result |= DrawSearchResultEntries(context, filter);
|
||
}
|
||
else
|
||
{
|
||
result |= SymbolBrowsing.Draw(context);
|
||
}
|
||
}
|
||
ImGui.EndChild();
|
||
|
||
ImGui.PopStyleColor(2);
|
||
ImGui.PopStyleVar(4);
|
||
|
||
var wasClickedOutside = ImGui.IsMouseClicked(ImGuiMouseButton.Left)
|
||
&& !resultAreaOnScreen.Contains(ImGui.GetMousePos());
|
||
if (wasClickedOutside)
|
||
{
|
||
result |= UiResults.ClickedOutside;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
private static void PrintTypeFilter(SymbolFilter filter)
|
||
{
|
||
if (filter.FilterInputType == null && filter.FilterOutputType == null)
|
||
return;
|
||
|
||
ImGui.PushFont(Fonts.FontSmall);
|
||
|
||
var inputTypeName = filter.FilterInputType != null
|
||
? TypeNameRegistry.Entries[filter.FilterInputType]
|
||
: string.Empty;
|
||
var outputTypeName = filter.FilterOutputType != null
|
||
? TypeNameRegistry.Entries[filter.FilterOutputType]
|
||
: string.Empty;
|
||
|
||
var isMultiInput = filter.OnlyMultiInputs ? "[..]" : "";
|
||
var headerLabel = $"{inputTypeName}{isMultiInput} -> {outputTypeName}";
|
||
ImGui.TextDisabled(headerLabel);
|
||
|
||
ImGui.PopFont();
|
||
}
|
||
|
||
private static UiResults DrawSearchResultEntries(GraphUiContext context, SymbolFilter filter)
|
||
{
|
||
var result = UiResults.None;
|
||
|
||
if (ImGui.IsKeyReleased(Key.CursorDown.ToImGuiKey()))
|
||
{
|
||
UiListHelpers.AdvanceSelectedItem(filter.MatchingSymbolUis!, ref _selectedSymbolUi, 1);
|
||
result = UiResults.SelectionChanged;
|
||
}
|
||
else if (ImGui.IsKeyReleased(Key.CursorUp.ToImGuiKey()))
|
||
{
|
||
UiListHelpers.AdvanceSelectedItem(filter.MatchingSymbolUis!, ref _selectedSymbolUi, -1);
|
||
result = UiResults.SelectionChanged;
|
||
}
|
||
|
||
var gotAMatch = filter.MatchingSymbolUis.Count > 0
|
||
&& (_selectedSymbolUi != null && !filter.MatchingSymbolUis.Contains(_selectedSymbolUi));
|
||
if (gotAMatch)
|
||
_selectedSymbolUi = filter.MatchingSymbolUis[0];
|
||
|
||
if (_selectedSymbolUi == null && EditorSymbolPackage.AllSymbolUis.Any())
|
||
_selectedSymbolUi = EditorSymbolPackage.AllSymbolUis.First();
|
||
|
||
// --- ImGuiListClipper integration (only when child is visible) ---
|
||
var count = filter.MatchingSymbolUis.Count;
|
||
if (count > 0)
|
||
{
|
||
unsafe
|
||
{
|
||
// Measure one row height once (symbol name text height).
|
||
if (_rowHeight <= 0)
|
||
{
|
||
var size = ImGui.CalcTextSize(filter.MatchingSymbolUis[0].Symbol.Name);
|
||
// Add padding similar to the selectable's visual height.
|
||
_rowHeight = size.Y + ImGui.GetStyle().FramePadding.Y * 2;
|
||
}
|
||
|
||
ImGuiListClipperPtr clipper =
|
||
new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
|
||
|
||
clipper.Begin(count, _rowHeight);
|
||
|
||
while (clipper.Step())
|
||
{
|
||
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
||
{
|
||
var symbolUi = filter.MatchingSymbolUis[i];
|
||
result |= DrawSymbolUiEntry(context, symbolUi);
|
||
}
|
||
}
|
||
|
||
clipper.End();
|
||
}
|
||
}
|
||
|
||
WindowContentExtend.ExtendToLastItem(200);
|
||
return result;
|
||
}
|
||
|
||
internal static UiResults DrawSymbolUiEntry(GraphUiContext context, SymbolUi symbolUi, bool showType=true)
|
||
{
|
||
var result = UiResults.None;
|
||
|
||
var symbolHash = symbolUi.Symbol.Id.GetHashCode();
|
||
ImGui.PushID(symbolHash);
|
||
|
||
var symbolNamespace = symbolUi.Symbol.Namespace;
|
||
var isRelevantNamespace = IsRelevantNamespace(context, symbolNamespace);
|
||
|
||
var type = symbolUi.Symbol.OutputDefinitions.Count > 0
|
||
? symbolUi.Symbol.OutputDefinitions[0]?.ValueType
|
||
: null;
|
||
|
||
|
||
TypeUiRegistry.TryGetPropertiesForType(type, out var properties);
|
||
var color = properties.Color;
|
||
|
||
|
||
ImGui.PushStyleColor(ImGuiCol.Header, ColorVariations.OperatorBackground.Apply(color).Rgba);
|
||
var hoverColor = ColorVariations.OperatorBackgroundHover.Apply(color).Rgba;
|
||
hoverColor.W = 0.3f;
|
||
|
||
ImGui.PushStyleColor(ImGuiCol.HeaderHovered, hoverColor);
|
||
ImGui.PushStyleColor(ImGuiCol.Text, ColorVariations.OperatorLabel.Apply(color).Rgba);
|
||
|
||
var isSelected = symbolUi == _selectedSymbolUi;
|
||
|
||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 14);
|
||
ImGui.SetNextItemWidth(20);
|
||
|
||
var size = ImGui.CalcTextSize(symbolUi.Symbol.Name);
|
||
|
||
if (ImGui.Selectable($"##Selectable{symbolHash}",
|
||
isSelected,
|
||
ImGuiSelectableFlags.None,
|
||
new Vector2(size.X, 0)))
|
||
{
|
||
result |= UiResults.Create;
|
||
_selectedSymbolUi = symbolUi;
|
||
}
|
||
|
||
ImGui.PopStyleVar();
|
||
|
||
// var dl = ImGui.GetForegroundDrawList();
|
||
// dl.AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), Color.Green);
|
||
|
||
var isHovered = ImGui.IsItemHovered();
|
||
if (isHovered)
|
||
{
|
||
// Let the Help window follow the hovered symbol; its preview replaces the summary tooltip.
|
||
HoveredHelpTarget.SetOperator(symbolUi.Symbol.Id);
|
||
|
||
if (!HelpWindow.HoverPreviewActive)
|
||
{
|
||
ImGui.SetNextWindowSize(new Vector2(300, 0));
|
||
ImGui.BeginTooltip();
|
||
OperatorHelp.DrawHelpSummary(symbolUi, false);
|
||
ImGui.EndTooltip();
|
||
}
|
||
}
|
||
|
||
ImGui.SameLine(ImGui.GetItemRectMin().X - ImGui.GetWindowPos().X);
|
||
ImGui.TextUnformatted(symbolUi.Symbol.Name);
|
||
|
||
if (showType && type != null)
|
||
{
|
||
ImGui.SameLine(0,10);
|
||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.3f);
|
||
ImGui.TextUnformatted(type.Name.ToString());
|
||
ImGui.PopStyleVar();
|
||
}
|
||
|
||
ImGui.PopStyleColor(3);
|
||
ImGui.PopID();
|
||
|
||
return result;
|
||
}
|
||
|
||
private static bool IsRelevantNamespace(GraphUiContext context, string symbolNamespace)
|
||
{
|
||
var projectNamespace = "user." + context.CompositionInstance.Symbol.SymbolPackage.AssemblyInformation.Name + ".";
|
||
var compositionNameSpace = context.CompositionInstance.Symbol.Namespace;
|
||
|
||
var isRelevantNamespace =
|
||
symbolNamespace.StartsWith("Lib.")
|
||
|| symbolNamespace.StartsWith("Types.")
|
||
|| symbolNamespace.StartsWith("Examples.Lib.")
|
||
|| symbolNamespace.StartsWith(projectNamespace)
|
||
|| symbolNamespace.StartsWith(compositionNameSpace);
|
||
|
||
return isRelevantNamespace;
|
||
}
|
||
|
||
private static bool _focusInputNextTime = true;
|
||
private static SymbolUi? _selectedSymbolUi;
|
||
private static ImRect _placeholderAreaOnScreen;
|
||
internal static readonly SymbolFilter Filter = new();
|
||
private static MagGraphItem? _placeholderItem;
|
||
private static MagGraphItem.Directions _connectionOrientation = MagGraphItem.Directions.Horizontal;
|
||
|
||
// Cached row height for clipper
|
||
private static float _rowHeight = 0;
|
||
|
||
[Flags]
|
||
internal enum UiResults
|
||
{
|
||
None = 1 << 1,
|
||
SelectionChanged = 1 << 2,
|
||
FilterChanged = 1 << 3,
|
||
Create = 1 << 4,
|
||
Cancel = 1 << 5,
|
||
ClickedOutside = 1 << 6,
|
||
}
|
||
}
|