Skip to content

File NativeMenu.cs

File List > LemonUI > LemonUI > Menus > NativeMenu.cs

Go to the documentation of this file


#if FIVEM
using CitizenFX.Core;
using CitizenFX.Core.Native;
using CitizenFX.Core.UI;
using Font = CitizenFX.Core.UI.Font;
#elif ALTV
using AltV.Net.Client;
using Font = LemonUI.Elements.Font;
using CancelEventArgs = System.ComponentModel.CancelEventArgs;
using CancelEventHandler = System.ComponentModel.CancelEventHandler;
#elif RAGEMP
using RAGE.Game;
using InstructionalButton = LemonUI.Scaleform.InstructionalButton;
#elif RPH
using Rage;
using Rage.Native;
using System.ComponentModel;
using Control = Rage.GameControl;
using Font = LemonUI.Elements.Font;
#elif SHVDN3 || SHVDNC
using GTA;
using GTA.Native;
using GTA.UI;
using CancelEventArgs = System.ComponentModel.CancelEventArgs;
using CancelEventHandler = System.ComponentModel.CancelEventHandler;
using Font = GTA.UI.Font;
#endif
using LemonUI.Elements;
using LemonUI.Scaleform;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using LemonUI.Tools;

namespace LemonUI.Menus
{
    public class NativeMenu : IContainer<NativeItem>, IEnumerable<NativeItem>
    {
        #region Constants

        internal const float nameHeight = 38;
        internal const float itemHeight = 37.4f;
        internal const float heightDiffDescImg = 4;
        internal const float heightDiffDescTxt = 3;
        internal const float posXDescTxt = 6;
        internal const float itemOffsetX = 6;
        internal const float itemOffsetY = 3;

        #endregion

        #region Fields

        public static readonly Sound DefaultActivatedSound = new Sound("HUD_FRONTEND_DEFAULT_SOUNDSET", "SELECT");
        public static readonly Sound DefaultCloseSound = new Sound("HUD_FRONTEND_DEFAULT_SOUNDSET", "BACK");
        public static readonly Sound DefaultUpDownSound = new Sound("HUD_FRONTEND_DEFAULT_SOUNDSET", "NAV_UP_DOWN");
        public static readonly Sound DefaultLeftRightSound = new Sound("HUD_FRONTEND_DEFAULT_SOUNDSET", "NAV_LEFT_RIGHT");
        public static readonly Sound DefaultDisabledSound = new Sound("HUD_FRONTEND_DEFAULT_SOUNDSET", "ERROR");

        internal static readonly Color colorWhiteSmoke = Color.FromArgb(255, 245, 245, 245);
        internal static readonly SizeF searchAreaSize = new SizeF(30, 1080);
        internal static readonly List<Control> controlsCamera = new List<Control>
        {
#if ALTV
            Control.LookUd,
            Control.LookLr,
#else
            Control.LookUpDown,
            Control.LookLeftRight,
#endif
        };
        internal static readonly List<Control> controlsGamepad = new List<Control>
        {
            Control.Aim,
            Control.Attack
        };

        private static readonly Control[] controls;
        private bool justOpenedControlChecks = false;
        private List<NativeItem> visibleItems = new List<NativeItem>();
        private string name = string.Empty;
        private bool visible = false;
        private bool justClosed = false;
        private bool safeZoneAware = true;
        private int index = -1;
        private float width = 433;
        private Alignment alignment = Alignment.Left;
        private PointF offset = PointF.Empty;
        private I2Dimensional bannerImage = null;
        private readonly ScaledRectangle nameImage = new ScaledRectangle(PointF.Empty, SizeF.Empty)
        {
            Color = Color.FromArgb(255, 0, 0, 0)
        };
        private readonly ScaledText nameText = new ScaledText(PointF.Empty, string.Empty, 0.345f, Font.ChaletLondon)
        {
            Color = colorWhiteSmoke
        };
        private readonly ScaledText countText = new ScaledText(PointF.Empty, string.Empty, 0.345f, Font.ChaletLondon)
        {
            Color = colorWhiteSmoke
        };
        private readonly ScaledTexture backgroundImage = new ScaledTexture("commonmenu", "gradient_bgd");
        private readonly ScaledTexture selectedRect = new ScaledTexture("commonmenu", "gradient_nav");
        private readonly ScaledTexture hoveredRect = new ScaledTexture("commonmenu", "gradient_nav")
        {
            Color = Color.FromArgb(20, 255, 255, 255)
        };
        private readonly ScaledTexture descriptionRect = new ScaledTexture("commonmenu", "gradient_bgd");
        private readonly ScaledText descriptionText = new ScaledText(PointF.Empty, string.Empty, 0.351f);
        private int maxItems = 10;
        private int firstItem = 0;
        private PointF searchAreaRight = PointF.Empty;
        private long heldSince = -1;
        private HeaderBehavior nameBehavior = HeaderBehavior.AlwaysShow;
        private string description = string.Empty;
        private string noItemsText = "There are no items available";

        #endregion

        #region Properties

        private bool ShouldDrawNameBackground => nameBehavior == HeaderBehavior.AlwaysShow || (nameBehavior == HeaderBehavior.ShowIfRequired && (ShouldDrawCount || !string.IsNullOrWhiteSpace(name)));
        private bool ShouldDrawCount => ItemCount == CountVisibility.Always || (ItemCount == CountVisibility.Auto && Items.Count > MaxItems);

        public bool Visible
        {
            get => visible;
            set
            {
                if (visible == value)
                {
                    return;
                }

                if (value)
                {
                    CancelEventArgs args = new CancelEventArgs();
                    Opening?.Invoke(this, args);
                    if (args.Cancel)
                    {
                        return;
                    }

                    if (ResetCursorWhenOpened)
                    {
                        ResetCursor();
                    }

                    justOpenedControlChecks = true;
                    visible = true;

                    SoundOpened?.PlayFrontend();

                    Shown?.Invoke(this, EventArgs.Empty);
                    TriggerSelectedItem();
                }
                else
                {
                    CancelEventArgs args = new CancelEventArgs();
                    Closing?.Invoke(this, args);
                    if (args.Cancel)
                    {
                        return;
                    }

                    visible = false;
                    justClosed = true;
                    Closed?.Invoke(this, EventArgs.Empty);
                    SoundClose?.PlayFrontend();
                }
            }
        }

        public ScaledText BannerText { get; set; }
        [Obsolete("Please use BannerText instead.", true)]
        public ScaledText Title
        {
            get => BannerText;
            set => BannerText = value;
        }
        [Obsolete("Please use BannerText.Font instead.", true)]
        public Font TitleFont
        {
            get => BannerText.Font;
            set => BannerText.Font = value;
        }
        public Font NameFont
        {
            get => nameText.Font;
            set => nameText.Font = value;
        }
        public Font DescriptionFont
        {
            get => descriptionText.Font;
            set => descriptionText.Font = value;
        }
        public Font ItemCountFont
        {
            get => countText.Font;
            set => countText.Font = value;
        }
        public I2Dimensional Banner
        {
            get => bannerImage;
            set
            {
                bannerImage = value;
                Recalculate();
            }
        }
        public PointF Offset
        {
            get => offset;
            set
            {
                offset = value;
                Recalculate();
            }
        }
        public NativeItem SelectedItem
        {
            get
            {
                // If there are no items or is over the maximum, return null
                int currentIndex = SelectedIndex;
                if (Items.Count == 0 || currentIndex >= Items.Count || currentIndex == -1)
                {
                    return null;
                }
                // Otherwise, return the correct item from the list
                return Items[currentIndex];
            }
            set
            {
                // If the item is not part of the menu, raise an exception
                if (!Items.Contains(value))
                {
                    throw new InvalidOperationException("Item is not part of the Menu.");
                }
                // Otherwise, set the correct index
                SelectedIndex = Items.IndexOf(value);
            }
        }
        public int SelectedIndex
        {
            get
            {
                // If there are no items or is over the maximum, return -1
                if (Items.Count == 0 || index >= Items.Count)
                {
                    return -1;
                }
                // Otherwise, return the real index
                return index;
            }
            set
            {
                // If the list of items is empty, don't allow the user to set the index
                if (Items == null || Items.Count == 0)
                {
                    throw new InvalidOperationException("There are no items in this menu.");
                }
                // If the value is over or equal than the number of items, raise an exception
                else if (value >= Items.Count)
                {
                    throw new InvalidOperationException($"The index is over {Items.Count - 1}.");
                }
                // If the value is under zero, raise an exception
                else if (value < 0)
                {
                    throw new InvalidOperationException($"The index is under zero.");
                }

                // Calculate the bounds of the menu
                int lower = firstItem;
                int upper = firstItem + maxItems;

                // Time to set the first item based on the total number of items
                // If the item is between the allowed values, do nothing because we are on the correct first item
                if (value >= lower && value < upper - 1)
                {
                }
                // If the upper bound + 1 equals the new index, increase it by one
                else if (upper == value)
                {
                    firstItem += 1;
                }
                // If the first item minus one equals the value, decrease it by one
                else if (lower - 1 == value)
                {
                    firstItem -= 1;
                }
                // Otherwise, set it somewhere
                else
                {
                    // If the value is under the max items, set it to zero
                    if (value < maxItems)
                    {
                        firstItem = 0;
                    }
                    // Otherwise, set it at the bottom
                    else
                    {
                        firstItem = value - maxItems + 1;
                    }
                }

                // Save the index
                index = value;

                // And update the items
                UpdateItemList();
                UpdateItems();

                // If the menu is visible, play the up and down sound
                if (Visible)
                {
                    SoundUpDown?.PlayFrontend();
                }

                // If an item was selected
                if (SelectedItem != null)
                {
                    // And trigger it
                    TriggerSelectedItem();
                }
            }
        }
        public float Width
        {
            get => width;
            set
            {
                width = value;
                Recalculate();
            }
        }
        public Alignment Alignment
        {
            get => alignment;
            set
            {
                if (alignment == value)
                {
                    return;
                }

                if (!Enum.IsDefined(typeof(Alignment), value) || value == Alignment.Center)
                {
                    throw new ArgumentException("The Menu can only be aligned to the Left and Right.", nameof(value));
                }

                alignment = value;
                Recalculate();
            }
        }
        public string Name
        {
            get => name;
            set
            {
                name = value ?? throw new ArgumentNullException(nameof(value));
                nameText.Text = value.ToUpperInvariant();
            }
        }
        [Obsolete("Please use Name instead.", true)]
        public string Subtitle
        {
            get => Name;
            set => Name = value ?? throw new ArgumentNullException(nameof(value));
        }
        public string Description
        {
            get => description;
            set => description = value ?? throw new ArgumentNullException(nameof(value));
        }
        public bool UseMouse { get; set; } = true;
        public bool CloseOnInvalidClick { get; set; } = true;
        public bool RotateCamera { get; set; } = false;
        public List<NativeItem> Items { get; } = new List<NativeItem>();

        public string NoItemsText
        {
            get => noItemsText;
            set => noItemsText = value ?? throw new ArgumentNullException(nameof(value));
        }
        public bool ResetCursorWhenOpened { get; set; } = true;
        public int MaxItems
        {
            get => maxItems;
            set
            {
                // If the number is under one, raise an exception
                if (value < 1)
                {
                    throw new InvalidOperationException("The maximum numbers on the screen can't be under 1.");
                }
                // Otherwise, save it
                maxItems = value;
            }
        }
        public bool SafeZoneAware
        {
            get => safeZoneAware;
            set
            {
                safeZoneAware = value;
                Recalculate();
            }
        }
        public CountVisibility ItemCount { get; set; }
        public InstructionalButtons Buttons { get; } = new InstructionalButtons(new InstructionalButton("Select", (Control)176 /*PhoneSelect*/), new InstructionalButton("Back", (Control)177 /*PhoneCancel*/))
        {
            Visible = true
        };
        public NativeMenu Parent { get; set; } = null;
        public bool AcceptsInput { get; set; } = true;
        public bool DisableControls { get; set; } = true;
        public int HeldTime { get; set; } = 166;
        public List<Control> RequiredControls { get; } = new List<Control>();
        [Obsolete("Please use NameBehavior instead.", true)]
        public SubtitleBehavior SubtitleBehavior
        {
            get => (SubtitleBehavior)HeaderBehavior;
            set => HeaderBehavior = (HeaderBehavior)value;
        }
        public HeaderBehavior HeaderBehavior
        {
            get => nameBehavior;
            set
            {
                if (nameBehavior == value)
                {
                    return;
                }

                nameBehavior = value;
                Recalculate();
            }
        }
        public Sound SoundOpened { get; set; } = DefaultActivatedSound;
        public Sound SoundActivated { get; set; } = DefaultActivatedSound;
        public Sound SoundClose { get; set; } = DefaultCloseSound;
        public Sound SoundUpDown { get; set; } = DefaultUpDownSound;
        public Sound SoundLeftRight { get; set; } = DefaultLeftRightSound;
        public Sound SoundDisabled { get; set; } = DefaultDisabledSound;

        #endregion

        #region Events

        public event CancelEventHandler Opening;
        public event EventHandler Shown;
        public event CancelEventHandler Closing;
        public event EventHandler Closed;
        public event SelectedEventHandler SelectedIndexChanged;
        public event ItemActivatedEventHandler ItemActivated;
        public event MenuModifiedEventHandler MenuModified;

        #endregion

        #region Constructors

        static NativeMenu()
        {
            // The controls required by the menu with both a gamepad and mouse + keyboard
            HashSet<Control> controlsRequired = new HashSet<Control>
            {
                // Menu Controls
                Control.FrontendAccept,
                Control.FrontendAxisX,
                Control.FrontendAxisY,
                Control.FrontendDown,
                Control.FrontendUp,
                Control.FrontendLeft,
                Control.FrontendRight,
                Control.FrontendCancel,
                Control.FrontendSelect,
                Control.CursorScrollDown,
                Control.CursorScrollUp,
                Control.CursorX,
                Control.CursorY,
#if ALTV
                Control.MoveUd,
                Control.MoveLr,
#else
                Control.MoveUpDown,
                Control.MoveLeftRight,
#endif
                // Camera
                Control.LookBehind,
#if ALTV
                Control.VehLookBehind,
#else
                Control.VehicleLookBehind,
#endif
                // Player
                Control.Sprint,
                Control.Jump,
                Control.Enter,
                Control.SpecialAbility,
                Control.SpecialAbilityPC,
                Control.SpecialAbilitySecondary,
#if ALTV
                Control.VehSpecialAbilityFranklin,
                // Driving
                Control.VehExit,
                Control.VehAccelerate,
                Control.VehBrake,
                Control.VehMoveLr,
                Control.VehHandbrake,
                Control.VehHorn,
                // Bikes
                Control.VehPushbikePedal,
                Control.VehPushbikeSprint,
                Control.VehPushbikeFrontBrake,
                Control.VehPushbikeRearBrake,
                // Flying
                Control.VehFlyThrottleUp,
                Control.VehFlyThrottleDown,
                Control.VehFlyYawLeft,
                Control.VehFlyYawRight,
                Control.VehFlyRollLR,
                Control.VehFlyRollLeftOnly,
                Control.VehFlyRollRightOnly,
                Control.VehFlyPitchUD,
                Control.VehFlyPitchUpOnly,
                Control.VehFlyPitchDownOnly,
#else
                Control.VehicleSpecialAbilityFranklin,
                // Driving
                Control.VehicleExit,
                Control.VehicleAccelerate,
                Control.VehicleBrake,
                Control.VehicleMoveLeftRight,
                Control.VehicleHandbrake,
                Control.VehicleHorn,
                // Bikes
                Control.VehiclePushbikePedal,
                Control.VehiclePushbikeSprint,
                Control.VehiclePushbikeFrontBrake,
                Control.VehiclePushbikeRearBrake,
                // Flying
                Control.VehicleFlyThrottleUp,
                Control.VehicleFlyThrottleDown,
                Control.VehicleFlyYawLeft,
                Control.VehicleFlyYawRight,
                Control.VehicleFlyRollLeftRight,
                Control.VehicleFlyRollLeftOnly,
                Control.VehicleFlyRollRightOnly,
                Control.VehicleFlyPitchUpDown,
                Control.VehicleFlyPitchUpOnly,
                Control.VehicleFlyPitchDownOnly,
#endif
#if RPH
                Control.ScriptedFlyUpDown,
                Control.ScriptedFlyLeftRight,
#elif ALTV
                Control.ScriptedFlyUd,
                Control.ScriptedFlyLr,
#else
                Control.FlyUpDown,
                Control.FlyLeftRight,
#endif
                // Rockstar Editor
                Control.SaveReplayClip,
                Control.ReplayStartStopRecording,
                Control.ReplayStartStopRecordingSecondary,
                Control.ReplayRecord,
                Control.ReplaySave,
            };

            controls = ((Control[])Enum.GetValues(typeof(Control))).Except(controlsRequired).ToArray();
        }

        public NativeMenu(string title) : this(title, string.Empty, string.Empty)
        {
        }

        public NativeMenu(string bqnnerText, string name) : this(bqnnerText, name, string.Empty)
        {
        }

        public NativeMenu(string bqnnerText, string name, string description) : this(bqnnerText, name, description, new ScaledTexture(PointF.Empty, new SizeF(0, 108), "commonmenu", "interaction_bgd"))
        {
        }

        public NativeMenu(string bqnnerText, string name, string description, I2Dimensional banner)
        {
            this.name = name;
            Description = description;
            bannerImage = banner;
            BannerText = new ScaledText(PointF.Empty, bqnnerText, 1.02f, Font.HouseScript)
            {
                Alignment = Alignment.Center
            };
            nameText.Text = name.ToUpperInvariant();
            Recalculate();
        }

        #endregion

        #region Tools

        private void UpdateItemList()
        {
            // Create a new list for the items
            List<NativeItem> list = new List<NativeItem>();

            // Iterate over the number of items while staying under the maximum
            for (int i = 0; i < MaxItems; i++)
            {
                // Calculate the start of our items
                int start = firstItem + i;

                // If the number of items is over the ones in the list, something went wrong
                // TODO: Decide what to do in this case (exception? silently ignore?)
                if (start >= Items.Count)
                {
                    break;
                }

                // Otherwise, return it as part of the iterator or add it to the list
                list.Add(Items[start]);
            }

            // Finally, replace the list of items
            visibleItems = list;
        }
        private void TriggerSelectedItem()
        {
            // Get the currently selected item
            NativeItem item = SelectedItem;

            // If is null or the menu is closed, return
            if (item == null || !Visible)
            {
                return;
            }

            // Update the panel
            RecalculatePanel();
            // And trigger the selected event for this menu
            SelectedEventArgs args = new SelectedEventArgs(index, index - firstItem);
            SelectedItem.OnSelected(this, args);
            SelectedIndexChanged?.Invoke(this, args);
        }
        private void RecalculatePanel()
        {
            if (SelectedItem?.Panel == null)
            {
                return;
            }

            const int separation = 10;

            PointF position = new PointF(descriptionRect.Position.X, descriptionRect.Position.Y + descriptionRect.Size.Height + separation);
            SelectedItem.Panel.Recalculate(position, Width);
        }
        public void ResetCursor()
        {
            const float extraX = 35;
            const float extraY = 325;

            PointF pos = PointF.Empty;
            if (SafeZoneAware)
            {
                SafeZone.SetAlignment(Alignment, GFXAlignment.Top);
                float x = 0;
                switch (Alignment)
                {
                    case Alignment.Left:
                        x = Offset.X + Width + extraX;
                        break;
                    case Alignment.Right:
                        x = Offset.X - Width - extraX;
                        break;
                }
                pos = SafeZone.GetSafePosition(x, Offset.Y + extraY).ToRelative();
                SafeZone.ResetAlignment();
            }
            else
            {
                float x = 0;
                switch (Alignment)
                {
                    case Alignment.Left:
                        x = Offset.X + Width + extraX;
                        break;
                    case Alignment.Right:
                        x = 1f.ToXScaled() - Offset.X - Width - extraX;
                        break;
                }
                pos = new PointF(x, Offset.Y + extraY).ToRelative();
            }
            // And set the position of the cursor
#if FIVEM
            API.SetCursorLocation(pos.X, pos.Y);
#elif RAGEMP
            Invoker.Invoke(Natives.SetCursorLocation, pos.X, pos.Y);
#elif RPH
            NativeFunction.CallByHash<int>(0xFC695459D4D0E219, pos.X, pos.Y);
#elif ALTV
            Alt.Natives.SetCursorPosition(pos.X, pos.Y);
#elif SHVDN3 || SHVDNC
            Function.Call(Hash.SET_CURSOR_POSITION, pos.X, pos.Y);
#endif
        }
        private void UpdateItems()
        {
            // Store the current values of X and Y
            PointF pos;
            if (SafeZoneAware)
            {
                SafeZone.SetAlignment(Alignment, GFXAlignment.Top);
                float x = 0;
                switch (Alignment)
                {
                    case Alignment.Left:
                        x = Offset.X;
                        break;
                    case Alignment.Right:
                        x = Offset.X - Width;
                        break;
                }
                pos = SafeZone.GetSafePosition(x, Offset.Y);
                SafeZone.ResetAlignment();
            }
            else
            {
                float x = 0;
                switch (Alignment)
                {
                    case Alignment.Left:
                        x = Offset.X;
                        break;
                    case Alignment.Right:
                        x = 1f.ToXScaled() - Width - Offset.X;
                        break;
                }
                pos = new PointF(x, Offset.Y);
            }

            // Add the heights of the banner and title (if there are any)
            if (bannerImage != null)
            {
                pos.Y += bannerImage.Size.Height;
            }
            if (ShouldDrawNameBackground || ShouldDrawCount)
            {
                countText.Text = $"{SelectedIndex + 1} / {Items.Count}";
                countText.Position = new PointF(pos.X + width - countText.Width - 6, pos.Y + 4.2f);
                pos.Y += nameImage.Size.Height;
            }

            // Set the position and size of the background image
            backgroundImage.literalPosition = new PointF(pos.X, pos.Y);
            backgroundImage.literalSize = new SizeF(width, itemHeight * visibleItems.Count);
            backgroundImage.Recalculate();
            // Set the position of the rectangle that marks the current item
            selectedRect.Position = new PointF(pos.X, pos.Y + ((index - firstItem) * itemHeight));
            // And then do the description background and text
            float description = pos.Y + ((Items.Count > maxItems ? maxItems : Items.Count) * itemHeight) + heightDiffDescImg;
            descriptionRect.Position = new PointF(pos.X, description);
            descriptionText.Position = new PointF(pos.X + posXDescTxt, description + heightDiffDescTxt);
            UpdateDescription();

            // Save the size of the items
            SizeF size = new SizeF(width, itemHeight);
            // And start recalculating them
            int i = 0;
            foreach (NativeItem item in visibleItems)
            {
                // Tell the item to recalculate the position
                item.Recalculate(new PointF(pos.X, pos.Y), size, item == SelectedItem);
                // And increase the index of the item and Y position
                i++;
                pos.Y += itemHeight;
            }

            // Finally, recalculate the panel of the selected item
            RecalculatePanel();
        }
        private void UpdateDescription()
        {
            descriptionText.Text = Items.Count == 0 || SelectedIndex == -1 ? NoItemsText : SelectedItem.Description;
            int lineCount = descriptionText.LineCount;
            descriptionRect.Size = new SizeF(width, (lineCount * (descriptionText.LineHeight + 5)) + (lineCount - 1) + 10);
        }
        private void ProcessControls()
        {
            // If the user wants to disable the controls, do so but only the ones required
            if (DisableControls)
            {
                bool isUsingController = Controls.IsUsingController;

                foreach (Control control in controls)
                {
                    // If the player is using a controller and is required on gamepads
                    if (isUsingController && controlsGamepad.Contains(control))
                    {
                        continue;
                    }
                    // If the player is usinng a controller or mouse usage is disabled and is a camera control
                    if ((isUsingController || !UseMouse) && controlsCamera.Contains(control))
                    {
                        continue;
                    }
                    // If the control is required by the mod developer
                    if (RequiredControls.Contains(control))
                    {
                        continue;
                    }

                    Controls.DisableThisFrame(control);
                }
            }

            // If the menu is just opened, don't start processing controls until the player has stopped pressing the accept or cancel buttons
            if (justOpenedControlChecks)
            {
                if (Controls.IsPressed((Control)177 /*PhoneCancel*/) || Controls.IsPressed(Control.FrontendPause) ||
                    Controls.IsPressed(Control.FrontendAccept) || Controls.IsPressed((Control)176 /*PhoneSelect*/) ||
                    Controls.IsPressed(Control.CursorAccept))
                {
                    return;
                }
                justOpenedControlChecks = false;
            }

            // If the controls are disabled, the menu has just been opened or the text input field is active, return
#if FIVEM
            bool isKeyboardActive = API.UpdateOnscreenKeyboard() == 0;
#elif ALTV
            bool isKeyboardActive = Alt.Natives.UpdateOnscreenKeyboard() == 0;
#elif RAGEMP
            bool isKeyboardActive = Invoker.Invoke<int>(Natives.UpdateOnscreenKeyboard) == 0;
#elif RPH
            bool isKeyboardActive = NativeFunction.CallByHash<int>(0x0CF2B696BBF945AE) == 0;
#elif SHVDN3 || SHVDNC
            bool isKeyboardActive = Function.Call<int>(Hash.UPDATE_ONSCREEN_KEYBOARD) == 0;
#endif
            if (!AcceptsInput || isKeyboardActive)
            {
                return;
            }

            // Check if the controls necessary were pressed
            bool backPressed = Controls.IsJustPressed((Control)177 /*PhoneCancel*/) || Controls.IsJustPressed(Control.FrontendPause);
            bool upPressed = Controls.IsJustPressed((Control)172 /*PhoneUp*/) || Controls.IsJustPressed(Control.CursorScrollUp);
            bool downPressed = Controls.IsJustPressed((Control)173 /*PhoneDown*/) || Controls.IsJustPressed(Control.CursorScrollDown);
            bool selectPressed = Controls.IsJustPressed(Control.FrontendAccept) || Controls.IsJustPressed((Control)176 /*PhoneSelect*/);
            bool clickSelected = Controls.IsJustPressed(Control.CursorAccept);
            bool leftPressed = Controls.IsJustPressed((Control)174 /*PhoneLeft*/);
            bool rightPressed = Controls.IsJustPressed((Control)175 /*PhoneRight*/);

            bool leftHeld = Controls.IsPressed((Control)174 /*PhoneLeft*/);
            bool rightHeld = Controls.IsPressed((Control)175 /*PhoneRight*/);
            bool upHeld = Controls.IsPressed((Control)172 /*PhoneUp*/) || Controls.IsPressed(Control.CursorScrollUp);
            bool downHeld = Controls.IsPressed((Control)173 /*PhoneDown*/) || Controls.IsPressed(Control.CursorScrollDown);

            // If the player pressed the back button, go back or close the menu
            if (backPressed)
            {
                Back();
                return;
            }

#if ALTV
            long time = Alt.Natives.GetGameTimer();
#elif RAGEMP
            long time = Misc.GetGameTimer();
#elif FIVEM || RPH || SHVDN3 || SHVDNC
            long time = Game.GameTime;
#endif

            if (HeldTime > 0 && (upHeld || downHeld || leftHeld || rightHeld))
            {
                if (heldSince == -1)
                {
                    heldSince = time;
                }
            }
            else
            {
                heldSince = -1;
            }

            // If the player pressed up, go to the previous item
            if ((upPressed && !downPressed) || (upHeld && !downHeld && heldSince > 0 && heldSince + HeldTime < time))
            {
                heldSince = time;
                Previous();
                return;
            }
            // If he pressed down, go to the next item
            if ((downPressed && !upPressed) || (downHeld && !upHeld && heldSince > 0 && heldSince + HeldTime < time))
            {
                heldSince = time;
                Next();
                return;
            }

            // Get the currently selected item for later use (for the sake of performance)
            NativeItem selectedItem = SelectedItem;

            // If the mouse controls are enabled and the user is not using a controller
            if (UseMouse && !Controls.IsUsingController)
            {
                // Enable the mouse cursor
                GameScreen.ShowCursorThisFrame();

                // If the camera should be rotated when the cursor is on the left and right sections of the screen, do so
                if (RotateCamera)
                {
                    if (GameScreen.IsCursorInArea(PointF.Empty, searchAreaSize))
                    {
#if FIVEM || SHVDN3 || SHVDNC
                        GameplayCamera.RelativeHeading += 5;
#elif ALTV
                        float current = Alt.Natives.GetGameplayCamRelativeHeading();
                        Alt.Natives.SetGameplayCamRelativeHeading(current + 5);
#elif RAGEMP
                        float current = Invoker.Invoke<float>(0x743607648ADD4587);
                        Invoker.Invoke(0xB4EC2312F4E5B1F1, current + 5);
#elif RPH
                        Camera.RenderingCamera.Heading += 5;
#endif
                    }
                    else if (GameScreen.IsCursorInArea(searchAreaRight, searchAreaSize))
                    {
#if FIVEM || SHVDN3 || SHVDNC
                        GameplayCamera.RelativeHeading -= 5;
#elif ALTV
                        float current = Alt.Natives.GetGameplayCamRelativeHeading();
                        Alt.Natives.SetGameplayCamRelativeHeading(current - 5);
#elif RAGEMP
                        float current = Invoker.Invoke<float>(0x743607648ADD4587);
                        Invoker.Invoke(0xB4EC2312F4E5B1F1, current - 5);
#elif RPH
                        Camera.RenderingCamera.Heading -= 5;
#endif
                    }
                }

                // If the player pressed the click button
                if (clickSelected)
                {
                    // Iterate over the items on the screen
                    foreach (NativeItem item in visibleItems)
                    {
                        // If the item is selected and slidable
                        if (item == selectedItem && item is NativeSlidableItem slidable)
                        {
                            // If the right arrow was pressed, go to the right
                            if (GameScreen.IsCursorInArea(slidable.RightArrow.Position, slidable.RightArrow.Size))
                            {
                                if (item.Enabled)
                                {
                                    slidable.GoRight();
                                    SoundLeftRight?.PlayFrontend();
                                }
                                else
                                {
                                    SoundDisabled?.PlayFrontend();
                                }
                                return;
                            }
                            // If the user pressed the left arrow, go to the right
                            else if (GameScreen.IsCursorInArea(slidable.LeftArrow.Position, slidable.LeftArrow.Size))
                            {
                                if (item.Enabled)
                                {
                                    slidable.GoLeft();
                                    SoundLeftRight?.PlayFrontend();
                                }
                                else
                                {
                                    SoundDisabled?.PlayFrontend();
                                }
                                return;
                            }
                        }

                        // If the cursor is inside of the selection rectangle
                        if (item.IsHovered)
                        {
                            if (item is NativeSeparatorItem)
                            {
                                return;
                            }

                            // If the item is selected, activate it
                            if (item == selectedItem)
                            {
                                if (item.Enabled)
                                {
                                    ItemActivated?.Invoke(this, new ItemActivatedArgs(selectedItem));
                                    item.OnActivated(this);
                                    SoundActivated?.PlayFrontend();
                                    if (item is NativeCheckboxItem checkboxItem)
                                    {
                                        checkboxItem.UpdateTexture(true);
                                    }
                                }
                                else
                                {
                                    SoundDisabled?.PlayFrontend();
                                }
                            }
                            // If is is not, set it as the selected item
                            else
                            {
                                SelectedItem = item;
                            }

                            // We found the item that was clicked, stop the function
                            return;
                        }
                    }

                    // If we got here, the user clicked outside of the selected item area
                    // So close the menu if required (same behavior of the interaction menu)
                    if (CloseOnInvalidClick)
                    {
                        if (selectedItem.Panel != null && selectedItem.Panel.Clickable && selectedItem.IsHovered)
                        {
                            return;
                        }
                        Visible = false;
                    }
                    return;
                }
            }

            // If the player pressed the left or right button, trigger the event and sound
            if (SelectedItem is NativeSlidableItem slidableItem && !upHeld && !downHeld)
            {
                if ((leftPressed && !rightPressed) || (leftHeld && heldSince > 0 && heldSince + HeldTime < time))
                {
                    heldSince = time;

                    if (SelectedItem.Enabled)
                    {
                        slidableItem.GoLeft();
                        SoundLeftRight?.PlayFrontend();
                    }
                    else
                    {
                        SoundDisabled?.PlayFrontend();
                    }
                    return;
                }
                if ((rightPressed && !leftPressed) || (rightHeld && heldSince > 0 && heldSince + HeldTime < time))
                {
                    heldSince = time;

                    if (SelectedItem.Enabled)
                    {
                        slidableItem.GoRight();
                        SoundLeftRight?.PlayFrontend();
                    }
                    else
                    {
                        SoundDisabled?.PlayFrontend();
                    }
                    return;
                }
            }

            // If the player selected an item, activate it
            if (selectPressed)
            {
                if (SelectedItem != null && SelectedItem.Enabled)
                {
                    ItemActivated?.Invoke(this, new ItemActivatedArgs(selectedItem));
                    SelectedItem.OnActivated(this);
                    SoundActivated?.PlayFrontend();
                    if (SelectedItem is NativeCheckboxItem check)
                    {
                        check.UpdateTexture(true);
                    }
                }
                else
                {
                    SoundDisabled?.PlayFrontend();
                }
            }
        }
        private void Draw()
        {
            NativeItem selected = SelectedItem;

            if (bannerImage != null)
            {
                bannerImage.Draw();
                BannerText?.Draw();
            }

            if (ShouldDrawNameBackground)
            {
                nameImage.Draw();
                nameText?.Draw();
                if (ShouldDrawCount)
                {
                    countText.Draw();
                }
            }

            if (!string.IsNullOrWhiteSpace(descriptionText.Text))
            {
                descriptionRect.Draw();
                descriptionText.Draw();
            }

            if (Items.Count == 0)
            {
                return;
            }

            backgroundImage?.Draw();

            for (int i = 0; i < visibleItems.Count; i++)
            {
                NativeItem item = visibleItems[i];

                if (item == selected)
                {
                    continue;
                }

                if (item.IsHovered && UseMouse && !(item is NativeSeparatorItem))
                {
                    hoveredRect.Position = item.lastPosition;
                    hoveredRect.Size = item.lastSize;
                    hoveredRect.Draw();
                }

                item.Draw();
            }
            // Continue with the white selection rectangle
            if (selected != null && !selected.UseCustomBackground)
            {
                selectedRect.Draw();
            }
            // And finish with the selected item on top (if any)
            if (selected != null)
            {
                selected.Draw();
                if (selected.Panel != null && selected.Panel.Visible)
                {
                    selected.Panel.Process();
                }
            }
        }

        #endregion

        #region Functions

        public IEnumerator<NativeItem> GetEnumerator() => Items.GetEnumerator();
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
        public void Add(NativeItem item) => Add(Items.Count, item);
        public void Add(NativeMenu menu) => AddSubMenu(menu);
        public virtual void Add(int position, NativeItem item)
        {
            if (Items.Contains(item))
            {
                throw new InvalidOperationException("The item is already part of the menu.");
            }
            if (position < 0 || position > Items.Count)
            {
                throw new ArgumentOutOfRangeException(nameof(position), "The index under Zero or is over the Item Count.");
            }
            if (item == null)
            {
                throw new ArgumentNullException(nameof(item));
            }

            Items.Insert(position, item);

            if (Items.Count != 0 && SelectedIndex == -1)
            {
                SelectedIndex = 0;
            }
            else
            {
                UpdateItemList();
            }

            MenuModified?.Invoke(this, new MenuModifiedEventArgs(item, ItemOperation.Added));

            UpdateItems();
        }
        public NativeSubmenuItem AddSubMenu(NativeMenu menu)
        {
            // If the menu is null, raise an exception
            if (menu == null)
            {
                throw new ArgumentNullException(nameof(menu));
            }

            // Create a new menu item, add it and return it
            NativeSubmenuItem item = new NativeSubmenuItem(menu, this);
            Add(item);
            return item;
        }
        public NativeSubmenuItem AddSubMenu(NativeMenu menu, string endlabel)
        {
            NativeSubmenuItem item = AddSubMenu(menu);
            item.AltTitle = endlabel;
            return item;
        }
        public void Remove(NativeItem item)
        {
            if (!Items.Contains(item))
            {
                return;
            }

            Items.Remove(item);

            if (SelectedIndex >= Items.Count)
            {
                SelectedIndex = Items.Count - 1;
            }
            else
            {
                UpdateItemList();
            }

            MenuModified?.Invoke(this, new MenuModifiedEventArgs(item, ItemOperation.Removed));

            UpdateItems();
        }
        public void Remove(Func<NativeItem, bool> pred)
        {
            for (int i = 0; i < Items.Count; i++)
            {
                NativeItem item = Items[i];

                if (!pred(item))
                {
                    continue;
                }

                Items.Remove(item);
                MenuModified?.Invoke(this, new MenuModifiedEventArgs(item, ItemOperation.Added));
            }

            if (SelectedIndex >= Items.Count)
            {
                SelectedIndex = Items.Count - 1;
            }
            else
            {
                UpdateItemList();
            }

            UpdateItems();
        }
        public void Clear()
        {
            List<NativeItem> items = new List<NativeItem>(Items);

            Items.Clear();

            foreach (NativeItem item in items)
            {
                MenuModified?.Invoke(this, new MenuModifiedEventArgs(item, ItemOperation.Added));
            }

            index = 0;
            firstItem = 0;

            UpdateItemList();
            UpdateItems();
        }
        public bool Contains(NativeItem item) => Items.Contains(item);
        public virtual void Process()
        {
            if (!visible)
            {
                if (!justClosed)
                {
                    return;
                }

#if FIVEM
                API.SetInputExclusive(0, (int)Control.PhoneCancel);
                API.SetInputExclusive(0, (int)Control.FrontendPause);
#elif ALTV
                Alt.Natives.SetInputExclusive(0, (int)Control.CellPhoneCancel);
                Alt.Natives.SetInputExclusive(0, (int)Control.FrontendPause);
#elif RAGEMP
                Invoker.Invoke(Natives.SetInputExclusive, 0, (int)Control.PhoneCancel);
                Invoker.Invoke(Natives.SetInputExclusive, 0, (int)Control.FrontendPause);
#elif RPH
                NativeFunction.CallByHash<int>(0x351220255D64C155, 0, (int)Control.CellphoneCancel);
                NativeFunction.CallByHash<int>(0x351220255D64C155, 0, (int)Control.FrontendPause);
#elif SHVDN3 || SHVDNC
                Function.Call(Hash.SET_INPUT_EXCLUSIVE, 0, (int)Control.PhoneCancel);
                Function.Call(Hash.SET_INPUT_EXCLUSIVE, 0, (int)Control.FrontendPause);
#endif

                if (!Controls.IsPressed((Control)177 /*PhoneCancel*/) && !Controls.IsPressed(Control.FrontendPause))
                {
                    justClosed = false;
                }

                return;
            }

            NativeItem selected = SelectedItem;
            if (selected != null && descriptionText.Text != selected.Description)
            {
                UpdateDescription();
            }

            Draw();
            ProcessControls();
            Buttons.Draw();
        }
        public virtual void Recalculate()
        {
            // Store the current values of X and Y
            PointF pos;
            if (SafeZoneAware)
            {
                float x = 0;
                switch (Alignment)
                {
                    case Alignment.Left:
                        x = Offset.X;
                        break;
                    case Alignment.Right:
                        x = Offset.X - Width;
                        break;
                }

                pos = SafeZone.GetPositionAt(new PointF(x, Offset.Y), Alignment, GFXAlignment.Top);
            }
            else
            {
                float x = 0;
                switch (Alignment)
                {
                    case Alignment.Left:
                        x = Offset.X;
                        break;
                    case Alignment.Right:
                        x = 1f.ToXScaled() - Width - Offset.X;
                        break;
                }
                pos = new PointF(x, Offset.Y);
            }

            // If there is a banner and is a valid element
            if (bannerImage != null && bannerImage is BaseElement bannerImageBase)
            {
                // Set the position and size of the banner
                bannerImageBase.literalPosition = new PointF(pos.X, pos.Y);
                bannerImageBase.literalSize = new SizeF(width, bannerImageBase.Size.Height);
                bannerImageBase.Recalculate();
                // If there is a text element, also set the position of it
                if (BannerText != null)
                {
                    BannerText.Position = new PointF(pos.X + 209, pos.Y + 22);
                }
                // Finally, increase the current position of Y based on the banner height
                pos.Y += bannerImageBase.Size.Height;
            }

            // Time for the name background
            // Set the position and size of it
            nameImage.literalPosition = new PointF(pos.X, pos.Y);
            nameImage.literalSize = new SizeF(width, nameHeight);
            nameImage.Recalculate();
            // If there is a text, also set the position of it
            if (nameText != null)
            {
                nameText.Position = new PointF(pos.X + 6, pos.Y + 4.2f);
            }
            // Finally, increase the size based on the name height
            // currentY += nameHeight;

            // Set the size of the selection rectangle
            selectedRect.Size = new SizeF(width, itemHeight);
            // And set the word wrap of the description
            descriptionText.WordWrap = width - posXDescTxt;

            // Set the right size of the rotation
            searchAreaRight = new PointF(1f.ToXScaled() - 30, 0);

            // Then, continue with an item update
            UpdateItems();
        }
        public void Back()
        {
            Visible = false;

            if (Visible)
            {
                return;
            }

            justClosed = true;

            if (Parent != null)
            {
                Parent.Visible = true;
            }
        }
        [Obsolete("Set Visible to true instead.", true)]
        public void Open() => Visible = true;
        [Obsolete("Set Visible to false instead.", true)]
        public void Close() => Visible = false;
        public void Previous()
        {
            if (Items.Count == 0)
            {
                return;
            }

            int nextIndex = SelectedIndex;

            while (true)
            {
                nextIndex -= 1;

                if (nextIndex < 0)
                {
                    nextIndex = Items.Count - 1;
                }

                if (Items[nextIndex] is NativeSeparatorItem)
                {
                    continue;
                }

                if (nextIndex == SelectedIndex)
                {
                    return;
                }

                SelectedIndex = nextIndex;
                return;
            }
        }
        public void Next()
        {
            if (Items.Count == 0)
            {
                return;
            }

            int nextIndex = SelectedIndex;

            while (true)
            {
                nextIndex += 1;

                if (nextIndex >= Items.Count)
                {
                    nextIndex = 0;
                }

                if (Items[nextIndex] is NativeSeparatorItem)
                {
                    continue;
                }

                if (nextIndex == SelectedIndex)
                {
                    return;
                }

                SelectedIndex = nextIndex;
                return;
            }
        }

        #endregion
    }
}