using System.Linq; using Microsoft.Xna.Framework; using MLEM.Maths; using MLEM.Textures; using MLEM.Ui.Style; namespace MLEM.Ui.Elements { /// /// A dropdown component to use inside of a . /// A dropdown is a component that contains a hidden panel which is displayed upon pressing the dropdown button. /// Elements can be added to the dropdown's through or one of its overloads, which additionally causes them to have correct gamepad navigation behavior. /// public class Dropdown : Button { /// /// The panel that this dropdown contains. It will be displayed upon pressing the dropdown button. /// To add elements to this panel, you can use or one of its overloads, which additionally causes them to have correct gamepad navigation behavior. /// public Panel Panel { get; private set; } /// /// The that contains the currently active arrow texture for this dropdown, which is either or . /// public Image Arrow { get; private set; } /// /// This property stores whether the dropdown is currently opened or not /// public bool IsOpen { get => !this.Panel.IsHidden; set { this.Panel.IsHidden = !value; this.UpdateArrowStyle(); this.OnOpenedOrClosed?.Invoke(this); } } /// /// An event that is invoked when changes /// public GenericCallback OnOpenedOrClosed; /// /// A style property containing the that should be passed to the child element. /// public StyleProp ArrowPadding { get => this.arrowPadding; set { this.arrowPadding = value; this.UpdateArrowStyle(); } } /// /// A style property containing the that should be displayed on this dropdown's when the dropdown is closed ( is ). /// public StyleProp ClosedArrowTexture { get => this.closedArrowTexture; set { this.closedArrowTexture = value; this.UpdateArrowStyle(); } } /// /// A style property containing the that should be displayed on this dropdown's when the dropdown is open ( is ). /// public StyleProp OpenedArrowTexture { get => this.openedArrowTexture; set { this.openedArrowTexture = value; this.UpdateArrowStyle(); } } /// /// A style property containing the that a paragraph added to this dropdown through should have by default. /// For elements added through other overloads of , this property has no effect. /// public StyleProp TextColor; /// /// A style property containing the that a paragraph added to this dropdown through should have when hovered. /// For elements added through other overloads of , this property has no effect. /// public StyleProp HoveredTextColor; private StyleProp openedArrowTexture; private StyleProp closedArrowTexture; private StyleProp arrowPadding; /// /// Creates a new dropdown with the given settings and no text or tooltip. /// /// The dropdown's anchor /// The dropdown button's size /// The height of the . If this is 0, the panel will be set to . /// Whether this dropdown's should automatically add a scroll bar to scroll towards elements that are beyond the area it covers. /// Whether this dropdown's 's scroll bar should be hidden automatically if the panel does not contain enough children to allow for scrolling. This only has an effect if is . public Dropdown(Anchor anchor, Vector2 size, float panelHeight = 0, bool scrollPanel = false, bool autoHidePanelScrollbar = true) : base(anchor, size) { this.Initialize(panelHeight, scrollPanel, autoHidePanelScrollbar); } /// /// Creates a new dropdown with the given settings /// /// The dropdown's anchor /// The dropdown button's size /// The text displayed on the dropdown button /// The text displayed as a tooltip when hovering over the dropdown button /// The height of the . If this is 0, the panel will be set to . /// Whether this dropdown's should automatically add a scroll bar to scroll towards elements that are beyond the area it covers. /// Whether this dropdown's 's scroll bar should be hidden automatically if the panel does not contain enough children to allow for scrolling. This only has an effect if is . public Dropdown(Anchor anchor, Vector2 size, string text = null, string tooltipText = null, float panelHeight = 0, bool scrollPanel = false, bool autoHidePanelScrollbar = true) : base(anchor, size, text, tooltipText) { this.Initialize(panelHeight, scrollPanel, autoHidePanelScrollbar); } /// /// Creates a new dropdown with the given settings /// /// The dropdown's anchor /// The dropdown button's size /// The text displayed on the dropdown button /// The text displayed as a tooltip when hovering over the dropdown button /// The height of the . If this is 0, the panel will be set to . /// Whether this dropdown's should automatically add a scroll bar to scroll towards elements that are beyond the area it covers. /// Whether this dropdown's 's scroll bar should be hidden automatically if the panel does not contain enough children to allow for scrolling. This only has an effect if is . public Dropdown(Anchor anchor, Vector2 size, Paragraph.TextCallback textCallback = null, Paragraph.TextCallback tooltipTextCallback = null, float panelHeight = 0, bool scrollPanel = false, bool autoHidePanelScrollbar = true) : base(anchor, size, textCallback, tooltipTextCallback) { this.Initialize(panelHeight, scrollPanel, autoHidePanelScrollbar); } /// /// Adds an element to this dropdown's /// /// The element to add /// The index to add the child at, or -1 to add it to the end of the list public Element AddElement(Element element, int index = -1) { this.Panel.AddChild(element, index); // Since the dropdown causes elements to be over each other, // usual gamepad code doesn't apply element.GetGamepadNextElement = (dir, usualNext) => { if (dir == Direction2.Left || dir == Direction2.Right) return null; if (dir == Direction2.Up) { var prev = element.GetOlderSibling(e => e.CanBeSelected); return prev ?? this; } else if (dir == Direction2.Down) { return element.GetSiblings(e => e.CanBeSelected && e.GetOlderSibling(s => s.CanBeSelected) == element).FirstOrDefault(); } return usualNext; }; return element; } /// /// Adds a pressable element to this dropdown's /// /// The text to display /// The resulting paragraph's event /// The index to add the child at, or -1 to add it to the end of the list public Paragraph AddElement(string text, GenericCallback pressed = null, int index = -1) { return this.AddElement(p => text, pressed, index); } /// /// Adds a pressable element to this dropdown's . /// By default, the paragraph's text color will change from to when hovering over it. /// /// The text to display /// The resulting paragraph's event /// The index to add the child at, or -1 to add it to the end of the list public Paragraph AddElement(Paragraph.TextCallback text, GenericCallback pressed = null, int index = -1) { var paragraph = new Paragraph(Anchor.AutoLeft, 1, text) { CanBeMoused = true, CanBeSelected = true, PositionOffset = new Vector2(0, 1) }; if (pressed != null) paragraph.OnPressed += pressed; paragraph.OnMouseEnter += e => paragraph.TextColor = this.HoveredTextColor; paragraph.OnMouseExit += e => paragraph.TextColor = this.TextColor; this.AddElement(paragraph, index); return paragraph; } private void Initialize(float panelHeight, bool scrollPanel, bool autoHidePanelScrollbar) { this.Panel = this.AddChild(new Panel(Anchor.TopCenter, Vector2.Zero, panelHeight == 0, scrollPanel, autoHidePanelScrollbar) { IsHidden = true }); this.Arrow = this.AddChild(new Image(Anchor.CenterRight, new Vector2(-1, 1), this.ClosedArrowTexture)); this.UpdateArrowStyle(); this.OnAreaUpdated += e => { this.Panel.Size = new Vector2(e.Area.Width / e.Scale, panelHeight); this.Panel.PositionOffset = new Vector2(0, e.Area.Height / e.Scale); }; this.OnOpenedOrClosed += e => this.Priority = this.IsOpen ? 10000 : 0; this.OnPressed += e => { this.IsOpen = !this.IsOpen; // close other dropdowns in the same root when we open if (this.IsOpen) { this.Root.Element.AndChildren(o => { if (o != this && o is Dropdown d && d.IsOpen) d.IsOpen = false; }); } }; this.GetGamepadNextElement = (dir, usualNext) => { // Force navigate down to our first child if we're open if (this.IsOpen && dir == Direction2.Down) return this.Panel.Children.FirstOrDefault(c => c.CanBeSelected) ?? usualNext; return usualNext; }; } /// protected override void InitStyle(UiStyle style) { base.InitStyle(style); this.ArrowPadding = this.ArrowPadding.OrStyle(style.DropdownArrowPadding); this.ClosedArrowTexture = this.ClosedArrowTexture.OrStyle(style.DropdownClosedArrowTexture); this.OpenedArrowTexture = this.OpenedArrowTexture.OrStyle(style.DropdownOpenedArrowTexture); this.TextColor = this.TextColor.OrStyle(style.DropdownTextColor); this.HoveredTextColor = this.HoveredTextColor.OrStyle(style.DropdownHoveredTextColor); this.UpdateArrowStyle(); } private void UpdateArrowStyle() { this.Arrow.Padding = this.Arrow.Padding.OrStyle(this.ArrowPadding, 1); this.Arrow.Texture = this.IsOpen ? this.OpenedArrowTexture : this.ClosedArrowTexture; } } }