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}