// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; namespace FlaxEngine.GUI { /// /// Dropdown menu control allows to choose one item from the provided collection of options. /// /// public class Dropdown : ContainerControl { /// /// The root control used by the to show the items collections and track item selecting event. /// /// [HideInEditor] protected class DropdownRoot : Panel { private bool _isMouseDown; /// /// Occurs when item gets clicked. Argument is item index. /// public Action ItemClicked; /// /// Occurs when popup lost focus. /// public Action LostFocus; /// /// The items container control. /// public ContainerControl ItemsContainer; /// public override bool OnMouseDown(Vector2 location, MouseButton button) { _isMouseDown = true; var result = base.OnMouseDown(location, button); _isMouseDown = false; if (!result) return false; var itemIndex = ItemsContainer?.GetChildIndexAt(location) ?? -1; if (itemIndex != -1) ItemClicked(itemIndex); return true; } /// public override void OnLostFocus() { base.OnLostFocus(); if (!_isMouseDown) { LostFocus?.Invoke(); } } /// public override void OnDestroy() { ItemClicked = null; LostFocus = null; ItemsContainer = null; base.OnDestroy(); } } /// /// The items. /// protected List _items = new List(); /// /// The popup menu. May be null if has not been used yet. /// protected DropdownRoot _popup; private bool _touchDown; /// /// The selected index of the item (-1 for no selection). /// protected int _selectedIndex = -1; /// /// Gets or sets the items collection. /// [EditorOrder(1), Tooltip("The items collection.")] public List Items { get => _items; set => _items = value; } /// /// Gets or sets the selected item (returns if no item is being selected). /// [HideInEditor, NoSerialize] public string SelectedItem { get => _selectedIndex != -1 ? _items[_selectedIndex] : string.Empty; set => SelectedIndex = _items.IndexOf(value); } /// /// Gets or sets the index of the selected. /// [EditorOrder(2), Limit(-1), Tooltip("The index of the selected item from the list.")] public int SelectedIndex { get => _selectedIndex; set { value = Mathf.Min(value, _items.Count - 1); if (value != _selectedIndex) { _selectedIndex = value; OnSelectedIndexChanged(); } } } /// /// Event fired when selected index gets changed. /// public event Action SelectedIndexChanged; /// /// Gets a value indicating whether this popup menu is opened. /// public bool IsPopupOpened => _popup != null; /// /// Gets or sets the font used to draw text. /// [EditorDisplay("Style"), EditorOrder(2000)] public FontReference Font { get; set; } /// /// Gets or sets the custom material used to render the text. It must has domain set to GUI and have a public texture parameter named Font used to sample font atlas texture with font characters data. /// [EditorDisplay("Style"), EditorOrder(2000), Tooltip("Custom material used to render the text. It must has domain set to GUI and have a public texture parameter named Font used to sample font atlas texture with font characters data.")] public MaterialBase FontMaterial { get; set; } /// /// Gets or sets the color of the text. /// [EditorDisplay("Style"), EditorOrder(2000)] public Color TextColor { get; set; } /// /// Gets or sets the color of the border. /// [EditorDisplay("Style"), EditorOrder(2000)] public Color BorderColor { get; set; } /// /// Gets or sets the background color when dropdown popup is opened. /// [EditorDisplay("Style"), EditorOrder(2010)] public Color BackgroundColorSelected { get; set; } /// /// Gets or sets the border color when dropdown popup is opened. /// [EditorDisplay("Style"), EditorOrder(2020)] public Color BorderColorSelected { get; set; } /// /// Gets or sets the background color when dropdown is highlighted. /// [EditorDisplay("Style"), EditorOrder(2000)] public Color BackgroundColorHighlighted { get; set; } /// /// Gets or sets the border color when dropdown is highlighted. /// [EditorDisplay("Style"), EditorOrder(2000)] public Color BorderColorHighlighted { get; set; } /// /// Gets or sets the image used to render dropdown drop arrow icon. /// [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The image used to render dropdown drop arrow icon.")] public IBrush ArrowImage { get; set; } /// /// Gets or sets the color used to render dropdown drop arrow icon. /// [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color used to render dropdown drop arrow icon.")] public Color ArrowColor { get; set; } /// /// Gets or sets the color used to render dropdown drop arrow icon (menu is opened). /// [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color used to render dropdown drop arrow icon (menu is opened).")] public Color ArrowColorSelected { get; set; } /// /// Gets or sets the color used to render dropdown drop arrow icon (menu is highlighted). /// [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color used to render dropdown drop arrow icon (menu is highlighted).")] public Color ArrowColorHighlighted { get; set; } /// /// Gets or sets the image used to render dropdown checked item icon. /// [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The image used to render dropdown checked item icon.")] public IBrush CheckedImage { get; set; } /// /// Initializes a new instance of the class. /// public Dropdown() : base(0, 0, 120, 18.0f) { AutoFocus = false; var style = Style.Current; Font = new FontReference(style.FontMedium); TextColor = style.Foreground; BackgroundColor = style.BackgroundNormal; BackgroundColorHighlighted = BackgroundColor; BackgroundColorSelected = BackgroundColor; BorderColor = style.BorderNormal; BorderColorHighlighted = style.BorderSelected; BorderColorSelected = BorderColorHighlighted; ArrowImage = new SpriteBrush(style.ArrowDown); ArrowColor = style.Foreground * 0.6f; ArrowColorSelected = style.BackgroundSelected; ArrowColorHighlighted = style.Foreground; CheckedImage = new SpriteBrush(style.CheckBoxTick); } /// /// Clears the items. /// public void ClearItems() { SelectedIndex = -1; _items.Clear(); } /// /// Adds the item. /// /// The item. public void AddItem(string item) { _items.Add(item); } /// /// Adds the items. /// /// The items. public void AddItems(IEnumerable items) { _items.AddRange(items); } /// /// Sets the items. /// /// The items. public void SetItems(IEnumerable items) { SelectedIndex = -1; _items.Clear(); _items.AddRange(items); } /// /// Called when selected item index gets changed. /// protected virtual void OnSelectedIndexChanged() { SelectedIndexChanged?.Invoke(this); } /// /// Called when item is clicked. /// /// The index. protected virtual void OnItemClicked(int index) { SelectedIndex = index; } /// /// Creates the popup menu (including items collection). /// protected virtual DropdownRoot CreatePopup() { // TODO: support using templates for the items collection container panel var popup = new DropdownRoot(); // TODO: support item templates var container = new VerticalPanel { AnchorPreset = AnchorPresets.StretchAll, BackgroundColor = BackgroundColor, AutoSize = false, Parent = popup, }; var border = new Border { BorderColor = BorderColorHighlighted, Width = 4.0f, AnchorPreset = AnchorPresets.StretchAll, Parent = popup, }; var itemsHeight = 20.0f; var itemsMargin = 20.0f; /* var itemsWidth = 40.0f; var font = Font.GetFont(); for (int i = 0; i < _items.Count; i++) { itemsWidth = Mathf.Max(itemsWidth, itemsMargin + 4 + font.MeasureText(_items[i]).X); } */ var itemsWidth = Width; var height = container.Margin.Height; for (int i = 0; i < _items.Count; i++) { var item = new Spacer { Height = itemsHeight, Width = itemsWidth, Parent = container, }; var label = new Label { X = itemsMargin, Size = new Vector2(itemsWidth - itemsMargin, itemsHeight), Font = Font, TextColor = Color.White * 0.9f, TextColorHighlighted = Color.White, HorizontalAlignment = TextAlignment.Near, Text = _items[i], Parent = item, }; height += itemsHeight; if (i != 0) height += container.Spacing; if (_selectedIndex == i) { var icon = new Image { Brush = CheckedImage, Size = new Vector2(itemsMargin, itemsHeight), Margin = new Margin(4.0f, 6.0f, 4.0f, 4.0f), //AnchorPreset = AnchorPresets.VerticalStretchLeft, Parent = item, }; } } popup.Size = new Vector2(itemsWidth, height); popup.ItemsContainer = container; return popup; } /// /// Called when popup menu gets shown. /// protected virtual void OnPopupShow() { } /// /// Called when popup menu gets hidden. /// protected virtual void OnPopupHide() { } /// /// Destroys the popup. /// protected virtual void DestroyPopup() { if (_popup != null) { OnPopupHide(); _popup.Dispose(); _popup = null; } } /// /// Shows the popup. /// public void ShowPopup() { var root = Root; if (_items.Count == 0 || root == null) return; // Setup popup DestroyPopup(); _popup = CreatePopup(); // Update layout _popup.UnlockChildrenRecursive(); _popup.PerformLayout(); // Bind events _popup.ItemClicked += index => { OnItemClicked(index); DestroyPopup(); }; _popup.LostFocus += DestroyPopup; // Show dropdown popup Vector2 locationRootSpace = Location + new Vector2(0, Height); var parent = Parent; while (parent != null && parent != Root) { locationRootSpace = parent.PointToParent(ref locationRootSpace); parent = parent.Parent; } _popup.Location = locationRootSpace; _popup.Parent = root; _popup.Focus(); OnPopupShow(); } /// /// Hides the popup. /// public void HidePopup() { DestroyPopup(); } /// public override void OnDestroy() { DestroyPopup(); base.OnDestroy(); } /// public override void DrawSelf() { // Cache data var clientRect = new Rectangle(Vector2.Zero, Size); float margin = clientRect.Height * 0.2f; float boxSize = clientRect.Height - margin * 2; bool isOpened = IsPopupOpened; bool enabled = EnabledInHierarchy; Color backgroundColor = BackgroundColor; Color borderColor = BorderColor; Color arrowColor = ArrowColor; if (!enabled) { backgroundColor *= 0.5f; arrowColor *= 0.7f; } else if (isOpened || _touchDown) { backgroundColor = BackgroundColorSelected; borderColor = BorderColorSelected; arrowColor = ArrowColorSelected; } else if (IsMouseOver) { backgroundColor = BackgroundColorHighlighted; borderColor = BorderColorHighlighted; arrowColor = ArrowColorHighlighted; } // Background Render2D.FillRectangle(clientRect, backgroundColor); Render2D.DrawRectangle(clientRect, borderColor); // Check if has selected item if (_selectedIndex > -1 && _selectedIndex < _items.Count) { // Draw text of the selected item var textRect = new Rectangle(margin, 0, clientRect.Width - boxSize - 2.0f * margin, clientRect.Height); Render2D.PushClip(textRect); var textColor = TextColor; Render2D.DrawText(Font.GetFont(), FontMaterial, _items[_selectedIndex], textRect, enabled ? textColor : textColor * 0.5f, TextAlignment.Near, TextAlignment.Center); Render2D.PopClip(); } // Arrow ArrowImage?.Draw(new Rectangle(clientRect.Width - margin - boxSize, margin, boxSize, boxSize), arrowColor); } /// public override void OnLostFocus() { _touchDown = false; base.OnLostFocus(); } /// public override void OnMouseLeave() { _touchDown = false; base.OnMouseLeave(); } /// public override bool OnMouseDown(Vector2 location, MouseButton button) { if (base.OnMouseDown(location, button)) return true; if (button == MouseButton.Left) { _touchDown = true; return true; } return false; } /// public override bool OnMouseUp(Vector2 location, MouseButton button) { if (_touchDown && button == MouseButton.Left) { _touchDown = false; ShowPopup(); return true; } return base.OnMouseUp(location, button); } /// public override bool OnTouchDown(Vector2 location, int pointerId) { if (base.OnTouchDown(location, pointerId)) return true; _touchDown = true; return true; } /// public override bool OnTouchUp(Vector2 location, int pointerId) { if (base.OnTouchUp(location, pointerId)) return true; if (_touchDown) { ShowPopup(); } return true; } /// public override void OnTouchLeave(int pointerId) { _touchDown = false; base.OnTouchLeave(pointerId); } } }