From 6a1ca131114c242bb2c71b49290838ab13c2f220 Mon Sep 17 00:00:00 2001 From: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com> Date: Thu, 12 Aug 2021 10:05:02 -0700 Subject: [PATCH] Implement Entity List Display and rework StorageComponent window (#4140) * Create EntityListDisplay * Rework ClientStorage window * Add styling * Remove unnecessary colors * Rename list * Make scrollbar push content * Change children update a bit * Add old index * Localize ClientStorageComponent * Add size return * Remove spaces * Fix usings --- .../Storage/ClientStorageComponent.cs | 198 ++++-------- Content.Client/Stylesheets/StyleNano.cs | 28 ++ .../Controls/EntityListDisplay.cs | 292 ++++++++++++++++++ .../en-US/components/storage-component.ftl | 4 + 4 files changed, 389 insertions(+), 133 deletions(-) create mode 100644 Content.Client/UserInterface/Controls/EntityListDisplay.cs create mode 100644 Resources/Locale/en-US/components/storage-component.ftl diff --git a/Content.Client/Storage/ClientStorageComponent.cs b/Content.Client/Storage/ClientStorageComponent.cs index 5f158365a6..a264f77e00 100644 --- a/Content.Client/Storage/ClientStorageComponent.cs +++ b/Content.Client/Storage/ClientStorageComponent.cs @@ -1,8 +1,9 @@ -using System; using System.Collections.Generic; using System.Linq; using Content.Client.Animations; +using Content.Client.Items.Components; using Content.Client.Hands; +using Content.Client.UserInterface.Controls; using Content.Shared.DragDrop; using Content.Shared.Storage; using Robust.Client.GameObjects; @@ -13,9 +14,11 @@ using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Shared.GameObjects; using Robust.Shared.IoC; +using Robust.Shared.Localization; using Robust.Shared.Maths; using Robust.Shared.Network; using Robust.Shared.Players; +using static Robust.Client.UserInterface.Control; using static Robust.Client.UserInterface.Controls.BoxContainer; namespace Content.Client.Storage @@ -47,6 +50,8 @@ namespace Content.Client.Storage base.OnAdd(); _window = new StorageWindow(this) {Title = Owner.Name}; + _window.EntityList.GenerateItem += GenerateButton; + _window.EntityList.ItemPressed += Interact; } protected override void OnRemove() @@ -104,7 +109,7 @@ namespace Content.Client.Storage _storedEntities = storageState.StoredEntities.Select(id => Owner.EntityManager.GetEntity(id)).ToList(); StorageSizeUsed = storageState.StorageSizeUsed; StorageCapacityMax = storageState.StorageSizeMax; - _window?.BuildEntityList(); + _window?.BuildEntityList(storageState.StoredEntities.ToList()); } /// @@ -172,14 +177,53 @@ namespace Content.Client.Storage return false; } + /// + /// Button created for each entity that represents that item in the storage UI, with a texture, and name and size label + /// + private void GenerateButton(EntityUid entityUid, Control button) + { + if (!Owner.EntityManager.TryGetEntity(entityUid, out var entity)) + return; + + entity.TryGetComponent(out ISpriteComponent? sprite); + entity.TryGetComponent(out ItemComponent? item); + + button.AddChild(new HBoxContainer + { + SeparationOverride = 2, + Children = + { + new SpriteView + { + HorizontalAlignment = HAlignment.Left, + VerticalAlignment = VAlignment.Center, + MinSize = new Vector2(32.0f, 32.0f), + OverrideDirection = Direction.South, + Sprite = sprite + }, + new Label + { + HorizontalExpand = true, + ClipText = true, + Text = entity.Name + }, + new Label + { + Align = Label.AlignMode.Right, + Text = item?.Size.ToString() ?? Loc.GetString("no-item-size") + } + } + }); + } + /// /// GUI class for client storage component /// private class StorageWindow : SS14Window { - private Control VSplitContainer; - private readonly BoxContainer _entityList; + private Control _vBox; private readonly Label _information; + public readonly EntityListDisplay EntityList; public ClientStorageComponent StorageEntity; private readonly StyleBoxFlat _hoveredBox = new() { BackgroundColor = Color.Black.WithAlpha(0.35f) }; @@ -189,20 +233,21 @@ namespace Content.Client.Storage { StorageEntity = storageEntity; SetSize = (200, 320); - Title = "Storage Item"; + Title = Loc.GetString("comp-storage-window-title"); RectClipContent = true; var containerButton = new ContainerButton { + Name = "StorageContainerButton", MouseFilter = MouseFilterMode.Pass, }; + Contents.AddChild(containerButton); var innerContainerButton = new PanelContainer { PanelOverride = _unHoveredBox, }; - containerButton.AddChild(innerContainerButton); containerButton.OnPressed += args => { @@ -214,42 +259,30 @@ namespace Content.Client.Storage } }; - VSplitContainer = new BoxContainer + _vBox = new BoxContainer() { Orientation = LayoutOrientation.Vertical, MouseFilter = MouseFilterMode.Ignore, }; - containerButton.AddChild(VSplitContainer); + containerButton.AddChild(_vBox); _information = new Label { - Text = "Items: 0 Volume: 0/0 Stuff", + Text = Loc.GetString("comp-storage-window-volume", ("itemCount", 0), ("usedVolume", 0), ("maxVolume", 0)), VerticalAlignment = VAlignment.Center }; - VSplitContainer.AddChild(_information); + _vBox.AddChild(_information); - var listScrollContainer = new ScrollContainer + EntityList = new EntityListDisplay { - VerticalExpand = true, - HorizontalExpand = true, - HScrollEnabled = false, - VScrollEnabled = true, + Name = "EntityListContainer", }; - _entityList = new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - HorizontalExpand = true - }; - listScrollContainer.AddChild(_entityList); - VSplitContainer.AddChild(listScrollContainer); - - Contents.AddChild(containerButton); - - listScrollContainer.OnMouseEntered += args => + _vBox.AddChild(EntityList); + EntityList.OnMouseEntered += args => { innerContainerButton.PanelOverride = _hoveredBox; }; - listScrollContainer.OnMouseExited += args => + EntityList.OnMouseExited += args => { innerContainerButton.PanelOverride = _unHoveredBox; }; @@ -264,122 +297,21 @@ namespace Content.Client.Storage /// /// Loops through stored entities creating buttons for each, updates information labels /// - public void BuildEntityList() + public void BuildEntityList(List entityUids) { - _entityList.DisposeAllChildren(); - - var storageList = StorageEntity.StoredEntities; - - var storedGrouped = storageList.GroupBy(e => e).Select(e => new - { - Entity = e.Key, - Amount = e.Count() - }); - - foreach (var group in storedGrouped) - { - var entity = group.Entity; - var button = new EntityButton() - { - EntityUid = entity.Uid, - MouseFilter = MouseFilterMode.Stop, - }; - button.ActualButton.OnToggled += OnItemButtonToggled; - //Name and Size labels set - button.EntityName.Text = entity.Name; - - button.EntitySize.Text = group.Amount.ToString(); - - //Gets entity sprite and assigns it to button texture - if (entity.TryGetComponent(out ISpriteComponent? sprite)) - { - button.EntitySpriteView.Sprite = sprite; - } - - _entityList.AddChild(button); - } + EntityList.PopulateList(entityUids); //Sets information about entire storage container current capacity if (StorageEntity.StorageCapacityMax != 0) { - _information.Text = String.Format("Items: {0}, Stored: {1}/{2}", storageList.Count, - StorageEntity.StorageSizeUsed, StorageEntity.StorageCapacityMax); + _information.Text = Loc.GetString("comp-storage-window-volume", ("itemCount", entityUids.Count), + ("usedVolume", StorageEntity.StorageSizeUsed), ("maxVolume", StorageEntity.StorageCapacityMax)); } else { - _information.Text = String.Format("Items: {0}", storageList.Count); + _information.Text = Loc.GetString("comp-storage-window-volume-unlimited", ("itemCount", entityUids.Count)); } } - - /// - /// Function assigned to button toggle which removes the entity from storage - /// - /// - private void OnItemButtonToggled(BaseButton.ButtonToggledEventArgs args) - { - if (args.Button.Parent is not EntityButton button) - { - return; - } - - args.Button.Pressed = false; - StorageEntity.Interact(button.EntityUid); - } - } - - /// - /// Button created for each entity that represents that item in the storage UI, with a texture, and name and size label - /// - private class EntityButton : Control - { - public EntityUid EntityUid { get; set; } - public Button ActualButton { get; } - public SpriteView EntitySpriteView { get; } - public Label EntityName { get; } - public Label EntitySize { get; } - - public EntityButton() - { - ActualButton = new Button - { - HorizontalExpand = true, - VerticalExpand = true, - ToggleMode = true, - MouseFilter = MouseFilterMode.Stop - }; - AddChild(ActualButton); - - var hBoxContainer = new BoxContainer - { - Orientation = LayoutOrientation.Horizontal - }; - EntitySpriteView = new SpriteView - { - MinSize = new Vector2(32.0f, 32.0f), - OverrideDirection = Direction.South - }; - EntityName = new Label - { - VerticalAlignment = VAlignment.Center, - HorizontalExpand = true, - Margin = new Thickness(0, 0, 6, 0), - Text = "Backpack", - ClipText = true - }; - - hBoxContainer.AddChild(EntitySpriteView); - hBoxContainer.AddChild(EntityName); - - EntitySize = new Label - { - VerticalAlignment = VAlignment.Bottom, - Text = "Size 6", - Align = Label.AlignMode.Right, - }; - - hBoxContainer.AddChild(EntitySize); - AddChild(hBoxContainer); - } } } } diff --git a/Content.Client/Stylesheets/StyleNano.cs b/Content.Client/Stylesheets/StyleNano.cs index 21005e0d0c..103205b6dc 100644 --- a/Content.Client/Stylesheets/StyleNano.cs +++ b/Content.Client/Stylesheets/StyleNano.cs @@ -5,6 +5,7 @@ using Content.Client.HUD; using Content.Client.HUD.UI; using Content.Client.Resources; using Content.Client.Targeting; +using Content.Client.UserInterface.Controls; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; @@ -38,6 +39,7 @@ namespace Content.Client.Stylesheets public const string StyleClassChatChannelSelectorButton = "chatSelectorOptionButton"; public const string StyleClassChatFilterOptionButton = "chatFilterOptionButton"; public const string StyleClassContextMenuCount = "contextMenuCount"; + public const string StyleClassStorageButton = "storageButton"; public const string StyleClassSliderRed = "Red"; public const string StyleClassSliderGreen = "Green"; @@ -139,6 +141,12 @@ namespace Content.Client.Stylesheets hotbarBackground.SetPatchMargin(StyleBox.Margin.All, 2); hotbarBackground.SetExpandMargin(StyleBox.Margin.All, 4); + var buttonStorage = new StyleBoxTexture(BaseButton); + buttonStorage.SetPatchMargin(StyleBox.Margin.All, 10); + buttonStorage.SetPadding(StyleBox.Margin.All, 0); + buttonStorage.SetContentMarginOverride(StyleBox.Margin.Vertical, 0); + buttonStorage.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4); + var buttonRectTex = resCache.GetTexture("/Textures/Interface/Nano/light_panel_background_bordered.png"); var buttonRect = new StyleBoxTexture(BaseButton) { @@ -496,6 +504,26 @@ namespace Content.Client.Stylesheets new StyleProperty("font-color", Color.FromHex("#E5E5E581")), }), + // Thin buttons (No padding nor vertical margin) + Element().Class(StyleClassStorageButton) + .Prop(ContainerButton.StylePropertyStyleBox, buttonStorage), + + Element().Class(StyleClassStorageButton) + .Pseudo(ContainerButton.StylePseudoClassNormal) + .Prop(Control.StylePropertyModulateSelf, ButtonColorDefault), + + Element().Class(StyleClassStorageButton) + .Pseudo(ContainerButton.StylePseudoClassHover) + .Prop(Control.StylePropertyModulateSelf, ButtonColorHovered), + + Element().Class(StyleClassStorageButton) + .Pseudo(ContainerButton.StylePseudoClassPressed) + .Prop(Control.StylePropertyModulateSelf, ButtonColorPressed), + + Element().Class(StyleClassStorageButton) + .Pseudo(ContainerButton.StylePseudoClassDisabled) + .Prop(Control.StylePropertyModulateSelf, ButtonColorDisabled), + // action slot hotbar buttons new StyleRule(new SelectorElement(typeof(ActionSlot), null, null, new[] {ContainerButton.StylePseudoClassNormal}), new[] { diff --git a/Content.Client/UserInterface/Controls/EntityListDisplay.cs b/Content.Client/UserInterface/Controls/EntityListDisplay.cs new file mode 100644 index 0000000000..d3923470fe --- /dev/null +++ b/Content.Client/UserInterface/Controls/EntityListDisplay.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Content.Client.Stylesheets; +using JetBrains.Annotations; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.GameObjects; +using Robust.Shared.Maths; + +namespace Content.Client.UserInterface.Controls +{ + public class EntityListDisplay : Control + { + public const string StylePropertySeparation = "separation"; + + public int? SeparationOverride { get; set; } + public Action? GenerateItem; + public Action? ItemPressed; + + private const int DefaultSeparation = 3; + + private readonly VScrollBar _vScrollBar; + + private List? _entityUids; + private int _count = 0; + private float _itemHeight = 0; + private float _totalHeight = 0; + private int _topIndex = 0; + private int _bottomIndex = 0; + private bool _updateChildren = false; + private bool _suppressScrollValueChanged; + + public int ScrollSpeedY { get; set; } = 50; + + private int ActualSeparation + { + get + { + if (TryGetStyleProperty(StylePropertySeparation, out int separation)) + { + return separation; + } + + return SeparationOverride ?? DefaultSeparation; + } + } + + public EntityListDisplay() + { + HorizontalExpand = true; + VerticalExpand = true; + RectClipContent = true; + MouseFilter = MouseFilterMode.Pass; + + _vScrollBar = new VScrollBar + { + HorizontalExpand = false, + HorizontalAlignment = HAlignment.Right + }; + AddChild(_vScrollBar); + _vScrollBar.OnValueChanged += ScrollValueChanged; + } + + public void PopulateList(List entities) + { + if (_count == 0 && entities.Count > 0) + { + EntityContainerButton control = new(entities[0]); + GenerateItem?.Invoke(entities[0], control); + control.Measure(Vector2.Infinity); + _itemHeight = control.DesiredSize.Y; + control.Dispose(); + } + _count = entities.Count; + _entityUids = entities; + _updateChildren = true; + InvalidateArrange(); + } + + private void OnItemPressed(BaseButton.ButtonEventArgs args) + { + if (args.Button is not EntityContainerButton button) + return; + ItemPressed?.Invoke(button.EntityUid); + } + + [Pure] + private Vector2 GetScrollValue() + { + var v = _vScrollBar.Value; + if (!_vScrollBar.Visible) + { + v = 0; + } + return new Vector2(0, v); + } + + protected override Vector2 ArrangeOverride(Vector2 finalSize) + { + var separation = (int) (ActualSeparation * UIScale); + + #region Scroll + var cHeight = _totalHeight; + var vBarSize = _vScrollBar.DesiredSize.X; + var (sWidth, sHeight) = finalSize; + + try + { + // Suppress events to avoid weird recursion. + _suppressScrollValueChanged = true; + + if (sHeight < cHeight) + sWidth -= vBarSize; + + if (sHeight < cHeight) + { + _vScrollBar.Visible = true; + _vScrollBar.Page = sHeight; + _vScrollBar.MaxValue = cHeight; + } + else + _vScrollBar.Visible = false; + } + finally + { + _suppressScrollValueChanged = false; + } + + if (_vScrollBar.Visible) + { + _vScrollBar.Arrange(UIBox2.FromDimensions(Vector2.Zero, finalSize)); + } + #endregion + + #region Rebuild Children + /* + * Example: + * + * var _itemHeight = 32; + * var separation = 3; + * 32 | 32 | Control.Size.Y 0 + * 35 | 3 | Padding + * 67 | 32 | Control.Size.Y 1 + * 70 | 3 | Padding + * 102 | 32 | Control.Size.Y 2 + * 105 | 3 | Padding + * 137 | 32 | Control.Size.Y 3 + * + * If viewport height is 60 + * visible should be 2 items (start = 0, end = 1) + * + * scroll.Y = 11 + * visible should be 3 items (start = 0, end = 2) + * + * start expected: 11 (item: 0) + * var start = (int) (scroll.Y + * + * if (scroll == 32) then { start = 1 } + * var start = (int) (scroll.Y + separation / (_itemHeight + separation)); + * var start = (int) (32 + 3 / (32 + 3)); + * var start = (int) (35 / 35); + * var start = (int) (1); + * + * scroll = 0, height = 36 + * if (scroll + height == 36) then { end = 2 } + * var end = (int) Math.Ceiling(scroll.Y + height / (_itemHeight + separation)); + * var end = (int) Math.Ceiling(0 + 36 / (32 + 3)); + * var end = (int) Math.Ceiling(36 / 35); + * var end = (int) Math.Ceiling(1.02857); + * var end = (int) 2; + * + */ + var scroll = GetScrollValue(); + var oldTopIndex = _topIndex; + _topIndex = (int) ((scroll.Y + separation) / (_itemHeight + separation)); + if (_topIndex != oldTopIndex) + _updateChildren = true; + + var oldBottomIndex = _bottomIndex; + _bottomIndex = (int) Math.Ceiling((scroll.Y + Height) / (_itemHeight + separation)); + _bottomIndex = Math.Min(_bottomIndex, _count); + if (_bottomIndex != oldBottomIndex) + _updateChildren = true; + + // When scrolling only rebuild visible list when a new item should be visible + if (_updateChildren) + { + _updateChildren = false; + + foreach (var child in Children.ToArray()) + { + if (child == _vScrollBar) + continue; + RemoveChild(child); + } + + if (_entityUids != null) + { + for (var i = _topIndex; i < _bottomIndex; i++) + { + var entity = _entityUids[i]; + + var button = new EntityContainerButton(entity); + button.OnPressed += OnItemPressed; + + GenerateItem?.Invoke(entity, button); + AddChild(button); + } + } + + _vScrollBar.SetPositionLast(); + } + #endregion + + #region Layout Children + // Use pixel position + var pixelWidth = (int)(sWidth * UIScale); + + var offset = (int) -((scroll.Y - _topIndex * (_itemHeight + separation)) * UIScale); + var first = true; + foreach (var child in Children) + { + if (child == _vScrollBar) + continue; + if (!first) + offset += separation; + first = false; + + var size = child.DesiredPixelSize.Y; + var targetBox = new UIBox2i(0, offset, pixelWidth, offset + size); + child.ArrangePixel(targetBox); + + offset += size; + } + #endregion + + return finalSize; + } + + protected override Vector2 MeasureOverride(Vector2 availableSize) + { + _vScrollBar.Measure(availableSize); + availableSize.X -= _vScrollBar.DesiredSize.X; + + var constraint = new Vector2(availableSize.X, float.PositiveInfinity); + + var childSize = Vector2.Zero; + foreach (var child in Children) + { + child.Measure(constraint); + if (child == _vScrollBar) + continue; + childSize = Vector2.ComponentMax(childSize, child.DesiredSize); + } + + _totalHeight = childSize.Y * _count + ActualSeparation * (_count - 1); + + return new Vector2(childSize.X, 0f); + } + + private void ScrollValueChanged(Robust.Client.UserInterface.Controls.Range _) + { + if (_suppressScrollValueChanged) + { + return; + } + + InvalidateArrange(); + } + + protected override void MouseWheel(GUIMouseWheelEventArgs args) + { + base.MouseWheel(args); + + _vScrollBar.ValueTarget -= args.Delta.Y * ScrollSpeedY; + + args.Handle(); + } + } + + public class EntityContainerButton : ContainerButton + { + public EntityUid EntityUid; + + public EntityContainerButton(EntityUid entityUid) + { + EntityUid = entityUid; + AddStyleClass(StyleNano.StyleClassStorageButton); + } + } +} diff --git a/Resources/Locale/en-US/components/storage-component.ftl b/Resources/Locale/en-US/components/storage-component.ftl new file mode 100644 index 0000000000..11070caf78 --- /dev/null +++ b/Resources/Locale/en-US/components/storage-component.ftl @@ -0,0 +1,4 @@ +comp-storage-no-item-size = None +comp-storage-window-title = Storage Item +comp-storage-window-volume = Items: { $itemCount }, Stored: { $usedVolume }/{ $maxVolume } +comp-storage-window-volume-unlimited = Items: { $itemCount }