using EventInput; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; using Microsoft.Xna.Framework.Input; namespace Barotrauma { public class GUIListBox : GUIComponent, IKeyboardSubscriber { protected List selected; public delegate bool OnSelectedHandler(GUIComponent component, object obj); /// /// Triggers when some element is clicked on the listbox. /// Note that is not set yet when this callback triggers, and returning false from the callback disallows selecting it. /// public OnSelectedHandler OnSelected; /// /// Triggers after some element has been selected from the listbox. /// public OnSelectedHandler AfterSelected; public delegate object CheckSelectedHandler(); public CheckSelectedHandler CheckSelected; public delegate void OnRearrangedHandler(GUIListBox listBox, object obj); public OnRearrangedHandler OnRearranged; /// /// A frame drawn behind the content of the listbox /// public GUIFrame ContentBackground { get; private set; } /// /// A frame that contains the contents of the listbox. The frame itself is not rendered. /// public GUIFrame Content { get; private set; } public GUIScrollBar ScrollBar { get; private set; } private readonly Dictionary childVisible = new Dictionary(); private int totalSize; private bool childrenNeedsRecalculation; private bool scrollBarNeedsRecalculation; private bool dimensionsNeedsRecalculation; // TODO: Define in styles? private int ScrollBarSize { get { //use the average of the "desired" size and the scaled size //scaling the bar linearly with the resolution tends to make them too large on large resolutions float desiredSize = 25.0f; float scaledSize = desiredSize * GUI.Scale; return (int)Math.Min((desiredSize + scaledSize) / 2.0f, Rect.Height / 3); } } public enum SelectMode { SelectSingle, SelectMultiple, RequireShiftToSelectMultiple, None } public SelectMode CurrentSelectMode = SelectMode.SelectSingle; public bool SelectMultiple { get { return CurrentSelectMode != SelectMode.SelectSingle; } set { CurrentSelectMode = value ? SelectMode.SelectMultiple : SelectMode.SelectSingle; } } public bool HideChildrenOutsideFrame = true; public bool ResizeContentToMakeSpaceForScrollBar = true; private bool useGridLayout; private GUIComponent scrollToElement; public bool AllowMouseWheelScroll { get; set; } = true; public bool AllowArrowKeyScroll { get; set; } = true; /// /// Scrolls the list smoothly /// public bool SmoothScroll { get; set; } /// /// Whether to only allow scrolling from one element to the next when smooth scrolling is enabled /// public bool ClampScrollToElements { get; set; } /// /// When set to true elements at the bottom of the list are gradually faded /// public bool FadeElements { get; set; } /// /// Adds enough extra padding to the bottom so the end of the scroll will only contain the last element /// public bool PadBottom { get; set; } /// /// When set to true always selects the topmost item on the list /// public bool SelectTop { get; set; } public bool UseGridLayout { get { return useGridLayout; } set { if (useGridLayout == value) return; useGridLayout = value; childrenNeedsRecalculation = true; scrollBarNeedsRecalculation = true; } } /// /// true if mouse down should select elements instead of mouse up /// private readonly bool useMouseDownToSelect = false; private Vector4? overridePadding; public Vector4 Padding { get { if (overridePadding.HasValue) { return overridePadding.Value; } if (Style == null) { return Vector4.Zero; } return Style.Padding; } set { dimensionsNeedsRecalculation = true; overridePadding = value; } } public GUIComponent SelectedComponent { get { return selected.FirstOrDefault(); } } public override bool Selected { get { return isSelected; } set { isSelected = value; } } public IReadOnlyList AllSelected => selected; public object SelectedData { get { return SelectedComponent?.UserData; } } public int SelectedIndex { get { if (SelectedComponent == null) return -1; return Content.RectTransform.GetChildIndex(SelectedComponent.RectTransform); } } public float BarScroll { get { return ScrollBar.BarScroll; } set { ScrollBar.BarScroll = value; } } public float BarSize { get { return ScrollBar.BarSize; } } public float TotalSize { get { return totalSize; } } public int Spacing { get; set; } public override Color Color { get { return base.Color; } set { base.Color = value; Content.Color = value; } } /// /// Disables the scroll bar without hiding it. /// public bool ScrollBarEnabled { get; set; } = true; public bool KeepSpaceForScrollBar { get; set; } public bool CanTakeKeyBoardFocus { get; set; } = true; public bool ScrollBarVisible { get { return ScrollBar.Visible; } set { if (ScrollBar.Visible == value) { return; } ScrollBar.Visible = value; dimensionsNeedsRecalculation = true; } } /// /// Automatically hides the scroll bar when the content fits in. /// public bool AutoHideScrollBar { get; set; } = true; private bool IsScrollBarOnDefaultSide { get; set; } public enum DragMode { NoDragging, DragWithinBox, DragOutsideBox } private DragMode currentDragMode = DragMode.NoDragging; public DragMode CurrentDragMode { get { return currentDragMode; } set { if (value == DragMode.NoDragging && currentDragMode != DragMode.NoDragging && isDraggingElement) { DraggedElement = null; } currentDragMode = value; } } private GUIComponent draggedElement; private Point dragMousePosRelativeToTopLeftCorner; private bool isDraggingElement => draggedElement != null; public bool HasDraggedElementIndexChanged { get; private set; } public GUIComponent DraggedElement { get { return draggedElement; } set { if (value == draggedElement) { return; } draggedElement = value; HasDraggedElementIndexChanged = false; if (value == null) { return; } dragMousePosRelativeToTopLeftCorner = PlayerInput.MousePosition.ToPoint() - value.Rect.Location; if (SelectMultiple) { if (!AllSelected.Contains(DraggedElement)) { Select(DraggedElement.ToEnumerable()); } } } } //This exists to work around the fact that rendering child //elements on top of the listbox's siblings is a clusterfuck. public bool HideDraggedElement = false; private readonly bool isHorizontal; /// /// Setting this to true and CanBeFocused to false allows the list background to be unfocusable while the elements can still be interacted with. /// public bool CanInteractWhenUnfocusable { get; set; } = false; public override Rectangle MouseRect { get { if (!CanBeFocused && !CanInteractWhenUnfocusable) { return Rectangle.Empty; } return ClampMouseRectToParent ? ClampRect(Rect) : Rect; } } public override bool PlaySoundOnSelect { get; set; } = false; public bool PlaySoundOnDragStop { get; set; } = false; public GUISoundType? SoundOnDragStart { get; set; } = null; public GUISoundType? SoundOnDragStop { get; set; } = null; #region enums public enum Force { Yes, No } public enum AutoScroll { Enabled, Disabled } public enum TakeKeyBoardFocus { Yes, No } public enum PlaySelectSound { Yes, No } private AutoScroll GetAutoScroll(bool b) { return b ? AutoScroll.Enabled : AutoScroll.Disabled; } #endregion /// For horizontal listbox, default side is on the bottom. For vertical, it's on the right. public GUIListBox(RectTransform rectT, bool isHorizontal = false, Color? color = null, string style = "", bool isScrollBarOnDefaultSide = true, bool useMouseDownToSelect = false) : base(style, rectT) { this.isHorizontal = isHorizontal; HoverCursor = CursorState.Hand; CanBeFocused = true; selected = new List(); this.useMouseDownToSelect = useMouseDownToSelect; ContentBackground = new GUIFrame(new RectTransform(Vector2.One, rectT), style) { CanBeFocused = false }; Content = new GUIFrame(new RectTransform(Vector2.One, ContentBackground.RectTransform), style: null) { CanBeFocused = false }; Content.RectTransform.ChildrenChanged += (_) => { scrollBarNeedsRecalculation = true; childrenNeedsRecalculation = true; }; if (style != null) { GUIStyle.Apply(ContentBackground, "", this); } if (color.HasValue) { this.color = color.Value; } IsScrollBarOnDefaultSide = isScrollBarOnDefaultSide; Point size; Anchor anchor; if (isHorizontal) { size = new Point((int)(Rect.Width - Padding.X - Padding.Z), (int)(ScrollBarSize * GUI.Scale)); anchor = isScrollBarOnDefaultSide ? Anchor.BottomCenter : Anchor.TopCenter; } else { // TODO: Should this be multiplied by the GUI.Scale as well? size = new Point(ScrollBarSize, (int)(Rect.Height - Padding.Y - Padding.W)); anchor = isScrollBarOnDefaultSide ? Anchor.CenterRight : Anchor.CenterLeft; } ScrollBar = new GUIScrollBar( new RectTransform(size, rectT, anchor) { AbsoluteOffset = isHorizontal ? new Point(0, IsScrollBarOnDefaultSide ? (int)Padding.W : (int)Padding.Y) : new Point(IsScrollBarOnDefaultSide ? (int)Padding.Z : (int)Padding.X, 0) }, isHorizontal: isHorizontal); UpdateScrollBarSize(); Enabled = true; ScrollBar.BarScroll = 0.0f; RectTransform.ScaleChanged += () => dimensionsNeedsRecalculation = true; RectTransform.SizeChanged += () => dimensionsNeedsRecalculation = true; UpdateDimensions(); rectT.ChildrenChanged += CheckForChildren; } private void CheckForChildren(RectTransform rectT) { if (rectT == ScrollBar.RectTransform || rectT == Content.RectTransform || rectT == ContentBackground.RectTransform) { return; } throw new InvalidOperationException($"Children were added to {nameof(GUIListBox)}, Add them to {nameof(GUIListBox)}.{nameof(Content)} instead."); } public void UpdateDimensions() { dimensionsNeedsRecalculation = false; ContentBackground.RectTransform.Resize(Rect.Size); bool reduceScrollbarSize = ResizeContentToMakeSpaceForScrollBar && (KeepSpaceForScrollBar ? ScrollBarEnabled : ScrollBarVisible); Point contentSize = reduceScrollbarSize ? CalculateFrameSize(ScrollBar.IsHorizontal, ScrollBarSize) : Rect.Size; Content.RectTransform.Resize(new Point((int)(contentSize.X - Padding.X - Padding.Z), (int)(contentSize.Y - Padding.Y - Padding.W))); if (!IsScrollBarOnDefaultSide) { Content.RectTransform.SetPosition(Anchor.BottomRight); } Content.RectTransform.AbsoluteOffset = new Point( IsScrollBarOnDefaultSide ? (int)Padding.X : (int)Padding.Z, IsScrollBarOnDefaultSide ? (int)Padding.Y : (int)Padding.W); ScrollBar.RectTransform.Resize(ScrollBar.IsHorizontal ? new Point((int)(Rect.Width - Padding.X - Padding.Z), ScrollBarSize) : new Point(ScrollBarSize, (int)(Rect.Height - Padding.Y - Padding.W))); ScrollBar.RectTransform.AbsoluteOffset = ScrollBar.IsHorizontal ? new Point(0, (int)Padding.W) : new Point((int)Padding.Z, 0); UpdateScrollBarSize(); } public void Select(object userData, Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled) { var children = Content.Children; int i = 0; foreach (GUIComponent child in children) { if (Equals(child.UserData, userData)) { Select(i, force, autoScroll); if (!SelectMultiple) { return; } } i++; } } private Point CalculateFrameSize(bool isHorizontal, int scrollBarSize) => isHorizontal ? new Point(Rect.Width, Rect.Height - scrollBarSize) : new Point(Rect.Width - scrollBarSize, Rect.Height); public Vector2 CalculateTopOffset() { int x = 0; int y = 0; if (ScrollBar.BarSize < 1.0f) { if (ScrollBar.IsHorizontal) { x -= (int)((totalSize - Content.Rect.Width) * ScrollBar.BarScroll); } else { y -= (int)((totalSize - Content.Rect.Height) * ScrollBar.BarScroll); } } return new Vector2(x, y); } private void CalculateChildrenOffsets(Action callback) { Vector2 topOffset = CalculateTopOffset(); int x = (int)topOffset.X; int y = (int)topOffset.Y; for (int i = 0; i < Content.CountChildren; i++) { GUIComponent child = Content.GetChild(i); if (child == null || !child.Visible) { continue; } if (RectTransform != null) { callback(i, new Point(x, y)); } if (useGridLayout) { void advanceGridLayout( ref int primaryCoord, ref int secondaryCoord, int primaryChildDimension, int secondaryChildDimension, int primaryParentDimension) { if (primaryCoord + primaryChildDimension + Spacing > primaryParentDimension) { primaryCoord = 0; secondaryCoord += secondaryChildDimension + Spacing; callback(i, new Point(x, y)); } primaryCoord += primaryChildDimension + Spacing; } if (ScrollBar.IsHorizontal) { advanceGridLayout( primaryCoord: ref y, secondaryCoord: ref x, primaryChildDimension: child.Rect.Height, secondaryChildDimension: child.Rect.Width, primaryParentDimension: Content.Rect.Height); } else { advanceGridLayout( primaryCoord: ref x, secondaryCoord: ref y, primaryChildDimension: child.Rect.Width, secondaryChildDimension: child.Rect.Height, primaryParentDimension: Content.Rect.Width); } } else { if (ScrollBar.IsHorizontal) { x += child.Rect.Width + Spacing; } else { y += child.Rect.Height + Spacing; } } } } private void RepositionChildren() { CalculateChildrenOffsets((index, offset) => { var child = Content.GetChild(index); if (child != draggedElement && child.RectTransform.AbsoluteOffset != offset) { child.RectTransform.AbsoluteOffset = offset; } }); } /// /// Scrolls the list to the specific element. /// /// public void ScrollToElement(GUIComponent component, PlaySelectSound playSelectSound = PlaySelectSound.No) { if (playSelectSound == PlaySelectSound.Yes) { SoundPlayer.PlayUISound(GUISoundType.Select); } List children = Content.Children.ToList(); int index = children.IndexOf(component); if (index < 0) { return; } void performScroll(GUIComponent c) { if (SmoothScroll && PadBottom) { scrollToElement = c; } else { float diff = isHorizontal ? c.Rect.X - Content.Rect.X : c.Rect.Y - Content.Rect.Y; ScrollBar.BarScroll += diff / TotalSize; } } if (!Content.Children.Contains(component) || !component.Visible) { performScroll(null); } else { performScroll(component); } } public void ScrollToEnd(float duration) { CoroutineManager.StartCoroutine(ScrollCoroutine()); IEnumerable ScrollCoroutine() { if (BarSize >= 1.0f) { yield return CoroutineStatus.Success; } float t = 0.0f; float startScroll = BarScroll; float distanceToTravel = ScrollBar.MaxValue - startScroll; float speed = distanceToTravel / duration; while (t < duration && !MathUtils.NearlyEqual(ScrollBar.MaxValue, BarScroll)) { t += CoroutineManager.DeltaTime; BarScroll += speed * CoroutineManager.DeltaTime; yield return CoroutineStatus.Running; } yield return CoroutineStatus.Success; } } private double lastDragStartTime; private void StartDraggingElement(GUIComponent child) { DraggedElement = child; if (Timing.TotalTime > lastDragStartTime + 0.2f) { lastDragStartTime = Timing.TotalTime; SoundPlayer.PlayUISound(SoundOnDragStart); } } private bool UpdateDragging() { if (CurrentDragMode == DragMode.NoDragging || !isDraggingElement) { return false; } if (!PlayerInput.PrimaryMouseButtonHeld()) { var draggedElem = draggedElement; OnRearranged?.Invoke(this, draggedElem.UserData); DraggedElement = null; if (PlaySoundOnDragStop) { SoundPlayer.PlayUISound(SoundOnDragStop); } RepositionChildren(); if (AllSelected.Contains(draggedElem)) { return true; } } else { Vector2 topOffset = CalculateTopOffset(); var mousePos = PlayerInput.MousePosition.ToPoint(); draggedElement.RectTransform.AbsoluteOffset = mousePos - Content.Rect.Location - dragMousePosRelativeToTopLeftCorner; if (CurrentDragMode != DragMode.DragOutsideBox) { var offset = draggedElement.RectTransform.AbsoluteOffset; draggedElement.RectTransform.AbsoluteOffset = isHorizontal ? new Point(offset.X, 0) : new Point(0, offset.Y); } int index = Content.RectTransform.GetChildIndex(draggedElement.RectTransform); int newIndex = index; Point draggedOffsetWhenReleased = Point.Zero; CalculateChildrenOffsets((i, offset) => { if (index != i) { return; } draggedOffsetWhenReleased = offset; }); Rectangle draggedRectWhenReleased = new Rectangle(Content.Rect.Location + draggedOffsetWhenReleased, draggedElement.Rect.Size); void shiftIndices( float mousePos, ref int draggedRectWhenReleasedLocation, int draggedRectWhenReleasedSize) { while (mousePos > (draggedRectWhenReleasedLocation + draggedRectWhenReleasedSize) && newIndex < Content.CountChildren-1) { newIndex++; draggedRectWhenReleasedLocation += draggedRectWhenReleasedSize; } while (mousePos < draggedRectWhenReleasedLocation && newIndex > 0) { newIndex--; draggedRectWhenReleasedLocation -= draggedRectWhenReleasedSize; } if (newIndex != index && AllSelected.Count > 1) { this.selected.Sort((a, b) => Content.GetChildIndex(a) - Content.GetChildIndex(b)); int draggedPos = AllSelected.IndexOf(draggedElement); if (newIndex < draggedPos) { newIndex = draggedPos; } if (newIndex >= Content.CountChildren - (AllSelected.Count - draggedPos)) { int max = Content.CountChildren - (AllSelected.Count - draggedPos); newIndex = max; } } } if (isHorizontal) { shiftIndices( mousePos.X, ref draggedRectWhenReleased.X, draggedRectWhenReleased.Width); } else { shiftIndices( mousePos.Y, ref draggedRectWhenReleased.Y, draggedRectWhenReleased.Height); } if (newIndex != index) { if (AllSelected.Count > 1) { this.selected.Sort((a, b) => Content.GetChildIndex(a) - Content.GetChildIndex(b)); int indexOfDraggedElem = AllSelected.IndexOf(draggedElement); IEnumerable allSelected = AllSelected; if (newIndex > index) { allSelected = allSelected.Reverse(); } foreach (var elem in allSelected) { elem.RectTransform.RepositionChildInHierarchy(newIndex + AllSelected.IndexOf(elem) - indexOfDraggedElem); } } else { draggedElement.RectTransform.RepositionChildInHierarchy(newIndex); } HasDraggedElementIndexChanged = true; } return true; } return false; } private void UpdateChildrenRect() { if (UpdateDragging()) { return; } if (SelectTop) { foreach (GUIComponent child in Content.Children) { child.CanBeFocused = !selected.Contains(child); if (!child.CanBeFocused) { child.State = ComponentState.None; } } } if (SelectTop && Content.Children.Any() && scrollToElement == null) { GUIComponent component = Content.Children.FirstOrDefault(c => (c.Rect.Y - Content.Rect.Y) / (float)c.Rect.Height > -0.1f); if (component != null && !selected.Contains(component)) { int index = Content.Children.ToList().IndexOf(component); if (index >= 0) { Select(index, autoScroll: AutoScroll.Disabled, takeKeyBoardFocus: TakeKeyBoardFocus.Yes); } } } for (int i = 0; i < Content.CountChildren; i++) { var child = Content.RectTransform.GetChild(i)?.GUIComponent; if (!(child is { Visible: true })) { continue; } // selecting if (Enabled && (CanBeFocused || CanInteractWhenUnfocusable) && child.CanBeFocused && child.Rect.Contains(PlayerInput.MousePosition) && GUI.IsMouseOn(child)) { child.State = ComponentState.Hover; var mouseDown = useMouseDownToSelect ? PlayerInput.PrimaryMouseButtonDown() : PlayerInput.PrimaryMouseButtonClicked(); if (mouseDown) { if (SelectTop) { ScrollToElement(child); } Select(i, autoScroll: AutoScroll.Disabled, takeKeyBoardFocus: TakeKeyBoardFocus.Yes, playSelectSound: PlaySelectSound.Yes); } if (CurrentDragMode != DragMode.NoDragging && (CurrentSelectMode != SelectMode.RequireShiftToSelectMultiple || (!PlayerInput.IsShiftDown() && !PlayerInput.IsCtrlDown())) && PlayerInput.PrimaryMouseButtonDown() && GUI.MouseOn == child) { StartDraggingElement(child); } } else if (selected.Contains(child)) { child.State = ComponentState.Selected; if (CheckSelected != null) { if (CheckSelected() != child.UserData) selected.Remove(child); } } else { child.State = !child.ExternalHighlight ? ComponentState.None : ComponentState.Hover; } } } public override void AddToGUIUpdateList(bool ignoreChildren = false, int order = 0) { if (!Visible) { return; } if (!ignoreChildren) { foreach (GUIComponent child in Children) { if (child == Content || child == ScrollBar || child == ContentBackground) { continue; } child.AddToGUIUpdateList(ignoreChildren, order); } } foreach (GUIComponent child in Content.Children) { if (!childVisible.ContainsKey(child)) { childVisible[child] = child.Visible; } if (childVisible[child] != child.Visible) { childVisible[child] = child.Visible; childrenNeedsRecalculation = true; scrollBarNeedsRecalculation = true; break; } } if (childrenNeedsRecalculation) { RecalculateChildren(); childVisible.Clear(); } UpdateOrder = order; GUI.AddToUpdateList(this); if (ignoreChildren) { OnAddedToGUIUpdateList?.Invoke(this); return; } int lastVisible = 0; for (int i = 0; i < Content.CountChildren; i++) { var child = Content.GetChild(i); if (!child.Visible) continue; if (!IsChildInsideFrame(child)) { if (lastVisible > 0) break; continue; } lastVisible = i; child.AddToGUIUpdateList(false, order); } if (ScrollBar.Enabled) { ScrollBar.AddToGUIUpdateList(false, order); } OnAddedToGUIUpdateList?.Invoke(this); } public override void ForceLayoutRecalculation() { base.ForceLayoutRecalculation(); Content.ForceLayoutRecalculation(); ScrollBar.ForceLayoutRecalculation(); } public void RecalculateChildren() { foreach (GUIComponent child in Content.Children) { ClampChildMouseRects(child); } RepositionChildren(); childrenNeedsRecalculation = false; } private void ClampChildMouseRects(GUIComponent child) { child.ClampMouseRectToParent = true; //no need to go through grandchildren if the child is a GUIListBox, it handles this by itself if (child is GUIListBox) { return; } foreach (GUIComponent grandChild in child.Children) { ClampChildMouseRects(grandChild); } } protected override void Update(float deltaTime) { if (!Visible) { return; } UpdateChildrenRect(); RepositionChildren(); if (scrollBarNeedsRecalculation) { UpdateScrollBarSize(); } if (FadeElements) { foreach (var (component, _) in childVisible) { float lerp = 0; float y = component.Rect.Y; float contentY = Content.Rect.Y; float height = component.Rect.Height; if (y < Content.Rect.Y) { float distance = (contentY - y) / height; lerp = distance; } float centerY = Content.Rect.Y + Content.Rect.Height / 2.0f; if (y > centerY) { float distance = (y - centerY) / (centerY - height); lerp = distance; } component.Color = component.HoverColor = ToolBox.GradientLerp(lerp, component.DefaultColor, Color.Transparent); component.DisabledColor = ToolBox.GradientLerp(lerp, component.Style.DisabledColor, Color.Transparent); component.HoverColor = ToolBox.GradientLerp(lerp, component.Style.HoverColor, Color.Transparent); foreach (var child in component.GetAllChildren()) { Color gradient = ToolBox.GradientLerp(lerp, child.DefaultColor, Color.Transparent); child.Color = child.HoverColor = gradient; if (child is GUITextBlock block) { block.TextColor = block.HoverTextColor = gradient; } } } } if (scrollToElement != null) { if (!scrollToElement.Visible || !Content.Children.Contains(scrollToElement)) { scrollToElement = null; } else { float diff = isHorizontal ? scrollToElement.Rect.X - Content.Rect.X : scrollToElement.Rect.Y - Content.Rect.Y; float speed = MathHelper.Clamp(Math.Abs(diff) * 0.1f, 5.0f, 100.0f); if (Math.Abs(diff) < speed || GUIScrollBar.DraggingBar != null) { speed = Math.Abs(diff); scrollToElement = null; } BarScroll += speed * Math.Sign(diff) / TotalSize; } } bool IsMouseOn() => FindScrollableParentListBox(GUI.MouseOn) == this || GUI.IsMouseOn(ScrollBar) || (CanInteractWhenUnfocusable && Content.Rect.Contains(PlayerInput.MousePosition)); if (PlayerInput.ScrollWheelSpeed != 0 && AllowMouseWheelScroll && IsMouseOn()) { if (SmoothScroll) { if (ClampScrollToElements) { bool scrollDown = Math.Clamp(PlayerInput.ScrollWheelSpeed, 0, 1) > 0; if (scrollDown) { SelectPrevious(takeKeyBoardFocus: TakeKeyBoardFocus.Yes, playSelectSound: PlaySelectSound.Yes); } else { SelectNext(takeKeyBoardFocus: TakeKeyBoardFocus.Yes, playSelectSound: PlaySelectSound.Yes); } } } else { ScrollBar.BarScroll -= (PlayerInput.ScrollWheelSpeed / 500.0f) * ScrollBar.UnclampedBarSize; } } ScrollBar.Enabled = ScrollBarEnabled && BarSize < 1.0f; if (AutoHideScrollBar) { ScrollBarVisible = ScrollBar.Enabled; } if (dimensionsNeedsRecalculation) { UpdateDimensions(); } } private static GUIListBox FindScrollableParentListBox(GUIComponent target) { if (target is GUIListBox listBox && listBox.ScrollBarEnabled && listBox.BarSize < 1.0f) { return listBox; } if (target?.Parent == null) { return null; } return FindScrollableParentListBox(target.Parent); } public void SelectNext(Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled, TakeKeyBoardFocus takeKeyBoardFocus = TakeKeyBoardFocus.No, PlaySelectSound playSelectSound = PlaySelectSound.No) { int index = SelectedIndex + 1; while (index < Content.CountChildren) { GUIComponent child = Content.GetChild(index); if (child.Visible && child.CanBeFocused) { Select(index, force, GetAutoScroll(!SmoothScroll && autoScroll == AutoScroll.Enabled), takeKeyBoardFocus, playSelectSound); if (SmoothScroll) { ScrollToElement(child, playSelectSound); } break; } index++; } } public void SelectPrevious(Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled, TakeKeyBoardFocus takeKeyBoardFocus = TakeKeyBoardFocus.No, PlaySelectSound playSelectSound = PlaySelectSound.No) { int index = SelectedIndex - 1; while (index >= 0) { GUIComponent child = Content.GetChild(index); if (child.Visible && child.CanBeFocused) { Select(index, force, GetAutoScroll(!SmoothScroll && autoScroll == AutoScroll.Enabled), takeKeyBoardFocus, playSelectSound); if (SmoothScroll) { ScrollToElement(child, playSelectSound); } break; } index--; } } public void Select(int childIndex, Force force = Force.No, AutoScroll autoScroll = AutoScroll.Enabled, TakeKeyBoardFocus takeKeyBoardFocus = TakeKeyBoardFocus.No, PlaySelectSound playSelectSound = PlaySelectSound.No) { if (childIndex >= Content.CountChildren || childIndex < 0 || CurrentSelectMode == SelectMode.None) { return; } GUIComponent child = Content.GetChild(childIndex); if (child is null) { return; } if (!child.Enabled) { return; } bool wasSelected = true; if (OnSelected != null) { // TODO: The callback is called twice, fix this! wasSelected = force == Force.Yes || OnSelected(child, child.UserData); } if (!wasSelected) { return; } if (CurrentSelectMode == SelectMode.SelectMultiple || (CurrentSelectMode == SelectMode.RequireShiftToSelectMultiple && PlayerInput.IsCtrlDown())) { if (selected.Contains(child)) { selected.Remove(child); } else { selected.Add(child); } } else if (CurrentSelectMode == SelectMode.RequireShiftToSelectMultiple && PlayerInput.IsShiftDown()) { var first = SelectedComponent ?? child; var last = child; int firstIndex = Content.GetChildIndex(first); int lastIndex = Content.GetChildIndex(last); int sgn = Math.Sign(lastIndex - firstIndex); selected.Clear(); selected.Add(first); for (int i = firstIndex + sgn; i != lastIndex; i += sgn) { if (Content.GetChild(i) is { Visible: true } interChild) { selected.Add(interChild); } } if (first != last) { selected.Add(last); } } else { selected.Clear(); selected.Add(child); } // Ensure that the selected element is visible. This may not be the case, if the selection is run from code. (e.g. if we have two list boxes that are synced) // TODO: This method only works when moving one item up/down (e.g. when using the up and down arrows) if (autoScroll == AutoScroll.Enabled) { if (ScrollBar.IsHorizontal) { if (child.Rect.X < MouseRect.X) { //child outside the left edge of the frame -> move left ScrollBar.BarScroll -= (float)(MouseRect.X - child.Rect.X) / (totalSize - Content.Rect.Width); } else if (child.Rect.Right > MouseRect.Right) { //child outside the right edge of the frame -> move right ScrollBar.BarScroll += (float)(child.Rect.Right - MouseRect.Right) / (totalSize - Content.Rect.Width); } } else { if (child.Rect.Y < MouseRect.Y) { //child above the top of the frame -> move up ScrollBar.BarScroll -= (float)(MouseRect.Y - child.Rect.Y) / (totalSize - Content.Rect.Height); } else if (child.Rect.Bottom > MouseRect.Bottom) { //child below the bottom of the frame -> move down ScrollBar.BarScroll += (float)(child.Rect.Bottom - MouseRect.Bottom) / (totalSize - Content.Rect.Height); } } } // If one of the children is the subscriber, we don't want to register, because it will unregister the child. if (takeKeyBoardFocus == TakeKeyBoardFocus.Yes && CanTakeKeyBoardFocus && RectTransform.GetAllChildren().None(rt => rt.GUIComponent == GUI.KeyboardDispatcher.Subscriber)) { Selected = true; GUI.KeyboardDispatcher.Subscriber = this; } // List box child components can be parents to other components that can play sounds when selected (e.g. store elements) // so the list box shouldn't play the Select sound if the GUI.MouseOn component has a sound to play if (playSelectSound == PlaySelectSound.Yes && PlaySoundOnSelect && !child.PlaySoundOnSelect && (GUI.MouseOn == null || GUI.MouseOn.Parent == Content || !GUI.MouseOn.PlaySoundOnSelect)) { SoundPlayer.PlayUISound(GUISoundType.Select); } AfterSelected?.Invoke(child, SelectedData); } public void Select(IEnumerable children) { if (CurrentSelectMode == SelectMode.None) { return; } Selected = true; selected.Clear(); selected.AddRange(children.Where(c => Content.Children.Contains(c))); foreach (var child in selected) { OnSelected?.Invoke(child, child.UserData); } AfterSelected?.Invoke(children.FirstOrDefault(), SelectedData); } public void Deselect() { Selected = false; if (GUI.KeyboardDispatcher.Subscriber == this) { GUI.KeyboardDispatcher.Subscriber = null; } selected.Clear(); } public void DeselectElement(GUIComponent child) { if (child == null) { return; } if (selected.Contains(child)) { selected.Remove(child); } } public void UpdateScrollBarSize() { scrollBarNeedsRecalculation = false; if (Content == null) { return; } totalSize = 0; var children = Content.Children.Where(c => c.Visible); if (useGridLayout) { int pos = 0; foreach (GUIComponent child in children) { if (ScrollBar.IsHorizontal) { if (pos + child.Rect.Height + Spacing > Content.Rect.Height) { pos = 0; totalSize += child.Rect.Width + Spacing; } pos += child.Rect.Height + Spacing; if (child == children.Last()) { totalSize += child.Rect.Width + Spacing; } } else { if (pos + child.Rect.Width + Spacing > Content.Rect.Width) { pos = 0; totalSize += child.Rect.Height + Spacing; } pos += child.Rect.Width + Spacing; if (child == children.Last()) { totalSize += child.Rect.Height + Spacing; } } } } else { foreach (GUIComponent child in children) { totalSize += (ScrollBar.IsHorizontal) ? child.Rect.Width : child.Rect.Height; } totalSize += Content.CountChildren * Spacing; if (PadBottom) { GUIComponent last = Content.Children.LastOrDefault(); if (last != null) { totalSize += Rect.Height - last.Rect.Height; } } } float minScrollBarSize = 20.0f; ScrollBar.UnclampedBarSize = ScrollBar.IsHorizontal ? Math.Min(Content.Rect.Width / (float)totalSize, 1.0f) : Math.Min(Content.Rect.Height / (float)totalSize, 1.0f); ScrollBar.BarSize = ScrollBar.IsHorizontal ? Math.Max(ScrollBar.UnclampedBarSize, minScrollBarSize / Content.Rect.Width) : Math.Max(ScrollBar.UnclampedBarSize, minScrollBarSize / Content.Rect.Height); } public override void ClearChildren() { Content.ClearChildren(); selected.Clear(); } public override void RemoveChild(GUIComponent child) { if (child == null) { return; } child.RectTransform.Parent = null; if (selected.Contains(child)) { selected.Remove(child); } if (draggedElement == child) { DraggedElement = null; } UpdateScrollBarSize(); } public override void DrawChildren(SpriteBatch spriteBatch, bool recursive) { //do nothing (the children have to be drawn in the Draw method after the ScissorRectangle has been set) return; } protected override void Draw(SpriteBatch spriteBatch) { if (!Visible) { return; } ContentBackground.DrawManually(spriteBatch, alsoChildren: false); Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; if (HideChildrenOutsideFrame && Content.CountChildren > 0) { spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, Content.Rect); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } int lastVisible = 0; int i = 0; foreach (GUIComponent child in Content.Children) { if (!child.Visible) { continue; } if (child == draggedElement && CurrentDragMode == DragMode.DragOutsideBox) { continue; } if (!IsChildInsideFrame(child)) { if (lastVisible > 0) { break; } continue; } lastVisible = i; child.DrawManually(spriteBatch, alsoChildren: true, recursive: true); i++; } if (isDraggingElement && CurrentDragMode == DragMode.DragOutsideBox && HideDraggedElement) { Rectangle drawRect = DraggedElement.Rect; int draggedElementIndex = Content.GetChildIndex(DraggedElement); CalculateChildrenOffsets((index, point) => { if (draggedElementIndex == index) { drawRect.Location = Content.Rect.Location + point; } }); GUI.DrawRectangle(spriteBatch, drawRect, Color.White * 0.5f, thickness: 2f); } if (HideChildrenOutsideFrame && Content.CountChildren > 0) { spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } if (isDraggingElement && CurrentDragMode == DragMode.DragOutsideBox && !HideDraggedElement) { draggedElement.DrawManually(spriteBatch, alsoChildren: true, recursive: true); } if (ScrollBarVisible) { ScrollBar.DrawManually(spriteBatch, alsoChildren: true, recursive: true); } } private bool IsChildInsideFrame(GUIComponent child) { if (child == null) { return false; } if (ScrollBar.IsHorizontal) { if (child.Rect.Right < Content.Rect.X) { return false; } if (child.Rect.X > Content.Rect.Right) { return false; } } else { if (child.Rect.Bottom < Content.Rect.Y) { return false; } if (child.Rect.Y > Content.Rect.Bottom) { return false; } } return true; } public void ReceiveTextInput(char inputChar) { GUI.KeyboardDispatcher.Subscriber = null; } public void ReceiveTextInput(string text) { } public void ReceiveCommandInput(char command) { } public void ReceiveEditingInput(string text, int start, int length) { } public void ReceiveSpecialInput(Keys key) { switch (key) { case Keys.Down: if (!isHorizontal && AllowArrowKeyScroll) { SelectNext(playSelectSound: PlaySelectSound.Yes); } break; case Keys.Up: if (!isHorizontal && AllowArrowKeyScroll) { SelectPrevious(playSelectSound: PlaySelectSound.Yes); } break; case Keys.Left: if (isHorizontal && AllowArrowKeyScroll) { SelectPrevious(playSelectSound: PlaySelectSound.Yes); } break; case Keys.Right: if (isHorizontal && AllowArrowKeyScroll) { SelectNext(playSelectSound: PlaySelectSound.Yes); } break; default: GUI.KeyboardDispatcher.Subscriber = null; break; } } } }