A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using UnityEditor;
5using UnityEngine;
6
7namespace Unity.VisualScripting
8{
9 public abstract class StateWidget<TState> : NodeWidget<StateCanvas, TState>, IStateWidget
10 where TState : class, IState
11 {
12 protected StateWidget(StateCanvas canvas, TState state) : base(canvas, state)
13 {
14 minResizeSize = new Vector2(State.DefaultWidth, 0);
15 }
16
17 public virtual bool canForceEnter => true;
18
19 public virtual bool canForceExit => true;
20
21 public virtual bool canToggleStart => true;
22
23
24 #region Model
25
26 protected TState state => element;
27
28 protected IStateDebugData stateDebugData => GetDebugData<IStateDebugData>();
29
30 protected State.Data stateData => reference.hasData ? reference.GetElementData<State.Data>(state) : null;
31
32 IState IStateWidget.state => state;
33
34 protected StateDescription description { get; private set; }
35
36 protected StateAnalysis analysis => state.Analysis<StateAnalysis>(context);
37
38 protected override void CacheDescription()
39 {
40 description = state.Description<StateDescription>();
41
42 title = description.title;
43 summary = description.summary;
44
45 titleContent.text = " " + title;
46 titleContent.image = description.icon?[IconSize.Small];
47 summaryContent.text = summary;
48
49 Reposition();
50 }
51
52 #endregion
53
54
55 #region Lifecycle
56
57 public override void BeforeFrame()
58 {
59 base.BeforeFrame();
60
61 if (currentContentOuterHeight != targetContentOuterHeight)
62 {
63 Reposition();
64 }
65 }
66
67 public override void HandleInput()
68 {
69 if (e.IsMouseDrag(MouseButton.Left) &&
70 e.ctrlOrCmd &&
71 !canvas.isCreatingTransition)
72 {
73 if (state.canBeSource)
74 {
75 canvas.StartTransition(state);
76 }
77 else
78 {
79 Debug.LogWarning("Cannot create a transition from this state.\n");
80 }
81
82 e.Use();
83 }
84 else if (e.IsMouseDrag(MouseButton.Left) && canvas.isCreatingTransition)
85 {
86 e.Use();
87 }
88 else if (e.IsMouseUp(MouseButton.Left) && canvas.isCreatingTransition)
89 {
90 var source = canvas.transitionSource;
91 var destination = (canvas.hoveredWidget as IStateWidget)?.state;
92
93 if (destination == null)
94 {
95 canvas.CompleteTransitionToNewState();
96 }
97 else if (destination == source)
98 {
99 canvas.CancelTransition();
100 }
101 else if (destination.canBeDestination)
102 {
103 canvas.EndTransition(destination);
104 }
105 else
106 {
107 Debug.LogWarning("Cannot create a transition to this state.\n");
108 canvas.CancelTransition();
109 }
110
111 e.Use();
112 }
113
114 base.HandleInput();
115 }
116
117 #endregion
118
119
120 #region Contents
121
122 protected virtual string title { get; set; }
123
124 protected virtual string summary { get; set; }
125
126 private GUIContent titleContent { get; } = new GUIContent();
127
128 private GUIContent summaryContent { get; } = new GUIContent();
129
130 #endregion
131
132
133 #region Positioning
134
135 public override IEnumerable<IWidget> positionDependers => state.transitions.Select(transition => (IWidget)canvas.Widget(transition));
136
137 public Rect titlePosition { get; private set; }
138
139 public Rect summaryPosition { get; private set; }
140
141 public Rect contentOuterPosition { get; private set; }
142
143 public Rect contentBackgroundPosition { get; private set; }
144
145 public Rect contentInnerPosition { get; private set; }
146
147 private float targetContentOuterHeight;
148
149 private float currentContentOuterHeight;
150
151 private bool revealInitialized;
152
153 private Rect _position;
154
155 public override Rect position
156 {
157 get { return _position; }
158 set
159 {
160 state.position = value.position;
161 state.width = value.width;
162 }
163 }
164
165 public override void CachePosition()
166 {
167 var edgeOrigin = state.position;
168 var edgeX = edgeOrigin.x;
169 var edgeY = edgeOrigin.y;
170 var edgeWidth = state.width;
171 var innerOrigin = EdgeToInnerPosition(new Rect(edgeOrigin, Vector2.zero)).position;
172 var innerX = innerOrigin.x;
173 var innerY = innerOrigin.y;
174 var innerWidth = EdgeToInnerPosition(new Rect(0, 0, edgeWidth, 0)).width;
175 var innerHeight = 0f;
176
177 var y = innerY;
178
179 if (showTitle)
180 {
181 using (LudiqGUIUtility.iconSize.Override(IconSize.Small))
182 {
183 titlePosition = new Rect
184 (
185 innerX,
186 y,
187 innerWidth,
188 Styles.title.CalcHeight(titleContent, innerWidth)
189 );
190
191 y += titlePosition.height;
192 innerHeight += titlePosition.height;
193 }
194 }
195
196 if (showTitle && showSummary)
197 {
198 y += Styles.spaceBetweenTitleAndSummary;
199 innerHeight += Styles.spaceBetweenTitleAndSummary;
200 }
201
202 if (showSummary)
203 {
204 summaryPosition = new Rect
205 (
206 innerX,
207 y,
208 innerWidth,
209 Styles.summary.CalcHeight(summaryContent, innerWidth)
210 );
211
212 y += summaryPosition.height;
213 innerHeight += summaryPosition.height;
214 }
215
216 if (showContent)
217 {
218 var contentInnerWidth = edgeWidth - Styles.contentBackground.padding.left - Styles.contentBackground.padding.right;
219
220 targetContentOuterHeight = revealContent ? (Styles.spaceBeforeContent + Styles.contentBackground.padding.top + GetContentHeight(contentInnerWidth) + Styles.contentBackground.padding.bottom) : 0;
221
222 if (!revealInitialized)
223 {
224 currentContentOuterHeight = targetContentOuterHeight;
225 revealInitialized = true;
226 }
227
228 currentContentOuterHeight = Mathf.Lerp(currentContentOuterHeight, targetContentOuterHeight, canvas.repaintDeltaTime * Styles.contentRevealSpeed);
229
230 if (Mathf.Abs(targetContentOuterHeight - currentContentOuterHeight) < 1)
231 {
232 currentContentOuterHeight = targetContentOuterHeight;
233 }
234
235 contentOuterPosition = new Rect
236 (
237 edgeX,
238 y,
239 edgeWidth,
240 currentContentOuterHeight
241 );
242
243 contentBackgroundPosition = new Rect
244 (
245 0,
246 Styles.spaceBeforeContent,
247 edgeWidth,
248 currentContentOuterHeight - Styles.spaceBeforeContent
249 );
250
251 contentInnerPosition = new Rect
252 (
253 Styles.contentBackground.padding.left,
254 Styles.spaceBeforeContent + Styles.contentBackground.padding.top,
255 contentInnerWidth,
256 contentBackgroundPosition.height - Styles.contentBackground.padding.top
257 );
258
259 y += contentOuterPosition.height;
260 innerHeight += contentOuterPosition.height;
261 }
262
263 var edgeHeight = InnerToEdgePosition(new Rect(0, 0, 0, innerHeight)).height;
264
265 _position = new Rect
266 (
267 edgeX,
268 edgeY,
269 edgeWidth,
270 edgeHeight
271 );
272 }
273
274 protected virtual float GetContentHeight(float width) => 0;
275
276 #endregion
277
278
279 #region Drawing
280
281 protected virtual bool showTitle => true;
282
283 protected virtual bool showSummary => !StringUtility.IsNullOrWhiteSpace(summary);
284
285 protected virtual bool showContent => false;
286
287 protected virtual NodeColorMix baseColor => NodeColor.Gray;
288
289 protected override NodeColorMix color
290 {
291 get
292 {
293 if (stateDebugData.runtimeException != null)
294 {
295 return NodeColor.Red;
296 }
297
298 var color = baseColor;
299
300 if (state.isStart)
301 {
302 color = NodeColor.Green;
303 }
304
305 if (stateData?.isActive ?? false)
306 {
307 color = NodeColor.Blue;
308 }
309 else if (EditorApplication.isPaused)
310 {
311 if (EditorTimeBinding.frame == stateDebugData.lastEnterFrame)
312 {
313 color = NodeColor.Blue;
314 }
315 }
316 else
317 {
318 color.blue = Mathf.Lerp(1, 0, (EditorTimeBinding.time - stateDebugData.lastExitTime) / Styles.enterFadeDuration);
319 }
320
321 return color;
322 }
323 }
324
325 protected override NodeShape shape => NodeShape.Square;
326
327 private bool revealContent
328 {
329 get
330 {
331 switch (BoltState.Configuration.statesReveal)
332 {
333 case StateRevealCondition.Always:
334
335 return true;
336 case StateRevealCondition.Never:
337
338 return false;
339 case StateRevealCondition.OnHover:
340
341 return isMouseOver;
342 case StateRevealCondition.OnHoverWithAlt:
343
344 return isMouseOver && e.alt;
345 case StateRevealCondition.WhenSelected:
346
347 return selection.Contains(state);
348 case StateRevealCondition.OnHoverOrSelected:
349
350 return isMouseOver || selection.Contains(state);
351 case StateRevealCondition.OnHoverWithAltOrSelected:
352
353 return isMouseOver && e.alt || selection.Contains(state);
354 default:
355
356 throw new UnexpectedEnumValueException<StateRevealCondition>(BoltState.Configuration.statesReveal);
357 }
358 }
359 }
360
361 private bool revealedContent;
362
363 private void CheckReveal()
364 {
365 var revealContent = this.revealContent;
366
367 if (revealContent != revealedContent)
368 {
369 Reposition();
370 }
371
372 revealedContent = revealContent;
373 }
374
375 protected override bool dim
376 {
377 get
378 {
379 var dim = BoltCore.Configuration.dimInactiveNodes && !analysis.isEntered;
380
381 if (isMouseOver || isSelected)
382 {
383 dim = false;
384 }
385
386 return dim;
387 }
388 }
389
390 public override void DrawForeground()
391 {
392 BeginDim();
393
394 base.DrawForeground();
395
396 if (showTitle)
397 {
398 DrawTitle();
399 }
400
401 if (showSummary)
402 {
403 DrawSummary();
404 }
405
406 if (showContent)
407 {
408 DrawContentWrapped();
409 }
410
411 EndDim();
412
413 CheckReveal();
414 }
415
416 private void DrawTitle()
417 {
418 using (LudiqGUIUtility.iconSize.Override(IconSize.Small))
419 {
420 GUI.Label(titlePosition, titleContent, invertForeground ? Styles.titleInverted : Styles.title);
421 }
422 }
423
424 private void DrawSummary()
425 {
426 GUI.Label(summaryPosition, summaryContent, invertForeground ? Styles.summaryInverted : Styles.summary);
427 }
428
429 private void DrawContentWrapped()
430 {
431 GUI.BeginClip(contentOuterPosition);
432
433 DrawContentBackground();
434
435 DrawContent();
436
437 GUI.EndClip();
438 }
439
440 protected virtual void DrawContentBackground()
441 {
442 if (e.IsRepaint)
443 {
444 Styles.contentBackground.Draw(contentBackgroundPosition, false, false, false, false);
445 }
446 }
447
448 protected virtual void DrawContent() { }
449
450 #endregion
451
452
453 #region Selecting
454
455 public override bool canSelect => true;
456
457 #endregion
458
459
460 #region Dragging
461
462 protected override bool snapToGrid => BoltCore.Configuration.snapToGrid;
463
464 public override bool canDrag => true;
465
466 public override void ExpandDragGroup(HashSet<IGraphElement> dragGroup)
467 {
468 if (BoltCore.Configuration.carryChildren)
469 {
470 foreach (var transition in state.outgoingTransitions)
471 {
472 if (dragGroup.Contains(transition.destination))
473 {
474 continue;
475 }
476
477 dragGroup.Add(transition.destination);
478
479 canvas.Widget(transition.destination).ExpandDragGroup(dragGroup);
480 }
481 }
482 }
483
484 #endregion
485
486
487 #region Deleting
488
489 public override bool canDelete => true;
490
491 #endregion
492
493
494 #region Resizing
495
496 public override bool canResizeHorizontal => true;
497
498 #endregion
499
500
501 #region Clipboard
502
503 public override void ExpandCopyGroup(HashSet<IGraphElement> copyGroup)
504 {
505 copyGroup.UnionWith(state.transitions.Cast<IGraphElement>());
506 }
507
508 #endregion
509
510
511 #region Actions
512
513 protected override IEnumerable<DropdownOption> contextOptions
514 {
515 get
516 {
517 if (Application.isPlaying && reference.hasData)
518 {
519 if (canForceEnter)
520 {
521 yield return new DropdownOption((Action)ForceEnter, "Force Enter");
522 }
523
524 if (canForceExit)
525 {
526 yield return new DropdownOption((Action)ForceExit, "Force Exit");
527 }
528 }
529
530 if (canToggleStart)
531 {
532 yield return new DropdownOption((Action)ToggleStart, "Toggle Start");
533 }
534
535 if (state.canBeSource)
536 {
537 yield return new DropdownOption((Action)MakeTransition, "Make Transition");
538 }
539
540 if (state.canBeSource && state.canBeDestination)
541 {
542 yield return new DropdownOption((Action)MakeSelfTransition, "Make Self Transition");
543 }
544
545 foreach (var baseOption in base.contextOptions)
546 {
547 yield return baseOption;
548 }
549 }
550 }
551
552 private void ForceEnter()
553 {
554 using (var flow = Flow.New(reference))
555 {
556 state.OnEnter(flow, StateEnterReason.Forced);
557 }
558 }
559
560 private void ForceExit()
561 {
562 using (var flow = Flow.New(reference))
563 {
564 state.OnExit(flow, StateExitReason.Forced);
565 }
566 }
567
568 protected void MakeTransition()
569 {
570 canvas.StartTransition(state);
571 }
572
573 protected void MakeSelfTransition()
574 {
575 canvas.StartTransition(state);
576 canvas.EndTransition(state);
577 }
578
579 protected void ToggleStart()
580 {
581 UndoUtility.RecordEditedObject("Toggle State Start");
582
583 state.isStart = !state.isStart;
584 }
585
586 #endregion
587
588
589 public static class Styles
590 {
591 static Styles()
592 {
593 title = new GUIStyle(BoltCore.Styles.nodeLabel);
594 title.fontSize = 12;
595 title.alignment = TextAnchor.MiddleCenter;
596 title.wordWrap = true;
597
598 summary = new GUIStyle(BoltCore.Styles.nodeLabel);
599 summary.fontSize = 10;
600 summary.alignment = TextAnchor.MiddleCenter;
601 summary.wordWrap = true;
602
603 titleInverted = new GUIStyle(title);
604 titleInverted.normal.textColor = ColorPalette.unityBackgroundDark;
605
606 summaryInverted = new GUIStyle(summary);
607 summaryInverted.normal.textColor = ColorPalette.unityBackgroundDark;
608
609 contentBackground = new GUIStyle("In BigTitle");
610 contentBackground.padding = new RectOffset(0, 0, 4, 4);
611 }
612
613 public static readonly GUIStyle title;
614
615 public static readonly GUIStyle summary;
616
617 public static readonly GUIStyle titleInverted;
618
619 public static readonly GUIStyle summaryInverted;
620
621 public static readonly GUIStyle contentBackground;
622
623 public static readonly float spaceBeforeContent = 5;
624
625 public static readonly float spaceBetweenTitleAndSummary = 0;
626
627 public static readonly float enterFadeDuration = 0.5f;
628
629 public static readonly float contentRevealSpeed = 15;
630 }
631 }
632}