Client LuaCsForBarotrauma
CircuitBoxUI.cs
1 #nullable enable
2 using System;
3 using System.Collections.Generic;
4 using System.Collections.Immutable;
5 using System.Linq;
7 using Microsoft.Xna.Framework;
8 using Microsoft.Xna.Framework.Graphics;
9 using Microsoft.Xna.Framework.Input;
10 
11 namespace Barotrauma
12 {
13  internal sealed class CircuitBoxUI
14  {
15  private readonly Camera camera;
16  private static readonly Vector2 gridSize = new Vector2(128f);
17  public readonly CircuitBox CircuitBox;
18  private bool componentMenuOpen;
19  private float componentMenuOpenState;
20 
21  private GUICustomComponent? circuitComponent;
22  private GUIFrame? componentMenu;
23  private GUIButton? toggleMenuButton;
24  private GUIFrame? selectedWireFrame;
25  private GUIListBox? componentList;
26  private GUITextBlock? inventoryIndicatorText;
27  private readonly Sprite? cursorSprite = GUIStyle.CursorSprite[CursorState.Default];
28 
29  private Option<RectangleF> selection = Option.None;
30  private string searchTerm = string.Empty;
31 
32  public static Option<CircuitBoxWireRenderer> DraggedWire = Option.None;
33 
34  public readonly CircuitBoxMouseDragSnapshotHandler MouseSnapshotHandler;
35 
36  public List<CircuitBoxWireRenderer> VirtualWires = new();
37 
38  public bool Locked => CircuitBox.Locked;
39 
40  public CircuitBoxUI(CircuitBox box)
41  {
42  camera = new Camera
43  {
44  MinZoom = 0.25f,
45  MaxZoom = 2f
46  };
47 
48  CircuitBox = box;
49  MouseSnapshotHandler = new CircuitBoxMouseDragSnapshotHandler(this);
50  }
51 
52  #region UI
53 
54  public void CreateGUI(GUIFrame parent)
55  {
56  GUIFrame paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.97f, 0.95f), parent.RectTransform, Anchor.Center), style: null);
57  circuitComponent = new GUICustomComponent(new RectTransform(Vector2.One, paddedFrame.RectTransform), onDraw: (spriteBatch, component) =>
58  {
59  Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle;
60  spriteBatch.End();
61  spriteBatch.GraphicsDevice.ScissorRectangle = component.Rect;
62 
63  spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable, transformMatrix: camera.Transform);
64  DrawCircuits(spriteBatch);
65  spriteBatch.End();
66 
67  spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);
68  DrawHUD(spriteBatch, component.Rect);
69  spriteBatch.End();
70 
71  spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect;
72  spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);
73  });
74 
75  GUIScissorComponent menuContainer = new GUIScissorComponent(new RectTransform(Vector2.One, paddedFrame.RectTransform, anchor: Anchor.Center))
76  {
77  CanBeFocused = false
78  };
79 
80  componentMenuOpen = true;
81  componentMenu = new GUIFrame(new RectTransform(new Vector2(1f, 0.4f), menuContainer.Content.RectTransform, Anchor.BottomRight));
82  toggleMenuButton = new GUIButton(new RectTransform(new Point(300, 30), GUI.Canvas) { MinSize = new Point(0, 15) }, style: "UIToggleButtonVertical")
83  {
84  OnClicked = (btn, userdata) =>
85  {
86  componentMenuOpen = !componentMenuOpen;
87  if (Locked) { componentMenuOpen = false; }
88 
89  foreach (GUIComponent child in btn.Children)
90  {
91  child.SpriteEffects = componentMenuOpen ? SpriteEffects.None : SpriteEffects.FlipVertically;
92  }
93 
94  return true;
95  }
96  };
97 
98  GUILayoutGroup menuLayout = new GUILayoutGroup(new RectTransform(Vector2.One, componentMenu.RectTransform), childAnchor: Anchor.TopCenter) { RelativeSpacing = 0.02f };
99  GUILayoutGroup headerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), menuLayout.RectTransform), isHorizontal: true);
100 
101  GUILayoutGroup labelLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1f), headerLayout.RectTransform), isHorizontal: true);
102 
103  GUILayoutGroup searchBarLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1f), headerLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true);
104  GUITextBlock searchBarLabel = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1f), searchBarLayout.RectTransform), "Filter");
105  GUITextBox searchbar = new GUITextBox(new RectTransform(new Vector2(0.85f, 1f), searchBarLayout.RectTransform), string.Empty, createClearButton: true);
106 
107  new GUIFrame(new RectTransform(new Vector2(0.5f, 0.01f), menuLayout.RectTransform), style: "HorizontalLine");
108 
109  componentList = new GUIListBox(new RectTransform(new Vector2(0.95f, 0.65f), menuLayout.RectTransform))
110  {
111  PlaySoundOnSelect = true,
112  UseGridLayout = true,
113  OnSelected = (_, o) =>
114  {
115  if (o is not ItemPrefab prefab) { return false; }
116 
117  CircuitBox.HeldComponent = Option.Some(prefab);
118  return true;
119  }
120  };
121 
122  GUILayoutGroup inventoryLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1f), headerLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.Center);
123  GUILayoutGroup indicatorLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.2f, 1f), inventoryLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
124  GUIImage indicatorIcon = new GUIImage(new RectTransform(new Vector2(0.5f, 0.8f), indicatorLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CircuitIndicatorIcon");
125  inventoryIndicatorText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), indicatorLayout.RectTransform), GetInventoryText(), font: GUIStyle.SubHeadingFont);
126 
127  int gapSize = GUI.IntScale(8);
128  selectedWireFrame = SubEditorScreen.CreateWiringPanel(Point.Zero, SelectWire);
129  selectedWireFrame.RectTransform.AbsoluteOffset = new Point(parent.Rect.X - (selectedWireFrame.Rect.Width + gapSize), parent.Rect.Y);
130 
131  foreach (ItemPrefab prefab in ItemPrefab.Prefabs.OrderBy(static p => p.Name))
132  {
133  if (!prefab.Tags.Contains("circuitboxcomponent")) { continue; }
134 
135  CreateComponentElement(prefab, componentList.Content.RectTransform);
136  }
137 
138  searchbar.OnTextChanged += (tb, s) =>
139  {
140  searchTerm = s;
141  UpdateComponentList();
142  return true;
143  };
144  int buttonHeight = (int)(GUIStyle.ItemFrameMargin.Y * 0.4f);
145  var settingsIcon = new GUIButton(new RectTransform(new Point(buttonHeight), parent.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(buttonHeight / 4), MinSize = new Point(buttonHeight) },
146  style: "GUIButtonSettings")
147  {
148  OnClicked = (btn, userdata) =>
149  {
150  GUIContextMenu.CreateContextMenu(
151  new ContextMenuOption("circuitboxsetting.resetview", isEnabled: true, onSelected: ResetCamera)
152  {
153  Tooltip = TextManager.Get("circuitboxsettingdescription.resetview")
154  },
155  new ContextMenuOption("circuitboxsetting.find", isEnabled: true,
156  new ContextMenuOption("circuitboxsetting.focusinput", isEnabled: true, onSelected: () => FindInputOutput(CircuitBoxInputOutputNode.Type.Input))
157  {
158  Tooltip = TextManager.Get("circuitboxsettingdescription.focusinput")
159  },
160  new ContextMenuOption("circuitboxsetting.focusoutput", isEnabled: true, onSelected: () => FindInputOutput(CircuitBoxInputOutputNode.Type.Output))
161  {
162  Tooltip = TextManager.Get("circuitboxsettingdescription.focusoutput")
163  },
164  new ContextMenuOption("circuitboxsetting.focuscircuits", isEnabled: CircuitBox.Components.Any(), onSelected: FindCircuit)
165  {
166  Tooltip = TextManager.Get("circuitboxsettingdescription.focuscircuits")
167  }));
168 
169 
170  void ResetCamera()
171  {
172  // Vector2.One because Vector2.Zero means no value
173  camera.TargetPos = Vector2.One;
174  }
175 
176  void FindInputOutput(CircuitBoxInputOutputNode.Type type)
177  {
178  var input = CircuitBox.InputOutputNodes.FirstOrDefault(n => n.NodeType == type);
179  if (input is null) { return; }
180 
181  camera.TargetPos = input.Position;
182  }
183 
184  void FindCircuit()
185  {
186  var closestComponent = CircuitBox.Components.MinBy(c => Vector2.DistanceSquared(c.Position, camera.Position));
187  if (closestComponent is null) { return; }
188 
189  camera.TargetPos = closestComponent.Position;
190  }
191 
192  return true;
193  }
194  };
195 
196  MouseSnapshotHandler.UpdateConnections();
197 
198  // update scales of everything
199  foreach (var node in CircuitBox.Components) { node.OnUICreated(); }
200 
201  foreach (var node in CircuitBox.InputOutputNodes) { node.OnUICreated(); }
202 
203  foreach (var wire in CircuitBox.Wires) { wire.Update(); }
204  }
205 
206  private string GetInventoryText() =>
207  CircuitBox.ComponentContainer is { } container
208  ? $"{container.Inventory.AllItems.Count()}/{container.Capacity}"
209  : "0/0";
210 
211  public void UpdateComponentList()
212  {
213  if (inventoryIndicatorText is { } text)
214  {
215  text.Text = GetInventoryText();
216  }
217 
218  if (componentList is null) { return; }
219 
220  var playerInventory = CircuitBox.GetSortedCircuitBoxItemsFromPlayer(Character.Controlled);
221 
222  foreach (GUIComponent child in componentList.Content.Children)
223  {
224  if (child.UserData is not ItemPrefab prefab) { continue; }
225 
226  child.Enabled = !CircuitBox.IsFull && (!CircuitBox.IsInGame() || CircuitBox.GetApplicableResourcePlayerHas(prefab, playerInventory).IsSome());
227 
228  if (child.GetChild<GUILayoutGroup>()?.GetChild<GUIImage>() is { } image)
229  {
230  image.Enabled = child.Enabled;
231  }
232 
233  child.ToolTip = child.Enabled
234  ? prefab.Description
235  : RichString.Rich(TextManager.GetWithVariable(new Identifier("CircuitBoxUIComponentNotAvailable"), new Identifier("[item]"), prefab.Name));
236 
237  if (string.IsNullOrWhiteSpace(searchTerm))
238  {
239  child.Visible = true;
240  continue;
241  }
242 
243  child.Visible = prefab.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase);
244  }
245  }
246 
247  private static bool SelectWire(GUIComponent component, object obj)
248  {
249  if (obj is not ItemPrefab prefab) { return false; }
250 
251  CircuitBoxWire.SelectedWirePrefab = prefab;
252  return true;
253  }
254 
255  private static void CreateComponentElement(ItemPrefab prefab, RectTransform parent)
256  {
257  GUIFrame itemFrame = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.9f), parent) { MinSize = new Point(0, 50) }, style: "GUITextBox")
258  {
259  UserData = prefab
260  };
261 
262  itemFrame.RectTransform.MinSize = new Point(0, itemFrame.Rect.Width);
263  itemFrame.RectTransform.MaxSize = new Point(int.MaxValue, itemFrame.Rect.Width);
264  itemFrame.ToolTip = prefab.Name;
265 
266  GUILayoutGroup paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), itemFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter)
267  {
268  Stretch = true,
269  RelativeSpacing = 0.03f,
270  CanBeFocused = false
271  };
272 
273  Sprite icon;
274  Color iconColor;
275 
276  if (prefab.InventoryIcon != null)
277  {
278  icon = prefab.InventoryIcon;
279  iconColor = prefab.InventoryIconColor;
280  }
281  else
282  {
283  icon = prefab.Sprite;
284  iconColor = prefab.SpriteColor;
285  }
286 
287  GUIImage? img = null;
288  if (icon != null)
289  {
290  img = new GUIImage(new RectTransform(new Vector2(1.0f, 0.8f), paddedFrame.RectTransform, Anchor.TopCenter), icon)
291  {
292  CanBeFocused = false,
293  LoadAsynchronously = true,
294  DisabledColor = Color.DarkGray * 0.8f,
295  Color = iconColor
296  };
297  }
298 
299  GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter),
300  text: prefab.Name, textAlignment: Alignment.Center, font: GUIStyle.SmallFont)
301  {
302  CanBeFocused = false
303  };
304 
305  textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width);
306  paddedFrame.Recalculate();
307 
308  if (img != null)
309  {
310  img.Scale = Math.Min(Math.Min(img.Rect.Width / img.Sprite.size.X, img.Rect.Height / img.Sprite.size.Y), 1.5f);
311  img.RectTransform.NonScaledSize = new Point((int)(img.Sprite.size.X * img.Scale), img.Rect.Height);
312  }
313  }
314 
315  #endregion
316 
317  private void DrawHUD(SpriteBatch spriteBatch, Rectangle screenRect)
318  {
319  float scale = GUI.Scale / 1.5f;
320  Vector2 offset = new Vector2(20, 40) * scale;
321 
322  foreach (var (character, cursor) in CircuitBox.ActiveCursors)
323  {
324  if (!cursor.IsActive) { continue; }
325 
326  Vector2 cursorWorldPos = camera.WorldToScreen(cursor.DrawPosition);
327 
328  if (cursor.Info.DragStart.TryUnwrap(out Vector2 dragStart))
329  {
330  DrawSelection(spriteBatch, dragStart, cursor.DrawPosition, cursor.Color);
331  }
332 
333  if (cursor.HeldPrefab.TryUnwrap(out ItemPrefab? otherHeldPrefab))
334  {
335  otherHeldPrefab.Sprite.Draw(spriteBatch, cursorWorldPos);
336  }
337 
338  cursorSprite?.Draw(spriteBatch, cursorWorldPos, cursor.Color, 0f, scale);
339  GUI.DrawString(spriteBatch, cursorWorldPos + offset, character.Name, cursor.Color, Color.Black, GUI.IntScale(4), GUIStyle.SmallFont);
340  }
341 
342  if (selection.TryUnwrap(out RectangleF rect))
343  {
344  Vector2 pos1 = rect.Location;
345  Vector2 pos2 = new Vector2(rect.Location.X + rect.Size.X, rect.Location.Y + rect.Size.Y);
346  DrawSelection(spriteBatch, pos1, pos2, GUIStyle.Blue);
347  }
348 
349  if (CircuitBox.HeldComponent.TryUnwrap(out ItemPrefab? component))
350  {
351  component.Sprite.Draw(spriteBatch, PlayerInput.MousePosition);
352  }
353  if (PlayerInput.PrimaryMouseButtonHeld() && MouseSnapshotHandler.LastConnectorUnderCursor.IsSome())
354  {
355  CircuitBoxWire.SelectedWirePrefab.Sprite.Draw(spriteBatch, PlayerInput.MousePosition, CircuitBoxWire.SelectedWirePrefab.SpriteColor, scale: camera.Zoom);
356  }
357 
358  foreach (var c in CircuitBox.Components)
359  {
360  c.DrawHUD(spriteBatch, camera);
361  }
362 
363  foreach (var n in CircuitBox.InputOutputNodes)
364  {
365  n.DrawHUD(spriteBatch, camera);
366  }
367 
368  if (Locked)
369  {
370  LocalizedString lockedText = TextManager.Get("CircuitBoxLocked")
371  .Fallback(TextManager.Get("ConnectionLocked"), useDefaultLanguageIfFound: false);
372 
373  Vector2 size = GUIStyle.LargeFont.MeasureString(lockedText);
374  Vector2 pos = new Vector2(screenRect.Center.X - size.X / 2, screenRect.Top + screenRect.Height * 0.05f);
375  GUI.DrawString(spriteBatch, pos, lockedText, Color.Red, Color.Black, 8, GUIStyle.LargeFont);
376  }
377  }
378 
379  private void DrawSelection(SpriteBatch spriteBatch, Vector2 pos1, Vector2 pos2, Color color)
380  {
381  Vector2 location = camera.WorldToScreen(pos1);
382  location.Y = -location.Y;
383  Vector2 location2 = camera.WorldToScreen(pos2);
384  location2.Y = -location2.Y;
385  MapEntity.DrawSelectionRect(spriteBatch, location, new Vector2(-(location.X - location2.X), location.Y - location2.Y), color);
386  }
387 
388  private const float lineBaseWidth = 1f;
389  private static float lineWidth;
390 
391  public static void DrawRectangleWithBorder(SpriteBatch spriteBatch, RectangleF rect, Color fillColor, Color borderColor)
392  {
393  GUI.DrawFilledRectangle(spriteBatch, rect, fillColor);
394  DrawRectangleOnlyBorder(spriteBatch, rect, borderColor);
395  }
396 
397  private static void DrawRectangleOnlyBorder(SpriteBatch spriteBatch, RectangleF rect, Color borderColor)
398  {
399  Vector2 topRight = new Vector2(rect.Right, rect.Top),
400  topLeft = new Vector2(rect.Left, rect.Top),
401  bottomRight = new Vector2(rect.Right, rect.Bottom),
402  bottomLeft = new Vector2(rect.Left, rect.Bottom);
403 
404  Vector2 offset = new Vector2(0f, lineWidth / 2f);
405 
406  spriteBatch.DrawLine(topRight, topLeft, borderColor, thickness: lineWidth);
407  spriteBatch.DrawLine(topLeft - offset, bottomLeft + offset, borderColor, thickness: lineWidth);
408  spriteBatch.DrawLine(bottomLeft, bottomRight, borderColor, thickness: lineWidth);
409  spriteBatch.DrawLine(bottomRight + offset, topRight - offset, borderColor, thickness: lineWidth);
410  }
411 
412  private void DrawCircuits(SpriteBatch spriteBatch)
413  {
414  camera.UpdateTransform(interpolate: true, updateListener: false);
415  SubEditorScreen.DrawOutOfBoundsArea(spriteBatch, camera, CircuitBoxSizes.PlayableAreaSize, GUIStyle.Red * 0.33f);
416  SubEditorScreen.DrawGrid(spriteBatch, camera, gridSize.X, gridSize.Y, zoomTreshold: false);
417  lineWidth = lineBaseWidth / camera.Zoom;
418 
419  Vector2 mousePos = GetCursorPosition();
420  mousePos.Y = -mousePos.Y;
421 
422  foreach (var label in CircuitBox.Labels)
423  {
424  if (label.IsSelected)
425  {
426  label.DrawSelection(spriteBatch, GetSelectionColor(label));
427  }
428 
429  label.Draw(spriteBatch, label.Position, label.Color);
430  }
431 
432  foreach (CircuitBoxWire wire in CircuitBox.Wires)
433  {
434  wire.Renderer.Draw(spriteBatch, GetSelectionColor(wire));
435  }
436 
437  foreach (var node in CircuitBox.Components)
438  {
439  if (node.IsSelected)
440  {
441  node.DrawSelection(spriteBatch, GetSelectionColor(node));
442  }
443 
444  node.Draw(spriteBatch, node.Position, node.Item.Prefab.SignalComponentColor * CircuitBoxNode.Opacity);
445  }
446 
447  foreach (var ioNode in CircuitBox.InputOutputNodes)
448  {
449  if (ioNode.IsSelected)
450  {
451  ioNode.DrawSelection(spriteBatch, GetSelectionColor(ioNode));
452  }
453 
454  Color color = ioNode.NodeType is CircuitBoxInputOutputNode.Type.Input ? GUIStyle.Green : GUIStyle.Red;
455  ioNode.Draw(spriteBatch, ioNode.Position, color * CircuitBoxNode.Opacity);
456  }
457 
458  if (MouseSnapshotHandler.IsDragging)
459  {
460  var draggedNodes = MouseSnapshotHandler.GetMoveAffectedComponents();
461  Vector2 dragOffset = MouseSnapshotHandler.GetDragAmount(GetCursorPosition());
462  foreach (CircuitBoxNode moveable in draggedNodes)
463  {
464  Color color = moveable switch
465  {
466  CircuitBoxComponent node => node.Item.Prefab.SignalComponentColor,
467  CircuitBoxLabelNode label => label.Color,
468  CircuitBoxInputOutputNode ioNode => ioNode.NodeType is CircuitBoxInputOutputNode.Type.Input ? GUIStyle.Green : GUIStyle.Red,
469  _ => Color.White
470  };
471  moveable.Draw(spriteBatch, moveable.Position + dragOffset, color * 0.5f);
472  }
473  }
474 
475  if (MouseSnapshotHandler.IsResizing && MouseSnapshotHandler.LastResizeAffectedNode.TryUnwrap(out var resize))
476  {
477  var (dir, node) = resize;
478  Vector2 dragOffset = MouseSnapshotHandler.GetDragAmount(GetCursorPosition());
479 
480  var rect = node.Rect;
481  rect.Y = -rect.Y;
482  rect.Y -= rect.Height;
483 
484  if (dir.HasFlag(CircuitBoxResizeDirection.Down))
485  {
486  rect.Height -= dragOffset.Y;
487  rect.Height = Math.Max(rect.Height, CircuitBoxLabelNode.MinSize.Y + CircuitBoxSizes.NodeHeaderHeight);
488  }
489 
490  if (dir.HasFlag(CircuitBoxResizeDirection.Right))
491  {
492  rect.Width += dragOffset.X;
493  rect.Width = Math.Max(rect.Width, CircuitBoxLabelNode.MinSize.X);
494  }
495 
496  if (dir.HasFlag(CircuitBoxResizeDirection.Left))
497  {
498  float oldWidth = rect.Width;
499  rect.Width -= dragOffset.X;
500  rect.Width = Math.Max(rect.Width, CircuitBoxLabelNode.MinSize.X);
501 
502  float actualResize = rect.Width - oldWidth;
503  rect.X -= actualResize;
504  }
505 
506  DrawRectangleOnlyBorder(spriteBatch, rect, GUIStyle.Yellow);
507  }
508 
509  if (DraggedWire.TryUnwrap(out CircuitBoxWireRenderer? draggedWire))
510  {
511  draggedWire.Draw(spriteBatch, GUIStyle.Yellow);
512  }
513  }
514 
515  private Color GetSelectionColor(CircuitBoxNode node) => GetSelectionColor(node.SelectedBy, node.IsSelectedByMe);
516 
517  private Color GetSelectionColor(CircuitBoxWire wire) => GetSelectionColor(wire.SelectedBy, wire.IsSelectedByMe);
518 
519  private Color GetSelectionColor(ushort selectedBy, bool isSelectedByMe)
520  {
521 #if !DEBUG
522  if (isSelectedByMe)
523  {
524  return GUIStyle.Yellow;
525  }
526 #endif
527 
528  foreach (var (_, cursor) in CircuitBox.ActiveCursors)
529  {
530  if (cursor.Info.CharacterID == selectedBy)
531  {
532  return cursor.Color;
533  }
534  }
535 
536  return GUIStyle.Yellow;
537  }
538 
539  private Vector2 cursorPos;
540  public Vector2 GetCursorPosition() => cursorPos;
541  public Option<Vector2> GetDragStart() => selection.Select(static f => f.Location);
542 
543  public void Update(float deltaTime)
544  {
545  cursorPos = camera.ScreenToWorld(PlayerInput.MousePosition);
546  foreach (CircuitBoxWire wire in CircuitBox.Wires)
547  {
548  wire.Update();
549  }
550 
551  bool foundSelected = false;
552  foreach (var node in CircuitBox.Components)
553  {
554  if (!node.IsSelectedByMe) { continue; }
555 
556  foundSelected = true;
557  if (circuitComponent is not null)
558  {
559  node.UpdateEditing(circuitComponent.RectTransform);
560  }
561 
562  break;
563  }
564 
565  if (!foundSelected)
566  {
567  CircuitBoxComponent.RemoveEditingHUD();
568  }
569 
570  bool isMouseOn = GUI.MouseOn == circuitComponent;
571 
572  if (isMouseOn)
573  {
574  Character.DisableControls = true;
575  }
576 
577  camera.MoveCamera(deltaTime, allowMove: true, allowZoom: isMouseOn, allowInput: isMouseOn, followSub: false);
578 
579  if (camera.TargetPos != Vector2.Zero && MathUtils.NearlyEqual(camera.Position, camera.TargetPos, 0.01f))
580  {
581  camera.TargetPos = Vector2.Zero;
582  }
583 
584  if (isMouseOn)
585  {
586  if (PlayerInput.PrimaryMouseButtonDown())
587  {
588  if (CircuitBox.HeldComponent.IsNone())
589  {
590  MouseSnapshotHandler.StartDragging();
591  }
592  else
593  {
594  MouseSnapshotHandler.ClearSnapshot();
595  }
596  }
597 
598  if (PlayerInput.DoubleClicked() && MouseSnapshotHandler.FindWireUnderCursor(cursorPos).IsNone())
599  {
600  var topmostNode = GetTopmostNode(MouseSnapshotHandler.FindNodesUnderCursor(cursorPos));
601  if (topmostNode is CircuitBoxLabelNode label && circuitComponent is not null)
602  {
603  label.PromptEditText(circuitComponent);
604  }
605  }
606 
607  if (PlayerInput.MidButtonHeld() || (PlayerInput.IsAltDown() && PlayerInput.PrimaryMouseButtonHeld()))
608  {
609  Vector2 moveSpeed = PlayerInput.MouseSpeed / camera.Zoom;
610  moveSpeed.X = -moveSpeed.X;
611  camera.Position += moveSpeed;
612  }
613 
614  if (PlayerInput.PrimaryMouseButtonHeld())
615  {
616  MouseSnapshotHandler.UpdateDrag(GetCursorPosition());
617  }
618 
619  if (MouseSnapshotHandler.IsWiring && MouseSnapshotHandler.LastConnectorUnderCursor.TryUnwrap(out var c))
620  {
621  Vector2 start = c.Rect.Center,
622  end = GetCursorPosition();
623 
624  end.Y = -end.Y;
625 
626  if (!c.IsOutput)
627  {
628  (start, end) = (end, start);
629  }
630 
631  if (DraggedWire.TryUnwrap(out var wire))
632  {
633  wire.Recompute(start, end, CircuitBoxWire.SelectedWirePrefab.SpriteColor);
634  }
635  else
636  {
637  DraggedWire = Option.Some(new CircuitBoxWireRenderer(Option.None, start, end, GUIStyle.Red, CircuitBox.WireSprite));
638  }
639  }
640  else
641  {
642  DraggedWire = Option.None;
643  }
644 
645  if (PlayerInput.SecondaryMouseButtonClicked())
646  {
647  OpenContextMenu();
648  }
649 
650  if (PlayerInput.PrimaryMouseButtonClicked())
651  {
652  bool selectedNode = false;
653  if (MouseSnapshotHandler.IsResizing && MouseSnapshotHandler.LastResizeAffectedNode.TryUnwrap(out var r))
654  {
655  var (dir, node) = r;
656  CircuitBox.ResizeNode(node, dir, MouseSnapshotHandler.GetDragAmount(cursorPos));
657  }
658 
659  if (CircuitBox.HeldComponent.TryUnwrap(out ItemPrefab? prefab))
660  {
661  CircuitBox.AddComponent(prefab, cursorPos);
662  }
663  else
664  {
665  if (MouseSnapshotHandler.IsDragging && PlayerInput.PrimaryMouseButtonReleased())
666  {
667  CircuitBox.MoveComponent(MouseSnapshotHandler.GetDragAmount(cursorPos), MouseSnapshotHandler.GetMoveAffectedComponents());
668  }
669  else if (!MouseSnapshotHandler.IsWiring)
670  {
671  selectedNode = TrySelectComponentsUnderCursor();
672  }
673  }
674 
675  if (MouseSnapshotHandler.IsWiring && MouseSnapshotHandler.LastConnectorUnderCursor.TryUnwrap(out var one))
676  {
677  if (MouseSnapshotHandler.FindConnectorUnderCursor(cursorPos).TryUnwrap(out var two))
678  {
679  CircuitBox.AddWire(one, two);
680  }
681  }
682 
683  if (MouseSnapshotHandler.LastWireUnderCursor.TryUnwrap(out var wire) && !MouseSnapshotHandler.IsDragging && !selectedNode)
684  {
685  CircuitBox.SelectWires(ImmutableArray.Create(wire), !PlayerInput.IsShiftDown());
686  }
687  else if (CircuitBox.Wires.Any(static wire => wire.IsSelectedByMe))
688  {
689  CircuitBox.SelectWires(ImmutableArray<CircuitBoxWire>.Empty, !PlayerInput.IsShiftDown());
690  }
691 
692  CircuitBox.HeldComponent = Option.None;
693  MouseSnapshotHandler.EndDragging();
694  }
695 
696  if (MouseSnapshotHandler.GetLastComponentsUnderCursor().IsEmpty && MouseSnapshotHandler.LastConnectorUnderCursor.IsNone())
697  {
698  UpdateSelection();
699  }
700 
701  // Allow using both Delete key and Ctrl+D for those who don't have a Delete key
702  bool hitDeleteCombo = PlayerInput.KeyHit(Keys.Delete) || (PlayerInput.IsCtrlDown() && PlayerInput.KeyHit(Keys.D));
703 
704  if (GUI.KeyboardDispatcher.Subscriber is null && hitDeleteCombo)
705  {
706  CircuitBox.RemoveComponents(CircuitBox.Components.Where(static node => node.IsSelectedByMe).ToArray());
707  CircuitBox.RemoveWires(CircuitBox.Wires.Where(static wire => wire.IsSelectedByMe).ToImmutableArray());
708  CircuitBox.RemoveLabel(CircuitBox.Labels.Where(static label => label.IsSelectedByMe).ToImmutableArray());
709  }
710  }
711 
712  if (componentMenu is { } menu && toggleMenuButton is { } button)
713  {
714  button.Enabled = !Locked;
715  componentMenuOpenState = componentMenuOpen && !Locked ? Math.Min(componentMenuOpenState + deltaTime * 5.0f, 1.0f) : Math.Max(componentMenuOpenState - deltaTime * 5.0f, 0.0f);
716 
717  menu.RectTransform.ScreenSpaceOffset = Vector2.Lerp(new Vector2(0.0f, menu.Rect.Height - 10), Vector2.Zero, componentMenuOpenState).ToPoint();
718  button.RectTransform.AbsoluteOffset = new Point(menu.Rect.X + ((menu.Rect.Width / 2) - (button.Rect.Width / 2)), menu.Rect.Y - button.Rect.Height);
719  }
720 
721  if (selectedWireFrame is { } wireFrame)
722  {
723  wireFrame.Visible = !Locked;
724  }
725 
726  camera.Position = Vector2.Clamp(camera.Position,
727  new Vector2(-CircuitBoxSizes.PlayableAreaSize / 2f),
728  new Vector2(CircuitBoxSizes.PlayableAreaSize / 2f));
729  }
730 
731  public void SetMenuVisibility(bool state)
732  => componentMenuOpen = state;
733 
734  private void UpdateSelection()
735  {
736  if (!PlayerInput.IsAltDown() && PlayerInput.PrimaryMouseButtonDown())
737  {
738  selection = Option.Some(new RectangleF(GetCursorPosition(), Vector2.Zero));
739  }
740 
741  if (!selection.TryUnwrap(out RectangleF rect)) { return; }
742 
743  if (!PlayerInput.PrimaryMouseButtonHeld())
744  {
745  selection = Option.None;
746  RectangleF selectionRect = Submarine.AbsRectF(rect.Location, rect.Size);
747 
748  float treshold = 12f / camera.Zoom;
749  if (selectionRect.Size.X < treshold || selectionRect.Size.Y < treshold) { return; }
750 
751  CircuitBox.SelectComponents(MouseSnapshotHandler.Nodes.Where(n => selectionRect.Intersects(n.Rect)).ToImmutableHashSet(), !PlayerInput.IsShiftDown());
752  }
753  else
754  {
755  RectangleF oldRect = rect;
756  rect.Size = camera.ScreenToWorld(PlayerInput.MousePosition) - rect.Location;
757  if (rect.Equals(oldRect)) { return; }
758 
759  selection = Option.Some(rect);
760  }
761  }
762 
763  private bool TrySelectComponentsUnderCursor()
764  {
765  CircuitBoxNode? foundNode = GetTopmostNode(MouseSnapshotHandler.GetLastComponentsUnderCursor());
766 
767  if (foundNode is CircuitBoxLabelNode && MouseSnapshotHandler.LastWireUnderCursor.IsSome())
768  {
769  foundNode = null;
770  }
771 
772  CircuitBox.SelectComponents(foundNode is null ? ImmutableArray<CircuitBoxNode>.Empty : ImmutableArray.Create(foundNode), !PlayerInput.IsShiftDown());
773  return foundNode is not null;
774  }
775 
776  private void OpenContextMenu()
777  {
778  var wireOption = MouseSnapshotHandler.FindWireUnderCursor(cursorPos);
779  var wireSelection = CircuitBox.Wires.Where(static w => w.IsSelectedByMe).ToImmutableArray();
780  var nodeOption = GetTopmostNode(MouseSnapshotHandler.FindNodesUnderCursor(cursorPos));
781  var nodeSelection = CircuitBox.Components.Where(static n => n.IsSelectedByMe).ToImmutableArray();
782  var labels = CircuitBox.Labels.Where(static l => l.IsSelectedByMe).ToImmutableArray();
783 
784  var option = new ContextMenuOption(TextManager.Get("delete"), isEnabled: (wireOption.IsSome() || nodeOption is CircuitBoxComponent or CircuitBoxLabelNode) && !Locked, () =>
785  {
786  if (wireOption.TryUnwrap(out var wire))
787  {
788  CircuitBox.RemoveWires(wire.IsSelected ? wireSelection : ImmutableArray.Create(wire));
789  }
790 
791  switch (nodeOption)
792  {
793  case CircuitBoxComponent node:
794  CircuitBox.RemoveComponents(node.IsSelected ? nodeSelection : ImmutableArray.Create(node));
795  break;
796  case CircuitBoxLabelNode label:
797  CircuitBox.RemoveLabel(label.IsSelected ? labels : ImmutableArray.Create(label));
798  break;
799  }
800  });
801 
802  var editLabel = new ContextMenuOption(TextManager.Get("circuitboxeditlabel"), isEnabled: nodeOption is CircuitBoxLabelNode && !Locked, () =>
803  {
804  if (circuitComponent is null) { return; }
805  if (nodeOption is not CircuitBoxLabelNode label) { return; }
806 
807  label.PromptEditText(circuitComponent);
808  });
809 
810  var editConnections = new ContextMenuOption(TextManager.Get("circuitboxrenameconnections"), isEnabled: nodeOption is CircuitBoxInputOutputNode && !Locked, () =>
811  {
812  if (circuitComponent is null) { return; }
813  if (nodeOption is not CircuitBoxInputOutputNode io) { return; }
814 
815  io.PromptEdit(circuitComponent);
816  });
817 
818  var addLabelOption = new ContextMenuOption(TextManager.Get("circuitboxaddlabel"), isEnabled: !Locked, () =>
819  {
820  CircuitBox.AddLabel(cursorPos);
821  });
822 
823  ContextMenuOption[] allOptions = { addLabelOption, editLabel, editConnections, option };
824 
825  // show component name in the header to better indicate what is about to be deleted
826  if (nodeOption is CircuitBoxComponent comp)
827  {
828  GUIContextMenu.CreateContextMenu(PlayerInput.MousePosition, comp.Item.Name, comp.Item.Prefab.SignalComponentColor, allOptions);
829  return;
830  }
831 
832  // also check if a wire is being deleted
833  if (wireOption.TryUnwrap(out var foundWire))
834  {
835  GUIContextMenu.CreateContextMenu(PlayerInput.MousePosition, foundWire.UsedItemPrefab.Name, foundWire.Color, allOptions);
836  return;
837  }
838 
839  GUIContextMenu.CreateContextMenu(allOptions);
840  }
841 
842  public CircuitBoxNode? GetTopmostNode(ImmutableHashSet<CircuitBoxNode> nodes)
843  {
844  CircuitBoxNode? foundNode = null;
845 
846  var allNodes = MouseSnapshotHandler.Nodes.ToImmutableArray();
847 
848  for (int i = allNodes.Length - 1; i >= 0; i--)
849  {
850  CircuitBoxNode node = allNodes[i];
851 
852  if (nodes.Contains(node))
853  {
854  foundNode = node;
855  break;
856  }
857  }
858 
859  return foundNode;
860  }
861 
862  public void AddToGUIUpdateList()
863  {
864  toggleMenuButton?.AddToGUIUpdateList();
865  selectedWireFrame?.AddToGUIUpdateList();
866  }
867  }
868 }
CursorState
Definition: GUI.cs:40