#nullable enable using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; namespace Barotrauma { internal sealed class CircuitBoxUI { private readonly Camera camera; private static readonly Vector2 gridSize = new Vector2(128f); public readonly CircuitBox CircuitBox; private bool componentMenuOpen; private float componentMenuOpenState; private GUICustomComponent? circuitComponent; private GUIFrame? componentMenu; private GUIButton? toggleMenuButton; private GUIFrame? selectedWireFrame; private GUIListBox? componentList; private GUITextBlock? inventoryIndicatorText; private readonly Sprite? cursorSprite = GUIStyle.CursorSprite[CursorState.Default]; private Option selection = Option.None; private string searchTerm = string.Empty; public static Option DraggedWire = Option.None; public readonly CircuitBoxMouseDragSnapshotHandler MouseSnapshotHandler; public List VirtualWires = new(); public CircuitBoxUI(CircuitBox box) { camera = new Camera { MinZoom = 0.25f, MaxZoom = 2f }; CircuitBox = box; MouseSnapshotHandler = new CircuitBoxMouseDragSnapshotHandler(this); } #region UI public void CreateGUI(GUIFrame parent) { GUIFrame paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.97f, 0.95f), parent.RectTransform, Anchor.Center), style: null); circuitComponent = new GUICustomComponent(new RectTransform(Vector2.One, paddedFrame.RectTransform), onDraw: (spriteBatch, component) => { Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = component.Rect; spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable, transformMatrix: camera.Transform); DrawCircuits(spriteBatch); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); DrawHUD(spriteBatch); spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); }); GUIScissorComponent menuContainer = new GUIScissorComponent(new RectTransform(Vector2.One, paddedFrame.RectTransform, anchor: Anchor.Center)) { CanBeFocused = false }; componentMenuOpen = true; componentMenu = new GUIFrame(new RectTransform(new Vector2(1f, 0.4f), menuContainer.Content.RectTransform, Anchor.BottomRight)); toggleMenuButton = new GUIButton(new RectTransform(new Point(300, 30), GUI.Canvas) { MinSize = new Point(0, 15) }, style: "UIToggleButtonVertical") { OnClicked = (btn, userdata) => { componentMenuOpen = !componentMenuOpen; foreach (GUIComponent child in btn.Children) { child.SpriteEffects = componentMenuOpen ? SpriteEffects.None : SpriteEffects.FlipVertically; } return true; } }; GUILayoutGroup menuLayout = new GUILayoutGroup(new RectTransform(Vector2.One, componentMenu.RectTransform), childAnchor: Anchor.TopCenter) { RelativeSpacing = 0.02f }; GUILayoutGroup headerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), menuLayout.RectTransform), isHorizontal: true); GUILayoutGroup labelLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1f), headerLayout.RectTransform), isHorizontal: true); GUILayoutGroup searchBarLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1f), headerLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true); GUITextBlock searchBarLabel = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1f), searchBarLayout.RectTransform), "Filter"); GUITextBox searchbar = new GUITextBox(new RectTransform(new Vector2(0.85f, 1f), searchBarLayout.RectTransform), string.Empty, createClearButton: true); new GUIFrame(new RectTransform(new Vector2(0.5f, 0.01f), menuLayout.RectTransform), style: "HorizontalLine"); componentList = new GUIListBox(new RectTransform(new Vector2(0.95f, 0.65f), menuLayout.RectTransform)) { PlaySoundOnSelect = true, UseGridLayout = true, OnSelected = (_, o) => { if (o is not ItemPrefab prefab) { return false; } CircuitBox.HeldComponent = Option.Some(prefab); return true; } }; GUILayoutGroup inventoryLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1f), headerLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.Center); GUILayoutGroup indicatorLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.2f, 1f), inventoryLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); GUIImage indicatorIcon = new GUIImage(new RectTransform(new Vector2(0.5f, 0.8f), indicatorLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CircuitIndicatorIcon"); inventoryIndicatorText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), indicatorLayout.RectTransform), GetInventoryText(), font: GUIStyle.SubHeadingFont); int gapSize = GUI.IntScale(8); selectedWireFrame = SubEditorScreen.CreateWiringPanel(Point.Zero, SelectWire); selectedWireFrame.RectTransform.AbsoluteOffset = new Point(parent.Rect.X - (selectedWireFrame.Rect.Width + gapSize), parent.Rect.Y); foreach (ItemPrefab prefab in ItemPrefab.Prefabs.OrderBy(static p => p.Name)) { if (!prefab.Tags.Contains("circuitboxcomponent")) { continue; } CreateComponentElement(prefab, componentList.Content.RectTransform); } searchbar.OnTextChanged += (tb, s) => { searchTerm = s; UpdateComponentList(); return true; }; int buttonHeight = (int)(GUIStyle.ItemFrameMargin.Y * 0.4f); var settingsIcon = new GUIButton(new RectTransform(new Point(buttonHeight), parent.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(buttonHeight / 4), MinSize = new Point(buttonHeight) }, style: "GUIButtonSettings") { OnClicked = (btn, userdata) => { GUIContextMenu.CreateContextMenu( new ContextMenuOption("circuitboxsetting.resetview", isEnabled: true, onSelected: ResetCamera) { Tooltip = TextManager.Get("circuitboxsettingdescription.resetview") }, new ContextMenuOption("circuitboxsetting.find", isEnabled: true, new ContextMenuOption("circuitboxsetting.focusinput", isEnabled: true, onSelected: () => FindInputOuput(CircuitBoxInputOutputNode.Type.Input)) { Tooltip = TextManager.Get("circuitboxsettingdescription.focusinput") }, new ContextMenuOption("circuitboxsetting.focusoutput", isEnabled: true, onSelected: () => FindInputOuput(CircuitBoxInputOutputNode.Type.Output)) { Tooltip = TextManager.Get("circuitboxsettingdescription.focusoutput") }, new ContextMenuOption("circuitboxsetting.focuscircuits", isEnabled: CircuitBox.Components.Any(), onSelected: FindCircuit) { Tooltip = TextManager.Get("circuitboxsettingdescription.focuscircuits") })); void ResetCamera() { // Vector2.One because Vector2.Zero means no value camera.TargetPos = Vector2.One; } void FindInputOuput(CircuitBoxInputOutputNode.Type type) { var input = CircuitBox.InputOutputNodes.FirstOrDefault(n => n.NodeType == type); if (input is null) { return; } camera.TargetPos = input.Position; } void FindCircuit() { var closestComponent = CircuitBox.Components.MinBy(c => Vector2.DistanceSquared(c.Position, camera.Position)); if (closestComponent is null) { return; } camera.TargetPos = closestComponent.Position; } return true; } }; MouseSnapshotHandler.UpdateConnections(); // update scales of everything foreach (var node in CircuitBox.Components) { node.OnUICreated(); } foreach (var node in CircuitBox.InputOutputNodes) { node.OnUICreated(); } foreach (var wire in CircuitBox.Wires) { wire.Update(); } } private string GetInventoryText() => CircuitBox.ComponentContainer is { } container ? $"{container.Inventory.AllItems.Count()}/{container.Capacity}" : "0/0"; public void UpdateComponentList() { if (inventoryIndicatorText is { } text) { text.Text = GetInventoryText(); } if (componentList is null) { return; } var playerInventory = CircuitBox.GetSortedCircuitBoxSortedItemsFromPlayer(Character.Controlled); foreach (GUIComponent child in componentList.Content.Children) { if (child.UserData is not ItemPrefab prefab) { continue; } child.Enabled = !CircuitBox.IsFull && (!CircuitBox.IsInGame() || CircuitBox.GetApplicableResourcePlayerHas(prefab, playerInventory).IsSome()); if (child.GetChild()?.GetChild() is { } image) { image.Enabled = child.Enabled; } child.ToolTip = child.Enabled ? prefab.Description : RichString.Rich(TextManager.GetWithVariable(new Identifier("CircuitBoxUIComponentNotAvailable"), new Identifier("[item]"), prefab.Name)); if (string.IsNullOrWhiteSpace(searchTerm)) { child.Visible = true; continue; } child.Visible = prefab.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase); } } private static bool SelectWire(GUIComponent component, object obj) { if (obj is not ItemPrefab prefab) { return false; } CircuitBoxWire.SelectedWirePrefab = prefab; return true; } private static void CreateComponentElement(ItemPrefab prefab, RectTransform parent) { GUIFrame itemFrame = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.9f), parent) { MinSize = new Point(0, 50) }, style: "GUITextBox") { UserData = prefab }; itemFrame.RectTransform.MinSize = new Point(0, itemFrame.Rect.Width); itemFrame.RectTransform.MaxSize = new Point(int.MaxValue, itemFrame.Rect.Width); itemFrame.ToolTip = prefab.Name; GUILayoutGroup paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), itemFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { Stretch = true, RelativeSpacing = 0.03f, CanBeFocused = false }; Sprite icon; Color iconColor; if (prefab.InventoryIcon != null) { icon = prefab.InventoryIcon; iconColor = prefab.InventoryIconColor; } else { icon = prefab.Sprite; iconColor = prefab.SpriteColor; } GUIImage? img = null; if (icon != null) { img = new GUIImage(new RectTransform(new Vector2(1.0f, 0.8f), paddedFrame.RectTransform, Anchor.TopCenter), icon) { CanBeFocused = false, LoadAsynchronously = true, DisabledColor = Color.DarkGray * 0.8f, Color = iconColor }; } GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter), text: prefab.Name, textAlignment: Alignment.Center, font: GUIStyle.SmallFont) { CanBeFocused = false }; textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width); paddedFrame.Recalculate(); if (img != null) { img.Scale = Math.Min(Math.Min(img.Rect.Width / img.Sprite.size.X, img.Rect.Height / img.Sprite.size.Y), 1.5f); img.RectTransform.NonScaledSize = new Point((int)(img.Sprite.size.X * img.Scale), img.Rect.Height); } } #endregion private void DrawHUD(SpriteBatch spriteBatch) { float scale = GUI.Scale / 1.5f; Vector2 offset = new Vector2(20, 40) * scale; foreach (var (character, cursor) in CircuitBox.ActiveCursors) { if (!cursor.IsActive) { continue; } Vector2 cursorWorldPos = camera.WorldToScreen(cursor.DrawPosition); if (cursor.Info.DragStart.TryUnwrap(out Vector2 dragStart)) { DrawSelection(spriteBatch, dragStart, cursor.DrawPosition, cursor.Color); } if (cursor.HeldPrefab.TryUnwrap(out ItemPrefab? otherHeldPrefab)) { otherHeldPrefab.Sprite.Draw(spriteBatch, cursorWorldPos); } cursorSprite?.Draw(spriteBatch, cursorWorldPos, cursor.Color, 0f, scale); GUI.DrawString(spriteBatch, cursorWorldPos + offset, character.Name, cursor.Color, Color.Black, GUI.IntScale(4), GUIStyle.SmallFont); } if (selection.TryUnwrap(out RectangleF rect)) { Vector2 pos1 = rect.Location; Vector2 pos2 = new Vector2(rect.Location.X + rect.Size.X, rect.Location.Y + rect.Size.Y); DrawSelection(spriteBatch, pos1, pos2, GUIStyle.Blue); } if (CircuitBox.HeldComponent.TryUnwrap(out ItemPrefab? component)) { component.Sprite.Draw(spriteBatch, PlayerInput.MousePosition); } foreach (var c in CircuitBox.Components) { c.DrawHUD(spriteBatch, camera); } foreach (var n in CircuitBox.InputOutputNodes) { n.DrawHUD(spriteBatch, camera); } } private void DrawSelection(SpriteBatch spriteBatch, Vector2 pos1, Vector2 pos2, Color color) { Vector2 location = camera.WorldToScreen(pos1); location.Y = -location.Y; Vector2 location2 = camera.WorldToScreen(pos2); location2.Y = -location2.Y; MapEntity.DrawSelectionRect(spriteBatch, location, new Vector2(-(location.X - location2.X), location.Y - location2.Y), color); } private const float lineBaseWidth = 1f; private static float lineWidth; public static void DrawRectangleWithBorder(SpriteBatch spriteBatch, RectangleF rect, Color fillColor, Color borderColor) { Vector2 topRight = new Vector2(rect.Right, rect.Top), topLeft = new Vector2(rect.Left, rect.Top), bottomRight = new Vector2(rect.Right, rect.Bottom), bottomLeft = new Vector2(rect.Left, rect.Bottom); Vector2 offset = new Vector2(0f, lineWidth / 2f); GUI.DrawFilledRectangle(spriteBatch, rect, fillColor); spriteBatch.DrawLine(topRight, topLeft, borderColor, thickness: lineWidth); spriteBatch.DrawLine(topLeft - offset, bottomLeft + offset, borderColor, thickness: lineWidth); spriteBatch.DrawLine(bottomLeft, bottomRight, borderColor, thickness: lineWidth); spriteBatch.DrawLine(bottomRight + offset, topRight - offset, borderColor, thickness: lineWidth); } private void DrawCircuits(SpriteBatch spriteBatch) { camera.UpdateTransform(interpolate: true, updateListener: false); SubEditorScreen.DrawOutOfBoundsArea(spriteBatch, camera, CircuitBoxSizes.PlayableAreaSize, GUIStyle.Red * 0.33f); SubEditorScreen.DrawGrid(spriteBatch, camera, gridSize.X, gridSize.Y, zoomTreshold: false); lineWidth = lineBaseWidth / camera.Zoom; Vector2 mousePos = GetCursorPosition(); mousePos.Y = -mousePos.Y; foreach (CircuitBoxWire wire in CircuitBox.Wires) { wire.Renderer.Draw(spriteBatch, GetSelectionColor(wire)); } foreach (var node in CircuitBox.Components) { if (node.IsSelected) { node.DrawSelection(spriteBatch, GetSelectionColor(node)); } node.Draw(spriteBatch, node.Position, node.Item.Prefab.SignalComponentColor * CircuitBoxNode.Opacity); } foreach (var ioNode in CircuitBox.InputOutputNodes) { if (ioNode.IsSelected) { ioNode.DrawSelection(spriteBatch, GetSelectionColor(ioNode)); } Color color = ioNode.NodeType is CircuitBoxInputOutputNode.Type.Input ? GUIStyle.Green : GUIStyle.Red; ioNode.Draw(spriteBatch, ioNode.Position, color * CircuitBoxNode.Opacity); } if (MouseSnapshotHandler.IsDragging) { var draggedNodes = MouseSnapshotHandler.GetMoveAffectedComponents(); Vector2 dragOffset = MouseSnapshotHandler.GetDragAmount(GetCursorPosition()); foreach (CircuitBoxNode moveable in draggedNodes) { Color color = moveable switch { CircuitBoxComponent node => node.Item.Prefab.SignalComponentColor, CircuitBoxInputOutputNode ioNode => ioNode.NodeType is CircuitBoxInputOutputNode.Type.Input ? GUIStyle.Green : GUIStyle.Red, _ => Color.White }; moveable.Draw(spriteBatch, moveable.Position + dragOffset, color * 0.5f); } } if (DraggedWire.TryUnwrap(out CircuitBoxWireRenderer? draggedWire)) { draggedWire.Draw(spriteBatch, GUIStyle.Yellow); } } private Color GetSelectionColor(CircuitBoxNode node) => GetSelectionColor(node.SelectedBy, node.IsSelectedByMe); private Color GetSelectionColor(CircuitBoxWire wire) => GetSelectionColor(wire.SelectedBy, wire.IsSelectedByMe); private Color GetSelectionColor(ushort selectedBy, bool isSelectedByMe) { #if !DEBUG if (isSelectedByMe) { return GUIStyle.Yellow; } #endif foreach (var (_, cursor) in CircuitBox.ActiveCursors) { if (cursor.Info.CharacterID == selectedBy) { return cursor.Color; } } return GUIStyle.Yellow; } private Vector2 cursorPos; public Vector2 GetCursorPosition() => cursorPos; public Option GetDragStart() => selection.Select(static f => f.Location); public void Update(float deltaTime) { cursorPos = camera.ScreenToWorld(PlayerInput.MousePosition); foreach (CircuitBoxWire wire in CircuitBox.Wires) { wire.Update(); } bool foundSelected = false; foreach (var node in CircuitBox.Components) { if (!node.IsSelectedByMe) { continue; } foundSelected = true; if (circuitComponent is not null) { node.UpdateEditing(circuitComponent.RectTransform); } break; } if (!foundSelected) { CircuitBoxComponent.RemoveEditingHUD(); } bool isMouseOn = GUI.MouseOn == circuitComponent; if (isMouseOn) { Character.DisableControls = true; } camera.MoveCamera(deltaTime, allowMove: true, allowZoom: isMouseOn, allowInput: isMouseOn, followSub: false); if (camera.TargetPos != Vector2.Zero && MathUtils.NearlyEqual(camera.Position, camera.TargetPos, 0.01f)) { camera.TargetPos = Vector2.Zero; } if (isMouseOn) { if (CircuitBox.HeldComponent.IsNone() && PlayerInput.PrimaryMouseButtonDown()) { MouseSnapshotHandler.StartDragging(); } if (PlayerInput.MidButtonHeld() || (PlayerInput.IsAltDown() && PlayerInput.PrimaryMouseButtonHeld())) { Vector2 moveSpeed = PlayerInput.MouseSpeed / camera.Zoom; moveSpeed.X = -moveSpeed.X; camera.Position += moveSpeed; } if (PlayerInput.PrimaryMouseButtonHeld()) { MouseSnapshotHandler.UpdateDrag(GetCursorPosition()); } if (MouseSnapshotHandler.IsWiring && MouseSnapshotHandler.LastConnectorUnderCursor.TryUnwrap(out var c)) { Vector2 start = c.Rect.Center, end = GetCursorPosition(); end.Y = -end.Y; if (!c.IsOutput) { (start, end) = (end, start); } if (DraggedWire.TryUnwrap(out var wire)) { wire.Recompute(start, end, CircuitBoxWire.SelectedWirePrefab.SpriteColor); } else { DraggedWire = Option.Some(new CircuitBoxWireRenderer(Option.None,start, end, GUIStyle.Red, CircuitBox.WireSprite)); } } else { DraggedWire = Option.None; } if (PlayerInput.SecondaryMouseButtonClicked()) { OpenContextMenu(); } if (PlayerInput.PrimaryMouseButtonClicked()) { if (CircuitBox.HeldComponent.TryUnwrap(out ItemPrefab? prefab)) { CircuitBox.AddComponent(prefab, cursorPos); } else { if (MouseSnapshotHandler.IsDragging && PlayerInput.PrimaryMouseButtonReleased()) { CircuitBox.MoveComponent(MouseSnapshotHandler.GetDragAmount(cursorPos), MouseSnapshotHandler.GetMoveAffectedComponents()); } else if (!MouseSnapshotHandler.IsWiring) { TrySelectComponentsUnderCursor(); } } if (MouseSnapshotHandler.IsWiring && MouseSnapshotHandler.LastConnectorUnderCursor.TryUnwrap(out var one)) { if (MouseSnapshotHandler.FindConnectorUnderCursor(cursorPos).TryUnwrap(out var two)) { CircuitBox.AddWire(one, two); } } CircuitBox.SelectWires(MouseSnapshotHandler.LastWireUnderCursor.TryUnwrap(out var wire) ? ImmutableArray.Create(wire) : ImmutableArray.Empty, !PlayerInput.IsShiftDown()); CircuitBox.HeldComponent = Option.None; MouseSnapshotHandler.EndDragging(); } if (MouseSnapshotHandler.GetLastComponentsUnderCursor().IsEmpty && MouseSnapshotHandler.LastConnectorUnderCursor.IsNone()) { UpdateSelection(); } // Allow using both Delete key and Ctrl+D for those who don't have a Delete key bool hitDeleteCombo = PlayerInput.KeyHit(Keys.Delete) || (PlayerInput.IsCtrlDown() && PlayerInput.KeyHit(Keys.D)); if (GUI.KeyboardDispatcher.Subscriber is null && hitDeleteCombo) { CircuitBox.RemoveComponents(CircuitBox.Components.Where(static node => node.IsSelectedByMe).ToArray()); CircuitBox.RemoveWires(CircuitBox.Wires.Where(static wire => wire.IsSelectedByMe).ToImmutableArray()); } } if (componentMenu is { } menu && toggleMenuButton is { } button) { componentMenuOpenState = componentMenuOpen ? Math.Min(componentMenuOpenState + deltaTime * 5.0f, 1.0f) : Math.Max(componentMenuOpenState - deltaTime * 5.0f, 0.0f); menu.RectTransform.ScreenSpaceOffset = Vector2.Lerp(new Vector2(0.0f, menu.Rect.Height - 10), Vector2.Zero, componentMenuOpenState).ToPoint(); button.RectTransform.AbsoluteOffset = new Point(menu.Rect.X + ((menu.Rect.Width / 2) - (button.Rect.Width / 2)), menu.Rect.Y - button.Rect.Height); } camera.Position = Vector2.Clamp(camera.Position, new Vector2(-CircuitBoxSizes.PlayableAreaSize / 2f), new Vector2(CircuitBoxSizes.PlayableAreaSize / 2f)); } private void UpdateSelection() { if (!PlayerInput.IsAltDown() && PlayerInput.PrimaryMouseButtonDown()) { selection = Option.Some(new RectangleF(GetCursorPosition(), Vector2.Zero)); } if (!selection.TryUnwrap(out RectangleF rect)) { return; } if (!PlayerInput.PrimaryMouseButtonHeld()) { selection = Option.None; RectangleF selectionRect = Submarine.AbsRectF(rect.Location, rect.Size); float treshold = 12f / camera.Zoom; if (selectionRect.Size.X < treshold || selectionRect.Size.Y < treshold) { return; } CircuitBox.SelectComponents(MouseSnapshotHandler.Nodes.Where(n => selectionRect.Intersects(n.Rect)).ToImmutableHashSet(), !PlayerInput.IsShiftDown()); } else { RectangleF oldRect = rect; rect.Size = camera.ScreenToWorld(PlayerInput.MousePosition) - rect.Location; if (rect.Equals(oldRect)) { return; } selection = Option.Some(rect); } } private void TrySelectComponentsUnderCursor() { CircuitBoxNode? foundNode = GetTopmostNode(MouseSnapshotHandler.GetLastComponentsUnderCursor()); CircuitBox.SelectComponents(foundNode is null ? ImmutableArray.Empty : ImmutableArray.Create(foundNode), !PlayerInput.IsShiftDown()); } private void OpenContextMenu() { var wireOption = MouseSnapshotHandler.FindWireUnderCursor(cursorPos); var wireSelection = CircuitBox.Wires.Where(static w => w.IsSelectedByMe).ToImmutableArray(); var nodeOption = GetTopmostNode(MouseSnapshotHandler.FindNodesUnderCursor(cursorPos)); var nodeSelection = CircuitBox.Components.Where(static n => n.IsSelectedByMe).ToImmutableArray(); var option = new ContextMenuOption(TextManager.Get("delete"), isEnabled: wireOption.IsSome() || nodeOption is CircuitBoxComponent, () => { if (wireOption.TryUnwrap(out var wire)) { CircuitBox.RemoveWires(wire.IsSelected ? wireSelection : ImmutableArray.Create(wire)); } if (nodeOption is CircuitBoxComponent node) { CircuitBox.RemoveComponents(node.IsSelected ? nodeSelection : ImmutableArray.Create(node)); } }); // show component name in the header to better indicate what is about to be deleted if (nodeOption is CircuitBoxComponent comp) { GUIContextMenu.CreateContextMenu(PlayerInput.MousePosition, comp.Item.Name, comp.Item.Prefab.SignalComponentColor, option); return; } // also check if a wire is being deleted if (wireOption.TryUnwrap(out var foundWire)) { GUIContextMenu.CreateContextMenu(PlayerInput.MousePosition, foundWire.UsedItemPrefab.Name, foundWire.Color, option); return; } GUIContextMenu.CreateContextMenu(option); } public CircuitBoxNode? GetTopmostNode(ImmutableHashSet nodes) { CircuitBoxNode? foundNode = null; var allNodes = MouseSnapshotHandler.Nodes.ToImmutableArray(); for (int i = allNodes.Length - 1; i >= 0; i--) { CircuitBoxNode node = allNodes[i]; if (nodes.Contains(node)) { foundNode = node; break; } } return foundNode; } public void AddToGUIUpdateList() { toggleMenuButton?.AddToGUIUpdateList(); selectedWireFrame?.AddToGUIUpdateList(); } } }