using System; using Robust.Shared.Input; using Robust.Shared.Maths; using Robust.Shared.ViewVariables; namespace Robust.Client.UserInterface.Controls { [Virtual] public class SplitContainer : Container { /// /// Defines how user-initiated moving of the split should work. See documentation /// for each enum value to see how the different options work. /// [ViewVariables(VVAccess.ReadWrite)] public SplitResizeMode ResizeMode { get; set; } /// /// Width of the split in virtual pixels /// [ViewVariables(VVAccess.ReadWrite)] public float SplitWidth { get => _splitWidth; set { _splitWidth = value; InvalidateMeasure(); } } private float _splitWidth; /// /// Virtual pixel offset from the edge beyond which the split cannot be moved. /// [ViewVariables(VVAccess.ReadWrite)] public float SplitEdgeSeparation { get; set; } private float _splitStart; public float SplitCenter { get => _splitStart + _splitWidth / 2; set { State = SplitState.Manual; _splitStart = value - _splitWidth / 2; ClampSplitCenter(); InvalidateMeasure(); } } public float SplitFraction { get { var size = Vertical ? Size.Y : Size.X; if (size == 0) return 0; return SplitCenter / size; } set { SplitCenter = value * (Vertical ? Size.Y : Size.X); } } private SplitState _splitState; private bool _dragging; private SplitOrientation _orientation; private bool Vertical => Orientation == SplitOrientation.Vertical; public SplitState State { get => _splitState; set { _splitState = value; InvalidateMeasure(); } } [ViewVariables(VVAccess.ReadWrite)] public SplitOrientation Orientation { get => _orientation; set { _orientation = value; InvalidateMeasure(); } } public SplitContainer() { MouseFilter = MouseFilterMode.Stop; _splitState = SplitState.Auto; _dragging = false; ResizeMode = SplitResizeMode.RespectChildrenMinSize; SplitWidth = 10; SplitEdgeSeparation = 10; } protected internal override void MouseMove(GUIMouseMoveEventArgs args) { base.MouseMove(args); if (ResizeMode == SplitResizeMode.NotResizable) return; if (_dragging) { var newOffset = Vertical ? args.RelativePosition.Y : args.RelativePosition.X; SplitCenter = newOffset; DefaultCursorShape = Vertical ? CursorShape.VResize : CursorShape.HResize; } else { // on mouseover, check if they are over the split and change the cursor accordingly var cursor = CursorShape.Arrow; if (CanDragAt(args.RelativePosition)) { cursor = Vertical ? CursorShape.VResize : CursorShape.HResize; } DefaultCursorShape = cursor; } } protected internal override void KeyBindDown(GUIBoundKeyEventArgs args) { base.KeyBindDown(args); if (ResizeMode == SplitResizeMode.NotResizable) return; if (_dragging || args.Function != EngineKeyFunctions.UIClick) return; if (CanDragAt(args.RelativePosition)) { _dragging = true; _splitState = SplitState.Manual; } } protected internal override void KeyBindUp(GUIBoundKeyEventArgs args) { base.KeyBindUp(args); if (args.Function != EngineKeyFunctions.UIClick) return; _dragging = false; DefaultCursorShape = CursorShape.Arrow; } private bool CanDragAt(Vector2 relativePosition) { if (Vertical) { return Math.Abs(relativePosition.Y - SplitCenter) <= _splitWidth; } return Math.Abs(relativePosition.X - SplitCenter) <= _splitWidth; } /// /// Ensures the split center is within all necessary limits /// /// min size of the first child, will calculate if null /// min size of the second child, will calculate if null /// private void ClampSplitCenter(Vector2? desiredSize = null, float? firstMinSize = null, float? secondMinSize = null) { // min / max x and y extents in relative virtual pixels of where the split can go regardless // of anything else. var controlSize = desiredSize ?? Size; var splitMax = (Vertical ? controlSize.Y : controlSize.X) - _splitWidth - SplitEdgeSeparation; _splitStart = MathHelper.Clamp(_splitStart, SplitEdgeSeparation, splitMax); if (ResizeMode == SplitResizeMode.RespectChildrenMinSize && ChildCount == 2) { var first = GetChild(0); var second = GetChild(1); firstMinSize ??= (Vertical ? first.DesiredSize.Y : first.DesiredSize.X); secondMinSize ??= (Vertical ? second.DesiredSize.Y : second.DesiredSize.X); var size = Vertical ? controlSize.Y : controlSize.X; _splitStart = MathHelper.Clamp(_splitStart, firstMinSize.Value, size - (secondMinSize.Value + _splitWidth)); } } protected override Vector2 ArrangeOverride(Vector2 finalSize) { if (ChildCount != 2) { return finalSize; } var first = GetChild(0); var second = GetChild(1); var firstExpand = Vertical ? first.VerticalExpand : first.HorizontalExpand; var secondExpand = Vertical ? second.VerticalExpand : second.HorizontalExpand; var firstDesiredSize = Vertical ? first.DesiredSize.Y : first.DesiredSize.X; var secondDesiredSize = Vertical ? second.DesiredSize.Y : second.DesiredSize.X; var size = Vertical ? finalSize.Y : finalSize.X; var ratio = first.SizeFlagsStretchRatio / (first.SizeFlagsStretchRatio + second.SizeFlagsStretchRatio); switch (_splitState) { case SplitState.Manual: // min sizes of children may have changed, ensure the offset still respects the defined limits ClampSplitCenter(finalSize, firstDesiredSize, secondDesiredSize); break; case SplitState.Auto: { if (firstExpand && secondExpand) { _splitStart = size * ratio - _splitWidth / 2; } else if (firstExpand) { _splitStart = size - secondDesiredSize - _splitWidth; } else if (secondExpand) { _splitStart = firstDesiredSize; } else { ratio = firstDesiredSize + secondDesiredSize <= 0 ? 0.5f : firstDesiredSize / (firstDesiredSize + secondDesiredSize); _splitStart = size * ratio - _splitWidth / 2; } _splitStart += MathHelper.Clamp(0f, firstDesiredSize - _splitStart, size - secondDesiredSize - _splitWidth - _splitStart); break; } } if (Vertical) { first.Arrange(new UIBox2(0, 0, finalSize.X, _splitStart)); second.Arrange(new UIBox2(0, _splitStart + _splitWidth, finalSize.X, finalSize.Y)); } else { first.Arrange(new UIBox2(0, 0, _splitStart, finalSize.Y)); second.Arrange(new UIBox2(_splitStart + _splitWidth, 0, finalSize.X, finalSize.Y)); } return finalSize; } protected override Vector2 MeasureOverride(Vector2 availableSize) { if (ChildCount != 2) { return Vector2.Zero; } var first = GetChild(0); var second = GetChild(1); if (State == SplitState.Manual) { var size = availableSize; if (Vertical) size.Y = _splitStart; else size.X = _splitStart; size = Vector2.ComponentMin(availableSize, size); first.Measure(size); size = availableSize; if (Vertical) size.Y = availableSize.Y - _splitStart - _splitWidth; else size.X = availableSize.X - _splitStart - _splitWidth; size = Vector2.ComponentMax(size, Vector2.Zero); second.Measure(size); } else { if (Vertical) availableSize.Y = MathF.Max(0, availableSize.Y - _splitWidth); else availableSize.X = MathF.Max(0, availableSize.X - _splitWidth); first.Measure(availableSize); if (Vertical) availableSize.Y = MathF.Max(0, availableSize.Y - first.DesiredSize.Y); else availableSize.X = MathF.Max(0, availableSize.X - first.DesiredSize.X); second.Measure(availableSize); } if (Vertical) { var width = MathF.Max(first.DesiredSize.X, second.DesiredPixelSize.X); var height = first.DesiredSize.Y + _splitWidth + second.DesiredPixelSize.Y; return (width, height); } else { var width = first.DesiredSize.X + _splitWidth + second.DesiredPixelSize.X; var height = MathF.Max(first.DesiredSize.Y, second.DesiredPixelSize.Y); return (width, height); } } /// /// Defines how user-initiated moving of the split should work /// public enum SplitResizeMode : sbyte { /// /// Don't allow user to move the split. /// NotResizable = -1, /// /// User can resize the split but can't shrink either child /// beyond its minimum size. /// /// This ensures that no child ends up with its content outside the /// edges of its container. /// RespectChildrenMinSize = 0, } /// /// Defines how the split position should be determined /// public enum SplitState : byte { /// /// Automatically adjust the split based on the width of the children /// Auto = 0, /// /// Manually adjust the split by dragging it /// Manual = 1 } public enum SplitOrientation : byte { Horizontal, Vertical } } }