ResizableChatBox.cs 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. using System.Numerics;
  2. using Robust.Client.Graphics;
  3. using Robust.Client.UserInterface;
  4. using Robust.Client.UserInterface.Controls;
  5. using Robust.Shared.Input;
  6. using Robust.Shared.Timing;
  7. namespace Content.Client.UserInterface.Systems.Chat.Widgets;
  8. public sealed class ResizableChatBox : ChatBox
  9. {
  10. public ResizableChatBox()
  11. {
  12. IoCManager.InjectDependencies(this);
  13. }
  14. // TODO: Revisit the resizing stuff after https://github.com/space-wizards/RobustToolbox/issues/1392 is done,
  15. // Probably not "supposed" to inject IClyde, but I give up.
  16. // I can't find any other way to allow this control to properly resize when the
  17. // window is resized. Resized() isn't reliably called when resizing the window,
  18. // and layoutcontainer anchor / margin don't seem to adjust how we need
  19. // them to when the window is resized. We need it to be able to resize
  20. // within some bounds so that it doesn't overlap other UI elements, while still
  21. // being freely resizable within those bounds.
  22. [Dependency] private readonly IClyde _clyde = default!;
  23. private const int DragMarginSize = 7;
  24. private const int MinDistanceFromBottom = 255;
  25. private const int MinLeft = 500;
  26. private DragMode _currentDrag = DragMode.None;
  27. private Vector2 _dragOffsetTopLeft;
  28. private Vector2 _dragOffsetBottomRight;
  29. private byte _clampIn;
  30. public Action<Vector2>? OnChatResizeFinish;
  31. protected override void EnteredTree()
  32. {
  33. base.EnteredTree();
  34. _clyde.OnWindowResized += ClydeOnOnWindowResized;
  35. }
  36. protected override void ExitedTree()
  37. {
  38. base.ExitedTree();
  39. _clyde.OnWindowResized -= ClydeOnOnWindowResized;
  40. }
  41. protected override void KeyBindDown(GUIBoundKeyEventArgs args)
  42. {
  43. if (args.Function == EngineKeyFunctions.UIClick)
  44. {
  45. _currentDrag = GetDragModeFor(args.RelativePosition);
  46. if (_currentDrag != DragMode.None)
  47. {
  48. _dragOffsetTopLeft = args.PointerLocation.Position / UIScale - Position;
  49. _dragOffsetBottomRight = Position + Size - args.PointerLocation.Position / UIScale;
  50. }
  51. }
  52. base.KeyBindDown(args);
  53. }
  54. protected override void KeyBindUp(GUIBoundKeyEventArgs args)
  55. {
  56. if (args.Function != EngineKeyFunctions.UIClick)
  57. return;
  58. if (_currentDrag != DragMode.None)
  59. {
  60. _dragOffsetTopLeft = _dragOffsetBottomRight = Vector2.Zero;
  61. _currentDrag = DragMode.None;
  62. // If this is done in MouseDown, Godot won't fire MouseUp as you need focus to receive MouseUps.
  63. UserInterfaceManager.KeyboardFocused?.ReleaseKeyboardFocus();
  64. OnChatResizeFinish?.Invoke(Size);
  65. }
  66. base.KeyBindUp(args);
  67. }
  68. // TODO: this drag and drop stuff is somewhat duplicated from Robust BaseWindow but also modified
  69. [Flags]
  70. private enum DragMode : byte
  71. {
  72. None = 0,
  73. Bottom = 1 << 1,
  74. Left = 1 << 2
  75. }
  76. private DragMode GetDragModeFor(Vector2 relativeMousePos)
  77. {
  78. var mode = DragMode.None;
  79. if (relativeMousePos.Y > Size.Y - DragMarginSize)
  80. {
  81. mode = DragMode.Bottom;
  82. }
  83. if (relativeMousePos.X < DragMarginSize)
  84. {
  85. mode |= DragMode.Left;
  86. }
  87. return mode;
  88. }
  89. protected override void MouseMove(GUIMouseMoveEventArgs args)
  90. {
  91. base.MouseMove(args);
  92. if (Parent == null)
  93. return;
  94. if (_currentDrag == DragMode.None)
  95. {
  96. var cursor = CursorShape.Arrow;
  97. var previewDragMode = GetDragModeFor(args.RelativePosition);
  98. switch (previewDragMode)
  99. {
  100. case DragMode.Bottom:
  101. cursor = CursorShape.VResize;
  102. break;
  103. case DragMode.Left:
  104. cursor = CursorShape.HResize;
  105. break;
  106. case DragMode.Bottom | DragMode.Left:
  107. cursor = CursorShape.Crosshair;
  108. break;
  109. }
  110. DefaultCursorShape = cursor;
  111. }
  112. else
  113. {
  114. var top = Rect.Top;
  115. var bottom = Rect.Bottom;
  116. var left = Rect.Left;
  117. var right = Rect.Right;
  118. var (minSizeX, minSizeY) = MinSize;
  119. if ((_currentDrag & DragMode.Bottom) == DragMode.Bottom)
  120. {
  121. bottom = Math.Max(args.GlobalPosition.Y + _dragOffsetBottomRight.Y, top + minSizeY);
  122. }
  123. if ((_currentDrag & DragMode.Left) == DragMode.Left)
  124. {
  125. var maxX = right - minSizeX;
  126. left = Math.Min(args.GlobalPosition.X - _dragOffsetTopLeft.X, maxX);
  127. }
  128. ClampSize(left, bottom);
  129. }
  130. }
  131. protected override void UIScaleChanged()
  132. {
  133. base.UIScaleChanged();
  134. ClampAfterDelay();
  135. }
  136. private void ClydeOnOnWindowResized(WindowResizedEventArgs obj)
  137. {
  138. ClampAfterDelay();
  139. }
  140. private void ClampAfterDelay()
  141. {
  142. _clampIn = 2;
  143. }
  144. protected override void FrameUpdate(FrameEventArgs args)
  145. {
  146. base.FrameUpdate(args);
  147. // we do the clamping after a delay (after UI scale / window resize)
  148. // because we need to wait for our parent container to properly resize
  149. // first, so we can calculate where we should go. If we do it right away,
  150. // we won't have the correct values from the parent to know how to adjust our margins.
  151. if (_clampIn <= 0)
  152. return;
  153. _clampIn -= 1;
  154. if (_clampIn == 0)
  155. ClampSize();
  156. }
  157. private void ClampSize(float? desiredLeft = null, float? desiredBottom = null)
  158. {
  159. if (Parent == null)
  160. return;
  161. // var top = Rect.Top;
  162. var right = Rect.Right;
  163. var left = desiredLeft ?? Rect.Left;
  164. var bottom = desiredBottom ?? Rect.Bottom;
  165. // clamp so it doesn't go too high or low (leave space for alerts UI)
  166. var maxBottom = Parent.Size.Y - MinDistanceFromBottom;
  167. if (maxBottom <= MinHeight)
  168. {
  169. // we can't fit in our given space (window made awkwardly small), so give up
  170. // and overlap at our min height
  171. bottom = MinHeight;
  172. }
  173. else
  174. {
  175. bottom = Math.Clamp(bottom, MinHeight, maxBottom);
  176. }
  177. var maxLeft = Parent.Size.X - MinWidth;
  178. if (maxLeft <= MinLeft)
  179. {
  180. // window too narrow, give up and overlap at our max left
  181. left = maxLeft;
  182. }
  183. else
  184. {
  185. left = Math.Clamp(left, MinLeft, maxLeft);
  186. }
  187. LayoutContainer.SetMarginLeft(this, -((right + 10) - left));
  188. LayoutContainer.SetMarginBottom(this, bottom);
  189. }
  190. protected override void MouseExited()
  191. {
  192. base.MouseExited();
  193. if (_currentDrag == DragMode.None)
  194. DefaultCursorShape = CursorShape.Arrow;
  195. }
  196. }