using System; using System.Collections.Generic; using System.Linq; using Unity.VisualScripting.Analytics; using UnityEditor; using UnityEngine; namespace Unity.VisualScripting { public abstract class VisualScriptingCanvas : ICanvas where TGraph : class, IGraph { public WidgetProvider widgetProvider { get; } public GraphSelection selection { get; } public ICanvasWindow window { get; set; } protected EventWrapper e { get; } public TGraph graph { get; } IGraph ICanvas.graph => graph; protected VisualScriptingCanvas(TGraph graph) { Ensure.That(nameof(graph)).IsNotNull(graph); this.graph = graph; e = new EventWrapper(GetType()); selection = new GraphSelection(); widgetProvider = new WidgetProvider(this); graph.elements.CollectionChanged += Recollect; widgets = new WidgetList(this); elementWidgets = new WidgetList(this); widgetsByAscendingZ = new WidgetList(this); visibleWidgetsByAscendingZ = new WidgetList(this); visibleWidgetsByDescendingZ = new WidgetList(this); } public virtual void Dispose() { graph.elements.CollectionChanged -= Recollect; widgetProvider.FreeAll(); } #region Context Shortcuts protected IGraphContext context => LudiqGraphsEditorUtility.editedContext.value; protected GraphReference reference => context.reference; #endregion public void Cache() { CacheWidgetCollections(); CacheWidgetItems(); CacheWidgetPositions(); CacheWidgetVisibility(); } #region Model public void CacheWidgetItems() { foreach (var widget in widgets) { widget.CacheItem(); } } #endregion #region Widgets IEnumerable ICanvas.widgets => widgets; private readonly WidgetList widgets; private readonly WidgetList elementWidgets; private IEnumerable GetWidgets() { foreach (var element in graph.elements) { foreach (var widget in GetWidgetsRecursive(this.Widget(element))) { yield return widget; } } } private IEnumerable GetWidgetsRecursive(IWidget widget) { yield return widget; foreach (var subWidget in widget.subWidgets) { foreach (var subWidgetRecursive in GetWidgetsRecursive(subWidget)) { yield return subWidgetRecursive; } } } private bool collectionsAreValid; public void Recollect() { collectionsAreValid = false; } public void CacheWidgetCollections() { // Dispose widgets that are no longer within the graph // so that they unregister any event handler widgetProvider.FreeInvalid(); // Remove invalid widgets from the drag group dragGroup.RemoveWhere(element => !widgetProvider.IsValid(element)); // Rebuild the widget collection widgets.Clear(); widgets.AddRange(GetWidgets()); elementWidgets.Clear(); elementWidgets.AddRange(widgets.OfType()); // Normalize and cache the z-ordering var zIndex = 0; foreach (var widget in elementWidgets.OrderBy(widget => widget.zIndex)) { widget.zIndex = zIndex++; } widgetsByAscendingZ.Clear(); // Convoluted way to avoid allocation while sorting var _widgetsByAscendingZ = ListPool.New(); foreach (var widget in widgets) { widget.Reposition(); _widgetsByAscendingZ.Add(widget); } _widgetsByAscendingZ.Sort((a, b) => a.zIndex.CompareTo(b.zIndex)); foreach (var widget in _widgetsByAscendingZ) { widgetsByAscendingZ.Add(widget); } _widgetsByAscendingZ.Free(); // Mark as complete collectionsAreValid = true; } #endregion #region Lifecycle public virtual void Open() { } public virtual void Close() { isLassoing = false; isDragging = false; foreach (var dragged in dragGroup) { if (widgetProvider.IsValid(dragged)) { this.Widget(dragged).EndDrag(); } } dragGroup.Clear(); } public void RegisterControls() { e.RegisterControl(FocusType.Keyboard); foreach (var widget in widgets) { widget.RegisterControl(); } } public void Update() { foreach (var widget in widgets) { widget.Update(); } } public void BeforeFrame() { if (!collectionsAreValid) { // Fetch the list of all widgets CacheWidgetCollections(); } // Update the widgets with the data from their model CacheWidgetItems(); // Send a before frame event to widgets foreach (var widget in widgets) { widget.BeforeFrame(); } // Calculate the positions for models that have been repositioned CacheWidgetPositions(); lastFrameTime = DateTime.Now; } public void OnGUI() { // Cache the mouse position across different windows HandleMouseMovement(); // Determine the hovered widget first to determine if the mouse is over the background DetermineHoveredWidget(); if (e.IsRepaint) { // Cache the list of visible widgets CacheWidgetVisibility(); } // Handle automated edge panning // Needed in repaint too because idling // near the edges should cause a pan too HandleEdgePanning(); if (!e.IsRepaint) { // Handle mouse / keyboard capture HandleEventCapture(); // Handle panning and zoom HandleViewportInput(); // Handle any input that takes priority over widgets HandleHighPriorityInput(); } // Draw the canvas background DrawBackground(); // Draw the widgets DrawWidgetsBackground(); DrawWidgetsForeground(); DrawWidgetsOverlay(); // Draw the canvas overlay DrawOverlay(); // Draw the drag and drop preview if (e.IsRepaint) { dragAndDropHandler?.DrawDragAndDropPreview(); } if (!e.IsRepaint) { // Handle the widgets' input HandleWidgetInput(); // Handle any input that is less important than the widget's HandleLowPriorityInput(); // Handle mouse / keyboard release HandleEventRelease(); } // Update timing for deltas lastEventTime = DateTime.Now; if (e.IsRepaint) { lastRepaintTime = DateTime.Now; } } protected void HandleEventCapture() { foreach (var widget in visibleWidgetsByDescendingZ) { widget.HandleCapture(); } e.HandleCapture(isMouseOverBackground, isMouseOver); } protected virtual void HandleHighPriorityInput() { } protected void HandleWidgetInput() { foreach (var widget in visibleWidgetsByDescendingZ) { widget.HandleInput(); } } protected void HandleEventRelease() { foreach (var widget in widgets) { widget.HandleRelease(); } e.HandleRelease(); } protected virtual void HandleLowPriorityInput() { HandleClipboard(); HandleLassoing(); HandleSelecting(); HandleDeleting(); HandleContext(); HandleDragAndDrop(); HandleMaximization(); } protected virtual void HandleMaximization() { if (e.IsMouseDown(MouseButton.Left) && e.clickCount == 2) { ToggleMaximized(); } } protected void ToggleMaximized() { window.maximized = !window.maximized; GUIUtility.hotControl = 0; GUIUtility.keyboardControl = 0; GUIUtility.ExitGUI(); } #endregion #region Viewing public float zoom { get; private set; } public Vector2 pan { get; private set; } private Rect _viewport; public Rect viewport { get => _viewport; set { if (value == viewport) { return; } _viewport = value; OnViewportChange(); } } private float viewTweenDuration; private Vector2 viewTweenStartPan; private Vector2 viewTweenEndPan; private float viewTweenStartZoom; private float viewTweenEndZoom; private DateTime viewTweenStart; private bool isTweeningView; public Vector2 mousePosition { get; private set; } private void HandleMouseMovement() { if (isMouseOver) { mousePosition = e.mousePosition; } } public bool isMouseOver => viewport.Contains(e.mousePosition); public bool isMouseOverBackground => isMouseOver && hoveredWidget == null; private readonly RectOffset dragPanOffset = new RectOffset(20, 20, 20, 20); private bool isResizing { get { foreach (var widget in widgets) { if ((widget as IGraphElementWidget)?.isResizing ?? false) { return true; } } return false; } } protected virtual bool shouldEdgePan => isDragging || isLassoing || isResizing; private readonly WidgetList widgetsByAscendingZ; private readonly WidgetList visibleWidgetsByAscendingZ; private readonly WidgetList visibleWidgetsByDescendingZ; public void UpdateViewport() { if (isTweeningView) { var t = (float)(DateTime.Now - viewTweenStart).TotalSeconds / viewTweenDuration; if (t >= 1) { isTweeningView = false; } else { pan = Vector2.Lerp(viewTweenStartPan, viewTweenEndPan, t); zoom = Mathf.Lerp(viewTweenStartZoom, viewTweenEndZoom, t); } } else { pan = graph.pan; zoom = graph.zoom; } } public void TweenViewport(Vector2 pan, float zoom, float duration) { if (context == null) { return; } isTweeningView = true; viewTweenDuration = duration; viewTweenStartPan = this.pan; viewTweenStartZoom = this.zoom; viewTweenEndPan = pan; viewTweenEndZoom = zoom; viewTweenStart = DateTime.Now; graph.pan = pan; graph.zoom = zoom; } public void CacheWidgetVisibility() { visibleWidgetsByAscendingZ.Clear(); visibleWidgetsByDescendingZ.Clear(); for (int i = 0; i < widgetsByAscendingZ.Count; i++) { var ascending = widgetsByAscendingZ[i]; var descending = widgetsByAscendingZ[widgetsByAscendingZ.Count - i - 1]; ascending.isVisible = IsVisible(ascending); descending.isVisible = IsVisible(descending); if (ascending.isVisible) { visibleWidgetsByAscendingZ.Add(ascending); } if (descending.isVisible) { visibleWidgetsByDescendingZ.Add(descending); } } } private void OnViewportChange() { foreach (var widget in widgets) { widget.OnViewportChange(); } } private void HandleViewportInput() { if (isTweeningView) { return; } var controlScheme = BoltCore.Configuration.controlScheme; if (isMouseOver && e.controlType == EventType.ScrollWheel) { bool zooming; bool scrolling; if (controlScheme == CanvasControlScheme.Default) { zooming = e.ctrlOrCmd; scrolling = !zooming; } else if (controlScheme == CanvasControlScheme.Alternate) { zooming = true; scrolling = false; } else { throw new UnexpectedEnumValueException(controlScheme); } if (zooming) { var zoomDelta = MathfEx.NearestMultiple(-e.mouseDelta.y * BoltCore.Configuration.zoomSpeed, GraphGUI.ZoomSteps); zoomDelta = Mathf.Clamp(graph.zoom + zoomDelta, GraphGUI.MinZoom, GraphGUI.MaxZoom) - graph.zoom; if (zoomDelta != 0) { var oldZoom = graph.zoom; var newZoom = graph.zoom + zoomDelta; var matrix = MathfEx.ScaleAroundPivot(mousePosition, (oldZoom / newZoom) * Vector3.one); graph.pan = matrix.MultiplyPoint(graph.pan); graph.zoom = newZoom; } HotkeyUsageAnalytics.HotkeyUsed(HotkeyUsageAnalytics.Hotkey.Zoom); e.Use(); } if (scrolling) { var panDelta = e.mouseDelta * BoltCore.Configuration.panSpeed; if (panDelta.magnitude != 0) { graph.pan += panDelta; } HotkeyUsageAnalytics.HotkeyUsed(HotkeyUsageAnalytics.Hotkey.Scroll); e.Use(); } } if (e.controlType == EventType.MouseDown || e.controlType == EventType.MouseDrag) { bool panning; if (controlScheme == CanvasControlScheme.Default) { panning = e.mouseButton == MouseButton.Middle; } else if (controlScheme == CanvasControlScheme.Alternate) { panning = (e.mouseButton == MouseButton.Middle) || (e.alt && e.mouseButton == (int)MouseButton.Left); } else { throw new UnexpectedEnumValueException(controlScheme); } if (panning) { var panDelta = -e.mouseDelta; if (panDelta.magnitude != 0) { graph.pan += panDelta; } if (e.mouseButton == MouseButton.Middle) HotkeyUsageAnalytics.HotkeyUsed(HotkeyUsageAnalytics.Hotkey.PanMmb); if (e.alt && e.mouseButton == (int)MouseButton.Left) HotkeyUsageAnalytics.HotkeyUsed(HotkeyUsageAnalytics.Hotkey.PanAltLmb); e.Use(); } } if (e.IsKeyDown(KeyCode.Home)) { ViewSelection(); HotkeyUsageAnalytics.HotkeyUsed(HotkeyUsageAnalytics.Hotkey.Home); e.Use(); } } private void HandleEdgePanning() { if (isTweeningView || !window.IsFocused() || !shouldEdgePan) { return; } var mousePosition = e.mousePosition; var dragSpeedMultiplier = 120 * BoltCore.Configuration.dragPanSpeed; var dragLeftSpeed = 0f; var dragRightSpeed = 0f; var dragTopSpeed = 0f; var dragBottomSpeed = 0f; if (mousePosition.x < viewport.x) { dragLeftSpeed = 3; } else if (mousePosition.x < viewport.x + dragPanOffset.left) { dragLeftSpeed = 1; } if (mousePosition.x > viewport.xMax) { dragRightSpeed = 3; } else if (mousePosition.x > viewport.xMax - dragPanOffset.right) { dragRightSpeed = 1; } if (mousePosition.y < viewport.y) { dragTopSpeed = 3; } else if (mousePosition.y < viewport.y + dragPanOffset.top) { dragTopSpeed = 1; } if (mousePosition.y > viewport.yMax) { dragBottomSpeed = 3; } else if (mousePosition.y > viewport.yMax - dragPanOffset.bottom) { dragBottomSpeed = 1; } var delta = new Vector2(dragRightSpeed - dragLeftSpeed, dragBottomSpeed - dragTopSpeed) * eventDeltaTime * dragSpeedMultiplier; graph.pan += delta; if (isDragging) { foreach (var draggedElement in dragGroup) { using (LudiqGUI.matrix.Override(Matrix4x4.identity)) { this.Widget(draggedElement).CachePosition(); } this.Widget(draggedElement).Drag(delta, dragConstraint); } } } private void Overview() { ViewElements(graph.elements); } private void ViewSelection() { ViewElements(selection.Count > 0 ? (IEnumerable)selection : graph.elements); } public void ViewElements(IEnumerable elements) { var view = GraphGUI.CalculateArea(elements.Select(e => this.Widget(e))); var pan = view.center; var padding = 50; var zoomX = (viewport.width * graph.zoom) / (view.width + padding); var zoomY = (viewport.height * graph.zoom) / (view.height + padding); var zoom = Mathf.Clamp(Mathf.Min(zoomX, zoomY), GraphGUI.MinZoom, GraphGUI.MaxZoom); TweenViewport(pan, zoom, BoltCore.Configuration.overviewSmoothing); } public bool IsVisible(IWidget widget) { return !widget.canClip || viewport.Overlaps(widget.clippingPosition); } #endregion #region Positioning private readonly List widgetsToReposition = new List(); public void CacheWidgetPositions() { widgetsToReposition.Clear(); widgetsToReposition.AddRange(widgets.Where(widget => !widget.isPositionValid).OrderByDependencies(widget => widget.positionDependencies)); using (LudiqGUI.matrix.Override(Matrix4x4.identity)) { foreach (var widget in widgetsToReposition) { widget.CachePositionFirstPass(); } foreach (var widget in widgetsToReposition) { widget.CachePosition(); widget.isPositionValid = true; } } } #endregion #region Hovering public IWidget hoveredWidget { get; private set; } private void DetermineHoveredWidget() { hoveredWidget = null; foreach (var widget in visibleWidgetsByDescendingZ) { if (widget.isMouseThrough) { hoveredWidget = widget; break; } } } #endregion #region Lassoing private Vector2 lassoOrigin; public bool isLassoing { get; private set; } public Rect lassoArea { get { return new Rect() { xMin = Mathf.Min(lassoOrigin.x, mousePosition.x), xMax = Mathf.Max(lassoOrigin.x, mousePosition.x), yMin = Mathf.Min(lassoOrigin.y, mousePosition.y), yMax = Mathf.Max(lassoOrigin.y, mousePosition.y), }; } } private void HandleLassoing() { if (e.IsMouseDrag(MouseButton.Left)) { if (!isLassoing) { lassoOrigin = mousePosition; isLassoing = true; } } else if (e.IsMouseUp(MouseButton.Left)) { if (isSelecting) { isLassoing = false; } if (isGrouping) { isLassoing = false; var group = new GraphGroup() { position = groupArea }; UndoUtility.RecordEditedObject("Create Graph Group"); graph.elements.Add(group); selection.Select(group); this.Widget(group).FocusLabel(); GUI.changed = true; } } } #endregion #region Selecting public bool isSelecting => isLassoing && !isGrouping; public Rect selectionArea => lassoArea; private void HandleSelecting() { if (e.IsValidateCommand("SelectAll")) { e.ValidateCommand(); } else if (e.IsExecuteCommand("SelectAll")) { HotkeyUsageAnalytics.HotkeyUsed(HotkeyUsageAnalytics.Hotkey.SelectAll); selection.Select(elementWidgets.Where(widget => widget.canSelect).Select(widget => widget.element)); e.Use(); } else if (e.IsMouseDown(MouseButton.Left)) { selection.Clear(); } else if (e.IsMouseDrag(MouseButton.Left)) { if (isSelecting) { // In selection mode, include any widget that overlaps the lasso selection.Select(elementWidgets.Where(widget => widget.canSelect && selectionArea.Overlaps(widget.hotArea)).Select(widget => widget.element)); e.Use(); } else if (isGrouping) { // In grouping mode, only inlude widgets that are encompassed by the lasso selection.Select(elementWidgets.Where(widget => widget.canSelect && groupArea.Encompasses(widget.position)).Select(widget => widget.element)); e.Use(); } } } #endregion #region Grouping public bool isGrouping => isLassoing && (e.ctrlOrCmd && graph.elements.Includes()); public Rect groupArea { get { var groupBackground = GraphGroupWidget.Styles.group.normal.background; var groupRect = selectionArea; groupRect.y -= GraphGroupWidget.Styles.headerHeight; groupRect.height += GraphGroupWidget.Styles.headerHeight; if (groupRect.width < groupBackground.width) { groupRect.width = groupBackground.width; } if (groupRect.height < groupBackground.height) { groupRect.height = groupBackground.height; } return groupRect; } } #endregion #region Dragging private static readonly Vector2 HorizontalDrag = new Vector2(1, 0); private static readonly Vector2 VerticalDrag = new Vector2(0, 1); private static readonly Vector2 FreeDrag = new Vector2(1, 1); private Vector2 dragConstraint = FreeDrag; public bool isDragging { get; private set; } private readonly HashSet dragGroup = new HashSet(); public void BeginDrag(EventWrapper e) { if (isDragging) { return; } UndoUtility.RecordEditedObject("Drag Graph Elements"); isDragging = true; dragGroup.Clear(); dragGroup.UnionWith(selection); foreach (var selected in selection) { this.Widget(selected).ExpandDragGroup(dragGroup); } LockDragOrigin(); foreach (var dragged in dragGroup) { this.Widget(dragged).BeginDrag(); } e.Use(); } public void Drag(EventWrapper e) { if (!isDragging) { return; } if (e.shift) { if (dragConstraint == FreeDrag) { LockDragOrigin(); if (Mathf.Abs(e.mouseDelta.x) > Mathf.Abs(e.mouseDelta.y)) { dragConstraint = HorizontalDrag; } else if (Mathf.Abs(e.mouseDelta.y) > Mathf.Abs(e.mouseDelta.x)) { dragConstraint = VerticalDrag; } } } else { dragConstraint = FreeDrag; } foreach (var dragged in dragGroup) { this.Widget(dragged).Drag(e.mouseDelta, dragConstraint); } e.Use(); } public void EndDrag(EventWrapper e) { if (!isDragging) { return; } UndoUtility.RecordEditedObject("Drag Graph Elements"); isDragging = false; foreach (var dragged in dragGroup) { this.Widget(dragged).EndDrag(); } dragGroup.Clear(); dragConstraint = FreeDrag; e.Use(); } private void LockDragOrigin() { foreach (var dragged in dragGroup) { this.Widget(dragged).LockDragOrigin(); } } #endregion #region Deleting public void DeleteSelection() { var deleted = false; var deleteGroup = new HashSet(); foreach (var element in selection) { deleteGroup.Add(element); this.Widget(element).ExpandDeleteGroup(deleteGroup); } foreach (var element in deleteGroup.OrderByDescending(e => e.dependencyOrder)) { if (this.Widget(element).canDelete) { UndoUtility.RecordEditedObject("Delete Graph Element"); graph.elements.Remove(element); selection.Remove(element); deleted = true; } } if (deleted) { GUI.changed = true; e.TryUse(); } } private void HandleDeleting() { if (e.IsValidateCommand("Delete")) { if (selection.Count > 0) { e.ValidateCommand(); } } else if (e.IsExecuteCommand("Delete")) { HotkeyUsageAnalytics.HotkeyUsed(HotkeyUsageAnalytics.Hotkey.Delete); DeleteSelection(); } else if (e.IsKeyDown(KeyCode.Delete)) { DeleteSelection(); } } #endregion #region Clipboard public virtual void ShrinkCopyGroup(HashSet copyGroup) { } private DateTime lastPasteTime; private void HandleClipboard() { if (e.IsValidateCommand("Copy")) { if (GraphClipboard.canCopySelection) { e.ValidateCommand(); } } else if (e.IsExecuteCommand("Copy")) { HotkeyUsageAnalytics.HotkeyUsed(HotkeyUsageAnalytics.Hotkey.Copy); GraphClipboard.CopySelection(); } if (e.IsValidateCommand("Cut")) { if (GraphClipboard.canCopySelection) { e.ValidateCommand(); } } else if (e.IsExecuteCommand("Cut")) { HotkeyUsageAnalytics.HotkeyUsed(HotkeyUsageAnalytics.Hotkey.Cut); GraphClipboard.CutSelection(); } if (e.IsValidateCommand("Paste")) { if (GraphClipboard.canPaste && (DateTime.Now - lastPasteTime).TotalSeconds >= 0.25) { e.ValidateCommand(); } } else if (e.IsExecuteCommand("Paste")) { HotkeyUsageAnalytics.HotkeyUsed(HotkeyUsageAnalytics.Hotkey.Paste); GraphClipboard.Paste(); lastPasteTime = DateTime.Now; } if (e.IsValidateCommand("Duplicate")) { if (GraphClipboard.canDuplicateSelection && (DateTime.Now - lastPasteTime).TotalSeconds >= 0.25) { e.Use(); } } else if (e.IsExecuteCommand("Duplicate")) { HotkeyUsageAnalytics.HotkeyUsed(HotkeyUsageAnalytics.Hotkey.Duplicate); GraphClipboard.DuplicateSelection(); lastPasteTime = DateTime.Now; } } #endregion #region Context protected IEnumerable GetExtendedContextOptions() { foreach (var item in context.extensions.SelectMany(extension => extension.contextMenuItems).OrderBy(item => item.label)) { yield return new DropdownOption(item.action, item.label); } } protected virtual IEnumerable GetContextOptions() { foreach (var extendedOption in GetExtendedContextOptions()) { yield return extendedOption; } if (GraphClipboard.canCopySelection) { yield return new DropdownOption((Action)GraphClipboard.CopySelection, "Copy Selection"); yield return new DropdownOption((Action)GraphClipboard.CutSelection, "Cut Selection"); } if (GraphClipboard.canDuplicateSelection) { yield return new DropdownOption((Action)GraphClipboard.DuplicateSelection, "Duplicate Selection"); } if (selection.Count > 0) { yield return new DropdownOption((Action)DeleteSelection, "Delete Selection"); } if (GraphClipboard.canPasteOutside) { yield return new DropdownOption((Action)((position) => GraphClipboard.PasteOutside(true, position)), "Paste"); } } protected virtual void OnContext() { var contextOptions = this.GetContextOptions().ToArray(); delayCall += () => { var _mousePosition = mousePosition; LudiqGUI.Dropdown ( e.mousePosition, delegate (object _action) { delayCall += () => { if (_action is Action action) { action.Invoke(); } else if (_action is Action positionedAction) { positionedAction.Invoke(_mousePosition); } }; }, contextOptions, null ); }; } private void HandleContext() { if (e.IsContextClick) { OnContext(); e.Use(); } } #endregion #region Layout public IEnumerable alignableAndDistributable { get { // [BOLT-1112] // Filter elements with a null graph reference as deserialization may occurs while parsing the list. // After deserialization elements are cleaned up before rebuilding the graph. // see Graph.OnAfterDependenciesDeserialized return selection .Where(element => element.graph != null) .Select(this.Widget) .Where(element => element.canAlignAndDistribute); } } public void Align(AlignOperation operation) { UndoUtility.RecordEditedObject("Align Graph Elements"); var alignable = alignableAndDistributable; var area = GraphGUI.CalculateArea(alignable); foreach (var widget in alignable) { var position = widget.position; var width = position.width; var height = position.height; switch (operation) { case AlignOperation.AlignLeftEdges: position.xMin = area.xMin; break; case AlignOperation.AlignCenters: position.xMin = area.xMin + (area.width / 2) - (width / 2); break; case AlignOperation.AlignRightEdges: position.xMin = area.xMax - width; break; case AlignOperation.AlignTopEdges: position.yMin = area.yMin; break; case AlignOperation.AlignMiddles: position.yMin = area.yMin + (area.height / 2) - (height / 2); break; case AlignOperation.AlignBottomEdges: position.yMin = area.yMax - height; break; } position.width = width; position.height = height; widget.position = position.PixelPerfect(); widget.Reposition(); } } public void Distribute(DistributeOperation operation) { UndoUtility.RecordEditedObject("Distribute Graph Elements"); var distributable = alignableAndDistributable; switch (operation) { case DistributeOperation.DistributeLeftEdges: distributable = distributable.OrderBy(editor => editor.position.xMin); break; case DistributeOperation.DistributeCenters: distributable = distributable.OrderBy(editor => editor.position.center.x); break; case DistributeOperation.DistributeRightEdges: distributable = distributable.OrderBy(editor => editor.position.xMax); break; case DistributeOperation.DistributeHorizontalGaps: distributable = distributable.OrderBy(editor => editor.position.xMin); break; case DistributeOperation.DistributeTopEdges: distributable = distributable.OrderBy(editor => editor.position.yMin); break; case DistributeOperation.DistributeMiddles: distributable = distributable.OrderBy(editor => editor.position.center.y); break; case DistributeOperation.DistributeBottomEdges: distributable = distributable.OrderBy(editor => editor.position.yMax); break; case DistributeOperation.DistributeVerticalGaps: distributable = distributable.OrderBy(editor => editor.position.yMin); break; } var index = 0; var count = distributable.Count(); var first = distributable.First(); var last = distributable.Last(); var currentX = first.position.xMin; var currentY = first.position.yMin; var innerTotalWidth = distributable.Skip(1).Take(count - 2).Sum(innerEditor => innerEditor.position.width); var innerTotalHeight = distributable.Skip(1).Take(count - 2).Sum(innerEditor => innerEditor.position.height); var horizontalGap = ((last.position.xMin - first.position.xMax) - innerTotalWidth) / (count - 1); var verticalGap = ((last.position.yMin - first.position.yMax) - innerTotalHeight) / (count - 1); foreach (var widget in distributable) { var ratio = (float)index / (count - 1); var position = widget.position; var width = position.width; var height = position.height; switch (operation) { case DistributeOperation.DistributeLeftEdges: position.xMin = first.position.xMin + (ratio * (last.position.xMin - first.position.xMin)); position.xMax = position.xMin + width; break; case DistributeOperation.DistributeCenters: position.xMin = first.position.center.x + (ratio * (last.position.center.x - first.position.center.x)) - (width / 2); position.xMax = position.xMin + width; break; case DistributeOperation.DistributeRightEdges: position.xMax = last.position.xMax - ((1 - ratio) * (last.position.xMax - first.position.xMax)); position.xMin = position.xMax - width; break; case DistributeOperation.DistributeHorizontalGaps: position.xMin = currentX; position.xMax = position.xMin + width; currentX = position.xMax + horizontalGap; break; case DistributeOperation.DistributeTopEdges: position.yMin = first.position.yMin + (ratio * (last.position.yMin - first.position.yMin)); position.yMax = position.yMin + height; break; case DistributeOperation.DistributeMiddles: position.yMin = first.position.center.y + (ratio * (last.position.center.y - first.position.center.y)) - (height / 2); position.yMax = position.yMin + height; break; case DistributeOperation.DistributeBottomEdges: position.yMax = last.position.yMax - ((1 - ratio) * (last.position.yMax - first.position.yMax)); position.yMin = position.yMax - height; break; case DistributeOperation.DistributeVerticalGaps: position.yMin = currentY; position.yMax = position.yMin + height; currentY = position.yMax + verticalGap; break; } widget.position = position.PixelPerfect(); widget.Reposition(); index++; } } #endregion #region Drawing protected virtual void DrawBackground() { if (BoltCore.Configuration.developerMode && BoltCore.Configuration.debug) { if (e.controlsMouse && e.controlsKeyboard) { EditorGUI.DrawRect(viewport, Color.cyan.WithAlpha(0.25f)); } else if (e.controlsMouse) { EditorGUI.DrawRect(viewport, Color.green.WithAlpha(0.25f)); } else if (e.controlsKeyboard) { EditorGUI.DrawRect(viewport, Color.blue.WithAlpha(0.25f)); } } } protected void DrawWidgetsBackground() { foreach (var widget in visibleWidgetsByAscendingZ) { if (e.IsRepaint || widget.backgroundRequiresInput) { widget.DrawBackground(); } } } protected void DrawWidgetsForeground() { foreach (var widget in visibleWidgetsByAscendingZ) { if (e.IsRepaint || widget.foregroundRequiresInput) { widget.DrawForeground(); } } } protected void DrawWidgetsOverlay() { foreach (var widget in visibleWidgetsByAscendingZ) { if (e.IsRepaint || widget.overlayRequiresInput) { widget.DrawOverlay(); } } } protected virtual void DrawOverlay() { if (isSelecting) { if (e.IsRepaint) { ((GUIStyle)"SelectionRect").Draw(selectionArea, false, false, false, false); } } if (isGrouping) { if (e.IsRepaint) { using (LudiqGUI.color.Override(GraphGroupWidget.Styles.AdjustColor(GraphGroup.defaultColor, true))) { GraphGroupWidget.Styles.group.Draw(groupArea, false, false, true, false); } } } if (BoltCore.Configuration.developerMode && BoltCore.Configuration.debug) { EditorGUI.DrawRect(new Rect(graph.pan.x, graph.pan.y - 2, 1, 5), Color.white); EditorGUI.DrawRect(new Rect(graph.pan.x - 2, graph.pan.y, 5, 1), Color.white); GUI.Label(new Rect(graph.pan, new Vector2(300, 48)), graph.pan.ToString("0"), EditorStyles.whiteLabel); GUI.Label(new Rect(mousePosition + new Vector2(16, 0), new Vector2(300, 16)), mousePosition.ToString("0"), EditorStyles.whiteLabel); GUI.Label(new Rect(mousePosition + new Vector2(16, 16), new Vector2(300, 16)), GUIUtility.GUIToScreenPoint(mousePosition).ToString("0"), EditorStyles.whiteLabel); GUI.Label(new Rect(mousePosition + new Vector2(16, 32), new Vector2(300, 16)), $"Widgets: {widgets.Where(IsVisible).Count()} / {widgets.Count()}", EditorStyles.whiteLabel); if (selection.Count > 0) { var selectionRect = GraphGUI.CalculateArea(selection.Select(e => this.Widget(e))); LudiqGUI.DrawEmptyRect(selectionRect, Color.cyan.WithAlpha(0.5f)); GUI.Label(new Rect(selectionRect.center, new Vector2(300, 16)), selectionRect.center.ToString(), EditorStyles.whiteLabel); } } } #endregion #region Drag & Drop private IEnumerable potentialDragAndDropHandlers { get { if (hoveredWidget != null && hoveredWidget is IDragAndDropHandler) { yield return (IDragAndDropHandler)hoveredWidget; } yield return this; foreach (var extension in context.extensions) { yield return extension; } } } private IDragAndDropHandler dragAndDropHandler { get; set; } private void HandleDragAndDrop() { if (e.controlType == EventType.DragPerform || e.controlType == EventType.DragUpdated) { foreach (var potentialDragAndDropHandler in potentialDragAndDropHandlers) { if (potentialDragAndDropHandler.AcceptsDragAndDrop()) { dragAndDropHandler = potentialDragAndDropHandler; break; } } if (dragAndDropHandler != null) { DragAndDrop.visualMode = dragAndDropHandler.dragAndDropVisualMode; if (e.controlType == EventType.DragPerform) { dragAndDropHandler.PerformDragAndDrop(); // Don't wait another frame (shows visually), exit right away dragAndDropHandler.ExitDragAndDrop(); dragAndDropHandler = null; } else if (e.controlType == EventType.DragUpdated) { dragAndDropHandler.UpdateDragAndDrop(); } } else { DragAndDrop.visualMode = DragAndDropVisualMode.Rejected; } e.Use(); } if (e.controlType == EventType.DragExited && dragAndDropHandler != null) { dragAndDropHandler.ExitDragAndDrop(); dragAndDropHandler = null; e.Use(); } } public virtual DragAndDropVisualMode dragAndDropVisualMode => DragAndDropVisualMode.Generic; public virtual bool AcceptsDragAndDrop() { return false; } public virtual void PerformDragAndDrop() { } public virtual void UpdateDragAndDrop() { } public virtual void DrawDragAndDropPreview() { } public virtual void ExitDragAndDrop() { } #endregion #region Timing private DateTime lastFrameTime; private DateTime lastEventTime; private DateTime lastRepaintTime; [DoNotSerialize] public float frameDeltaTime => (float)(DateTime.Now - lastFrameTime).TotalSeconds; [DoNotSerialize] public float eventDeltaTime => (float)(DateTime.Now - lastEventTime).TotalSeconds; [DoNotSerialize] public float repaintDeltaTime => (float)(DateTime.Now - lastRepaintTime).TotalSeconds; #endregion #region Window public virtual void OnToolbarGUI() { EditorGUI.BeginChangeCheck(); BoltCore.Configuration.carryChildren = GUILayout.Toggle(BoltCore.Configuration.carryChildren, "Carry", LudiqStyles.toolbarButton); if (EditorGUI.EndChangeCheck()) { BoltCore.Configuration.Save(); } EditorGUI.BeginDisabledGroup(alignableAndDistributable.Count() < 2); var alignPosition = GUILayoutUtility.GetRect(new GUIContent("Align"), LudiqStyles.toolbarPopup); if (GUI.Button(alignPosition, "Align", LudiqStyles.toolbarPopup)) { LudiqGUI.FuzzyDropdown ( alignPosition, EnumOptionTree.For(), (object)null, (operation) => Align((AlignOperation)operation) ); } var distributePosition = GUILayoutUtility.GetRect(new GUIContent("Distribute"), LudiqStyles.toolbarPopup); if (GUI.Button(distributePosition, "Distribute", LudiqStyles.toolbarPopup)) { LudiqGUI.FuzzyDropdown ( distributePosition, EnumOptionTree.For(), (object)null, (operation) => Distribute((DistributeOperation)operation) ); } EditorGUI.EndDisabledGroup(); if (GUILayout.Button("Overview", LudiqStyles.toolbarButton)) { Overview(); } if (GUILayout.Toggle(window.maximized, "Full Screen", LudiqStyles.toolbarButton) != window.maximized) { ToggleMaximized(); } if (BoltCore.Configuration.developerMode) { EditorGUI.BeginChangeCheck(); BoltCore.Configuration.debug = GUILayout.Toggle(BoltCore.Configuration.debug, "Debug", LudiqStyles.toolbarButton); if (EditorGUI.EndChangeCheck()) { BoltCore.Configuration.Save(); } } } public Queue delayedCalls { get; } = new Queue(); public event Action delayCall { add { lock (delayedCalls) { delayedCalls.Enqueue(value); } } remove { } } #endregion } }